送给前端开发者的一份新年礼物
大家好,新年快樂!今天,我開源了一個 React 的項目。這個項目雖小,但是五臟六腑俱全。
先來介紹下這個項目的技術棧:
- React 全家桶:React 16 + Redux + React-router 4.0 + Immutable.js
- ES6 + ES7 語法
- 網(wǎng)絡請求:Axios + Socket.io
- UI 框架:Antd-mobile
- 后端:Express + MongoDB
React 是什么
React 其實只是一個 UI 框架,頻繁進行 DOM 操作的代價是很昂貴的,所以 React 使用了虛擬 DOM 的技術,每當狀態(tài)發(fā)生改變,就會生成新的虛擬 DOM 并與原本的進行改變,讓變化的地方去渲染。并且為了性能的考慮,只對狀態(tài)進行淺比較(這是一個很大的優(yōu)化點)。
React 已經(jīng)成為當今最流行的框架之一,但是他的學習成本并不低并且需要你有一個良好的 JS 基礎。由于React 只是一個 UI 框架,所以你想完成一個項目,你就得使用他的全家桶,更加提高了一個學習成本。所以本課程也是針對初學者,讓初學者能夠快速的上手 React 。
React 組件
如何寫好規(guī)劃好一個組件決定了你的 React 玩的溜不溜。一個組件你需要考慮他提供幾個對外暴露的接口,內(nèi)部狀態(tài)通過局部狀態(tài)改變還是全局狀態(tài)改變好。并且你的組件應該是利于復用和維護的。
組件的生命周期
- render 函數(shù)會在 UI 渲染時調(diào)用,你多次渲染就會多次調(diào)用,所以控制一個組件的重復渲染對于性能優(yōu)化很重要
- componentDidMount 函數(shù)只會在組件渲染以后調(diào)用一次,通常會在這個發(fā)起數(shù)據(jù)請求
- shouldComponentUpdate 是一個很重要的函數(shù),他的返回值決定了是否需要生成一個新的虛擬 DOM 去和之前的比較。通常遇到的性能問題你可以在這里得到很好的解決
- componentWillMount 函數(shù)會在組件即將銷毀時調(diào)用,項目中在清除聊天未讀消息中用到了這個函數(shù)
父子組件參數(shù)傳遞
在項目中我使用的方式是單個模塊頂層父組件通過 connect 與 Redux 通信。子組件通過參數(shù)傳遞的方式獲取需要的參數(shù),對于參數(shù)類型我們應該規(guī)則好,便于后期 debug。
性能上考慮,我們在參數(shù)傳遞的過程中盡量只傳遞必須的參數(shù)。
路由
在 React-router 4.0 版本,官方也選擇了組件的方式去書寫路由。
下面介紹一下項目中使用到的按需加載路由高階組件
import React, { Component } from "react"; // 其實高階組件就是一個組件通過參數(shù)傳遞的方式生成新的組件 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,組件間通信不多,邏輯不復雜,你也就不需要使用這個庫,畢竟這個使用這個庫的開發(fā)成本很大。
Redux 是與 React 解耦的,所以你想和 Redux 通信就需要使用 React-redux,你在 action 中使用異步請求就得使用 Redux-thunk,因為 action 只支持同步操作。
Redux 的組成
Redux 由三部分組成:action,store,reducer。
Action 顧名思義,就是你發(fā)起一個操作,具體使用如下:
export function getOrderSuccess(data) { // 返回的就是一個 action,除了第一個參數(shù)一般這樣寫,其余的參數(shù)名隨意return { type: GET_ORDER_SUCCESS, payload: data }; } 復制代碼Action 發(fā)出去以后,會丟給 Reducer。Reducer 是一個純函數(shù)(不依賴于且不改變它作用域之外的變量狀態(tài)的函數(shù)),他接收一個之前的 state 和 action 參數(shù),然后返回一個新的 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 中進行改變。
說明完了這些基本概念,我覺得是時候?qū)?Redux 進行一點深入的挖掘。
自己實現(xiàn) Redux
之前說過 Store 是個容器,那么可以寫下如下代碼
class Store {constructor() {}// 以下兩個都是 store 的常用 APIdispatch() {}subscribe() {} } 復制代碼Store 容納了 state,并且能隨時訪問 state 的值,那么可以寫下如下代碼
class Store {constructor(initState) {// _ 代表私有,當然不是真的私有,便于教學就這樣寫了this._state = initState}getState() {return this._state}// 以下兩個都是 store 的常用 APIdispatch() {}subscribe() {} } 復制代碼接下來我們考慮 dispatch 邏輯。首先 dispatch 應該接收一個 action 參數(shù),并且發(fā)送給 reducer 更新 state。然后如果用戶 subscribe 了 state,我們還應該調(diào)用函數(shù),那么可以寫下如下代碼
dispatch(action) {this._state = this.reducer(this.state, action)this.subscribers.forEach(fn => fn(this.getState())) } 復制代碼reducer 邏輯很簡單,在 constructor 時將 reducer 保存起來即可,那么可以寫下如下代碼
constructor(initState, reducer) {this._state = initStatethis._reducer = reducer } 復制代碼現(xiàn)在一個 Redux 的簡易半成品已經(jīng)完成了,我們可以來執(zhí)行下以下代碼
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 函數(shù), subscribe 函數(shù)調(diào)用如下
store.subscribe(() =>console.log(store.getState()) ) 復制代碼所以 subscribe 函數(shù)應該接收一個函數(shù)參數(shù),將該函數(shù)參數(shù) push 進數(shù)組中,并且調(diào)用該函數(shù)
subscribe(fn) {this.subscribers = [...this.subscribers, fn];fn(this.value); } constructor(initState, reducer) {this._state = initStatethis._reducer = reducerthis.subscribers = [] } 復制代碼自此,一個簡單的 Redux 的內(nèi)部邏輯就完成了,大家可以運行下代碼試試。
Redux 中間件的實現(xiàn)我會在課程中講解,這里就先放下。通過這段分析,我相信大家應該不會對 Redux 還是很迷惑了。
Immutable.js
我在該項目中使用了該庫,具體使用大家可以看項目,這里講一下這個庫到底解決了什么問題。
首先 JS 的對象都是引用關系,當然你可以深拷貝一個對象,但是這個操作對于復雜數(shù)據(jù)結構來說是相當損耗性能的。
Immutable 就是解決這個問題而產(chǎn)生的。這個庫的數(shù)據(jù)類型都是不可變的,當你想改變其中的數(shù)據(jù)時,他會clone 該節(jié)點以及它的父節(jié)點,所以操作起來是相當高效的。
這個庫帶來的好處是相當大的: - 防止了異步安全問題 - 高性能,并且對于做 React 渲染優(yōu)化提供了很大幫助 - 強大的語法糖 - 時空穿梭 (就是撤銷恢復)
當然缺點也是有點: - 項目傾入性太大 (不推薦老項目使用) - 有學習成本 - 經(jīng)常忘了重新賦值。。。
對于 Immutable.js 的使用也會在視頻中講述
性能優(yōu)化
- 減少不必要的渲染次數(shù)
- 使用良好的數(shù)據(jù)結構
- 數(shù)據(jù)緩存,使用 Reselect
具體該如何實現(xiàn)性能優(yōu)化,在課程的后期也會講述
聊天相關
在聊天功能中我用了 Socket.io 這個庫。該庫會在支持的瀏覽器上使用 Websocket,不支持的會降級使用別的協(xié)議。
Websocket 底下使用了 TCP 協(xié)議,在生產(chǎn)環(huán)境中,對于 TCP 的長鏈接理論上只需要保證服務端收到消息并且回復一個 ACK 就行。
在該項目的聊天數(shù)據(jù)庫結構設計上,我將每個聊天存儲為一個 Document,這樣后續(xù)只需要給這個 Document 的 messages 字段 push 消息就行。
const chatSchema = new Schema({messageId: String,// 聊天雙方bothSide: [{user: {type: Schema.Types.ObjectId},name: {type: String},lastId: {type: String}}],messages: [{// 發(fā)送方from: {type: Schema.Types.ObjectId,ref: "user"},// 接收方to: {type: Schema.Types.ObjectId,ref: "user"},// 發(fā)送的消息message: String,// 發(fā)送日期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];}});// 發(fā)送聊天對象昵稱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) {// 不存在就自己創(chuàng)建保存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 不存在就得發(fā)送發(fā)送方昵稱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 小時以上,但是本人畢竟不是專職講師,還是一線開發(fā)者,所以一周只會更新 2 - 3 小時視頻,視頻會在群內(nèi)第一時間更新鏈接。
因為大家太熱情了,幾天不到加了600多人,所以還是開通了一個訂閱號用于發(fā)布視頻更新。
最后
這是項目地址,覺得不錯的可以給我點個 Star。
本篇文章也是我 18 年的第一篇博客,祝大家新年快樂,在新的一年學習更多的知識!
總結
以上是生活随笔為你收集整理的送给前端开发者的一份新年礼物的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: New to My Oracle Sup
- 下一篇: GIt -- Window下配置 git
