webpack 动态引入
最近在写一个公用库时,遇到一个需求:
在某个初始化函数内需要根据传参来确实初始化哪一种图表,而两种界面用到的第三方库不同
如果将两个库都引入,然后根据条件判断去执行对应库里的函数,就会造成明明只想要 A 图表,却将 B 图表需要的第三方库也打包进去
并且由于是条件判断,因此无法做树摇(webpack 只分析代码,不执行代码),无论如何分包,两个库都会被打包进去
于是想到,通过动态引入另外两种工具库:
exports.init = echarts => {
if (echarts) {
import('echarts'/* webpackChunkName: "echarts" */).then(res => {
console.log(res)
})
} else {
import('@antv/g2'/* webpackChunkName: "ant" */).then(res => {
console.log(res)
})
}
}代码写完以后打包,发布到 npm,紧接着在工程内引入使用:
import { init } from 'tools'
init(true)结果发现报错了,echarts.js 引入失败: 
然后一看链接路径,js 文件夹下确实没有这个文件
因为在打包我自己的 tools 时,webpack 将 echarts 单独分出来了
但是在工程中动态使用,请求的路径不对,所以导致 404
webpack 动态引入实现原理
那么我们看一下 webpack 是如何得到这个路径的(部分代码已省略,仅展示与获取路径相关代码):
/* 重点在于得出 __webpack_require__.p */
/* webpack/runtime/publicPath */
(() => {
// ...省略部分代码
var scripts = document.getElementsByTagName("script");
/* 从后往前,获取一个 src */
if(scripts.length) {
var i = scripts.length - 1;
while (i > -1 && !scriptUrl) scriptUrl = scripts[i--].src;
}
// ...省略部分代码
if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/");
/* 通过正则,剔除哈希,剔除问号参数拼接,剔除文件名,最终得到前缀路径,并赋值给 __webpack_require__.p */
__webpack_require__.p = scriptUrl;
})();
/*
* 重点在于定义 __webpack_require__.l 这个函数
* 该函数传入完整的 url、chunkId 以及其它参数
* 被 __webpack_require__.f.j 函数调用
* */
/* webpack/runtime/load script */
(() => {
__webpack_require__.l = (url, done, key, chunkId) => {
// ...省略部分代码
var script = {}
// ...省略部分代码
script.src = url;
// ...省略部分代码
script.onerror = onScriptComplete.bind(null, script.onerror);
script.onload = onScriptComplete.bind(null, script.onload);
// ...省略部分代码
document.head.appendChild(script);
};
})();
/*
* 重点在于 __webpack_require__.f.j 这个函数
* 将第一步中得到的 __webpack_require__.p 和 chunk 组合得到完整的 url
* 然后调用 __webpack_require__.l 函数加载动态引入的库
* */
/* webpack/runtime/jsonp chunk loading */
(() => {
__webpack_require__.f.j = (chunkId, promises) => {
// ...省略部分代码
// 组装 URL
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
// ...省略部分代码
var error = new Error();
var loadingEnded = (event) => {
if(__webpack_require__.o(installedChunks, chunkId)) {
// ...省略部分代码
// 之前的报错就是在这里抛出的
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
error.name = 'ChunkLoadError';
error.type = errorType;
error.request = realSrc;
installedChunkData[1](error);
}
};
__webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId);
};
})();可以看出,动态加载大致的步骤:
- 通过
chunkId得到单独分包的那个库的文件名 - 通过最后一个有效的
script上的src属性和正则获取前缀路径 - 将前缀路径和要加载的文件名拼接,调用相关函数加载脚本
而 404 的原因就是出在获取 __webpack_require__.p 这个过程
由于默认它是去读取当前项目其它 js 的请求路径,然后作为 echarts.js 的前缀路径
而其它 js 一般是我们当前项目里打包出来的 js,一般都在一个统一的文件夹下
而这个文件夹下肯定不可能有 echarts.js,因为它在 tools 打包过程已经处理了,不会在项目中再打包一次
那么如何解决这个问题呢?最简单的方法就是直接把 echarts.js 放到正确的文件夹下
但是这样每次项目上打包后都要手动重新放一次,万一哪次忘记了就 gg
既然这样,那就在项目自己的 webpack 配置里加上 CopyPlugin 不就可以了
但是有一个问题,就是以后每个使用 tools 的项目加上这个配置
作为一个工具库,肯定是希望在保持代码结构整洁的前提下,使用的门槛越低越好,因此这样也不合适
想到是 __webpack_require__.p 获取的不对而出的问题,那是不是要想办法让它能获取到对的值呢
publicPath
查看官网发现,__webpack_require__.p 之所以会通过上述的逻辑获取
是因为 publicPath 这个属性在 targets 为 web 时的默认值是 auto
而我们在没有设置 browserslist 的情况下,target 的默认值刚好为 web: 
所以如果我们设置正确的 publicPath,是不是就有可能让 __webpack_require__.p 是对的,从而拼接出对的路径
设置
publicPath的方式有两种:
一种是
output内打包时就配置好另一种是通过
window.__webpack_public_path__ = 'xxx'来配置,这种可以达到动态配置的效果
因此尝试修改 publicPath 再打包试试
但是又有问题了,什么才是正确的路径呢,因为 tools 在打包后不清楚会被谁使用,因此正确的路径我们无法判断
因此在不对项目做任何入侵的情况下想要让这个路径正确,靠修改 publicPath 好像是不行
那如果在项目中使用 tools 时,将正确的 publicPath 传参过去可不可以呢
动态引入跨工程出错原因
项目中使用 tools 库的时候,echarts.js 是在 node_modules 中的 tools 文件夹内的
而在使用 tools 这个库的时候,由于是已经被打包的代码,因此已经完成依赖收集过程
因此在项目上使用 tools 时,不会再对该库里面的内容走依赖收集流程
因此项目在打包过程中不会处理 echart.js 这一文件
这也就意味着:
- 在开发环境时,
webpack-dev-server不会将echarts.js打包进内存,因此通过dev-server开启的服务器永远无法访问到这一文件
webpack-dev-server主要由express、webpack-dev-middleware等组成其中
webpack-dev-middleware以watch模式启动webpack并将每次打包后的内容缓存在内存中提供给服务器,为服务器提供静态资源而如果某一个文件没有进行依赖收集,就不会打包进内存,就无法通过
webpack-dev-server启动的服务器访问
webpack本身就支持watch模式,为何还要用另外一个插件提供静态资源给服务器呢?因为
webpack本身的watch模式每次都会生成实体文件,而webpack-dev-middleware不会生成文件,只会将其缓存在内存中(使用nodeJS的memory-fs模块)
- 在生产环境时,
echarts.js这个文件不会被单独打包进我们项目的dist目录
因此可以知道,就算在使用 tools 时给他传参,进而修改 publicPath,也没有办法在项目里自动引入对应 js
那这种动态导入就没法使用了么,很显然不是
假如我们把 tools 库里的代码移到我们项目里然后去执行的话,可以发现是能分别正常引入两个 js 的:
首先,由于动态引入的这部分代码是由项目本身打包的,因此会进行依赖收集,并将
echarts.js分包进项目的output目录因此无论是打包后还是开发模式下,都是可以读到这个文件的,而如果不做特殊处理,
echarts.js必定是和项目里打包的其它js放在一起,因此拼接的路径刚好是对的其次就算路径不对,我们也可以通过
publicPath及时修正
解决方案
因此我们可以假设,如果能让 import('echarts') 和 import('@antv/g2') 这部分代码原封不动放到项目里,让动态引入的库交给项目自己的打包工具处理,应该就没问题了
而想要让上述动态引入的语法不被打包,这里有两种方案:
- 第一种方案:依然使用
webpack
将动态引入相关的代码单独提取到一个文件内,例如叫
active-code.js然后在配置中增加
noParse,值填写active-code,那么在打包tools库时,这个文件就会被略过而不被打包但是需要注意,在库中引入自己本地的
js时,不能写相对路径,而应该带上库名例如
import('tools/lib/local.js'),因为引入的代码是未打包过直接交给项目处理的同时,应该通过
CopyPlugin将对应文件放入最终包的对应目录,例如 dist/lib/ 内另外我们在开发这个库过程中,如果用了本地的
js,由于直接写包名(tools/lib/local.js)因此肯定会
import不到这个包,这时需要用 NormalModuleReplacementPlugin 重定向资源
- 第二种方案:更换打包工具,使用
rollup
由于
rollup默认构建目标就是ES6,因此只需要很少的配置就可完成打包,并保留import语法
个人是推荐使用第二种方案,rollup 使用见rollup开发ESModule
