你们好,新年快乐!今天,我开源了一个 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 年的第一篇博客,祝你们新年快乐,在新的一年学习更多的知识!