摘要:后面會利用這個框架來做實踐。接下來就是我們要繼續探討的同構同構數據處理的探討我們都知道,瀏覽器端獲取數據需要發起請求,實際上發起的請求就是對應服務端一個路由控制器。是有生命周期的,官方給我們指出的綁定,應該在里來進行。
眾所周知,目前的 WEB 應用,用戶體驗要求越來越高,WEB 交互變得越來越豐富!前端可以做的事越來越多,去年 Node 引領了前后端分層的浪潮,而 React 的出現讓分層思想可以更多徹底的執行,尤其是 React 同構 (Universal or Isomorphic) 這個黑科技到底是怎么實現的,我們來一探究竟。
React 服務端方法如果熟悉 React 開發,那么一定對 ReactDOM.render 方法不陌生,這是 React 渲染到 DOM 中的方法。
現有的任何開發模式都離不開 DOM 樹,如圖:
服務端渲染就要稍作改動,如圖:
比較兩張圖可以看出,服務端渲染需要把 React 的初次渲染放到服務端,讓 React 幫我們把業務 component 翻譯成 string 類型的 DOM 樹,再通過后端語言的 IO 流輸出至瀏覽器。
我們來看 React 官方給我們提供的服務端渲染的API:
React.renderToString 是把 React 元素轉成一個 HTML 字符串,因為服務端渲染已經標識了 reactid,所以在瀏覽器端再次渲染,React 只是做事件綁定,而不會將所有的 DOM 樹重新渲染,這樣能帶來高性能的頁面首次加載!同構黑魔法主要從這個 API 而來。
React.renderToStaticMarkup,這個 API 相當于一個簡化版的 renderToString,如果你的應用基本上是靜態文本,建議用這個方法,少了一大批的 reactid,DOM 樹自然精簡了,在 IO 流傳輸上節省一部分流量。
配合 renderToString 和 renderToStaticMarkup 使用,createElement 返回的 ReactElement 作為參數傳遞給前面兩個方法。
React 玩轉 Node有了解決方案,我們就可以動手在 Node 來做一些事了。后面會利用 KOA 這個 Node 框架來做實踐。
我們新建應用,目錄結構如下,
react-server-koa-simple ├── app │ ├── assets │ │ ├── build │ │ ├── src │ │ │ ├── img │ │ │ ├── js │ │ │ └── css │ │ ├── package.json │ │ └── webpack.config.js │ ├── middleware │ │ └── static.js(前端靜態資源托管中間件) │ ├── plugin │ │ └── reactview(reactview 插件) │ └── views │ ├── layout │ │ └── Default.js │ ├── Device.js │ └── Home.js ├── .babelrc ├── .gitgnore ├── app.js ├── package.json └── README.md
首先,我們需要實現一個 KOA 插件,用來實現 React 作為服務端模板的渲染工作,方法是將 render 方法插入到 app 上下文中,目的是在 controller 層中調用,this.render(viewFileName, props, children) 并通過 this.body 輸出文檔流至瀏覽器端。
/* * koa-react-view.js * 提供 react server render 功能 * { * options : { * viewpath: viewpath, // the root directory of view files * doctype: "", * extname: ".js", // view層直接渲染文件名后綴 * writeResp: true, // 是否需要在view層直接輸出 * } * } */ module.exports = function(app) { const opts = app.config.reactview || {}; assert(opts && opts.viewpath && util.isString(opts.viewpath), "[reactview] viewpath is required, please check config!"); const options = Object.assign({}, defaultOpts, opts); app.context.render = function(filename, _locals, children) { let filepath = path.join(options.viewpath, filename); let render = opts.internals ? ReactDOMServer.renderToString : ReactDOMServer.renderToStaticMarkup; // merge koa state let props = Object.assign({}, this.state, _locals); let markup = options.doctype || ""; try { let component = require(filepath); // Transpiled ES6 may export components as { default: Component } component = component.default || component; markup += render(React.createElement(component, props, children)); } catch (err) { err.code = "REACT"; throw err; } if (options.writeResp) { this.type = "html"; this.body = markup; } return markup; }; };
然后,我們來寫用 React 實現的服務端的 Components,
/* * react-server-koa-simple - app/views/Home.js * home模板 */ render() { let { microdata, mydata } = this.props; let homeJs = `${microdata.styleDomain}/build/${microdata.styleVersion}/js/home.js`; let scriptUrls = [homeJs]; return (); }
這里做了幾件事,初始化 DOM 樹,用 data 屬性作服務端數據埋點,渲染前后端公共 Content 模塊,引用前端模塊
而客戶端,我們就可以很方便地拿到了服務端的數據,可以直接拿來使用,
import ReactDOM from "react-dom"; import Content from "./components/Content.js"; const microdata = JSON.parse(appEle.getAttribute("data-microdata")); const mydata = JSON.parse(appEle.getAttribute("data-mydata")); ReactDOM.render(, document.getElementById("demoApp") );
然后,到了啟動一個簡單的 koa 應用的時候,完善入口 app.js 來驗證我們的想法,
const koa = require("koa"); const koaRouter = require("koa-router"); const path = require("path"); const reactview = require("./app/plugin/reactview/app.js"); const Static = require("./app/middleware/static.js"); const App = ()=> { let app = koa(); let router = koaRouter(); // 初始化 /home 路由 dispatch 的 generator router.get("/home", function*() { // 執行view插件 this.body = this.render("Home", { microdata: { domain: "http://localhost:3000" }, mydata: { nick: "server render body" } }); }); app.use(router.routes()).use(router.allowedMethods()); // 注入 reactview const viewpath = path.join(__dirname, "app/views"); app.config = { reactview: { viewpath: viewpath, // the root directory of view files doctype: "", extname: ".js", // view層直接渲染文件名后綴 beautify: true, // 是否需要對dom結構進行格式化 writeResp: false, // 是否需要在view層直接輸出 } } reactview(app); return app; }; const createApp = ()=> { const app = App(); // http服務端口監聽 app.listen(3000, ()=> { console.log("3000 is listening!"); }); return app; }; createApp();
現在,訪問上面預先設置好的路由,http://localhost:3000/home 來驗證 server render,
服務端:
瀏覽器端:
react-router 和 koa-router 統一我們已經建立了服務端渲染的基礎了,接著再考慮下如何把后端和前端的路由做統一。
假設我們的路由設置成 /device/:deviceID 這種形式,
那么服務端是這么來實現的,
// 初始化 device/:deviceID 路由 dispatch 的 generator router.get("/device/:deviceID", function*() { // 執行view插件 let deviceID = this.params.deviceID; this.body = this.render("Device", { isServer: true, microdata: microdata, mydata: { path: this.path, deviceID: deviceID, } }); });
以及服務端 View 模板,
render() { const { microdata, mydata, isServer } = this.props; const deviceJs = `${microdata.styleDomain}/build/${microdata.styleVersion}/js/device.js`; const scriptUrls = [deviceJs]; return (); }
前端 app 入口:app.js
function getServerData(key) { return JSON.parse(appEle.getAttribute(`data-${key}`)); }; // 從服務端埋點處獲取 microdata, mydata let microdata = getServerData("microdata"); let mydata = getServerData("mydata"); ReactDOM.render(, document.getElementById("demoApp")); 前后端公用的 Iso.js 模塊,前端路由同樣設置成 /device/:deviceID:
class Iso extends Component { static propTypes = { // ... }; // 包裹 Route 的 Component,目的是注入服務端傳入的 props wrapComponent(Component) { const { microdata, mydata } = this.props; return React.createClass({ render() { return React.createElement(Component, { microdata: microdata, mydata: mydata }, this.props.children); } }); } // LayoutView 為路由的布局; DeviceView 為參數處理模塊 render() { const { isServer, mydata } = this.props; return (); } } 這樣我就實現了服務端和前端路由的同構!
無論你是初次訪問這些資源路徑: /device/all, /device/pc, /device/wireless,還是在頁面手動切換這些資源路徑效果都是一樣的,既保證了初次渲染有符合預期的 DOM 輸出的用戶體驗,又保證了代碼的簡潔性,最重要的是前后端代碼是一套,并且由一位工程師開發,有沒有覺得很棒?
其中注意幾點:
Iso 的 render 模塊需要判斷isServer,服務端用createMemoryHistory,前端用browserHistory;
react-router 的 component 如果需要注入 props 必須對其進行包裹 wrapComponent。因為服務端渲染的數據需要通過傳 props 的方式,而react-router-route 只提供了 component,并不支持繼續追加 props。截取 Route 的源碼,
propTypes: { path: string, component: _PropTypes.component, components: _PropTypes.components, getComponent: func, getComponents: func },為什么服務端獲取數據不和前端保持一致,在 Component 里作數據綁定,使用 fetchData 和數據綁定!只能說,你可以大膽的假設。接下來就是我們要繼續探討的同構model!
同構數據處理的探討我們都知道,瀏覽器端獲取數據需要發起 ajax 請求,實際上發起的請求 URL 就是對應服務端一個路由控制器。
React 是有生命周期的,官方給我們指出的綁定 Model,fetchData 應該在 componentDidMount 里來進行。在服務端,React 是不會去執行componentDidMount 方法的,因為,React 的 renderTranscation 分成兩塊: ReactReconcileTransaction 和ReactServerRenderingTransaction,其在服務端的實現移除掉了在瀏覽器端的一些特定方法。
而服務端處理數據是線性的,是不可逆的,發起請求 > 去數據庫獲取數據 > 業務邏輯處理 > 組裝成 html-> IO流輸出給瀏覽器。顯然,服務端和瀏覽器端是矛盾的!
實驗的方案你或許會想到利用 ReactClass 提供的 statics 來做點文章,React 確實提供了入口,不僅能包裹靜態屬性,還能包裹靜態方法,并且能 DEFINE_MANY:
/** * An object containing properties and methods that should be defined on * the component"s constructor instead of its prototype (static methods). * * @type {object} * @optional */ statics: SpecPolicy.DEFINE_MANY,利用 statics 把我們的組件擴展成這樣,
class ContentView extends Component { statics: { fetchData: function (callback) { ContentData.fetch().then((data)=> { callback(data); }); } }; // 瀏覽器端這樣獲取數據 componentDidMount() { this.constructor.fetchData((data)=> { this.setState({ data: data }); }); } ... });ContentData.fetch() 需要實現兩套:
服務端:封裝服務端service層方法
瀏覽器端:封裝ajax或Fetch方法
服務端調用:
require("ContentView").fetchData((data)=> { this.body = this.render("Device", { isServer: true, microdata: microdata, mydata: data }); });這樣可以解決數據層的同構!但我并不認為這是一個好的方法,好像回到 JSP 時代。
我們團隊現在使用的方法:
參考資料本文完整運行的 例子
https://facebook.github.io/react/docs/getting-started.html
https://github.com/facebook/react
https://github.com/rackt/react-router
https://github.com/koajs/koa
https://github.com/alexmingoia/koa-router
https://github.com/koajs/react-view
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/79019.html
摘要:同構的關鍵要素完善的屬性及生命周期與客戶端的時機是同構的關鍵。的一致性在前后端渲染相同的,將輸出一致的結構。以上便是在同構服務端渲染的提供的基礎條件。可以將封裝至的中,在服務端上生成隨機數并傳入到這個中,從而保證隨機數在客戶端和服務端一致。 原文地址 React 的實踐從去年在 PC QQ家校群開始,由于 PC 上的網絡及環境都相當好,所以在使用時可謂一帆風順,偶爾遇到點小磕絆,也能夠...
摘要:今天,其實講的是在實現同構過程中看到過,可能非常容易被忽視更小的一個點。每一個架構的框架都會涉及到層的展現,也不例外。這種說法即對也不對。總結其實,實現非常簡單,我們也從一些維度看到了設計一個的一般方法。 在之前我們有過一篇『React 同構實踐與思考』的專欄文章,給讀者實踐了用 React 怎么實現同構。今天,其實講的是在實現同構過程中看到過,可能非常容易被忽視更小的一個點 —— R...
摘要:前端每周清單第期微服務實踐,與,組件技巧,攻防作者王下邀月熊編輯徐川前端每周清單專注前端領域內容,以對外文資料的搜集為主,幫助開發者了解一周前端熱點分為新聞熱點開發教程工程實踐深度閱讀開源項目巔峰人生等欄目。 前端每周清單第 26 期:Node.js 微服務實踐,Vue.js 與 GraphQL,Angular 組件技巧,HeadlessChrome 攻防 作者:王下邀月熊 編輯:徐川...
摘要:盤點一下,模式反應了典型的控制權問題。異步狀態管理與控制權提到控制權話題,怎能少得了這樣的狀態管理工具。狀態管理中的控制主義和極簡主義了解了異步狀態中的控制權問題,我們再從全局角度進行分析。 控制權——這個概念在編程中至關重要。比如,輪子封裝層與業務消費層對于控制權的爭奪,就是一個很有意思的話題。這在 React 世界里也不例外。表面上看,我們當然希望輪子掌控的事情越多越好:因為抽象層...
摘要:盤點一下,模式反應了典型的控制權問題。異步狀態管理與控制權提到控制權話題,怎能少得了這樣的狀態管理工具。狀態管理中的控制主義和極簡主義了解了異步狀態中的控制權問題,我們再從全局角度進行分析。 控制權——這個概念在編程中至關重要。比如,輪子封裝層與業務消費層對于控制權的爭奪,就是一個很有意思的話題。這在 React 世界里也不例外。表面上看,我們當然希望輪子掌控的事情越多越好:因為抽象層...
閱讀 2000·2023-04-25 16:53
閱讀 1442·2021-10-13 09:39
閱讀 606·2021-09-08 09:35
閱讀 1639·2019-08-30 13:03
閱讀 2121·2019-08-30 11:06
閱讀 1831·2019-08-30 10:59
閱讀 3188·2019-08-29 17:00
閱讀 2288·2019-08-23 17:55