文档地址
课程地址
源码 提取码:963h
console.log('hello, world');
node hello.js
nodejs 中不能使用 DOM(document) 和 BOM(window) 的 API:
但是下面的 API 是相通的:
console.log('hello, world');
setTimeout(() => {
console.log('fuck the world');
}, 1000);
nodejs 中的顶级对象为 global,也可以使用 globalThis 访问顶级对象:
console.log(global);
console.log(global === globalThis); // true
字节序列
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.write(file, data[, option], callback);
const fs = require('fs');
fs.writeFile('./hello.txt', 'hello, world\n', err => { // async
if (err) {
console.log('write failed');
} else {
console.log('write success');
}
});
writeFile 默认是异步的,对于下面的代码:
const fs = require('fs');
fs.writeFile('./hello.txt', 'hello, world\n', err => { // async
if (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);
const fs = require('fs');
fs.appendFile('./hello.txt', 'fuck, world\n', err => { // async
if (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 => { // async
if (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) => { // async
if (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 方法也可以
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});
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);
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);
});
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));
一系列键值对
请求体的内容格式是非常灵活的,可以设置任意内容
也是一系列键值对,可以自定义
响应体内容的类型是非常灵活的,常见的类型有 HTML、CSS、JS、图片、JSON
const http = require('http');
const server = http.createServer((request, response) => {
response.end('hello, http server
');
});
server.listen(9000, () => {
console.log('listening on 9000');
});
解决中文乱码:
response.setHeader('content-type','text/html;charset=utf-8');
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
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>
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>Documenttitle>
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>Documenttitle>
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">
const http = require('http');
const fs = require('fs');
const server = http.createServer((request, response) => {
if(request.method !== 'GET') {
response.statusCode = 405;
response.end(' 405 Method not allowed
');
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(' 404 Not Found
');
case 'EPERM':
response.statusCode = 403;
response.end(' 403 Forbidden
');
default:
response.statusCode = 500;
response.end('error');
}
}
response.end(data);
});
});
server.listen(9000, () => {
console.log('listening on 9000');
});
其中拆分出的每个文件就是一个模块 ,模块的内部数据是私有的,不过模块可以暴露内部数据以便其他模块使用
// 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"
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 是一个淘宝构建的 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
npm i -g yarn
nvm = node version manager
基于 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>Documenttitle>
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 服务:
使用 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>submitbutton>
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(' 404 not found
');
return;
}
}
next();
});
app.use(express.static(__dirname + '/public'));
app.all('*', (req, res) => {
res.end('404 not found');
});
app.listen(3000, () => {
console.log('listening...');
});
什么是 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;
模板引擎:分离用户界面和业务数据
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);
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
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>Documenttitle>
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>Documenttitle>
head>
<body>
<% if(isLogin) { %>
<span> 欢迎回来 span>
<% } else { %>
<button>登录button> <button>注册button>
<% } %>
body>
html>
// 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>Documenttitle>
head>
<body>
<%= title %>
body>
html>
用于快速创建一个应用的骨架
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;
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Portraittitle>
head>
<body>
<h1>Portraith1>
<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');
});
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>Documenttitle>
<link
href="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-05div>
<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">
<span
class="glyphicon glyphicon-remove"
aria-hidden="true"
>span>
div>
div>
div>
<div class="panel panel-success">
<div class="panel-heading">2023-04-15div>
<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">
<span
class="glyphicon glyphicon-remove"
aria-hidden="true"
>span>
div>
div>
div>
div>
div>
div>
div>
body>
html>
表单页面:
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="/css/bootstrap.css"
rel="stylesheet"
/>
<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>
<input
name="title"
type="text"
class="form-control"
id="item"
/>
div>
<div class="form-group">
<label for="time">发生时间label>
<input
name="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>
<input
name="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>
<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>
npm i [email protected]
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'});
});
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"
/>
<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>
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>Documenttitle>
<link
href="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">
<span
class="glyphicon glyphicon-remove"
aria-hidden="true"
>span>
div>
div>
div>
<% }) %>
div>
div>
div>
div>
body>
html>
前端代码修改:
<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 是一个基于分布式文件存储的数据库,跟 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 是一个对象文档模型库,方便使用代码操作 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: Number
is_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.0
return;
}
// 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;
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>Documenttitle>
<link
href="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 %>">
<span
class="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>
一个接口就是 服务中的一个路由规则
,根据请求响应结果。一个接口一般由如下几个部分组成:
接口示例
接口化有利于前后端分离
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
获取所有歌曲:
新增一首歌曲:
删除 id 为 4 的歌曲:
修改 id 为 3 的歌曲:
// api/account.js
var 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 是 HTTP 服务器发送到用户浏览器并保存在本地的一小块数据
浏览器向服务器发送请求时,会自动将 当前域名下
可用的 cookie 设置在请求头中,然后传递给服务器
这个请求头的名字也叫 cookie ,所以将 cookie 理解为一个 HTTP 的请求头也是可以的
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);
app.get('/remove-cookie', (req, res) => {
res.clearCookie('theme');
res.send('删除成功');
});
用 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 是保存在服务器端的一块儿数据 ,保存当前访问用户的相关信息
作用:实现会话控制,可以识别用户的身份,快速获取当前用户的相关信息
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.sid
secret: 'atguigu', //参与加密的字符串(又称签名)
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session
store: 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=admin
if(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.username
res.send('购物车');
} else {
res.send('请登录');
}
});
app.get('/logout', (req, res) => {
req.session.destroy(() => {
res.send('退出成功');
});
});
app.listen(3000);
// 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;
}
//判断 data
if(!data){
return res.send('账号或密码错误~~');
}
//写入session
req.session.username = data.username;
req.session._id = data._id;
//登录成功响应
res.render('success', {msg: '登录成功', url: '/account'});
});
});
//退出登录
router.post('/logout', (req, res) => {
//销毁 session
req.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.sid
secret: 'atguigu', //参与加密的字符串(又称签名) 加盐
saveUninitialized: false, //是否为每次请求都设置一个cookie用来存储session的id
resave: true, //是否在每次请求时重新保存session 20 分钟 4:00 4:20
store: 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>
<link
href="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 %>">
<span
class="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">
// 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 是服务端生成并返回给 HTTP 客户端的一串加密字符串, token 中保存着用户信息
token 的作用:实现会话控制,可以识别用户的身份,主要用于移动端 APP
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 部分:
// 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;
}
//判断 data
if(!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) => {
//销毁 session
req.session.destroy(() => {
res.render('success', {msg: '退出成功', url: '/login'});
})
});
module.exports = router;
// app.js
const authApiRouter = require('./routes/api/auth');
app.use('/api', authApiRouter);
// 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;