一.前言
先解釋一下Nuxt.js和Next.js雖然只有一個字母之差,但它們是不同的兩個服務端渲染框架.
什麼是Next.js?
引用Next中文官網的一句話:
Next.js 是一個輕量級的 React 服務端渲染應用框架。
Next.js帶來了很多好的特性:
- 默認服務端渲染模式,以文件系統爲基礎的客戶端路由(注意:沒有專門路由)
- 代碼自動分割使頁面加載更快
- 以webpack的熱替換(HMR)爲基礎的開發環境
- 使用React的JSX和ES6的module,模塊化和維護更方便
- 可以運行在Express和其他Node.js的HTTP 服務器上
- 可以定製化專屬的babel和webpack配置
使用Next服務器端渲染好處:
- 對SEO友好
- 提升在手機及低功耗設備上的性能
- 快速顯示首頁
二.Next.js學習
按照國際慣例,先來一個hello world應用
先執行創建命令:
mkdir 項目名
cd 項目名
npm init -y
npm i react react-dom next --save
mkdir pages//一定要叫這個名,不能更改
配置package.json中的scripts屬性
"scripts": {
"dev":"next",
"build":"next build",
"start":"next start"
},
在pages文件夾裏創建一個index.js頁面,簡單寫點內容:
export default () => {
return (
<div>
<p>hello world</p>
</div>
)
}
執行npm run dev,出來效果:
頁面導航
路由跳轉(組件跳轉和事件跳轉)
Link組件跳轉:
1,引入Link組件
import Link from 'next/link';
2.使用
注意點:
路徑是用href;
文字裏面要用標籤包裹(標籤可以是a標籤或者其他標籤都可以,但Link標籤裏只能寫一個其他標籤);
給Link標籤設置style樣式是無效的,因爲Link是一個高階組件(HOC),但我們可以給子元素設置樣式.
href屬性也可以改爲對象寫法:
<Link href={{pathname:"/next-route/teacher"}} >
<button style={{color:'red'}}>去教師頁面</button>
</Link>
同時對象寫法可以傳遞query參數過去
代碼:
<Link href={{pathname:"/next-route/teacher",query:{id:1}}} >
<button style={{color:'red'}}>去教師頁面</button>
</Link>
顯示效果:
事件路由跳轉
第一步:引入Router對象
import Router from "next/router";
第二步:添加跳轉事件
留意一下:瀏覽器輸入網址的請求跳轉方式network裏會請求頁面和js,但通過點擊跳轉的方式只有js,沒有再次請求頁面.
自定義404頁面
直接在pages文件夾裏創建一個_error.js頁面(只能叫這個名字)
創建公共導航組件(components文件夾)
不要寫在pages有路由的文件夾裏,在根目錄裏我們要創建一個單獨的components文件夾,寫代碼如下:
import Link from "next/link"
const Mynav=()=>{
return (
<div>
<Link href={{pathname:"/next-route/teacher",query:{}}} >
<button style={{color:'red'}}>去教師頁面</button>
</Link>
<Link href={{pathname:"/next-route/student",query:{}}} >
<button style={{color:'red'}}>去學生頁面</button>
</Link>
</div>
)
}
export default Mynav
在路由主頁中引入使用
導航效果就出來了:
student和teacher頁面引入方式和上面一樣.
佈局組件的使用(layouts文件夾)
第一步:創建佈局組件
在根目錄裏創建一個layouts文件夾,裏面寫我的佈局組件,上面導航是共用的,但是下面主體內容會動態變化,怎麼實現呢?直接使用react裏面的this.props.children屬性即可動態渲染主體內容
第二步:使用佈局組件(核心:把佈局組件寫成雙標籤形式,在雙標籤裏放入要顯示的動態內容即可)
效果:
同樣方式,在teacher和student頁面也把Mynav組件去掉,也改成佈局組件Mylayout動態內容顯示方式(這樣Mynav組件就只有在Mylayout裏引入一次,這樣就實現了佈局組件來佈局)
這樣在路由主頁,教師頁面和學生頁面都採用了佈局組件,實現了Mynav導航組件只在佈局組件裏導入一次.比如如果我們後面還要加一個尾部固定組件的話,那我們只需要在佈局組件裏再增加一個尾部組件即可,這樣非常方便.
全局佈局組件
上面的Mylayout佈局組件在主頁,教師頁和學生頁等每個頁面都引入了一次,有沒有辦法全局一次引入呢?辦法如下:
在pages文件加下創建_app.js(只能叫這個名字),寫如下代碼(是固定寫法):
import React from 'react'
import App, { Container } from 'next/app'
import Mylayout from './../layouts/Mylayout'
// Layout就是要寫的佈局組件,其它是固定寫法
class Layout extends React.Component {
render() {
const { children } = this.props
return <Mylayout>{children}</Mylayout>
}
}
export default class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return (
<Container>
<Layout>
<Component {...pageProps} />
</Layout>
</Container>
)
}
}
這樣其它頁面都不用引入Mylayout局部組件了,只寫自己的內容即可.
Link組件路由參數傳遞和獲取的方法
現在需求是老師渲染列表頁面點擊某位老師要進入詳情頁面並把id以參數方式傳遞過去.
在教師頁面修改如下:
// import Mynav from '../../../components/Mynav'
// import Mylayout from './../../../layouts/Mylayout'
import Link from 'next/link'
const teacherList=[
{name:"teacher1",id:1},
{name:"teacher2",id:2},
{name:"teacher3",id:3},
]
const Teacher =()=>{
return (
<div>
{/* <Mynav/> */}
{/* <Mylayout>
<p> teacher頁面</p>
</Mylayout> */}
<p> teacher頁面</p>
<ul>
{
teacherList.map((item)=>{
return (
<li key={item.id}>
<Link href={`/next-route/teacher/detail?id=${item.id}`}>
<a>{item.name}</a>
</Link>
</li>
)
})
}
</ul>
</div>
)
}
export default Teacher;
準備一個老師的詳情頁面,引入withRouter高階組件
,在withRouter()方法裏將組件傳遞過去,然後在props.router.query.id裏得到傳遞過來的參數(重要:withRouter可以獲取url裏的參數)
import {withRouter} from 'next/router';
const Detail=withRouter((props)=>{
console.log(props);
return (
<div>
這是{props.router.query.id}老師詳情頁面
</div>
)
})
export default Detail;
如果不引入withRouter是得不到query這個屬性,所以在next.js中一定要引入withRouter這個方法.
withRouter這個高階組件會講當前的路由對象注入到組件中去,並將路由對象綁定到組件的props這個參數上.
使用淺層路由優化路徑顯示
上面教師詳情頁顯示路徑如下
但路徑不好看,我們怎麼實現teacher/3這樣簡潔呢?使用next裏的淺層路由即可
其實就是使用Link組件有一個as屬性,它可以給路徑起別名,在教師頁面操作如下:
解決淺層路由刷新頁面找不到頁面的問題
上面的教師詳情頁當刷新頁面時,會找不到頁面,因爲通過as屬性,給browser history來個路由掩飾,但是按刷新按鈕路由就找不到了,因爲服務器回去重新找/p/xxxx頁面,但是實際上此時並不存在xxxx頁面,這個問題實際要服務器端協助解決(實際就是後臺將我們別名的路由地址轉爲原來真實的路徑),方法如下:
- 安裝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.js裏創建自定義路由
server.get('/next-route/teacher/:id', (req, res) => {
const actualPage = '/next-route/teacher/detail'
const queryParams = { id: req.params.id }
app.render(req, res, actualPage, queryParams)
})
靜態文件(即不需要打包的文件)
比如我們用到的圖片,它是不需要打包的,做法是在根目錄裏新建一個static文件夾(只能叫這個名字),在要用的地方寫絕對路徑即可.
請求數據接口(isomorphic-unfetch工具請求數據,裏面實現了函數組件和類組件的寫法)
isomorphic-unfetch支持服務器端渲染.使用方法如下:
1.安裝isomorphic-unfetch
npm install --save isomorphic-unfetch
2.使用
引入
import fetch from 'isomorphic-unfetch'
記住:fetch方法調用前要使用組件.getInitiaProps賦值一個函數,在函數裏面調用fetch方法.
因爲使用異步靜態方法getInitialProps獲取數據,此靜態方法能夠獲取所有的數據,並將其解析成一個 JavaScript對象,然後將其作爲屬性附加到 props對象上
上面是函數組件,類組件的話寫法如下:
這樣數據就出來了.
注意:getInitialProps 不能 在子組件上使用,只能使用在pages文件夾的頁面中進行調用。
同時,getInitialProps接收一個上下文對作爲參數,這個對象包含以下屬性:
- pathname: URL的 path部分
- query: URL的 query string部分,並且其已經被解析成了一個對象
- asPath: 在瀏覽器上展示的實際路徑(包括 query字符串)
- req: HTTP request 對象 (只存在於服務器端)
- res: HTTP response 對象 (只存在於服務器端)
- jsonPageRes: 獲取的響應數據對象 Fetch Response (只存在於客戶端)
- err: 渲染時發生錯誤拋出的錯誤對象
樣式寫法
next.js支持普通的react樣式外,還有自己的獨特樣式,寫法如下:
上面寫法有兩個屬性要注意
jsx:它僅限作用於當前組件,子組件不會生效;
global:它不但作用域當前組件,子組件也會生效.
豆瓣電影項目
創建電影主頁面pages/index.js:
創建公共導航組件components/Movieheader.js:
import Link from 'next/link';
const Movieheader =()=>(
<div class="movie-header">
<style jsx>
{`
.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;
}
`}
</style>
<ul>
<li>
<Link href="/movie/type?type=in-theaters"><a>正在熱映</a></Link>
</li>
<li>
<Link href="/movie/type?type=cooming_soon"><a>即將上映</a></Link>
</li>
<li>
<Link href="/movie/type?type=top250"><a>top250</a></Link>
</li>
</ul>
</div>
)
export default Movieheader;
電影列表頁面pages/movie/type/index.js:
注意數據請求發送方式.
import fetch from 'isomorphic-unfetch';
import {withRouter} from 'next/router'
import Link from 'next/link'
const Movietype = withRouter(props => (
<div className="movie-type">
<h1>這是電影詳情頁</h1>
<ul>
{props.movieList.map(item => {
return (
<div key={item.id} className="movie-box">
{/* 提示:地址裏的type值獲取方式可以使用withRouter高階組件獲取 */}
<Link href={`/movie/detail?id=${item.id}&type=${props.router.query.type}`}>
<div>
<img src={item.img} alt={item.title}></img>
<h4>{item.title}</h4>
<p>評分:{item.rating}</p>
</div>
</Link>
</div>
)
})}
</ul>
<style jsx>
{`
.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: 140%;
box-shadow: 0 0 10px #bbb;
}
.movie-box:hover {
box-shadow: rgba(0,0,0,0.3) 0px 19px 60px;
}
`}
</style>
</div>
))
Movietype.getInitialProps = async function(context) {
let res = await fetch(`http://localhost:3301/${context.query.type}`)
// console.log(context.query.type);
let data = await res.json()
console.log(data)
return {
movieList: data
}
}
export default Movietype
電影詳情頁面
import fetch from 'isomorphic-unfetch'
const Detail=(props)=>(
<div className="detail">
<div className="detail-box">
<img src={props.detail.img} alt={props.detail.title}/>
<h4>{props.detail.title}</h4>
<p>電影類型:{props.detail.genres.join(',')}</p>
<p>上映時間:{props.detail.details[0].year}</p>
<p>劇情介紹:{props.detail.details[0].summary}</p>
</div>
<style jsx>{`
.detail {
width: 40%;
margin: 0 auto;
padding: 20px;
box-sizing: border-box;
box-shadow: 0 0 10px #bbb;
}
.detail-box {
text-align: center;
}
`}</style>
</div>
)
Detail.getInitialProps=async function(context) {
let res= await fetch(`http://localhost:3301/${context.query.type}/${context.query.id}?_embed=details`)
let data =await res.json();
console.log(data);
return {
detail:data
}
}
export default Detail;
最後,介紹一下SEO搜索引擎優化
其實很簡單,next.js自己封裝了一個head組件,需要seo的地方按需引入,在裏面寫各自的標籤即可.
完整效果:
到此,next.js就學到這裏了.最後附上全部項目代碼克隆鏈接:
[email protected]:huanggengzhong/SSR.git