Next.js多端项目搭建指南

5,750 阅读9分钟

前言

Next.js 是一个基于 React 的轻量级、灵活的 Web 应用开发框架,可用于构建静态网站、单页面应用、服务器渲染应用和原生应用等各种类型的 Web 应用。 Next.js 提供了一些独特的特性,包括:

  1. 预渲染:Next.js 可以在构建时或请求时预渲染页面,以提高页面加载速度和 SEO。
  2. 自动代码分割:Next.js 可以根据页面的需求自动将代码拆分成更小的模块,以提高页面加载速度。
  3. 服务端渲染:Next.js 通过在服务器上渲染 React 组件,可以提供更好的性能和 SEO。
  4. 客户端路由:Next.js 支持客户端路由,可以在不重新加载整个页面的情况下切换页面。
  5. 静态导出:Next.js 可以将整个应用程序静态导出为 HTML、CSS 和 JavaScript 文件,以便于部署到任何托管平台。

Next.js 还提供了一些其他有用的功能,例如样式和 CSS 模块化、支持 TypeScript、自动化测试等。它还具有灵活的插件系统和生态系统,可以轻松扩展和定制应用程序。

总之,Next.js 是一个非常强大和灵活的 Web 应用开发框架,可以帮助开发人员更快速、更高效地构建各种类型的 Web 应用。

以上内容来自 ChatGPT

关于Next.js的基本使用这里就不多说,本文主要讲述如何基于Next.js快速搭建一个多端应用(PC、Mobile、Tablet)。

详细步骤

Next.js作为一个全栈框架,是完全可以支持在服务端进行 UA 嗅探进而动态返回不同端的首页资源的。本文将从搭建项目开始,逐步完成一个支持多端的 SSR | SSG 应用的搭建。

demo项目技术栈:Next.js@13.3.1、Typescriptpnpm
demo项目地址:next-multiterminal

初始化项目

使用 next-app 指令初始化一个空项目

pnpm create next-app next-multiterminal --typescript

项目初始化的具体配置如下:

1-1.png

demo项目里设置的配置为:

  • 启用eslint
  • 不引入Tailwind css
  • 创建src目录
  • 不使用app目录
  • 设置路径别名 @/ => src/

大家可以根据自己的喜好配置这些初始化参数。

安装依赖

在项目里需要检测UA,为此需要安装一个依赖:is-mobile。当然也可以选择其他的库,甚至是自己写一个方法来进行UA嗅探。

pnpm add is-mobile

声明静态路由

Next.js会根据文件目录结构注册静态路由,为了支持PC、Mobile、Tablet三个终端展示不同的资源页,需要创建三个目录(即声明三个静态路由):

1-2.png

静态路由跟终端的关系是:

路由终端类型
/pcPC端
/mobile手机端
/tablet平板电脑

每个静态路由下有一个入口文件 index.tsx:

1-3.png

设置重定向转发

首先需要明确浏览器嗅探实现的能力,主要是两点:

  1. 访问首页,应用能够自动根据UA嗅探结果重定向到指定的终端展示页 / => /pc | /mobile | /tablet
  2. 访问某个静态路由,应用能够自动根据UA嗅探结果重定向到当前终端的静态路由 /pc | /mobile | /tablet => 匹配该终端的路由

实现目标1

访问首页能够自动重定向到指定的路由页

首页路由(/)默认会进入到src/pages/index.tsx文件里,因此需要在这里进行UA嗅探并根据嗅探结果重定向到指定的路由页。 为了获得UA对象,需要拿到http请求体。这就需要用到Next.js提供的一个钩子函数 getServerSideProps

If you export a function called getServerSideProps (Server-Side Rendering) from a page, Next.js will pre-render this page on each request using the data returned by getServerSideProps.

如果你从页面导出一个名为getServerSideProps(服务器端渲染)的函数,Next.js会在每次请求时使用getServerSideProps返回的数据预渲染该页。

getServerSideProps入参是一个 ctx对象,该对象包含有 reqres对象。src/pages/index.tsx完整代码如下:

import { NextPageContext } from "next/dist/shared/lib/utils";
import isMobile from "is-mobile";

export default function Index() {
  return <></>;
}

export async function getServerSideProps(ctx: NextPageContext) {
  // 是否为移动端(平板电脑不算做移动端)
  const isMobileDevice = isMobile({ ua: ctx.req, tablet: false });
  // 是否为平板电脑
  const isTabletDevice =
    !isMobileDevice && isMobile({ ua: ctx.req, tablet: true });
  let route = "/pc";
  if (isMobileDevice) {
    route = "/mobile";
  } else if (isTabletDevice) {
    route = "/tablet";
  }
  return {
    props: {},
    redirect: {
      destination: route,
      permanent: true,
    },
  };
}

注意: src/pages/index.tsx本身不需要返回任何内容,因此直接 return <></>即可。我们主要用到的就是 getServerSideProps函数。

实现目标2

访问非首页能够自动重定向到指定的路由页

对于目标2,需要在 _app.tsx 中定义重定向逻辑,这里需要用到Next.js提供的另一个API getInitialProps:

getInitialProps enables server-side rendering in a page and allows you to do initial data population, it means sending the page with the data already populated from the server. This is especially useful for SEO.

getInitialProps will disable Automatic Static Optimization.

getInitialProps 能够在页面中启用服务器端渲染,并允许您进行初始数据填充,这意味着从服务器发送的页面已经填充了数据。这对于 SEO 特别有用。

getInitialProps 将禁用自动静态优化。

getInitialProps有一个入参 ctx,ctx拥有的属性有:

  • pathname - 当前请求路由
  • query - URL中的查询参数
  • asPath - 当前请求的完整路径(包含参数)
  • req - HTTP request object (server only)
  • res - HTTP response object (server only)
  • err - 错误对象

做UA嗅探主要用到的就是 req对象。_app.tsx完整代码如下:

import "@/styles/globals.css";
import App, { AppProps, AppContext } from "next/app";
import Router from "next/router";
import { NextPageContext } from "next/dist/shared/lib/utils";
import isMobile from "is-mobile";

export default function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

function redirect(ctx: NextPageContext, location: string) {
  if (ctx.req) {
    ctx.res?.writeHead(308, { Location: location });
    ctx.res?.end();
  } else {
    Router.push(location);
  }
}

MyApp.getInitialProps = async (context: AppContext) => {
  const { ctx } = context;
  // 是否为移动端(平板电脑不算做移动端)
  const isMobileDevice = isMobile({ ua: ctx.req, tablet: false });
  // 是否为平板电脑终端
  const isTabletDevice =
    !isMobileDevice && isMobile({ ua: ctx.req, tablet: true });
  if (isMobileDevice && !ctx.pathname.startsWith("/mobile")) {
    redirect(ctx, "/mobile");
  } else if (isTabletDevice && !ctx.pathname.startsWith("/tablet")) {
    redirect(ctx, "/tablet");
  } else if (
    !isMobileDevice &&
    !isTabletDevice &&
    !ctx.pathname.startsWith("/pc")
  ) {
    redirect(ctx, "/pc");
  }
  return {
    ...App.getInitialProps(context),// 必须加上这个,表示透传_app.tsx默认的值
  };
};

至此我们已经完成了不同首页自动重定向到终端路由以及终端路由间的互相切换,效果如下:

1-4.gif

SSG模式下的调整

基于getServerSidePropsgetInitialProps API我们已经实现了在 SSR 模式下的多端应用搭建。
而Next.js除了提供 SSR 能力,还提供了 SSG 能力。该能力可以在构建阶段生成指定的路由页,对于内容型站点非常好用。但是使用 Next.js 的 SSG能力有一些要求:

  • 组件中不能使用getServerSideProps API
  • next.config.jsimagesunoptimized配置必须为true
  • 不能使用在next.config.js里配置redirect
  • ...

而在上文的演示里,我们正好用到了getServerSidePropsAPI,这与 SSG 模式不匹配,需要进行方案修改。通过翻阅官方文档,可以发现 Next.js 提供了替代方案,那就是:middleware

Middleware allows you to run code before a request is completed, then based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly.

Middleware runs before cached content, so you can personalize static files and pages. Common examples of Middleware would be authentication, A/B testing, localized pages, bot protection, and more. Regarding localized pages, you can start with i18n routing and implement Middleware for more advanced use cases.

中间件允许您在请求完成之前运行代码,然后基于传入的请求,您可以通过重写、重定向、修改请求或响应头或直接响应来修改响应。

中间件在缓存内容之前运行,因此您可以个性化静态文件和页面。中间件的常见示例包括身份验证、A/B测试、本地化页面、bot保护等。关于本地化页面,您可以从i18n路由开始,并为更高级的用例实现中间件。

定义中间件的方式也很简单,在src/pages目录下新建一个middleware.ts|js文件,然后在里面写逻辑即可。
疑问:为什么在中间件里可以重定向呢?
解答:因为Next.js里中间件的执行顺序在文件路由之前,因此可以重新制定路由目的。

调整1-定义middleware

新建 src/pages/middleware.ts,在里面实现UA嗅探并根据嗅探结果重定向:

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import isMobile from "is-mobile";

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const url = request.nextUrl.clone();
  const ua = request.headers.get("user-agent") || "";
  const isMobileDevice = isMobile({ ua, tablet: false });
  const isTabletDevice = !isMobileDevice && isMobile({ ua, tablet: true });
  url.pathname = "/pc";
  if (isMobileDevice) {
    url.pathname = "/mobile";
  } else if (isTabletDevice) {
    url.pathname = "/tablet";
  }
  if (isMobileDevice && !url.pathname.startsWith("/mobile")) {
    return NextResponse.redirect(url);
  } else if (isTabletDevice && !url.pathname.startsWith("/tablet")) {
    return NextResponse.redirect(url);
  } else if (
    !isMobileDevice &&
    !isTabletDevice &&
    !url.pathname.startsWith("/pc")
  ) {
    return NextResponse.redirect(url);
  }
  return response;
}

// 监听指定的一级路由(也可以监听所有/*)
export const config = {
  matcher: ["/", "/pc", "/mobile", "/tablet"],
};

需要注意的是,在上面的代码里,传入isMobile中的ua参数类型是 string(这是is-mobile库支持的调用方式),而不是HttpRequest

调整2-删除src/pages/index.tsx中的getServerSideProps

// import { NextPageContext } from "next/dist/shared/lib/utils";
// import isMobile from "is-mobile";

export default function Index() {
  return <></>;
}

// export async function getServerSideProps(ctx: NextPageContext) {
//   const isMobileDevice = isMobile({ ua: ctx.req, tablet: false });
//   const isTabletDevice =
//     !isMobileDevice && isMobile({ ua: ctx.req, tablet: true });
//   let route = "/pc";
//   if (isMobileDevice) {
//     route = "/mobile";
//   } else if (isTabletDevice) {
//     route = "/tablet";
//   }
//   return {
//     props: {},
//     redirect: {
//       destination: route,
//       permanent: true,
//     },
//   };
// }

调整3-修改next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  // 增加下面这项配置——关闭image自动优化
  images: {
    unoptimized: true,
  },
};
module.exports = nextConfig;

调整完毕后执行如下指令即可进行SSG模式下构建:

pnpm build
pnpm export

export结果:

1-5.png

总结

通过上面的步骤,我们基于Next.js搭建了一个支持多端访问的应用。相比通过css实现多端,直接在代码层次上实现物理隔离的多端展示功能更加强大,开发也更加灵活。
因为Next.js也支持SSG,为了兼容SSG模式下的限制,需要对重定向逻辑进行修正,利用middleware实现重定向。
上面demo的主要技术点有:

  • 在Next.js的SSR模式下,可以使用getInitialPropsgetServerSideProps进行UA嗅探并重定向到指定终端路由页
  • 在Next.js的SSG模式下,需要借助getInitialPropsmiddleware进行UA嗅探和路由重定向

参考

next.js docs
is-mobile

推荐模板

这里推荐一个模板项目c-shopping,一个基于next.js的web电商系统,支持响应式交互,界面优雅,功能丰富,小巧迅速,包含一个电商平台MVP完整功能,具备良好的审美风格与编码设计。 APP 开源地址: github.com/huanghanzhi… 
项目在线演示地址: shop.huanghanlian.com/ 
vercel 部署地址: c-shopping-three.vercel.app/