文档地址
课程地址
源码 提取码:963h
hello wrold
console.log('hello, world');
node hello.js
nodejs 中不能使用 DOM(document) 和 BOM(window) 的 API:
- document
- window
- history
- navigator
- location
但是下面的 API 是相通的:
- console
- timer
console.log('hello, world');setTimeout(() => {console.log('fuck the world');
}, 1000);
nodejs 中的顶级对象为 global,也可以使用 globalThis 访问顶级对象:
console.log(global);
console.log(global === globalThis); // true
Buffer
字节序列
let buf1 = Buffer.alloc(10);
let buf2 = Buffer.allocUnsafe(10); // 有旧数据
let buf3 = Buffer.from('hello'); // str -> buffer
let buf4 = Buffer.from([101, 102, 103, 104]);let s4 = buf4.toString();
console.log(buf3[0]);
buf3[0] = 361; // 溢出
fs
writeFile
fs.write(file, data[, option], callback);
const fs = require('fs');fs.writeFile('./hello.txt', 'hello, world\n', err => { // asyncif (err) {console.log('write failed');} else {console.log('write success');}
});
异步与同步
writeFile 默认是异步的,对于下面的代码:
const fs = require('fs');fs.writeFile('./hello.txt', 'hello, world\n', err => { // asyncif (err) {console.log('write failed');} else {console.log('write success');}
});console(1 + 1);
2 会先输出。原因在于写文件被主线程派发给一个子线程去执行,写文件不会阻塞主线程
下面是使用同步写入:
const fs = require('fs');
fs.writeFileSync('./test.txt', 'test');
console(1 + 1);
appendFile
const fs = require('fs');fs.appendFile('./hello.txt', 'fuck, world\n', err => { // asyncif (err) {console.log('append failed');} else {console.log('appeend success');}
});
同步版本:
fs.appendFileSync('./hello.txt', 'fuck, world\n')
使用 writeFile 实现追加写入:
fs.writeFile('./hello.txt', 'fuck, world\n', {flag: 'a'}, err => { // asyncif (err) {console.log('append failed');} else {console.log('appeend success');}
});
流式写入
fs.createWriteStream(path[, options]);
let ws = fs.createWriteStream('./观书有感.txt');
ws.write('半亩方塘一鉴开\r\n');
ws.write('天光云影共徘徊\r\n');
ws.write('问渠那得清如许\r\n');
ws.write('为有源头活水来\r\n');
ws.end();
文件读取
异步读取
const fs = require('fs');fs.readFile('hello.txt', (err, data) => { // asyncif (err) {console.log('append failed');} else {console.log(data.toString());}
});
同步读取
let data = readFileSync('hello.txt');
流式读取
fs.createReadStream(path[, options])
//创建读取流对象
let rs = fs.createReadStream('./观书有感.txt');
//每次取出 64k 数据后执行一次 data 回调
rs.on('data', data => {console.log(data);console.log(data.length);
});
//读取完毕后, 执行 end 回调
rs.on('end', () => {console.log('读取完成');
});
文件复制
同步读写
const fs = require('fs');
let data = fs.readFileSync('hello.txt');
fs.writeFileSync('hello-2.txt', data);
流式读写
const rs = fs.createReadStream('hello.txt');
const ws = fs.createWriteStream('hello-3.txt');
rs.on('data', chunk => {ws.write(chunk);
});
或者如下简写:
rs.pipe(ws);
文件重命名
文件重命名通过 rename()
实现:
fs.rename(oldPath, newPath, callback);
fs.renameSync(oldPath, newPath);
fs.rename('./观书有感.txt', './论语/观书有感.txt', (err) =>{if(err) throw err;console.log('移动完成')
});
fs.renameSync('./座右铭.txt', './论语/我的座右铭.txt');
文件删除
fs.unlink(path, callback)
fs.unlinkSync(path)
const fs = require('fs');
fs.unlink('./test.txt', err => {if(err) throw err;console.log('删除成功');
});
fs.unlinkSync('./test2.txt');
使用 rm 方法也可以
文件夹操作
mkdir
fs.mkdir(path[, options], callback)
fs.mkdirSync(path[, options])
//异步创建文件夹
fs.mkdir('./page', err => {if(err) throw err;console.log('创建成功');
});
//递归异步创建
fs.mkdir('./1/2/3', {recursive: true}, err => {if(err) throw err;console.log('递归创建成功');
});
//递归同步创建文件夹
fs.mkdirSync('./x/y/z', {recursive: true});
readdir
fs.readdir(path[, options], callback)
fs.readdirSync(path[, options])
//异步读取
fs.readdir('./论语', (err, data) => {if(err) throw err;console.log(data);
});
//同步读取
let data = fs.readdirSync('./论语');
console.log(data);
rmdir
fs.rmdir(path[, options], callback)
fs.rmdirSync(path[, options])
//异步删除文件夹
fs.rmdir('./page', err => {if(err) throw err;console.log('删除成功');
});
//异步递归删除文件夹
fs.rmdir('./1', {recursive: true}, err => {if(err) {console.log(err);}console.log('递归删除')
});
//同步递归删除文件夹
fs.rmdirSync('./x', {recursive: true})
查看资源状态
fs.stat(path[, options], callback)
fs.statSync(path[, options])
//异步获取状态
fs.stat('./data.txt', (err, data) => {if(err) throw err;console.log(data);console.log(data.isFile());console.log(data.isDirectory());
});
//同步获取状态
let data = fs.statSync('./data.txt');
绝对路径:
__dirname
__dirname 保存着当前文件所在目录的绝对路径,可以使用 __dirname 与文件名拼接成绝对路径
批量重命名练习
const fs = require('fs');
const files = fs.readdirSync('./code');files.forEach(item => {let [num, name] = item.split('-');if (Number(num) < 10) {num = '0' + num;}let newName = num + '-' + name;fs.renameSync(`./code${item}`, `./code/${newName}`);console.log(item);
});
path
const path = require('path');
//获取路径分隔符
console.log(path.sep);
//拼接绝对路径
console.log(path.resolve(__dirname, 'test'));
//解析路径
let pathname = 'D:/program file/nodejs/node.exe';
console.log(path.parse(pathname));
//获取路径基础名称
console.log(path.basename(pathname))
//获取路径的目录名
console.log(path.dirname(pathname));
//获取路径的扩展名
console.log(path.extname(pathname));
HTTP 协议
请求行
请求方法
url
协议版本号
请求头
一系列键值对
请求体
请求体的内容格式是非常灵活的,可以设置任意内容
响应行
协议版本号
响应状态码
响应状态字符串
响应头
也是一系列键值对,可以自定义
响应体
响应体内容的类型是非常灵活的,常见的类型有 HTML、CSS、JS、图片、JSON
http
const http = require('http');const server = http.createServer((request, response) => {response.end('<h1>hello, http server</h1>');
});server.listen(9000, () => {console.log('listening on 9000');
});
解决中文乱码:
response.setHeader('content-type','text/html;charset=utf-8');
发送 post 请求
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="http://localhost:9000" method="post"><input type="text" name="username"><input type="text" name="password"><input type="submit" value="提交"></form>
</body>
</html>
获取 HTTP 请求报文
获取请求体
const http = require('http');const server = http.createServer((request, response) => {response.setHeader('content-type','text/html;charset=utf-8');let body = '';request.on('data', chunk => {body += chunk;});request.on('end', () => {console.log(body);response.end('hello, nodejs http')});
});server.listen(9000, () => {console.log('listening on 9000');
});
获取请求路径和查询字符串
const http = require('http');
const url = require('url');const server = http.createServer((request, response) => {response.setHeader('content-type','text/html;charset=utf-8');console.log(url.parse(request.url).pathname);console.log(url.parse(request.url, true).query);
});server.listen(9000, () => {console.log('listening on 9000');
});
或者使用内置的 URL 类:
const http = require('http');const server = http.createServer((request, response) => {let url = new URL(request.url, 'http://localhost');console.log(url.pathname);console.log(url.searchParams.get('keyword'));response.end('url new');
});server.listen(9000, () => {console.log('listening on 9000');
});
练习
const http = require('http');const server = http.createServer((request, response) => {response.setHeader('content-type','text/html;charset=utf-8');let {url, method} = request;if(url == '/login' && method == 'GET') {response.end('登录');} else if (url == '/reg' && method == 'GET') {response.end('注册');} else {response.end('wrong');}
});server.listen(9000, () => {console.log('listening on 9000');
});
设置响应报文
const http = require('http');const server = http.createServer((request, response) => {response.statusCode = 200; // 响应行response.setHeader('content-type','text/html;charset=utf-8'); // 响应头response.end('response'); // 响应体
});server.listen(9000, () => {console.log('listening on 9000');
});
设置响应头:
response.setHeader('server','nodejs');
response.setHeader('myheader','test test test');
response.setHeader('test',['a', 'b', 'c']); // 设置多个同名响应头
设置响应体:
response.write('hello');
练习
搭建 HTTP 服务,响应一个 4 行 3 列的表格,并且要求表格有隔行换色效果 ,且点击单元格能高亮显示
服务端代码:
const http = require('http');
const fs = require('fs');const server = http.createServer((request, response) => {response.setHeader('content-type','text/html;charset=utf-8');fs.readFile('table.html', (err, data) => {response.end(data);});
});server.listen(9000, () => {console.log('listening on 9000');
});
前端代码:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<style>td {padding: 20px 40px;}table tr:nth-child(odd) {background-color: #aef;}table tr:nth-child(even) {background-color: #fcb;}table, td {border-collapse: collapse;}
</style>
<body><table border="1"><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr></table>
</body>
<script>let tds = document.querySelectorAll('td');tds.forEach(item => {item.onclick = function() {this.style.background = '#bfa';}});
</script>
</html>
网页资源的加载都是循序渐进的,首先获取 HTML 的内容, 然后解析 HTML 在发送其他资源的请求,如 CSS,Javascript,图片等
练习扩展
将前端的 html css js 三者分离:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<link rel="stylesheet" href="table.css">
<body><table border="1"><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr></table>
</body>
<script src="table.js"></script>
</html>
// table.js
let tds = document.querySelectorAll('td');
tds.forEach(item => {item.onclick = function() {this.style.background = '#bfa';}
});
// table.css
td {padding: 20px 40px;
}
table tr:nth-child(odd) {background-color: #aef;
}
table tr:nth-child(even) {background-color: #fcb;
}
table, td {border-collapse: collapse;
}
服务端:
const http = require('http');
const fs = require('fs');const server = http.createServer((request, response) => {let {pathname} = new URL(request.url, 'http://localhost');if(pathname === '/') {fs.readFile('table.html', (err, data) => {response.end(data);});} else if (pathname === '/table.js') {fs.readFile('table.js', (err, data) => {response.end(data);});} else if (pathname === '/table.css') {fs.readFile('table.css', (err, data) => {response.end(data);});} else {response.end('error');}
});server.listen(9000, () => {console.log('listening on 9000');
});
获取 page 下的静态资源服务:
const http = require('http');
const fs = require('fs');const server = http.createServer((request, response) => {let {pathname} = new URL(request.url, 'http://localhost');let filePath = __dirname + '/page' + pathname;fs.readFile(filePath, (err, data) => {if (err) {response.statusCode = 500;response.end('read file failed');}response.end(data);});
});server.listen(9000, () => {console.log('listening on 9000');
});
<link rel="stylesheet" href="/css/app.css">
MIME 类型
错误处理
const http = require('http');
const fs = require('fs');const server = http.createServer((request, response) => {if(request.method !== 'GET') {response.statusCode = 405;response.end('<h1> 405 Method not allowed </h1>');return;}let {pathname} = new URL(request.url, 'http://localhost');let filePath = __dirname + '/page' + pathname;fs.readFile(filePath, (err, data) => {if (err) {console.log(err);switch(err.code) {case 'ENOENT':response.statusCode = 404;response.end('<h1> 404 Not Found </h1>');case 'EPERM':response.statusCode = 403;response.end('<h1> 403 Forbidden </h1>');default:response.statusCode = 500;response.end('error');}}response.end(data);});
});server.listen(9000, () => {console.log('listening on 9000');
});
GET 与 POST 场景区别
模块化
其中拆分出的每个文件就是一个模块 ,模块的内部数据是私有的,不过模块可以暴露内部数据以便其他模块使用
// me.js
function tiemo() {console.log("tiemo");
}function niejiao() {console.log('niejiao');
}module.exports = {tiemo,niejiao
};
const me = require('./me.js'); // 不能省略 ./me.tiemo();
me.niejiao();
导入文件夹
// package.json
{"main": "./app.js"
}
// app.js
module.exports = "this is module1";
// index.js
const m1 = require('./module1'); // 不能省略 ./
console.log(m1); // "this is module1"
导入模块的流程
CommonJS 模块化规范
包管理工具
npm
node package manager
初始化包
npm init
npm init 命令的作用是将文件夹初始化为一个『包』, 交互式创建 package.json 文件
{"name": "test","version": "1.0.0","description": "learning npm","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"author": "daniel","license": "ISC"
}
搜索包
下载安装包
npm install uniq
使用包:
const uniq = require('uniq');let a1 = [1, 2, 3, 4, 5, 3, 2, 1];const result = uniq(a1);console.log(result)
开发依赖与生产依赖
npm i -S jquery # 生产依赖
npm i -D less # 开发依赖
全局安装
npm i -g nodemon
npm root -g # 查看全局安装位置
安装所有依赖:
指定版本与删除包
npm i jquery@1.11.2
npm remove uniq # 局部删除
npm r -g nodemon # 全局删除
配置命令别名
{
...
"scripts": {
"server": "node server.js",
"start": "node index.js",
},
...
}
配置完成之后,可以使用别名执行命令
npm run server
npm run start
cnpm
cnpm 是一个淘宝构建的 npmjs.com 的完整镜像,也称为『淘宝镜像』,网址https://npmmirror.com/
cnpm 服务部署在国内阿里云服务器上 , 可以提高包的下载速度 官方也提供了一个全局工具包 cnpm ,操作命令与 npm 大体相同
npm install -g cnpm --registry=https://registry.npmmirror.com
用 npm 也可以使用淘宝镜像:
使用命令行配置:
npm config set registry https://registry.npmmirror.com/
或者使用 nrm 工具配置:
nrm = npm registry manager
npm i -g nrm
nrm use taobao
nrm use npm
yarn
npm i -g yarn
nvm = node version manager
express 框架
基于 nodejs 的 web 开发框架
node init
node i express
一个最简单的 webserver:
const express = require('express');const app = express();app.get('/home', (req, res) => {res.end('express hello');
});app.listen(3000, () => {console.log('listening...');
});
路由
const express = require('express');const app = express();app.get('/home', (req, res) => {res.end('express hello');
});app.get('/', (req, res) => {res.end('home');
});app.post('/login', (req, res) => {res.end('login');
});app.all('/test', (req, res) => {res.end('test');
});app.all('*', (req, res) => {res.end('404 not found');
});app.listen(3000, () => {console.log('listening...');
});
发送 post 请求:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><form action="http://localhost:3000/login" method="post"><button>登录</button></form>
</body>
</html>
获取请求报文参数
//导入 express
const express = require('express');
//创建应用对象
const app = express();
//获取请求的路由规则
app.get('/request', (req, res) => {//1. 获取报文的方式与原生 HTTP 获取方式是兼容的console.log(req.method);console.log(req.url);console.log(req.httpVersion);console.log(req.headers);//2. express 独有的获取报文的方式//获取查询字符串console.log(req.query); // 『相对重要』console.log(req.path);// 获取指定的请求头console.log(req.get('host'));res.send('请求报文的获取');
});
//启动服务
app.listen(3000, () => {console.log('启动成功....')
});
获取路由参数
路由参数指的是 URL 路径中的参数(数据),使用 req.params
获取
app.get('/:id.html', (req, res) => { //:id 是占位符res.send('商品详情, 商品 id 为' + req.params.id);
});
const express = require('express');
const {singers} = require('singers.json');const app = express();app.get('/singer/:id.html', (req, res) => {let {id} = req.params;let singer = singers.find(item => {if (item.id === Number(id)) {return true;}});if (!singer) {res.statusCode = 404;res.end('404 not found');} else {res.end(singer);}
});
响应设置
express 框架封装了一些 API 来方便给客户端响应数据,并且兼容原生 HTTP 模块的获取方式
//获取请求的路由规则
app.get("/response", (req, res) => {//1. express 中设置响应的方式兼容 HTTP 模块的方式res.statusCode = 404;res.statusMessage = 'xxx';res.setHeader('abc','xyz');res.write('响应体');res.end('xxx');//2. express 的响应方法res.status(500); //设置响应状态码res.set('xxx','yyy');//设置响应头res.send('中文响应不乱码');//设置响应体//连贯操作res.status(404).set('xxx','yyy').send('你好朋友')//3. 其他响应res.redirect('http://atguigu.com'); //重定向res.download('./package.json'); //下载响应res.json(); //响应 JSON,一般用于接口化开发res.sendFile(__dirname + '/home.html') //响应文件内容
});
中间件
全局中间件
每一个请求到达服务端之后都会执行全局中间件函数
const express = require('express');
const fs = require('fs');
const path = require('path');const app = express();function recordMiddleware(req, res, next) { // 全局中间件函数let {url, ip} = req;fs.appendFileSync(path.resolve(__dirname, './access.log'), `${url} ${ip}\r\n`);//执行next函数(当如果希望执行完中间件函数之后,仍然继续执行路由中的回调函数,必须调用next)next();
}app.use(recordMiddleware); // 注册全局中间件app.get('/home', (req, res) => {res.end('home');
});app.get('/admin', (req, res) => {res.end('admin');
});app.all('*', (req, res) => {res.end('404 not found');
});app.listen(3000, () => {console.log('listening...');
});
路由中间件
如果只需要对某一些路由进行功能封装 ,则就需要路由中间件
const express = require('express');
const fs = require('fs');
const path = require('path');const app = express();let checkCodeMiddleware = (req, res, next) => { // 全局中间件函数if (req.query.code === '521') {next();} else {res.send('code error');}
}app.get('/setting', checkCodeMiddleware, (req, res) => {res.end('home');
});app.get('/admin', checkCodeMiddleware, (req, res) => {res.end('admin');
});app.all('*', (req, res) => {res.end('404 not found');
});app.listen(3000, () => {console.log('listening...');
});
静态资源中间件
app.use(express.static(__dirname + '/public'));
获取请求体数据
按照要求搭建 http 服务:
- GET /login 显示表单网页
- POST /login 获取表单中的用户名和密码
使用 body-parser 中间件:
const express = require('express');
const bodyParser = require('body-parser');//处理 querystring 格式的请求体
let urlParser = bodyParser.urlencoded({extended:false});const app = express();app.get('/login', (req, res) => {res.sendFile(__dirname + '/form.html');
});app.post('/login', urlParser, (req, res) => {//用户名console.log(req.body.username);//密码console.log(req.body.password);res.send('获取请求体数据');
});app.all('*', (req, res) => {res.end('404 not found');
});app.listen(3000, () => {console.log('listening...');
});
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>登录</title>
</head>
<body><form action="/login" method="post">username: <input type="text" name="username"> <br />password: <input type="text" name="password"> <br /><button>submit</button></form>
</body>
</html>
防盗链
禁止其他域名访问本网站资源
referer
请求头能够携带当前域名信息:
referer: https://www.jj20.com/
const express = require('express');const app = express();app.use((req, res, next) => {let referer = req.get('referer'); // 获取 referer 请求头if (referer) {let url = new URL(referer);let hostname = url.hostname;if (hostname !== '127.0.0.1') {res.status(404).send('<h1> 404 not found</h1>');return;}}next();
});app.use(express.static(__dirname + '/public'));app.all('*', (req, res) => {res.end('404 not found');
});app.listen(3000, () => {console.log('listening...');
});
Router 路由模块化
什么是 Router:express 中的 Router 是一个完整的中间件和路由系统,可以看作是一个小型的 app 对象
Router 的作用:对路由进行模块化,更好地管理路由
//server.js
const express = require('express');const app = express();const homeRouter = require('./homeRouter');
const adminRouter = require('./adminRouter');app.use(homeRouter);
app.use(adminRouter);app.all('*', (req, res) => {res.end('404 not found');
});app.listen(3000, () => {console.log('listening...');
});
// homeRouter.js
const express = require('express');const router = express.Router();router.get('/home', (req, res) => {res.send('home');
});router.get('/search', (req, res) => {res.send('search');
});module.exports = router;
// adminRouter.js
const express = require('express');const router = express.Router();router.get('/admin', (req, res) => {res.send('admin');
});router.get('/setting', (req, res) => {res.send('setting');
});module.exports = router;
ejs 模板引擎
模板引擎:分离用户界面和业务数据
npm i ejs
<% code %> // 执行 js 代码
<%= code %> // 输出转义的数据到模板上
<%- code %> // 输出非转义的数据到模板上
const ejs = require('ejs');let y = 'you';let res = ejs.render('i love <%= y %>', {y: y});
console.log(res);
也可以对 html 文档中的内容进行替换:
// ejs.js
const ejs = require('ejs');
const fs = require('fs');let y = 'you';let html = fs.readFileSync('./index.html').toString();let res = ejs.render(html, {y: y});
console.log(res);
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><h1>i love <%= y %> </h1>
</body>
</html>
列表渲染
// ejs.js
const ejs = require('ejs');
const fs = require('fs');const arr = ['appal', 'orange', 'banana', 'grapes'];let html = fs.readFileSync('./index.html').toString();let res = ejs.render(html, {arr: arr});console.log(res);
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><ul><% arr.forEach(item => { %><li><%= item %></li><% }) %></ul>
</body>
</html>
条件渲染
// ejs.js
const ejs = require('ejs');
const fs = require('fs');let isLogin = false;let html = fs.readFileSync('./index.html').toString();let res = ejs.render(html, {isLogin: isLogin});console.log(res);
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><% if(isLogin) { %><span> 欢迎回来 </span><% } else { %><button>登录</button> <button>注册</button><% } %>
</body>
</html>
express 中使用 ejs
// ejs.js
const express = require('express');
const path = require('path');const app = express();app.set('view engine', 'ejs'); // 设置引擎
app.set('views', path.resolve(__dirname, './views')); // 设置模板路径app.get('/home', (req, res) => {let title = 'hello, world';res.render('home', {title}); // 将 title render 给 home.ejs
});app.listen(3000, () => {console.log('listening on 3000');
})
// views/home.ejs
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><%= title %>
</body>
</html>
express-generator
用于快速创建一个应用的骨架
npm i -g express-generator
express -e prj_name
cd prj_name
npm i // 安装依赖
npm start
然后浏览器访问 http://localhost:3000/
即可
// app.js
app.use('/', indexRouter);
app.use('/users', usersRouter); // users 是路由前缀
文件上传
// routers/index.js
var express = require('express');
var router = express.Router();
const {formidable} = require('formidable');/* GET home page. */
router.get('/', function(req, res, next) {res.render('index', { title: 'Express' });
});router.get('/portrait', (req, res) => {res.render('portrait');
});router.post('/portrait', (req, res) => {const form = formidable({multiples: true,uploadDir: __dirname + '/../public/images',keepExtensions: true});form.parse(req, (err, fields, files) => {if (err) {next(err);return;}let url = '/images/' + files.portrait[0].newFilename;res.send(url);});
});module.exports = router;
<!--portrait.ejs-->
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Portrait</title>
</head>
<body><h1>Portrait</h1><form action="/portrait" method="post" enctype="multipart/form-data">用户名:<input type="text" name="username"> <br>头像:<input type="file" name="portrait"><button>点击提交</button></form>
</body>
</html>
记账本案例实践
初始化项目:
express -e
npm i
npm start
添加 2 个路由和 2 个静态页面:
// routes/index.js
/* GET home page. */
router.get('/account', function(req, res, next) {// res.render('index', { title: 'Express' });res.render('list');
});// 发送表单页面
router.get('/account/create', function(req, res, next) {// res.render('index', { title: 'Express' });res.render('create');
});// 处理表单数据
router.post('/account', (req, res) => {console.log(req.body);res.send('add account');
});
<!-- views/list.ejs -->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><linkhref="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"rel="stylesheet"/><style>label {font-weight: normal;}.panel-body .glyphicon-remove{display: none;}.panel-body:hover .glyphicon-remove{display: inline-block}</style></head><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><h2>记账本</h2><hr /><div class="accounts"><div class="panel panel-danger"><div class="panel-heading">2023-04-05</div><div class="panel-body"><div class="col-xs-6">抽烟只抽煊赫门,一生只爱一个人</div><div class="col-xs-2 text-center"><span class="label label-warning">支出</span></div><div class="col-xs-2 text-right">25 元</div><div class="col-xs-2 text-right"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></div></div></div><div class="panel panel-success"><div class="panel-heading">2023-04-15</div><div class="panel-body"><div class="col-xs-6">3 月份发工资</div><div class="col-xs-2 text-center"><span class="label label-success">收入</span></div><div class="col-xs-2 text-right">4396 元</div><div class="col-xs-2 text-right"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></div></div></div></div></div></div></div></body>
</html>
表单页面:
<!-- views/create.ejs -->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>添加记录</title><linkhref="/css/bootstrap.css"rel="stylesheet"/><!-- 以 public 为根目录 --><link href="/css/bootstrap-datepicker.css" rel="stylesheet"></head><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><h2>添加记录</h2><hr /><form method="post" action="/account"><div class="form-group"><label for="item">事项</label><inputname="title"type="text"class="form-control"id="item"/></div><div class="form-group"><label for="time">发生时间</label><inputname="time"type="text"class="form-control"id="time"/></div><div class="form-group"><label for="type">类型</label><select class="form-control" id="type" name="type"><option value="-1">支出</option><option value="1">收入</option></select></div><div class="form-group"><label for="account">金额</label><inputname="account"type="text"class="form-control"id="account"/></div><div class="form-group"><label for="remarks">备注</label><textarea class="form-control" id="remarks" name="remark"></textarea></div><hr><button type="submit" class="btn btn-primary btn-block">添加</button></form></div></div></div><!-- 以 public 为根目录 --><script src="/js/jquery.min.js"></script><script src="/js/bootstrap.min.js"></script><script src="/js/bootstrap-datepicker.min.js"></script><script src="/js/bootstrap-datepicker.zh-CN.min.js"></script><script src="/js/main.js"></script></body>
</html>
lowdb
npm i lowdb@1.0.0
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');const adapter = new FileSync('db.json');
const db = low(adapter);// Set some defaults
db.defaults({ posts: [], user: {} }).write()
/*
{"posts": [],"user": {}
}
*/// Add a post
db.get('posts').push({ id: 1, title: 'lowdb is awesome'}).write()
/*
{"posts": [{"id": 1,"title": "lowdb is awesome"}],"user": {}
}
*/db.get('posts').unshift({id: 2, title: 'hello, world'}).write()
/*{"posts": [{"id": 2,"title": "hello, world"},{"id": 1,"title": "lowdb is awesome"}],"user": {}}
*/console.log(db.get('posts').value())db.get('posts').remove({id: 2}).write()db.get('posts').find({id: 1}).assign({title: "fuck, world"}).write()
保存账单信息
// data/db.json
{"accounts": []
}
安装 shortid:
npm i shortid
收到 post 请求后存入数据库:
// index.js
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const adapter = new FileSync(__dirname + '/../data/db.json');
const db = low(adapter);
const shortid = require("shortid");router.post('/account', (req, res) => {console.log(req.body);let id = shortid.generate();db.get("accounts").unshift({id: id, ...req.body}).write();res.render('success', {msg: "添加成功~~~", url: '/account'});
});
<!-- success.ejs -->
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>提醒</title><linkhref="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"rel="stylesheet"/><style>.h-50{height: 50px;}</style>
</head>
<body><div class="container"><div class="h-50"></div><div class="alert alert-success" role="alert"><h1><%= msg %></h1><p><a href="<%= url %>">点击跳转</a></p></div></div>
</body>
</html>
账单列表
<!-- list.ejs -->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><linkhref="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"rel="stylesheet"/><style>label {font-weight: normal;}.panel-body .glyphicon-remove{display: none;}.panel-body:hover .glyphicon-remove{display: inline-block}</style></head><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><h2>记账本</h2><hr /><div class="accounts"><% accounts.forEach(item => {%><div class="panel <%= item.type === '-1' ? 'panel-danger' : 'panel-success'%>"><div class="panel-heading"><%= item.time %></div><div class="panel-body"><div class="col-xs-6"><%= item.title %></div><div class="col-xs-2 text-center"><span class="label <%= item.type === '-1' ? 'label-warning' : 'label-success'%>"><%= item.type === '-1' ? '支出' : '收入'%></span></div><div class="col-xs-2 text-right"><%= item.account %></div><div class="col-xs-2 text-right"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></div></div></div><% }) %> </div></div></div></div></body>
</html>
删除账单
前端代码修改:
<!-- list.ejs -->
<a href="/account/<%= item.id %>">
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</a>
后端代码修改:
// index.js
router.get("/account/:id", (req, res) => {let id = req.params.id;db.get('accounts').remove({id: id}).write();res.render('success', {msg: '删除成功', url: '/account'});
});
MongoDB
MongoDB 是一个基于分布式文件存储的数据库,跟 json 文件非常类似
- 一个 json 文件好比是一个数据库,一个 Mongodb 服务下可以有 N 个数据库
- json 文件中的一级属性的数组值好比是集合
- 数组中的对象好比是文档
- 对象中的属性有时也称之为字段
一般情况下:
一个项目使用一个数据库
一个集合会存储同一类型的数据
启动数据库
Mongodb 默认使用 C:/data/db
作为文件存储路径
mongod # 启动数据库服务端
mongo # 启动数据库客户端
show dbs
默认有 3 个数据库:
admin
config
local
数据库与集合命令
show dbs // 显示所有的数据库
use db_name // 切换到指定的数据库,如果不存在则创建
db // 显示当前所在的数据库
use db_name
db.dropDatabase() // 删除当前数据库
db.createCollection('users')
show collections
db.collection_name.drop() // 删除某个集合
db.collection_name.renameCollection(collection_name2) // 重命名集合
文档命令
use bilibili
db.users.insert({name: 'hcc', age: 18})
db.users.insert({name: 'hnp', age: 19})
db.users.insert({name: 'ss', age: 20})db.users.find() // 查询所有文档
db.users.find({age: 20}) // 按条件查询
db.users.update({name: 'ss'}, {$set: {age: 24}}) // 更新
db.users.remove({name: 'ss'})
mongoose
Mongoose 是一个对象文档模型库,方便使用代码操作 mongodb 数据库
npm init
npm i mongoose
连接数据库
const mongoose = require('mongoose');// 如果数据库不存在,则自动创建
mongoose.connect('mongodb://127.0.0.1:27017/bilibili');// 使用 once 绑定的回调函数,只执行一次
mongoose.connection.once('open', () => {console.log('open');
});
mongoose.connection.on('error', () => {console.log('error');
});
mongoose.connection.on('close', () => {console.log('close');
});
setTimeout(() => {mongoose.disconnect();
});
插入文档
mongoose.connection.once('open', () => {let BookSchema = new mongoose.Schema({title: String,author: String,price: Number});let BookModel = mongoose.model('books', BookSchema);BookModel.create({title: 'xiyouji',author: 'wuchengen',price: 19}).then((data) => {console.log(data);mongoose.disconnect();});console.log('open');
});
mongoose.connection.once('open', () => {let BookSchema = new mongoose.Schema({title: String,author: String,price: Numberis_hot: Boolean,tags: Array,pub_time: Date});let BookModel = mongoose.model('books', BookSchema);BookModel.create({title: 'xiyouji',author: 'wuchengen',price: 19,is_hot: true,tags: ['a', 'b', 'c']pub_time: new Date()}).then((data) => {console.log(data);mongoose.disconnect();})console.log('open');
});
字段值验证(约束):Mongoose 有一些内建验证器,可以对字段值进行验证
let BookSchema = new mongoose.Schema({title: {type: String,required: true},author: {type: String,default: 'anonymous'},gender: {type: String,enum: ['M', 'F']},username: {type: String,unique: true // unique 需要重建集合才能有效果},price: Number});
删除文档
// 删除单条
SongModel.deleteOne({_id:'5dd65f32be6401035cb5b1ed'}, function(err){if(err) throw err;console.log('删除成功');mongoose.connection.close();
});// 删除多条
SongModel.deleteMany({author:'Jay'}, function(err){if(err) throw err;console.log('删除成功');mongoose.connection.close();
});
更新文档
SongModel.updateOne({author: 'JJ Lin'}, {author: '林俊杰'}, function (err) {if(err) throw err;mongoose.connection.close();
});SongModel.updateMany({author: 'Leehom Wang'}, {author: '王力宏'}, function (err) {if(err) throw err;mongoose.connection.close();
});
读取文档
查询一条数据:
SongModel.findOne({author: '王力宏'}, function(err, data){if(err) throw err;console.log(data);mongoose.connection.close();
});
//根据 id 查询数据
SongModel.findById('5dd662b5381fc316b44ce167', function(err, data){if(err) throw err;console.log(data);mongoose.connection.close();
});
批量查询数据:
//不加条件查询
SongModel.find(function(err, data){if(err) throw err;console.log(data);mongoose.connection.close();
});
//加条件查询
SongModel.find({author: '王力宏'}, function(err, data){if(err) throw err;console.log(data);mongoose.connection.close();
});
条件控制
db.students.find({id:{$gt:3}});
逻辑运算:
db.students.find({$or:[{age:18}, {age:24}]});
db.students.find({$and: [{age: {$lt:20}}, {age: {$gt: 15}}]});
正则:
db.students.find({name:/imissyou/});
db.students.find({name: new RegExp('imissyou')});
个性化读取
字段筛选:
//0:不要的字段
//1:要的字段
SongModel.find().select({_id:0,title:1}).exec(function(err,data){if(err) throw err;console.log(data);mongoose.connection.close();
});
数据排序:
//sort 排序
//1:升序
//-1:倒序
SongModel.find().sort({hot:1}).exec(function(err,data){if(err) throw err;console.log(data);mongoose.connection.close();
});
BookModel.find().select({name: 1, price: 1, _id: 0}).sort({price: 1}).exec((err, data) => {if (err) {console.log('error');return;}console.log(data);
});
数据截取:
//skip 跳过 limit 限定
SongModel.find().skip(10).limit(10).exec(function(err,data){if(err) throw err;console.log(data);mongoose.connection.close();
});
BookModel.find()
.select({name: 1, price: 1, _id: 0})
.sort({price: -1})
.skip(3) // 跨过 3 个
.limit(3) // 取 3 个
.exec((err, data) => {if (err) {console.log('error');return;}console.log(data);
});
模块化
// db/db.js
const {DBHOST, DBPORT, DBNAME}= require('../config/config');module.exports = function (success, error) {if (typeof error !== 'function') {error = () => {console.log('connection error');}}const mongoose = require('mongoose');mongoose.connect(`mongodb://${DBHOST}:${DBPORT}/${DBNAME}`);mongoose.connection.once('open', () => {success();});mongoose.connection.on('error', () => {error();});mongoose.connection.on('close', () => {console.log('close');});
}
// config/config.js
module.exports = {DBHOST: '127.0.0.1', DBPORT: 27017,DBNAME: 'bilibili'
}
// models/BookModel.js
const mongoose = require('mongoose');let BookSchema = new mongoose.Schema({title: String,author: String,price: Number
});
let BookModel = mongoose.model('books', BookSchema);module.exports = BookModel;
// demo1.js
const mongoose = require('mongoose');
const db = require('./db/db.js');
const BookModel = require('./models/BookModel.js');db(() => {BookModel.create({title: 'xiyouji',author: 'wuchengen',price: 19}).then((data) => {console.log(data);mongoose.disconnect();});console.log('success');
});
数据库可视化工具:robo3t
记账本案例优化
// config/config.js
module.exports = {DBHOST: '127.0.0.1', DBPORT: 27017,DBNAME: 'bilibili'
}
// db/db.js
const {DBHOST, DBPORT, DBNAME}= require('../config/config');module.exports = function (success, error) {if (typeof error !== 'function') {error = () => {console.log('connection error');}}const mongoose = require('mongoose');mongoose.connect(`mongodb://${DBHOST}:${DBPORT}/${DBNAME}`);mongoose.connection.once('open', () => {success();});mongoose.connection.on('error', () => {error();});mongoose.connection.on('close', () => {console.log('close');});
}
// models/AccountModel.js
const mongoose = require('mongoose');let AccountSchema = new mongoose.Schema({title: {type: String,required: true},time: Date,type: {type: Number,default: -1},account: {type: Number,required: true},remark: String
});let AccountModel= mongoose.model('accounts', AccountSchema);
module.exports = AccountModel;
// bin/www
db(() => {// content of www
});
将字符串转为日期对象:
npm i moment
moment('2023-02-24').toDate();
// index.js
var express = require('express');
var router = express.Router();
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');const adapter = new FileSync(__dirname + '/../data/db.json');
const db = low(adapter);
const shortid = require("shortid");
const moment = require("moment");
const AccountModel = require("../models/AccountModel")/* GET home page. */
router.get('/account', function(req, res, next) {// res.render('index', { title: 'Express' });// let accounts = db.get('accounts').value();AccountModel.find().sort({time: -1}).exec((err, data) => {if (err) {res.status(500).send("读取失败"); // 修改 mongoose 版本号到 6.8.0return;}// data.time = moment(data.time).format("YYYY-MM-DD");res.render('list', {accounts: data, moment: moment});});
});router.get('/account/create', function(req, res, next) {// res.render('index', { title: 'Express' });res.render('create');
});router.post('/account', (req, res) => {console.log(req.body);// let id = shortid.generate();// db.get("accounts").unshift({id: id, ...req.body}).write();AccountModel.create({...req.body,time: moment(req.time).toDate(),}).then((data) => {console.log(data);// res.status(500).send("插入失败!");// mongoose.disconnect();});res.render('success', {msg: "添加成功~~~", url: '/account'});
});router.get("/account/:id", (req, res) => {let id = req.params.id;// db.get('accounts').remove({id: id}).write();AccountModel.deleteOne({_id: id}, (err, data) => {if (err) {res.status(500).send("删除失败");return;}res.render('success', {msg: '删除成功', url: '/account'});});
});module.exports = router;
<!-- list.ejs -->
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><linkhref="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"rel="stylesheet"/><style>label {font-weight: normal;}.panel-body .glyphicon-remove{display: none;}.panel-body:hover .glyphicon-remove{display: inline-block}</style></head><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><div class="row"><h2 class="col-xs-6">记账本</h2><h2 class="col-xs-6 text-right"><a href="/account/create" class="btn btn-primary">添加账单</a></h2></div><hr /><div class="accounts"><% accounts.forEach(item => {%><div class="panel <%= item.type === -1 ? 'panel-danger' : 'panel-success'%>"><div class="panel-heading"><%= moment(item.time).format("YYYY-MM-DD") %></div><div class="panel-body"><div class="col-xs-6"><%= item.title %></div><div class="col-xs-2 text-center"><span class="label <%= item.type === -1 ? 'label-warning' : 'label-success'%>"><%= item.type === -1 ? '支出' : '收入'%></span></div><div class="col-xs-2 text-right"><%= item.account %></div><div class="col-xs-2 text-right"><a class="delBtn" href="/account/<%= item._id %>"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></a></div></div></div><% }) %> </div></div></div></div></body>
</html>
<script>let delBtns = document.querySelectorAll(".delBtn");delBtns.forEach(item => {item.addEventListener("click", function(e) {if (confirm("确认删除?")) {return true;} else{e.preventDefault();}});});
</script>
API接口
一个接口就是 服务中的一个路由规则
,根据请求响应结果。一个接口一般由如下几个部分组成:
- 请求方法
- 接口地址(URL)
- 请求参数
- 响应结果
接口示例
RESTful API
接口化有利于前后端分离
json-server
json-server 本身是一个 JS 编写的工具包,可以快速搭建 RESTful API 服务
npm i -g json-server
准备一个 json 文件:
// db.json
{"song": [{ "id": 1, "name": "干杯", "singer": "五月天" },{ "id": 2, "name": "当", "singer": "动力火车" },{ "id": 3, "name": "不能说的秘密", "singer": "周杰伦" }]
}
启动 json-server:
json-server --watch db.json
访问:
http://localhost:3000/song
http://localhost:3000/song/1
apifox
GET
获取所有歌曲:
POST
新增一首歌曲:
DELETE
删除 id 为 4 的歌曲:
PATCH
修改 id 为 3 的歌曲:
记账本案例增加接口
// api/account.jsvar express = require('express');
var router = express.Router();
const moment = require("moment");
const AccountModel = require("../../models/AccountModel")// 获取账单列表
router.get('/account', function(req, res, next) {AccountModel.find().sort({time: -1}).exec((err, data) => {if (err) {// res.status(500).send("读取失败");res.json({code: '1001',msg: '读取失败',data: null});return;}// res.render('list', {accounts: data, moment: moment});res.json({code: '0000',msg: '读取成功',data: data});});
});// 新增记录
router.post('/account', (req, res) => {console.log(req.body);// 表单验证...AccountModel.create({...req.body,time: moment(req.time).toDate(),}).then((data) => {console.log(data);res.json({code: '0000',msg: '添加成功',data: data});});// res.render('success', {msg: "添加成功~~~", url: '/account'});
});// 删除账单
router.delete("/account/:id", (req, res) => {let id = req.params.id;AccountModel.deleteOne({_id: id}, (err, data) => {if (err) {// res.status(500).send("删除失败");res.json({code: '1000',msg: '删除失败',data: null});return;}// res.render('success', {msg: '删除成功', url: '/account'});res.json({code: '0000',msg: '删除成功',data: null});});
});// 获取单条账单
router.get('/account/:id', (req, res) => {let id = req.params.id;AccountModel.findById(id, (err, data) => {if (err) {return res.json({code: '1004',msg: '查询失败',data: null});}res.json({code: '0000',msg: '查询成功',data: data});});
});// 更新单个账单
router.patch('/account/:id', (req, res) => {let {id} = req.params;AccountModel.updateOne({_id: id}, req.body, (err, data) => {if (err) {return res.json({code: '1005',msg: '更新失败',data: null});}AccountModel.findById(id, (err, data) => {if (err) {return res.json({code: '1004',msg: '查询失败',data: null});}res.json({code: '0000',msg: '更新成功',data: data});});});
});module.exports = router;
// app.js
...
const accountRouter = require('./routes/api/account');
app.use('/api', accountRouter);
...
会话控制
所谓会话控制就是 对会话进行控制
。HTTP 是一种 无状态
的协议,它没有办法区分多次的请求是否来自于同一个客户端, 无法区分用户,而产品中又大量存在的这样的需求,所以我们需要通过 会话控制
来解决该问题
常见的会话控制技术有三种:
- cookie
- session
- token
cookie
cookie 是 HTTP 服务器发送到用户浏览器并保存在本地的一小块数据
- cookie 是保存在浏览器端的一小块数据
- cookie 是按照域名划分保存的
浏览器向服务器发送请求时,会自动将 当前域名下
可用的 cookie 设置在请求头中,然后传递给服务器
这个请求头的名字也叫 cookie ,所以将 cookie 理解为一个 HTTP 的请求头也是可以的
express 设置 cookie
const express = require('express');const app = express();app.get('/set-cookie', (req, res) => {// res.cookie('name', 'zhangsan'); // 会在浏览器关闭后自动销毁res.cookie('name', 'zhangsan', {maxAge: 60 * 1000});res.cookie('theme', 'blue');res.send('home');
});
app.listen(3000);
express 删除 cookie
app.get('/remove-cookie', (req, res) => {res.clearCookie('theme');res.send('删除成功');
});
express 提取 cookie
用 cookie-parser
中间件:
npm i cookie-parser
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get('/get-cookie', (req, res) => {console.log(req.cookies);res.send(`欢迎您 ${req.cookies.name}`);
});app.listen(3000);
session
session 是保存在服务器端的一块儿数据 ,保存当前访问用户的相关信息
作用:实现会话控制,可以识别用户的身份,快速获取当前用户的相关信息
const express = require('express');
const session = require('express-session');
const mongoStore = require('connect-mongo');const app = express();app.use(session({name: 'sid', //设置cookie的name,默认值是:connect.sidsecret: 'atguigu', //参与加密的字符串(又称签名)saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的idresave: true, //是否在每次请求时重新保存sessionstore: mongoStore.create({mongoUrl: 'mongodb://127.0.0.1:27017/bilibili' //数据库的连接配置}),cookie: {httpOnly: true, // 开启后前端无法通过 JS 操作 document.cookie() ×maxAge: 1000 * 60 * 5 // 这一条 是控制 sessionID 的过期时间的!!!},
})); app.get('/', (req, res) => {res.send('home');
});app.get('/login', (req, res) => {// http://localhost:3000/login?username=admin&password=adminif(req.query.username === 'admin' && req.query.password === 'admin') {req.session.username = 'admin';res.send('登录成功');} else {res.send('登录失败');}
});app.get('/cart', (req, res) => {if(req.session.username) { // 中间件根据 sid 查询数据库设置 req.session.usernameres.send('购物车');} else {res.send('请登录');}
});app.get('/logout', (req, res) => {req.session.destroy(() => {res.send('退出成功');});
});app.listen(3000);
cookie 与 session 的区别
记账本案例注册功能
// views/auth/reg.ejs
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>注册</title><link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />
</head><body><div class="container"><div class="row"><div class="col-xs-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4"><h2>注册</h2><hr /><form method="post" action="/reg"><div class="form-group"><label for="item">用户名</label><input name="username" type="text" class="form-control" id="item" /></div><div class="form-group"><label for="time">密码</label><input name="password" type="password" class="form-control" id="time" /></div><hr><button type="submit" class="btn btn-primary btn-block">注册</button></form></div></div></div>
</body></html>
// UserModel.js
const mongoose = require('mongoose');let UserSchema = new mongoose.Schema({username: String,password: String
});let UserModel= mongoose.model('users', UserSchema);
module.exports = UserModel;
// routes/web/auth.js
var express = require('express');
var router = express.Router();
const UserModel= require("../../models/UserModel");
const md5 = require('md5');
//注册
router.get('/reg', (req, res) => {//响应 HTML 内容res.render('auth/reg');
});//注册用户
router.post('/reg', (req, res) => {//做表单验证//获取请求体的数据UserModel.create({...req.body, password: md5(req.body.password)}, (err, data) => {if(err){res.status(500).send('注册失败, 请稍后再试~~');return;}res.render('success', {msg: '注册成功', url: '/login'});});});//登录页面
router.get('/login', (req, res) => {//响应 ejs 内容res.render('auth/login');
});//登录操作
router.post('/login', (req, res) => {//获取用户名和密码let {username, password} = req.body;//查询数据库UserModel.findOne({username: username, password: md5(password)}, (err, data) => {//判断if(err){res.status(500).send('登录, 请稍后再试~~');return;}//判断 dataif(!data){return res.send('账号或密码错误~~');}//写入sessionreq.session.username = data.username;req.session._id = data._id;//登录成功响应res.render('success', {msg: '登录成功', url: '/account'});});
});//退出登录
router.post('/logout', (req, res) => {//销毁 sessionreq.session.destroy(() => {res.render('success', {msg: '退出成功', url: '/login'});})
});module.exports = router;
// views/auth/login.ejs
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>登录</title><link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet" />
</head><body><div class="container"><div class="row"><div class="col-xs-12 col-md-8 col-md-offset-2 col-lg-4 col-lg-offset-4"><h2>登录</h2><hr /><form method="post" action="/login"><div class="form-group"><label for="item">用户名</label><input name="username" type="text" class="form-control" id="item" /></div><div class="form-group"><label for="time">密码</label><input name="password" type="password" class="form-control" id="time" /></div><hr><button type="submit" class="btn btn-primary btn-block">登录</button></form></div></div></div>
</body>
</html>
// app.js
const session = require('express-session');
const MongoStore = require('connect-mongo');
const {DBHOST, DBNAME, DBPORT} = require('./config/config');
const authRouter = require('./routes/web/auth');//设置 session 的中间件
app.use(session({name: 'sid', //设置cookie的name,默认值是:connect.sidsecret: 'atguigu', //参与加密的字符串(又称签名) 加盐saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的idresave: true, //是否在每次请求时重新保存session 20 分钟 4:00 4:20store: MongoStore.create({mongoUrl: `mongodb://${DBHOST}:${DBPORT}/${DBNAME}` //数据库的连接配置}),cookie: {httpOnly: true, // 开启后前端无法通过 JS 操作maxAge: 1000 * 60 * 60 * 24 * 7 // 这一条 是控制 sessionID 的过期时间的!!!},
}));
用户登录检测
// index.js
var express = require('express');
var router = express.Router();
const moment = require("moment");
const AccountModel = require("../../models/AccountModel");
const checkLoginMiddleware = require('../../middlewares/checkLoginMiddleware');/* GET home page. */
router.get('/account', checkLoginMiddleware, function(req, res, next) {AccountModel.find().sort({time: -1}).exec((err, data) => {if (err) {res.status(500).send("读取失败");return;}res.render('list', {accounts: data, moment: moment});});
});router.get('/account/create', checkLoginMiddleware, function(req, res, next) {res.render('create');
});router.post('/account', checkLoginMiddleware, (req, res) => {console.log(req.body);AccountModel.create({...req.body,time: moment(req.time).toDate(),}).then((data) => {console.log(data);});res.render('success', {msg: "添加成功~~~", url: '/account'});
});router.get("/account/:id", checkLoginMiddleware, (req, res) => {let id = req.params.id;AccountModel.deleteOne({_id: id}, (err, data) => {if (err) {res.status(500).send("删除失败");return;}res.render('success', {msg: '删除成功', url: '/account'});});
});module.exports = router;
// middlewares/checkLoginMiddleware.js
function checkLoginMiddleware(req, res, next) {if (!req.session.username) {return res.redirect('/login');}next();
}
module.exports = checkLoginMiddleware;
退出登录:
// list.ejs
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><linkhref="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.css"rel="stylesheet"/><style>label {font-weight: normal;}.panel-body .glyphicon-remove{display: none;}.panel-body:hover .glyphicon-remove{display: inline-block}</style></head><body><div class="container"><div class="row"><div class="col-xs-12 col-lg-8 col-lg-offset-2"><div class="row text-right"><div class="col-xs-12" style="padding-top: 20px;"><form method="post" action="/logout"><button href="/logout" class="btn btn-danger">退出</a> <!--新增退出按钮--></form></div></div><hr><div class="row"><h2 class="col-xs-6">记账本</h2><h2 class="col-xs-6 text-right"><a href="/account/create" class="btn btn-primary">添加账单</a></h2></div><hr /><div class="accounts"><% accounts.forEach(item => {%><div class="panel <%= item.type === -1 ? 'panel-danger' : 'panel-success'%>"><div class="panel-heading"><%= moment(item.time).format("YYYY-MM-DD") %></div><div class="panel-body"><div class="col-xs-6"><%= item.title %></div><div class="col-xs-2 text-center"><span class="label <%= item.type === -1 ? 'label-warning' : 'label-success'%>"><%= item.type === -1 ? '支出' : '收入'%></span></div><div class="col-xs-2 text-right"><%= item.account %></div><div class="col-xs-2 text-right"><a class="delBtn" href="/account/<%= item._id %>"><spanclass="glyphicon glyphicon-remove"aria-hidden="true"></span></a></div></div></div><% }) %> </div></div></div></div></body>
</html>
<script>let delBtns = document.querySelectorAll(".delBtn");delBtns.forEach(item => {item.addEventListener("click", function(e) {if (confirm("确认删除?")) {return true;} else{e.preventDefault();}});});
</script>
CSRF = Cross-Site Request Forgery,跨站请求伪造
在同一浏览器下,访问一个攻击页面,攻击页面就可以向被攻击页面的服务器发送请求:
<link rel="stylesheet" href="http://127.0.0.1:3000/logout">
首页和 404
// views/404.ejs
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>404</title>
</head>
<body><script src="//volunteer.cdn-go.cn/404/latest/404.js"></script>
</body>
</html>
// routes/web/index.js
/* GET home page. */
router.get('/', (req, res) => {res.redirect('/account');
});
// app.js
// catch 404 and forward to error handler
app.use(function(req, res, next) {// next(createError(404));res.render('404');
});
token
token 是什么:token 是服务端生成并返回给 HTTP 客户端的一串加密字符串, token 中保存着用户信息
token 的作用:实现会话控制,可以识别用户的身份,主要用于移动端 APP
token 需要手动添加到请求报文中
JWT
JWT(JSON Web Token )是目前最流行的跨域认证解决方案,可用于基于 token 的身份验证。JWT 使 token 的生成与校验更规范
npm i jsonwebtoken
const jwt = require('jsonwebtoken');//创建 token
// jwt.sign(数据, 加密字符串, 配置对象)
let token = jwt.sign({username: 'zhangsan'
}, 'atguigu', {expiresIn: 60 //单位是 秒
});console.log(token);//解析 token
jwt.verify(token, 'atguigu', (err, data) => {if (err) {console.log('校验失败~~');return;}console.log(data);
});
jwt
一个 token 实例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwiaWF0IjoxNjk5MTg4NDU0LCJleHAiOjE2OTkxODg1MTR9.
Y1cQlEPAFIcfl5w4rUj9Xh_WOXYpG1c6QZmpjtkhFbM
分为 3 部分:
- Header(头部)
- Payload(负载)
- Signature(签名)
记账本案例添加token
// api/auth.js
var express = require('express');
var router = express.Router();
const UserModel= require("../../models/UserModel");
const md5 = require('md5');
const jwt = require('jsonwebtoken');//登录操作
router.post('/login', (req, res) => {//获取用户名和密码let {username, password} = req.body;//查询数据库UserModel.findOne({username: username, password: md5(password)}, (err, data) => {//判断if(err){res.json({code: '2001',msg: '数据库读取失败',data: null});return;}//判断 dataif(!data){return res.json({code: '2002',msg: '账号或密码错误~~',data: null});}let token = jwt.sign({username: data.username, _id: data._id}, 'atguigu', {expiresIn: 60 * 60 *24});res.json({code: '0000',msg: '登录成功',data: token});});
});//退出登录
router.post('/logout', (req, res) => {//销毁 sessionreq.session.destroy(() => {res.render('success', {msg: '退出成功', url: '/login'});})
});module.exports = router;
// app.js
const authApiRouter = require('./routes/api/auth');
app.use('/api', authApiRouter);
用户登录,下放 token
token校验
// middleWares/checkTokenMiddleware.js
const jwt = require('jsonwebtoken');function checkTokenMiddleware(req, res, next) {let token = req.get('token');if(!token) {return res.json({code: '2003',msg: 'token 缺失',data: null});}jwt.verify(token, 'atguigu', (err, data) => {if (err) {return res.json({code: '2004',msg: '身份校验失败',data: null});}req.user = data;next();});
}
module.exports = checkTokenMiddleware;
将 token 放在头中发送请求:
// api/account.js
const express = require('express');
const router = express.Router();
const moment = require("moment");
const AccountModel = require("../../models/AccountModel")
const jwt = require('jsonwebtoken');
const checkTokenMiddleware = require('../../middlewares/checkTokenMiddleware');// 获取账单列表
router.get('/account', checkTokenMiddleware, function(req, res, next) {// console.log(req.user);AccountModel.find({username: req.user.username}).sort({time: -1}).exec((err, data) => {if (err) {// res.status(500).send("读取失败");res.json({code: '1001',msg: '读取失败',data: null});return;}// res.render('list', {accounts: data, moment: moment});res.json({code: '0000',msg: '读取成功',data: data});});
});// 新增记录
router.post('/account', checkTokenMiddleware, (req, res) => {console.log(req.body);// 表单验证...AccountModel.create({...req.body,time: moment(req.time).toDate(),}).then((data) => {console.log(data);res.json({code: '0000',msg: '添加成功',data: data});});// res.render('success', {msg: "添加成功~~~", url: '/account'});
});// 删除账单
router.delete("/account/:id", checkTokenMiddleware, (req, res) => {let id = req.params.id;AccountModel.deleteOne({_id: id}, (err, data) => {if (err) {// res.status(500).send("删除失败");res.json({code: '1000',msg: '删除失败',data: null});return;}// res.render('success', {msg: '删除成功', url: '/account'});res.json({code: '0000',msg: '删除成功',data: null});});
});// 获取单条账单
router.get('/account/:id', checkTokenMiddleware, (req, res) => {let id = req.params.id;AccountModel.findById(id, (err, data) => {if (err) {return res.json({code: '1004',msg: '查询失败',data: null});}res.json({code: '0000',msg: '查询成功',data: data});});
});// 更新单个账单
router.patch('/account/:id', checkTokenMiddleware, (req, res) => {let {id} = req.params;AccountModel.updateOne({_id: id}, req.body, (err, data) => {if (err) {return res.json({code: '1005',msg: '更新失败',data: null});}AccountModel.findById(id, (err, data) => {if (err) {return res.json({code: '1004',msg: '查询失败',data: null});}res.json({code: '0000',msg: '更新成功',data: data});});});
});module.exports = router;