Webpack 打包记 —— 如何在 Script 标签中以 async 模式引用 Webpack 中的 external 包?

最近在用 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 的文档,大概理清了思路:

  1. C 包是一个标准的 commonjs 包,是不可能直接放在 script 标签中的
  2. 如果把 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')

又做了几次实现,得出了几个结论:

  1. 在全新 VUE 工程中,设置 global 模式和 commonjs 模式,可以出现上面这两种代码变换
  2. 第一种代码可以顺利 import 到 B,而第二种代码会 import 失败
  3. Proj A 无论在哪种配置下都只能得到第二种代码

因为自己也做一些编译器的工作,大概知道 webpack 的把戏,我们代码中的 import 或者 require 最后都会变成 webpack 自己特定的 require 函数,这个 require 函数会根据 external 包里面配置的模式采用不同的方式去调用,也就是上面看到的这两种代码。

另外一方面,在 web 上运行的时候,第二种代码是无论如何都不可能工作的,必须是第一种。也就是说想要实现在 script 标签中引入一个 external 包有几个前提条件:

  1. B 包的 libTarget 必须是 UMD 模式,所以第一天我胡乱修改 B 的打包参数是没有意义的。
  2. 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为例:

  1. B 包一定需要是 UMD 包(非 webpack 打包出来的 UMD 我不是很确定,毕竟没做过实验,核心逻辑是 B 的 script 被加载后,需要向浏览器环境写入一个全局变量。所以我猜测不做一些其他工作, AMD 包也是不行的)
  2. Proj A 在 externals 配置中必须为 global 模式(global 至少有两种配置方式,第一种模式给我添了很大的麻烦,第二种比较顺利,大家酌情考虑)
externals: {
    B : 'B'  // 在纯 VUE proj 中正常,在 Electron proj 中不正常
    B : 'global B' // 均 ok
}
  1. 我最终并没有直接把 B 写在 script 中,而是用了一个叫做 VueScript2 的 lib 帮我实现异步加载,这样方便我在 Loading 页面中控制页面流程。不要试图用 require.ensure 直接帮你加载 url ,这样会出问题。
  2. 所有涉及 B 包调用的地方都需要改用 require.ensure 方式来加载。
  3. 即使在 require.ensure 模式下,依然也可以在文件头部使用同步 import 语句,并不会影响 typescript 的类型系统,但是切记不能调用任何东西。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容

  • 回家过年文/一树繁花 离家的人们打包一年来的汗水还有思念和牵挂听着思念的歌或是唱着流浪的歌以一张回家的票承载又一季...
    一树繁花_649d阅读 131评论 0 0
  • 历史的弃儿——慈禧侍女荣儿传05 第一章 初次入宫 5.接见家属(下) 第二天,是大年初二。 等梳头刘当差下来以后...
    半床诗阅读 551评论 0 2
  • 作者:维吉丽亚·萨提亚 请你爱我之前先爱你自己爱我的同时也爱着你自己你若不爱你自己你便无法来爱我这是爱的法则因为你...
    王妍阅读 575评论 0 0
  • 最近啃完了《大数据时代》,从以下三个方面概括一下自己的感受。 1、大数据的时代性: 现代社会已经不是一个过分依赖直...
    Bboy阿树阅读 327评论 0 1