UNPKG

node-web-mvc

Version:
951 lines (681 loc) 23.3 kB
<p align="center"> spring style web mvc framework </p> <h1 align="center">Node Web Mvc</h1> ## 创建应用 ```npm npm create node-web-mvc@latest ``` ### spring风格 > index.ts ```js import { SpringApplication, SpringBootApplication } from 'node-web-mvc'; @SpringBootApplication({ // 代码热更新: 在该目录下的文件改动支持热更新(无需重启服务) 注意:在process.env.NODE_ENV === 'production'时强制无效 hot: './test', // 启动时需要加载的模块目录, 在不配置时默认为 process.cwd() scanBasePackages: './test', // 配置服务端口相关 // server: { port: 8080 } }) export default class DemoApplication { static main() { SpringApplication.run(DemoApplication); // 启动后执行逻辑 } } ``` > WebAppConfigurer.ts ```ts import { WebMvcConfigurationSupport } from 'node-web-mvc'; @Configuration export default class WebAppConfigurer extends WebMvcConfigurationSupport { // 这里可以扩展配置... } ``` ## Controller 控制器 完成启动配置后,可以在控制器目录下定义对应的控制器 控制器的定义风格和`Spring Mvc`风格一致。 > 例如: ```js import { RestController, RequestMapping, GetMapping } from 'node-web-mvc'; @RestController @RequestMapping('/home') class HomeController { @GetMapping({ value:'/index',method:'GET' }) index(){ return 'Hi i am home index'; } } ``` 更多的控制器配置,我们可以阅读后面通过注解来完善控制器。 ## Route 路由映射 ### `@RequestMapping` 该注解用于将请求映射到指定控制器。 有两种使用方式 #### 简要模式 仅配置访问路径,例如: 以下例子中,仅配置 以 `/home` 来访问`HomeController` ```js @RequestMapping('/home') class HomeController { } ``` #### 详细配置 通过传入一个对象[`RequestMapping`](#RequestMapping)来进行详细映射。 > 以下例子通过`@RequestMapping`配置 允许在`GET`方式下通过`/home/index`路由来访问 `HomeController`的`index`函数 ```js @RequestMapping('/home') class HomeController { @RequestMapping({ value:'/index',method:'GET' }) index(){ return 'Hi i am home index'; } } ``` 在大多数情况下,我们只会配置`路由`与`请求类型` 可以通过以下几个注解来进行快捷配置。 - `@GetMapping` 映射一个`method`为 `GET`的请求 ```js @RequestMapping('/home') class HomeController { @GetMapping('/index') index(){ return 'Hi i am home index'; } } ``` - `@PostMapping` 映射一个`method`为 `POST`的请求 - `@PutMapping` 映射一个`method`为 `PUT`的请求 - `@DeleteMapping` 映射一个`method`为 `DELETE`的请求 - `@PatchMapping` 映射一个`method`为 `PATCH`的请求 #### 路由风格 通过 `@RequestMapping` 等注解配置路由时,可以有以下几种配置风格 - `普通`路由 ```js @GetMapping('/detail/index') ``` - `参数占位`类型 > 使用 `{}` 来标识占位 通过`占位`映射的路由参数,可以通过[`@PathVariable`](#PathVariable) 注解来提取 ```js @GetMapping('/detail/{id}') ``` > `正则`风格路由 ```js @GetMapping('/route/{}') ``` ## Arguments 参数提取 我们可以通过以下几个注解来定义请求参数的提取方式。 - `@RequestParam` 提取类型为`urleoncoded`的参数 - `@RequestBody` 提取整个`body`内容,通常是提取成为一个`json`对象 - `@PathVariable` 提取路由中的占位参数 - `@RequestHeader` 提取请求头中的指定名的请求头做为参数 - `@ServletRequest` 用于提取`request` 对象 - `@ServletResponse` 用于提取`response` 对象 ### RequestParam 从`urleoncoded`的内容中提取指定名称的参数 `@RequestParam` 作为参数注解,不进行任何配置,默认会以参数名来作为提取名依据 ```js @RequestMapping('/home') class HomeController { @GetMapping('/index') index(@RequestParam name){ return `Hi ${name}, i am home index`; } } ``` 同时`@RequestParam` 也可以进行详细配置[`ParamAnnotation`](#ParamAnnotation) > 例如: 将url中传递过来的`userName`值提取给`index`中的 `name`参数 ```js @RequestMapping('/home') class HomeController { @GetMapping('/index') index(@RequestParam({ value:'userName', required:true }) name){ return `Hi ${name}, i am home index`; } } ``` 文件上传参数提取 ```js import { MultipartFile } from 'node-web-mvc'; @Api({ description:'上传' }) @RequestMapping('/upload') class HomeController { // 单个文件上传 @ApiOperation({ value: '上传文件', notes: '上传证书文件' }) // 配置swagger 生成上传表单 @ApiImplicitParams([ { name: 'files', value: '证书', required: true, dataType: 'file' }, { name: 'id', value: '用户id', required: true }, ]) @PostMapping({ value: '/file', produces: 'application/json' }) async index(@RequestParam file: MultipartFile,@RequestParam id){ // 保存文件 await file.transferTo('appqdata/images/' + file.name); return { code:0, message:'上传成功' } } // 多个文件上传 @PostMapping({ value: '/files', produces: 'application/json' }) async index(@RequestParam files: Array<MultipartFile>){ // 保存文件 for (let file of files) { await file.transferTo('appdata/images/' + file.name) } return { code:0, message:'上传成功' } } } ``` ### RequestBody 提取整个`body`内容,通常是提取成为一个`json`对象 ```js @RequestMapping('/order') class OrderController { @GetMapping('/save') saveOrder(@RequestBody order){ console.log(order); } } ``` ### PathVariable 从请求路由中提取路径参数 ```js @RequestMapping('/order') class OrderController { @GetMapping('/detail/:id') detail(@PathVariable id){ return `Order ${id}`; } } ``` ### RequestHeader 从请求头中提取参数 ```js @RequestMapping('/home') class HomeController { @GetMapping('/index') detail(@RequestHeader('content-type') ct){ return `content-type: ${ct}`; } } ``` ### ServletRequest 提取`request`整个对象。 ```js @RequestMapping('/home') class HomeController { @GetMapping('/index') detail(@ServletRequest request){ } } ``` ### ServletResponse 提取`response`整个对象。 ```js @RequestMapping('/home') class HomeController { @GetMapping('/index') detail(@ServletResponse response){ } } ``` ## Responsee 返回内容 在控制器具体函数中,我们可以返回以下几种类型来将内容返回到客户端。 - ModelAndView 返回一个视图 - String 返回一个字符串 - Object 如果需要正常返回,需要通过`RequestMapping`指定produces为`application/json` - Promise 返回一个异步结果 - Middlewares 返回一个类express的中间件执行结果 ```js import { RequestMapping, GetMapping, Middlewares } from 'node-web-mvc'; @RequestMapping('/home') class HomeController { @GetMapping('/index') index(){ return new Middlewares([ (req,resp,next)=> next() ]) } } ``` ```js @RequestMapping('/home') class HomeController { @GetMapping('/index') index(){ return new ModelAndView('home/index'); } @GetMapping('/string') strings(){ return `output :String`; } @GetMapping({ value: '/object', produces:'application/json' }) list(){ return [ { name:'张三',id:100 } ]; } } ``` ## 异常处理 框架可以通过以下两个注解来进行控制器异常处理 - `ExceptionHandler` - `ControllerAdvice` ### ExceptionHandler 如果将`ExceptionHandler`标注在控制器的函数上,则表示当前控制器的函数执行异常时,会使用当前标注的函数来进行异常处理。 ```js import { GetMapping, RequestMapping, ExceptionHandler } from 'node-web-mvc'; @RequestMapping('/home') export default class HomeController { @GetMapping('/index') index(){ throw new Error('error'); } @ExceptionHandler handleException(error){ // 返回一个 json 异常对象 return { code:error.code,message:error.message }; } } ``` ### ControllerAdvice 利用`ControllerAdvice` 来进行全局异常控制 定义一个异常处理类,然后使用`ControllerAdvice`标注当前类为全局控制器处理, 最后在该类上定义一个异常处理函数,然后通过`ExceptionHandler`标注成异常处理函数。 例如: > AppException.ts ```js @ControllerAdvice class AppException { @ExceptionHandler handleException(error){ // 返回一个 json 异常对象 return { code:error.code,message:error.message }; } } ``` ## Resource 静态资源 框架也提供了静态资源服务,以及针对静态资源设定缓存策略等,同时也支持`gzip`压缩处理。 ```js import { Registry } from 'node-web-mvc'; // 启动Mvc Registry.launch({ resource:{ gzipped:true,// 默认不开启gzip // 默认可不填写,默认值为: // application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain mimeTypes:'text/css,text/html', }, addResourceHandlers(registry){ registry .addResourceHandler('/swagger-ui/**') .addResourceLocations('/a/b/swagger-ui/') .setCacheControl({ maxAge:0 }) // .addResolver(new CustomResolver()) } }); ``` ### 自定义ResourceResolver ... ## View 视图 框架默认不具备视图渲染功能,不过我们可以自定义视图解析器来支持渲染像`ejs` ,`handlebars`等类型的视图。 #### 第一步 实现一个ejs 视图(`View`) > ./EjsView.ts ```js /** * @module EjsView * @description Razor视图 */ import ejs from 'ejs'; import { View } from 'node-web-mvc'; export default class EjsView extends View { /** * 进行视图渲染 * @param model 当前视图的内容 * @param request 当前视图 * @param response */ render(model, request, response) { return ejs.renderFile(this.url, model).then((html) => { response.setHeader('Content-Type', 'text/html'); response.setStatus(200).end(html, 'utf8'); }) } } ``` #### 第二步 实现一个ejs视图解析器 > EjsViewResolver.ts 通过重写`UrlBasedViewResolver` 的`internalResolve` 来解析`ejs`的视图 ```js import fs from 'fs'; import path from 'path'; import { UrlBasedViewResolver,HttpServletRequest,View } from 'node-web-mvc' import EjsView from './EjsView'; export default class EjsViewResolver extends UrlBasedViewResolver { internalResolve(viewName: string, model: any, request: HttpServletRequest): View { const file = path.resolve(viewName); if (fs.existsSync(file)) { return new EjsView(viewName); } return null; } } ``` #### 第三步 注册ejs视图解析器 启动时通过`addViewResolvers`配置来注册视图解析器。 ```js import { Registry } from 'node-web-mvc'; // 启动Mvc Registry.launch({ // ... 其他配置 // 通过配置,来注册ejs视图解析器s addViewResolvers(registry) { // 注册ejs视图解析器 registry.addViewResolver(new EjsViewResolver('test/WEB-INF/', '.ejs')) } }); ``` ## Interceptor 拦截器 框架同时也内置了拦截器,我们可以通过自定义拦截器来完成一些请求的前置,以及后置处理。 ### 自定义权限校验拦截器 #### 第一步 通过继承于`HandlerInterceptorAdapter`来实现一个拦截器 > AuthorizationInterceptor.ts ```js import { HandlerInterceptorAdapter } from 'node-web-mvc'; export default class AuthorizationInterceptor extends HandlerInterceptorAdapter { /** * 在处理action前,进行请求预处理 * @param { HttpRequest } request 当前请求对象 * @param { HttpResponse } response 当前响应对象 * @param { ControllerContext } handler 当前拦截待执行的函数相关信息 * @returns { boolean } * 返回值:true表示继续流程(如调用下一个拦截器或处理器);false表示流程中断(如登录检查失败),不会继续调用其他的拦截器或处理器,此时我们需要通过response来产生响应; */ preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: HandlerMethod): boolean { // 假设我们添加了一个UserLogin注解 const annotation = handler.getAnnotation(UserLogin); if (annotation) { const nativeAnnotation = annotation.nativeAnnotation; // 进行权限校验 } return true; } /** * 在处理完action后的拦截函数,可对执行完的接口进行处理 * @param { HttpRequest } request 当前请求对象 * @param { HttpResponse } response 当前响应对象 * @param { ControllerContext } handler 当前拦截待执行的函数相关信息 * @param { any } result 执行action返回的结果 */ postHandle(request: HttpServletRequest, response: HttpServletResponse, handler: HandlerMethod, result): void { } /** * 在请求结束后的拦截器 (无论成功还是失败都会执行此拦截函数) * (这里可以用于进行资源清理之类的工作) * @param { HttpRequest } request 当前请求对象 * @param { HttpResponse } response 当前响应对象 * @param { ControllerContext } handler 当前拦截待执行的函数相关信息 * @param { any } ex 如果执行action出现异常时,此参数会有值 */ afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: HandlerMethod, ex): void { } } ``` #### 第二步 启动时通过`addInterceptors`配置来注册拦截器。 ```js import { Registry } from 'node-web-mvc'; import AuthorizationInterceptor from './interceptors/AuthorizationInterceptor'; // 启动Mvc Registry.launch({ // ... 其他配置 // 通过配置来注册拦截器 addInterceptors(registry) { registry.addInterceptors(new AuthorizationInterceptor()) // registry // .addInterceptors(new AuthorizationInterceptor()) // .excludePathPatterns('/root/a','/root/b') // .addPathPatterns('/root') } }); ``` ## HttpMessageConverter 内容转换 框架内置了以下几种类型的请求内容转换 - JsonMessageConverter 将`application/json`的http.body正文转换成`json`对象. - UrlencodedMessageConverter 用于转换类型为`application/x-www-form-urlencoded`的请求内容。 - MultipartMessageConverter 用于转换类型为`multipart/form-data`的请求内容 如果您需要处理其他类型的请求内容,可以自定义一个转换器 ### 自定义Http转换器 #### 第一步 通过实现`HttpMessageConverter`接口来实现一个转换器 > XmlHttpMessageConverter.ts ```js import { MediaType, ServletContext, HttpMessageConverter,RequestMemoryStream } from 'node-web-mvc'; import xml2js from 'xml2js'; export default class XmlHttpMessageConverter implements HttpMessageConverter { /** * 判断当前转换器是否能处理当前内容类型 * @param mediaType 当前内容类型 例如: application/xml */ canRead(mediaType: MediaType): boolean { return mediaType.name === 'application/xml'; } /** * 判断当前内容是否能写 * @param mediaType 当前内容类型 例如: application/xml */ canWrite(mediaType: MediaType): boolean { return mediaType.name === 'application/xml'; } // getSupportedMediaTypes(): Array<string> /** * 读取当前消息内容 * @param servletRequest */ read(servletContext: ServletContext, mediaType: MediaType): any { return new Promise((resolve, reject) => { new RequestMemoryStream(servletContext.request, (buffers) => { xml2js.parseString(buffers.toString('utf8'), (err, data) => { err ? reject(err) : resolve(data); }); }); }) } /** * 写出当前内容 * @param data 当前数据 * @param mediaType 当前内容类型 * @param servletContext 当前请求上下文 */ write(data: any, mediaType: MediaType, servletContext: ServletContext) { return new Promise((resolve) => { const builder = new xml2js.Builder(); const xml = builder.buildObject(data); servletContext.response.write(xml, resolve); }) } } ``` #### 第二步 通过`addMessageConverters`将`XmlHttpMessageConverter`进行注册。 > Launch.ts ```js import { Registry } from 'node-web-mvc'; import XmlHttpMessageConverter from './interceptors/XmlHttpMessageConverter'; // 启动Mvc Registry.launch({ // ... 其他配置 // 注册XmlHttpMessageConverter addMessageConverters(converters) { converters.addMessageConverters(new XmlHttpMessageConverter()); } }); ``` #### 第三步 这样就可以在控制器中使用了 > DataController.ts ```js import { RequestMapping, PostMapping, RequestBody } from 'node-web-mvc'; @RequestMapping('/data') export default class DataController { // 这里:同时测试 读取xml 以及返回xml @PostMapping({ value: '/receieve', consumes: 'application/xml', produces: 'application/xml' }) receieve(@RequestBody data){ console.log('xml data',data); return data; } } ``` ## ArgumentResolver 参数解析 框架内置了以下几种类型的请求参数解析 - `@RequestParam` 提取类型为`urleoncoded`的参数 - `@RequestBody` 提取整个`body`内容,通常是提取成为一个`json`对象 - `@PathVariable` 提取路由中的占位参数 - `@RequestHeader` 提取请求头中的指定名的请求头做为参数 - `@ServletRequest` 用于提取`request` 对象 - `@ServletResponse` 用于提取`response` 对象 如果您需要处理其他类型的请求内容,可以自定义一个参数解析器 ### 自定义参数解析器 例如,以下实现通过 `UserId` 注解来提取当前登录用户id。 #### 第一步 定义一个`UserId`注解 > UserId.ts ```js import { Target, ElementType } from 'node-web-mvc'; class UserId { constructor(){ // 注解构造函数 } } // 公布注解 export default Target(ElementType.PARAMETER)(UserId); ``` #### 第二步 通过实现`HandlerMethodArgumentResolver`接口来实现一个解析器 > UserIdArgumentResolver.ts ```js import { ServletContext,MethodParameter, HandlerMethodArgumentResolver } from 'node-web-mvc'; import UserIdAnnotation from './UserIdAnnotation'; export default class UserIdArgumentResolver implements HandlerMethodArgumentResolver { supportsParameter(paramater: MethodParameter, servletContext: ServletContext) { return paramater.hasParameterAnnotation(UserIdAnnotation) } resolveArgument(parameter: MethodParameter, servletContext: ServletContext): any { const cookies = servletContext.request.cookies; const token = cookies.token; // 从token中解析出用户id return TokenService.decode(token).userId; } } ``` #### 第三步 通过`addArgumentResolvers`将`PathVariableMapMethodArgumentResolver`进行注册。 ```js import { Registry } from 'node-web-mvc'; import UserIdArgumentResolver from './UserIdArgumentResolver'; // 启动Mvc Registry.launch({ // ... 其他配置 // 注册 addArgumentResolvers(resolvers) { resolvers.addArgumentResolvers(new UserIdArgumentResolver()); } }); ``` #### 第四步 这样就可以在控制器中使用了 ```js import { RequestMapping, PostMapping } from 'node-web-mvc'; import UserId from './UserId'; @RequestMapping('/data') export default class DataController { @PostMapping('/home') receieve(@UserId id){ console.log('id',id); } } ``` ## 热更新 在启动时,可通过配置`hot`配置启用热更新服务, 在热更新服务下,控制器代码以及及依赖模块改动,无需重启服务器。 ### hot.preload 在修改一个文件时,会触发热更,在执行热更新前,会触发`preload`,如果您希望 您的某个依赖模块需要进行特定处理,则可以再该文件中订阅`hot.preload` > 例如: ControllerFactory.ts 再一些控制器模块修改时,需要进行一些前置处理 ```js import { hot } from 'node-web-mvc'; // 订阅preload hot.create(module).preload((old) => { // old 为当前即将进行热更新的模块旧模块,此时可以根据old来进行一些清理操作 }) ``` ### hot.accept 在模块热更新后,同此此函数来接受更新 ```js import { hot } from 'node-web-mvc'; // 订阅preload hot.create(module).preload((new,old) => { // new 为当前热更新后的新模块对象 // old 为热更新前的模块对象 }) ``` ## Swagger 框架支持swagger文档生成功能 可通过以下注解来完成文档元数据定义 - `@Api` 定义一个接口服务 ```js @Api({ description: '首页控制器' }) class HomeConntroller { } ``` - `@ApiOperation` 定义一个接口操作 ```js @Api({ description: '首页控制器' }) class HomeConntroller { @ApiOperation({ value: '首页列表数据', notes: '这是备注' }) index(){ } } ``` - `@ApiImplicitParams` 定义接口操作参数信息 > 如果不需要配置参数详细设定,一般可以不使用`ApiImplicitParams` 因为框架会自动根据每个参数的提取类型来自动生成swagger参数配置。 ```js @Api({ description: '首页控制器' }) class HomeConntroller { @ApiOperation({ value: '首页列表数据', notes: '这是备注',returnType:'返回数据类型' }) @GetMapping('/index') index(){ } @ApiOperation({ value: '上传文件', notes: '上传证书文件' }) @ApiImplicitParams([ { value: 'file', desc: '证书', required: true, dataType: MultipartFile }, { value: 'desc', desc: '描述', required: true, paramType: 'formData' }, { value: 'id', desc: '用户id', required: true } ]) @PostMapping('/upload') upload(file: MultipartFile,@RequestParam desc,@RequestParam id) { return file.transferTo('appdata/images/' + file.name); } } ``` - `@ApiModel` 定义一个实体类 ```js @ApiModel({ value: '用户信息', description: '用户信息。。' }) export default class UserInfo { } ``` - `@ApiModelProperty` 定义实体类属性 ```js @ApiModel({ value: '用户信息', description: '用户信息。。' }) export default class UserInfo { @ApiModelProperty({ value: '用户名', required: true, example: '张三' }) public userName: string @ApiModelProperty({ value: '用户编码', required: true, example: 1 }) public userId: number } ```