Client Side Js

作者 val-town2d3ec654b6a7無授權條款收錄於 2026年10月8日更新於 2026年10月8日

Use when a val needs to ship JavaScript that runs in the browser — React apps, vanilla DOM scripts, canvas/games, htmx/Alpine, or any client-side module beyond a single inline snippet. Explains how Val Town serves transpiled .ts/.tsx/.jsx modules with no build step, how the browser resolves their imports, and how to load third-party deps.

AI 產生的概覽

說明如何在 Val Town 上不必建置步驟就能提供並載入用戶端 JavaScript 模組。

功能
此技能說明 Val Town 如何透過 HTTP 提供轉譯後的 .ts、.tsx 與 .jsx 模組,讓瀏覽器不必使用打包工具即可執行。內容涵蓋以 serveFile 提供檔案、以 serveImmutableFile 達成帶版本的不可變快取、瀏覽器對本機與第三方模組的匯入解析方式,以及 React 版本鎖定。它也列出應避免的做法,以及如何驗證所提供模組回傳的是 JavaScript。
適用情境
當需要從 Val Town 的 val 發布瀏覽器端 JavaScript 時使用,例如 React 應用程式、原生 DOM 指令碼、canvas 遊戲、htmx 或 Alpine,或任何超過單段內嵌程式碼的用戶端模組。在排查模組提供、匯入解析或快取行為時也適用。
執行需求
執行於 Val Town,不需要建置步驟或打包工具。使用 std/utils 中的 serveFile、serveImmutableFile 與 immutableFileUrl 輔助函式,並可能從 esm.sh 等 CDN 載入第三方 ESM 相依套件。需要網路存取以提供模組並取得相依套件。不附帶指令碼,僅為說明文件。

Client-side 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).

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 pathapp.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.

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.

Alternative: serve directly from 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.

How imports resolve in the browser

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/[email protected]/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.

React specifics

Pin all React-family imports to the same version (18.2.0) and pass [email protected],[email protected] 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.

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.

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.

來源與署名

來源:val-town/plugins位於plugin/skills/client-side-js提交2d3ec65

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架