Node.js--》简易资金管理系统后台项目实战(后端)

        今天开始使用 node + vue3 + ts搭建一个简易资金管理系统的前后端分离项目,因为前后端分离所以会分两个专栏分别讲解前端与后端的实现,后端项目文章讲解可参考:前端链接,我会在前后端的两类专栏的最后一篇文章中会将项目代码开源到我的GithHub上,大家可以自行去进行下载运行,希望本文章对有帮助的朋友们能多多关注本专栏,学习更多前端node知识,然后开篇先简单介绍一下本项目用到的技术栈都有哪几个方面(阅读本文章能够学习到的技术):

node:基于Chrome V8引擎的开源、跨平台的JavaScript运行环境。

express:基于node.js的Web应用框架,帮助开发者快速构建可靠、高效的Web应用程序。

pm2:针对node应用生产环境进程管理工具,可帮助简化应用程序部署运行和监视等工作。

MongoDB:面向文档的NoSQL数据库系统,它可以灵活地存储非结构化数据。

Apifox:易用界面和功能,帮助开发人员和团队更高效地创建、设计和管理 RESTful API。

jwt:用于定义在网络上传输的信息安全可验证和可信任的方式,用于身份验证和授权机制。

目录

express搭建服务器

连接MongoDB数据库

搭建注册接口

搭建登录接口

获取登录信息

添加和获取信息

编辑和删除信息


express搭建服务器

首先我们需要创建一个文件夹用于存放后端项目,然后将文件夹拖到编辑器vscode当中,接下来需要打开终端执行 npm init 初始化package.json文件,相关步骤如下:

接下来开始装载 express 框架,终端执行如下命令:

npm install express

接下来我们在项目文件夹中创建入口文件 server.js 然后开始使用 express 框架:

const express  = require("express")
const app = express()
// 设置路由
app.get('/', (req,res) =>{res.send('hello world')
})
// 设置端口号
const post = process.env.PORT || 8080
app.listen(post, () => {console.log(`Server is running on port ${post},url: http://127.0.0.1:${post}`)
})

为了方便代码的执行,这里我们采用 pm2 进程管理工具进行使用,关于pm2的安装和具体的使用教程可以参考我之前的文章:深入理解 PM2:Node.js 应用部署和管理利器 ,为了方便简介,这里我们可以直接在package.json文件中设置一下我们的命令,如 下:

pm2进程管理工具是不会随着你将终端关闭而停止运行的,如果是第一次运行项目的话,可以根据我们上面设置的命令执行:npm run start 即可,如果想关闭项目的话,执行 pm2 stop 加项目名称就行了,如果实时查看日志,执行 pm2 log 即可,具体的实操自己体会,这里不再赘述。

接下来我们打开我们本地的 5000 端口,可以看到我们后端运行的项目了:

连接MongoDB数据库

接下来我们在本地创建一个 MongoDB数据库,用来后面后端编写相应接口所需要存放的数据,如果不了解MongoDB的朋友推荐可以参考一下我之前的文章:MongoDB数据库 ,接下来我们需要开始执行 node 连接MongoDB数据库了。

启动MongoDB服务

点击win键输入cmd,点击以管理员身份运行

执行 net start MongoDB 命令,运行MongoDB数据库服务:

创建MongoDB数据库

使用 Navicat 图形化管理工具,创建数据库。点击新建连接,选择 MongoDB

配置相应参数,连接直接默认即可,一般情况下,我们连接只需要连接主库查数据,所以选择独立的这个连接方式就可以,填写好常规参数可以点击测试连接是否正常,即可连接!因为是个人测试嘛,使用的是本地的localhost:

出现如下界面说明连接成功,我们直接点击确定即可。 

注意:进入到数据库之后,我们删除默认的数据库,重新创建一个名为 node_fund 名称的数据库

连接MongoDB数据库

接下来我们需要借助vscode工具来连接mongodb数据库,首先终端执行如下命令安装相应的包:

npm install mongoose

安装完成之后,我们在server.js中执行如下命令进行验证是否连接数据库成功:

// 引入 express 服务框架
const express  = require("express")
// 引入 mongoose 数据库服务
const mongoose = require("mongoose")
const app = express()
// 连接数据库的 URL
const MongoUrl = 'mongodb://localhost:27017/node_fund'
// 连接数据库
mongoose.connect(MongoUrl).then(() => {console.log('连接成功')
}).catch((err) => {console.log('连接失败', err)
})
// 设置路由
app.get('/', (req,res) =>{res.send('hello world')
})
// 设置端口号
const post = process.env.PORT || 5000
app.listen(post, () => {console.log(`Server is running on port ${post},url: http://127.0.0.1:${post}`)
})

当我们执行 pm2 log 查看日志的时候可以看到,我们的终端打印了连接成功的字眼:

当我们更改 MongoUrl 的默认url的时候,终端就会打印出相应的连接失败以及相应报错:

搭建注册接口

接下来我们需要开始搭建真正的接口了,首先我们需要在项目根目录下创建相关文件夹编写接口:

在编写接口之前,我们需要先安装 body-parser ,为了方便地在node中处理 POST 请求的请求体数据,从而更轻松地进行数据处理和响应,终端执行如下命令进行安装:

npm install body-parser

安装完成之后,我们还需要在入口文件 server.js 中使用它来解析请求体:

const express  = require("express")
const bodyParser = require("body-parser")
const app = express()// 使用 body-parser 中间件
app.use(bodyParser.urlencoded({ extended: false })) // 解析表单数据
app.use(bodyParser.json()) // 解析 JSON 格式的请求体

插件安装完成之后,我们还需要在项目根目录下创建一个models文件,用于存放从数据库获取数据所需要的数据及其相应的类型,这里需要借助 Schema 属性:

const mongoose = require('mongoose')// 定义模式,用于指定数据的结构和字段。
const Schema = mongoose.Schema
// 使用Schema变量来定义具体的数据模型
const userSchema = new Schema({name: {type: String,required: true},email: {type: String,required: true},password: {type: String,required: true},avatar: {type: String,},identity: {type: String,required: true},date: {type: Date,default: Date.now},
})
/** * 创建了一个名为users的MongoDB集合,并使用userSchema指定了集合中文档的结构* 将前一步创建的模型赋值给一个变量User,使其成为我们操作users集合的接口。*/
module.exports = User = mongoose.model('users', userSchema)

定义完相应的注册登录接口所需类型之后,接下来就需要正式在user.js文件中编写相应的注册接口,代码如下:

// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()// 引入具体的数据类型
const User = require('../../models/User')/*** 注册接口* POST api/users/register */
router.post('/register', (req,res) => {// 查询数据库中是否拥有邮箱User.findOne({ email: req.body.email }).then((user) => {if (user) {return res.status(400).json({ email: '邮箱已被注册' })} else {// 注册新邮箱const newUser = new User({name: req.body.name,email: req.body.email,avatar,password: req.body.password,identity: req.body.identity})}})
})module.exports = router

因为密码是至关重要的数据,所以这里我们需要对用户的密码进行一个加密,终端执行如下命令:

npm install bcrypt

下载成功之后,导入 bcrypt 然后对数据进行相应的hash加密,修改后的代码如下:

// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()
const bcrypt = require("bcrypt")// 引入具体的数据类型
const User = require('../../models/User')/*** 注册接口* POST api/users/register */
router.post('/register', (req,res) => {// 查询数据库中是否拥有邮箱User.findOne({ email: req.body.email }).then((user) => {if (user) {return res.status(400).json({ email: '邮箱已被注册' })} else {// 注册新邮箱const newUser = new User({name: req.body.name,email: req.body.email,avatar,password: req.body.password,identity: req.body.identity})// 进行密码加密bcrypt.genSalt(10, (err, salt) => {bcrypt.hash(newUser.password, salt, (err, hash) => {if (err) throw errnewUser.password = hashnewUser.save().then(user => res.json(user)).catch(err => console.log(err))})})}})
})module.exports = router

写完相应代码之后,这里我们还需要将该路由代码在入口文件 server.js 中进行一个引入:

const express  = require("express")
const app = express()// 引入user.js
const users = require('./routes/api/user')
// 使用routes
app.use('/api/users', users)

接下来我们需要借助接口测试工具 Apifox 进行相应的接口测试,如果不了解 Apifox 的朋友,可以参考我之前的文章:Apifox:详细使用教程,带你轻松拿捏 ,为了便于测试,我们先把 avatar 图像参数先删除,先测试一下其他参数。当我们输入相关路径及其参数点击发送之后:

当我们再次点击发送之后,就会出现当前的邮箱已被注册,符合逻辑规律:

接下来我们需要处理 avatar 头像参数,需要将图片数据保存到数据库中,可以使用 Buffer 对象来处理二进制数据,并将其存储为 Buffer 类型的字段。这里我们需要安装如下的这个插件进行处理

npm install multer

然后代码进行如下方式的修改:

// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()
const bcrypt = require("bcrypt")
const multer = require('multer');
const jwt = require("jsonwebtoken")
const passport = require("passport")// 引入具体的数据类型
const User = require('../../models/User')// 配置 multer
const storage = multer.diskStorage({destination: function (req, file, cb) {cb(null, 'public/images') // 设置图片保存的路径},filename: function (req, file, cb) {const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9)cb(null, file.fieldname + '-' + uniqueSuffix + '.' + file.mimetype.split('/')[1]) // 设置图片的文件名}
})
const upload = multer({ storage: storage })/*** 注册接口* POST api/users/register */
router.post('/register', upload.single('avatar'), (req,res) => {// 查询数据库中是否拥有邮箱User.findOne({ email: req.body.email }).then((user) => {if (user) {return res.status(400).json('邮箱已被注册')} else {const avatarUrl = req.protocol + '://' + req.get('host') + '/images/' + req.file.filename;// 注册新邮箱const newUser = new User({name: req.body.name,email: req.body.email,avatar: avatarUrl, // 使用上传的图片的文件名作为 avatar 字段的值password: req.body.password,identity: req.body.identity})// 进行密码加密bcrypt.genSalt(10, (err, salt) => {bcrypt.hash(newUser.password, salt, (err, hash) => {if (err) throw errnewUser.password = hashnewUser.save().then(user => res.json(user)).catch(err => console.log(err))})})}})
})

接下来进行Apifox接口测试之后,得到的结果如下:

如果想访问图片资源的话,这里我们还需要在入口文件出将静态资源设置为可访问:

app.use(express.static('public'));

当我们打开 Navicat 可视化管理工具之后,找到我们创建的对应数据,打开集合就能看到我们在测试工具Apifox生成的数据,在数据库当中也呈现出来了:

搭建登录接口

和注册接口一样,原理就是我们拿到用户请求过来的email和password之后,进行数据库的一个查询,如果查询当前数据库没有用户传递过来的email数据,就返回用户不存在,否则的话就开始比较密码进行相应的匹配得到对应的结果:

/*** 登录接口* POST api/users/login*/
router.post('/login', (req,res) => {const email = req.body.emailconst password = req.body.password// 查询数据库User.findOne({ email }).then(user => {if (!user) {return res.status(404).json({ email: '用户不存在!' })}// 密码匹配bcrypt.compare(password, user.password).then(isMatch => {if (isMatch) {res.json({ msg: 'success' })} else {return res.status(400).json({ password: '密码错误!' })}})})
})

比如说我们拿一下数据库当中真实存在的数据进行测试一下,得到的结果肯定是 success :

如果说我们随便输入密码或者用户名可能不是success了,举个例子改一下密码:

接下来我们需要给登录成功的 success 返回相应的token,终端执行如下命令按照相应包:

npm install jsonwebtoken

在导入 const jwt = require("jsonwebtoken") 之后,在密码匹配的地方执行jwt设置一个标记,过期时间我们设置了一个小时,然后token的话和前面的字符串进行了一个拼接:

在Apifox接口测试工具进行测试得到的相应结果如下:

获取登录信息

登录的接口写完,我们还需要写一个获取登录信息的接口函数,首先判断我们是否登录成功即需要验证一下我们当前登录的token是否存在且正确,所以这里我们需要借助 passport-jwt等工具进行:

npm install passport-jwt passport

接下来我们需要在入口文件 server.js 中初始化passport,然后将逻辑代码单独抽离出来:

// passport初始化
app.use(passport.initialize())
// 引入passport逻辑功能代码
require("./config/passport")(passport)

在抽离出来的config文件夹下的passport文件,这里我们开始书写真正的token校验:

const JwtStrategy = require("passport-jwt").Strategy
ExtractJwt = require("passport-jwt").ExtractJwt
const mongoose = require("mongoose")
const User = mongoose.model("users")const opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken()
opts.secretOrKey = 'secret'module.exports = passport => {passport.use(new JwtStrategy(opts, (jwt_payload, done) => {User.findById(jwt_payload.id).then(user => {if (user) {return done(null, user)}return done(null, false)}).catch((err) => {console.log(err)})}))
}

在user.js路由文件夹下,我们开始书写获取登录信息的接口了,这里我们仍然将图片资源进行应该路径的拼接:

/*** 获取登录信息* POST api/users/current*/
router.get('/current', passport.authenticate("jwt", { session: false }), (req,res) => {res.json({id: req.user.id,name: req.user.name,email: req.user.email,avatar: req.user.avatar,identity: req.user.identity})
})

通过Apifox得到的结果如下:

添加和获取信息

注册和登录的功能写完之后,接下来需要我们开始编写添加和获取信息的接口,这里我们要在api文件夹下再新建一个 profile 文件,以及在模型models文件夹下新建 Profile文件:

创建的两个文件都要在入口文件 server.js 中进行引入:

// 引入profile.js
const profile = require('./routes/api/profile')
// 使用routes
app.use('/api/profile', profile)

接下来我们先编写数据模型文件的代码,具体代码如下:

const mongoose = require('mongoose')// 定义模式,用于指定数据的结构和字段。
const Schema = mongoose.Schema
// 使用Schema变量来定义具体的数据模型
const ProfileSchema = new Schema({type: {type: String,},describe: {type: String,},income: {type: String,required: true},expend: {type: String,required: true},cash: {type: String,required: true},remark: {type: String,},date: {type: Date,default: Date.now},
})
/** * 创建了一个名为users的MongoDB集合,并使用userSchema指定了集合中文档的结构* 将前一步创建的模型赋值给一个变量User,使其成为我们操作users集合的接口。*/
module.exports = Profile = mongoose.model('profile', ProfileSchema)

根据上面讲解的编写接口的经验,我们很容易就写出添加和获取信息的即可,如下:

// 用户登录 / 注册相关的内容
const express = require('express')
const router = express.Router()
const passport = require("passport")// 引入具体的数据类型
const Profile = require('../../models/Profile')/*** 创建信息接口* POST api/profiles/add*/
router.post("/add", (req, res, next) => {passport.authenticate("jwt", { session: false }, (err, user, info) => {// 判断错误情况if (err) return res.status(500).json({ error: "Internal Server Error" });if (!user) return res.status(401).json({ error: "Unauthorized" });const profileFields = {};if (req.body.type) profileFields.type = req.body.type;if (req.body.describe) profileFields.describe = req.body.describe;if (req.body.income) profileFields.income = req.body.income;if (req.body.expend) profileFields.expend = req.body.expend;if (req.body.cash) profileFields.cash = req.body.cash;if (req.body.remark) profileFields.remark = req.body.remark;new Profile(profileFields).save().then((profile) => {res.json(profile);}).catch(err => {res.status(500).json(err);})})(req, res, next);
})/*** 获取所有信息* POST api/profiles*/
router.get( '/', passport.authenticate('jwt', { session: false }), (req, res) => {Profile.find().then((profiles) => {if (!profiles || profiles.length === 0) return res.status(404).json({ error: '数据为空!' })const profileData = { profiles: profiles }res.json(profileData)}).catch((err) => res.status(404).json(err));}
);
module.exports = router

获取单个信息的接口也很简单,只需要在路径拼接上id,id后面前端传递给我们即可:

/*** 获取单个信息* POST api/profiles/:id*/
router.get('/:id', passport.authenticate('jwt', { session: false }), (req, res) => {Profile.findOne({ _id: req.params.id }).then((profile) => {if (!profile) return res.status(404).json({ error: '数据为空!' });res.json(profile);}).catch((err) => res.status(404).json(err));
});

通过Apifox测试的结果如下:

编辑和删除信息

编辑信息接口和添加信息接口写法大体一致,如下:

/*** 编辑信息接口* POST api/profiles/edit*/
router.post("/edit/:id", passport.authenticate("jwt", { session: false }), (req, res) => {const profileFields = {};if (req.body.type) profileFields.type = req.body.type;if (req.body.describe) profileFields.describe = req.body.describe;if (req.body.income) profileFields.income = req.body.income;if (req.body.expend) profileFields.expend = req.body.expend;if (req.body.cash) profileFields.cash = req.body.cash;if (req.body.remark) profileFields.remark = req.body.remark;Profile.findOneAndUpdate({ _id: req.params.id },{ $set: profileFields },{ new: true }).then(profile => res.json(profile))}
)

删除信息的接口也很简单:

/*** 删除信息* POST api/profiles/:id*/
router.delete('/delete/:id', passport.authenticate('jwt', { session: false }), (req, res) => {Profile.findOneAndDelete({ _id: req.params.id }).then(profile => {res.json(profile);}).catch(err => res.status(404).json('删除失败'));    
});

表示删除成功!

ok!后端的一些基础代码已经写完了,下一篇将借助这些接口开始前端页面的书写了,等前后端项目全部写完,我再将源码开源出来,敬请期待!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/166632.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vulnhub靶机Funbox11

下载地址:Funbox: Scriptkiddie ~ VulnHub 主机发现 arp-scan -l 目标192.168.21.164 端口扫描 nmap --min-rate 1000 -p- 192.168.21.164 端口好多处理一下吧 nmap --min-rate 1000 -p- 192.168.21.164 |grep open |awk -F / {print $1} |tr \n , 端口服务版本…

冰蝎默认加密的流量解密

破解冰蝎的默认加密 流量包分析 上传的冰蝎流量包 POST /web-zh/DVWA/vulnerabilities/upload/ HTTP/1.1 Host: 192.168.197.111 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0 Accept: text/html,application/xhtmlxml,a…

文件读取结束的判定

大家好啊,我们今天来补充文件操作的读取结束的判定。 被错误使用的feof 牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾…

ArcGIS笔记9_数据源缺少空间参考信息?如何定义坐标系?

本文目录 前言Step 1 确定好要赋予给目标文件的恰当坐标系Step 2 定义坐标系 前言 有时从其他软件转换得到了shp文件,拖到ArcGIS后却出现“未知的空间参考:添加的数据源缺少空间参考信息 不能投影”的提示,如下图所示: 这种情况就…

图详解第六篇:多源最短路径--Floyd-Warshall算法(完结篇)

文章目录 多源最短路径--Floyd-Warshall算法1. 算法思想2. dist数组和pPath数组的变化3. 代码实现4. 测试观察5. 源码 前面的两篇文章我们学习了两个求解单源最短路径的算法——Dijkstra算法和Bellman-Ford算法 这两个算法都是用来求解图的单源最短路径的算法,区别在…

Rclone连接Onedrive

一、Rclone介绍 Rclone是一款的命令行工具,支持在不同对象存储、网盘间同步、上传、下载数据。 我们这里连接的onedrive,其他网盘请查看官方文档。 注意: 需要先在Windows下配置好了,然后再将rclone配置文件复制到Linux的rclone配…

【编解码】解码字符串中的 UNICODE 字符

前言 由于前后端交互中编码的问题,出现了这样的一串字符: {"share_names":["\u4e2d\u6587\u8def\u5f84"]}出现了unicode编码作为字符串内容的情况,直接用json解析的话会报错,所以在json解析前需要先进行转码…

每日刷题|贪心算法初识

食用指南:本文为作者刷题中认为有必要记录的题目 推荐专栏:每日刷题 ♈️今日夜电波:悬溺—葛东琪 0:34 ━━━━━━️💟──────── 3:17 &#x1f…

[ROS2系列] ORBBEC(奥比中光)AstraPro相机在ROS2进行rtabmap 3D建图

目录 背景: 一、驱动AstraPro摄像头 二、安装rtabmap error1:缺包 三、尝试 四、参数讲解 五、运行 error2: Did not receive data since 5 seconds! 六、效果​编辑 error4: 背景: 1、设备:pc;jeston agx …

使用VGG框架实现从二分类到多分类

一.数据集的准备 与之前的不同,这一次我们不使用开源数据集,而是自己来制作数据集。重点需要解决的问题是对数据进行预处理,如每一个图片的大小均不同,需要进行resize,还需要对每一张图片打标签等操作。 数据集文件 …

【Netty专题】【网络编程】从OSI、TCP/IP网络模型开始到BIO、NIO(Netty前置知识)

目录 前言前置知识一、计算机网络体系结构二、TCP/IP协议族2.1 简介*2.2 TCP/IP网络传输中的数据2.3 地址和端口号2.4 小总结 三、TCP/UDP特性3.1 TCP特性TCP 3次握手TCP 4次挥手TCP头部结构体 3.2 UDP特性 四、总结 课程内容一、网络通信编程基础知识1.1 什么是Socket1.2 长连…

微软 Win11 Dev 预览版 Build 23570 发布,修复文件资源管理器卡顿问题

本心、输入输出、结果 文章目录 微软 Win11 Dev 预览版 Build 23570 发布,修复文件资源管理器卡顿问题前言微软 Win11 Dev 预览版 Build 23570 发布,修复文件资源管理器卡顿问题完整的更新日志[Windows 中的 Copilot][开始菜单][任务栏搜索][设置] 已知问…

面向对象设计原则之依赖倒置原则

目录 定义原始定义进一步的理解 作用实现方法代码示例 面向对象设计原则之开-闭原则 面向对象设计原则之里式替换原则 面向对象设计原则之依赖倒置原则 面向对象设计原则之单一职责原则 定义 依赖倒置原则(Dependence Inversion Principle)&#xff0c…

【广州华锐互动】全屋智能家电VR虚拟仿真演示系统

在过去的几年中,智能家居的概念已经逐渐进入人们的生活。然而,它的真正潜力和最终形态可能还未被完全发掘。一种新兴的技术,虚拟现实(VR),为我们提供了一种全新的方式来理解和体验智能家居。VR公司广州华锐…

FFT64点傅里叶变换verilog蝶形运算,代码和视频

名称:FFT64点verilog傅里叶变换 软件:Quartus 语言:Verilog 代码功能: 使用verilog代码实现64点FFT变换,使用蝶形运算实现傅里叶变换 演示视频:http://www.hdlcode.com/index.php?mhome&cView&…

STM32F4X之GPIO

一、GPIO概述 主控芯片信息如下: 主频:168MHZ内核:ARM-M4FLASH:1MSRAM:192KB引脚:100GPIO:82电压:1.8~3.6V 1.1GPIO概念及其作用 GPIO概念:通用输入输出(General Purpose Input Output),主要作用…

How to add a jar to a project in eclipse?

Project -> Properties -> Java Build Path -> Libraries -> Add External JARs

前端多媒体处理工具——ffmpeg的使用

写在前面 在前端领域,FFmpeg 是一个非常有用的工具,它提供了多种媒体格式的封装和解封装,包括多种音视频编码、多种协议的流媒体、多种色彩格式转换、多种采样率转换、多种码率切换等。可以在多种操作系统安装使用。 安装 下载FFmpeg 在网…

免费Scrum管理工具-Leangoo领歌

Leangoo领歌是一款永久免费的专业的敏捷开发管理工具,提供端到端敏捷研发管理解决方案,涵盖敏捷需求管理、任务协同、进展跟踪、统计度量等。 
 Leangoo领歌上手快、实施成本低,可帮助企业快速落地敏捷,提质增效、缩短周期、加速…

vue PWA serviceWorker 有新内容时,如何自动刷新内容

vue PWA serviceWorker 有新内容时,如何自动刷新内容 一、问题描述 vue 自带的 pwa 插件可以很方便管理 serviceWorker 的使用,但会有一个问题。 ServiceWorker 的运行机制是这样的: 后台检测到新版本新版 ServiceWorker 下载并安装安装完…