跨平台 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.ts
和 taro.ts
;index
仅导出平台无关的内容,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-use
和 cross-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,但本质上与第二种方式一样,故不进行讨论)