# 源码构建

基于 NPM 托管的项目都有一个 package.json 文件,该文件是对项目的描述,我们一般会配置 script 字段作为NPM的执行脚本,Vue2源码的构建脚本如下:

{
  "scripts": {
    "dev": "node examples/server.js",
    "build": "npm run build:main && npm run build:logger",
    "build:main": "node scripts/build-main.js",
    "build:logger": "node scripts/build-logger.js",
    "lint": "eslint src test",
    "test": "npm run lint && npm run test:types && npm run test:unit && npm run test:ssr && npm run test:e2e && npm run test:esm",
    "test:unit": "jest --testPathIgnorePatterns test/e2e",
    "test:e2e": "start-server-and-test dev http://localhost:8080 \"jest --testPathIgnorePatterns test/unit\"",
    "test:ssr": "cross-env VUE_ENV=server jest --testPathIgnorePatterns test/e2e",
    "test:types": "tsc -p types/test",
    "test:esm": "node test/esm/esm-test.js",
    "coverage": "jest --testPathIgnorePatterns test/e2e --coverage",
    "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
    "release": "node scripts/release.js",
    "docs": "vuepress dev docs",
    "docs:build": "vuepress build docs"
  }
}
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

我们可以发现,跟 build 相关的命令共有三条。

当我们执行 npm run build 时,实际上就是在执行 node scripts/build-main.jsnode scripts/build-logger.js

# build:main

我们先找到 scripts/build-main.jss 文件,看他是如何构建的。

// scripts/build-main.js
const { run } = require('./build')

const files = [
  'dist/vuex.esm.browser.js',
  'dist/vuex.esm.browser.min.js',
  'dist/vuex.esm.js',
  'dist/vuex.js',
  'dist/vuex.min.js',
  'dist/vuex.common.js'
]

run('rollup.main.config.js', files)
成功
1
2
3
4
5
6
7
8
9
10
11
12
13

代码较为简单,从./build中导入run函数,然后调用run函数,传入rollup.main.config.jsfiles数组。

我们先看rollup.main.config.js文件

// rollup.main.config.js
import { createEntries } from './rollup.config'

export default createEntries([
  { input: 'src/index.js', file: 'dist/vuex.esm.browser.js', format: 'es', browser: true, transpile: false, env: 'development' },
  { input: 'src/index.js', file: 'dist/vuex.esm.browser.min.js', format: 'es', browser: true, transpile: false, minify: true, env: 'production' },
  { input: 'src/index.js', file: 'dist/vuex.esm.js', format: 'es', env: 'development' },
  { input: 'src/index.cjs.js', file: 'dist/vuex.js', format: 'umd', env: 'development' },
  { input: 'src/index.cjs.js', file: 'dist/vuex.min.js', format: 'umd', minify: true, env: 'production' },
  { input: 'src/index.cjs.js', file: 'dist/vuex.common.js', format: 'cjs', env: 'development' }
])
成功
1
2
3
4
5
6
7
8
9
10
11

上述代码可以看出,这是调用./rollup.config文件中的createEntries方法。

我们再找到createEntries的定义,看它是如何创建构建配置的。

// rollup.config.js

import buble from '@rollup/plugin-buble'
import replace from '@rollup/plugin-replace'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { terser } from 'rollup-plugin-terser'
import pkg from './package.json'

const banner = `/*!
 * vuex v${pkg.version}
 * (c) ${new Date().getFullYear()} Evan You
 * @license MIT
 */`

export function createEntries (configs) {
  return configs.map((c) => createEntry(c))
}

function createEntry (config) {
  const c = {
    input: config.input,
    plugins: [],
    output: {
      banner,
      file: config.file,
      format: config.format
    },
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }

  if (config.format === 'umd') {
    c.output.name = c.output.name || 'Vuex'
  }

  // 替换环境变量
  c.plugins.push(replace({
    __VERSION__: pkg.version,
    __DEV__: config.format !== 'umd' && !config.browser
      ? `(process.env.NODE_ENV !== 'production')`
      : config.env !== 'production'
  }))

  if (config.transpile !== false) {
    // 将 ES6+ 的代码转换为 ES5 代码
    c.plugins.push(buble())
  }

  // 解析和处理 Node.js 模块导入语句
  c.plugins.push(resolve())
  // 将 CommonJS 模块转换为 ES6 模块
  c.plugins.push(commonjs())

  if (config.minify) {
    // 压缩
    c.plugins.push(terser({ module: config.format === 'es' }))
  }

  return c
}
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

代码较为简单,可以看出来,就是循环传入的configs,并为每项添加rollup配置并导出,注释较为详细,不展开讲了

那么此刻我们知道了,createEntries方法就是传入一个数组,数组中的每一项都是一个对象,表示一个构建配置。

回过头,我们再看run函数,它是在build.js文件中定义的,它接收两个参数,一个是配置列表,一个是需要构建的文件列表。

const fs = require('fs-extra')
const chalk = require('chalk')
const execa = require('execa')
const { gzipSync } = require('zlib')
const { compress } = require('brotli')

async function run (config, files) {
  await Promise.all([build(config), copy()])
  checkAllSizes(files)
}

async function build (config) {
  await execa('rollup', ['-c', config], { stdio: 'inherit' })
}

async function copy () {
  await fs.copy('src/index.mjs', 'dist/vuex.mjs')
}

function checkAllSizes (files) {
  console.log()
  files.map((f) => checkSize(f))
  console.log()
}

// 检查文件大小
function checkSize (file) {
  const f = fs.readFileSync(file)
  const minSize = (f.length / 1024).toFixed(2) + 'kb'
  const gzipped = gzipSync(f)
  const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb'
  const compressed = compress(f)
  const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb'
  console.log(
    `${chalk.gray(
      chalk.bold(file)
    )} size:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}`
  )
}

module.exports = { run }
成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

可以看出run函数实际上就是使用rollup将生成好的配置项循环执行,并在构建完成后执行checkAllSizes函数,检查所有文件的大小。

# build:logger

然后我们找到 scripts/build-logger.js 文件,看他是如何构建的。

const { run } = require('./build')

const files = ['dist/logger.js']

run('rollup.logger.config.js', files)
成功
1
2
3
4
5

跟执行main一样,都是调用了run函数,传入了配置项和需要构建的文件列表。此处不赘述