『译』Webpack Import 异步加载

原文链接:Webpack and Dynamic Imports: Doing it Right

webpack和异步加载

在webpackv1中,我们推荐使用AMD的require或者webpack的require.ensure来实现动态加载模块。但在这篇文章,我们将介绍webpackV2支持的ES2015 dynamic import方式来加载模块,需要用到babel插件和webpack一些特性。

基础用法

语法比较简单:

1
import("module/foo").then(foo => console.log(foo.default))

上面的代码将在运行时加载foo模块,打印出模块的默认输出。import接收一个字符串参数。

动态加载模块的语法

假设你的应用在移动端和PC端有不用的展现方式,只有一种响应式的设计不能满足要求的,那么就需要在不同的设备上加载不同的页面去渲染。作为一个聪明的开发者,如果用户使用的是移动端那么你是不希望去加载PC端的代码,反之亦然。那么将会用以下方式处理:

1
2
3
4
5
6
7
8
9
export default function pageLoader(platform) {
switch (platform="desktop") {
case "mobile":
return import("components/MyMobileComponent");

case "desktop":
return import("components/MyDesktopComponent");
}
}

“但是,这只是一个简单的例子,真正项目里不会只有一个页面要处理”

所以,尽管我们使用了组件的动态加载方式,但还是不够灵活,让我们重构一下代码:

1
2
3
4
5
6
7
8
export default function pageLoader(platform, componentName) {
switch (platform="desktop") {
case "mobile":
return import(`components/mobile/${componentName}`);
case "desktop":
return import(`components/desktop/${componentName}`);
}
}

“还是不太好,应用不只是支持这两个设备,以后可能会支持更多的设备”

1
2
3
4
5
const load = (platform="desktop") => componentName => import(`components/${platform}/${componentName}`);
export const loadDesktopComponent = load("desktop");
export const loadMobileComponent = load("mobile");

export default load;

在这个例子中,我们采用了更优雅的方法。可以适用任何我们想用的平台。

加载文件存在的问题

在webpack中通常我们处理图片的方式是使用file-loader,file-loader会将文件路径映射到模块内。

1
import(`assets/images/${imageName}.jpg`).then( src => ... )

现在存在的问题是:如果你想要动态加载一个文件,像这个例子中的图片,webpack会生成一个额外的chunk:

1
2
3
4
5
6
7
8
9
10
11
webpackJsonp([4],{

/***/ 850:
/***/ (function(module, exports, __webpack_require__) {

module.exports = __webpack_require__.p + "6089b36e59a28c41600c17626366cde0.jpg";

/***/ })

});
//# sourceMappingURL=4.chunk.a6e0a95123529f9afcc5.js.map

所以最大的问题是: 当你请求一个异步加载的图片时,会先发一个网络请求去请求该模块的chunk,再去请求图片。每个图片都需要请求两次才能加载成功,如果使用https的话那么就更糟糕了。

那么如何解决这个问题呢:

webpack“神奇的注释”

webpack为动态引入增加了一个非常好的特性:神奇的注释。在配置中添加一些注释,以告诉webpack如何创建并加载该chunk。

Webpack Mode

1
import(/* webpackMode: "eager" */ `assets/images/${imageName}.jpg`)

这将使webpack把该异步加载的chunk加入到其父chunk中,不再去单独创建一个chunk。这种方式下,所有的文件路径在父chunk加载的时候就加载进来了。
webpack有四种模式(lazy,lazy-once,eager,weak)来处理异步加载。具体看这里

Webpack Chunk名称

“hey,我注意到webpack将异步加载的模块命名为数字,这样调试起来有点麻烦,因为我不知道某个特定的chunk是否加载!”

我们既然可以控制加载方式,那么也可以通过注释去改变chunk的名称,例如:

1
2
import(/* webpackChunkName: "foo-image" */ "assets/images/foo.jpg");
import(/* webpackChunkName: "bar-module" */ "modules/bar");

Prefetch/Preload

注:这个特性在webpackV4.6之后才有

如果你使用的是http2,那么最好将大chunk分割成小的chunk。所以,最好的方式是将异步加载的chunk和父chunk分开并且在请求图片前加载好图片的chunk。
那就需要用webpackPrefetch: true来替代webpackMode: eager。

“那么Prefetch和Preload有什么区别吗?”

webpack的文档解释的更详细:

  • A preloaded chunk starts loading in parallel to the parent chunk. A prefetched chunk starts after the parent chunk finish.
    preload:异步chunk和其父chunk是并行加载的。prefetch:异步chunk是在父chunk加载完之后再加载的。
  • A preloaded chunk has medium priority and instantly downloaded. A prefetched chunk is downloaded in browser idle time.
    preload:异步chunk在浏览器中有中等优先级,会立即下载。prefetch:异步chunk会在浏览器空闲的时候下载。
  • A preloaded chunk should be instantly requested by the parent chunk. A prefetched chunk can be used anytime in the future.
    preload:异步chunk在父chunk加载完之后会立刻发起请求。prefetch:异步chunk会在父chunk加载完之后的任意时间发起。
  • Browser support is different.

浏览器支持是有差异的

因为prefetch chunk会在浏览器空闲的时候加载,所以可以加数字注释说明chunk加载的顺序。

1
2
3
4
5
6
// 0 is same as true
import(/* webpackPrefetch: 0 */ "assets/images/foo.jpg");
// this loads first as 1 > 0 (or true)
import(/* webpackPrefetch: 1 */ "modules/bar");
// this one will be the last!
import(/* webpackPrefetch: -100 */ "modules/slowpoke");

顺序将是:bar>foo>slowpoke。

深入研究代码分割

如果你想要深入研究在单页应用中是如何进行懒加载的,可以看我之前的两篇文章,虽然是用react作为例子的,你可以在任何基于SPA的框架/库中运用相同的思想:

Lazy Loading with React and Webpack 2

Lazy Loading with React + Redux and Webpack 2

结束语

代码分割是一个强大的功能,可以使应用变得更快,更智能的去加载依赖关系。但是正如uncle Ben曾经说的:”with great power comes great responsibility” 。了解工具是如何工作的才能最大限度发挥他的性能,希望这篇文章能对你有所帮助!