https://github.com/baidu/san-composition
https://github.com/baidu/san-composition
Last synced: 3 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/baidu/san-composition
- Owner: baidu
- License: mit
- Created: 2021-09-02T08:06:32.000Z (about 4 years ago)
- Default Branch: master
- Last Pushed: 2022-11-21T08:17:25.000Z (almost 3 years ago)
- Last Synced: 2025-04-26T17:47:31.964Z (6 months ago)
- Language: JavaScript
- Size: 258 KB
- Stars: 31
- Watchers: 3
- Forks: 6
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# san-composition
[](https://npmjs.org/package/san-composition)
[](https://npmjs.org/package/san-composition)
[](https://coveralls.io/github/baidu/san-composition?branch=master)
[](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`
count: {{ count }}
+1
`;
}, 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` 的返回值是一个对象,包含get、set、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`{{ count|triple }}`;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`
count: {{ count }}
+1
`;
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`
count: {{ count }}
double: {{ double }}
triple: {{ count|triple }}
+1
`;// 初始化数据
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('My Child'), 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 */`
个人收藏
联系人
`;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).