课程地址:【已完结】全网最详细Vue3源码解析!(一行行带你手写Vue3源码)
第一部分:实现vue3环境搭建(对应课程的第1-3节)
VUE2与VUE3的对比:
也即vue2的痛点:
- 对TypeScript支持不友好,所有属性都放在this对象上,难以推导数据类型
- 大量API挂载在vue对象原型上,难以实现TreeShaking
- 架构层面对跨平台DOM渲染开发支持不友好(这一点我不太理解)
- 推出CompositionAPI(受reactHook启发)
- 对虚拟DOM进行了重写,对模板的编译进行了优化
Monorepo介绍
Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,是一种将多个peckage放在一个repo中的代码管理模式。其有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。更多可参阅:带你了解更全面的 Monorepo - 优劣、踩坑、选型
vue3中使用 yarn workspace+lerna 来管理项目。
Monorepo环境搭建
1、项目初始化:由于Monorepo是不支持npm的,所以使用yarn来初始化一个项目:
yarn init -y
2、 Monorope分包:打开项目根目录中的package.json文件,在其中加入如下配置:private与workspaces
//package.json
{
"private": true,
"workspaces": [
"packages/*"
],
"name": "vue3",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"typescript": "^5.3.3"
},
"devDependencies": {
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3",
"execa": "^8.0.1",
"rollup": "^4.9.6",
"rollup-plugin-typescript2": "^0.36.0"
},
"type":"module"
}
- private:表示私有
- workspaces:表示分包,所有的包都会放在packages目录下
3、新建两个包文件夹
根目录下新建packages文件夹,在packages目录下新建reactivity与shared文件夹,并分别进入reactivity与shared文件夹中,分别执行 yarn init -y 生成各自的package.json文件,并在这两个文件夹下各自创建src文件,在src中创建入口文件index.ts;
在两个index.ts文件中随意编写一点不同的代码(如不写代码,稍后打包时会报错),如:
let a = 1
export {
a
}
4、安装ts:注意:如果是所有包都要用的,比如ts,那就安装在最外层的package.json中,即项目根目录中的package.json中:
yarn add typescript -D -W
-D表示开发环境;由于进行了分包,所以在根目录中安装需要加上 -W 配置项
5、通过ts的tsc命令自动生成配置项tsconfig.json:
npx tsc --init
6、安装rollup打包的相关依赖,在命令行中执行如下命令:
yarn add rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa -D -W
7、创建一个脚本执行命令scripts,在根目录中的package.json中:
8、配置打包选项,分别在reactivity和shared文件夹下的package.json文件中配置各个包对应的打包配置:
将name分别修改为"@vue/reactivity" 与 “@vue/shared”;
reactivity文件夹下的package.json增加如下配置:
shared文件夹下的package.json增加的配置中去掉global:
"buildOptions":{
"name":"VueReactivity", // shared文件夹下为"VueShared"
"formats":[
"esm-bundler",
"cjs",
"global" // shared文件夹下的package.json增的配置中去掉此行
]
}
9、编写打包配置文件build.js:
// build.js
/ 1、获取打包目录
// const fs = require("fs")
// const execa = require("execa")
import fs from "fs"
import {execa} from "execa"
// 读取packages下所有文件名字,并过滤掉非文件夹,只保留packages下的文件夹
const dirs = fs.readdirSync("packages").filter(p=>{
if(!fs.statSync(`packages/${p}`).isDirectory()){
return false
}
return true
})
console.log('888888',dirs) //[ 'reactivity', 'shared' ]
// 2、进行打包 并行打包
async function build(target){
console.log(target,666)
// await execa('rollup',['-c --bundleConfigAsCjs',"--environment",`TARGET:${target}`],{stdio:'inherit'})
await execa('rollup',['-c',"--environment",`TARGET:${target}`],{stdio:'inherit'})
}
function runParaller(dirs,itemfn){
let result = []
for(let item of dirs){
result.push(itemfn(item))
}
return Promise.all(result)
}
runParaller(dirs,build).then(()=>{
console.log("成功")
})
10、根目录下新建rollup配置文件,rollup.config.js,在其中编写配置代码:
// 编写打包配置
// import ts from 'rollup-plugin-typescript2' //解析ts
// import json from '@rollup/plugin-json'
// import resolvePlugin from '@rollup/plugin-node-resolve' // 解析第三方插件
// import path from 'path' // 处理路径
const ts = require('rollup-plugin-typescript2') //解析ts
const json = require('@rollup/plugin-json')
const resolvePlugin = require('@rollup/plugin-node-resolve') // 解析第三方插件
const path = require('path') // 处理路径
// (2)获取文件路径
let packagesDir = path.resolve(__dirname,'packages')
// console.log(packagesDir, 888) // D:\suyou\study\vue3-source-code\vue3\packages
// 2.1 获取需要打包的包
let packageDir = path.resolve(packagesDir, process.env.TARGET)
// console.log(packageDir, 888) // D:\suyou\study\vue3-source-code\vue3\packages\reactivity
// 2.1 获取到每个包的项目配置
const resolve = p => path.resolve(packageDir, p)
const pkg = require(resolve('package.json')) // 获取json
const packageOptions = pkg.buildOptions || {}
const name = path.basename(packageDir)
console.log(packageOptions, 888) //{ name: 'VueReactivity', formats: [ 'esm-bundler', 'cjs', 'global' ] }
console.log(name, 888) // reactivity
// 3 创建一个表
const outputOptions = {
"esm-bundler":{
file: resolve(`dist/${name}.esm-bundler.js`),
format:'es'
},
"cjs": {
file: resolve(`dist/${name}.cjs.js`),
format: 'cjs'
},
"global": {
file: resolve(`dist/${name}.global.js`),
format: 'iife'
},
}
const options = pkg.buildOptions
function createConfig(format,output){
// 进行打包
output.name = options.name
output.sourcemap = true
// 生成rollup配置
return {
input:resolve('src/index.ts'),
output,
plugins:[
json(),
ts({ //解析ts
tsconfig:path.resolve(__dirname,'tsconfig.json')
}),
resolvePlugin() //解析第三方插件
]
}
}
// export default options.formats.map(format => createConfig(format, outputOptions[format]))
module.exports = options.formats.map(format => createConfig(format, outputOptions[format]))
11、将 tsconfig.json中的 target 和module 字段都改为 ESNext
12、运行打包:npm run build,成功:
此时会发现reactivity文件夹和shared文件夹下会出现打包生成的dist文件夹:
13、根目录下package.json文件中新增 dev 运行脚本命令:
14、scripts目录下新建dev.js并在其中编写:
'-c’处增加w,表示实时监控文件更新并自动编译
// dev.js
import {execa} from "execa"
// 进行打包
async function build(target){
// console.log(target,666) // reactivity
// {stdio:'inherit'} 配置项表示在子进程中输出内容可以在主包中输出
await execa('rollup',['-cw','--environment',`TARGET:${target}`],{stdio:'inherit'})
}
build('reactivity')
运行npm run dev,此时可以修改代码后实时打包:
15、解决包之间的引入问题:
在reactivity的入口文件中引入shared:
import {b} from '@vue/shared'
此时 @vue/shared 处会出现波浪线,提示找不到这个模块。这时就需要到tsconfig.json中增加如下配置:
// 解决引入的问题 ts
"moduleResolution": "node", //node
"baseUrl": ".", // 根路径
"paths":{
"@vue/*":["packages/*/src"]
}
总结
1、yarn init -y初始工程
2、package.json文件中,增加:
“private”:“true”;
“workspaces”:[“packages/*”]
3、项目目录下新建文件夹packages,在其中新建两个文件夹reactivity、shared,分别进入这两个文件夹,执行yarn init -y,生成各自的package.json,分别在其package.json文件中增加配置:
修改name:“@vue/reactivity” name:“@vue/shared”
增加buildOptions:{
“name”:“VueReactivity” “name”:“VueShared”
format:[
“esm-bundler”,
“cjs”,
“global”
]
}
4、分别在文件夹reactivity、shared中新建src文件夹,并在其中新建index.ts入口文件
5、使用ts就要安装ts,因为ts是全局使用的,所以需要安装在根目录下,即在根目录下执行 yarn add typescript -D -W (-D表示开发环境,由于进行了分包,所以在根目录中安装需要加上 -W 配置项)
6、ts安装完成后,node_modules目录下会新增typescript-bin-tsc,此时使用tsc生成ts配置文件:执行:npx tsc --init,自动生成 tsconfig.json 文件
7、安装rollup打包的相关依赖:
yarn add rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa -D -W
8、配置脚本执行命令:根目录 package.json 文件中,新增:
“scripts”:{
“build”:“node scripts/build.js”
}
9、根目录下新建scripts目录,并在其中创建build.js文件,在其中编写打包逻辑代码(详见代码)
10、根目录下新建rollup配置文件,rollup.config.js,在其中编写配置代码(详见代码)
11、将 tsconfig.json中的 target 和 module 字段都改为 ESNext(不然运行打包会报错)
12、配置开发环境打包:根目录package.json中添加dev运行命令,并在scripts目录下新建对应的dev.js文件,在其中编写打包逻辑代码(详见代码)
13、解决包之间引入问题,在tsconfig.json中增加相应配置(详见代码)
14、运行打包 npm run build 或 npm run dev,实现都可以成功打包
实践问题
编写build.js这一步跟着课程里的代码自己实践中,我遇到了问题,记录如下:
猜测报错信息大致是说execa不支持用require方式引入,改成import试试吧,又出现如下报错:
按照提示在package.json中增加"type": “module”,还是报错:
报错信息说execa没提供default暴露的接口,那就用 import {} 方式试试:
这是注意报错,不是execa引入有问题了,而是fs有问题了,因为我们配置了"type": “module”,所以把fs引入方式也改成import方式:
此时的报错信息为找不到rollup配置文件了。
在rollup.config.js配置文件中编写如下代码运行后报错如下:
按照错误提示把rollup.config.js文件后缀分别改为.cjs和.mjs,以及在execa()执行配置项中增加–bundleConfigAsCjs ,都无效;
在execa()执行配置项中增加–bundleConfigAsCjs 时报这个错:
最终解决方案如下:将依赖的引入方式都改为require形式,同时将配置文件后缀改为.cjs:
解决思路来源于rollup官网中的一句话。看来还是要多看官方文档。
rollup.config.js文件中的配置编写完成后,运行报错如下:
将export default 方式改为 module.exports 方式后此报错解决,出现如下报错:
参考 第11 步解决: