学什么
- webpack基础配置
- webpack高级配置
- webpack性能优化
- tapable钩子
- AST抽象语法树的应用
- webpack原理分析,手写webpack
- 手写常见的loader和plugin
目标
- 掌握webpack的安装
- 掌握webpack的基础配置
- 掌握loader的配置
- 掌握plugin的配置
- 了解webpack的性能优化
- 了解webpack的tapable
- 了解AST的应用
-
深入学习webpack原理,手写webpack
1. webpack基础
1.1 webpack的安装
全局安装webpack
npm i webpack webpack-cli -g
项目中安装webpack(推荐)
npm i webpack webpack-cli -D
1.2 webpack的使用
webpack-cli
npm 5.2以上的版本中提供了一个npx命令。
npx想要解决的主要问题就是调用项目内部安装的模块,原理就是在node_modules下的.bin目录中找到对应的命令执行。
使用webpack命令:npx webpack
webpack4.0以后可以实现0配置打包构建,0配置的特点就是限制较多,无法自定义很多配置
开发中常用的还是使用webpack配置进行打包构建
webpack配置
webpack有四大核心概念:
- 入口(entry):程序的入口js
- 出口(output):打包后存放的位置
- loader:用于对模块的源代码进行转换
- 插件(plugins):插件的目的在于解决loader无法实现的其他事
1. 配置webpack.config.js
2. 运行npx webpack
const path = require('path')
module.exports = {
// 入口文件配置
entry: "./src/index.js",
//出口文件配置
output:{
// 输出的路径,webpack2起就规定必须是绝对路径
path: path:join(__dirname,'dist'),
//输出文件的名字
filename:'bundle.js'
},
mode:'development'//默认为production,可以手动设置为development,区别就是能否进行压缩混淆
}
将npx webpack命令配置到packages.json的脚本中
1. 配置package.json
2. 运行npm run build
{
"name": "webpack-basic",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "webpack"
},
"devDependencies": {
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1"
}
}
开发时自动编译工具
每次要编译代码时,手动运行 npm run build 就会变得很麻烦。
webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:
1. webpack Watch Mode
2. webpack-dev-sever
3. webpack-dev-middleware
多数场景中,可能需要使用 webpack-dev-server,但是不妨探讨一下以上所有的选项。
watch
在 webpack指令后添加--watch参数即可。
作用:
-
监听文件变动,并自动重新构建 但不刷新浏览器。
-
适用于后端渲染的项目,例如 后端负责 HTML 渲染,前端仅需重新打包。
使用方法:
webpack --watch
或在Webpack配置中:
module.exports={
watch: true
};
特点:
-
增量编译:只重新编译修改的部分,速度比完整构建快。
-
无服务器:不会启动 HTTP 服务器,只是监听变化并重新打包。
-
不自动刷新:需要手动刷新浏览器查看变化。
适用场景:
-
需要手动刷新页面的场景(如后端渲染项目)。
-
代码变动频率不高的情况。
webpack-dev-server(推荐)
1. 安装 devServer
devServer需要依赖webpack,必须在项目依赖中安装webpack
npm i webpack-dev-server webpack -D
2. index.html中修改<script src="/bundle.js"></script>
3. 运行:npx webpack-dev-server
4. 运行:npx webpack-dev-server --hot --open --port 8090
5. 配置package.json的scripts:"dev": "webpack-dev-server --hot --open --port 8090"
6. 运行npm run dev
devServer会在内存中生成一个打包好的bundle.js,专供开发时使用,打包效率高,修改代码后会自动重新打包以及刷新浏览器,用户体验非常好。
还可以通过配置文件对devServer的参数进行修改然后修改packages.json中的scripts:"dev":"webpack-dev-server",运行npm run dev:
const path = require('path')
module.exports = {
// ⼊⼝⽂件配置
entry: './src/index.js',
//出⼝⽂件配置项
output: {
// 输出的路径,webpack2起就规定必须是绝对路径
path: path.join("$dirname, 'dist'),
// 输出⽂件名字
filename: 'bundle.js'
},
devServer: {
port: 8090,
open: true,
hot: true
},
mode: 'development'
}
作用:
-
启动一个 本地开发服务器,提供静态文件服务。
-
支持 HMR(Hot Module Replacement),可以局部更新代码,无需刷新整个页面。
特点:
-
自带 HTTP 服务器,不用手动刷新页面。
-
支持 HMR,更快更新代码。
-
内存中编译,不会写入
dist
目录,性能更好。
适用场景:
-
SPA(单页应用) 开发。
-
前端单独开发环境,无需后端配合。
webpack-dev-middeware
webpack-dev-middleware是一个容器,它可以把webpack处理后的文件传递给一个服务器(server)。webpack-dev-server在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。
1. 安装 express 和 webpack-dev-middleware;
npm i express webpack-dev-middleware -D
2. 新建 server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
const app = express();
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, {
publicPath: '/'
}));
app.listen(3000, function () {
console.log('http:"#localhost:3000');
});
3. 配置package.json中的scripts:"server":"node server.js"
4. 运行:npm run server
如果使用webpack-dev-middleware,必须使用html-webpack-plugin插件,否则html文件无法正确的输出到express服务器的根目录
特点
优点
-
集成到自定义服务器,适用于 Koa、Express 等框架。
-
和
webpack-dev-server
类似,但更灵活。 -
支持热更新,但需要配合
webpack-hot-middleware
。
缺点
-
需要手动配置 Node.js 服务器,比
webpack-dev-server
复杂。 -
不能独立运行,必须嵌入 Express/Koa。
适用场景
-
SSR(服务端渲染)应用(如 Next.js)。
-
需要 自定义服务器,如 API 代理、身份验证等。
-
需要 更灵活的 Webpack 控制。
不同使用场景:
-
如果只是想监听文件变化,生成构建产物 →
watch
-
如果需要本地开发服务器,并支持热更新 →
webpack-dev-server
-
如果项目已经有 Express/Koa 服务器,并希望集成 Webpack 编译 →
webpack-dev-middleware
处理css
1. 安装 npm i css-loader style-loader -D
2. 配置webpack.config.js
module: {
rules: [
// 配置的是⽤来解析.css⽂件的loader(style-loader和css-loader)
{
// ⽤正则匹配当前访问的⽂件的后缀名是 .css
test: /\.css$/,
use: ['style-loader', 'css-loader'] // webpack底层调⽤这些包的顺序是从右到左
}
]
}
loader的释义:
1. css-loader:解析css文件
2. style-loader:将解析出来的结果放到html中,使其生效
处理less 和 sass
npm i less less-loader sass-loader node-sass -D
{ test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] }
{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }
处理字体和图片
1. npm i file-loader url-loader -D
url-loader 封装了file-loader,所以使用url-loader时需要安装file-loader
{
test: /\.(png|jpg|gif)/,
use:[{
loader:'url-loader',
options:{
// limit表示如果图片大于5KB,就以路径形式展示,小于的话就用base64格式展示
limit: 5 * 1024,
//打包输出目录
outputPath:'images',
//打包输出图片名称
name: '[name]-[hash:4].[ext]'
}
}]
}
babel
1. npm i babel-loader @babel/core @babel/preset-env webpack -D
2. 如果需要支持更高级别的ES6语法,可以继续安装插件:
npm i @babel/plugin-proposal-class-properties -D
也可以根据需要在babel官网找插件进行安装
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/env'],
plugins: ['@babel/plugin-proposal-class-properties']
}
},
exclude: /node_modules/
}
官方更建议的做法是在项目根目录下新建一个.babelrc的babel配置文件
{
"presets": ["@babel/env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
如果需要使用generator,无法直接使用Babel进行转换,因为会将generator转换为一个regeneratorRuntime,然后使用mark和wrap来实现genrator,但由于babel并没有内置regeneratorRuntime,所以无法直接使用需要安装插件:
npm i @babel/plugin-transform-runtime -D
同时还需安装运行时依赖:
npm i @babel/runtime -D
在 .babelrc 中添加插件:
{
"presets": [
"@babel/env"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}
如果需要使用ES6/7中对象原型提供的新方法,Babel默认情况无法转换,即使用了transform-runtime的插件也不支持转换原型上的方法
需要使用另一个模块:
npm i @babel/polyfill -S
该模块需要在使用新方法的地方直接引入:
import '@babel/polyfill'
source map的使用:
devtool
此选项控制是否生成,以及如何生成source map。
使用 SourceMapDevToolPlugin进行更细粒度的配置。查看source-map-loader来处理已有的source map。选择一种source map格式来增强调试过程。不同的值会明显影响到构建和重新构建。
可以直接使用 SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin来代替使用devtool选项,它有更多的选项,但是切勿同时使用devtool选项和SourceMapDevToolPlugin/EvalSourceMapDevToolPlugin插件。因为devtool选项在内部添加过这些插件,所以会应用两次插件。
2. webpack高级配置
HTML中img标签的图片资源处理。
1. 安装npm install -S html-withing-loader
2. 在webpack.config.js文件中添加loader
{
test:/\.(html|html)$/i,
loader:'html-withing-loader',
}
使用时,只需要在html中正常引入图片即可,webpack会找到对应的资源进行打包,并修改html中的引用路径
多页应用打包
1. 在webpack.config.js中修改入口和出口配置
entry: {
main: './src/main.js',
other: './src/other.js'
},
output: {
path: path.join("$dirname, './dist/'),
// filename: 'bundle.js',
// 多⼊⼝⽆法对应⼀个固定的出⼝, 所以修改filename为[name]变量
filename: '[name].js',
publicPath: '/'
},
plugins: [
// 如果⽤了html插件,需要⼿动配置多⼊⼝对应的html⽂件,将指定其对应的输出⽂件
new HtmlWebpackPlugin({
template: './index.html',
filename: 'index.html',
chunks: ['main']
}),
new HtmlWebpackPlugin({
template: './index.html',
filename: 'other.html',
// chunks: ['other', 'main']
chunks: ['other']
})
]
2. 修改入口对象,支持多个js入口,同时修改output输出的文件名为'[name].js'表示各自已入口文件名作为输出文件名,但是html-webpack-plugin不支持此功能,所以需要再拷贝一份,用于生成两个html页面,实现多页面应用。
第三方库的两种引入方式
可以通过expose-loader进行全局变量的注入,同时也可以使用内置插件webpack.ProvidePlugin对每个模块的闭包空间,注入一个变量,自动加载模块,而不必到处import或require。
expose-loader将库引入到全局作用域。
1. 安装expose-loader
npm i -D expose-loader
2. 配置loader
module:{
rules:[{
test:require.resolve('jquery'),
use:{
loader:'expose-loader',
options:'$'
}
}]
}
tips: require.resolve用来获取模块的绝对路径,所以这里的loader只会作用于jquery模块,并且只在bundle中使用到它时,才进行处理。
webpack.ProvidePlugin将库自动加载到每个模块。
1. 引入webpack
const webpack = require('webpack')
2. 创建插件对象
要自动加载jquery,我们可以将两个变量都指向对应的node模块
new webpack.ProvidePlugin({
$:'jquery',
jQuery:'jquery'
})
Development / Production 不同配置文件打包
项目开发时一般需要使用两套配置文件,用于开发阶段打包(不压缩代码,不优化代码,增加效率)和线上阶段打包(压缩代码,优化代码,打包后直接上线使用)
抽取三个配置文件:
webpack.base.js
webpack.prod.js
webpack.dev.js
步骤如下:
1. 将在开发环境和生产环境公用的配置放入base中,不同的配置各自放入prod或dev文件中
2. 然后在dev和prod中使用webpack-merge把自己的配置与base配置进行合并后到处
npm i -D webpack-merge
3. 将package.json中的脚本参数进行修改,通过--config手动指定特定的配置文件
定义环境变量
除了区分不同的配置文件进行打包,还需要在开发时知道当前的环境是开发阶段或线上阶段,所以可以借助内置插件DefinePlugin来定义环境,最终可以实现开发阶段与上线阶段的api地址自动切换。
1. 引入webpack
const webpack = require('webpack')
2. 创建插件对象,并定义环境变量
new webpack.DefinePlugin({
IS_DEV:'false'
})
3. 在src打包的代码环境下可以直接使用
使用devServer解决跨域问题
HMR的使用
启用HMR只需要在webpack配置中添加:hot:true,
HMR(热模块替换)是一种 Webpack 提供的功能,它可以在代码修改后,自动更新特定模块,而不会刷新整个页面。
与普通的 Live Reload(浏览器刷新)不同,HMR 只更新变更的模块,比如:
-
修改 CSS 时,HMR 只替换 CSS 样式,不刷新整个页面。
-
修改 JS 逻辑 时,HMR 只替换模块,不会影响已存在的应用状态(比如 Vue/React 组件的 UI 状态不会丢失)。
-
修改 React/Vue 组件 时,组件会重新渲染,而不丢失
state
。
需要对某个模块进行热更新时,可以通过module.hot.accept方法进行文件监控。
只要模块内容发生变化,就会触发回调函数,从而可以重新读取模块内容,进行相应的操作。
HMR 的工作原理
HMR 的核心是 Webpack 在浏览器和 Webpack Dev Server 之间建立 WebSocket 连接,并在文件变更时执行以下步骤:
-
监听文件变化:Webpack 监视源码文件,一旦文件发生变化,重新编译该模块。
-
通知 Webpack Dev Server:Webpack 通过 WebSocket 告知浏览器有模块更新。
-
增量更新:浏览器端的 Webpack runtime 只替换修改的模块,而不刷新整个页面。
3. webpack优化
production模块打包自带优化
tree shaking
tree shaking 是一个术语,通常用于打包时移除Javascript中的未引用的代码,它依赖于ES6模块系统中import和export的静态结构特性。
开发时引入一个模块后,如果只使用其中一个功能,上线打包时只会把用到的功能打包进bundle,其他没用到的功能不会打包进来,可以实现最基础的优化。
scope hoisting
scope hoisting的作用是将模块之间的关系进行结果推测,可以让 Webpack 打包出来的代码文件更小、运行的更快。
scope hoisting的实现原理其实很简单:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中,但前提是不能造成代码冗余。
因此只有那些被引用了一次的模块才能被合并。
由于scope hoisting需要分析出模块之间的依赖关系,因此源码必须采用ES6模块化语句,不然它将无法生效。
代码压缩:
所有代码使用uglifyJsPlugin插件进行压缩、混淆。
css优化
将css提取到独立文件中
mini-css-ectract-plugin是用于将css提取为独立的文件的插件,对每个包含css的js文件都会创建一个css文件,支持按需加载css和sourceMap
只能用在webpack4中,有以下的优点:
- 异步加载
- 不重复编译,性能很好
- 容易使用
- 只针对css
使用方法:
1. 安装
npm i -D mini-css-extract-plugin
2. 在webpack配置文件中引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
3. 创建插件对象,配置抽离的css文件名,支持placeholder语法
new MiniCssExtractPlugin({
filename:'[name].css'
})
4. 将原来配置的所有style-loader替换为MiniCssExtractPlugin.loader
{
test: /\.css$/,
// webpack读取loader时 是从右到左的读取, 会将css⽂件先交给最右侧的loader来处理
// loader的执⾏顺序是从右到左以管道的⽅式链式调⽤
// css-loader: 解析css⽂件
// style-loader: 将解析出来的结果 放到html中, 使其⽣效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] },
// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass
loader'] },
自动添加css前缀
使用postcss,需要用到postcss-loader autoprefixer
2. 修改webpack配置文件中的loader,将postcss-loader放置在css-loader的右边(调用链从右到左)
{
test: /\.css$/,
// webpack读取loader时 是从右到左的读取, 会将css⽂件先交给最右侧的loader来处理
// loader的执⾏顺序是从右到左以管道的⽅式链式调⽤
// css-loader: 解析css⽂件
// style-loader: 将解析出来的结果 放到html中, 使其⽣效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
// { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] },
{ test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader',
'less-loader'] },
// { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] },
{ test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss
loader', 'sass-loader'] },
3. 项目根目录下添加postcss的配置文件:postcss.config.js
4. 在postcss的配置文件中使用插件
module.exports = {
plugins: [require('autoprefixer')]
}
开启css压缩
需要使用optimize-css-assets-webpack-plugin的插件来完成css压缩
但是由于配置css压缩时会覆盖掉webpack默认的优化配置,导致JS代码无法压缩,还需要手动把JS代码压缩插件导入进来:terser-webpack-plugin
1. 安装
npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin
2. 导入插件
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
3. 在webpack配置文件中添加配置节点
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
tips:webpack4默认采用的JS压缩插件为uglifyjs-webpack-plugin,在mini-css-extract-plugin上一个版本中还推荐使用该插件,但最新的v0.6中建议使用teser-webpack-plugin来完成js代码压缩,具体原因未在官网说明,那就按照最新文档即可。
JS代码分离
Code Splitting是webpack打包时用到的重要的优化特性之一,此特性能够把代码分离的到不同的bundle中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
ps:
正在努力加工中……