全栈开发一个自己的微信小程序 Node.js+Express+Mysql+Vue+ElementUI 实现登录注册 列表增删改查 浏览记录 收藏功能

10,735 阅读12分钟

前言

因为下班后才有空做自己的产品,所以这个产品前前后后花了一个多月的时间,虽然在开发期间遇到很多问题,但是最终都解决了,完成后感觉很自豪,感谢帮助过我的各位大佬,真心感谢。

小序

记得雷布斯曾经说过:“我们要做米粉心中最牛逼的科技公司“。说实话,小米对我的影响很大(虽然我是个假米粉,但不是为了给小米打广告),小米这十几年做出了很多用心的作品,使得大众能买到更高性价比的科技产品,我很欣赏小米,每次有一些超前的想法我都会去小米官方的公众号评论或者反馈,也非常想进小米,但是我知道我现在还不够资历,我还需要不断提升。

虽然小米跟我没什么直接关系,但是他们的精神,是积极的的、充满力量的,是鼓舞并深深影响着我的,很多朋友说我是小米手机粉, 殊不知我迷的是小米的精神,因为我也想做一个很牛逼的产品,提供给成千上万的人使用,我会因此感到无比自豪。

有个灵魂拷问:程序员35岁能干嘛?

前江后浪推前浪,我们如果没有爬上去管理层,那就会变成又贵又没用的东西, 也就是职场中的奢侈品 , 35岁之后,要明白我们终将被时代抛弃,因为企业需要新鲜的活力和更符合当下的创新,换做你是老板,你可能也会。 35岁以上的职场,只有两种人,职场中的奢侈品,和吃草的万里马。 把体力存进去,换成经验,换成智慧,换成钱的同同时,你要有一个梦想,你能喜欢并且35岁后依旧能干甚至可以赖以为生。

现在的大多数年轻人都没有梦想,有的被浮躁、慵懒、缺少动力环绕着,有的被房子车子成了所谓的梦想,我也只能感慨,因为我可能也是他们中的一员,但是,我坚信我的未来绝对不是他们那种,我要冲出重围,找到我的梦想,为之奋斗,就算失败了,我不负韶华!

其实,小时候的我们都有过梦想,长大后,我要当科学家,当医生......,还记得吗?可是后面由于一系列的原因(此处....没有条件?没有正确引导?没有人支持?...哽咽中......省略几万字原因......),这个梦想不见了,长大后为了生计、车子、房子、孩子、家庭,梦想两字变得奢侈,变得遥不可及。

基于以上,我大概想通了,支持、引导是何其重要,所以我要做一个工具,去发现、筛选、引导、支持想要有梦想或者有梦想不知道怎么实现的同学。

我想要做的产品,是希望能帮助自己更好的规划人生,甚至希望能够造福所有迷茫中的人。所以自己琢磨着尝试开发一个全栈产品,可是,光靠激情没用,我只是一个前端,不是后台,不会写接口,也不会操作服务器,更不要说部署上线了。。。想想实在是太多不会的了。

不巧的是,遇上疫情,这更让我想迫切实现我的产品梦。

人还是要有梦想的,当你有了这个梦想,自然会有一股非三分钟热度的冲劲。另外,当看了很多商业大佬都是中年成家后才开始创业的你就会发现,年龄不是问题,重点是你有没有想去做的决心和终身去奋斗的坚持。

所谓万事都是开头难,管他呢,谁退谁怂,试了再说。于是,我的个人产品开始了。

产品简介

我的梦想,想做一个很牛逼的产品: 微信小程序《有梦必达》。

有梦必达初衷是一个帮助迷茫的、还没有梦想的、还在寻找梦想的、有梦想却还不知道怎么实现的童鞋更好的寻找或实现梦想的微信小程序。

有梦必达的初始设计功能有:登录、注册、找回密码、梦想列表、梦想详情、推荐课程、收藏、浏览记录、阅读数。

产品不够完善,仍需要完善,不喜勿喷。

gitHub开源地址

后台模板地址: vue-admin-template

后台Node所有接口地址: nodeExpress

开发流程

备注:如果有同学完全按照此博客开发产品,可能会遇到坑,例如版本问题导致一些未知bug或者安装系统限制等等。在我开发过程中就遇到过很多小问题,不过只要坚持解决问题,这些问题都难不倒我。即使一开始遇到未知bug还是有点慌,但是后面通过各种方法寻找答案,解决完毕时会很自豪。

总体开发流程如下:

1、有一颗完成梦想的决心
2、购买阿里云域名并且备案
3、购买阿里云服务器Cent Os
4、服务器安装Node、Npm、Cnpm、Mysql、Nginx并配置、PM2
5、本地安装Navicat Premium,连接远程服务器的Mysql、创建数据表、安装Xshell连接远程服务器、Xftp查看服务器文件
6、Node+Express创建功能接口
7、Vue+Element创建后台管理系统
8、部署后台管理系统至域名服务器下
9、阿里云后台域名升级HTTP=> HTTPS,获得证书后放至服务器并nginx配置
10、微信后台设置域名为https(需要https才能正常请求)
11、微信小程序+Vant前端展示
12、发布微信小程序

一、有一颗完成梦想的决心

如果想做一个产品,就应该把他当做一个梦想来看待,因为有了梦想,就会有冲劲,自己会觉得有意义,从而时刻保持冲劲,有热情去完成他,而不是三天打鱼两天晒网,顶多坚持几天就半途而废,到最后,产品没完成,自己也觉得自己能力有问题。所以,即使梦想再难,我们也要完成他,即使会慢一点!

二、购置阿里云域名并且备案

此处较为简单,直接去阿里云官网-购置域名即可 阿里云万网链接地址

备注:购买域名后,需要做域名备案才能在线上访问(咱们要做就做正规的) 阿里云首次备案参考链接地址

亦可参考其他博客方法备案,能实现备案目的就行

三、购置阿里云服务器Cent Os

四、服务器安装Node、Npm、Cnpm、Mysql、Nginx并配置、PM2

五、本地安装Navicat Premium,连接远程服务器的Mysql、创建数据表、安装Xshell连接远程服务器、Xftp查看服务器文件

由于三、四、五步骤篇幅过长,直接移驾到开源中国预览

六、Node+Express创建功能接口

nodejs部分:

1、package.json

{
  "name": "nodeDream",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "parchments",
  "license": "MIT",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "multer": "^1.4.2",
    "mysql": "^2.17.1",
    "nodemon": "^2.0.2",
    "request": "^2.88.2"
  }
}

2、connect.js

const path = require("path");
const mysql = require("mysql");
const express = require("express");
const app = express();//实例化一个app
const router = express.Router();
const cors = require('cors');
const jwt = require('jsonwebtoken');  //用来生成token
const request = require('request');
const bodyParser = require('body-parser');
app.use(bodyParser.json());//post请求req.body为空的处理 json请求
app.use(bodyParser.urlencoded({extended: false}));// 表单请求

// 全局配置跨域
app.use(cors());

//本地访问不了已上传的图片,需要开启静态资源路径访问
const pathname = __dirname;
//静态文件访问
app.use(express.static(pathname));
// 使用静态文件   这样可以获取静态文件的东西
app.use(express.static('vueDream/dist'))


//校验token
function verifyToken(req, res, next){
	let secretOrPrivateKey = 'jwtDream';//密钥
	jwt.verify(req.headers.token, secretOrPrivateKey, function (__err, decode) {
		//时间失效的时候/ 伪造的token
		if (__err) {
			return res.status(401).send({
				code: 401,
				data: null,
				message: "登录过期,请重新登录"
			});
		} else {
			next();
		}
	})
}
//这里处理全局拦截,一定要写在最上面
app.all('*', (req, res, next) => {
	//设置响应头
    res.header("Access-Control-Allow-Origin", "*"); //*表示允许的域名地址,本地则为'http://localhost'
    res.header("Access-Control-Allow-Headers", "*");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("Content-Type", "application/json;charset=utf-8");
	
	//如果是登录 注册 找回密码  需要放行  无需校验token
	let isLogin = req.url.indexOf('login');
	let isRegister = req.url.indexOf('register');
	let isForgotPassword = req.url.indexOf('forgotPassword');
	let isCollection = req.url.indexOf('collection');
	let isHistory = req.url.indexOf('history');
	// let isWechatLogin = req.url.indexOf('wechatLogin');
	// let isGetWxAccessToken = req.url.indexOf('getWxAccessToken');
	//微信小程序  并且是收藏接口需要校验token
	let systype = req.headers.systype;
	if(systype === 'wechat'){
		if(isCollection !== -1 || isHistory !== -1){
			verifyToken(req, res, next)
		} else {
			next();
		}
	} else if(systype === 'pc'){
		//pc   登录注册找回密码放行
		if(isLogin !== -1 || isRegister !== -1 || isForgotPassword !== -1){
			next();
		}else{
			verifyToken(req, res, next)
		}
	}
})

//这一步目的是,当访问根路径时,把前端代码读取出来并显示
app.get('/', (req, res) => {
	//服务器地址vueDream/dist/index.html
    res.sendFile(path.resolve(__dirname, 'vueDream', 'dist', 'index.html'));
})

//配置mysql
const option = {
	host: "www.yousit",
	// host: "49.985.02.01",
	user: 'admin',
	port: '3306', 
    password: "mmmmmmmm.",
    database: "node",
    connectTimeout: 5000, //连接超时
    //multipleStatements: false //是否允许一个query中包含多条sql语句
}

let pool;
repool();
function Res ({ code = 200, message = '', data = {} }) {
    this.code = code;
    this.message = message;
    this.data = data;
}
function resJson (_res, result) {
    return _res.json(new Res(result))
}

//断线重连机制
function repool() {
    //创建连接池
    pool = mysql.createPool({
        ...option,
        waitForConnections: true, //当无连接池可用时,等待(true)还是抛错(false)
        connectionLimit: 200, //连接数限制
        queueLimit: 0 //最大连接等待数(0为不限制)
    })
    pool.on('error', err => {
        err.code === 'PROTOCOL_CONNECTION_LOST' && setTimeout(repool, 2000)
    })
    app.all('*', (_,__, next) => {
        pool.getConnection( err => {
            err && setTimeout(repool, 2000) || next()
        })
    })
}

module.exports = { app, pool, router, resJson , jwt , request}

3、app.js

const { app, pool } = require('./connect');

//test
app.all('/api', (req, res) => {
    pool.getConnection((err, conn) => {
        res.json({ type: 'test success'})
        pool.releaseConnection(conn) // 释放连接池,等待别的连接使用
    })
})

//引入上传路由
const multerUpload = require('./routes/upload');
const user = require('./routes/user');
const dreamList = require('./routes/dream');
const collectionList = require('./routes/collection');
const historyList = require('./routes/history');
//使用路由
app.use('/upload', multerUpload);
app.use('/user', user);
app.use('/dream', dreamList);
app.use('/collection', collectionList);
app.use('/history', historyList);

//查看链接成功
app.get('/api/test', function (req, res) {
    res.json({ message: "连接成功" })
});

//开启监听
app.listen(8888, () => {
	console.log("服务器端口8888开启中...");
})

express接口部分:

1、其中列表增删改查接口

//梦想接口
const {app, pool, router, resJson, jwt } = require('../connect');

// 查询
/* 
  按分页显示账号列表的路由 /getData
*/
app.post("/api/dream/getData", (req, res) => {
		//后期需要补充校验
		console.log("前端传过来的",req.body)
		// 接收前端参数
		let { pageSize, pageNo , name , userId} = req.body;
		// 默认值
		pageSize = pageSize ? pageSize : 5;
		pageNo = pageNo ? pageNo : 1;
		name = name ? name : null;
		   
		// 构造sql语句 (查询所有数据 按照时间排序)
		let sqlStr = `select * from dream`;(有大佬不建议此写法,会有性能问题)
		//let sqlStr = `select d.name,d.id from dream d`;(有大佬建议此写法)		// 执行sql语句
		pool.getConnection((err, conn) => {
			conn.query(sqlStr, (err, data) => {
			  if (err) throw err;
			  // 计算数据总条数
			  let total = data.length;
		   
			  // 分页条件 (跳过多少条)
			  let n = (pageNo - 1) * pageSize;
			  //   sqlStr += ` limit ${n}, ${pageSize}`;//表示从pageNo条数据取,取pageSize条数据  此处空格不能去掉不然无响应
			  // 拼接分页的sql语句
				if(name){
					sqlStr += ` where name like '%${name}%'`;
					// 执行sql语句 (查询对应页码的数据)
					conn.query(sqlStr, (_err, _data) => {
					  if (_err) throw _err;
					  res.send({
						  code: 1,
						  data: {
							  rows: _data,
							  total: _data.length,
							  pageNo: pageNo,
							  pageSize: pageSize,
						  },
						  message: '查询成功!'
					  });
					});
				}else{
					sqlStr += ` limit ${n} , ${pageSize}`;
					conn.query(sqlStr, (_err, data) => {
					  if (_err) throw _err;
					  res.send({
						  code: 1,
						  data: {
							  rows: data,
							  total: total,
							  pageNo: pageNo,
							  pageSize: pageSize,
						  },
						  message: '查询成功!'
					  });
					});
				}
			});
			pool.releaseConnection(conn) // 释放连接池,等待别的连接使用
		})
  });

//添加接口
app.post('/api/dream/add', (req, res) => {
    //后期需要补充校验
    const data = req.body;
    const name = req.body.name;
    const sqlSameName = `select name from dream where name='${name}'`;
    //先查询数据库 dream 表里是否有前端传来的name值了 如果有返回重复提示 否则插入数据库
	pool.getConnection((err, conn) => {
		conn.query(sqlSameName, data, (_err, _results) => {
			if(_err){console.log(_err); return false;}
			//根据查询表结果个数判断,如果1为数据库已经存在此名称,不可插入   0代表数据库不存在此名称,可插入
			if(_results.length > 0){
				return res.json({
					code: 0, 
					message: "不可重复添加!", 
					data: null
				})
			}else{
				const sqlStr = 'insert into dream set ?';
				conn.query(sqlStr, data, (err, results) => {
					console.log(data)
					if (err) throw err;
					res.json({
						code: 1,
						message: '添加成功',
						data: results
					});
				})
			}
		})
        pool.releaseConnection(conn) // 释放连接池,等待别的连接使用
    })
});

//修改
app.post('/api/dream/edit', function (req, res) {
    //后期需要补充校验
    const data = req.body;
    const id = req.body.id;
    // let { name, collectionStatus, price, age, experience, education, analysis, introduce, duty, ask, coverImagePath, planImagePathArray, viedoUrl}  = req.body;
    let { name, coverImagePath, content, viedoUrl , recommend}  = req.body;
    let modSql = `update dream set 
					name='${name}', 
					coverImagePath='${coverImagePath}', 
					content='${content}', 
					viedoUrl='${viedoUrl}',
					recommend='${recommend}'
					where id ='${id}'`;
	let nameSql = `select * from dream where name='${name}' and id !='${id}'`;
	//先查询数据库 dream 表里是否有前端传来的name值了 如果有返回重复提示 否则更新数据库
	pool.getConnection((err, conn) => {
		conn.query(nameSql, data, (err, results) => {
			console.log(results)
			if(results.length >= 1){
				return res.json({
					code: 0, 
					message: "名称已经存在!", 
					data: null
				})
			}else{
				conn.query(modSql, data, (err, results) => {
					res.json({
						code: 1,
						message: '修改成功',
						data: results
					});
				})
			}
		})
        pool.releaseConnection(conn) // 释放连接池,等待别的连接使用
    })
});
//查看
app.post('/api/dream/show', function (req, res) {
    //后期需要补充校验
    let data = req.body;
	//id是商品id
    let { id , userId}  = req.body;
    let modSql = `select * from dream where id='${id}'`;
	pool.getConnection((err, conn) => {
		conn.query(modSql, data, (err, results) => {
			if (err) {
				console.log("查询失败原因",err)
				return res.json({
					code: 0, 
					message: "查询失败", 
					affectedRows: err
				})
			}
			// 1.查询出当前readCount
			results[0].readCount = results[0].readCount+1;
			let newReadCount = results[0].readCount;
			
			// 2.更新列表的id readCount
			let sqlDreamCountStr = `update dream set readCount='${newReadCount}' where id ='${id}'`;
			conn.query(sqlDreamCountStr, data, (_err, _data) => {
			  if (_err) throw _err;
			  console.log('更新列表的id readCount成功');
			});
			
			// 返回
			res.json({
				code: 1,
				message: '查询成功',
				data: results
			});
		})
        pool.releaseConnection(conn) // 释放连接池,等待别的连接使用
    })
});

//  删除
app.post('/api/dream/del', (req, res) => {
    //后期需要补充校验
    console.log(req.body)
    // let sqlStr = `DELETE FROM dream WHERE id = ${req.body.id}`;//单个删除
    let sqlStr = `DELETE FROM dream WHERE id in (${req.body})`;
	pool.getConnection((err, conn) => {
		conn.query( sqlStr, (err , results) => {
			if(err) {
				console.log(err);
			}else {
				res.json({
					code: 1,
					message: '删除成功',
					data: results
				});
			}
		})
        pool.releaseConnection(conn) // 释放连接池,等待别的连接使用
    })
})

module.exports = router;

2、更多功能接口 请前往github预览

七、Vue+Element创建后台管理系统

以vue-admin-template作为基础后台框架,查看vue-admin-template地址,搭建我的小程序后台管理系统,为实现登录、注册、用户列表增删改查、梦想列表增删改查、梦想详情、收藏、收藏列表增删改查、浏览记录等功能

八、部署后台管理系统至域名服务器下 九、阿里云后台域名升级HTTP=> HTTPS,获得证书后放至服务器并nginx配置

由于八、九步骤篇幅过长,直接移驾到开源中国预览

十、微信后台设置域名为https(需要https才能正常请求)

此处需要阿里云后台设置开放端口 https的开放端口

十一、微信小程序+Vant前端展示

区分环境变量、封装http请求、封装接口地址

十二、发布微信小程序

        正常审核时间为1-7个,本人第一次上传审核时间是2天,后面审核速度越来越快,最快的是30分钟内(上午发布的)。每个小程序审核时间不一,只是提供参考,建议做完后提前上传审核以免项目超时,也建议项目评估时最低需要预估1-2天的审核时间,以免尴尬。

成果展示