一、初识 Nodejs
1.初识 Nodejs
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine
Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境
基于 Express 框架
- (opens new window),可以快速构建 Web 应用
- 基于 Electron 框架
- (opens new window),可以构建跨平台的桌面应用
- 基于 restify 框架
- (opens new window),可以快速构建 API 接口项目
- 读写和操作数据库、创建实用的命令行工具辅助前端开发、etc…
nodejs官网:https://nodejs.org/en/
2.初识Node-使用Node运行JS代码
node -v 查看node版本
在地址栏输入powershell可以弹出最新版的Windows终端
在终端中的命令
上箭头 可以快速定位到上一次执行的命令
tab自动补全地址
esc能够快速清空当前已输入的命令
cls命令,可以清空终端
3.fs文件系统模块
3.1.读取文件
# 简单文件读取
语法格式:
fs.readFile(path[, options], callback)
path:文件路径options:配置选项,若是字符串则指定编码格式
encoding:编码格式flag:打开方式callback:回调函数
err:错误信息data:读取的数据,如果未指定编码格式则返回一个 Buffer
//1.导入fs模块来操作文件
const fs = require('fs');
//2.调用fs.readFile() 方法读取文件
//参数1:读取文件的存放路径
//参数2:读取文件时候采用的编码格式,一般默认指定utf8
//参数3:回调函数,拿到读取失败和成功的结果 err dataStr
fs.readFile('./files/1.txt','utf8',function(err,dataStr){
if(err){ return console.log('读取文件失败',err.message);}
console.log('读取文件成功',dataStr);//读取文件成功 111
})
3.2.写入文件
案例:将一个文件的内容,复制到另一个文件中,并修改文件的内容
//1.导入fs模块
const fs = require('fs');
//2.调用 fs.readFile()读取文件的内容
fs.readFile('./files/成绩.txt','utf8',function(err,dataStr){
//3.判断是否读取成功
if(err){
return console.log(err.message);
}
//4.先把成绩的数据 按照空格进行分割
const arrOld = dataStr.split(' ');
//5.循环分割后 的数组,对每一项数据,进行字符串的替换操作
const arrNew = [];
arrOld.forEach(function(value){
arrNew.push(value.replace('=',':'));
})
//6.把新数组中的每一项,进行合并,得到一个新的字符串
const newStr = arrNew.join('\r\n');
console.log(newStr);
//7.调用fs.writeFile()方法,把处理完毕的成绩,写入到新文件中
fs.writeFile('./files/成绩-ok',newStr,function(err){
if(err){
return console.log(err.message);
}
console.log('成绩写入成功');
})
})
3.3.文件路径的问题
__dirname 表示当前文件所处的目录
出现路径拼接错误的问题,是因为提供了./或../开头的绝对路径
如果要解决这个问题,可以直接提供一个完整的文件存放路径就行
或者写__dirname(表示当前文件所处的目录)再拼接要读取的文件路径即可
const fs = require('fs');
//出现路径拼接错误的问题,是因为提供了./或../开头的绝对路径
//如果要解决这个问题,可以直接提供一个完整的文件存放路径就行
// fs.readFile('./files/1.txt','utf8',function(err,dataStr){
// if(err){
// return console.log('读取文件失败'+err.message);
// }
// console.log('读取文件成功'+dataStr);
// })
//移植性非常差,不利于维护
fs.readFile(__dirname+'/files/1.txt','utf8',function(err,dataStr){
if(err){
return console.log('读取文件失败'+err.message);
}
console.log('读取文件成功'+dataStr);
})
//__dirname 表示当前文件所处的目录
// console.log(__dirname);
其它操作
验证路径是否存在:
fs.exists(path, callback)fs.existsSync(path)获取文件信息:
fs.stat(path, callback)fs.stat(path)删除文件:
fs.unlink(path, callback)fs.unlinkSync(path)列出文件:
fs.readdir(path[,options], callback)fs.readdirSync(path[, options])截断文件:
fs.truncate(path, len, callback)fs.truncateSync(path, len)建立目录:
fs.mkdir(path[, mode], callback)fs.mkdirSync(path[, mode])删除目录:
fs.rmdir(path, callback)fs.rmdirSync(path)重命名文件和目录:
fs.rename(oldPath, newPath, callback)fs.renameSync(oldPath, newPath)监视文件更改:
fs.watchFile(filename[, options], listener)
4.path路径模块
path 模块是 Node.js 官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求。
4.1 路径拼接 path.join()
const path = require('path');
const fs = require('fs');
// //注意 ../会抵消前面的路径
// const pathStr= path.join('/a','/b/c','../../','./d','e');
// console.log(pathStr);// \a\d\e
fs.readFile(path.join(__dirname,'/files/2.txt'),'utf8',function(err,dataStr){
if(err)
{
return console.log('失败咯',err);
}
console.log('成功',dataStr);
})
4.2获取路径中文件名 path.basename()
使用
path.basename()方法,可以获取路径中的最后一部分,常通过该方法获取路径中的文件名
path.basename(path[, ext])
- path: 文件路径
- ext: 文件扩展名
const path = require('path');
//定义文件存放路径
const fpath = 'a/b/c/index.html';
const fullName = path.basename(fpath);
console.log(fullName);//index.html
//获取除了文件后缀的其他部分
const nameWithoutExt = path.basename(fpath,'.html');
console.log(nameWithoutExt);//index
4.3 获取路径中文件扩展名 path.extname()
const path = require('path');
//这是文件的存放路径
const fpath = '/a/b/c/index.html';
//获取路径中扩展名部分
const text = path.extname(fpath);
console.log(text);//.html
案例;时钟案例
//1.导入fs模块
const fs = require('fs');
//2.导入path模块
const path = require('path');
//正则表达式 分别匹配<style></style> 和 <script></script>标签
const regStyle = /<style>[\s\S]*<\/style>/
const regScript = /<script>[\s\S]*<\/script>/
fs.readFile(path.join(__dirname,'./素材/index.html'),'utf8',function(err,dataStr){
if(err){
return console.log('读取HTML文件失败'+err.message);
}
//读取文件成功后,调用对应的三个方法,分别拆解出css,js,html文件
resolveCss(dataStr);
resolveJs(dataStr);
resolveHTML(dataStr);
})
//处理css样式
function resolveCss(htmlStr){
//使用正则提取页面中的<style></style>标签
const r1 = regStyle.exec(htmlStr);
const newCss = r1[0].replace('<style>','').replace('</style>','');
//调用fs。writeFile()方法,将提取的样式,写入到clock目录中index.css文件里面
fs.writeFile(path.join(__dirname,'./clock/index.css'),newCss,err=>{
if(err){return console.log('写入CSS样式失败',err.message);}
console.log('写入CSS样式成功');
})
}
//处理JS脚本
function resolveJs(htmlStr){
//使用正则提取页面中的<script></script>标签
const r2 = regScript.exec(htmlStr);
//将提取出来的内容做进一步的处理
const newJs = r2[0].replace('<script>','').replace('</script>','');
fs.writeFile(path.join(__dirname,'/clock/index.js'),newJs,err=>{
if(err){return console.log('写入CSS脚本失败',err.message);}
console.log('写入CSS脚本成功');
})
}
//处理html文件
function resolveHTML(htmlStr){
const newHTML = htmlStr.replace(regStyle,'<link rel="stylesheet" href="./index.css" />')
.replace(regScript,'<script src="./index.js"></script>');
fs.writeFile(path.join(__dirname,'/clock/index.html'),newHTML,err=>{
if(err){
return console.log('输出html失败',err.message);
}
console.log('输出HTML成功');
})
}
5.http 模块
5.1.创建基本 Web 服务器
ctrl+c停止终端
req.url是客户端请求的URL地址
req.method是客户端请求的method类型
res.end()是向客户端发送指定的内容,并结束这次请求的处理过程
res.setHeader('Content-Type','text/html; charset=utf-8'):设置 Content-Type 响应头,解决中文乱码的问题
//1.导入http模块
const http = require('http');
//2.创建web服务器实例
const server = http.createServer();
//3.为服务器实例绑定request事件,监听客户端的请求
//req是请求对象,包含了与客户端相关的数据和属性
server.on('request',(req,res)=>{
//req.url是客户端请求的URL地址
const url = req.url;
//req.method是客户端请求的method类型
const method = req.method;
const str = `你访问的Url地址为${url},请求的method类型是${method}`;
// 设置 Content-Type 响应头,解决中文乱码的问题
res.setHeader('Content-Type','text/html; charset=utf-8')
//res.end()是向客户端发送指定的内容,并结束这次请求的处理过程
res.end(str);
})
//4.启动服务器
server.listen('80',function(){
console.log('server running at http://127.0.0.1');
})
5.2.根据不同的url响应不同的html内容
const http = require('http');
const server = http.createServer();
server.on('request',(req,res)=>{
//1.获取请求的url地址
const url = req.url;
//2.设置默认的响应内容为404 Not found
let content = '<h1>404 Not found!</h1>';
//3.判断用户请求是否为/或index.html首页
if(url ==='/' || url ==="/index.html")
{
content = '<h1>首页</h1>';
}
//4.判断用户请求的是否为/about.html关于页面
else if(url ==='/about.html')
{
content = '<h1>关于页面</h1>';
}
//5.设置Content-Type响应头,防止中文乱码
res.setHeader('Content-Type','text/html; charset=utf-8');
//6.使用res.end()把内容响应给客户端
res.end(content);
})
server.listen('81',function(){
console.log('server running at http://127.0.0.1:81');
})
案例 时钟web
//1.导入http模块
const http = require('http');
//2.导入fs模块
const fs = require('fs');
//3.导入path模块
const path = require('path');
//创建web服务器
const server = http.createServer();
//监听web服务器的request事件
server.on('request',(req,res)=>{
//获取到客户端请求的URL地址
const url = req.url;
//把请求的URL地址映射为具体文件的存放路径
// const fPath = path.join(__dirname,url);
let fpath = '';
if(url ==='/')
{
fpath = path.join(__dirname,'/clock/index.html');
}
else{
fpath = path.join(__dirname,'/clock',url);
}
//根据映射过来的文件路径读取文件
fs.readFile(fpath,'utf8',(err,dataStr)=>{
if(err) return res.end('404 No Found');
res.end(dataStr);
})
})
server.listen('80',()=>{
console.log('server running at http://127.0.0.1');
})
6.模块化
6.1.模块化概念
- 模块化是指解决一个复杂问题时,自顶向下逐层把系统划分为若干模块的过程,模块是可组合、分解和更换的单元。
- 模块化可提高代码的复用性和可维护性,实现按需加载。
- 模块化规范是对代码进行模块化拆分和组合时需要遵守的规则,如使用何种语法格式引用模块和向外暴露成员。
6.2.Node.js 中模块的分类
使用require(),加载模块
- 内置模块
- 自定义模块
- 第三方模块
在使用require加载用户自定义模块期间,可以省略.js的后缀名
const m1 = require('./15.m1');
6. 3.Node.js 中的模块作用域
- 和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域
- 防止全局变量污染
自定义模块.js
const username='张三';
function sayHello(username){
console.log(`我是${username}`);
}
使用自定义模块的js
const custom = require('./17.模块作用域');
console.log(custom);//{} 不能访问别的模块内容
6. 4.模块作用域的成员
- 自定义模块中都有一个
module对象,存储了和当前模块有关的信息- 在自定义模块中,可以使用
module.exports对象,将模块内的成员共享出去,供外界使用。导入自定义模块时,得到的就是module.exports指向的对象。- 默认情况下,
exports和module.exports指向同一个对象。最终共享的结果,以module.exports指向的对象为准。
console.log(module);

6.4.1module.exports对象
在一个自定义模块中,默认情况下,model.exports ={}
在外界使用require导入一个自定义模块的时候,得到的成员,就是那个模块中,通过module.export指向的那个对象
自定义模块.js
//在一个自定义模块中,默认情况下,model.exports ={}
const age = 20;
//向module.exports对象挂载username属性
module.exports.username = 'zs';
//向module.exports对象挂载sayHello方法
module.exports.sayHello = function(){
console.log('Hello!');
}
module.exports.age = age;
//{ username: 'zs', sayHello: [Function], age: 20 }
//让module.export 指向一个全新的对象,原先的对象被覆盖
module.exports = {
nickname:'小粉',
sayHi(){
console.log('Hi');
}
}
//{ nickname: '小粉', sayHi: [Function: sayHi] }
使用自定义模块的js
//在外界使用require导入一个自定义模块的时候,得到的成员,就是那个模块中,通过module.export指向的那个对象
const m = require('./19.自定义模块');
console.log(m);
6.4.2exports对象
exports对象.js
const username = 'lgx';
exports.username = username;
exports.age = 20;
exports.sayHi = function(){
console.log('一曲离歌笑江湖');
}
//最终 向外共享的对象,永远都是module.exports所指向的对象
使用自定义模块的js
const m = require('./20.exports对象');
console.log(m);//{ username: 'lgx', age: 20, sayHi: [Function] }
6.4.3module.exports与exports
默认情况下,exports 和 module.exports 指向同一个对象。最终共享的结果,以 module.exports 指向的对象为准。
console.log(exports);//{}
console.log(module.exports);//{}
console.log(exports === module.exports);//true
6.5CommonJS 模块化规范
- 每个模块内部,
module变量代表当前模块module变量是一个对象,module.exports是对外的接口- 加载某个模块即加载该模块的
module.exports属性,require()方法用于加载模块
7.包与npm-包
(1)搜索包共享平台:https://www.npmjs.com/
(2)下载包:https://registry.npmjs.org
终端中输入npm -v 查询npm包版本
7.1.格式化时间传统做法
//1.定义格式化时间的方法
function dateFormat(dtStr){
const dt = new Date();
const y = dt.getFullYear();
const m = padZero(dt.getMonth() +1);
const d = padZero(dt.getDate());
const hh = padZero(dt.getHours());
const mm = padZero(dt.getMinutes());
const ss = padZero(dt.getSeconds());
// return 'YYYY-MM-DD HH:mm:ss';
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
//定义补零的函数
function padZero(n){
return n>9?n:'0'+n;
}
module.exports ={
dateFormat
}
//导入自定义的格式化时间的模块
const TIME = require('./21.dateFormat');
//调用方法 进行日期格式化
const dt = new Date();
const newDT = TIME.dateFormat(dt)
console.log(newDT);//2022-03-13 21:41:14
安装包命令:npm install 包的完整名称 简写为 npm i 包的完整名称
核心依赖包,记录在dependencies节点中
7.2.引入第三方包后做法
//1.导入需要的包
//注意导入的名称,就是装包时候的名称
const moment = require('moment');
const dt = moment().format('YYYY-MM-DD HH:mm:ss');
console.log(dt);
npm i moment@2.29.1 (改变指定的版本号)
2(大版本).29(功能).1(Bug)
7.3.包管理配置文件
7.3.1.快速创建package.json
在项目根目录中创建一个叫做package.json的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除node_modules目录之后,在团队成员之间共享项目的源代码。
npm init -y
package.json中的dependencies属性:包含安装所有包及其版本
7.3.2.安装所有包
npm i / npm install
7.3.3.卸载包
npm uninstall
7.3.4.devDependencies节点
开发依赖包
只会在项目开发阶段用到,在项目上线之后不会用到,把这些包记录在devDependencies节点中
在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies节点中
npm i 包名 -D 简写为 npm install 包名 --save-dev
7.3.5解决下包速度慢的问题
切换成淘宝镜像
查看当前的下包镜像源
npm config get registry
将下包的镜像源切换为淘宝镜像源
npm config set registry=http://registry.npm.taobao.org/
检查镜像源是否下载成功
npm config get registry
7.3.6nrm
切换包的镜像源
通过npm包管理器,将nrm安装为全局可用的工具
npm i nrm -g
查看所有可用的镜像源
nrm ls
将下包的镜像源切换为taobao镜像
nrm use taobao
卸载全局安装的包
npm uninstall 包名 -g
7.3.7i5ting_toc 把md文档转为html页面的小工具
将i5ting-toc 安装为全局包
npm install -g i5ting_toc
调用i5ting_toc,轻松实现md转html的功能
i5ting_toc -f 要转换的md文件路径 -o
7.3.8开发属于自己的包
7.3.8.1开发包
{
"name":"itheima-tools-lgx",
"version": "1.0.0",
"main":"index.js",
"description": "提供了格式化时间、HTML Escape相关的功能-离歌笑",
"keywords": [
"itheima",
"dateFoemat",
"escape"
],
"license":"ISC"
}
1.新建 itheima-tools-lgx(自定义名称),作为包的根目录
2.在包根目录下新建三个文件
package.json (包管理文件)
index.js (包的入口文件)
README.md (包的说明文档)
3.在包共享平台上检测包是否重复
package.json
{
"name":"itheima-tools-lgx",
"version": "1.0.0",
"main":"index.js",
"description": "提供了格式化时间、HTML Escape相关的功能-离歌笑",
"keywords": [
"itheima",
"dateFoemat",
"escape"
],
"license":"ISC"
}
index.js
//这是包的入口文件
const date = require('./src/dateFormat');
const escape = require('./src/htmlEscape');
//向外暴露需要的成员
module.exports = {
...date,
...escape
}
./src/dateFormat.js
//格式化时间的方法
function dateFormat(dataStr){
const dt = new Date();
const y = padZero(dt.getFullYear());
const m = padZero(dt.getMonth()+1);
const d = padZero(dt.getDate());
const hh = padZero(dt.getHours());
const mm = padZero(dt.getMinutes());
const ss = padZero(dt.getSeconds());
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
//补零的方法
function padZero(n)
{
return n > 9 ? n: '0'+n;
}
module.exports = {
dateFormat
}
./src/htmlEscape.js
//定义转义HTM字符的函数
function htmlEscape(htmlStr){
return htmlStr.replace(/<|>|"|&/g,(match)=>{
switch(match){
case '<':
return '<';
case '>':
return '>';
case '"':
return '"';
case '&':
return '&';
}
})
}
//定义还原HTML字符串的函数
function htmlUnEscape(str){
return str.replace(/<|>|"|&/g,(match)=>{
switch(match){
case '<':
return '<';
case '>':
return '>';
case '"':
return '"';
case '&':
return '&';
}
})
}
module.exports = {
htmlEscape,
htmlUnEscape
}
README.md
## 安装
```
npm install itheima-tools-lgx
```
## 导入
```js
const itheima = require('itheima-tools-lgx')
```
## 格式化时间
```js
// 调用 dateFormat 对时间进行格式化
const dtStr = itheima.dateFormat(new Date())
// 结果 2020-04-03 17:20:58
console.log(dtStr)
```
## 转义 HTML 中的特殊字符
```js
// 带转换的 HTML 字符串
const htmlStr = '<h1 title="abc">这是h1标签<span>123 </span></h1>'
// 调用 htmlEscape 方法进行转换
const str = itheima.htmlEscape(htmlStr)
// 转换的结果 <h1 title="abc">这是h1标签<span>123&nbsp;</span></h1>
console.log(str)
```
## 还原 HTML 中的特殊字符
```js
// 待还原的 HTML 字符串
const str1 = itheima.htmlUnEscape(str)
// 输出的结果 <h1 title="abc">这是h1标签<span>123 </span></h1>
console.log(str1)
```
## 开源协议
ISC
7.3.8.2发布包
1.npm login 先登录,然后根据要求输入账号密码 lgx-lxy
2. 切换到要发布的包根目录下输入npm publish 发布包
7.3.8.3删除已发布的包
npm unpublish 包名 --force (删除72小时以内发布的包,在24小时内不允许重复发布)
8.模块加载机制
模块第一次加载后会被缓存,即多次调用
require()不会导致模块的代码被执行多次,提高模块加载效率。
8.1内置模块加载
内置模块加载优先级最高。
8.2自定义模块加载
加载自定义模块时,路径要以 ./ 或 ../ 开头,否则会作为内置模块或第三方模块加载。
导入自定义模块时,若省略文件扩展名,则 Node.js 会按顺序尝试加载文件:
- 按确切的文件名加载
- 补全
.js扩展名加载 - 补全
.json扩展名加载 - 补全
.node扩展名加载 - 报错
8.3 第三方模块加载
- 若导入第三方模块, Node.js 会从当前模块的父目录开始,尝试从
/node_modules文件夹中加载第三方模块。 - 如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在 C:\Users\bruce\project\foo.js 文件里调用了 require('tools'),则 Node.js 会按以下顺序查找:
C:\Users\bruce\project\node_modules\toolsC:\Users\bruce\node_modules\toolsC:\Users\node_modules\toolsC:\node_modules\tools
8.4目录作为模块加载
当把目录作为模块标识符进行加载的时候,有三种加载方式:
- 在被加载的目录下查找
package.json的文件,并寻找main属性,作为require()加载的入口 - 如果没有
package.json文件,或者main入口不存在或无法解析,则 Node.js 将会试图加载目录下的index.js文件。 - 若失败则报错
二、Express
基于 Node.js 平台,快速、开放、极简的 Web 开发框架
Express 是用于快速创建服务器的第三方模块。
1.Express 初体验
1.1基本使用
安装 Express:
npm install express
app.get() app.post()
监听客户端 GET和POST 请求,并向客户端响应具体的内容
res.send()
调用express提供的res.send()方法,向客户端响应一个JSON对象
req.query
通过req.query 可以获取到客户端发送过来的 查询参数 ?a=1&b=2 ,默认是一个空对象
http://127.0.0.1?a=1&b=2
req.params
/user/:id (冒号后面的名称,如 id 是可以改的)
req.params 是动态匹配到的URL参数,默认也是一个空对象
http://127.0.0.1/user/387
结果:{"id": "387"}
创建服务器,监听客户端请求,并返回内容:
//1.导入express
const express = require('express');
//2.创建web服务器
const app = express();
//4.监听客户端 GET和POST 请求,并向客户端响应具体的内容
app.get('/user',(req,res)=>{
//调用express提供的res.send()方法,向客户端响应一个JSON对象
res.send({name:'lgx',age:'20',gender:'boy'});
})
app.post('/user',(req,res)=>{
res.send('请求成功');
})
app.get('/',(req,res)=>{
//通过req.query 可以获取到客户端发送过来的 查询参数 ?a=1&b=2
//注意:默认情况下,req.query是一个空对象 {}
console.log(req.query);
res.send(req.query);
})
//注意:这里的:id是一个动态参数
app.get('/user/:lgx/:name',(req,res)=>{
//req.params 是动态匹配到的URL参数,默认也是一个空对象
console.log(req.params);
res.send(req.params);
})
//http://127.0.0.1/user/387
//结果:{"id": "387"}
//3.启动web服务器
app.listen(80,()=>{
console.log('express server running at http://127.0.0.1');
})
1.2托管静态资源
- 通过
express.static()方法可创建静态资源服务器,向外开放访问静态资源。- Express 在指定的静态目录中查找文件,并对外提供资源的访问路径,存放静态文件的目录名不会出现在 URL 中
- 访问静态资源时,会根据托管顺序查找文件
- 可为静态资源访问路径添加前缀
访问静态资源时,会根据托管顺序查找文件
app.use(express.static('./clock'));
app.use(express.static('./files'));
可为静态资源访问路径添加前缀
app.use('/files',express.static('./files'));
完整代码
const express = require('express');
const app = express();
//在这里,调用express.static()方法,快速的对外提供静态资源
app.use('/files',express.static('./files'));
app.use(express.static('./clock'));
app.listen(80,(req,res)=>{
console.log('express server running at http://127.0.0.1');
})
/*
可直接访问 public, files 目录下的静态资源
http://localhost:3000/index.html
http://localhost:3000/index.css
http://localhost:3000/index.js
通过带有 /files前缀的地址访问 files 目录下的文件
http://localhost:8080/files/index.html
*/
安装nodemon能够监听文件的变动,自动重启项目
npm install -g nodemon
2.Express 路由
2.1创建路由模块
const express = require('express');
const app = express();
//挂载路由
app.get('/',(req,res)=>{
res.send('Hello World!');
})
app.post('/user',(req,res)=>{
res.send('Hello POST');
})
app.listen(80,(req,res)=>{
console.log('express server running at http://127.0.0.1');
})
2.2注册路由模块
模块化路由.js
const express = require('express');
const app = express();
//1.导入路由模块
const router = require('./03.router');
//2.注册路由模块
// app.use(router);
//为路由模块添加前缀
app.use('/api',router);
//注意 app.use()的作用,就是来注册全局中间件
app.listen(80,()=>{
console.log('http:/127.0.0.1');
})
router.js
//这是路由模块
//1.导入Express
const express = require('express');
//2.创建路由对象
const router = express.Router();
//3.挂载具体的路由
router.get('/user/list',(req,res)=>{
res.send('GET USER LIST');
})
router.post('/user/add',(req,res)=>{
res.send('GET USER ADD');
})
//4.向外导出路由对象
module.exports = router;
3.Express 中间件
- 中间件是指流程的中间处理环节
- 服务器收到请求后,可先调用中间件进行预处理
- 中间件是一个函数,包含
req, res, next三个参数,next()参数把流转关系交给下一个中间件或路由
中间件注意事项;
- 在注册路由之前注册中间件(错误级别中间件除外)
- 中间件可连续调用多个
- 别忘记调用
next()函数next()函数后别写代码- 多个中间件共享
req、res对象
opp.get('/',function(req,res,next){})
带有next()参数的才是中间件,不带的是路由
next函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由
3.1最简单的中间件函数
const express = require('express');
const app = express();
//定义一个最简单的中间件函数
const nw = function(req,res,next){
console.log('这是最简单的中间件函数');
//把流转关系,转交给下一个中间件或路由
next();
}
app.listen(80,(req,res)=>{
console.log('http://127.0.0.1');
})
3.2.全局中间件
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件
通过
app.use()定义的中间件为全局中间件
const express = require('express');
const app = express();
//定义一个最简单的中间件函数
// const nw = function(req,res,next){
// console.log('这是最简单的中间件函数');
// //把流转关系,转交给下一个中间件或路由
// next();
// }
// //将nw注册为全局生效的中间件
// app.use(nw);
// 全局中间件简写
app.use(function(req,res,next){
console.log('这是最简单的中间件函数2');
next();
});
app.get('/',(req,res)=>{
console.log('调用了/这个路由');
res.send("Home Page.");
})
app.get('/user',(req,res)=>{
console.log('调用了/user这个路由');
res.send("Home Page~");
})
app.listen(80,(req,res)=>{
console.log('http://127.0.0.1');
})
全局中间件简写
app.use(function(req,res,next){
console.log('这是最简单的中间件函数2');
next();
});
3.2.1中间件的作用
多个中间件之间, 共享同一份 req 和 res 。基于这样的特性,我们可以在 上游 的中间件中, 统一 为 req 或 res 对象添 加 自定义 的 属性 或 方法 ,供 下游 的中间件或路由进行使用。
const express = require('express');
const app = express();
// 全局中间件简写
app.use(function(req,res,next){
//获取请求到达服务器的时间
const time = Date.now();
//为req对象,挂载自定义属性,从而把时间共享给后面所有路由
req.startTime = time;
next();
});
app.get('/',(req,res)=>{
res.send("Home Page."+req.startTime);
})
app.get('/user',(req,res)=>{
res.send("Home Page~"+req.startTime);
})
app.listen(80,(req,res)=>{
console.log('http://127.0.0.1');
})
3.2.2定义多个全局中间件
const express = require('express');
const app = express();
//定义第一个中间件函数
app.use(function(req,res,next){
console.log('全局中间件1');
next();
})
//定义第二个中间件函数
app.use(function(req,res,next){
console.log('全局中间件2');
next();
})
//定义一个路由
app.get('/lgx',(req,res)=>{
console.log('一曲离歌笑江湖');
res.send('一曲离歌笑江湖');
})
app.listen(80,()=>{
console.log('http://127.0.0.1');
})
3.3局部中间件
3.3.1局部中间件
//导入express模块
const express = require('express');
//创建express的服务器实例
const app = express();
//1.定义中间件函数
const nw1 = function(req,res,next){
console.log('调用了局部生效的中间件');
next();
}
//2.创建路由
app.get('/',nw1,(req,res)=>{
res.send('一曲离歌笑江湖1');
})
app.get('/lgx',(req,res)=>{
res.send('一曲离歌笑江湖2');
})
app.listen(80,()=>{
console.log('http://127.0.0.1');
})
3.3.2定义多个局部中间件
app.get('/',nw1,nw2,(req,res)=>{
res.send('一曲离歌笑江湖1');
})
//或者
app.get('/',[nw1,nw2],(req,res)=>{
res.send('一曲离歌笑江湖1');
})
3.4了解中间件的5个使用注意事项
一定要在路由之前注册中间件客户端发送过来的请求,可以连续调用多个中间件进行处理执行完中间件的业务代码之后,不要忘记调用 next() 函数为了防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象
3.5 中间件分类
3.5.1.应用级别的中间件
- 通过
app.use()或app.get()或app.post(),绑定到app实例上的中间件- express()
3.5.2.路由级别的中间件
- 绑定到
express.Router()实例上的中间件,叫做路由级别的中间件。用法和应用级别中间件没有区别。应用级别中间件是绑定到app实例上,路由级别中间件绑定到router实例上。(全局,局部中间件都可以设置)
const express = require('express');
const app = express();
const router = express.Router();
//路由级别的中间件
// router.use(function(req,res,next){
// console.log('TIME:'+Date.now());
// next();
// })
const nw = function(req,res,next){
console.log('TIME666:'+Date.now());
next();
}
app.use('/',router);
app.get('/',nw,(req,res)=>{
res.send('一曲离歌笑江湖');
})
app.listen(80,()=>{
console.log('http://127.0.0.1');
})
3.5.3.错误级别的中间件
- 用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
- 错误级别中间件的处理函数中,必须有 4 个形参,形参顺序从前到后分别是
(err, req, res, next)。- 错误级别的中间件必须注册在所有路由之后
const express = require('express');
const app = express();
//1.定义路由
app.get('/',(req,res)=>{
//人为的制造错误
throw new Error('服务器内部发送错误');
res.send('一曲离歌笑江湖');
})
//2.定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use(function(err,req,res,next){
console.log('发生了错误!'+err.message);
res.send('Error:'+err.message);
})
app.listen(80,()=>{
console.log('http://127.0.0.1');
})
3.5.4Express 内置中间件
自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:
express.static快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)express.json解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)express.urlencoded解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
1.通过express.json()这个中间件,解析表单中的JSON格式的数据
app.use(express.json());
2.通过express.urlencodeed()这个中间件,来解析表单中url-encodeed格式的数据
app.use(express.urlencoded({extended:false}));
3.在服务器 可以使用req.body这个属性,来获取json格式的表单数据和url-encodeed格式的数据
console.log(req.body)
完整代码
const express = require('express');
const app = express();
//注意:除了错误级别的中间件,其他的中间件,必须在路由之前进行配置
//通过express.json()这个中间件,解析表单中的JSON格式的数据
app.use(express.json());
//通过express.urlencodeed()这个中间件,来解析表单中url-encodeed格式的数据
app.use(express.urlencoded({extended:false}));
app.post('/user',(req,res)=>{
//在服务器 可以使用req.body这个属性,来接收客户端发送过来的请求体数据
//默认情况下,如果不配置解析表单数据的中间件,则req.body 默认等于 undefined
console.log(req.body);
res.send('ok');
})
app.post('/book',(req,res)=>{
//在服务器 可以使用req.body这个属性,来获取json格式的表单数据和url-encodeed格式的数据
console.log(req.body)
res.send('ok2');
})
app.listen(80,()=>{
console.log("Express server running at http://127.0.0.1");
})
3.5.5第三方中间件
1.运行npm install bod-parser安装中间件
2.使用require导入中间件
3.调用app.use()注册并使用中间件
//1.导入解析表单数据的中间件 body-parser
const parser = require('body-parser');
//2.使用app.use() 注册中间件
app.use(parser.urlencoded({extended:false}));
完整代码
const express = require('express');
const app = express();
//1.导入解析表单数据的中间件 body-parser
const parser = require('body-parser');
//2.使用app.use() 注册中间件
app.use(parser.urlencoded({extended:false}));
app.post('/lgx',(req,res)=>{
//如果没有配置任何解析表单数据的中间件,则req.body默认等于undefined
console.log(req.body);
res.send('ook');
})
app.listen(80,()=>{
console.log("Express server running at http://127.0.0.1");
})
3.6自定义中间件
3.6.1监听 req 的 data 事件
在中间件中,需要监听 req 对象的 data 事件,来获取客户端发送到服务器的数据。如果数据量比较大,无法一次性发送完毕,则客户端会 把数据切割后 , 分批发送到服务器 。所以 data 事件可能会触发多次,每一次触发 data 事件时, 获取到数据只是完整数据的一部分 ,需要手动对接收到的数据进行拼接。
req.on('data',(chunk)=>{str += chunk;})
3.6.2监听 req 的 end 事件
当请求体数据 接收完毕 之后,会自动触发 req 的 end 事件。因此,我们可以在 req 的 end 事件中, 拿到并处理完整的请求体数据。
req.on('end',()=>{console.log(str);})
3.6.3使用 querystring 模块解析请求体数据
Node.js 内置了一个 querystring 模块,专门用来处理查询字符串。通过这个模块提供的 parse() 函数,可以轻松把查询字符串,解析成对象的格式。
//导入Node.js内置的querystring模块
const qs = require('querystring');
//调用qs.parse()方法,把查询字符串解析为对象
console.log(qs.parse(str));
3.6.4将解析出来的数据对象挂载为 req.body
上游 的 中间件 和 下游 的 中间件及路由 之间, 共享同一份 req 和 res 。因此,我们可以将解析出来的数据,挂载为 req的自定义属性,命名为 req.body ,供下游使用。
3.6.5将自定义中间件封装为模块
为了优化代码的结构,我们可以把自定义的中间件函数,封装为独立的模块
完整代码
custom-body-parser.js(自己封装的中间件模块)
//导入Node.js内置的querystring模块
const qs = require('querystring');
const bodyParser = function (req,res,next) {
//定义中间件具体的业务逻辑
//1.定义一个str字符串,专门用来存储客户端发送过来的请求体数据
let str='';
//2.监听req的data事件
req.on('data',(chunk)=>{
str += chunk;
})
//3.监听req的end事件
req.on('end',()=>{
//在str中存放的是完整的请求体数据
// console.log(str);
//1000:把字符串格式的请求体数据,解析成对象格式
//调用qs.parse()方法,把查询字符串解析为对象
const body = qs.parse(str);
req.body = body;
next();
})
};
module.exports = bodyParser;
对自定义的中间件进行模块化的拆分.js
//导入express模块
const express = require("express");
//创建express的服务器实例
const app = express();
//1.导入自己封装的中间件模块
const customBodyParser = require('./14.custom-body-parser');
//2.将自定义的中间件函数注册为全局可用的中间件
app.use(customBodyParser);
app.post('/lgx',(req,res)=>{
res.send(req.body);
})
app.listen(80,()=>{
console.log('http://127.0.0.1');
})
4.使用Express写接口
4.1cors 中间件解决跨域
- 安装中间件:
npm install cors- 导入中间件:
const cors = require('cors')- 配置中间件:
app.use(cors)
4.2.CORS跨域资源共享
- CORS(Cross-Origin Resource Sharing,跨域资源共享)解决跨域,是通过 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源
- 浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可解除浏览器端的跨域访问限制
- CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
- CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
4.3CORS 常见响应头
4.3.1Access-Control-Allow-Origin:制定了允许访问资源的外域 URL
res.setHeader('Access-Control-Allow-Origin', 'http://lgx.com')
res.setHeader('Access-Control-Allow-Origin', '*')
4.3.2Access-Control-Allow-Headers
默认情况下,CORS 仅支持客户端向服务器发送如下的 9 个请求头:Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
4.3.3Access-Control-Allow-Methods
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过
Access-Control-Alow-Methods来指明实际请求所允许使用的 HTTP 方法
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
res.setHEader('Access-Control-Allow-Methods', '*')
4.4CORS 请求分类
4.4.1简单请求
- 请求方式:GET、POST、HEAD 三者之一
- HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值 application/x-www-formurlencoded、multipart/form-data、text/plain)
4.4.2预检请求
- 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
- 请求头中包含自定义头部字段
- 向服务器发送了 application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据
4.5JSONP接口
概念:浏览器端通过 <script> 标签的 src 属性,请求服务器上的数据,同时,服务器返回一个函数的调用。这种请求数据的方式叫做 JSONP。
特点:
JSONP 不属于真正的 Ajax 请求,因为它没有使用 XMLHttpRequest 这个对象。
JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求。如果项目中已经配置了 CORS 跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。否则 JSONP 接口会被处理成开启了 CORS 的接口。
完整案例
使用express写接口.js
//使用express
const express = require('express');
//创建服务器实例
const app = express();
//配置解析表单数据的中间件
app.use(express.urlencoded({extended:false}));
//必须在配置cors中间件之前,配置JSONP的接口
app.get('/api/jsonp',(req,res)=>{
//TODO:定义 JSON接口具体的实现过程
//1.得到函数的名称
const funName = req.query.callback;
//2.定义要发送到客户端的数据对象
const data = {name:'lgx',age:22};
//3.拼接出一个函数的调用
const scriptStr = `${funName}(${JSON.stringify(data)})`
//4.把拼接的字符串,响应给客户端
res.send(scriptStr);
})
//一定要在路由之前,配置cors这个中间件,从而解决接口跨域的问题
const cors = require('cors');
app.use(cors());
//导入路由模块
const router = require('./16.apiRouter');
//把路由模块,注册到app上
app.use('/api',router);
//启动服务器
app.listen(80,()=>{
console.log("http://127.0.0.1");
})
apiRouter.js
const express = require('express');
const router = express.Router();
//这里挂载对应的路由
//定义GET接口
router.get('/lgx',(req,res)=>{
//通过req.query获取客户端通过查询字符串,发送到服务器的数据
const query = req.query;
//调用res.send() 方法,向客户端响应处理的结果
res.send({
status :0,//0表示处理成功 1表示失败
msg:'GET请求成功!!',//状态的描述
data:query//需要响应给客户端的数据
});
})
//定义POST接口
router.post('/lxy',(req,res)=>{
// 通过 req.body 获取请求体中包含的 url-encoded 格式的数据
const body = req.body;
// 调用 res.send() 方法,向客户端响应结果
res.send({
status:0,//0表示处理成功 1表示失败
msg:"POST请求成功!!!",//状态的描述
data:body,//需要响应给客户端的数据
});
// res.setHeader('Access-Control-Allow-Headers','Content-Type','X-Custom-Header');
// res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD');
// Response.setHeader('Access-Control-Allow-Methods','*');
})
//定义Delete接口
router.delete('/delete',(req,res)=>{
res.send({
status:0,
msg:'DELETE请求成功'
})
})
module.exports = router;
测试接口跨域问题.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<title>17.测试接口跨域问题</title>
</head>
<body>
<button id="btnGet">GET</button>
<button id="btnPost">POST</button>
<button id="btnDelete">Delete</button>
<button id="btnJSONP">JSONP</button>
<script>
$(function(){
//1.测试GET接口
$("#btnGet").on('click',function(){
$.ajax({
type:'GET',
url:'http://127.0.0.1/api/lgx',
data:{name:'lgx',age:18},
success:function(res){
console.log(res);
console.log(res.msg);
}
})
})
//2.测试POST接口
$('#btnPost').on('click',function(){
$.ajax({
type:'POST',
url:'http://127.0.0.1/api/lxy',
data:{name:'lxy',age:'10'},
success:function(res){
console.log(res);
console.log(res.msg);
}
})
})
//3.为删除按钮绑定点击事件处理函数
$("#btnDelete").on('click',function(){
$.ajax({
type:'delete',
url:'http://127.0.0.1/api/delete',
success:function(res){
console.log(res);
}
})
})
//4.btnJSONP
$("#btnJSONP").click('on',function(){
$.ajax({
type:'GET',
url:'http://127.0.0.1/api/jsonp',
dataType:'jsonp',//表示要发起JSONP的请求
success:function(res){
console.log(res);
}
})
})
})
</script>
</body>
</html>
三、数据库和身份认证
1.Node 操作 mysql
1.1数据表查询
1.1.1数据表
DataType 数据类型:int 整数varchar ( len ) 字符串 tinyint ( 1 ) 布尔值
字段的特殊标识:PK (Primary Key) 主键、唯一标识NN (Not Null) 值不允许为空UQ (Unique) 值唯一AI (Auto Increment) 值自动增长
select* from users
select username,password from users
insert into users(username,password) values ('tony stark','098123')
update users set password = '888888' where id = 4
update users set password = 'admin123',status = 1 where id = 2
delete from users where id = 4
1.1.2where子句
select * from users where status = 1
select * from users where id > 2
select * from users where username <> 'lxy'
select * from users where username != 'lxy'
1.1.3And和Or运算符
select * from users where id <3 and status = 0
select * from users where status = 1 or username = 'lgx'
1.1.4order by 子句
(1)DESC降序,默认是升序(ASC)
select * from users order by status
select * from users order by status asc
select * from users order by id desc
(2) 多重排序
select * from users order by status desc, username desc
1.1.5COUNT(*)函数
select count(*) from users where status = 0
1.1.6使用AS关键字为列设置别名
select count(*) AS total from users where status = 0
select password as pw , username as uname from users
1.2配置 mysql 模块
(1)安装 mysql 模块
npm install mysql
(2)建立连接
//1.导入mysql模块
const mysql = require('mysql');
//2.建立与MYSQL数据库的连接关系
const db = mysql.createPool({
host:'127.0.0.1',//数据库的IP地址
user:'root',//登录数据库的账号
password:'admin123',//登录数据库的密码
database:'my_db_01'//指定要操作哪个数据库
})
(3)测试是否正常工作
//3.检测mysql 模块能否正常工作
db.query('SELECT 1',(err,results)=>{
//mysql模块工作期间报错了
if(err){return console.log(err.message);}
//只要打印出[ RowDataPacket { '1': 1 } ]的结果,就证明数据库连接正常
console.log(results);
})
1.3操作 mysql 数据库
(1)查询数据
//查询users表中所有数据
db.query('select * from users',(err,results)=>{
if(err){return console.log(err.message);}
//注意:如果执行的是select查询语句,则执行的结果是数组
console.log(results);
})
//[ RowDataPacket {id: 1,username: 'lgx',password: '123456',status: 0},
// RowDataPacket {id: 2,username: 'lxy',password: 'admin123',status: 1},]
(2)插入数据
//向users表插入数据
const user = {username:'cbd',password:'123ddd'};
//定义待执行的SQL语句
// ? 表示占位符
const sqlStr = 'insert into users (username,password) values (?,?)';
//执行SQL语句
db.query(sqlStr,[user.username,user.password],(err,results)=>{
if(err){return console.log(err.message);}
//注意:如果执行的是insert into插入语句,则result是一个对象
//可以通过affectedRows属性,来判断是否插入数据成功
if(results.affectedRows === 1)
{
console.log("插入数据成功");
}
})
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据:
const users = {username:'lqc',password:'123456'};
const sqlStr = 'insert into users set ?';
db.query(sqlStr,users,(err,results)=>{
if(err){return console.log(err.message);}
if(results.affectedRows === 1){console.log("插入数据成功");}
})
(3)更新数据
const users = {id:10,username:'bjq',password:'0624700'};
const sqlStr = 'update users set username = ? , password = ? where id = ?';
db.query(sqlStr,[users.username,users.password,users.id],(err,results)=>{
if(err){return console.log(err.message);}
//注意:执行了update语句之后,执行的结果,也是一个对象,可以通过 affectedRows,判断是否更新成功
if(results.affectedRows === 1)
{
console.log("更新数据成功");
}
})
快捷方式:
const users = {id:13,username:"bzt",password:'30624700'};
const sqlStr = 'update users set ? where id = ?';
db.query(sqlStr,[users,users.id],(err,results)=>{
if(err){return console.log(err.message);}
if(results.affectedRows===1){console.log("更新数据成功");}
})
(4)删除数据
//删除数据
const sqlStr = 'delete from users where id = ?';
db.query(sqlStr,12,(err,results)=>{
if(err){return console.log(err.message);}
//注意:执行delete语句之后,结果也是一个对象,也会包含affectedRows属性
if(results.affectedRows===1){console.log("删除成功");}
})
使用 delete 语句会真正删除数据,保险起见,使用标记删除的形式,模拟删除的动作。即在表中设置状态字段,标记当前的数据是否被删除。
//标记删除
const sqlStr = 'update users set status = ? where id = ?';
db.query(sqlStr,[1,5],(err,results)=>{
if(err){return console.log(err.message);}
if(results.affectedRows === 1){console.log("标记删除成功");}
})
2.Web 开发模式
2.1服务端渲染
服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接动态生成的。因此客户端不需要使用 Ajax 额外请求页面的数据。
app.get('/index.html', (req, res) => {
const user = { name: 'Bruce', age: 29 }
const html = `<h1>username:${user.name}, age:${user.age}</h1>`
res.send(html)
})
优点:
- 前端耗时短。浏览器只需直接渲染页面,无需额外请求数据。
- 有利于 SEO。服务器响应的是完整的 HTML 页面内容,有利于爬虫爬取信息。
缺点:
- 占用服务器资源。服务器需要完成页面内容的拼接,若请求比较多,会对服务器造成一定访问压力。
- 不利于前后端分离,开发效率低。
2.2 前后端分离
前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。后端只负责提供 API 接口,前端使用 Ajax 调用接口。
优点:
- 开发体验好。前端专业页面开发,后端专注接口开发。
- 用户体验好。页面局部刷新,无需重新请求页面。
- 减轻服务器的渲染压力。页面最终在浏览器里生成。
缺点:
- 不利于 SEO。完整的 HTML 页面在浏览器拼接完成,因此爬虫无法爬取页面的有效信息。Vue、React 等框架的 SSR(server side render)技术能解决 SEO 问题。
2.3如何选择
- 企业级网站,主要功能是展示,没有复杂交互,且需要良好的 SEO,可考虑服务端渲染
- 后台管理项目,交互性强,无需考虑 SEO,可使用前后端分离
- 为同时兼顾首页渲染速度和前后端分离开发效率,可采用首屏服务器端渲染+其他页面前后端分离的开发模式
3.身份认证
3.1Session 认证机制
服务端渲染推荐使用 Session 认证机制
3.1.1Session 工作原理
3.1.2Express 中使用 Session 认证
(1)安装 express-session 中间件
npm install express-session
(2)配置中间件
const session = require('express-session')
app.use(
session({
secret: 'Bruce', // secret 的值为任意字符串
resave: false,
saveUninitalized: true,
})
)
(3)向 session 中存数据
中间件配置成功后,可通过 req.session 访问 session 对象,存储用户信息
app.post('/api/login', (req, res) => {
req.session.user = req.body
req.session.isLogin = true
res.send({ status: 0, msg: 'login done' })
})
(4)从 session 取数据
app.get('/api/username', (req, res) => {
if (!req.session.isLogin) {
return res.send({ status: 1, msg: 'fail' })
}
res.send({ status: 0, msg: 'success', username: req.session.user.username })
})
(5)清空 session
app.post('/api/logout', (req, res) => {
// 清空当前客户端的session信息
req.session.destroy()
res.send({ status: 0, msg: 'logout done' })
})
完整代码:
app.js
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// TODO_01:请配置 Session 中间件
const session = require('express-session');
app.use(
session({
secret:'xjqxz',
resave:false,//固定格式
saveUninitialized:true//固定格式
})
)
// 托管静态页面
app.use(express.static('./pages'))
// 解析 POST 提交过来的表单数据
app.use(express.urlencoded({ extended: false }))
// 登录的 API 接口
app.post('/api/login', (req, res) => {
// 判断用户提交的登录信息是否正确
if (req.body.username !== 'admin' || req.body.password !== '000000') {
return res.send({ status: 1, msg: '登录失败' })
}
// TODO_02:请将登录成功后的用户信息,保存到 Session 中
//注意:只有成功配置了express-session这个中间件之后,才能通过req点出来session这个属性
req.session.user = req.body;//用户的信息
req.session.islogin = true;//用户的登录状态
res.send({ status: 0, msg: '登录成功' })
})
// 获取用户姓名的接口
app.get('/api/username', (req, res) => {
// TODO_03:请从 Session 中获 取用户的名称,响应给客户端
if(!req.session.islogin){return res.send({status:1,msg:'fail'})}
res.send({
status:0,
msg:'success',
username:req.session.user.username
});
})
// 退出登录的接口
app.post('/api/logout', (req, res) => {
// TODO_04:清空 Session 信息
req.session.destroy();
res.send({
status:0,
msg:'退出登录成功'
})
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(80, function () {
console.log('Express server running at http://127.0.0.1:80')
})
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./jquery.js"></script>
</head>
<body>
<h1>首页</h1>
<button id="btnLogout">退出登录</button>
<script>
$(function () {
// 页面加载完成后,自动发起请求,获取用户姓名
$.get('/api/username', function (res) {
// status 为 0 表示获取用户名称成功;否则表示获取用户名称失败!
if (res.status !== 0) {
alert('您尚未登录,请登录后再执行此操作!')
location.href = './login.html'
} else {
alert('欢迎您:' + res.username)
}
})
// 点击按钮退出登录
$('#btnLogout').on('click', function () {
// 发起 POST 请求,退出登录
$.post('/api/logout', function (res) {
if (res.status === 0) {
// 如果 status 为 0,则表示退出成功,重新跳转到登录页面
location.href = './login.html'
}
})
})
})
</script>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./jquery.js"></script>
</head>
<body>
<!-- 登录表单 -->
<form id="form1">
<div>账号:<input type="text" name="username" /></div>
<div>密码:<input type="password" name="password" /></div>
<button>登录</button>
</form>
<script>
$(function () {
// 监听表单的提交事件
$('#form1').on('submit', function (e) {
// 阻止默认提交行为
e.preventDefault()
// 发起 POST 登录请求
$.post('/api/login', $(this).serialize(), function (res) {
// status 为 0 表示登录成功;否则表示登录失败!
if (res.status === 0) {
location.href = './index.html'
} else {
alert('登录失败!')
}
})
})
})
</script>
</body>
</html>
3.2JWT 认证机制
前后端分离推荐使用 JWT(JSON Web Token)认证机制,是目前最流行的跨域认证解决方案
3.2.1JWT 工作原理
Session 认证的局限性:
- Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。
- 当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
- 当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制
3.2.2JWT 工作原理图:
用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。

JWT 组成部分:
- Header、Payload、Signature
- Payload 是真正的用户信息,加密后的字符串
- Header 和 Signature 是安全性相关部分,保证 Token 安全性
- 三者使用
.分隔
3.2.3JWT 组成部分
- Header、Payload、Signature
- Payload 是真正的用户信息,加密后的字符串
- Header 和 Signature 是安全性相关部分,保证 Token 安全性
- 三者使用
.分隔
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTcsInVzZXJuYW1lIjoiQnJ1Y2UiLCJwYXNzd29yZCI6IiIsIm5pY2tuYW1lIjoiaGVsbG8iLCJlbWFpbCI6InNjdXRAcXEuY29tIiwidXNlcl9waWMiOiIiLCJpYXQiOjE2NDE4NjU3MzEsImV4cCI6MTY0MTkwMTczMX0.bmqzAkNSZgD8IZxRGGyVlVwGl7EGMtWitvjGD-a5U5c
3.2.4JWT 使用方式:
- 客户端会把 JWT 存储在 localStorage 或 sessionStorage 中
- 此后客户端与服务端通信需要携带 JWT 进行身份认证,将 JWT 存在 HTTP 请求头 Authorization 字段中
- 加上 Bearer 前缀
Authorization: Bearer <token>
3.3 Express 使用 JWT
3.3.1安装
- jsonwebtoken 用于生成 JWT 字符串
- express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
npm install jsonwebtoken express-jwt
3.3.2定义 secret 密钥
- 为保证 JWT 字符串的安全性,防止其在网络传输过程中被破解,需定义用于加密和解密的 secret 密钥
- 生成 JWT 字符串时,使用密钥加密信息,得到加密好的 JWT 字符串
- 把 JWT 字符串解析还原成 JSON 对象时,使用密钥解密
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 密钥为任意字符串
const secretKey = 'lgxlxy = .='
3.3.3生成 JWT 字符串
调用 jwt.sign() 方法生成 JWT 字符串。
app.post('/api/login', (req, res) => {
...
res.send({
status: 200,
message: '登录成功',
// jwt.sign() 生成 JWT 字符串
//参数1:用户的信息对象
//参数2:加密的秘钥
//参数3:配置对象,可以配置当前token的有效期
// 尽量不保存敏感信息,因此只有用户名,没有密码
token: jwt.sign({username: userInfo.username}, secretKey, {expiresIn: '10h'})
//expiresIn: '10h' 有效时间30秒钟
})
})
3.3.4JWT 字符串还原为 JSON 对象
- 客户端访问有权限的接口时,需通过请求头的
Authorization字段,将 Token 字符串发送到服务器进行身份认证- 服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
// unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限
//注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到req.user属性上
app.use(expressJWT({ secret: secretKey, algorithms: ['HS256']}).unless({ path: [/^\/api\//] }))
const tokenStr = jwt.sign({ username: userinfo.username ,status:userinfo.status}, secretKey, { expiresIn: '30s' })
//记录多个参数
{ username: userinfo.username ,status:userinfo.status}
3.3.5获取用户信息
当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用
req.user对象,来访问从 JWT 字符串中解析出来的用户信息请求头的
Authorization字段 的值是Bearer + token(记得token前要加Bearer)头信息Authorization: Bearer <token>
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user, // 要发送给客户端的用户信息
})
})
3.3.6捕获解析 JWT 失败后产生的错误
- 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
- 通过 Express 的错误中间件,捕获这个错误并进行相关的处理
// TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err,req,res,next)=>{
//这次错误是由token解析失败导致的
if(err.name ==='UnauthorizedError'){
return res.send({
status:401,
message:'无效的token'
})
}
res.send({
status:500,
message:'未知的错误'
})
})
完整代码:
// 导入 express 模块
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 允许跨域资源共享
const cors = require('cors')
app.use(cors())
// 解析 post 表单数据的中间件
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false }))
// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'itheima No1 ^_^'
// TODO_04:注册将 JWT 字符串解析还原成 JSON 对象的中间件
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
app.use(expressJWT({ secret: secretKey, algorithms: ['HS256']}).unless({ path: [/^\/api\//] }))
// 登录接口
app.post('/api/login', function (req, res) {
// 将 req.body 请求体中的数据,转存为 userinfo 常量
const userinfo = req.body
console.log(userinfo);
// 登录失败
if (userinfo.username !== 'admin' || userinfo.password !== '000000') {
return res.send({
status: 400,
message: '登录失败!',
})
}
// 登录成功
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// 参数1:用户的信息对象
// 参数2:加密的秘钥
// 参数3:配置对象,可以配置当前 token 的有效期
// 记住:千万不要把密码加密到 token 字符中
const tokenStr = jwt.sign({ username: userinfo.username ,status:userinfo.status}, secretKey, { expiresIn: '30s' })
res.send({
status: 200,
message: '登录成功!',
token: tokenStr, // 要发送给客户端的 token 字符串
})
})
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user, // 要发送给客户端的用户信息
})
})
// TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err,req,res,next)=>{
//这次错误是由token解析失败导致的
if(err.name ==='UnauthorizedError'){
return res.send({
status:401,
message:'无效的token'
})
}
res.send({
status:500,
message:'未知的错误'
})
})
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(8888, function () {
console.log('Express server running at http://127.0.0.1:8888')
})
四、大事件后台 API 项目
开发文档(如下)
1. 项目初始化
1.1 创建项目
1.新建 api_server 文件夹作为项目根目录,并在项目根目录中运行如下的命令,初始化包管理配置文件:
npm init -y
2.运行如下的命令,安装特定版本的 express:
npm i express@4.17.1
3.在项目根目录中新建 app.js 作为整个项目的入口文件,并初始化如下的代码:
const express = require('express')
// 创建 express 的服务器实例
const app = express()
// write your code here...
// 调用 app.listen 方法,指定端口号并启动web服务器
app.listen(3007, function () {
console.log('api server running at http://127.0.0.1:3007')
})
1.2 配置 cors 跨域
1.运行如下的命令,安装 cors 中间件:
npm i cors@2.8.5
2.在 app.js 中导入并配置 cors 中间件:
// 导入 cors 中间件
const cors = require('cors')
// 将 cors 注册为全局中间件
app.use(cors())
1.3 配置解析表单数据的中间件
通过如下的代码,配置解析 application/x-www-form-urlencoded 格式的表单数据的中间件:
app.use(express.urlencoded({ extended: false }))
1.4 初始化路由相关的文件夹
1.在项目根目录中,新建 router 文件夹,用来存放所有的路由模块
路由模块中,只存放客户端的请求与处理函数之间的映射关系
2.在项目根目录中,新建 router_handler 文件夹,用来存放所有的 路由处理函数模块
路由处理函数模块中,专门负责存放每个路由对应的处理函数
1.5 初始化用户路由模块
1.在 router 文件夹中,新建 user.js 文件,作为用户的路由模块,并初始化代码:
const express = require('express')
// 创建路由对象
const router = express.Router()
// 注册新用户
router.post('/reguser', (req, res) => {
res.send('reguser OK')
})
// 登录
router.post('/login', (req, res) => {
res.send('login OK')
})
module.exports = router
2.在 app.js 中,导入注册用户路由模块 :
const userRouter = require('./router/user')
app.use('/api', userRouter)
1.6 抽离用户路由模块中的处理函数
目的:为了保证
路由模块的纯粹性,所有的路由处理函数,必须抽离到对应的路由处理函数模块中
1.在 /router_handler/user.js 中,使用 exports 对象,分别向外共享如下两个 路由处理函数 :
/**
* 在这里定义和用户相关的路由处理函数,供 /router/user.js 模块进行调用
*/
// 注册用户的处理函数
exports.regUser = (req, res) => {
res.send('reguser OK')
}
// 登录的处理函数
exports.login = (req, res) => {
res.send('login OK')
}
2.将 /router/user.js 中的代码修改为如下结构:
const express = require('express')
const router = express.Router()
// 导入用户路由处理函数模块
const userHandler = require('../router_handler/user')
// 注册新用户
router.post('/reguser', userHandler.regUser)
// 登录
router.post('/login', userHandler.login)
module.exports = router
2.登录注册
2.1 新建 ev_users 表
1.在 test 数据库中,新建 ev_users 表如下:

2.2 安装并配置 mysql 模块
在 API 接口项目中,需要安装并配置
mysql这个第三方模块,来连接和操作 MySQL 数据库
- 运行如下命令,安装
mysql模块:
1999

被折叠的 条评论
为什么被折叠?



