Next.js服務端渲染
先來說一下服務端渲染吧,目前做的項目確實是服務端渲染的,但是不是我搭的,我個人寫的一些東西都不是服務端渲染的,所以自己動動手試試官方的Next.js
,知識這東西就是拿來分享學習的嘛,學會了就是我的~哈哈
1.1 客戶端渲染
最原始的前端,頁面在瀏覽器獲取到JavaScript和CSS等文件之後開始渲染,完全在客戶端(也就是瀏覽器),路由是客戶端路由,也就是現在大部分的SPA單頁應用。
1.2服務端渲染
頁面由服務端渲染過後直接返回html頁面給前端,url的變更會刷新整個頁面,也就是以前的php和jsp模式
1.3 同構
高端點的詞Universal APP,爲什麼要同構,因爲客戶端渲染存在一個缺點,就是首屏加載過大文件或過多文件會變得特別慢,因此可以把首屏放在客戶端來渲染來提升首屏速度,首屏加載過後路又開始交給客戶端控制,又變成了SPA應用,整個代碼都是用JavaScript來編寫,服務端採用nodejs。
2 開始一個Nextjs應用
學習編程的我們都知道,入門系列是hello world,這裏我覺得next.js還是挺友好的,因爲它真的免除了我們平時所理解的服務端渲染的各種繁雜配置,只需要簡單的幾步就可以新建一個Universal App。
2.1 安裝依賴
好吧,你只需要新建一個文件夾,然後運行下面一段命令
// 初始化應用
yarn init
// 安裝三個依賴
yarn add react react-dom next
// package.json配置啓動
{
...
// 新增啓動方式,選擇使用next啓動
"script": {
"dev": "next"
}
...
}
OK,你已經完成了基於next的服務端搭建,是不是真的很簡單。
接下來我們運行一下yarn dev。
what?居然報錯了,好吧,原來Next.js默認從 pages 目錄下取頁面進行渲染返回給前端展示,並默認取 pages/index.js 作爲系統的首頁進行展示。因此,我們需要新建一個pages目錄。接下在再重新啓動一下試試。
2.2 新建pages文件夾以及文件
因爲3000經常被另一個項目使用,所以我把啓動端口改成3006了,修改script的啓動方式即可:
"script": {
"dev": "next -p 3006"
}
然後我們訪問 http://localhost:3006:
ok,得到了一個非常簡潔的一個頁面,講道理,我很喜歡這種簡潔的頁面。爲啥404了呢,原來我們只新建了pages目錄,剛剛也說了,它默認根路由頁面是pages下的index.js,所以我們新建一個index.js。
const Home = () => (
<h1>我是Next的首頁</h1>
);
export default Home;
ok,現在就沒啥問題了。因爲next.js默認開啓服務端渲染,也就無需我們進行任何的配置,因此現在這個極其簡單的應用就是一個Universal React APP。從頁面元素我們也可以看出來:
2.3 Next路由
看到這裏,新手小夥伴應該跟我一樣感嘆Next.js強大的同時也會有一個疑問,等一下,怎麼就渲染了?路由你都沒配置憑啥就出首頁了,一般的SPA至少也會配置路由才能進行頁面跳轉,這裏沒配置路由首頁出來了我還有其他頁面呢,怎麼辦?
這些東西還都是Next給我們配置好的,剛纔說了Next.js默認匹配pages目錄的index.js作爲根路徑/,其他的路徑也是這樣按文件名匹配的。
而我們的各種路由跳轉依賴的不再是react-router而是next.js給我們封裝好的路由(其實redux也是,後面會說到)。
// /pages/index.js頁面 ----> /
import React, { Fragment } from 'react';
import Link from 'next/link';
const Home = () => (
<Fragment>
<h1>我是Next的首頁</h1>
<Link href='/userList'>
<a>用戶列表頁</a>
</Link>
</Fragment>
);
export default Home;
// -- /pages/userList.js ----> /userList
const UserList = () => (
<h2>我是用戶列表頁</h2>
);
export default UserList;
厲害了我的哥,不僅跳轉成功,而且對應的history也都幫我們封裝好了,後退也都正常,而且可以看到,我們無需在前端或者node端配置任何路由相關,只需要按照它的模板去寫就可以了。並且前端頁面的跳轉也是無刷新的~
不過蘿蔔白菜各有所愛,雖然個人覺得很強大但是看不到路由還是感覺怪怪的,而且寫法也有變化,也就是路由必須在pages路徑下纔可以,這樣工程大了以後豈不是會很混亂,而且頁面和路由融合在一起redux怎麼辦,感覺很臃腫啊,作爲一個剛從純前端SPA過度過來的肯定很彆扭,我覺得不可能這麼low吧,肯定不會吧,稍後再探索吧。
3. 接入AntDesign
個人對於腳手架的UI有一種執念,如果搭建出來就是一個首頁+a標籤跳轉,實在不是我這個處女座的風格,因此第二步我就想引用UI框架 —— ant-design
,相信很多使用react的開發者用的也都是這個UI框架吧。因爲以前自己在配製的時候也經常採坑,所以還是在這裏記錄一下~
3.1 安裝依賴
既然是安裝ant-design,那麼這兩個東西肯定是不能少的,一個是antd另一個就是antd官方的按需加載babel插件babel-plugin-import
。
// 安裝依賴
yarn add antd babel-plugin-import
因爲現在開發環境大部分過渡到ES6/ES7語法了,因此還需要安裝一個babel的裝飾器轉化插件babel-plugin-transform-decorators-legacy
,說實話這個插件具體是幹啥的我還真沒太仔細看,不過裝上它在babel裏配置就可以使用antd了。
當然還有其他方法,我這裏只是使用了這一種方法~
// 根目錄新建.babelrc文件
{
"presets": ["next/babel"],
"plugins": [
"transform-decorators-legacy",
[
"import",
{
"libraryName": "antd",
"style": "css"
}
]
]
}
配置好了,我們來試一試,yarn dev
啓動項目,額,一大堆報錯,爲啥呢?因爲原本在其他腳手架配置的時候需要在webpack裏配置一些東西嘛,這個怎麼可能沒有配置文件呢?
當然有了,只不過改名了,叫做next.config.js
了,我們在服務端跑正常的css是不可以的。我們可以引入一下next-css這個包,然後require.extensions['.css']
,還是那句話,我不理解,以後再深入研究一下,目前目的是可用~但是配置方案查到了就在這裏寫一下。
// 安裝依賴
yarn add @zeit/next-css
// 根目錄下創建next.config.js,內容如下
/* eslint-disable */
const withCss = require('@zeit/next-css');
// fix: prevents error when .css files are required by node
if (typeof require !== 'undefined') {
require.extensions['.css'] = (file) => {}
}
module.exports = withCss();
好了,現在我們在啓動,就沒有報錯了,畢竟是官方解決方案,還是好使~把首頁的a標籤換成antd的button試試效果,效果是下面這樣:
額,果然沒這麼簡單,這又咋的了,也沒有任何報錯,也沒有任何提示,顯而易見就是樣式沒加載進來吧。。。繼續查,OK,明白了,其實antd的樣式已經有了,只不過在頁面上沒被引進來。爲什麼這麼說呢?
- 第一個就是渲染出來的頁面head標籤裏沒有任何的CSS樣式,
- 第二個就是antd的樣式文件已經被打包放進.next文件夾的static文件夾裏面了。
原因找到了,接下來就是解決問題了
3.2 Next.js Head組件
解決問題就是我們需要把那個style.css放到頁面裏,但是我翻遍了整個工程目錄,都沒有找到正常React SPA的那個index.html,尷尬了,有問題還是得找官方文檔啊,查完過後發現了這個東西,Head,想看具體的可以點進去看官網,寫的挺詳細的~,就是我們可以使用這個head組件來爲我們的頁面添加head信息。
// /pages/index.js
import React, { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';
import Head from 'next/head';
const Home = () => (
<Fragment>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta charSet='utf-8' />
<title>Next-Antd-Demo</title>
<link rel='stylesheet' href='/_next/static/style.css' />
</Head>
<Fragment>
<h1>我是Next的首頁</h1>
<Link href='/userList'>
<Button type='primary'>用戶列表頁</Button>
</Link>
</Fragment>
</Fragment>
);
export default Home;
OK,到現在而言是不是有點NB了,O(∩_∩)O哈哈~,真的是採坑系列啊,配置一個UI組件就這麼麻煩。估計接下來有坑可踩啦!
3.3 抽離Head爲Layout
一般的應用都會有個菜單Menu導航條之類的嘛,所以頁面就做頁面的事情,head放裏面感覺怪怪的,還是按照習慣把Head抽離出來當成一個高級父組件吧。個人習慣,就新建了一個components文件夾,裏面新建Layout.js。
// /components/Layout.js
import Head from 'next/head';
export default ({ children }) => (
<div>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta charSet='utf-8' />
<title>Next-Antd-Scafflod</title>
<link rel='stylesheet' href='/_next/static/style.css' />
</Head>
<style jsx global>{`
body {
}
`}</style>
{children}
</div>
);
// /pages/index.js
import React, { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';
import Layout from '../components/Layout';
const Home = () => (
<Layout>
<Fragment>
<h1>Hello Next.js</h1>
<Link href='/userList'>
<Button type='primary'>用戶列表頁</Button>
</Link>
</Fragment>
</Layout>
);
export default Home;
講到這裏,整個Antd的配置基本就完成了吧,哈哈,沒想到講個antd配置能寫這麼多,真實厲害了~既然UI框架嘛,順便我就把CSS也寫了吧。看Next官網可以很明確瞭解到它推崇的是css-in-js
,具體鏈接大家請點這裏Next Css-in-Js
,說白了,可以把它理解成用類Vue的形式寫React,組件內部使用下面這種形式來修改樣式
<style jsx>{`
p {
color: blue;
}
div {
background: red;
}
@media (max-width: 600px) {
div {
background: blue;
}
}
`}</style>
<style global jsx>{`
body {
background: black;
}
`}</style>
這裏需要注意的是,組件內部的css並不是子組件繼承父組件,就是組件內部使用,如果想要子組件繼承父組件樣式,需要將style jsx
改成style global jsx
這種形式,說實話,越看越像Vue,
除了上面那種官方推薦的方法以外,還有其他很多種Css-in-Js的樣例,其中個人還是比較推薦styled-components的,大家感興趣可以去看官方文檔,寫的真的很不錯。
以前我在用antd的時候,都會根據重置一下自帶配色以及一些其他的默認屬性,這裏我才用了以前的方式結果出錯了,以前的方式是依賴babel-plugin-import,在babelrc文件裏將"style": “css"改成"style”: true,這樣,babel-plugin-import會加載.less文件,然後在webpack裏面配置less-loader的modifyVars變量進行覆蓋:
config.module.rules.push({
test: /\.less$/,
use: [
{
loader: "style-loader"
}, {
loader: "css-loader"
}, {
loader: "less-loader",
options: {
sourceMap: true,
modifyVars: AntdTheme
}
}
]
})
但是在next框架裏如果使用less方式引入服務端渲染會過不去,這算是一個坑?用下面這種方式就好了,無關痛癢~
<style jsx global>{`
.ant-btn-primary {
background-color: #ec6a00
}
`}</style>
你看,也可以改,不過個人覺得antd的配色還是挺不錯的,哈哈,就別改了。我認爲官方後續會增強的吧!
可能官方早就有解決方案了吧,只不過我還是不太會用?因爲我看除了next-css
包以外還提供了next-less
包,這個包應該就是用來加載less文件的吧我看了一下這個包還支持css-modules
,不過我配置了一下還是不太對,並且我對目前這種寫法還覺得挺舒服的,就不多浪費時間了,大家感興趣的可以攻克一下,解決了可以留言個地址給我,萬分感謝~
4 目錄重構
來說一說爲什麼要目錄重構吧,Next.js很強大,爲我們封裝了路由,只需要在pages下面新建js文件,裏面寫上我們熟悉的頁面也就是react組件,就會渲染出來!
其實對於開發來說沒區別,但是項目龐大以後,一個路由對應一個js文件,但是如果頁面很複雜其實不是這個React組件也會很複雜,不是很符合組件化理念,後期也不好維護啊。而且,肯定要加redux的,這樣的話就更加混亂了。所以現在趁着還清醒,趕快重新構建一下~
4.1 抽離Layout
首先,我們在跟目錄下新建一個components文件夾,專門用來放我們的組件,新寫一個Header.js:
// /components/Header.js
import React, { Component } from 'react';
import Link from 'next/link';
import { color_youdao, color_youdao_border } from '../constants/CustomTheme';
class Header extends Component {
constructor(props) {
super(props);
const { title } = props;
this.state = { title };
}
render() {
const { title } = this.state;
return (
<div className='header-container'>
<Link href='/'>
<div className='logo-container'>
<img className='logo' alt='logo' src='/static/logo.png' />
<span className='sys-name'>XXX系統</span>
</div>
</Link>
<h2>{title}</h2>
<style jsx>{`
.header-container {
height: 60px;
background-color: ${color_youdao};
border: 1px solid ${color_youdao_border};
margin-bottom: 10px;
}
h2 {
text-align: center;
line-height: 60px;
font-size: 1.6rem;
font-weight: 500;
color: #fff;
}
.logo-container {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
top: 15px;
left: 20px;
cursor: pointer;
}
.sys-name {
display: inline-block;
margin-left: 10px;
font-size: 20px;
color: #fff;
font-weight: 600;
}
.logo {
width: 30px;
height: 30px;
}
`}</style>
</div>
)
}
}
export default Header;
然後,把Layout.js
裏面加上Header,然後我們每個組件都使用Layout嵌套,就完成了整個骨架的搭建~真的很簡單!
// /components/Layout.js
import { Fragment } from 'react';
import Head from 'next/head';
import Header from './Header';
export default ({title, children }) => (
<Fragment>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta charSet='utf-8' />
<title>Next-Antd-Demo</title>
<link rel='stylesheet' href='/_next/static/style.css' />
</Head>
<style jsx global>{`
* {
margin: 0;
padding: 0;
}
body {
font-family: Helvetica, 'Hiragino Sans GB', 'Microsoft Yahei', '微軟雅黑', Arial, sans-serif;
}
.content-container {
display: flex;
flex-direction: column;
align-items: center;
}
`}</style>
<Header title={title} />
<div className='content-container'>
{children}
</div>
</Fragment>
);
OK,現在Layout組件就暫時算完成了。
4.2 抽離頁面組件
上面提到過,pages作爲next的路由索引目錄,那麼我就想讓它專心的做路由,就不要做組件的複雜邏輯了,因此,我想把組件的內部實現全部放在components文件夾下。然後路由頁面只需要直接引用組件就可以啦~
// /components/Home/Home.js 頁面組件
import React, { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';
import Layout from '../Layout';
const Home = () => (
<Layout title='首頁'>
<Fragment>
<h1>Hello Next.js</h1>
<Link href='/userList'>
<Button type='primary'>用戶列表頁</Button>
</Link>
</Fragment>
</Layout>
);
export default Home;
// /pages/index.js 路由組件
import Home from '../components/Home/Home';
export default Home;
其實很簡單,但是這麼看起來就很清晰嘛,O(∩_∩)O哈哈~
4.3 靜態資源引用
靜態資源的引用比如圖片,你可以使用CDN然後src直接填寫url,也可以通過工程內部的靜態文件引用。Next同樣爲我們提供了非常簡便的方式,與引用css一樣,css是通過Head組件來引入頁面的,靜態文件next官網推薦我們在根目錄新建一個static文件夾,然後靜態文件放在static文件夾下,引用的時候使用絕對路徑的形式,next就會找到它們~就像下面這樣:
<img className='logo' alt='logo' src='/static/logo.png' />
4.4 抽離靜態常量
然後就是抽離靜態常量,這個就很簡單了,新建一個constants文件夾,把我們經常用到的常量在裏面定義好,然後就可以使用了,比如CSS的配色(從我選擇的系統配色不知道小夥伴是不是能猜出來我是哪個公司的),_比如後面引入Redux的Action type。
// /constants/ConstTypes.js
export const RoleType = {
1: '管理員',
10: '普通用戶'
}
// 使用
import { RoleType } from '../../constants/ConstTypes';
現在基本暫時完成了目錄重構(將來引入redux肯定還得重構一次)。目錄結構如下:
-- root
| -- components // 組件目錄
| -- constants // 常量目錄
| -- pages // 路由目錄
| -- static // 靜態資源目錄
| -- .babelrc
| -- .eslintrc
| -- .gitignore
| -- package.json
| -- ...其他配置文件
5 再談路由
Next.js的路由剛開始給我的感覺就是,我靠,很NB啊。但是慢慢的用起來發現,坑還真不少。前面也提到了,它是以pages下面的js文件作爲路由路徑驚醒匹配的,所以也就是說你想用到的頁面全要以js文件的形式放進pages,那麼層級嵌套關係怎麼辦?ok,嘗試了一下,很容易解決了。
5.1 路由層級
[需求]: 與用戶相關的包括用戶列表,用戶詳細信息等等…這兩個功能應該是同屬於用戶子模塊,所以應該與首頁不是同級關係。
[解決辦法]:pages下面新建子目錄user裏面包括userList.js和userDetail.js。
– pages
| – user
| – userList.js
| – userDetail.js
| – index.js
可以看到,這樣算是解決了一個問題。
5.2 路由參數
緊接着,問題又出現了,我們需要查看用戶詳情,以往來說,很容易想到 /user/userDetail/:username,這種嘛,參數通過url的params獲取,但是,悲劇了。查了一下Next.js路由API,人家沒給你提供params,只提供了query。
5.3 query形式路由
也就是說,暫時我們需要/user/userDetail?username=XXX的形式來實現工程,雖然說沒什麼問題,但是可能每個人習慣不一樣吧。當然,對於我這種好說話的人,我可以接受O(∩_∩)O哈哈~
// 其實Next的Link組件的href屬性可以傳入一個對象
<Link href={{ pathname: '/user/userDetail', query: { username: text } }}>
<a>{text}</a>
</Link>
ok,實現效果就是這樣,反正符合預期,只是使用query代替params了。
P.S.真實是我不想費事搞這個東西,應該是可以解決的,稍後說我的想法
5.4 params形式路由
下面我來說說我的理解吧:
首先,是爲什麼它不支持params形式的路由,前面提到過了,他是根據pages下的js文件來匹配路由的,那麼你用params的路由勢必
/user/userDetail/:username
,那麼解析器會以爲我應該尋找的是pages目錄下面user目錄下面UserDetail目錄下面的${username}
文件,不用想肯定找不到啊,這時候就是404頁面了。所以這是我的理解,他爲什麼只使用query。
其次,我認爲兩者只是形式上的區別,並沒有本質上的區別,也就是實現效果是一樣的,都能跳轉到指定頁面嘛,何必糾結呢?
最後,就是我看完路有部分的文檔,我認爲是可以做到params形式的跳轉的,官方文檔裏可以自定義server:
// 官方文檔自定義server
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) => {
// Be sure to pass `true` as the second argument to `url.parse`.
// This tells it to parse the query portion of the URL.
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')
})
})
從上面可以看出來,我們可以將a路由匹配到b頁面。也就是我們可以把/user/userDetail/:username
的路由匹配到/user/userDetail?username=${username}
上面嘛。不就解決問題了O(∩_∩)O 哈哈,機智如我,不過我沒試驗過,只是猜測,目前優先想開發一個系統,這裏留坑,以後有機會再填~
5.5 路由填坑
上面在談到params路由和query路由的時候,留了一個坑,在這裏還是來解決一下
事實證明,就是我想的那樣,可以使用custom server然後重新匹配路由渲染的頁面就可以了。不過這樣會出現一個小問題,就是在網速過慢的時候重新匹配的頁面沒渲染出來之前,控制檯會出現報錯,重新渲染之後消失。這就類似於302,剛開始是404頁面,然後被重定向到另一個,哈哈~
可以看一下控制檯,會出現一個報錯,但是這樣確實會成功使用params的路由。而且我發現一個問題,第一次進新頁面的時候Next.js渲染會特別的慢,不知道是不是一個bug,還是我哪裏寫的有問題,再多研究研究~
5.6 報錯問題
好吧,去官網github查了一圈,也有人跟我一樣提了相同的issue,最後我看了一下發現,原來寫法出問題了,雖然可以正常執行,但是會在正確頁面出現之前404以下。處女座的我不能忍受還是改回來吧~
// 路由應該這麼寫
<Link href={`/user/userDetail?username=${text}`} as={`/user/userDetail/${text}`}>
<a>{text}</a>
</Link>
// server.js
server.get('/user/userDetail', (req, res) => {
return app.render(req, res, `/user/userDetail/${req.query.username}`);
});
上面那樣就可以了,具體代碼在下方~
5.7 開發模式下首次加載antd不出樣式
最後,我還是把antd的css形式換成了less形式,一方面是因爲確實配置主題色以及其他覆蓋樣式還是有需求的,另一方面是重點了,在開發模式下,next.js打開新頁面的pending time實在過長,這個過長的pending time導致第一次antd的樣式加載不出來。而換成less的模式雖然也很慢,但是樣式卻不會發生改變,所以還是切換到less了。
可以看出來,同樣是到新頁面去渲染一個table組件,雖然第一次加載時間都很長,但是less的形式是可以加載出來css的。哈哈。所以還是使用less吧,開發模式下,所有頁面的第二次加在都沒有問題,速度也很快。
next.js的生產環境還是比較快的,開發環境打開第一次新頁面確實有點慢,基本都要5s左右…
5.8 生產模式
上面截圖也看到了,Next.js在開發模式下頁面第一次的pending時間是非常長的,基本都要達到5s左右,當然也可能是我寫的代碼有問題?不過我去官方demo下面隨便用了一個,也是很慢的。
不禁我就思考了,如果上線項目第一次加載也這麼慢,怎麼可以呢?正在我考慮要不要半途而廢的時候,我嘗試了一下用生產模式打包一下,如果打包完生產環境首次加載也特別慢,那麼不扯淡呢嗎?拜拜了您嘞~
因爲我用的custom-server
,所以scripts變成了下面這樣:
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "set NODE_ENV=production && node server.js"
},
打包完之後也是正常訪問,下面是打包完以後的訪問效果。
可以看到,無論是首次加載頁面,還是第一次進入其他頁面,速度都是挺快的, 所以我原諒了開發時的慢速度了,還是接着學吧,O(∩_∩)O哈哈~
6 集成 Redux
一個程序員爲了不長房租答應房東教他孩子學習編程_北漂不易,且行且珍惜希望每一個北漂程序員都能早日財富自由,如果實在太累了就換個城市吧
6.1 繼續填坑
上一講有關路由的坑還是沒填明白,原本params路由自認爲已經沒問題了,不過最近在測試的時候,發現進入系統的時候是沒問題的,但是如果在params路由頁面進行刷新,會404頁面。所以,繼續fix~
// server.js
server.get('/user/userDetail', (req, res) => {
return app.render(req, res, `/user/userDetail/${req.query.username}`);
});
server.get('*', (req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;
if (typeof pathname !== 'undefined' && pathname.indexOf('/user/userDetail/') > -1) {
const query = { username: pathname.split('/')[3] };
return app.render(req, res, '/user/userDetail', query);
}
return handle(req, res);
});
上面這樣就真的可以了,刷新頁面也沒有任何問題~
寫過react SPA的大家應該基本都用過redux,按照官方教程一頓複製粘貼基本都能用,需要注意的就是redux會創建一個全局唯一的store包在整個應用的最外層。喏,這個是redux官方的示例:
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
那麼問題來了,我得有個東西讓他包起來對不對,在Next.js上來就跟我說了,默認是index,然後在組件裏再使用link來進行跳轉,這跟傳統的router有點區別啊。怎麼辦呢?官方給我們的解決辦法就是APP,用它來實現將應用包成一個整體(原諒我這麼理解了)。
注意了:下面也是約定俗成的
我們需要在pages文件夾下新建一個_app.js文件,不好意思其他名字不可以,然後寫上如下代碼,就可以啦~
// /pages/_app.js
export default class MyApp extends App {
render () {
const {Component, pageProps} = this.props
return (
<Container>
<Component {...pageProps} />
</Container>
)
}
}
ok,這樣就可以了。因爲我們什麼也沒幹,只是在pages文件夾下增加了一個_app.js,怎麼來看是否起作用了呢,我打印了一下props的router(因爲稍後重構頁面的時候會用到),可以看出來,雖然還是渲染的首頁,但是控制檯可以打印出router信息,所以還是那句話,既然選擇了Next.js就需要按照它制定的規則來~
6.2 重構Layout
前面文章說了,整個系統的架構大概就是上下佈局,頂部導航欄是固定的,所以抽離出來了一個Layout組件,這樣的話每一次每一個新組建外部都需要包一層Layout並且需要手動傳title,才能正確展示,有了APP這個組件我們就可以來重構一下Layout,這樣就不需要每個頁面都包一層Layout了~
// constants.js
// 路由對應頁面標題
export const RouterTitle = {
'/': '首頁',
'/user/userList': '用戶列表',
'/user/userDetail': '用戶詳情'
};
```// components/Home/Home.js
import { Fragment } from 'react';
import { Button } from 'antd';
import Link from 'next/link';
const Home = () => (
<Fragment>
<h1>Hello Next.js</h1>
<Link href='/user/userList'>
<Button type='primary'>用戶列表頁</Button>
</Link>
</Fragment>
);
export default Home;
// /pages/_app.js
import App, {Container} from 'next/app';
import Layout from '../components/Layout';
import { RouterTitle } from '../constants/ConstTypes';
export default class MyApp extends App {
constructor(props) {
super(props);
const { Component, pageProps, router } = props;
this.state = { Component, pageProps, router };
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.Component !== prevState.Component
|| nextProps.pageProps !== prevState.pageProps
|| nextProps.router !== prevState.router) {
return {
Component: nextProps.Component,
pageProps: nextProps.pageProps,
router: nextProps.router
};
}
return null;
}
render () {
const { Component, pageProps, router } = this.props;
return (
<Container>
<Layout title={RouterTitle[router.pathname]}>
<Component {...pageProps} />
</Layout>
</Container>
);
}
}
好啦,現在這樣就可以了,內部可能也需要小改一下。總之Layout部分就抽離出來了。越來越有規範化的系統樣子了~
這裏說一點我的感想,因爲Next幫我們做了很多配置的東西,所以在寫起來的時候就是需要按照它的約定俗成的規則,比如路由,APP,靜態資源這種。我覺得這樣寫有好處也有壞處吧,仁者見仁智者見智,至少我是挺喜歡的,因爲出問題了看文檔很快就會解決,其他的自行配置的SSR框架就會因人而異的出現各種莫名bug,還不知道要怎麼去解決~
6.3 狀態管理Redux準備
react這個框架只專注於View層,其他很多東西都需要額外引入,狀態管理redux就是一個React應用必備的東西,所以慢慢的也就變成是React全家桶一員,關於狀態管理機制不是這裏所要講的,太深奧了,還不太會的應該好好看看react相關知識了,這裏只是講在Next.js裏如何引入redux以及redux-saga
(如果喜歡用redux-thun
k可以用redux-thunk
,不過我覺得thunk不需要配置啥,所以就用saga寫例子了)。還是老樣子,引入了新東西,就需要提前安裝啊~
// 安裝redux相關依賴
yarn add redux redux-saga react-redux
// 安裝next.js對於redux的封裝依賴包
yarn add next-redux-wrapper next-redux-saga
如果你使用的是單純的客戶端SPA應用(類似於create-react-app創建的那種),那麼只安裝redux和redux-saga就可以了,因爲我們是基於next.js來搭建的腳手架,所以還是按照人家的標準來的~
瞭解redux的都知道,store,reducer,action這些合起來共同完成redux的狀態管理機制, 因爲我們選擇使用redux-saga來處理異步函數,所以還需要一個saga文件。因此我們一個一個來:
store
// /redux/store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { exampleInitialState } from './reducer';
import rootSaga from './saga';
const sagaMiddleware = createSagaMiddleware();
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
// 開發模式打印redux信息
const { logger } = require('redux-logger');
middleware.push(logger);
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
function configureStore (initialState = exampleInitialState) {
const store = createStore(
rootReducer,
initialState,
bindMiddleware([sagaMiddleware])
);
// saga是系統的常駐進程
store.runSagaTask = () => {
store.sagaTask = sagaMiddleware.run(rootSaga);
};
store.runSagaTask();
return store;
}
export default configureStore;
爲了方便調試,開發時我又引入了redux-logger
,用於打印redux相關信息。
老生常談,這次我也簡單的來用redux官方最簡單的示例計數器Counter來簡單地實現了,最後的視線效果如下圖:
//actions
// /redux/actions.js
export const actionTypes = {
FAILURE: 'FAILURE',
INCREMENT: 'INCREMENT',
DECREMENT: 'DECREMENT',
RESET: 'RESET',
};
export function failure (error) {
return {
type: actionTypes.FAILURE,
error
};
}
export function increment () {
return {type: actionTypes.INCREMENT};
}
export function decrement () {
return {type: actionTypes.DECREMENT};
}
export function reset () {
return {type: actionTypes.RESET};
}
export function loadData () {
return {type: actionTypes.LOAD_DATA};
}
// reducer
import { actionTypes } from './actions';
export const exampleInitialState = {
count: 0,
};
function reducer (state = exampleInitialState, action) {
switch (action.type) {
case actionTypes.FAILURE:
return {
...state,
...{error: action.error}
};
case actionTypes.INCREMENT:
return {
...state,
...{count: state.count + 1}
};
case actionTypes.DECREMENT:
return {
...state,
...{count: state.count - 1}
};
case actionTypes.RESET:
return {
...state,
...{count: exampleInitialState.count}
};
default:
return state;
}
}
export default reducer;
6.4 redux-saga
上面兩個內容還沒有涉及到saga部分,因爲簡單的reudx計數器並沒有涉及到異步函數,所以使用saga這麼高級的功能我們還需要請求一下數據~😄。正好有個用戶列表頁,我們這裏使用下面這個API獲取一個線上可用的用戶列表數據用戶數據接口
/* global fetch */
import { all, call, put, take, takeLatest } from 'redux-saga/effects';
import { actionTypes, failure, loadDataSuccess } from './actions';
function * loadDataSaga () {
try {
const res = yield fetch('https://jsonplaceholder.typicode.com/users');
const data = yield res.json();
yield put(loadDataSuccess(data));
} catch (err) {
yield put(failure(err));
}
}
function * rootSaga () {
yield all([
takeLatest(actionTypes.LOAD_DATA, loadDataSaga)
]);
}
export default rootSaga;
然後在我們用用戶列表頁初始化獲取數據,代碼如下:
import { connect } from 'react-redux';
import UserList from '../../components/User/UserList';
import { loadData } from '../../redux/actions';
UserList.getInitialProps = async (props) => {
const { store, isServer } = props.ctx;
if (!store.getState().userData) {
store.dispatch(loadData());
}
return { isServer };
};
const mapStateToProps = ({ userData }) => ({ userData });
export default connect(mapStateToProps)(UserList);
說實話這個地方稀裏糊塗弄出來的,next.js與原本的react寫法還是有些區別,狀態容器和展示容器劃分的也不是很分明,我暫時使用路由部分來做狀態容器,反正也成功了,下一節來重新劃分一下redux目錄結構,爭取讓項目更加合理一些~
這次時間拖的比較久,真的抱歉,最近思路也有點斷,不在科研狀態,哈哈。希望大家不要見怪,開始靜下心了!這裏還是偏使用,遠離還是建議大家去看redux相關文檔,講得更清楚,這裏只是next.js怎麼使用redux-saga
。接下來想了一下,讓工程目錄更加合理,然後就是把Next.js還沒涉及到的統一寫幾個Demo給大家示範一下~
7 目錄再重構
上一節引入了redux以及使用redux-saga
來進行異步函數的處理,而上一節的目錄只是簡單的引入redux而已,redux可是相當龐大和複雜的,並且也算是個人習慣了吧。action分離,reducer分離,狀態組件container等等。我喜歡把這些東西劃分的清清楚楚,這樣一個項目維護起來纔會方便~這一節就從頭到尾來進行目錄的劃分,因爲Next.js和原本的React SPA項目有一定的區別,主要體現在路由部分,所以我也是按照自己的理解和舒服的方式進行目錄重構!
重構完的目錄
// ================ 目錄結構 ================== //
——————
| -- asserts // ant-design全局less變量設置文件夾
| -- components // React展示組件(也就是UI組件)文件夾
| -- constants // 整個應用的常量文件夾
| -- ActionsTypes.js // 存放所有action type的常量文件
| -- ApiUrlForBE.js // 存放所有後端數據的apiUrl
| -- ...
| -- containers // React狀態組件文件夾
| -- pages // Next.js路由文件夾
| -- redux
| -- actions // 處理整個應用所有的action
| -- middlewares // 中間件,處理各種特殊情況,比如獲取失敗之後的message提醒
| -- reducers // 處理整個應用所有的reducer
| -- sagas // 處理整個應用所有的saga
| -- store.js
| -- static // 存放整個應用所有的靜態資源(如圖片等)
| -- .babelrc
| -- .eslintrc
| -- .gitignore
| -- next.config.js // Next.js配置文件
| -- package.json
| -- server.js // 服務端server文件
| ...
原諒我臭不要臉一下,個人認爲這個結構還是非常清晰的,只不過可能新手寫起來可能會覺得有些繁瑣,不過項目大的情況下,state樹很大,這種結構非常的清晰~
7.1 重構actions
其實actions完全可以放在一個文件裏使用,不過項目龐大了以後維護起來還是有些麻煩的,所以按照組件化思想,每一個組件對應一個action,或者每一個大功能塊對應一個action還是比較合理的。
-- redux
| -- actions
| -- home.js // 處理首頁action
| -- user.js // 處理與用戶有關action
| ... // 其他action
7.2 重構reducers
reducer部分肯定是要分離的,因爲redux的官方爲我們提供combineReducer這個API就是合併不同組件的reducer的,所以可以理解爲redux的reducer推薦就是根據組件進行劃分的~就如同整個應用只有一個狀態樹一樣,每一個reducer負責處理樹的不同枝葉派發出來的action。具體reducer內容還是去看redux官方文檔吧。
7.3 重構sagas
-- redux
| -- reducers
| -- home // 首頁部分reducer
| -- user // 用戶相關reducer
| ... // 其他reducer
| index.js // rootReducer,由combineReducer生成
7.4 抽離container
這裏需要特別說明一下~~~由於Next.js的特殊原因,其實已經做到了UI組件的分離,其實這一層container完全可以由pages文件夾代替,也就是可以用路由組件通過react-redux的connect函數封裝一下,這樣就變成了一個帶狀態的路由組件,不知道大家明不明白我說的話。。。下面是兩種方法,大家按需自己採取,以UserList組件爲例:
- 第一種,抽離container
// /conatiners/user/UserList.js
import { connect } from 'react-redux';
import { fetchUserListData } from '../../redux/actions/user';
import UserList from '../../components/User/UserList';
const mapStateToProps = state => ({
list: state.user.list.list,
});
const mapDispatchToProps = dispatch => ({
fetchUserListData() {
dispatch(fetchUserListData());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(UserList);
// pages/user/userList.js
import UserList from '../../containers/user/UserList';
import { fetchUserListData } from '../../redux/actions/user';
// 這部分內容下一章節講~
UserList.getInitialProps = async (props) => {
const { store, isServer } = props.ctx;
if (store.getState().user.list.list.length === 0) {
store.dispatch(fetchUserListData());
}
return { isServer };
};
export default UserList;
簡單來說其實就是路由組件導入的是狀態組建UserList.js,而狀態組建是通過react-redux的connect方法封裝UI組件UserList.js而得來的。
- 第二種,帶狀態的路由組件
// /pages/user/userList.js
import { connect } from 'react-redux';
import UserList from '../../containers/user/UserList';
import { fetchUserListData } from '../../redux/actions/user';
UserList.getInitialProps = async (props) => {
const { store, isServer } = props.ctx;
if (store.getState().user.list.list.length === 0) {
store.dispatch(fetchUserListData());
}
return { isServer };
};
const mapStateToProps = state => ({
list: state.user.list.list,
});
const mapDispatchToProps = dispatch => ({
fetchUserListData() {
dispatch(fetchUserListData());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(UserList);
簡單來說,就是在路由組件內把UI組件UserList.js通過connect變成了狀態組件。
個人推薦第一種方法,雖然寫起來稍微麻煩了一些,但是第二種方法完全是因爲Next.js的特殊性才能實現的,當然,對於Next.js來說,第二種方式確實更簡單一些~
經歷了上面幾個部分的重構,整個基於Next.js的服務端渲染腳手架基本結構也就成型了。在搭建過程中還是遇到了很多坑的,不過也都一點點的踩過去了。希望對大家有些幫助,個人認爲這個結構還是值得參考一下的~原本到這裏就可以結束文章了,不過我在使用過程又發現了一些坑,順便的Next.js還有一些內容我還沒碰過,就幫大家都踩一踩,下一節來一個其他內容的大雜燴~
8. Next.js其他需要了解的知識點
8.1 獲取數據&&getInitialProps
獲取數據,依然是Next與普通的React SPA應用不同的地方,React應用基本都有自己的路由組件(當然大部分是react-router
),我們可以通過路由組件爲我們提供的方法,比如react-router
的onEnter()
方法或者universal-router
的beforeEnter()
方法。
這裏給大家推薦一個區別於react-router
的路由組件universal-router
而Next.js沒有路由組件,所以具體方式肯定不同於路由組件的方式,具體不同就體現在Next.js爲我們提供了一個區別於React的新生命週期——getIntialProps()
,下面來說說這個API的牛X之處。
使用方法
在React.Component使用
import React from 'react'
export default class extends React.Component {
static async getInitialProps({ req }) {
const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
return { userAgent }
}
render() {
return (
<div>
Hello World {this.props.userAgent}
</div>
)
}
}
在stateless組件內使用
const Page = ({ stars }) =>
<div>
Next stars: {stars}
</div>
Page.getInitialProps = async ({ req }) => {
const res = await fetch('https://api.github.com/repos/zeit/next.js');
const json = await res.json();
return { stars: json.stargazers_count };
}
export default Page;
這個生命週期是脫離於React的正常生命週期的,不過我們依然可以在組件里正常使用react組件的各種生命週期函數。
8.2 服務端可用
這真是getInitialProps
這個生命週期的過人之處了,他可以在服務端運行,這樣做有什麼好處呢?減少抓取數據的次數
- React老生命週期內獲取數據
以抓取用戶列表爲例,我們可以在組件裏的componentDidMount
生命週期內獲取
// /components/user/userList.js
...
componentDidMount() {
this.props.fetchUserList();
}
從上圖我們可以看出來,每次進入用戶列表頁,都會重新抓取用戶數據。有人可能會說,這不廢話嗎,react不就這樣嗎,路由都切換了啊。沒錯,正常就是應該這樣,所以才說Next.js的這個新生命週期牛逼啊。
- 使用
getInitialProps
生命週期
// /pages/user/userList.js
import UserList from '../../containers/user/UserList';
import { fetchUserListData } from '../../redux/actions/user';
UserList.getInitialProps = async (props) => {
const { store, isServer } = props.ctx;
if (store.getState().user.list.list.length === 0) {
store.dispatch(fetchUserListData());
}
return { isServer };
};
export default UserList;
兄弟們發現沒,進入系統後只會在第一次進入路由的時候獲取數據,之後再進入因爲服務端緩存過數據,所以不需要重新獲取,減少了獲取次數~
具體原因就是因爲static getInitialProps()
這個生命週期是可以在服務端運行的,當頁面第一次加載時,服務器收到請求,getInitialProps()
會執行,getInitialProps()
返回的數據,會序列化後添加到 window.__NEXT_DATA__.props
上,寫入HTML源碼裏,類似於。這樣服務端的getInitialProps()
就實現了把數據傳送給了客戶端。當我們通過Next.js的路由Link來進行頁面跳轉的時候,客戶端就會從window.__NEXT_DATA__
裏獲取數據渲染頁面,就無需重新獲取數據,算是提升性能的話一種方式吧~
8.3 存在問題——踩坑
這裏其實還真遇到一個坑,可能有很多人遇到過了,也可能沒人遇到過。具體問題描述起來大概是這個樣子,我們在getInitialProps
裏面預獲取數據,以用戶列表爲例,在首次加載的時候都是沒有問題的包括各種客戶端跳轉。不過當我們在用戶列表頁面進行刷新的時候,其實他就沒有再走getInitialProps
這個生命週期了,因此頁面會沒有可以渲染的數據,就會出現空頁面,因爲他認爲這個應該從window.__Next_DATA__
裏面獲取,而不是重新獲取數據~那麼爲什麼刷新頁面之後沒有走這個getIntialProps
,講道理,我還真沒太弄清楚,不過確實刷新頁面next.js會給我們在props
裏返回一個isServer:true
,但是控制檯並沒有獲取數據。
我們可以很清楚地看到,頁面數據通過redux-saga
獲取,在pages的getIntialProps()
裏面,代碼如下:
import { fetchUserListData } from '../../redux/actions/user';
UserList.getInitialProps = async (props) => {
const { store, isServer } = props.ctx;
if (store.getState().user.list.list.length === 0) {
store.dispatch(fetchUserListData());
}
return { isServer };
};
上面fetchUserListData()
就是抓取數據的action
,返回值就會存入state,渲染數據列表。很明顯,在第一次加載的時候是抓取成功的。但是刷新頁面後,沒有dispatch這個action,也就是表明,刷新頁面沒有走這個getIntialProps
這個生命週期!!!
上面纔是關鍵問題所在,不刷新頁面的情況下是正常的,刷新頁面沒有走這個生命週期,而我們很多數據都是需要預獲取的,所以說還挺坑的,事實上,很多人遇到這個問題,而且我在next官方給出的reudx-demo
裏面也發現這個問題,也就是說他們官方的demo刷新也會出現這個問題。
8.4 解決辦法
既然是踩坑,當然有解決辦法啦~而且還是兩種:
- 第一種:在組件生命週期裏判斷isServer
剛剛問題描述過了,也就是正常加載和通過路由跳轉頁面,數據會正常渲染且會從瀏覽器的window.__NEXT_DATA__
獲取來減少不必要的網絡請求~,而在頁面進行刷新的時候不會重新請求數據並且window.__NEXT_DATA__
裏也找不到我們想要的數據。不過通過控制檯信息我們可以發現問題所在以及解決辦法。那就是,第一次啓動系統的時候返回的isServer是false,而瀏覽器刷新頁面的時候isServer返回的是true,我們可以在組件裏進行這個變量的判斷,如果是true,就重新進行一次數據抓取。
// /components/user/UserList.js
...
componentDidMount() {
if(this.props.isServer) {
// 需要重新抓取數據
this.props.fetchUserListData();
}
}
...
從上圖可以看到,刷新頁面的時候,我們會重新獲取數據渲染頁面,如果不刷新就不會重新獲取。還是可行的這個方法~
- 第二種:換一種方式預獲取數據
另一種方法就比較高級了,原理我依然不知道,但是就是好用,哈哈,這東西真是邪門,爲什麼這麼說呢,其實本質沒改變什麼,就是換了種寫法就可以。具體就是,上面的寫法我在getInitalProps
裏面寫了dispatch了一個獲取數據的action,從上一節或者代碼裏你們可以看到,其實這個action就是fetch一個api獲取數據返回state。這就是redux一個獲取數據的基本過程,這種方法在刷新時行不通,而行得通的方法是:要通過isomorphic-unfetch
這個來拉取服務端的數據。
// /pages/user/userList
import fetch from 'isomorphic-unfetch';
import UserList from '../../containers/user/UserList';
import { fetchUserListDataSuccess } from '../../redux/actions/user';
UserList.getInitialProps = async (props) => {
const { store, isServer } = props.ctx;
let userData;
if (store.getState().user.list.list.length === 0) {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
userData = await res.json();
store.dispatch(fetchUserListDataSuccess(userData));
}
return { isServer };
};
export default UserList;
8.5 Document
這個組件從我使用的角度來看,作用跟我前幾章有個地方的目的是一樣的,就是我們在Next.js裏沒有類似create-react-app
裏面的index.html。因此我們沒有辦法定義最後渲染的html的結構,比如title,meta等標籤。我最開始是通過next/head
的Head組件來實現的,但是head組件其實最後生成的就是html的head標籤。而Document組件是完全幫助我們構造html結構。
// 除去Layout的Head結構
// pages文件夾新增_document.js文件
// ./pages/_document.js
import Document, { Head, Main, NextScript } from 'next/document';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<html>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1' />
<meta charSet='utf-8' />
<title>Next-Antd-Scafflod</title>
<link rel='shortcut icon' href='/static/favicon.ico' type='image/ico'/>
<link rel='stylesheet' href='/_next/static/style.css' />
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
);
}
}
document.js
是隻在Next.js的服務端來進行渲染的,客戶端只是拿到服務端渲染過後的html字符串渲染前端頁面,上面提到的window.__NEXT_DATA__
就是存放在NextScript
裏的。
8.6 Dynamic Import
其實以前在寫服務端渲染項目的時候會遇到很多坑,最常見的就是比如我想引入一些外部組件,這些組件裏有window,document等這種客戶端變量,而這些變量在服務端是不存在的,因此在服務端渲染的時候就會報錯,所以就很麻煩,需要webpack各種配置然後在異步引入。比如:富文本編輯器。而next直接爲我們封裝了動態引入的import,不出意外用的應該就是webpack的import方法,管他呢,好用就行。下面就給大家簡單是演示一下其中一個功能,就是動態引入一個富文本編輯器,然後空白期loading另一個組件~用法非常簡單,就是下面這樣:
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(import('braft-editor'), {
loading: () => <p>正在加載組件...</p>
});
render() {
return (
<Fragment>
<h1>用戶信息:{this.state.username}</h1>
<div style={{ width: '50%', height: '400px', }}>
<DynamicComponent />
</div>
</Fragment>
);
}
詳細的Next爲我們提供了更多的方法,感興趣的可以去官網看文檔,有四種異步引入的方法,其中還包含只在服務端引入~文檔地址
8.7 error handling
錯誤處理,目前很多優秀的腳手架都爲我們提供了錯誤處理,比如404和500的時候的頁面渲染,Next.js同樣,內部自動爲我們封裝了errorPage
。也就是我們其實什麼都不用幹,就可以享受這個服務。比如我在系統裏隨便輸入一個網址,會出現下面的結果:
然後你還可以自己定義你的errorPage
頁面,方法非常的簡單,就是在pages文件夾下面新建一個_error.js
的文件,裏面寫上你的errorPage
代碼就可以了,下面就簡單寫一個,其實就是從官網扒下來的~
// /pages/_error.js
import React from 'react'
export default class Error extends React.Component {
static getInitialProps({ res, err }) {
const statusCode = res ? res.statusCode : err ? err.statusCode : null;
return { statusCode }
}
render() {
return (
<p>
{this.props.statusCode
? `An error ${this.props.statusCode} occurred on server`
: 'An error occurred on client'}
</p>
)
}
}
ok,可以看到,很明顯的生效了。雖然效果差不多,但是你如果按照自己的來寫,肯定是沒問題的。哈哈~
8.8 Static HTML export
又一個高級功能,它支持我們把各種路由導出成靜態頁面,不過你細想其實也沒啥大用,畢竟我們項目都是有邏輯的,導出靜態頁面也不能操作,哈哈。不過既然是挺牛逼的一個功能,就拿來試試。
- 第一步,在config文件夾裏配置一下頁面和路由
exportPathMap: async (defaultPathMap) => {
return {
'/home': { page: '/' },
'/userList': { page: '/user/userList' },
}
},
- 第二步,package.json添加export命令
"scripts": {
...
// 新增導出命令
"export": "yarn build && next export"
},
- 第三步,運行
yarn export
命令
運行完命令之後,根目錄下會出現一個out文件夾,真的是非常神奇,裏面有頁面文件夾和必要的靜態資源。
然後我們打開index.html
訪問一下應該就是我們的首頁了。
emm…這個首頁有點奇怪,靜態資源和css都不太對勁兒,至於爲什麼我就不去追究了,肯定有辦法的。不過我只是試試功能,時間有限準備休息了,哈哈。感興趣的大家自己研究研究。
9 總結
寫到這裏,Next.js踩坑入門系列就寫完了。非常感謝有很多小夥伴一直在看~~