10分钟带你从0到1搭建monorepo 工程化项目(demo工程)

前言

自己重新搭建了一套前端项目架构 基于 lerna yarn 的 monrepo的仓库, 主要是后面会学习输出的一些东西, 整个架子先搭建起来。

  1. 2d 和 3d 公共 util 的封装
  2. 个人 npm 包的发布 (rollup)
  3. 2d React 项目 搭建(vite)
  4. 3d react 项目 搭建 (webpack)
  5. 搭建一套基于webpack 5 的cli

每个项目都有一些特定的依赖, 但是也会有一些相同的依赖。比如eslint、 babel 的一些基础配置,或者一些通用的脚本文件。读完本篇文章你可以学到 从0 到 1 搭建 monorepo 前端工程化项目 如下图所示:

0分钟带你从0到1搭建monorepo

项目架构图

为什么使用monorepo

monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略("mono" 来源于希腊语 μόνος 意味单个的,而 "repo",显而易见地,是 repository 的缩写)。将不同的项目的代码放在同一个代码仓库中,这种「把鸡蛋放在同一个篮子里」的做法可能乍看之下有些奇怪,但实际上,这种代码管理方式有很多好处,无论是世界一流的互联网企业 Google,Facebook,还是社区知名的开源项目团队 Babe、都使用了 monorepo 策略管理他们的代码。这是Taro 的官方源码库:

0分钟带你从0到1搭建monorepo

taro

至于他的优点如下:

  1. 代码重用将变得非常容易:由于所有的项目代码都集中于一个代码仓库,我们将很容易抽离出各个项目共用的业务组件或工具,并通过 Typescript,Lerna 或其他工具进行代码内引用;
  2. 赖管理将变得非常简单, 可以轻松的做到版本依赖管理 和版本号自动升级
  3. 发布npm 包 也很特别简单, 提取公共方法 直接公共包,可以快速发布到npm 上
  4. 还有一个 最大的 优点 就是 避免重复安装包, 减少的磁盘空间, 降低了构建时间

这两项好处全部都可以由一个成熟的包管理工具来完成,对前端开发而言,即是 yarn(1.0 以上)或 npm(7.0 以上)通过名为 workspaces 的特性实现的(⚠️ 注意:支持 workspaces 特性的 npm 目前依旧不是 LTS 版本)。

yarn

这里的话 我们全局安装 yarn

npm install yarn -g

然后新建一个文件夹 进入到目录中执行

yarn init  

会在项目的根目录生成 package.json

这时候我们在项目根目录

新建packages 目录

在package.json 新增下面字段 workspace

{  "name": "yarn-test",  "version": "1.0.0",  "private": true,  "workspaces": [    "packages/*"  ],  "main": "index.js",  "license": "MIT"}

表示工作区是packages 下的所有子目录,

private: true 表示项目的根目录 不会被发布出去

假设项目中有foo和bar两个package:

mono-demo/|--package.json|--packages/| |--foo/| | |--package.json| |--bar/| | |--package.json

yarn workspace <workspace_name>

在指定的package中运行指定的命令。

# 在foo中添加react,react-dom作为devDependenciesyarn workspace foo add react react-dom --dev# 移除bar中的lodash依赖yarn workspace bar remove lodash# 运行bar中package.json的 scripts.test 命令yarn workspace bar run test

yarn workspaces run

在所有package中运行指定的命令,若某个package中没有对应的命令则会报错。

# 运行所有package(foo、bar)中package.json的 scripts.build 命令yarn workspaces run build

yarn workspaces info [–json]

查看项目中的workspace依赖树。

例如我的bar依赖了foo,如下:

// bar/package.json{ "name": "bar", "version": "1.0.0", "dependencies": { "foo": "^1.0.0" }}

在项目中的依赖结构是这样的(假设foo/package.json的版本匹配bar的依赖版本,否则会另外安装一个匹配的foo):

/package.json/yarn.lock/node_modules/node_modules/foo -> /packages/foo/packages/foo/package.json/packages/bar/package.json

那么运行yarn workspaces info会得到如下输出:

yarn workspaces { "bar": { "location": "packages/bar", "workspaceDependencies": [ "foo" ], "mismatchedWorkspaceDependencies": [] }, "foo": { "location": "packages/foo", "workspaceDependencies": [], "mismatchedWorkspaceDependencies": [] }}

比如我的一些依赖是所有package 通用的 比如 eslint、babel… 我们就使用下面的这个命令 加一个 -W 就可以了

yarn <add|remove>-W

  • -W: –ignore-workspace-root-check ,允许依赖被安装在workspace的根目录

管理根目录的依赖。

# 安装eslint作为根目录的devDependenciesyarn add eslint -D -W

lerna

**Lerna**是社区主流的monorepo管理工具之一,集成了依赖管理、版本发布管理等功能。

使用Learn管理的项目的目录结构和yarn workspace类似。

我们根目录安装

yarn add lerna -D -W

然后执行

npx lerna init

然后项目中就会生成lerna.json

我们进行下面配置

{ "packages": ["packages/*"], "command": { "run": { "npmClient": "yarn" }, "publish": { "ignoreChanges": ["ignored-file", "*.md"], "message": "chore(release): publish", "registry": "https://npm.pkg.github.com" } }, "version": "independent", "useWorkspaces": true, "npmClient": "yarn"}

这里 同样使用 workspace, 指定项目 使用yarn 进行包管理

这里有一个很重要的字段 "version": "independent",

这是表示使用 独立模式 Lerna 项目允许维护人员彼此独立地增加包版本。每次发布时,您都会收到有关已更改的每个包的提示,以指定它是补丁、次要、主要还是自定义更改。独立模式允许您更具体地更新每个包的版本,并且对一组组件有意义。这里搭配 semantic-release 这个npm包 感兴趣的可以去了解下。

下面我介绍一些lerna 的一些命令: 大家可以去github lerna 看的更多

lerna bootstrap:等同于 lerna link yarn install,用于创建符合链接并安装依赖包;

lerna run:会像执行一个 for 循环一样,在所有子项目中执行 npm script 脚本,并且,它会非常智能的识别依赖关系,并从根依赖开始执行命令;

lerna exec:像 lerna run 一样,会按照依赖顺序执行命令,不同的是,它可以执行任何命令,例如 shell 脚本;

lerna publish:发布代码有变动的 package,因此首先您需要在使用 Lerna 前使用 git commit 命令提交代码,好让 Lerna 有一个 baseline;

lerna add:将本地或远程的包作为依赖添加至当前的 monorepo 仓库中,该命令让 Lerna 可以识别并追踪包之间的依赖关系,因此非常重要

tsconfig

作为一个ts 项目, 在项目根目录安装 ts

yarn add typesript -D -W

首先在项目中生成 tsconfig.json

npx tsc --init

然后在项目根目录生成tsconfig.json 这里 划重点 我们把基础的 tsconfig.json 放在这里 ,然后 新建一个项目 生成tsconfig.json 都是继承根目录的 tsconfig.json 类似于这样

{ "extends": "../../tsconfig.json", "compilerOptions": { "target": "es2018", "module": "ESNext", "outDir": "./dist" }, "include": ["./src/**/*.ts"] // * 匹配0或者多个字符 (不包括目录分割符) **/递归匹配任意子目录}

这是一个子项目的tsconfig.json

至于tsconfig.json 文件 详细配置,你可以自己百度。

Babel

Babel 配置文件合并的方式与 TypeScript 如出一辙,甚至更加简单,我们只需在子项目中的 .babelrc 文件中这样声明即可:

{  "extends": "../.babelrc"}

scripts

我们在全局新建一个scripts 文件夹 可能 是 shell 文件 也有可能是ts 文件。我们都知道 ts 文件 是不能直接执行, 都是先编译成js 然后再执行, 这也太麻烦了吧。好在社区已经提供的 ts-node 可以允许你直接运行ts 文件 这东西实现的原理 大概就是

我们可以使用 ts-node 某个 ts 文件,来直接执行这个 ts 文件,它的原理就是修改了 require hook,也就是 Module._extensions['.ts'] 来实现的。

在 require hook 里面做 ts 的编译,然后后面直接执行编译后的 js,这样就能达到直接执行 ts 文件的效果。

所以我们重写 Module._extensions['.ts'] 方法,在里面读取文件内容,然后调用 ts.transpileModule 来把 ts 转成 js,之后调用 Module._compile 来处理编译后的 js。

yarn add ts-node -D -W

新建一个 test.ts 文件

const foo = {  baz: {    a: 1,  },}console.log(foo)

然后 在package.json

编写如下脚本:

 "test": "ts-node ./scripts/test.ts "

然后执行 yarn test

0分钟带你从0到1搭建monorepo

res

其实这个ts-node 有一个坑 就是 文件引用问题 当你的 ts 脚本文件 引用了当前项目的其他包 可能就会出现 执行报错

我们在package 新建一个util 然后 新建了一个 index.ts 文件

const add = (a: number, b: number) => a bexport default add

然后我在根目录的 tsconfig.json 进行别名配置

"baseUrl": "./packages", // 根路径 路径映射, "paths": { "@fly/util": ["./util/index.ts"] }

我们在 脚本 引入 这个加法 函数

import add from '@fly/util'console.error(add(2, 3))

然后继续执行

会报下面这个错误

0分钟带你从0到1搭建monorepo

error

大家注意看 我画框的地方,大体就是 由于 我们 ts-node 在执行的过程中, 由于tsconfig.json 的 "module": "CommonJS", 会将

import add from '@fly/util' 编译成

const add = require("@fly/util")

由于我们这个是ts 别名配置 当然找不到 这个模块, 如果你是webpack 项目的话 ,可以 配置别名 解决, 会进行路径替换,

但是我们在写脚手架的时候,不可能用到webpack 就是 node 环境, 这里我怎么去解决呢

社区也提供了解决方案

0分钟带你从0到1搭建monorepo

ts-config

大概意思就是:使用它来加载位置在 tsconfig.json 的路径部分中指定的模块。支持在运行时加载和通过 API 加载。

Typescript 默认模仿模块的 Node.js 运行时解析策略。但它也允许使用路径映射,允许指定任意模块路径(不以“/”或“.”开头)并将其映射到文件系统中的物理路径。typescript 编译器可以从 tsconfig 解析这些路径,因此它可以编译。但是,如果您尝试使用 node(或 ts-node)执行编译后的文件,它只会在 node_modules 文件夹中一直查找到文件系统的根目录,因此不会找到 tsconfig 中路径指定的模块 这句话 很重要, 所以我们刚才的报错,就是这个原因 而这个库 就是帮我们解决的。

yarn add tsconfig-paths -D -W

如何使用呢

ts-node --project customLocation/tsconfig.json -r tsconfig-paths/register "test/**/*.ts"

这里的话最好ts config.json 的 common js 因为我们是在node 环境

所以我在项目新建一个tsconfigs 用来 存放不同的 ts配置 同样继承 根目录

{  "extends": "../tsconfig",  "compilerOptions": {    "module": "CommonJS"  }}

执行命令

ts-node --project ./tsconfigs/cmj.json -r tsconfig-paths/register  ./scripts/test.ts 

0分钟带你从0到1搭建monorepo

res

执行成功 很舒服哇。看到这里觉得有帮助的话 可以帮忙点个赞吧

但是这里还会有个问题 如图:

0分钟带你从0到1搭建monorepo

eslint

这其实是eslint import 的配置 ,如果你配置了的话 安装下面这个npm

yarn  add eslint-import-resolver-typescript -D -W

光从名字就可以看出和这个问题极为相关。从项目 README 可以发现,这个 lib 可以在 TypeScript 项目使 eslint-plugin-import 找到正确的 .ts 和 .tsx 文件,也能识别 tsconfig.json 的 path 配置(路径别名 2),甚至 monorepo 这类一个 git 仓库多个项目的工程也支持。

用法也很简单在 eslint 的"import/resolver":指向当前配置了 path 的 tsconfig 的路径即可,eslint 就会自动识别就不会报错了。

{ "plugins": ["import"], "rules": { "import/no-unresolved": "error" }, "settings": { "import/parsers": { // 使用 TypeScript parser "@typescript-eslint/parser": [".ts", ".tsx"] }, "import/resolver": { // 默认使用根目录 tsconfig.json "typescript": { // 从 <roo/>@types 读取类型定义 "alwaysTryTypes": true, }, // 使用指定路径 tsconfig.json, <root>/path/to/folder/tsconfig.json "typescript": { "directory": "./path/to/folder" }, // monorepos 这类多 tsconfig.json // 可以用 glob 这类匹配模式 "typescript": { "directory": "./packages/*/tsconfig.json" }, // 或者数组 "typescript": { "directory": [ "./packages/module-a/tsconfig.json", "./packages/module-b/tsconfig.json" ] }, // 也可以混合使用 "typescript": { "directory": [ "./packages/*/tsconfig.json", "./other-packages/*/tsconfig.json" ] } } }}

上面就是官方的用法, 下面我们就开始 eslint 详细用法吧

eslint

对于eslint 的配置其实我们可以如法炮制,我们项目的根目录 新建 .eslintrc 然后我们每个子项目 同样继承外部的 .eslintrc

好的下面我们开始安装 eslint

yarn add eslint -D -W

然后我们生成eslint 的配置文件

npx eslint --init

由于我们的项目 是基于 React 和 ts 的 所以在选择的按照下面进行选择

0分钟带你从0到1搭建monorepo

eslint-config

这时候在我们的根目录 就会出现**.eslint.yml** 这个配置文件 这里你也可以选择你喜欢的配置文件格式, 个人比较喜欢YAML 这种方式

env: browser: true node: trueextends: - plugin:react/recommended - eslint:recommended - airbnbparser: '@typescript-eslint/parser'parserOptions: ecmaFeatures: JSX: true ecmaVersion: 2020 sourceType: moduleplugins: - react - '@typescript-eslint' - react-hooksrules:

然后就会出现下面这个文件我来一一解读下面每一条的配置文件

env

  1. 表示你在 eslint 想启用的环境 我们是前端嘛 所以 就是 node 和 browser,官网支持的还是比较多的extends单从字面上去理解就是 继承 其他的配置 可以是 文件路径 形式的 或者是 下载的插件包的 这里的话 一般npm包的 格式是下面这样子的eslint-config-packagename
    我们在配置的时候 前面的 eslint-config 可以省略, 我这使用的是airbnb 的配置。或者安装的包的名字 是这样子eslint-plugin-packagename
    然后就可以 eslint-plugin 可以省略,然后就可以像下面使用plugin:xxxx/recommended
    或者 是子目录下继承根目录的 eslint 例如如下:extends: ../../.eslintrc
    当一切准备就绪后,我们的项目目录应该大致呈如下所示的结构:.

  1. ├── package.json
    ├── .eslintrc
    └── packages/
    │ ├── tsconfig.json
    │ ├── .babelrc
    ├── project_1/
    │ ├── index.js
    │ ├── .eslintrc
    │ ├── .babelrc
    │ ├── tsconfig.json
    │ └── package.json
    └───project_2/
    ├── index.js
    ├── .eslintrc
    ├── .babelrc
    ├── tsconfig.json
    └── package.json

    parserESLint 默认使用Espree作为其解析器, 但是由于我们项目是 ts 文件, 所以安装yarn add @typescript-eslint/parser
    作用 就是将 TypeScript 转换成与 estree 兼容的形式,以便在ESLint中使用。eslint 也是对AST 进行操作,但是对TS 是不支持的,所以做一层转换成 js.parserOption有了解析器肯定就有解析参数 ecmaFeatures:
    jsx: true // 表示支持jsx 语法 但是React 对 ESLint 无法识别的JSX语法应用特定的语义。如果你正在使用 React 并且想要 React 语义支持,我们建议你用 eslint-plugin-react。
    ecmaVersion: 2020 // 默认的js 的版本
    sourceType: module // esm 模式
    plugins正如上面所说,在解析参数配置了支持jsx 语法, 但是 ESLint 无法识别的JSX语法应用特定的语义。如果你正在使用 React 并且想要 React 语义支持,我们建议你用 eslint-plugin-react。这就是所谓的插件 ,这里安装了 react 和 react-hooks 两个插件yarn add eslint-plugin-react eslint-plugin-react-hooks
    然后我们就可以 plugins 进行配置了 同样可以省略 eslint-plugin

rules

rules 的定义我们一般看到的就是这样子, 具体的配置可以自己查官方列表

---rules: eqeqeq: off curly: error quotes: - error - double

然后你如果安装了插件

就可以插件名/ 规则 可以对默认的插件配置 进行重写, 比如下面这样子

'react-hooks/rules-of-hooks': 2'react-hooks/exhaustive-deps': 2

与VScode 集成

 "editor.codeActionsOnSave": {        "source.fixAll.eslint": true  }

prettier

prettier 的出现其实为了解决代码格式的问题?这是eslint 无法做到的

Prettier中文的意思是漂亮的、美丽的,是一个流行的代码格式化的工具,我们来看如何结合ESLint来使用。首先我们需要安装三个依赖:

安装 npm 包

yarn add prettier eslint-config-prettier eslint-plugin-prettier  -D -W

  1. prettier:prettier插件的核心代码
  2. eslint-config-prettier:解决ESLint中的样式规范和prettier中样式规范的冲突,以prettier的样式规范为准,使ESLint中的样式规范自动失效
  3. eslint-plugin-prettier:将prettier作为ESLint规范来使用

然后在项目的根目录下创建.prettierrc文件:

{ "printWidth": 120, "semi": false, "trailingComma": "all", "singleQuote": true, "arrowParens": "always"}

接着修改.eslintrc.js文件,引入prettier:

extends: - plugin:react/recommended # - plugin:import/recommended - eslint:recommended - airbnb - prettier plugins: - react - '@typescript-eslint' - react-hooks # - import - prettier

分别在extends 和 plugins 加入 prettier

husky和lint-staged构建代码工作流

在这之前我先简单介绍 下Husky 和 lint-staged 这两个东西 到底是干什么的??

husky

husky目前是前端工程化必备的一个工具了, husky 这个 npm 包 说白了就是在git 提交前 提供一些钩子 hooks 方便你运行一些脚本命令。下面跟着我可以实操一下

yarn add husky -D -W

第二步:在packgae.json中添加prepare脚本

{  "scripts": {    "prepare": "husky install"  }}

prepare脚本会在npm install(不带参数)之后自动执行。也就是说当我们执行npm install安装完项目依赖后会执行 husky install命令,该命令会创建.husky/目录并指定该目录为git hooks所在的目录。

第三步 添加 git hooks

npx husky add .husky/pre-commit "yarn lint-staged"

这时候在我们根目录 就会出现 .husky 的目录

0分钟带你从0到1搭建monorepo

husky

然后写入 pre-commit 脚本

#!/bin/sh. "$(dirname "$0")/_/husky.sh"yarn lint-staged

由于我们还没有装 lint-staged , 直接运行会报错

lint-staged

yarn add lint-staged -D -W

Lint-staged 用于对 Git 暂存区中的文件执行代码检测, 然后我们同样也可以做一些操作

我们在package.json 增加如下配置

 "lint-staged": {    "*.@(js|ts|tsx)": [      "eslint --ext .ts,.tsx,.js --fix",      "prettier --write",      "git add"    ],    "*.@(yml|yaml)": [      "prettier --parser yaml --write"    ],    "*.md": [      "prettier --parser markdown --write"    ],    "*.json": [      "prettier --parser json --write"    ]  },

我们看下第一个,当匹配到 以 js 或者 ts 或者是tsx 结尾的文件时候, 这时候我们就可以 结合 之前 的eslint 和 prettirer 做一些操作

  1. eslint 对这些文件 检测 并自动修复
  2. 然后 使用 prettier — write 进行自动检测
  3. 最后添加到 暂存区中

我们run 一下 看下效果:

0分钟带你从0到1搭建monorepo

husky 录屏

你会发现好像已经成功了, 但是最后 还是失败了,因为我在husky 还加了一个hook 用于在提交命令前 规范 commit-msg

我们输入下面命令

npx husky add .husky/commit-msg 'yarn commitlint --edit "$1"' 

然后在.husky 的目录 就会出现下面这张图

0分钟带你从0到1搭建monorepo

commit

由于运行了这个脚本 commitlint 这个脚本 但是我们 安装 老样子

yarn  add commitlint  @commitlint/config-conventional  @commitlint/config-lerna-scopes -D -W

**@commitlint/config-conventional ** 这里会使用默认angular 团队的提交规范

@commitlint/config-lerna-scopes 由于我们是lerna 多个packages 主要是用来限制在packages 里的包 不在的 就会报错,

packages├── api├── app└── web❯ echo "build(api): change something in api's build" | commitlint⧗   input: build(api): change something in api's build✔   found 0 problems, 0 warnings❯ echo "test(foo): this won't pass" | commitlint⧗   input: test(foo): this won't pass✖   scope must be one of [api, app, web] [scope-enum]✖   found 1 problems, 0 warnings

foo 不属于项目中的一个包, 所以 直接 会报错。

在项目的根目录 新建:commitlint.config.js

module.exports = {  extends: ['@commitlint/config-conventional', '@commitlint/config-lerna-scopes'],}

这样我们提交的时候就会做到我们上面的限制, 项目代码的提交规范 还是很重要的

生成changelog

首先,聊一下什么是 CHANGELOG ,为什么需要 CHANGELOG ?它记录你项目所有的commit信息并归类版本,可以快速跳转到该条commit记录,甚至可以显示修改人信息一眼发现bug的创建者。它能让你方便知道项目里哪个版本做了哪些功能有哪些bug等信息。也方便排查bug,对于提交记录一目了然,不用一个一个去翻去查。

这里我们安装 这个包

yarn  add  standard-version -D -W

这里,就直接用 standard-version 来实现自动生成 CHANGELOG 了。conventional-changelog 咱们就不聊了,毕竟是它推荐咱们用 standard-version (这都是同一个团队的东西,基于conventional-changelog实现的)。

semantic-release 还有这个 进行生成

至于这两个的区别??我们看下

semantic-release 自动化整个包发布工作流程,包括:确定下一个版本号、生成发布说明和发布包。

虽然两者都基于结构化提交消息的相同基础,但标准版本采用不同的方法,为您处理版本控制、更改日志生成和 git 标记,而无需自动推送(到 GitHub)或发布(到 npm 注册表)。使用标准版本只会影响您的本地 git 存储库 – 它根本不会影响远程资源。运行标准版本后,您可以查看发布状态、纠正错误并遵循对您的代码库最有意义的发布策略。我们认为它们都是很棒的工具,如果对他们的用例有意义,我们鼓励人们使用语义发布而不是标准版本

然后我们在package.json 新增这脚本

"release": "standard-version",

为了 让我们的changeLog 看起来好看一点在项目根目录 新增 .versionrc.js

module.exports = { types: [ { type: 'feat', section: '✨ Features | 新功能' }, { type: 'fix', section: ' Bug Fixes | Bug 修复' }, { type: 'init', section: ' Init | 初始化' }, { type: 'docs', section: '✏️ Documentation | 文档' }, { type: 'style', section: ' Styles | 风格' }, { type: 'refactor', section: '♻️ Code Refactoring | 代码重构' }, { type: 'perf', section: '⚡ Performance Improvements | 性能优化' }, { type: 'test', section: '✅ Tests | 测试' }, { type: 'revert', section: '⏪ Revert | 回退' }, { type: 'build', section: '‍ Build System | 打包构建' }, { type: 'chore', section: ' Chore | 构建/工程依赖/工具' }, { type: 'ci', section: ' Continuous Integration | CI 配置' }, ],}

然后在husky 新增 pre-push 脚本

npx husky add .husky/pre-commit "yarn release"

这样我们在 git push 提交代码的时候 就会自动生成changelog

0分钟带你从0到1搭建monorepo

changeLog

参考

  1. https://www.zhihu.com/question/318476028/answer/1895685159
  2. https://juejin.cn/post/7036688014206042143
  3. https://juejin.cn/post/6868472838613893127

最后

很感谢你能够看完,如果觉得Fly哥写的不错的话,可以点个赞鼓励一下,项目源码 目前在github

https://github.com/wzf1997/fly 已经开源, 感兴趣的可以学习一下

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。