首页关于友链

基于 Deno 的全栈框架 Slow 简介

swwind

注意:距离本文最后一次更新已经超过 1 年,世界线的变动可能会导致故事走向不同的结局


Slow 是一款全新的全栈框架,主要基于 Deno 运行时。

项目结构说明

/app 目录

文件/目录说明
/app/root.tsx整个前端的根结构
/app/routes/存放路由文件

其他文件都不管,只要你 import 了就会打包进去。

路由

每个目录可以匹配一层请求的路径,可以使用 () 小括号来忽略该层路径的匹配,也可以使用 [] 方括号来匹配动态的路由参数。

如果需要匹配全部子路径,那么可以使用 [...] 目录。

文件匹配的路径路由参数
/index.tsx/{}
/foo/index.tsx/foo/{}
/(foo)/bar/index.tsx/bar/{}
/foo/[bar]/index.tsx/foo/anything/{"bar":"anything"}
/foo/[...]/index.tsx/foo/bar/baz/{"$":["bar","baz"]}

上面的表格中使用 index.tsx 用来表示对应页面的前端代码文件,你可以使用其他后缀名,但是开头必须是 index。

下面是一个路由目录下支持的文件和使用例。

index.tsx

用于存放前端代码,只需要使用 export default 即可。

例如对于 React 框架,你可以直接如下书写。

// index.tsx
export default function Page() {
  return <h1>Page</h1>;
}

loader.xxx.ts

用于存放获取数据方面的代码,可以提供给前端进行渲染。

名称前面的 xxx 可以自由定义,也可以没有,后缀也可以自选。

一个文件中只能使用一个 default export,你需要使用帮助函数 defineLoader 嵌套你的整个 loader 函数来获取前后端分离的支持。

// loader.something.ts
import { defineLoader } from "stale";

export default defineLoader(async (req: Request) => {
  // ask database...
  return { anything: ["JSON serializable"] };
});
// index.tsx
import useSomething from "./loader.something.ts";

export default function Render() {
  const something = useSomething();
  return <h1>{something.anything[0]}</h1>;
}

action.xxx.ts

用于存放一般的 POST 请求接口,可以提供给前端作为响应。

用法和上面的 loader 一样。

// action.create.ts
import { defineAction } from "stale";

export default defineAction(async (req: Request) => {
  const formData = await req.formData();
  const name = formData.get("name");
  const password = formData.get("password");
  // do something...
  return { ok: true };
});

使用的时候可能需要使用表单提交。

// index.tsx
import { Form } from "stale";
import useCreateUser from "./action.create.ts";

export default function Render() {
  const createUser = useCreateUser();

  return (
    <Form action={createUser}>
      <input type="text" name="name" />
      <input type="password" name="password" />
      <button>submit</button>
    </Form>
  );
}

layout.xxx.tsx

layout 文件用于表示嵌套的 UI 模型。

例如,如果你想要加载 /app/routes/foo/index.tsx 页面,那么整个页面的嵌套顺序可能是这样的:

  • /app/root.tsx
  • /app/routes/layout.tsx
  • /app/routes/foo/layout.tsx
  • /app/routes/foo/index.tsx

如果你要对某个单独的页面使用特殊的 layout 模板,那么你可以将后缀改成 .xxx.tsx 来确定。

例如,如果你想要加载 /foo/index.custom.tsx,那么整个页面的嵌套顺序可能是这样的:

  • /app/root.tsx
  • /app/routes/layout.custom.tsx
  • /app/routes/foo/layout.custom.tsx
  • /app/routes/foo/index.custom.tsx

注意如果以上列举的任何一个 layout 文件不存在,则不会嵌套在页面中。即对于指定 namespace 的 index 文件,不会加载任何默认 namespace 的 layout 文件,除非你显式创建了该 namespace 的 layout 文件并且 export 了默认的 layout 文件。

// layout.tsx
export default function (props) {
  return <div>{props.children}</div>;
}

Layout 中可以正常使用 loader 和 action 的功能。

数据获取

上面已经简单介绍了 loader 和 action 的功能,下面是一些更加详细的例子,介绍如何进行错误处理。

// /[name]/loader.ts
export default defineLoader(async (req, params) => {
  const animeName = params.name;

  const anime = await kv.get(["anime", animeName]);
  // 用 throw 来抛出异常,默认会返回 500 请求
  if (!anime.value) {
    throw new Error("Not found!");
  }
  // 用 throw URL 来重定向网页
  if (!anime.value) {
    throw new URL("/path", req.url);
  }
  // 用 throw Response 来抛出可处理的异常
  if (!anime.value) {
    throw new Response("Not found!", { status: 404 });
  }

  return { anime: ["oshi no ko"] };
});

在其他页面里面也可以引用这个 loader 的数据,但是必须是该目录下面的子页面。

// /[name]/very/deep/nested/index.ts
import useLoaderData from "../../../loader.ts";

export default function () {
  const loaderData = useLoaderData();
  return <div>{loaderData.anime[0]}</div>;
}

其他事情

没有!