一、简单初始化
1.首先,创建一个新的目录并初始化一个 Node.js 项目:
mkdir faas-service
cd faas-service
npm init -y
npm install express body-parser vm2 multer
2.创建server.js
const express = require('express');
const bodyParser = require('body-parser');
const multer = require('multer');
const fs = require('fs');
const path = require('path');
const { NodeVM } = require('vm2');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.use(bodyParser.json());
// 路由:注册新函数
app.post('/functions', upload.single('function'), (req, res) => {
const { functionName } = req.body;
const file = req.file;
if (!functionName || !file) {
return res.status(400).json({ error: '函数名称和代码文件是必需的。' });
}
const destPath = path.join(__dirname, 'functions', `${functionName}.js`);
fs.rename(file.path, destPath, (err) => {
if (err) {
return res.status(500).json({ error: '无法保存函数文件。' });
}
res.status(201).json({ message: `函数 ${functionName} 已注册。` });
});
});
// 路由:调用函数
app.post('/invoke/:functionName', async (req, res) => {
const { functionName } = req.params;
const funcPath = path.join(__dirname, 'functions', `${functionName}.js`);
if (!fs.existsSync(funcPath)) {
return res.status(404).json({ error: '函数未找到。' });
}
const code = fs.readFileSync(funcPath, 'utf-8');
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
timeout: 1000,
require: {
external: true,
builtin: ['*'],
},
});
try {
const func = vm.run(code);
const result = await func(req.body);
res.json({ result });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`FaaS 服务已启动,监听端口 ${PORT}`);
});
3.调整package.json
{
"name": "faas-service",
"version": "1.0.0",
"description": "简单 FaaS 服务",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"keywords": [],
"author": "wei",
"license": "MIT",
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.21.0",
"multer": "^1.4.5-lts.1",
"vm2": "^3.9.19"
}
}
module.exports = async (event) => {
const name = event.name || 'World';
return { message: `Hello, ${name}!` };
};
二、调用示例函数
使用 Postman 或其他工具发送 POST 请求到 /invoke/hello:
- URL: http://localhost:3000/invoke/hello
- 方法: POST
- Body: JSON 格式
{
"name": "FaaS"
}
响应:
{
"result": {
"message": "Hello, FaaS!"
}
}
三、注册新函数
同样使用 Postman 上传新的函数:
- URL: http://localhost:3000/functions
- 方法: POST
- Body: form-data
- functionName: add
- function: 上传 add.js 文件
module.exports = async (event) => {
const { a, b } = event;
return { result: a + b };
};
响应:
{
"message": "函数 add 已注册。"
}
然后调用新注册的函数:
- URL: http://localhost:3000/invoke/add
- 方法: POST
- Body: JSON 格式
{
"a": 5,
"b": 3
}
{
"result": {
"result": 8
}
}
增加get请求支持
// 路由:调用函数(支持 GET 和 POST)
app.all('/invoke/:functionName', async (req, res) => {
const { functionName } = req.params;
const funcPath = path.join(__dirname, 'functions', `${functionName}.js`);
if (!fs.existsSync(funcPath)) {
return res.status(404).json({ error: '函数未找到。' });
}
const code = fs.readFileSync(funcPath, 'utf-8');
// 根据请求方法获取输入数据
let input;
if (req.method === 'GET') {
input = req.query;
} else if (req.method === 'POST') {
input = req.body;
} else {
return res.status(405).json({ error: '方法不允许。' });
}
try {
const script = new VMScript(code, funcPath);
const func = vm.run(script, funcPath);
const result = await func(input);
res.json({ result });
} catch (error) {
res.status(500).json({ error: error.message, stack: error.stack });
}
});
四、增加日志模块
const rfs = require('rotating-file-stream'); // 引入 rotating-file-stream 库
const morgan = require('morgan'); // 引入 morgan 库
// 创建 Rotating File Stream 用于 HTTP 访问日志
const accessLogStream = rfs.createStream('access.log', {
interval: '1d', // 每天轮换一次
size: '10M', // 当日志文件达到10MB时进行轮换
path: logDirectory,
compress: 'gzip', // 压缩旧的日志文件
maxFiles: 10 // 保留最近10个轮换的日志文件
});
// 创建 Rotating File Stream 用于模块加载日志
const moduleLoadLogStream = rfs.createStream('module-load.log', {
interval: '1d', // 每天轮换一次
size: '10M', // 当日志文件达到10MB时进行轮换
path: logDirectory,
compress: 'gzip', // 压缩旧的日志文件
maxFiles: 10 // 保留最近10个轮换的日志文件
});
// 错误监听
accessLogStream.on('error', (err) => {
console.error('Rotating File Stream (accessLogStream) 错误:', err);
});
moduleLoadLogStream.on('error', (err) => {
console.error('Rotating File Stream (moduleLoadLogStream) 错误:', err);
});
// 使用 Morgan 进行 HTTP 请求日志记录
app.use(morgan('combined', { stream: accessLogStream }));
// 日志记录函数
function logModuleLoad(moduleName, status, type) {
const message = `模块加载尝试: [${type}] "${moduleName}" - ${status}`;
const timestamp = new Date().toISOString();
moduleLoadLogStream.write(`${timestamp} ${message}\n`);
}
五、添加配置文件禁止模块的加载和模块白名单信息
# 配置文件
# 全局配置
allowedExternalModules:
- lodash
- axios
# 模块配置
moduleWhitelist:
- lodash
- axios
- moment
- path
- buffer
# 内置模块配置
builtinWhitelist:
- path
- buffer
引入js-yaml解析配置文件
六、调整目录结构,提取方法
const fs = require("fs");
const path = require("path");
const logger = require("../util/logger"); // 引入 logger.js
const logModuleLoad = logger.logModuleLoad;
const { VMScript } = require("vm2"); // 假设你使用 vm2 来运行脚本
const { createVM, watchConfigFile } = require("../util/vmUtil");
// 初始化 NodeVM
let vm = createVM();
// 动态更新配置并重新初始化 Mock
watchConfigFile(vm);
// 路由:注册新函数
async function addFunction(req, res) {
const { functionName, functionCode } = req.body;
if (!functionName || !functionCode) {
return res.status(400).json({ error: "函数名称和代码是必需的。" });
}
const sanitizedFunctionName = path
.basename(functionName)
.replace(/[^a-zA-Z0-9_]/g, "");
const destPath = path.join(
__dirname,
"../functions",
`${sanitizedFunctionName}.js`
);
const destDir = path.dirname(destPath);
// 检查目标目录是否存在
if (!fs.existsSync(destDir)) {
try {
fs.mkdirSync(destDir, { recursive: true });
logModuleLoad("目录创建", `目录 "${destDir}" 已创建。`, "函数");
} catch (mkdirErr) {
logModuleLoad(
"目录创建错误",
`创建目录 "${destDir}" 时出错: ${mkdirErr.message}`,
"函数"
);
return res.status(500).json({ error: "无法创建目标目录。" });
}
}
// 检查目标文件是否已存在
if (fs.existsSync(destPath)) {
// 删除临时文件
return res.status(409).json({ error: "函数已存在。" });
}
// 将字符串内容写入文件
fs.writeFile(destPath, functionCode, "utf-8", (err) => {
if (err) {
logModuleLoad(
"文件写入错误",
`写入文件 "${destPath}" 时出错: ${err.message}`,
"函数"
);
return res.status(500).json({ error: "无法保存函数文件。" });
}
logModuleLoad("函数注册", `函数 "${functionName}" 注册成功。`, "函数");
res.status(201).json({ message: `函数 ${functionName} 注册成功。` });
});
}
// 路由:注册新函数,上传文件
async function addFunctionFile(req, res) {
const { functionName } = req.body;
const file = req.file;
if (!functionName || !file) {
return res.status(400).json({ error: "函数名称和代码文件是必需的。" });
}
const sanitizedFunctionName = path
.basename(functionName)
.replace(/[^a-zA-Z0-9_]/g, "");
const destPath = path.join(
__dirname,
"../functions",
`${sanitizedFunctionName}.js`
);
const destDir = path.dirname(destPath);
// 检查目标目录是否存在
if (!fs.existsSync(destDir)) {
try {
fs.mkdirSync(destDir, { recursive: true });
logModuleLoad("目录创建", `目录 "${destDir}" 已创建。`, "函数");
} catch (mkdirErr) {
logModuleLoad(
"目录创建错误",
`创建目录 "${destDir}" 时出错: ${mkdirErr.message}`,
"函数"
);
return res.status(500).json({ error: "无法创建目标目录。" });
}
}
// 检查目标文件是否已存在
if (fs.existsSync(destPath)) {
// 删除临时文件
deleteFile(file);
return res.status(409).json({ error: "函数已存在。" });
}
// 尝试重命名文件
fs.rename(file.path, destPath, (err) => {
if (err) {
logModuleLoad(
"文件重命名错误",
`重命名文件 "${file.path}" 到 "${destPath}" 时出错: ${err.message}`,
"函数"
);
// 删除临时文件
deleteFile(file);
return res.status(500).json({ error: "无法保存函数文件。" });
}
logModuleLoad("函数注册", `函数 "${functionName}" 注册成功。`, "函数");
res.status(201).json({ message: `函数 ${functionName} 注册成功。` });
// 删除临时文件
deleteFile(file);
});
}
async function updateFunction(req, res) {
const { functionName } = req.params;
const file = req.file;
if (!functionName || !file) {
return res.status(400).json({ error: "函数名称和代码文件是必需的。" });
}
const destPath = path.join(__dirname, "../functions", `${functionName}.js`);
// 检查目标文件是否存在
if (fs.existsSync(destPath)) {
logModuleLoad("函数不存在", `函数 "${functionName}" 不存在。`, "函数");
return res.status(409).json({ error: "函数不存在。" });
}
// 尝试重命名文件
fs.rename(file.path, destPath, (err) => {
if (err) {
logModuleLoad(
"文件重命名错误",
`重命名文件 "${file.path}" 到 "${destPath}" 时出错: ${err.message}`,
"函数"
);
// 删除临时文件
deleteFile(file);
return res.status(500).json({ error: "无法保存函数文件。" });
}
logModuleLoad("函数更新", `函数 "${functionName}" 更新成功。`, "函数");
res.status(200).json({ message: `函数 ${functionName} 更新成功。` });
// 删除临时文件
deleteFile(file);
});
}
async function deleteFunction(req, res) {
const { functionName } = req.params;
const funcPath = path.join(
__dirname,
"..",
"functions",
`${functionName}.js`
);
if (!fs.existsSync(funcPath)) {
logModuleLoad("函数未找到", `函数 "${functionName}" 未找到。`, "函数");
return res.status(404).json({ error: "函数未找到。" });
}
fs.unlink(funcPath, (err) => {
if (err) {
logModuleLoad(
"函数删除错误",
`删除函数 "${functionName}" 时出错: ${err.message}`,
"函数"
);
return res.status(500).json({ error: "无法删除函数。" });
}
logModuleLoad("函数删除", `函数 "${functionName}" 删除成功。`, "函数");
res.status(200).json({ message: `函数 ${functionName} 删除成功。` });
});
}
// 查询所有
async function listFunctions(req, res) {
const functionsDir = path.join(__dirname, "../functions");
const functions = [];
fs.readdirSync(functionsDir).forEach((file) => {
if (path.extname(file) === ".js") {
functions.push(path.basename(file, ".js"));
}
});
res.json({ functions });
}
function deleteFile(file) {
// 删除临时文件
fs.unlink(file.path, (unlinkErr) => {
if (unlinkErr) {
logModuleLoad(
"临时文件删除错误",
`删除临时文件 "${file.path}" 时出错: ${unlinkErr.message}`,
"函数"
);
}
});
}
// 路由:调用函数
async function invoke(req, res) {
const { functionName } = req.params;
const funcPath = path.join(
__dirname,
"..",
"functions",
`${functionName}.js`
);
if (!fs.existsSync(funcPath)) {
logModuleLoad("函数未找到", `函数 "${functionName}" 未找到。`, "函数");
return res.status(404).json({ error: "函数未找到。" });
}
const code = fs.readFileSync(funcPath, "utf-8");
// 根据请求方法获取输入数据
let input;
if (req.method === "GET") {
input = req.query;
} else if (req.method === "POST") {
input = req.body;
} else {
logModuleLoad(
"方法不允许",
`函数 "${functionName}" 的请求方法 "${req.method}" 不允许。`,
"函数"
);
return res.status(405).json({ error: "方法不允许。" });
}
try {
logModuleLoad("执行函数", `执行函数 "${functionName}"`, "函数");
const script = new VMScript(code, funcPath);
const func = vm.run(script, funcPath);
const result = await func(input);
res.json({ result });
} catch (error) {
logModuleLoad(
"执行错误",
`执行函数 "${functionName}" 时出错: ${error.message}\n${error.stack}`,
"函数"
);
res.status(500).json({ error: error.message, stack: error.stack });
}
}
module.exports = {
addFunction,
addFunctionFile,
deleteFunction,
updateFunction,
listFunctions,
invoke,
};
const express = require("express");
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const {
addFunction,
addFunctionFile,
deleteFunction,
updateFunction,
listFunctions,
invoke,
} = require("../controller/functions.js");
const router = express.Router();
// 创建上传目录(如果不存在)
const dir = path.resolve(__dirname, "..", "uploads");
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/"); // 文件保存路径
},
filename: function (req, file, cb) {
cb(null, Date.now() + path.extname(file.originalname)); // 重命名文件以避免冲突
},
});
const maxSizeInBytes = 3 * 1024 * 1024; // 3MB
const uploadMiddleware = multer({
storage: storage,
limits: { fileSize: maxSizeInBytes },
});
router.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === "LIMIT_FILE_SIZE") {
return res.status(400).send("File size exceeds the limit of 3MB.");
}
}
next(err);
});
router.get("/", (req, res) => {
res.send("hello world");
});
// 路由:添加函数(支持 GET 和 POST)
router.post("/addFunction", uploadMiddleware.single("function"), addFunction);
router.post(
"/addFunctionFlie",
uploadMiddleware.single("function"),
addFunctionFile
);
// 路由:删除函数
router.post("/deleteFunction", deleteFunction);
// 路由:更新函数
router.post(
"/updateFunction",
uploadMiddleware.single("function"),
updateFunction
);
// 路由:查询所有函数
router.post("/listFunctions", listFunctions);
// 路由:调用函数
router.all("/api/:functionName", invoke);
module.exports = router;
const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream');
const moment = require('moment');
const logDirectory = path.join(__dirname,"..",'logs'); // 日志目录
// 确保日志目录存在
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
}
const pad = num => (num > 9 ? "" : "0") + num;
const generator = (time, index, baseName = "file") => {
time = time || new Date();
// if (!time) return `${baseName}.log`;
var month = time.getFullYear() + "-" + pad(time.getMonth() + 1);
var day = pad(time.getDate());
return `${month}/${month}-${day}-${baseName}.log`;
};
// 创建 Rotating File Stream 用于 HTTP 访问日志
const accessLogStream = rfs.createStream((time, index) => generator(time, index, "http-access"), {
interval: '1d', // 每天轮换一次
size: '10M', // 当日志文件达到10MB时进行轮换
path: logDirectory,
compress: 'gzip', // 压缩旧的日志文件
maxFiles: 10 // 保留最近10个轮换的日志文件
});
// 创建 Rotating File Stream 用于模块加载日志
const moduleLoadLogStream = rfs.createStream((time, index) => generator(time, index, "module-load"), {
interval: '1d', // 每天轮换一次
size: '10M', // 当日志文件达到10MB时进行轮换
path: logDirectory,
compress: 'gzip', // 压缩旧的日志文件
maxFiles: 10 // 保留最近10个轮换的日志文件
});
// 错误监听
accessLogStream.on('error', (err) => {
console.error('Rotating File Stream (accessLogStream) 错误:', err);
});
moduleLoadLogStream.on('error', (err) => {
console.error('Rotating File Stream (moduleLoadLogStream) 错误:', err);
});
// 自定义日志格式化函数
const customFormat = (tokens, req, res) => {
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
return [
tokens['remote-addr'](req, res), // 替换 tokens.ip 为 tokens['remote-addr']
'-',
tokens['remote-user'](req, res),
'[' + timestamp + ']',
'"' + tokens.method(req, res) + ' ' + tokens.url(req, res) + ' ' + tokens['http-version'](req, res) + '"',
tokens.status(req, res),
tokens.res(req, res, 'content-length'),
'-',
tokens['referrer'](req, res),
tokens['user-agent'](req, res)
].join(' ');
};
// 日志记录函数
function logModuleLoad(moduleName, status, type) {
const message = `模块加载尝试: [${type}] "${moduleName}" - ${status}`;
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
moduleLoadLogStream.write(`${timestamp} ${message}\n`);
}
module.exports = {
accessLogStream,
moduleLoadLogStream,
customFormat,
logModuleLoad
};
const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream');
const moment = require('moment');
const logDirectory = path.join(__dirname,"..",'logs'); // 日志目录
// 确保日志目录存在
if (!fs.existsSync(logDirectory)) {
fs.mkdirSync(logDirectory);
}
const pad = num => (num > 9 ? "" : "0") + num;
const generator = (time, index, baseName = "file") => {
time = time || new Date();
// if (!time) return `${baseName}.log`;
var month = time.getFullYear() + "-" + pad(time.getMonth() + 1);
var day = pad(time.getDate());
return `${month}/${month}-${day}-${baseName}.log`;
};
// 创建 Rotating File Stream 用于 HTTP 访问日志
const accessLogStream = rfs.createStream((time, index) => generator(time, index, "http-access"), {
interval: '1d', // 每天轮换一次
size: '10M', // 当日志文件达到10MB时进行轮换
path: logDirectory,
compress: 'gzip', // 压缩旧的日志文件
maxFiles: 10 // 保留最近10个轮换的日志文件
});
// 创建 Rotating File Stream 用于模块加载日志
const moduleLoadLogStream = rfs.createStream((time, index) => generator(time, index, "module-load"), {
interval: '1d', // 每天轮换一次
size: '10M', // 当日志文件达到10MB时进行轮换
path: logDirectory,
compress: 'gzip', // 压缩旧的日志文件
maxFiles: 10 // 保留最近10个轮换的日志文件
});
// 错误监听
accessLogStream.on('error', (err) => {
console.error('Rotating File Stream (accessLogStream) 错误:', err);
});
moduleLoadLogStream.on('error', (err) => {
console.error('Rotating File Stream (moduleLoadLogStream) 错误:', err);
});
// 自定义日志格式化函数
const customFormat = (tokens, req, res) => {
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
return [
tokens['remote-addr'](req, res), // 替换 tokens.ip 为 tokens['remote-addr']
'-',
tokens['remote-user'](req, res),
'[' + timestamp + ']',
'"' + tokens.method(req, res) + ' ' + tokens.url(req, res) + ' ' + tokens['http-version'](req, res) + '"',
tokens.status(req, res),
tokens.res(req, res, 'content-length'),
'-',
tokens['referrer'](req, res),
tokens['user-agent'](req, res)
].join(' ');
};
// 日志记录函数
function logModuleLoad(moduleName, status, type) {
const message = `模块加载尝试: [${type}] "${moduleName}" - ${status}`;
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
moduleLoadLogStream.write(`${timestamp} ${message}\n`);
}
module.exports = {
accessLogStream,
moduleLoadLogStream,
customFormat,
logModuleLoad
};
const express = require('express');
const morgan = require('morgan'); // 引入 morgan 库
const logger = require('./util/logger'); // 引入 logger.js
const router = require('./router/index'); // 引入路由模块
const app = express();
// 使用内置的 JSON 解析中间件
app.use(express.json());
// 使用 Morgan 进行 HTTP 请求日志记录
// app.use(morgan('combined', { stream: accessLogStream }));
// 使用自定义格式的日志记录中间件
app.use(morgan(logger.customFormat, { stream: logger.accessLogStream }));
app.use(router);
// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务已启动,端口 ${PORT}`);
});
七、使用postman测试
第一次调用
module.exports = async (event) => {
const name = event.name || 'World';
return { message: `Hello, ${name}!` };
};