用yarn workspace + lerna实现一个monorepo仓库

77 阅读5分钟

monorepo是一种仓库管理策略,一个monorepo仓库中包含多个项目,各项目间可能存在依赖关系。

monorepo与polyrepo优劣对比

注:polyrepo指一个团队内多个有关联的项目各自有独立的仓库。

polyrepomonorepo
依赖管理存在重复依赖,每个项目需要安装各自的依赖,可能安装相同的依赖,占用存储空间较大。可进行依赖提升,多个项目依赖的包可提升至顶层node_module,减少占用存储空间。
仓库权限控制灵活管控,每个项目可分别视情况对开发者授权。none or all,以仓库维度为开发者授权,一授权即对所有项目授权。
代码共享困难,需要为共享代码单独创建一个仓库,并将其作为npm包发布供其他项目依赖,意味着你需要为其进行开发、CI的相关配置。多个项目对共享代码仓库不一致的版本依赖也可能会带来一些兼容问题。方便,多个项目的代码在一个仓库维护,易于抽离出公共函数、组件等。
重复代码,由于项目间代码共享过于繁杂,项目团队可能选择直接在各自的项目中自行实现,造成大量重复代码。,方便地代码共享以减少重复代码的产生。
工具、规范统一性不统一,每个仓库有各自的测试、运行、构建命令,不同的eslint规则配置等。降低了开发人员的开发体验,且不利于形成统一的团队风格。统一,在仓库根目录配置风格统一的命令、eslint规则等。在对仓库进行了相关配置后,可以快速地创建一个新项目,而无需过多配置。

用yarn workspace + lerna实现一个monorepo仓库

本节将介绍从0到1搭建一个monorepo前端仓库的过程,涉及的工具有yarn、lerna、eslint、vue-cli-servce等

创建&初始化git仓库

在github创建一个你的仓库,clone下来,或本地创建一个git仓库push到远程,本地进入到项目根目录。

安装yarn&开启workspace

  • 安装yarn
npm install yarn -g
  • 新建packages文件夹
mkdir packages
  • 配置package.json
{
    ...,
    "private": true, // 重要,防止发布整个仓库
    "workspaces": [
        "packages/*"
    ],
    ...
}

安装&配置lerna

yarn add lerna --dev -W && yarn lerna init

创建项目

手动创建项目

用vue-cli-service创建一个vue项目

  • 安装vue-cli-service
yarn global add vue-cli-service
  • 创建一个vue项目my-app,用于开发应用
cd packages && vue create my-app
  • 修改packages/my-app/package.json的name属性
{
	"name": "@mono/my-app", // workspace的名字
	...
}
  • 创建一个vue项目my-components,用于开发组件
cd packages && vue create my-components
  • 修改packages/my-components/package.json的name属性
{
	"name": "@mono/my-components", // workspace的名字
	...
}

用lerna创建项目

除了在packages目录下手动创建项目外,由于已经安装了lerna,也可以用lerna来创建一个项目。这里暂不使用这个方法。

yarn lerna create <project name>

配置eslint

  • 安装eslint、typescript及相关依赖
yarn add eslint typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser--dev -W
  • 配置tsconfig.json
{
    "compilerOptions": {
        "incremental": true,
        "outDir": "./dist",
        "baseUrl": ".",
        "moduleResolution": "Node",
        "module": "CommonJS",
        "target": "ES2018",
        "sourceMap": true,
        "lib": [
            "esnext"
        ],
        "esModuleInterop": true,
        "alwaysStrict": true,
        "strictNullChecks": true,
        "noImplicitAny": true,
        "noImplicitReturns": true,
        "noImplicitThis": true,
        "noUnusedLocals": true,
        "resolveJsonModule": true,
        "paths": {
            "@mono/*": [
                "packages/*/src/main.js"
            ]
        },
        "allowJs": false
    },
    "exclude": ["node_modules", "dist", "*.d.ts"]
}
  • 配置.eslintrc.js文件
module.exports = {
	parser: "@typescript-eslint/parser",
	parserOptions: {
		project: "./tsconfig.json",
		tsconfigRootDir: __dirname,
	},
	overrides: [{
		"files": ["*.vue"], // vue文件使用vue parser
		"parser": "vue-eslint-parser"
	}],
	extends: [
		"eslint:recommended",
		"plugin:@typescript-eslint/recommended",
	],
	plugins: ["@typescript-eslint"],
	env: {
		es6: true,
		node: true,
	},
	rules: {
		// 启用额外规则
		indent: ["error", 2],
		"linebreak-style": ["error", "unix"],
		quotes: ["error", "double"],
		semi: ["error", "always"],
		// override configuration set by extending "eslint:recommended"
		"no-empty": "warn",
		"no-cond-assign": ["error", "always"],
		"quote-props": ["error", "as-needed", { unnecessary: true }],
		// 禁用基础配置汇总的规则
		"for-direction": "off",
	},
	ignorePatterns: ["dist", "node_modules", ".eslintrc.js"],
};
  • 在package.json文件中配置eslint脚本
{
	...,
	"scripts": {
		...,
		"lint": "eslint ."
	},
	...
}
  • 删除packages/my-app/package.json和packages/my-components/package.json中的eslintConfig(统一使用顶层.eslintrc.js的配置)。
  • 如果你需要对某个项目进行个性化的eslint规则配置,保留eslintConfig并进行相关配置即可,注意将eslintConfig中的root属性置为false。

在项目(workspaca)间协同

我们将在@mono/my-app中使用@mono/my-components中导出的组件

  • 在package.json中配置运行@mono/my-app、@mono/my-components项目的命令
{
	...,
	"scripts": {
		"serve:app": "yarn workspace @mono/my-app run serve",
		"build:app": "yarn workspace @mono/my-app run build",
		"serve:comp": "yarn workspace @mono/my-components run serve",
		"build:comp": "yarn workspace @mono/my-components run build",
		"lint": "eslint ."
	}
}
  • 将packages/my-components/src/components/HelloWorld.vue的template改为以下内容(后续在@mono/my-app项目中使用时方便辨别)
<template>
	<div class="hello">
		<h1>My Component HelloWorld</h1>
	</div>
</template>
  • 从packages/my-components/src/main.js中导出HelloWorld组件
import MonoHelloWorld from "./components/HelloWorld.vue";

export {
	MonoHelloWorld,
};
  • 修改packages/my-components/package.json,添加main属性配置
{
	...,
	"main": "src/main.js"
	...
}
  • 命令行运行以下脚本,为@mono/my-app项目添加@mono/my-components相关依赖,注意版本需要与packages/my-components/package.json中的匹配
 yarn workspace @mono/my-app add @mono/my-components@0.1.0
  • 在packages/my-app/src/App.vue中引入@mono/my-components的组件
<template>
	...
	<MonoHelloWorld />
</template>

<script>
...
import { MonoHelloWorld } from "@mono/my-components";

export default {
    name: "App",
    components: {
        HelloWorld,
        MonoHelloWorld,
    },
}
</script>
  • 在命令行运行以下命令,启动@mono/my-app本地服务,浏览器打开http://localhost:8080,即可看到已经成功引用了@mono/my-components组件库中的HelloWorld组件
yarn serve:app

image.png

思考

yarn和lerna两者的分工是什么?

yarn主要负责包依赖管理,lerna负责版本管理和git、npm包发布等。

为什么用yarn workspace而不是用lerna的依赖管理功能?

lerna虽然具备使用符号链接实现monorepo(lerna add)的功能。但lerna的monorepo功能是monorepo初流行时的产物,目前lerna在v7及之后的版本已经不再维护依赖管理的相关命令了,而是将相关的功能交给搞管理工具去做(npm、yarn、pnpm)。参考官方解释:Legacy Package Management

参考

monorepo.tools/