Java编程网

分享 Java Web 开发相关知识

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

注意:此帖子最初发布在marmelab.com上

在本文中,我们将定义什么是StencilJS以及如何使用它来构建设计系统。作为前提,您应该知道StencilJS不是框架。它是一个生成可重用的Web组件的编译器,该组件可以嵌入到其他任何地方。

什么是设计系统?

许多出版商都有图形章程,其中定义了有关其视觉识别的一组规则。

鉴于我们社会中的技术发展,这些规则越来越多地广泛涉及数字媒体,尤其是网络。

在过去的几年中,Web开发领域已经达成共识。确实,大多数javascript框架很大程度上都是基于component的。组件具有许多优势,例如逻辑可重用性,集中式风格和易于测试。它们的使用提供了远离21世纪初的丰富而统一的浏览体验。

结果,出现了一种新趋势,将图形章程和独立UI组件的原理融合在一起:这就是Design System

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

什么是StencilJS?

如前所述,StencilJS不是另一个Web框架,例如ReactJS或VueJSStencilJS是一个工具链,可促进基于Web组件标准的可重用和可扩展设计系统的构建。它是由Ionic Framework团队专门为此目的创建的。

尽管与前端框架完全不同,但StencilJS使用了前端开发中的许多众所周知的概念和技术-这通常会使开发人员感到困惑。因此,StencilJS提供了:

  • 虚拟DOM
  • JSX(就像在ReactJS中一样)
  • 反应性数据(例如AngularJS $ watch)
  • 异步渲染(受React Fiber的启发)
  • TypeScript支持

通过结合所有这些功能,StencilJS能够生成符合标准的组件。而且,StencilJS自动添加支持较旧的浏览器所需的polyfill 。这是StencilJS网站提供的浏览器支持表格。

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

因此,总而言之,使用StencilJS可以使您构建一个为将来做好准备的设计系统,这要归功于它符合即将到来的标准,自动polyfill和高级API。

一个简单的案例研究

在本节中,我将详细介绍使用StencilJS从头开始创建设计系统。对于示例,我将为Marmelab创建一个名为的设计系统mml。这是下面的初始化过程。

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

创建一个组件

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

[email protected]:~/Projets/marmelab/mml$ npm run generate
> [email protected] 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

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>
    );
  }
}

现在您可能会想:我们应该使用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>

由于我们使用Shadow DOM,因此只有github-card.css标记中的样式才会对的显示产生影响github-card。特殊:host选择器引用标签本身(又名Host)。

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

如果我们使用chrome检查DOM devtools,那么我们将看到。

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

在集成商忙了几分钟之后,这是我从html和css中得到的。可悲的是,目前所有事情都是静态的,在现实生活中,我的追随者少于42个

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

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

<github-card login="jdemangeon"></github-card>

将道具传递给组件

ReactJS,StencilJS提供statepropslifecycle hooks。因此,第一步是在组件中声明login道具和user状态。而login将接收GitHub用户的名称,user将接收来自GitHub API的用户对象。

- 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}`}>
    ...
  }
}

与React相反,没有this.props对象可以使用StencilJS访问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;
+   }
+ }

然后,我们可以在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>
              ...

就像道具一样,状态属性通过直接附加到实例this。因此,您在命名时必须非常小心,以避免状态与道具之间发生冲突。

是的,它有效。但是,如果我更新login属性会怎样?

- <github-card login="jdemangeon"></github-card>
+ <github-card login="marmelab"></github-card>

好吧,我的组件没有改变…但是为什么呢?因为所使用的componentWillLoad生命周期方法仅在组件安装到DOM之前被调用一次。为了反映道具(和状态)的变化,我们也必须实现componentWillUpdate

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

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

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

componentWillLoadcomponentWillUpdate以及componentWillRender生命周期方法是特殊的。他们可以返回一个Promise,该Promise可用于等待下一个渲染。

是!我们的组件no显示用户信息,并且在login道具更改时也会更改。如果我们要允许开发人员显示其他信息怎么办?为了实现此目标,我们将使用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>
              ...

HTML中的一点变化…

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

和田田!生活不是很美吗?在这种情况下,只有一个slot,但是您需要知道,slots由于使用了命名槽,可以使用多个。

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

实际上,如果按原样应用示例,则会在组件的其余部分之前看到文本“ I like pastis”闪烁。这是很正常的,因为我们使用“正常”的html标签,并且浏览器将在执行javascript之前显示文本。

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

为避免此问题,hydrated一旦安装组件,StencilJS将在这些组件上应用一个类。因此,有必要声明以下样式以避免出现此问题。

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

如果您已经使用过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>
    );
  }
}

如我们所见,代码足够简单,易于阅读。但是,它假定github-cardhtml标记已在浏览器中注册(幸运的是,在加载mml库时,StencilJS会处理该标记)。没有显式导入。当然,由于我们使用TypeScript,所以所有类型都被键入,甚至包括UIEventHTMLInputElement浏览器事件。

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

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

组件测试

StencilJS中有2种不同的测试类型:单元测试端到端(e2e)。StencilJS使用Jest进行单元测试,使用Puppeteer进行e2e测试。如果您已经知道这两个出色的工具,您将不会迷路。

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

感谢Puppeteer,可以微调浏览器测试的配置。包括touch management,或landscape mode,最多viewport emulation。使用单元测试,还可以嘲笑许多功能,如HTTP RefererCookiesUrl,等。

实际上,我在这两种测试模式之间没有发现任何显着差异。一个只是速度较慢,因为它会通过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);
});

并非所有框架都允许轻松集成自定义元素(Web组件)。一个网站列出了它们各自的兼容率。

设计系统目标

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

因此,StencilJS可以为每个组件生成ES5ECMAScript模块(esm)版本。它还可以在markdown或中生成相应的文档json

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

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

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

www输出目标文件夹中,我们可以通过提供服务来直接测试我们的组件。您可以直接在原始博客文章中对其进行测试。输入您的Github用户名,按Enter,然后Voilà!

这是输出目标文档。

结论

StencilJS无法替换Web框架,例如ReactJS或VueJS。实际上,Web组件无法接收复杂的属性数据(又名prop组件)。像任何html标记一样,它们只能接收标量/文本数据(aka attribute)。这使得它们的使用非常有限。

有些人利用骇客来解决这个限制,但我不确定游戏是否值得。

因此,如果要在应用程序中创建没有逻辑的图形组件(哑组件),或者要创建可在任何环境下集成的小部件,StencilJS是一个很好的选择

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注