欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

vue npm run build 探索

程序员文章站 2022-05-30 09:48:05
...

vue npm run build 探索

vue-cli 是一个很好的工具. 它帮助我们集成了babel和webpack, 以及一些常用的插件.
让我们不用过多的关心配置, 专心业务.

但是, 秉承着一个对技术的好奇.
还是驱使我去看了看vue-cli的代码.

想要知道 npm run build 到底为我们做了什么

前言

文章涉及到一些常用的库, 大家可以先简单了解一下, 再看文章可能会更好理解.

webpack

这里只做一些简单的介绍, 具体可以看官网 .

webpack 是一个现代JavaScript应用的静态打包工具, 当webpack处理应用时,它内部建立了一个映射项目中需要的每一个模块的依赖图.

webpack 通过插件和loader, 来进行打包的处理. 非常的灵活.
但是, webpack的配置是一件极其复杂而笨重的事情.

webpack-chain

webpack-chain是一个可以动态生成的webpack配置的工具
它可以给rules和plugins进行分组具名, 从而更好地管理loader和插件.

vue-cli中也使用了webpack-chain, 用于生成webpack的配置

webpack-merge

webpack-merge用于合并webpack的配置项

const { merge } = require('webpack-merge');
// Keys matching to the right take precedence:
const output = merge(
  { fruit: "apple", color: "red" },
  { fruit: "strawberries" }
);
console.log(output);
// { color: "red", fruit: "strawberries"}

babel

babel 是一个js的编译工具. 具体可以看官网.

webpack 和 babel通过 babel-loader 关联在了一起.
使得在用webpack打包时, 可以编译代码.(例如: 把ES6的语法转成ES5)

vue-cli babel

module.exports = {
  transpileDependencies: [
    // can be string or regex
    'my-dep',
    /other-dep/
  ]
}

vue-cli webpack

vue webpack 配置方式

vue-cli 配置webpack可以通过vue.config.js中

  1. configureWebpack 简单的配置方式, 合并默认配置
  2. chainWebpack 链式操作 (高级)

从命令入手

我们知道打包时, 我们会执行npm run build

这个对应 package.json script 中的

"build": "vue-cli-service build",

npm run 会去找 node_modules 下面的 .bin 目录下 vue-cli-service

我们可以看到一个 vue-cli-service 文件. ( #!/bin/sh 是对应linux系统的脚本解释器)

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")

case `uname` in
    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../@vue/cli-service/bin/vue-cli-service.js" "[email protected]"
  ret=$?
else 
  node  "$basedir/../@vue/cli-service/bin/vue-cli-service.js" "[email protected]"
  ret=$?
fi
exit $ret

代码很少, 意思就是找 node_modules 下面的 @vue/cli-service/bin/vue-cli-service.js

// 省去一些不关键的代码  ...
const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

const command = args._[0]

service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

此时, 我们便清晰了. 想要知道怎么打包的,
那我们就要去看看 Service 到底做了什么.

Service

代码的位置
node_modules\@vue\cli-service\lib\Service.js

特别注意: 下面的代码, 我会去掉很多的代码. 但是, 不会影响到我们对整体流程的理解.

我们可以通过以下命令, 来查看vue项目应用的webpack配置(导出到一个 output.js 的文件中)

vue inspect > output.js

初始化service

首先看

const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

在源代码中, 我们可以看到定义的 class Service

其中, vue-cli plugins

Vue CLI 使用了一套基于插件的架构。如果你查阅一个新创建项目的 package.json,就会发现依赖都是以 @vue/cli-plugin- 开头的。

module.exports = class Service {
  constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) {
    process.VUE_CLI_SERVICE = this
    this.initialized = false
    this.context = context
    this.inlineOptions = inlineOptions
    this.webpackChainFns = []
    this.webpackRawConfigFns = []
    this.devServerConfigFns = []
    this.commands = {}
    // Folder containing the target package.json for plugins
    this.pkgContext = context
    // package.json containing the plugins
    this.pkg = this.resolvePkg(pkg)
    // 这里会初始化一些插件, 包括一些内置的和在package.json中配置的
    // 例如
    // "@vue/cli-plugin-babel": "^4.5.0",
    // "@vue/cli-plugin-eslint": "^4.5.0",
    this.plugins = this.resolvePlugins(plugins, useBuiltIn)
    // pluginsToSkip will be populated during run()
    this.pluginsToSkip = new Set()
    // resolve the default mode to use for each command
    // this is provided by plugins as module.exports.defaultModes
    // so we can get the information without actually applying the plugin.
    this.modes = this.plugins.reduce((modes, { apply: { defaultModes }}) => {
      return Object.assign(modes, defaultModes)
    }, {})
  }
}

vue-cli 也是基于插件式的. 下面的方法加载插件.
其中, 我们会经常加载例如 @vue/cli-plugin-babel 的插件
vue-cli 在打包时, 会读取项目中 package.json 中 @vue/cli-plugin开头的插件

  this.plugins = this.resolvePlugins(plugins, useBuiltIn)

cli-plugin-babel

我们以加载vue的babel插件为例, 看看插件做了哪些配置.

module.exports = (api, options) => {
  const useThreads = process.env.NODE_ENV === 'production' && !!options.parallel
  const cliServicePath = path.dirname(require.resolve('@vue/cli-service'))
  // 在vue.config.js中配置的 transpileDependencies
  const transpileDepRegex = genTranspileDepRegex(options.transpileDependencies)

  // 这里babel 会尝试加载配置文件
  babel.loadPartialConfigSync({ filename: api.resolve('src/main.js') })

  api.chainWebpack(webpackConfig => {
    webpackConfig.resolveLoader.modules.prepend(path.join(__dirname, 'node_modules'))
    // 通过 webpack.chain 来配置babel
    const jsRule = webpackConfig.module
      .rule('js')
        .test(/\.m?jsx?$/)
        .exclude
          .add(filepath => {
            // always transpile js in vue files
            if (/\.vue\.jsx?$/.test(filepath)) {
              return false
            }
            // exclude dynamic entries from cli-service
            if (filepath.startsWith(cliServicePath)) {
              return true
            }

            // only include @babel/runtime when the @vue/babel-preset-app preset is used
            if (
              process.env.VUE_CLI_TRANSPILE_BABEL_RUNTIME &&
              filepath.includes(path.join('@babel', 'runtime'))
            ) {
              return false
            }

            // 如果在配置的编译依赖中, 则会被babel编译
            if (transpileDepRegex && transpileDepRegex.test(filepath)) {
              return false
            }
            // Don't transpile node_modules
            return /node_modules/.test(filepath)
          })
          .end()
          // 省去了 cache-loader的处理
    jsRule
      .use('babel-loader')
        .loader(require.resolve('babel-loader'))
  })
}

通过jsRule(具名rule, 由webpack-chain 提供的方法), 来配置babel-loader.

执行run

// command就是你在命令行输入的参数
// 例如 npm run build, 那么 command 就是 build
const command = args._[0]

service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

我们看看 run, 我把分析写在了代码注释里.

// 这里, 我们可以看到根据命令, 来选择不同的环境, 从而加载不同的命令
  async run (name, args = {}, rawArgv = []) {
    // 省去了很多代码
    // load env variables, load user config, apply plugins
    this.init(mode)
    let command = this.commands[name]
    const { fn } = command
    // 如果执行的build 方法
    // 那么会 node_modules\@vue\cli-service\lib\commands\build\index.js 导出的函数fn
    return fn(args, rawArgv)
  }

  init (mode = process.env.VUE_CLI_MODE) {
    this.mode = mode

    // 这里根据不同的环境, 加载不同的文件
    // 例如: .env.production 
    // load mode .env
    if (mode) {
      this.loadEnv(mode)
    }
    // load base .env
    this.loadEnv()

    // 加载用户传入的配置项, 并且合并默认的配置
    const userOptions = this.loadUserOptions()
    this.projectOptions = defaultsDeep(userOptions, defaults())

    debug('vue:project-config')(this.projectOptions)
    // @vue/cli-plugin-babel 
    // 应用一些vue插件, 包括内置的例如build, serve 
    this.plugins.forEach(({ id, apply }) => {
      if (this.pluginsToSkip.has(id)) return
      apply(new PluginAPI(id, this), this.projectOptions)
    })

    // 链式调用webpack的配置
    if (this.projectOptions.chainWebpack) {
      this.webpackChainFns.push(this.projectOptions.chainWebpack)
    }
    // webpack原生的配置
    if (this.projectOptions.configureWebpack) {
      this.webpackRawConfigFns.push(this.projectOptions.configureWebpack)
    }
  }

我们找到了内置插件 build的代码

// apply(new PluginAPI(id, this), this.projectOptions)
// args是命令行输入的参数 api是new PluginAPI(id, this) options是this.projectOptions 
await build(args, api, options) 

node_modules\@vue\cli-service\lib\commands\build\index.js

async function build (args, api, options) {
  const webpack = require('webpack')
  const validateWebpackConfig = require('../../util/validateWebpackConfig')

  if (args.dest) {
    // Override outputDir before resolving webpack config as config relies on it (#2327)
    options.outputDir = args.dest
  }

  const targetDir = api.resolve(options.outputDir)
  const isLegacyBuild = args.target === 'app' && args.modern && !args.modernBuild

  // resolve raw webpack config
  let webpackConfig
  // ..省去根据不同模式来选择不同配置的代码
  // 并且根据用户传入的配置进行
  // chainWebpack, 
  // webpack-chain获取到最终要使用的配置 webpackConfig
  webpackConfig = require('./resolveAppConfig')(api, args, options)
  // check for common config errors
  validateWebpackConfig(webpackConfig, api, options, args.target)

  return new Promise((resolve, reject) => {
    webpack(webpackConfig, (err, stats) => {
      // 省去了一些错误处理
      resolve()
    })
  })
}

OK, 至此我们看到了, webpack() 的调用, 之后, 就开始进行打包了.
打包的工作有webpack 来处理.

总结

vue-cli 是集成了webpack, babel 及其一些常用的插件的一个脚手架.
同时, vue-cli 还使用了 webpack-chain 来处理webpack的配置文件.

并且, 通过vue.config.js的 chainWebpack 来灵活地改变配置.

vue-cli 自己又有自己的一套插件.
vue-cli 帮我们做了很多配置上的处理. 使用它的默认配置, 往往能够满足开发的需要.

当然, 如果需要更多灵活地配置, 就需要了解一些代码上的处理.

代码部分, 我只讲了大致流程. 如果想看详细的代码, 可以自己在一个 vue项目中查看.