相比于 Next.js 我更喜爱 Remix

如题,本文以 Next.js 和 Remix 实现访问量统计为例。讲述我为什么更喜爱 Remix 框架。

Next.js Pageviews

首先以 Next.js 为例,其实在我的眼里, Next.js 更像是一个动态网站生成器。它与 Gatsby (另一个基于 React 的网站生成器)相比,门槛较低,能够疾速上手。而 Gatsby 则须要肯定的 GraphQL 根底。

废话不多说,开始正题。 页面拜访点击这个性能必定是须要数据存储的(数据库或者缓存、键值对存储等后端服务都能够作为代替)。

在我之前的文章里做过 Next.js 与 Remix 的比照——《网站的将来:Next.js 与 Remix》,Next.js 中 API 路由是须要避免在 pages/api/ 目录下的,而 Remix 就是路由,会更加灵便一些。

所以在 Next.js 实现的时候,须要先配置接口。这里我就先放一个比拟有名的理论我的项目:

  • 源码: https://github.com/leerob/lee...
  • 网站: https://leerob.io/

pages/api/views/[slug].ts 中实现接口的实现:

// https://github.com/leerob/leerob.io/blob/main/pages/api/views/%5Bslug%5D.tsimport type { NextApiRequest, NextApiResponse } from 'next';import prisma from 'lib/prisma';export default async function handler(  req: NextApiRequest,  res: NextApiResponse) {  try {    const slug = req.query.slug.toString();    if (req.method === 'POST') {      const newOrUpdatedViews = await prisma.views.upsert({        where: { slug },        create: {          slug        },        update: {          count: {            increment: 1          }        }      });      return res.status(200).json({        total: newOrUpdatedViews.count.toString()      });    }    if (req.method === 'GET') {      const views = await prisma.views.findUnique({        where: {          slug        }      });      return res.status(200).json({ total: views.count.toString() });    }  } catch (e) {    return res.status(500).json({ message: e.message });  }}

GET 申请为查问, POST 申请为更新。

而后在页面中的话,须要应用 Fetch 库,如 swr 来进行调用:

// https://github.com/leerob/leerob.io/blob/main/components/ViewCounter.tsximport { useEffect } from 'react';import useSWR from 'swr';import fetcher from 'lib/fetcher';import { Views } from 'lib/types';export default function ViewCounter({ slug }) {  const { data } = useSWR<Views>(`/api/views/${slug}`, fetcher);  const views = new Number(data?.total);  useEffect(() => {    const registerView = () =>      fetch(`/api/views/${slug}`, {        method: 'POST'      });    registerView();  }, [slug]);  return <span>{`${views > 0 ? views.toLocaleString() : '–––'} views`}</span>;}

这里你会发现,页面在刚加载的时候,显示的文章浏览量是 ---- views,并且在后盾里发了一个申请,实现后才会将文章计数更新到页面中展现进去。当我拜访文章列表页面的时候,其实页面上发送了茫茫多的网络申请。

在前后端不拆散的我的项目中实现前后端齐全拆散的代码,并通过 HTTP 的申请再去调用操作。这个操作就……一言难尽,反正性能并不高,对于 Google 搜索引擎收录来说或者影响不大,然而百度就必定是无解的。

Remix Pageviews

因为 Remix 并不能提供 SSG (动态站点生成) 性能,所以前后端并不拆散。这对于一些简略的动静需要的网站零碎来说,就十分的敌对,甚至在 Typescript 编写代码的时候,都不必特地去关注类型定义的问题。

app 目录中搁置了我的项目的代码,routes 目录下自定义路由。比方我这个服务的实现,能够放在 app/services/views.server.ts ,其中,如果代码仅会跑在后端运行,能够用 .server.ts 的后缀进行辨别。

这里的实现也绝对会更简略一些,只须要两个办法即可,一个是写入数据计数,另一个是查问。在 Remix 中,我进行了一些设计思路上的优化,将写入计数和查问都在一次性实现。

因为是应用了 Cloudflare KV 存储(收费服务),所以实现起来有点像 Redis 的用法,总访问量的统计也须要一个键名独自计数。

// services/views.server.ts// 定义返回值的类型, slug 示意地址 /slug 或者 total 示意总数export interface PageView {  slug: string;  pv: number;}// 因为本地开发环境中,无奈调用 Cloudflare Worker KV 绑定的存储桶,所以写了一个简略的 Mock 办法const mockDb: KVNamespace = {  // eslint-disable-next-line  async get(...args: any[]) {    return Promise.resolve('9999999');  },  // eslint-disable-next-line  async put(...args: any[]) {    return Promise.resolve();  }};export class ViewsModel {  db: KVNamespace;  constructor(db?: KVNamespace) {    this.db = db || mockDb;  }  // 办法一,用于对特定门路进行计数,并减少拜访总数  // 记录实现后,间接将数值作为 return 后果,缩小了再次调用  async visit(slug: string) {    const [views, total] = await Promise.all(      [slug, 'total'].map((key) => this.db.get(key, 'text'))    );    const pv = Number(views || 0) + 1;    const pvTotal = Number(total || 0) + 1;    await Promise.all([      this.db.put(slug, pv.toString()),      this.db.put('total', pvTotal.toString())    ]);    return [      { slug, pv },      { slug: 'total', pv: pvTotal }    ] as PageView[];  }  // 专门为文章列表页筹备的接口,能够批量查问文章访问量  async list(slugs: string[]) {    const result = await Promise.allSettled(      slugs.map((slug) =>        this.db          .get(slug, 'text')          .then((views) => ({ slug, pv: Number(views || 0) } as PageView))      )    );    return result      .filter((x) => x.status === 'fulfilled')      .map((x: { value: PageView }) => x.value);  }}

而后查问的话依据须要,我将 visit 办法的应用放在了 root.tsx 下:

// app/root.tsximport { ViewsModel,PageView } from './services/views.server';// 疏忽了其余的代码,只保留外围的局部// 类型定义export type CustomEnv = {  VIEWS: KVNamespace;};export type AppContext = {  env: CustomEnv;};export const loader: LoaderFunction = async ({ request, context = {} }) => {  const { env = {} }: CustomEnv = context as AppContext;  const url = new URL(request.url);  const slug = url.pathname;  // eslint-disable-next-line  const PV = new ViewsModel(env.VIEWS);  const views = await PV.visit(slug);  const data: LoaderData = {    views  };  return json(data);};

代码中类型的定义和默认值的设置占了很大一部分,须要解释一下:因为我目前采纳的计划是筹备把网站放在 Cloudflare Pages 上(没搞服务器,所以应用的全是收费的计划),KV Namespace 存储计划在本地开发环境中无奈调试。才有了这么多奇怪的打补丁一样的代码,疏忽这一部分。只看核心内容:

  • 首先是通过 Request 的 URL 取出以后页面的 Slug,并进行计数。
  • 而后将后果返回给 loader 办法。

几行代码实现了接口的调用和数据的查问,该局部会随着页面路由的加载主动执行并将后果返回(我不太确定是否动静路由中每次路由扭转都会触发,如果这里与我构想的不统一,后续我会回来批改这篇文章)。

而后在 App 中应用该数据即可:

// app/root.tsx// 仍然是方才那个页面,局部代码export type LoaderData = {  views: PageView[]};function App() {  const data = useLoaderData<LoaderData>();  return (    <html>      <head>        <meta charSet='utf-8' />        <meta name='viewport' content='width=device-width,initial-scale=1' />        <Meta />        <Links />      </head>      <body>        <div id='app' className='relative'>          <div>            <pre>{JSON.stringify(data, null, 2)}</pre>          </div>          <Outlet />        </div>        <ScrollRestoration />        <Scripts />        {process.env.NODE_ENV === 'development' && <LiveReload />}      </body>    </html>  );}

这里,data 就会返回这样类型的数据:

{  views: [    { slug: '/', pv: 99999},    { slug: 'total', pv: 99999}  ]}

把数据传给组件或者状态治理中即可。同理,能够在 routes/blog.tsx 页面中退出 loader 办法,来获取页面上的文章列表,间接拼接每个文章的浏览量数据。

这样的框架在设计和编码的过程中,仿佛更合乎软件工程的高内聚、低耦合的思维。能够真正意义上的去实现模块化和微前端的开发。

目前我还在摸索中,能够继续关注我的 Remix 集体网站我的项目:

  • Github: https://github.com/willin/wil...
  • 码云镜像: https://gitee.com/willin/will...
  • 开发预览: https://willin-wang.pages.dev/
  • 正式版公布: https://willin.wang