背景
自從完成了客戶端和管理后臺項(xiàng)目后,一個(gè)完整的web
應(yīng)用前端方面的項(xiàng)目算是搭建完成了祷肯。最后還需要有一個(gè)提供API服務(wù)的后端項(xiàng)目服務(wù)前端應(yīng)用運(yùn)行筐摘,經(jīng)過一個(gè)月的開發(fā),現(xiàn)在基本上已經(jīng)全部完成侵俗,經(jīng)過部署后,完成了最后的上線運(yùn)行。
項(xiàng)目使用技術(shù)棧
本項(xiàng)目是基于nodejs
主要使用koa+mongodb
為核心開發(fā)的輕量級服務(wù)端應(yīng)用挠唆。接口是按照RESTful
風(fēng)格進(jìn)行設(shè)計(jì)
使用主要中間件有
koa-compress
,
koa-parameter
,koa-connect-history-api-fallback
,koa-static
,koa-mount
贿条。具體使用方式請?jiān)?code>koa官方倉庫查看
數(shù)據(jù)庫操作:mongoose
接口權(quán)限驗(yàn)證:jsonwebtoken
用戶密碼加密:bcryptjs
上傳資源存儲(chǔ):koa-multer
路由分發(fā):koa-router
接口參數(shù)解析: koa-bodyparser
開發(fā)過程
數(shù)據(jù)庫設(shè)計(jì)
本項(xiàng)目選擇使用mongodb
作為數(shù)據(jù)存儲(chǔ)的數(shù)據(jù)庫雹仿,因?yàn)槠鋵τ谇岸碎_發(fā)者有著天然的有好性,使用容易上手整以。
由于本人涉獵服務(wù)端領(lǐng)域尚處于初級階段胧辽,在數(shù)據(jù)庫設(shè)計(jì)方面經(jīng)驗(yàn)有限,在此分享出來僅供參考使用公黑。
mongodb
使用bson
類型作為數(shù)據(jù)存儲(chǔ)格式邑商。由于跟前端js的json
類型可以互相轉(zhuǎn)換使用摄咆。所以這就減小了入門者設(shè)計(jì)數(shù)據(jù)庫表字段的難度,我們可以參照前端頁面需要展示的數(shù)據(jù)進(jìn)行設(shè)計(jì)人断。
然后通過使用mongoose
這個(gè)庫作為快速操作數(shù)據(jù)庫的模型吭从,我們可以用代碼的形式設(shè)計(jì)mongodb
表字段的模型,經(jīng)過mongoose
的編譯可以存儲(chǔ)到真實(shí)的數(shù)據(jù)庫表中恶迈。
拿本項(xiàng)目中的商品
表來說涩金,這是一個(gè)聲明好的mongooseSchema
{
name: String,
price: Number,
oldPrice: Number,
description: String,
sellCount: Number,
rating: Number,
info: String,
menuID: ObjectId,
image: String,
online: { type: Boolean, default: true },
},
通過mongoose
模型的編譯方法后存儲(chǔ)到數(shù)據(jù)庫中的字段是這個(gè)樣子
數(shù)據(jù)庫存儲(chǔ)字段
Field | Type | Description |
---|---|---|
menuID | ObjectId | 商品分類 ID |
name | String | 商品標(biāo)題 |
info | String | 商品信息 |
description | String | 商品簡介 |
image | String | 商品封面 |
online | Boolean | 是否發(fā)布 |
oldPrice | Number | 商品原價(jià) |
price | Number | 商品售價(jià) |
sellCount | Number | 售賣個(gè)數(shù) |
查看完整的模型文件點(diǎn)這里
接口搭建
一個(gè)完整的API接口從接收請求到響應(yīng)數(shù)據(jù)完成,中間這個(gè)過程就是服務(wù)端處理各種代碼邏輯的蝉绷。這其中主要包括暴露接口地址
,接口權(quán)限驗(yàn)證
鸭廷,請求參數(shù)驗(yàn)證
,查詢數(shù)據(jù)庫
熔吗,返回響應(yīng)信息
這幾個(gè)階段辆床。為了符合服務(wù)端業(yè)務(wù)邏輯分層設(shè)計(jì)的模式,每一個(gè)處理階段都可以抽離到一個(gè)單獨(dú)的模塊桅狠,最后再把各種相關(guān)聯(lián)的模塊組裝起來打包成一個(gè)完整的項(xiàng)目讼载,這樣的模塊化設(shè)計(jì)可以很大的增強(qiáng)項(xiàng)目的維護(hù)性
和可讀性
。用目錄結(jié)構(gòu)的方式展現(xiàn)就是這個(gè)樣子的
├── model // 數(shù)據(jù)庫模型
│ ├── administrator.js
│ ├── seller.js
│ ├── rating.js
│ ├── category.js
│ └── food.js
├── helper
│ ├── validatorRules.json // 參數(shù)驗(yàn)證規(guī)則
│ ├── mongoose.js // mongoose連接腳本
│ ├── middleware.js // 項(xiàng)目中間件
│ └── util.js // 工具函數(shù)
├── controller // 控制器
│ ├── administrator.js
│ ├── seller.js
│ ├── rating.js
│ ├── category.js
│ └── food.js
├── config
│ └── config.default.json // 項(xiàng)目配置文件
├── router
│ └── index.js // 路由配置
model
文件夾用來放數(shù)據(jù)庫表模型中跌,數(shù)據(jù)庫存儲(chǔ)了哪些字段在這個(gè)文件目錄查看一目了然咨堤。helper
目錄存放了一些輔助的項(xiàng)目文件和一些腳本,其中middleware.js
這個(gè)文件存放了整個(gè)項(xiàng)目的所有中間件漩符,按照模式分層的原則一喘,我把服務(wù)端接口的一些處理邏輯都抽離到了中間件里,其中包括接口權(quán)限驗(yàn)證
嗜暴,請求參數(shù)驗(yàn)證
這兩個(gè)主要的代碼處理邏輯凸克。controller
目錄則是存放接口業(yè)務(wù)邏輯的地方,我們也把他叫做控制器
闷沥,查詢數(shù)據(jù)庫
和返回響應(yīng)信息
也是在這個(gè)模塊里面完成的萎战。最后就是統(tǒng)一分發(fā)路由接口,router
目錄是項(xiàng)目所有接口分發(fā)的地方舆逃,在這里可以把不同的控制器分發(fā)到一個(gè)或多個(gè)路由接口地址上蚂维,這樣可以實(shí)現(xiàn)控制器文件的復(fù)用,不需要寫重復(fù)的業(yè)務(wù)代碼路狮。
權(quán)限驗(yàn)證和登錄(包含注冊功能)
面向多用戶服務(wù)的后端項(xiàng)目虫啥,權(quán)限驗(yàn)證是不可或缺的。本項(xiàng)目使用了authorization
請求頭驗(yàn)證的方式判斷每一個(gè)請求的權(quán)限奄妨。為了方便處理涂籽,我把這一塊的代碼邏輯抽離到了一個(gè)中間件里。這樣對每一個(gè)接口是否驗(yàn)證權(quán)限也容易管理和閱讀展蒂。本項(xiàng)目的權(quán)限驗(yàn)證使用jsonwebtoken
這個(gè)第三方插件作為生成秘鑰token
的工具又活,用戶在登錄的時(shí)候服務(wù)端會(huì)生成一個(gè)token
響應(yīng)到前端,前端根據(jù)運(yùn)行環(huán)境把它存儲(chǔ)下來锰悼,之后的每一個(gè)請求根據(jù)業(yè)務(wù)需要攜帶這個(gè)token
傳遞到服務(wù)端柳骄,服務(wù)端根據(jù)設(shè)置好的驗(yàn)證規(guī)則返回不同的驗(yàn)證結(jié)果,這就是本項(xiàng)目接口權(quán)限驗(yàn)證
整體運(yùn)行過程箕般。
通過控制器中的用戶登錄接口分析其中的業(yè)務(wù)邏輯時(shí)怎么處理的
// 這里僅展示業(yè)務(wù)邏輯代碼
async login(ctx) {
const { username, password } = ctx.request.body;
let result = await AdministratorModel.findOne({ username });
//如果沒有結(jié)果則 創(chuàng)建新用戶
if (!result) {
// 加密密碼
const hashPass = await bcrypt.hash(password, 10);
const newUser = await AdministratorModel.create({ password: hashPass, username });
const token = jwt.sign({ username, role: newUser.role, level: newUser.level }, secretKey, {
expiresIn,
});
return (ctx.body = { admin: omit(newUser.toObject(), ["password"]), token });
}
if (!bcrypt.compareSync(password, result.password)) {
ctx.status = 400;
return (ctx.body = { message: "密碼錯(cuò)誤" });
}
const user = result.toObject();
const token = jwt.sign(user, secretKey, { expiresIn });
ctx.body = { admin: omit(user, ["password"]), token };
},
為了支持管理后天首次登陸即注冊
的功能耐薯,本登錄代碼接口也包含了用戶注冊的業(yè)務(wù)邏輯。經(jīng)過參數(shù)解析和校驗(yàn)的過程后(代碼部分以中間件處理的方式在其他模塊)丝里,通過解構(gòu)即取到了前端傳遞的有效參數(shù)曲初。根據(jù)數(shù)據(jù)庫查詢的結(jié)果處理不同的業(yè)務(wù)邏輯,在取到創(chuàng)建后的用戶信息后需通過jsonwebtoken
的簽名生成一個(gè)token
杯聚,此token
也是其他接口在驗(yàn)證用戶登錄狀態(tài)時(shí)唯一的驗(yàn)證信息臼婆。
通過登錄接口生成token
后我們就可以對其他需要添加訪問權(quán)限的接口進(jìn)行鑒權(quán)驗(yàn)證了。下面是通過驗(yàn)證token
是否有效判斷用戶登錄狀態(tài)的邏輯代碼中間件
// 統(tǒng)一抽離到一個(gè)中間件中幌绍,這里省略了引入其他模塊的過程
module.exports={
adminRequired() {
return async (ctx, next) => {
let token = ctx.headers["authorization"];
if (!token) {
ctx.status = 400;
return (ctx.body = { message: "沒有傳遞token" });
}
token = token.split(" ")[1];
try {
var decodeToken = jwt.verify(token, secretKey, { expiresIn });
} catch (error) {
ctx.status = 403;
if (error.name === "TokenExpiredError") {
return (ctx.body = { message: "過期的token" });
}
return (ctx.body = { message: "無效的token" });
}
ctx.state.adminInfo = decodeToken;
await next();
};
},
}
請求進(jìn)入到這里后颁褂,通過認(rèn)證請求頭先提取到token
變量,當(dāng)token
取到具體值后傀广,再使用jsonwebtoken
內(nèi)部提供的驗(yàn)證函數(shù)校驗(yàn)颁独,根據(jù)不同的驗(yàn)證結(jié)果響應(yīng)不同的狀態(tài)碼和錯(cuò)誤信息。具體的驗(yàn)證結(jié)果錯(cuò)誤類型自行到插件倉庫查看伪冰,這里不做詳細(xì)介紹誓酒。當(dāng)驗(yàn)證通過后會(huì)解析出token
的簽名內(nèi)容,如果鑒權(quán)接口其他地方的業(yè)務(wù)邏輯需要用到此信息的話贮聂,我們可以把它掛載到koa
提供的特定命名空間字段上靠柑,這樣方便局部的邏輯代碼獲取。
備注:
token
使用的認(rèn)證類型需要根據(jù)前后端開發(fā)人員的約定使用寂汇,本項(xiàng)目使用Bearer ${token}
的格式作為令牌訪問頭
為了符合koa
中間件導(dǎo)出格式的設(shè)計(jì)原則病往,這個(gè)文件的中間件是以閉包的形式導(dǎo)出的,實(shí)際應(yīng)用到接口上的是這個(gè)閉包函數(shù)骄瓣,這樣設(shè)計(jì)的好處是我們在調(diào)用中間件函數(shù)的時(shí)可以傳遞參數(shù)進(jìn)去停巷,內(nèi)部實(shí)際生效的中間件可以根據(jù)外部傳遞的參數(shù)做邏輯上的處理。在路由配置表里面統(tǒng)一使用這個(gè)中間件的方式是這個(gè)樣子的
//部分代碼省略
const Router = require("koa-router");
const router = new Router({ prefix: "/api" });
const middleware = require("../helper/middleware");
router.post("/admin/foods", middleware.adminRequired(),FoodController.createOne);
數(shù)據(jù)庫分頁查詢功能
對于大多數(shù)前端項(xiàng)目榕栏,分頁顯示數(shù)據(jù)在一個(gè)非常常見的功能畔勤,對應(yīng)到服務(wù)端的代碼邏輯就是數(shù)據(jù)庫的過濾查詢。使用mongoose
提供的過濾查詢操作API
可以很容易完成這個(gè)需求扒磁,當(dāng)我們用到的地方比較多的時(shí)候庆揪,問題就出現(xiàn)了。對于前端請求的接口路徑一般是這個(gè)樣子的/api/foods?page=1&size=20
妨托,我們需要對傳遞的querystirng
做進(jìn)一步的判斷和解析才能應(yīng)用到數(shù)據(jù)庫參數(shù)的查詢上缸榛。問題是很多個(gè)接口都需要這個(gè)功能吝羞,使用起來比較繁瑣,那不如我們把這個(gè)解析查詢參數(shù)的過程抽離成一個(gè)模塊内颗,這樣更方便我們使用和維護(hù)【牛現(xiàn)在讓我們看一下封裝好的全部代碼吧!
module.exports = {
resolvePagination(pagination = {}) {
const defaults = { page: 1, size: 10 };
pagination.page = parseInt(pagination.page, 10);
pagination.size = parseInt(pagination.size, 10);
if (Number.isNaN(pagination.page) || pagination.page <= 0) {
pagination.page = defaults.page;
}
if (Number.isNaN(pagination.size) || pagination.size <= 0) {
pagination.size = defaults.size;
}
const { page, size } = pagination;
return {
page,
size,
};
},
resolveFilterOptions(filter = {}) {
let sort = {
createdAt: -1,
};
sort = defaults({}, filter.sort, sort);
const { page, pageSize } = resolvePagination({
page: filter.page,
size: filter.size,
});
return {
limit: size,
skip: (page - 1) * size,
sort,
};
},
};
首先通過resolvePagination
這個(gè)函數(shù)我們可以解析出有效的query
參數(shù)均澳,在通過resolveFilterOptions
這個(gè)函數(shù)解析出來符合mongoose
數(shù)據(jù)篩選操作的查詢選項(xiàng)恨溜。通過模塊化引入的操作方式,應(yīng)用到實(shí)際的數(shù)據(jù)庫查詢過程中如下
// 代碼片段來自項(xiàng)目`controller`目錄
const { resolveFilterOptions, resolvePagination } = require("../helper/utils");
module.exports={
async queryListByOpts(ctx) {
const { page, size } = resolvePagination({ page: ctx.query.page, size: ctx.query.size });
const { skip, limit, sort } = resolveFilterOptions({ page, size });
const total = await FoodModel.countDocuments();
var results = await FoodModel.find().populate("category").sort(sort).skip(skip).limit(limit);
ctx.body = {
data: results,
total,
pagination: {
page,
size,
},
};
},
}
從代碼中可以看到以獲取到前端傳遞的query
類型參數(shù)為解析值找前,resolvePagination
函數(shù)負(fù)責(zé)解析有效的數(shù)據(jù)分頁查詢選項(xiàng)糟袁,resolveFilterOptions
函數(shù)解析出來了mongoose
特定查詢語句格式的參數(shù),我們通過分離業(yè)務(wù)代碼和邏輯代碼的方式有效增強(qiáng)了代碼的模塊化結(jié)構(gòu)躺盛,也增加了代碼的復(fù)用性项戴,提高了項(xiàng)目的開發(fā)效率。
應(yīng)用的部署和運(yùn)行
本項(xiàng)目使用github-actions
的持續(xù)集成功能自動(dòng)部署到云服務(wù)器槽惫,有了持續(xù)集成的服務(wù)肯尺,就省去了項(xiàng)目手動(dòng)構(gòu)建,測試躯枢,發(fā)布這一系列流程则吟,而且降低了手動(dòng)操作程序出錯(cuò)的風(fēng)險(xiǎn),具體的配置文件如下
name: Deploy files
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: copy file via ssh key
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: ${{ secrets.SERVER_PORT }}
source: "*"
target: "/var/www/elm-seller-server"
- name: executing remote ssh commands using ssh key
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
key: ${{ secrets.SERVER_SSH_KEY }}
port: "22"
script: |
cd /var/www/elm-seller-server
npm install
npm start
從配置文件中看出锄蹂,服務(wù)器發(fā)布的環(huán)境變量都采用了加密的方式傳遞氓仲,比如${{secrets.SERVER_HOST}}
這個(gè)環(huán)境變量,真實(shí)的存儲(chǔ)值需要我們在github
倉庫的設(shè)置面板里的secret
選項(xiàng)配置的得糜,當(dāng)本地使用git
管理的倉庫推送到遠(yuǎn)程倉庫的時(shí)候就會(huì)觸發(fā)github-actions
的自動(dòng)部署操作敬扛,同時(shí)我們還可以在workflows
文件夾下面配置多個(gè)以.yml
結(jié)尾的配置文件,一個(gè)配置文件對應(yīng)一個(gè)actions
部署任務(wù)朝抖,本項(xiàng)目我就使用了兩個(gè)持續(xù)集成的任務(wù)啥箭,因?yàn)轫?xiàng)目對應(yīng)的說明文檔也需要及時(shí)的更新發(fā)布。至于部署文件模板怎么選擇需要根據(jù)個(gè)人的需求自己選擇設(shè)置治宣,github-actions
官方市場提供了常用的集成任務(wù)模板供我們選擇
發(fā)布到云服務(wù)器的應(yīng)用我選擇使用PM2
管理應(yīng)用急侥,應(yīng)用啟動(dòng)的配置文件點(diǎn)這里。pm2是一個(gè)面對node
應(yīng)用的管理工具侮邀,我們可以方便的查看坏怪,重啟,刪除绊茧,停止铝宵,啟動(dòng)應(yīng)用
API文檔編寫
文檔的撰寫是一個(gè)后端項(xiàng)目不可或缺的一部分內(nèi)容,學(xué)會(huì)寫文檔可以回顧項(xiàng)目從設(shè)計(jì)到開發(fā)的過程华畏,發(fā)現(xiàn)有問題的地方可以第一時(shí)間發(fā)現(xiàn)鹏秋,及時(shí)的修復(fù)bug
尊蚁。項(xiàng)目文檔是使用markdown
語法編寫的REAEME
文件,所有文件均在項(xiàng)目的docs
目錄內(nèi)侣夷。文檔使用vuepress
作為構(gòu)建工具預(yù)覽和發(fā)布枝誊。具體使用方式自行查看官方文檔,不做詳細(xì)介紹
文檔發(fā)布地址:https://konglingwen94.github.io/elm-seller-server
工具和環(huán)境
vscode
mac
node
mongodb
git
github
postman
ssh
總結(jié)心得
從項(xiàng)目的需求規(guī)劃惜纸,到數(shù)據(jù)庫表設(shè)計(jì),api接口邏輯關(guān)注點(diǎn)的分離绝骚,最后成功的部署運(yùn)行以及文檔的撰寫完成耐版,自己初步掌握了服務(wù)端項(xiàng)目完整的開發(fā)流程,并積累了一些開發(fā)經(jīng)驗(yàn)可以在這分享压汪。
作為編程開發(fā)人員粪牲,在項(xiàng)目開發(fā)過程中遇到困難是很正常的,尤其是在調(diào)試代碼的時(shí)候各種各樣的錯(cuò)誤信息看的"眼花繚亂",尤其是服務(wù)端node
的環(huán)境沒有瀏覽器客戶端調(diào)試方便止剖。遇到代碼出錯(cuò)不要怕腺阳,我們需要一步步排查出錯(cuò)的原因,如果錯(cuò)誤信息看起來不直觀我們可以借助第三方工具調(diào)試穿香,本項(xiàng)目我使用的是nodemon
這個(gè)工具亭引,他可以熱加載應(yīng)用,也可以開啟debug
的命令打開一個(gè)類似瀏覽器開發(fā)者工具的調(diào)試面板皮获,我們可以在控制臺面板查看程序拋出的錯(cuò)誤信息焙蚓,在source
面板查看出錯(cuò)的代碼堆棧,借助這些工具的分析洒宝,只要有耐心购公,一點(diǎn)點(diǎn)思考出現(xiàn)錯(cuò)誤的問題,最終我們一定可以解決它雁歌。
支持
感謝所有點(diǎn)贊和關(guān)注的小伙伴們宏浩,對本項(xiàng)目有興趣的同學(xué)可以一塊和我交流,歡迎在下面留言靠瞎!
如果您對本項(xiàng)目由好的建議或者發(fā)現(xiàn)bug
可以到項(xiàng)目倉庫提issues
,也歡迎您的收藏和關(guān)注比庄,謝謝!
倉庫地址:https://github.com/konglingwen94/elm-seller-server
文檔地址:https://konglingwen94.github.io/elm-seller-server
w