NodeJS 入门笔记

文档地址

课程地址

源码 提取码:963h

hello wrold

console.log('hello, world');
node hello.js

nodejs 中不能使用 DOM(document) 和 BOM(window) 的 API:

  • document
  • window
  • history
  • navigator
  • location

NodeJS 入门笔记_第1张图片

但是下面的 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 => {		// 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);

appendFile

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 方法也可以

文件夹操作

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');

NodeJS 入门笔记_第2张图片

绝对路径:

__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

NodeJS 入门笔记_第3张图片

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 协议

请求行

NodeJS 入门笔记_第4张图片

请求方法

NodeJS 入门笔记_第5张图片

url

在这里插入图片描述

协议版本号

请求头

一系列键值对

请求体

请求体的内容格式是非常灵活的,可以设置任意内容

NodeJS 入门笔记_第6张图片

NodeJS 入门笔记_第7张图片

NodeJS 入门笔记_第8张图片

响应行

协议版本号

响应状态码

NodeJS 入门笔记_第9张图片

响应状态字符串

响应头

也是一系列键值对,可以自定义

响应体

响应体内容的类型是非常灵活的,常见的类型有 HTML、CSS、JS、图片、JSON

http

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');

发送 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:9000" method="post">
        <input type="text" name="username">
        <input type="text" name="password">
        <input type="submit" value="提交">
    form>
body>
html>

获取 HTTP 请求报文

NodeJS 入门笔记_第10张图片

获取请求体

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');
});

练习

NodeJS 入门笔记_第11张图片

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');
});

设置响应报文

NodeJS 入门笔记_第12张图片

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');
});

NodeJS 入门笔记_第13张图片

<link rel="stylesheet" href="/css/app.css">

MIME 类型

NodeJS 入门笔记_第14张图片

错误处理

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'); });

GET 与 POST 场景区别

NodeJS 入门笔记_第15张图片

NodeJS 入门笔记_第16张图片

模块化

其中拆分出的每个文件就是一个模块 ,模块的内部数据是私有的,不过模块可以暴露内部数据以便其他模块使用

// 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();

NodeJS 入门笔记_第17张图片

导入文件夹

// package.json
{
    "main": "./app.js"
}
// app.js
module.exports = "this is module1";
// index.js
const m1 = require('./module1');       // 不能省略 ./
console.log(m1);		// "this is module1"

导入模块的流程

NodeJS 入门笔记_第18张图片

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"
}

NodeJS 入门笔记_第19张图片

搜索包

下载安装包

npm install uniq

NodeJS 入门笔记_第20张图片

使用包:

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	# 查看全局安装位置

安装所有依赖:

NodeJS 入门笔记_第21张图片

指定版本与删除包

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

NodeJS 入门笔记_第22张图片

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

NodeJS 入门笔记_第23张图片

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...');
});

路由

NodeJS 入门笔记_第24张图片

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') //响应文件内容
});

中间件

NodeJS 入门笔记_第25张图片

全局中间件

每一个请求到达服务端之后都会执行全局中间件函数

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...');
});

NodeJS 入门笔记_第26张图片

路由中间件

如果只需要对某一些路由进行功能封装 ,则就需要路由中间件

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'));

NodeJS 入门笔记_第27张图片

获取请求体数据

按照要求搭建 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>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 路由模块化

什么是 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);

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>

条件渲染

NodeJS 入门笔记_第28张图片

// 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>

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>Documenttitle>
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;

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>

lowdb

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

MongoDB 是一个基于分布式文件存储的数据库,跟 json 文件非常类似

NodeJS 入门笔记_第29张图片

  • 一个 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');
});

NodeJS 入门笔记_第30张图片

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();
});

条件控制

NodeJS 入门笔记_第31张图片

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>

API接口

一个接口就是 服务中的一个路由规则 ,根据请求响应结果。一个接口一般由如下几个部分组成:

  • 请求方法
  • 接口地址(URL)
  • 请求参数
  • 响应结果

接口示例

NodeJS 入门笔记_第32张图片

RESTful API

NodeJS 入门笔记_第33张图片

接口化有利于前后端分离

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

获取所有歌曲:

NodeJS 入门笔记_第34张图片

POST

新增一首歌曲:

NodeJS 入门笔记_第35张图片

DELETE

删除 id 为 4 的歌曲:

NodeJS 入门笔记_第36张图片

PATCH

修改 id 为 3 的歌曲:

NodeJS 入门笔记_第37张图片

记账本案例增加接口

// 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);
...

NodeJS 入门笔记_第38张图片

会话控制

所谓会话控制就是 对会话进行控制。HTTP 是一种 无状态 的协议,它没有办法区分多次的请求是否来自于同一个客户端, 无法区分用户,而产品中又大量存在的这样的需求,所以我们需要通过 会话控制 来解决该问题

常见的会话控制技术有三种:

  • cookie
  • session
  • token

cookie

cookie 是 HTTP 服务器发送到用户浏览器并保存在本地的一小块数据

  • cookie 是保存在浏览器端的一小块数据
  • cookie 是按照域名划分保存的

NodeJS 入门笔记_第39张图片

浏览器向服务器发送请求时,会自动将 当前域名下 可用的 cookie 设置在请求头中,然后传递给服务器

这个请求头的名字也叫 cookie ,所以将 cookie 理解为一个 HTTP 的请求头也是可以的

NodeJS 入门笔记_第40张图片
NodeJS 入门笔记_第41张图片

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 是保存在服务器端的一块儿数据 ,保存当前访问用户的相关信息

作用:实现会话控制,可以识别用户的身份,快速获取当前用户的相关信息

NodeJS 入门笔记_第42张图片

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);

cookie 与 session 的区别

NodeJS 入门笔记_第43张图片

记账本案例注册功能

// 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">

首页和 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

NodeJS 入门笔记_第44张图片
token 需要手动添加到请求报文中

NodeJS 入门笔记_第45张图片

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;
    }
    //判断 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);

用户登录,下放 token
NodeJS 入门笔记_第46张图片

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 放在头中发送请求:

NodeJS 入门笔记_第47张图片

// 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;

前后端介绍

NodeJS 入门笔记_第48张图片

你可能感兴趣的:(javascript,前端,node.js,后端,http)