学习 Svelte

267 阅读2分钟

我正在参加「掘金·启航计划」

1. 搭建项目

npm create vite@latest myapp -- --template svelte
cd myapp
npm install
npm run dev


or SvelteKit

npm create svelte@latest my-app
cd my-app
npm install
npm run dev -- --open

2.svelte 基础知识

生命周期

  • onMount: 组件挂载时调用。
<script>
	import { onMount } from 'svelte';

	let photos = [];

	onMount(async () => {
		const res = await fetch(`/tutorial/api/album`);
		photos = await res.json();
	});
</script>
  • onDestroy: 组件销毁时执行。
  • beforeUpdate: 在数据更新前执行。
  • afterUpdate: 在数据更新完成后执行。
  • tick: DOM元素更新完成后执行。
import { tick } from 'svelte';

let text = `Select some text and hit the tab key to toggle uppercase`;

async function handleKeydown(event) {
	const { selectionStart, selectionEnd, value } = this;
	await tick();
	this.selectionStart = selectionStart;
	this.selectionEnd = selectionEnd;
}

组件格式

# 在`.svelte`文件中,这三个部分( script, styles和元素标签)都是可选的。


<script>
	// logic goes here
</script>

<!-- markup (zero or more items) goes here -->

<style>
	/* styles go here */
</style>

 '反应性(reactive)' 分配

# 由于Svelte的反应性是基于分配的,因此使用`.push()`和 `.splice()`和这样的数组方法不会自动触发更新。解决此问题的选项可以使用
  • 解决该问题的一种方法是添加一个多余的赋值语句:
function addNumber() {
	numbers.push(numbers.length + 1);
	numbers = numbers;
}
  • 但还有一个更惯用的解决方案:
function addNumber() {
	numbers = [...numbers, numbers.length + 1];
}

你可以使用类似的模式来替换 popshiftunshift 和 splice 方法

一个简单的经验法则是:被更新的变量的名称必须出现在赋值语句的左侧。例如下面这个:

const foo = obj.foo;
foo.bar = 'baz';

$::声明反应性,值改变的时候再次运行

当它们依赖的值发生更改时,它们都会在 component 更新之前立即运行。 这个功能有点像 Vue 的 computed。 $: 可以监听表达式内部的变化从而做出响应

<script>
    let count = 1;

    // the `$:` means 're-run whenever these values change'
    // Svelte替你将 `let`插入声明
    $: 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替你将 let插入声明。

用 $前缀来存储store值


<script>
	import { writable } from 'svelte/store';

	const count = writable(0);
	console.log($count); // logs 0

	count.set(1);
	console.log($count); // logs 1

	$count = 2;
	console.log($count); // logs 2
</script>

一个store 必须包含一个 .subscribe方法,该方法必须接受subscription 函数作为其参数

<script context="module">

一个<script> 标签具有一个context="module"熟悉,在模块首次 evaluates 时运行一次,而不是针对每个组件实例运行一次。 此块中声明的值可以在常规的 <script>代码块中访问 (和 component 的标签), 反之亦然。

你可以使用export绑定到该块,它们将作为已编译模块导出。

你不能使用 export default来绑定,因为组件本身就是默认导出(export default)。

带有 module 声明的 <script> 内代码不具有反应性。虽然变量自身会更新,但重新分配它们不会触发重新渲染,对于多个组件之间共享的值,请考虑使用 store.

<script context="module">
	let totalComponents = 0;

	// 此处允许执行import操作,例如
	// `import Example, { alertTotal } from './Example.svelte'`
	export function alertTotal() {
		alert(totalComponents);
	}
</script>

<script>
	totalComponents += 1;
	console.log(`total number of times this component has been created: ${totalComponents}`);
</script>

style

<style> 标签块中的样式仅仅生效于component内部。

<style>
    p {
        /* 这只会影响此组件中的<p>标签 */
        color: burlywood;
    }
</style>



  • :global(...)

使用 :global(...) 修饰符来添加全局样式

<style>
	:global(body) {
		/* 这里样式全局应用于 <body>内都 */
		margin: 0;
	}

	div :global(strong) {
		/* 这里表示全局应用于被<div>包裹的<strong>标签 */
		color: goldenrod;
	}
</style>

模板语法

  • 标签

小写标签诸如<div>之类,表示常规的HTML标签,大写标签例如 <Widget> 或 <Namespace.Widget>,表示这是一个 component

<script>
  import Widget from './Widget.svelte';
</script>

<div>
  <Widget/>
</div>
  • props 属性
# 默认情况下,属性的使用方式与HTML属性的使用方法一样。

<div class="foo">
  <button disabled>can't touch this</button>
</div>

# 属性值也可以去掉引号

<input type=checkbox>

# 属性值可以包含JavaScript表达式

<a href="page/{p}">page {p}</a>

#JavaScript表达式,表达式可能会包含导致常规HTML语法高亮失效,使之不能正常显示语法高亮,因此有必要使用引号来避免此情况
<button disabled={!clickable}>...</button>
<button disabled="{number !== 42}">...</button>

# 当出现属性名和值一样时 (`name={name}`),可以简写为`{name}`

<!-- 两者是等价的 -->
<button disabled={disabled}>...</button>
<button {disabled}>...</button>

# *`$$props`* 可以传递所有 props 属性到一个 component 中,包含未使用`export`声明的 props 属性。它在特殊情况下很有用,但通常不推荐使用,因为Svelte难以优化

<Widget {...$$props}/>

  • 文本表达式

  • 注释

  • {#if ...}条件渲染

{#if answer === 42}
	<p>what was the question?</p>
{/if}


{#if porridge.temperature > 100}
	<p>too hot!</p>
{:else if 80 > porridge.temperature}
	<p>too cold!</p>
{:else}
	<p>just right!</p>
{/if}
  • {#each ...} 列表渲染
<ul>
	{#each items as item}
		<li>{item.name} x {item.qty}</li>
	{/each}
</ul>

# 空数据: 是空数组的情况下,还可以组合 {:else} 一起使用。
{#each todos as todo}
	<p>{todo.text}</p>
{:else}
	<p>No tasks today!</p>
{/each}

  • {#await ...} 异步渲染
{#await promise}
	<!-- promise is pending -->
	<p>waiting for the promise to resolve...</p>
{:then value}
	<!-- promise was fulfilled -->
	<p>The value is {value}</p>
{:catch error}
	<!-- promise was rejected -->
	<p>Something went wrong: {error.message}</p>
{/await}
  • {@html ...} HTML内容插入

在 Vue 中有 v-html 方法,它可以将 HTML 标签渲染出来。在 Svelte 中也有这个方法,在插值前面使用 @html 标记一下即可 但此方法有可能遭受 XSS 攻击

<div class="blog-post">
	<h1>{post.title}</h1>
	{@html post.content}
</div>
  • {@debug ...} 调试模式
<script>
	let user = {
		firstname: 'Ada',
		lastname: 'Lovelace'
	};
</script>

{@debug user}

<h1>Hello {user.firstname}!</h1>
  • @const

注意必须放在 {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <Component /> or <svelte:fragment />

<script>
	export let boxes;
</script>

{#each boxes as box}
	{@const area = box.width * box.height}
	{box.width} * {box.height} = {area}
{/each}

{@const} is only allowed as direct child of {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <Component /> or <svelte:fragment />.

样式

<div style:color={myColor}>...</div>
<div style="color: blue;" style:color="red">This will be red</div>


<div class="{active ? 'active' : ''}">...</div>
<div class:active={active}>...</div>

<!-- Shorthand, for when name and value match -->
<div class:active>...</div>

在 HTML 里可以使用 class:xxx 动态设置要激活的类。这里的 xxx 是对应的类名。
语法是 class:xxx={state} ,当 state 为 true 时,这个样式就会被激活使用

<button
	class:selected="{current === 'bar'}"
	on:click="{() => current = 'bar'}"
>bar</button>

事件

on:eventname={handler}

on:eventname|modifiers={handler}


<script>
	let count = 0;

	function handleClick(event) {
		count += 1;
	}
</script>

<button on:click={handleClick}>
	count: {count}
</button>

modifiers(事件修饰符)以下这些修饰符可以用:
on:click|once|capture={...}

preventDefault :禁止默认事件。在程序运行之前调用 event.preventDefault()
stopPropagation :调用 event.stopPropagation(), 防止事件到达下一个标签
passive :改善了 touch/wheel 事件的滚动表现(Svelte会在合适的地方自动加上它)
capture:表示在 capture阶段而不是bubbling触发其程序
once :程序运行一次后删除自身
self:点击自身触发
trusted:


数据绑定

<!-- These are equivalent -->
<input bind:value={value}>
<input bind:value>  // 简写,bind:value 绑定的属性是 value ,而在 JS 中声明的变量名也叫 value ,此时就可以使用简写的方式


# 单选框和复选框
<script>
	let tortilla = 'Plain';
	let fillings = [];
</script>

<!-- grouped radio inputs are mutually exclusive -->
<input type="radio" bind:group={tortilla} value="Plain">
<input type="radio" bind:group={tortilla} value="Whole wheat">
<input type="radio" bind:group={tortilla} value="Spinach">

<!-- grouped checkbox inputs populate an array -->
<input type="checkbox" bind:group={fillings} value="Rice">
<input type="checkbox" bind:group={fillings} value="Beans">
<input type="checkbox" bind:group={fillings} value="Cheese">
<input type="checkbox" bind:group={fillings} value="Guac (extra)">

bind:this

针对传统的DOM节点,请使用 bind:this来绑定
<script>
	import { onMount } from 'svelte';

	let canvasElement;

	onMount(() => {
		const ctx = canvasElement.getContext('2d');
		drawStuff(ctx);
	});
</script>

<canvas bind:this={canvasElement}></canvas>

use:action use 指令

其本质是元素上的生命周期函数。它们可用于譬如以下几个方面:

  • 与第三方库对接
  • 延迟加载图片
  • 工具提示(tooltips)
  • 添加自定义事件处理程序
<script>
	import { clickOutside } from "./click_outside.js";

	let showModal = true;
</script>

<button on:click={() => (showModal = true)}>Show Modal</button>
{#if showModal}
	<div class="box" use:clickOutside on:outclick={() => (showModal = false)}>
		Click outside me!
	</div>
{/if}

<style>
	.box {
		--width: 100px;
		--height: 100px;
		position: absolute;
		width: var(--width);
		height: var(--height);
		left: calc(50% - var(--width) / 2);
		top: calc(50% - var(--height) / 2);
		display: flex;
		align-items: center;
		padding: 8px;
		border-radius: 4px;
		background-color: #ff3e00;
		color: #fff;
		text-align: center;
		font-weight: bold;
	}
</style>

组件通讯

  • 父组件传数据到子组件
# 父组件
<script>
  import Child from './Child.svelte'
</script>

<div>子组件 Child 的内容:</div>
<Child number="123456" />


# 子组件
<script>
  export let number = '666'
</script>

<div>data:{number}</div>
  • 子组件转数据到父组件:createEventDispatcher
# 子组件 child
<script>
  import { createEventDispatcher } from 'svelte'
  const dispatch = createEventDispatcher()

  function print() {
    dispatch('printPhone', '13288888888')
  }
</script>

<button on:click={printData}>传递数据</button>

# 父组件
<script>
  import Child from './Child.svelte'
  function print(data) {
    console.log(`数据: ${data.detail}`)
  }
</script>

<div>子组件 Phone 的内容:</div>
<Phone on:printData={print} />

插槽

在子组件中使用 <slot> 标签,可以接收父组件传进来的 HTML 内容

# 子组件
<div class="box">
	<slot></slot>
</div>

# 父组件
<script>
	import Box from './Box.svelte';
</script>

<Box>
	<h2>Hello!</h2>
	<p>This is a box. It can contain anything.</p>
</Box>

3.路由

install

npm install svelte-spa-router

使用

# routes.js
// Import the wrap method
import {wrap} from 'svelte-spa-router/wrap'

// Note that Author and Book are not imported here anymore, so they can be imported at runtime
import Home from './routes/Home.svelte'
import NotFound from './routes/NotFound.svelte'

const routes = {
    '/': Home,

    // Wrapping the Author component
    '/author/:first/:last?': wrap({
        asyncComponent: () => import('./routes/Author.svelte')
    }),

    // Wrapping the Book component
    '/book/*': wrap({
        asyncComponent: () => import('./routes/Book.svelte')
    }),

    // Catch-all route last
    '*': NotFound,
}

export default routes;

#  App.svelte
import Router from 'svelte-spa-router'
import routes from './routes';

<main>
  <Router {routes} />
</main>

# 切换路由
<a href="#/book/123">#/book/123</a>

or

<script>
  import { link } from 'svelte-spa-router';
  const path = '/about/wise';
</script>

<a use:link={path}>To About Page</a>

# 获取路由参数,在对应路由页
>对于动态路由,如果在组件中以 props 的形式导出了一个 params ,通过  /#/book/123 访问 Detail 组件
<script>
    export let params = {};
    console.log(params);
</script>

如果路由上还携带有 query 参数(比如:/#/book/123?name=wise&blog=cnblogs)

可以通过 svelte-spa-router 提供的 querystring 拿到

import {location, querystring} from 'svelte-spa-router'

<div>book</div>

<div>#/book/123?a=1&b=2 获取参数</div>
<div>{$location}</div>
<div>{$querystring}</div>

<script>
  export let params = {};
  console.log(params); // book/123---> {wild: 123}

  import {location, querystring} from 'svelte-spa-router'
  // 只读的store 要使用 $ 读取
  console.log($location, $querystring)
</script>

4.store使用

# store.js
import { writable } from 'svelte/store';

export const count = writable(0);

# App.svelte
<script>
	import { count } from './stores.js';
  // 只读的store 要使用 $ 读取
</script>

<h1>The count is {$count}</h1>

通过 set 和 update 设置数据更新. 通过 subscribe 订阅值的变化

# App.svelte
<script>
	import { count } from './stores.js';
	import Incrementer from './Incrementer.svelte';
	import Resetter from './Resetter.svelte';

	let countValue;

	count.subscribe(value => {
		countValue = value;
	});
</script>
<h1>The count is {countValue}</h1>

<Incrementer/>
<Resetter/>

# Incrementer.svelte
<script>
	import { count } from './stores.js';

	function increment() {
		count.update(n => n + 1);
	}
</script>

<button on:click={increment}>
	+
</button>

# Resetter.svelte
<script>
	import { count } from './stores.js';

	function reset() {
		count.set(0);
	}
</script>

<button on:click={reset}>
	reset
</button>

# stores.js
import { writable } from 'svelte/store';

export const count = writable(0);

参考