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.ts
routes/index.ts
routes/admin/_middleware.ts
routes/admin/index.ts
routes/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 其实也我也没有太多实战经验,如有不对的地方,欢迎大家留言指正。