使用React做同構應用
React是用於開發資料不斷變化的大型應用程式的前端view框架,結合其他輪子例如redux
和react-router
就可以開發大型的前端應用。
React開發之初就有一個特別的優勢,就是前後端同構。
什麼是前後端同構呢?就是前後端都可以使用同一套程式碼生成頁面,頁面既可以由前端動態生成,也可以由後端伺服器直接渲染出來
最簡單的同構應用其實並不複雜,複雜的是結合webpack,router之後的各種複雜狀態不容易解決
一個極簡單的小例子
html
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>React同構</title>
<link href="styles/main.css" rel="stylesheet" />
</head>
<body>
<div id="app">
<%- reactOutput %>
</div>
<script src="bundle.js"></script>
</body>
</html>
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(<AppRoot state={{} }/>);
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);
});
你看服務端渲染的原理就是,服務端呼叫react的renderToString
方法,在伺服器端生成文字,插入到html文字之中,輸出到瀏覽器客戶端。然後客戶端檢測到這些已經生成的dom,就不會重新渲染,直接使用現有的html結構。
然而現實並不是這麼單純,使用react做前端開發的應該不會不使用webpack
,React-router
,redux
等等一些提高效率,簡化工作的一些輔助類庫或者框架,這樣的應用是不是就不太好做同構應用了?至少不會向上文這麼簡單吧?
做當然是可以做的,但複雜度確實也大了不少
結合框架的例子
webpack-isomorphic-tools
這個webpack外掛的主要作用有兩點
-
獲取webpack打包之後的入口檔案路徑,包括js,css
-
把一些特殊的檔案例如大圖片、編譯之後css的對映儲存下來,以便在伺服器端使用
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 = {
// 專案根目錄
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
}
}
}
}
這些檔案配置好之後,當再執行webpack打包命令的時候就會生成一個叫做webpack-assets.json
的檔案,這個檔案記錄了剛才生成的如檔案的路徑以及css,img對映表
客戶端的配置到這裡就結束了,來看下服務端的配置
服務端的配置過程要複雜一些,因為需要使用到WebpackIsomorphicToolsPlugin
生成的檔案,
我們直接使用它對應的服務端功能就可以了
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
初始化之後再啟動伺服器
文章開頭我們知道react是可以執行在服務端的,其實不光是react,react-router,redux也都是可以執行在伺服器端的
既然前端我們使用了react-router,也就是前端路由,那後端又怎麼做處理呢
其實這些react-router在設計的時候已經想到了這些,設計了一個api: match
match({routes, location}, (error, redirectLocation, renderProps) => {
matchResult = {
error,
redirectLocation,
renderProps
}
})
match方法在伺服器端解析了當前請求路由,獲取了當前路由的對應的請求引數和對應的元件
知道了這些還不足以做服務端渲染啊,比如一些頁面自己作為一個元件,是需要在客戶端向服務
器發請求,獲取資料做渲染的,那我們怎麼把渲染好資料的頁面輸出出來呢?
那就是需要做一個約定,就是前端單獨放置一個獲取資料,渲染頁面的方法,由後端可以呼叫,這樣邏輯就可以保持一份,
保持好的維護性
但是怎麼實現呢?實現的過程比較簡單,想法比較繞
1.呼叫的介面的方式必須前端通用
2.渲染頁面的方式必須前後端通用
先來第一個,大家都知道前端呼叫介面的方式通過ajax,那後端怎麼使用ajax呢?有一個庫封裝了伺服器端的fetch
方法實現,可以用來做這個
由於ajax方法需要前後端通用,那就要求這個方法裡面不能夾雜著客戶端或者服務端特有的api
呼叫。
還有個很重要的問題,就是許可權的問題,前端有時候是需要登入之後才可以呼叫的介面,後端直接呼叫
顯然是沒有cookie的,怎麼辦呢?解決辦法就是在使用者第一個請求進來之後儲存cookie甚至是全部的http
頭資訊,然後把這些資訊傳進fetch方法裡面去
通用元件方法必須寫成類的靜態成員,否則後端獲取不到,名稱也必須統一
static getInitData (params = {}, cookie, dispatch, query = {}) {
return getList({
...params,
...query
}, cookie)
.then(data => dispatch({
type: constants.article.GET_LIST_VIEW_SUCCESS,
data: data
}))
}
再看第二個問題,前端渲染頁面自然就是改變state或者傳入props就可以更新檢視,伺服器端怎麼辦呢?
redux是可以解決這個問題的
因為伺服器端不像前端,需要在初始化之後再去更新檢視,伺服器端只需要先把資料準備好,然後直接一遍生成
檢視就可以了,所以上圖的dispatch
方法是由前後端都可以傳入
渲染頁面的後端方法就比較簡單了
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) =>
<script src={`http://${client.host}:${client.port}` + javascript[script]} key={i} />
)
}
get styles () {
const { assets } = this.props
const { styles, assets: _assets } = assets
const stylesArray = Object.keys(styles)
// styles (will be present only in production with webpack extract text plugin)
if (stylesArray.length !== 0) {
return stylesArray.map((style, i) =>
<link href={`http://${client.host}:${client.port}` + assets.styles[style]} key={i} rel="stylesheet" type="text/css" />
)
}
// (will be present only in development mode)
// It`s not mandatory but recommended to speed up loading of styles
// (resolves the initial style flash (flicker) on page load in development mode)
// const scssPaths = Object.keys(_assets).filter(asset => asset.includes(`.css`))
// return scssPaths.map((style, i) =>
// <style dangerouslySetInnerHTML={{ __html: _assets[style]._style }} key={i} />
// )
}
render () {
const { component, store } = this.props
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<title>前端部落格</title>
<link rel="icon" href="/favicon.ico" />
{this.styles}
</head>
<body>
<div id="root" dangerouslySetInnerHTML={{ __html: renderToString(component) }} />
<script dangerouslySetInnerHTML={{ __html: `window.__INITIAL_STATE__=${JSON.stringify(store.getState())};` }} />
{this.scripts}
</body>
</html>
)
}
}
ok了,頁面重新整理的時候,是後端直出的,點選跳轉的時候是前端渲染的
做了一個相對來說比較完整的案例,使用了react+redux+koa+mongodb開發的,還做了個爬蟲,爬取了一本小說