美文网首页让前端飞Web前端之路
React服务端渲染-next.js

React服务端渲染-next.js

作者: 娜姐聊前端 | 来源:发表于2020-03-29 18:10 被阅读0次

    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

    相关文章

      网友评论

        本文标题:React服务端渲染-next.js

        本文链接:https://www.haomeiwen.com/subject/mwmuuhtx.html