學習 React.js:用 Node 和 React.js 創建一個實時的 Twitter 流
Build A Real-Time Twitter Stream with Node and React.js
By Ken Wheeler (@ken_wheeler)
簡介
歡迎來到學習 React 的第二章,該系列文章將集中在怎麼熟練並且有效的使用臉書的 React 庫上。如果你沒有看過第一章,概念和起步,我非常建議你繼續看下去之前,回去看看。
今天我們準備創建用 React 來創建一個應用,通過 Isomorphic Javascript。
Iso-啥?
Isomorphic. Javascript. 意思是說一份代碼在服務端和客戶端都可以跑。
這個概念被用在許多框架上,比如 Rendr, Meteor & Derby。你用 React 同樣也能實現,今天現在我們就開始來學。
為什麼那麽神奇?
我跟很多人一樣,都是 Angular 粉,不過有一個痛點是在處理 SEO 的時候非常麻煩。
不過我覺得 Google 應該會執行並且給 Javascript 做索引吧?
哦哈,肯定沒有啦。他們只是給你提供處理靜態 HTML 的機會。你還是要用 PhantomJS 或者其他第三方服務來生成 HTML 的。
那麽來看 React。
React 在客戶端很厲害,不過它可以在服務端渲染這就很不一樣了。這是因為 React 用了虛擬 DOM 來代替真的那個,並且允許我們渲染我們的組件。
開始
好吧屌絲們,讓我們把真傢伙掏出來吧。我們將構建一個英勇,它可以顯示這篇文章的推,並且可以實時加載。下面是一些需求:
- 它會監聽 Twitter 流 API,並且黨有新的推進來的時候,把它們保存下來
- 保存的時候,會推送一個事件到客戶端,以便視圖的更新
- 頁面會在服務端渲染,客戶端只是把他們拿過來
- 我們將實現無限滾動,每次加載十條推
- 新推進來的時候將會有一個提醒條提示用戶去看查看他們
下面是我們的效果圖。請去看看實際的 Demo ,確認我們的所有東西都是實時顯示的。
讓我們來看看除了 React 之外還要用到的一些工具:
- Express - 一個 node.js 頁面應用框架
- Handlebars - 一個末班語言,我們將會用來寫我們的佈局模板
- Browserify - 一個依賴包處理工具,通過它我們可以用 CommonJS 語法
- Mongoose - 一個 MongoDB 對象模型庫
- Socket.io - 實時雙向通訊庫
- nTwitter - Node.js Twitter API 庫
服務端
讓我們開始構建我們應用的服務端。從這裏下載工程文件,然後跟著下面做:
目錄結構
<!-- lang: js -->
components/ // React Components Directory
---- Loader.react.js // Loader Component
---- NotificationBar.react.js // Notification Bar Component
---- Tweet.react.js // Single Tweet Component
---- Tweets.react.js // Tweets Component
---- TweetsApp.react.js // Main App Component
models/ // Mongoose Models Directory
---- Tweet.js // Our Mongoose Tweet Model
public/ // Static Files Directory
---- css
---- js
---- svg
utils/
----streamHandler.js // Utility method for handling Twitter stream callbacks
views/ // Server Side Handlebars Views
----layouts
-------- main.handlebars
---- home.handlebars
app.js // Client side main
config.js // App configuration
package.json
routes.js // Route definitions
server.js // Server side main
PACKAGE.JSON
<!-- lang: js -->
{
"name": "react-isomorph",
"version": "0.0.0",
"description": "Isomorphic React Example",
"main": "app.js",
"scripts": {
"watch": "watchify app.js -o public/js/bundle.js -v",
"browserify": "browserify app.js | uglifyjs > public/js/bundle.js",
"build": "npm run browserify ",
"start": "npm run watch & nodemon server.js"
},
"author": "Ken Wheeler",
"license": "MIT",
"dependencies": {
"express": "~4.9.7",
"express-handlebars": "~1.1.0",
"mongoose": "^3.8.17",
"node-jsx": "~0.11.0",
"ntwitter": "^0.5.0",
"react": "~0.11.2",
"socket.io": "^1.1.0"
},
"devDependencies": {
"browserify": "~6.0.3",
"nodemon": "^1.2.1",
"reactify": "~0.14.0",
"uglify-js": "~2.4.15",
"watchify": "~2.0.0"
},
"browserify": {
"transform": [
"reactify"
]
}
}
如果上面的你都做了,只要簡單的執行一下 npm install
然後去喝杯水。等你回來之後,我們的所有需要的依賴包應該就會準備好了,然後該我們來動手了。
現在我們有一些可以用到的命令:
npm run watch
執行該命令會啟動 watchify 的監控,當我們編輯 js 文件時,他們將在保存時獲取 browserifiednpm run build
執行該命令回編譯我們的 bundle.js 並且打包壓縮成生產模式npm start
執行該命令將會啟動監控並通過 nodemon 運行我們的應用node server
該命令用於執行我們的英勇。在生產模式環境下,我強烈建議使用諸如 forever 或者 pm2 之類的工具。
配置服務器
為了保持我們可以集中精神在 React 上,我假設我們都有基於 Express 的開發經驗。如果你不熟悉我說的內容的話,你可以去閱讀一些有幫助的關聯文章,尤其是 ExpressJS 4.0 – New Features & Upgrading from 3.0
下面的文件中,我們主要做了四件事情:
- 通過 Express 啟動服務
- 鏈接我們的 MongoDB 數據庫
- 初始化我們的 socket.io 鏈接
- 創建我們的 Twitter stream 鏈接
SERVER.JS
<!-- lang: js -->
// Require our dependencies
var express = require(‘express‘),
exphbs = require(‘express-handlebars‘),
http = require(‘http‘),
mongoose = require(‘mongoose‘),
twitter = require(‘ntwitter‘),
routes = require(‘./routes‘),
config = require(‘./config‘),
streamHandler = require(‘./utils/streamHandler‘);
// Create an express instance and set a port variable
var app = express();
var port = process.env.PORT || 8080;
// Set handlebars as the templating engine
app.engine(‘handlebars‘, exphbs({ defaultLayout: ‘main‘}));
app.set(‘view engine‘, ‘handlebars‘);
// Disable etag headers on responses
app.disable(‘etag‘);
// Connect to our mongo database
mongoose.connect(‘mongodb://localhost/react-tweets‘);
// Create a new ntwitter instance
var twit = new twitter(config.twitter);
// Index Route
app.get(‘/‘, routes.index);
// Page Route
app.get(‘/page/:page/:skip‘, routes.page);
// Set /public as our static content dir
app.use("/", express.static(__dirname + "/public/"));
// Fire it up (start our server)
var server = http.createServer(app).listen(port, function() {
console.log(‘Express server listening on port ‘ + port);
});
// Initialize socket.io
var io = require(‘socket.io‘).listen(server);
// Set a stream listener for tweets matching tracking keywords
twit.stream(‘statuses/filter‘,{ track: ‘scotch_io, #scotchio‘}, function(stream){
streamHandler(stream,io);
});
nTwitter 允許我們訪問 Twitter Streaming API,因此我們使用了 statuses/filter
端點,以及 track
屬性,然後返回使用了 #scotchio hash 標籤或者提到 scotch_io 的推。你可以使用 Twitter Streaming API 裏面提供的端點,隨意修改這個查詢鏈接。
Models
在我們的應用中,使用了 Mongoose 來定義我們的 Tweet 模型。當從 Twitter steam 接收到我們的數據的時候,我們需要把它們保存到什麼地方,然後還需要靜態的查詢方法,用來配合應用的查詢參數返回子數據集。
TWEET.JS
<!-- lang: js -->
var mongoose = require(‘mongoose‘);
// Create a new schema for our tweet data
var schema = new mongoose.Schema({
twid : String
, active : Boolean
, author : String
, avatar : String
, body : String
, date : Date
, screenname : String
});
// Create a static getTweets method to return tweet data from the db
schema.statics.getTweets = function(page, skip, callback) {
var tweets = [],
start = (page * 10) + (skip * 1);
// Query the db, using skip and limit to achieve page chunks
Tweet.find({},‘twid active author avatar body date screenname‘,{skip: start, limit: 10}).sort({date: ‘desc‘}).exec(function(err,docs){
// If everything is cool...
if(!err) {
tweets = docs; // We got tweets
tweets.forEach(function(tweet){
tweet.active = true; // Set them to active
});
}
// Pass them back to the specified callback
callback(tweets);
});
};
// Return a Tweet model based upon the defined schema
module.exports = Tweet = mongoose.model(‘Tweet‘, schema);
在定義了我們的 schema 之後,我們創建一個叫做 getTweets
的靜態方法。它有三個參數, page
,skip
& callback
。
當我們有一個應用,不但在服務端渲染,而且還在後台有數據流不斷保存數據到數據庫,我們需要一個方法來確保,當我們請求下一頁推的時候,它能處理我們已經加載到服務端的推。
這就是 skip
參數的作用。如果我們有 2 條新的推進來,然後我們點了下一頁,我們需要往前移兩位,以確保索引的正確性,這樣我們才不會拿到重複的數據。
處理數據流
當我們的 Twitter stream 鏈接發送一個新的 Tweet 事件,我們需要一個方法來接收數據,把它們保存到數據庫,並且向客戶端推送。
STREAMHANDLER.JS
<!-- lang: js -->
var Tweet = require(‘../models/Tweet‘);
module.exports = function(stream, io){
// When tweets get sent our way ...
stream.on(‘data‘, function(data) {
// Construct a new tweet object
var tweet = {
twid: data[‘id‘],
active: false,
author: data[‘user‘][‘name‘],
avatar: data[‘user‘][‘profile_image_url‘],
body: data[‘text‘],
date: data[‘created_at‘],
screenname: data[‘user‘][‘screen_name‘]
};
// Create a new model instance with our object
var tweetEntry = new Tweet(tweet);
// Save ‘er to the database
tweetEntry.save(function(err) {
if (!err) {
// If everything is cool, socket.io emits the tweet.
io.emit(‘tweet‘, tweet);
}
});
});
};
我們先用模型發送請求,然後我們的流推送事件,獲取那些希望要保存的數據,保存好,然後通過 socket 事件把我們剛保存下來的數據推送到客戶端。
路由
我們的路由也是這篇文章中很精彩的一部分。我們來看看 routes.js
。
ROUTES.JS
<!-- lang: js -->
var JSX = require(‘node-jsx‘).install(),
React = require(‘react‘),
TweetsApp = require(‘./components/TweetsApp.react‘),
Tweet = require(‘./models/Tweet‘);
module.exports = {
index: function(req, res) {
// Call static model method to get tweets in the db
Tweet.getTweets(0,0, function(tweets, pages) {
// Render React to a string, passing in our fetched tweets
var markup = React.renderComponentToString(
TweetsApp({
tweets: tweets
})
);
// Render our ‘home‘ template
res.render(‘home‘, {
markup: markup, // Pass rendered react markup
state: JSON.stringify(tweets) // Pass current state to client side
});
});
},
page: function(req, res) {
// Fetch tweets by page via param
Tweet.getTweets(req.params.page, req.params.skip, function(tweets) {
// Render as JSON
res.send(tweets);
});
}
}
在上面的代碼中,我們有兩個要求:
- 在 index 路由,我們需要從我們 React 源中返回全頁面的渲染
- 在頁面路由,我們需要返回一個 JSON 字符串,其中符合我們參數的推數據
通過請求我們的 React 組件,調用它的 renderComponentToString
方法,我們把組件轉換為字符串,然後傳給 home.handlebars
模板。
我們利用 Tweets
模型來查詢那些經由數據流鏈接保存到數據庫的推。基於接收到的查詢,我們把組件渲染成 String
。
注意當我們定義想要渲染的組件的時候,用的是 non-JSX 語法。這是因為我們在路由文件裏面,並且它不會被轉化。
讓我們來看一下 render
方法:
<!-- lang: js -->
// Render our ‘home‘ template
res.render(‘home‘, {
markup: markup, // Pass rendered react markup
state: JSON.stringify(tweets) // Pass current state to client side
});
我們返回的不單止是字符串化的標籤,我們還傳回來 state 屬性。為了讓我們的服務端知道上一次它傳給客戶端的狀態,我們需要把上一次的狀態也一起傳給客戶端,這樣才能保持同步。
模板
在我們的應用中有兩套主要模板,都簡單到爆。我們先看佈局視圖,它用於包裝我們的目標模板。
MAIN.HANDLEBARS
<!-- lang: js -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>React Tweets</title>
<link rel="stylesheet" type="text/css" href="css/style.css">
</head>
<body>
{{{ body }}}
<script src="https://cdn.socket.io/socket.io-1.1.0.js"></script>
<script src="js/bundle.js"></script>
</body>
</html>
{{{body}}} 是我們的模板 home.handlebars
加載進去的位置。在這個頁面我們為 socket.io 和我們用 Browserify 生成的 bundle.js 文件添加了 script tags 。
HOME.HANDLEBARS
<!-- lang: js -->
<section id="react-app">{{{ markup }}}</div>
<script id="initial-state" type="application/json">{{{state}}}</script>
在我們的 home.handlebars
模板,我們用來處理在我們路由中生成的組件,然後插入到 {{{markup}}}
。
之後我們處理 state,我們用一個 script tag 來存放從我們服務端過來的狀態 JSON 字符串。當在客戶端初始化 React 組件的時候,我們從這裏拿狀態值,然後刪除它。
客戶端渲染
在服務端我們用 renderComponentToString
來生成組件,不過因為用到 Browserify,我們需要在客戶端提供一個入口來存放狀態值,以及掛載應用組件。
APP.JS
<!-- lang: js -->
/** @jsx React.DOM */
var React = require(‘react‘);
var TweetsApp = require(‘./components/TweetsApp.react‘);
// Snag the initial state that was passed from the server side
var initialState = JSON.parse(document.getElementById(‘initial-state‘).innerHTML)
// Render the components, picking up where react left off on the server
React.renderComponent(
<TweetsApp tweets={initialState}/>,
document.getElementById(‘react-app‘)
);
我們先從我們加到 home.handlebars
的 script 元素上拿我們的初始狀態。解析 JSON 數據,然後調用 React.renderComponent
。
因為我們要用 Browserify 來打包文件,並且要訪問 JSX 轉化,所以當我們把組件作為參數傳遞時,可以用 JSX 語法。
我們用從組件屬性上拿到的狀態值來初始化組件。它可以通過組件內置方法 this.props
來訪問。
最後,我們第二個參數將把我們渲染好的組件掛載到 home.handlebars
的 #react-app
div 元素上。
Isomorphic Components
現在我們萬事俱備了,終於開始要寫邏輯了。下面的文件中,我們渲染了一個叫做 TweetsApp 的自定義組件。
讓我們來創建 TweetsApp 類。
<!-- lang: js -->
module.exports = TweetsApp = React.createClass({
// Render the component
render: function(){
return (
<div className="tweets-app">
<Tweets tweets={this.state.tweets} />
<Loader paging={this.state.paging}/>
<NotificationBar count={this.state.count} onShowNewTweets={this.showNewTweets}/>
</div>
)
}
});
我們的應用有四個子組件。我們需要一個 Tweets 列表顯示組件,一個 Tweet 列表項組件,一個在頁面結果加載的時候用的轉圈圈組件,還有一個通知條。我們把它們包裝到 tweets-app
類的 div 元素中。
和我們從服務端通過組件的 props 把 state 傳出來一樣,我們把當前狀態通過 props 向下傳給子組件。
問題來了,到底狀態從哪裡來的?
在 React 中,通常認為通過 props 傳遞 state 是一種反模式。但是黨我們設置初始狀態,從服務端傳出狀態的時候,不在這種範圍內。因為 getInitialState
方法只在第一次掛載我們的組件的時候會被屌用,我們需要用 componentWillReceiveProps
方法來確保我們再次掛載組件的時候讓它再次拿到狀態:
<!-- lang: js -->
// Set the initial component state
getInitialState: function(props){
props = props || this.props;
// Set initial application state using props
return {
tweets: props.tweets,
count: 0,
page: 0,
paging: false,
skip: 0,
done: false
};
},
componentWillReceiveProps: function(newProps, oldProps){
this.setState(this.getInitialState(newProps));
},
除了我們的推,我們還要從服務端拿到狀態,在客戶端的狀態有一些新的屬性。我們用 count
屬性來跟蹤有多少未讀推。未讀推是那些在頁面加載完成之後,通過 socket.io 加載,但是還沒有看過的。它會在我們每次調用 showNewTweets
的時候更新。
page
屬性保持跟蹤當前我們已經從服務端加載了多少頁數據了。黨開始一頁的加載,在事件開始,但是數據沒有返回的時候,我們的 paging
屬性將會被設為 true,防止重複執行,直到當前的查詢結束。 done
屬性會在所有的頁面都被加載完成之後設置為 true 。
我們的 skip
屬性就像 count
,不過從來不會被重置。這就給了我們一個值,我們當前數據庫中有多少數據是需要無視的,因為我們在除此加載的時候沒有把它們計算在內。這就防止了我們在頁面上讀取到重複推。
這樣依賴,我們已經完全可以在服務端渲染我們的組件了。但是,我們客戶端上狀態也會發生變化,比如說 UI 交互和 socket 事件,我們需要一些方法來處理它們。
我們可以用 componentDidMount
方法來判斷是否可以安全執行這些方法了,因為這個方法只有在組件在客戶端掛載完成的時候會被執行。
<!-- lang: js -->
// Called directly after component rendering, only on client
componentDidMount: function(){
// Preserve self reference
var self = this;
// Initialize socket.io
var socket = io.connect();
// On tweet event emission...
socket.on(‘tweet‘, function (data) {
// Add a tweet to our queue
self.addTweet(data);
});
// Attach scroll event to the window for infinity paging
window.addEventListener(‘scroll‘, this.checkWindowScroll);
},
在上面的代碼中,我們設置了兩個事件來修改狀態,以及訂閱我們的組件渲染狀態。第一個是 socket 堅挺。當有新的推被推送過來的時候,我們調用 addTweet
方法來把它加到未讀隊列中。
<!-- lang: js -->
// Method to add a tweet to our timeline
addTweet: function(tweet){
// Get current application state
var updated = this.state.tweets;
// Increment the unread count
var count = this.state.count + 1;
// Increment the skip count
var skip = this.state.skip + 1;
// Add tweet to the beginning of the tweets array
updated.unshift(tweet);
// Set application state
this.setState({tweets: updated, count: count, skip: skip});
},
Tweets 是在當頁上的未讀推隊列,直到用戶點擊 NotificationBar 組件的時候才會被現實。當被顯示的時候,通過我們調用 showNewTweets
,onShowNewTweets
會被傳遞回來。
<!-- lang: js -->
// Method to show the unread tweets
showNewTweets: function(){
// Get current application state
var updated = this.state.tweets;
// Mark our tweets active
updated.forEach(function(tweet){
tweet.active = true;
});
// Set application state (active tweets + reset unread count)
this.setState({tweets: updated, count: 0});
},
這個方法會被我們的推一直循環,用來設置他們的 active 屬性為 true, 然後設置我們的 state。然後把所有的未顯示推顯示出來(通過 CSS)。
我們的第二個事件是堅挺 window
scroll 事件,並且激活我們的 checkWindowScroll
事件,來檢查是否我們需要加載一個新頁面。
<!-- lang: js -->
// Method to check if more tweets should be loaded, by scroll position
checkWindowScroll: function(){
// Get scroll pos & window data
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
var s = document.body.scrollTop;
var scrolled = (h + s) > document.body.offsetHeight;
// If scrolled enough, not currently paging and not complete...
if(scrolled && !this.state.paging && !this.state.done) {
// Set application state (Paging, Increment page)
this.setState({paging: true, page: this.state.page + 1});
// Get the next page of tweets from the server
this.getPage(this.state.page);
}
},
在我們的 checkWindowScroll
方法中,如果我們確定到達了頁面的底部,並且當前沒有在 paging,而且沒有到達最後一頁,我們調用 getPage
方法:
<!-- lang: js -->
// Method to get JSON from server by page
getPage: function(page){
// Setup our ajax request
var request = new XMLHttpRequest(), self = this;
request.open(‘GET‘, ‘page/‘ + page + "/" + this.state.skip, true);
request.onload = function() {
// If everything is cool...
if (request.status >= 200 && request.status < 400){
// Load our next page
self.loadPagedTweets(JSON.parse(request.responseText));
} else {
// Set application state (Not paging, paging complete)
self.setState({paging: false, done: true});
}
};
// Fire!
request.send();
},
如果推被返回,我們會根據給出的參數返回 JSON 數據,然後再用 loadPagedTweets
方式加載:
<!-- lang: js -->
// Method to load tweets fetched from the server
loadPagedTweets: function(tweets){
// So meta lol
var self = this;
// If we still have tweets...
if(tweets.length > 0) {
// Get current application state
var updated = this.state.tweets;
// Push them onto the end of the current tweets array
tweets.forEach(function(tweet){
updated.push(tweet);
});
// This app is so fast, I actually use a timeout for dramatic effect
// Otherwise you‘d never see our super sexy loader svg
setTimeout(function(){
// Set application state (Not paging, add tweets)
self.setState({tweets: updated, paging: false});
}, 1000);
} else {
// Set application state (Not paging, paging complete)
this.setState({done: true, paging: false});
}
},
這個方法從我們的狀態對象裏面拿到當前的推列表,然後把新的推加載到最後。我在調用 setState 之前用了 setTimeout,因此我們可以確確實實看到加載會有那麽一丟丟延時。
來看看我們完整版組件:
TWEETSAPP
<!-- lang: js -->
/** @jsx React.DOM */
var React = require(‘react‘);
var Tweets = require(‘./Tweets.react.js‘);
var Loader = require(‘./Loader.react.js‘);
var NotificationBar = require(‘./NotificationBar.react.js‘);
// Export the TweetsApp component
module.exports = TweetsApp = React.createClass({
// Method to add a tweet to our timeline
addTweet: function(tweet){
// Get current application state
var updated = this.state.tweets;
// Increment the unread count
var count = this.state.count + 1;
// Increment the skip count
var skip = this.state.skip + 1;
// Add tweet to the beginning of the tweets array
updated.unshift(tweet);
// Set application state
this.setState({tweets: updated, count: count, skip: skip});
},
// Method to get JSON from server by page
getPage: function(page){
// Setup our ajax request
var request = new XMLHttpRequest(), self = this;
request.open(‘GET‘, ‘page/‘ + page + "/" + this.state.skip, true);
request.onload = function() {
// If everything is cool...
if (request.status >= 200 && request.status < 400){
// Load our next page
self.loadPagedTweets(JSON.parse(request.responseText));
} else {
// Set application state (Not paging, paging complete)
self.setState({paging: false, done: true});
}
};
// Fire!
request.send();
},
// Method to show the unread tweets
showNewTweets: function(){
// Get current application state
var updated = this.state.tweets;
// Mark our tweets active
updated.forEach(function(tweet){
tweet.active = true;
});
// Set application state (active tweets + reset unread count)
this.setState({tweets: updated, count: 0});
},
// Method to load tweets fetched from the server
loadPagedTweets: function(tweets){
// So meta lol
var self = this;
// If we still have tweets...
if(tweets.length > 0) {
// Get current application state
var updated = this.state.tweets;
// Push them onto the end of the current tweets array
tweets.forEach(function(tweet){
updated.push(tweet);
});
// This app is so fast, I actually use a timeout for dramatic effect
// Otherwise you‘d never see our super sexy loader svg
setTimeout(function(){
// Set application state (Not paging, add tweets)
self.setState({tweets: updated, paging: false});
}, 1000);
} else {
// Set application state (Not paging, paging complete)
this.setState({done: true, paging: false});
}
},
// Method to check if more tweets should be loaded, by scroll position
checkWindowScroll: function(){
// Get scroll pos & window data
var h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
var s = document.body.scrollTop;
var scrolled = (h + s) > document.body.offsetHeight;
// If scrolled enough, not currently paging and not complete...
if(scrolled && !this.state.paging && !this.state.done) {
// Set application state (Paging, Increment page)
this.setState({paging: true, page: this.state.page + 1});
// Get the next page of tweets from the server
this.getPage(this.state.page);
}
},
// Set the initial component state
getInitialState: function(props){
props = props || this.props;
// Set initial application state using props
return {
tweets: props.tweets,
count: 0,
page: 0,
paging: false,
skip: 0,
done: false
};
},
componentWillReceiveProps: function(newProps, oldProps){
this.setState(this.getInitialState(newProps));
},
// Called directly after component rendering, only on client
componentDidMount: function(){
// Preserve self reference
var self = this;
// Initialize socket.io
var socket = io.connect();
// On tweet event emission...
socket.on(‘tweet‘, function (data) {
// Add a tweet to our queue
self.addTweet(data);
});
// Attach scroll event to the window for infinity paging
window.addEventListener(‘scroll‘, this.checkWindowScroll);
},
// Render the component
render: function(){
return (
<div className="tweets-app">
<Tweets tweets={this.state.tweets} />
<Loader paging={this.state.paging}/>
<NotificationBar count={this.state.count} onShowNewTweets={this.showNewTweets}/>
</div>
)
}
});
子組件
我們的主組件裏面有四個子組件,根據我們當前的狀態值來組成當前的界面。讓我們來看看它們是怎樣和它們的父組件一起工作的。
TWEETS
<!-- lang: js -->
/** @jsx React.DOM */
var React = require(‘react‘);
var Tweet = require(‘./Tweet.react.js‘);
module.exports = Tweets = React.createClass({
// Render our tweets
render: function(){
// Build list items of single tweet components using map
var content = this.props.tweets.map(function(tweet){
return (
<Tweet key={tweet.twid} tweet={tweet} />
)
});
// Return ul filled with our mapped tweets
return (
<ul className="tweets">{content}</ul>
)
}
});
我們的 Tweets 組件通過它的 tweets
屬性傳遞了我們當前狀態的推組,並用來渲染我們的推。在我們的 render
方法中,我們創建了一個推列表,然後執行 map 方法來處理我們的推數組。每次遍歷都會創建一個新的子 Tweet
控件,然後加載到無序列表裏面去。
TWEET
<!-- lang: js -->
/** @jsx React.DOM */
var React = require(‘react‘);
module.exports = Tweet = React.createClass({
render: function(){
var tweet = this.props.tweet;
return (
<li className={"tweet" + (tweet.active ? ‘ active‘ : ‘‘)}>
<img src={tweet.avatar} className="avatar"/>
<blockquote>
<cite>
<a href={"http://www.twitter.com/" + tweet.screenname}>{tweet.author}</a>
<span className="screen-name">@{tweet.screenname}</span>
</cite>
<span className="content">{tweet.body}</span>
</blockquote>
</li>
)
}
});
我們的單個 Tweet 組件,渲染的是列表中獨立的每個推 item。我們通過渲染一個基於推的 active 狀態的 active
class,這樣可以把它們從隊列中隱藏掉。
每個推數據用來填裝預定義的推模板,所以我們的推就像我們期待的那樣被顯示出來。
NOTIFICATIONBAR
<!-- lang: js -->
/** @jsx React.DOM */
var React = require(‘react‘);
module.exports = NotificationBar = React.createClass({
render: function(){
var count = this.props.count;
return (
<div className={"notification-bar" + (count > 0 ? ‘ active‘ : ‘‘)}>
<p>There are {count} new tweets! <a href="#top" onClick={this.props.onShowNewTweets}>Click here to see them.</a></p>
</div>
)
}
});
我們的 Notification Bar 被固定在頁面的頂端,然後用來顯示當前有多少未讀推,當被點擊的時候,顯示所有隊列中的推。
我們基於我們是否有未讀推來確定是否顯示它,這個屬性是 count
在我們的錨點tag,有一個 onClick 句柄,被綁定到它父組件的 showNewTweets
的onShowNewTweets
。這就允許我們在父組件中處理事件,使得我們的狀態值是可控的。
LOADER
<!-- lang: js -->
/** @jsx React.DOM */
var React = require(‘react‘);
module.exports = Loader = React.createClass({
render: function(){
return (
<div className={"loader " + (this.props.paging ? "active" : "")}>
<img src="svg/loader.svg" />
</div>
)
}
});
我們的 loader 組件是一個花式 svg 轉圈圈動畫。它被用在 paging 過程中,表示我們正在加載一個新頁。通過我們的 paging
屬性,設置 active
類,這控制著我們的組件是否會被顯示 (通過 CSS)。
組裝
好了全都完成了,讓我們在命令行中瀟灑的寫下 node server
!你可以在本地執行或者看看下面的 live demo。如果你想看到有新推進來的樣子,最簡單的方法就是把這片文章共享出去,然後你就看到有新推了!
在下一節的學習 React 中,我們將會學習怎麼利用 Facebook 的 Flux 框架來處理單向數據流。 Flux 是 Facebook 建議的 React 英勇的補充框架。我們將會看看一些開源的很牛的實現了 Flux 的庫。
敬請期待。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。