python+django自动化部署日志采用‌WebSocket前端实时展示

ops/2024/11/28 5:26:29/

一、开发环境搭建和配置

# channels是一个用于在Django中实现WebSocket、HTTP/2和其他异步协议的库。
pip install channels#channels-redis是一个用于在Django Channels中使用Redis作为后台存储的库。它可以用于处理#WebSocket连接的持久化和消息传递。
pip install channels-redis#安装 docker(此处采用 mac安装)
brew install --cask docker

二、django应用模块目录

deployments
├── Consumers.py
├── __init__.py
├── __pycache__
│   ├── Consumers.cpython-313.pyc
│   ├── __init__.cpython-313.pyc
│   ├── admin.cpython-313.pyc
│   ├── apps.cpython-313.pyc
│   ├── models.cpython-313.pyc
│   ├── routing.cpython-313.pyc
│   └── views.cpython-313.pyc
├── admin.py
├── apps.py
├── migrations
│   ├── 0001_initial.py
│   ├── __init__.py
│   └── __pycache__
│       ├── 0001_initial.cpython-313.pyc
│       └── __init__.cpython-313.pyc
├── models.py
├── routing.py
├── tests.py
└── views.py

三、django模块相关代码

***************************/operation/settings.py***************************
CHANNEL_LAYERS = {'default': {'BACKEND': 'channels_redis.core.RedisChannelLayer','CONFIG': {"hosts": ["redis://:xx@xx:6379/0",],},},
}INSTALLED_APPS = ['deployments','channels'
]ASGI_APPLICATION = 'operation.asgi.application'
***************************/operation/asgi.py***************************"""
ASGI config for operation project.It exposes the ASGI callable as a module-level variable named ``application``.For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/Django项目支持ASGI的入口点,ASGI是一个Python标准,用于异步Web服务器、Web框架和Web应用之间的通信,它允许你编写异步的Django视图和其他组件,以提高应用的性能和响应能力,部署时需要用到部署的时候才用到
"""import osfrom django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack# 设置 DJANGO_SETTINGS_MODULE 环境变量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'operation.settings')# 获取 ASGI 应用
django_asgi_app = get_asgi_application()# 延迟导入其他模块
from deployments import routing  # 这里是你需要延迟导入的模块application = ProtocolTypeRouter({"http": django_asgi_app,"websocket": AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns))
})
************************/deployments/routing.py************************
from django.urls import pathfrom . import Consumerswebsocket_urlpatterns = [path('ws/cicd_progress/', Consumers.ChatConsumer.as_asgi())
]
************************/deployments/views.py***************************import asyncio
import json
import logging
import os
import subprocessimport git
from django.http import JsonResponse, HttpResponse
from django.views.decorators.csrf import csrf_exempt
from git import Repofrom deployments.models import Deployment
from operation import settings
from operation.common.enum.BuildProjectEnum import BuildProjectEnum
from operation.common.enum.ResponeCodeEnum import ResponseCodeEnum
from operation.common.exception.BusinessException import BusinessException
from operation.common.utils.CommonResult import CommonResult
from operation.common.utils.PageUtils import paginate_queryset
from asgiref.sync import sync_to_async# Create your views here."""
****************************创建 deployments API视图****************************""""""获取应用列表
"""
@csrf_exempt
def getDeploymentsList(request):try:if request.method == 'POST':json_data = request.bodydata = json.loads(json_data)page = data.get('page',1)page_size = data.get('page_size',10)else:raise BusinessException(ResponseCodeEnum.METHOD_ERROR.message,ResponseCodeEnum.METHOD_ERROR.code)deployment_list = Deployment.objects.all().order_by('created_at')deployment_lists, pagination_info = paginate_queryset(deployment_list, page, page_size)deployment_lists = [Deployment.to_dict() for Deployment in deployment_lists]logging.info(type(deployment_lists))return JsonResponse(CommonResult.success_pagination(deployment_lists, pagination_info),json_dumps_params={'ensure_ascii': False})except BusinessException as e:return JsonResponse(CommonResult.error(e.code, e.message), json_dumps_params={'ensure_ascii': False})"""增加应用
"""
@csrf_exempt
def addDeployment(request):if request.method == "GET":return BusinessException(ResponseCodeEnum.METHOD_ERROR.message, ResponseCodeEnum.METHOD_ERROR.code)try:json_data = request.bodydata = json.loads(json_data)name = data.get('name')if name is None:raise BusinessException(ResponseCodeEnum.PARAMS_ERROR.message, ResponseCodeEnum.PARAMS_ERROR.code)#保存应用Deployment.objects.create(name=name,status=BuildProjectEnum.NOT_STARTED.code)return JsonResponse(CommonResult.success_data(None), json_dumps_params={'ensure_ascii': False})except BusinessException as e:return JsonResponse(CommonResult.error(e.code, e.message), json_dumps_params={'ensure_ascii': False})"""*************************************部署角本*************************************报错: You cannot call this from an async context - use a thread or sync_to_async.解决方案:1.使用 sync_to_async  2.使用 使用线程
"""
# 定义全局变量 progress
progress = 0
async def cicd_execute(project_id, project_name, send_progress,send_news):global progress  # 声明 progress 为全局变量# 每次点击发布时,重置 progress 为0progress = 0# 1.初始化阶段await init_project(project_id, project_name, send_progress)# 2.构建阶段await build_and_deploy_project(project_id, project_name, send_progress,send_news)# 3.运行阶段await run_project(project_id, project_name, send_progress)"""1.初始化阶段
"""
async def init_project(project_id, project_name, send_progress):global progress  # 声明 progress 为全局变量try:logging.info("=================开始初始化 项目 %s" % (project_name))logging.info("初始化中......")# 更新状态为 "初始化中" 记录进度值progress += 20await update_deployment_status(project_id, project_name, BuildProjectEnum.INITIALIZING.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.INITIALIZING.code, ResponseCodeEnum.SUCCESS.status_code,ResponseCodeEnum.SUCCESS.message)projectName = project_nameprojectId = project_idif projectName is None or projectId is None:raise BusinessException(ResponseCodeEnum.PARAMS_ERROR.message, ResponseCodeEnum.PARAMS_ERROR.code)# 环境变量配置set_environment_variables()# 加载仓库 拉取代码await clone_or_pull_repo(project_name)# 更新状态为 "初始化成功" 记录进度值progress += 20await update_deployment_status(project_id, project_name, BuildProjectEnum.INITIAL_SUCCESS.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.INITIAL_SUCCESS.code, ResponseCodeEnum.SUCCESS.status_code,ResponseCodeEnum.SUCCESS.message)logging.info("初始化成功......")except BusinessException as e:logging.info("初始化失败......")# 更新状态为 "初始化失败"await update_deployment_status(project_id, project_name, BuildProjectEnum.INITIAL_FAILURE.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.INITIAL_FAILURE.code,ResponseCodeEnum.INTERNAL_SERVER_ERROR.status_code,ResponseCodeEnum.INTERNAL_SERVER_ERROR.message)"""2.构建阶段
"""
async def build_and_deploy_project(project_id, project_name, send_progress,send_news):global progresslogging.info("=================构建 项目 %s" % (project_name))logging.info("部署中......")# 更新状态为 "部署中" 记录进度值 60progress += 20await update_deployment_status(project_id, project_name, BuildProjectEnum.DEPLOYMENT_IN.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.DEPLOYMENT_IN.code, ResponseCodeEnum.SUCCESS.status_code,ResponseCodeEnum.SUCCESS.message)projectName_image = project_name + "-image"projectName_dir = os.path.join(settings.PROJECT_REPO_DIR, project_name)await clean_old_containers(projectName_image)await clean_old_images(projectName_image)os.chdir(projectName_dir)logging.info(f"当前工作目录: {os.getcwd()}")set_environment_variables()try:# 执行 maven 打包await execute_maven_build(send_news)# 构建 imageawait build_docker_image(projectName_image)# 更新状态为 "部署成功" 记录进度值 80progress += 20await update_deployment_status(project_id, project_name, BuildProjectEnum.DEPLOYMENT_SUCCESS.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.DEPLOYMENT_SUCCESS.code, ResponseCodeEnum.SUCCESS.status_code,ResponseCodeEnum.SUCCESS.message)logging.info("部署成功......")except subprocess.CalledProcessError as ex:# 更新状态为 "部署失败"logging.info("部署失败......")await update_deployment_status(project_id, project_name, BuildProjectEnum.DEPLOYMENT_FAILURE.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.DEPLOYMENT_FAILURE.code,ResponseCodeEnum.INTERNAL_SERVER_ERROR.status_code,ResponseCodeEnum.INTERNAL_SERVER_ERROR.message)"""3.运行阶段
"""
async def run_project(project_id, project_name, send_progress):global progresslogging.info("=================启动 项目 %s" % (project_name))logging.info("启动中......")# 更新状态为 "启动中" 记录进度值 70progress += 10await update_deployment_status(project_id, project_name, BuildProjectEnum.STARTING.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.STARTING.code, ResponseCodeEnum.SUCCESS.status_code,ResponseCodeEnum.SUCCESS.message)projectName_image = project_name + "-image"try:await run_docker_container(projectName_image)# 更新状态为 "启动成功" 记录进度值 80progress += 10await update_deployment_status(project_id, project_name, BuildProjectEnum.STARTED_SECCESS.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.STARTED_SECCESS.code, ResponseCodeEnum.SUCCESS.status_code,ResponseCodeEnum.SUCCESS.message)logging.info("启动成功......")except subprocess.CalledProcessError as ex:# 更新状态为 "启动失败"logging.info("启动失败......")await update_deployment_status(project_id, project_name, BuildProjectEnum.STARTED_FAILURE.code, progress)# 发送事件await send_progress(progress, BuildProjectEnum.STARTED_FAILURE.code,ResponseCodeEnum.INTERNAL_SERVER_ERROR.status_code,ResponseCodeEnum.INTERNAL_SERVER_ERROR.message)"""运行 docker
"""async def run_docker_container(projectName_image):logging.info(f"=================运行 docker run -d -p 8084:8084 {projectName_image} .")await sync_to_async(subprocess.run)(["docker", "run", "-d", "-p", "8084:8084", projectName_image], check=True)logging.info("启动容器 success......")"""加载环境变量
"""def set_environment_variables():logging.info("加载环境变量......")os.environ['PATH'] = settings.MAVEN_PATH + os.environ['PATH']jdk_path = settings.JDK_PATHos.environ['JAVA_HOME'] = jdk_pathos.environ['PATH'] = f"{jdk_path}/bin:{os.environ['PATH']}"logging.info("环境变量加载成功......")"""加载仓库 拉取代码
"""async def clone_or_pull_repo(project_name):logging.info("拉取仓库代码......")project_repo_url = settings.PROJECT_REPO_URLproject_repo_dir = settings.PROJECT_REPO_DIRif not os.path.exists(project_repo_dir):await sync_to_async(Repo.clone_from)(project_repo_url, project_repo_dir)else:project_repo = await sync_to_async(Repo)(project_repo_dir)await sync_to_async(project_repo.remotes.origin.pull)()logging.info("拉取仓库代码成功......")"""清理旧容器
"""async def clean_old_containers(projectName_image):cid = await sync_to_async(subprocess.check_output)(["docker", "ps", "-a", "-q", "--filter", f"ancestor={projectName_image}"])if cid:# 去除末尾的换行符并转换为字符串cid_str = cid.decode().strip()logging.info("=================cid: %s" % (cid_str))logging.info(f"清理旧容器 docker rm -f {cid_str}")await sync_to_async(subprocess.run)(["docker", "rm", "-f", cid_str])"""清理旧镜像
"""
async def clean_old_images(projectName_image):iid = await sync_to_async(subprocess.check_output)(["docker", "images", "-q", projectName_image])if iid:# 去除末尾的换行符并转换为字符串iid_str = iid.strip()logging.info("=================iid: %s" % (iid_str))logging.info(f"清理旧镜像 docker rmi -f {iid_str}")await sync_to_async(subprocess.run)(["docker", "rmi", "-f", iid_str])"""maven 打包构建
"""
async def execute_maven_build(send_news):#sync_to_async是Django Channels中的一个工具,用于将同步函数转换为异步函数,但它不能直接处理subprocess.Popen的异步读取。# processObject = await sync_to_async(subprocess.run)(["mvn", "clean", "install", "-DskipTests"], check=True)# 使用 asyncio.create_subprocess_exec 创建异步进程processObject = await asyncio.create_subprocess_exec('mvn', 'clean', 'install', '-DskipTests',stdout=asyncio.subprocess.PIPE,stderr=asyncio.subprocess.PIPE)logging.info("maven install success......")logging.info("发送执行日志......")await deployment_log_output(processObject,send_news,ResponseCodeEnum.SUCCESS)"""构建镜像
"""
async def build_docker_image(projectName_image):logging.info(f"=================构建镜像 docker build -t {projectName_image} .")await sync_to_async(subprocess.run)(["docker", "build", "-t", projectName_image, "."], check=True)logging.info("构建镜像 success......")"""更新model
"""
async def update_deployment_status(project_id, project_name, status, progress):await sync_to_async(Deployment.update_deployment_model)(project_id, project_name, status=status, progress=progress)"""定义一个异步的 函数 deployment_log_outputasync def:协程(协程是由用户程序控制的) 可以暂停和恢复执行部署日志模块记录processObject: 进程对象send_news: 处理消息的方法buildProjectEnum:构建项目的 Enum类responseCodeEnum: 响应Enum类progress: 进度值
"""
async def deployment_log_output(processObject,send_news,responseCodeEnum:ResponseCodeEnum):while True:# await 关键字用于暂停当前协程的执行 直到 process.stdout.readline() 完成并返回结果。在等待期间,其他协程可以继续执行。line = await processObject.stdout.readline()#当读完 break 终止 循环if not line:break#发送日志 even  await:等待发送完成,再继续执行await send_news(line.decode('utf-8'), responseCodeEnum)#等待子进程执行完毕await processObject.wait()logging.info(f"发送日志信息成功: %s" %(line.decode('utf-8')))
************************/deployments/Consumers.py***************************
import os
from typing import Anyimport django
import loggingfrom channels.generic.websocket import AsyncWebsocketConsumer
import jsonfrom deployments.views import cicd_execute
from operation.common.enum.ResponeCodeEnum import ResponseCodeEnum# 手动设置 DJANGO_SETTINGS_MODULE
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'operation.settings')# 配置 Django
django.setup()class ChatConsumer(AsyncWebsocketConsumer):async def connect(self):logging.info("connect")await self.accept()async def disconnect(self, close_code):pass"""Exception inside application: You cannot call this from an async context - use a thread or sync_to_async.Traceback (most recent call last):这个错误提示表明你在异步上下文中调用了同步代码,这在 Django Channels 中是不允许的。你需要将同步代码转换为异步代码,或者使用 sync_to_async 包装同步代码。"""async def receive(self, text_data):text_data_json = json.loads(text_data)id = text_data_json['id']project_name = text_data_json['projectName']# 调用CI/CD执行函数await self.channel_layer.send(self.channel_name,{'type': 'execute_cicd','id': id,'project_name': project_name,})async def execute_cicd(self, event):id = event['id']project_name = event['project_name']# 调用CI/CD执行函数await cicd_execute(id, project_name, self.send_progress,self.send_news)"""发送 进度"""async def send_progress(self, progress,status,code,message):await self.send(text_data=json.dumps({'type': 'progress','code': code,'message': message,'data': {'progress': progress,  #进度值'status': status        # 状态}}))"""发送 执行日志: 类型注解,用于明确指定 responseCodeEnum 参数的类型是 ResponseCodeEnum"""async def send_news(self,logInfo: Any,responseCodeEnum:ResponseCodeEnum):await self.send(text_data=json.dumps({'type': 'news','code': responseCodeEnum.code,'message': responseCodeEnum.message,'data': {'logInfo':logInfo   #日志信息}}))

四、启动django项目 (需要支持WebSocket或其他异步通信,使用daphne启动)

"""和项目交互基本上都是基于这个文件,一般都是在终端输入python3 manage.py [子命令]manage.py输入python3 manage.py help查看更多启动项目  python3 manage.py runserver 创建app模块 python3 manage.py startapp app生成迁移文件,描述如何将模型更改应用到数据库中 python3 manage.py makemigrations将这些文件迁移应用到数据库中 python3 manage.py migrate如果你需要支持WebSocket或其他异步通信,使用daphne operation.asgi:application。如果你只需要简单的HTTP服务,使用python manage.py runserver。export DJANGO_SETTINGS_MODULE=your_project_name.settings"""
#安装Daphne
pip install daphne#终端输入
daphne operation.asgi:application

五、前端代码

<template><div class="app-container"><el-card shadow="always"><el-form ref="searchForm" :inline="true" :model="searchMap" style="margin-top: 20px"><el-form-item><el-button type="primary" icon="el-icon-search" @click="searchLog('searchForm')">搜索</el-button><el-button type="primary" icon="el-icon-clear" @click="resetForm('searchForm')">重置</el-button></el-form-item></el-form></el-card><el-card shadow="always"><template><el-button size="mini" icon="el-icon-plus" type="primary" @click="openAddDrawer()">新增</el-button></template></el-card><el-row :gutter="24"><el-col :span="24"><el-card shadow="always"><div><el-table ref="multipleTable" v-loading="listLoading" :data="getDeploymentListDto" border fit highlight-current-row style="width: 100%;" class="tb-edit"><el-table-column prop="id" label="应用id" width="180px" align="center"></el-table-column><el-table-column prop="name" label="应用名称" width="180px" align="center"></el-table-column><el-table-column prop="status" label="状态" width="180px" align="center" :formatter="statusFormatter"></el-table-column><el-table-column prop="created_at" label="创建时间" width="180px" align="center"></el-table-column><el-table-column prop="actions" label="操作"><template slot-scope="scope"><el-button type="text" @click="redeploy(scope.row)">重新部署</el-button></template></el-table-column><!-- <el-table-column label="部署进度"><template slot-scope="scope"><el-progress :percentage="scope.row.progress" v-if="scope.row.progress"></el-progress></template></el-table-column> --><el-table-column label="部署进度"><template slot-scope="scope"><div style="display: flex; align-items: center;"><el-progress :percentage="scope.row.progress" v-if="scope.row.progress" style="flex: 1;"></el-progress><el-button type="text" @click="viewLogs(scope.row)" style="margin-left: 10px;">查看日志</el-button></div></template></el-table-column></el-table></div><div class="block"><el-pagination :current-page="currentPage" :page-sizes="[5, 10, 15, 20]" :page-size="5" layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange" @current-change="handleCurrentChange" /></div></el-card></el-col></el-row><!--新增项目 Drawer 层--><el-drawer title="增加应用" :visible.sync="addDrawerVisible" :direction="direction" size="50%"><el-card shadow="always"><el-form ref="addProjectForm" :model="addProjectForm" status-icon :rules="rules" label-width="100px" class="demo-ruleForm"><el-form-item label="应用名称" prop="name"><el-input v-model="addProjectForm.name" placeholder="请输入应用名称" /></el-form-item><el-form-item><el-button type="primary" @click="addProjectSubmitForm('addProjectForm')">保存</el-button></el-form-item></el-form></el-card></el-drawer><!-- 日志弹出层 --><el-drawer title="日志信息" :visible.sync="viewLogDrawerVisible" :direction="direction" size="50%"><el-card shadow="always"><el-scrollbar ref="logScrollbar" style="height: 800px;"> <!--设置滚动区域的高度--><div v-html="logContent"></div></el-scrollbar></el-card></el-drawer></div>
</template><script>import {getDeploymentsList,viewCICD,addDeployment} from '@/api/deployment/deployment-request'import {message} from 'rhea-promise'import {ElScrollbar} from 'element-ui';export default {name: 'DeploymentPage',data() {return {getDeploymentListDto: [], // 数据传给list,列表渲染的数据total: 0,listLoading: true,dialogVisible: false,currentPage: 1,addDrawerVisible: false,viewLogDrawerVisible: false,direction: 'rtl',logContent: '',jsonstr: {"id": "","projectName": ""},addProjectForm: {name: ""},rules: {name: [{required: true,message: '请输入应用名称',trigger: 'blur'}],},params: {page: 1, // 当前页码page_size: 5 // 每页显示数目},searchMap: {},socket: null}},mounted() {this.getDeploymentsList()},beforeDestroy() {if (this.socket) {this.socket.close()}},methods: {handleSizeChange(val) {console.log(`每页 ${val} 条`)this.params.page_size = valthis.getDeploymentsList()},handleCurrentChange(val) {console.log(`当前页: ${val}`)this.params.page = valthis.getDeploymentsList()},// 格式化 列statusFormatter(row, column, cellValue) {switch (cellValue) {case '0':return '未启动';case '1':return '初始化中';case '2':return '初始化成功';case '3':return '初始失败';case '4':return '部署中';case '5':return '部署成功';case '6':return '部署失败';case '7':return '启动中';case '8':return '启动成功';case '9':return '启动失败';default:return '未知状态';}},// 重置功能, element ui 提供的功能resetForm(formName) {console.log(this.$refs[formName].resetFields)this.$refs[formName].resetFields()this.getDeploymentsList()},searchLog(formName) {console.log(this.searchMap)this.getDeploymentsList()},//新增项目openAddDrawer() {this.addDrawerVisible = true; // 显示Drawer},viewLogs(row) {// 这里可以添加查看日志的逻辑// console.log('查看日志:', row);// // 例如,可以打开一个新的对话框或跳转到日志页面// this.$message.info(`查看日志: ${row.name}`);this.viewLogDrawerVisible = true},// 操作日志列表 ajax 请求getDeploymentsList() {// 目标需求:在历史查询列表页面中加入查询的转圈的loading加载动画。this.dataLoading = trueconsole.log('请求参数:' + this.params)getDeploymentsList(this.params).then((res) => {console.log('响应:', res.data.data)this.getDeploymentListDto = res.data.datathis.total = res.data.pagination.totalsetTimeout(() => { // 超过指定超时时间  关闭查询的转圈的loading加载动画this.listLoading = false}, 1.5 * 1000)})},addProjectSubmitForm(formName) {this.$refs[formName].validate((valid) => {if (valid) {this.addProjectInfo();} else {console.log('error submit!!');this.getDeploymentsList();return false;}});},//增加应用addProjectInfo() {this.dataLoading = trueaddDeployment(this.addProjectForm).then((res) => {console.log(res);this.addDrawerVisible = false; // 关闭抽屉this.getDeploymentsList();this.$refs.addOrgForm.resetFields();// this.$router.push('/organizationList'); // 假设列表页的路由路径是 /listsetTimeout(() => {this.listLoading = false}, 1.5 * 1000)})},redeploy(row) {// row.status = '3'; // 更新状态为“部署中”// row.progress = 0; // 初始化进度this.socket = new WebSocket('ws://localhost:8000/ws/cicd_progress/');this.jsonstr.id = row.id;this.jsonstr.projectName = row.name;// alert(JSON.stringify(this.jsonstr));this.socket.onopen = () => {this.socket.send(JSON.stringify(this.jsonstr));};this.socket.onmessage = (event) => {const data = JSON.parse(event.data);// alert(event.data)if (data.type === 'progress') {// 处理进度消息if (data.code !== 200) {// 处理错误消息row.status = data.data.status; // 更新当前状态console.error(data.message);this.$message.error(data.message); // 显示错误消息this.getDeploymentsList();} else {// 更新进度条const progress = data.data.progress; // 假设服务器返回的进度数据在progress字段中row.progress = progress; // 更新进度条的百分比row.status = data.data.status; //更新当前状态this.getDeploymentsList();}} else if (data.type === 'news') {// 处理日志消息// this.$message.info(data.message); // 显示日志消息this.logContent += `<p>${data.data.logInfo}</p>`; // 追加日志this.$nextTick(() => {this.scrollToBottom(); // 每次更新日志后滚动到底部});} else {console.error('Unknown message type:', data.type);}};this.socket.onclose = () => {console.log('WebSocket closed');row.status = '5'; // 假设部署成功后状态为5};this.socket.onerror = (error) => {console.error('WebSocket error:', error);row.status = '6'; // 假设部署失败后状态为6this.$message.error('WebSocket 连接错误'); // 显示错误消息};},scrollToBottom() {const scrollbar = this.$refs.logScrollbar.$refs.wrap;scrollbar.scrollTop = scrollbar.scrollHeight;},}}
</script><style>
</style>

六、效果

#启动前端项目 点击重新发布  点击查看日志
npm run dev


http://www.ppmy.cn/ops/137267.html

相关文章

TCP三次握手与四次挥手(TCP重传机制,2MSL)超详细!!!计算机网络

本篇是关于3次握手和四次挥手的详细解释~ 如果对你有帮助&#xff0c;请点个免费的赞吧&#xff0c;谢谢汪。&#xff08;点个关注也可以&#xff01;&#xff09; 如果以下内容需要补充和修改&#xff0c;请大家在评论区多多交流~。 目录 1. TCP头部&#xff1a; 2. 三次握手…

Python中的简单爬虫

文章目录 一. 基于FastAPI之Web站点开发1. 基于FastAPI搭建Web服务器2. Web服务器和浏览器的通讯流程3. 浏览器访问Web服务器的通讯流程4. 加载图片资源代码 二. 基于Web请求的FastAPI通用配置1. 目前Web服务器存在问题2. 基于Web请求的FastAPI通用配置 三. Python爬虫介绍1. 什…

MongoDB相关问题

视频教程 【GeekHour】20分钟掌握MongoDB Complete MongoDB Tutorial by Net Ninja MongoDB开机后调用缓慢的原因及解决方法 问题分析&#xff1a; MongoDB开机后调用缓慢&#xff0c;通常是由于以下原因导致&#xff1a; 索引重建&#xff1a; MongoDB在启动时会重建索引…

Spring集成测试

Spring集成测试是一种用于测试Spring应用程序中各个组件之间的交互和集成的测试方法。它通常用于验证应用程序的整体行为&#xff0c;而不仅仅是单个组件的功能。以下是一些常见的Spring集成测试技术和工具&#xff1a; Spring TestContext Framework: Spring提供了一个强大的测…

【linux】tar命令讲解笔记

Linux tar 命令 Linux tar&#xff08;英文全拼&#xff1a;tape archive &#xff09;命令用于备份文件。 tar 是 Linux 和 Unix 系统中用于归档文件和目录的强大命令行工具。 tar 名字来自 "tape archive"&#xff08;磁带归档&#xff09;&#xff0c;最初用于将…

微信小程序WXSS全局样式与局部样式的使用教程

微信小程序WXSS全局样式与局部样式的使用教程 引言 在微信小程序的开发中,样式的设计与实现是提升用户体验的关键部分。WXSS(WeiXin Style Sheets)作为微信小程序的样式表语言,不仅支持丰富的样式功能,还能通过全局样式与局部样式的灵活运用,帮助开发者构建美观且易于维…

LemoBook - 一个创新性的无数据库网站解决方案!

&#x1f4e2; 激动地向大家介绍我最近的开源项目&#xff1a;LemoBook - 一个创新性的无数据库网站解决方案&#xff01; &#x1f31f; 主要特点&#xff1a; 零数据库依赖&#xff1a;完全基于 GitHub 进行内容存储和版本控制现代技术栈&#xff1a;Next.js 14 Tailwind …

鸢尾花Iris训练数据和测试数据的分割和训练数据的散点图矩阵绘制

鸢尾花Iris训练数据和测试数据的分割和训练数据的散点图矩阵绘制 鸢尾花Iris训练数据和测试数据的分割和训练数据的散点图矩阵绘制 鸢尾花Iris训练数据和测试数据的分割和训练数据的散点图矩阵绘制一、训练数据和测试数据1.1 训练数据&#xff08;training data&#xff09;1.2…