React 同构实践与思索

尽人皆知,如今的 WEB 运用,用户体验要求愈来愈高,WEB 交互变得愈来愈雄厚!前端能够做的事愈来愈多,客岁 Node 引领了前后端分层的海潮,而 React 的涌现让分层头脑能够更多完全的实行,尤其是 React 同构 (Universal or Isomorphic) 这个黑科技终究是怎样完成的,我们来一探终究。

React 效劳端要领

假如熟习 React 开辟,那末肯定对 ReactDOM.render 要领不生疏,这是 React 衬着到 DOM 中的要领。

现有的任何开辟情势都离不开 DOM 树,如图:
《React 同构实践与思索》

效劳端衬着就要稍作修改,如图:
《React 同构实践与思索》

比较两张图能够看出,效劳端衬着须要把 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: '<!DOCTYPE html>',
 *     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 || '<!DOCTYPE html>';

    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 (
    <Default
      microdata={microdata}
      scriptUrls={scriptUrls}
      title={"demo"}>
      <div id="demoApp"
        data-microdata={JSON.stringify(microdata)}
        data-mydata={JSON.stringify(mydata)}>
        <Content mydata={mydata} microdata={microdata} />
      </div>
    </Default>
  );
}

这里做了几件事,初始化 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(
  <Content mydata={mydata} microdata={microdata} />,
  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: '<!DOCTYPE html>',
      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 同构实践与思索》

  • 浏览器端: 《React 同构实践与思索》

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 (
    <Default
      microdata={microdata}
      scriptUrls={scriptUrls}
      title={"demo"}>
      <div id="demoApp"
        data-microdata={JSON.stringify(microdata)}
        data-mydata={JSON.stringify(mydata)}>
        <Iso
          microdata={microdata}
          mydata={mydata}
          isServer={isServer}
        />
      </div>
    </Default>
  );
}

前端 app 进口:app.js

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

// 从效劳端埋点处 <div id="demoApp"> 猎取 microdata, mydata
let microdata = getServerData('microdata');
let mydata = getServerData('mydata');

ReactDOM.render(
  <Iso microdata={microdata} mydata={mydata} isServer={false} />,
  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 (
      <Router history={isServer ? createMemoryHistory(mydata.path || '/') : browserHistory}>
        <Route path="/"
          component={this.wrapComponent(LayoutView)}>
          <IndexRoute component={this.wrapComponent(DeviceView)} />
          <Route path="/device/:deviceID" component={DeviceView} />
        </Route>
      </Router>
    );
  }
}

如许我就完成了效劳端和前端路由的同构!

不管你是初次接见这些资本途径: /device/all, /device/pc, /device/wireless,照样在页面手动切换这些资本途径结果都是一样的,既保证了初次衬着有相符预期的 DOM 输出的用户体验,又保证了代码的简洁性,最主要的是前后端代码是一套,而且由一名工程师开辟,有无以为很棒?

个中注重几点:

  1. Iso 的 render 模块须要推断isServer,效劳端用createMemoryHistory,前端用browserHistory;

  2. 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() 须要完成两套:

  1. 效劳端:封装效劳端service层要领

  2. 浏览器端:封装ajax或Fetch要领

效劳端挪用:

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

如许能够处理数据层的同构!但我并不认为这是一个好的要领,彷佛回到 JSP 时期。

我们团队如今运用的要领:
《React 同构实践与思索》

参考资料

本文完全运转的 例子

    原文作者:qingliang_hu
    原文地址: https://segmentfault.com/a/1190000004671209
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞