代码分割之动态import和webpack
无论在vue里,还是在react中,我们都知道通过动态import()可以按需加载组件、页面路由。那么,动态import是什么?用于哪里,怎么用,如何实现?跟着我一起揭开其面纱吧!
介绍
import()是一个“function-like”的动态模块引入,其现在处于TC39的提案中,且在4个月前,也就是2019年6月份,移到stage 4中,而chrome 61、edge16、ios10.3、firefox60、opera48、safari10.1版本以上就开始支持了,预计不久的将来会成为正式的语法标准。
现有的语法形式是静态声明的,他们只接受字符串字面量,不能是变量,因为其是在编译阶段对模块进行静态分析、打包、tree shaking,而非运行时。这对于90%的设计来说是great!然而,动态加载js模块的场景也存在很多,如多个模块择优选择加载、静态模块加载失败后通过动态引入来增加程序健壮性等等。但是现有的语法不支持动态加载js模块。
因此 Domenic Denicola 和module-loading社区产生了增加动态导入想法,并被tc39列入提案,提案中,建议增加import(specifier)语法形式,其用法类似于函数,却没有函数的某些特性。它返回一个Promise。
用法
<!DOCTYPE html>
<nav>
<a href="books.html" data-entry-module="books">Books</a>
<a href="movies.html" data-entry-module="movies">Movies</a>
<a href="video-games.html" data-entry-module="video-games">Video Games</a>
</nav>
<main>Content will load here!</main>
<script>
const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
link.addEventListener("click", e => {
e.preventDefault();
import(`./section-modules/${link.dataset.entryModule}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
});
}
</script>
我们可以看到
- import()可以用于script脚本中,而import只能用于模块module中(type="module"除外)
- 当import()用于模块中时,其在任意位置声明执行,不会被提升。而import则会被提升,导致会存在临时死区问题
- import()可以接受任意的字符串,而不是静态字符串。
- import()不会建立一个可静态分析依赖项
再举个例子
a.js
console.log('from static import')
let number = 0;
export default function count() {
number ++;
return number
}
main.js
import count from './a.js'
console.log('start----')
console.log(count())
import('./a.js').then(_module => {
console.log('a.js----')
console.log(_module.default())
})
import('./a-dynamic.js').then(_module => {
console.log('dy 1----')
console.log(_module.default())
})
import('./a-dynamic.js').then(_module => {
console.log('dy 2----')
console.log(_module.default())
})
console.log('end')
a-dynamic.js
console.log('from dynatic import')
let number = 0
export default function count() {
number ++;
return number
}
运行 main.js,在node中输出结果如下:
from static import
start----
1
end
a.js----
2
from dynatic import
dy 2----
1
dy 1----
2
我们更改main.js为如下
import('./a.js').then(_module => {
console.log('a.js----')
console.log(_module.default())
})
import('./a-dynamic.js').then(_module => {
console.log('dy 1----')
console.log(_module.default())
})
import('./a-dynamic.js').then(_module => {
console.log('dy 2----')
console.log(_module.default())
})
console.log('end')
import count from './a.js'
console.log('start----')
console.log(count())
其输出结果如下:
from static import
end
start----
1
a.js----
2
from dynatic import
dy 2----
1
dy 1----
2
通过以上例子我们可以看到
- 先静态引入a.js模块,再动态引入a.js模块,均调用count()函数,我们可以看到,a.js模块只会被加载一次。
- 先动态引入a.js模块,再静态引入a.js模块,静态声明被提升。动态引入不会被提升
- 两次动态导入a-dynamic.js,a-dynamic.js只会被加载一次,其运行顺序为dy2--->dy1 这个是出乎意料的
从上所示,输出顺序,不如我们期望看到的那样,那通过babel转换后的呢?我们看下,通过babel转换后的代码,如下:
"use strict";
require("core-js/modules/web.dom.iterable");
require("core-js/modules/es6.array.iterator");
require("core-js/modules/es6.string.iterator");
require("core-js/modules/es6.weak-map");
require("core-js/modules/es6.promise");
require("core-js/modules/es6.object.to-string");
var _a = _interopRequireDefault(require("./a.js"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; if (obj != null) { var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
Promise.resolve().then(function () {
return _interopRequireWildcard(require('./a.js'));
}).then(function (_module) {
console.log('a.js----');
console.log(_module.default());
});
Promise.resolve().then(function () {
return _interopRequireWildcard(require('./a-dynamic.js'));
}).then(function (_module) {
console.log('dy 1----');
console.log(_module.default());
});
Promise.resolve().then(function () {
return _interopRequireWildcard(require('./a-dynamic.js'));
}).then(function (_module) {
console.log('dy 2----');
console.log(_module.default());
});
Promise.resolve().then(function () {
return _interopRequireWildcard(require('./a-dynamic.js'));
}).then(function (_module) {
console.log('dy 3----');
console.log(_module.default());
});
console.log('end');
console.log('start----');
console.log((0, _a.default)());
babel将其转换成了Promise的写法,从上图我们可看出,编译后的代码,运行结果如下:

其执行顺序与转换前完全不一致。前者由于我们没有看node 中import()的实现,不知其具体原因,而后者,完全采用promise及通过weakMap 弱引用的方式设置缓存、防止内存消耗与泄露实现一次加载、多次执行。
好了,到这里,我们已经知道了,import()的用法,大致实现过程、以及import()用法上的注意点。接下来说下webpack如何通过import()实现代码分割
webpack import()代码分割
这期间,webpack充当什么角色呢?由于webpack实在babel对代码转换之前运行的,因此webpack会对代码进行解析,将调用import()之处作为分离的模块起点,并打包它和它引用的子模块分离到单独的chunk中。再通过babel转换器,进行转换。
完!
水平有限
如有理解错误之处,请指正~