React服務(wù)端渲染-next.js

React服務(wù)端渲染-next.js

前端項(xiàng)目大方向上可以分為兩種模式:前臺(tái)渲染和服務(wù)端渲染鳖轰。

前臺(tái)渲染-SPA應(yīng)用是一個(gè)主要陣營寝受,如果說有什么缺點(diǎn)寒匙,那就是SEO不好蝇闭。因?yàn)槟J(rèn)的HTML文檔只包含一個(gè)根節(jié)點(diǎn)呻率,實(shí)質(zhì)內(nèi)容由JS渲染。并且呻引,首屏渲染時(shí)間受JS大小和網(wǎng)絡(luò)延遲的影響較大礼仗,因此,某些強(qiáng)SEO的項(xiàng)目逻悠,或者首屏渲染要求較高的項(xiàng)目元践,會(huì)采用服務(wù)端渲染SSR。

Next.js 是一個(gè)輕量級(jí)的 React 服務(wù)端渲染應(yīng)用框架蹂风。

熟悉React框架的同學(xué)卢厂,如果有服務(wù)端渲染的需求,選擇Next.js是最佳的決定惠啄。

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

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

初始化項(xiàng)目

方式1:手動(dòng)擼一個(gè)

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

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

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

運(yùn)行以下命令啟動(dòng)開發(fā)(dev)服務(wù)器:

npm run dev // 默認(rèn)端口為3000
npm run dev -p 6688 // 可以用你喜歡的端口

服務(wù)器啟動(dòng)成功任内,但是打開localhost:3000撵渡,會(huì)報(bào)404錯(cuò)誤。
那是因?yàn)閜ages目錄下無文件夾死嗦,因而趋距,無可用頁面展示。

利用腳手架:create-next-app

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

如果想用官網(wǎng)模板越除,可以在 https://github.com/zeit/next.js/tree/canary/examples 里面選個(gè)中意的节腐,比如hello-world外盯,然后運(yùn)行如下腳本:

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

下面,我們來看看Next有哪些與眾不同的地方翼雀。

Next.js特點(diǎn)

特點(diǎn)1:文件即路由

在pages目錄下饱苟,如果有a.js,b.js狼渊,c.js三個(gè)文件箱熬,那么,會(huì)生成三個(gè)路由:

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

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

方式一:利用文件目錄

需要在/list目錄下添加一個(gè)動(dòng)態(tài)目錄即可米苹,如下圖:

image

方式二:自定義server.js

修改啟動(dòng)腳本使用server.js:

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

自定義server.js:

下面這個(gè)例子使 /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')
  })
})

特點(diǎn)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

如果項(xiàng)目中用到了Redux良瞧,那么,接口獲得的初始化數(shù)據(jù)需要傳遞給ctx.req亏较,從而在前臺(tái)初始化Redux時(shí)莺褒,才能夠?qū)⒊跏紨?shù)據(jù)帶過來!Q┣椤遵岩!

特點(diǎn)3:_app.js和_document.js

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

  • 頁面布局
  • 當(dāng)路由變化時(shí)保持頁面狀態(tài)
  • 使用componentDidCatch自定義處理錯(cuò)誤
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 用于初始化服務(wù)端時(shí)添加文檔標(biāo)記元素,比如自定義meta標(biāo)簽宴凉。

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>
    )
  }
}

特點(diǎn)4:淺路由

如果通過<Link href={href}></Link>或者<a href={href}></a>做路由跳轉(zhuǎn)誊锭,那么,目標(biāo)頁面一定是全渲染弥锄,執(zhí)行getInitialProps鉤子函數(shù)丧靡。
淺層路由允許改變 URL但是不執(zhí)行getInitialProps 生命周期∽严荆可以加載相同頁面的 URL温治,得到更新后的路由屬性pathnamequery,并不失去 state 狀態(tài)戒悠。

因?yàn)闇\路由不會(huì)執(zhí)行服務(wù)端初始化數(shù)據(jù)函數(shù)熬荆,所以服務(wù)端返回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ù)改變拆座,比如我們假定有個(gè)其他路由about,而你向下面代碼樣運(yùn)行:
Router.push('/?counter=10', '/about?counter=10', { shallow: true })
那么這將會(huì)出現(xiàn)新頁面冠息,即使我們加了淺層路由挪凑,但是它還是會(huì)卸載當(dāng)前頁,會(huì)加載新的頁面并觸發(fā)新頁面的getInitialProps逛艰。

Next.js踩坑記錄

踩坑1:訪問window和document對(duì)象時(shí)要小心躏碳!

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

如果需要使用這些對(duì)象镇眷,在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個(gè)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項(xiàng)目中翅雏,接口一般都是在componentDidMount中調(diào)用,然后根據(jù)數(shù)據(jù)渲染頁面人芽。而componentDidMount是瀏覽器端可用的鉤子函數(shù)望几。
到了SSR項(xiàng)目中,componentDidMount不會(huì)被調(diào)用萤厅,這個(gè)點(diǎn)在踩坑1中已經(jīng)提到橄抹。
SSR中,數(shù)據(jù)是提前獲取惕味,渲染HTML害碾,然后將整個(gè)渲染好的HTML發(fā)送給瀏覽器,一次性渲染好赦拘。所以,當(dāng)你在Next的鉤子函數(shù)getInitialProps中調(diào)用接口時(shí)芬沉,用戶信息是不可知的躺同!不可知阁猜!

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

要知道畏邢,用戶是否登錄,登錄用戶是否有權(quán)限检吆,那必須在瀏覽器端有了用戶操作之后才會(huì)發(fā)生變化舒萎。
這時(shí),你只能在特定頁面(如果只有某個(gè)頁面的某個(gè)接口需要鑒權(quán))蹭沛,或者在_app.js這個(gè)全局組件上添加登錄態(tài)判斷:componentDidMount中調(diào)用登錄態(tài)接口臂寝,并根據(jù)當(dāng)前用戶狀態(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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掏缎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子萝挤,更是在濱河造成了極大的恐慌御毅,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件怜珍,死亡現(xiàn)場離奇詭異端蛆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)酥泛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門今豆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人柔袁,你說我怎么就攤上這事呆躲。” “怎么了捶索?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵插掂,是天一觀的道長。 經(jīng)常有香客問我,道長辅甥,這世上最難降的妖魔是什么酝润? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮璃弄,結(jié)果婚禮上要销,老公的妹妹穿的比我還像新娘。我一直安慰自己夏块,他們只是感情好疏咐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著脐供,像睡著了一般浑塞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上患民,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天缩举,我揣著相機(jī)與錄音,去河邊找鬼匹颤。 笑死仅孩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的印蓖。 我是一名探鬼主播辽慕,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼赦肃!你這毒婦竟也來了溅蛉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤他宛,失蹤者是張志新(化名)和其女友劉穎船侧,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厅各,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镜撩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了队塘。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袁梗。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖憔古,靈堂內(nèi)的尸體忽然破棺而出遮怜,到底是詐尸還是另有隱情,我是刑警寧澤鸿市,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布锯梁,位于F島的核電站即碗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏涝桅。R本人自食惡果不足惜拜姿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望冯遂。 院中可真熱鬧,春花似錦谒获、人聲如沸蛤肌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽裸准。三九已至,卻和暖如春赔硫,著一層夾襖步出監(jiān)牢的瞬間炒俱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工爪膊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留权悟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓推盛,卻偏偏與公主長得像峦阁,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子耘成,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容