Monorepo丝滑方法论:引用模块热更新

编程入门 行业动态 更新时间:2024-10-10 19:20:50

Monorepo丝滑方法论:引用<a href=https://www.elefans.com/category/jswz/34/1771428.html style=模块热更新"/>

Monorepo丝滑方法论:引用模块热更新

前言

虽然 monorepo 一题之前聊过很多,对技术选择边界也反复洞察,但随着时间的流逝和实践的领域、量级拓宽,自省:越发重要的心智点是哪个?

毫无疑问是: 引用模块热更新

什么是 “引用模块热更新” :就是你在 monorepo 中开发项目时,跨子包引用代码,改动其他子包的源代码文件,要能在当前项目中自动检测到,支持热更新,从而你就不需要去重新构建那个子包的产物,然后手动刷新页面了。

下面我们从几个角度探讨一种 trade off 的丝滑解法,在阅读前,我们默认读者已经具备了 熟练的 webpack 基础 或是 框架开发者

解法

Tranpile Includes

Why not watch ?

先做一个质问,为什么不能 watch 模式启动那个子包,然后他自己就可以随改动,自动重构建产物了?

  1. 一是构建时间有延迟。假如是 tsc 还好,假如是 rollup rebuild 单文件花费的时间更是让人难以忍耐;其次对于大型子包 sdk ,最快的 tsc 都要近 10s 时,更不用说 rollup 了。

    当然,也可以说用 esbuild 就解决了构建时间的问题了吧,但是 esbuild bundle 是单文件的,单文件变动意味着整体 reload ,而不是局部 hmr 了,同样对主项目会造成整体刷新的破坏,试想你打开了一个 Modal 弹窗组件,下次要重新打开(这里不聊 unjs 思路的单文件 transpile ,不是本文重点)。

  2. 二是检测不到产物变化。由于子包链接在 node_modules 里,一般框架侧都会 exclude 排除掉,不会监听,所以即使是产物变动了也不会热更新页面。

所以解法是什么,有什么痛点我们就聚焦解什么,很显然,既然他不识别,我们就手动配 webpack rule 规则让他在我们的范围内。

规则分离

因为我们的子包代码都是 ts 的,所以必然要让 ts 资源全部进入我们的 transpile 范围,故需要让 .js.ts 资源匹配规则分离,所有 .ts 规则不设 exclude.js 规则继续保持 exclude 排除掉 node_modules ,示例如下:

// js 资源
{test: /\.js$/,exclude: /node_modules/use: [// transpiler ...]
}// ts 资源
{test: /\.(ts|tsx|jsx)$/,use: [// transpiler ...]
}

如此一来,第三方包的 .ts 资源就会进入 transpile 范围,当然,还有更细致的做法,比如对 exclude 做函数判断,在某个 @scope/* 内就不要 exclude 。

重定位

如上我们解了无法 transpile 其他子包 ts 资源的问题,现在我们重定位他们,直接读取这些子包的源码,这样就不需要构建他们了,让他们真正成为我们项目的一部分。

做法是:首先需要找到这些子包的 src 源码位置,然后通过 alias 重定位包的引用到他的源码处。如何寻找?这里推荐使用最流行的 monorepo 发包工具 changesets 的底层 @manypkg/get-packages ,示例如下:

import { getPackages } from '@manypkg/get-packages'
import { join } from 'path'const collectAllProjectsEntryAlias = async () => {const workspaces = await getPackages(// ↓ 项目根目录,一般是 process.cwd()projectRootPath);return workspaces.packages.reduce((obj, pkg) => {const name = pkg.packageJson?.name;if (name) {obj[name] = join(pkg.dir, 'src');}return obj;},{},);
}

这里只是一个简易逻辑,根据场景的不同,你可能还需要规范化你子包 package.json 中的字段,以及判断文件夹存在性,还有预期在使用 npm 包的版本还是本地版本等。

之后你只需要将他们 merge 进 alias 即可。

回顾一下,在以前,我们使用的是 "main": "dist/index.js" 导出的内容,而现在我们通过 alias

{alias: {'some-pkg': '/path/to/some-pkg/src'}
}

使用的是他的源码 some-pkg/src/index.ts 导出的内容,搭配对 ts 的 transpile,即可做到无缝热刷新!

TypeScript Type Support

无疑,直接定位到 src 是无法识别到类型变动的,我们直接使用 tsconfig.json#compilerOptions.paths 来做识别就可以了,示例如下:

{"compilerOptions": {"paths": {"some-pkg/*": ["../packages/some-pkg/*"],}}
}

如果要做自动化,可以考虑让项目 tsconfig.json 继承框架生成的 .temp/tsconfig.base.json 来动态修改。

到此为止,我们的热更新问题完全解掉 😎 ,但真的有这么简单吗?

Ensure Single Instance

多实例的底层逻辑

在 monorepo 中,我们很容易忽视一个重要事实:

  • 在读子包时,他的依赖是从他的目录作为起始点想上寻找的。

这意味着假如你正在使用 react 开发可复用的组件,此时按照规范,你应该让使用你组件包的人的 react 唯一,也就是不能将 react 写入 dependencies ,而是写入 devDependencies ,因为你本地开发要使用,同时为了提示安装你包的人,还要将其写入 peerDependencies

{"devDependencies": {"react": "^18.0.0"},"peerDependencies": {"react": "^18.0.0"}
}

但是在 monorepo 中可不同于 npm 包的使用只会安装 dependencies ,子包的 devDependencies 同样在我们本地!这就会导致该包的组件使用的 react 是从他目录向上寻找到的 react ,和主应用中的 react 不一样!造成多 hooks 应用崩溃问题。

对于 react 重复来说,这是致命错误,会导致应用崩溃。另一个不致命的经典 case 是:两份 antd ,会导致 Message 弹出提示不成队列。

在这种 “多实例” 影响下,先不论致命错误我们无法承受,各种不好的体验问题,甚者很多依赖都是子包带了一份,主应用也有一份,最后产物体积成倍增加。

困境解法

关于这一问题,也被我自称为 peerDependencies 困境,我们之前在 pnpm monorepo 体系中探讨过:

  • 《 pnpm monorepo 之多组件实例和peerDependencies困境回溯 》

该文中的解法有两个,一是通过 alias 别名统一定位到主应用保持实例唯一;二是将他们都提升到全局,从而两者都向上会找到同一个实例。无论是哪种解法都不具备自动的拓展性,有人工劳作成本。

有没有一种自动化的丝滑解决方案?

可以有,只要一切都在约定规范内。这里的 trade off 是,我们通过 自动读 同仓库的其他子包 package.jsonpeerDependencies 字段,然后将他们全 尝试定位 到我们的主应用即可。

这里有两个关键词:

  • 自动读:原理同如上解热更新的方法,使用 workspace 工具读出来这些子包 package.json 中的 peerDependencies 有哪些就可以了。

  • 尝试定位:一些依赖主应用必定有,比如 react ,而一些不重要的、主应用用不到的依赖,我们的主应用根本不需要装,所以是 尝试定位 ,定位不到就用子包的,定位到了就用主应用的保持唯一。此处的方法应该是 requre.resolve()resolve() ,简单示例如下:

    import resolve from 'resolve'
    import { dirname } from 'path'const alias = {}try {const depPkgDir = dirname(resolve.sync(`${depName}/package.json`, { basedir: projectRootPath }))alias[depName] = depPkgDir
    } catch {}
    

需要定位防止多实例的依赖一定在 peerDependencies 有吗?正确的,一定有,只要开发规范。

如此一来,我们就可以解掉多实例的问题。回过头来,只要开发者按照正式的 FE 界开发规范,能把依赖写在 peerDependencies 就可以做到自动加载,在业界规范下说,这不是一种约定了,而是一种基础能力。

另外,对于重要的依赖,会导致应用 crash 崩溃的核心依赖我们应该手动锁死位置,如 react 核心的几个依赖:

  • react

  • react-router

  • react-router-dom

当然,对于内部开发的场景,为了彻底避免开发者不规范开发子包,不写 peerDependencies ,可以维护一份重点依赖名单,他们一般是 ui 库或是每个项目都可能有的依赖,比如 antd / arco-design 等,然后给这些重点依赖在框架侧就永久重定位。

注:不要过大预期定位的优点,不是核心依赖尽量不要定位,定位应该保持最小范围,大部分依赖他们往往并不会导致体验问题或致命错误,但是他们的版本在子包和主应用不一样,一旦出现大版本 Breaking change 则得不偿失。

总结

我们通过解决热更新和重要的多实例问题,让 monorepo 的仓内项目开发更加丝滑。

进一步下钻,我们根本不需要让开发者理解这一切,这些逻辑应该在框架侧黑盒自带,当识别到该项目在 monorepo 中自动开启,因为这一切对于开发者来说都很 “理所当然“ 。

往广度谈,monorepo 的丝滑方法还有哪些,比如使用 pnpmchangesets, 框架侧的丝滑做法还有哪些,比如自动 polyfill 、默认最佳现代构建策略等,由于这些不在本文主题范围,留给读者自行探索研究。

以上。

更多推荐

Monorepo丝滑方法论:引用模块热更新

本文发布于:2024-03-23 20:10:47,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1742319.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:模块   Monorepo   滑方

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!