Node.js 開發(fā)實(shí)踐,前端工程師的MVP利器
作者:愚坤,掘金優(yōu)秀作者,一名沒上高中的前端工程師,目前就職水滴籌。
https://juejin.cn/post/6898612811891474440
什么是 MVP,來自偉大的百科:
Minimum Viable Product –最簡化可實(shí)行產(chǎn)品。
是指以最低成本盡可能展現(xiàn)核心概念的產(chǎn)品策略,即是指用最快、最簡明的方式建立一個(gè)可用的產(chǎn)品原型,這個(gè)原型要表達(dá)出你產(chǎn)品最終想要的效果,然后通過迭代來完善細(xì)節(jié)。該術(shù)語由弗蘭克·羅賓遜和埃里克·里斯推廣于Web應(yīng)用程序 ,它也可能涉及到進(jìn)行市場前手的分析。
前言:
Node是前端工程師的貴人,拓寬了前端工程師的能力邊界,對比前幾年用Dreamweaver寫table頁面的我來說,感受到的變化是日新月異;前端搞搞工程化和框架什么的也就算了,竟然連編輯器都自己搞?,js你說你是不是有點(diǎn)過分了?
當(dāng)然,這個(gè)過分的js幫助了我很多,從被后端大佬揪著耳朵按到工位上溫聲細(xì)語的說:“我套完頁面樣式亂了,幫我調(diào)下樣式”,演變成大佬氣沖沖的跑到我工位慈眉善目的拍著桌子說:“TM接口參數(shù)傳錯(cuò)了”。
感謝Node吧,至少我可以在自己的工位上改自己寫的Bug了?。
言歸正傳,再這么貧真就寫不下去代碼了,隨著Node能力的發(fā)展,我自己感覺出來自己有點(diǎn)飄了,因?yàn)橛杏肒indle看書的陋習(xí),一直覺的市面上所有的kindle筆記軟件都是垃圾?,于是自己寫了一個(gè)滿意的垃圾;這都不算啥,我居然因?yàn)橐獪p肥,就寫了個(gè)體重記錄小程序,上線以后我沖著鏡子里渾身贅肉的自己喊:“以為自己就是Node嗎?過分”?。
體重記錄小程序的故事并沒有突兀的結(jié)束,有些用戶反饋有bug,我借口taro更新太快項(xiàng)目跑不起來了,而且騰訊云函數(shù)我用的很不方便,于是很不負(fù)責(zé)的停更了;在年后疫情期間,因?yàn)閷?shí)在太閑就打開了后臺(tái)留言,看到有一個(gè)莫名其妙的留言說尋求合作??♂?。
我忐忑的撥通了電話,在說明了我是小程序的開發(fā)者以后,這個(gè)人上來就開始說瞎話:“你這個(gè)小程序太好了”??♂?,他闡述了一下自己的經(jīng)歷,是一位開了8年健身房的教練,后來混不下去把健身房關(guān)了,做在線減脂指導(dǎo),竟然收入還不錯(cuò),真是造化弄人?,他咨詢我可以一起做一個(gè)減脂管理系統(tǒng)嗎?不要錢那種,我恬不知恥的說:“好呀”。
不久我們見面了,約在北京東五環(huán)外的常營龍湖·長楹天街,他問我可以吃川菜嗎?我說可以,于是我們找了一家老屋川菜館,坐在我面前的這個(gè)人因?yàn)槌粤艘豢谧约狐c(diǎn)的辣子雞,然后拿著紙巾一把鼻涕一把淚的懺悔自己是個(gè)假重慶人??♂?,我羞澀的吐掉了口中的辣椒,一起構(gòu)想了我們小程序的未來,現(xiàn)在回想起自己的高談闊論,都有些不好意思?。
正文
上邊的段子根據(jù)個(gè)人情況改編,純屬娛樂,如果沒有Node的開發(fā)能力,別人可能會(huì)安慰我:“你還是去寫個(gè)頁面把”,? 苦澀的淚水從眼眶涌出。
簡單介紹了下最近折騰的3個(gè)項(xiàng)目的由來,從第一個(gè)體重記錄小程序,到Kindle筆記工具,再到現(xiàn)在的一套小程序 + 后臺(tái),作為一個(gè)前端程序員獨(dú)立作出一套可以跑起來的小系統(tǒng)還是比較有成就感的,雖然可能會(huì)被吐槽:不就是增刪改查嗎??但是不用擔(dān)心被吐槽:又沒寫過增刪改查懂個(gè)屁??
下邊內(nèi)容介紹了3個(gè)項(xiàng)目的積累,重點(diǎn)貼一下第三個(gè)項(xiàng)目Node用到的代碼。共同交流,懇請斧正。
21天體重記錄小程序
累計(jì)7千用戶和每天不超過20個(gè)活躍用戶的數(shù)據(jù),還有3篇實(shí)踐筆記。小程序提供的Node云函數(shù) + 數(shù)據(jù)庫,可以不花一毛錢就能跑起來自己的小程序,最早是原生寫法,后來切換到Taro React語法,效率提高很多,對小程序登錄流程、云開發(fā)有了一些經(jīng)驗(yàn)積累,也意識(shí)到自己對表結(jié)構(gòu)設(shè)計(jì)的欠缺。
奇怪的是竟然累計(jì)了7千用戶,用戶從哪來的呢,難道是因?yàn)槊制鸬暮脝幔?后續(xù)準(zhǔn)備再更新探索下。


【小程序 + 云開發(fā)】體重記錄小程序 上手筆記 【小程序 + 云開發(fā) 】 隨機(jī)讀取數(shù)據(jù)并生成分享圖片 上手筆記 【小程序 + 云開發(fā)】體重排行榜 上手筆記
kindle 筆記整理工具
最早是在本地開發(fā),開發(fā)了用戶注冊、密碼找回、書籍管理、筆記管理的功能,然后買服務(wù)器部署到線上。
前端使用的?Ant Pro,因?yàn)橐肓?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;border-radius: 4px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.047);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Echarts,沒有做按需引入,所以第一次進(jìn)入比較慢,目前只有自己用,就沒優(yōu)化。
整套流程跑起來以后,用著自己做的小工具覺得還挺香,也有信心做一些更大的挑戰(zhàn)了。
地址:http://nihaojob.com/



減脂管理系統(tǒng)開發(fā)
終于到今天的主題了,先說下應(yīng)用場景,學(xué)員在報(bào)名減脂教練的課程后,教練需要先了解學(xué)員日常飲食、睡眠、運(yùn)動(dòng)等生活習(xí)慣,然后根據(jù)學(xué)員狀況定制運(yùn)動(dòng)計(jì)劃和飲食方案,以及日常的運(yùn)營如對學(xué)員的飲食和運(yùn)動(dòng)的打卡審核、積分減重排行榜、知識(shí)庫等。
主要的6個(gè)功能:
教練賬號(hào)管理 問卷收集 方案下發(fā) 打卡審核 知識(shí)管理 積分、減重排行榜
后臺(tái)預(yù)覽:



小程序預(yù)覽:

知識(shí)點(diǎn)
服務(wù)器 域名 備案
我是從滴滴云上賣的服務(wù)器,一年才幾百塊,域名是之前在騰訊買的nihaojob.com, 備案過程中滴滴說有政策調(diào)整,花了20多天的時(shí)間,備案建議提前做準(zhǔn)備,備案期間可以把Nginx + Node + Mongodb環(huán)境搭建起來。

HTTPS證書申請與Nginx配置
微信小程序的開發(fā)域名必須是HTTPS,滴滴云有免費(fèi)的證書。
證書申請后需要在域名解析匯總增加TXT記錄,不懂就問滴滴云客服,服務(wù)很nice。
證書申請成功后,把證書上傳到服務(wù)器,在Nginx的/etc/nginx/conf.d目錄下,https.conf文件中ssl_certificate、ssl_certificate_key配置證書路徑。
server {
listen 443 ssl;
server_name api.nihaojob.com;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #按照這個(gè)協(xié)議配置
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;#按照這個(gè)套件配置
ssl_prefer_server_ciphers on;
ssl_certificate /root/sslcert/cert.crt;
ssl_certificate_key /root/sslcert/private.key;
location / {
alias /root/coach/coach-fe/dist/;
}
location /prod-api/ {
proxy_pass http://127.0.0.1:3000/;
}
}
跨域設(shè)置
這里設(shè)置了跨域請求頭,因?yàn)?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;border-radius: 4px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.047);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">Origin是根據(jù)入?yún)淼?,很容易造?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;font-size: 14px;border-radius: 4px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.047);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">CROS攻擊,對安全系數(shù)有要求的系統(tǒng)還是用別的方案吧,也可以使用express推薦的cors中間件。
app.all('*',?function?(req,?res,?next)?{
??res.header("Access-Control-Allow-Origin",?req.headers.origin);
??res.header('Access-Control-Allow-Headers',?'Origin,?X-Requested-With,?Content-Type,?Accept,?token');
??res.header("Access-Control-Allow-Methods",?"PUT,POST,GET,DELETE,OPTIONS");
??res.header("Access-Control-Allow-Credentials",?"true");
??res.header("Content-Type",?"application/json;charset=utf-8");
??if?(req.method?==?'OPTIONS')?{
????res.send(200);?/*讓options請求快速返回*/
??}
??else?{
????next();
??}
});
express jwt使用
前端在登錄時(shí)根據(jù)用戶id生成一個(gè)Token發(fā)給前端,前端之后的所有請求都攜帶這個(gè)Token,后端根據(jù)Token解開后的用戶id來進(jìn)行數(shù)據(jù)操作。
利用jsonwebtoken生成Token,express-jwt進(jìn)行校驗(yàn)和非必需登錄接口檢查。
個(gè)人認(rèn)為開發(fā)同學(xué)都應(yīng)該深挖一下無狀態(tài)Token機(jī)制與有狀態(tài)session機(jī)制的知識(shí)點(diǎn)。
//引入
const?jwt?=?require('jsonwebtoken');
const?expressJwt?=?require('express-jwt');
//定義簽名字符串
const?secret?=?'anyThingString';
//使用中間件驗(yàn)證token合法性
app.use(expressJwt({
??secret:?secret,
??getToken:?function?fromHeaderOrQuerystring(req)?{
????if?(req.cookies?&&?req.cookies.token?||?req.headers.token)?{?//?使用query.token
??????return?req.headers.token;
????}
????return?null;
??}
}).unless({
??path:?['/login','/users/register',?'/students/login',?]??//除了這些地址,其他的URL都需要驗(yàn)證
}));
//攔截器
app.use(function?(err,?req,?res,?next)?{
??//當(dāng)token驗(yàn)證失敗時(shí)會(huì)拋出如下錯(cuò)誤
??if?(err.name?===?'UnauthorizedError')?{
????//這個(gè)需要根據(jù)自己的業(yè)務(wù)邏輯來處理(?具體的err值?請看下面)
????res.status(401).send({?code:?-1,?msg:?'未登錄',?status:?41002?});
??}
});
//?解析token中間件?后續(xù)所有接口可直接使用req.tokenDecode獲取參數(shù)
app.use((req,?res,?next)?=>?{
??//?獲取token
??let?token?=?req.cookies.token?||?req.headers.token;
??if?(token)?{
????let?decoded?=?jwt.decode(token,?secret);
????req.tokenDecode?=?decoded
??}
??next()
});
//返回token給客戶端.
app.use('/login',?async?function?(req,?res)?{
??const?{?mail,?password?}=?req.body;
??//?格式校驗(yàn)
??if(?mail?===?''?||?password?===?''?){
????return?res.send({?code:?-1,?msg:?'非法參數(shù)'?});
??}
??const?userInfo??=?await?userModel.findOne({?mail:?mail,?password:?password??})
??if?(!userInfo)?{
????res.send({?code:?-1,?msg:?'信息錯(cuò)誤'?});
??}?else?{
????const?{?_id,?mail,?mobile,?type?}?=?userInfo;
????//生成token
????const?token?=?jwt.sign({?_id,?mail,?mobile,?type,?},?secret,?{
??????expiresIn:?3600?*?2?//秒到期時(shí)間
????});
????res.cookie('token',?token,?{
??????maxAge:?1000?*?60?*?60?*?24?*?30,
??????httpOnly:?true
????});
????res.send({?code:?1,?msg:?'登錄成功',?status:?'ok',?data:{?userInfo,?token?}?});
??}
});
環(huán)境變量
環(huán)境變量在npm script中設(shè)置,本地開發(fā)時(shí)用nodemon yarn start,部署線上環(huán)境時(shí)使用pm2 start --name coEnd npm -- run startPro。
需要根據(jù)環(huán)境變量走不同的數(shù)據(jù)庫連接地址和圖片前綴地址,如果公眾號(hào)或者小程序有區(qū)分測試和正式環(huán)境,也可以在這里配置APPID和SECRET。
"scripts":?{
????"start":?"NODE_ENV=development?node?./bin/www",
????"startPro":?"NODE_ENV=production?node?./bin/www"
??},
//?全局配置文件?/utils/config.js
const?configBase?=?{
?mongoDb:?{?//?數(shù)據(jù)庫
????????development:'mongodb://127.0.0.1:27017/coach',
????????production:'mongodb://127.0.0.1:27017/coach',
????},
????fileUrl:{?//?圖片地址
????????development:'http://127.0.0.1:3000/',
????????production:'https://api.nihaojob.com/prod-api/',
????}
}
//?引用的配置對象?在各分模塊中調(diào)用
let?infoConfig?=?{}
let?envKey?=?process.env.NODE_ENV
//?預(yù)知環(huán)境變量
let?keys?=?['development','production']
//?拼接引用的配置對象
Object.keys(configBase).forEach(item?=>?{
?//?預(yù)知環(huán)境變量
?if(keys.includes(envKey)){
??infoConfig[item]?=?configBase[item][envKey]
?}else{//?本地
??infoConfig[item]?=?configBase[item].host
?}
})
module.exports?=?infoConfig;
//?使用
const?{?fileUrl?}?=?require("../utils/config");
Mongooes 連接
在app.js中執(zhí)行?require('./utils/dbs')(),并且把DB實(shí)例掛到global上。
//?配置文件
var?baseConfig?=?require('./config.js');
const?dbs?=?async?function?(env)?{
????const?mongoose?=?require('mongoose');
????mongoose.connect(baseConfig.mongoDb,?{?useNewUrlParser:?true,?auto_reconnect:?true,?poolSize:?10?});
????const?db?=?mongoose.connection;
????db.on('error',?console.error.bind(console,?'數(shù)據(jù)庫鏈接失敗'));
????db.once('open',?callback?=>?{
????????console.log(`數(shù)據(jù)庫鏈接成功,地址:${baseConfig.mongoDb}`)
????});
????global.db?=?db;
}
module.exports?=?dbs;
Mongooes 增刪改
這部分不多說了,利ORM框架Mongooes增刪改特別簡單,先創(chuàng)建模型再根據(jù)模型操作。
const?mongoose?=?require('mongoose');
const?{?db?}?=?global;
//?創(chuàng)建Model
const?model?=?new?mongoose.Schema({
????coachId:String,?//?教練ID
},?{
????timestamps:?true,?//?自動(dòng)增加創(chuàng)建、更新時(shí)間
});
const?dbManage?=?db.model('tag',?model);
//?創(chuàng)建
dbManage.create({?name:''?})
//?更新?前邊為查詢條件?后邊為更新內(nèi)容
dbManage.updateOne({_id:'id'},{?name:''?})
//?刪除
dbManage.remove({_id?:?'id'})
//?查找
dbManage.find({_id?:?'id'?})
dbManage.findOne({_id?:?'id'?})
Mongooes 查詢
查詢的功能比較多了,比如字符串模糊查詢,常見的分頁、排序,時(shí)間范圍搜索等。直接貼一個(gè)模板吧,copy直接用版,哈哈。
router.get('/getList',?async?function(req,?res,?next)?{
????const?{?nickName?=?'',?name?=?'',?tel?=?'',?newStatus?=?'',?start?=?'1900-01-01',?end?=?'2222-01-01'??}?=?req.query
????const?{?current?=?1,?pageSize?=?20??}?=?req.query
????const?startTime?=?moment(start).startOf('day')
????const?endTime?=?moment(end).startOf('day')
????const?queryParams?=?{
????????$and:?[
????????????{?nickName:?{?$regex:?new?RegExp(nickName,?'i')?}?},?//?昵稱模糊查詢
????????????{?name:?{?$regex:?new?RegExp(name,?'i')?}?},?//?姓名模糊查詢
????????????{?tel:?{?$regex:?new?RegExp(tel,?'i')?}?},???//手機(jī)號(hào)模糊查詢
????????],
????????coachId:?req.tokenDecode._id,
????????createdAt:?{
????????????$gte:?startTime.toDate(),
????????????$lte:?moment(endTime).endOf('day').toDate()
????????????//?$lte:?moment(today).endOf('day').toDate()?//?查詢當(dāng)天
????????}
????}
????if?(newStatus?!==?''){
????????//?數(shù)字狀態(tài)模糊查詢
????????queryParams.$and.push({?newStatus:?newStatus?},)
????}
????//?分頁?skip跳過數(shù)?limit每頁數(shù)?sort排序方式
????const?resault?=?await?studentsModel.find(queryParams).skip(Number(pageSize)?*?Number(current-1)).limit(Number(pageSize)).sort({?createdAt:?'desc'?});
????//?總數(shù)
????const?total?=?await?studentsModel.find(queryParams).count()
????res.send({
????????code:1,
????????data:{
????????????list:?resault,
????????????page:?{
????????????????total,
????????????????current:?Number(current),?//?當(dāng)前頁
????????????????pageSize:?Number(pageSize)
????????????}
????????},
????});
});
圖片上傳
很多地方都要用到圖片上傳,使用formidable插件,設(shè)置上傳路徑為public,根據(jù)環(huán)境變量 + 文件名拼接圖片地址,單獨(dú)把圖片地址存到一張表中,方便其他地方復(fù)用。
var?express?=?require('express');
var?router?=?express.Router();
const?path?=?require('path');
const?formidable?=?require('formidable');
var?inFileModel?=?require("../model/inFile");
//?環(huán)境變量配置
const?{?fileUrl?}?=?require("../utils/config");
//?上傳文件
router.post('/',?async?function?(req,?res,?next)?{
????var?imgPath?=?path.dirname(__dirname)?+?'/public';
????var?form?=?new?formidable.IncomingForm();
????form.encoding?=?'utf-8';?//設(shè)置編輯
????form.uploadDir?=?imgPath;?//設(shè)置上傳目錄
????form.keepExtensions?=?true;?//保留后綴
????form.maxFieldsSize?=?2?*?1024?*?1024;?//文件大小
????form.type?=?true;
????const?awaitFn?=?()?=>?{
????????return?new?Promise((resolve,?reject)?=>?{
????????????form.parse(req,?(err,?fields,?files)?=>?{
????????????????let?src?=?files.file.path.split('/');
????????????????let?urlString?=?src[src.length?-?1]
????????????????if?(err)?{?reject(err);?}
????????????????resolve(urlString);
????????????});
????????});
????};
????//?文件名
????const?filePath?=?await?awaitFn();
????//?儲(chǔ)存上傳記錄
????const?fileInfo?=?await?inFileModel.create({?fileName:fileUrl?+?filePath,?})
????res.send({?code:1,?msg:'上傳成功',?data:fileInfo?})
});
module.exports?=?router;
定時(shí)獲取accesstoken
有過微信開發(fā)經(jīng)驗(yàn)的同學(xué)都知道,調(diào)用微信服務(wù)端api需要accesstoken,時(shí)效2小時(shí),利用CronJob定時(shí)獲取accesstoken并保存成文件,獲取失敗時(shí)利用nodemailer發(fā)送報(bào)警郵件。
在部署時(shí)單獨(dú)跑一個(gè)PM2進(jìn)程,pm2 start cronTask.js。
//?cronTask.js
var?CronJob?=?require('cron').CronJob;
const?moment?=?require('moment');
const?{?writeFile?}?=?require('fs').promises
const?axios?=?require('axios')
var?weConfig?=?require('./utils/weConfig');
var?nodemailer?=?require('nodemailer');
//?獲取微信token
var?getWeToken?=?new?CronJob('0?0?0/1?*?*?*',?async?function?()?{
??console.log('獲取token任務(wù)執(zhí)行:',?moment().format('YYYY-MM-DD?HH:mm:ss'));
??const?{?APPID,?SECRET?}?=?weConfig
??const?url?=?`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${SECRET}`
??axios.get(url).then(res?=>?{
????if?(res?&&?res.status?===?200?&&?res.data)?{
??????//?保存accesstoken文件
??????writeFile('./accesstoken.txt',?res.data.access_token,?'utf8')
????}?else?{
??????//?發(fā)送報(bào)警內(nèi)容
??????sendMail('[email protected]',?'保存錯(cuò)誤:'?+?JSON.stringify(res))
????}
??})
},?null,?true,?'America/Los_Angeles');
function?sendMail(mail,?text)?{
??//?發(fā)送郵箱配置
??var?mailTransport?=?nodemailer.createTransport({
????host:?'smtp.163.com',
????port:?465,
????secureConnection:?true,?//?使用SSL方式(安全方式,防止被竊取信息)
????auth:?{
??????user:?'郵箱',
??????pass:?'密碼'
????},
??});
??const?options?=?Object.assign(mailTransport,?{?from:?'[email protected]',?to:?mail?},?{
????subject:?'token獲取失敗報(bào)警',
????text:?text,
??})
??mailTransport.sendMail(options,?(err,?msg)?=>?{
????if?(err)?{
??????res.send({?code:?-1,?msg:?err?});
??????return
????}
????mailTransport.close();
??});
}
getWeToken.start();
生成小程序參數(shù)二維碼
讀取accesstoken.txt獲取token,利用axios發(fā)送給微信服務(wù)器獲取圖片,這塊有個(gè)點(diǎn)需要注意,請求會(huì)直接返回圖片,需設(shè)置responseType: 'arraybuffer'直接把buffer數(shù)據(jù)保存為圖片。
var?express?=?require('express');
var?router?=?express.Router();
const?{?fileUrl?}?=?require("../utils/config");
const?{?writeFile,?readFile?}?=?require('fs').promises
const?axios?=?require('axios')
router.post('/getQrcode',?async?function?(req,?res,?next)?{
??const?{?path?=?'',?scene?=?''?}?=?req.body;
??//?格式校驗(yàn)
??if?(path?===?''){
????res.send({?code:?-1,?msg:?'非法參數(shù)'?});
????return
??}
??const?accessToken?=?await?readFile('./accesstoken.txt')
??const?url?=?`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${accessToken.toString()}`
??const?data?=?await?axios.post(url,{
????scene:?scene,
????page:?path,
????width:430,
??},{
????headers:{
??????"Content-Type":?"application/json"
????},
????responseType:?'arraybuffer',
??})
??if?(data.data){
????const?fileName?=??'qrCode_'?+?new?Date().getTime()?+?'.jpg'
????await?writeFile('./public/'?+?fileName,?data.data)
????res.send({?code:?1,?data:?fileUrl?+?fileName?});
??}else{
????res.send({?code:-1,?msg:'請求錯(cuò)誤'?})
??}
});
module.exports?=?router;
for await使用
我們有一個(gè)用戶列表,需要根據(jù)用戶列表里的用戶id查詢另外一張列表里的用戶詳情,將他們拼接成一個(gè)新的列表返回給前端,我不太會(huì)用用、關(guān)聯(lián)查詢,探索出一個(gè)比較笨的方法,用for await這種方法實(shí)現(xiàn)的。
router.get('/getTopList',?async?function?(req,?res,?next)?{
????const?{?coachId?}?=?req.tokenDecode;
????let?data?=?await?topModel.find({?coachId?}).sort({?integral:?'desc'?});
????if?(data?&&?data.length?!==?0)?{
????????data?=?JSON.parse(JSON.stringify(data))
????????for?(const?item?of?data)?{
????????????const?info?=?await?studentsModel.findOne({?_id:?item.studentsId})
????????????item.info?=?info
????????}
????}
????res.send({code:?1,?data?})
});
taro小程序
這篇筆記的重點(diǎn)在Node上,就不展開聊了,簡單寫下登錄、request封裝、環(huán)境變量。
登錄
登錄的流程是,用戶點(diǎn)擊openType為getUserInfo的按鈕發(fā)起授權(quán),授權(quán)成功后調(diào)用Taro.login獲取code,再把code發(fā)給后端,后端通過code、APPID、SECRET獲取openid,剩下的就是用openid來綁定用戶關(guān)系了。
getUserInfo按鈕 => 授權(quán) => getCode => 獲取openid
taro-request
taro官網(wǎng)上有一個(gè)taro-request的封裝,蠻好用的地址。
環(huán)境變量
Taro的環(huán)境變量從process.env.NODE_ENV中讀取,內(nèi)置環(huán)境變量為development、production,前端需要根據(jù)環(huán)境變量走不同的環(huán)境。
const?getBaseUrl?=?(url)?=>?{
??let?BASE_URL?=?'';
??if?(process.env.NODE_ENV?===?'development')?{
????//開發(fā)環(huán)境
????BASE_URL?=?'http://localhost:3000'
??}?else?{
????//?生產(chǎn)環(huán)境
????BASE_URL?=?'https://api.nihaojob.com/prod-api'
??}
??return?BASE_URL
}
export?default?getBaseUrl;
后臺(tái)部分
后臺(tái)使用vue-element-admin模板,幾乎沒有復(fù)雜的內(nèi)容,接入了圖表、富文本、圖片上傳,就不展開了,后續(xù)會(huì)開發(fā)發(fā)菜單、權(quán)限管理,有可能使用node-casbin或acl實(shí)現(xiàn)。
部署
前端靜態(tài)文件直接使用Nginx指定靜態(tài)目錄,后端接口通過PM2啟動(dòng)服務(wù),并用Nginx的proxy_pass轉(zhuǎn)到后端服務(wù)端口上。HTTPS證書申請與Nginx配置小節(jié)中有貼出,列一下自己最常用的幾個(gè)PM2命令。
//?npm?script?啟動(dòng)應(yīng)用
$?pm2?start?--name?應(yīng)用名稱?npm?--?run?'npm?sctipt名稱'
//?應(yīng)用列表
$?pm2?list
//?查看日志
$?pm2?log
//?重啟應(yīng)用
$?pm2?restart?'應(yīng)用id'

總結(jié)
開頭段子中有調(diào)侃后端大佬,純粹只是玩笑;互聯(lián)網(wǎng)技術(shù)日新月異,大家都在齊頭并進(jìn),前端的內(nèi)容都學(xué)不完,又怎敢對不懂的行業(yè)指手畫腳。希望自己擁抱變化,保持敬畏。
聽說每個(gè)程序員都有一個(gè)創(chuàng)業(yè)夢,前端工程師真的可以借助Node跑起來自己的第一個(gè)MVP。
??愛心三連擊 1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的「點(diǎn)贊,在看」是我創(chuàng)作的動(dòng)力。
2.關(guān)注公眾號(hào)
程序員成長指北,回復(fù)「1」加入高級(jí)前端交流群!「在這里有好多 前端?開發(fā)者,會(huì)討論?前端 Node 知識(shí),互相學(xué)習(xí)」!3.也可添加微信【ikoala520】,一起成長。
“在看轉(zhuǎn)發(fā)”是最大的支持
