Webpack之恋(6)-按需拆分代码

楔子

Webpack支持两种按需拆分,切入点分别是import()require.ensure(),首选前者,它是ECMAScript的建议,import也是es6的关键词;后者是webpack所特有的。
webpack

动态导入import

Webpack把import()作为拆分点,把导入的模块放在单独的chunk中。import()接收模块名作为参数并返回Promise:import(name) -> Promise
用于演示的目录结构:

demo
├── index.html
├── index.js
└── webpack.config.js

index.js内容

//index.js
function test(){
import('moment').then(function(moment){
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
}).catch(function(err){
console.log(err);
});
}
test();

webpack.config.js内容

module.exports = { 
entry: './index.js',
output: {
filename: 'dist.js',
},
module: {
rules: [{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: 'es2015',
plugins: ['syntax-dynamic-import']
}
}]
}]
}
}

index.html引用的内容

<!doctype html>
<script src="0.dist.js"></script>
<script src="dist.js"></script>

由于使用动态的导入,因此需要使用插件syntax-dynamic-import,安装相关类库

npm install --save-dev babel-core babel-loader babel-plugin-syntax-dynamic-import babel-preset-es2015 webpack
npm install --save moment

执行webpack并用浏览器打开index.html从控制台可见打印

May 2nd 2017, 3:35:39 pm dist.js:160:5

自从webpack2.4开始,可以在动态导入中使用魔术注释来指定模块的chunk名字,格式

import(/* webpackChunkName: "my-chunk-name" */ 'module');

如修改上面的index.js内容为

function test(){
import(/* webpackChunkName: "my-chunk-name" */'moment').then(function(moment){
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
}).catch(function(err){
console.log(err);
});
}
test();

执行webpack可以从输出看到main的chunk和注释定义的my-chunk-name

Hash: dbaf8e1f1b79f4dff368
Version: webpack 2.4.1
Time: 4477ms
Asset Size Chunks Chunk Names
0.dist.js 606 kB 0 [emitted] [big] my-chunk-name
dist.js 6.12 kB 1 [emitted] main
[0] ./~/moment/moment.js 140 kB {0} [built]

问题 & 解决

publicPath

现在修改下项目结构为

demo
├── app
│   └── index.html
├── src
│   └── index.js
└── webpack.config.js

各文件内容如下

// src/index.js
function test(){
import(/* webpackChunkName: "my-chunk-name" */'moment').then(function(moment){
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
}).catch(function(err){
console.log(err);
});
}
test();

// webpack.config.js
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname,'app/assets/js'),
filename: 'dist.js',
},
module: {
rules: [{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: 'es2015',
plugins: ['syntax-dynamic-import']
}
}]
}]
}
}
// app/index.html
<!doctype html>
<script src="assets/js/0.dist.js"></script>
<script src="assets/js/dist.js"></script>

执行webpack后用浏览器预览index.html,不论是你本地浏览(file://)还是服务器浏览(http://),你将会看到类似这样的提示:

Error: Loading chunk 0 failed.
堆栈跟踪:
onScriptComplete@file:///home/james/mine/temp/j-s/demo/app/assets/js/dist.js:98:24

这是就需要设置output.publicPath属性,那么它与output.path属性有上面却别呢?output.path属性是生成的文件存放的物理路径,而output.publicPath是被引用的路径。由于index.html的引用路径是assets/js,因此把webpack.config.js的output.publicPath设置为assets/js即可。

// webpack.config.js
const path = require('path');

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname,'app/assets/js'),
filename: 'dist.js',
publicPath: 'assets/js/'
},
module: {
rules: [{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: 'es2015',
plugins: ['syntax-dynamic-import']
}
}]
}]
}
}

而之前没设置output.publicPath也可以使用,纯属路径巧合。再来理解下publicPath

publicPath: "https://cdn.example.com/assets/", // 使用https协议的CDN
publicPath: "//cdn.example.com/assets/", // 与当前协议相同(http或https)的CDN
publicPath: "/assets/", // 服务器绝对路径
publicPath: "assets/", // 相对于html的相对路径
publicPath: "../assets/", // 相对于html的相对路径
publicPath: "", // 与html同级目录

浏览器兼容

当你使用import时,旧浏览器可能不兼容,你需要使用polyfill如es6-promise 或 promise-polyfill来适配(shim)。因此你需要在入口文件

import Es6Promise from 'es6-promise';
Es6Promise.polyfill();
// or
import 'es6-promise/auto';
// or
import Promise from 'promise-polyfill';
if (!window.Promise) {
window.Promise = Promise;
}

ES2017 async/await

当结合es2017的async/await使用import时,需要更多的插件

npm install --save-dev babel-plugin-transform-async-to-generator babel-plugin-transform-regenerator babel-plugin-transform-runtime

// index.js
async function determineDate() {
const moment = await import('moment');
return moment().format('MMMM Do YYYY, h:mm:ss a');
}

determineDate().then(str => console.log(str));
// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'dist.js',
},
module: {
rules: [{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: 'es2015',
plugins: [
'syntax-dynamic-import',
'transform-async-to-generator',
'transform-regenerator',
'transform-runtime'
]
}
}]
}]
}
};

require.ensure

require.ensure的格式如下

require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)

  • dependencies是字符串数组,声明在callback被执行之前可用的模块;
  • callback是当dependencies被加载完毕可用状态后,webapck执行的回调函数;虽然callback的参数是实现require函数的,但这个形参的名字却不能随意,必须是require;
  • errorCallback是可选项,当webpack加载dependecies失败时执行;
  • chunkName可选项,指定chunk的名字;

webpack编译时静态解析require.ensure,依赖(dependency)以及在回调函数中require的资源都会被添加一个新的chunk中。

demo

演示项目的结构

ensure-demo/
├── index.html
├── index.js
└── webpack.config.js

各文件内容

// index.js
function test(){
require.ensure([],function(require){
var moment = require('moment');
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
});
}
test();

// webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: 'dist.js'
},
module: {
rules: [{
test: /\.js$/,
use: [{
loader: 'babel-loader',
options: {
presets: 'es2015'
}
}]
}]
}
}
// index.html
<!doctype html>
<script src="0.dist.js"></script>
<script src="dist.js"></script>
dependencies
function test(){
require.ensure(['./load.js'],function(require){
var moment = require('moment');
console.log(moment().format('MMMM Do YYYY, h:mm:ss a'));
});
}
test();

dependencies是不被执行的,如load.js,只是作为callback可执行的条件,如果需呀执行load.js需要require(‘./load.js’)

系列终止

鉴于目前官方已经链接有Webpack中文文档,本系列将到此终止,感谢关注.

翟前锋 wechat
欢迎订阅我的微信公众号:zhaiqianfeng!