阅读某个库的源码,除了阅读功能性的代码之外,还应该多多注意工程化部分的代码,例如:Vuex 中的构建脚本和发布脚本,都很有意思,很值得我们借鉴和学习。今天,让我们一起看看 Vuex 的源码是如何构建的。
Vuex 的代码构建使用了 Rollup ,如果对其不怎么了解的话,可以先看看它的官方文档。
1,package.json
首先我们应该看 Vuex 项目的 package.json 文件,里面与构建有关的部分如下所示:
{
"scripts": {
"build": "npm run build:main && npm run build:logger",
"build:main": "node scripts/build-main.js",
"build:logger": "node scripts/build-logger.js"
}
}
这里定义了三个 npm 脚本命令,在命令行中,我们可以使用 npm run 命令 执行相应的脚本,例如:
npm run build:main
# 等同于执行了
node scripts/build-main.js
我们还可以在 scripts 中定义脚本命令执行其他的 npm 脚本命令,例如上面的 build,它就调用执行了 build:main 和 build:logger 脚本命令,下面简要说明下每个脚本的作用。
- "build:main": "node scripts/build-main.js":使用 node 执行 scripts/build-main.js 文件,用于构建 Vuex 的功能代码。
- "build:logger": "node scripts/build-logger.js":使用 node 执行 scripts/build-logger.js 文件,用于构建 logger 插件。
- "build": "npm run build:main && npm run build:logger":执行 build:main 和 build:logger 脚本命令。
下面以 "build:main": "node scripts/build-main.js" 为例说明下 Vuex 的构建,"build:logger": "node scripts/build-logger.js" 和其思路一样,就不过多赘述了。
2,/scripts/build-main.js
"build:main": "node scripts/build-main.js" 是使用 node 执行 scripts/build-main.js 文件,我们看下这个文件的具体内容:
// 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)
build-main.js 执行 build.js 文件中导出的 run 方法,参数是 'rollup.main.config.js' 以及 files,这个 'rollup.main.config.js' 是 rollup 的配置文件,files 参数与 rollup 的构建无关,它只是为 build.js 中的 checkAllSizes 方法提供参数,具体看 scripts/build.js 的内容。
3,/scripts/build.js
// /scripts/build.js
// fs-extra是系统fs模块的拓展,提供了更多便利的API,并继承了fs模块的API。
const fs = require('fs-extra')
// chalk的作用是:美化终端输出的文本
const chalk = require('chalk')
// execa的作用是:在js代码中执行shell命令
const execa = require('execa')
// gzipSync:node官方库提供的函数,用于压缩文件
const { gzipSync } = require('zlib')
// compress:第三方库brotli提供的函数,用于压缩文件
const { compress } = require('brotli')
// 导出的 run 方法,主线思路在这里。
async function run(config, files) {
// build(config) 使用 rollup 进行代码的构建;copy() 只是简单地将 src/index.mjs 拷贝到 dist/vuex.mjs。
// 注意 build(config) 和 copy() 是同时进行的。
await Promise.all([build(config), copy()])
// 检查 rollup 构建出的文件的尺寸,将相关信息打印出来
checkAllSizes(files)
}
// 执行 rollup 构建代码,配置文件是 config
async function build(config) {
await execa('rollup', ['-c', config], { stdio: 'inherit' })
}
// 简单地将 src/index.mjs 拷贝到 dist/vuex.mjs
async function copy() {
await fs.copy('src/index.mjs', 'dist/vuex.mjs')
}
// 检查 rollup 构建出的文件的尺寸,将相关信息打印出来
function checkAllSizes(files) {
console.log()
// 对每一个文件进行遍历检查
files.map((f) => checkSize(f))
console.log()
}
// 检查每个文件的函数,打印出尺寸相关的信息
function checkSize(file) {
// 使用 fs 读取文件
const f = fs.readFileSync(file)
// 文件原始的尺寸
const minSize = (f.length / 1024).toFixed(2) + 'kb'
// 使用 gzipSync 压缩文件
const gzipped = gzipSync(f)
// 使用 gzipSync 压缩过后的文件大小
const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb'
// 使用 compress 压缩文件
const compressed = compress(f)
// 使用 compress 压缩过后的文件大小
const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb'
// 使用 console.log() 输出相关信息,这里借助 chalk 输出带有样式的文本。
console.log(
`${chalk.gray(
chalk.bold(file)
)} size:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}`
)
}
module.exports = { run }
其中的 run 方法就是构建的主要思路,上面的代码都添加了注释,请自行查看。copy() 和 checkAllSizes(files) 比较简单,现在说说 build(config),build() 函数执行 rollup 构建操作,rollup 的配置文件是 config,在这里就是 'rollup.main.config.js',我们来看看这个 'rollup.main.config.js' 文件。
4,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' }
])
这个文件导出了用于构建 Vuex 功能代码的配置。我们可以看到,这个文件并不是直接导出配置,而是借助了 rollup.config.js 中的 createEntries 函数创建 rollup 配置,我们到 rollup.config.js 文件中具体看一下。
5,rollup.config.js
这里的主要内容是 rollup 的配置,可以点击 rollup 官网 查看 rollup 的使用方法。
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) {
c.plugins.push(buble())
}
c.plugins.push(resolve())
c.plugins.push(commonjs())
if (config.minify) {
c.plugins.push(terser({ module: config.format === 'es' }))
}
return c
}