跨平台 Hooks npm 包的接口设计

一、Overview

Taro 是用 React 的方式来写小程序,拥有与 React 一致的 api,因此可通过相同的实现来同时满足多端的需求。现在正在构建一个可跨端使用的 Hooks 包,关键的问题在于如何处理包的依赖。

当在小程序中使用时,实际上是依赖了  @tarojs/taro,而在 web 中使用时需要依赖 React:

// cross-use(Hooks 包的包名)
function useMyHook() {
  return useState();
}

// 业务方使用时
import { useMyHook } from "cross-use";

function App() {
  useMyHook(); // 在小程序中,需要使用来自 `@tarojs/taro` 的 useState
  return null;
}

function App() {
  useMyHook(); // 在 web 中,需要使用来自 `react` 的 useState
  return null;
}

接下来要探讨的是,同样的一个 Hook,如何在不同的环境中使用正确的依赖,而且使用方尽可能简单。

二、详细设计

目前想到了两种方式,但都有一些缺点,未寻得既简单又无害的路。

在描述两种方式之前,先定义 cross-use(Hooks 包的包名) 包的构成(Hook 的实现有一条原则,凡是平台无关的方法均写成以 react 为依赖):

// useCount.ts,通用 Hook,可跨端使用
import { useState } from "react";

export function useCount() {
  const [count, set] = useState(0);
  return {
    count,
    add: () => set((pre) => pre + 1),
  };
}

// usePageIdMini.ts,小程序专用 Hook,仅 taro 环境可用
import { useMemo, useScope } from "@tarojs/taro";

export function usePageIdMini(): string {
  const ctx = useScope();
  return useMemo(() => {
    try {
      return ctx.getPageId();
    } catch (error) {
      return "";
    }
  }, [ctx]);
}

❎ cross-use 对外暴露一个 exports 文件,使用方通过配置构建工具的别名来处理依赖问题

cross-use 的 exports 文件,按常规方式导出:

// index.ts of cross-use
export _ from './useCount';
export _ from './usePageIdMini';

既然使用方的关键是依赖问题,那么可以在引用的项目中配置别名来显式指定依赖。

在 web 项目中,不会依赖特定平台的能力,如上述的 usePageIdMini,且因为 cross-use 的编写原则,所以无需改动。

import { useCount } from "cross-use";

const App = () => {
  const { count } = useCount();
  return <div>{count}</div>;
};

而当在小程序项目中使用时,则需要注意依赖问题,要将 react 配置为 @tarojs/taro

import { useCount, usePageIdMini } from "cross-use";
import { Block, View } from "@tarojs/components";

const App = () => {
  const { count } = useCount();
  const pageId = usePageIdMini();
  return <View>{count}</View>;
};

虽然项目中的依赖问题被解决了,但这样的解决方式以及导出设计有一些不足:

使用比较麻烦,需要配置别名,但好在一个项目只需要配置一次。
别名配置具有传染性,会污染依赖了 cross-use 的包。
假如有一个小程序工具包(taro-utils)的实现依赖了 cross-use 中的通用部分(如 useCount),那么业务方在使用 taro-utils 的时候也需要配置别名。
web 项目看似在使用时无需配置,但实际上在编译时会有一个错误,即找不到 @tarojs/taro,若是为了解决这个在最终打包后也用不上的 taro 依赖而去安装他,岂不是一种新的麻烦和浪费。
react@tarojs/taro 不能共存;编译工具的别名配置都是全局的,那么项目中两种依赖无法共存。(实际的项目中也没有两种共存编译的情况,所以该条可不计)
考虑到别名配置的繁琐性和污染性,没有采用这种方式,而是选择了下面的做法。

✅ cross-use 暴露两个 exports 文件,使用方选择性导入以处理依赖问题

cross-use 提供两个 exports 文件,分别是 index.tstaro.tsindex 仅导出平台无关的内容,taro 专用于 taro 的环境:

// index.ts
export * from './useCount';

// taro.ts
export _ from './index'; // 需要将其中的 react 导入转换为 @tarojs/taro
export _ from './usePageIdMini';

另外在以 taro.ts 作为入口进行编译的时候,将所有 react 的导入提前转换为 @tarojs/taro,即 import xxx from 'react' => import xxx from '@tarojs/taro'

如此一来使用方只需要根据当前的环境选择入口即可:

// web 项目
import { useCount } from "cross-use";

const App = () => {
  const { count } = useCount();
  return <div>{count}</div>;
};

// 小程序项目
import { useCount, usePageIdMini } from "cross-use/taro"; // 使用特定的路径

const App = () => {
  const { count } = useCount();
  const pageId = usePageIdMini();
  return <View>{count}</View>;
};

当 cross-use 作为基础依赖时,也没有问题:

// 开发 web-utils
import { useCount } from "cross-use";
import { useState } from "react";

export function useWeb() {
  useState();
  return useCount();
}

// 开发 taro-utils
import { useCount } from "cross-use/taro";
import { useState } from "@tarojs/taro";

export function useWeb() {
  useState();
  return useCount();
}

这样一来:

在 web 项目中使用的是 index 入口,不会包含 taro 相关的问题,也无需关注
在小程序项目中只需使用 taro 入口,无需触及编译配置

缺点方面比起第一种方式看起来要小很多了:

cross-use 的 taro 部分难以基于第三方 Hooks 库(如 react-use)进行开发,一旦引入,使用方在小程序环境依旧得配置别名,用  @tarojs/taro 替换 react
看似杜绝了使用第三方的路子,但本着不重复造轮子的原则,实际上影响比较小;而当真得需要依赖第三方的方法时,将局部方法 fork 一份实际上影响是微乎其微的
在每个文件中初次使用 cross-use/taro 路径时,编辑器无法自动导入

最后需要注意的是,有些场景会出现同时引用了 cross-usecross-use/taro 的情况,比如下面这个 useBusiness.ts 文件,通过某种方式该文件共享于某 web、小程序项目:

// useBusiness.ts

import { useCount } from "cross-use"; // 这里选择从主路径导出

export function useBusiness() {
  return useCount();
}

// 使用方都是引用 useBusiness 使用
import { useBusiness } from "./useBusiness";

对于这种情况,根据方法的实现、实际项目的环境相互替换别名即可:

web 项目无需改动
小程序项目中配置别名,用 cross-use/taro 替换 cross-use

(还有一种方式是通过一份源码分发不同环境的包,例如 cross-use、cross-use-taro,但本质上与第二种方式一样,故不进行讨论)

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。
comments powered by Disqus