关于前端:React-Native工程Monorepo改造实践

31次阅读

共计 5934 个字符,预计需要花费 15 分钟才能阅读完成。

图片来自:https://unsplash.com
本文作者:ddx

背景

目前云音乐内有多个 RN 收银台场景散布在不同的工程,比方页面收银台,浮层收银台,共性收银台等,后续可能还会有别的收银台场景。

那在开发过程中存在的问题就是每个收银台的外围逻辑如商品展现、领取形式展现、下单购买等逻辑都大致相同,而每次有批改或者新增需要的时候都须要开发屡次,反复代码较多效率低下。

尽管能够通过发 npm 包的模式复用代码,然而有些组件和代码块不太好抽成包,还会带来调试麻烦,发版等问题。所以为了进步代码复用,进步开发效率,咱们心愿可能在一个仓库内蕴含多个工程,也就是 Monorepo 模式。

Monorepo

什么是 Monorepo

Monorepo 是一种将多个我的项目的代码集中在同一个仓库中的软件开发策略,与之对抗的是传统的 MultiRepo 策略,即每个我的项目在一个独自的仓库进行治理。

目前像社区内一些驰名的开源我的项目 Babel、React 和 Vue 等都是用这种策略来治理代码。

Monorepo 解决的问题

要想晓得 Monorepo 解决了哪些问题与其劣势,咱们先来看下 MultiRepo 存在的问题。

当咱们在 MultiRepo 下两个工程之前须要复用一些代码时,往往会采纳抽成 npm 包的模式。但当 npm 包有改变时咱们须要做以下事件:

  1. 批改 npm 包代码,通过 npm link 与两个工程调试
  2. 调试实现后公布新版本
  3. 两个工程降级 npm 包新版本,再进行公布

整个流程能够看出还是比拟繁琐的,那如果是在 Monorepo 下咱们能够将公共局部抽成一个 workspace,咱们的两个工程别离也是 workspace 能够间接援用公共 workspace 的代码,工具会帮咱们治理这些依赖关系,开发过程中调试起来也十分不便,而且不波及到发包,版本依赖等,公共局部代码改变实现后两个工程部署即可。

从上述能够看出 Monorepo 次要有 代码复用容易 调试不便 简化依赖治理 等长处,这也是咱们抉择这个计划的起因。

当然 Monorepo 也有一些毛病,比方:仓库体积大、工程权限不好管制等。所以不论是 Monorepo 还是 MultiRepo 都不是完满的计划,只有能解决当下的问题就是好计划。

Monorepo 的工具

目前业界最常见的实现 monorepo 工具和计划有 lerna、yarn workspace 和 pnpm 等。

Lerna

lerna 是一个通过应用 git 和 npm 来优化多包仓库管理工作流的工具,多用于多个 npm 包相互依赖的大型前端工程,提供了许多 CLI 命令帮忙开发者简化从 npm 开发,调试到发版的整个流程。然而目前已官宣进行保护。

Pnpm

pnpm 是一个新型的依赖包管理工具,并反对 workspace 性能,它的劣势次要是通过全局存储和硬链接来来磁盘空间并晋升装置速度,通过软链接来解决幻影依赖问题。然而 RN 的构建工具 metro 对于符号链接的解析还存在问题须要革新,老本较大。

Yarn workspace

yarn workspace 是 yarn 提供的 Menorepo 依赖管理机制,是一个底层的工具,用于在仓库根目录下治理多个 package 的依赖,人造反对 hoist 性能,装置依赖时会将 packages 中雷同的依赖晋升到根目录,缩小反复依赖装置。workspace 之间的援用在依赖装置时通过 yarn link 建设软链,代码批改时能够在依赖其的 workspace 中实时失效,调试不便。

通常业界支流计划是 lerna + yarn worksapce,lerna 负责公布和版本升级,yarn workspace 负责依赖治理。因为咱们的 RN 工程是页面工程,不波及到发 npm 包,而且须要依赖晋升的性能(这个前面会说到),所以最终采纳 yarn worspace 计划。

Metro

在工程革新之前,咱们先理解下 ReactNative 的构建工具 Metro。

Metro 在构建过程中次要会经验三个阶段:

  1. Resolution:此阶段 Metro 会从入口文件登程剖析所依赖的模块生成一个所有模块的依赖图,次要是应用 jest-haste-map 这个包做依赖剖析。这个阶段和 Transformation 阶段是并行的;
  2. Transformation:此阶段次要是将模块代码转换成指标平台可辨认的格局;
  3. Serialization:此阶段次要是将 Transform 后的模块进行序列化,而后组合这些模块生成一个或多个 Bundle

jest-haste-map 是单元测试框架 Jest 的其中一个包,次要用来获取监听的所有文件及其依赖关系。

工程革新

接下来就是对工程的革新,首先咱们将两个 RN 工程放在一个工程下,并依照 yarn workspace 的形式进行配置,而后通过脚手架(这里应用的是公司外部自研的脚手架)别离创立 app- a 和 app- b 两个 RN 工程,如下所示

rn-mono
|-- apps
  |-- app-a
  |-- app-b
|-- package.json
// package.json
{
  ...
  "workspaces": {
    "packages": ["apps/*"]
  },
  "private": true
}

接着咱们运行

yarn install

发现 packages 中雷同的依赖都会装置在根目录下的 node_modules 中,接着咱们用如下启动 app- a 或 app-b

yarn workspace app-a run dev

这时如果你的 app- a 工程中的 dev 启动命令是用相对路径的形式可能会呈现命令找不到的状况,比方

// app-a/package.json
{
  // 这里的 react-native 是装置在了根目录,所以会找不到命令,须要批改下门路
  "script": {"dev": "node ./node_modules/react-native/local-cli/cli.js start"}
}

那如果是调用 ./node_modules/.bin 中的命令则不须要,因为在装置依赖的时候 packages 中 .bin 中的命令会有个软链指向根目录下 ./node_modules/.bin 中的命令。启动胜利后,这时关上页面会报如下谬误:

这是因为 jest-haste-map 在做依赖剖析时通过 metro.config.js 中的 watcherFolders 配置项来指定须要监听变动的文件目录。

watcherFolders 默认值为工程根目录,此时也就是 app- a 中目录,然而咱们的模块都是装置在根目录下,所以会找不到。咱们须要批改下 metro.config.js 中 watcherFolders

// app-a/metro.config.js

const path = require('path');

module.exports = {watchFolders: [path.resolve(__dirname, '../../node_modules')],
};

批改实现后咱们重新启动,再关上页面后发现曾经能够失常关上了,同样的形式 app- b 也能够失常运行。

然而咱们对工程进行 monorepo 革新的目标是为了抽离公共组件,复用代码。所以咱们在根目录下建设个 common 的文件夹来寄存公共局部,此时根目录下的 pacage.json 中的 packages 和 apps 里每个 app 的 metro.config.js 中 watchFolder 配置都须要退出 common

rn-mono
|-- common
    |-- package.json
|-- apps
    |-- app-a
    |-- app-b
|-- package.json
// package.json
{
  ...
  "workspaces": {
    "packages": [
        "apps/*",
        "common"
      ],
  },
  "private": true
}

// apps/app-a/metro.config.js
const path = require('path');

module.exports = {watchFolders: [path.resolve(__dirname, '../../node_modules'), path.resolve(__dirname, '../../common')],
};

接着在 common 中增加个 Button 组件,package.json 中增加相应的依赖,版本要和 apps 中对应依赖的版本保持一致

{
  ...
  "dependencies": {
    "react": "16.8.6",
    "react-native": "0.60.5",
  },
}

而后 yarn install 重新安装下,这时在根目录的 node_modules 下就能够看到 common 模块软链到了 common 目录,所以在 app- a 中引入 common 时就能够像 npm 包一样间接引入,同样 app- b 也能够。

import common from 'common';

到这里咱们 RN 工程的 monorepo 革新也根本实现了。

依赖晋升

这里解释下为什么须要依赖晋升。

咱们先来看下勾销依赖晋升会有什么问题,能够在根目录中的 package.json 中 nohoist 配置来指定不须要晋升装置到根目录的模块


{
  ...
  "workspaces": {
    "packages": [
        "apps/*",
        "common"
      ],
    "nohoist": ["**react**"],
  },
  "private": true
}

而后从新 yarn install,启动 app- a 后会发现报如下谬误

这是因为有些模块 jest-haste-map 在做依赖剖析生成 dependency graph 时发现在两个不同的目录下会产生命名抵触,导致报错。所以咱们须要依赖晋升,将所用到的雷同依赖装置到根目录,这样只会装置一次。

雷同依赖的版本保持一致

尽管有了依赖晋升但如果每个 packages 中雷同依赖的版本不统一,同样会导致雷同的依赖会装置屡次的状况呈现,根目录和对应的 package 中都会有。这种状况除了会产生以上问题外还有可能产生其余潜在的问题,比方依赖客户端的第三方模块,如果存在多个版本在 bundle 执行时会屡次注册组件导致组件注册失败,在调用时会产生找不到组件的报错。

尽管能够在 metro 中配置 blacklistRE 和 extraNodeModules 来表明要读取哪个地位的依赖,然而这种形式并不通用,每次在引入新的依赖时都要去配置下较为繁琐。所以咱们须要将每个 packages 中的依赖版本保持一致。

人为的去约定这个规定必定是不平安的,能够开发一个依赖版本的 lint 检测工具,在提交代码的时候做强制性的检测。

咱们最终的计划是开发一个检测脚本联合 gitlab-ci 在分支代码 push 的时候检测,未通过则不容许 push 代码来防止危险。

// .gitlab-ci.yml
test-dev-version:
  stage: test
  before_script:
    - npm install --registry http://rnpm.hz.netease.com
  script:
    - npm run depVerLint
  only:
    changes:
      - "package.json"
      - "packages/**/package.json"

工程迁徙过渡

如果是将多个正在疾速迭代的工程迁徙到一个 Monorepo 仓库时,必定会遇到存量开发分支代码同步问题。比方咱们要将工程 A 迁徙到新仓库,如果咱们只是基于 master 分支将代码 copy 到新工程,并在革新开发过程中还有组内其他同学也在基于 master 拉取分支做开发,并在你革新实现前开发实现合并到了 master,此时你新工程的代码是落后的,要想同步只能手动 copy 改变的代码,很容易出错。为了解决这个问题咱们能够应用 git subtree。
git subtree 容许将一个仓库作为子仓库嵌套在另一个仓库里,所以这里咱们能够将工程 A 作为一个子工程增加到 Monorepo 新工程对应的 packages 目录下,如果有更新能够间接应用 pull 进行同步。

# 增加
git subtree add --prefix=apps/app-a https://github.com/xxxx/app-a.git master --squash

# 更新
git subtree pull --prefix=apps/app-a https://github.com/xxxx/app-a.git master --squash

对于新工程或者新的开发分支就能够间接此工程下进行开发了。

构建

因为咱们的构建机还不反对 yarn,所以间接应用 yarn workspace 的命令是有问题的。目前的做法是将 yarn 作为 devDependency,而后在根目录下创立个脚本文件,将每个 package 的构建命令收敛在一起。联合 yarn workspace 的命令,这样只须要在构建时传入不同的 package name 即可。

## scripts/build.sh

PLATFORM=$1
PROJECT=$2
EXEC_PARAMS=${@:2}
YARN="${PWD}/node_modules/.bin/yarn"

...

echo "start yarn install"
${YARN} cache clean
${YARN} install

echo "start build"
echo "${YARN} workspace ${PROJECT} run build:${PLATFORM} ${EXEC_PARAMS}"
${YARN} workspace ${PROJECT} run build:${PLATFORM} ${EXEC_PARAMS}
// package.json
{
  ...
  "workspaces": {
    "packages": ["apps/*"],
  },
  "private": true,
  "scripts": {"build": "./script/build.sh"},
}

比方对 app- a 进行构建,就能够


npm run build ios app-a

## 实际上执行的是 yarn workspace app-a run build:ios

总结

至此对 React Native 工程的 menorepo 革新根本实现了,对于多个性能相似的工程采纳 Monorepo 的治理形式的确会不便代码复用和调试,进步咱们的开发效率。如果公司外部其余场景有相似的需要,将来布局能够将其积淀出一个脚手架。

目前对于 h5 工程的 Monorepo 计划曾经较为成熟了,然而对 RN 工程来说因为构建机制不同无奈齐全实用,可参考的材料也较少。本文也是通过实际记录了一些踩坑教训,如果你有更好的实际,欢送留言一起探讨。

本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

正文完
 0