原文:Build a universal React and Node Appjavascript
演示:https://judo-heroes.herokuapp.com/css
譯者:nzbinhtml
譯者的話:這是一篇很是優秀的 React 教程,該文對 React 組件、React Router 以及 Node 作了很好的梳理。我是 9 月份讀的該文章,當時跟着教程作了一遍,收穫很大。可是因爲時間緣由,直到如今才與你們分享,幸虧趕在年末以前完成了譯文,不然必定會成爲 2016 年的小遺憾。翻譯倉促,其中還有個別不通順的地方,望見諒。前端
將 Node.js 做爲運行 web 程序的後端系統的一個優點就是咱們只需使用 JavaScript 這一種語言。因爲這個緣由,先後端能夠共享一些代碼,這能夠將瀏覽器及服務器中重複的代碼減小到最小。建立 JavaScript 代碼的藝術是 "環境未知的",現在被看作 "通用的 JavaScript",這條術語在通過 很 長時間 爭論 以後,彷佛取代了原始的名稱 "同構的 JavaScript"。java
咱們在建立一個通用的 JavaScript 應用程序時,主要考慮的是:node
通用的 JavaScript 仍然是一個很是新的領域,尚未框架或者方法能夠成爲解決全部這些問題的 "事實上" 的標準。儘管,已經有無數穩定的以及衆所周知的庫和工具能夠成功地構建一個通用的 JavaScript 的 Web 應用程序。react
在這篇文章中,咱們將使用 React (包括 React Router 庫) 和 Express 來構建一個展現通用渲染和路由的簡單的應用程序。咱們也將經過 Babel 來享受使人愉快的 EcmaScript 2015 語法以及使用 Webpack 構建瀏覽器端的代碼。webpack
我是一個 柔道迷 ,因此咱們今天要建立的應用叫作 "柔道英雄"。 這個 web 應用展現了最有名的柔道運動員以及他們在奧運會及著名國際賽事中得到的獎牌狀況。git
這個 app 有兩個主要的視圖:es6
一個是首頁,你能夠選擇運動員:
另外一個是運動員頁面,展現了他們的獎牌及其餘信息:
爲了更好的理解工做原理,你能夠看看這個應用的 demo 而且瀏覽一下整個視圖。
不管如何,你可能會問本身! 是的,它看起來像一個很是簡單的應用,有一些數據及視圖...
其實應用的幕後有一些普通用戶不會注意的特殊的事情,但卻使開發很是有趣: 這個應用使用了通用渲染及路由!
咱們可使用瀏覽器的開發者工具證實這一點。 當咱們在瀏覽器中首次載入一個頁面(任意頁面, 不須要是首頁, 試試 這一個) ,服務器提供了視圖的全部 HTML 代碼而且瀏覽器只需下載連接的資源(圖像, 樣式表及腳本):
而後當咱們切換視圖的時候,一切都在瀏覽器中發生:沒有從服務器加載的 HTML 代碼, 只有被瀏覽器加載的新資源 (以下示例中的 3 張新圖片) :
咱們能夠在命令行使用 curl 命令作另外一個快速測試 (若是你仍然不相信):
curl -sS "https://judo-heroes.herokuapp.com/athlete/teddy-riner"
你將看到整個從服務器端生成的 HTML 頁面(包括被 React 渲染的代碼):
我保證你如今已經信心滿滿地想要躍躍欲試,因此讓咱們開始編碼吧!
在教程的最後,咱們的文件結構會像下面的文件樹同樣:
├── package.json ├── webpack.config.js ├── src │ ├── app-client.js │ ├── routes.js │ ├── server.js │ ├── components │ │ ├── AppRoutes.js │ │ ├── AthletePage.js │ │ ├── AthletePreview.js │ │ ├── AthletesMenu.js │ │ ├── Flag.js │ │ ├── IndexPage.js │ │ ├── Layout.js │ │ ├── Medal.js │ │ └── NotFoundPage.js │ ├── data │ │ └── athletes.js │ ├── static │ │ ├── index.html │ │ ├── css │ │ ├── favicon.ico │ │ ├── img │ │ └── js │ └── views ` └── index.ejs
主文件夾中有 package.json
(描述項目而且定義依賴) 和 webpack.config.js
(Webpack 配置文件)。
餘下的代碼都保存在 src
文件夾中, 其中包含路由 (routes.js
) 和渲染 (app-client.js
和 server.js
) 所需的主要文件。它包含四個子文件夾:
components
: 包含全部的 React 組件data
: 包含數據 "模塊"static
: 包含應用所需的全部靜態文件 (css, js, images, etc.) 和一個測試應用的 index.html。
views
: 包含渲染服務器端的 HTML 內容的模板。須要在你的電腦上安裝 Node.js (最好是版本 6) 和 NPM。
在硬盤上的任意地方建立一個名爲 judo-heroes
的文件夾而且在給目錄下打開終端,而後輸入:
npm init
這將會啓動 Node.js 項目並容許咱們添加全部須要的依賴。
咱們須要安裝 babel, ejs, express, react 和 react-router 。 你能夠輸入如下命令:
npm install --save babel-cli@6.11.x babel-core@6.13.x \ babel-preset-es2015@6.13.x babel-preset-react@6.11.x ejs@2.5.x \ express@4.14.x react@15.3.x react-dom@15.3.x react-router@2.6.x
咱們也須要安裝 Webpack (以及它的 Babel loader 擴展) 和 http-server 做爲開發依賴:
npm install --save-dev webpack@1.13.x babel-loader@6.2.x http-server@0.9.x
如今, 我建設你已經具有了 React 和 JSX 以及基於組件方法的基礎知識。 若是沒有,你能夠讀一下 excellent article on React components 或者 React related articles on Scotch.io。
首先咱們只專一於建立一個實用的 "單頁應用" (只有客戶端渲染). 稍後咱們將看到如何經過添加通用的渲染和路由來改進它。
所以咱們須要一個 HTML 模板做爲應用的主入口,將其保存在 src/static/index.html
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"></div> <script src="/js/bundle.js"></script> </body> </html>
這裏沒有什麼特別的。只需強調兩件事:
src/static/css/
。/js/bundle.js
文件。 以後的文章會介紹如何使用 Webpack 和 Babel 生成該文件, 因此你如今不用擔憂。在一個真實的應用中,咱們可能會使用 API 來獲取應用所需的數據。
在這個案例中只有 5 個運動員及其相關信息的不多的數據, 因此能夠簡單點,把數據保存在 JavaScript 模塊中。這種方法能夠很簡單的在組件或模塊中同步導入數據, 避免增長複雜度以及在通用 JavaScript 項目中管理異步 API 的陷阱, 這也不是這篇文章的目的。
讓咱們看一下這模塊:
// src/data/athletes.js const athletes = [ { 'id': 'driulis-gonzalez', 'name': 'Driulis González', 'country': 'cu', 'birth': '1973', 'image': 'driulis-gonzalez.jpg', 'cover': 'driulis-gonzalez-cover.jpg', 'link': 'https://en.wikipedia.org/wiki/Driulis_González', 'medals': [ { 'year': '1992', 'type': 'B', 'city': 'Barcelona', 'event': 'Olympic Games', 'category': '-57kg' }, { 'year': '1993', 'type': 'B', 'city': 'Hamilton', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '1995', 'type': 'G', 'city': 'Chiba', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '1995', 'type': 'G', 'city': 'Mar del Plata', 'event': 'Pan American Games', 'category': '-57kg' }, { 'year': '1996', 'type': 'G', 'city': 'Atlanta', 'event': 'Olympic Games', 'category': '-57kg' }, { 'year': '1997', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '1999', 'type': 'G', 'city': 'Birmingham', 'event': 'World Championships', 'category': '-57kg' }, { 'year': '2000', 'type': 'S', 'city': 'Sydney', 'event': 'Olympic Games', 'category': '-57kg' }, { 'year': '2003', 'type': 'G', 'city': 'S Domingo', 'event': 'Pan American Games', 'category': '-63kg' }, { 'year': '2003', 'type': 'S', 'city': 'Osaka', 'event': 'World Championships', 'category': '-63kg' }, { 'year': '2004', 'type': 'B', 'city': 'Athens', 'event': 'Olympic Games', 'category': '-63kg' }, { 'year': '2005', 'type': 'B', 'city': 'Cairo', 'event': 'World Championships', 'category': '-63kg' }, { 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': '-63kg' }, { 'year': '2006', 'type': 'G', 'city': 'Cartagena', 'event': 'Central American and Caribbean Games', 'category': 'Tema' }, { 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'Pan American Games', 'category': '-63kg' }, { 'year': '2007', 'type': 'G', 'city': 'Rio de Janeiro', 'event': 'World Championships', 'category': '-63kg' }, ], }, { // ... } ]; export default athletes;
爲簡潔起見這裏的文件已被截斷,咱們只是展現一個運動員的數據。若是你想看所有的代碼, 在官方倉庫中查看。你能夠把文件下載到 src/data/athletes.js
。
如你所見,這個文件包含了一個對象數組。數組中的每一個對象表明一個運動員,包含一些通用的信息好比 id
, name
和 country
,另一個對象數組表明運動員得到的獎牌。
你能夠在倉庫中下載 全部的圖片文件 ,複製到: src/static/img/
。
咱們將把應用的視圖分紅若干個組件:
AthletePreview
, Flag
, Medal
和 AthletesMenu
Layout
組件,做爲主組件用來定義應用的通用樣式(header, content 和 footer)IndexPage
和 AthletePage
NotFoundPage
AppRoutes
組件咱們將要建立的第一個組件會展現一個漂亮的國旗以及它所表明的國家名:
// src/components/Flag.js import React from 'react'; const data = { 'cu': { 'name': 'Cuba', 'icon': 'flag-cu.png', }, 'fr': { 'name': 'France', 'icon': 'flag-fr.png', }, 'jp': { 'name': 'Japan', 'icon': 'flag-jp.png', }, 'nl': { 'name': 'Netherlands', 'icon': 'flag-nl.png', }, 'uz': { 'name': 'Uzbekistan', 'icon': 'flag-uz.png', } }; export default class Flag extends React.Component { render() { const name = data[this.props.code].name; const icon = data[this.props.code].icon; return ( <span className="flag"> <img className="icon" title={name} src={`/img/${icon}`}/> {this.props.showName && <span className="name"> {name}</span>} </span> ); } }
你可能注意到這個組件使用了一個國家的數組做爲數據源。 這樣作是有道理的,由於咱們只須要很小的數據。因爲是演示應用,因此數據不會變。在真實的擁有巨大以及複雜數據的應用中,你可能會使用 API 或者不一樣的機制將數據鏈接到組件。
在這個組件中一樣須要注意的是咱們使用了兩個不一樣的 props, code
和 showName
。第一個是強制性的, 必須傳遞給組件以顯示對應的國旗。 showName
props 是可選的,若是設置爲 true ,組件將會在國旗的後面顯示國家名。
若是你想在真實的 app 中建立可重用的組件,你須要添加 props 的驗證及默認值, 但咱們省略這一步,由於這不是咱們要構建的應用程序的目標。
Medal
組件與 Flag
組件相似。它接受一些 props,這些屬性表明與獎牌相關的數據: type
(G
表示金牌, S
表示銀牌以及 B
表示銅牌), year
(哪一年贏得), event
(賽事名稱), city
(舉辦比賽的城市)以及 category
(運動員贏得比賽的級別)。
// src/components/Medal.js import React from 'react'; const typeMap = { 'G': 'Gold', 'S': 'Silver', 'B': 'Bronze' }; export default class Medal extends React.Component { render() { return ( <li className="medal"> <span className={`symbol symbol-${this.props.type}`} title={typeMap[this.props.type]}>{this.props.type}</span> <span className="year">{this.props.year}</span> <span className="city"> {this.props.city}</span> <span className="event"> ({this.props.event})</span> <span className="category"> {this.props.category}</span> </li> ); } }
做爲前面的組件,咱們也使用一個小對象將獎牌類型的代碼映射成描述性名稱。
這一步咱們將要建立在每一個運動員頁面的頂端顯示的菜單,這樣用戶不須要返回首頁就能夠很方便的切換運動員:
// src/components/AthletesMenu.js import React from 'react'; import { Link } from 'react-router'; import athletes from '../data/athletes'; export default class AthletesMenu extends React.Component { render() { return ( <nav className="atheletes-menu"> {athletes.map(menuAthlete => { return <Link key={menuAthlete.id} to={`/athlete/${menuAthlete.id}`} activeClassName="active"> {menuAthlete.name} </Link>; })} </nav> ); } }
這個組件很是簡單, 可是有幾個須要注意的地方:
map
方法遍歷全部的運動員,給每一個人生成一個 Link
。Link
是 React Router 爲了在視圖間生成連接所提供的特殊組件。activeClassName
屬性,噹噹前路由與連接路徑匹配時會添加 active
的類。 AthletePreview
組件用在首頁顯示運動員的圖片及名稱。來看一下它的代碼:
// src/components/AthletePreview.js import React from 'react'; import { Link } from 'react-router'; export default class AthletePreview extends React.Component { render() { return ( <Link to={`/athlete/${this.props.id}`}> <div className="athlete-preview"> <img src={`img/${this.props.image}`}/> <h2 className="name">{this.props.name}</h2> <span className="medals-count"><img src="/img/medal.png"/> {this.props.medals.length}</span> </div> </Link> ); } }
代碼很是簡單。咱們打算接受許多 props 來描述運動員的特徵,好比 id
, image
, name
以及 medals
。再次注意咱們使用 Link
組件在運動員頁面建立了一個連接。
既然咱們已經建立了全部的基本組件,如今咱們開始建立那些給應用程序提供視覺結構的組件。 第一個是 Layout
組件, 它的惟一用途就是給整個應用提供展現模板,包括頁頭區、 主內容區以及頁腳區:
// src/components/Layout.js import React from 'react'; import { Link } from 'react-router'; export default class Layout extends React.Component { render() { return ( <div className="app-container"> <header> <Link to="/"> <img className="logo" src="/img/logo-judo-heroes.png"/> </Link> </header> <div className="app-content">{this.props.children}</div> <footer> <p> This is a demo app to showcase universal rendering and routing with <strong>React</strong> and <strong>Express</strong>. </p> </footer> </div> ); } }
組件很是簡單,只需看代碼就能瞭解它是如何工做的。 咱們在這裏使用了一個有趣的 props, children
屬性. 這是 React 提供給每一個組件的特殊屬性,容許在一個組件中嵌套組件。
咱們將在路由的部分看到 React Router 如何在 Layout
組件中嵌套另外一個組件。
這個組件構成了整個首頁,它包含了以前定義的一些組件:
// src/components/IndexPage.js import React from 'react'; import AthletePreview from './AthletePreview'; import athletes from '../data/athletes'; export default class IndexPage extends React.Component { render() { return ( <div className="home"> <div className="athletes-selector"> {athletes.map(athleteData => <AthletePreview key={athleteData.id} {...athleteData} />)} </div> </div> ); } }
在這個組件中咱們須要注意,咱們使用了以前定義的 AthletePreview
組件。基本上咱們在數據模塊中遍歷全部的運動員, 給每一個人建立一個 AthletePreview
組件。由於 AthletePreview
組件的數據是未知的,因此咱們須要使用 JSX 擴展操做符 ({...object}
) 來傳遞當前運動員的全部信息。
咱們用一樣的方式建立 AthletePage
組件:
// src/components/AthletePage.js import React from 'react'; import { Link } from 'react-router'; import NotFoundPage from './NotFoundPage'; import AthletesMenu from './AthletesMenu'; import Medal from './Medal'; import Flag from './Flag'; import athletes from '../data/athletes'; export default class AthletePage extends React.Component { render() { const id = this.props.params.id; const athlete = athletes.filter((athlete) => athlete.id === id)[0]; if (!athlete) { return <NotFoundPage/>; } const headerStyle = { backgroundImage: `url(/img/${athlete.cover})` }; return ( <div className="athlete-full"> <AthletesMenu/> <div className="athlete"> <header style={headerStyle}/> <div className="picture-container"> <img src={`/img/${athlete.image}`}/> <h2 className="name">{athlete.name}</h2> </div> <section className="description"> Olympic medalist from <strong><Flag code={athlete.country} showName="true"/></strong>, born in {athlete.birth} (Find out more on <a href={athlete.link} target="_blank">Wikipedia</a>). </section> <section className="medals"> <p>Winner of <strong>{athlete.medals.length}</strong> medals:</p> <ul>{ athlete.medals.map((medal, i) => <Medal key={i} {...medal}/>) }</ul> </section> </div> <div className="navigateBack"> <Link to="/">« Back to the index</Link> </div> </div> ); } }
如今, 你必定能夠理解上面的大部分代碼以及如何用其它的組件建立這個視圖。須要強調的是這個頁面組件只能從外部接受運動員的 id, 因此咱們引入數據模塊來檢索運動員的相關信息。咱們在 render
方法開始以前對數據採用了 filter
函數。咱們也考慮了接受的 id 在數據模塊中不存在的狀況。這種狀況下會渲染 NotFoundPage
組件,咱們會在後面的部分建立這個組件。
最後一個重要的細節是咱們經過 this.props.params.id
(而不是簡單的 this.props.id
)來訪問 id:當在 Route
中使用組件時, React Router 會建立一個特殊的對象 params
,而且它容許給組件傳遞路由參數。當咱們知道如何設置應用的路由部分時,這個概念更容易理解。
如今讓來看看 NotFoundPage
組件, 它是生成 404 頁面代碼的模板:
// src/components/NotFoundPage.js import React from 'react'; import { Link } from 'react-router'; export default class NotFoundPage extends React.Component { render() { return ( <div className="not-found"> <h1>404</h1> <h2>Page not found!</h2> <p> <Link to="/">Go back to the main page</Link> </p> </div> ); } }
咱們建立的最後一個組件是 AppRoutes
組件,它是使用 React Router 渲染全部視圖的主要組件。這個組件將使用 routes
模塊,讓咱們先睹爲快:
// src/routes.js import React from 'react' import { Route, IndexRoute } from 'react-router' import Layout from './components/Layout'; import IndexPage from './components/IndexPage'; import AthletePage from './components/AthletePage'; import NotFoundPage from './components/NotFoundPage'; const routes = ( <Route path="/" component={Layout}> <IndexRoute component={IndexPage}/> <Route path="athlete/:id" component={AthletePage}/> <Route path="*" component={NotFoundPage}/> </Route> ); export default routes;
在這個文件中咱們使用 React Router 的 Route
組件將路由映射到以前定義的組件中。注意如何在一個主 Route
組件中嵌套路由。我解釋一下它的原理:
/
路徑映射到 Layout
組件。這容許咱們在應用程序的每一個部分使用自定義的 layout 。在嵌套路由中定義的組件將會代替 this.props.children
屬性在 Layout
組件中被渲染,咱們在以前已經討論過。IndexRoute
,這個特殊的路由所定義的組件會在咱們瀏覽父路由(/)的索引頁時被渲染。咱們將 IndexPage
組件做爲索引路由。athlete/:id
被映射爲 AthletePage
。注意咱們使用了命名參數 :id
。因此這個路由會匹配全部前綴是 /athlete/
的路徑, 餘下的部分將關聯參數 id
並對應組件中的 this.props.params.id
。*
會將其它路徑映射到 NotFoundPage
組件。這個路由必須被定義爲最後一條 。如今看一下如何在 AppRoutes
組件中經過 React Router 使用路由:
// src/components/AppRoutes.js import React from 'react'; import { Router, browserHistory } from 'react-router'; import routes from '../routes'; export default class AppRoutes extends React.Component { render() { return ( <Router history={browserHistory} routes={routes} onUpdate={() => window.scrollTo(0, 0)}/> ); } }
基本上咱們只需導入 Router
組件,而後把它添加到 render
函數中。router 組件會在 router
屬性中接收路由的映射。咱們也定義了 history
屬性來指定要使用 HTML5 的瀏覽歷史記錄(as an alternative you could also use hashHistory).
最後咱們也添加了 onUpdate
回調函數,它的做用是每當鏈接被點擊後窗口都會滾動到頂部。
完成咱們的應用程序的首個版本的最後一部分代碼就是編寫在瀏覽器中啓動 app 的 JavaScript 邏輯代碼:
// src/app-client.js import React from 'react'; import ReactDOM from 'react-dom'; import AppRoutes from './components/AppRoutes'; window.onload = () => { ReactDOM.render(<AppRoutes/>, document.getElementById('main')); };
咱們在這裏惟一要作的就是導入 AppRoutes
組件,而後使用 ReactDOM.render
方法渲染。React app 將會在 #main
DOM 元素中生成。
在運行應用以前,咱們須要使用 Webpack 生成包含全部 React 組件的 bundle.js
組件。這個文件將會被瀏覽器執行,所以 Webpack 要確保將全部模塊轉換成能夠在大多數瀏覽器環境執行的代碼。 Webpack 會把 ES2015 和 React JSX 語法轉換成相等的 ES5 語法(使用 Babel), 這樣就能夠在每一個瀏覽器中執行。此外, 咱們可使用 Webpack 來優化最終生成的代碼,好比將全部的腳本壓縮合併成一個文件。
來寫一下 webpack 的配置文件:
// webpack.config.js const webpack = require('webpack'); const path = require('path'); module.exports = { entry: path.join(__dirname, 'src', 'app-client.js'), output: { path: path.join(__dirname, 'src', 'static', 'js'), filename: 'bundle.js' }, module: { loaders: [{ test: path.join(__dirname, 'src'), loader: ['babel-loader'], query: { cacheDirectory: 'babel_cache', presets: ['react', 'es2015'] } }] }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) }), new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false }, mangle: true, sourcemap: false, beautify: false, dead_code: true }) ] };
在配置文件的第一部分,咱們定義了文件入口以及輸出路徑。 文件入口是啓動應用的 JavaScript 文件。Webpack 會使用遞歸方法將打包進 bundle 文件的那些包含或導入的資源進行篩選。
module.loaders
部分會對特定文件進行轉化。在這裏咱們想使用 Babel 的 react
和 es2015
設置將全部引入的 JavaScript 文件轉化成 ES5 代碼。
最後一部分咱們使用 plugins
聲明及配置咱們想要使用的全部優化插件:
DefinePlugin
容許咱們在打包的過程當中將 NODE_ENV
變量定義爲全局變量,和在腳本中定義的同樣。 有些模塊 (好比 React) 會依賴於它啓用或禁用當前環境(產品或開發)的特定功能。DedupePlugin
刪除全部重複的文件 (模塊導入多個模塊).OccurenceOrderPlugin
能夠減小打包後文件的體積。UglifyJsPlugin
使用 UglifyJs 壓縮和混淆打包的文件。如今咱們已經準備好生成 bundle 文件,只需運行:
NODE_ENV=production node_modules/.bin/webpack -p
NODE_ENV
環境變量和 -p
選項用於在產品模式下生成 bundle 文件,這會應用一些額外的優化,好比在 React 庫中刪除全部的調試代碼。
若是一切運行正常,你將會在 src/static/js/bundle.js
目錄中看到 bundle 文件。
咱們已經準備好玩一玩應用程序的第一個版本了!
咱們尚未 Node.js 的 web 服務器,所以如今咱們可使用 http-server
模塊(以前安裝的開發依賴) 運行一個簡單的靜態文件服務器:
node_modules/.bin/http-server src/static
如今你的應用已經能夠在 http://localhost:8080 上運行。
好了,如今花些時間玩一玩,點擊全部的連接,瀏覽全部的部分。
一切彷佛工做正常? 嗯,是的! 只是有一些錯誤警告... 若是你在首頁以外的部分刷新頁面, 服務器會返回 404 錯誤。
解決這個問題的方法有不少。咱們會使用通用路由及渲染方案解決這個問題,因此讓咱們開始下一部分吧!
咱們如今準備將應用程序升級到下一個版本,並編寫缺乏的服務器端部分。
爲了具備服務端路由及渲染, 稍後咱們將使用 Express 編寫一個相對較小的服務端腳本。
渲染部分將使用 ejs 模板替換 index.html
文件,並保存在 src/views/index.ejs
:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Judo Heroes - A Universal JavaScript demo application with React</title> <link rel="stylesheet" href="/css/style.css"> </head> <body> <div id="main"><%- markup -%></div> <script src="/js/bundle.js"></script> </body> </html>
與原始 HTML 文件僅有的不一樣就是咱們在 #main
div 元素中使用了模板變量 <%- markup -%>
,爲了在服務端生成的 HTML 代碼中包含 React markup 。
如今咱們準備寫服務端應用:
// src/server.js import path from 'path'; import { Server } from 'http'; import Express from 'express'; import React from 'react'; import { renderToString } from 'react-dom/server'; import { match, RouterContext } from 'react-router'; import routes from './routes'; import NotFoundPage from './components/NotFoundPage'; // initialize the server and configure support for ejs templates const app = new Express(); const server = new Server(app); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // define the folder that will be used for static assets app.use(Express.static(path.join(__dirname, 'static'))); // universal routing and rendering app.get('*', (req, res) => { match( { routes, location: req.url }, (err, redirectLocation, renderProps) => { // in case of error display the error message if (err) { return res.status(500).send(err.message); } // in case of redirect propagate the redirect to the browser if (redirectLocation) { return res.redirect(302, redirectLocation.pathname + redirectLocation.search); } // generate the React markup for the current route let markup; if (renderProps) { // if the current route matched we have renderProps markup = renderToString(<RouterContext {...renderProps}/>); } else { // otherwise we can render a 404 page markup = renderToString(<NotFoundPage/>); res.status(404); } // render the index template with the embedded React markup return res.render('index', { markup }); } ); }); // start the server const port = process.env.PORT || 3000; const env = process.env.NODE_ENV || 'production'; server.listen(port, err => { if (err) { return console.error(err); } console.info(`Server running on http://localhost:${port} [${env}]`); });
代碼添加了註釋, 因此不難理解其中原理。
其中重要的代碼就是使用 app.get('*', (req, res) => {...})
定義的 Express 路由。 這是一個 Express catch-all 路由,它會在服務端將全部的 GET 請求編譯成 URL 。 在這個路由中, 咱們使用 React Router match
函數來受權路由邏輯。
ReactRouter.match
接收兩個參數:第一個參數是配置對象,第二個是回調函數。配置對象須要有兩個鍵值:
routes
: 用於傳遞 React Router 的路由配置。在這裏,咱們傳遞用於服務端渲染的相同配置。location
: 這是用來指定當前請求的 URL 。回調函數在匹配結束時調用。它接收三個參數, error
, redirectLocation
以及 renderProps
, 咱們能夠經過這些參數肯定匹配的結果。
咱們可能有四種須要處理的狀況:
renderProps
對象參數包含了咱們須要渲染組件的數據。咱們須要渲染的組件是 RouterContext
(包含在 React Router 模塊中),這就是使用 renderProps
中的值渲染整個組件樹的緣由。這是服務器端路由機制的核心,咱們使用 ReactDOM.renderToString
函數渲染與當前路由匹配的組件的 HTML 代碼。
最後,咱們將產生的 HTML 代碼注入到咱們以前編寫的 index.ejs
模板中,這樣就能夠獲得發送到瀏覽器的 HTML 頁面。
如今咱們準備好運行 server.js
腳本,可是由於它使用 JSX 語法,因此咱們不能簡單的使用 node
編譯器運行。咱們須要使用 babel-node
以及以下的完整的命令 (從項目的根文件夾) :
NODE_ENV=production node_modules/.bin/babel-node --presets 'react,es2015' src/server.js
如今你的應用已經能夠在 http://localhost:3000 上運行,由於是教程,項目到此就算完成了。
再次任意地檢查應用,並嘗試全部的部分和連接。你會注意到這一次咱們能夠刷新每一頁而且服務器可以識別當前路由並呈現正確的頁面。
小建議: 不要忘了輸入一個隨意的不存在的 URL 來檢查 404 頁面!