什么是webpack
webpack是什么,官网中是这么说的。
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
从上面的概念中,我们可以看出webpack是一个会把应用程序中具有依赖关系的每个模块打包成一个或者多个bundle的工具。既然是工具,就是代替人工完成某些复杂的操作,减少开发工作的繁琐,所以我们需要掌握的就是如何使用它。
但我们在使用之前,需要明白文中的几个概念:
-
什么是模块:了解Node的同学都知道,在Node的世界里,万物皆模块;那当然,基于Node的产物webpack对模块的定义和node是类似的,模块可大可小,大到一个项目一个工程,小至一个文件甚至一行代码。
-
什么是budle:和模块相比,budle的定义则显得比较局限,它是指打包后的资源文件,一个文件就是一个budle,一个前端工程可以打包成一个文件或者多个文件的文件夹,这就是概念中提到的“一个或多个 bundle”。
此外,除了上述的两个概念之外,这里还需要在补充几个相关概念。
-
hash:hash是和项目相关的hash值(一个项目一个hash值),是webpack用来给标记budle文件的唯一标识,也是监测该资源文件在二次打包时是不是要覆盖的依据,通常用固定字节的字符串来表示。
-
chunk/chunkhash:chunk是指代码块,也可以泛指为1个chunk=1budle。
我们知道一个打包资源中会有多个budle文件,如果我们修改其中一个,那和项目相关的hash值就会统一改变,而那些没有修改过的、已打包好的、并且已经缓存的budle文件也会因此失效,需要重新打包,为了解决这个问题,就出现了chunkhash的概念,一个budle文件就对应一个chunkhash。
-
contenthash:和chunkhash相比,contenthash的粒度就更细了,它主要是针对一个bundle中的内容,如果内容不变,contenthash就不变,所以我们通常会把项目中的js和css分离开来,对于css的打包文件命名我们就采用contenthash。
-
为什么要用webpack
-
前后端分离开发模式是催化剂
随着开发模式的转变,现在的前端开发已经不是想以往那些php、jsp那些,但凡懂点html/css的后端程序员都可以上手做的,现在的前端已经开始专业化,而我们的代码也是以工程的形式存在,你可以使用任何框架和技术,但是最后跑在用户浏览器上的东西还是和以前一样,就是静态资源和html,而从框架代码到浏览器识别的资源之间的转化就是由webpack来完成的。
-
工程化、模块化的产物
工程化、模块化是我们研发代码的一种重要思想,同时也正是这种思想的存在,才会在前端领域中衍生出那么多框架和技术,比如:Vue、React、Angular等工程化框架,TS、less、Scss等各种语言的扩展,这些都是提高我们开发效率的利器,但是同时这些语言和框架是不能被浏览器所识别的,怎样让我们的代码更优雅的被浏览器识别并运行,这就是webpack的主要职责。
webpack的安装
环境准备
webpack是基于nodek开发的工程化工具,所以开发环境支持node,并且node版本和webpack的匹配也很重要,具体参见webpack官网的发布说明,尽量使用最新版本以提高打包速度。
全局安装 不推荐
# 安装webpack V4+版本时,需要额外安装webpack-cli
npm install webpack webpack-cli -g
# 检查版本
webpack -v
# 卸载
npm uninstall webpack webpack-cli -g
项目安装 推荐
# 安装最新的稳定版本
npm i -D webpack
# 安装指定版本
npm i -D webpack@<version>
# 安装最新的体验版本 可能包含bug,不不要⽤用于⽣生产环境
npm i -D webpack@beta
# 安装webpack V4+版本时,需要额外安装webpack-cli
npm i -D webpack-cli
webpack的构建
默认配置
首先我们先看一下webpack的默认配置代码,如下:
const path = require('path')
module.exports = {// 必填,webpack的执行入口entry: './src/main.js',output: {// 合并输出的文件名filename: 'main.js',// 合并输出的绝对存放地址path: path.resolve(__dirname, './dist')}
}
其中,我们需要注意的就是
- webpack默认只支持JS模块和JSON模块
- 支持CommonJS Es moudule AMD等模块类型
- webpack4⽀持零配置使⽤,但是可用性很差,一般都需要额外扩展配置
执行构建
可以通过这两种方式进行项目构建
# 方式1:npx方式
npx webpack# 方式2:npm script
# 首先需要在package.json中添加test指令对应的执行脚本
----------------------------------------------"scripts":{ "test": "webpack" // 原理就是通过shell脚本在node_modules/.bin⽬录下创建⼀个软链接},
----------------------------------------------
# 然后在执行如下脚本
npm run test
构建成功后会发现⽬录下多出⼀个 dist ⽬录,⾥⾯有个 main.js ,这个⽂件是⼀个可执行的JavaScript文件,里面包含webpackBootstrap启动函数。
SourceMap
源代码与打包后的代码的映射关系,通过sourceMap定位到源代码。
在dev模式中,默认开启,关闭的话可以在配置文件⾥ devtool:"none"
// devtool的介绍 https://webpack.js.org/configuration/devtool#devtool
eval: 速度最快,使用eval包裹模块代码,
source-map: 产生.map文件
cheap: 较快,不包含列信息
Module: 第三⽅模块,包含loader的sourcemap(比如jsx to js, babel的sourcemap)
inline: 将.map作为DataURI嵌入,不单独生成.map⽂件
配置推荐:
devtool:"cheap-module-eval-source-map",// 开发环境配置// 线上不推荐开启
devtool:"cheap-module-source-map", // 线上生成配置
WebpackDevServer
webpack-dev-server提升开发效率的利器,可以解决重复打包生成dist目录,这是因为devServer把打包后的模块不会放在dist目录下,二是放到内存中,从而提升速度。
# 安装
npm install webpack-dev-server -D# 修改package.json配置
"scripts": {"server": "webpack-dev-server"
},# 修改webpack.config.js配置
devServer: {contentBase: "./dist", open: true,port: 8081
}# 启动
npm run server
跨域问题的解决
- 设置服务器代理
devServer: {......proxy: {"/api": {target: "http://localhost:9092"}}
}
- 本地mock server
devServer: {......// before()是devServer的钩子函数,除了before之外还有after,本质上只有执行时间顺序的问题// app是一个本地的node服务,可以是express,也可以是koa,这里是expressbefore(app, server) {app.get('/api/mock.json', (req, res) => {res.json({hello: 'world'})})}
}
Hot Module Replacement (HMR:热模块替换)
-
启动hmr
devServer: {contentBase: "./dist",open: true,hot: true, // 开启HMRhotOnly: true // 即便HMR不生效,浏览器也不自动刷新,就开启hotOnly },
-
配置webpack.config.js
const webpack = require("webpack");// 在插件配置处添加 plugins: [new CleanWebpackPlugin(),new webpack.HotModuleReplacementPlugin() ]
-
注意启动HMR后,css抽离会不生效,还有不支持contenthash,chunkhash, 建议使用style-loader将css处理到html中
-
处理js模块HMR需要使用module.hot.accept来观察模块更新,从⽽更新
if (module.hot) {// path为需要监听变化的js的相对路径module.hot.accept(path, function() {// 执行操作,重新渲染修改后的js效果}); }
webpack的基本概念
entry
指定webpack打包⼊口文件,webpack 执⾏构建的第一步将从Entry开始,可理解成输入,有三种不同的输入方式
// 字符串: 单入口
entry: "./src/index.js",// 数组: 单入口,最终将数组文件合并成一个
entry: ["./src/index.js", "./src/other.js"],// 对象: 多入口
entry: { index: "./src/index.js",other: "./src/other.js"
}
output
打包转换后的⽂件输出到磁盘位置:输出结果,在webpack经过⼀系列处理并得出最终想要的代码后输出结果。
// 默认处理(单入口)
output: {filename: "bundle.js",// 输出⽂文件的名称path: path.resolve(__dirname, "dist")// 输出⽂文件到磁盘的⽬目录,必须是绝对路路径
},// 多⼊口的处理
output: {filename: "[name][chunkhash:8].js",// 利用占位符,⽂件名称不要重复path: path.resolve(__dirname, "dist")// 输出⽂件到磁盘的目录,必须是绝对路径
},
mode
mode用来指定当前的构建环境,主要取值有:
- production // 生产环境的开启会有帮助模块压缩,处理副作用等一些功能
- development // 开发环境的开启会有利于热更新的处理,识别哪个模块变化
- none // 5+取消了
设置mode可以自动触发webpack内置的函数,达到优化的效果
none
的赋值在5+
版本中取消了,包括production
时启动的函数中TerserPlugin
改成为UglifyJsPlugin
module
模块,在webpack里一切皆模块,⼀个模块对应着一个文件。
webpack会从配置的 Entry 开始递归找出所有依赖的模块。当webpack处理到不认识的模块时,需要在webpack中的module 处进⾏配置,当检测到是什么格式的模块,使⽤什么loader来处理。
module:{rules:[{test:/\.xxx$/, //指定匹配规则 use:{loader: 'xxx-load'//指定使⽤用的loader }}]
}
loader
webpack 默认只支持.json 和 .js模块,不支持不认识其他格式的模块,而loader就是用来转换这些不认识模块的模块转换器,可以把模块原内容按照需求转换成新内容。
常⻅的loader
style-loader
css-loader
less-loader
sass-loader
ts-loader // 将Ts转换成js
babel-loader // 转换ES6、7等js新特性语法
file-loader // 处理理图⽚片⼦子图
eslint-loader
...
Plugins
plugins
选项用于以各种方式自定义 webpack 构建过程。
也就是说,webpack的打包过程是有生命周期的概念的,每个阶段都有对应的钩子函数,而plugin就可以在webpack运行到某个阶段的时候进行一些操作,类似生命周期钩子函数的扩展插件,最终完成在webpack构建过程中的特定时机注入扩展逻辑来生成自己想要的结果。
Loader详解
上面我们了解到loader是webpack构建过程中的模块转换器,下面我们着重看几个常用的loader
静态资源处理类
-
file-loader
file-loader是把打包⼊口中识别出的资源模块,移动到输出目录,并且返回⼀个地址名称,所以file-loader通常用来处理那些只需要从源代码移到打包目录的静态资源,比如:txt、svg、csv、excel以及各种图片、字体文件。
module: {rules: [// test 表示需要识别文件的后缀名,即是这样的后缀名的文件需要由该loader处理test: /\.(png|jpe?g|gif)$/,// use 表示处理上述匹配文件的loader,如果是一个loader,需要用对象表示,如果是多个loader,则需要用数组use: {loader: 'file-loader',options: {// [name]老资源模块的名称 [hash]hash值 [ext]老资源的后缀名name: '[name]_[hash].[ext]'// 打包后的存放位置outputPath: 'images/'}}] }
-
url-loader
url-loader是file-loader的加强版本,它的内部实现也是基于file-loader,所以可以处理file-loader可以处理的资源。
同时url-loader会根据默认的(或者配置的)limit参数,将满足条件的图片转换成base64格式,并打包到js里,从而减小http请求,提高页面加载速度。但是这种操作只针对小体积的图片,不适用大图片。
module: {rules: [{test: /\.(png|jpe?g|gif)$/,use: {loader: 'url-loader',options: {name: '[name]_[hash].[ext]',outputPath: 'images/',// 小于2048Byte(2KB),才转换成base64limit: 2048}}}] }
样式处理类
-
css-loader
分析css模块之间的关系,并合成⼀个css
-
style-loader
style-loader可以会把css-loader生成的内容,以style挂载到页面的head部分
-
less-loader
less-loader会把less语法转成css
# 在多loader转换时,执行顺序为从右到左,从下到上 module: {rules: [{test: /\.css$/,use: ['style-loader', 'css-loader']},{test: /\.scss$/,use: ['style-loader', 'css-loader', 'less-loader']}] }
-
postcss-loader
postcss-loader也比较常见,通过用来批量转化css,比如:自动添加前缀以适配不同版本的浏览器,具体的浏览器适配规则可以参见https://caniuse.com/
// webpack.config.js中的配置 module: {rules: [{test: /\.css$/,use: ['style-loader', 'css-loader', 'postcss-loader']}] }// 此外,需要新建postcss.config.js module.exports = {plugins: [require('autoprefixer')({// last 2 versions: 最近的两个大版本// >1%: 全球市场份额大于1%的浏览器overrideBrowerslist: ['last 2 versions', '>1%']})] }
JS脚本处理类
babel-loader
在使用babel-loader,我们需要了解几个概念
-
Babel是JavaScript编译器器,能将ES6代码转换成ES5代码,让我们开发过程中放⼼使用JS新特性⽽不用担⼼心兼容性问题。并且还可以通过插件机制根据需求灵活的扩展。
-
babel-loader是webpack与babel的通信桥梁,不做把es6转成es5的⼯作,这部分⼯作需要用到
@babel/preset-env
来做,@babel/preset-env里包含了es6,7,8转es5的转换规则- 在使用preset-env时,需要注意的就是按需引入,由配置参数
useBuiltIns
决定 - useBuiltIns选项是babel7的新功能,这个选项告诉babel如何配置@babel/polyfill。 它有三个参数可以使⽤:
- entry:需要在webpack的⼊口文件里
import "@babel/polyfill"
⼀次。 babel会根据你的使⽤情况导⼊垫片,没有使⽤的功能不会被导入相应的垫⽚。 - usage:不需要import,全⾃动检测,但是要安装@babel/polyfill。(试验阶段)
- false:如果你
import "@babel/polyfill"
,它不会排除掉没有使用的垫⽚,程序体积会庞⼤。(不推荐)
- entry:需要在webpack的⼊口文件里
- 请注意: usage的⾏为类似 babel-transform-runtime,不会造成全局污染,因此也不会对类似 Array.prototype.includes() 进行 polyfill。
["@babel/preset-env",{targets: { // 需要指定代码运行的浏览器环境edge: "17",firefox: "60", chrome: "67",safari: "11.1"},corejs: 2,// 新版本需要指定核心库版本useBuiltIns: "usage"// 按需注入} ]
- 在使用preset-env时,需要注意的就是按需引入,由配置参数
-
默认的Babel只⽀持let等⼀些基础的特性转换,Promise等⼀些还有转换过来,这时候需要借助垫片
@babel/polyfill
,把es的新特性都装进来,来弥补低版本浏览器中缺失的特性,其原理是语法转换,也就是说在转换后的文件里定义一个promise,并挂载到window对象上。 -
Babel在执⾏编译的过程中,会从项⽬根⽬录下的
.babelrc
JSON 文件中读取配置。没有该⽂件会从loader的options地⽅读取配置//.babelrc {presets: [["@babel/preset-env",{targets: { // 需要指定代码运行的浏览器环境edge: "17",firefox: "60", chrome: "67",safari: "11.1"},corejs: 2,// 新版本需要指定核心库版本useBuiltIns: "usage"// 按需注入}]] }//webpack.config.js {test: /\.js$/,exclude: /node_modules/, loader: "babel-loader" }
这是最后的全部配置
module: {rules: [test: /\.js$/,exclude: /node_modules/,use: {loader: "babel-loader", options: {presets: [["@babel/preset-env",{targets: { // 需要指定代码运行的浏览器环境edge: "17",firefox: "60", chrome: "67",safari: "11.1"},corejs: 2,// 新版本需要指定核心库版本useBuiltIns: "usage"// 按需注入}]]}}] }
Plugin详解
HtmlWebpackPlugin
HtmlWebpackPlugin
简化了HTML文件的创建,以便为你的webpack包提供服务。这对于在文件名中包含每次会随着编译而发生变化哈希的 webpack bundle 尤其有用。 你可以让插件为你生成一个HTML文件,使用lodash模板提供你自己的模板,或使用你自己的loader。
// 配置参数
title: ⽤来⽣成⻚面的 title 元素
filename: 输出的 HTML ⽂件名,默认是 index.html, 也可以直接配置带有子目录。
template: 模板⽂件路路径,⽀持加载器,⽐如 html!./index.html
inject: true | 'head' | 'body' | false ,注⼊所有的资源到特定的 template 或者 templateContent 中,如果设置为 true 或者 body,所有的 javascript 资源将被放置到 body 元 素的底部,'head' 将放置到 head 元素中。
favicon: 添加特定的 favicon 路径到输出的 HTML ⽂文件中。 minify: {} | false , 传递 html-minifier 选项给 minify 输出
hash: true | false, 如果为 true, 将添加⼀个唯一的 webpack 编译 hash 到所有包含的脚本和 CSS 文件,对于解除 cache 很有用。
cache: true | false,如果为 true, 这是默认值,仅仅在⽂件修改之后才会发布⽂件。
showErrors: true | false, 如果为 true, 这是默认值,错误信息会写入到 HTML ⻚面中。
chunks: 允许只添加某些块 (⽐如,仅 unit test 块) chunksSortMode: 允许控制块在添加到⻚面之前的排序方式,支持的值:'none' | 'default' | {function}-default:'auto' excludeChunks: 允许跳过某些块,(比如,跳过单元测试的块)
// 使用案例
const path = require('path')
const htmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {......plugins: [new htmlWebpackPlugin(// 插件参数传递,使用对象{title: 'my App',filename: 'index.html',template: './src/index.html'})]
}// index.html 中获取插件参数
<title><%= htmlWebpackPlugin.options.title %></title>
CleanWebpackPlugin
其实 clean-webpack-plugin 很容易知道它的作用,就是来清除文件的。
一般这个插件是配合 webpack -p
这条命令来使用,就是说在为生产环境编译文件的时候,先把 build或dist
(就是放生产环境用的文件) 目录里的文件先清除干净,再生成新的。
// 使用案例
const cleanWebpackPlugin = require('clean-webpack-plugin')
module.exports = {......plugins: [new cleanWebpackPlugin()]
}
MiniCssExtractPlugin
一般我们的 css 是直接打包进 js⾥面的,我们希望能单独⽣成 css文件。 因为单独⽣成css,css可以和js并行下载,提高⻚面加载效率
借助MiniCssExtractPlugin 完成抽离css
// webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");module: {rules: [{test: /\.scss$/,use: [// "style-loader", // 不再需要style-loader,⽤MiniCssExtractPlugin.loaderMiniCssExtractPlugin.loader,"css-loader", // 编译css "postcss-loader", "sass-loader" // 编译scss]}]
},
plugins: [new MiniCssExtractPlugin({filename: "css/[name]_[contenthash:6].css",chunkFilename: "[id].css" })
]
性能优化
提升检索速度
1. 缩小loader处理范围
优化loader配置
-
test include exclude三个配置项来缩小loader的处理范围
-
推荐include
include: path.resolve(__dirname, "./src"),
2. resolve.modules
-
resolve.modules⽤于配置webpack去哪些目录下寻找第三⽅模块,默认是[‘node_modules’]
-
寻找第三方模块,默认是在当前项⽬录下的node_modules⾥⾯去找,如果没有找到,就会去上⼀级目录…/node_modules找,再没有会去…/…/node_modules中找,以此类推,和Node.js的模块寻找机制很类似。
-
如果我们的第三方模块都安装在了项目根⽬录下,就可以直接指明这个路径。
module.exports = {resolve:{modules: [path.resolve(__dirname, "./node_modules")] } }
3. resolve.alias
resolve.alias配置通过别名来将原导⼊路径映射成⼀个新的导⼊路径
拿react为例,我们引⼊的react库,⼀般存在两套代码
-
cjs
- 采⽤用commonJS规范的模块化代码
-
umd
- 已经打包好的完整代码,没有采用模块化,可以直接执⾏
默认情况下,webpack会从⼊口文件
./node_modules/bin/react/index
开始递归解析和处理依赖的⽂件。我们可以直接指定文件,避免这处的耗时。alias: {"@": path.join(__dirname, "./pages"),react: path.resolve(__dirname, "./node_modules/react/umd/react.production.min.js"),"react-dom": path.resolve(__dirname, "./node_modules/react-dom/umd/react-dom.production.min.js") }
4. resolve.extensions
resolve.extensions在导入语句没带文件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存在。
默认值:
extensions:['.js','.json','.jsx','.ts']
- 后缀尝试列表尽量的⼩
- 导⼊语句尽量的带上后缀
5. externals
我们可以将⼀一些JS⽂文件存储在 CDN 上(减少 Webpack 打包出来的 js 体积),在 index.html 中通过 标签引⼊入,如:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title>
</head>
<body><div id="root">root</div><script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>
我们希望在使用时,仍然可以通过 import 的⽅式去引⽤(如 import $ from ‘jquery’ ),并且希望 webpack 不不会对其进⾏打包,此时就可以配置 externals 。
//webpack.config.js
module.exports = {//...externals: {//jquery通过script引⼊入之后,全局中即有了了 jQuery 变量量 'jquery': 'jQuery'}
}
6. CDN
CDN通过将资源部署到世界各地,使得⽤户可以就近访问资源,加快访问速度。要接⼊CDN,需要把⽹
⻚的静态资源上传到CDN服务上,在访问这些资源时,使⽤CDN服务提供的URL。
// webpack.config.js
output:{publicPath: '//cdnURL.com', //指定存放JS⽂文件的CDN地址
}
- 有cdn服务器地址
- 确保静态资源⽂件的上传与否
提升加载速度
1. css压缩
-
借助 optimize-css-assets-webpack-plugin
-
借助 cssnano
# 安装 npm install cssnano -D npm i optimize-css-assets-webpack-plugin -D
// webpack.config.js const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); new OptimizeCSSAssetsPlugin({cssProcessor: require("cssnano"), //引⼊cssnano配置压缩选项 cssProcessorOptions: {discardComments: { removeAll: true }} })
2. html压缩
-
借助html-webpack-plugin
new htmlWebpackPlugin({ title: "my app",template: "./index.html", filename: "index.html", minify: {// 压缩HTML⽂文件removeComments: true, // 移除HTML中的注释 collapseWhitespace: true, // 删除空⽩符与换行符 minifyCSS: true // 压缩内联css} }),
3. 摇树(tree Shaking)
webpack2.x开始支持 tree shaking概念,顾名思义,“摇树”,清除⽆用 css,js(Dead Code)
Dead Code ⼀般具有以下⼏几个特征
- 代码不会被执⾏,不可到达
- 代码执⾏的结果不会被用到
- 代码只会影响死变量(只写不读)
- Js tree shaking只支持ES module的引⼊方式
css tree shaking
npm i glob-all purify-css purifycss-webpack --save-dev
const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[// 清除⽆用 css new PurifyCSS({paths: glob.sync([// 要做 CSS Tree Shaking 的路径⽂件path.resolve(__dirname, './src/*.html'), // 请注意,我们同样需要对html⽂件进行 tree shakingpath.resolve(__dirname, './src/*.js')])})
]
Js tree shaking
-
只支持import方式引入,不支持commonjs的方式引入
-
只要mode是production就会生效,develpoment的tree shaking是不生效的,因为webpack为了方便你的调试
-
可以查看打包后的代码注释以辨别是否⽣效
-
生产模式不需要配置,默认开启
// webpack.config.js optmization: {usedExports: true // 只要导出的模块被使用了,再做打包 }
副作用
// package.json
"sideEffects":false //正常对所有模块进⾏行行tree shaking , 仅⽣生产模式有效,需要配合 usedExports 或者 在数组⾥里里⾯面排除不不需要tree shaking的模块
"sideEffects":['*.css','@babel/polyfill']
4. 代码分割(code Splitting)
单页面应用spa
打包完后,所有页面只生成一个bundle.js
- 代码体积变大,不利于下载
- 没有合理利用浏览器资源
多页面应用mpa
如果多个页面引入了一些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载一次就缓存起来,避免重复下载。
配置
其实code Splitting概念 与 webpack并没有直接的关系,只不过webpack中提供了一种更加方便的方法供我们实现代码分割,基于https://webpack.js.org/plugins/split-chunks-plugin/
optimization: {splitChunks: {chunks: 'async',// 对同步 initial,异步 async,所有的模块有效 allminSize: 30000,// 最⼩尺寸,当模块大于30kbmaxSize: 0,// 对模块进行二次分割时使⽤用,不推荐使⽤minChunks: 1,// 打包⽣成的chunk⽂件最少有几个chunk引⽤了这个模块maxAsyncRequests: 5,// 最⼤异步请求数,默认5 maxInitialRequests: 3,// 最⼤初始化请求书,⼊口文件同步请求,默认3 automaticNameDelimiter: '-',// 打包分割符号name: true,// 打包后的名称,除了布尔值,还可以接收⼀个函数function cacheGroups: {// 缓存组vendors: {test: /[\\/]node_modules[\\/]/, name:"vendor",// 要缓存的分隔出来的 chunk 名称 priority: -10// 缓存组优先级数字越大,优先级越⾼}, other:{chunks: "initial",// 必须三选⼀: "initial"|"all"|"async"(默认)test: /react|lodash/,// 正则规则验证,如果符合就提取 chunk, name:"other",minSize: 30000,minChunks: 1,},default: {minChunks: 2,priority: -20,reuseExistingChunk: true// 可设置是否重⽤用该chunk}}}
}
平时使用下面的配置即可
optimization:{ // 帮我们自动做代码分割 splitChunks:{chunks:"all",// 默认是⽀持异步,我们使用all }
}
5. 作⽤域提升(Scope Hoisting)
作用域提升(Scope Hoisting)是指 webpack 通过 ES6 语法的静态分析,分析出模块之间的依赖关系,尽可能地把模块放到同一个函数中。下⾯通过代码示例来理理解:
// hello.js
export default 'Hello, Webpack'; // index.js
import str from './hello.js';
console.log(str);
打包后, hello.js 的内容和 index.js 会分开
通过配置 optimization.concatenateModules=true`:开启 Scope Hoisting
// webpack.config.js
module.exports = { optimization: {concatenateModules: true}
};
我们发现hello.js内容和index.js的内容合并在一起了!所以通过 Scope Hoisting 的功能可以让 Webpack 打包出来的代码文件更小、运行的更快
6. 动态链接库(DllPlugins)
Dll动态链接库 其实就是做缓存
项目中引入了很多第三方库,这些库在很⻓的⼀段时间内,基本不会更新,打包的时候分开打包来提升打包速度,⽽DllPlugin动态链接库插件,其原理就是把⽹⻚依赖的基础模块抽离出来打包到dll
文件中, 当需要导入的模块存在于某个dll
中时,这个模块不再被打包,⽽是去dll
中获取。
- 动态链接库只需要被编译⼀次,项⽬中⽤到的第三方模块,很稳定,例如react,react-dom,只要没有升级的需求
- webpack已经内置了对动态链接库的⽀持
- DllPlugin:⽤于打包出⼀个单独的动态链接库文件
- DllReferencePlugin:用于在主要的配置⽂件中引⼊DllPlugin插件打包好的动态链接库⽂件
新建webpack.dll.config.js⽂文件,打包基础模块
我们在 index.js 中使⽤了第三方库 react 、 react-dom ,接下来,我们先对这两个库先进行打包。
// webpack.dll.config.js
const path = require("path");
const { DllPlugin } = require("webpack");
module.exports = {mode: "development", entry: {react: ["react", "react-dom"] //! node_modules? },output: {path: path.resolve(__dirname, "./dll"),filename: "[name].dll.js",library: "react"}, plugins: [new DllPlugin({// manifest.json⽂文件的输出位置path: path.join(__dirname, "./dll", "[name]-manifest.json"), // 定义打包的公共vendor⽂文件对外暴暴露露的函数名name: "react"})]
}
在package.json中添加
"dev:dll": "webpack --config ./build/webpack.dll.config.js",
运行
npm run dev:dll
你会发现多了一个dll⽂文件夹,⾥边有dll.js文件,这样我们就把我们的React这些已经单独打包了
- dll⽂件包含了大量模块的代码,这些模块被存放在⼀个数组里。⽤数组的索引号为ID,通过变量将⾃己暴露在全局中,就可以在window.xxx访问到其中的模块
- Manifest.json 描述了与其对应的dll.js包含了哪些模块,以及ID和路径
打包好之后需要将dll文件注入index.html中,使用依赖add-asset-html-webpack-plugin
// webpack.config.js
new AddAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, '../dll/react.dll.js') // 对应的 dll ⽂件路径
})
运行项目
npm run dev
这个理解起来不费劲,操作起来很费劲。所幸,在Webpack5中已经不用它了,而是⽤ HardSourceWebpackPlugin ,⼀样的优化效果,但是使用却及其简单
-
提供中间缓存的作⽤
-
⾸次构建没有太大的变化
-
第⼆次构建时间就会有较大的节省
// webpack.config.js const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') const plugins = [new HardSourceWebpackPlugin() ]
7. 并发任务(happypack)
运行在 Node.之上的Webpack是单线程模型的,也就是说Webpack需要⼀个一个地处理任务,不能同时处理多个任务。 Happy Pack 就能让Webpack做到这一点,它将任务分解给多个⼦进程去并发执行,⼦进程处理完后再将结果发送给主进程。从⽽发挥多核CPU电脑的威力。
// webpack.config.js
var happyThreadPool = HappyPack.ThreadPool({ size: 5 });// module中添加
rules: [ {test: /\.jsx?$/, exclude: /node_modules/, use: [{loader: "happypack/loader?id=babel" }] },{test: /\.css$/,include: path.resolve(__dirname, "./src"), use: ["happypack/loader?id=css"]},
]
//在plugins中增加
plugins:[new HappyPack({// ⽤唯一的标识符id,来代表当前的HappyPack是⽤来处理⼀类特定的文件 id:'babel',// 如何处理理.js⽂文件,⽤用法和Loader配置中⼀一样 loaders:['babel-loader?cacheDirectory'],threadPool: happyThreadPool,}),new HappyPack({id: "css",loaders: ["style-loader", "css-loader"] }),
]
原理剖析
自定义webpack
我们以实现一个简易的mini-webpack为目的,来串联一下webpack的打包原理。
-
首先定义一个webpack类
- 这个类需要有接收webpack.config.js这样的配置文件
- 这个类需要有一个执行构建的主方法,可以通过
new webpack(options).start()
开始构建
class webpack {// 定义构造函数constructor(options) {// 获取配置文件中的入口和出口const {entry, output} = optionsthis.entry = entrythis.output = outputthis.modules = [] // 定义数组用来存放入口模块和其所有的依赖模块}// 开始构建的主方法start() {} }
-
解析入口文件,获取语法结构树AST
- 这里会涉及到
parser.parse
方法
const ast = parser.parse(entryInfo, {sourceType: "module" })
- 这里会涉及到
-
找到所有的依赖模块
- 这里会涉及到babel/core中的
traverse
方法 - 并且只需要找到type为
ImportDeclaration
的节点
traverse(ast, {ImportDeclaration({ node }) {} })
- 这里会涉及到babel/core中的
-
将AST转化为code
- 将 AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core中的
transformFromAst
和 @babel/preset-env。
// 提取内容,转化处理 const { code } = transformFromAst(ast, null, {presets: ["@babel/preset-env"] })
- 将 AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core中的
-
递归查看所有依赖项
- 递归查看的时候需要利用的技巧就是利用动态数组的长度遍历,一边push一边foreach
// 1. 获取入口文件 console.log('--------------1--------------') const entryInfo = this.parse(this.entry) this.modules.push(entryInfo)// 2. 递归分析其他模块 console.log('--------------2--------------') for(let i = 0; i < this.modules.length; i++) {const item = this.modules[i]const { denpendencies } = item // 获取依赖关系if (denpendencies) {for (let j in denpendencies) {this.modules.push(this.parse(denpendencies[j]))}} }
-
重写require函数,输出bundle
- 这块需要注意的就是引入模块的相对路径转化绝对路径
- 闭包和自执行函数的运用
(function(obj){function require(module){// 将相对地址转成绝对地址并获取function reRequire(relativePath){return require(obj[module].denpendecies[relativePath])}var exports = {}(function(require, exports, code) {eval(code)})(reRequire, exports, obj[module].code)return exports}require('${this.entry}') })(${JSON.stringify(obj)})
-
完整的mini-webpack代码
// mini-webpack.js const fs = require("fs") const path = require("path") const parser = require("@babel/parser") const traverse = require("@babel/traverse").default const { transformFromAst } = require("@babel/core")// 导出一个打包类,通过 new webpack(options).start() 进行构建 module.exports = class webpack {// 定义构造函数constructor(options) {// 获取配置文件中的入口和出口const {entry, output} = optionsthis.entry = entrythis.output = outputthis.modules = [] // 定义数组用来存放入口模块和其所有的依赖模块}// 开始构建的主方法start() {console.log('开始构建。。。。。。')// 1. 获取入口文件console.log('--------------1--------------')const entryInfo = this.parse(this.entry)this.modules.push(entryInfo)// 2. 递归分析其他模块console.log('--------------2--------------')for(let i = 0; i < this.modules.length; i++) {const item = this.modules[i]const { denpendencies } = item // 获取依赖关系if (denpendencies) {for (let j in denpendencies) {this.modules.push(this.parse(denpendencies[j]))}}}console.log(this.modules);// 3. 结构转化, {文件名: {依赖,代码}}console.log('--------------3--------------')const obj = {}this.modules.forEach((item) => {obj[item.entryFile] = {denpendencies: item.denpendencies,code: item.code}})console.log(obj)// 4. 生成bundle文件console.log('--------------4--------------')this.generate(obj)}// 根据入口文件,获取依赖和内容代码parse(entryFile) {// 获取到index.js中的文本内容const entryInfo = fs.readFileSync(entryFile, "utf-8")// 根据内容原文获取抽象语法树AST, ast.program.body才是内容的主体const ast = parser.parse(entryInfo, {sourceType: "module"})// 提取依赖,转化处理const denpendencies = {}; // 用来记录依赖的相对地址和绝对地址的对应关系traverse(ast, {// 提取type为ImportDeclaration的节点node,node.source.value为import()的参数ImportDeclaration({ node }) {// 通过node.source.value获取为相对路径,需要转换为绝对地址,并和相对路径一一对应const prefixPath = "./" // 这里是Mac系统的地址类型,不兼容Windows和Linuxdenpendencies[node.source.value] = prefixPath + path.join(path.dirname(entryFile), node.source.value)console.log(denpendencies)}})// 提取内容,转化处理const { code } = transformFromAst(ast, null, {presets: ["@babel/preset-env"]})console.log(code)return {entryFile,denpendencies,code}}// 根据所有模块及其依赖和代码,生成bundlegenerate(obj) {// 根据output参数,获取bundle存放路径const filePath = path.join(this.output.path, this.output.filename)// 创建bundle的内容,内容主要有// 创建自执行函数,处理require module exports//const bundle = `(function(obj){function require(module){function reRequire(relativePath){return require(obj[module].denpendecies[relativePath])}var exports = {}(function(require, exports, code) {eval(code)})(reRequire, exports, obj[module].code)return exports}require('${this.entry}')})(${JSON.stringify(obj)})`fs.writeFileSync(filePath, bundle, "utf-8")} }
结语
关于webpack的就写到这里。其实工具对于我们研发人员来说,就是提升效率的一个途径,这样的途径有很多中,比如:有gulp、rollup、grant还有百度fis等等,都各有千秋,用好了可以事半功倍,用不好反而事倍功半。
以上就是我对webpack使用过程中的简要总结,希望能帮到大家。同时有不恰之处,还望大家批评指正。
最后喜欢我的小伙伴也可以通过关注公众号“剑指大前端”,或者扫描下方二维码联系到我,进行经验交流和分享,同时我也会定期分享一些大前端干货,让我们的开发从此不迷路。
关注后,回复“webpack”,即可获取最新webpack讲解教学视频哦 🙂🙂🙂