从零开始的Webpack4配置
作为一个前端工程师,不管怎么说,webpack配置还是要了解一下的。虽然一般情况下都用不上,但是人无我有,人有我优,是不是就能在市场有有一点优势了呢?学了webpack之后,以后要在项目中装其他乱七八糟的插件的时候也不会一脸懵逼,不知道这些东西怎么配置的,免得每次配东西都要百度。话不多说,开始学习吧!
这是根据知乎上看到的文章写的,我照着教程走了一遍,为了巩固学习成果,写篇文章记录一下。我学习过程中的代码都在这里。
webpack 的核心价值就是前端源码的打包,即将前端源码中每一个文件(无论任何类型)都当做一个 pack ,
然后分析依赖,将其最终打包出线上运行的代码。webpack 的四个核心部分
- entry 规定入口文件,一个或者多个
- output 规定输出文件的位置
- loader 各个类型的转换工具
- plugin 打包过程中各种自定义功能的插件
基础配置
初始化环境
首先创建一个文件夹my_webpack,然后npm init -y
,-y 命令表示在你初始化的时候所有的选择yes or no的选项全部选yes,也就是说全部使用默认配置来初始化。然后安装webpack,npm i webpack webpack-cli -D
,-D表示依赖安装到开发环境,生产环境中不会用到,对应到package.json文件的话,就是会添加到devDependencies下面。
然后创建src
目录,在src
目录下创建index.js
文件,里面随便写点console.log('webpack start')
。然后在根目录下创建webpack.config.js
,内容如下:
// path模块是node.js中提供用于处理文件路径和目录路径的实用工具
const path = require('path')
module.exports = {
// mode 可选 development 或 production ,默认为后者
// production 会默认压缩代码并进行其他优化(如 tree shaking)
mode: 'development',
// __dirname就是src目录之前的目录结构,是根据文件地址自动生成的
// path.join()就是把参数拼接成目录结构,如以下的entry值就是 __dirname/src/index.js
// path的具体API可以参考node.js
entry: path.join(__dirname, 'src', 'index'),
output: {
filename: 'bundle.js', // 表示打包出来的压缩文件名
// 表示打包出来的文件放在那个目录下面,这里指: __dirname/dist/ 下面
path: path.join(__dirname, 'dist')
}
}
然后在package.json
里的scripts
里添加打包命令
"scripts": {
"build": "webpack"
},
然后运行npm run build
命令,就可以看到打包文件放在dist
目录下面了。
区分dev与build
使用 webpack 需要两个最基本的功能:第一,开发的代码运行一下看看是否有效;第二,开发完毕了将代码打包出来。这两个操作的需求、配置都是完全不一样的。例如,运行代码时不需要压缩以便 debug ,而打包代码时就需要压缩以减少文件体积。因此,这里我们还是先把两者分开,方便接下来各个步骤的讲解。
这里先看看path.join()方法的执行结果,以便接下来的理解
首先,安装 npm i webpack-merge -D
,那么webpack-merge
是干什么的呢?
// webpack-merge做了两件事:它允许连接数组并合并对象,而不是覆盖组合
const merge = require("webpack-merge");
merge(
{a : [1],b:5,c:20},
{a : [2],b:10, d: 421}
)
//合并后的结果
{a : [1,2] ,b :10 , c : 20, d : 421}
然后根目录新建 build 目录,其中新建如下三个文件。
// webpack.common.js 开发和成产环境都用的到的公共配置
const path = require('path')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
module.exports = {
entry: path.join(srcPath, 'index') // 就是指的 src/index.js
}
// webpack.dev.js 运行代码的配置(该文件暂时用不到,先创建了,下文会用到)
const path = require('path')
const webpackCommonConf = require('./webpack.common.js') // 引入公共配置
const { smart } = require('webpack-merge')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
// 那么这里使用 webpack-merge 的方法就可以把公共配置与开发环境的配置合并在一起了
module.exports = smart(webpackCommonConf, {
mode: 'development'
})
// webpack.prod.js 打包代码的配置
const path = require('path')
const webpackCommonConf = require('./webpack.common.js')
const { smart } = require('webpack-merge')
const srcPath = path.join(__dirname, '..', 'src')
const distPath = path.join(__dirname, '..', 'dist')
// 那么这里使用 webpack-merge 的方法就可以把公共配置与生产环境的配置合并在一起了
module.exports = smart(webpackCommonConf, {
mode: 'production',
output: {
// contentHash指哈希码,这里 :8指的是取哈希码的前8位
filename: 'bundle.[contentHash:8].js', // 打包代码时,加上 hash 戳
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
}
})
修改 package.json
中的 scripts
:
"scripts": {
/* 表示打包时要用到webpack.prod.js中的配置 */
"build": "webpack --config build/webpack.prod.js"
},
重新运行 npm run build
即可看到打包出来的代码。最后,别忘了将根目录下的 webpack.config.js
删除。
这将引发一个新的问题:js
代码中将如何判断是什么环境呢?需要借助 webpack.DefinedPlugin
插件来定义全局变量。可以在 webpack.dev.js
和 webpack.prod.js
中做如下配置:
// 引入 webpack
const webpack = require('webpack')
// 增加 webpack 配置
plugins: [
new webpack.DefinePlugin({
// 注意:此处 webpack.dev.js 中写 'development' ,webpack.prod.js 中写 'production'
ENV: JSON.stringify('development')
})
]
最后,修改 src/index.js
只需加入一行 console.log(ENV)
,然后重启 npm run dev
即可看到效果。
JS 模块化
webpack
默认支持 js
各种模块化,如常见的 commonJS
和 ES6 Module
。但是推荐使用 ES6 Module
,因为 production
模式下,ES6 Module
会默认触发 tree shaking
,而 commonJS
则没有这个福利。究其原因,ES6 Module
是静态引用,在编译时即可确定依赖关系,而 commonJS
是动态引用。
es6
语法不熟的可以参考这里。
启动本地服务
上文创建的 webpack.dev.js
一直没使用,下面就要用起来。
使用 html
启动本地服务,肯定需要一个 html 页面作为载体,新建一个 src/index.html 并初始化内容。
<!DOCTYPE html>
<html>
<head><title>Document</title></head>
<body>
<p>this is index html</p>
</body>
</html>
要使用这个 html
文件,还需要安装 npm i html-webpack-plugin -D
,然后配置 build/webpack.common.js
,因为无论 dev
还是 prod
都需要打包 html
文件。
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html'
})
]
重新运行 npm run build
会发现打包出来了 dist/index.html
,且内部已经自动插入了打包的 js
文件。
webpack-dev-server
有了 html
和 js
文件,就可以启动服务了。首先安装 npm i webpack-dev-server -D
,然后打开 build/webpack.dev.js
配置。只有运行代码才需要本地 server
,打包代码时不需要。
// 启动本地服务器需要安装webpack-dev-server模块,然后在开发模式下配置devServer,生产模式下不需要
devServer: {
port: 3001, // 端口号
progress: true, // 显示打包的进度条
contentBase: distPath, // 根目录
open: true, // 自动打开浏览器
compress: true, // 启动gzip压缩
},
打开 package.json
修改 scripts
,增加 "dev": "webpack-dev-server --config build/webpack.dev.js"
, 。然后运行 npm run dev
,打开浏览器访问 localhost:3001
即可看到效果。
解决跨域
实际开发中,server
端提供的端口地址和前端可能不同,导致 ajax
收到跨域限制。使用 webpack-dev-server
可配置代理,解决跨域问题。如有需要,在 build/webpack.dev.js
中增加如下配置。
devServer: {
proxy: {
// 将本地 /api/xxx 代理到localhost:3000/api/xxx
'/api': 'http://localhost:3000',
// 将本地 /api2/xxx 代理到localhost:3000/xxx
'/api2': {
target: 'http://localhost:3000',
changeOrigin: true, // 允许跨域
pathRewrite: {
'/api2': ''
}
}
}
},
处理 ES6
使用 babel
由于现在浏览器还不能保证完全支持 ES6
,将 ES6
编译为 ES5
,需要借助 babel
这个神器。安装 babel npm i babel-loader @babel/core @babel/preset-env -D
,然后修改 build/webpack.common.js
配置
module: {
rules: [
{
test: /\.js$/, // 正则匹配 .js文件
loader: ['babel-loader'], // babel-loader转换es6代码
include: srcPath, // 检测的js文件位于这里
exclude: /node_modules/ // 这里的js文件不会被检测到
},
]
},
还要根目录下新建一个 .babelrc
json 文件,内容下:
{
"presets": ["@babel/preset-env"],
"plugins": []
}
在 src/index.js
中加入一行 ES6
代码,如箭头函数 const fn = () => { console.log('this is fn') }
。然后重新运行 npm run dev
,可以看到浏览器中加载的 js
中,这个函数已经被编译为 function
形式。
使用高级特性
babel 可以解析 ES6
大部分语法特性,但是无法解析 class
、静态属性、块级作用域,还有很多大于 ES6
版本的语法特性,如装饰器。因此,想要把日常开发中的 ES6
代码全部转换为 ES5
,还需要借助很多 babel
插件。
安装 npm i @babel/plugin-proposal-class-properties @babel/plugin-transform-block-scoping @babel/plugin-transform-classes -D
,然后配置 .babelrc
{
"presets": ["@babel/preset-env"],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-block-scoping",
"@babel/plugin-transform-classes"
]
}
在 src/index.js
中新增一段 class
代码,然后重新运行 npm run build
,打包出来的代码会将 class
转换为 function
形式。
source map
source map
用于反解析压缩代码中错误的行列信息,dev
时代码没有压缩,用不到 source map
,因此要配置 build/webpack.prod.js
module.exports = smart(webpackCommonConf,{
// 生产环境下推荐使用1或3,生成独立的map文件
// source map 用于反解析压缩代码中错误的行列信息,dev时代码没有压缩,所以用不到source map,所以用在prod中
// devtool: 'source-map', // 1、生成独立的source-map文件
// devtool: 'eval-source-map', // 2、不会生成独立的文件,集成到打包出来的js文件中
// devtool: 'cheap-moudle-source-map', // 3、生成单独地souce map文件,但没有列信息(因为文件体积较小)
devtool: 'cheap-module-eval-source-map', // 4、同3,但不会生成独立的文件,集成到打包出来的js文件中
}}
生产环境下推荐使用 1 或者 3 ,即生成独立的 map
文件。修改之后,重新运行 npm run build
,会看到打包出来了 map
文件。
处理样式
处理 css
安装必要插件 npm i style-loader css-loader -D
,然后配置 build/webpack.common.js
module: {
rules: [
{ /* js loader */ },
{
test: /\.css$/, // 正则匹配 .css文件
loader: ['style-loader', 'css-loader'] // loader 的执行顺序是:从后往前
}
]
},
新建一个 css
文件,然后引入到 src/index.js
中 import './css/index.css'
,重新运行 npm run dev
即可看到效果。
处理 less
less sass 都是常用 css
预处理语言,以 less
为例讲解。安装必要插件 npm i less less-loader -D
,然后配置 build/webpack.common.js
module: {
rules: [
{ /* js loader */ },
{
test: /\.(css|less)$/, // 可以同时匹配 .css和 .less文件
loader: ['style-loader', 'css-loader', 'less-loader'] // 增加 'less-loader' ,注意顺序
}
]
},
新建一个 less
文件,然后引入到 src/index.js
中 import './css/index.less'
,重新运行 npm run dev
即可看到效果。
自动添加前缀
一些 css3
的语法,例如 transform: rotate(45deg)
; 为了浏览器兼容性需要加一些前缀,如 webkit-
,可以通过 webpack
来自动添加。安装 npm i postcss-loader autoprefixer -D
,然后配置build/webpack.common.js
module: {
rules: [
{ /* js loader */ },
{
// 添加css、less转换器,postcss-loader用于给css属性加浏览器兼容前缀,如webkit-,此外还需要创建postcss.config.js文件
test: /\.(css|less)$/,
loader: ['style-loader','css-loader','less-loader','postcss-loader'] // loader的执行顺序是从后往前
}
]
},
还要新建一个 postcss.config.js
文件,内容是
module.exports = {
plugins: [require('autoprefixer')]
}
重新运行 npm run dev
即可看到效果,自动增加了必要的前缀。
抽离 css 文件
默认情况下,webpack
会将 css
代码全部写入到 html
的 <style>
标签中,但是打包代码时需要抽离到单独的 css
文件中。安装 npm i mini-css-extract-plugin -D
然后配置 build/webpack.prod.js
(打包代码时才需要,运行时不需要)
// 引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 增加 webpack 配置
module: {
rules: [
{
test: /\.(css|less)$/, // 需要抽离的样式文件
loader: [
// MiniCssExtractPlugin用于在打包时将css抽离到单独的css文件中
MiniCssExtractPlugin.loader, // 这里不再使用style-loader
'css-loader',
'less-loader',
'postcss-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/main.[contentHash:8].css'
})
]
如需要压缩 css
,需要安装 npm i terser-webpack-plugin optimize-css-assets-webpack-plugin -D
,然后增加配置build/webpack.prod.js
// 引入插件
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
// 增加 webpack 配置
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
},
运行 npm run build
即可看到打包出来的 css
是独立的文件,并且是被压缩过的。
处理图片
要在 js
中 import
图片,或者在 css 中设置背景图片。安装 npm i file-loader -D
然后配置 build/webpack.common.js
module: {
rules: [{
// 用来处理在js中import图片,在css中使用背景图片的操作
test: /\.(png|jpg|gif|PNG|JPG|GIF)$/,
use: 'file-loader'
}]
}
打包之后,dist
目录下会生成一个类似 917bb63ba2e14fc4aa4170a8a702d9f8.jpg
的文件,并被引入到打包出来的结果中。
多页应用
src
下有 index.js
index.html
和 other.js other.html
,要打包输出两个页面,且分别引用各自的 js
文件。
第一,配置输入输出
// 在webpack.common.js中配置入口文件
// 多页应用
entry: {
index: path.join(srcPath,'index.js'),
other: path.join(srcPath,'other.js')
},
// 在webpack.prod.js文件中配置出口文件
output: {
// filename: 'bundle.[contentHash:8].js', //打包代码时,加上hash戳
filename: '[name].[contentHash:8].js', // 多页应用时打包出来的文件名不一样
path: distPath,
// publicPath: 'http://cdn.abc.com' // 修改所有静态文件url的前缀
},
第二,配置 html 插件
plugins: [
// 生成 index.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'index.html'),
filename: 'index.html',
// chunks 表示该页面要引用哪些 chunk (即上面的 index 和 other),默认全部引用
chunks: ['index'] // 只引用 index.js
}),
// 生成 other.html
new HtmlWebpackPlugin({
template: path.join(srcPath, 'other.html'),
filename: 'other.html',
chunks: ['other'] // 只引用 other.js
}),
]
抽离公共代码
多个页面或者入口,如果引用了同一段代码,如上文的多页面例子中,index.js
和 other.js
都引用了 import './common.js'
,则 common.js
应该被作为公共模块打包。webpack v4
开始弃用了 commonChunkPlugin
改用 splitChunks
,可修改 build/webpack.prod.js
中的配置
module.exports = smart(webpackCommonConf,{
optimization: {
// 分割代码块
splitChunks: {
// 缓存分组
cacheGroups: {
// 公共的模块
common: {
chunks: 'initial',
minSize: 0, // 公共模块的大小限制
minChunks: 2 // 公共模块最少复用过几次
}
}
}
},
})
重新运行 npm run build
,即可看到有 common
模块被单独打包出来,就是 common.js
的内容。
第三方模块
同理,如果我们的代码中引用了 jquery lodash
等,也希望将第三方模块单独打包,和自己开发的业务代码分开。这样每次重新上线时,第三方模块的代码就可以借助浏览器缓存,提高用户访问网页的效率。修改配置文件,增加下面的 vendor: {...}
配置。
optimization: {
// 此配置用于打包时压缩css文件
minimizer: [new TerserWebpackPlugin({}), new OptimizeCssAssetsWebpackPlugin({})],
// 分割代码块,用于抽离公共代码
splitChunks: {
// 缓存分组
cacheGroups: {
// 第三方模块
vendor:{
priority: 1, // 权限更高,优先抽离,很重要!!
test: /node_modules/,
chunks: 'initial',
minSize: 0, // 大小限制
minChunks: 1 // 最少复用过几次
},
// 公用的模块
common: {
chunks: 'initial',
minSize: 0, // 公共模块的大小限制
minChunks: 2, // 公共模块最少复用过几次
}
}
}
}
重启 npm run build
,即可看到 vendor
模块被打包出来,里面是 jquery
或者 lodash
等第三方模块的内容。
懒加载
webpack
支持使用 import(...)
语法进行资源懒加载。安装 npm i @babel/plugin-syntax-dynamic-import -D
然后将插件配置到 .babelrc
中。
新建 src/dynamic-data.js
用于测试,内容是 export default { message: 'this is dynamic' }
。然后在 src/index.js
中加入
setTimeout(() => {
import('./dynamic-data.js').then(res => {
console.log(res.default.message) // 注意这里的 default
})
}, 1500)
重新运行 npm run dev
刷新页面,可以看到 1.5s 之后打印出 this is dynamic
。而且,dynamic-data.js
也是 1.5s 之后被加载进浏览器的 —— 懒加载,虽然文件名变了。
重新运行 npm run build
也可以看到 dynamic-data.js
的内容被打包一个单独的文件中。