简介
Svelte是一个开源的JavaScript组件框架,用于构建Web应用程序。Svelte采取了与所有现有的Web框架不同的方法,如React、Angular和Vue,它们遵循声明式的、状态驱动的代码。
Svelte捆绑包的大小明显小于大多数框架,因为它没有任何依赖关系(只有开发依赖关系在package.js 文件中可用)。由于这些特性,Svelte已经成为2021年最受欢迎、最令人畏惧、最想要的Web框架。
由于这种受欢迎程度,开发者们推出了几个很棒的UI组件框架/库,如Svelte Material UI、Smelt、Svelte Materialify和Sveltestrap。
但是,建立你自己的Svelte组件库会是什么样子呢?幸运的是,有几个模板可以开始使用,比如 Svelte提供的官方 模板和Svelte 3组件模板,它们被认为是构建自己的组件库的首选。
然而,这些模板是非常有主见的,你可能无法看到构建组件库本身所需的底层工具和技术。在这篇文章中,我们将学习如何使用Svelte新的SvelteKit自行构建一个组件库。
什么是SvelteKit?
SvelteKit可以说是Sapper或NextJS的继承者。它包含了大量很酷的功能,比如服务器端渲染、路由和代码拆分。
SvelteKit在引擎盖下使用了Vite,这很令人惊讶,因为Sapper和大多数工具是使用Snowpack开发的。Vite 2是与框架无关的,并以SSR为核心设计。
SvelteKit仍处于测试阶段,但它非常稳定,而且有许多项目在生产中使用这个框架。
开始使用SvelteKit
对于这个项目,我们将使用一个骨架项目作为我们库的基础。
让我们使用SvelteKit来初始化该项目。你需要执行以下命令并选择Svelte给出的选项。
集成Storybook
现在是时候整合Storybook了,这是一个开源的工具,用于孤立地构建UI组件和页面。它简化了UI开发和测试,这对我们的组件库开发是非常理想的。它允许我们构建组件,而不必担心SvelteKit中的配置或开发服务器。
在你的SvelteKit项目根部,执行以下命令。这将识别并生成Svelte的必要配置。
npx sb init
在用SvelteKit项目设置Storybook时,你可能会面临一些问题。当你启动服务器时,Storybook会抛出一个错误,像这样。
这个问题是由于package.json 文件下的“type”:”module” 的属性而抛出的,这意味着我们不能使用ESM要求的语法。
为了克服这个问题,你可以在Storybook的配置文件中做一个小的调整。只需将你的Svelte Storybook配置文件的扩展名从.js 改为.jcs ,在main.cjs 文件内,确保你注释掉svelteOptions 的属性,该属性由require 命令组成。
进行上述调整后,你可以运行下面的命令来启动Storybook服务器。
npm run storybook
该命令将在浏览器中打开一个新的标签,加载我们SvelteKit项目的Storybook应用程序。
构建组件前需要考虑的因素
在构建组件之前,请考虑以下因素,因为它们将帮助我们遵循正确的准则。
道具的使用
"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",
}}
/>
注意,我们通过向模板传递几个参数,为我们的按钮组件创建了几个模板。
现在在故事书窗口中,你将能够看到一个按钮。
你可以从下面提供的控制器中从主按钮切换到副按钮。你也可以从Actions日志中清楚地查看这个自定义组件的事件类型。
切换组件
现在让我们来创建一个切换组件。首先创建Toggle.svelte 和Toggle.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中被渲染。这个故事由两个模板组成,分别称为Labeled和Blank,并将用不同的道具或参数渲染同一个组件的两个实例。
输入字段组件
最后,我们将创建一个输入字段组件,它的样式可以为输入的每个状态显示令人愉悦的颜色。
在你的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 组件,让我们看看如何为这些组件编写故事以及它是如何渲染的。在这里,我们将传入一些参数,如backgroundColor 和state 。
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” 的小属性。
现在你所要做的就是在项目根目录下运行以下命令。
有了这个,你可以维护单元测试,从而提高你的组件的质量。
打包和发布到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.js 或src/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.