Svelte 入门

658 阅读6分钟

背景介绍

这段时间准备开发一个 JS 插件, 一种引入JS文件即可使用的脚本。分析了需求需要响应式数据、状态管理。在技术选型上如果使用 React 或者 Vue 框架打包后体积会很臃肿。于是, 一开始选择了 svelte 和 rollup 来进行开发。后来在开发过程中因为引入第三方库, rollup 还需要做很多配置, 于是放弃了 rollup 转而使用 vite 进行开发省去了不少麻烦。

下面总结一下 Svelte 项目的搭建过程。

项目搭建

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

www.svelte.cn/docs#before…

<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 项目开发,打造简洁高效的前端应用。