专栏名称: 前端大全
分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯
目录
相关文章推荐
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  6 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  17 小时前  
启四说  ·  启四VIP策略网站,有哪些功能?如何使用? ·  17 小时前  
前端早读课  ·  【第3451期】前端 TypeError ... ·  昨天  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
江苏司法行政在线  ·  宿迁司法行政人、江苏监狱戒毒民警,给您拜年啦! ·  3 天前  
51好读  ›  专栏  ›  前端大全

10分钟速成:轻松搭建前端monorepo架构与CI/CD自动化!

前端大全  · 公众号  · 前端  · 2024-10-16 10:10

正文

作者:文学与代码

https://juejin.cn/post/7401112990441275426

正文

今天我们主要讨论3方面内容:

  1. 如何搭建比较高效好用的monorepo工程
  2. 前端如何基于搭建的monorepo工程实现自定义cli工具
  3. 普通前端项目以及monorepo工程项目自动化cicd核心问题以及解决方案

基于 pnpm-workspace + Turborepo + lerna 搭建 monorepo 的 cli 工程

首先利用 pnpm 初始化一个工程

执行命令初始化工程:


pnpm init -y


项目中安装开发依赖 lerna:

pnpm i -D lerna

配置命令:

  "scripts": {
    "lerna-init""lerna init",
    "lerna-create""lerna create"
  }

搭建多包环境:

建立 pnpm-workspace.yaml 文件,并且配置:

packages:
  - 'packages/*'

新建packages目录:

image.png

初始化 lerna 配置:

pnpm lerna-init
image.png

创建 cli 的核心包:

lerna-create @frontend-dev-cli/core
image.png
image.png

这样之后,lerna 就给我们创建好了一个包的默认模板。

集成 ts

我们的 cli 全部采用 ts 进行开发,所以我们需要搭建一套多包的 ts 环境。

  1. 我们在根目录下新建一个 ts 的配置文件:
image.png

加入如下的配置:


{
  "compilerOptions": {
    "module""commonjs",
    "declaration"true,
     "outDir""./build",
    "noImplicitAny"false,
    "removeComments"true,
    "noLib"false,
    "emitDecoratorMetadata"true,
    "experimentalDecorators"true,
    "target""es6",
    "sourceMap"true,
    "lib": [
      "es6",
      "esnext",
      "dom"
    ],
    "types": [
      "node"
    ],
    "resolveJsonModule"true
  },
  "include": [
    "src/**/*.ts" // 明确指定匹配 .ts 文件
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts",
    "package.json"
  ]
}

在根目录下新建一个 src 测试文件夹,在里面加入 index.ts 以及 a.ts 两个测试 ts 的文件:


// a.ts

export const a = () => {
    console.log('adjddj');
}

export const b = () => {
    console.log('adjddj');
}


// index.ts

import { a } from './a'

export default function main ({
    a()
    console.log('main')
}

main()

然后执行以下命令在根目录安装 typescript 的开发依赖:

pnpm i -D typescript -w

然后,配置编译的脚本:

 "build""tsc"

执行 pnpm build 命令:

image.png

出现这个错误的原因是因为我们在 ts 编译配置中的:

    "types": [
      "node"
    ],

设置 ts 的编译的宿主环境是 node,但是 ts 没有找到 node 的类型文件。所以我们执行:

 pnpm i -D @types/node -w

安装 node 的类型文件。然后再次执行构建命令:

构建完成了。在著项目中测试完毕之后我们再到创建的 core 子项目中去新建一个 index.ts 文件并且再子项目中配置构建命令:

image.png
image.png

配置子应用的 ts 配置文件:

{
    "extends""../../tsconfig.json",
    "compilerOptions": {
        "outDir""./build"
    },
    "include": [
        "./lib"
    ]
}

直接继承了父应用的全局配置,并且指定了本应用的编译目录和输出目录。然后我们尝试再 core 项目中执行 build 命令:

pnpm build

可以看到在子项目中是可以成功调用到父项目中安装的脚本命令的,并且成功按照父项目中统一配置的 ts 配置文件的规则来进行编译了。这就是 monorepo 架构的好处。包括像 eslint 这些代码格式校验工具,jest 这些测试工具,我们都只需要再根项目下配置依次就可以了,子项目中直接就可以集成根项目中的配置。至此,ts环境准备完毕。

打通多包项目之间的调用关系

我们先新建一个新的包:

 pnpm lerna-create @frontend-dev-cli/utils

加入如下的工具导出:

在这个子项目中加入同样的编译命令:

在 utils 这个子项目中执行 build 命令,产生构建结果:

然后我们调整 package.json 中的 files 配置:

  "files": [
    "build"
  ],

也就是当执行 publish 操作的时候,我们只需要上传 build 目录里面的内容就可以了。然后我们尝试在 core 这个子项目中引入utils这个项目。我们切换到 core 项目中,执行以下命令:

这样,utils这个子项目就被链接进来了:

image.png

然后我们直接在 core 中引入 utils 中导出的内容:

我们执行以下构建 core 项目的命令:

正常构建了,我们执行以下构建的结果:

至此。子应用之间的调用也测试通过了。

优化开发体验

优化开发体验主要是两个方面:

  1. 每一个子应用中只要ts代码变化了都需要重新触发ts代码的重新构建
  2. 我们在主项目中需要一个一次性可以执行所有子项目构建操作的命令

先解决第一个问题,这个问题很好解决,我们可以在每一个子应用中的 tsc 命令调用的时候加入 watch 参数:

  "scripts": {
    "build""tsc --watch"
  },

我们重新执行 core 中的build命令,然后重新改以下源码:

image.png

此时tsc就会一直监控源码的变化,一旦源码变化就会自动编译,并将编译结果输出到 build 目录。

image.png

我们就可以看到最新的代码执行结果了。要解决第二个问题。其实目前有两种常见解决方案:

  1. 利用我们已经安装的 lerna 工具,lerna 中有支持一次性并行执行的命令。
  2. 比较新的一个工具:turbo。这个工具相对效率更高,体验更好,我们本次采用这个工具来解决这个问题。

首先还是在全局安装依赖:

pnpm i -D turbo -w

然后再根目录下新建turbo.json 文件,配置如下内容:

{
    "tasks": {
      "build": {
        "dependsOn": [],
        "outputs": ["build/**"]
      }
    }
  }

然后调整根目录下的 build 命令并且指定包管理器:

  "packageManager""[email protected]",
  "scripts": {
    "lerna-init""lerna init",
    "lerna-create""lerna create",
    "build""turbo run build"
  },

再根目录下执行 pnpm build:

image.png

turbo 对编译结果是进行了本地缓存的以及加速的,所以编译非常的快,体验很棒:

image.png

至此我们就优化了本地开发的编译问题。

利用 lerna 来进行 monorepo 发包:

首先不管是 lerna 还是 pnpm 发布包之前都必须提交本地 git,所以请先将自己本地的 git 改动全部提交到远程仓库上。然后我们就在package.json 中添加如下的发包命令:

 "lerna-publish""lerna publish"

然后我们执行命令,利用 lerna 进行发包:

image.png

然后进入一系列和 lerna 的交互之后,就可以进行 lerna 的发包了。发布完成之后,我们可ui前往 npm 仓库查看:

image.png
image.png

至此,我们利用 lerna 完成了多包项目的发包操作。我们再来提前扩展一个点哈,因为我们后面就要实现多包工程自动化发布的cicd。而cicd肯定是在服务器上自动执行的,不能够有交互。lerna实际上是考虑到了这一点的,它的命令行提供了如下的参数:

"lerna-publish""lerna publish 0.0.2 --yes"

就可以直接指定所有的子包发布的版本以及跳过所有的交互命令行了。而且在绝大多数情况下,尤其是在要实现自动化cicd的 monorepo 项目,保持所有子包版本的统一性是最佳的实践。lerna在包发布完成之后,会自动基于现在的新发布的版本来自动打上一个git tag,以及自动把这个版本推送到远程分支:

包括自动更新对统一作用域包的版本依赖,都会自动更新:

image.png

这些都算是很好的自动化实践。

自定义 cli 工具

cli的原理以及搭建前端研发脚手架:

cli本质上就是一个命令行工具,通过和用户进行命令行交互来实现指定的功能。前端实现cli其实很简单:

  1. 在package.json 中加上 bin 字段:

cli 想不本质上也就是一个 npm 包,但是和普通的 npm 包不同的是,它的package.json 文件中多了一个 bin 字段,bin 字段实际上就是配置命令的名称以及对应的可执行文件,我们将 core 包改造成一个 cli 程序:

  "bin": {
    "frontend""build/index.js"
  },

我们在 package.json 中配置了如上的命令,实际上就是注册了 cli 的命令是 frontend。frontend对应的可执行文件是 build/index.js,也就是我们期望,当在控制台上输入 frontend 后,操作系统会自动执行 build/index.js。改造完这个之后,我们需要让目前的操作系统上存在frontend命令,要做到这一点,我们可以这样做,在 core 包所在的目录的下输入:

 npm link
image.png

本质上就是在 npm 全局目录下设置一个软连接,链向了我们本地正在开发的包。这样做了之后,我们在命令行中尝试输入注册的cli命令:

我们可以看到操作系统已经可以正常识别命令了,因为我们的 node 以及 npm 目录是早已经被注册到了环境变量中的,而全局npm目录下存在 frontend 命令以及对应的执行文件,所以操作系统就可以正常找到了。只是目前这个文件依然是一个普通的文件,不是一个可之心那个文件,所以操作系统直接使用记事本将文件的内容打开了。我们需要将其改造成一个可执行文件:

image.png

方案其实很简单,就是在开头加了一行注释,这行注释就是告诉操作系统,需要调用 node 进程来执行这个文件,这样做了之后我们重新链接,然后重新执行命令:

image.png

至此,我们就搭建了 cli 的基础能力。

利用 yargs 库注册并解析命令行参数:

可以注册和解析命令行参数的库有很多,个人比较喜欢 yargs 这个老牌的库。我们可以新建一个包:

@frontend-dev-cli/cli

image.png
image.png

在这个包下负责封装 yargs,

image.png

进阶这我们快速基于 yargs 来封装一个命令行程序:在 index.ts 内:

import yargs = require('yargs')

// 注册 cli 全局 options

const initGlobalOptions = (yargsIns: yargs.Argv) => {
  return yargsIns
    .option("debug", {
      alias"d",
      defaultfalse,
      describe"开启脚手架调试模式",
      type"boolean",
    })
    .option('targetPath', {
        alias't',
        default'',
        describe'指定要执行的目标目录',
        type:'string',
    })
    .option('flushed', {
      alias'f',
      defaultfalse,
      describe'前置更新',
      type:'boolean',
  })
}

export default function cli({
  // 初始化 cli、注册cmd
  return enrollCommand(initGlobalOptions(yargs('''')))
  .usage("Usage: $0  [options]")
  .demandCommand(1"A command is required. Pass --help to see all available commands and options.")
  .recommendCommands()
  .strict()
  .alias("h""help")
  .alias("v""version")
  .fail((msg, err, yargsInstance) => {
    console.log('自定义错误内容', msg)
  })
}

在enrollCommand.ts 内:

const enrollCreateCommand = (cli: yargs.Argv) => {
  // 添加注册 cmd 的逻辑
}

const enrollPublishCommand = (cli: yargs.Argv) => {
// 添加注册 cmd 的逻辑
}

const enrollDownloadCommand = (cli: yargs.Argv) => {
// 添加注册 cmd 的逻辑
}

export default function (cli: yargs.Argv{
  // 注册 create 命令
  enrollCreateCommand(cli)
  // 注册 publish 命令
  enrollPublishCommand(cli)
  // 注册 download 命令
  enrollDownloadCommand(cli)
  return cli
}

其实就是针对 yargs 进行了一些简单的自定义以及提供注册 cmd以及options。然后我们将cli包链接到 core 包下,在 core 中进行引用:

image.png
#!/usr/bin/env node

import { sum, a, aa } from "@frontend-dev-cli/utils"
import cli from '@frontend-dev-cli/cli'

function core({
  // 开始注册 cli
  cli()
}

core()

然后我们执行 cli 命令进行测试:

image.png

好像任何反应都没有。这个原因是因为 yargs 需要我们将用户在控制台输入的参数喂给它去解析:

import cli from '@frontend-dev-cli/cli'

function core({
  // 开始注册 cli
  cli().parse(process.argv.slice(2))
}

core()

然后我们再次输入命令:

image.png

因为篇幅有限,我们的cli程序本身还有很多细节可以去优化,这里就不再赘述了。







请到「今天看啥」查看全文