UNPKG

san-composition

Version:
527 lines (398 loc) 14.6 kB
# san-composition [![NPM version](http://img.shields.io/npm/v/san-composition.svg?style=flat-square)](https://npmjs.org/package/san-composition) [![License](https://img.shields.io/github/license/baidu/san-composition.svg?style=flat-square)](https://npmjs.org/package/san-composition) [![Coverage Status](https://img.shields.io/coveralls/github/baidu/san-composition.svg?style=flat-square)](https://coveralls.io/github/baidu/san-composition?branch=master) [![Build Status](https://github.com/baidu/san-composition/workflows/CI/badge.svg)](https://github.com/baidu/san-composition/actions) 随着业务的不断发展,前端项目往往变得越来越复杂,过去我们使用 options 定义组件的方式随着功能的迭代可读性可能会越来越差,当我们为组件添加额外功能时,通常需要修改(initData、attached、computed等)多个代码块;而在一些情况下,按功能来组织代码显然更有意义,也更方便细粒度的代码复用。 san-composition 提供一组与定义组件 options 的 key 对应的方法来定义组件的属性和方法,让开发者可以通过逻辑相关性来组织代码,从而提高代码的可读性和可维护性。 - [安装](#安装) - [基础用法](#基础用法) - [进阶篇](#进阶篇) - [API](https://github.com/baidu/san-composition/blob/master/docs/api.md) ## 安装 **NPM** ``` npm i --save san-composition ``` ## 基础用法 在定义一个组件的时候,我们通常需要定义模板、初始化数据、定义方法、添加生命周期钩子等。在使用组合式 API 定义组件时,我们不再使用一个 options 对象声明属性和方法,而是用各个 option 对应的方法来解决组件的各种属性、方法的定义。 > 注意:所有的组合式 API 方法都只能在 defineComponent 方法的第一个函数参数运行过程中执行。 ### 定义模板 使用 [template](https://github.com/baidu/san-composition/blob/master/docs/api.md#template) 方法来定义组件的模板: ```js import san from 'san'; import { defineComponent, template } from 'san-composition'; export default defineComponent(() => { template` <div> <span>count: {{ count }} </span> <button on-click="increment"> +1 </button> </div> `; }, san); ``` ### 定义数据 使用 [data](https://github.com/baidu/san-composition/blob/master/docs/api.md#data) 方法来初始化组件的一个数据项: ```js import san from 'san'; import { defineComponent, template, data } from 'san-composition'; const App = defineComponent(() => { template(/* ... */); const count = data('count', 1); }, san); ``` `data` 的返回值是一个对象,包含getset、merge、splice等方法。我们可以通过对象上的方法对数据进行获取和修改操作。 ### 定义计算数据 使用 [computed](https://github.com/baidu/san-composition/blob/master/docs/api.md#computed) 方法来定义一个计算数据项: ```js const App = defineComponent(() => { template`.....`; const name = data('name', { first: 'Donald', last: 'Trump' }); const fullName = computed('fullName', function() { return name.get('first') + ' ' + name.get('last'); }); }, san); ``` ### 定义过滤器 使用 [filters](https://github.com/baidu/san-composition/blob/master/docs/api.md#filters) 方法来为组件添加过滤器: ```js const App = defineComponent(() => { template`<div> {{ count|triple }} </div>`; const count = data('count', 1); filters('triple', value => value * 3); }, san); ``` ### 定义方法 使用 [method](https://github.com/baidu/san-composition/blob/master/docs/api.md#method) 来定义方法,我们强烈建议按照 `data` 和 `method` 根据业务逻辑就近定义: ```js import san from 'san'; import { defineComponent, template, data, method } from 'san-composition'; const App = defineComponent(() => { template` <div> <span>count: {{ count }} </span> <button on-click="increment"> +1 </button> </div> `; const count = data('count', 1); method('increment', () => count.set(count.get() + 1)); }, san); ``` ### 生命周期钩子 下面的 [onAttached](https://github.com/baidu/san-composition/blob/master/docs/api.md#onAttached) 方法,为组件添加 attached 生命周期钩子: ```js import san from 'san'; import {defineComponent, template, onAttached} from 'san-composition'; const App = defineComponent(() => { template`...`; onAttached(() => { console.log('onAttached'); }); }, san); ``` 生命周期钩子相关的 API 是通过在对应的钩子前面加上 on 命名的,所以它与组件的生命周期钩子一一对应。 | **Option API** | **组合式API中的Hook** | | -------------- | --------------------- | | construct | onConstruct | | compiled | onCompiled | | inited | onInited | | created | onCreated | | attached | onAttached | | detached | onDetached | | disposed | onDisposed | | updated | onUpdated | | error | onError | **一个完整的例子** 下面的例子展示了如何使用 San 组合式 API 定义组件: ```js import san from 'san'; import { defineComponent, template, data, computed, filters, watch, components, method, onCreated, onAttached } from 'san-composition'; export default defineComponent(() => { // 定义模板 template` <div> <span>count: {{ count }} </span> <input type="text" value="{= count =}" /> <div>double: {{ double }} </div> <div>triple: {{ count|triple }} </div> <button on-click="increment"> +1 </button> <my-child></my-child> </div> `; // 初始化数据 const count = data('count', 1); // 添加方法 method('increment', () => count.set(count.get() + 1)); // 监听数据变化 watch('count', newVal => { console.log('count updated: ', newVal); }); // 添加计算数据 computed('double', () => count.get() * 2); // 添加过滤器 filters('triple', n => n * 3); // 定义子组件 components({ 'my-child': defineComponent(() => template('<div>My Child</div>'), san) }); // 生命周期钩子方法 onAttached(() => { console.log('onAttached'); }); onAttached(() => { console.log('another onAttached'); }); onCreated(() => { console.log('onCreated'); }); }, san); ``` ## 进阶篇 组合式 API 使用的要点,在于 **按功能** 定义相应的数据、方法、生命周期运行逻辑等。如果在一个完整的定义函数里流水账般声明一整个组件,为用而用地使用组合式 API 是没有意义的。 本篇以一个简单的例子,通过两小节对组合式 API 的使用给予一些指导: - [实现可组合](#实现可组合) 讲述如何将一个使用 class 声明的组件改造成使用组合式 API 实现 - [实现可复用](#实现可复用) 在上节基础上,讲述如何 **按功能** 拆分成多个函数并实现复用 ### 实现可组合 假设我们要开发一个联系人列表,从业务逻辑上看,该组件具备以下功能: 1. 联系人列表,以及查看、收藏操作; 2. 收藏列表,以及查看、取消收藏操作; 3. 通过表单筛选联系人 首先,我们用 Class API 来实现: ```js class ContactList extends san.Component { static template = /* html */` <div class="container"> <div class="contact-list-filter"> <s-icon type="search"> <s-input on-change="changeKeyword"></s-input> </div> <div class="favorite-list"> <h2>个人收藏</h2> <contact-list data="{{favoriteList|filterList(keyword)}}" on-open="onOpen" on-favorite="onFavorite" /> </div> <div class="contact-list"> <h2>联系人</h2> <contact-list data="{{contactList|filterList(keyword)}}" on-open="onOpen" on-favorite="onFavorite" /> </div> </div> `; initData() { return { // 功能 1 contactList: [] // 功能 2 favoriteList: [], // 功能 3 keyword: '' }; } filters: { filterList(item, keyword) { // ... } }, static components = { 's-icon': Icon, 's-input': Input, 's-button': Button, 'contact-list': ContactListComponent, } attached() { this.getContactList(); this.getFavoriteList(); } // 功能 1 getContactList() { // ... } // 功能 2 getFavoriteList() { // ... } // 功能 1 & 2 async onOpen(item) { // ... } // 功能 1 & 2 async onFavorite(item) { // ... } // 功能 3 changeKeyword(value) { this.data.set('keyword', value); } } ``` 随着组件功能的丰富,Class API 的逻辑会变得越来越发散,我们往往需要在多个模块中跳跃来阅读某个功能的实现,代码的可读性会变差。接下来,我们通过组合式 API 按照功能来组织代码: ```js const ContactList = defineComponent(() => { template`...`; components({ 's-icon': Icon, 's-input': Input, 's-button': Button, 'contact-list': ContactListComponent, }); // 功能 1 & 2 method({ onOpen: item => {/* ... */}, onFavorite: item => {/* ... */} }); filters('filterList', (item, keyword) => { // ... }); // 功能 1 const contactList = data('contactList', []); method('getContactList', () => { // ... contactList.set([/* ... */]); }); onAttached(function () { this.getContactList(); }); // 功能 2 const favoriteList = data('favoriteList', []); method('getFavoriteList', () => { // ... favoriteList.set([/* ... */]); }); onAttached(function () { this.getFavoriteList(); }); // 功能 3 const keyword = data('keyword', ''); method('changeKeyword', value => { keyword.set(value); }); }, san); ``` ### 实现可复用 按照功能来组织的代码有时候逻辑代码块也会比较长,我们可以考虑对组合的逻辑进行一个封装: ```js /** * @file utils.js */ import { ... } from 'san-composition'; // 功能 1 export const useContactList = () => { const contactList = data('contactList', []); method('getContactList', () => { // ... contactList.set([/* ... */]); }); onAttached(function () { this.getContactList(); }); }; // 功能 2 export const useFavoriteList = () => { const favoriteList = data('favoriteList', []); method('getFavoriteList', () => { // ... favoriteList.set([/* ... */]); }); onAttached(function () { this.getFavoriteList(); }); }; // 功能 3 export const useSearchBox = () => { const keyword = data('keyword', ''); method('changeKeyword', value => { keyword.set(value); }); }; // 该 hook 函数提供一个默认名字为 filterList 的过滤器,可以通过参数修改这个过滤器的名称 export const useFilterList = ({filterList = 'filterList'}) => { filters(filterList, (item, keyword) => { // ... }); }; ``` 另外,对于一些常用基础 UI 组件,我们也可以封装一个方法: ```js export const useUIComponents = () => { components({ 's-button': Button, 's-icon': Icon, 's-input': Input }); }; ``` 我们对联系人列表组件再进行一下重构: ```js import { useContactList, useFavoriteList, useSearchBox, useUIComponents, useFilterList } from 'utils.js'; const ContactList = defineComponent(() => { template`...`; useUIComponents(); components({ 'contact-list': ContactListComponent, }); method({ onOpen: item => {/* ... */}, onFavorite: item => {/* ... */} }); useFilterList(); // 功能 1 useContactList(); // 功能 2 useFavoriteList(); // 功能 3 useSearchBox(); }, san); ``` 假设新的需求来了,我们需要一个新的组件,不展示收藏联系人: ```js import { useContactList, useFavoriteList, useSearchBox, useUIComponents } from 'utils.js'; const ContactList = defineComponent(() => { // 模板当然也要做一些调整,这里省略了 template`...`; useUIComponents(); components({ 'contact-list': ContactListComponent, }); method({ onOpen: item => {/* ... */}, onFavorite: item => {/* ... */} }); useFilterList(); useContactList(); useSearchBox(); }, san); ``` ### `context` 的使用 在组合式 API 中我们不推荐使用 `this` ,它会造成一些混淆;但有些时候我们需要获取组件实例的一些属性和方法,这个时候就需要用到 `context` 参数了。 `context` 对象是和组件实例相关的上下文。`context` 对象暴露了组件实例的 `dispatch`、`fire`、`nextTick`、`ref` 等方法,提供了 `context.component` 来访问组件实例,提供了 `context.data` 方法来访问或创建一个 DataProxy。 ```js defineComponent(context => { template`...`; const count = data('count', 1); // 在 method 中,我们不能使用 data 方法,但可以通过 context.data 来操作数据 method('increment', function () { // 访问已经定义了的 DataProxy 实例 const count2 = context.data('count'); // 如果未被定义,则创建一个新的 DataProxy 实例 const kiev = context.data('kiev'); // 定义 'kiev' 的值 为 'need peace' kiev.set('need peace'); // 等价于 context.component.data.set('kiev', 'need peace'); // 这里 count.get() 和 count2.get() 是等价的 context.dispatch('increment:count', count.get()); }); }, san); ``` ## License san-composition is [MIT licensed](./LICENSE).