2. Next.js服務器端渲染
學習目標
- 了解Next.js的作用
- 掌握Next.js中的路由
- 掌握Next.js中布局組件的創建
- 掌握Next.js中的靜態文件服務
- 掌握Next.js中獲取頁面數據的方法
- 掌握Next.js中組件樣式的書寫
- 使用Next.js完成豆瓣電影案例
- 能夠自定義頭部元素head
2.1 什么是Next.js?
Next.js是一個基於React的一個服務端渲染簡約框架。它使用React語法,可以很好的實現代碼的模塊化,有利於代碼的開發和維護。
Next.js帶來了很多好的特性:
- 默認服務端渲染模式,以文件系統為基礎的客戶端路由
- 代碼自動分割使頁面加載更快
- 以webpack的熱替換(HMR)為基礎的開發環境
- 使用React的JSX和ES6的module,模塊化和維護更方便
- 可以運行在Express和其他Node.js的HTTP 服務器上
- 可以定制化專屬的babel和webpack配置
使用服務器端渲染好處:
- 對SEO友好
- 提升在手機及低功耗設備上的性能
- 快速顯示首頁
2.2 Next.js初體驗
mkdir hello-next
cd hello-next
npm init -y
npm install --save react react-dom next
mkdir pages
配置package.json中的scripts屬性
{
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
}
}
此時npm run dev
會得到一個404頁面
創建一個pages/index.js
const Index = () => (
<div>
<p>Hello Next.js</p>
</div>
)
export default Index
創建一個pages/next-route/teacher.js頁面
const Teacher = () => (
<div>
<p>教師頁面</p>
</div>
)
export default Teacher
2.3 頁面導航
2.3.1 路由跳轉
- Link組件
- Link組件內不能直接寫文字,必須使用標簽包裹,標簽可以是任何標簽,但是必須只能保證Link組件內只有一個子元素;
- 給Link組件設置樣式不會生效,因為Link組件是一個HOC(高階組件),但是可以給它里面的子元素設置樣式;
import Link from 'next/link'
<Link href="/teachers">
<a>教師頁面</a>
</Link>
組件<Link>
可接收 URL 對象,而且它會自動格式化生成 URL 字符串。例如:
<Link href={{pathname: '/teachers', query: {id: 1}}}>
<a>教師頁面</a>
</Link>
- 命名式路由
import Router from 'next/router'
export default () => (<div><span onClick={() => Router.push('/teacher')}>教師</span></div>)
URL對象語法:
Router.push({pathname: '/teacher', query: {id: 1}})
注意:如果沒有匹配到的話,默認會去找_error.js
中定義的組件; 路由跳轉不會向服務器發送請求頁面的請求。
2.3.2 創建組件
-
普通組件
組件的創建可以在任何的文件夾下面,但是不要放在pages下面,因為組件並不需要url
-
布局組件
利用this.props.children
-
全局布局組件, 創建_app.js,模板入下:
import React from 'react' import App, {Container} from 'next/app' class Layout extends React.Component { render () { const {children} = this.props return <div className='layout'> {children} </div> } } export default class MyApp extends App { render () { const {Component, pageProps} = this.props return <Container> <Layout> <Component {...pageProps} /> </Layout> </Container> } }
2.3.3 query strings
- 創建一個帶query的鏈接
- 如果你想應用里每個組件都處理路由對象,你可以使用
withRouter
高階組件。從next/router中引入withRouter,注入路由對象到Next.js中的組件作為組件屬性,從而獲取query對象 - 組件使用props.router.query.xxx獲取query strings
2.3.4 Clean URLs with Route Masking
通過as屬性,給browser history來個路由掩飾,但是按刷新按鈕路由就找不到了,因為服務器回去重新找/p/xxxx頁面,但是實際上此時並不存在xxxx頁面
// /post?title=xxxx 會變成 /p/xxxx
<Link as={`/p/${props.id}`} href={`/post?title=${props.title}`}>
<a>{props.title}</a>
</Link>
2.3.5 服務器端支持Clean URLs
- 安裝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文件中scripts字段
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "NODE_ENV=production node server.js"
}
- 創建自定義路由
server.get('/teacher/:id', (req, res) => {
const actualPage = '/teacher/detail'
const queryParams = { id: req.params.id }
app.render(req, res, actualPage, queryParams)
})
2.4 靜態文件服務
項目的根目錄新建 static
文件夾,代碼通過 /static/
開頭的路徑來引用此文件夾下的文件,例如:
export default () => <img src="/static/logo.png" />
2.5 獲取頁面數據
- 下載isomorphic-unfetch :
npm install --save isomorphic-unfetch
- 引入
import fetch from 'isomorphic-unfetch';
使用異步靜態方法getInitialProps
獲取數據,此靜態方法能夠獲取所有的數據,並將其解析成一個 JavaScript
對象,然后將其作為屬性附加到 props
對象上
當頁面初次加載時,getInitialProps
只會在服務端執行一次。getInitialProps
只有在路由切換的時候(如Link
組件跳轉或命名式路由跳轉)時,客戶端的才會被執行。
注意:getInitialProps 不能 在子組件上使用,只能使用在pages
頁面中。
// Index是一個組件
Index.getInitialProps = async function() {
const res = await fetch('http://localhost:3301/in_theaters')
const data = await res.json()
// 這段數據會在服務器端打印,客戶端連請求都不會發
console.log(data)
return {
// 組件中通過props.shows可以訪問到數據
movieList: data
}
}
如果你的組件是一個類組件,你需要這樣寫:
export default class extends React.Component {
static async getInitialProps() {
const res = await fetch('http://localhost:3301/in_theaters')
const data = await res.json()
console.log(data);
return {movieList: data}
}
render() {
return (
<div>
{this.props.movieList.map(item => (
<p key={item.id}>{item.title}</p>
))}
</div>
)
}
}
getInitialProps
: 接收的上下文對象包含以下屬性:
-
pathname
:URL
的path
部分 -
query
:URL
的query string
部分,並且其已經被解析成了一個對象 -
asPath
: 在瀏覽器上展示的實際路徑(包括query
字符串) -
req
:HTTP request
對象 (只存在於服務器端) -
res
:HTTP response
對象 (只存在於服務器端) -
jsonPageRes
: 獲取的響應數據對象 Fetch Response (只存在於客戶端) -
err
: 渲染時發生錯誤拋出的錯誤對象
// 在另外一個組件中,可以使用context參數(即上下文對象)來獲取頁面中的query
Post.getInitialProps = async function (context) {
const { id } = context.query
const res = await fetch(`http://localhost:3301/in_theaters/${id}?_embed=details`)
const data = await res.json()
console.log(data)
return {movieDetail: data}
}
2.6 組件樣式
-
css樣式文件
-
css in js
-
styled-jsx
-
scoped
如果添加了
jsx
屬性,只作用於當前組件,不包括子組件
<style jsx>{`
h1, a {
font-family: "Arial";
}
ul {
padding: 0;
}
li {
list-style: none;
margin: 5px 0;
}
a {
text-decoration: none;
color: blue;
}
a:hover {
opacity: 0.6;
}
`}</style>
-
global
作用於當前組件,包括子組件
<style jsx global>{``}</style>
2.7 豆瓣電影案例
接口
獲取電影列表:http://localhost:3301/in_theaters
(in_theaters可以替換為coming_soon及top250)
獲取電影詳情:http://localhost:3301/in_theaters/1?_embed=details
2.7.1 豆瓣電影首頁
MovieHeader
組件樣式
.movie-header {
position: fixed;
top: 0;
left: 0;
right: 0;
}
ul {
display: flex;
justify-content: space-around;
align-items: center;
padding: 15px 0;
background-color: #1e2736;
margin: 0;
}
li {
list-style: none;
line-height: 30px;
height: 30px;
}
li a {
color: white;
}
li a:hover {
color: red;
}
2.7.2 豆瓣電影列表頁
.movie-type {
display: flex;
flex-direction: column;
align-items: center;
}
.movie-box {
display: flex;
flex-direction: column;
align-items: center;
margin: 20px 0;
padding: 10px 0;
width: 40%;
box-shadow: 0 0 10px #bbb;
}
.movie-box:hover {
box-shadow: rgba(0,0,0,0.3) 0px 19px 60px;
}
2.7.3 豆瓣電影詳情頁
.detail {
width: 40%;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
box-shadow: 0 0 10px #bbb;
}
.detail-box {
text-align: center;
}
2.8 自定義頭部元素head
引入next/head
export default () => {
<div>
<Head>
<meta name="keywords" content="" key="viewport" />
</Head>
</div>
}
注意:在卸載組件時,<head>
的內容將被清除。請確保每個頁面都在其<head>
定義了所需要的內容,而不是假設其他頁面已經加過了