最近花了點時間研究React同構實踐,遇上過年回家,心也散了,兩個月沒寫文章。javascript
同構也算是前端的一個應用模式,目的是爲了加速首屏顯示時間和seo優化,不少公司都將同構做爲前端優化的一個優化點來作,同時Raect16版本中也添加了不少對同構的支持,能夠看出FB也是默認支持這一場景使用的。css
一套代碼既能夠在服務端運行又能夠在客戶端運行,這就是同構應用。簡而言之, 就是服務端直出和客戶端渲染的組合, 可以充分結合二者的優點,並有效避免二者的不足。html
歸納地說,同構就是服務端(Node)替客戶端請求接口,獲取到數據後,將有數據和結構的頁面渲染好以後返回給客戶端,這樣避免了客戶端頁面首次渲染,同時服務端RPC比客戶端請求要快。前端
SPA:服務端替客戶端請求數據,完成第一次render,將render完成以後的html頁面返回給客戶端,相對於客戶端渲染,客戶端第一次獲取的html是個有數據有結構的html,結合樣式文件下載客戶端能夠較快的看到首屏內容。java
SSR:服務端Node也能夠運行React解析出頁面內容,而且要比客戶端更快;客戶端一般要在render一次以後請求數據,數據返回以後再render一次,服務端渲染能夠解決客戶端重複渲染問題。node
以一個常見的場景爲例:react
進入頁面,componentDidMount中請求數據,同時頁面loading,請求返回後,取消loading,頁面可交互。webpack
SPA:git
SSR:es6
經過上述流程圖可發現,理論上同構要比客戶端渲染要快,並且體驗要好。
原理了解以後,動手以前思考一些可能出現的問題:
1. Node服務器如何識別es6以及React
Node識別ES6可使用babel-register
插件,該插件使用起來跟.babelrc
同樣簡便。
React中有一個renderToString
方法,該方法將解析好的jsx片斷以html字符串形式輸出,就是爲了同構而誕生。
2. 服務端如何引入js,css,圖片,字體等靜態資源
實現方法有多種,我這裏使用webpack-isomorphic-tools
插件來實現,以後會作介紹。
3. 服務端如何路由匹配
一般咱們只作首頁,或者關鍵頁面的服務端渲染,至關於從首頁進去是服務端渲染,可是從項目其餘頁面進入就跟正常的SPA同樣。因此在服務端將要ssr的路由匹配出來,其餘的路由仍交給SPA。
4. SSR的Redux怎麼辦
一般來說,咱們從接口獲取數據,都要將一些數據放到store中,便於其餘頁面共享。
SSR中,服務端跟SPA公用一部分action和reducer,相同的reducer生成的store是同樣的,以後再經過createStore時候將store注入進入,返回給客戶端。
5. SSR的開發流程怎樣
實際上SSR開發一般是在一個項目基礎上改,而不是從新搭建一個項目,比較不少人拿它當作優化,而不是重構。
一般來講咱們一個項目按照SPA模式開發,針對特定頁面作SSR的修改,修改以後的項目既能夠SPA也能夠SSR,只不過SPA模式時對應頁面獲取不到數據,由於獲取數據的方法均被修改。
6. SSR以後,項目的JS體積是否會減少
不會減少,所謂同構,其實就是服務端藉助客戶端的JS去渲染頁面,沒有影響到客戶端的JS,仍是正常打包,客戶端作代碼分割也不會受影響。
帶着上面的問題來看同構如何實現。
React實現同構方法有多重,並且都較爲成熟,這裏選用的webpack-isomorphic-tools
插件來實現。
先插一嘴,如今有個叫Next框架,索性也試了下,真的很簡便,快速搭建SSR項目,可是問題也很明顯:
因此沒有選用該框架進行嘗試,不過該框架憑藉着簡單易上手,將來仍是頗有市場的。
上面第二個問題就提到了服務端如何處理靜態資源,這裏使用webpack-isomorphic-tools
插件,該插件處理靜態資源的。
首先一個webpack-isomorphic-tools-configuration.js
文件配置你想要處理的文件格式與處理方法,跟webpack配置loader相似:
const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); const config = require('../config/'); module.exports = { webpack_assets_file_path: `${config.base_path}/webpack-assets.json`, webpack_stats_file_path: `${config.base_path}/webpack-stats.json`, assets: { images: { extensions: ['png', 'jpg', 'gif', 'ico', 'svg'] }, fonts: { extensions: ['woff', 'woff2', 'eot', 'ttf', 'swf', 'otf'] }, // styles: { // extensions: ['scss', 'css'], // filter: function(module, regex, options, log) { // if (options.development) { // // in development mode there's webpack "style-loader", // // so the module.name is not equal to module.name // return webpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log); // } else { // // in production mode there's no webpack "style-loader", // // so the module.name will be equal to the asset path // return regex.test(module.name); // } // }, // // How to correctly transform kinda weird `module.name` // // of the `module` created by Webpack "css-loader" // // into the correct asset path: // path: webpackIsomorphicToolsPlugin.style_loader_path_extractor, // // // How to extract these Webpack `module`s' javascript `source` code. // // Basically takes `module.source` and modifies its `module.exports` a little. // parser: webpackIsomorphicToolsPlugin.css_loader_parser // } } }
該文件配置能夠參考官方文檔。
而後在webpack中,配置對應的資源的時候,引入該文件
// 同構處理靜態資源的插件 const webpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); const webpackIsomorphicToolsPluginIns = new webpackIsomorphicToolsPlugin(require('./webpack-isomorphic-tools-configuration')).development(); ... module: { rules: [ ... { test: webpackIsomorphicToolsPluginIns.regular_expression('images'), loader: 'url-loader?limit=8192', // 這樣在小於8K的圖片將直接以base64的形式內聯在代碼中,能夠減小一次http請求。 options: { name: 'assets/images/[name]_[hash:8].[ext]' } }, { test: webpackIsomorphicToolsPluginIns.regular_expression('fonts'), loader: 'url-loader', options: { name: 'assets/fonts/[name].[ext]' } } ] } plugins: [ webpackIsomorphicToolsPluginIns, .... ]
而後運行webpack,將文件打包以後,會生成一個webpack-assets.json
文件,該文件就是存儲靜態資源的映射關係的json:
{ "javascript": { "app": "/assets/js/app_360a53bf78ee0e398bb2.js", "vendor": "/assets/js/vendor_360a53bf78ee0e398bb2.js" }, "styles": { "app": "/assets/css/app_360a53bf78ee0e398bb2.css" }, "assets": { "./public/images/react.svg": "data...." }, "webpack": { "version": "2.7.0" } }
這樣咱們經過該json文件就能夠獲取到對應靜態資源。
這裏選用Express框架做爲服務器,緣由就是簡單,不少人也選用koa,都同樣。
這裏服務端啓動部分跟正常的Express啓動相似:
import render from "./render"; import fetch from "./fetch"; app.use('*', (req, res, next) => { const { promises, store } = fetch(req); Promise.all(promises).then(data => { const html = render(req, res, store); res.send(html); }).catch(err =>{ console.log('err'); console.log(err); res.end('server error,please visit later') }) });
核心在於路由部分:這裏匹配全部路由(也能夠是首頁路由),使用fetch
方法獲取到了promises
和store
,而後再用render
方法生成了html返回給客戶端,這兩個方法都是封裝過的,
咱們在SPA開發中,請求通常都封裝成actionCreator,方便調用與修改,SSR中就共用了actionCreator和reducer。
fetch
方法以下:
import 'isomorphic-fetch'; import { createStore } from "redux"; import {actions} from '../src/actions/'; import reducer from "../src/reducers"; const fetchHomeList = (store) => { return fetch('http://localhost:9000/api/aaa') .then((response)=>{ console.log('then response------'); return response.json(); }) .then((res)=>{ console.log(res.data.length); store.dispatch(actions.updateHomeList(res.data)); return res; }) .catch((res)=>{ console.log('catch res------'); console.log(res); }); }; export default function (req) { const store = createStore(reducer); const promises = [ fetchHomeList(store) ]; return { promises, store } }
在fetch
文件中,咱們將首頁須要獲取的數據經過isomorphic-fetch
來獲取,而後跟SPA同樣,dispatch
到store
中,而後暴露出去。
SSR返回的是首次渲染事後的html,首次渲染就是在render
方法中實現的:
import fs from 'fs' import path from 'path' import React from 'react'; // import ReactDOM from 'react-dom'; import { StaticRouter as Router } from "react-router-dom" import { renderToString } from "react-dom/server" import { Provider } from "react-redux" import Routes from '../src/route'; function getAssets() { return getAssets.assets || (() => { getAssets.assets = JSON.parse(fs.readFileSync(path.join(__dirname, '../webpack-assets.json'))); return getAssets.assets })() } export default function render(req, res, store) { const context = {}; const html = renderToString( <Provider store={store}> <Router location={req.baseUrl} context={context}> <Routes /> </Router> </Provider> ); // <Route>中訪問/,重定向到/home路由時 if (context.url) { res.redirect('/home'); return; } const main = getAssets(); const app = main.javascript.app; const vendor = main.javascript.vendor; const style = main.styles.app; return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <link href=${style} rel="stylesheet"></link> <title>SSR</title> </head> <body> <div id="root"> ${html} </div> </body> <script> window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())} </script> <script src=${vendor}></script> <script src=${app}></script> </html> ` }
這裏顯而易見,咱們準備一段html模板,跟SPA那個html模板相似,將renderToString
的片斷塞進去,同時根據webpack-assets.json
獲取到打包好的js和css,塞進去;最後,將上面剛剛配置好的store注入進去。
咱們製做一個接口,使用setTimeout 500ms模擬網絡開銷,效果以下:
(gif上傳不上去不知道爲啥。。。)
能夠看到SSR要比SPA明顯的更快速獲得首屏效果。
項目完成的同時,也在思考一些問題:
1. 既然SSR首屏速度快,爲什麼不全部路由全都SSR
全部頁面SSR能夠作,這樣每一個頁面的首屏都會很快,同時js也會小不少。可是帶來的問題服務器壓力會很大,維護起來成本較高。並且服務端畢竟是模擬客戶端環境渲染,一些地方仍是不同的好比沒有Document,沒有window對象,沒法進行DOM操做等。因此推薦首頁等重要頁面進行SSR。
2. 若是接口時間過長,是否是白屏時間較長
確實有這個問題,理論上講,RPC要比客戶端請求快不少,這樣能夠節省不少時間;可是若是接口很慢會形成白屏時間過長,得不償失。因此接口很慢的頁面不建議作SSR,同時接口也應該有嚴格的規範控制接口返回時間。
3. 若是項目首頁有很重的邏輯,或者Layout中有重邏輯該如何
頁面若是有很重的邏輯好比判斷不少不一樣條件,作出不少相應處理;依次請求不少接口,或者一塊兒請求大量數據等狀況,這些邏輯處理都須要一同寫進SSR中。
4. Node服務器帶來的維護及併發壓力等問題
使用Node服務器的話,還涉及到服務器的平常維護問題,日誌收集,錯誤報警等問題,以及性能問題。要求前端(SA)有必定的Node服務器的維護經驗,這時前端已經不是純前端了。
5. 什麼項目適合SSR
這個問題纔是關鍵的問題。並非全部項目都適合SSR,就好像不是全部項目都適合Redux同樣。根據SSR特色適合場景:
歡迎你們提出些不一樣意見用以討論。