Redux+React Router+Node.js全栈开发


前言


本文是个人对慕课网《Redux+React Router+Node.js全栈开发》的图文总结。

第一部分 项目准备

一、配置开发环境

  1. 安装create-react-app脚手架依赖

  1. 创建项目

看到已经创建成功。

  1. cd到项目目录下,执行npm start

可以看到项目已经在localhost:3000端口跑起来了。

在package.json配置文件里,我们可以看到,执行npm start,实际上是在执行react-scripts里的start。

  1. 自定义配置webpack,需要执行npm run eject

可以看到,webpack的配置文件和脚本文件已经弹出到项目根目录下了:

这样就可以自定义修改相关的配置了。

二、尝试express+mongodb开发web后台接口

  1. 安装express

  1. 根目录下创建server/server.js

  1. CLI下进入server文件夹,执行node server.js

  1. 看到已监听成功并正常返回数据

  1. 再试一下模拟后端接口返回数据

重新启动server.js,可以看到已经成功接收到数据了:

  1. 如果不想每次修改路由都重启服务器的话,可以安装nodemon依赖来实现自动化

通过nodemon server.js启动服务器:

随便改个返回的数据后,发现现在服务器会自动重启:

可以把nodemon server/server.js命令行写在package.json的scripts里,方便启动:

这样就能以npm run server来启动了。

三、尝试通过mongodb实现数据库增删改查

  1. 去官网下载并安装mongodb

  2. 配置mongodb

(1)在C盘目录下创建mongodb文件夹,里面创建data和logs文件夹

(2)cd到mongodb安装路径下的bin目录,执行:

看到waiting for connection on port 27017,代表启动成功:

(3)创建一个本地服务,省去每次启动的麻烦

同样在mongodb的安装目录bin下执行以下命令:

mongod.exe –logpath C:\mongodb\logs\mongodb.log –logappend –dbpath C:\mongodb\data –directoryperdb –serviceName MongoDB –install

在任务管理器中看到已经有MongoDB的服务了。

(4)通过命令行启动/关闭mongodb服务

  1. 安装mongoose依赖

  1. 配置server.js,连接mogodb

命令行里看到已成功连接mongodb。

  1. 通过mongoose对mongodb实现CRUD(增删改查)

(1)增create

在控制台下可以看到成功打印出我们增加的数据了:

(2)删deleteOne/deleteMany/bulkWrite

(3)改updateOne/updateMany/bulkWrite

(4)查find/findOne

我们把userModel文档里所有name为xiaoming的数据返回:

在localhost:8888/data下查看返回的数据:

注:

1)使用findOne则返回查找到的第一个数据

2)不写过滤条件则返回全部

四、使用antd-mobile作为UI组件

  1. 安装antd-mobile组件依赖

  1. 使用babel-plugin-import实现按需加载

五、使用redux管理数据状态

  1. 安装redux

  1. 使用redux-thunk实现异步状态更新

  1. 安装react-redux

(1)Provider

(2)connect

这里的connect可以用装饰器模式来书写:

先安装装饰器兼容依赖:

Babel版本6.x及以下:

Babel版本7.x:

在package.json里开启插件配置:

注意,根据这个插件的官方文档提醒,在babel 7.X以上的版本里已经不需要单独安装该插件了,有其他的配置可以实现(链接)。

六、使用react-router 4实现路由跳转

  1. 安装react-router-dom

  1. 在新版react-router-dom里,路由已经真正意义上成为组件了,所以要学会常用路由组件BrowerRouter/Route/Redirect/Switch/Link的用法。

七、使用axios发送http请求

  1. 安装axios

  1. 由于涉及到跨域请求,需要在package.json里设置代理:

  1. 在app.js里试试

  1. 查看结果

  1. 使用axios拦截器

(1)创建config.js并设置拦截器

(2)在app.js里引入并执行拦截器:

(3)查看效果:

第二部分 页面编写

一、注册

  1. 安装cookie parser

  1. 完成login/register页面基本内容

注:UI部分是基于antd-mobile组件库制作的。

  1. 由于涉及牛人/BOSS两种用户身份,路由跳转涉及比较复杂的逻辑判断,所以单独抽出一个authrouter来首先获取并处理路由跳转:

  1. 配置后端数据库(server.js)

(1)把原server.js里关于mongodb数据库操作相关的全部剥离到server/model.js下

(2)在server目录下创建user.js,用户存放用户判断的路由

(3)在server.js里引入userRouter,同时通过app.use的方式把”/user”路由引导到userRouter下

(4)接着,就可以在AuthRoute组件里监听到/user/info接口了

  1. 在AuthRoute组件里进行路由跳转的逻辑判断:

(1)获取url

由于AuthRoute已经不是路由的component,所以无法直接获取this.props.history,需要通过react-router-dom里提供您的withRouter实现:

注意:这里使用装饰器模式,需要引入对应的Babel支持才行(见前文关于装饰器模式的描述)。

如果不用装饰器模式,则需要用withRouter将组建包裹起来输出:

(2)根据当前url及用户登录信息判断是否需要跳转至登录页

  1. 创建并配置redux/user.redux.js

这里的做法是,把redux中的reducer和action都写在一个js文件内,然后export出去。

(1)reducers

(2)actions

  1. 在reducer.js里引入user.redux里的user(reducer):

  1. 在register里引入connect和register(action),并以装饰器模式绑定到模块上:

同时在点击提交按钮时将注册信息提交到redux里:

可以看到已成功通过props获取redux的信息:

  1. 完成mongodb数据模型

(1)在model.js里创建schema并暴露接口:

(2)在server/user.js里利用schema来查询数据库,并通过/list接口返回给前端:

(3)查看效果,可以看到在/user/list接口下是可以查到数据的:

  1. 后端接收注册页面发送的post数据并修改数据库:

(1)由于需要解析前端http发送的post数据,所以需要安装body-parser依赖:

(2)在server/server.js里引入cookie-parser/body-parser中间件,并通过app.use来启动

这样配置后,可以路由里获取request.body里的data了。

(3)配置/register路由

首先,通过findOne查询数据库中是否已存在用户,若不存在,则通过create来创建并写入数据库:

  1. 实现注册后跳转的功能

(1)user.redux.js里的reducers增加一个redirectTo字段:

(2)创建redux/util.js,作用是在用户注册后,根据注册类型及是否存在头像,处理并返回重定向的url:

(3)在user.redux.js里,在action.type为success时,刷新redirectTo的数据:

(4)在register.js里引入Redirect组件,并通过redux里的redirectTo数据来判断跳转:

  1. 对注册时的密码使用md5进行加密

(1)安装utility第三方依赖库

(2)在server/user.js里使用md5对密码进行加密

(3)再注册试试

可以看到密码已经使用md5加密了

(4)加盐

md5加密虽然是不可逆很难解密的,但是一些简单的密码依然可以利用”彩虹表”获知密码,所以实际工作里会有一种”加盐”的操作来增加密码复杂性。

创建utils/md5.js文件,编写md5加盐函数:

二、登录

登录页面大部分功能与注册页相同,不赘述,记录几个重点:

  1. 数据库查询后返回的过滤参数

前端通过/user/login接口将登录页的用户名和密码发送给后端,后端通过findOne查询数据库后,将查询到的内容作为data返回给前端。而密码是不应该返回的,所以在查询时,可以通过过滤参数实现:

  1. 用户在登录后保存cookie,记录登录状态

当用户通过AuthRoute组件时在访问/info接口时,后端尝试从request.header里获取cookies信息,若无则直接返回code为1,然后跳转至登录页面;若有,则从服务器里查询id,并返回用户信息给前端:

注意:同样的,前面用户在注册成功后,也要保存cookie:

三、完善BOSS/牛人信息页(bossinfo/geniusinfo)

这部分比较简单,完成效果:

值得一提的是:

  1. 通过对比当前的pathname与this.props.redirectTo,判断是否进行路由跳转(作用是在点击保存后进行跳转):

  1. 对于/update这个后端接口,我们是先从cookie里获取userid,(这个userid实际上就是MongoDB数据库在create时返回的_id):

mongodb官方API文档:

  1. 对于avatarSeletor这个组件,可以用prop-types依赖包来对this.props里的参数进行强检验:

(1)安装prop-types

(2)在AvatarSelector组件里使用prop-types:

做个试验:在bossinfo.js里把props过去的内容改为字符串,结果发现报错了:

四、完成牛人列表/BOSS列表(dashboard组件)

这里值得注意的是:

  1. 路由的实现方式

(1)在入口文件index.js里的路由里,将所有其余路由引向DashBoard这个组件

(2)然后在DashBoard这个路由组件里,再通过navList使用map进行循环渲染的方式,创建子路由:

(3)同时,根据用户当前的类型(即redux里的type为boss或genius),来渲染不同的底部NavLinkBar:

  1. 个人中心

(1)使用browser-cookies依赖包来清除浏览器中指定的cookie

五、实现聊天功能

为了实现实时聊天的功能,需要用到基于websocket协议的socket.io库。

我们平时用的Ajax,是基于HTTP协议,只能从客户端像服务端单向发起请求,而websocket协议能实现服务端也能主动向客户端发送数据,实现双向通信。

  1. 安装socket.io依赖库(包括socket.io/socket.io-client)

  1. 配置server/server.js

因为express要和socket一起使用,所以我们需要从node自带的http依赖包里引入Server,从而通过Server来监听8888端口(因为socket是作用在http下的Server里的)。

运行后,在控制台可以看到socket.io已启动成功。

  1. 在客户端里(Chat组件)引入socket.io-client,并尝试给服务端发送信息

通过socket.emit,来给服务端发送信息:

  1. 在server.js里,通过socket.on来获取客户端发送过来的信息:

可以看到控制台已经成功打印出信息了:

这里有两个重大的坑!!!!

第一,

socket.io的接口(on/emit)一旦建立后不会覆盖,后面每多执行一次on/emit,就会多创建一个监听,这样会导致一个后果就是监听越来越多,就会出现这样的情况:

这里分析一下具体原因:

chat.js里,在componentDidMount里判断当前是否有聊天记录(即this.props.chat.chatmsg.length),若无则执行this.props.recvMsg,该函数是从chat.redux.js里传过来的action,执行它等于执行socket.on(‘recvmsg’),即通过socket.io创建一个监听端口。

这就导致了,当没聊天记录时,刷新/chat/user路由,每次刷新就会多创建一个on监听,于是出现上面的问题。

解决办法是,把recvmsg和getMsgList的请求放在DashBoard里去执行,这样就可以避免因多次加载Chat导致问题:

第二,

如果要通过socket.io发送广播,需要用io.emit(或socket.broadcast.emit)才能实现全局广播,否则跨窗口是收不到的。

值得一提的几个学习点:

(1)mongodb数据库操作:

(2)还有redux中间件的好处:

通过中间件,能更好的封装异步action,对外只暴露一个接口以供调用,这样就能更好地实现模块化编程以及组件的封装。

(3)由于对于从redux里prop过来的部分state数据是异步获取,然后再次触发render的,所以在render里需要加多个判断,如果数据不存在则不参与render,否则会报错:

第三部分 React进阶

一、react核心概念和基本原理

  1. 虚拟DOM

  2. JSX语法(实际上在JS里书写的html会被转换为React.createElement)

  3. 生命周期

  4. react的主要优化点在shouldComponentUpdate这个生命周期里。

二、redux原理:自己实现一个迷你版的redux

三、React性能优化

  1. 传递参数时的性能消耗

(1)不推荐,且存在内存消耗的写法:

(2)推荐的写法

或者把要传递的参数写在construtor里也行,目的都是为了避免每次props都在内存中新建对象。

另外,每次只把子组件需要用到的数据props过去,避免传递多余的数据。

  1. 事件绑定this的性能消耗

  1. shouldComponentUpdate

第一种方法,我们通过手动在组件里对shouldComponentUpdate进行是否更新的配置:

用一个实际例子来说明问题:

(1)App有两个state,num会影响App本身的页面,title会影响Demo这个子组件:

(2)我们在url里添加?react_perf后缀,这样可以在chrome自带的performance里调起对react性能的分析:

(3)先点击2下addNum,再点击2下changeTitle:

(4)我们可以看到,不管是前面2次addNum还是后面2次的changeTitle,都触发了Demo组件的update:

而实际上前面2次只改变了state里的num,对prop给Demo的title是没影响的。这就是我们需要进行性能优化的地方。

(5)我们在Demo组件里的shouldComponentUpdate生命周期里加入判断:

(6)再查看一下performance性能表现:

这次可以看到,前面2次点击addNum已经没有再触发Demo的update了(渲染时间为0)。

而后面2次是正常触发update的。

第二种方法,我们可以用React 15.x版本后带有的PureComponent来实现:

官方文档关于PureComponent的说明:

我们在Demo里这么使用:

使用PureComponent后,前面2次addNum在触发刷新时直接就看不到Demo组件的渲染了,强悍!

四、关于PureComponent原理及immutable.js/seamless-immutable的作用

PureComponent进行的是浅比较,即遇到对象类型的数据会比较其内存位置,而不会深入对比具体的值。

实际上如果要检测数据的变化,我们需要对数据进行深比较:

然而当数据结构复杂以后,深比较对性能的损耗是比较大的,所以React里的PureComponent为浅比较,但这样实际上也存在不准确的情况,例如{a:”a”} !== {a:”a”}所以会导致重新render,但实际上却是不应该的。

为了解决这个问题,可以使用immutable.js/seamless-immutable库,该库的作用在于可以创建不可变的数据结构,在此前提下任何数据都能直接使用===来进行判断。

五、Redux性能优化

使用reselect库,在redux里对state进行计算时进行优化,采用备忘录的模式对数据进行缓存处理。

六、服务端渲染(Server Side Render)

(一)概念

  1. 传统的服务端渲染:JSP、smarty、jinja2

优点:首屏非常快

缺点:每次获取数据都要生成一个新的html

  1. 浏览器端渲染:通过JS基于Ajax异步获取数据并操作DOM

优点:更新时刷新快

缺点:首屏慢、不利于SEO

  1. 前后端同构,首屏服务端渲染

html由服务端(Node)渲染,浏览器端只做注水(即把事件注入页面)。

(二)服务端渲染实操

  1. 让node支持es6语法

我们现在在server.js里书写的都是es5的语法,因为Node环境不支持es6,这也是为什么我们要在server.js里使用require而不是import的方式引入依赖:

  1. 安装babel-cli:

  1. 更改package.json里的scripts命令行:

  1. 使用ReactDomServer.renderToString实现服务端渲染:

  1. 类似的,我们可以自己写出全部的服务端渲染页面,并以jsx的形式在server.js里引入并通过renderToString渲染到首屏。

这里有几点需要注意:

(1)在node里的路由要使用staticRouter,而不是BrowserRouter;

(2)直接在node里渲染首屏会报错,因为node并不能解析css、图片等文件,所以需要借助一些模块,比如css-modules-require-hook、asset-require-hook(需要放到React组件的前面才能生效,且图片要在React组件里以require的方式引入,如果是import也会报错)。

(3)上面把App这个组件当做res直接从Node返回给前端是不科学的,因为还缺少骨架。所谓的骨架,即public目录下的index,这里的内容就是React在render时挂钩的DOM节点:

所以我们也要把这个骨架给加上(以字符串模板的形式),然后再返回前端:

(4)但是现在,样式还是有问题的,原因是我们的css和js文件是放在static/manifest.json里的,所以还需要import 进来:

(5)现在就可以在首屏里填写keywords/description/author等来优化SEO了:

七、其他

  1. 使用async/await代替Promise,书写更优雅的异步代码

  2. 使用ant-design附带的motion design创建更流畅的动画体验

例如进出场动画:

(1)安装进出场动画依赖:

(2)在chat.js里引入并使用:

这样配置后,在对话页面就发现聊天信息的出现有动画效果了。

此外,也可以使用ReactCSSTransitionGroup来创建动画。

第四部分 项目打包编译

  1. 执行npm run build:

  1. 在项目根目录下生成了build文件夹:

这时,如果直接把index.html放在浏览器里打开是无法访问的。

  1. 在server.js里配置express

(1)引入path处理相对路径

(2)使用express.static中间件,将路径引到本地绝对路径里:

这里的express.static作用是将指定路径下的资源对外开放访问。

(3)使用app.use拦截路由并指定跳转文件:

(4)这样,就能直接在localhost:8888端口把html跑起来了,不再需要npm start来另外搭建一个dev-server服务器:

可以看到8888端口下已经有本地的静态文件目录了:

  1. 这里如果要将项目上线,需要几个步骤:

(1)购买域名

(2)将DNS解析到服务器的IP

(3)安装nginx设置反向代理

(4)使用pm2管理node进程

0%