routing-controllers简介

routing-controllers是一个基于express/koa的Node.js框架,它提供了非常多的装饰器,可以使开发者以一种“依赖注入”的方式编写controllers。

routing-controllers源码地址:点击此处

routing-controllers工作原理

1、一些铺垫

首先,我们这里以routing-controllers和koa框架两者组合编写web服务为例,样例代码如下:

app.ts

import 'reflect-metadata'
import { createKoaServer } from 'routing-controllers'
import { UserController } from './UserController'const app = createKoaServer({controllers: [UserController] // we specify controllers we want to use
}) // register controllers routes in our koa application
app.listen(3001) // run koa appconsole.log('koa server is running on port 3001. Open http://localhost:3001/users/')

UserController.ts

import "reflect-metadata";
import {Request} from "express";
import {Controller} from "../../src/decorator/Controller";
import {Get} from "../../src/decorator/Get";
import {Req} from "../../src/index";
import {Post} from "../../src/decorator/Post";
import {Put} from "../../src/decorator/Put";
import {Patch} from "../../src/decorator/Patch";
import {Delete} from "../../src/decorator/Delete";
import {ContentType} from "../../src/decorator/ContentType";@Controller()
export class UserController {@Get("/users")@ContentType("application/json")getAll() {return [{ id: 1, name: "First user!" },{ id: 2, name: "Second user!" }];}@Get("/users/:id")getOne(@Req() request: Request) {return "User #" + request.params.id;}@Post("/users")post(@Req() request: Request) {let user = JSON.stringify(request.body); // probably you want to install body-parser for expressreturn "User " + user + " !saved!";}@Put("/users/:id")put(@Req() request: Request) {return "User #" + request.params.id + " has been putted!";}@Patch("/users/:id")patch(@Req() request: Request) {return "User #" + request.params.id + " has been patched!";}@Delete("/users/:id")remove(@Req() request: Request) {return "User #" + request.params.id + " has been removed!";}}

2、MetadataArgsStorage

那么routing-controllers框架是如何工作的呢?
我相信有很多朋友在了解routing-controllers工作流程时,会直接把断点打在上面app.ts文件中的createKoaServer函数上,然后一步一步按F11去了解routing-controllers是如何注册Controllers,以及新建koa server的。

但这样的话你会漏掉很多东西!
你会漏掉routing-controllers这个框架中最基础的一部分——MetadataArgsStorage
以及在routing-controllers这个框架中是如何利用TypeScript中的decorator语法,来实现res、req等等参数,或get、post等方法的注入的。

2-1、TypeScript装饰器

首先,我们要简单补充一些TypeScript装饰器的知识:
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入,例如routing-controllers中的Get装饰器:

export function Get(route?: string|RegExp): Function {return function (object: Object, methodName: string) {// 装饰器实现代码};
}@Get("/users")
getAll() {// 使用get请求访问/users后的responsereturn [{ id: 1, name: "First user!" },{ id: 2, name: "Second user!" }];
}

Get函数会在getAll函数运行前执行,routing-controllers就是通过大量使用装饰器,将get、post等方法以及res、rep等参数注入到controller的函数中的。

2-2、TypeScript装饰器种类及执行顺序

TypeScript装饰器有如下几种:
1、类装饰器,用于类的构造函数(下面例子中的ClassDecorator)。
2、方法装饰器,用于方法的属性描述符上(下面例子中的MethodDecorator)。
3、方法参数装饰器,用于方法的参数上(下面例子中的Param1Decorator)。
4、属性装饰器,用于类的属性上(下面例子中的PropertyDecorator)。

举个例子:

function ClassDecorator() {return function (target) {console.log("I am class decorator");}
}
function MethodDecorator() {return function (target, methodName: string, descriptor: PropertyDescriptor) {console.log("I am method decorator");}
}
function Param1Decorator() {return function (target, methodName: string, paramIndex: number) {console.log("I am parameter1 decorator");}
}
function Param2Decorator() {return function (target, methodName: string, paramIndex: number) {console.log("I am parameter2 decorator");}
}
function PropertyDecorator() {return function (target, propertyName: string) {console.log("I am property decorator");}
}@ClassDecorator()
class Hello {@PropertyDecorator()greeting: string;@MethodDecorator()greet( @Param1Decorator() p1: string, @Param2Decorator() p2: string) { }
}

具体执行结果及执行顺序是:

I am property decorator
I am parameter2 decorator
I am parameter1 decorator
I am method decorator
I am class decorator

有多个参数装饰器时,从最后一个参数依次向前执行。
方法装饰器和方法参数装饰器中方法参数装饰器先执行。
类装饰器总是最后执行。
方法和属性装饰器,谁在前面谁先执行。因为参数属于方法一部分,所以参数会一直紧紧挨着方法执行。

2-3、routing-controllers装饰器实现细节及MetadataArgsStorage

我们以routing-controllers中的Get装饰器为例,其具体实现逻辑在源码中src/decorator/Get.ts文件中:

export function Get(route?: string|RegExp): Function {return function (object: Object, methodName: string) {getMetadataArgsStorage().actions.push({type: "get",target: object.constructor,method: methodName,route: route});};
}

而Get装饰器是这样用的:

@Controller()
export class UserController {@Get("/users")getAll() {return [{ id: 1, name: "First user!" },{ id: 2, name: "Second user!" }];}
}

从上面的代码,我们不难看出Get装饰器做了一个很简单的事情,将路由(/users)、方法名(getAll)、该方法所在的controller的构造函数(UserController.constructor)添加到MetadataArgsStorage的actions中。

2-4、MetadataArgsStorage实现细节

那么肯定有朋友会问了,MetadataArgsStorage是什么?他是做什么用的?具体技术细节又是如何呢?
别着急,我们一个一个来解答~

2-4-1、单例模式在MetadataArgsStorage中的应用

首先在src/index.ts文件中,我们可以找到getMetadataArgsStorage函数的实现:

export function getMetadataArgsStorage(): MetadataArgsStorage {if (!(global as any).routingControllersMetadataArgsStorage)(global as any).routingControllersMetadataArgsStorage = new MetadataArgsStorage();return (global as any).routingControllersMetadataArgsStorage;
}

不难看出,在这里我们先判断global对象中是否有routingControllersMetadataArgsStorage属性,若有则直接返回,若没有则新建一个routingControllersMetadataArgsStorage。这就保证了在global对象下永远只有一个routingControllersMetadataArgsStorage,是一种懒汉式单例的实现方式。

2-4-2、MetadataArgsStorage的作用

我们首先来看看MetadataArgsStorage的结构,这部分的实现在src/metadata-builder/MetadataArgsStorage.ts文件下,我们可以使用getMetadataArgsStorage方法来直接获得MetadataArgsStorage对象,下面是样例代码中的MetadataArgsStorage对象:

MetadataArgsStorage {controllers:[ { type: 'default',target: [Function: UserController],route: undefined } ],middlewares: [],interceptors: [],uses: [],useInterceptors: [],actions:[ { type: 'get',target: [Function: UserController],method: 'getAll',route: '/users' },{ type: 'get',target: [Function: UserController],method: 'getOne',route: '/users/:id' },{ type: 'post',target: [Function: UserController],method: 'post',route: '/users' },{ type: 'put',target: [Function: UserController],method: 'put',route: '/users/:id' },{ type: 'patch',target: [Function: UserController],method: 'patch',route: '/users/:id' },{ type: 'delete',target: [Function: UserController],method: 'remove',route: '/users/:id' } ],params:[ { type: 'request',object: [UserController],method: 'getOne',index: 0,parse: false,required: false },{ type: 'request',object: [UserController],method: 'post',index: 0,parse: false,required: false },{ type: 'request',object: [UserController],method: 'put',index: 0,parse: false,required: false },{ type: 'request',object: [UserController],method: 'patch',index: 0,parse: false,required: false },{ type: 'request',object: [UserController],method: 'remove',index: 0,parse: false,required: false } ],responseHandlers:[ { type: 'content-type',target: [Function: UserController],method: 'getAll',value: 'application/json' } ] }

我们可以看到通过装饰器,routing-controllers已经将UserController中的所有信息提取出来,存入MetadataArgsStorage中了。

3、createKoaServer函数

既然routing-controllers已经将我们编写的所有controller中的信息提取并存入MetadataArgsStorage中了,那么接下来就应该新建server对象并将这些controller注入到server当中了。
我们可以看到,在样例代码的app.ts中,有一个createKoaServer函数,它初始化并返回了app对象,那么它具体做了什么呢?我们可以关注源代码中的src/index.ts文件,简化后的代码如下:

/*** 创建koa服务器,并加载所有的action*/
export function createKoaServer(options?: RoutingControllersOptions): any {const driver = new KoaDriver();return createServer(driver, options);
}/*** 加载所有的action到指定的服务器*/
export function createServer<T extends BaseDriver>(driver: T, options?: RoutingControllersOptions): any {createExecutor(driver, options);return driver.app;
}/*** 加载所有的action*/
export function createExecutor<T extends BaseDriver>(driver: T, options: RoutingControllersOptions = {}): void {// 在这里导入所有的controllers、中间件、错误处理器(代码不重要,略过)...// 创建一个RoutingControllers对象,处理所有传入的controllernew RoutingControllers(driver, options).initialize().registerInterceptors(interceptorClasses).registerMiddlewares("before", middlewareClasses).registerControllers(controllerClasses).registerMiddlewares("after", middlewareClasses);
}

不难看出,createKoaServer函数其实做了两件事:
1、初始化一个KoaDriver对象,并返回driver.app。
2、初始化一个RoutingControllers对象,传入KoaDriver对象和配置项,并执行Interceptors、Middleware、Controller的注册。

接下来我们将按照顺序分别解读在这两个对象中,routing-controllers是如何做的。

3-1、KoaDriver对象

/*** Integration with koa framework.*/
export class KoaDriver extends BaseDriver {constructor(public koa?: any, public router?: any) {super();this.loadKoa();this.loadRouter();this.app = this.koa;}/*** 初始化server*/initialize() {const bodyParser = require("koa-bodyparser");this.koa.use(bodyParser());if (this.cors) {const cors = require("kcors");if (this.cors === true) {this.koa.use(cors());} else {this.koa.use(cors(this.cors));}}}/*** 注册中间件*/registerMiddleware(middleware: MiddlewareMetadata): void {if ((middleware.instance as KoaMiddlewareInterface).use) {this.koa.use(function (ctx: any, next: any) {return (middleware.instance as KoaMiddlewareInterface).use(ctx, next);});}}/*** 注册action*/registerAction(actionMetadata: ActionMetadata, executeCallback: (options: Action) => any): void {// ...一些处理action的逻辑const uses = actionMetadata.controllerMetadata.uses.concat(actionMetadata.uses);const beforeMiddlewares = this.prepareMiddlewares(uses.filter(use => !use.afterAction));const afterMiddlewares = this.prepareMiddlewares(uses.filter(use => use.afterAction));const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute);const routeHandler = (context: any, next: () => Promise<any>) => {const options: Action = {request: context.request, response: context.response, context, next};return executeCallback(options);};// 将所有action注册到koa中this.router[actionMetadata.type.toLowerCase()](...[route,...beforeMiddlewares,...defaultMiddlewares,routeHandler,...afterMiddlewares]);}/*** 注册路由*/registerRoutes() {this.koa.use(this.router.routes());this.koa.use(this.router.allowedMethods());}/*** 动态加载koa*/protected loadKoa() {if (require) {if (!this.koa) {try {this.koa = new (require("koa"))();} catch (e) {throw new Error("koa package was not found installed. Try to install it: npm install koa@next --save");}}} else {throw new Error("Cannot load koa. Try to install all required dependencies.");}}/*** 动态加载koa-router*/private loadRouter() {if (require) {if (!this.router) {try {this.router = new (require("koa-router"))();} catch (e) {throw new Error("koa-router package was not found installed. Try to install it: npm install koa-router@next --save");}}} else {throw new Error("Cannot load koa. Try to install all required dependencies.");}}...
}

从上述代码中,我们不难看出,KoaDriver对象在初始化时,会加载koa、koa-router两个模块,并提供initialize、registerRoutes、registerRoutes、registerRoutes等方法供RoutingController对象调用。

3-2、RoutingController对象

export class RoutingControllers<T extends BaseDriver> {/*** 用于处理controller中action的参数*/private parameterHandler: ActionParameterHandler<T>;/*** 用于新建metadata对象*/private metadataBuilder: MetadataBuilder;/*** 全局拦截器*/private interceptors: InterceptorMetadata[] = [];constructor(private driver: T, private options: RoutingControllersOptions) {this.parameterHandler = new ActionParameterHandler(driver);this.metadataBuilder = new MetadataBuilder(options);}/*** 初始化driver*/initialize(): this {...}/*** 注册拦截器*/registerInterceptors(classes?: Function[]): this {...}/*** 注册controller中的action*/registerControllers(classes?: Function[]): this {...}/*** 注册server的中间件*/registerMiddlewares(type: "before"|"after", classes?: Function[]): this {...}/*** 执行controller中的action.*/protected executeAction(actionMetadata: ActionMetadata, action: Action, interceptorFns: Function[]) {...}/*** 处理action的执行结果*/protected handleCallMethodResult(result: any, action: ActionMetadata, options: Action, interceptorFns: Function[]): any {...}/*** 创建拦截器*/protected prepareInterceptors(uses: InterceptorMetadata[]): Function[] {...}
}

从上述代码中,我们可以看出,在初始化RoutingControllers对象时,首先新建了一个ActionParameterHandler对象,以及一个MetadataBuilder对象。

ActionParameterHandler用于处理controller中action的参数。
MetadataBuilder会根据MetadataArgsStorage中的数据新建Metadata对象。

3-2-1、RoutingController对象中的initialize方法
    initialize(): this {this.driver.initialize();return this;}

在initialize方法中,我们调用了传入的driver对象中的initialize方法,根据“3-1、KoaDriver对象”此节中的内容,我们可以知道其实该方法首先加载了koa-bodyparser模块,并根据跨域配置按需加载kcors模块。

3-2-2、RoutingController对象中的registerInterceptors方法
    registerInterceptors(classes?: Function[]): this {const interceptors = this.metadataBuilder.buildInterceptorMetadata(classes).sort((middleware1, middleware2) => middleware1.priority - middleware2.priority).reverse();this.interceptors.push(...interceptors);return this;}

在registerInterceptors方法中,我们使用metadataBuilder对象创建了拦截器数组,并赋值给RoutingController对象中的interceptors属性。

而其中使用的buildInterceptorMetadata方法的具体逻辑在src/metadata-builder/MetadataBuilder.ts中,具体如下:

    buildInterceptorMetadata(classes?: Function[]): InterceptorMetadata[] {return this.createInterceptors(classes);}protected createInterceptors(classes?: Function[]): InterceptorMetadata[] {const interceptors = !classes ? getMetadataArgsStorage().interceptors : getMetadataArgsStorage().filterInterceptorMetadatasForClasses(classes);return interceptors.map(interceptorArgs => new InterceptorMetadata({target: interceptorArgs.target,method: undefined,interceptor: interceptorArgs.target}));}

实际上就是从MetadataArgsStorage中过滤出Interceptor,并返回对应的InterceptorMetadata数组。=(PS:后续的buildMiddlewareMetadata、buildControllerMetadata实现和InterceptorMetadata类似,就不展开介绍了)=

3-2-2、RoutingController对象中的registerControllers方法
    registerControllers(classes?: Function[]): this {const controllers = this.metadataBuilder.buildControllerMetadata(classes);controllers.forEach(controller => {controller.actions.forEach(actionMetadata => {const interceptorFns = this.prepareInterceptors([...this.interceptors,...actionMetadata.controllerMetadata.interceptors,...actionMetadata.interceptors]);this.driver.registerAction(actionMetadata, (action: Action) => {return this.executeAction(actionMetadata, action, interceptorFns);});});});this.driver.registerRoutes();return this;}

registerControllers方法是routing-controllers框架中很重要的一个方法,它将controller中的action注册到了KoaServer实例(driver对象)中。

首先,registerControllers方法使用metadataBuilder对象,根据MetadataArgsStorage中的数据创建了controllers对象,然后针对controller中action属性的每一项,调用KoaServer实例(driver对象)中的registerAction方法,该方法的实现如下:

    /*** 注册action*/registerAction(actionMetadata: ActionMetadata, executeCallback: (options: Action) => any): void {// ...一些处理action的逻辑const uses = actionMetadata.controllerMetadata.uses.concat(actionMetadata.uses);const beforeMiddlewares = this.prepareMiddlewares(uses.filter(use => !use.afterAction));const afterMiddlewares = this.prepareMiddlewares(uses.filter(use => use.afterAction));const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute);const routeHandler = (context: any, next: () => Promise<any>) => {const options: Action = {request: context.request, response: context.response, context, next};return executeCallback(options);};// 将所有action注册到koa中this.router[actionMetadata.type.toLowerCase()](...[route,...beforeMiddlewares,...defaultMiddlewares,routeHandler,...afterMiddlewares]);}

该方法实际上是将controller中诸如@Get(’/user’)之类的逻辑全部注册到了router对象中,然后注册到KoaServer实例中。

当我们把这些路由全部注册完毕后,又是如何执行的呢?这里我们就要关注executeAction方法了:

 protected executeAction(actionMetadata: ActionMetadata, action: Action, interceptorFns: Function[]) {// compute all parametersconst paramsPromises = actionMetadata.params.sort((param1, param2) => param1.index - param2.index).map(param => this.parameterHandler.handle(action, param));// after all parameters are computedreturn Promise.all(paramsPromises).then(params => {// execute action and handle resultconst allParams = actionMetadata.appendParams ? actionMetadata.appendParams(action).concat(params) : params;const result = actionMetadata.methodOverride ? actionMetadata.methodOverride(actionMetadata, action, allParams) : actionMetadata.callMethod(allParams);return this.handleCallMethodResult(result, actionMetadata, action, interceptorFns);}).catch(error => {// otherwise simply handle error without action executionreturn this.driver.handleError(error, actionMetadata, action);});}

没错,它实际上就是使用了一个Promise.all,分别执行了@Get(’/user’)对应的方法,以及刚才在interceptor中注册的拦截器方法。

3-2-3、RoutingController对象中的registerMiddlewares方法
    registerMiddlewares(type: "before"|"after", classes?: Function[]): this {this.metadataBuilder.buildMiddlewareMetadata(classes).filter(middleware => middleware.global && middleware.type === type).sort((middleware1, middleware2) => middleware2.priority - middleware1.priority).forEach(middleware => this.driver.registerMiddleware(middleware));return this;}

在registerMiddleware方法中,我们使用metadataBuilder对象创建了拦截器数组,并调用KoaServer实例(driver对象)中的registerMiddleware方法,该方法在“3-1、KoaDriver对象”一节中有提到,具体代码如下:

    registerMiddleware(middleware: MiddlewareMetadata): void {if ((middleware.instance as KoaMiddlewareInterface).use) {this.koa.use(function (ctx: any, next: any) {return (middleware.instance as KoaMiddlewareInterface).use(ctx, next);});}}

实际上就是调用了koa中的use方法。

4、一些铺垫

所以,综上,routing-controllers工作原理可以简单概括为:

1、执行controller文件中的decorator逻辑,将controller中的数据存入MetadataArgsStorage对象,该对象是一个单例对象,是global对象的一个属性。
2、创建对应的express或koa的Driver对象。
3、创建RoutingControllers对象,并根据MetadataBuilder从MetadataArgsStorage中创建对应的InterceptorsMetadata、ControllersMetadata、MiddlewaresMetadata,然后根据上述Metadata分别注册interceptors、controllers的action、middlewares至对应的express或koa实例中。
4、使用app.listen开启express或koa服务。

routing-controllers工作原理解析相关推荐

  1. 【深度学习】谷歌大脑EfficientNet的工作原理解析

    [深度学习]谷歌大脑EfficientNet的工作原理解析 文章目录 1 知识点准备1.1 卷积后通道数目是怎么变多的1.2 EfficientNet 2 结构2.1 方式2.2 MBConv卷积块2 ...

  2. 揭开SAP Fiori编程模型规范里注解的神秘面纱 - @OData.publish工作原理解析

    Jerry的前一篇文章 揭开SAP Fiori编程模型规范里注解的神秘面纱 - @ObjectModel.readOnly工作原理解析,给大家分享了@ObjectModel.readOnly这个注解对 ...

  3. 2 计算机控制器的组成,组合逻辑控制器组成结构及工作原理解析

    组合逻辑控制器组成结构及工作原理解析 按照控制信号产生的方式不同,控制器分为微程序控制器和组合逻辑控制器两类 微程序控制器是将全部控制信号存贮在控制存储器中. 优点:控制信号的逻辑设计.实现及改动都较 ...

  4. 交换机原理_交换机工作原理解析

    原文连接:http://www.elecfans.com/dianzichangshi/20171204593673.html 交换机原理 数据传输基于OSI七层模型,而交换机就工作于其第二层,即数据 ...

  5. 六轴机械臂控制原理图_六轴工业机器人工作原理解析

    原标题:六轴工业机器人工作原理解析 常见的六轴关节机器人的机械结构如图1所示: 六个伺服电机直接通过谐波减速器.同步带轮等驱动六个关节轴的旋转,注意观察一.二.三.四轴的结构,关节一至关节四的驱动电机 ...

  6. Retrofit2 工作原理解析(一)

    Retrofit2 工作原理解析(一) 概述 Retrofit是square公司开源的一款类型安全的http请求框架,用于Java和Android程序.Retrofit可以说是restful风格的一个 ...

  7. MSN,QQ,IP Messenger,飞鸽传书,的工作原理解析

    MSN,QQ,飞鸽传书,的工作原理解析 http://apps.hi.baidu.com/share/detail/14190263 关键字:MSN,QQ,飞鸽传书,IP Messenger,传文件, ...

  8. iommu 工作原理解析之dma remapping

    深入了解iommu系列二:iommu 工作原理解析之dma remapping: https://zhuanlan.zhihu.com/p/479963917

  9. 基于TensorFolw的人工智能影像诊断平台工作原理解析

    文章对TensorFolw人工智能影像诊断平台的工作原理进行了解析,希望这篇文章能够帮助你更好地理解 Tensorflow. 使用人工智能来辅助病理医生对样本进行诊断,不仅能够大幅度提高医师的诊断效率 ...

  10. Servlet 工作原理解析

    2019独角兽企业重金招聘Python工程师标准>>> 从 Servlet 容器说起 要介绍 Servlet 必须要先把 Servlet 容器说清楚,Servlet 与 Servle ...

最新文章

  1. ideal如何快速导入import_Spring的@Import注解详解
  2. 第五期 IP数据包结构和OSI第三层网络层
  3. 1.1.4 错题知识整理(机器语言、汇编语言、正则语言、解释程序、编译、汇编)
  4. Swift之深入解析如何自定义操作符
  5. python全套学习方法_python学习方法总结(内附python全套学习资料)
  6. 冈萨雷斯《数字图像处理》读书笔记(三)——空间滤波
  7. android无法自动旋屏,Android 手动设置屏幕方向后不能自动转屏问题
  8. Apache Commons介绍(转载)
  9. CYQ.Data V4.5.5 版本发布[顺带开源Emit编写的快速反射转实体类FastToT类]
  10. 遍历二叉树(四种方式:前序、中序、后序、层序)
  11. Tomcat 7 的domain域名配置,Tomcat 修改JSESSIONID
  12. Spring Boot的MyBatis注解:@MapperScan和@Mapper
  13. python语法学习第十天--魔法方法
  14. 众人帮怎么发布悬赏任务?发布任务所需要求条件是什么?
  15. 测试用例——微信发红包
  16. 用python给pdf批量添加水印,并给pdf加密
  17. java面试题大合集
  18. [EndNote]EndNote在Word中的工具条消失了怎么办?-知乎转载
  19. VMware虚拟磁盘VMDK格式说明书1.1---3 The Descriptor File描述文件
  20. 目标跟踪算法综述与分析

热门文章

  1. lol比尔吉沃特服务器未响应,LOL比尔吉沃特9月30日网络波动公告 引起卡机掉线丢包状况...
  2. 与奥运会有关的常用英语术语及句子
  3. 【Python】简单判定身份证是否合法、性别
  4. Google Earth Engine(GEE)——美国近地表高精度实时气象数据集(2500米分辨率)
  5. Win10系统截图新工具的快捷键
  6. 指令下载Google网盘数据遇到的无法连接问题
  7. git branch -vv
  8. 什么是UTF-8编码
  9. (14)树莓派B+使用L298N驱动控制四驱车并实现一个简单的web控制端
  10. MySQL: Incorrect string value: '\xF0\xA4\xBD\x82'分析解决