从零开始构建 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

编写本文耗费了笔者大量精力,若是本文让你有所收获,请不要吝惜点赞哦 👍

阅读原文


微信扫描二维码 获取最新技术原创

相关文章
相关标签/搜索