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

使用 StencilJS 构建您自己的设计系统

使用 StencilJS 构建您自己的设计系统

注:本文最初发布于marmelab.com

本文将介绍StencilJS是什么,以及如何使用它来构建设计系统。首先需要了解的是,StencilJS不是一个框架,而是一个编译器,它可以生成可重用的 Web 组件,这些组件可以嵌入到任何地方。

什么是设计系统?

许多出版商都有视觉形象规范,定义了一系列关于其视觉识别的规则。

鉴于我们社会的技术发展,这些规则越来越多地、主要地与数字媒体有关,尤其是网络。

近几年来,Web 开发领域逐渐形成了一种共识:大多数 JavaScript 框架都基于组件。组件具有诸多优势,例如逻辑可重用、集中式风格和易于测试。它们的使用带来了丰富且统一的浏览体验,使网页设计摆脱了 21 世纪初的风格。

因此,一种新的趋势出现了,它将图形规范和独立 UI 组件的原则融合在一起:这就是设计系统

设计系统

什么是StencilJS?

如前所述,StencilJS 并非 ReactJS 或 VueJS 等其他 Web 框架。StencilJS是一套工具链,旨在基于Web 组件标准构建可重用且可扩展的设计系统。它由Ionic 框架团队专门为此目的而创建。

尽管 StencilJS 与前端框架截然不同,但它使用了许多前端开发中广为人知的概念和技术——这常常会让开发者感到困惑。因此,StencilJS 提供了以下功能:

  • 虚拟DOM
  • JSX(例如 ReactJS 中的 JSX)
  • 响应式数据(例如 AngularJS 的 $watch)
  • 异步渲染(灵感来自 React Fiber)
  • TypeScript支持

通过结合所有这些特性,StencilJS 能够生成符合标准的组件。此外,StencilJS 还会自动添加支持旧版浏览器所需的 polyfill 。以下是 StencilJS 官网提供的浏览器支持列表。

浏览器支持

总而言之,StencilJS符合即将推出的标准,具有自动 polyfill 和高级 API,因此使用 StencilJS 可以构建面向未来的设计系统。

一个简单的案例研究

在本节中,我将详细介绍如何使用 StencilJS 从零开始创建一个设计系统。例如,我将创建一个名为 Marmelab 的设计系统mml。以下是初始化过程。

Marmelab 设计系统

创建组件

项目设置完成后,我们就可以创建第一个组件了,在本例中是“Github Card”。StencilJS 为此提供了一个特殊的 npm 脚本命令generate

julien@julien-P553UA:~/Projets/marmelab/mml$ npm run generate
> mml@0.0.1 generate /home/julien/Projets/marmelab/mml
> stencil generate

✔ Component tag name (dash-case): … github-card
✔ Which additional files do you want to generate? › Stylesheet, Spec Test, E2E Test

$ stencil generate github-card

The following files have been generated:
 - src/components/github-card/github-card.tsx
 - src/components/github-card/github-card.css
 - src/components/github-card/github-card.spec.ts
 - src/components/github-card/github-card.e2e.ts
Enter fullscreen mode Exit fullscreen mode

StencilJS 已经生成了创建组件所需的所有文件,甚至包括测试文件!以下是该命令生成的文件(不包括测试文件)。

// ./src/components/github-card/github-card.css

:host {
  display: block;
}

// ./src/components/github-card/github-card.tsx

import { Component, Host, h } from '@stencil/core';

@Component({
  tag: 'github-card',
  styleUrl: 'github-card.css',
  shadow: true
})
export class GithubCard {
  render() {
    return (
      <Host>
        <slot></slot>
      </Host>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

你可能会想:我们应该用 StencilJS,为什么这个命令生成的是 Angular 代码?嗯,虽然它看起来像 Angular 代码,但实际上它是 StencilJS 代码。我承认,模块开头的注解确实容易让人困惑。不过别担心,你的思路是对的 ;)

因此,该@Component注解允许我们声明一个 StencilJS 组件,该组件可以使用多个选项进行配置:

  • tag:我们将要注册组件的标签名称。
  • styleUrl:指向相应样式文件的相对 URL
  • shadow启用浏览器Shadow DOM封装
  • 其他选择……

在该render方法中,Host组件代表组件的根元素,即标签本身。在其内部,slot可以按照如下所述,将子元素注入到我们的自定义元素中。

<github-card>
   <div>I'm a children, and i'll replace the <!-- <slot>--></div>
</github-card>
Enter fullscreen mode Exit fullscreen mode

由于我们使用了 Shadow DOM,因此只有标签内的样式github-card.css才会影响元素的显示github-card。特殊的:host选择器指的是标签本身(即Host)。

然后,我们的组件就存在了,我们可以在浏览器中使用yarn start(即stencil build --dev --watch --serve)看到它(目前还不太明显)。

如果我们使用 Chrome 检查 DOM devtools,就会看到这样的内容。

StencilJS DOM

体验了几分钟集成者的角色后,我用一些 HTML 和 CSS 代码得到了以下结果。可惜的是,目前所有内容都是静态的,而且我在现实生活中的粉丝远没有 42 个 ^^。

HTML GitHub 卡片

下一章,我将解释如何使用自定义元素上的特殊属性来配置显示哪个用户。login

<github-card login="jdemangeon"></github-card>
Enter fullscreen mode Exit fullscreen mode

向组件传递属性

与ReactJS类似,StencilJS 也提供了 `get_username`stateprops`get_state`lifecycle hooks属性。因此,第一步是在组件中声明一个login`name` 属性和一个`state` 属性。`name` 属性接收 GitHub 用户名,而`state` 属性接收来自 GitHub API 的用户对象。userloginuser

- import { Component, Host, h } from "@stencil/core";
+ import { Component, Host, h, Prop, State } from "@stencil/core";


@Component({
  tag: "github-card",
  styleUrl: "github-card.css",
  shadow: true
})
export class GithubCard {
+  @Prop() login: string;
+  @State() user: any;

  render() {
    ...
-     <a class="avatar" href={`https://github.com/jdemangeon`}>
+     <a class="avatar" href={`https://github.com/${this.login}`}>
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

与 React 不同,StencilJS 没有this.props对象可以用来访问 props。prop 值直接附加到this实例上。因此,我们可以login使用this.login.

因此,我们的链接是最新的,并且根据login属性指向正确的个人资料。其他数据,例如关注者数量、仓库数量等,不是动态的,需要调用 GitHub API 获取。

外部和内部 API

我其实不想让我的 GitHub 人气成为焦点……但这就像抽奖一样,亲爱的!所以,在本节中,我们将探讨如何从组件调用 GitHub API,并根据响应显示实际值。

首先,我们将声明一个调用 GitHub 并为用户赋值的函数。第二步,我们将在挂载组件时调用此函数


export class GithubCard {
  @Prop() login: string;
  @State() user: any;

+ async componentWillLoad() {
+   return this.fetchUser(this.login);
+ }

+ async fetchUser(login: string) {
+   const response = await fetch(`https://api.github.com/users/${login}`);
+
+   if (response.status === 200) {
+     this.user = await response.json();
+   } else {
+     this.user = null;
+   }
+ }
Enter fullscreen mode Exit fullscreen mode

然后,我们就可以在render()方法内部使用用户信息。如果用户不存在或尚未检索到用户,我们将返回空值null

  render() {
+   if (!this.user) {
+     return null;
+   }

    return (
      <Host>
        <div class="card">
          <div class="header" />
          <a class="avatar" href={`https://github.com/${this.login}`}>
-           <img src="https://avatars0.githubusercontent.com/u/1064780" alt={this.login} />
+           <img src={this.user.avatar_url} alt={this.login} />
          </a>
          <div>
-           <h1>Julien Demangeon</h1>
+           <h1>{this.user.name}</h1>
            <ul>
              <li>
                <a
                  target="_blank"
                  href={`https://github.com/${this.login}?tab=repositories`}
                >
-                 <strong>42</strong>Repos
+                 <strong>{this.user.public_repos}</strong>Repos
                </a>
              </li>
              ...
Enter fullscreen mode Exit fullscreen mode

与 props 一样,state 属性也是通过 . 直接附加到实例上的this。因此,您必须非常小心命名,以避免 state 和 props 之间发生冲突。

是的,它确实有效。但是如果我更新login属性会发生什么?

- <github-card login="jdemangeon"></github-card>
+ <github-card login="marmelab"></github-card>
Enter fullscreen mode Exit fullscreen mode

我的组件没有发生变化……这是为什么呢?因为componentWillLoad所使用的生命周期方法只在组件挂载到 DOM 之前调用一次。为了反映 prop(和 state)的变化,我们componentWillUpdate也必须实现相应的方法。

export class GithubCard {
  @Prop() login: string;
  @State() user: any;

  async componentWillLoad() {
    return this.fetchUser(this.login);
  }

+ async componentWillUpdate() {
+   return this.fetchUser(this.login);
+ }
Enter fullscreen mode Exit fullscreen mode

componentWillLoad生命周期方法componentWillUpdate比较componentWillRender特殊。它们可以返回一个Promise,该 Promise 可用于等待下一次渲染。

是的!我们的组件不会显示用户信息,并且会随着loginprop 的变化而变化。如果我们想允许开发者显示其他信息呢?为了实现这个目标,我们将使用一个slot.

   return (
      <Host>
        <div class="card">
          <div class="header" />
          <a class="avatar" href={`https://github.com/${this.login}`}>
            <img src={this.user.avatar_url} alt={this.login} />
          </a>
          <div>
            <h1>{this.user.name}</h1>
+           <slot />
            <ul>
              <li>
              ...
Enter fullscreen mode Exit fullscreen mode

HTML 代码稍作修改……

- <github-card login="jdemangeon"></github-card>
+ <github-card login="jdemangeon">
+   <span>I like Pastis!</span>
+ </github-card>
Enter fullscreen mode Exit fullscreen mode

瞧!生活是不是很美好?虽然这里只有一个slot,但你需要知道,由于有命名槽位,slots所以可以使用多个

我喜欢巴黎

实际上,如果您直接应用示例,您会看到“我喜欢茴香酒”这几个字在组件其他部分显示之前闪烁。这是很正常的,因为我们使用的是普通的 HTML 标签,而且浏览器会在 JavaScript 执行之前显示这些文本。

为了避免这个问题,StencilJS 会hydrated在组件挂载后为其应用一个类。因此,需要在下面声明样式以避免该问题。

    <style type="text/css">
      github-card {
        display: none;
      }
      github-card.hydrated {
        display: block;
      }
    </style>
Enter fullscreen mode Exit fullscreen mode

如果你已经使用过VueJS,你可能已经了解v-cloak属性,它的原理与此相同。AngularJS 也遵循同样的原理ng-cloak

StencilJS API 提供了许多功能,这里很难一一介绍。我强烈建议您查阅官方文档,内容非常详尽。

组成部件

我们已经构建了一个独立组件。如何从其他组件与其交互?我们将创建一个用户选择器,以便更改卡片中的用户。

所以,这次我们使用相同的generate命令github-card-selector。下面是最终的组件。

// src/components/github-card-selector/github-card-selector.tsx

import { Component, Host, h, State } from "@stencil/core";

@Component({
  tag: "github-card-selector",
  styleUrl: "github-card-selector.css",
  shadow: true
})
export class GithubSelector {
  @State() login: string;

  handleLoginChange(e: UIEvent) {
    const target = e.target as HTMLInputElement;
    this.login = target.value;
  }

  render() {
    return (
      <Host>
        <input
          onChange={this.handleLoginChange.bind(this)}
          placeholder="Github username"
        />
        {this.login && <github-card login={this.login} />}
      </Host>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

如我们所见,这段代码简洁易读。然而,它假定github-cardhtml 标签已在浏览器中注册(幸运的是,StencilJS 在加载 mml 库时会自动处理这一点)。代码中没有显式导入。当然,由于我们使用了 StencilJS TypeScript,所有元素都是类型化的,甚至包括UIEventHTMLInputElement浏览器事件。

您可以在以下地址找到项目源代码:https://github.com/marmelab/mml

组件测试

StencilJS 中有两种不同的测试类型:单元测试端到端(e2e) 测试。StencilJS 使用Jest进行单元测试,使用Puppeteer进行 e2e 测试。如果您已经熟悉这两个优秀的工具,就不会感到困惑。

玩笑与木偶师

借助 Puppeteer,可以对浏览器测试进行精细的配置调整。这包括 ` touch management<script>`、`<script>` 等,landscape mode直至 `<script> viewport emulation`。使用单元测试,还可以模拟许多功能,例如HTTP Referer`<script> `、 Cookies`<script> Url`、`<script>` 等。

事实上,我并没有发现这两种测试模式之间有任何显著差异。其中一种速度较慢仅仅是因为它需要经过 Puppeteer,但并没有带来任何额外的价值。我认为最好是在组件集成到最终应用程序之后再进行端到端测试

有关使用 StencilJS 进行测试的更多信息,请参阅此处的文档

框架互操作性

如前所述,StencilJS 并非框架,而只是一个 Web 组件编译器。尽管专注于这项独特的任务,但StencilJS 的根本目标是实现基于新标准的端到端 Web 应用程序开发。

完全使用 StencilJS 构建复杂的应用程序仍然很困难。因此,StencilJS 提供了一系列函数,允许将组件直接注入到现有的 Web 应用程序中。下面是一个使用 ReactJS 的示例。

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import { applyPolyfills, defineCustomElements } from '<your-design-system-lib>/loader';

ReactDOM.render(<App />, document.getElementById('root'));

applyPolyfills().then(() => {
  defineCustomElements(window);
});
Enter fullscreen mode Exit fullscreen mode

并非所有框架都支持轻松集成自定义元素(Web组件)。一些网站会列出各个框架的兼容性信息。

设计系统目标

StencilJS 的最大特点之一是它可以同时生成各种构建版本,以满足所有预期需求和目标。

因此,StencilJS 可以为每个组件生成ES5ECMAScript Modules (esm)版本。它还可以生成相应的文档markdownjson

我们可以通过这种方式配置目标stencil.config.js

import { Config } from '@stencil/core';

export const config: Config = {
  namespace: 'mml',
  outputTargets: [
    {
      type: 'dist',
      esmLoaderPath: '../loader'
    },
    {
      type: 'docs-readme'
    },
    {
      type: 'www',
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

从输出目标文件夹中,我们可以直接运行组件来测试它。你可以在原文博客文章www中直接测试。输入你的 GitHub 用户名,按回车键,瞧!

以下是输出目标文档。

结论

StencilJS 无法取代ReactJS 或 VueJS 等 Web 框架。事实上,Web Components 无法接收复杂的属性数据(prop在组件术语中也称为“属性”)。与任何 HTML 标签一样,它们只能接收标量/文本数据(也称为“属性” attribute)。这使得它们的用途非常有限。

有些人使用作弊手段绕过了这个限制,但我不太确定这款游戏是否值得这么做。

因此,如果您想在应用程序中创建不带逻辑的图形组件(哑组件),或者如果您想创建可以独立集成到任何地方的小部件,那么 StencilJS 是一个非常好的选择。

文章来源:https://dev.to/juliendemangeon/build-your-own-design-system-with-stenciljs-1jfg