深入浅出 "import all from 'umi' " 实现原理
前言
由于部门业务和组织的调整,笔者负责的业务域新增了多个产品线,涉及多个中后台系统。目前的问题是这些系统所使用的技术栈和开发流程都不统一,上手成本较高,每个系统基本都是专人维护,当某个业务线需求吞吐出现压力时也难以临时调配资源。为了降低开发和上手成本,考虑去适当地统一下研发范式,而应用框架是中后台领域范式统一的最佳实践之一,所以笔者和小伙伴们最近也在调研一些开源的应用框架,如 umi
、ice
等。
Umi 作为蚂蚁推出的一款开源的企业级前端应用框架,拥有生命周期完善的插件体系,自然进入了我们的调研范围。近期 Umi 发布了 4.0 版本,笔者在翻看其官方文档时,看到以下一段有意思的话:
原文链接: https://umijs.org/docs/introduce/philosophy#import-all-from-umi
从这段话中我们发现了一个有趣的功能 import all from 'umi'
,简单说就是所有能力都从 umi
中去 import
获取,Umi 还支持通过插件扩展 import all from 'umi'
的能力。
那么 Umi 究竟是怎么做到的呢?别着急,我们再来翻一翻 Umi 的源码一探究竟。
源码分析
umi
从 demo 说起
我们先从一个一般性的使用案例着手分析,这里以 $ pnpm dlx create-umi@latest
创建的一个新 Umi 项目为例:
可以看到,在页面中是直接通过 import { useModel } from '@umijs/max'
来使用 useModel
方法的。
由此顺藤摸瓜,我们直接到 /examples/with-use-model/node_modules/@umijs/max/index.js
下的 @umijs/max
看下是否有导出 useModel
方法:
@umijs/max
导出的其实是 umi
包的全部方法,那么继续在 node_modules
中看下 umi
的代码,其入口在 /examples/with-use-model/node_modules/.pnpm/node_modules/dist/index.js
中:
注:关于 pnpm 可以参考笔者的另外一篇文章了解详情,从一个构建问题再谈依赖包加载机制。
由此看出,其实 umi
包也没有实际导出 useModel
方法。
webpack
构建时
按照 Umi 官方的说法,@umijs/max
是一个插件集,@umijs/max
内置了数据流管理插件,那么 useModel
一定是由插件注入的,.umirc.ts
配置文件也证实了这点:
但由于我们的主题并不是研究 Umi 插件本身,所以在这里我们不过多去纠结 Umi 插件实现原理,而专注于分析 Umi 插件是如何搞定 webpack
构建时并以此来支持通过插件扩展 import from 'umi'
的能力的。
alias
这个时候我们需要翻看下@umijs/max
的源码,在 /packages/max/src/plugins/maxAlias.ts
文件中,@umijs/max
基于 Umi 插件协议修改了 webpack
配置,配置了 alias
别名 @@/exports
:
那么 @@/eports
的物理文件究竟又指向哪里呢?我们可以在 /packages/preset-umi/src/features/configPlugins/configPlugins.ts
中找到答案:
也就是说,实际上 @umijs/max
或 @@/exports
最后都是指向的同一个物理文件,即 /examples/with-use-model/src/.umi/exports
:
模板
众所周知,Umi 项目中 .umi
目录下存放的是临时文件,那么这个 /.umi/exports
临时文件又是如何生成的呢?答案在 packages/preset-umi/src/features/tmpFiles/tmpFiles.ts
中:
这里向插件 register 了一个临时文件的生产方法 hooks
供 applyPlugins
使用,在项目 dev
或 build
的时候就会生成 /.umi/exports
文件。
以上,便是 Umi 实现 "import all from '@umijs/max' " 的全过程。
vitekit
在 Umi 官方文档中也提到:“Remix、prisma、vitekit 等框架和工具都有类似实现”,为了加深理解,我们也一道来分析一下 vitekit 又是如何实现这个能力的呢。
从 demo 说起
同样地,将 vitekit 源码 clone 到本地后,继续从官方使用案例着手分析,这里以 /playground/test-simple/app/routes/index.vue
为例:
相同的地方是,页面中依然是直接从 .vitekit-package/index
中获取 useRoute
和 useNavigate
方法。
不同的地方是,vitekit 不是利用 alias
别名,而是利用 .vitekit-package
依赖包的方式:
在 .vitekit-package
中又间接引用了 @vitekit/framework-vue
:
vite 构建时
vitekit 的 “magic” 在于:其实项目中也没有直接依赖 .vitekit-package
依赖包,而 node_modules
中却自动引入了这个依赖。
很显然,这也肯定离不开构建时的支持,在源码 /vitekit/src/node.ts
中可以一目了然:
- 首先,vitekit 也定义了一个小巧的插件协议,内部实现了插件加载机制,修改文件导入导出。
- 其次,自动生成
/node_modules/.vitekit-package
依赖包:
以上,便是 vitekit
的实现方式。这种通过 node_modules
依赖的方式,好处在于不太需要一个比较复杂的构建时插件协议,去暴露出来修改构建时配置的能力。
综上,umi
和 vitekit
的实现思路大体是类似的:
- 首先,定义了构建时插件机制,让插件能拓展
import all from 'xxx'
的能力。 - 其次,通过临时文件(
.umi
)或临时依赖(.vitekit-package
) 的方式,把各个插件拓展的能力收集在一个文件或者包中,统一导出,供业务中统一使用。 - 最后,通过
alias
别名或者node_modules
依赖的方式,实际暴露import all from 'xxx'
的能力。
手写实践
本着 talk is cheap, show me the code
的原则,我们参考 umi
的思路,尝试来写一个极简的 demo,并以此完成本次学习之旅。
第一步,编写一个生成一个临时文件 exports
的函数:
第二步,修改 webpack
配置,配置 alias
别名:
第三步,准备好测试 demo
。这里我们以一个简单输出 hello world
的函数为例,并在页面中引用这个函数:
最后,执行 pnpm start
启动项目,可以看到最后生成的临时文件:
同时去检查下页面展现和控制台输出,与预期相符:
注:笔者功力有限,本文仍然可能存在理解纰漏,欢迎留言指正。 另外,欢迎关注笔者的语雀数字空间,工作之余一起探讨和学习。
参考文档
转载自:https://juejin.cn/post/7231080269503283259