文档章节

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

开源中国匿名会员
 开源中国匿名会员
发布于 2015/04/17 00:46
字数 5538
阅读 4600
收藏 102

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-Site

React 在客戶端很厲害,不過它可以在服務端渲染這就很不一樣了。這是因為 React 用了虛擬 DOM 來代替真的那個,並且允許我們渲染我們的組件。

#開始

好吧屌絲們,讓我們把真傢伙掏出來吧。我們將構建一個英勇,它可以顯示這篇文章的推,並且可以實時加載。下面是一些需求:

  • 它會監聽 Twitter 流 API,並且黨有新的推進來的時候,把它們保存下來
  • 保存的時候,會推送一個事件到客戶端,以便視圖的更新
  • 頁面會在服務端渲染,客戶端只是把他們拿過來
  • 我們將實現無限滾動,每次加載十條推
  • 新推進來的時候將會有一個提醒條提示用戶去看查看他們

下面是我們的效果圖。請去看看實際的 Demo ,確認我們的所有東西都是實時顯示的。

react-tweets-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 文件時,他們將在保存時獲取 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

<!-- 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 的靜態方法。它有三個參數, pageskip & 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 元素中。

react-tweets-demo

和我們從服務端通過組件的 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 組件的時候才會被現實。當被顯示的時候,通過我們調用 showNewTweetsonShowNewTweets 會被傳遞回來。

<!-- 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 句柄,被綁定到它父組件的 showNewTweetsonShowNewTweets 。這就允許我們在父組件中處理事件,使得我們的狀態值是可控的。

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-tweets-demo-2

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

敬請期待。

© 著作权归作者所有

开源中国匿名会员
粉丝 78
博文 104
码字总数 113453
作品 2
徐汇
技术主管
私信 提问
加载中

评论(9)

开源中国匿名会员
开源中国匿名会员 博主

引用来自“crazymus”的评论

为什么要用react呢?没看明白。。。
學學,沒啥壞處。
crazymus
crazymus
为什么要用react呢?没看明白。。。
开源中国匿名会员
开源中国匿名会员 博主

引用来自“Shazi199”的评论

字的筆畫有點多,看著累
加油
Shazi199
Shazi199
字的筆畫有點多,看著累
开源中国匿名会员
开源中国匿名会员 博主

引用来自“刘刚”的评论

js的库太多了15
沒辦法,都看看唄。知道概念就行了。 React 剛開始看感覺跟 JSP/ASP 之類的流程太像了。
开源中国匿名会员
开源中国匿名会员 博主

引用来自“jxpxwh”的评论

是繁体字啊。
玩WOW,懶得切了。
jxpxwh
jxpxwh
是繁体字啊。
刘刚
刘刚
js的库太多了15
开源中国匿名会员
开源中国匿名会员 博主
大半夜的,唉。
學習 React.js:用 React.js 和 Flux 創建一個簡單的購物車

[Creating A Simple Shopping Cart with React.js and Flux][1] Ken Wheeler (@ken_wheeler) 簡介 歡迎來到學習 React 的第四章這也是最後一章!到現在,我們已經學習了怎樣利用 React 的 AP...

开源中国匿名会员
2015/04/20
4.7K
10
學習 React.js:瞭解 Flux,React.js 的架構

[Getting To Know Flux, the React.js Architecture][1] Ken Wheeler (@ken_wheeler) 簡介 歡迎來到學習 React 的第三章。今天我們將會學習臉書的 Flux 架構的工作方式,以及我們怎麼把它應該...

开源中国匿名会员
2015/04/17
4.1K
0
OSChina 技术周刊第三十期 —— IT 公司开源软件整理计划

每周技术抢先看,总有你想要的! 移动开发 【翻译】Intel® INDE for Google Android Studio 【软件】移动应用开发框架 Samurai-Native 【博客】學習 React.js:用 Node 和 React.js 創建一個...

OSC编辑部
2015/04/19
4.2K
9
【开源访谈】Python China 创始人 Hsiaoming Yang 访谈实录

【嘉宾简介】 Hsiaoming Yang,Python china创始人,GitHub 上最活跃的国内开发者之一,常用 ID:lepture。 【正文】 1、请简单地介绍一下你自己(技术背景、学习经历、工作经历)。 我叫 Hs...

孔小菜
2015/06/01
5.7K
16
Angular 2 系列: 简介

[Angular 2 Series: Introduction][1] By Max on April 2, 2015 現在, [Angular 2][2] 和 [Ionic 2][3] 呼之欲出。我們超讚的小隊正在為下一個大版本的 Ionic 和 Angular 2 在努力工作。我為...

开源中国匿名会员
2015/05/02
640
0

没有更多内容

加载失败,请刷新页面

加载更多

关于运维,该怎么决定它的方向,这个似工作又似兴趣的存在

我之前主要从事网络、桌面、机房管理等相关工作,这些工作使我迷惘,这应该是大多数运维人都经历过的过程; 18年国庆,我从国内前三的消费金融公司裸辞,下海创业,就是想要摆脱这样的困境。...

网络小虾米
4分钟前
1
0
Java Timer的用法

Timer timer = new Timer(); timer.schedule(new TimerTask() { public void run() { System.out.println("11232"); } }, 200000 , 1000); public void schedule(TimerTask task, long delay......

林词
8分钟前
3
0
使用js动态加载外部js文件以及动态创建script脚本

动态脚本指的是在页面加载时不存在,但将来的某一时刻通过修改该DOM动态添加的脚本。和操作HTML元素一样,创建动态脚本也有两种方式:插入外部文件和直接插入JavaScript代码。 动态加载外的外...

Bing309
15分钟前
2
0
从零开始入门 K8s | Kubernetes 网络概念及策略控制

作者 | 阿里巴巴高级技术专家 叶磊 一、Kubernetes 基本网络模型 本文来介绍一下 Kubernetes 对网络模型的一些想法。大家知道 Kubernetes 对于网络具体实现方案,没有什么限制,也没有给出特...

阿里巴巴云原生
19分钟前
2
0
天气获取

本文转载于:专业的前端网站➨天气获取 $.get("http://wthrcdn.etouch.cn/WeatherApi", { citykey: cityCode }, function (d) { //创建文档对象 var parser = new ......

前端老手
19分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部