Skip to content

Latest commit

 

History

History
630 lines (479 loc) · 17.5 KB

doc.md

File metadata and controls

630 lines (479 loc) · 17.5 KB

functional-mini 使用文档

快速开始

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

Step 1. 安装依赖

执行 npm i functional-mini --save

Step 2. 编写页面的逻辑

使用 Hooks 编写逻辑,然后利用 alipayPagewechatPage 生成对应平台的 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);
  });

  // 将这些值提交到视图层
  return {
    count,
    // 判断 count 是否为奇数
    isOdd: count % 2 !== 0,
  };
};

// 生成配置,并返回给小程序框架的构造函数
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 订阅。
const Counter = () => {
  const [count, setCount] = useState(0);

  useOnShow(() => {
    console.log(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);
  });
  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>

在自定义组件里监听页面生命周期方法

我们可以使用 usePageShowusePageHide 这两个 hooks 监听页面的 show 与 hide 事件。

import { usePageShow } from 'functional-mini/component';

const useCounter = () => {
  const [value, setValue] = useState(0);
  usePageShow(() => {
    // 页面 show 时会触发
    console.log('page show');
  });
  return value;
};

获取父组件传递的参数

我们可以通过 props 获取父组件传入的 props。与 page 不同,我们需要通过 alipayComponentwechatComponent 的第二个参数定义 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));

将函数传递给子组件

有时,我们需要把函数传递给子组件,用以定制组件的内部功能。比如,在 Antd Mini 的表单组件中,常常需要由外部业务传入 onFormat 方法,用于指定文案的展示形式。

  • 在微信端,我们需要使用 useEvent 注册函数并设置 { handleResult: true }。这样,在子组件的 props 里就可以获取到父组件传递的函数。handleResult 开启后,事件函数会挂载到组件实例的 data 上;因此,useEvent 不能使用在 properties 中已存在的事件名。
  • 在支付宝端,我们使用 useEvent 注册函数即可,无需额外参数。
<!-- 微信小程序 -->
<child onFormat="{{ onFormat }}" />
<!-- 支付宝小程序 -->
<child onFormat="onFormat" />
<!-- 兼容两个平台 -->
<child onFormat="{{ onFormat ? onFormat : 'onFormat' }}" />
import { useEvent } from 'functional-mini/component';

// 父组件
const Counter = (props) => {
  useEvent(
    'onFormat',
    () => {
      return 'value';
    },
    {
      // 由于支付宝小程序原生支持传递函数,
      // 此变量只在微信小程序生效
      handleResult: true,
    },
  );
  return {};
};

// 子组件
const CounterChild = (props) => {
  let formatText;
  if (props.onFormat) {
    formatText = props.onFormat();
  }
  return { formatText };
};

子组件向父组件传递数据

  • 在微信端,我们通过 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);
  });

  return {};
};
import { wechatComponent } from 'functional-mini/component';

const Counter = (props) => {
  useEvent('onClickCounter', () => {
    props.handleChange(props.value + 1);
  });

  return {};
};

获取页面、组件实例

在页面里可以通过 usePage 获取页面实例。相当于小程序 PageComponentthis

不要使用页面、组件实例调用 datasetData,可能会发生不可预期的事情。

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

const MyPage = (props) => {
  const page = 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 运行时环境

functional-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 的选项。

  • wechatPage(pageHook)

    在微信小程序中使用,用于构造传递给 Page 的选项。

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

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

  • usePageShow
  • usePageHide