使用React作同構應用

使用React作同構應用

React是用於開發數據不斷變化的大型應用程序的前端view框架,結合其餘輪子例如reduxreact-router就能夠開發大型的前端應用。javascript

React開發之初就有一個特別的優點,就是先後端同構。css

什麼是先後端同構呢?就是先後端均可以使用同一套代碼生成頁面,頁面既能夠由前端動態生成,也能夠由後端服務器直接渲染出來html

最簡單的同構應用其實並不複雜,複雜的是結合webpack,router以後的各類複雜狀態不容易解決前端

一個極簡單的小例子

htmljava

<!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>

jsnode

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

然而現實並非這麼單純,使用react作前端開發的應該不會不使用webpack,React-router,redux等等一些提升效率,簡化工做的一些輔助類庫或者框架,這樣的應用是否是就不太好作同構應用了?至少不會向上文這麼簡單吧?webpack

作固然是能夠作的,但複雜度確實也大了很多git

結合框架的例子

webpack-isomorphic-tools

這個webpack插件的主要做用有兩點github

  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...

相關文章
相關標籤/搜索