前言
为什么需要打包?
平时开发时,项目里用到的是前端框架Vue、React,还会用到ES6模块化语法,还有Less/Sass等CSS预处理器等来开发,这些都需要编译成JS、CSS、HTML、静态资源等才能在浏览器运行,所以需要打包来完成这些处理。
有哪些打包工具呢?
- Grunt
- Gulp
- Parcel
- Webpack
- Rollup
- Vite
- …
Webpack目前最为流行
Webpack的介绍
官网的介绍:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
Webpack核心概念
webpack一般不配置也是可以用的,但是我们经常也需要用配置做一些调整,一般在项目的根目录下新建一个webpack.config.js来添加配置项
入口(entry)
entry就是打包的起点,进入入口后webpack会找出哪些模块和库是入口依赖的,每个依赖项随即被处理,最后输出到bundles文件中。
entry的默认值是./src/index.js,可以在配置文件中修改,可以有多个入口文件
module.exports={
//入口
entry:"./src/main.js",//绝对路径、相对路径均可
}
输出(output)
output就是告诉webpack它所创建的bundles应该输出到哪里,以及如何命名这些文件(默认值是./dist)
module.exports = {
//输出
output: {
//path是文件输出目录,必须绝对路径
path: path.resolve(__dirname, "../dist"),
//输出文件名
filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
clean: true, // 自动将上次打包目录资源清空
},
}
加载器 (loader)
loader让webpack能够处理那些非JavaScript文件
loader有两个属性:
- test:识别哪些文件需要转换
- use::这些文件用什么loader进行转换
module.exports={ //加载器 module:{ rules:[ { // 用来匹配 .css 结尾的文件 test: /\.css$/, /* use 数组里面 Loader 执行顺序是从右到左, css-loader:负责将 Css 文件编译成 Webpack 能识别的模块 style-loader:会动态创建一个 Style 标签,里面放置 Webpack 中 Css 模块内容 */ use: ["style-loader", "css-loader"], }, ... ] } }
插件(plugins)
扩展webpack功能,通过在webpack构建流程中注入钩子实现。const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports={ plugins:[ new HtmlWebpackPlugin({ // 新的html文件有两个特点:1. 内容和源文件一致 2. 自动引入打包生成的js等资源 template: path.resolve(__dirname, "../public/index.html"), }), ], }
模式(mode)
开发模式(development)
开发模式就是开发代码时使用的模式。
这个模式下我们主要做两件事:
1.编译代码,使浏览器能识别运行
开发时我们有样式资源、字体图标、图片资源、html 资源等,webpack 默认都不能处理这些资源,所以我们要加载配置来编译这些资源
2.代码质量检查,树立代码规范
提前检查代码的一些隐患,让代码运行时能更加健壮。
提前检查代码规范和格式,统一团队编码风格,让代码更优雅美观。
生产模式(production)
生产模式是开发完成代码后,我们需要得到代码将来部署上线。这个模式下我们主要对代码进行优化,让其运行性能更好。
优化主要从两个角度出发:
- 优化代码运行性能
- 优化代码打包速度
compiler对象
compilation对象
高级进阶
提升开发体验
SourceMap
SourceMap是什么?
SourceMap(源代码映射)是一个用来生成源代码与构建后代码一一映射的文件的方案。它会生成一个 xxx.map 文件,里面包含源代码和构建后代码每一行、每一列的映射关系。当构建后代码出错了,会通过 xxx.map 文件,从构建后代码出错位置找到映射后源代码出错位置,从而让浏览器提示源代码文件出错位置,帮助我们更快的找到错误根源。使用
开发模式:cheap-module-source-map
优点:打包编译速度快,只包含行映射
缺点:没有列映射
module.exports = {
// 其他省略
mode: "development",
devtool: "cheap-module-source-map",
};
生产模式:source-map
优点:包含行/列映射
缺点:打包编译速度更慢
module.exports = {
// 其他省略
mode: "production",
devtool: "source-map",
};
提升打包构建速度
HotModuleReplacement
HotModuleReplacement是什么?
HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面
用法
module.exports = {
// 其他省略
devServer: {
host: "localhost", // 启动服务器域名
port: "3000", // 启动服务器端口号
open: true, // 是否自动打开浏览器
hot: true, // 开启HMR功能(只能用于开发环境,生产环境不需要了)
},
};
//入口文件的js代码需要更改
// main.js
import count from "./js/count";
import sum from "./js/sum";
// 引入资源,Webpack才会对其打包
import "./css/iconfont.css";
import "./css/index.css";
import "./less/index.less";
import "./sass/index.sass";
import "./sass/index.scss";
import "./styl/index.styl";
const result1 = count(2, 1);
console.log(result1);
const result2 = sum(1, 2, 3, 4);
console.log(result2);
// 判断是否支持HMR功能
if (module.hot) {
module.hot.accept("./js/count.js", function (count) {
const result1 = count(2, 1);
console.log(result1);
});
module.hot.accept("./js/sum.js", function (sum) {
const result2 = sum(1, 2, 3, 4);
console.log(result2);
});
}
但是在实际开发中不会这么用的,一般用vue-loader、react-hot-loader
OneOf
OneOf是什么?
打包时每个文件都会经过所有 loader 处理,虽然因为 test 正则原因实际没有处理上,但是都要过一遍。比较慢。Oneof的作用就是只能匹配上一个 loader, 剩下的就不匹配了
用法
module.exports = {
entry: "./src/main.js",
output: {
path: undefined, // 开发模式没有输出,不需要指定输出目录
filename: "static/js/main.js", // 将 js 文件输出到 static/js 目录中
// clean: true, // 开发模式没有输出,不需要清空输出结果
},
module: {
rules: [
{
oneOf: [
{
// 用来匹配 .css 结尾的文件
test: /\.css$/,
// use 数组里面 Loader 执行顺序是从右到左
use: ["style-loader", "css-loader"],
}
...
]
}
]
}
}
Include/Exclude
Include/Exclude是什么?
开发时我们需要使用第三方的库或插件,所有文件都下载到 node_modules 中了。而这些文件是不需要编译可以直接使用的。所以我们在对 js 文件处理时,要排除node_modules 下面的文件
include,包含,只处理 xxx 文件
exclude,排除,除了 xxx 文件以外其他文件都处理
用法
module.exports = {
...
module: {
rules: [
{
oneOf: [
...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
loader: "babel-loader",
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
}),
...
],
...
};
Cache
Cache是什么?
每次打包时 js 文件都要经过 Eslint 检查 和 Babel 编译,速度比较慢。我们可以缓存之前的 Eslint 检查 和 Babel 编译结果,这样第二次打包时速度就会更快了。
module.exports = {
module: {
rules: [
{
oneOf: [
...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
},
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
}),
...
],
}
Thead
Thead是什么?
多进程打包,可以开启多进程同时处理 js 文件。
const TerserPlugin = require("terser-webpack-plugin");
// cpu核数
const threads = os.cpus().length;
module.exports = {
module: {
rules: [
{
oneOf: [
...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
use: [
{
loader: "thread-loader", // 开启多进程
options: {
workers: threads, // 数量
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
},
},
],
},
],
},
],
},
plugins: [
new ESLintWebpackPlugin({
// 指定检查文件的根目录
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
// 缓存目录
cacheLocation: path.resolve(
__dirname,
"../node_modules/.cache/.eslintcache"
),
threads, // 开启多进程
}),
...
],
...
};
减少代码体积
Tree Shaking
Tree Shaking是什么?
开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们可能只用上极小部分的功能。这样将整个库都打包进来,体积就太大了。Tree Shaking 就是移除 JavaScript 中的没有使用上的代码。
Webpack 已经默认开启了这个功能,无需其他配置。
Babel
Babe是什么?
Babel 为编译的每个文件都插入了辅助代码,使代码体积过大!Babel 对一些公共方法使用了非常小的辅助代码,比如 _extend。默认情况下会被添加到每一个需要它的文件中。可以将这些辅助代码作为一个独立模块,来避免重复引入。
@babel/plugin-transform-runtime: 禁用了 Babel 自动对每个文件的 runtime 注入,而是引入 @babel/plugin-transform-runtime 并且使所有辅助代码从这里引用。
用法
module.exports = {
module: {
rules: [
{
oneOf: [
...
{
test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules代码不编译
include: path.resolve(__dirname, "../src"), // 也可以用包含
use: [
{
loader: "thread-loader", // 开启多进程
options: {
workers: threads, // 数量
},
},
{
loader: "babel-loader",
options: {
cacheDirectory: true, // 开启babel编译缓存
cacheCompression: false, // 缓存文件不要压缩
plugins: ["@babel/plugin-transform-runtime"], // 减少代码体积
},
},
],
},
],
},
],
},
Image Minimizer
Image Minimizer是什么?
开发如果项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。
我们可以对图片进行压缩,减少图片体积。image-minimizer-webpack-plugin是用来压缩图片的插件
注意:如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。
用法
const ImageMinimizerPlugin = require("image-minimizer-webpack-plugin");
module.exports = {
...
plugins: [
optimization: {
minimizer: [
// 压缩图片
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
["gifsicle", { interlaced: true }],
["jpegtran", { progressive: true }],
["optipng", { optimizationLevel: 5 }],
[
"svgo",
{
plugins: [
"preset-default",
"prefixIds",
{
name: "sortAttrs",
params: {
xmlnsOrder: "alphabetical",
},
},
],
},
],
],
},
},
}),
],
},
};
优化代码运行性能
Code Split
Code Split是什么?
如果打包代码时会将所有 js 文件打包到一个文件中,体积会太大,应该实现当前渲染某一页就加载某一页的代码,所以需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。
用法
1.多个入口文件
//可以配置多个入口文件
module.exports = {
entry: {
main: "./src/main.js",
app: "./src/app.js",
},
}
配置多个入口文件,打包后会生成多个js文件(配置多少个入口,就会生成多少个js文件)
2. 提取重复代码
如果多入口文件中都引用了同一份代码,会导致代码重复,体积更大。可以提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它就好。
3. 按需加载,动态导入
想要实现按需加载,动态导入模块。
//点击事件发生时才导入
document.getElementById("btn").onclick = function () {
// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
import("./math.js").then(({ sum }) => {
alert(sum(1, 2, 3, 4, 5));
});
};
4. 单入口
开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。可以这样配置:
module.exports = {
...
optimization: {
// 代码分割配置
splitChunks: {
chunks: "all", // 对所有模块都进行分割
}
}
}
Preload / Prefetch
Preload / Prefetch是什么?
当按需加载时加载一个很大的资源时会发生卡顿,可以在浏览器空闲时间,加载后续需要使用的资源可以用上 Preload 或 Prefetch 技术
- Preload:告诉浏览器立即加载资源。
- Prefetch:告诉浏览器在空闲时才开始加载资源。
它们共同点是:
- 都只会加载资源,并不执行。
- 都有缓存。
它们区别:
- Preload加载优先级高,Prefetch加载优先级低。
- Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面资源,也可以加载下一个页面需要使用的资源。
总结: