近期,由 React Router 原班团队打造,基于 TypeScript 与 React,内建 React Router V6 特性的全栈 Web 框架 Remix 正式开源。目前占据 Github 趋势总榜前 3.

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第1张

大家好,我是皮汤。周五翻 Github 趋势榜看到了 Remix 这个内容,觉得挺有发展前景的,初步了解了一下具体的特性,分享给大家。

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第2张

近期,由 React Router 原班团队打造,基于 TypeScript 与 React,内建 React Router V6 特性的全栈 Web 框架 Remix 正式开源。目前占据 Github 趋势总榜前 3,Github 标星 5K+ Star:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第3张

Remix 开源之后可以说是在 React 全栈框架领域激起千层浪,绝对可以算是 Next.js 的强劲对手。Remix 的特性如下:

  • 追求速度,然后是用户体验(UX),支持任何 SSR/SSG 等
  • 基于 Web 基础技术,如 HTML/CSS 与 HTTP 以及 Web Fecth API,在绝大部分情况可以不依赖于 JavaScript 运行,所以可以运行在任何环境下,如 Web Browser、Cloudflare Workers、Serverless 或者 Node.js 等
  • 客户端与服务端一致的开发体验,客户端代码与服务端代码写在一个文件里,无缝进行数据交互,同时基于 TypeScript,类型定义可以跨客户端与服务端共用
  • 内建文件即路由、动态路由、嵌套路由、资源路由等
  • 干掉 Loading、骨架屏等任何加载状态,页面中所有资源都可以预加载(Prefetch),页面几乎可以立即加载
  • 告别以往瀑布式(Waterfall)的数据获取方式,数据获取在服务端并行(Parallel)获取,生成完整 HTML 文档,类似 React 的并发特性
  • 提供开发网页需要所有状态,开箱即用;提供所有需要使用的组件,包括 <Links> 、<Link>、 <Meta> 、<Form> 、<Script/> ,用于处理元信息、脚本、CSS、路由和表单相关的内容
  • 内建错误处理,针对非预期错误处理的 <ErrorBoundary> 和开发者抛出错误处理的 <CatchBoundary>

特性这么多?不明觉厉!接下来我们就尝试一一来展示这些 Remix 的特性?。

一致的开发体验

Remix 提供基于文件的路由,将读取数据、操作数据和渲染数据的逻辑都写在同一个路由文件里,方便一致性处理,这样可以跨客户端和服务端逻辑共享同一套类型定义。

看一段官网的代码:

  1. importtype{Post}from"~/post";
  2. import{Outlet,Link,useLoaderData,useTransition}from"remix";
  3. letpostsPath=path.join(__dirname,"..","posts");
  4. asyncfunctiongetPosts(){
  5. letdir=awaitfs.readdir(postsPath);
  6. returnPromise.all(
  7. dir.map(async(filename)=>{
  8. letfile=awaitfs.readFile(path.join(postsPath,filename));
  9. let{attributes}=parseFrontMatter(file.toString());
  10. invariant(
  11. isValidPostAttributes(attributes),
  12. `${filename}hasbadmetadata!`
  13. );
  14. return{
  15. slug:filename.replace(/.md$/,""),
  16. title:attributes.title,
  17. };
  18. })
  19. );
  20. }
  21. asyncfunctioncreatePost(post:Post){
  22. letmd=`---\ntitle:${post.title}\n---\n\n${post.markdown}`;
  23. awaitfs.writeFile(path.join(postsPath,post.slug+".md"),md);
  24. returngetPost(post.slug);
  25. }
  26. exportasyncfunctionloader({request}){
  27. returngetProjects();
  28. }
  29. exportasyncfunctionaction({request}){
  30. letform=awaitrequest.formData();
  31. constpost=createPost({title:form.get("title")});
  32. returnredirect(`/posts/${post.id}`);
  33. }
  34. exportdefaultfunctionProjects(){
  35. letposts=useLoaderData<Post[]>();
  36. let{state}=useTransition();
  37. letbusy=state==="submitting";
  38. return(
  39. <div>
  40. {posts.map((post)=>(
  41. <Linkto={post.slug}>{post.title}</Link>
  42. ))}
  43. <Formmethod="post">
  44. <inputname="title"/>
  45. <buttontype="submit"disabled={busy}>
  46. {busy?"Creating...":"CreateNewPost"}
  47. </button>
  48. </Form>
  49. <Outlet/>
  50. </div>
  51. );
  52. }

上述是一个路由文件,如果它是 src/routes/posts/index.tsx 文件,那么我们开启服务器,通过 localhost:3000/posts 就可以访问到这个文件,这就是文件即路由,而默认导出的 Projects 函数,即为一个 React 函数式组件,此函数的返回模板则为访问这个路由的 HTML 文档。

  • 每个路由函数,如 Projects 可以定义一个 loader 函数,类似处理 GET 请求的服务端函数,可以获取到路由信息,为初次服务端渲提供数据,在这个函数中可以获取文件系统、请求数据库、进行其他网络请求,然后返回数据,在我们的 Projects 组件里,可以通过 Remix 提供的 useLoaderData 钩子拿到 loader 函数获取到的数据。
  • 每个路由函数也可以定义一个 action 函数,用于进行实际的操作,类似处理非 GET 请求,如 POST/PUT/PATCH/DELETE 的操作的函数,它可以操作修改数据库、写入文件系统等,同时其返回的结果可能是实际的数据或是重定向到某个新页面,如 redirect("/admin")。当 action 函数返回数据或错误信息时,我们可以通过 Remix 提供的 useActionData 钩子拿到这个返回的错误信息,进行前端的展示等。

值得注意的是,action 函数是在 <Form method="post"> 表单里,用户点击提交按钮之后自动调用,Remix 通过 Fetch API 的形式去调用,然后在前端不断的轮询获取调用结果,且自动处理用户多次点击时的竞争情况。

你的浏览器网络面板将呈现如下情况,自动 Remix 发起 POST 请求,然后处理重定向到 /post/${post.id} ,同时加载对应的 /posts 和 /posts/${post.id} 对应的路由页面内容。

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第4张

通过 Remix 提供的 useTransition 钩子,我们可以拿到表单提交的状态,当请求还未返回结果时,我们可以通过这个状态 state 判断是否要展示一个加载状态,提示用户当前的请求进展。

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第5张

同时 Post 类型在 useLoaderData

有同学可能注意到了,上面我们整个页面渲染、到发起创建 Post 请求、到后台创建 Post,到重定向到 Post 详情,这整个过程,我们无需在前端使用任何 JavaScript 相关的内容,仅仅通过 HTML 与 HTTP 就完成了这个交互,所以 Remix 的网站在 Disbaled JavaScript 运行环境下也可以正常工作。

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第6张

通过上图我们可以看到,即使 JavaScript 已经关闭了,我们的网站依然可以正常运行。

强大的嵌套路由体系

基于文件即路由的理念,我们无需集中的维护一套路由定义,当我们创建了对应的文件之后,Remix 就为我们注册了对应的路由。

而 Remix 最具特色的功能之一就是嵌套路由。在 Remix 中,一个页面通常包含多层级页面,每个子页面控制自身的 UI 展现,而且独立控制自身的数据加载和代码分割。

拿官网的例子来看如下:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第7张

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第8张

上述页面的对应关系如下:

  • 整个页面模块为 / 、而对应到 /sales 则是右边的整块天蓝色内容、/sales/invoices 对应到黄色的部分、/sales/invoices/102000 则对应到右下角的红色部分

整个路由分层,对应到整个页面的分层视图,而每个分层下的代码都是独立编写,视图渲染独立渲染,数据独立获取,错误独立展示。

来看一个实际例子:

  1. //src/root.tsx
  2. import{
  3. Outlet,
  4. exportdefaultfunctionApp(){
  5. return(
  6. <Document>
  7. <Layout>
  8. <Outlet/>
  9. </Layout>
  10. </Document>
  11. );
  12. }
  13. functionDocument(){}
  14. functionLayout(){}
  1. //src/routes/admin.tsx
  2. import{Outlet,Link,useLoaderData}from"remix";
  3. import{getPosts}from"~/post";
  4. importtype{Post}from"~/post";
  5. importadminStylesfrom"~/styles/admin.css";
  6. exportletlinks=()=>{
  7. return[{rel:"stylesheet",href:adminStyles}];
  8. };
  9. exportletloader=()=>{
  10. returngetPosts();
  11. };
  12. exportdefaultfunctionAdmin(){
  13. letposts=useLoaderData<Post[]>();
  14. return(
  15. <divclassName="admin">
  16. <nav>
  17. <h1>Admin</h1>
  18. <ul>
  19. {posts.map((post)=>(
  20. <likey={post.slug}>
  21. <Linkto={post.slug}>{post.title}</Link>
  22. </li>
  23. ))}
  24. </ul>
  25. </nav>
  26. <main>
  27. <Outlet/>
  28. </main>
  29. </div>
  30. );
  31. }
  1. //src/routes/admin/index.tsx
  2. import{Link}from"remix";
  3. exportdefaultfunctionAdminIndex(){
  4. return(
  5. <p>
  6. <Linkto="new">CreateaNewPost</Link>
  7. </p>
  8. );
  9. }
  1. //src/routes/admin/new.tsx
  2. import{useTransition,useActionData,redirect,Form}from"remix";
  3. importtype{ActionFunction}from"remix";
  4. import{createPost}from"~/post";
  5. importinvariantfrom"tiny-invariant";
  6. exportletaction:ActionFunction=async({request})=>{
  7. awaitnewPromise((res)=>setTimeout(res,1000));
  8. letformData=awaitrequest.formData();
  9. lettitle=formData.get("title");
  10. letslug=formData.get("slug");
  11. letmarkdown=formData.get("markdown");
  12. leterrors={};
  13. if(!title)errors.title=true;
  14. if(!slug)errors.slug=true;
  15. if(!markdown)errors.markdown=true;
  16. if(Object.keys(errors).length){
  17. returnerrors;
  18. }
  19. awaitcreatePost({title,slug,markdown});
  20. returnredirect("/admin");
  21. };
  22. exportdefaultfunctionNewPost(){
  23. leterrors=useActionData();
  24. lettransition=useTransition();
  25. return(
  26. <Formmethod="post">
  27. <p>
  28. <label>
  29. PostTitle:{errors?.title&&<em>Titleisrequired</em>}
  30. <inputtype="text"name="title"/>
  31. </label>
  32. </p>
  33. <p>
  34. <label>
  35. PostSlug:{errors?.slug&&<em>Slugisrequired</em>}{""}
  36. <inputtype="text"name="slug"/>
  37. </label>
  38. </p>
  39. <p>
  40. <labelhtmlFor="markdown">Markdown:</label>{""}
  41. {errors?.markdown&&<em>Markdownisrequired</em>}
  42. <br/>
  43. <textarearows={20}name="markdown"/>
  44. </p>
  45. <p>
  46. <buttontype="submit">
  47. {transition.submission?"Create...":"CreatePost"}
  48. </button>
  49. </p>
  50. </Form>
  51. );
  52. }

上述代码渲染的页面如下:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第9张

整个 App 网站是由 <Document> 嵌套 <Layout> 组成,其中 <Outlet> 是路由的填充处,即上图中绿色的部分。当我们访问 localhost:3000/ 时,其中填充的内容为 src/routes/index.tsx 路由文件对应的渲染内容,而当我们访问 localhost:3000/admin 时,对应的是 src/routes/admin.tsx 路由文件对应的渲染内容。

而我们在 的 src/routes/admin.tsx 继续提供了 <Outlet> 路由显然组件,意味着当我们继续添加分级(嵌套)路由时,如访问 http://localhost:3000/admin/new 那么这个 <Outlet> 会渲染 src/routes/admin/new.tsx 对应路由文件的渲染内容,而访问 http://localhost:3000/admin 时,<Outlet> 部分会渲染 src/routes/admin/index.tsx 对应路由文件的渲染内容,见下图:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第10张

而这种嵌套路由是自动发生的,当你创建了一个 src/routes/admin.tsx 之后,又创建了一个同名的文件夹,并在文件夹下建立了其它文件,那么这些文件的文件名会被注册为下一级的嵌套路由名:

  • localhost:3000/admin 同时注册 src/routes/admin.tsx 和 src/routes/admin/index.tsx
  • localhost:3000/admin/new 注册 src/routes/admin/new.tsx

通过这种文件即路由,同名文件夹下文件即嵌套路由的方式,然后通过在父页面里面通过 的方式渲染根据子路由渲染子页面内容,极大的增加了灵活性,且每个子路由对应独立的路由文件,具有独立的数据处理逻辑、内容渲染逻辑、错误处理逻辑。

上述嵌套路由一个显而易见的优点就是,某个部分如果报错了,结合后续会提到的 ErrorBoundary 和 CatchBoundary 这个部分可以显示错误的页面,而用户仍然可以操作其他部分,而不需要刷新整个页面以重新加载使用,极大提高网站容错性。

再见,加载状态

通过嵌套路由,Remix 可以干掉几乎所有的加载状态、骨架屏,现在很多应用都是在前端组件里进行数据获取,获取前置数据之后,然后用前置数据去获取后置的数据,形成了一个瀑布式的获取形式,当数据量大的时候,页面加载就需要很长时间,所以绝大部分网站都会放一个加载的状态,如小菊花转圈圈,或者体验更好一点的骨架屏,如下:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第11张

这是因为这些应用缺乏类似 Remix 这样的嵌套路由的概念,访问某个路由时,就是访问这个路由对应的页面,只有这个页面加载出来之后,里面的子组件渲染时,再进行数据的获取,再加载子组件,如此往复,就呈现瀑布流式的加载,带来了很多中间的加载状态。

而 Remix 提供了嵌套路由,当访问路由 localhost:3000/admin/new 时,会加载三级路由,同时这三个路由对应的页面独立、并行加载,独立、并行获取数据,最后发送给客户端的是一个完整的 HTML 文档,如下过程:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第12张

可见虽然我们首屏拿到内容可能会慢一点,但是再也不需要加载状态,再见,菊花图 ??,再见,骨架屏??。

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第13张

同时借助嵌套路由,当我们鼠标 Hover 到某个链接准备点击切换某个子路由时,Remix 提供了预获取(Prefetch)功能,可以提前并行获取子路由文档和各种资源,包括 CSS、图片、相关数据等,这样当我们实际点击这个链接切换子路由时,页面可以立即呈现出来:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第14张

完善的错误处理

我们的网站经常会遇到问题,使用其他框架编写时,网站遇到问题可能用户就需要重新刷新网站,而对于 Remix 来说,基于嵌套路由的理念,则无需重新刷新,只需要在对应的错误的子路由展示错误信息,而页面的其他部分仍然可以正常工作:

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第15张

比如我们上图的右下角子路由出现了问题,那么这块会展示出问题时的错误页面,而其他页面部分仍然展示正常的信息。

正因为错误经常发生,且处理错误异常困难,包含客户端、服务端的各种错误,包含预期的、非预期的错误等,所以 Remix 内建了完善的错误处理机制,提供了类似 React 的 ErrorBoundary 的理念。

在 Remix 中,每个路由函数对应一个 ErrorBoundary 函数:

  1. exportdefaultfunctionRouteFunction(){}
  2. exportfunctionErrorBoundary({error}){
  3. console.error(error);
  4. return(
  5. <div>
  6. <h2>Ohsnap!</h2>
  7. <p>
  8. Therewasaproblemloadingthisinvoice
  9. </p>
  10. </div>
  11. );
  12. }

ErrorBoundary 函数代表处理那些来自 loader 和 action,客户端或服务端的非预期的错误,当出现这些非预期的错误时,就会激活这个函数,显示对应函数的表示错误信息的 UI。

同时每个路由函数对应着一个 CatchBoundary 函数:

  1. import{useCatch}from"remix";
  2. exportfunctionCatchBoundary(){
  3. letcaught=useCatch();
  4. return(
  5. <div>
  6. <h1>Caught</h1>
  7. <p>Status:{caught.status}</p>
  8. <pre>
  9. <code>{JSON.stringify(caught.data,null,2)}</code>
  10. </pre>
  11. </div>
  12. );
  13. }

CatchBoundary 函数对应着预期的错误,即你在 loader、action 函数中,在客户端或服务端,手动抛出的 Response 错误,这些错误的路径是可预期的,在 CatchBoundary 中,通过 useCatch 钩子获取这些抛出的 Response 错误,然后展示对于的错误信息的 UI。

当我们没有在子路由中添加 ErrorBoundary 或 CatchBoundary 函数时,一旦遇到错误,这些错误就会向更上一级的路由冒泡,直至最顶层的路由页面,所以你只最好在最顶层的路由文件里声明一个 ErrorBoundary 和 CatchBoundary 函数,用于捕获所有可能的错误,然后在代码审查( Code Review)时及时排查出来。

 Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理) 开源 第16张

基于 Web 基础技术

Remix 专注于用 Web 基础技术,HTML/CSS + HTTP 等解决问题,同时提供了在 Web 全栈开发框架中所需要的所有状态和所有基础组件。

其中相关状态包含:

  1. //加载数据的状态
  2. useLoaderData()
  3. //更新数据的状态
  4. useActionData()
  5. //提交表单等相关状态
  6. useFormAction()
  7. useSubmit()
  8. //统一的加载状态
  9. useTransition()
  10. //错误抓取状态等
  11. useCatch()

以及 Web 网站组成的基础组件:

  • <Meta> 用于动态的设置网页的元信息,方便 SEO
  • <Script> 用于告知 Remix 是否需要在加载网页时导入相关 JS,因为大部分情况下 Remix 编写的页面无需 JS 也能正常工作
  • <Form> 用于替代原生的 <form> 方便在客户端和服务端进行表单操作,接管提交时的相应功能,使用 Fetch API 发起请求等,以及处理多次重复提交的竞争状态等

同时在路由函数所在文件里,可以通过声明 link 、meta 、links 、headers 等函数来声明对应的功能:

  • links 变量函数:表示此页面需要加载的资源,如 CSS、图片等
  1. importtype{LinksFunction}from"remix";
  2. importstylesHreffrom"../styles/something.css";
  3. exportletlinks:LinksFunction=()=>{
  4. return[
  5. //addafavicon
  6. {
  7. rel:"icon",
  8. href:"/favicon.png",
  9. type:"image/png"
  10. },
  11. //addanexternalstylesheet
  12. {
  13. rel:"stylesheet",
  14. href:"https://example.com/some/styles.css",
  15. crossOrigin:"true"
  16. },
  17. //addalocalstylesheet,remixwillfingerprintthefilenamefor
  18. //productioncaching
  19. {rel:"stylesheet",href:stylesHref},
  20. //prefetchanimageintothebrowsercachethattheuserislikelytosee
  21. //astheyinteractwiththispage,perhapstheyclickabuttontorevealin
  22. //asummary/detailselement
  23. {
  24. rel:"prefetch",
  25. as:"image",
  26. href:"/img/bunny.jpg"
  27. },
  28. //onlyprefetchitifthey'reonabiggerscreen
  29. {
  30. rel:"prefetch",
  31. as:"image",
  32. href:"/img/bunny.jpg",
  33. media:"(min-width:1000px)"
  34. }
  35. ];
  36. };
  • links 函数:声明需要 Prefetch 的页面,当用户点击之前就加载好资源
  1. exportfunctionlinks(){
  2. return[{page:"/posts/public"}];
  3. }
  • meta 函数:与 组件类似,声明页面需要的元信息
  1. //src/root.tsx
  2. import{
  3. Outlet,
  4. exportdefaultfunctionApp(){
  5. return(
  6. <Document>
  7. <Layout>
  8. <Outlet/>
  9. </Layout>
  10. </Document>
  11. );
  12. }
  13. functionDocument(){}
  14. functionLayout(){}
0
  • headers 函数:定义此页面发送 HTTP 请求时,带上的请求头信息
  1. //src/root.tsx
  2. import{
  3. Outlet,
  4. exportdefaultfunctionApp(){
  5. return(
  6. <Document>
  7. <Layout>
  8. <Outlet/>
  9. </Layout>
  10. </Document>
  11. );
  12. }
  13. functionDocument(){}
  14. functionLayout(){}
1

由此可见,Remix 提供了整个全栈 Web 开发生命周期所需要的几乎的一切内容,且内置最佳实践,确保你付出很少的努力就能开发出性能卓越、体验优秀的网站!

当然这篇文章并不能包含所有 Remix 的特性,看到这里仍然对 Remix 感兴趣的同学可以访问官网(https://Remix.run/)详细了解哦~ 官网提供了非常详细的实战教程帮助你使用 Remix 开发实际的应用。

了解了 Remix 的特性之后,你对 Remix 有什么看法呢?你觉得它能超过 Next.js ??

转载请说明出处
知优网 » Next.js 强劲对手来了! Remix 正式宣布开源(nextjs原理)

发表评论

您需要后才能发表评论