用 React 写官网,你又多了一个选择-Next.SSG

3,864 阅读5分钟

前情概要

我们都知道,官网编写使用 React 客户端渲染会有冲突,因为 是单页面应用,且 DOM 因为是JS渲染,会不利于项目的 SEO (搜索引擎优化)。而且由于 React 运行时初次渲染会首先出现一个白屏,这样对用户体验来说是破坏性的。

但是对于习惯了JSX编写习惯的我们,很难再回到 Native JS 的时代,我的诉求有以下几点:

  1. 最好以 React 技术栈为基础
  2. 生成的代码利于 SEO
  3. 页面快速加载,提升用户体验
  4. 现代化的代码编写方式(即:高编码效率、高编码体验、高编码质量)

正好最近一个朋友有一个官网的需求,我就顺手帮忙做了。记得自己上次写官网还是用的 HTML + JQ,那是很久以前的记忆了。这次接到后,马上有(手)条(忙)不(脚)紊(乱)的开始查阅资料,经过一顿Search 操作后,有了解到当前比较热门的一些利好SEO的开发框架,最后也是选择了 Next.SSG。用起来发现很顺手,非常好用,相比以前的开发体验有了质的飞跃;同时也在开发过程中踩过一些坑,在此整理出来给大家排雷,希望能对大家有帮助。

流行 SEO 方案对比

方案优势劣势
prerender 预渲染部署方便,开发成本低;1. 无法render动态改变的页面(如:某商品详情页) ;2. 页面太多时造成存储负担;
Next 服务端渲染一步到位,开发自主控制页面渲染;可以结合next框架,实现快速开发;1. 对于已在线上运营的spa项目改造成本太大;
seo-mask1. 无需改动源代码;2. 自由决定需要被爬取的内容;1.需要另外维护一套网站代码(开发成本极低);2.爬虫升级后,mask页面有被过滤的风险;
  • prerender 预渲染方案:鉴于劣势较多,且开发体验稍差,我们在此不做赘述;
  • seo-mask 对于现有大型系统,改动成本较高时会有较大优势;

基于项目需求匹配度,我们选择基于 React 的 Next.js 进行开发

快速熟悉 Next.js

Next.js 是一个轻量级的 React 服务端渲染框架、Next.js 的预渲染可以与前端 React 无缝对接

它支持三种渲染方式包括

  • 客户端渲染 BSR (客户端渲染,用 JS、Vue、React 创建 HTML)
  • 静态页面生成 SSG (页面静态化,将React 组件提前渲染成 HTML)
  • 服务端渲染 SSR (服务端渲染)

你可以通过 Next 官网 了解更多

本次我们选择 Next SSG 方案,它相比其他两者具有以下三个优势:

  1. 可以打包生成静态页面,方便前后端分离;
  2. 可以结合 Next 框架,实现快速开发;
  3. 不依赖 Node 服务器资源;

那既然选好了技术方案,那我们就愉快的开始写代码吧~

开发项目

快速开始

我们基于 create-next-app 快速搭建项目

# 初始化
$ npx create-next-app@latest

# 初始化带ts
$ npx create-next-app@latest --typescript

搭建后,官网文档写的非常详细,暂不作赘述,可以参考文档 nextjs quick start

Next 编写 React 应用,注意这六点!

路由配置

在纯 React 项目中,我们通过 react-router 相关插件实现路由配置

但是在 Next 中,需要借助 next/route 实现,有以下特点:

  • 访问路由与 src/pages 下的文件/文件夹路径相同

如:想要访问 /demo1/demo2 路由,在pages文件夹下创建 demo1demo2 文件夹即可,然后 在 index.js 写业务逻辑即可。

|-pages
  |--demo1
    |--index.js
  |--demo2
    |--index.js
  • 路由跳转使用 标签
import Link from 'next/link'

const Comp = () => {

    return (
        <Link href='/demo'>
            <div>jump router link</div>
        </Link>
    )
}

路由跳转也可以使用 js 方式调转

import { useRouter } from 'next/router'

const Comp = () => {
    const handleJump = () => {
        router.push('/demo')
    }
    
    return (
        <div>jump router link</div>
    )
}

动态路由

Next 也支持配置动态路由,虽然没有像 react-router 一样的配置,但 Next 也对动态路由进行了支持。

我们通过将文件命名规则为左右中括号,中间的即为动态参数,如 demo1/[locale].js 中的 locale 参数,然后我们通过 router.query 对象获取到对应的动态参数。

// pages 文件夹命名

|-pages
  |--demo1
    |--[locale].js
// 获取路由参数

import { useRouter } from 'next/router'

const Comp = () => {
    const router = useRouter() || {}
    const { locale } = router.query || {}

    return (
        <div>dynamic routing</div>
    )
}

通用头部和底部

next 对生成 SSG 静态页面,提供了配置HTML的 <head></head> 模块,我们可以把SEO信息维护在这个组件下,就不用像写静态页面一样每个html文件都写复制一套啦。

import Head from 'next/head'

export default function Header() {
    return (
        <Head>
          <meta httpEquiv='Content-Type' content='text/html; charset=UTF-8'></meta>
          <meta httpEquiv='X-UA-Compatible' content='IE=edge,chrome=1'></meta>
          <title>
            demo page, with seo info
          </title>
          ....
        </Head>
    )
}

Image 标签使用

在 Next.js 10 版以后发布了 <Image /> 这个组件,方便我们提升图片资源加载效率的问题,它使用起来非常方便,基本属于开箱即用

import Image from "next/image";

function Demo() {
  return (
    <>
      <h1>My Homepage</h1>
      <Image
        src="/me.png"
        alt="Picture of the author"
        width={500}
        height={500}
      />
      <p>Welcome to my homepage!</p>
    </>
  );
}

export default Demo;
踩坑问题及解决方案:

但是我在引入本地路径资源时遇到了一个问题。由于他使用的是相对路径,我在项目开发完成进行打包时,发现生成的 SSG 文件发布后,向对图片无法展示的问题。在经过一顿 google 之后,有指出在 next.config.js 文件中修改 images.loader 的配置的,配置如下图:

但是经过一顿尝试后发现都无法在生产环境正常展示图片。

所谓黄天不负有心人,经过大半夜的研究后,发现可以直接使用 html 原生 img 标签,终于解决了这个问题,以下是我基于此封装的自定义 Image 组件。于是又可以愉快的继续其他开发啦。

export default function Image({ src, alt,...props }) {
  const realSrc = src.startsWith('http') ? src : `/static${src}`

  return (
    <picture>
      <source srcSet={realSrc} type="image/png" />
      <img src={realSrc} {...props} alt={ alt || 'image'} />
    </picture>
  )
}

另外说明一下 Image.loder 的配置

images: {
    // webpack loader 加载方式
    loader: 'akamai',
    // base path
    path: '',
    // 注意:如果图片资源是远程地址url,需要配置支持访问的域名,否则也不会正常展示图片
    domains: [
      'p16-sign-va.tiktokcdn.com',
      'sf16-sg-default.akamaized.net',
      'v16m-default.akamaized.net'
    ]
  }

异步请求

A. 初始化请求

Next.js提供了getStaticProps方法来实现页面的SSG,其基本用法如下:

function Blog({ posts }) {

  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

export async function getStaticProps() {

  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
  }
}

export default Blog
B. 表单提交

如果有表单相关的请求,可以使用form 提交的方式,进行ajax请求

export default function () {

    const handleSubmit = async (event) => {
        event.preventDefault()
        const res = await fetch('https://.../posts')
        const posts = await res.json()
    }

    return (
    <form onSubmit={handleSubmit}>
          <input
              placeholder='Paste link here'
              className='mb-search-input'
              value={pasteInfo}
              onChange={handleChange}
            />
          <button type='submit'>
            {T('Download')}
          </button>
      </form>
    )
}

Script 标签引入

由于没有 HTML 预渲染文件,当我们有一些 诸如谷歌统计、埋点统计等脚本需要直接引入时,next 也为我们提供了 Script 组件,我们以谷歌统计为例;如下图,我们可以通过官方提供的next/script 插件快速注册 Script 标签组件,从而满足功能要求。

// 在 入口文件 _app.js 中增加以下代码

import { useEffect } from 'react'
import Script from 'next/script'
import { useRouter } from 'next/router'
import * as gtag from '../utils/gtag'

function MyApp({ Component, pageProps }) {
  const router = useRouter()

  // 当路由变化时,则我们需要更新 goole pv 信息
  useEffect(() => {
    const handleRouteChange = (url) => {
      gtag.pageView(url)
    }
    router.events.on('routeChangeComplete', handleRouteChange)
    router.events.on('hashChangeComplete', handleRouteChange)
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange)
      router.events.off('hashChangeComplete', handleRouteChange)
    }
  }, [router.events])
  
  return (
     <>
     // 脚本引入
      <Script
        strategy='afterInteractive'
        src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
      />
      <Script
        id='gtag-init'
        strategy='afterInteractive'
        dangerouslySetInnerHTML={{
          __html: `
            console.log('google analytics init')
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${gtag.GA_TRACKING_ID}', {
              page_path: window.location.pathname,
            });
          `,
        }}
      />
      <Component {...pageProps} />
    </>
    )
}

写在最后

以上是我熟悉这一块的过程中进行的整理总结,希望能帮到大家。

另外,文章中可能会有一些问题,欢迎大家指正与交流