專案背景
前端開發過程中常常需要用到的圖片等資源,除了使用常見的第三方圖床外,我們也可以自己搭建一個私有圖床,為團隊提供前端基礎服務。本文旨在回顧總結下自建圖床的後端部分實現方案,希望能夠給有類似需求的同學一些借鑑和方案。另外說一下,由於是前端基礎建設,這裡我們完全由前端同學所熟悉的node.js來實現所需要的後端服務需求。
方案
後端部分架構選型,由於這裡主要是為前端業務開發人員提供基建服務,而集團平臺也提供了各種雲服務,並且並不會出現過多的高併發等場景,因而在語言選擇上還是以前端同學所熟悉的node.js為主,這裡我們團隊主要以express框架為主,在整個大的專網技術團隊中,後端仍然以java為主,node主要作為中間層BFF來對部分介面進行開發聚合等,因而主體仍然以單體架構為主,微服務形式則採用service mesh的雲服務產品(如:istio)來和java同學進行配合,而沒有采用一些node.js的微服務框架(比如:nest.js中有微服務相關的設定,以及seneca等)。由於是單體應用,鑑於express的中介軟體機制,通過路由對不同模組進行了分離,本圖床服務中提供的服務都隔離在imagepic的模組下;在資料庫選擇方面,圖床這裡僅僅需要一個鑑權機制,其他並沒有特別額外的持久化需求,這裡我選擇了mongodb作為資料庫持久化資料(ps:雲中介軟體提供的mongodb出現了接入問題,後續通過CFS(檔案儲存系統)+FaaS來實現了替代方案);由於圖床功能的特殊性,對於上傳圖片進行了流的轉換,這裡會用到一個臨時圖片儲存的過程,通過雲產品的CFS(檔案儲存系統)來進行持久化儲存,定期進行資料的刪除;而真正的圖片儲存則是放在了COS(物件儲存)中,相較於CFS的檔案介面規範,COS則是基於亞馬遜的S3規範的,因而這裡更適宜於作為圖片的儲存載體
目錄
db
- \_\_temp\_\_
- imagepic
deploy
dev
- Dockerfile
- pv.yaml
- pvc.yaml
- server.yaml
production
- Dockerfile
- pv.yaml
- pvc.yaml
- server.yaml
- build.sh
faas
- index.js
- model.js
- operator.js
- read.js
- utils.js
- write.js
server
api
- openapi.yaml
lib
- index.js
- cloud.js
- jwt.js
- mongodb.js
routes
imagepic
- auth
- bucket
- notification
- object
- policy
- index.js
- minio.js
- router.js
utils
- index.js
- is.js
- pagination.js
- reg.js
- uuid.js
- app.js
- config.js
- index.js
- main.js
實踐
對涉及到部分介面需要進行鑑權判斷,這裡使用的是jwt進行相關的許可權校驗
原始碼
faas
這裡抽象出來了雲函式來為後端服務提供能力,模擬實現類似mongodb相關的一些資料庫操作
model.js
定義的model相關的資料格式
/**
* documents 資料結構
* @params
* _name String 檔案的名稱
* _collections Array 檔案的集合
* @examples
* const documents = {
* "_name": String,
* "_collections": Array
* }
*/
exports.DOCUMENTS_SCHEMA = {
"_name": String,
"_collections": Array
}
/**
* collections 資料結構
* @params
* _id String 集合的預設id
* _v Number 集合的自增數列
* @examples
* const collections = {
* "_id": String,
* "_v": Number,
* }
*/
exports.COLLECTIONS_SCHEMA = {
"_id": String
}
read.js
node的fs模組讀檔案操作
const {
isExit,
genCollection,
genDocument,
findCollection,
findLog,
stringify,
fs,
compose,
path
} = require('./utils');
exports.read = async (method, ...args) => {
let col = '', log = '';
const isFileExit = isExit(args[0], `${args[1]}_${args[2]['phone']}.json`);
console.log('isFileExit', isFileExit)
const doc = genDocument(...args);
switch (method) {
case 'FIND':
col = compose( stringify, findCollection )(doc, genCollection(...args));
log = compose( stringify, findLog, genCollection )(...args);
break;
};
if(isFileExit) {
return fs.promises.readFile(path.resolve(__dirname, `../db/${args.slice(0,2).join('/')}_${args[2][`phone`]}.json`), {encoding: 'utf-8'}).then(res => {
console.log('res', res);
console.log(log)
return {
flag: true,
data: res,
};
})
} else {
return {
flag: false,
data: {}
};
}
};
write.js
node的fs模組的寫檔案操作
const {
isExit,
fs,
path,
stringify,
compose,
genCollection,
addCollection,
addLog,
updateCollection,
updateLog,
removeCollection,
removeLog,
genDocument
} = require('./utils');
exports.write = async (method, ...args) => {
console.log('write args', args, typeof args[2]);
const isDirExit = isExit(args.slice(0, 1));
const doc = genDocument(...args);
let col = '', log = '';
switch (method) {
case 'ADD':
col = compose( stringify, addCollection )(doc, genCollection(...args));
log = compose( stringify, addLog, genCollection )(...args);
break;
case 'REMOVE':
col = compose( stringify, removeCollection )(doc, genCollection(...args));
log = compose( stringify ,removeLog, genCollection )(...args);
break;
case 'UPDATE':
col = compose( stringify, updateCollection )(doc, genCollection(...args));
log = compose( stringify, updateLog, genCollection )(...args);
break;
}
if (!isDirExit) {
return fs.promises.mkdir(path.resolve(__dirname, `../db/${args[0]}`))
.then(() => {
console.log(`建立資料庫${args[0]}成功`);
return true;
})
.then(flag => {
if (flag) {
return fs.promises.writeFile(path.resolve(__dirname, `../db/${args.slice(0,2).join('/')}_${args[2][`phone`]}.json`), col)
.then(() => {
console.log(log);
return true;
})
.catch(err => console.error(err))
}
})
.catch(err => console.error(err))
} else {
return fs.promises.writeFile(path.resolve(__dirname, `../db/${args.slice(0,2).join('/')}_${args[2][`phone`]}.json`), col)
.then(() => {
console.log(log)
return true;
})
.catch(err => console.error(err))
}
};
operator.js
const { read } = require('./read');
const { write } = require('./write');
exports.find = async (...args) => await read('FIND', ...args);
exports.remove = async (...args) => await write('REMOVE', ...args);
exports.add = async (...args) => await write('ADD', ...args);
exports.update = async (...args) => await write('UPDATE', ...args);
utils.js
共用工具包
const { DOCUMENTS_SCHEMA, COLLECTIONS_SCHEMA } = require('./model');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const fs = require('fs');
exports.path = path;
exports.uuid = uuidv4;
exports.fs = fs;
exports.compose = (...funcs) => {
if(funcs.length===0){
return arg=>arg;
}
if(funcs.length===1){
return funcs[0];
}
return funcs.reduce((a,b)=>(...args)=>a(b(...args)));
};
exports.stringify = arg => JSON.stringify(arg);
exports.isExit = (...args) => fs.existsSync(path.resolve(__dirname, `../db/${args.join('/')}`));
console.log('DOCUMENTS_SCHEMA', DOCUMENTS_SCHEMA);
exports.genDocument = (...args) => {
return {
_name: args[1],
_collections: []
}
};
console.log('COLLECTIONS_SCHEMA', COLLECTIONS_SCHEMA);
exports.genCollection = (...args) => {
return {
_id: uuidv4(),
...args[2]
}
};
exports.addCollection = ( doc, col ) => {
doc._collections.push(col);
return doc;
};
exports.removeCollection = ( doc, col ) => {
for(let i = 0; i < doc._collections.length; i++) {
if(doc._collections[i][`_id`] == col._id) {
doc._collections.splice(i,1)
}
}
return doc;
};
exports.findCollection = ( doc, col ) => {
return doc._collections.filter(f => f._id == col._id)[0];
};
exports.updateCollection = ( doc, col ) => {
doc._collections = [col];
return doc;
};
exports.addLog = (arg) => {
return `增加了集合 ${JSON.stringify(arg)}`
};
exports.removeLog = () => {
return `移除集合成功`
};
exports.findLog = () => {
return `查詢集合成功`
};
exports.updateLog = (arg) => {
return `更新了集合 ${JSON.stringify(arg)}`
};
lib
cloud.js
業務操作使用雲函式
const {
find,
update,
remove,
add
} = require('../../faas');
exports.cloud_register = async (dir, file, params) => {
const findResponse = await find(dir, file, params);
if (findResponse.flag) {
return {
flag: false,
msg: '已註冊'
}
} else {
const r = await add(dir, file, params);
console.log('cloud_register', r)
if (r) {
return {
flag: true,
msg: '成功'
}
} else {
return {
flag: false,
msg: '失敗'
}
}
}
}
exports.cloud_login = async (dir, file, params) => {
const r = await find(dir, file, params);
console.log('cloud_read', r)
if (r.flag == true) {
if (JSON.parse(r.data)._collections[0].upwd === params.upwd) {
return {
flag: true,
msg: '登入成功'
}
} else {
return {
flag: false,
msg: '密碼不正確'
}
}
} else {
return {
flag: false,
msg: '失敗'
}
}
}
exports.cloud_change = async (dir, file, params) => {
const r = await update(dir, file, params);
console.log('cloud_change', r)
if (r) {
return {
flag: true,
msg: '修改密碼成功'
}
} else {
return {
flag: false,
msg: '失敗'
}
}
}
jwt.js
jwt驗證相關配置
const jwt = require('jsonwebtoken');
const {
find
} = require('../../faas');
exports.jwt = jwt;
const expireTime = 60 * 60;
exports.signToken = (rawData, secret) => {
return jwt.sign(rawData, secret, {
expiresIn: expireTime
});
};
exports.verifyToken = (token, secret) => {
return jwt.verify(token, secret, async function (err, decoded) {
if (err) {
console.error(err);
return {
flag: false,
msg: err
}
}
console.log('decoded', decoded, typeof decoded);
const {
phone,
upwd
} = decoded;
let r = await find('imagepic', 'auth', {
phone,
upwd
});
console.log('r', r)
if (r.flag == true) {
if (JSON.parse(r.data)._collections[0].upwd === decoded.upwd) {
return {
flag: true,
msg: '驗證成功'
}
} else {
return {
flag: false,
msg: '登入密碼不正確'
}
}
} else {
return {
flag: false,
msg: '登入使用者未找到'
}
}
});
}
auth
用於登入註冊驗證
const router = require('../../router');
const url = require('url');
const {
pagination,
isEmpty,
isArray,
PWD_REG,
NAME_REG,
EMAIL_REG,
PHONE_REG
} = require('../../../utils');
const {
// mongoose,
cloud_register,
cloud_login,
cloud_change,
signToken
} = require('../../../lib');
// const Schema = mongoose.Schema;
/**
* @openapi
* /imagepic/auth/register:
post:
summary: 註冊
tags:
- listObjects
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/register'
responses:
'200':
content:
application/json:
example:
code: "0"
data: {}
msg: "成功"
success: true
*/
router.post('/register', async function (req, res) {
const params = req.body;
console.log('params', params);
let flag = true,
err = [];
const {
name,
tfs,
email,
phone,
upwd
} = params;
flag = flag && PWD_REG.test(upwd) &&
EMAIL_REG.test(email) &&
PHONE_REG.test(phone);
if (!PWD_REG.test(upwd)) err.push('密碼不符合規範');
if (!EMAIL_REG.test(email)) err.push('郵箱填寫不符合規範');
if (!PHONE_REG.test(phone)) err.push('手機號碼填寫不符合規範');
// const registerSchema = new Schema({
// name: String,
// tfs: String,
// email: String,
// phone: String,
// upwd: String
// });
// const Register = mongoose.model('Register', registerSchema);
if (flag) {
// const register = new Register({
// name,
// tfs,
// email,
// phone,
// upwd
// });
// register.save().then((result)=>{
// console.log("成功的回撥", result);
// res.json({
// code: "0",
// data: {},
// msg: '成功',
// success: true
// });
// },(err)=>{
// console.log("失敗的回撥", err);
// res.json({
// code: "-1",
// data: {
// err: err
// },
// msg: '失敗',
// success: false
// });
// });
let r = await cloud_register('imagepic', 'auth', {
name,
tfs,
email,
phone,
upwd
});
if (r.flag) {
res.json({
code: "0",
data: {},
msg: '成功',
success: true
});
} else {
res.json({
code: "-1",
data: {
err: r.msg
},
msg: '失敗',
success: false
});
}
} else {
res.json({
code: "-1",
data: {
err: err.join(',')
},
msg: '失敗',
success: false
})
}
});
/**
* @openapi
* /imagepic/auth/login:
post:
summary: 登入
tags:
- listObjects
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/login'
responses:
'200':
content:
application/json:
example:
code: "0"
data: {token:'xxx'}
msg: "成功"
success: true
*/
router.post('/login', async function (req, res) {
const params = req.body;
console.log('params', params);
let flag = true,
err = [];
const {
phone,
upwd
} = params;
flag = flag && PWD_REG.test(upwd) &&
PHONE_REG.test(phone);
if (!PWD_REG.test(upwd)) err.push('密碼不符合規範');
if (!PHONE_REG.test(phone)) err.push('手機號碼填寫不符合規範');
// const registerSchema = new Schema({
// name: String,
// tfs: String,
// email: String,
// phone: String,
// upwd: String
// });
// const Register = mongoose.model('Register', registerSchema);
if (flag) {
// const register = new Register({
// name,
// tfs,
// email,
// phone,
// upwd
// });
// register.save().then((result)=>{
// console.log("成功的回撥", result);
// res.json({
// code: "0",
// data: {},
// msg: '成功',
// success: true
// });
// },(err)=>{
// console.log("失敗的回撥", err);
// res.json({
// code: "-1",
// data: {
// err: err
// },
// msg: '失敗',
// success: false
// });
// });
let r = await cloud_login('imagepic', 'auth', {
phone,
upwd
});
if (r.flag) {
const token = signToken({
phone,
upwd
}, 'imagepic');
// console.log('token', token)
res.json({
code: "0",
data: {
token: token
},
msg: '成功',
success: true
});
} else {
res.json({
code: "-1",
data: {
err: r.msg
},
msg: '失敗',
success: false
});
}
} else {
res.json({
code: "-1",
data: {
err: err.join(',')
},
msg: '失敗',
success: false
})
}
});
/**
* @openapi
* /imagepic/auth/change:
post:
summary: 修改密碼
tags:
- listObjects
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/change'
responses:
'200':
content:
application/json:
example:
code: "0"
data: {token:'xxx'}
msg: "成功"
success: true
*/
router.post('/change', async function (req, res) {
const params = req.body;
console.log('params', params);
let flag = true,
err = [];
const {
phone,
opwd,
npwd
} = params;
flag = flag && PWD_REG.test(opwd) &&
PWD_REG.test(npwd) &&
PHONE_REG.test(phone);
if (!PWD_REG.test(opwd)) err.push('舊密碼不符合規範');
if (!PWD_REG.test(npwd)) err.push('新密碼不符合規範');
if (!PHONE_REG.test(phone)) err.push('手機號碼填寫不符合規範');
if (flag) {
let r = await cloud_login('imagepic', 'auth', {
phone: phone,
upwd: opwd
});
if (r.flag) {
const changeResponse = await cloud_change('imagepic', 'auth', {
phone: phone,
upwd: npwd
});
if(changeResponse.flag) {
res.json({
code: "0",
data: {},
msg: '成功',
success: true
});
} else {
res.json({
code: "-1",
data: {
err: changeResponse.msg
},
msg: '失敗',
success: false
});
}
} else {
res.json({
code: "-1",
data: {
err: r.msg
},
msg: '失敗',
success: false
});
}
} else {
res.json({
code: "-1",
data: {
err: err.join(',')
},
msg: '失敗',
success: false
})
}
})
module.exports = router;
bucket
桶操作相關的介面
const minio = require('../minio');
const router = require('../../router');
const url = require('url');
const {
pagination,
isEmpty,
isArray
} = require('../../../utils');
/**
* @openapi
* /imagepic/bucket/listBuckets:
summary: 查詢所有儲存桶
get:
parameters:
- name: pageSize
name: pageNum
in: query
description: user id.
required: false
tags:
- List
responses:
'200':
content:
application/json:
example:
code: "0"
data: [
{
"name": "5g-fe-file",
"creationDate": "2021-06-04T10:01:42.664Z"
},
{
"name": "5g-fe-image",
"creationDate": "2021-05-28T01:34:50.375Z"
}
]
message: "成功"
success: true
*/
router.get('/listBuckets', function (req, res) {
const params = url.parse(req.url, true).query;
console.log('params', params);
minio.listBuckets(function (err, buckets) {
if (err) return console.log(err)
// console.log('buckets :', buckets);
res.json({
code: "0",
// 分頁處理
data: isEmpty(params) ?
buckets :
isArray(buckets) ?
( params.pageSize && params.pageNum ) ?
pagination(buckets, params.pageSize, params.pageNum) :
[] :
[],
msg: '成功',
success: true
})
})
})
module.exports = router;
object
用於圖片物件相關的介面
const minio = require('../minio');
const router = require('../../router');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const {
pagination
} = require('../../../utils');
const {
verifyToken
} = require('../../../lib');
/**
* @openapi
* /imagepic/object/listObjects:
get:
summary: 獲取儲存桶中的所有物件
tags:
- listObjects
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/listObjects'
responses:
'200':
content:
application/json:
example:
code: "0"
data: 49000
msg: "成功"
success: true
*/
router.post('/listObjects', function (req, res) {
const params = req.body;
// console.log('listObjects params', params)
const {
bucketName,
prefix,
pageSize,
pageNum
} = params;
const stream = minio.listObjects(bucketName, prefix || '', false)
let flag = false,
data = [];
stream.on('data', function (obj) {
data.push(obj);
flag = true;
})
stream.on('error', function (err) {
console.log(err)
data = err;
flag = false;
})
stream.on('end', function (err) {
if (flag) {
// 分頁處理
res.json({
code: "0",
data: pageNum == -1 ? {
total: data.length,
lists: data
} : {
total: data.length,
lists: pagination(data, pageSize || 10, pageNum || 1)
},
msg: '成功',
success: true
})
} else {
res.json({
code: "-1",
data: err,
msg: '失敗',
success: false
})
}
})
})
/**
* @openapi
* /imagepic/object/getObject:
post:
summary: 下載物件
tags:
- getObject
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/getObject'
responses:
'200':
content:
application/json:
example:
code: "0"
data: 49000
msg: "成功"
success: true
*/
router.post('/getObject', function (req, res) {
const params = req.body;
// console.log('statObject params', params)
const {
bucketName,
objectName
} = params;
minio.getObject(bucketName, objectName, function (err, dataStream) {
if (err) {
return console.log(err)
}
let size = 0;
dataStream.on('data', function (chunk) {
size += chunk.length
})
dataStream.on('end', function () {
res.json({
code: "0",
data: size,
msg: '成功',
success: true
})
})
dataStream.on('error', function (err) {
res.json({
code: "-1",
data: err,
msg: '失敗',
success: false
})
})
})
})
/**
* @openapi
* /imagepic/object/statObject:
post:
summary: 獲取物件後設資料
tags:
- statObject
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/statObject'
responses:
'200':
content:
application/json:
example:
code: "0"
data: {
"size": 47900,
"metaData": {
"content-type": "image/png"
},
"lastModified": "2021-10-14T07:24:59.000Z",
"versionId": null,
"etag": "c8a447108f1a3cebe649165b86b7c997"
}
msg: "成功"
success: true
*/
router.post('/statObject', function (req, res) {
const params = req.body;
// console.log('statObject params', params)
const {
bucketName,
objectName
} = params;
minio.statObject(bucketName, objectName, function (err, stat) {
if (err) {
return console.log(err)
}
// console.log(stat)
res.json({
code: "0",
data: stat,
msg: '成功',
success: true
})
})
})
/**
* @openapi
* /imagepic/object/presignedGetObject:
post:
summary: 獲取物件臨時連線
tags:
- presignedGetObject
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/presignedGetObject'
responses:
'200':
content:
application/json:
example:
code: "0"
data: "http://172.24.128.7/epnoss-antd-fe/b-ability-close.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=7RGX0TJQE5OX9BS030X6%2F20211126%2Fdefault%2Fs3%2Faws4_request&X-Amz-Date=20211126T031946Z&X-Amz-Expires=604800&X-Amz-SignedHeaders=host&X-Amz-Signature=27644907283beee2b5d6f468ba793db06cd704e7b3fb1c334f14665e0a8b6ae4"
msg: "成功"
success: true
*/
router.post('/presignedGetObject', function (req, res) {
const params = req.body;
// console.log('statObject params', params)
const {
bucketName,
objectName,
expiry
} = params;
minio.presignedGetObject(bucketName, objectName, expiry || 7 * 24 * 60 * 60, function (err, presignedUrl) {
if (err) {
return console.log(err)
}
// console.log(presignedUrl)
res.json({
code: "0",
data: presignedUrl,
msg: '成功',
success: true
})
})
})
/**
* @openapi
* /imagepic/object/putObject:
post:
summary: 上傳圖片
tags:
- putObject
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/putObject'
responses:
'200':
content:
application/json:
example:
code: "0"
data: ""
msg: "成功"
success: true
*/
router.post('/putObject', multer({
dest: path.resolve(__dirname, '../../../../db/__temp__')
}).single('file'), async function (req, res) {
console.log('/putObject', req.file, req.headers);
const verifyResponse = await verifyToken(req.headers.authorization, 'imagepic');
console.log('verifyResponse', verifyResponse)
const bucketName = req.headers.bucket,
folder = req.headers.folder,
originName = req.file['originalname'],
file = req.file['path'],
ext = path.extname(req.file['originalname']),
fileName = req.file['filename'];
console.log('folder', folder);
if (!verifyResponse.flag) {
fs.unlink(path.resolve(__dirname, `../../../../db/__temp__/${fileName}`), function (err) {
if (err) {
console.error(`刪除檔案 ${fileName} 失敗,失敗原因:${err}`)
}
console.log(`刪除檔案 ${fileName} 成功`)
});
return res.json({
code: "-1",
data: verifyResponse.msg,
msg: '未滿足許可權',
success: false
})
} else {
const fullName = folder ? `${folder}/${originName}` : `${originName}`;
fs.stat(file, function (err, stats) {
if (err) {
return console.log(err)
}
minio.putObject(bucketName, fullName, fs.createReadStream(file), stats.size, {
'Content-Type': `image/${ext}`
}, function (err, etag) {
fs.unlink(path.resolve(__dirname, `../../../../db/__temp__/${fileName}`), function (err) {
if (err) {
console.error(`刪除檔案 ${fileName} 失敗,失敗原因:${err}`)
}
console.log(`刪除檔案 ${fileName} 成功`)
});
if (err) {
return res.json({
code: "-1",
data: err,
msg: '失敗',
success: false
})
} else {
return res.json({
code: "0",
data: etag,
msg: '成功',
success: true
})
}
})
})
}
});
/**
* @openapi
* /imagepic/object/removeObject:
post:
summary: 刪除圖片
tags:
- removeObject
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/removeObject'
responses:
'200':
content:
application/json:
example:
code: "0"
data: ""
msg: "成功"
success: true
*/
router.post('/removeObject', async function (req, res) {
console.log('/removeObject', req.body, req.headers);
const verifyResponse = await verifyToken(req.headers.authorization, 'imagepic');
if (!verifyResponse.flag) {
return res.json({
code: "-1",
data: verifyResponse.msg,
msg: '未滿足許可權',
success: false
})
} else {
const {
bucketName,
objectName
} = req.body;
minio.removeObject(bucketName, objectName, function (err) {
if (err) {
return res.json({
code: "-1",
data: err,
msg: '失敗',
success: false
})
}
return res.json({
code: "0",
data: {},
msg: '成功',
success: true
})
})
}
});
module.exports = router;
總結
在針對前端圖床的後端介面開發過程中,切實感受到使用Serverless方式進行資料側開發的簡單,對於node.js來說更好的使用faas形式進行相關的函式粒度的業務開發可能更加有適用場景,而對於其他目前已有的一些其他場景,node.js在後端市場中其實很難撼動java、go、c++等傳統後端語言的地位的,因而個人認為在某些場景,比如重IO以及事件模型為主的業務中,node.js的Serverless化可能會成為後續發展勢頭,配合其他重計算場景的多語言後端服務形式或許才是未來的一種形態。(ps:這裡只是用到了faas這麼一個概念,真正的Serverless不應該僅僅是用到了這麼一個函式的業態,更重要的對於baas層的排程才是服務端更應該注重的,是不是Serverless無所謂,我們主要關注的應該是服務而不是資源)