阅读 767

前端工程化:如何使用monorepo进行多项目的高效管理

前言

假设我们有4个项目

  1. electron:使用Electron创建的一个桌面端项目,UI和功能大部分与web一致

  2. web:使用React创建的一个web项目

  3. service:使用Nest.js创建的一个后端服务,负责给webelectron提供bff支持

  4. ssr:使用Next.js 创建的一个后端SSR服务,负责给webelectron提供ssr支持

我们的任务就是需要开发或者维护这4个项目,这4个项目里会用到一些可复用的代码package如下

类别描述
common常量定义、hooks、utils等
controls原子组件,负责ui
icons图标
openapiaxios,根据swagger的json生成ts代码
components业务组件
apps由业务组件组成的路由组件

如果分拆成多个项目多个仓库,这些可复用的代码

  1. 要么就是被开发人员来回复制粘贴

  2. 要么就是找多个仓库单独维护多个package,然后publish到npm仓库或者本地开多个项目然后npm link

  3. 要么就是每个仓库里单独写一遍这种本可以抽出复用的逻辑

那么,有没有更好的方法可以提升我们团队的开发质量和发布质量呢?以下是我们的诉求

  1. 将项目和可复用package同处一个仓库

  2. 在项目walk后,改动package的代码,项目可热更新

  3. 如有需要,在部署时,可发布package的代码,供除该monorepo之外的仓库使用

monorepo

monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略

比如ReactVscodeBabel (如下图)都使用了 monorepo 管理他们的代码。

企业微信截图_3d6be9dd-5047-405d-8b03-e7c2bc883d67.png

目前有挺多方式可以搭建monorepo

  1. yarn workspaces:Yarn提供的monorepo的依赖管理机制

  2. lerna:一个开源的管理工具,用于管理包含多个软件包(package)的JavaScript 项目

我们使用lerna来初始化项目

官网文档:Lerna · 是一个管理工具,用于管理包含多个软件包(package)的 JavaScript 项目 | Lerna 中文文档 (lernajs.cn)

github:github.com/lerna/lerna

安装

# 首先使用 npm 将 Lerna 安装到全局环境中: # 推荐使用 Lerna 2.x 版本。 npm install --global lerna  # 接下来,我们将创建一个新的 git 代码仓库: git init monorepo && cd monorepo  # 现在,我们将上述仓库转变为一个 Lerna 仓库: lerna init 复制代码

你的代码仓库目前应该是如下结构

企业微信截图_64219c06-b6e0-46bc-815c-7a2d5613e1af.png

目录改造

  1. 新建applications 文件夹,内容为具体的端项目

  2. packages 文件夹内新建多个子package,每一个package对应我们前言提到的可复用的代码package

  3. 更改每个package以及applicationspackage.json里的name

  4. lerna.json的packages里增加"applications/*"

package

类别name描述
common@monorepo/common常量定义、hooks、utils等
controls@monorepo/controls原子组件,负责ui
icons@monorepo/icons图标
openapi@monorepo/openapiaxios,根据swagger的json生成ts代码
components@monorepo/components业务组件
apps@monorepo/apps由业务组件组成的路由组件

applications

类别name描述
electron@monorepo/electronelectron项目,ui和功能与web基本一致
web@monorepo/webweb项目
service@monorepo/service一个后端服务,负责给web和electron提供bff支持
ssr@monorepo/ssr一个后端SSR服务,负责给web和electron提供ssr支持

lerna.json

{   "packages": [     "packages/*",     "applications/*"   ],   "version": "0.0.0" } 复制代码

经过目录改造后,你的代码仓库目前应该是如下结构

image.png

tsconfig

使用tsconfig里的paths帮助我们做模块解析

根目录

{   "compilerOptions": {     "allowSyntheticDefaultImports": true,     "baseUrl": ".",     "downlevelIteration": true,     "esModuleInterop": true,     "experimentalDecorators": true,     "forceConsistentCasingInFileNames": true,     "importHelpers": true,     "isolatedModules": true,     "jsx": "react",     "module": "commonjs",     "moduleResolution": "node",     "newLine": "lf",     "noImplicitAny": true,     "noImplicitThis": true,     "noImplicitUseStrict": true,     "noUnusedLocals": true,     "noUnusedParameters": true,     "paths": {       "@monorepo/apps": ["./packages/apps/src"],       "@monorepo/apps/lib/*": ["./packages/apps/src/*"],       "@monorepo/apps/es/*": ["./packages/apps/src/*"],       "@monorepo/common/lib/*": ["./packages/common/src/*"],       "@monorepo/common/es/*": ["./packages/common/src/*"],       "@monorepo/components": ["./packages/components/src"],       "@monorepo/components/lib/*": ["./packages/components/src/*"],       "@monorepo/components/es/*": ["./packages/components/src/*"],       "@monorepo/controls": ["./packages/controls/src"],       "@monorepo/controls/lib/*": ["./packages/controls/src/*"],       "@monorepo/controls/es/*": ["./packages/controls/src/*"],       "@monorepo/icons": ["./packages/icons/src"],       "@monorepo/icons/lib/*": ["./packages/icons/src/*"],       "@monorepo/icons/es/*": ["./packages/icons/src/*"],       "@monorepo/openapi": ["./packages/openapi/src"],       "@monorepo/openapi/dist/lib/*": ["./packages/openapi/src/*"]     },     "pretty": true,     "resolveJsonModule": true,     "skipLibCheck": true,     "sourceMap": true,     "strictFunctionTypes": true,     "strictNullChecks": true,     "strictPropertyInitialization": true,     "target": "es5"   } } 复制代码

package/applications

{     "extends": "../../tsconfig.json", // 按具体项目路径决定     "compilerOptions": {         "lib": ["dom", "dom.iterable", "esnext"]     } } 复制代码

package打包

package里的代码更类似于一个库,而非一个应用。因此不需要用webpack那么重的打包器,可以使用gulp或者rollup来打包,可以按需按以下格式进行输出

  1. cjs

  2. esm

  3. umd

使用gulp或者rollup也是因人而异的,比如这个package不需对外发布,则可以使用gulp;如果这个package需要对外发布,不论是发布到npm还是发布一个sdk包,那么都可以使用rollup来打包

以下是一个rollup的简单配置

import typescript from "rollup-plugin-typescript2"; import common from "rollup-plugin-commonjs"; import NodePath from "path"; import autoprefixer from "autoprefixer"; import url from "rollup-plugin-url"; import RollupJson from "@rollup/plugin-json"; import RollupUrl from "@rollup/plugin-url"; import RollupBabel from "@rollup/plugin-babel"; import RollPostcss from "rollup-plugin-postcss"; import RollProgress from "rollup-plugin-progress"; import peerDepsExternal from "rollup-plugin-peer-deps-external"; import pkg from "./package.json"; console.info("EXPECTED EXTERNALS", [...Object.keys(pkg.peerDependencies || {})]); const rollBabelConfig = {     babelHelpers: "runtime",     exclude: "node_modules/**", }; const rollPostcssConfig = {     inject: true,     minimize: true,     modules: true,     plugins: [         autoprefixer({             remove: false,         }),     ], }; export default {     input: "./src/index.ts",     output: [         {             format: "cjs",             dir: "lib",             sourcemap: true,             preserveModules: true,             preserveModulesRoot: "src",         },         {             dir: "es",             format: "esm",             sourcemap: true,             preserveModules: true,             preserveModulesRoot: "src",         },     ],     declaration: true,     external: [...Object.keys(pkg.peerDependencies || {})],     plugins: [         peerDepsExternal(),         RollPostcss(rollPostcssConfig),         url({             url: "inline",             limit: 1000,             emitFiles: true,         }),         RollupUrl({             fileName: "[dirname][hash][extname]",             sourceDir: NodePath.join(__dirname, ".."),         }),         typescript(),         RollupBabel(rollBabelConfig),         common({             include: /\/node_modules\//,         }),         RollupJson(),         RollProgress(),     ], }; 复制代码

以下是一个gulp的配置

import { src, dest, parallel } from "gulp"; function copyAssets(toDir: string) {     return function copyAssets() {         return src(["src/**/*.less", "src/**/*.png", "src/**/*.gif"]).pipe(dest(toDir));     }; } export default parallel(copyAssets("lib"), copyAssets("es")); 复制代码

在项目安装package

applications是我们具体的项目,package是可复用的软件包

这里需要使用lerna add命令帮助我们在项目中安装package

leran add <PackageName>:相当于 npm install 某个依赖, 默认所有包同时安装依赖, 也可以接收一个参数 --scope=PackageName, 可以只针对该包安装对应依赖将本地或远程 package 作为依赖项添加到当前 Lerna 存储库中的软件包。和yarn addnpm install不同,一次只能添加一个软件包,使用方法如下

lerna add <package>[@version] [--dev] [--exact] [--peer] 复制代码

因此我们使用lerna add命令来将@monorepo/common安装给@monorepo/electron@monorepo/web

lerna add @monorepo/common --scope=@monorepo/electron --scope=@monorepo/web 复制代码

image.png

可以看到electronweb项目里,都成功安装了@monorepo/common

image.png 但是,这2个项目明明都用到了同一个依赖,为什么要在2个项目里都单独安装一次呢,想要解决这个问题,需要使用yarn workspaces配合lerna

yarn workspaces

yarn workspaces可以帮助我们便利的享受一条 yarn 命令安装或者升级所有依赖,可以使多个项目共享同一个 node_modules 目录

在根目录的packages.json里进行配置

{   "name": "root",   "private": true, + "workspaces": [ +   "applications/*", +   "packages/*" + ],   "devDependencies": {     "lerna": "^4.0.0"   } } 复制代码

可以看到yarn workspaces帮助我们做了整合

image.png

webpack配置

我们需要使用webpack帮助我们实现修改package的代码,项目热更新,我们在@monorepo/web按以下配置更改

以下为create-react-app 创建的项目

  1. 创建alias.js

  2. 创建modules.js

  3. 创建paths.js

alias.js

在webpack.config.js中,通过设置resolve属性可以配置查找“commonJS/AMD模块”的基路径,也可以设置搜索的模块后缀名,还可以设置别名alias

设置别名可以让后续引用的地方减少路径的复杂度

function getAlias() {     if (process.env.NODE_ENV === "development") {         return {             "@monorepo/apps/lib": "@monorepo/apps/src",             "@monorepo/apps/es": "@monorepo/apps/src",             "@monorepo/apps": "@monorepo/apps/src",             "@monorepo/common/lib": "@monorepo/common/src",             "@monorepo/common/es": "@monorepo/common/src",             "@monorepo/components/lib": "@monorepo/components/src",             "@monorepo/components/es": "@monorepo/components/src",             "@monorepo/components": "@monorepo/components/src",             "@monorepo/controls/lib": "@monorepo/controls/src",             "@monorepo/controls/es": "@monorepo/controls/src",             "@monorepo/controls": "@monorepo/controls/src",             "@monorepo/icons/lib": "@monorepo/icons/src",             "@monorepo/icons/es": "@monorepo/icons/src",             "@monorepo/icons": "@monorepo/icons/src",             "@monorepo/openapi/dist/lib": "@monorepo/openapi/src",             "@monorepo/openapi": "@monorepo/openapi/src"         };     }     return {}; } module.exports = getAlias(); 复制代码

modules.js

在modules.js中将alias.js设置的对象进行格式化后,配置在getModules return的webpackAliases值里

const alias = require("./alias"); ... function getWebpackAliases(options = {}) {     const baseUrl = options.baseUrl;     if (!baseUrl) {         return alias;     }     const baseUrlResolved = path.resolve(paths.appPath, baseUrl);     if (path.relative(paths.appPath, baseUrlResolved) === "") {         return {             src: paths.appSrc,             ...alias,         };     }     return alias; } ... // 如有Jest function getJestAliases(options = {}) {   const baseUrl = options.baseUrl   if (!baseUrl) {     return alias   }   const baseUrlResolved = path.resolve(paths.appPath, baseUrl)   if (path.relative(paths.appPath, baseUrlResolved) === '') {     return {       '^src/(.*)$': '<rootDir>/src/$1',       ...alias,     }   }   return alias } ... return { webpackAliases: getWebpackAliases(options), ... } 复制代码

paths.js

paths中暴露projectDirectory ,路径为根目录

module.exports = {     projectDirectory: resolveApp("../../"),   ... }; 复制代码

webpack.config.js

在Webpack.config.js的module里增加关于“isEnvDevelopment && paths.projectDirectory].filter(Boolean)”的配置,使得本地开发环境修改package里的代码可以实现热更新

{ test: /\.(js|mjs|jsx|ts|tsx)$/, include: [paths.appSrc, isEnvDevelopment && paths.projectDirectory].filter(Boolean) ... } 复制代码

后记

总而言之,上文叙述的是某些场景下使用monorepo的开发模式将各自独立的项目变成一个统一的工程整体,解决提升研发效率和工程质量

正如uWyndA 的 2021 年终总结 - 掘金 (juejin.cn)提到,只是在对的场景使用对的技术,毕竟软件开发没有「银弹」

另外,大家在使用monorepo解决了自身项目需求时,产生过哪些问题呢?这些问题可以解决吗?欢迎大家在留言区一起讨论


作者:uWydnA
链接:https://juejin.cn/post/7043990636751503390


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐