dkvirus / dva

介绍 dva 所涉及的技术,形成一个完整体系
52 stars 12 forks source link

Roadhog 底层原理 #10

Open dkvirus opened 5 years ago

dkvirus commented 5 years ago
dkvirus commented 5 years ago

mock 功能如何实现?

src/dev.js

$ roadhog dev 启动项目时,如果项目根目录下有 .roadhog.mock.js 文件,就会启动 Mock 功能。

入口文件 bin/roadhog.js => src/roadhog.js => src/scripts/dev.js,开启一个子进程 => src/scripts/realDev.js => src/dev.js 文件。运行 $ roadhog dev 核心处理代码就是这个文件。

src/dev.js

import dev from 'af-webpack/dev';
import { applyMock } from './utils/mock';

export default function runDev(opts = {}) {
  const { cwd = process.cwd(), entry } = opts;

  // .......

  dev({
    webpackConfig,
    proxy: config.proxy || {},
    beforeServer(devServer) {
      try {
        applyMock(devServer);
      } catch (e) {
        console.log(e);
      }
    },
    afterServer(devServer) {
      returnedWatchConfig(devServer);
    },
    openBrowser: true,
  });
}

对外暴露一个接口 runDev,接口内部处理一些参数,最后调用 dev 方法,dev 是 af-webpack 底层库对于 webpack 的封装,其中 af 指的是 ali-financial(阿里金融)。dev 的 beforeServer 参数字面意思是启动服务前的一个钩子函数,可以看到就是这里加载的 Mock 相关功能。

src/utils/mock.js

这个文件由7个方法组成,一个个看。

入口方法

applyMock() 入口方法。但不处理核心逻辑,核心逻辑交给 realApplyMock() 处理,这一点和 scripts 目录下的 dev.js 和 realDev.js 颇为相似,看来是大佬的个人编码习惯~~

export function applyMock(devServer) {
  try {
    realApplyMock(devServer);
    error = null;
  } catch (e) {
    error = e;
    outputError();

    // 加载 Mock 失败,可能原因配置文件或者 mock 目录写的有问题,监听这两个文件的变化,
    // 只要有修改就重启 Mock 功能。chokidar.watch 功能和 fs.watch 类似,监听文件变化,但更稳定
    // ....
    const watcher = chokidar.watch([configFile, mockDir], {});
    watcher.on('change', path => {
        applyMock(devServer);
    });
  }
}

几个工具类方法

在看 realApplyMock() 方法之前,先看几个工具类方法。

image

上图为 Roadhog-Mock 写法,其中键值除了对象自面量就是函数,压根没有字符串的示例。代理中只有键值为字符串,才会调用 createProxy 方法,对于这个方法不觉明历??而且 Mock 为什么会涉及到代理的东东??奇怪。

补充:在 Roadhog PR 里找到答案了,roadhog#795 提出加代理需求,个人感觉一团糟就是个挖坑的过程,没想到 PR 还被通过了。mock 功能很清晰,就是本地造假数据,脱离真实服务器也可以启动项目进行项目演示,代理完全可以在 .webpackrc.js 中添加,功能分离,更优雅。

配置:['/api/v1/(.*)']: 'localhost:8008',键值可以是字符串,Roadhog 官方文档未更新相关特性。

/**
 * 引入 .roadhog.mock.js,该文件导出对象
 * { 
 *   [`GET /users`] (req, res) { res.json({  }) },
 *   [`GET /roles`] (req, res) { res.json({  }) },
 * }
 */
function getConfig() {
    // require.cache 第一次看到,了解一波:https://cnodejs.org/topic/5993c0d54e3c4e5a7021b112
}

/**
 * 解析键值,返回请求方法和请求地址
 * For Example:[`GET /api/v1/users`] (req, res) { res.json({  }) }
 * key 就是键值,上述列子中:key = `GET /api/v1/users`
 * return { method: 'GET', path: '/api/v1/users' }
 */
function parseKey(key) {
}

/**
 * 启动 Mock 功能报错处理方法
 */
export function outputError() {
  console.log('xxxxx');
}

/**
 * 创建 Mock 处理函数
 * https://github.com/sorrycc/roadhog#mock => roadhog 文档有介绍可以返回一个方法,
 * 也可以直接返回一个对象字面量,createMockHandler() 实现这一功能。
 */
function createMockHandler(method, path, value) {
    if (typeof value === 'function') {
      value(...args);
    } else {
      res.json(value);
    }
}

/**
 * 创建代理
 */
function createProxy(method, pathPattern, target) {
  return proxy(filter, { target: realTarget, pathRewrite });
}

核心处理逻辑

realApplyMock() 处理核心逻辑。

function realApplyMock(devServer) {
  // Step1 
  const config = getConfig();       // 获取 mock 造的假数据对象
  const { app } = devServer;

  // Step2:键值是字符串的放到 proxyRules,其它类型的键值放到 mockRules 中
  const proxyRules = [], mockRules = [];

  Object.keys(config).forEach(key => {
    const keyParsed = parseKey(key);    // 解析获得请求方法和请求地址

    if (typeof config[key] === 'string') {
      proxyRules.push({ path, method: keyParsed.method, target: config[key] });
    } else {
      mockRules.push({ path: keyParsed.path, method: keyParsed.method, target: config[key] });
    }
  });

  // Step3:question?what is app apis.
  proxyRules.forEach(proxy => {
    app.use(proxy.path, createProxy(proxy.method, proxy.path, proxy.target));
  });

  /**
   * body-parser must be placed after http-proxy-middleware
   * https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/modify-post.md
   */
  devServer.use(bodyParser.json({ limit: '5mb', strict: false }));
  devServer.use(bodyParser.urlencoded({
      extended: true,
      limit: '5mb',
    }));

  mockRules.forEach(mock => {
    app[mock.method](
      mock.path,
      createMockHandler(mock.method, mock.path, mock.target),
    );
  });

  // Step4:调整 stack,把 historyApiFallback 放到最后,question??
  let lastIndex = null;
  app._router.stack.forEach((item, index) => {
    // .......
  });

  // Step5:监听 mock 目录和 .roadhog.mock.js 文件变化,有变化,自动重启
  const watcher = chokidar.watch([configFile, mockDir], {});
  watcher.on('change', path => {
    applyMock(devServer);
  });
}

Mock 核心代码读下来还算 ok,只是 realApplyMock 方法中 devServer 这东东是 af-webpack 里封装的对象,api 还不熟悉。Step3 中注册 mock 数据,涉及到 devServer 参数,这是 af-webpack 封装的。Step4 historyApiFallback 是 webpack 中的一个配置。