React.js生态系统概览 [译]
JavaScript领域发展速度很快,甚至有人认为这已经引起了负效应。一个前端库从早期开发的小玩具,到流行,再到过时,可能也就几个月时间。判断一个工具能否在几年内依然保持活力都快成了一门艺术了。 React.js在两年前发布时,我刚开始学Angular,React在我看来只是又一个模板库而已。这两年间,Angular得到了JavaScript开发者的认同,它几乎成了现代前端开发的代名词。我还看到一些很保守的团队都在用它,这让我觉得Angular好像就是未来。 但突然发生了件奇怪的事,Angular好像成了奥斯本效应的受害者,或者说它被提前宣布了死亡。Angular团队宣布,Angular 2将会完全不同,基本没有从Angular 1升级迁移的东西,而且Angular 2在接下来的一年里还用不了。这告诉了那些想开发新Web项目的人:你想用一个马上要被淘汰了的框架写项目吗? 开发者们的忧虑影响到了正在建立的React社区,但React总标榜它只是MVC中的视图层(V),让一些依赖完整MVC框架做开发的人感觉有点失望。如何补充其他部分的功能?自己写吗?还是用别的三方库?要是的话该选哪一个呢? 果然,Facebook(React.js的创始)出了另一个杀手锏:Flux工作流,它声称要填补模型层(M)和控制层(C )的功能。Facebook还称Flux只是一种“模式”,不是个框架,他们的Flux实现只是这个模式的一个例子。就像他们所说,这个实现过于简单,但还是要写很多代码和一堆重复的模板才跑得起来。 这时开源社区发力了,一年后便有了各种Flux实现库,甚至都出来比较他们的元项目了。Facebook激起了社区的兴趣,不是给出现成的东西,而是鼓励大家提出自己的解决方案,这点很不错。 当你要结合各种库开发一个完整架构时,摆脱了框架的束缚,独立的库还可以在很多地方重用,在自己构建架构的过程中,这个优点很明显。 这便是为什么React相关的东西这么有意思。它们可以很容易地在其他JavaScript环境中实现重用。就算你不打算用React,看看它的生态系统都能受到启发。可以试试强大又容易配置的模块化打包工具Webpack来简化构建系统,或者用Babel转译器马上开始用ECMAScript 6甚至ECMAScript 7来写代码。 在这篇文章里我会给你概览一遍这些有意思的库和特性,来探索下React整个生态系统吧。
构建系统创建一个新的Web项目时,首先要考虑的可能就是构建系统了。它不只是做为个运行脚本的工具,还能优化你的项目结构。一个构建系统必须能包括下面几个最主要功能:
最近几年,Yeoman,Bower与Grunt被誉为现代前端开发的三剑客。他们解决了生成基础模板,包管理和各种通用任务问题,后面也很多人从Grunt换到了Gulp。 在React的生态系统里,基本上可以丢掉这些东西了,不是说你用不到他们,而是说可以用更先进的Webpack与NPM。怎么做到的呢?Webpack是一个模块化打包工具,用它可以在浏览器环境下使用Node.js中常用的CommonJS模块语法。其实它要更简单点,因为你不用为了前端另外学一种包管理方案。只需要用NPM,就可以做到服务端与前端模块的共用。也不用处理JS文件按顺序加载的问题,因为它能够从每个文件的import语法中推测出依赖关系,整个串联成一个可以在浏览器中加载的脚本。
Webpack
更强大的是Webpack不只像同类工具Browserify,它还可以处理其他类型的资源。例如用加载器,可以将任何资源文件转换成JavaScript函数,去内联或加载引用到的文件。用不着手工预处理还有从HTML中引用资源了,只要在JavaScript中 {
"name": "react-example-filmdb","version": "0.0.1","description": "Isomorphic React + Flux film database example","main": "server/index.js","scripts": {
"build": "./node_modules/.bin/webpack --progress --stats --config ./webpack/prod.config.js","dev": "node --harmony ./webpack/dev-server.js","prod": "NODE_ENV=production node server/index.js","test": "./node_modules/.bin/karma start --single-run","postinstall": "npm run build"
}
...
}
这些东西就可以代替Gulp与Bower了。当然,还是可以继续用Yeoman去生成应用基础模板的。要是Yeoman生成不了你需要的东西时(其实大多时候也都需要删掉那些用不到的库),还可以从Github上Clone一个基础模板,然后再改改。 马上试试新的ECMAScriptJavaScript在这几年有很大改善,移除糟粕稳定语言后,我们看到了很多新特性,ECMAScript 6(ES6)草案已经定稿。ECMAScript 7也已纳入标准化日程中,它们的特性也都已经被很多库采用了。 ECMAScript 7 可能你觉得到IE支持之前都用不上这些JS新特性,但实际我们不需要等浏览器完全支持,ES转译器已经广泛应用了。目前最好的ES转译器是Babel,它能够把ES6+代码转换成ES5,所以你马上就能用上新ES特性了(指已经在Babel中实现的那些,其实一般新出的特性也会很快被支持)。 Babel 新的JavaScript特性在所有前端框架里都可以用,更新的React能很好的在ES6与ES7下运行。这些新特性可以解决在用React开发时遇到的一些问题。来看下这些改善吧,它们对React项目很有用。稍后我们再看看怎样利用这些语法,来搭配使用React的工具和库。 ES6 Classes面向对象编程是一种强大又广泛适用的范式,但在JavaScript里感觉有点不一样。Backbone,Ember,Angular,或React等大多数前端框架都有它们自己的定义类和创建对象的方式。在ES6中,有原生的类支持了,它简洁清晰而不用我们自己实现,例如: React.createClass({
displayName: 'HelloMessage',render() {
return <div>Hello {this.props.name}</div>;
}
})
使用ES6就可以写成: class HelloMessage extends React.Component {
render() {
return <div>Hello {this.props.name}</div>;
}
}
再看个详细的例子: 'Counter',getDefaultProps: function(){
return {initialCount: 0};
},getInitialState: function() {
return {count: this.props.initialCount}
},propTypes: {initialCount: React.PropTypes.number},tick() {
this.setState({count: this.state.count + 1});
},render() {
return (
<div onClick={this.tick}> Clicks: {this.state.count} </div> );
}
});
ES6可以写成: Counter extends Component {
static propTypes = {initialCount: React.PropTypes.number};
static defaultProps = {initialCount: 0};
constructor(props) {
super(props);
this.state = {count: props.initialCount};
}
state = {count: this.props.initialCount};
tick() {
this.setState({count: this.state.count + 1});
}
render() {
return (
<div onClick={this.tick.bind(this)}> Clicks: {this.state.count} </div> );
}
}
在这里不用再写 装饰器装饰器是ES7中的特性。通过一个包装函数,来增强函数或类的行为。例如想为一些组件使用同一个change handler, 而又不想inheritance antipattern,则可以用类的装饰器去实现。定义一个装饰器: addChangeHandler: function(target) {
target.prototype.changeHandler = function(key,attr,event) {
var state = {};
state[key] = this.state[key] || {};
state[key][attr] = event.currentTarget.value;
this.setState(state);
};
return target;
}
在这里,函数 要应用装饰器,只需要: MyClass = addChangeHandler(MyClass) 或者用更优雅的ES7写法: @addChangeHandler
class MyClass {
...
}
因为React没有双向数据绑定,在用到input之类的控件时,代码就会比较冗余, LoginInput extends Component {
constructor(props) {
super(props);
this.state = {
login: {}
};
}
render() {
return (
<input type='text' value={this.state.login.username} onChange={this.changeHandler.bind(this, 'login','username')} /> <input type='password' value={this.state.login.password} onChange=password')} /> )
}
}
用户名输入框发生改变时,输入框的值会被直接存到 箭头函数 JavaScript的动态上下文 DirectorsStore {
onFetch(directors) {
var _this = this;
this.directorsHash = {};
directors.forEach(function(x){
_this.directorsHash[x._id] = x;
})
}
}
在ES6中,函数 onFetch(directors) {
this.directorsHash = {};
directors.forEach((x) => {
this.directorsHash[x._id] = x;
})
}
解构赋值ES6中的解构赋值允许在赋值表达式左边写个复合对象: var o = {p: 42,q: true};
var {p,q} = o;
console.log(p); // 42
console.log(q); // true
在React中有什么实际用处吗?看下面这个例子: function makeRequest(url,method,params) {
var config = {
url: url,method: method,params: params
};
...
}
用解构方式,可以一次给几个键赋值。 var config = {url,params}; ... } 解构赋值也可以用来加载一个模块的子集: const {clone,assign} = require('lodash');
function output(data,optional) {
var payload = clone(data);
assign(payload,optional);
}
函数的默认,剩余,扩展参数在ES6中函数传参更强大了,可以为函数设置默认参数: http(endpoint,method='GET') {
console.log(method)
...
}
http('/api') // GET
觉得 networkAction(context,...rest) {
// rest is an array
return method.apply(context,rest);
}
要是不想调用 myArguments = ['foo','bar',123];
myFunction(...myArguments);
Generator与Async函数 Generator是一种可以暂停执行,保存状态并稍后恢复执行的函数,每次遇到 function* sequence(from,to) {
console.log('Ready!');
while(from <= to) {
yield from++;
}
}
调用Generator函数: > var cursor = sequence(1,0)">3)
Ready!
> cursor.next()
{ value: false }
> cursor.next()
{ value: 2,0)">3,0)">false }
> cursor.next()
{ value: undefined,0)">true }
当调用Generator函数时,会立即执行到第一次遇到的 当然咯,Generator不只能用来创建数字序列。它能够暂停和恢复函数的执行,这便可以不需要回调函数就完成异步流的控制。 我们用异步函数来证明这一点。一般我们会做些I/O操作,这里为了简单起见,用 asyncDouble(x) {
var deferred = Promise.defer();
setTimeout(function(){
deferred.resolve(x*2);
},0)">1000);
return deferred.promise;
}
然后写一个消费者函数: consumer(generator){
var cursor = generator();
var value;
function loop() {
var data = cursor.next(value);
if (data.done) {
return;
} else {
data.value.then(x => {
value = x;
loop();
})
}
}
loop();
}
这个函数接受Generator函数作为参数,只要 resolve会调用 myGenerator(){
const data1 = yield asyncDouble(1);
console.log(`Double 1 = ${data1}`);
const data2 = yield asyncDouble(2);
console.log(`Double 2 = ${data2}`);
const data3 = yield asyncDouble(3);
console.log(`Double 3 = ${data3}`);
}
consumer(myGenerator);
Generator函数 Double 1 = 2
Double 2 = 4
Double 3 = 6
上面的示例代码不推荐在生产环境中用,可以用更完善的co库,能很简单地用yield处理异步,还包含了错误处理: co(function *(){
var a = yield Promise.resolve(1);
console.log(a);
var b = yield Promise.resolve(2);
console.log(b);
var c = yield Promise.resolve(3);
console.log(c);
}).catch(function(err){
console.error(err.stack);
});
在ES7中,将异步处理的改进更近了一步,增加了 async function (){
try {
var a = await Promise.resolve(1);
console.log(a);
var b = await Promise.resolve(2);
console.log(b);
var c = await Promise.resolve(3);
console.log(c);
} catch (err) {
console.error(err.stack);
}
};
有了这些特性,不会觉得JavaScript写异步代码麻烦了,而且在任何地方都可以用。 Generator不仅简洁明了,还能做些用回调很难实现的事。比如Node.js中koaWeb框架的中间件。koa的目标是替换掉Express,它有个很不错的特性:中间件的上下游都可以对服务端响应做进一步修改。看看这段koa服务器代码: // Response time logger middleware
app.use(function *(next){
// Downstream
var start = new Date;
yield next;
// Upstream
this.body += ' World';
var ms = new Date - start;
console.log('%s %s - %s',this.method,this.url,ms);
});
// Response handler
app.use(function *(){
this.body = 'Hello';
});
app.listen(3000);
Koa 进入第一个中间件时(上游),遇到yield后,先进入第二个中间件(下游)将响应体设置为 var start;
// Downstream middleware
app.use(function(req,res,next) {
start = new Date;
next();
// Already returned,cannot continue here
});
// Response
app.use(function (req,next){
res.send('Hello World')
next();
});
// Upstream middleware
app.use(function(req,next) {
// res already sent,cannot modify
var ms = new Date - start;
console.log( 也许你已经发现问题了,使用全局的
StoreFlux模型的store有两个任务:作为action的处理器,保存状态。继续看看登录例子是怎么做的吧。 创建一个 LoginStore {
constructor() {
this.bindActions(LoginActions);
this.user = null;
this.error = null;
}
...
处理器定义起来很方便,只需要在对应action名字前加 ...
onLogin(data) {
if (data.ok) {
this.user = data.user;
this.error = null;
router.transitionTo('home');
} else {
this.user = null;
this.error = data.error
}
}
onLogout() {
this.user = null;
}
}
Component 通常将store绑定到component的方式是用mixin。但mixin快过时了,需要找个其他方式,有个新方法是使用嵌套的component。将我们的component包装到另一个component里面,它会托管store的监听然后重新调用渲染。component会在 export default class Login extends Component {
render() {
return (
<AltContainer stores={{LoginStore: LoginStore}}> <LoginPage/> </AltContainer> )}
}
@changeHandler
export default class LoginPage extends Component {
constructor(props) {
super(props);
this.state = {
loginForm: {}
};
}
login() {
LoginActions.login(this.state.loginForm)
}
render() {
return (
<Alert>{{this.props.LoginStore.error}}</Alert> <Input label='Username' type=loginForm','username')} /> <Input label='Password' type=password')} /> <Button onClick={this.login.bind(this)}>Login</Button> )}
}
同构渲染同构Web应用是近些年来的热点话题,它能解决传统单页面应用最大的问题:浏览器用JavaScript动态创建HTML,如果浏览器禁用了JavaScript,内容就无法显示了,搜索引擎索引不到Web页面,内容不会出现在搜索结果中。实际上有方法解决这个问题,但做的并不够好。同构的方式是通过服务端渲染内容来解决问题。Node.js是服务端的JavaScript,React当然也能运行在服务端。 一些使用单例方式的Flux库,很难做服务端渲染,单例的Flux 在遇到并发请求时,store数据就会变得混乱。一些Flux库用实例来解决这个问题,但需要在代码间传递实例。Alt实际也提供了Flux实例,但它用单例解决服务端渲染问题,它会在每次请求后清空store,以便并发请求时每次store都是初始状态。 React.renderToString函数是服务端渲染的核心,整个React应用运行在服务端。服务端生成HTML后响应给浏览器。浏览器JavaScript运行时,再渲染剩余部分。可以用Iso库实现这点,它同时也被集成到了Alt中。 首先,我们在服务端用 // We use react-router to run the URL that is provided in routes.jsx
var getHandler = function(routes,url) {
var deferred = Promise.defer();
Router.run(routes,url,function (Handler) {
deferred.resolve(Handler);
});
return deferred.promise;
};
app.use(function *(next) {
yield next;
// We seed our stores with data
alt.bootstrap(JSON.stringify(this.locals.data || {}));
var iso = new Iso();
const handler = yield getHandler(reactRoutes,this.request.url);
const node = React.renderToString(React.createElement(handler));
iso.add(node,alt.flush());
this.render('layout',{html: iso.render()});
});
在客户端,拿到了服务端的状态数据,用来初始化 Iso.bootstrap(function (state,_,container) {
// Bootstrap the state from the server
alt.bootstrap(state)
Router.run(routes,Router.HistoryLocation,function (Handler,req) {
let node = React.createElement(Handler)
React.render(node,container)
})
})
一些有用的库例如CSS布局容器,样式表单,按钮,验证,日期选择器等等。 React-Bootstrap Twitter的Bootstrap框架应用已经非常普遍,对那些不想花时间写CSS的开发者很有用。特别是在原型开发阶段,Bootstrap不可或缺。要在React中使用Bootatrap,可以试试React-Bootstrap,它使用原生React component重新实现了Bootstrap的jQuery插件。代码简洁易懂: <Navbar brand='React-Bootstrap'>
<Nav> <NavItem eventKey={1} href='#'>Link</NavItem> <NavItem eventKey={2} href='#'>Link</NavItem> <DropdownButton eventKey={3} title='Dropdown'> <MenuItem eventKey='1'>Action</MenuItem> <MenuItem eventKey='2'>Another action</MenuItem> <MenuItem eventKey='3'>Something else here</MenuItem> <MenuItem divider /> <MenuItem eventKey='4'>Separated link</MenuItem> </DropdownButton> </Nav> </Navbar>
个人感觉这才是简单易懂的HTML吧。 也可以在Webpack中使用Bootstrap,看你喜欢哪个CSS预处理器。支持自定义引入的Bootstrap组件,可以在CSS代码中使用LESS或SASS的全局变量。 React RouterReact Router几乎已经成了React的路由标准了。它支持嵌套路由,重定向,同构渲染。基于JSX的例子: <Router history={new BrowserHistory}>
<Route path="/" component={App}> <Route path="about" name="about" component={About}/> <Route path="users" name="users" component={Users} indexComponent={RecentUsers}> <Route path="/user/:userId" name="user" component={User}/> </Route> <Route path="*" component={NoMatch}/> </Route> </Router>
React Router可以用 <nav>
<Link to="about">About</Link> <Link to="users">Users</Link> </nav>
还有集成了React-Bootstrap的路由库,不想手写Bootstrap的 <Nav>
<NavItemLink to="about">About</NavItemLink> <NavItemLink to="users">Users</NavItemLink> </Nav>
Formsy-React表单开发通常都比较麻烦,用formsy-react来简化一下吧,它可以用来管理验证和数据模型。Formsy-React不包含实际的表单输入,而是推荐用户自己写(这是正确的)。如果只用通用表单,可以用formsy-react-components。它包括了Bootstrap类: import Formsy from 'formsy-react';
import {Input} from 'formsy-react-components';
export default class FormsyForm extends Component {
enableButton() {
this.setState({canSubmit: true});
}
disableButton() {
this.setState({canSubmit: true});
}
submit(model) {
FormActions.saveEmail(model.email);
}
render() {
return (
<Formsy.Form onValidSubmit={this.submit} onValid={this.enableButton} onInvalid={this.disableButton}> <Input name="email" validations="isEmail" validationError="This is not a valid email" required/> <button type="submit" disabled={!this.state.canSubmit}>Submit</button> </Formsy.Form> )}
}
日期与选择器日期与选择器组件算是UI库的锦上添花了,遗憾的是这两个组件在Bootstrap 3上被移除了,因为它们对于一个通用CSS框架来说比较特殊了。不过我发现了两个不错的代替:react-pikaday和react-select。我尝试过10多个库,这两个总体来说很不错。非常易用: import Pikaday from 'react-pikaday';
import Select from 'react-select';
export default class CalendarAndTypeahead extends Component {
constructor(props){
super(props);
this.options = [
{ value: 'one',label: 'One' },{ value: 'two',0)">'Two' }
];
}
dateChange(date) {
this.setState({date: date});
},selectChange(selected) {
this.setState({selected: selected});
},render() {
return (
<Pikaday value={this.state.date} onChange={this.dateChange} /> <Select name="form-field-name" value={this.state.selected} options={this.options} onChange={selectChange} /> )}
}
总结 - React.js这篇文章介绍了目前Web开发的一些技术与框架。有些是React相关的,因为React的开放性,它们也能被用在其他环境。有时候技术进步总会被对新事物的恐惧所阻碍,我希望这篇文章可以打消你对尝试React,Flux和新的ECMAScript的疑虑。 要是感兴趣,可以看看我用以上技术构建的示例应用。源码在Github上。 感谢能坚持阅读到这里 :)
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |