最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Monorepo 在 React Native 项目的实践

    正文概述 掘金(todoit)   2021-04-06   579

    本文基于 React Native 项目讲解 Monorepo,不过泛前端项目也可以参考。

    什么是 Monorepo 呢?谷歌一下,找到下面一张图

    Monorepo 在 React Native 项目的实践

    看图释义:就是你有一个巨无霸项目(Monolith),里面模块甚多,依赖错综复杂,给维护带来了困难。

    为了维护方便,你想要模块化,把不同功能划分到不同子项目中去。此时,你可以让每个子项目对应一个独立的 Git 仓库,让这些模块之间从物理上进行隔离,不能随意相互引用。这就是 Multirepo。

    不过这些子项目本就是一体,它们组合在一起才能构成一个完整的项目,将它们分割到不同仓库,给开发、构建带来了不便。那么有没有更好的组织方式呢?那就是 Monorepo 了。不同功能依然划分到不同子项目,但这些项目都在同一个 Git 仓库中。

    Monorepo 是一种代码组织方式,在微服务、iOS、Android 开发中也是经常使用。譬如 iOS 可以借助 Cocoapods 实现 Monorepo,而 Android 的开发工具 Android Studio 天然就支持 Monorepo。

    笔者曾经写过一篇文章依赖注入实现组件化,来介绍 Android 项目如何做组件化,使用的就是 Android Studio 天然支持的 Monorepo 来组织代码。

    Monorepo 在 React Native 项目的实践

    如上图所示,app 是个子项目,它负责组装其它项目,也是整个工程的入口。business-a-ui、business-b-ui、business-c、common-api、common-ui 也都是子项目。

    这些子项目(模块)从物理上都分属不同的文件目录,那么如何禁止它们随意导入其它模块呢?答案是:依赖声明。如果一个子项目不声明依赖另外一个子项目,那么就不能导入。

    我们如何在 React Native 项目或者前端项目中实现 Monorepo 呢?

    有哪些工具可以帮我们将不同业务线或功能划分到不同子项目(目录)中去?

    怎样并禁止子项目(目录)之间随意导入呢?

    Yarn Workspace

    Yarn 是 Node 的包管理器,相对 Npm 的优点之一便是 yarn workspace。

    使用 Yarn Workspace 可以帮助我们实现前端项目的模块化、组件化。

    如何使用 Yarn Workspace,看官方文档足矣。

    使用以下命令,创建一个 React Native 项目

    npx react-native-create-app MonoDemo
    

    可以看到,生成的项目结构大致如下

    Monorepo 在 React Native 项目的实践

    我们参考 Android 原生项目组织代码的方式来组织我们的 React Native 项目代码。

    1. 修改 package.json 文件,添加如下配置

      "workspaces": [
          "app",
          "packages/*"
      ],
      

      这表明,app 目录本身,以及 packages 下的每一个子目录都是一个子项目(模块)。

    2. 创建 app 子项目(目录),并把 inde.js、App.tsx 移动到 app 目录下的 src 目录中。如图所示:

      Monorepo 在 React Native 项目的实践

      在 app 目录下创建 package.json 文件,内容为:

       {
         "name": "app",
         "version": "1.0.0",
         "dependencies": {
      
         }
       }
      
    3. 修改 android/app/build.gradle 文件,把 entryFile: "index.js" 替换为 entryFile: "app/src/index.js"

      project.ext.react = [
          entryFile: "app/src/index.js",
          enableHermes: false,  // clean and rebuild if changing
      ]
      

      修改 MainApplication.java 文件,把 getJSMainModuleName 的返回值由 index 修改为 app/src/index

      @Override
      protected String getJSMainModuleName() {
          return "app/src/index";
      }
      
    4. 修改 AppDelegate.m 文件,将 jsBundleURLForBundleRoot 的值由 index 修改为 app/src/index

      NSURL *jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"app/src/index" fallbackResource:nil];
      

      使用 Xcode 打开 ios 项目,修改 Build Phases 中名为 Bundle React Native code and images 的脚本,在 react-native-xcode.sh 后面添加参数 app/src/index.js。注意 app 前面有个空格。

      如图:

      Monorepo 在 React Native 项目的实践

    现在,我们有了一个叫 app 的子项目,并且整个工程可以正常运行了。这也是我们拆分一个 Monolith 项目为 Monorepo 项目的第一步。

    一个完整的项目是由多个子项目组合而成,app 是整个工程的入口,负责组装其它子项目,app 可以依赖其它子项目,但其它子项目不能依赖 app

    我们把其它的子项目都放到 packages 目录下。

    模块间依赖

    在 packages 目录下,创建三个子项目,为别为 common, module-a, module-b, 如图所示

    Monorepo 在 React Native 项目的实践

    我们给所有子模块都添加了 @sdcx 作为 scope,一方面避免和其它第三方组件库有冲突,另一方面方便导入。

    common

    common 模块中的文件和内容如下

    // common/package.json
    {
      "name": "@sdcx/common",
      "version": "1.0.0",
      "main": "src/index",
      "dependencies": {}
    }
    

    由于 main 字段默认值是 index, 而我们的代码文件都放到了 src 中,因此需要手动指定 main 的值为 src/index。

    // common/src/index.ts
    export function log(...args: string[]) {
      console.log(...args)
    }
    
    export const DEFAULT_NAME = 'Listen'
    

    module-a

    module-a 模块中的文件和内容如下

    // module-a/package.json
    {
      "name": "@sdcx/module-a",
      "version": "1.0.0",
      "main": "src/index",
      "dependencies": {
        "@sdcx/common": "1.0.0"
      }
    }
    

    模块 module-a 在它的 package.json 文件中声明了对 common 模块的依赖,由于 common 模块在它的 package.json 文件中配置 name 为 @sdcx/common,因此其它模块在声明对 common 模块的依赖时,也要使用这个名字。

    // module-a/src/index.ts
    import { log } from '@sdcx/common'
    
    export function setupGlobalStyle() {
      log('现在开始设置全局样式')
      // 配置全局样式
      Garden.setStyle({
        topBarStyle: 'dark-content',
        statusBarColorAndroid: Platform.Version > 21 ? undefined : '#4A4A4A',
      })
    }
    

    module-b

    module-b 模块中的文件和内容如下

    // module-b/package.json
    {
      "name": "@sdcx/module-b",
      "version": "1.0.0",
      "main": "src/index",
      "dependencies": {}
    }
    
    // module-b/src/Flower.tsx
    export function Flower() {
      return <Image source={require('./images/flower_1.png')} />
    }
    
    // module-b/src/index.ts
    export * from './Flower'
    

    调整 app 模块

    修改 app 模块中的文件和内容如下

    app 声明了对其它所有子模块的依赖

    // app/package.json
    {
      "name": "app",
      "version": "1.0.0",
      "dependencies": {
        "@sdcx/common": "1.0.0",
        "@sdcx/module-a": "1.0.0",
        "@sdcx/module-b": "1.0.0"
      }
    }
    

    app 在 index.ts 文件中使用 module-a 模块

    // app/src/index.ts
    import { setupGlobalStyle } from '@sdcx/module-a'
    
    // 配置全局样式
    setupGlobalStyle()
    

    app 在 App.tsx 文件中使用了 common 和 module-b 模块

    // app/src/App.tsx
    import { DEFAULT_NAME, log } from '@sdcx/common'
    import { Flower } from '@sdcx/module-b'
    
    function App() {
      const [name, setName] = useState(DEFAULT_NAME)
      const [text, setText] = useState('')
    
      return (
        <View style={styles.container}>
          <View style={styles.row}>
            <Flower />
            <Welcome name={name} />
          </View>
          <Button
            
            onPress={() => {
              const n = text || DEFAULT_NAME
              log(`向 ${n} 打招呼`)
              setName(n)
            }}
          />
        </View>
      )
    }
    

    背后的魔法

    可见,依赖子项目就像依赖第三方库一样。Yarn Workspace 是怎么做到的呢?

    是通过软链,从 node_modules 链接到子项目所在对应文件夹。

    当我们通过 @sdcx/common 这样的方式导入依赖时,默认会去 node_modules 目录下查找,那能不能找到呢?还真找到了,打开 node_modules 目录看一眼

    Monorepo 在 React Native 项目的实践

    我们在 node_modules 目录下 @sdcx 这个 scope 找到了我们的三个子项目,仔细看一下,这三个目录右边都有一个箭头,这表示软链接。Node 模块解析器首先在 node_modules 下寻找,发现这几个模块是软链,然后顺着软链找到了模块的真正所在。

    限制导入

    Multirepo 把不同模块分割在不同 Git 仓库中,从物理上隔离了模块,不会出现导入未经声明依赖的模块的问题。

    而在 Monorepo 中,是否可以导入未经声明依赖的模块呢?

    我们的 module-b,并未在它的 package.json 文件中声明依赖 common,那么它是否可以导入并使用 common 模块呢?我们来试试看

    修改 module-b/src/Flower.tsx 文件

    import { log } from '@sdcx/common'
    
    export function Flower() {
      useEffect(() => {
        log(`渲染了 Flower `)
      })
    
      return <Image source={require('./images/flower_1.png')} />
    }
    

    运行项目,发现毫无问题,我们可以在控制台上看到 渲染了 Flower 字样。

    修改为

    import { log } from '../../packages/common'
    

    也一样毫无问题。

    未经声明依赖,就可以导入并使用其它子模块,无法满足从物理上隔离的需求。

    此时,我们需要引入 eslint-plugin-workspaces 这个 eslint 插件来拯救。

    yarn add eslint-plugin-workspaces -D -W
    

    -D 表示这是个 dev 依赖,-W 表示把依赖安装到 workspace 中,也就是项目根目录下的 package.json 文件中,因为我们的项目现在有好多个 package.json 文件呢。

    然后配置 .eslintrc.js 文件

    module.exports = {
      root: true,
      plugins: ['workspaces'],
      rules: {
        'workspaces/no-relative-imports': 'error',
        'workspaces/require-dependency': 'error',
      },
    }
    

    运行 npm run lint 就会得到未经声明不得导入的提示。

    项目管理者只需要关注每个子项目的 package.json 文件,就可以知道是否有模块依赖了它不应该依赖的模块,保证依赖路径,确保项目可维护性。

    如果某些情况还限制不了,可以配合 no-restricted-imports 这条 eslint 规则使用。

    安装第三方依赖

    每个子项目都可以有自己的依赖,但整个工程只有一个 node_module 文件夹,因为 yarn 会把这些依赖拍平,都放到根目录的 node_module 文件夹下。

    如官方文档所示,为某个子项目安装依赖,使用如下形式

    yarn workspace @sdcx/module-b add @react-native-community/viewpager
    

    如果想要为所有子模块都安装同样的依赖,使用如下形式

    yarn add eslint-plugin-workspaces -D -W
    

    为什么不用 Lerna

    说起 Monorepo,几乎都会提到 Lerna。而且 Yarn Workspace 可以和 Lerna 配合使用。总的来说,Yarn 负责依赖管理,Lerna 负责发布。React Native 工程是一个 App,并不需要发布到 Npm 仓库,Lerna 在这里没有用武之地。

    基础库适合使用 Monorepo 吗

    我们 App 团队有 30 几个基础库,譬如开源的有 hybrid-navigation,react-native-platform。

    这些基础库,有 UI 相关的,有平台相关的,有第三方 SDK 相关的,它们都独立成库,每个库都配备 Example 项目,几乎都包含三端代码,可以单独运行测试。

    这些组件库没有相关性,互不依赖,它们不应该放在一起,而应该分离。

    有的组件库,譬如日志组件,还包含服务器端代码,web 前端代码,这些代码都放到了同一个 repo 当中。

    因为我们的 App 是由不同业务线和基础业务组合而成,不同业务线分割在不同子项目中,方便管理,万一日后某条业务线被砍,可以方便移除代码。

    是的,我们按照业务线拆分子项目。

    源码

    最后附上源码,希望我们的经验能对你有所启发。


    起源地下载网 » Monorepo 在 React Native 项目的实践

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元