Node.js是一个开源、跨平台的 JavaScript 运行时环境
官网
注:
Node.js 作为一个 JavaScript 的运行环境,仅仅提供了基础的功能和 API。然而,基于 Node.js 提供的这些基础功能之上,很多强大的工具和框架如雨后春笋,层出不穷,所以学会了 Node.js ,可以胜任更多的工作和岗位
如果希望通过 Node.js 来运行 Javascript 代码,则必须在计算机上安装 Node.js 环境才行
官网下载安装即可
区分 LTS 版本和 Current 版本的不同
查看已安装的 Node.js 的版本号
打开终端,在终端输入命令 node –v
后,按下回车键,即可查看已安装的 Node.js 的版本号
nvm 全称 Node Version Manager,顾名思义它是用来管理 node 版本的工具,可以方便地安装&切换不同版本的node
先卸载干净电脑上的node环境,C:\Users\用户\AppData\Roaming\node
下面的文件也要解决干净!并下载nvm
下载地址
安装点击下一步即可
检查安装是否成功,终端输入nvm命令,无误表示安装成功!
几个常用命令:
版本号
:安装指定的node版本版本号
:切换指定node版本版本号
:卸载指定node版本fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求
如果要在 JavaScript 代码中,使用 fs 模块来操作文件,则需要使用如下的方式先导入它:const fs = require('fs')
具体语法:fs.readFile(path[,options],callback)
// 导入 fs 模块,来操作文件
const fs = require('fs')
// 调用 readFile() 方法来读取文件
fs.readFile('./诗词.txt', 'utf8', (err, data) => {
// 失败结果
// 如果读取成功 则失败结果为 null
// 如果读取失败 则失败结果为 错误对象 data的值为 undefined
if (err) {
return console.log('读取文件失败:' + err.message)
}
console.log('读取文件成功,文件内容是:' + data);
})
具体语法:fs.writeFile(file,data[,options],callback)
// 导入 fs 文件系统模块,操作文件
const fs = require('fs')
// 调用 writeFile() 方法,来写入文件
fs.writeFile('./励志语句.txt', '你今天的优势会被明天的趋势所替代', (err) => {
// 如果文件写入成功 则返回 null 反之返回错误对象
if (err) {
return console.log('文件写入失败:' + err.message)
}
console.log("文件写入成功!")
})
在使用 fs 模块操作文件时,如果提供的操作路径是以 ./ 或 …/ 开头的相对路径时,很容易出现路径动态拼接错误的问题
原因:代码在运行的时候,会以执行 node 命令时所处的目录,动态拼接出被操作文件的完整路径
解决方案:在使用 fs 模块操作文件时,直接提供完整的路径,不要提供 ./ 或 …/ 开头的相对路径,从而防止路径动态拼接的问题
const fs = require('fs')
// 会出现动态拼接问题
// fs.readFile('./诗词.txt', 'utf8', (err, data) => {
// if (err) return console.log('读取文件失败:' + err.message)
// console.log('读取文件成功,文件内容为:' + data)
// })
// __dirname 表示当前文件所处的目录
fs.readFile(__dirname + '/诗词.txt', 'utf8', (err, data) => {
if (err) return console.log('读取文件失败:' + err.message)
console.log('读取文件成功,文件内容为:' + data)
})
path 模块是 Node.js 官方提供的、用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求
如果要在 JavaScript 代码中,使用 path 模块来处理路径,则需要使用如下的方式先导入它:const path = require('path')
使用 path.join() 方法,可以把多个路径片段拼接为完整的路径字符串
具体语法:path.join([...paths])
const path = require('path')
// ../ 会抵消前面的路径
let pathStr = path.join('/a', '/b/c', '../', '/d')
console.log(pathStr) // \a\b\d
let filePath = path.join(__dirname, './诗词.txt')
console.log(filePath) // 当前文件所处目录/诗词.txt
使用 path.basename() 方法,可以从一个文件路径中,获取到文件的名称部分
具体语法:path.basename(path[,ext])
const path = require('path')
const url = 'http:localhost:8086/pages/index.html'
let fileName = path.basename(url)
console.log(fileName) // index.html
let noSuffixName = path.basename(url, '.html')
console.log(noSuffixName) // index
使用 path.extname() 方法,可以获取路径中的扩展名部分
const path = require('path')
const url = 'http:localhost:8086/pages/index.html'
let suffix = path.extname(url)
console.log(suffix) // .html
什么是客户端与服务器?
在网络中,负责消费资源的电脑,叫做客户端;负责对外提供网络资源的电脑,叫做服务器
http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。通过 http 模块提供的 http.createServer() 方法,就能方便的把一台普通的电脑,变成一台 Web 服务器,从而对外提供 Web 资源服务
如果希望使用 http 模块创建 Web 服务器,则需要先导入它:const http = require('http')
IP 地址就是互联网上每台计算机的唯一地址,因此 IP 地址具有唯一性,IP 地址的格式:通常用“点分十进制”表示成(a.b.c.d)的形式,其中,a,b,c,d 都是 0~255 之间的十进制整数。例如:192.188.1.1
尽管 IP 地址能够唯一地标记网络上的计算机,但IP地址是一长串数字,不直观,而且不便于记忆,于是人们又发明了另一套字符型的地址方案,即所谓的域名(Domain Name)地址
IP地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器(DNS,Domain name server)的电脑中。使用者只需通过比较好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供 IP 地址和域名之间的转换服务的服务器
注:在开发测试期间, 127.0.0.1 对应的域名是 localhost,它们都代表我们自己的这台电脑,在使用效果上没有任何区别!
在一台电脑中,可以运行成百上千个 web 服务。每个 web 服务都对应一个唯一的端口号。客户端发送过来的网络请求,通过端口号,可以被准确地交给对应的 web 服务进行处理
注:
创建 web 服务器的基本步骤
// 1. 导入 http 模块
const http = require('http')
// 2. 创建 web 服务器实例
const server = http.createServer()
// 3. 为服务器实例绑定 request 事件,监听客户端的请求
server.on('request', (req, resp) => {
console.log('Welcome to my website')
})
// 4. 启动服务器
server.listen(8081, () => {
console.log('server is running at http://127.0.0.1:8081')
})
只要服务器接收到了客户端的请求,就会调用通过 server.on() 为服务器绑定的 request 事件处理函数
如果想在事件处理函数中,访问与客户端相关的数据或属性,可通过如下方式:
const http = require('http')
const server = http.createServer()
// req 是请求对象,包含了与客户端相关的数据和属性
server.on('request', req => {
// req.url 客户端请求的地址
const url = req.url
// req.method 客户端请求的方式/类型
const method = req.method
const str = `Your request url is ${url},and request method is ${method}`
console.log(str)
})
server.listen(80, () => {
console.log('server is running at http://localhost')
})
在服务器的 request 事件处理函数中,如果想访问与服务器相关的数据或属性,可以使用如下的方式:
const http = require('http')
const server = http.createServer()
// req 是请求对象,包含了与客户端相关的数据和属性
server.on('request', (req, resp) => {
// req.url 客户端请求的地址
const url = req.url
// req.method 客户端请求的方式/类型
const method = req.method
const str = `Your request url is ${url},and request method is ${method}`
console.log(str)
// 调用 resp end()方法 向客户端响应一些内容
resp.end(str)
})
server.listen(80, () => {
console.log('server is running at http://localhost')
})
当调用 res.end() 方法,向客户端发送中文内容的时候,会出现乱码问题,此时,需要手动设置内容的编码格式:
const http = require('http')
const server = http.createServer()
server.on('request', (req, resp) => {
// 定义字符串 包含中文
const str = `你请求的 URL 地址是 ${req.url},请求的方式是${req.method}`
// 设置 Content-Type 响应头 解决中文乱码问题
resp.setHeader('Content-Type', 'text/html;charset=utf8')
resp.end(str)
})
server.listen(80, () => {
console.log('server is running at http://localhost')
})
模块化,就是遵守固定的规则,把一个大文件拆分成独立并互相依赖的多个小模块
把代码进行模块化拆分的好处:
模块化规范就是对代码进行模块化的拆分与组合时,需要遵守的那些规则
模块化规范的好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用, 利人利己
Node.js 中根据模块来源的不同,将模块分为了 3 大类,分别是:
使用强大的 require() 方法,可以加载需要的内置模块、用户自定义模块、第三方模块进行使用
具体语法:const 自定义名称 = require(模块名称)
注:使用 require() 方法加载其它模块时,会执行被加载模块中的代码!
和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域
模块作用域的好处:防止了全局变量污染的问题!
在每个 .js 自定义模块中都有一个 module 对象,它里面存储了当前模块有关的信息
在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用
外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指向的对象
// 向 module.exports 对象上 挂载 username 属性
module.exports.username = 'admin'
module.exports.sayHello = () => {
console.log(Hello)
}
注:使用 require() 方法导入模块时,导入的结果,永远以 module.exports 指向的对象为准
由于 module.exports 单词写起来比较复杂,为了简化向外共享成员的代码,Node 提供了 exports 对象。默认情况 下,exports 和 module.exports 指向同一个对象。最终共享的结果,还是以 module.exports 指向的对象为准
console.log(module.exports)
console.log(exports)
console.log(module.exports === exports)
exports.username = '小昭'
exports.sayHi = () => {
console.log('Hi')
}
Node.js 遵循了 CommonJS 模块化规范,CommonJS 规定了模块的特性和各模块之间如何相互依赖
CommonJS 规定:
Node.js 中的第三方模块又叫做包
注:Node.js 中的包都是免费且开源的,不需要付费即可免费下载使用
由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发时,效率很低
包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大的提高了开发效率
国外有一家 IT 公司,叫做 npm, Inc,这家公司旗下有一个非常著名的包共享平台网站
网站搜索需要的包
服务器下载需要的包
npm, Inc公司提供了一个包管理工具,可以通过这个包管理工具,把需要的包下载到本地使用
包管理工具的名字叫做 Node Package Manager(简称 npm 包管理工具),这个包管理工具会随着 Node.js 的安装包一起被安装到了用户的电脑上
如果想在项目中安装指定名称的包,需要运行以下命令
具体语法:npm install 包的完整名称 / npm i 包的完整名称
,安装多个包使用空格隔开:npm i jquery bootstrap layui
// 导入需要的包
const moment = require('moment')
const dt = moment().format('YYYY-MM-DD HH:mm:ss')
console.log(dt)
初次装包完成后,在项目文件夹下多一个叫做 node_modules 的文件夹和 package-lock.json 的配置文件
注:请尽量不要手动修改 node_modules 或 package-lock.json 文件中的任何代码,npm 包管理工具会自动维护它们
默认情况下,使用 npm install 命令安装包的时候,会自动安装最新版本的包。如果需要安装指定版本的包,可以在包名之后,通过 @ 符号指定具体的版本
列出指定包的版本号:npm view 包名 versions
具体语法:npm i 包的名称@版本号
包的版本号是以“点分十进制”形式进行定义的,总共有三位数字,例如2.29.3
每位数字代表含义:
版本号提升规则:只要前面的版本号增长了,则后面的版本号归零
npm 规定,在项目根目录中,必须提供一个叫做 package.json 的包管理配置文件。用来记录与项目有关的一些配置信息
在项目根目录中,创建一个叫做 package.json 的配置文件,即可用来记录项目中安装了哪些包。从而方便剔除 node_modules 目录之后,在团队成员之间共享项目的源代码
注:在项目开发中,一定要把 node_modules 文件夹,添加到 .gitignore 忽略文件中
npm 包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建 package.json 这个包管理
具体语法:npm init -y
注:项目文件夹的名称一定要使用英文命名,否则会出问题,所以请不要使用中文且不能出现空格!
在package.json 文件中,dependencies 节点,专门用来记录使用 npm install 命令安装了哪些包
一次性安装所有的依赖包:npm install / npm i
卸载指定的包:npm uninstall 具体包名
注:npm uninstall 命令执行成功后,会把卸载的包,自动从 package.json 的 dependencies 中移除掉
如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到 devDependencies 节点中
与之对应的,如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到 dependencies 节点中
具体语法:npm i 包名 --save-dev / npm i 包名 -D
为什么下包速度慢?
在使用 npm 下包的时候,默认从国外的服务器进行下载,此时,网络数据的传输需要经过漫长的海底光缆,因此下包速度会很慢
淘宝 NPM 镜像服务器
淘宝在国内搭建了一个服务器,专门把国外官方服务器上的包同步到国内的服务器,然后在国内提供下包的服务。 从而极大的提高了下包的速度
镜像(Mirroring)是一种文件存储形式,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本即为镜像
切换 npm 的下包镜像源
下包的镜像源,指的就是下包的服务器地址
# 查看当前的下包镜像源
npm config get registry
# 将下包的镜像源切换为淘宝镜像源
npm config set registry=https://registry.npm.taobao.org/
# 检查镜像源是否下载成功
npm config get registry
使用 npm 包管理工具下载的包,共分为两大类,分别是:
被安装到项目的 node_modules 目录中的包,都是项目包
项目包又分为两类,分别是:
全局包
在执行 npm install 命令时,如果提供了 -g 参数,则会把包安装为全局包
全局包会被安装到C:\Users\用户目录\AppData\Roaming\npm\node_modules
目录下
npm i 包名 -g
npm uninstall 包名 -g
注:
模块在第一次加载后会被缓存。 这也意味着多次调用 require() 不会导致模块的代码被执行多次
注:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存中加载,从而提高模块的加载效率
内置模块是由 Node.js 官方提供的模块,内置模块的加载优先级最高
使用 require() 加载自定义模块时,必须指定以 ./ 或 …/ 开头的路径标识符
在加载自定义模块时,如果没有指定 ./ 或 …/ 这样的路径标识符,则 node 会把它当作内置模块或第三方模块进行加载
在使用 require() 导入自定义模块时,如果省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下的文件:
如果加载的不是内置模块也不是自定义模块,则 Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录
Express 是基于 Node.js 平台,快速、开放、极简的 Web 开发框架
简单理解:Express 的作用和 Node.js 内置的 http 模块类似,是专门用来创建 Web 服务器的
Express 的本质:就是一个 npm 上的第三方包,提供了快速创建 Web 服务器的便捷方法
Express 是基于内置的 http 模块进一步封装出来的,能够极大的提高开发效率
Express 中文官网
最常见的两种服务器,分别是:
使用 Express,可以方便、快速的创建 Web 网站的服务器或 API 接口的服务器
在项目所处的目录中,运行如下的终端命令,即可将 express 安装到项目中使用:npm i [email protected]
// 导入 express
const express = require('express')
// 创建 web 服务器
const app = express()
// 启动 web 服务器
app.listen(80, () => {
console.log('express server is running at http://localhost')
})
通过 app.get() 方法,可以监听客户端的 GET 请求
通过 app.post() 方法,可以监听客户端的 POST 请求
通过 res.send() 方法,可以把处理好的内容,发送给客户端
获取 URL 中携带的查询参数,通过 req.query 对象,可以访问到客户端通过查询字符串的形式,发送到服务器的参数
获取 URL 中的动态参数,通过 req.params 对象,可以访问到 URL 中,通过:
匹配到的动态参数
// 导入 express
const express = require('express')
// 创建 web 服务器
const app = express()
// 监听客户端的 GET 请求和 POST 请求,并向客户端响应具体的内容
app.get('/goods', (req, resp) => {
// 调用 express 提供的 resp.send() 方法,向客户端响应一个JSON对象
resp.send({ "gname": "小米手机", "price": 1999, "detail": "有点狠的真旗舰" })
})
// 创建路由规则 (挂载路由)
app.post('/goods', (req, resp) => {
// 调用 express 提供的 resp.send() 方法,向客户端响应一个文本字符串
resp.send('请求成功!')
})
// 创建路由规则
app.get('/', (req, resp) => {
// 通过 req.query 可以获取到客户端发送过来的 查询参数
// 默认情况下 req.query 为空对象
console.log(req.query)
resp.send(req.query)
})
// 创建路由规则
app.get('/goods/:gid/:price', (req, resp) => {
// req.params 是动态匹配的 URL 参数,默认也是一个空对象
console.log(req.params)
resp.send(req.params)
})
// 启动 web 服务器
app.listen(80, () => {
console.log('express server is running at http://localhost')
})
express 提供了一个非常好用的函数,叫做 express.static(),通过它,可以非常方便地创建一个静态资源服务器
通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问
如果要托管多个静态资源目录,请多次调用 express.static() 函数:
// 调用 express.static() 方法,快速对外提供静态资源
app.use(express.static('./public'))
app.use(express.static('./files'))
注:访问静态资源文件时,express.static() 函数会根据目录的添加顺序查找所需的文件
如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:
app.use('/public', express.static('public'))
app.use(express.static('./files'))
在编写调试 Node.js 项目的时候,如果修改了项目的代码,则需要频繁的手动 close 掉,然后再重新启动,非常繁琐
可以使用 nodemon 这个工具,它能够监听项目文件的变动,当代码被修改后,nodemon 会自动帮我们重启项目,极大地方便了开发和调试
安装地址
将 nodemon 安装为全局可用的工具:npm install -g nodemon
修改 windows 执行策略
windows 默认不允许 npm 全局命令执行脚本文件,所以需要修改执行策略
以 管理员身份 打开 powershell 命令行
输入命令:set-ExecutionPolicy remoteSigned
传统的方式,是运行 node app.js 命令来启动项目。这样做的坏处是: 代码被修改之后,每次都需要手动重启项目
现在,可以将 node 命令替换为 nodemon 命令,使用 nodemon app.js 来启动项目
这样做的好处是:代码被修改之后,会被 nodemon 监听到,从而实现自动重启项目的效果
nodemon .\index.js
什么是路由?广义上来讲,路由就是映射关系
在 Express 中,路由指的是客户端的请求与服务器处理函数之间的映射关系
Express 中的路由分 3 部分组成,分别是请求的类型、请求的 URL 地址、处理函数,具体格式:app.METHOD(PATH,HADNLER)
// 匹配 POST 请求 且请求路径为 /login
app.post('/login', (req, res) => {
res.send('登录成功');
});
每当一个请求到达服务器之后,需要先经过路由的匹配,如果请求类型和请求的 URL 同时匹配成功,才会调用对应的处理函数
路由匹配注意点:
为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块
// 导入 express 模块
const express = require('express')
// 调用 express.Router() 函数创建路由对象
const router = express.Router()
// 向路由对象上挂载具体的路由
router.get('/user', (req, resp) => {
resp.send('查询用户成功')
})
router.post('/user', (req, resp) => {
resp.send('添加用户成功')
})
// exports 向外导入路由对象
module.exports = router
// 导入路由模块
const userRouter = require('./模块化路由')
// 注册路由模块
app.use(userRouter)
类似于托管静态资源时,为静态资源统一挂载访问前缀一样
// 导入路由模块
const userRouter = require('./模块化路由')
// 注册路由模块
app.use('/api', userRouter)
何为中间件?中间件(Middleware ),特指业务流程的中间处理环节
一个请求到达 Express 的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
Express 的中间件,本质上就是一个 function 处理函数,Express 中间件的格式:app.get('/',(req,resp,next)) { next() }
注:中间件函数的形参列表中,必须包含 next 参数。而路由处理函数中只包含 req 和 resp
next 函数是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由
客户端发起的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件
通过调用 app.use(中间件函数),即可定义一个全局生效的中间件
const express = require('express');
const app = express()
app.use((req, resp, next) => {
// 获取请求到达服务器的时间
const time = Date.now();
// 为 req 挂载自定义属性,从而把时间共享给后面的所有路由
req.startTime = time
// 把流转关系,转交给下一个中间件或路由
next()
})
app.get('/', (req, resp) => {
console.log('调用了 / 路由')
resp.send('Home page')
})
app.get('/login', (req, resp) => {
console.log('调用了 /login 路由')
resp.send('Login page')
})
app.listen(80, () => {
console.log('express is running at http://localhost')
})
中间件的作用:多个中间件之间,共享同一份 req 和 resp。基于这样的特性,可以在上游的中间件中,统一为 req 或 resp 对象添加自定义的属性或方法,供下游的中间件或路由进行使用
可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用
const express = require("express");
const app = express()
// 定义第一个全局中间件
app.use((req, resp, next) => {
console.log('调用了第一个全局中间件')
next()
})
// 定义第二个全局中间件
app.use((req, resp, next) => {
console.log('调用了第二个全局中间件')
next()
})
// 创建路由
app.get('/goods', (req, resp) => {
resp.send('查询商品成功')
})
app.listen(80, () => {
console.log('express is running at http://localhost')
})
不使用 app.use() 定义的中间件,叫做局部生效的中间件
const express = require('express')
const app = express()
// 定义中间件函数
const mw1 = (req, resp, next) => {
console.log('调用了局部生效的中间件函数')
next()
}
// 创建路由
app.get('/user/:id', mw1, (req, resp) => {
resp.send(`获取id为${req.params.id}号的用户信息成功`)
})
app.listen(80, () => {
console.log('express is running at http://localhost')
})
可以在路由中,通过如下两种等价的方式,使用多个局部中间件
app.get('/user/:id', mw1, mw2, (req, resp) => { resp.send(`获取id为${req.params.id}号的用户信息成功`) })
app.get('/user/:id', [mw1, mw2], (req, resp) => { resp.send(`获取id为${req.params.id}号的用户信息成功`) })
注:
Express 官方把常见的中间件用法,分成了 5 大类,分别是:
应用级别的中间件:通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件,叫做应用级别的中间件
绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上
错误级别的中间件:错误级别中间件的作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
错误级别中间件的 function 处理函数中,必须有 4 个形参,形参顺序从前到后,分别是(err, req, res, next)
const express = require('express')
const app = express()
// 创建路由
app.get('/user', (req, resp) => {
// 人为制造错误
throw new Error('服务器内部发生错误!')
resp.send('查询用户信息成功')
})
// 定义全局错误级别的中间件 捕获整个项目的异常
app.use((err, req, resp, next) => {
console.log('发生了错误:' + err.message)
resp.send('Error:' + err.message)
})
app.listen(8080, () => {
console.log('express is running at http://localhost:8080')
})
注:错误级别的中间件, 必须注册在所有路由之后!其他中间件反之(路由之前)
Express内置的中间件
自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验
const express = require('express')
const app = express()
// 除错误级别的中间件 其他的中间件 必须在路由之前进行配置
// 通过 express.json() 这个中间件 来解析表单中 JSON 格式的数据
app.use(express.json())
// 通过 express.urlencoded() 这个中间件 来解析表单中 url-encoded 格式的数据
app.use(express.urlencoded({ extended: false }))
app.post('/user', (req, resp) => {
// 使用 req.body 这个属性 来接收客户端发送过来的请求体数据
// 默认情况下 如果不配置解析表单数据的中间件 则 req.body 默认为 undefined
console.log(req.body)
resp.send('ok')
})
app.post('/goods', (req, resp) => {
console.log(req.body)
resp.send('ok')
})
// 指定端口号 并启动 web 服务器
app.listen(8086, () => {
console.log('Express server is running at http://127.0.0.1:8086')
})
第三方的中间件:非 Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置 第三方中间件,从而提高项目的开发效率
具体使用步骤:
创建基本的服务器
// 导入 express 模块
const express = require('express')
// 导入 cors 模块
const cors = require('cors')
// 导入路由模块
const router = require('./apiRouter')
// 创建 express 服务器实例
const app = express()
// 配置解析表单数据的中间件
app.use(express.urlencoded({ extended: false }))
// 在路由之前配置cors中间件 从而解决接口跨域问题
app.use(cors())
// 把路由模块注册到 app 上
app.use('/api', router)
// 指定端口号 并启动 web 服务器
app.listen(80, () => {
console.log('Express server is running at http://127.0.0.1')
})
创建 API 路由模块
const express = require('express')
const router = express.Router()
module.exports = router
路由模块编写接口
// 挂载路由
router.get('/get', (req, resp) => {
// 获取到客户端通过查询字符串 发送到服务器的数据
const query = req.query
// 调用 resp.send() 方法,向客户端响应数据
resp.send({
status: 1, // 1 表示处理成功 0 表示处理失败
msg: 'GET 请求成功', // 状态描述
data: query // 响应给客户端的数据
})
})
router.post('/post', (req, resp) => {
// 获取客户端通过请求体 发送到服务器的 url-encoded 数据
const body = req.body
resp.send({
status: 1,
msg: 'POST 请求成功',
data: body
})
})
解决接口跨域问题的方案主要有两种:
cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题
npm i [email protected]
安装中间件const cors = require('cors')
导入中间件app.use(cors())
配置中间件CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头, 就可以解除浏览器端的跨域访问限制
注:
Access-Control-Allow-Origin
响应头部中可以携带一个 Access-Control-Allow-Origin 字段,其语法为: Access-Control-Allow-Origin: origin |*
其中,origin 参数的值指定了允许访问该资源的外域 URL
如果指定了 Access-Control-Allow-Origin 字段的值为*通配符 ,表示允许来自任何域的请求
只允许该地址进行跨域访问:resp.setHeader('Access-Control-Allow-Origin', 'http:www.baidu.com')
任何地址都可以跨域访问:resp.setHeader('Access-Control-Allow-Origin', '*')
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 对额外的请求头进行声明,否则这次请求会失败!
// 只允许 GET、POST、DELETE、PUT 请求方法
resp.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,PUT')
// 允许所有的 HTTP 请求方法
resp.setHeader('Access-Control-Allow-Methods', '*')
Access-Control-Allow-Methods
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法
// 允许客户端额外向服务器发送 Content-Type 请求体 和 Customer-Header
// 多个请求之前使用 , 英文逗号分割
resp.setHeader('Access-Control-Allow-Headers', 'Content-Type,Customer-Header')
同时满足以下两大条件的请求,就属于简单请求:
只要符合以下任何一个条件的请求,都需要进行预检请求:
在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据
简单请求和预检请求的区别
目前主流的Web开发模式有两种,分别是:
服务端渲染:服务器发送给客户端的HTML页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端是不需要使用Ajax这样的技术额外请求页面的数据
优点:
缺点:
前后端分离:前后端分离的开发模式,依赖于Ajax技术的广泛应用。简而言之,前后端分离的Web开发模式,就是后端只负责提供API接口,前端使用Ajax调用接口的开发模式
优点:
缺点:
如何选择Web开发模式?
不谈业务场景而盲目选择使用何种开发模式都是耍流氓
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染+其他页面前后端分离的开发模式
身份认证(Authentication) 又称"身份验证”、“鉴权” ,是指通过一定的手段,完成对用户身份的确认
对于服务端渲染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案
HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,即服务器不会主动保留每次 HTTP 请求的状态
如何突破 HTTP 无状态的限制
注:现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie
Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成
不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器
Cookie几大特性:
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动 将 Cookie 保存在浏览器中
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份
由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器
注:千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等
npm i express-session
const session = require('express-session')
当 express-session 中间件配置成功后,即可通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息
// 导入 express 模块
const express = require('express')
// 创建 express 服务器实例
const app = express()
// 导入 session 中间件
const session = require('express-session')
// 配置 session 中间件
app.use(
session({
secret: 'Mrzhang',
resave: false,
saveUninitialized: true,
})
)
// 配置解析表单的中间件
app.use(express.json())
// 挂载静态资源
app.use(express.static('./pages'))
// 登录 API 接口
app.post('/api/login', (req, resp) => {
// console.log(req.body)
const user = req.body
// 判断用户是否登录成功
if (user.username !== 'admin' || user.password !== '8848') return resp.send({ status: 1, msg: '登录失败,用户名或密码错误' })
// 只有配置了 session 中间件,才能通过req对象获得 session 这个属性
req.session.userInfo = user // 将用户信息,存储到 session 中
req.session.isLogin = true // 将用户登录状态,存储到 session 中
resp.send({ status: 0, msg: '登录成功!' })
})
// 获取用户名接口
app.get('/api/username', (req, resp) => {
// 判断用户是否登录
if (!req.session.isLogin) return resp.send({ status: 1, msg: "你还没有登录!" })
// 获取 session
resp.send({ status: 0, msg: 'success', username: req.session.userInfo.username })
})
// 退出登录接口
app.post('/api/logout', (req, resp) => {
// 清空 session 信息
req.session.destroy()
resp.send({ status: 0, msg: "退出登录成功!" })
})
// 指定端口号 并启动 web 服务器
app.listen(80, () => {
console.log('Express server is running at http://127.0.0.1')
})
Session 认证机制需要配合 Cookie 才能实现。由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证
注:
JWT(英文全称:JSON Web Token)是目前最流行的跨域认证解决方案
用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份
JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)
三者之间使用英文的“.”分隔,具体格式:Header.PayLoad.Signature
三个组成部分各自代表的含义
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是**把 JWT 放在 HTTP 请求头的 Authorization 字段中,**具体格式:Authorization:token
安装 JWT 相关的包,具体命令:npm i [email protected] [email protected]
导入 JWT 相关的包
使用 require() 函数,分别导入 JWT 相关的两个包:
// 导入用于生成 JWT 字符串的包
const jwt = require('jsonwebtoken')
// 导入用于将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包
const expressJWT = require('express-jwt')
定义 secret 密钥
为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,需要专门定义一个用于加密和解密 的 secret 密钥:
// 定义 secret 秘钥,建议将秘钥命名为 secretKey
const secretKey = 'Mrzhang ^_^'
将 JWT 字符串还原为 JSON 对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证。 此时,服务器可以通过 express-jwt 这个中间件,自动将客户端发送过来的 Token 解析还原成 JSON 对象
使用 req.user 获取用户信息
express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.user
对象,来访问从 JWT 字符串中解析出来的用户信息
当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理
// 导入 express 模块
const express = require('express')
// 导入用于生成 JWT 字符串的包
const jwt = require('jsonwebtoken')
// 导入用于将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包
var expressjwt = require("express-jwt")
// 创建 express 服务器实例
const app = express()
// 解析客户端发送过来的 JSON 数据
app.use(express.json())
// 定义 secret 秘钥,建议将秘钥命名为 secretKey
const secretKey = 'Mrzhang ^_^'
// 注册将 JWT 字符串解析还原成 JSON 对象的中间件 会把解析出来的用户信息 挂载到 req.user 属性上
// expressJWT({secretKey}) 用来解析 Token 的中间件
// .unless({path:[]}) 指定那些接口不需要访问权限
app.use(expressjwt({ secret: secretKey, algorithms: ["HS256"] }).unless({ path: [/^\/api\//] }))
// 登录 API 接口
app.post('/api/login', (req, resp) => {
const user = req.body
if (user.username !== 'admin' || user.password !== '8888') return resp.send({ status: 400, msg: '用户名或密码错误' })
// 用户登录成功之后,生成 JWT 字符串,通过 token 属性响应给客户端
// 调用 jwt.sign() 生成 JWT 字符串,三个参数分别是:用户信息对象、加密秘钥、配置对象
// 一定不要把密码加密到 Token 中
const tokenStr = jwt.sign({ username: user.username }, secretKey, { expiresIn: '360s' })
resp.send({ status: 200, msg: "登录成功", token: 'Bearer ' + tokenStr })
})
// 有权限的接口
app.get('/admin/user', (req, resp) => {
// 使用 req.user 获取用户信息 并使用 data属性将用户信息响应给客户端
console.log(req.user)
resp.send({
status: 200,
msg: '获取用户信息成功',
data: req.user
})
})
// 声明全局错误中间件 捕获解析 JWT 失败后产生的错误 注:声明到路由之后
app.use((err, req, resp, next) => {
// token 解析失败导致的
if (err.name === 'UnauthorizedError') {
return resp.send({
status: 401,
msg: 'token 无效'
})
}
// 其他原因导致的错误
resp.send({ status: 500, msg: '未知错误' })
})
// 指定端口号 并启动 web 服务器
app.listen(80, () => {
console.log('Express server is running at http://127.0.0.1')
})
node.js如何实现模块化?
node.js 遵循了 CommonJS 的模块化规范。其中:
require()
方法module.exports
对象模块化的好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本,极大方便了各个模块之间的相互调用,利人利己
在 ES6 模块化规范诞生之前,JavaScript 社区已经尝试并提出了 AMD、CMD、CommonJS 等模块化规范
但是,这些由社区提出的模块化标准,还是存在一定的差异性与局限性、并不是浏览器与服务器通用的模块化标准,例如:
太多的模块化规范给开发者增加了学习的难度与开发的成本。因此,大一统的 ES6 模块化规范诞生了!
ES6 模块化规范是浏览器端与服务器端通用的模块化开发规范。它的出现极大的降低了前端开发者的模块化学习成本,开发者不需再额外学习 AMD、CMD 或 CommonJS 等模块化规范
ES6 模块化规范中定义:
import
关键字export
关键字ES6 的模块化主要包含如下 3 种用法:
具体语法: export default 默认导出的成员
let x = 30
let y = 60
function sayHello(){
console.log('say Hi')
}
let arr = [
{
'uname':'admin',
'uage':18,
'uemail':'[email protected]'
}
]
// 默认导出
export default {
x,sayHello,arr
}
注:每个模块中,只允许使用唯一的一次 export default,否则会报错!
具体语法: import 接收名称 from ‘模块标识符’
import myData from './01_defaultExport.js'
/**
* 打印结果为:
* {
x: 30,
sayHello: [Function: sayHello],
arr: [ { uname: 'admin', uage: 18, uemail: '[email protected]' } ]
}
*/
console.log(myData)
// 单个数据输出
console.log(myData.x)
console.log(myData.arr)
注:默认导入时的接收名称可以任意名称,只要是合法的成员名称即可!
具体语法: export 按需导出的成员
let x = 30
let y = 60
function sayHello(){
console.log('say Hi')
}
let arr = [
{
'uname':'admin',
'uage':18,
'uemail':'[email protected]'
}
]
// 按需导出
export { y, arr }
具体语法: import { 需要导入的成员 } from ‘模块标识符’
import { y, arr as userInfo } from './02_needExport.js'
/**
* 输出结果为:
* 60 [ { uname: 'admin', uage: 18, uemail: '[email protected]' } ]
*/
console.log(y, userInfo)
注:
如果只想单纯地执行某个模块中的代码,并不需要得到模块中向外共享的成员。此时,可以直接导入并执行模块代码
// 03_directExport.js
let x = 30
let y = 60
console.log(x, y)
for (let i = 0; i < 5; i++) {
console.log(i)
}
// ------------------
// 直接导入并执行该模块代码,不需要得到模块向外共享的成员
import './03_directExport.js'
async/await 是 ES8(ECMAScript 2017)引入的新语法,用来简化 Promise 异步操作。在 async/await 出现之前,开发者只能通过链式 .then() 的方式处理 Promise 异步操作
then 链式调用的优点: 解决了回调地狱的问题
then 链式调用的缺点: 代码冗余、阅读性差、 不易理解
import thenFs from 'then-fs'
async function getFiles(){
const f1 = await thenFs.readFile('./files/1.txt','utf8')
console.log(f1)
const f2 = await thenFs.readFile('./files/2.txt','utf8')
console.log(f2)
const f3 = await thenFs.readFile('./files/3.txt','utf8')
console.log(f3)
}
getFiles()
注:
JavaScript 是一门单线程执行的编程语言。也就是说,同一时间只能做一件事情
单线程执行任务队列的问题: 如果前一个任务非常耗时,则后续的任务就不得不一直等待,从而导致程序假死的问题
为了防止某个耗时任务导致程序假死的问题,JavaScript 把待执行的任务分为了两类:
JavaScript 主线程从“任务队列”中读取异步任务的回调函数,放到执行栈中依次执行。由于这个过程是循环不断的,所以整个的这种运行机制又称为 EventLoop(事件循环)
JavaScript 把异步任务又做了进一步的划分,异步任务又分为两类,分别是:
宏任务和微任务的执行顺序
每一个宏任务执行完之后,都会检查是否存在待执行的微任务
如果有,则执行完所有微任务之后,再继续执行下一个宏任务