一、大概思路
(一)开发阶段的打包构建
- 配置打包的入口文件和输出目录等信息。
- 清空构建目录旧文件。处理js文件。
- 处理打包js/css/vue/图片字体等。
- 将打包结果注入html。
- 启动服务器预览页面,并监听变化。
- 增加eslint检测。
- 配置sourceMap等信息。
(二)发布阶段的打包构建
- 配置打包的入口文件和输出目录等信息。
- 清空构建目录旧文件。处理js文件。
- 处理打包js/css/vue/图片字体等(发布阶段还需要对css/js进行压缩混淆)。
- 拷贝public资源。
- 将打包结果注入html。
- 增加eslint检测。
- 配置sourceMap等信息。
二、“开发阶段打包构建”具体实现
(一)准备工作
- 请确保自己本地已有npm或yarn等包管理工具,本文用npm做演示。
- 下载该演示项目,或者自行vue-cli创建一个项目。
- 在该文件夹下空白处“按shift+鼠标右键”,选中“在此处打开命令行/powershell窗口”打开命令行窗口,或者自行通过命令窗口cd到该文件目录下。
- 命令行输入
npm init
回车,自行填写信息一路回车,最后生成package.json配置文件。 - 命令行输入
npm install webpack webpack-cli -D
安装webpack打包构建工具(注意:版本不一致可能会导致报错,本篇演示用的版本是"webpack": "5.36.2"
和"webpack-cli": "3.3.12"
,详细版本见最后附录)。 - 在示例项目下新建webpack.config.js作为webpack的配置文件。
(二)配置打包入口和输出等信息
在webpack.config.js中添加以下代码,命令行输入npx webpack
运行,默认会去执行webpack.config.js文件,如果成功生成temp/main.js文件则成功(会有报错后面解决)。
const path = require('path')
module.exports = {
entry: './src/main.js',
output: {
filename: '[name]_[contenthash:8].js', // 生成文件的名字
path: path.join(__dirname, 'temp') // 生成文件放在哪,output.path必须是绝对路径
},
mode: 'none', // 设置webpack运行模式,有production/development/none三种取值,不同模式会内置不同功能
}
(三)清空输出目录的旧文件
如果多运行几次打包命令npx webpack
就会发现,生成的index.js越来越多,而我们用到的其实只有最新的那个。为了避免冗余,我们可以使用插件在每次打包之前先删除下目录里的旧文件。npm install clean-webpack-plugin -D
安装删除文件的插件,然后增加plugins配置。配置完后再执行打包npx webpack
则只会保留最新的文件。
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: '[name]_[contenthash:8].js', // 生成文件的名字
path: path.join(__dirname, 'temp') // 生成文件放在哪,output.path必须是绝对路径
},
mode: 'none', // 设置webpack运行模式,有production/development/none三种取值,不同模式会内置不同功能
plugins: [
new CleanWebpackPlugin(),
],
}
(四)添加loader处理css/js/vue/图片/字体
- 处理css:由于webpack默认只对js进行打包处理,所以要单独对css进行处理。安装css处理所需要的loader,
npm install style-loader css-loader less less-loader -D
。其中css-loader只会处理css,并不会将css代码嵌入到最后打包内容里,需要再用style-loader将css嵌入到style里。
- 多个loader时执行顺序是从下到上依次执行,所以反过来先写style-loader再css-loader(先处理再将结果插入,若有less要先转成css再用css-loader处理)。
- 处理js:webpack对js的处理只是打包合并,之所以能解析import export也是对模块化做了支持,对于一些es6新语法特性还需要用babel进行转化。
npm install babel-loader @babel/core @babel/preset-env -D
安装相关依赖,记得设置@babel/preset-env才能转码成功,因为转码插件都放在这里面。 - 处理vue: 安装vue处理需要的loader和依赖,
npm install vue vue-loader vue-template-compiler -D
,vue-loader还要搭配插件VueLoaderPlugin使用。 - 处理字体和图片:安装依赖
npm install file-loader url-loader -D
。一般较小的资源可以用url-loader转为base64进行加载,当资源较大base64会影响打包体积影响运行速度,所以超过大小用file-loader加载对应路径资源。
- 注意:在file-loader v4.3.0版本之后默认使用了esModule语法,所以会导致图片的路径变成
[object module]
。解决方案可以修改图片的引用方式,更简单的直接给loader设置
esModule: false
。
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
entry: './src/main.js',
output: {
filename: '[name]_[contenthash:8].js', // 生成文件的名字
path: path.join(__dirname, 'temp') // 生成文件放在哪,output.path必须是绝对路径
},
mode: 'none', // 设置webpack运行模式,有production/development/none三种取值,不同模式会内置不同功能
module: {
rules: [
{
test: /\.vue$/i,
use: 'vue-loader',
},
{
test: /\.js$/i,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env'],
],
},
},
},
{
test: /\.(css|less)$/i,
use: [
'style-loader',
'css-loader',
'less-loader',
],
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/i,
use: {
loader: 'url-loader', // 用url-loader将较小的资源转出base64加载,较大的会影响打包体积影响运行速度
options: {
limit: 10 * 1024, // 超过10kb大小用file-loader加载
name: 'img/[name].[contenthash:8].[ext]', // 指定file-loader处理生成路径名字
esModule: false, // 解决图片路径变成[object module]的问题
},
},
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
use: {
loader: 'url-loader', // 用url-loader将较小的资源转出base64加载,较大的会影响打包体积影响运行速度
options: {
limit: 10 * 1024, // 超过10kb大小用file-loader加载
name: 'font/[name]_[contenthash:8].[ext]', // 指定file-loader处理生成路径名字
esModule: false, // 解决字体路径变成[object module]的问题
},
},
},
],
},
plugins: [
new CleanWebpackPlugin(),
new VueLoaderPlugin(),
],
}
经过上述处理,再运行npx webpack
此时已经没有报错了。接下来我们就将打包出来的结果注入到html中并启动服务器预览看是否正常。
(五)将打包结果注入html
-
npm install html-webpack-plugin -D
安装webpack处理html的插件。plugin和loader是webpack两个核心。loader一般用于对文件内容进行编译,并将处理结果直接插入到编译后的文件。而plugin可以对文件进行增删改等其他loader做不到的事情,比如删除和拷贝文件。 - webpack中还提供了definePlugin可以用于为全局注入变量,然后在html等文件中访问该变量,值必须是可运行的js语句,即
eval(变量值)
不报错,所以如果值是字符串得加双引号否则认为是变量会报错。
依据此可以实现不同运行环境下加入不同值,比如mode: 'production'
时,会默认注入process.env.NODE_ENV = 'production'
用于判断当前运行环境。但上面我们设置了mode: 'none'
所以要自己注入这个变量。
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Webpack = require('webpack')
module.exports = {
...,
plugins: [
new CleanWebpackPlugin(),
new VueLoaderPlugin(),
new Webpack.DefinePlugin({
BASE_URL: '"../public/"', // 必须是可运行的js语句,即eval(BASE_URL)不报错,所以此处得加双引号否则认为是变量会报错
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV) || '"development"', // 直接读取node环境中的变量,如果未定义则设置为development
},
}),
new HtmlWebpackPlugin({
title: '测试标题', // 打包后html里的title
filename: 'index.html', // 打包后的文件名
template: './public/index.html', // 依据哪个模板文件来生成最后的html
}),
],
}
增加上述代码之后命令行输入npx webpack
打包,会生成temp下对应文件,浏览器手动打开temp/index.html,如果页面显示正常即成功。
(六)服务器启动并自动刷新
- 运行webpack的时候加个
--watch
,能监听文件变化自动重新打包但需要手动刷新,可以用browser-sync temp --file "**/*"
实现自动刷新。但由于又要写入磁盘再读出磁盘,会比较慢。 - webpack-dev-server能自动运行打包编译和监听刷新,将打包内容写入缓存而不是磁盘,减少了磁盘读写。默认会将构建输出的资源作为加载文件,如果没构建的资源要自己配置额外资源路径。安装插件
npm install webpack-dev-server -D
,并增加以下配置。 - 由于自动刷新整个页面会丢失原操作和文本,但有时我们希望保留原来输入框的内容只变更改动的地方,此时就需要热更新。模块热更新HMR(hot module replacement)可以实时替换改变的模块但不影响整体运行状态。通过设置
hot: true
可以开启热更新。
module.exports = {
...,
devServer: {
contentBase: ['public', '.'], // 额外指定找不到的资源(比如public里没打包到temp的静态资源)去哪里找
port: 8080,
open: true, // 是否自动启动浏览器
// hot: true, // 如果hot处理代码有报错,则仍然会自动刷新整个页面
hotOnly: true, // 只做热更新,无论有无报错也不自动刷新整个页面
},
}
如果用的是webpack-dev-server4,已经取消了hotOnly和contentBase,可以将配置改成如下
module.exports = {
...,
devServer: {
static: {
directory: './public'
}, // 额外指定找不到的资源(比如public里没打包到temp的静态资源)去哪里找
port: 8080,
open: true, // 是否自动启动浏览器
hot: true, // 如果hot处理代码有报错,则仍然会自动刷新整个页面
},
}
命令行运行npx webpack-dev-server
启动服务器,修改文件会自动编译刷新则代表成功。若出现Cannot find module 'webpack-cli/bin/config-yargs'
报错,要修改webpack-cli版本,因为webpack-cli早在4.0版本后就移除了yargs,版本兼容没做好,可以在命令行输入npm install webpack-cli@3 -D
将webpack-cli还原到3.3.12版本的版本,再次执行npx webpack-dev-server
就可以了。
(七)sourceMap配置错误定位
由于现在运行的文件是经过编译的,跟我们开发时写的代码差异比较大,如果有报错信息只能定位到编译后的代码上,而无法定位到我们开发的具体代码位置,这点不利于我们调试和处理问题。source map源码地图指的是编译后代码和源码的对应关系,解决编译之后无法查看的问题。webpack提供了devtool可以让我们配置sourceMap模式。
常用的sourceMap模式有eval-source-map、cheap-eval-source-map、cheap-module-eval-source-map
等。eval-source-map
生成了source-map文件能定位问题行和列,cheap-eval-source-map
生成了简单source-map文件只能定位行,cheap-module-eval-source-map
是定位编译前文件所在行。即带cheap不能定位列信息;带module是和没loader加工过的源代码一模一样。
开发阶段建议使用cheap-module-eval-source-map
,因为一般每行不超过80不需要定位列,且需要看源代码,虽然要定位编译前文件所在行处理多启动慢,但只有第一次慢后面修改不再重复处理不会慢。发布阶段建议用none
,即不暴露源代码,因为一般应该在开发阶段就把错误调试处理完毕。实在担心上线后有报错要调试,可以用nosources-source-map
只提供错误的行列信息但显示给用户空白代码,这样再到自己源代码里找行列就可以定位到错误位置。
module.exports = {
...,
devtool: 'eval-cheap-module-source-map', // 此处写法和官方文档不一样,因为webpack5之后的devtool检测规则是^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$
}
(八)增加eslint检测
eslint主要用于检测代码风格和语法是否符合规范,能让团队的代码保持统一规范方便维护。命令行输入npm install eslint
安装eslint,然后npx eslint --init
生成eslint配置文件。npm install eslint-loader -D
安装对应loader再增加下面loader配置。
{
test: /\.(js|vue)$/i,
exclude: '/node_modules/', // node_modules下的文件不需要eslint检测
use: 'eslint-loader',
enforce: 'pre', // 强制最先执行这个loader
},
此时在main.js里写一些语法错误,运行npx webpack-dev-server
会看到eslint报错即代表成功。下面报错表示在main.js文件的第1行第22列缺少了分号。
D:\project\test\webpack_example\src\main.js
1:22 error Missing semicolon semi
如果觉得有些检测不符合团队的开发习惯,可以前往刚才init生成的.eslinterc
配置文件里修改rules。具体的配置规则可以前往"https://eslint.cn/docs/rules/关键字"查看,比如上面的关键词是最后的semi
(表示是否最后加分号),可以前往"https://eslint.cn/docs/rules/semi"查看修改配置规则。
// 修改.eslintrc
{
"rules": {
"semi": ["error", "never"], // 不需要分号结尾
"linebreak-style": ["error", "windows"] // 采用windows的CRLF换行
}
}
至此,开发阶段的webpack打包构建已经配置完毕。
三、“发布阶段打包构建”具体实现
发布阶段的构建大致跟开发阶段一样,但由于发布的内容是面向客户的,有一些要做优化。主要区别在以下几点:
- 不需要启动服务器调试。
- 构建的输出目录不同,sourceMap等配置不同。
- 由于无法访问本地资源,需要将public等不编译资源一起拷贝到输出目录。
- 为避免源码泄露和文件过大加载过慢,css等资源要进行压缩混淆处理,而不是简单地注入。
(一)准备工作
由于开发阶段和发布阶段有很多类似操作,所以我们可以将部分公共配置抽离出来复用,然后通过不同变量和命令来执行不同的打包操作。
- 新建
webpack.common.js、webpack.dev.js、webpack.prod.js
三个文件,分别用于存放公共配置、开发阶段配置、发布阶段配置。 - 将之前的
webpack.config.js
的内容复制到webpack.common.js
里,然后webpack.dev.js
先直接简单引入导出。
const common = require('./webpack.common.js')
module.exports = common
-
npm install cross-env -D
安装兼容设置环境变量的库(window和mac不兼容NODE_ENV=development
这样设置变量),然后在package.json里的script增加以下配置(即先设置当前环境变量,再根据对应配置文件进行打包):
"scripts": {
...,
"dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
},
- 命令行输入
npm run dev
,则会直接运行package.json中scripts.dev的命令,如果能跟之前一样正常打开页面则成功。
(二)抽离devServer
首先npm install webpack-merge -D
安装webpack中专门用于合并配置信息的库(比Object.assign多做了一些特殊处理),然后将webpack.common.js
文件里的devServer剪切到webpack.dev.js
里。再次运行npm run dev
看是否正常。
// webpack.dev.js
const common = require('./webpack.common.js')
const {merge} = require('webpack-merge') // 版本不一样,有些是直接merge = 有些要{merge} =
module.exports = merge(common, {
devServer: {
contentBase: ['public', '.'], // 额外指定找不到的资源(比如public里没打包到dist的静态资源)去哪里找
port: 8080,
open: true, // 是否自动启动浏览器
// hot: true, // 如果hot处理代码有报错,则仍然会自动刷新整个页面
hotOnly: true, // 只做热更新,无论有无报错也不自动刷新整个页面
// overlay: { // 这里配置 html 页面是否显示 eslint 错误信息蒙版
// errors: true,
// warnings: true,
// },
},
})
(三)根据环境变量配置不同内容
新建webpack_config.js
文件(记得是_不是.,因为webpack.config.js
是webpack的默认运行文件),写入以下内容:
const path = require('path')
module.exports = {
dev: {
mode: 'development',
outputPath: path.join(__dirname, 'temp'),
devtool: 'eval-cheap-module-source-map',
},
build: {
mode: 'production',
outputPath: path.join(__dirname, 'dist'),
devtool: 'nosources-source-map',
},
}
更改webpack.common.js
的以下四处内容,再次运行npm run dev
跟之前一样正常打开页面则成功。
let config = require('./webpack_config.js')
config = process.env.NODE_ENV === 'production' ? config.build : config.dev
module.exports = {
...,
output: {
filename: '[name]_[contenthash:8].js', // 生成文件的名字
path: config.outputPath, // 生成文件放在哪,output.path必须是绝对路径
},
mode: 'none', // 此处修改为config.mode,为了方便演示暂时还用none
devtool: config.devtool,
plugins: [
...,
new Webpack.DefinePlugin({
...,
'process.env': {
NODE_ENV: JSON.stringify(process.env.NODE_ENV || config.env), // 要是eval能运行的语句,所以用JSON.stringify包裹字符串
},
}),
],
}
(四)拷贝public下的资源
npm install copy-webpack-plugin -D
安装插件,在webpack.prod.js
写入以下内容。然后命令行输入npm run build
打包,看到生成dist目录下各文件,点击index.html页面正常打开即代表成功。
const common = require('./webpack.common.js')
const {merge} = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = merge(common, {
plugins: [
new CopyWebpackPlugin({patterns: [{from: 'public', to: 'public'}]}), // 拷贝public到对应目录,旧版本是new CopyWebpackPlugin(['public'])
],
})
(五)压缩混淆资源
- 之前我们的css都是直接通过style-loader注入的,这样不方便进行专门的css处理、按需加载和HMR热更新。我们可以用mini-css-extract-plugin来将css提取到单独的文件中,
npm install mini-css-extract-plugin -D
安装插件,更改以下文件内容:
// webpack_config.js
module.exports = {
dev: {
...,
extract: false,
},
build: {
...,
extract: true,
},
}
// webpack.common.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.(css|less)$/i,
use: [
config.extract ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'less-loader',
],
},
],
},
plugins: [
...,
]
.concat(config.extract ? [new MiniCssExtractPlugin({filename: '[name]_[contenthash:8].css'})] : []),
}
- 上面将css单独提取出来后,我们就可以对css和js进行专门的处理压缩了。其实webpack本身就会对js的打包进行一些优化处理,但其他资源需要我们自行处理。
npm install optimize-css-assets-webpack-plugin -D
安装压缩css的插件,正常也是将该插件放在plugin选项,但官方建议放在optimization
里的minimizer
中,这样可以统一控制是否要开启压缩,比如mode: 'production'
会自动开启minimizer
。
- 注意:当我们配置了
minimizer
此项后,会导致webpack认为我们要自定义处理,则不会再自动压缩js,所以需要npm install terser-webpack-plugin -D
然后手动添加js压缩插件terser-webpack-plugin
。
// webpack.prod.js增加下面内容
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserWebpackPlugin(), // js压缩插件
new OptimizeCssAssetsWebpackPlugin(),
],
},
}
至此,发布阶段的webpack打包构建也已经配置完成。更多定制化的功能可以前往webpack官网查看。
四、附录
最后,附上本篇演示使用的各个包的版本号。如果操作中遇到版本不兼容问题,可以尝试使用下方的版本:
{
"devDependencies": {
"@babel/core": "^7.14.0",
"@babel/preset-env": "^7.14.1",
"babel-loader": "^8.2.2",
"clean-webpack-plugin": "^4.0.0-alpha.0",
"copy-webpack-plugin": "^8.1.1",
"cross-env": "^7.0.3",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^4.0.2",
"eslint-plugin-html": "^6.1.2",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-vue": "^7.9.0",
"eslint-plugin-vue-libs": "^4.0.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.3.1",
"less": "^4.1.1",
"less-loader": "^8.1.1",
"mini-css-extract-plugin": "^1.6.0",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"style-loader": "^2.0.0",
"terser-webpack-plugin": "^5.1.1",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.6",
"vue-style-loader": "^4.1.3",
"vue-template-compiler": "^2.6.12",
"webpack": "^5.36.2",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3"
},
"dependencies": {
"vue": "^2.6.12"
}
}