• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

KJServlet: KJServlet 是一个基于 Nashorn 编写的 Web 服务框架,这意味着你可以使用 ...

原作者: [db:作者] 来自: 网络 收藏 邀请

KJServlet - 一个基于 Java 的 JS web 框架

Nashorn 已死

Nashorn 生于Java 8,死于Java 11,由于之后 Nashorn 会被从 JDK 中完全移除,本工程不再进行维护。

简介

KJServlet 是一个轻量级的 Javascript web 框架,该框架基于由 Java 8 开始引入的 Nashorn 引擎,这意味着你所写的 javascript 代码最终会运行在 JVM 的环境下,也正因为如此,你可以非常容易的使用各种已经存在的第三方 Java 类库来构建你的应用程序。

这也是一个比较自由风格的框架,支持你使用不同的代码风格来编写你的逻辑。拿编写 controller 为例,你可以将使用流行的面向函数变成的方式使用函数来当作入口,或许你习惯了面向对象的风格,你也可以使用对象和方法来作为入口;在编写 controller 方法的时候,你可以使用回调(callback)的方式来返回页面数据,也可以直接返回数据由框架来确定如何返回,并且,该框架支持了 MVC,你可以使用非常熟悉的 JSP、Freemarker、Velocity 来编写你的显示层代码。

这个文档是本框架的使用文档,绝大部分内容是在描述框架提供的接口和使用方式,偶尔会有一些设计的内容,但是非常少,如果你们对框架的设计本身有兴趣,在读源代码的时候有问题,可以随时联系我。另外由于这个框架非常的新,并且目前只有一个人在维护,一些错误和 BUG 是无可避免的,所以如果出现了问题,除了可以在这里提交 BUG 之外,也可以随时邮件联系我,我的邮箱未 [email protected]。在国内可发送邮件到 [email protected]

关于许可:就目前而言,该框架提供两种许可,默认的情况是 GPL,如果你想将该框架作为商业用途,又不想开放源代码的话,可以随时联系我,我会提供一个 Apache 的许可。

快速开始

本框架所有的接口都基于J2EE的标准接口,所以你可以非常容易的将其嵌入到任何的 J2EE 项目当中。你可以按照以下步骤来运行一下例子:

  1. 下载kjservlet-[version].jar并将其放入你的工程lib目录,大部分情况下,这个目录的位置是
 [webapps]/WEB-INF/lib
  1. 如果你使用 maven,由于这个框架尚未提交到中央库,所以你可以下载本工程,通过 maven install 到本地仓库,之后在你的例子工程的 pom.xml 加入:
	<dependency>		<groupId>me.keijack.kjservlet</groupId>		<artifactId>kjservlet</artifactId>		<version>0.1.0</version>	</dependency>
  1. 在你 web 工程的 web.xml 中增加以下的 servlet 和 servlet-mapping:
	<servlet>		<display-name>k-js-servlet</display-name>		<servlet-name>KJServlet</servlet-name>		<servlet-class>org.keijack.kjservlet.KJServlet</servlet-class>	</servlet>	<servlet-mapping>		<servlet-name>KJServlet</servlet-name>		<url-pattern>/*</url-pattern>	</servlet-mapping>
  1. 在你的 class path 目录下(大部分情况下,这个目录是 ./WEB-INF/classes)增加一个 javascript 文件 demo.js
  2. demo.js文件上增加下述方法:
function sayHello(req){    var name=req.param["name"];    return "<!DOCTYPE html><html><head><title>Say Hello</title></head><body>Hello, " + name + "</body></html>";}
  1. 启动服务器,并且用浏览器方位http://127.0.0.1:8080/kjservlet-demo/demo/sayHello?name=World,“Hello world” 就会显示在浏览器上了。

更多详细的内容,请参考以下的用户手册。

路由

就如你在上面例子所看到的,你甚至不需要配置任何的路由信息,为何?因为这个框架是基于目录来进行路由的。

就上述的例子而言,如果你将demo.js放到一个目录,例如./WEB-INF/classes/path中,那么,你的访问地址就得变为http://127.0.0.1:8080/kjservlet-demo/path/demo/sayHello?name=World

事实上,除了路经之外,还是有一些别的配置项会影响路由规则的。 这个框架在每次启动的时候都会从 classpath 目录下尝试去读取一个名为global.js的 javascript 文件,你可以在这个文件中增加一些配置内容。

$webapp = {    controller : {        fileHome : "classpath:/org/keijack/kjservlet/demo/controller", // 你的 controller 以及其引入的 javascript 文件的根目录, 默认情况下是 "classpath:"        fileSuffix : ".js", // 你的 controller 以及其引入的 javascript 文件的后缀,默认是".js"        suffix : "", // 你访问服务器的 url 的后缀,例如常用的 .do 或者 .action    },    resources : [ "*.html", "/images/*" ], // 一些静态文件的规则。};

global.js在这个框架中是一个非常重要的文件,他不是完整意义上的配置文件,因为在上面的 javascript 会被执行,一些全局的方法你也可以在这里定义。具体的内容,请参考global.js文件相关的章节内容。

以”/“未分割,除了服务器,上下文路经之外,前面的部分是计算的 controller 文件的路经,最后的一段则是 controller 的函数或者是对象和方法。在上面例子中,sayHello便是处理这次请求的函数。

还有另外一种方式可以配置路由,这一点我们稍后再述。

本框架的设计未面向函数设计。所以,在你的 controller js 文件中,你完全可以使用全局函数,而不必拘泥于使用对象。事实上,每个请求都会运行在一个新的上下文(context)里,这意味着你甚至可以在 controller 文件中使用全局变量,这些”全局变量“事实上只在当次请求有效。当然,你也完全使用对象来管理你的代码。

如上述例子,如果使用对象方法而非函数的话,你可以定义这样一个对象在你的demo.js

var person = {    yieldName : function(req) {        var name = req.parameters.name;        return "<!DOCTYPE html><html><head><title>Say Hello</title></head><body>My name is " + name + "!</body></html>";    }};

你可以使用以下链接访问该 controller 方法:

http://[your_server_host]:[your_server_port]/[your_servlet_context]/demo/person.yieldName?name=John

You can add alias to routes, at your $webapp object, add a property named alias. for example:本框架主要的路由规则都是依据路经来计算的,如果你还是比较习惯使用路由配置的方法的话,那么你可以使用aliases的配置来进行配置。请注意,这个只是别名,原路经按照规范访问依然有效。

$webapp = {    controller : {        fileHome : "classpath:/org/keijack/kjservlet/demo/controller",         fileSuffix : ".js",          suffix : "",     },    aliases : { // 别名    	"/yieldMyName" : "/demo/person.yieldName",    },    resources : [ "*.html", "/images/*" ], };

增加了上述配置之后,你可以使用以下链接访问上面对象的例子。

http://[your_server_host]:[your_server_port]/[your_servlet_context]/yieldMyName?name=John

引入其他的文件

上一章节说明了本框架的路由规则,通过路经来寻找处理一个请求的 javascript 文件。但是,将所有的逻辑放在一个文件中并不是最佳的实践,所以在实际业务当中,大部分情况下我们需要在一个文件中引入其他的文件。

Nashorn 内置提供了一个方法 load(文件全路经),不过我们并不推荐你使用这个方法,而是使用框架提供的 import(文件相对路经) 的方法。

以下是你应该使用 imports() 方法的理由:

  • 在一个范围里(global.js运行在全局范围内,每个请求运行在单独的范围,关于范围这一点,后续有详细说明),import 方法之后引入一个文件一次。所以对于一些通用的方法而言,通过 import 引入,会更为高效。也不会因为重复引入而导致一些问题。

  • import 方法使用的是相对路经,所以有更好的迁移性,不会因为你迁移了路经就导致引入错误。注意!在 controller 范围引入文件是,相对路经的根目录是你在 global.js$webapp 对象中配置的 fileHome 属性的值(默认是 classpath),而在全局范围内,import 方法的根目录固定是 classpath。

  • 使用 import 方法,你可以使用 . 符号来替代 / 符号,这对于熟悉 Java 的程序员来说更为熟悉。也因此,请注意 *在你的文件路经当中你不能使用.符号来命名,而且在引入时不能包含文件的后缀名。在 controller 范围中引入文件,你的后缀名可以通过在 global.js$webapp 中的 fileExtension 来进行配置;而在全局范围内,你所有的引入文件后缀名必须是.js *

编写 Controller 函数/方法

Request 和 Response 参数

就如你自己编写 Servlet 类一样,你的 controller 函数/方法会接收到两个参数,第一个参数是 request,第二个参数是 response。这两个参数本身就是对 HttpServletRequest 和 HttpServletResponse 进行了 js 对象封装。所以,理论上,你应该从 request 对象获取用户提交的内容,然后通过 response 对象将结果数据写回。但是,似乎你在快速开始章节的并不是这样,你只看到一个 request 参数,关于这一点,会在后续说明。

参考以下的 controller 方法

function dosth(req, res){    var someParamVal = req.parameters["name-defined-in-form-input"]; // 如果你的参数足够简单,你甚至只用 ".",例如你的参数名是"simpleName"的话,你可以使用: req.parameters.simpleName    var serviceResult = someServiceObject.doService(someParamVal);    res.write(serviceResult);}

如果你更习惯使用回调方法的话,你可以这样来编写:

function dosth(req, res){    var someParamVal = req.parameters["name-defined-in-form-input"];     someServiceObject.doService(someParamVal, function (serviceCallbackResult) {        res.write(serviceCallbackResult);    });}

以下是 request 对象和 response 对象的完整属性、方法列表。

Request 对象的完整属性和方法(命名为 req)

  • req.oriRequest,原始的 HttpServletRequest Java 对象.
  • req.session,通过原始的 request.getSession() 获取的 HttpSession Java 对象。
  • req.authType,通过原始的 request.getAuthType()获取的字符串对象。
  • req.method,通过原始的 request.getMethod()获取的字符串对象。
  • req.contentLength,通过原始的 request.getContentLength()获取的long对象,标识请求内容的字节数。
  • req.contentType,通过原始的 request.getContentType()获取的字符串对象,标识请求内容的类型。
  • req.queryString,通过原始的 request.getQueryString()获取的字符串对象,是你访问地址 "?" 后面的内容。
  • req.protocol,通过原始的 request.getProtocol()获取的字符串对象,是本次访问的协议。
  • req.schema,通过原始的 request.getSchema()获取的字符串对象,是本次访问的协议,可能是“http”或者是“https”。
  • req.serverName,服务器名,通过原始的 request.getServerName() 获取的字符串对象。
  • req.serverPort,服务器端口,通过原始的 request.getServerPort() 获取的数字对象。
  • req.contextPath,上下文,通过原始的 request.getContextPath() 获取的字符串对象。;
  • req.requestURI,请求资源地址,不包含协议,地址,端口,访问参数等信息,包含当前上下文地址. 通过原始的 request's getRequestURI() 获取的字符串对象。
  • req.requestUrireq.requestURI的别名。
  • req.urireq.requestURI的别名。
  • req.ctxUri,不带上下文地址的资源请求地址.
  • req.servletPath,当前 servlet 的路径,通过原始的 request's getServletPaht() 获得的字符串对象。
  • req.requestURL,请求地址,整个访问地址?前的一段,通过原始的 request's getRequestURL() 获取,已经通过 toString() 方法转成字符串对象。
  • req.requestUrlreq.requestURL的别名。
  • req.urlreq.requestURL的别名。
  • req.headers,请求头部,通过原始封装 request's getHeader(name) 方法生成的 JSON 对象。如, req.headers["user-agent"]可以获得头部的user-agen数据。
  • req.headerreq.headers的别名。
  • req.parameterValues,请求参数,包含了有URL的?后的通过字符串传递的部分和当 conten-type 是application/x-www-form-urlencoded 或者 multipart/form-data 时请求体里的部分。该参数总是返回一个数组(即使该数组里只有一个元素)。如果这时一个multipart/form-data请求,并且该请求包含上传文件,那么文件参数会被解析为以下的JSON对象:
{    "filename": "somePicture.jpg", // the file's name    "contentType": "image/jpg", // the content type    "content": "xxxxxx" // a string that get from the multipart content,                         // you can use getBytes() method to get the byte array,                         // and write it into a file.}
  • req.parameterValuereq.parameterValues的别名。
  • req.paramValsreq.parameterValues的别名。
  • req.parameters,类似req.parameterValues,但只返回一个值,再有多个同名的请求参数时,返回第一个。
  • req.parameterreq.parameters的别名。
  • req.paramsreq.parameters的别名。
  • req.paramreq.parameters的别名。
  • req.pathValues,路径参数,详细的内容请参考路径参数章节。
  • req.pathValuereq.pathValues的别名。
  • req.setAttribute(name, val),原始 request 的 setAttribute(name, value) 方法的封装。
  • req.setAttr(name, val)req.setAttribute(name, val) 的别名。
  • req.getAttribute(name, defaultValue),原始 request's getAttribute(name) 方法的封装,不同的是增加了默认参数,如果原方法返回 null 值,这个方法返回指定的默认值。
  • req.getAttr(name, defaultValue)req.getAttribute(name, defaultValue)的别名。
  • req.removeAttribute(name),原始request's removeAttribute(name) 方法的封装。
  • req.removeAttr(name)req.removeAttribute(name)的别名。
  • req.rmAttr(name)req.removeAttribute(name)的别名。
  • req.readRequestBody(),返回请求体,如题 content-type 是 application/x-www-form-urlencoded 的话,该方法返回 null。
  • req.data,如果请求的 content-type 是 application/json的话,该参数是通过 JSON.parse(req.readRequestBody) 转成的 JSON 对象。

Response 对象的完整属性和方法(命名为 res)

  • res.oriResponse,原始的 HttpServletResponse Java 对象。
  • res.contentType,标识当前返回体的内容类型,通过原始的 response 的 setContentType(contentType) 方法进行设值。
  • res.headers,返回体对象的头部参数,是一个JSON对象,会通过原始的 response 的 addHeader(name, value) 方法将该对象中的数据加到返回体中。
  • res.headerres.headers的别名。
  • res.addHeader(name, value),原始的 response 的 addHeader(name, value) 方法的封装。
  • res.setHeader(name, value),原始的 response 的 setHeader(name, value) 方法的封装。
  • res.sendError(code, message),原始的 response 的 sendError(code, message) 方法的封装,其中第二个参数message为可选参数。
  • res.redirect(url),原始的 response 的 sendRedirect(url) 方法的封装。
  • res.write(data), 将数据写入 response 的输出流中。data 参数可以为 JSON 对象,该方法会自动将 JSON 转为字符串写入输出流。
  • res.writeByte(bytes), 只接受 Java 的 byte[] 类型的参数,使用原始 response 的 getOutputStream().write(bytes) 方法写数据到输出流中; 使用该方法,程序会自动生成头部数据中的Content-Length参数。

另一种响应方式

在此章节,将描述另一种响应的方式,你无须使用response对象,仅通过方法返回值,就能使得请求获得正确的响应。

正如你在快速开始章节所见,你根本无须使用response对象,而直接通过返回 JSON 对象、字符串等数据来进行响应请求。框架将会使用合适的响应方式来处理这些数据。

从原理上来说,如果方法返回的是一个符合格式的 JSON 数据,框架会自动进行响应的处理。而在编码过程中,我们并不需要知道具体的该 JSON 对象的结构,你只需使用框架提供的全局对象 $renderer来处理你的数据就个可以。

$renderer 对象的方法如下::

  • $renderer.render(contentType, content, headers),生成一个自定义内容类型的结构化 JSON 对象。其中,如果你的contentType参数传入的是null、空字符串、false,框架会根据你传入的参数来判断你的响应类型是什么;content参数仅接受 string 类型;headers参数是可选的,该参数的类型应为一个 JSON 对象,所有该对象中的数据都会用原始的response 的 addHeader(name, value) 方法写入响应头部。.
  • $renderer.text(text, headers),生成一个内容类型为 "text/plain"的结构化 JSON;headers参数为可选。
  • $renderer.html(data, headers),生成一个内容类型为 "text/html"的结构化 JSON;headers参数为可选。
  • $renderer.json(data, headers),生成一个内容类型为 "application/json"的结构化 JSON,其中参数data应为一个 JSON 对象;headers参数为可选。.
  • $renderer.bytes(data, headers)data应为 Java 的 byte[]对象,headers参数为可选。框架将使用原始 response 的 getOutputStream().write(bytes)将数据写入响应体。
  • $renderer.redirect(url, headers), 生成重定向到指定 url的结构化JSON,其中headers参数为可选。
  • $renderer.forward(url, data, headers),生成一个转发请求到指定的 url的结构化JSON,data参数为可选,如果传入该对象,框架将调用原始的 request.addAttruibute(name, value) 方法将数据写入请求;headers参数亦为可选参数。
  • $renderer.error(code, message),生成一个发送错误编码的结构化JSON,其中message参数为可选。
  • $renderer.view(viewFileLocation, data, headers), 生成一个供框架 MVC 特性使用的结构化 JSON,框架 MVC 特性在后续章节叙述。

以下是使用$renderer对象的例子,使用了该对象,你的代码风格大概如下:

function dosth(req){	var param = req.parameters;	var serviceResult = someServiceObject.doService(param);	return $renderer.json(serviceResult);}

事实上,在大部分情况下,$renderer对象都不是必须显示编写的,你可以直接返回你的业务对象,框架会对这些对象进行自动适配:

function dosth(req){	var param = req.parameters;	var serviceResult = someServiceObject.doService(param);	return serviceResult; // 该返回值为一个 JSON 对象}

你还可以返回类似如下架构的数据:

    return "<!DOCTYPE html><html>....</html>"; // 会响应为 "text/html"。
    return "<?xml version="1.0" encoding="UTF-8"?><tag>...</tag>"; // 会响应为 "text/xml"。
    return "redirect:/url"; // 会重定向至 /url
    return "forward:/url"; // 会转发至 /url
    return "some text"; // 会响应为 "text/plain" 

MVC

MVC 是一个非常常见的设计模式,该设计模式的主要原则在于分层,将代码分为逻辑层(Model)、控制层(Controller)和展示层(View)。代码分层的好处在于解耦,使得代码易读和更好维护。

网站开发者都喜欢使用 MVC 模式,特别是 Java Web 的开发者,也因此,用 Java 语言提供的模板引擎特别多。得益于此,在我们的框架中使用 MVC 模式也变得非常的容易。

KJServlet 支持 JSP、Freemarker 和 Velocity 模板引擎用做 View 层。

正如上章所见,你需要使用 $renderer.view 方法来使用 MVC 模式,参考例子如下:

function dosth(req){    var param = req.parameters;	var serviceResult = someServiceObject.doService(param);	return $renderer.view("/WEB-INF/pages/view.jsp", serviceResult); // serviceResult 为 JSON 对象}

框架默认使用 JSP 作为模板引擎,不会你可以在global.js 文件中的 $webapp 对象中自定义自己的模板引擎,例子如下:

$webapp = {    controller : {        fileHome : "classpath:/org/keijack/kjservlet/demo/controller", // 你的 controller 方法的路径,默认为 "classpath:"        fileSuffix : ".js", // 你的 controller 文件的后缀,默认为 ".js"         suffix : "", // 你 controller 请求的后缀,默认为 ""    },    resources : [ "*.html", "/images/*" ],    view : {        resolver : "jsp", // 模板引擎类型,可为"jsp"、"freemarker"或"velocity"        prefix : "/WEB-INF/pages/", // view 文件路径的前缀。        suffix : ".jsp", // view 文件路径后缀。    }, }

如果你在 global.js中配置了以上参数,你的 controller 方法将会简化为:

function dosth(req){    var param = req.parameters;	var serviceResult = someServiceObject.doService(param);	return $renderer.view("view", serviceResult); // 模板文件将为 /WEB-INF/pages/view.jsp }

注意!如果你使用 freemarker 或者 volocity 模板引擎,你需要自己增加这些引擎的依赖。如果你使用 maven 和 freemarker,你需要在你的 pom.xml中加入以下的依赖配置

    <dependency>        <groupId>org.freemarker</groupId>        <artifactId>freemarker</artifactId>        <version>2.3.26-incubating</version>    </dependency>

$renderer.view(templateFileLocation, data, header) 中的第二个参数data应该是一个 JSON 对象,框架会将该 JSON 对象转换成一个 Java Map 对象然后再传递到你的模板引擎中,因此,在你的模板引擎中,你可以使用 Map 的方式来在使用该对象。

假设你的data对象如下:

function dosth(req){    var data = {"userName": "Jhon",                "sex": "male",                "age": 28,                "department": { "name": "HR",                                "phone": "+01xxxxxx",                },                "subordinates": ["Mike", "Lily"]    };    return $renderer.view("view", data);}

在你的模板文件中 -- 以 freemarker 为例 -- 可以这样来使用:

<!DOCTYPE html><html><head>    <title>Personal Details</title></head><body>    <h1>Personal Details of ${userName}</h1>    <p>sex: ${sex}</p>    <p>age: ${age}</p>    <p>department: ${department.name}</p>    <p>subordinates: <#list subordinates as sbn>${sbn}, </#list></body></html>

data对象中,你还可以使用对象方法/函数,再模板引擎中,你需要使用 call 或者 apply 使用这些方法。

注意!call方法能够接收最多10 个参数。而 apply 总是接受一个数据对象总为参数。

假设你的data对象如下:

    var data = {"userName": "Jhon",                "sex": "male",                "isAdult": function(){                	// 注意:再这个方法中不要使用 `this`因为最终,这些方法会封装为一个 Java 对象。                 	return data.age >= 18;                },                "age": 28,                "department": { "name": "HR",                                "phone": "+01xxxxxx",                },                "subordinates": ["Mike", "Lily"]    };

注意!不要再data对象的方法中使用 this,因为最终这些方法会被封装为一个 Java 对象 JSFunctionWrapper,因此this的指向并不是当前的 JSON 对象。

那么,再你的模板文件中,你可以如下调用该方法:

<!DOCTYPE html><html><head>    <title>Personal Details</title></head><body>    <h1>Personal Details of ${userName}</h1>    <p>sex: ${sex}</p>    <p>is adult: ${isAult.call()}</p>    <p>age: ${age}</p>    <p>department: ${department.name}</p>    <p>subordinates: <#list subordinates as sbn>${sbn}, </#list></body></html>

如果你认为以上3个模板引擎都不适用,你甚至可以自定义你自己的模板引擎:

$webapp = {    controller : {        fileHome : "classpath:/org/keijack/kjservlet/demo/controller",         fileSuffix : ".js",          suffix : "",     },    resources : [ "*.html", "/images/*" ],    view : {        resolver : function(viewFileLocation, data, headers){            // viewFileLocation 已经拼装好以下配置中的 prefix 和suffix。            // 生成最终的 View 层渲染的 html 字符串            var html = ...;            // 最后通过 $renderer 返回一个结构化的对象。            return $renderer.html(html, headers);         },        prefix : "/WEB-INF/pages/",         suffix : ".jsp",     }, }

标注和面向切面编程(AOP)

Javascript 不像 Java 一样具有内置的标注体系,并且,Javascript 的对象生成方式和 Java 有非常大的区别,你甚至无须使用 new 便可以定义对象,所以在 Javascript 中,其实很难如 Java 一样做到面向切面编程。然而,通过一些小把戏,我们还是勉强做了一些面向切面的内容。

如果你熟悉 Javascript,你必然对 strict mode有所耳闻。如果你向希望一个方法使用 strict mode 来执行,你需要在你的方法开始时加入一行 "use strict",我们的 AOP 灵感也时来源于此。请参考以下代码:

function dosth(req){    "use strict";    "@post"; // KJSeverlet 内置的标注,该方法只接受 POST 类型的请求。    "@myOwnAnno"; // 用户自定义的标注    // 你的逻辑代码    ...    "@Anno2"; // 如果你的标注放在逻辑体里,该标注不会被框架读取。        ...}

在以上的例子中,你获得的标注将是 ["@post", "@myOwnAnno"]。

注意!标注仅仅在 Controller 方法中生效!

我们已经知道如何进行标注了,那么如何使用这些标注呢?我们回到在global.js文件中的$webapp对象中来:

$webapp = {    controller : {        fileHome : "classpath:/org/keijack/kjservlet/demo/controller",         fileSuffix : ".js",          suffix : "",     },    resources : [ "*.html", "/images/*" ],    view : {        resolver : "jsp",         prefix : "/WEB-INF/pages/",         suffix : ".jsp",     },    interceptors : {        intercept : "@myOwnAnno", // 如果你对多个标准进行相同的切面,该参数可以传入一个数组        // intercept: ["@myOwnAnno1", "@MyOwnAnno2"],        before : function(req, res, ctx) {            // controller 方法之前执行的代码            return true; // 如果希望方法继续执行,你必须返回 true,如果返回 false,那么,controller 方法将被终止,不再执行。        },        after : function(req, res, result, ctx) {            // controller 方法执行之后执行的代码。        },        onError : function(req
                      

鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap