IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> Monorepo丝滑方法论:引用模块热更新 -> 正文阅读

[JavaScript知识库]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 体系中探讨过:

该文中的解法有两个,一是通过 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 、默认最佳现代构建策略等,由于这些不在本文主题范围,留给读者自行探索研究。

以上。

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-06-25 18:01:20  更:2022-06-25 18:04:01 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 16:45:37-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码