Skip to content

博客

Svelte With JSDoc and Typescript

在一些情况下(如小项目等),我更喜欢用 Javascript ,同时也不介意使用 Typescript, 特别在一些较大的项目,特别是在使用 Svelte 进行表单验证的时候。 在使用 Svelte 不涉及表单验证等复杂场景的时候, 直接使用 Javascript 和 JSDoc 也可以获得不错的类型提示体验也很方便。 但在更复杂的场景如使用SuperformsZod 进行表单验证的时候, Typescript 会有更友好的编码体验(类型提示和类型检查)。

类型注释和使用可以在同一个文件,如:

/**
* @typedef Invoice
* @type {Object}
* @property {string} invoice
* @property {string} paymentStatus
* @property {string} totalAmount
* @property {string} paymentMethod - payment method
*/
/** @type {Array<Invoice>} */
export const invoices = [
{
invoice: "INV001",
paymentStatus: "Paid",
totalAmount: "$250.00",
paymentMethod: "Credit Card"
}
]

也可以在跨文件共享使用类型注释:

types.js
/**
* @typedef Invoice
* @type {Object}
* @property {string} invoice
* @property {string} paymentStatus
* @property {string} totalAmount
* @property {string} paymentMethod - payment method
*/
// file: invoice.js
/** @type { import('./types.js').Invoice[] } */
export const invoices = [
{
invoice: "INV001",
paymentStatus: "Paid",
totalAmount: "$250.00",
paymentMethod: "Credit Card",
},
];

在 Svelte components、Props、Slots、Events、Bindings 时,使用 Typescript 只需要将 lang="ts 添加到 script 标签:

  • components
<script lang="ts">
let name: string = 'world';
function greet(name: string) {
alert(`Hello, ${name}!`);
}
</script>
  • Props
<script lang="ts">
export let name: string;
</script>
  • Slots
<script lang="ts">
export let name: string;
</script>
<slot {name} />
<!-- Later -->
<Comp let:name>
<!-- ^ Inferred as string -->
{name}
</Comp>
  • Events
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
event: null; // does not accept a payload
click: string; // has a required string payload
type: string | null; // has an optional string payload
}>();
function handleClick() {
dispatch('event');
dispatch('click', 'hello');
}
function handleType() {
dispatch('event');
dispatch('type', Math.random() > 0.5 ? 'world' : null);
}
</script>
<button on:click={handleClick} on:keydown={handleType}>Click</button>

使用 zod validation library 时使用 Typescript 会更方便:

const accountSchema = z.object({
name: z
.string({ required_error: 'Required.' })
.min(2, 'Name must be at least 2 characters.')
.max(30, 'Name must not be longer than 30 characters.'),
language: z.enum(languages.map(lang => lang.value) as [Language, ...Language[]]),
dob: z
.string()
.datetime()
.optional()
.refine((date) => (date === undefined ? false : true), 'Please select a valid date.')
});
type AccountSchema = typeof accountSchema

此时如果不是在.ts文件使用zod, 而是有JSDoc注释 的常规.js文件,可以使用下面的方式,在JSDoc 中推断Zod类型:

export const _formSchema = z.object({
username: z.string().min(2, '最少两个字符').max(50, '最多50个字符'),
email: z.string().email('无效邮箱地址'),
bio: z.string().min(4).max(160).default("I own a computer."),
urls: z
.array(z.string().url())
.default(["https://shadcn.com", "https://twitter.com/shadcn"]),
})
// Extract the inferred type as a JSDoc type
/** @typedef { z.infer<typeof _formSchema>} _form */

Svelte UI Components

Svelte 有 React 模块化,但程序体积更小,也有Vue 的简单,但我觉得Svelte 更新简洁。Svelte 是一种编译型框架,构建时将组件的逻辑转换为高效的原生JavaScript代码。 由于不使用虚拟DOM,Svelte具有卓越的性能表现,这点和React 、 Svelte 有非常大的不同,也是尝试它的一个原因之一。

React、Svelte 和 Vue 都是用于构建用户界面的流行 JavaScript 框架。它们都提供了声明式编程范式,可让您轻松创建和维护复杂的 UI。但是,这三个框架之间也存在一些关键差异。

  • React

React 是由 Facebook 于 2015 年创建的,是目前最受欢迎的前端框架之一。它使用虚拟 DOM 来有效地更新 UI,并提供广泛的功能和组件库。React 以其灵活性和可扩展性而闻名,但其学习曲线可能有点陡峭。

  • Svelte

Svelte 是 2016 年由 Rich Harris 创建的,是一个相对较新的框架。它使用编译器方法来将您的代码转换为高效的底层代码,从而提供卓越的性能。Svelte 还具有响应式系统,可确保您的 UI 始终保持最新状态。Svelte 以其易用性和简洁性而闻名,但它还没有 React 那么大的社区或生态系统。

  • Vue

Vue 是 2014 年由 Evan You 创建的,是一个渐进式框架。它旨在易于学习和使用,同时仍提供构建复杂应用程序所需的功能。Vue 提供了模板语法,可让您轻松创建 UI,以及用于路由和状态管理的强大工具。Vue 以其平衡性易用性和功能性而闻名。

  • 比较
特性ReactSvelteVue
创建者FacebookRich HarrisEvan You
发布日期201520162014
范式声明式声明式声明式
视图更新虚拟DOM编译器响应式系统
学习曲线中等容易容易
功能 广相对较少平衡
社区和生态系统大型中型中型

Component Library:

Headless UI Svelte

Tailwind CSS Components:

CSS only:

[Rust]thiserror错误处理

thiserror库提供了方便的派生宏,简化了Rust中自定义错误的创建和处理。

编程时,错误处理是无法规避且重要的地方,和Golang一样,Rust没有异常,只有错误。在使用Rust时,编译器会不断督促我们,处理哪些错误。这也使得程序 更加健壮,因为这使得代码运行部署前,就能发现错误并进行适当处理,基本不存在漏掉不处理的情况。

可恢复的(recoverable)和 不可恢复的(unrecoverable)错误

Section titled “可恢复的(recoverable)和 不可恢复的(unrecoverable)错误”

Rust 将错误分为两大类:可恢复的(recoverable)和 不可恢复的(unrecoverable)错误。使用Result<T,E>类型,处理可恢复的错误,panic!宏,在程序遇到 不可恢复的错误时直接中断程序的执行。

Rust 使用Result类型类处理可恢复的错误。

enum Result<T, E>
Ok(T),
Err(E),
}

Result 是一个枚举类型,TE是泛型类型参数,程序成功运行时返回的值T, 程序运行失败时返回的错误类型E。

Rust在标准库中提供了一个trait,sdt::error::Error,目前错误处理都是基于这个trait来进行,一个结构体/枚举如果实现了这个trait,那么我们认为,它就是一个错误类型。

/// Errors may provide cause information. [`Error::source()`] is generally
/// used when errors cross "abstraction boundaries". If one module must report
/// an error that is caused by an error from a lower-level module, it can allow
/// accessing that error via [`Error::source()`]. This makes it possible for the
/// high-level module to provide its own errors while also revealing some of the
/// implementation for debugging.
#[stable(feature = "rust1", since = "1.0.0")]
#[cfg_attr(not(test), rustc_diagnostic_item = "Error")]
#[rustc_has_incoherent_inherent_impls]
#[allow(multiple_supertrait_upcastable)]
pub trait Error: Debug + Display {
#[stable(feature = "error_source", since = "1.30.0")]
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
...
}

使用thiserror宏,可以使用极大简化,Cargo.toml中添加:

[dependencies]
thiserror = "1.0"

编译器支持:要求rustc 1.56+。

use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}

只要为struct或者每个成员提供#error("..."),那么就会自定义的错误生成Display实现,错误消息支持从错误中插入字段简写:

  • #[error(“{var}”)] ⟶ write!(”{}”, self.var)
  • #[error(“{0}”)] ⟶ write!(”{}”, self.0)
  • #[error(“{var:?}”)] ⟶ write!(”{:?}”, self.var)
  • #[error(“{0:?}”)] ⟶ write!(”{:?}”, self.0)

对于每一个含有#[from]属性的,都会生成一个From实现。

#[derive(Error, Debug)]
pub enum MyError {
Io {
#[from]
source: io::Error,
backtrace: Backtrace,
},
}

错误可以使用 error(transparent)sourceDisplay 方法直接转发到底层错误,而不添加额外的消息。这适用于需要“任何其他”变体的枚举。

use log::SetLoggerError;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("{0} not found")]
NotFoundError(String),
#[error(transparent)]
RedisError(#[from] redis::RedisError),
}

#[from]属性标记意味着redis::RedisError会被AppError中的RedisError转换为AppError::RedisError

thiserror 库作者建议到,如果你是在开发库,涉及自己的专用错误类型,便于调用者发生错误时准确收到信息,则用thiserror ,而如果是开发应用,不关心返回的是什么错误类型,只是希望处理起来更简单则使用 anyhow 。

Vite 加载 wasm-pack 构建ES模块

学习rust 构建 WebAssembly 的官方教程,教程中是使用 webpack 作为 Javascript 打包器和开发服务器的(注意,使用 Rust 和 WebAssembly 不要求 webpack 或者其他类似工具,只是为了开发方便)。但不太喜欢 webpack 配置太过繁琐,而以前常用的 Parcel 对 WebAssembly 支持不太友好,而 Vite 正好可以满足需要,所以使用了Vite, 下面记录的配置。

在学习一段时间zig来构建wasm后,发现 zig 语言本身用起来的是很舒服,可也有些问题,主要还是语言还没有发布1.0版本,语言生态 环境还不够完善,就连 build.zig 构建 API 也在频繁变动中,Github.com 上的相关包在 zig 最新版本也无法顺利通过构建。其他的相关包,就更少了。 相比于此, rust 社区在2018开始就将 WebAssembly 作为,注重改善编程体验几个不同领域之一(其他还有Command Line、Networking, Embedded)。用 rust 来开发 WebAssembly 过程要舒适非常多,相关的生态也非常完备,社区活跃度也很高。

wasm-pack build 命令支持多个 --target 参数,这里选择的是 web, 输出可以在浏览器中作为ES模块原始导入的JS,其他几个参数可以相见官方文档Deploying Rust and WebAssembly

Vite 5.0 版本对 WebAssembly 的 ES 模块集成提案 尚未支持。 请使用 vite-plugin-wasm 或其他社区上的插件来处理。

将构建出来的本地wasm package 添加到 package.json 中:

"dependencies": {
"wasm-game-of-life": "file:../pkg"
},
Terminal window
yarn add --dev vite@5.0.0

根目录下添加配置文件vite.config.js:

import { defineConfig } from 'vite'
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({});
Terminal window
yarn add --dev vite-plugin-wasm
yarn add --dev vite-plugin-top-level-await
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
plugins: [
wasm(),
topLevelAwait()
],
optimizeDeps: {
exclude: ["wasm-game-of-life"],
},
});

完成代码放在Github

Zig_allcator

Zig 语言不干预堆内存分配, 没有隐藏的内存分配,没有new关键字或其他任何使用堆分配器的语言功能,整个堆都是由库或者用户代码而非语言本身所管理的。

Zig 标准库提供了一中分配内存的模式,这允许程序员精准选择标准库中内存如何完成分配,在标准库中不会背着你偷偷分配内存。

Zig 提供的分配器 allocator 有:

释放内存惯用模式是使用deferdefer 在退出作用域时指定给定的代码,『作用域退出』包括到达作用域的结尾或从作用域返回。

Zig 的 defer 类似于 Go 的 defer,但存在一个主要区别。在 Zig 中,defer 将在其包含作用域的末尾运行。在 Go 中,defer 是在包含函数的末尾运行。

通过使用defer,指定分配器在退出作用域时释放分配的内存。

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
var arr = try allocator.alloc(usize, try getRandomCount());
defer allocator.free(arr);
for (0..arr.len) |i| {
arr[i] = i;
}
std.debug.print("{any}\n", .{arr});

这是最基本的分配器,线程安全且无锁,每次内存的分配(allocation)和释放(free),都会触发一次直接的系统调用,它会直接要求操作系统分配或者释放整页内存, 即使是单直接内存分配也可能导致保留数千字节的内存。由于通过系统调用要求操作系统 OS 分配内存,这对于速读来说也是及其低效的。

const std = @import("std");
const expect = std.testing.expect;
test "allocation" {
const allocator = std.heap.page_allocator;
const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);
try expect(memory.len == 100);
try expect(@TypeOf(memory) == []u8);
}
const page_allocator: type = if (builtin.target.isWasm())
Allocator{
.ptr = undefined,
.vtable = &WasmPageAllocator.vtable,
}
else if (builtin.target.os.tag == .plan9)
Allocator{
.ptr = undefined,
.vtable = &SbrkAllocator(std.os.plan9.sbrk).vtable,
}
else if (builtin.target.os.tag == .freestanding)
root.os.heap.page_allocator
else
Allocator{
.ptr = undefined,
.vtable = &PageAllocator.vtable,
};

这是一种通用的、线程安全的分配器,可以作为程序的主分配器。这是一个安全的分配器,可以防止双重释放( double-free )、使用后释放( use-after-free) 还可以检测泄漏。可以通过它的配置结构(.{}),将安全检测和线程安全关闭掉:

test "GPA" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const bytes = try allocator.alloc(u8, 100);
defer allocator.free(bytes);
}