新前端時代的服務端渲染也提了好久了,各類技術演進層出不窮。css
帶你領略一下基於React和Nodejs架構的服務端渲染技術,讓你快速上手,開發Next服務端渲染的項目。前端
本文參考官方文檔,作精簡總結,用一些小例子,讓你快速理解Next.js項目的開發。node
全文3800字左右,閱讀全文約15分鐘。react
本文同步發表於知乎專欄:前端微志。歡迎關注。webpack
服務端渲染,是指頁面的渲染和生成由服務器來完成,並將渲染好的頁面返回客戶端。而客戶端渲染是頁面的生成和數據的渲染過程是在客戶端(瀏覽器或APP)完成。ios
隨着先後端分離模式的興起,從服務端渲染到服務端渲染的演進,前端Web App給前端開發帶來了很大的便利,並減輕後端服務器壓力。web
先後端解耦,讓前端專一作好用戶UI層,專一於提高用戶體驗,讓後端專一於業務邏輯處理,分離成微服務,專心作好一件事。ajax
先後端分離前的服務端渲染技術有:PHP,ASP,JSP等方式,分離後的前端SPA(單頁面應用)渲染擁有獨立的路由和頁面渲染(React,Vue和Angular等),而SPA的最大問題是對SEO不友好,當項目對SEO有需求時,SPA就不是一個好的選擇。express
近兩年來,React和Vue也開始支持服務端渲染(Server Side Render ),行業內也有這方面的實踐,其中掘金就使用Vue的SSR功能作了全站服務端渲染,且效果良好。npm
咱們如今介紹一個React生態中表現突出的服務端渲染框架:Next.js。
Next.js是什麼?它有什麼優勢?
Next.js是一個基於React的一個服務端渲染簡約框架。它使用React語法,能夠很好的實現代碼的模塊化,有利於代碼的開發和維護。
Next.js帶來了不少好的特性:
默認服務端渲染模式,以文件系統爲基礎的客戶端路由
代碼自動分隔使頁面加載更快
(以頁面爲基礎的)簡潔的客戶端路由
以webpack的熱替換爲基礎的開發環境
使用React的JSX和ES6的module,模塊化和維護更方便
能夠運行在Express和其餘Node.js的HTTP 服務器上
能夠定製化專屬的babel和webpack配置
怎麼開始構建一個Next.js項目?
在開始構建Next.js項目以前,須要作好一些準備:
首先,無論你使用哪一個操做系統,你須要一個趁手的命令行工具,在Mac系統和Linux下自帶的命令行工具比較好用,在Windows系統下,我推薦一個命令行工具:Cmder;
已經在本地安裝好Nodejs和Npm;
熟悉React技術棧開發及ES6語法;
熟悉Express架構的Nodejs開發。
下面開始構建一個Next.js項目。
建立項目
在命令行工具中依次執行下面語句:
// 在本地建立一個項目跟目錄 $ 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
命令執行完畢後,在瀏覽器中打開 http://localhost:3000,你會看到這樣的頁面:
這個頁面是Next.js默認生成的404頁面,而開啓服務後訪問的之因此是404頁面,是由於咱們尚未設置項目主頁。
建立頁面
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 便可看到設置好的頁面。
多頁面構建
使用Next.js的目的就是構建非SPA的多頁面項目。下面開始構建多頁面應用,建立第二個頁面。
在 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
在瀏覽器中訪問首頁 http://localhost:3000 時,點擊 About Page便可跳轉到about頁面。
Link組件是經過location.history的瀏覽器API保存歷史路由,因此,你能夠經過瀏覽器左上角的前進和後退按鈕來切換歷史路由。而在開發過程當中,你不須要再單獨寫客戶端路由的配置。
Link組件是React的高階組件的實現,不能對它進行樣式的設置,它只是起到路由的跳轉功能,可是它的複用性強,只要包含一個能觸發onClick事件的組件便可。
組件複用
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> )
在 pages/about.js 中一樣引入 Header 組件,在瀏覽器上經過點擊導航切換頁面。
進一步封裝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
建立動態頁面
使用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> )
訪問 http://localhost:3000/ 訪問,頁面以下:
有了列表頁,須要再寫一個博客的詳情頁,從上面的代碼中也可看到,咱們須要建立一個 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> )
在博文列表頁,點擊博文名可跳轉到對應博文的詳情頁。
用路由遮蓋(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頁面。這是由於路由遮蓋默認只在客戶端路由中有效,要想在服務端也支持路由遮蓋,須要在服務端單獨設置路由解析的方法。
服務端支持路由遮蓋
上面說到,服務器默認不支持路由遮蓋,要讓服務器支持它,須要單獨對路由進行設置。下面以 Express (你也可使用Koa等其餘Nodejs的Web服務器框架)建立後端服務器講解如何設置服務器來支持路由遮蓋。
首先須要將 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('/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) })
請求接口,獲取數據
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 來處理異步請求,並將取到的數據當作一個屬性賦給頁面,頁面拿到這個屬性的值後會用於頁面的初始化渲染。頁面展現效果以下圖:
樣式化組件
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>。
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
執行完命令後,可在 http://host:3000 訪問。其中,host 是指服務器的IP地址。