为什么javascript可以在浏览器中被执行
待执行的js代码 => javascript解析引擎
不同浏览器的javascript解析引擎不一样
为什么javascript可以操作dom和bom
浏览器的javascript运行环境
运行环境是指:代码正常运行所需的必要环境
比如chrome浏览器运行环境:
负责解析javascript代码
由运行环境提供的特殊接口、只能在所属的运行环境中被调用
javascript能否做后端开发
需要在nodejs的运行环境中可以
nodejs是一个基于chromeV8引擎的javascript运行环境
nodejs的javascript运行环境
nodejs可以做什么
只是提供了基础的功能和api,然而,基于nodejs的基础功能,很多强大的工具和框架如雨后春笋,层出不穷
学习路径
浏览器的javascript学习路径
nodejs学习路径
定义
nodejs官方提供的、用来操作文件的模块。它提供了一系列方法和属性,用来满足用户对文件的操作需求
使用
const fs = require('fs');
fs.readFile()
fs.readFile(path[, options], callback);
fs.readFile('./notes.md', 'utf-8', (err, data) => {
if (err) {
console.log('err :>> ', err);
return "文件读取失败";
}
console.log('data :>> ', data);
reurn "文件读取成功";
})
fs.writeFile()
fs.writeFile(path, data[, options], callback);
fs.writeFile("11.txt", "hello", "utf8", (err) => {
if (err) {
console.log('err :>> ', err);
return "文件读取失败";
}
return "文件写入成功";
});
fs模块-路径动态拼接问题
背景:在使用fs模块操作文件时,如果提供的操作路径是以./
或../
开头的相对路径时,很容易出现路径动态拼接错误的问题
原因:代码在运行的时候,会执行node命令时所处的目录
,动态拼接出被操作文件的完整路径
如下代码:代码路径为/user/local/test.js
,读取的文件在/user/local/notes.md
,当在/user/local
执行node test.js
和在/user
执行node local/test.js
效果时是不一样的
fs.readFile('./notes.md', 'utf-8', (err, data) => {
if (err) {
console.log('err :>> ', err);
return "文件读取失败";
}
console.log('data :>> ', data);
return "文件读取成功";
})
解决方案
使用绝对路径:可移植性差,不利于维护
使用__dirname
表示当前文件所在的绝对目录
fs.writeFile(__dirname + "/11.txt", "hello", "utf8", (err) => {
if (err) {
console.log('err :>> ', err);
return "文件读取失败";
}
return "文件写入成功";
});
延伸:如果想要读取上一层的文件的话,需要使用path.join
来进行拼接
const path = require('path');
const fs = require('fs');
const filePath = path.join(__dirname, '../example.txt');
fs.readFile(filePath, (err, data) => {
if (err) throw err;
console.log(data.toString());
});
定义
nodejs官方提供的,用来处理路径的模块,提供了一系列的方法和属性,用来满足用户对路径的处理需求
导入
const path = require('path');
path.join()
const pathStr = path.join('a', '/b', 'c', '../', 'd');
console.log('pathStr :>> ', pathStr); // pathStr :>> a/b/d
const pathStr1 = path.join(__dirname, pathStr, "../f");
console.log('pathStr1 :>> ', pathStr1); // /Users/xxx/Documents/code/learnningNotes/js/nodejs/testcode/a/b/f
参数说明
../
的话会自动去掉最后一个层级/a
和a
拼接效果是一样的注意:凡是涉及到路径拼接的操作,都要使用path.join()
方法进行处理,不要直接使用+
进行字符串的拼接
path.basename()
const filePath = '/user/local/test.txt';
let fullName = path.basename(filePath);
console.log('fullName :>> ', fullName); // test.txt
let nameWithoutExtension = path.basename(filePath, '.txt');
console.log('nameWithoutExtension :>> ', nameWithoutExtension); // test
参数说明
path.extname()
获取路径中的文件扩展名
path.extname(path)
const filePath = '/user/local/test.txt';
let extname = path.extname(filePath);
console.log('extname :>> ', extname); // .txt
综合案例:将目录下的index.html中的html、script、style拆分出来,放到同级下的一个test目录中
步骤分析
导入需要的模块并创建正则表达式
const path = require('path');
const fs = require('fs');
const regStyle = /', '');
fs.writeFile(path.join(__dirname, 'test/index.css'), newCSS, err => {
if (err) return console.error('写入css文件失败:', err.message);
console.log('写入css成功');
});
}
自定义resolveJS方法
function resolveJS(data) {
const r1 = regScript.exec(data);
const newJS = r1[0].replace('', '');
fs.writeFile(path.join(__dirname, 'test/index.js'), newJS, err => {
if (err) return console.error('写入js文件失败:', err.message);
console.log('写入js成功');
});
}
自定义resolveHTML方法
function resolveHTML(data) {
const newHTML = data.replace(regStyle, '')
.replace(regScript, '');
fs.writeFile(path.join(__dirname, 'test/index.html'), newHTML, err => {
if (err) return console.error('写入html文件失败:', err.message);
console.log('写入html成功');
});
}
两个注意点
什么是http模块
客户端:在网络节点中,负责消费资源的电脑。
服务器:负责对外提供网络资源的电脑。
http模块就是nodejs提供的,用来创建web服务器的模块。通过http模块提供的http.createServer()
方法,就能方便的把一台普通的电脑,变成一台web服务器,从而对外提供web资源服务
导入
const http = require('http');
了解http模块的作用
服务器和普通电脑的区别:服务器上安装了web服务器软件
,例如IIS、Apache等。通过安装这些服务器软件,就能把一台普通的电脑变成一台web服务器。
在nodejs中,我们不需要使用IIS、Apache等第三方web服务器软件,因为我们可以基于nodejs提供的http模块,通过几行简单的代码,就能轻松的写一个服务器软件,从而对外提供web服务。
服务器相关的概念
IP地址
点分十进制
表示成a.b.c.d
的形式域名和域名服务器
端口号
创建最基本的web服务器
创建web服务器的基本步骤
const http = require('http');
// 创建web服务器实例
const server = http.createServer();
// 为服务器实例绑定request事件,监听客户端的请求
server.on('request', (req, res) => {
console.log("监听到了request");
console.log('req :>> ', req);
console.log('res :>> ', res);
});
// 调用服务器实例的`.listen()`方法,即可启动当前的web服务器实例
server.listen(80, () => {
console.log('http server running at http://127.0.0.1');
})
req请求对象
只要服务器接收到了客户端的请求,就会调用通过server.on()
为服务器绑定的request事件处理函数
如果想要在事件处理函数中,访问与客户端相关的数据或者属性,可以使用如下的方式
server.on('request', (req, res) => {
console.log('req.url :>> ', req.url);
console.log('req.method :>> ', req.method);
});
res响应对象
在服务器的request事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式
res.end()方法的作用:向客户端发送指定的内容,并结束这次请求的处理过程
server.on('request', (req, res) => {
res.end("Hello, world!");
});
解决中文乱码的问题
当调用res.end()
向客户端发送中文内容的时候,会出现乱码的问题,此时,需要手动设置内容的编码格式
如我返回的是我爱宝宝
,接收到的是鎴戠埍瀹濆疂
需要设置响应头Content-Type
的值为text/html;charset=utf-8
res.setHeader('Content-Type', 'text/html;charset=utf-8');
res.end("我爱宝宝");
根据不同的url响应不同的html内容
server.on('request', (req, res) => {
const url = req.url;
let content = '404 not found
';
if (url === '/' || url === '/index.html') {
content = '首页
'
} else if (url === '/about.html') {
content = '关于页面
';
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(content);
});
案例:将上面的拆分index.html的几个文件返回给客户端
const http = require('http');
const path = require('path');
const fs = require('fs');
const server = http.createServer();
// 为服务器实例绑定request事件,监听客户端的请求
server.on('request', (req, res) => {
console.log("进入了request");
const url = req.url;
console.log('url :>> ', url);
let content = "";
fs.readFile(path.join(__dirname, 'test', url), 'utf8', (err, data) => {
console.log("进入了readFile");
console.log('data :>> ', data);
if (err) {
content = '404 not found
';
} else {
content = data;
}
// res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(content);
});
});
// 启动服务器
// 调用服务器实例的`.listen()`方法,即可启动当前的web服务器实例
server.listen(80, () => {
console.log('http server running at http://127.0.0.1');
})
什么是模块化
编程领域的模块化
遵守固定的规则,把一个大文件拆分成独立相互依赖的多个小模块
好处
模块化规范
就是对代码进行拆分和组合时,需要遵守的那些规则
例如
好处
分类
加载模块
使用require()
方法,可以加载需要的模块
// 加载内置模块
const fs = require('fs');
// 加载自定义模块(需要指定路径)(可以省略js后缀)
const custom = require('./custom.js');
// 加载第三方模块
const moment = require('moment');
注意:使用require方法加载其他模块时,会执行被加载模块中的代码
模块作用域
和函数作用域蕾丝,在自定义模块中定义的变量、方法等成员,只能在当前模块内访问,这种模块级别的访问限制,叫做模块作用域
// customer.js
const username = 'lan';
function sayHello() {
console.log("hello:", username);
}
// test.js
const customer = require('./customer.js');
console.log("customer", customer); // {}
好处
向外共享模块作用域中的成员
module对象
在每个自定义模块中都有一个module对象,它里面存储了和当前模块有关的信息
Module {
id: '.',
path: '/Users/feng.lan/Documents/code/learnningNotes/js/nodejs/testcode',
exports: {},
filename: '/Users/feng.lan/Documents/code/learnningNotes/js/nodejs/testcode/module.js',
loaded: false,
children: [
Module {
id: '/Users/feng.lan/Documents/code/learnningNotes/js/nodejs/testcode/customer.js',
path: '/Users/feng.lan/Documents/code/learnningNotes/js/nodejs/testcode',
exports: {},
filename: '/Users/feng.lan/Documents/code/learnningNotes/js/nodejs/testcode/customer.js',
loaded: true,
children: [],
paths: [Array]
}
],
paths: [
'/Users/feng.lan/Documents/code/learnningNotes/js/nodejs/testcode/node_modules',
'/Users/feng.lan/Documents/code/learnningNotes/js/nodejs/node_modules',
'/Users/feng.lan/Documents/code/learnningNotes/js/node_modules',
'/Users/feng.lan/Documents/code/learnningNotes/node_modules',
'/Users/feng.lan/Documents/code/node_modules',
'/Users/feng.lan/Documents/node_modules',
'/Users/feng.lan/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
module.exports对象
在自定义模块中,可以使用module.exports
对象,把模块内的成员共享出去,供外界使用
外界使用require()
导入自定义的模块时,得到的就是module.exports
所指的对象
注意:使用require()
导入模块时,导入的结果,永远以module.exports
指向的对象为准
const username = "lan";
function sayHello() {
console.log("hello:", username);
}
module.exports.nickname = 'feng';
module.exports.printName = function() {
console.log("printName");
}
module.exports = {
username,
sayHello
}
exports对象
由于module.exports单词写起来比较复杂,为了简化向外共享成员的代码,nodejs提供了exports对象,默认情况下,exports和module.exports指向同一个对象
最终共享的结果,还是以module.exports指向的对象为准
exports和module.exports的使用误区
console.log(module.exports === exports); // true
关键点:在没有重新改变引用对象的时候,两者相等,但是一旦其中一个改变引用对象之后,就以module.exports
为准
时刻谨记,require()模块时,得到的永远是module.exports指向的对象
module.exports.username = 'lan';
exports.age = 12;
// 这种情况下获取到的是 {username: 'lan', age: 12}
module.exports.username = 'lan';
exports = {
gender: '男',
age: 12
}
// 这种情况下获取到的是 {name: 'lan'}
exports.usename = 'lan';
module.exports = {
gender: 'man',
age: 12
}
// 这种情况得到的是 {gender: 'man', age: 12}
nodejs中的模块化规范
遵循commonjs模块化规范,规定了模块的特性和各模块之间如何相互依赖
什么是包
nodejs中的第三方模块有叫做包
包的来源
由第三方个人或者团队开发出来的,免费供所有人使用
注意:nodejs中的包都是免费开源的,不需要付费即可免费下载使用
为什么需要包
在哪里下载包
搜索:网站https://www.npmjs.com/
,他是全球最大的包共享平台,你可以从这网站上搜索任何你需要的包
下载:通过https://registry.npmjs.org/
的服务器,来对外共享所有的包,我们可以从这服务器上下载自己所需要的包
如何下载包
包管理工具:npm包管理工具(Node Package Manager),这个工具随着nodejs的安装包一起被安装到了用户的电脑上
初次安装包之后多了哪些文件
安装指定版本
默认情况下,是安装最新版本的,如果需要指定版本,使用@
npm i moment@2.22.2
包的语义化版本规范
点分十进制
的形式定义的,总共有3位数字,如2.22.2
版本号提升规则:只要前面的版本号增长了,后面的版本号归零
包管理配置文件
npm规定:在项目根目录中,必须提供一个叫做package.json
的包管理配置文件,用来记录与项目有关的一些配置信息,如
快速创建package.json
npm init -y
注意:上述命令,只能在英文
目录中运行,所以项目文件夹的名称一定要使用英文命名,不要使用中文,不要出现空格
dependencies:用来记录安装过哪些包
devDependencies:只在项目开发阶段会用到,项目上线之后不会用到
```js
npm i webpack -D
npm install webpack --save--dev
```
npm下载速度慢
原因:默认从国外https://registry.npmjs.org
服务器进行下载,此时网络数据传输需要经过漫长的海底光缆
方案:淘宝npm镜像服务器
淘宝在国内搭建了一个服务器,专门把国外官方服务器上的包同步到国内的服务器,然后在国内提供下包的服务,极大的提高了下包的速度
切换npm的下包镜像源
```shell
# 查看当前下包镜像源
npm config get registry
# 将下包的镜像源切换为淘宝镜像源
npm config get registry=https://registry.npm.taobao.org/
```
nrm
为了方便切换下包的镜像源,我们可以安装nrm这个小工具,利用nrm提供的终端命令,可以快速查看和切换下包的镜像源
npm i nrm -g
nrm ls
nrm use taobao
可能存在的报错
/opt/homebrew/Cellar/nvm/0.39.1_1/versions/node/v16.18.0/lib/node_modules/nrm/cli.js:9
const open = require('open');
^
Error [ERR_REQUIRE_ESM]: require() of ES Module /opt/homebrew/Cellar/nvm/0.39.1_1/versions/node/v16.18.0/lib/node_modules/nrm/node_modules/open/index.js from /opt/homebrew/Cellar/nvm/0.39.1_1/versions/node/v16.18.0/lib/node_modules/nrm/cli.js not supported.
Instead change the require of index.js in /opt/homebrew/Cellar/nvm/0.39.1_1/versions/node/v16.18.0/lib/node_modules/nrm/cli.js to a dynamic import() which is available in all CommonJS modules.
at Object.<anonymous> (/opt/homebrew/Cellar/nvm/0.39.1_1/versions/node/v16.18.0/lib/node_modules/nrm/cli.js:9:14) {
code: 'ERR_REQUIRE_ESM'
}
原因是缺少相应的open
包
// 这里好像要这个版本才行,我安装最新的版本也没有解决
npm install -g nrm open@8.4.2 --save
规范的包结构
package.json
这个包管理配置文件package.json
中必须包含name
,version
,main
这3个属性,代表名字、版本号、入口开发属于自己的包
初始化package.json
{
"name": "lan-tools",
"version": "1.0.0",
"description": "feng.lan测试包",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["feng", "lan"],
"author": "feng.lan",
"license": "ISC"
}
在index.js中初始化自己的代码,并暴露对应的成员
function printInfo() {
console.log("这里是feng.lan的测试函数");
}
function sayHello(name) {
console.log("hello " + name);
}
module.exports = {
printInfo,
sayHello,
}
编写包的说明文档
包根目录中的README.md
文件
### 安装
> npm install lan-tools
### 导入
> const lanTools = require('lan-tools');
### 输出信息
> lanTools.printInfo();
### sayHello
> lanTools.sayHello("feng.lan");
发布包
注册npm账号
终端使用npm login
命令,依次输入用户名、密码、邮箱、邮箱验证码后,即可登录成功
存在的问题:在登录的时候需要将包仓库设置为npm官方源,因为登陆是根据你这个设置的,否则比如你的是淘宝源,他会提示你正在登陆淘宝源,那你注册的npm账号就登陆不上去
```shell
npm WARN adduser `adduser` will be split into `login` and `register` in a future version. `adduser` will become an alias of `register`. `login` (currently an alias) will become its own command.
npm notice Log in on https://registry.npm.taobao.org/
```
将终端切换到包的根目录之后,运行npm publish
删除已经发布的包
npm unpublish 包名 --force
注意
优先从缓存中进行加载
模块在第一次加载之后会被缓存,这也意味着多次调用require()
不会导致模块的代码被执行多次
无论是内置模块、用户自定义模块、还是第三方模块,他们都会优先从缓存中加载,从而提高模块的加载效率
内置模块的加载机制
内置模块的加载优先级最高
即使自定义模块有同名的fs
模块,也是加载nodejs官方的fs
模块
自定义模块的加载机制
必须要以./
或者../
开头的路径标志符,如果没有用./
或者../
,则node会把他当作内置模块或者第三方模块进行加载
如果省略了文件的扩展名,则nodejs会按顺序分别尝试加载一下的文件
确切的文件名
进行加载.js
扩展ing进行加载.json
扩展名进行加载.node
扩展名进行加载第三方模块的加载机制
nodejs会从当前模块的父目录开始,尝试从/node_modules
目录中加载第三方模块
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录
例如,在/user/local/project/foo.js
中调用了require('tools')
,则nodejs会按以下顺序进行查找
目录作为模块
当把目录作为模块标志符,传递给require()
进行加载的时候,有3种加载方式
package.json
,并寻找main
属性,作为require()
的入口index.js
文件Error: Cannot find module 'xxx'
定义
官网:https://www.expressjs.com.cn/
进一步理解
内置的http内置模块用起来很复杂,开发效率低
express是基于内置的http模块进一步封装出来的,能够极大的提高开发效率
experss能做什么
安装
npm i express`
基本使用
// 导入express
const express = require('express');
// 创建web服务器
const app = express();
// 启动web服务器
app.listen(80, () => {
console.log('express server running at http://127.0.0.1 and listening on 80')
})
监听get、post请求,并返回响应内容
const express = require('express');
const app = express();
app.listen(80, () => {
console.log('express server running at http://127.0.0.1 and listening on 80')
})
app.get('/getMethods', (req, res) => {
console.log('getMethods');
res.send({
type: 'get',
name: 'lan',
age: 12
})
})
app.post('/postMethods', (req, res) => {
console.log('postMethods');
res.send({
type: 'post',
name: 'lan',
age: 12
})
})
获取url的查询参数
app.get('/getMethods', (req, res) => {
const params = req.query;
res.send(params)
})
获取url中的动态参数
通过:
匹配动态参数
例如接口/user/:id
用来查询某个用户信息
app.get('/user/:id', (req, res) => {
const params = req.params;
res.send(params);
})
// 可以多个动态参数拼接
app.get('/user/:id/:username', (req, res) => {
const params = req.params;
res.send(params);
})
express.static()
express提供了一个非常好用的函数,叫做express.statis()
,通过它,我们可以非常方便的创建一个静态资源服务器,例如,通过如下代码就可以将pulbic
目录下的图片、css文件、js文件对外开放访问了
```js
// 目录位置相对于根目录
app.use(express.static('public'));
// url访问方式 public/demo.js
// http://127.0.0.1/demo.js
```
托管多个静态资源目录,请多次调用即可
app.use(express.static('public'));
app.use(express.static('static'));
挂载路径前缀
如果希望访问静态资源之前,加上路径前缀
```js
app.use('/public', express.static('public'));
```
注意:/public
的/
不可少
作用
在编写调试nodejs项目的时候,如果修改了项目的代码,则需要频繁的手动close掉,然后再重新启动,非常繁琐
nodemon能够监听项目文件的改动,当代吗被修改后,会自动重新启动项目
安装
npm install -g nodemon
使用
启动命令从node index.js
改为nodemon index.js
概念
指的是客户端的请求
和服务器处理函数
之间的映射关系
由3部分组成,请求的类型、请求的url地址、处理函数
app.METHOD(PATH, HANDLER);
app.get('/', () => console.log('get请求'));
app.post('/', () => console.log('post请求'));
路由匹配过程
每当一个请求到达服务器以后,需要先经过路由匹配,只有匹配成功之后,才会调用对应的处理函数
先后顺序
进行匹配请求类型
和请求的url
需要同时匹配
成功,才会调用对应的处理函数最简单的路由
直接挂载在app
上
app.get('/', () => console.log('get请求'));
app.post('/', () => console.log('post请求'));
路由模块化
为了方便对路由进行模块化的管理,不建议直接将路由挂载到app上
,而是推荐将路由抽离为单独的模块
./js
文件express.Router()
函数创建路由对象module.exports()
向外共享路由对象app.use()
函数注册路由模块// router.js
const express = require('express');
// 创建路由对象
const router = express.Router();
// 挂载路由
router.get('/user/list', (req, res) => {
res.send('get user list');
})
router.post('/user/add', (req, res) => {
res.send('add new user');
})
// 向外导出路由对象
module.exports = router;
const express = require('express');
const router = require('./router.js');
const app = express();
// 注册路由模块,全剧中间件
app.use(router);
app.listen(80, () => {
console.log('express server running at http://127.0.0.1 and listening on 80')
})
为路由添加前缀
app.use('/api', router);
定义
当一个请求到达express服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
本质上是一个处理函数
,中间件的格式
如下
```js
app.get('/', (req, res, next) => {
/* some code */
next();
})
```
注意:中间件函数的形参列表,必须包含next参数
,而路由处理参数只包含req和res
next函数的作用
是实现多个中间件连续调用
的关键,它表示把流转关系转交
给下一个中间件或路由
定义中间件函数
const express = require('express');
const router = require('./router.js');
const app = express();
const mw = (req, res, next) => {
console.log('最简单的中间件');
next();
}
// 全局生效的中间件
app.use(mw);
app.use('/api', router);
app.listen(80, () => {
console.log('express server running at http://127.0.0.1 and listening on 80')
})
全局生效的中间件
客户端发起的任何请求
,到达服务器之后,都会触发
的中间件
通过app.use(wm)
,即可定义一个全局生效的中间件
```js
app.use(mw);
```
定义多个全局中间件,客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用
```js
app.use((req, res, next) => {
console.log('调用了第一个中间件');
next();
})
app.use((req, res, next) => {
console.log('调用了第二个中间件');
next();
})
```
作用
局部生效的中间件
不使用app.use()
定义的中间件
```js
const mw = (req, res, next) => {
console.log('局部生效的中间件');
next();
}
// 挂载路由
router.get('/user/list', mw, (req, res) => {
console.log('startTime', req.startTime)
res.send({
txt: 'get user list',
date: req.startTime
});
})
```
定义多个局部中间件
```js
// 方式一:使用数组
router.get('/user/list', [mw1, mw2, mw3], (req, res) => {})
// 方式二:多个参数
router.get('/user/list', mw1, mw2, mw3, (req, res) => {})
```
几个注意事项
next()
next()
函数之后不要再写额外的代码中间件的分类
应用级别的中间件
通过app.use()
、app.get()
或app.post()
,绑定到app实例上的中间件
路由级别的中间件
绑定到express.Router()
实例上的中间件
用法和应用级别的中间件没有任何区别,只不过应用级别是绑定到app实例上
,路由级别是绑定到router实例上
错误级别的中间件
专门用来捕获项目中发生的异常错误,从而防止项目异常崩溃的问题
格式:必须有4个形参(err, req, res, next)
```js
const express = require('express');
const app = express();
const mw = (err, req, res, next) => {
console.log('发生了错误', err.message);
res.send({
code: 500,
msg: err.message
})
}
app.use('/user/list', (req, res) => {
throw new Error('服务器错误了');
res.send('data');
});
app.use(mw);
app.listen(80, () => {
console.log('express server running at http://127.0.0.1 and listening on 80')
})
```
必须要在所有路由之后
express内置的中间件
从express4.16.0版本开始,内置了3个
常用的中间件
express.static 快速托管静态资源的内置中间件,例如html文件、图片等(完全兼容
)
express.json 解析json格式的请求提数据(4.16.0+
)
express.urlencoded 解析URL-encoded格式的请求提数据(4.16.0+
)
默认情况下,如果不配置解析表单数据的中间件,则req.body
默认等于undefined
// 配置解析application/json格式数据的内置中间件
app.use(express.json());
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({ extended: false }));
第三方的中间件
非nodejs官方内置的,而是由第三方开发出来的中间件
比如body-parser
这个中间件
npm install body-parser
require
进行导入app.use()
注册并使用解析form-data中的数据中间件
const multipart = require('connect-multiparty');
const multipartMiddleware = multipart();
app.use(multipartMiddleware);
app.post('/api/login', (req, res) => {
const data = req.body;
res.send(data);
})
自定义中间件实现
描述:手动模拟一个类似express.urlencoded,来解析post提交到服务器的表单数据
实现步骤
定义中间件
监听req的data事件
如果数据量比较大,无法一次性发送完毕,则客户端会把数据切割后,分批发送到服务器,所以data事件可能会触发多次,每一次触发data事件时,只是完整数据的一部分,需要手动对接收到的数据进行拼接
监听req的end事件
当请求体数据接收完毕之后,会自动触发req的end事件
使用querystring模块解析请求体数据
nodejs内置了一个querystring
模块,专门用来处理查询字符串
,这个模块的parse()
函数,可以轻松把查询字符串解析成对象的格式
将解析出来的数据对象挂载为req.body
将自定义的中间件封装为模块
app.use((req, res, next) => {
console.log('进入了自定义中间件')
let str = "";
req.on('data', (chunk) => {
console.log('chunk :>> ', chunk);
str += chunk;
})
req.on('end', () => {
console.log('完整数据', str);
const data = qs.parse(str);
console.log('data :>> ', data);
req.body = data;
next();
})
})
使用express写接口
get接口
router.get('/user/list', (req, res) => {
res.send({
status: 200,
data: req.query,
msg: 'GET 请求成功',
});
})
post接口
router.post('/user/add', (req, res) => {
res.send({
status: 200,
data: req.body,
msg: 'POST 请求成功',
});
})
背景
上面的get和post接口,不支持跨于请求
解决方案主要由2种
使用cors中间件
```js
const cors = require('cors');
app.use(cors());
```
限制
在服务端进行配置,客户端浏览器无需做任何额外的配置,即可请求开启了cors的接口
在浏览器有兼容性,只有支持XMLHttpRequest Level2
的浏览器才能用cors(IE10+,Chrome4+,FireFox3.5+)
Access-Control-Allow-Origin
// 请求头
Access-Control-Allow-Origin: <origin> | *
// 实际设置
res.setHeader('Access-Control-Allow-Origin', '*');
Access-Control-Allow-Headers
默认情况下,cors仅支持客户端向服务器发送如下的9个请求头
如果客户端向服务端发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Headers
对额外的请求头进行声明
,否则这次请求会失败
```js
// 多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header');
```
Access-Control-Allow-Methods
默认情况下,cors仅支持客户端发起get、post、head请求
如果客户端希望通过put、delete等方式请求服务器的资源,则需要在服务端,通过Access-Control-Allow-Methods
来指明实际请求所允许使用的http方法
```js
res.setHeader('Access-Control-Allow-Methods', 'GET POST DELETE HEAD');
res.setHeader('Access-Control-Allow-Headers', '*');
```
简单请求
同时满足下面2个条件的请求,就是简单请求
无自定义头部字段
)
预检请求
只要符合下面任何一个条件的请求,都需要进行预检请求
之外
的请求自定义头部
字段application/json
格式的数据在浏览器和服务器正式通信之前,浏览器会先发送OPTION
请求进行预检,以获取服务器是否允许该实际请求,所以这一次的OPTION
请求称为预检请求,服务器成功响应预检请求之后
,才会发送真正的请求,并且携带真实数据
jsonp
概念:浏览器端通过标签的
src
属性,请求服务器上的数据,同时,服务器返回一个函数的调用,这种请求数据的方式叫做jsonp
特点:
// 必须要cors中间件之前,配置jsonp接口
app.get('/api/jsonp', (req, res) => {
// 1. 获取客户端发送过来的回调函数的ing子
const callback = req.query.callback;
// 2. 得到要通过jsonp形式发送给客户端的数据
const data = {
name: 'lan',
age: 12
}
// 3. 根据前两步得到的数据,拼接出一个函数调用的字符串
const str = `${callback}(${JSON.stringify(data)})`
// 4. 把上一步拼接得到的字符串,响应给客户端的script标签进行解析
res.send(str);
})
function jsonpHandle() {
$.ajax({
url: 'http://127.0.0.1/api/jsonp',
dataType: 'jsonp',
jsonpCallback: 'myCallback',
success: function(data) {
console.log(data);
},
error: function(jqXHR, textStatus, errorThrown) {
console.error(errorThrown);
}
});
}
function myCallback(data) {
console.log('执行了myCallback', data);
}
定义
用来组织、存储、管理数据的仓库
可以对数据库的数据进行增删改查操作
常见的数据库及分类
关系型数据库:MySql、Oracle、SQL Server
非关系型数据库:Mongodb
传统型数据库的数据组织结构
关系
需要的软件
安装mysql和navicat
mysql:https://blog.csdn.net/weixin_44427181/article/details/127552892
mysql -uroot -p
navicat:
使用navicat连接数据库遇到错误
1045 - Access denied for user ‘root’@‘localhost’ (using password: YES)
参考:https://zhuanlan.zhihu.com/p/158806221
mac 2002 - Can’t connect to server on ‘127.0.0.1’ (36)
这个我重新启动电脑就好了。。。
最基本的数据库命令
// 登陆数据库
mysql -u root -p
// 显示数据库
show databases;
// 创建数据库
create database MyDB_one;
// 设置编码
create database MyDB_three charset utf8;
// 查看数据库信息
show create database MyDB_one;
// 进入或者切换数据库
use MyDB_one
// 显示表
show tables;
// 创建表
create table Phone_table(pid INT, name CHAR(20), price INT);
// 增加字段
alter table Phone_table add color CHAR(20);
express中mysql模块安装和配置
npm install mysql
// 1. 导入mysql模块
const mysql = require('mysql');
// 2. 建立与mysql数据库的连接
const db = mysql.createPool({
host: '127.0.0.1',
user: 'root',
port: 3306,
password: 'feng.lan',
database: 'user',
})
db.query('select * from user', (err, res) => {
if (err) return console.error('err----', err);
console.log('res-----', res);
})
```js
// 查询
db.query('select * from user', (err, res) => {
if (err) return console.error('err----', err.message);
console.log('res-----', res);
})
const user = {
name: '王五',
age: 15
}
// 插入(按字段插入)
const sqlStr = 'INSERT INTO user (name, age) VALUES (?, ?)';
db.query(sqlStr, [user.name, user.age], (err, res) => {
if (err) return console.error('err----', err.message);
if (res.affectedRows === 1) {
console.log('插入数据成功', res);
}
})
// 插入(按行插入)需要对象的每个属性和字段一一对应
const sqlStr1 = 'INSERT INTO user SET ?';
db.query(sqlStr1, user, (err, res) => {
if (err) return console.error('err----', err.message);
if (res.affectedRows === 1) {
console.log('插入数据成功', res);
}
})
// 更新
const user = {
id: 1,
name: '张三一',
age: 1
}
const sqlStr = 'UPDATE user SET name = ?, age = ? WHERE id = ?';
db.query(sqlStr, [user.name, user.age, user.id], (err, res) => {
if (err) return console.error('err----', err.message);
if (res.affectedRows === 1) {
console.log('更新数据成功', res);
}
})
// 快捷更新
const sqlStr1 = 'UPDATE user SET ? WHERE id = ?';
db.query(sqlStr1, [user, user.id], (err, res) => {
if (err) return console.error('err----', err.message);
if (res.affectedRows === 1) {
console.log('更新数据成功', res);
}
})
// 删除
const sqlStr = 'DELETE FROM user WHERE id = ?';
db.query(sqlStr, 1, (err, res) => {
if (err) return console.error('err----', err.message);
if (res.affectedRows === 1) {
console.log('删除数据成功', res);
}
})
```
web开发模式
服务端渲染
服务器发送给客户端的html页面,是在服务器通过字符串的拼接,动态生成的
因此客户端不需要ajax这样的技术额外请求页面的数据
app.get('/index.html', (req, res) => {
const user = {
name: 'zhangsan',
age: 12
}
const html = `姓名:
${user.name},年龄:${user.age}`;
res.send(html);
})
优点
前端耗时少
:因为服务器负责动态生成html内容,浏览器只要直接渲染页面即可,尤其是移动端,更省电有利于seo
:因为服务器响应的是完整的html内容,所以爬虫更容易获取信息缺点
占用服务器资源
:如果请求较多,会对服务器造成一定的访问压力不利于前后端分离,开发效率低
:使用服务端渲染,无法进行分工合作,尤其前端复杂度高的项目前后端分离的web开发模式
后端只负责提供api接口,前端使用ajax调用接口的开发模式
优点
开发体验好
:前端专注ui,后端专注api用户体验好
:可以轻松实现页面的局部刷新减轻了服务端渲染的压力
:因为最终的页面都是在浏览器中生成的缺点
不利于SEO
:因为完整的html页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面有效信息
解决方案:利用Vue、React等前端框架的SSR
能够很好的解决SEO的问题
如何选择开发模式
比如企业级网站,主要是展示,没有复杂的交互,并且需要良好的SEO,就需要服务端渲染
类似后台管理项目,交互性比较强,不需要考虑SEO,就可以使用前后端分离的开发模式
身份认证
定义
又称“身份验证”、“鉴权”,是通过一定手段,完成对用户的身份认证
为什么需要身份认证
为了确认当前使用的用户,是该系统的用户
不同开发模式的身份认证
Session认证机制
JWT认证机制
Session认证机制
http协议的无状态性
指的是客户端的每次http请求都是独立的
,连续多个请求之间没有直接关系,服务器不会主动保留每次http请求的状态
Cookie
4kb
的字符串自动
把当前域名
下的所有未过期
的cookie一同发送到服务器总结特性:自动发送、域名独立、过期时限、4KB限制
Cookie在身份认证中的作用
客户端第一次
请求服务器的时候,服务器通过响应头
的形式,向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中
之后,当客户端浏览器每次请求服务器
的时候,浏览器会自动将身份认证相关的cookie,通过请求头
的形式发送给服务器,服务器就可以检验客户端的身份了
Cookie不具有安全性
由于cookie时存储在浏览器中的,而且浏览器也提供了读写Cookie的api,因此cookie很容易被伪造
因此不建议服务器将重要的隐私数据(比如用户信息、密码等),通过cookie的形式发送给浏览器
Session工作原理
在Express中使用Session认证
安装express-session中间件
npm install express-session
向Session中存数据
const session = require("express-session");
const express = require("express");
const app = express();
// 配置Session
app.use(
session({
secret: "secret", // 属性值可以为任意字符串
resave: false, // 固定写法
saveUninitialized: true, // 固定写法
})
);
// 配置解析application/json格式数据的内置中间件
app.use(express.json());
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({ extended: false }));
// 登陆接口
app.post('/api/login', (req, res) => {
console.log('res.body :>> ', res.body);
// 判断用户信息是否正确
if (req.body.username !== 'lan' || req.body.password !== '1234') {
return res.send({
code: 500,
msg: '登录失败'
})
}
// 存储用户信息
req.session.user = req.body;
req.session.isLogin = true;
res.send({
code: 200,
msg: '登陆成功',
})
})
app.listen(80, () => {
console.log('express server running at http://127.0.0.1 and listening on 80')
})
从session中取数据
// 获取用户姓名接口
app.get('/api/getUserName', (req, res) => {
// 判断用户是否登陆
if (!req.session.isLogin) {
return res.send({
code: 500,
msg: '用户未登录'
})
}
res.send({
code: 200,
data: req.session.user.username,
msg: 'success'
})
})
清空session
// 退出登录接口
app.post('/api/logout', (req, res) => {
req.session.destroy();
res.send({
code: 200,
msg: '退出登录成功',
})
})
JWT认证机制
Session认证的局限性
Session认证需要配合Cookie
才能实现,由于Cookie默认不支持跨域访问,所以当涉及到前端跨域请求
后端接口的时候,需要做很多额外配置
,才能实现Session认证
什么是JWT(JSON Web Token)
是目前最流行的跨域解决方案
工作原理
总结:用户的信息通过Token字符串的形式,保存在客户端浏览器中,服务端通过还原Token字符串的形式来认证用户的身份
JWT组成部分
通常由3部分组成,3者之间使用英文的.
分隔
Header
(头部):安全性相关部分
Payload
(有效荷载):真正的用户信息部分,是用户信息加密之后的字符串
Signature
(签名):安全性相关部分
// 格式
Header.Payload.Signature
// 示例
aaaaaa.bbbbbbb.cccccc
JWT的使用方式
Authorization
字段中Authorization: Bearer <token>
在Express中使用JWT
安装
npm install jsonwebtoken express-jwt
生成
JWT字符串解析
还原成JSON对象定义secret密钥
为了保证JWT字符串的安全性,防止在网络传输过程中被别人破解,需要专门定一个用于加密
和解密
的secret密钥
进行加密
进行解密
本质就是一个字符串
定义和使用
express-jwt
这个中间件,就可以把解析出来的用户信息,挂在到req.user
属性中req.user
的内容,是jwt.sign()
第一个参数存储的对象示例代码
const express = require("express");
const cors = require("cors");
const multipart = require("connect-multiparty");
const app = express();
const multipartMiddleware = multipart();
const jwt = require("jsonwebtoken");
const expressJWT = require("express-jwt");
app.use(cors());
app.use(multipartMiddleware);
// 配置解析application/json格式数据的内置中间件
app.use(express.json());
// 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded({ extended: false }));
// 定义一个secre密钥
const secretKey = "hello world";
// expressJWT.expressjwt({ secret: secretKey })是用来解析Token的中间件
// .unless({ path: [/^\/api\//] })用来指定哪些接口不需要访问权限
app.use(
expressJWT.expressjwt({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/api\//] })
);
// 用户登录接口
app.post("/api/login", (req, res) => {
const userInfo = req.body;
// 判断用户信息是否正确
if (userInfo.username !== "lan" || userInfo.password !== "1234") {
return res.send({
code: 500,
msg: "登录失败",
});
}
res.send({
code: 200,
// 生成JWT 3个参数分别是:用户信息对象、加密密钥、配置对象
token: jwt.sign(
{
username: userInfo.username
},
secretKey,
{
expiresIn: "30s",
algorithm: "HS256",
}
),
msg: "登陆成功",
});
});
// 获取用户姓名接口
app.get("/admin/getUserName", (req, res) => {
res.send({
code: 200,
data: req.auth,
msg: "success",
});
});
app.listen(80, () => {
console.log("express server running at http://127.0.0.1 and listening on 80");
});
捕获JWT失败后产生的错误
当使用express-jwt
解析Token字符串时,如果客户端发送过来的token字符串过期或者不合法,就会报错,影响项目正常运行
可以通过Express的错误中间件,捕获这个错误并进行处理
// 错误捕获
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedEror') {
return res.send({
code: 401,
msg: '无效的token'
})
}
res.send({
code: 500,
msg: '未知错误'
})
})