送給前端開發者的一份新年禮物

你們好,新年快樂!今天,我開源了一個 React 的項目。這個項目雖小,可是五臟六腑俱全。react

先來介紹下這個項目的技術棧:ios

  • React 全家桶:React 16 + Redux + React-router 4.0 + Immutable.js
  • ES6 + ES7 語法
  • 網絡請求:Axios + Socket.io
  • UI 框架:Antd-mobile
  • 後端:Express + MongoDB

React 是什麼

React 其實只是一個 UI 框架,頻繁進行 DOM 操做的代價是很昂貴的,因此 React 使用了虛擬 DOM 的技術,每當狀態發生改變,就會生成新的虛擬 DOM 並與本來的進行改變,讓變化的地方去渲染。而且爲了性能的考慮,只對狀態進行淺比較(這是一個很大的優化點)。git

React 已經成爲當今最流行的框架之一,可是他的學習成本並不低而且須要你有一個良好的 JS 基礎。因爲React 只是一個 UI 框架,因此你想完成一個項目,你就得使用他的全家桶,更加提升了一個學習成本。因此本課程也是針對初學者,讓初學者可以快速的上手 React 。github

React 組件

如何寫好規劃好一個組件決定了你的 React 玩的溜不溜。一個組件你須要考慮他提供幾個對外暴露的接口,內部狀態經過局部狀態改變仍是全局狀態改變好。而且你的組件應該是利於複用和維護的。數據庫

組件的生命週期

生命週期

  • render 函數會在 UI 渲染時調用,你屢次渲染就會屢次調用,因此控制一個組件的重複渲染對於性能優化很重要
  • componentDidMount 函數只會在組件渲染之後調用一次,一般會在這個發起數據請求
  • shouldComponentUpdate 是一個很重要的函數,他的返回值決定了是否須要生成一個新的虛擬 DOM 去和以前的比較。一般遇到的性能問題你能夠在這裏獲得很好的解決
  • componentWillMount 函數會在組件即將銷燬時調用,項目中在清除聊天未讀消息中用到了這個函數
父子組件參數傳遞

在項目中我使用的方式是單個模塊頂層父組件經過 connect 與 Redux 通訊。子組件經過參數傳遞的方式獲取須要的參數,對於參數類型咱們應該規則好,便於後期 debug。redux

性能上考慮,咱們在參數傳遞的過程當中儘可能只傳遞必須的參數。後端

路由

在 React-router 4.0 版本,官方也選擇了組件的方式去書寫路由。數組

下面介紹一下項目中使用到的按需加載路由高階組件瀏覽器

import React, { Component } from "react";
// 其實高階組件就是一個組件經過參數傳遞的方式生成新的組件
export default function asyncComponent(importComponent) {
  class AsyncComponent extends Component {
    constructor(props) {
      super(props);
        // 存儲組件
      this.state = {
        component: null
      };
    }

    async componentDidMount() {
    // 引入組件是須要下載文件的,因此是個異步操做
      const { default: component } = await importComponent();

      this.setState({
        component: component
      });
    }
    // 渲染時候判斷文件下完沒有,下完了就渲染出來
    render() {
      const C = this.state.component;

      return C ? <C {...this.props} /> : null; } } return AsyncComponent; } 複製代碼

Redux

Redux 一般是個另新手困惑的點。首先,不是每一個項目都須要使用 Redux,組件間通訊很少,邏輯不復雜,你也就不須要使用這個庫,畢竟這個使用這個庫的開發成本很大。緩存

Redux 是與 React 解耦的,因此你想和 Redux 通訊就須要使用 React-redux,你在 action 中使用異步請求就得使用 Redux-thunk,由於 action 只支持同步操做。

Redux 的組成

Redux 由三部分組成:action,store,reducer。

Action 顧名思義,就是你發起一個操做,具體使用以下:

export function getOrderSuccess(data) {
// 返回的就是一個 action,除了第一個參數通常這樣寫,其他的參數名隨意
  return { type: GET_ORDER_SUCCESS, payload: data };
}
複製代碼

Action 發出去之後,會丟給 Reducer。Reducer 是一個純函數(不依賴於且不改變它做用域以外的變量狀態的函數),他接收一個以前的 state 和 action 參數,而後返回一個新的 state 給 store。

export default function(state = initialState, action) {
  switch (action.type) {
    case GET_ALL_ORDERS:
      return state.set("allOrders", action.payload);
    default:
      break;
  }
  return state;
}
複製代碼

Store 很容易和 state 混淆。你能夠把 Store 當作一個容器,state 存儲在這個容器中。Store 提供一些 API 讓你能夠對 state 進行訪問,改變等等。

PS:state 只容許在 reducer 中進行改變。

說明完了這些基本概念,我以爲是時候對 Redux 進行一點深刻的挖掘。

本身實現 Redux

以前說過 Store 是個容器,那麼能夠寫下以下代碼

class Store {
  constructor() {}
    
    // 如下兩個都是 store 的經常使用 API
  dispatch() {}

  subscribe() {}
}
複製代碼

Store 容納了 state,而且能隨時訪問 state 的值,那麼能夠寫下以下代碼

class Store {
  constructor(initState) {
  // _ 表明私有,固然不是真的私有,便於教學就這樣寫了
    this._state = initState
  }
  
  getState() {
    return this._state
  }
    
    // 如下兩個都是 store 的經常使用 API
  dispatch() {}

  subscribe() {}
}
複製代碼

接下來咱們考慮 dispatch 邏輯。首先 dispatch 應該接收一個 action 參數,而且發送給 reducer 更新 state。而後若是用戶 subscribe 了 state,咱們還應該調用函數,那麼能夠寫下以下代碼

dispatch(action) {
    this._state = this.reducer(this.state, action)
    this.subscribers.forEach(fn => fn(this.getState()))
}
複製代碼

reducer 邏輯很簡單,在 constructor 時將 reducer 保存起來便可,那麼能夠寫下以下代碼

constructor(initState, reducer) {
    this._state = initState
    this._reducer = reducer
}
複製代碼

如今一個 Redux 的簡易半成品已經完成了,咱們能夠來執行下如下代碼

const initState = {value: 0}
function reducer(state = initState, action) {
    switch (action.type) {
        case 'increase':
            return {...state, value: state.value + 1}
        case 'decrease': {
            return {...state, value: state.value - 1}
        }
    }
    return state
}
const store = new Store(initState, reducer)
store.dispatch({type: 'increase'})
console.log(store.getState()); // -> 1
store.dispatch({type: 'increase'})
console.log(store.getState()); // -> 2
複製代碼

最後一步讓咱們來完成 subscribe 函數, subscribe 函數調用以下

store.subscribe(() =>
  console.log(store.getState())
)
複製代碼

因此 subscribe 函數應該接收一個函數參數,將該函數參數 push 進數組中,而且調用該函數

subscribe(fn) {
    this.subscribers = [...this.subscribers, fn];
    fn(this.value);
}
constructor(initState, reducer) {
    this._state = initState
    this._reducer = reducer
    this.subscribers = []
}
複製代碼

自此,一個簡單的 Redux 的內部邏輯就完成了,你們能夠運行下代碼試試。

Redux 中間件的實現我會在課程中講解,這裏就先放下。經過這段分析,我相信你們應該不會對 Redux 仍是很迷惑了。

Immutable.js

我在該項目中使用了該庫,具體使用你們能夠看項目,這裏講一下這個庫到底解決了什麼問題。

首先 JS 的對象都是引用關係,固然你能夠深拷貝一個對象,可是這個操做對於複雜數據結構來講是至關損耗性能的。

Immutable 就是解決這個問題而產生的。這個庫的數據類型都是不可變的,當你想改變其中的數據時,他會clone 該節點以及它的父節點,因此操做起來是至關高效的。

這個庫帶來的好處是至關大的: - 防止了異步安全問題 - 高性能,而且對於作 React 渲染優化提供了很大幫助 - 強大的語法糖 - 時空穿梭 (就是撤銷恢復)

固然缺點也是有點: - 項目傾入性太大 (不推薦老項目使用) - 有學習成本 - 常常忘了從新賦值。。。

對於 Immutable.js 的使用也會在視頻中講述

性能優化

  • 減小沒必要要的渲染次數
  • 使用良好的數據結構
  • 數據緩存,使用 Reselect

具體該如何實現性能優化,在課程的後期也會講述

聊天相關

在聊天功能中我用了 Socket.io 這個庫。該庫會在支持的瀏覽器上使用 Websocket,不支持的會降級使用別的協議。

Websocket 底下使用了 TCP 協議,在生產環境中,對於 TCP 的長連接理論上只須要保證服務端收到消息而且回覆一個 ACK 就行。

在該項目的聊天數據庫結構設計上,我將每一個聊天存儲爲一個 Document,這樣後續只須要給這個 Document 的 messages 字段 push 消息就行。

const chatSchema = new Schema({
  messageId: String,
  // 聊天雙方
  bothSide: [
    {
      user: {
        type: Schema.Types.ObjectId
      },
      name: {
        type: String
      },
      lastId: {
        type: String
      }
    }
  ],
  messages: [
    {
      // 發送方
      from: {
        type: Schema.Types.ObjectId,
        ref: "user"
      },
      // 接收方
      to: {
        type: Schema.Types.ObjectId,
        ref: "user"
      },
      // 發送的消息
      message: String,
      // 發送日期
      date: { type: Date, default: Date.now }
    }
  ]
});
// 聊天具體後端邏輯
module.exports = function() {
  io.on("connection", function(client) {
    // 將用戶存儲一塊兒
    client.on("user", user => {
      clients[user] = client.id;
      client.user = user;
    });
    // 斷開鏈接清除用戶信息
    client.on("disconnect", () => {
      if (client.user) {
        delete clients[client.user];
      }
    });
    // 發送聊天對象暱稱
    client.on("getUserName", id => {
      User.findOne({ _id: id }, (error, user) => {
        if (user) {
          client.emit("userName", user.user);
        } else {
          client.emit("serverError", { errorMsg: "找不到該用戶" });
        }
      });
    });
    // 接收信息
    client.on("sendMessage", data => {
      const { from, to, message } = data;
      const messageId = [from, to].sort().join("");
      const obj = {
        from,
        to,
        message,
        date: Date()
      };
      // 異步操做,找到聊天雙方
      async.parallel(
        [
          function(callback) {
            User.findOne({ _id: from }, (error, user) => {
              if (error || !user) {
                callback(error, null);
              }
              callback(null, { from: user.user });
            });
          },
          function(callback) {
            User.findOne({ _id: to }, (error, user) => {
              if (error || !user) {
                callback(error, null);
              }
              callback(null, { to: user.user });
            });
          }
        ],
        function(err, results) {
          if (err) {
            client.emit("error", { errorMsg: "找不到聊天對象" });
          } else {
            // 尋找該 messageId 是否存在
            Chat.findOne({
              messageId
            }).exec(function(err, doc) {
              // 不存在就本身建立保存
              if (!doc) {
                var chatModel = new Chat({
                  messageId,
                  bothSide: [
                    {
                      user: from,
                      name: results[0].hasOwnProperty("from")
                        ? results[0].from
                        : results[1].from
                    },
                    {
                      user: to,
                      name: results[0].hasOwnProperty("to")
                        ? results[0].to
                        : results[1].to
                    }
                  ],
                  messages: [obj]
                });
                chatModel.save(function(err, chat) {
                  if (err || !chat) {
                    client.emit("serverError", { errorMsg: "後端出錯" });
                  }
                  if (clients[to]) {
                    // 該 messageId 不存在就得發送發送方暱稱
                    io.to(clients[to]).emit("message", {
                      obj: chat.messages[chat.messages.length - 1],
                      name: results[0].hasOwnProperty("from")
                        ? results[0].from
                        : results[1].from
                    });
                  }
                });
              } else {
                doc.messages.push(obj);

                doc.save(function(err, chat) {
                  if (err || !chat) {
                    client.emit("serverError", { errorMsg: "後端出錯" });
                  }
                  if (clients[to]) {
                    io.to(clients[to]).emit("message", {
                      obj: chat.messages[chat.messages.length - 1]
                    });
                  }
                });
              }
            });
          }
        }
      );
    });
  });
};
複製代碼

課程中的這塊功能將會以重點來說述,而且會單獨開一個小視頻講解應用層及傳輸層必知知識。

課程相關

視頻預計會在 20 小時以上,可是本人畢竟不是專職講師,仍是一線開發者,因此一週只會更新 2 - 3 小時視頻,視頻會在羣內第一時間更新連接。

由於你們太熱情了,幾天不到加了600多人,因此仍是開通了一個訂閱號用於發佈視頻更新。

最後

這是項目地址,以爲不錯的能夠給我點個 Star。

本篇文章也是我 18 年的第一篇博客,祝你們新年快樂,在新的一年學習更多的知識!

相關文章
相關標籤/搜索