前言

众所不周知咱最近写了一个简单的去除 tracker 参数的在线小工具,当然也包含跟随重定向短链,要避免带 tracker 的短链泄漏隐私当然需要一个服务器代替自己访问一下,获取到重定向的完整链接后再处理掉 tracker,盯了 Fresh 这个框架很久了,感觉还是不错的于是打算用这个项目试试水。

然后就遇到了 Preact(Fresh 底层使用的 UI 框架是 Preact)与 React 生态的兼容问题,这篇文章就来简述一下可能遇到的不同程度的坑,以及如何解决。

三种不同程度的问题

React 别名设定

要设定别名主要是因为大多数的包都是为 React 开发的,所以它们很多的东西都是直接从 React 中导入,但并不打包(也就是作为 peerDependencies),于是就可以将它们的 import Something from 'react' 给“重定向”到 import Something from 'preact/compat',这样这个 React 生态的包就可以在 Preact 项目中使用了。

Preact 文档里其实有提到需要将 reactreact-dom 等一系列包设置别名到 Preact 兼容层(preact/compat),如果你的项目是使用 Webpack、Parcel、Rollup 等打包工具打包的话,文档里已经有了设置别名的方法,也包含直接在网页上用 ESM 引入的方式通过 import map 定义别名。

Deno 中 esm.sh 的别名

但如果你在 Deno 中使用,或者是在用 Fresh(那就只能 Deno 了),则需要在 deno.json 中定义 import map,而且要借助 esm.sh 的别名能力,才能重定向你所依赖的包的导入语句:

1
2
3
4
5
6
7
{
/* other options ... */
"imports": {
"@tanstack/react-query": "https://esm.sh/@tanstack/[email protected]?alias=react:preact/compat&external=preact"
},
/* other options ... */
}

这里简单解释一下这两个查询参数的作用。alias 参数是将包中的所有从 react 的导入重定向到 preact/compat,external 参数将 preact 及其下面的包定义为外部依赖,即不会在这个包中包含 preact 下面的包,这主要是防止项目中出现多个不同版本的 preact,因为 esm.sh 会默认将没有写版本号的包认为是最新版哦。当然你也可以显式指定它里面的 preact 版本:[email protected],但感觉不如定义为外部依赖,可以给工具链节约一点微小的资源,显式指定版本的话之后版本升级改起来还麻烦。

由于所依赖的这个包里面的所有从 react 的导入语句都被重定向到 Preact 兼容层了,因此理论上不会遇到程序性和类型的问题了,如果有的话可能是 esm.sh 的别名没有生效或者 Preact 兼容层有问题。

motion 别名不生效

背景

咱在项目里用了 motion,但是设置的 esm.sh 别名没有生效,一直报类型错误,原因是 motion 里面的组件需要 React 里面的 node 类型,而 Preact 的 JSX node 类型是不一样的,报错如下:

类型错误

探究

esm.sh 交付的只是编译后的 JavaScript 文件,那么类型信息是哪里来的呢?其实 esm.sh 上有提到这是 Deno 的功能,而 esm.sh 为类型做了兼容:

Deno supports type definitions for modules with a types field in their package.json file through the X-TypeScript-Types header. This makes it possible to have type checking and auto-completion when using those modules in Deno. (link).

知道了类型哪里来的咱们就可以看一下 motion 这个包为什么就这么与众不同不听 alias 的话了。

类型 1

如果你经历过 esm.sh 的调试或者用过的话,就知道包名和导入路径之间那串长长的看起来像是什么编码的字符串其实是用于透传查询参数之类的信息的,而到了这里,motion 的整个类型定义文件里只有一个从 framer-motion(motion 以前的名字)导入的类型的重新导出,这里并没有 react 的导入,因此别名没有应用,而 esm.sh 的别名似乎不会在类型上透传(或者是压根就不会透传?),总之在 URL 里丢掉了别名的信息,不出意外的话就要出意外了,咱们访问一下它导入的这个类型定义文件。

类型 2

果然里面有一个从 React 导入的类型,而且由于别名选项缺失,esm.sh 没有把它重定向到 Preact 兼容层,于是就出现了怎么定义别名都会报错的类型问题。

感觉这个是 motion 的代码结构和 esm.sh 的兼容问题,esm.sh 大概是不会给导入的其他包再应用别名(也就是透传别名选项),而 motion 里这个类型定义是直接从另一个包的重新导出,导致了 esm.sh 别名的失效。咱们可以瞅一眼 motion 的仓库结构:

Motion 仓库 1

Motion 仓库 2

基本验证上面所说的,不过关于 esm.sh 的别名透传的机制还没验证,到这里已经有这个棘手问题的解决办法,咱就懒得继续深挖,就留给其他有好奇心的小猫吧。

解决

所以这个问题最终的解决方法就是自己导入并且重新导出 motion/react 这个包,并且通过 Deno 的功能强行指定类型,这个类型是上面类型的图 1 里那个 framer-motion 的类型(因为它反正是定义在这里的,而 motion/react 的类型只是对它的重新导出),并给它设置一个别名到 Preact 兼容层:

1
2
// @deno-types="https://esm.sh/[email protected]?alias=react:preact/compat&external=preact"
export * from 'motion/react'

然后之后用符号的话就从这个文件里导入就好了。

1
import { motion } from './your-file-name.ts'

另外咱还发现似乎和 Deno 解析模块的机制有关系,这个强制覆盖的类型不一定要导出并且在别的地方导入,只要在导入的时候强制指定过类型,之后就算导入原来的 motion/react 这个包也会变成咱们自己指定的类型(大概是缓存原因),也就是这样子也可以:

1
2
// @deno-types="https://esm.sh/[email protected]?alias=react:preact/compat&external=preact"
import * as _ from 'motion/react'

然后导入语句就不用修改:

1
import { motion } from 'motion/react'

但是咱不确定这是不是 Deno 有意设计的功能,还是不太建议依赖这种没有在文档中标注出来的功能,因为随时有可能改掉。最稳妥的方式还是自己重新导出并指定类型。

参考