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)
初始化項(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)目錄即可米苹,如下圖:
方式二:自定義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温治,得到更新后的路由屬性pathname
和query
,并不失去 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