React服務端渲染-next.js

React服務端渲染-next.js

前端項目大方向上可以分為兩種模式:前臺渲染和服務端渲染。

前臺渲染-SPA應用是一個主要陣營,如果說有什么缺點,那就是SEO不好。因為默認的HTML文檔只包含一個根節(jié)點,實質(zhì)內(nèi)容由JS渲染。并且,首屏渲染時間受JS大小和網(wǎng)絡延遲的影響較大,因此,某些強SEO的項目,或者首屏渲染要求較高的項目,會采用服務端渲染SSR。

Next.js 是一個輕量級的 React 服務端渲染應用框架。

熟悉React框架的同學,如果有服務端渲染的需求,選擇Next.js是最佳的決定。

  • 默認情況下由服務器呈現(xiàn)
  • 自動代碼拆分可加快頁面加載速度
  • 客戶端路由(基于頁面)
  • 基于 Webpack 的開發(fā)環(huán)境,支持熱模塊替換(HMR)

官方文檔
中文官網(wǎng)-帶有測試題

初始化項目

方式1:手動擼一個

mkdir next-demo //創(chuàng)建項目
cd next-demo //進入項目
npm init -y // 快速創(chuàng)建package.json而不用進行一些選擇
npm install --save react react-dom next // 安裝依賴
mkdir pages //創(chuàng)建pages,一定要做,否則后期運行會報錯

然后打開 next-demo 目錄下的 package.json 文件并用以下內(nèi)容替換 scripts 配置段:

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

運行以下命令啟動開發(fā)(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

如果想用官網(wǎng)模板,可以在 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

如果有動態(tài)路由的需求,比如http://localhost:3000/list/:id,那么,可以有兩種方式:

方式一:利用文件目錄

需要在/list目錄下添加一個動態(tài)目錄即可,如下圖:

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中初始化數(shù)據(jù)

不同于前端渲染(componentDidMount),Next.js有特定的鉤子函數(shù)初始化數(shù)據(jù),如下:

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,那么,接口獲得的初始化數(shù)據(jù)需要傳遞給ctx.req,從而在前臺初始化Redux時,才能夠?qū)⒊跏紨?shù)據(jù)帶過來!??!

特點3:_app.js和_document.js

_app.js可以認為是頁面的父組件,可以做一些統(tǒng)一布局,錯誤處理之類的事情,比如:

  • 頁面布局
  • 當路由變化時保持頁面狀態(tài)
  • 使用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>做路由跳轉(zhuǎn),那么,目標頁面一定是全渲染,執(zhí)行getInitialProps鉤子函數(shù)。
淺層路由允許改變 URL但是不執(zhí)行getInitialProps 生命周期。可以加載相同頁面的 URL,得到更新后的路由屬性pathnamequery,并不失去 state 狀態(tài)。

因為淺路由不會執(zhí)行服務端初始化數(shù)據(jù)函數(shù),所以服務端返回HTML的速度加快,但是,返回的為空內(nèi)容,不適合SEO。并且,你需要在瀏覽器鉤子函數(shù)componentDidMount 中重新調(diào)用接口獲得數(shù)據(jù)再次渲染內(nèi)容區(qū)。

淺路由模式比較適合搜索頁面,比如,每次的搜索接口都是按照keyword參數(shù)發(fā)生變化:
/search?keyword=a/search?keyword=b

使用方式如下:

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

然后可以在componentdidupdate鉤子函數(shù)中監(jiān)聽 URL 的變化。

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

注意:
淺層路由只作用于相同 URL 的參數(shù)改變,比如我們假定有個其他路由about,而你向下面代碼樣運行:
Router.push('/?counter=10', '/about?counter=10', { shallow: true })
那么這將會出現(xiàn)新頁面,即使我們加了淺層路由,但是它還是會卸載當前頁,會加載新的頁面并觸發(fā)新頁面的getInitialProps。

Next.js踩坑記錄

踩坑1:訪問window和document對象時要小心!

window和document對象只有在瀏覽器環(huán)境中才存在。所以,如果直接在render函數(shù)或者getInitialProps函數(shù)中訪問它們,會報錯。

如果需要使用這些對象,在React的生命周期函數(shù)里調(diào)用,比如componentDidMount

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

踩坑2:集成antd

集成antd主要是加載CSS樣式這塊比較坑,還好官方已經(jīng)給出解決方案,參考: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樣式。具體配置參考上面官網(wǎng)給的例子。

踩坑3:接口鑒權(quán)

SPA項目中,接口一般都是在componentDidMount中調(diào)用,然后根據(jù)數(shù)據(jù)渲染頁面。而componentDidMount是瀏覽器端可用的鉤子函數(shù)。
到了SSR項目中,componentDidMount不會被調(diào)用,這個點在踩坑1中已經(jīng)提到。
SSR中,數(shù)據(jù)是提前獲取,渲染HTML,然后將整個渲染好的HTML發(fā)送給瀏覽器,一次性渲染好。所以,當你在Next的鉤子函數(shù)getInitialProps中調(diào)用接口時,用戶信息是不可知的!不可知!

  • 如果用戶已經(jīng)登錄,getInitialProps中調(diào)用接口時,會帶上cookie信息
  • 如果用戶未登錄,自然不會攜帶cookie
  • 但是,用戶到底有沒有登錄呢???getInitialProps中,你無法通過接口(比如getSession之類的API)得知

要知道,用戶是否登錄,登錄用戶是否有權(quán)限,那必須在瀏覽器端有了用戶操作之后才會發(fā)生變化。
這時,你只能在特定頁面(如果只有某個頁面的某個接口需要鑒權(quán)),或者在_app.js這個全局組件上添加登錄態(tài)判斷:componentDidMount中調(diào)用登錄態(tài)接口,并根據(jù)當前用戶狀態(tài)做是否重定向到登錄頁的操作。

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

都可以參考官網(wǎng)給出的Demo,例子十分豐富:https://github.com/zeit/next.js/tree/7.0.0-canary.8/examples

小結(jié)

Next.js的其他用法和React一樣,比如組件封裝,高階函數(shù)等。
demo code: https://github.com/etianqq/next-app

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容