前言

Vue3正式版发布没多久,当初赶上公司在拆分项目,很多模块都被拆分成了单独的子项目,刚好由我来负责项目的技术选型和搭建,之前的项目团队都是Vue2来开发的,Vue2的一些不足就不多说了,本来新项目想直接上react,边开发边学习的,但是考虑到了后边很可能有其他同事来接手,如果都是react代码,学习的成本就会较大,刚好Vue3也发正式版了,索性就直接上Vue3,如果只是写写业务的话,Vue2Vue3,基本上看看文档几天就能够上手直接一把梭。

Vue3用了也有一段时间了,但是对于其原理和一些新玩法新花样,还是没有完全吃透,就像问:Vue2Vue3响应式原理有啥不同?答:Vue2用的是Object.definePropertyVue3用的是Proxy,然后就没有然后了,完全经不住深挖。之前也看过Vue2的原理,经过这小两年时间的洗礼,基本上已经忘了个七七八八,所以趁着现在正在写重学前端这一系列文章,那么Vue3原理这块,也打算尽早提上日程,和之前的那些文章一样,我依旧会用最适合小白的文字,来慢慢讲解,同时对我来说,每次写一篇文章都是一次新的学习,那我们就开始吧!

学完本篇文章你能收获到

  • 什么是Monorepo
  • pnpm的基本概念以及简单用法,使用pnpm搭建Monorepo环境;
  • Vue3组成的简介,实现自己的Vue3的搭建;

Vue3设计思想的变化

我们先来了解一下Vue3Vue2在哪些设计方面有了很大的变化:

  • Vue3将很多功能都设计成了单独的模块,比如可以直接import { ref, reactive } from 'vue'使用响应式的方法,模块间耦合度低,可以独立使用;而Vue2没办法单独使用部分模块,就算只用到了响应式的部分,也只能引入完整的Vuejs
  • Vue2很多的方法,都是直接挂载到vm也就是实例上了,导致没使用的这些方法,也会被打包进最终的打包文件中;Vue3中的功能,因为进行了模块拆分,都是函数式API,所以打包的时候利用Tree-shaking机制,做到了按需引入,有效的减少打包的体积。
  • Vue3可以自定义渲染器,增强了扩展能力,暴露了很多的方法,可以进行自定义逻辑;而在一些跨平台的框架中比如小程序,如果想使用Vue2作为技术栈,则需要在Vue2的源码基础上,改动源码的逻辑,才能进行打包,这相当于破坏了源码,随着更新也会出现一些问题。

搭建开发环境

一上来如果就带着大家去看源码,相信没有多少人能够接收,直接就把文章pass掉了,所以,我们可以先自己简单实现Vue3的原理,实现一个简版的Vue3,之后我们再去调试源码,再去看,就会变得没那么困难和难以接受了。我们先搭建一个开发环境。我们要使用Monorepo的方式来搭建整个项目,那什么是Monorepo呢?

Monorepo是目前很多大型开源项目,管理代码的一个方式,就是在一个git项目仓库中管理多个模块或者工具包。Vue3的源码就是采用这种方式,将模块拆分到package目录中,那么好处就是:

  • 一个仓库可维护多个模块或工具包,不用到处找各自的仓库。
  • 方便每个模块的版本管理和依赖管理,模块之间的引用和调用变的十分方便。

我们使用pnpm这个工具,来搭建Monorepo环境。简单说下pnpm是个啥,其实就是个包管理工具,特点就是非常快,并且能节省磁盘空间。大体的使用方法和npm没啥区别,具体细节可以查看官方文档,我们这里直接用,不来细说。

上文说到Vue3采用的是Monorepo这种方式,所以,我们只需要大致写一下Vue3中包含的各种包,就能实现一个简单版本的Vue3了。常见的包有:

  • reactivity:响应式系统

  • compiler-core:编译核心

  • compiler-dom:针对浏览器的编译模块

    ……

生成项目的基本架构

首先,如果没有安装过pnpm,需要全局安装,这里使用npm install pnpm -g这个方式来安装;之后mkdir vue3-source-code来创建文件夹,使用pnpm init -y命令,初始化package.json文件。之后我们创建如下所示的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
vue3-source-code
——| packages文件夹 // 存放Vue3相关的所有包
|—— reactivity文件夹 // 响应式原理的包
|—— src
|—— index.ts // 入口文件代码
|—— shared文件夹 // 存放一些公共方法的包
|—— src
|—— index.ts // 入口文件代码
——| scripts文件夹 // 存放我们自定义的一些脚本
——| .npmrc // npm配置文件
——| package.json
——| pnpm-workspace.yaml // 管理workspace的配置文件

接下来,我们开始完善配置项和一些调试代码,让项目能够跑的通: 在项目根目录,使用tsc --init命令,来初始化tsconfig.json文件,如果没有安装过tsc,需要先全局执行npm install typescript -g命令。我们直接把以下配置填写在tsconfig.json文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ts.config.json文件
{
"compilerOptions": {
"outDir": "dist", // 输出的目录
"sourceMap": true, // 启用sourcemap
"target": "es2016", // 目标语法
"module": "esnext", // 模块格式为esm
"moduleResolution": "node", // 模块解析方式
"strict": false, // 严格模式,可以使用any
"resolveJsonModule": true, // 解析json模块
"esModuleInterop": true, // 允许通过es6语法引入commonjs模块
"jsx": "preserve", // jsx 不转义
"lib": ["esnext", "dom"], // 支持的类库 esnext及dom
"baseUrl": "./",
"paths": {
"@vue/*": ["packages/*/src"]
}
}
}

心细的朋友可能发现,最后两个配置项没有注释,我们一会再来解释这两个配置项的作用。

1
2
// .npmrc文件
shamefully-hoist = true

这个配置项非常有意思,我们来解释一下这个配置项是啥意思呢?那么这里不得不提及一下npm在安装依赖时候的特征了:那就是会将依赖拍平在node_modules文件夹中,而pnpm在安装依赖之后,则不会将依赖拍平在node_modules文件夹中。举个栗子🌰,在一个空白项目中,如果我们使用了npm install webpack命令,那么当你打开node_modules文件夹的时候,会发现安装了一大堆依赖,此时我们在项目中使用require('express'),发现依旧不会报错,因为在安装webpack的时候,也用到了express这个依赖,而且都拍平在node_modules文件夹下了,所以在项目中require('express')是能够找到,而且不会报错的;但果我们使用pnpm install webpack的话,此时再打开node_modules文件,会发现少了很多东西,观察目录结构会发现,其实依赖都被放在了.pnpm这个文件夹下,此时如果我们直接require('express'),则就会报错,因为node_modules目录下,根本不存在express模块。那么,在.npmrc文件中加入了shamefully-hoist = true这个配置项,就能够将.pnpm中的依赖,拍平在node_modules文件夹中,达到的效果就和npm很类似了。

接下来cd进入shared目录,使用pnpm init -y命令初始化,并将package.json文件中配置项改为"name": "@vue/shared" ......

1
2
3
4
// shared/src/index.ts 文件中,我们先写一个判断是否为数组的方法,并将其导出
export const isArray = value => {
return Array.isArray(value)
}

同样的方法,cd进入reactivity目录下,使用pnpm init -y命令初始化,并将package.json文件中配置项改为"name": "@vue/reactivity" ......。接下来,如果我们想在reactivity/src/index.ts文件中,使用shared包中暴露出来的那个isArray方法,那么应该如何引入呢?首先想到的就是我直接import { isArray } from '../../shared/src/index.ts'不就完了么,相对路径一把梭,乍看一眼没啥问题,但是稍微一想,像shared,reactivity这种包,最后发布可是要打包完后,单独发布到npm上边的,这时候使用相对路径,那肯定就不太合适了吧。有朋友又会说了,那直接用import { isArray } from '@vue/shared'来导入不就好了么?没错,但是如果不进行任何配置,这种写法是去哪里找@vue/shared的这个包呢?node_modules目录中,那node_modules目录中没有这个shared包啊,该怎么办呢?聪明的朋友已经还记得,上文我们在配置tsconfig.json文件的时候,埋下了一个伏笔。没错,就是最后两个配置项。

1
2
3
4
"baseUrl": "./",
"paths": {
"@vue/*": ["packages/*/src"]
}

首先,baseUrl可以将根路径定位在当项目的根目录。其次paths可以自定义寻找包的路径,比如上边配置的意思就是,只要import了以@vue/*开头的包,那么就会去packages文件夹下的*/src目录下寻找。所以加上了这个配置项,我们在reactiviey/src/index.ts文件中,就可以正常的导入shared模块了。

1
2
3
4
5
6
7
8
// reactiviey/src/index.ts
import { isArray } from '@vue/shared'

console.log(isArray([1, 2, 3]))

export {
isArray
}

pnpm-workspace.yaml文件中,我们先填写如下内容,代表packages文件夹下所有的目录,都当做包来管理。

1
2
packages: 
- 'packages/*'

至此,一个简单的Monorepo环境就已经搭建好啦~,那么有朋友可能会问了,这咋跑起来?我们写一个库肯定得边写边调试的吧,都看不到效果,怎么知道写的有没有问题呢?别急,我们继续往下走。

编写脚本进行开发环境打包

对于开发环境,我们使用esbuild包进行打包,对于生产环境,我们使用rollup进行打包。 首先在项目根目录先安装包pnpm intall esbuild -D -w,之所以加上-w是为了能够让依赖成功安装在项目根目录,不然就会报错。我们先把入口写成固定为reactivity/src/index.ts。具体的配置,可以查看esbuild的官方文档,下边就直接写上需要的配置项,并简单做下注释解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// scripts/dev.js 文件
const { context } = require('esbuild')
const path = require('path')

const target = 'reactivity'

context({
// 打包入口
entryPoints: [path.resolve(__dirname, `../packages/${target}/src/index.ts`)],
outfile: path.resolve(__dirname, `../packages/${target}/dist/${target}.js`),
bundle: true, // 把里文件中的依赖也同时打包进来
sourcemap: true, // 生成sourcemap,可以调试
format: 'esm', // 打包出来的是esm模块
platform: 'browser',
}).then(ctx => {
// 监听文件变化,只要发生了改动,就重新打包编译结果
ctx.watch().then(() => {
console.log('watching~~~')
})
})

package.json文件中添加一个命令,进行打包。

1
2
3
4
// package.json
"scripts": {
"dev": "node scripts/dev.js"
}

之后,执行npm run dev命令,可以看到,在reactivity文件夹下,生成了reactivity.jsreactivity.js.map两个文件,我们打开reactivity.js文件,可以看到,打包结果为:

1
2
3
4
5
6
7
8
9
10
11
// packages/shared/src/index.ts
var isArray = (value) => {
return Array.isArray(value);
};

// packages/reactivity/src/index.ts
console.log(isArray([1, 2, 3]));
export {
isArray
};
//# sourceMappingURL=reactivity.js.map

那么此时js文件就已经被成功的打包了,在dist目录下,我们新建个index.html文件,看看刚才打包的结果,能不能在页面上用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- reactivity/dist/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { isArray } from './reactivity.js'
console.log('index.html文件中测试代码:', isArray([2, 3, 4]))
</script>
</body>
</html>

特别要注意的是,<script>标签要加上type="module",因为我们是通过esModule的方式进行导入导出的。此时我们要启动一个本地服务器,来查看index.html文件,这里方式很多,我推荐使用一条命令,能够直接启动一个本地服务器,并且不需要安装任何东西。我们cd进入到reactivity目录下,执行npx serve dist命令(要保证npm版本≥5.2才能够使用npx),这套命令就是将dist文件夹作为服务器根目录,然后将index.html文件默认作为主文件入口进行展示,执行完毕后,可以看到默认的端口是3000,我们直接在浏览器中打开localhost:3000,打开控制台,可以发现输出了2行代码,一个是reactivity.js文件中输出的,一个是index.html文件中,导入进来输出的。

到这里,我们开发环境的基本架子,就搭建好了,至于生产环境的配置,我们之后的文章会提到,接下来的文章,我们首先来学习一下Vue3的响应式原理吧!

作者:柠檬soda水
链接:https://juejin.cn/post/7195065768231305274
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。