MidWay
Midway
是一个适用于构建 Serverless 服务.传统应用,微服务,小程序后端的 Node.js 框架
Midway 可以使用 Koa,Express 或 Egg.js 作为基础 Web 框架。它还提供了独立使用的基本解决方案,例如 Socket.io,GRPC,Dubbo.js 和 RabbitMQ 等。
此外,Midway 也适用于前端/全栈开发人员的 Node.js 无服务器框架。构建下一个十年的应用程序。可在 AWS,阿里云,腾讯云和传统 VM /容器上运行。与 React 和 Vue 轻松集成。
特性
🐘 全功能:支持 Web 应用/Serverless/FaaS/微服务/小程序后端等多种场景,基于装饰器和依赖注入开发企业级应用
🐦 前端集成:全新的云端一体应用研发体验,零 API 调用,使用 "React Hooks " 风格一体研发
🐴 跨平台:支持部署至普通 Server 或 Serverless/FaaS 环境
🐶 扩展:组件化扩展能力,另外支持使用 Koa/Express/Egg.js 生态插件
🐂 示例: 官方提供多种场景的示例代码,方便开发者快速上手
🐷 TypeScript:Midway 3.x 版本基于 TypeScript 构建,提供完整的类型定义
快速入门
介绍
如果你没有接触过 Midway,没关系,本章节我们将从实例的角度,一步步地搭建出一个 Midway
标准应用,展示天气信息,让你能快速的入门 Midway
.
初始化项目
我们推荐直接使用脚手架,只需几条简单指令,即可快速生成项目。
npm init midway@latest -y
选择koa-v3
项目进行初始化创建,项目名可以自定,比如 weather-sample
现在我们可以体验下:
$ cd weather-sample
$ npm i
$ npm run dev
$ open http://localhost:7001
编写 Controller
如果你熟悉 web 开发或者 Mvc, 就知道第一步我们要编写Controller
和Router
在脚手架创建的文件中,我们已经有了一些文件,我们暂时忽略它
在controller
目录中,新建一个src/controller/weather.controller.ts
文件,内容如下
- 里面是一个控制器类 包含了一个异步的函数
import { Controller, Get } from "@midwayjs/core";
@Controller("/")
export class WeatherController {
// 这里是装饰器,定义一个路由
@Get("/weather")
async getWeatherInfo(): Promise<any> {
return {
code: 200,
data: {
city: "北京",
temperature: "20℃",
weather: "晴",
},
};
}
}
添加参数处理
在示例中 我们需要一个 URL 参数来展示不同城市的天气
通过 @Query
装饰器,我们可以获取 URL 上的参数
import { Controller, Get, Query } from "@midwayjs/core";
@Controller("/")
export class WeatherController {
// 这里是装饰器,定义一个路由
@Get("/weather")
async getWeatherInfo(@Query("cityId") cityId: string): Promise<any> {
console.log(cityId);
return {
code: 200,
data: {
city: "北京",
temperature: "20℃",
weather: "晴",
cityId,
},
};
}
}
- 访问
编写 Service 层
在实际项目中,Controller 一般用来接收请求参数,校验参数,不会包括特别复杂的逻辑,复杂而复用的逻辑,我们应该封装为 Service 文件。
我们来添加一个 Service 用来获取天气信息,其中包括一个 http 请求,获取远端的数据。
代码如下:
// src/service/weather.service.ts
import { Provide, makeHttpRequest } from "@midwayjs/core";
@Provide()
export class WeatherService {
async getWeather(cityId: string) {
return await makeHttpRequest(
`https://midwayjs.org/resource/${cityId}.json`,
{
dataType: "json",
}
);
}
}
makeHttpRequest 方法是 Midway 内置的 http 请求方法
然后我们来添加定义,良好的类型定义可以帮助我们减少代码错误。
在 src/interface.ts
文件中,我们增加天气信息的数据定义。
// src/interface.ts
export interface WeatherInfo {
weatherinfo: {
city: string;
cityid: string;
temp: string;
WD: string;
WS: string;
SD: string;
AP: string;
njd: string;
WSE: string;
time: string;
sm: string;
isRadar: string;
Radar: string;
};
}
这样,我们就可以在 Service
中进行标注了。
import { Provide, makeHttpRequest } from "@midwayjs/core";
import { WeatherInfo } from "../interface";
@Provide()
export class WeatherService {
async getWeather(cityId: string) {
const result: any = await makeHttpRequest(
`https://midwayjs.org/resource/${cityId}.json`,
{
dataType: "json",
}
);
console.log(result);
if (result.status == 200) {
return result.data as WeatherInfo;
}
}
}
这里使用 @Provide 装饰器修饰类,便于后续 Controller 注入该类
同时我们修改下之前的Controller
文件
import { Controller, Get, Inject, Query } from "@midwayjs/core";
import { WeatherInfo } from "../interface";
import { WeatherService } from "../service/weather.service";
@Controller("/")
export class WeatherController {
@Inject()
weatherService: WeatherService;
@Get("/weather")
async getWeatherInfo(@Query("cityId") cityId: string): Promise<WeatherInfo> {
return this.weatherService.getWeather(cityId);
}
}
注意
这里使用
@Inject
装饰器注入WeatherService
,是Midway
依赖注入的标准用法这里也同步修改了方法的返回值类型
到这里,我们可以请求 http://127.0.0.1:7001/weather?cityId=101010100
查看返回的结果。
你的第一个 Midway
接口已经开发完成了,你可以在前端代码中直接调用了,接下去,我们将利用这个接口完成一个服务端渲染的页面。
模板渲染
从这里开始,我们需要用到一些 Midway 的扩展能力。
Midway
对应的扩展包 我们称为组件
,也就是标准的npm
包
这里我们需要利用到@midwayjs/view-nunjucks
组件
安装
可以使用下面的命令来安装
$ npm i @midwayjs/view-nunjucks --save
安装完成后 我们在src/configuration.ts
文件中启动组件
// ...
import * as view from "@midwayjs/view-nunjucks";
@Configuration({
imports: [
koa,
// ...
view,
],
importConfigs: [join(__dirname, "./config")],
})
export class MainConfiguration {
// ...
}
注意
configuration 文件是
Midway
的生命周期入口文件,承担了组件开关,配置加载和生命周期管理的作用imports
就是用来导入(开启)组件的方法
配置
- 在
src/config/config.default.ts
中配置组件,指定为nunjucks
模板。
import { MidwayConfig } from "@midwayjs/core";
export default {
// ...
view: {
defaultViewEngine: "nunjucks",
},
} as MidwayConfig;
- 同时我们调整 Controller 的代码,将返回 JSON 变成模板渲染
// src/controller/weather.controller.ts
import { Controller, Get, Inject, Query } from "@midwayjs/core";
import { WeatherService } from "../service/weather.service";
import { Context } from "@midwayjs/koa";
@Controller("/")
export class WeatherController {
@Inject()
weatherService: WeatherService;
@Inject()
ctx: Context;
@Get("/weather")
async getWeatherInfo(@Query("cityId") cityId: string): Promise<void> {
const result = await this.weatherService.getWeather(cityId);
if (result) {
await this.ctx.render("info", result.weatherinfo);
}
}
}
渲染
- 在根目录(非 src 里面)添加
view/info.html
文件
<!DOCTYPE html>
<html>
<head>
<title>天气预报</title>
<style>
.weather_bg {
background-color: #0d68bc;
height: 150px;
color: #fff;
font-size: 12px;
line-height: 1em;
text-align: center;
padding: 10px;
}
.weather_bg label {
line-height: 1.5em;
text-align: center;
text-shadow: 1px 1px 1px #555;
background: #afdb00;
width: 100px;
display: inline-block;
margin-left: 10px;
}
.weather_bg .temp {
font-size: 32px;
margin-top: 5px;
padding-left: 14px;
}
.weather_bg sup {
font-size: 0.5em;
}
</style>
</head>
<body>
<div class="weather_bg">
<div>
<p>{{city}}({{WD}}{{WS}})</p>
<p class="temp">{{temp}}<sup>℃</sup></p>
<p>气压<label>{{AP}}</label></p>
<p>湿度<label>{{SD}}</label></p>
</div>
</div>
</body>
</html>
到这一步,我们访问 http://127.0.0.1:7001/weather?cityId=101010100
已经可以看到渲染的模板内容了。
错误处理
别忘了,我们还有一些异常的逻辑需要处理
一般来说,每个对外的调用都需要做异常捕获,并且将异常转变为我们自己业务的错误,这样才能有更好的体验
为此,我们需要定义一个我们自己的业务错误,创建一个src/error/weather.error.ts
文件
创建文件
// src/error/weather.error.ts
import { MidwayError } from "@midwayjs/core";
export class WeatherEmptyDataError extends MidwayError {
constructor(err?: Error) {
super("weather data is empty", {
cause: err,
});
if (err?.stack) {
this.stack = err.stack;
}
}
}
调整 Service 层代码
- 抛出异常
// src/service/weather.service.ts
import { Provide, makeHttpRequest } from "@midwayjs/core";
import { WeatherInfo } from "../interface";
import { WeatherEmptyDataError } from "../error/weather.error";
@Provide()
export class WeatherService {
async getWeather(cityId: string): Promise<WeatherInfo> {
if (!cityId) {
throw new WeatherEmptyDataError();
}
try {
const result = await makeHttpRequest<WeatherInfo>(
`https://midwayjs.org/resource/${cityId}.json`,
{
dataType: "json",
}
);
if (result.status === 200) {
return result.data as WeatherInfo;
}
} catch (error) {
throw new WeatherEmptyDataError(error);
}
}
}
注意
将 http 的调用请求进行错误捕获,将错误包裹,返回一个我们系统的业务错误
如有必要,我们可以定义更多的错误,分配错误 Code 等
到了这一步 我们还需要将异常进行业务处理,比如多个位置抛出WeatherEmptyDataError
我们需要统一的格式返回
错误处理器可以完成这个功能,我们需要创建一个src/filter/weather.filter.ts
文件,内容如下:
//src/filter/weather.filter.ts
import { Catch } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { WeatherEmptyDataError } from "../error/weather.error";
@Catch(WeatherEmptyDataError)
export class WeatherErrorFilter {
async catch(err: WeatherEmptyDataError, ctx: Context) {
ctx.logger.error(err);
return "<html><body><h1>weather data is empty</h1></body></html>";
}
}
- 然后在根目录(src/configuration.ts)中加载过滤器
import { Configuration, App } from "@midwayjs/core";
import * as koa from "@midwayjs/koa";
import { WeatherErrorFilter } from "./filter/weather.filter";
// ...
@Configuration({
// ...
})
export class MainConfiguration {
@App()
app: koa.Application;
async onReady() {
// ...
// add filter
this.app.useFilter([WeatherErrorFilter]);
}
}
这样,当每次请求中获取到了 WeatherEmptyDataError
错误,会使用相同的返回值返回给浏览器,同时会在日志中记录原始的错误信息。
数据模拟
在编写代码的时候,我们的接口经常还处在无法使用的阶段,为了尽可能的降低影响,可以使用模拟数据来代替
比如我们的天气接口,我们就可以在本地和测试环境模拟
我们需要创建一个src/mock/data.mock.ts
文件,内容如下
// src/mock/data.mock.ts
import {
Mock,
ISimulation,
App,
Inject,
IMidwayApplication,
MidwayMockService,
} from "@midwayjs/core";
import { WeatherService } from "../service/weather.service";
@Mock()
export class WeatherDataMock implements ISimulation {
@App()
app: IMidwayApplication;
@Inject()
mockService: MidwayMockService;
async setup(): Promise<void> {
const originMethod = WeatherService.prototype.getWeather;
this.mockService.mockClassProperty(
WeatherService,
"getWeather",
async (cityId) => {
if (cityId === "101010100") {
return {
weatherinfo: {
city: "北京测试中",
cityid: "101010100",
temp: "27.9",
WD: "南风",
WS: "小于3级",
SD: "28%",
AP: "1002hPa",
njd: "暂无实况",
WSE: "<3",
time: "17:55",
sm: "2.1",
isRadar: "1",
Radar: "JC_RADAR_AZ9010_JB",
},
};
} else {
return originMethod.apply(this, [cityId]);
}
}
);
}
enableCondition(): boolean | Promise<boolean> {
// 模拟类启用的条件
return ["local", "test", "unittest"].includes(this.app.getEnv());
}
}
这里仅仅模拟 101010100
这样我们就可以在本地和测试环境使用模拟数据了