詳解使用Next.js構(gòu)建服務(wù)端渲染應(yīng)用
next.js簡(jiǎn)介
最近在學(xué)React.js,React官方推薦使用next.js框架作為構(gòu)建服務(wù)端渲染的網(wǎng)站,所以今天來研究一下next.js的使用。
next.js作為一款輕量級(jí)的應(yīng)用框架,主要用于構(gòu)建靜態(tài)網(wǎng)站和后端渲染網(wǎng)站。
框架特點(diǎn)
- 使用后端渲染
- 自動(dòng)進(jìn)行代碼分割(code splitting),以獲得更快的網(wǎng)頁(yè)加載速度
- 簡(jiǎn)潔的前端路由實(shí)現(xiàn)
- 使用webpack進(jìn)行構(gòu)建,支持模塊熱更新(Hot Module Replacement)
- 可與主流Node服務(wù)器進(jìn)行對(duì)接(如express)
- 可自定義babel和webpack的配置
使用方法
創(chuàng)建項(xiàng)目并初始化
mkdir server-rendered-website cd server-rendered-website npm init -y
安裝next.js
使用npm或者yarn安裝,因?yàn)槭莿?chuàng)建React應(yīng)用,所以同時(shí)安裝react和react-dom
npm:
npm install --save react react-dom next
yarn:
yarn add react react-dom next
在項(xiàng)目根目錄下添加文件夾pages(一定要命名為pages,這是next的強(qiáng)制約定,不然會(huì)導(dǎo)致找不到頁(yè)面),然后在package.json文件里面添加script用于啟動(dòng)項(xiàng)目:
"scripts": {
"dev": "next"
}
如下圖

創(chuàng)建視圖
在pages文件夾下創(chuàng)建index.js文件,文件內(nèi)容:
const Index = () => ( <div> <p>Hello next.js</p> </div> ) export default Index
運(yùn)行
npm run next
在瀏覽器中打開http://localhost:3000/,網(wǎng)頁(yè)顯示如下:

這樣就完成了一個(gè)最簡(jiǎn)單的next網(wǎng)站。
前端路由
next.js前端路由的使用方式非常簡(jiǎn)單,我們先增加一個(gè)page,叫about,內(nèi)容如下:
const About = () => ( <div> <p>This is About page</p> </div> ) export default About;
當(dāng)我們?cè)跒g覽器中請(qǐng)求https://localhost:3000/about時(shí),可以看到頁(yè)面展示對(duì)應(yīng)內(nèi)容。(==這里需要注意:請(qǐng)求url的path必須和page的文件名大小寫一致才能訪問,如果訪問localhost:3000/About的話是找不到about頁(yè)面的。==)
我們可以使用傳統(tǒng)的a標(biāo)簽在頁(yè)面之間進(jìn)行跳轉(zhuǎn),但每跳轉(zhuǎn)一次,都需要去服務(wù)端請(qǐng)求一次。為了增加頁(yè)面的訪問速度,推薦使用next.js的前端路由機(jī)制進(jìn)行跳轉(zhuǎn)。
next.js使用next/link實(shí)現(xiàn)頁(yè)面之間的跳轉(zhuǎn),用法如下:
import Link from 'next/link' const Index = () => ( <div> <Link href="/about" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" > <a>About Page</a> </Link> <p>Hello next.js</p> </div> ) export default Index
這樣點(diǎn)擊index頁(yè)面的AboutPage鏈接就能跳轉(zhuǎn)到about頁(yè)面,而點(diǎn)擊瀏覽器的返回按鈕也是通過前端路由進(jìn)行跳轉(zhuǎn)的。 官方文檔說用前端路由跳轉(zhuǎn)是不會(huì)有網(wǎng)絡(luò)請(qǐng)求的,實(shí)際會(huì)有一個(gè)對(duì)about.js文件的請(qǐng)求,而這個(gè)請(qǐng)求來自于頁(yè)面內(nèi)動(dòng)態(tài)插入的script標(biāo)簽。但是about.js只會(huì)請(qǐng)求一次,之后再訪問是不會(huì)請(qǐng)求的,畢竟相同的script標(biāo)簽是不會(huì)重復(fù)插入的。 但是想比于后端路由還是大大節(jié)省了請(qǐng)求次數(shù)和網(wǎng)絡(luò)流量。前端路由和后端路由的請(qǐng)求對(duì)比如下:
前端路由:

后端路由:

Link標(biāo)簽支持任意react組件作為其子元素,不一定要用a標(biāo)簽,只要該子元素能響應(yīng)onClick事件,就像下面這樣:
<Link href="/about" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" > <div>Go about page</div> </Link>
Link標(biāo)簽不支持添加style和className等屬性,如果要給鏈接增加樣式,需要在子元素上添加:
<Link href="/about" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >
<a className="about-link" style={{color:'#ff0000'}}>Go about page</a>
</Link>
Layout
所謂的layout就是就是給不同的頁(yè)面添加相同的header,footer,navbar等通用的部分,同時(shí)又不需要寫重復(fù)的代碼。在next.js中可以通過共享某些組件實(shí)現(xiàn)layout。
我們先增加一個(gè)公共的header組件,放在根目錄的components文件夾下面(頁(yè)面級(jí)的組件放pages中,公共組件放components中):
import Link from 'next/link';
const linkStyle = {
marginRight: 15
}
const Header = () => (
<div>
<Link href="/" rel="external nofollow" rel="external nofollow" >
<a style={linkStyle}>Home</a>
</Link>
<Link href="/about" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >
<a style={linkStyle}>About</a>
</Link>
</div>
)
export default Header;
然后在index和about頁(yè)面中引入header組件,這樣就實(shí)現(xiàn)了公共的layout的header:
import Header from '../components/Header'; const Index = () => ( <div> <Header /> <p>Hello next.js</p> </div> ) export default Index;
如果要增加footer也可以按照header的方法實(shí)現(xiàn)。
除了引入多個(gè)header、footer組件,我們可以實(shí)現(xiàn)一個(gè)整體的Layout組件,避免引入多個(gè)組件的麻煩,同樣在components中添加一個(gè)Layout.js文件,內(nèi)容如下:
import Header from './Header';
const layoutStyle = {
margin: 20,
padding: 20,
border: '1px solid #DDD'
}
const Layout = (props) => (
<div style={layoutStyle}>
<Header />
{props.children}
</div>
)
export default Layout
這樣我們只需要在頁(yè)面中引入Layout組件就可以達(dá)到布局的目的:
import Layout from '../components/Layout'; const Index = () => ( <Layout> <p>Hello next.js</p> </Layout> ) export default Index;
頁(yè)面間傳值
通過url參數(shù)(query string)
next中的頁(yè)面間傳值方式和傳統(tǒng)網(wǎng)頁(yè)一樣也可以用url參數(shù)實(shí)現(xiàn),我們來做一個(gè)簡(jiǎn)單的博客應(yīng)用:
首先將index.js的內(nèi)容替換成如下來展示博客列表:
import Link from 'next/link';
import Layout from '../components/Layout';
const PostLink = (props) => (
<li>
<Link href={`/post?title=${props.title}`}>
<a>{props.title}</a>
</Link>
</li>
);
export default () => (
<Layout>
<h1>My Blog</h1>
<ul>
<PostLink title="Hello next.js" />
<PostLink title="next.js is awesome" />
<PostLink title="Deploy apps with Zeit" />
</ul>
</Layout>
);
通過在Link的href中添加title參數(shù)就可以實(shí)現(xiàn)傳值。
現(xiàn)在我們?cè)偬砑硬┛偷脑斍轫?yè)post.js:
import { withRouter } from 'next/router';
import Layout from '../components/Layout';
const Post = withRouter((props) => (
<Layout>
<h1>{props.router.query.title}</h1>
<p>This is the blog post content.</p>
</Layout>
));
export default Post;
上面代碼通過withRouter將next的router作為一個(gè)prop注入到component中,實(shí)現(xiàn)對(duì)url參數(shù)的訪問。
運(yùn)行后顯示如圖:
列表頁(yè)

點(diǎn)擊進(jìn)入詳情頁(yè):

使用query string可以實(shí)現(xiàn)頁(yè)面間的傳值,但是會(huì)導(dǎo)致頁(yè)面的url不太簡(jiǎn)潔美觀,尤其當(dāng)要傳輸?shù)闹刀嗔酥?。所以next.js提供了Route Masking這個(gè)特性用于路由的美化。
路由偽裝(Route Masking)
這項(xiàng)特性的官方名字叫Route Masking,沒有找到官方的中文名,所以就根據(jù)字面意思暫且翻譯成路由偽裝。所謂的路由偽裝即讓瀏覽器地址欄顯示的url和頁(yè)面實(shí)際訪問的url不一樣。實(shí)現(xiàn)路由偽裝的方法也很簡(jiǎn)單,通過Link組件的as屬性告訴瀏覽器href對(duì)應(yīng)顯示為什么url就可以了,index.js代碼修改如下:
import Link from 'next/link';
import Layout from '../components/Layout';
const PostLink = (props) => (
<li>
<Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
<a>{props.title}</a>
</Link>
</li>
);
export default () => (
<Layout>
<h1>My Blog</h1>
<ul>
<PostLink id="hello-nextjs" title="Hello next.js" />
<PostLink id="learn-nextjs" title="next.js is awesome" />
<PostLink id="deploy-nextjs" title="Deploy apps with Zeit" />
</ul>
</Layout>
);
運(yùn)行結(jié)果:

瀏覽器的url已經(jīng)被如期修改了,這樣看起來舒服多了。而且路由偽裝對(duì)history也很友好,點(diǎn)擊返回再前進(jìn)還是能夠正常打開詳情頁(yè)面。但是如果你刷新詳情頁(yè),確報(bào)404的錯(cuò)誤,如圖:

這是因?yàn)樗⑿马?yè)面會(huì)直接向服務(wù)器請(qǐng)求這個(gè)url,而服務(wù)端并沒有該url對(duì)應(yīng)的頁(yè)面,所以報(bào)錯(cuò)。為了解決這個(gè)問題,需要用到next.js提供的自定義服務(wù)接口(custom server API)。
自定義服務(wù)接口
自定義服務(wù)接口前我們需要?jiǎng)?chuàng)建服務(wù)器,安裝Express:
npm install --save express
在項(xiàng)目根目錄下創(chuàng)建server.js 文件,內(nèi)容如下:
const express = require('express');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare()
.then(() => {
const server = express();
server.get('*', (req, res) => {
return handle(req, res);
});
server.listen(3000, (err) => {
if (err) throw err;
console.log('> Ready on http://localhost:3000');
});
})
.catch((ex) => {
console.error(ex.stack);
process.exit(1);
});
然后將package.json里面的dev script改為:
"scripts": {
"dev": "node server.js"
}
運(yùn)行npm run dev后項(xiàng)目和之前一樣可以運(yùn)行,接下來我們需要添加路由將被偽裝過的url和真實(shí)的url匹配起來,在server.js中添加:
......
const server = express();
server.get('/p/:id', (req, res) => {
const actualPage = '/post';
const queryParams = { title: req.params.id };
app.render(req, res, actualPage, queryParams);
});
......
這樣我們就把被偽裝過的url和真實(shí)的url映射起來,并且query參數(shù)也進(jìn)行了映射。重啟項(xiàng)目之后就可以刷新詳情頁(yè)而不會(huì)報(bào)錯(cuò)了。但是有一個(gè)小問題,前端路由打開的頁(yè)面和后端路由打開的頁(yè)面title不一樣,這是因?yàn)楹蠖寺酚蓚鬟^去的是id,而前端路由頁(yè)面顯示的是title。這個(gè)問題在實(shí)際項(xiàng)目中可以避免,因?yàn)樵趯?shí)際項(xiàng)目中我們一般會(huì)通過id獲取到title,然后再展示。作為Demo我們偷個(gè)小懶,直接將id作為后端路由頁(yè)面的title。
之前我們的展示數(shù)據(jù)都是靜態(tài)的,接下來我們實(shí)現(xiàn)從遠(yuǎn)程服務(wù)獲取數(shù)據(jù)并展示。
遠(yuǎn)程數(shù)據(jù)獲取
next.js提供了一個(gè)標(biāo)準(zhǔn)的獲取遠(yuǎn)程數(shù)據(jù)的接口:getInitialProps,通過getInitialProps我們可以獲取到遠(yuǎn)程數(shù)據(jù)并賦值給頁(yè)面的props。getInitialProps即可以用在服務(wù)端也可以用在前端。接下來我們寫個(gè)小Demo展示它的用法。我們打算從TVMaze API 獲取到一些電視節(jié)目的信息并展示到我的網(wǎng)站上。首先,我們安裝isomorphic-unfetch,它是基于fetch實(shí)現(xiàn)的一個(gè)網(wǎng)絡(luò)請(qǐng)求庫(kù):
npm install --save isomorphic-unfetch
然后我們修改index.js如下:
import Link from 'next/link';
import Layout from '../components/Layout';
import fetch from 'isomorphic-unfetch';
const Index = (props) => (
<Layout>
<h1>Marvel TV Shows</h1>
<ul>
{props.shows.map(({ show }) => {
return (
<li key={show.id}>
<Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
<a>{show.name}</a>
</Link>
</li>
);
})}
</ul>
</Layout>
);
Index.getInitialProps = async function () {
const res = await fetch('https://api.tvmaze.com/search/shows?q=marvel');
const data = await res.json();
return {
shows: data
}
}
export default Index;
以上代碼的邏輯應(yīng)該很清晰了,我們?cè)?code>getInitialProps中獲取到電視節(jié)目的數(shù)據(jù)并返回,這樣在Index的props就可以獲取到節(jié)目數(shù)據(jù),再遍歷渲染成節(jié)目列表。
運(yùn)行項(xiàng)目之后,頁(yè)面完美展示:

接下來我們來實(shí)現(xiàn)詳情頁(yè),首先我們把/p/:id的路由修改為:
...
server.get('/p/:id', (req, res) => {
const actualPage = '/post';
const queryParams = { id: req.params.id };
app.render(req, res, actualPage, queryParams);
});
...
我們通過將id作為參數(shù)去獲取電視節(jié)目的詳細(xì)內(nèi)容,接下來修改post.js的內(nèi)容為:
import fetch from 'isomorphic-unfetch';
import Layout from '../components/Layout';
const Post = (props) => (
<Layout>
<h1>{props.show.name}</h1>
<p>{props.show.summary.replace(/<[/]?p>/g, '')}</p>
<img src={props.show.image.medium} />
</Layout>
);
Post.getInitialProps = async function (context) {
const { id } = context.query;
const res = await fetch(`https://api.tvmaze.com/shows/${id}`);
const show = await res.json();
return { show };
}
export default Post;
重啟項(xiàng)目(修改了server.js的內(nèi)容需要重啟),從列表頁(yè)進(jìn)入詳情頁(yè),已經(jīng)成功的獲取到電視節(jié)目的詳情并展示出來:

增加樣式
到目前為止,咱們做的網(wǎng)頁(yè)都太平淡了,所以接下來咱們給網(wǎng)站增加一些樣式,讓它變得漂亮。
對(duì)于React應(yīng)用,有多種方式可以增加樣式。主要分為兩種:
- 使用傳統(tǒng)CSS文件(包括SASS,PostCSS等)
- 在JS文件中插入CSS
使用傳統(tǒng)CSS文件在實(shí)際使用中會(huì)用到挺多的問題,所以next.js推薦使用第二種方式。next.js內(nèi)部默認(rèn)使用styled-jsx框架向js文件中插入CSS。這種方式引入的樣式在不同組件之間不會(huì)相互影響,甚至父子組件之間都不會(huì)相互影響。
styled-jsx
接下來,我們看一下如何使用styled-jsx。將index.js的內(nèi)容替換如下:
import Link from 'next/link';
import Layout from '../components/Layout';
import fetch from 'isomorphic-unfetch';
const Index = (props) => (
<Layout>
<h1>Marvel TV Shows</h1>
<ul>
{props.shows.map(({ show }) => {
return (
<li key={show.id}>
<Link as={`/p/${show.id}`} href={`/post?id=${show.id}`}>
<a className="show-link">{show.name}</a>
</Link>
</li>
);
})}
</ul>
<style jsx>
{`
*{
margin:0;
padding:0;
}
h1,a{
font-family:'Arial';
}
h1{
margin-top:20px;
background-color:#EF141F;
color:#fff;
font-size:50px;
line-height:66px;
text-transform: uppercase;
text-align:center;
}
ul{
margin-top:20px;
padding:20px;
background-color:#000;
}
li{
list-style:none;
margin:5px 0;
}
a{
text-decoration:none;
color:#B4B5B4;
font-size:24px;
}
a:hover{
opacity:0.6;
}
`}
</style>
</Layout>
);
Index.getInitialProps = async function () {
const res = await fetch('https://api.tvmaze.com/search/shows?q=marvel');
const data = await res.json();
console.log(`Show data fetched. Count: ${data.length}`);
return {
shows: data
}
}
export default Index;
運(yùn)行項(xiàng)目,首頁(yè)變成:

增加了一點(diǎn)樣式之后比之前好看了一點(diǎn)點(diǎn)。我們發(fā)現(xiàn)導(dǎo)航欄的樣式并沒有變。因?yàn)镠eader是一個(gè)獨(dú)立的的component,component之間的樣式不會(huì)相互影響。如果需要為導(dǎo)航增加樣式,需要修改Header.js:
import Link from 'next/link';
const Header = () => (
<div>
<Link href="/" rel="external nofollow" rel="external nofollow" >
<a>Home</a>
</Link>
<Link href="/about" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" rel="external nofollow" >
<a>About</a>
</Link>
<style jsx>
{`
a{
color:#EF141F;
font-size:26px;
line-height:40px;
text-decoration:none;
padding:0 10px;
text-transform:uppercase;
}
a:hover{
opacity:0.8;
}
`}
</style>
</div>
)
export default Header;
效果如下:

全局樣式
當(dāng)我們需要添加一些全局的樣式,比如rest.css或者鼠標(biāo)懸浮在a標(biāo)簽上時(shí)出現(xiàn)下劃線,這時(shí)候我們只需要在style-jsx標(biāo)簽上增加global關(guān)鍵詞就行了,我們修改Layout.js如下:
import Header from './Header';
const layoutStyle = {
margin: 20,
padding: 20,
border: '1px solid #DDD'
}
const Layout = (props) => (
<div style={layoutStyle}>
<Header />
{props.children}
<style jsx global>
{`
a:hover{
text-decoration:underline;
}
`}
</style>
</div>
)
export default Layout
這樣鼠標(biāo)懸浮在所有的a標(biāo)簽上時(shí)會(huì)出現(xiàn)下劃線。
部署next.js應(yīng)用
Build
部署之前我們首先需要能為生產(chǎn)環(huán)境build項(xiàng)目,在package.json中添加script:
"build": "next build"
接下來我們需要能啟動(dòng)項(xiàng)目來serve我們build的內(nèi)容,在package.json中添加script:
"start": "next start"
然后依次執(zhí)行:
npm run build npm run start
build完成的內(nèi)容會(huì)生成到.next文件夾內(nèi),npm run start之后,我們?cè)L問的實(shí)際上就是.next文件夾的內(nèi)容。
運(yùn)行多個(gè)實(shí)例
如果我們需要進(jìn)行橫向擴(kuò)展(Horizontal Scale)以提高網(wǎng)站的訪問速度,我們需要運(yùn)行多個(gè)網(wǎng)站的實(shí)例。首先,我們修改package.json的start script:
"start": "next start -p $PORT"
如果是windows系統(tǒng):
"start": "next start -p %PORT%"
然后運(yùn)行build: npm run build,然后打開兩個(gè)命令行并定位到項(xiàng)目根目錄,分別運(yùn)行:
PORT=8000 npm start PORT=9000 npm start
運(yùn)行完成后打開localhost:8000和localhost:9000都可以正常訪問:

通過以上方法雖然能夠打包并部署,但是有個(gè)問題,我們的自定義服務(wù)server.js并沒有運(yùn)行,導(dǎo)致在詳情頁(yè)刷新的時(shí)候依然會(huì)出現(xiàn)404的錯(cuò)誤,所以我們需要把自定義服務(wù)加入app的邏輯中。
部署并使用自定義服務(wù)
我們將start script修改為:
"start": "NODE_ENV=production node server.js"
這樣我們就解決了自定義服務(wù)的部署。重啟項(xiàng)目后刷新詳情頁(yè)也能夠正常訪問了。
到此為止,我們已經(jīng)了解了next.js的大部分使用方法,如果有疑問可以查看next.js官方文檔,也可以給我留言討論。
本文Demo源碼:Github
源碼next.js官網(wǎng):https://nextjs.org/
next.js官方教程:https://nextjs.org/learn
next.js Github:https://github.com/zeit/next.js
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
canvas實(shí)現(xiàn)圖片根據(jù)滑塊放大縮小效果
本文主要介紹了canvas實(shí)現(xiàn)圖片根據(jù)滑塊放大縮小效果的實(shí)例,具有很好的參考價(jià)值,下面跟著小編一起來看下吧2017-02-02
JS實(shí)現(xiàn)jQuery的append功能
jQuery中可以直接使用$el.append()為元素添加字符串型dom, 但是最近轉(zhuǎn)戰(zhàn)Vue, 再使用jQuery明顯不合適了, 所以通過查找資料, 封裝一個(gè)可以實(shí)現(xiàn)同樣效果的方法.2021-05-05
用html+css+js實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的圖片切換特效
這篇文章主要介紹了用html+css+js實(shí)現(xiàn)的一個(gè)簡(jiǎn)單的圖片切換特效,需要的朋友可以參考下2014-05-05
js字符串的各種格式的轉(zhuǎn)換 ToString,F(xiàn)ormat
平時(shí)我們經(jīng)常會(huì)需要將字符轉(zhuǎn)換為各種不同的格式,例如錢:0元需要轉(zhuǎn)換為0.00顯示;需要轉(zhuǎn)換為16進(jìn)制顯示的數(shù),這樣的例子有很多2011-08-08
setTimeout 函數(shù)在前端延遲搜索實(shí)現(xiàn)中的作用詳解
這篇文章主要為大家介紹了setTimeout 函數(shù)在前端延遲搜索實(shí)現(xiàn)中的作用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
使用D3.js構(gòu)建實(shí)時(shí)圖形的示例代碼
這篇文章主要介紹了使用D3.js構(gòu)建實(shí)時(shí)圖形的示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
javascript監(jiān)聽鼠標(biāo)滾輪事件淺析
這篇文章主要介紹了javascript監(jiān)聽鼠標(biāo)滾輪事件淺析,使用具體例子說明,同時(shí)考慮了不同的瀏覽器,需要的朋友可以參考下2014-06-06
JavaScript調(diào)用Activex控件的事件的實(shí)現(xiàn)方法
最近在搞一個(gè)客戶端調(diào)用activex控件的開發(fā)。一些實(shí)現(xiàn)方法小結(jié),需要的朋友可以參考下。2010-04-04
layui在form表單頁(yè)面通過Validform加入簡(jiǎn)單驗(yàn)證的方法
今天小編就為大家分享一篇layui在form表單頁(yè)面通過Validform加入簡(jiǎn)單驗(yàn)證的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-09-09

