npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@fxjs/webx

v1.12.1-postalpha.28

Published

[![Build Status](https://travis-ci.org/fxjs-modules/webx.svg)](https://travis-ci.org/fxjs-modules/webx) [![NPM version](https://img.shields.io/npm/v/@fxjs/webx.svg)](https://www.npmjs.org/package/@fxjs/webx)

Downloads

31

Readme

@fxjs/webx

Build Status NPM version

fibjs webx 应用程序 api/视图和扩展资源框架

Install

npm install @fxjs/webx [--save]

Test

npm test

建立基础脚本

const http = require('http');
const util = require('util')
const Session = require('@fxjs/session')
const App = require('../');

var app = new App('sqlite:test.db', {
  uuid: true
});
app.db.use(require('./defs/person'));

var session = new Session(new util.LruCache(20000), {
  timeout: 60 * 1000
});

var svr = new http.Server(8080, [
  session.cookie_filter,
  {
    '/1.0': app
  }
]);
svr.run();

其中 person 是 Model 定义模块,内容如下:

module.exports = db => {
  db.define('person', {
    name: String,
    sex: ["male", "female"],
    age: Number
  });
};

这是一个标准的 orm 定义,同样可以使用 orm 的其它功能,比如类型检查,事件等。

API 数据格式

对于 POST 和 PUT 请求,请求的主体必须是 JSON 格式,而且 HTTP header 的 Content-Type 需要设置为 application/json。

curl -X PUT \
  -H "Content-Type: application/json" \
  -d '{"name": "tom","sex":"male","age":23}' \
  http://localhost/1.0/person/57fbbdb0a2400000

对于所有的请求,响应格式都是一个 JSON 对象。

一个请求是否成功是由 HTTP 状态码标明的。一个 2XX 的状态码表示成功,而一个 4XX 表示请求失败。当一个请求失败时响应的主体仍然是一个 JSON 对象,但是总是会包含 code 和 message 这两个字段,你可以用它们来进行调试。举个例子,如果一个请求权限认证失败,会返回以下信息:

{
  "code": 4030501,
  "message": "The operation isn’t allowed for clients due to class-level permissions."
}

code 编码分为三个部分,前三位 403 表示错误类型,05 表示数据表编号,01 表示详细错误编码。

对于 GET 请求,通常会返回对象数据,根据 GET 请求的地址不同,可能会返回一个对象,也可能会返回一个数组。比如:

{
  "name": "tom",
  "sex": "male",
  "age": 23
}

或者:

[
  {
    "name": "tom",
    "sex": "male",
    "age": 23
  },
  {
    "name": "lily",
    "sex": "female",
    "age": 22
  }
]

特殊字段

对象数据中,有四个特殊含义的字段,是不允许通过 API 更改的。分别是 id, updatedAt, createdAt, createdBy.

其中 id, updatedAt, createdAt 单个字段会自动创建和修改。createdBy 则需要自行指定类型。

基础对象访问 API

完成这样的数据定义,便直接拥有了一整套符合 REST api 规范的接口调用:

| url | method | action | |---------------------|--------|------------| | /1.0/:className | POST | 创建新对象 | | /1.0/:className/:id | GET | 读取对象 | | /1.0/:className/:id | PUT | 修改对象 | | /1.0/:className/:id | DELETE | 删除对象 | | /1.0/:className | GET | 查询对象列表 |

创建新对象

为了创建一个新的对象,应该向 class 的 URL 发送一个 POST 请求,其中应该包含对象本身。例如,要创建如上所说的对象:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name": "tom","sex":"male","age":23}' \
  http://localhost/1.0/person

当创建成功时,HTTP 的返回是 201 Created,响应的主体是一个 JSON 对象,包含新的对象的 objectId 和 createdAt 时间戳:

{
  "createdAt": "2017-11-25T01:39:35.931Z",
  "id": "57fbbdb0a2400000"
}

读取对象

当你创建了一个对象时,你可以通过发送一个 GET 请求到返回的 header 的 Location 以获取它的内容。例如,为了得到我们上面创建的对象:

curl -X GET http://localhost/1.0/person/57fbbdb0a2400000

返回的主体是一个 JSON 对象包含所有用户提供的 field 加上 createdAtupdatedAtid 字段:

{
  "name": "tom",
  "sex": "male",
  "age": 23,
  "createdAt": "2017-11-25T01:39:35.931Z",
  "updatedAt": "2017-11-25T01:39:35.931Z",
  "id": "57fbbdb0a2400000"
}

通过设置返回字段 keys,可以定制返回的内容,keys 的内容是一个以 , 分割的字段名称字符串,:

curl -X GET http://localhost/1.0/person/57fbbdb0a2400000?keys=name%2Csex

将返回:

{
  "name": "tom",
  "sex": "male"
}

修改对象

为了更改一个对象已经有的数据,你可以发送一个 PUT 请求到对象相应的 URL 上,任何你未指定的 key 都不会更改,所以你可以只更新对象数据的一个子集。例如,我们来更改我们对象的一个 age 字段:

curl -X PUT \
  -H "Content-Type: application/json" \
  -d '{"age": 25}' \
  http://localhost/1.0/person/57fbbdb0a2400000

返回的 JSON 对象会包含 updatedAtid 字段,表明更新发生的时间:

{
  "updatedAt": "2017-11-25T01:39:35.931Z",
  "id": "57fbbdb0a2400000"
}

删除对象

为了删除一个对象,可以发送一个 DELETE 请求到指定的对象的 URL,比如:

curl -X DELETE http://localhost/1.0/person/57fbbdb0a2400000

查询对象列表

通过发送一个 GET 请求到类的 URL 上,不需要任何 URL 参数,你就可以一次获取多个对象。下面就是简单地获取所有用户:

curl -X GET http://localhost/1.0/person

返回的值就是一个 JSON 对象包含了 results 字段,它的值就是对象的列表:

[
  {
    "name": "tom",
    "sex": "male",
    "age": 23,
    "createdAt": "2017-11-25T01:39:35.931Z",
    "updatedAt": "2017-11-25T01:39:35.931Z",
    "id": "57fbbdb0a2400000"
  },
  {
    "name": "lily",
    "sex": "female",
    "age": 22,
    "createdAt": "2017-11-25T01:39:35.931Z",
    "updatedAt": "2017-11-25T01:39:35.931Z",
    "id": "57fbbdb0a2400001"
  }
]

keys 字段定制

与对象查询一样,查询列表时可以通过设定 keys 来定制返回结果所包含的字段。keys 的内容是一个以 , 分割的字段名称字符串,例如:

curl -X GET http://localhost/1.0/person?keys=name%2Cage

将指定只返回 nameage 两个字段。

where 过滤条件

通过 where 参数的形式可以对查询对象做出约束。

where 参数的值应该是 JSON 编码过的。就是说,如果你查看真正被发出的 URL 请求,它应该是先被 JSON 编码过,然后又被 URL 编码过。最简单的使用 where 参数的方式就是包含应有的 key 和 value。例如,如果我们想要搜索名字为 tom 的用户,我们应该这样构造查询:

curl -X GET http://localhost/1.0/person?where=%7B%22name%22%3A%22tom%22%7D

where 的值为一个 urlencode 后的 JSON 字符串,内容为:{"name":"tom"}

除了完全匹配一个给定的值以外,where 也支持比较的方式,比如包含。where 参数支持如下选项:

| key | operation | sample | |--------------|-----------|-----------------------| | eq | 等于 | {"name":{"eq":"tom"}} 或者 {"name":"tom"} | | ne | 不等于 | {"name":{"ne":"tom"}} | | gt | 大于 | {"age":{"gt":"24"}} | | gte | 大于等于 | {"age":{"gte":"24"}} | | lt | 小于 | {"age":{"lt":"24"}} | | lte | 小于等于 | {"age":{"lte":"24"}} | | like | 模糊查询 | {"name":{"like":"%m"}} | | not_like | 模糊查询 | {"name":{"not_like":"%m"}} | | between | 区间比较 | {"age":{"between":[22,25]}} | | not_between | 区间比较 | {"age":{"not_between":[22,25]}} | | in | 枚举 | {"name":{"in":["tom","lily"]}} | | not_in | 枚举 | {"name":{"not_in":["tom","lily"]}} | | or | 或运算 | {"or":[{"name":"tom"},{"age":24}]} |

skip 跳过记录

通过 skip 选项,可以跳过指定的记录数,达到翻页的效果。

curl -X GET http://localhost/1.0/person?skip=100

limit 返回记录限制

通过 limit 选项,可以限制返回记录数,limit 的有效数字为 1-1000,缺省为 100。

curl -X GET http://localhost/1.0/person?limit=100

order 指定排序方式

通过 order 选项,设定返回结果集的排序方式,字段名前包含 - 时为倒序。

curl -X GET http://localhost/1.0/person?order=-id

count 返回结果总数

在请求时增加 count 可以在返回指定内容的同时返回结果集的总数。

curl -X GET http://localhost/1.0/person?count=1&limit=1

此时返回结果将包含 countresults 两个字段,分别包含总数和结果:

{
  "count": 2,
  "results": [
    {
      "name": "tom",
      "sex": "male",
      "age": 23,
      "createdAt": "2017-11-25T01:39:35.931Z",
      "updatedAt": "2017-11-25T01:39:35.931Z",
      "id": "57fbbdb0a2400000"
    }
  ]
}

建立扩展对象

通过 orm 定义 hasOne 和 hasMany,可以定义对象之间的关联关系,并在 API 上体现出来,例如:

module.exports = db => {
  var Person = db.models.person;

  var Pet = db.define('pet', {
    name: String
  });

  Person.hasMany('pets', Pet);
};

扩展对象访问 API

下面是扩展对象的 API 定义:

| url | method | action | |--------------------------------------|--------|---------------| | /1.0/:className/:id/:extendName | PUT | 设置扩展对象 | | /1.0/:className/:id/:extendName | POST | 创建扩展对象 | | /1.0/:className/:id/:extendName/:rid | GET | 读取扩展对象 | | /1.0/:className/:id/:extendName/:rid | PUT | 修改扩展对象 | | /1.0/:className/:id/:extendName/:rid | DELETE | 删除扩展对象 | | /1.0/:className/:id/:extendName | GET | 查询扩展对象列表 |

设置扩展对象

设置扩展对象是将两个独立的对象建立联系。比如 tom 领养了一只叫 cat 的宠物,可以用下面的操作实现:

curl -X PUT \
  -H "Content-Type: application/json" \
  -d '{"id": "57fbbdb0a2400007"}' \
  http://localhost/1.0/person/57fbbdb0a2400000/pets

在调用里需要在 body 内指定 cat 的 id。

创建扩展对象

直接创建扩展对象,可以在创建对象的同时,建立对象之间的联系。比如:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"name": "cat"}' \
  http://localhost/1.0/person/57fbbdb0a2400000/pets

将创建一只名叫 cat 的宠物,并建立与 tom 的关联关系。

读取扩展对象

读取扩展对象与读取基础对象很相似,也同样支持 keys 选项:

curl -X GET http://localhost/1.0/person/57fbbdb0a2400000/pets/57fbbdb0a2400007

修改扩展对象

读取扩展对象与读取基础对象很相似:

curl -X PUT \
  -H "Content-Type: application/json" \
  -d '{"name": "cat 1"}' \
  http://localhost/1.0/person/57fbbdb0a2400000/pets/57fbbdb0a2400007

删除扩展对象

删除扩展对象不会删除对象本身,只会解除对象之间的关系:

curl -X DETELE http://localhost/1.0/person/57fbbdb0a2400000/pets/57fbbdb0a2400007

注意 在设定了 reverse 名的 hasOne 关系中, revesed 的一方无法对发起方做删除操作, @fxjs/orm 底层认为该操作不合理. 尽管 @fxjs/webx 的 ACL/OACL 机制在此场景下可以正确计算出用户预期的 ACL 值, 但限于 @fxjs/orm 的支持不足, 我们无法进行这样的操作.

查询扩展对象列表

查询扩展对象列表与查询基础对象列表很相似,也同样支持 keys 以及条件过滤等选项:

curl -X GET http://localhost/1.0/person/57fbbdb0a2400000/pets

ACL

可以通过定义 Model 的 ACL 控制数据权限。比如:

const orm = require('@fxjs/orm');

module.exports = db => {
  db.define('blog', {
    title: String,
    detail: String,
    note: String
  }, {
    ACL: function(session) {
      return {
        "*": {
          "*": false
        },
        "57fbbdb0a2400000": {
          "*": true
        },
        "roles": {
          "user": {
            "read": true
          }
        }
      };
    }
  });
};

如果定义 Model 时未指定 ACL,则等同于设定了缺省权限:

{
  "*": {
    "*": true
  }
}

主体

ACL 主体描述有三种,用户 id,用户 role*, id 表示一个具体的用户,role 表示具有某个角色的用户,* 表示所有用户:

| 主体 | 描述 | 优先级 | |-------|-------------|-------| | id | 具体用户的 id | 1 | | role | 用户组名 | 2 | | * | 全体 | 3 |

在检查权限时,首先会匹配 id 对应的权限,如果未指定,则匹配用户 role 对应的权限,如果仍为指定,则查看是否指定了 * 的权限,如果 * 也未指定,则没有权限。

比如上面的权限配置,指定了 user 用户组可以阅读,用户 57fbbdb0a2400000 拥有全部权限,而其它用户没有任何权限。

权限

ACL 根据 API 行为将权限分类五种:

| 权限 | 描述 | 允许类型 | |--------|------------|----------------------| | create | 创建对象 | true / false / array | | read | 读取对象 | true / false / array | | write | 修改对象 | true / false / array | | delete | 删除对象 | true / false | | find | 查询对象列表 | true / false | | * | 匹配所有权限 | true / false / array |

权限制定 true 为允许访问,为 false 为禁止访问,为 array 为只允许指定的字段的访问。deletefind 不接受 array,如果设置了 array 则视同为 true。如果指定的权限不存在,则匹配同主体下的 * 权限。若都不存在,再一次查询下一优先级的主体。

比如以上例子,如果需要设定 user 只允许读取 titledetail,其他人可以读取 title,则可以这样设定:

{
  "*": {
    "*": false,
    "read": ['title']
  },
  "57fbbdb0a2400000": {
    "*": true
  },
  "roles": {
    "user": {
      "read": ['title', 'detail']
    }
  }
}

对象权限

在 Model 上设定的是整个类的权限,如果需要对具体的对象设定权限,可以通过设置 OACL 来实现:

module.exports = db => {
  db.define('person', {
    name: String,
    sex: ["male", "female"],
    age: Number
  }, {
    ACL: function(session) {
      return {
        "*": {
          "*": false
        }
      }
    },
    OACL: function(session) {
      var _acl = {};
      if(this.id === session.id)
        _acl[session.id] = {
          "*": true
        };

      return _acl;
    }
  });
};

在这个例子中,当访问者是对象本人时,将被允许全部操作,否则禁止一切访问。会按照以下步骤检查权限:

  • person[57fbbdb0a2400000] => OACL
  • person => ACL

扩展对象权限

扩展对象的访问权限控制和基础对象权限相似,唯一不同的是在 ACL 需要单独指定:

module.exports = db => {
  var Person = db.define('person', {
    name: String,
    sex: ["male", "female"],
    age: Number
  },{
    ACL: function(session) {
      return {
        "*": {
          "read": ['name', 'sex'],
          "extends": {
            "pets": {
              "read": true,
              "find": true
            }
          }
        }
      }
    },
    OACL: function(session) {
      var _acl = {};
      if(this.id === session.id)
        _acl[session.id] = {
          "*": true,
          "extends": {
            "pets": {
              "*": true
            }
          }
        };

      return _acl;
    }
  });

  var Pet = db.define('pet', {
    name: String
  });

  Person.hasMany('pets', Pet);
};

这个定义中,任何人都可以查阅个人信息的 namesex,并自由查阅和搜索他的 pets,用户本人可以操作自己的所有数据,以及拥有自己宠物信息的全部权限。

在检查扩展对象的访问权限时,会分别检查对象权限和扩展对象权限。比如以下请求:

curl -X GET http://localhost/1.0/person/57fbbdb0a2400000/pets/57fbbdb0a2400007

会按照以下步骤检查权限:

  • pets[57fbbdb0a2400007] => OACL
  • person[57fbbdb0a2400000] => OACL => extends => pets
  • person => ACL => extends => pets
  • pets => ACL

Utils

类型

如果要使用 @fxjs/webx 的高级特性, 你需要了解至少以下类型. 更多的类型可参见 @types/app.d.ts

interface FibAppORMModelFunction {
    (req: FibAppReq, data: FibAppReqData): FibAppModelFunctionResponse
}

interface FibAppSetupChainFn {
    (origReq: FibAppHttpRequest, classname: string, func: FibAppORMModelFunction): void;
}

interface FibAppReq {
    session: FibAppSession
    query: FibAppReqQuery
    request?: FibAppHttpRequest
    error?: APPError
}

interface FibAppReqData {
    [key: string]: any;
}

Model Function

可以为 Model 定义 api,对于复杂数据操作, 可以通过自定义 Function 来完成,

绝大多数权限可以通过 ACL 控制完成,不需要通过 Function 来完成基于对象的权限. Function 可用于完成一些复杂的操作,

  • 基于数据的权限, 比如根据审批状态,赋予不同用户组权限
  • 多项修改, 比如需要修改多条数据库记录
  • 基础 Rest 操作的组合
  • 其它任何你认为需要的操作

Model Function 都是 FibAppORMModelFunction 类型, 需通过 POST /:classname/:func 的方式进行调用, 在调用到 func 之前, request 已通过 app.filterRequest 进行过滤.

  • app.diagram

绘制数据模型

在完成数据定义以后,可以使用 app.diagram() 绘制数据模型的 svg 格式类图,保存至文件会得到类似下面的图像: diagram

  • app.dbPool

app.dbapp.dbPool 的别名, 它本质上是一个 fib-pool 对象

  • app.api

使用 app.api 定制高级操作

通过内部方法, 直接进行 rest 风格的操作, 详情可参考 @types/app.d.ts 中的 FibAppInternalApis.

  • app.api.post: FibAppIneternalApiFunction__Post
  • app.api.get: FibAppIneternalApiFunction__Get
  • app.api.find: FibAppIneternalApiFunction__Find
  • app.api.put: FibAppIneternalApiFunction__Put
  • app.api.del: FibAppIneternalApiFunction__Del
  • app.api.eget: FibAppIneternalApiFunction__Eget
  • app.api.efind: FibAppIneternalApiFunction__Efind
  • app.api.epost: FibAppIneternalApiFunction__Epost
  • app.api.eput: FibAppIneternalApiFunction__Eput
  • app.api.edel: FibAppIneternalApiFunction__Edel
  • app.api.elink: FibAppIneternalApiFunction__Elink

如果你对它们的实现感兴趣, 可以参考 src/classes 目录下中的实现, 其中单实体的操作(post, get, find, put, del)在 src/classes/base.ts 中; 对实体的扩展示例的操作(eget, efind, epost, eput, edel, elink)的实现在 src/classes/extend.ts

你可以在 Model Function 中调用 app.api 上的 rest 风格函数, 来定制属于你的函数, 比如

module.exports = db => {
  var Person = db.define('person', {
    name: String,
    sex: ["male", "female"],
    age: Number
  }, {
    functions: {
      /**
       * getUserByNicknames 为 FibAppORMModelFunction 类型
       * 
       * @param fibAppReq
       *    fibAppReq 包含了
       *    - session
       *    - query object
       *    - 原生的 request 信息
       *        - 如果 request 在传给 @fxjs/webx 之前经过过滤被挂载了别的属性, 这些属性也有效, 比如 @fxjs/session 对 request 添加的字段
       *          - request.session, 与上一层的 session 为同一对象
       *          - request.sessionid
       * 
       * @param fibAppReqData
       *    来自 POST 请求, `request.json()`, 默认为 {}
       */
      getUserByNicknames (fibAppReq, fibAppReqData) {
        // rest get 操作
        var data = app.api.get(fibAppReq, db, Person, fibAppReqData.id)
        // rest post 操作
        var postRes = app.api.post(fibAppReq, db, Person, fibAppReqData.id, {
          name: 'test',
          sex: 'male',
          age: 18
        })

        if (postRes.error)
          throw postRes.error

        // 方法的返回值必须为 FibAppModelFunctionResponse 类型
        return {
          success: postRes.success
        }
      }
    }
  });
};

FibAppInternalApis 中的所有所有 rest 操作函数, 内部都经过了 app.filterRequest 过滤.

  • app.filterRequest: FibAppSetupChainFn

注意该函数无返回值, 而是以最后一个参数作为回调函数. 更多详情可参考 @types/app.d.ts

使用 app.filterRequest 为 app 定制个性化的路由

app.filterRequest 是上述所有的实体相关的操作函数(所有的 rest 操作函数, 和可定制的 Model Function)的前置条件, 它主要做了两件事情:

  1. 过滤原生的 HttpRequest 对象 request 为 FibAppReq 对象, 并传给 func 作为第一个参数;
  2. request.json() 的结果作为 FibAppReqData 类型对象, 并传给 func 作为最后一个参数;

由于 @fxjs/webx 本质上是一个 mq.Routing 对象, 你可以用其 API 定制更多的个性化的路由, 比如

// 挂载一个静态目录
app.get('/static', http.fileHandler(path.resolve(__dirname, './static'), {}, true))
// 定制特别的 API
app.post('/__with_cls', (request) => {
  /**
   * 第二个参数传模型名, 表示寻找内置 model
   */
  app.filterRequest(request, 'person',
    /** 
     * @param req: FibAppReq
     * @param db: FibAppDb
     * @param __internal_model__: FxOrmNS.FibOrmFixedModel
     * @param data: FibAppData
     */
    (req, db, __internal_model__, data) => {
      // Do what you want to do
    }
  )
})
// 定制无关内置模型的 API
app.get('/__null_cls', (request) => {
  /**
   * 第二个参数传 '', 表示不寻找内置 model, 此时, 回调函数类型为 FibAppFilterableApiFunction__NullModel, 即第三个参数 __null_cls__ 为 null;
   */
  app.filterRequest(request, '',
    /** 
     * @param req: FibAppReq
     * @param db: FibAppDb
     * @param __null_cls__: null
     * @param data: FibAppData
     */
    (req, db, __null_cls__, data) => {
      // Do what you want to do
    }
  )
})

实际上, Model Function 正是通过 app.filterRequest 实现的, 详情可参考 src/classes/index.ts 中关于 app.post(':classname/:func', ...) 的实现.

View Functions

通过在 orm 的 opts 中添加 viewFunctions , 可以定义该模型相关的视图处理函数; 基于此, 你可以使得 webx 具有直接输出 html 的能力: 当来自客户端的 http 请求头是 Accept: text/html 时, webx 会尝试使用 viewFunctions 定义的函数处理视图. 参考下例:

const fpug = require('fib-pug')
const ejs = require('ejs')

db.define('user', {
    name: String,
    sex: ["male", "female"],
    age: Number,
    password: String,
    salt: String
}, {
    ...
    viewFunctions: {
        /**
         * @ctx `Accept: text/html`
         * 
         * 当客户端发起  GET /user/1 时, 会调用此函数;
         * 
         * 如果 id=1 的 user 存在, 则 result = {sucess: ...}; 否则 result = {error: ...}
         */
        get (apiResult) {
            let tpl = fpug.compile(
                    fs.readTextFile(path.resolve(__dirname, './tpl.get.pug'))
                )
            return {
                success: tpl(apiResult && {user: apiResult.success} || {})
            }
        },
        /**
         * @ctx `Accept: text/html`
         * 
         * 当客户端发起  GET /user 时, 会调用此函数
         * 
         * apiResult = {sucess: ...};
         */
        find (apiResult) {
            let tpl = ejs.compile(
                    fs.readTextFile(path.resolve(__dirname, './tpl.find.ejs'))
                )
            return {
                success: tpl(apiResult && {users: apiResult.success} || {})
            }
        },
        /**
         * @ctx `Accept: text/html`
         * 
         * 当客户端发起  GET /user/profile 时, 会调用此函数.
         * 
         * 由于 static: true, 此时 handler 的第一个参数 _ 恒为 null
         * 
         * @note 注意该路由与 /user/1 同属于 /:classname/:id 格式, 但 webx
         * 会优先尝试调用该方法.
         * 
         */
        profile: {
            static: true,
            /** _ is null */
            handler (_) {
                let tpl = ejs.compile(
                        fs.readTextFile(path.resolve(__dirname, './tpl.profile.ejs'))
                    )
                return {
                    success: tpl()
                }
            }
        }
    }
});

调用优先级

当客户端发起 Accept: text/html 的 http 请求时, viewFunctions 的调用优先级是这样的:

  • /:classname/:idOrFunc ==> viewFunctions.idOrFunc > viewFunctions.get
  • /:classname ==> viewFunctions.function

定义选项

viewFunctions: {
  get: {
    // {boolean}, default false
    static: true,
    // static === true, apiResult 为 null; 否则, apiResult 为 {success: ...} 或 {error: ...}
    handler (apiResult) {
      return {
        success: ...
      }
    }
  },
  /**
   * 此时 等价于 
   * {
   *    static: true,
   *    handler: func2
   * }
   */
  func2 () {
    return {
      success: ...
    }
  }
}
  1. 如果 viewFunctionstatic 为 true, viewFunction 函数的第一个参数为 null;
  2. 如果 viewFunctions 没有 viewFunction.get, viewFunction.find, viewFunction.eget, viewFunction.efind 定义时, 所有该导向这些viweFunction的请求都等价于直接请求对应的 webx 内部 API 方法.
  3. 如果 viewFunctionstatic 不为 true, 如果存在对应的 webx 内部 API 方法 , 则 viewFunction 函数的第一个参数是该对应方法的返回值;

关于第 2 点, 比如 Model user 有如下定义

viewFunctions: {
}

此时发起 {Accept: text/html, GET /user/1} 请求, 不会通过 viewFunctions 处理, 该请求等价于 {Accept: applicaton/json, GET /user/1} 请求的结果. 同理, 此时发起 {Accept: text/html, GET /user} 请求等价于发起 {Accept: applicaton/json, GET /user} 请求.

关于第 3 点, 比如 Model user 有如下定义

viewFunctions: {
  func1: {
    static: false,
    handler (apiResult) {
      
    }
  }
}

此时发起 {Accept: text/html, GET /user/func1} 请求, 由于它可以被认为是请求 id=fun1 的 user 的信息, 因此, 如果 id=func1 的 user 真的存在且可以被请求道, 则此时 handler 的第一个参数 apiResult 就是 { success: [userInfo] }; 若 id=fun1 的 user 不存在或者因ACL 权限无法被用户访问到, 则 apiResult 为 { error: ... }

这个特性意味着你可以对某些 Model 的特定对象做特殊处理, 比如, 对 id=888 的 User 设定为 Lucky Dog, 返回特别的 html 给客户端 :)

viewFunction 对比 function

共同点

viewFunctionfunction 很相似

  1. 都要返回符合 FibAppResponse 格式 的对象
  2. 都是 ORM Model 的定义选项

区别

  1. function 处理 webx 中的 POST /:classname/:func 请求; viewFunctions 处理 webx 中的 GET /:classname/:funcAccept 头包含 text/html 的请求
  2. function 函数的返回值, webx 会尝试以 json 的方式写入 HttpResponse; viewFunction 函数的返回值, webx 会尝试以文本的方式写入 HttpResponse