在此前写的文章“从零基础入门进行小程序开发实战”中,已经介绍过背单词的小程序,因为没有备案的服务器资源只能使用系统后台提供的缓存功能存储用户数据。缓存有大小限制,而且只提供key-value的存储方式,使用起来也很不方便。
最近域名和服务器已经申请下来,网站备案也在进行中,准备自己搭建数据库服务器和开发一套实现restful api的后台代码。关于技术栈的选择也颇花费了一些功夫,传统的技术路线JAVA和.net core都能提供相关的成熟的框架,我本人技术背景对这方面也很熟悉,可是既然是自己兴趣又不是公司的项目,当然还是想要尝试一下新的不一样的技术实现。
研究了Python的Flask框架和基于nodejs的koa2框架,都是大名鼎鼎,可之前接触不多,最后选择了koa2框架,写小程序的后台,顺便也学习一下这方面的开发。
koa2是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。 使用 koa 编写 web 应用,可以免除重复繁琐的回调函数嵌套, 并极大地提升错误处理的效率。koa 不在内核方法中绑定任何中间件, 它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。
在开始项目前,先简单介绍下node相关的一些基础知识,通过npm init初始化一个node项目时,会生成一个package.json的配置文件,包括项目名称、版本、作者、依赖等相关信息,主要说一下其中的bin字段。
很多包都有一个或多个可执行的文件,希望放在PATH中,(实际上,就是这个功能让npm可执行的)。
当你要用这个功能时,需要给package.json中的bin字段添加一个命令名,并指向需要执行的文件(即后文的入口文件)。初始化的时候npm会将他链接到prefix/bin(全局初始化)或者./node_modules/.bin/(本地初始化)。
如果还没有nodejs和npm,首先需要安装这两个软件,注意最低版本要求。
脚手架对于前端程序员并不陌生,像vue-cli,react-native-cli等,全局安装后,只需要在命令行中敲入一个简单的命令,便可帮我们快速的生成一个初始项目,如vue init webpack projectName,即可生成一个初始的vue项目。
安装Koa2脚手架非常简单:
先执行下面命令:
npm install -g koa-generator
使用koa-generator生成koa2项目
在你的工作目录下,输入:
$ koa2 HelloKoa2
成功创建项目后,进入项目目录(安装项目依赖) :
npm install
启动运行项目:
$ npm start
项目启动后,默认端口号是3000,在浏览器中运行就能看到页面。
├─src 应用目录(可设置)
│ ├─controller 控制器
│ ├─config 配置文件
│ ├─db 数据库相关配置
├─ model 各个数据模型
├─ index.js 数据库配置页面
│ ├─middleWares 中间件
│ ├─routes 路由
├─ index.js 路由入口文件
├─ users.js 用户路由
│ ├─service 服务
│ ├─App.js 入口文件
package.json文件
每个Nodejs项目的根目录下面,一般都会有一个package.json文件。该文件可以由npm init生成,定义了项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。 package.json文件内部就是一个JSON对象,该对象的每一个成员就是当前项目的一项设置。
mysql模块是node操作MySQL的引擎,可以在node.js环境下对MySQL数据库进行建表,增、删、改、查等操作。
"dependencies": {
"glob": "^7.1.3",
"jsonwebtoken": "^8.4.0",
"koa": "^2.6.2",
"koa-body": "^4.0.4",
"koa-bodyparser": "^4.2.1",
"koa-cors": "0.0.16",
"koa-jwt": "^3.5.1",
"koa-router": "^7.4.0",
"koa-session": "^5.10.0",
"koa-static": "^5.0.0",
"mysql": "^2.16.0"
}
在package.json依赖中设置"mysql": "^2.16.0"
const config = require('../config').database
const mysql = require('mysql')
const pool = mysql.createPool(config)
const query = function(sql,succCb,errCb){
pool.getConnection(function(err,conn){
if (err) {
let data = {
code:500,
message:"请求失败",
data:err
};
errCb(data);
}else {
conn.query(sql,function(err,result){
if (err) {
let data = {
code:500,
message:"请求失败",
data:err
};
errCb(data);
}else {
succCb(result);
conn.release();
}
})
}
})
}
module.exports = query;
const query = require('./query')
const Tools = require('./tools')
const mysql = require('mysql')
const config = require('../config').database
const pool = mysql.createPool(config)
const getByPage = function(tb,page,limit){
return new Promise((resolve,reject)=>{
let start = (page-1)*limit;
let command = `select * from ${tb} limit ${start},${limit}`;
query(command,function(res){
let data = {
code:200,
message:'获取成功',
data:{
list:res,
pagination:{
size:res.length,
currentPage:parseInt(page)
}
}
}
query(`select count(*) from ${tb}`,function(res){
data.data.pagination['total'] = res[0]["count(*)"];
data.data.pagination['totalPage'] = parseInt(res[0]["count(*)"]/limit) + ((res[0]["count(*)"]%limit)>0?1:0);
resolve(data);
},function(err){
resolve(data)
})
},function(err){
resolve(err);
})
})
}
const getForeignInfo = function(tb,filter,foreign){//主表,筛选条件,外键信息
let queryStr = '';//查询条件
for (let key in filter) {
queryStr += `${tb}.${key}=${filter[key]}&`;
}
queryStr = queryStr.substr(0,queryStr.length-1);
let as = '';
let join = '';
let tables = ` from ${tb} ${tb}`;
for (let key1 in foreign) {
let table = foreign[key1].table;
let data = foreign[key1].data;
let key = key1;
join += ` join ${table} ${table} on ${tb}.${key}=${table}.id `;
for(let key2 in data){
as += `,${table}.${key2} as ${data[key2]}`
}
}
let str = `select ${tb}.*`+as+tables+join+(queryStr==''?'':'where '+queryStr);
console.log(str);
return str;
}
const Sql = {
queryAll:function(tb,filter,foreign){ //获取表的全部记录
if (filter && !Tools.isEmptyObject(filter)) { //分页
return getByPage(tb,filter.page,filter.limit,foreign)
}else { //全部
return new Promise((resolve,reject)=>{
let str = `select * from ${tb}`;
if (foreign) {
str = getForeignInfo(tb,filter,foreign);
}
query(str,function(res){
let data = {
code:200,
message:'获取成功',
data:{
list:res,
size:res.length
}
}
resolve(data);
},function(err){
resolve(err);
})
})
}
},
query:function(tb,id,foreign){ //根据id获取
return new Promise((resolve,reject)=>{
query(`select * from ${tb} where id=${id}`,function(res){
let data = {
code:200,
message:res.length==0?'查无数据':'获取成功',
data:res.length==0?{}:res[0]
}
resolve(data);
},function(err){
resolve(err);
})
})
},
queryByField:function(tb,fieldName,fieldValue){ //根据field获取
return new Promise((resolve,reject)=>{
query(`select * from ${tb} where ${fieldName}="${fieldValue}"`,function(res){
let data = {
code:200,
message:res.length==0?'查无数据':'获取成功',
data:res.length==0?{}:res[0]
}
resolve(data);
},function(err){
resolve(err);
})
})
},
insert:function(tb,data){ //插入一条记录
return new Promise((resolve,reject)=>{
let [keys,values] = [[],[]];
for (let key in data) {
if (data.hasOwnProperty(key)) {
keys.push(key);
if (Object.prototype.toString.call(data[key]) == '[object String]') {
values.push(`"${data[key]}"`)
}else {
values.push(data[key])
}
}
}
query(`insert into ${tb} (${keys}) values (${values})`,function(res){
let id = res.insertId;
let data = {
code:200,
message:'添加成功',
data:res
}
query(`select * from ${tb} where id=${id}`,function(res){
data.data = res[0];
resolve(data);
},function(err){
resolve(data);
})
},function(err){
resolve(err);
})
})
},
insertRows:function(tb,arr){ //插入多条记录
return new Promise((resolve,reject)=>{
let [keys,values] = [[],[]];
for (let i = 0; i < arr.length; i++) {
let [data,value] = [arr[i],[]];
for (let key in data) {
if (data.hasOwnProperty(key)) {
if (i==0) {
keys.push(key);
}
if (Object.prototype.toString.call(data[key]) == '[object String]') {
value.push(`"${data[key]}"`)
}else {
value.push(data[key])
}
}
}
values.push(`(${value})`);
}
query(`insert into ${tb} (${keys}) values ${values}`,function(res){
let data = {
code:200,
message:'添加成功',
data:res
}
let ids = [];
for (let i = 0; i < res.affectedRows; i++) {
ids.push(res.insertId+i);
}
query(`select * from ${tb} where id in (${ids})`,function(res){
data.data = {
list:res,
size:res.length
};
resolve(data);
},function(err){
resolve(data);
})
},function(err){
resolve(err);
})
})
},
update:function(tb,id,data){ //根据id修改单条记录
return new Promise((resolve,reject)=>{
let [str,index] = ['',0];
for (let key in data) {
if (data.hasOwnProperty(key)) {
if (index!=0) {
str += ','
}
if (Object.prototype.toString.call(data[key]) == '[object String]'){
str += `${key}="${data[key]}"`
}
else {
str += `${key}=${data[key]}`
}
index++;
}
}
query(`update ${tb} set ${str} where id=${id}`,function(res){
let data = {
code:200,
message:'修改成功',
data:res
}
query(`select * from ${tb} where id=${id}`,function(res){
data.data = res[0];
resolve(data);
},function(err){
resolve(data);
})
},function(err){
resolve(err);
})
})
},
updateRows:function(tb,arr){ //修改多条记录
return new Promise((resolve,reject)=>{
let [str,ids,len,keys] = ['',[],arr.length,Object.keys(arr[0])];
for (let x = 0; x < len; x++) {
ids.push(arr[x].id);
}
for (let i = 0; i < keys.length; i++) {
let k = keys[i];
if (k!='id') {
str += `${k} = case id `;
for (let j = 0; j < len; j++) {
str += `when ${arr[j].id} then `;
if (Object.prototype.toString.call(arr[j][k]) == '[object String]'){
str += `"${arr[j][k]}" `
}
else{
str += `${arr[j][k]} `
}
}
str += 'end'
if (i<keys.length-1) {
str += ','
}
}
}
query(`update ${tb} set ${str} where id in (${ids})`,function(res){
let data = {
code:200,
message:'修改成功',
data:res
}
query(`select * from ${tb} where id in (${ids})`,function(res){
data.data = {
list:res,
size:res.length
};
resolve(data);
},function(err){
resolve(data);
})
},function(err){
resolve(err);
})
})
},
delete:function(tb,id){ //根据id删除单条记录
return new Promise((resolve,reject)=>{
query(`delete from ${tb} where id=${id}`,function(res){
let data = {
code:200,
message:'删除成功',
data:res
}
resolve(data);
},function(err){
resolve(err);
})
})
},
deleteRows:function(tb,data){ //根据id数组删除多条记录
return new Promise((resolve,reject)=>{
query(`delete from ${tb} where id in (${data})`,function(res){
let data = {
code:200,
message:'删除成功',
data:res
}
resolve(data);
},function(err){
resolve(err);
})
})
},
search:function(tb,data,foreign){ //根据条件准确查询
let queryStr = '';//查询条件
for (let key in data) {
queryStr += `${key}=${data[key]}&`;
}
queryStr = queryStr.substr(0,queryStr.length-1);
let str;
if (foreign) {
str = getForeignInfo(tb,data,foreign);
}else {
str = `select * from ${tb} where ${queryStr}`
}
return new Promise((resolve,reject)=>{
query(str,function(res){
resolve({
code:200,
message:'获取成功',
data:res
});
},function(err){
resolve(err);
});
})
},
searchVague:function(tb,val,fields,foreign){//根据条件模糊查询
let str = `select * from ${tb} where concat(`;
for (let i = 0; i < fields.length; i++) {
str += `${fields[i]},`;
}
str = str.substring(0,str.length-1);
str += `) like %${val}%`;
if (fields.length==1){
str = `select * from ${tb} where ${fields[0]} like '%${val}%'`;
}
return new Promise((resolve,reject)=>{
query(str,function(res){
resolve({
code:200,
message:'获取成功',
data:res
});
},function(err){
resolve(err);
});
})
}
}
module.exports = Sql;
引入 "jsonwebtoken": "^8.4.0",模块
const jwt = require('jsonwebtoken');
const Token = {
encrypt:function(data,time){ //data加密数据,time过期时间
return jwt.sign(data, 'token', {expiresIn:time})
},
decrypt:function(token){
try {
let data = jwt.verify(token, 'token');
return {
token:true,
id:data.id
};
} catch (e) {
return {
token:false,
data:e
}
}
}
}
module.exports = Token
登录成功返回token代码:
token = jwt.sign({id:res[0].id}, 'token', {expiresIn: '15d'})
需求:小程序用户输入单词,返回这个单词的详细介绍,包含中文释义和常用例句。
建立wordDesc表
添加一个routers文件
wordDesc.js
const router = require('koa-router')(); //路由
const Sql = require('../utils/sql');
const query = require('../utils/query');
const Tools = require('../utils/tools');
const jwt = require('jsonwebtoken');
const Token = require('../utils/token')
const tbName = 'worddesc';
const preUrl = '/api/worddesc';
let codeList = {};
api/worddesc/test
.get(`${preUrl}/:word`,async(ctx,next)=>{ //获取word desc
let word = ctx.params.word;
let data = Token.decrypt(ctx.header.authorization);
if (data.token) {
let res = await Sql.queryByField(tbName,"word", word);
ctx.body = res;
}else {
ctx.body = {
code:401,
message:'failed',
data:data
};
}
})
{
"code": 200,
"message": "获取成功",
"data": {
"id": 2,
"word": "test",
"description": "this is a test.",
"bookid": "2",
"createdtime": null,
"lastupdatetime": null,
"maxl": 2
}
分页获取列表数据
/api/worddesc?page=2&count=2
.get(`${preUrl}`,async(ctx,next)=>{ //获取word desc
let data = Token.decrypt(ctx.header.authorization);
if (data.token) {
let page = ctx.query.page;
if(page == undefined){
page = 1;
}
let count = ctx.query.count;
if(count ==undefined){
count = 500;
}
let filter = {
page:page,
limit:count
}
let res = await Sql.queryAll(tbName,filter);
ctx.body = res;
}else {
ctx.body = {
code:401,
message:'failed',
data:data
};
}
})
{
"code": 200,
"message": "获取成功",
"data": {
"list": [
{
"id": 2,
"word": "test",
"description": "this is a test.",
"bookid": "2",
"createdtime": null,
"lastupdatetime": null,
"maxl": 2
},
{
"id": 3,
"word": "year3",
"description": "year3",
"bookid": "3",
"createdtime": null,
"lastupdatetime": null,
"maxl": 3
},
{
"id": 4,
"word": "ljtest",
"description": "desc........",
"bookid": "2",
"createdtime": null,
"lastupdatetime": null,
"maxl": null
},
{
"id": 6,
"word": "lj2",
"description": null,
"bookid": null,
"createdtime": null,
"lastupdatetime": null,
"maxl": null
},
{
"id": 7,
"word": "lj3",
"description": null,
"bookid": null,
"createdtime": null,
"lastupdatetime": null,
"maxl": null
},
{
"id": 8,
"word": "lj4",
"description": null,
"bookid": null,
"createdtime": null,
"lastupdatetime": null,
"maxl": null
}
],
"pagination": {
"size": 6,
"currentPage": 1,
"total": 6,
"totalPage": 1
}
}
}
同样增加添加、修改、删除api
.post(`${preUrl}`,async(ctx,next)=>{ //添加信息
let data = Token.decrypt(ctx.header.authorization);
let word = ctx.request.body.word;
if (data.token) {
let wordRes = await Sql.queryByField(tbName,"word",word);
console.log("------------");
console.log(wordRes);
if(wordRes.data.id != undefined)
{
let res = await Sql.update(tbName,wordRes.data.id,ctx.request.body);
ctx.body = res;
}else{
let res = await Sql.insert(tbName,ctx.request.body);
ctx.body = res;
}
}else {
ctx.body = {
code:401,
message:'failed',
data:data
};
}
})
.delete(`${preUrl}/:word`,async(ctx,next)=>{ //修改信息
let data = Token.decrypt(ctx.header.authorization);
let word = ctx.params.word;
if (data.token) {
let wordRes = await Sql.queryByField(tbName,"word",word);
if(wordRes.data.id != undefined)
{
let res = await Sql.delete(tbName,wordRes.data.id );
ctx.body = res;
}else{
ctx.body = {
code:200,
message:'no record',
data:''
};
}
}else {
ctx.body = {
code:401,
message:'failed',
data:data
};
}
})
.put(`${preUrl}/:word`,async(ctx,next)=>{ //修改信息
let data = Token.decrypt(ctx.header.authorization);
let word = ctx.params.word;
console.log(word);
if (data.token) {
let wordRes = await Sql.queryByField(tbName,"word",word);
console.log("------------");
console.log(wordRes);
if(wordRes.data.id != undefined)
{
console.log("id------------");
console.log(wordRes.data.id);
console.log(ctx.request.body);
let res = await Sql.update(tbName,wordRes.data.id ,ctx.request.body);
ctx.body = res;
}else{
ctx.body = {
code:200,
message:'no record',
data:''
};
}
}else {
ctx.body = {
code:401,
message:'failed',
data:data
};
}
})
开发过程中命令行输入 node app.js 可以打开命令窗口启动运行,窗口中显示调试或错误信息,关闭窗口则结束进程。
生产环境中可以使用pm2来启动进程,M2是可以用于生产环境的Nodejs的进程管理工具,并且它内置一个负载均衡。它不仅可以保证服务不会中断一直在线,并且提供0秒reload功能,还有其他一系列进程管理、监控功能。并且使用起来非常简单。
安装pm2
npm install -g pm2
下面列出常用命令
$ npm install pm2 -g # 命令行安装 pm2
$ pm2 start app.js -i 4 #后台运行pm2,启动4个app.js
# 也可以把'max' 参数传递给 start
# 正确的进程数目依赖于Cpu的核心数目
$ pm2 start app.js --name my-api # 命名进程
$ pm2 list # 显示所有进程状态
$ pm2 monit # 监视所有进程
$ pm2 logs # 显示所有进程日志
$ pm2 stop all # 停止所有进程
$ pm2 restart all # 重启所有进程
$ pm2 reload all # 0秒停机重载进程 (用于 NETWORKED 进程)
$ pm2 stop 0 # 停止指定的进程
$ pm2 restart 0 # 重启指定的进程
$ pm2 startup # 产生 init 脚本 保持进程活着
$ pm2 web # 运行健壮的 computer API endpoint (http://localhost:9615)
$ pm2 delete 0 # 杀死指定的进程
$ pm2 delete all # 杀死全部进程
对于nodejs能够流行起来一点都不感到意外,开发起来太简单和方便了。跟java这些传统的技术相比,写nodejs脚本甚至感觉不像是在编程,真的像玩一样,极大了拉低了程序员的门槛。
还有一个事实就是JavaScript是Web开发者们熟知的语言,大部分人都了解JavaScript或多少使用过它。所以说,从其他技术转型到Node.js是很简单的。
跟java、.net这些传统的技术路线相比,nodejs项目在安装、调试、部署和发布都很方便,很多Web服务器和云服务提供商都支持Node.js的Web应用。