本文会分享一个我在理论工作中遇到的案例,从最开始的需要剖析到我的项目搭建,以及最初落地的架构的整个过程。最终实现的成果是应用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.jsimport { 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