写一个搭建本地mock服务器的命令行工具

之前一直使用mockjs模拟接口返回数据,由于其内部是改写XHR实现,因此存在一些局限性,比如无法拦截JSONP请求、无法直接在小程序中使用,最近恰好有点时间,因此决定写一个工具解决这些问题。

<!--more-->

整个包已发布在npm上,

其中koa-mock是整个项目的核心包,根据请求url和method,自动注入路由并返回对应的mock数据;而mock-server则是一个命令行工具,根据指定的mock模板文件,在本地快速启动一个服务器。

相关依赖

1. mockjs使用及实现

1.1. 统一管理mock模板

在开发过程中,一般后两种形式的调用比较多,且我会把所有的mock请求通过一个_mock.js文件进行管理,这样做有几个好处

可以很方便地切换mock环境

在webpack中,通过环境参数判断开发环境,并引入指定的mock文件即可

 entry: {
      index: isDev ? [
          getPath("./js/_mock.js"),
          getPath("./index.js")
      ] : getPath("./index.js")
  },

这样避免侵入逻辑代码,后续还要将对应的mock代码进行移除

let apiModel = {
    submit(){
        return Mock.mock({
            // mock数据
            code: 1,
            msg: '',
            data: {}
        })
    }
}

将模板文件当做接口文档

接口文档可能会更新不及时,而mock模板应用在最新的开发环境中。 相对于文档而言,mock的字段模板对开发人员更加友好,因此与其把mock代码散落在控制器和接口中,不如把mock模板整合在一起,作为接口文档进行管理

1.2. 原理实现

mockjs的Mock.mock接口,支持下面几种形式的调用

Mock.mock( template )
Mock.mock( rurl, template )
Mock.mock( rurl, rtype, template )

在浏览器版本中,如果指定了rurl,则会拦截对应的请求,并直接返回模板数据,文档上介绍了其实现原理是通过模拟XHR对象来实现的

从 1.0 开始,Mock.js 通过覆盖和模拟原生 XMLHttpRequest 的行为来拦截 Ajax 请求

这种实现有一些限制,比如JSONP形式请求、小程序内的网络请求等场景,则使用rurl形式进行拦截的操作不太靠谱。

koa-mock中间件就是为了解决这个问题,可以在koa中拦截对应的url并返回模板数据。甚至可以在前端和后台环境中共用一套mock模板。

2. 本地mock服务器

2.1. koa-mock中间件

与浏览器环境不同的是,在mock服务器中,需要根据rurl,劫持对应的路由,并直接返回数据。

因此可以在Mock.mock方法调用时,收集url对应的mock模板,然后在中间件中根据ctx.url,获取该url的模板。

因此现在问题就是如何收集url对应的mock配置了,我的处理方式是改写改方法,在内部通过一个对象保存依赖

Mock._urls = {}

Mock.mock = function () {
    let args = arguments
    // let config = getConfigFromArgs(args)
    // Mock._urls[url].push(config)

    return mock.apply(Mock, args)
}

这样在中间件中,只需要根据url和Mock._urls,获取其对应的配置即可

let middleware = function () {
    return async function (ctx, next) {
        let url = ctx.url,
            allConfig = util.getUrlAllConfig(Mock._urls, url),
            data

        // 如果对应的url有模板配置,则拦截请求并返回数据
        if (Array.isArray(allConfig)) {
            let template = util.getMockTemplate(allConfig, ctx.method)
            data = template ? Mock.mock(template) : null
        }

        if (data) {
            ctx.body = data
        } else {
            await next()
        }
    }
}

2.2. mock-server

上面存在的一个问题是,在_mock.js等模板中引入的Mock对象,和在中间件中引入的Mock对象,必须是同一个才行,否则无法完成依赖。

由于我们的预期是将_mock.js模板文件解耦,服务端和浏览器端都可以公用同一套模板,很明显浏览器版本中的Mock和中间件中的Mock对象不一定是同一个,除非采用同构渲染的项目。

我的解决办法是使用eval读取模板文件,然后将中间件中使用的Mock对象注入到模板文件上,编写模板文件时不用考虑其中的mock接口,到底是哪个对象提供的。

const mockMiddleware = require("@shymean/koa-mock")
let start = (file, port) => {
    fs.readFile(file, 'utf-8', function (err, tpl) {
        {
            // 注入相同的Mock对象
            let Mock = mockMiddleware.Mock
            let res = eval(tpl)
        }

        app.use(mockMiddleware());
        app.listen(port);

        console.log(`mock server listen at ${port}`);
    })
}

这样做的一个好处是,我们可以读取任意磁盘位置的模板文件,而不用担心mock-server莫生效。

同样也存在缺点,由于只有启动服务时会读取模板文件,对于模板文件的更新需要重启整个服务器,需要单独进行处理。

3. NodeJS开发命令行工具

上面理清了整个工具的核心实现,现在需要把他们封装成一个命令行工具,预期目标是通过指令,直接启动一个指定模板文件的mock服务器

mock -p 9999 -f ./_mock.js

从命令可以看出,我们需要处理两个地方,

  • 终端中关联mock命令到指定脚本,添加环境变量
  • 解析命令行参数

3.1. 不同系统的终端命令

windows的path环境变量

参考:

执行某个指令,实际上是运行某个应用程序(XXX.exe),在应用程序的安装目录下,可以通过应用程序名直接启动,但是在其他目录下,只能通过完整路径进行启动。难道每次运行命令都要这么麻烦?

通过设置path环境变量可以解决这个问题,path环境变量中存放的值,就是一连串的路径。

系统执行用户命令时,若用户未给出程序所在的完整路径,

  • 首先在当前目录下寻找相应的可执行文件、批处理文件(另外一种可以执行的文件)等。
  • 若找不到,再依次在PATH保存的这些路径中寻找相应的可执行的程序文件。系统就以第一次找到的为准;
  • 若搜寻完PATH保存的所有路径都未找到,则提示命令不存在

可以通过set path指令来设置path环境变量,这种方式只对当前命令窗口有效

set path=%path%;D:\Java\jdk1.6.0_24\bin

如果需要持久设置,可以在windows操作系统中可以通过我的电脑-〉系统属性-〉高级系统设置->环境变量,来查看和设置系统的path环境变量。

在windows上可以通过dokey设置别名,详情可以参考这里

linux 参考

shell环境依赖于多个文件的设置。当shell被调用时,它从两个初始文件读取命令。

  • /etc/profile包含了系统变量,它由系统管理员维护,由系统管理员设置本地系统变量和特殊命令。
  • 普通用户的启动信息文件($HOME/.bash_project)由各用户自己维护,该文件可以被修改以实现任何特定的系统初始化。

在linux下,$PATH环境变量决定了shell将到哪些目录中寻找命令或程序,PATH的值是一系列目录,当运行一个程序时,Linux在这些目录下进行搜寻编译链接。

可以通过export指令设置path变量,但是只在当前终端有效

echo $PATH
export PATH=/opt/STM/STLinux-2.3/devkit/sh4/bin:$PATH

如果需要持久设置环境变量,一般做法是修改上面提到的初始文件

vim /etc/profile
# 在文档最后添加新的path
export PATH="/opt/STM/STLinux-2.3/devkit/sh4/bin:$PATH"

# 保存退出,重新读取初始文件
source /etc/profile

可以通过alias属性为指令设置别名

vim /etc/profile
alias ls='ls --color=auto'

3.2. 关联终端命令

上面将shell脚本手动添加到环境变量中,然后使用mock指令的方式显得比较繁琐,npm提供了注册环境变量的快捷方法:向package.json中添加bin参数

"bin": {
  "mock": "./bin/index.js"
}

然后执行npm link,就可以快速使用mock指令了。 其中,指定以node运行对应的shell脚本,脚本头部需添加注解

#!/usr/bin/env node
require('../index.js')

3.3. 解析命令行参数

在nodejs中可以通过process.argv来获取命令行参数,该属性返回的是一个包含参数的数组,具体的含义需要我们手动去实现

通过上面的命令行参数格式不难理解,

  • -p表示参数名,后空格接的9999表示参数值
  • -f同理

这里使用的工具是yargs,该插件为我们封装了命令行参数

let yargs = require('yargs')

// 对单个参数进行配置
yargs.option('p', {
    alias : 'port',
    demand: false,
    default: 7654,
    describe: 'server port',
    type: 'number'
})

yargs.option('f', {
    alias : 'file',
    demand: true,
    default: './_mock.js',
    describe: 'mock template',
    type: 'number'
})

let argv = yargs.argv

let {file, port} = argv

这样就可以很方便地进行业务处理了。

4. npm本地包及发布

4.1. 安装本地包

由于整个项目拆分成了koa-mock中间件和mock-server两个工具,因此在开发时需要安装本地包,整个过程可分为下面几步

  • 首先打包对应的文件压缩包,在文件夹中使用npm pack将整个文件夹打包,会显示生成name-0.0.1.tgz,这个名称和版本号是在package.json中定义。
  • 然后切换到需要安装该包的项目目录下,使用npm install path/name-0.0.1.tgz安装对应的模块压缩包。
    • 需要注意不能在包文件夹中直接使用npm install name-0.0.1.tgz,会出现Refusing to install name as a dependency of itself的错误信息
    • 可以在整个项目文件夹的node_modules文件夹中发现我们的模块包了。
  • 最后在项目的文件中就可以直接使用使用该包导出的接口了

修改本地工具包后,需要重新执行上面的操作,更新本地依赖。

4.2. 发布包

发布npm包到npm仓库,需要注意下面几个问题

将镜像切换回npm,推荐nrm工具

因为本地的npm镜像一般会选择国内的淘宝镜像,需要将npm镜像切回到官网

npm set registry https://registry.npmjs.org/

手动设置比较麻烦,这里推荐工具nrm,可以方便地在不同的npm进行安装

npm i nrm -g
# 查看可使用的镜像
nrm ls
# 使用对应的镜像名
nrm use npm

登录npm账户

发包需要先登录账号,可以去~/.npmrc查看当前登录用户

npm adduser
npm login
# 切换到包根目录发布
npm publish

选择合适的包名

注意包名和版本号,是否已经存在了,目前npm的包名为了防止“误植”攻击,会自动检测相近的包名,参考这篇文章

如果发现返回403错误解决办法是为包名添加命名空间

"name": "@shymean/koa-mock",

然后修改发布权限

npm publish --access=public

正式发布 如果上面设置都没问题了,就可以使用

npm publish

进行发布了,每次重新发布需要更新版本号,以v1.0.1形式保存在package.json中,需要注意版本号的设置,会影响npm install的更新

  • 第一种caret(箭头)表示: ^2.0.2能帮你下载最新的2.x.x的包,不能下载1.x.x的包。比如最新的是2.1.0, 就是直接下载2.1.0。
  • 二种tilde(波浪线)表示: ~2.0.2能帮你下载2.0.x的最新包,不能下载2.1.x的包,比 ^ 要更加谨慎一些。比如最新的包如果是2.0.3, 就会下载,而如果是2.1.3就不会下载。
  • 第三种没有任何符号就表示严格匹配。

5. 小结

这篇文章内容有点杂

  • 前面主要整理在通过mock模板管理mock数据的好处,以及在本地实现mock-server的思路
  • 后面主要整理了如何开发命令行工具,以及如何进行npm本地包的安装和发布

其中一些知识点,是很早之前整理在有道云笔记上的,有时候经常要去查阅,比较麻烦,因此一并整理在这里。