vue3+vite+ts+pinia 后台管理项目总结

前言

近期准备面试中,觉得项目经验这里还需要再次总结,也参考了很多jym的文章,在这里如果发现有引用且介意,请联系我

VUE3+VITE+TS

项目中的要点

  1. 使用vue3+vite+ts 构建项目基础框架,配置代码运行环境,对项目进行模块化划分,并集成element组件库、axios请求库;
  2. 利用axios拦截器实现了权限校验,对系统的登入权限进行控制,避免了无效请求;
  3. 利用vue-router路由钩子函数实现系统角色权限控制,动态路由实现页面级权限、vue指令实现按钮级权限;
  4. 封装包含PageHeader页头组件 + EpTable通用表格渲染组件 + BtnGroup通用按钮组 + EpDialog确认弹窗组件;
  5. 二次封装ElForm表单组件,根据具体的业务需求,灵活使用ElForm表单进行表单校验;
  6. 打包上线

配置路由

  • vue3创建router/index.ts
  • 写路由的时候氛围无需权限路由(比如登录)和需要权限的路由。
  • 在vite.config.ts里配置跨域 一般是proxy。
  • element 按需引入也是在vite.config.ts里通过AutoImport和Compontes去引入
 

// 配置element的中文 import zhCn from 'element-plus/dist/locale/zh-cn.mjs'

权限管理:

  • 登录流程:

登录成功 -> 判断是否存在用户信息 -> 是? 跳转页面 ; 如果不存在该用户,那么要先创建该用户角色拥有的权限再判断能否跳转页面 总体来说就是用户关联角色、角色又关联菜单

  • 路由权限

    • 方式1:加载完整路由表,加全局守卫判断meta,无权限时都跳转到登录页
    • 方式2:先挂载白名单(公共路由表),登录后再通过addRoute动态挂载路由表
  • 页面级权限:(使用的是方式2)

    • 登录之后利用路由守卫来进行,先在根目录创建permission.ts
    • 路由前置守卫里判断,要预先配置访问路由白名单,引入pinia的权限实例usePermissionStore来获取权限内的菜单,并添加到动态路由。
    • 获取用户信息这部分放到pinia,先看后端返回的数据,一般menus返回的是菜单及页面,permissions返回的是按钮权限列表,所以只需要遍历menus判断是菜单或者页面,注意要转成树形结构。
    • 最后通过router.addRoute()方法添加到路由列表就好。
  • 按钮级权限:

    • 按钮级权限一般都是通过v-if或者封装一个公共方法来判断,这里用的是自定义指令
    • 新建一个目录存放自定义指令,去自动遍历目录内的指令文件并注册,且在main.ts中引入
    • 新建directives/modules/permission.ts来编写权限指令
    • 需要先去接收一个权限标识,首先判断用户是否是管理员,如果是不做多余的处理,如果不是,则去判断用户的权限列表是否存在该权限标识,如果不存在,那么移除按钮。
  • 接口权限

    • axios的响应拦截器里发现返回码是400/401,直接到登录页;
    • axios的所有请求均携带token,在请求拦截器里统一添加请求头,当服务端无响应时,也跳转到登录页。
  • 主页面开发(layout、侧边栏、标签栏、页面切换过渡和页面缓存)

    • 控制侧边栏折叠的按钮是通过slot的方式传入的顶部导航栏,这里用provide发送,在菜单组件用inject接收。
    • 左侧的菜单根据保存的菜单数据来渲染就可以。
    • 组件结构如下layout -> components -> Sidebar -> index.vue , SidebarMenu.vue,SidebarItem.vue
    • 先引用element的滚动条组件,然后再引入SidebarMenu组件。
    • 这个组件,先从pinia中获取菜单数据,并传递给子组件进行渲染;设置上默认展开项,遍历菜单的时候加上一层递归,如果有二级或三级菜单也可以设置上默认展开;对菜单进行排序,采用版本号排序。
    • 然后来看SidebarItem组件
    • 首先去判断该菜单是否要在菜单栏隐藏,然后判断菜单是一级菜单还是二级菜单,也支持只有一个二级菜单的一级菜单直接显示。这些都可以通过alwaysShow配置决定。
  • 标签栏:

    • 需要注意的点标签的数据是哪里赖的,监听route,在route变化时,将新的路由信息添加到标签列表。通过方法pinia封装的方法tagsViewStore.addView()添加,之后用router-link来渲染标签数据。
    • addView()方法主要根据标签的meta属性来判断是否渲染
  • 前端字典:

    • 后端应该都会维护一个叫字典项,大概知道不太懂,如果后端没有提供,自己也有过一定的摸索。
    • 使用的时候引入需要渲染的数据,el-form-item 里的 el-radio遍历数据,并可以在数据配置default,就渲染一个默认值。

封装的思路

封装组件

这里以封装的 EpTable通用表格渲染组件 为例:

  • 发现这些字段的格式高度统一,sortable控制当前字段可否排序,fixed控制当前字段列是否固定,prop定义当前列要渲染数据的哪个字段,label定义列标题,width定义列宽,formatter定义当前字段的加工处理方式,default插槽定义当前列的复杂渲染方式;
  • 最终以配置的方式将表格所有字段的渲染方式处理为一个props,其值为一个数组,对每个要处理的字段以一个对象的形式进行渲染配置;
  • 将最后一列的操作按钮(编辑+删除)固定写死,也可以根据条件渲染;
  • 以上都是组件配置,还提供events事件和便捷操作API;
  • 比如当用户在特定行点击删除的时候,EpTable会给父组件发送自定义事件deleteItem,携带的载荷为当前行row,由父组件处理事件,将数据中相同id的item予以删除
  • 用户在表格中执行多选,EpTable知道用户实时选中了哪些行
  • 父组件通过refEpTable获取EpTable实例在进一步通过调用refEpTable.value.getSelectedItems()获取用户实时选中的所有行,再执行批量操作
封装指令

这里以按钮权限时封装的自定义指令v-permission为例

  • 可以在调用时先接收权限标识,判断该用户是否是管理员,如果是不做处理;如果不是,那么再去判断该用户是否有对应的权限,如果没有,那么就移除该按钮

  • 指令的使用

 

<el-button type="primary" @click="showMenuPop()" v-permission="'sys:menu:add'" >添加菜单</el-button>

  • 指令的内部逻辑

mounted(el: any, binding: any) { const permissionStore = usePermissionStore() const staffStore = useStaffStore() if (staffStore.staff?.role_code === superAdminRole) { return } const hasPermission = permissionStore.permissions.includes(binding.value) if (!hasPermission) { el.remove() } }

封装hook

以【获取页面组件的实时滚动位置钩子useScroll】为例,

  • 该函数可以实时获取页面的滚动位置,调用useScroll()会到响应式数据scrollTop
  • hook内要定义响应式输入const scrollTop = ref(0)
  • 需要将真正在滚动的元素作为参数传递给该hookconst scrollTop = useScroll(scrollingElement)
  • 组件挂载时对其根元素添加scroll事件,组件卸载时移除该DOM事件监听器,以避免内存泄露;
  • scroll事件监听器中,实时获取根元素的scrollToP,同步给ref数据scrollTop:

const scrollHandler = (e) => { scrollTop.value = root.scrollTop }

  • 最终调用该hook的组件得到的就是其根元素实时滚动的位置这一响应式数据
封装axios
  • 先讲如何跨域

    • 开发阶段:通过配置vite的热更新服务器实现跨域,其原理是访问本地的/api,开发服务器会自动代理到服务器的后端地址,服务器之间的互访是不受浏览器同源策略(CORS policy)限制的,跨域得以实现;
    • 生产阶段: 使用Nginx替代开发服务器实现跨域,跨域原理与开发阶段相同;
  • 封装axios实例(包括baseUrl,timeout等基础设置)+ 实例拦截器

    • 在请求拦截器中写入通用配置,请求头中统一携带token
    • 在响应拦截器中统一过滤数据,直接提取res.data等
    • 还可以封装通过的POSTGETPUTDELETE请求,发请求只需要写入url、data等
    • 通常还可以封装通用的处理错误的方法
封装模块

以添加用户一条换电订单为例

  • 用户在订单列表头部点击新增按钮,进入新增页;
  • 新增页加载一个空白的订单数据表单,预先按照指定的空白订单数据JSON模板;
  • 用户修改该表单中的各个数据项,表单的所有数据项通过双向数据绑定的方式,将电影的所有字段同步到一个响应式对象filmForm中;
  • 对于电影海报和电影演职人员头像,采用立即上传的方式,使用的是ElementPlus中提供的Upload组件;
  • 最后用户点击提交,将表单数据格式化为后台所需要的数据格式,具体细节为:将演职人员头像由[{name,url}]格式化为[{name,role,avtarAddress}],将数据由[{name,url}]格式化为poster-string形式,再通过ajax的POST请求将表单数据发送服务端,等待服务端返回;
  • 如果数据提交成功,导航调回列表页;

打包上线、优化配置

先说一个插件的使用 视图分析工具 rollup-plugin-visualizer

  • 在项目中使用 rollup-plugin-visualizer 插件可以生成可视化的代码分析报告,看看哪些模块占用了空间,帮助我们更好地了解构建过程中的文件大小、依赖关系等信息
  • vite.config.ts中引入 rollup-plugin-visualizer 插件,并将其添加到插件列表中。
  • 执行命令yarn build打包出来,视图会自动跳出,保存在项目根目录下
  • stats.html这个就是的视图文件,可以直观的看到各个模块占据空间的大小
配置打包文件分类输出

将js,css,图片等资源分别打包到对应的文件夹下,这种方式适合小型项目或者需要快速搭建原型的项目。Vite的默认配置能够很好地满足这些项目的需求,我们就不需要花费太多时间在打包配置上。

js最小拆包

通过最小化拆分包,我们可以将项目代码划分为多个模块或块,每个模块只包含当前页面或功能所需的代码。当用户访问特定页面时,只需加载该页面所需的模块,而无需加载整个项目代码。

这可以减少初始加载时间和资源消耗,适合较大型的项目或者对打包配置有特殊需求的项目

  • vite.config.ts

export default defineConfig({ // 其他配置项... build: { rollupOptions: { output: { // 最小化拆分包 manualChunks(id) { if (id.includes("node_modules")) { // 通过拆分包的方式将所有来自node_modules的模块打包到单独的chunk中 return id .toString() .split("node_modules/")[1] .split("/")[0] .toString(); } }, // 设置chunk的文件名格式 chunkFileNames: (chunkInfo) => { const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split("/") : []; const fileName1 = facadeModuleId[facadeModuleId.length - 2] || "[name]"; // 根据chunk的facadeModuleId(入口模块的相对路径)生成chunk的文件名 return `js/${fileName1}/[name].[hash].js`; }, // 设置入口文件的文件名格式 entryFileNames: "js/[name].[hash].js", // 设置静态资源文件的文件名格式 assetFileNames: "[ext]/[name].[hash:4].[ext]", }, }, }, });

剔除console和debugger
  1. vite 4.X 版本已经不集成 terser,需要自行下载。
  2. 在vite.config.ts中去配置
  • vite.config.ts

import { defineConfig } from "vite"; export default defineConfig({ build: { minify: 'terser', // 启用 terser 压缩 terserOptions: { compress: { pure_funcs: ['console.log'], // 只删除 console.log //drop_console: true, // 删除所有 console drop_debugger: true, // 删除 debugger } } } })

图片资源压缩
  • 安装vite-plugin-imagemin插件,由于这个插件在国内不好安装,所以需要先改配置
  • 使用yarn安装需要在package.json添加以下配置

"resolutions": { "bin-wrapper": "npm:bin-wrapper-china" }

  • 安装
 

cmd

yarn add vite-plugin-imagemin -D

  • vite.config.ts
import viteImagemin from 'vite-plugin-imagemin';
 plugin: [ viteImagemin({ gifsicle: { optimizationLevel: 7,// 设置GIF图片的优化等级为7 interlaced: false // 不启用交错扫描 },
 optipng: { optimizationLevel: 7 // 设置PNG图片的优化等级为7 }, 
mozjpeg: { quality: 20 // 设置JPEG图片的质量为20 }, 
pngquant: { quality: [0.8, 0.9], // 设置PNG图片的质量范围为0.8到0.9之间 speed: 4 // 设置PNG图片的优化速度为4 }, 
svgo: { plugins: [ { name: 'removeViewBox' // 启用移除SVG视图框的插件 }, { name: 'removeEmptyAttrs', active: false // 不启用移除空属性的插件 } ] } }) ]
路由懒加载
  • 路由懒加载的实现方式通常是使用动态导入
  • 比如在 Vue 项目中使用 import() 来导入需要懒加载的组件。当用户访问到对应的路由时,该组件才会被异步加载,实现了按需加载的效果。

const routes = [ { path: '/', name: 'Home', component: () => import('../views/Home.vue') }, { path: '/about', name: 'About', component: () => import('../views/About.vue') } ]

使用 gzip 压缩

gzip压缩是一种常用的数据压缩算法,它可以减小文件的大小,从而减少文件的传输时间和占用空间。gzip压缩算法基于DEFLATE算法,使用了哈夫曼编码和LZ77算法来实现高效的数据压缩。

当使用gzip压缩文件时,文件会被转换为一种经过压缩和编码的格式。这种格式可以通过减少冗余数据和使用更紧凑的编码方式来降低文件的大小。压缩后的文件通常以.gz为扩展名。

  • 安装插件

yarn add vite-plugin-compression2 -D

  • vite.config.ts
import { defineConfig } from 'vite';
 import compression from 'vite-plugin-compression';
 export default defineConfig({ // 其他配置项... plugins: [ // 其他插件... compression({ algorithm: "gzip", 
// 指定压缩算法为gzip,[ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw'] ext: ".gz", // 指定压缩后的文件扩展名为.gz threshold: 10240, // 仅对文件大小大于threshold的文件进行压缩,默认为10KB deleteOriginFile: false, // 是否删除原始文件,默认为false filter: /\.(js|css|json|html|ico|svg)(\?.*)?$/i, // 匹配要压缩的文件的正则表达式,默认为匹配.js、.css、.json、.html、.ico和.svg文件 compressionOptions: { level: 9 }, // 指定gzip压缩级别,默认为9(最高级别)
 verbose: true, //是否在控制台输出压缩结果
 disable: false, //是否禁用插件 }), ], });

typescript

个人对ts浅薄的理解:就是type + javascript ,js 有的 ts 都有,所有js 代码都可以在 ts 里面运行。

ts是js的超集。

ts的基本类型
  • Boolean
  • Number
  • String
  • Symbol
  • undefined 是所有类型的子类型
  • null 是所有类型的子类型
  • any 任何类型都可以被归为 any 类型
  • never 永不存在的值的类型
函数类型
  • 函数的类型实际上指的是:函数参数返回值的类型
  • 为函数指定类型的两种方式:
    1. 单独指定参数、返回值的类型
    2. 同时指定参数、返回值的类型

type AddFn = (num1: number, num2: number) => number const add: AddFn = (num1, num2) => {  return num1 + num2 }

void类型

如果函数没有返回值,那么,函数返回值类型为:void

function greet(name: string): void {  console.log('Hello', name) } // 如果什么都不写,此时,add 函数的返回值类型为: 
void const add = () => {} // 这种写法是明确指定函数返回值类型为 void,与上面不指定返回值类型相同 const add = (): void => {} // 但,如果指定 返回值类型为 undefined,此时,函数体中必须显示的 return undefined 才可以 const add = (): undefined => {  // 此处,返回的 undefined 是 JS 中的一个值  return undefined }function greet(name: string): void {  console.log('Hello', name) } // 如果什么都不写,此时,add 函数的返回值类型为: void const add = () => {} // 这种写法是明确指定函数返回值类型为 void,与上面不指定返回值类型相同 const add = (): void => {} // 但,如果指定 返回值类型为 undefined,此时,函数体中必须显示的 return undefined 才可以 const add = (): undefined => {  // 此处,返回的 undefined 是 JS 中的一个值  return undefined }

对象类型

JS 中的对象是由属性和方法构成的,而 TS 对象的类型就是在描述对象的结构

// 空对象 let person: {} = {} ​ // 有属性的对象 let person: { name: string } = {  name: '同学' } ​ // 既有属性又有方法的对象 // 在一行代码中指定对象的多个属性类型时,使用 `;`(分号)来分隔 let person: { name: string; sayHi(): void } = {  name: 'jack',  sayHi() {} } ​ // 对象中如果有多个类型,可以换行写: // 通过换行来分隔多个属性类型,可以去掉 `;` let person: {  name: string  sayHi(): void } = {  name: 'jack',  sayHi() {} } ​ // 方法的类型也可以使用箭头函数形式 {    greet(name: string):string,    greet: (name: string) => string }

interface类型

当一个对象类型被多次使用时,一般会使用接口(interface)来描述对象的类型,达到复用的目的

  • 使用 interface 关键字来声明接口
  • 声明接口后,可以直接使用接口名称作为变量名称
  • 每行只有一个属性,不能使用分号

interface IPerson {  name: string  age: number  sayHi(): void } let person: IPerson = {  name: 'jack',  age: 19,  sayHi() {} }

接口继承
  • 如果两个接口之间有相同的属性或方法,可以将公共的属性或方法抽离出来,通过继承来实现复用
  • 比如,这两个接口都有 x、y 两个属性,重复写两次,可以,但很繁琐

interface Point2D { x: number; y: number } // 继承 Point2D interface Point3D extends Point2D {  z: number }

interface 和 type的区别
  • 相同点:都可以给对象指定类型
  • 不同点:
    • interface
      • 只能为对象指定类型
      • 可以使用extends继承
      • 多个同名的interface会合并
    • type
      • 不仅可以为对象指定类型,实际上可以为任意类型指定别名
      • 可以使用&运算符实现继承效果
      • 多个同名的type会报错
联合类型
  • 解释:|(竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种

let arr: (number | string)[] = [1, 'a', 3, 'b']

ts中calss类的关键字
  • extends 通过extends关键字来实现继承
  • super 子类没有定义自己的属性,可以不写super,如果子类有自己属性,可以用super把父类的属性继承过来
  • public 共有的,一个类里默认所有的方法和属性都是public
  • private 私有的,只属于类自己,实例和继承都访问不到
  • static 是静态属性,类的常量,实例不能访问
类型断言

需要更加明确的值时,用到断言

const aLink = document.getElementById('link') as HTMLAnchorElement // 或者用 <> 语法 const aLink = <HTMLAnchorElement>document.getElementById('link')

泛型

泛型是可以在保证类型安全前提下,让函数等与多种类型一起工作,从而实现复用。

例如:定义泛型函数

需求:创建一个 id 函数,传入什么数据就返回该数据本身(也就是说,参数和返回值类型相同)

function id<Type>(value: Type): Type { return value } function id<T>(value: T): T { return value } /** 语法:在函数名称的后面添加 `<>`(尖括号),尖括号中添加类型变量,比如此处的 Type 类型变量 Type,是一种特殊类型的变量,它处理类型而不是值 该类型变量相当于一个类型容器,能够捕获用户提供的类型(具体是什么类型由用户调用该函数时指定) 因为 Type 是类型,因此可以将其作为函数参数和返回值的类型,表示参数和返回值具有相同的类型 类型变量 Type,可以是任意合法的变量名称 **/ // 调用函数 输入什么值 那么就返回什么类型的值 const num = id<number>(10) const str = id<string>('a')

  • 泛型接口

interface IdFunc<Type> {  id: (value: Type) => Type  ids: () => Type[] } ​ let obj: IdFunc<number> = {  id(value) { return value },  ids() { return [1, 3, 5] } }

解释:

  • 接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口
  • 接口的类型变量,对接口中所有其他成员可见,也就是接口中所有成员都可以使用类型变量
  • 用泛型接口时,需要显式指定具体的类型(比如,此处的 IdFunc)
  • 此时,id 方法的参数和返回值类型都是number ; ids 方法的返回值类型是 number[]

websocket

WebSocket是什么

websocket是用来实现客户端和服务器之间数据通信的一种手段;即浏览器和服务器只需要建议一次连接,两者之间就可以实现双向数据传输

问题:已经有了HTTP协议,为什么还需要WebSocket? HTTP协议的缺陷: 通信只能是客户端发起, 不具备服务器推送功能,也就是说服务器不能主动向客户端推送消息. 这种单向通信方式,需要使用[轮询]查询方式,每隔一段时间就发出一次询问,了解服务器有没有新的消息.这种方式效率很低,浪费资源。

WebSocket使用

1. 建立连接
  • 使用WebSocket,通过构造函数实例化
// 构造一个 webSocket 对象 const socket = new WebSocket('ws://localhost:8080'); // const socket = new WebSocket('wss://localhost:8080'); ws.onopen = function(evt) { console.log("Connection open ..."); ws.send("Hello WebSockets!"); }; ws.onmessage = function(evt) { console.log( "Received Message: " + evt.data); }; ws.onclose = function(evt) { console.log("Connection closed."); };
  • 实例化对象包含的属性

    cb82a86afdd5461da8a264dee3b24d0b~tplv-k3u1fbpfcp-zoom-in-crop-mark_1512_0_0_0.webp

每个属性的含义:

  • binaryType:使用二进制的数据类型连接;
  • bufferedAmount(只读):未发送至服务器的字节数;
  • extensions(只读):服务器选择的扩展;
  • onclose:用于指定连接关闭后的回调函数;
  • onerror:用于指定连接失败后的回调函数;
  • onmessage:用于指定当从服务器接受到信息时的回调函数;
  • onopen:用于指定连接成功后的回调函数;
  • protocol(只读):用于返回服务器端选中的子协议的名字;
  • readyState(只读):返回当前 WebSocket 的连接状态,共有 4 种状态:
    • CONNECTING — 正在连接中,对应的值为 0;
    • OPEN — 已经连接并且可以通讯,对应的值为 1;
    • CLOSING — 连接正在关闭,对应的值为 2;
    • CLOSED — 连接已关闭或者没有连接成功,对应的值为 3;
  • url(只读):返回值为当构造函数创建 WebSocket 实例对象时 URL 的绝对路径;

WebSocket方法:

  • send(data):通过 WebSocket 连接传输至服务器的数据队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值;
  • close():关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作;

WebSocket事件:

  • close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置;
  • error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置;
  • message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置;
  • open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置;
2. 项目中使用WebSocket
<script> export default { data() { return { socket: null, aliveTime: new Date().getTime(), checkTimer: null } }, computed: { token() { return this.$store.getters.token } }, beforeDestroy() { clearInterval(this.checkTimer) this.socket && this.socket.close() }, mounted() { if (this.socket && this.socket.readyState === 1) { clearInterval(this.checkTimer) this.socket.close() } if (this.socket && this.socket.readyState === 3) { this.initWebSocket() } this.getData() }, methods: { getData() { // ...... this.initWebSocket() }, initWebSocket() { if (typeof WebSocket === 'undefined') { this.$message({ message: '您的浏览器不支持WebSocket' }) return false } this.checkTimer && clearInterval(this.checkTimer) this.socket && this.socket.close() this.aliveTime = new Date().getTime() const token = this.token.split('Bearer ')[1] const wsurl = `wss://${process.env.VUE_APP_DOMAIN}/ws?token=${token}` this.socket = new WebSocket(wsurl) this.socket.onmessage = this.websocketonmessage this.socket.onerror = this.websocketonerror this.checkTimer = setInterval(this.checkWebsocketAlive, 5 * 1000) }, websocketonmessage(e) { const response = JSON.parse(e.data) if (response.message === 'success') { const data = response.data // 处理 data } // 这里的场景是服务端主动推数据,接收到消息说明连接正常 if (response.message === 'connection alive') { this.aliveTime = new Date().getTime() } }, websocketonerror() { clearInterval(this.checkTimer) this.socket.close() }, checkWebsocketAlive() { const now = new Date().getTime() if (now - this.aliveTime > 60 * 1000) { this.aliveTime = now this.initWebSocket() } }, } } </script>
3. 应用场景
  • 即时聊天通信
  • 多玩家游戏
  • 在线协同编辑/编辑
  • 实时数据流的拉取与推送
  • 体育/游戏实况
  • 实时地图位置

解释:

  • 即时Web应用程序:即时Web应用程序使用一个Web套接字在客户端显示数据,这些数据由后端服务器连续发送。在WebSocket中,数据被连续推送/传输到已经打开的同一连接中,这就是为什么WebSocket更快并提高了应用程序性能的原因。 例如在交易网站或比特币交易中,这是最不稳定的事情,它用于显示价格波动,数据被后端服务器使用Web套接字通道连续推送到客户端。

  • 游戏应用程序:在游戏应用程序中,你可能会注意到,服务器会持续接收数据,而不会刷新用户界面。屏幕上的用户界面会自动刷新,而且不需要建立新的连接,因此在WebSocket游戏应用程序中非常有帮助。

  • 聊天应用程序:聊天应用程序仅使用WebSocket建立一次连接,便能在订阅户之间交换,发布和广播消息。它重复使用相同的WebSocket连接,用于发送和接收消息以及一对一的消息传输。

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

子伟-H5

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值