使用Next搭建React SSR工程架構之基礎篇(二)
Next.js
是一個基於React
的一個服務端渲染簡約框架。它使用React
語法,可以很好的實現程式碼的模組化,有利於程式碼的開發和維護
1.1 Next.js帶來了很多好的特性
- 預設服務端渲染模式,以檔案系統為基礎的客戶端路由
-
程式碼自動分隔使頁面載入更快
-(以頁面為基礎的)簡潔的客戶端路由 -
以
webpack
的熱替換為基礎的開發環境 -
使用
React
的JSX
和ES6
的module
,模組化和維護更方便 -
可以執行在
Express
和其他Node.js
的HTTP
伺服器上 -
可以定製化專屬的
babel
和webpack
配置
二、構建一個基本的Next專案
2.1 建立專案
// 在本地建立一個專案跟目錄 $ mkdir hello-next // 切換到專案根目錄 $ cd hello-next // 用npm初始化專案 $ npm init -y // 將react和next安裝到本地依賴 $ npm install --save react react-dom next // 建立資料夾 pages $ mkdir pages
建立完資料夾之後,開啟hello-next
檔案下的package.json
檔案,在scripts
下新增一個script
,如下
{ "scripts": { "dev": "next" } }
$ npm run dev
2.2 建立頁面
Next.js
是從伺服器生成頁面,再返回給前端展示。Next.js
預設從pages
目錄下取頁面進行渲染返回給前端展示,並預設取pages/index.js
作為系統的首頁進行展示。注意,pages
是預設存放頁面的目錄,路由的根路徑也是pages
目錄
-
在
pages/index.js
中建立一個React
函式式元件
const Index = () => ( <div> <p>Hello Next.js</p> </div> ) export default Index
Next.js
預設使用Webpack
構建專案,webpack
的熱部署功能一樣能提升開發效率。建立完pages/index.js
後,再訪問http://localhost:3000
即可看到設定好的頁面
-
在
pages
目錄下建立檔案pages/about.js
export default () => ( <div> <p>This is the about page</p> </div> )
建立完之後,可以通過http://localhost:3000/about
訪問該頁面。至此,所有的頁面的路由都是通過後端伺服器來控制的,要想實現客戶端路由,需要藉助Next.js
的Link API
。
Link API
從next/link
中可以引用到Link
元件。在pages/index.js
檔案中引用Link
,修改如
// This is the Link API 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
-
Link
元件是通過location.history
的瀏覽器API儲存歷史路由,所以,你可以通過瀏覽器左上角的前進和後退按鈕來切換歷史路由。而在開發過程中,你不需要再單獨寫客戶端路由的配置 -
Link
元件是React
的高階元件的實現,不能對它進行樣式的設定,它只是起到路由的跳轉功能,但是它的複用性強,只要包含一個能觸發onClick
事件的元件即可
2.3 元件複用
Next.js
是以多頁面為中心,只要將頁面檔案放在pages
目錄下,就可以在瀏覽器上以檔名為路由名來訪問到
-
元件的設定跟React一樣,通過
export
匯出,通過import
匯入。一般,只要不想讓使用者通過頁面直接訪問的元件,都不放在pages
目錄下。對除了pages
目錄,元件放在哪個目錄下沒有要求,開發者可以自定義設定
下面再components
目錄下,建立一個公用元件Header
,用於各個檔案的頭部導航,通過導航可以在頁面見切換
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
在pages/index.js
中引入Header
import Header from '../components/Header' export default () => ( <div> <Header /> <p>Hello Next.js</p> </div> )
進一步封裝Header
元件,建立一個自動包含Header
和Content
的元件components/MyLayout.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
2.4 建立動態頁面
使用Next.js
建立動態頁面,與使用React
或Vue
建立一個SPA
頁面大體相同,唯一的區別就是頁面的渲染主體不同,前者是Nodejs
伺服器獲取到後端資料渲染完頁面後再返回給前端展示,後者是前端先獲取頁面主體架構,再通過ajax
的方式請求後端的資料,在前端渲染展示
以一個簡易的部落格頁面為例,建立部落格列表頁,修改pages/index.js
import Layout from '../components/MyLayout.js' import Link from 'next/link' 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="Learn Next.js is awesome"/> <PostLink title="Deploy apps with Zeit"/> </ul> </Layout> )
有了列表頁,需要再寫一個部落格的詳情頁,從上面的程式碼中也可看到,我們需要建立一個pages/post.js
檔案
import Layout from '../components/MyLayout.js' export default (props) => ( <Layout> <h1>{props.url.query.title}</h1> <p>This is the blog post content.</p> </Layout> )
2.5 用路由遮蓋(Route Masking)的乾淨的URL
Next.js
上提供了一個獨特的特性:路由遮蓋(Route Masking)。它可以使得在瀏覽器上顯示的是路由A
,而App
內部真正的路由是B
。這個特性可以讓我們來設定一些比較簡潔的路由顯示在頁面,而系統背後是使用一個帶引數的路由。比如上面的例子中,位址列中顯示的是http://localhost:3000/post?title=Hello%20Next.js
,這個地址含有一個title
引數,看著很不整潔。下面我們就用Next.js
來改造路由,使用路由遮蓋來建立一個更加簡潔的路由地址。比如我們將該地址改造成http://localhost:3000/p/hello-nextjs
首先我們要修改pages/index.js
下的PostLink
元件,會使用到next/link
元件的as
屬性,並給元件新增一個屬性id
import Layout from '../components/MyLayout.js' import Link from 'next/link' 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="Learn Next.js is awesome"/> <PostLink id="deploy-nextjs" title="Deploy apps with Zeit"/> </ul> </Layout> )
-
當在
Link
元件上使用as
屬性時,瀏覽器上顯示的是as
屬性的值,走的是客戶端路由,而伺服器真正對映的是href
屬性的值,走的是服務端路由 -
這樣就會有一個問題,如果在前端路由間切換不會有問題,可以正常顯示,但是在頁面
http://localhost:3000/p/hello-nextjs
時重新整理頁面,會顯示404
頁面。這是因為路由遮蓋預設只在客戶端路由中有效,要想在服務端也支援路由遮蓋,需要在服務端單獨設定路由解析的方法
2.6 服務端支援路由遮蓋
上面說到,伺服器預設不支援路由遮蓋,要讓伺服器支援它,需要單獨對路由進行設定。下面以Express
(你也可以使用Koa等其他Nodejs的Web伺服器框架)建立後端伺服器講解如何設定伺服器來支援路由遮蓋
$ 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('/p/:id', (req, res) => { const actualPage = '/post' const queryParams = { title: req.params.id } app.render(req, res, actualPage, queryParams) }) 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" } }
這時候,伺服器已經可以支援路由遮蓋了,在顯示遮蓋路由的頁面,重新整理頁面也可以正常顯示內容。具體的實現是在伺服器中對/p/*
開頭的路由進行重寫,然後重定向到/post
開頭的路由上,最後將內容返回給前端。具體程式碼是這一段:
server.get('/p/:id', (req, res) => { const actualPage = '/post' const queryParams = { title: req.params.id } app.render(req, res, actualPage, queryParams) })
2.7 請求介面,獲取資料
Next.js
在React
的基礎上為元件添加了一個新的特性:getInitialProps
(有點像是getInitialState
),它用於獲取並處理元件的屬性,返回元件的預設屬性。我們可以在改方法中請求資料,獲取頁面需要的資料並渲染返回給前端頁面
引入一個支援在客戶端和伺服器端傳送fetch
請求的外掛isomorphic-unfetch
,當然你也可以使用axios
等其他工具
$ npm install --save isomorphic-unfetch
然後修改pages/index.js
裡的內容,換成下面這樣:
import Layout from '../components/MyLayout.js' import Link from 'next/link' import fetch from 'isomorphic-unfetch' const Index = (props) => ( <Layout> <h1>Batman TV Shows</h1> <ul> {props.shows.map(({show}) => ( <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=batman') const data = await res.json() console.log(`Show data fetched. Count: ${data.length}`) return { shows: data } } export default Index
上述程式碼中,在getInitialProps
中使用了async
和await
來處理非同步請求,並將取到的資料當做一個屬性賦給頁面,頁面拿到這個屬性的值後會用於頁面的初始化渲染
2.8 樣式化元件
Next.js
提供了一個css-in-js
的特性,它允許你在元件內部寫一些樣式,你只需要在元件內使用<style jsx>
標籤來寫css
即可。舉個例子,比如我們在pages/index.js
裡新增樣式
import Layout from '../components/MyLayout.js' import Link from 'next/link' function getPosts () { return [ { id: 'hello-nextjs', title: 'Hello Next.js' }, { id: 'learn-nextjs', title: 'Learn Next.js is awesome' }, { id: 'deploy-nextjs', title: 'Deploy apps with ZEIT' } ] } export default () => ( <Layout> <h1>My Blog</h1> <ul> {getPosts().map((post) => ( <li key={post.id}> <Link as={`/p/${post.id}`} href={`/post?title=${post.title}`}> <a>{post.title}</a> </Link> </li> ))} </ul> <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> </Layout> )
在上述程式碼中,我們沒有直接使用<style>
標籤來書寫樣式程式碼,而是寫在一個模板字串({`
})裡面。
Next.js使用
babel外掛來解析
styled-jsx` ,它支援樣式名稱空間,未來還將支援變數賦值。
需要注意的是:styled-jsx
的樣式不會應用到子元件,如果想要該樣式適用於子元件,可以在styled-jsx
標籤新增屬性global:<style jsx global>。
2.9 怎麼部署一個next.js專案
Next.js
專案的部署,需要一個Node.js
的伺服器,可以選擇Express
,Koa
或其他Nodejs
的Web伺服器。本文中以Express
為例來部署Next
專案。
伺服器的入口檔案就使用上文中提到的server.js
,在server.js
裡添加了針對部署環境的選擇,程式碼如下
const dev = process.env.NODE_ENV !== 'production'
為了區分部署環境,我們需要在package.json
中修改script
屬性如下
"scripts": { "build": "next build", "start": "NODE_ENV=production node server.js -", "dev": "NODE_ENV=dev node server.js" }
其中,build
命令是用於打包專案,start
命令是用於生產環境部署,dev 命令是用於本地開發。
執行如下命令即可將Next
專案 部署到伺服器
$ npm run build $ npm run start