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 只做了一件事,就是校验 npmnode 环境是否符合项目标准。

当然,如果我们想加一些其他的校验也可以,比如我们在打包前要求 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 中也可以配置不同的模块来适配多种语言,例如解析 TypeScriptSass 等,也可以对我们 js 代码进行兼容性处理。

webpack 核心配置如下:

  • Entry:入口,打包入口文件,递归解析依赖的源头
  • Module:模块 ,webpack 模块配置,用于引入工具模块用作语言翻译
  • resolve:模块规则,配置以什么规则寻找模块,配置路径映射等
  • output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果

vue 项目中,webpack 打包的配置分为两块: webpack.base.conf.jswebpack.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:配置模块入口,当前项目入口为 srcmain.js
  • output:该项配置打包后文件存放及读取位置
    • path 为打包位置,默认为 static 目录
    • filename 配置入口文件名,nameentry 对象 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 执行校验

配置模板分成三块:

  • 匹配文件:通过testincludeexclude 匹配需要解析的文件
  • 解析规则:通过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 ,反之也是相同,所以我们需要将编译后的 cssjs 分离开来(感觉这个配置对于生产环境并没有优化点)

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 分析基本完成,有不对的地方还请多多指正。

Logo

前往低代码交流专区

更多推荐