├── .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 | [](https://www.npmjs.com/package/datagent)
4 | [](https://www.npmjs.com/package/datagent)
5 | [](https://travis-ci.org/lpreterite/datagent)
6 | [](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 | 
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 | [](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 | [](https://www.npmjs.com/package/datagent)
4 | [](https://travis-ci.org/lpreterite/datagent)
5 | [](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 | [](https://www.npmjs.com/package/datagent)
4 | [](https://www.npmjs.com/package/datagent)
5 | [](https://travis-ci.org/lpreterite/datagent)
6 | [](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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------