Fresh 快速入门
2022-09-12 · 3,167 chars · 16 min read
前段时间因为 Islands Architecture 的原因,了解了一下 Fresh,整体感觉还是不错的,设计思路很棒,简洁而高效。虽然还有部分功能缺失,目前也很难在企业级应用开发中使用,但是了解其基本功能和架构设计,对我们开阔思路、突破桎梏还是大有裨益的。所以,这里梳理一份快速入门的文档,分享给大家,希望多少能有些帮助。
起步#
首先我们把环境准备好,项目跑起来。本文基于以下版本:
- Deno:1.25.2
- Fresh:1.1.0
安装 Deno#
第一步是安装 Deno,在 macOS 和 Linux 上:
curl -fsSL https://deno.land/x/install/install.sh | sh
在 windows 上我建议是使用 wsl,之后全部按照 Linux 操作即可。如果想直接安装,可以参考官方文档,里面有更多的安装方式和升级指南等。
目前 Deno 和 Fresh 还处在密集开发阶段,我在自己玩的过程中,就遇到过版本问题而需要升级的情况。
创建并启动项目#
接下来创建项目,Fresh 的官方文档是这么教大家建项目的:
deno run -A -r https://fresh.deno.dev my-project cd my-project deno task start
我不建议用这个脚手架,因为项目过于简单了,也不方便调试和学习。我们比较建议直接 clone 官方仓库,既包含 Fresh 源码,也有官方的文档站点:
git clone https://github.com/denoland/fresh.git --branch main --depth 10
然后执行 deno task www,就在本地将官网运行起来了。
目录结构#
因为我们是直接 clone Fresh 源码的。要看 Fresh 项目的目录结构,只看 www 目录即可:
. ├── README.md ├── components/ ├── data/ ├── deno.json ├── dev.ts ├── fresh.gen.ts ├── import_map.json ├── islands/ ├── main.ts ├── routes/ ├── static/ ├── twind.config.ts └── utils/
这里面有几个主要的文件(夹)需要关注一下:
dev.ts:开发入口。本地开发就是启动这个文件。main.ts:线上环境运行的入口。fresh.gen.ts:清单文件,包含 routes 和 islands,Fresh 自动生成的。routes/:所有 route 都要在这个目录下,这是约定,后面细说。islands/:所有 island 都要在这个目录下,这是约定,后面细说。static/:存放静态文件,例如 favicon.ico 等。
Routes#
路由(Route)在 Fresh 里是一个很重要的概念,渲染页面、API 响应、数据请求等都是路由的延伸。
匹配规则#
所有的路由必须放在 routes 目录下,类似 Next.js 的 File-system routing,或者叫做 “约定式路由” 也行。看几个例子就一目了然了:
| 路由文件地址 | 对应的 pathname |
|---|---|
routes/index.tsx | / |
routes/about.tsx | /about |
routes/about/contact.tsx | /about/contact |
Component route#
每个路由文件都可以 export default 一个组件,用来渲染相应的页面:
// routes/index.tsx
export default function Home() {
return <div>Hello Fresh!</div>
}
访问 / 就会看到:
<div>Hello Fresh!</div>
Handler route#
默认导出的组件会响应为 HTML,除此之外,还可以导出一个 handler 函数,用来自定义响应,可以像接口一样返回 json,也可以返回 HTML。
handler 函数接受 Request 对象,返回 Response 或者 Promise<Response>:
// routes/api/random-uuid.ts
import { Handlers } from '$fresh/server.ts'
export const handler: Handlers = (req) => {
const uuid = crypto.randomUUID()
return new Response(JSON.stringify(uuid), {
headers: { 'Content-Type': 'application/json' },
})
}
上面的写法会处理所有的 HTTP methods,还可以指定只处理 GET 或者 POST,此时 handler 是个对象,相应的 HTTP method 是 key:
// routes/api/random-uuid.ts
import { Handlers } from '$fresh/server.ts'
export const handler: Handlers = {
GET(req) {
const uuid = crypto.randomUUID()
return new Response(JSON.stringify(uuid), {
headers: { 'Content-Type': 'application/json' },
})
},
}
Mixed handler and component route#
不知道大家注意到没,component route 使用的默认导出 export default,而 handler route 使用的是具名导出 export const handler,所以在一个路由中可以同时混合使用两种。
例如在响应 HTML 的同时,又想添加一个自定义 Headers:
// routes/about.tsx
import { Handlers } from '$fresh/server.ts'
export const handler: Handlers = {
async GET(req, ctx) {
const resp = await ctx.render()
resp.headers.set('X-Custom-Header', 'Hello')
return resp
},
}
export default function AboutPage() {
return (
<main>
<h1>About</h1>
<p>This is the about page.</p>
</main>
)
}
混合使用时,需要在 handler 里面手动调用 ctx.render(),和 koa,express 之类的框架挺像的。
聪明的你可能已经想到了,没错,所谓的 component route 其实有个默认的 handler,就是单纯的 render 组件。
动态路由#
除了最开始提到的几个 “静态” 路由例子外,还支持动态的路由。例如 routes/greet/[name].tsx 文件,可以匹配 /greet/Luca 或者 /greet/John。
:name 参数可以通过组件的 props 获取:
// routes/greet/[name].tsx
import { PageProps } from '$fresh/server.ts'
export default function GreetPage(props: PageProps) {
const { name } = props.params
return (
<main>
<p>Greetings to you, {name}!</p>
</main>
)
}
Fetching data#
Fresh 强调的是 “Just-in-time rendering” 和 “No build”,需要自身的简洁、高性能、边缘网络的加持,否则不会比 SSG 响应的更快。
基于 Fresh 这些特性,数据请求就很简单了,CSR 的 Island 直接在客户端 fetch data 即可。SSR 可以直接使用 Deno 请求,在渲染的时候通过 props 传入组件:
// routes/github/[username].tsx
import { Handlers, PageProps } from '$fresh/server.ts'
interface User {
login: string
name: string
avatar_url: string
}
export const handler: Handlers<User | null> = {
async GET(_, ctx) {
const { username } = ctx.params
const resp = await fetch(`https://api.github.com/users/${username}`)
if (resp.status === 404) {
return ctx.render(null)
}
const user: User = await resp.json()
return ctx.render(user)
},
}
export default function Page({ data }: PageProps<User | null>) {
if (!data) {
return <h1>User not found</h1>
}
return (
<div>
<img src={data.avatar_url} width={64} height={64} />
<h1>{data.name}</h1>
<p>{data.login}</p>
</div>
)
}
传入 ctx.render 的数据可以通过 props.data 在组件内获取。
Islands#
重头戏来了,可以这么说,我看 Fresh 就是为了 Islands 😁😁😁。关于 Islands Architecture 是什么,之前的文章说的很清楚了,不了解的朋友可以去看看。
Fresh 约定所有 Islands 组件放在 islands/ 目录下。
这些组件都是普通的 Preact 组件,与 Routes 组件的最大不同是,Route Components 只在服务端运行,在客户端看到的都是渲染好的纯 HTML;而 Islands 组件还会在客户端加载 JS 代码并执行,所以可以支持用户交互。如果需要响应用户交互,那这部分的组件必须放在 islands/ 目录下。
举个例子,Counter 计数器组件,+1, -1 两个小功能:
// islands/Counter.tsx
import { useState } from 'preact/hooks'
import { IS_BROWSER } from '$fresh/runtime.ts'
interface CounterProps {
start: number
}
export default function Counter(props: CounterProps) {
const [count, setCount] = useState(props.start)
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count - 1)} disabled={!IS_BROWSER}>
-1
</button>
<button onClick={() => setCount(count + 1)} disabled={!IS_BROWSER}>
+1
</button>
</div>
)
}
然后在 route 里引用 island:
// routes/index.ts
import Counter from '../islands/Counter.tsx'
export default function Home() {
return (
<div>
<p>Welcome to Fresh.</p>
<Counter start={3} />
</div>
)
}
这样 Counter 组件就可以在客户端响应用户交互了,在客户端也仅需要加载 Counter 相关的 JS 代码。
中间件#
中间件总结起来就主要是三点:
- 命名为
_middleware.ts,放在routes/目录下。一个就是 handler 函数,多个就是 handler 数组。 - 通过
ctx.next()来向下执行。 - 通过
ctx.state来传递数据(向下或者向上)。
中间件的代码大概长这样:
// routes/_middleware.ts
import { MiddlewareHandlerContext } from '$fresh/server.ts'
interface State {
data: string
}
export async function handler(req: Request, ctx: MiddlewareHandlerContext<State>) {
ctx.state.data = 'myData'
const resp = await ctx.next()
resp.headers.set('server', 'fresh server')
return resp
}
在路由 handler 中读取中间件写入的数据:
// routes/myHandler.ts
export const handler: Handlers<any, { data: string }> = {
GET(_req, ctx) {
return new Response(`middleware data is ${ctx.state.data}`)
},
}
执行流程类似 koa 的洋葱模型。
举个例子,假设有这样的目录结构:
routes/_middleware.tsroutes/index.tsroutes/admin/_middleware.tsroutes/admin/index.tsroutes/admin/signin.ts
请求 /:
- 执行
routes/_middleware.ts - 调用
ctx.next()后执行routes/index.ts
请求 /admin
- 执行
routes/_middleware.ts - 调用
ctx.next()后执行routes/admin/_middleware.ts - 调用
ctx.next()后执行routes/admin/index.ts
请求 /admin/signin
- 执行
routes/_middleware.ts - 调用
ctx.next()后执行routes/admin/_middleware.ts - 调用
ctx.next()后执行routes/admin/signin.ts
异常页面#
404: Not Found#
约定 routes/_404.tsx 这个路由,用来渲染 404 页面。
// routes/_404.tsx
import { UnknownPageProps } from '$fresh/server.ts'
export default function NotFoundPage({ url }: UnknownPageProps) {
return <p>404 not found: {url.pathname}</p>
}
如果在别的路由中,例如 routes/greet/[name].tsx 里,遇到了不存在的 :name,想要渲染 404 页面,可以在路由 handler 里调用 ctx.renderNotFound 来展示 404 页面。
500: Internal Server Error#
500 错误也类似,如果发生异常,会使用 routes/_500.tsx 渲染页面:
import { ErrorPageProps } from '$fresh/server.ts'
export default function Error500Page({ error }: ErrorPageProps) {
return <p>500 internal error: {(error as Error).message}</p>
}
其他#
还有一些东西没讲,例如表单提交,这种需求我就根本就没遇到过,万一遇到就临时抱佛脚好了。
再比如静态文件的处理,Fresh 约定是放在 /static 目录,但是默认没有缓存头。Fresh 提供的 asset 函数我没仔细研究,感觉上很麻烦,如果真的在生产环境使用的话,还是动静分离,单独处理静态文件好了。
Fresh 从 1.1 版本开始支持 plugin,有兴趣的朋友可以自己去看看。
开发和调试#
Hot Reload#
目前 Fresh 的热更新还很初级,watch 文件变动后,直接刷新页面:
// /_frsh/refresh.js
let reloading = false
const buildId = 'a4d4936c-97a8-48ee-a8db-3398236fa519'
new EventSource('/_frsh/alive').addEventListener('message', (e) => {
if (e.data !== buildId && !reloading) {
reloading = true
location.reload()
}
})
Debug#
Debug 是我很关注的部分,因为几乎没什么是比调试一遍 Demo 更好的学习方式了。但是在实际操作的时候却非常不顺利。
一开始尝试的是 VSCode,官方文档还是空的,安装 Deno 插件后自己折腾一下也能跑起来,配置大概是这样的:
// .vscode/launch.json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"request": "launch",
"name": "Launch Program",
"type": "node",
"program": "${workspaceFolder}/www/dev.ts",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "/Users/keenwon/.deno/bin/deno",
"runtimeArgs": [
"run",
"--import-map",
"./.vscode/import_map.json",
"--inspect",
"--allow-all"
],
"attachSimplePort": 9229
}
]
}
启动调试后,routes/ 下的 componet 和 handler 都是可以断点到的。调试基本的业务逻辑没大问题,但想 debug 整个启动过程就不行了,断点毫无反应。
尝试使用 --inspect-brk 和 chrome devtools,debugger 自动在第一行代码出暂停了,但是却看不到源码,没法继续调试。Github 上面看到了相关的 issue,提出来挺久了,等了几天没啥进度,目前 Deno 的代办 issues 还是还多的,目测短时间内解决不了。
不过调试无非两种,安装官方的 IDE 插件,在 IDE 里面调试,或者支持 V8 Inspector Protocol 的,手动链接到 Chrome 调试。大家使用的时候可以再试试,可能 bug 已经修复,文档也更完善了。
部署#
部署这部分就不多说了,使用 Deno Deploy 是不可能的 😅。官方还给了使用 docker 方案的示例:
FROM denoland/deno:1.25.0
ARG GIT_REVISION
ENV DENO_DEPLOYMENT_ID=${GIT_REVISION}
WORKDIR /app
COPY . .
RUN deno cache main.ts --import-map=import_map.json
EXPOSE 8000
CMD ["run", "-A", "main.ts"]
其实就是启动入口文件 main.ts。
话说回来,没有边缘计算网络的加持,这种模式在运行机制上和普通 SSR 差别不大。
总结#
Fresh 除了官方宣传的几个特点之外,还有几处我觉得挺有趣的地方。
首先肯定就是 Islands Architecture,单看这一点,我认为是当之无愧的 “The next-gen web framework”。它几乎占了全部的优点,客户端只需加载少量的 JS 代码、纯静态组件(页面)无需 hydrate、良好的性能 、支持 SEO、基于组件的现代化开发模式...。不过,不支持 SSG 这点确实遗憾,现阶段 CDN + SSG 肯定要比 Edge Runtime 更容易应用。即使你的前端基础设施拥有可靠好用的边缘计算能力,但再快的计算也不如不计算,SSG “永不过时”。
另外,Fresh 深度集成了 Preact,无需构建流程,可以直接在服务端渲染 Preact 组件。相比 React,Preact 在大多数情况下确实好用也够用。不像传统 React SSR 要构建两份代码分别用于 server 和 client,Fresh 拥有框架级的支持,虽然没有在生产环境大规模验证过,但这个思路却是非常赞的!
handler route 和 component route 非常像是把 koa、express 简化处理,同时用 JSX 替换了传统的模板引擎,让我们可以像写前端代码一样写后端 deno 应用。
当然了,就像开头说的,Fresh 肯定还是不成熟的,静态文件处理、Debug、Hot Reload、第三方生态等方面都还很单薄,但这并不影响我们学习了解它的设计思路和架构。
最后,Fresh 其实也我也没有太多实战经验,如有不对的地方,欢迎大家留言指正。



