學習 React.js:用 Node 和 React.js 創建一個實時的 Twitter 流

前端之家收集整理的这篇文章主要介绍了學習 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 庫

##服務端

讓我們開始構建我們應用的服務端。從這裏下載工程文件,然後跟著下面做:

目錄結構

  1. <!-- lang: js -->
  2. components/ // React Components Directory
  3. ---- Loader.react.js // Loader Component
  4. ---- NotificationBar.react.js // Notification Bar Component
  5. ---- Tweet.react.js // Single Tweet Component
  6. ---- Tweets.react.js // Tweets Component
  7. ---- TweetsApp.react.js // Main App Component
  8. models/ // Mongoose Models Directory
  9. ---- Tweet.js // Our Mongoose Tweet Model
  10. public/ // Static Files Directory
  11. ---- css
  12. ---- js
  13. ---- svg
  14. utils/
  15. ----streamHandler.js // Utility method for handling Twitter stream callbacks
  16. views/ // Server Side Handlebars Views
  17. ----layouts
  18. -------- main.handlebars
  19. ---- home.handlebars
  20. app.js // Client side main
  21. config.js // App configuration
  22. package.json
  23. routes.js // Route definitions
  24. server.js // Server side main

PACKAGE.JSON

  1. <!-- lang: js -->
  2. {
  3. "name": "react-isomorph","version": "0.0.0","description": "Isomorphic React Example","main": "app.js","scripts": {
  4. "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"
  5. },"author": "Ken Wheeler","license": "MIT","dependencies": {
  6. "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"
  7. },"devDependencies": {
  8. "browserify": "~6.0.3","nodemon": "^1.2.1","reactify": "~0.14.0","uglify-js": "~2.4.15","watchify": "~2.0.0"
  9. },"browserify": {
  10. "transform": [
  11. "reactify"
  12. ]
  13. }
  14. }

如果上面的你都做了,只要簡單的執行一下 npm install 然後去喝杯水。等你回來之後,我們的所有需要的依賴包應該就會準備好了,然後該我們來動手了。

現在我們有一些可以用到的命令:

  • npm run watch 執行該命令會啟動 watchify 的監控,當我們編輯 js 文件時,他們將在保存時獲取 browserified
  • npm 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

  1. <!-- lang: js -->
  2. // Require our dependencies
  3. 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');
  4.  
  5. // Create an express instance and set a port variable
  6. var app = express();
  7. var port = process.env.PORT || 8080;
  8.  
  9. // Set handlebars as the templating engine
  10. app.engine('handlebars',exphbs({ defaultLayout: 'main'}));
  11. app.set('view engine','handlebars');
  12.  
  13. // Disable etag headers on responses
  14. app.disable('etag');
  15.  
  16. // Connect to our mongo database
  17. mongoose.connect('mongodb://localhost/react-tweets');
  18.  
  19. // Create a new ntwitter instance
  20. var twit = new twitter(config.twitter);
  21.  
  22. // Index Route
  23. app.get('/',routes.index);
  24.  
  25. // Page Route
  26. app.get('/page/:page/:skip',routes.page);
  27.  
  28. // Set /public as our static content dir
  29. app.use("/",express.static(__dirname + "/public/"));
  30.  
  31. // Fire it up (start our server)
  32. var server = http.createServer(app).listen(port,function() {
  33. console.log('Express server listening on port ' + port);
  34. });
  35.  
  36. // Initialize socket.io
  37. var io = require('socket.io').listen(server);
  38.  
  39. // Set a stream listener for tweets matching tracking keywords
  40. twit.stream('statuses/filter',{ track: 'scotch_io,#scotchio'},function(stream){
  41. streamHandler(stream,io);
  42. });

nTwitter 允許我們訪問 Twitter Streaming API,因此我們使用了 statuses/filter 端點,以及 track 屬性,然後返回使用了 #scotchio hash 標籤或者提到 scotch_io 的推。你可以使用 Twitter Streaming API 裏面提供的端點,隨意修改這個查詢鏈接。

##Models

在我們的應用中,使用了 Mongoose 來定義我們的 Tweet 模型。當從 Twitter steam 接收到我們的數據的時候,我們需要把它們保存到什麼地方,然後還需要靜態的查詢方法,用來配合應用的查詢參數返回子數據集。

TWEET.JS

  1. <!-- lang: js -->
  2. var mongoose = require('mongoose');
  3.  
  4. // Create a new schema for our tweet data
  5. var schema = new mongoose.Schema({
  6. twid : String,active : Boolean,author : String,avatar : String,body : String,date : Date,screenname : String
  7. });
  8.  
  9. // Create a static getTweets method to return tweet data from the db
  10. schema.statics.getTweets = function(page,skip,callback) {
  11.  
  12. var tweets = [],start = (page * 10) + (skip * 1);
  13.  
  14. // Query the db,using skip and limit to achieve page chunks
  15. Tweet.find({},'twid active author avatar body date screenname',{skip: start,limit: 10}).sort({date: 'desc'}).exec(function(err,docs){
  16.  
  17. // If everything is cool...
  18. if(!err) {
  19. tweets = docs; // We got tweets
  20. tweets.forEach(function(tweet){
  21. tweet.active = true; // Set them to active
  22. });
  23. }
  24.  
  25. // Pass them back to the specified callback
  26. callback(tweets);
  27.  
  28. });
  29.  
  30. };
  31.  
  32. // Return a Tweet model based upon the defined schema
  33. module.exports = Tweet = mongoose.model('Tweet',schema);

在定義了我們的 schema 之後,我們創建一個叫做 getTweets 的靜態方法。它有三個參數, pageskip & callback

當我們有一個應用,不但在服務端渲染,而且還在後台有數據流不斷保存數據到數據庫,我們需要一個方法來確保,當我們請求下一頁推的時候,它能處理我們已經加載到服務端的推。

這就是 skip 參數的作用。如果我們有 2 條新的推進來,然後我們點了下一頁,我們需要往前移兩位,以確保索引的正確性,這樣我們才不會拿到重複的數據。

##處理數據流

當我們的 Twitter stream 鏈接發送一個新的 Tweet 事件,我們需要一個方法來接收數據,把它們保存到數據庫,並且向客戶端推送。

STREAMHANDLER.JS

  1. <!-- lang: js -->
  2. var Tweet = require('../models/Tweet');
  3.  
  4. module.exports = function(stream,io){
  5.  
  6. // When tweets get sent our way ...
  7. stream.on('data',function(data) {
  8.  
  9. // Construct a new tweet object
  10. var tweet = {
  11. 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']
  12. };
  13.  
  14. // Create a new model instance with our object
  15. var tweetEntry = new Tweet(tweet);
  16.  
  17. // Save 'er to the database
  18. tweetEntry.save(function(err) {
  19. if (!err) {
  20. // If everything is cool,socket.io emits the tweet.
  21. io.emit('tweet',tweet);
  22. }
  23. });
  24.  
  25. });
  26.  
  27. };

我們先用模型發送請求,然後我們的流推送事件,獲取那些希望要保存的數據,保存好,然後通過 socket 事件把我們剛保存下來的數據推送到客戶端。

##路由

我們的路由也是這篇文章中很精彩的一部分。我們來看看 routes.js

ROUTES.JS

  1. <!-- lang: js -->
  2. var JSX = require('node-jsx').install(),React = require('react'),TweetsApp = require('./components/TweetsApp.react'),Tweet = require('./models/Tweet');
  3. module.exports = {
  4.  
  5. index: function(req,res) {
  6. // Call static model method to get tweets in the db
  7. Tweet.getTweets(0,function(tweets,pages) {
  8.  
  9. // Render React to a string,passing in our fetched tweets
  10. var markup = React.renderComponentToString(
  11. TweetsApp({
  12. tweets: tweets
  13. })
  14. );
  15.  
  16. // Render our 'home' template
  17. res.render('home',{
  18. markup: markup,// Pass rendered react markup
  19. state: JSON.stringify(tweets) // Pass current state to client side
  20. });
  21.  
  22. });
  23. },page: function(req,res) {
  24. // Fetch tweets by page via param
  25. Tweet.getTweets(req.params.page,req.params.skip,function(tweets) {
  26.  
  27. // Render as JSON
  28. res.send(tweets);
  29.  
  30. });
  31. }
  32.  
  33. }

在上面的代碼中,我們有兩個要求:

  • 在 index 路由,我們需要從我們 React 源中返回全頁面的渲染
  • 在頁面路由,我們需要返回一個 JSON 字符串,其中符合我們參數的推數據

通過請求我們的 React 組件,調用它的 renderComponentToString 方法,我們把組件轉換為字符串,然後傳給 home.handlebars 模板。

我們利用 Tweets 模型來查詢那些經由數據流鏈接保存到數據庫的推。基於接收到的查詢,我們把組件渲染成 String

注意當我們定義想要渲染的組件的時候,用的是 non-JSX 語法。這是因為我們在路由文件裏面,並且它不會被轉化。

讓我們來看一下 render 方法

  1. <!-- lang: js -->
  2. // Render our 'home' template
  3. res.render('home',{
  4. markup: markup,// Pass rendered react markup
  5. state: JSON.stringify(tweets) // Pass current state to client side
  6. });

我們返回的不單止是字符串化的標籤,我們還傳回來 state 屬性。為了讓我們的服務端知道上一次它傳給客戶端的狀態,我們需要把上一次的狀態也一起傳給客戶端,這樣才能保持同步。

##模板

在我們的應用中有兩套主要模板,都簡單到爆。我們先看佈局視圖,它用於包裝我們的目標模板。

MAIN.HANDLEBARS

  1. <!-- lang: js -->
  2. <!doctype html>
  3. <html lang="en">
  4. <head>
  5. <Meta charset="utf-8">
  6. <Meta name="viewport" content="width=device-width,initial-scale=1">
  7. <title>React Tweets</title>
  8. <link rel="stylesheet" type="text/css" href="css/style.css">
  9. </head>
  10. <body>
  11. {{{ body }}}
  12. <script src="https://cdn.socket.io/socket.io-1.1.0.js"></script>
  13. <script src="js/bundle.js"></script>
  14. </body>
  15. </html>

{{{body}}} 是我們的模板 home.handlebars 加載進去的位置。在這個頁面我們為 socket.io 和我們用 Browserify 生成的 bundle.js 文件添加了 script tags 。

HOME.HANDLEBARS

  1. <!-- lang: js -->
  2. <section id="react-app">{{{ markup }}}</div>
  3. <script id="initial-state" type="application/json">{{{state}}}</script>

在我們的 home.handlebars 模板,我們用來處理在我們路由中生成的組件,然後插入到 {{{markup}}}

之後我們處理 state,我們用一個 script tag 來存放從我們服務端過來的狀態 JSON 字符串。當在客戶端初始化 React 組件的時候,我們從這裏拿狀態值,然後刪除它。

##客戶端渲染

在服務端我們用 renderComponentToString生成組件,不過因為用到 Browserify,我們需要在客戶端提供一個入口來存放狀態值,以及掛載應用組件。

APP.JS

  1. <!-- lang: js -->
  2. /** @jsx React.DOM */
  3.  
  4. var React = require('react');
  5. var TweetsApp = require('./components/TweetsApp.react');
  6.  
  7. // Snag the initial state that was passed from the server side
  8. var initialState = JSON.parse(document.getElementById('initial-state').innerHTML)
  9.  
  10. // Render the components,picking up where react left off on the server
  11. React.renderComponent(
  12. <TweetsApp tweets={initialState}/>,document.getElementById('react-app')
  13. );

我們先從我們加到 home.handlebars 的 script 元素上拿我們的初始狀態。解析 JSON 數據,然後調用 React.renderComponent

因為我們要用 Browserify 來打包文件,並且要訪問 JSX 轉化,所以當我們把組件作為參數傳遞時,可以用 JSX 語法。

我們用從組件屬性上拿到的狀態值來初始化組件。它可以通過組件內置方法 this.props 來訪問。

最後,我們第二個參數將把我們渲染好的組件掛載到 home.handlebars#react-app div 元素上。

##Isomorphic Components

現在我們萬事俱備了,終於開始要寫邏輯了。下面的文件中,我們渲染了一個叫做 TweetsApp 的自定義組件。

讓我們來創建 TweetsApp 類。

  1. <!-- lang: js -->
  2. module.exports = TweetsApp = React.createClass({
  3. // Render the component
  4. render: function(){
  5.  
  6. return (
  7. <div className="tweets-app">
  8. <Tweets tweets={this.state.tweets} />
  9. <Loader paging={this.state.paging}/>
  10. <NotificationBar count={this.state.count} onShowNewTweets={this.showNewTweets}/>
  11. </div>
  12. )
  13.  
  14. }
  15. });

我們的應用有四個子組件。我們需要一個 Tweets 列表顯示組件,一個 Tweet 列表項組件,一個在頁面結果加載的時候用的轉圈圈組件,還有一個通知條。我們把它們包裝到 tweets-app 類的 div 元素中。

和我們從服務端通過組件的 props 把 state 傳出來一樣,我們把當前狀態通過 props 向下傳給子組件。

問題來了,到底狀態從哪裡來的?

在 React 中,通常認為通過 props 傳遞 state 是一種反模式。但是黨我們設置初始狀態,從服務端傳出狀態的時候,不在這種範圍內。因為 getInitialState 方法只在第一次掛載我們的組件的時候會被屌用,我們需要用 componentWillReceiveProps 方法來確保我們再次掛載組件的時候讓它再次拿到狀態:

  1. <!-- lang: js -->
  2. // Set the initial component state
  3. getInitialState: function(props){
  4.  
  5. props = props || this.props;
  6.  
  7. // Set initial application state using props
  8. return {
  9. tweets: props.tweets,count: 0,page: 0,paging: false,skip: 0,done: false
  10. };
  11.  
  12. },componentWillReceiveProps: function(newProps,oldProps){
  13. this.setState(this.getInitialState(newProps));
  14. },

除了我們的推,我們還要從服務端拿到狀態,在客戶端的狀態有一些新的屬性。我們用 count 屬性來跟蹤有多少未讀推。未讀推是那些在頁面加載完成之後,通過 socket.io 加載,但是還沒有看過的。它會在我們每次調用 showNewTweets 的時候更新。

page 屬性保持跟蹤當前我們已經從服務端加載了多少頁數據了。黨開始一頁的加載,在事件開始,但是數據沒有返回的時候,我們的 paging 屬性將會被設為 true,防止重複執行,直到當前的查詢結束。 done 屬性會在所有的頁面都被加載完成之後設置為 true 。

我們的 skip 屬性就像 count,不過從來不會被重置。這就給了我們一個值,我們當前數據庫中有多少數據是需要無視的,因為我們在除此加載的時候沒有把它們計算在內。這就防止了我們在頁面上讀取到重複推。

這樣依賴,我們已經完全可以在服務端渲染我們的組件了。但是,我們客戶端上狀態也會發生變化,比如說 UI 交互和 socket 事件,我們需要一些方法來處理它們。

我們可以用 componentDidMount 方法來判斷是否可以安全執行這些方法了,因為這個方法只有在組件在客戶端掛載完成的時候會被執行。

  1. <!-- lang: js -->
  2. // Called directly after component rendering,only on client
  3. componentDidMount: function(){
  4.  
  5. // Preserve self reference
  6. var self = this;
  7.  
  8. // Initialize socket.io
  9. var socket = io.connect();
  10.  
  11. // On tweet event emission...
  12. socket.on('tweet',function (data) {
  13.  
  14. // Add a tweet to our queue
  15. self.addTweet(data);
  16.  
  17. });
  18.  
  19. // Attach scroll event to the window for infinity paging
  20. window.addEventListener('scroll',this.checkWindowScroll);
  21.  
  22. },

在上面的代碼中,我們設置了兩個事件來修改狀態,以及訂閱我們的組件渲染狀態。第一個是 socket 堅挺。當有新的推被推送過來的時候,我們調用 addTweet 方法來把它加到未讀隊列中。

  1. <!-- lang: js -->
  2. // Method to add a tweet to our timeline
  3. addTweet: function(tweet){
  4.  
  5. // Get current application state
  6. var updated = this.state.tweets;
  7.  
  8. // Increment the unread count
  9. var count = this.state.count + 1;
  10.  
  11. // Increment the skip count
  12. var skip = this.state.skip + 1;
  13.  
  14. // Add tweet to the beginning of the tweets array
  15. updated.unshift(tweet);
  16.  
  17. // Set application state
  18. this.setState({tweets: updated,count: count,skip: skip});
  19.  
  20. },

Tweets 是在當頁上的未讀推隊列,直到用戶點擊 NotificationBar 組件的時候才會被現實。當被顯示的時候,通過我們調用 showNewTweetsonShowNewTweets 會被傳遞回來。

  1. <!-- lang: js -->
  2. // Method to show the unread tweets
  3. showNewTweets: function(){
  4.  
  5. // Get current application state
  6. var updated = this.state.tweets;
  7.  
  8. // Mark our tweets active
  9. updated.forEach(function(tweet){
  10. tweet.active = true;
  11. });
  12.  
  13. // Set application state (active tweets + reset unread count)
  14. this.setState({tweets: updated,count: 0});
  15.  
  16. },

這個方法會被我們的推一直循環,用來設置他們的 active 屬性為 true, 然後設置我們的 state。然後把所有的未顯示推顯示出來(通過 CSS)。

我們的第二個事件是堅挺 window scroll 事件,並且激活我們的 checkWindowScroll 事件,來檢查是否我們需要加載一個新頁面。

  1. <!-- lang: js -->
  2. // Method to check if more tweets should be loaded,by scroll position
  3. checkWindowScroll: function(){
  4.  
  5. // Get scroll pos & window data
  6. var h = Math.max(document.documentElement.clientHeight,window.innerHeight || 0);
  7. var s = document.body.scrollTop;
  8. var scrolled = (h + s) > document.body.offsetHeight;
  9.  
  10. // If scrolled enough,not currently paging and not complete...
  11. if(scrolled && !this.state.paging && !this.state.done) {
  12.  
  13. // Set application state (Paging,Increment page)
  14. this.setState({paging: true,page: this.state.page + 1});
  15.  
  16. // Get the next page of tweets from the server
  17. this.getPage(this.state.page);
  18.  
  19. }
  20. },

在我們的 checkWindowScroll 方法中,如果我們確定到達了頁面的底部,並且當前沒有在 paging,而且沒有到達最後一頁,我們調用 getPage 方法:

  1. <!-- lang: js -->
  2. // Method to get JSON from server by page
  3. getPage: function(page){
  4.  
  5. // Setup our ajax request
  6. var request = new XMLHttpRequest(),self = this;
  7. request.open('GET','page/' + page + "/" + this.state.skip,true);
  8. request.onload = function() {
  9.  
  10. // If everything is cool...
  11. if (request.status >= 200 && request.status < 400){
  12.  
  13. // Load our next page
  14. self.loadPagedTweets(JSON.parse(request.responseText));
  15.  
  16. } else {
  17.  
  18. // Set application state (Not paging,paging complete)
  19. self.setState({paging: false,done: true});
  20.  
  21. }
  22. };
  23.  
  24. // Fire!
  25. request.send();
  26.  
  27. },

如果推被返回,我們會根據給出的參數返回 JSON 數據,然後再用 loadPagedTweets 方式加載:

  1. <!-- lang: js -->
  2. // Method to load tweets fetched from the server
  3. loadPagedTweets: function(tweets){
  4.  
  5. // So Meta lol
  6. var self = this;
  7.  
  8. // If we still have tweets...
  9. if(tweets.length > 0) {
  10.  
  11. // Get current application state
  12. var updated = this.state.tweets;
  13.  
  14. // Push them onto the end of the current tweets array
  15. tweets.forEach(function(tweet){
  16. updated.push(tweet);
  17. });
  18.  
  19. // This app is so fast,I actually use a timeout for dramatic effect
  20. // Otherwise you'd never see our super sexy loader svg
  21. setTimeout(function(){
  22.  
  23. // Set application state (Not paging,add tweets)
  24. self.setState({tweets: updated,paging: false});
  25.  
  26. },1000);
  27.  
  28. } else {
  29.  
  30. // Set application state (Not paging,paging complete)
  31. this.setState({done: true,paging: false});
  32.  
  33. }
  34. },

這個方法從我們的狀態對象裏面拿到當前的推列表,然後把新的推加載到最後。我在調用 setState 之前用了 setTimeout,因此我們可以確確實實看到加載會有那麽一丟丟延時。

來看看我們完整版組件:

TWEETSAPP

  1. <!-- lang: js -->
  2. /** @jsx React.DOM */
  3.  
  4. var React = require('react');
  5. var Tweets = require('./Tweets.react.js');
  6. var Loader = require('./Loader.react.js');
  7. var NotificationBar = require('./NotificationBar.react.js');
  8.  
  9. // Export the TweetsApp component
  10. module.exports = TweetsApp = React.createClass({
  11.  
  12. // Method to add a tweet to our timeline
  13. addTweet: function(tweet){
  14.  
  15. // Get current application state
  16. var updated = this.state.tweets;
  17.  
  18. // Increment the unread count
  19. var count = this.state.count + 1;
  20.  
  21. // Increment the skip count
  22. var skip = this.state.skip + 1;
  23.  
  24. // Add tweet to the beginning of the tweets array
  25. updated.unshift(tweet);
  26.  
  27. // Set application state
  28. this.setState({tweets: updated,// Method to get JSON from server by page
  29. getPage: function(page){
  30.  
  31. // Setup our ajax request
  32. var request = new XMLHttpRequest(),// Method to show the unread tweets
  33. showNewTweets: function(){
  34.  
  35. // Get current application state
  36. var updated = this.state.tweets;
  37.  
  38. // Mark our tweets active
  39. updated.forEach(function(tweet){
  40. tweet.active = true;
  41. });
  42.  
  43. // Set application state (active tweets + reset unread count)
  44. this.setState({tweets: updated,// Method to load tweets fetched from the server
  45. loadPagedTweets: function(tweets){
  46.  
  47. // So Meta lol
  48. var self = this;
  49.  
  50. // If we still have tweets...
  51. if(tweets.length > 0) {
  52.  
  53. // Get current application state
  54. var updated = this.state.tweets;
  55.  
  56. // Push them onto the end of the current tweets array
  57. tweets.forEach(function(tweet){
  58. updated.push(tweet);
  59. });
  60.  
  61. // This app is so fast,// Method to check if more tweets should be loaded,// Set the initial component state
  62. getInitialState: function(props){
  63.  
  64. props = props || this.props;
  65.  
  66. // Set initial application state using props
  67. return {
  68. tweets: props.tweets,// Called directly after component rendering,only on client
  69. componentDidMount: function(){
  70.  
  71. // Preserve self reference
  72. var self = this;
  73.  
  74. // Initialize socket.io
  75. var socket = io.connect();
  76.  
  77. // On tweet event emission...
  78. socket.on('tweet',function (data) {
  79.  
  80. // Add a tweet to our queue
  81. self.addTweet(data);
  82.  
  83. });
  84.  
  85. // Attach scroll event to the window for infinity paging
  86. window.addEventListener('scroll',this.checkWindowScroll);
  87.  
  88. },// Render the component
  89. render: function(){
  90.  
  91. return (
  92. <div className="tweets-app">
  93. <Tweets tweets={this.state.tweets} />
  94. <Loader paging={this.state.paging}/>
  95. <NotificationBar count={this.state.count} onShowNewTweets={this.showNewTweets}/>
  96. </div>
  97. )
  98.  
  99. }
  100.  
  101. });

##子組件

我們的主組件裏面有四個子組件,根據我們當前的狀態值來組成當前的界面。讓我們來看看它們是怎樣和它們的父組件一起工作的。

TWEETS

  1. <!-- lang: js -->
  2. /** @jsx React.DOM */
  3.  
  4. var React = require('react');
  5. var Tweet = require('./Tweet.react.js');
  6.  
  7. module.exports = Tweets = React.createClass({
  8.  
  9. // Render our tweets
  10. render: function(){
  11.  
  12. // Build list items of single tweet components using map
  13. var content = this.props.tweets.map(function(tweet){
  14. return (
  15. <Tweet key={tweet.twid} tweet={tweet} />
  16. )
  17. });
  18.  
  19. // Return ul filled with our mapped tweets
  20. return (
  21. <ul className="tweets">{content}</ul>
  22. )
  23.  
  24. }
  25.  
  26. });

我們的 Tweets 組件通過它的 tweets 屬性傳遞了我們當前狀態的推組,並用來渲染我們的推。在我們的 render 方法中,我們創建了一個推列表,然後執行 map 方法來處理我們的推數組。每次遍歷都會創建一個新的子 Tweet 控件,然後加載到無序列表裏面去。

TWEET

  1. <!-- lang: js -->
  2. /** @jsx React.DOM */
  3.  
  4. var React = require('react');
  5.  
  6. module.exports = Tweet = React.createClass({
  7. render: function(){
  8. var tweet = this.props.tweet;
  9. return (
  10. <li className={"tweet" + (tweet.active ? ' active' : '')}>
  11. <img src={tweet.avatar} className="avatar"/>
  12. <blockquote>
  13. <cite>
  14. <a href={"http://www.twitter.com/" + tweet.screenname}>{tweet.author}</a>
  15. <span className="screen-name">@{tweet.screenname}</span>
  16. </cite>
  17. <span className="content">{tweet.body}</span>
  18. </blockquote>
  19. </li>
  20. )
  21. }
  22. });

我們的單個 Tweet 組件,渲染的是列表中獨立的每個推 item。我們通過渲染一個基於推的 active 狀態的 active class,這樣可以把它們從隊列中隱藏掉。

每個推數據用來填裝預定義的推模板,所以我們的推就像我們期待的那樣被顯示出來。

NOTIFICATIONBAR

  1. <!-- lang: js -->
  2. /** @jsx React.DOM */
  3.  
  4. var React = require('react');
  5.  
  6. module.exports = NotificationBar = React.createClass({
  7. render: function(){
  8. var count = this.props.count;
  9. return (
  10. <div className={"notification-bar" + (count > 0 ? ' active' : '')}>
  11. <p>There are {count} new tweets! <a href="#top" onClick={this.props.onShowNewTweets}>Click here to see them.</a></p>
  12. </div>
  13. )
  14. }
  15. });

我們的 Notification Bar 被固定在頁面的頂端,然後用來顯示當前有多少未讀推,當被點擊的時候,顯示所有隊列中的推。

我們基於我們是否有未讀推來確定是否顯示它,這個屬性是 count

在我們的錨點tag,有一個 onClick 句柄,被綁定到它父組件的 showNewTweetsonShowNewTweets 。這就允許我們在父組件中處理事件,使得我們的狀態值是可控的。

LOADER

  1. <!-- lang: js -->
  2. /** @jsx React.DOM */
  3.  
  4. var React = require('react');
  5.  
  6. module.exports = Loader = React.createClass({
  7. render: function(){
  8. return (
  9. <div className={"loader " + (this.props.paging ? "active" : "")}>
  10. <img src="svg/loader.svg" />
  11. </div>
  12. )
  13. }
  14. });

我們的 loader 組件是一個花式 svg 轉圈圈動畫。它被用在 paging 過程中,表示我們正在加載一個新頁。通過我們的 paging 屬性,設置 active 類,這控制著我們的組件是否會被顯示 (通過 CSS)。

##組裝

好了全都完成了,讓我們在命令行中瀟灑的寫下 node server !你可以在本地執行或者看看下面的 live demo。如果你想看到有新推進來的樣子,最簡單的方法就是把這片文章共享出去,然後你就看到有新推了!

在下一節的學習 React 中,我們將會學習怎麼利用 Facebook 的 Flux 框架來處理單向數據流。 Flux 是 Facebook 建議的 React 英勇的補充框架。我們將會看看一些開源的很牛的實現了 Flux 的庫。

敬請期待。

猜你在找的React相关文章