# 源码构建

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

{
  "scripts": {
    "build": "node build/build.js",
  }
}
成功
1
2
3
4
5

当我们执行 npm run build 时,实际上就是在执行 node build/build.js, 我们找到 build/build.js 文件,看他是如何构建的。

# 构建配置

// build/build.js

// 如果不存在dist文件夹,则创建dist文件夹
if (!fs.existsSync('dist')) {
  fs.mkdirSync('dist')
}

build(configs)
成功
1
2
3
4
5
6
7
8

主要逻辑很简单,就是先判断项目中有没有dist文件夹,如果没有则创建一个。最后执行build函数将configs传入进行构建。

我们先看configs是啥,其定义在build/build.js

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

// 拼接完整路径
const resolve = _path => path.resolve(__dirname, '../', _path)

module.exports = [
  // browser dev
  {
    file: resolve('dist/vue-router.js'),
    format: 'umd',
    env: 'development'
  },
  {
    file: resolve('dist/vue-router.min.js'),
    format: 'umd',
    env: 'production'
  },
  {
    file: resolve('dist/vue-router.common.js'),
    format: 'cjs'
  },
  {
    input: resolve('src/entries/esm.js'),
    file: resolve('dist/vue-router.esm.js'),
    format: 'es'
  },
  {
    input: resolve('src/entries/esm.js'),
    file: resolve('dist/vue-router.mjs'),
    format: 'es'
  },
  {
    input: resolve('src/entries/esm.js'),
    file: resolve('dist/vue-router.esm.browser.js'),
    format: 'es',
    env: 'development',
    transpile: false
  },
  {
    input: resolve('src/entries/esm.js'),
    file: resolve('dist/vue-router.esm.browser.min.js'),
    format: 'es',
    env: 'production',
    transpile: false
  },
  {
    input: resolve('src/composables/index.js'),
    file: resolve('./composables.mjs'),
    format: 'es'
  },
  {
    input: resolve('src/composables/index.js'),
    file: resolve('./composables.js'),
    format: 'cjs'
  }
].map(genConfig)

function genConfig (opts) {
  const config = {
    input: {
      // 如果没有指定入口文件,则使用src/index.js为入口文件
      input: opts.input || resolve('src/index.js'),
      plugins: [
        // 去除 Flow 类型注解产生的空格
        flow(),
        // 解析和处理 Node.js 模块导入语句
        node(),
        // 将 CommonJS 模块转换为 ES6 模块
        cjs(),
        // 全局替换
        replace({
          __VERSION__: version
        })
      ],
      // 声明外部依赖,打包时不构建
      external: ['vue']
    },
    output: {
      file: opts.file,
      format: opts.format,
      banner,
      name: 'VueRouter'
    }
  }

  if (opts.env) {
    config.input.plugins.unshift(
      // 替换环境变量
      replace({
        'process.env.NODE_ENV': JSON.stringify(opts.env)
      })
    )
  }

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

  return config
}
成功
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105

上述代码较为简单,可以看出来,就是处理rollup配置并导出,注释较为详细,不展开讲了

这里需要提一下的是,在处理入口文件input的时候,默认为src/index.js为入口文件。

梳理完configs后,再返回看build函数

// build/build.js

function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    // 循环构建,直到所有的构建过一遍
    buildEntry(builds[built])
      .then(() => {
        built++
        if (built < total) {
          next()
        }
      })
      .catch(logError)
  }

  next()
}

function buildEntry ({ input, output }) {
  const { file, banner } = output
  // 最后生成文件名后缀为min.js的为构建环境
  const isProd = /min\.js$/.test(file)
  return rollup
    .rollup(input)
    // generate可以生成输出,指定output为输出选项
    .then(bundle => bundle.generate(output))
    .then(bundle => {
      // console.log(bundle)
      const code = bundle.output[0].code
      if (isProd) {
        // 如果是构建环境,则压缩代码
        const minified =
          (banner ? banner + '\n' : '') +
          terser.minify(code, {
            toplevel: true,
            output: {
              ascii_only: true
            },
            compress: {
              pure_funcs: ['makeMap']
            }
          }).code
        return write(file, minified, true)
      } else {
        return write(file, code)
      }
    })
}

function write (dest, code, zip) {
  return new Promise((resolve, reject) => {
    // 打印log
    function report (extra) {
      console.log(
        blue(path.relative(process.cwd(), dest)) +
          ' ' +
          getSize(code) +
          (extra || '')
      )
      resolve()
    }
    // 写入文件
    fs.writeFile(dest, code, err => {
      if (err) return reject(err)
      if (zip) {
        // 有压缩选项则写入完毕后压缩文件
        zlib.gzip(code, (err, zipped) => {
          if (err) return reject(err)
          report(' (gzipped: ' + getSize(zipped) + ')')
        })
      } else {
        report()
      }
    })
  })
}
成功
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78

从上述代码中,我们可以看出

  1. build 函数接受上一步我们分析的 configs, 并使用promise 异步变同步循环执行 buildEntry 函数。
  2. buildEntry 函数将config作为调用rollup的参数进行构建
  3. 构建完毕后,如果是生产环境,使用terser压缩代码, 并生成压缩文件。否则直接生成文件