我试图用热重载来编写一个mockAPI。这意味着我希望在编辑架构或返回的数据后重新启动/更新服务器数据。一切如预期。我唯一缺少的是重新加载。实际上,服务器正在重新启动,但数据仍然相同(我用Postman进行了测试(。
我假设在编辑模式或返回的数据后必须重新启动服务器。我注意到graphqlHTTP已经存在一个回调函数来监听模式和rootValue,但这也不起作用。
注意:我不想杀死Node进程
index.js
/** require file system */
const fs = require('fs');
const md5 = require('md5');
const path = require('path');
const { readFileSync } = require('fs');
/** Start MockAPI */
const http = require('http');
const expressModule = require('express');
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
module.exports = class mockAPI {
constructor(options = { host: '127.0.0.1', port: 3000, namespace: '' }) {
/** start express */
this.modules();
this.directories = {
data: './data',
schema: './schema'
};
this.ignore = ['.DS_Store', 'Thumbs.db'];
this.host = options.host;
this.port = options.port;
this.namespace = options.namespace;
this.registerRoutes({
data: this.getRoutes(this.directories.data),
schema: this.getRoutes(this.directories.schema)
});
/** start watching (true) */
this.start(true);
}
modules() {
/** start express */
this.express = expressModule();
this.server = http.createServer(this.express);
}
start(watch) {
/** start server */
return new Promise((resolve, reject) => {
this.server
.listen(this.port, this.host, () => {
const restart = !watch ? 're' : '';
console.info(
'x1b[1mx1b[32m%sx1b[0mx1b[0m',
`n> MockAPI server ${restart}open on ... http://${this.server.address().address}:${
this.server.address().port
}n`
);
/** start watching API files */
if (watch) this.watch(this.directories);
})
.on('error', err => {
if (err) reject(err);
});
resolve();
});
}
stop(restart) {
/** stop server */
return new Promise((resolve, reject) => {
this.server.close(err => {
if (err) reject(err);
const color = restart ? '33' : '31';
const message = restart ? 'restart' : 'closed';
console.info(
`x1b[1mx1b[${color}m%sx1b[0mx1b[0m`,
`> MockAPI server ${message} on ... http://${this.host}:${this.port}`
);
resolve();
});
});
}
watch(directories) {
this.md5Previous = null;
this.fsWait = false;
Object.keys(directories).map(key => {
const dir = directories[key];
fs.watch(path.resolve(__dirname, dir), { recursive: true }, (event, filename) => {
if (event === 'change' && filename) {
/** if people exec multiple times save */
if (this.fsWait) return false;
const md5Current = md5(fs.readFileSync(path.resolve(__dirname, `${dir}/${filename}`)));
/** compare file hashes */
if (md5Current === this.md5Previous) return false;
/** restart server */
this.fsWait = true;
setTimeout(async () => {
this.fsWait = false;
this.md5Previous = md5Current;
console.info('x1b[33m%sx1b[0m', `- ${filename} changed`);
/**
* any solution here?
* in this scope
*/
await this.server.removeAllListeners('upgrade');
await this.server.removeAllListeners('request');
await this.stop(true);
await this.modules();
await this.registerRoutes({
data: this.getRoutes(this.directories.data),
schema: this.getRoutes(this.directories.schema)
});
this.start(false);
/**
* any solution here?
* in this scope
*/
}, 1000);
}
});
});
return true;
}
getRoutes(dir) {
const absPath = path.resolve(__dirname, dir);
let routes = [];
const readDir = path => {
let data = [];
fs.readdirSync(path, { withFileTypes: true }).forEach(file => {
if (this.ignore.includes(file.name)) return false;
if (file.isDirectory()) return readDir(`${path}/${file.name}`);
const route = path === absPath ? '' : path.replace(absPath, '');
const name = file.name.replace(/.js|.graphql/, '');
const endpoint = name.replace(/s+/g, '-');
data.push({
name: name,
dir: route,
file: file.name,
route: `${route}/${endpoint}`
});
});
routes = [...routes, ...data];
};
readDir(absPath);
/** sort array by route */
return routes.sort((a, b) => a.route.localeCompare(b.route));
}
registerRoutes(routes) {
if (routes.schema.length !== routes.data.length) {
console.error(
'x1b[31m%sx1b[0m',
`- schema.length(${routes.schema.length}) !== data.length(${routes.data.length}).n Each schema must match a data file in the same file structure with the same file name.`
);
process.exit;
}
routes.data.forEach(async (vdata, key) => {
if (vdata.route === routes.schema[key].route) {
const typeDefs = await readFileSync(
`./mock-api/schema${routes.schema[key].dir}/${routes.schema[key].file}`
).toString('utf-8');
let schema = await buildSchema(typeDefs);
let data = await require(`./data${vdata.dir}/${vdata.file}`);
this.express.use(
this.namespace + vdata.route,
graphqlHTTP({
schema: schema,
rootValue: data,
graphiql: true
})
);
}
});
}
};
如果有人能帮我就太好了。
如果你想重现这个过程,只需在与这个代码/代码段/文件相同的根目录中创建一个目录/data和/schema,并调用任何yarn或npm命令,加载此模块(index.js(。在/data目录中放置一个js文件来描述解析器,并在/schema文件夹中放置GraphQL文件来描述模式。每个文件必须具有相同的名称才能匹配路由,路由由文件结构注册。
因此,如果将/data/test/data.js放在/data中,则必须在/schema/test/data.graphql中编写模式,并且可以使用访问该模式http://127.0.0.1/test/data。
以下是可以快速复制的示例。
data.js
/**
* Returns GraphQL data
* @returns {object} data
*/
module.exports = {
hello: () => {
return 'Hello world!';
}
};
data.graphql
type Query {
hello: String
}
为了澄清我想要的是,我需要刷新、清除、删除或任何其他方法来删除存储的数据。
解决方案是创建一个带有spawn的子进程。
index.js
/** file system */
const { spawn } = require('child_process');
/** require environment variables */
let env = require('../config/env');
/**
* Returns Mock API server as child process
* @param {boolean} initial process
* @returns {object} process data
* --------------------------------
*/
const server = initial => {
/** merge environment variables */
env = Object.assign(process.env, env, { INITIAL: initial });
/** start child process for the Mock API */
let child = spawn('node', ['mock-api/MockAPI.js'], {
env: env
});
/** child info/logs */
child.stdout.on('data', data => {
try {
/** check if data is object */
const obj = JSON.parse(data.toString());
/** handle object data */
if ({}.hasOwnProperty.call(obj, 'restart')) {
child.kill();
}
} catch (error) {
console.log(data.toString());
}
});
/** child errors */
child.stderr.on('data', data => {
console.error(
'x1b[1mx1b[31m%sx1b[0mx1b[0m',
`> Mock API server crashed on ... http://${env.MOCK_API_HOST}:${env.MOCK_API_PORT}n`,
data.toString()
);
});
child.on('close', () => {
/** Wait for process to exit, then run again */
setTimeout(() => server(false), 500);
});
/** kill child process if parent process gets killed */
process.on('exit', () => child.kill('SIGINT'));
return child;
};
module.exports = (() => server(true))();
MockAPI.js
/** file system */
const fs = require('fs');
const md5 = require('md5');
const path = require('path');
const { readFileSync } = require('fs');
/** server data */
const http = require('http');
const expressModule = require('express');
const express = expressModule();
const server = http.createServer(express);
/** graphql */
const { graphqlHTTP } = require('express-graphql');
const { buildSchema } = require('graphql');
/** define globals */
const data = './data';
const ignore = ['.DS_Store', 'Thumbs.db'];
const env = process.env;
const host = env.MOCK_API_HOST || '127.0.0.1';
const port = env.MOCK_API_PORT || 3000;
const namespace = env.MOCK_API_NAMESPACE || '';
/**
* get routes data from ./data directory
*
* NOTE:
* each graphql schema must
* match a JS data file in
* the same directory
*
* @param {string} directory
* @returns {Array} routes
*/
const getRoutes = dir => {
const root = path.resolve(__dirname, dir);
const routes = [];
const data = path =>
fs.readdirSync(path, { withFileTypes: true }).forEach(file => {
if (ignore.includes(file.name) || file.name.endsWith('.graphql')) return false;
if (file.isDirectory()) return data(`${path}/${file.name}`);
const name = file.name.replace(/.js/, '');
const route = path === root ? '' : path.replace(root, '');
const endpoint = name.replace(/s+/g, '-');
routes.push({
name: name,
data: file.name,
directory: route,
graphql: `${name}.graphql`,
route: `${route}/${endpoint}`
});
});
data(root);
return routes;
};
/**
* register all routes
*
* NOTE:
* each graphql schema must
* match a JS data file in
* the same directory
*
* @param {Array} routes
*/
const registerRoutes = routes => {
routes.forEach(async route => {
const graphql = path.resolve(__dirname, `./data${route.directory}/${route.graphql}`);
await fs.stat(graphql, (error, stats) => {
error, stats;
});
const data = path.resolve(__dirname, `./data${route.directory}/${route.data}`);
await fs.stat(data, (error, stats) => {
error, stats;
});
const typeDefs = await buildSchema(readFileSync(graphql).toString('utf-8'));
const rootValue = await require(data);
express.use(
namespace + route.route,
graphqlHTTP({
schema: typeDefs,
rootValue: rootValue,
graphiql: true
})
);
});
};
/**
* start watching data files
*
* stop current process if files
* were edited and restart the server
*
* @param {String} directory
*/
const watch = directory => {
let md5Previous = null;
let fsWait = false;
fs.watch(path.resolve(__dirname, directory), { recursive: true }, (event, filename) => {
if ((event === 'change' || event === 'rename') && filename) {
/** if people exec multiple times save */
if (fsWait) return false;
const md5Current = md5(fs.readFileSync(path.resolve(__dirname, `${directory}/${filename}`)));
/** compare file hashes */
if (md5Current === md5Previous) return false;
/** restart server */
fsWait = true;
setTimeout(async () => {
fsWait = false;
md5Previous = md5Current;
console.info(
'x1b[33m%sx1b[0m',
`- ${filename} changedn> Mock API server restarts on ... http://${host}:${port}`
);
/** restart child process */
console.log(JSON.stringify({ restart: true }));
}, 2000);
}
});
};
/**
* initialize server
*/
const init = () => {
registerRoutes(getRoutes(data));
server
.listen(port, host, () => {
const open = env.INITIAL === 'true' ? 'open' : 'reopened';
console.info(
'x1b[1mx1b[32m%sx1b[0mx1b[0m',
`> Mock API server ${open} on ... http://${server.address().address}:${
server.address().port
}`
);
/** start watching data files */
watch(data);
})
.on('error', error => {
if (error) console.log('[ERROR] Mock API: ', error);
});
};
init();