# functional-mini 使用文档
## 快速开始
我们以最简单的计数器页面为例。
### step 1. 安装依赖
执行 `npm i functional-mini --save`
### step 2. 编写页面的逻辑
使用 Hooks 编写逻辑,然后利用 `alipayPage`, `wechatPage` 生成对应平台的 option 传递给 Page。
```javascript
import {
useState,
useEvent,
alipayPage,
wechatPage,
} from 'functional-mini/page'; // 从 functional-mini/page 引入 hooks
// 编写页面逻辑
const Counter = ({ query }) => {
//通过 props 获取 query
const [count, setCount] = useState(0);
// 绑定视图层的 add 事件
useEvent(
'add',
() => {
setCount(count + 1);
},
[count],
);
// 将这些值提交到视图层
return {
count,
isOdd: count / 2 === 1,
};
};
// 生成配置,并返回给小程序框架的构造函数
Page(alipayPage(Counter)); // 支付宝小程序使用 alipayPage
// 或
Page(wechatPage(Counter)); // 微信小程序使用 wechatPage
```
### step 3. 视图层代码保持不变
视图层代码和各端原生规范一致,没有任何变化。
这里是把 `{counter: number, isOdd: boolean}`渲染到视图层、并绑定 `add`事件的示意代码。
```html
```
至此,一个简单的计数器页面就实现完成了!
### 和普通 React 组件的异同
1. `functional-mini`运行在小程序的逻辑层,它的返回结果是一个 JSON , 等价于 Page 和 Component 的 data。逻辑层不能写 JSX。
2. 使用`useEvent`注册视图层的事件监听
3. 逻辑层没有 DOM,所以无法使用 `useContext`
4. React 组件的生命周期与组件 props 更新、销毁一致。如 onLoad 对应函数组件 mount、onUnload 对应 unmount、props 更新会触发函数重新运行。其他更多事件可以使用 hooks 订阅
5. 使用生命周期、事件相关的 hooks 时,别忘了声明函数中依赖的变量(即 `deps`参数)
```javascript
const Counter = () => {
const [count, setCount] = useState(0);
useOnShow(() => {
console.log(count);
}, [count]); // 不要忘了这里的 count
};
```
## 常见用法
### 注册页面生命周期
下面是页面生命周期与 hooks 对应关系,详细参数可以看 [支付宝小程序](https://opendocs.alipay.com/mini/framework/page-detail) 与 [微信小程序](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html) 文档。
小程序页面生命周期 |
import { hook } from 'functional-mini/page'
|
onLoad onUnload |
useEffect(() => {
// 相当于 onLoad。query 参数在函数 Props 中获取
return () => {
// 相当于 onUnload
};
}, []);
|
onShow |
useOnShow |
onReady |
useOnReady |
onHide |
useOnHide |
### 注册页面事件
下面是页面生命周期与事件处理的 hooks,详细参数可以看 [支付宝小程序](https://opendocs.alipay.com/mini/framework/page-detail) 与 [微信小程序](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html) 文档。
#### 微信小程序
| 微信小程序页面事件 | `import { hook } from 'functional-mini/page'` |
| ------------------ | --------------------------------------------- |
| onPullDownRefresh | useOnPullDownRefresh |
| onReachBottom | useOnReachBottom |
| onShareAppMessage | useOnShareAppMessage |
| onPageScroll | useOnPageScroll |
| onTabItemTap | useOnTabItemTap |
| onResize | useOnResize |
#### 支付宝小程序
| 支付宝小程序页面事件 | `import { hook } from 'functional-mini/page'` |
| -------------------- | --------------------------------------------- |
| onPullDownRefresh | useOnPullDownRefresh |
| onReachBottom | useOnReachBottom |
| onShareAppMessage | useOnShareAppMessage |
| onPageScroll | useOnPageScroll |
| onTabItemTap | useOnTabItemTap |
| onTitleClick | useOnTitleClick |
| onOptionMenuClick | useOnOptionMenuClick |
| beforeTabItemTap | useBeforeTabItemTap |
| onKeyboardHeight | useOnKeyboardHeight |
| onBack | useOnBack |
| onSelectedTabItemTap | useOnSelectedTabItemTap |
| beforeReload | useBeforeReload |
### 注册组件生命周期
下面是小程序自定义组件生命周期和 hooks 对应关系。详细参数可以看 [支付宝小程序](https://opendocs.alipay.com/mini/framework/component-lifecycle?pathHash=9b628e01) 与 [微信小程序](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html) 文档。
#### 微信小程序
微信小程序 |
import { hook } from 'functional-mini/component' |
created detached |
useEffect(() => {
// 相当于 created
return () => {
// 相当于 detached
};
}, []);
|
attached |
useAttached |
ready |
useReady |
moved |
useMoved |
#### 支付宝小程序
支付宝小程序 |
import { hook } from 'functional-mini/component' |
onInit didUnmount |
useEffect(() => {
// 相当于 onInit
return () => {
// 相当于 didUnmount
};
}, []);
|
created detached |
没有对应,可以使用 onInit 与 didUnmount 代替。 |
attached |
useAttached |
didMount |
useDidMount |
ready |
useReady |
deriveDataFromProps |
我们可以在渲染过程中更新 state,以达到实现 deriveDataFromProps 的目的。 |
didUpdate |
useEffect(() => {
// 相当于 didUpdate
}, [deps]);
|
moved |
useMoved |
### 设置页面的初始 data
在组件真正运行前,functional 会在小程序里执行一次预渲染(理解为 server-side-render / SSR) ,收集返回值,这里的数据会作为页面初始化的 data。
预渲染时,所有的 useEffect 、生命周期 hooks 都不会被触发。
```javascript
const Counter = function () {
const [counter, setCounter] = useState(0);
useOnLoad(() => console.log('Load'), []);
useEffect(() => {
setCounter(1); // 不会在预渲染时触发
}, []);
return {
// 会被收集
foo: 'aa',
counter,
};
};
/*
Page({
data: { // <---- 收集到的是这个数据
foo: 'aa',
counter: 0,
}
});
*/
```
### 注册视图层事件
我们可以使用 `useEvent` 这个 hook 来注册事件。
```html
```
下面是在 useCounter 这个自定义的 hooks 注册 `clickButton` 的例子。
```typescript
import { useEvent } from 'functional-mini/page'; // 在小程序页面
import { useEvent } from 'functional-mini/component'; // 在小程序组件里
const useCounter = () => {
const [value, setValue] = useState(0);
useEvent(
'clickButton',
() => {
setValue(value + 1);
},
[value], // 要声明依赖
);
return value;
};
```
### 精细控制 setData 频次
函数式组件返回 JSON 后,`functional-mini`会对每个 key 做浅比较,如果和小程序实例上的数据不一致,就自动触发 setData 完成同步。
如果有场景需要减小 setData 的性能损耗,可以使用 `useMemo` 把不变化的数据固定下来。
这里是一个使用案例:
```diff
import { useMemo } from 'functional-mini/page';
const MyPage = function(props) {
const maxCount = props.query.max;
- // 每次都创建新的 longList 对象,会对最终的 setData 性能有损耗
- const longList = [];
- for(let i = 0; i <= maxCount; i++) {
- longList.push('big content');
- }
+ // 固定依赖项,减少更新次数
+ const longList = useMemo(() => {
+ const longList = [];
+ for(let i = 0; i <= maxCount; i++) {
+ longList.push('big content');
+ }
+ return longList;
+ }, [maxCount])
return {
...data, // 其他数据
longList,
};
}
```
### 组件间通信与事件
我们以受控的 Counter 组件为例,介绍 functional 如何开发一个组件。
```html
```
#### 获取父组件传递的参数
我们可以通过 props 获取父组件的传入的 props。 和 page 不同,我们需要通过 `alipayComponent`, `wechatComponent` 的第二个参数定义 props 的类型。
```javascript
import {
wechatComponent,
alipayComponent,
} from 'functional-mini/component';
function Counter = (props) => {
console.log(props.value) // 通过 props 获取
}
const defaultProps = {
value: 1
}
Component(
alipayComponent(Counter, defaultProps),
);
Component(
wechatComponent(Counter, defaultProps),
);
```
#### 子组件向父组件传递数据
- 在微信端,我们通过 useComponent 获取组件实例 , 然后通过 `component.triggerEvent('eventname', value)`的方式向父组件传递数据。
- 为支付宝端,我们通过 `props.eventname(value)` 向父组件传递数据。
```javascript
import { wechatComponent, useComponent } from 'functional-mini/component';
const Counter = (props) => {
const { triggerEvent } = useComponent();
useEvent(
'onClickCounter',
() => {
triggerEvent('handleChange', props.value + 1);
},
[props.value, triggerEvent],
);
return {};
};
```
```javascript
import { wechatComponent } from 'functional-mini/component';
const Counter = (props) => {
useEvent(
'onClickCounter',
() => {
props.handleChange(props.value + 1);
},
[props.value, props.handleChange],
);
return {};
};
```
#### 获取页面、组件实例
在页面里可以通过 usePage 获取页面实例。 相当于小程序 Page 和 Component 的 `this`。
⚠️ 不要使用页面、组件实例调用 data , setData 可能会发生不可预期的事情。
```typescript
import { usePage } from 'functional-mini/page';
const MyPage = (props) => {
const component = usePage();
return {};
};
```
在组件里可以通过 useComponent 获取组件实例
```typescript
import { useComponent } from 'functional-mini/component';
const MyComponent = (props) => {
const component = useComponent();
return {};
};
```
## FAQ
### 为何无法使用 JSX?
小程序采用的是渲染与逻辑隔离的双线程架构。为了降低项目的复杂度,我们选择正视它的存在,探索适合双线程环境的新技术解决方案,而不是试图向开发者隐藏这些限制。
JSX 语法的一个主要特性就是视图和逻辑的混写,这与我们的设计理念显然是冲突的,因此我们决定在这个项目中剔除 JSX。
当然,我们也不排除在未来重新引入 JSX 的可能性,但前提是它能带来更显著的优势,比如更好的 IDE 支持和 TSX 类型等。然而,即便如此,JSX 仍将受到一些限制,比如视图必须是独立的文件,并不能像在普通的 React 项目中那样与逻辑代码混写。
### 关于 React 运行时环境
`function-mini` 使用了 [preact](https://preactjs.com/) 作为 React 运行时基础。由于运行时的特殊性,我们做了一些环境适配工作(如替换了几个 document 的接口实现),并将适配后的 preact 内置在了库中。
适配过程主要体现在 rollup 的构建插件中,如果感兴趣,你可以在 [这里](https://github.com/ant-design/functional-mini/blob/main/scripts/rollup-plugins/virtual-document.ts)看到细节。
### `functional-mini` 尚不是一个跨端开发的库
`functional-mini`目前分别适配了支付宝和微信端的运行环境,但它尚不能帮你实现“一次开发多端运行”。
主要原因有:
1. 各端视图层编写语法不同
2. 各端生命周期的触发顺序、参数细节、事件方法都有差异
3. 我们没有提供抹平各端 JSAPI 的库
如有跨端需求,你可以尝试自行实现必要的上层封装。
也欢迎大家在 issue 中分享自己的实践方案,共同讨论交流。
## API 列表
### page
page 相关的 API 统一从 `functional-mini/page` 导入。
#### 页面构造函数
- alipayPage(pageHook)
在支付宝小程序中使用,构造传递给 Page 的 option
- wechatPage(pageHook)
在微信小程序中使用,构造传递给 Page 的 option
#### functional hooks
- usePage()
获取页面实例
#### React hooks
下面是 React 内置的hooks, 详细用法可以看 [React](https://react.dev/reference/react) 官方文档。
- useState
- useReducer
- useEffect
- useRef
- useCallback
- useMemo
#### 生命周期与页面事件 hooks
详细参数可以看 [支付宝小程序](https://opendocs.alipay.com/mini/framework/page-detail) 与 [微信小程序](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html) 文档。
- useOnShow
- useOnReady
- useOnHide
- useOnPullDownRefresh
- useOnReachBottom
- useOnShareAppMessage
- useOnPageScroll
- useOnTabItemTap
- useOnResize
- useOnTitleClick
- useOnOptionMenuClick
- useBeforeTabItemTap
- useOnKeyboardHeight
- useOnBack
- useOnSelectedTabItemTap
- useBeforeReload
### Component
component 相关的 API 统一从 `functional-mini/component` 导入。
#### 组件构造函数
- alipayComponent(componentHook, defaultProps)
在支付宝小程序中使用,构造传递给 Component 的 option
- wechatComponent(componentHook, defaultProps)
在微信小程序中使用,构造传递给 Component 的 option
```typescript
const functionOption = wechatComponent(Counter, {
label: 'button', // 我们会根据 defaultProps 的类型生成组件的 properties
});
```
#### functional hooks
- useComponent
获取组件实例
#### React hooks
下面是 React 内置的hooks, 详细用法可以看 [React](https://react.dev/reference/react) 官方文档。
- useState
- useReducer
- useEffect
- useRef
- useCallback
- useMemo
#### 生命周期与页面事件 hooks
详细参数可以看 [支付宝小程序](https://opendocs.alipay.com/mini/framework/component-lifecycle?pathHash=9b628e01) 与 [微信小程序](https://developers.weixin.qq.com/miniprogram/dev/reference/api/Component.html) 文档。
- useAttached
- useDidMount
- useReady
- useMoved