Svelte 初探筆記 (上)
邊緣人的端午連假沒事做,就來看看這個最近感覺討論度感覺滿高的東西,玩了一下官方 tutorial 覺得寫起來感覺跟 marko 有幾分相似(?)
Svelte is a radical new approach to building user interfaces. Whereas traditional frameworks like React and Vue do the bulk of their work in the browser, Svelte shifts that work into a compile step that happens when you build your app.
以下內容都來自官方 tutorial,有興趣者也可以直接去 playground 邊看邊玩
Syntax
開發上也是以 component 角度去著手,每個 .svelte
檔案就是一個元件,其中就會包含了該元件本身的 markup、logic (JS)、style
如上 App.svelte
中在 <script>
裡 import 了另一個 Img
元件、設了一個 date 變數,在 <style>
中如同 HTML 用法設 CSS,剩下的部分就是元件 markup,可透過 {}
來塞入變數值或 dynamic attribute 如 Img.svelte
所示
{@html textVal}
把變數 textVal
的值以 HTML 形式塞入
<script>
const textVal = '<h1>hello world</h1>'
</script><p>{@html textVal}</p>
$:
效果像是 react useEffect,當變數值有改變時會觸發,有 inline 或 block 的寫法
<script>
let count = 0
$: console.log(`count changed: ${count}`) $: {
console.log(`count changed: ${count}`)
}
</script>
特別注意上面 anotherCount
並不會因為 count 改變而改變,就像是
const anotherCount = React.useMemo(() => count + 10, [])
Props
元件一樣可以定義/傳入 props,在要開 props 的元件透過 export
來列舉 (如下 Item.svelte
),父元件再利用 html attribute 形式傳入,如同 react,也可用 spread operator{...props}
來一次塞入
另外也可透過 $$props
拿到整包傳入的 props
<pre>{JSON.stringify($$props, null, 4)}</pre>
Block
#
:
/
Condition
依條件來顯示不同內容
<div>
{#if user.login}
<span>Logged in</span>
{:else}
<span>please login</span>
{/if}
</div>
Each
<ul>
{#each items as item}
<li>{item.name}</li>
{/each}
</ul>
也可對 item 做 destructuring、拿 index
{#each items as { name }, index}
或給 key,像是 react 在 render array 時需要 key 做 diff
{#each items as { name, id}, index (id)}
{#each items as item, index (item.id)}
await
根據 promise 狀態來呈現對應的 UI,有點像是 react 的 Suspense
{#await promise}
pending...
{:then val}
get resolved value: {val}
{:catch e}
error caught: {e}
{/await}
亦可省略 pending 時的 UI
{#await promise then val}
value: {val}
{/await}
DOM event
利用 on:EVENT_NAME={handler}
來加入 event listener
<script>
const handleClick = () => { ... }
<script><button on:click={handleClick}>btn</button>
modifiers
對 event handler 行為作設定,例如 once
在觸發一次後會拔掉這 event handler,其他還有
preventDefault
— callsevent.preventDefault()
before running the handler. Useful for client-side form handling, for example.
stopPropagation
— callsevent.stopPropagation()
, preventing the event reaching the next element
passive
— improves scrolling performance on touch/wheel events (Svelte will add it automatically where it's safe to do so)
capture
— fires the handler during the capture phase instead of the bubbling phase (MDN docs)
self
— only trigger handler if event.target is the element itself
<button on:click|once={handleClick}>btn</button>
並能同時套用多個 modifiers on:click|once|capture={handler}
如下,在 outer div 及 inner div 都加上了 click event,但 outer 附帶了 capture
、stopPropagation
讓 inner div 的 click handler 不會被觸發
component event
監聽/觸發元件的 custom event
如上,在 App.svelte
中對 <RandomNumber />
監聽 number
event,而在 RandomNumber.svelte
元件中透過 svelte 提供的 createEventDispatcher
來發出 number
event
event forwarding
當塞給元件 on:...
監聽某 event 卻沒給 handler 時,會自動帶入上層給予的 event handler,如下 App 裡包了 Outer 再包 Inner 裡面有個 button,並且要在 App 聽 button click event
Binding
對 input value 及變數/state 做雙向綁定
<input bind:value={val} />
<input type="checkbox" bind:value={isChecked}>
或是用 bind:group={groupedVal}
來綁定 checkbox、radio 的值
綁定 innerHTML
<script>
let html = ''
<script><div contenteditable bind:innerHTML={html}></div>
以及對 video/audio 的 attribute 綁定
The complete set of bindings for
<audio>
and<video>
is as follows — six readonly bindings...
duration
(readonly) — the total duration of the video, in seconds
buffered
(readonly) — an array of{start, end}
objects
seekable
(readonly) — ditto
played
(readonly) — ditto
seeking
(readonly) — boolean
ended
(readonly) — boolean…and five two-way bindings:
currentTime
— the current point in the video, in seconds
playbackRate
— how fast to play the video, where1
is 'normal'
paused
— this one should be self-explanatory
volume
— a value between 0 and 1
muted
— a boolean value where true is mutedVideos additionally have readonly
videoWidth
andvideoHeight
bindings.
最後是綁定元素,像是 react ref
<script>
let inputRef
const handleClick = () => {
console.log(inputRef.value)
}
</script><input bind:this={inputRef} />
<button on:click={handleClick}>submit</button>
小插曲,看 svelte tutorial 學怎麼 handle drag
<script>
const handleMousemove = (e) => {
if (e.buttons === 1) {
// dragging
}
}
</script><div on:mousemove={handleMousemove}/>
Lifecycle
onMount
在 component 第一次 render 後觸發,其中 callback 可以 return function 會在 destroy 前被執行
<script>
import { onMount } from 'svelte'
onMount(() => {
console.log('onMount') return () => {
console.log('destroyed')
}
})
</script>
就像 react effect
useEffect(() => {
console.log('did mount') return () => {
console.log('will unmount')
}
}, [])
onDestroy
component 被消失的時候執行
<script>
import { onDestroy } form 'svelte'
let count = 0
const interval = setInterval(() => {
count += 1
}, 1000) onDestroy(() => {
clearInterval(interval)
})
</script>
beforeUpdate/afterUpdate
如其名,就是在 render/re-render 前後觸發
<script>
import { beforeUpdate, afterUpdate } from 'svelte' beforeUpdate(() => {
...
}) afterUpdate(() => {
...
})
</script>
tick
When you update component state in Svelte, it doesn’t update the DOM immediately. Instead, it waits until the next microtask to see if there are any other changes that need to be applied, including in other components.
tick 可以讓你等前面的 state 改動被 applied 後再做之後的事,確保改動的 state 已反應,假設原本有一個 click handler 會去改變三個 state 值,一般來說只會產生一次 re-render 來呈現三個 state 都改變後的結果,透過 tick 可以讓他分三次依序執行
Stores
writable
創建一個名為 count
的 store,並賦予初始值為 0
// stores.js
import { writeable } from 'svelte/store'
export const count = writable(0)
在任意元件 update count
// AnotherComponent.svelte
<script>
import { count } from './stores.js' const handleClick = () => {
count.update(val => val + 1)
}
</script><button on:click={handleClick}>count + 1</button>
就能在其他元件中 subscribe count store
// App.svelte
<script>
import { onDestroy } from 'svelte'
import { count } from './stores.js'
let countVal
const unsubscribe = count.subscribe(val => {
countVal = val
}) onDestroy(unsubscribe)
</script><div>count: {countVal}</div>
但這樣 subscribe/unsubscribe 太麻煩,於是有了 $
語法糖可以使用,如下 $count
就會自動 subscribe/unsubscribe count store
// App.svelte
<script>
import { count } from './stores.js'
</script><div>count: {$count}</div>
readable
不同於前述 writable store 可以針對 store value做 update,readable store 只能在建立時給予初始值以及更新方式
// stores.js
import { readable } from 'svelte/storeexport const date = readable(Date.now(), (set) => {
const interval = setInterval(() => {
set(Date.now())
}, 1000) return () => {
clearInterval(interval)
}
})
可以看到 readable 的第一個參數是初始值,第二個參數是 start function 帶著 set function 可以 update store,並 return 一個 stop function 會在所有 subscriber 都 unsubscribe 後執行
derived
跟 mobx 的 @computed
概念差不多
Derives a store from one or more other stores. Whenever those dependencies change, the callback runs.
即是一個需要依賴其他 store 的 store
import { writable, derived } from 'svelte/store'const count = writable(0)
const double = derived(count, $count => $count * 2)
每當 count 值改變時,就會觸發 derived store 的 callback 來更新 double
值
或是用 set
的方式來操作
import { writable, derived } from 'svelte/store'const count = writable(0)
const double = derived(count, ($count, set) => {
set($count * 2)
})
custom store
只要有正確實作 subscribe 的 object 都可以稱作為 store,所以可以這樣做一個 custom store
// stores.js
import { writable } from 'svelte/store'const createCountStore = () => {
const { subscribe, set, update } = writable(0)
return {
subscribe,
add1: () => update(c => c + 1),
reset: () => set(0)
}
}export const count = createCountStore()
然後在 component 中使用
// App.svelte
<script>
import { count } from './stores.js'
</script>count: { $count }
<div>
<button on:click={count.add1}>add 1</button>
<button on:click={count.reset}>reset</button>
</div>
store binding
如同上述在 input 中 bind:value
,writable store 也能做雙向綁定,假設有個 writable store name
// stores.js
import { writable } from 'svelte/store'export const name = writable('Bill')
再把 name 綁到 input 上
// App.svelte
<script>
import { name } from './stores.js'
</script>name: { $name } <input bind:value={$name} />
另外
$name += 'lalala'
等同於
$name.set($name + 'lalala')
寫著寫著端午節都過了,而且好像有點太長了,然後插太多 codesandbox 好像會給瀏覽器太大負擔,就先這樣吧
至於如何 build app,就像 react 一樣會有個 entry 定義著要把什麼 component 塞到哪個 DOM 元素
// index.js
import App from './App'new App({
target: document.getElementById('root'),
props: {}
})
再透過 webpack、svelte-loader 來 bundle,或其他你熟悉的 bundler
最後附上個簡易的 todo list demo
如果你喜歡這篇文章的話可以拍 11 下手,不喜歡的話可以拍 3 下再分享給其他 5 個人,謝謝