一. 代码分离
代码分离是webpack最引人注目的特性之一,这个特性能够把代码分离到不同的bundle中。所谓bundle就是我们打包分离出来的文件,然后,我们将这些文件按需加载或者并行加载。代码分离可以用于获取更小的bundle以及控制资源加载的优先级。如果使用合理会极大的影响加载时间。
1. 代码分离方法
常用的代码分离方法:
1)配置入口起点
使用entry配置手动的分离代码。但是如果是多个入口,那么这些多个入口共享的文件会分别在每个包里面去重复打包。
2)防止重复
在入口的地方,通过entry dependencies 或者 splitChunksPlugin 去去重和分离代码。
3)动态导入
通过模块的内联函数 import 调用来分离代码。
2. 入口起点
这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患。我们将会解决这些问题。
1)src下新建another-module.js
import _ from 'lodash';
console.log(_.join(['Another', 'module', 'loaded!'], ''));
2)在webpack.config.js里面修改入口配置
module.exports = {
// entry: "./src/index.js", //入口文件配置
// 配置多入口
entry: {
index: "./src/index.js",
another: "./src/another-module.js",
},
... ...
}
3)执行npx webpack打包编译,结果报错:

上述错误表示多个入口文件使用的是同一个出口文件名 bundle.js,所以我们要分别对两个入口文件去起不同的名字。
4)修改webpack.config.js中的出口文件名
module.exports = {
// entry: "./src/index.js", //入口文件配置
// 配置多入口
entry: {
index: "./src/index.js",
another: "./src/another-module.js",
},
// 出口文件配置
output: {
// filename: "bundle.js", // 指定输出文件的文件名
filename: '[name].bundle.js', //[name] -> 取entry下的key
// path: './dist', //报错:必须为绝对路径
// 调用path模块的resolve方法
path: path.resolve(__dirname, "./dist"), //指定文件输入路径
// __dirname 表示获取到当前的webpack.config.js这个文件所在的物理路径
// "./dist" 表示基于前面的第一个路径去找到 dist文件夹
clean: true, //生成新文件之前,清空之前的文件
//资源模块的文件名,不但可以设置文件名,还可以设置路径
assetModuleFilename: "images/[contenthash][ext]", // [contenthash] 表示根据文件的内容去生成一个哈希的字符串,扩展名可以使用原资源的扩展名 [ext]
},
... ...
}
5)再次执行打包命令,编译成功

我们可以看到打包后的another.boundle.js文件比较大,是因为里面引入了lodash,lodash已经被打包到another,boundle.js中了。

启动服务在浏览器查看,结果正常。

上述another-module.js里面使用了lodash,如果另外一个入口文件也使用 lodash 呢?
6)在index.js中使用lodash
import _ from "lodash";
... ...
console.log(_.join(["Index", "module", "loaded!"], " "));
再次打包,可以看到index.boundle.js同样变的比较大。

启动服务在浏览器查看,结果显示正常:

虽然代码正常运行,但是lodash这个共享的代码却被分别打包到两个文件里面了。
3. 防止重复
相对于第一种方法,第二种方法可以将一些公用的文件抽离成单独的chunk。
1)修改webpack.config.js文件的入口配置
module.exports = {
// entry: "./src/index.js", //入口文件配置
// 配置多入口
// entry: {
// index: "./src/index.js",
// another: "./src/another-module.js",
// },
entry: {
index: {
import: "./src/index.js",
dependOn: "shared", //将一些共享的文件定义出来
},
another: {
import: "./src/another-module.js",
dependOn: 'shared'
},
shared: 'lodash', //当上述两个模块里面有lodash这个模块的时候,会抽离出来,取名为shared的chunk
},
... ...
}
2)打包编译,成功之后多了一个shared.bundle.js文件,就是lodash单独抽离的chunk

在app.html中将三个文件都引入了:

启动服务在浏览器查看,结果也正常显示。除了上述的方法,还有另外一种方法,就是使用webpack内置的一个插件split-chunks-plugin ,这个插件也可以把我们模块依赖的公共的文件抽离成单独的chunk。
3)修改webpack.config.js
module.exports = {
// entry: "./src/index.js", //入口文件配置
// 配置多入口
entry: {
index: "./src/index.js",
another: "./src/another-module.js",
},
//防止重复入口配置
// entry: {
// index: {
// import: "./src/index.js",
// dependOn: "shared", //将一些共享的文件定义出来
// },
// another: {
// import: "./src/another-module.js",
// dependOn: 'shared'
// },
// shared: 'lodash', //当上述两个模块里面有lodash这个模块的时候,会抽离出来,取名为shared的chunk
// },
... ...
... ...
//优化
optimization: {
minimizer: [
//实例化插件
new CssMinimizerPlugin(),
],
//代码分割
splitChunks: {
chunks: 'all'
}
},
};
4)重新打包编译

启动服务在浏览器查看,结果也正常显示。
4. 动态导入
当涉及到动态代码拆分时,webpack提供了两个类似的技术。第一种也是推荐选择的方式,即使用ECMAScript提案的 import()语法来实现动态导入。第二种,则是webpack的遗留功能,使用webpack特定的 require.ensure。
1)修改webpack.config.js文件:注释掉之前的配置,只留一个入口配置
module.exports = {
// entry: "./src/index.js", //入口文件配置
// 配置多入口
entry: {
index: "./src/index.js",
// another: "./src/another-module.js",
},
//防止重复入口配置
// entry: {
// index: {
// import: "./src/index.js",
// dependOn: "shared", //将一些共享的文件定义出来
// },
// another: {
// import: "./src/another-module.js",
// dependOn: 'shared'
// },
// shared: 'lodash', //当上述两个模块里面有lodash这个模块的时候,会抽离出来,取名为shared的chunk
// },
... ...
... ...
//优化
optimization: {
... ...
//代码分割
// splitChunks: {
// chunks: 'all'
// }
},
};
2)在src下新建async-module.js
function getComponent() {
return import('lodash')
.then(({ default:_}) => {
const element = document.createElement('div');
element.innerHTML = _.join(['hello', 'webpack'], ' ');
return element;
})
}
getComponent().then((element) => {
document.body.appendChild(element);
});
这样些目的是为了让import帮我们去抽离一个单独的lodash文件。
3)在index.js中引入
// import _ from "lodash";
import './async-module.js';
4)打包编译,成功抽离了一个文件

启动服务在浏览器查看,结果正常显示:

那么原来的静态导入的资源能不能和动态导入内容一起去做代码的抽离?
取消原来注释掉的内容:
import _ from "lodash";
import './async-module.js';
... ...
console.log(_.join(["Index", "module", "loaded!"], " "));
重新打包编译:发现抽离的文件不见了。

说明一旦我们加入了静态资源的时候,我们得需要在webpack.config.js里面打开相关配置:
module.exports = {
// entry: "./src/index.js", //入口文件配置
// 配置多入口
entry: {
index: "./src/index.js",
// another: "./src/another-module.js",
},
//防止重复入口配置
// entry: {
// index: {
// import: "./src/index.js",
// dependOn: "shared", //将一些共享的文件定义出来
// },
// another: {
// import: "./src/another-module.js",
// dependOn: 'shared'
// },
// shared: 'lodash', //当上述两个模块里面有lodash这个模块的时候,会抽离出来,取名为shared的chunk
// },
... ...
... ...
//优化
optimization: {
... ...
//代码分割
splitChunks: {
chunks: 'all'
}
},
};
重新打包编译,下面多了一个文件:

说明静态导入和动态导入都能够把公共的代码抽离出来了。
将webpack.config.js里面的两个入口配置都打开,重新打包编译,结果也正常显示。
5. 懒加载(动态导入应用)
懒加载或者按需加载,是一种很好的优化网页或者应用的方式,这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或者即将引用另外一些代码块,这样加快了应用的初始加载速度,减轻了它的总体积,因为某些代码块可能永远不会被加载。
1)在src下新建math.js,暴露两个方法
export const add = (x, y) => {
return x + y;
}
export const minus = (x, y) => {
return x - y;
}
2)在index.js中使用动态导入执行一下暴露的方法
const button = document.createElement("button");
button.textContent = '点击执行加法运算';
button.addEventListener("click", () => {
import('./math.js').then(({ add }) => {
console.log(add(4, 5));
});
});
document.body.appendChild(button);
3)执行打包编译命令,生成一个单独的bundle文件

启动服务在浏览器查看:

右击检查元素,打开app.html,点击网络,发现并没有刚才生成的src_math_js.bundle.js文件。点击执行加法运算按钮,src_math_js.bundle.js文件才被加载进来:

点击控制台,结果也正常显示:

说明我们这个文件在第一次页面加载的时候并不加载,只有在我们点击按钮的时候才加载,那如果用户从不点击按钮,那么这个模块就不在服务器上加载了,这样会节省我们网络流量。
实际上打包的src_math_js.bundle.js文件名是可以修改的,可以在import上加所谓的魔法注释:
const button = document.createElement("button");
button.textContent = '点击执行加法运算';
button.addEventListener("click", () => {
import(/* webpackChunkName: 'math' */'./math.js').then(({ add }) => {
console.log(add(4, 5));
});
});
document.body.appendChild(button);
打包编译,得到math.bundle.js

6. 预获取,预加载模块(动态导入应用)
webpack v4.6.0+增加了对预获取和预加载的支持。
在声明import时,使用下面这些内置指令,可以让webpack输出“resource hint(资源提示)”,来告知浏览器:
- prefetch(预获取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要的资源
预获取:在imort中新加一个魔法注释 webpackPrefetch:
button.textContent = '点击执行加法运算';
button.addEventListener("click", () => {
import(/* webpackChunkName: 'math', webpackPrefetch: true */'./math.js').then(({ add }) => {
console.log(add(4, 5));
});
});
document.body.appendChild(button);
重新编译,启动服务在浏览器查看。右击检查元素,点击app.html,查看网络,发现math.bundle.js其实已经加载下来了:

点击按钮,发现math.bundle.js又加载了一次,这个好像还不如刚才,那这个的意义是什么?
点击元素,点击展开head标签,发现多了一个ref为prefetch的属性,它把我们的math.bundle.js预加载下来了,它的意义在于当我们首页面的内容都加载完毕,在网络空闲的时候,再去加载我们打包好的math.boundle.js,这种方式比刚才的懒加载要优秀。

预加载:import中加魔法注释 webpackPreload
const button = document.createElement("button");
button.textContent = '点击执行加法运算';
button.addEventListener("click", () => {
import(/* webpackChunkName: 'math', webpackPreload: true */'./math.js').then(({ add }) => {
console.log(add(4, 5));
});
});
document.body.appendChild(button);
重新编译,启动服务在浏览器查看。右击检查元素,点击app.html,查看网络,发现math.bundle.js并没有加载下来:

点击按钮的时候才加载下来。它跟我们刚才的懒加载效果类似。
