关于前端:使用monorepo实现跨项目组件共享

4次阅读

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

本文会分享一个我在理论工作中遇到的案例,从最开始的需要剖析到我的项目搭建,以及最初落地的架构的整个过程。最终实现的成果是应用 mono-repo 实现了跨我的项目的组件共享。在本文中你能够看到:

  1. 从接到需要到深入分析并构建架构的整个思考过程。
  2. mono-repo的简略介绍。
  3. mono-repo实用的场景剖析。
  4. 产出一个能够跨我的项目共享组件的我的项目架构。

本文产出的架构模板曾经上传到 GitHub,如果你刚好须要一个 mono-repo + react 的模板,间接 clone 下来吧:https://github.com/dennis-jiang/mono-repo-demo

需要

需要详情

是这么个状况,我还是在那家外企供职,不久前咱们接到一个需要:要给外国的政府部门或者他的代理机构开发一个能够缴纳水电费,顺便还能卖卖可乐的网站。次要应用场景是市政厅之类的中央,相似这个样子:

这张图是我在网上轻易找的某银行的图片,跟咱们应用场景有点相似。他有个自助的 ATM 机,远处还有人工柜台。咱们也会有自助机器,另外也会有人工柜台,这两个中央都能够交水电费,汽车罚款什么的,惟一有个区别是人工那里除了交各种账单,还可能会卖点货色,比方口渴了买个可乐,烟瘾犯了来包中华。

需要剖析

下面只是个详情,要做下来还有很多货色须要细化,柜员应用的性能和客户自助应用的性能看起来差不多,细想下来区别还真不少:

  1. 无论是交账单还是卖可乐,咱们都能够将它视为一个商品,既然卖商品那必定有上架和下架的性能,也就是商品治理,这个必定只能做在柜员端。
  2. 市政厅人员泛滥,也会有上下级关系,一般柜员可能没有权限上 / 下架,他可能只有售卖权限,上 / 下架可能须要经理能力操作,这意味着柜员界面还须要权限治理。
  3. 权限治理的根底必定是用户治理,所以柜员界面须要做登陆和注册。
  4. 客户自助界面只能交账单不能卖可乐很好了解,因为是自助机,旁边无人值守,如果摆几瓶可乐,他可能会拿了可乐不付钱。
  5. 那客户自助交水电费须要登陆吗?不须要!跟国内差不多,只须要输出卡号和姓名等根本信息就能够查问到账单,而后线上信用卡就付了。所以客户界面不须要登陆和用户治理。

从下面这几点剖析咱们能够看出,柜员界面会多很多性能,包含商品治理,用户治理,权限治理等,而客户自助界面只能交账单,其余性能都没有。

原型设计

基于下面几点剖析,咱们的设计师很快设计了两个界面的原型。

这个是柜员界面的

柜员界面看起来也很清新,下面一个头部,左上角显示了以后机构的名称,右上角显示了以后用户的名字和设置入口。登陆 / 登出相干性能点击用户名能够看到,商品治理,用户治理须要点击设置按钮进行跳转。

这个是客户自助界面的

这个是客户界面的,看起来根本是一样的,只是少了用户和设置那一块,卖的货色少了可乐,只能交账单。

技术

当初需要根本曾经理分明了,上面就该咱们技术出马了,进行技术选型和架构落地。

一个站点还是两个站点?

首先咱们须要思考的一个问题就是,柜员界面和客户界面是做在一个网站外面,还是独自做两个网站?因为两个界面高度类似,所以咱们齐全能够做在一起,在客户自助界面暗藏掉右上角的用户和设置就行了。

然而这外面其实还暗藏着一个问题:柜员界面是须要登陆的,所以他的入口其实是登陆页;客户界面不须要登陆,他的入口应该间接就是售卖页 。如果将他们做在一起,因为不晓得是柜员应用还是客户应用,所以入口只能都是登录页,柜员间接登陆进入售卖页,对于客户能够独自加一个“客户自助入口”让他进入客户的售卖页面。然而这样 用户体验不好,客户原本不须要登陆的,你给他看一个登录页可能会造成困惑,可能须要频繁求教工作人员才晓得怎么用,会升高整体的工作效率,所以产品经理并不承受这个,要求客户一进来就须要看到客户的售卖页面。

而且从技术角度思考,当初咱们是一个 if...else... 暗藏用户和设置就行了,那万一当前两个界面差别变大,客户界面要求更花哨的成果,就不是简略的一个 if...else... 能搞定的了。所以最初咱们决定部署两个站点,柜员界面和客户界面独自部署到两个域名上

组件反复

既然是两个站点,思考到我的项目的可扩展性,咱们创立了两个我的项目。然而这两个我的项目的 UI 在目前阶段是如此类似,如果咱们写两套代码,势必会有很多组件是反复的,比拟典型的就是下面的商品卡片,购物车组件等。其实除了下面能够看到这些会反复外,咱们往深刻想,交个水费,咱们必定还须要用户输出姓名,卡号之类的信息,所以点了水费的卡片后必定会有一个输出信息的表单,而且这个表单在柜员界面和客户界面根本是一样的,除了水费表单外,还有电费表单,罚单表单等等,所以能够预感反复的组件会十分多。

作为一个有谋求的工程师,这种反复组件必定不能靠 CV 大法来解决,咱们得想方法让这些组件能够复用。那组件怎么复用呢?提个公共组件库嘛,置信很多敌人都会这么想。咱们也是这么想的,然而公共组件库有多种组织形式,咱们次要思考了这么几种:

独自 NPM 包

再创立一个我的项目,这个我的项目专门放这些可复用的组件,相似于咱们平时用的 antd 之类的,创立好后公布到公司的公有 NPM 仓库上,应用的时候间接这样:

import {Cart} from 'common-components';

然而,咱们须要复用的这些组件跟 antd 组件有一个实质上的区别:咱们须要复用的是业务组件,而不是单纯的 UI 组件 antdUI 组件库为了保障通用性,根本不带业务属性,款式也是凋谢的。然而我这里的业务组件不仅仅是几个按钮,几个输入框,而是一个残缺的表单,包含前端验证逻辑都须要复用, 所以我须要复用的组件其实是跟业务强绑定的。因为他是跟业务强绑定的,即便我将它作为一个独自的 NPM 包公布进来,公司的其余我的项目也用不了。一个不能被其余我的项目共享的 NPM 包,始终感觉有点违和呢。

git submodule

另一个计划是 git submodule,咱们照样为这些共享组件创立一个新的 Git 我的项目,然而不公布到 NPM 仓库去骚扰他人,而是间接在咱们主我的项目以git submodule 的形式援用他。git submodule的根本应用办法网上有很多,我这里就不啰嗦了,次要说几个毛病,也是咱们没采纳他的起因:

  1. 实质上 submodule 和主我的项目是两个不同的git repo,所以你须要为每个我的项目创立一套脚手架(代码标准,公布脚本什么的)。
  2. submodule其实只是主我的项目保留了一个对子我的项目的依赖链接,阐明了以后版本的主我的项目依赖哪个版本的子项目,你须要小心的应用 git submodule update 来治理这种依赖关系。如果没有正确应用 git submodule update 而搞乱了版本的依赖关系,那就呵呵了。。。
  3. 公布的时候须要本人小心解决依赖关系,先发子项目,子项目好了再公布主我的项目。

mono-repo

mono-repo是当初越来越风行的一种项目管理形式了,与之绝对的叫 multi-repomulti-repo 就是 多个仓库 ,下面的git submodule 其实就是 multi-repo 的一种形式,主我的项目和子项目都是独自的 git 仓库,也就形成了 多个仓库 。而mono-repo 就是 一个大仓库 ,多个我的项目都放在 一个 git 仓库 外面。当初很多出名开源我的项目都是采纳的 mono-repo 的组织形式,比方 BabelReact ,Jest, create-react-app, react-router 等等。mono-repo特地适宜分割严密的多个我的项目,比方本文面临的这种状况,上面咱们就进入本文的主题,认真看下mono-repo

mono-repo

其实我之前写 react-router 源码解析的时候就提到过 mono-repo,过后就说有机会独自写一篇mono-repo 的文章,本文也算是把坑填上了。所以咱们先从 react-router 的源码构造动手,来看下 mono-repo 的整体状况,下图就是 react-router 的源码构造:

咱们发现他有个 packages 文件夹,外面有四个我的项目:

  1. react-router:是 React-Router 的外围库,解决一些共用的逻辑
  2. react-router-config:是 React-Router 的配置解决库
  3. react-router-dom:浏览器上应用的库,会援用 react-router 外围库
  4. react-router-native:反对 React-Native 的路由库,也会援用 react-router 外围库

这四个我的项目都是为 react 的路由治理服务的,在业务上有很强的关联性,实现一个性能可能须要多个我的项目配合能力实现。比方修某个 BUG 须要同时改 react-router-domreact-router的代码,如果他们在不同的 Git 仓库,须要在两个仓库外面别离批改,提交,打包,测试,而后还要批改彼此依赖的版本号能力失常工作。然而应用了 mono-repo,因为他们代码都在同一个 Git 仓库,咱们在一个commit 外面就能够批改两个我的项目的代码,而后对立打包,测试,公布,如果咱们应用了 lerna 管理工具,版本号的依赖也是自动更新的,切实是不便太多了。

lerna

lerna是最出名的 mono-repo 的管理工具,明天咱们就要用它来搭建后面提到的共享业务组件的我的项目,咱们指标的我的项目构造是这个样子的:

mono-repo-demo/                  --- 主我的项目,这是一个 Git 仓库
  package.json
  packages/
    common/                      --- 共享的业务组件
      package.json
    admin-site/                  --- 柜员网站我的项目
      package.json
    customer-site/               --- 客户网站我的项目
      package.json

lerna init

lerna初始化很简略,先创立一个空的文件夹,而后运行:

npx lerna init

这行命令会帮我创立一个空的 packages 文件夹,一个 package.jsonlerna.json,整个构造长这样:

package.json中有一点须要留神,他的 private 必须设置为 true,因为mono-repo 自身的这个 Git 仓库并不是一个我的项目,他是多个我的项目,所以他本人不能间接公布,公布的应该是 packages/ 上面的各个子项目。

"private": true,

lerna.json初始化长这样:

{
  "packages": ["packages/*"],
  "version": "0.0.0"
}

packages字段就是标记你子项目的地位,默认就是 packages/ 文件夹,他是一个数组,所以是反对多个不同地位的。另外一个须要特地留神的是 version 字段,这个字段有两个类型的值,一个是像下面的 0.0.0 这样一个具体版本号,还能够是 independent 这个关键字。如果是 0.0.0 这种具体版本号,那 lerna 治理的所有子项目都会有雷同的版本号 —-0.0.0,如果你设置为independent,那各个子项目能够有本人的版本号,比方子项目 1 的版本号是0.0.0,子项目 2 的版本号能够是0.1.0

创立子项目

当初咱们的 packages/ 目录是空的,依据咱们后面的构想,咱们须要创立三个我的项目:

  1. common:共享的业务组件,自身不须要运行,放各种组件就行了。
  2. admin-site:柜员站点,须要可能运行,应用 create-react-app 创立吧
  3. customer-site:客户站点,也须要运行,还是应用 create-react-app 创立

创立子项目能够应用 lerna 的命令来创立:

lerna create <name>

也能够本人手动创立文件夹,这里 common 子项目我就用 lerna 命令创立吧,lerna create common,运行后 common 文件夹就呈现在 packages 上面了:

这个是应用 lerna create 默认生成的目录构造,__test__文件夹上面放得是单元测试内容,lib上面放得是代码。因为我是筹备用它来放共享组件的,所以我把目录结构调整了,默认生成的两个文件夹都删了,新建了一个 components 文件夹:

另外两个可运行站点都用 create-react-app 创立了,在 packages 文件夹下运行:

npx create-react-app admin-site; npx create-react-app customer-site;

几个我的项目都创立完后,整个我的项目构造是这样的:

依照 mono-repo 的常规,这几个子项目的名称最好命名为 @< 主项目名称 >/< 子项目名称 >,这样当他人援用你的时候,你的这几个我的项目都能够在node_modules 的同一个目录上面,目录名字就是 @< 主项目名称 >,所以咱们手动改下三个子项目package.json 外面的 name 为:

@mono-repo-demo/admin-site
@mono-repo-demo/common
@mono-repo-demo/customer-site

lerna bootstrap

下面的图片能够看到,packages/上面的每个子项目有本人的 node_modules,如果将它关上,会发现很多反复的依赖包,这会占用咱们大量的硬盘空间。lerna 提供了另一个弱小的性能:将子项目的依赖包都提取到最顶层 ,咱们只须要 先删除子项目的 node_modules 再跑上面这行命令就行了

lerna bootstrap --hoist

删除曾经装置的子项目 node_modules 能够手动删,也能够用这个命令:

lerna clean

yarn workspace

lerna bootstrap --hoist尽管能够将子项目的依赖晋升到顶层,然而他的形式比拟粗犷:先在每个子项目运行npm install,等所有依赖都装置好后,将他们挪动到顶层的node_modules。这会导致一个问题,如果多个子项目依赖同一个第三方库,然而需要的版本不同怎么办?比方咱们三个子项目都依赖antd,然而他们的版本不齐全一样:

// admin-site
"antd": "3.1.0"

// customer-site
"antd": "3.1.0"

// common
"antd": "4.9.4"

这个例子中 admin-sitecustomer-site须要的 antd 版本都是 3.1.0,然而common 须要的版本却是 4.9.4,如果应用lerna bootstrap --hoist 来进行晋升,lerna会晋升用的最多的版本,也就是 3.1.0 到顶层,而后把子我的项目的 node_modules 外面的 antd 都删了。也就是说 common 去拜访 antd 的话,也会拿到 3.1.0 的版本,这可能会导致 common 我的项目工作不失常。

这时候就须要介绍 yarn workspace 了,他能够解决后面说的版本不统一的问题,lerna bootstrap --hoist 会把所有子项目用的最多的版本挪动到顶层,而 yarn workspace 则会查看每个子项目外面依赖及其版本,如果版本不一样则会留在子项目本人的node_modules 外面,只有齐全一样的依赖才会晋升到顶层。

还是以下面这个 antd 为例,应用 yarn workspace 的话,会把 admin-sitecustomer-site3.1.0 版本挪动到顶层,而 common 我的项目下会保留本人 4.9.4antd,这样每个子项目都能够拿到本人须要的依赖了。

yarn workspace应用也很简略,yarn 1.0以上的版本默认就是开启 workspace 的,所以咱们只须要在顶层的 package.json 加一个配置就行:

// 顶层 package.json
{
  "workspaces": ["packages/*"]
}

而后在 lerna.json 外面指定 npmClientyarn,并将 useWorkspaces 设置为true

// lerna.json
{
  "npmClient": "yarn",
  "useWorkspaces": true
}

应用了 yarn workspace,咱们就不必lerna bootstrap 来装置依赖了,而是像以前一样 yarn install 就行了,他会主动帮咱们晋升依赖,这里的 yarn install 无论在顶层运行还是在任意一个子项目运行成果都是一样的。

启动子我的项目

当初咱们建好了三个子项目,要启动 CRA 子项目,能够去那个目录下运行 yarn start,然而频繁切换文件夹切实是太麻烦了。其实有了lerna 的帮忙咱们能够间接在顶层运行,这须要用到 lerna 的这个性能:

lerna run [script]

比方咱们在顶层运行了 lerna run start,这相当于去每个子项目上面都去执行yarn run start 或者 npm run start,具体是yarn 还是 npm,取决于你在lerna.json 外面的这个设置:

"npmClient": "yarn"    

如果我只想在其中一个子项目运行命令,应该怎么办呢?加上 --scope 就行了,比方我就在顶层的 package.json 外面加了这么一行命令:

// 顶层 package.json
{
  "scripts": {"start:aSite": "lerna --scope @mono-repo-demo/admin-site run start"}
}

所以咱们能够间接在顶层运行 yarn start:aSite,这会启动后面说的管理员站点,他其实运行的命令还是lerna run start,而后加了--scope 来指定在管理员子项目下运行,@mono-repo-demo/admin-site就是咱们管理员子项目的名字,是定义在这个子项目的 package.json 外面的:

// 管理员子项目 package.json
{"name": "@mono-repo-demo/admin-site"}

而后咱们理论运行下 yarn start:aSite 吧:

看到了咱们相熟的 CRA 转圈圈,阐明到目前为止咱们的配置还算顺利,哈哈~

创立公共组件

当初我的项目根本构造曾经有了,咱们建一个公共组件试一下成果。咱们就用 antd 创立一个交水费的表单吧,也很简略,就一个姓名输入框,一个查问按钮。

//  packages/common/components/WaterForm.js

import {Form, Input, Button} from 'antd';
const layout = {
  labelCol: {span: 8,},
  wrapperCol: {span: 16,},
};
const tailLayout = {
  wrapperCol: {
    offset: 8,
    span: 16,
  },
};

const WaterForm = () => {const onFinish = (values) => {console.log('Success:', values);
  };

  const onFinishFailed = (errorInfo) => {console.log('Failed:', errorInfo);
  };

  return (
    <Form
      {...layout}
      name="basic"
      initialValues={{remember: true,}}
      onFinish={onFinish}
      onFinishFailed={onFinishFailed}
    >
      <Form.Item
        label="姓名"
        name="username"
        rules={[
          {
            required: true,
            message: '请输出姓名',
          },
        ]}
      >
        <Input />
      </Form.Item>

      <Form.Item {...tailLayout}>
        <Button type="primary" htmlType="submit">
          查问
        </Button>
      </Form.Item>
    </Form>
  );
};

export default WaterForm;

引入公共组件

这个组件写好了,咱们就在 admin-site 外面援用下他,要援用下面的组件,咱们须要先在 admin-sitepackage.json外面将这个依赖加上,咱们能够去手动批改他,也能够应用 lerna 命令:

lerna add @mono-repo-demo/common --scope @mono-repo-demo/admin-site

这个命令成果跟你手动改 package.json 是一样的:

而后咱们去把 admin-site 默认的 CRA 圈圈改成这个水费表单吧:

而后再运行下:

嗯?报错了。。。如果我说这个谬误是我预料之中的,你信吗????

共享脚手架

认真看下下面的谬误,是报在 WaterForm 这个组件外面的,错误信息是说:jsx 语法不反对,最初两行还给了个倡议,叫咱们引入 babel 来编译。这些都阐明了一个同问题:babel 的配置对 common 子项目没有失效 。这其实是预料之中的,咱们的admin-site 之所以能跑起来是因为 CRA 帮咱们配置好了这些脚手架,而 common 这个子项目并没有配置这些脚手架,天然编译不了。

咱们这几个子项目都是 React 的,其实都能够共用一套脚手架,所以我的计划是:将 CRA 的脚手架全副 eject 进去,而后手动挪到顶层,让三个子项目共享。

首先咱们到 admin-site 上面运行:

yarn eject

这个命令会将 CRA 的 config 文件夹和 scripts 文件夹弹出来,同时将他们的依赖增加到 admin-sitepackage.json外面。所以咱们要干的就是手动将 config 文件夹和 scripts 文件夹挪动到顶层,而后将 CRA 增加到 package.json 的依赖也移到最顶层,具体 CRA 改了 package.json 外面的哪些内容能够通过 git 看进去的。挪动过后的我的项目构造长这样:

留神 CRA 我的项目的启动脚本在 scripts 文件夹外面,所以咱们须要略微批改下 admin-site 的启动命令:

// admin-site package.json

{"scripts": "node ../../scripts/start.js",}

当初咱们应用 yarn start:aSite 依然会报错,所以咱们持续批改 babel 的设置。

首先在 config/paths 外面增加上咱们 packages 的门路并 export 进来:

而后批改 webpacka 配置,在 babel-loaderinclude门路外面增加上这个门路:

当初再运行下咱们的我的项目就失常了:

最初别忘了,还有咱们的 customer-site 哦,这个解决起来就简略了,因为后面咱们曾经调好了整个主我的项目的构造,咱们能够将 customer-site 的其余依赖都删了,只保留@mono-repo-demo/common,而后调整下启动脚本就行了:

这样客户站点也能够引入公共组件并启动了。

公布

最初要留神的一点是,当咱们批改实现后,须要公布了,肯定要应用lerna publish,他会主动帮我更新依赖的版本号。比方我当初略微批改了一下水费表单,而后提交:

当初我试着公布一下,运行

lerna publish

运行后,他会让你抉择新的版本号:

我这里抉择一个 minor,也就是版本号从0.0.0 变成 0.1.0, 而后lerna 会自动更新相干的依赖版本,包含:

  1. lerna.json本人版本号升为0.1.0

  2. common的版本号变为0.1.0

  3. admin-site的版本号也变为 0.1.0,同时更新依赖的common0.1.0

  4. customer-site的变动跟 admin-site 是一样的。

independent version

下面这种公布策略,咱们批改了 common 的版本,admin-site的版本也变成了一样的,按理来说,这个不是必须的,admin-site只是更新依赖的 common 版本,本人的版本不肯定是降级一个 minor,兴许只是一个patch 这种状况下,admin-site的版本要不要跟着变,取决于 lerna.json 外面的 version 配置,后面说过了,如果它是一个固定的指,那所有子项目版本会保持一致,所以 admin-site 版本会跟着变,咱们将它改成 independent 就会不一样了。

// lerna.json
{"version": "independent"}

而后我再改下 common 再公布试试:

在运行下 lerna publish,咱们发现他会让你本人一个一个来选子项目的版本,我这里就能够抉择将common 降级为 0.2.0,而admin-site 只是依赖变了,就能够降级为0.1.1:

具体采纳哪种策略,是每个子项目版本都保持一致还是各自版本独立,大家能够依据本人的我的项目状况决定。

总结

这个 mono-repo 工程我曾经把代码清理了一下,上传到了 GitHub,如果你刚好须要一个 mono-repo + react 的我的项目模板,间接 clone 吧:https://github.com/dennis-jiang/mono-repo-demo

上面咱们再来回顾下本文的要点:

  1. 事件的起源是咱们接到了一个外国人交水电费并能卖东西的需要,有柜员端和客户自助端。
  2. 通过剖析,咱们决定将柜员端和客户自助端部署为两个站点。
  3. 为了这两个站点,咱们新建了两个我的项目,这样扩展性更好。
  4. 这两个我的项目有很多长得一样的业务组件,咱们须要复用他们。
  5. 为了复用这些业务组件,咱们引入了 mono-repo 的架构来进行项目管理,mono-repo特地适宜分割严密的多个我的项目。
  6. mono-repo最闻名的工具是lerna
  7. lerna能够主动治理各个我的项目之间的依赖以及node_modules
  8. 应用 lerna bootstrap --hoist 能够将子项目的 node_modules 晋升到顶层,解决 node_modules 反复的问题。
  9. 然而 lerna bootstrap --hoist 在晋升时如果遇到各个子项目援用的依赖版本不统一,会晋升应用最多的版本,从而导致少数派那个找不到正确的依赖,产生谬误。
  10. 为了解决晋升时版本抵触的问题,咱们引入了 yarn workspace,他也会晋升用的最多的版本,然而会为少数派保留本人的依赖在本人的node_modules 上面。
  11. 咱们示例中两个 CRA 我的项目都有本人的脚手架,而 common 没有脚手架,咱们调整了脚手架,将它挪到了最顶层,从而三个我的项目能够共享。
  12. 公布的时候应用lerna publish,他会自动更新外部依赖,并更新各个子项目本人的版本号。
  13. 子项目的版本号规定能够在 lerna.json 外面配置,如果配置为固定版本号,则各个子项目保持一致的版本,如果配置为 independent 关键字,各个子项目能够有本人不同的版本号。

参考资料

  1. Lerna 官网:https://lerna.js.org/
  2. Yarn workspace: https://classic.yarnpkg.com/en/docs/workspaces/

文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。

欢送关注我的公众号进击的大前端第一工夫获取高质量原创~

“前端进阶常识”系列文章源码地址:https://github.com/dennis-jiang/Front-End-Knowledges

正文完
 0