next.js簡介
最近在學React.js,React官方推薦使用next.js框架作爲構建服務端渲染的網站,所以今天來寫一些Demo( Github地址)研究一下next.js的使用。
next.js作爲一款輕量級的應用框架,主要用於構建靜態網站和後端渲染網站。
框架特點
- 使用後端渲染
- 自動進行代碼分割(code splitting),以獲得更快的網頁加載速度
- 簡潔的前端路由實現
- 使用webpack進行構建,支持模塊熱更新(Hot Module Replacement)
- 可與主流Node服務器進行對接(如express)
- 可自定義babel和webpack的配置
使用方法
創建項目並初始化
mkdir server-rendered-website
cd server-rendered-website
npm init -y
安裝next.js
使用npm或者yarn安裝,因爲是創建React應用,所以同時安裝react和react-dom
npm:
npm install --save react react-dom next
yarn:
yarn add react react-dom next
在項目根目錄下添加文件夾pages(一定要命名爲pages,這是next的強制約定,不然會導致找不到頁面),然後在package.json文件裏面添加script用於啓動項目:
"scripts": {
"dev": "next"
}
如下圖
創建視圖
在pages文件夾下創建index.js文件,文件內容:
const Index = () => (
<div>
<p>Hello next.js</p>
</div>
)
export default Index
運行
npm run next
在瀏覽器中打開http://localhost:3000/,網頁顯示如下:
這樣就完成了一個最簡單的next網站。
前端路由
next.js前端路由的使用方式非常簡單,我們先增加一個page,叫about,內容如下:
const About = () => (
<div>
<p>This is About page</p>
</div>
)
export default About;
當我們在瀏覽器中請求https://localhost:3000/about時,可以看到頁面展示對應內容。(==這裏需要注意:請求url的path必須和page的文件名大小寫一致才能訪問,如果訪問localhost:3000/About的話是找不到about頁面的。==)
我們可以使用傳統的a標籤在頁面之間進行跳轉,但每跳轉一次,都需要去服務端請求一次。爲了增加頁面的訪問速度,推薦使用next.js的前端路由機制進行跳轉。
next.js使用next/link實現頁面之間的跳轉,用法如下:
import Link from 'next/link'
const Index = () => (
<div>
<Link href="/about">
<a>About Page</a>
</Link>
<p>Hello next.js</p>
</div>
)
export default Index
這樣點擊index頁面的AboutPage鏈接就能跳轉到about頁面,而點擊瀏覽器的返回按鈕也是通過前端路由進行跳轉的。 官方文檔說用前端路由跳轉是不會有網絡請求的,實際會有一個對about.js文件的請求,而這個請求來自於頁面內動態插入的script標籤。但是about.js只會請求一次,之後再訪問是不會請求的,畢竟相同的script標籤是不會重複插入的。 但是想比於後端路由還是大大節省了請求次數和網絡流量。前端路由和後端路由的請求對比如下:
前端路由:
後端路由:
Link標籤支持任意react組件作爲其子元素,不一定要用a標籤,只要該子元素能響應onClick事件,就像下面這樣:
<Link href="/about">
<div>Go about page</div>
</Link>
Link標籤不支持添加style和className等屬性,如果要給鏈接增加樣式,需要在子元素上添加:
<Link href="/about">
<a className="about-link" style={{color:'#ff0000'}}>Go about page</a>
</Link>
Layout
所謂的layout就是就是給不同的頁面添加相同的header,footer,navbar等通用的部分,同時又不需要寫重複的代碼。在next.js中可以通過共享某些組件實現layout。
我們先增加一個公共的header組件,放在根目錄的components文件夾下面(頁面級的組件放pages中,公共組件放components中):
import Link from 'next/link';
const linkStyle = {
marginRight: 15
}
const Header = () => (
<div>
<Link href="/">
<a style={linkStyle}>Home</a>
</Link>
<Link href="/about">
<a style={linkStyle}>About</a>
</Link>
</div>
)
export default Header;
然後在index和about頁面中引入header組件,這樣就實現了公共的layout的header:
import Header from '../components/Header';
const Index = () => (
<div>
<Header />
<p>Hello next.js</p>
</div>
)
export default Index;
如果要增加footer也可以按照header的方法實現。
除了引入多個header、footer組件,我們可以實現一個整體的Layout組件,避免引入多個組件的麻煩,同樣在components中添加一個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}
</div>
)
export default Layout
這樣我們只需要在頁面中引入Layout組件就可以達到佈局的目的:
import Layout from '../components/Layout';
const Index = () => (
<Layout>
<p>Hello next.js</p>
</Layout>
)
export default Index;
頁面間傳值
通過url參數(query string)
next中的頁面間傳值方式和傳統網頁一樣也可以用url參數實現,我們來做一個簡單的博客應用:
首先將index.js的內容替換成如下來展示博客列表:
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
參數就可以實現傳值。
現在我們再添加博客的詳情頁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作爲一個prop注入到component中,實現對url參數的訪問。
運行後顯示如圖:
列表頁
點擊進入詳情頁:
使用query string可以實現頁面間的傳值,但是會導致頁面的url不太簡潔美觀,尤其當要傳輸的值多了之後。所以next.js提供了Route Masking這個特性用於路由的美化。
路由僞裝(Route Masking)
這項特性的官方名字叫Route Masking,沒有找到官方的中文名,所以就根據字面意思暫且翻譯成路由僞裝。所謂的路由僞裝即讓瀏覽器地址欄顯示的url和頁面實際訪問的url不一樣。實現路由僞裝的方法也很簡單,通過Link
組件的as
屬性告訴瀏覽器href對應顯示爲什麼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>
);
運行結果:
瀏覽器的url已經被如期修改了,這樣看起來舒服多了。而且路由僞裝對history也很友好,點擊返回再前進還是能夠正常打開詳情頁面。但是如果你刷新詳情頁,確報404的錯誤,如圖:
這是因爲刷新頁面會直接向服務器請求這個url,而服務端並沒有該url對應的頁面,所以報錯。爲了解決這個問題,需要用到next.js提供的自定義服務接口(custom server API)。
自定義服務接口
自定義服務接口前我們需要創建服務器,安裝Express:
npm install --save express
在項目根目錄下創建server.js 文件,內容如下:
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"
}
運行npm run dev
後項目和之前一樣可以運行,接下來我們需要添加路由將被僞裝過的url和真實的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和真實的url映射起來,並且query參數也進行了映射。重啓項目之後就可以刷新詳情頁而不會報錯了。但是有一個小問題,前端路由打開的頁面和後端路由打開的頁面title不一樣,這是因爲後端路由傳過去的是id,而前端路由頁面顯示的是title。這個問題在實際項目中可以避免,因爲在實際項目中我們一般會通過id獲取到title,然後再展示。作爲Demo我們偷個小懶,直接將id作爲後端路由頁面的title。
之前我們的展示數據都是靜態的,接下來我們實現從遠程服務獲取數據並展示。
遠程數據獲取
next.js提供了一個標準的獲取遠程數據的接口:getInitialProps
,通過getInitialProps
我們可以獲取到遠程數據並賦值給頁面的props。getInitialProps
即可以用在服務端也可以用在前端。接下來我們寫個小Demo展示它的用法。我們打算從TVMaze API 獲取到一些電視節目的信息並展示到我的網站上。首先,我們安裝isomorphic-unfetch,它是基於fetch實現的一個網絡請求庫:
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;
以上代碼的邏輯應該很清晰了,我們在getInitialProps
中獲取到電視節目的數據並返回,這樣在Index的props就可以獲取到節目數據,再遍歷渲染成節目列表。
運行項目之後,頁面完美展示:
接下來我們來實現詳情頁,首先我們把/p/:id
的路由修改爲:
...
server.get('/p/:id', (req, res) => {
const actualPage = '/post';
const queryParams = { id: req.params.id };
app.render(req, res, actualPage, queryParams);
});
...
我們通過將id作爲參數去獲取電視節目的詳細內容,接下來修改post.js的內容爲:
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;
重啓項目(修改了server.js的內容需要重啓),從列表頁進入詳情頁,已經成功的獲取到電視節目的詳情並展示出來:
增加樣式
到目前爲止,咱們做的網頁都太平淡了,所以接下來咱們給網站增加一些樣式,讓它變得漂亮。
對於React應用,有多種方式可以增加樣式。主要分爲兩種:
1. 使用傳統CSS文件(包括SASS,PostCSS等)
2. 在JS文件中插入CSS
使用傳統CSS文件在實際使用中會用到挺多的問題,所以next.js推薦使用第二種方式。next.js內部默認使用styled-jsx框架向js文件中插入CSS。這種方式引入的樣式在不同組件之間不會相互影響,甚至父子組件之間都不會相互影響。
styled-jsx
接下來,我們看一下如何使用styled-jsx。將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 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;
運行項目,首頁變成:
增加了一點樣式之後比之前好看了一點點。我們發現導航欄的樣式並沒有變。因爲Header是一個獨立的的component,component之間的樣式不會相互影響。如果需要爲導航增加樣式,需要修改Header.js:
import Link from 'next/link';
const Header = () => (
<div>
<Link href="/">
<a>Home</a>
</Link>
<Link href="/about">
<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;
效果如下:
全局樣式
當我們需要添加一些全局的樣式,比如rest.css或者鼠標懸浮在a標籤上時出現下劃線,這時候我們只需要在style-jsx
標籤上增加global
關鍵詞就行了,我們修改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
這樣鼠標懸浮在所有的a標籤上時會出現下劃線。
部署next.js應用
Build
部署之前我們首先需要能爲生產環境build項目,在package.json中添加script:
"build": "next build"
接下來我們需要能啓動項目來serve我們build的內容,在package.json中添加script:
"start": "next start"
然後依次執行:
npm run build
npm run start
build完成的內容會生成到.next
文件夾內,npm run start
之後,我們訪問的實際上就是.next
文件夾的內容。
運行多個實例
如果我們需要進行橫向擴展( Horizontal Scale)以提高網站的訪問速度,我們需要運行多個網站的實例。首先,我們修改package.json的start script:
"start": "next start -p $PORT"
如果是windows系統:
"start": "next start -p %PORT%"
然後運行build: npm run build
,然後打開兩個命令行並定位到項目根目錄,分別運行:
PORT=8000 npm start
PORT=9000 npm start
運行完成後打開localhost:8000和localhost:9000都可以正常訪問:
通過以上方法雖然能夠打包並部署,但是有個問題,我們的自定義服務server.js並沒有運行,導致在詳情頁刷新的時候依然會出現404的錯誤,所以我們需要把自定義服務加入app的邏輯中。
部署並使用自定義服務
我們將start script修改爲:
"start": "NODE_ENV=production node server.js"
這樣我們就解決了自定義服務的部署。重啓項目後刷新詳情頁也能夠正常訪問了。
到此爲止,我們就瞭解了next.js的基本使用方法,如果有疑問可以查看next.js官方文檔,也可以給我留言討論。
- 本文Demo源碼:Github 源碼
- next.js官網:https://nextjs.org/
- next.js官方教程:https://nextjs.org/learn
- next.js Github:https://github.com/zeit/next.js