从头到尾实现个人项目的前后端分离:结合小程序、Koa Node.js、MongoDB和Vue.js实战指南
前言
掘金潜藏良久,游历各种技术文章,悄悄点赞都藏,偶尔逛逛沸点,留言互动。心中一直拥想自己写点什么的想法,但是写点什么呢???这是一个值得思考的问题,想了几年了也没想出来,尤其是看过别人的文章之后,因为总觉得自己太菜写不出什么东西,所以迟迟未敢动笔。
不过最近想明白了,不追求一定要输出多么高深的东西,就只是写篇文章记录下自己的学习,有输入就得有输出,有想法就得有行动,只要开始,一切都会慢慢变好。
全文的很多内容,官网上都可以找到(请叫我一把梭)
写着写着发现内容太多,所以拆出来两篇。这样文章就不用一指头划不到底了。哈哈哈哈
最后有代码地址。
项目说明:
用户端:
技术栈:微信纯原生小程序
说明:主要是商品展示功能,大概页面为首页+列表页+详情页
后台:
技术栈:NodeJS+Koa+mongodb(MongoDB Node Driver)
说明:接受后管平台上传的数据并将其保存在数据库
后台管理端:
技术栈:Vue+Element UI+VueRouter
说明:主要功能有banner上传、商品种类添加、商品列表、商品详情
效果图
- 后管和小程序放一起的截图。
业务流程图
关于选型的纠结
数据库和Node框架名词听过很多,但真的要选择用哪一个的时候,真的是一脸懵逼。。。 网上关于MySQL和MangoDB的介绍有很多,在这里就不多赘述了(koa和express也一样),下面只说下我简单粗暴的选型理由。
数据库选型
MySQL?还是MangoDB?
选择:MangoDB
MySQL是关系型数据库MangoDB是文档型,因为要把图片存在数据库中,所以选择MangoDB作为数据库类型。(不要问我为啥要把图片放数据库里)
Nodejs中框架的选择
koa?还是express?
koa是express开发的,语法较新,小巧精悍,由于项目不大,koa足够。
NodeJs连接数据库工具选择mongodb VS mongoose?
mongoose相当于mongodb的一层封装,好比Koa至于express,因为想要学习基本操作,所以选了官方提供的mongodb,即MongoDB Node Driver(哎~,后面付出了的代价有些大)
MongoDB Node Driver官方文档说明
好了,该选的都选好了,现在开始操作。
操作。。。。什么????
NodeJs? 数据库? NodeJs操作数据库?还是NodeJs监听前端请求?
在东一下西一下后,感觉做了很多事情,但是没一件有头绪,所以梳理了下操作步骤。
mongodb的概念解析
SQL 术语/概念 | MongoDB 术语/概念 | 解释/说明 |
---|---|---|
database | database | 数据库 |
table | collection | 数据库表/集合 |
row | document | 数据记录行/文档 |
column | field | 数据字段/域 |
index | index | 索引 |
table joins | 表连接,MongoDB 不支持 | |
primary key | primary key | 主键,MongoDB 自动将_id 字段设置为主键 |
mongodb的下载和安装(window版)
详情请见这篇文章(拆出来的第一篇文章) 一个前端小白的数据库之路:MongoDB和可视化工具的下载和安装
NodeJs操作数据库
拆出来的第二篇文章
从0开始:NodeJs操作MongoDB(官网搬运版)
NodeJs监听前端请求并返回内容
没有单独新建Node项目,直接在NodeJs操作数据库的那个项目直接操作的。
- 安装所需依赖包
npm i koa koa-bodyparser koa-router
- 新建一个damo.js文件
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
app.use(bodyParser()); // 解析request的body
const router = require('koa-router')();
router.post('/management-system/user/addUser', async (ctx) => {
console.log(ctx);
console.log(ctx.request.body, '请求参数---用户信息');
ctx.status = 200;
ctx.body = {
code: 200,
data: {
msg: '请求成功啦!',
},
};
});
app.use(router.routes());
app.listen(3031); // 该端口号为后管项目启动的端口号
console.log('正在监听 3031...');
- 前端请求效果如下图
第一阶段基本完成,一切都很顺利。
第二阶段只是用户信息的增、删、改、查,没有其他多余步骤,也还算顺利。
之所以说还算顺利,是因为基本流程逻辑都可以实现,但是没能在mongodb中实现限制传入数据库类型的操作。像如下这种类型限制。
let StudentSchema = mongoose.Schema({
name: String,
age: Number
})
顺利的进行完了,现在说说特别不顺利的:图片上传。
卡住的地方太多了,每次卡住都会问自己为啥要选mongodb,选个mongoose他不香吗? 操作简单,常见问题一搜一大把。何必卡在这里。。。。
虽然卡住多次,每次也在纠结要不要换成mongoose,几次纠结后,还是决定不换,遇到问题解决问题就好了,不能逃避。
遇到的主要问题:
- Node如何获取到文件流
- 如何将获取到的文件流转换为数据库能接受的形式并上传至服务器
- 如何将图片从数据库中取出来并转变成前端可以接受的形式
- 异步问题
1.Node如何获取到文件流
前面不是说 ctx.request.body中可以拿到请求数据吗,但是文件数据流却不在ctx.request.body中,而是在 ctx.request.files中。
router.post('/management-system/user/addUser', async (ctx) => {
console.log(ctx); // 请求实例
console.log(ctx.request.body, '请求参数---用户信息');
console.log(ctx.request.files,'------文件数据在这里')
});
第一个问题解决了,下面看看第二个问题。
2. 如何将获取到的文件流转换为数据库能接受的形式并上传至服务器
网上或者官网找到的图片上传都是下面例子这个样子。node读取本地图片存储数据库,从数据库中读取图片在存于本地。
GridFS是官方推荐的上传数据的形式,可以将大数据进行自动切割存储。
mongodb驱动 GridFS API 文档
thecodebarbarian.com/mongodb-gri…
依据文档可以实现上传和下载功能
const { MongoClient, GridFSBucket } = require('mongodb');
var fs = require('fs');
const assert = require('assert');
const URL = 'mongodb://127.0.0.1:27017';
const MYDATA = 'MANAGEMENT_BUCKET';
const MYCOLLECTION = 'Bucket';
const client = new MongoClient(URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
retryWrites: true,
});
async function run() {
try {
await client.connect();
const database = client.db(MYDATA);
database.collection(MYCOLLECTION);
const bucket = new GridFSBucket(database);
// console.log('db===============', bucket);
// 图片上传 读取本地111.png上传至数据库,该图片在数据库的名字为111.png
// fs.createReadStream('./111.png')
// .pipe(bucket.openUploadStream('111.png'))
// .on('error', function (error) {
// assert.ifError(error);
// })
// .on('finish', function () {
// console.log('done!');
// process.exit(0);
// });
// 图片下载 从数据库中读取111.png,将其改为output.png输出
bucket
.openDownloadStreamByName('111.png')
.pipe(fs.createWriteStream('./output.png'))
.on('error', function (error) {
assert.ifError(error);
})
.on('finish', function (e) {
console.log('done!', e);
process.exit(0);
});
} finally {
// Ensures that the client will close when you finish/error
// await client.close();
}
}
run().catch(console.dir);
但这并不是我想要的,node拿到的是文件流,不是可被如上方法读取的图片,作为一个菜鸟也没找到可以转换的方法。尝试了很久最终找到如下解决办法。就一行代码
const { path, name } = ctx.request.files.files;
- 画一下图片上传的大概流程(图中忽略了具体网络请求过程)
- 文件上传
// 文件上传至数据库
router.post('/file/uploadFiles', async (ctx) => {
await client.connect();
const database = client.db(MYDATA);
database.collection(MYCOLLECTION);
const bucket = new GridFSBucket(database);
// 解决问题的代码
const { path, name } = ctx.request.files.files;
fs.createReadStream(path)
.pipe(bucket.openUploadStream(name))
.on('error', function (error) {
assert.ifError(error);
})
.on('finish', function (e) {
console.log('done!------', e);
});
});
图片上传成功后,数据库会生成两个集合,一个存放文件信息,另一个存放文件切片。内部会根据id,自行拆分组合。
这个问题搞定。
3. 如何将图片从数据库中取出来并转变成前端可以接受的形式
解决问题的也是一行代码。
const base64 = data.toString('base64');
哎~ 知道的简单的要死,不知道的快要被难死了。。。。。
router.post('/file/downloadFiles', async (ctx) => {
await client.connect();
const database = client.db(MYDATA);
database.collection(MYCOLLECTION);
const bucket = new GridFSBucket(database);
// 下载.jpg 是已经被上传到数据库的文件名,可以根据ID查询图片
const result = await bucket
.openDownloadStreamByName('下载.jpg')
.on('data', (data) => {
// data 为从数据库中拿到的bucket形式文件流
const base64 = data.toString('base64');
// base64可以获得
let img = 'data:image/png;base64,' + base64;
console.log(img,'我拿到了bese64 啦啦啦啦啦啦啦!')
});
});
到这一步,大的问题基本都解决了,开开心心写代码。
开心不过1秒钟。。。。。。。。。
前端拿不到bese64的图片,马上来到下一个问题。
4. 异步问题
- 文件下载(存在异步问题,执行顺序不如预期)
- 问题现象描述:图片被处理成base64之前,请求主体ctx已经被返还给前端,致使前端拿不到想要的内容。
router.post('/file/downloadFiles', async (ctx) => {
await client.connect();
const database = client.db(MYDATA);
database.collection(MYCOLLECTION);
const bucket = new GridFSBucket(database);
const result = await bucket
.openDownloadStreamByName('下载.jpg')
.on('data', (data) => {
const base64 = data.toString('base64');
// base64可以获得
let img = 'data:image/png;base64,' + base64;
ctx.status = 200;
ctx.body = {
code: 200,
data: img,
};
console.log('后执行', ctx.status);
return Promise.resolve('====pppp');
});
console.log('先执行', ctx.status); // ctx.status 404
return result;
});
代码执行打印处的结果顺序为:
先执行 404
后执行 200
跟想要的结果正好相反。
- 解决办法
// 将数据库取出GridFSBucketReadStream流转为base64
async function GridFSBucketReadStreamToBase64(database) {
return new Promise(async function (resolve, reject) {
const bucket = new GridFSBucket(database);
await bucket.openDownloadStreamByName('下载.jpg').on('data', (data) => {
// base64可以获得
let base64Img = 'data:image/png;base64,' + data.toString('base64');
resolve(base64Img);
});
});
}
router.post('/file/downloadFiles', async (ctx) => {
await client.connect();
const database = client.db(MYDATA);
database.collection(MYCOLLECTION);
let base64 = await GridFSBucketReadStreamToBase64(database);
console.log(ctx.status, 'ctx.stauts=先执行', base64);
ctx.status = 200;
ctx.body = {
code: 200,
data: base64,
};
});
一个图片的异步问题解决了。还有多张图片处理问题。如:一次请求3张banner图片。
- banner图片存储思路
-
将图片以文件流的形式存统一储于数据库MANAGEMENT_BUCKET,另将文件名称按类型存于Banner集合中。
-
先在Banner集合中查出所有图片名称,然后根据图片名称循环拿到转换的图片。
获取图片流时,遇到异步循环问题。
和下面的参考文章遇到的问题以及解决思路简直一模一样。
异步循环参考文章: zhuanlan.zhihu.com/p/70785259
banner_controller.js文件 解决后代码
let bannerList = [];
promiseArr = bannerIdList.map(async (item) => {
const base64 = await GridFSBucketReadStreamToBase64(bucket, item['_id']);
console.log(base64.length);
bannerList.push({ img: base64, id: item['_id'] });
});
await Promise.all(promiseArr);
console.log(bannerList);
到此,真的就可以开开心心写代码了。后面的过程很快,大概2、3天就写完了全部代码。
项目地址
代码未重构,有代码洁癖的不喜勿喷。
github.com/MangoSeven/…
结束语
这篇文章拖拖拉拉写了近一个礼拜,终于完成了。全文没有什么高深的技术,大多都是基础的内容,更多的可能是整个项目的新路历程吧。
看了下最初的小程序文件夹,2019年7月,原以为只是想写个简单的小程序,做一个产品展示,从原型到初稿完成大概用了一礼拜时间。
后面发现单纯的小程序,无法解决大量图片问题,在云开发和学Nodejs之间纠结很一段时间,选了Nodejs,但是后来什么都没有做,被搁置了很长时间,直到今年年初,和一个朋友说了下自己的想法,我们一拍即合,我来写前端,他来写后台。
一切都看似顺利的进行,画需求图、需求讲解、各自工作计划,小程序和后管都开发完了,胜利就在眼前。哪知道这仅仅是个开始,异地联调环境搭建用了半个多月,才请求到对方服务器。又过了一个月左右我们调试通了第一个登陆接口。
嗯。。。。。也是最后一个接口。
恍恍惚惚又过去了半年。
从最初开始那天,它像一根刺卡在那里,咽不下去也吐不出来,很难受。直到今年我决定拔掉这根卡着的刺,然后也就有了这篇文章。
既然开始了,那就做完吧。 很感谢你能耐心的读完,这篇流水账般的文章。