双旦已过,新年将至,midwayJs 向大家献上贺礼,首先奉上地址: https://github.com/midwayjs/midway/,欢迎 star ✨✨✨。
之前我们向社区开放了我们的治理工具,也就是 Pandora.js 工具包,用于整个 Node.js 应用的监控和治理,我们承诺这不是结束,只是开源的开始。
随着内部全栈应用数的越来越多,以及阿里业务不断提升的复杂度,比如店铺,搭建以及渲染等服务,随着人员的不断调动,产品的结构,代码的层级都随着不断的调整,我们急需一个能降低代码复杂度的解决方案,帮助我们渡过人员寒冬,这就对我们内部的基础架构体系提出了不同的要求。
以往我们只需要让用户启动服务器,满足 RPC/HTTP 服务即可,而在真正的全栈领域,似乎没有太多的钻研和沉淀。对此,我们将内部使用的 midway 整体解决方案进行了一次重塑,并且在设计之初就提出未来将对外进行开源。
正巧我们的第一款 Typescript 产品 Pandora.js 开源完毕,给了我们将代码用 Typescript 重写的信心,也随着 Egg.js 社区的壮大,我们相信,在不同的领域中,一定会有不同的产品,不同的解决方案。
Midway 正式基于这些考虑,将 IoC 引入到了框架中,同时学习了 NestJs ,引入了不少自定义的装饰器,增强开发体验,也将搭配团队的其他产品,Pandora.js 和 Sandbox,将 Node.js 的开发体验朝着全新的场景发展,让用户在开发过程中享受到前所未有的愉悦感。
在这里感谢前期的 beta 测试中向我们提意见以及试用的同学,感谢大家的包容和支持,特别是 @ZQun 和 @yuu2lee4 两位的积极参与。
下面来介绍新版本 midway 的一些特性。
-
基于 IoC 体系业务代码进行解耦,依赖统一管理统一初始化
-
常见的 web 场景装饰器简化业务开发
-
支持 Egg.js 的所有插件体系,框架装饰器统一编码风格
-
基于 Typescript ,面向接口编程的编码体验
依赖注入疑问
在一年前,我们的业务代码是重重耦合,到处初始化,实例重复,但这并不是业务同学在代码架构方面的问题,而是在不断的业务迭代,交接下,早就脱离了最初的设想,代码的设计跟不上需求的速度。
为此,我们尝试引入了依赖注入的方案。依赖注入最早听到是在 Java 端的 spring 框架,在 JS 方面,最早我们使用了 XML 做为基础的 IoC 方案,虽然解决了不少耦合和初始化的问题,也发现前端在 XML 的感受吐槽颇多。
去年 Typescript 的大力发展之后,内部的很多项目都切换了过来,经过我们的调研,除了 NestJs 进行了自研以及在 Typescript 领域比较出名的 Inversify 模块,似乎很少有现成的易于扩展的模块。
基于这些情况,我们进行了这方面的自建,一方面方便内部的扩展,能更好的在现有的体系上扩展装饰器,请求作用域等,另一方面也可以提升本身的能力,方便后续迭代。
我们产出了 injection
模块,作为我们整个框架的依赖注入基础。
如今,injection
承载起了整个 midway 体系,它将框架代码,业务代码,插件等都组合到了一起,像一个纽带在这些之间传输数据。
通过依赖注入容器的管理,如上图非常复杂的应用也能良好的维护和运作。
想看完整大图,可以点击这里。
面向装饰器开发
得益于 Typescript 对 ES6 的良好支持,提供了一种为类声明和成员添加注释和元编程语法的方法。装饰器作为TypeScript的实验性功能能够让我们在开发中简化代码。虽然是语法糖,但是带来的好处却不少。
我们拿一个简单的例子,从 Controller 一步步经过 Service/Manager 向数据库拿数据,在多层的架构体系下,以往的代码大概率需要 new 出不同的实例,并且需要绑定到路由层,这边为了方便理解,代码放到了一起。
export = (app) => {
const home = new HomeController();
app.get('/', home.index);
}
class HomeController extends Controller {
reportService: IReportService;
constructor() {
this.reportService = new ReportService();
}
async index(ctx) {
ctx.body = await this.reportService.getReport();
}
}
class ReportService implements IReportService {
reporter: IReportManager;
constructor() {
this.reporter = new ReporterManager();
}
async getReport(id: number) {
return await this.reporter.get(id);
}
}
class ReporterManager implements IReportManager {
db;
constructor() {
this.initDB();
}
initDB() {
// open connection
}
async get() {
// return data from db;
}
}
经过 IoC 相关的 @provide
和 @inject
装饰器修饰以及其他 web 层的装饰器修饰过后,不仅仅只是代码量的减少,业务的代码也不再有实例化的过程。以往还需要考虑在构造器中做异步的操作,比如初始化时需要做异步连接数据库,这个时候也不再需要考虑,直接使用 @init
装饰即可。
至此,我们会更加专注于面向接口进行编程,抽象,将代码设计的时间更多的花在理解需求,解决问题上。
@provide()
@controller()
export class HomeController {
@inject()
reportService: IReportService;
@get('/')
async index(ctx) {
ctx.body = await this.reportService.getReport();
}
}
@provide()
class ReportService implements IReportService {
@inject()
reporter: IReportManager;
async getReport(id: number) {
return await this.reporter.get(id);
}
}
@provide()
class ReporterManager implements IReportManager {
@inject()
db;
@init()
initDB() {
// open connection
}
async get() {
// return data from db;
}
}
入口能力
就像上面提到的 @controller
装饰器类似,针对入口型的代码,我们在框架层面扩展了其他装饰器,比如针对计划任务形式我们提供了 @schedule
装饰器,简化用户开发的代码量。
import { schedule } from 'midway';
@schedule({
interval: 2333, // 2.333s 间隔
type: 'worker', // 指定某一个 worker 执行
})
export class HelloCron {
// 定时执行的具体任务
async exec(ctx) {
ctx.logger.info(process.pid, 'hello');
}
}
在下一版本中,我们将开放自定义装饰器的能力,方便更多场景的使用。
框架扩展
由于在大多数场景下,使用了装饰器已经依赖注入的写法,使得自己的业务代码,乃至三方的模块都能很好的融在一起,除了这些之外,有的同学会疑问,原本的插件,配置,上下文部分如何融入到这个体系,我们这就来解答。
在原本熟悉的体系中,只要有 app
, ctx
对象就无敌了,所有的东西都可以拿。而在 midway 中,为了和 web 层进行解耦,我们隐去了这些对象,只希望业务代码和 IoC 容器打交道。
为此我们提供了 @config
和 @plugin
装饰器用于获取不同的方法,通过这样的形式和框架进行解耦,比如在任意代码中如下使用。
@provide()
class ReportService implements IReportService {
@config('env')
env;
@plugin('httpclient')
httpclient;
@inject()
reporter: IReportManager;
async getReport(id: number) {
const rid = this.httpclient.request('/api/' + id);
return await this.reporter.get(rid);
}
}
正是这样一点点的调整,我们将整个应用的代码风格保持了到了一致,不管代码几经易手,维护的同学也能快速上手,并且继续迭代下去。
最后
正向我们在 Pandora.js 发布时说的那样,midway 也是 MidwayJs 团队长期维护的一款产品,同样不会是最后一款,前几个月,我们就计划将我们的监控平台 Sandbox 带出来回馈给社区,虽然道阻且长,任务艰辛,我们依旧在努力前行,欢迎关注。
最后,midway 的地址在这 https://github.com/midwayjs/midway/,归属在 midwayJs Group 下。欢迎走过路过点个 Star,给我们提提建议,提提代码。
Midway 官网:https://midwayjs.org/midway/
前排支持
赞一个! 我之前也写了一个, 不过是插件扩展的, 基本上把 nest 的核心都抄了过来,不过 inject 保留了, 个人不喜欢依赖来依赖去.
文档不错,但跟egg一样,默认给带worker了,还默认了pandora。 我觉得worker这类的通过选项开启就好了,默认给最简的模式会好一些。 其实像多进程和多线程或多或少都是增加了开发的复杂度,我是觉得框架还是最好少点这东西。 我宁愿用pandora或pm2来启动多实例,包括egg的agent机制,我认为大多场景必要性不大。
@zy445566 只是默认生成了一个pandora的配置文件,没有pandora依赖哦
@zy445566 类似纯 koa 开发,单进程模式的体验我们也很喜欢的,调试开发都方便,现在因为是基于 egg-core 的底层往上扩展,进程模型直接沿用了 egg 的那一套,由于有 master 的存在,似乎不太容易独立运行进程,这块我们也会思考下:)
@czy88840616 死马年前会做 egg 单进程这块
@atian25 恩,我看到 issue 了
期待下~
贡献可能略显不足,但是通过参与,自己也得到很多提升和思考~ 加油!🤗
想问问和egg是什么关系?互补,还是同一类框架?
本小白对两个框架都不熟~~
[CNodeMD]
@atian25 这波可以,周围生态得赶紧完善~
恭喜
@dbit-xia 弥补egg 的不足,跟 nest.js spring 是同一类框架
@dbit-xia 不能说是弥补egg的不足,egg和周边都已经很成熟了,midway算是基于egg进行升级和拓展吧,换了一种开发方式,支持typescript,支持IoC,同时熟悉egg开发的也会有亲切感~
赞赞赞 测试一波 Midway 也是淘宝团队吗?
@sinazl 是的, MidwayJS 隶属于淘宝技术部前端部门,产出 midway/pandora.js/sandbox 几类产品。
依赖注入的方式很不错,同时可以利用egg的生态,基于ts开发体验提升了一个层级
@dbit-xia midway / beidou 都属于 egg 的上层框架
撒花~周末尝试看看~
fdsfds
@atian25 @ZQun @zuohuadong 谢谢,了解了,看样子并不依赖egg,算不算抢egg的饭碗( ̄o ̄) . z Z
[CNodeMD]
@dbit-xia 上层框架的意思就是,他们的底层是 egg…
支持,😀 From Noder
@atian25 "上层框架"怎么理解?是依赖egg 还是基于egg源码的改造?
[CNodeMD]
@dbit-xia 基于 egg 实现的更高一层 Level 的框架,融入 egg 生态圈。
如图,如果你熟悉 Egg 的话,会知道 Egg 的定位是:『框架的框架』。
其中一个很重要的特性就是可以基于 Egg 构建适合自己团队或特定业务场景的上层框架。
这是内置特性,用起来也几乎没啥区别,可以看下文档。或者看下我这篇 Slide : https://github.com/atian25/blog/issues/20
egg本身已经很不错了,midway会使node的生态会越来越好。 我们招人可以更好的策反java/python程序员啦。
加油
@wbget JavaScript 明星项目! https://risingstars.js.org/2018/zh/
node.js框架,值得一提的是,nuxt 和 next 分别用于ssr 渲染。 也就是说,今年增长最多的是 nest ,并且npm下载量已经达到了 koa 的 1/4 。
midway 学习了 nest, 也比较看好。 egg … 不建议
@zuohuadong eggjs我们已经用于生产环境半年多了,满足我们本身的各种需求。 nest也关注了。 靠前的这些框架都很不错,大家酌情了解熟悉一到俩个,我觉得就很不错了。 人生苦短,早下班才是硬道理。
地方
请问 关于事务控制是依赖于插件还是框架本身有控制
@waitingsong 目前 midway 做的 IoC 为控制层,事务没有涉猎。
@wbget 同意,人生苦短,满足就行。。学习框架,更多的是学习下理念,设计,其他的看缘分吧。
@czy88840616 谢谢
@czy88840616 作为一个小白,我想请教一下,新框架的出现必定是为了解决现有框架的缺陷而诞生的。Midway 是解决什么问题,问题再具体点,我一直觉得 Express 是最简单粗暴的 Node 框架,Midway 又能做哪些 Express 不能做的事情?
@dkvirus 我猜,是做到 不简单而优雅 吧
@czy88840616 请问 midway 支持作为 gRPC 服务端 开发么(或者同时启动服务端、客户端)。官方文档里面好像没找到。
@ZQun 多谢
会egg是不是学起来很快
浓浓后端 spring的既视感…感觉这款 “前端框架” 是不是后端主力开发的?哈哈哈哈… controller ,service, 就差 dao了
支持一个
srping ioc typescript
@waitingsong nest 有这个,在微服务里。 midway 现在生态还没完善
@dkvirus 简单理解为 一个是工具包,一个是框架~
支持一个
@phonegap100 会用egg,看一遍midway文档就能上手了
来自酷炫的 CNodeMD
@zuohuadong nest 也是一个可选项,不过暂时没时间做评估。你能否简要说说两者 (nest, midway/egg) 的优缺点? 我关心的是稳定性,可维护(测试、扩展)性。 之前在zhihu好像看到有说 nest 的测试用例中不方便 DI注入于是事务控制不好做。
@waitingsong nest 2017 年底就发布了。 马上6.x ,生态也相对来说比较成熟了。
midway加油
加油
关注看看~
@ZQun 好的谢谢
有个关于类型的问题: controller/home.ts
import { controller, get, provide } from 'midway';
@provide()
@controller('/')
export class HomeController {
@get('/')
async index(ctx) {
ctx.body = `Welcome to midwayjs!`;
}
}
index() 方法的入口参数 ctx 并没有类型,默认是 any。 那么如何给 ctx 增加类型呢?
对于 egg,这样的代码就有类型提示:
import { Controller } from 'egg-win'
export default class HomeController extends Controller {
public async index() {
const { ctx } = this
ctx.body = await ctx.service.test.sayHi('sundsoft.com')
}
}
declare module 'egg' {
export interface IController {
home: HomeController
}
}
@waitingsong 只能手动指定,egg 默认是继承来的声明合并后附加所有信息所以带类型,这个是参数只能指定了
@yviscool 那从哪儿引入呢? 比如 config.default.ts egg 的有类型
import { EggAppConfig, PowerPartial } from 'egg'
export default (appInfo: EggAppConfig) => {
const config = <DefaultConfig> {}
// should change to your own for every new project!
config.keys = appInfo.name + 0.00842891111
config.siteFile = {
'/favicon.ico': fs.readFileSync(path.join(appInfo.baseDir, 'app/public/favicon.png')),
}
}
···
而 midway 的啥都没有:
```ts
export = (appInfo: any) => {
const config: any = exports = {};
// use for cookie sign key, should change to your own and keep security
config.keys = appInfo.name + '_1558269532558_9642';
// add your config here
config.middleware = [
];
return config;
};
还有文档也是胡编乱造: https://midwayjs.org/midway/guide.html#注入配置
假如 config.default.ts 中有一些代码。
export const hello = 1;
你(写文档的哥们)给我试试在 config.default.ts
中直接导出个 hello ?
感觉 midway 的 ts 有点简陋吧。还是 midway-init
生成出的 ts 模板不正确?
👍
@waitingsong https://github.com/midwayjs/midway/blob/86170febb2471468b79fc2a32450f4e707ed5ee6/packages/midway-web/src/index.ts#L10 大部分 egg 导出的定义都已经导出,可以直接从 midway 上取到。
import { controller, get, provide, Context, Application, EggAppConfig } from 'midway';
@provide()
@controller('/')
export class HomeController {
@get('/')
async index(
ctx: Context,
next: any,
// app: Application,
// config: EggAppConfig,
) {
ctx.body = `Welcome to midwayjs!`;
}
}
这样 ctx 就能提示了,看了一下 config, mid 的没有生成类型文件,
import { EggAppConfig } from 'egg';
import ExportConfigDefault from '../../config/config.default';
type ConfigDefault = ReturnType<typeof ExportConfigDefault>;
declare module 'egg' {
type NewEggAppConfig = ConfigDefault;
interface EggAppConfig extends NewEggAppConfig { }
}
egg 默认是生成的了 这样的一个 type/ config 然后合并就有提示类型了, @czy88840616 很好奇,为什么 mid 没有
@yviscool 目前 config 没提供,是因为这个目录是少数几个强依赖目录结构的,一方面我们还在想办法换一种配置形式,另一方面目前提配置定义的用户不多,我们会考虑加进去。
@yviscool 谢谢 就是说现在用
import { provide, config } from 'midway';
@provide()
export class BaseService {
@config('hello')
config; // 1
}
注入方式获取到的 config 是无法获取到类型哟?
@czy88840616 我的egg配置
export default (appInfo: EggAppConfig) => {
const config = <DefaultConfig> {}
// should change to your own for every new project!
config.keys = appInfo.name + 0.0084211111
config.siteFile = {
'/favicon.ico': fs.readFileSync(path.join(appInfo.baseDir, 'app/public/favicon.png')),
}
config.cors = {
origin: '*',
// allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
allowMethods: 'GET,HEAD,POST,PATCH,OPTIONS',
maxAge: 600,
}
config.security = {
csrf: {
enable: false,
},
}
return config
}
这些配置参数,没类型定义不好设置和检查呀……
@czy88840616 midway的类型命名是否考虑去掉 I
前缀呢
https://angular.cn/guide/styleguide#style-03-03
考虑不要在接口名字前面加 I 前缀。 考虑用接口作为数据模型。 为何? TypeScript 指导原则不建议使用 “I” 前缀。 为何?单独一个类的代码量小于类+接口。 为何?类可以作为接口使用(只是用 implements 代替 extends 而已)。
@waitingsong 可以的,只是示例里加了 I,实际使用 lint 里应该没限制的。
@waitingsong 我们之前考虑,config 大多还是单一属性或者简单的单层对象,如果是变成现在注入的形式,就会省略一个 key,这样定一个完整的 config interface 没有太大意义,反而要考虑拆分成本。
@config(xxx)
config;
看起来直接写 string/number/ {xxx: xxx} 会更快。
@waitingsong 呃,我可以把 egg 的东西导出的时候 as 一下,但是。。。总觉得不是很好。。
@czy88840616 我这个一个ocr服务,config 相当复杂,层级有个三四层。看来用注解方式获取配置不理想(对于ts来说)。
@czy88840616 底层(egg)的东西还是不改动的好。 midway 自己遵循主流风格规范吧。
@waitingsong 可以单独针对某一个复杂的 key 定义 interface,这样 config 中和实际服务中都可以用。
@yviscool 有个问题
import { controller, get, provide, Context, Application, EggAppConfig } from 'midway';
@provide()
@controller('/')
export class HomeController {
@get('/')
async index(
ctx: Context,
next: any,
app: Application,
// config: EggAppConfig,
) {
this.app ??
ctx.body = `Welcome to midwayjs!`;
}
}
假如要使用 app 入口参数,那么就得把 next 形参也写上。但是如果方法里面没用到这个 next 参数,并且 tsconfig.json 里面开启了 noUnusedLocals
那编译就过不了。
貌似类方法也没定义 this 的类型,这该咋办?
@waitingsong mid 类方法应该只有 ctx 和 next 参数, 上面那个 app 和 config 是我用来演示的
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"strict": true,
"noImplicitAny": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"charset": "utf8",
"allowJs": false,
"pretty": true,
"noEmitOnError": false,
//"noUnusedLocals": true,
//"noUnusedParameters": true,
//"allowUnreachableCode": false,
//"allowUnusedLabels": false,
"strictPropertyInitialization": false,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"inlineSourceMap": true,
"importHelpers": true
},
"exclude": [
"app/public",
"app/views",
"node_modules*"
]
}
把这几个注释干掉就好了
@czy88840616 有个类构造方法的问题
import { provide, Context } from 'midway';
import { IUserService, IUserOptions, IUserResult } from '../interface';
@provide('userService')
export class UserService implements IUserService {
constructor(foo) {
}
async getUser(options: IUserOptions): Promise<IUserResult> {
return {
id: options.id,
username: 'mockedName',
phone: '12345678901',
email: 'xxx.xxx@xxx.com',
};
}
}
构造方法没有入口参数 (foo值为未定义)? this对象(constructor 方法内的或者 getUser 方法内的)除了类自身的属性和方法,类型上面没有其它属性 (egg的 this 上面有 app 等一堆对象),是设计如此么? 感觉从 egg 要转移到 midway,TS的变化有点不适应……
@yviscool 你这个有点削足适屐吧 我项目模板设置
"compilerOptions": {
"alwaysStrict": true,
"charset": "utf8",
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"importHelpers": false,
"inlineSourceMap": true,
"module": "commonjs",
"newLine": "lf",
"noFallthroughCasesInSwitch": true,
"pretty": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"target": "ES2018",
"types" : ["node"]
},
@yviscool 测试了下, controller 类方法入口参数只有 ctx 和 next 两个形参的。
@czy88840616
文档 https://midwayjs.org/midway/guide.html#框架增强注入 下 injection
的链接打不开
web.npm.alibaba-inc.com/package/injection
有个跳转定义的问题:
- 使用
midway-init
初始化一个新项目,选择 模板1,然后npm i
安装依赖 - 用 vscode 打开项目目录
- 打开
src/app/controller/user.ts
- 在 行13
const user: IUserResult = await this.service.getUser({id});
处 getUser 方法上使用 F12
结果是跳转到 src/interface.ts
文件中 IUserService
类型定义上,而非预期的 src/service/user.ts
中 UserService.getUser()
方法定义上。
在类注入的 service: IUserService
上面 F12 跳转也是到类型定义上。
你们是如何解决这个跳转问题的呢?
@waitingsong
你可以把 IUserService 改成 service 下面的 UseSerrvice , 官方也说了 接口定义文件,自由定义
,单单看这个 interface 层可能没有什么卵用,java 好多项目都是这样 IxxxxService接口 和 xxxxSerivce 实现, 各人理解是有些代码细节我不关心或者我不想暴露,再或则我换个语言重写,我只关心参数以及返回,这时候这一层可能就有用了,这个纯属个人喜好了,反正我不喜欢维护两份代码。
@yviscool 多谢。 如下方式解决:
import { Context, controller, get, inject, provide } from 'midway';
// import { IUserService } from '../../interface';
import { UserService } from '../../service/user';
@provide()
@controller('/user')
export class UserController {
[@inject](/user/inject)('userService')
service: UserService;
@get('/:id')
async getUser(ctx: Context): Promise<void> {
const id: number = ctx.params.id;
const user = await this.service.getUser({id});
ctx.body = {success: true, message: 'OK', data: user};
}
}
对于接口层我认为存在有意义,不过可以不按照通常(midway 模板)那样使用,可以这样:
- 方案中心或者开发组长根据概设在写详设的时候,接口设计直接以
.ts
文档格式提供 - 开发人员导入接口文档 ts 文件,然后 Service 类
implements
指定的接口 - controller 中注入的是类(就是上面我的代码那样),而非类实现的接口。这样方便F12跳转定义
- 若新增需求或者需求变更,走
issue
流程,然后重复步骤 2-3
估计Chrome也是这么考虑的,所以在 Angular 的风格建议中不推荐接口名使用 I
前缀的一个原因吧: interface
更多是作为类型使用而非如 java 那样严格的接口设计使用。
另外,感觉
@inject(‘userService’) service: UserService
有些累赘,不知道是否有这种实现,注入参数直接使用类:
@inject(UserService) service;
@waitingsong 注入参数直接使用类是可以的,但就是没提示,因为 ts 只是类型标注,不能动态赋予类型的,你没标注就没提示。感觉 mid 不适合你用,强扭的瓜不甜的。还是自己扩展一个顺手的。
@yviscool 个人看法 egg 比较底层, nestjs 在 spring 的路上走得有些远。 midway 中庸些还行, 就是自己不大熟悉。
@waitingsong 额,这个是内网地址。。。
@yviscool 看了文档,原来可以这样:
import { Context, controller, get, inject, provide } from 'midway';
import { UserService } from '../../service/user';
@provide()
@controller('/user')
export class UserController {
@inject()
userService: UserService;
}
挖坟