你們好,新年快樂!今天,我開源了一個 React 的項目。這個項目雖小,可是五臟六腑俱全。react
先來介紹下這個項目的技術棧:ios
React 其實只是一個 UI 框架,頻繁進行 DOM 操做的代價是很昂貴的,因此 React 使用了虛擬 DOM 的技術,每當狀態發生改變,就會生成新的虛擬 DOM 並與本來的進行改變,讓變化的地方去渲染。而且爲了性能的考慮,只對狀態進行淺比較(這是一個很大的優化點)。git
React 已經成爲當今最流行的框架之一,可是他的學習成本並不低而且須要你有一個良好的 JS 基礎。因爲React 只是一個 UI 框架,因此你想完成一個項目,你就得使用他的全家桶,更加提升了一個學習成本。因此本課程也是針對初學者,讓初學者可以快速的上手 React 。github
如何寫好規劃好一個組件決定了你的 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 是與 React 解耦的,因此你想和 Redux 通訊就須要使用 React-redux,你在 action 中使用異步請求就得使用 Redux-thunk,由於 action 只支持同步操做。
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 進行一點深刻的挖掘。
以前說過 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 仍是很迷惑了。
我在該項目中使用了該庫,具體使用你們能夠看項目,這裏講一下這個庫到底解決了什麼問題。
首先 JS 的對象都是引用關係,固然你能夠深拷貝一個對象,可是這個操做對於複雜數據結構來講是至關損耗性能的。
Immutable 就是解決這個問題而產生的。這個庫的數據類型都是不可變的,當你想改變其中的數據時,他會clone 該節點以及它的父節點,因此操做起來是至關高效的。
這個庫帶來的好處是至關大的: - 防止了異步安全問題 - 高性能,而且對於作 React 渲染優化提供了很大幫助 - 強大的語法糖 - 時空穿梭 (就是撤銷恢復)
固然缺點也是有點: - 項目傾入性太大 (不推薦老項目使用) - 有學習成本 - 常常忘了從新賦值。。。
對於 Immutable.js 的使用也會在視頻中講述
具體該如何實現性能優化,在課程的後期也會講述
在聊天功能中我用了 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 年的第一篇博客,祝你們新年快樂,在新的一年學習更多的知識!