在此前寫的文章“從零基礎入門進行小程序開發實戰”中,已經介紹過背單詞的小程序,因為沒有備案的服務器資源只能使用系統后臺提供的緩存功能存儲用戶數據。緩存有大小限制,而且只提供key-value的存儲方式,使用起來也很不方便。
最近域名和服務器已經申請下來,網站備案也在進行中,準備自己搭建數據庫服務器和開發一套實現restful api的后臺代碼。關于技術棧的選擇也頗花費了一些功夫,傳統的技術路線JAVA和.net core都能提供相關的成熟的框架,我本人技術背景對這方面也很熟悉,可是既然是自己興趣又不是公司的項目,當然還是想要嘗試一下新的不一樣的技術實現。
研究了Python的Flask框架和基于nodejs的koa2框架,都是大名鼎鼎,可之前接觸不多,最后選擇了koa2框架,寫小程序的后臺,順便也學習一下這方面的開發。
介紹
koa2是由 Express 原班人馬打造的,致力于成為一個更小、更富有表現力、更健壯的 Web 框架。 使用 koa 編寫 web 應用,可以免除重復繁瑣的回調函數嵌套, 并極大地提升錯誤處理的效率。koa 不在內核方法中綁定任何中間件, 它僅僅提供了一個輕量優雅的函數庫,使得編寫 Web 應用變得得心應手。
node基礎知識介紹
在開始項目前,先簡單介紹下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數據庫
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;
登錄和JWT認證實現
引入 "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'})
restful api實現示例
需求:小程序用戶輸入單詞,返回這個單詞的詳細介紹,包含中文釋義和常用例句。
建立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
};
}
})
管理和發布api
開發過程中命令行輸入 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應用。