2019年10月5日凌晨,Vue 的做者尤雨溪公布了 Vue3 的源代码。固然,它暂时还不是完整的 Vue3,而是 pre-alpha 版,只完成了一些核心功能。github 命名为 vue-next
,寓意下一代 vue 。在笔者发文前,已经有不少大佬陆续发布了一些解读 Vue3 源码的文章。可是,本文并不打算再增长一篇解读源码的文章,而是以项目参与者的视角,经过动手实践,一步步理解和搭建本身的 Vue3 项目。所以,为了达到最佳效果,建议读者,一边阅读本文,一边打开终端跟着一步步动手实践。你将掌握全部构建 Vue3 所必须的知识。css
在此以前,建议先将 nodejs 版本升级到 v10.0 以上,笔者测试过,低于 v10.0 如下版本会出现各类揪心的错误,笔者本身使用的是 v10.13.0。html
git clone https://github.com/gtvue/vue3.git
cd vue3
git log --oneline && tree -aI .git
复制代码
能够看到 github 已经帮咱们建立了如下三个基础文件,并作了初始化提交。vue
f9fa484 (HEAD -> master, origin/master, origin/HEAD) Initial commit
.
├── .gitignore
├── LICENSE
└── README.md
复制代码
cd ..
git clone https://github.com/vuejs/vue-next.git
复制代码
cd vue-next
tree --dirsfirst -aI ".git*|.vscode|*.lock" -C -L 1
复制代码
只展开第一级目录,除去 .git 开头,.vscode,以及 .lock 文件,能够看到主要有 3 个目录和 8 个文件。node
.
├── .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
复制代码
# | directories | what is it ? | how to use ? |
---|---|---|---|
1 | .circleci | 云端持续集成工具 CircleCI 配置目录 |
circleci.com |
2 | packages | 源码目录 | —— |
3 | scripts | 构建脚本目录 | —— |
# | 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 |
git checkout `git log --pretty=format:"%h" | tail -1`
git log --pretty=format:"'%an' commited at %cd : %s"
复制代码
显示,尤雨溪于 2018 年 9 月 19 日 中午 11 点 35 分首次提交了 vue-next
。时至今日已通过去了一年多。react
'Evan You' commited at Wed Sep 19 11:35:38 2018 -0400 : init (graduate from prototype)
复制代码
不妨看看尤大在第一次建立项目时,都添加了那些文件。git
$ 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 出一个新的分支,以便尽情查阅。github
git checkout -b InitialCommit
复制代码
了解 JS 项目最重要的文件莫过于 package.json
,它的做用至关于整个项目的总设计图。那么看下尤大在第一次提交时,package.json 到底有啥。typescript
是否是感受特别清爽,它简洁到只有4个字段。其中咱们须要关心的是 scripts
和 devDependencies
。构建脚本很是简单,除了熟悉的 dev
和 build
,还有一个用于对项目源码全部 TypeScript 代码进行格式化的 lint
。开发依赖也是很是精简,是采用 TypeScript 开发,并用 Rollupjs 打包 Js ,最基本的依赖安装。构建脚本 dev
和 build
依然是尤大一直热衷的方式,即将全部构建逻辑放在两个 js 文件中,scripts/dev.js
和 scripts/build.js
,并用 node
解释执行。所以,要了解整个项目的核心构建过程,就须要去研究这两个文件的实现。npm
启动开发模式的代码很是简单,只有10几行代码,实际就是使用 execa
执行项目里安装(node_modules)的可执行文件。函数原型为 execa(exefile, [arguments], [options])
,返回一个 Promise
对象。json
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]
。
rollup.config.js
打包 js ,-w 观测源文件变化,并自动从新打包process.ENV
读取,这里设置了两个环境变量,process.ENV.TARGET = process.argv[2] || 'runtime-dom'
和 process.ENV.FORMATS = umd
了解更多 rollup 参数,参考rollup 命令行参数。
一共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
,表示是这生产构建。
经过分析构建脚本 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 命令行接口-配置文件 。
你可能会问 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 。
知道项目的构建打包方式,终于要说咱们的构建目标(也是前文的 target) packages
了。咱们知道 Vue 是由 lerna
管理的多 package (npm 包)项目。这些 pacakge 就存放在 packages 目录下,每一个 pacakge 都是一个与包名相同的子目录。
tree -I *.md --dirsfirst -L 2 -C packages
复制代码
运行如下代码,尝试生产构建:
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",
复制代码
尝试单独构建:
# 先移除已经构建的 dist 目录
rm -rf packages/*/dist
npm run build:core
npm run build:observer
npm run build:runtime-dom
npm run build:scheduler
复制代码
虽然屡次提到 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 的所有)。
用于在新项目中首次初始化 lerna 。它会在项目根目录下建立 package.json
, lerna.json
文件和一个空目录 packages
,可选的 -i
或 --independent
用于设置多个 pacakges 使用独立的版本,默认使用相同的版本。 固然 vue-next
已经初始化了,就无需再次运行,而且 vue-next
使用相同的版本,目前都是 3.0.0-alpha.1
,共同的版本保存在 lerna.json
文件中。
列出项目中全部的 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
复制代码
这是 lerna 最重要的一个命令。用于在不 publish 到 npm 前,解决各 pacakages 之间相互依赖的问题。它会根据各 pacakge 下的 package.json
文件中依赖,建立本地包引用的符号链接
,至关于 npm-link
的做用,固然比起单独在每一个 package 中 link 本地依赖要简单得多。如今只须要运行一次命令,就能自动将全部 pacakges 依赖 link 起来。 这样咱们就能够在每一个 pacakage 的代码中,直接经过包名称,require 或 import 使用。
lerna bootstrap
复制代码
执行完后,就能够看到,依赖项目中其余 pacakge 的 pacake 目录下多了个 node_modules 目录,里面存储的不是实际包文件,而是一个本地 pacakge 的符号连接,所以也能节省多个 package 具备相同依赖时的磁盘空间。
检查自最近一次发布以来,有那些 pacakge 发生了改动。做用相似于 package 维度的 git-status
。
显示自最近一次发布以来,文件改动的内容。做用相似于 package 维度的 git-diff
,它会和 git-diff
同样显示文件更改的地方。 例如前文,咱们对源码作了更改,能够看到以下结果:
固然,咱们也能够指定看某个 package 的改动,只须要在命令后增长 pacakge 名称,注意不是目录名称,而是由 package.json 中的 name 字段定义的包名,例如:@vue/runtime-dom
。读者能够自行尝试。
这个不用说了,就是 npm-publish
的多包发布版。
咱们已经仔细研究了一番 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
复制代码
cd ../vue3
lerna init
复制代码
lerna 自动建立了 package.json
和 lerna.json
两个配置文件,以及存放项目全部包的 packages
目录,固然如今仍是一个什么都没有的空目录。
tree -aI .git --dirsfirst -C
复制代码
在进行下一步以前,先提交一次。
git add . && git commit -m "Add lerna for managing packages"
复制代码
将 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"
}
复制代码
安装依赖:
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 构建目录:
cd .. && cp -r vue-next/scripts vue3
复制代码
拷贝配置文件:
cp vue-next/{rollup.config.js,tsconfig.json,jest.config.js,.prettierrc} vue3
复制代码
cp -r vue-next/packages/* vue3/packages
复制代码
$ 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 |
建立本地 packages 的符号连接:
# rm -rf packages/*/{dist,node_modules}
lerna bootstrap
复制代码
启动开发模式:
yarn dev
复制代码
构建全部 packages :
yarn build
# tree -I "*.md|*.json|*.ts|__tests__|node_modules|*.html|*.js|*.css" --dirsfirst -L 2 -C packages
复制代码
查看打包文件大小:
yarn size-runtime
yarn size-compiler
yarn size
复制代码
代码规范检查:
yarn lint
复制代码
测试:
yarn test
复制代码
perfect ! 一切顺利 。
git add .
git commit -m "Start vue3"
复制代码
恭喜!你如今已经有一个本身的 Vue3 项目。不断为本身的 Vue3 贡献代码吧,值得庆幸的是,你还能够持续跟进尤大进度,而且无缝“参考”最新代码,来来完善你的项目。
本文源码地址:github.com/gtvue/vue3
编写本文耗费了笔者大量精力,若是本文让你有所收获,请不要吝惜点赞哦 👍
微信扫描二维码 获取最新技术原创