使用React做同構應用

frontoldman發表於2017-04-28

使用React做同構應用

React是用於開發資料不斷變化的大型應用程式的前端view框架,結合其他輪子例如reduxreact-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外掛的主要作用有兩點

  1. 獲取webpack打包之後的入口檔案路徑,包括js,css

  2. 把一些特殊的檔案例如大圖片、編譯之後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開發的,還做了個爬蟲,爬取了一本小說

https://github.com/frontoldma…

相關文章