教程万字长文保姆级教你制作自己的多功能QQ机器人

Posted 小锋学长生活大爆炸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了教程万字长文保姆级教你制作自己的多功能QQ机器人相关的知识,希望对你有一定的参考价值。

转载请注明出处:小锋学长生活大爆炸( https://xfxuezhang.blog.csdn.net/)
若发现存在部分图片缺失,可以访问原文: 万字长文保姆级教你制作自己的多功能QQ机器人 - 小锋学长生活大爆炸

​​​​​​

目录

前言

功能清单

免费领取轻量应用云服务器

SSH连接服务器

常见Ubuntu软件安装与问题修复

搭建mirai环境

Python控制mirai篇

debug输出封装

交互授权

绑定bot

释放bot

未读消息的数量

获取最新的消息

解析消息内容

向好友发送消息

向群发送消息

向群发送富文本消息

Q群消息转发

类似QMsg酱的消息通知

多功能切换的实现设计

翻译查询

领取腾讯免费翻译API

机器人接入翻译功能

实时天气

领取免费的和风天气API

机器人接入天气功能

实时热搜

领取免费的天行热搜API

机器人接入热搜功能

照片上传

领取腾讯对象存储COS

机器人接入图片上传功能

自行添加小功能函数总结

控制树莓派舵机与屏显

腾讯云服务器搭建MQTT环境

待实现功能

接入控制ESP32(实现智能家居控制)

完整代码整理


前言

        QQ、微信是我们平常使用最多的通讯工具,网上也有很多通过软件去控制QQ/微信的开源工具,通过这些工具,我们可以实现许多有意思的效果,而不仅仅局限于消息聊天。
        自从微信网页版被官方禁用后,微信的软件工具几乎已经失效了,现有的一些是通过hook微信本身来实现,这种很容易被官方检测并封号。另一些是通过注册企业号来控制,但不直观且功能受限。
        这里我们借助相对更开放的QQ来制作我们的机器人,对比几款工具后,最终选择了mirai

先放一张整体结构图:

 

功能清单

        网上现有开源的机器人大多只是实现了类似“自动推送天气、接入图灵机器人自动聊天”等等,大多属于自娱自乐,没有发挥最大用途。
        因此,我们的QQ机器人(暂且取名为“小锋仔”)是根据日常所需而制,包含常用功能且设计得易于扩展。
目前包含的功能有:

  • 类似QMsg酱的消息通知
  • QQ群消息转发
  • 翻译查询
  • 照片上传
  • 实况天气
  • 实时热搜
  • 控制树莓派舵机
  • 控制树莓派屏显
  • ... ...

将来可能包含的功能有:

  • 接入控制ESP32(实现智能家居控制)

        接下来详细介绍如何自己搭建一个这样的QQ机器人。篇幅较长保姆级详细,建议收藏后慢慢看。

免费领取轻量应用云服务器

首先为了 能运行mirai,且随时随地能连接,我们需要有一个 具备公网IP的服务器。这里使用腾讯云的 免费服务器

        对于还不想买的童靴,可以免费领取腾讯云提供的1个月服务器试用套餐。步骤:

  1. 进入官网领取:云产品免费试用;需要选购的进:轻量应用服务器专场 (限时3年388,点我进);不清楚怎么操作的可以看教程:腾讯云产品免费试用教程
  2. 领取完成后,由于后面需要用到端口,因此这里我们提前开放2个端口:88889966

        这里腾讯云可能有个小特点。如果发现在控制台防火墙放行后,还是无法访问。需要再在服务器里放行一下端口。这里先写着,大家可以在后面一节中连接上了服务器,再回过来这里输入指令。

sudo apt install firewalld -y
sudo firewall-cmd --list-all
sudo firewall-cmd --permanent --zone=public --add-port=8888/tcp && sudo firewall-cmd --reload
sudo firewall-cmd --permanent --zone=public --add-port=9966/tcp && sudo firewall-cmd --reload
sudo systemctl start firewalld.service

SSH连接服务器

        服务器初始化完成后,就可以通过SSH去连接了。这里我们可以直接使用powershell来连接,其他SSH软件我强推mobaxterm!!安装包也已经准备好了:MobaXterm.exe

  1. 搜索打开powershell:

  1. 输入以下命令连接SSH:
ssh 用户名@<公网ip>
  1. 或者使用MobaXterm软件:

  1. 先更新一下软件库:
sudo apt upgrade -y
sudo apt autoremove -y
  1. 一般不建议使用管理员账户,因此我们要自己新建一个账户:
sudo adduser sxf


然后将账户加入sudoers组:

sudo apt install vim
sudo vim /etc/sudoers


        然后退出软件,重新用新建的账号登录即可。
        至此,服务器环境就搭建完成了。

常见Ubuntu软件安装与问题修复

        这篇博客里记录了很多我在使用过程中,常用软件的安装,非常详细且经过亲测,时不时也会更新内容,大家可以收藏以备下次使用。
Ubuntu20.04 + VirtualBox相关_小锋学长生活大爆炸的博客-CSDN博客


搭建mirai环境

        接下来就要在服务器上搭建QQ机器人(mirai)基础环境。搭建完成后,我们就可以远程跟机器人进行交互。
        官方mirai的github仓库:GitHub - mamoe/mirai: 高效率 QQ 机器人支持库
        由于github是国外的,而官方已经不再支持gitee的维护,因此如果大家无法访问上面的连接,可以用我帮大家下载下来的安装包:
        其他的一些文档:Mirai | mirai
        官方论坛:主页 | MiraiForum
下面开始正式安装:

  1. 先SSH连接上服务器,建议不要用root用户登录。
  2. 下载安装包mcl-installer-a02f711-linux-amd64:
mkdir qqbot
cd qqbot
wget http://xfxuezhang.cn/web/share/QQBot/mcl-installer-a02f711-linux-amd64
sudo chmod +x mcl-installer-a02f711-linux-amd64

        此时需要输入密码(在上面选购并装完服务器后会显示,当时要求记下的)。

./mcl-installer-a02f711-linux-amd64

        此时进入安装流程,弹出的几个选项都直接回车选默认即可。

  1. 安装完成后,还需要安装mirai-api-http。在当前页面下,继续输入:
./mcl --update-package net.mamoe:mirai-api-http --channel stable-v2 --type plugin
  1. 编辑_config/net.mamoe.mirai-api-http/setting.yml_配置文件 (没有则自行创建)
## 配置文件中的值,全为默认值

## 启用的 adapter, 内置有 http, ws, reverse-ws, webhook
adapters:
  - http
  - ws

## 是否开启认证流程, 若为 true 则建立连接时需要验证 verifyKey
## 建议公网连接时开启
enableVerify: true
verifyKey: 1234567890

## 开启一些调式信息
debug: false

## 是否开启单 session 模式, 若为 true,则自动创建 session 绑定 console 中登录的 bot
## 开启后,接口中任何 sessionKey 不需要传递参数
## 若 console 中有多个 bot 登录,则行为未定义
## 确保 console 中只有一个 bot 登陆时启用
singleMode: false

## 历史消息的缓存大小
## 同时,也是 http adapter 的消息队列容量
cacheSize: 4096

## adapter 的单独配置,键名与 adapters 项配置相同
adapterSettings:
  ## 详情看 http adapter 使用说明 配置
  http:
    # 0.0.0.0是允许远程访问,localhost只能同机器访问
    host: 0.0.0.0
    port: 8888
    cors: ["*"]
    unreadQueueMaxSize: 100
  
  ## 详情看 websocket adapter 使用说明 配置
  ws:
    host: localhost
    port: 8080
    reservedSyncId: -1
  1. 启动mirai即可:
./mcl

        首次启动会自动下载jar包。等待启动完成后,输入"?",可以查看所有支持的mcl命令。

  1. 使用以下命令即可登录QQ号:
/login  [password] 

        如果想要启动mcl后自动登录QQ号,可以用:

/autoLogin add   

        也可以设置不同的设备登录。

/autoLogin setConfig  protocol android_PAD

        它对应的配置文件其实就在:config/Console/AutoLogin.yml

  1. 现在QQ风控很严了,第一次登录很有可能遇到“需要滑动验证码”的。建议申请小号使用,以免发生不测。并且首次使用时在QQ“账号安全设置”中关闭“安全登录检查”、“陌生设备登录保护”。如果遇到验证码,可以尝试:

    1. 将Captcha link通过另一个QQ,发给待登录的mirai-QQ,手机登录mirai-QQ并点击链接,手动完成滑块验证,然后回到mobaxterm输入回车;

  1. 如果不行,就参考这个链接的方法:GitHub - project-mirai/mirai-login-solver-selenium: SliderCaptcha solver
  2. 还不行,再参考这个链接的方法:mirai-login-solver-selenium | mirai
  3. 还有一个小技巧可以尝试。在手机端先通过手机号登录QQ,如果没问题,再通过手机号在mirai上登录。手机建议先登录上mirai-QQ,有时可能会弹窗提示“是否允许陌生设备登录”等等,要手动点确认的。
  4. 另外,最新申请的QQ号,一般可以成功登录mirai。
  5. 如果以上都不行,目前的终极方案是使用miraiAndroidMiraiAndroid
  • 在手机上的MiraiAndroid登录QQ后导出device.json
  • 将cache目录下的3个文件account.secrets、servers.json、session.bin也复制出来

  • 接下来点击左上角, 再点击“工具”。选择你机器人的账号, 选择 导出 DEVICE.JSON 将其导出。

  • 再次回到服务器端,进入 “bots/<你的QQ号>” 下面, 将导出的 device.json 复制放入。对应的cache文件夹也复制放入。

  • 再次执行 ./mcl 启动 mirai-console 看看效果。
  • 若仍有问题,欢迎加入文末Q群交流。
  1. 至此,mcl就已经能正常接收QQ消息了。而我们的实现代码对mcl的控制,就是通过mirai-api-http插件来实现的。根据上面第4步配置的_setting.yml_文件,再参考官方API文档HttpAdapter文档,即可实现互联互通。(讲起来比较麻烦,no bibi,后面直接show me the code)。

Python控制mirai篇

        当服务器成功运行了mirai后,我们就可以在本地进行Python脚本的编写了。由于最新的mirai-api-http变更过接口规范,因此网上某些一两年前的代码已经失效了。本教程对应的mirai-api-http使用的是最新的2.x版本。
        接下来的操作,都默认已经完成“启动mcl并login了QQ号”
        在上面setting.yml中,有两个配置项值得注意,他是我们脚本可以控制的密钥:

verifyKey: 1234567890
http: port: 8888

debug输出封装

        简单封装下。直接用print也是可以的。

class Logger:
    def __init__(self, level='debug'):
        self.level = level

    def DebugLog(self, *args):
        if self.level == 'debug':
            print(*args)

    def TraceLog(self, *args):
        if self.level == 'trace':
            print(*args)

    def setDebugLevel(self, level):
        self.level = level.lower()

交互授权

        在交互前,脚本需要先向mirai获取一个verifyKey,之后在每个请求时候,都需要带上这个key,也叫session。其中,参数auth_key对应了上面setting.yml里的verifyKey。

auth_key = '1234567890'

def verifySession(self, auth_key):
    """每个Session只能绑定一个Bot,但一个Bot可有多个Session。
        session Key在未进行校验的情况下,一定时间后将会被自动释放"""
    data = "verifyKey": auth_key
    url = self.addr+'verify'
    res = requests.post(url, data=json.dumps(data)).json()
    logger.DebugLog(res)
    if res['code'] == 0:
        return res['session']
    return None

绑定bot

        使用此方法校验并激活你的Session,同时将Session与一个已登录的Bot绑定。

qq = '121215'             # mirai登录的那个QQ
session = 'grge8484'     # 上面verifySession函数的返回值

def bindSession(self, session, qq):
    """校验并激活Session,同时将Session与一个已登录的Bot绑定"""
    data = "sessionKey": session, "qq": qq
    url = self.addr + 'bind'
    res = requests.post(url, data=json.dumps(data)).json()
    logger.DebugLog(res)
    if res['code'] == 0:
        self.session = session
        return True
    return False

释放bot

        使用此方式释放session及其相关资源(Bot不会被释放)

def releaseSession(self, session, qq):
    """不使用的Session应当被释放,长时间(30分钟)未使用的Session将自动释放,
        否则Session持续保存Bot收到的消息,将会导致内存泄露(开启websocket后将不会自动释放)"""
    data = "sessionKey": session, "qq": qq
    url = self.addr + 'release'
    res = requests.post(url, data=json.dumps(data)).json()
    logger.DebugLog(res)
    if res['code'] == 0:
        return True
    return False

未读消息的数量

        获取当前有多少条未读消息。

def getMessageCount(self, session):
    url = self.addr + 'countMessage?sessionKey='+session
    res = requests.get(url).json()
    if res['code'] == 0:
        return res['data']
    return 0

获取最新的消息

        获取消息后会从队列中移除。

def fetchLatestMessage(self, session):
    url = self.addr + 'fetchLatestMessage?count=10&sessionKey='+session
    res = requests.get(url).json()
    if res['code'] == 0:
        return res['data']
    return None

解析消息内容

        简单实现了部分消息类型的解析,会有消息丢失,请根据使用需求自行调整。

data = 'xxx'  # 可以是上面getMsgFromGroup函数的返回值

def parseGroupMsg(self, data):
    res = []
    if data is None:
        return res
    for item in data:
        if item['type'] == 'GroupMessage':
            type = item['messageChain'][-1]['type']
            if type == 'Image':
                text = item['messageChain'][-1]['url']
            elif type == 'Plain':
                text = item['messageChain'][-1]['text']
            elif type == 'Face':
                text = item['messageChain'][-1]['faceId']
            else:
                logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
                continue
                name = item['sender']['memberName']
                group_id = str(item['sender']['group']['id'])
                group_name = item['sender']['group']['name']
                res.append('text': text, 'type': type, 'name': name, 'groupId': group_id, 'groupName': group_name)
                return res

向好友发送消息

        向指定好友发送消息。

def sendFriendMessage(self, session, qq, msg):
    msg_list = msg.split(r'\\n')
    msg_chain = [ "type": "Plain", "text": m+'\\n'  for m in msg_list]

    data = 
        "sessionKey": session,
        "target": qq,
        "messageChain": msg_chain
    
    url = self.addr + 'sendFriendMessage'
    try:
        res = requests.post(url, data=json.dumps(data)).json()
        except:
            logger.DebugLog(">> 发送失败")
            return 0
        if res['code'] == 0:
            return res['messageId']
        return 0

向群发送消息

        也只是简单实现。

def sendMsgToGroup(self, session, group, msg):
    text = msg['text']
    type = msg['type']
    name = msg['name']
    group_id = msg['groupId']
    group_name = msg['groupName']
    content1 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
        name, group_id, group_name, text)
    content2 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
        name, group_id, group_name)
    logger.DebugLog(">> 消息类型:" + type)
    if type == 'Plain':
        message = ["type": type, "text": content1]
    elif type == 'Image':
        message = [
            "type": 'Plain', "text": content2,
            "type": type, "url": text]
    elif type == 'Face':
        message = ["type": 'Plain', "text": content2,
                   "type": type, "faceId": text]
    else:
        logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
        return 0
    data = 
        "sessionKey": session,
        "group": group,
        "messageChain": message
    
    logger.DebugLog(">> 消息内容:" + str(data))
    url = self.addr + 'sendGroupMessage'
    try:
        res = requests.post(url, data=json.dumps(data)).json()
        except:
            logger.DebugLog(">> 转发失败")
            return 0
        logger.DebugLog(">> 请求返回:" + str(res))
        if res['code'] == 0:
            return res['messageId']
        return 0

向群发送富文本消息

        跟上面的差不多,消息类型变了一下,从而支持类似html形式的消息发送。

def sendPlainTextToGroup(self, session, group, msg):
    msg_list = msg.split(r'\\n')
    msg_chain = [ "type": "Plain", "text": m+'\\n'  for m in msg_list]
    data = 
        "sessionKey": session,
        "group": group,
        "messageChain": msg_chain
    
    url = self.addr + 'sendGroupMessage'
    try:
        res = requests.post(url, data=json.dumps(data)).json()
        except:
            logger.DebugLog(">> 转发失败")
            return 0
        logger.DebugLog(">> 请求返回:" + str(res))
        if res['code'] == 0:
            return res['messageId']
        return 0

        以上就是几个简单、常用的函数。基于这些函数,就已经可以实现蛮多有趣的功能了。

Q群消息转发

        这部分可以直接参考之前的博客:Q群消息转发例程。其实也就是把上面的函数整合一下,放一个完整版:

import requests
from time import sleep

class Logger:
    def __init__(self, level='debug'):
        self.level = level

    def DebugLog(self, *args):
        if self.level == 'debug':
            print(*args)

    def TraceLog(self, *args):
        if self.level == 'trace':
            print(*args)

    def setDebugLevel(self, level):
        self.level = level.lower()

logger = Logger()
class QQBot:
    def __init__(self):
        self.addr = 'http://43.143.12.250:8888/'
        self.session = None

    def verifySession(self, auth_key):
        """每个Session只能绑定一个Bot,但一个Bot可有多个Session。
        session Key在未进行校验的情况下,一定时间后将会被自动释放"""
        data = "verifyKey": auth_key
        url = self.addr+'verify'
        res = requests.post(url, data=json.dumps(data)).json()
        logger.DebugLog(res)
        if res['code'] == 0:
            return res['session']
        return None

    def bindSession(self, session, qq):
        """校验并激活Session,同时将Session与一个已登录的Bot绑定"""
        data = "sessionKey": session, "qq": qq
        url = self.addr + 'bind'
        res = requests.post(url, data=json.dumps(data)).json()
        logger.DebugLog(res)
        if res['code'] == 0:
            self.session = session
            return True
        return False

    def releaseSession(self, session, qq):
        """不使用的Session应当被释放,长时间(30分钟)未使用的Session将自动释放,
        否则Session持续保存Bot收到的消息,将会导致内存泄露(开启websocket后将不会自动释放)"""
        data = "sessionKey": session, "qq": qq
        url = self.addr + 'release'
        res = requests.post(url, data=json.dumps(data)).json()
        logger.DebugLog(res)
        if res['code'] == 0:
            return True
        return False

    def fetchLatestMessage(self, session):
        url = self.addr + 'fetchLatestMessage?count=10&sessionKey='+session
        res = requests.get(url).json()
        if res['code'] == 0:
            return res['data']
        return None

    def parseGroupMsg(self, data):
        res = []
        if data is None:
            return res
        for item in data:
            if item['type'] == 'GroupMessage':
                type = item['messageChain'][-1]['type']
                if type == 'Image':
                    text = item['messageChain'][-1]['url']
                elif type == 'Plain':
                    text = item['messageChain'][-1]['text']
                elif type == 'Face':
                    text = item['messageChain'][-1]['faceId']
                else:
                    logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
                    continue
                name = item['sender']['memberName']
                group_id = str(item['sender']['group']['id'])
                group_name = item['sender']['group']['name']
                res.append('text': text, 'type': type, 'name': name, 'groupId': group_id, 'groupName': group_name)
        return res

    def getMessageCount(self, session):
        url = self.addr + 'countMessage?sessionKey='+session
        res = requests.get(url).json()
        if res['code'] == 0:
            return res['data']
        return 0

    def sendPlainTextToGroup(self, session, group, msg):
        msg_list = msg.split(r'\\n')
        msg_chain = [ "type": "Plain", "text": m+'\\n'  for m in msg_list]
        data = 
          "sessionKey": session,
          "group": group,
          "messageChain": msg_chain
        
        url = self.addr + 'sendGroupMessage'
        try:
            res = requests.post(url, data=json.dumps(data)).json()
        except:
            logger.DebugLog(">> 转发失败")
            return 0
        logger.DebugLog(">> 请求返回:" + str(res))
        if res['code'] == 0:
            return res['messageId']
        return 0

    def sendMsgToGroup(self, session, group, msg):
        text = msg['text']
        type = msg['type']
        name = msg['name']
        group_id = msg['groupId']
        group_name = msg['groupName']
        content1 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
            name, group_id, group_name, text)
        content2 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
            name, group_id, group_name)
        logger.DebugLog(">> 消息类型:" + type)
        if type == 'Plain':
            message = ["type": type, "text": content1]
        elif type == 'Image':
            message = [
                "type": 'Plain', "text": content2,
                "type": type, "url": text]
        elif type == 'Face':
            message = ["type": 'Plain', "text": content2,
                       "type": type, "faceId": text]
        else:
            logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
            return 0
        data = 
                "sessionKey": session,
                "group": group,
                "messageChain": message
                
        logger.DebugLog(">> 消息内容:" + str(data))
        url = self.addr + 'sendGroupMessage'
        try:
            res = requests.post(url, data=json.dumps(data)).json()
        except:
            logger.DebugLog(">> 转发失败")
            return 0
        logger.DebugLog(">> 请求返回:" + str(res))
        if res['code'] == 0:
            return res['messageId']
        return 0

    def sendMsgToAllGroups(self, session, receive_groups, send_groups, msg_data):
        # 对每条消息进行检查
        for msg in msg_data:
            group_id = msg['groupId']
            # 接收的消息群正确(目前只支持 消息类型)
            if group_id in receive_groups:
                # 依次将消息转发到目标群
                for g in send_groups:
                    logger.DebugLog(">> 当前群:"+g)
                    if g == group_id:
                        logger.DebugLog(">> 跳过此群")
                        continue
                    res = self.sendMsgToGroup(session, g, msg)
                    if res != 0:
                        logger.TraceLog(">> 转发成功!".format(g))

    def sendFriendMessage(self, session, qq, msg):
        msg_list = msg.split(r'\\n')
        msg_chain = [ "type": "Plain", "text": m+'\\n'  for m in msg_list]

        data = 
          "sessionKey": session,
          "target": qq,
          "messageChain": msg_chain
        
        url = self.addr + 'sendFriendMessage'
        try:
            res = requests.post(url, data=json.dumps(data)).json()
        except:
            logger.DebugLog(">> 发送失败")
            return 0
        if res['code'] == 0:
            return res['messageId']
        return 0

def qqTransfer():
    with open('conf.json', 'r+', encoding="utf-8") as f:
        content = f.read()
    conf = json.loads(content)

    auth_key = conf['auth_key']
    bind_qq = conf['bind_qq']
    sleep_time = conf['sleep_time']
    debug_level = conf['debug_level']

    receive_groups = conf['receive_groups']
    send_groups = conf['send_groups']

    logger.setDebugLevel(debug_level)

    session = bot.verifySession(auth_key)
    logger.DebugLog(">> session: "+session)
    bot.bindSession(session, bind_qq)
    while True:
        cnt = bot.getMessageCount(session)
        if cnt:
            logger.DebugLog('>> 有消息了 => '.format(cnt))
            logger.DebugLog('获取消息内容')
            data = bot.fetchLatestMessage(session)
            if len(data) == 0:
                logger.DebugLog('消息为空')
                continue
            logger.DebugLog(data)
            logger.DebugLog('解析消息内容')
            data = bot.parseGroupMsg(data)
            logger.DebugLog(data)
            logger.DebugLog('转发消息内容')
            bot.sendMsgToAllGroups(session, receive_groups, send_groups, data)
        # else:
        #     logger.DebugLog('空闲')
        sleep(sleep_time)
    bot.releaseSession(session, bind_qq)

        其中,conf.json内容为:


  "auth_key": "1234567890",
  "bind_qq":  "123456",                                                     # mirai登录的QQ (复制时记得删我)
  "sleep_time": 1,
  "receive_groups": ["913182235", "977307922"],  # 要接受消息的群 (复制时记得删我)
  "send_groups": ["913182235", "977307922"],         # 要发送消息的群 (复制时记得删我)
  "debug_level": "debug"

        下面,我们就先从类似QMsg酱的消息通知开始。

类似QMsg酱的消息通知

设计目标:通过调用指定的URL,小锋仔机器人就会给指定的好友发送指定的消息。
        关于QMsg酱的使用教程可以看:免费的QQ微信消息推送机器人
        前面我们特地开放了9966端口,因此可以使用Flask来监听这个端口。
        本着越简单越好的原则,我们把“发给好友还是群”、“目标好友或群的号”、“发送的内容”三部分都拼接到URL上,因此有:

http://43.143.12.250:9966/QQ/send/friend?target=123&msg=hello
http://43.143.12.250:9966/QQ/send/group?target=123&msg=hello

因此,代码可以写成:

from flask import Flask, request

app = Flask(__name__)

@app.route('/QQ/send/friend', methods=['GET'])
def qqListenMsgToFriend():
    # 类似于Qmsg的功能
    # flask做得接收HTTP请求转为QQ消息
    qq = request.args.get('target', None)
    msg = request.args.get('msg', None)
    bot.sendFriendMessage(bot.session, qq, msg)
    return 'Hello World! Friend!'

@app.route('/QQ/send/group', methods=['GET'])
def qqListenMsgToGroup():
    # 类似于Qmsg的功能
    # flask做得接收HTTP请求转为QQ消息
    qq = request.args.get('target', None)
    msg = request.args.get('msg', None)
    bot.sendPlainTextToGroup(bot.session, qq, msg)
    return 'Hello World! Group!'

if __name__ == '__main__':
    app.run(port='9966', host='0.0.0.0')

        由于Flask和小锋仔QQBot都要阻塞运行,因此稍微变动一下,让小锋仔以子线程的形式运行即可。

if __name__ == '__main__':
    t = threading.Thread(target=qqTransfer)
    t.setDaemon(True)
    t.start()

    app.run(port='9966', host='0.0.0.0')

        测试一下:

http://localhost:9966/QQ/send/friend?target=1061700625&msg=hello


        如果我们把这个脚本放到服务器上去运行,那么链接就变成了:

http://43.143.12.250:9966/QQ/send/friend?target=1061700625&msg=hello

        当然,能发消息的前提是“先加好友”或“加群”啦。

多功能切换的实现设计

        上面我们进行了简单地尝鲜。
1、从这部分开始,我们涉及的功能比较杂,为了能更好的区分功能,需要设计一个简单的交互协议。

  • 我们发送的内容可以分为:功能选择 与 消息详情
  • 为了区分他俩,可以在选择功能时添加指定前缀,如“CMD+翻译”;
  • 小锋仔接收到后,进入翻译模式准备;
  • 发送指令详情时,就不加前缀。而小锋仔则将收到的消息进行翻译,再把结果返回。

        根据以上内容,小锋仔需要记录的状态信息至少有:

class StatusStore:
    def __init__(self, from_qq:int=None, is_cmd:bool=False, func_name:str=None, need_second:bool=False, msg:str=None) -> None:
        self.from_qq = from_qq          # 发送者的QQ号
        self.is_cmd = is_cmd            # 是否是指令(选择功能)
        self.func_name = func_name      # 选择的功能的名称
        self.need_second = need_second  # 是否需要经过两步:先发cmd指令,再发详细内容
        self.msg = msg                  # 本次发送的消息内容
    
    def detail(self):
        return self.__dict__

2、并且我们设置,只有从指定QQ发过来消息,才能响应。因此在接收到消息时,需要判断对方的信息。对于好友类型的消息,mirai返回格式如消息类型说明


  "type": "FriendMessage",
  "sender": 
    "id": 123,
    "nickname": "",
    "remark": ""
  ,
  "messageChain": [] // 数组,内容为下文消息类型

因此,我们可以从"type"和 "sender:id"入手判断。
3、我们暂时考虑只有一个主QQ能发送指令的情况。
4、定义一个类来专门管理不同功能的函数,例如:

class MultiFunction:
    """多功能函数集合"""
    def __init__(self) -> None:
        pass

    @staticmethod
    def translate(original:str, convert:str='zh2en') -> str:
        return '假装是翻译结果' 
    
    @staticmethod
    def uploadImage(image_path:str) -> str:
        return '假装是上传结果' 

    @staticmethod
    def weather(city:str) -> str:
        return '假装是天气结果' 
    
    @staticmethod
    def hotNews(status_store:StatusStore) -> str:
        return '假装是热搜结果' 


# 多功能函数的映射
# function: 功能对应函数名
# need_second: 是否需要经过两步:先发cmd指令,再发详细内容
# desc: 需要经过两步时,第一次返回的提示语
function_map = 
    '翻译': 'function': MultiFunction.translate, 'need_second': True, 'desc': '请输入您要翻译的内容~', 
    '天气': 'function': MultiFunction.weather, 'need_second': True, 'desc': '请问是哪座城市的天气呢?', 
    '热搜': 'function': MultiFunction.hotNews, 'need_second': False


def choiceFunction(store_obj:StatusStore):
    res = ''
    if function_map.get(store_obj.func_name):
        res = function_map.get(store_obj.func_name)['function'](store_obj.msg)
    return res 

5、大致实现流程的想法是:


        对应代码实现:

def analyzeFriendMsg(self, data):
    if data is None or data['type'] != 'FriendMessage':
        return None, None, None
    sender_id = data['sender']['id']
    msg_type = data['messageChain'][-1]['type']
    if msg_type == 'Plain':
        msg_text = data['messageChain'][-1]['text']
    elif msg_type == 'Image':
        msg_text = data['messageChain'][-1]['url']
    else:
        msg_text = ''
        return sender_id, msg_type, msg_text

        最终的框架就是:

def xiaofengzai():
    auth_key = '1234567890'     # settings.yml中的verifyKey
    bind_qq = '3126229950'      # mirai登录的QQ
    target_qq = '1061700625'    # 我们自己用的主QQ
    target_qq = int(target_qq)  # 接收到的消息里,QQ是int类型的
    sleep_time = 1              # 轮询间隔
    status_store = 

    session = bot.verifySession(auth_key)
    logger.DebugLog(">> session: "+session)
    bot.bindSession(session, bind_qq)
    while True:
        cnt = bot.getMessageCount(session)
        if not cnt:
            sleep(sleep_time)
            continue
        logger.DebugLog('>> 有消息了 => '.format(cnt))
        logger.DebugLog('获取消息内容')
        data = bot.fetchLatestMessage(session)
        if len(data) == 0:
            logger.DebugLog('消息为空')
            sleep(sleep_time)
            continue
        logger.DebugLog(data)
        logger.DebugLog('解析消息内容')

        sender_id, msg_type, msg_text = bot.analyzeFriendMsg(data[0])
        if not sender_id or sender_id != target_qq:
            sleep(sleep_time)
            continue

        if msg_text.strip().lower().startswith('cmd'):
            _, func_name = msg_text.strip().split('\\n')[0].split()
            func_name = func_name.strip()
            store_obj = StatusStore(from_qq=sender_id, is_cmd=True, func_name=func_name)
            # 不需要发两次,直接调用函数返回结果即可
            func_info = function_map.get(func_name)
            if not func_info:
                res = '指令[]暂不支持'.format(func_name)
            elif func_info.get('need_second'):
                res = '收到你的指令:\\n'.format(func_name, func_info.get('desc') or '已进入对应状态, 请继续发送详细内容')
                # 添加或更新记录
                status_store[sender_id] = store_obj
            else:
                res = '请求结果为:\\n' + str(choiceFunction(store_obj))
                status_store.pop(sender_id, '')
        else:
            res = '请先发送指令哦...'
            store_obj = status_store.get(sender_id)
            if store_obj and store_obj.is_cmd:
                store_obj.msg = msg_text
                res = '请求结果为:\\n' + str(choiceFunction(store_obj))
                status_store.pop(sender_id, '')
        
        bot.sendFriendMessage(session, qq=sender_id, msg=res)

        看一下效果:


        至此,骨架有了,接下来开始填充功能了。

翻译查询

        根据上面的骨架可知,我们只需要实现MultiFunction类下的translate函数即可。如果想快速测试函数效果,可以使用以下代码,而不用先启动mirai:

res = choiceFunction(StatusStore(func_name='翻译', msg='你好'))
print(res)

领取腾讯免费翻译API

        要做翻译,最方便的就是调用API了(没错,调包侠!)。
        这里使用腾讯的翻译API,可以免费领取:领取腾讯翻译API。点进链接后,往下拖到“云产品体验”专区,选择“人工智能”,下面有“机器翻译”。他的调用量是每月更新,非常的良心了。


        点击“立即体验”,进入控制台界面,虽然上面显示的是“开通付费版”,但不用担心,他是有免费额度的,更何况你账户里又没充余额,哈哈哈。

        支持很多类型的翻译,这次我们先选文本翻译机器翻译 文本翻译-API 文档-文档中心-腾讯云

        我们用SDK的方式,免去了自己封装复杂的加密步骤:

pip install --upgrade tencentcloud-sdk-python

        然后去获取密钥API密钥管理,记下APPID、SecretId、SecretKey


机器人接入翻译功能

        小锋仔bot结合翻译功能,直接上代码:

import json
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.tmt.v20180321 import tmt_client, models

def translate(original:str, convert:str='en'):
    secretId = 'xxx'  # 从API控制台获取
    secretKey = 'xxx' # 从API控制台获取
    AppId = 12123     # 从API控制台获取
    try:
        cred = credential.Credential(secretId, secretKey)
        client = tmt_client.TmtClient(cred, "ap-guangzhou")
        req = models.TextTranslateRequest()
        params = 
            "SourceText": original,
            "Source": "auto",
            "Target": convert,
            "ProjectId": AppId
        
        req.from_json_string(json.dumps(params))
        resp = client.TextTranslate(req)
        # print(resp.to_json_string())
        return resp.TargetText
    except TencentCloudSDKException as err:
        print(err)
        return ''

        使用测试效果:

print(choiceFunction(StatusStore(func_name='翻译', msg='你好')))

# 输出:
"TargetText": "Hello", "Source": "zh", "Target": "en", "RequestId": "a1b17f47-751e-44cd-89a5-6a22e9f2c444"
Hello


实时天气

领取免费的和风天气API

        天气部分,我们是用免费的和风天气API:实时天气 - API
        首先也要进行登录并获取KEY,这个步骤官网讲的很详细,图文并茂的,这边就不多写了,大家可以跳转过去(注意我们选的是Web API):创建应用和KEY - RESOURCE

机器人接入天气功能

        同样的,直接上代码:

def weather(city:str) -> str:
    url_api_weather = 'https://devapi.qweather.com/v7/weather/now?'
    url_api_geo = 'https://geoapi.qweather.com/v2/city/lookup?'
    weather_key = 'xxxxx'  # 和风天气控制台的key

    # 实况天气
    def getCityId(city_kw):
        url_v2 = url_api_geo + 'location=' + city_kw + '&key=' + weather_key
        city = requests.get(url_v2).json()['location'][0]
        return city['id']

    city_name = '广州'
    city_id = getCityId(city_name)
    url = url_api_weather + 'location=' + city_id + '&key=' + weather_key
    res = requests.get(url).json()
    text = "<天气信息获取失败>"
    if res['code'] == '200' or res['code'] == 200:
        text = '实时天气:\\n 亲爱的 小主, 您所在的地区为 ,\\n 现在的天气是 ,\\n 气温 °, 湿度 %,\\n 体感气温为 °,\\n 风向 , 风速 km/h'.format(
            city_name, res['now']['text'], res['now']['temp'], res['now']['humidity'], res['now']['feelsLike'], res['now']['windDir'], res['now']['windSpeed']) 
    return text 

        测试效果:


  • 本文是整理了JS中的一些重点,难点,以及不好理解的知识点
  • 本文非常详细,深入的讲解,包学包会
    在这里插入图片描述

1. JS函数

1.1 函数(Function)是什么?

  • 函数(方法)是由事件驱动的或者当它被调用时执行的可重复使用的代码块 —— 官方说明
  • 向来觉得官方的文档是有些生硬的,举个例子:

函数可以看做是功能(以一辆汽车为例,如下图),这些都可以看做成是方法

  • 刹车
  • 油门
  • 鸣笛
  • 档位

在这里插入图片描述

  • 这些功能任何一个里面都有很多个零件的配合,共同完成某一个任务,我们只需要去调用(踩刹车,踩油门,按喇叭,挂挡),功能就会执行
  • 函数也是一样的,它内部封装了一些操作,只有我们去调用的时候才会执行

1.2 一个最简单的函数及触发方法

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>我的第一个方法</title>
    </head>
    <body>
        <button onclick="myFunction()">点击触发函数</button>
        <script>
            // 必须有 function关键字,命名通常为驼峰命名,首字母小写
            function myFunction(){
                alert("这是我的函数");
            }
        </script>
    </body>
</html>

1.3 带参数的函数(形参与实参)

  • 形参 : 函数中定义的变量(此时是没有值的,只是一个代称)
  • 实参 : 在运行时的函数调用时传入的参数(实际的值)
  • js中,方法中即使定义了形参,调用时不传实参也不会报错
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>形参与实参</title>
    </head>
    <body>
        <!-- 这里的52是实参 -->
        <button onclick="addNum(5, 2)">计算5+2的值</button>
        <script>
            // 此处的num1,与num2便是形参
            function addNum(num1, num2){
                alert(num1 + num2)
            }
        </script>
    </body>
</html>

1.4 带有返回值的函数 ———— return

function fn(a, b){
    return a*b;
}
// 调用并给num赋值
let num = fn(3, 5);
console.log(num) // 得到15

1.5 js函数内置对象 ———— arguments(重点,考点)

  • 它是函数一创建就有的
  • 是一个类数组(并不是真正的数组)
  • 方法调用时,可以得到所有传进来的参数
  • 你传多少,我就能拿到多少
function fn(){
    console.log(arguments)
 }
 fn(1, 2, 3, 4);

在这里插入图片描述
经典应用 ———— 求一组参数的总和

 function fn(){
    let sum = 0;
    for(let i = 0; i < arguments.length; i++){
        sum += arguments[i];
    }
    // 返回 sum
    return sum
 }
 let allSum = fn(1, 2, 3, 4);
 console.log(allSum)    // 得到10

1.6 函数内的变量

  • 在函数内的定义的变量均为局部变量
  • 函数运行完之后就会销毁(垃圾回收机制),所以外界无法访问
  • 变量应尽量避免重名(局部与全局变量可能会混淆,导致一些意料之外的问题
function fn() {
    // 此为局部变量
    let a = 5;
    console.log(a)
}
fn();
console.log(a) // 此处报错,无法访问

1.7 匿名函数(难点)

  • 顾名思义指的是没有名字的函数
  • 必须采用下面的语法,否则会报错
(function (){
    //由于没有执行该匿名函数,所以不会执行匿名函数体内的语句。
    console.log("666");
})

匿名自执行函数(类似于JS的单例模式)

(function (){
    console.log("666"); // 此处会打印666
})()

2. JS事件

  • HTML 事件是发生在 HTML 元素上的事情。
  • JavaScript 可以触发这些事件。
  • 可以看做是用户的某些操作,或者说业务需要监听的某些操作

2.1 HTML事件

  • HTML 页面完成加载
  • HTML input 字段改变时
  • HTML 按钮被点击

常用事件整理

事件名说明
onchange()HTML 元素改变(一般用于表单元素)
onclick ()用户点击 HTML 元素
onmouseover()用户在一个HTML元素上移动鼠标
onmouseout()用户从一个HTML元素上移开鼠标
onkeydown()用户按下键盘按键
onkeyup()键盘按键弹起
onload()浏览器已完成页面的加载

2.2 JavaScript 事件一般用于做什么?

  • 页面加载时触发事件
  • 页面关闭时触发事件
  • 用户点击按钮执行动作
  • 验证用户输入内容的合法性
  • …(用户的一切操作都可以监听

2.3 事件实例

<input id="test" type="button" value="提交"/>
<script>
// 页面加载完触发
window.onload = function(){
    let test = document.getElementById("test");   
    test.addEventListener("click",myfun2);   
    test.addEventListener("click",myfun1);
}
function myfun1(){  
    alert("你好1");
}
function myfun2(){ 
    alert("你好2");
}
</script>

3. JavaScript 对象

在JS里 —— 万物皆为对象
在这里插入图片描述

  • 字符串也可以是一个对象
  • 日期是一个对象
  • 数学和正则表达式也是对象
  • 数组是一个对象
  • 函数也可以是对象

3.1 对象定义

  • 对象是变量的容器
  • 写法以键值对的方式(键名:键值)
  • 键值对称之为对象的属性
  • 循环对象一般用 for in
// 对象定义
let person = {
    firstName:"ouyang",
    lastName:"xiu",
    age:18
};

// 循环对象
for(let key in person){
	console.log(key);	// 键名
	console.log(person[key])	// 键值
}

3.2 大厂经典面试题分析

let obj = Object.create(null) 与 let obj = {} 有什么区别?

  • 之前腾讯面试的时候,问了这个问题:对象字面量创建对象与 Object.create(null)创建对象有什么区别?
  • 一开始是有点懵的,不都是创建对象么,能有啥不同,后面我去试了一下,结果发现还蛮有意思:
let obj = {};
let obj2 = Object.create(null);
console.log(obj);
console.log(obj2)
  • 控制台打印
    在这里插入图片描述
  • 乍一看,好像没啥区别,都是一个花括号
  • 然而,展开后,确实大有不同
    在这里插入图片描述
  • Object.create(null)创建的对象是非常纯净的,没有任何其它元素
  • 而另一个let创建的对象是带有_proto_的,下面有一些方法与属性,这便是js的原型链继承,它继承了Object的方法和属性。这便是区别。

所以这种区别导致了使用场景不同

  • 如果需要对象的继承属性和方法,那就使用 let obj = {};
  • 如果只需要一个纯净的对象,那就使用 Object.create(null)
  • 比如说,我只需要用对象来保存一些数据,然后进行循环取用,提高循环效率。
  • 这个时候如果对象有原型链,那便会在循环的时候去循环它的各个属性和方法
  • 然而这不是必要的,我们只是要他里面的元素而已,前者会影响循环效率
    在这里插入图片描述

4. JavaScript prototype(原型对象)

  • 此属性是函数特有的
  • 每个函数都会默认添加一个
  • 用于继承属性和方法
// 创建构造函数
function Person(name, age) {
    this.age = age;
    this.name= name;
    this.fn = function(){
        console.log(this.name)
    }
}
// 创建实例
let person1 = new Person("小明", 18);
let person2 = new Person("小红", 20);
person1.fn(); // 继承父级的方法
person2.fn();
console.log(person1)
console.log(person2)

执行结果
在这里插入图片描述

  • 要添加一个新的属性需要在在构造器函数中添加:
function Person(name, age, sex) {
   // sex为新属性
   this.sex = sex;
   this.age = age;
   this.name= name;
   this.fn = function(){
       console.log(this.name)
   }
}

4.1 prototype 继承

所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法:

  • Date 对象从 Date.prototype 继承
  • Array 对象从 Array.prototype 继承
  • Person 对象从 Person.prototype 继承

所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例

  • JavaScript 对象有一个指向一个原型对象的链
  • 当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾(逐级查找
  • Date 对象, Array 对象, 以及 Person 对象从 Object.prototype 继承。

4.2 添加属性和方法

function Person(name, age, sex) {
    // sex为新属性
    this.sex = sex;
    this.age = age;
    this.name= name;
    this.fn = function(){
        console.log(this.name)
    }
}
Person.prototype.newVal = "我是新添加在原型上的值";
let person1 = new Person("小明", 18);

console.log(person1)
  • 一样可以通过继承拿到
    在这里插入图片描述

5. call和apply及bind三者的区别(面试重点)

  • this指向,apply,call,bind的区别是一个经典的面试问题
  • 同时在项目中会经常使用到的原生的js方法。
  • 也是ES5中的众多坑的一个

5.1 从this说起

  • this指向 = 谁调用,指向谁(这是错误的!!!
  • this永远指向最后一个调用它的那个对象(正解)

如何解决this指向问题?

在这里插入图片描述

  1. 使用ES6中箭头函数

  2. 函数内部使用_this = this

  3. 使用apply,call,bind方法

  4. new实例化一个对象

5.2 谈谈apply,call,bind

  • apply()
let obj = {
    name : "小明",
    func1: function () {
        console.log(this.name)
    },
    func2: function () {
        setTimeout(  function () {
            this.func1()
        }.apply(name),1000);
    }
};
obj.func2()            // 小明
  1. apply() 方法调用一个函数,其具有一个指定的this值,以及作为一个数组(或者类似数组的对象)提供的参数,fun.apply(thisArg, [argsArray])

  2. thisArg:在fun函数运行时指定的this值。指定this的值并不一定是函数执行时真正的this值,如果是原始值的this会指向该原始值的自动包装对象。

  3. argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给fun函数。参数为null或者undefined,则表示不需要传入任何参数。

  • call()
let obj2 = {
    name : "小红",
    func1: function () {
        console.log(this.name)
    },
    func2: function () {
        setTimeout(  function () {
            this.func1()
        }.call(name),1000);
    }
};
obj2.func2()            // 小红
  1. call() 调用一个函数,其具有一个指定的this值,以及若干个参数列表,fun.call(thisArg, arg1, arg2, …)

  2. thisArg:在fun函数运行时指定的this值。指定this的值并不一定是函数执行时真正的this值,如果是原始值的this会指向该原始值的自动包装对象。

  3. arg1, arg2, …:若干个参数列表

  • bind()
let obj3 = {
    name : "小猪",
    func1: function () {
        console.log(this.name)
    },
    func2: function () {
        setTimeout(  function () {
            this.func1()
        }.bind(name)(),1000);
    }
};
obj3.func2()            // 小猪
  1. bind() 创建一个新的函数,当被调用时,将其this的关键字设置为提供的值,在调用新函数时,在任何提供一个给定的参数序列。

  2. bind创建了一个新函数,必须手动去调用

5.3 区别

  • apply和call基本类似,他们的区别只是传入的参数不同。
  • apply传入的参数是包含多个参数的数组
  • call传入的参数是若干个参数列表
  • bind方法会创建一个新的函数,当被调用的时候,将其this关键字设置为提供的值,我们必须手动去调用
    在这里插入图片描述

6. Javascript的事件流模型(面试重点)

  • 事件冒泡:事件开始由最具体的元素接受,然后逐级向上传播
    在这里插入图片描述

  • 事件捕捉:事件由最不具体的节点先接收,然后逐级向下,一直到最具体的(与上面相反

  • DOM事件流:三个阶段:事件捕捉,目标阶段,事件冒泡

7. 防抖与节流(面试精选)

7.1 函数防抖

  • 当持续触发事件时,一段时间内只能触发一次。将几次操作合并为一此操作进行。比如说有一条赛车通道,赛车通过的时间为5s,5s之后到达终点,执行领奖操作
    在这里插入图片描述
  • 这5s之内只允许一辆赛车在通道内,如果第一辆赛车还在通道内,此时第二辆赛车已经进来了,那么销毁第一辆赛车,从第二辆车入场重新计时5s执行领奖操作
    在这里插入图片描述

应用场景(数据抖动问题)

let telInput = document.querySelector('input');
telInput.addEventListener('input', function(e) {
        //如果直接每次发请求,会导致性能问题
        //数据请求
        let timeOut = null;
        if(timeOut){
			clearTimeout(timeOut)
		}else{
			timeOut = setTimeout(()=>{
			  $.ajax({})
			  },2000)
	    }
})

7.2 函数节流

  • 当持续触发事件时,保证一定时间段内只调用一次事件处理函数。节流,顾名思义,节制流入或流出。
  • 比如说水龙头放水,一旦打开开关,水流就会很快,我们要做的就是限制流出

应用场景(客运站问题)

  • 把整个事件处理器比喻成客运站,如果客运大巴到站就走,那么路上肯定会发生交通拥堵,而且车大部分是空的

  • 因为没给时间上客,虚假繁忙的情况肯定是不好的,那么怎么处理呢?

  • 设置一个时间间隔,时间间隔内只允许执行一次,客运站大巴设定一个时间,到点才会走

let throttle = function(func, delay) {            
  let prev = Date.now();            
  return function() {                
    var context = this;                
    var args = arguments;                
    var now = Date.now();                
    if (now - prev >= delay) {                    
      func.apply(context, args);                    
      prev = Date.now();                
    }            
  }        
}        
function demo() {            
  //do something
    //ajax({})
    //...      
}        
box.addEventListener('touchmove', throttle(demo, 2000));

8. JS中的虚拟DOM是什么?(面试重点)

在这里插入图片描述

8.1 为什么要有虚拟dom?

  • 文档对象模型或 DOM 定义了一个接口,该接口允许 JavaScript 之类的语言访问和操作 HTML 文档
  • 但是此接口需要付出代价,大量非常频繁的 DOM 操作会使页面速度变慢
  • 虚拟dom的出现就是为了解决操作dom的性能问题

8.2 虚拟dom是什么?好处是?

  • 本质就是JS对象
  • 真实节点抽象成JS对象(文档结构树)
  • 虚拟节点(VNode)表示 DOM 树中的节点。当需要操纵时,可以在虚拟 DOM的 内存中执行计算和操作,而不是在真实 DOM 上进行操纵
  • 相对于直接操作dom,这自然会更快

9. 手写一个new,实现同等功能

function Person(name) {
    this.name = name
    this.sayName= function () {
        console.log(`我是 ${this.name}!`)
    }
}


function myNew(that, ...args) {
    const obj = Object.create(null)
    obj.__proto__ = that.prototype
    const res = that.call(obj, ...args)
    return res instanceof Object ? res : obj
}
let person= myNew(Person, '小明')
person.sayWorld(); // 我是小明

10. 获得页面url参数的值(常用)

function getQueryString(name) { 
  var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); 
  var r = window.location.search.substr(1).match(reg); 
  if (r != null) return unescape(r[2]); 
  return null; 
} 

1. 希望本文能对大家有所帮助,如有错误,敬请指出

2. 原创不易,还请各位客官动动发财的小手支持一波(关注、评论、点赞、收藏)
3. 拜谢各位!

在这里插入图片描述

以上是关于教程万字长文保姆级教你制作自己的多功能QQ机器人的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript保姆级教程 ——— 重难点详细解析(万字长文,建议收藏)

JavaScript保姆级教程 ——— 重难点详细解析(万字长文,建议收藏)

建议收藏万字长文,教你发布鸿蒙(HarmonyOS)组件到Maven中央仓库,全网最全教程!

阿里内部第一本“凤凰架构”,保姆级教你构建可靠大型分布式系统

保姆级教你三字诀15天玩会Linux系统-避免踩坑!!!收藏不亏

帝王级教你如何注册使用ChatGPT教程及登录注册安装方法