This article will share a case I met in practical work, from the initial requirement analysis to project construction and the final implementation of the architecture. The end result is component sharing across projects using Mono-repo. In this article you can see:

  1. The entire thought process from receiving requirements to in-depth analysis and building an architecture.
  2. mono-repoA brief introduction to.
  3. mono-repoAnalysis of applicable scenarios.
  4. Produce a project architecture that shares components across projects.

If you need a mono-repo + React template, clone it:Github.com/dennis-jian…

demand

Demand profiles

This is the case, I still work in that foreign company, not long ago we received a demand: to foreign government departments or his agency to develop a website that can pay water and electricity, while also selling coke. The main use scenario is a town hall or something like this:

This picture is a picture of a bank that I randomly found on the Internet, which is somewhat similar to the scene we used. He has a self-service ATM with a manual counter in the distance. We will also have self-service machines, and there will also be manual counters, both of which can pay utilities, car fines and so on. The only difference is that in addition to paying all kinds of bills, there may also be selling points, such as buying a coke when thirsty, or coming to pack China when addicted to cigarettes.

Demand analysis

The above is just an overview, there are still a lot of things to be refined, the function used by the teller and the function used by the customer self-help look similar, think about the difference is really many:

  1. Whether it is paying bills or selling coke, we can regard it as a commodity. Since selling commodities must have the function of putting on and taking off the shelves, that is, commodity management, this can only be done at the teller’s end.
  2. There are many people in the town hall, and there is also a relationship between superiors and subordinates. An ordinary teller may not have the permission to go on/off the shelves, but he may only have the permission to sell. The manager may be required to go on/off the shelves, which means that the teller interface also needs permission management.
  3. The basis of authority management must be user management, so the teller interface needs to do login and registration.
  4. Customer self-service interface can only pay the bill can not sell coke is very easy to understand, because it is self-service machine, next to the unattended, if put a few bottles of coke, he may take coke without paying.
  5. Do customers need to log in to pay water and electricity bills by themselves? Don’t need to! Just like in China, you only need to input basic information such as the card number and name to check the bill, and then pay with your credit card online. So the client interface does not need login and user management.

From the above analysis, we can see that the teller interface will have many functions, including commodity management, user management, authority management, etc., while the customer self-service interface can only pay bills, other functions are not available.

The prototype design

Based on the above analysis, our designers quickly designed two interface prototypes.

This is from the teller interface:

The teller interface also looks clean, with the top header showing the name of the current organization in the upper left corner and the name of the current user and setting entry in the upper right corner. Login/logout related functions click the user name to see, commodity management, user management need to click the Settings button to jump.

This is for the customer self-service interface:

This is the customer interface, it looks basically the same, but without the user and Settings part, it sells less coke, and only pays bills.

technology

Now the basic requirements have been clear to the manager, it is our technology to get out of the way, technical selection and architecture landing.

One site or two?

The first question we need to consider is whether the teller interface and the customer interface should be done in one website or two separate websites. Since the two interfaces are highly similar, we can do it together, and just hide the user and Settings in the upper right corner of the customer self-service interface.

But there is a hidden problem: the teller interface needs to be logged in, so his entrance is actually the landing page; The customer interface does not require login, his entry should be directly to the sales page. If they are put together, we do not know whether they are used by the teller or the customer, so the entrance can only be the login page. The teller can directly log in and enter the sales page. For the customer, a “self-service entrance” can be added separately for him to enter the customer’s sales page. But this is not a good user experience, customer didn’t need to log in, you show him a login page may cause confusion, may need to frequently consult the staff didn’t know how to use, can reduce the working efficiency of the whole, so the product manager do not accept this, demands customer came in and need to see the customer’s sales page.

And technically, right now we’re an if… else… Just hide the user and Settings. In case the two interfaces become very different and the client interface needs more fancy effects, it’s not a simple if… else… We can handle it. So in the end we decided to deploy two sites, the teller interface and the client interface on two separate domains.

Component repeat

Since these are two sites, we created two projects for scalability. However, the UI of the two projects is so similar at this stage that if we wrote two sets of code, there would be a lot of components that would be duplicated, typically the above commodity cards, shopping cart components, etc. In fact, in addition to the above you can see these repeats, us into deep thinking, make water, we certainly need to input from the user’s name, card number, so point it out after water is bound to have a form of input information, and the form in the clerk and customer interface is the same as the base, in addition to water form, form, and electricity ticket form, etc., So you can expect a lot of duplicated components.

As an ambitious engineer, this kind of repetitive component can’t be solved by CV, we have to find a way to make these components reusable. How do you reuse components? Bring up a public component library, I believe many friends will think so. We think so too, but there are several ways to organize a common component library, and we mainly consider these:

Separate NPM package

Create a project for these reusable components, such as antD, and publish them to the company’s private NPM repository. Use them like this:

import { Cart } from 'common-components';
Copy the code

However, the components we need to reuse differ from ANTD components in one essential way: we need to reuse business components, not pure UI components. The antdUI component library has no business attributes and is open in style to ensure generality. However, the business components here are not just a few buttons and input boxes, but a complete form, including front-end validation logic, which needs to be reused, so the components I need to reuse are actually strongly bound to the business. Because it is strongly tied to the business, even if I distribute it as a separate NPM package, other projects in the company won’t use it. An NPM package that can’t be shared by other projects always feels a bit out of line.

git submodule

Another option is git submodule. We create a new Git project for these shared components, but instead of publishing to the NPM repository to annoy others, we reference it directly in our main project as a Git submodule. Git submodule can be used in the following ways:

  1. In essencesubmoduleAnd the main project are two different thingsgit repo, so you need to create a set of scaffolding (code specifications, release scripts, etc.) for each project.
  2. submoduleIn fact, the main project saves a dependency link to the subproject, indicating which version of the main project depends on which subproject, you need to use carefullygit submodule updateTo manage this dependency. If not used correctlygit submodule updateAnd messed up the version of the dependency, that is ha ha…
  3. When publishing, you need to carefully handle dependencies by publishing the sub-projects first, and then releasing the main project when the sub-projects are ready.

mono-repo

Mono-repo is an increasingly popular approach to project management, as opposed to multi-repO. A multi-repo is a multi-repository, and the Above Git submodule is a multi-repo approach. The main project and its subprojects are separate Git repositories, thus constituting multiple repositories. Mono-repo is a large repository with multiple projects in a Git repository. Many well-known open source projects are organized by mono-repo, such as Babel, React,Jest, create-react-app, react-Router, etc. Mono-repo is particularly well suited to multiple projects that are closely linked, such as this one, so let’s dive into the subject of this article and take a closer look at Mono-repo.

mono-repo

Mono-repo mono-repo mono-repo Mono-repo Mono-repo Mono-repo Mono-Repo Mono-Repo Mono-Repo Mono-repo: React-router: Mono-repo: React-router: Mono-repo: React-router: Mono-repo: React-router: Mono-repo

We found that he had a package folder with four projects in it:

  1. React-router: a core library for react-router that handles common logic

  2. React-router-config is the react-router configuration processing library

  3. React-router-dom: a library used by browsers that references the react-router core library

  4. React-router-native: supports the react-native routing library and references the react-Router core library

The four projects are all for the route management service of react, and they are closely related to each other. To complete a function, multiple projects may be required. For example, fixing a BUG requires changing the react-router-dom and react-router code at the same time. If they are in different Git repositories, you need to modify, commit, package, test, and then change the version numbers that depend on each other to make them work properly. However, with Mono-repo, because they are all in the same Git repository, we can modify the code of both projects in a single commit, and then package, test, and publish the code in a unified way. If we use lerna management tool, the version number dependency is also updated automatically, which is much more convenient.

lerna

Lerna is the most well-known mono-repo management tool, and today we are going to use it to build the aforementioned shared business component project. Our target project structure looks like this:

Mono-repo-demo / -- Main project, This is a Git repository package.json packages/ common/ -- shared business components package.json admin-site/ -- package.json customer-site/ -- Git repository package.json packages/ common/ -- shared business components package.json admin-site/ -- Package. json customer-site/ -- Client web site project package.jsonCopy the code

lerna init

Lerna initialization is simple: create an empty folder and run:

npx lerna init
Copy the code

Json and lerna.json. This command will create an empty packages folder, a package.json folder and lerna.json.

Json: Private must be set to true because mono-repo’s Git repository itself is not a project. It is a multi-project repository, so it cannot publish directly. It should publish packages/ sub-projects.

"private": true.Copy the code

The lerna.json initialization looks like this:

{
  "packages": [
    "packages/*"]."version": "0.0.0"
}
Copy the code

The packages field marks the location of your subprojects. The default is Packages/folder, which is an array and therefore supports multiple locations. Another area of special attention is the Version field, which has two types of values, a specific version number like 0.0.0 above, and possibly the independent keyword. If the specific version number is 0.0.0, all subprojects managed by Lerna will have the same version number —-0.0.0. If you set it to independent, each subproject can have its own version number, for example, the version number of subproject 1 is 0.0.0, and the version number of subproject 2 is 0.1.0.

Create a subproject

Now that our packages/ directory is empty, we need to create three projects based on our previous assumptions:

  1. common: Shared business components, do not need to run itself, put various components on the line.
  2. admin-site: teller site, need to be able to run, usecreate-react-appCreate!
  3. customer-site: customer site, also need to run, or usecreate-react-appcreate

Create subprojects can be created using lerna’s commands:

lerna create <name>
Copy the code

Lerna create common; lerna create common; lerna create common;

This is the default generated directory structure using lerna Create, where the __test__ folder is the unit test content and the lib folder is the code. Since I was planning to use it for shared components, I changed the directory structure to delete both default generated folders and create a new components folder:

The other two runnable sites are created with create-react-app and run under packages:

npx create-react-app admin-site; npx create-react-app customer-site;
Copy the code

After several projects have been created, the overall project structure looks like this:

Mono-repo’s convention is that it’s best to name these subprojects @< main project name >/< subproject name > so that when someone references you, all your projects can be in the same directory as @< main project name > in node_modules. So we manually change the name of the three subprojects package.json to:

@mono-repo-demo/admin-site
@mono-repo-demo/common
@mono-repo-demo/customer-site
Copy the code

lerna bootstrap

As you can see from the picture above, each of the packages/ subprojects has its own node_modules, and if you turn it on, you’ll find a lot of duplicate dependency packages, which can take up a lot of hard disk space. Lerna provides another powerful feature: to extract all the dependencies of the subproject to the top layer, we need to delete the node_modules of the subproject and run the following command:

lerna bootstrap --hoist
Copy the code

To delete the installed subproject node_modules, you can either do it manually or use this command:

lerna clean
Copy the code

yarn workspace

Lerna bootstrap –hoist hoist the dependencies of subprojects to the top level, but it does it in a rude way: run NPM install on each subproject, and when all dependencies are installed, move them to the top node_modules. This leads to a problem, what if multiple subprojects rely on the same third-party library, but the requirements are in different versions? For example, our three subprojects all rely on ANTD, but their versions are not exactly the same:

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

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

// common
"antd": "4.9.4"
Copy the code

In this example, admin-site and customer site both need ANTD version 3.1.0, while common needs ANTD version 4.9.4. If lerna bootstrap –hoist is used for promotion, Lerna will promote the most used version. So 3.1.0 to the top level, and then delete antDS in node_modules of the subproject. This means that when Common accesses ANTD, it will also get version 3.1.0, which may cause the Common project to not work properly.

Lerna Bootstrap –hoist moves the version that is most used by all the subprojects to the top level. Yarn Workspace checks dependencies and their versions for each subproject. If the version is different, it stays in the child project’s own node_modules, and only identical dependencies are promoted to the top level.

If yarn Workspace is used, admin-site and customer-site versions 3.1.0 will be moved to the top level, while the Common project will keep its own 4.9.4 ANTD, so that each subproject can get its own dependencies.

Yarn 1.0 or later allows workspace to be enabled by default, so we just need to add a configuration to the package.json layer at the top:

/ / top-level package. Json
{
  "workspaces": [
    "packages/*"]}Copy the code

Set npmClient to YARN in lerna.json and set useWorkspaces to true:

// lerna.json
{
  "npmClient": "yarn"."useWorkspaces": true
}
Copy the code

With Yarn Workspace, we don’t need lerna bootstrap to install dependencies. Instead, we need yarn install as before. It will automatically promote dependencies. The YARN install here has the same effect whether it runs at the top level or in any of the subprojects.

Start sub-project

Now we have three subprojects. To start the CRA subproject, we can go to that directory and run YARN Start, but it is too much trouble to switch folders frequently. With lerna’s help, we can run it directly at the top level, using lerna’s function:

lerna run [script]
Copy the code

For example, we run lerna run start at the top level. This is equivalent to executing yarn run start or NPM run start for each subproject, depending on the Settings in lerna.json:

"npmClient": "yarn"    
Copy the code

What if I only want to run commands in one of the subprojects? Just add –scope, for example, to the top-level package.json command:

/ / top-level package. Json
{
  "scripts": {
    "start:aSite": "lerna --scope @mono-repo-demo/admin-site run start"}}Copy the code

So we can run yarn Start :aSite directly at the top level, which will start the administrator site. Lerna run start is the same command as lerna run start, and then add –scope to specify that it will run under the administrator subproject. @mono-repo-demo/admin-site is the name of our admin subproject, which is defined in package.json:

// Admin subproject package.json
{
  "name": "@mono-repo-demo/admin-site"
}
Copy the code

Then let’s actually run yarn start:aSite:

See the familiar CRA circles, so far our configuration is good, ha ha ~

Creating a common component

Now that the basic structure of the project is in place, let’s create a common component to see what happens. Let’s use ANTD to create a form to pay water charges, also very simple, a name input box, a query button.

// 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"
        name="username"
        rules={[
          {
            required: true.message:'Please enter name ',},]} >
        <Input />
      </Form.Item>

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

export default WaterForm;
Copy the code

Introducing common components

To reference the above component, we need to add the dependency to the package.json file of admin-site. We can modify it manually or use lerna:

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

This command has the same effect as if you had changed package.json manually:

Let’s change the default CRA circle of admin-site to this form:

Then run:

Huh? Error reported… Would you believe me if I said I expected this mistake 😜

Shared scaffolding

The JSX syntax is not supported. The last two lines also suggest that we introduce Babel to compile. All of this illustrates the same problem: Babel’s configuration does not work for the Common subproject. This was expected. Our admin-site worked because CRA configured the scaffolding for us, while the common subproject did not configure the scaffolding and could not compile.

All of our React sub-projects can share a set of scaffolding. Therefore, my solution is to eject all the scaffolding of CRA, and then manually move it to the top layer for the three sub-projects to share.

Admin-site = admin-site = admin-site = admin-site = admin-site

yarn eject
Copy the code

This command will pop up the CRA config and scripts folders and add their dependencies to the admin-site package.json. So what we need to do is manually move the config folder and scripts folder to the top layer, and then move the CRA dependencies to package.json to the top layer, and see what CRA changes to package.json with Git. The moved project structure looks like this:

Note that the CRA project startup script is in the scripts folder, so we need to modify the admin-site startup command slightly:

// admin-site package.json

{
  "scripts": "node .. /.. /scripts/start.js",}Copy the code

Now we still get an error using YARN Start :aSite, so we continue to change the Settings for Babel.

Add our packages paths to config/ Paths and export them:

Then modify the webPacka configuration and add this path to babel-loader’s include path:

Now run our project again and it will work:

Finally, don’t forget about our customer-site. This is easy to handle because we have already adjusted the structure of the whole main project. We can delete all the other dependencies of customer-site, just keep @mono-repo-demo/common. Then adjust the startup script:

This allows the client site to also introduce common components and start up.

release

The last thing to note is that when we are done and need to publish, be sure to use Lerna publish, it will automatically update the dependent version number for me. For example, I now modify the water bill form slightly and submit:

Now I’m going to try to publish it and run it

lerna publish
Copy the code

After running, it will let you select a new version number:

Here I select a minor, where the version number changes from 0.0.0 to 0.1.0, and lerna automatically updates the relevant dependent versions, including:

  1. Lerna. json version changed to 0.1.0:

  2. The version number of Common changes to 0.1.0:

  3. The admin-site version number is also changed to 0.1.0, and the update dependency common is 0.1.0:

  4. The change of customer-site is the same as that of admin-site.

independent version

In the above release strategy, we changed the version of Common, and the admin-site version also became the same. This is not necessary. Admin-site only updates the dependent version of Common, and its version may not be a minor upgrade, but a patch. In this case, the admin-site version will change depending on the version configuration in lerna.json. As mentioned earlier, if it is a fixed reference, all subproject versions will be the same, so admin-site version will change with it. If we change it to independent, it will be different.

// lerna.json
{
  "version": "independent"
}
Copy the code

Then I’ll change common and try again:

< lerna publish > common 0.2.0 > admin-site 0.1.1 < lerna publish > admin-site 0.1.1 >

Depending on your project, you can decide which strategy to adopt, whether each subproject version should be consistent or independent.

conclusion

thismono-repoI’ve cleaned up the code and uploaded it to GitHub in case you need onemono-repo + reactClone it directly with the project template:Github.com/dennis-jian…

Let’s review the main points of this article:

  1. It all started when we received a request from a foreigner to pay his utility bill and sell things, both at the teller end and at the customer self-service end.
  2. After analysis, we decided to deploy the teller side and the customer self-service side as two sites.
  3. For these two sites, we created two new projects that scale better.
  4. Both projects have many business components that look the same, and we need to reuse them.
  5. To reuse these business components, we introducedmono-repoFor project management,mono-repoIdeal for multiple projects that are closely related.
  6. mono-repoThe most famous tool islerna.
  7. lernaCan automatically manage dependencies between projects as wellnode_modules.
  8. uselerna bootstrap --hoistCan be subprojectsnode_modulesGet to the top. Solve itnode_modulesRepeated questions.
  9. butlerna bootstrap --hoistIn the process of promotion, if the dependency versions referenced by each subproject are inconsistent, the version that is most used will be promoted, which will cause the minority to fail to find the correct dependency and make errors.
  10. In order to solve the problem of version conflict when promoting, we introducedyarn workspaceHe will also promote the most used version, but will keep his own dependencies for the minoritynode_modulesThe following.
  11. Both CRA projects in our example have their own scaffolding, whilecommonWithout scaffolding, we adjusted the scaffolding and moved it to the top floor so that the three projects could be shared.
  12. Use it at launchlerna publishIt automatically updates internal dependencies and updates each subproject with its own version number.
  13. The version number rule for subprojects can be found inlerna.jsonIf the version number is set to a fixed one, each subproject keeps the same version. If the version number is set toindependentKeyword, each subproject can have its own different version number.

The resources

  1. Lerna official website: lerna.js.org/
  2. Yarn workspace: classic.yarnpkg.com/en/docs/wor…

At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.

Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~

“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…

“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…