手写 Vue 源码 === 搭建 Monorepo 环境

目录

1.全局安装 pnpm

2.创建.npmrc 文件

.npmrc

设置 shamefully-hoist=true:

3.配置 workspace

4.环境搭建

5.初始化 TS

6.创建模块

7.开发环境esbuild打包


Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)。 Vue3 源码采用 monorepo 方式进行管理,将模块拆分到 package 目录中。作为一个个包来管理,这样职责划分更加明确。

一个仓库可维护多个模块,不用到处找仓库
方便版本管理和依赖管理,模块之间的引用,调用都非常方便

Vue3 中使用pnpm workspace来实现monorepo (pnpmopen in new window是快速、节省磁盘空间的包管理器。主要采用符号链接的方式管理模块)

1.全局安装 pnpm

npm install pnpm -g # 全局安装pnpm
pnpm init  # 初始化配置文件

2.创建.npmrc 文件

.npmrc

.npmrcnpm 的配置文件,用于定义和管理 npm 的各种行为,比如:

设置私有 registry(注册源)

保存 token、认证信息

配置包的安装路径、缓存路径等

各种行为开关,如是否生成 package-lock.json

.npmrc 文件可以存在于不同级别:

位置 作用范围
项目根目录下的 .npmrc 仅影响当前项目
用户主目录(如 ~/.npmrc 影响当前用户的所有项目
全局(如 /etc/npmrc 影响所有用户

shamefully-hoist=true 是什么?

这是 pnpm(而非 npm) 的一个配置项,用于控制依赖的 hoisting(提升)方式。

默认行为(pnpm 默认):

pnpm 会把每个依赖安装在自己的隔离目录中,避免依赖污染。

这有助于防止 “依赖地狱”,但某些依赖可能找不到自己需要的库(尤其是老旧的依赖)。

设置 shamefully-hoist=true

pnpm 会将所有依赖都提升到项目的 node_modules 根目录下。

这种方式 更像 npm 和 Yarn 的行为,兼容性好,但失去 pnpm 原本的隔离优势。。

shamefully-hoist = true

这里您可以尝试一下安装Vue3pnpm install vue此时默认情况下vue3中依赖的模块不会被提升到node_modules下。 添加羞耻的提升可以将 Vue3,所依赖的模块提升到node_modules

3.配置 workspace

新建 pnpm-workspace.yaml

pnpm-workspace.yamlpnpm 用于管理 Monorepo 项目的配置文件

它告诉 pnpm 哪些子项目(package)属于这个 Monorepo,使它可以统一安装依赖、共享缓存、加速构建。

packages:
    - "packages/*"

将 packages 下所有的目录都作为包进行管理。这样我们的 Monorepo 就搭建好了。确实比lerna + yarn workspace更快捷

packages/*  是子项目的路径匹配规则(glob 模式)。

这些路径下的项目会被 pnpm 识别为 Monorepo 的一部分,参与联动管理。

 

4.环境搭建

打包项目 Vue3 采用 rollup 进行打包代码,安装打包所需要的依赖

开发依赖
typescript 在项目中支持 Typescript
esbuild 构建工具,默认支持 TS
minimist 命令行参数解析

在 pnpm 中,-w 是 --workspace-root 的简写,表示将依赖安装到工作区根目录(适用于 monorepo 项目)。 

适用场景:当你的项目使用 pnpm 工作区(monorepo 结构,包含多个子包)时,-w 会将依赖安装在根目录的 node_modules 中,而不是某个子包内。

用途:通常用于安装全局开发工具(如 TypeScript、esbuild),让所有子包共享这些依赖,避免重复安装。

对比

不加 -w:依赖会安装到当前工作目录的子包中。

加 -w:依赖安装到 monorepo 根目录,所有子包均可访问。

pnpm install typescript minimist esbuild -D -w

5.初始化 TS

复杂的框架项目开发,使用类型语言非常有利于代码的维护,在编码期间就可以帮我们做类型检查,避免错误。所以 TS 已经是主流框架的标配~

Vue2 早期采用 Flow 来进行类型检测 (Vue2 中对 TS 支持并不友好), Vue3 源码采用 Typescript 来进行重写。同时 Vue2.7 也采用 TS 进行重写。TS 能对代码提供良好的类型检查,同时也支持复杂的类型推导。

pnpm tsc --init

先添加些常用的ts-config配置,后续需要其他的在继续增加

 

{
	"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"] // 支持的类库 esnext及dom
	}
}

6.创建模块

我们现在packages目录下新建两个 package,用于下一章手写响应式原理做准备

reactivity 响应式模块
shared 共享模块

所有包的入口均为src/index.ts 这样可以实现统一打包

reactivity/package.json

{
	"name": "@vue/reactivity",
	"version": "1.0.0",
	"main": "index.js",
	"module": "dist/reactivity.esm-bundler.js",
	"unpkg": "dist/reactivity.global.js",
	"buildOptions": {
		"name": "VueReactivity",
		"formats": ["esm-bundler", "esm-browser","cjs", "global"]
	}
}

shared/package.json

{
	"name": "@vue/shared",
	"version": "1.0.0",
	"main": "index.js",
	"module": "dist/shared.esm-bundler.js",
	"buildOptions": {
		"formats": ["esm-bundler", "cjs"]
	}
}

  name:  包名称,采用 Vue 官方的组织包命名方式,表示这是 Vue 的响应式系统核心模块

version: 包版本号,遵循语义化版本控制规范

main: 主入口文件,当通过 CommonJS (require) 方式引入时会加载这个文件

module:  ES Module 入口文件,面向现代打包工具(如 webpack, Rollup)  特点:保留 __DEV__ 等编译时标志,方便进行生产环境优化

unpkg:  专为浏览器直接使用设计的 UMD 格式文件 特点:自动挂载到全局变量,可通过 <script> 标签直接引入

buildOptions: 构建配置 

name:UMD 构建的全局变量名称,浏览器中可通过 window.VueReactivity 访问
formats: 指定要生成的包格式

formats 为自定义的打包格式

global 立即执行函数的格式,会暴露全局对象「 浏览器全局变量格式(UMD) 」
esm-browser 在浏览器中使用的格式,内联所有的依赖项。
esm-bundler 在构建工具中使用的格式,不提供 .prod 格式,在构建应用程序时会被构建工具一起进行打包压缩。 「给打包工具使用的 ES Module(保留开发环境判断)」
cjs 在 node 中使用的格式,服务端渲染。 CommonJS 格式


pnpm install @vue/shared --workspace --filter @vue/reactivity

🔹 pnpm install

这是使用 pnpm(一个高效的包管理工具)安装依赖的命令,类似于 npm installyarn add

🔹 @vue/shared

这是你要安装的包。@vue/shared 是 Vue 源码中的一个共享工具包,包含一些公共函数,供 Vue 的各个子模块使用。

🔹 --workspace

这个标志告诉 pnpm你正在 Monorepo(多包仓库)环境中操作,你希望把依赖加到某个工作空间(子包)里,而不是根目录。

🔹 --filter @vue/reactivity

这部分指定:我只对 @vue/reactivity 这个子包进行操作。换句话说,是在 @vue/reactivity 这个包里安装 @vue/shared

@vue/reactivity 这个包里安装 @vue/shared 作为依赖。 并且使用本地目录


配置ts引用关系

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

7.开发环境esbuild打包

 

 创建开发时执行脚本, 参数为要打包的模块

解析用户参数

"scripts": {
    "dev": "node scripts/dev.js reactivity -f esm"
}
// 这个文件会帮我们打包 packages 下的模块,最终打包成 js

/**
 * node dev.js 要打包的模块名 -f 要打包的格式  === process.argv.slice(2)
 * 获取命令行参数 通过 process.argv获取 { 参数1: 用哪个命令执行的 node, 参数2: 执行的文件名 }
 * node 中不能直接使用 esm 的模块,所以需要使用 cjs 的模块  「package.json  type设置为 module」
 *
 * node 中的 esm 模块没有 __dirname 和 require 所以需要使用 url 模块来获取当前文件的绝对路径
 * require 函数 是 node 中的一个函数,可以用来加载模块
 * __dirname 是 node 中的一个全局变量,可以用来获取当前文件的目录名
 * __filename 是 node 中的一个全局变量,可以用来获取当前文件的绝对路径
 * resolve 拼接路径 
 * 
 *
 * esbuild.context 是 esbuild 的实例,可以用来打包模块
    entryPoints: [entry], // 入口文件
    outfile: resolve(__dirname, `../packages/${target}/dist/${target}.js`), // 输出文件
    bundle: true, // 打包 「将所有文件打包成一个文件 reactivity 依赖 shared 会打包成一个文件」
    platform: "browser", // 平台 「打包后给浏览器使用」
    sourcemap: true, // 源码映射
    format: format, // 格式 「cjs:require,  esm:import,  iife:script」
    globalName: pkg.buildOptions.name, // 全局变量名 「供全局使用,因为iife 是一个闭包」
 */

// 获取命令行参数
import minimist from "minimist";
// 获取当前文件的绝对路径
import { fileURLToPath } from "url";
// dirname 获取当前文件的目录名 resolve 获取绝对路径
import { dirname, resolve } from "path";
// 创建 require 函数
import { createRequire } from "module";
// 打包工具
import esbuild from "esbuild";

const __filename = fileURLToPath(import.meta.url); // 获取当前文件的绝对路径 file=>/user
const __dirname = dirname(__filename); // dirname 获取当前文件的目录名
const require = createRequire(import.meta.url); // 创建 require 函数「根据当前文件的绝对路径创建」

const args = minimist(process.argv.slice(2));
const target = args._[0] || "reactivity"; // 要打包的模块名
const format = args.f || "iife"; // 要打包的格式

// 入口文件 根据 target 获取入口文件 「resolve 拼接路径」
const entry = resolve(__dirname, `../packages/${target}/src/index.js`);
// 获取 package.json 文件
const pkg = require(`../packages/${target}/package.json`);

esbuild
  .context({
    entryPoints: [entry], // 入口文件
    outfile: resolve(__dirname, `../packages/${target}/dist/${target}.js`), // 输出文件
    bundle: true, // 打包 「将所有文件打包成一个文件 reactivity 依赖 shared 会打包成一个文件」
    platform: "browser", // 平台 「打包后给浏览器使用」
    sourcemap: true, // 源码映射
    format: format, // 格式 「cjs:require,  esm:import,  iife:script」
    globalName: pkg.buildOptions.name, // 全局变量名 「供全局使用,因为iife 是一个闭包」
  })
  .then((ctx) => {
    console.log("打包成功");
    return ctx.watch(); // 监听文件变化
  })
  .catch((err) => {
    console.log(err);
  });

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容