client-side-js
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClient-side JavaScript
客户端 JavaScript
Val Town has no build step and no bundler. A client-side module is just a file
in your val that you serve over HTTP; Val Town transpiles it per request. You point
a at a route that returns the file, and the browser runs
it. There is nothing to configure (no webpack/vite/esbuild).
<script type="module">Val Town 没有构建步骤,也没有 bundler。客户端模块只是一个位于你的 val 中、通过 HTTP 提供的文件;Val Town 会在每次请求时对其进行转译。你只需让 指向一个返回该文件的路由,浏览器便会运行它。这里没有任何需要配置的内容(无需 webpack/vite/esbuild)。
<script type="module">Serving a module
提供模块
serveFilestd/utilsContent-Type.ts.tsx.jsxtext/javascriptts
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"));Then load it from your HTML:
html
<script type="module" src="/app.tsx"></script>The path you serve at and the file's location are up to you. A common shortcut is a
wildcard that serves a whole directory of modules and assets:
ts
app.get("/client/**/*", (c) => serveFile(c.req.path));serveFileimport.meta.urlstd/utilsserveFileContent-Type.ts.tsx.jsxtext/javascriptts
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 中加载它:
html
<script type="module" src="/app.tsx"></script>提供文件的路径及其实际位置由你决定。一种常见做法是使用通配符,提供整个模块和资源目录:
ts
app.get("/client/**/*", (c) => serveFile(c.req.path));serveFileimport.meta.urlDefault: versioned, immutably cached modules
默认:带版本且不可变缓存的模块
serveImmutableFilets
import { immutableFileUrl, serveImmutableFile } from "https://esm.town/v/std/utils/index.ts";
app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));In the never-cached HTML shell, stamp the entry module:
→
(42 = the val's current version). Relative imports resolve under the same prefix,
so only the entry needs stamping — one route and one stamped URL cover the whole
client graph.
immutableFileUrl("/frontend/index.tsx")/__immutable/42/frontend/index.tsx- Old-version URLs 404 after a publish (like Next.js build assets); a reload picks up the new version.
- Retrofitting an existing val without touching its shell? Also point its old
file route at — bare paths then 302 into versioned space, at one redirect per page view.
serveImmutableFile
serveImmutableFilets
import { immutableFileUrl, serveImmutableFile } from "https://esm.town/v/std/utils/index.ts";
app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));在从不缓存的 HTML shell 中,标记入口模块:
→
(42 = val 的当前版本)。相对 import 会在相同前缀下解析,因此只需标记入口——一条路由和一个带版本标记的 URL 即可覆盖整个客户端模块图。
immutableFileUrl("/frontend/index.tsx")/__immutable/42/frontend/index.tsx- 发布后,旧版本 URL 会返回 404(类似 Next.js 构建资源);刷新页面即可获取新版本。
- 如果想在不改动 shell 的情况下改造现有 val,也可以将原有文件路由指向 ——此时裸路径会通过 302 重定向进入带版本的空间,每次页面查看仅发生一次重定向。
serveImmutableFile
Alternative: serve directly from esm.town
备选方案:直接从 esm.town 提供
Every val file already has a public esm.town URL that transpiles on demand, so you
can skip and point a script straight at it:
serveFilehtml
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>serveFile每个 val 文件都已拥有一个公开的 esm.town URL,并会按需转译,因此你可以跳过 ,直接让 script 指向它:
serveFilehtml
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>通常更推荐使用 ,因为模块会从你控制的路径以同源方式提供,而且无需硬编码自己的 val URL。
serveFileHow imports resolve in the browser
浏览器如何解析 import
The transpiler does not bundle or rewrite imports — it only strips types and JSX.
So every import in a client module must be something the browser can fetch as a
URL:
-
Local imports need explicit extensions.resolves to
import { x } from "./util.ts"(or relative to the served path) and must be served too — by the same route or a wildcard. Omitting the extension (/util.ts) 404s../util -
Third-party deps need full ESM URLs. Bare specifiers likedon't resolve in the browser. Import from a CDN such as esm.sh, with versions pinned:
import React from "react"tsimport { createRoot } from "https://esm.sh/react-dom@18.2.0/client";An import map in the HTML is an option if you want bare specifiers in client code.
The same model works for any client code — React, vanilla DOM scripts, a canvas
game loop, Alpine, htmx. Only the imports differ; for a plain module with no
dependencies there's nothing to load from a CDN at all.
.ts转译器不会打包或重写 import——它只会移除类型标注和 JSX。因此,客户端模块中的每个 import 都必须是浏览器能够作为 URL 获取的内容:
-
本地 import 需要明确指定扩展名。会解析为
import { x } from "./util.ts"(或相对于所提供路径进行解析),并且该文件也必须被提供——可通过同一路由或通配符提供。省略扩展名(/util.ts)会返回 404。./util -
第三方依赖需要完整的 ESM URL。 浏览器无法解析这样的裸标识符。你应当从 esm.sh 等 CDN 导入,并固定版本:
import React from "react"tsimport { createRoot } from "https://esm.sh/react-dom@18.2.0/client";如果希望在客户端代码中使用裸标识符,也可以在 HTML 中添加 import map。
同一模型适用于所有客户端代码——React、原生 DOM 脚本、canvas 游戏循环、Alpine、htmx。唯一区别在于 import;对于不含依赖的普通 模块,完全不需要从 CDN 加载任何内容。
.tsReact specifics
React 注意事项
Pin all React-family imports to the same version (18.2.0) and pass
on libraries that depend on React. Mismatched
copies cause . See the
skill for JSX and styling conventions.
?deps=react@18.2.0,react-dom@18.2.0Cannot read properties of null (reading 'useState')react-ui将所有 React 系列 import 固定到同一版本(18.2.0),并在依赖 React 的库上传递 。副本版本不匹配会导致 。有关 JSX 和样式约定,请参阅 skill。
?deps=react@18.2.0,react-dom@18.2.0Cannot read properties of null (reading 'useState')react-uiWhat not to do
不应采用的做法
- No app logic in inline blobs or template-string HTML. Put client code in real
<script>/.tsfiles so it's typed, linted, and reviewable. A few lines of inline bootstrap are fine; the app is not..tsx - No bundler / build command. There is no build step to add.
- from Hono does not work on Val Town — use
serveStatic.serveFile
- 不要在行内 代码块或模板字符串 HTML 中放置应用逻辑。 应将客户端代码放入真正的
<script>/.ts文件中,以便进行类型检查、lint 和审查。少量行内 bootstrap 代码没有问题,但整个应用不应如此编写。.tsx - 不要使用 bundler / 构建命令。 没有可添加的构建步骤。
- Hono 的 在 Val Town 上无法使用——请改用
serveStatic。serveFile
Verifying changes
验证更改
Fetch the module's URL (e.g. ) and confirm it returns ,
not HTML or an error. Add to the HTML shell to pipe
browser errors into , then load the page and check the logs. Don't report
the change as done without both.
/app.tsxtext/javascripthttps://esm.town/v/std/catchget_logs获取模块的 URL(例如 ),确认返回的是 ,而不是 HTML 或错误。将 添加到 HTML shell,把浏览器错误传递到 ,然后加载页面并检查日志。两者都完成前,不要将这项更改报告为已完成。
/app.tsxtext/javascripthttps://esm.town/v/std/catchget_logs