0%

JavaScript

Array.prototype.filter()

filter() 方法创建给定数组一部分的浅拷贝,其包含通过所提供函数实现的测试的所有元素。

Array.prototype.reduce()

reduce()方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

Array.prototype.every()

every()方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

JSON.parse()

将JSON字符串转换为对象格式

JSON.stringify()

将对象格式转换为JSON字符串

String.prototype.startsWith()

startsWith() 方法用来判断当前字符串是否以另外一个给定的子字符串开头,并根据判断结果返回 true 或 false

React

PubSubJS

解决React兄弟组件之间不能直接传输数据的问题,通过消息的订阅和发布在任意位置相互传输数据

1
2
3
4
// 消息的订阅,响应收据
var token = PubSub.subscribe('MY TOPIC', (msg, data) => {...});
// 消息的发布,请求数据
PubSub.publish('MY TOPIC', 'hello world!')

react-router-dom

react-router-dom是什么?
1. react的一个插件库。
2. 专门用来实现一个SPA应用。(single page web application,SPA)
3. 基于react的项目基本都会用到此库。
内置组件:

1
2
3
4
5
6
7
1.	<BrowserRouter>
2. <HashRouter>
3. <Route>
4. <Redirect>
5. <Link>
6. <NavLink>
7. <Switch>
1
2
3
4
// withRouter可以加工一般组件,让一般组件具备路由组件所特有的API
// withRouter的返回值是一个新组件
import {withRouter} from 'react-router-dom'
export default withRouter(...)

Ant Design

antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品
安装

1
2
npm install antd
yarn add antd

nanoid

nanoid可随机生成一个ID值

1
2
3
4
5
6
// 安装
npm i nanoid
// 引入
npm {nanoid} from 'nanoid'
// 调用
nanoid()

函数柯里化的作用

函数柯里化可以在函数需要传参时,避免意外调用函数而传参失败
示例:

1
2
3
4
5
6
7
8
9
saveFormdata = () => {
...
}

<form onSubmit={this.subData}>
用户名:<input type="text" name="username" onChange={this.saveFormdata('username')} />
密码:<input type="password" onChange={this.saveFormdata('password')} name="password"/>
<button>提交</button>
</form>

在上述代码中,input输入框定义的函数,由于增加参数,导致saveFormdata的返回值默认返回(undefined),从而失败。tips:在JS中,函数没有return,则默认返回undefined

1
2
3
4
5
6
7
// 这行代码的意思是将saveFormdata的返回值交给onChange作为回调函数
// 默认返回undefined,onChange 失效
saveFormdata = () => {
...
}

onChange={this.saveFormdata('password')}

解决方法:把一个函数作为返回值交给onChange做为回调函数,这也称为高阶函数-函数柯里化

1
2
3
4
5
6
7
8
// 这行代码的意思是把一个函数作为返回值交给onChange做为回调函数
// return的函数才是作为真正的回调函数
saveFormdata = (dataType) => {
return (event) => {
...
}
}
onChange={this.saveFormdata('password')}

概论

map()方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成,map()方法需要return

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 箭头函数
map((element) => { /* … */ })
map((element, index) => { /* … */ })
map((element, index, array) => { /* … */ })

// 回调函数
map(callbackFn)
map(callbackFn, thisArg)

// 内联回调函数
map(function(element) { /* … */ })
map(function(element, index) { /* … */ })
map(function(element, index, array){ /* … */ })
map(function(element, index, array) { /* … */ }, thisArg)

参数

callbackFn
生成新数组元素的函数,使用三个参数:
currentValue
callbackFn 数组中正在处理的当前元素。
index
callbackFn 数组中正在处理的当前元素的索引。
array
map 方法调用的数组。
thisArg (可选)
执行 callbackFn 函数时被用作 this 的值。

返回值

一个新数组,每个元素都是回调函数的返回值。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arr = [
{name:'张三',age:18,sex:'男'},
{name:'李四',age:19,sex:'女'},
{name:'王五',age:20,sex:'男'},
{name:'赵六',age:21,sex:'男'},
]
arr.map( (item,index) => {
return console.log(item,index);
})
/*
{name: '张三', age: 18, sex: '男'} 0
{name: '李四', age: 19, sex: '女'} 1
{name: '王五', age: 20, sex: '男'} 2
{name: '赵六', age: 21, sex: '男'} 3
*/

身份验证

什么是身份验证

身份认证(Authentication)又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
日常生活中的身份认证随处可见,例如:高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
在 Web 开发中,也涉及到用户身份的认证,例如:各大网站的手机验证码登录、邮箱密码登录、二维码登录等。

为什么需要身份验证

身份认证的目的,是为了确认当前所声称为某种身份的用户,确实是所声称的用户。例如,你去找快递员取快递,你要怎么证明这份快递是你的。
在互联网项目开发中,如何对用户的身份进行认证,是一个值得深入探讨的问题。例如,如何才能保证网站不会错误的将“马云的存款数额”显示到“马化腾的账户”上。

不同开发模式下的身份验证

对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
1.服务端渲染推荐使用 Session 认证机制
2.前后端分离推荐使用 JWT 认证机制

Session 认证机制

  1. HTTP 协议的无状态性
    了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提。
    HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态。
    图片1

  2. 如何突破 HTTP 无状态的限制
    对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡。
    图片2
    注意:现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie。

3.什么是 Cookie
Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成。
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。
Cookie的几大特性:
1.自动发送
2.域名独立
3.过期时限
4.4KB 限制

4.Cookie 在身份认证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
图片3

  1. Cookie 不具有安全性
    由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
    图片4
    注意:千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。

  2. 提高身份认证的安全性
    为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有收银机确认存在的会员卡,才能被正常使用。
    图片5
    这种“会员卡 + 刷卡认证”的设计理念,就是 Session 认证机制的精髓。

  3. Session 的工作原理
    图片6

在 Express 中使用 Session 认证

1
2
安装express-session 中间件
npm install express-session
1
2
3
4
5
6
7
8
// 1.导入 session 中间件
const session = require('express-session')
// 2.配置 session中间件
app.use(session({
secret: 'keyboard cat', // secret 属性的值可以为任意字符串
resave: false, // 固定写法
saveUninitialized: true // 固定写法
}))

向session中存数据
当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息:

1
2
3
4
5
6
7
8
9
10
app.post('/api/login',(req,res) => {
console.log(req.body);
// 判断用户提交的登录信息是否正确
if(req.body.username !== 'admin' || req.body.password !== '000000') {
return res.send({status:1,msg:'登录失败!'})
}
req.session.user = req.body //将用户的信息,存储到Session中
req.session.islogin = true //将用户的登录状态,存储到Session中
res.send({status:0,msg:'登录成功'})
})

向session中取数据
可以直接从 req.session 对象上获取之前存储的数据,示例代码如下:

1
2
3
4
5
6
7
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})
})

清空session
调用 req.session.destroy() 函数,即可清空服务器保存的 session 信息。

1
2
3
4
5
6
7
8
9
// 退出登录的接口
app.post('/api/logout',(req,res) => {
// 清空当前客户端对应的 session 信息
req.session.destroy()
res.send({
status:0,
msg:'退出登录成功'
})
})

JWT 认证机制

了解 Session 认证的局限性

Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证。

注意:
当前端请求后端接口不存在跨域问题的时候,推荐使用 Session 身份认证机制。
当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。

什么是 JWT

JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案。

JWT 的工作原理

图片7
总结:用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。

JWT 的组成部分

JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。
三者之间使用英文的“.”分隔,格式如下:

1
Header.payload.signature

下面是 JWT 字符串的示例:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjcyNjM1OTg5LCJleHAiOjE2NzI3MjIzODl9.m3Gff8aWPE9kiRgSzXT3rmwnK9iBXPqfb1mRUf5e8Do

JWT 的三个部分各自代表的含义

JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。
其中:
Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
Header 和 Signature 是安全性相关的部分,只是为了保证 Token 的安全性。
图片8

JWT 的使用方式

客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的 Authorization 字段中,格式如下:

1
Authorization: Bearer <token>

在 Express 中使用 JWT

1
npm install jsonwebtoken express-jwt

其中:
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 1.导入用于生成jwt字符串的包
const jwt = require('jsonwebtoken')
// 2.导入用于解析客户端发送过来的 jwt 字符串,解析还原成 json 对象的包
const expressJWT = require('express-jwt')

// 允许跨域资源共享
const cors = require('cors')
app.use(cors())

// 对post提交的数据进行解析
const bodyParser = require('body-parser')
app.use(bodyParser.urlencoded({ extended: false}))

// 3.定义 secret 密钥
const secretkey = 'itheima'

// 将jwt字符串还原为json对象
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
app.use(expressJWT({ secret:secretkey,algorithms:['HS256']}).unless({ path:[/^\/api\//] }))

// 登录接口
app.post('/api/login',(req,res) => {
const userinfo = req.body
// 登录失败
if(req.body.username !== 'admin' || req.body.password !== '000000') {
return res.send({status:400,msg:'登录失败!'})
}
// 登录成功
// 加密,生成 jwt 字符串,通过 token 发送给客户端
// 调用 jwt.sign()生成 jwt字符串,三个参数分别是:用户信息对象,加密密钥,配置对象
// 千万不要把密码加密到 token 中
const tokenStr = jwt.sign({username:userinfo.username},secretkey,{expiresIn: '24h'})
res.send({
status: 200,
msg: '登录成功!',
token: tokenStr
})
})

// 这是一个有权限的 API 接口
app.get('/admin/getinfo',(req,res) => {
console.log(req.user)
res.send({
status: 200,
msg: '获取用户信息成功',
data: req.user
})
})

// 捕获解析 jwt 失败后产生的错误
app.use((err,req,res,next) => {
// token 解析失败导致的错误
if(err.name === 'UnauthorizedError') {
return res.send({status: 401,msg: '无效的token'})
}
// 其他原因导致的错误
res.send({status: 500,msg:'未知的错误'})

})

MySQL的基本使用

数据库的概念

什么是数据库

数据库(database)是用来组织、存储和管理数据的仓库。
当今世界是一个充满着数据的互联网世界,充斥着大量的数据。数据的来源有很多,比如出行记录、消费记录、浏览的网页、发送的消息等等。除了文本类型的数据,图像、音乐、声音都是数据。
为了方便管理互联网世界中的数据,就有了数据库管理系统的概念(简称:数据库)。用户可以对数据库中的数据进行新增、查询、更新、删除等操作。

常见的数据库及分类

市面上的数据库有很多种,最常见的数据库有如下几个:
1.MySQL 数据库(目前使用最广泛、流行度最高的开源免费数据库;Community + Enterprise)
2.Oracle 数据库(收费)
3.SQL Server 数据库(收费)
4.Mongodb 数据库(Community + Enterprise)

传统型数据库的数据组织结构

数据的组织结构:指的就是数据以什么样的结构进行存储。
传统型数据库的数据组织结构,与 Excel 中数据的组织结构比较类似。
因此,我们可以对比着 Excel 来了解和学习传统型数据库的数据组织结构。

  1. Excel 的数据组织结构
    每个 Excel 中,数据的组织结构分别为工作簿、工作表、数据行、列这 4 大部分组成
    图片1
    1.整个 Excel 叫做工作簿
    2.users 和 books 是工作表
    3.users 工作表中有 3 行数据
    4.每行数据由 6 列信息组成
    5.每列信息都有对应的数据类型

  2. 传统型数据库的数据组织结构
    在传统型数据库中,数据的组织结构分为数据库(database)、数据表(table)、数据行(row)、字段(field)这 4 大部分组成
    1.数据库类似于 Excel 的工作簿
    2.数据表类似于 Excel 的工作表
    3.数据行类似于 Excel 的每一行数据
    4.字段类似于 Excel 的列
    5.每个字段都有对应的数据类型

3.实际开发中库、表、行、字段的关系

1.在实际项目开发中,一般情况下,每个项目都对应独立的数据库。
2.不同的数据,要存储到数据库的不同表中,例如:用户数据存储到 users 表中,图书数据存储到 books 表中。
3.每个表中具体存储哪些信息,由字段来决定,例如:我们可以为 users 表设计 id、username、password 这 3 个字段。
4.表中的行,代表每一条具体的数据。

使用 MySQL Workbench 管理数据库

数据表中的DataType 数据类型和字段的特殊标识
图片2
DataType 数据类型:
1.int 整数
2.varchar(len) 字符串
3.tinyint(1) 布尔值

字段的特殊标识:
1.PK(Primary Key)主键、唯一标识
2.NN(Not Null)值不允许为空
3.UQ(Unique)值唯一
4.AI(Auto Increment)值自动增长

使用SQL管理数据库

1.什么是SQL
SQL(英文全称:Structured Query Language)是结构化查询语言,专门用来访问和处理数据库的编程语言。能够让我们以编程的形式,操作数据库里面的数据。
三个关键点:
1.SQL 是一门数据库编程语言
2.使用 SQL 语言编写出来的代码,叫做 SQL 语句
3.SQL 语言只能在关系型数据库中使用(例如 MySQL、Oracle、SQL Server)。非关系型数据库(例如 Mongodb)不支持 SQL 语言

2.SQL能做什么
1.从数据库中查询数据
2.向数据库中插入新的数据
3.更新数据库中的数据
4.从数据库删除数据
5.可以创建新数据库
6.可在数据库中创建新表
7.可在数据库中创建存储过程、视图
8.etc…

3.SQL的学习目标
重点掌握如何使用 SQL 从数据表中:
查询数据(select) 、插入数据(insert into) 、更新数据(update) 、删除数据(delete)

额外需要掌握的 4 种 SQL 语法:
where 条件、and 和 or 运算符、order by 排序、count(*) 函数
注意:SQL 语句中的关键字对大小写不敏感。

查询数据(select)
1
2
3
4
5
6
-- 从 from指定的[表中],查询出[所有的]数据.*表示[所有列]
select * from 表名称
select * from users
-- 从from指定的[表中],查询出指定 列名称(字段)的数据
select 列名称 from 表名称
select username from users
插入数据(insert into)
1
2
insert into table_name (列1,列2,...) values (值1,值2,...)
insert into users (username,password) values ('tony stark','000000')
更新数据(update)
1
2
3
4
5
6
-- 语法解读:
-- 1.用 update 指定要更新哪个表中的数据
-- 2.用 set 指定列对应的新值
-- 3.用 where 指定更新的条件
update 表名称 set 列名称 = 新值 where 列名称 = 新值
update users set password='888888' where id = 7
删除数据(delete)
1
2
3
-- 从指定的表中,根据 where 条件,删除对应的数据行
delete from 表名称 where 列名称 = 值
delete from users where id = 4
SQL 的 WHERE 子句

WHERE 子句用于限定选择的标准。在 SELECT、UPDATE、DELETE 语句中,皆可使用 WHERE 子句来限定选择的标准。

SQL 的 AND 和 OR 运算符

AND 和 OR 可在 WHERE 子语句中把两个或多个条件结合起来。
AND 表示必须同时满足多个条件,相当于 JavaScript 中的 && 运算符,例如 if (a !== 10 && a !== 20)
OR 表示只要满足任意一个条件即可,相当于 JavaScript 中的 || 运算符,例如 if(a !== 10 || a !== 20)

1
2
select * from users where status=0 and id<3
select * from users where status=1 or username='zs'
SQL 的 ORDER BY 子句

ORDER BY 语句用于根据指定的列对结果集进行排序。
ORDER BY 语句默认按照升序对记录进行排序。
如果您希望按照降序对记录进行排序,可以使用 DESC 关键字。

1
2
3
4
-- order by 默认进行升序排序
-- 其中,asc关键字代表着升序
select * from users order by status;
select * from users order by status asc;
1
2
-- desc关键字代表着降序
select * from users order by id desc

多重排序
对 users 表中的数据,先按照 status 字段进行降序排序,再按照 username 的字母顺序,进行升序排序,示例如下:

1
select * from users order by status desc,username asc
SQL的COUNT(*)函数

COUNT(*) 函数用于返回查询结果的总数据条数,语法格式如下:

1
2
3
4
select count(*) from 表名称
select count(*) from users where status=0
-- 使用AS 为列设置别名
select count(*) as total from users where status=0

在项目中操作mysql

在项目中操作数据库的步骤

1.安装操作 MySQL 数据库的第三方模块(mysql)
2.通过 mysql 模块连接到 MySQL 数据库
3.通过 mysql 模块执行 SQL 语句

1.安装mysql模块

1
npm i mysql

2.配置mysql模块
在使用 mysql 模块操作 MySQL 数据库之前,必须先对 mysql 模块进行必要的配置,主要的配置步骤如下:

1
2
3
4
5
6
7
8
9
// 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.测试mysql模块能否正常工作
调用 db.query() 函数,指定要执行的 SQL 语句,通过回调函数拿到执行的结果:

1
2
3
4
5
db.query('select 1',(err,results) => {
if(err) return console.log(err.message);
// 只要能打印出 [RowDataPacket {'1',1}]的结果,就证明数据库连接正常
console.log(results);
});

查询数据(select)

1
2
3
4
5
6
7
const sqlStr = 'select * from users';
db.query(sqlStr,(err,results) => {
// 查询失败
if(err) return console.log(err.message);
// 查询成功
console.log(results);
});

插入数据(insert into)

1
2
3
4
5
6
7
8
// 1.要插入到users表中的数据对象
const user = {username: 'Spider-Man5',password: 'pcc5'};
// 2.待执行的sql语句,其中英文的?表示占位符
const sqlStr = 'insert into users (username,password) values (?,?)';
db.query(sqlStr,[user.username,user.password],(err,results) => {
if(err) return console.log(err.message); //失败
if(results.affectdRows === 1) {console.log('插入数据成功');} //成功
})

向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据:
快捷方式

1
2
3
4
5
6
const user = {username: 'Spider-Man5',password: 'pcc5'};
const sqlStr = 'INSERT INTO users SET ?';
db.query(sqlStr,user,(err,results) => {
if(err) return console.log(err.message); //失败
if(results.affectdRows === 1) {console.log('插入数据成功');} //成功
})

更新数据(update)

1
2
3
4
5
6
7
8
9
// 1.要更新的数据对象
const user = {id:5,username:'aaa',password:'000'};
// 2.要执行的SQL语句
const sqlStr = 'update users set username=?,password=? where id=?'
// 3.调用 db.query() 执行 sql 语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr,[user.username,user.password,user.id],(err,results) => {
if(err) return console.log(err.message);
if(results.affectedRows === 1) {console.log('更新数据成功!');}
})

更新表数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速更新表数据:

1
2
3
4
5
6
const user = {id:5,username:'aaa',password:'000'};
const sqlStr = 'update users set ? where id=?';
db.query(sqlStr,[user,user.id],(err,results) => {
if(err) return console.log(err.message);
if(results.affectedRows === 1) {console.log('更新数据成功!');}
})

删除数据(delete)

1
2
3
4
5
const sqlStr = 'delete from users where id=?';
db.query(sqlStr,5,(err,results) => {
if(err) return console.log(err.message);
if(results.affectedRows === 1) {console.log('删除数据成功!');}
})

标记删除

使用 DELETE 语句,会把真正的把数据从表中删除掉。为了保险起见,推荐使用标记删除的形式,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于 status 这样的状态字段,来标记当前这条数据是否被删除。
当用户执行了删除的动作时,我们并没有执行 DELETE 语句把数据删除掉,而是执行了 UPDATE 语句,将这条数据对应的 status 字段标记为删除即可。

1
2
3
4
5
const sqlStr = 'update users set status=1 where id=?';
db.query(sqlStr,2,(err,results) => {
if(err) return console.log(err.message);
if(results.affectedRows === 1) {console.log('删除数据成功!');}
})

使用Express写接口

1.创建基本的服务器

1
2
3
4
5
6
7
8
9
10
11
12
// 导入 express模块
const express = require('express');
// 创建 express的服务器实例
const app = express();
// write your code here...
// 调用app.listen方法,指定端口号并启动web服务器
// app.js [导入并注册路由模块]
const router = require('./ApiRouter.js');
app.use('/api',router);
app.listen(80,() => {
console.log('express server running at http://127.0.0.1');
})

2.创建API路由模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require('express');

// ↓↓↓可将其写在服务器模块中,用app.use()注册,这里我写在路由模块
// 导入Express第三方中间件 cors ,解决跨域
const cors = require('cors');
const router = express.Router();
router.use(cors());
router.use(express.urlencoded({extended: false}));
router.use(express.json());
// ↑↑↑可将其写在服务器模块中,用app.use()注册,这里我写在路由模块

router.get('/get',(req,res) => {
const query = req.query;
res.send({
status: 0,
msg: 'GET请求成功',
data: query
})
});
router.post('/post',(req,res) => {
const body = req.body;
res.send({
status: 0,
msg: 'POST请求成功',
data: body
})
});
module.exports = router;

接口的跨域问题

解决接口跨域问题的方案主要有两种:
1.CORS(主流的解决方案,推荐使用)
2.JSONP(有缺陷的解决方案:只支持GET请求)

使用 cors 中间件解决跨域问题

cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题。
使用步骤分为如下 3 步:
1.运行 npm install cors 安装中间件
2.使用 const cors = require(‘cors’) 导入中间件
3.在路由之前调用 app.use(cors()) 配置中间件

什么是CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可以解除浏览器端的跨域访问限制。
图片1 图片2

CORS 的注意事项

1.CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
2.CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。

CORS 响应头部 - Access-Control-Allow-Origin

响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法如下:

1
Access-Control-Allow-Origin: <origin> | *

其中,origin 参数的值指定了允许访问该资源的外域 URL。
如果指定了 Access-Control-Allow-Origin 字段的值为通配符 *,表示允许来自任何域的请求

例如,下面的字段值将只允许来自 http://itcast.cn 的请求:

1
res.setHeader('Access-Control-Allow-Origin','http://itcast.cn');

1
2
const corsOptions = {origin:'http://itcast.com'}
app.get('/get',cors(corsOptions),(req,res) => {...})

CORS 响应头部 - Access-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 对额外的请求头进行声明,否则这次请求会失败!

1
2
3
// 允许客户端额外向服务器发送 Content-Type 请求头和 X-Custom-Header 请求头
// 注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers','Content-Type,X-Custom-Header')

CORS 响应头部 - Access-Control-Allow-Methods

默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods来指明实际请求所允许使用的 HTTP 方法。
示例代码如下:

1
2
3
4
// 只允许 POST、GET、DELETE、HEAD请求方法
res.setHeader('Access-Control-Allow-Methods','POST,GET,DELETE,HEAD');
// 允许所有的HTTP请求方法
res.setHeader('Access-Control-Allow-Methods','*');

CORS请求的分类

客户端在请求 CORS 接口时,根据请求方式和请求头的不同,可以将 CORS 的请求分为两大类,分别是:
1.简单请求
2.预检请求

简单请求
同时满足以下两大条件的请求,就属于简单请求:
1.请求方式:GET、POST、HEAD 三者之一
2.HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

预检请求
只要符合以下任何一个条件的请求,都需要进行预检请求:
1.请求方式为 GET、POST、HEAD 之外的请求 Method 类型
2.请求头中包含自定义头部字段
3.向服务器发送了 application/json 格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。

简单请求和预检请求的区别

简单请求的特点:客户端与服务器之间只会发生一次请求。
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION 预检请求成功之后,才会发起真正的请求。

JSONP接口

JSONP 的概念与特点

概念:浏览器端通过 script 标签的 src 属性,请求服务器上的数据,同时,服务器返回一个函数的调用。这种请求数据的方式叫做 JSONP。
特点:
1.JSONP 不属于真正的 Ajax 请求,因为它没有使用 XMLHttpRequest 这个对象。
2.JSONP 仅支持 GET 请求,不支持 POST、PUT、DELETE 等请求。

创建 JSONP 接口的注意事项

如果项目中已经配置了 CORS 跨域资源共享,为了防止冲突,必须在配置 CORS 中间件之前声明 JSONP 的接口。否则 JSONP 接口会被处理成开启了 CORS 的接口。示例代码如下:

1
2
3
4
5
6
// 优先创建 JSONP接口[这个接口不会被处理成CORS接口]
app.get('/api/jsonp',(req,res) => { });
// 再配置 CORS 中间件[后续的所有接口,都会被处理成 CORS接口]
app.use(cors());
// 这是一个开启了 CORS的接口
app.get('/api/get',(req,res) => { });

实现 JSONP 接口的步骤

1.获取客户端发送过来的回调函数的名字
2.得到要通过 JSONP 形式发送给客户端的数据
3.根据前两步得到的数据,拼接出一个函数调用的字符串
4.把上一步拼接得到的字符串,响应给客户端的 script 标签进行解析执行

实现 JSONP 接口的具体代码

1
2
3
4
5
6
7
8
9
10
app.get('/api/jsonp',(req,res) => {
// 1.获取客户端发送过来的回调函数的名字
const funcName = req.query.callback;
// 2.得到要通过jsonp形式发送给客户端的数据
const data = {name:'zs',age:22};
// 3.根据前两步得到的数据,拼接出一个函数调用的字符串
const scriptStr = `${funcName}(${JSON.stringify(data)})`;
// 4.把上一步拼接得到的字符串,响应给客户端的<script>标签进行解析执行
res.send(scriptStr);
});

在网页中使用 jQuery 发起 JSONP 请求

调用 $.ajax() 函数,提供 JSONP 的配置选项,从而发起 JSONP 请求,示例代码如下:

1
2
3
4
5
6
7
8
9
10
$('.btnJSONP').on('click',() => {
$.ajax({
method: 'GET',
url: 'http://127.0.0.1/api/jsonp',
dataType: 'jsonp',
success: (res) => {
console.log(res);
}
})
})

中间件的概念

什么是中间件

中间件(Middleware),特指业务流程的中间处理环节

Express中间件的调用流程

当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
图片1

Express中间件的格式

Express的中间件,本质上就是一个function处理函数,Express中间件的格式如下:
图片2
注意:中间件函数的形参列表中,必须包含next函数。而路由处理函数中只包含req和res

next函数的作用

next 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由

中间件函数

定义中间件函数

1
2
3
4
5
6
const mv = function(req,res,next) {
console.log('这是一个最简单的中间件函数');
// 注意:在当前中间件的业务处理完毕后,必须调用 next()函数
// 表示把流转关系转交给下一个中间件或路由
next();
}

全局生效的中间件

客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件。
通过调用 app.use(中间件函数),即可定义一个全局生效的中间件,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
const mv = function(req,res,next) {
console.log('这是一个最简单的中间件函数');
next();
}
// 全局生效的中间件
app.use(mv);
// 简写形式
app.use((req,res,next) => {
console.log('这是一个最简单的中间件函数');
next();
})

中间件的作用

多个中间件之间,共享同一份 req 和 res。基于这样的特性,我们可以在上游的中间件中,统一为 req 或 res 对象添加自定义的属性或方法,供下游的中间件或路由进行使用。

图片3

定义多个全局中间件

可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
app.use((req,res,next) => {     // 第1个全局中间件
console.log('调用了第1个全局中间件');
next();
})
app.use((req,res,next) => { // 第2个全局中间件
console.log('调用了第2个全局中间件');
next();
})
app.get('/user',(req,res) => { // 请求这个路由,会依次触发上述两个全局中间件
res.send('Home page')
})

局部生效的中间件

不使用 app.use() 定义的中间件,叫做局部生效的中间件,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义中间件函数 mv1
const mv1 = (req,res,next) => {
console.log('这是一个局部中间件');
next();
};
// mv1 这个中间件只在"当前路由中生效",这种用法属于"局部生效的中间件"
app.get('/',mv1,(req,res) => {
res.send('Home page');
})
// mv1 这个中间件不会影响下面这个路由
app.get('/user',(req,res) => {
res.send('User page')
})

定义多个局部中间件

可以在路由中,通过如下两种等价的方式,使用多个局部中间件:

1
2
3
4
5
6
7
// 以下两种写法是"完全等价"的,可根据自己的喜好,选择任意一种方式进行使用
app.get('/',mv1,mv2,(req,res) => {
res.send('Home page')
});
app.get('/',[mv1,mv2],(req,res) => {
res.send('Home page')
})

了解中间件的5个使用注意事项

1.一定要在路由之前注册中间件
2.客户端发送过来的请求,可以连续调用多个中间件进行处理
3.执行完中间件的业务代码之后,不要忘记调用 next()函数
4.为了防止代码逻辑混乱,调用next()函数后不要再写额外的代码
5.连续调用多个中间件时,多个中间件之间,共享req和res对象

中间件的分类

为了方便大家理解和记忆中间件的使用,Express 官方把常见的中间件用法,分成了 5 大类,分别是:
1.应用级别的中间件
2.路由级别的中间件
3.错误级别的中间件
4.Express 内置的中间件
5.第三方的中间件

1.应用级别的中间件

通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件,叫做应用级别的中间件,代码示例如下:

1
2
3
4
5
6
7
8
// 应用级别的中间件(全局中间件)
app.use((req,res,next) => {
next()
})
// 应用级别的中间件(局部中间件)
app.get('/',mv1,(req,res) => {
res.send('Home page.')
})

2.路由级别的中间件

绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。它的用法和应用级别中间件没有任何区别。只不过,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上,代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
// 创建路由模块
const express = require('express');
// 创建路由对象
const router = express.Router();
// 路由级别的中间件
router.use((req,res,next) => {
console.log('这是一个路由级别的中间件');
next();
})
// 向外导出路由对象
module.exports = router

错误级别的中间件

错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题。
格式:错误级别中间件的 function 处理函数中,必须有 4 个形参,形参顺序从前到后,分别是 (err, req, res, next)

1
2
3
4
5
6
7
8
app.get('/',(req,res) => {                      // 1.路由
throw new Error('服务器内部发生了错误!') // 1.1 抛出一个自定义的错误
res.send('Home page');
})
app.use((err,erq,res,next) => { // 2. 错误级别的中间件
console.log('发生了错误' + err.message); // 2.1 在服务器打印错误消息
res.send('Error!'+ err.message); // 2.2 向客户端响应错误相关的内容
})

注意:错误级别的中间件,必须注册在所有路由之后

Express内置的中间件

自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:

  1. express.static >快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)
  2. express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
  3. express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)
    1
    2
    3
    4
    // 配置解析 application/json 格式数据的内置中间件
    app.use(express.json());
    // 配置解析 application/x-www-form-urlencoded 格式数据的内置中间件
    app.use(express.urlencoded({extended: false}))

第三方的中间件

非 Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。
例如:在 express@4.16.0 之前的版本中,经常使用 body-parser 这个第三方中间件,来解析请求体数据。使用步骤如下:
1.运行 npm install body-parser 安装中间件
2.使用 require 导入中间件
3.调用 app.use() 注册并使用中间件
注意:Express 内置的 express.urlencoded 中间件,就是基于 body-parser 这个第三方中间件进一步封装出来的。

自定义中间件

需求描述与实现步骤
自己手动模拟一个类似于 express.urlencoded 这样的中间件,来解析 POST 提交到服务器的表单数据。
实现步骤:
1.定义中间件
2.监听 req 的 data 事件
3.监听 req 的 end 事件
4.使用 querystring 模块解析请求体数据
5.将解析出来的数据对象挂载为 req.body
6.将自定义中间件封装为模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// custom-body-parser.js 模块中的代码
const qs = require('querystring');
function bodyParser(req,res,next) {
// 中间件的业务逻辑
// 定义变量,用来存储客户端发送过来的请求体数据
let str = '';
// 监听 req对象的data事件(客户端发送过来的新的请求体数据)
req.on('data',(chunk) => {
str += chunk;
});
// 监听req的end事件
req.on('end',() => {
const body = qs.parse(str);
console.log(body);
req.body = body;
next();
})
};
module.exports = bodyParser;
1
2
3
4
// 导入自定义的中间件模块
const myBodyParser = require('./custom-body-parser.js');
// 注册自定义的中间件模块
app.use(myBodyParser);

路由的概念

广义上来说,路由就是映射关系
在Express中,路由指的是客户端的请求与服务器处理函数之间的映射关系
Express中的路由分3部分组成,分别是请求的类型,请求的URL地址、处理函数,格式如下:

1
app.method(path,handler)

在Express中使用路由最简单的方式,就是把路由挂载到app上,示例如下:

1
2
3
4
5
6
7
8
9
10
11
const express = require('express');
const app = express();
app.listen(80,() => {
console.log('express server running at 127.0.0.1');
});
app.get('/',(req,res) => {
res.send('Hello world');
});
app.post('/',(req,res) => {
res.send('post request');
});

路由的模块化

为了方便对路由进行模块化的管理,Express不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块
将路由抽离为单独模块的步骤如下:
1.创建路由模块对应的 .js 文件
2.调用 express.Router() 函数创建路由对象
3.向路由对象上挂载具体的路由
4.使用 module.exports 向外共享路由对象
5.使用 app.use() 函数注册路由模块

创建路由模块

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建路由模块
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

注册路由模块和为路由模块添加前缀

1
2
3
4
5
6
7
8
9
10
11
const express = require('express');
const app = express();
app.listen(80,() => {
console.log('http://127.0.0.1');
})
// 导入路由模块
const userRouter = require('../router/user.js');
// 使用app.use() 注册路由模块
app.use(userRouter);
// 为路由模块添加访问前缀
// app.use('/api',userRouter);

Node.js中的第三方模块又叫做包,不同于Node.js中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用

为什么需要包?

由于Node.js的内置模块仅提供了一些底层的API,导致在基于内置模块进行项目开发时,效率很低。包是基于内置模块封装出来的,提供了更高级,更方便的API,极大的提高了开发效率
包和内置模块之间的关系,类似与jQuery和浏览器内置API之间的关系
全球最大的包共享平台:https://www.npmjs.com/
服务器地址: https://registry.npmjs.org/

如何安装包

npm,Inc.公司提供了一个包管理工具,我们可以使用这个包管理工具,从服务器把需要的包下载到本地使用
这个包管理工具的名字叫做Node Package Manager(简称npm包管理工具),这个包管理工具随着Node.js的安装包一起被安装到了用户的电脑上
在项目中安装包:

npm install 包名
简写:
npm i 包名

初次安装包后多的文件夹

初次安装包完成后,在项目文件夹多一个叫做node_modules的文件夹和package-lock.json的配置文件
其中:
node_modules 文件夹用来存放所有已安装到项目中的包。require() 导入第三方包时,就是从这个目录中查找并加载包。
package-lock.json 配置文件用来记录 node_modules 目录下的每一个包的下载信息,例如包的名字、版本号、下载地址等。

安装指定版本的包

npm install 默认安装最新版本的包。如果需要安装指定版本的包,可以在包名之后,通过@指定具体的版本,列如:

npm i moment@2.22.2

包的语义化版本规范

包的版本号是以“点分十进制”形式进行定义的,总共有三位数字,例如 2.24.0
其中每一位数字所代表的的含义如下:
第1位数字:大版本
第2位数字:功能版本
第3位数字:Bug修复版本

版本号提升的规则:只要前面的版本号增长了,则后面的版本号归零。

包管理配置文件

npm 规定,在项目根目录中,必须提供一个叫做 package.json 的包管理配置文件。用来记录与项目有关的一些配置信息。例如:
项目的名称、版本号、描述等
项目中都用到了哪些包
哪些包只在开发期间会用到
那些包在开发和部署时都需要用到

快速创建package.json

npm init -y
注意:
上述命令只能在英文的目录下成功运行!所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不能出现空格。
运行 npm install 命令安装包的时候,npm 包管理工具会自动把包的名称和版本号,记录到 package.json 中。

一次性安装所有的包

当我们拿到一个剔除了 node_modules 的项目之后(剔除 node_modules是因为体积太大),需要先把所有的包下载到项目中,才能将项目运行起来。
否则会报类似于下面的错误:
Error: Cannot find module ‘moment’
可以运行
npm install(或npm i)
一次性安装所有的依赖包

卸载包

npm uninstall 包名

devDependencies节点

如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到 devDependencies 节点中。
与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到 dependencies 节点中。

npm install 包名 –save-dev
简写:
npm i 包名 -D

解决下包速度慢的问题

切换npm的下包镜像源

1
2
3
4
5
6
# 查看当前的下包镜像源
npm config get registry
# 将下包的镜像源切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org/
# 检查镜像源是否下载成功
npm config get registry

包的分类

1.项目包
那些被安装到项目的 node_modules 目录中的包,都是项目包。
项目包又分为两类,分别是:
开发依赖包(被记录到 devDependencies 节点中的包,只在开发期间会用到)
核心依赖包(被记录到 dependencies 节点中的包,在开发期间和项目上线之后都会用到)
2.全局包
在执行 npm install 命令时,如果提供了 -g 参数,则会把包安装为全局包。
全局包会被安装到 C:\Users\用户目录\AppData\Roaming\npm\node_modules 目录下。
注意:
只有工具性质的包,才有全局安装的必要性。因为它们提供了好用的终端命令。
判断某个包是否需要全局安装后才能使用,可以参考官方提供的使用说明即可。

规范的包结构

一个规范的包,它的组成结构,必须符合以下 3 点要求:
包必须以单独的目录而存在
包的顶级目录下要必须包含 package.json 这个包管理配置文件
package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。

模块的加载机制

优先从缓存中加载

模块在第一次加载后会被缓存。 这也意味着多次调用 require() 不会导致模块的代码被执行多次。
注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率。

内置模块的加载机制

内置模块是由 Node.js 官方提供的模块,内置模块的加载优先级最高。
例如,require(‘fs’) 始终返回内置的 fs 模块,即使在 node_modules 目录下有名字相同的包也叫做 fs。

自定义模块的加载机制

使用 require() 加载自定义模块时,必须指定以 ./ 或 ../ 开头的路径标识符。在加载自定义模块时,如果没有指定 ./ 或 ../ 这样的路径标识符,则 node 会把它当作内置模块或第三方模块进行加载。

同时,在使用 require() 导入自定义模块时,如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下的文件:
按照确切的文件名进行加载
补全 .js 扩展名进行加载
补全 .json 扩展名进行加载
补全 .node 扩展名进行加载
加载失败,终端报错

第三方模块的加载机制

如果传递给 require() 的模块标识符不是一个内置模块,也没有以 ‘./’ 或 ‘../’ 开头,则 Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在 ‘C:\Users\itheima\project\foo.js’ 文件里调用了 require(‘tools’),则 Node.js 会按以下顺序查找:
C:\Users\itheima\project\node_modules\tools
C:\Users\itheima\node_modules\tools
C:\Users\node_modules\tools
C:\node_modules\tools

目录作为模块

当把目录作为模块标识符,传递给 require() 进行加载的时候,有三种加载方式:
在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口
如果目录里没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件。
如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error: Cannot find module ‘xxx’

什么是模块化

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元
编程领域的模块化:
编程领域的模块化,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块。
把代码进行模块化拆分的好处:
1.提高了代码的复用性
2.提高了代码的可维护性
3.可以实现按需加载

Node.js中模块的分类

Node.js中根据模块来源的不同,将模块分为了3大类,分别是:
内置模块(内置模块是由Node.js官方提供的,列如fs、path、http等)
自定义模块(用户创建的每个.js文件,都是自定义模块)
第三方模块(由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载)

1
2
3
4
5
6
// 1.加载内置的fs模块
const fs = require('fs');
// 2.加载用户的自定义模块
const custom = require('./custom.js');
// 3.加载第三方模块
const moment = require('moment');

Node.js中的模块作用域

和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域,模块作用域防止了全局变量污染的问题

向外共享模块作用域中的成员

1.module对象
在每个.js自定义模块中都有一个module对象,它里面存储了和当前模块有关的信息,打印如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Module {
id: '.',
path: 'C:\\Users\\admin\\Desktop\\node.js基础\\day3',
exports: {},
filename: 'C:\\Users\\admin\\Desktop\\node.js基础\\day3\\模块化.js',
loaded: false,
children: [],
paths: [
'C:\\Users\\admin\\Desktop\\node.js基础\\day3\\node_modules',
'C:\\Users\\admin\\Desktop\\node.js基础\\node_modules',
'C:\\Users\\admin\\Desktop\\node_modules',
'C:\\Users\\admin\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules'
]
}

2.module.exports对象
在自定义模块中,可以使用module.exports对象,将模块内的成员共享出去,供外界使用
外界用require()方法导入自定义模块时,得到的就是module.exports所指向的对象

Node.js中的模块化规范

Node.js遵循了CommonJS模块化规范,CommonJS规定了模块的特性和各模块之间如何相互依赖
CommonJS规定:
1.每个模块内部,module变量代表当前模块
2.module变量是一个对象,它的exports属性(即module.exports)是对外的接口
3.加载某个模块,其实是加载该模块的module.exports属性。require()方法用于加载模块