近期准备面试中,觉得项目经验这里还需要再次总结,也参考了很多jym的文章,在这里如果发现有引用且介意,请联系我
// 配置element的中文 import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
登录成功 -> 判断是否存在用户信息 -> 是? 跳转页面 ; 如果不存在该用户,那么要先创建该用户角色拥有的权限再判断能否跳转页面 总体来说就是用户关联角色、角色又关联菜单
路由权限
页面级权限:(使用的是方式2)
permission.ts
pinia
的权限实例usePermissionStore
来获取权限内的菜单,并添加到动态路由。pinia
,先看后端返回的数据,一般menus返回的是菜单及页面,permissions
返回的是按钮权限列表,所以只需要遍历menus判断是菜单或者页面,注意要转成树形结构。router.addRoute()
方法添加到路由列表就好。按钮级权限:
v-if
或者封装一个公共方法来判断,这里用的是自定义指令main.ts
中引入directives/modules/permission.ts
来编写权限指令接口权限
主页面开发(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() } }
以【获取页面组件的实时滚动位置钩子useScroll
】为例,
useScroll()
会到响应式数据scrollTop
const scrollTop = ref(0)
hook
:const scrollTop = useScroll(scrollingElement)
scroll
事件,组件卸载时移除该DOM事件监听器,以避免内存泄露;scroll
事件监听器中,实时获取根元素的scrollToP
,同步给ref数据scrollTop
:const scrollHandler = (e) => { scrollTop.value = root.scrollTop }
先讲如何跨域
封装axios实例(包括baseUrl,timeout等基础设置)+ 实例拦截器
POST
、GET
、PUT
、DELETE
请求,发请求只需要写入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的默认配置能够很好地满足这些项目的需求,我们就不需要花费太多时间在打包配置上。
通过最小化拆分包,我们可以将项目代码划分为多个模块或块,每个模块只包含当前页面或功能所需的代码。当用户访问特定页面时,只需加载该页面所需的模块,而无需加载整个项目代码。
这可以减少初始加载时间和资源消耗,适合较大型的项目或者对打包配置有特殊需求的项目
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]", }, }, }, });
- vite 4.X 版本已经不集成 terser,需要自行下载。
- 在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
插件,由于这个插件在国内不好安装,所以需要先改配置package.json
添加以下配置"resolutions": { "bin-wrapper": "npm:bin-wrapper-china" }
cmd
yarn add vite-plugin-imagemin -D
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 // 不启用移除空属性的插件 } ] } }) ]
import()
?来导入需要懒加载的组件。当用户访问到对应的路由时,该组件才会被异步加载,实现了按需加载的效果。const routes = [ { path: '/', name: 'Home', component: () => import('../views/Home.vue') }, { path: '/about', name: 'About', component: () => import('../views/About.vue') } ]
gzip压缩是一种常用的数据压缩算法,它可以减小文件的大小,从而减少文件的传输时间和占用空间。gzip压缩算法基于DEFLATE算法,使用了哈夫曼编码和LZ77算法来实现高效的数据压缩。
当使用gzip压缩文件时,文件会被转换为一种经过压缩和编码的格式。这种格式可以通过减少冗余数据和使用更紧凑的编码方式来降低文件的大小。压缩后的文件通常以
.gz
为扩展名。
yarn add vite-plugin-compression2 -D
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, //是否禁用插件 }), ], });
个人对ts浅薄的理解:就是type + javascript ,js 有的 ts 都有,所有js 代码都可以在 ts 里面运行。
ts是js的超集。
any
?类型函数参数
和返回值
的类型type AddFn = (num1: number, num2: number) => number const add: AddFn = (num1, num2) => { ?return num1 + num2 }
如果函数没有返回值,那么,函数返回值类型为: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 IPerson { ?name: string ?age: number ?sayHi(): void } let person: IPerson = { ?name: 'jack', ?age: 19, ?sayHi() {} }
interface Point2D { x: number; y: number } // 继承 Point2D interface Point3D extends Point2D { ?z: number }
|
(竖线)在 TS 中叫做联合类型,即:由两个或多个其他类型组成的类型,表示可以是这些类型中的任意一种let arr: (number | string)[] = [1, 'a', 3, 'b']
extends
关键字来实现继承需要更加明确的值时,用到断言
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
)number
; ids
方法的返回值类型是 number[]
websocket是用来实现客户端和服务器之间数据通信的一种手段;即浏览器和服务器只需要建议一次连接,两者之间就可以实现双向数据传输
问题:已经有了HTTP协议,为什么还需要WebSocket? HTTP协议的缺陷: 通信只能是客户端发起, 不具备服务器推送功能,也就是说服务器不能主动向客户端推送消息. 这种单向通信方式,需要使用[轮询]查询方式,每隔一段时间就发出一次询问,了解服务器有没有新的消息.这种方式效率很低,浪费资源。
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."); };
每个属性的含义:
WebSocket
方法:
- send(data):通过 WebSocket 连接传输至服务器的数据队列,并根据所需要传输的数据的大小来增加 bufferedAmount 的值;
- close():关闭 WebSocket 连接,如果连接已经关闭,则此方法不执行任何操作;
WebSocket
事件:
- close:当一个 WebSocket 连接被关闭时触发,也可以通过 onclose 属性来设置;
- error:当一个 WebSocket 连接因错误而关闭时触发,也可以通过 onerror 属性来设置;
- message:当通过 WebSocket 收到数据时触发,也可以通过 onmessage 属性来设置;
- open:当一个 WebSocket 连接成功时触发,也可以通过 onopen 属性来设置;
<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>
解释:
即时Web
应用程序:即时Web
应用程序使用一个Web
套接字在客户端显示数据,这些数据由后端服务器连续发送。在WebSocke
t中,数据被连续推送/传输到已经打开的同一连接中,这就是为什么WebSocket
更快并提高了应用程序性能的原因。 例如在交易网站或比特币交易中,这是最不稳定的事情,它用于显示价格波动,数据被后端服务器使用Web套接字通道连续推送到客户端。
游戏应用程序:在游戏应用程序中,你可能会注意到,服务器会持续接收数据,而不会刷新用户界面。屏幕上的用户界面会自动刷新,而且不需要建立新的连接,因此在WebSocket
游戏应用程序中非常有帮助。
聊天应用程序:聊天应用程序仅使用WebSocket
建立一次连接,便能在订阅户之间交换,发布和广播消息。它重复使用相同的WebSocket
连接,用于发送和接收消息以及一对一的消息传输。