关于 client-side-js
面向 Val Town 开发者的客户端 JavaScript 技能,无需构建步骤即可向浏览器提供模块。内容涵盖转译、导入、不可变缓存和验证。
主要功能
- 01提供并转译 TS、TSX 和 JSX
- 02使用带版本的不可变文件 URL
- 03通过明确 URL 解析浏览器导入
- 04验证响应类型和浏览器错误
适用场景
- 01无需打包器构建浏览器界面
- 02从 Val 提供本地模块和资源
- 03缓存客户端文件以供重复访问
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ val-town/ plugins --skill client-side-js安装后在 Claude Code 中输入/client-side-js确认已加载。
详细介绍
客户端 JavaScript
Val Town 没有构建步骤,也没有打包器。客户端模块就是 Val 中的一个文件,通过 HTTP 提供服务;Val Town 会在每次请求时转译该文件。只需让 <script type="module"> 指向一个返回该文件的路由,浏览器就会运行它。无需进行任何配置(没有 webpack/vite/esbuild)。
提供模块服务
std/utils 中的 serveFile 会读取文件,并使用正确的 Content-Type 提供服务。对于 .ts、.tsx 和 .jsx 文件,它会转译为 JavaScript——移除类型、编译 JSX——并以 text/javascript 提供服务。你提供的是源文件;浏览器收到的是可运行的 JS。
import { serveFile } from "https://esm.town/v/std/utils/index.ts";
// in any HTTP handler — serve a client module at some URL path
app.get("/app.tsx", (c) => serveFile("/app.tsx"));
然后从 HTML 中加载它:
<script type="module" src="/app.tsx"></script>
服务路径和文件所在位置都由你决定。常见的快捷方式是使用通配符,一次提供整个目录中的模块和资源:
app.get("/client/**/*", (c) => serveFile(c.req.path));
serveFile 默认使用当前 Val。如果你从非入口文件调用它,且路径无法解析,请将 import.meta.url 作为第二个参数传入。
默认方式:带版本号、不可变缓存的模块
serveImmutableFile 可以利用浏览器的不可变缓存加快 Val 前端速度;发布时会递增 Val 的版本号,从而自动使缓存失效。实测:重复访问耗时从 665ms 降至 157ms,且无需请求任何资源。
import { immutableFileUrl, serveImmutableFile } from "https://esm.town/v/std/utils/index.ts";
app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));
在永不缓存的 HTML 外壳中,为入口模块添加版本号:
immutableFileUrl("/frontend/index.tsx") → /__immutable/42/frontend/index.tsx
(42 = Val 当前的版本号)。相对导入会在相同前缀下解析,因此只需为入口添加版本号——一个路由和一个带版本号的 URL 就能覆盖整个客户端依赖图。
- 发布后,旧版本 URL 会返回 404(类似 Next.js 的构建资源);重新加载即可获取新版本。
- 想要为现有 Val 添加此功能,又不修改外壳?也可以让旧文件路由指向
serveImmutableFile——裸路径会重定向(302)到带版本号的路径,每次页面浏览仅需一次重定向。
替代方案:直接从 esm.town 提供服务
每个 Val 文件都有一个公开的 esm.town URL,可按需转译,因此你可以跳过 serveFile,直接让脚本指向该 URL:
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>
通常更推荐使用 serveFile,因为模块会通过你控制的路径从同源提供服务,而且你无需硬编码自己的 Val URL。
浏览器如何解析导入
转译器不会打包或重写导入——只会移除类型和 JSX。因此,客户端模块中的每个导入都必须是浏览器可以通过 URL 获取的内容:
-
本地导入需要显式扩展名。
import { x } from "./util.ts"会解析为/util.ts(或相对于所提供服务的路径),并且该文件也必须通过同一路由或通配符提供服务。省略扩展名(./util)会返回 404。 -
第三方依赖需要完整的 ESM URL。 像
import React from "react"这样的裸模块说明符无法在浏览器中解析。请从 esm.sh 等 CDN 导入,并固定版本:import { createRoot } from "https://esm.sh/[email protected]/client";
如果想在客户端代码中使用裸模块说明符,也可以在 HTML 中添加 导入映射。
此模式适用于任何客户端代码——React、原生 DOM 脚本、canvas 游戏循环、Alpine、htmx。区别仅在于导入内容;对于没有依赖的普通 .ts 模块,完全无需从 CDN 加载任何内容。
React 特别说明
将所有 React 系列导入固定为相同版本(18.2.0),并在依赖 React 的库上添加 [email protected],[email protected]。副本版本不一致会导致 Cannot read properties of null (reading 'useState')。JSX 和样式约定请参阅 react-ui 技能。
注意事项
- 不要把应用逻辑写在内联
<script>代码块或模板字符串 HTML 中。 将客户端代码放在真正的.ts/.tsx文件中,以便进行类型检查、lint 和代码审查。少量内联引导代码可以接受;应用代码则不行。 - 不要使用打包器或构建命令。 无需添加构建步骤。
- Hono 的
serveStatic在 Val Town 上无法使用——请改用serveFile。
验证更改
获取模块的 URL(例如 /app.tsx),并确认其返回的是 text/javascript,而不是 HTML 或错误信息。将 https://esm.town/v/std/catch 添加到 HTML 外壳中,以便将浏览器错误传递到 get_logs,然后加载页面并检查日志。两项都完成后,才能报告更改已完成。