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
迄今为止,透过该插件我们
- 创建一个名为
react-app的appliction类型程式 - 创建一个名为
ui-taib-ui的lib类型程式 - 为lib
ui-taib-ui创建了button组件 - 为lib
ui-taib-ui配置了storybook - (即将)为lib
ui-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样式并没有生效(字体颜色应该变红)
解决步骤
// 当前目录结构
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中正常显示(字体变红)
额外的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')],
...
}
组件库使用工作区级颜色配置
重启Storybook查看配置是否生效
使用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';
尝试打包组件库
现在我们的组件库默认的构建器是rollup,而当我们执行打包命令,查看构建产物时
这不如我意,css没压缩,css是额外目录,我还想要打包cjs,我后期的开发我还要引入Rollup插件。。。
甚至是当你选择编译器为SWC时,他没有ts类型声明r文件构建产物
没错,我可能有各种需求,而当前的插件提供的默认配置不能满足我,我需要自定义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;
};
此时的构建产物
自定义Nx插件,完成以上所有步骤
To be continued...
自定义插件√ 请移步主页