声明: 我是学习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上关注我。