React.js开发生态系统概览 [译-转]
React.js 開發(fā)生態(tài)系統(tǒng)概覽 [譯]
JavaScript領(lǐng)域發(fā)展速度很快,甚至有人認(rèn)為這已經(jīng)引起了負(fù)效應(yīng)。一個(gè)前端庫從早期開發(fā)的小玩具,到流行,再到過時(shí),可能也就幾個(gè)月時(shí)間。判斷一個(gè)工具能否在幾年內(nèi)依然保持活力都快成了一門藝術(shù)了。
React.js在兩年前發(fā)布時(shí),我剛開始學(xué)Angular,React在我看來只是又一個(gè)模板庫而已。這兩年間,Angular得到了JavaScript開發(fā)者的認(rèn)同,它幾乎成了現(xiàn)代前端開發(fā)的代名詞。我還看到一些很保守的團(tuán)隊(duì)都在用它,這讓我覺得Angular好像就是未來。
但突然發(fā)生了件奇怪的事,Angular好像成了奧斯本效應(yīng)的受害者,或者說它被提前宣布了死亡。Angular團(tuán)隊(duì)宣布,Angular 2將會(huì)完全不同,基本沒有從Angular 1升級遷移的東西,而且Angular 2在接下來的一年里還用不了。這告訴了那些想開發(fā)新Web項(xiàng)目的人:你想用一個(gè)馬上要被淘汰了的框架寫項(xiàng)目嗎?
開發(fā)者們的憂慮影響到了正在建立的React社區(qū),但React總標(biāo)榜它只是MVC中的視圖層(V),讓一些依賴完整MVC框架做開發(fā)的人感覺有點(diǎn)失望。如何補(bǔ)充其他部分的功能?自己寫嗎?還是用別的三方庫?要是的話該選哪一個(gè)呢?
果然,Facebook(React.js的創(chuàng)始)出了另一個(gè)殺手锏:Flux工作流,它聲稱要填補(bǔ)模型層(M)和控制層(C )的功能。Facebook還稱Flux只是一種“模式”,不是個(gè)框架,他們的Flux實(shí)現(xiàn)只是這個(gè)模式的一個(gè)例子。就像他們所說,這個(gè)實(shí)現(xiàn)過于簡單,但還是要寫很多代碼和一堆重復(fù)的模板才跑得起來。
這時(shí)開源社區(qū)發(fā)力了,一年后便有了各種Flux實(shí)現(xiàn)庫,甚至都出來比較他們的元項(xiàng)目了。Facebook激起了社區(qū)的興趣,不是給出現(xiàn)成的東西,而是鼓勵(lì)大家提出自己的解決方案,這點(diǎn)很不錯(cuò)。
當(dāng)你要結(jié)合各種庫開發(fā)一個(gè)完整架構(gòu)時(shí),擺脫了框架的束縛,獨(dú)立的庫還可以在很多地方重用,在自己構(gòu)建架構(gòu)的過程中,這個(gè)優(yōu)點(diǎn)很明顯。
這便是為什么React相關(guān)的東西這么有意思。它們可以很容易地在其他JavaScript環(huán)境中實(shí)現(xiàn)重用。就算你不打算用React,看看它的生態(tài)系統(tǒng)都能受到啟發(fā)。可以試試強(qiáng)大又容易配置的模塊化打包工具Webpack來簡化構(gòu)建系統(tǒng),或者用Babel轉(zhuǎn)譯器馬上開始用ECMAScript 6甚至ECMAScript 7來寫代碼。
在這篇文章里我會(huì)給你概覽一遍這些有意思的庫和特性,來探索下React整個(gè)生態(tài)系統(tǒng)吧。
構(gòu)建系統(tǒng)
創(chuàng)建一個(gè)新的Web項(xiàng)目時(shí),首先要考慮的可能就是構(gòu)建系統(tǒng)了。它不只是做為個(gè)運(yùn)行腳本的工具,還能優(yōu)化你的項(xiàng)目結(jié)構(gòu)。一個(gè)構(gòu)建系統(tǒng)必須能包括下面幾個(gè)最主要功能:
- 管理內(nèi)部與外部依賴
- 運(yùn)行編譯器和預(yù)處理器(例如CoffeeScript與SASS)
- 為生產(chǎn)環(huán)境優(yōu)化資源(例如Uglify)
- 運(yùn)行開發(fā)環(huán)境的Web Server,文件監(jiān)控,瀏覽器自動(dòng)刷新
最近幾年,Yeoman,Bower與Grunt被譽(yù)為現(xiàn)代前端開發(fā)的三劍客。他們解決了生成基礎(chǔ)模板,包管理和各種通用任務(wù)問題,后面也很多人從Grunt換到了Gulp。
在React的生態(tài)系統(tǒng)里,基本上可以丟掉這些東西了,不是說你用不到他們,而是說可以用更先進(jìn)的Webpack與NPM。怎么做到的呢?Webpack是一個(gè)模塊化打包工具,用它可以在瀏覽器環(huán)境下使用Node.js中常用的CommonJS模塊語法。其實(shí)它要更簡單點(diǎn),因?yàn)槟悴挥脼榱饲岸肆硗鈱W(xué)一種包管理方案。只需要用NPM,就可以做到服務(wù)端與前端模塊的共用。也不用處理JS文件按順序加載的問題,因?yàn)樗軌驈拿總€(gè)文件的import語法中推測出依賴關(guān)系,整個(gè)串聯(lián)成一個(gè)可以在瀏覽器中加載的腳本。
更強(qiáng)大的是Webpack不只像同類工具Browserify,它還可以處理其他類型的資源。例如用加載器,可以將任何資源文件轉(zhuǎn)換成JavaScript函數(shù),去內(nèi)聯(lián)或加載引用到的文件。用不著手工預(yù)處理還有從HTML中引用資源了,只要在JavaScript中require CSS/SASS/LESS文件就好了,Webpack會(huì)根據(jù)配置文件的描述去搞定一切。它還提供了個(gè)開發(fā)環(huán)境的Web Server,文件監(jiān)控器,你可以在package.json中使用scripts域去定義一個(gè)任務(wù):
{"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了。當(dāng)然,還是可以繼續(xù)用Yeoman去生成應(yīng)用基礎(chǔ)模板的。要是Yeoman生成不了你需要的東西時(shí)(其實(shí)大多時(shí)候也都需要?jiǎng)h掉那些用不到的庫),還可以從Github上Clone一個(gè)基礎(chǔ)模板,然后再改改。
馬上試試新的ECMAScript
JavaScript在這幾年有很大改善,移除糟粕穩(wěn)定語言后,我們看到了很多新特性,ECMAScript 6(ES6)已經(jīng)定稿。ECMAScript 7也已納入標(biāo)準(zhǔn)化日程中,它們的特性也都已經(jīng)被很多庫采用了。
可能你覺得到IE支持之前都用不上這些JS新特性,但實(shí)際我們不需要等瀏覽器完全支持,ES轉(zhuǎn)譯器已經(jīng)廣泛應(yīng)用了。目前最好的ES轉(zhuǎn)譯器是Babel,它能夠把ES6+代碼轉(zhuǎn)換成ES5,所以你馬上就能用上新ES特性了(指已經(jīng)在Babel中實(shí)現(xiàn)的那些,其實(shí)一般新出的特性也會(huì)很快被支持)。
新的JavaScript特性在所有前端框架里都可以用,更新的React能很好的在ES6與ES7下運(yùn)行。這些新特性可以解決在用React開發(fā)時(shí)遇到的一些問題。來看下這些改善吧,它們對React項(xiàng)目很有用。稍后我們再看看怎樣利用這些語法,來搭配使用React的工具和庫。
ES6 Classes
面向?qū)ο缶幊淌且环N強(qiáng)大又廣泛適用的范式,但在JavaScript里感覺有點(diǎn)不一樣。Backbone,Ember,Angular,或React等大多數(shù)前端框架都有它們自己的定義類和創(chuàng)建對象的方式。在ES6中,有原生的類支持了,它簡潔清晰而不用我們自己實(shí)現(xiàn),例如:
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>;} }再看個(gè)詳細(xì)的例子:
React.createClass({displayName: '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可以寫成:
class Counter extends React.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>);} }在這里不用再寫getDefaultProps和getInitialState這兩個(gè)React的生命周期函數(shù)了。getDefaultProps改為了類的靜態(tài)變量defaultProps,初始state也只需要定義在構(gòu)造函數(shù)中。這種方式唯一的缺點(diǎn)是,在JSX中使用的方法的上下文不會(huì)再自動(dòng)綁定到類實(shí)例了,必須用bind來指定。
裝飾器 *
裝飾器是ES7中的特性。通過一個(gè)包裝函數(shù),來增強(qiáng)函數(shù)或類的行為。例如想為一些組件使用同一個(gè)change handler, 而又不想inheritance antipattern,則可以用類的裝飾器去實(shí)現(xiàn)。定義一個(gè)裝飾器:
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; }在這里,函數(shù)addChangeHandler添加了changeHandler方法到目標(biāo)類target的實(shí)例上。
要應(yīng)用裝飾器,只需要:
MyClass = addChangeHandler(MyClass)或者用更優(yōu)雅的ES7寫法:
@addChangeHandler class MyClass {... }因?yàn)镽eact沒有雙向數(shù)據(jù)綁定,在用到input之類的控件時(shí),代碼就會(huì)比較冗余,changeHandler函數(shù)則把它簡化了。第一個(gè)參數(shù)表示在state對象中使用的key,它將存儲(chǔ)input的一個(gè)數(shù)據(jù)對象。第二個(gè)參數(shù)是屬性,它表示input的值。這兩個(gè)參數(shù)在JSX中被傳入:
@addChangeHandler class LoginInput extends React.Component {constructor(props) {super(props);this.state = {login: {}};}render() {return (<inputtype='text'value={this.state.login.username}onChange={this.changeHandler.bind(this, 'login', 'username')} /><inputtype='password'value={this.state.login.password}onChange={this.changeHandler.bind(this, 'login', 'password')} />)} }用戶名輸入框發(fā)生改變時(shí),輸入框的值會(huì)被直接存到this.state.login.username中,不需要一個(gè)個(gè)去寫handler了。
箭頭函數(shù)
JavaScript的動(dòng)態(tài)上下文this不太直觀,成了開發(fā)者的常痛了。比如在類里,包裝函數(shù)中的this都被指向了全局變量。要fix這個(gè)問題,通常是把this存到外部作用域下(例如_this),然后在內(nèi)部函數(shù)中用它:
class DirectorsStore {onFetch(directors) {var _this = this;this.directorsHash = {};directors.forEach(function(x){_this.directorsHash[x._id] = x;})} }在ES6中,函數(shù)function(x) { 可以寫成 (x) => {。這種箭頭方式的函數(shù)定義不僅將內(nèi)部的this綁定到了外部作用域,而且看起來很簡潔*,在寫大量異步代碼時(shí)很有用:
onFetch(directors) {this.directorsHash = {};// for each循環(huán)處理函數(shù)directors.forEach((x) => {this.directorsHash[x._id] = x;}) }解構(gòu)賦值
ES6中的解構(gòu)賦值允許在賦值表達(dá)式左邊寫個(gè)復(fù)合對象:
var o = {p: 42, q: true}; var {p, q} = o;console.log(p); // 42 console.log(q); // true在React中有什么實(shí)際用處嗎?看下面這個(gè)例子:
function makeRequest(url, method, params) {var config = {url: url,method: method,params: params};... }用解構(gòu)方式,可以一次給幾個(gè)鍵賦值。url, method, params這些值會(huì)被自動(dòng)賦給有著同名鍵的對象中。這讓代碼更不易出錯(cuò):
function makeRequest(url, method, params) {var config = {url, method, params};... }這樣看起來更好,但就是對javascript ES6不熟悉的時(shí)候,看著很奇怪。
解構(gòu)賦值也可以用來加載一個(gè)模塊的子集:
const {clone, assign} = require('lodash'); function output(data, optional) {var payload = clone(data);assign(payload, optional); }函數(shù)的默認(rèn),剩余,擴(kuò)展參數(shù)
在ES6中函數(shù)傳參更強(qiáng)大了,可以為函數(shù)設(shè)置默認(rèn)參數(shù):
function http(endpoint, method='GET') {console.log(method)... }http('/api') // GET覺得arguments用起來很麻煩?可以把剩余參數(shù)寫成一個(gè)數(shù)組:
function networkAction(context, method, ...rest) {// rest is an arrayreturn method.apply(context, rest); }要是不想調(diào)用apply()方法,可以把數(shù)組擴(kuò)展成函數(shù)的參數(shù):
myArguments = ['foo', 'bar', 123]; myFunction(...myArguments);Generator與Async函數(shù)
Generator是一種可以暫停執(zhí)行,保存狀態(tài)并稍后恢復(fù)執(zhí)行的函數(shù),每次遇到y(tǒng)ield關(guān)鍵字時(shí),就會(huì)暫停執(zhí)行。Generator寫法:
**注意function 后面帶著* 星號** function* sequence(from, to) {console.log('Ready!');while(from <= to) {yield from++;} }調(diào)用Generator函數(shù):
> var cursor = sequence(1,3) > cursor.next() Ready! { value: 1, done: false } > cursor.next() { value: 2, done: false } > cursor.next() { value: 3, done: false } > cursor.next() { value: undefined, done: true }當(dāng)調(diào)用Generator函數(shù)時(shí),會(huì)立即執(zhí)行到第一次遇到的yield關(guān)鍵字處暫停。在調(diào)用next()函數(shù)后,返回yield后的表達(dá)式的值(value),然后Generator里再繼續(xù)執(zhí)行之后的代碼。每次遇到y(tǒng)ield都返回一個(gè)值,在第三次調(diào)用next()后,Generator函數(shù)終止,最后調(diào)用的next()將返回{ value: undefined, done: true }。
當(dāng)然咯,Generator不只能用來創(chuàng)建數(shù)字序列。它能夠暫停和恢復(fù)函數(shù)的執(zhí)行,這便可以不需要回調(diào)函數(shù)就完成異步流的控制。
我們用異步函數(shù)來證明這一點(diǎn)。一般我們會(huì)做些I/O操作,這里為了簡單起見,用setTimeout模擬。這個(gè)異步函數(shù)立即返回一個(gè)promise。(ES6有原生的promise):
function asyncDouble(x) {var deferred = Promise.defer();setTimeout(function(){deferred.resolve(x*2);}, 1000);return deferred.promise; }然后寫一個(gè)消費(fèi)者函數(shù):
function 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(); }這個(gè)函數(shù)接受Generator函數(shù)作為參數(shù),只要yield有值,就會(huì)繼續(xù)調(diào)用next()方法。這個(gè)例子中,yield的值是promise,所以必須等待promise調(diào)用resolve后,再遞歸調(diào)用loop()循環(huán)。
resolve會(huì)調(diào)用then()方法,把resolve的結(jié)果賦值給value,value被定義在函數(shù)外部,它會(huì)被傳給下一個(gè)next(value)。這次next的調(diào)用為yield產(chǎn)生了一個(gè)返回值(即value)。這樣可以不用回調(diào)函數(shù)來寫異步調(diào)用了:
function* 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函數(shù)myGenerator將在每次遇到y(tǒng)ield時(shí)暫停,等待消費(fèi)者函數(shù)的promise去resolve。在控制臺(tái)每隔1秒的輸出:
Double 1 = 2 Double 2 = 4 Double 3 = 6上面的示例代碼不推薦在生產(chǎn)環(huán)境中用,可以用更完善的co庫,能很簡單地用yield處理異步,還包含了錯(cuò)誤處理:
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中,將異步處理的改進(jìn)更近了一步,增加了async和await關(guān)鍵字,無需使用Generator。上面的例子可以寫成:
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); } };有了這些特性,不會(huì)覺得JavaScript寫異步代碼麻煩了,而且在任何地方都可以用。
Generator不僅簡潔明了,還能做些用回調(diào)很難實(shí)現(xiàn)的事。比如Node.js中koa Web框架的中間件。koa的目標(biāo)是替換掉Express,它有個(gè)很不錯(cuò)的特性:中間件的上下游都可以對服務(wù)端響應(yīng)做進(jìn)一步修改。看看這段koa服務(wù)器代碼:
// Response time logger middleware app.use(function *(next){// Downstreamvar start = new Date;yield next;// Upstreamthis.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);進(jìn)入第一個(gè)中間件時(shí)(上游),遇到y(tǒng)ield后,先進(jìn)入第二個(gè)中間件(下游)將響應(yīng)體設(shè)置為Hello,然后再從yield恢復(fù)執(zhí)行,繼續(xù)執(zhí)行上游中間件代碼,為響應(yīng)體追加了World,然后打印了時(shí)間日志,在這里上下游共享了相同的上下文(this)。這要比Express更強(qiáng)大,如果用Express來寫的話可能會(huì)是這樣:
var start; // Downstream middleware app.use(function(req, res, next) {start = new Date;next();// Already returned, cannot continue here });// Response app.use(function (req, res, next){res.send('Hello World')next(); });// Upstream middleware app.use(function(req, res, next) {// res already sent, cannot modifyvar ms = new Date - start;console.log('%s %s - %s', this.method, this.url, ms);next(); });app.listen(3000);也許你已經(jīng)發(fā)現(xiàn)問題了,使用全局的start變量在出現(xiàn)并發(fā)請求時(shí)會(huì)產(chǎn)生污染。
用koa時(shí),可以很容易地用Generator做異步操作:
app.use(function *(){try {const part1 = yield fs.readFile(this.request.query.file1, 'utf8');const part2 = yield fs.readFile(this.request.query.file2, 'utf8');this.body = part1 + part2;} catch (err) {this.status = 404this.body = err;} });app.listen(3000);可以設(shè)想下這個(gè)例子在Express中用promise和回調(diào)會(huì)是什么樣子。
上面討論的Node.js這些跟React有關(guān)系嗎?當(dāng)然咯,Node是React的首選后端服務(wù)器。因?yàn)镹ode也用JavaScript,這樣可以前后端共享代碼,用來構(gòu)建同構(gòu)的React Web應(yīng)用,稍后會(huì)介紹到。
Flux庫
React很擅長創(chuàng)建視圖組件,但我們還需要一種方式去管理數(shù)據(jù)和整個(gè)應(yīng)用的狀態(tài)。Flux應(yīng)用架構(gòu)被普遍認(rèn)為是React最好的補(bǔ)充。要是你對Flux還很陌生,建議看看這篇快速導(dǎo)覽。
目前還沒有出現(xiàn)大多數(shù)人都認(rèn)同的Flux實(shí)現(xiàn),Facebook官方的Flux實(shí)現(xiàn)很多人都覺得很冗余。我們主要關(guān)心能不能少寫點(diǎn)模板代碼,配置方便,并且給多層組件提供些好用的功能,支持服務(wù)端渲染等等這些。可以看看這里給這些實(shí)現(xiàn)庫列出的一些指標(biāo)。我關(guān)注了Alt, Reflux, Flummox, Fluxxor, 和Marty.js。
我選擇庫的方式并不能說客觀,但很有用。Fluxxor是我發(fā)現(xiàn)的第一個(gè)庫,但現(xiàn)在看起來它有點(diǎn)舊了。Marty.js很有意思,有很多功能,但還是需要寫很多模板代碼,有些功能看起來比較多余。Reflux看起來蠻有吸引力,但感覺對初學(xué)者來說有點(diǎn)難,還缺少合適的文檔。Flummox和Alt很相似,但Alt不需要寫很多模板代碼,開發(fā)也非常活躍,文檔更新塊,而且有個(gè)Slack社區(qū)。所以我選了Alt。
Alt Flux
Alt的Flux實(shí)現(xiàn)簡單而強(qiáng)大。Facebook的Flux文檔描述了很多關(guān)于dispatcher的東西,但在Alt中這些都可以忽略,因?yàn)閐ispatcher被隱式關(guān)聯(lián)到了action,通常不需要寫多余代碼。只需要關(guān)心store,action,component。這三層對應(yīng)MVC模型:store為模型層,action為控制層,component為視圖層。差異是Flux模型是單向數(shù)據(jù)流,意味著控制層不能直接修改視圖層,而是觸發(fā)模型層后,視圖層被動(dòng)修改。這對有些Angular開發(fā)者來說已經(jīng)是最佳實(shí)踐了。
- component初始化action
- store監(jiān)聽action然后更新數(shù)據(jù)
- component被綁定到store,當(dāng)數(shù)據(jù)更新時(shí)就重新渲染
Action
使用Alt庫時(shí),action通常有兩種寫法:自動(dòng)與手動(dòng)。自動(dòng)action用generateActions函數(shù)創(chuàng)建,它們直接發(fā)給dispatcher,手動(dòng)方法則寫成action類的方法,它們可以附帶一個(gè)payload發(fā)給dispatcher。常用例子是自動(dòng)action通知store有關(guān)app的一些事件。其余由手動(dòng)action負(fù)責(zé),它是處理服務(wù)端交互的首選方式。
REST API調(diào)用這種就屬于action,完整流程如下:
對于AJAX請求,我們可以用axios庫,無縫地處理JSON數(shù)據(jù)和頭數(shù)據(jù)。可以用ES7的async/await關(guān)鍵字,免去promise與回調(diào)。如果POST響應(yīng)狀態(tài)不是2XX,拋出錯(cuò)誤,然后發(fā)出返回的數(shù)據(jù)或接收到的錯(cuò)誤。
看個(gè)簡單的Alt登錄示例,在這里注銷action不需要做任何事情,只需要通知store,所以可以自動(dòng)生成它。登錄action是手動(dòng)的,會(huì)把登錄數(shù)據(jù)作為一個(gè)參數(shù)發(fā)給action的創(chuàng)建者。從服務(wù)端獲取響應(yīng)后,發(fā)出數(shù)據(jù),有錯(cuò)誤的話就發(fā)出錯(cuò)誤。
class LoginActions {constructor() {// Automatic actionthis.generateActions('logout');}// Manual actionasync login(data) {try {const response = await axios.post('/auth/login', data);this.dispatch({ok: true, user: response.data});} catch (err) {console.error(err);this.dispatch({ok: false, error: err.data});}} }module.exports = (alt.createActions(LoginActions));Store
Flux模型的store有兩個(gè)任務(wù):作為action的處理器,保存狀態(tài)。繼續(xù)看看登錄例子是怎么做的吧。
創(chuàng)建一個(gè)LoginStore,有兩個(gè)狀態(tài)屬性:user用于當(dāng)前登錄的用戶,error用于當(dāng)前登錄相關(guān)的錯(cuò)誤。為了減少模板代碼數(shù)量,Alt可以用一個(gè)bindActions函數(shù)綁定所有action到store類。
class LoginStore {constructor() {this.bindActions(LoginActions);this.user = null;this.error = null;}... }處理器定義起來很方便,只需要在對應(yīng)action名字前加on。因此login的action由onLogin處理。注意action名的首字母是陀峰命名法的大寫。在LoginStore中,有以下兩個(gè)處理器,被對應(yīng)的action調(diào)用:
...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;this.error = null;}Component
通常將store綁定到component的方式是用mixin。但mixin快過時(shí)了,需要找個(gè)其他方式,有個(gè)新方法是使用嵌套的component。將我們的component包裝到另一個(gè)component里面,它會(huì)托管store的監(jiān)聽然后重新調(diào)用渲染。component會(huì)在props中接收到store的狀態(tài)。這個(gè)方法對于smart and dumb component的代碼組織很有用,是以后的趨勢。對于Alt來說,包裝component是由AltContainer實(shí)現(xiàn)的:
export default class Login extends React.Component {render() {return (// AltContainer<AltContainer stores={{LoginStore: LoginStore}}><LoginPage/></AltContainer>)} }LoginPage component也用了我們之前介紹過的changeHandler裝飾器。LoginStore的數(shù)據(jù)用來顯示登陸失敗后的錯(cuò)誤,重新渲染則由AltContainer負(fù)責(zé)。點(diǎn)擊登錄按鈕后執(zhí)行l(wèi)oginaction,完整的Alt工作流:
@changeHandler export default class LoginPage extends React.Component {constructor(props) {super(props);this.state = {loginForm: {}};}login() {LoginActions.login(this.state.loginForm)}render() {return (<Alert>{{this.props.LoginStore.error}}</Alert><Inputlabel='Username'type='text'value={this.state.login.username}onChange={this.changeHandler.bind(this, 'loginForm', 'username')} /><Inputlabel='Password'type='password'value={this.state.login.password}onChange={this.changeHandler.bind(this, 'loginForm', 'password')} /><Button onClick={this.login.bind(this)}>Login</Button>)} }同構(gòu)渲染
同構(gòu)Web應(yīng)用是近些年來的熱點(diǎn)話題,它能解決傳統(tǒng)單頁面應(yīng)用最大的問題:瀏覽器用JavaScript動(dòng)態(tài)創(chuàng)建HTML,如果瀏覽器禁用了JavaScript,內(nèi)容就無法顯示了,搜索引擎索引不到Web頁面,內(nèi)容不會(huì)出現(xiàn)在搜索結(jié)果中。實(shí)際上有方法解決這個(gè)問題,但做的并不夠好。
同構(gòu)的方式是通過服務(wù)端渲染內(nèi)容來解決問題。Node.js是服務(wù)端的JavaScript,React當(dāng)然也能運(yùn)行在服務(wù)端。
一些使用單例方式的Flux庫,很難做服務(wù)端渲染,單例的Flux 在遇到并發(fā)請求時(shí),store數(shù)據(jù)就會(huì)變得混亂。一些Flux庫用實(shí)例來解決這個(gè)問題,但需要在代碼間傳遞實(shí)例。Alt實(shí)際也提供了Flux實(shí)例,但它用單例解決服務(wù)端渲染問題,它會(huì)在每次請求后清空store,以便并發(fā)請求時(shí)每次store都是初始狀態(tài)。
React.renderToString函數(shù)是服務(wù)端渲染的核心,整個(gè)React應(yīng)用運(yùn)行在服務(wù)端。服務(wù)端生成HTML后響應(yīng)給瀏覽器。瀏覽器JavaScript運(yùn)行時(shí),再渲染剩余部分。可以用Iso庫實(shí)現(xiàn)這點(diǎn),它同時(shí)也被集成到了Alt中。
首先,我們在服務(wù)端用alt.bootstrap初始化Flux,這會(huì)初始化Flux store數(shù)據(jù)。然后區(qū)分哪個(gè)URL對應(yīng)的渲染哪個(gè)component,哪個(gè)由客戶端Router渲染。由于使用alt的單例版,在每次渲染完后,需要使用alt.flush()初始化store以便為下次請求做好準(zhǔn)備。使用iso插件,可以把Flux的狀態(tài)數(shù)據(jù)序列化到HTML字符串中,以便客戶端知道去哪找到這份狀態(tài)數(shù)據(jù):
// 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 dataalt.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()}); });在客戶端,拿到了服務(wù)端的狀態(tài)數(shù)據(jù),用來初始化alt的狀態(tài)。然后可以運(yùn)行Router和React.render去更新服務(wù)端生成的HTML:
Iso.bootstrap(function (state, _, container) {// Bootstrap the state from the serveralt.bootstrap(state)Router.run(routes, Router.HistoryLocation, function (Handler, req) {let node = React.createElement(Handler)React.render(node, container)}) })一些有用的庫
例如CSS布局容器,樣式表單,按鈕,驗(yàn)證,日期選擇器等等。
React-Bootstrap
Twitter的Bootstrap框架應(yīng)用已經(jīng)非常普遍,對那些不想花時(shí)間寫CSS的開發(fā)者很有用。特別是在原型開發(fā)階段,Bootstrap不可或缺。要在React中使用Bootatrap,可以試試React-Bootstrap,它使用原生React component重新實(shí)現(xiàn)了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>個(gè)人感覺這才是簡單易懂的HTML吧。
也可以在Webpack中使用Bootstrap,看你喜歡哪個(gè)CSS預(yù)處理器。支持自定義引入的Bootstrap組件,可以在CSS代碼中使用LESS或SASS的全局變量。
React Router
React Router幾乎已經(jīng)成了React的路由標(biāo)準(zhǔn)了。它支持嵌套路由,重定向,同構(gòu)渲染。基于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可以用Link component做應(yīng)用導(dǎo)航,只需要指定路由名稱:
<nav><Link to="about">About</Link><Link to="users">Users</Link> </nav>還有集成了React-Bootstrap的路由庫,不想手寫B(tài)ootstrap的active類,可以用react-router-bootstrap:
<Nav><NavItemLink to="about">About</NavItemLink><NavItemLink to="users">Users</NavItemLink> </Nav>Formsy-React
表單開發(fā)通常都比較麻煩,用formsy-react來簡化一下吧,它可以用來管理驗(yàn)證和數(shù)據(jù)模型。Formsy-React不包含實(shí)際的表單輸入,而是推薦用戶自己寫(這是正確的)。如果只用通用表單,可以用formsy-react-components。它包括了Bootstrap類:
import Formsy from 'formsy-react'; import {Input} from 'formsy-react-components'; export default class FormsyForm extends React.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}><Inputname="email"validations="isEmail"validationError="This is not a valid email"required/><button type="submit" disabled={!this.state.canSubmit}>Submit</button></Formsy.Form>)} }日期與選擇器
日期與選擇器組件算是UI庫的錦上添花了,遺憾的是這兩個(gè)組件在Bootstrap 3上被移除了,因?yàn)樗鼈儗τ谝粋€(gè)通用CSS框架來說比較特殊了。不過我發(fā)現(xiàn)了兩個(gè)不錯(cuò)的代替:react-pikaday和react-select。我嘗試過10多個(gè)庫,這兩個(gè)總體來說很不錯(cuò)。非常易用:
import Pikaday from 'react-pikaday'; import Select from 'react-select';export default class CalendarAndTypeahead extends React.Component {constructor(props){super(props);this.options = [{ value: 'one', label: 'One' },{ value: 'two', label: 'Two' }];}dateChange(date) {this.setState({date: date});},selectChange(selected) {this.setState({selected: selected});},render() {return (<Pikadayvalue={this.state.date}onChange={this.dateChange} /><Selectname="form-field-name"value={this.state.selected}options={this.options}onChange={selectChange} />)} }總結(jié) - React.js
這篇文章介紹了目前Web開發(fā)的一些技術(shù)與框架。有些是React相關(guān)的,因?yàn)镽eact的開放性,它們也能被用在其他環(huán)境。有時(shí)候技術(shù)進(jìn)步總會(huì)被對新事物的恐懼所阻礙,我希望這篇文章可以打消你對嘗試React, Flux和新的ECMAScript的疑慮。
要是感興趣,可以看看作者用以上技術(shù)構(gòu)建的示例應(yīng)用。源碼在Github上。
本文譯自2015年年中的《Navigating the React.JS Ecosystem》 - Tomas Holas
總結(jié)
以上是生活随笔為你收集整理的React.js开发生态系统概览 [译-转]的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 五g网络需要换手机吗(汉典五字的基本解释
- 下一篇: HTML5 中的下载简化处理