大家好,新年快乐!今天,我开源了一个 React 的项目。这个项目虽小,但是五脏六腑俱全。

先来介绍下这个项目的技术栈:

  • 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 并与原本的进行改变,让变化的地方去渲染。并且为了性能的考虑,只对状态进行浅比较(这是一个很大的优化点)。

React 已经成为当今最流行的框架之一,但是他的学习成本并不低并且需要你有一个良好的 JS 基础。由于React 只是一个 UI 框架,所以你想完成一个项目,你就得使用他的全家桶,更加提高了一个学习成本。所以本课程也是针对初学者,让初学者能够快速的上手 React 。

React 组件

如何写好规划好一个组件决定了你的 React 玩的溜不溜。一个组件你需要考虑他提供几个对外暴露的接口,内部状态通过局部状态改变还是全局状态改变好。并且你的组件应该是利于复用和维护的。

组件的生命周期
  • render 函数会在 UI 渲染时调用,你多次渲染就会多次调用,所以控制一个组件的重复渲染对于性能优化很重要
  • componentDidMount 函数只会在组件渲染以后调用一次,通常会在这个发起数据请求
  • shouldComponentUpdate 是一个很重要的函数,他的返回值决定了是否需要生成一个新的虚拟 DOM 去和之前的比较。通常遇到的性能问题你可以在这里得到很好的解决
  • componentWillMount 函数会在组件即将销毁时调用,项目中在清除聊天未读消息中用到了这个函数
父子组件参数传递

在项目中我使用的方式是单个模块顶层父组件通过 connect 与 Redux 通信。子组件通过参数传递的方式获取需要的参数,对于参数类型我们应该规则好,便于后期 debug。

性能上考虑,我们在参数传递的过程中尽量只传递必须的参数。

路由

在 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 的常用 APIdispatch() {}subscribe() {}
}
复制代码

Store 容纳了 state,并且能随时访问 state 的值,那么可以写下如下代码

class Store {constructor(initState) {// _ 代表私有,当然不是真的私有,便于教学就这样写了this._state = initState}getState() {return this._state}// 以下两个都是 store 的常用 APIdispatch() {}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 = initStatethis._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 = initStatethis._reducer = reducerthis.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 年的第一篇博客,祝大家新年快乐,在新的一年学习更多的知识!

送给前端开发者的一份新年礼物相关推荐

  1. 送给SQL开发者的一份新年礼物!麦聪软件发布一款纯Web化SQL开发工具,免安装还免费!

    2023年新年伊始,麦聪软件再次迎来一个好消息:一款100%自主研发的纯Web化SQL开发工具--SQL Studio 1.0正式发布.这款产品让SQL开发者在Navicat.DBeaver之外,又多 ...

  2. 1024 献礼,10 个前端开发者必收的高赞资源

    不知不觉<IT 程序狮>的微博已经运营 3 年多了,我在微博上陆续的发布了 5200+ 技术分享.开源项目.编程灵感以及一些工具推荐的博文,不少内容得到了大V的推荐,也有幸收获了一批热情的 ...

  3. android开发入门与实践_Electron从入门到实践,前端开发者开发桌面App的最佳选择...

    在开始之前,我想您一定会有这样的困惑:标题里的Electron 是什么?Electron能做什么?许多伟大的公司使用Electron框架的原因又是什么? 带着这些问题和疑惑,通过本文的介绍,可助您全面 ...

  4. 微信皮肤css,微信小程序实现皮肤功能(夜间模式)_婳祎_前端开发者

    老规矩,先上效果图 个人对夜间模式这个功能情有独钟 晚上黑灯瞎火的看手机,屏幕亮度就算调到最低依然很是刺眼呀 所以我一直用某浏览器,因为有夜间模式 言归正传,依然是分析功能点 1.点击按钮,切换一套 ...

  5. 自学前端开发:想要学习成为一名优秀的前端开发者,代码之外需要关注的问题

    不要只知道蒙着头写代码,想要学习成为一名优秀的前端开发者,你还有许多代码之外值得关注的问题. 学好英语 一定要学好英语,虽然英文不好也可以学会前端.但你一定会遇到比英语好的人更多的困难.因为你只能看中 ...

  6. 一个好的web前端开发者,是怎么学习的?

    T 行业的变化快是众人皆知的,需要持续去学习新的知识内容.但是,往往我们工作之后,经常发现学习的东西很少了,学习效率非常低,感觉自己到了一个瓶颈期,久而久之,就演变成『一年工作经验,重复去用十年』的怪 ...

  7. 送给前端的你,推荐几篇前端汇总文章。(来自知乎专栏)

    送给前端的你,推荐几篇前端汇总文章.(来自知乎专栏) 来源:https://zhuanlan.zhihu.com/p/22229868 作者:路人甲 链接:https://zhuanlan.zhihu ...

  8. 前端开发者应该知道的 Centos/Docker/Nginx/Node/Jenkins 操作

    点击上方 程序员成长指北,关注公众号 回复1,加入高级 Node 进阶交流群 来源:ask_the_sky https://juejin.cn/post/6951684431597797389 服务器 ...

  9. 一个好的web前端开发者,是怎么学习的?前端开发培训机构哪个比较好

    IT 行业的变化快是众人皆知的,需要持续去学习新的知识内容.但是,往往我们工作之后,经常发现学习的东西很少了,学习效率非常低,感觉自己到了一个瓶颈期,久而久之,就演变成『一年工作经验,重复去用十年』的 ...

最新文章

  1. 数据列表DataList模板之实例
  2. 《中国人工智能学会通讯》——8.2 自然界中的鸽群导航行为
  3. Google Cloud 下一站之战略转变
  4. RSS接触 - RSS中的XML文档
  5. SAP CRM settype的创建,背后发生了什么
  6. CSS3 Media Query:移动 Web 的完美开端
  7. Apache Dubbo集群容错
  8. HDU 3400 Line belt (三分)
  9. python 删除变量_Cathy的python学习笔记(二)——变量及字符串
  10. 在使用Assimp库时编译器报错:C2589 “(”:“::”右边的非法标记 AssimpLoadStl
  11. 习题 6.13 编一程序,将两个字符串连接起来,不要用strcat函数。
  12. SICP练习1.17
  13. 网页中视频在线播放脚本
  14. 需要分配较大空间,插入和删除不需要移动元素的线性表,其存储结构是 。 A 单链表 B 静态链表 C 线性链表 D 顺序存储结构
  15. 制作 macOS High Sierra U盘
  16. Android studio实现仿微信界面
  17. HTML CSS个人网页设计与实现——人物介绍丁真(学生个人网站作业设计)
  18. 《物联网实战指南》读书笔记
  19. 活动回顾| Apache Doris 的过去、现在与未来
  20. 微信小程序的轮播图宽高

热门文章

  1. USACO-Section1.4 Mixing Milk (贪心算法)
  2. 最大字段和---C语言实现
  3. Python subprocess.check_output 执行shell命令 返回结果(单次执行shell命令)
  4. 正则表达式:贪婪匹配与非贪婪匹配
  5. 应用netstat查看目标机TCP连接的状况
  6. Python中NotImplementedError的使用方法(抽象类集成子类实现)
  7. Qt:Qt实现飞秋拦截助手—ARP攻击
  8. Python-----规范化开发
  9. PL/SQL 训练12--动态sql和绑定变量
  10. Spring 配置多个数据源,并实现动态切换