项目背景
前端开发过程中常常需要用到的图片等资源,除了使用常见的第三方图床外,我们也可以自己搭建一个私有图床,为团队提供前端基础服务。本文旨在回顾总结下自建图床的后端部分实现方案,希望能够给有类似需求的同学一些借鉴和方案。另外说一下,由于是前端基础建设,这里我们完全由前端同学所熟悉的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无所谓,我们主要关注的应该是服务而不是资源)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。