Skip to content

第二章:Vue 3 源码结构-搭建框架雏形

01:前言

从本章开始我们将要去搭建咱们自己的 vue 框架项目,我们叫它 vue-next-mini 。

在搭建的过程中,我们将会参考 vue 源代码的项目结构与构建方案,从而可以建出一个小而美的 vue 框架。

所以本章的内容将会分为两个大的部分:

  1. vue 源码解析:在这部分,我们需要下载 vue 的源码,同时对它进行 debuger 和 源码阅读。
  2. 构建 vue-next-mini :在这部分,我们会参考 vue 源代码的项目结构,来创建我们自己的 vue-next-mini

那么明确好了以上内容之后,接下来就让我们来开始本章的学习吧。

02:探索源码设计:Vue3 源码设计大解析

在本小节中,我们需要做两件事情:

  1. 下载 vue 源代码(版本:V 3.2.37)
  2. 了解 vue 的源码结构

下载 vue 源代码(版本:V 3.2.37)

大家可以点击这里 进入 vue3 源代码的 github 仓库。

想要下载 vue 源代码 那么主要有三种方式(常用的,忽略了不常用的比如 Github cli):

  1. 直接点击 code -> Download ZIP 进行下载。这样下载下来的代码是 **不包含(这会导致在打包时出现错误)**git 提交记录,仅为当前的代码。
  2. 点击 code -> 复制 HTTPS || SSH 的 URL,在一个空文件夹中通过 git 指令:
shell
	// HTTPS
	git clone https://github.com/vuejs/core.git

shell
	// SSH
	git clone git@github.com:vuejs/core.git

进行获取。 3. 点击 Fork 按钮,Fork 当前的项目到你自己的仓库下,再在你的自己仓库下执行上面两步方式进行下载。这样做可以依据当前版本生成自己的代码仓库。

那么为了保证大家下载的源码版本与课程相同,强烈推荐 大家从**这里** 执行步骤二,以保证版本与课程相同。

vue 的源码结构

js
vue-next-3.2.37-master
├── BACKERS.md	// 赞助声明	
├── CHANGELOG.md	// 更新日志
├── LICENSE	// 开源协议
├── README.md	// 项目声明文件
├── SECURITY.md	// 报告漏洞,维护安全的声明文件
├── api-extractor.json	// TypeScript 的 API 分析工具
├── jest.config.js	// 测试相关
├── netlify.toml	// 自动化部署相关
├── package.json	// npm 包管理工具
├── packages	// 核心代码区
│   ├── compiler-core	// 重要:编译器核心代码 	
│   ├── compiler-dom	// 重要:浏览器相关的编译模块
│   ├── compiler-sfc	// 单文件组件(.vue)的编译模块
│   ├── compiler-ssr	// 服务端渲染的编译模块
│   ├── global.d.ts	// 全局的 ts 声明
│   ├── reactivity	// 重要:响应式的核心代码
│   ├── reactivity-transform	// 已过期 无需关注
│   ├── runtime-core	// 重要:运行时的核心代码,内部针对不同平台进行了实现
│   ├── runtime-dom	// 重要:基于浏览器平台的运行时
│   ├── runtime-test	// runtime 测试相关
│   ├── server-renderer	// 服务器渲染
│   ├── sfc-playground	// sfc 工具 比如:https://sfc.vuejs.org/
│   ├── shared	// 重要:共享的工具类
│   ├── size-check	// 测试运行时包大小
│   ├── template-explorer	// 提供了一个线上的测试(https://template-explorer.vuejs.org)用于把 template 转化为 render
│   ├── vue	// 重要:测试实例、打包之后的 dist 都会放在这里
│   └── vue-compat	// 用于兼容 vue2 的代码
├── pnpm-lock.yaml	// 使用 pnpm 下载的依赖包版本
├── pnpm-workspace.yaml // pnpm 相关配置
├── rollup.config.js	// rollup 的配置文件
├── scripts
│   ├── bootstrap.js
│   ├── build.js
│   ├── dev.js
│   ├── filter-e2e.js
│   ├── filter-unit.js
│   ├── preinstall.js
│   ├── release.js
│   ├── setupJestEnv.ts
│   ├── utils.js
│   └── verifyCommit.js
├── test-dts
│   ├── README.md
│   ├── component.test-d.ts
│   ├── componentTypeExtensions.test-d.tsx
│   ├── defineComponent.test-d.tsx
│   ├── functionalComponent.test-d.tsx
│   ├── h.test-d.ts
│   ├── index.d.ts
│   ├── inject.test-d.ts
│   ├── reactivity.test-d.ts
│   ├── reactivityMacros.test-d.ts
│   ├── ref.test-d.ts
│   ├── setupHelpers.test-d.ts
│   ├── tsconfig.build.json
│   ├── tsconfig.json	// TypeScript 配置文件
│   ├── tsx.test-d.tsx
│   └── watch.test-d.ts
└── tsconfig.json

03:创建测试实例:在 Vue 源码中运行测试实例

那么现在我们已经大致了解了 vue 源代码的基本结构,那么接下来我们来看一下,如何在 Vue3 的代码中运行测试实例,并进行 debugger

运行 vue3 源码

  1. 因为 vue3 是通过 pnpm作为包管理工具的,所以想要运行 vue3 那么首先需要 安装 pnpm
  2. 我们可以直接通过如下指令安装 pnpm
shell
	npm install -g pnpm
  1. pnpm 会通过一个 集中管理 的方式来管理 电脑中所有项目 的依赖包,以达到 节约电脑磁盘 的目的,具体可点击 这里查看
  2. 安装完 pnpm 之后,接下来就可以在 项目根目录 下通过:
	pnpm i

的形式安装依赖 5. 等所有的依赖包安装完成之后,执行:

	pnpm run build

运行测试实例

之前的时候说过 packages/vue/examples 下放的是测试的实例,所以我们可以在这里新建一个文件夹,用来表示代码的测试实例。

  1. 创建 packages/vue/examples/read-test 文件夹
  2. 在该文件夹中创建第一个测试实例:reactive.html,并写入如下代码(这些代码可能大家会不太理解,没有关系!后面会进行详细讲解,这里只是为了测试 vue 代码的 debugger):
vue
<!DOCTYPE html>
<html lang='en'>
<head>
  <meta charset='UTF-8'>
  <title>Title</title>
  <script src='../../../dist/vue.global.js'></script>
</head>
<body>
  <div id='app'></div>
</body>
<script>
  const { reactive, effect } = Vue

  const obj = reactive({
    name: '张三'
  })

  effect(() => {
    document.querySelector('#app').innerText = obj.name;
  })

  setTimeout(() => {
    obj.name = '李四'
  }, 200)
</script>
</html>
  1. 注意:该代码不可以直接放入到浏览器中运行
  2. 需要运行在服务器中,vscode 插件 Live Server

04:跟踪解析运行行为:为 vue 开启 SourceMap

此时,我们已经成功的运行了一个测试实例代码,但是在这样的一个测试实例代码中, Vue 内部是如何执行的呢?

如果想要知道这个,那么我们需要对 vue 的代码进行 debugger 来跟踪 vue 代码的执行。 那么问题来了,如何对 Vue 进行 debugger 操作呢? 如果想要对 vue 进行 debugger 操作,那么我们必须要开启 vue 的 source-map 功能。

开启 Vue 的 SourceMap

那么如何开启 Vue 的 source-map 呢?

  1. 打开 package.json 可以发现,当我们执行 npm run build时,其实执行的是 node script/build.js 指令。
  2. 这就意味着,它的配置文件读取的是 script/build.js 这个文件。
  3. 那么在该文件中存在这样的一行代码:
js
	sourceMap ? `SOURCE_MAP:true` : ``
  1. 也就是说,这里的 sourceMap 变量,决定了 SOURCE_MAP: true 还是 ''
  2. 而这个值,最终被设置到环境变量中,在 rollup.config.js 中,通过:
js
	output.sourcemap = !!process.env.SOURCE_MAP

的形式,赋值给 output.sourcemap 6. output.sourcemap 则决定了,最终打包,是否会包含 source-map

	sourceMap: output.sourcemap
  1. 所以,根据以上代码,只要 script/build.js 中的 sourceMap 变量的值为 true,则最终会打包包含 sourcemap 的包。
  2. 那么 sourceMap 变量的值是如何确定的呢?
  3. script/build.js 中,我们可以看到如下代码:
js
	const sourceMap = args.sourcemap || args.s
  1. args 的值为
	const args = require('minimist')(process.argv.slice(2))
  1. 从代码可知, argsminimist的导出对象。
  2. 所以我们需要看下 minimist这个依赖包是干什么的?
  3. 根据官网的实例代码可知:
js
	var argv = require('minimist')(process.argv.slice(2));
  console.log(argv);
  
  $ node example/parse.js -a beep -b boop
  { _: [], a: 'beep', b: 'boop' }
  1. 我们可以在执行 npx 指令时,通过 -a beep 的形式为 require('minimist')(process.argv.slice(2));导出的值增加属性
  2. 所以,根据以上代码,我们可以在package.json 中修改 build 指令:
js
	"build": "node scripts/build.js -s"
  1. 其中的 -s 表示:我们将为 scripts/build.js 文件中的 args 新增一个属性 s
  2. 而这个 s 将决定了 sourceMap 常量的值为 true
  3. 此时,我们再执行 npm run build 可以发现,打包出的所有文件都将包含一个 xxxx.map 文件
  4. 这样我们就开后了源代码的 source-map
  5. 有了 source-map 之后,接下来我们就可以对代码进行 debugger 了。

05:授人以渔:如何针对源代码进行 debugger

此时我们已经成功开启了 SourceMap,那么开启了 sourceMap 之后有什么改变呢? 此时我们来看刚才启动的项目。 在刚才启动的项目中,按 F12 打开控制台,进入 Sources 模块,此时可以看到如下内容:

image-20230810214816844 其中左侧所展示的,就是当前使用到的 vue 源代码。

那么我们知道此时我们使用了 reactive 方法声明的响应式数据,reactive 方法对应的代码位置在 packages/reactivity/src/reactive.ts 中第 90行:

image-20230810224127166

那么此时我们就可以在这里打上一个断点,来跟踪整个 reactive 的代码执行逻辑。

刷新页面,可以看到,此时代码已经进入了 debugger

image-20230810224504671

那么这样我们就已经成功的为 vue 的测试实例开启了 debugger 功能,后续我们开发之中,就可以利用这样方式,来跟踪并查看 vue 源码的执行逻辑。

那么最后我们再总结一下这几个小节所介绍的步骤:

想要对 vue 代码执行 debugger 那么公分为一下步骤:

  1. 下载 vue 源代码,推荐通过 该仓库 下载指定版本(注意:直接下载 ZIP 文件会导致 build 出错
  2. 为源代码开启 sourcemap ,以方便后续进行 debugger
  3. packages/vue/example 中,创建文件,导入 ../../../dist/vue.global.js 书写测试实例
  4. 通过 Live Server 启动服务
  5. 在浏览器控制台的 Sources 中查看运行代码,并进行 debugger

06:授人以渔:如何阅读源码

那么在上一小节中我们已经知道了如何对 vue 的源代码进行 debugger ,但是如果想要学习或者了解 vue 的代码执行,那么光靠 debugger 是不够的,除此之外我们还需要掌握另外一个能力,那就是如何阅读源代码

阅读源代码的误区

很多同学在阅读源代码的时候,都会面临一个 误区 ,那就是: 我需要把源代码中每一行代码都读明白

这是一个非常不对的行为,很容易让我们事倍功半

所以在这里我们需要先给大家明确一点:阅读源代码绝对不是要读明白其中每一行代码的意思,而是在众多的业务代码中寻找到主线,跟随这个主线来进行阅读

阅读源码的正确姿势

想要快速、轻松的阅读源码,正确的姿势非常重要,主要有两点:

  1. 摒弃边缘情况
  2. 跟随一条主线

摒弃边缘情况

在大型项目的源码中,都会充斥着非常多的业务代码,这些业务代码是用来处理很多 边缘情况 的,如果我们过分深究这些业务代码则会让我们陷入到一个代码泥潭中,在繁琐的业务中找不到方向。

所以,我们在阅读源码之前,必须要明确好一点,那就是:仅阅读核心逻辑

跟随一条主线

对于像 vue 这种量级的项目来说,哪怕我们只去阅读它的核心代码,你也会发现也是非常困难的。

我们之前说过,vue 的核心大致可以分为三块:

  1. 响应性
  2. 运行时
  3. 编译器

每一大块的内部又分为很多的业务分支。所以哪怕仅阅读核心代码已然是一个浩大的工作量。

所以说我们还需要另外一个方式,那就是:跟随一条主线

举个例子:我们以前面的 packages/vue/examples/read-test/reactive.html 为例

在该代码中,我们通过 reactive 声明了一个响应式数据。

js
// 声明响应式数据 obj
const obj = reactive({
  name: '张三'
})

那么我们就可以以改代码为主线,来去查看 reactive 方法的主线逻辑:

  1. 首先在 reactive 方法中进行了一个判断逻辑,判断 target 是否只读的,如果是只读的就直接返回 target ,意思是:传的是啥返回啥。

  2. 如果不是只读的,则触发 createReactiveObject 方法:

image-20230810235449888

  1. createReactiveObject 方法中,又进行了一推判断,最后返回了 proxy 实例对象,所以我们得到的 obj 应该就是一个 proxy 实例

image-20230811000150858

  1. 打印 obj 你会发现确实如此

这样的一个简单的例子,就是告诉大家应该如何来通过 debugger 配合 正确姿势 来快速的阅读源代码。

总结

这一小节我们讲解了如何阅读源代码,以上方式不光可以应用到 vue 中,也可以应用到其他的框架之中,所以我们把这一小节叫做 授人以渔

当然,我们这里只是通过一个简单的方式来进行了举例,在大家实际阅读的过程之中,肯定还是会遇到很多的困难的,不过好在,在这个过程中,我们会一起来进行阅读~~

07:开始搭建自己的框架:创建 vue-next-mini

那么经过我们到现在的学习,我们大概了解了 Vue 源码中一些大概模块,并且也知道了如何对 vue 的代码进行实例测试、代码跟踪与代码阅读。

那么明确好了这些之后,接下来我们就可以创建咱们自己的 vue 框架项目:vue-next-mini。

创建 vue-next-mini 与我们之前创建项目不同,不可以再借助 vue-cli 或 vite 等脚手架工具快速生成 vue 项目基本结构了,所以我们需要从 0 来搭建这样的一个项目。

  1. 创建 vue-next-mini 文件夹

  2. 通过 VSCode 打开

  3. 在终端中,通过

    shell
    npm init -y

创建 package.json

  1. 创建 packages 文件夹,作为:核心代码 区域
  2. 创建 packages/vue 文件夹:打包、测试实例、项目整体入口模块
  3. 创建 packages/shared 文件夹:共享公共方法模块
  4. 创建 packages/compiler-core 文件夹:编辑器核心模块
  5. 创建 packages/compiler-dom 文件夹:浏览器部分编辑器模块
  6. 创建 packages/reactivity 文件夹:响应性模块
  7. 创建 packages/runtime-core 文件夹:运行时核心模块
  8. 创建 packages/runtime-dom 文件夹:浏览器部分运行时模块

因为 Vue3 是使用 TS 进行构建的,所以在我们的项目中,也将通过 TS 进行构建整个项目,那么我们又应该如何在项目中使用 ts 呢?

请看下一节:框架导入 ts 配置

08:为框架进行配置:导入ts

想要在项目使用 ts 构建(课程使用的 ts 版本为 4.7.4),那么首先我们在项目中创建对应的 tsconfig.json 配置文件。

  1. 在项目根目录中,创建 tsconfig.json 文件。

  2. tsconfig.json 文件指令编译项目所需的 入口文件编译器 配置

  3. 我们也可以通过以下指令来生成 包含默认配置tsconfig.json 文件:

    shell
    // 需要先安装 typescript 
    npm install -g typescript@4.7.4
    
    // 生成默认配置
    tsc -init
  4. tsconfig.json 中指定如下配置:

    json
    // https://www.typescriptlang.org/tsconfig 也可以使用 tsc -init 生成默认的 tsconfig.json 文件进行属性查找
    {
      // 编辑器配置
      "compilerOptions": {
        // 根目录
        "rootDir": "",
        // 严格模式标志
        "strict": true,
        // 指定类型脚本如何从给定的模块说明符查找文件。
        "moduleResolution": "node",
        // https://www.typescriptlang.org/tsconfig#esModuleInterop
        "esModuleInterop": true,
        // JS 语言版本
        "target": "es5",
        // 允许未读取局部变量
        "noUnusedLocals": false,
        // 允许未读取的参数
        "noUnusedParameters": false,
        // 允许解析 json
        "resolveJsonModule": true,
        // 支持语法迭代:https://www.typescriptlang.org/tsconfig#downlevelIteration
        "downlevelIteration": true,
        // 允许使用隐式的 any 类型,这样有助于我们简化 ts 的复杂度,从而更加专注于逻辑本身
        "noImplicitAny": false,
        // 模块化
        "module": "esnext",
        // 转化为 JavaScript 时从 TypeScript 文件中删除所有注释
        "removeComments": false,
        // 禁用 sourceMap
        "sourceMap": false,
        // https://www.typescriptlang.org/tsconfig#lib
        "lib": ["esnext", "dom"]
      },
      // 入口
      "include": [
        "packages/*/src"
      ]
    }
  5. 配置项的详细介绍,大家可以点击 这里 进行查看

09:引入代码格式化工具:prettier 让你的代码结构更加规范

因为对于 vue 而言,它是一个开源的可以被众多可发者贡献的框架项目,所以为了保证整个项目的代码书写具备统一风格, vue 导入了 eslintprettier 进行代码格式控制。

但是对于我们而言,因为这并不是一个开源的代码仓库,所以我们无需专门导入 eslint 增加项目的额外复杂度,只需要导入 prettier 帮助我们控制代码格式即可。

  1. VSCode 扩展中,安装 prettier 辅助插件

    image-20230811235035239

  2. 在项目根目录下,创建 .prettierrc 文件:

json
  {
  	// 结尾无分号
    "semi": false,
    // 全局使用单引号
    "singleQuote": true,
    // 每行长度为 80
    "printWidth": 80,
    // 不添加尾随 ,号
    "trailingComma": "none",
    // 省略箭头函数括号
    "arrowParens": "avoid"
	}
  1. 至此 prettier 配置成功。 将来我们就可以指定 prettier 作为项目的代码格式化工具了。

10:模块打包器:rollup

rollup是一个模块打包器,和 webpack 一样可以将 JavaScript 打包为指定的模块。

但是不同的是,对于 webpack 而言,它在打包的时候会产生许多 冗余的代码 ,这样的一种情况在我们开发大型项目的时候没有什么影响,但是如果我们是开发一个 的时候,那么这些冗余的代码就会大大增加库体积,这就不美好了。

所以说我们需要一个 小而美 的模块打包器,这就是 rollup

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或 应用程序

因为 rollup 并不是咱们课程的重点,所以我们不会花费大量的篇幅来讲解 rollup 的概念,只会讲解 rollup 的一些基础概念,能够满足我们当前的使用即可。大家可以把 rollup 理解为一个打包 的模块打包器,而在应用程序的打包选择 webpack。

rollup

我们可以在项目根目录下,创建 rollup.config.js 文件作为 rollup 的配置文件(就像 webpack.config.js 一样):

js
// import resolve from '@'

/**
 * 默认导出一个数组,数组的每一个对象都是一个单独的导出文件配置,详细可查:
 * https://www.rollupjs.com/guide/big-list-of-options
 */
export default [
    {
        // 入口文件
        input: 'packages/vue/src/index.ts',
        // 打包文件
        output: [ 
            // 导出一个 iife 模式的包
            {
                // 开始 sourceMap
                sourceMap: true,
                // 导出文件地址
                file: './packages/vue/dist/vue.js',
                // 生辰包的格式
                format: 'iife',
                // 变量名
                name: 'Vue'
            }
        ],
        // 插件
        plugins: [
            // ts 支持
            typescript({
                sourceMap: true     
            }),
            // 模块导入的路径补全    
            resolve(),
            // 转 commonjs 为 ESM
            commonjs()
        ]
    }
]

依赖包的详细版本为:

js
	"devDependencies": {
    "@rollup/plugin-commonjs": "^22.0.1",
    "@rollup/plugin-node-resolve": "^13.3.0",
    "@rollup/plugin-typescript": "^8.3.4",
  }

那么至此我们就配置了一个基本的 rollup 的配置文件 然后我们可以在 input 路径下创建对应的 index.ts, 并写入初始代码:

js
	console.log('hello vue-next-mini')

同时因为我们使用的是 ts,所以还需要安装:tslib typescript

shell
	pnpm i --save-dev tslib@2.4.0 typescript@4.7.4

那么至此,所有的配置完成。 此时我们可以在 package.json 中新增一个 scripts

js
	"build": "rollup -c"

打包报错:

image-20230812100709918

原因:rollup 版本太高了

解决方法:将rollup的版本降低 2.68.0

11:初见框架雏形:配置路径映射

我们知道在当前的项目中,shared 文件夹内承担的是公开的工具方法,比如我们可以创建如下文件:packages/shared/src

js
/**
 * 判断是否为一个数组
 */
export const isArray = Array.isArray

那么这个方法可能会在项目的多个地方被使用,所以我们可能会经常使用到如下代码:package/vue/src/index.ts

js
	import { isArray } from '@vue/shared'
	console.log(isArray([]))

其中,我们期望可以通过 '@vue/shared' 来直接导入 packages/shared/src/index.ts 下的 isArray 方法。 那么如果想要达到这样的效果,那么就必须要设置 tsconfig路径映射 功能。 在 tsconfig.json 中添加如下代码:

	{
    // 编辑器配置
    "compilerOptions": {
      ...
      // 设置快捷导入
      "baseUrl": ".",
      "paths": {
        "@vue/*": [
          "packages/*/src"
        ]
      }
    },
	}

这表示,我们可以通过 @vue/* 代替 packages/*/src/index 的路径

那么此时,我们的导入即可成功,可重新执行 npm run build 进行测试

12:总结

在本章节中,我们主要做了两件事情:

  1. 了解了 vue 的源码设计,同时也知道了如何对阅读框架的源代码,并且对 vue 的源码进行 debugger
  2. 创建了咱们自己的 vue-next-mini 库,并且对该项目进行了结构和配置上的初始化 那么做完了这些之后,从下一章开始我们就要开始逐步的接触到 vue 核心的代码和设计内容,并且逐步实现 vue-next-mini 了。

那么让我们拭目以待吧

Released under the MIT License.