Vue 项目 build 流程解析(webpack工具解析)
注:本篇文章解析框架为 vue2.0
本篇文章通过解析简单的项目打包步骤试着去了解我们的 Vue
项目是怎么打包的。
build.js 干了什么
首先我们贴上 build.js
代码,方便后续解读:
'use strict'
// 版本校验解析
require('./check-versions')()
process.env.NODE_ENV = 'production'
// 引用解析
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
// 执行build解析
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
从上述代码备注中,我们将代码分开三部解析,分别为:版本校验解析、引用解析、项目打包
版本校验
执行的第一项就是版本校验,我们打开代码看看校验里面做了什么事情
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
首先是引入工具及配置,作用分别为:
- chalk:能打印颜色信息的插件,主要用来打印一些重要信息
- semver:语义化版本工具,在本文件中主要做版本提出及当前版本是否符合版本规则校验,详细用途可参照语义化版本控制模块-Semver
- packageConfig:
package
配置内容,本文件中主要获取node
版本与npm
版本 - shelljs:
shelljs
是脚本语言解析器,可以调用其中的方法做到执行底层操作命令,比如shell.cd('lib');
进入lib
目录,本文件中主要校验npm
环境
接下来是一个工具方法:
// 执行命令并返回执行结果
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
初始化获取 node
信息及 npm
信息
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version), // 提取当前node版本
versionRequirement: packageConfig.engines.node // 提取框架版本要求
}
]
// 检查是否有npm运行环境
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'), // 获取当前npm版本
versionRequirement: packageConfig.engines.npm // 获取框架版本要求
})
}
最后执行我们的校验方法:
module.exports = function () {
// 填充报错内容的数组
const warnings = []
// 循环校验(其实只有npm与node)当前环境版本与要求版本是否匹配
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
// 判断当前环境版本是否符合项目版本要求,如果不符合,我们将报错提示放入报错数组中
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
// 判断当前报错数组是否有内容(是否有环境不符合项目要求),如果有,打印报错内容并退出本次进程
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
总的来说,check-version.js
只做了一件事,就是校验 npm
与 node
环境是否符合项目标准。
当然,如果我们想加一些其他的校验也可以,比如我们在打包前要求 webpack
版本低于 7.19.5
,我们可以这样配置:
首先我们在 package.json
配置项目要求版本:
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0",
"webpack": "< 7.19.5"
},
然后,我们在 check-version.js
中添加校验 webpack
的配置项:
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
{
name: 'webpack',
currentVersion: exec('npm webpack -v'),
versionRequirement: packageConfig.engines.webpack
}
]
这时候我们执行打包命令: npm run build
可以看到报错如下:
因为此时我们 webpack
版本是 7.20.5
,而项目要求版本是小于 7.19.5
所以,编译失败。
引用解析
做完版本校验之后,我们开始执行打包逻辑,首先最重要的事就是改变当前的环境为 production
,这是为了确保有些生产环境不需要打包进去的工具或者逻辑被打包(比如上一篇讲的 mockjs
),其次便是一些工具与配置项引入:
// 将当前环境置为生产环境
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
// 创建一个动态的打包进度文本打印
const spinner = ora('building for production...')
spinner.start()
这一块引入的工具及配置功能如下
- ora:这是一个优雅的终端旋转器,简单的来说就是可以在终端显示文本前加一个旋转的棍儿,用来标识当前正在执行状态,缓解使用者等待情绪
- rimraf:这个就是一个包装
rm -rf
命令的工具,工具使用方法为rimraf(path, callback)
- path:处理文件路径工具
- chalk:这个上面有讲,有颜色的打印插件
- webpack:打包工具,后面会讲到
- config:项目配置,里面包含打包路径、是否开启分析图等
- webpackConfig:
webpack
配置,其中包含webpack
打包入口、出口、插件等
项目打包
最后要执行的便是核心的一步:项目打包,打包工具为 webpack
,其配置及作用我将在后面说明,首先看一下打包代码:
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
打包分为三部:
- 打包目录下文件删除:使用
rm
方法执行删除命令,删除掉打包目录下面的文件夹 - 执行打包:
webpack
进行打包 - 打包结果展示:关闭
spinner
展示,打印提示及报错
webpack 配置了什么
首先要说到的是 webpack
是什么?
官网描述:webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
简单地说就是从一个或者多个入口开始,顺藤摸瓜的将所有引用集合然后压缩,在加载界面时能够进行模块式的加载一个 js
文件与其相关资源文件。在 webpack
中也可以配置不同的模块来适配多种语言,例如解析 TypeScript
、Sass
等,也可以对我们 js
代码进行兼容性处理。
webpack
核心配置如下:
- Entry:入口,打包入口文件,递归解析依赖的源头
- Module:模块 ,
webpack
模块配置,用于引入工具模块用作语言翻译 - resolve:模块规则,配置以什么规则寻找模块,配置路径映射等
- output:输出结果,在
Webpack
经过一系列处理并得出最终想要的代码后输出结果
在 vue
项目中,webpack
打包的配置分为两块: webpack.base.conf.js
及 webpack.prod.conf.js
,前者为公共配置,后者为打包的配置。
公共配置
路径配置
首先是三个打包路径相关配置:
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
- context:默认执行启动
webpack
时所在的当前工作目录,配置中所有路径以该路径为基础,当前以项目目录为基础 - entry:配置模块入口,当前项目入口为
src
下main.js
- output:该项配置打包后文件存放及读取位置
path
为打包位置,默认为static
目录filename
配置入口文件名,name
为entry
对象key
,例如上面生成app.js
, 如果有多个入口,也会生成多个文件publicPath
为静态资源的路径,默认为/
,可配置为其他外部地址
依赖解析
resolve
配置项
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
两个配置作用如下:
- extensions:当我们引入组件
demo.vue
时,我们可以import demo from demo
也可以import demo from demo.vue
那么系统怎么知道引入的是什么文件呢?那就依赖于这个配置项,在引入的时候,系统会先寻找demo.js
发现没有之后会寻找demo.vue
以此按顺序比对 - alias:该配置项为路径映射,在上述例子中配置项配置两个映射:
vue$
、@
,当我们要引入src/component/demo.vue
,则可写为@/component/demo.vue
文件解析
文件解析的目的有三个:
- 对于浏览器来说,是无法识别
.vue
、.less
这种文件的,所以我们在编译的时候需要工具将其转化为.js
文件 js
代码在不同浏览器上需要进行兼容性处理- 引入
Eslint
执行校验
配置模板分成三块:
- 匹配文件:通过
test
、include
、exclude
匹配需要解析的文件 - 解析规则:通过
use
配置解析模块数组,也可通过loader
配置一组解析模块,解析顺序从右往左按顺序处理 - 顺序调整:通过
enforce
配置,修改默认执行顺序,可以将一个loader
执行顺序放置在最前或者最后
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
}, // babel解析js文件,适配js代码兼容性
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}, // 将8KB以下的图片进行base64转换,减少界面加载图片时间
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},// 将8KB以下的媒体文件进行base64转换,减少界面加载视频时间
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}, // 将8KB以下的图标文件进行base64转换,减少界面加载图标文件时间
{
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader'
} // 将less文件分别通过less-loader、css-loader、style-loader流水线对less文件进行解析
]
},
首先第一项配置的是 ESlint
解析配置,由于 Eslint
配置并不是必须的,所以我们通过配置项判断是否解析,Eslint
配置如下:
const createLintingRule = () => ({
// 匹配.js或者.vue文件
test: /\.(js|vue)$/,
// 解析模块为 eslint-loader
loader: 'eslint-loader',
// 将解析顺序排至最前
enforce: 'pre',
// 解析范围为src文件夹下及test文件夹下
include: [resolve('src'), resolve('test')],
// 指定错误报告的格式规范
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
第二项为 vue
文件的解析,解析配置比较复杂,这次暂时不做详细说明
其他配置规则已在备注中说明
打包配置
打包配置内容合并了公共配置与打包需要的配置,打包配置主要为 Plugins
扩展功能,所以其他配置暂不做介绍
webpack.DefinePlugin
配置编译时的全局常量
new webpack.DefinePlugin({
'process.env': env
}),
uglifyjs-webpack-plugin
配置 js
文件打包压缩,将 js
解析压缩成为浏览器可识别的小文件
new UglifyJsPlugin({
// uglify配置项,配置是否压缩为false
uglifyOptions: {
compress: {
warnings: false
}
},
// 是否将错误信息映射到模块(设置为true将会减慢编译速度)
sourceMap: config.build.productionSourceMap,
// 使用多进程提高构建速度
parallel: true
}),
extract-text-webpack-plugin
在没有该项配置打包时,css
将会被打包到 js
文件中,这样我们更新 css
的时候,也会再编译 js
,反之也是相同,所以我们需要将编译后的 css
与 js
分离开来(感觉这个配置对于生产环境并没有优化点)
new ExtractTextPlugin({
// 生产的css文件名
filename: utils.assetsPath('css/[name].[contenthash].css'),
// 支持提取异步引入的css
allChunks: true,
}),
optimize-css-assets-webpack-plugin
这个工具主要是做 css
压缩用的
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
html-webpack-plugin
这个工具主要是去创建一个入口的 html
文件,文件会引入 webpack
打包生成的 output
文件,如果有多个 output
也会引入多个文件,配置及作业如下所示
new HtmlWebpackPlugin({
filename: config.build.index, // 文件路径及名称
template: 'index.html', // 本地模板文件位置,这里引入项目内index.html
inject: true, // 向template注入所有静态资源,项目中将打包后生成所有的js都引入进index.html
// 传递 html-minifier 选项给 minify 输出
minify: {
removeComments: true, // 去除html注释
collapseWhitespace: true, // 折叠文本节点中的空白,如果置为false,html文件将会缩进显示
removeAttributeQuotes: true // 尽可能删除属性周围的引号
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency' // thunk插入到html的排列顺序,此处规定按照CommonsChunkPlugin规则排序
}),
webpack.HashedModuleIdsPlugin
这个工具是为了解决打包污染问题,当我们只修改部分文件时,vue
会将所有文件都重新编译一次,这样每次编译代码量会变大,浏览器需要重新获取所有的静态文件,而 webpack.HashedModuleIdsPlugin
以模块相对路径生成 hash
作为模块 id
,做到只更新变动的代码
new webpack.HashedModuleIdsPlugin()
webpack.optimize.ModuleConcatenationPlugin
这个工具能够预编译所有模块到一个闭包中,以提升代码在浏览器中执行速度
new webpack.optimize.ModuleConcatenationPlugin()
webpack.optimize.CommonsChunkPlugin
该模块主要用作公共模块的拆分独立,在进入系统的时候最开始加载一次,后续都从缓存中获取,减少大量的静态文件获取,提高加载界面速度,下面是项目内拆分规则:
// split vendor js into its own file
// 将node_modules内容拆分到vendor中
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// 该模块与上述HashedModuleIdsPlugin共同作用避免公共chunk改变
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// 从代码中提取共享模块,并将其绑定在一个单独的模块中
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
copy-webpack-plugin
该工具执行打包流程的最后一步,将打包好的文件赋值到目标目录中:
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
compression-webpack-plugin
该工具为可选工具,主要作用为准备资源的压缩版本以通过 Content-Encoding
为其提供服务,默认关闭,可在 config/index.js
中打开:
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
注意,开启该功能需要下载组件,由于高版本不兼容,建议下载老版本组件:
npm install --save-dev compression-webpack-plugin@1.1.12
webpack-bundle-analyzer
该工具为可视化资源分析工具,能够比较详细的知道那一块代码内存占用较大,通常用于发布包大小优化:
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
如果想要看到分析图,需要执行以下命令:
npm run build --report
执行后就可以看到分析图了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZQ25bGar-1639550253515)(./image/analyzer.png)]
有关 vue
项目的 build
分析基本完成,有不对的地方还请多多指正。
threshold: 10240,
minRatio: 0.8
})
)
注意,开启该功能需要下载组件,由于高版本不兼容,建议下载老版本组件:
```shell
npm install --save-dev compression-webpack-plugin@1.1.12
webpack-bundle-analyzer
该工具为可视化资源分析工具,能够比较详细的知道那一块代码内存占用较大,通常用于发布包大小优化:
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
如果想要看到分析图,需要执行以下命令:
npm run build --report
执行后就可以看到分析图了:
有关 vue
项目的 build
分析基本完成,有不对的地方还请多多指正。