当前位置: 首页 > 科技观察

深度解读新一代全栈框架Fresh

时间:2023-03-17 18:20:19 科技观察

大家好,我是三元。今天给大家介绍一个新的框架,Fresh,由Deno的作者出品。它最近发布了1.0正式版,宣布支持生产环境,在Github上也很受欢迎。现在是详细介绍这个解决方案的时候了。向上。接下来我将从框架定位、上手体验、优劣势评估、源码实现等方面为大家深入解读Fresh框架。框架定位首先,从定位来看,Fresh属于Web全栈开发框架。你熟悉这个词吗?相信大家已经想到了,比如现在大名鼎鼎的Next.js和新的Remix都是走这条路的。那么,作为Next.js和Remix的竞争对手,Fresh有哪些值得一提的亮点,或者说有哪些不同之处呢?主要包括以下几个方面:第一,Fresh基于Deno运行时,由原Deno团队开发,享有Deno的一系列工具链和生态优势,如内置测试工具,支持http导入,等等。其次,在渲染性能上,Fresh整体采用了IslandsSSR架构(类似于前面介绍的Astro),实现了客户端按需Hydration,在渲染性能上具有一定的优势。当然还有一个突出的地方就是构建层是Bundle-less,即应用代码可以不打包直接部署上线。这部分的具体实现后面会介绍。最后,与Next.js和Remix不同的是,Fresh的前端渲染层是由Preact完成的,包括Islands架构的实现也是基于Preact,不支持其他前端框架。上手体验在使用Fresh之前,需要在机器上安装Deno:如果没有安装,可以先去Deno官方安装:https://deno.land/。接下来可以输入如下命令初始化项目:denorun-A-rhttps://fresh.deno.devmy-project项目的工程脚本在deno.json文件中:{"tasks":{//-A表示允许Deno读取环境变量"start":"denorun-A--watch=static/,routes/dev.ts"},"importMap":"./import_map.json"}然后就可以执行denotaskstart命令启动项目了:终端显示Fresh从文件目录中扫描了3条路线和1个岛屿组件,我们可以观察到项目的目录结构:.├──README.md├──components│└──Button.tsx├──deno.json├──dev.ts├──fresh.gen.ts├──import_map.json├──islands│└──Counter.tsx├──main.ts├──routes│├──[name].tsx│├──api││└──joke.ts│└──index.tsx├──static│├──favicon.ico│└───logo.svg└──utils└──twind.ts可以关注routes和islands两个目录,[name].tsx,api/joke.ts和index.tsx分别对应三个routeively,而islands目录下的每个文件都对应一个island组件。开发者无需手工编写路由文件,Fresh可以在服务端自动生成路由到文件的映射关系。显然Fresh实现了常规路由的功能,类似于Next.js。每个孤岛组件都需要有一个默认导出,用于暴露组件。使用起来比较简单,这里就不介绍了。路由组件更加灵活,既可以作为API服务使用,也可以作为组件渲染。下面我们来分析一下脚手架项目的几个文件示例。第一个是api/joke.ts文件。该文件的作用是提供服务端的数据接口,不承载任何前端渲染逻辑。你只需要在这个文件中写一个处理函数,如下代码所示://api/joke.tsimport{HandlerContext}from"$fresh/server.ts";constJOKES=[//省略具体内容];exportconsthandler=(_req:Request,_ctx:HandlerContext):Response=>{//随机返回一个笑话字符串returnnewResponse(body);};当访问/api/joke路由时,可以得到handler返回的数据:接下来的两个文件是index.tsx和[name].tsx,第一个文件对应根路由,即/,和访问效果如下:后者为动态路由,可以通过路由参数渲染:exportdefaultfunctionGreet(props:PageProps){return

Hello{props.params.name}
;}接入效果如下:同时也可以在路由组件中同时编写前端组件和handler函数,如下代码所示://修改[name]的内容。tsx如下/**@jsxh*/import{h}from"preact";import{HandlerContext,PageProps}from"$fresh/server.ts";exportfunctionhandler(req:Request,ctx:HandlerContext){consttitle="一些标题数据";returnctx.render({title});}exportdefaultfunctionGreet(props:PageProps){return
Getdata:{props.data.title}
;}从handler的第二个参数(ctx对象)中,我们可以取出render方法,通过输入组件需要的数据,手动调用完成渲染效果如下:以上我们体验了Fresh的几个核心功能,包括项目初始化、路由组件开发、服务端接口开发、组件数据获取和常规路由,相信从中你也能体会到Fresh的简洁与强大。优缺点分析那么,正如Fresh官网所说,Fresh能否成为下一代Web全栈框架呢?下面我们来盘点一下Fresh的优缺点。使用Fresh的优势可以概括为:享受Deno带来的开发优势,从安装依赖、开发、测试、部署直接使用Deno的工具链,降低工程成本;基于Island架构,带来更小的客户端操作时间开销和更好的渲染性能;无需打包即可开发和部署应用程序,从而降低构建成本并减轻重量;而缺点也很明显,包括以下几个方面:只支持Preact框架,不支持React,这是致命的;由于架构原因,开发阶段没有HMR能力,只有页面reload;对于Island组件,它们必须放在islands目录中。对于更复杂的应用,精神负担会很重。不过Astro在这方面更优雅,可以通过组件指令来指定孤岛组件,比如。一方面,Fresh能解决的问题,比如Hydration的性能问题,其他框架(Astro)都可以解决,而且可以比它做得更好。另一方面,Fresh的一些缺点是致命的,Deno现在很难做到。真的很流行,所以我不认为Fresh是未来会广泛流行的Web框架,但是对于Deno和Preact用户来说,我认为Fresh足以撼动像Next.js这样的框架的原有地位。源码实现Fresh的内部实现并不是特别复杂。虽然我们不一定用到Fresh,但是我觉得Fresh的代码还是值得一读的,可以从中学到很多东西。Github地址:https://github.com/denoland/fresh可以先去仓库examples/counter查看示例项目,通过denotaskstart命令启动。入口文件为dev.ts,会调用Fresh收集路由文件和孤岛文件,生成Manifest信息。接下来进入核心环节——创建Server,具体逻辑在server/mod.ts中:;awaitserve(ctx.handler(),opts);}fromManifest是一个工厂方法,目的是根据之前扫描的Manifest信息生成服务器上下文对象(ServerContext),所以Server的核心是ServerContext:classServerContext{staticasyncfromManifest(manifest:Manifest,opts:FreshOptions,){//省略中间处理逻辑returnnewServerContext()}}fromManifest实际上是进一步处理(规范化)manifest信息生成Route对象和Island对象的实例ServerContext已初始化。接下来Fresh会调用ServerContext的handler方法,交给标准库http/server的serve方法调用。因此handler方法也是整个server的核心实现,它有两个主要的实现部分:中间件机制的实现,即洋葱模型的实现,具体逻辑在私有方法#composeMiddlewares中;页面渲染逻辑的实现是在#handlers()中的私有方法中。前者不是本文的重点,有兴趣的同学可以看完文章继续研究。这里主要关注页面渲染的逻辑是如何实现的。#handlers()方法定义了几乎所有路由的处理逻辑,包括路由组件渲染、404组件渲染、Error组件渲染、静态资源加载等逻辑,我们可以关注路由组件渲染,主要是这个逻辑:for(Object.entries(route.handler)){routes[`${method}@${route.pattern}`]=(req,ctx,params)=>handler(req,{...ctx,params,render:createRender(req,params),renderNotFound:createUnknownRender(req,{}),});}whilerouting在objectnormalize(即fromManifest方法)的过程中,默认实现route.handleris:let{handler}=(moduleasRouteModule);handler??={};if(component&&typeofhandler==="object"&&handler.GET===undefined){//划重点!handler.GET=(_req,{render})=>render();}constroute:Route={pattern,url,name,component,handler,csp:Boolean(config?.csp??false),};因此,路由组件的处理最终都会进入render函数,我们来看看render函数是如何创建的://简化代码constgenRender=(route,status)=>{returnasync(req,params,error)=>{returnasync(data)=>{//执行渲染逻辑系列constresp=awaitinternalRender();const[正文]=resp;返回新的响应(正文);}}}constcreateRender=genRender(route,Status.OK);个人认为render函数的生成逻辑比较抽象,需要静下心来理清各个函数的调用顺序。这不难理解。我们还是关注核心的渲染逻辑,主要是internalRender函数的实现:import{renderasinternalRender}from"./render.tsx";您可以转到render.tsx进一步阅读。该文件主要做以下工作:记录项目中声明的所有Islands组件。在Preact中拦截vnode创建逻辑的目的是匹配之前记录的Island组件。如果能匹配到,记录Island组件的props信息,在组件上注解id值是Island的id,number是位置全局道具列表中Island的道具,方便补水时查找对应组件的道具。调用Preact的renderToString方法将组件渲染为HTML字符串。将客户端水合物逻辑注入HTML。拼接完整的HTML返回给前端。值得注意的是客户端hydrate方法的实现,传统的SSR一般直接向根节点调用hydrate,但是在Islands架构中,Fresh对每个Island都是独立渲染的,实现如下:hydrate方法name也可以叫reviveexportfunctionrevive(islands:Record,props:any[]){functionwalk(node:Node|null){//1.获取评论节点信息,解析出Islandidconsttag=node!.nodeType===8&&((nodeasComment).data.match(/^\s*frsh-(.*)\s*$/)||[])[1];让endNode:节点|空=空;if(tag){conststartNode=node!;const孩子=[];constparent=node!.parentNode;//获取当前Island节点的所有子节点while((node=node!.nextSibling)&&node.nodeType!==8){children.push(node);}startNode.parentNode!.removeChild(startNode);//删除起始标记节点const[id,n]=tag.split(":");//2.单独渲染Island组件render(h(islands[id],props[Number(n)]),htmlElement);结束节点=节点;}//3.继续遍历DOM树,直到找到所有Island节点constsib=node!.nextSibling;constfc=node!.firstChild;如果(endNode){endNode.parentNode?.removeChild(endNode);//删除结束标记节点}if(sib)walk(sib);如果(fc)步行(fc);}walk(document.body);}至此,服务器端和客户端的渲染过程就完成了。回顾整个过程,为什么FreshBundle-less的构建过程?我们不妨关注一下Islands组件是如何加载到客户端的。首先,服务端可以通过拦截vnode的实现来感知项目中使用了哪些Island组件,比如Counter组件。然后服务端会注入相应的import代码挂在Globally上,通过注入HTML。浏览器执行这些代码时,会向服务器发起对/islands/Counter的请求。服务端收到请求后会实时编译打包Counter组件,然后将结果返回给浏览器,以便浏览器获取Esbuild.编译产品并执行它。所以这个过程完全发生在运行时,也就是说我们不需要在启动项目的时候就把所有的组件打包,而是在运行时按需构建,受益于Esbuild极快的构建速度一般可以达到构建毫秒级的速度,对服务的运行压力不大。总结以上就是本文的全部内容。从框架定位、上手体验、优缺点评测、源码实现等方面介绍了时下流行的Fresh框架。最后需要跟大家说明一下,Fresh中Islands架构的实现是基于Preact的。我也借鉴了Fresh的思路,通过拦截React.createElement方法在React中实现了Islands架构。代码放在react-islands仓库中(地址:https://github.com/sanyuan0704/react-islands),代码不多,相当于简化版的Fresh。有兴趣的朋友可以拉下来看看~