Turborepo & father & pnpm & lerna 搭建前端组件库经验分享

本文介绍了如何利用dumi搭建脚手架,配置pnpm进行包管理,使用lerna管理组件间的依赖,通过turbo加速多包打包过程。在组件库的构建过程中,还涉及了father的配置和使用,以及如何优化打包速度,如通过git变更检测来只打包修改过的组件。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们用 dumi 做演示,脚手架搭建跟着官网教程走就行了

https://d.umijs.org/guide

先提一嘴,这里用的 father4,不是 father build(father2),用 father2 打包没啥问题,如果是 father2 迁移 father4 的话有一些需要注意的地方,这个我后面会再写一篇文章来讲

配置pnpm

官网:

https://pnpm.io/zh/

如果全局没有 pnpm,需要先安装

npm install -g pnpm

如果刚刚执行过 npm install 了,或者用过 yarn 了,就把 node_modules 删了,然后执行

pnpm install

安装完成后可能会弹出类似于下方截图的内容
在这里插入图片描述
typescript 默认是没有装的可以装一下
在 package.json 中增加下面的内容

"pnpm": {
    "peerDependencyRules": {
      "ignoreMissing": [
        "react",
        "react-dom",
        "antd",
        "dva",
        "postcss",
        "webpack",
        "eslint",
        "stylelint",
        "redux",
        "@babel/core"
      ]
    }
  },

增加 .npmrc 文件,增加下面的内容

registry=https://registry.npmjs.org/
strict-peer-dependencies=false

我们使用 lerna 主要是用于管理包依赖,用了的话与 dumi 默认放组件的位置会有所不同,dumi 放在 src 文件夹中,但是 monorepo 模式下通常会放在 packages 中,这个文件夹默认没有,暂时不管。因为组件目录变了,pnpm 需要额外配置一些内容,在组件库根目录下创建 pnpm-workspace.yaml 文件,配置内容参考

packages:
  # all packages in direct subdirs of packages/
  - 'packages/*'
  # all packages in subdirs of components/
  - 'components/**'
  # exclude packages that are inside test directories
  - '!**/test/**'

主要是 packages,另外两个如果暂时没用到不配置也行

配置lerna

官网:

https://lerna.js.org/docs/getting-started

如果没有安装 lerna ,需要全局安装

npm install lerna@5.6.2 -g

为了避免一些问题,这里暂时安装5.6.2的版本,最新的已经是6.x的版本了,暂时不管,继续往下执行
安装完成之后执行

lerna init

这个命令会帮助我们创建 package.json 以及 lerna.json ,创建完成之后 package.json 中被添加了这样一段内容

"workspaces": [
    "packages/*"
  ]

因为我们用 pnpm,用到了 pnpm-workspace.yaml,所以这一段用不到了,要删掉
我们继续用 lerna 帮我们创建几个组件包

lerna create @mino/tag packages/tag --yes
lerna create @mino/button packages/button --yes

lerna 会帮我们创建两个组件包,这里演示的话只留 package.json 就行了,其余的可以删掉
然后我们在两个组件目录下创建 src 目录,在下面创建 index.ts 文件,写一点内容
/src/index.tsx

import React from 'react';

const index: React.FC = () => {
  return (
    <div>this is button</div>
  )
}

export default index;

会报错,显示没有安装 react
在这里插入图片描述
在根目录和组件目录下的 package.json 中都配一下 react 的依赖

"dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }

然后再执行一次 pnpm install
装完之后就不会报错了,然后同样的内容在 tag 的组件目录下也写一份
最后,在根目录的 package.json 下配置组件私有(避免误发布)

"private": true,

配置turbo

官网:

https://turbo.build/repo/docs

安装 npm install turbo --global
然后添加 turbo.json 到根目录

{
  "$schema": "https://turborepo.org/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    }
  }
}

然后到根目录下的 package.json ,配置下面的内容

"packageManager": "pnpm@7.27.1"	// 这里写你安装的 pnpm 版本

配置father

地址:

https://github.com/umijs/father

执行命令 pnpm add father -w 或者 pnpm install father -w

装的是 father4,装2的话可能会遇到一些问题,后面单独出文章讲一下(迁移可能确实会遇到需要装2的情况,等 father4 支持 umd 打包文件名会好很多)

在每一个组件目录的 package.json 中配置内容

"scripts": {
    "build": "father build"
  },

然后每个组件目录下面都要配置 .father.ts ,可以参考 umi 的方法,在根目录写一个 .fatherrc.base.ts,然后组件目录下用 extends 加载配置(仅限father4)

//.fatherrc.base.ts
import { defineConfig } from 'father';

export default defineConfig({
  cjs: {
    output: 'lib',
  },
  esm: {
    output: 'es',
  },
  umd: {
    output: 'dist'
  }
});

// packages/button/.fatherrc.ts
import { defineConfig } from 'father';

export default defineConfig({
  extends: '../../.fatherrc.base.ts',
});

关于三种类型的打包输出文件,这里就是按需配置,笔者这边有遇到三种类型都要的场景。关于三种类型在什么情况下使用,可以参考

https://github.com/umijs/father/blob/master/docs/guide/build-mode.md
https://github.com/umijs/father/blob/master/docs/guide/umd.md#%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9

umd 类型可能还需要配置 runtimeHelpers,但是 father4 改为约定式了,需要安装 @babel/runtime

pnpm add @babel/runtime -w

要注意在需要用到的组件的 packages.json 中配置上

开始打包

进行了上面的步骤之后就可以开始打包了,如果遇到问题欢迎在评论区留言
直接执行

pnpm turbo build
// 或者 npx turbo build

在这里插入图片描述
turbo 打包就是快,因为 turbo 是同时打包多个组件的。
在这里插入图片描述
可以看到,我们需要的打包类型全部都成功输出了

小总结

从头到尾没有太多的解释说明,只讲了步骤,主要这四个玩意内容量基本都是能独占一篇文章的,展开讲暂时没有那么多时间,这里简单说明一下。
pnpm 是包管理工具,用它的原因就是速度比 npm 和 yarn 快很多,pnpm 的 node_modules 格式也相比前两个有很大的不同,当然快的主要原因是 pnpm 用软硬连接解决重复依赖的问题,而前两者主要靠 copy
lerna 在这里主要是用来管理包依赖的问题,实际上处理 monorepo 工作的是 turbo ,并不是 lerna。而 turbo 强力的 cache 功能我们并没有使用,因此在这里 turbo 快主要在于它同时对多个组件同时进行打包。
father 是实际处理打包任务的那个,在 father 2 的时候其实内置了 monorepo 方案,在升级到v4之后不再内置,也因此我们要自己提供,这也是我们用到 turbo 的原因之一

更进一步提升打包速度

turbo 有着很强大的功能 remote caching,这个笔者暂时没有体验过,也暂时没有机会体验,后面有机会肯定会尝试一下。
那么在不使用 remote caching 的情况下我们要如何提升速度呢?我们这里没有挂载 git ,可能看不太出来。当我们挂载 git 仓库的时候就能发现,我们用 turbo 帮我们跑打包任务的时候,有些组件的代码并没有发生变更,但是 turbo 依旧帮我们用 father 重新打包了组件,这个其实就浪费了时间。
在 turbo 官网中,我们可以查询到一些相关的内容
在这里插入图片描述
简单来说就是 turbo 能够帮我们监听 git 变更,来帮我们只打包发生变更的文件。那么 turbo 既然有解决方案为什么我还要单独拉出来讲呢,因为我发现不太好用。不知道是不是环境配置原因,我在实际使用中发现这个功能有时候并没有生效,有时候会有延迟,有时候 turbo 甚至根本就拿不到最新的变更。各位可以直接使用 turbo 的提供的这个功能试试,在了解真正的原因之前我自己写了一个根据 git 变更打包的脚本,借助 turbo 的 filter来实现。
首先需要在根目录的 package.json 中添加执行脚本

"scripts": {
    "start": "npm run dev",
    "dev": "dumi dev",
    "build": "dumi build && node scripts/build.js",
    "prepare": "husky install && dumi setup"
  },

主要看 build,我们增加执行我们自己的脚本,然后我们新增这个 build.js 脚本

// build.js
const { spawn, exec } = require('child_process');

const execg = spawn('git', ['log', '-2']);

const commitArray = [];

execg.stdout.on('data', (data) => {
  const commitContent = Buffer.from(data).toString();
  const commitStrList = commitContent.split('commit');
  const commit = commitStrList[1].split('Author')[0].trim().split('\n')[0];
  commitArray.push(commit)
  if (commitArray.length === 2) {
    const childExec = spawn('git', ['diff', commitArray[0], commitArray[1]]);

    childExec.stdout.on('data', (data) => {
      const diffContent = Buffer.from(data).toString();
      getChangedFiles(diffContent);
    });

    childExec.stderr.on('data', (data) => {
      process.stderr.write(`stderr: ${data}`);
    });

    childExec.on('close', () => {
      buildChangeFiles();
    });
  }
});

execg.stderr.on('data', (data) => {
  process.stderr.write(data);
});

const changeFiles = new Set();
// 获取变更的文件
const getChangedFiles = (content) => {
  const catchReg = /--- a\/packages\/[a-z\-]*/g;
  let res;
  while (res = catchReg.exec(content)) {
    changeFiles.add(res[0].split('a/packages/')[1]);
  }
}

// 打包变更文件
const buildChangeFiles = () => {
  const fileList = [...changeFiles];
  console.log('变更组件列表: ', fileList);
  if (fileList.length !== 0) {
    const paramsList = fileList.map(item => `--filter=@--/${item}`);
    paramsList.unshift('turbo', 'build');
    console.log('paramsList: ', paramsList);
    console.log("开始打包变更文件")
    // spawn('pnpm', ['--version']);
    const buildOut = exec(`pnpm ${paramsList.join(' ')}`);

    buildOut.stdout.on('data', function (data) {
      process.stdout.write(data);
    });

    buildOut.stderr.on('data', function (data) {
      process.stderr.write(data);
    });
  }
}

概括一下上面的代码,我们通过脚本执行一些 git 命令,来获取最近两次的git 提交信息

const execg = spawn('git', ['log', '-2']);

然后解析这些信息,获取到两次 commit 的 hash,在用这两个 hash 执行 git 命令获取这两次提交的变更内容(主要考虑到方便后面调整),在解析变更内容获取到具体发生变更的组件

// 获取变更的文件
const getChangedFiles = (content) => {
  const catchReg = /--- a\/packages\/[a-z\-]*/g;
  let res;
  while (res = catchReg.exec(content)) {
    changeFiles.add(res[0].split('a/packages/')[1]);
  }
}

最后执行命令,用 turbo 打包发生变更的文件,它的格式是这样的

turbo run build --filter=@组织名/组件名1 --filter=@组织名/组件名2

当然还有别的格式,详情参考官网。
这样,我通过脚本就做到了原来 turbo Head 描述的相关功能,它会只打包发生变更的组件,这样能节省很多时间
不过我上面这段代码有一个问题,就是如果你是改变的 package.json 中的内容,比如你升级了某个依赖包,这样上面的代码是不会有任何动作的,你需要自己添加相关内容,当 package.json 中的依赖发生变更时,最好能打包全部的组件,当然,如果你有手段确定具体依赖这个包的组件,那么你可以选择只打包对应的组件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值