diff --git a/packages/fes-preset-built-in/package.json b/packages/fes-preset-built-in/package.json index 4e9260c1..0cced841 100644 --- a/packages/fes-preset-built-in/package.json +++ b/packages/fes-preset-built-in/package.json @@ -41,6 +41,9 @@ "vue-loader": "^16.1.2", "webpack-bundle-analyzer": "4.3.0", "cli-highlight": "^2.1.4", - "webpack-chain": "6.5.1" + "webpack-chain": "6.5.1", + "body-parser": "^1.19.0", + "cookie-parser": "^1.4.5", + "mockjs": "^1.1.0" } } diff --git a/packages/fes-preset-built-in/src/index.js b/packages/fes-preset-built-in/src/index.js index e4d3599c..a9fd1dff 100644 --- a/packages/fes-preset-built-in/src/index.js +++ b/packages/fes-preset-built-in/src/index.js @@ -56,7 +56,10 @@ export default function () { require.resolve('./plugins/commands/dev'), require.resolve('./plugins/commands/help'), require.resolve('./plugins/commands/info'), - require.resolve('./plugins/commands/webpack') + require.resolve('./plugins/commands/webpack'), + + // mock + require.resolve('./plugins/commands/mock') ] }; } diff --git a/packages/fes-preset-built-in/src/plugins/commands/mock/index.js b/packages/fes-preset-built-in/src/plugins/commands/mock/index.js new file mode 100644 index 00000000..c76ab0af --- /dev/null +++ b/packages/fes-preset-built-in/src/plugins/commands/mock/index.js @@ -0,0 +1,174 @@ +import { existsSync, readFileSync } from 'fs'; +import { resolve } from 'path'; +import { chokidar, lodash } from '@umijs/utils'; +import bodyParser from 'body-parser'; +import cookieParser from 'cookie-parser'; +import Mock from 'mockjs'; + + +export default (api) => { + let mockFlag = false; // mock 开关flag + let mockPrefix = '/'; // mock 过滤前缀 + let mockFile = ''; // mock 文件 + let loadMock = ''; // mock 对象 + + api.describe({ + key: 'mock', + config: { + schema(joi) { + return joi.alternatives(joi.boolean(), joi.object()); + } + } + }); + + const createMock = () => { + // 判断是否为 Object,仅 {} + function isObject(value) { + return Object.prototype.toString.call(value) === '[object Object]'; + } + // 对 array、object 遍历处理 + function traversalHandler(val, callback) { + if (lodash.isArray(val)) { + val.forEach(callback); + } + if (isObject(val)) { + Object.keys(val).forEach((key) => { callback(val[key], key); }); + } + } + // 根据参数个数获取配置 + function getOption(arg) { + const len = arg.length; + // 默认配置 + const option = { + headers: { + 'Cache-Control': 'no-cache' + }, + statusCode: 200, + cookies: [], + timeout: 0 + }; + if (len === 0) return option; + if (len === 1) { + const newOption = arg[0]; + if (isObject(newOption)) { + traversalHandler(newOption, (value, key) => { + if (key === 'headers') { + traversalHandler(newOption.headers, (headervalue, headerkey) => { + option.headers[headerkey] = newOption.headers[headerkey]; + }); + } else { + option[key] = newOption[key]; + } + }); + } + } else { + option.url = arg[0]; + option.result = arg[1]; + } + return option; + } + // 把基于 cgiMockfile 的相对绝对转成绝对路径 + function parsePath(value) { + const PROJECT_DIR = process.env.PWD || process.cwd(); + return resolve(PROJECT_DIR, value); + } + + const requestList = []; + const cgiMock = (...arg) => { + const option = getOption(arg); + if (!option.url || !option.result) return; + requestList.push(option); + }; + cgiMock.file = function (file) { + return readFileSync(parsePath(file)); + }; + + // mock是否打开 + mockFlag = isObject(api.config.mock) ? true : api.config.mock; + if (!mockFlag) return; + // mock打开情况下,配置的过滤前缀 + mockPrefix = api.config.mock.prefix || mockPrefix; + // mock文件处理 + mockFile = parsePath('./mock.js'); + if (!existsSync(mockFile)) { + api.logger.info('mock.js File does not exist, please check'); return; + } + // 清除require的缓存,保证 mock 文件修改后拿到最新的 mock.js + if (require.cache[mockFile]) { + delete require.cache[mockFile]; + } + const projectMock = require(mockFile); + if (!lodash.isFunction(projectMock)) { + api.logger.info('mock.js should export Function'); return; + } + // mock对象与 mock.js 结合 + projectMock(cgiMock, Mock); + + return (req, res, next) => { + // 如果请求不是以 cgiMock.prefix 开头,直接 next `${mockPrefix}/` + if (!req.path.startsWith(`${mockPrefix}/`)) { + return next(); + } + + // 请求以 cgiMock.prefix 开头,匹配处理 + const matchRequet = requestList.filter(item => req.path.search(item.url) !== -1)[0]; + if (!matchRequet) { + return next(); + } + + // set header + res.set(matchRequet.headers); + // set Content-Type + matchRequet.type && res.type(matchRequet.type); + // set status code + res.status(matchRequet.statusCode); + // set cookie + traversalHandler(matchRequet.cookies, (item) => { + const name = item.name; + const value = item.value; + delete item.name; + delete item.value; + res.cookie(name, value, item); + }); + + // do result + if (lodash.isFunction(matchRequet.result)) { + matchRequet.result(req, res); + } else if ( + lodash.isArray(matchRequet.result) || isObject(matchRequet.result) + ) { + !matchRequet.type && res.type('json'); + res.json(matchRequet.result); + } else { + !matchRequet.type && res.type('text'); + res.send(matchRequet.result.toString()); + } + }; + }; + + api.onStart(() => { + loadMock = createMock(); + if (!mockFlag) return; + + chokidar.watch(mockFile, { + ignoreInitial: true + }).on('change', () => { + api.logger.info('mock.js changed,reload'); + loadMock = createMock(); + }); + }); + api.addBeforeMiddlewares(() => { + if (!mockFlag) return []; + return [ + bodyParser.json(), + bodyParser.urlencoded({ + extended: false + }), + cookieParser() + ]; + }); + api.addBeforeMiddlewares(() => (req, res, next) => { + if (!mockFlag) return next(); + loadMock(req, res, next); + }); +}; diff --git a/packages/fes-template/.fes.js b/packages/fes-template/.fes.js index db77aa6f..869280bb 100644 --- a/packages/fes-template/.fes.js +++ b/packages/fes-template/.fes.js @@ -12,6 +12,15 @@ export default { admin: ["/", "/onepiece"] } }, + mock: { + prefix: '/v2' + }, + proxy: { + '/v2': { + 'target': 'https://api.douban.com/', + 'changeOrigin': true, + }, + }, layout: { title: "Fes.js", footer: 'Created by MumbelFe', diff --git a/packages/fes-template/mock.js b/packages/fes-template/mock.js new file mode 100644 index 00000000..b48cd9dc --- /dev/null +++ b/packages/fes-template/mock.js @@ -0,0 +1,130 @@ +module.exports = (cgiMock, Mock) => { + const { Random } = Mock; + + // 测试 proxy 与 mock 用例集合 + cgiMock('/movie/in_theaters_mock', (req, res) => { + res.send(JSON.stringify({ + code: '0', + msg: '', + result: { + text: 'movie: movie/in_theaters_mock ~~~~~' + } + })); + }); + cgiMock('/movie/test_mock', (req, res) => { + res.send(JSON.stringify({ + code: '0', + msg: '', + result: { + text: 'mock: movie/test_mock' + } + })); + }); + + // 测试用例: mock.js change,重现请求,需要能拉最新的数据 + cgiMock('/watchtest', (req, res) => { + res.send(JSON.stringify({ + code: '0', + msg: '', + result: { + text: '通过 register 测试 mock watch: 初始状态' + } + })); + }); + + // 返回一个数字 + // cgiMock('/number', 666); + cgiMock('/number', 999); + + // 返回一个json + cgiMock({ + url: '/json', + result: { + code: '400101', msg: "不合法的请求:Missing cookie 'wb_app_id' for method parameter of type String", transactionTime: '20170309171146', success: false + } + }); + + // 利用 mock.js 产生随机文本 + cgiMock('/text', Random.cparagraph()); + + // 返回一个字符串 利用 mock.js 产生随机字符 + cgiMock('/random', Mock.mock({ + 'string|1-10': '★' + })); + + // 正则匹配url, 返回一个字符串 + cgiMock(/\/abc|\/xyz/, 'regexp test!'); + + // option.result 参数如果是一个函数, 可以实现自定义返回内容, 接收的参数是是经过 express 封装的 req 和 res 对象. + cgiMock(/\/function$/, (req, res) => { + res.send('function test'); + }); + + // 返回文本 readFileSync + cgiMock('/file', cgiMock.file('./package.json')); + + // 更复杂的规则配置 + cgiMock({ + url: /\/who/, + method: 'GET', + result(req, res) { + if (req.query.name === 'kwan') { + res.json({ kwan: '孤独患者' }); + } else { + res.send('Nooooooooooo'); + } + }, + headers: { + 'Content-Type': 'text/plain', + 'Content-Length': '123', + ETag: '12345' + }, + cookies: [ + { + name: 'myname', value: 'kwan', maxAge: 900000, httpOnly: true + } + ] + }); + + // 携带参数的请求 + cgiMock('/v2/audit/list', (req, res) => { + const { + currentPage, pageSize, isAudited + } = req.body; + res.send({ + code: '0', + msg: '', + data: { + currentPage, + pageSize, + totalPage: 2, + totalCount: 12, + pageData: Array.from({ length: pageSize }, () => ({ + title: Random.title(), + authorName: Random.cname(), + authorId: Random.name(), + createTime: Date.now(), + updateTime: Date.now(), + readCount: Random.integer(60, 1000), + favoriteCount: Random.integer(1, 50), + postId: '12323', + serviceTag: '业务类型', + productTag: '产品类型', + requestTag: '需求类型', + handleTag: '已采纳', + postType: 'voice', + postStatus: isAudited ? 'pass' : 'auditing', + auditStatus: 'audit1' + })) + } + }); + }); + + // multipart/form-data 类型 + cgiMock('/v2/upload', (req, res) => { + res.send({ + code: '0', + msg: '文件上传成功' + }); + }); +}; diff --git a/packages/fes-template/package.json b/packages/fes-template/package.json index d5e5c1de..1cc9e9bf 100644 --- a/packages/fes-template/package.json +++ b/packages/fes-template/package.json @@ -54,6 +54,7 @@ "@webank/fes-plugin-enums": "^2.0.0-alpha.0", "@webank/fes-plugin-jest": "^2.0.0-alpha.0", "@webank/fes-plugin-vuex": "^2.0.0-alpha.0", + "@webank/fes-plugin-request": "2.0.0-alpha.1", "ant-design-vue": "2.0.0-rc.3", "vue": "3.0.5", "vuex": "^4.0.0-rc.2" diff --git a/packages/fes-template/src/pages/index.vue b/packages/fes-template/src/pages/index.vue index 1ba658b3..c7e30af0 100644 --- a/packages/fes-template/src/pages/index.vue +++ b/packages/fes-template/src/pages/index.vue @@ -22,11 +22,9 @@ import { ref, onMounted, computed } from 'vue'; import { useStore } from 'vuex'; import { - access, useAccess, useRouter, useI18n, locale, enums + access, useAccess, useRouter, useI18n, locale, enums, request } from '@webank/fes'; -console.log(__DEV__); - export default { setup() { const fes = ref('fes upgrade to vue3'); @@ -81,6 +79,25 @@ export default { accessId.value = '11'; }, 4000); // router.push('/onepiece'); + + console.log('测试 mock!!'); + request('/v2/file').then((data) => { + console.log(data); + }).catch((err) => { + console.log(err); + }); + request('/v2/movie/in_theaters_mock').then((data) => { + console.log(data); + }).catch((err) => { + console.log(err); + }); + + console.log('测试 proxy!!'); + request('/v2/movie/in_theaters_proxy').then((resp) => { + console.log(resp); + }).catch((err) => { + console.log(err); + }); }); return { accessId,