React服务端渲染-next.js

时间:2022-07-25
本文章向大家介绍React服务端渲染-next.js,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

React服务端渲染-next.js

前端项目大方向上可以分为两种模式:前台渲染和服务端渲染。

前台渲染-SPA应用是一个主要阵营,如果说有什么缺点,那就是SEO不好。因为默认的HTML文档只包含一个根节点,实质内容由JS渲染。并且,首屏渲染时间受JS大小和网络延迟的影响较大,因此,某些强SEO的项目,或者首屏渲染要求较高的项目,会采用服务端渲染SSR。

Next.js 是一个轻量级的 React 服务端渲染应用框架。

熟悉React框架的同学,如果有服务端渲染的需求,选择Next.js是最佳的决定。

  • 默认情况下由服务器呈现
  • 自动代码拆分可加快页面加载速度
  • 客户端路由(基于页面)
  • 基于 Webpack 的开发环境,支持热模块替换(HMR)

官方文档 中文官网-带有测试题

初始化项目

方式1:手动撸一个

mkdir next-demo //创建项目
cd next-demo //进入项目
npm init -y // 快速创建package.json而不用进行一些选择
npm install --save react react-dom next // 安装依赖
mkdir pages //创建pages,一定要做,否则后期运行会报错

然后打开 next-demo 目录下的 package.json 文件并用以下内容替换 scripts 配置段:

"scripts": {
  "dev": "next",
  "build": "next build",
  "start": "next start"
}

运行以下命令启动开发(dev)服务器:

npm run dev // 默认端口为3000
npm run dev -p 6688 // 可以用你喜欢的端口

服务器启动成功,但是打开localhost:3000,会报404错误。 那是因为pages目录下无文件夹,因而,无可用页面展示。

利用脚手架:create-next-app

npm init next-app
# or
yarn create next-app

如果想用官网模板,可以在 https://github.com/zeit/next.js/tree/canary/examples 里面选个中意的,比如hello-world,然后运行如下脚本:

npm init next-app --example hello-world hello-world-app
# or
yarn create next-app --example hello-world hello-world-app

下面,我们来看看Next有哪些与众不同的地方。

Next.js特点

特点1:文件即路由

在pages目录下,如果有a.js,b.js,c.js三个文件,那么,会生成三个路由:

http://localhost:3000/a
http://localhost:3000/b
http://localhost:3000/c

如果有动态路由的需求,比如http://localhost:3000/list/:id,那么,可以有两种方式:

方式一:利用文件目录

需要在/list目录下添加一个动态目录即可,如下图:

image

方式二:自定义server.js

修改启动脚本使用server.js:

"scripts": {
    "dev": "node server.js"
  },

自定义server.js:

下面这个例子使 /a 路由解析为./pages/b,以及/b 路由解析为./pages/a

// This file doesn't go through babel or webpack transformation.
// Make sure the syntax and sources this file requires are compatible with the current node version you are running
// See https://github.com/zeit/next.js/issues/1245 for discussions on Universal Webpack or universal Babel
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer((req, res) => {
    // Be sure to pass `true` as the second argument to `url.parse`.
    // This tells it to parse the query portion of the URL.
    const parsedUrl = parse(req.url, true)
    const { pathname, query } = parsedUrl

    if (pathname === '/a') {
      app.render(req, res, '/b', query)
    } else if (pathname === '/b') {
      app.render(req, res, '/a', query)
    } else {
      handle(req, res, parsedUrl)
    }
  }).listen(3000, err => {
    if (err) throw err
    console.log('> Ready on http://localhost:3000')
  })
})

特点2:getInitialProps中初始化数据

不同于前端渲染(componentDidMount),Next.js有特定的钩子函数初始化数据,如下:

import React, { Component } from 'react'
import Comp from '@components/pages/index'
import { AppModal, CommonModel } from '@models/combine'

interface IProps {
  router: any
}
class Index extends Component<IProps> {
  static async getInitialProps(ctx) {
    const { req } = ctx

    try {
      await AppModal.effects.getAppList(req)
    } catch (e) {
      CommonModel.actions.setError(e, req)
    }
  }

  public render() {
    return <Comp />
  }
}

export default Index

如果项目中用到了Redux,那么,接口获得的初始化数据需要传递给ctx.req,从而在前台初始化Redux时,才能够将初始数据带过来!!!

特点3:_app.js和_document.js

_app.js可以认为是页面的父组件,可以做一些统一布局,错误处理之类的事情,比如:

  • 页面布局
  • 当路由变化时保持页面状态
  • 使用componentDidCatch自定义处理错误
import React from 'react'
import App, { Container } from 'next/app'
import Layout from '../components/Layout'
import '../styles/index.css'

export default class MyApp extends App {

    componentDidCatch(error, errorInfo) {
        console.log('CUSTOM ERROR HANDLING', error)
        super.componentDidCatch(error, errorInfo)
    }

    render() {
        const { Component, pageProps } = this.props
        return (
            <Container>
                <Layout>
                    <Component {...pageProps} />
                </Layout>
            </Container>)
    }
}

_document.js 用于初始化服务端时添加文档标记元素,比如自定义meta标签。

import Document, {
  Head,
  Main,
  NextScript,
} from 'next/document'
import * as React from 'react'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx)
    return { ...initialProps }
  }

  props

  render() {
    return (
      <html>
        <Head>
          <meta charSet="utf-8" />
          <meta httpEquiv="x-ua-compatible" content="ie=edge, chrome=1" />
          <meta name="renderer" content="webkit|ie-comp|ie-stand" />
          <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no,viewport-fit=cover"
          />
          <meta name="keywords" content="Next.js demo" />
          <meta name="description" content={'This is a next.js demo'} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}

特点4:浅路由

如果通过<Link href={href}></Link>或者<a href={href}></a>做路由跳转,那么,目标页面一定是全渲染,执行getInitialProps钩子函数。 浅层路由允许改变 URL但是不执行getInitialProps 生命周期。可以加载相同页面的 URL,得到更新后的路由属性pathnamequery,并不失去 state 状态。

因为浅路由不会执行服务端初始化数据函数,所以服务端返回HTML的速度加快,但是,返回的为空内容,不适合SEO。并且,你需要在浏览器钩子函数componentDidMount 中重新调用接口获得数据再次渲染内容区。

浅路由模式比较适合搜索页面,比如,每次的搜索接口都是按照keyword参数发生变化: /search?keyword=a/search?keyword=b

使用方式如下:

const href = '/search?keyword=abc'
const as = href
Router.push(href, as, { shallow: true })

然后可以在componentdidupdate钩子函数中监听 URL 的变化。

componentDidUpdate(prevProps) {
  const { pathname, query } = this.props.router
  const { keyword } = router.query
  if (keyword) {
      this.setState({ value: keyword })
      ...
  }
}

注意: 浅层路由只作用于相同 URL 的参数改变,比如我们假定有个其他路由about,而你向下面代码样运行: Router.push('/?counter=10', '/about?counter=10', { shallow: true }) 那么这将会出现新页面,即使我们加了浅层路由,但是它还是会卸载当前页,会加载新的页面并触发新页面的getInitialProps

Next.js踩坑记录

踩坑1:访问window和document对象时要小心!

window和document对象只有在浏览器环境中才存在。所以,如果直接在render函数或者getInitialProps函数中访问它们,会报错。

如果需要使用这些对象,在React的生命周期函数里调用,比如componentDidMount

componentDidMount() {
    document.getElementById('body').addEventListener('scroll', function () {
      ...
    })
  }

踩坑2:集成antd

集成antd主要是加载CSS样式这块比较坑,还好官方已经给出解决方案,参考:https://github.com/zeit/next.js/tree/7.0.0-canary.8/examples/with-ant-design

多安装4个npm包:

"dependencies": {
    "@zeit/next-css": "^1.0.1",
    "antd": "^4.0.4",
    "babel-plugin-import": "^1.13.0",
    "null-loader": "^3.0.0",
  },

然后,添加next.config.js.babelrc加载antd样式。具体配置参考上面官网给的例子。

踩坑3:接口鉴权

SPA项目中,接口一般都是在componentDidMount中调用,然后根据数据渲染页面。而componentDidMount是浏览器端可用的钩子函数。 到了SSR项目中,componentDidMount不会被调用,这个点在踩坑1中已经提到。 SSR中,数据是提前获取,渲染HTML,然后将整个渲染好的HTML发送给浏览器,一次性渲染好。所以,当你在Next的钩子函数getInitialProps中调用接口时,用户信息是不可知的!不可知!

  • 如果用户已经登录,getInitialProps中调用接口时,会带上cookie信息
  • 如果用户未登录,自然不会携带cookie
  • 但是,用户到底有没有登录呢???getInitialProps中,你无法通过接口(比如getSession之类的API)得知

要知道,用户是否登录,登录用户是否有权限,那必须在浏览器端有了用户操作之后才会发生变化。 这时,你只能在特定页面(如果只有某个页面的某个接口需要鉴权),或者在_app.js这个全局组件上添加登录态判断:componentDidMount中调用登录态接口,并根据当前用户状态做是否重定向到登录页的操作。

踩坑4:集成 typescript, sass, less 等等

都可以参考官网给出的Demo,例子十分丰富:https://github.com/zeit/next.js/tree/7.0.0-canary.8/examples

小结

Next.js的其他用法和React一样,比如组件封装,高阶函数等。 demo code: https://github.com/etianqq/next-app