用Node.js做一个聊天室

一、简述本系统

本系统主要参考https://juejin.im/post/5a73ddcff265da4e81237429和https://www.cnblogs.com/hawk-zz/p/6934880.html

有用户和管理员两种角色,用户可以随意注册但管理员不能注册,如果数据库中没有管理员信息,后台会自动生成一个管理员。用户成功登陆会进入聊天室,管理员成功登陆会进入管理员界面。

系统用例图如下:

用Node.js做一个聊天室_第1张图片

用户系统功能结构图(左),管理员系统功能结构图(右)

用Node.js做一个聊天室_第2张图片用Node.js做一个聊天室_第3张图片

系统总结构

node-chat3/
|
+- models/
|  |
|  +- Admin.js
|  +- User.js
+- node_modules/  <--npm安装的所有依赖包
+- public/
|  |
|  +- css/
|  |  |
|  |  +- css.css  <--负责用户聊天室的css
|  |  +- bootstrap-theme.min.css
|  |  +- bootstrap.min.css
|  +- fonts/
|  +- img/
|  +- js/
|  |  |
|  |  +- js.js  <--负责用户聊天室的js
|  |  +- admin_js.js  <--负责管理员界面的js
|  |  +- bootstrap.min.js
|  |  +- jquery-1.11.2.min.js
+- routers/
|  |
|  +- register.js  <--管理注册的路由
|  +- login.js  <--管理登录的路由
|  +- index.js  <--一个什么都有的路由(我不知道要叫它什么)
|  +- exit.js  <--管理退出的路由
+- schemas/
|  |
|  +- admin.js  <--定义管理员数据库的结构
|  +- user.js  <--定义用户数据库的结构
+- views/
|  |
|  +- register.html  <--注册界面
|  +- login.html  <--登录界面
|  +- home.html  <--聊天室界面
|  +- admin.html  <--管理员界面
+- app.js  <--入口
+- package-lock.json
+- package.json

二、如何预览效果

1、源代码在此:https://github.com/Clara-hy/node-chat,下载到本地,如果下载后的压缩包以.zip.egt结尾,那就把egt删掉,将后缀变成zip结尾就可以进行解压了。下面我将代码下载到E:盘进行演示,前提是已经下载好Node.js和MongoDB

2、本地运行的方法:在cmd里依次敲入"npm install"和"node app.js",然后在浏览器中打开:http://localhost:8880即可。如果中途遇到有报错,对着报错信息解决

3、详细的操作步骤:

  1. 用Node.js做一个聊天室_第4张图片将cmd调至node-chat3当前目录
  2. 输入npm install:
    用Node.js做一个聊天室_第5张图片
  3. 输入node app.js:
    用Node.js做一个聊天室_第6张图片
    发现报错了,说没有socket.io模块,那我安装这个模块就好了。
  4. 输入npm install socket.io
    用Node.js做一个聊天室_第7张图片
  5. 再次输入node app.js:
    用Node.js做一个聊天室_第8张图片
    因为我的数据库之前存在管理员的信息,所以直接显示出来。如果是第一次安装,数据库没有管理员信息的话,后台会自动生成一个管理员数据:用Node.js做一个聊天室_第9张图片
  6. 去浏览器输入localhost:8880
  7. 登录界面。用户登录会进入聊天室,管理员登录会进入管理界面。用Node.js做一个聊天室_第10张图片
  8. 聊天室界面

    用Node.js做一个聊天室_第11张图片
    支持发表情和图片,可以修改昵称、头像和密码。可以进行多用户聊天,我没截出来了。注意:最好用不同的浏览器代表不同的用户,因为如果是同一个浏览器扮演多个用户,虽然效果也能实现,但是在数据库里会看到session一直被替代为最近的那个用户或管理员,只有多个浏览器是才会有多条session。如下图,我用chrome登录“ddd”的账号,qq浏览器登录“kaka”的账号:

    用Node.js做一个聊天室_第12张图片

  9. 管理员界面

    用Node.js做一个聊天室_第13张图片

    允许管理员做的:①添加管理员 ②重置用户状态(用户上线为true,用户下线为false。因为一些不可控的原因会导致用户不正常下线,即用户下线了但状态仍未true,这会使得用户下一次无法登陆到聊天室。这是就要管理员将用户的状态重置一下了)③删除用户 ④修改管理员自己的密码

    在管理员界面可以看到用户的密码是加密了的,这里用到的是SHA256加密,后面会讲到。

    管理员界面未实现的有:①搜索 ②分页显示

4、如果遇到数据库报错,也许是因为数据库没打开,那应该要先启动MongoDB了。打开它的路径是:D:\Mongo\bin\mongod.exe

用Node.js做一个聊天室_第14张图片
如果出现框内的信息则表明成功打开,否则失败。同时我下载了MongoDB Compass Community,方便查看数据库的变化(如果想只看效果,可以先不打开)。总的来说就是遇到报错去百度吧

5、如果遇到npm下载不了或者比较慢的,可以用cnpm。我的解决方法是:

# 第一步:查看自己的安装源(显示为https://registry.npmjs.org/
npm config get registry
 
# 第二步:更换npm源为国内淘宝镜像
npm config set registry http://registry.npm.taobao.org/

# 第三步:下载需要的东西
npm install [email protected] --save
 
# 第四步:还原npm源
npm config set registry https://registry.npmjs.org/

三、实现

讲解的思路分成:①贴参考链接 ②路由 ③一些功能的实现

(1)参考链接

关于聊天室,有三个完整的可以实现帖子,从易到难:(我都自己实现过,现在这个系统大部分是源于第三个,我只是在它上面加功能而已)

https://www.jianshu.com/p/b608a765519a--->https://juejin.im/post/5a73ddcff265da4e81237429--->https://www.cnblogs.com/hawk-zz/p/6934880.html

要善于用F12来看报错信息

(2)路由

这个路由不是我们上网用的路由,而是一个管理请求入口和页面映射关系的东西。

路由在整个实现中起的作用最重要,我自己也是花了很多的时间去学习怎么配路由。一旦懂路由怎么运行之后,读懂代码或者是往系统里加功能都是很快、很容易上手的。

本系统的路由是用express框架搭建的,首先要去了解express是什么,有以下几个链接:不懂就实操+多看或者找其他的帖子看叭

https://www.w3cschool.cn/nodejs/nodejs-express-framework.html

http://www.expressjs.com.cn/starter/installing.html

对我代码里的路由进行简单的讲解

1、静态文件托管

目的:为了能够调用public文件夹和node_modules文件夹里的资源。不托管的话会显示资源调用失败。

// app.js
//静态文件托管
app.use('/public', express.static(__dirname + '/public'));
app.use('/node_modules', express.static(__dirname + '/node_modules'));

2、app.use

app.use的第一个参数是绑定的路由,第二个参数是访问资源的实际位置,因为这是在app.js里写的,所以这个实际位置要以app.js所处的位置为标准。

//app.js
app.use('/', require('./routers/index'));//任何指向/的http请求都会执行它
app.use('/login', require('./routers/login'));// 挂载至 /login 的中间件,任何指向 /login 的http请求都会执行它
app.use('/register', require('./routers/register'));
app.use('/exit', require('./routers/exit'));

拿“app.use('/', require('./routers/index'));”举例,

用Node.js做一个聊天室_第15张图片
首先先看一下app.js和index.js的位置,index.js是在routers文件夹里,app.js在routers文件夹外面,所以app.js要访问index.js的话路径是“./routers/index”。"./"为当前目录,"/"为根目录,"/routers"根目录下的routers目录。“./routers/index”的解释就是:app.js当前目录下的routers目录里的index.js文件,app.js的当前目录是node-chat3这个总文件。

“app.use('/', require('./routers/index'));”的效果是:将“/”映射到require('./routers/index')这个路由。说的直白点就是,当我输入网址:localhost:8880/是等价于localhost:8880/index。所以,下面这三个的也是这样。

app.use('/login', require('./routers/login'));// localhost:8880/login <=> login.js
app.use('/register', require('./routers/register'));// localhost:8880/register <=> register.js
app.use('/exit', require('./routers/exit'));// localhost:8880/exit <=> exit.js

话说回来,index.js是什么呢?截取它里面的一部分代码:

// index.js
router.get('/', function (req, res) {
    res.redirect('/login');
});

router.get会处理来自'/'的路由,处理的结果是重定向到'/login'路由,意味着localhost:8880/会定向到localhost:8880/login,而/login对应的是login.js,截取它里面的一部分代码:

// login.js
router.get('/', function (req, res) {
    res.render('login.html');
});

router.get是处理来自'/'的路由,会返回login.html文件,这是一个登录页面。所以登录界面就是这么产生的啦

3、实现从登录界面到聊天室界面

用Node.js做一个聊天室_第16张图片当我按下登录的按钮之后

// login.html
$('#login').on({
    $.post('/login/signIn', {username: username, password: password, kind: kind, captcha:captcha}, function (res) {
        if (res.success == 1) {
        location.href = 'home';
        }else if(res.success == 2){
            location.href = 'admin';
        } 
        else {
            alert(res.err);
        }
    }, 'json');
})

// login.js
router.post('/signIn', function (req, res, next) {
    // 如果满足用户的登录条件,则将res.success赋值为1(详细请看源码)
})

login.html里的$.post的第一个参数'/login/signIn'实际对应的是localhost:8880/login/signIn,即 将第二个参数里的数据对象post到localhost:8880/login/signIn里,所以在 login.js里就要写一个处理来自/signIn的请求。(router.post的第一个参数是'/signIn',不是'/login.signIn',因为 login.js本身就代表'/login'这个路由)

当login.js处理完之后,res.success = 1,返回到 login.html,判断res.success是否等于1,判断为true,所以路由会定位到'/home',即localhost:8880/home。但是看app.js那里,我并没有绑定/home的路由,/home的实现是在index.js那里:

// index.js
router.get('/home', function (req, res) {
    //...其他的代码
    res.render('home', {
        username: userInfo.username,
        image: userInfo.image
    });
});

 通过res.render,渲染出home.html,并且将用户名和头像传给了home.html,使得页面能调用两个变量。(用swig模板引擎调用变量,但是swig已经淘汰了,现在一般会用ejs)

管理员的话就是从登录界面到管理员界面,思路大致相同。

(3)一些功能的实现

具体看https://www.jianshu.com/p/b608a765519a和https://juejin.im/post/5a73ddcff265da4e81237429吧,emit、on什么的都比较详细。我的理解是,on是绑定事件,emit是触发事件。比如在服务器端(app.js)绑定一个login(登录)事件,当用户成功登陆时客户端(js.js)会触发login事件。

例子1:实现在线用户列表

// js.js
socket.emit('login', {username: username});

// app.js
socket.on('login', function (data) {//登录
        var username = data.username;
        socket.username = username;
        User.find().then(function (data) {
            for (var i = 0; i < data.length; i++) {
                if (!data[i].state) {
                    data.splice(i, 1);
                    i = i-1; 
                }
            }
            socket.emit('loginSuccess', data);// 服务器端触发loginSuccess事件
            socket.broadcast.emit('user_list', data);
            socket.broadcast.emit('userIn', username);
            socket.emit('user_list', data);
            socket.emit('userIn', username);
        });

    });

// js.js
socket.on('loginSuccess', function (data) {// 绑定在客户端的loginSuccess事件
    userUpdate(data);
});

function userUpdate(data) {
    var len = data.length;
    var str = '';
    for (var i = 0; i < len; i++) {
        str += '
  • '; str += ''; str += '' + data[i].username + '' } $('#peopleList').html(str); $('#list-count span').html(len); }
  • 每一个用户登录成功后,服务端会先建立一个包含所有在线人员的数组,然后触发绑定在客户端的“loginSuccess”事件和“user_list”事件,这两个事件都将调用userUpdate函数,该函数是用来渲染列表的。

    例子2:聊天室

    包含以下几个内容:用户上线/下线提醒、发送/接收文字信息、表情功能和图片功能。

    用户上线/下线提醒:用户登录成功后,服务端还会触发“userIn”事件,该事件也是绑定在客户端的。用户下线则触发“userOut”事件。

    // app.js
    socket.on('login', function (data) {//登录
            var username = data.username;
            socket.username = username;
            User.find().then(function (data) {
                for (var i = 0; i < data.length; i++) {
                    if (!data[i].state) {
                        data.splice(i, 1);
                        i = i-1; 
                    }
                }
                socket.emit('loginSuccess', data);// 服务器端触发loginSuccess事件
                socket.broadcast.emit('user_list', data);
                socket.broadcast.emit('userIn', username);// 服务器端触发userIn事件
                socket.emit('user_list', data);
                socket.emit('userIn', username);
            });
    
        });
    
    // js.js
    socket.on('userIn', function (data) {  //有人加入
        var html = '
  • @ ' + data + ' @上线
  • '; $('#MsgList').append(html); });

    发送/接收文字信息:

    用Node.js做一个聊天室_第17张图片

    发送信息是在按下“sendBtn”之后发生的。先检查输入框是否为空,不可以发送空信息。然后存入发送方的昵称和头像,调用meSendMsg(msg,0)和触发绑定在服务器端的“postNewMsg”事件,最后将输入框置空。meSendMsg的第二个参数是将文字和图片区分开,0为文字发送,1为图片发送。meSendMsg是实现聊天框中右侧的显示。postNewMsg会触发newMsg事件,该事件调用getMsg函数,实现聊天框中左侧的显示。发送/接收信息的思路:

    用Node.js做一个聊天室_第18张图片

    表情功能:思路是先将表情显示到页面,然后点击某一表情能解析对应的格式,最后按下发送按钮之后客户端能解析表情并将表情显示出来。具体实现方法:先将表情下载到本地,用“emoji(X)”的格式进行编排。调用init函数在页面渲染表情,点击表情时将表情解析为“[emojiX]”的格式进行发送。表情详解

    // js.js
    
    // 渲染表情
    init();
    function init() {
    	for(var i = 0; i < 141; i++) {
    	$('.emoji').append(`
  • `); } } // 用户点击表情 $('.emoji li img').click((ev)=>{ ev = ev||window.event; var src = ev.target.src; var emoji = src.replace(/\D*/g, '').substr(6,3);/*将src非数字的字符转为空字符。substr的第二个参数3是指长度为3*/ var old = $('#msgInput').val(); $('#msgInput').val(old+'[emoji'+emoji+']'); $('.selectBox').css('display', "none"); }); // 我方发送信息时调用该函数。n=0为发送文字or表情,n=1为发送图片 function meSendMsg(msg, n) { var src = $('#userImage').attr('src'); var name = $('#username').text(); var html = '
  • '; html += '
    '; html += '
    '; html += ''; html += '

    ' + name + '

    '; html += '
    '; html += '
    '; if (n == 0) { // 将msg进行过滤(即表情转换) let content = ''; while(msg.indexOf('[') > -1) { var start = msg.indexOf('['); var end = msg.indexOf(']'); content += `${msg.substr(0, start)}`; content += `` msg = msg.substr(end+1); } if(msg.length !== 0)content += `${msg.substr(0)}`; // alert(content);//用来测试 html += content; } else if (n == 1) { html += ''; } html += '
  • '; $('#MsgList').append(html); var Li = $('#MsgList li'); var len = Li.length; var LiH = Li.eq(len - 1).height(); var h = document.getElementById('MsgList').scrollHeight; document.getElementById('MsgList').scrollTop = h + LiH; }

    例子3:其余功能(包括管理员界面、加密和验证码)

    管理员界面:用的是bootstrap-table插件将用户的数据显示出来。参考链接1,参考链接2

    加密:用户密码加密指用户登录时对用户密码的加密,防内部攻击和外部攻击。防内部攻击是指避免管理员拿到用户的明文密码冒充用户,防外部攻击是指如果网站被黑客入侵,黑客也只能拿到加密后的密码,而不是用户的明文密码。

    加密手段是密码加盐,原理是在密码尾部插入用户唯一的id的后五位数字,再对修改后的字符串进行SHA256运算。SHA256运算用到Node.js里的crypto[ˈkrɪptoʊ]模块。

    使用SHA256而不使用MD5或者SHA-1的原因是MD5是一个弱哈希算法,相对容易发生碰撞,SHA-1是已经被破解的哈希算法,这两种不适合再用来加密。参考链接

    因为用户密码是加了密的,所以如果用户忘记密码可以在登陆界面点击“重置密码”进行重置。

    验证码:使用的模块是svg-captcha[ˈkæptʃə]。需要用到session:什么是session,express-session设置session详解

    效果:进入登录界面时,会产生一个包含正确验证码信息的session,这个session保存在数据库中,用户或管理员在点击登录按钮后,先对输入的验证码与数据库中的验证码进行匹配,如果正确则去检查用户名和密码是否正确,如果验证码不匹配则要求重新出入验证码。

    // index.js
    router.get('/captcha',(req,res)=>{
        const cap = captcha.create({
            inverse: false,// 翻转颜色
            fontSize: 36,// 字体大小
            noise: 3,// 噪声线条数
            width: 80,// 宽度
            height: 30,// 高度
            size: 4,
            ignoreChars: '0o1ilOIL',
            color: true,
            background: '#cc9966'
        });
        req.session.captcha = cap.text.toLowerCase(); // session 存储
        res.type('svg'); // 响应的类型
        res.send(cap.data);
    });
    
    // login.html
    

    一个小总结

    1、实现图片发送、修改头像等与图片相关的

    // 用户更换头像时调用的函数
    function editImageFn(e) {
        var e = e || window.event;
        var files = e.target.files || e.dataTransfer.files;
        var fs = new FileReader();
        fs.readAsDataURL(files[0]);
        fs.onload = function () {
            $('#editImage').attr('src', this.result);
        }
    }
    // 点击“发送图片”按钮时调用的函数
    function changeFiles(e) {
        var e = e || window.event;
        var files = e.target.files || e.dataTransfer.files;
        var len = files.length;
        if (len === 0) return false;
        for (var i = 0; i < len; i++) {
            var fs = new FileReader();
            fs.readAsDataURL(files[i]);
            fs.onload = function () {//该事件在读取操作完成时触发。
                var username = $('#username').text();
                var img = $('#userImage').attr('src');
                socket.emit('postImg', {imgData: this.result, username: username, image: img});
                meSendMsg(this.result, 1);
            }
        }
    }

    2、实现表情发送

    3、Bootstrap的Model(模态框)插件。Model的目的是显示来自一个单独的源的内容,可以在不离开父窗体的情况下有一些互动。子窗体可提供信息、交互等。

    本系统用Model的地方很多,比如用户修改密码、修改昵称头像,好处:不用写页面就能实现这些功能。

    4、Mongoose的整个过程:在 Mongoose 中,所有数据都由Schema[ˈskiːmə] 开始创建。Schema定义了数据库的数据结构,比如管理员数据库中,有管理员名字、密码和登录状态,而且这三个都是String。 Model是由Schema编译而成的构造器,具有抽象属性和行为,可以对数据库进行增删查改。

        Mongooose中,有三个比较重要的概念,分别是Schema、Model、Entity。它们的关系是:Schema生成Model,Model创造Document,Model和Document都可对数据库操作造成影响,但Model比Document更具操作性

      Schema用于定义数据库的结构。类似创建表时的数据定义(不仅仅可以定义文档的结构和属性,还可以定义文档的实例方法、静态模型方法、复合索引等),每个Schema会映射到mongodb中的一个collection,Schema不具备操作数据库的能力

      Model是由Schema编译而成的构造器,具有抽象属性和行为,可以对数据库进行增删查改。Model的每一个实例(instance)就是一个文档document

      Document是由Model创建的实体,它的操作也会影响数据库

    MongoDB数据库的资料:Mongoose基础入门,初入 mongo,第一次 nosql,MongoDB入门指南,详细图解mongodb下载、安装、配置与使用,mongoose入门(我反复看了挺多遍的)

    5、Mongoose的增删查改 代码参考:Mongoose基础入门

    // 增 save()、create()、insertMany()
    var mongoose = require('mongoose');
    mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
        if(!err){
            var schema = new mongoose.Schema({ age:Number, name: String});        
            var temp = mongoose.model('temp', schema);
            //使用链式写法    
            new temp({age:10,name:'save'}).save(function(err,doc){
                //[ { _id: 59720bc0d2b1125cbcd60b3f, age: 10, name: 'save', __v: 0 } ]
                console.log(doc);        
            });         
        }           
    });
    // 删 remove()
    temp.find({name:/huo/},function(err,doc){
        doc.forEach(function(item,index,arr){
            item.remove(function(err,doc){  //文档的remove()方法的回调函数参数可以省略
                //{ _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 30 }
                //{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 60 }
                console.log(doc);
            })
        })
    })  
    //查 find()、findById()、findOne()
    var mongoose = require('mongoose');
    mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
        if(!err){
            var schema = new mongoose.Schema({ age:Number, name: String});        
            var temp = mongoose.model('temp', schema);
            temp.find(function(err,docs){
                //[ { _id: 5971f93be6f98ec60e3dc86c, name: 'huochai', age: 27 },
                //{ _id: 5971f93be6f98ec60e3dc86d, name: 'wang', age: 18 },
                //{ _id: 5971f93be6f98ec60e3dc86e, name: 'huo', age: 30 },
                //{ _id: 5971f93be6f98ec60e3dc86f, name: 'li', age: 12 } ]
                console.log(docs);
            })
        }
    });
    // 改 update()、updateMany()
    var mongoose = require('mongoose');
    mongoose.connect("mongodb://u1:123456@localhost/db1", function(err) {
        if(!err){
            var schema = new mongoose.Schema({ age:Number, name: String});        
            var temp = mongoose.model('temp', schema);   
            temp.update({age:{$gte:20}},{age:40},function(err,raw){
                //update()方法中的回调函数不能省略,否则数据不会被更新。如果回调函数里并没有什么有用的信息,则可以使用exec()简化代码
                //{ n: 1, nModified: 1, ok: 1 }
                console.log(raw);
            })
        }           
    });

    6、socket是什么:客户端<=>服务器双向通信的技术

    补充express(来源

    // 很简单就可以跑起来了
    var app = require('express');
    app.get('/', function(req, res){
        res.send("hello express");
    })
    app.listen(3001);
    // 请求参数一般有四种:路径参数、查询字符串、表单、json
    // 响应有两种:纯文本(res.send) json(res.json)
    var express = require('express');
    var app = express();
    //使用中间件
    app.use(express.urlencoded({extended: false}))//解析表单的中间件
    app.use(express.json())//解析json的中间件
    //添加静态路径
    app.use("/static", express.static("static"))
    
    app.get('/', function(req, res){
        console.log(req.headers);//请求头
        res.header("b","c");//设置响应头部,b属性的值为c(跨域有用到)
        res.send("hello express");
    })
    
    //路径参数 localhost:3001/path/1
    app.get('/path/:id', function(req, res){
        console.log(req.params.id);//1
        res.send(req.params.id);
    })
    
    //查询字符串 localhost:3001/querystring?username=haha
    app.get('/querystring', function(req, res){
        console.log(req.query.username);//haha
        res.send(req.query.username);
    })
    
    //表单(需要添加中间件来解析表单)
    app.post('/form', function(req, res){
        console.log(req.body);//表单提交一般会放在body里
        res.send(req.body.username);
    })
    
    //json(需要添加中间件来解析json)
    app.post('/json0', function(req, res){
        console.log(req.body);
        res.send(req.body.username);
        //res.json(req.body);res.json后面接的是对象
    })
    app.listen(3001);

    2020.03.29对项目再一次总结

    关于Bootstrap

    • 为什么使用Bootstrap:我第一个项目,当时布局这块学的不太好,这个框架对新手比较友好,不用在布局上花费较多的时间,让我有更多的时间实现里面的功能。
    • bootstrap相关问题,如"col3"类是什么,响应式怎么实现的(我答的不太好,通过百分比实现的)

    关于加密

    • 对称加密与非对称加密的区别
    • 应用(https,这里又可以引申去ssl了)
    • 常见算法(des、md5、sha256、rsa)
    • 密码加盐的作用:指路

    关于Mongodb

    • 数据传输用的是json,为什么要用json,json数据格式的特点
    • 关系型数据库与非关系型数据库有什么区别
    • mongodb能设主键吗(我没记错的话是自动生成_id当主键的)
    • 能用mongodb做索引吗(这个只问过一次,我没回答上来)

    关于Node

    • exports 和 module.exports 的区别很好的文章
      • module.exports 初始值为一个空对象 {}
      • exports 是指向的 module.exports 的引用
      • require() 返回的是 module.exports 而不是 exports
      • module.exports 指向新的对象时,exports 断开了与 module.exports 的引用,那么通过 exports = module.exports 让 exports 重新指向 module.exports 即可。
    • express具体怎么使用
      • express.Router()是中间件 中间件原理
    • 由于自己目前在学习小程序,小程序的tcb-router是koa风格的,可以说一下koa与express的区别
      • koa2能使用async、await,express不能
      • koa2有洋葱模型和ctx上下文,express没有

    websocket的几个方法

    用户修改密码等用了bootstrap的模态框

    bootstrap-table与mongodb的交互

    $.post和$.get的使用(ajax的封装)

    你可能感兴趣的:(前端,MongoDB,Node.js)