client-side-js

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Client-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
<script type="module">
at a route that returns the file, and the browser runs it. There is nothing to configure (no webpack/vite/esbuild).
Val Town 没有构建步骤,也没有 bundler。客户端模块只是一个位于你的 val 中、通过 HTTP 提供的文件;Val Town 会在每次请求时对其进行转译。你只需让
<script type="module">
指向一个返回该文件的路由,浏览器便会运行它。这里没有任何需要配置的内容(无需 webpack/vite/esbuild)。

Serving a module

提供模块

serveFile
from
std/utils
reads a file and serves it with the correct
Content-Type
. For
.ts
,
.tsx
, and
.jsx
it transpiles to JavaScript — strips types, compiles JSX — and serves
text/javascript
. You serve the source file; the browser receives runnable JS.
ts
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));
serveFile
defaults to the current val. If you call it from a non-entrypoint file and paths don't resolve, pass
import.meta.url
as the second argument.
std/utils
中的
serveFile
会读取文件,并以正确的
Content-Type
提供文件。对于
.ts
、
.tsx
和
.jsx
,它会转译为 JavaScript——移除类型标注、编译 JSX——然后以
text/javascript
提供。你提供的是源文件;浏览器收到的是可直接运行的 JS。
ts
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));
serveFile
默认使用当前 val。如果从非入口点文件调用它时路径无法解析,请将
import.meta.url
作为第二个参数传入。

Default: versioned, immutably cached modules

默认:带版本且不可变缓存的模块

serveImmutableFile
makes your val's frontend faster by letting browsers cache files immutably; publishing bumps the val's version, which invalidates automatically. Measured: repeat visits 665ms → 157ms with zero asset requests.
ts
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:
immutableFileUrl("/frontend/index.tsx")
→
/__immutable/42/frontend/index.tsx
(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.
  • 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
    serveImmutableFile
    — bare paths then 302 into versioned space, at one redirect per page view.
serveImmutableFile
允许浏览器不可变地缓存文件,从而提升 val 前端的性能;每次发布都会更新 val 的版本号并自动使缓存失效。实测:重复访问时间从 665ms 降至 157ms,且不产生任何资源请求。
ts
import { immutableFileUrl, serveImmutableFile } from "https://esm.town/v/std/utils/index.ts";

app.get("/__immutable/*", (c) => serveImmutableFile(c.req.path));
在从不缓存的 HTML shell 中,标记入口模块:
immutableFileUrl("/frontend/index.tsx")
→
/__immutable/42/frontend/index.tsx
(42 = val 的当前版本)。相对 import 会在相同前缀下解析,因此只需标记入口——一条路由和一个带版本标记的 URL 即可覆盖整个客户端模块图。
  • 发布后,旧版本 URL 会返回 404(类似 Next.js 构建资源);刷新页面即可获取新版本。
  • 如果想在不改动 shell 的情况下改造现有 val,也可以将原有文件路由指向
    serveImmutableFile
    ——此时裸路径会通过 302 重定向进入带版本的空间,每次页面查看仅发生一次重定向。

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
serveFile
and point a script straight at it:
html
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>
serveFile
is usually preferred because the module is served same-origin from a path you control, and you don't have to hardcode your own val URL.
每个 val 文件都已拥有一个公开的 esm.town URL,并会按需转译,因此你可以跳过
serveFile
,直接让 script 指向它:
html
<script type="module" src="https://esm.town/v/youruser/yourval/app.tsx"></script>
通常更推荐使用
serveFile
,因为模块会从你控制的路径以同源方式提供,而且无需硬编码自己的 val URL。

How 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.
    import { x } from "./util.ts"
    resolves to
    /util.ts
    (or relative to the served path) and must be served too — by the same route or a wildcard. Omitting the extension (
    ./util
    ) 404s.
  • Third-party deps need full ESM URLs. Bare specifiers like
    import React from "react"
    don't resolve in the browser. Import from a CDN such as esm.sh, with versions pinned:
    ts
    import { 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
.ts
module with no dependencies there's nothing to load from a CDN at all.
转译器不会打包或重写 import——它只会移除类型标注和 JSX。因此,客户端模块中的每个 import 都必须是浏览器能够作为 URL 获取的内容:
  • 本地 import 需要明确指定扩展名。
    import { x } from "./util.ts"
    会解析为
    /util.ts
    (或相对于所提供路径进行解析),并且该文件也必须被提供——可通过同一路由或通配符提供。省略扩展名(
    ./util
    )会返回 404。
  • 第三方依赖需要完整的 ESM URL。 浏览器无法解析
    import React from "react"
    这样的裸标识符。你应当从 esm.sh 等 CDN 导入,并固定版本:
    ts
    import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
    如果希望在客户端代码中使用裸标识符,也可以在 HTML 中添加 import map。
同一模型适用于所有客户端代码——React、原生 DOM 脚本、canvas 游戏循环、Alpine、htmx。唯一区别在于 import;对于不含依赖的普通
.ts
模块,完全不需要从 CDN 加载任何内容。

React specifics

React 注意事项

Pin all React-family imports to the same version (18.2.0) and pass
?deps=react@18.2.0,react-dom@18.2.0
on libraries that depend on React. Mismatched copies cause
Cannot read properties of null (reading 'useState')
. See the
react-ui
skill for JSX and styling conventions.
将所有 React 系列 import 固定到同一版本(18.2.0),并在依赖 React 的库上传递
?deps=react@18.2.0,react-dom@18.2.0
。副本版本不匹配会导致
Cannot read properties of null (reading 'useState')
。有关 JSX 和样式约定,请参阅
react-ui
skill。

What not to do

不应采用的做法

  • No app logic in inline
    <script>
    blobs or template-string HTML.
    Put client code in real
    .ts
    /
    .tsx
    files so it's typed, linted, and reviewable. A few lines of inline bootstrap are fine; the app is not.
  • No bundler / build command. There is no build step to add.
  • serveStatic
    from Hono does not work
    on Val Town — use
    serveFile
    .
  • 不要在行内
    <script>
    代码块或模板字符串 HTML 中放置应用逻辑。
    应将客户端代码放入真正的
    .ts
    /
    .tsx
    文件中,以便进行类型检查、lint 和审查。少量行内 bootstrap 代码没有问题,但整个应用不应如此编写。
  • 不要使用 bundler / 构建命令。 没有可添加的构建步骤。
  • Hono 的
    serveStatic
    在 Val Town 上无法使用
    ——请改用
    serveFile
    。

Verifying changes

验证更改

Fetch the module's URL (e.g.
/app.tsx
) and confirm it returns
text/javascript
, not HTML or an error. Add
https://esm.town/v/std/catch
to the HTML shell to pipe browser errors into
get_logs
, then load the page and check the logs. Don't report the change as done without both.
获取模块的 URL(例如
/app.tsx
),确认返回的是
text/javascript
,而不是 HTML 或错误。将
https://esm.town/v/std/catch
添加到 HTML shell,把浏览器错误传递到
get_logs
,然后加载页面并检查日志。两者都完成前,不要将这项更改报告为已完成。