
前言
本文主要復(fù)盤筆者的nodeJS,通過一個線上的實戰(zhàn)案例來總結(jié)node生態(tài)常用的技術(shù)點和最佳實踐。后面會花費大概一個月的時間輸出3篇以實戰(zhàn)為主的nodeJs項目,本文是第一篇,主要介紹如何使用nodeJs開發(fā)一個圖床應(yīng)用。該項目對于測試和個人服務(wù)型網(wǎng)站非常實用,大家可以基于此擴展出更強大的應(yīng)用。本文的圖床項目主要使用Koa進行開發(fā),不熟悉的可以先研究一下koa官網(wǎng),或者看筆者之前寫的nodeJS的文章。
你將收獲
- Node應(yīng)用基本架構(gòu)方式以及開發(fā)NodeJS應(yīng)用的流程。
- Koa + Koa-Router + glob + Node基本API使用。
- 跨域解決方案Koa Cors的使用介紹,以及如何和前協(xié)作跨域。
- 基于@koa/multer封裝文件上傳中間件。
- 使用React開發(fā)前端應(yīng)用以及xui基本使用。
正文
首先圖床應(yīng)用要保證不同域下都可以訪問我們的圖片資源,不存在跨域問題,并且可以支持在不同域下的應(yīng)用都可以上傳圖片到圖床上,如下圖所示:

結(jié)合上圖我們可以先做應(yīng)用的需求分析:

以上是一個非常簡單的圖床應(yīng)用的需求分析,我們接下來將根據(jù)這個分析來搭建項目架構(gòu)并開發(fā)我們的應(yīng)用程序。在開始之前我們先看看簡單的實現(xiàn)效果:



這個展示界面只是一個例子,我們可以通過前端的方式設(shè)計專屬于自己的圖床管理界面。這里提供的公共API在任何域名下都是可以調(diào)用的,沒有跨域問題。
前臺地址:基于xui搭建的圖床界面前臺。
api開放地址:圖床開放地址(免費勿黑)。
1、Node應(yīng)用基本架構(gòu)方式以及開發(fā)NodeJS應(yīng)用的流程
有關(guān)nodejs的項目架構(gòu)以及如何組織nodejs目錄,我在30分鐘教你優(yōu)雅的搭建nodejs開發(fā)環(huán)境及目錄設(shè)計這篇文章中有詳細的說明,大家在讀完本文之后可以學習研究一下.
開發(fā)任何一個應(yīng)用之前首先要做的就是了解需求,需求理清楚之后就可以做技術(shù)選型了,開發(fā)基于nodeJS的后端應(yīng)用的技術(shù)方案很多,如果對nodejs很熟悉,完全可以使用原生nodejs來開發(fā)應(yīng)用; 對于中小型應(yīng)用我們可以直接采用Koa來開發(fā),其中間件機制和插拔式的設(shè)計理念可以很方便的讓我們開發(fā)自己的中間件;如果是涉及到比較復(fù)雜的業(yè)務(wù)線我們可以采用egg.js或者nest.js來作為nodeJS的框架選型,由于本文的圖床應(yīng)用比較簡單,所以筆者這里直接采用koa生態(tài)來做開發(fā). 接下來先看看我們圖床應(yīng)用的目錄結(jié)構(gòu):

2.Koa + Koa-Router + glob + Node基本API使用

學習koa最快的方式就是直接看官方文檔, koa的官方文檔非常簡單也非常詳細,所以不懂的可以先看看官網(wǎng).
(1)服務(wù)端路由(接口)設(shè)計
服務(wù)端路由我們主要使用koa-router, 使用方式也很簡單, 代碼如下:
const Koa = require('koa');
const Router = require('@koa/router');
const app = new Koa();
const router = new Router();
// 獲取列表的路由接口
router.get('/api/list', (ctx, next) => {
// 獲取列表的邏輯
});
// 上傳圖片的路由接口
router.post('/api/upload', (ctx, next) => {
// 上傳圖片的邏輯
});
app
.use(router.routes())
.use(router.allowedMethods());復(fù)制代碼
因為圖床的應(yīng)用非常簡單,我們這里就直接使用傳統(tǒng)的方式實現(xiàn), 有關(guān)nodeJS的MVC架構(gòu)可以參考我之前寫的node的文章.
(2)使用glob來批量獲取圖片路徑
這里批量獲取圖片路徑我們主要使用glob來通過遍歷目錄來獲取, 這種方式在圖片數(shù)據(jù)量小的時候可以使用,但是一旦圖片量指數(shù)級增長,更建議用數(shù)據(jù)庫來存取,畢竟IO操作還是比較費性能的.筆者這里為了方便采用glob來實現(xiàn). glob是一個基于node的第三發(fā)庫,支持我們使用模式匹配的方式遍歷文件目錄, 具體用法如下:
import glob from 'glob'
// 讀取文件
router.get('/api/v0/files',
ctx => {
const files = glob.sync(`${staticPath}/uploads/*`)
const result = files.map(item => {
return `${config.staticPath}${item.split('public')[1]}`
})
ctx.body = {
state: 200,
result
}
}
);復(fù)制代碼
這樣就實現(xiàn)了批量獲取圖片的api,是不是很簡單呢? 我們只需要訪問這個接口,就可以拿到圖床的所有圖片列表了.當我們訪問這個接口時,會返回如下數(shù)據(jù):

3、跨域解決方案Koa Cors的使用介紹,以及如何和前協(xié)作跨域
由于瀏覽器同源策略,凡是發(fā)送請求url的協(xié)議、域名、端口三者之間任意一個與當前頁面地址不同就被算作跨域。實現(xiàn)跨域的方式也很多,比如JSONP跨域,nginx反向代理,服務(wù)器端修改header,設(shè)置document.domain,使用postMessage技術(shù)等,但是目前主流的方式還是基于cors來實現(xiàn).
為了讓圖床提供的服務(wù)給不同的域使用, 我們需要配置跨域,這里我們采用koa2-cors提供的應(yīng)答式跨域解決方案,其實原理也很簡單,就是配置http的請求響應(yīng)頭信息, 讓我們的服務(wù)器支持不同的ip訪問.其基本用法如下:
import cors from 'koa2-cors'
// 設(shè)置跨域
app.use(cors({
origin: function (ctx) {
console.log(111, ctx.url)
if (ctx.url.indexOf('/api/v0') > -1) {
return "*"; // 允許來自所有域名請求
}
return 'http://qutanqianduan.com'; // 這樣就能只允許 http://qutanqianduan.com 這個域名的請求了
},
exposeHeaders: ['WWW-Authenticate', 'Server-Authorization'], // 獲取額外的header信息
maxAge: 5, // 該字段可選,用來指定本次預(yù)檢請求的有效期,單位為秒
credentials: true,
allowMethods: ['GET', 'POST', 'DELETE'], // 請求允許的方法
allowHeaders: ['Content-Type', 'Authorization', 'Accept', 'x-requested-with'] // 允許的header字段名
}))復(fù)制代碼
通過以上的配置,我們就可以實現(xiàn)基本的跨域了.如果我們想只讓某些特定的接口實現(xiàn)跨域,我們可以設(shè)置接口白名單, 也可以通過設(shè)置域名白名單來達到只讓特定的域名訪問我們的api接口.這種情況更適用于公司內(nèi)部多個子系統(tǒng)間互相協(xié)作通信的情景.
4、基于@koa/multer封裝文件上傳中間件
服務(wù)器要想接受客戶端上傳的文件,我們還需要提供文件上傳接口, 這里筆者采用koa生態(tài)比較主流的實現(xiàn)方式@koa/multer. 具體使用介紹官網(wǎng)寫的也很詳細,大家可以看官網(wǎng)學習@koa/multer。
(1)實現(xiàn)文件上傳接口
接下來我們基于它實現(xiàn)文件上傳中間件.具體實現(xiàn)如下:
import multer from '@koa/multer'
import { resolve } from 'path'
import fs from 'fs'
const rootImages = resolve(__dirname, '../../public/uploads')
//上傳文件存放路徑、及文件命名
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, rootImages)
},
filename: function (req, file, cb) {
let [name, type] = file.originalname.split('.');
cb(null, `${name}_${Date.now().toString(16)}.${type}`)
}
})
//文件上傳限制
const limits = {
fields: 10,//非文件字段的數(shù)量
fileSize: 1024 * 1024 * 2,//文件大小 單位 b
files: 1//文件數(shù)量
}
export const復(fù)制代碼
由以上代碼可知我們在destination目錄下設(shè)置了文件上傳的目標目錄, 通過filename接口來設(shè)置上傳之后的文件名. limits是對文件操作的限制,具體的可以根據(jù)自己的需求來配置.
其次結(jié)合koa-router來實現(xiàn)文件上傳接口:
// lib/upload.js
// 為了捕獲multer的錯誤
export const uploadSingleCatchError = async (ctx, next) => {
let err = await upload.single('file')(ctx, next).then(res => res)
.catch(err => err);
if(err) {
ctx.status = 500
ctx.body = {
state: 500,
msg: err.message
}
}
}
// index.js
// 上傳文件
router.post('/api/v0/upload', uploadSingleCatchError,
ctx => {
let { filename, path, size } = ctx.file;
let { source } = ctx.request.body || 'unknow';
let url = `${config.staticPath}${path.split('/public')[1]}`
ctx.body = {
state: 200,
filename,
url,
source,
size
}
}
);復(fù)制代碼
這樣我們就能通過任意一個客戶端上傳圖片到我們的圖床上了.
(2)刪除文件接口實現(xiàn)
我們用原生nodejs實現(xiàn)刪除文件的功能, 這里會用到fs模塊,具體實現(xiàn)如下:
// lib/upload.js
// 刪除文件
export const delFile = (path) => {
return new Promise((resolve, reject) => {
fs.unlink(path, (err) => {
if(err) {
reject(err)
}else {
resolve(null)
}
})
})
}// 刪除文件接口
router.get('/api/v0/del',
async ctx => {
const { id } = ctx.query
if(id) {
const err = await delFile(`${staticPath}/uploads/${id}`)
if(!err) {
ctx.body = {
state: 200,
result: '刪除成功'
}
}else {
ctx.code = 500
ctx.body = {
state: 500,
result: '文件不存在,刪除失敗'
}
}
}else500 ctx.body = {
state: 500,
result: 'id不能為空'
}
}
}
)復(fù)制代碼
這樣,我們在自己的客戶端應(yīng)用中點擊刪除按鈕就可以刪除圖床上的文件了.當然本圖傳應(yīng)用還有很多接口實現(xiàn)細節(jié), 這里就不一一介紹了,感興趣的朋友可以研究一下.
5、使用React開發(fā)前端應(yīng)用以及xui基本使用
接下來借來實現(xiàn)我們的圖床客戶端,客戶端的實現(xiàn)以及設(shè)計風格完全可以由自己來定,所以這里只是介紹一下筆者實現(xiàn)的客戶端,筆者將采用react全家桶以及自己開發(fā)的第三方ui庫xui——基于react的輕量級UI組件庫來實現(xiàn),關(guān)于如何開發(fā)一個專屬于自己的組件庫,可以參考筆者之前的文章. 首先我們簡單開發(fā)一個圖床應(yīng)用的界面:

我們先引入組件庫:
import React, { Component } from 'react'
import {
Notification,
message,
Layout,
Icon
} from '@alex_xu/xui'
const復(fù)制代碼
接著搭建我們的頁面:
class UploadPage extends Component {
state = {
fileList: []
}
componentDidMount() {
fetch(apiUrl + '/files').then(res => res.json()).then(res => {
this.setState({
fileList: res.result
})
})
}
showAddress = (item) => {
Notification.config({
placement: 'topRight',
})
Notification.pop({
type: 'success',
message: '圖片地址',
duration: 10,
description: item
})
}
render() {
return (
<div className="upload-wrap">
<Layout>
<Header fixed>
<div className="logo"><Icon type="FaBattleNet" style={{fontSize: '30px', marginRight: '12px'}} />XOSS</div>
</Header>
<Content style={{marginTop: '48px', backgroundColor: '#f0f2f5'}}>
{
this.state.fileList.map((item, i) => {
return <div key={i} className="imgBox" notallow={this.showAddress.bind(this, item)}>
<img src={item} alt=""/>
<span className="del-btn" notallow={this.delFile.bind(this, item)}><Icon type="FaMinusCircle" style={{fontSize: '24px'}} /></span>
</div>
})
}
</Content>
<Footer style={{color: 'rgba(0,0,0, .5)'}}>趣談前端 -- 徐小夕</Footer>
</Layout>
</div>
)
}
}
export default UploadPage復(fù)制代碼
關(guān)于http庫我們可以使用任何一種主流的庫比如axios, umi-request等. 本客戶端代碼已發(fā)布到github,大家可以clone本地運行一下:
基于react+redux+redux-thunk+xui開發(fā)的todoOA管理平臺。
本文轉(zhuǎn)載自微信公眾號「趣談前端」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系趣談前端公眾號。
