theydy / notebook

记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
0 stars 0 forks source link

抽取 vue-cli 中 devServer.proxy 支持配置代理服务器字符串的逻辑 #31

Open theydy opened 3 years ago

theydy commented 3 years ago

问题

在给一个 react 项目配置本地代理服务的时候,发现 webpack 本身的 devServer.proxy 不像 vue-cli 的 devServer.proxy 那样支持配置一个指向开发环境 API 服务器的字符串。当配置代理服务器字符串的时候,根据 vue-cli 文档的说法:这会告诉开发服务器将任何未知请求 (没有匹配到静态文件的请求) 代理到开发环境 API 服务器

不幸的是,我现在就是要用这个,所以看了下 vue-cli 的实现,把这段代码抄过来了。当然由于 webpack 代理服务器配置实际上用的是 http-proxy-middleware,而 vue-cli 只是在 webpack 上又包了一层,所以直接按照 http-proxy-middleware 文档肯定也能写,但是怎么想还是抄 vue-cli 的代码更靠谱。

最后实现


// prepareProxy.js
const fs = require('fs');
const url = require('url');
const path = require('path');
const address = require('address');

const defaultConfig = {
  logLevel: 'silent',
  secure: false,
  changeOrigin: true,
  ws: true,
  xfwd: true,
};

/**
 * @param proxy
 * @param appPublicFolder
 */
function prepareProxy(proxy, appPublicFolder) {
  if (!proxy) {
    return undefined;
  }

  if (typeof proxy !== 'string') {
    console.log('proxy must be a string');
    process.exit(1);
  }

  /**
   * @param pathname
   */
  function mayProxy(pathname) {
    const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1));
    const isPublicFileRequest = fs.existsSync(maybePublicPath) && fs.statSync(maybePublicPath).isFile();
    const isWdsEndpointRequest = pathname.startsWith('/sockjs-node'); // used by webpackHotDevClient
    return !(isPublicFileRequest || isWdsEndpointRequest);
  }

  /**
   * @param target
   * @param usersOnProxyReq
   * @param context
   */
  function createProxyEntry(target, usersOnProxyReq, context) {
    // #2478
    // There're a little-known use case that the `target` field is an object rather than a string
    // https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/https.md
    if (typeof target === 'string' && process.platform === 'win32') {
      target = resolveLoopback(target);
    }
    return {
      target,
      context(pathname, req) {
        // is a static asset
        if (!mayProxy(pathname)) {
          return false;
        }
        if (context) {
          // Explicit context, e.g. /api
          return pathname.match(context);
        } else {
          // not a static request
          if (req.method !== 'GET') {
              return true;
          }
          // Heuristics: if request `accept`s text/html, we pick /index.html.
          // Modern browsers include text/html into `accept` header when navigating.
          // However API calls like `fetch()` won’t generally accept text/html.
          // If this heuristic doesn’t work well for you, use a custom `proxy` object.
          return req.headers.accept && req.headers.accept.indexOf('text/html') === -1;
        }
      },
      onProxyReq(proxyReq, req, res) {
        if (usersOnProxyReq) {
          usersOnProxyReq(proxyReq, req, res);
        }
        // Browsers may send Origin headers even with same-origin
        // requests. To prevent CORS issues, we have to change
        // the Origin to match the target URL.
        if (!proxyReq.agent && proxyReq.getHeader('origin')) {
          proxyReq.setHeader('origin', target);
        }
      },
      onError: onProxyError(target),
    };
  }

  if (!/^http(s)?:\/\//.test(proxy)) {
    console.log('When "proxy" is specified in package.json it must start with either http:// or https://');
    process.exit(1);
  }

  return [{ ...defaultConfig, ...createProxyEntry(proxy) }];
}

/**
 * @param proxy
 */
function resolveLoopback(proxy) {
  const o = new url.URL(proxy);
  o.host = undefined;
  if (o.hostname !== 'localhost') {
    return proxy;
  }
  // Unfortunately, many languages (unlike node) do not yet support IPv6.
  // This means even though localhost resolves to ::1, the application
  // must fall back to IPv4 (on 127.0.0.1).
  // We can re-enable this in a few years.
  /* try {
    o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
  } catch (_ignored) {
    o.hostname = '127.0.0.1';
  } */

  try {
    // Check if we're on a network; if we are, chances are we can resolve
    // localhost. Otherwise, we can just be safe and assume localhost is
    // IPv4 for maximum compatibility.
    if (!address.ip()) {
      o.hostname = '127.0.0.1';
    }
  } catch (_ignored) {
    o.hostname = '127.0.0.1';
  }
  return url.format(o);
}

// We need to provide a custom onError function for httpProxyMiddleware.
// It allows us to log custom error messages on the console.
/**
 * @param proxy
 */
function onProxyError(proxy) {
  return (err, req, res) => {
    const host = req.headers && req.headers.host;
    console.log('Proxy error:' + ' Could not proxy request ' + req.url + ' from ' + host + ' to ' + proxy + '.');
    console.log(
      'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
        err.code +
        ').'
    );
    console.log();

    // And immediately send the proper error response to the client.
    // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
    if (res.writeHead && !res.headersSent) {
      res.writeHead(500);
    }
    res.end(
      'Proxy error: Could not proxy request ' +
        req.url +
        ' from ' +
        host +
        ' to ' +
        proxy +
        ' (' +
        err.code +
        ').'
    );
  };
}

module.exports.prepareProxy = prepareProxy;

// webpack.config.js

var { prepareProxy } = require('./prepareProxy');

var proxy = prepareProxy(
  'http://xxx.test.com',
  path.resolve(__dirname, 'public')
);

module.exports = {
  devServer: {
    proxy,
  }
}