Browse Source

chore: 搭建了一个简单的 koa mock server

WuXinyu 6 năm trước cách đây
mục cha
commit
c5eea05faa
5 tập tin đã thay đổi với 557 bổ sung0 xóa
  1. 144 0
      mock/api/question/get.json
  2. 72 0
      mock/index.js
  3. 217 0
      mock/serializers.js
  4. 5 0
      mock/utils.js
  5. 119 0
      script/build-icon.js

+ 144 - 0
mock/api/question/get.json

@@ -0,0 +1,144 @@
+{
+  "questions": [
+    {
+      "kind": "CHOICE",
+      "stem": "计算机中常采用原码、反码、补码和移码表奈数据,其中;±0 编码相同的是( )。",
+      "options": {
+        "A": "原码和补码",
+        "B": "反码和补码\n\n>",
+        "C": "补码和移码",
+        "D": "原码和移码"
+      },
+      "answer": "【答案】C",
+      "key_points": "【考点】空",
+      "analysis": "【解析】本题考查计算机系统数据编码基础知识。设机器字长为n(即采用n个二进制位表示数据),最高位是符号位,0表示正号,1表示负号。原码表示方式下,除符号位外,n-1位表示数值的绝对值。因此,n为8时,[+0]原=0000000,[-0]原=1 0000000正数的反码与原码相同,负数的反码则是其绝对值按位求反。n为8时,数值0的反码表示有两种形式:[+0]反=00000000,[-0]反=111111111。正数的补码与其原码和反码相同,负数的补码则等于其反码的末尾加1。在补码表示中,0有唯一的编码:[+0]原=0000000,[-0]原=0000000。移码表示法是在数×上增加一个偏移量来定义的,常用于表示浮点数中的阶码。机器字长为n时,在偏移量为2的情况下,只要将补码的符号位取反便可获得相应的移码表示。",
+      "tags": null,
+      "knowledgeId": [
+        "CF.BCS"
+      ],
+      "lastModified": "2020-02-07 21:26:13",
+      "id": 1005,
+      "_links": {
+        "self": {
+          "href": "http://localhost:9090/api/tq/1005"
+        },
+        "CHOICEQuestion": {
+          "href": "http://localhost:9090/api/tq/1005"
+        }
+      }
+    },
+    {
+      "kind": "TRUE_FALSE",
+      "stem": "某指令流水线由 5 段组成,第 1、3、5 段所需时间为△t,第 2、4 段所需时间分别为 3△t、2△t,如下图所示,那么连续输入n条指令时的吞吐率(单位时内可执行的指令个 数)TP 为( )。\n\n![](https://seec.oss-cn-beijing.aliyuncs.com/%E9%A2%98%E5%BA%93/%E9%A2%98%E7%9B%AE%E5%9B%BE%E7%89%87%E6%95%B0%E6%8D%AE/11/media/image5.png)\n\n![](https://seec.oss-cn-beijing.aliyuncs.com/%E9%A2%98%E5%BA%93/%E9%A2%98%E7%9B%AE%E5%9B%BE%E7%89%87%E6%95%B0%E6%8D%AE/11/media/image6.png)",
+      "options": null,
+      "answer": "【答案】B",
+      "key_points": "【考点】空",
+      "analysis": "【解析】本题考查计算机系统流水线方面的基础知识。吞吐率和建立时间是使用流水线技术的两个重要指标。吞吐率是指单位时间里流水线处理机流出的结果数。对指令而言,就是单位时间里执行的指令数。流水线开始工作,须经过一定时间才能达到最大吞吐率,这就是建立时间。若D2个子过程所用时间一样,均为△t0,则建立时间T0=m△t0。本题目中,连续输入n条指令时,第1条指令需要的时间为(1+3+1+2+1)△t,之后,每隔3△t便完成1条指令,即流水线一旦建立好,其吞吐率为最长子过程所需时间的倒数。综合n条指令的时间为(1+3+1+2+1)△t+(n-1)×3△t",
+      "tags": null,
+      "knowledgeId": [
+        "CF.CQ",
+        "CF.CB"
+      ],
+      "lastModified": "2020-02-07 21:26:22",
+      "id": 1006,
+      "_links": {
+        "self": {
+          "href": "http://localhost:9090/api/tq/1006"
+        },
+        "CHOICEQuestion": {
+          "href": "http://localhost:9090/api/tq/1006"
+        }
+      }
+    },
+    {
+      "kind": "CHOICE",
+      "stem": "下面关于漏洞扫描系统的叙述,错误的是( )。",
+      "options": {
+        "A": "漏洞扫描系统是一种自动检测目标主机安全弱点,的程序",
+        "B": "黑客利用漏洞扫描系统可以发现月标主机的安全漏洞",
+        "C": "漏洞扫描系统可以用于发现网络入侵者",
+        "D": "漏洞扫描系统的实现依赖于系统漏洞库的完善"
+      },
+      "answer": "【答案】C",
+      "key_points": "【考点】空",
+      "analysis": "【解析】漏洞扫描系统是一种自动检测远程或本地主机安全性弱点的程序。通过使用漏洞扫描系统,系统管理员能够发现所维护的Web服务器的各种TCP端口分配、提供的服务、Web服务软件版本和这些服务及软件呈现在因特网上的安全漏洞,及时发现并修补,以免网络攻击者利用系统漏洞进行攻击或窃取信息。可见,漏洞扫描并不能发现网络入侵者。",
+      "tags": null,
+      "knowledgeId": [
+        "SEE.RU",
+        "CF.SSDM"
+      ],
+      "lastModified": "2020-02-17 10:06:21",
+      "id": 1007,
+      "_links": {
+        "self": {
+          "href": "http://localhost:9090/api/tq/1007"
+        },
+        "CHOICEQuestion": {
+          "href": "http://localhost:9090/api/tq/1007"
+        }
+      }
+    },
+    {
+      "kind": "CHOICE",
+      "stem": "网络安全包含了网络信息的可用性、保密性、完整性和网络通信对象的真实性。其中,数字签名是对( )的保护;",
+      "options": {
+        "A": "可用性",
+        "B": "保密性",
+        "C": "连通性",
+        "D": "真实性"
+      },
+      "answer": "【答案】D",
+      "key_points": "【考点】空",
+      "analysis": "【解析】本题考查网络安全方面的基础知识。数字签名(Digital Signature)技术是不对称加密算法的典型应用。数字签名的应用过程是:数据源发送方使用自己的私钥对数据校验和或其他与数据内容有关的变量进行加密处理,完成对数据的合法“签名”;数据接收方则利用对方的公钥来解读收到的“数字签名”,并将解读结果用于对数据完整性的检验,以确认签名的合法性。数字签名技术是在网络系统虚拟环境中确认身份的重要技术,完全可以代替现实过程中的“亲笔签字”,在技术和法律上有保证,可见数字签名是对签名真实性的保护。",
+      "tags": null,
+      "knowledgeId": [
+        "CF.SSDM",
+        "CF.NCB"
+      ],
+      "lastModified": "2020-02-10 14:26:09",
+      "id": 1008,
+      "_links": {
+        "self": {
+          "href": "http://localhost:9090/api/tq/1008"
+        },
+        "CHOICEQuestion": {
+          "href": "http://localhost:9090/api/tq/1008"
+        }
+      }
+    },
+    {
+      "kind": "CHOICE",
+      "stem": "计算机感染特洛伊木马后的典型现象是( )。",
+      "options": {
+        "A": "程序异常退出",
+        "B": "有未知程序试图建立网络连接",
+        "C": "邮箱被垃圾邮件填满",
+        "D": "Windows 系统黑屏"
+      },
+      "answer": "【答案】B",
+      "key_points": "【考点】空",
+      "analysis": "【解析】计算机感染特洛伊木马后,会受到特洛伊木马程序的控制,有以下几种典型现象。●有未知程序试图建立网络连接。●系统中有可疑的进程在运行等现象。●系统运行速度越来越慢,且CPU资源占用率高。●任务表中有可疑的文件在运行。●系统长时间读/写硬盘或搜索软盘。●系统经常死机或重启等。",
+      "tags": null,
+      "knowledgeId": [
+        "CF.NCB",
+        "CF.SSDM"
+      ],
+      "lastModified": "2020-02-07 21:26:47",
+      "id": 1009,
+      "_links": {
+        "self": {
+          "href": "http://localhost:9090/api/tq/1009"
+        },
+        "CHOICEQuestion": {
+          "href": "http://localhost:9090/api/tq/1009"
+        }
+      }
+    }
+  ],
+  "page": {
+    "size": 5,
+    "totalElements": 1005,
+    "totalPages": 201,
+    "number": 2
+  }
+}

+ 72 - 0
mock/index.js

@@ -0,0 +1,72 @@
+const fs = require('fs').promises
+const path = require('path')
+const Koa = require('koa')
+const { pathToRegexp } = require('path-to-regexp')
+const cors = require('@koa/cors')
+const logger = require('koa-logger')
+const { Random, mock } = require('mockjs')
+const randexp = require('randexp').randexp
+const installSerializers = require('./serializers')
+
+const PORT = 4000
+const DATA_ROOT = 'api'
+
+const app = new Koa()
+
+Random.extend({
+  phone: () => randexp(/^1[345798][0-9]{9}$/)
+})
+
+installSerializers(Random, mock)
+
+app.use(cors())
+app.use(logger())
+app.use(async (ctx) => {
+  const request = ctx.request
+  const method = request.method.toLowerCase()
+  const pathname = request.path
+  const routes = await traverseDir(DATA_ROOT)
+
+  const matchedRoute = routes.find((route) => {
+    const regexp = pathToRegexp(`/${route}`.replace(/\/_/g, '/:'))
+    return regexp.test(pathname)
+  })
+
+  if (matchedRoute) {
+    const filePath = path.join(__dirname, matchedRoute, `${method}.json`)
+    const rawData = await fs.readFile(filePath)
+    // ctx.append('authorization', 'basic test-token')
+    ctx.body = mock(JSON.parse(rawData))
+  } else {
+    ctx.throw(404, `pathname or method not found: ${method} ${pathname}`)
+  }
+})
+
+app.listen(PORT)
+// eslint-disable-next-line no-console
+console.log(`mock start at http://localhost:${PORT} port`)
+
+/**
+ * list all dir path
+ * @param {string} dir root path
+ */
+async function traverseDir(dir) {
+  const basePath = path.join(__dirname, `${dir}`)
+  const fileList = await fs.readdir(basePath)
+  const allFiles = []
+  let hasCurrentDir = false
+
+  for (const filename of fileList) {
+    const filepath = path.join(basePath, filename)
+    const stat = await fs.lstat(filepath)
+    const unixFilePath = `${dir}/${filename}`
+    if (stat.isDirectory()) {
+      const subFiles = await traverseDir(unixFilePath)
+      allFiles.push(...subFiles)
+    } else if (!hasCurrentDir) {
+      hasCurrentDir = true
+      allFiles.push(dir)
+    }
+  }
+  return allFiles
+}

+ 217 - 0
mock/serializers.js

@@ -0,0 +1,217 @@
+const { randomArrayValue } = require('./utils')
+
+// basic types
+const basicExtendsTypes = () => ({
+  LOCAL_DATE_TIME: () => Math.floor(new Date().getTime() / 1000),
+  USER_ROLE: (role) => role || randomArrayValue(['STUDENT', 'TEACHER']),
+  BUILD_STATUS: (status) =>
+    status ||
+    randomArrayValue([
+      'SUCCESS',
+      'UNSTABLE',
+      'FAILURE',
+      'TIMEOUT',
+      'WAITING',
+      'RUNNING'
+    ]),
+  PROBLEM_TYPE: (problemType) =>
+    problemType ||
+    randomArrayValue([
+      'JAVA',
+      'JAVA_DATABASE',
+      'PYTHON',
+      'JAVA_COVERAGE',
+      'PYTHON_COVERAGE',
+      'JAVA_MUTATION'
+    ]),
+  EXAM_STATUS: (examStatus) =>
+    examStatus ||
+    randomArrayValue([
+      'PREPARING',
+      'UNAVAILABLE',
+      'READY',
+      'AVAILABLE',
+      'FINISHED',
+      'CLOSED'
+    ])
+})
+
+// serializer type
+const serializersPlugin = (mock) => ({
+  USER_SERIALIZER: (role, id) =>
+    mock({
+      [isNaN(id) ? 'id|1-2000' : 'id']: isNaN(id) ? 1 : id,
+      'score|90-100': 1,
+      username: '@NAME',
+      name: '@NAME',
+      email: '@EMAIL',
+      phone: '@PHONE',
+      role: `@USER_ROLE(${role})`,
+      createdAt: '@LOCAL_DATE_TIME'
+    }),
+  RESULT_SERIALIZER: (deleted) =>
+    mock({
+      'id|1-1000': 1,
+      'score|90-100': 1,
+      lastCommitAt: '@LOCAL_DATE_TIME',
+      deleted: typeof deleted === 'boolean' ? deleted : '@BOOLEAN',
+      exam: {
+        'id|1-1000': 1,
+        name: '@NAME',
+        course: '南京大学软件工程第一期'
+      },
+      owner: {
+        'id|1-1000': 1,
+        username: '@NAME'
+      },
+      'commits|10-20': [
+        {
+          'id|1-1000': 1,
+          finished: '@BOOLEAN',
+          'score|90-100': 1,
+          createdAt: '@LOCAL_DATE_TIME'
+        }
+      ]
+    }),
+  RECORD_SERIALIZER: (status) =>
+    mock({
+      'id|1-1000': 1,
+      'number|1-1000': 1,
+      status: `@BUILD_STATUS(${status})`,
+      consoleOutput: '@PARAGRAPH(1)',
+      'score|90-100': 1,
+      commit: {
+        'id|1-1000': 1,
+        finished: '@BOOLEAN',
+        'score|90-100': 1,
+        createdAt: '@LOCAL_DATE_TIME'
+      },
+      'failures|10-20': [
+        {
+          name: '@TITLE',
+          details: '@SENTENCE',
+          trace: '@PARAGRAPH(1)'
+        }
+      ]
+    }),
+  PROBLEM_SERIALIZER: (type) =>
+    mock({
+      'id|1-1000': 1,
+      name: '@NAME',
+      type: `@PROBLEM_TYPE(${type})`,
+      description: '@SENTENCE',
+      'publicFiles|0-4': ['@URL'],
+      'protectedFiles|0-4': ['@URL'],
+      'privateFiles|0-4': ['@URL'],
+      createdAt: '@LOCAL_DATE_TIME',
+      modifiedAt: '@LOCAL_DATE_TIME',
+      deprecated: '@BOOLEAN',
+      creator: {
+        'id|1-1000': 1,
+        username: '@NAME'
+      }
+    }),
+  PAGE_SERIALIZER: () => ({
+    page: 1,
+    limit: 20,
+    size: 20,
+    total: 100
+  }),
+  EXAM_SERIALIZER: (status) =>
+    mock({
+      'id|1-1000': 1,
+      name: '@NAME',
+      createdAt: '1578405025',
+      startAt: '1578405025',
+      endAt: '1588405025',
+      url: '@URL',
+      status: `@EXAM_STATUS(${status})`,
+      creator: {
+        'id|1-1000': 1,
+        username: '@NAME'
+      },
+      joined: '@BOOLEAN',
+      'courseId|1-1000': 1,
+      courseName: '南京大学软件工程第一期'
+    }),
+  COURSE_SERIALIZER: () =>
+    mock({
+      'id|1-1000': 1,
+      name: '南京大学软件工程第一期',
+      code: '@ID'
+    }),
+  CODE_SERIALIZER: () =>
+    mock({
+      'examId|1-1000': 1,
+      fetchAt: '@LOCAL_DATE_TIME',
+      startAt: '@LOCAL_DATE_TIME',
+      endAt: '@LOCAL_DATE_TIME',
+      'codeList|10-100': [
+        {
+          'questionId|1-1000': 1,
+          language: 'Java',
+          projectName: '软工一作业',
+          editable: {
+            '/a/b/c.java': btoa(`class a{
+              public void helloWorld1(){
+              }
+            }`),
+            '/a/b/a.java': btoa(`class a{
+              public void helloWorld2(){
+              }
+            }`),
+            '/a/c/a.java': btoa(`class a{
+              public void helloWorld3(){
+              }
+            }`),
+            '/a/b/d.java': btoa(`class a{
+              public void helloWorld4(){
+              }
+            }`)
+          },
+          uneditable: {
+            '/README.md': btoa('')
+          }
+        }
+      ]
+    }),
+  COMMIT_SERIALIZER: () =>
+    mock({
+      'id|1-1000': 1,
+      hash: '@STRING(50)',
+      finished: '@BOOLEAN',
+      'score|70-100': 1,
+      createdAt: '@LOCAL_DATE_TIME',
+      result: {
+        'id|1-1000': 1,
+        'score|70-100': 1,
+        exam: {
+          'id|1-1000': 1,
+          name: '@SENTENCE'
+        },
+        owner: {
+          'id|1-1000': 1,
+          username: '@NAME'
+        }
+      },
+      'records|10': [
+        {
+          'id|1-1000': 1,
+          status: '@BUILD_STATUS',
+          'score|70-100': 1,
+          problem: {
+            'id|1-1000': 1,
+            name: '@SENTENCE'
+          }
+        }
+      ]
+    })
+})
+function btoa(str) {
+  if (Buffer.byteLength(str) !== str.length) throw new Error('bad string!')
+  return Buffer.from(str, 'binary').toString('base64')
+}
+module.exports = function(random, mock) {
+  random.extend(basicExtendsTypes())
+  random.extend(serializersPlugin(mock))
+}

+ 5 - 0
mock/utils.js

@@ -0,0 +1,5 @@
+function randomArrayValue(arr = []) {
+  return Array.isArray(arr) ? arr[Math.floor(Math.random() * arr.length)] : ''
+}
+
+exports.randomArrayValue = randomArrayValue

+ 119 - 0
script/build-icon.js

@@ -0,0 +1,119 @@
+const fs = require('fs').promises
+const util = require('util')
+const rimraf = util.promisify(require('rimraf'))
+const path = require('path')
+const SVGO = require('svgo')
+const compiler = require('vue-template-compiler')
+const { camelCase, upperFirst } = require('lodash')
+
+const rawDir = '../src/assets/eva-icons'
+const outputDir = '../src/components/EvaIcon'
+const prefix = 'eva-icon'
+
+const fullRawDir = path.join(__dirname, rawDir)
+const fullOutputDir = path.join(__dirname, outputDir)
+
+buildAllSVG(prefix, fullRawDir, fullOutputDir)
+
+const svgoConfig = {
+  onlyMatchedOnce: false,
+  plugins: [
+    {
+      removeXMLNS: true
+    },
+    {
+      removeAttrs: { attrs: '(stroke|fill|class)' }
+    },
+    { removeStyleElement: true },
+    {
+      addAttributesToSVGElement: {
+        attribute: {
+          fill: 'currentColor',
+          width: '1em',
+          height: '1em'
+        }
+      }
+    }
+  ]
+}
+
+const svgo = new SVGO(svgoConfig)
+
+async function buildAllSVG(prefix, inputDir, outputDir) {
+  try {
+    await rimraf(outputDir)
+    console.log('clear output dir')
+    fs.mkdir(outputDir, { recursive: true })
+  } catch (e) {
+    console.log('no cache')
+    console.log(e)
+  }
+
+  const files = await collectAllSubFilePath(inputDir)
+  for (const filePath of files) {
+    const file = await fs.readFile(filePath)
+    const optimizedSVG = await (await svgo.optimize(file)).data
+    const svgRender = compiler
+      .compile(`<i aria-label="icon" class="${prefix}">${optimizedSVG}</i>`)
+      .render.replace(/_/g, '_vm._')
+
+    const fileBasename = path.basename(filePath, '.svg')
+    const filename = upperFirst(camelCase(`${prefix}-${fileBasename}`))
+
+    fs.writeFile(
+      path.join(outputDir, `${filename}.js`),
+      `export default {
+  functional: true,
+  render(_h,_vm){
+    ${removeWith(svgRender)}
+  }
+}
+`
+    )
+
+    fs.writeFile(
+      path.join(outputDir, `${filename}.d.ts`),
+      `declare const ${filename}: any;
+export default ${filename};
+`
+    )
+
+    fs.appendFile(
+      path.join(outputDir, 'index.js'),
+      `export { default as ${filename} } from './${filename}'
+`
+    )
+    fs.appendFile(
+      path.join(outputDir, 'index.d.ts'),
+      `export { default as ${filename} } from './${filename}'
+`
+    )
+  }
+}
+
+function removeWith(str) {
+  const regex = /^with\(this\)\{(.*)}$/
+  return regex.exec(str)[1]
+}
+
+/**
+ * get all sub file path
+ * @param {string} dirPath
+ * @return {Promise<string[]>} allFilePath
+ */
+async function collectAllSubFilePath(dirPath) {
+  const fileList = await fs.readdir(dirPath)
+  const allFiles = []
+
+  for (const filename of fileList) {
+    const filePath = path.join(dirPath, filename)
+    const fileStat = await fs.stat(filePath)
+    if (fileStat.isDirectory()) {
+      const subFileList = await collectAllSubFilePath(filePath)
+      allFiles.push(...subFileList)
+    } else {
+      allFiles.push(filePath)
+    }
+  }
+  return allFiles
+}