关于ide:Nextjs-强劲对手来了-Remix-正式宣布开源

7次阅读

共计 10241 个字符,预计需要花费 26 分钟才能阅读完成。

以下文章来源于程序员巴士,作者一只图雀

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

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

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 提供基于文件的路由,将读取数据、操作数据和渲染数据的逻辑都写在同一个路由文件里,不便一致性解决,这样能够跨客户端和服务端逻辑共享同一套类型定义。

看一段官网的代码:

import type {Post} from "~/post";
import {Outlet, Link, useLoaderData, useTransition} from "remix";

let postsPath = path.join(__dirname, "..", "posts");

async function getPosts() {let dir = await fs.readdir(postsPath);
  return Promise.all(dir.map(async (filename) => {let file = await fs.readFile(path.join(postsPath, filename));
      let {attributes} = parseFrontMatter(file.toString());
      invariant(isValidPostAttributes(attributes),
        `${filename} has bad meta data!`
      );
      return {slug: filename.replace(/.md$/, ""),
        title: attributes.title,
      };
    })
  );
}

async function createPost(post: Post) {let md = `---\ntitle: ${post.title}\n---\n\n${post.markdown}`;
  await fs.writeFile(path.join(postsPath, post.slug + ".md"), md);
  return getPost(post.slug);
}

export async function loader({request}) {return getProjects();
}

export async function action({request}) {let form = await request.formData();
  const post = createPost({title: form.get("title") });
  return redirect(`/posts/${post.id}`);
}

export default function Projects() {let posts = useLoaderData<Post[]>();
  let {state} = useTransition();
  let busy = state === "submitting";

  return (
    <div>
      {posts.map((post) => (<Link to={post.slug}>{post.title}</Link>
      ))}

      <Form method="post">
        <input name="title" />
        <button type="submit" disabled={busy}>
          {busy ? "Creating..." : "Create New Post"}
        </button>
      </Form>
      
      <Outlet />
    </div>
  );
}

上述是一个路由文件,如果它是 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} 对应的路由页面内容。

通过 Remix 提供的 useTransition 钩子,咱们能够拿到表单提交的状态,当申请还未返回后果时,咱们能够通过这个状态 state 判断是否要展现一个加载状态,提醒用户以后的申请停顿。

同时 Post 类型在 useLoaderData<Post[]>()createPost(post: Post)时能够共用。

有同学可能留神到了,下面咱们整个页面渲染、到发动创立 Post 申请、到后盾创立 Post,到重定向到 Post 详情,这整个过程,咱们无需在前端应用任何 JavaScript 相干的内容,仅仅通过 HTML 与 HTTP 就实现了这个交互,所以 Remix 的网站在 Disbaled JavaScript 运行环境下也能够失常工作。

通过上图咱们能够看到,即便 JavaScript 曾经敞开了,咱们的网站仍然能够失常运行。

🤯 弱小的嵌套路由体系

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

而 Remix 最具特色的性能之一就是嵌套路由。在 Remix 中,一个页面通常蕴含多层级页面,每个子页面管制本身的 UI 展示,而且独立管制本身的数据加载和代码宰割。

拿官网的例子来看如下:

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

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

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

来看一个理论例子:

// src/root.tsx
import {
  Outlet,
  
export default function App() {
  return (
    <Document>
      <Layout>
        <Outlet />
      </Layout>
    </Document>
  );
}

function Document() {}
function Layout() {}
// src/routes/admin.tsx
import {Outlet, Link, useLoaderData} from "remix";
import {getPosts} from "~/post";
import type {Post} from "~/post";
import adminStyles from "~/styles/admin.css";

export let links = () => {return [{ rel: "stylesheet", href: adminStyles}];
};

export let loader = () => {return getPosts();
};

export default function Admin() {let posts = useLoaderData<Post[]>();
  return (
    <div className="admin">
      <nav>
        <h1>Admin</h1>
        <ul>
          {posts.map((post) => (<li key={post.slug}>
              <Link to={post.slug}>{post.title}</Link>
            </li>
          ))}
        </ul>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}
// src/routes/admin/index.tsx
import {Link} from "remix";

export default function AdminIndex() {
  return (
    <p>
      <Link to="new">Create a New Post</Link>
    </p>
  );
}
// src/routes/admin/new.tsx
import {useTransition, useActionData, redirect, Form} from "remix";
import type {ActionFunction} from "remix";
import {createPost} from "~/post";
import invariant from "tiny-invariant";

export let action: ActionFunction = async ({request}) => {await new Promise((res) => setTimeout(res, 1000));
  let formData = await request.formData();

  let title = formData.get("title");
  let slug = formData.get("slug");
  let markdown = formData.get("markdown");

  let errors = {};
  if (!title) errors.title = true;
  if (!slug) errors.slug = true;
  if (!markdown) errors.markdown = true;

  if (Object.keys(errors).length) {return errors;}

  await createPost({title, slug, markdown});

  return redirect("/admin");
};

export default function NewPost() {let errors = useActionData();
  let transition = useTransition();

  return (
    <Form method="post">
      <p>
        <label>
          Post Title: {errors?.title && <em>Title is required</em>}
          <input type="text" name="title" />
        </label>
      </p>
      <p>
        <label>
          Post Slug: {errors?.slug && <em>Slug is required</em>}{" "}
          <input type="text" name="slug" />
        </label>
      </p>
      <p>
        <label htmlFor="markdown">Markdown:</label>{" "}
        {errors?.markdown && <em>Markdown is required</em>}
        <br />
        <textarea rows={20} name="markdown" />
      </p>
      <p>
        <button type="submit">
          {transition.submission ? "Create..." : "Create Post"}
        </button>
      </p>
    </Form>
  );
}

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

整个 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 对应路由文件的渲染内容,见下图:

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

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

通过这种文件即路由,同名文件夹下文件即嵌套路由的形式,而后通过在父页面外面通过 <Outlet> 的形式渲染依据子路由渲染子页面内容,极大的减少了灵活性,且每个子路由对应独立的路由文件,具备独立的数据处理逻辑、内容渲染逻辑、错误处理逻辑。

上述嵌套路由一个不言而喻的长处就是,某个局部如果报错了,联合后续会提到的 ErrorBoundaryCatchBoundary 这个局部能够显示谬误的页面,而用户依然能够操作其余局部,而不须要刷新整个页面以从新加载应用,极大进步网站容错性。

👋🏻 再见,加载状态

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

这是因为这些利用不足相似 Remix 这样的嵌套路由的概念,拜访某个路由时,就是拜访这个路由对应的页面,只有这个页面加载进去之后,外面的子组件渲染时,再进行数据的获取,再加载子组件,如此往返,就出现瀑布流式的加载,带来了很多两头的加载状态。

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

可见尽管咱们首屏拿到内容可能会慢一点,然而再也不须要加载状态,再见,菊花图 👋🏻,再见,骨架屏👋🏻。

同时借助嵌套路由,当咱们鼠标 Hover 到某个链接筹备点击切换某个子路由时,Remix 提供了预获取(Prefetch)性能,能够提前并行获取子路由文档和各种资源,包含 CSS、图片、相干数据等,这样当咱们理论点击这个链接切换子路由时,页面能够立刻出现进去:

😇 欠缺的错误处理

咱们的网站常常会遇到问题,应用其余框架编写时,网站遇到问题可能用户就须要从新刷新网站,而对于 Remix 来说,基于嵌套路由的理念,则无需从新刷新,只须要在对应的谬误的子路由展现错误信息,而页面的其余局部依然能够失常工作:

比方咱们上图的右下角子路由呈现了问题,那么这块会展现出问题时的谬误页面,而其余页面局部依然展现失常的信息。

正因为谬误常常产生,且处理错误异样艰难,蕴含客户端、服务端的各种谬误,蕴含预期的、非预期的谬误等,所以 Remix 内建了欠缺的错误处理机制,提供了相似 React 的 ErrorBoundary 的理念。

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

export default function RouteFunction() {}

export function ErrorBoundary({error}) {console.error(error);
  return (
    <div>
      <h2>Oh snap!</h2>
      <p>
        There was a problem loading this invoice
      </p>
    </div>
  );
}

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

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

import {useCatch} from "remix";

export function CatchBoundary() {let caught = useCatch();

  return (
    <div>
      <h1>Caught</h1>
      <p>Status: {caught.status}</p>
      <pre>
        <code>{JSON.stringify(caught.data, null, 2)}</code>
      </pre>
    </div>
  );
}

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

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

🌟 基于 Web 根底技术

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

其中相干状态蕴含:

// 加载数据的状态
useLoaderData()

// 更新数据的状态
useActionData()

// 提交表单等相干状态
useFormAction()
useSubmit()

// 对立的加载状态
useTransition()

// 谬误抓取状态等
useCatch()

以及 Web 网站组成的根底组件:

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

同时在路由函数所在文件里,能够通过申明 linkmetalinksheaders 等函数来申明对应的性能:

  • links 变量函数:示意此页面须要加载的资源,如 CSS、图片等
import type {LinksFunction} from "remix";
import stylesHref from "../styles/something.css";

export let links: LinksFunction = () => {
  return [
    // add a favicon
    {
      rel: "icon",
      href: "/favicon.png",
      type: "image/png"
    },

    // add an external stylesheet
    {
      rel: "stylesheet",
      href: "https://example.com/some/styles.css",
      crossOrigin: "true"
    },

    // add a local stylesheet, remix will fingerprint the file name for
    // production caching
    {rel: "stylesheet", href: stylesHref},

    // prefetch an image into the browser cache that the user is likely to see
    // as they interact with this page, perhaps they click a button to reveal in
    // a summary/details element
    {
      rel: "prefetch",
      as: "image",
      href: "/img/bunny.jpg"
    },

    // only prefetch it if they're on a bigger screen
    {
      rel: "prefetch",
      as: "image",
      href: "/img/bunny.jpg",
      media: "(min-width: 1000px)"
    }
  ];
};
  • links 函数:申明须要 Prefetch 的页面,当用户点击之前就加载好资源
export function links() {return [{ page: "/posts/public"}];
}
  • meta 函数:与 <Meta> 组件相似,申明页面须要的元信息
import type {MetaFunction} from "remix";

export let meta: MetaFunction = () => {
  return {
    title: "Josie's Shake Shack", // <title>Josie's Shake Shack</title>
    description: "Delicious shakes", // <meta name="description" content="Delicious shakes">
    "og:image": "https://josiesshakeshack.com/logo.jpg" // <meta property="og:image" content="https://josiesshakeshack.com/logo.jpg">
  };
};
  • headers 函数:定义此页面发送 HTTP 申请时,带上的申请头信息
export function headers({loaderHeaders, parentHeaders}) {
  return {
    "X-Stretchy-Pants": "its for fun",
    "Cache-Control": "max-age=300, s-maxage=3600"
  };
}

由此可见,Remix 提供了整个全栈 Web 开发生命周期所须要的简直的所有内容,且内置最佳实际,确保你付出很少的致力就能开发出性能卓越、体验优良的网站!

当然这篇文章并不能蕴含所有 Remix 的个性,看到这里依然对 Remix 感兴趣的同学能够拜访官网 (https://remix.run/) 具体理解哦~ 官网提供了十分具体的实战教程帮忙你应用 Remix 开发理论的利用。

理解了 Remix 的个性之后,你对 Remix 有什么认识呢?你感觉它能超过 Next.js 🐴?

开源前哨 日常分享热门、乏味和实用的开源我的项目。参加保护 10 万 + Star 的开源技术资源库,包含:Python、Java、C/C++、Go、JS、CSS、Node.js、PHP、.NET 等。

正文完
 0