用Svelte建立你自己的组件库

3,731 阅读6分钟

简介

Svelte是一个开源的JavaScript组件框架,用于构建Web应用程序。Svelte采取了与所有现有的Web框架不同的方法,如React、Angular和Vue,它们遵循声明式的、状态驱动的代码。

Svelte捆绑包的大小明显小于大多数框架,因为它没有任何依赖关系(只有开发依赖关系在package.js 文件中可用)。由于这些特性,Svelte已经成为2021年最受欢迎、最令人畏惧、最想要的Web框架

由于这种受欢迎程度,开发者们推出了几个很棒的UI组件框架/库,如Svelte Material UISmeltSvelte MaterialifySveltestrap

但是,建立你自己的Svelte组件库会是什么样子呢?幸运的是,有几个模板可以开始使用,比如 Svelte提供的官方 模板Svelte 3组件模板,它们被认为是构建自己的组件库的首选。

然而,这些模板是非常有主见的,你可能无法看到构建组件库本身所需的底层工具和技术。在这篇文章中,我们将学习如何使用Svelte新的SvelteKit自行构建一个组件库。

什么是SvelteKit?

SvelteKit可以说是Sapper或NextJS的继承者。它包含了大量很酷的功能,比如服务器端渲染、路由和代码拆分。

SvelteKit在引擎盖下使用了Vite,这很令人惊讶,因为Sapper和大多数工具是使用Snowpack开发的。Vite 2是与框架无关的,并以SSR为核心设计。

SvelteKit仍处于测试阶段,但它非常稳定,而且有许多项目在生产中使用这个框架。

开始使用SvelteKit

对于这个项目,我们将使用一个骨架项目作为我们库的基础。

让我们使用SvelteKit来初始化该项目。你需要执行以下命令并选择Svelte给出的选项。

Initializing sveltekit

集成Storybook

现在是时候整合Storybook了,这是一个开源的工具,用于孤立地构建UI组件和页面。它简化了UI开发和测试,这对我们的组件库开发是非常理想的。它允许我们构建组件,而不必担心SvelteKit中的配置或开发服务器。

在你的SvelteKit项目根部,执行以下命令。这将识别并生成Svelte的必要配置。

npx sb init

在用SvelteKit项目设置Storybook时,你可能会面临一些问题。当你启动服务器时,Storybook会抛出一个错误,像这样。

storybook error

这个问题是由于package.json 文件下的“type”:”module” 的属性而抛出的,这意味着我们不能使用ESM要求的语法。

为了克服这个问题,你可以在Storybook的配置文件中做一个小的调整。只需将你的Svelte Storybook配置文件的扩展名从.js 改为.jcs ,在main.cjs 文件内,确保你注释掉svelteOptions 的属性,该属性由require 命令组成。

Storybook configuration files

进行上述调整后,你可以运行下面的命令来启动Storybook服务器。

npm run storybook

该命令将在浏览器中打开一个新的标签,加载我们SvelteKit项目的Storybook应用程序。

Storybook welcome page

构建组件前需要考虑的因素

在构建组件之前,请考虑以下因素,因为它们将帮助我们遵循正确的准则。

道具的使用

"props "这个词在所有主要的框架和库中都很常见,比如Vue和React。道具将数据传递给子组件或实现组件通信。

槽和$$slots 使用

即使props允许你通过传递数据来重用组件,它也会带来严格的父子关系。这意味着它的HTML内容将永远控制着子组件,而父组件只能传递不同的值,所以组件不能用props组成在一起。

这就是槽的用武之地。槽在保持可重用性的同时,允许父组件控制子组件的内容,包括它里面的HTML元素。通过添加<slots/> 标签,你可以从父代传递HTML或markdown,而不仅仅是数值。

避免嵌套和全局CSS

在构建组件时,避免使用嵌套的和全局的CSS,因为它们不会被范围化,这意味着它们会泄露给所有子组件。

处理事件

在构建组件时,确保你添加或处理适当的事件。你必须使用Svelte的API,即createEventDispatcher ,这在调度事件时很有用。

创建组件

让我们为我们的库创建一些组件。首先,删除由Storybook生成的默认的story 文件夹,并在src 目录下创建一个stories 文件。

接下来,我们将重点讨论项目结构。在src 目录下创建一个名为lib 的目录。这个lib 目录是SvelteKit的一个特殊目录,因为它将允许我们使用一个特殊的符号,称为 [$lib](https://kit.svelte.dev/docs#modules-$lib)$lib ,可以用来别名src/lib 目录,并帮助我们访问组件和实用模块,而无需使用相对路径,如../../../../ .

按钮组件

现在让我们在lib 目录下创建我们的按钮组件,名为Button.svelte

<script>
  import { createEventDispatcher } from 'svelte';

  export let primary = false;
  export let size = 'medium';
  export let label = '';

  const dispatch = createEventDispatcher();
   /**
   * Button click handler
   */
   function onClick(event) {
    dispatch('click', event);
  }
</script>

<button
  type="button"
  class={['sveltio-button', `sveltio-button--${size}`,
   `sveltio-button--${primary?'primary':'secondary'}`].join(' ')
   }
  on:click={onClick}>
  {label}
</button>

<style>
.sveltio-button {
    font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-weight: 700;
    border: 0;
    border-radius: 3px;
    cursor: pointer;
    display: inline-block;
    line-height: 1;
}
.sveltio-button--primary {
    color: #1b116e;
    background-color: #6bedb5;
}
.sveltio-button--secondary {
    color: #ffffff;
    background-color: #1b116e;
    box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
.sveltio-button--small {
    font-size: 12px;
    padding: 10px 16px;
}
.sveltio-button--medium {
    font-size: 14px;
    padding: 11px 20px;
}
.sveltio-button--large {
    font-size: 16px;
    padding: 12px 24px;
}

.sveltio-button--primary :hover {
    color: #1b116e;
    background-color: #55bd90;
}

.sveltio-button--primary :active {
    color: #1b116e;
    background-color: #55bd90;
    border: solid 2px #1b116e;
}

.sveltio-button--primary :disabled {
    color: #1b116e;
    opacity: 0.5;
    background-color: #6bedb5;
}

.sveltio-button--secondary :hover {
    color: #1b116e;
    background-color: #55bd90;
}

.sveltio-button--secondary :active {
    color: #1b116e;
    background-color: #6bedb5;
    border: solid 2px #1b116e;
}

.sveltio-button--secondary :disabled {
    color: #ffffff;
    opacity: 0.5;
    background-color: #1b116e;
}

</style>

注意,我们已经在同一个文件中的<style> 标签下添加了样式。

现在让我们为我们的Button组件创建一个故事文件,叫做Button.stories.svelte

  <script>
    import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
    import Button from "./Button.svelte";
  </script>


  <Meta
    title="Sveltio/Button"
    component={Button}
    argTypes={{
      label: { control: "text" },
      primary: { control: "boolean" },
      backgroundColor: { control: "color" },
      size: {
        control: { type: "select", options: ["small", "medium", "large"] },
      },
      onClick: { action: "onClick" },
    }}
  />


  <Template let:args>
    <Button {...args} on:click={args.onClick} />
  </Template>


  <Story
    name="Primary"
    args={{
      primary: true,
      label: "Button",
    }}
  />


  <Story
    name="Secondary"
    args={{
      label: "Button",
    }}
  />
  <Story
    name="Large"
    args={{
      size: "large",
      label: "Button",
    }}
  />


  <Story
    name="Small"
    args={{
      size: "small",
      label: "Button",
    }}
  />



注意,我们通过向模板传递几个参数,为我们的按钮组件创建了几个模板。

现在在故事书窗口中,你将能够看到一个按钮。

Storybook button

你可以从下面提供的控制器中从主按钮切换到按钮。你也可以从Actions日志中清楚地查看这个自定义组件的事件类型。

切换组件

现在让我们来创建一个切换组件。首先创建Toggle.svelteToggle.stories.svelte 文件。

Toggle.svelte

<script>
    export let label = '';
    export let isToggled = false;
    export let style = '';
</script>

<label {style} class="sveltio-toggle-label">
    <input type="checkbox" class="sveltio-input" bind:checked={isToggled} />
    <div class="sveltio-toggle" />
    {label}
</label>

<style>
    .sveltio-toggle-label {
    --width: 40px;
    --height: calc(var(--width) / 2);
    --radius: calc(var(--height) / 2);
    display: flex;
}

.sveltio-toggle {
    position: relative;
    width: var(--width);
    height: var(--height);
    border-radius: var(--radius);
    border: solid 1px #c2c2c3;
    transition: background-color 0.3s ease;
    margin-right: 5px;
    background-color: var(--toggleBackgroundColor, #c2c2c3);
}

.sveltio-toggle::after {
    content: '';
    position: absolute;
    top: -1px;
    left: -1px;
    height: var(--height);
    width: var(--height);
    border-radius: var(--radius);
    background-color: var(--toggleButtonColor, #ffffff);
    box-shadow: 2px 4px 6px rgba(0, 0, 0, 0.3);
    transition: transform 0.3s ease;
}
.sveltio-input {
    display: none;
}

.sveltio-input:checked + .toggle {
    background-color: var(--toggleCheckedBackgroundColor, #1b116e);
}

.sveltio-input:checked + .toggle::after {
    transform: translate3d(100%, 0, 0);
}

</style>

Toggle.stories.svelte

<script>
    import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
    import Toggle from "./Toggle.svelte";
  </script>


  <Meta
    title="Sveltio/Toggle"
    component={Toggle}
    argTypes={{
      label: { control: "text" },
      primary: { control: "boolean" },
      backgroundColor: { control: "color" },
      size: {
        control: { type: "select", options: ["small", "medium", "large"] },
      },
      onClick: { action: "onClick" },
    }}
  />


  <Template let:args>
    <Toggle {...args} on:click={args.onClick} />
  </Template>


  <Story
    name="Labeled"
    args={{
      primary: true,
      label: "Check me",
    }}
  />


  <Story
    name="Blank"
    args={{
      label: "",
    }}
  />

现在让我们看看这个组件将如何在Storybook中被渲染。这个故事由两个模板组成,分别称为LabeledBlank,并将用不同的道具或参数渲染同一个组件的两个实例。

Storybook toggle component

输入字段组件

最后,我们将创建一个输入字段组件,它的样式可以为输入的每个状态显示令人愉悦的颜色。

在你的Input.svelte 文件中写下以下内容。

<script>
    import { createEventDispatcher } from 'svelte';

    export let placeholder = '';
    export let label = '';
    export let disabled = false;


    export let state = "active";


    const dispatch = createEventDispatcher();

     /**
     * input change handler
     */
     function onChange(event) {
      dispatch('click', event);
    }
</script>

<label >

    {#if label}
        <span class="sveltio-input-label">{label}</span>
    {/if}


    <input
    disabled={disabled}
    type="text"
    class={['sveltio-input',`sveltio-input--${state}`].join(' ')}
    placeholder={placeholder}
    >
</label>

<style>
    .sveltio-input {
    font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-weight: 700;
    min-height: 25px;
    margin: 2px;
    border-radius: 3px;
    border: solid 2px #353637;
    background-color: #ffffff;
}

.sveltio-input ::focus {
    border: solid 2px #1b116e;
}

.sveltio-input--success {
    border: solid 2px #067d68;
}

.sveltio-input--error {
    border: solid 2px #a9150b;
}

.sveltio-input--disabled {
    color: #e4e3ea;
    border: solid 2px #e4e3ea;
}

.sveltio-input ::-webkit-input-placeholder {
    color: red;
}

.sveltio-input-label {
    font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
    font-weight: 700;
}

</style>

现在,因为我们已经创建了带有样式的input 组件,让我们看看如何为这些组件编写故事以及它是如何渲染的。在这里,我们将传入一些参数,如backgroundColorstate

Input.stories.svelte:

<script>
    import { Meta, Template, Story } from "@storybook/addon-svelte-csf";
    import Input from "./Input.svelte";
  </script>


  <Meta
    title="Sveltio/Input"
    component={Input}
    argTypes={{
      backgroundColor: { control: "color" },
      state: {
        control: { type: "select", options: ["active","success", "error"] },
      },
      onChange: { action: "onChange" },
      disabled:{ control: "boolean" }
    }}
  />


  <Template let:args>
    <Input {...args} on:change={args.onChange} />
  </Template>


  <Story
    name="Active"
    args={{
      placeholder: "Text Input Active",
      state:"active"
    }}
  />


  <Story
    name="Success"
    args={{
      placeholder: "Text Input Success",
      state: "success",
    }}
  />
  <Story
    name="Error"
    args={{
      placeholder: "Text Input Error",
      state: "error"
    }}
  />


  <Story
    name="Disabled"
    args={{
      state: "disabled",
      disabled:true
    }}
  />



以类似的方式,你可以创建其他的网络组件,并为其他组件创建故事,继续建立你的组件库。你可以通过这个链接找到我们建立的组件的全部代码。

用svelte-testing-library和Jest测试

开发Web应用程序最关键的方面之一是为我们的组件运行和维护测试。使用Svelte,运行测试的过程类似于我们使用React、Vue或Angular得到的结果。

有几个工具可以编写和运行测试,如Mocha、Karma、Jasmine和Jest。对于这个项目,我们将使用Jest作为我们的测试运行器。然而,即使是Jest也略有不足,因为我们需要渲染我们的组件,并检查它在执行动作后的表现。

为了这个目的,我们将使用一个叫做测试库的工具。这个工具可以帮助我们编写测试,就像一个真正的用户正在处理这些元素一样,而且还支持所有主要的前端框架和库。

我们还将使用测试库的一个额外的插件,名为user-event,它允许我们模仿用户事件,如在输入框中输入或点击按钮。我们还将使用一个名为jest-dom的插件,它扩展了Jest的DOM相关匹配能力,我们需要它,因为我们要处理的是网络组件。

现在让我们像这样把这些库作为dev依赖项安装到我们的项目中。

npm install --save-dev jest babel-jest svelte-jester  @testing-library/svelte @testing-library/user-event @testing-library/jest-dom @testing-library/dom

现在,让我们在我们项目的根目录下添加一些配置文件。从Jest.config.cjs 文件开始,其中包含Jest的配置,还有一个.babelrc ,其中包含一些预设,用于将文件转换为ES2015 JavaScript。

Jest.config.cjs

module.exports = {
    transform: {
        "^.+\\.js$": "babel-jest",
        "^.+\\.svelte$": "svelte-jester"
    },
    moduleFileExtensions: ['js', 'svelte'],
    moduleNameMapper: {
        '^\\$lib(.*)$': '<rootDir>/src/lib$1',
        '^\\$app(.*)$': [
            '<rootDir>/.svelte-kit/dev/runtime/app$1',
            '<rootDir>/.svelte-kit/build/runtime/app$1'
        ]
    },
    setupFilesAfterEnv: ["@testing-library/jest-dom/extend-expect"],
    testEnvironment: "jsdom"
};

.babelrc

{
    "presets": [["@babel/preset-env", {"targets": {"node": "current"}}]]
}

上面的测试将检查按钮中的文本是否可用,并做一些断言。

让我们为我们已经创建的Input 文件添加另一个测试。让我们在test 目录下称其为Input.test.js

import '@testing-library/jest-dom';
import Input from '$lib/Input/Input.svelte';
import { render } from '@testing-library/svelte';

describe('Input component', () => {
    it('Input Has Placeholder', () => {
        const { getByPlaceholderText } = render(Input, { placeholder: 'Hello Sveltio' });
        expect(getByPlaceholderText('Hello Sveltio')).toBeInTheDocument();
    });
});
The above test will check if the input field consists of the placeholder we pass as a prop.

在我们运行这些测试之前,我们将在scripts 下为package.json 文件添加一个叫做“test”:”jest” 的小属性。

现在你所要做的就是在项目根目录下运行以下命令。

Jest button test

有了这个,你可以维护单元测试,从而提高你的组件的质量。

打包和发布到npm

现在是向世界发布你的项目的时候了!有几个工具可以用来将你的组件作为一个包导出,但我们将使用SvelteKit内置的一个很酷的功能。首先,将这个属性添加到package.json 文件中。

"package": "svelte-kit package"

现在你所要做的就是在项目的根目录下运行以下程序。

npm run package

如果你没有初始化TypeScript SvelteKit项目,你将需要安装一个名为svelte2tsx 的依赖项,它可以将Svelte组件的源代码转换为TSX。

这个命令将把所有在src/lib 文件夹下的文件,作为一个包来使用。这个命令会在你的项目根部生成一个名为package 的新目录,在这个目录中,你会发现有一个新的package.json 文件。这个文件由一个名为exports 的属性组成,它由我们所开发的各个组件的所有路径或入口点组成。

package 目录下的package.json 文件中输入以下代码。

{
  "name": "sveltio",
  "version": "0.0.1",
  "devDependencies": {
   //some dependencies
  },
  "type": "module",
  "dependencies": {},
  "exports": {
    "./package.json": "./package.json",
    "./Button.svelte": "./Button/Button.svelte",
    "./Input.svelte": "./Input/Input.svelte",
    "./Modal.svelte": "./Modal/Modal.svelte",
    "./Toggle.svelte": "./Toggle/Toggle.svelte"
  }
}

如果你的库由src/lib/index.jssrc/lib/index.svelte 等文件组成,它将被视为包根。这使得使用我们库的组件作为ES模块导入变得更加容易。

例如,如果你有一个src/lib/Button.svelte 组件和一个重新导出它的src/lib/index.js 模块,你的库的消费者可以做以下任何一项。

import { Button } from 'your-library';

import Button from 'your-library/Button.svelte';

现在我们已经使用SvelteKit创建了一个包,是时候把它作为一个npm模块发布了。你所需要做的就是在项目的根目录下执行以下命令。

npm publish ./package

上述命令将发布我们用SvelteKit创建的包。请确保包的名称和包的版本组合不存在;如果存在,包将不会被发布到npm上。

总结

你可以看到为什么SvelteKit在开发者社区中值得更多关注。SvelteKit有非常棒的功能,而且创建包也很容易。有了Storybook和Jest这样的工具,创建一个孤立的组件和维护组件的测试就变得简单而高效。欲了解更多信息,请查阅SvelteKit文档

The postBuild your own component library with Svelteappeared first onLogRocket Blog.