我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
阅读一个库的源码前,最好先看一下这个项目是如何构建的,尤其是像 Vue 这样代码量大,并且支持多个版本、多个平台的开源库。通过解读项目的构建过程,也可以更加了解整体代码的架构设计。
1,从 package.json 开始
与构建有关的三个命令如下,运行不同的命令,能够进行不同 Vue 版本的构建工作。我们可以看到 "build:ssr" 和 "build:weex" 命令都是执行 "build" 命令完成任务的,只不过后面传递的参数不一样,这些参数在 build/build.js 的执行过程中起到过滤 rollup 配置对象的作用。所以,我们阅读下 build/build.js 的代码就可以了。
"scripts": {
"build": "node build/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex"
},
2,build/build.js
构建的代码都在 build/build.js 代码中。
2-1,获取 rollup 配置对象数组,根据命令参数过滤配置对象数组
// builds 是 rollup 配置对象的数组
let builds = require('./config').getAllBuilds()
// 根据命令行传递的参数,过滤掉不需要打包的版本的配置对象。
// package.json 中一共有三个打包命令:
// (1)"build": "node build/build.js",
// (2)"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
// (3)"build:weex": "npm run build -- weex",
// 不同的命令有不同的打包对象,对于不需要打包的配置对象,过滤掉
if (process.argv[2]) {
const filters = process.argv[2].split(',')
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
// filter out weex builds by default
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
}
首先执行 require('./config') 中的 getAllBuilds 方法,获取 rollup 的配置对象数组,这部分内容在下面说。
然后根据执行命令时候的参数,将配置对象数组中不需要编译的版本过滤掉。
2-2,接下来开始代码的构建工作
// 进行编译操作,传递的参数是 rollup 配置对象的数组
build(builds)
// 对 builds(rollup配置对象数组)进行遍历编译操作
function build (builds) {
let built = 0
const total = builds.length
const next = () => {
// 针对某一个配置对象,进行编译操作
buildEntry(builds[built]).then(() => {
built++
if (built < total) {
next()
}
}).catch(logError)
}
next()
}
// 根据 rollup 配置对象进行编译的方法
function buildEntry (config) {
const output = config.output
const { file, banner } = output
const isProd = /min\.js$/.test(file)
// 使用 rollup 提供的方法进行编译操作
return rollup.rollup(config)
// 编译完成之后,执行 bundle 的 generate 方法,生成目标代码
// rollup 文章的解释:
// generate code and a sourcemap
// const { code, map } = await bundle.generate(outputOptions);
.then(bundle => bundle.generate(output))
// 在这里,就能够拿到最终生成的代码了
.then(({ code }) => {
// 如果是生成生产环境的代码的话,在这里进行代码的压缩操作
if (isProd) {
// 压缩后的代码字符串:minified
var minified = (banner ? banner + '\n' : '') + uglify.minify(code, {
output: {
ascii_only: true
},
compress: {
pure_funcs: ['makeMap']
}
}).code
// 将最终生成的代码写到文件中,并且在控制台打印出日志
return write(file, minified, true)
} else {
// 将最终生成的代码写到文件中,并且在控制台打印出日志
return write(file, code)
}
})
}
构建的入口是 build 函数,在 build 函数中,对 builds 配置对象数组进行遍历执行 buildEntry 函数。
在 buildEntry 函数中借助 rollup 提供的 API 进行代码的构建。
2-3,将生成的代码字符串写到文件系统中
// 将最终生成的代码写到文件中,并且在控制台打印出日志
function write (dest, code, zip) {
return new Promise((resolve, reject) => {
// 封装一个打印日志的方法
function report (extra) {
console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
resolve()
}
// 利用 node 中的 writeFile 将生成的代码写到文件系统中
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()
}
})
})
}
使用 Node 中的 fs.writeFile() 将生成的代码字符串写到文件系统中。
3,Rollup 配置对象数组的生成
在 2-1 中,生成配置的代码是:let builds = require('./config').getAllBuilds(),所以我们先看一下 build/config.js 中导出的 getAllBuilds 函数。
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
该函数的返回值就是 Rollup 用于构建的配置对象的数组。
看一下 builds 的内容:
// 不同版本 Vue.js 的基础配置信息
const builds = {
// Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
'web-runtime-cjs': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.common.js'),
format: 'cjs',
banner
},
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.js'),
format: 'cjs',
alias: { he: './entity-decoder' },
banner
},
// Runtime only (ES Modules). Used by bundlers that support ES Modules,
// e.g. Rollup & Webpack 2
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// Runtime+compiler CommonJS build (ES Modules)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es',
alias: { he: './entity-decoder' },
banner
},
// runtime-only build (Browser)
'web-runtime-dev': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.js'),
format: 'umd',
env: 'development',
banner
},
......
}
builds 对象是不同版本 Vue 用于构建的基础配置信息,包括构建入口、构建出口、模块规范等。
然后通过 .map(genConfig) 生成真正能够应用于 Rollup 构建的配置对象数组。
// 根据 builds 中的基础配置信息,生成 rollup 能够运行的配置对象
function genConfig (name) {
// 取出该版本 Vue 所对应的基础配置对象
const opts = builds[name]
// 开始构建 Rollup 配置对象,很简单,取出 opts 中的数据,赋值到 config 对象中即可
const config = {
input: opts.entry,
external: opts.external,
plugins: [
replace({
__WEEX__: !!opts.weex,
__WEEX_VERSION__: weexVersion,
__VERSION__: version
}),
flow(),
buble(),
alias(Object.assign({}, aliases, opts.alias))
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
banner: opts.banner,
name: opts.moduleName || 'Vue'
}
}
if (opts.env) {
// replace 插件能够替换代码中的字符串
config.plugins.push(replace({
'process.env.NODE_ENV': JSON.stringify(opts.env)
}))
}
Object.defineProperty(config, '_name', {
enumerable: false,
value: name
})
return config
}
注意:从 builds 中的配置可知,Vue 构建的入口在 src/platforms 中。