共计 11036 个字符,预计需要花费 28 分钟才能阅读完成。
本文会分享一个我在理论工作中遇到的案例,从最开始的需要剖析到我的项目搭建,以及最初落地的架构的整个过程。最终实现的成果是应用 mono-repo
实现了跨我的项目的组件共享。在本文中你能够看到:
- 从接到需要到深入分析并构建架构的整个思考过程。
mono-repo
的简略介绍。mono-repo
实用的场景剖析。- 产出一个能够跨我的项目共享组件的我的项目架构。
本文产出的架构模板曾经上传到 GitHub,如果你刚好须要一个 mono-repo + react 的模板,间接 clone 下来吧:https://github.com/dennis-jiang/mono-repo-demo
需要
需要详情
是这么个状况,我还是在那家外企供职,不久前咱们接到一个需要:要给外国的政府部门或者他的代理机构开发一个能够缴纳水电费,顺便还能卖卖可乐的网站。次要应用场景是市政厅之类的中央,相似这个样子:
这张图是我在网上轻易找的某银行的图片,跟咱们应用场景有点相似。他有个自助的 ATM 机,远处还有人工柜台。咱们也会有自助机器,另外也会有人工柜台,这两个中央都能够交水电费,汽车罚款什么的,惟一有个区别是人工那里除了交各种账单,还可能会卖点货色,比方口渴了买个可乐,烟瘾犯了来包中华。
需要剖析
下面只是个详情,要做下来还有很多货色须要细化,柜员应用的性能和客户自助应用的性能看起来差不多,细想下来区别还真不少:
- 无论是交账单还是卖可乐,咱们都能够将它视为一个商品,既然卖商品那必定有上架和下架的性能,也就是商品治理,这个必定只能做在柜员端。
- 市政厅人员泛滥,也会有上下级关系,一般柜员可能没有权限上 / 下架,他可能只有售卖权限,上 / 下架可能须要经理能力操作,这意味着柜员界面还须要权限治理。
- 权限治理的根底必定是用户治理,所以柜员界面须要做登陆和注册。
- 客户自助界面只能交账单不能卖可乐很好了解,因为是自助机,旁边无人值守,如果摆几瓶可乐,他可能会拿了可乐不付钱。
- 那客户自助交水电费须要登陆吗?不须要!跟国内差不多,只须要输出卡号和姓名等根本信息就能够查问到账单,而后线上信用卡就付了。所以客户界面不须要登陆和用户治理。
从下面这几点剖析咱们能够看出,柜员界面会多很多性能,包含商品治理,用户治理,权限治理等,而客户自助界面只能交账单,其余性能都没有。
原型设计
基于下面几点剖析,咱们的设计师很快设计了两个界面的原型。
这个是柜员界面的:
柜员界面看起来也很清新,下面一个头部,左上角显示了以后机构的名称,右上角显示了以后用户的名字和设置入口。登陆 / 登出相干性能点击用户名能够看到,商品治理,用户治理须要点击设置按钮进行跳转。
这个是客户自助界面的:
这个是客户界面的,看起来根本是一样的,只是少了用户和设置那一块,卖的货色少了可乐,只能交账单。
技术
当初需要根本曾经理分明了,上面就该咱们技术出马了,进行技术选型和架构落地。
一个站点还是两个站点?
首先咱们须要思考的一个问题就是,柜员界面和客户界面是做在一个网站外面,还是独自做两个网站?因为两个界面高度类似,所以咱们齐全能够做在一起,在客户自助界面暗藏掉右上角的用户和设置就行了。
然而这外面其实还暗藏着一个问题:柜员界面是须要登陆的,所以他的入口其实是登陆页;客户界面不须要登陆,他的入口应该间接就是售卖页 。如果将他们做在一起,因为不晓得是柜员应用还是客户应用,所以入口只能都是登录页,柜员间接登陆进入售卖页,对于客户能够独自加一个“客户自助入口”让他进入客户的售卖页面。然而这样 用户体验不好,客户原本不须要登陆的,你给他看一个登录页可能会造成困惑,可能须要频繁求教工作人员才晓得怎么用,会升高整体的工作效率,所以产品经理并不承受这个,要求客户一进来就须要看到客户的售卖页面。
而且从技术角度思考,当初咱们是一个 if...else...
暗藏用户和设置就行了,那万一当前两个界面差别变大,客户界面要求更花哨的成果,就不是简略的一个 if...else...
能搞定的了。所以最初咱们决定部署两个站点,柜员界面和客户界面独自部署到两个域名上。
组件反复
既然是两个站点,思考到我的项目的可扩展性,咱们创立了两个我的项目。然而这两个我的项目的 UI 在目前阶段是如此类似,如果咱们写两套代码,势必会有很多组件是反复的,比拟典型的就是下面的商品卡片,购物车组件等。其实除了下面能够看到这些会反复外,咱们往深刻想,交个水费,咱们必定还须要用户输出姓名,卡号之类的信息,所以点了水费的卡片后必定会有一个输出信息的表单,而且这个表单在柜员界面和客户界面根本是一样的,除了水费表单外,还有电费表单,罚单表单等等,所以能够预感反复的组件会十分多。
作为一个有谋求的工程师,这种反复组件必定不能靠 CV 大法来解决,咱们得想方法让这些组件能够复用。那组件怎么复用呢?提个公共组件库嘛,置信很多敌人都会这么想。咱们也是这么想的,然而公共组件库有多种组织形式,咱们次要思考了这么几种:
独自 NPM 包
再创立一个我的项目,这个我的项目专门放这些可复用的组件,相似于咱们平时用的 antd
之类的,创立好后公布到公司的公有 NPM 仓库上,应用的时候间接这样:
import {Cart} from 'common-components';
然而,咱们须要复用的这些组件跟 antd
组件有一个实质上的区别:咱们须要复用的是业务组件,而不是单纯的 UI 组件 。antd
UI 组件库为了保障通用性,根本不带业务属性,款式也是凋谢的。然而我这里的业务组件不仅仅是几个按钮,几个输入框,而是一个残缺的表单,包含前端验证逻辑都须要复用, 所以我须要复用的组件其实是跟业务强绑定的。因为他是跟业务强绑定的,即便我将它作为一个独自的 NPM 包公布进来,公司的其余我的项目也用不了。一个不能被其余我的项目共享的 NPM 包,始终感觉有点违和呢。
git submodule
另一个计划是 git submodule
,咱们照样为这些共享组件创立一个新的 Git 我的项目,然而不公布到 NPM 仓库去骚扰他人,而是间接在咱们主我的项目以git submodule
的形式援用他。git submodule
的根本应用办法网上有很多,我这里就不啰嗦了,次要说几个毛病,也是咱们没采纳他的起因:
- 实质上
submodule
和主我的项目是两个不同的git repo
,所以你须要为每个我的项目创立一套脚手架(代码标准,公布脚本什么的)。 submodule
其实只是主我的项目保留了一个对子我的项目的依赖链接,阐明了以后版本的主我的项目依赖哪个版本的子项目,你须要小心的应用git submodule update
来治理这种依赖关系。如果没有正确应用git submodule update
而搞乱了版本的依赖关系,那就呵呵了。。。- 公布的时候须要本人小心解决依赖关系,先发子项目,子项目好了再公布主我的项目。
mono-repo
mono-repo
是当初越来越风行的一种项目管理形式了,与之绝对的叫 multi-repo
。multi-repo
就是 多个仓库
,下面的git submodule
其实就是 multi-repo
的一种形式,主我的项目和子项目都是独自的 git 仓库
,也就形成了 多个仓库
。而mono-repo
就是 一个大仓库
,多个我的项目都放在 一个 git 仓库
外面。当初很多出名开源我的项目都是采纳的 mono-repo
的组织形式,比方 Babel
,React
,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
文件夹,外面有四个我的项目:
- react-router:是
React-Router
的外围库,解决一些共用的逻辑 - react-router-config:是
React-Router
的配置解决库 - react-router-dom:浏览器上应用的库,会援用
react-router
外围库 - react-router-native:反对
React-Native
的路由库,也会援用react-router
外围库
这四个我的项目都是为 react
的路由治理服务的,在业务上有很强的关联性,实现一个性能可能须要多个我的项目配合能力实现。比方修某个 BUG 须要同时改 react-router-dom
和react-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.json
和lerna.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/
目录是空的,依据咱们后面的构想,咱们须要创立三个我的项目:
common
:共享的业务组件,自身不须要运行,放各种组件就行了。admin-site
:柜员站点,须要可能运行,应用create-react-app
创立吧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-site
和customer-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-site
和customer-site
的 3.1.0
版本挪动到顶层,而 common
我的项目下会保留本人 4.9.4
的antd
,这样每个子项目都能够拿到本人须要的依赖了。
yarn workspace
应用也很简略,yarn 1.0
以上的版本默认就是开启 workspace
的,所以咱们只须要在顶层的 package.json
加一个配置就行:
// 顶层 package.json
{
"workspaces": ["packages/*"]
}
而后在 lerna.json
外面指定 npmClient
为yarn
,并将 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-site
的package.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-site
的package.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-loader
的include
门路外面增加上这个门路:
当初再运行下咱们的我的项目就失常了:
最初别忘了,还有咱们的 customer-site
哦,这个解决起来就简略了,因为后面咱们曾经调好了整个主我的项目的构造,咱们能够将 customer-site
的其余依赖都删了,只保留@mono-repo-demo/common
,而后调整下启动脚本就行了:
这样客户站点也能够引入公共组件并启动了。
公布
最初要留神的一点是,当咱们批改实现后,须要公布了,肯定要应用lerna publish
,他会主动帮我更新依赖的版本号。比方我当初略微批改了一下水费表单,而后提交:
当初我试着公布一下,运行
lerna publish
运行后,他会让你抉择新的版本号:
我这里抉择一个 minor
,也就是版本号从0.0.0
变成 0.1.0
, 而后lerna
会自动更新相干的依赖版本,包含:
lerna.json
本人版本号升为0.1.0
:common
的版本号变为0.1.0
:admin-site
的版本号也变为0.1.0
,同时更新依赖的common
为0.1.0
: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
上面咱们再来回顾下本文的要点:
- 事件的起源是咱们接到了一个外国人交水电费并能卖东西的需要,有柜员端和客户自助端。
- 通过剖析,咱们决定将柜员端和客户自助端部署为两个站点。
- 为了这两个站点,咱们新建了两个我的项目,这样扩展性更好。
- 这两个我的项目有很多长得一样的业务组件,咱们须要复用他们。
- 为了复用这些业务组件,咱们引入了
mono-repo
的架构来进行项目管理,mono-repo
特地适宜分割严密的多个我的项目。 mono-repo
最闻名的工具是lerna
。lerna
能够主动治理各个我的项目之间的依赖以及node_modules
。- 应用
lerna bootstrap --hoist
能够将子项目的node_modules
晋升到顶层,解决node_modules
反复的问题。 - 然而
lerna bootstrap --hoist
在晋升时如果遇到各个子项目援用的依赖版本不统一,会晋升应用最多的版本,从而导致少数派那个找不到正确的依赖,产生谬误。 - 为了解决晋升时版本抵触的问题,咱们引入了
yarn workspace
,他也会晋升用的最多的版本,然而会为少数派保留本人的依赖在本人的node_modules
上面。 - 咱们示例中两个 CRA 我的项目都有本人的脚手架,而
common
没有脚手架,咱们调整了脚手架,将它挪到了最顶层,从而三个我的项目能够共享。 - 公布的时候应用
lerna publish
,他会自动更新外部依赖,并更新各个子项目本人的版本号。 - 子项目的版本号规定能够在
lerna.json
外面配置,如果配置为固定版本号,则各个子项目保持一致的版本,如果配置为independent
关键字,各个子项目能够有本人不同的版本号。
参考资料
- Lerna 官网:https://lerna.js.org/
- Yarn workspace: https://classic.yarnpkg.com/en/docs/workspaces/
文章的最初,感激你破费贵重的工夫浏览本文,如果本文给了你一点点帮忙或者启发,请不要悭吝你的赞和 GitHub 小星星,你的反对是作者继续创作的能源。
欢送关注我的公众号进击的大前端第一工夫获取高质量原创~
“前端进阶常识”系列文章源码地址:https://github.com/dennis-jiang/Front-End-Knowledges