一个完整的网站,只需一个 JavaScript 文件(续)
早在四月份,我们发布了“一个完整的网站,只需一个 JavaScript 文件”。然而,根据一些评论,我们意识到我们没有充分传达我们认为这种练习的独特之处——它的简洁性和性能,同时又不牺牲用户期望从现代网站获得的功能。
所以这是一篇后续博客文章和一个新的 playground,演示了一个功能齐全的 Web 应用程序,具有动态 API 端点、动态渲染、表单功能——所有这些都在一个 JavaScript 文件中。
动态渲染
着陆页显示您的位置、当地日期和时间。
让我们先看看位置是如何获取的
const ip = (context.remoteAddr as Deno.NetAddr).hostname;
const res = await fetch("http://ip-api.com/json/" + ip);
const data: APIData = await res.json();
return ssr(() => <Landing {...data} />);
首先我们获取 IP 地址,使用 router,IP 地址在路由回调的第二个参数中可用。一旦我们有了 IP 地址,我们就向 IP-API 发送 http 请求,它返回位置数据,例如城市和国家。我们将此数据传递给 <Landing>
组件,并在其中呈现。
为了显示当地日期和时间,我们在组件中使用 Date
构造函数和 timezone
参数进行渲染。请注意,timezone
是 IP 地址响应的一部分,与 city
和 country
一起。
function Landing({
country,
city,
timezone,
}: APIData) {
return (
<div class="min-h-screen p-4 flex gap-12 flex-col items-center justify-center">
<h1 class="text-2xl font-semibold">
Welcome to Bagel Search
</h1>
<p class="max-w-prose">
It's currently {new Date().toLocaleString("en-US", {
dateStyle: "full",
timeStyle: "medium",
timeZone: timezone,
})} in {city}, {country}—the perfect time and place to look up a bagel.
</p>
<a
href="/search"
class="px-4 py-2.5 rounded-md font-medium leading-none bg-gray-100 hover:bg-gray-200"
>
Click here to search for bagels
</a>
</div>
);
}
动态路由
为了展示动态路由,我们为每个百吉饼动态生成了一个路由,遵循以下模式:/bagels/:id
。
正如之前的帖子中提到的,router 在底层使用了 URLPattern
,因此我们可以使用适当的 URLPattern
语法进行模式匹配。在本例中,我们有 /bagels/:id
,这意味着它将匹配任何路径,该路径将是 /bagels/
后跟任何有效值(例如 /bagels/foo
,但它将无法匹配任何子路径,如 /bagels/foo/bar
)。
serve(router(
{
"/bagels/:id": (_req, _context, matches) => {
return ssr(() => <Bagel id={matches.id} />);
},
// Other routes removed in this example for simplification.
},
));
我们可以使用 matches.id
从路径中获取 :id
,因为 router 创建了一个键值对,其中 id
作为键,字符串作为 matches
中的值。
然后,我们将 id
传递给 <Bagel>
组件
function Bagel({ id }: { id: string }) {
const name = id.replaceAll("-", " ");
const bagel = bagels.find((bagel) =>
bagel.name.toLowerCase() === name.toLowerCase()
);
if (bagel === undefined) {
return (
<div>
The bagel '{name}' does not exist
</div>
);
}
return (
<div class="min-h-screen p-4 flex flex-col items-center justify-center">
<div class="w-3/4 lg:w-1/4">
<div class="w-full bg-gray-200 rounded-lg overflow-hidden">
<img
src={bagel.image}
class="w-full object-center object-cover"
alt={bagel.name}
/>
</div>
<div class="mt-3 flex items-center justify-between">
<h1 class="font-semibold">{bagel.name}</h1>
<p class="text-lg font-medium text-gray-900">
${bagel.price.toFixed(2)}
</p>
</div>
<p class="mt-1 text-gray-600">
{bagel.description}
</p>
<div class="mt-3 flex items-center justify-between">
<div>
<a href="/" class="underline text-blue-400">Home</a>
</div>
<div>
<a href="/search" class="underline text-blue-400">Back to Search</a>
</div>
</div>
</div>
</div>
);
}
首先,我们将 id
进行 slugify,使其更易于人类阅读。接下来,我们使用该 slug 来匹配数据中百吉饼名称的 slugify 版本。
如果找不到百吉饼,我们返回一个错误。否则,我们呈现匹配的百吉饼的信息。
表单功能
“这一切都很棒,但我如何找到有哪些百吉饼呢?” 好问题!
我们添加了一个 /search
页面,其中包含一个文本输入框,您可以在其中输入内容。按下回车键后,页面上的结果将根据您的输入进行过滤。
这是使用表单实现的。默认情况下,<form>
标签使用 urlencoded 搜索参数作为值。当提交表单时(在我们的例子中,通过按回车键),它会获取 <input>
标签的 name
属性和值,并将其编码为搜索参数。
例如,我们的文本输入框具有属性 name="search"
。如果我们写入 foo
并提交它,它会重定向到当前页面,并在查询字符串中附加 ?search=foo
。我们还将表单方法设置为 GET
,因此它将发出 GET
请求而不是 POST
请求。
然后,我们的路由器只需要解析查询字符串
serve(router(
{
"/search": (req) => {
const search = new URL(req.url).searchParams.get("search");
return ssr(() => <Search search={search ?? ""} />);
},
},
));
然后,我们将 search
值传递给 <Search>
组件
function Search({ search }: { search: string }) {
const foundBagels = bagels.filter((bagel) =>
bagel.name.toLowerCase().includes(search.toLowerCase())
);
return (
<form
method="get"
class="min-h-screen p-4 flex gap-8 flex-col items-center justify-center"
>
<input
type="text"
name="search"
value={search}
class="h-10 w-96 px-4 py-3 bg-gray-100 rounded-md leading-4 placeholder:text-gray-400"
placeholder="Search and press enter..."
/>
<output name="result" for="search" class="w-10/12 lg:w-1/2">
<ul class="space-y-2">
{foundBagels.length > 0 &&
foundBagels.map((bagel) => (
<li class="hover:bg-gray-100 p-1.5 rounded-md">
<a href={`/bagels/${bagel.name.replaceAll(" ", "-")}`}>
<div class="font-semibold">{bagel.name}</div>
<div class="text-sm text-gray-500">{bagel.description}</div>
</a>
</li>
))}
<li>
{foundBagels.length === 0 &&
<div>No results found. Try again.</div>}
</li>
</ul>
</output>
</form>
);
}
我们过滤掉所有不包含 search
值的百吉饼并显示它们。为了获得无缝的用户体验,我们还将 <input>
值设置为 search
。
下一步是什么?
编程意味着管理复杂性。虽然有很多方法可以拼凑一个网站,但代码越简单和明显,编程就越容易、越快——也更有趣。
这里提到的所有内容都可以在这个playground中查看。