Vue

以前在公司一直使用的都是若依框架,所以对一些内容掌握的并不是很好,所以趁着在家的这段时间,自己写一个管理系统,那么就涉及到了动态路由,对不同权限的用户,展示不同菜单栏和向router中追加与该用户权限匹配的路由。版本:Vue3+VueRouter4。1.为什么使用动态路由:如果直接将路由表写死的话,那么在用户未登录的情况下,用户可以直接通过手动输入url,达到目标页面。当用户登录后,我们拿着后端返回的路由列表,去匹配本地动态路由列表中的路由->路由对比。假设后端返回了该用户具有A路由,并且本地动态路由列表中也存在A路由,那么就直接将本地的A路由添加到router中。为什么要进行路由对比?因为本地动态路由表中存储的是所有动态路由表(需要用户权限访问的),而当前用户并不一定拥有访问所有路由的权限。所以要从本地动态路由列表中筛选出后端返回的那些路由列表,然后追加到router路由表中,这样在用户就不能访问到那些它不具备权限的路由页面了。当然,如果通过抓包工具篡改返回的数据,还是可以访问到的,所以后端也需要进行验证才更加安全。我们应该先写一份公共的路由,这个公共的路由可以在未登录的情况下访问,例如:Layout布局、登录页、404页。Layout布局肯定是不能在未登录的情况下访问,所以后续会使用全局前置路由守卫进行判断。将它写在公共路由的原因是因为:后续追加的动态路由,需要添加到Layout布局路由的子节点路由中(嵌套路由)。并且应该将这个公共路由表,添加到路由初始化时的routes属性中。也应该将需要权限才能访问的动态路由,写到一个数组中,这个数组就是本地的动态路由表。import{createRouter,Router,createWebHashHistory,RouteRecordRaw}from'vue-router';importLayoutfrom'@/layout/index.vue';//Layout布局//公共路由表constconstantRoutes:RouteRecordRaw[]=[{path:'/',name:'Layout',component:Layout,},{path:'/login',name:'Login',component:()=>import('@/view/login.vue'),meta:{title:'登录页'}},{path:'/:pathMatch(.*)*',name:'NotFound',component:()=>import('@/view/404.vue'),meta:{title:'页面丢失了~'}}];//动态路由表constasyncRoutes:RouteRecordRaw[]=[{path:'/',name:'/',component:()=>import('@/view/index.vue'),meta:{title:'主控台'}},{path:'/goods/list',name:'/goods/list',component:()=>import('@/view/commodity/commodity.vue'),meta:{title:'商品管理'}}];//创建路由,并导出实例exportconstrouter:Router=createRouter({history:createWebHashHistory(),routes:constantRoutes});还差一个路由对比的功能,我们可以在上面的文件中,在写一个添加路由的函数,在里面实现路由对比,并且将它暴露出去。//参数:接收后端给的用户可访问的路由列表。exportfunctionaddRouters(menus:any[]){//功能:路由对比,将匹配的添加到路由constrouteComparison=(menus:any[])=>{//遍历后端返回的路由列表menus.forEach((menuItem:any)=>{//判断当前遍历的这个路由,在本地动态路由列表是否存在,frontpath相当于本地的pathconstisMatching=asyncRoutes.find((asyncItem:any)=>menuItem.frontpath===asyncItem.path);//如果匹配到了,则逻辑与一下:它是否已经被注册过了,如果没有被注册过我们在添加进去。if(isMatching&&!router.hasRoute(isMatching.path)){//将匹配出来的路由,添加到Layout布局路由的childrens中。router.addRoute('Layout',isMatching);}//判断当前遍历的这个路由是否有子节点,如果有子节点且子节点长度大于0,则进行递归,使用子节点进行对比。if(mentItem.child&&mentItem.child.length>0){routeComparison(mentItem.child);}});}//调用路由对比addRouteHandler(menus);}这个时候,我们可以完善一下全局前置路由守卫的代码,在src下新建:premission.ts文件,并在main.ts中引入。importstorefrom'@/store';import{router,addRouters}from'@/router';//从Cookies中(获取|删除)Token。import{getToken,removeToken}from'@/composables/auth';//是否获取了用户信息consthasGetUserInfo=false;//全局路由前置守卫router.beforeEach(async(to,from,next)=>{//获取Tokenconsttoken=getToken();//如果没有Token,并且访问的不是登录页,直接重定向到登录页,这里就会防止用户未登录直接进入Layout布局界面。if(!token&&to.path!=='/login'){returnnext('/login');}//如果有Token,但是访问的是登录页if(token&&to.path==='/login'){//从哪里来的,回哪里去returnnext(from.path);}//如果有Token,并且没有获取用户信息呢if(token&&!hasGetUserInfo){//拉取用户信息去,如果token过期或者被非法篡改,会在axios的拦截器中进行处理。constgetInfoRes=awaitstore.dispatch("getInfo");//进行追加路由addRouters(getInfoRes.menus);//将hasGetUserInfo置truehasGetUserInfo=true;}//在最后必须要放行next();});在登录之后,服务器会返回Token,然后将Token存储到cookies中,然后调用router.push('/'),跳转首页。接着就会触发全局路由前置守卫,接着会同步获取用户信息,然后追加路由。2.刷新后变404页或者空白页这样,动态路由就添加完了,但是存在着一个问题,下面进行问题复现:当前浏览器的路由地址处于asyncRoutes列表上的某一个路由时,如果按F5刷新,页面就会变成404或者空白。如果constRoutes中存在匹配404的路由规则,那么就会显示404,否则就是空白页,并且控制台会有一个警告。但是吧,你在追加完成路由以后,调用router.getRoutes()方法,又可以看到已经将路由追加进去了。马德,这个问题其实也困扰了我很多天,看了很多解决方案,但是我觉得答案并不靠谱,下面进行简单的分析。按照正常的代码逻辑来看,会执行以下三步:获取用户信息、追加路由、放行路由。这三个步骤都是同步执行的,并且我动态路由都已经添加好了,放行后应该不会出现问题啊。分析:首先,我们将404这个路由项注释掉,并且在beforeEach()回调函数里的第一行打上断点,输入:debugger;即可。router.beforeEach((to,from,next)=>{debugger;//.......});在刷新时,观察控制台,可以看到,给我们抛出了一个警告,意思为:没有找到与路径对应的位置。[VueRouterwarn]:Nomatchfoundforlocationwithpath"刷新的那个动态路由的path"由此得知,在进入beforeEach()中的回调函数前,就已经出现问题了。我们可以尝试将debugger;删除,在相同位置,打印一下回调函数中的to属性。console.log(to);//结果{"fullPath":"/goods/list","path":"/goods/list","query":{},"hash":"","params":{},"matched":[],"meta":{},"href":"#/goods/list"}可以看到matched数组,是一个空的,代表着没有匹配到相关的路由。我们在前面将404路由注释了,如果你将它解开注释,那么这里的matched就会只有一个元素,就是404路由。<router-view/>会渲染matched上的内容。我们的项目中共有两个router-view,在app.vue中有一个,在Layout布局有一个。假设matched数组中有两个元素,第一个元素是Layout路由,第二个是/goods/list路由。那么app.vue中的router-view就会渲染Layout布局组件,然后Layout布局中的router-view渲染/goods/list路由组件。matched元素越靠前,使用的router-view就越靠外层。原因:其实这一切都与to有关,我们在刷新之后,触发了全局前置路由守卫,然后会调用它里面的回调函数。试想一下,此时的路由数组中只有那三个静态路由(假设这句话包括404路由)。那么在触发beforeEach的回调函数时,vueRouter需要给matched设置匹配的路由,如果没有设置404页,那么这个数组它是空的,就会发出一个警告,既然是空的,那么没内容可以给router-view渲染,然后就会出现一个空白页。如果有404页,在动态路由添加之前,输入一个不存在的地址,matched数组的元素肯定是一个404,所以就会让router-view渲染404页呗~总而言之,这个to,是动态路由没有追加进来时的to,所以才会这样。解决:首先我们要知道:在全局前置守卫中,next()、next('/')两个的区别。在全局前置守卫中,调用next()代表着放行的意思,而调用next(‘/‘)代表着重定向的意思。两个的区别在于:next('/')会中断此导航,并重新触发路由守卫,而next()就不会,它就单纯的放行。所以!!!!我们可以在追加路由后,重新触发一次路由守卫,而不是直接放行,这样就能解决问题了。修改全局前置路由守卫中的代码:if(token&&!hasGetUserInfo){//拉取用户信息去,如果token过期或者被非法篡改,会在axios的拦截器中进行处理。constgetInfoRes=awaitstore.dispatch("getInfo");//进行追加路由addRouters(getInfoRes.menus);//将hasGetUserInfo置truehasGetUserInfo=true;//当添加完成后,直接进行一次重定向returnnext(to.path);}//一定要放行next();Ok!!问题解决~

Vue

今天在项目开发时,由于素材都是存在本地了,然后还没压缩,导致小程序项目超出了2MB,然后无法编译了,就了解了一下分包,在采取分包后,顺利解决!本篇文章中,小程序采用的是Uniapp框架,和原生小程序中的分包流程相同,只不过无需修改manifest.json文件。分包简介在微信小程序中,默认只有一个主包,在项目打包时,所有的资源(页面及资源等)都被放到了主包中。造成的问题:在小程序中,主包的体积最大只可以达到2MB,如果超出了2MB,则不通过了。对于以上的问题,可以采取分包,分包就相当于将指定的页面及资源,单独放在一个包中,不在放到主包中,从而减少主包的体积。分包的上限及条件:开发者可以分N个包,但是要求每个包不允许超过2MB,整个小程序所有分包大小不能超过20MB。分包的好处:主包中一般存放的都是Tabbar页面,如果将非Tabbar页面从主包中移除,则可以加快加载速度。分包中的内容,默认不加载,只有需要用到它时,再去现场加载,当然,可以通过分包预下载去提前加载分包中的内容。使用分包-uniapp注意:如果是Tabbar页面,或者是多个分包需要用到的公共资源,请放到主包中。分包可以访问主包中的资源,而主包不允许访问分包中的资源。假设:现在有一个用户端[公共的],一个管理端[需要权限],我们可以将用户端放到主包中,将管理端的内容分成一个包。因为用户在进入小程序时,默认进入的都是用户端,没必要将管理端的资源也加载上,只有当拥有权限并且在切换到管理端时,我们在去加载管理端的内容。//当前文件结构树[未分包],默认都存放到pages中,当分包后,pages中的内容就是主包的内容。pages:|-...//假设用户端页面有N个,这里就不写了|-backStage/index/index.vue:管理端首页|-backStage/xxxxx/index.vue:管理端第二页/*分包准备:1.新建一个文件夹,和pages同级,*每个文件夹相当于一个分包*,并将静态资源命名为static然后放到分包的根目录下。2.在该分包下新建一个pages文件夹,该文件夹中存储页面文件夹。3.然后将分包中的内容,从主包中移走。*///主包pages:|-...//用户端的内容//管理端分包backStagePackage:|-pages:管理端页面|-index/index.vue:管理端首页|-xxxxx/index.vue:管理端第二页|-static:静态资源然后打开manifest.json文件,在mp-weixin节点中的根节点下,添加配置:"optimization":{"subPackages":true}接着打开pages.json配置分包:在和pages同级节点下,添加subPackages字段,值为数组,它用于配置分包。它的每一个元素是一个对象,每个元素及为一个分包。使用管理端分包为例,首先在数组中添加一个对象,对象中拥有:root,pages,name,independent字段。root属性:它的值为分包文件夹的名称,即:backStagePackage。pages属性:它的值为一个数组,每一个元素填写分包中的页面路径即可(不需要加上分包名)。name属性:分包别名,分包预下载时可以使用。independent属性:是否开启独立分包,默认不开启(false)。"subPackages":[{"root":"backStagePackage","pages":["pages/index/index","pages/xxxxx/index"]}]

2022-6-12 564 0
Vue

1.优化前先分析依赖//首先安装依赖npminstallwebpack-bundle-analyzer--save-dev//然后在vue.config.js中进行引入constBundleAnalyzerPlugin=require('webpack-bundle-analyzer').BundleAnalyzerPlugin;//然后添加如下代码configureWebpack:{plugins:[newBundleAnalyzerPlugin({analyzerMode:'server',analyzerHost:'127.0.0.1',analyzerPort:8888,reportFilename:'report.html',defaultSizes:'parsed',openAnalyzer:true,generateStatsFile:false,statsFilename:'stats.json',statsOptions:null,logLevel:'info'}),]}当打包或者运行时,会在本地8888端口展示依赖情况,然后自行分析。2.采用路由懒加载在写路由时,不要将所有的组件都一下加载全,应该使用import()进行懒加载。这样只有当访问这个路由时,才会加载对应的模块。cosntroutes=[{path:"/home",component:()=>import('@/view/Home')//使用import进行懒加载}];3.进行GZIP压缩需要在Webpack中构建gz文件,然后在Nginx中启用gzip。//首先安装依赖npminstallcompression-webpack-plugin@1.1.12//然后在vue.config.js中进行引入constCompressionWebpackPlugin=require('compression-webpack-plugin');//然后添加以下代码configureWebpack:{plugins:[newCompressionWebpackPlugin({filename:'[path].gz[query]',algorithm:'gzip',test:/\.js$|\.json$|\.css/,threshold:10240,//只有大小大于该值的资源会被处理minRatio:0.8,//只有压缩率小于这个值的资源才会被处理})]}然后在Nginx中进行以下配置,启用GZIP。#开启gzip。gzipon;#开启后如果能找到.gz文件,直接返回该文件,不会启用服务端压缩。gzip_staticon;#文件大于指定size才压缩,以kb为单位。gzip_min_length1;#用于识别http协议的版本,早期的浏览器不支持gzip压缩,用户会看到乱码,所以为了持前期版本加了此选项,目前此项基本可以忽略gzip_http_version1.1;#压缩级别,1-9,值越大压缩比越大,但更加占用CPU,且压缩效率越来越低。gzip_comp_level9;#压缩的文件类型。gzip_typestext/cssapplication/javascriptapplication/json;4.使用CDN引入资源在打包时,会将import导入的东西,打包成js文件,然后该文件就会很大.如果我们的服务器带宽很小,那么还要加载这些资源就很慢,这样的话我们可以使用cdn的方式,去让用户请求其他服务器获取资源。我们需要在index.html中进行使用link或者script标签的方式去引入对应的资源。然后在vue.config.js中进行配置。//Key:和package中的名称一样,value:全局变量名//一定要将Vue引入[通过cdn,也要在这里进行配置],因为有些文件依赖着它。configureWebpack:{externals:{'vue':"Vue",'element-ui':"ELEMENT",'axios':'axios','jquery':'$','vue-router':'VueRouter','vuex':'Vuex'}}

2022-5-30 496 0
2022-5-28 459 0
Vue

1.需求:在三级联动菜单中,我们一共有一千多个内容,并且需要点击其中的一个内容,就需要去跳转路由。2.解决分析:在渲染时为组件绑定上@click事件、使用事件委托。3.分析绑定@click:如果为每一个内容都绑定一个@click,那么就相当于添加了一千多个回调函数,对性能很不友好,但是可以实现功能。4.分析事件委托:如果我们为三级联动菜单的父元素添加一个点击事件,同样,也可以解决该问题,并且,采用该方式只需要添加一个@click,通过查找触发源去判断执行哪些操作。在JavaScript中,拥有事件冒泡的机制,当点击一个子元素时,触发父元素的事件,然后在爷爷元素触发事件,直到触发到祖元素的事件。在祖元素的点击事件回调函数中,拥有一个event属性,该属性是一个对象,里面有一个target属性,该属性存储的就是触发源头的DOM信息,这样就知道点击的哪个元素,从而触发了祖元素的事件。然后我们在通过自定义属性或者标签名,去判断该执行哪些操作。5.事件委托和事件冒泡:事件委托是利用事件冒泡的机制,它们并不一样。6.事件委托的缺点:如果在向上冒泡时,突然被某一层给把事件冒泡阻止掉了[event.stopPropagation],那么就不会冒泡到祖元素了,接着事件委托对应的处理函数也就不会执行了。对于不支持冒泡的事件,无法向上级传递。

2022-5-3 330 0
Vue

1.说明在编程式路由导航当中,如果重复点击一个链接[路由导航],那么就会抛出一个NavigationDuplicated异常,该异常并不会影响我们的程序执行。在声明式路由导航中,不会出现这种情况。这种情况只会在vue-<ahref="mailto:router@3.1.0">router@3.1.0</a>+版本出现,因为它引入了primise。目前<aclass="at-link"href="https://github.com/3">@3</a>.1.1版本,官方已经修复了此问题。2.为什么会出现这样的错误router.push(location,onComplete?,onAbort?)router.replace(location,onComplete?,onAbort?)以上是路由跳转的两个函数,注意看它的第二个和第三个参数,在官方文档中是这么写的:当导航跳转成功完成之后,会执行onComplete回调,它相当于resolve。当导航到相同的路由,或者此次路由未导航成功之前,又导航到其他路由时就会执行onAbort回调,它相当于reject。在3.1.0+后,可以省略这两个参数,如果忽略了,那么就会返回一个Promise。如果填写了这两个参数,返回的就是undefined。那么,当没有传递第二个和第三个参数时,并且点击的是同一个导航,就会返回reject。当返回reject后,我们并未捕获这个错误,然后控制台就会报红。解决办法1:降级处理,使用#3.1.1版本。解决办法2:在调用方法时,传递上第二个和第三个参数。this.$router.push("/search",()=>{},()=>{});解决办法3:在push后链式调用catch()方法,捕获错误。注意:不能使用try…catch的形式去捕获,这种捕获方式只能捕获同步方法的错误。this.$router.push("/search").catch((err)=>{});解决办法4(优):对push和replace方法进行二次封装,在router->index.js中封装即可。//保存一份原有的方法letoriginPush=VueRouter.prototype.push;letoriginReplace=VueRouter.prototype.replace;//修改原型上的函数内容,如果传递了resolve和reject则使用传递的,否则默认处理内容为空//在调用原有的push时[相当于调用上面保存的那一份],应该让VueRouter实例调用,应该修改上下文。VueRouter.prototype.push=function(location,resolve,reject){if(resolve&&reject){originPush.call(this,resolve,reject)}else{originPush.call(this,location,(res)=>{},(err)=>{});}}VueRouter.prototype.replace=function(location,resolve,reject){if(resolve&&reject){originReplace.call(this,resolve,reject)}else{originReplace.call(this,location,(res)=>{},(err)=>{});}}

Vue

.sync修饰符:注意:该修饰符在Vue3.0版本中已经取消了,被替换成了v-model。1、简介:在一般情况下,当父组件给子组件传递一个自定义属性后,例如传递的自定义属性是一个字符串,那么我们在子组件中是不可以直接进行修改的,如果修改的话,就会报错。但是我们可以在父组件中为这个子组件的实例上绑定一个自定义事件,当子组件触发该自定义事件时,在去修改父组件中的数据,从而达到想要的效果。当我们使用.sync修饰符时,就相当于为被传递的自定义属性值,添加了一个双向绑定,从而就可以在子组件中,去直接修改父组件中的数据。2、未使用.sync修饰符修改父组件数据:顺序:首先在父组件中,为子组件传递数据,以及绑定自定义事件(changeText)。然后点击子组件中的按钮,触发自身实例上的自定义事件,从而修改父元素数据。页面:在父组件中,为子组件传递数据,以及绑定自定义事件。在changeText事件中,$event的值是$emit()触发事件时传递的值,然后这个值,就赋值给data中的text了,从而达到了子给父传值的效果。<divid="app"><test:prop-text='text'@changeText='text=$event'></test></div>子组件:接收父组件传递的自定义属性,在点击按钮时,触发changeText事件,并携带值。lettest={template:`<div><p>{{propText}}</p><button@click='changeText'>修改propText</button></div>`,props:["propText"],methods:{changeText(){this.$emit("propText","text的值被子组件修改咯!~");}}}父组件:注册子组件。constvm=newVue({el:"#app",data:{text:"我现在展示的是App组件的内容~"},components:{test},});3、使用.sync修饰符修改父组件数据-传递字符串:页面:对比第2行代码:在:prop-text后添加了.sync修饰符。并且将绑定的自定义事件删除了。<divid="app"><test:prop-text.sync='text'></test></div>子组件:对比第10行代码,触发的事件名需要写成以下格式,其中update:是固定的。update:在props中接收的属性名当触发该事件时,就会去寻找组件自身实例上prop-text属性绑定的值,然后去修改它。lettest={template:`<div><p>{{propText}}</p><button@click='changeText'>修改propText</button></div>`,props:["propText"],methods:{changeText(){this.$emit("update:propText","text的值被子组件修改咯!~");}}}父组件:constvm=newVue({el:"#app",data:{text:"我现在展示的是App组件的内容~"},components:{test},});

2022-4-1 703 0
Vue

0x01-工作原理:0、State:它是一个对象,里面存储的是N个组件共享的数据。1、VueComponents:一堆组件,这些组件需要共享State中的数据。2、dispatch():这是一个API,当调用它之后,就会进入Actions这个步骤。参数:①:执行的函数名,假设这里的函数是add,然后就会去Actions对象中去匹配这个add函数。②:为这个函数参数传递的数据,假设这里的参数是1。2.1、Actions:它里面存储的是一个对象,这个对象中,全是函数,这些函数是要我们自己写的,既然在dispatch()中选择了add函数,那么在这个对象中肯定是要写一个add函数的。并且在这个对象中,函数可以接收到两个参数:①:context->(上下文)。②:value->(就是dispatch()传递的第二个参数:1)。然后我们需要在这个函数中,使用context属性来调用commit()函数。在调用commit()函数时,需要为该函数传递两个参数,请看下面。3、commit():当使用context上下文属性调用commit()方法后,就会进入Mutations这个步骤。在调用commit()函数时,需要为该函数传递两个参数:①:调用Mutations中的哪个函数,肯定是add。②:传递的参数,一般就是Actions中的value属性。3.1、Mutations:它里面存储的也是一个对象,它存储是也是全是函数,这些函数也是我们自己添加的。我们在commit()中选择调用add函数,那么该对象中肯定也要有一个add()函数的。在这个对象中的方法,也可以接收到两个参数:①:state对象。②:value,也就是commit传递的第二个属性。然后,我们就可以在这个函数中直接操作state对象中的属性了。4、render:当State中的属性值发生变化时,就会重新帮我们解析组件。5、Actions和Mutations的区别:如果在调用dispatch()函数时,我们不知道传递一个什么数据,必须要请求后端的接口才能知道传递什么,那么我们就需要在Actions对象中的函数中请求后端接口,然后在调用commit()。或者我们需要进行延时操作,在500毫秒之后在操作State对象中的数据,那么就需要在Actions对象中的函数中进行延时操作,在500毫秒之后直接调用commit()进行操作State对象。那么也就发现,如果有逻辑业务层操作的话,都是在Actions对象函数中完成业务逻辑。而Mutations中的函数,是不要进行业务逻辑的,它负责的就是最终修改State中的数据。如果我们知道初始传递一个什么数据,则可以不进行Actions这一步,直接调用commit()进行Mutations这一步进行修改数据即可。并且它们的函数名也应该有所区分,在Actions中的函数进行小写,而在Mutations中的函数进行大写。6、总结:如果知道初始数据,并且不需要业务逻辑,则可以让组件直接调用commit()函数。如果需要业务处理,则必须让组件调用dispatch()函数,然后在通过上下文调用commit()函数。Mutations这一步骤,才是最终修改State数据的步骤。

2022-2-16 271 0