React组件库搭建(一)Monorepo篇(基于Nx)

3,343 阅读12分钟

Nx对Monorepo的支持

可借助Nx官方维护的Plugin,快速构建你的React、Vue、Angular应用(社区插件生态甚至有Golang和.net)

现在Monorepo主要有两种形态,一种是开源社区广泛流行的Package-Based Repos(vue\react\Midway的方案,即Lerna),另一种则是Google等大企业在自身拥有多个应用程式,程式关联性强,面临业务场景重复的程式而采用的Integrated Repos

本文主要介绍Integrated形态,通过阅读,你将了解到

  • 通过Nx-Generators启动React和Nextjs应用
  • 通过Nx-Generators创建共享React组件库
  • 通过Nx-Generators为组件库配置Storybook和 jest/vitest test
  • 通过Nx-Generators为组件库配置Tailwindcss
  • 通过Nx-Affected和Nx-Graph为应用程式优化CI构建CI测试
  • Nx自定义Executors和Nx自定义Generators,构建自己的自定义Plugin简化你的工作流。
  • 其它配置项:taiwindcss-prettier-plugin、git-commit-lint

开箱即用的Nx

使用create-nx-workspace创建Nx工作区

  • 包管理工具选择pnpm,
  • 形态选择integrated,
  • apps选项会创建一个相对干净的Nx工作空间(无多余的插件)
  • repository name : 你的工作区命名
  • 启动分布式缓存,加快CI构建速度
 npx create-nx-workspace --packageManager=pnpm

 >  NX   Lets create a new workspace [https://nx.dev/getting-started/intro]                                             
√ Choose your style                     · integrated
√ What to create in the new workspace   · apps
√ Repository name                       · juejin
√ Enable distributed caching to make your CI faster · Yes

 >  NX   Nx is creating your v15.2.3 workspace.

   To make sure the command works reliably in all environments, and that the preset is applied correctly,
   Nx will run "pnpm install" several times. Please wait.

⠼ Installing dependencies with pnpm

最基本的工作区认识

通过观察package.json,此时我们的Nx工作区,只包含了基础的npm包

"devDependencies": {
    "@nrwl/cli": "15.2.3",
    "@nrwl/nx-cloud": "latest",
    "@nrwl/workspace": "15.2.3",
    "nx": "15.2.3",
    "prettier": "^2.6.2",
    "typescript": "~4.8.2"
  }

Nx对于开发者来说(自Nx13后),Plugin是最重要的概念。

可以运行pnpm exec nx list 查看当前工作区已安装的Nx插件

如下所示,当前Nx工作区存在5个官方维护的Nx插件,可安装有react和nextjs,express等

pnpm exec nx list

 >  NX   Local workspace plugins:



 >  NX   Installed plugins:

   @nrwl/jest (executors,generators)
   @nrwl/linter (executors,generators)
   @nrwl/nx-cloud (generators)
   @nrwl/workspace (executors,generators)
   nx (executors)


 >  NX   Also available:

   @nrwl/angular (executors,generators)
   @nrwl/cypress (executors,generators)
   @nrwl/detox (executors,generators)
   @nrwl/esbuild (executors,generators)
   @nrwl/expo (executors,generators)
   @nrwl/express (generators)
   @nrwl/js (executors,generators)
   @nrwl/nest (generators)
   @nrwl/next (executors,generators)
   @nrwl/node (executors,generators)
   @nrwl/nx-plugin (executors,generators)
   @nrwl/react (executors,generators)
   @nrwl/react-native (executors,generators)
   @nrwl/rollup (executors,generators)
   @nrwl/storybook (executors,generators)
   @nrwl/web (executors,generators)
   @nrwl/webpack (executors,generators)

Nx对于开发者的核心概念是Plugin,而Plugin又有两个核心概念,executors 和 Generators

如下4方面进行官方维护的Plugin使用

使用Nx-Generators启动React和Nextjs应用

一般情况下我们可以选择CRA|webpack|Vite初始化一个React应用 ,而Nx官方插件@nrwl/react可以选择webpack和vite初始化一个React应用

安装@nrwl/react并初始化应用

pnpm add -D @nrwl/react @nrwl/next

使用插件创建一个react应用

nx g @nrwl/react:application

我的command line options

  • 应用名:reactApp
  • css选择,none(可选SACSS\LESS)
  • 不添加react-Router
  • 使用webpack构建(可选Vite)
 nx g @nrwl/react:application

>  NX  Generating @nrwl/react:application

√ What name would you like to use for the application? · reactApp
√ Which stylesheet format would you like to use? · none
√ Would you like to add React Router to this application? (y/N) · false
√ Which bundler do you want to use to build the application? · webpack

此时你的工作区的app目录下会初始化一个react应用

当前apps目录结构

apps
├─  react-app
    ├─ ..
    ├─ project.json
    ├─ src
    │  ├─ app
    │  │  ├─ app.spec.tsx
    │  │  ├─...
    │  ├─ assets
    │  ├─ environments
    │  │  ├─ environment.prod.ts
    │  │  └─ environment.ts
    │  ├─ favicon.ico
    │  ├─ index.html
    │  └─ main.tsx
    ├─ tsconfig.app.json
    ├─ ...

使用插件初始化一个Nextjs应用

  • 创建一个名为nextApp的应用
  • 不创建cypress(刚才创建react应用的时候没指定,默认创建一个/apps/react-app-e2e/)
pnpm exec nx generate @nrwl/next:application nextApp --e2eTestRunner=none

此时的apps目录结构

apps
  ├─react-app
  ├─react-app-e2e
  ├─next-app
    ├─..
    ├─ next.config.js
    ├─ pages
    │  ├─ ...
    ├─ project.json
    ├─ public
    ├─ specs
    │  └─ index.spec.tsx
    ├─ tsconfig.json
    └─ tsconfig.spec.json

tips 可以运行 pnpm exec nx g remove react-app-e2e 把刚生成react应用的测试应用给删了,后期有需求可通过@nrwl/cypress插件为某个程式重新配置。

tips 当前我的Nx-cli和workspace版本处于15.2.3,而react插件的版本是15.9.2,会造成创建应用失败,为了创建最新的react版本,会使用Nx migrate命令

Q:插件版本不兼容,运行 nx g @nrwl/reat:application 创建应用失败

报错如下

Cannot find module 'nx/src/utils/code-frames'

pnpm exec nx g @nrwl/react:application --dry-run

>  NX  Generating @nrwl/react:application

√ What name would you like to use for the application? · reatApp
√ Which stylesheet format would you like to use? · none
√ Would you like to add React Router to this application? (y/N) · false
√ Which bundler do you want to use to build the application? · webpack

 >  NX   Cannot find module 'nx/src/utils/code-frames'

   Require stack:
   - C:\software\webapp\space\juejin\node_modules\.pnpm\@nrwl+js@15.9.2_nx@15.2.3+typescript@4.8.4\node_modules\@nrwl\js\src\utils\typescript\run-type-check.js
   - C:\software\webapp\space\juejin\node_modules\.pnpm\@nrwl+js@15.9.2_nx@15.2.3+typescript@4.8.4\node_modules\@nrwl\js\src\index.js
   - C:\software\webapp\space\juejin\node_modules\.pnpm\@nrwl+react@15.9.2_62b6jm4wacpyxtczg72vprwqwi\node_modules\@nrwl\react\src\generators\application\lib\create-application-files.js
   - C:\software\webapp\space\juejin\node_modules\.pnpm\@nrwl+react@15.9.2_62b6jm4wacpyxtczg72vprwqwi\node_modules\@nrwl\react\src\generators\application\application.js
   - C:\software\webapp\space\juejin\node_modules\.pnpm\nx@15.2.3\node_modules\nx\src\config\workspaces.js
   - C:\software\webapp\space\juejin\node_modules\.pnpm\nx@15.2.3\node_modules\nx\src\config\configuration.js
   - C:\software\webapp\space\juejin\node_modules\.pnpm\nx@15.2.3\node_modules\nx\src\command-line\generate.js

解决方法

运行 Nx migrate [O:package] vesion,会把当前工作区的插件升级迁移到合适版本

 pnpm exec nx migrate 15.9.2
Fetching meta data about packages.
It may take a few minutes.
Fetching nx@15.9.2
Fetching nx@15.9.2
Fetching @nrwl/next@15.9.2
Fetching @nrwl/workspace@15.9.2
Fetching @nrwl/next@15.9.2
Fetching @nrwl/react@15.9.2
Fetching @nrwl/workspace@15.9.2
Fetching @nrwl/react@15.9.2

 >  NX   The migrate command has run successfully.

   - package.json has been updated.
   - migrations.json has been generated.

Nx会搜索合适的插件版本,帮你修改package.json中相应插件的版本

之后依次执行

pnpm install --no-frozen-lockfile
pnpm exec nx migrate --run-migrations

此时再执行生成器的命令,就安然无恙创建React和Next应用了

Nx-Generators创建共享React组件库

Nx工作区目录下,其中的apps目录一般存放相对独立的应用,libs目录则存放一些公共的api,共享组件等

在Nx插件中,如果你编写自定义插件,则Generatos部分你可能要区分apps应用和libs应用

使用@nrwl/react:components 创建一个react lib

nx generate @nrwl/react:library taibUi --unitTestRunner=jest --bundler=rollup --directory=ui --importPath=nx-awesome-lib --publishable --buildable
  • 创建名为taib-ui的应用
  • 指定jest测试当前libs程式(可选择vites)
  • 使用rolluo打包程序
  • 指定当前应用在libs/ui目录下
  • 可发布的npm包
  • 发布在npm的名称(对应package.json -> name)
  • 可在dist目录构建产物

此时你的libs目录会生成一个新的程式

libs
├─ ui
    ├─ taib-ui
    ├─ .babelrc
    ├─ ...
    ├─ src
    │  ├─ index.ts
    │  └─ lib
    │     ├─ ui-taib-ui.spec.tsx
    │     └─ ui-taib-ui.tsx
    ├─ tsconfig.json
    ├─ ...

接下来仍是通过@nrwl/react这一插件,配置Storybook和Taiwindcss

使用Nx-Generators为组件库配置Storybook和e2e test

使用Nx-generators为新创建的组件库创建组件

  • 指定项目ui-taib-ui下生成一个Button组件
  • 导出生成的组件(自动更新入口文件index.ts)
 nx generate @nrwl/react:component Button --project=ui-taib-ui

>  NX  Generating @nrwl/react:component

√ Should this component be exported in the project? (y/N) · true
CREATE libs/ui/taib-ui/src/lib/button/button.spec.tsx
CREATE libs/ui/taib-ui/src/lib/button/button.tsx
UPDATE libs/ui/taib-ui/src/index.ts

由此我们得到生成器产出的两个文件

//libs/ui/taib-ui/src/lib/button/button.tsx

/* eslint-disable-next-line */
export interface ButtonProps {}

export function Button(props: ButtonProps) {
  return (
    <div>
      <h1>Welcome to Button!</h1>
    </div>
  );
}

export default Button;

import { render } from '@testing-library/react';

import Button from './button';

//libs/ui/taib-ui/src/lib/button/button.spec.tsx

describe('Button', () => {
  it('should render successfully', () => {
    const { baseElement } = render(<Button />);
    expect(baseElement).toBeTruthy();
  });
});


为刚生成react lib 配置storybook

现在我们创建了一个共享的UI组件库,且为该组件库添加了一个Button组件,此时我们还不确定该组件库要运用于哪个应用,或者说负责前端应用的伙伴根本就没搭建项目。

而Storybook就是专注于组件库开发的工具

一行命令为某个projece配置Storybook(基于@nrwl/react:storybook-configuration)

nx generate @nrwl/react:storybook-configuration ui-taib-ui --bundler=webpack --tsConfiguration
  • 指定为ui-taib-ui程式配置storybook
  • 指定模块打包器为webpack
  • 指定Storybook的配置文件为Ts

my command line

 nx generate @nrwl/react:storybook-configuration ui-taib-ui --bundler=webpack --tsConfiguration

>  NX  Generating @nrwl/react:storybook-configuration

√ Configure a cypress e2e app to run against the storybook instance? (Y/n) · false
√ Automatically generate *.stories.ts files for components declared in this project? (Y/n) · true
√ Automatically generate test files in the Cypress E2E app generated by the cypress-configure generator? (Y/n) · false
√ Configure a static file server for the storybook instance? (Y/n) · true
Fetching @nrwl/storybook...
adding .storybook folder to your library
Fetching @nrwl/web...
UPDATE package.json
CREATE libs/ui/taib-ui/.storybook/main.ts
CREATE libs/ui/taib-ui/.storybook/preview.ts
CREATE libs/ui/taib-ui/.storybook/tsconfig.json
UPDATE libs/ui/taib-ui/tsconfig.lib.json
UPDATE libs/ui/taib-ui/tsconfig.json
UPDATE libs/ui/taib-ui/.eslintrc.json
UPDATE nx.json
UPDATE libs/ui/taib-ui/project.json
CREATE libs/ui/taib-ui/src/lib/button/button.stories.tsx
CREATE libs/ui/taib-ui/src/lib/ui-taib-ui.stories.tsx

tips @nrwl/react/generators/storybook-configuration是基于@nrwl/storybook封装的,你也可以使用@nrwl/storybook为你的React/Vue/Angular/web component 程式配置storybook.(或者自定义插件)

tips 关于测试方面,Nx官方维护的插件,几乎所有的Generators,无论是生成app还是lib,api都默认配置Jest或cypress,若不喜可研究官方的IDE插件--Nx Console慢慢调试 (VSCode , JetBrains and Neovim),或者自定义插件移除

使用Nx-Generators为组件库配置Tailwindcss

仍是借助@nrwl/react这个插件,为我们的lib配置taiwindcss

迄今为止,透过该插件我们

  1. 创建一个名为react-app的appliction类型程式
  2. 创建一个名为ui-taib-ui的lib类型程式
  3. 为libui-taib-ui创建了button组件
  4. 为libui-taib-ui配置了storybook
  5. (即将)为libui-taib-ui配置Tailwindcss

为现有lib配置Taiwindcss

pnpm exec nx generate @nrwl/react:setup-tailwind ui-taib-ui

该命令实质就是向package.json -> devDependencies 添加了postcss 和taiwindcss,并在程式集根目录添加postcss.config.js和taiwindcss.config.js文件

缺点一,由于我们是lib类型的程式,且css style我们选择了None ,Taiwindcss的样式是没有实际引入到我们的组件中的,在后期打包的过程中我们仍需手动的在入口文件引入Tw样式

而官方的插件又暂时没有完整配置,此时我们就可考虑使用自定义插件,改进@nrwl/react的工作流

缺点二,目前的Taiwindcss配置完全无法在Storybook生效,仍需手动引入Tailwindcss样式

my command panel

 pnpm exec nx generate @nrwl/react:setup-tailwind ui-taib-ui

>  NX  Generating @nrwl/react:setup-tailwind                                                                             
Could not find stylesheet to update. Add the following imports to your stylesheet (e.g. styles.css):

@tailwind base;
@tailwind components;
@tailwind utilities;

See our guide for more details: https://nx.dev/guides/using-tailwind-css-in-react
CREATE libs/ui/taib-ui/postcss.config.js
CREATE libs/ui/taib-ui/tailwind.config.js
UPDATE package.json

当然我们先手动解决当前的问题

在button.tsx添加tw 样式

/* eslint-disable-next-line */
export interface ButtonProps {}

export function Button(props: ButtonProps) {
  return (
    <div>
      <h1 className="text-red-500">Welcome to Button!</h1>  // add tw styles
    </div>
  );
}

export default Button;

运行storybook开发测试当前程式

pnpm exec nx storybook ui-taib-ui
  • 由我们选择的Webpack构建起Storybook

此时Taiwindcss样式并没有生效(字体颜色应该变红)

微信图片编辑_20230403034305.jpg

解决步骤

// 当前目录结构
taib-ui
├─ .storybook
│  ├─ main.ts
│  ├─ preview.ts
│  └─ tsconfig.json
├─ jest.config.ts
├─ package.json
├─ postcss.config.js
├─ project.json
├─ README.md
├─ src
│  ├─ ...
├─ tailwind.config.js
├─ ...

在当前lib级程式目录下.stroybook引入TW样式

新建一个 import-tw.css的文件

@tailwind base; 
@tailwind components;
@tailwind utilities;

修改 taib-ui/.storybook/preview.ts

import './import-tw.css';

此时如果你已经运行了Storybook服务,因为你修改了配置文件,所以需要重新启动它

终止你的进程,重新运行pnpm exec nx storybook ui-taib-ui

在命令行我们可以看到,Nx的Cache 自动忽略掉了我们刚添加的文件,你的storybook构建得很快

info => Ignoring cached manager due to change in C:\software\webapp\space\juejin\libs\ui\taib-ui\.storybook\import-tw.css

而我们的Tw样式也在storybook中正常显示(字体变红)

7777.png

额外的Tailwindcss配置

虽然Taiwindcss已经生效于Storybook中,也能满足开发,但工程化,不嫌配置多。。。个人使用仍需额外的配置,如Prettier-Plugin-taiwindcss,可对你随手编写的TW类进行Sort

效果展示

<!-- Before --> 
<button class="text-white px-4 sm:px-8 py-2 sm:py-3 bg-sky-700 hover:bg-sky-800">...</button>
<!-- After -->
<button class="bg-sky-700 px-4 py-2 text-white hover:bg-sky-800 sm:px-8 sm:py-3">...</button>

或者在工作区级的目录下新建一个taiwin-workspace-preset.config.js

在该配置文件下启用Taiwindcss的通用插件,如@tailwindcss/typography,主题颜色配置,响应式断点设置等等

这样在你新建下一个组件库时,就能避免重复的TW-config

小总结

以上我们主要使用了Nx插件的Generators类,和一小部分Executors(如运行storybook),我们发现了两个仍需手动配置的缺点。以及自定义taiwindcss.config.js的需求。

自定义插件提前认知

以上的所有配置,都可自己编写插件,一行命令配置自己的初始化lib

阅读本系列文章

你可以通过诸如

pnpm exec nx g @taibui/devkit:init

完成以下配置

  • 通过自定义插件创建共享React组件库,配置正确的Storybook和jest、配置自定义需求的Tailwindcss

当然,一切自动的前提,是你了解手动。

tips 为taiwindcss 正名,有人极度厌恶,而我视之为利器,目前已使用一年,退可配合php陶冶情操,进可配合Framer-motion写出复杂的动画页面。至于不好维护的问题,后续的文章我会和大家分享一些HeadlessUI + tailwindcss构建组件库的经验 ,用过了就真的回不去了^-^

手动配置Taiwindcss的额外选项

在工作区新建taiwind-workspace-preset.config.js

我们设置了一个主题色和启用taiwind的官方插件typography

 pnpm add -D @tailwindcss/typography

工作区级TW配置文件

module.exports = {
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: '#00d1b2',
        },
      },
    },
  },
  variants: {
    extend: {},
  },
  plugins: [require('@tailwindcss/typography')],
};

在程式级的taiwind配置文件继承引入工作区级配置

module.exports = {
...
  presets: [require('../../../taiwind-worksapce-preset.config.js')],
  ...
}
组件库使用工作区级颜色配置

微信截图_20230403050011.png

重启Storybook查看配置是否生效

88888.png

使用Rollup打包我们的组件库

可执行打包命令

pnpm exec nx  build ui-taib-ui

但我们的构建产物是没有Tw类的,因为我们压根就没有在入口文件引入样式,只是在Storybook引入了。

因此仍需手动的引入Tw类

在入口文件同级目录下新建import-tw.css

@tailwind base;
@tailwind components;
@tailwind utilities;

入口文件引入css

import './import-tw.css';

export * from './lib/button/button';
export * from './lib/ui-taib-ui';

9999.png

尝试打包组件库

现在我们的组件库默认的构建器是rollup,而当我们执行打包命令,查看构建产物时

这不如我意,css没压缩,css是额外目录,我还想要打包cjs,我后期的开发我还要引入Rollup插件。。。

12132132444.png

甚至是当你选择编译器为SWC时,他没有ts类型声明r文件构建产物

4456546.png

没错,我可能有各种需求,而当前的插件提供的默认配置不能满足我,我需要自定义Rollup配置!!!

手动配置Rollup

在程式级根目录下新建custom-rollup.config.js

  • 将css启用压缩
  • 不把css作为额外的文件
const postcss = require('rollup-plugin-postcss');
const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');

// const typescript = require('@rollup/plugin-typescript');

const path = require('path');

module.exports = (config) => {
  const oldPlugins = config.plugins;
  const changePlugins = [
    postcss({
      plugins: [
        tailwindcss(path.join(__dirname, 'tailwind.config.js')),
        autoprefixer(),
      ],
      inject: true,
      extract: false,
      minimize: true,
      autoModules: false,
    }),
  ];

  const newPlugins = oldPlugins.map((item) => {
    const addPlugin = changePlugins.find((item2) => item2.name === item.name);
    return {
      ...item,
      ...addPlugin,
    };
  });

  const newConfig = {
    input: config.input,
    output: [
      config.output,
      {
        format: 'cjs',
        dir: 'C:\\software\\webapp\\space\\juejin/dist/libs/ui/taib-ui',
        name: 'UiTaibUi',
        entryFileNames: '[name].cjs',
        chunkFileNames: '[name].cjs',
      },
    ],
    plugins: [
      ...newPlugins,
      //   typescript({
      //     tsconfig: path.join(__dirname, 'tsconfig.lib.json'),
      //   }),
    ],
  };

  return newConfig;
};

此时的构建产物

456547.png

自定义Nx插件,完成以上所有步骤

To be continued...

自定义插件√ 请移步主页