node-web-mvc v3.3.30
创建应用
npm create node-web-mvc@latestspring风格
index.ts
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
import { WebMvcConfigurationSupport } from 'node-web-mvc';
@Configuration
export default class WebAppConfigurer extends WebMvcConfigurationSupport {
  // 这里可以扩展配置...
}Controller 控制器
完成启动配置后,可以在控制器目录下定义对应的控制器
控制器的定义风格和Spring Mvc风格一致。
例如:
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
@RequestMapping('/home')
class HomeController { 
}详细配置
通过传入一个对象RequestMapping来进行详细映射。
以下例子通过
@RequestMapping配置 允许在GET方式下通过/home/index路由来访问HomeController的index函数
@RequestMapping('/home')
class HomeController { 
  @RequestMapping({ value:'/index',method:'GET' })
  index(){
    return 'Hi i am home index';
  }
}在大多数情况下,我们只会配置路由与请求类型 可以通过以下几个注解来进行快捷配置。
@GetMapping映射一个method为GET的请求
@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 等注解配置路由时,可以有以下几种配置风格
普通路由
@GetMapping('/detail/index')参数占位类型
使用
{}来标识占位
通过占位映射的路由参数,可以通过@PathVariable 注解来提取
@GetMapping('/detail/{id}')
正则风格路由@GetMapping('/route/{}')
Arguments 参数提取
我们可以通过以下几个注解来定义请求参数的提取方式。
@RequestParam提取类型为urleoncoded的参数@RequestBody提取整个body内容,通常是提取成为一个json对象@PathVariable提取路由中的占位参数@RequestHeader提取请求头中的指定名的请求头做为参数@ServletRequest用于提取request对象@ServletResponse用于提取response对象
RequestParam
从urleoncoded的内容中提取指定名称的参数
@RequestParam 作为参数注解,不进行任何配置,默认会以参数名来作为提取名依据
@RequestMapping('/home')
class HomeController { 
  @GetMapping('/index')
  index(@RequestParam name){
    return `Hi ${name}, i am home index`;
  }
}同时@RequestParam 也可以进行详细配置ParamAnnotation
例如: 将url中传递过来的
userName值提取给index中的name参数
@RequestMapping('/home')
class HomeController { 
  @GetMapping('/index')
  index(@RequestParam({ value:'userName', required:true }) name){
    return `Hi ${name}, i am home index`;
  }
}文件上传参数提取
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对象
@RequestMapping('/order')
class OrderController { 
  @GetMapping('/save')
  saveOrder(@RequestBody order){
    console.log(order);
  }
}PathVariable
从请求路由中提取路径参数
@RequestMapping('/order')
class OrderController { 
  @GetMapping('/detail/:id')
  detail(@PathVariable id){
    return `Order ${id}`;
  }
}RequestHeader
从请求头中提取参数
@RequestMapping('/home')
class HomeController { 
  @GetMapping('/index')
  detail(@RequestHeader('content-type') ct){
    return `content-type: ${ct}`;
  }
}ServletRequest
提取request整个对象。
@RequestMapping('/home')
class HomeController { 
  @GetMapping('/index')
  detail(@ServletRequest request){
    
  }
}ServletResponse
提取response整个对象。
@RequestMapping('/home')
class HomeController { 
  @GetMapping('/index')
  detail(@ServletResponse response){
    
  }
}Responsee 返回内容
在控制器具体函数中,我们可以返回以下几种类型来将内容返回到客户端。
ModelAndView 返回一个视图
String 返回一个字符串
Object 如果需要正常返回,需要通过
RequestMapping指定produces为application/jsonPromise 返回一个异步结果
Middlewares 返回一个类express的中间件执行结果
import { RequestMapping, GetMapping, Middlewares } from 'node-web-mvc';
@RequestMapping('/home')
class HomeController {
  @GetMapping('/index')
  index(){
    return new Middlewares([
      (req,resp,next)=> next()
    ])
  }
}@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 }
    ];
  }
}异常处理
框架可以通过以下两个注解来进行控制器异常处理
ExceptionHandlerControllerAdvice
ExceptionHandler
如果将ExceptionHandler标注在控制器的函数上,则表示当前控制器的函数执行异常时,会使用当前标注的函数来进行异常处理。
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
@ControllerAdvice
class AppException {
  @ExceptionHandler
  handleException(error){
    // 返回一个 json 异常对象
    return { code:error.code,message:error.message };
  }
}Resource 静态资源
框架也提供了静态资源服务,以及针对静态资源设定缓存策略等,同时也支持gzip压缩处理。
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
/**
 * @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的视图
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配置来注册视图解析器。
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
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配置来注册拦截器。
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
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
import { Registry } from 'node-web-mvc';
import XmlHttpMessageConverter from './interceptors/XmlHttpMessageConverter';
// 启动Mvc  
Registry.launch({
  // ... 其他配置
  // 注册XmlHttpMessageConverter
  addMessageConverters(converters) {
    converters.addMessageConverters(new XmlHttpMessageConverter());
  }
});第三步
这样就可以在控制器中使用了
DataController.ts
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
import { Target, ElementType } from 'node-web-mvc';
class UserId {
  constructor(){
    // 注解构造函数
  }
}
// 公布注解
export default Target(ElementType.PARAMETER)(UserId);第二步
通过实现HandlerMethodArgumentResolver接口来实现一个解析器
UserIdArgumentResolver.ts
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进行注册。
import { Registry } from 'node-web-mvc';
import UserIdArgumentResolver from './UserIdArgumentResolver';
// 启动Mvc  
Registry.launch({
  // ... 其他配置
  // 注册
  addArgumentResolvers(resolvers) {
    resolvers.addArgumentResolvers(new UserIdArgumentResolver());
  }
});第四步
这样就可以在控制器中使用了
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 再一些控制器模块修改时,需要进行一些前置处理
import { hot } from 'node-web-mvc';
// 订阅preload
hot.create(module).preload((old) => {
  // old 为当前即将进行热更新的模块旧模块,此时可以根据old来进行一些清理操作
})hot.accept
在模块热更新后,同此此函数来接受更新
import { hot } from 'node-web-mvc';
// 订阅preload
hot.create(module).preload((new,old) => {
  // new 为当前热更新后的新模块对象
  // old 为热更新前的模块对象
})Swagger
框架支持swagger文档生成功能
可通过以下注解来完成文档元数据定义
@Api定义一个接口服务
@Api({ description: '首页控制器' })
class HomeConntroller {
}@ApiOperation定义一个接口操作
@Api({ description: '首页控制器' })
class HomeConntroller {
  @ApiOperation({ value: '首页列表数据', notes: '这是备注' })
  index(){
  }
}@ApiImplicitParams定义接口操作参数信息
如果不需要配置参数详细设定,一般可以不使用
ApiImplicitParams因为框架会自动根据每个参数的提取类型来自动生成swagger参数配置。
@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定义一个实体类
@ApiModel({ value: '用户信息', description: '用户信息。。' })
export default class UserInfo {
}@ApiModelProperty定义实体类属性
@ApiModel({ value: '用户信息', description: '用户信息。。' })
export default class UserInfo {
  @ApiModelProperty({ value: '用户名', required: true, example: '张三' })
  public userName: string
  @ApiModelProperty({ value: '用户编码', required: true, example: 1 })
  public userId: number
}9 months ago
10 months ago
10 months ago
10 months ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago