doc.md 16 KB

functional-mini 使用文档

快速开始

我们以最简单的计数器页面为例。

step 1. 安装依赖

执行 npm i functional-mini --save

step 2. 编写页面的逻辑

使用 Hooks 编写逻辑,然后利用 alipayPage, wechatPage 生成对应平台的 option 传递给 Page。

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事件的示意代码。

<!-- 支付宝 -->
<button onTap="add">
  <text>{{count}}</text>
  <text>isOdd: {{isOdd}}</text>
</button>

<!-- 微信 -->
<button bind:tap="add">
  <text>{{count}}</text>
  <text>isOdd: {{isOdd}}</text>
</button>

至此,一个简单的计数器页面就实现完成了!

和普通 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参数)

    const Counter = () => {
    const [count, setCount] = useState(0);
    
    useOnShow(() => {
    console.log(count);
    }, [count]); // 不要忘了这里的 count
    };
    

常见用法

注册页面生命周期

下面是页面生命周期与 hooks 对应关系,详细参数可以看 支付宝小程序微信小程序 文档。

小程序页面生命周期 import { hook } from 'functional-mini/page'
onLoad
onUnload
useEffect(() => {
  // 相当于 onLoad。query 参数在函数 Props 中获取
  return () => {
    // 相当于 onUnload
  };
}, []);
onShow useOnShow
onReady useOnReady
onHide useOnHide

注册页面事件

下面是页面生命周期与事件处理的 hooks,详细参数可以看 支付宝小程序微信小程序 文档。

微信小程序

微信小程序页面事件 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 对应关系。详细参数可以看 支付宝小程序微信小程序 文档。

微信小程序

微信小程序 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 都不会被触发。

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 来注册事件。

<!-- 支付宝注册点击事件 -->
<button onTap="clickButton">Click</button>

<!-- 微信注册点击事件 -->
<button bind:tap="clickButton">Click</button>

下面是在 useCounter 这个自定义的 hooks 注册 clickButton 的例子。

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 把不变化的数据固定下来。
这里是一个使用案例:

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 如何开发一个组件。

<!-- 下面是父组件调用 counter 的代码 -->

<!-- 微信 -->
<counter value="{{counterValue}}" bind:onChange="handleChange" />

<!-- 支付宝 -->
<counter value="{{counterValue}}" onChange="handleChange" />

<!-- 下面是 counter 视图层的实现 -->

<!-- 微信 -->
<button class="counter" bind:tap="onClickCounter">{{value}}</button>

<!-- 支付宝 -->
<button class="counter" onTap="onClickCounter">{{value}}</button>

获取父组件传递的参数

我们可以通过 props 获取父组件的传入的 props。 和 page 不同,我们需要通过 alipayComponent, wechatComponent 的第二个参数定义 props 的类型。

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) 向父组件传递数据。

    import { wechatComponent, useComponent } from 'functional-mini/component';
    
    const Counter = (props) => {
    const { triggerEvent } = useComponent();
    
    useEvent(
    'onClickCounter',
    () => {
      triggerEvent('handleChange', props.value + 1);
    },
    [props.value, triggerEvent],
    );
    
    return {};
    };
    
    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 可能会发生不可预期的事情。

import { usePage } from 'functional-mini/page';

const MyPage = (props) => {
  const component = usePage();

  return {};
};

在组件里可以通过 useComponent 获取组件实例

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 作为 React 运行时基础。由于运行时的特殊性,我们做了一些环境适配工作(如替换了几个 document 的接口实现),并将适配后的 preact 内置在了库中。
适配过程主要体现在 rollup 的构建插件中,如果感兴趣,你可以在 这里看到细节。

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 官方文档。

  • useState
  • useReducer
  • useEffect
  • useRef
  • useCallback
  • useMemo

生命周期与页面事件 hooks

详细参数可以看 支付宝小程序微信小程序 文档。

  • 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

  const functionOption = wechatComponent(Counter, {
    label: 'button', // 我们会根据 defaultProps 的类型生成组件的 properties
  });

functional hooks

  • useComponent

获取组件实例

React hooks

下面是 React 内置的hooks, 详细用法可以看 React 官方文档。

  • useState
  • useReducer
  • useEffect
  • useRef
  • useCallback
  • useMemo

生命周期与页面事件 hooks

详细参数可以看 支付宝小程序微信小程序 文档。

  • useAttached
  • useDidMount
  • useReady
  • useMoved