加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

使用React做同构应用

发布时间:2020-12-15 07:22:23 所属栏目:百科 来源:网络整理
导读:使用React做同构应用 React是用于开发数据不断变化的大型应用程序的前端view框架,结合其他轮子例如 redux 和 react-router 就可以开发大型的前端应用。 React开发之初就有一个特别的优势,就是前后端同构。 什么是前后端同构呢?就是前后端都可以使用同一套

使用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','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/],`${cssLoader}`)
      },include: [/node_modules/],`${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,log)
        }
        return regex.test(module.name)
      },path(module,log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module,log);
        }
        return module.name
      },parser(module,log) {
        if (options.development) {
          return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module,log);
        }
        return module.source
      }
    },less: {
      extensions: ['less'],filter: function(module,log)
      {
        if (options.development)
        {
          return webpack_isomorphic_tools_plugin.style_loader_filter(module,log)
        }

        return regex.test(module.name)
      },path: function(module,log)
      {
        if (options.development)
        {
          return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module,log);
        }

        return module.name
      },parser: function(module,log)
      {
        if (options.development)
        {
          return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module,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,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...

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读