1936 字
10 分钟
Svelte邂逅Web Components了解前端趋势

Svelte邂逅Web Components了解前端趋势#

介绍#

IE的退出为底层前端技术的发展注入了新活力。以往,为了确保在不同浏览器上的兼容性,Vue与React等主流框架引入了大量中间层来实现功能,这导致各框架构建了复杂的渲染生命周期,学习难度大,技术栈转换也不够便捷。但如今,随着新浏览器API的不断涌现以及ECMAScript标准与W3C标准的持续演进,原本依赖中间层实现的功能正逐步得到浏览器的原生支持。
在这一趋势下,SvelteJS等框架脱颖而出。它没有在原生浏览器生命周期中添加过多额外的生命周期与上下文,却能实现与Vue和React相媲美的功能。SvelteJS的学习曲线较为平缓,非常适合刚学完HTML、CSS、JavaScript的开发者进阶学习。而且,熟悉SvelteJS后,再切换到Vue或React也会相对容易,因此越来越受到开发者的关注和喜爱。在编译时就将html、js、css一次性编译好,而不需要像vue与react一样在带一个运行时,生为后来者的他避开了以前框架的坑,站在了巨人的肩膀上,目前Start数量已经与3大框架差不了太多了。
Web Component 标准的诞生为前端开发者带来了巨大便利,然而其问世时机稍显尴尬,恰逢 Vue、React、Angular 等框架风头正劲,导致其光芒被掩盖,至今仍就不温不火。但 Web Component 拥有独特优势,它基于浏览器接口实现,与所使用的 JavaScript 框架无关,只需编写一次组件,便可在多种框架中无缝使用。

上手#

如果你有学过vuejs2那么你上手的时候会非常熟悉svelte的语法以及声明式的绑定,例如下面这段代码+layout.svelte 中的personalizaDrawer。其代码分为3块script、html、css这3块,设计与vue有着很大相似之处,代码清晰明了。当如如果想具体了解学习还是要去svelte.dev,里面有交互式学习很容上手的,概念也不多都是很目前很成熟的一些东西。

<script>
import { goto } from '$app/navigation';
import '../app.css';
import { Circle2 } from 'svelte-loading-spinners';
import { globalState } from '$lib/store';
import { EventsOn, EventsOff } from '$lib/wailsjs/runtime';
import '@fluentui/web-components/drawer.js';
import '@fluentui/web-components/button.js';
import '@fluentui/web-components/dialog.js';
import Options from '$lib/components/options/index.svelte';
import Personaliza from '$lib/components/personaliza/index.svelte';
import About from '$lib/components/about/index.svelte';
import { GetActivatedTheme } from '$lib/wailsjs/go/main/App';
import { onMount } from 'svelte';
import { changeMainThemeEvent, loadBackgroundImage } from '$lib/theme';
/** @type {{children: import('svelte').Snippet}} */
let { children } = $props();
/** @type {import('@fluentui/web-components').Drawer} */
let optionsDrawer;
/** @type {import('@fluentui/web-components').Drawer} */
let personalizaDrawer;
/** @type {import('@fluentui/web-components').Dialog} */
let aboutDialog;
function heidOptionsDrawer() {
optionsDrawer.hide();
}
function heidPersonalizaDrawer() {
personalizaDrawer.hide();
}
try {
EventsOff('optionsEvent', 'personalizaEvent', 'aboutEvent');
EventsOn('aboutEvent', (event) => {
aboutDialog.show();
});
EventsOn('optionsEvent', (event) => {
optionsDrawer.show();
personalizaDrawer.hide();
aboutDialog.hide();
});
EventsOn('personalizaEvent', (event) => {
personalizaDrawer.show();
optionsDrawer.hide();
aboutDialog.hide();
});
} catch (error) {
console.error(error);
}
onMount(async () => {
changeMainThemeEvent(await GetActivatedTheme());
});
</script>
<div class="app h-full">
<div class="background-image"></div>
<main class="h-full">
{@render children()}
</main>
<footer></footer>
<fluent-drawer class="m-0 p-0" type="model" position="end" size="full" bind:this={optionsDrawer}>
<Options heid={heidOptionsDrawer} />
</fluent-drawer>
<fluent-drawer
class="m-0 p-0"
type="model"
position="end"
size="full"
bind:this={personalizaDrawer}
>
<Personaliza heid={heidPersonalizaDrawer} />
</fluent-drawer>
<fluent-dialog bind:this={aboutDialog}>
<About />
</fluent-dialog>
{#if $globalState.loading}
<div class="background-image"></div>
<div
class="loading bg-base absolute left-0 right-0 top-0 z-50 flex h-lvh w-full items-center justify-center"
>
<Circle2 size="50" unit="lvh" />
</div>
{/if}
</div>
<style>
main {
opacity: var(--edit-opacity);
}
.loading {
opacity: var(--edit-opacity);
}
.background-image {
opacity: 1;
position: absolute;
top: 0;
left: 0;
width: 100lvw;
height: 100lvh;
background-image: var(--background-image); /* 设置背景图片路径 */
background-size: cover; /* 调整背景图片大小以覆盖整个容器 */
background-position: center; /* 居中背景图片 */
}
</style>

在上面这段代码中使用了Microsoft开源的fluentui的web component组件,以dialog举例,导入依赖后,使用起来就和普通的组件一样<fluent-dialog></fluent-dialog>。上面这段代码还是比较复杂的,来看个干净简单一点的吧index.html。里头只用了原生的html与js代码。其中的fluent-buttonfluent-text-area全是使用fluentui的web-components组件,当然感兴趣的话可以去看看fluentui的组件源码。第一次使用的时候我也是一头雾水无从下手,但是在查看了fluentui 组件定义的index.d之后逐渐会用了。

<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
<script type="module" src="https://unpkg.com/@fluentui/web-components">
</script>
<title>HyperLinkStretch</title>
<style>
.content {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
align-items: center;
width: 100%;
height: 95vh;
}
.input {
width: 100%;
display: flex;
align-content: center;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.input_area {
max-height: 2.5rem;
max-width: 40rem;
min-width: 13rem;
flex: 5;
margin-bottom: 0.5rem;
}
.input_button {
height: 2.5rem;
flex: 1;
max-width: 5rem;
min-width: 3rem;
margin-bottom: 0.5rem;
}
.result {
width: 100%;
}
.result_card {
margin: 0 auto;
max-width: 46rem;
min-width: 13rem;
padding: 0.5rem;
}
.result_qr img {
width: 100%;
height: auto;
}
.result_qr {
width: min(90svw, 90svh);
height: min(90svw, 90svh);
padding: 1rem;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.result_link {
flex: 5;
max-width: 100%;
max-height: 10rem;
padding-left: 1rem;
overflow-y: scroll;
}
.result_link_url {
word-wrap: break-word;
}
.result_button {
flex: 3;
position: relative;
right: 0.5rem;
text-align: right;
height: 2rem;
min-width: 10rem;
/* white-space: nowrap; */
}
.result_tool {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 1rem;
padding-bottom: 0.5rem;
}
a {
display: inline-block;
padding: 10px 20px;
color: #000;
text-decoration: none;
transition: 0.3s ease;
}
a:hover {
color: #0473ce;
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #000, 1px 1px 0 #000;
}
@media (max-width: 41rem) {
.content {
height: auto;
}
.result_link {
max-height: 100%;
overflow-y: auto;
}
.result_tool {
justify-content: center;
}
#show_qr {
display: none;
}
#copy_result_link_url {
width: 100%;
margin-left: 0.5rem;
}
.skeleton {
display: none;
}
.result_button {
text-align: center;
max-height: 100%;
flex: 1 0 100%;
}
.input_area {
max-height: 100%;
min-height: 2.5rem;
}
.result_link {
max-width: 40rem;
min-width: 13rem;
flex: 1 0 100%;
}
.input_button {
max-width: 40rem;
min-width: 13rem;
flex: 1 0 100%;
}
}
</style>
</head>
<body>
<script type="text/javascript">
function isValidUrl(url) {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
}
async function generate(url) {
isValidUrl(url) || window.alert("请输入正确的url")
const response = await fetch(`/api/v1/generate?url=${url}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
})
const result = await response.json()
var currentUrl = window.location.href;
// 使用 URL 对象来解析 URL
var url = new URL(currentUrl);
// 从 URL 对象中提取主机名
var host = url.hostname;
// 打印主机名
const longUrl = `${window.location.protocol}//${host}${window.location.port ? ":" + window.location.port : ""}${result.targetURL}`
document.getElementById("result_link_url").innerText = longUrl
document.getElementById("qrcode").innerHTML=""
new QRCode(document.getElementById("qrcode"), {
text: longUrl,
width: 2048,
height: 2048,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRCode.CorrectLevel.H
});
document.getElementById("result").hidden = false;
document.getElementById("copy_result_link_url").setAttribute("data-clipboard-text", longUrl)
}
window.onload = () => {
document.getElementById("content").addEventListener("click", (event) => {
const url = document.getElementById("qrcode").hidden = true
})
document.getElementById("show_qr").addEventListener("click", (event) => {
event.stopPropagation()
const url = document.getElementById("qrcode").hidden = false
})
document.getElementById("qrcode").addEventListener("click", (event) => {
const url = document.getElementById("qrcode").hidden = true
})
document.getElementById("input_button").addEventListener("click", (event) => {
const url = document.getElementById("input_area").currentValue
generate(url)
})
var clipboard = new ClipboardJS('#copy_result_link_url');
clipboard.on('success', function (e) {
console.info('Action:', e.action);
console.info('Text:', e.text);
console.info('Trigger:', e.trigger);
window.alert("copy success")
});
clipboard.on('error', function (e) {
console.error('Action:', e.action);
console.error('Trigger:', e.trigger);
window.alert("copy error")
});
}
</script>
<div id="content" class="content">
<fluent-tooltip id="tooltip" anchor="anchor-default">
点击前往github仓库
</fluent-tooltip>
<h1 id="anchor-default" aria-describedby="tooltip"><a
href="https://github.com/langbiantianya/HyperLinkStretch">一个长链</a></h1>
<div class="input">
<fluent-text-area id="input_area" class="input_area"
placeholder="请输入包含”http://“或“https://”网址"></fluent-text-area>
<fluent-skeleton class="skeleton" style="width: 1rem;" shape="rect" shimmer="false"></fluent-skeleton>
<fluent-button id="input_button" class="input_button" appearance="accent">生成</fluent-button>
</div>
<div id="result" class="result" hidden>
<fluent-card class="result_card">
<div class="result_tool">
<span style="line-height: 2rem; flex: 8;">该应用用于娱乐用途,请勿在生产上使用。</span>
<div class="result_button">
<fluent-button id="show_qr">二维码</fluent-button>
<fluent-button id="copy_result_link_url" appearance="accent" data-clipboard-text=""
data-clipboard-action="copy">复制链接
</fluent-button>
</div>
</div>
<div class="result_link">
<span id="result_link_url" class="result_link_url"></span>
</div>
</div>
</fluent-card>
</div>
<div id="qrcode" class="result_qr"></div>
</body>
</html>

实际开发的例子#

例子的话上面2块代码的出处就是我写的2个项目。
其中比较纯粹的web component请看这个HyperLinkStretch一个简单的Demo。
结合了svelte与web component的请看这个cherry-markdown-webview复杂一些的项目,目前还在持续开发中。

个人认为的未来趋势#

国内的大环境目前还是很烂的,大多以外包为主,各种admin框架已经定死前端框架,国内领导层年纪都挺大的也不愿意了解新技术跟换技术栈,稳定大于一切,加上国内也没有大厂站台与推广,想真正流行起来难度还是很大的。我个人认为未来的趋势应该还是会退却多余的中间层而使用新的api,当然像React这种按需渲染的性能还是很诱人的。像是Svelte这种框架倒是很适合不怎么写前端的后端使用例如我,可以了解前端技术的变化以写出更符合前端使用习惯的接口。正所谓不会写前端的后端不是好后端,不会写后端的前端不是好前端。