monorepo环境搭建
- 首先创建一个文件夹并使用
pnpm init
初始化指定项目是es6"type": "module",
。然后创建.npmrc
文件写入shamefully-hoist = true
项目的所有依赖项提升到 node_modules 目录的顶层,作用是在其他地方可以直接引入nodemoudles中的包使用
- 然后创建monorepo所需的包目录packages和
pnpm-workspace.yaml
文件指定包入口。公共的包放nodemoduels下,不同的包则放在各自packages中,开启如下命令后,则限制安装包,pnpm install vue -w
代表安装到nodemoules中
pnpm-workspace.yaml
packages:- "/packages/*"
pnpm install typescript esbuild minimist -D -w
安装开发环境需要的公共依赖,minimist是用来解析命令行。npx tsc --init
初始化ts配置
{"compilerOptions": {"outDir": "dist", //输出目录"sourceMap": true, //生成sourceMap"target": "ES2016","module": "ESNext","moduleResolution": "node", //模块解析方式"strict": false,"resolveJsonModule": true, //解析json文件"esModuleInterop": true, //允许使用es6引入commonjs模块"jsx": "preserve", //保留jsx语法不转义"lib": ["ESNext","DOM"] //编译时包含的库文件}
}
- 创建如下文件,约束所有的packages的包格式入口都在src下的index.ts中,dev.js为开发命令,在package.json中添加运行命令
指明开发环境打包命令文件,读取的模块reactivity ,-f 随意命令的变量接受后面的打包规范esm,也可以是iife立即执行函数的规范(function(){})()"scripts": {"dev":"node ./scripts/dev.js reactivity -f esm"},
执行pnpm run dev
命令执行dev.js文件
dev.js
import minimist from "minimist";
// slice(2)过滤前面的固定格式
// node ./scripts/dev.js ((reactivity,模块2 -f esm)-f esm) 括号内的=process.argv.slice(2) 结果minimist处理后为{ _: [ 'reactivity' ], f: 'esm' }const args = minimist(process.argv.slice(2));
console.log(args);
- 给package目录中的每一个模块创建固定的打包入口即src/index.ts。
获取当前文件dev所在位置import.meta.url
获取file:///....
开头的数据。不方便理解所以是使用fileURLToPath
解析为常规路径
import {fileURLToPath} from 'url'
console.log(import.meta.url,fileURLToPath(import.meta.url));
然后获取文件夹位置。在es6中没有require对象和__dirname
import {dirname} from 'path'
// 获取当前文件所在目录的路径
const _dirname = dirname(_filename);
console.log(_dirname); //E:\个人项目\vue3.4-source-code\scripts
- 获取入口,并且在每一个模块中创建package.json文件指明依赖关系
const entry = resolve(_dirname,`../packages/${targe}/src/index.ts`)
{"name": "@vueq/reactivity", // 定义模块名,方便引入"version": "1.0.0","module": "dist/reactivity.esm-bundler.js", 指明了es入口"unpkg": "dist/reactivity.global.js", //cdn全局引入"buildOptions": { 构建的相关选项"name": "VueReactivity", 全局变量名formats:定义了构建输出的格式,当前支持以下几种:esm-bundler: 用于打包工具的 ES 模块格式。esm-browser: 用于浏览器的 ES 模块格式。cjs: CommonJS 格式,通常用于 Node.js 环境。global: 全局变量格式,适合直接在浏览器中使用"formats": ["esm-bundler","esm-browser","cjs","global"]}
}
{"name": "@vueq/shared","version": "1.0.0","module": "dist/shared.esm-bundler.js","buildOptions": {"formats": ["esm-bundler","cjs"]}
}
测试在shared文件的index中创建一个方法测试isObject。然后尝试在reactivity中引入,但是需要以模块化的方式引入而非路径方式
设置ts类型引入提示
"baseUrl": ".","paths": {"@vueq/*": ["packages/*/src"]}
此时可以识别本地shared包中的导出方法
然后还需要将本地shared包安装到reactivity中pnpm add @vue/shared --workspace --filter @vue/reactivity
添加--workspace
代表是本地包,--filter @vue/reactivity
是安装给谁。这里将@vue/reactivity
修改为@vueq/...
,是因为安装第三方的vue的时候,本地和第三方包同名了,安装了的时候将本地包处理到了第三方目录中,导致了同名部分问题本地取代了第三方的同名包,暂时不知道哪里出错,所以更改名字
搭建esbuild开发环境
在dev.js文件中添加
const pkg = require(`../packages/${target}/package.json`)esbuild.context({entryPoints:[entry], // 打包入口outfile:resolve(_dirname,`../packages/${target}/dist/${target}.js`), // 打包出口bundle:true, // 使用到的资源打包到一起 ,rectivity中用到shared,会打包到一起platform:'browser', // 打包给浏览器sourcemap: true ,// 可以调式源码format, // 打包的模块规范 cjs ems iife globalName:pkg.buildOptions?.name // 若是iife立即执行函数,需要设置全局变量接收
}).then(res=>{console.log('打包开始')return res.watch() // 监控持续打包
})
当前esm形式打包。输出如下
iife的打包如下
reactive函数
下面是原生的使用情况,reactive基于proxy实现响应式。effect
是副作用函数用于监听响应数据改变更新视图。watch和computed都是基于effect完成,默认初始化会执行一次,响应式数据更新后会再次执行,所以如下代码可以发现视图更新
<div id="app"></div><script type="module">import {reactive,effect} from '/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js'const state = reactive({name:"张三",age:20})console.log(state);effect(()=>{app.innerHTML = `姓名${state.name} 年龄${state.age}`})setTimeout(()=>{state.age ++ },1000)</script>
现在修改引入指向本地的函数。
在reactivity的src文件中创建reactive和effect函数并在index中导出。
reactive.ts中编写如下代码
import { isObject } from "@vueq/shared";const handler:ProxyHandler<any> = {get(target,key,receiver){ // receiver只代理对象proxy变量},set(target,key,value,receiver){return true}
}export function reactive (target){return createReactiveObject(target)
}function createReactiveObject(target){if(!isObject(target)){return target}const proxy = new Proxy(target,handler)return proxy
}
原生reactive使用细节,如果是同一个对象进行多次响应式处理,则会缓存,使用相同的内存地址, 若已经代理过的对象再次被代理,则不会被处理
定义一个缓存,若访问的是同一个对象地址则直接返回代理过的对象
const reactiveMap = new WeakMap()function createReactiveObject(target){if(!isObject(target)){return target}const existProxy = reactiveMap.get(target)if(existProxy){return existProxy}const proxy = new Proxy(target,handler)reactiveMap.set(target,proxy)return proxy
}
被代理的对象中一定会设置get和set,那么可以在get中设置一个唯一值,代表当前对象是已经被reactive代理过的,若再次被代理可以直接返回。
enum ReactiveFlags {IS_REACTIVE = '__v_isReactive'
}function createReactiveObject(target){
。。。// 手动触发get,只有被代理的对象身上才有该属性if(target[ReactiveFlags.IS_REACTIVE]){ //第一次obj还没有被代理所以不会进入get和setreturn target}
。。。。
}
完善mutableReactiveHandler,使用Proxy代理会返回一个监听对象,当前执行监听对象proxy访问的时候才会触发get和set
Proxy代理对象中的get和set中的target代理对象即Proxy中的第一个参数obj。return target[key]
这个方式的话相当于obj.age。obj并不是代理对象,所以写法不正确。如果写成代理对象,则代理对象会在get中递归访问报错,所以需要使用Reflect处理。此时receiver会从target中读取key值,从而避免拦截操作,不会递归
Reflect.get(target, key, receiver):修改target在读取过程中的this指向receiver
target 是原始的目标对象。
key 是要访问的属性名。
receiver 是调用者对象(通常是代理对象 proxy)。
export const mutableReactiveHandler:ProxyHandler<any> = {get(target,key,receiver){if(key === ReactiveFlags.IS_REACTIVE){return true}// 取值的时候和effect映射起来return Reflect.get(target,key,receiver)},set(target,key,value,receiver){// 找到属性,让对应的effect重新执行return Reflect.set(target,key,value,receiver)}
}
effect函数
const obj = {name:"张三",age:20}const state = reactive(obj)effect(()=>{app.innerHTML = `姓名${state.name} 年龄${state.age}`})setTimeout(()