一种webpack打包时的external增强兜底方案
如果让你编写一个 jssdk,且里面要用到 vue,你会如何在 jssdk 中加载 vue?
问题出现
首先,我们的前提条件时你的 jssdk 必然要用到 vue,即你对 vue 是个强依赖。于是你可能想“直接把 vue 打包进 jssdk 然后交付 jssdk 产物”。 然而对于当下的市场环境来说,大部分使用你项目的人,都是在 vue 环境下使用你的项目。
于是你如果 jssdk 内部额外构建打包了一个 vue,就意味着你把宿主环境下的“vue 环境”给白白浪费了。假设宿主环境下真有一个 vue,那你何必要在自己的 jssdk 内部额外打包一份呢。这种情况下,你 jssdk 内部直接把 vue 直接声明成 external 就可以实现跟宿主共享 vue 来构建最终 webapp 产物。
然而问题是:你在构建 jssdk 的时候根本无法预判宿主项目到底有没有 vue 啊!
external 的问题
你可能会想到,不如我就无脑写死 “external”吧。反正无论宿主他有没有使用 vue,我都强迫宿主环境要安装 vue。于是你通过 package.json 里面声明 peerDependencies 告诉宿主环境必须安装 vue 你的 jssdk 才能正常工作。
然而宿主可能会质疑你:“你 jssdk 想用 vue,凭什么让我给你装,你 jssdk 内部自己装不就行了”。
明确目标
于是,为了能更优雅一些,尽量不给宿主环境增加心智负担。我设计了这样的一个想法:
- 当预编译的宿主环境(或浏览器宿主环境)有 vue 时候,我们就直接用宿主环境下的 vue module,跟宿主项目共用 vue 模块。
- 当宿主环境没有 vue 的时候,我们就在运行时通过 import 动态懒加载来获取 vue(例如通过 webpack )
增强兜底
所以经过实操,我总结出这样的一套实践方案:
- 将 vue 声明成 external,且不要声明成 peerDependencies。
- 配置 jssdk 的导出模式为 umd,且配置好 external 在不同运行环境情形下的注入方式。
- 如果运行时检测到宿主环境没有注入 vue,那我们通过 import 懒加载方式来远程加载自己所需要的 vue
external 的 webpack 配置代码如下:
1 | externals: { |
output 的配置如下:
1 | output: { |
有了上述配置后,我们还要解决宿主环境缺少 vue 时候的主动加载问题。具体代码如下:
1 | // common/vue.js |
上面代码其实是一段运行时要执行的函数,该函数的目的就是在运行时检测到底 Vue 有没有“在宿主构建时打包成功” 或者 “在浏览器运行时用 window.Vue 注入成功”。 如果都没有,那说明构建时宿主也没装 vue,且浏览器中宿主也没有提前加载一个 vue 的 cdn 链接。那就等于没有任何 Vue 注入。于是上述代码就会自行去加载一个 vue 来使用。
然后,我们 jssdk 其他代码中但凡使用到 vue 的地方,就这样引用 vue 即可:
1 | import { resolveVue } from "./common/vue.js"; |
resolveVue 就是包含了检测 vue 和拉取 vue 能力的一个函数,然后交付一个完整的 vue 对象。
方案缺点
万一宿主没有提前加载 vue,那么意味着你的 jssdk 运行时刻才会动态加载 vue。这或许会带来一定的性能损耗。但是之所以设计上述动态加载方案,理由有 3:
1、大部分宿主都有 vue,所以只有少数情况下才可能会用到动态懒加载。
2、一般 jssdk 都是页面运行后,被宿主环境按需调用。大部分情况下 jssdk 并不会在页面初始化过程中就立刻运行。所以 jssdk 自身其实有足够的时间去把 vue 拉回来。
3、一般 jssdk 都不是核心业务功能,可能是一些异步渲染或异步采集信息的功能。所以总体上对核心业务影响不大。
原理分析
我们看看编译产物直接截图分享下原理:
那如果宿主环境没有呢。则会看到我们 jssdk 中对应调用的地方,就是直接走 webpack 动态懒加载了:
效果收益
基于此,我可以保持暴露的主体 jssdk 的体积足够小。如下图是核心 main.js 的体积(未压缩):
然后万一任何使用我的 jssdk 的宿主环境下有引入 vue(无论是编译宿主,还是浏览器宿主)。则我们的 jssdk 就不会再去加载自身的 vue。
最大的好处是:此方案不需要宿主环境有任何感知。