[Development summary] Mirai-js development summary

Mirai-js

Mirai-js is a QQ robot development framework running on the Node.js platform, Mirai’s community SDK,

Mirai-js is based on the mirai-api-http plug-in of mirai-console. mirai-api-http provides Mirai's full platform interface via http.

This is my first attempt to develop a framework. Since the robot functions are all implemented by mirai-api-http, the focus is on the design of the framework. I have indeed learned a lot of things and I have a sense of accomplishment.

Project warehouse: https://github.com/Drincann/Mirai-js

Development document: https://drincann.github.io/Mirai-js

Libraries, frameworks and SDKs

SDK generally refers to a series of development tools provided to developers, including frameworks, libraries, documentation, and even hardware.

As for libraries and frameworks, in some cases their boundaries are not clear.

Generally speaking, the complexity of the framework is higher than that of the library. This is because the framework takes over the main part of the entire development process, or the main control flow. The library focuses on encapsulating some relatively independent functional blocks, providing some slightly higher granularity but more convenient interfaces, and increasing development efficiency.

For the development of an application, only one framework can be introduced, because there is only one main control flow, but multiple libraries can be introduced and let them be responsible for multiple parts.

Several modules of Mirai-js

Mirai-js has three modules, Bot, , Message, Middlewareexpose them to develop a class caught.

There is also a module called Waiter, which also exposes a class, but only to Bot. It as Botan internal class, through Botthe public field provide external interfaces.

BotIs the center of the frame, the other modules are around Botto do the expansion. An instance of it will take over the entire life cycle of a session of the mirai-api-http service.

MessageIs used to Botprovide a convenient interface to generate a message when the transmitted data.

MiddlewareMiddleware is a class that implements mode, provides a series of middleware functions, and packaged for developers to customize the middleware interface for easier handling Botvarious messages events.

WaiterAchieved Botsynchronization message flows io, he allows asynchronous message flow obstruction from any part of io, and enter returns from a specified user.

How to deal with coupling

This is a headache. Javascript itself is a dynamic type and has no interface, which brings great difficulties to the design of the framework.

Message

To make me Messageeasier to use module that allows a direct pass when sending messages Messageinstance.

Bot.sendMessageReceiving a MessageChainparameter, it is a MessageType[]while MessageTypebut also more complex, so I passed Messagegeneration to MesageChain.

The initial interface is like this:

bot.sendMessage({
    
    
    message: new Message().addText('Hello').addAt(1019933576).done(),
    // ...
});

MessageExamples of the addxxxmethod used to maintain the internal instance MessageChainpush a MessageTypewhile the donemethod in the last call for internal maintenance MessageChain.

But this design is not intuitive, so that I sometimes forget to call it when developing applications done, and this design seems very redundant.

So I make Botand Messagemodule coupled to allow Bot.sendMessagedirect pass an Messageinstance.

bot.sendMessage({
    
    
    message: new Message().addText('Hello').addAt(1019933576),
    // ...
});

Later, when using Typescript write type declaration, through MessageChainGetablean interface to decouple them, but also Bot.sendMessageeasy to expand.

Middleware

This module has a lot to say in the design Waitercarried out when some complex changes, then use the Typescripttime and changed it back, then it's easier to understand and logical design and ease of Botdocking.

Middleware It is used like this:

bot.on('FriendMessage', new Middleware()
       .textProcessor()
       .catch(error => console.log(error))
       .done(async data => {
    
    
   // 开发者的消息处理逻辑 
});

After several changes through the interface form without any changes, but logic has changed, this is due to the Waiterimplementation of the callback function must distinguish between ordinary and middleware with a callback function, this will be a slowly saying, look Waiter.

Initially, Middlewarethe donemethod returns a callback function with the inlet middleware.

Waiter It is used like this:

bot.on('FriendMessage', new Middleware()
       .textProcessor()
       .done(async data => {
    
    
   if(data.text?.includes('/unload')){
    
    
       bot.sendMessage({
    
    message: new Message().addText('请输入 /confirm 确认 /unload 操作')});
       // 将异步操作阻塞,等待消息
       // wait 方法将等待一次事件,并将回调函数的返回值返回到外层
       let userInput = await bot.waiter.wait('FriendMessage' , data => data.messageChain.filter((val) => val.type == 'Plain').map(val => val.text).join(''));
       
       if(userInput?.includes('/confirm')){
    
    
           // 指定操作
       } 
   }
});

or:

bot.on('FriendMessage', new Middleware()
       .textProcessor()
       .done(async data => {
    
    
   if(data.text?.includes('/unload')){
    
    
       bot.sendMessage({
    
    message: new Message().addText('请输入 /confirm 确认 /unload 操作')});
       // 将异步操作阻塞,等待消息
       // wait 方法将等待一次事件,并将回调函数的返回值返回到外层
       let userInput = await bot.waiter.wait('FriendMessage' , new Middelware().FriendFilter([data.sender.id]).textProcessor().done(data => data.text););
       
       if(userInput?.includes('/confirm')){
    
    
           // 指定操作
       } 
   }
});

As the waitmethod needs to get the return value of the callback, first of all we are waiting for its return to normal callback function, called directly. For the callback function entry with middleware, he is by a nextpackage of middleware recursive chain, the return value means nothing entrance.

So I have Middlewarea donemethod to return the callback function entry was packed, I added the entrance of the second parameter resolveand a callback function at the developer will provide its return value passed resolvecallback.

So I was Waiterprovided a way to asynchronously middleware package task, but this time the callback function must distinguish between normal and middleware function entry, because of the common functions directly synchronized to wait, while the middleware function entry you need to be packaged Promise Then wait synchronously, but they are essentially functions, there is no difference.

At this time I have two solutions,

First of all, I can distinguish between the number of parameters passed to the function entry to judge, because I offer the possibility of asynchronous middleware package for the function entry, add a resolveparameter, while the average callback function should be only one parameter. But this is not stable, because I am not sure that the ordinary callback function passed in by the developer has only one parameter.

The second solution is to make Middlewarethe donemethod returns the current instance, function entry and maintenance in instances where a public field, this time in Waiterneed is a function or judgment passed Middlewareinstance, so that you can separate the area, but the problem is, at the same time also need to change Botthe onmethod, because I no longer return function entry, but rather an instance, it will increase the degree of coupling, but also increases the complexity, but I had no better idea, so he Did this.

Later I took a moment and looked after Typescript decided to write a type declaration, in the middle, me Middlewareand Messagedeclared two interfaces respectively EntryGetableand MessageChainthen implement the appropriate interface as the class in js and let them inherit, in Botthe Determine whether it is an instance of these interface classes. This is an attempt to decouple.

Finally, the middleware function may suddenly realize that the inlet, i.e., donethe return inlet, a Promise packaged directly, and the developer in the callback resolveto the outside. In this way, the behavior of the ordinary callback function and the middleware will be exactly the same.

Recognizing this, I deleted EntryGetablethe interface, packed Middleware.done, and then change all the related dependencies.

Middleware pattern

The middleware implemented at the beginning was very ridiculous. It just traversed all the middlewares in turn without any control. Later, using this design encountered great difficulties when developing some middlewares.

So I checked the information and realized a most basic middleware model:

/**
 * @description 生成一个带有中间件的事件处理器
 * @param {function} callback 事件处理器
 */
done(callback) {
    
    
    return data => {
    
    
        return new Promise(resolve => {
    
    
            try {
    
    
                // 从右侧递归合并中间件链
                this.middleware.reduceRight((next, middleware) => {
    
    
                    return () => middleware(data, next);
                }, async () => {
    
    
                    // 最深层递归,即开发者提供的回调函数
                    let returnVal = callback instanceof Function ? (await callback(data)) : undefined;

                    // 异步返回
                    resolve(returnVal);
                })();
            } catch (error) {
    
    
                // 优先调用开发者的错误处理器
                if (this.catcher) {
    
    
                    this.catcher(error);
                } else {
    
    
                    throw error;
                }
            }
        });
    };
}

The core logic is very elegant, and it can be abstracted into three lines:

middlewareArray.reduceRight((next, middleware) => {
    
    
    return () => middleware(data, next);
}, callback)();

This is actually a recursive packaging process, from the deepest callbackpoint to keep the outer packaging, get an outermost function entry, the more outer layer nextwill call the deeper layer of middleware.

asynchronous

Writing this framework slightly increases the understanding of Promises and greatly improves the proficiency of using Promises. I also touched on the generator asynchronous task synchronization algorithm realized by Thunk packaging. It's just that the packaging method is very inelegant and it is not used.

Good design

From native APIs to libraries and frameworks, the interface granularity is getting bigger and bigger, which leads to higher and higher development efficiency while also reducing the degree of freedom.

So I think a good design is a progressive design, and a progressive framework can often give developers more freedom. This gradual approach is achieved through independent modules. The more independent the modules, the more room the developer has for his design.

The original intention of developing this framework is very simple. After Tencent killed a batch of commercial robot platforms, I urgently needed to shift positions. Mirai is relatively safe, after all, it is open source. But I haven't used java much, there is a certain learning cost, and the efficiency of java development may be slightly worse.

I'm quite comfortable with js, so I watched a few Mirai community SDKs, node-mirai, mirai-ts, etc. on the js platform. The experience was not very good, and the source code did not feel stable. So I am going to understand the interface of mirai-api-http first, and encapsulate some libraries for use. However, when I get started, I feel that I can do this. Some good designs popped out of my mind and I wanted to write it. .

A strange thing

I use the WebSocket interface provided by mirai-api-http for message push. I accidentally encountered a bug while writing the event handling section.

I just implemented some simple logic of event handling that night and went to dinner. My test code was still hanging, and it was time to test stability. After eating, I found that the robot shouldn't call anything. I interrupted and found that no news was pushed from the server to the local.

After several tests, it was found that in about 3-5 minutes, if the WebSocket connection does not have any data push, the server will inexplicably ignore this link and no longer push any messages, but the link has not been disconnected. Server ping solves this problem.

I wondered if the document was missed, and I didn't find it after looking through it for a long time.

So issue#255 was raised in mirai-api-http, but their developers said that there is no relevant logic. After graia (mirai python sdk), someone just realized the related problem, and they also solved the problem after communicating.

It was strange. Later, I found that the community SDKs did not do similar processing. Their robots always stopped any news push within a few minutes, so I also mentioned some PRs. Some of them seem to have fallen into disrepair for a long time, and no one has merged until now.

development tools

The developers of mirai-ts are very powerful. When fixing their WebSocket ignored bugs, they found that their projects also had eslint and prettier checks before commit. I checked it and found out that it was achieved through git hook. There is a mature package called husky that is used to do this. So I copied the configuration of mirai-ts. I also learned more about the configuration of eslint.

commit specification

This time, git was actually applied in practice. In the past, it was add, commit, then push push, pull pull, and it was gone. This is the first time that I learned in practice how to develop on multiple branches at the same time. How to deal with the commit history in the final merge.

If you take a look at the first few commits of Mirai-js, it is really disgusting. At that time, I always wrote for one day, and did not submit until the end of the evening, and then carefully wrote all the work I did today into the message.

Later, I found that everyone advocated more small-grained submission, and learned some specifications on how to write messages.

The initial workload of the development is relatively large, and it is basically the encapsulation of some core functions. I remember that I handed it over 80 times a day. Later, it was discovered that the color depth of the green grid of github is relative.

above sea level

Mirai-js has only recently been released to npm. This is the first release, and I have also learned about some version number control specifications.

What to do in the future

Now the development of applications based on Mirai-js is independent, which means that multiple robot applications need to run multiple scripts, which is very inconvenient.

The current idea is to build a plug-in system, whether it should be coupled with the existing system is still under consideration.

in conclusion

From the beginning of the month started to develop, one after another and now there are half a month, and now is 2021.02.22, the main work is basically completed within a few days previous, behind some tinkering, in addition to Waiteroutside are The icing on the cake.

Up to now, there are more than 30 stars, and it is indeed a sense of accomplishment to write a practical thing.

Guess you like

Origin blog.csdn.net/qq_16181837/article/details/113941088