最近在用 Electron + VUE + Webpack 做一个 Web IDE,下面简称 Proj A,A 依赖了我们的编译器 Proj,下面简称 Proj B,而 B 依赖了 lib C。C 包 size 很大,大概有5M,因为 webpack 打包时会分析依赖关系,合并成一个文件,导致每次 build 会巨慢无比,高达十几分钟,最后内存不足 fail 掉。虽然看资料有说可以提升内存,但是无论如何这个打包时间是不能接受的。于是就打算用 external 模式来排除 lib C,然后在 script 标签中来引用它。
第一天
多次尝试后,发现失败了,因为 B 包依赖 C 是按照 node 的方式来引入,用 gulp 来打包。在 Proj A 中尝试配置各种 external 选项,还折腾了 requirejs 什么的,发现都不行。最后又回头来读了 webpack 的文档,大概理清了思路:
- C 包是一个标准的 commonjs 包,是不可能直接放在 script 标签中的
- 如果把 C 改成 UMD 方式打包,不仅需要拿 C 的代码重新来 build,而且 B 的调用代码也需要修改
所以搞清这个问题后,决定换个思路,既然 webpack 天生就干这种依赖打包的事,干脆把 B 和 C 的代码打包到一起,这样把 B 做成一个 UMD 包。
说干就干,开始在 Proj B 中配置 webpack ,一路还比较顺利就生成出了 B 包,没压缩时大概 6、7M 的样子。
有了这个 B 包后,就在 Proj A 的 externals 中排除 B,把 B 加入到 script 标签中(此时还是同步引入)。一运行,嗯哼,失败!
Import * as B from ‘B’
这行代码无论怎样都过不去,提示 B 找不到。
一番折腾到深夜,各种调配 externals 的几种模式,又反复修改 proj B 的 libTarget ,进行各种排列组合,还是失败!
中途还把 B 的导出 改成了 default,import 语句变为如下依然不行:
Import B from ‘B’
在几近崩溃中睡觉……
第二天
稍微清醒了一些,情绪没那么失控了,决定先做一下对比分析。于是全新创建了一个 VUE 的初始工程,尝试把 B 放入 script 标签中。一运行,import 代码正常通过,靠,居然没问题。既然标准的 VUE 工程行,那 Proj A 肯定也行......
一番折腾后,无果。各种调配了 external 的参数,对比了两边的情况,依然不行。
……
继续折腾后,似乎看到了一点点线索。
在 chrome 的 debugger 窗口中,发现 source 这栏里面,会出现一些和 external 有关的源代码。我猜应该是根据 source map 自动生成出来的。有几个 external 包,就有几个这样的文件。文件的内容就一行代码:
module.exports = B
跟了一下代码发现,每次初始化的时候,都会走到这里。根据 externals 的配置,我已经知道了如果把 B 放在 script 中,初始化的时候会创建一个全局变量 B,上面这行代码就是在读取这个全局变量。
把 external B 配置改成 commonjs 模式,这样代码就变成了:
module.exports = require('B')
又做了几次实现,得出了几个结论:
- 在全新 VUE 工程中,设置 global 模式和 commonjs 模式,可以出现上面这两种代码变换
- 第一种代码可以顺利 import 到 B,而第二种代码会 import 失败
- Proj A 无论在哪种配置下都只能得到第二种代码
因为自己也做一些编译器的工作,大概知道 webpack 的把戏,我们代码中的 import 或者 require 最后都会变成 webpack 自己特定的 require 函数,这个 require 函数会根据 external 包里面配置的模式采用不同的方式去调用,也就是上面看到的这两种代码。
另外一方面,在 web 上运行的时候,第二种代码是无论如何都不可能工作的,必须是第一种。也就是说想要实现在 script 标签中引入一个 external 包有几个前提条件:
- B 包的 libTarget 必须是 UMD 模式,所以第一天我胡乱修改 B 的打包参数是没有意义的。
- Proj A 配置的 external 模式必须是 global
思路理清后,就坚定的把 B 设置为了 global,在当时我所知道的配置 global 的方式是:
externals: {
B : 'B'
// 这里需要单独强调一下,以这下面行代码为例
// import * as compiler from 'my-compiler'
// 左边的 B 是指 'my-compiler' 这个名字,也就是 NPM 包的名字
// 而右边的 B 是在 Proj B 中用 webpack 打包时 output 中的 library name
// 假设 library name 是 'compiler',这里的配置就是:
// 'my-compiler' : 'compiler'
}
但是在这种配置下,全新的 VUE 工程可以顺利跑过,生成的代码也是:
module.exports = B
而 Proj A 无论如何都不行,代码永远都是
module.exports = require('B')
一番折腾后……
发现了一点新线索,就是 Proj A 是 Electron + VUE 工程,它在 debugger 窗口中的 external 文件会比我配置的多出了几个。最明显的就是 Electron 自己的包。很明显 Electron 自身的包也很大,排除一下也是很合理的,但是我没有设置过这玩意啊。回头复盘一下,猜测应该是 target 搞的鬼,因为 Electron 的 target 是 'electron-renderer’,普通 VUE 工程默认是 ‘web’。这两个参数就决定了 webpack 会做一些不一样的事情。
于是我猜测,会不会是 webpack 在打包 Electron、默认添加这几个 external 配置时,顺带把我的配置给搞成了 commonjs 模式呢 ?搞不好是个 bug,不然这个问题说不通。
接下来就下载了 webpack 源代码,通篇搜索 Electron 这些关键字,最后发现了一些线索,原来还有一个 global 关键字。之前在翻阅各种文档的时候看到有提过这个东西,但是官方 3.0 的文档里面又没有说这个事,所以一直被我忽略了。先不管是不是 bug 了,尝试改改配置看看:
externals: {
B : 'global B'
}
改完配置,果然生成的代码就变成了:
module.exports = B
运行顺利通过。终于可以睡觉……
第三天
B 包已经排除, webpack 打包速度飞快,但是有一个问题就是 B 包太大,网页加载有点卡,尝试给 B 的 script 标签添加 async 。 一运行,失败!老问题又出现了,真是五雷轰顶。
分析是因为 B 的 script 加载比调用的 page 要晚,那么添加一个 Loading 页,等 script 加载完了再跳过去。一运行,继续失败!在 Loading 页一出现,就提示 import B 失败。这就奇怪了,import B 的页面都还没出来,怎么就失败了呢?
去掉 webpack 打包的 ugly 参数,把 build 后的主文件打开看看,原来所有的 import 或者 require 在文件一加载时就会全部执行一遍。执行的过程就会去调用这段代码:
module.exports = B
而此时全局变量 B 还没创建,所以必定失败,且此后这个 require 的过程不会再执行了。
此时一瞬间就想起了前两天看到的 require.ensure ,原来它就是这样的 require。于是尝试把 Proj A 中 import B 的代码都改成了 require.ensure 模式,运行顺利通过。然后再对比打包文件,发现原来那些 require 代码不见了,变成了一个 promise,在 promise 中再 require 就安全了。
但是此时又有一个新问题,那就是我的代码都是用 typescript 写的,如果不能在文件头部 import,改用动态 require,那么我所有的类型信息都没了,代码完全没法写了啊,这样肯定不行。
于是又尝试在头部继续 import,在调用的代码继续使用 require.ensure,居然一切正常。打开 build 出来的主文件一看,原来如此:
Webpack 在打包时会做依赖分析,即使你在文件头部使用同步 import 或者 require,它并不一定会生成那些特殊 require,关键是要看你 import 出来的东西是否有在代码中被使用到。
不管是下面哪种情况:
Import B from ‘B’
Import * as B from ‘B’
Import { init } from ‘B’
只要你没有在代码中使用到任何 B.xxx 、new B() 或者 init 这样代码是根本不会在头部生成 require 代码的。另外要特别注意的是在 require.ensure 中也必须使用 promise 返回给你的那个对象,不能直接用上面同步 import 的对象。
require.ensure('B', function(b){
b.init(); // 运行通过
init(); //运行失败
})
所以这么看,只要能在文件头部 import,那么 typescript 也可以继续用了,不会触发错误。完美!
总结
如果要以 async 的方式在 script 标签中引入 external 包,还是以 Proj A、B为例:
- B 包一定需要是 UMD 包(非 webpack 打包出来的 UMD 我不是很确定,毕竟没做过实验,核心逻辑是 B 的 script 被加载后,需要向浏览器环境写入一个全局变量。所以我猜测不做一些其他工作, AMD 包也是不行的)
- Proj A 在 externals 配置中必须为 global 模式(global 至少有两种配置方式,第一种模式给我添了很大的麻烦,第二种比较顺利,大家酌情考虑)
externals: {
B : 'B' // 在纯 VUE proj 中正常,在 Electron proj 中不正常
B : 'global B' // 均 ok
}
- 我最终并没有直接把 B 写在 script 中,而是用了一个叫做 VueScript2 的 lib 帮我实现异步加载,这样方便我在 Loading 页面中控制页面流程。不要试图用 require.ensure 直接帮你加载 url ,这样会出问题。
- 所有涉及 B 包调用的地方都需要改用 require.ensure 方式来加载。
- 即使在 require.ensure 模式下,依然也可以在文件头部使用同步 import 语句,并不会影响 typescript 的类型系统,但是切记不能调用任何东西。