node-web-mvc
Version:
node spring mvc
951 lines (681 loc) • 23.3 kB
Markdown
<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';
注意:在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';
export default class WebAppConfigurer extends WebMvcConfigurationSupport {
// 这里可以扩展配置...
}
```
## Controller 控制器
完成启动配置后,可以在控制器目录下定义对应的控制器
控制器的定义风格和`Spring Mvc`风格一致。
> 例如:
```js
import { RestController, RequestMapping, GetMapping } from 'node-web-mvc';
class HomeController {
index(){
return 'Hi i am home index';
}
}
```
更多的控制器配置,我们可以阅读后面通过注解来完善控制器。
## Route 路由映射
### ``
该注解用于将请求映射到指定控制器。
有两种使用方式
#### 简要模式
仅配置访问路径,例如: 以下例子中,仅配置 以 `/home` 来访问`HomeController`
```js
class HomeController {
}
```
#### 详细配置
通过传入一个对象[`RequestMapping`](#RequestMapping)来进行详细映射。
> 以下例子通过``配置 允许在`GET`方式下通过`/home/index`路由来访问 `HomeController`的`index`函数
```js
class HomeController {
index(){
return 'Hi i am home index';
}
}
```
在大多数情况下,我们只会配置`路由`与`请求类型` 可以通过以下几个注解来进行快捷配置。
- `` 映射一个`method`为 `GET`的请求
```js
class HomeController {
index(){
return 'Hi i am home index';
}
}
```
- `` 映射一个`method`为 `POST`的请求
- `` 映射一个`method`为 `PUT`的请求
- `` 映射一个`method`为 `DELETE`的请求
- `` 映射一个`method`为 `PATCH`的请求
#### 路由风格
通过 `` 等注解配置路由时,可以有以下几种配置风格
- `普通`路由
```js
```
- `参数占位`类型
> 使用 `{}` 来标识占位
通过`占位`映射的路由参数,可以通过[``](#PathVariable) 注解来提取
```js
```
> `正则`风格路由
```js
```
## Arguments 参数提取
我们可以通过以下几个注解来定义请求参数的提取方式。
- `` 提取类型为`urleoncoded`的参数
- `` 提取整个`body`内容,通常是提取成为一个`json`对象
- `` 提取路由中的占位参数
- `` 提取请求头中的指定名的请求头做为参数
- `` 用于提取`request` 对象
- `` 用于提取`response` 对象
### RequestParam
从`urleoncoded`的内容中提取指定名称的参数
`` 作为参数注解,不进行任何配置,默认会以参数名来作为提取名依据
```js
class HomeController {
index( name){
return `Hi ${name}, i am home index`;
}
}
```
同时`` 也可以进行详细配置[`ParamAnnotation`](#ParamAnnotation)
> 例如: 将url中传递过来的`userName`值提取给`index`中的 `name`参数
```js
class HomeController {
index( name){
return `Hi ${name}, i am home index`;
}
}
```
文件上传参数提取
```js
import { MultipartFile } from 'node-web-mvc';
class HomeController {
// 单个文件上传
// 配置swagger 生成上传表单
async index( file: MultipartFile, id){
// 保存文件
await file.transferTo('appqdata/images/' + file.name);
return {
code:0,
message:'上传成功'
}
}
// 多个文件上传
async index( files: Array<MultipartFile>){
// 保存文件
for (let file of files) {
await file.transferTo('appdata/images/' + file.name)
}
return {
code:0,
message:'上传成功'
}
}
}
```
### RequestBody
提取整个`body`内容,通常是提取成为一个`json`对象
```js
class OrderController {
saveOrder( order){
console.log(order);
}
}
```
### PathVariable
从请求路由中提取路径参数
```js
class OrderController {
detail( id){
return `Order ${id}`;
}
}
```
### RequestHeader
从请求头中提取参数
```js
class HomeController {
detail( ct){
return `content-type: ${ct}`;
}
}
```
### ServletRequest
提取`request`整个对象。
```js
class HomeController {
detail( request){
}
}
```
### ServletResponse
提取`response`整个对象。
```js
class HomeController {
detail( response){
}
}
```
## Responsee 返回内容
在控制器具体函数中,我们可以返回以下几种类型来将内容返回到客户端。
- ModelAndView 返回一个视图
- String 返回一个字符串
- Object 如果需要正常返回,需要通过`RequestMapping`指定produces为`application/json`
- Promise 返回一个异步结果
- Middlewares 返回一个类express的中间件执行结果
```js
import { RequestMapping, GetMapping, Middlewares } from 'node-web-mvc';
class HomeController {
index(){
return new Middlewares([
(req,resp,next)=> next()
])
}
}
```
```js
class HomeController {
index(){
return new ModelAndView('home/index');
}
strings(){
return `output :String`;
}
list(){
return [
{ name:'张三',id:100 }
];
}
}
```
## 异常处理
框架可以通过以下两个注解来进行控制器异常处理
- `ExceptionHandler`
- `ControllerAdvice`
### ExceptionHandler
如果将`ExceptionHandler`标注在控制器的函数上,则表示当前控制器的函数执行异常时,会使用当前标注的函数来进行异常处理。
```js
import { GetMapping, RequestMapping, ExceptionHandler } from 'node-web-mvc';
export default class HomeController {
index(){
throw new Error('error');
}
handleException(error){
// 返回一个 json 异常对象
return { code:error.code,message:error.message };
}
}
```
### ControllerAdvice
利用`ControllerAdvice` 来进行全局异常控制
定义一个异常处理类,然后使用`ControllerAdvice`标注当前类为全局控制器处理,
最后在该类上定义一个异常处理函数,然后通过`ExceptionHandler`标注成异常处理函数。
例如:
> AppException.ts
```js
class AppException {
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';
export default class DataController {
// 这里:同时测试 读取xml 以及返回xml
receieve( data){
console.log('xml data',data);
return data;
}
}
```
## ArgumentResolver 参数解析
框架内置了以下几种类型的请求参数解析
- `` 提取类型为`urleoncoded`的参数
- `` 提取整个`body`内容,通常是提取成为一个`json`对象
- `` 提取路由中的占位参数
- `` 提取请求头中的指定名的请求头做为参数
- `` 用于提取`request` 对象
- `` 用于提取`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';
export default class DataController {
receieve( 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文档生成功能
可通过以下注解来完成文档元数据定义
- `` 定义一个接口服务
```js
class HomeConntroller {
}
```
- `` 定义一个接口操作
```js
class HomeConntroller {
index(){
}
}
```
- `` 定义接口操作参数信息
> 如果不需要配置参数详细设定,一般可以不使用`ApiImplicitParams` 因为框架会自动根据每个参数的提取类型来自动生成swagger参数配置。
```js
class HomeConntroller {
index(){
}
upload(file: MultipartFile, desc, id) {
return file.transferTo('appdata/images/' + file.name);
}
}
```
- `` 定义一个实体类
```js
export default class UserInfo {
}
```
- `` 定义实体类属性
```js
export default class UserInfo {
public userName: string
public userId: number
}
```