本文会分享一个我在实际工作中遇到的案例,从最开始的需求分析到项目搭建,以及最后落地的架构的整个过程。最终实现的效果是使用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.jsonpackages/common/                      --- 共享的业务组件package.jsonadmin-site/                  --- 柜员网站项目package.jsoncustomer-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.Itemlabel="姓名"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圈圈改成这个水费表单吧:

然后再运行下:

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

使用mono-repo实现跨项目组件共享相关推荐

  1. 如何搭建一个内部组件共享平台

    如今前端越来越趋于组件化的开发方式,最大的益处就是UI页面和逻辑的共用. 在开发者的眼里,如果你打开一个网站,组件化的开发方式会让你看起来像这个样子: 逻辑功能上我们会封装成一个库,然后NPM发布到公 ...

  2. 【前端框架-Vue-基础】$attr及$listeners实现跨多级组件的通信

    父子 A 组件与 B 组件之间的通信: (父子组件) 如上图所示,A.B.C三个组件依次嵌套,按照 Vue 的开发习惯,父子组件通信可以通过以下方式实现: A to B 通过props的方式向子组件传 ...

  3. Svelte入门——Web Components实现跨框架组件复用(二)

    在上节中,我们一起了解了如何使用Svelte封装Web Component,从而实现在不同页面间使用电子表格组件. Svelte封装组件跨框架复用,带来的好处也十分明显: 1.使用框架开发,更容易维护 ...

  4. 实现Android跨进程组件通信能有多简单?

    实现Android跨进程组件通信能有多简单? 作为一个Android开发,都要会点组件化知识.组件化的主要的特点,是剥离依赖,但组件间不直接依赖后,通信问题要怎么解决呢. 通常我们用的一下这种类似Bi ...

  5. NLP-预训练模型-2019:ALBert【 轻Bert;使用 “输入层向量矩阵分解”、“跨层参数共享” 减少参数量;使用SOP代替NSP】【较Bert而言缩短训练及推理时间】

    预训练模型(Pretrained model):一般情况下预训练模型都是大型模型,具备复杂的网络结构,众多的参数量,以及在足够大的数据集下进行训练而产生的模型. 在NLP领域,预训练模型往往是语言模型 ...

  6. 如何利用 DITA 实现高效的跨部门内容共享?

    19 世纪,达尔文说:"适者生存":21 世纪,达尔文信息分类架构 (Darwin Information Typing Architecture, DITA) 主张对信息进行模块 ...

  7. 历史性的时刻!OpenTiny 跨端、跨框架组件库正式升级 TypeScript,10 万行代码重获新生!

    大家好,我是 Kagol,OpenTiny 开源社区运营,TinyVue 跨端.跨框架组件库核心贡献者,专注于前端组件库建设和开源社区运营. 微软于3月16日发布了 TypeScript 5.0 版本 ...

  8. node.js require 自动执行脚本 并生成html,利用node.js实现自动生成前端项目组件的方法详解...

    本文主要给大家介绍了关于利用node.js实现自动生成前端项目组件的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 脚本编写背景 写这个小脚本的初衷是,项目本身添加一个组件太 ...

  9. 怎么查看地址值_西门子S7-200 SMART PID回路控制,配置PID向导,查看项目组件

    本篇我们继续来学习西门子S7-200 SMART的PID回路控制,首先介绍一下如何配置PID向导.在工具菜单功能区单击PID按钮打开PID回路向导对话框,选择要组态的回路,最多可组态8个回路,这里我们 ...

最新文章

  1. 20210709未来智能实验室收录资料
  2. ListView和GridView的一些杂七八杂的内容
  3. 例子:倒计时按钮可用
  4. 阿里云开发者大会:资源加应用酝酿云存储变局
  5. unix基础之改变文件权限
  6. tensorflow 之 最近用到的几个小操作tf.reshape,tf.convert_to_tensor,tf.where
  7. 多麦克风做拾音的波束_麦克风丨人声应该用动圈话筒还是电容话筒?
  8. java贪吃蛇撞壁转弯代码_java贪吃蛇demo
  9. CentOS 下重装 jdk
  10. 更改管理员密码以后网站不能启动
  11. Python基础——numpy.ndarray一维数组与多维数组
  12. android蓝牙hfp client使用例
  13. python3 下载网络图片
  14. 爬动漫“上瘾”之后,放弃午休,迫不及待的用Python薅了腾Xun动漫的数据,啧啧啧
  15. EDA安装------Centos7安装ANSYS
  16. 免费网络视频监控软件cmsclient
  17. PHP接入谷歌验证器(Google Authenticator)
  18. LeetCode 43. 字符串相乘【c++/java详细题解】
  19. debian10使用蓝牙aptx
  20. Eclipse 导入maven项目报 Unknown Faceted Project Problem (Java Version Mismatch) 问题解决

热门文章

  1. html爱情表白网页模板下载_响应式爱情表白网页代码_HTML静态页面下载
  2. 关于电脑双屏后,微信截屏会黑屏,且截屏范围不全的问题解决方法
  3. llc谐振闭环电路基于simulink
  4. 【Android 开发实例】时间管理APP开发之整体包架构分析
  5. 基于resnet-18的轻量型车道检测研究
  6. 谁来切分1.8万亿元的社区服务蛋糕?
  7. itextpdf给pdf添加水印
  8. mysql条件下触发_MySQL的( )可在一定条件下被触发、自动执行一些语句序列。_学小易找答案...
  9. C语言的getchar和putchar
  10. Hive 本地模式,远程模式模式的搭建、命令行操作、Hive JDBC操作