前端模块打包工具 Webpack
打包工具解决的是前端整体的模块化,并不是单指JavaScript模块化
核心特性:
- 模块打包器(Module bundler)
- 模块加载器(Loader)
- 代码拆分(Code Splitting)
- 资源模块(Asset Module)
- 在webpack4.0以后支持0配置的方式直接启动打包
- 默认会将src/index.js 打包到 dist/main.js
- webpack.config.js是运行在node环境中的js文件
下面是一个简单的webpack.config.js文件
const path = require("path");
// 自定义入口和导出文件
module.exports = {
mode: "development", // 打包模式
entry: "./src/main.js", // 入口文件
output: {
filename: "bundle.js", // 输出文件
path: path.join(__dirname, "output"), // 输出文件所在目录
},
module: {
rules: [
{
test: /.css$/, // 匹配打包过程中遇到的文件路径
// 匹配到文件所使用的loader 并且是从后往前执行,先执行的要放在数组的最后面
use: [
"style-loader", // 把css-loader转换的结果通过style标签的形式追加到页面上
"css-loader" // 将css文件转换为js模块
]
},
{
test: /.png$/, // 打包png后缀的文件
use: {
// 使用该方式的话url-loader和file-loader都要安装
loader: "url-loader", // 使用url-loader, 超出limit的部分该加载器会调用file-loader
options: {
limit: 10 * 1024, // 限制大小不能超过10kb, 小文件使用url-loader
},
},
},
]
}
};
- 上面的代码中我们就使用到了很多加载器,我们可以对loader做一个分类
- 编译转换类
例如:css-loader
, - 文件操作类
例如:file-loader
- 代码检查类
注意:
- Webpack只是打包工具
- 加载器用来编译转换代码
核心工作原理
webpack会根据配置找到其中一个文件作为打包入口,一般都是js文件, 然后根据入口文件中的import,require语句解析推断出所依赖的资源模块,分别解析每个资源模块对应的依赖,就会形成一个所有文件的关系依赖树,最后webpack会递归这个依赖树,找到每个节点所对应的配置文件,根据配置文件中的rules属性找到模块对应的加载器,让加载器去加载这个模块,将结果放到打包结果文件当中
- Loader是Webpack的核心特性,通过不同的Loader可以加载任何类型的资源
开发一个Loader
- 综上我们自己开一个md的loader,以至于对Webpack的核心特性Loader的理解
- 先创建一个md文件
## 关于我
张轩赫, 一名手艺人~~~
- 接下来我们写Loader.js文件
// npm安装一个解析md文件的模块
const marked = require("marked");
// 每个Loader必须导出一个函数, 用source接收参数,然后return将处理结果输出
module.exports = (source) => {
// 解析markdown文件,解析为了html代码
const html = marked(source);
// 返回的必须是一个标准的js代码,返回的结果直接拼接到模块当中
return `module.exports = ${JSON.stringify(html)}`;
};
- 接下来我们配置一个webpack.config.js文件
const path = require("path");
module.exports = {
mode: "none", // 打包模式
entry: "./src/main.js", // 入口文件
output: {
filename: "bundle.js", // 打包后文件
path: path.join(__dirname, "dist"), // 打包后文件目录
},
module: {
rules: [
{
test: /.md$/, // md文件
use: "./markdown-loader.js", // 这里支持相对路径,使用自己开发的md-loader
},
],
},
};
- 最后我们执行打包指令
npx webpack
- 会多出一个dist文件夹且会打包出一个bundle.js文件,打包的第100行代码结果如下
/* 1 */
/***/ (function(module, exports) {
module.exports = "<h2 id=\"关于我\">关于我</h2>\n<p>张轩赫, 一名手艺人<del>~</del></p>\n"
/***/ })
/******/ ]);
至此一个简单的Loader功能就实现了,我们可以发现Loader的工作原理实际上就是文件从输入到输出的转换,对同一个文件可以依次使用多个Loader
插件机制
插件机制是Webpack中另外一个核心特性,目的是增强Webpack自动化能力,解决项目中除了资源加载以外的其他自动化工作
// 导入清除输出目录插件模块(大部分插件导出的都是一个类型)
// 该插件需要解构成员
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
// 默认导出的就是插件的类型
const HtmlWebpackPlugin = require("html-webpack-plugin");
plugins: [
// 清除输出目录
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: "webpack", // 配置属性title,可以自定义配置
filename: "index.html", // 输出的文件名,默认输出index.html
template: "./src/index.html", // 模板文件 相对路径
}),
new CopyWebpackPlugin({
patterns: [
{ from: "public", to: "./" }, // 将public目录下的所有文件拷贝到输出目录
],
}),
],
以上是我们会常用的三个插件,插件特性相比于Loader(加载器)拥有更宽的能力范围.
- 那么webpack的插件机制是怎么实现的呢???
其实,Plugin是通过钩子机制实现的,类似于web中的事件.
- plugin(插件)必须是一个函数或者一个包含apply方法的对象
开发一个插件
- 接下来我们自己开发一个简单的插件,该插件的作用是清除打包后js文件中多于的/ /代码
- 直接在webpack.config.js文件中定义
// 定义一个插件类,包含apply方法
class MyPlugin {
// 根据插件特性,要么是一个函数,要么是一个具有apply方法的对象. 参数complier是所有的配置信息,同时也需要这个对象去注册钩子函数
apply(complier) {
console.log("MyPlugin 启动");
// 访问到emit钩子,通过tap方法注册一个钩子函数
// 接收两个参数, 第一个参数是插件名称, 第二个参数是此次打包的上下文;
complier.hooks.emit.tap("MyPlugin", (compilation) => {
// 获取资源文件信息
for (let i in compilation.assets) {
console.log("打包后每个文件的名称" + i);
// 该插件只处理JS文件
if (i.endsWith(".js")) {
// 文件资源内容 通过source方法可以拿到文件内容
const contents = compilation.assets[i].source();
const withoutComments = contents.replace(
/\/\*\*+\*\//g,
""
);
// 定义的source,size这些方法必须是一个函数,否则打包失败
compilation.assets[i] = {
source: () => withoutComments,
// webpack要求必须有size这个方法,没有就报错
size: () => withoutComments.length,
};
// 其实就是重新定义了source,size方法
// compilation.assets[i].source = () => withoutComments;
// compilation.assets[i].size = () => withoutComments.length;
}
}
});
}
}
- 在webpack.config.js文件中直接使用
plugins: [
new MyPlugin(),
]
以上,一个webpack简单的插件就开发完成了!!!
- 那么现在我们就清楚的知道了,插件(plugin)是通过webpack生命周期的钩子函数中挂载函数实现的
Webpack-dev-server
我们现在编译了代码也打包了代码,现在当然是要模拟服务器来跑代码了
常用的插件有browser-sync, serve, http-server ... 很多很多.但是这些插件都有一个问题是,需要我们有两次的操作,先执行webpack打包,然后再执行服务器(就算是监听了webpack的行为配合browser-sync,也要开启两个控制台来跑)
哈哈,其实说了这么多,表达的意思就是,既然已经在用webpack,那么就用webpack的插件吧~~~
- webpack有一个插件集成了自动编译 和自动刷新浏览器等功能
- 安装了这个插件之后,这个插件有一个指令 webpack-dev-server
- 这个插件运行的结果并不会将结果打包到磁盘当中(没有dist目录), 他是将打包结果暂时存放在内存当中的,减少了磁盘独写操作
只要是webpack输出的文件都可以直接被访问
devServer: {
// 额外为开发服务器指定一个静态资源目录
contentBase: "./public",
},
可能有些人会问了, CopyWebpackPlugin 那个插件类不就是做了移动静态资源文件的工作吗?, 事实确实如此,但是在实际开发中,避免多次copy, 尽量不使用copy-webpack-plugin, 而是在devServer这个对象下配置一个contentBase. 只有在项目要测试上线前,再使用copy-webpack-plugin
- 在实际开发中应该也会经常遇到跨域的问题
- devServer通过配置可以在开发阶段解决跨域问题
配置如下:
devServer: {
proxy: {
// 请求路径前缀
"/api": {
// http://localhost:8080/api => https://api.github.com/api
target: "https://api.github.com", // 代理目标
pathRewrite: {
"^/api": "", // pathRewrite 代理路径的重写(会以正则的方式替换) 根据实际项目路径进行配置,通常为空
},
// 目前我的配置重写了路径之后相当于是, http://localhost:8080 => https://api.github.com
changeOrigin: true, // 不能使用localhost:8080 作为请求代理目标的 主机名
},
},
},
接下来我们在开发阶段通过/api 访问的 https://api.github.com 地址下的接口当将会是同源接口,不存在跨域问题
- 但是现在存在一个自动刷新的问题:
- 每次的代码修改都是通过页面的刷新更新到页面中, 但是我们希望页面不刷新的情况下,模块也可以及时刷新(要求真多),这样开发起来才更方便
热更新
HMR(Hot Module Replacement)
模块热替换(模块热更新): 应用运行过程中实时替换某个模块,应用状态不受影响
这么强大的功能,如何启动呢?
很好的是HMR已经集成在了webpack-dev-server中,我们不需要再安装其他模块了
- 使用指令 webpack-dev-server --hot 可以开启
- 在配置文件通过配置开启
const webpack = require("webpack");
module.exports = {
// 省略其他配置
devServer: {
hot: true,
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
}
- 通过简单的配置即可实现样式的热更新, 但是修改JS代码还是刷新了网页,这是怎么回事呢?
因为style-loader内部已经实现了样式的热更新. 有些人可能会说,项目中啥也没配置也一样可以热更新啊?
因为用了框架,然后框架内部做了处理,所以可以达到js热更新
- 如果我们要配置则需要在入口文件里设置
- 也就是main.js文件内配置 HMR API
// 注册一个模块更新后的处理事件
module.hot.accept("依赖模块的路径", () => {
// 处理函数 这里是修改后重新赋值的一些逻辑处理
console.log("模块更新了");
});
- HMR注意事项
- 处理 HRM 的代码报错会导致自动刷新
修改开启热更新关键字即可
devServer: {
// hot: true, // 开启热更新报错刷新页面
hotOnly: true, // 报错后不刷新页面
},
- 没有启用 HMR 的情况下, HMR API报错,没有accept
在webpack.config.js配置文件中没有开启热更新导致的(上面代码的参数值没有设置为true)
// 先判断是否存在是否开启插件 存在这个module.hot对象
if(module.hot) {
// 注册一个模块更新后的处理事件
module.hot.accept("依赖模块的路径", () => {
// 处理函数 这里是修改后重新赋值的一些逻辑处理
console.log("模块更新了");
});
}
- 代码中多了一些与业务无关的代码
生产环境注释掉该插件即可,正如官网文档所说,千万不要在生产模式中使用该插件
plugins: [
// 注释掉这个插件 处理热更新逻辑不会被打包
// new webpack.HotModuleReplacementPlugin(),
],
不同环境下的配置
一般有两种方式:
- 配置文件根据环境不同导出不同配置
- 一个环境对应一个配置文件(多文件配置)
- 在配置文件中配置不同配置:
const webpack = require("webpack")
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const CopyWebpackPlugin = require("copy-webpack-plugin")
// 传入的, 环境名参数,argv运行环境的所有参数
module.exports = (env,argv) => {
const config = {
mode: "development",
entry: "./src/main.js",
output: {
filename: "js/index.js",
},
devtool: "source-map",
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: "file-loader",
options: {
outputPath: "img",
name: "[name].[ext]"
}
}
},
],
],
},
plugins: [
new HtmlWebpackPlugin({
title: "Webpack HMR",
template: "./src/index.html",
}),
// new webpack.HotModuleReplacementPlugin(), // 使用热替换插件
],
devServer: {
// hot: true, // 开启热更新
hotOnly: true, // 报错后不刷新页面
},
};
// 判断是不是生产环境
if(env === "production") {
config.mode = "production"
config.devtool = false
config.plugins = [
...config.plugins,
// 下面两个插件时在开发阶段可以省略的插件
new CleanWebpackPlugin(), // 清空打包目录
new CopyWebpackPlugin({
patterns: [
{ from: "public", to: "./" }, // 将public目录下的所有文件拷贝到输出目录
],
}),
]
}
return config
}
- 多文件配置
- 要一个公共的配置文件,随便定义名称比如 webpack.common.js,里面设置基础的配置内容
- 后重新定义一个webpack.prod.js的文件,用来打包生产模式
// 引入公共配置文件
const common = require("./webpack.common");
// 生产环境下需要的两个插件
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const copyWebpackPlugin = require("copy-webpack-plugin");
// webpack中合并参数的方法,结构出来合并的方法
const { merge } = require("webpack-merge");
module.exports = merge(common, {
mode: "production",
plugins: [
new CleanWebpackPlugin(),
new copyWebpackPlugin({
patterns: [
{ from: "public", to: "./" }, // 将public目录下的所有文件拷贝到输出目录
],
}),
],
});
最后执行命令 进行指定文件的打包
npx webpack --config webpack.prod.js
指令较长,可以将这个指令设置在package.json文件的scripts对象内
"build": " webpack --config webpack.prod.js"
就这样,一个多文件不同环境的打包配置就完成了
webpack内置的优化功能
- DefinePlugin
- 该插件的作用是为代码注入全局成员
- 该插件会自动启用,会向全局注入一个
process.env.NODE_ENV
常量,来表示当前的运行环境
- Tree-Shaking
- 在生产模式下自动开启,清除没有用到的代码
- 实现原理:
- 在
webpack
的配置文件内添加一个optimization
对象
- 在
// 集中配置webpack内部优化功能
optimization: {
usedExports: true, // 只导出外部使用的代码,
minimize: true, // 移除未引用过的代码并压缩
sideEffects: true, //检查package.json中有没有 sideEffects这个字段,值是标识有副作用的代码文件,作用不打包没有标识且没有用到的模块
},
sideEffects
需要在package.json文件中配置参数来标识有副作用的代码,使得这些代码会被打包
"sideEffects": [
"./src/extend.js",
"*.css"
]
代码分割
- 之前我们都在说
webpack
是将所有资源文件打包到一个文件里,现在又要分割开??? - 正所谓物极必反,资源太大不行,太散也不行,所以模块打包时极其有必要的
- 根据设置的规则打包到不同的
bundle.js
中
代码分割的两种方式:
- 多入口打包
- 动态导入
- 多入口打包
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "none",
// 多入口配置
entry: {
index: "./src/index.js",
album: "./src/album.js",
},
// 多出口配置(动态参数)
output: {
filename: "[name].bundle.js",
},
module: {
rules: [
{
test: /.css$/,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: "Multi Entry index",
template: "./src/index.html",
filename: "index.html",
chunks: ["index"], // 每一个打包入口会形成一个独立的chunk
}),
new HtmlWebpackPlugin({
title: "Multi Entry album",
template: "./src/album.html",
filename: "album.html",
chunks: ["album"], // 每个页面配置不同的chunk
}),
],
};
在不同的入口文件中肯定存在公共的模块,如果项目很大,那么就会有很多重复的引入代码,这肯定是不好的
我们只需要在webpack优化配置中添加一个参数即可
optimization: {
splitChunks: {
chunks: "all", // 提取所有的公共模块到一个文件中
}
}
- 动态导入
让模块实现按需加载,提高响应速度
动态导入的模块会被自动分包
只要使用按需加载的方式导入模块,webpack打包时会自动处理分包和按需加载
import("./模块").then((module) => {
console.log(module)
});
只不过这样打包出来的文件会以1、2、3数字开头,如果你想自定义打包后文件名称则可以使用魔法注释
import(/* webpackChunkName: "index" */ "./模块").then((module) => {
console.log(module)
});
// 这样打包后的js文件就会以index开头,替换默认的1、2、3这些数字开头
mini-css-extract-plugin
将css代码提取出来
如果使用mini-css-extract-plugin
这个插件的话就不需要style-loader
这个加载器,因为style-loader
是将样式通过style标签注入,使用了mini-css-extract-plugin
插件提取出的css文件通过link引入到文件中,
但是只单纯使用mini-css-extract-plugin
提取css代码的话,提取出来代码是未被压缩过的,因为webpack默认只会压缩js的文件,其他的文件都需要插件支持,同样的这里还需要结合另外一个插件optimize-css-assets-webpack-plugin
来压缩打包后的css代码
官方推荐将该插件定义在minimizer数组中
optimization: {
// 定义minimizer数组的话会认为自定义使用压缩插件 , 所以这里还需要再引入webpack内置压缩js的插件
minimizer: [
new OptimizeCssAssetsWebpackPlugin(), // 压缩打包后的css文件
new TerserWebpackPlugin(), // 压缩打包后的js文件
],
},
输出文件名Hash
缓存策略中,失效时间过长的话,一旦应用发生更新,不能及时更新到客户端,为了解决这个问题,我们可以在生产模式下的文件名使用Hash,一旦文件有变化,文件名称也会发生变化[hash]
所有的文件拥有同样的hash值[chunkhash]
同一路径的打包hash值相同[contenthash]
文件级别的hash,不同的文件不同的hash
最优的方式: 使用contenthash
output: {
filename: "[name]-[contenthash].bundle.js", // contenthash修改哪个文件打包后哪个hash值才改变
},
知识点挺多的
非常的详细,把webpack的工作思路说得很清楚