Node.js学习笔记
Node.js是一个跨平台JavaScript运行环境,使开发者可以搭建服务器端的JavaScript应用程序。作用:使用Node.js编写服务器端程序。编写数据接口,提供网页资源浏览功能等前端工程化:为后续学习Vue和React等框架做铺垫。前端工程化就是指开发项目直到上线,过程中集成的所有工具和技术。Node.js可以主动读取前端代码内容。
Node.js
目录
学习视频:
什么是Node.js?
Node.js
是一个跨平台JavaScript运行环境,使开发者可以搭建服务器端的JavaScript应用程序。
作用:使用Node.js
编写服务器端程序。
- 编写数据接口,提供网页资源浏览功能等
- 前端工程化:为后续学习
Vue
和React
等框架做铺垫。
前端工程化就是指开发项目直到上线,过程中集成的所有工具和技术。
Node.js
可以主动读取前端代码内容。
Node.js为何能执行JS?
浏览器执行JS代码依靠的是内核中的V8引擎(C++程序),而Node.js是基于Chrome V8引擎进行封装(运行环境)。
二者都支持ECMAScript标准语法,但是Node.js有独立的API。
Node.js环境没有DOM和BOM等。
使用Node.js
新建JS文件编写完代码后,在终端输入node js文件路径
,按下回车执行。
fs模块
fs 文件系统 | Node.js v20 文档 (nodejs.cn)
-
加载fs模块:
const fs = require('fs')
-
写入文件内容:
fs.writerFile('文件路径', '写入内容', err ⇒ { 写入后的回调函数 })
写入内容会覆盖文件原有的内容。
-
读取文件内容:
fs.readFile('文件路径', '编码格式', (err, data) => {读取后的回调函数})
data 是文件内容的 Buffer 数据流,看具体内容要写 data.toString()。
-
例子:
const fs = require('fs') fs.readFile('./try.txt', 'utf-8', (err, data) => { console.log(data.toString()) }) fs.writeFile('./try.txt', 'Welcome', err=>{ console.log(err) })
path模块
path 路径 | Node.js v20 文档 (nodejs.cn)
可以得到文件的绝对路径。
-
加载path模块:
const path = require('path')
-
获取文件名:
path.basename('文件路径')
window或者posix上运行结果不一样,可以指定系统得到一致的结果:
path.win32.basename('win系统的文件路径')
/path.posix.basename('posix系统的文件路径')
-
获取路径目录:
path.dirname('文件路径')
-
获取绝对路径:
path.join()
配合内置变量__dirname
:path.join(__dirname, '.', 'try.txt')
-
例子:
const path = require('path') console.log(__dirname) const result = path.join(__dirname, '.', 'try.txt') console.log(result)
http模块
可以用来创建web服务器。
每台web服务器都会有自己的IP地址,IP不好记,有了域名,IP和域名之间通过域名服务器(DNS)进行转换。
http协议默认端口号是80(会省略)。
-
加载http模块:
const http = require('http')
-
创建web服务器实例:
const server = http.createServer()
-
为服务器实例绑定事件:
server.on('request',(req, res) ⇒ { })
req.url
是客户端请求的URL地址,req.method是客户端的method请求类型。res.end( )
可以向客户端发送指定的内容(响应内容),并结束这次请求的过程。如果中文乱码了,可以设置res.setHeader('Content-Type', 'text/html; charset=utf-8' )
。 -
启动服务器:
server.listen(端口号, ( ) ⇒ { })
根据不同的url响应不同的内容:
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
const url = req.url
let content = '<h1>404 not found</h1>'
if (url === '/' || url === '/index.html') {
content = '<h1>首页</h1>'
} else if(url === '/about.html'){
content = '<h1>关于我</h1>'
}
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(content)
})
server.listen(8080, () => {
console.log('server running at http://localhost:8080')
})
模块化
Node.js中模块分为内置模块(如fs、path、http)、自定义模块(用户创建的js文件)和第三方模块(包)。
内置模块的优先级最高。
CommonJS规范
-
加载模块使用require(),require会执行被加载模块中的代码。
加载自定义模块时需要的是路径(.js文件后缀可省略),其他两个模块的加载都是写名字就行。
-
另外,模块也有作用域,类似于函数作用域,外部访问不了模块内定义的变量:
-
每个js文件内置了一个module对象(存储了当前模块的属性和信息):
-
使用require加载自定义模块时,得到的成员就是该模块
module.exports
指向的对象。上文得到的对象为空是因为自定义模块默认的module.exports指向的就是一个{}
。module.exports太长了,可以用exports代替(默认情况下二者指向同一个对象),但是模块加载的还是module.exports指向的对象:
exports.username = '张三' moudule.exports = { gender:'男', age:18 } // { gender:'男', age:18 } moudule.exports.username = '张三' exports = { gender:'男', age:18 } // { username = '张三' } moudule.exports.username = '张三' exports.age= 18 // { username = '张三', age:18 } exports = { gender:'男', age:18 } module.exports = exports module.exports.username = '张三' // { username = '张三', gender:'男', age:18 }
npm与包
基本使用:
- 创建包配置文件:
npm init -y
- 安装包:
npm i xxx
指定版本npm i xxx@2.0.1
- 安装的包会分到项目的node_modules文件夹中,分为开发依赖包(开发期间用到)和核心依赖包(开发和部署都会用到),前者安装:
npm i xxx -D
这样这个包就会被记录到devDependencies
中,后者正常安装,会被记录到dependencies
。 - 安装全局包:
npm install xxx -g
,会被安装到C:\Users\用户目录\AppData\Roaming\npm\node_modules
目录下。
一个规范的包要求:
- 包必须以单独的目录存在
- 包的顶级目录下一定要有package.json 这个包管理配置文件
- package.json 中必须包含 name(包名), version(包版本), main(包的入口)
加载机制
require加载包就是先在node_modules里面找到对应的包,然后找到package.json,在里面找到main字段,再通过该字段对应的文件路径找到对应的入口文件,进行加载。
require在加载模块时都是优先从缓存里面加载,多次加载不会重复执行模块里的代码。
加载自定义模块时没有文件后缀名,则会按顺序补全进行查找:
- 按照文件名加载
- 补全.js进行加载
- 补全.json进行加载
- 补全.node进行加载
- 加载失败,报错
加载第三方模块的机制:如果require里的既不是内置也没有./
或者../
,就会从/node_modules
目录加载,如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
例如,假设在’C:\Users\itheima\project\tool.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
如果是文件夹作为模块去加载,有三种加载方式:
- 在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require 加载的入口
- 如果目录里没有 packagejson 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件。
- 如果以上两步都失败了, 则 Node.js 会在终端打印错误消息, 报告模块的缺失:Error: Cannot find module ‘xxx’
Express框架
基本使用
基于http模块封装的,专门用来创建服务器(Web网站服务器和API接口服务器),但是功能更加强大。
- 安装:
npm i express
- 创建服务器
- 导入express:
const express = require('express')
- 创建web服务器:
const app = express()
- 启动服务器:
app.listen(端口号, ( ) ⇒ { })
- 导入express:
- 监听请求:
app.get('url', (req, res) ⇒ { })
监听什么类型就是app.xxx - 返回响应内容:
res.send()
- 如果url里面携带了
?x=1&y=1
请求参数,可以使用req.query
得到请求参数 - 如果url里面是携带的
:id
的动态请求参数,可以使用req.params
得到
const express = require('express')
const app = express()
app.listen(8080, () => {
console.log('Server running at http://localhost:8080')
})
// 挂载路由(express的路由)
app.get('/user', (req, res) => {
res.send({username: '张三', age:18})
})
app.post('/userinfo', (req, res) => {
console.log(req.query)
res.send('请求成功')
})
app.get('/product/:id', (req, res) => {
console.log(req.params)
res.send({id: req.params.id, price: 36.00, name: '黄金帅苹果'})
})
托管静态资源
express.static('指定目录')
对外开放静态资源:
const express = require('express')
const app = express()
app.use(express.static('./clock'))
Express在指定的静态目录中查找文件,并对外提供资源的访问路径。因此,存放静态文件的目录名不会出现在URL中。
如果希望路径加上目录,就可以挂载访问前缀:
app.use('/public', express.static('public'))
Tip:多次调用express.static托管多个静态资源时,这个函数会根据目录的添加顺序查找文件。
nodemon
当项目代码被修改后,可以自动重启项目,方便开发调试。
- 安装:
npm install -g nodemon
- 使用:
nodemon xxx.js
路由模块化
不把路由挂载到app上,而是挂载到router模块上。
// user.js
const express = require('express')
const router= express.Router()
router.get('/user/list',function(req,res){ res.send('Get user list.') }
router.post('/user/add',function(req,res){ res.send('Add new user.') }
module.exports = router
// index.js
const express = require('express')
const app = express()
// 导入路由模块
const userRouter = require('./user.js')
// 注册路由模块
app.use(userRouter)
// 如果要添加访问前缀
app.use('/api', userRouter)
Tip:app.use()就是用来注册全局中间件的
中间件
一个业务流程,除了开始(需求)和结尾(成果)的其他中间处理环节都叫中间件。
当一个请求到达Express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理,最后再交给路由进行处理。
express的中间件本质就是一个函数,跟路由函数有点像,但是参数多了一个next
:
const mid = (req, res, next)=>{ next() }
next的作用就是把流程交给下一个中间件或者路由。
多个中间件共享一份req或者res,所以中间件可以用来给这两个添加属性或者方法,供下一个中间件或路由使用。
注册全局中间件: app.use(mid)
如果注册了多个中间件时,会按照中间件定义先后依次执行。
const express = require('express')
const app = express()
app.use((req, res, next) => {
console.log('执行了中间件函数')
const t = Date.now()
req.startTime = t
next()
})
app.use((req, res, next) => {
console.log(`执行了第二个中间件函数,时间为${req.startTime}`)
next()
})
app.get('/time', (req, res) => {
console.log('执行了路由')
res.send(`现在时间为${req.startTime}`)
})
app.listen(8080, () => {
console.log('Server running at http://localhost:8080')
})
如果不使用app.use进行中间件注册,那就是局部的中间件,局部中间件只有在使用它的路由才会生效,不会影响其他的路由模块。
const express = require('express')
const app = express()
const mid = (req, res, next) => {
console.log('执行了中间件函数')
const t = Date.now()
req.startTime = t
next()
}
app.get('/time',mid, (req, res) => {
console.log('执行了time路由')
res.send(`现在时间为${req.startTime}`)
})
app.post('/user', (req, res) => {
console.log('执行了user路由')
res.send('请求成功')
})
app.listen(8080, () => {
console.log('Server running at http://localhost:8080')
})
可以定义多个局部中间件:
app.get('/time', [mid1, mid2, mid3], (req,res)=>{ })
app.get('/time', mid1, mid2, mid3, (req,res)=>{ })
// 两种方法是等价的
中间件使用注意事项:
- 一定要在路由之前注册中间件
- 客户端发送过来的请求,可以连续调用多个中间件进行处理
- 执行完中间件的业务代码之后,不要忘记调用next函数
- 为了防止代码逻辑混乱,调用next函数后不要再写额外的代码
- 连续调用多个中间件时,多个中间件之间,共享req和res对象
中间件分类:
-
绑定到app实例上的中间件,应用级别。
-
绑定到router上的中间件,路由级别。
-
错误级别:
function(err, req, res, next){ }
捕获一切错误并做出处理。特殊:错误中间件要注册在所有路由之后。
const express = require('express') const app = express() app.post('/', (req, res) => { throw new Error('发生错误') res.send('请求成功') }) app.use((err, req, res, next) => { console.log(err.message) res.send(err.message) }) app.listen(8080, () => { console.log('Server running at http://localhost:8080') })
-
内置级别:
express.static 快速托管静态资源的内置中间件,例如:HTML 文件、图片、CSS 样式等。
express.json解析JSON格式的请求体数据。
express.urlencoded解析URL-encoded 格式的请求体数据。
app.use(express.json()) app.use(express.urlencoded()) app.post('/user', (req, res)=>{ // 可以通过req.body得到请求体数据 console.log(req.body) // 如果没挂载中间件 则为undefined res.send('finished') })
在postman中可以点击此处进行不同格式(URL / JSON)请求体的编辑
写接口
-
创建基本服务器和api路由模块:
// expressDemo.js const express = require('express') const app = express() const router = require('./apiRouter') app.use('/api', router) app.listen(8080, () => { console.log('server running at http://localhost:8080') })
// apiRouter.js const express = require('express') const router = express.Router() module.exports = router
-
编写GET / POST /… 接口:
// apiRouter.js router.get('/user', (req, res) => { const query = req.query res.send({ status: 0, msg: 'GET请求成功', data: query }) })
-
基于cors解决跨域问题:
CORS(Cross-OriginResourceSharing,跨域资源共享)由一系列HTTP响应头组成,这些HTTP响应头决定浏览器是否阻止前端JS代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了CORS相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。
npm install cors // 安装 const cors = require('cors') // 导入 app.use(cors()) // 在路由运行前注册
cors的三个响应头
// Access-Control-Allow-Origin res.setHeader('Access-Control-Allow-Origin', '某个url' / '*')
只接受指定url的跨域请求 如果是 * 则表示所有跨域请求都接受
// Access-Control-Allow-Headers res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custome-Header')
默认情况下,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对额外的请求头进行声明,否则这次请求会失败!
// Access-Control-Allow-Methods res.setHeader('Access-Control-Allow-Methods', '*' / 'POST, GET, DELETE, HEAD')
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。
如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的HTTP方法。
cors的分类
简单请求
同时满足以下两大条件的请求:
请求方式:GET、POST、HEAD 三者之一;
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)。
预检请求
在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。只要符合以下任何一个条件的请求,都需要进行预检请求:
请求方式为 GET、POST、HEAD 之外的请求 Method 类型;
请求头中包含自定义头部字段;
向服务器发送了 application/json 格式的数据。
mysql模块
安装模块和配置:
// 安装
npm install mysql
// 导入
const mysql = require('mysql')
// 配置
const db = mysql.createPool({
host: '127.0.0.1', // 数据库ip地址
user: 'root', // 登录数据库的账号
password: 'root', // 登录数据库的密码
database: 'my_db_1' // 指定要操作哪个数据库
})
// 测试数据库是否正常工作
db.query('SELECT 1', (err, res) => {
if (err) return console.log(err.message)
console.log(res) // 打印[ RowDataPacket { '1': 1 } ]即是正常工作
})
插入数据:
// 插入数据
const user = { name: '张三', age: 18, gender: '男' }
db.query('INSERT INTO user (name, age, gender) VALUES (?, ?, ?)',
[user.name, user.age, user.gender],
(err, res) => {
if (err) return console.log('插入数据失败')
if(res.affectedRows === 1) console.log('插入数据成功')
}
)
// 这种写法也可以
const sqlStr = 'INSERT INTO user (name, age, gender) VALUES (?, ?, ?)'
db.query(sqlStr, Object.values(user), (err, res) => {
if (err) return console.log('插入数据失败')
if(res.affectedRows === 1) console.log('插入数据成功')
})
// 如果要插入的数据字段与数据表一一对应,也可以这么写
db.query('INSERT INTO user SET ?', user, (err, res) => {
if (err) return console.log('插入数据失败')
if(res.affectedRows === 1) console.log('插入数据成功')
})
更新数据:
// 把sqlStr替换,传入数据时按占位符顺序即可
sqlStr = 'UPDATE user SET age=?,gender=? WHERE name=?'
db.query(sqlStr, [19,'女','李四'], (err, res) => {
if (err) return console.log('更新数据失败')
if (res.affectedRows === 1) console.log('更新数据成功')
})
// 更新字段与数据表一一对应
sqlStr = 'UPDATE user SET ? WHERE name=?'
删除数据:
sqlStr = 'DELETE FROM user WHERE name=?'
查询数据:
sqlStr = 'SELECT * FROM user'
对表的增删改查其实就是SQL语句的不同。
身份认证机制
web开发模式主要分为服务端渲染和前后端分离,服务端渲染模式中服务器发送给客户端的HTML页面是在服务器通过字符串的拼接动态生成的,不需要使用Ajax这样的技术额外请求页面的数据。而前后端分离就是后端负责提供API接口,前端通过Ajax调用接口的模式。
两种不同的模式使用不同的认证机制:前者为Session,后者为JWT。
Session
这里的cookie就是服务器用来认证的,相当于身份证,它是不支持跨域访问的。
所以Session一般用于前端请求后端不存在跨域问题的场景。
// 安装中间件
npm install express-session
// 导入中间件
const session = require('express-session')
// 配置session中间件
app.use({
secret: 'keyboard cat', // 可以为任意字符串,安全密钥
resave: false, // 用于控制是否在每次请求时重新保存 session
saveUninitialized: true // 用于控制是否自动保存未初始化的 session
})
配置好后即可使用req.session访问和使用,从而存储用户信息。
const session = require('express-session')
const express = require('express')
const app = express()
app.use(session({
secret: 'admin', // 可以为任意字符串,安全密钥
resave: false, // 用于控制是否在每次请求时重新保存 session
saveUninitialized: true // 用于控制是否自动保存未初始化的 session
}))
app.use(express.urlencoded())
// 登录
app.post('/api/login', (req, res) => {
if (req.body.username !== 'admin' || req.body.password !== 'admin') {
return res.send({ status:1, message:'登录失败' })
}
req.session.user = req.body
req.session.isLogin = true
res.send({ status:0, message:'登录成功' })
})
// 获取用户信息
app.get('/api/userinfo', (req, res) => {
if (!req.session.isLogin) {
return res.send({ status: 1, message: 'Fail. Error: it is not loginning' })
}
res.send({ status:0, message:'success', username: req.session.user.username })
})
// 获取session
app.get('/api/session', (req, res) => {
res.send(req.session)
})
// 登出
app.post('/api/logout', (req, res) => {
// 清空session
req.session.destroy()
res.send({ status:0, message:'success' })
})
app.listen(8080, () => {
console.log('server running at http://127.0.0.1:8080')
})
JWT
前端请求后端需要跨域的时候就可以使用JWT(JSON Web Token)认证机制。
JWT的工作原理:用户信息通过Token字符串的形式,保存在客户端浏览器中。服务器通过还原Token字符串的形式来认证用户身份。
JWT通常有三部分:Header(头部).Payload(有效荷载).Signature(签名)
,Payload才是用户信息,头部和签名是为了安全性。
Token放在请求头的Authorization字段格式一般为:Authorization: Bearer <token>
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的Authorization字段,将Token字符串发送到服务器进行身份认证。
// 安装模块
// jsonwebtoken用于生成JWT字符串
// express-jwt用于将JWT字符串解析还原成JSON对象
npm install jsonwebtoken express-jwt
const express = require('express')
const app = express()
// 导入
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 定义secret密钥,用于加密和解密JWT
const secretKey = 'helloworld'
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
// 调用express-jwt中间件 用于解密
// .unless部分表示以/api开头的都不需要访问权限, algorithms是加密算法
app.use(expressJWT.expressjwt({ secret: secretKey, algorithms: ["HS256"] }).unless({ path:[/^\/api\//] }))
app.post('/api/login', (req, res) => {
if (req.body.username !== 'admin' || req.body.password !== 'admin') {
return res.send({ status: 1, message: '登录失败' })
}
res.send({
status: 200,
msg: '登录成功',
// 根据密钥对信息进行加密生成token
token: jwt.sign(
{ username: '张三' },
secretKey, // 加密密钥
{ expiresIn: '100s' } // token过期时间
)
})
})
app.get('/admin/userInfo', (req, res) => {
res.send({
status: 200,
msg: '获取用户信息成功',
data: req.auth // express-jwt解析完用户信息后会自动挂载到req.auth上
})
})
// 错误中间件处理错误情况
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.send({ status: 401, message: '无效的token' })
}
res.send({ status:500, message:'未知错误' })
})
app.listen(8080, () => {
console.log('server running at http://127.0.0.1:8080')
})
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)