前言
2019年10月5日凌晨,Vue 的作者尤雨溪公布了 Vue3 的源代码。当然,它暂时还不是完整的 Vue3,而是 pre-alpha 版,只完成了一些核心功能。github 命名为 vue-next
,寓意下一代 vue 。在笔者发文前,已经有很多大佬陆续发布了一些解读 Vue3 源码的文章。但是,本文并不打算再增加一篇解读源码的文章,而是以项目参与者的视角,通过动手实践,一步步理解和搭建自己的 Vue3 项目。因此,为了达到最佳效果,建议读者,一边阅读本文,一边打开终端跟着一步步动手实践。你将掌握所有构建 Vue3 所必须的知识。
在此之前,建议先将 nodejs 版本升级到 v10.0 以上,笔者测试过,低于 v10.0 以下版本会出现各种揪心的错误,笔者自己使用的是 v10.13.0。
一. 创建项目
1. 创建 github 仓库
2. 克隆仓库到本地
代码语言:javascript复制git clone https://github.com/gtvue/vue3.git
cd vue3
git log --oneline && tree -aI .git
可以看到 github 已经帮我们创建了以下三个基础文件,并做了初始化提交。
代码语言:javascript复制f9fa484 (HEAD -> master, origin/master, origin/HEAD) Initial commit
.
├── .gitignore
├── LICENSE
└── README.md
二. 参考 vue-next
1. 克隆 vue-next
代码语言:javascript复制cd ..
git clone https://github.com/vuejs/vue-next.git
2. 查看 vue-next 目录结构
代码语言:javascript复制cd vue-next
tree --dirsfirst -aI ".git*|.vscode|*.lock" -C -L 1
只展开第一级目录,除去 .git 开头,.vscode,以及 .lock 文件,可以看到主要有 3 个目录和 8 个文件。
代码语言:javascript复制.
├── .circleci
├── packages
├── scripts
├── .prettierrc
├── README.md
├── api-extractor.json
├── jest.config.js
├── lerna.json
├── package.json
├── rollup.config.js
└── tsconfig.json
3 directories, 8 files
3. 3 个目录
# | directories | what is it ? | how to use ? |
---|---|---|---|
1 | .circleci | 云端持续集成工具 CircleCI 配置目录 | circleci.com |
2 | packages | 源码目录 | —— |
3 | scripts | 构建脚本目录 | —— |
4. 8 个文件
# | files | what is it ? | how to use ? |
---|---|---|---|
1 | .prettierrc | 代码格式化工具 prettier 的配置文件 | prettier.io |
2 | README.md | 项目介绍 | —— |
3 | api-extractor.json | TypeScript 的API提取和分析工具 api-extractor 的配置文件 | api-extractor.com |
4 | jest.config.js | JavaScript 测试框架 jest 的配置文件 | jestjs.io |
5 | lerna.json | JavaScript 多 package 项目管理工具 lerna 的配置文件 | lerna.js.org |
6 | package.json | npm 配置文件 | docs.npmjs.com |
7 | rollup.config.js | JavaScript 模块打包器 rollup 的配置文件 | rollupjs.org rollupjs.com |
8 | tsconfig.json | TypeScript 配置文件 | tslang.cn typescriptlang.org |
5. 回到初次提交
代码语言:javascript复制git checkout `git log --pretty=format:"%h" | tail -1`
git log --pretty=format:"'%an' commited at � : %s"
显示,尤雨溪于 2018 年 9 月 19 日 中午 11 点 35 分首次提交了 vue-next
。时至今日已经过去了一年多。
'Evan You' commited at Wed Sep 19 11:35:38 2018 -0400 : init (graduate from prototype)
不妨看看尤大在第一次创建项目时,都添加了那些文件。
代码语言:javascript复制$ tree --dirsfirst -aI ".git*|.vscode|*.lock" -C -L 1
.
├── packages
├── scripts
├── .prettierrc
├── lerna.json
├── package.json
├── rollup.config.js
└── tsconfig.json
2 directories, 5 files
对比现在的目录结构,第一次提交的文件要干净一些,具体来说,少了持续集成工具 CircleCI
,测试工具 jest
和 API 提取工具 api-extractor
。只有源码及源码构建和包管理相关的文件。而这些正是整个项目最重要的部分,这里我们可以把它看作是要自己开发一个类似 vue3
的 JavaScript 库所需要的启动工程。可见这些文件对我们来说是非常的重要。为了不“改变历史”,我们不妨 checkout 出一个新的分支,以便尽情查阅。
git checkout -b InitialCommit
6. package.json
了解 JS 项目最重要的文件莫过于 package.json
,它的作用相当于整个项目的总设计图。那么看下尤大在第一次提交时,package.json 到底有啥。
是不是感觉特别清爽,它简洁到只有4个字段。其中我们需要关心的是 scripts
和 devDependencies
。构建脚本非常简单,除了熟悉的 dev
和 build
,还有一个用于对项目源码所有 TypeScript 代码进行格式化的 lint
。开发依赖也是非常精简,是采用 TypeScript 开发,并用 Rollupjs 打包 Js ,最基本的依赖安装。构建脚本 dev
和 build
依然是尤大一直热衷的方式,即将所有构建逻辑放在两个 js 文件中,scripts/dev.js
和 scripts/build.js
,并用 node
解释执行。因此,要了解整个项目的核心构建过程,就需要去研究这两个文件的实现。
6.1 scripts/dev.js
启动开发模式的代码非常简单,只有10几行代码,实际就是使用 execa
执行项目里安装(node_modules)的可执行文件。函数原型为 execa(exefile, [arguments], [options])
,返回一个 Promise
对象。
const execa = require('execa')
const { targets, fuzzyMatchTarget } = require('./utils')
const target = fuzzyMatchTarget(process.argv[2] || 'runtime-dom')
execa(
'rollup',
[
'-wc',
'--environment',
`TARGET:${target},FORMATS:umd`
],
{
stdio: 'inherit'
}
)
因此,node scripts/dev.js
等效于在 package.json
中的 "dev": "rollup -wc --environment TARGET:[target],FORMATS:umd"
, 其中,[target]
来自命令参数 node scripts/dev.js [target]
。
- -wc: -w 和 -c 组合,-c 使用配置文件
rollup.config.js
打包 js ,-w 观测源文件变化,并自动重新打包 - —environment: 设置传递到文件中的环境变量,可以在JS文件中,通过
process.ENV
读取,这里设置了两个环境变量,process.ENV.TARGET = process.argv[2] || 'runtime-dom'
和process.ENV.FORMATS = umd
了解更多 rollup 参数,参考rollup 命令行参数。
6.2 scripts/build.js
一共70行代码,为了节省篇幅,这里只截取了主执行代码。这是一个异步立即调用函数,获取命令行 node scripts/build.js [target]
中 target 参数(可选)赋值给 target 变量,如果 target 不空,就单独构建 target ,为空,就构建所有 targets 。而所谓的 target 就是 vue packages/
目录下的各个子 pacakge
(和子目录名相同)。
const fs = require('fs-extra')
const path = require('path')
const zlib = require('zlib')
const chalk = require('chalk')
const execa = require('execa')
const dts = require('dts-bundle')
const { targets, fuzzyMatchTarget } = require('./utils')
const target = process.argv[2]
;(async () => {
if (!target) {
await buildAll(targets)
checkAllSizes(targets)
} else {
await buildAll(fuzzyMatchTarget(target))
checkAllSizes(fuzzyMatchTarget(target))
}
})()
...
这里 buildAll(targets)
就是一个简单的 for 循环:for (const target of targets) { await build(target) }
。因此,构建的核心是 build(target)
函数。
async function build (target) {
const pkgDir = path.resolve(`packages/${target}`)
await fs.remove(`${pkgDir}/dist`)
await execa('rollup', [
'-c',
'--environment',
`NODE_ENV:production,TARGET:${target}`
], { stdio: 'inherit' })
const dtsOptions = {
name: target === 'vue' ? target : `@vue/${target}`,
main: `${pkgDir}/dist/packages/${target}/src/index.d.ts`,
out: `${pkgDir}/dist/index.d.ts`
}
dts.bundle(dtsOptions)
console.log()
console.log(chalk.blue(chalk.bold(`generated typings at ${dtsOptions.out}`)))
await fs.remove(`${pkgDir}/dist/packages`)
}
我们发现,构建部分和 scripts/dev.js
惊人地相似。也是使用 execa
调用 rollup
,只是少了 -w
参数,即不需要监测源文件的变化。并且传递了了环境变量 process.ENV.NODE_ENV = production
,表示是这生产构建。
7. rollup.config.js
通过分析构建脚本 scripts/dev.js
和 scripts/build.js
,我们知道了,不管是开发构建还是生产构建,最终都是使用 rollup -c rollup.config.js
的方式,使用配置文件 rollup.config.js
的配置来完成 JS 的构建打包。配置文件自身也是一个 JS 脚本,意味着里面也可以有很多逻辑代码,事实上,前文讲到的环境变量TARGET
, FORMATS
, NODE_ENV
,也是用在这个文件中的。
if (!process.env.TARGET) {
throw new Error('TARGET package must be specified via --environment flag.')
}
// 此处省略 n 行 ...
const inlineFromats = process.env.FORMATS && process.env.FORMATS.split(',')
const packageFormats = inlineFromats || packageOptions.formats || defaultFormats
const packageConfigs = packageFormats.map(format => createConfig(configs[format]))
if (process.env.NODE_ENV === 'production') {
packageFormats.forEach(format => {
if (format === 'cjs') {
packageConfigs.push(createProductionConfig(format))
}
if (format === 'umd' || format === 'esm-browser') {
packageConfigs.push(createMinifiedConfig(format))
}
})
}
module.exports = packageConfigs
rollup 配置文件既可以是一个 ES
模块,也可以是一个 CommonJS
模块,这里使用的是后者。并且支持导出单个配置对象,或配置对象数组,这里导出的一个配置对象数组 packageConfigs
,这样做是为了一次打包多个模块或 package 。
rollup 配置文件参考 rollup 命令行接口-配置文件 。
8. TypeScript
你可能会问 TypeScript
在哪里? 事实上, TypeScript
是以 rollup 插件的形式使用的。 依然可以在 rollup 配置文件 rollup.config.js
创建配置对象函数 createConfig()
中找到它的踪影。
const ts = require('rollup-plugin-typescript2')
// 此处省略 n 行 ...
function createConfig(output, plugins = []) {
// 此处省略 n 行 ...
const tsPlugin = ts({
check: process.env.NODE_ENV === 'production' && !hasTSChecked,
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
tsconfigOverride: {
compilerOptions: {
declaration: process.env.NODE_ENV === 'production' && !hasTSChecked
}
}
})
return {
plugins: [
tsPlugin,
...plugins
]
}
}
顺藤摸瓜,我们发现了,TypeScript 插件 tsPlugin
指定了配置文件 tsconfig.json
。因此,要了解 rollup 打包 TypeScript 做了哪些配置,就可以”移步” tsconfig.json
文件了。关于 TypeScript
的配置可参考 tsconfig.json 。
9. packages
知道项目的构建打包方式,终于要说我们的构建目标(也是前文的 target) packages
了。我们知道 Vue 是由 lerna
管理的多 package (npm 包)项目。这些 pacakge 就存放在 packages 目录下,每个 pacakge 都是一个与包名相同的子目录。
tree -I *.md --dirsfirst -L 2 -C packages
运行以下代码,尝试生产构建:
代码语言:javascript复制npm i && npm run build
会发现在打包 observer
时会报错。错误在源码文件 packages/observer/src/autorun.ts
的第 110 行处变量定义。将const runners = new Set()
改成 const runners:Set<Autorun> = new Set()
。重新 npm run build
。
npm run build
tree -I "*.md|*.json|*.ts" --dirsfirst -L 2 -C packages
正如前文 6.2 小节所说,如果不带任何参数运行 node scripts/build.js
(npm run build
的构建脚本)将构建打包所有 packages
。 如果单独打包某一 package ,就需要指定对应包名作为参数。在项目根目录的 package.json
文件 "scripts"
字段添加如下内容:
"build:core": "node scripts/build.js core",
"build:observer": "node scripts/build.js observer",
"build:runtime-dom": "node scripts/build.js runtime-dom",
"build:scheduler": "node scripts/build.js scheduler",
尝试单独构建:
代码语言:javascript复制# 先移除已经构建的 dist 目录
rm -rf packages/*/dist
npm run build:core
npm run build:observer
npm run build:runtime-dom
npm run build:scheduler
10. lerna
虽然多次提到 Vue 是使用 lerna
管理的多 packages
项目。但是到目前为止,即使我们已经完成了所有 packages
的打包构建,依然没有看到 lerna
的用武之地。 事实上,正如我们所说,lerna 是用于管理项目里的多个 packages
,它并不参与构建。lerna 也并没有我们想象的那样复杂。这里引用一段官方的介绍:
Lerna is a tool that optimizes the workflow around managing multi-package repositories with git and npm.
翻译过来就是:lerna
是一个工作流优化工具,用于优化使用 git
和 npm
来管理在同一个 git 仓库有多个 npm 包的项目的工作流(念起来拗口,但道理很简单)。 隐含的意思就是,即使我们不使用 lerna
我们依然可以通过 git 和 npm 来管理这样的多包仓库,但是当 packages
越来越多,各 packages
之间还相互依赖,这个工作流就会变得异常复杂。而 lerna
的出现就是让这一切变得和管理一个 package
一样的简单。
既然说到这,不妨就一探究竟,lerna 到底给 vue 项目带来那些便利。首先全局安装 lerna
:
npm install --global lerna
关于 lerna 命令行的使用可以参考 官网 。这里简单演示以下几个比较常用的命令(事实上这些基本就是 lerna 的全部)。
10.1 lerna init [—independent/-i]
用于在新项目中首次初始化 lerna 。它会在项目根目录下创建 package.json
, lerna.json
文件和一个空目录 packages
,可选的 -i
或 --independent
用于设置多个 pacakges 使用独立的版本,默认使用相同的版本。 当然 vue-next
已经初始化了,就无需再次运行,并且 vue-next
使用相同的版本,目前都是 3.0.0-alpha.1
,共同的版本保存在 lerna.json
文件中。
10.2 lerna ls
列出项目中所有的 pacakges ,名称是各 pacakge 下的 package.json
中 name
字段。
$ lerna ls
info cli using local version of lerna
lerna notice cli v3.17.0
@vue/core
@vue/observer
@vue/runtime-dom
@vue/scheduler
lerna success found 4 packages
10.3 lerna bootstrap
这是 lerna 最重要的一个命令。用于在不 publish 到 npm 前,解决各 pacakages 之间相互依赖的问题。它会根据各 pacakge 下的 package.json
文件中依赖,创建本地包引用的符号连接
,相当于 npm-link
的作用,当然比起单独在每个 package 中 link 本地依赖要简单得多。现在只需要运行一次命令,就能自动将所有 pacakges 依赖 link 起来。 这样我们就可以在每个 pacakage 的代码中,直接通过包名称,require 或 import 使用。
lerna bootstrap
执行完后,就可以看到,依赖项目中其他 pacakge 的 pacake 目录下多了个 node_modules 目录,里面存储的不是实际包文件,而是一个本地 pacakge 的符号链接,因此也能节省多个 package 具有相同依赖时的磁盘空间。
10.4 lerna changed
检查自最近一次发布以来,有那些 pacakge 发生了改动。作用类似于 package 维度的 git-status
。
10.5 lerna diff [package?]
显示自最近一次发布以来,文件改动的内容。作用类似于 package 维度的 git-diff
,它会和 git-diff
一样显示文件更改的地方。 例如前文,我们对源码做了更改,可以看到如下结果:
当然,我们也可以指定看某个 package 的改动,只需要在命令后增加 pacakge 名称,注意不是目录名称,而是由 package.json 中的 name 字段定义的包名,例如:@vue/runtime-dom
。读者可以自行尝试。
10.6 lerna publish
这个不用说了,就是 npm-publish
的多包发布版。
三. 构建自己的 vue3
1. 准备工作
我们已经仔细研究了一番 vue-next
的构建工程。接下来,我们可以参照它来构建自己的 vue3
。在这之前,我们先将前文对 vue-next
的 InitialCommit
分支改动做一次提交。
git add .
git commit -m "fix type error of autorun.ts and add some build scripts"
现在在我们的工作目录下,有两个项目:vue-next 和 vue3。vue-next 是我们要参考的项目,vue3 是我们自己构建的项目。vue-next 项目有两个分支,master 和从第一次提交检出的 InitialCommit 分支,当然 InitialCommit 已经不是最初的那个分支,我们成功修复了一个 BUG,虽然改变了历史,但是无所谓,因为,我们的目的仅仅是一个参考,而不是合并进原来的历史。现在我们可以任意切换 master 分支和 InitialCommit 分支,以便根据需要参考不同地方的代码。
下面的步骤,我们都将以 vue-next
的 master 分支为参考。因此,先切换到 master 分支。
git checkout master
2. lerna 初始化
代码语言:javascript复制cd ../vue3
lerna init
lerna 自动创建了 package.json
和 lerna.json
两个配置文件,以及存放项目所有包的 packages
目录,当然现在还是一个什么都没有的空目录。
tree -aI .git --dirsfirst -C
在进行下一步之前,先提交一次。
代码语言:javascript复制git add . && git commit -m "Add lerna for managing packages"
3. 构建工程
将 vue-next
根目录下的 package.json 中 “scripts” 复制到 vue3
的 package.json 中:
"scripts": {
"dev": "node scripts/dev.js",
"build": "node scripts/build.js",
"size-runtime": "node scripts/build.js runtime-dom -p -f esm-browser",
"size-compiler": "node scripts/build.js compiler-dom -p -f esm-browser",
"size": "yarn size-runtime && yarn size-compiler",
"lint": "prettier --write --parser typescript 'packages/**/*.ts'",
"test": "jest"
}
安装依赖:
代码语言:javascript复制yarn add -D typescript brotli chalk execa fs-extra lint-staged minimist prettier yorkie
yarn add -D rollup rollup-plugin-alias rollup-plugin-json rollup-plugin-replace rollup-plugin-terser rollup-plugin-typescript2
yarn add -D jest ts-jest @types/jest
拷贝整个 scripts 构建目录:
代码语言:javascript复制cd .. && cp -r vue-next/scripts vue3
拷贝配置文件:
代码语言:javascript复制cp vue-next/{rollup.config.js,tsconfig.json,jest.config.js,.prettierrc} vue3
4. 拷贝最新源码
代码语言:javascript复制cp -r vue-next/packages/* vue3/packages
5. 最新源码的 package
代码语言:javascript复制$ cd vue3 && lerna ls
lerna notice cli v3.16.5
@vue/compiler-core
@vue/compiler-dom
@vue/reactivity
@vue/runtime-core
@vue/runtime-dom
@vue/runtime-test
@vue/server-renderer
vue
lerna success found 8 packages
$ tree -I "*.ts" -L 1 -C packages
packages
├── compiler-core
├── compiler-dom
├── reactivity
├── runtime-core
├── runtime-dom
├── runtime-test
├── server-renderer
├── shared
├── template-explorer
└── vue
10 directories, 0 files
可以看到有 10 个目录,但只有 8 个 pacakge 。这是因为,lerna 只对包含 package.json 文件, 并且 “private” 字段不为 True 的目录才会识别成一个 package ,当然这对 npm 也是必须的。这 8 个目录以及对应的包名如下:
目录 | package |
---|---|
compiler-core | @vue/compiler-core |
compiler-dom | @vue/compiler-dom |
reactivity | @vue/reactivity |
runtime-core | @vue/runtime-core |
runtime-dom | @vue/runtime-dom |
runtime-test | @vue/runtime-test |
server-renderer | @vue/server-renderer |
vue | vue |
6. 构建测试
创建本地 packages 的符号链接:
代码语言:javascript复制# rm -rf packages/*/{dist,node_modules}
lerna bootstrap
启动开发模式:
代码语言:javascript复制yarn dev
构建所有 packages :
代码语言:javascript复制yarn build
# tree -I "*.md|*.json|*.ts|__tests__|node_modules|*.html|*.js|*.css" --dirsfirst -L 2 -C packages
查看打包文件大小:
代码语言:javascript复制yarn size-runtime
yarn size-compiler
yarn size
代码规范检查:
代码语言:javascript复制yarn lint
测试:
代码语言:javascript复制yarn test
perfect ! 一切顺利 。
7. 提交
代码语言:javascript复制git add .
git commit -m "Start vue3"
The End
恭喜!你现在已经有一个自己的 Vue3 项目。不断为自己的 Vue3 贡献代码吧,值得庆幸的是,你还可以持续跟进尤大进度,并且无缝“参考”最新代码,来来完善你的项目。
本文源码地址:https://github.com/gtvue/vue3