资讯专栏INFORMATION COLUMN

React 同构实践与思考

MageekChiu / 2767人阅读

摘要:后面会利用这个框架来做实践。接下来就是我们要继续探讨的同构同构数据处理的探讨我们都知道,浏览器端获取数据需要发起请求,实际上发起的请求就是对应服务端一个路由控制器。是有生命周期的,官方给我们指出的绑定,应该在里来进行。

众所周知,目前的 WEB 应用,用户体验要求越来越高,WEB 交互变得越来越丰富!前端可以做的事越来越多,去年 Node 引领了前后端分层的浪潮,而 React 的出现让分层思想可以更多彻底的执行,尤其是 React 同构 (Universal or Isomorphic) 这个黑科技到底是怎么实现的,我们来一探究竟。

React 服务端方法

如果熟悉 React 开发,那么一定对 ReactDOM.render 方法不陌生,这是 React 渲染到 DOM 中的方法。

现有的任何开发模式都离不开 DOM 树,如图:

服务端渲染就要稍作改动,如图:

比较两张图可以看出,服务端渲染需要把 React 的初次渲染放到服务端,让 React 帮我们把业务 component 翻译成 string 类型的 DOM 树,再通过后端语言的 IO 流输出至浏览器。

我们来看 React 官方给我们提供的服务端渲染的API:

React.renderToString 是把 React 元素转成一个 HTML 字符串,因为服务端渲染已经标识了 reactid,所以在浏览器端再次渲染,React 只是做事件绑定,而不会将所有的 DOM 树重新渲染,这样能带来高性能的页面首次加载!同构黑魔法主要从这个 API 而来。

React.renderToStaticMarkup,这个 API 相当于一个简化版的 renderToString,如果你的应用基本上是静态文本,建议用这个方法,少了一大批的 reactid,DOM 树自然精简了,在 IO 流传输上节省一部分流量。

配合 renderToStringrenderToStaticMarkup 使用,createElement 返回的 ReactElement 作为参数传递给前面两个方法。

React 玩转 Node

有了解决方案,我们就可以动手在 Node 来做一些事了。后面会利用 KOA 这个 Node 框架来做实践。

我们新建应用,目录结构如下,

react-server-koa-simple
├── app
│   ├── assets
│   │   ├── build
│   │   ├── src
│   │   │    ├── img
│   │   │    ├── js
│   │   │    └── css
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── middleware
│   │   └── static.js(前端静态资源托管中间件)
│   ├── plugin
│   │   └── reactview(reactview 插件)
│   └── views
│       ├── layout
│       │    └── Default.js
│       ├── Device.js
│       └── Home.js
├── .babelrc
├── .gitgnore
├── app.js
├── package.json
└── README.md

首先,我们需要实现一个 KOA 插件,用来实现 React 作为服务端模板的渲染工作,方法是将 render 方法插入到 app 上下文中,目的是在 controller 层中调用,this.render(viewFileName, props, children) 并通过 this.body 输出文档流至浏览器端。

/*
 * koa-react-view.js
 * 提供 react server render 功能
 * {
 *   options : {
 *     viewpath: viewpath,                 // the root directory of view files
 *     doctype: "",
 *     extname: ".js",                     // view层直接渲染文件名后缀
 *     writeResp: true,                    // 是否需要在view层直接输出
 *   }
 * }
 */
module.exports = function(app) {
  const opts = app.config.reactview || {};
  assert(opts && opts.viewpath && util.isString(opts.viewpath), "[reactview] viewpath is required, please check config!");
  const options = Object.assign({}, defaultOpts, opts);

  app.context.render = function(filename, _locals, children) {
    let filepath = path.join(options.viewpath, filename);

    let render = opts.internals
      ? ReactDOMServer.renderToString
      : ReactDOMServer.renderToStaticMarkup;

    // merge koa state
    let props = Object.assign({}, this.state, _locals);
    let markup = options.doctype || "";

    try {
      let component = require(filepath);
      // Transpiled ES6 may export components as { default: Component }
      component = component.default || component;
      markup += render(React.createElement(component, props, children));
    } catch (err) {
      err.code = "REACT";
      throw err;
    }
    if (options.writeResp) {
      this.type = "html";
      this.body = markup;
    }
    return markup;
  };
};

然后,我们来写用 React 实现的服务端的 Components,

/*
 * react-server-koa-simple - app/views/Home.js
 * home模板
 */

render() {
  let { microdata, mydata } = this.props;
  let homeJs = `${microdata.styleDomain}/build/${microdata.styleVersion}/js/home.js`;
  let scriptUrls = [homeJs];

  return (
    
      
); }

这里做了几件事,初始化 DOM 树,用 data 属性作服务端数据埋点,渲染前后端公共 Content 模块,引用前端模块

而客户端,我们就可以很方便地拿到了服务端的数据,可以直接拿来使用,

import ReactDOM from "react-dom";
import Content from "./components/Content.js";

const microdata = JSON.parse(appEle.getAttribute("data-microdata"));
const mydata = JSON.parse(appEle.getAttribute("data-mydata"));

ReactDOM.render(
  ,
  document.getElementById("demoApp")
);

然后,到了启动一个简单的 koa 应用的时候,完善入口 app.js 来验证我们的想法,

const koa = require("koa");
const koaRouter = require("koa-router");
const path = require("path");
const reactview = require("./app/plugin/reactview/app.js");
const Static = require("./app/middleware/static.js");

const App = ()=> {
  let app = koa();
  let router = koaRouter();

  // 初始化 /home 路由 dispatch 的 generator
  router.get("/home", function*() {
    // 执行view插件
    this.body = this.render("Home", {
      microdata: {
        domain: "//localhost:3000"
      },
      mydata: {
        nick: "server render body"
      }
    });
  });
  app.use(router.routes()).use(router.allowedMethods());

  // 注入 reactview
  const viewpath = path.join(__dirname, "app/views");
  app.config = {
    reactview: {
      viewpath: viewpath,                 // the root directory of view files
      doctype: "",
      extname: ".js",                     // view层直接渲染文件名后缀
      beautify: true,                     // 是否需要对dom结构进行格式化
      writeResp: false,                    // 是否需要在view层直接输出
    }
  }
  reactview(app);

  return app;
};

const createApp = ()=> {
  const app = App();

  // http服务端口监听
  app.listen(3000, ()=> {
    console.log("3000 is listening!");
  });

  return app;
};
createApp();

现在,访问上面预先设置好的路由,http://localhost:3000/home 来验证 server render,

服务端:

浏览器端:

react-router 和 koa-router 统一

我们已经建立了服务端渲染的基础了,接着再考虑下如何把后端和前端的路由做统一。

假设我们的路由设置成 /device/:deviceID 这种形式,
那么服务端是这么来实现的,

// 初始化 device/:deviceID 路由 dispatch 的 generator
router.get("/device/:deviceID", function*() {
  // 执行view插件
  let deviceID = this.params.deviceID;
  this.body = this.render("Device", {
    isServer: true,
    microdata: microdata,
    mydata: {
      path: this.path,
      deviceID: deviceID,
    }
  });
});

以及服务端 View 模板,

render() {
  const { microdata, mydata, isServer } = this.props;
  const deviceJs = `${microdata.styleDomain}/build/${microdata.styleVersion}/js/device.js`;
  const scriptUrls = [deviceJs];

  return (
    
      
); }

前端 app 入口:app.js

function getServerData(key) {
  return JSON.parse(appEle.getAttribute(`data-${key}`));
};

// 从服务端埋点处 
获取 microdata, mydata let microdata = getServerData("microdata"); let mydata = getServerData("mydata"); ReactDOM.render( , document.getElementById("demoApp"));

前后端公用的 Iso.js 模块,前端路由同样设置成 /device/:deviceID

class Iso extends Component {
  static propTypes = {
    // ...
  };

  // 包裹 Route 的 Component,目的是注入服务端传入的 props
  wrapComponent(Component) {
    const { microdata, mydata } = this.props;

    return React.createClass({
      render() {
        return React.createElement(Component, {
          microdata: microdata,
          mydata: mydata
        }, this.props.children);
      }
    });
  }

  // LayoutView 为路由的布局; DeviceView 为参数处理模块
  render() {
    const { isServer, mydata } = this.props;

    return (
      
        
          
          
        
      
    );
  }
}

这样我就实现了服务端和前端路由的同构!

无论你是初次访问这些资源路径: /device/all, /device/pc, /device/wireless,还是在页面手动切换这些资源路径效果都是一样的,既保证了初次渲染有符合预期的 DOM 输出的用户体验,又保证了代码的简洁性,最重要的是前后端代码是一套,并且由一位工程师开发,有没有觉得很棒?

其中注意几点:

Iso 的 render 模块需要判断isServer,服务端用createMemoryHistory,前端用browserHistory;

react-router 的 component 如果需要注入 props 必须对其进行包裹 wrapComponent。因为服务端渲染的数据需要通过传 props 的方式,而react-router-route 只提供了 component,并不支持继续追加 props。截取 Route 的源码,

propTypes: {
  path: string,
  component: _PropTypes.component,
  components: _PropTypes.components,
  getComponent: func,
  getComponents: func
},

为什么服务端获取数据不和前端保持一致,在 Component 里作数据绑定,使用 fetchData 和数据绑定!只能说,你可以大胆的假设。接下来就是我们要继续探讨的同构model!

同构数据处理的探讨

我们都知道,浏览器端获取数据需要发起 ajax 请求,实际上发起的请求 URL 就是对应服务端一个路由控制器。

React 是有生命周期的,官方给我们指出的绑定 Model,fetchData 应该在 componentDidMount 里来进行。在服务端,React 是不会去执行componentDidMount 方法的,因为,React 的 renderTranscation 分成两块: ReactReconcileTransactionReactServerRenderingTransaction,其在服务端的实现移除掉了在浏览器端的一些特定方法。

而服务端处理数据是线性的,是不可逆的,发起请求 > 去数据库获取数据 > 业务逻辑处理 > 组装成 html-> IO流输出给浏览器。显然,服务端和浏览器端是矛盾的!

实验的方案

你或许会想到利用 ReactClass 提供的 statics 来做点文章,React 确实提供了入口,不仅能包裹静态属性,还能包裹静态方法,并且能 DEFINE_MANY:

/**
 * An object containing properties and methods that should be defined on
 * the component"s constructor instead of its prototype (static methods).
 *
 * @type {object}
 * @optional
 */
statics: SpecPolicy.DEFINE_MANY,

利用 statics 把我们的组件扩展成这样,

class ContentView extends Component {
  statics: {
    fetchData: function (callback) {
      ContentData.fetch().then((data)=> {
        callback(data);
      });
    }
  };
  // 浏览器端这样获取数据
  componentDidMount() {
    this.constructor.fetchData((data)=> {
      this.setState({
        data: data
      });
    });
  }
  ...
});

ContentData.fetch() 需要实现两套:

服务端:封装服务端service层方法

浏览器端:封装ajax或Fetch方法

服务端调用:

require("ContentView").fetchData((data)=> {
  this.body = this.render("Device", {
    isServer: true,
    microdata: microdata,
    mydata: data
  });
});

这样可以解决数据层的同构!但我并不认为这是一个好的方法,好像回到 JSP 时代。

我们团队现在使用的方法:

参考资料

本文完整运行的 例子

https://facebook.github.io/react/docs/getting-started.html

https://github.com/facebook/react

https://github.com/rackt/react-router

https://github.com/koajs/koa

https://github.com/alexmingoia/koa-router

https://github.com/koajs/react-view

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79019.html

相关文章

  • React同构直出优化总结

    摘要:同构的关键要素完善的属性及生命周期与客户端的时机是同构的关键。的一致性在前后端渲染相同的,将输出一致的结构。以上便是在同构服务端渲染的提供的基础条件。可以将封装至的中,在服务端上生成随机数并传入到这个中,从而保证随机数在客户端和服务端一致。 原文地址 React 的实践从去年在 PC QQ家校群开始,由于 PC 上的网络及环境都相当好,所以在使用时可谓一帆风顺,偶尔遇到点小磕绊,也能够...

    alaege 评论0 收藏0
  • koa 实现 react-view 原理

    摘要:今天,其实讲的是在实现同构过程中看到过,可能非常容易被忽视更小的一个点。每一个架构的框架都会涉及到层的展现,也不例外。这种说法即对也不对。总结其实,实现非常简单,我们也从一些维度看到了设计一个的一般方法。 在之前我们有过一篇『React 同构实践与思考』的专栏文章,给读者实践了用 React 怎么实现同构。今天,其实讲的是在实现同构过程中看到过,可能非常容易被忽视更小的一个点 —— R...

    zxhaaa 评论0 收藏0
  • 前端每周清单:Node.js 微服务实践,Vue.js GraphQL,Angular 组件技巧

    摘要:前端每周清单第期微服务实践,与,组件技巧,攻防作者王下邀月熊编辑徐川前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。 前端每周清单第 26 期:Node.js 微服务实践,Vue.js 与 GraphQL,Angular 组件技巧,HeadlessChrome 攻防 作者:王下邀月熊 编辑:徐川...

    wall2flower 评论0 收藏0
  • React 进阶设计控制权问题

    摘要:盘点一下,模式反应了典型的控制权问题。异步状态管理与控制权提到控制权话题,怎能少得了这样的状态管理工具。状态管理中的控制主义和极简主义了解了异步状态中的控制权问题,我们再从全局角度进行分析。 控制权——这个概念在编程中至关重要。比如,轮子封装层与业务消费层对于控制权的争夺,就是一个很有意思的话题。这在 React 世界里也不例外。表面上看,我们当然希望轮子掌控的事情越多越好:因为抽象层...

    superw 评论0 收藏0
  • React 进阶设计控制权问题

    摘要:盘点一下,模式反应了典型的控制权问题。异步状态管理与控制权提到控制权话题,怎能少得了这样的状态管理工具。状态管理中的控制主义和极简主义了解了异步状态中的控制权问题,我们再从全局角度进行分析。 控制权——这个概念在编程中至关重要。比如,轮子封装层与业务消费层对于控制权的争夺,就是一个很有意思的话题。这在 React 世界里也不例外。表面上看,我们当然希望轮子掌控的事情越多越好:因为抽象层...

    rubyshen 评论0 收藏0

发表评论

0条评论

MageekChiu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<