声明: 我是学习Ember.js团队的一员,本篇文章不是要比较react和ember.这两个框架都非常棒!
如果一个使用Ember的团队想要重用React团队的组件该怎么做呢?或者您可能知道并喜欢多个前端工具集。本篇文章正适合这些人,当然还有思想开放的开发者!
这些都是基于我在企业工作时做的改变,截止目前为止在生产环境上已经使用了6个月的经验之谈。要注意的唯一因素是通过确保应用程序不包含React库的重复项来压缩大小。
接下来首先要让Ember项目能够识别JSX的语法,给予它编译JSX代码的能力。在Ember项目中运行下面的命令:
npm install --save-dev babel-plugin-transform-class-properties babel-plugin-transform-react-jsx
在ember-cli-build.js文件中中, 做如下的修改:
ember-cli-build.js.diff
'use strict'; const EmberApp = require('ember-cli/lib/broccoli/ember-app'); module.exports = function(defaults) { let app = new EmberApp(defaults, { - // Add options here+ babel: { + plugins: [ + 'transform-class-properties', + 'transform-react-jsx', + ] + } });
接着,我们要确保有 eslint 来识别JSX代码. 在Ember项目中运行下面的代码:
npm install --save-dev eslint-plugin-babel eslint-plugin-react babel-eslint;
将如下修改添加到.eslintrc.js文件中:
diff --git a/.eslintrc.js b/.eslintrc.jsindex 99f9d25..b2970eb 100644--- a/.eslintrc.js+++ b/.eslintrc.js@@ -1,11 +1,17 @@ module.exports = { root: true, + parser: 'babel-eslint', parserOptions: { ecmaVersion: 2017, - sourceType: 'module'+ sourceType: 'module', + ecmaFeatures: { + jsx: true + } }, plugins: [ - 'ember'+ 'babel', + 'ember', + 'react', ], extends: [ 'eslint:recommended', @@ -15,6 +21,8 @@ module.exports = { browser: true }, rules: { + 'react/jsx-uses-react': 'error', + 'react/jsx-uses-vars': 'error', }, overrides: [ // node files
运行如下命令在项目中添加React和React DOM
npm install --save react react-dom
接着在ember-cli-build.js文件中做如下修改:
ember-cli-build.js.diff
'use strict';const EmberApp = require('ember-cli/lib/broccoli/ember-app');const glob = require('glob');module.exports = function(defaults) { let app = new EmberApp(defaults, { // Add options here babel: { plugins: [ 'transform-class-properties', 'transform-react-jsx', ] } }); // Use `app.import` to add additional libraries to the generated // output files. // // If you need to use different assets in different // environments, specify an object as the first parameter. That // object's keys should be the environment name and the values // should be the asset to use in that environment. // // If the library that you are including contains AMD or ES6 // modules that you would like to import into your application // please specify an object with the list of modules as keys // along with the exports of each module as its value.+ app.import({ + development: 'node_modules/react/umd/react.development.js', + production: 'node_modules/react/umd/react.production.min.js'+ }); + + app.import({ + development: 'node_modules/react-dom/umd/react-dom.development.js', + production: 'node_modules/react-dom/umd/react-dom.production.min.js'+ }); return app.toTree(); };
添加这些imports会在app中引入全局React和ReactDOM对象。这非常重要, 因为任何我们要引入的React库要正常工作都需要全局调用这些对象。
让我们创建vendor shims,以便我们可以让这些库使用es6导入语法。我们不在这些imports上使用amd transformation的原因是在使用transformation时不会创建全局对象。
运行以下命令,并使用下面所示的要点替换生成的文件的内容。接着在ember-cli-build.js文件中引入它们。
ember generate vendor-shim react ember generate vendor-shim react-dom
ember-cli-build.js.diff
// please specify an object with the list of modules as keys // along with the exports of each module as its value. app.import('node_modules/react/umd/react.production.min.js'); app.import('node_modules/react-dom/umd/react-dom.production.min.js'); + app.import('vendor/shims/react.js'); + app.import('vendor/shims/react-dom.js'); return app.toTree(); };
react-dom.js
(function() { function vendorModule() { 'use strict'; return { 'default': self['ReactDOM'], __esModule: true, }; } define('react-dom', [], vendorModule); })();
react.js
(function() { function vendorModule() { 'use strict'; return { 'default': self['React'], __esModule: true, }; } define('react', [], vendorModule); })();
创建一个可以创建React组件容器的基类. 这个想法的背后原理是将React的组件包含在Ember的组件内。这样做有助于简化 simple这些组件。接下来创建一个包含以下内容的app/react-component.js文件。
react-component.js
import Component from '@ember/component';import ReactDOM from 'react-dom';export default Component.extend({ /** * We don't need a template since we're only creating a * wrapper for our React component **/ layout: '', /** * Renders a react component as the current ember element * @param {React.Component} reactComponent. e.g., <HelloWorld /> */ reactRender(reactComponent) { ReactDOM.render(reactComponent, this.element); }, /** * Removes a mounted React component from the DOM and * cleans up its event handlers and state. */ unmountReactElement() { ReactDOM.unmountComponentAtNode(this.element); }, /** * Cleans up the rendered react component as the ember * component gets destroyed */ willDestroyComponent() { this._super(); this.unmountReactElement(); } })
首先我们运行ember g component hell-world 创建必修的‘hello world’ 组件, 并将如下内容添加到hello-world.js文件:
import ReactComponent from '../../react-component';let Greeter = ({name}) => <h2>Hello from {name}!!!</h2>;export default ReactComponent.extend({ didInsertElement() { this._super(...arguments); this.reactRender(<Greeter name="React"/>); } });
这太简单了 。 注意在第8行我们将值'React'传入到React组件中,这个属性可以是Ember组件的属性。现在来做一个更加复杂的示例。
在app中添加react-aria-modal 。并运行npm install --save @sivakumar-kailasam/react-aria-modal
接着在ember-cli-build.js中做如下修改:
ember-cli-build.js.diff
+ app.import('node_modules/@sivakumar-kailasam/react-aria-modal/dist/react-aria-modal.js', { + using: [{ + transformation: 'amd', + as: 'react-aria-modal'+ }] + });
现在可以在app中使用它了,先创建一个component的容器。
ember g component aria-modal
dialog-modal.js
import React from 'react';import AriaModal from 'react-aria-modal';export default class DemoModal extends React.Component { state = { modalActive: false }; activateModal = () => { this.setState({ modalActive: true }); }; deactivateModal = () => { this.setState({ modalActive: false }); }; getApplicationNode = () => { return document.getElementById('ember-application'); }; render() { const modal = this.state.modalActive ? <AriaModal titleText={this.props.title} onExit={this.deactivateModal} initialFocus="#demo-one-deactivate" getApplicationNode={this.getApplicationNode} underlayStyle={{ paddingTop: '2em' }} > <div id="demo-two-modal" className="modal"> <header className="modal-header"> <h2 id="demo-two-title" className="modal-title"> {this.props.title} </h2> </header> <div className="modal-body"> <p> Here is a modal {' '} <a href="#">with</a> {' '} <a href="#">some</a> {' '} <a href="#">focusable</a> {' '} parts. </p> <input onChange={(e) => this.props.onTextChange(e.target.value)} value={this.props.title}/> </div> <footer className="modal-footer"> <button id="demo-one-deactivate" onClick={this.deactivateModal}> deactivate modal </button> </footer> </div> </AriaModal> : false; return ( <div> <button onClick={this.activateModal}> activate modal </button> {modal} </div> ); } }
modal.js
import ReactComponent from '../../react-component';import DemoModal from './demo-modal';import { get, set } from '@ember/object'; export default ReactComponent.extend({ title: 'An awesome demo', onTextChange(text) { set(this, 'title', text); this.renderModal(); }, didInsertElement() { this._super(...arguments); this.renderModal(); }, renderModal() { this.reactRender( <DemoModal title={get(this, 'title')} onTextChange={(text) => this.onTextChange(text)} /> ); } });
这个例子演示了在React和Ember组件间绑定方法的一种方式。通过绑有Ember组件的方法的React组件传值来更新标题,并重新渲染react组件。
注意以下的动图记录了如何立即重新渲染更新的内容。这是因为增加的更新应用到了已经渲染的React组件中。可以在文章末点击demo网站链接体验。
上面这些,你可能自己很轻松的做出来了。但是直到现在我还有个重要的因素没提到。
你要引入的React组件需要接受UMD模块加载规范。可以阅读学习 https://medium.freecodecamp.org/javascript-modules-a-beginner-s-guide-783f7d7a5fcc 了解UMD和其他模块化加载格式,
必须在react-aria-modal的fork上设置[rollup.js](https://rollupjs.org/guide/en) 才能运行这个演示应用程序。在这里 https://github.com/davidtheclark/react-aria-modal/compare/master...sivakumar-kailasam:master 查阅rollup的功能。
如果你的React组件项目是用了webpack,你可以查阅 https://github.com/bvaughn/react-virtualized 找到需要生成多种模块格式输出的webpack的设置。
在https://sivakumar-kailasam.github.io/react-integration-sample/ 可以看到开发好的app,在repo查看出现在这篇博文的代码。试试用Ember和React的开发者工具查看这个app玩玩!
编辑: Alex LaFroscia 在这篇文章的基础上发布了一个实验性的addon(插件)https://github.com/alexlafroscia/ember-cli-react .这是我为什么热爱emer社区!
如果你喜欢这篇文章, 在twitter @sivakumar_k上关注我。