摘要:使用做同構(gòu)應(yīng)用是用于開(kāi)發(fā)數(shù)據(jù)不斷變化的大型應(yīng)用程序的前端框架,結(jié)合其他輪子例如和就可以開(kāi)發(fā)大型的前端應(yīng)用。然后客戶端檢測(cè)到這些已經(jīng)生成的就不會(huì)重新渲染,直接使用現(xiàn)有的結(jié)構(gòu)。
使用React做同構(gòu)應(yīng)用
React是用于開(kāi)發(fā)數(shù)據(jù)不斷變化的大型應(yīng)用程序的前端view框架,結(jié)合其他輪子例如redux和react-router就可以開(kāi)發(fā)大型的前端應(yīng)用。
React開(kāi)發(fā)之初就有一個(gè)特別的優(yōu)勢(shì),就是前后端同構(gòu)。
什么是前后端同構(gòu)呢?就是前后端都可以使用同一套代碼生成頁(yè)面,頁(yè)面既可以由前端動(dòng)態(tài)生成,也可以由后端服務(wù)器直接渲染出來(lái)
最簡(jiǎn)單的同構(gòu)應(yīng)用其實(shí)并不復(fù)雜,復(fù)雜的是結(jié)合webpack,router之后的各種復(fù)雜狀態(tài)不容易解決
一個(gè)極簡(jiǎn)單的小例子html
React同構(gòu) <%- reactOutput %>
js
import path from "path"; import Express from "express"; import AppRoot from "../app/components/AppRoot" import React from "react"; import {renderToString} from "react-dom/server" var app = Express(); var server; const PATH_STYLES = path.resolve(__dirname, "../client/styles"); const PATH_DIST = path.resolve(__dirname, "../../dist"); app.use("/styles", Express.static(PATH_STYLES)); app.use(Express.static(PATH_DIST)); app.get("/", (req, res) => { var reactAppContent = renderToString(); console.log(reactAppContent); res.render(path.resolve(__dirname, "../client/index.ejs"), {reactOutput: reactAppContent}); }); server = app.listen(process.env.PORT || 3000, () => { var port = server.address().port; console.log("Server is listening at %s", port); });
你看服務(wù)端渲染的原理就是,服務(wù)端調(diào)用react的renderToString方法,在服務(wù)器端生成文本,插入到html文本之中,輸出到瀏覽器客戶端。然后客戶端檢測(cè)到這些已經(jīng)生成的dom,就不會(huì)重新渲染,直接使用現(xiàn)有的html結(jié)構(gòu)。
然而現(xiàn)實(shí)并不是這么單純,使用react做前端開(kāi)發(fā)的應(yīng)該不會(huì)不使用webpack,React-router,redux等等一些提高效率,簡(jiǎn)化工作的一些輔助類庫(kù)或者框架,這樣的應(yīng)用是不是就不太好做同構(gòu)應(yīng)用了?至少不會(huì)向上文這么簡(jiǎn)單吧?
做當(dāng)然是可以做的,但復(fù)雜度確實(shí)也大了不少
結(jié)合框架的例子 webpack-isomorphic-tools這個(gè)webpack插件的主要作用有兩點(diǎn)
獲取webpack打包之后的入口文件路徑,包括js,css
把一些特殊的文件例如大圖片、編譯之后css的映射保存下來(lái),以便在服務(wù)器端使用
webpack配置文件
import path from "path"; import webpack from "webpack"; import WebpackIsomorphicToolsPlugin from "webpack-isomorphic-tools/plugin"; import ExtractTextPlugin from "extract-text-webpack-plugin"; import isomorphicToolsConfig from "../isomorphic.tools.config"; import {client} from "../../config"; const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(isomorphicToolsConfig) const cssLoader = [ "css?modules", "sourceMap", "importLoaders=1", "localIdentName=[name]__[local]___[hash:base64:5]" ].join("&") const cssLoader2 = [ "css?modules", "sourceMap", "importLoaders=1", "localIdentName=[local]" ].join("&") const config = { // 項(xiàng)目根目錄 context: path.join(__dirname, "../../"), devtool: "cheap-module-eval-source-map", entry: [ `webpack-hot-middleware/client?reload=true&path=http://${client.host}:${client.port}/__webpack_hmr`, "./client/index.js" ], output: { path: path.join(__dirname, "../../build"), filename: "index.js", publicPath: "/build/", chunkFilename: "[name]-[chunkhash:8].js" }, resolve: { extensions: ["", ".js", ".jsx", ".json"] }, module: { preLoaders: [ { test: /.jsx?$/, exclude: /node_modules/, loader: "eslint-loader" } ], loaders: [ { test: /.jsx?$/, loader: "babel", exclude: [/node_modules/] }, { test: webpackIsomorphicToolsPlugin.regular_expression("less"), loader: ExtractTextPlugin.extract("style", `${cssLoader}!less`) }, { test: webpackIsomorphicToolsPlugin.regular_expression("css"), exclude: [/node_modules/], loader: ExtractTextPlugin.extract("style", `${cssLoader}`) }, { test: webpackIsomorphicToolsPlugin.regular_expression("css"), include: [/node_modules/], loader: ExtractTextPlugin.extract("style", `${cssLoader2}`) }, { test: webpackIsomorphicToolsPlugin.regular_expression("images"), loader: "url?limit=10000" } ] }, plugins: [ new webpack.HotModuleReplacementPlugin(), new ExtractTextPlugin("[name].css", { allChunks: true }), webpackIsomorphicToolsPlugin ] } export default config
webpack-isomorphic-tools 配置文件
import WebpackIsomorphicToolsPlugin from "webpack-isomorphic-tools/plugin" export default { assets: { images: { extensions: ["png", "jpg", "jpeg", "gif", "ico", "svg"] }, css: { extensions: ["css"], filter(module, regex, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log) } return regex.test(module.name) }, path(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log); } return module.name }, parser(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log); } return module.source } }, less: { extensions: ["less"], filter: function(module, regex, options, log) { if (options.development) { return webpack_isomorphic_tools_plugin.style_loader_filter(module, regex, options, log) } return regex.test(module.name) }, path: function(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log); } return module.name }, parser: function(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log); } return module.source } } } }
這些文件配置好之后,當(dāng)再運(yùn)行webpack打包命令的時(shí)候就會(huì)生成一個(gè)叫做webpack-assets.json
的文件,這個(gè)文件記錄了剛才生成的如文件的路徑以及css,img映射表
客戶端的配置到這里就結(jié)束了,來(lái)看下服務(wù)端的配置
服務(wù)端的配置過(guò)程要復(fù)雜一些,因?yàn)樾枰褂玫?b>WebpackIsomorphicToolsPlugin生成的文件,
我們直接使用它對(duì)應(yīng)的服務(wù)端功能就可以了
import path from "path" import WebpackIsomorphicTools from "webpack-isomorphic-tools" import co from "co" import startDB from "../../server/model/" import isomorphicToolsConfig from "../isomorphic.tools.config" const startServer = require("./server") var basePath = path.join(__dirname, "../../") global.webpackIsomorphicTools = new WebpackIsomorphicTools(isomorphicToolsConfig) // .development(true) .server(basePath, () => { const startServer = require("./server") co(function *() { yield startDB yield startServer }) })
一定要在WebpackIsomorphicTools初始化之后再啟動(dòng)服務(wù)器
文章開(kāi)頭我們知道react是可以運(yùn)行在服務(wù)端的,其實(shí)不光是react,react-router,redux也都是可以運(yùn)行在服務(wù)器端的
既然前端我們使用了react-router,也就是前端路由,那后端又怎么做處理呢
其實(shí)這些react-router在設(shè)計(jì)的時(shí)候已經(jīng)想到了這些,設(shè)計(jì)了一個(gè)api: match
match({routes, location}, (error, redirectLocation, renderProps) => { matchResult = { error, redirectLocation, renderProps } })
match方法在服務(wù)器端解析了當(dāng)前請(qǐng)求路由,獲取了當(dāng)前路由的對(duì)應(yīng)的請(qǐng)求參數(shù)和對(duì)應(yīng)的組件
知道了這些還不足以做服務(wù)端渲染啊,比如一些頁(yè)面自己作為一個(gè)組件,是需要在客戶端向服務(wù)
器發(fā)請(qǐng)求,獲取數(shù)據(jù)做渲染的,那我們?cè)趺窗唁秩竞脭?shù)據(jù)的頁(yè)面輸出出來(lái)呢?
那就是需要做一個(gè)約定,就是前端多帶帶放置一個(gè)獲取數(shù)據(jù),渲染頁(yè)面的方法,由后端可以調(diào)用,這樣邏輯就可以保持一份,
保持好的維護(hù)性
但是怎么實(shí)現(xiàn)呢?實(shí)現(xiàn)的過(guò)程比較簡(jiǎn)單,想法比較繞
1.調(diào)用的接口的方式必須前端通用
2.渲染頁(yè)面的方式必須前后端通用
先來(lái)第一個(gè),大家都知道前端調(diào)用接口的方式通過(guò)ajax,那后端怎么使用ajax呢?有一個(gè)庫(kù)封裝了服務(wù)器端的
fetch方法實(shí)現(xiàn),可以用來(lái)做這個(gè)
由于ajax方法需要前后端通用,那就要求這個(gè)方法里面不能夾雜著客戶端或者服務(wù)端特有的api
調(diào)用。
還有個(gè)很重要的問(wèn)題,就是權(quán)限的問(wèn)題,前端有時(shí)候是需要登錄之后才可以調(diào)用的接口,后端直接調(diào)用
顯然是沒(méi)有cookie的,怎么辦呢?解決辦法就是在用戶第一個(gè)請(qǐng)求進(jìn)來(lái)之后保存cookie甚至是全部的http
頭信息,然后把這些信息傳進(jìn)fetch方法里面去
通用組件方法必須寫成類的靜態(tài)成員,否則后端獲取不到,名稱也必須統(tǒng)一
static getInitData (params = {}, cookie, dispatch, query = {}) { return getList({ ...params, ...query }, cookie) .then(data => dispatch({ type: constants.article.GET_LIST_VIEW_SUCCESS, data: data })) }
再看第二個(gè)問(wèn)題,前端渲染頁(yè)面自然就是改變state或者傳入props就可以更新視圖,服務(wù)器端怎么辦呢?
redux是可以解決這個(gè)問(wèn)題的
因?yàn)榉?wù)器端不像前端,需要在初始化之后再去更新視圖,服務(wù)器端只需要先把數(shù)據(jù)準(zhǔn)備好,然后直接一遍生成
視圖就可以了,所以上圖的dispatch方法是由前后端都可以傳入
渲染頁(yè)面的后端方法就比較簡(jiǎn)單了
import React, { Component, PropTypes } from "react" import { renderToString } from "react-dom/server" import {client} from "../../config" export default class Html extends Component { get scripts () { const { javascript } = this.props.assets return Object.keys(javascript).map((script, i) =>