发布于 2026-01-06 12 阅读
0

Monorepos:Lerna、TypeScript、CRA 和 Storybook 的结合 让我们偷懒一下:介绍:Lerna 项目设置:UI 组件库包 项目设置:CRA 应用 添加 TypeScript UI 库(使用 Storybook 和 TypeScript) 使用 CRA 添加 TypeScript 从 UI 库加载组件

单体仓库:Lerna、TypeScript、CRA 和 Storybook 的组合

咱们偷懒吧:

隆重介绍:勒纳

项目设置:UI组件库包

项目设置:CRA应用程序

添加 TypeScript

基于 Storybook 和 TypeScript 的 UI 库

TypeScript 和 CRA

从我们的 UI 库中加载组件

咱们偷懒吧:

GitHub 上的仓库。
这是初始仓库的代码。
我还将此仓库设为了模板仓库。
模板按钮的图片

本文详细介绍了原因、如何避免错误以及如何自行操作。如果您想为现有代码库设置单体仓库,或者在扩展单体仓库时遇到错误,本文将非常有用。

已更新至使用 react-scripts v4.0.2!

更新后,模板包含以下内容:

  • 最新的 React@^17.0.1 和 Storybook
  • UI库部分的一些示例故事和组件
  • 这些组件可以使用 CSS 和 SCSS,CSS 会连同类型定义一起构建到输出文件夹中。
  • 修改UI 库会触发 Storybook 热重载,构建UI 库会触发 CRA 热重载。

所以,对于那些不太懒的人来说:

如果你在多个项目中使用过 ReactJS,或者正在构建多个应用,那么你可能已经接触过 lerna 了。由于配置 webpack 可能比较复杂,所以通常会尽可能地选择使用 create-React-app。因此,我们将探讨如何将其与集中式 TypeScript 配置配合使用,该配置也将用于我们的 UI 组件库,我们将把该组件库放在一个单独的仓库中。我们将使用 yarn,因为我们也会用到 yarn workspaces。

yarn init

我们将私有包作为单体仓库的根目录。为什么要设为私有包?因为私有包不会发布到 npm,根目录仅用于组织所有内容,最后,定义 yarn workspaces 也只能在私有包中生效。

勒纳标志

隆重介绍:勒纳

首先,你需要安装 lerna,虽然你可以全局安装,但我建议将其安装在你的 monorepo 中,除非你(以及你的 monorepo 的贡献者)想要使用 lerna 创建很多 monorepo,并且它是你的标准工具集的一部分。

yarn add lerna -D

现在我们有了 Lerna,它为单体仓库提供了组织工具。例如初始化:

yarn lerna init

这将创建一个lerna.json文件和一个packages文件夹。由于我们将使用 yarn workspaces,因此需要将 yarn 定义为 ournpmClient并设置useWorkspaces为 true。lerna.json最终我们的配置将如下所示:

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true
}
Enter fullscreen mode Exit fullscreen mode

这就是Lerna所需的所有配置。

由于我们使用的是 yarn workspaces,我们需要修改我们的配置package.json,添加以下内容:

"workspaces": [
    "packages/*"
  ],
Enter fullscreen mode Exit fullscreen mode

注意:你的packages文件夹名称不必是这个。你也可以将 ui-lib、apps 和 server 代码放在不同的子文件夹中。但是,要同时使用 workspaces 和 lerna,你应该在两者中都定义lerna.json它们package.json

Storybookjs 标志

项目设置:UI组件库包

在单体仓库中初始化子包与普通仓库非常相似,只是在设置名称时需要注意一点。你只需切换到相应的目录:

cd packages && mkdir my-monorepo-ui-lib && cd my-monorepo-ui-lib

初始化一个包:

yarn init

但是,名称是……这是利用了npm 组织作用域@my-org/my-monorepo-ui-lib这一特性,如果您想以该组织的身份发布,则需要您在 npmjs.com 上设置一个组织@my-org

这并非强制性的,但它能帮助我们找到开发单体仓库时出现 bug 的原因:

  • 软件包名称并不总是与目录名称相同。
  • 配置文件和脚本参数有时需要包名,有时需要目录名。
  • 即使您从未打算发布,也可以使用此语法。

快速简易的软件包安装

我们希望在 UI 库中构建可复用的 React 组件,但稍后 create-react-app 包会决定我们使用哪个版本的 React。因此,react 和 react-dom 只能作为peerDependencyUI 库的一部分。Storybook 是我们快速测试 React 组件的方式,所以我们会将其添加为一个devDependency

yarn add react react-dom -P
yarn add @storybook/react babel-loader -D

我们一直都是这么做的,对吧?结果发现,现在node_modules我们的ui-lib包里多了一个文件夹,里面有 `<package_name>` 和 ` react<package_name> react-dom`@storybook文件。但我们希望把包放在根目录下,所以 lerna 可以帮助我们从根包目录下实现这一点:

cd ../..
yarn lerna bootstrap

现在根目录下有一个 node_modules 文件夹,其中包含 `<script>` react、` <script>`react-dom和`<script> @storybook` 文件。node_modules我们 ui-lib 包内的文件夹仍然存在,其中包含一个 ` <script>`.bin文件夹,里面存放着 storybook 的命令行(bash/cmd)脚本,用于启动和构建项目。所有执行命令行脚本的工具,例如 storybook、tsc 和 create-react-app,并不一定知道它们是在 monorepo 中运行的,它们在操作系统上执行命令,并且通常是为“普通”的 npm 仓库构建的。

故障排除 bash 和 cmd 脚本:storybook、tsc、react-scripts

在 ui-lib 内部,如果我们尝试运行

yarn start-storybook

它会执行脚本,但会提示我们还没有 Storybook 配置文件:

在“./.storybook/config.{ext}”中创建 Storybook 配置文件

如果我们将其作为脚本添加到 ui-lib 中,也会得到同样的错误package.json(这是理所当然的):

  "scripts": {
    "story": "start-storybook" 
  },
Enter fullscreen mode Exit fullscreen mode

让我们通过创建文件来修复这个错误。packages/my-monorepo-ui-lib/.storybook/config.js

import { configure } from '@storybook/react'

const req = require.context('../src', true, /\.story\.(ts|tsx)$/)

configure(() => {
  req.keys().forEach(filename => req(filename))
}, module);
Enter fullscreen mode Exit fullscreen mode

以及packages/my-monorepo-ui-lib/src一个文件夹,目前可以为空。在我们的 ui-lib 中,运行

yarn start-storybook

yarn story

现在虽然是空的,但运行正常。

一旦我们进入根目录并尝试从那里运行命令行脚本,区别就显而易见了:

cd ../..
yarn start-storybook

我们遇到了和之前一样的错误。原因是根node_modules-folder目录下也包含命令行脚本,并且会尝试查找相对于包的 Storybook 配置。Lerna 在这里也能帮到我们,在根目录下我们可以调用

yarn lerna run story --stream

该命令将并行运行所有包的“story”脚本,并将脚本输出“流式”输出到控制台。这仅适用于所谓的“生命周期脚本”,即在子包中定义的脚本package.json,因此以下命令将无法工作:

yarn lerna run start-storybook

这也是你会看到诸如此类的脚本定义的原因:

"tsc": "tsc",

但为了避免混淆,最好选择一个不同的名称,尤其是因为很多人会全局安装 tsc 和其他工具。

应用预览版

项目设置:CRA应用程序

将 CRA 与 yarn workspaces 结合使用来创建新软件包时要格外小心:

cd packages
create-react-app my-monorepo-cra-app

这将引发错误,因为CRA会将文件复制到node_modules其安装文件夹(此处为 `<path> packages/my-monorepo-cra-app/node_modules`)之外,而yarn workspaces则确保所有内容都安装在根node_modules文件夹中。因此,请在根目录中package.json删除`<path>`。

  "workspaces": [
    "packages/*"
  ],
Enter fullscreen mode Exit fullscreen mode

运行 CRA 后再将其添加回去。然后在根文件夹中运行

yarn lerna bootstrap

您的依赖项将被整齐地移动到根目录node_modules。运行

yarn lerna run start --stream

将启动您的 CRA-App,即它的 JavasScript 版本。

Typescript 标志

添加 TypeScript

Monorepo 可以帮助集中管理配置,所以我们会在 Monorepo 的根目录创建一个通用的 tsconfig.json 文件。如果每个子项目都能使用这个文件就太好了,但 CRA 需要对其 TypeScript 配置做一些假设,所以它会添加/覆盖 tsconfig 文件中的值。不过这也不错,因为它不会直接覆盖文件,我们可以从另一个 tsconfig 文件进行扩展。另一方面,在我们的库项目中,我们则更加自由,可以根据需要修改 webpack 配置。

如何构建你的 TypeScript 配置

这个决定取决于你的单体仓库中需要多少个 TypeScript 包以及哪些类型的 TypeScript 包:

  • 一个 CRA 应用,一个 UI 库:Go for
    • 根目录下有一个 tsconfig.json 文件,其中包含一些外观设置,例如removeComments:不与 CRA 冲突且不特定于库的设置,例如库导出。
    • 由此延伸,在您的 CRA 软件包中自动生成。
    • 最后,还有一个用于设置“outDir”:”lib”和配置库声明导出的配置项。这需要与库中的设置相对应package.json
  "main": "./lib/index.js",
  "types": "./lib/index.d.ts",
Enter fullscreen mode Exit fullscreen mode
  • 许多 CRA 应用:结构与上述相同。原因是,目前使用 CRA 意味着您需要重新编译库才能对 CRA 应用进行更改。react-scripts start不过,运行时node_modules也会监视该文件夹,因此您可以tsc在启动 CRA 后以监视模式运行库。
  • 许多库:在根目录创建一个额外的配置文件tsconfig.lib.json,用于统一导出设置。如果您的某个库依赖于另一个库,请查看 TypeScript 的路径映射项目引用功能。

除了 TypeScript 之外,create-react-app 还开箱即用地支持 CSS、SCSS 和 JSON 导入,只需少量配置即可。我们将typings.d.ts在根目录添加一个用于这些类型的文件,以便默认情况下可以导入这些文件类型:

declare module "*.json" {
  const value: any;
  export default value;
}
declare module '*.scss' {
  const content: any;
  export default content;
}
declare module '*.css' {
  interface IClassNames {
    [className: string]: string
  }
  const classNames: IClassNames;
  export = classNames;
}
Enter fullscreen mode Exit fullscreen mode

这是我们能使用的最小 tsconfig.json 文件:

{
  "exclude": ["node_modules"],
  "files":  ["./typings.d.ts"],
  "compilerOptions": {
    "jsx": "react",
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}
Enter fullscreen mode Exit fullscreen mode

我们希望在所有包中使用 TypeScript,这可以通过以下lerna add命令完成:

yarn lerna add typescript -D

我们skipLibCheck也将其包含在内,因为我们希望 tsc 运行速度快。

基于 Storybook 和 TypeScript 的 UI 库

在构建 UI 库时,最好遵循一致的模式。我们的目标是只需运行 `tsc` 就能获得可运行的 JavaScript 代码,如果可以通过清晰的结构避免使用 webpack,则无需使用 webpack。

尤其重要的是:

  • 按用途分离(工具类放在一个文件夹,React 组件放在另一个文件夹)
  • 防止循环导入/导出(工具类在 React 组件之前导出 - 如果使用工厂模式,请不要将它们放在工具类中,而应在 React 组件之后导出)。
  • 方便其他人扩展库(将你的 React 组件与其故事和单元测试放在一起)。

所以你的文件夹结构最终可能看起来像这样:

每个文件夹中都有 index.ts 文件,组件文件夹中有 *.spec.ts 和 *.story.ts 文件。

任何名为 `.py` 的文件index.ts要么是文件树中的叶子节点,导出单元测试后的代码;要么是一个分支,导出其子文件夹。单元测试和用户故事不会被导出,它们的文件可以通过配置从编译后的代码中排除。以下是文件示例:

export * from utils; export * from './myuihelper'

不过,我们确实需要 webpack 来完成一项任务:为 Storybook 配置 TypeScript。既然如此,我们也可以顺便添加对 scss 和其他一些文件类型的支持。

cd packages/my-monorepo-ui-lib
yarn add @babel/core @types/storybook__react awesome-typescript-loader babel-loader node-sass sass-loader source-map-loader style-loader -D

由于我们使用 yarn workspaces,因此不需要引导程序,我们的包可以在根文件node_modules夹中找到。

直接将其添加到包内是解决lerna add与组织范围结合使用时出现的错误的一种变通方法:

lerna 警告:未找到可添加@babel /core 的软件包

更简洁的方法是使用lerna add参数--scope,但这与我们设置组织范围的方式不兼容。命令如下:

yarn lerna add @babel/core @types/storybook__react awesome-typescript-loader babel-loader node-sass sass-loader source-map-loader style-loader --scope=@my-org/my-monorepo-ui-lib -D

您是不是想知道这个--scope参数的作用?
这里,--scope它是安装范围参数,也@my-org就是 npmjs-organization 范围。这样,所有这些包都会被添加到我们的 UI 库包中。

文件 webpack.config.js 位于 packages/my-monorepo-ui-lib/.storybook 中

我们的 UI 库的 webpack 配置相对较短:

const path = require('path');
module.exports = {
  module: {
    rules: [{
        test: /\.scss$/,
        loaders: ["style-loader", "css-loader", "sass-loader"],
        include: path.resolve(__dirname, '../')
      },
      {
        test: /\.css/,
        loaders: ["style-loader", "css-loader"],
        include: path.resolve(__dirname, '../')
      },
      {
        enforce: 'pre',
        test: /\.js$/,
        loader: "source-map-loader",
        exclude: [
          /node_modules\//
        ]
      },
      {
        test: /\.tsx?$/,
        include: path.resolve(__dirname, '../src'),
        loader: 'awesome-typescript-loader',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
        loader: "file-loader"
      }
    ]
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"]
  }
};
Enter fullscreen mode Exit fullscreen mode

我们可以使用一个最小化的 tsconfig.json 文件,它只是继承自根目录下的 tsconfig.json 文件,并将输出放在lib-folder 文件夹中:

{
  "include": [
    "src"
  ],
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "lib",
    "declaration": true
  }
}
Enter fullscreen mode Exit fullscreen mode

这使我们能够编译 TypeScript 文件并运行 Storybook,但我们想做的远不止这些!(以后可能会减少一些工作……)

对于我们的库项目,我们需要生成声明文件(以 *.d.ts 结尾的文件)。否则,我们会收到如下错误:

找不到模块“@my-org/my-monorepo-ui-lib”的声明文件。“.../lerna-typescript-cra-uilib-starter/packages/my-monorepo-ui-lib/lib/index.js”隐式具有“any”类型。my
-monorepo-cra-app:尝试查找该文件,或者添加一个包含TS7016 的npm install @types/my-org__my-monorepo-ui-lib新声明 (.d.ts) 文件。declare module '@my-org/my-monorepo-ui-lib';

澄清一下:我们的构建过程中不使用 Webpack,而是使用 tsc。我们配置的 Webpack 是 Storybook 使用的。

TypeScript 和 CRA

我们集中管理 TypeScript 配置的局限性取决于 create-react-app 对 TypeScript 的使用方式。在撰写本文时,将 CRA 应用从 JavaScript 切换到 TypeScript 的方法是将 index.js 文件更改为 index.tsx 并添加所有必需的依赖项。有关更改,请参阅 CRA 的文档:https://create-react-app.dev/docs/adding-typescript

在我们的 CRA 软件包中,我们运行

yarn add typescript @types/node @types/react @types/react-dom @types/jest -D

tsconfig.json然后我们将ui-lib 中的最小代码复制到 CRA App 包中。如果我们运行

yarn start

现在,CRAcompilerOptions将被添加到我们的tsconfig.json……

从我们的 UI 库中加载组件

现在是时候将我们的 UI 库加载到 CRA 应用中了,运行以下命令即可安装:

yarn lerna add @my-org/my-monorepo-ui-lib

但您可能已经注意到,我们还没有为这个库做太多构建配置。为什么我们没有早点做呢?原因很简单:CRA、Lerna 和 Storybook 都在不断发展,TypeScript、npm 甚至 JavaScript 本身也在不断发展。ES6模块是语言中内置的一项强大的新特性,可以取代之前的模块管理方案。唯一的问题是它还没有被完全采用,但我们希望成为一个优秀的库提供商,所以我们提供了一个备选方案。因此,让我们将库导出为 ES6 模块——以及一个“旧版”的模块管理系统。否则,我们会遇到诸如以下的错误:

意外的标记“export”

如果你想深入了解这个主题,这篇关于 nodejs 模块和 npm 的博客是一个不错的起点。

npm 作为我们的包管理解决方案,早在 ES6 和 TypeScript 兴起之前就已存在,因此我们可以在内部为我们的库项目设置不同的入口点package.json

  • “main”是最古老的,它指向我们ES6之前的导出文件(“./lib/index.js”)。
  • “types”是我们存放类型声明的地方("/lib/index.d.ts")
  • “module”是我们ES6模块的入口点("/lib-esm/index.js")

我们的项目从一开始就使用 TypeScript 编写,因此我们将类型声明打包到我们的包中。如果您看到自己导入了@types某些包,那是因为这些项目的核心是用 JavaScript 编写的,类型定义是后来添加的。

因此,我们设置了一个tsconfig.esm.json导出为 ES6 模块的功能:

{
  "include": [
    "src"
  ],
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "lib-esm",
    "module": "esnext",
    "target": "esnext",
    "moduleResolution": "node",
    "lib": ["dom", "esnext"],
    "declaration": false
  }
}
Enter fullscreen mode Exit fullscreen mode

这样做会执行以下操作:

  • 我们的模块将放入lib-esm-folder 中,该文件夹是我们指定的module-entrypoint package.json
  • 我们的模块解析策略是“node”。如果不设置,我们会收到类似这样的错误:

src/index.ts:1:15 - 错误 TS2307:找不到模块“./utils”。
1 export * from './utils';

这样一来,我们的库就拥有一个支持最新 JavaScript 特性的导出版本和一个向下兼容的导出版本,从而能够服务更广泛的用户群体。需要注意的是,为了确保与不同浏览器的兼容性,我们最终的 App 在底层使用了 Babel。

我们已经在lib-folder 中发出了声明,所以这里不会再发出一次。

最后,我们将在库中添加一个库构建脚本package.json

"libbuild": "tsc && tsc --build tsconfig.esm.json"
Enter fullscreen mode Exit fullscreen mode

我们准备将库包添加到 CRA 包中。我们可以为包版本设置通配符,使其始终为最新版本。

"dependencies": {
    "@my-org/my-monorepo-ui-lib": "*",
Enter fullscreen mode Exit fullscreen mode

现在,我们可以在 CRA 应用中添加来自库的组件,并且该组件已完全进行类型检查:

<MyNewComponent text=在 React 渲染函数中”/>

而且由于 monorepos 应该能让我们的工作更轻松,我们将在根目录中添加脚本package.json来启动 storybook,并在启动 CRA 应用之前执行库构建:

  "scripts": {
    "story": "lerna run story --stream",
    "prestart": "lerna run libbuild --stream",
    "start": "lerna run start --stream"
  }
Enter fullscreen mode Exit fullscreen mode

希望这能避免您在使用 monorepo-setup 时可能遇到的大多数常见错误。如果您有其他建议,欢迎在评论区留言!

文章来源:https://dev.to/shnydercom/monorepos-lerna-typescript-cra-and-storybook-combined-4hli