acwing-Diango项目 (后半)

news2024/10/7 16:19:06

acwing-Django项目

文章目录

  • acwing-Django项目
  • 前言
  • 5. 创建账号系统
  • 5.1用户名密码登录
    • 写登录界面
    • 写注册界面
    • 写动作 实现三个函数 register login logout
  • 5.2 Web端acapp一键登录
    • 在django中集成redis(准备工作)
      • 首先 pip install django_redis
      • 配置一下缓存
      • 启动redis-server
      • redis在django中怎么来操作
    • 第三方授权登录的流程
      • 在数据库增加一个openid
      • 接下来就要实现这个流程
        • 第一步申请授权码code
        • 第二步申请授权令牌access_token和用户的openid
        • 第三步申请用户身份信息
  • 5.3 acapp端实现一键授权登录
  • 6. 实现联机对战
    • 统一长度单位
    • 增加联机对战模式
    • 配置django_channels
      • django_channels是负责客户端和server双向通信的
      • 配置过程
      • 前端和后端的连接
    • 编写同步函数(核心)
      • 同步第一个事件,create_player
      • 同步第二个事件move_to()
      • 同步第三个事件shoot_fireball()
      • 同步第四个事件attack
      • 计分板
      • 技能cd
      • 同步第五个事件 blink 闪现技能
  • 7. 实现聊天系统
  • 8. 实现匹配系统
    • 8.2项目收尾
  • 9. Rest Framework与JWT身份验证


前言

本文来自acwing上付费的Django项目,在写的时候mark一下做一个梳理总结
ip地址:8.134.129.207
通过django打开需要在cd acapp,
python3 manage.py runserver 0.0.0.0:8000

通过acwing分配的域名打开需要cd acapp

uwsgi --ini scripts/uwsgi.ini

启动django_channels

daphne -b 0.0.0.0 -p 5015 acapp.asgi:application

启动Thrift服务,cd acapp/match_system/src/,

./main.py

可以在django项目的根目录也就是cd acapp中,python3 manage.py shell打开交互式python环境
from django.core.cache import cache可以导入缓存模块
from django.contrib.auth.models import User, 导入User模块,使用 User 模型来查询数据库中的用户信息了

然后,https://app4189.acapp.acwing.com.cn
或者是 在acwing的网址点到我的应用

5. 创建账号系统

5.1用户名密码登录

cd acapp, python3 manage.py createsuperuser
我创建的炒鸡用户是ljh, 密码是Ljh
在网址后面/admin就能登录了

在models里创建新的数据库的表
cd acapp/game/models,
因为有很多表要创,所以mkdir player, 在python里创建文件夹一定要在里面整一个__init__.py
然后vim player.py,用来储存player表的信息
路径 : acapp/game/models/player/player.py
[
(如果忘记想加入的django关键字)
cd acapp,
python3 manage.py shell

from django.db import models
可以去试
]

重要:
数据库里的表table, 对应django里的class
数据库里的每一条数据, 对应class里的每一条对象

from django.db import models
from django.contrib.auth.models import User

class Player(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)   #和每个用户一一对应
    photo = models.URLField(max_length=256, blank=True)      #头像,是用户的一个属性,如果以后相加别的属性照猫画虎

    def __str__(self):
        return str(self.user)

要让自己定义的表出现在后台管理页面的话,需要让他注册过来
cd acapp/game, vim admin.py

  1 from django.contrib import admin
  2 from game.models.player.player import Player
  3
  4 # Register your models here.
  5
  6 admin.site.register(player)

每次定义完表更新完数据库之后,需要执行 cd acapp,
python3 manage.py makemigrations
python3 manage.py migrate
这样的话后台就会出现一个新的表Players,这样的话后台就写好了

每写一个函数需要这三个东西
views——实现具体的调用数据库的逻辑
urls——实现一个路由
js里面实现一个调用

前后端分离,需要让后台知道是哪个端
cd acapp/game/static/js/src, vim zbase.js
加了一个acwingos, 这个参数会有一些接口,在网站上就没这个参数,拿这个条件就知道前端在哪执行的


cd acapp/game/views/settings , 用户的信息以后都放在settings里
vim getinfo.py

from django.http import JsonResponse
from game.models.player.player import Player

def getinfo_acapp(request):
    player = Player.objects.all()[0]
    return JsonResponse({
        'result': "success",
        'username': player.user.username,
        'photo': player.photo,
    })

def getinfo_web(request):
    player = Player.objects.all()[0]
    return JsonResponse({
        'result': "success",
        'username': player.user.username,
        'photo': player.photo,
    })

def getinfo(request):
    platform = request.GET.get('platform')
    if platform == "ACAPP":
        return getinfo_acapp(request)
    elif platform == "WEB":
        return getinfo_web(request)

写完之后写一下路由,cd acapp/game/urls, cd settings
vim index.py

from django.urls import path
from game.views.settings.getinfo import getinfo

urlpatterns = [
    path("getinfo/", getinfo, name="settings_getinfo"),

]

这样的话, views和urls就写完了,然后要写js
我们需要在menu之前设置一个登录界面,判断用户是否登录
cd acapp/game/static/js/src, mkdir settings, 然后在settings里创建一个类,vim zbase.js

class Settings {
    constructor(root) {
        this.root = root;
        this.platform = "WEB"; // 默认是web端
        if (this.root.AcWingOS) this.platform = "ACAPP";

        this.start();
    }

    start() {//在创建的时候执行
        this.getinfo();
    }

    register() { //打开注册界面
    }

    login() {//打开登录界面
    }

    getinfo() {//从服务器获取用户信息
        let outer = this;

        $.ajax({
            url: "https://app4189.acapp.acwing.com.cn/settings/getinfo/",
            type: "GET",
            data: {
                platform: outer.platform,
            },
            success: function(resp) {
                console.log(resp);
                if (resp.result === "success") {
                    outer.hide();
                    outer.root.menu.show();
                } else {//否则要登录
                    outer.login();
                }
            }
        });
    }

    hide() {
    }

    show() {
    }
}

执行流程,首先这个js文件是在前端执行的,会先执行settings的构造函数,在构造函数里面会去执行start()函数,在start函数中会去执行getinfo()函数,getinfo函数会向后端发一个请求,在请求发送之后路由会告诉他应该找views里的getinfo函数,它就会路由到views里的getinfo函数,我们会发现它的platform是web,所以它就会路由到web, 在getinfo_web里它就会返回用户名和头像,返回到我们的resp里

然后下一步在 player里的render()函数中把头像给加进去

class Player extends AcGameObject {
    constructor (playground, x, y, radius, color, speed, is_me) {
        super();
        this.playground = playground;
        this.ctx = this.playground.game_map.ctx;
        this.x = x;
        this.y = y;
        this.vx = 0;
        this.vy = 0;
        this.damage_x = 0;
        this.damage_y = 0;
        this.damage_speed = 0;
        this.move_length = 0;
        this.radius = radius;
        this.color = color;
        this.speed = speed;
        this.is_me = is_me;
        this.eps = 0.1;
        this.friction = 0.9;
        this.spent_time = 0;

        this.cur_skill = null;

        if (this.is_me) {//如果是用户自己,把头像传过来
            this.img = new Image();
            this.img.src = this.playground.root.settings.photo;
        }


    }

    start() {
        if (this.is_me) {//如果是自己的话,调用监听事件
            this.add_listening_events();
        } else {//如果不是自己
            let tx = Math.random() * this.playground.width;
            let ty = Math.random() * this.playground.height;
            this.move_to(tx, ty);
        }
    }

    add_listening_events() {//监听事件,鼠标点击,按键盘
        let outer = this;
        this.playground.game_map.$canvas.on("contextmenu", function(){ //取消右键的菜单
            return false;
        });
        this.playground.game_map.$canvas.mousedown(function(e) { //获取右键点击的坐标
            const rect = outer.ctx.canvas.getBoundingClientRect(); //这个接口用来获得鼠标点击的相对坐标
            if (e.which === 3) {
                outer.move_to(e.clientX - rect.left, e.clientY - rect.top);

            } else if(e.which === 1) {//点的是鼠标左键的话
                if (outer.cur_skill === "fireball") {//如果当前技能是火球的话
                    outer.shoot_fireball(e.clientX - rect.left, e.clientY - rect.top);//朝tx,ty坐标发火球
                }
                outer.cur_skill = null;//左键点完发完火球之后,这个状态清空
            }

        });

        $(window).keydown(function(e) {//获取键盘信息
            if (e.which === 81) {//百度keycode,js键盘按钮81代表q键
                outer.cur_skill = "fireball";

                return false;//代表后续不处理了
            }

        });
    }

    shoot_fireball(tx, ty) {
        let x = this.x;
        let y = this.y;
        let radius = this.playground.height * 0.01;
        let angle = Math.atan2(ty - this.y, tx - this.x);
        let vx = Math.cos(angle), vy = Math.sin(angle);
        let color = "orange";
        let speed = this.playground.height * 0.5;
        let move_length = this.playground.height * 1;
        new FireBall(this.playground, this, x, y, radius, vx, vy, color, speed, move_length, this.playground.height * 0.01);
    }

    get_dist(x1, y1, x2, y2) {
        let dx = x1 - x2;
        let dy = y1 - y2;
        return Math.sqrt(dx * dx + dy * dy);

    }

    move_to(tx, ty) {
        this.move_length = this.get_dist(this.x, this.y, tx, ty); //移动的模长
        let angle = Math.atan2(ty - this.y, tx - this.x); //求移动向量的角度
        this.vx = Math.cos(angle); //表示速度,其实是1*cos(angle)
        this.vy = Math.sin(angle);

    }

    is_attacked(angle, damage) {
        for (let i = 0; i < 10 + Math.random() * 5; i ++) {//被击打之后的粒子效果,随机出现一些粒子
            let x = this.x, y = this.y;
            let radius = this.radius * Math.random() * 0.1;
            let angle = Math.PI * 2 * Math.random();
            let vx = Math.cos(angle), vy = Math.sin(angle);
            let color = this.color;
            let speed = this.speed * 10;
            let move_length = this.radius * Math.random() * 5;
            new Particle(this.playground, x, y, radius, vx, vy, color, speed, move_length);
        }
        this.radius -= damage;
        if (this.radius < 10) {
            this.destroy();
            return false;
        }
        this.damage_x = Math.cos(angle);
        this.damage_y = Math.sin(angle);
        this.damage_speed = damage * 100;
    }

    update() {
        this.spent_time += this.timedelta / 1000;
        if (! this.is_me && this.spent_time > 5 && Math.random() < 1 / 300.0) {
            let player = this.playground.players[Math.floor(Math.random() * this.playground.players.length)];
            let tx = player.x + player.speed * this.vx * this.timedelta / 1000 * 0.5;
            let ty = player.y + player.speed * this.vy * this.timedelta / 1000 * 0.5;
            this.shoot_fireball(tx, ty);
        }

        if (this.damage_speed > 10) {
            this.vx = this.vy = 0;
            this.move_length = 0;
            this.x += this.damage_x * this.damage_speed * this.timedelta / 1000;
            this.y += this.damage_y * this.damage_speed * this.timedelta / 1000;
            this.damage_speed *= this.friction;
        } else {
            if (this.move_length < this.eps) {
                this.move_length = 0;
                this.vx = this.vy = 0;
                if (!this.is_me) {//对于robots,不能停,循环着随机移动
                    let tx = Math.random() * this.playground.width;
                    let ty = Math.random() * this.playground.height;
                    this.move_to(tx, ty);
                }
            } else {
                let moved = Math.min(this.move_length, this.speed * this.timedelta / 1000); //不能移出界,moved表示的是每秒真实移动的距离
                this.x += this.vx * moved;
                this.y += this.vy * moved;
                this.move_length -= moved;
            }
        }
        this.render();
    }

    render() {// 渲染
        if (this.is_me) {//如果是自己的话渲染一个头像
            this.ctx.save();
            this.ctx.beginPath();
            this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
            this.ctx.stroke();
            this.ctx.clip();
            this.ctx.drawImage(this.img, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2);
            this.ctx.restore();
        } else {//如果不是自己的话随机一个圆
            this.ctx.beginPath();
            this.ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
            this.ctx.fillStyle = this.color;
            this.ctx.fill();
        }
    }

    on_destroy() {
        for (let i = 0; i < this.playground.players.length; i ++) {
            if (this.playground.players[i] === this) {
                this.playground.players.splice(i, 1);
            }
        }
    }

}

写登录界面

cd acapp/game/static/js/src/settings, vim zbase.js


cd acapp/game/static/css, vim game.css
把ac_game_settings加到css文件


背景就有了

cd acapp/game/static/js/src/settings, vim zbase.js
然后写输入框里的东西,用户名,密码 登录按钮


cd acapp/game/static/css, vim game.css
然后把登录界面的东西写到css文件,css文件是描述js那些类的具体形状


这样写完之后,登录界面的雏形就完成了

写注册界面

cd acapp/game/static/js/src/settings, vim zbase.js
复制登录界面改一改就可以

然后监听事件函数,在login界面点击注册和在register界面点击登录

写动作 实现三个函数 register login logout

和后端交互的老三样,
views是处理数据的
urls 路由
js 前端怎么调用

首先写views

cd acapp/game/views/settings, 每一个操作创建一个新的文件,方便调试
vim login.py, vim logout.py, vim register.py(用户默认头像在这里定义)


然后写路由
cd acapp/game/urls/settings, vim index.py
把路由写好


然后写js, 前端怎么调用,有login_on_remote, register_on_remote, 这些 函数,


5.2 Web端acapp一键登录

在django中集成redis(准备工作)

首先 pip install django_redis

配置一下缓存

在cd acapp/acapp vim settings.py
复制到

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
    },
}
USER_AGENTS_CACHE = 'default'

启动redis-server

直接在 cd acapp, 执行sudo redis-server /etc/redis/redis.conf 就可以了

redis在django中怎么来操作

cd acapp, python3 manage.py shell
from django.core.cache import cache


cache.set('ljh', 1, None) #第三个参数是多少时间过期,单位是秒
cache.keys('*')           #查所有的数据
cache.keys('l*')          #查所有以l开头的,redis支持正则表达式
cache.has_key('ljh')      #返回true,判断有无
cache.get('ljh')          #查一下ljh的值
cache.delete('ljh')       #删除

第三方授权登录的流程

在这里插入图片描述

用户点完第三方登录按钮,就像web发送了一个申请,说我要用第三方的账号去登录;然后web就把appid报给第三方,第三方会给用户一个页面询问是否要把信息授权给刚刚这个网站。

如果用户同意的话,表明我要把信息授权给刚刚那个网站,acwing接受到同意的信息,会把一个授权码发给网站;网站收到授权码之后再把它加上自己的身份信息appid, app-secret,再向acwing申请一个授权令牌access-token和用户的id openid(用来识别用户的东西);网站拿到这两个东西之后就能向第三方申请用户的信息了。这里是一个用户名和一个头像。

在数据库增加一个openid

cd acapp/game/models/player, vim player.py
改一下数据库,加一个openid是一个字符串


两句话更新一下数据库
cd acapp

python3 manage.py makemigrations
python3 manage.py migrate

接下来就要实现这个流程

在这里插入图片描述
cd acapp/game/views/settings, mkdir acwing创建一个文件夹acwing表示授权登录

今天搞的是web端,所以再mkdir web, acapp

第一步申请授权码code

cd acapp/game/views/settings/acwing/web,
vim apply_code.py
vim receive_code.py 写一下这两个文件



请求地址:https://www.acwing.com/third_party/api/oauth2/web/authorize/

请求方法:GET
https://www.acwing.com/third_party/api/oauth2/web/authorize/?appid=APPID&redirect_uri=REDIRECT_URI&scope=SCOPE&state=STATE

在这里插入图片描述

cd acapp/game/urls/settings/, mkdir acwing, vim index.py写一下acwing的路由,再返回上一级把路由include一下

后端写差不多,cd acapp/game/static/src, vim zbase.js 写一下前端

第二步申请授权令牌access_token和用户的openid

cd acapp/game/views/settings/acwing/web, vim receive_code.py

请求方法:GET
https://www.acwing.com/third_party/api/oauth2/access_token/?appid=APPID&secret=APPSECRET&code=CODE

在这里插入图片描述
在这里插入图片描述

第三步申请用户身份信息

请求地址:https://www.acwing.com/third_party/api/meta/identity/getinfo/

请求方法:GET
https://www.acwing.com/third_party/api/meta/identity/getinfo/?access_token=ACCESS_TOKEN&openid=OPENID

在这里插入图片描述

在这里插入图片描述
apply_code.py如下

from django.http import JsonResponse
from urllib.parse import quote
from random import randint
from django.core.cache import cache


def get_state():
    res = ""
    for i in range(8):
        res += str(randint(0, 9))
    return res


def apply_code(request):
    appid = "4189"
    redirect_uri = quote("https://app4189.acapp.acwing.com.cn/settings/acwing/web/receive_code/")
    scope = "userinfo"
    state = get_state()

    cache.set(state, True, 7200)   # 有效期2小时

    apply_code_url = "https://www.acwing.com/third_party/api/oauth2/web/authorize/"
    return JsonResponse({
        'result': "success",
        'apply_code_url': apply_code_url + "?appid=%s&redirect_uri=%s&scope=%s&state=%s" % (appid, redirect_uri, scope, state)
    })

receive_code.py如下

from django.shortcuts import redirect
from django.core.cache import cache
import requests
from django.contrib.auth.models import User
from game.models.player.player import Player
from django.contrib.auth import login
from random import randint


def receive_code(request):
    data = request.GET
    code = data.get('code')
    state = data.get('state')

    if not cache.has_key(state):
        return redirect("index")
    cache.delete(state)

    apply_access_token_url = "https://www.acwing.com/third_party/api/oauth2/access_token/"
    params = {
        'appid': "4189",
        'secret': "2a79c385f35e4533ab803031fab68e3d",
        'code': code
    }

    access_token_res = requests.get(apply_access_token_url, params=params).json()

    access_token = access_token_res['access_token']
    openid = access_token_res['openid']

    players = Player.objects.filter(openid=openid)
    if players.exists():  # 如果该用户已存在,则无需重新获取信息,直接登录即可
        login(request, players[0].user)
        return redirect("index")

    get_userinfo_url = "https://www.acwing.com/third_party/api/meta/identity/getinfo/"
    params = {
        "access_token": access_token,
        "openid": openid
    }
    userinfo_res = requests.get(get_userinfo_url, params=params).json()
    username = userinfo_res['username']
    photo = userinfo_res['photo']

    while User.objects.filter(username=username).exists():  # 找到一个新用户名
        username += str(randint(0, 9))

    user = User.objects.create(username=username)
    player = Player.objects.create(user=user, photo=photo, openid=openid)

    login(request, user)

    return redirect("index")

5.3 acapp端实现一键授权登录

在views settings acwing acapp实现上节课相似的两个后端函数

cd acapp/game/views/settings/acwing/acapp, vim apply_code.py

from django.http import JsonResponse
from urllib.parse import quote
from random import randint
from django.core.cache import cache


def get_state():
    res = ""
    for i in range(8):
        res += str(randint(0, 9))
    return res


def apply_code(request):
    appid = "4189"
    redirect_uri = quote("https://app4189.acapp.acwing.com.cn/settings/acwing/acapp/receive_code/")
    scope = "userinfo"
    state = get_state()

    cache.set(state, True, 7200)   # 有效期2小时

    return JsonResponse({
        'result': "success",
        'appid': appid,
        'redirect_uri': redirect_uri,
        'scope': scope,
        'state': state,
    })

写一个方法写一个路由,cd acapp/game/urls/settings/acwing, vim index.py

from django.urls import path
from game.views.settings.acwing.web.apply_code import apply_code as web_apply_code
from game.views.settings.acwing.web.receive_code import receive_code as web_receive_code
from game.views.settings.acwing.acapp.apply_code import apply_code as acapp_apply_code
from game.views.settings.acwing.acapp.receive_code import receive_code as acapp_receive_code


urlpatterns = [
    path("web/apply_code/", web_apply_code, name="settings_acwing_web_apply_code"),
    path("web/receive_code/", web_receive_code, name="settings_acwing_web_receive_code"),
    path("acapp/apply_code/", acapp_apply_code, name="settings_acwing_acapp_apply_code"),
    path("acapp/receive_code/", acapp_receive_code, name="settings_acwing_acapp_receive_code"),
]

然后写前端 cd acapp/game/static/js/src/settings, vim zbase.js

在这里插入代码片

然后写 cd acapp/game/views/settings/acwing/acapp, vim receive_code.py

from django.http import JsonResponse
from django.core.cache import cache
import requests
from django.contrib.auth.models import User
from game.models.player.player import Player
from random import randint


def receive_code(request):
    data = request.GET

    if "errcode" in data:
        return JsonResponse({
            'result': "apply failed",
            'errcode': data['errcode'],
            'errmsg': data['errmsg'],
        })

    code = data.get('code')
    state = data.get('state')

    if not cache.has_key(state):
        return JsonResponse({
            'result': "state not exist"
        })
    cache.delete(state)

    apply_access_token_url = "https://www.acwing.com/third_party/api/oauth2/access_token/"
    params = {
        'appid': "4189",
        'secret': "2a79c385f35e4533ab803031fab68e3d",
        'code': code
    }

    access_token_res = requests.get(apply_access_token_url, params=params).json()

    access_token = access_token_res['access_token']
    openid = access_token_res['openid']

    players = Player.objects.filter(openid=openid)
    if players.exists():  # 如果该用户已存在,则无需重新获取信息,直接登录即可
        player = players[0]
        return JsonResponse({
            'result': "success",
            'username': player.user.username,
            'photo': player.photo,
        })


    get_userinfo_url = "https://www.acwing.com/third_party/api/meta/identity/getinfo/"
    params = {
        "access_token": access_token,
        "openid": openid
    }
    userinfo_res = requests.get(get_userinfo_url, params=params).json()
    username = userinfo_res['username']
    photo = userinfo_res['photo']

    while User.objects.filter(username=username).exists():  # 找到一个新用户名
        username += str(randint(0, 9))

    user = User.objects.create(username=username)
    player = Player.objects.create(user=user, photo=photo, openid=openid)

    return JsonResponse({
        'result': "success",
        'username': player.user.username,
        'photo': player.photo,
    })

6. 实现联机对战

统一长度单位

统一长宽比
cd acapp/game/static/js/src/playground, vim zbase.js

增加一个自动调节长宽比的resize()函数

在这里插入代码片

然后让resize函数影响到真实的黑框大小
cd acapp/game/static/js/src/playground/game_map, vim zbase.js

增加一个resize()函数

在这里插入代码片

然后出现这两个问题
1.地图没有居中, 这个可以调整Css文件 .ac-game-playground
2.滑动窗口的时候地图边缘会先变白
cd acapp/game/static/css, vim zbase.css

在这里插入代码片

cd acapp/game/static/js/src/playground/game_map, vim zbase.js
在resize()函数中,涂一层不渐变的蒙版

在这里插入代码片

接着把地图中的所有元素的大小变成相对大小,地图里有三种角色,particle, player, skill
cd acapp/game/static/js/src/playground,vim zbase.js, 首先要从初始化的时候来改
然后进到player里去改,cd player
然后去改子弹,cd acapp/game/static/js/src/playground/skill/fireball
然后进到粒子里,cd particle

增加联机对战模式

cd acapp/game/static/js/src/menu, vim zbase.js 更改一下点击多人模式按钮的逻辑
cd acapp/game/static/js/src/playground, vim zbase.js 在打开playground界面做一下模式的分离
cd …/player, vim zbase.js , 变一下character参数(me robot enemy)所有is_me都改一下

配置django_channels

django_channels是负责客户端和server双向通信的

我们要同步四个事件:
create_player, move_to, shoot_fireball,

attack; 因为网络有延迟,所以fps游戏是否击中的判断都是在本地
attack是把自己击中别人的信息广播到所有玩家

在这里可以说是没有同步每一时刻的坐标而是同步操作;同步坐标的话服务器压力会更大

http协议是一个单向协议,我们只能向服务器请求信息,如果我们没有向服务器请求,服务器是不能主动向客户端发送信息的;
但在这里我们同步的事件,比如1号玩家在客户端动了一下,信息传到服务器,服务器要把信息同步给其它玩家;
所以我们使用websocket协议 ws,这个协议是双向的;支持客户端向服务器发请求也支持服务器向客户端发请求。

http 有 加密的协议 https; ws 也有加密的版本wss; django_channel就是让我们的django支持wss协议
在这里插入图片描述
玩家的血量等信息需要动态的通信维护,需要做到实时所以对读写效率要求很高,需要用内存数据库redis来存;django本身也要存一下信息存到redis的话效率更高

配置过程

  1. 安装channels_redis:
pip install channels_redis
  1. 配置acapp/asgi.py,cd acapp/acapp
import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from game.routing import websocket_urlpatterns

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'acapp.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
})
  1. 配置acapp/settings.py
    在INSTALLED_APPS中添加channels,添加后如下所示:
INSTALLED_APPS = [ 
    'channels',
    'game.apps.GameConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

然后在文件末尾添加:

ASGI_APPLICATION = 'acapp.asgi.application'
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}
  1. 配置game/routing.py, cd acapp/game, vim routing.py
    这一部分的作用相当于http的urls。
    内容如下:
from django.urls import path

websocket_urlpatterns = [
]

  1. 编写game/consumers
    这一部分的作用相当于http的views。
    cd acapp/game/consumers/multiplayer, vim index.py

参考示例:

from channels.generic.websocket import AsyncWebsocketConsumer
import json

class MultiPlayer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()
        print('accept')

        self.room_name = "room"
        await self.channel_layer.group_add(self.room_name, self.channel_name)

    async def disconnect(self, close_code):
        print('disconnect')
        await self.channel_layer.group_discard(self.room_name, self.channel_name)


    async def receive(self, text_data):
        data = json.loads(text_data)
        print(data)
  1. 启动django_channels
    在~/acapp目录下执行:
daphne -b 0.0.0.0 -p 5015 acapp.asgi:application

前端和后端的连接

写一下路由:
cd acapp/game, vim routing.py
写一下前端的调用
cd acapp/game/static/js/src/playground/socket/multiplayer, vim zbase.js

编写同步函数(核心)

为了给每个对象一个身份证,因为在不同窗口中需要区别对象的身份, 需要在不同的窗口中知道谁是谁,需要给每个对象创建一个uuid来同步这些对象在所有窗口中的身份编号;
这个是一个八位的随机数,重复的概率非常低可以不计。

每一个对象创建出来之后都有一个对应的uuid

cd acapp/game/static/js/src/playground/ac_game_object, vim zbase.js


同步第一个事件,create_player

在不同的窗口都能同步创建玩家;我们在redis存所有玩家的信息

引入room概念每个房间有个上限人数
cd acapp/acapp, vim settings.py

ROOM_CAPACITY = 3

改一下后端的函数,这个是相当于http中views的东西
有一套创建房间建立连接然后向本地返回玩家信息等一系列逻辑
cd acapp/game/consumers/multiplayer, vim index.py


然后我们有一个单独的事件表示创建玩家,
在cd acapp/game/static/js/src/playground/, vim zbase.js

class AcGamePlayground {
    constructor(root) {
        this.root = root;
        this.$playground = $(`<div class="ac-game-playground"></div>`);

        this.hide();
        this.root.$ac_game.append(this.$playground);

        this.start();
    }

    get_random_color() {
        let colors = ["blue", "red", "pink", "grey", "green"];
        return colors[Math.floor(Math.random() * 5)];
    }

    start() {
        let outer = this;
        $(window).resize(function() {
            outer.resize();
        });
    }

    resize() {
        this.width = this.$playground.width();
        this.height = this.$playground.height();
        let unit = Math.min(this.width / 16, this.height / 9);
        this.width = unit * 16;
        this.height = unit * 9;
        this.scale = this.height;

        if (this.game_map) this.game_map.resize();
    }

    show(mode) {  // 打开playground界面
        let outer = this;
        this.$playground.show();

        this.width = this.$playground.width();
        this.height = this.$playground.height();
        this.game_map = new GameMap(this);

        this.resize();

        this.players = [];
        this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, "white", 0.15, "me", this.root.settings.username, this.root.settings.photo));

        if (mode === "single mode") {
            for (let i = 0; i < 5; i ++ ) {
                this.players.push(new Player(this, this.width / 2 / this.scale, 0.5, 0.05, this.get_random_color(), 0.15, "robot"));
            }
        } else if (mode === "multi mode") {
            this.mps = new MultiPlayerSocket(this);
            this.mps.uuid = this.players[0].uuid;

            this.mps.ws.onopen = function() {
                outer.mps.send_create_player(outer.root.settings.username, outer.root.settings.photo);
            };
        }

    }

    hide() {  // 关闭playground界面
        this.$playground.hide();
    }
}

cd acapp/game/static/js/src/playground/socket/multiplayer, vim zbase.js
把send_create_player写好,这样我们就会向后台发送一个请求


写一下后端的逻辑

向后台发送一个请求之后,会传过来一个字典create_player
cd acapp/game/consumers/multiplayer, vim index.py
我们根据event做一个路由, 也需要定义一下create_player和接收群发消息的地方

from channels.generic.websocket import AsyncWebsocketConsumer
import json
from django.conf import settings
from django.core.cache import cache

class MultiPlayer(AsyncWebsocketConsumer):
    async def connect(self):
        self.room_name = None

        for i in range(1000):   #枚举一下房间
            name = "room-%d" % (i)   #当前的房间名就是room-i
            if not cache.has_key(name) or len(cache.get(name)) < settings.ROOM_CAPACITY: #如果说房间为空或者没满
                self.room_name = name
                break

        if not self.room_name: #房间不够了 请排队
            return

        await self.accept() #接受这个请求,加入房间

        if not cache.has_key(self.room_name): #如果没有这个房间
            cache.set(self.room_name, [], 3600)  # 有效期1小时

        for player in cache.get(self.room_name):
            await self.send(text_data=json.dumps({ #建立完连接之后,向本地发送玩家信息, .dumps()是把字典变成字符串

                'event': "create_player",
                'uuid': player['uuid'],
                'username': player['username'],
                'photo': player['photo'],
            }))

        await self.channel_layer.group_add(self.room_name, self.channel_name)

    async def disconnect(self, close_code):
        print('disconnect')
        await self.channel_layer.group_discard(self.room_name, self.channel_name);


    async def create_player(self, data):
        players = cache.get(self.room_name)
        players.append({
            'uuid': data['uuid'],
            'username': data['username'],
            'photo': data['photo']
        })
        cache.set(self.room_name, players, 3600)  #当最后一个玩家创建完之后,整个对局会保存一个小时
        await self.channel_layer.group_send(# 群发消息
            self.room_name,
            {
                'type': "group_create_player",  #type里的关键字为什么重要,是把create_player这个函数发给组内的所有人
                'event': "create_player",
                'uuid': data['uuid'],
                'username': data['username'],
                'photo': data['photo'],
            }
        )

    async def group_create_player(self, data): #群发的消息要有一个接受的地方,type的关键字是什么,这里函数名就是什么
        await self.send(text_data=json.dumps(data))

    async def receive(self, text_data):
        data = json.loads(text_data)
        event = data['event']
        if event == "create_player":  #如果说当前事件是create_player的话, 我们就执行一下create_player
            await self.create_player(data)

我们后端有的东西,前端也应该要有
cd acapp/game/static/js/src/playground/socket/multiplayer, vim zbase.js

class MultiPlayerSocket {
    constructor(playground) {
        this.playground = playground;

        this.ws = new WebSocket("wss://app4189.acapp.acwing.com.cn/wss/multiplayer/");

        this.start();
    }

    start() {
        this.receive();
    }

    receive () {
        let outer = this;

        this.ws.onmessage = function(e) {
            let data = JSON.parse(e.data);
            let uuid = data.uuid;
            if (uuid === outer.uuid) return false;

            let event = data.event;
            if (event === "create_player") {
                outer.receive_create_player(uuid, data.username, data.photo);
            }
        };
    }

    send_create_player(username, photo) {
        let outer = this;
        this.ws.send(JSON.stringify({
            'event': "create_player",
            'uuid': outer.uuid,
            'username': username,
            'photo': photo,
        }));
    }

    receive_create_player(uuid, username, photo) {
        let player = new Player(
            this.playground,
            this.playground.width / 2 / this.playground.scale,
            0.5,
            0.05,
            "white",
            0.15,
            "enemy",
            username,
            photo,
        );

        player.uuid = uuid;
        this.playground.players.push(player);
    }
}

同步第二个事件move_to()

通信的部分,前端和后端
cd acapp/game/static/js/src/playground/socket/multiplayer, vim zbase.js

cd acapp/game/consumers/multiplayer, vim index.py

然后调用部分
cd acapp/game/static/js/src/playground/player, vim zbase.js

cd acapp/game/static/js/src/playground/, vim zbase.js

理一下整个过程:

我在多人模式里点击鼠标,监听事件知道我这个行为,我自己因为调用本窗口的move_to()函数可以在窗口内移动;别人怎么知道我移动呢?
如果是多人模式,会通过写的send_move_to()函数向服务器发送信息。send_move_to()通过web_socket向服务器发送一个事件。服务器里的话,会有一个receive()函数接受到信息,发现事件是move_to()会调用后端写的move_to()函数。在这个move_to()函数里会向所有的channel.layer群发,群发我们这个uuid玩家移动的消息。每一个窗口都会在前端接收到信息,路由到前端的receive_move_to()函数,然后在这里,每个玩家就会在各自的窗口里调用各自的move_to()函数了

1 前端通过写的send_move_to()函数向服务器发送信息

add_listening_events() {
        let outer = this;
        this.playground.game_map.$canvas.on("contextmenu", function() {
            return false;
        });
        this.playground.game_map.$canvas.mousedown(function(e) {
            if (outer.playground.state !== "fighting")
                return false;

            const rect = outer.ctx.canvas.getBoundingClientRect();
            if (e.which === 3) {
                let tx = (e.clientX - rect.left) / outer.playground.scale;
                let ty = (e.clientY - rect.top) / outer.playground.scale;
                outer.move_to(tx, ty);

                if (outer.playground.mode === "multi mode") {
                    outer.playground.mps.send_move_to(tx, ty);
                }

2 send_move_to()通过web_socket向服务器发送一个事件

send_move_to(tx, ty) {
        let outer = this;
        this.ws.send(JSON.stringify({
            'event': "move_to",
            'uuid': outer.uuid,
            'tx': tx,
            'ty': ty,
        }));
    }

3 服务器里的话,会有一个receive()函数接受到信息,发现事件是move_to()会调用后端写的move_to()函数。在这个move_to()函数里会向所有的channel.layer群发,广播我们这个uuid玩家移动的消息。(self.channel_layer.group_send)

async def receive(self, text_data):
        data = json.loads(text_data)
        event = data['event']
        if event == "create_player":
            await self.create_player(data)
        elif event == "move_to":
            await self.move_to(data)


async def move_to(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': "group_send_event",
                'event': "move_to",
                'uuid': data['uuid'],
                'tx': data['tx'],
                'ty': data['ty'],
            }
        )


4 每一个窗口都会在前端接收到这个操作的信息,路由到前端的receive_move_to()函数,然后在这里,每个玩家就会在各自的窗口里调用各自的move_to()函数了

receive () {
        let outer = this;

        this.ws.onmessage = function(e) {
            let data = JSON.parse(e.data);
            let uuid = data.uuid;
            if (uuid === outer.uuid) return false;

            let event = data.event;
            if (event === "create_player") {
                outer.receive_create_player(uuid, data.username, data.photo);
            } else if (event === "move_to") {
                outer.receive_move_to(uuid, data.tx, data.ty);
            } else if (event === "shoot_fireball") {
                outer.receive_shoot_fireball(uuid, data.tx, data.ty, data.ball_uuid);
            } else if (event === "attack") {
                outer.receive_attack(uuid, data.attackee_uuid, data.x, data.y, data.angle, data.damage, data.ball_uuid);
            } else if (event === "blink") {
                outer.receive_blink(uuid, data.tx, data.ty);
            }
        };
    }



receive_move_to(uuid, tx, ty) {
        let player = this.get_player(uuid);

        if (player) {
            player.move_to(tx, ty);
        }
    }

同步第三个事件shoot_fireball()

同步第四个事件attack

在某一个窗口发生击中,attacker -> attackee, 从前端传到后端广播这个击中的事件

判断碰撞只有在炮弹发出者所在窗口里判断

因为只同步事件,在子弹碰撞的时候由于网络延迟等导致三角函数计算会出现误差;我们在子弹击中的时候,同步一下attackee的位置,做一个补偿
我击中了你,你在我窗口的位置将会被同步给所有窗口

计分板

渲染一个题头
我们在playground里定义三个阶段,waiting(等人凑够三个),fighting(可以操作),over(挂了)

技能cd

在player里
实现一个技能冷却cd , coldtime, 只有自己会有
实现一个图标在右下角,并且创建一个蒙版随着技能的coldtime画圆

同步第五个事件 blink 闪现技能

知道距离(斜边),先求角度(用函数搞出来),然后用三角函数求位置的偏移量,原坐标加偏移量就是闪现之后的坐标

闪现之后停下来

然后再把这个闪现blink函数同步给所有窗口

7. 实现聊天系统

前端渲染出来这个聊天框
cd /acapp/game/static/js/src/playground/chat_field, vim zbase.js
这个聊天框就是html里面的元素不用通过canvas画出来,其中包含两个部分,history 和 input
构造一系列函数然后在playground里调用

广播这个聊天系统的逻辑和上一节同步各种操作的逻辑是一样的
cd acapp/game/static/js/src/playground/socket/multiplayer, vim zbase.js
在这里实现send() 和 receive()函数
然后是写后端
cd acapp/game/consumers/multiplayer, vim index.py
在这里写广播的逻辑,也就是self.channel_layer.group_send()

 async def message(self, data):
        await self.channel_layer.group_send(
            self.room_name,
            {
                'type': "group_send_event",
                'event': "message",
                'uuid': data['uuid'],
                'username': data['username'],
                'text': data['text'],
            }
        )

然后在playground里调用一下就行了

8. 实现匹配系统

需要额外的进程来做匹配,进程之间的通信,可以用Thrift; 通过ip 和 端口号来通信
在这里插入图片描述
我们的服务器通过Trift服务,向另外一个专门做匹配的进程,请求信息
为什么用Trift,考虑延时性,如果计算量很大的话,对系统资源是一种浪费;因此要用另一个进程去做

cd acapp, mkdir match_system - 把match_system放到和game同级的目录下
cd acapp/match_system/, mkdir thrift - 放一些配置文件
cd acapp/match_system/, mkdir src - 实现具体逻辑

cd cd acapp/match_system/thrift/, vim match.thrift


cd acapp/match_system/src/ ,
thrift --gen py match.thrift 基于thrift文件创建想要语言版本的代码
产生了一个gen-py文件夹,把它改名称 match_server, 其实就是产生了一个包,可以将它import

8.2项目收尾

  1. 加密、压缩js代码
    安装terser:
sudo apt-get update
sudo apt-get install npm
sudo npm install terser -g

terser不仅支持文件输入,也支持标准输入。结果会输出到标准输出中。

使用方式:

terser xxx.js -c -m
  1. 清理监听函数
    在AcAPP关闭之前触发的事件可以通过如下api添加:
AcWingOS.api.window.on_close(func);

注意:

同一个页面中,多个acapp引入的js代码只会加载一次,因此AC_GAME_OBJECTS等全局变量是同一个页面、同一个acapp的所有窗口共用的。
各自创建的局部变量是独立的,比如new AcGame()创建出的对象各个窗口是独立的。

  1. 编写每局游戏的结束界面
  2. 更新战绩
  3. 添加favicon.ico
    cd acapp/game/templates/multiends/

9. Rest Framework与JWT身份验证

之前的代码是通过 cookie session id验证的,即在浏览器的cookie里,然后发给服务器去在服务器的数据库去查表,确定用户的身份
如果存在session里,不方便做跨域

为了解决这个问题,使用JWT框架,这个可以把信息存在内存或者local storage, 方便js去传
JWT- Jason Web Tokens

Rest Framework与JWT身份验证

有两个token, 一个access一个refresh(刷新我们的令牌)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/179284.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

特征工程——文本特征

文本特征 expansion编码 consolidation编码 文本长度特征 标点符号特征 词汇属性特征 特殊词汇特征 词频特征 TF-IDF特征 LDA特征 下面的文章主要是梯度提升树模型展开的&#xff0c;抽取的特征主要为帮助梯度提升树模型挖掘其挖掘不到的信息&#xff0c;本文介绍的所…

NodeJS Web 框架 Express 之中间件

NodeJS Web 框架 Express 之中间件参考描述中间件next()一个简单的中间件函数使用全局中间件局部中间件共享注意事项位置next()分类错误级中间件内置中间件express.urlencoded()express.json()第三方中间件参考 项目描述哔哩哔哩黑马程序员搜索引擎Bing 描述 项目描述Edge109…

从0-1开始 测试ZLMediaKit推拉流性能、延时性能

流媒体开发系列文章 文章目录流媒体开发系列文章前言一、环境准备&#xff1f;二、拉流测试过程三、推流测试过程三、延时测试总结前言 目前、比较有名的流媒体服务器有ZLMediaKit、srs、live555、eadydarwin等。因为srs是单线程服务、对于多核服务器的支持需要通过部署多个服…

pytorch深度学习基础(十一)——常用结构化CNN模型构建

结构化CNN模型构建与测试前言GoogLeNet结构Inception块模型构建resNet18模型结构残差块模型构建denseNet模型结构DenseBlocktransition_block模型构建结尾前言 在本专栏的上一篇博客中我们介绍了常用的线性模型&#xff0c;在本文中我们将介绍GoogleNet、resNet、denseNet这类…

APT之木马静态免杀

前言 这篇文章主要是记录手动编写代码进行木马免杀&#xff0c;使用工具也可以免杀&#xff0c;只不过太脚本小子了&#xff0c;而且工具的特征也容易被杀软抓到&#xff0c;指不定哪天就用不了了&#xff0c;所以要学一下手动去免杀木马&#xff0c;也方便以后开发一个只属于…

blender导入骨骼动画方法[psa动作]

先导入女性的psk文件 然后调整缩放大小和人物一样,包括角度朝向. ctrla应用所有改变 然后选择psk文件以及其他人物模型的全部 ,然后 在Layout-物体-父级 -附带空顶相点组 image.png之后会发现所有人物多了修改器,点击其中一个修改器 点添加修改器 -数据传递 勾选顶点数据-选择顶…

人员动作行为AI分析系统 yolov5

人员动作行为AI分析系统通过pythonyolo系列网络学习模型&#xff0c;对现场画面人员行为进行实时分析监测&#xff0c;自动识别出人的各种异常行为动作&#xff0c;立即抓拍存档预警同步回传给后台。 我们使用YOLO算法进行对象检测。YOLO是一个聪明的卷积神经网络(CNN)&#xf…

带滤波器的PID控制仿真-1

采用低通滤波器可有效地滤掉噪声信号&#xff0c;在控制系统的设计中是一种常用的方法。基于低通滤波器的信号处理实例设低通滤波器为&#xff1a;采样时间为1ms&#xff0c;输入信号为带有高频正弦噪声&#xff08; 100Hz&#xff09;的低频&#xff08;0.2Hz)正弦信号。采用低…

离散数学与组合数学-05树

文章目录离散数学与组合数学-05树5.1 认识树5.1.1 树的模型5.1.2 树的应用5.2 无向树5.2.1 定义5.2.2 树的性质5.2.3 性质应用5.3 生成树5.3.1 引入5.3.2 定义5.3.3 算法5.3.4 应用5.4 最小生成树5.4.1 引入5.4.2 定义5.4.3 算法5.5 根树5.5.1 根数定义5.5.2 倒置法5.5.3 树的家…

【编程入门】开源记事本(SwiftUI版)

背景 前面已输出多个系列&#xff1a; 《十余种编程语言做个计算器》 《十余种编程语言写2048小游戏》 《17种编程语言10种排序算法》 《十余种编程语言写博客系统》 《十余种编程语言写云笔记》 本系列对比云笔记&#xff0c;将更为简化&#xff0c;去掉了网络调用&#xff0…

C++模板进阶

这篇文章是对模板初阶的一些补充&#xff0c;让大家在进行深一层的理解。 文章目录1. 非类型模板参数2. 模板的特化2.1 概念2.2 函数模板特化2.3 类模板特化2.3.1 全特化2.3.2 偏特化2.4 类模板特化应用示例3 模板分离编译3.1 什么是分离编译3.2 模板的分离编译3.3 解决方法4.…

【各种**问题系列】什么是 LTS 长期支持

目录 &#x1f341; 什么是长期支持&#xff08;LTS&#xff09;版本&#xff1f; &#x1f342; LTS 版本的优点&#xff1a; &#x1f341; 什么是 Ubuntu LTS&#xff1f; &#x1f342; Ubuntu LTS 软件更新包括什么&#xff1f; 在 Linux 的世界里&#xff0c;特别是谈…

【Java开发】Spring Cloud 08 :链路追踪

任何一个架构难免会出现bug&#xff0c;微服务相比于单体架构日志查询更为困难&#xff0c;因此spring cloud推出了Sleuth等组件的链路追踪技术来实现报错信息的定位及查询。项目源码&#xff1a;尹煜 / coupon-yinyu GitCode1 调用链追踪我们可以想象这样一个场景&#xff0c…

单一数字评估指标、迁移学习、多任务学习、端到端的深度学习

目录1.单一数字评估指标(a single number evaluation metric)有时候要比较那个分类器更好&#xff0c;或者哪个模型更好&#xff0c;有很多指标&#xff0c;很难抉择&#xff0c;这个时候就需要设置一个单一数字评估指标。例1&#xff1a;比较A&#xff0c;B两个分类器的性能&a…

Android MVVM的实现

Android MVVM的实现 前言&#xff1a; 在我们写一些项目的时候&#xff0c;通常会对一些常用的一些常用功能进行抽象封装&#xff0c;简单例子&#xff1a;比如BaseActivity&#xff0c;BaseFragment等等…一般这些Base会去承载一些比如标题栏&#xff0c;主题之类的工作&…

提权漏洞和域渗透历史漏洞整理

Windows提权在线辅助工具 https://i.hacking8.com/tiquan/&#x1f334;Kernel privilege escalation vulnerability collection, with compilation environment, demo GIF map, vulnerability details, executable file (提权漏洞合集) https://github.com/Ascotbe/Kernelhu…

恶意代码分析实战 13 反调试技术

13.1 Lab16-01 首先&#xff0c;将可执行文件拖入IDA中。 我们可以看到有三处都调用了sub_401000函数&#xff0c;并且代码都在哪里停止执行。由于没有一条线从这些方框中引出&#xff0c;这就意味着函数可能终止了程序。 右侧每一个大框中都包含一个检查&#xff0c;这个检查…

Makefile学习②:Makefile基本语法

Makefile学习②&#xff1a;Makefile基本语法 Makefile基本语法 目标&#xff1a; 依赖 &#xff08;Tab&#xff09;命令 目标&#xff1a;一般是指要编译的目标&#xff0c;也可以是一个动作 依赖&#xff1a;指执行当前目标所要依赖的先项&#xff0c;包括其他目标&#xf…

neural collaborative filtering 阅读笔记

本文主要介绍了一种一种基于神经网络的技术&#xff0c;来解决在含有隐形反馈的基础上进行推荐的关键问题————协同过滤。 2.1 Learning from Implicit Data yui1,(ifinteraction(useru,itemi)isobserved)y_{ui} 1,(if interaction (user u, item i) is observed)yui​1,(…

还在为ElementUI的原生校验方式苦恼吗,快用享受element-ui-verify插件的快乐吧(待续)

element-ui-verify 本文章意在介绍element-ui-verify插件使用&#xff0c;以及对比elementUI原生校验方式&#xff0c;突显该插件用少量代码也能实现原生的校验效果甚至更好。 1.先观察一个示例 <template><d2-container><el-form :model"ruleForm&qu…