最簡(jiǎn)單的服務(wù)端渲染框架-Next.js
快速入門
Next.js是一個(gè)用于React應(yīng)用的極簡(jiǎn)的服務(wù)端渲染框架旁趟∽咐矗框架中集成了Webpack皮假,Babel等一系列React相關(guān)的工具并進(jìn)行了默認(rèn)的配置转锈。因此省去了復(fù)雜的配置過程,實(shí)現(xiàn)了一鍵搭建開發(fā)環(huán)境和打包構(gòu)建尤误。同時(shí)提供了自定義配置接口侠畔,可以在默認(rèn)配置的基礎(chǔ)上對(duì)工具進(jìn)行自定義配置,滿足個(gè)性化需求损晤。
安裝
使用npm
安裝: npm install next --save
為了方便的使用next
提供的命令软棺,把命令寫在package.json
文件的scripts
中:
{
"scripts": {
"dev": "next", // 運(yùn)行開發(fā)服務(wù)器,并監(jiān)控源代碼尤勋,具備hod reload功能
"build": "next build", // 以生產(chǎn)模式打包代碼
"start": "next start" // 啟動(dòng)Next服務(wù)器喘落,可以自定義服務(wù)器和端口
"init": "next init" // 初始化項(xiàng)目,創(chuàng)建基礎(chǔ)的文件夾和index頁面文件
}
}
之后斥黑,在項(xiàng)目的根目錄下創(chuàng)建pages
文件夾和static
文件夾揖盘,分別用來放對(duì)應(yīng)的頁面資源和靜態(tài)資源。
Note:也可以使用npm run init命令自動(dòng)生成锌奴。
運(yùn)行
如果使用npm run init
命令的話兽狭,現(xiàn)在pages
文件夾下已經(jīng)有了index.js
文件,如果是手動(dòng)創(chuàng)建pages
文件夾的話鹿蜀,現(xiàn)在在該文件下創(chuàng)建一個(gè)index.js
文件箕慧,內(nèi)容為:
export default () => <p>Hello, world</p>
接著執(zhí)行npm run dev命令并在瀏覽器中打開http://localhost:3000。
現(xiàn)在茴恰,就得到了一個(gè)采用服務(wù)端渲染的極簡(jiǎn)React應(yīng)用颠焦,這個(gè)應(yīng)用還實(shí)現(xiàn)了自動(dòng)代碼分割,保證每個(gè)頁面只會(huì)加載自身的依賴往枣,不會(huì)有依賴冗余伐庭。
Next的核心就是pages和static文件夾。其中pages文件夾用于存放每個(gè)頁面的頂層組件分冈,static用于存放項(xiàng)目中的靜態(tài)資源圾另。
Next
會(huì)將pages
中的文件結(jié)構(gòu)自動(dòng)映射為對(duì)應(yīng)的路由結(jié)構(gòu),例如現(xiàn)在該文件夾下有兩個(gè)文件:pages/index.js和pages/about.js
雕沉。則對(duì)應(yīng)的路由分別為/
和/about
集乔。并且支持多級(jí)目錄,例如page/foo/bar.js
對(duì)應(yīng)的路由為/foo/bar
坡椒。
static
文件夾用來存放靜態(tài)文件扰路,例如現(xiàn)在有一個(gè)圖片文件static/image.png
,使用的時(shí)候引用/static/image.png
就可以了:
export default () => (
<img src="/static/mage.png" />
)
打包完成后倔叼,Next會(huì)在項(xiàng)目根目錄生成一個(gè).next
文件夾汗唱,其中的兩個(gè)文件夾dist
和bundles
,dist
文件夾中存放著編譯后的源代碼丈攒,用于服務(wù)端渲染渡嚣。bunldes
文件夾中存放著pages
中每個(gè)頁面打包后的整體代碼的JSON格式。在應(yīng)用的初始頁面,會(huì)使用dist
文件夾中的代碼進(jìn)行服務(wù)端渲染识椰,而其他使用路由到達(dá)的頁面绝葡,則將bundles
文件夾中的對(duì)應(yīng)JSON格式的代碼返回客戶端執(zhí)行渲染。
Next的出現(xiàn)大大簡(jiǎn)化了React應(yīng)用開發(fā)的配置和構(gòu)建工作腹鹉,使開發(fā)者能夠?qū)W⒂诮M件的開發(fā)藏畅,而不需要在Webpack,Babel等工具上花費(fèi)過多的精力功咒∮溲郑基于簡(jiǎn)單的文件系統(tǒng),就可以創(chuàng)建包含路由功能和服務(wù)端渲染的React應(yīng)用力奋。需要注意的是:創(chuàng)建的應(yīng)用中只有初始頁面采用服務(wù)端渲染榜旦,其他通過路由操作到達(dá)的頁面均為客戶福渲染。
組件
Next對(duì)React組件的getInitialProps
生命周期方法做了改造景殷,傳入一個(gè)上下文對(duì)象溅呢,該對(duì)象在服務(wù)端渲染和客戶端渲染時(shí),具有不同的屬性:
- req: HTTP請(qǐng)求對(duì)象(服務(wù)端渲染獨(dú)有)
- res: HTTP響應(yīng)對(duì)象(服務(wù)端渲染獨(dú)有)
- pathname: URL中的路徑部分
- query:URL中的查詢字符串部分解析出的對(duì)象
- err:錯(cuò)誤對(duì)象猿挚,如果在渲染時(shí)發(fā)生了錯(cuò)誤
- xhr:XMLHttpRequest對(duì)象(客戶端渲染獨(dú)有)
因此咐旧,可以在組件的getInitialProps
方法中處理上下文對(duì)象,控制傳入組件的props數(shù)據(jù)绩蜻。例如:
import React from 'react'
export default class extends React.Component {
static async getInitialProps ({ req }) {
return req
? { userAgent: req.headers['user-agent'] }
: { userAgent: navigator.userAgent }
}
render () {
return <div>
Hello World {this.props.userAgent}
</div>
}
}
上面的例子根據(jù)是否有req
對(duì)象來判斷是服務(wù)端渲染還是客戶端渲染铣墨,然后采用對(duì)應(yīng)的方式取得用戶代碼數(shù)據(jù)并傳入組件的props
中。
獲取數(shù)據(jù)
組件的getInitialProps
還可以用來獲取數(shù)據(jù):
import React, { Component } from 'react';
import 'isomorphic-fetch';
export default class extends Component {
static async getInitialProps() {
const res = await fetch('https://api.github.com/repos/zeit/next.js');
const json = await res.json();
return {
stars: json.stargazers_count
};
}
render() {
return <div>{this.props.stars}</div>
}
}
需要注意的一點(diǎn)是办绝,getInitialProps
方法執(zhí)行完畢之后伊约,才會(huì)執(zhí)行組件的render
方法。這也就導(dǎo)致了如果網(wǎng)絡(luò)狀況不佳的情況下孕蝉,會(huì)出現(xiàn)長(zhǎng)時(shí)間的等待碱妆。并且只有每個(gè)頁面的頂層組件的getInitialProps
會(huì)被執(zhí)行,所以想在子組件中獲取數(shù)據(jù)的話只能在其他生命周期函數(shù)例如componentDidMount
配合組件的state
實(shí)現(xiàn):
export default class extends Component {
constructor(props) {
super(props);
this.state = {
stars: 0
}
}
async componentDidMount() {
const res = await fetch('https://api.github.com/repos/zeit/next.js');
const json = await res.json();
this.setState({
stars: json.stargazers_count
});
}
render() {
return <div>{this.state.stars}</div>
}
}
CSS
NEXT組件中聲明CSS昔驱,目前主要有兩種方式:
- 1?內(nèi)嵌CSS
- 2?CSS-in-JS
內(nèi)嵌(Built-in)CSS
Next采用的內(nèi)嵌CSS方案是styled-jsx
庫,也是Next所推薦的CSS聲明方式上忍。優(yōu)點(diǎn)是具有組件級(jí)的獨(dú)立作用域骤肛,避免了樣式污染問題。并且支持完整的CSS功能窍蓝,如:hover等腋颠。
import React from 'react'
export default () => (
<div>
Hello world
<p>scoped!</p>
<style jsx>{`
p {
color: blue;
}
div {
background: red;
}
div:hover {
background: blue;
}
@media (max-width: 600px) {
div {
background: blue;
}
}
`}</style>
</div>
)
CSS-in-JS
Next支持多種CSS-in-JS方案,例如基本的在組件style屬性中寫樣式:
import React from 'react'
export default () => (
<div style={{color: red}}>
Hello world
</div>
)
還有其他的CSS-in-JS庫吓笙,可以根據(jù)自己的需要和喜好靈活選擇淑玫。
路由系統(tǒng)
Link組件
Next中提供了一個(gè)組件,用來實(shí)現(xiàn)路由功能。例如絮蒿,我們的應(yīng)用有兩個(gè)頁面:pages/index.js
和pages/about.js
尊搬,想要實(shí)現(xiàn)頁面跳轉(zhuǎn),只需要:
<Link>組件的工作流程和瀏覽器很相似:
- 1?獲取新的組件
- 2?如果新組件定義了
getInitialProps
土涝,則獲取數(shù)據(jù)佛寿,如果發(fā)生錯(cuò)誤,則渲染_error.js
- 3?步驟1但壮,2完成之后冀泻,執(zhí)行
pushState
并渲染新組件
每個(gè)頂層組件中還會(huì)傳入一個(gè)url對(duì)象,提供了幾個(gè)路由相關(guān)的方法:
- pathname:String-當(dāng)前URL不包括查詢字符串的path部分
- query:Object-當(dāng)前URL中查詢字符串解析成的對(duì)象
- back-后退
- push(url, as=url)-使用傳入的url(字符串)執(zhí)行pushState操作
- replace(url, as=url)-使用傳入的url(字符串)執(zhí)行replaceState操作 注意:push和replace方法中的第二個(gè)參數(shù)as為可選項(xiàng)蜡饵,只有在服務(wù)端配置了自定義路由才有作用弹渔。
Router對(duì)象
除了使用<Link>組件之外,Next還提供了一個(gè)Router對(duì)象滿足命令式寫法的需要:
import Router from 'next/router'
export default () => (
<div>Click <span onClick={() => Router.push('/about')}>here</span> to read more</div>
)
與url對(duì)象相比溯祸,Router對(duì)象多了一個(gè)route屬性肢专,值為當(dāng)前的路由。 需要注意的是您没,Router對(duì)象中的屬性和方法僅可以在客戶端部分使用鸟召,服務(wù)端渲染的頁面無法使用,否則會(huì)報(bào)錯(cuò)氨鹏。
路由事件
Router對(duì)象還提供了三個(gè)路由事件方法:
- routeChangeStart(url) - 路由變化開始時(shí)觸發(fā)
- routeChangeComplete(url) - 路由變化完成時(shí)觸發(fā)
- routeChangeError(err, url) - 路由變化發(fā)生錯(cuò)誤時(shí)觸發(fā) 如果使用Router.push(url, as)或相似的方法并傳入了as參數(shù)欧募,則路由事件方法中的url參數(shù)值為as的值,否則仆抵,url參數(shù)的值是路由舔磚目標(biāo)的URL
注意:與Router對(duì)象中其他的屬性和方法不同的是跟继,這三個(gè)路由事件方法可以在服務(wù)端渲染的頁面使用。
監(jiān)聽路由變化:
Router.onRouteChangeStart = (url) => {
console.log('App is changing to: ', url)
}
取消監(jiān)聽:
Router.onRouteChangeStart = null;
如果路由加載取消了(連續(xù)快速點(diǎn)擊兩個(gè)鏈接)镣丑,就會(huì)觸發(fā)routeChangeError的回調(diào)舔糖,傳入的err參數(shù)中將包含一個(gè)cancelled屬性,值為true莺匠。
Router.onRouteChangeError = (err, url) => {
if (err.cancelled) {
console.log(`Route to ${url} was cancelled!`)
}
}
預(yù)獲取頁面
Next提供了一個(gè)基于ServiceWorker
實(shí)現(xiàn)的金吗,具有預(yù)獲取頁面功能的模塊:next/prefetch
。 使用預(yù)獲取功能趣竣,可以使APP預(yù)加載那些可能到達(dá)的頁面摇庙,提升網(wǎng)站的使用體驗(yàn)和性能。當(dāng)然遥缕,前提是你的瀏覽器必須支持ServiceWorker
卫袒。并且預(yù)獲取功能只支持應(yīng)用內(nèi)的頁面,不支持外部鏈接单匣。
<Link>組件
next/prefetch
模塊也提供了一個(gè)具有預(yù)獲取功能的<Link>組件夕凝,代替路由系統(tǒng)中的<Link>組件宝穗,使用方法一致:
import Link from 'next/prefetch'
export default () => (
<nav>
<ul>
<li><Link href='/'><a>Home</a></Link></li>
<li><Link href='/about'><a>About</a></Link></li>
<li><Link href='/contact'><a>Contact</a></Link></li>
</ul>
</nav>
)
此外預(yù)獲取功能可以精確控制到每個(gè)<Link>標(biāo)簽,使用prefetch屬性來控制開關(guān):
<Link href='/contact' prefetch={false}><a>Home</a></Link>
prefetch方法
和路由器一樣码秉,預(yù)獲取模塊也提供了一個(gè)prefetch方法逮矛,用來方便命令式的寫法:
import { prefetch } from 'next/prefetch'
export default ({ url }) => (
<div>
<a onClick={ () => setTimeout(() => url.pushTo('/dynamic'), 100) }>
100ms后執(zhí)行路由跳轉(zhuǎn)
</a>
{
預(yù)獲取頁面
prefetch('/dynamic')
}
</div>
)
自定義配置
如果默認(rèn)的配置無法滿足需要的話,Next還提供了諸多的自定義配置接口泡徙,可以根據(jù)自己的需求靈活配置橱鹏。
自定義服務(wù)器和路由
默認(rèn)的服務(wù)器和路由系統(tǒng)可能無法滿足需要,比如堪藐,我需要把/a的路由解析到pages/b.js莉兰,把/b的路由解析到pages/a.js,此時(shí)礁竞,就需要通過自定義糖荒,手動(dòng)控制頁面渲染來實(shí)現(xiàn),在項(xiàng)目根目錄下創(chuàng)建server.js文件:
// server.js
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) => {
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')
})
})
你可以選擇自己喜歡的服務(wù)端框架模捂,express或者koa等捶朵,進(jìn)行自定義。
自定義head
Next提供了<HEAD>組件狂男,可以自定義頁面<head>標(biāo)簽中的內(nèi)容综看。每個(gè)組件都可以在內(nèi)部自定義<head>的內(nèi)容:
import Head from 'next/head'
export default () => (
<div>
<Head>
<title>My page title</title>
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
</Head>
<p>Hello world!</p>
</div>
)
每個(gè)頁面組件只需要定義本頁面需要的<head>內(nèi)容,并且對(duì)于相同的標(biāo)簽岖食,例如<title>
红碑。會(huì)按照組件渲染的順序,后定義的覆蓋先定義的內(nèi)容泡垃。
自定義Document
在前面的例子中析珊,服務(wù)端渲染時(shí),所有的頁面我們只需要寫內(nèi)容組件蔑穴,這是因?yàn)槭褂昧四J(rèn)的<Document>
模板忠寻。當(dāng)然,可以自定義自己的服務(wù)端渲染模板存和。首先奕剃,創(chuàng)建pages/_document.js
文件,寫上內(nèi)容:
// pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
static async getInitialProps (ctx) {
const props = await Document.getInitialProps(ctx)
return { ...props, customValue: 'hi there!' }
}
render () {
return (
<html>
<Head>
<style>{`body { margin: 0 } /* custom! */`}</style>
</Head>
<body className="custom_class">
{this.props.customValue}
<Main />
<NextScript />
</body>
</html>
)
}
}
其中的ctx對(duì)象與其他組件中的getInitialProps
方法中收到的參數(shù)一樣捐腿,只不過多了一個(gè)額外的方法:renderPage()
纵朋。
自定義錯(cuò)誤處理
Next中,有一個(gè)默認(rèn)組件error.js叙量,負(fù)責(zé)處理404或者500這種錯(cuò)誤。當(dāng)然九串,你也可以自定義一個(gè)_error.js組件覆蓋默認(rèn)的錯(cuò)誤處理組件:
// _error.js
import React from 'react'
export default class Error extends React.Component {
static getInitialProps ({ res, xhr }) {
const statusCode = res ? res.statusCode : (xhr ? xhr.status : null)
return { statusCode }
}
render () {
return (
<p>{
this.props.statusCode
? `An error ${this.props.statusCode} occurred on server`
: 'An error occurred on client'
}</p>
)
}
}
自定義配置
相對(duì)Next進(jìn)行自定義配置的話绞佩,可以在項(xiàng)目根目錄下創(chuàng)建一個(gè)next.config.js
// next.config.js
module.exports = {
/* 自定義配置 */
}
自定義Webpack配置
在創(chuàng)建好的next.config.js文件中寺鸥,可以擴(kuò)展Webpack配置:
module.exports = {
webpack: (config, { dev }) => {
// 修改config對(duì)象
return config
}
}
該函數(shù)接收默認(rèn)的Webpack config對(duì)象作為參數(shù),返回修改后的config對(duì)象品山。需要注意的是胆建,next.config.js文件會(huì)被直接執(zhí)行,因?yàn)橹荒苁褂帽緳C(jī)安裝的Node.js所支持的JS語法肘交。
警告:不建議在自定義Webpack配置中添加loader以支持新的文件類型笆载!因?yàn)橹挥锌蛻舳虽秩镜拇a會(huì)經(jīng)過打包,而服務(wù)端執(zhí)行的是源代碼涯呻,并沒有經(jīng)過Webpack處理凉驻,因此新的loader對(duì)服務(wù)端渲染不起作用。所以最好是使用Babel插件來處理新的文件類型复罐,因?yàn)闊o論是客戶端還是服務(wù)端渲染的代碼涝登,都會(huì)經(jīng)過Babel處理。
自定義Babel配置
自定義Babel配置效诅,只需要在項(xiàng)目根目錄下創(chuàng)建.babelrc
文件胀滚,因?yàn)樽远x配置會(huì)覆蓋默認(rèn)配置,而不是擴(kuò)展默認(rèn)配置乱投。因此需要把next preset
寫到.babelrc
中咽笼。例如:
{
"presets": [
"next/babel", // Next默認(rèn)配置
"stage-0"
],
}
部署
生產(chǎn)模式下,需要先使用生產(chǎn)模式構(gòu)建代碼戚炫,再啟動(dòng)服務(wù)器剑刑。因此,需要兩條命令:
next build
next start
Next官方推薦使用now作為部署工具嘹悼,只要在package.json
文件中寫入:
{
"name": "my-app",
"dependencies": {
"next": "latest"
},
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
接著運(yùn)行now命令叛甫,就可以實(shí)現(xiàn)一鍵部署。