需求 – 要求构建一个 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 连接一些点:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>The Little Button That Could</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/button.css">
</head>
<body>
<main>
<button class="btn">Go</button>
</main>
</body>
</html>
为了方便测试我们可以在 ./css/button.css 中添加一点颜色:
.btn {
color: hotpink;
}
此时效果:
现在在浏览器中打开该 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 "create-vite@2.6.6" 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 <button>Go</button>;
};
function App() {
return (
<div className="App">
<Button />
</div>
);
}
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 <button className={styles.btn}>Go</button>;
};
function App() {
return (
<div className="App">
<Button />
</div>
);
}
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 更新为:
<template>
<div id="app">
<Button class="btn">Go</Button>
</div>
</template>
<script>
import Button from './components/Button.vue'
export default {
name: 'App',
components: {
Button
}
}
</script>
我们将用 src/components/Button.vue 替换任何 src/components/* :
<template>
<button :class="classes"><slot /></button>
</template>
<script>
export default {
name: 'Button',
computed: {
classes() {
return {
[this.$style.btn]: true,
}
}
}
}
</script>
<style module>
.btn {
color: slateblue;
}
</style>
让我们稍微分解一下:
- :class=”classes” 使用 Vue 的绑定来调用计算类方法。
- 反过来,classes 方法通过 this.$style.btn 语法利用 Vue 中的 CSS 模块,该语法将使用 <style module> 标记中包含的样式。
目前,我们对颜色进行硬编码:slateblue 只是为了测试组件内的工作是否正常。 尝试使用 yarn dev 再次启动应用程序。 如果您看到带有我们声明的测试颜色的按钮,那么它就可以工作了!
现在我们将编写一个 Node 脚本,将 littlebutton-css/css/button.css 复制到 Button.vue 文件中,类似于我们为 React 实现所做的操作。 如前所述,该组件是一个 SFC,因此我们必须使用简单的正则表达式以稍微不同的方式执行此操作。
将以下 Node.js 小脚本添加到 littlebutton-vue/copystyles.js:
const fs = require("fs");
let css = fs.readFileSync("../littlebutton-css/css/button.css", "utf8");
const vue = fs.readFileSync("./src/components/Button.vue", "utf8");
// Take everything between the starting and closing style tag and replace
const styleRegex = /<style module>([\s\S]*?)<\/style>/;
let withSynchronizedStyles = vue.replace(styleRegex, `<style module>\n${css}\n</style>`);
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。 您应该看到我们的样式模块被替换为:
<style module>
.btn {
color: hotpink;
}
</style>
使用 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:
<script>
import Button from './Button.svelte';
</script>
<main>
<Button>Go</Button>
</main>
另外,在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
下添加,内容如下:
<button class="btn">
<slot></slot>
</button>
<script>
</script>
<style>
.btn {
color: saddlebrown;
}
</style>
最后一件事: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 = /<style>([\s\S]*?)<\/style>/;
let withSynchronizedStyles = svelte.replace(styleRegex, `<style>\n${css}\n</style>`);
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
替换为:
<main>
<little-button>Go</little-button>
</main>
有了这个,让我们运行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 <style module> 设置中的 Vue)
- Angular 的视图封装
当然,我很明显地指出,这些并不是在上述每个框架中执行 CSS 的唯一方法(例如,CSS-in-JS 是一个流行的选择),但它们无疑是公认的实践,并且对于我们更大的框架来说效果很好。 目标——拥有单一 CSS 事实来源来驱动所有框架实现。
例如,如果我们的按钮正在使用中,并且我们的设计团队决定将边框半径从 4px 更改为 3px,我们可以更新一个文件,并且任何单独的实现都将保持同步。
如果您有一个多语言开发人员团队,喜欢在多个框架中工作,或者说一个离岸团队(Angular 的生产力是 3 倍)负责构建后台应用程序,但您的旗舰产品是创建使用 React。 或者,您正在构建一个临时管理控制台,并且希望尝试使用 Vue 或 Svelte。 你明白了。
收尾工作
好的,我们的 monorepo 架构处于一个非常好的位置。 但就开发者体验而言,我们可以做一些事情来使其更加有用。
更好的启动脚本
让我们回到顶级 monorepo 目录,并使用以下内容更新其 package.json 脚本部分,这样我们就可以在无需 cd 的情况下启动任何框架实现:
// ...
"scripts": {
"start:react": "yarn workspace littlebutton-react dev",
"start:vue": "yarn workspace littlebutton-vue dev ",
"start:svelte": "yarn workspace littlebutton-svelte dev",
"start:angular": "yarn workspace littlebutton-angular start"
},
更好的基础样式
我们还可以为按钮提供一组更好的基础样式,以便它从一个漂亮、中性的位置开始。 这是我在 littlebutton-css/css/button.css 文件中所做的事情。
.btn {
--button-dark: #333;
--button-line-height: 1.25rem;
--button-font-size: 1rem;
--button-light: #e9e9e9;
--button-transition-duration: 200ms;
--button-font-stack:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Ubuntu,
"Helvetica Neue",
sans-serif;
display: inline-flex;
align-items: center;
justify-content: center;
white-space: nowrap;
user-select: none;
appearance: none;
cursor: pointer;
box-sizing: border-box;
transition-property: all;
transition-duration: var(--button-transition-duration);
color: var(--button-dark);
background-color: var(--button-light);
border-color: var(--button-light);
border-style: solid;
border-width: 1px;
font-family: var(--button-font-stack);
font-weight: 400;
font-size: var(--button-font-size);
line-height: var(--button-line-height);
padding-block-start: 0.5rem;
padding-block-end: 0.5rem;
padding-inline-start: 0.75rem;
padding-inline-end: 0.75rem;
text-decoration: none;
text-align: center;
}
/* Respect users reduced motion preferences */
@media (prefers-reduced-motion) {
.btn {
transition-duration: 0.001ms !important;
}
}
让我们测试一下! 使用新的和改进的启动脚本启动四个框架实现中的每一个,并确认样式更改已生效。
一个 CSS 文件更新扩散到四个框架 – 很酷,嗯!?
设置主要模式
我们将为每个按钮添加一个模式道具,并接下来实现主要模式。 主按钮可以是任何颜色,但我们将使用绿色阴影作为背景和白色文本。 同样,在基础样式表中:
.btn {
--button-primary: #14775d;
--button-primary-color: #fff;
/* ... */
}
然后,在 @media (prefers-reduced-motion)
查询之前,将以下 btn-primary 添加到同一基础样式表中:
.btn-primary {
background-color: var(--button-primary);
border-color: var(--button-primary);
color: var(--button-primary-color);
}
更新每个组件以采用模式属性
现在我们已经添加了由 .btn-primary 类表示的新主要模式,我们希望同步所有四个框架实现的样式。 因此,让我们向顶级脚本中添加更多 package.json 脚本:
"sync:react": "yarn workspace littlebutton-react syncStyles",
"sync:vue": "yarn workspace littlebutton-vue syncStyles",
"sync:svelte": "yarn workspace littlebutton-svelte syncStyles",
"sync:angular": "yarn workspace littlebutton-angular syncStyles"
请务必遵守 JSON 的逗号规则! 根据您在脚本中放置这些行的位置:{…},您需要确保没有丢失或尾随逗号。
继续运行以下命令以完全同步样式:
yarn sync:angular && yarn sync:react && yarn sync:vue && yarn sync:svelte
运行这个不会改变任何东西,因为我们还没有应用主类,但是如果你查看框架的按钮组件 CSS,你至少应该看到 CSS 已经被复制了。
React
如果您还没有这样做,请仔细检查更新的 CSS 是否已复制到littlebutton-react/src/button.css
中。 如果没有,您可以运行yarn syncStyles
。 请注意,如果您忘记运行yarn syncStyles
,我们的开发脚本将在我们下次启动应用程序时为我们执行此操作:
"dev": "yarn syncStyles && vite",
对于我们的 React 实现,我们还需要在littlebutton-react/src/button.module.css
中添加一个由新的.btn-primary
组成的组合 CSS 模块类:
.btnPrimary {
composes: btn-primary from './button.css';
}
我们还将更新littlebutton-react/src/App.jsx
:
import "./App.css";
import styles from "./button.module.css";
const Button = ({ mode }) => {
const primaryClass = mode ? styles[`btn${mode.charAt(0).toUpperCase()}${mode.slice(1)}`] : '';
const classes = primaryClass ? `${styles.btn} ${primaryClass}` : styles.btn;
return <button className={classes}>Go</button>;
};
function App() {
return (
<div className="App">
<Button mode="primary" />
</div>
);
}
export default App;
使用顶级目录中的 yarn start:react 启动 React 应用程序。 如果一切顺利,您现在应该看到绿色的主按钮。
请注意,为了简洁起见,我将 Button 组件保留在 App.jsx 中。 如果您感到困扰,请随意将 Button 组件整理到自己的文件中。
Vue
再次仔细检查按钮样式是否已复制,如果没有,请运行yarn syncStyles
。
接下来,对littlebutton-vue/src/components/Button.vue
的 <script> 部分进行以下更改:
<script>
export default {
name: 'Button',
props: {
mode: {
type: String,
required: false,
default: '',
validator: (value) => {
const isValid = ['primary'].includes(value);
if (!isValid) {
console.warn(`Allowed types for Button are primary`);
}
return isValid;
},
}
},
computed: {
classes() {
return {
[this.$style.btn]: true,
[this.$style['btn-primary']]: this.mode === 'primary',
}
}
}
}
</script>
现在我们可以更新littlebutton-vue/src/App.vue
中的标记以使用新的 mode 属性:
<Button mode="primary">Go</Button>
现在,您可以从顶级目录中执行 yarn start:vue 并检查是否有相同的绿色按钮。
Svelte
让我们进入 littlebutton-svelte 并验证littlebutton-svelte/src/Button.svelte
中的样式是否复制了新的.btn-primary
类,如果需要的话还可以复制yarn syncStyles
。 同样,如果您碰巧忘记了,开发脚本将在下次启动时为我们执行此操作。
接下来,更新 Svelte 模板以传递 Primary 模式。 在 src/App.svelte
中:
<script>
import Button from './Button.svelte';
</script>
<main>
<Button mode="primary">Go</Button>
</main>
我们还需要更新 src/Button.svelte
组件本身的顶部以接受 mode 属性并应用 CSS Modules 类:
<button class="{classes}">
<slot></slot>
</button>
<script>
export let mode = "";
const classes = [
"btn",
mode ? `btn-${mode}` : "",
].filter(cls => cls.length).join(" ");
</script>
请注意,在此步骤中不应触及 Svelte 组件的 <styles> 部分。
现在,您可以从 littlebutton-svelte(或从更高目录中的yarn start:svelte
)执行yarn dev
来确认绿色按钮成功了!
Angular
相同的事情,不同的框架:检查样式是否被复制并运行yarn syncStyles
(如果需要)。
让我们将 mode 属性添加到littlebutton-angular/src/app/app.component.html
文件中:
<main>
<little-button mode="primary">Go</little-button>
</main>
现在我们需要设置类 getter 的绑定,以根据模式是否传递到组件来计算正确的类。 将其添加到littlebutton-angular/src/components/button.component.html
(并注意绑定是通过方括号进行的):
<button [class]="classes">Go</button>
接下来,我们实际上需要在LittleButton-Angular/src/Components/Button.Component.ts
的组件中创建类绑定:
import { Component, Input } from '@angular/core';
@Component({
selector: 'little-button',
templateUrl: './button.component.html',
styleUrls: ['./button.component.css'],
})
export class ButtonComponent {
@Input() mode: 'primary' | undefined = undefined;
public get classes(): string {
const modeClass = this.mode ? `btn-${this.mode}` : '';
return [
'btn',
modeClass,
].filter(cl => cl.length).join(' ');
}
}
我们使用 Input 指令来获取 mode 属性,然后创建一个类访问器,如果已传入模式类,则会添加该模式类。
启动它并寻找绿色按钮!
代码完成
如果您已经完成了这一步,那么恭喜您 – 您已经完成了代码! 如果出现问题,我鼓励您在 GitHub 的 the-little-button-that-could-series 分支上交叉引用源代码。 由于捆绑器和软件包有突然更改的趋势,因此如果您碰巧遇到任何依赖性问题,您可能需要将软件包版本固定到此分支中的版本。
花点时间回顾一下我们刚刚构建的四个基于框架的按钮组件实现。 它们仍然足够小,可以很快注意到一些有趣的差异,例如如何传入 props、如何绑定到 props、以及如何防止 CSS 名称冲突以及其他细微的差异。 当我继续向 AgnosticUI 添加组件(它支持完全相同的四个框架)时,我不断思考哪个可以提供最佳的开发人员体验。 你怎么认为?
结论
希望我已经激起了您的兴趣,并且您现在真的对创建不依赖于特定框架的 UI 组件库和/或设计系统感兴趣。 也许您对如何实现这一目标有更好的想法——我很想在评论中听到您的想法!