引言
随着业务的不断发展,前端项目往往变得越来越复杂,过去我们使用 options 定义组件的方式随着功能的迭代可读性可能会越来越差,当我们为组件添加额外功能时,通常需要修改(initData、attached、computed等)多个代码块;而在一些情况下,按功能来组织代码显然更有意义,也更方便细粒度的代码复用。
san-composition 提供一组与定义组件 options 的 key 对应的方法来定义组件的属性和方法,让开发者可以通过逻辑相关性来组织代码,从而提高代码的可读性和可维护性。
安装
NPM
1
| npm i --save san-composition
|
基础用法
在定义一个组件的时候,我们通常需要定义模板、初始化数据、定义方法、添加生命周期钩子等。在使用组合式 API 定义组件时,我们不再使用一个 options 对象声明属性和方法,而是用各个 option 对应的方法来解决组件的各种属性、方法的定义。
注意:所有的组合式 API 方法都只能在 defineComponent 方法的第一个函数参数运行过程中执行。
定义模板
使用 template 方法来定义组件的模板:
1 2 3 4 5 6 7 8 9 10 11 12
| 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 方法来初始化组件的一个数据项:
1 2 3 4 5 6 7
| import san from 'san'; import { defineComponent, template, data } from 'san-composition';
const App = defineComponent(() => { template(); const count = data('count', 1); }, san);
|
data
的返回值是一个对象,包含get、set、merge、splice等方法。我们可以通过对象上的方法对数据进行获取和修改操作。
定义计算数据
使用 computed 方法来定义一个计算数据项:
1 2 3 4 5 6 7 8 9 10 11 12
| 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 方法来为组件添加过滤器:
1 2 3 4 5 6 7
| const App = defineComponent(() => { template('<div> {{ count|triple }} </div>');
const count = data('count', 1); filters('triple', value => value * 3); }, san);
|
定义方法
使用 method 来定义方法,我们强烈建议按照 data
和 method
根据业务逻辑就近定义:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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 方法,为组件添加 attached 生命周期钩子:
1 2 3 4 5 6 7 8 9
| 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 定义组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| 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 实现
- 实现可复用 在上节基础上,讲述如何 按功能 拆分成多个函数并实现复用
实现可组合
假设我们要开发一个联系人列表,从业务逻辑上看,该组件具备以下功能:
- 联系人列表,以及查看、收藏操作;
- 收藏列表,以及查看、取消收藏操作;
- 通过表单筛选联系人
首先,我们用 Class API 来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| class ContactList extends san.Component { static template = ` <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 { contactList: [] favoriteList: [], keyword: '' }; }
filters: { filterList(item, keyword) { } },
static components = { 's-icon': Icon, 's-input': Input, 's-button': Button, 'contact-list': ContactListComponent, }
attached() { this.getContactList(); this.getFavoriteList(); }
getContactList() { }
getFavoriteList() { }
async onOpen(item) { }
async onFavorite(item) { }
changeKeyword(value) { this.data.set('keyword', value); } }
|
随着组件功能的丰富,Class API 的逻辑会变得越来越发散,我们往往需要在多个模块中跳跃来阅读某个功能的实现,代码的可读性会变差。接下来,我们通过组合式 API 按照功能来组织代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| const ContactList = defineComponent(() => { template('/* ... */'); components({ 's-icon': Icon, 's-input': Input, 's-button': Button, 'contact-list': ContactListComponent, });
method({ onOpen: item => {}, onFavorite: item => {} });
filters('filterList', (item, keyword) => { });
const contactList = data('contactList', []); method('getContactList', () => { contactList.set([]); }); onAttached(function () { this.getContactList(); });
const favoriteList = data('favoriteList', []); method('getFavoriteList', () => { favoriteList.set([]); }); onAttached(function () { this.getFavoriteList(); });
const keyword = data('keyword', ''); method('changeKeyword', value => { keyword.set(value); }); }, san);
|
实现可复用
按照功能来组织的代码有时候逻辑代码块也会比较长,我们可以考虑对组合的逻辑进行一个封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
import { ... } from 'san-composition';
export const useContactList = () => { const contactList = data('contactList', []); method('getContactList', () => { contactList.set([]); }); onAttached(function () { this.getContactList(); }); };
export const useFavoriteList = () => { const favoriteList = data('favoriteList', []); method('getFavoriteList', () => { favoriteList.set([]); }); onAttached(function () { this.getFavoriteList(); }); };
export const useSearchBox = () => { const keyword = data('keyword', ''); method('changeKeyword', value => { keyword.set(value); }); };
export const useFilterList = ({filterList = 'filterList'}) => { filters(filterList, (item, keyword) => { }); };
|
另外,对于一些常用基础 UI 组件,我们也可以封装一个方法:
1 2 3 4 5 6 7
| export const useUIComponents = () => { components({ 's-button': Button, 's-icon': Icon, 's-input': Input }); };
|
我们对联系人列表组件再进行一下重构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { useContactList, useFavoriteList, useSearchBox, useUIComponents, useFilterList } from 'utils.js'; const ContactList = defineComponent(() => { template('/* ... */');
useUIComponents();
components({ 'contact-list': ContactListComponent, });
method({ onOpen: item => {}, onFavorite: item => {} });
useFilterList();
useContactList();
useFavoriteList();
useSearchBox(); }, san);
|
假设新的需求来了,我们需要一个新的组件,不展示收藏联系人:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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);
|
this 的使用
在组合式 API 中我们不推荐使用 this
,它会造成一些混淆,但有时候可能不得不使用,这时候注意不要在对应的方法中使用箭头函数。
1 2 3 4 5 6 7 8 9 10
| defineComponent(() => { template(); const count = data('count', 1);
method('increment', function () { this.dispatch('increment:count', count.get()); }); }, san);
|