如何在 Monorepo 中制作支持多个框架的组件?

需求 – 要求构建一个 Button 组件在四个框架中使用,但是,却只使用一个公共的按钮 css 文件!

这个想法对我来说非常重要。 我一直在开发一个名为 AgnosticUI 的组件库,其目的是构建不依赖于任何特定 JavaScript 框架的 UI 组件。 AgnosticUI 适用于 React、Vue 3、Angular 和 Svelte。 这正是我们今天在本文中要做的事情:构建一个可跨所有这些框架工作的按钮组件。

为什么是 Monorepo?

接下来,我们将建立一个基于 Yarn 工作区的小型 monorepo。 我认为这样的好处有如下几方面:

1. 耦合

我们正在尝试构建一个跨多个框架仅使用 onebutton.css 文件的单个按钮组件。 因此,从本质上讲,各种框架实现和单一真实来源 CSS 文件之间存在一些有目的的耦合。 monorepo 设置提供了一种方便的结构,有助于将我们的 singlebutton.css 组件复制到各种基于框架的项目中。

2. 工作流程

假设按钮需要调整——比如“焦点环”的实现,或者我们搞砸了组件模板中的 aria 的使用。 理想情况下,我们希望在一个地方纠正问题,而不是在单独的存储库中进行单独的修复。

3. 测试

我们希望可以方便地同时启动所有四个按钮实现来进行测试。 随着此类项目的发展,可以肯定会有更多适当的测试。 例如,在 AgnosticUI 中,我目前正在使用 Storybook,并且经常启动所有框架 Storybook,或者在整个 monorepo 上运行快照测试。

我相信当所有包都使用相同的编程语言编码、紧密耦合并依赖相同的工具时,monorepo 特别有用。

4. 配置

是时候深入研究代码了——首先在命令行上创建一个顶级目录来容纳项目,然后进入它。

首先,让我们初始化项目:

$ yarn init
yarn init v1.22.15
question name (articles): littlebutton
question version (1.0.0): 
question description: my little button project
question entry point (index.js): 
question repository url: 
question author (Rob Levin): 
question license (MIT): 
question private: 
success Saved package.json

这给了我们一个 package.json 文件,其中包含如下内容:

{
  "name": "littlebutton",
  "version": "1.0.0",
  "description": "my little button project",
  "main": "index.js",
  "author": "Rob Levin",
  "license": "MIT"
}

创建基础工作区

我们可以使用以下命令设置第一个:

mkdir -p ./littlebutton-css

接下来,我们需要将以下两行添加到 monorepo 的顶级 package.json 文件中,以便我们保持 monorepo 本身的私有性。 它还声明了我们的工作区:

// ...
"private": true,
"workspaces": ["littlebutton-react", "littlebutton-vue", "littlebutton-svelte", "littlebutton-angular", "littlebutton-css"]

现在进入 littlebutton-css 目录。 我们将再次使用 yarn init 生成一个 package.json。 由于我们已将目录命名为 littlebutton-css(与我们在 package.json 中的 workspaces 中指定的方式相同),我们只需按回车键并接受所有提示即可:

$ cd ./littlebutton-css && yarn init
yarn init v1.22.15
question name (littlebutton-css): 
question version (1.0.0): 
question description: 
question entry point (index.js): 
question repository url: 
question author (Rob Levin): 
question license (MIT): 
question private: 
success Saved package.json

此时,目录结构应该如下所示:

├── littlebutton-css
│   └── package.json
└── package.json

此时我们仅创建了 CSS 包工作区,因为我们将使用 vite 等工具生成框架实现,这些工具反过来会为您生成 package.json 和项目目录。 我们必须记住,我们为这些生成的项目选择的名称必须与我们在 package.json 中指定的名称相匹配,以便我们之前的工作区能够正常工作。

HTML 和 CSS

让我们留在 ./littlebutton-css 工作区并使用普通 HTML 和 CSS 文件创建简单的按钮组件。

touch index.html ./css/button.css

现在我们的项目目录应该如下所示:

littlebutton-css
├── css
│   └── button.css
├── index.html
└── package.json

让我们继续用 ./index.html 中的一些样板 HTML 连接一些点:




  
  The Little Button That Could
  
  
  


  

为了方便测试我们可以在 ./css/button.css 中添加一点颜色:

.btn {
  color: hotpink;
}

此时效果:

如何在 Monorepo 中制作支持多个框架的组件?_第1张图片

现在在浏览器中打开该 index.html 页面。 如果您看到带有粉红色文本的丑陋通用按钮……成功!

特定于框架的工作区

所以我们刚刚完成的是按钮组件的基线。 我们现在要做的就是对其进行一些抽象,以便它可以扩展到其他框架等。 例如,如果我们想在 React 项目中使用按钮怎么办? 我们需要在 monorepo 中为每个项目提供工作空间。 我们将从 React 开始,然后依次介绍 Vue 3、Angular 和 Svelte。

React

我们将使用 vite 生成 React 项目,vite 是一个非常轻量级且速度极快的构建器。 预先警告一下,如果你尝试使用 create-react-app 来做到这一点,那么你很可能会在以后遇到与 React-scripts 的冲突以及来自其他框架(如 Angular)的 Webpack 或 Babel 配置的冲突。

为了让我们的 React 工作区继续运行,让我们返回终端并 cd 回到顶级目录。 从那里,我们将使用 vite 初始化一个新项目 – 让我们称之为 LittleButton-React – 当然,我们会在提示时选择 React 作为框架:

$ yarn create vite
yarn create v1.22.15
[1/4]   Resolving packages...
[2/4]   Fetching packages...
[3/4]   Linking dependencies...
[4/4]   Building fresh packages...

success Installed "[email protected]" with binaries:
      - create-vite
      - cva
✔ Project name: … littlebutton-react
✔ Select a framework: › react
✔ Select a variant: › react

Scaffolding project in /Users/roblevin/workspace/opensource/guest-posts/articles/littlebutton-react...

Done. Now run:

  cd littlebutton-react
  yarn
  yarn dev

✨  Done in 17.90s.

接下来我们使用以下命令初始化 React 应用程序:

cd littlebutton-react
yarn
yarn dev

安装并验证 React 后,让我们用以下代码替换 src/App.jsx 的内容以容纳我们的按钮:

import "./App.css";

const Button = () => {
  return ;
};

function App() {
  return (
    
); } export default App;

现在我们将编写一个小 Node 脚本,将 Littlebutton-css/css/button.css 直接复制到我们的 React 应用程序中。 这一步对我来说可能是最有趣的一步,因为它既神奇又丑陋。 这很神奇,因为这意味着我们的 React 按钮组件真正从基线项目中编写的相同 CSS 派生出其样式。 这很丑陋,因为我们正在从一个工作区伸出手来,从另一个工作区获取文件。

将以下小 Node 脚本添加到 littlebutton-react/copystyles.js:

const fs = require("fs");
let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
fs.writeFileSync("./src/button.css", css, "utf8");

让我们在 package.json 脚本中放置一个节点命令来运行该命令,该脚本发生在 littlebutton-react/package.json 中的 dev 脚本之前。 我们将添加一个 syncStyles 并更新开发以在 vite 之前调用 syncStyles:

"syncStyles": "node copystyles.js",
"dev": "yarn syncStyles && vite",

现在,每当我们使用 yarn dev 启动 React 应用程序时,我们都会首先复制 CSS 文件。 本质上,我们“强迫”自己不要偏离 React 按钮中 CSS 包的 button.css。

但我们还想利用 CSS Modules 来防止名称冲突和全局 CSS 泄漏,因此我们还需要执行一个步骤来将其连接起来(来自同一个 littlebutton-react 目录):

touch src/button.module.css

接下来,将以下内容添加到新的 src/button.module.css 文件中:

.btn {
  composes: btn from './button.css';
}

我发现组合是 CSS 模块最酷的功能之一。 简而言之,我们正在批量复制 Button.css 的 HTML/CSS 版本,然后根据我们的一个 .btn 样式规则进行组合。

这样,我们可以返回到 src/App.jsx 并将 CSS 模块样式导入到我们的 React 组件中:

import "./App.css";
import styles from "./button.module.css";

const Button = () => {
  return ;
};

function App() {
  return (
    
); } export default App;

哇! 让我们暂停一下并尝试再次运行我们的 React 应用程序:

yarn dev

到这儿,如果一切顺利,您应该会看到相同的通用按钮,但带有亮粉色文本。 在继续讨论下一个框架之前,让我们返回到顶级 monorepo 目录并更新其 package.json:

{
  "name": "littlebutton",
  "version": "1.0.0",
  "description": "toy project",
  "main": "index.js",
  "author": "Rob Levin",
  "license": "MIT",
  "private": true,
  "workspaces": ["littlebutton-react", "littlebutton-vue", "littlebutton-svelte", "littlebutton-angular"],
  "scripts": {
    "start:react": "yarn workspace littlebutton-react dev"
  }
}

从顶级目录运行 yarn 命令以安装 monorepo 提升的依赖项。

我们对此 package.json 所做的唯一更改是一个新的脚本部分,其中包含一个用于启动 React 应用程序的脚本。 通过添加 start:react,我们现在可以从顶级目录运行 yarn start:react,它将启动我们刚刚在 ./littlebutton-react 中构建的项目,而不需要 cd , 超级方便!

接下来我们将讨论 Vue 和 Svelte。 事实证明,我们可以对它们采取非常相似的方法,因为它们都使用单文件组件 (SFC)。 基本上,我们可以将 HTML、CSS 和 JavaScript 全部混合到一个文件中。 无论您是否喜欢 SFC 方法,它对于构建演示或原始 UI 组件来说肯定足够了。

Vue

按照 vite 脚手架文档中的步骤,我们将从 monorepo 的顶级目录运行以下命令来初始化 Vue 应用程序:

yarn create vite littlebutton-vue --template vue

这会生成脚手架,其中包含一些提供的说明来运行入门 Vue 应用程序:

cd littlebutton-vue
yarn
yarn dev

这应该会在浏览器中启动一个起始页面,其中包含一些标题,例如“Hello Vue 3 + Vite”。 从这里,我们可以将 src/App.vue 更新为:



我们将用 src/components/Button.vue 替换任何 src/components/* :





让我们稍微分解一下:

  • :class=”classes” 使用 Vue 的绑定来调用计算类方法。
  • 反过来,classes 方法通过 this.$style.btn 语法利用 Vue 中的 CSS 模块,该语法将使用 `); fs.writeFileSync("./src/components/Button.vue", withSynchronizedStyles, "utf8");

    这个脚本有点复杂,但是使用替换通过正则表达式在开始和结束样式标签之间复制文本也不错。

    现在让我们将以下两个脚本添加到 littlebutton-vue/package.json 文件中的 scripts 子句中:

    "syncStyles": "node copystyles.js",
    "dev": "yarn syncStyles && vite",

    现在运行yarn syncStyles并再次查看./src/components/Button.vue。 您应该看到我们的样式模块被替换为:

    使用 yarn dev 再次运行 Vue 应用程序并验证您是否获得了预期的结果——是的,一个带有粉红色文本的按钮。 如果是这样,我们就可以进入下一个框架工作区了!

    Svelte

    根据 Svelte 文档,我们应该从 monorepo 的顶级目录开始,使用以下内容启动我们的 littlebutton-svelte 工作区:

    npx degit sveltejs/template littlebutton-svelte
    cd littlebutton-svelte
    yarn && yarn dev

    确认您可以访问 http://localhost:5000 的“Hello World”起始页。 然后,更新 littlebutton-svelte/src/App.svelte:

    
    

    另外,在littlebutton-svelte/src/main.js中,我们要删除 name 属性,使其看起来像这样:

    import App from './App.svelte';
    
    const app = new App({
      target: document.body
    });
    
    export default app;

    最后,在littlebutton-svelte/src/Button.svelte下添加,内容如下:

    
    
    
    
    

    最后一件事:Svelte 似乎在 package.json 中命名我们的应用程序:"name":"svelte-app"。 将其更改为"name":"littlebutton-svelte",使其与顶级 package.json 文件中的工作区名称一致。

    再次,我们可以将基准littlebutton-css/css/button.css复制到我们的 Button.svelte 中。 如前所述,该组件是一个 SFC,因此我们必须使用正则表达式来完成此操作。 将以下节点脚本添加到littlebutton-svelte/copystyles.js

    const fs = require("fs");
    let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
    const svelte = fs.readFileSync("./src/Button.svelte", "utf8");
    const styleRegex = /`);
    fs.writeFileSync("./src/Button.svelte", withSynchronizedStyles, "utf8");

    这与我们在 Vue 中使用的复制脚本非常相似, 然后,我们将在 package.json 脚本中添加类似的脚本:

    "dev": "yarn syncStyles && rollup -c -w",
    "syncStyles": "node copystyles.js",

    现在运行 yarn syncStyles && yarn dev。 如果一切顺利,我们应该再次看到一个带有亮粉色文本的按钮。

    如果这开始让你觉得重复,我想说的是欢迎来到我的世界。 我在这里向您展示的过程本质上与我用来构建 AgnosticUI 项目的过程相同!

    Angular

    你现在可能已经知道该怎么做了。 从 monorepo 的顶级目录中,安装 Angular 并创建一个 Angular 应用程序。 如果我们要创建一个成熟的 UI 库,我们可能会使用 ng 生成库甚至 nx。 但为了让事情尽可能简单,我们将设置一个样板 Angular 应用程序,如下所示:

    npm install -g @angular/cli ### unless you already have installed
    ng new littlebutton-angular ### choose no for routing and CSS
    ? Would you like to add Angular routing? (y/N) N
    ❯ CSS 
      SCSS   [ https://sass-lang.com/documentation/syntax#scss ] 
      Sass   [ https://sass-lang.com/documentation/syntax#the-indented-syntax ] 
      Less   [ http://lesscss.org ]
    
    cd littlebutton-angular && ng serve --open

    确认 Angular 设置后,让我们更新一些文件。 cd Littlebutton-Angular,删除 src/app/app.component.spec.ts 文件,并在 src/components/button.component.ts 中添加按钮组件,如下所示:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'little-button',
      templateUrl: './button.component.html',
      styleUrls: ['./button.component.css'],
    })
    export class ButtonComponent {}

    将以下内容添加到 src/components/button.component.html 中:

    .btn {
      color: fuchsia;
    }

    在 src/app/app.module.ts 中:

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    
    import { AppComponent } from './app.component';
    import { ButtonComponent } from '../components/button.component';
    
    @NgModule({
      declarations: [AppComponent, ButtonComponent],
      imports: [BrowserModule],
      providers: [],
      bootstrap: [AppComponent],
    })
    export class AppModule {}

    接下来,将 src/app/app.component.ts 替换为:

    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
    })
    export class AppComponent {}

    然后,将 src/app/app.component.html 替换为:

    Go

    有了这个,让我们运行yarn start并验证我们的按钮与紫红色文本是否按预期渲染。

    同样,我们想要从基准工作区复制 CSS。 我们可以通过将其添加到littlebutton-angular/copystyles.js来做到这一点:

    const fs = require("fs");
    let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
    fs.writeFileSync("./src/components/button.component.css", css, "utf8");

    Angular 的优点在于它使用 ViewEncapsulation,默认情况下会模拟模仿,根据文档,

    […] Shadow DOM 的行为,通过预处理(和重命名)CSS 代码来有效地将 CSS 范围限制到组件的视图。

    这基本上意味着我们可以直接复制 button.css 并按原样使用它。

    最后,通过在脚本部分添加这两行来更新 package.json 文件:

    "start": "yarn syncStyles && ng serve",
    "syncStyles": "node copystyles.js",

    这样,我们现在可以再次运行 yarn start,并验证我们的按钮文本颜色(紫红色)现在是亮粉色。

    我们刚刚做了什么?

    让我们从编码中休息一下,思考一下大局以及我们刚刚所做的事情。 基本上,我们已经建立了一个系统,其中对 CSS 包的 button.css 的任何更改都将作为 copystyles.js 节点脚本的结果复制到所有框架实现中。 此外,我们为每个框架纳入了惯用约定:

    • Vue 和 Svelte 的 SFC
    • React 的 CSS 模块(以及 SFC