前端 monorepo 模式开发实践

背景

随着业务逐渐复杂,对于页面性能秒开,组件化,跟客户端的交互等在整个开发链路变得越来越重要,而单独基于每个链路都开一个项目仓库显然不好维护,所以交易的 monorepo 就产生了,在这个统一的仓库下管理所有的 npm 包更合理,目前很多前端技术社区也是这么做的,所以我们也借鉴了这个模式。

monorepo 介绍

使用 monorepo 首先要知道什么是 monorepo。这个文档介绍了 monorepo 的,为什么要使用它,以及使用 monorepo 构建工具。
管理多个不同项目的单一存储库,这些项目有可能是相关的,但通常又是逻辑相对独立的。下面这张图很好的说明了 monorepo 的概念:

基于 lerna 开发

先来熟悉下 lerna 的几个常用命令,后面跑项目的时候用的到:
创建项目和依赖相关的命令:
learn init --初始化项目
learn create --创建包(package)
learn add --安装依赖
learn link --链接依赖

开发和测试过程中要用到的:
learn exec --执行shell脚本
learn run --执行 npm 的命令
learn clern --清空依赖
learn bootstrap --重新安装依赖

发布上线相关命令:
learn version --修改版本号
learn changed --查看之前版本的所有变更
learn diff --查看 diff
learn publish --发布项目

初始化仓库

git clone xxx/test-monorepo.git
cd test-monorepo
yarn add lerna -g

新建 .gitignore 文件

.node_modules
.idea
.vscode
dist

运行 lera init命令,这个命令就是在仓库下新建三个文件:

  • package.json
  • lerna.json (lerna的配置文件)
  • packages (包集合)
    创建 .npmrc 文件
// 如果是私有npm,可以配置上
registry=https://npm.xxxx.com

创建包

包的发布地址就是私有仓库 https://npm.xxxx.com/ ,这个npm 私服是通过 verdaccio 部署的,这个部署私服部分后面再说,先建子包。
创建一个子包:这个包的功能是在发布构建成功之后,压缩dist目录,然后上传压缩包到部署分支下,可以通过解压看到打包构建分产物,然后本地在 http-server 下启动一个服务,可以本地预览页面,这是个webpack 插件形式。也可能会存在有脚本改变了构建产物,所以也可以当作脚本在最后一步执行。

创建 webpack-zip-dist 包

lerna create webpack-zip-dist 

根据命令提示,输入相应的信息,包的入口为lib/index.js,这个文件是 通过 typescript 打出来的,所以后面会配置 typescript 的开发环境,整个仓库都使用 ts 开发。创建的包都会在 packages目录下,进入到 webpack-zip-dist 目录下:

cd packages/webpack-zip-dist
mkdir src && cd src && touch index.ts

开发环境下入口是 src/index.ts。
给仓库根目录下添加 tsconfig 的配置,这个配置子包都可以继承,子包也可以配置单独的 ts 规则,执行yarn add typescript -g

// tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "importHelpers": true,
    "esModuleInterop": true,
    "outDir": "lib",
    "declaration": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "baseUrl": "./",
  },
  "include": ["packages/*"],
  "exclude": [
    "node_modules",
    "lib"
  ]
}

回到子包 webpack-zip-dist 也新建一个子包的 ts 配置文件,继承仓库的配置:

// packages/webpack-zip-dist/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./lib"
  },
  "include": [
    "./src"
  ]
}

配置构建命令

子包的 package.json 文件加入script 命令:"build": "tsc"
安装依赖:npm install
在项目跟目录下执行构建ts文件命令 lerna run build,代码会将 src 下的文件编译到 lib 文件下。

这样就有了一个最简单的 npm 包,下面就讲一下如何部署私有 npm,以及发布包到私有 npm 上。

实现一个 webpack 插件包

link 包本地调试:
yarn build 之后运行
目录运行:npm link webpack-dist-zip

admin@xxx-mbp webpack-dist-zip % npm link webpack-dist-zip
added 1 package from 1 contributor in 0.301s
/Users/admin/code/test-monorepo/packages/webpack-dist-zip/node_modules/webpack-dist-zip -> /Users/admin/.nvm/versions/node/v14.18.2/lib/node_modules/webpack-dist-zip

引入包的项目下运行:yarn link webpack-dist-zip
这样调试的时候每次只需要执行 yarn build 就可以了,yarn link 一次就行。

这个插件包的功能是将项目打包构建之后的产物以 zip 压缩包的形式上传到仓库的根目录下,这样就可以解压看到实际部署的产物是什么样,而不用登陆服务器去查看,主要解决服务器部署和构建产物不一致的情况,方便查找问题。

实现思路就是:监听webpack 的钩子函数compiler.hooks.afterEmit.tapAsyncafterEmit 事件阶段表示 资源已经写入文件系统,这个时候去压缩dist 目录即可。

代码如下:

// webpack-dist-zip/index.ts
const Archiver = require('archiver')
const fs = require('fs')
const path = require('path')
const shell = require('shelljs')
import { paramsInterface } from './type'

const archive = Archiver('zip', { zlib: { level: 9 } })

class WebpackZipDistPlugin {
  options: {
    outputPath?: string
    targetZipFilename?: string
  }
  constructor(options: paramsInterface) {
    this.options = options || {
      outputPath: 'dist',
      targetZipFilename: 'archive-dist',
    }
  }
  apply (compiler) {
    // afterEmit 资源已经写入文件系统
    compiler.hooks.afterEmit.tapAsync('WebpackZipDist', (callback) => {
      try {
        this.executeAsScript()
        callback()
      } catch(e){
        callback()
      }
    })
  }

  archiveDirectory() {
    console.log('开始压缩dist目录,生成文件...')
    // 生成的压缩目录名
    const { outputPath, targetZipFilename } = this.options
    const filepath = path.resolve(process.cwd(), `${targetZipFilename}.zip`)
    // 可写流写入数据
    const output = fs.createWriteStream(filepath)
    // 建立管道连接
    archive.pipe(output)
    // 压缩outputPath: dist目录,压缩包名targetZipFilename: archiver-dist
    archive.directory(outputPath, targetZipFilename)
    archive.finalize()
    console.log('压缩dist目录成功...')
  }

  pushFileToCurrentBranch() {
    console.log('正在提交zip到远程部署分支...')
    // console.log('shell', shell)
    shell.exec('git add . \n git commit -m "build 压缩包提交成功" \n')
    const branch = shell.exec('git branch | grep "*"').slice(2)
    shell.exec(`git push origin ${branch}`)
    console.log(branch, '分支提交成功...')
  }

  // 作为脚本使用,已经存在dist目录了
  executeAsScript() {
    const { outputPath } = this.options
    const statsObj = fs.statSync(path.resolve(process.cwd(), outputPath))
    if (statsObj.isDirectory()) {
      this.archiveDirectory()
      this.pushFileToCurrentBranch()
    }
  }
}

export default WebpackZipDistPlugin

到这里上传压缩包就实现了,到项目下面找到配置文件使用,在构建完成之后就会上传压缩包到代码仓库:

const WebpackZipDistPlugin  = require('webpack-dist-zip').default
...
if (process.env.BUILD_ENV === 'production') {
      config.plugin('define-plugin').use(
        new WebpackZipDistPlugin({
          outputPath: 'dist',
          targetZipFilename: 'archive-dist'
        })
      )
    }
...

测试完成之后将包 unlink 即可:yarn unlink webpack-dist-zip

给工具包新增单元测试

这个仓库还有个工具函数包,下面给工具函数包新增单元测试。
本文采用 Jest 测试框架,在此工具包下面安装 jest 包,新增测试命令
yarn add jest -D

// package.json
"scripts": {
    "test": "jest",
    "build": "tsc"
  },

到包下面编写测试文件,我这里展示测试测试工具包里面的一个函数,编写测试文件,跑几种测试用例,工具函数的功能是将单位为分的价格展示为元,要处理价格是0的情况,如果价格真实就是0 就展示0,如果不是只是一个默认值0就展示'--'的情况:

/**
 * 
 * @param price 价格分
 * @param showZero 是否展示0的价格 默认展示0
 * @returns 返回价格或者'--'
 */
function priceDisplay(price: number, showZero = true) {
  const isUndefined = (price === undefined) || (price === null)
  if (isUndefined) return '--'

  const numberPrice = Number(price)
  let showText: number | string = ''
  // 如果输入的价格是0,showZero: true展示0, false展示'--'
  if (numberPrice === 0) {
    if (showZero) {
      showText = 0
    } else {
      showText = '--'
    }
  } else {
    showText = numberPrice / 100
  }
  return showText
}

export default priceDisplay

到工具包下面找到tests目录,在文件h5-utils.test.js中针对这个函数写一下测试用例,

'use strict';

const h5Utils = require('..');
const { priceDisplay } = h5Utils

describe('h5deal-utils', () => {
    test('展示价格0', () => {
        expect(priceDisplay(0)).toBe(0);
    })
    test('不展示价格0', () => {
        expect(priceDisplay(0, false)).toBe('--');
    })
    test('价格为分', () => {
        expect(priceDisplay(2800)).toBe(28);
    })
    test('价格输入为字符串0,展示0', () => {
        expect(priceDisplay('0')).toBe(0);
    })
    test('价格输入为字符串0,不展示0', () => {
        expect(priceDisplay('0', false)).toBe('--');
    })
    test('价格输入为字符串的分:', () => {
        expect(priceDisplay('2800')).toBe(28);
    })
});

保存之后本地执行测试:yarn test

image.png

测试用例都跑通过了。

发布包

在项目最外层的 package.json 中配置打包和发布命令"publish": "lerna run build && lerna publish"

// package.json
{
  "dependencies": {
    "lerna": "^4.0.0",
    "typescript": "^4.6.4"
  },
  "name": "h5deal-monorepo",
  "private": true,
  "scripts": {
    "publish": "lerna run build && lerna publish"
  },
  "version": "0.0.4",
  "devDependencies": {
    "@types/react": "^18.0.9"
  }
}

提交本地代码之后,直接运行 npm run publish,这个命令会在打包之后,选择要更新的版本号,然后直接上传到私服 npm.xxx.com上。

admin@xxx-mbp h5deal-monorepo % npm run publish
> lerna run build && lerna publish

lerna notice cli v4.0.0
lerna info Executing command in 2 packages: "yarn run build"
lerna info run Ran npm script 'build' in 'webpack-dist-zip' in 2.2s:
yarn run v1.22.5
$ tsc
Done in 2.00s.
lerna info run Ran npm script 'build' in 'h5-utils' in 2.5s:
yarn run v1.22.5
$ tsc
Done in 2.30s.
lerna success run Ran npm script 'build' in 2 packages in 2.5s:
lerna success - h5-utils
lerna success - webpack-dist-zip
lerna notice cli v4.0.0
lerna info current version 0.0.9
lerna info Looking for changed packages since v0.0.9
? Select a new version (currently 0.0.9) Patch (0.0.10)

Changes:
 - h5-utils: 0.0.9 => 0.0.10
 - webpack-dist-zip: 0.0.9 => 0.0.10

? Are you sure you want to publish these packages? Yes
lerna info execute Skipping releases
lerna info git Pushing tags...
lerna info publish Publishing packages to npm...
lerna WARN lifecycle Skipping root "publish" because it has already been called
lerna WARN lifecycle Skipping root "postpublish" because it has already been called
Successfully published:
 - h5-utils@0.0.10
 - webpack-dist-zip@0.0.10
lerna success published 2 packages

访问 https://npm.xxx.com/-/web/detail/h5-utils 查看包的版本:
可以看到最新的版本已经是 0.0.10

部署私有 npm

通过 verdaccio 部署私服,具体的实现方式可以参照网上资料。

参考:
https://semaphoreci.com/blog/what-is-monorepo
https://monorepo.tools/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,002评论 6 509
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,777评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,341评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,085评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,110评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,868评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,528评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,422评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,938评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,067评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,199评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,877评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,540评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,079评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,192评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,514评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,190评论 2 357

推荐阅读更多精彩内容