npm package: @roundjs/fission-utils
Description: ## 说明
Author: 13
Latest version: 1.0.14-alpha.1

# @roundjs/composition-ali

## 说明

`@roundjs/composition-ali` 是基于 `reactivity` api 的小程序开发库，它能让你用 `Composition API` 写小程序，就像写 Vue 3 一样。

- 微信小程序：[@roundjs/composition-wx](https://www.npmjs.com/package/@roundjs/composition-wx)
  - 支持：微信，qq，百度，字节，京东，快手，飞书
- 支付宝小程序：[@roundjs/composition-ali](https://www.npmjs.com/package/@roundjs/composition-ali)
  - 支持：支付宝，钉钉，口碑
  
#### 功能导航：

- [Page createPage](#page-createPage)
- [Component createComponent](#component-createComponent)
- [App createApp](#app-createApp)
- [响应式 APi](#watch-effect)
- [依赖注入 inject/provide](#inject-provide)
- [AOP](#AOP)
- [性能优化](#optimizePath)

## 使用

```bash
npm i @roundjs/composition-ali
# or
yarn add @roundjs/composition-ali
```

(ps：1.1.xx之后的版本使用方式 由`Page(createPage())` 改为更简洁的直接 `createPage()`)

## <span id="page-createPage">Page</span>

```javascript
import { createPage, reactive, computed } from '@roundjs/composition-ali';

createPage({
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2),
    });

    function increment() {
      state.count++;
    }

    return {
      state,
      increment,
    };
  },
});
```

在使用上为了不影响原小程序语法，先使用 `createPage` 包装一层：

`Page(obj)` -> `createPage(obj)`

如果 `setup` 返回一个对象，则对象的属性将会被合并到页面实例上，可以直接在页面模版中使用。

```html
<button onTap="increment">
  Count is: {{ state.count }}, double is: {{ state.double }}
</button>
```

注意 `setup` 返回的 `ref` 在模板中会自动解开，不需要写 `.value`。

### setup

- 调用时机

`setup` 会在 `onLoad` 阶段被调用。返回的数据和方法也会在此时才会被合并到页面实例上，所以模版初次渲染时数据可能是 `undefined`。不过小程序模版对此做了兼容，所以不用担心会报错。

- 参数

`setup` 函数的第一个参数与 onLoad 的参数相同。

```javascript
createPage({
  setup(query) {
    // query 为打开当前页面路径中的参数
  },
});
```

### 生命周期

生命周期对应关系：

```javascript
import { createPage, onShow, onHide, onUnload } from '@roundjs/composition-ali';

createPage({
  setup() {
    onShow(() => {
      console.log('show');
    });
    onHide(() => {
      console.log('hide');
    });
    onUnload(() => {
      console.log('unload');
    });
  },
});
```

|       原生        |    组合式 api     |
| :---------------: | :---------------: |
|      onLoad       |       setup       |
|      onShow       |      onShow       |
|      onReady      |      onReady      |
|      onHide       |      onHide       |
|     onUnload      |     onUnload      |
|   onTitleClick    |   onTitleClick    |
|   onReachBottom   |   onReachBottom   |
| onPullDownRefresh | onPullDownRefresh |
| onShareAppMessage | onShareAppMessage |
|   onPageScroll    |   onPageScroll    |
|   onTabItemTap    |   onTabItemTap    |

为了兼容自定义`hooks`，页面也可以使用 `onMounted`和`onUnmounted` 生命周期

`createPage` 第二个参数为配置项，以下两个生命周期需要配置才可生效

- `onPageScroll`
- `onShareAppMessage`

#### onPageScroll

监听页面滚动会引起小程序渲染层与逻辑层的通信。为避免定义空的 `onPageScroll` 监听造成不必要的性能损耗，需要使用 `createPage` 的第二个参数提前告知 `composition-ali` 是否会调用 `onPageScroll()` 钩子。

```javascript
import { createPage, onPageScroll } from '@roundjs/composition-ali';

createPage(
  {
    setup() {
      onPageScroll(({ scrollTop }) => {
        console.log('scrollTop:', scrollTop);
      });
    },
  },
  {
    listenPageScroll: true, // 默认为 false
  }
);
```

#### onShareAppMessage

由于小程序会根据是否定义了 `onShareAppMessage` 监听来决定页面是否可以转发，所以需要使用 `createPage` 的第二个参数提前告知 Vue Mini 是否会调用 `onShareAppMessage()` 钩子。又由于 `onShareAppMessage` 会返回自定义转发内容，所以一个页面只能有一个 `onShareAppMessage` 监听。

```javascript
import { createPage, onShareAppMessage } from '@roundjs/composition-ali';

createPage(
  {
    setup() {
      // 仅第一次调用，且 `canShareToOthers` 为 `true`，且 `onShareAppMessage` 选项不存在时才生效。
      onShareAppMessage(() => {
        return {
          title: '小程序示例',
          desc: '小程序官方示例Demo，展示已支持的接口能力及组件。',
          path: 'page/component/component-pages/view/view?param=123',
        };
      });
    },
  },
  {
    canShareToOthers: true, // 默认为 false
  }
);
```

### 原生混用

```javascript
import { createPage, ref } from '@roundjs/composition-ali';

createPage({
  setup() {
    const count = ref(0);

    function increment() {
      count.value++;
    }

    return {
      count,
      increment,
    };
  },
  data: {
    number: 0,
  },
  add() {
    this.setData({ number: this.data.number + 1 });
  },
});
```

如果名称相同，`setup()` 返回的数据或方法会覆盖原生语法声明的数据或方法。你应该避免出现这种情况。

请不要在其他选项中访问 `setup()` 返回的数据或方法，这将引起混乱。如果确实有此需求，应该将相关逻辑搬到 `setup()` 内。

### 简洁语法

```javascript
createPage(() => {
  const count = ref(0);

  function increment() {
    count.value++;
  }

  return {
    count,
    increment,
  };
});
```

## <span id="component-createComponent">Component</span>

组件 `setup` 函数第一个参数为 `props`（跟 vue3 一样，不可解构使用）
第二个参数包含`emit`方法向父组件通知

[props](https://v3.cn.vuejs.org/api/options-data.html#props) 和 [emits](https://v3.cn.vuejs.org/api/options-data.html#emits) 参照vue3的使用

（注意`props`声明方式跟原生小程序写法不一样）

```javascript
import { createComponent, watch } from '@roundjs/composition-ali';

createComponent({
  emits: ['change'],
  props: {
    count: {
      default: 0
    },
  },
  setup(props, { emit }) {
    console.log(props.count)

    watch(
      () => props.count,
      (val) => {
        emit('change')
        console.log('watch props.count', val)
      }
    );

    const count = computed(() => props.count * 2);

    return {
      count,
    };
  },
});
```

### 生命周期

生命周期对应关系：

|    原生    | 组合式 api  |
| :--------: | :---------: |
|  didMount  |    setup    |
|  didMount  |  onMounted  |
| didUpdate  |  onUpdated  |
| didUnmount | onUnmounted |
|  onError   |   onError   |

#### 页面声明周期

组件可使用部分**页面**的声明周期: `onShow` `onHide` `onPullDownRefresh` `onReachBottom` `onPageScroll` `onTabItemTap`

在组件里使用`onShow`时可能会错过初次的`onShow`调用

## <span id="app-createApp">App</span>

```javascript
import { createApp } from '@roundjs/composition-ali';

createApp({
  setup() {
    const greeting = 'Hello World!';

    return {
      greeting,
    };
  },
});
```

### 注意

`createApp` 需要注意，`setup` 返回的对象不会经过处理，依然是响应式

返回什么就是什么：

```javascript
createApp({
  setup() {
    const greeting = ref('Hello World!');

    return {
      greeting,
    };
  },
});

const app = getApp();

console.log(app.greeting.value); // Hello World!
```

### 生命周期

生命周期对应关系：

|         原生         |      组合式 api      |
| :------------------: | :------------------: |
|       onLaunch       |        setup         |
|        onShow        |      onAppShow       |
|        onHide        |      onAppHide       |
|       onError        |      onAppError      |
|    onPageNotFound    |    onPageNotFound    |
| onUnhandledRejection | onUnhandledRejection |

## <span id="watch-effect">响应式 api</span>

响应式 api 跟 vue3 使用相同，可参考 [响应式 api](https://v3.cn.vuejs.org/api/reactivity-api.html) 文档

### 响应性基础 API

- reactive
- readonly
- isProxy
- isReactive
- isReadonly
- toRaw
- markRaw
- shallowReactive
- shallowReadonly

### Refs

- ref
- unref
- toRef
- toRefs
- isRef
- customRef
- shallowRef
- triggerRef

### Computed 与 watch

- computed
- watchEffect
- watchPostEffect
- watchSyncEffect
- watch

（`watch`参数 `flush: 'post'` 或者 使用`watchPostEffect` api，可在数据改变并且**小程序页面完成渲染**后触发回调）

### Effect 作用域 API

- effectScope
- getCurrentScope
- onScopeDispose



### hooks

#### useNextTick

```javascript
{
  setup() {
    const nextTick = useNextTick()
    const count = ref(0)

    const changeCount = () => {
      count.value++
      nextTick(() => {
        // dom更新后
      })

      await nextTick()
    }
  }
}
```

也可从`setup`的`context`中引入

```javascript
{
  setup(xxx, { nextRender }) {
    const count = ref(0)

    const changeCount = () => {
      count.value++
      nextRender(() => {
        // dom更新后
      })

      await nextRender()
    }
  }
}
```

#### useApp

跟小程序的`getApp`不一样的是可在`createApp`的`setup`里使用



## <span id="inject-provide">依赖注入</span>

与 `Vue` 一样，`composition-ali` 提供了依赖注入功能，以解决 `props` 深度透传的问题。它们是一对 `provide / inject` 函数，它们的使用方式和 `API` 均与 `Vue` 一致。

### Provide

```javascript
// parent-component.js
import {
  createComponent,
  ref,
  provide,
  readonly,
} from '@roundjs/composition-ali';

createComponent({
  setup() {
    const count = ref(0);

    function increment() {
      count.value++;
    }

    provide('count', readonly(count));
    provide('increment', increment);
  },
});
```

### Inject

```javascript
// deep-child-component.js
import { createComponent, inject } from '@roundjs/composition-ali';

createComponent({
  setup() {
    const count = inject('count');
    const increment = inject('increment');

    return {
      count,
      increment,
    };
  },
});
```

> 注意
> 依赖注入对执行顺序有所要求，`provide` 必须先于 `inject` 执行，所以在 `createPage` 的 `setup` 函数中调用 `provide` 可能会遇到问题。

### <span id="AOP">AOP</span>

AOP（切面编程），主要对 `App/Page/Component` 内方法调用进行统一劫持，可用于埋点，链路分析，日志等功能

```javascript
import { aop, setupAOP } from '@roundjs/composition-ali';

const pageAop = aop(
  {
    onLoad(args, name) {
      console.log('所有页面onLoad调用之前都会触发', args);
    },
    onShareAppMessage: {
      after(result, args, name) {
        console.log(`${name}方法被调用之后，after一般获取函数返回值操作`);
        return result;
      },
    },
  },
  (args, name) => {
    console.log('other 其他函数调用');
  }
);

// 装载全局，要在createApp/createPage/defineComponent之前调用
setupAOP(({ page, app, component }) => {
  page.add(pageAop);
});
```

- aop(AopOptions, Other)
- aop(Other)

`aop` 方法有两个参数，`AopOptions`代码劫持某一具体函数，`Other`代表劫持除了`AopOptions`内声明的其他函数

可以只传入`Other`来劫持所有函数调用

**after**请务必返回`result`结果

**aop 函数只会劫持一层对象**，如需多层可用以下写法：

```javascript
aop({
  onShow(args, name) {} // 函数形式默认 before
  onHide: { // 对象形式 before after
    before(args, name) {}
    after(result, args, name) {return result}
  }
  methods: aop({ // 深度劫持
    handleClick(args, name) {}
  })
}, {
  before(args, name) => { // Other 也可以写成对象形式，可通过name来判断对象名
    console.log('除了onShow,onHide的方法都会触发Other')
  }
})
```

### <span id="optimizePath">优化路径</span>

可通过修改配置参数来启用**优化路径**模式

```javascript
import { setGlobalConfig, createPage } from '@roundjs/composition-wx';

// 全局配置
setGlobalConfig({
  optimizePath: true,
});

// or

// 优先级更高的局部配置
createPage({ setup() {} }, { optimizePath: true });
createComponent({ setup() {} }, { optimizePath: true });
```

在 `createPage` 和 `createComponent` 上配置优先级更高，会先取示例上的配置，然后取全局配置

启用 **优化路径** 模式后，`setData`会采用路径形式来修改数据，例：

`list[0].deep.name = 'xxx'`

这个修改中，如果不启用 `optimizePath` 则默认修改的是整个 `list`： `setData({ list })`，如果 list 数据过长就会造成性能下降

如果启用 `optimizePath` 则会精确修改变动值 `setData({ 'list[0].deep.name': 'xxx' })`，从而提高 `setData` 性能

ps：因为`optimizePath` 模式会多出来 `diff` 数据的时间，实际上会比原生 `setData({ 'list[0].deep.name': 'xxx' })` 慢，但是比不启用`optimizePath`直接 `setData({list})`更快

## 参考

[vue-mini](https://github.com/vue-mini/vue-mini)