Node.js

目录

学习视频:

00.学习目标_哔哩哔哩_bilibili

什么是Node.js?

Node.js是一个跨平台JavaScript运行环境,使开发者可以搭建服务器端的JavaScript应用程序。

作用:使用Node.js编写服务器端程序。

  • 编写数据接口,提供网页资源浏览功能等
  • 前端工程化:为后续学习VueReact等框架做铺垫。

前端工程化就是指开发项目直到上线,过程中集成的所有工具和技术。

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)

  1. 加载fs模块: const fs = require('fs')

  2. 写入文件内容: fs.writerFile('文件路径', '写入内容', err ⇒ { 写入后的回调函数 })

    写入内容会覆盖文件原有的内容。

  3. 读取文件内容: fs.readFile('文件路径', '编码格式', (err, data) => {读取后的回调函数})

    data 是文件内容的 Buffer 数据流,看具体内容要写 data.toString()

  4. 例子:

    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)

可以得到文件的绝对路径。

  1. 加载path模块: const path = require('path')

  2. 获取文件名: path.basename('文件路径')

    window或者posix上运行结果不一样,可以指定系统得到一致的结果:path.win32.basename('win系统的文件路径') / path.posix.basename('posix系统的文件路径')

  3. 获取路径目录: path.dirname('文件路径')

  4. 获取绝对路径: path.join()配合内置变量 __dirname

    path.join(__dirname, '.', 'try.txt')

  5. 例子:

    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(会省略)。

  1. 加载http模块:const http = require('http')

  2. 创建web服务器实例: const server = http.createServer()

  3. 为服务器实例绑定事件: server.on('request',(req, res) ⇒ { })

    req.url 是客户端请求的URL地址,req.method是客户端的method请求类型。

    res.end( ) 可以向客户端发送指定的内容(响应内容),并结束这次请求的过程。如果中文乱码了,可以设置res.setHeader('Content-Type', 'text/html; charset=utf-8' )

  4. 启动服务器: 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规范

  1. 加载模块使用require(),require会执行被加载模块中的代码。

    加载自定义模块时需要的是路径(.js文件后缀可省略),其他两个模块的加载都是写名字就行。

    在这里插入图片描述

  2. 另外,模块也有作用域,类似于函数作用域,外部访问不了模块内定义的变量:

    在这里插入图片描述

  3. 每个js文件内置了一个module对象(存储了当前模块的属性和信息):

    在这里插入图片描述

  4. 使用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与包

基本使用:

  1. 创建包配置文件:npm init -y
  2. 安装包:npm i xxx 指定版本 npm i xxx@2.0.1
  3. 安装的包会分到项目的node_modules文件夹中,分为开发依赖包(开发期间用到)和核心依赖包(开发和部署都会用到),前者安装:npm i xxx -D 这样这个包就会被记录到 devDependencies中,后者正常安装,会被记录到dependencies
  4. 安装全局包: npm install xxx -g,会被安装到 C:\Users\用户目录\AppData\Roaming\npm\node_modules目录下。

一个规范的包要求:

  1. 包必须以单独的目录存在
  2. 包的顶级目录下一定要有package.json 这个包管理配置文件
  3. package.json 中必须包含 name(包名), version(包版本), main(包的入口)

加载机制

require加载包就是先在node_modules里面找到对应的包,然后找到package.json,在里面找到main字段,再通过该字段对应的文件路径找到对应的入口文件,进行加载。

require在加载模块时都是优先从缓存里面加载,多次加载不会重复执行模块里的代码。

加载自定义模块时没有文件后缀名,则会按顺序补全进行查找:

  1. 按照文件名加载
  2. 补全.js进行加载
  3. 补全.json进行加载
  4. 补全.node进行加载
  5. 加载失败,报错

加载第三方模块的机制:如果require里的既不是内置也没有./或者../,就会从/node_modules目录加载,如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。

例如,假设在’C:\Users\itheima\project\tool.js’文件里调用了 require(‘tools’),则 Node.js 会按以下顺序查找:

  1. C:\Users\itheima\project\node_modules\tools
  2. C:\Users\itheima\node_modules\tools
  3. C:\Users\node_modules\tools
  4. C:\node_modules\tools

如果是文件夹作为模块去加载,有三种加载方式:

  1. 在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require 加载的入口
  2. 如果目录里没有 packagejson 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件
  3. 如果以上两步都失败了, 则 Node.js 会在终端打印错误消息, 报告模块的缺失:Error: Cannot find module ‘xxx’

Express框架

基本使用

基于http模块封装的,专门用来创建服务器(Web网站服务器和API接口服务器),但是功能更加强大。

  1. 安装: npm i express
  2. 创建服务器
    1. 导入express: const express = require('express')
    2. 创建web服务器:const app = express()
    3. 启动服务器:app.listen(端口号, ( ) ⇒ { })
  3. 监听请求:app.get('url', (req, res) ⇒ { }) 监听什么类型就是app.xxx
  4. 返回响应内容:res.send()
  5. 如果url里面携带了?x=1&y=1请求参数,可以使用req.query得到请求参数
  6. 如果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

当项目代码被修改后,可以自动重启项目,方便开发调试。

  1. 安装:npm install -g nodemon
  2. 使用: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)=>{ })
// 两种方法是等价的

中间件使用注意事项:

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

中间件分类:

  1. 绑定到app实例上的中间件,应用级别。

  2. 绑定到router上的中间件,路由级别。

  3. 错误级别: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')
    })
    
  4. 内置级别:

    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)请求体的编辑

    在这里插入图片描述

写接口

  1. 创建基本服务器和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
    
    
  2. 编写GET / POST /… 接口:

    // apiRouter.js
    router.get('/user', (req, res) => {
      const query = req.query
      res.send({
        status: 0,
        msg: 'GET请求成功',
        data: query
      })
    })
    
    
  3. 基于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')
})

在这里插入图片描述
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐