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)
初始化項目
方式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)目錄即可,如下圖:

方式二:自定義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,得到更新后的路由屬性pathname和query,并不失去 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