Express.js-GraphQL API热重载不起作用



我试图用热重载来编写一个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,并调用任何yarnnpm命令,加载此模块(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();

最新更新