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
編寫本文耗費了筆者大量精力,若是本文讓你有所收穫,請不要吝惜點贊哦 👍
微信掃描二維碼 獲取最新技術原創