背景介绍
这段时间准备开发一个 JS 插件, 一种引入JS文件即可使用的脚本。分析了需求需要响应式数据、状态管理。在技术选型上如果使用 React 或者 Vue 框架打包后体积会很臃肿。于是, 一开始选择了 svelte 和 rollup 来进行开发。后来在开发过程中因为引入第三方库, rollup 还需要做很多配置, 于是放弃了 rollup 转而使用 vite 进行开发省去了不少麻烦。
下面总结一下 Svelte 项目的搭建过程。
项目搭建
- svelte 官网: www.svelte.cn/
- vite + svelte 模板: stackblitz.com/edit/vitejs…
mkdir demo-plguin
cd demo-plguin
pnpm init
将 vite 提供的模板下载到自己的项目中。
目录结构
├── .gitignore # Git忽略配置
├── .vscode # VS Code配置
│ └── extensions.json # 推荐扩展
├── README.md # 项目说明
├── index.html # 应用入口HTML
├── jsconfig.json # JavaScript配置
├── package-lock.json # 锁定依赖版本
├── package.json # 项目依赖与脚本
├── public # 静态资源
│ └── vite.svg # 示例资源
├── src # 源码目录
│ ├── App.svelte # 主组件
│ ├── app.css # 全局样式
│ ├── assets # 资源文件
│ ├── lib # 通用库
│ ├── main.js # 应用入口
│ └── vite-env.d.ts # 类型声明
├── svelte.config.js # Svelte配置
└── vite.config.js # Vite配置
vite.config.js
vite 配置文件,设置打包后文件为 IIFE 立即执行脚本。
import {defineConfig} from 'vite'
import {svelte} from '@sveltejs/vite-plugin-svelte'
// https://vite.dev/config/
export default defineConfig({
plugins: [
svelte(),
build: {
rollupOptions: {
input: {
main: 'src/main.js', // 主入口文件
},
output: {
format: 'iife', // 指定打包格式为 IIFE
entryFileNames: 'demo-plguin.js', // 输出的文件名
name: 'demoPlugin', // IIFE 全局变量的名称
},
},
},
})
src/main.js
svelte 项目入口文件
import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
target: document.getElementById('app'),
})
export default app
Svelte 基础组件
一个 svelte 组件通常包括三部分, JS、CSS 和 HTML。不同于 Vue , Svelte 组件可以不写 template 。
如下面的代码, 这就是一个简单的 Svelte 组件。
<script>
import svelteLogo from './assets/svelte.svg'
import viteLogo from '/vite.svg'
import Counter from './lib/Counter.svelte'
</script>
<style>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.
</style>
<main>
<div>Hello Svelte</div>
</main>
Svelte 实现响应式数据
使用 svelte 首先是为了他轻量级实现的响应式数据。下面介绍一下在 svelte 如何使用响应式数据。
svelte 实现响应式数据很简单, 不需要像 vue 的 ref reactive 直接用 let 定义变量就可以。
<script>
let count = 0;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
- svelte 是如何实现数据响应式的?
Svelte 实现计算属性
在 svelte 中实现类似 vue 的 computed 的计算属性要使用 定义的变量都会重新计算。
<script>
let count = 1;
// the `$:` means 're-run whenever these values change'
$: doubled = count * 2;
$: quadrupled = doubled * 2;
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Count: {count}
</button>
<p>{count} * 2 = {doubled}</p>
<p>{doubled} * 2 = {quadrupled}</p>
Svelte 实现 Watch 监听
svelte 实现类似 vue 中 watch 的功能还是使用 $ 即可, 以下有几种情况
监听响应式数据变化并补充判断条件
<script>
let count = 0;
$: if (count >= 10) {
alert(`count is dangerously high!`);
count = 9;
}
function handleClick() {
count += 1;
}
</script>
<button on:click={handleClick}>
Clicked {count} {count === 1 ? 'time' : 'times'}
</button>
监听变量变化
<script>
let count = 0;
// 使用 $: 监听 count 变化
$: {
console.log(`Count changed to ${count}`);
}
let user = { name: "Alice", age: 25 };
// 监听对象变化
$: console.log(`User data: ${JSON.stringify(user)}`);
function updateAge() {
user = { ...user, age: user.age + 1 }; // 创建新对象触发响应式
}
let items = [1, 2, 3];
// 监听数组变化
$: {
console.log(`Items updated: ${items.join(", ")}`);
}
function addItem() {
items = [...items, items.length + 1]; // 创建新数组触发响应式
}
</script>
<button on:click={() => count++}>Increment</button>
用 stores 实现全局或可观察的数据监听
Svelte 提供了 writable 和 readable stores,可以更方便地实现数据的观察和响应。`
<script>
import { writable } from 'svelte/store';
// 创建 store
let count = writable(0);
// 订阅 store 变化
count.subscribe(value => {
console.log(`Count changed to ${value}`);
});
function increment() {
count.update(n => n + 1);
}
</script>
<button on:click={increment}>Increment</button>
<p>{$count}</p>
手动触发监听: 函数式监听
如果需要更精细地控制,可以使用函数封装监听逻辑
<script>
let count = 0;
function watch(variable, callback) {
$: callback(variable);
}
watch(count, value => {
console.log(`Count is now: ${value}`);
});
</script>
<button on:click={() => count++}>Increment</button>
Svelte 组件间传参
父组件使用 key={} 传递, 子组件用 export let key 接收。
父组件
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42}/>
子组件
<script>
export let answer;
// 设置默认值
export let answer = 'a mystery';
</script>
<p>The answer is {answer}</p>
使用扩展运算符
<script>
import Info from './Info.svelte';
const pkg = {
name: 'svelte',
version: 3,
speed: 'blazing',
website: 'https://svelte.dev'
};
</script>
<Info {...pkg}/>
<script>
export let name;
export let version;
export let speed;
export let website;
</script>
<p>
The <code>{name}</code> package is {speed} fast.
Download version {version} from <a href="https://www.npmjs.com/package/{name}">npm</a>
and <a href={website}>learn more here</a>
</p>
Svelte 事件与父子组件通信
类似于 react 向下传递函数。
使用 on: 绑定事件
<script>
let m = { x: 0, y: 0 };
function handleMousemove(event) {
m.x = event.clientX;
m.y = event.clientY;
}
</script>
<style>
div { width: 100%; height: 100%; }
</style>
<div on:mousemove={handleMousemove}>
The mouse position is {m.x} x {m.y}
</div>
行内事件
<script>
let m = { x: 0, y: 0 };
</script>
<style>
div { width: 100%; height: 100%; }
</style>
<div on:mousemove="{e => m = { x: e.clientX, y: e.clientY }}">
The mouse position is {m.x} x {m.y}
</div>
事件修饰符
<script>
function handleClick() {
alert('no more alerts')
}
</script>
<button on:click|once={handleClick}>
Click me
</button>
组件事件传递
<script>
import Inner from './Inner.svelte';
function handleMessage(event) {
alert(event.detail.text);
}
</script>
<Inner on:message={handleMessage}/>
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function sayHello() {
dispatch('message', {
text: 'Hello!'
});
}
</script>
<button on:click={sayHello}>
Click to say hello
</button>
Dom事件转发
父组件传递 click 事件, 子组件直接绑定使用
<script>
import CustomButton from './CustomButton.svelte';
function handleClick() {
alert('clicked');
}
</script>
<CustomButton on:click={handleClick}/>
<style>
</style>
<button on:click>
Click me
</button>
表单数据双向绑定
Input
使用 bind:value 实现
<script>
let name = '';
</script>
<input bind:value={name} placeholder="enter your name">
<p>Hello {name || 'stranger'}!</p>
<textarea bind:value={text}></textarea>
checkbox
<script>
let yes = false;
</script>
<label>
<input type=checkbox bind:checked={yes}>
Yes! Send me regular email spam
</label>
group
<script>
let scoops = 1;
let flavours = ['Mint choc chip'];
let menu = [
'Cookies and cream',
'Mint choc chip',
'Raspberry ripple'
];
function join(flavours) {
if (flavours.length === 1) return flavours[0];
return `${flavours.slice(0, -1).join(', ')} and ${flavours[flavours.length - 1]}`;
}
</script>
{#each menu as flavour}
<label>
<input type=checkbox bind:group={flavours} value={flavour}>
{flavour}
</label>
{/each}
还有 file / select bindings/ selecte multiple / each block bingdings / media elements / dimensions / canvas / components 详见: www.svelte.cn/examples#co…
模板逻辑语法
类似于 vue v-if v-show v-for 这些指令 svelte 也支持。
IF 语句
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{/if}
{#if !user.loggedIn}
<button on:click={toggle}>
Log in
</button>
{/if}
ELSE 语句
<script>
let user = { loggedIn: false };
function toggle() {
user.loggedIn = !user.loggedIn;
}
</script>
{#if user.loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}
Else-if 语句
<script>
let x = 7;
</script>
{#if x > 10}
<p>{x} is greater than 10</p>
{:else if 5 > x}
<p>{x} is less than 5</p>
{:else}
<p>{x} is between 5 and 10</p>
{/if}
ForEach 语句
<script>
let cats = [
{ id: 'J---aiyznGQ', name: 'Keyboard Cat' },
{ id: 'z_AbfPXTKms', name: 'Maru' },
{ id: 'OUtn3pvWmpg', name: 'Henri The Existential Cat' }
];
</script>
<h1>The Famous Cats of YouTube</h1>
<ul>
{#each cats as { id, name }, i}
<li><a target="_blank" href="https://www.youtube.com/watch?v={id}">
{i + 1}: {name}
</a></li>
{/each}
</ul>
带 Key 的 EACH
<script>
import Thing from './Thing.svelte';
let things = [
{ id: 1, color: 'darkblue' },
{ id: 2, color: 'indigo' },
{ id: 3, color: 'deeppink' },
{ id: 4, color: 'salmon' },
{ id: 5, color: 'gold' }
];
function handleClick() {
things = things.slice(1);
}
</script>
<button on:click={handleClick}>
Remove first thing
</button>
<div style="display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1em">
<div>
<h2>Keyed</h2>
{#each things as thing (thing.id)}
<Thing current={thing.color}/>
{/each}
</div>
<div>
<h2>Unkeyed</h2>
{#each things as thing}
<Thing current={thing.color}/>
{/each}
</div>
</div>
Await 语句
await blocks 用于在 Svelte 中处理异步数据加载,展示加载中(waiting)、成功(then)、和错误(catch)的不同状态。
<script>
let promise = getRandomNumber();
async function getRandomNumber() {
const res = await fetch(`tutorial/random-number`);
const text = await res.text();
if (res.ok) {
return text;
} else {
throw new Error(text);
}
}
function handleClick() {
promise = getRandomNumber();
}
</script>
<button on:click={handleClick}>
generate random number
</button>
{#await promise}
<p>...waiting</p>
{:then number}
<p>The number is {number}</p>
{:catch error}
<p style="color: red">{error.message}</p>
{/await}
生命周期
onMount
component挂载到DOM后立即执行的回调
<script>
import { onMount } from 'svelte';
let photos = [];
onMount(async () => {
const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`);
photos = await res.json();
});
</script>
<style>
.photos {
width: 100%;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 8px;
}
figure, img {
width: 100%;
margin: 0;
}
</style>
<h1>Photo album</h1>
<div class="photos">
{#each photos as photo}
<figure>
<img src={photo.thumbnailUrl} alt={photo.title}>
<figcaption>{photo.title}</figcaption>
</figure>
{:else}
<!-- this block renders when photos.length === 0 -->
<p>loading...</p>
{/each}
</div>
onDestory
计划在component卸载后运行的回调。
<script>
import { onInterval } from './utils.js';
let seconds = 0;
onInterval(() => seconds += 1, 1000);
</script>
<p>
The page has been open for
{seconds} {seconds === 1 ? 'second' : 'seconds'}
</p>
import { onDestroy } from 'svelte';
export function onInterval(callback, milliseconds) {
const interval = setInterval(callback, milliseconds);
onDestroy(() => {
clearInterval(interval);
});
}
update
<script>
import Eliza from 'elizabot';
import { beforeUpdate, afterUpdate } from 'svelte';
let div;
let autoscroll;
beforeUpdate(() => {
autoscroll = div && (div.offsetHeight + div.scrollTop) > (div.scrollHeight - 20);
});
afterUpdate(() => {
if (autoscroll) div.scrollTo(0, div.scrollHeight);
});
const eliza = new Eliza();
let comments = [
{ author: 'eliza', text: eliza.getInitial() }
];
function handleKeydown(event) {
if (event.key === 'Enter') {
const text = event.target.value;
if (!text) return;
comments = comments.concat({
author: 'user',
text
});
event.target.value = '';
const reply = eliza.transform(text);
setTimeout(() => {
comments = comments.concat({
author: 'eliza',
text: '...',
placeholder: true
});
setTimeout(() => {
comments = comments.filter(comment => !comment.placeholder).concat({
author: 'eliza',
text: reply
});
}, 500 + Math.random() * 500);
}, 200 + Math.random() * 200);
}
}
</script>
<style>
.chat {
display: flex;
flex-direction: column;
height: 100%;
max-width: 320px;
}
.scrollable {
flex: 1 1 auto;
border-top: 1px solid #eee;
margin: 0 0 0.5em 0;
overflow-y: auto;
}
article {
margin: 0.5em 0;
}
.user {
text-align: right;
}
span {
padding: 0.5em 1em;
display: inline-block;
}
.eliza span {
background-color: #eee;
border-radius: 1em 1em 1em 0;
}
.user span {
background-color: #0074D9;
color: white;
border-radius: 1em 1em 0 1em;
}
</style>
<div class="chat">
<h1>Eliza</h1>
<div class="scrollable" bind:this={div}>
{#each comments as comment}
<article class={comment.author}>
<span>{comment.text}</span>
</article>
{/each}
</div>
<input on:keydown={handleKeydown}>
</div>
tick
返回一个promise,该promise将在应用state变更后返回resolves,或者在下一个微任务中(如果没有)更改。类似于 vue 的 nextTick。
<script>
import { tick } from 'svelte';
let text = `Select some text and hit the tab key to toggle uppercase`;
async function handleKeydown(event) {
if (event.key !== 'Tab') return;
event.preventDefault();
const { selectionStart, selectionEnd, value } = this;
const selection = value.slice(selectionStart, selectionEnd);
const replacement = /[a-z]/.test(selection)
? selection.toUpperCase()
: selection.toLowerCase();
text = (
value.slice(0, selectionStart) +
replacement +
value.slice(selectionEnd)
);
await tick();
this.selectionStart = selectionStart;
this.selectionEnd = selectionEnd;
}
</script>
<style>
textarea {
width: 100%;
height: 200px;
}
</style>
<textarea value={text} on:keydown={handleKeydown}></textarea>
掌握了上述知识点,便能轻松上手 Svelte 项目开发,打造简洁高效的前端应用。