文章目录
分享也是再次学习的过程,所以即使已经有非常多这类文章,我也还是再写一遍。一起学习一起进步。
原理
几个概念
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
流程
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
- 确定入口:根据配置中的 entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
Webpack 是一个庞大的 Node.js 应用,如果你阅读过它的源码,你会发现实现一个完整的 Webpack 需要编写非常多的代码。 但你无需了解所有的细节,只需了解其整体架构和部分细节即可。
webpack是一个简单强大的工具。 对 Webpack 的开发者来说,它是一个扩展性的高系统。
Webpack 把复杂的实现隐藏了起来,给用户暴露出的只是一个简单的工具,让用户能快速达成目的。 同时整体架构设计合理,扩展性高,开发扩展难度不高,通过社区补足了大量缺失的功能,让 Webpack 几乎能胜任任何场景。
初始化项目
mkdir first-webpack
cd first-webpack
mkdir src
npm init -y //初始化
新建index.js和index.html文件
// index.js
console.log('hello world')
// index.html
<!DOVTYPE html>
<html lang='en'>
<head>
<meta charset ="utf-8">
<title > xxxx</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
安装webpack并配置
安装webpack
npm add webpack webpack-cli --save-dev
新建一个build文件夹存放webpack配置文件
mkdir build
touch webpack.dev.js
在webpack.dev.js中书写基本配置
const path = require('path');
module.export = {
mode: 'development',
entry: './src/index.js',
module: {},
plugins: [ ],
output: {
filname: 'bundle.js',
path: path.resolve(__dirname, '../dist')
}
}
package.json更改
“script”: {
"start": "webpack --config ./build/webpack.dev.js"
}
配置babel
使用es6,babel是必不可少的。
npm install @babel/polyfill core-js@2 --save
npm install babel-loader @babel/core @babel/preset-react --save-dev
// @babel/polyfill: 模拟一个es6+的环境,提供es6方法和函数的垫片
// core-js@2:@babel/preset-env实现按需引入polyfill时,声明core-js版本
// babel-loader和@babel/core是核心模块
// @babel/preset-env是一个智能预设,允许您使用最新的JavaScript
// @babel/preset-react 转换JSX
扩展:
如果是开发工具库,想要实现按需替换,可以使用下面下面两个工具来实现
@babel/plugin-transform-runtime避免 polyfill 污染全局变量,减小打包体积,因此更适合作为开发工具库
@babel/runtime-corejs2作为生产环境依赖,约等于@babel/runtime + babel-polyfill,使用了@babel/runtime-corejs2,就无需再使用@babel/runtime了
.babellrc文件
{
"presets": [
[
"@babel/preset-env", { // 将es6的语法翻译成es5语法
"targets": {
"chrome": "67",
},
"useBuiltIns": "usage", // 做@babel/polyfill补充时,按需补充,用到什么才补充什么,
"corejs": "2",
}
],
[
"@babel/preset-react",
],
"plugins": [
"@babel/plugin-proposal-class-properties"
]
}
更改webpack.dev.js
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: [{
loader: 'babel-loader',
}],
}]
}
安装react
安装 react 、react-dom、react-rouder
npm install react react-dom react-router react-router-dom --save
这部分代码较多,可以在github上查看src的代码;
https://github.com/LuoShengMen/React-Whole-barrels/commit/55a0a0efdf5f581886a303326259fa9b2ff88444
webpack-dev-server
书写完成上述代码运行npm start后,打开index.html你会发现没有任何内容,此时我们需要配置一个简单的WEB服务器,指向index.html。
// 安装webpack-dev-server
npm install webpack-dev-server --save-dev
// 配置webpack.dev.js
...
devServer: {
contentBase: path.join(__dirname, '../dist')
},
...
// 更改npm start 命令
"start": "webpack-dev-server --config ./build/webpack.dev.js"
运行 npm start 就可以运行代码
使用html-webpack-plugin和clean-webpack-plugin插件
到目前为止,我们会发现都需要手动都将index.html放到dist文件夹中,并手动引入bundle.js.这个问题可以通过html-webpack-plugin解决。引入html-webpack-plugin后,在plugins生成一个实例,HtmlWebpackPlugin可以接受一个参数作为模版文件,打包结束后自动生成一个以参数为模版的html文件。并把打包生成的js文件自动引入到html文件中。clean-webpack-plugin可以实现在每次打包之前都把上一次的打包文件清空,这样避免了冗余文件的存在,用法也是直接在plugins里面生成一个实例.
// 安装html-webpack-plugin和clean-webpack-plugin
npm install html-webpack-plugin clean-webpack-plugin --save-dev
更改webpack.dev.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
...
plugins: [ // 插件
new HtmlWebpackPlugin({ // 向dist文件中自动添加模版html
template: 'src/index.html',
}),
new CleanWebpackPlugin(), // 打包后先清除dist文件,先于HtmlWebpackPlugin运行
],
less配置
样式使用less预处理器,那么就需要使用less,less-loader,css-loader,style-loader等。
npm install less less-loader css-loader style-loader postcss-loader --save-dev
// less-loader 编译less
// css-loader // 编译css
// style-loader创建style标签,并将css添加进去
// postcss-loader提供自动添加厂商前缀的功能,但是需要配合autoprefixer插件来使用
npm install autoprefixer --save-dev
更改webpack.dev.js配置
rules: [
...
{
test: /\.less$/,
exclude: /node_modules/,
use: ['style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
}, 'less-loader', 'postcss-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
}
]
postcss.config.js
module.exports = { // 自动添加css厂商前缀
plugins: [
require('autoprefixer')
]
}
图标与图片的处理
npm install file-loader url-loader --save-dev
file-loader帮助我们做两件事情:
1.当遇到图片文件时会将其打包移动到dist目录下
2.接下来会获得图片模块的地址,并将地址返回到引入模块到变量之中.
url-loader基本上可以实现file-loader的功能,但是有一区别就是经过url-laoder打包后的dist文件下是不存在image文件的,这是因为url-loader会把图片转换成base64的字符串直接放在bundle.js里面。
好处:直接将图片打包到js里,不用额外到请求图片,省了http请求
坏处:如果遇到打包到文件非常大,那么加载会加载很长时间,影响体验因此我们可以这样配置webpack.dev.js
rules: [
...
{
test: /\.(png|jpg|gif|jpeg)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]', // placeholder 占位符
outputPath: 'images/', // 打包文件名
limit: 204800, // 小于200kb则打包到js文件里,大于则使用file-loader的打包方式打包到imgages里
},
},
},
{
test: /\.(eot|woff2?|ttf|svg)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]-[hash:5].min.[ext]', // 和上面同理
outputPath: 'fonts/',
limit: 5000,
}
},
}
]
模块热替HMR
模块热替换也称为HMR,代码更新时只会更新被修改部分都显示。有如下几点
- 针对于样式调试更加方便
- 只会更新被修改代码的那部分显示,提升开发效率
- 保留在完全重新加载页面时丢失的应用程序状态
HMR配置有两种方式,一种cli方式,一种Node.js API方式,我们这里采用第二种方式。
我们通过在自定义开发服务下,使用插件webpack-dev-middleware和webpack-hot-middleware配合实现HMR
// 安装webpack-dev-middleware webpack-Hot-middleware
npm install webpack-dev-middleware webpack-hot-middleware --save-dev
// 不要忘记安装express,我们是通过express来启动本地服务
npm install express --save-dev
新建dev-server.js
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require("webpack-hot-middleware")
const config = require('./webpack.dev.js');
const complier = webpack(config); // 编译器,编译器执行一次就会重新打包一下代码
const app = express(); // 生成一个实例
const DIST_DIR = path.resolve(__dirname, '../', 'dist'); // 设置静态访问文件路径
let devMiddleware = webpackDevMiddleware(complier, {
quiet: true,
noInfo: true,
stats: 'minimal'
})
let hotMiddleware = webpackHotMiddleware(complier,{
log: false,
heartbeat: 2000
})
app.use(devMiddleware)
app.use(hotMiddleware)
// 设置访问静态文件的路径
app.use(express.static(DIST_DIR))
app.listen(8080, () => {
console.log("成功启动:localhost:"+ 8080)
}) //监听端口
更改webpack.dev.js
const webpack = require('webpack');
...
entry: {
//实现刷新浏览器webpack-hot-middleware/client?noInfo=true&reload=true 是必填的
main: ['webpack-hot-middleware/client?noInfo=true&reload=true', './src/index.js']
},
...
plugins: [
...
new webpack.NamedModulesPlugin(), //用于启动HMR时可以显示模块的相对路径
new webpack.HotModuleReplacementPlugin(), // 开启模块热更新,热加载和模块热更新不同,热加载是整个页面刷新
]
修改启动命令
"start" : "node ./build/dev-server.js"
public path
CDN通过将资源部署到世界各地,使得用户可以就近访问资源,加快访问速度。如果我们把网页的静态资源上传到CDN服务上,在访问这些资源时,publicPath填写的就是CDN提供URL
我们当前用/,相对于当前路径,是因为我们的资源在同一文件夹下。
webpack.dev.js
output: {
...
publicPath : '/'
}
使用souracemap
sourceMap本质上是一种映射关系,打包出来的js文件中的代码可以映射到代码文件的具体位置,这种映射关系会帮助我们直接找到在源代码中的错误。可以直接在devtool中使用.合理的使用source-map可以帮助我们提高开发效率,更快的定位到错误位置。生产环境和开发环境的devtool配置是不同的。我们可以在webpack.dev.js中添加devtool。
devtool:"cheap-module-eval-source-map",// 开发环境配置最佳实践
devtool:"cheap-module-source-map", // 生产配置最佳实践
生产环境构建
到目前为止我们配置的都是开发环境的webpack,开发环境(development)和生产环境(production)的构建目标差异很大,而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间.新建
webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: "production", // 只要在生产模式下, 代码就会自动压缩,自动启用 tree shaking
devtool:"cheap-module-source-map",
entry: { // 入口文件
main: './src/index.js'
},
module: {
rules: ...省略 //代码和dev中相同
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new CleanWebpackPlugin(),
],
output: {
publicPath: "/",
filename: 'bundle.js',
path: path.resolve(__dirname, '../dist')
}
}
添加打包脚本
"build": "webpack --config ./build/webpack.prod.js"
执行npm run build后,你会发现dist文件夹下已经生成一系列文件。你会发现生产环境下的配置和开发环境下的配置有很多相同,接下来我们会对webpack配置进行优化。
提取公共配置
webpack.dev.js和webpack.prod.js中有很多相同对配置,我们可以将公共配置提取出来,再使用webpack-merge来将不同环境下的配置合并起来。
npm install webpack-merge --save
复制代码webpack配置文件更改
webpack.dev.js
...
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {
mode: 'development',
devtool:"cheap-module-eval-source-map",
entry: {
main: ['webpack-hot-middleware/client?noInfo=true&reload=true', './src/index.js']
},
devServer: {
contentBase: path.join(__dirname, '../dist')
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
],
output: {}
}
module.exports = merge.smart(commonConfig, devConfig)
webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const prodConfig = {
mode: "production", // 只要在生产模式下, 代码就会自动压缩
devtool:"cheap-module-source-map",
entry: {
main: './src/index.js'
},
module: {},
plugins: [],
output: {}
}
module.exports = merge.smart(commonConfig, prodConfig)
webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const commonConfig = {
module: {
...太多了省略吧
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new CleanWebpackPlugin(),
],
output: {
publicPath: "/",
filename: 'bundle.js',
path: path.resolve(__dirname, '../dist')
}
}
module.exports = commonConfig;
浏览器缓存(Cathing)
为了解决浏览器文件缓存问题,例如:代码更新后,文件名称未改变,浏览器非强制刷新后,浏览器去请求文件时认为文件名称未改变而直接从缓存中读取不去重新请求。我们可以在webpack.prod.js输出文件名称中添加hash值.使用HashedModuleIdsPlugin的原因是可以当更改某一个文件时,只改变这一个文件的hash值,而不是所有的文件都改变。
plugins: [
...
new webpack.HashedModuleIdsPlugin(), //根据模块的相对路径生成一个四位数的hash
new webpack.optimize.CommonsChunkPlugin({ // 配合上面的插件使用
name: 'runtime'
})
],
output: {
filename: '[name].[contenthash].js', // entry对应的key值
chunkFilename: '[name].[contenthash].js', // 间接引用的文件会走这个配置
},
运行npm run build命令后,会发现dist文件中js文件名中已经有了hash值
记得同步修改 webpack.common.js 和 webpack.dev.js
指定环境
可以通过指定环境,来使webpack进行选择性编译,择性编译是指根据打包是环境的不同,选择性地让特定的语句有效,让特定的语句无效。这样可以对具体用户的环境进行代码优化,从而删除或添加一些重要代码。最简单的例子,在开发环境中,我们打印日志,但在生产环境中,我们让所有打印日志的语句无效(让程序不运行打印的语句,甚至让打包出来的文件根本就不包含打印日志的语句)我们可以使用 webpack 内置的 DefinePlugin 来实现。
// webpack.dev.js
...
plugins: [
...
new Webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
}),
]
// webpack.prod.js
plugins: [
...
new Webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
]