前情概要
我们都知道,官网编写使用 React 客户端渲染会有冲突,因为 是单页面应用,且 DOM 因为是JS渲染,会不利于项目的 SEO (搜索引擎优化)。而且由于 React 运行时初次渲染会首先出现一个白屏,这样对用户体验来说是破坏性的。
但是对于习惯了JSX编写习惯的我们,很难再回到 Native JS 的时代,我的诉求有以下几点:
- 最好以 React 技术栈为基础
- 生成的代码利于 SEO
- 页面快速加载,提升用户体验
- 现代化的代码编写方式(即:高编码效率、高编码体验、高编码质量)
正好最近一个朋友有一个官网的需求,我就顺手帮忙做了。记得自己上次写官网还是用的 HTML + JQ,那是很久以前的记忆了。这次接到后,马上有(手)条(忙)不(脚)紊(乱)的开始查阅资料,经过一顿Search 操作后,有了解到当前比较热门的一些利好SEO的开发框架,最后也是选择了 Next.SSG。用起来发现很顺手,非常好用,相比以前的开发体验有了质的飞跃;同时也在开发过程中踩过一些坑,在此整理出来给大家排雷,希望能对大家有帮助。
流行 SEO 方案对比
| 方案 | 优势 | 劣势 |
|---|---|---|
| prerender 预渲染 | 部署方便,开发成本低; | 1. 无法render动态改变的页面(如:某商品详情页) ;2. 页面太多时造成存储负担; |
| Next 服务端渲染 | 一步到位,开发自主控制页面渲染;可以结合next框架,实现快速开发; | 1. 对于已在线上运营的spa项目改造成本太大; |
| seo-mask | 1. 无需改动源代码;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 方案,它相比其他两者具有以下三个优势:
- 可以打包生成静态页面,方便前后端分离;
- 可以结合 Next 框架,实现快速开发;
- 不依赖 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文件夹下创建 demo1,demo2 文件夹即可,然后 在 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} />
</>
)
}
写在最后
以上是我熟悉这一块的过程中进行的整理总结,希望能帮到大家。
另外,文章中可能会有一些问题,欢迎大家指正与交流