基于Node.js+express+MySQL+Bootstrap实现的简单登录注册
2017-11-24 14:31
1431 查看
初学node.js及express框架,想要利用express搭建一个服务端实现简单的注册功能,接下来开始简单叙述思想并附上实现代码(代码中已有详细注释,按如下模块顺序学习实现)。
基于Bootstrap实现登录注册切换页面及其交互,包括对输入信息的验证等
利用express搭建一个简单的web服务端,将页面注入服务端,实现通过服务器访问到该页面
利用MySQL建表实现数据持久化,进一步实现登录注册,能实现数据库数据的增删改查
遇到的问题及解决办法:
利用node.js中express.static()设置静态资源,资源放置的路径问题。
解决办法: express.static是Express 提供的内置中间件,用来设置静态文件如:图片, CSS, JavaScript 等。可以在express安装包目录的同级目录中新建文件夹,用于放置本地的静态资源,这样就可以实现直接通过URL访问本地静态资源。(注:该文件夹中的目录结构也能体现在URL中)
数据库如何设计?
解决办法: 利用MySQL,用户ID可以设置为自增序列,因此在插入时不用插入ID,更加方便!
数据库user表设计如下:
如何将与数据库操作相关的数据及方法与服务器搭建相关代码分离
解决办法: 可以利用node.js模块系统进行解决,初学者可以参考Node.js模块系统,将数据库操作相关的方法均封装在connectMysql.js中,然后在服务器端相关代码封装在service_index.js中,在service_index.js中引入connectMysql.js。
具体实现方法如下:
首先在被引用的connectMysql.js中设置一个被访问入口:
然后在调用外部js(connectMysql.js)的service_index.js文件中引入connectMysql.js(注:注意路径问题,此处将两个文件放在同一文件夹下)
引入方法
调用方法
node.js连接数据库、操作数据库均使用了回调函数,回调函数的确能解决I/O阻塞问题,但若想将回调函数的值作为函数的返回值却遇到了难题,直接利用return返回。
解决办法: 初学者可以参考
node.js如何获取异步函数回调的返回值。
采用Async、Q、Promise等第三方库处理异步回调
我在此处利用了第三方库——Async进行解决
简单介绍一下,Async是第三方专门处理异步的库,能很好的控制异步流程,一个简单的例子读取两个文件的内容,并对合并结果进行处理的例子如下:
另外,除了需要控制异步流程,为了获取回调函数异步处理后的值,还需要继续借助回调函数获取原本需要return的值,如下为一个例子:
用以上方法,利用回调函数获取数据是一种不错的解决办法。
在本实例中,以插入注册用户为例,具体解决办法如下:
首先,在connectMysql.js中,由于在添加用户时应该先查询是否已存在该用户,需要先进行查询操作,判断后再进行插入操作,因此需要引入async库控制流程,下为数据库操作方法:
另外,在service_index.js中需要利用回调函数异步调用上述addUser方法返回的结果值以进行进一步判断操作(用户已存在则提示用户已存在,成功插入则进入欢迎界面)
连接数据库时出现Cannot enqueue Handshake after invoking quit.
解决办法: 该问题出现的原因是在于node连接上mysql后如果因网络原因丢失连接或者用户手工关闭连接后,原有的连接挂掉,需要重新连接,但连着多次connect 或 end 都会报错,因此解决办法是在一会话中只进行一次连接和一次断开。
结果截图
登录界面
注册界面
登录输入
登录成功
登录失败
注册界面提示信息
注册失败
注册成功
附录(代码):
register.html
2.register.js
3.service_index.js
4.connectMysql.js
基于Bootstrap实现登录注册切换页面及其交互,包括对输入信息的验证等
利用express搭建一个简单的web服务端,将页面注入服务端,实现通过服务器访问到该页面
利用MySQL建表实现数据持久化,进一步实现登录注册,能实现数据库数据的增删改查
遇到的问题及解决办法:
利用node.js中express.static()设置静态资源,资源放置的路径问题。
解决办法: express.static是Express 提供的内置中间件,用来设置静态文件如:图片, CSS, JavaScript 等。可以在express安装包目录的同级目录中新建文件夹,用于放置本地的静态资源,这样就可以实现直接通过URL访问本地静态资源。(注:该文件夹中的目录结构也能体现在URL中)
数据库如何设计?
解决办法: 利用MySQL,用户ID可以设置为自增序列,因此在插入时不用插入ID,更加方便!
数据库user表设计如下:
如何将与数据库操作相关的数据及方法与服务器搭建相关代码分离
解决办法: 可以利用node.js模块系统进行解决,初学者可以参考Node.js模块系统,将数据库操作相关的方法均封装在connectMysql.js中,然后在服务器端相关代码封装在service_index.js中,在service_index.js中引入connectMysql.js。
具体实现方法如下:
首先在被引用的connectMysql.js中设置一个被访问入口:
exports.connectMysql = function() { var testValue; function testFunc() { /* 此处省略具体方法及数据 */ } return {'testFunc':testFunc,'testValue':testValue} }
然后在调用外部js(connectMysql.js)的service_index.js文件中引入connectMysql.js(注:注意路径问题,此处将两个文件放在同一文件夹下)
引入方法
var connectMysql = require('./connectMysql');
调用方法
var connection = connectMysql.connectMysql();//获取入口 /* 如下方式调用方法及变量 */ connection.queryData("SELECT * FROM user");
node.js连接数据库、操作数据库均使用了回调函数,回调函数的确能解决I/O阻塞问题,但若想将回调函数的值作为函数的返回值却遇到了难题,直接利用return返回。
解决办法: 初学者可以参考
node.js如何获取异步函数回调的返回值。
采用Async、Q、Promise等第三方库处理异步回调
我在此处利用了第三方库——Async进行解决
简单介绍一下,Async是第三方专门处理异步的库,能很好的控制异步流程,一个简单的例子读取两个文件的内容,并对合并结果进行处理的例子如下:
var async = require('async') , fs = require('fs'); async.parallel([ function(callback){ fs.readFile('/etc/passwd', function (err, data) { if (err) callback(err); callback(null, data); }); }, function(callback){ fs.readFile('/etc/passwd2', function (err, data2) { if (err) callback(err); callback(null, data2); }); } ], function(err, results){ // 在这里处理data和data2的数据,每个文件的内容从results中获取 // results是一个包含结果的数组[data,data1] });
另外,除了需要控制异步流程,为了获取回调函数异步处理后的值,还需要继续借助回调函数获取原本需要return的值,如下为一个例子:
function getTBS(callback){ ng.get("http://tieba.baidu.com/dc/common/tbs",function(data,status,headers){ callback(null,data) },headers,'utf8') } getTBS(function(err,result){ if(err) throw err; console.log(result) //{"tbs":"a2bdd05a3fd08e561463670847","is_login":1} });
用以上方法,利用回调函数获取数据是一种不错的解决办法。
在本实例中,以插入注册用户为例,具体解决办法如下:
首先,在connectMysql.js中,由于在添加用户时应该先查询是否已存在该用户,需要先进行查询操作,判断后再进行插入操作,因此需要引入async库控制流程,下为数据库操作方法:
function addUser(response, callback) { //顺序执行回调函数 async.parallel([ //查询是否已有该记录 function(callback) { var sql = 'SELECT passwd FROM user WHERE username="' + response.username + '"'; connection.query(sql, function(err, result) { if (err) callback(err); var res = false; if (result.length == 0) { res = true; } callback(null, res) }); } ], function(err, results) { //根据查询记录进行分别处理 var flag = false; console.log(results); if (results[0]) { var addSql = 'INSERT INTO user(userId,username,phonenum,passwd) VALUES(0,?,?,?)'; var addSqlParams = [String(response.username), String(response.phonenum), String(response.passswd)]; //增 connection.query(addSql, addSqlParams, function(err, result) { if (err) { console.log('[INSERT ERROR] - ', err.message); } console.log('--------------------------INSERT----------------------------'); //console.log('INSERT ID:',result.insertId); console.log('INSERT ID:', result); console.log('-----------------------------------------------------------------\n\n'); }); flag = true; } else { flag = false; } callback(null, flag); }); }
另外,在service_index.js中需要利用回调函数异步调用上述addUser方法返回的结果值以进行进一步判断操作(用户已存在则提示用户已存在,成功插入则进入欢迎界面)
app.post('/register_post', urlencodedParser, function(req, res) { var response = { "phonenum": req.body.phonenum, "username": req.body.usernameR, "passswd": req.body.passswd1, }; connection.addUser(response, function(err, result) { if (err) throw err; console.log(result); if (result) { //插入成功 res.send("嘿," + response.username + ",欢迎加入!"); } else { res.send("用户已存在"); //成功则返回带有用户名的页面 } }); })
连接数据库时出现Cannot enqueue Handshake after invoking quit.
解决办法: 该问题出现的原因是在于node连接上mysql后如果因网络原因丢失连接或者用户手工关闭连接后,原有的连接挂掉,需要重新连接,但连着多次connect 或 end 都会报错,因此解决办法是在一会话中只进行一次连接和一次断开。
结果截图
登录界面
注册界面
登录输入
登录成功
登录失败
注册界面提示信息
注册失败
注册成功
附录(代码):
register.html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>注册/登录</title> <!-- 包含头部信息用于适应不同设备 --> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> .nav-tabs>li { width: 50%; text-align: center } .nav-tabs>li.active>a, .nav-tabs>li.active>a:focus, .nav-tabs>li.active>a:hover { color: #FFF !important; cursor: default; background-color: #2e6da4 !important; border: 1px solid #ddd !important; border-bottom-color: transparent; } .tab-pane { margin-top: 20px; } .requisiteTip { color: red; } .tip { /* size: 0.6em; */ /* display: none; */ } </style> <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css"> <!-- Normalize.css在 HTML 元素的默认样式中提供了更好的跨浏览器一致性。 --> <link rel="stylesheet" href="../css/Normalize.css"> <script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script> <script src="https://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <script src="../js/angular.js"></script> <script src="../js/angular.min.js"></script> </head> <body> <div class="container"> <ul id="myTab" class="nav nav-tabs"> <li class="active"><a href="#login" data-toggle="tab">账号登录</a></li> <li><a href="#reg" data-toggle="tab">账号注册</a></li> </ul> <div id="myTabContent" class="tab-content"> <div class="tab-pane fade in active" id="login"> <form role="form" class="form-horizontal" action="http://127.0.0.1:8088/login_post" method="POST"> <div class="form-group"> <label for="username" class="col-sm-2 control-label pull-left"> <span class="glyphicon glyphicon-user"></span> 名称 </label> <div class="col-sm-10"> <input type="text" class="form-control" id="username" name="username" placeholder="请输入用户名"> </div> </div> <div class="form-group"> <label for="passwd" class="col-sm-2 control-label"> <span class="glyphicon glyphicon-lock"></span> 密码 </label> <div class="col-sm-10"> <input type="password" class="form-control" id="passwd" name="passwd" placeholder="请输入密码"> </div> </div> <div class="form-group"> <div class=""> <div class="checkbox pull-left" style="display:inline-block;margin-left:5%"> <label><input type="checkbox" id="remember">请记住我 </label> </div> <div class="checkbox pull-right" style="display:inline-block;margin-right:8%"> <a href="#">忘记密码</a> </div> </div> </div> <div class="form-group"> <input type="submit" id="loginBtn" class="btn btn-primary btn-lg" value="登录" style="width:92%;margin-left:4%"> </div> </form> </div> <div class="tab-pane fade" id="reg"> <form role="form" class="form-horizontal" action="http://127.0.0.1:8088/register_post" method="POST"> <div class="form-group"> <label for="phonenum" class="col-sm-2 control-label pull-left"> <span class="glyphicon glyphicon-phone"></span> 手机 <span class="requisiteTip">*</span> </label> <div class="col-sm-10"> <input type="text" class="form-control" id="phonenum" name="phonenum" placeholder="请输入您的手机号"> </div> </div> <div class="tip" id="phonenumTip"></div> <div class=" form-group"> <label for="usernameR" class="col-sm-2 control-label pull-left"> <span class="glyphicon glyphicon-user"></span> 用户名 <span class="requisiteTip ">*</span> </label> <div class="col-sm-10"> <input type="text" class="form-control" id="usernameR" name="usernameR" placeholder="请输入用户名 "> </div> </div> <div class="tip" id="usernameTip"></div> <div class="form-group "> <label for="passswd1 " class="col-sm-2 control-label "> <span class="glyphicon glyphicon-lock"></span> 密码 <span class="requisiteTip ">*</span> </label> <div class="col-sm-10 "> <input type="password" class="form-control " id="passswd1" name="passswd1" placeholder="请输入密码"> </div> </div> <div class="tip" id="passswd1Tip"></div> <div class="form-group "> <label for="passswd2" class="col-sm-2 control-label "> <span class="glyphicon glyphicon-lock "></span> 确认密码 <span class="requisiteTip ">*</span> </label> <div class="col-sm-10 "> <input type="password" class="form-control " id="passswd2" name="passswd2" placeholder="请再次输入密码 "> </div> </div> <div class="tip" id="passswd2Tip"></div> <br> <div class="form-group "> <input type="submit" id="registerBtn" class="btn btn-primary btn-lg " style="width:92%;margin-left:4% " value="注册"> </div> </form> </div> </div> <div style="height:200px "></div> <nav class="navbar navbar-default navbar-fixed-bottom " style="background-color:inherit "> <div style="text-align:center "> 版权所有 <span class="glyphicon glyphicon-copyright-mark "></span> hxy </div> </nav> </div> </body> <script> $('.navbar').css('position', 'absolute'); $('li[name="reg_tag "]').on('click', function() { if (!$(this).hasClass('active')) { $(this).addClass('active'); $('li[name="login_tag "]').removeClass('active'); } }) $('li[name="login_tag "]').on('click', function() { if (!$(this).hasClass('active')) { $(this).addClass('active'); $('li[name="reg_tag "]').removeClass('active'); } }) </script> <script src="../nodeJs/connectMysql.js"></script> <script src="../js/pageJs/register.js"></script> </html>
2.register.js
var flag1 = false; var flag2 = false; var flag3 = false; var flag4 = false; $("#passswd1").on('click', function() { console.log($(this).val()); var text = '<h6 class="text-info"> <span class="glyphicon glyphicon-exclamation-sign"></span> 6-16位常用数字,字母及常用符号,区分大小写</h6> <br > '; if ($(this).val() == "") { $("#passswd1Tip").html(""); $("#passswd1Tip").append(text); $("#passswd1Tip").css('display', 'block'); } }); $("#passswd1").on('blur', function() { var keywd = $(this).val(); if (keywd != '') { console.log(keywd); var patt = /^[0-9a-zA-Z~!@#$%^&*()_+=-\[\]\;',./|:"<>?"\{\}]{6,16}$/; console.log(patt.test(keywd)); if (!patt.test(keywd)) { $("#passswd1Tip").html(""); var text = '<h6 class="text-info"> <span class="glyphicon glyphicon-exclamation-sign"></span> 输入密码格式错误,6-16位常用数字,字母及常用符号,区分大小写</h6> <br > '; $("#passswd1Tip").append(text); $("#passswd1Tip").css('display', 'block'); flag3 = false; enableBtn("registerBtn"); } else { $("#passswd1Tip").html(""); $("#passswd1Tip").css('display', 'none'); flag3 = true; enableBtn("registerBtn"); } } }); $("#passswd2").on('blur', function() { var keywd = $(this).val(); var keywd1 = $("#passswd1").val(); console.log(keywd); console.log(keywd1); if (keywd != '') { if (keywd1 != keywd) { $("#passswd2Tip").html(""); var text = '<h6 class="text-info"> <span class="glyphicon glyphicon-exclamation-sign"></span> 两次输入的密码不相同,请重新输入</h6> <br > '; $("#passswd2Tip").append(text); $("#passswd2Tip").css('display', 'block'); flag4 = false; enableBtn("registerBtn"); } else { $("#passswd2Tip").html(""); $("#passswd2Tip").css('display', 'none'); flag4 = true; enableBtn("registerBtn"); } } else { $("#passswd2Tip").html(""); var text = '<h6 class="text-info"> <span class="glyphicon glyphicon-exclamation-sign"></span> 请再次输入密码</h6> <br > '; $("#passswd2Tip").append(text); $("#passswd2Tip").css('display', 'block'); flag4 = false; enableBtn("registerBtn"); } }); $("#phonenum").on("click", function() { var text = '<h6 class="text-info"> <span class="glyphicon glyphicon-exclamation-sign"></span> 请输入11位手机号码</h6> <br> '; if ($(this).val() == "") { $("#phonenumTip").html(""); $("#phonenumTip").append(text); $("#phonenumTip").css('display', 'block'); } }); $("#phonenum").on("blur", function() { var phonenum = $(this).val(); if (phonenum != '') { console.log(phonenum); var patt = /^1[3,5,7,8,9]{1}\d{9}$/; console.log(patt.test(phonenum)); if (!patt.test(phonenum)) { $("#phonenumTip").html(""); var text = '<h6 class="text-info"> <span class="glyphicon glyphicon-exclamation-sign"></span> 输入密码格式错误,6-16位常用数字,字母及常用符号,区分大小写</h6> <br > '; $("#phonenumTip").append(text); $("#phonenumTip").css('display', 'block'); flag1 = false; enableBtn("registerBtn"); } else { $("#phonenumTip").html(""); $("#phonenumTip").css('display', 'none'); flag1 = true; enableBtn("registerBtn"); } } }); $("#usernameR").on("click", function() { console.log("www"); var text = '<h6 class="text-info"> <span class="glyphicon glyphicon-exclamation-sign"></span> 请输入您的用户名</h6> <br> '; if ($(this).val() == "") { $("#usernameTip").html(""); $("#usernameTip").append(text); $("#usernameTip").css('display', 'block'); } }); $("#usernameR").on("blur", function() { var username = $(this).val(); if (username == '') { flag2 = false; enableBtn("registerBtn"); } else { $("#usernameTip").html(""); $("#usernameTip").css('display', 'none'); flag2 = true; enableBtn("registerBtn"); } }); function enableBtn(btnId) { console.log(flag1); console.log(flag2); console.log(flag3); console.log(flag4); if (flag1 && flag2 && flag3 && flag4) { $("#" + btnId).attr("disabled", false) console.log("1"); } else { $("#" + btnId).attr("disabled", "disabled") console.log("2"); } console.log($("#" + btnId).attr("disabled")); } $("#remember").on('click', function() { if ($(this)[0].checked) { setCookie('username', $("username").val()); setCookie('passwd', $("passwd").val()); } else { deleteCookie('username'); deleteCookie('passwd'); } }); function setCookie(name, value) { var argv = setCookie.arguments; var argc = setCookie.arguments.length; var expires = (argc > 2) ? argv[2] : null; if (expires != null) { var LargeExpDate = new Date(); LargeExpDate.setTime(LargeExpDate.getTime() + (expires * 1000 * 3600 * 24)); } document.cookie = name + "=" + escape(value) + ((expires == null) ? "" : ("; expires=" + LargeExpDate.toGMTString())); } function getCookie(Name) { var search = Name + "=" if (document.cookie.length > 0) { offset = document.cookie.indexOf(search) if (offset != -1) { offset += search.length end = document.cookie.indexOf(";", offset) if (end == -1) end = document.cookie.length return unescape(document.cookie.substring(offset, end)) } else return "" } } function deleteCookie(name) { var expdate = new Date(); expdate.setTime(expdate.getTime() - (86400 * 1000 * 1)); setCookie(name, "", expdate); } $("#loginBtn").on('click', function() { var sessionStorage = new window.sessionStorage(); sessionStorage.setItem("username", $("#username").val()); sessionStorage.setItem("passwd", $("#passwd").val()); console.log("session:%s-%s", sessionStorage.username, sessionStorage.passwd); });
3.service_index.js
var express = require("express");
var bodyParser = require('body-parser');
var connectMysql = require('./connectMysql');
var session = require('express-session');
var cookieParser = require('cookie-parser');
var app = express();
app.use(cookieParser());
app.use(session({
secret: '12345',
cookie: { maxAge: 60000 },
resave: false,
saveUninitialized: true
}));
// 创建 application/x-www-form-urlencoded 编码解析
var urlencodedParser = bodyParser.urlencoded({ extended: false })
var connection = connectMysql.connectMysql();
app.use(express.static('note-taking'));
app.get('/', function(request, response) {
var connection = connectMysql.connectMysql();
connection.queryData("SELECT * FROM user");
})
app.post('/login_post', urlencodedParser, function(req, res) {
var response = {
"username": req.body.username,
"passwd": req.body.passwd
};
connection.checkUser(response, function(err, result) {
if (err) throw err;
if (result) {
//用户名及密码正确
req.session.username = response.username;
req.session.passwd = response.passwd;
console.log("session:%s-%s", req.session.username, req.session.passwd);
res.send(req.session.username); //成功则返回带有用户名的页面
} else {
res.send("用户名或密码错误"); //成功则返回带有用户名的页面
}
});
})
app.post('/register_post', urlencodedParser, function(req, res) { var response = { "phonenum": req.body.phonenum, "username": req.body.usernameR, "passswd": req.body.passswd1, }; connection.addUser(response, function(err, result) { if (err) throw err; console.log(result); if (result) { //插入成功 res.send("嘿," + response.username + ",欢迎加入!"); } else { res.send("用户已存在"); //成功则返回带有用户名的页面 } }); })
var server = app.listen(8088, 'localhost', function(req, res) {
console.log(__dirname);
console.log("The Server is running at http://%s:%s", server.address().address, server.address().port);
});
4.connectMysql.js
var async = require('async');
exports.connectMysql = function() {
/* 此处省略具体方法及数据 */
//连接数据库
var mysql = require('mysql');
var connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '123456',
database: 'nodetest'
});
connection.connect();
function test() {
console.log("nihao");
}
function checkUser(response, callback) {
var sql = 'SELECT passwd FROM user WHERE username="' + response.username + '"';
connection.query(sql, function(err, result) {
var res = false;
if (result.length > 0) {
if (result[0]['passwd'] == response.passwd) {
res = true;
}
}
callback(null, res)
});
}
function addUser(response, callback) { //顺序执行回调函数 async.parallel([ //查询是否已有该记录 function(callback) { var sql = 'SELECT passwd FROM user WHERE username="' + response.username + '"'; connection.query(sql, function(err, result) { if (err) callback(err); var res = false; if (result.length == 0) { res = true; } callback(null, res) }); } ], function(err, results) { //根据查询记录进行分别处理 var flag = false; console.log(results); if (results[0]) { var addSql = 'INSERT INTO user(userId,username,phonenum,passwd) VALUES(0,?,?,?)'; var addSqlParams = [String(response.username), String(response.phonenum), String(response.passswd)]; //增 connection.query(addSql, addSqlParams, function(err, result) { if (err) { console.log('[INSERT ERROR] - ', err.message); } console.log('--------------------------INSERT----------------------------'); //console.log('INSERT ID:',result.insertId); console.log('INSERT ID:', result); console.log('-----------------------------------------------------------------\n\n'); }); flag = true; } else { flag = false; } callback(null, flag); }); }
return { 'checkUser': checkUser, 'test': test, 'connection': connection, 'addUser': addUser };
}
相关文章推荐
- node.js+express+mySQL+ejs+bootstrop实现网站登录注册功能
- Node.js+Express+MySql实现用户登录注册
- nodejs+express+mongodb简单实现注册登录
- nodejs+express+mongodb简单实现注册登录写发博客
- Node.js+Express+MySql实现用户登录注册功能
- Node.js+Express+MongoDB实现简单登录注册功能
- node.js基于express框架搭建一个简单的注册登录Web功能
- express实现登录注册(mysql+mongodb),简单添加session(两种)
- 使用 NodeJS+Express+MySQL 实现简单的增删改查
- ant design+node.js+mongoose实现一个简单的注册登录功能
- node.js非常简单实现登录注册功能-学习小demo
- 用node和express连接mysql实现登录注册的实现代码
- 例子:实现最新版本Node.js中Express+mongodb的登录注册页面
- (NodeJS学习文章收集三) node.js基于express框架搭建一个简单的注册登录Web功能
- 用node和express连接mysql实现登录注册
- 使用node.js实现简单注册登录功能
- Node.js基于express搭建注册登录功能
- node.js实现用户登录注册简单示例
- Maven + Spring MVC+Mybatis + MySQL +AngularJS + Bootstrap 实现简单微博应用(三)前后台交互
- Simple server side cache for Express with Node.js——Express 实现简单的服务器端缓存【翻译】