談談魔法消失UI框架Svelte

NO IMAGE

最近基於公司業務需求,可能會要開發一款瀏覽器插件,調查後發現插件UI開發本質上就是開發頁面。於是我便開始尋找一個非常小又非常快的新玩具(工具)。畢竟前端 3 大框架無論哪一個去開發瀏覽器插件都無異於大炮打蚊子。至於開發效率極低的 Dom 操作我也不想去碰了。於是我就找到了這個已經在國外非常火熱的魔法消失 UI 框架 —— Svelte

Svelte是什麼

Svelte 是一個編譯型的前端組件框架。該框架沒有使用虛擬 dom,而是通過編譯在應用狀態發生改變時提供異步響應。

編譯型框架

任何前端框架都是有運行時的,(以 Vue 為例) 該框架至少需要在瀏覽器攜帶虛擬dom 以及 diff 算法。如果在頁面中直接引入 Vue 腳本,還需要追加 Vue 前端編譯器代碼。可以參考Vue 對不同構建版本的解釋

Svelte 則不同,它從開始就決定把其他框架在瀏覽器所完成的大部分工作轉換到構建中的編譯步驟,以便於減少應用代碼量。它通過靜態分析來做到按需提供功能(完全不需要引入),同時它也可以分析得出根據你當前的修改精準更新 dom的代碼來提升性能。

我們以最簡單的代碼為例子。

// App.svelte
<h1>Hello world!</h1>
// main.js
import App from './App.svelte';
const app = new App({
target: document.body
});
export default app;

實際上開發版會被編譯為(為了簡化,只分析部分,不分析全部代碼)

// IIFE 立即執行函數表達式
var app = (function () {
'use strict';
// 空函數,用於某些需要提供函數的代碼
function noop() { }  
// 當前 元素所在的行 列 前面有多少字符等信息,開發版存在
function add_location(element, 
file, 
line, 
column, 
char) {
element.__svelte_meta = {
loc: { file, line, column, char }
};
}
//   
// 操作dom輔助函數,減少代碼量...(相當於運行時)  
function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
function detach(node) {
node.parentNode.removeChild(node);
}
function element(name) {
return document.createElement(name);
}
function children(element) {
return Array.from(element.childNodes);
}
// 異步處理修改過的組件
// (實際上這些代碼在當前場景下不需要,可以去除,但沒必要)
const dirty_components = [];
const binding_callbacks = [];
const render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = Promise.resolve();
let update_scheduled = false;
function schedule_update() {
// ... 
}
function add_render_callback(fn) {
// ...    
}
function flush() {
// ...  
}
function update($$) {
}
// 當前文件,開發版
const file = "src\\App.svelte";
function create_fragment(ctx) {
let h1;
const block = {
// 創建  
c: function create() {
h1 = element("h1");
h1.textContent = "Hello world!";
add_location(h1, file, 1, 9, 10);
},
// 掛載剛才創建的元素
m: function mount(target, anchor) {
// 上面的 target 是 document.body  
insert_dev(target, h1, anchor);
},
// 修改,髒組件在上述 update 中被調用 
p: noop,
// 刪除  
d: function destroy(detaching) {
if (detaching) detach_dev(h1);
}
};
dispatch_dev("SvelteRegisterBlock", {
block,
id: create_fragment.name,
type: "component",
source: "",
ctx
});
return block;
}  
}());

可以看到,在開發版本編譯完成後,你所寫的所有代碼都變成了原生的 js 操作Dom。同時具有很高的可讀性(這點非常重要)。我在我的另一篇 blog 優化 web 應用程序性能方案總結 也表明了,提升代碼的覆蓋率是所有優化機制中收益是最高的。這意味著可以加載更少的代碼,執行更少的代碼,消耗更少的資源,緩存更少的資源。同時在 Vue 3 中也會又靜態分析而進行的按需提供優化。

值得一提的是,因為 Svelte 是編譯型框架,無論是開發還是生產環境,都會在相同文件夾誕生同樣的文件(至少在筆者開始寫時候是這樣的結果,於2020-1-4)。如果前端沒有使用構建部署工具又或者像我這樣僅僅想要開發一個瀏覽器插件的情況下,可能會造成因為文件夾已經存在而忘記進行構建命令,從而錯誤的使用開發版所產生的代碼。

同時,Svelte 直接支持各種編譯配置項目。只要在組件中添加 options 即可:

<svelte:options option={value}/>
  • immmutable 當你確認了當前已經使用不可變數據結構,編譯器會執行簡單的引用相等(而不是對象屬性)來確定值是否更改,以便獲得更高的性能優化,默認為 false。當此選項設置為true時,如果父組件修改了子組件的對象屬性,則子組件將不會檢測到更改並且也不會重新渲染。

    <svelte:options immmutable={true}/>
    
  • tag 可以將手寫的組件編譯成 Web Components 而讓其他框架使用。根據如下就可以使用。至於 Web Components 則可以參考阮一峰的 Web Components 入門實例教程。這也是新框架不可或缺的功能點。

    <svelte:options tag="my-custom-element"/>
    
  • accessors 可以為組件的 props 添加 getter 和 setter,默認為false。

  • namespace 是將要使用該組件的命名空間,一種用途是為 SVG 指定命名空間。

當一門語言的能力不足,而用戶的運行環境又不支持其它選擇的時候,這門語言就會淪為 “編譯目標” 語言

幾年前的 js 便是這樣的語言,當時有表現力更強的 coffeeScript,也有限制性更強的 Typescript。隨著時間的推移,前端的想要編譯的不再侷限於編程語言層面上,而在框架層面也想做的更多。

自從 2018 年末,前端框架更多的往編譯型發展 。一種是為了更強大的表現能力和性能增益,如同 Svelte 一般, 另一種則是為了抹平多個平臺的差距, 例如 國內的各個小程序框架 Taro(React 系,適合新項目), Mpx(微信小程序,適合老項目)等。

更高的表現力

對比同類型的 Elm Imba 以及 ClojureScript 這些編譯型框架,無論是工具鏈還是語法表現力, Svelte 的對於前端小夥伴們友好度是最高的。如果想要學習 Svelte,可以去看官網提供的 tutorial 模塊,Svelte 官網對於新手的友好度也是非常棒的。下面則介紹一些特定的語法。

狀態與雙向數據綁定

利用 let 可以設置狀態,利用bind:value可以進行數據綁定。

<script>
// 直接使用 數據
let name = 'world';
// 配置項,可以直接傳入 input
const inputAttrs = {
// input 類型為 text  
type: 'text',
// 最大長度
maxlength: 10
};
</script>
<!-- 名字, 同時傳遞屬性可以利用 ...語法來進行優化 -->
<input {...inputAttrs} bind:value={name} />
<hr/>
Hello {name} 

可以看到, 上述代碼很容易進行了雙向數據綁定,以及非常強大的代碼表現能力。

屬性的使用

父組件:

	
<script>
import Hello from './Hello.svelte';
</script>
<Hello name="Mark" />

Hello 組件:

	
<script>
// 這樣便是可以被外部接受,但是name也可以被內部修改
export let name = 'World';
// 計算屬性,name 修改了,doubleName 發生改變
$: doubleName = name + name
</script>
<div>
Hello, {name}!
</div>
<hr/>
{doubleName}

同組件直接的交互

這個功能就是最吸引我的地方,我用過很多組件框架。但對於同組件之間的交互都是要寫到父組件中作為業務類型組件。例如地址之間的交互(默認地址),複雜表單之間的交互, 代碼如下所示:

Input 組件

<!-- 模塊 -->
<script context="module">
// 組件全局 Map
const map = new Map();
// 清楚所有的輸入數據,導出函數
export function clearAll() {
map.forEach(clearfun => {
clearfun()
});
}
</script>
<script>
import { onMount } from 'svelte';
// 記錄的標籤
export let index;
let value = ''
// 掛載時候把當前組件的標籤和函數放入map
onMount(() => {
map.set(index, clear);
});
// 把當前 input 元素的數值清空
function clear  ()  {
value = ''
}
// 輸入時候把其他的數據清除
function clearOthers() {
map.forEach((clearfun, key) => {
if (key !== index) clearfun();
});
}
</script>
<div>
<button on:click={clear}>清楚當前輸入數據</button>
<input on:input={clearOthers} type="text" bind:value={value}>
{value}
</div>

App 組件:

<script>
import Input, { clearAll } from './Input.svelte'
</script>
<div>
// 可以清楚子組件輸入的全部數據
<button on:click={clearAll}>清楚全部數據</button>
<Input index='1'/>
<Input index='2'/>
<Input index='3'/>
<Input index='4'/>
<Input index='5'/>
</div>

如此,同組件內之間的交互便完成了,非常的簡單,但是又是因為 module 全局性的,無論是否在同一父組件內,所有的子組件都會有全局的功能。如果有兩個以上的模塊在同一頁面中,又需要添加多餘的屬性來為輔助開發。所以這個功能是一把可能會傷到自己的利器。

其他

Svelte 有許多簡單好用的語法以及動畫,這裡就不一一介紹了。因為實在是太簡單了,如果你使用過其他類型的框架,可能不到幾小時就可以上手寫業務代碼了。當然,如果在開發插件過程中遇到一些不可避免的問題,我也會記錄下來再寫一篇 blog 。

無可避免的缺點

Svelte 已經開發到 3.0 版本後了,大致上來看,一些開發上的問題可能沒有,但是畢竟沒有大公司支持,所以可能還是會有一些不可避免的缺陷。

  • 不支持 TypeScript

  • 生態環境不夠好

  • 單組件文件後綴名為 svelte,過於冗長

  • 暫時沒有構建工具

  • if 判斷 for 循環 不好用(為了編譯)

    
    {#if user.loggedIn}
    <button on:click={toggle}>
    Log out
    </button>
    {/if}
    {#if !user.loggedIn}
    <button on:click={toggle}>
    Log in
    </button>
    {/if}
    {#each cats as { id, name }, i}
    <li>
    <a target="_blank" href="https://www.youtube.com/watch?v={id}">
    {i + 1}: {name}
    </a>
    </li>
    {/each}
    

鼓勵一下

如果你覺得這篇文章不錯,希望可以給與我一些鼓勵,在我的 github 博客下幫忙 star 一下。
博客地址

參考文檔

Svelte 官網

相關文章

《程序人生》2020無畏年少青春,迎風瀟灑前行|年度徵文

將前端技術棧移植到掌上遊戲機

圖解JavaScript對象—現代JavaScript教程

MySQL二進制日誌複製、GTID複製與半同步複製