從零開始構建 vue3

前言

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

一. 建立項目

1. 建立 github 倉庫

2. 克隆倉庫到本地

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
複製代碼

二. 參考 vue-next

1. 克隆 vue-next

cd ..
git clone https://github.com/vuejs/vue-next.git
複製代碼

2. 查看 vue-next 目錄結構

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
複製代碼

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. 回到初次提交

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
複製代碼

6. package.json

瞭解 JS 項目最重要的文件莫過於 package.json ,它的做用至關於整個項目的總設計圖。那麼看下尤大在第一次提交時,package.json 到底有啥。typescript

是否是感受特別清爽,它簡潔到只有4個字段。其中咱們須要關心的是 scriptsdevDependencies 。構建腳本很是簡單,除了熟悉的 devbuild,還有一個用於對項目源碼全部 TypeScript 代碼進行格式化的 lint 。開發依賴也是很是精簡,是採用 TypeScript 開發,並用 Rollupjs 打包 Js ,最基本的依賴安裝。構建腳本 devbuild 依然是尤大一直熱衷的方式,即將全部構建邏輯放在兩個 js 文件中,scripts/dev.jsscripts/build.js ,並用 node 解釋執行。所以,要了解整個項目的核心構建過程,就須要去研究這兩個文件的實現。npm

6.1 scripts/dev.js

啓動開發模式的代碼很是簡單,只有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]

  • -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.jsscripts/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
複製代碼

運行如下代碼,嘗試生產構建:

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.jsnpm 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
複製代碼

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 是一個工做流優化工具,用於優化使用 gitnpm 來管理在同一個 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.jsonname 字段。

$ 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-nextInitialCommit 分支改動作一次提交。

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 初始化

cd ../vue3
lerna init
複製代碼

lerna 自動建立了 package.jsonlerna.json 兩個配置文件,以及存放項目全部包的 packages 目錄,固然如今仍是一個什麼都沒有的空目錄。

tree -aI .git --dirsfirst -C
複製代碼

在進行下一步以前,先提交一次。

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"
}
複製代碼

安裝依賴:

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
複製代碼

4. 拷貝最新源碼

cp -r vue-next/packages/* vue3/packages
複製代碼

5. 最新源碼的 package

$ 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 的符號連接:

# 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 ! 一切順利 。

7. 提交

git add .
git commit -m "Start vue3"
複製代碼

The End

恭喜!你如今已經有一個本身的 Vue3 項目。不斷爲本身的 Vue3 貢獻代碼吧,值得慶幸的是,你還能夠持續跟進尤大進度,而且無縫「參考」最新代碼,來來完善你的項目。

本文源碼地址:github.com/gtvue/vue3

Thank you

編寫本文耗費了筆者大量精力,若是本文讓你有所收穫,請不要吝惜點贊哦 👍

閱讀原文


微信掃描二維碼 獲取最新技術原創

相關文章
相關標籤/搜索