├── .nvmrc ├── docs ├── .nojekyll ├── _navbar.md ├── assets │ └── images │ │ ├── datagent-run.png │ │ ├── method-hooks-data.png │ │ ├── hook-function-input-output.png │ │ ├── queue-input-output-protocol.png │ │ └── hook-function-setting-yourself.png ├── 1.0 │ ├── _navbar.md │ ├── README.md │ └── API.md ├── index.html ├── TODOList.md ├── README.md └── API.md ├── .gitattributes ├── test ├── mocha.opts ├── README.md ├── setup.js ├── units │ ├── queue.spec.js │ ├── contact.spec.js │ ├── agent.spec.js │ ├── schema.spec.js │ ├── operation.spec.js │ ├── remote.spec.js │ └── model.spec.js └── examples │ ├── Remote.class.js │ └── custom-remote.test.js ├── .nycrc ├── .travis.yml ├── src ├── utils │ └── index.js ├── context.js ├── queue.js ├── index.js ├── remote.js ├── contact.js ├── schema.js ├── operations.js ├── agent.js └── model.js ├── .babelrc ├── .vscode └── launch.json ├── LICENSE ├── .gitignore ├── rollup.config.js ├── CHANGELOG.md ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.13.1 -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require @babel/register -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | * [学习](README.md) 2 | 3 | * [API](API.md) 4 | 5 | * 版本 6 | * [2.x](README.md) 7 | * [1.x](1.0/README.md) -------------------------------------------------------------------------------- /docs/assets/images/datagent-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpreterite/datagent/HEAD/docs/assets/images/datagent-run.png -------------------------------------------------------------------------------- /docs/1.0/_navbar.md: -------------------------------------------------------------------------------- 1 | * [学习](1.0/README.md) 2 | 3 | * [API](1.0/API.md) 4 | 5 | * 版本 6 | * [2.x](README.md) 7 | * [1.x](1.0/README.md) -------------------------------------------------------------------------------- /docs/assets/images/method-hooks-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpreterite/datagent/HEAD/docs/assets/images/method-hooks-data.png -------------------------------------------------------------------------------- /docs/assets/images/hook-function-input-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpreterite/datagent/HEAD/docs/assets/images/hook-function-input-output.png -------------------------------------------------------------------------------- /docs/assets/images/queue-input-output-protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpreterite/datagent/HEAD/docs/assets/images/queue-input-output-protocol.png -------------------------------------------------------------------------------- /docs/assets/images/hook-function-setting-yourself.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpreterite/datagent/HEAD/docs/assets/images/hook-function-setting-yourself.png -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@istanbuljs/nyc-config-babel", 3 | "exclude": [ 4 | "test", 5 | "node_modules", 6 | "**/node_modules" 7 | ] 8 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | os: 3 | - linux 4 | node_js: 5 | - '8' 6 | install: 7 | - yarn 8 | cache: 9 | yarn: true 10 | directories: 11 | - node_modules 12 | branches: 13 | only: 14 | - master 15 | - develop 16 | sudo: 17 | - true -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # 测试内容 2 | 3 | ```js 4 | 5 | field('data', format()) 6 | 7 | function field(fieldName, action){ 8 | return async ctx =>{ 9 | const fieldVal = ctx.result[fieldName] 10 | ctx.result[fieldName] = await action({ ...ctx, result:fieldVal }); 11 | return Promise.resolve(ctx) 12 | } 13 | } 14 | 15 | ``` -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * setup.js is bootstrap in mocha-webpack, must use current nodejs verstion supported script code. 3 | * The way is not support ES6~7 and higher ECMAscript version. 4 | */ 5 | 6 | var chai = require("chai"); 7 | var chaiAsPromised = require("chai-as-promised"); 8 | chai.use(chaiAsPromised); 9 | 10 | global.assert = chai.assert; 11 | global.axios = require('axios'); 12 | global.MockAdapter = require('axios-mock-adapter'); 13 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function awaitTo(promise) { 2 | return promise.then(data => { 3 | return [null, data]; 4 | }).catch(err => [err]); 5 | } 6 | export function existError(compare, err){ 7 | return (...vals)=>{ 8 | if(!compare(...vals)){throw err} 9 | } 10 | } 11 | export const isDef = val => typeof val !== 'undefined' 12 | export const isString = val => typeof val === 'string' 13 | export const isArray = val => val.constructor === Array 14 | export const isFunction = val => val.constructor === Function -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | 2 | function Context(options){ 3 | const { scope, method, ..._opts } = {args:null, result:null, ...options} 4 | const context = { 5 | ..._opts 6 | } 7 | Object.defineProperties(context, { 8 | "scope":{ 9 | get(){ 10 | return scope 11 | } 12 | }, 13 | "method":{ 14 | get(){ 15 | return method 16 | } 17 | } 18 | }) 19 | return context 20 | } 21 | 22 | const factory = options => new Context(options) 23 | export const constructor = Context 24 | export default factory -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "modules": false, 7 | "targets": "> 5%" 8 | } 9 | ] 10 | ], 11 | "env": { 12 | "test": { 13 | "retainLines": true, 14 | "sourceMaps": "inline", 15 | "presets": [ 16 | [ 17 | "@babel/env", 18 | { 19 | "targets": { 20 | "node": "current" 21 | } 22 | } 23 | ] 24 | ], 25 | "plugins": ["istanbul"] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/queue.js: -------------------------------------------------------------------------------- 1 | export {default as context} from "./context" 2 | 3 | export const compose = (...list) => acc => list.reduce((acc, fn) => acc.then(fn), Promise.resolve(acc)); 4 | export function generate(queues) { 5 | const queue = compose(...queues); 6 | return (args, ctx={}) => { 7 | ctx = Object.assign(ctx, {args}); 8 | return new Promise((resolve, reject) => { 9 | queue(ctx) 10 | .then(ctx=>resolve(ctx.result)) 11 | .catch(reject); 12 | }); 13 | } 14 | } 15 | export function wrap(method){ 16 | return ctx=>{ 17 | return method.apply(ctx.scope, ctx.args).then(data=>Promise.resolve({...ctx, result: data})) 18 | } 19 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as hooks from "./operations" 2 | import * as utils from "./utils/" 3 | import { default as model, constructor as Model } from "./model" 4 | import { default as contact, constructor as Contact } from "./contact" 5 | import { default as schema, constructor as Schema } from "./schema" 6 | import { default as agent, constructor as Agent } from "./agent" 7 | import { default as context, constructor as Context } from "./context" 8 | import { default as remote, constructor as Remote } from "./remote" 9 | const classes = { Model, Contact, Schema, Agent, Context, Remote } 10 | const constructors = classes 11 | export { hooks, utils, model, contact, schema, agent, context, remote, classes, constructors } 12 | export default { utils, hooks, model, contact, schema, agent, context, remote, classes, constructors } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha-webpack Tests", 11 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--require", 14 | "test/setup.js", 15 | "test/**/*.{test,spec}.js" 16 | ], 17 | "sourceMaps": true, 18 | "env": { 19 | "NODE_ENV": "test" 20 | }, 21 | "internalConsoleOptions": "openOnSessionStart" 22 | }, 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "name": "Launch Program", 27 | "program": "${workspaceFolder}\\src\\index.js" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Datagent - 一个用于模块化管理前端请求的工具 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 21 | 22 |
23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) packy (github.com/packy) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .reify-cache 64 | .nyc_output 65 | coverage 66 | dist -------------------------------------------------------------------------------- /test/units/queue.spec.js: -------------------------------------------------------------------------------- 1 | import * as _queue from '../../src/queue'; 2 | 3 | describe('queue Class Test', function() { 4 | var err, result; 5 | var ctx = {}; 6 | var data = []; 7 | 8 | function asyncFun(fn, time){ 9 | return new Promise((resolve,reject)=>{ 10 | setTimeout(()=>{ 11 | try{ 12 | resolve(fn()); 13 | }catch(e){ 14 | reject(e); 15 | } 16 | }, time); 17 | }); 18 | } 19 | 20 | describe('queue.generate()', function() { 21 | it('应当输出内容顺序为[1,2,3]的数组', async function() { 22 | const operations = [ 23 | async function (ctx) { 24 | return await asyncFun(()=>{ 25 | ctx.result = [].concat(ctx.args, 1); 26 | return ctx; 27 | }, 150); 28 | }, 29 | async function (ctx) { 30 | return await asyncFun(() => { 31 | ctx.result = [].concat(ctx.result || [], 2); 32 | return ctx; 33 | }, 100); 34 | }, 35 | async function (ctx) { 36 | return await asyncFun(() => { 37 | ctx.result = [].concat(ctx.result, 3); 38 | return ctx; 39 | }, 200); 40 | }, 41 | ]; 42 | 43 | result = await _queue.generate(operations)(data, ctx); 44 | 45 | return assert.includeOrderedMembers(result, [1,2,3]); 46 | }); 47 | }); 48 | }); -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import { terser } from 'rollup-plugin-terser' 4 | import babel from 'rollup-plugin-babel' 5 | // import istanbul from 'rollup-plugin-istanbul' 6 | 7 | import pkg from './package.json' 8 | const browser = "dist/datagent.umd.js" 9 | 10 | const name = "datagent" 11 | const sourcemap = true 12 | 13 | const plugins = [] 14 | if(process.env.BUILD === 'production'){ 15 | plugins.push(terser({ sourcemap })) 16 | } 17 | 18 | export default [ 19 | { 20 | input: 'src/index.js', 21 | output: [ 22 | { name, file: browser, format: 'umd', exports: 'named', sourcemap }, 23 | { name, file: pkg.main, format: 'cjs', exports: 'named', sourcemap }, 24 | { name, file: pkg.module, format: 'es', exports: 'named', sourcemap } 25 | ], 26 | plugins: [ 27 | babel({ 28 | "exclude": 'node_modules/**', 29 | "presets": [ 30 | [ 31 | "@babel/preset-env", 32 | { 33 | "useBuiltIns": "entry", 34 | "include": ["@babel/plugin-transform-destructuring"] 35 | } 36 | ] 37 | ] 38 | }), 39 | resolve(), 40 | commonjs(), 41 | //// babel is use istanbul, so rollup not to use istanbul in plugin 42 | // istanbul({ 43 | // exclude: ['test/**/*','node_modules/**/*'] 44 | // }), 45 | 46 | ...plugins 47 | ] 48 | } 49 | ] -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | ## 2.0.0-beta.4 3 | 4 | - 优化低版本浏览器兼容问题,增加展开语法的低阶转换处理 5 | 6 | ## 2.0.0-beta.2 7 | 8 | - 为了让webpack优先价值esm模式代码,package.json已移除browser设置。 9 | 10 | ## 2.0.0-beta.1 11 | 12 | - 增加`agent`类提供统一处理数据对象方法的调用,提供`before`,`after`,`error`事件做额外处理 13 | - 数据对象(`Model`)移除`field`设置 14 | - 数据对象(`Model`)的钩子设置支持自定义执行顺序,移除`before`和`after`执行函数列表 15 | - 数据对象(`Model`)调用方法不再支持动态增加钩子函数 16 | - `format`,`filter`钩子函数必须设置数据模型(`Schema`),不再支持默认使用数据对象(`Model`)的`field`设置 17 | - 构建环境从`webpack`改用`rollup`,项目使用[`sao-esmodule-mold`](https://github.com/lpreterite/sao-esmodule-mold)模板基于[`sao`](https://github.com/saojs/sao)生成 18 | - 更新所有代码编码方式,移除class改用function方式定义 19 | - 更新说明文档和API文档,提供可访问地址:https://lpreterite.github.io/datagent/ 20 | - 更新测试内容 21 | 22 | ## 1.1.5 23 | 24 | - 优化低版本浏览器兼容问题,增加展开语法的低阶转换处理 25 | 26 | ## 1.1.4 27 | 28 | - 构建工具从webpack改为rollup 29 | - 为了让webpack优先价值esm模式代码,package.json已移除browser设置。 30 | 31 | ## 1.1.3 32 | 33 | - 修复当`id`等于`null`时会作为id加至`POST`请求链接上的问题 34 | 35 | ## 1.1.2 36 | 37 | - 修复在`save:before`的钩子下处理传入参数时,把最后参数作为处理的数据对象进行格式化。 38 | 39 | ## 1.1.1 40 | 41 | - 添加`getField`的钩子处理方法 42 | - 修复format函数在处理null值时会转换的问题 43 | 44 | ## 1.1.0 45 | 46 | - find与destroy方法改为接受params参数(不再只是id)。 47 | - fieldSet默认值default支持使用函数:`{ type: Date, default: Date.now }`。 48 | - 修改format规则:当字段值与默认值一致时,不作任何处理直接输出原有的值。 49 | - 文档加上`mapSendHook`与`mapReceiveHook`例子。 50 | 51 | ## 1.0.3 52 | 53 | - 修复判断对象是否为新对象的方法逻辑,当`id`为`0`,`null`,`undefined`都判断为新对象。 54 | 55 | ## 1.0.2 56 | 57 | - 修复数据模型方法调用时设置的after hooks会在数据模型定义的after hooks前被调用的问题 #3 58 | 59 | ## 1.0.1 60 | 61 | - 修复`DataModel.prototype.delete`调用卡死问题(#2) 62 | - 调试项目命令行去掉`--debug`参数(#1) 63 | - 说明文档添加简单使用例子,更新引用钩子方法的使用 -------------------------------------------------------------------------------- /test/examples/Remote.class.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch" 2 | import { URLSearchParams } from "url" 3 | 4 | class Remote { 5 | constructor(options){ 6 | const { baseURL, withJson=true } = { ...options } 7 | this._baseURL = baseURL 8 | this._withJson = withJson 9 | } 10 | sync(options){ 11 | let { method, data, body, headers } = options 12 | const url = this._baseURL + options.url 13 | if(this._withJson){ 14 | headers = !!headers ? headers : {} 15 | headers['Content-Type'] = 'application/json' 16 | body = JSON.stringify(data) 17 | }else{ 18 | body = data 19 | } 20 | return fetch(url, { method, body, headers }).then(res=>new Promise((resolve, reject)=>{ 21 | res.json().then(data=>resolve({ 22 | status: res.status, 23 | statusText: res.statusText, 24 | data, 25 | headers: res.headers, 26 | url: res.url 27 | }), reject) 28 | })) 29 | } 30 | get(url, _params={}){ 31 | const params = new URLSearchParams() 32 | Object.keys(_params).forEach(key=>params.append(key, _params[key])) 33 | url += `/${params.toString()}` 34 | return this.sync({ method: "GET", url }) 35 | } 36 | post(url, data){ 37 | return this.sync({ method: "POST", url, data }) 38 | } 39 | put(url, data){ 40 | return this.sync({ method: "PUT", url, data }) 41 | } 42 | patch(url, data){ 43 | return this.sync({ method: "PATCH", url, data }) 44 | } 45 | delete(url, data){ 46 | return this.sync({ method: "DELETE", url, data }) 47 | } 48 | } 49 | export default Remote -------------------------------------------------------------------------------- /test/units/contact.spec.js: -------------------------------------------------------------------------------- 1 | import _remote from '../../src/remote'; 2 | import _contact from '../../src/contact'; 3 | import datagent from '../../src/'; 4 | 5 | describe('Contact Class Test', () => { 6 | let contact, remotes; 7 | 8 | beforeEach(() => { 9 | remotes = { 10 | 'base': axios.create('localhost/api'), 11 | 'test': axios.create('localhost:8881/api') 12 | }; 13 | contact = _contact(); 14 | }); 15 | 16 | describe('instance.remote()', () => { 17 | it('应当根据名称记录远端服务', () => { 18 | contact.remote('base', _remote(remotes.base)); 19 | contact.remote('test', _remote(remotes.test)); 20 | assert.exists(contact.remote('base'), '没有添加至连接器中'); 21 | }) 22 | it('应当支持默认设置', () => { 23 | const testRemote = _remote(remotes.test); 24 | contact.remote('base', _remote(remotes.base)); 25 | contact.remote('test', testRemote); 26 | contact.default('test'); 27 | assert(contact.remote() === testRemote, "获得远端服务应为test"); 28 | }) 29 | it('当不传入名称时应当返回默认远程服务', () => { 30 | contact.remote('base', _remote(remotes.base)); 31 | contact.default('base') 32 | assert.exists(contact.remote(), '没有返回远程服务'); 33 | }) 34 | it('当传入名称没有找到远端服务时要报错', () => { 35 | contact.remote('base', _remote(remotes.base)); 36 | assert.throws(()=>contact.remote('develop'), /No 'develop' found in remotes/); 37 | }) 38 | }) 39 | 40 | describe('instance.default()', ()=>{ 41 | it('输入参数name必须是字符串', ()=>{ 42 | contact = datagent.contact(remotes); 43 | assert.throws(()=>contact.default(233), /The name must be String in contact/); 44 | }) 45 | it('输入参数name必须存在于remote的键', () => { 46 | contact = datagent.contact(remotes); 47 | assert.throws(()=>contact.default('233'), /No '233' found in remotes/); 48 | }) 49 | }) 50 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datagent", 3 | "version": "2.0.0-beta.4", 4 | "description": "一个用于模块化管理前端请求的工具", 5 | "main": "dist/datagent.cjs.js", 6 | "module": "dist/datagent.esm.js", 7 | "jsnext:main": "dist/datagent.esm.js", 8 | "scripts": { 9 | "build": "cross-env BUILD=production rollup -c", 10 | "watch": "rollup -c -w", 11 | "pretest": "rollup -c", 12 | "test": "cross-env NODE_ENV=test nyc mocha --require test/setup.js test/**/*.spec.js", 13 | "prepublish": "cross-env BUILD=production npm test", 14 | "build:doc": "documentation build src/** -f md -o docs/API.md" 15 | }, 16 | "files": [ 17 | "dist", 18 | "src" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/packy/datagent.git" 23 | }, 24 | "keywords": [ 25 | "datagent", 26 | "restful", 27 | "model", 28 | "data", 29 | "data-filter", 30 | "data-model" 31 | ], 32 | "author": "packy-tang (http://github.com/packy)", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/packy/datagent/issues" 36 | }, 37 | "homepage": "https://github.com/packy/datagent#readme", 38 | "peerDependencies": { 39 | "axios": "^0.19.0" 40 | }, 41 | "devDependencies": { 42 | "@babel/cli": "^7.6.4", 43 | "@babel/core": "^7.6.4", 44 | "@babel/preset-env": "^7.6.3", 45 | "@babel/register": "^7.6.2", 46 | "@istanbuljs/nyc-config-babel": "^2.1.1", 47 | "axios": "^0.19.0", 48 | "axios-mock-adapter": "^1.17.0", 49 | "babel-plugin-istanbul": "^5.2.0", 50 | "chai": "^4.2.0", 51 | "chai-as-promised": "^7.1.1", 52 | "cross-env": "^5.2.0", 53 | "fecha": "^3.0.3", 54 | "mocha": "^6.1.1", 55 | "node-fetch": "^2.6.0", 56 | "nyc": "^13.3.0", 57 | "reify": "^0.18.1", 58 | "rollup": "~1.9.0", 59 | "rollup-plugin-babel": "^4.3.3", 60 | "rollup-plugin-commonjs": "^9.3.4", 61 | "rollup-plugin-css-only": "^1.0.0", 62 | "rollup-plugin-istanbul": "^2.0.1", 63 | "rollup-plugin-node-resolve": "^4.2.1", 64 | "rollup-plugin-terser": "^4.0.4", 65 | "rollup-plugin-vue": "^4.7.2", 66 | "rollup-watch": "^4.3.1", 67 | "url": "^0.11.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/examples/custom-remote.test.js: -------------------------------------------------------------------------------- 1 | import datagent from "../../src/" 2 | import CustomRemote from './Remote.class' 3 | 4 | const handle = res => { 5 | let err, result; 6 | if(res.status < 200){ 7 | err = new Error(res.statusText); 8 | } 9 | result = res.data; 10 | return Promise.resolve([err, result]); 11 | }; 12 | 13 | describe('Custom Remote Test', function(){ 14 | let contact, remote, post; 15 | before(function (){ 16 | contact = datagent.contact( 17 | //remote的设定 18 | { 19 | base: { baseURL: 'https://jsonplaceholder.typicode.com' } 20 | }, 21 | //生成时替换为自定义的remote 22 | { 23 | RemoteConstructor: CustomRemote 24 | } 25 | ) 26 | }) 27 | after(function (){ 28 | post = null 29 | }) 30 | it('发送GET请求', async function(){ 31 | this.timeout(5000) 32 | let err, result; 33 | [err, result] = await contact.remote().get('/todos/3').then(handle); 34 | assert.propertyVal(result, 'title', 'fugiat veniam minus') 35 | }) 36 | it('发送POST请求', async function(){ 37 | this.timeout(5000) 38 | let err, result; 39 | [err, result] = await contact.remote().post('/todos', { title: "do something", completed: false, userId: 1 }).then(handle); 40 | assert.property(result, "id", "应当返回id字段"); 41 | }) 42 | it('发送PUT请求', async function(){ 43 | this.timeout(5000) 44 | let err, result; 45 | [err, result] = await contact.remote().put('/todos/3', { id: 3, title: "fugiat veniam minus", completed: true, userId: 1 }).then(handle); 46 | assert.propertyVal(result, "completed", true); 47 | }) 48 | it('发送PATCH请求', async function(){ 49 | this.timeout(5000) 50 | let err, result; 51 | [err, result] = await contact.remote().patch('/todos/3', { id: 3, completed: true }).then(handle); 52 | assert.propertyVal(result, "completed", true); 53 | }) 54 | it('发送DELETE请求', async function(){ 55 | this.timeout(5000) 56 | const res = await contact.remote().delete('/todos/3'); 57 | assert.propertyVal(res, "status", 200); 58 | }) 59 | }) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datagent 2 | 3 | [![npm version](https://img.shields.io/npm/v/datagent.svg)](https://www.npmjs.com/package/datagent) 4 | [![NPM downloads](http://img.shields.io/npm/dm/datagent.svg)](https://www.npmjs.com/package/datagent) 5 | [![build status](https://travis-ci.org/lpreterite/datagent.svg?branch=master)](https://travis-ci.org/lpreterite/datagent) 6 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Flpreterite%2Fdatagent.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Flpreterite%2Fdatagent?ref=badge_shield) 7 | 8 | `Datagent`是一个用于模块化管理前端请求的工具,提供数据格式化、多服务源切换、语义化数据定义等功能。在 React,Vue,Angular 等现代 JavaScript 框架下,UI 显示均以数据驱动为中心,服务端提供的数据不是所有场合都能符合 UI 所需的结构。格式化数据、转义数据的代码往往不可避免的写在UI组件、业务逻辑代码或是页面等各个地方,导致冗余代码、逻辑复杂又难以维护等问题。面对这类情况可使用`Datagent`解决这类问题,不单单能统一调取后端服务和格式化从服务端获得的数据,定义一些处理后还能用于所有场景,让你更方便同步UI状态。 9 | 10 | ![datagent-run](./docs/assets/images/datagent-run.png) 11 | 12 | > 你可以马上尝试在`codepen`上的[例子](https://codepen.io/packy1980/pen/OEpNWW/)。 13 | 14 | ## 安装 15 | 16 | ```sh 17 | npm install -S datagent 18 | //or 19 | yarn add datagent 20 | ``` 21 | 22 | 目前正式版本为`1.x`,下面是安装`2.0`版本尝尝鲜。 23 | 24 | ```sh 25 | npm install -S datagent@next 26 | // or 27 | yarn add datagent@next 28 | ``` 29 | 30 | ## 文档 31 | 32 | - [介绍](https://lpreterite.github.io/datagent/#/?id=介绍) 33 | - [什么是 datagent.js](https://lpreterite.github.io/datagent/#/?id=什么是-datagentjs) 34 | - [开始](https://lpreterite.github.io/datagent/#/?id=开始) 35 | - [管理你的服务](https://lpreterite.github.io/datagent/#/?id=管理你的服务) 36 | - [定义数据字段](https://lpreterite.github.io/datagent/#/?id=定义数据字段) 37 | - [数据处理](https://lpreterite.github.io/datagent/#/?id=数据处理) 38 | - [统一调用](https://lpreterite.github.io/datagent/#/?id=统一调用) 39 | - [深入了解](https://lpreterite.github.io/datagent/#/?id=深入了解) 40 | - [远端与axios](https://lpreterite.github.io/datagent/#/?id=远端与axios) 41 | - [自定义字段类型](https://lpreterite.github.io/datagent/#/?id=自定义字段类型) 42 | - [方法与钩子](https://lpreterite.github.io/datagent/#/?id=方法与钩子) 43 | - [自定义钩子](https://lpreterite.github.io/datagent/#/?id=自定义钩子) 44 | - [迁移](https://lpreterite.github.io/datagent/#/?id=迁移) 45 | - [从 1.x 迁移](https://lpreterite.github.io/datagent/#/?id=从-1x-迁移) 46 | 47 | ## License 48 | 49 | Datagent是根据[MIT协议](/LICENSE)的开源软件 50 | 51 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Flpreterite%2Fdatagent.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Flpreterite%2Fdatagent?ref=badge_large) 52 | -------------------------------------------------------------------------------- /src/remote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Promise based HTTP client for the browser and node.js 3 | * @external axios 4 | * @see {@link https://www.npmjs.com/package/axios} 5 | */ 6 | 7 | /** 8 | * Requests can be made by passing the relevant config to axios. 9 | * @external axios.config 10 | * @see {@link https://www.npmjs.com/package/axios#axios-api} 11 | */ 12 | 13 | /** 14 | * 远端,一般指后端服务,远端作为记录后端服务的功能节点 15 | * 16 | * @param {axios} origin - 服务源头,一般指`axios` 17 | * @property {axios} origin - 服务源头,一般指`axios` 18 | * @class 19 | * 20 | * @example 21 | * import axios from "axios" 22 | * import datagent from "datagent" 23 | * const remote = datagent.remote(axios.create({ baseURL: "http://localhost:8081" })) 24 | * 25 | * remote.get('/user', { q: "pa" }).then(res=>console.log(res)) 26 | * // request 'http://localhost:8081/user?q=pa' 27 | * // output respond like: { status: 200, data: {...}, headers: {...} } 28 | */ 29 | function Remote(origin){ 30 | /** 31 | * 发起请求 32 | * @param {axios.config} options 33 | * @memberof Remote 34 | * @return {Promise} 35 | */ 36 | const sync = (options)=>{ 37 | return origin(options); 38 | } 39 | const methods = { 40 | /** 41 | * 发起GET请求 42 | * @param {String} url 请求地址 43 | * @param {*} params 请求参数 44 | * @memberof Remote 45 | * @return {Promise} 46 | */ 47 | get: (url, params)=>sync({ method: 'GET', url, params }), 48 | 49 | /** 50 | * 发起POST请求 51 | * @param {String} url 请求地址 52 | * @param {*} data 请求参数 53 | * @memberof Remote 54 | * @return {Promise} 55 | */ 56 | post: (url, data)=>sync({ method: 'POST', url, data }), 57 | 58 | /** 59 | * 发起PUT请求 60 | * @param {String} url 请求地址 61 | * @param {*} data 请求参数 62 | * @memberof Remote 63 | * @return {Promise} 64 | */ 65 | put: (url, data)=>sync({ method: 'PUT', url, data }), 66 | 67 | /** 68 | * 发起PATCH请求 69 | * @param {String} url 请求地址 70 | * @param {*} data 请求参数 71 | * @memberof Remote 72 | * @return {Promise} 73 | */ 74 | patch: (url, data)=>sync({ method: 'PATCH', url, data }), 75 | 76 | /** 77 | * 发起DELETE请求 78 | * @param {String} url 请求地址 79 | * @param {*} data 请求参数 80 | * @memberof Remote 81 | * @return {Promise} 82 | */ 83 | delete: (url, data)=>sync({ method: 'DELETE', url, data }) 84 | } 85 | const context = { 86 | sync, 87 | ...methods 88 | } 89 | 90 | Object.defineProperties(context, { 91 | "origin":{ 92 | get(){ 93 | return origin 94 | } 95 | } 96 | }) 97 | 98 | return Object.freeze(context) 99 | } 100 | 101 | const factory = origin => new Remote(origin) 102 | export const constructor = Remote 103 | export default factory -------------------------------------------------------------------------------- /test/units/agent.spec.js: -------------------------------------------------------------------------------- 1 | import datagent from '../../src/'; 2 | const { awaitTo } = datagent.utils 3 | 4 | describe('Agent Test', function () { 5 | let mock, hosts, contact; 6 | describe('on()', function () { 7 | before(function() { 8 | hosts = { 9 | base: 'http://localhost/api', 10 | test: 'http://localhost:8081/api' 11 | }; 12 | 13 | contact = datagent.contact({ 14 | base: axios.create({ baseURL: hosts.base }) 15 | }); 16 | 17 | mock = { 18 | base: new MockAdapter(contact.remote('base').origin) 19 | } 20 | }) 21 | afterEach(function () { 22 | mock.base.reset(); 23 | }) 24 | 25 | it('出错时回调on绑定的error函数', async function () { 26 | let [err, result] = await awaitTo(new Promise((resolve, reject)=>{ 27 | mock 28 | .base 29 | .onGet(hosts.base + '/users') 30 | .reply(401); 31 | const model = datagent.model({ 32 | name: 'user', 33 | url: '/users', 34 | contact 35 | }); 36 | const agent = datagent.agent([model]) 37 | agent.on('error', err=>resolve(err)) 38 | agent.fetch(model.name) 39 | })) 40 | assert.equal(result, "Error: Request failed with status code 401", "应该返回错误信息为:'Error: Request failed with status code 401'"); 41 | }) 42 | it('每次执行方法前回调on绑定的before函数', async function () { 43 | let [err, result] = await awaitTo(new Promise((resolve, reject)=>{ 44 | mock 45 | .base 46 | .onGet(hosts.base + '/users') 47 | .reply(200, [{id:1, name:"Packy"}]); 48 | const model = datagent.model({ 49 | name: 'user', 50 | url: '/users', 51 | contact 52 | }); 53 | const agent = datagent.agent([model]) 54 | agent.on('before', ({model_name, action})=>resolve(`The ${model_name} use ${action}`)) 55 | agent.fetch(model.name) 56 | })) 57 | assert.equal(result, "The user use fetch", "应该返回的信息为:'The user use fetch'"); 58 | }) 59 | it('每次执行方法后回调on绑定的after函数', async function () { 60 | const data = [{id:1, name:"Packy"}] 61 | let [err, result] = await awaitTo(new Promise((resolve, reject)=>{ 62 | mock 63 | .base 64 | .onGet(hosts.base + '/users') 65 | .reply(200, data); 66 | const model = datagent.model({ 67 | name: 'user', 68 | url: '/users', 69 | contact 70 | }); 71 | const agent = datagent.agent([model]) 72 | agent.on('after', (err, result)=>resolve(result.data)) 73 | agent.fetch(model.name) 74 | })) 75 | assert.sameDeepMembers(result, data); 76 | }) 77 | }) 78 | }) -------------------------------------------------------------------------------- /src/contact.js: -------------------------------------------------------------------------------- 1 | import { existError, isDef, isString } from "./utils/" 2 | import { constructor as Remote } from "./remote" 3 | /** 4 | * 链接管理器 5 | * 6 | * @param {*} [remotes={}] 7 | * @class 8 | * 9 | * @example 10 | * import axios from "axios" 11 | * import datagent from "datagent" 12 | * const contact = datagent.contact({ base: axios.create({ baseURL: 'http://localhost:8081' }) }) 13 | * 14 | * console.log('contact has test:'+contact.has('test')) 15 | * // output: 'contact has test:false' 16 | * 17 | * // use default remote 18 | * console.log(contact.default().get('/user')) 19 | * // request 'http://localhost:8081/user' 20 | * // output respond like: { status: 200, data: {...}, headers: {...} } 21 | * 22 | * // set nothing parmas will get default remote with remote function 23 | * console.log(contact.remote().get('/user')) 24 | * // request 'http://localhost:8081/user' 25 | * // output respond like: { status: 200, data: {...}, headers: {...} } 26 | */ 27 | function Contact(remotes={}, options){ 28 | options = { RemoteConstructor: Remote, ...options } 29 | const { RemoteConstructor } = options 30 | let _default = null 31 | Object.keys(remotes).forEach(remoteName=>{ 32 | remotes[remoteName]=new RemoteConstructor(remotes[remoteName]) 33 | }) 34 | _default = Object.values(remotes).shift() 35 | 36 | /** 37 | * 判断是否存在远端 38 | * 39 | * @memberof Contact 40 | * @param {String} name 远端名称 41 | * @return {Boolean} true 存在, false 不存在 42 | */ 43 | const has = name=>Object.keys(remotes).indexOf(name) > -1 44 | const getRemote = name=>{ 45 | existError(isString, new Error(`The name must be String in contact`))(name) 46 | existError(isDef, new Error(`No '${name}' found in remotes`))(remotes[name]) 47 | return remotes[name] 48 | } 49 | const setRemote = (name, remote)=>(remotes[name]=remote) 50 | const setDefault = name=>_default=getRemote(name) 51 | 52 | const context = { 53 | has, 54 | /** 55 | * 获取或设定默认远端 56 | * 57 | * @memberof Contact 58 | * @param {String?} name 远端名称,不传参为获取默认远端 59 | * @return {Remote} 返回默认远端 60 | */ 61 | default:name=>!!name?setDefault(name):_default, 62 | 63 | /** 64 | * 获取或设定远端 65 | * 66 | * @memberof Contact 67 | * @param {Array} args 68 | * @param {String} args[].name 远端名称,不传参为获取默认远端 69 | * @param {Remote} args[].remote 远端,不传参为获取远端 70 | * @return {Remote} 返回默认远端 71 | * 72 | * @example 73 | * //获得默认远端 74 | * contact.remote() 75 | * //获得远端 76 | * contact.remote('base') 77 | * //设置远端 78 | * contact.remote('test', datagent.remote(axios.create({ baseURL: "http://localhost:8082" }))) 79 | */ 80 | remote: (...args)=>{ 81 | const [name, remote] = args 82 | if(args.length>1) return setRemote(name, remote) 83 | else if(!name) return _default 84 | else return getRemote(name) 85 | } 86 | } 87 | 88 | return Object.freeze(context) 89 | } 90 | 91 | 92 | const factory = (remotes, options) => new Contact(remotes, options) 93 | export const constructor = Contact 94 | export default factory -------------------------------------------------------------------------------- /test/units/schema.spec.js: -------------------------------------------------------------------------------- 1 | import fecha from "fecha"; 2 | import { serialize, format, filter } from '../../src/schema'; 3 | 4 | class DateFormat { 5 | static format(format){ 6 | return val => { 7 | if (typeof val === 'string') return val; 8 | if (typeof val === "number") val = new Date(val); 9 | return fecha.format(val, format); 10 | } 11 | } 12 | static parse(format) { 13 | return val => { 14 | if (val.constructor === Date) return val; 15 | if (typeof val === "number") return new Date(val); 16 | return fecha.parse(val, format); 17 | } 18 | } 19 | } 20 | 21 | describe('Schema Class Test', function(){ 22 | describe('format()', function(){ 23 | it('数据没有定义字段给与默认值', function(){ 24 | const fieldSet = {id: { type: Number, default: 3 }}; 25 | const data = format({ id: undefined }, fieldSet); 26 | assert.equal(data.id, 3); 27 | }) 28 | it('没有设置字段定义时数据字段原样输出', function(){ 29 | const fieldSet = { 30 | name: { type: String, default: "packy" } 31 | }; 32 | const data = format({ id: 0 }, fieldSet); 33 | assert.equal(data.id, 0); 34 | }) 35 | it('数据字段类型应当是设定的类型', function () { 36 | const fieldSet = { id: { type: String, default: null } }; 37 | const data = format({ id:1 }, fieldSet); 38 | assert.isString(data.id, '类型不一致'); 39 | }) 40 | it('应当保留所有数据字段', function () { 41 | const fieldSet = { 42 | id: { type: String, default: null }, 43 | sex: { type: String, default: 0 }, 44 | }; 45 | const data = format({ id: 1, sex: 1, nickname: "Packy" }, fieldSet); 46 | assert.isDefined(data.nickname, "nickname字段应该保留"); 47 | }) 48 | it('支持自定义类型转换', function () { 49 | const fieldSet = { 50 | created_at: { type: DateFormat.format('YYYY-MM-DD HH:mm:ss'), default: '' } 51 | }; 52 | const data = format({ id: 1, sex: 1, nickname: "Packy", created_at: Date.now() }, fieldSet); 53 | assert.isString(data.created_at, "created_at字段应当被转义为String"); 54 | }) 55 | }) 56 | describe('filter()', function () { 57 | it('应当只保留给定字段', function () { 58 | const data = filter({ id: 1, sex: 1, nickname: "Packy" }, ['id','sex']); 59 | assert.isUndefined(data.nickname, "nickname字段应该被过滤掉"); 60 | }) 61 | }) 62 | describe('serialize()', function () { 63 | it('应当返回所以设定字段的默认值', function () { 64 | const format = { 65 | id: { type: String, default: 1 }, 66 | sex: { type: String, default: 0 }, 67 | nickname: { type: String, default: 'Packy' } 68 | }; 69 | const data = serialize(format); 70 | assert.include(data, {id:1,sex:0,nickname:'Packy'}); 71 | }) 72 | it('支持defaul值为函数', function () { 73 | const now = Date.now(); 74 | 75 | const format = { 76 | id: { type: String, default: 1 }, 77 | sex: { type: String, default: 0 }, 78 | nickname: { type: String, default: 'Packy' }, 79 | created_at: { type: Date, default:()=>now } 80 | }; 81 | const data = serialize(format); 82 | assert.include(data, { id: 1, sex: 0, nickname: 'Packy', created_at: now }); 83 | }) 84 | }) 85 | }); 86 | -------------------------------------------------------------------------------- /test/units/operation.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | respondData, 3 | formatFor, 4 | filterFor 5 | } from '../../src/operations'; 6 | import _schema from '../../src/schema'; 7 | 8 | describe('Hook operations function test', ()=>{ 9 | describe('respondData', ()=>{ 10 | let ctx = {}; 11 | it('当respond的status小于200时,上下文的result应当替换为respond.data', async () => { 12 | ctx = { 13 | result: { 14 | status: 200, 15 | data: [{ id: 1, name: 'Tom' }] 16 | } 17 | }; 18 | const context = await respondData()(ctx); 19 | assert.equal(context.result.constructor, Array); 20 | }) 21 | it('当respond的status大于200时,应当抛出错误', async () => { 22 | ctx = { 23 | result: { 24 | status: 500 25 | } 26 | }; 27 | try{ 28 | const context = await respondData()(ctx); 29 | } catch (e) { 30 | assert.equal(e.constructor, Error); 31 | } 32 | }) 33 | }) 34 | describe('formatFor', () => { 35 | let ctx, schema; 36 | beforeEach(()=>{ 37 | schema = _schema({ 38 | id: { type: Number, default: null }, 39 | name: { type: String, default: null }, 40 | sex: { type: Number, default: 0 }, 41 | }); 42 | }) 43 | it('默认转义ctx.result的数据', async () => { 44 | ctx = { 45 | method: 'find', 46 | result: { id: 1, name: 'Tom' } 47 | }; 48 | ctx = await formatFor(schema)(ctx); 49 | assert.ownInclude(ctx.result, { id: 1, name: 'Tom', sex: 0 }); 50 | }) 51 | it('支持指定转义数据的字段', async () => { 52 | ctx = { 53 | method: 'find', 54 | result: { 55 | status: 200, 56 | data: [{ id: 1, name: 'Tom' }] 57 | } 58 | }; 59 | ctx = await respondData()(ctx); 60 | ctx = await formatFor(schema, (ctx, format)=>{ 61 | ctx.result=ctx.result.map(item=>format(item)) 62 | })(ctx); 63 | assert.ownInclude(ctx.result[0], { id: 1, name: 'Tom', sex: 0 }); 64 | }) 65 | }) 66 | describe('filterFor', () => { 67 | let ctx, schema; 68 | beforeEach(() => { 69 | schema = _schema({ 70 | id: { type: Number, default: null }, 71 | name: { type: String, default: null }, 72 | sex: { type: Number, default: 0 }, 73 | }); 74 | }) 75 | it('默认过滤ctx.result的字段', async () => { 76 | ctx = { 77 | method: 'find', 78 | result: { id: 1, name: 'Tom', sex: 0, abc: "111" } 79 | }; 80 | ctx = await filterFor(schema)(ctx); 81 | assert.ownInclude(ctx.result, { id: 1, name: 'Tom', sex: 0 }); 82 | }) 83 | it('支持指定过滤数据的字段', async () => { 84 | ctx = { 85 | method: 'find', 86 | result: { 87 | status: 200, 88 | data: [{ id: 1, name: 'Tom', sex: 0, abc: "111" }] 89 | } 90 | }; 91 | ctx = await respondData()(ctx); 92 | ctx = await filterFor(schema, (ctx, filter)=>{ 93 | ctx.result=ctx.result.map(item=>filter(item, ['id','name','sex'])) 94 | })(ctx); 95 | assert.ownInclude(ctx.result[0], { id: 1, name: 'Tom', sex: 0 }); 96 | }) 97 | }) 98 | }) -------------------------------------------------------------------------------- /src/schema.js: -------------------------------------------------------------------------------- 1 | function value(val){ 2 | if(typeof val === 'function') return val() 3 | return val 4 | } 5 | const isDef = val=>typeof val != "undefined" 6 | 7 | export function serialize(fieldSet){ 8 | return Object.keys(fieldSet).reduce((result,field)=>({...result, [field]:value(fieldSet[field].default)}), {}) 9 | } 10 | export function format(data, fieldSet){ 11 | const _defaults = serialize(fieldSet) 12 | return { 13 | ..._defaults, 14 | ...Object.keys(data).reduce((result, field)=>{ 15 | const fieldVal = isDef(fieldSet[field]) ? fieldSet[field].type(data[field]) : data[field] 16 | return {...result, [field] : isDef(data[field]) ? fieldVal : _defaults[field] } 17 | }, {}) 18 | } 19 | } 20 | export function filter(data, fields){ 21 | return fields.reduce((result, field)=>({...result, [field]:data[field]}), {}) 22 | } 23 | 24 | /** 25 | * 数据模型,记录并提供数据格式化操作 26 | * 27 | * @param {*} [fieldSet={}] - 字段设定 28 | * @property {String[]} fields - 字段名称列表 29 | * @property {Object} fieldSet - 字段设定 30 | * @class 31 | * 32 | * @example 33 | * import datagent from "datagent" 34 | * const userSchema = datagent.schema({ 35 | * id: { type: Number, default: null }, 36 | * username: { type: String, default: "" }, 37 | * nickname: { type: String, default: "" } 38 | * }) 39 | * 40 | * console.log(userSchema.fields) 41 | * // ['id', 'username', 'nickname'] 42 | */ 43 | function Schema(fieldSet={}){ 44 | fieldSet = {...fieldSet} 45 | const _fields = Object.keys(fieldSet) 46 | 47 | const context = { 48 | /** 49 | * 获得初次化的数据 50 | * @memberof Schema 51 | * @return {Object} 52 | * 53 | * @example 54 | * const user = userSchema.serialize() 55 | * console.log(user) 56 | * // { id: null, username: "", nickname: "" } 57 | */ 58 | serialize: ()=>serialize(fieldSet), 59 | /** 60 | * 格式化数据,根据字段类型的设定转义数据 61 | * @memberof Schema 62 | * @param {Object} data - 原数据 63 | * @return {Object} 格式化后数据 64 | * 65 | * @example 66 | * const user = userSchema.format({ id: "12345", username: "PackyTang", nickname: "packy" }) 67 | * console.log(user) 68 | * // Id converted to numeric 69 | * // { id: 12345, username: "PackyTang", nickname: "packy" } 70 | */ 71 | format: data=>format(data, fieldSet), 72 | /** 73 | * 过滤字段,移除所有未指定的字段数据 74 | * @memberof Schema 75 | * @param {Object} data - 原数据 76 | * @param {String[]} fields - 保留字段的列表 77 | * @return {Object} 过滤后数据 78 | * 79 | * @example 80 | * const user = userSchema.filter({ id: "12345", username: "PackyTang", nickname: "packy" }, ['id','username']) 81 | * console.log(user) 82 | * // { id: "12345", username:"PackyTang" } 83 | */ 84 | filter: (data, fields=_fields)=>filter(data, fields) 85 | } 86 | 87 | Object.defineProperties( 88 | context, 89 | { 90 | "fields":{ 91 | get(){ 92 | return _fields 93 | } 94 | }, 95 | "fieldSet":{ 96 | get(){ 97 | return fieldSet 98 | } 99 | } 100 | } 101 | ) 102 | 103 | return Object.freeze(context) 104 | } 105 | 106 | const factory = (fieldSet={}) => new Schema(fieldSet) 107 | export const constructor = Schema 108 | export default factory -------------------------------------------------------------------------------- /src/operations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 钩子 3 | * 4 | * @module hooks 5 | */ 6 | 7 | /** 8 | * 用于钩子的方法,提取返回的结果。从`respond`中提取`data`内容传至下一个方法。 9 | * 10 | * @memberof hooks 11 | * @export respondData 12 | * @returns 13 | * 14 | * @example 15 | * import axios from 'axios' 16 | * import datagent from "datagent" 17 | * const { respondData } = datagent.hooks 18 | * 19 | * const contact = datagent.contact({ 20 | * base: axios.create({ baseURL: 'localhost/api' }) 21 | * }) 22 | * 23 | * const userModel = datagent.model({ 24 | * name: 'user', 25 | * contact, 26 | * hooks: { 27 | * fetch: method=>[method(), respondData()] 28 | * } 29 | * }) 30 | * userModel.fetch().then(data=>console.log) 31 | * // [GET] localhost/api/user 32 | * // respond => { status: 200, data: [{id:1, name:'Tony'},{id:2, name:'Ben'}] } 33 | * // fetch => [{id:1, name:'Tony'},{id:2, name:'Ben'}] 34 | */ 35 | export function respondData(){ 36 | return ctx=>{ 37 | const res = ctx.result; 38 | if(res.status < 200){ 39 | const err = new Error(res.message); 40 | err.response = res.response; 41 | throw err 42 | }; 43 | ctx.result = res.data; 44 | return Promise.resolve(ctx); 45 | } 46 | } 47 | 48 | /** 49 | * 用于钩子的方法,格式化数据。 50 | * 51 | * @memberof hooks 52 | * @export formatFor 53 | * @param {Schema} schema 54 | * @param {Function} [handle=(ctx, format)=>ctx.result=format(ctx.result)] 55 | * @returns 56 | * 57 | * @example 58 | * import axios from 'axios' 59 | * import datagent from "datagent" 60 | * const { formatFor } = datagent.hooks 61 | * 62 | * const contact = datagent.contact({ 63 | * base: axios.create({ baseURL: 'localhost/api' }) 64 | * }) 65 | * 66 | * const userSchema = datagent.schema({ 67 | * id: { type: String, default: null }, 68 | * sex: { type: Number, default: 0 } 69 | * }) 70 | * 71 | * const userModel = datagent.model({ 72 | * name: 'user', 73 | * contact, 74 | * hooks: { 75 | * fetch: method=>[method(), formatFor(userSchema, (ctx, format)=>ctx.result=ctx.result.data.map(format))] 76 | * } 77 | * }) 78 | * userModel.fetch().then(data=>console.log) 79 | * // [GET] localhost/api/user 80 | * // respond => { status: 200, data: [{id:1, name:'Tony'},{id:2, name:'Ben'}] } 81 | * // fetch => [{id:'1', name:'Tony'},{id:'2', name:'Ben'}] 82 | */ 83 | export function formatFor(schema, handle=(ctx, format)=>ctx.result=format(ctx.result)){ 84 | return ctx=>{ 85 | handle(ctx, schema.format) 86 | return ctx 87 | } 88 | } 89 | 90 | /** 91 | * 用于钩子的方法,过滤对象字段。 92 | * 93 | * @memberof hooks 94 | * @export filterFor 95 | * @param {Schema} schema 96 | * @param {Function} [handle=(ctx, filter)=>ctx.result=filter(ctx.result)] 97 | * @returns 98 | * 99 | * @example 100 | * import axios from 'axios' 101 | * import datagent from "datagent" 102 | * const { filterFor } = datagent.hooks 103 | * 104 | * const contact = datagent.contact({ 105 | * base: axios.create({ baseURL: 'localhost/api' }) 106 | * }) 107 | * 108 | * const userSchema = datagent.schema({ 109 | * id: { type: String, default: null }, 110 | * sex: { type: Number, default: 0 } 111 | * }) 112 | * 113 | * const userModel = datagent.model({ 114 | * name: 'user', 115 | * contact, 116 | * hooks: { 117 | * fetch: method=>[ 118 | * filterFor(userSchema, (ctx, filter)=>{ 119 | * const [data, ...args] = ctx.args 120 | * ctx.args=[filter(data), ...args] 121 | * }), 122 | * method() 123 | * ] 124 | * } 125 | * }) 126 | * userModel.save({ name: 'cathy' }) 127 | * // [POST] localhost/api/user 128 | * // request => { name: 'cathy', sex: 0 } 129 | */ 130 | export function filterFor(schema, handle=(ctx, filter)=>ctx.result=filter(ctx.result)){ 131 | return ctx=>{ 132 | handle(ctx, schema.filter) 133 | return ctx 134 | } 135 | } -------------------------------------------------------------------------------- /test/units/remote.spec.js: -------------------------------------------------------------------------------- 1 | import _remote from '../../src/remote'; 2 | 3 | const handle = res => { 4 | let err, result; 5 | if(res.data.code < 200){ 6 | err = new Error(res.data.msg); 7 | } 8 | result = res.data.data; 9 | return Promise.resolve([err, result]); 10 | }; 11 | 12 | describe('Remote Class Test', function () { 13 | let mock, remote; 14 | let baseURL = 'http://localhost/api'; 15 | before(function () { 16 | remote = _remote(axios.create({ baseURL })); 17 | // init mock XHR 18 | mock = new MockAdapter(remote.origin); 19 | }) 20 | after(function () { 21 | mock.restore(); 22 | }) 23 | 24 | describe('instance.get()', function () { 25 | afterEach(function(){ 26 | mock.reset(); 27 | }) 28 | it('应当发起[GET]请求', async function () { 29 | const data = [ 30 | { id: 1, name: 'John Smith' }, 31 | { id: 2, name: 'Packy Tang' } 32 | ]; 33 | mock.onGet(baseURL + '/users').reply(200, { code: 200, data, msg: '' }); 34 | 35 | let err, result; 36 | [err, result] = await remote.get('/users').then(handle); 37 | assert.sameDeepMembers(result, data); 38 | }) 39 | it('当传入参数时,[GET]请求的路由应当带上传入参数', async function () { 40 | mock 41 | .onGet(baseURL + '/user', { params: { id: 1 } }) 42 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith' }, msg: '' }); 43 | 44 | let err, result; 45 | [err, result] = await remote.get('/user', { id: 1 }).then(handle); 46 | assert.propertyVal(result, 'id', 1 ); 47 | }) 48 | }) 49 | describe('instance.post()', function () { 50 | afterEach(function () { 51 | mock.reset(); 52 | }) 53 | it('应当发起[POST]请求', async function () { 54 | mock 55 | .onPost(baseURL + '/user', { name: 'Cathy Yan' }) 56 | .reply(200, { code: 200, data: { id: 3, name: 'Cathy Yan' }, msg: '' }); 57 | 58 | let err, result; 59 | [err, result] = await remote.post('/user', { name: 'Cathy Yan' }).then(handle); 60 | assert.propertyVal(result, 'id', 3); 61 | }) 62 | }) 63 | describe('instance.put()', function () { 64 | afterEach(function () { 65 | mock.reset(); 66 | }) 67 | it('应当发起[PUT]请求', async function () { 68 | mock 69 | .onPut(baseURL + '/user/3', { id: 3, name: 'Cathy Yang' }) 70 | .reply(200, { code: 200, data: { id: 3, name: 'Cathy Yang' }, msg: '' }); 71 | 72 | let err, result; 73 | [err, result] = await remote.put('/user/3', { id: 3, name: 'Cathy Yang' }).then(handle); 74 | assert.propertyVal(result, 'name', 'Cathy Yang'); 75 | }) 76 | }) 77 | describe('instance.patch()', function () { 78 | afterEach(function () { 79 | mock.reset(); 80 | }) 81 | it('应当发起[PATCH]请求', async function () { 82 | mock 83 | .onPatch(baseURL + '/user/3', { name: 'Cathy Yan' }) 84 | .reply(200, { code: 200, data: { id: 3, name: 'Cathy Yan' }, msg: '' }); 85 | 86 | let err, result; 87 | [err, result] = await remote.patch('/user/3', { name: 'Cathy Yan' }).then(handle); 88 | assert.propertyVal(result, 'name', 'Cathy Yan'); 89 | }) 90 | }) 91 | describe('instance.delete()', function () { 92 | afterEach(function () { 93 | mock.reset(); 94 | }) 95 | it('应当发起[DELETE]请求', async function () { 96 | mock 97 | .onDelete(baseURL + '/user/3') 98 | .reply(200, { code: 200, data: { id: 3, name: 'Cathy Yan' }, msg: '' }); 99 | 100 | let err, result; 101 | [err, result] = await remote.delete('/user/3').then(handle); 102 | assert.propertyVal(result, 'id', 3); 103 | }) 104 | }) 105 | }) 106 | -------------------------------------------------------------------------------- /src/agent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据对象代理,提供方法执行前后事件通知。 3 | * 4 | * @fires Agent#before 5 | * @fires Agent#after 6 | * @fires Agent#error 7 | * @param {Object[]} _models 8 | * @param {Object} options 9 | * @property {Object} models 10 | * @class 11 | * 12 | * @example 13 | * import axios from "axios" 14 | * import datagent from "datagent" 15 | * 16 | * const contact = datagent.contact({ base: axios.create({ baseURL: "http://localhost:8081" }) }) 17 | * const userModel = datagent.model({ name: 'user', contact }) 18 | * const roleModel = datagent.model({ name: 'role', contact }) 19 | * const agent = datagent.agent([userModel, roleModel]) 20 | * 21 | * // 记录加载状态 22 | * const loader = { [userModel.name]:false, [roleModel.name]:false } 23 | * ;(async ()=>{ 24 | * agent.on("error", err=>console.error(err)) 25 | * agent.on("before", ctx=>loader[ctx.model_name] = true) //改为加载中 26 | * agent.on("after", (err, result, ctx)=>loader[ctx.model_name] = false) //改为加载完成 27 | * 28 | * const [err, user_res] = await agent.fetch(userModel.name, { q: "pa" }) 29 | * // request 'http://localhost:8081/user?q=pa' 30 | * // output respond like: { status: 200, data: {...}, headers: {...} } 31 | * const [err, role_res] = await agent.fetch(userModel.name, { id: user_res.data.user.role_id }) 32 | * // request 'http://localhost:8081/role?q=pa' 33 | * // output respond like: { status: 200, data: {...}, headers: {...} } 34 | * }) 35 | */ 36 | function Agent(_models, options){ 37 | const { model_name="name" } = {...options} 38 | const MODEL_NAME = model_name 39 | const models = [].concat(_models).reduce((result, model)=>({...result, [model[MODEL_NAME]]:model}),{}) 40 | 41 | const events = { 42 | error: err=>console.error(err), 43 | before: ctx=>ctx, 44 | after: (err, result, ctx)=>{} 45 | } 46 | function eventWrapper(result, fn, args){ 47 | return fn(...args) || result 48 | } 49 | 50 | /** 51 | * 执行对应的模型对象方法 52 | * 53 | * @param {String} model_name 模型对象名称 54 | * @param {String} action 模型对象方法名 55 | * @param {*} query 传入方法的参数 56 | * @param {*} options 传入方法的设置 57 | * @memberof Agent 58 | * @return {Promise} 59 | */ 60 | function active(model_name, action, query, options){ 61 | options = {...options} 62 | let ctx = {model_name, action, query, options} 63 | /** 64 | * 执行对象方法前的事件 65 | * 66 | * @event Agent#before 67 | * @type {Object} 执行上下文 68 | * @property {String} model_name 模型对象名称 69 | * @property {String} action 模型对象方法名 70 | * @property {*} query 传入方法的参数 71 | * @property {*} options 传入方法的设置 72 | */ 73 | events.before(ctx) 74 | return models[ctx.model_name][ctx.action](ctx.query, ctx.options) 75 | .then(data=>[null, data], err=>([err])) 76 | .then(([err, result])=>{ 77 | 78 | /** 79 | * 执行对象方法后出错的事件 80 | * 81 | * @event Agent#error 82 | * @type {Error} 错误 83 | */ 84 | if(err) events.error(err) 85 | 86 | /** 87 | * 执行对象方法后的事件 88 | * 89 | * @event Agent#after 90 | * @property {Error} err 错误 91 | * @property {Object} result 返回结果,默认返回的是`respond`对象 92 | * @property {Object} ctx 执行上下文,参考Agent#before中的上下文参数 93 | */ 94 | events.after(err, result, ctx) 95 | return [err, result] 96 | }) 97 | } 98 | 99 | const context = { 100 | //operator 101 | active, 102 | /** 103 | * 获取多条数据 104 | * 105 | * @param {String} model_name 模型对象名称 106 | * @param {*} query 传入方法的参数 107 | * @param {*} options 传入方法的设置 108 | * @memberof Agent 109 | * @return {Promise} 110 | */ 111 | fetch(model_name, query, options){ return active(model_name, "fetch", query, options) }, 112 | 113 | /** 114 | * 获取单条数据 115 | * 116 | * @param {String} model_name 模型对象名称 117 | * @param {Object} query 传入方法的参数 118 | * @param {String} query.id 必须传入id 119 | * @param {*} options 传入方法的设置 120 | * @memberof Agent 121 | * @return {Promise} 122 | */ 123 | find(model_name, query, options){ return active(model_name, "find", query, options) }, 124 | 125 | /** 126 | * 保存数据 127 | * 128 | * 默认发起`[POST]`请求,传入数据如果带上`id`参数将会发起`[PUT]`请求 129 | * 130 | * @param {String} model_name 模型对象名称 131 | * @param {*} query 传入方法的参数 132 | * @param {*} options 传入方法的设置 133 | * @memberof Agent 134 | * @return {Promise} 135 | */ 136 | save(model_name, query, options){ return active(model_name, "save", query, options) }, 137 | 138 | /** 139 | * 销毁数据 140 | * 141 | * @param {String} model_name 模型对象名称 142 | * @param {Object} query 传入方法的参数 143 | * @param {String} query.id 必须传入id 144 | * @param {*} options 传入方法的设置 145 | * @memberof Agent 146 | * @return {Promise} 147 | */ 148 | destroy(model_name, query, options){ return active(model_name, "destroy", query, options) }, 149 | 150 | /** 151 | * 设置事件回调 152 | * 153 | * @param {String} event_name 事件名称 154 | * @param {Function} cb 回调参数 155 | * @memberof Agent 156 | * @return {Promise} 157 | */ 158 | on(event_name, cb){ 159 | if(!events[event_name]) return false 160 | events[event_name] = cb 161 | } 162 | } 163 | 164 | Object.defineProperties(context, { 165 | "models":{ 166 | get(){ 167 | return models 168 | } 169 | } 170 | }) 171 | 172 | return Object.freeze(context) 173 | } 174 | 175 | const factory = (_models, options) => new Agent(_models, options) 176 | export const constructor = Agent 177 | export default factory -------------------------------------------------------------------------------- /docs/TODOList.md: -------------------------------------------------------------------------------- 1 | # TODOList 2 | 3 | - [x] 定义字段默认值处理 4 | - [x] 空值判断的处理 5 | - [x] 定义字段值转义处理 6 | - [x] ~~数据模型的链式调用设计与使用例子~~ 7 | - [x] 数据模型代理的设计及使用例子 8 | - [ ] socket远端的支持与使用 9 | 10 | ## 定义字段默认值处理 11 | 12 | > **没有定义和空值均给与默认值** 13 | 14 | ### 空值判断的处理 15 | 16 | > **null、NaN、undefined均为空值** 17 | > 18 | > `{}`对象,`[]`数组属于存在值的情况均不处理 19 | 20 | ## 定义字段值转义处理 21 | 22 | ```js 23 | // # 1.0.0 行为 24 | export const format = (data, fieldSet) => convert(fieldSet, { format:true })(data); 25 | export const schema = fieldSet => convert(fieldSet, { format: true })({}, Object.keys(fieldSet)); 26 | // convert耦合了默认值与转义处理 27 | ``` 28 | 29 | 1.0.0版本值的转义与默认值处理是耦合一起,当时转义失败处理将给默认值进行处理,新版梳理完整后将会先处理默认值后再转义值内容,转义失败将直接输出错误内容。 30 | 31 | ```js 32 | export const schema = (fieldSet)=>{...} 33 | export const convert = (data, fieldSet)=>{...} 34 | export const format = (data, fieldSet)=>convert({...schema(data)}, fieldSet) 35 | ``` 36 | 37 | ## 数据模型的链式调用设计与使用例子 38 | 39 | ~~为何需要改为链式调用?目前数据交互过程是以发起请求的方式与服务端进行交互的。考虑到需要支持处理socket源的处理时,需要~~ 40 | 41 | 调研了一下发现并不是很需要做成链式调用来支持socket部分的处理,反而要做好Remote部分的设计来支持socket的使用 42 | 43 | ## 数据模型代理的设计及使用例子 44 | 45 | ```js 46 | // 定义远端链接 47 | // utils/contact.js 48 | import axios from "axios" 49 | import { datagent } from "datagent" 50 | export default datagent.contact({base: axios.create()}) 51 | 52 | // 定义数据字段 53 | // models/tags.schema.js 54 | import { datagent } from "datagent" 55 | 56 | function Fen(){} 57 | function Yan(){} 58 | 59 | const ServeSchema = datagent.schema({ 60 | pice: { type: Fen, default: null }, 61 | updated_at: { type: String, default: "" } 62 | }) 63 | const ViewSchema = datagent.schema({ 64 | pice: { type: Yan, default: null }, 65 | updated_at: { type: Date, default: null } 66 | }) 67 | const PaginationSchema = datagent.schema({ 68 | total: { type: Number, default: 0 }, 69 | page: { type: Number, default: 1 }, 70 | data: { type: Array, default: [] }, 71 | }) 72 | 73 | // 设置数据模型处理细节 74 | // models/tags.model.js 75 | import { datagent } from "datagent" 76 | import contact from "utils/contact" 77 | const { respondData, requestHandle, formatFor } = datagent.hooks 78 | 79 | const MODEL_NAME = "tags" 80 | const TagsModel = datagent.model({ 81 | name: MODEL_NAME, 82 | contact, 83 | // or 84 | // url: "/tags", 85 | methods: { 86 | disabled(...args){ 87 | const [params, {origin}] = args 88 | return this.remote(origin).get(`/course_orders`, params); 89 | } 90 | }, 91 | hooks: { 92 | fetch: method=>([method(), respondData(), requestHandle(), formatFor(ViewSchema, (ctx,format)=>ctx.result.data=format(ctx.result.data))]), 93 | find: method=>([method(), respondData(), requestHandle(), formatFor(ViewSchema)]), 94 | save: method=>([formatFor(ServeSchema, (ctx,format)=>ctx.args=format(ctx.args)), method(), respondData(), requestHandle()]), 95 | disabled: method=>([method(), respondData(), requestHandle()]), 96 | } 97 | }) 98 | export default TagsModel 99 | 100 | // 页面代码 101 | // pages/tags.js 102 | import { datagent, asyncTo } from "datagent" 103 | const datagent = datagent.agent([TagsModel]) 104 | const TAGS = TagsModel.name 105 | 106 | const page = { 107 | init(){ 108 | // UI处理 109 | datagent.on('error', err=>this.$$error(err)) 110 | datagent.on('before', ctx=>this.$$loading(ctx.model_name)) 111 | datagent.on('after', (err, result, ctx)=>this.$$loaded(ctx.model_name)) 112 | }, 113 | 114 | // 请求 115 | async refresh(name, query){ 116 | const [err, result] = await asyncTo(datagent.fetch(name, query)) 117 | if(err) return 118 | this.list = result 119 | } 120 | } 121 | ``` 122 | 123 | ### 生成与执行串行函数 124 | 125 | ```js 126 | const options = { action: [...] } 127 | function action(...args){ 128 | const ctx = { scope: this, args, methodName: "action", result: null } 129 | const queues = generate(options.action) //生成 130 | return queues(args, ctx) //执行 131 | } 132 | ``` 133 | 134 | ### 模型方法 135 | 136 | 自带方法包括: 137 | 138 | - fetch 139 | - find 140 | - save 141 | - destroy 142 | 143 | 自定义方法需要放置在`methods`的设定里 144 | 145 | ### 钩子 146 | 147 | 钩子处理之前用设置的方式出入进行处理,必定存在方法请求前后的问题这次改版将取消这种设定。钩子改为提供方法执行时进行设定,并把执行方法作为钩子参数传入,在返回串行函数。使用这种方式更能灵活自由地组合钩子处理的函数。 148 | 149 | #### 存在的问题 150 | 151 | 这样改版后存在些问题:没有`before/after`钩子后数据处理的对象无法判断。before处理的是方法传入的数据(`params`),而after处理的是核心方法返回后的结果(`result`)。 152 | 153 | 基于这个问题,可能钩子处理的函数需要改为提供`指定数据字段`的处理函数,像下面这样使用: 154 | 155 | ```js 156 | function Fen(){} 157 | function Yan(){} 158 | 159 | const { respondData, requestHandle, formatFor } = datagent.hooks 160 | const ServeSchema = datagent.schema({ 161 | pice: { type: Fen, default: null }, 162 | updated_at: { type: String, default: "" } 163 | }) 164 | const ViewSchema = datagent.schema({ 165 | pice: { type: Yan, default: null }, 166 | updated_at: { type: Date, default: null } 167 | }) 168 | const PaginationSchema = datagent.schema({ 169 | total: { type: Number, default: 0 }, 170 | page: { type: Number, default: 1 }, 171 | data: { type: Array, default: [] }, 172 | }) 173 | 174 | // //只执行不处理返回值,然后默认返回ctx上下文 175 | // format(ViewSchema, (ctx,format)=>ctx.result.data=format(ctx.result.data)) 176 | // format(PaginationSchema, (ctx,format)=>ctx.result=format(ctx.result)) 177 | // format(ViewSchema, (ctx,format)=>ctx.result=format(ctx.result)) 178 | // format(ServeSchema, (ctx,format)=>ctx.args=format(ctx.args)) 179 | 180 | ... 181 | hooks: { 182 | //针对返回数据结构再指定处理的数据规则 183 | fetch: method=>[method(), respondData(), requestHandle(), formatFor(ViewSchema, (ctx,format)=>ctx.result.data=format(ctx.result.data))], 184 | //默认处理ctx.result的结果字段 185 | find: method=>[method(), respondData(), requestHandle(), formatFor(ViewSchema, /**defaultFun:(ctx,format)=>ctx.result=format(ctx.result)**/)], 186 | //当需要指定处理哪个字段时,在外部提供指定规则 187 | save: method=>[formatFor(ServeSchema, (ctx,format)=>ctx.args=format(ctx.args)), method(), respondData(), requestHandle()], 188 | disabled: method=>[method(), respondData(), requestHandle()], 189 | } 190 | ... 191 | ``` 192 | 193 | `(ctx,format)=>ctx.result=format(ctx.result)`执行方法不处理返回内容,默认返回ctx上下文。 194 | 195 | 数据字段定义由于获得数据后的处理和发送给服务的处理有可能并不是一样的,目前将在`model`废除`fields`设置,数据处理将在`hooks`公开声明处理。 196 | 197 | -------------------------------------------------------------------------------- /src/model.js: -------------------------------------------------------------------------------- 1 | import { existError, isDef, isString, isArray, isFunction } from "./utils/" 2 | import * as queue from "./queue" 3 | 4 | export const isNew = data => !isDef(data.id) || !data.id 5 | export const getURI = (id, url, emulateIdKey) => emulateIdKey ? url : (url + ((isDef(id) && !!id) ? `/${id}` : '')) 6 | 7 | export const restful = { 8 | /** 9 | * 获取数据列表 10 | * 11 | * @memberof Model 12 | * @param {Object} params 请求参数,可选 13 | * @param {Object} opts 请求设置,可选 14 | * @param {String} opts.origin 请求的远端名称 15 | * @param {Object} ctx 不用设置 16 | * @returns {Promise} 17 | */ 18 | fetch(params, opts, ctx) { 19 | const { origin } = {...opts} 20 | const { contact, url } = ctx.options 21 | return contact.remote(origin).get(url, params) 22 | }, 23 | 24 | /** 25 | * 获取单个数据,根据id取得数据 26 | * 27 | * @memberof Model 28 | * @param {Object} params 请求参数,可选 29 | * @param {Object} opts 请求设置,可选 30 | * @param {String} opts.origin 请求的远端名称 31 | * @param {Object} ctx 不用设置 32 | * @returns {Promise} 33 | */ 34 | find(params, opts, ctx) { 35 | const { origin } = {...opts} 36 | const { contact, getURL, emulateIdKey } = ctx.options 37 | const { id } = params 38 | if (emulateIdKey) params[emulateIdKey] = params.id 39 | else delete params.id 40 | return contact.remote(origin).get(getURL(id), params ) 41 | }, 42 | 43 | /** 44 | * 储存数据,同步数据至远端,根据数据对象是否包含`id`进行新增或更新操作。 45 | * 46 | * @memberof Model 47 | * @param {Object} data 发送数据,必须 48 | * @param {Object} opts 请求设置,可选 49 | * @param {String} opts.origin 请求的远端名称 50 | * @param {Object} ctx 不用设置 51 | * @returns {Promise} 52 | */ 53 | save(data, opts, ctx) { 54 | const { origin } = {...opts} 55 | const { contact, getURL, isNew } = ctx.options 56 | const { id } = data 57 | const _url = getURL(id) 58 | const method = isNew(data) ? 'post' : 'put' 59 | return contact.remote(origin)[method](_url, data) 60 | }, 61 | 62 | /** 63 | * 删除数据,根据id通知远端销毁数据。别名方法`delete()` 64 | * 65 | * @memberof Model 66 | * @param {Object} params 请求参数,可选 67 | * @param {Object} opts 请求设置,可选 68 | * @param {String} opts.origin 请求的远端名称 69 | * @param {Object} ctx 不用设置 70 | * @returns {Promise} 71 | */ 72 | destroy(params, opts, ctx) { 73 | const { origin } = {...opts} 74 | const { contact, getURL, emulateIdKey } = ctx.options 75 | const { id } = params 76 | if (emulateIdKey) params[emulateIdKey] = params.id 77 | else delete params.id 78 | return contact.remote(origin).delete(getURL(id), params) 79 | }, 80 | delete(...args){ 81 | return this.destroy(...args) 82 | } 83 | } 84 | 85 | /** 86 | * 数据对象 87 | * 88 | * @param {Object} options 89 | * @param {String} options.name 对象名称; 必需,用于拼接请求地址 90 | * @param {String?} options.url 请求地址; 可选,用于发起请求 91 | * @param {Contact} options.contact 链接管理器; 必需,用于发起请求 92 | * @param {Object} options.methods 对象方法集合; 可选,设置对象的自定义方法 93 | * @param {Object} options.hooks 对象方法的钩子集合; 可选,设置对象方法的钩子处理 94 | * @param {Boolean} options.emulateIdKey 默认为`false`; 可选,默认行为是不将`id`作为请求数据发送并将`id`拼接至请求地址,如:`http://localhost/user/10422` 95 | * @property {String} name 对象名称;可更改 96 | * @property {String} url 请求地址;可更改 97 | * @property {Contact} contact 链接管理器;可更改 98 | * @returns 99 | * 100 | * @example 101 | * import axios from "axios" 102 | * import datagent from "datagent" 103 | * const { respondData } = datagent.hooks 104 | * 105 | * const contact = datagent.contact({ base: axios.create({ baseURL: "http://localhost:8081" }) }) 106 | * const userModel = datagent.model({ 107 | * name: 'user', 108 | * url: 'users', 109 | * contact, 110 | * methods: { 111 | * // 自定义方法,向服务端发送`[PATCH]`请求,禁用用户 112 | * disabled(data, opts, ctx){ 113 | * // 最全的处理方法(推荐) 114 | * const { origin } = {...opts} 115 | * const { contact, url, getURL, emulateIdKey, isNew } = ctx.options 116 | * const { id } = data 117 | * const _url = getURL(id, url, emulateIdKey) 118 | * return contact.remote(origin).patch(_url, {...data, disabled: true}) 119 | * }, 120 | * enabled(data, opts){ 121 | * //简单的处理方法 122 | * const { origin } = {...opts} 123 | * const { id } = data 124 | * return this.contact.remote(origin).patch(this.getURL(id), {id, disabled: 1}) 125 | * } 126 | * }, 127 | * hooks: { 128 | * fetch: method=>[method(), respondData()], 129 | * find: method=>[method(), respondData()], 130 | * save: method=>[method(), respondData()], 131 | * destroy: method=>[method(), respondData()], 132 | * disabled: method=>[method(), respondData()] 133 | * } 134 | * }) 135 | * 136 | * //对象名称 137 | * console.log(userModel.name) 138 | * // output: user 139 | * 140 | * //获得用户数据列表 141 | * console.log(userModel.fetch({q: 'pa'})) 142 | * // request: [GET]'http://localhost:8081/users?q=pa' 143 | * // respond: { status: 200, data: [{id:1, username:'packy'}], headers: {...} } 144 | * // output: [{id:1, username:'packy'}] 145 | * 146 | * //屏蔽用户 147 | * console.log(userModel.disabled({id:20})) 148 | * // request: [PATCH]'http://localhost:8081/users/20' 149 | */ 150 | function Model(options){ 151 | let { name, url, contact, methods={}, hooks={}, emulateIdKey=false } = { ...options } 152 | existError(val=>(isDef(val) && isString(val)), new Error('options.name must be string in Model'))(name) 153 | existError(isDef, new Error('options.contact must be Contact class in Model'))(contact) 154 | const _url = isDef(url) ? url : `/${name}` 155 | 156 | const getURL = (opts=>(id, url=opts.url, emulateIdKey=opts.emulateIdKey)=>getURI(id, url, emulateIdKey))({url:_url, emulateIdKey}) 157 | const context = { 158 | getURL 159 | } 160 | 161 | methods = { ...restful, ...methods } 162 | Object.keys(methods).forEach(methodName=>{ 163 | let method = queue.wrap(methods[methodName]) 164 | let hook = hooks[methodName] 165 | context[methodName] = (...args)=>{ 166 | /** 167 | * 钩子需要传入method的方法 168 | * 存入至钩子的method方法需要包裹成串行方法可执行的方法 169 | * 170 | * method方法执行传入的第二个参数opts需要提供model自带的一些参数如:contact, url, getURL, emulateIdKey 171 | * 提供model自带的参数的目的在于不提供this调取内部变量和函数,已传入的方式提供 172 | * @ignore 173 | * **/ 174 | const options = { contact, url:_url, getURL, emulateIdKey, isNew } 175 | const ctx = queue.context({ scope: context, method: methodName, options }) 176 | // 先判断是否有钩子函数,有执行并获得方法串数组,没有则提供只有当前方法的数组 177 | const method_queue = hook ? hook(()=>method) : [method] 178 | existError(val=>isArray(val)&&val.every(fn=>isFunction(fn)), new Error("hook return result must be Array in Model"))(method_queue) 179 | // 串联方法组并生成可执行方法 180 | const method_exec = queue.generate(method_queue) 181 | // 执行串联后的方法 182 | const _args = new Array(3) 183 | args.forEach((arg,index)=>_args.splice(index, 1, arg)) 184 | _args.splice(2, 1, ctx) 185 | return method_exec(_args, ctx) 186 | } 187 | }) 188 | 189 | 190 | Object.defineProperties(context, { 191 | "name":{ 192 | get(){ 193 | return name 194 | }, 195 | set(val){ 196 | name = val 197 | } 198 | }, 199 | "url":{ 200 | get(){ 201 | return _url 202 | }, 203 | set(val){ 204 | url = val 205 | } 206 | }, 207 | "contact": { 208 | get(){ 209 | return contact 210 | }, 211 | set(val){ 212 | contact = val 213 | } 214 | } 215 | }) 216 | 217 | return Object.freeze(context) 218 | } 219 | 220 | 221 | const factory = options => new Model(options) 222 | export const constructor = Model 223 | export default factory -------------------------------------------------------------------------------- /docs/1.0/README.md: -------------------------------------------------------------------------------- 1 | # Datagent 2 | 3 | [![npm version](https://img.shields.io/npm/v/datagent.svg)](https://www.npmjs.com/package/datagent) 4 | [![build status](https://travis-ci.org/lpreterite/datagent.svg?branch=master)](https://travis-ci.org/lpreterite/datagent) 5 | [![NPM downloads](http://img.shields.io/npm/dm/datagent.svg)](https://www.npmjs.com/package/datagent) 6 | 7 | `Datagent`是一个用于前端Ajax请求的模块化工具,提供字段定义,方法扩展,切换源等功能。在如React, Vue, Angular等现代前端框架下不同UI层面通信的数据我们称为视图模型(ViewModel)。现在互联网常用于客户端与服务器间通信都是基于RESTful方式设计的持久化服务,这种基于持久化的设计可以借助`Datagent`将通信数据表示为数据模型(DataModel)。数据模型管理着数据字段和通信服务,同时为编排业务代码提供相关方法的钩子进行预处理或后处理。 8 | 9 | > 你可以马上尝试在`codepen`上的[例子](https://codepen.io/packy1980/pen/OEpNWW/)。 10 | 11 | ## 安装 12 | 13 | npm 14 | 15 | ```sh 16 | npm install datagent 17 | ``` 18 | 19 | yarn 20 | 21 | ```sh 22 | yarn add datagent 23 | ``` 24 | 25 | ## 远端、链接、数据模型 26 | 27 | 这三种定义的关系就如标题一样,链接和数据模型都是建立在远端之上。远端可以是一个服务,而链接管理着远端,在数据模型需要操作数据时就必须使用链接取得远端才能完成通信。 28 | 29 | 以下是三种常用类: 30 | 31 | - 远端 Remote 32 | - 链接 Contact 33 | - 数据模型 DataModel 34 | 35 | 用`Datagent`快速创建一个包含远端的链接: 36 | 37 | ```js 38 | import axios from "axios" 39 | import Datagent from "datagent" 40 | 41 | const contact = Datagent.Contact({ 42 | base: axios.create({ baseURL: '/api' }) 43 | }) 44 | ``` 45 | 46 | 数据模型实例化时把链接作为参数传入: 47 | 48 | ```js 49 | const UserModel = Datagent.Model({ name: 'user' }) 50 | const $user = new UserModel({ contact }) 51 | ``` 52 | 53 | 尝试请求数据: 54 | 55 | ```js 56 | $user.find({id:1}).then(data=>{ 57 | // [GET] /api/user 58 | // => { status:200, data: { id:1, nickname:'Tony' } } 59 | console.log(data); 60 | }) 61 | ``` 62 | 63 | ## 数据模型 64 | 65 | ```js 66 | // api/user.js 67 | import axios from "axios" 68 | import { HOST } from "./config" 69 | 70 | export function getUsers(params){ 71 | return axios.get(`${HOST}/api/user`, { params }) 72 | } 73 | export function getUserById(id){ 74 | return axios.get(`${HOST}/api/user/${id}`) 75 | } 76 | export function createUser(data){ 77 | return axios.post(`${HOST}/api/user`, data) 78 | } 79 | export function updateUser(data){ 80 | return axios.put(`${HOST}/api/user/${data.id}`, data) 81 | } 82 | export function deleteUserById(id){ 83 | return axios.delete(`${HOST}/api/user/${data.id}`) 84 | } 85 | ``` 86 | 87 | 上面是常见的基于`Restful`接口代码,随着项目持续发展这类代码只会越来越多。当需要修改时不能直观知道接口请求参数和返回数据,则不是那么容易降低出错的可能,这将会带来较多的麻烦。使用`Datagent`定义数据模型能减少较多的手写代码冗余。 88 | 89 | ```js 90 | // models/user.model.js 91 | import Datagent from "datagent" 92 | const UserModel = Datagent.Model({ name: "user" }) 93 | ``` 94 | 95 | ## 定义字段 96 | 97 | 字段是数据模型的有效描述。在数据模型中定义的字段,虽然在请求(或提交)数据时并不会进行过滤或格式化,这钟功能将在钩子以可选的方式提供设置。 98 | 99 | 其次的作用在于为你的项目提高可读性,为接口部分提供更多的描述。一目了然的代码使同事能更快的加入项目。 100 | 101 | ```js 102 | // models/user.model.js 103 | import Datagent from "datagent" 104 | const UserModel = Datagent.Model({ 105 | name: "user", 106 | fields: { 107 | id: { type: Number, default: null }, 108 | name: { type: String, default: '' }, 109 | disabled: { type: String, default: '' }, 110 | } 111 | }) 112 | ``` 113 | 114 | ## 模型的使用 115 | 116 | `Datagent`通讯部分是基于`Restful`的方式,数据模型则提供统一的处理数据的方法: 117 | 118 | - fetch:请求多份数据,发送`[GET] /user`请求。 119 | - find:根据`id`请求单份数据,发送`[GET] /user/:id`请求。 120 | - save:根据`id`提交数据。`id`不存在则新增数据,发送`[POST] /user`请求;`id`存在则更新数据,发送`[PUT] /user`请求。 121 | - destroy:根据`id`请求销毁数据,发送`[DELETE] /user/:id`请求。 122 | 123 | ```js 124 | // test.js 125 | import contact from "contact" 126 | 127 | const $user = new UserModel({ contact }) 128 | 129 | $user.fetch({ disabled: 0 }).then(users=>{ 130 | // [GET] /api/user?disabled=0 131 | // => { status: 200, data: [] } 132 | console.log(users) 133 | }) 134 | 135 | $user.find({id:1}).then(user=>{ 136 | // [GET] /api/user/1 137 | // => { status: 200, data: { id:1, name:"Tony", disabled: 1 } } 138 | console.log(user) 139 | }) 140 | 141 | $user.save({ name:"Ben", disabled: 0 }).then(res=>{ 142 | // [POST] /api/user | { name:"Ben", disabled: 0 } 143 | // => { status: 200, data: { id:2, name:"Ben", disabled: 0 } } 144 | console.log(res) 145 | }) 146 | 147 | $user.save({ id:1, name:"Tony", disabled: 1 }).then(res=>{ 148 | // [PUT] /api/user/1 | { id:1, name:"Tony", disabled: 1 } 149 | // => { status: 200, data: { id:1, name:"Tony", disabled: 1 } } 150 | console.log(res) 151 | }) 152 | 153 | // 发送带id的DELETE请求 154 | $user.destroy({id:2}).then(res=>{ 155 | // [DELETE] /api/user/2 156 | // => { status: 200, data: { id:2, name:"Ben", disabled: 0 } } 157 | console.log(res) 158 | }) 159 | ``` 160 | 161 | 除了自带方法之外,还可以根据业务的需要添加方法。 162 | 163 | ```js 164 | // models/user.model.js 165 | import Datagent from "datagent" 166 | const UserModel = Datagent.Model({ 167 | name: "user", 168 | fields: { 169 | id: { type: Number, default: null }, 170 | name: { type: String, default: '' }, 171 | disabled: { type: String, default: '' }, 172 | }, 173 | methods: { 174 | login(account){ 175 | return this.remote().post(this._url + '/login', account) 176 | }, 177 | logout(){ 178 | return this.remote().get(this._url + '/logout') 179 | } 180 | } 181 | }) 182 | 183 | const $user = new UserModel() 184 | $user.login({ email: 'tony1990@qq.com', password: '******' }).then(res=>{ 185 | // [POST] /api/user/login | { email: 'tony1990@qq.com', password: '******' } 186 | // => { status: 200 } 187 | }) 188 | $user.logout().then(res=>{ 189 | // [GET] /api/user/logout 190 | // => { status: 200 } 191 | }) 192 | ``` 193 | 194 | 模型实例提供`remote()`方法获得远端服务,再使用远端服务发送信息至服务器登录,关于远端的使用可以参看[API参考 - Remote](./API.md#Remote) 195 | 196 | ## 钩子 197 | 198 | 钩子是数据模型的方法使用前后的预设方法,用于处理数据的一种设计。 199 | 200 | 目前钩子只有两种:`before`, `after`。 201 | 202 | 钩子能在定义数据模型时设置: 203 | 204 | ```js 205 | import axios from 'axios' 206 | import Datagent from "datagent" 207 | const { respondData } = Datagent.Hooks 208 | 209 | const contact = Datagent.Contact({ 210 | base: axios.create({ baseURL: 'localhost/api' }) 211 | }) 212 | 213 | const UserModel = Datagent.Model({ 214 | name: 'user', 215 | hooks: { 216 | fetch: { after:[respondData()] } 217 | } 218 | }) 219 | const $user = new UserModel({ contact }) 220 | $user.fetch().then(data=>console.log) 221 | // [GET] localhost/api/user 222 | // respond => { status: 200, data: [{id:1, name:'Tony'},{id:2, name:'Ben'}] } 223 | // respondData => [{id:1, name:'Tony'},{id:2, name:'Ben'}] 224 | ``` 225 | 226 | 也能在调用方法时进行设置: 227 | 228 | ```js 229 | $user.fetch({}, { 230 | hooks:{ after: [ respondData() ] } 231 | }).then(data=>console.log) 232 | // [GET] localhost/api/user 233 | // respond => { status: 200, data: [{id:1, name:'Tony'},{id:2, name:'Ben'}] } 234 | // respondData => [{id:1, name:'Tony'},{id:2, name:'Ben'}] 235 | ``` 236 | 237 | `respondData`方法为我们把返回的`resquest.data`抽出来,再传递至下一个方法。钩子支持设置`fetch`, `find`, `save`, `delete`等模型自身或定义的方法,让一些业务代码或者额外的处理写在方法调用前,达到减少冗余代码的目的。 238 | 239 | 目前`Datagent`提供了以下一些钩子处理的方法: 240 | 241 | - [`respondData()`](./API.md#respondData) 242 | - [`format()`](./API.md#format) 243 | - [`formatFor()`](./API.md#formatFor) 244 | - [`filter()`](./API.md#filter) 245 | - [`filterFor()`](./API.md#filterFor) 246 | 247 | 钩子方法并不多,需要各位多提供意见及时完善满足更多需求,暂时没能满足你需要的欢迎在这里提issue。 248 | 249 | 有时候一些处理需在所有钩子生效,比如以下状况: 250 | 251 | ```js 252 | import Datagent from "datagent"; 253 | const Model = Datagent.Model({ 254 | hooks: { 255 | fetch: { after: [respondData(), format()]}, 256 | find: { after: [respondData(), format()]}, 257 | save: { before: [format()], after: [respondData()]}, 258 | delete: {after: [respondData()]}, 259 | publish: {after: [respondData()]} 260 | } 261 | } 262 | ``` 263 | 264 | 这种例子较为常见,这里提供两个处理函数方便钩子的设置: 265 | 266 | ```js 267 | import Datagent from "datagent"; 268 | const Model = Datagent.Model({ 269 | hooks: { 270 | // 只设置发送数据前的钩子,save:before 271 | ...Datagent.mapSendHook([format()]), 272 | // 设置接收数据后的钩子,包括:fetch:after, find:after 273 | ...Datagent.mapReceiveHook([respondData(), requestHandle(), format()]) 274 | } 275 | } 276 | ``` 277 | 278 | 经过上面的例子相信对`Datagent`的使用有了一定的兴趣。`Datagent`提供的数据模型还有字段、方法、钩子等功能在后面再一一细说。如果想了解得更多,可以阅读[API参考](./API.md)或源代码。 -------------------------------------------------------------------------------- /test/units/model.spec.js: -------------------------------------------------------------------------------- 1 | import datagent from '../../src/'; 2 | import { awaitTo } from '../../src/utils/'; 3 | import { formatFor, respondData } from '../../src/operations'; 4 | 5 | const parseData = ()=> ctx => { 6 | let result; 7 | if (ctx.result.code < 200) { 8 | throw new Error(ctx.result.msg); 9 | } 10 | result = ctx.result.data; 11 | ctx.result = result 12 | return Promise.resolve(ctx); 13 | }; 14 | 15 | describe('Model Test', function () { 16 | let mock, hosts, contact, model, userSchema; 17 | before(function () { 18 | hosts = { 19 | base: 'http://localhost/api', 20 | test: 'http://localhost:8081/api' 21 | }; 22 | 23 | contact = datagent.contact({ 24 | base: axios.create({ baseURL: hosts.base }), 25 | test: axios.create({ baseURL: hosts.test }) 26 | }); 27 | 28 | userSchema = datagent.schema({ 29 | id: { type: Number, default: 0 }, 30 | nickname: { type: String, default: '' }, 31 | sex: { type: Number, default: '1' }, 32 | create_at: { type: String, default: Date.now() }, 33 | disabled: { type: Number, default: 0 } 34 | }) 35 | 36 | model = datagent.model({ 37 | name: 'user', 38 | url: '/users', 39 | contact, 40 | methods: { 41 | ban(id, opts) { 42 | const { origin } = {...opts} 43 | // return this.save({ id, disabled: 1 }, opts); 44 | return this.contact.remote(origin).patch(this.getURL(id), {id, disabled: 1}) 45 | }, 46 | errorTest() { 47 | throw new Error('just a bug'); 48 | } 49 | }, 50 | hooks: { 51 | fetch:method=>[method(),respondData(), parseData()], 52 | find:method=>[method(),respondData(), parseData()], 53 | save:method=>[method(),respondData(), parseData()], 54 | destroy:method=>[method(),respondData(), parseData()], 55 | } 56 | }); 57 | 58 | // init mock XHR 59 | mock = { 60 | base: new MockAdapter(contact.remote('base').origin), 61 | test: new MockAdapter(contact.remote('test').origin) 62 | } 63 | }) 64 | after(function () { 65 | mock.base.restore(); 66 | mock.test.restore(); 67 | }) 68 | 69 | describe('instance.fetch()', function () { 70 | afterEach(function () { 71 | mock.base.reset(); 72 | mock.test.reset(); 73 | }) 74 | it('应当发起[GET]请求', async function () { 75 | const data = [ 76 | { id: 1, name: 'John Smith' }, 77 | { id: 2, name: 'Packy Tang' } 78 | ]; 79 | mock 80 | .base 81 | .onGet(hosts.base + '/users') 82 | .reply(200, { code: 200, data, msg: '' }); 83 | 84 | let err, result; 85 | [err, result] = await awaitTo(model.fetch()) 86 | assert.sameDeepMembers(result, data); 87 | }) 88 | it('当传入参数时,[GET]请求的路由应当带上传入参数', async function () { 89 | mock 90 | .base 91 | .onGet(hosts.base + '/users', { params: { q: 'John' } }) 92 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith' }, msg: '' }); 93 | 94 | let err, result; 95 | [err, result] = await awaitTo(model.fetch({ q: 'John' })) 96 | assert.propertyVal(result, 'id', 1); 97 | }) 98 | it('当传入origin参数时,应当切换至对应的远端服务', async function () { 99 | mock 100 | .test 101 | .onGet(hosts.test + '/users', { params: { q: 'John' } }) 102 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith' }, msg: '' }); 103 | 104 | let err, result; 105 | [err, result] = await awaitTo(model.fetch({ q: 'John' }, { origin: 'test' })) 106 | assert.propertyVal(result, 'id', 1); 107 | }) 108 | }) 109 | describe('instance.find()', function () { 110 | afterEach(function () { 111 | mock.base.reset(); 112 | mock.test.reset(); 113 | }) 114 | it('应当发起带有id路由的[GET]请求', async function () { 115 | mock 116 | .base 117 | .onGet(hosts.base + '/users/1') 118 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith' }, msg: '' }); 119 | 120 | let err, result; 121 | [err, result] = await awaitTo(model.find({id: 1})) 122 | assert.propertyVal(result, 'id', 1); 123 | }) 124 | it('当传入origin参数时,应当切换至对应的远端服务', async function () { 125 | mock 126 | .test 127 | .onGet(hosts.test + '/users/1') 128 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith' }, msg: '' }); 129 | 130 | let err, result; 131 | [err, result] = await awaitTo(model.find({id: 1}, { origin: 'test' })) 132 | assert.propertyVal(result, 'id', 1); 133 | }) 134 | }) 135 | describe('instance.save()', function () { 136 | afterEach(function () { 137 | mock.base.reset(); 138 | mock.test.reset(); 139 | }) 140 | it('当数据不存在id字段时,应当发送[POST]请求新增对象', async function () { 141 | mock 142 | .base 143 | .onPost(hosts.base + '/users', { name: 'Cathy Yan' }) 144 | .reply(200, { code: 200, data: { id: 3, name: 'Cathy Yan' }, msg: '' }); 145 | 146 | let err, result; 147 | [err, result] = await awaitTo(model.save({ name: 'Cathy Yan' })) 148 | assert.propertyVal(result, 'id', 3); 149 | }) 150 | it('当id字段为空字符时,应当发送[POST]请求新增对象', async function () { 151 | mock 152 | .base 153 | .onPost(hosts.base + '/users', { name: 'Cathy Yan' }) 154 | .reply(200, { code: 200, data: { id: 3, name: 'Cathy Yan' }, msg: '' }); 155 | 156 | let err, result; 157 | [err, result] = await awaitTo(model.save({ id: undefined, name: 'Cathy Yan' })) 158 | assert.propertyVal(result, 'id', 3); 159 | }) 160 | it('当数据包含id字段时,应当发送[PUT]请求更新对象', async function () { 161 | mock 162 | .base 163 | .onPut(hosts.base + '/users/3', { id: 3, name: 'Cathy Yan' }) 164 | .reply(200, { code: 200, data: { id: 3, name: 'Cathy Yan' }, msg: '' }); 165 | 166 | let err, result; 167 | [err, result] = await awaitTo(model.save({ id: 3, name: 'Cathy Yan' })) 168 | assert.propertyVal(result, 'id', 3); 169 | }) 170 | }) 171 | describe('instance.destroy()', function () { 172 | afterEach(function () { 173 | mock.base.reset(); 174 | mock.test.reset(); 175 | }) 176 | it('应当发起带有id路由的[DELETE]请求', async function () { 177 | mock 178 | .base 179 | .onDelete(hosts.base + '/users/1') 180 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith' }, msg: '' }); 181 | 182 | let err, result; 183 | [err, result] = await awaitTo(model.destroy({id:1})) 184 | assert.propertyVal(result, 'id', 1); 185 | }) 186 | it('当传入origin参数时,应当切换至对应的远端服务', async function () { 187 | mock 188 | .test 189 | .onDelete(hosts.test + '/users/1') 190 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith' }, msg: '' }); 191 | 192 | let err, result; 193 | [err, result] = await awaitTo(model.destroy({id:1}, { origin: 'test' })) 194 | assert.propertyVal(result, 'id', 1); 195 | }) 196 | }) 197 | 198 | describe('一些额外情况', function () { 199 | it('方法内发生错误时应当报错', async function () { 200 | let err, result; 201 | try { 202 | [err, result] = await model.errorTest({}); 203 | } catch (e) { 204 | assert.equal("just a bug", e.message); 205 | } 206 | }) 207 | it('支持自定义方法', async function () { 208 | mock 209 | .base 210 | .onPatch(hosts.base + '/users/1', { id: 1, disabled: 1 }) 211 | .reply(200, { code: 200, data: { id: 1, name: 'John Smith', disabled: 1 }, msg: '' }); 212 | 213 | let err, result; 214 | [err, result] = await awaitTo(model.ban(1)) 215 | assert.propertyVal(result.data.data, 'disabled', 1); 216 | mock.base.reset(); 217 | }) 218 | it('Model类方法钩子应当生效', async function () { 219 | model = datagent.model({ 220 | name: 'user', 221 | url: '/users', 222 | contact, 223 | fields: { 224 | id: { type: Number, default: 0 }, 225 | nickname: { type: String, default: '' }, 226 | sex: { type: Number, default: '1' }, 227 | create_at: { type: String, default: Date.now() }, 228 | disabled: { type: Number, default: 0 } 229 | }, 230 | methods: { 231 | typeahead(q, opts) { 232 | return this.fetch({ q }, opts); 233 | } 234 | }, 235 | hooks: { 236 | fetch(method){ 237 | return [ 238 | method(), 239 | (ctx) => { 240 | const result = ctx.result; 241 | if (result.data.code < 200) return; 242 | result.data.data = result.data.data.map((item, index)=>{ 243 | item.order=index; 244 | return item; 245 | }); 246 | return Promise.resolve(ctx); 247 | }, 248 | respondData(), 249 | parseData() 250 | ] 251 | } 252 | } 253 | }); 254 | 255 | mock 256 | .base 257 | .onGet(hosts.base + '/users', { params: { q: 'John' } }) 258 | .reply(200, { code: 200, data: [{ id: 1, name: 'John Smith' }], msg: '' }); 259 | 260 | let err, result; 261 | [err, result] = await awaitTo(model.typeahead('John')) 262 | assert.propertyVal(result[0], 'order', 0); 263 | mock.base.reset(); 264 | }) 265 | it('自定义方法的钩子应当生效', async function(){ 266 | model = datagent.model({ 267 | name: 'user', 268 | url: '/users', 269 | contact, 270 | fields: { 271 | id: { type: Number, default: 0 }, 272 | nickname: { type: String, default: '' }, 273 | sex: { type: Number, default: '1' }, 274 | create_at: { type: String, default: Date.now() }, 275 | disabled: { type: Number, default: 0 } 276 | }, 277 | methods: { 278 | typeahead(q, opts) { 279 | return this.fetch({ q }, opts); 280 | } 281 | }, 282 | hooks: { 283 | typeahead(method){ 284 | return [ 285 | (ctx) => { 286 | let [query] = ctx.args; 287 | query = query.q ? query.q : query.keyword; 288 | ctx.args = [query]; 289 | return Promise.resolve(ctx); 290 | }, 291 | method(), 292 | respondData(), 293 | parseData() 294 | ] 295 | } 296 | } 297 | }); 298 | 299 | mock 300 | .base 301 | .onGet(hosts.base + '/users', { params: { q: 'John' } }) 302 | .reply(200, { code: 200, data: [{ id: 1, name: 'John Smith' }], msg: '' }); 303 | 304 | let err, result; 305 | [err, result] = await awaitTo(model.typeahead({ keyword: 'John' })) 306 | assert.propertyVal(result[0], 'id', 1); 307 | mock.base.reset(); 308 | }) 309 | }); 310 | }); -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Datagent 2 | 3 | [![npm version](https://img.shields.io/npm/v/datagent.svg)](https://www.npmjs.com/package/datagent) 4 | [![NPM downloads](http://img.shields.io/npm/dm/datagent.svg)](https://www.npmjs.com/package/datagent) 5 | [![build status](https://travis-ci.org/lpreterite/datagent.svg?branch=master)](https://travis-ci.org/lpreterite/datagent) 6 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Flpreterite%2Fdatagent.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Flpreterite%2Fdatagent?ref=badge_shield) 7 | 8 | `Datagent`是一个用于模块化管理前端请求的工具,提供数据格式化、多服务源切换、语义化数据定义等功能。在 React,Vue,Angular 等现代 JavaScript 框架下,UI 显示均以数据驱动为中心,服务端提供的数据不是所有场合都能符合 UI 所需的结构。格式化数据、转义数据的代码往往不可避免的写在UI组件、业务逻辑代码或是页面等各个地方,导致冗余代码、逻辑复杂又难以维护等问题。面对这类情况可使用`Datagent`解决这类问题,不单单能统一调取后端服务和格式化从服务端获得的数据,定义一些处理后还能用于所有场景,让你更方便同步UI状态。 9 | 10 | ![datagent-run](./assets/images/datagent-run.png) 11 | 12 | > 你可以马上尝试在`codepen`上的[例子](https://codepen.io/packy1980/pen/OEpNWW/)。 13 | 14 | ## 安装 15 | 16 | ```sh 17 | npm install -S datagent 18 | //or 19 | yarn add datagent 20 | ``` 21 | 22 | 目前正式版本为`1.x`,下面是安装`2.0`版本尝尝鲜。 23 | 24 | ```sh 25 | npm install -S datagent@next 26 | // or 27 | yarn add datagent@next 28 | ``` 29 | 30 | ## License 31 | 32 | Datagent是根据[MIT协议](https://github.com/lpreterite/datagent/blob/master/LICENSE)的开源软件 33 | 34 | ## 介绍 35 | 36 | ### 什么是 datagent.js 37 | 38 | 39 | 40 | Datagent 是由`data`与`agent`组合而成的词,意思为数据代理。后端返回的数据有时候是结构上的不同,有时候是字段类型上的不同,前端无法拿起就用需要各种处理。解决方法本很简单,就是每次获得数据后做一遍处理。在日渐增多的系统下,这种处理可能出现在各种地方,维护起来非常吃力。Datagent 的出现是为了解决上面这种情况而诞生,Datagent 关注的是如何管理你的代码,为提高易读性和易维护性而助力。如果你没有一套后端服务的方案管理,不妨试试`Datagent`可能有意想不到的惊喜哦!🙈 41 | 42 | ### 开始 43 | 44 | 45 | 46 | 使用 Datagent 无法马上开箱即用,它需你的适度的了解。了解如何合理地使用,了解什么是远端、链接管理器、数据模型、数据对象、数据对象代理等概念。不用着急,阅读完这篇文档用不着多少分钟,接下来会逐步讲解如何使用。 47 | 48 | ### 管理你的服务 49 | 50 | 51 | 52 | 服务,一般指的是后端服务,前端展示的数据内容大多来自后端服务。在一些项目,后端服务并不只有一个,当需要对接多个的时候代码上都会稍稍有点混乱。下面使用 Datagent 的链接管理器来管理多个服务: 53 | 54 | ```js 55 | // #api 56 | import axios from "axios" 57 | import datagent from "datagent" 58 | export default datagent.contact({ 59 | local: axios.create({ baseURL: "http://localhost" }), 60 | baidu: axios.create({ baseURL: "http://baidu.com" }) 61 | }) 62 | ``` 63 | 64 | 在你需要请求数据时,只需要加载上面的文件进行后续操作: 65 | 66 | ```js 67 | // #user.detail.vue 68 | import api from "./api" 69 | 70 | export default { 71 | async mounted() { 72 | const res = await api.remote().get(`/user/1`) 73 | if (res.status > 201) throw new Error("http error") 74 | this.detail = res.data 75 | }, 76 | data: { 77 | detail: {} 78 | } 79 | } 80 | ``` 81 | 82 | ### 定义数据字段 83 | 84 | 数据是软件系统中最主要的内容,有时候在不同模块中描述同一样事物的数据结构是一样的,编码过程中能统一定义这种数据,在维护时就更能从代码中看出这份数据包含哪些内容了。 85 | 86 | ```js 87 | // #user.schema.js 88 | import datagent from "datagent" 89 | export default datagent.schema({ 90 | id: { type: Number, default: null }, 91 | username: { type: String, default: "" }, 92 | role_id: { type: Number, default: null }, 93 | permission: { type: Array, default: [] }, 94 | updated_at: { type: Date, default: null }, 95 | created_at: { type: Date, default: null } 96 | }) 97 | ``` 98 | 99 | 上面是用户数据的数据定义例子,在你 UI 层需要使用默认值时可使用以下代码: 100 | 101 | ```js 102 | // #user.detail.vue 103 | import api from "./api" 104 | import userSchema from "./user.schema" 105 | 106 | export default { 107 | async mounted() { 108 | const res = await api.remote().get(`/user/1`) 109 | if (res.status > 201) throw new Error("http error") 110 | this.detail = res.data 111 | }, 112 | data: { 113 | detail: userSchema.serialize() 114 | } 115 | } 116 | ``` 117 | 118 | ### 数据处理 119 | 120 | 121 | 122 | 在获得后端数据后有时并不能符合 UI 格式,比如获得数据的更新时间数据类型是 String 类型,使用如 iview 的 datapicker 这类组件用户操作后返回的是 Data 类型。 123 | 124 | 对于这种情况可以使用`Datagent`在获得数据后进行转变字段的数据类型,一般设置在数据对象的方法钩子处,进行统一的转换: 125 | 126 | ```js 127 | // #user.model.js 128 | import contact from "./api" 129 | import userSchema from "./user.schema" 130 | import datagent from "datagent" 131 | const { respondData, formatFor } = datagent.hooks 132 | export default datagent.model({ 133 | name: "user", 134 | contact, 135 | hooks: { 136 | find: method => [ 137 | method(), //执行原来的方法,比如当前的方法find 138 | respondData(), //从respond提取返回的结果 139 | formatFor(userSchema) //格式化指定的内容,默认是返回的结果 140 | ] 141 | } 142 | }) 143 | ``` 144 | 145 | 经过上面的设置当你用数据对象的方法请求数据后,就会获得格式化完成的数据: 146 | 147 | ```js 148 | // #user.detail.vue 149 | import userModel from "./user.model" 150 | import userSchema from "./user.schema" 151 | 152 | export default { 153 | async mounted() { 154 | const userData = await userModel.find({ id: 1 }) 155 | this.detail = userData 156 | }, 157 | data: { 158 | detail: userSchema.serialize() 159 | } 160 | } 161 | ``` 162 | 163 | `respond`回来的数据: 164 | 165 | ```json 166 | { 167 | "id": "1", 168 | "username": "packy", 169 | "role_id": "1", 170 | "permission": [], 171 | "updated_at": "2019/11/08 11:45:30", 172 | "created_at": "2018/01/08 01:32:11" 173 | } 174 | ``` 175 | 176 | 页面`detail`获得的数据: 177 | 178 | ```json 179 | { 180 | "id": 1, 181 | "username": "packy", 182 | "role_id": 1, 183 | "permission": [], 184 | "updated_at": "Fri Nov 08 2019 11:45:30 GMT+0800", //typeof Date 185 | "created_at": "Mon Jan 08 2018 01:32:11 GMT+0800" //typeof Date 186 | } 187 | ``` 188 | 189 | ### 统一调用 190 | 191 | 192 | 193 | 在目前常见的 UI 页面设计中,UI 状态离不开加载态。管理 UI 状态是一件麻烦事,如要做到按加载的数据来管理 UI 相应位置的状态便需要在每次请求统一处理。datagent 也提供的工具帮助你解决这种问题: 194 | 195 | ```js 196 | // #user.detail.vue 197 | import datagent from "datagent" 198 | import userModel from "./user.model" 199 | import userSchema from "./user.schema" 200 | const agent = datagent.agent([userModel]) 201 | 202 | export default { 203 | beforeCreate() { 204 | agent.on("error", err => { 205 | alert(err.message) 206 | console.error(err) 207 | }) 208 | agent.on("before", ctx => (this.loading[ctx.name] = true)) 209 | agent.on("after", (err, result, ctx) => (this.loading[ctx.name] = false)) 210 | }, 211 | async mounted() { 212 | const userData = await agent.find(userModel.name, { id: 1 }) 213 | this.detail = userData 214 | }, 215 | data: { 216 | detail: userSchema.serialize(), 217 | loading: { 218 | [userModel.name]: false 219 | } 220 | } 221 | } 222 | ``` 223 | 224 | ## 深入了解 225 | 226 | ### 远端与axios 227 | 228 | 229 | 230 | 远端的设计给了datagent能换不同的Http请求工具。datagent默认支持的axios是前端最常用的http请求工具,当你需要改成其他的请求工具,远端这层的抽象就起到了一个非常好的作用。下面例子用浏览器默认支持的`fetch`替换axios: 231 | 232 | ```js 233 | // Remote.class.js 234 | import fetch from "node-fetch" 235 | import { URLSearchParams } from "url" 236 | 237 | class Remote { 238 | constructor(options){ 239 | const { baseURL, withJson=true } = { ...options } 240 | this._baseURL = baseURL 241 | this._withJson = withJson 242 | } 243 | sync(options){ 244 | let { method, data, body, headers } = options 245 | const url = this._baseURL + options.url 246 | if(this._withJson){ 247 | headers = !!headers ? headers : {} 248 | headers['Content-Type'] = 'application/json' 249 | body = JSON.stringify(data) 250 | }else{ 251 | body = data 252 | } 253 | return fetch(url, { method, body, headers }).then(res=>new Promise((resolve, reject)=>{ 254 | res.json().then(data=>resolve({ 255 | status: res.status, 256 | statusText: res.statusText, 257 | data, 258 | headers: res.headers, 259 | url: res.url 260 | }), reject) 261 | })) 262 | } 263 | get(url, _params={}){ 264 | const params = new URLSearchParams() 265 | Object.keys(_params).forEach(key=>params.append(key, _params[key])) 266 | url += `/${params.toString()}` 267 | return this.sync({ method: "GET", url }) 268 | } 269 | post(url, data){ 270 | return this.sync({ method: "POST", url, data }) 271 | } 272 | put(url, data){ 273 | return this.sync({ method: "PUT", url, data }) 274 | } 275 | patch(url, data){ 276 | return this.sync({ method: "PATCH", url, data }) 277 | } 278 | delete(url, data){ 279 | return this.sync({ method: "DELETE", url, data }) 280 | } 281 | } 282 | export default Remote 283 | ``` 284 | 285 | 数据对象中的方法访问服务时是透过链接管理器进行的,所以最终需要在生成链接管理器时把构造器替换掉,这样请求就不是用`axios`而是用`fetch`: 286 | 287 | ```js 288 | import datagent from "datagent" 289 | import CustomRemote from './Remote.class' 290 | const contact = datagent.contact( 291 | //remote的设定 292 | { 293 | base: { baseURL: 'https://jsonplaceholder.typicode.com' } 294 | }, 295 | //生成时替换为自定义的remote 296 | { 297 | RemoteConstructor: CustomRemote 298 | } 299 | ) 300 | 301 | contact.remote().get('/todos/3').then(res=>{ 302 | console.log(res.data) 303 | }) 304 | ``` 305 | 306 | 输出结果: 307 | 308 | ```json 309 | { 310 | userId: 1, 311 | id: 3, 312 | title: "fugiat veniam minus", 313 | completed: false 314 | } 315 | ``` 316 | 317 | 关于Remote的详情可以查看[API文档](./API.md#remote-1),自定义完整例子参考[仓库测试的例子](../test/examples/custom-remote.test.js) 318 | 319 | ### 自定义字段类型 320 | 321 | 322 | 323 | 数据模型的字段类型除了支持系统的`Array`,`Number`,`String`等类型外,还支持自定义的类型。 324 | 325 | 接下来让我们看看例子: 326 | 327 | ```js 328 | //# Yuan.type.js 329 | export function Yuan(val) { 330 | return (parseInt(val) / 100).toFixed(2) 331 | } 332 | ``` 333 | 334 | 经过沟通知道后端服务返回的商品价格是以分为单位的,前端显示的时候需要对其进行转换,这里我们先自定义字段类型 Yuan(元)。 335 | 336 | ```js 337 | //# good.schema.js 338 | import datagent from "datagent" 339 | import Yuan from "./Yuan.type" 340 | export default datagent.schema({ 341 | id: { type: Number, default: null }, 342 | good_name: { type: String, default: "" }, 343 | good_type: { type: String, default: "" }, 344 | price: { type: Yuan, default: 0 }, 345 | updated_at: { type: Date, default: null }, 346 | created_at: { type: Date, default: null } 347 | }) 348 | ``` 349 | 350 | 然后在商品的模型中将价格的字段类型改为`Yuan`。 351 | 352 | ```js 353 | import goodSchema from "./good.schema" 354 | console.log( 355 | goodSchema.format({ 356 | good_name: "《人月神话》", 357 | good_type: "book", 358 | price: "48000", 359 | updated_at: "Tue Nov 19 2019 14:11:12 GMT+0800", 360 | created_at: "Tue Nov 19 2019 14:11:12 GMT+0800" 361 | }) 362 | ) 363 | ``` 364 | 365 | 下面就是经过数据模型的方法转换后的数据: 366 | 367 | ```json 368 | { 369 | "good_name": "《人月神话》", 370 | "good_type": "book", 371 | "price": "48.00", 372 | "updated_at": "Tue Nov 19 2019 14:11:12 GMT+0800", 373 | "created_at": "Tue Nov 19 2019 14:11:12 GMT+0800" 374 | } 375 | ``` 376 | 377 | 上面例子是将请求数据的价格字段从`分`转变为`元`,用自定义的类型就能满足此类需求。系统提供的类型均是`Function`,所以字段类型只要是`Function`就能支持。 378 | 379 | ### 方法与钩子 380 | 381 | 382 | 383 | ![method-hooks-data](./assets/images/method-hooks-data.png) 384 | 385 | 数据对象的方法执行过程,实际是**串行执行多个函数的过程**。拿`fetch()`作为例子,首先执行内部的`fetch()`从服务端获取数据;然后再执行`respondData()`函数从`respond`提取数据出来(data);最后执行`format()`函数对提取出来的数据进行格式化处理。 386 | 387 | ```js 388 | // #user.model.js 389 | import contact from "./api" 390 | import userSchema from "./user.schema" 391 | import datagent from "datagent" 392 | const { respondData, formatFor } = datagent.hooks 393 | export default datagent.model({ 394 | name: "user", 395 | contact, 396 | hooks: { 397 | fetch: method => [ 398 | // 用怎样的钩子函数,完全可选可控 399 | method(), 400 | respondData(), 401 | formatFor(userSchema) 402 | ] 403 | } 404 | }) 405 | ``` 406 | 407 | `fetch`的钩子设置函数中传入的`method`函数实质为`fetch()`方法,这样就能更灵活地控制它与其他钩子函数间的执行顺序了。 408 | 409 | ### 自定义方法 410 | 411 | 日常100%会遇到需要在`model`内容增加新的方法来实现新的交互,下面就是给`user`增加启用/禁用功能。 412 | 413 | ```js 414 | // #user.model.js 415 | import contact from "./api" 416 | import userSchema from "./user.schema" 417 | import datagent from "datagent" 418 | const { respondData, formatFor } = datagent.hooks 419 | export default datagent.model({ 420 | name: "user", 421 | contact, 422 | methods: { 423 | // 自定义方法,向服务端发送`[PATCH]`请求,禁用用户 424 | disabled(data, opts, ctx){ 425 | // 最全的处理方法(推荐) 426 | const { origin } = {...opts} 427 | const { contact, url, getURL, emulateIdKey, isNew } = ctx.options 428 | const { id } = data 429 | const _url = getURL(id, url, emulateIdKey) 430 | return contact.remote(origin).patch(_url, {...data, disabled: true}) 431 | }, 432 | enabled(data, opts){ 433 | //简单的处理方法 434 | const { origin } = {...opts} 435 | const { id } = data 436 | return this.contact.remote(origin).patch(this.getURL(id), {id, disabled: 1}) 437 | } 438 | }, 439 | hooks: { 440 | fetch: method => [ 441 | // 用怎样的钩子函数,完全可选可控 442 | method(), 443 | respondData(), 444 | formatFor(userSchema) 445 | ] 446 | } 447 | }) 448 | ``` 449 | 450 | ### 自定义钩子 451 | 452 | 453 | 454 | ![queue-input-output-protocol](./assets/images/queue-input-output-protocol.png) 455 | 456 | 钩子函数之间是基于`Promise`和`执行上下文(Context)`两份协议进行通讯。钩子函数接收上下文作为传入的参数,无论处理情况最终都会抛出`Promise`包裹的上下文内容,传给下一个钩子函数就行后续操作。 457 | 458 | `Promise`就不用过多说明,执行上下文包含以下内容: 459 | 460 | | 名称 | 类型 | 必须 | 描述 | 461 | | ------ | ------------ | ---------------- | ------------------------------------------------------ | 462 | | scope | `Object` | 是 | 方法执行的上下文,影响 this 指向 | 463 | | args | `Array` | 是,可以为空数组 | 来自方法的传入参数,在执行方法时决定了存放的参数与数量 | 464 | | method | `String` | 是 | 方法名称,一般是原方法的名称 | 465 | | result | `any` | 否 | 默认为 null,用来存放最终抛出的结果 | 466 | 467 | 如果需要传递更多信息,直接添加至上下文内就可以了,如: 468 | 469 | ```js 470 | export function setUser(user){ 471 | return ctx => { 472 | ...ctx, 473 | user 474 | } 475 | } 476 | ``` 477 | 478 | `respondData`可作为钩子函数的完整参考例子: 479 | 480 | ![hook-function-input-output.png](./assets/images/hook-function-input-output.png) 481 | 482 | ```js 483 | // # datagent/src/operations:35 484 | export function respondData() { 485 | return ctx => { 486 | const res = ctx.result 487 | if (res.status < 200) { 488 | const err = new Error(res.message) 489 | err.response = res.response 490 | throw err 491 | } 492 | ctx.result = res.data 493 | return Promise.resolve(ctx) 494 | } 495 | } 496 | ``` 497 | 498 | 更多例子可看[datagent/src/operations.js](../src/operations.js) 499 | 500 | ## 迁移 501 | 502 | ### 从 1.x 迁移 503 | 504 | [陆续补上,敬请期待] 505 | 506 | #### FAQ 507 | 508 | 509 | 510 | [陆续补上,敬请期待] 511 | 512 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Table of Contents 4 | 5 | - [Agent][1] 6 | - [Parameters][2] 7 | - [Properties][3] 8 | - [Examples][4] 9 | - [active][5] 10 | - [Parameters][6] 11 | - [fetch][7] 12 | - [Parameters][8] 13 | - [find][9] 14 | - [Parameters][10] 15 | - [save][11] 16 | - [Parameters][12] 17 | - [destroy][13] 18 | - [Parameters][14] 19 | - [on][15] 20 | - [Parameters][16] 21 | - [Agent#before][17] 22 | - [Properties][18] 23 | - [Agent#error][19] 24 | - [Agent#after][20] 25 | - [Properties][21] 26 | - [Contact][22] 27 | - [Parameters][23] 28 | - [Examples][24] 29 | - [has][25] 30 | - [Parameters][26] 31 | - [default][27] 32 | - [Parameters][28] 33 | - [remote][29] 34 | - [Parameters][30] 35 | - [Examples][31] 36 | - [Model][32] 37 | - [Parameters][33] 38 | - [Properties][34] 39 | - [Examples][35] 40 | - [fetch][36] 41 | - [Parameters][37] 42 | - [find][38] 43 | - [Parameters][39] 44 | - [save][40] 45 | - [Parameters][41] 46 | - [destroy][42] 47 | - [Parameters][43] 48 | - [hooks][44] 49 | - [respondData][45] 50 | - [Examples][46] 51 | - [formatFor][47] 52 | - [Parameters][48] 53 | - [Examples][49] 54 | - [filterFor][50] 55 | - [Parameters][51] 56 | - [Examples][52] 57 | - [axios][53] 58 | - [axios.config][54] 59 | - [Remote][55] 60 | - [Parameters][56] 61 | - [Properties][57] 62 | - [Examples][58] 63 | - [sync][59] 64 | - [Parameters][60] 65 | - [get][61] 66 | - [Parameters][62] 67 | - [post][63] 68 | - [Parameters][64] 69 | - [put][65] 70 | - [Parameters][66] 71 | - [patch][67] 72 | - [Parameters][68] 73 | - [delete][69] 74 | - [Parameters][70] 75 | - [Schema][71] 76 | - [Parameters][72] 77 | - [Properties][73] 78 | - [Examples][74] 79 | - [serialize][75] 80 | - [Examples][76] 81 | - [format][77] 82 | - [Parameters][78] 83 | - [Examples][79] 84 | - [filter][80] 85 | - [Parameters][81] 86 | - [Examples][82] 87 | 88 | ## Agent 89 | 90 | 数据对象代理,提供方法执行前后事件通知。 91 | 92 | ### Parameters 93 | 94 | - `_models` **[Array][83]<[Object][84]>** 95 | - `options` **[Object][84]** 96 | 97 | ### Properties 98 | 99 | - `models` **[Object][84]** 100 | 101 | ### Examples 102 | 103 | ```javascript 104 | import axios from "axios" 105 | import datagent from "datagent" 106 | 107 | const contact = datagent.contact({ base: axios.create({ baseURL: "http://localhost:8081" }) }) 108 | const userModel = datagent.model({ name: 'user', contact }) 109 | const roleModel = datagent.model({ name: 'role', contact }) 110 | const agent = datagent.agent([userModel, roleModel]) 111 | 112 | // 记录加载状态 113 | const loader = { [userModel.name]:false, [roleModel.name]:false } 114 | ;(async ()=>{ 115 | agent.on("error", err=>console.error(err)) 116 | agent.on("before", ctx=>loader[ctx.model_name] = true) //改为加载中 117 | agent.on("after", (err, result, ctx)=>loader[ctx.model_name] = false) //改为加载完成 118 | 119 | const [err, user_res] = await agent.fetch(userModel.name, { q: "pa" }) 120 | // request 'http://localhost:8081/user?q=pa' 121 | // output respond like: { status: 200, data: {...}, headers: {...} } 122 | const [err, role_res] = await agent.fetch(userModel.name, { id: user_res.data.user.role_id }) 123 | // request 'http://localhost:8081/role?q=pa' 124 | // output respond like: { status: 200, data: {...}, headers: {...} } 125 | }) 126 | ``` 127 | 128 | ### active 129 | 130 | 执行对应的模型对象方法 131 | 132 | #### Parameters 133 | 134 | - `model_name` **[String][85]** 模型对象名称 135 | - `action` **[String][85]** 模型对象方法名 136 | - `query` **any** 传入方法的参数 137 | - `options` **any** 传入方法的设置 138 | 139 | Returns **[Promise][86]** 140 | 141 | ### fetch 142 | 143 | 获取多条数据 144 | 145 | #### Parameters 146 | 147 | - `model_name` **[String][85]** 模型对象名称 148 | - `query` **any** 传入方法的参数 149 | - `options` **any** 传入方法的设置 150 | 151 | Returns **[Promise][86]** 152 | 153 | ### find 154 | 155 | 获取单条数据 156 | 157 | #### Parameters 158 | 159 | - `model_name` **[String][85]** 模型对象名称 160 | - `query` **[Object][84]** 传入方法的参数 161 | - `query.id` **[String][85]** 必须传入id 162 | - `options` **any** 传入方法的设置 163 | 164 | Returns **[Promise][86]** 165 | 166 | ### save 167 | 168 | 保存数据 169 | 170 | 默认发起`[POST]`请求,传入数据如果带上`id`参数将会发起`[PUT]`请求 171 | 172 | #### Parameters 173 | 174 | - `model_name` **[String][85]** 模型对象名称 175 | - `query` **any** 传入方法的参数 176 | - `options` **any** 传入方法的设置 177 | 178 | Returns **[Promise][86]** 179 | 180 | ### destroy 181 | 182 | 销毁数据 183 | 184 | #### Parameters 185 | 186 | - `model_name` **[String][85]** 模型对象名称 187 | - `query` **[Object][84]** 传入方法的参数 188 | - `query.id` **[String][85]** 必须传入id 189 | - `options` **any** 传入方法的设置 190 | 191 | Returns **[Promise][86]** 192 | 193 | ### on 194 | 195 | 设置事件回调 196 | 197 | #### Parameters 198 | 199 | - `event_name` **[String][85]** 事件名称 200 | - `cb` **[Function][87]** 回调参数 201 | 202 | Returns **[Promise][86]** 203 | 204 | ## Agent#before 205 | 206 | 执行对象方法前的事件 207 | 208 | Type: [Object][84] 209 | 210 | ### Properties 211 | 212 | - `model_name` **[String][85]** 模型对象名称 213 | - `action` **[String][85]** 模型对象方法名 214 | - `query` **any** 传入方法的参数 215 | - `options` **any** 传入方法的设置 216 | 217 | ## Agent#error 218 | 219 | 执行对象方法后出错的事件 220 | 221 | Type: [Error][88] 222 | 223 | ## Agent#after 224 | 225 | 执行对象方法后的事件 226 | 227 | ### Properties 228 | 229 | - `err` **[Error][88]** 错误 230 | - `result` **[Object][84]** 返回结果,默认返回的是`respond`对象 231 | - `ctx` **[Object][84]** 执行上下文,参考Agent#before中的上下文参数 232 | 233 | ## Contact 234 | 235 | 链接管理器 236 | 237 | ### Parameters 238 | 239 | - `remotes` **any** (optional, default `{}`) 240 | - `options` 241 | 242 | ### Examples 243 | 244 | ```javascript 245 | import axios from "axios" 246 | import datagent from "datagent" 247 | const contact = datagent.contact({ base: axios.create({ baseURL: 'http://localhost:8081' }) }) 248 | 249 | console.log('contact has test:'+contact.has('test')) 250 | // output: 'contact has test:false' 251 | 252 | // use default remote 253 | console.log(contact.default().get('/user')) 254 | // request 'http://localhost:8081/user' 255 | // output respond like: { status: 200, data: {...}, headers: {...} } 256 | 257 | // set nothing parmas will get default remote with remote function 258 | console.log(contact.remote().get('/user')) 259 | // request 'http://localhost:8081/user' 260 | // output respond like: { status: 200, data: {...}, headers: {...} } 261 | ``` 262 | 263 | ### has 264 | 265 | 判断是否存在远端 266 | 267 | #### Parameters 268 | 269 | - `name` **[String][85]** 远端名称 270 | 271 | Returns **[Boolean][89]** true 存在, false 不存在 272 | 273 | ### default 274 | 275 | 获取或设定默认远端 276 | 277 | #### Parameters 278 | 279 | - `name` **[String][85]?** 远端名称,不传参为获取默认远端 280 | 281 | Returns **[Remote][90]** 返回默认远端 282 | 283 | ### remote 284 | 285 | 获取或设定远端 286 | 287 | #### Parameters 288 | 289 | - `args` **[Array][83]** 290 | - `args[].name` **[String][85]** 远端名称,不传参为获取默认远端 291 | - `args[].remote` **[Remote][90]** 远端,不传参为获取远端 292 | 293 | #### Examples 294 | 295 | ```javascript 296 | //获得默认远端 297 | contact.remote() 298 | //获得远端 299 | contact.remote('base') 300 | //设置远端 301 | contact.remote('test', datagent.remote(axios.create({ baseURL: "http://localhost:8082" }))) 302 | ``` 303 | 304 | Returns **[Remote][90]** 返回默认远端 305 | 306 | ## Model 307 | 308 | 数据对象 309 | 310 | ### Parameters 311 | 312 | - `options` **[Object][84]** 313 | - `options.name` **[String][85]** 对象名称; 必需,用于拼接请求地址 314 | - `options.url` **[String][85]?** 请求地址; 可选,用于发起请求 315 | - `options.contact` **[Contact][91]** 链接管理器; 必需,用于发起请求 316 | - `options.methods` **[Object][84]** 对象方法集合; 可选,设置对象的自定义方法 317 | - `options.hooks` **[Object][84]** 对象方法的钩子集合; 可选,设置对象方法的钩子处理 318 | - `options.emulateIdKey` **[Boolean][89]** 默认为`false`; 可选,默认行为是不将`id`作为请求数据发送并将`id`拼接至请求地址,如:`http://localhost/user/10422` 319 | 320 | ### Properties 321 | 322 | - `name` **[String][85]** 对象名称;可更改 323 | - `url` **[String][85]** 请求地址;可更改 324 | - `contact` **[Contact][91]** 链接管理器;可更改 325 | 326 | ### Examples 327 | 328 | ```javascript 329 | import axios from "axios" 330 | import datagent from "datagent" 331 | const { respondData } = datagent.hooks 332 | 333 | const contact = datagent.contact({ base: axios.create({ baseURL: "http://localhost:8081" }) }) 334 | const userModel = datagent.model({ 335 | name: 'user', 336 | url: 'users', 337 | contact, 338 | methods: { 339 | // 自定义方法,向服务端发送`[PATCH]`请求,屏蔽用户 340 | disabled(data, opts, ctx){ 341 | // 最全的处理方法(推荐) 342 | const { origin } = {...opts} 343 | const { contact, url, getURL, emulateIdKey, isNew } = ctx.options 344 | const { id } = data 345 | const _url = getURL(id, url, emulateIdKey) 346 | return contact.remote(origin).patch(_url, {...data, disabled: true}) 347 | }, 348 | enabled(data, opts){ 349 | //简单的处理方法 350 | const { origin } = {...opts} 351 | const { id } = data 352 | return this.contact.remote(origin).patch(this.getURL(id), {id, disabled: 1}) 353 | } 354 | }, 355 | hooks: { 356 | fetch: method=>[method(), respondData()], 357 | find: method=>[method(), respondData()], 358 | save: method=>[method(), respondData()], 359 | destroy: method=>[method(), respondData()], 360 | disabled: method=>[method(), respondData()] 361 | } 362 | }) 363 | 364 | //对象名称 365 | console.log(userModel.name) 366 | // output: user 367 | 368 | //获得用户数据列表 369 | console.log(userModel.fetch({q: 'pa'})) 370 | // request: [GET]'http://localhost:8081/users?q=pa' 371 | // respond: { status: 200, data: [{id:1, username:'packy'}], headers: {...} } 372 | // output: [{id:1, username:'packy'}] 373 | 374 | //屏蔽用户 375 | console.log(userModel.disabled({id:20})) 376 | // request: [PATCH]'http://localhost:8081/users/20' 377 | ``` 378 | 379 | Returns **any** 380 | 381 | ### fetch 382 | 383 | 获取数据列表 384 | 385 | #### Parameters 386 | 387 | - `params` **[Object][84]** 请求参数,可选 388 | - `opts` **[Object][84]** 请求设置,可选 389 | - `opts.origin` **[String][85]** 请求的远端名称 390 | - `ctx` **[Object][84]** 不用设置 391 | 392 | Returns **[Promise][86]** 393 | 394 | ### find 395 | 396 | 获取单个数据,根据id取得数据 397 | 398 | #### Parameters 399 | 400 | - `params` **[Object][84]** 请求参数,可选 401 | - `opts` **[Object][84]** 请求设置,可选 402 | - `opts.origin` **[String][85]** 请求的远端名称 403 | - `ctx` **[Object][84]** 不用设置 404 | 405 | Returns **[Promise][86]** 406 | 407 | ### save 408 | 409 | 储存数据,同步数据至远端,根据数据对象是否包含`id`进行新增或更新操作。 410 | 411 | #### Parameters 412 | 413 | - `data` **[Object][84]** 发送数据,必须 414 | - `opts` **[Object][84]** 请求设置,可选 415 | - `opts.origin` **[String][85]** 请求的远端名称 416 | - `ctx` **[Object][84]** 不用设置 417 | 418 | Returns **[Promise][86]** 419 | 420 | ### destroy 421 | 422 | 删除数据,根据id通知远端销毁数据。别名方法`delete()` 423 | 424 | #### Parameters 425 | 426 | - `params` **[Object][84]** 请求参数,可选 427 | - `opts` **[Object][84]** 请求设置,可选 428 | - `opts.origin` **[String][85]** 请求的远端名称 429 | - `ctx` **[Object][84]** 不用设置 430 | 431 | Returns **[Promise][86]** 432 | 433 | ## hooks 434 | 435 | 钩子 436 | 437 | ### respondData 438 | 439 | 用于钩子的方法,提取返回的结果。从`respond`中提取`data`内容传至下一个方法。 440 | 441 | #### Examples 442 | 443 | ```javascript 444 | import axios from 'axios' 445 | import datagent from "datagent" 446 | const { respondData } = datagent.hooks 447 | 448 | const contact = datagent.contact({ 449 | base: axios.create({ baseURL: 'localhost/api' }) 450 | }) 451 | 452 | const userModel = datagent.model({ 453 | name: 'user', 454 | contact, 455 | hooks: { 456 | fetch: method=>[method(), respondData()] 457 | } 458 | }) 459 | userModel.fetch().then(data=>console.log) 460 | // [GET] localhost/api/user 461 | // respond => { status: 200, data: [{id:1, name:'Tony'},{id:2, name:'Ben'}] } 462 | // fetch => [{id:1, name:'Tony'},{id:2, name:'Ben'}] 463 | ``` 464 | 465 | Returns **any** 466 | 467 | ### formatFor 468 | 469 | 用于钩子的方法,格式化数据。 470 | 471 | #### Parameters 472 | 473 | - `schema` **[Schema][92]** 474 | - `handle` **[Function][87]** (optional, default `(ctx,format)=>ctx.result=format(ctx.result)`) 475 | 476 | #### Examples 477 | 478 | ```javascript 479 | import axios from 'axios' 480 | import datagent from "datagent" 481 | const { formatFor } = datagent.hooks 482 | 483 | const contact = datagent.contact({ 484 | base: axios.create({ baseURL: 'localhost/api' }) 485 | }) 486 | 487 | const userSchema = datagent.schema({ 488 | id: { type: String, default: null }, 489 | sex: { type: Number, default: 0 } 490 | }) 491 | 492 | const userModel = datagent.model({ 493 | name: 'user', 494 | contact, 495 | hooks: { 496 | fetch: method=>[method(), formatFor(userSchema, (ctx, format)=>ctx.result=ctx.result.data.map(format))] 497 | } 498 | }) 499 | userModel.fetch().then(data=>console.log) 500 | // [GET] localhost/api/user 501 | // respond => { status: 200, data: [{id:1, name:'Tony'},{id:2, name:'Ben'}] } 502 | // fetch => [{id:'1', name:'Tony'},{id:'2', name:'Ben'}] 503 | ``` 504 | 505 | Returns **any** 506 | 507 | ### filterFor 508 | 509 | 用于钩子的方法,过滤对象字段。 510 | 511 | #### Parameters 512 | 513 | - `schema` **[Schema][92]** 514 | - `handle` **[Function][87]** (optional, default `(ctx,filter)=>ctx.result=filter(ctx.result)`) 515 | 516 | #### Examples 517 | 518 | ```javascript 519 | import axios from 'axios' 520 | import datagent from "datagent" 521 | const { filterFor } = datagent.hooks 522 | 523 | const contact = datagent.contact({ 524 | base: axios.create({ baseURL: 'localhost/api' }) 525 | }) 526 | 527 | const userSchema = datagent.schema({ 528 | id: { type: String, default: null }, 529 | sex: { type: Number, default: 0 } 530 | }) 531 | 532 | const userModel = datagent.model({ 533 | name: 'user', 534 | contact, 535 | hooks: { 536 | fetch: method=>[ 537 | filterFor(userSchema, (ctx, filter)=>{ 538 | const [data, ...args] = ctx.args 539 | ctx.args=[filter(data), ...args] 540 | }), 541 | method() 542 | ] 543 | } 544 | }) 545 | userModel.save({ name: 'cathy' }) 546 | // [POST] localhost/api/user 547 | // request => { name: 'cathy', sex: 0 } 548 | ``` 549 | 550 | Returns **any** 551 | 552 | ## axios 553 | 554 | - **See: [https://www.npmjs.com/package/axios][93] 555 | ** 556 | 557 | Promise based HTTP client for the browser and node.js 558 | 559 | ## axios.config 560 | 561 | - **See: [https://www.npmjs.com/package/axios#axios-api][94] 562 | ** 563 | 564 | Requests can be made by passing the relevant config to axios. 565 | 566 | ## Remote 567 | 568 | 远端,一般指后端服务,远端作为记录后端服务的功能节点 569 | 570 | ### Parameters 571 | 572 | - `origin` **[axios][95]** 服务源头,一般指`axios` 573 | 574 | ### Properties 575 | 576 | - `origin` **[axios][95]** 服务源头,一般指`axios` 577 | 578 | ### Examples 579 | 580 | ```javascript 581 | import axios from "axios" 582 | import datagent from "datagent" 583 | const remote = datagent.remote(axios.create({ baseURL: "http://localhost:8081" })) 584 | 585 | remote.get('/user', { q: "pa" }).then(res=>console.log(res)) 586 | // request 'http://localhost:8081/user?q=pa' 587 | // output respond like: { status: 200, data: {...}, headers: {...} } 588 | ``` 589 | 590 | ### sync 591 | 592 | 发起请求 593 | 594 | #### Parameters 595 | 596 | - `options` **[axios.config][96]** 597 | 598 | Returns **[Promise][86]** 599 | 600 | ### get 601 | 602 | 发起GET请求 603 | 604 | #### Parameters 605 | 606 | - `url` **[String][85]** 请求地址 607 | - `params` **any** 请求参数 608 | 609 | Returns **[Promise][86]** 610 | 611 | ### post 612 | 613 | 发起POST请求 614 | 615 | #### Parameters 616 | 617 | - `url` **[String][85]** 请求地址 618 | - `data` **any** 请求参数 619 | 620 | Returns **[Promise][86]** 621 | 622 | ### put 623 | 624 | 发起PUT请求 625 | 626 | #### Parameters 627 | 628 | - `url` **[String][85]** 请求地址 629 | - `data` **any** 请求参数 630 | 631 | Returns **[Promise][86]** 632 | 633 | ### patch 634 | 635 | 发起PATCH请求 636 | 637 | #### Parameters 638 | 639 | - `url` **[String][85]** 请求地址 640 | - `data` **any** 请求参数 641 | 642 | Returns **[Promise][86]** 643 | 644 | ### delete 645 | 646 | 发起DELETE请求 647 | 648 | #### Parameters 649 | 650 | - `url` **[String][85]** 请求地址 651 | - `data` **any** 请求参数 652 | 653 | Returns **[Promise][86]** 654 | 655 | ## Schema 656 | 657 | 数据模型,记录并提供数据格式化操作 658 | 659 | ### Parameters 660 | 661 | - `fieldSet` **any** 字段设定 (optional, default `{}`) 662 | 663 | ### Properties 664 | 665 | - `fields` **[Array][83]<[String][85]>** 字段名称列表 666 | - `fieldSet` **[Object][84]** 字段设定 667 | 668 | ### Examples 669 | 670 | ```javascript 671 | import datagent from "datagent" 672 | const userSchema = datagent.schema({ 673 | id: { type: Number, default: null }, 674 | username: { type: String, default: "" }, 675 | nickname: { type: String, default: "" } 676 | }) 677 | 678 | console.log(userSchema.fields) 679 | // ['id', 'username', 'nickname'] 680 | ``` 681 | 682 | ### serialize 683 | 684 | 获得初次化的数据 685 | 686 | #### Examples 687 | 688 | ```javascript 689 | const user = userSchema.serialize() 690 | console.log(user) 691 | // { id: null, username: "", nickname: "" } 692 | ``` 693 | 694 | Returns **[Object][84]** 695 | 696 | ### format 697 | 698 | 格式化数据,根据字段类型的设定转义数据 699 | 700 | #### Parameters 701 | 702 | - `data` **[Object][84]** 原数据 703 | 704 | #### Examples 705 | 706 | ```javascript 707 | const user = userSchema.format({ id: "12345", username: "PackyTang", nickname: "packy" }) 708 | console.log(user) 709 | // Id converted to numeric 710 | // { id: 12345, username: "PackyTang", nickname: "packy" } 711 | ``` 712 | 713 | Returns **[Object][84]** 格式化后数据 714 | 715 | ### filter 716 | 717 | 过滤字段,移除所有未指定的字段数据 718 | 719 | #### Parameters 720 | 721 | - `data` **[Object][84]** 原数据 722 | - `fields` **[Array][83]<[String][85]>** 保留字段的列表 (optional, default `_fields`) 723 | 724 | #### Examples 725 | 726 | ```javascript 727 | const user = userSchema.filter({ id: "12345", username: "PackyTang", nickname: "packy" }, ['id','username']) 728 | console.log(user) 729 | // { id: "12345", username:"PackyTang" } 730 | ``` 731 | 732 | Returns **[Object][84]** 过滤后数据 733 | 734 | [1]: #agent 735 | 736 | [2]: #parameters 737 | 738 | [3]: #properties 739 | 740 | [4]: #examples 741 | 742 | [5]: #active 743 | 744 | [6]: #parameters-1 745 | 746 | [7]: #fetch 747 | 748 | [8]: #parameters-2 749 | 750 | [9]: #find 751 | 752 | [10]: #parameters-3 753 | 754 | [11]: #save 755 | 756 | [12]: #parameters-4 757 | 758 | [13]: #destroy 759 | 760 | [14]: #parameters-5 761 | 762 | [15]: #on 763 | 764 | [16]: #parameters-6 765 | 766 | [17]: #agentbefore 767 | 768 | [18]: #properties-1 769 | 770 | [19]: #agenterror 771 | 772 | [20]: #agentafter 773 | 774 | [21]: #properties-2 775 | 776 | [22]: #contact 777 | 778 | [23]: #parameters-7 779 | 780 | [24]: #examples-1 781 | 782 | [25]: #has 783 | 784 | [26]: #parameters-8 785 | 786 | [27]: #default 787 | 788 | [28]: #parameters-9 789 | 790 | [29]: #remote 791 | 792 | [30]: #parameters-10 793 | 794 | [31]: #examples-2 795 | 796 | [32]: #model 797 | 798 | [33]: #parameters-11 799 | 800 | [34]: #properties-3 801 | 802 | [35]: #examples-3 803 | 804 | [36]: #fetch-1 805 | 806 | [37]: #parameters-12 807 | 808 | [38]: #find-1 809 | 810 | [39]: #parameters-13 811 | 812 | [40]: #save-1 813 | 814 | [41]: #parameters-14 815 | 816 | [42]: #destroy-1 817 | 818 | [43]: #parameters-15 819 | 820 | [44]: #hooks 821 | 822 | [45]: #responddata 823 | 824 | [46]: #examples-4 825 | 826 | [47]: #formatfor 827 | 828 | [48]: #parameters-16 829 | 830 | [49]: #examples-5 831 | 832 | [50]: #filterfor 833 | 834 | [51]: #parameters-17 835 | 836 | [52]: #examples-6 837 | 838 | [53]: #axios 839 | 840 | [54]: #axiosconfig 841 | 842 | [55]: #remote-1 843 | 844 | [56]: #parameters-18 845 | 846 | [57]: #properties-4 847 | 848 | [58]: #examples-7 849 | 850 | [59]: #sync 851 | 852 | [60]: #parameters-19 853 | 854 | [61]: #get 855 | 856 | [62]: #parameters-20 857 | 858 | [63]: #post 859 | 860 | [64]: #parameters-21 861 | 862 | [65]: #put 863 | 864 | [66]: #parameters-22 865 | 866 | [67]: #patch 867 | 868 | [68]: #parameters-23 869 | 870 | [69]: #delete 871 | 872 | [70]: #parameters-24 873 | 874 | [71]: #schema 875 | 876 | [72]: #parameters-25 877 | 878 | [73]: #properties-5 879 | 880 | [74]: #examples-8 881 | 882 | [75]: #serialize 883 | 884 | [76]: #examples-9 885 | 886 | [77]: #format 887 | 888 | [78]: #parameters-26 889 | 890 | [79]: #examples-10 891 | 892 | [80]: #filter 893 | 894 | [81]: #parameters-27 895 | 896 | [82]: #examples-11 897 | 898 | [83]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array 899 | 900 | [84]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object 901 | 902 | [85]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String 903 | 904 | [86]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise 905 | 906 | [87]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function 907 | 908 | [88]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error 909 | 910 | [89]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean 911 | 912 | [90]: #remote 913 | 914 | [91]: #contact 915 | 916 | [92]: #schema 917 | 918 | [93]: https://www.npmjs.com/package/axios 919 | 920 | [94]: https://www.npmjs.com/package/axios#axios-api 921 | 922 | [95]: #axios 923 | 924 | [96]: #axiosconfig 925 | -------------------------------------------------------------------------------- /docs/1.0/API.md: -------------------------------------------------------------------------------- 1 | # API 参考 2 | 3 | - [Datagent](#datagent) 4 | - [Datagent.Contact()](#datagentcontact) 5 | - [Datagent.Model()](#datagentmodel) 6 | - [Datagent.mapSendHook()](#datagentmapSendHook) 7 | - [Datagent.mapReceiveHook()](#datagentmapReceiveHook) 8 | - [Remote](#remote) 9 | - [remote.origin](#remoteorigin) 10 | - [Contact](#contact) 11 | - [contact.remote()](#contactremote) 12 | - [传入单个参数为取得远端](#传入单个参数为取得远端) 13 | - [传入多个参数可设置远端](#传入多个参数可设置远端) 14 | - [contact.default()](#contactdefault) 15 | - [contact.has()](#contacthas) 16 | - [Model](#model) 17 | - [model.fetch()](#modelfetch) 18 | - [model.find()](#modelfind) 19 | - [model.save()](#modelsave) 20 | - [model.destroy()](#modeldestroy) 21 | - [model.remote()](#modelremote) 22 | - [model.contact](#modelcontact) 23 | - [DataModel](#datamodel) 24 | - [DataModel.schema](#datamodelschema) 25 | - [DataModel实例的方法](#DataModel实例的方法) 26 | - [Schema](#schema) 27 | - [schema.format()](#schemaformat) 28 | - [schema.filter()](#schemafilter) 29 | - [schema.default()](#schemadefault) 30 | - [schema.fieldSet](#schemafieldset) 31 | - [Schema.format()](#schemaformat) 32 | - [Schema.filter()](#schemafilter) 33 | - [Schema.default()](#schemadefault) 34 | - [Operations](#operations) 35 | - [respondData](#responddata) 36 | - [format](#format) 37 | - [formatFor](#formatfor) 38 | - [filter](#filter) 39 | - [filterFor](#filterfor) 40 | - [getField](#getField) 41 | 42 | ## Datagent 43 | 44 | ### Datagent.Contact() 45 | 46 | 快速生成链接(Contact)对象并设置远端(Remote)内容。 47 | 48 | 参数: 49 | 50 | | 字段 | 限制 | 描述 | 51 | |----------|--------------|--------------| 52 | | remotes | 必须, Object | 远端设定 | 53 | | defaults | 可选, String | 设置默认远端 | 54 | 55 | 返回[`Contact`](#Contact) 56 | 57 | ```js 58 | import axios from 'axios' 59 | import Datagent from "datagent" 60 | const { filter } = Datagent.Hooks 61 | 62 | const contact = Datagent.Contact({ 63 | base: axios.create({ baseURL: 'localhost/api' }), 64 | test: axios.create({ baseURL: 'localhost:8880/api' }) 65 | }) 66 | 67 | // [GET] localhost/api/user 68 | // => { status:200, data:[...] } 69 | contact.remote().get('/user').then(res=>console.log) 70 | ``` 71 | 72 | ### Datagent.Model() 73 | 74 | 生成`DataModel`的工厂方法。 75 | 76 | 参数: 77 | 78 | | 字段 | 限制 | 描述 | 79 | |---------|--------------|------| 80 | | options | 可选, Object | | 81 | 82 | options格式: 83 | 84 | | 字段 | 限制 | 描述 | 85 | |---------|---------------|------------------------------------------| 86 | | name | 可选, String | 类名 | 87 | | url | 可选, String | 远端地址,默认为``/${name}`` | 88 | | contact | 可选, Contact | 链接 | 89 | | fields | 可选, Object | 字段设定,格式参考[`Schema`](#Schema) | 90 | | methods | 可选, Object | 方法 | 91 | | hooks | 可选, Object | 钩子,使用参考[`Operations`](#operations) | 92 | 93 | 返回[`DataModel`](#DataModel) 94 | 95 | 一个较为完整的例子: 96 | 97 | ```js 98 | import axios from 'axios' 99 | import Datagent from "datagent" 100 | const { respondData, filter } = Datagent.Hooks 101 | 102 | const contact = Datagent.Contact({ 103 | base: axios.create({ baseURL: 'localhost/api' }) 104 | }) 105 | 106 | const UserModel = Datagent.Model({ 107 | name: 'user', 108 | fields: { 109 | id: { type: Number, default: null }, 110 | name: { type: String, default: '' }, 111 | disabled: { type: String, default: '' }, 112 | }, 113 | methods: { 114 | disable(data){ 115 | return this.remote().post({ ...data, disabled: 1 }) 116 | }, 117 | enable(data){ 118 | return this.remote().post({ ...data, disabled: 0 }) 119 | } 120 | }, 121 | hooks: { 122 | disable: { before:[filter(['id','disabled'])] }, 123 | enable: { before:[filter(['id','disabled'])] } 124 | } 125 | }) 126 | 127 | const $user = new UserModel({ contact }) 128 | 129 | $user.disable({ id:1, name:'Tony' }).then(res=>console.log) 130 | // [POST] localhost/api/user | { id:1, disabled:1 } 131 | // => { status: 200, data: {...} } 132 | 133 | $user.enable({ id:1, name:'Tony' }).then(res=>console.log) 134 | // [POST] localhost/api/user | { id:1, disabled:0 } 135 | // => { status: 200, data: {...} } 136 | ``` 137 | 138 | ## Datagent.mapSendHook() 139 | 140 | 设置发送数据前的钩子(save:before) 141 | 142 | 参数: 143 | 144 | | 字段 | 限制 | 描述 | 145 | |-------|-------------|------------------| 146 | | hooks | 必须, Array | 一堆钩子处理函数 | 147 | 148 | 使用: 149 | 150 | ```js 151 | import Datagent from "datagent"; 152 | const Model = Datagent.Model({ 153 | hooks: { 154 | ...Datagent.mapSendHook([format()]) 155 | } 156 | } 157 | ``` 158 | 159 | ## Datagent.mapReceiveHook() 160 | 161 | 设置接收数据后的钩子,包括:fetch:after, find:after 162 | 163 | 参数: 164 | 165 | | 字段 | 限制 | 描述 | 166 | |-------|-------------|------------------| 167 | | hooks | 必须, Array | 一堆钩子处理函数 | 168 | 169 | 使用: 170 | 171 | ```js 172 | import Datagent from "datagent"; 173 | const Model = Datagent.Model({ 174 | hooks: { 175 | ...Datagent.mapReceiveHook([respondData(), requestHandle(), format()]) 176 | } 177 | } 178 | ``` 179 | 180 | ## Remote 181 | 182 | 初次化参数: 183 | 184 | | 字段 | 限制 | 描述 | 185 | |--------|-------------------------|--------------| 186 | | origin | 必须, `axios`的实例对象 | 远端服务的源 | 187 | 188 | ```js 189 | import axios from 'axios' 190 | import Remote from 'datagent/src/classes/Remote.class' 191 | const remote = new Remote({ origin: axios.create({ baseURL: 'localhost/api' }) }) 192 | ``` 193 | 194 | - `remote.get(url[, params])` 195 | - `remote.post(url, data)` 196 | - `remote.put(url, data)` 197 | - `remote.patch(url, data)` 198 | - `remote.delete(url)` 199 | 200 | 以上跟`axios`提供的方法使用上是一致的。 201 | 202 | - `remote.sync(options)` 203 | 204 | `sync`方法的使用与`axios`是一致的。 205 | 206 | ### remote.origin 207 | 208 | 获取服务源头,一般返回的是`axios`实例对象。 209 | 210 | ## Contact 211 | 212 | ```js 213 | import axios from 'axios' 214 | import Remote from 'datagent/src/classes/Remote.class' 215 | import Contact from 'datagent/src/classes/Contact.class' 216 | const contact = new Contact(); 217 | ``` 218 | 219 | ### contact.remote() 220 | 221 | `contact.remote()`是一个多态方法。 222 | 223 | #### 传入单个参数为取得远端 224 | 225 | | 字段 | 限制 | 描述 | 226 | |------|--------------|---------------------------------------| 227 | | name | 可选, String | 远端名称,默认为空时取得首次设置的远端 | 228 | 229 | ```js 230 | const baseRemote = contact.remote('base') 231 | ``` 232 | 233 | #### 传入多个参数可设置远端 234 | 235 | | 字段 | 限制 | 描述 | 236 | |---------|--------------|-------------------------------------------| 237 | | name | 必须, String | 远端名称 | 238 | | remote | 必须, Remote | 远端 | 239 | | options | 可选, Object | 配置参数,`{ default:'name' }`用于设置默认值 | 240 | 241 | ```js 242 | contact.remote('base', new Remote({ origin: axios.create({ baseURL: 'localhost/api' }) })) 243 | ``` 244 | 245 | ### contact.default() 246 | 247 | 设置链接使用`remote`方法时获得的默认远端。 248 | 249 | 参数: 250 | 251 | | 字段 | 限制 | 描述 | 252 | |------|--------------|----------| 253 | | name | 必须, String | 远端名字 | 254 | 255 | ```js 256 | contact.default('test'); 257 | ``` 258 | 259 | ### contact.has() 260 | 261 | 判断是否存在某名字的远端 262 | 263 | 参数: 264 | 265 | | 字段 | 限制 | 描述 | 266 | |------|--------------|----------| 267 | | name | 必须, String | 远端名字 | 268 | 269 | 返回:布尔值`Boolean` 270 | 271 | ```js 272 | const hasRemote = contact.has('test') 273 | console.log(hasRemote) // false 274 | ``` 275 | 276 | ## Model 277 | 278 | 初次化参数: 279 | 280 | | 字段 | 限制 | 描述 | 281 | |---------|--------------|------| 282 | | options | 可选, Object | | 283 | 284 | options对象字段: 285 | 286 | | 字段 | 限制 | 描述 | 287 | |--------------|------|----------------------------------------------------------------------------------------| 288 | | name | 必须, String | 类名 | 289 | | url | 可选, String | 远端地址,默认为``/${name}`` | 290 | | contact | 必须, Contact | 链接 | 291 | | emulateIdKey | 可选, String | 仿真ID,默认为`false`,当设置值如`id`时会在请求数据是把仿真ID以`query`的方式添加至地址中 | 292 | 293 | ```js 294 | import axios from 'axios' 295 | import Remote from 'datagent/src/classes/Remote.class' 296 | import Contact from 'datagent/src/classes/Contact.class' 297 | import Model from 'datagent/src/classes/Model.class' 298 | 299 | const contact = new Contact(); 300 | contact.remote('base', new Remote({ origin: axios.create({ baseURL: 'localhost/api' }) })) 301 | contact.remote('test', new Remote({ origin: axios.create({ baseURL: 'localhost:8880/api' }) })) 302 | 303 | const model = new Model({ 304 | name: 'user', 305 | contact 306 | }) 307 | ``` 308 | 309 | ### model.fetch() 310 | 311 | 取得数据列表 312 | 313 | 参数: 314 | 315 | | 字段 | 限制 | 描述 | 316 | |---------|--------------|---------------------------------| 317 | | params | 可选, Object | 请求接口参数 | 318 | | options | 可选, Object | 配置,`{ origin }`用于设置访问源 | 319 | 320 | ```js 321 | // [GET] localhost/api/user 322 | // => { status: 200, data:[...] } 323 | model.fetch().then(res=>console.log) 324 | ``` 325 | 326 | 访问不同远端: 327 | 328 | ```js 329 | // [GET] localhost:8880/api/user 330 | // => { status: 200, data:[...] } 331 | model.fetch({}, {origin:'test'}).then(res=>console.log) 332 | ``` 333 | 334 | ### model.find() 335 | 336 | 根据id取得数据 337 | 338 | 参数: 339 | 340 | | 字段 | 限制 | 描述 | 341 | |---------|--------------|---------------------------------| 342 | | params | 必须, Object | 参数对象必须包含id | 343 | | options | 可选, Object | 配置,`{ origin }`用于设置访问源 | 344 | 345 | ```js 346 | // [GET] localhost/api/user/1 347 | // => { status: 200, data:{...} } 348 | model.find({id:1}).then(res=>console.log) 349 | ``` 350 | 351 | ### model.save() 352 | 353 | 同步数据至远端,根据数据对象是否包含`id`进行新增或更新操作。 354 | 355 | 参数: 356 | 357 | | 字段 | 限制 | 描述 | 358 | |---------|--------------|---------------------------------| 359 | | data | 必须, Object | 需同步的模型数据 | 360 | | options | 可选, Object | 配置,`{ origin }`用于设置访问源 | 361 | 362 | 新增数据: 363 | 364 | ```js 365 | // [POST] localhost/api/user/1 | { name: 'Tony' } 366 | // => { status: 200, data:{...} } 367 | model.save({ name: 'Tony' }).then(res=>console.log) 368 | ``` 369 | 370 | 更新数据: 371 | 372 | ```js 373 | // [PUT] localhost/api/user/1 | { id:1, name: 'Tony', disabled: 0 } 374 | // => { status: 200, data:{...} } 375 | model.save({ id:1, name: 'Tony', disabled: 0 }).then(res=>console.log) 376 | ``` 377 | 378 | ### model.destroy() 379 | 380 | 根据id通知远端销毁数据,`delete()`与此方法相同。 381 | 382 | 参数: 383 | 384 | | 字段 | 限制 | 描述 | 385 | |---------|--------------|---------------------------------| 386 | | params | 必须, Object | 参数对象必须包含id | 387 | | options | 可选, Object | 配置,`{ origin }`用于设置访问源 | 388 | 389 | ```js 390 | // [DELETE] localhost/api/user/1 391 | // => { status: 200, data:{...} } 392 | model.destroy({id:1}).then(res=>console.log) 393 | ``` 394 | 395 | ### model.remote() 396 | 397 | 使用方法与`contact.remote()`一致,这里不再详细说明。 398 | 399 | ### model.contact 400 | 401 | 访问链接 402 | 403 | ```js 404 | console.log(model.contact.constructor === Contact) // true 405 | ``` 406 | 407 | ## DataModel 408 | 409 | 继承模型类(Model)并把模型类下的方法进行二次封装实现钩子处理。 410 | 411 | 初始化参数: 412 | 413 | | 字段 | 限制 | 描述 | 414 | |--------------|---------------|----------------------------------------------------------------------------------------| 415 | | name | 可选, String | 类名 | 416 | | url | 可选, String | 远端地址,默认为``/${name}`` | 417 | | contact | 可选, Contact | 链接 | 418 | | emulateIdKey | 可选, String | 仿真ID,默认为`false`,当设置值如`id`时会在请求数据是把仿真ID以`query`的方式添加至地址中 | 419 | 420 | `name`,`url`,`contact`参数在使用`Datagent.Model()`定义时均可以设置,所以在模型初次化时不一定需要提供。 421 | 422 | ```js 423 | import axios from 'axios' 424 | import Datagent from 'datagent' 425 | import Schema from 'datagent/src/classes/Schema.class' 426 | 427 | const contact = Datagent.Contact({ 428 | base: axios.create({ baseURL: 'localhost/api' }) 429 | }) 430 | 431 | const UserModel = Datagent.Model({ name: 'user', contact }) 432 | const $user = new UserModel(); 433 | ``` 434 | 435 | ### DataModel.schema 436 | 437 | `Datagent.Model`方法提供的字段设置,背后其实是生成了一个`Schema`类。 438 | 439 | ```js 440 | console.log(UserModel.schema.constructor === Schema) // true 441 | ``` 442 | 443 | 创建的实例也包含`schema` 444 | 445 | ```js 446 | console.log($user.schema.constructor === Schema) // true 447 | ``` 448 | 449 | ### DataModel实例的方法 450 | 451 | 实例方法进行了一些有趣的封装处理,当调用方法时会先运行预先设置的前置钩子方法等处理完后才会真正调用真实的方法函数,然后再把结果传入后置钩子方法进行后续处理,当处理完才真正完成处理输出结果。 452 | 453 | 运行顺序大概像这样子: 454 | 455 | ```js 456 | //方法队列 457 | [ 458 | f1(), 459 | f2(), 460 | fetch(), 461 | e1(), 462 | e2() 463 | ] 464 | ``` 465 | 466 | 实际处理的方式我称为“折叠方法”,把一个队列的方法顺序运行并得出结果,具体想了解更多可看源码`datagent/src/utls/index.js:14`的`compose`方法。 467 | 468 | 这里只写`fetch`方法作为例子,其他方法使用是一致的。 469 | 470 | 参数: 471 | 472 | | 字段 | 限制 | 描述 | 473 | |---------|--------------|---------------------------------| 474 | | id | 必须, Object | 对象id | 475 | | options | 可选, Object | 配置,`{ origin }`用于设置访问源;`{ hooks }`用于设置一次性钩子,接受包含`before`与`after`字段的对象 | 476 | 477 | 调用时设置一次性钩子: 478 | 479 | ```js 480 | $user.fetch({ keyword:'Ti' }, { 481 | hooks: { 482 | before: [ctx=>{ 483 | const params = ctx.args[0]; 484 | params.q = params.keyword; 485 | delete params.keyword; 486 | return ctx; 487 | }] 488 | } 489 | }).then(res=>console.log) 490 | // [GET] /api/user?q=Ti 491 | // => { status:200, data:[...] } 492 | ``` 493 | 494 | ## Schema 495 | 496 | 初次化参数: 497 | 498 | | 字段 | 限制 | 描述 | 499 | |----------|--------------|---------------------------------------------------------------------------------------| 500 | | fieldSet | 必须, Object | 包含字段名(key)与字段设定(val)的哈希数据,例子:{ id: { type: Number, default: null } } | 501 | 502 | 字段设定(fieldSet)的字段: 503 | 504 | | 字段 | 限制 | 描述 | 505 | |---------|----------------|-------------------------------------------| 506 | | type | 必须, Function | 定义字段类型 | 507 | | default | 可选, any | 定义字段默认值,默认值类型可字段类型不一致 | 508 | 509 | ```js 510 | import Schema from 'datagent/src/classes/Schema.class' 511 | 512 | const schema = new Schema({ 513 | id: { type: Number, default: null }, 514 | name: { type: String, default: '' }, 515 | disabled: { type: Number, default: 0 } 516 | }) 517 | ``` 518 | 519 | ### schema.format() 520 | 521 | 格式化对象 522 | 523 | 参数: 524 | 525 | | 字段 | 限制 | 描述 | 526 | |------|--------------|------------------| 527 | | data | 必须, Object | 需要格式化的对象 | 528 | 529 | ```js 530 | const data = { name: 'Tony' } 531 | const result = schema.format(data) 532 | console.log(result) // { id:null, name:'Tony', disabled: 0 } 533 | ``` 534 | 535 | ### schema.filter() 536 | 537 | 过滤字段 538 | 539 | 参数: 540 | 541 | | 字段 | 限制 | 描述 | 542 | |--------|---------------------|-------------------------------------------------------------| 543 | | data | 必须, Object | 需要处理的对象 | 544 | | fields | 可选, Array | 需要保留的字段名称,默认是初次化时传入的对象所包含的字段列表 | 545 | 546 | ```js 547 | const data = { name: 'Tony', sex: 1, disabled: 0 } 548 | const result = schema.filter(data) 549 | console.log(result) // { name:'Tony', disabled: 0 } 550 | ``` 551 | 552 | 保留给定字段 553 | 554 | ```js 555 | const data = { name: 'Tony', sex: 1, disabled: 0 } 556 | const result = schema.filter(data, ['name','sex']) 557 | console.log(result) // { name:'Tony', sex: 1 } 558 | ``` 559 | 560 | ### schema.default() 561 | 562 | 获取一个包含默认值的对象 563 | 564 | ```js 565 | const defaultData = schema.default(); 566 | console.log(result) // { id:null, name:'', disabled: 0 } 567 | ``` 568 | 569 | ### schema.fieldSet 570 | 571 | 取得field设定 572 | 573 | ```js 574 | console.log(schema.fieldSet); 575 | /* 576 | { 577 | id: { type: Number, default: null }, 578 | name: { type: String, default: '' }, 579 | disabled: { type: Number, default: 0 } 580 | } 581 | */ 582 | ``` 583 | 584 | ### Schema.format() 585 | 586 | 格式化对象,`Schema`的静态方法 587 | 588 | 参数: 589 | 590 | | 字段 | 限制 | 描述 | 591 | |----------|--------------|------------------| 592 | | data | 必须, Object | 需要格式化的对象 | 593 | | fieldSet | 必须, Object | 格式化依据 | 594 | 595 | ```js 596 | const data = { name: 'Tony' } 597 | const result = Schema.format(data) 598 | console.log(result) // { id:null, name:'Tony', disabled: 0 } 599 | ``` 600 | 601 | ### Schema.filter() 602 | 603 | 过滤字段,`Schema`的静态方法 604 | 605 | 参数: 606 | 607 | | 字段 | 限制 | 描述 | 608 | |--------|---------------------|-------------------------------------------------------------| 609 | | data | 必须, Object | 需要格式化的对象 | 610 | | fields | 必须, Array | 需要保留的字段名称,默认是初次化时传入的对象所包含的字段列表 | 611 | 612 | ```js 613 | const data = { name: 'Tony', sex: 1, disabled: 0 } 614 | const result = Schema.filter(data, ['name','sex']) 615 | console.log(result) // { name:'Tony', sex: 1 } 616 | ``` 617 | 618 | ### Schema.default() 619 | 620 | 获取一个包含默认值的对象,`Schema`的静态方法 621 | 622 | ```js 623 | const fieldSet = { 624 | id: { type: Number, default: null }, 625 | name: { type: String, default: '' }, 626 | disabled: { type: Number, default: 0 } 627 | } 628 | const data = { name: 'Tony' } 629 | const result = Schema.default(fieldSet) 630 | console.log(result) // { id:null, name:'Tony', disabled: 0 } 631 | ``` 632 | 633 | ## Operations 634 | 635 | ### respondData 636 | 637 | 用于钩子的方法,提取返回的结果。从`respond`中提取`data`内容传至下一个方法。 638 | 639 | 限制: 640 | 641 | | 钩子 | 是否支持 | 描述 | 642 | |--------|----------|------| 643 | | before | ✘ | | 644 | | after | ✔ | | 645 | 646 | ```js 647 | import axios from 'axios' 648 | import Datagent from "datagent" 649 | const { respondData } = Datagent.Hooks 650 | 651 | const contact = Datagent.Contact({ 652 | base: axios.create({ baseURL: 'localhost/api' }) 653 | }) 654 | 655 | const UserModel = Datagent.Model({ 656 | name: 'user', 657 | contact, 658 | hooks: { 659 | fetch: { after:[respondData()] } 660 | } 661 | }) 662 | const $user = new UserModel() 663 | $user.fetch().then(data=>console.log) 664 | // [GET] localhost/api/user 665 | // respond => { status: 200, data: [{id:1, name:'Tony'},{id:2, name:'Ben'}] } 666 | // respondData => [{id:1, name:'Tony'},{id:2, name:'Ben'}] 667 | ``` 668 | 669 | ### format 670 | 671 | 用于钩子的方法,格式化数据。 672 | 673 | 参数: 674 | 675 | | 字段 | 限制 | 描述 | 676 | |--------|-------------|----------------------------------------| 677 | | schema | 可选,Schema | 默认使用数据模型设定的schema进行格式化 | 678 | 679 | 限制: 680 | 681 | | 钩子 | 是否支持 | 描述 | 682 | |--------|----------|-----------------------------------------------------| 683 | | before | ✔ | 为传入参数格式化 | 684 | | after | ✔ | 为返回结果格式化;返回结果是数组时格式化数组内的对象 | 685 | 686 | ```js 687 | import axios from 'axios' 688 | import Datagent from "datagent" 689 | const { respondData, format } = Datagent.Hooks 690 | 691 | const contact = Datagent.Contact({ 692 | base: axios.create({ baseURL: 'localhost/api' }) 693 | }) 694 | 695 | const UserModel = Datagent.Model({ 696 | name: 'user', 697 | contact, 698 | fields: { 699 | id: { type: Number, default: null }, 700 | name: { type: String, default: '' }, 701 | disabled: { type: String, default: '' }, 702 | }, 703 | hooks: { 704 | find: { after:[respondData(), format()] } 705 | } 706 | }) 707 | const $user = new UserModel() 708 | $user.find({id:1}).then(data=>console.log) 709 | // [GET] localhost/api/user 710 | // respond => { status: 200, data: {id:1, name:'Tony', disabled: 0 } } 711 | // format => {id:1, name:'Tony', disabled:'0'} 712 | ``` 713 | 714 | ### formatFor 715 | 716 | 用于钩子的方法,格式化指定数据。 717 | 718 | 参数: 719 | 720 | | 字段 | 限制 | 描述 | 721 | |--------|-------------|----------------------------------------| 722 | | field | 必须,String | 需格式化的字段名称 | 723 | | schema | 可选,Schema | 默认使用数据模型设定的schema进行格式化 | 724 | 725 | 限制: 726 | 727 | | 钩子 | 是否支持 | 描述 | 728 | |--------|----------|-----------------------------------------------------| 729 | | before | ✔ | 为传入参数格式化 | 730 | | after | ✔ | 为返回结果格式化;返回结果是数组时格式化数组内的对象 | 731 | 732 | ```js 733 | import axios from 'axios' 734 | import Datagent from "datagent" 735 | const { respondData, formatFor } = Datagent.Hooks 736 | 737 | const contact = Datagent.Contact({ 738 | base: axios.create({ baseURL: 'localhost/api' }) 739 | }) 740 | 741 | const RoleModel = Datagent.Model({ 742 | name: 'role', 743 | contact, 744 | fields: { 745 | id: { type: Number, default: null }, 746 | name: { type: String, default: '' }, 747 | disabled: { type: String, default: '' }, 748 | } 749 | }) 750 | 751 | const UserModel = Datagent.Model({ 752 | name: 'user', 753 | contact, 754 | fields: { 755 | id: { type: Number, default: null }, 756 | name: { type: String, default: '' }, 757 | disabled: { type: String, default: '' }, 758 | }, 759 | hooks: { 760 | find: { after:[respondData(), formatFor('role', RoleModel.schema)] } 761 | } 762 | }) 763 | 764 | const $user = new UserModel() 765 | $user.find({id:1}).then(data=>console.log) 766 | // [GET] localhost/api/user 767 | // respond => { status: 200, data: { id:1, name:'Tony', disabled: 0, role: { id: 1, name:'admin', disabled: 0 } } } 768 | // formatFor => { id:1, name:'Tony', disabled: 0, role: { id: 1, name:'admin', disabled: '0' } } 769 | ``` 770 | 771 | ### filter 772 | 773 | 用于钩子的方法,过滤对象字段。 774 | 775 | 参数: 776 | 777 | | 字段 | 限制 | 描述 | 778 | |--------|--------------------|----------------------------| 779 | | fields | 可选,Array | 默认使用数据模型设定的字段 | 780 | 781 | 限制: 782 | 783 | | 钩子 | 是否支持 | 描述 | 784 | |--------|----------|-----------------------------------------------------| 785 | | before | ✔ | 为传入参数过滤字段 | 786 | | after | ✔ | 为返回结果过滤字段;返回结果是数组时过滤字段数组内的对象 | 787 | 788 | ```js 789 | import axios from 'axios' 790 | import Datagent from "datagent" 791 | const { filter } = Datagent.Hooks 792 | 793 | const contact = Datagent.Contact({ 794 | base: axios.create({ baseURL: 'localhost/api' }) 795 | }) 796 | 797 | const UserModel = Datagent.Model({ 798 | name: 'user', 799 | contact, 800 | fields: { 801 | id: { type: Number, default: null }, 802 | name: { type: String, default: '' }, 803 | disabled: { type: String, default: '' }, 804 | }, 805 | hooks: { 806 | save: { before:[filter(['id','disabled'])] } 807 | } 808 | }) 809 | const $user = new UserModel() 810 | const data = { id:1, name:'Tony', disabled: '1' }; 811 | $user.save(data).then(data=>console.log) 812 | // [PUT] localhost/api/user | { id: 1, disabled: '1' } 813 | // => { status: 200, data: {id:1, name:'Tony', disabled: 1 } } 814 | ``` 815 | 816 | ### filterFor 817 | 818 | 用于钩子的方法,过滤指定对象字段。 819 | 820 | 参数: 821 | 822 | | 字段 | 限制 | 描述 | 823 | |--------|--------------------|----------------------------| 824 | | field | 必须,String | 需过滤的字段名称 | 825 | | fields | 可选,Array | 默认使用数据模型设定的字段 | 826 | 827 | 限制: 828 | 829 | | 钩子 | 是否支持 | 描述 | 830 | |--------|----------|-----------------------------------------------------| 831 | | before | ✔ | 为传入参数过滤字段 | 832 | | after | ✔ | 为返回结果过滤字段;返回结果是数组时过滤字段数组内的对象 | 833 | 834 | ```js 835 | import axios from 'axios' 836 | import Datagent from "datagent" 837 | const { filterFor } = Datagent.Hooks 838 | 839 | const contact = Datagent.Contact({ 840 | base: axios.create({ baseURL: 'localhost/api' }) 841 | }) 842 | 843 | const RoleModel = Datagent.Model({ 844 | name: 'role', 845 | contact, 846 | fields: { 847 | id: { type: Number, default: null }, 848 | name: { type: String, default: '' }, 849 | disabled: { type: String, default: '' }, 850 | } 851 | }) 852 | 853 | const UserModel = Datagent.Model({ 854 | name: 'user', 855 | contact, 856 | fields: { 857 | id: { type: Number, default: null }, 858 | name: { type: String, default: '' }, 859 | disabled: { type: String, default: '' }, 860 | }, 861 | hooks: { 862 | save: { before:[filterFor('role', ['id','disabled'])] } 863 | } 864 | }) 865 | 866 | const $user = new UserModel() 867 | const data = { id:1, name:'Tony', disabled: '1', role: { id: 1, name:'admin', disabled: '1' } } 868 | $user.save(data).then(data=>console.log) 869 | // [PUT] localhost/api/user | { id:1, name:'Tony', disabled: '1', role: { id: 1, disabled: '1' } } 870 | // => { status: 200, data: {id:1, name:'Tony', disabled: 1 } } 871 | ``` 872 | 873 | ### getField 874 | 875 | 用于钩子的方法,提取指定字段进行后续操作。 876 | 877 | 参数: 878 | 879 | | 字段 | 限制 | 描述 | 880 | |--------|--------------------|----------------------------| 881 | | field | 必须,String | 需要处理的字段 | 882 | |action|必须, Function| 后续处理的函数,可使用钩子的函数方法:format, filter, formatFor等 | 883 | 884 | 限制: 885 | 886 | | 钩子 | 是否支持 | 描述 | 887 | |--------|----------|-----------------------------------------------------| 888 | | before | ✔ | 为传入参数处理字段 | 889 | | after | ✔ | 为返回结果处理字段 | 890 | 891 | ```js 892 | import axios from 'axios' 893 | import Datagent from "datagent" 894 | const { filter, getField } = Datagent.Hooks 895 | 896 | const contact = Datagent.Contact({ 897 | base: axios.create({ baseURL: 'localhost/api' }) 898 | }) 899 | 900 | const RoleModel = Datagent.Model({ 901 | name: 'role', 902 | contact, 903 | fields: { 904 | id: { type: Number, default: null }, 905 | name: { type: String, default: '' }, 906 | disabled: { type: String, default: '' }, 907 | } 908 | }) 909 | 910 | const UserModel = Datagent.Model({ 911 | name: 'user', 912 | contact, 913 | fields: { 914 | id: { type: Number, default: null }, 915 | name: { type: String, default: '' }, 916 | disabled: { type: String, default: '' }, 917 | }, 918 | hooks: { 919 | save: { before:[getField('role', filter(['id','disabled']))] } 920 | } 921 | }) 922 | 923 | const $user = new UserModel() 924 | const data = { id:1, name:'Tony', disabled: '1', role: { id: 1, name:'admin', disabled: '1' } } 925 | $user.save(data).then(data=>console.log) 926 | // [PUT] localhost/api/user | { id:1, name:'Tony', disabled: '1', role: { id: 1, disabled: '1' } } 927 | // => { status: 200, data: {id:1, name:'Tony', disabled: 1 } } 928 | ``` 929 | --------------------------------------------------------------------------------