• [技术干货] 常见的JS数据存储方式及其特点
    在前端开发中,经常需要在浏览器端存储和管理数据。为了实现数据的持久化存储和方便的访问,JavaScript提供了多种数据存储方式。本文将介绍几种常见的前端JS数据存储方式及其特点。1. CookieCookie是一种小型的文本文件,由浏览器保存在用户计算机上。使用JavaScript可以读取和写入Cookie。Cookie主要用于存储少量的数据,并且会在每次HTTP请求中自动发送到服务器。但是,Cookie的存储容量有限,通常不超过4KB,而且受浏览器设置的限制。2. LocalStorageLocalStorage是HTML5引入的一种持久化的本地存储机制。它允许在浏览器中存储大量的数据,并且在浏览器关闭后数据仍然保持不变。LocalStorage使用简单的键值对来存储数据,可以通过JavaScript的localStorage对象进行访问和操作。LocalStorage的存储容量通常为5MB或更大,但是由于是在用户浏览器上存储数据,因此需要注意不要存储敏感或重要的信息。3. SessionStorageSessionStorage与LocalStorage类似,也是一种HTML5提供的本地存储机制。不同的是,SessionStorage中存储的数据在浏览器会话结束后就会被清除,而不是持久化存储。SessionStorage适用于临时保存会话数据,比如在同一浏览器选项卡或窗口之间共享数据。4. IndexedDBIndexedDB是一种高级的客户端数据库,用于在浏览器中存储结构化数据。它提供了事务性的存储机制和丰富的查询功能。IndexedDB适用于需要存储大量结构化数据或进行复杂查询的场景。与其他存储方式相比,IndexedDB的学习曲线较陡峭,需要使用JavaScript API进行操作。5. Web Storage APIWeb Storage API是一个统一的API,包括LocalStorage和SessionStorage。它提供了一套简单的接口,用于访问和操作这两种存储方式。通过Web Storage API,可以轻松地读取、写入和删除数据。6.其他存储方式除了上述提到的存储方式,还有一些其他的存储方式可供选择,如WebSQL、FileSystem API等。然而,由于这些存储方式的兼容性和使用限制等问题,它们的使用已经逐渐减少,不再被广泛推荐。总结开发人员可以根据具体需求选择合适的存储方式。Cookie适合存储小型数据和会话相关信息,LocalStorage和SessionStorage适合在浏览器中进行持久化或临时存储,IndexedDB适合处理大量结构化数据和复杂查询。使用这些数据存储方式,可以有效管理和利用前端的数据。如果你想要查看当前网站存储了哪些数据,可以点击“F12”,在开发者工具中,切换到 “存储” 选项卡 其中,会话存储就是 SessionStorage;本地存储就是localStorage
  • [技术干货] JavaScript获取http,http://请求协议头,域名,端口,url
    window.location.protocol 获取http,http://请求协议头  window.location.host 获取域名并带端口 xxx.com 如果有端口就是 xxx.com:8080  window.location.hostname; 获取域名: xxx.com  window.location.href; 获取整个url地址:http://xxx.com/xxx.html  window.location.pathname; 获取域名后面请求路径url: /xxx/xxx.html  window.location.port 获取端口比如 8080 ———————————————— 原文链接:https://blog.csdn.net/u012834688/article/details/125640832 
  • [问题求助] 坐席二次开发问题
    【问题来源】    汉口银行    【问题简要】    座席二次开发问题【问题类别】    CC-Gateway【AICC解决方案版本】    AICC版本:AICC 22.200    UAP版本:UAP9600 V100R005C00SPC113    CTI版本:  ICD V300R008C25spc012【期望解决时间】    尽快【问题现象描述】您好,二次开发CCGateway接续接口需要使用轮询接口做会话保活。这个是在坐席前端轮询调用接口:https://ip:port/agentgateway/resource/agentevent/{agentid}?calldata=XXX做会话保持吗?
  • [技术干货] javaScript获取键值对的三种方法?keys 、entire.....
    Javascript获取数据对象里面的键key和value大家都知道在后端接口的数据处理后在返回是一件非常重要的事情,根据返回的方法不同,返回的格式也就有所区别,比如通过json的数据格式来返回,再或者通过键值对的方式来返回等等,所以为了更好的让大家理解和区别开键值对是怎么来工作的,这里介绍几种方法让大家更好的了解。方法一:使用 keys值的方法来获取对象的键值key方法二:使用entries方法来获取对象的键值key方法三:使用for循环获取对象的键值key三种方法都可以获取到数组中的key值,同时对象是键值的集合,我们需要一个键值来获取对象中的值是多少,键值对在java、javascrtipt中都有着广泛的应用。那问题来了,如果我们在检索值的过程中只要有他的键值就可以 但是我们如果没有key是多少呢?方法一介绍:Object.keys()Object.keys()使用函数返回对象键的数组值是多少我们以javascript为例,javascript对象作为参数传递给了该函数,如果我们在输出一个数组的时候他的键值对的顺序是不会随机排列的,而是和原始的对象中顺序相同,通用我们将数组传递给给了 对象的键值对,它也会通过数组的索引作为输出返回该结果,并且返回的参数中同样带有索引,所以keys的函数会返回这些索引的数组。代码演示:var zeyuli1 = ["1", "2", "3"];var zeyuli2 = { 0: "1", 4: "0", 2: "3", 3: "0"};var zeyuli3 = { "id": "1", "name": "2", "old": "3"};console.log(Object.keys(zeyuli1));console.log(Object.keys(zeyuli2));console.log(Object.keys(zeyuli3));大家可以手动练习一下看看结果是多少呢?方法二介绍:Object.entries(OBJ)Object.entries(OBJ)的方法运用起来要比keys的函数要灵活的多方法也多的多,在整个对象划分为每个不同的数组时候用key和value形式来表示键值对的形成,使用方法二我们可以得到一个对象中的所有键值对,这就是和方法一的本质区别。语法:Object.entries(OBJ)参数:和方法一的本质区别就是 Object.entries(OBJ)的参数是OBJ注意:和方法一的函数同样具备对象的属性顺序。代码演示:var  zeyuli1 = { "id": "1", "name": "2", "old": "24"};console.log(JSON.stringify(Object.entries(zeyuli1)));方法三介绍:for循环的使用遍历大家都知道经常使用到for循环,那么如何使用for才能让数组都经历一次对象遍历呢?学过的小伙伴都知道 使用的是for和in的固定搭配,使用i来保存对象的key值,而object[i]保存与对象中的key相对应的值。代码展示如下:var a = { "id": "1", "name": "1", "old": {"name": "2", "appearance": "3"}};for(let i in a){  console.log(i);  console.log(a[i]);}使用的嵌套结构中也可以用console.log(i)来获取键值。
  • [技术干货] JS 防抖与节流
    滚动对页面渲染的影响web页面渲染会经历如下步骤:js加载 —> style加载 —> layout(确定布局) —> paint(页面绘制) —> composite(组合控件)网页生成的时候,至少会渲染(Layout+Paint)一次。用户访问的过程中,还会不断重新的重排(reflow)和重绘(repaint),用户scroll行为和resize行为会导致页面不断的进行重新渲染,如果间隔频繁,容易造成浏览器卡帧。为什么滚动scroll、窗口resize需要优化滚动事件的应用很频繁:图片懒加载、下滑自动加载数据、侧边浮动导航栏等。在绑定scroll、resize事件时,但它发生的时候,它被触发的频率非常高,间隔很近。在日常开发的页面中,事件中涉及到的大量的位置计算、DOM操作、元素重绘等等这些都无法在下一个scroll事件出发前完成的情况下,浏览器会卡帧。防抖与节流在进行窗口的resize、scroll、输出框内容校验等操纵的时候,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致浏览器卡顿。为了用户更好的体验,就可以采用防抖(debounce)和节流(throttle)的方式来减少调用的频率,减轻浏览器的负担。防抖Debounce使用情景有些场景事件触发的频率过高(mousemove onkeydown onkeyup onscroll)回调函数执行的频率过高也会有卡顿现象。 可以一段时间过后进行触发去除无用操作原理当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。代码示例以下是一个简单的JavaScript代码示例,它实现了一个基本的防抖功能:function debounce(func, delay) { let timer = null; return function() { const context = this; const args = arguments; clearTimeout(timer); timer = setTimeout(function() { func.apply(context, args); }, delay); } }这个代码定义了一个名为debounce的函数,它接受两个参数:要防抖的函数和防抖的时间间隔。当调用debounce函数时,它会返回一个新的函数,这个新函数将被用来代替原来的函数。每次调用新函数时,它会清除之前设置的定时器,并创建一个新的定时器。如果在指定时间间隔内再次调用新函数,定时器将被清除并重新设置。只有在指定时间间隔内没有再次调用新函数时,定时器才会在最后一次调用后触发,并调用原始函数。这个防抖函数可以用于限制某些操作的执行频率,例如在用户连续输入时进行搜索操作。这可以减少不必要的网络请求和计算资源消耗。节流Throttle使用情景图片懒加载ajax数据请求加载原理当持续触发事件时,保证一定时间段内只调用一次事件处理函数。代码示例以下是一个简单的JavaScript代码示例,它实现了一个基本的节流功能:function throttle(func, delay) { let timer = null; return function() { const context = this; const args = arguments; if (!timer) { timer = setTimeout(function() { func.apply(context, args); timer = null; }, delay); } } }这个代码定义了一个名为throttle的函数,它接受两个参数:要节流的函数和节流的时间间隔。当调用throttle函数时,它会返回一个新的函数,这个新函数将被用来代替原来的函数。每次调用新函数时,它会检查是否已经设置了一个定时器。如果没有,它会创建一个新的定时器,并在指定的时间间隔后调用原始函数。如果已经设置了定时器,则新函数将不执行任何操作。这个节流函数可以用于限制某些操作的执行频率,例如在用户滚动页面时更新页面元素的位置。
  • [技术干货] this.$toast() 了解一下?
    在平时的开发过程中,我们总是先写好一个组件,然后在需要的页面中用 import 引入即可,但如果是下面这种类型的组件呢 上面这种类型的浮层提示有一个很大的特点,就是使用频率特别高,几乎每个页面都会用到它,于是乎我们就要在每个页面中去引入该组件,并且在每个页面都得通过一个变量来控制它的显隐,这显然不是我们想要的。。。那我们想要的是什么样呢?用过一些 UI 框架的同学们应该知道有这样一种用法:this.$toast({ duration: 3000, content: '这是一条消息提示'});复制代码没错,就是这么简单的一句话就万事大吉了(就是用 js 调用组件而已啦)。那这种效果究竟是怎么实现的呢?今天就让我们来(手把手 )一探究竟吧!前置知识不知道小伙伴们有没有用过 Vue.extend() 这个东东,反正我是很少碰过,印象不深,所以这里我们先来短暂了解一下 Vue.extend() 主要是用来干嘛的。先来个官方说明(不多的,坚持下):  没怎么看懂?没关系,不重要,你只要记住(加少许理解)以下用法即可:// 导入以往的普通组件import Main from './main.vue';// 用 Vue.extend 创建组件的模板(构造函数)let mainConstructor = Vue.extend(Main);// 实例化组件let instance = new mainConstructor();// 挂载到相应的元素上instance.$mount('#app');复制代码不知道你看懂没有,上面的 Vue.extend(Main) 就是一个基于 main.vue 的组件模板(构造函数),instance 是实例化的组件,$mount() 是手动挂载的意思。其中 Vue.extend() 和 $mount() 就是我们通过 js 调用、渲染并挂载组件的精髓所在,相当于早前的 createElement 和 appendChild,有异曲同工之效。这个点需要我们好好熟悉一下,所以你可以先停下来屡屡思路。补充一下:$mount() 里面如果没有参数,说明组件只是渲染了但还没有挂载到页面上,如果有正确的(元素)参数则直接挂载到元素下面。写一个 toast 组件js 调用归调用,最原始的组件还是要有的,只是我们不通过 import 来引入到页面中而已。ok,我们就以最开始的那个 toast 图片来简单写一下这个 vue 组件(message 和 alert 也是一样的)。这里就直接上代码啦,毕竟它的结构简单到爆了,也不是本章节的重点:<!-- main.vue --><template> <div class="toast"> <p>服务器错误,请稍后重试</p> </div></template><script>export default { name: "Toast", mounted() { setTimeout(() => { // 3s 后通过父级移除子元素的方式来移除该组件实例和 DOM 节点 this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, 3000); }};</script><style lang="scss" scoped>.toast { display: flex; align-items: center; justify-content: center; position: fixed; top: 0; bottom: 0; left: 0; right: 0; color: #fff; z-index: 9999; background: transparent; > p { padding: 12px 22px; font-size: 18px; border-radius: 4px; background: rgba(17, 17, 17, 0.7); }}</style>复制代码上面的内容想必大家应该都能看懂,所以这里就直接讲下面的重点了。写一个 main.js我们在 main.vue 的同级目录下新建一个 main.js 文件。我们先瞟一眼文件内容(也不多,已经是个最简版了):// main.jsimport Vue from "vue"; // 引入 Vue 是因为要用到 Vue.extend() 这个方法import Main from "./main.vue"; // 引入刚才的 toast 组件let ToastConstructor = Vue.extend(Main); // 这个在前面的前置知识内容里面有讲到let instance;const Toast = function() { instance = new ToastConstructor().$mount(); // 渲染组件 document.body.appendChild(instance.$el); // 挂载到 body 下};export default Toast;复制代码上面的代码暴露了一个 Toast 函数。为什么要暴露一个函数呢?原因很简单:你想想,我们最终是不是要根据 this.$toast() 来调用一个组件,说白了,通过 js 调用,本质就是调用一个 函数。也就是说 this.$toast() 就是执行了上面代码中导出的 export default Toast,也就是执行了 Toast 函数(const Toast = function() {}),所以当我们调用 this.$toast() 的时候其实就是执行了 Toast() 函数。而 Toast() 函数只做了一件事情:就是通过手动挂载的方式把组件挂载到 body 下面。补充一下:一般来说我们常见的是 $mount("#app"),也就是把组件挂载到 #app 下面,<router-view /> 也包含在 #app 中,但是我们这种 toast 提示是放在 body 下面的,也就是说它不受 #app 和 <router-view /> 的管控,所以当我们切换页面(路由)的时候,这个 toast 组件是不会跟着立马消失的,这点要注意哦。这里顺便给个组件的目录结构开始调用调用方式很简单,首先我们在入口文件 main.js(和上面不是同一个) 里加上两行代码,这样我们就能在需要的地方直接用 js 调用它了,如下图所示:  然后在页面中测试一下,就像下面这样子:  运行一下代码:  嗯,挺好,小有成就的 feel 。支持可传参数别急,我们好像还漏了点什么。。。对了,现在还不支持传参呢,直接调用 this.$toast() 就只能显示————服务器错误,请稍后重试(这下全都是后端的锅了)。但我们可是个有追求的前端,不能局限于此,所以现在让我们来尝试增加下两个可配置参数,这里拿 duration 和 content 举个栗子。首先我们要修改 main.vue 组件里面的内容(其实没啥大变化),就像下面这样:<!-- main.vue 可配置版 --><template> <div class="toast"> <p>{{ content }}</p> </div></template><script>// 主要就改了 dataexport default { name: "Toast", data() { return { content: "", duration: 3000 }; }, mounted() { setTimeout(() => { this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, this.duration); }};</script>复制代码上面的代码应该算是浅显易懂了,接下来我们看下 main.js 里面改了啥:// main.js 可配置版import Vue from "vue";import Main from "./main.vue";let ToastConstructor = Vue.extend(Main);let instance;const Toast = function(options = {}) { // 就改了这里,加了个 options 参数 instance = new ToastConstructor({ data: options // 这里的 data 会传到 main.vue 组件中的 data 中,当然也可以写在 props 里 }); document.body.appendChild(instance.$mount().$el);};export default Toast;复制代码其实 main.js 也没多大变化,就是在函数里面加了个参数。要注意的是 new ToastConstructor({ data: options }) 中的 data 就是 main.vue 组件中的 data,不是随随便便取的字段名,传入的 options 会和组件中的 data 合并(Vue 的功劳)。em。。。是的,就这么简单,现在让我们继续来调用一下它:<script>export default { methods: { showToast() { this.$toast({ content: "哈哈哈哈,消失的贼快", duration: 500 }); } }};</script>复制代码运行一下就可以看到:  当然,这还没完,我们继续添加个小功能点。。支持 this.$toast.error()这里我们打算支持 this.$toast.error() 和 this.$toast.success() 这两种方式,所以我们第一步还是要先去修改一下 main.vue 文件的内容(主要就是根据 type 值来修改组件的样式),就像下面这样:<!--main.vue--><template> <div class="toast" :class="type ? `toast--${type}` : ''"> <p>{{ content }}</p> </div></template><script>export default { ... data() { return { type: "", content: "", duration: 3000 }; }, ...};</script><style lang="scss" scoped>.toast { ... &--error p { background: rgba(255, 0, 0, 0.5); } &--success p { background: rgba(0, 255, 0, 0.5); }}</style>复制代码其次,this.$toast.error() 其实就等价于 Toast.error(),所以我们现在的目的就是要给 Toast 函数扩充方法,也比较简单,就先看代码再解释吧:// main.jsconst Toast = function(options = {}) { ...};// 以下就是在 Toast 函数中拓展 ["success", "error"] 这两个方法["success", "error"].forEach(type => { Toast[type] = options => { options.type = type; return Toast(options); };});export default Toast;复制代码我们可以看到 Toast.error() 和 Toast.success() 最终还是调用 Toast(options) 这个函数,只不过在调用之前需要多做一步处理,就是将 ["success", "error"] 作为一个 type 参数给合并进 options 里面再传递,仅此而已。那就试试效果吧:<script>export default { methods: { showToast() { this.$toast({ content: "这是正常的" }); }, showErrorToast() { this.$toast.error({ content: "竟然失败了" }); }, showSuccessToast() { this.$toast.success({ content: "居然成功了" }); } }};</script>复制代码 大赞无疆,大。赞。。无。。。疆。。。。。结语至此,一个通过 js 调用的简单 toast 组件就搞定啦,短短的几行代码还是挺考验 js 功底的。当然这只是个超简易版的 demo,显然不够完善和健壮,所以我们可以在此基础上扩充一下,比如当 duration <= 0 的时候,我们让这个 toast 一直显示,然后扩展一个 close 方法来关闭等等之类的。不过还是那句老话,实践才是检验真理的唯一标准。纸上得来终觉浅,绝知此事要躬行。step by step, day day up !作者:尤水就下链接:https://juejin.cn/post/6844903825711562766
  • [技术干货] JS 判断数组是否包含某个元素方法集合
    1、JavaScript indexOf() 方法 定义和用法 indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置。 语法 stringObject.indexOf(searchvalue,fromindex) 参数    描述 searchvalue    必需。规定需检索的字符串值。 fromindex    可选的整数参数。规定在字符串中开始检索的位置。它的合法取值是 0 到 stringObject.length - 1。如省略该参数,则将从字符串的首字符开始检索。 说明 该方法将从头到尾地检索字符串 stringObject,看它是否含有子串 searchvalue。开始检索的位置在字符串的 fromindex 处或字符串的开头(没有指定 fromindex 时)。如果找到一个 searchvalue,则返回 searchvalue 的第一次出现的位置。stringObject 中的字符位置是从 0 开始的。 提示和注释 注释:indexOf() 方法对大小写敏感! 注释:如果要检索的字符串值没有出现,则该方法返回 -1。 2、jQuery.inArray()方法 定义和用法 $.inArray() 函数用于在数组中查找指定值,并返回它的索引值(如果没有找到,则返回-1) 提示:源数组不会受到影响,过滤结果只反映在返回的结果数组中。 语法 $.inArray( value, array [, fromIndex ] ) 参数    描述 value    任意类型 用于查找的值。 array    Array类型 指定被查找的数组。 fromIndex    可选。Number类型 指定从数组的指定索引位置开始查找,默认为 0 3、JavaScript find() 方法 定义和用法 find() 方法返回通过测试(函数内判断)的数组的第一个元素的值。 find() 方法为数组中的每个元素都调用一次函数执行:  当数组中的元素在测试条件时返回 true 时, find() 返回符合条件的元素,之后的值不会再调用执行函数。 如果没有符合条件的元素返回 undefined  注意:find() 对于空数组,函数是不会执行的。 注意:find() 并没有改变数组的原始值。 注意:JavaScript 版本为ECMAScript 6 用法  array.find(function(currentValue, index, arr),thisValue) 参数    描述 function    必需。数组每个元素需要执行的函数。 currentValue    必需。当前元素 函数参数 index    可选。当前元素的索引值函数参数 arr    可选。当前元素所属的数组对象函数参数 thisValue    可选。 传递给函数的值一般用 “this” 值。如果这个参数为空, “undefined” 会传递给 “this” 值 返回值 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined。 4、JavaScript findIndex() 方法 定义和用法 findIndex() 方法返回传入一个测试条件(函数)符合条件的数组第一个元素位置。 findIndex() 方法为数组中的每个元素都调用一次函数执行:  当数组中的元素在测试条件时返回 true 时, findIndex() 返回符合条件的元素的索引位置,之后的值不会再调用执行函数。 如果没有符合条件的元素返回 -1  注意:find() 对于空数组,函数是不会执行的。 注意:find() 并没有改变数组的原始值。 注意:JavaScript 版本为ECMAScript 6 用法 array.findIndex(function(currentValue, index, arr), thisValue) 参数    描述 function    必需。数组每个元素需要执行的函数。 currentValue    必需。当前元素 函数参数 index    可选。当前元素的索引值函数参数 arr    可选。当前元素所属的数组对象函数参数 thisValue    可选。 传递给函数的值一般用 “this” 值。如果这个参数为空, “undefined” 会传递给 “this” 值 返回值 返回符合测试条件的第一个数组元素索引,如果没有符合条件的则返回 -1。 5、for 循环和 if 判断var arr = [1, 5, 10, 15];/* 传统for */for(let i=0; i<arr.length; i++) { if(arr[i] === 查找值) { //则包含该元素 }}/* for...of */for(v of arr) { if(v === 查找值) { //则包含该元素 }}/* forEach */arr.forEach(v=>{ if(v === 查找值) { //则包含该元素 }})———————————————— 原文链接:https://blog.csdn.net/github_38336924/article/details/83089873 
  • 使用canvas制作炫彩背景【转】
    1.定义canvas标签:1<canvas id="canvas"></canvas>2.开始js部分,先定义变量:123456789101112131415161718/* 获取画布 */var canvas = document.querySelector("#canvas");var ctx = canvas.getContext('2d');/* 定义一个字符串数组,后面字符串会从里随机选值 */var text = "SAFAF1D56FLK43F7PHM76VC9XNJ23";/* 定义 w为窗口宽度,h为窗体高度 */var w=window.innerWidth;var h=window.innerHeight;/* 设置len为20,其为背景里每条字符串的长度 */var len = 20;/* 设置num为100,窗口一共显示100条字符串 */var num = 100;/* 定义数组,里面存取每条字符串的字符与位置 */var arr=[];/* 画布宽等于窗口宽 */canvas.width=window.innerWidth;/* 画布高等于窗口高 */canvas.height=window.innerHeight;3.初始化字符串数组,先给每条字符串位置,字符先不给:123456789101112/* 初始化字符串数组 */        for(let i=0;i<num;i++){            /* 用.push方法给arr数组添加值 */            arr.push({                /* 字符先为空 */                str:[],                /* x轴位置为窗口宽度乘上一个0带1的随机数 */                x: parseInt(w*Math.random()),                /* y轴位置为窗口高度乘上一个0带1的随机数,再减个150把,可以为负数 */                y: parseInt(h*Math.random()-150)            })        }4.绘制每条字符串:123456789101112131415161718192021222324252627282930/* 绘制每条字符串 */       function txt(){           /* 给个循环,共绘制num条 */           for(let i=0;i<num;i++){               /* 设置变量letter为当前arr数组里的第i条字符串 */               var letter = arr[i];               /* 让字符串的字符为空 */               letter.str = [];               /* 给个循环,一个字符一个字符的拼接成字符串 */               for(let k=0;k<len;k++){                   /* 随机选取text里的一个字符 */                   letter.str.push(text[Math.floor(Math.random() * text.length)]);               }               /* 再来循环,开始绘制渲染字符串的每个字符 */               for(let j=0;j<len;j++){                   if(j==len-1){                       /* 第一个字符为白色 */                       ctx.fillStyle = `rgb(255, 255, 255)`;                   }else{                       /* 后面的为绿色,慢慢变不透明 */                       ctx.fillStyle = `rgba(0, 255, 21,${j*0.15})`;                   }                        /* 渲染字符 */                    ctx.font = "20px FangSong";                    ctx.fillText(letter.str[j],letter.x,letter.y+j*15);               }           }           /* 调用更新 */           move();       }5.更新字符串:12345678910111213141516/* 让字符串移动,若某字符串出了可视区,则重新生成 */       function move(){           /* 来个循环,给全部字符串更新位置 */           for(let j=0;j<num;j++){               /* y轴位置加3 */               arr[j].y=arr[j].y+3;               /* 如果改字符已经走出窗口了重新赋值 */               if(arr[j].y>=h){                   arr[j]={                       str:[],                x: parseInt(w*Math.random()),                y: parseInt(h*Math.random()-150)                   }               }           }       }6.设置动画过程:12345678setInterval(function(){            /* 清屏 */           ctx.clearRect(0,0,w,h);           /* 渲染 */           move();           /* 更新 */           txt();        },50); 7.在窗口大小改变时,设置canvas画布能实时铺满屏幕:123456789101112131415/* 绑定窗口大小发生改变事件,重新绘制字符串数组,让canvas随时铺满浏览器可视区 */        window.onresize=resizeCanvas;        function resizeCanvas(){            w=canvas.width=window.innerWidth;            h=canvas.height=window.innerHeight;            /* 重新给全部字符串赋值 */            for(let j=0;j<num;j++){                    arr[j]={                        str:[],                 x: parseInt(w*Math.random()),                 y: parseInt(h*Math.random()-150)                    }                }            }               resizeCanvas();转载自https://www.jb51.net/article/279297.htm
  • [技术干货] 两个函数相互调用如何防止死循环【转】
    最近碰到了一个问题,就是两个函数相互调用遭遇死循环的问题,想了半天终于想出了一个算法破解,姑且叫它熵递减算法。问题的抽象代码如下1234567891011/**  methodA 和 methodB 循环调用,是死循环* */function methodA() {    console.log('A的事情');    methodB();}function methodB() {    console.log('B的事情');    methodA();}不论调用哪个方法,都会产生死循环。我想要的效果是:如果触发A方法时,也执行一下B方法,到此为止不再循环下去,反之亦然。因此,必须能判断方法是主动发起的,还是被动的。抽象代码如下:12345678910111213141516171819/**  解决问题的关键在于,判断方法是主动发起的,还是被动的* */function methodA() {    console.log('A的事情');    if('A是主动的'){        methodB();    }else{        // 不再调用下去    }}function methodB() {    console.log('B的事情');    if('B是主动的'){        methodA();    }else{        // 不再调用下去    }}解法,就是熵递减算法,如下1234567891011121314151617181920212223/** 熵递减算法* */var pairMethodStep = 2;function methodA() {    pairMethodStep --;    console.log('A的事情');    if(pairMethodStep === 1){        methodB();    }else{        pairMethodStep = 2;    }}function methodB() {    pairMethodStep --;    console.log('B的事情');    if(pairMethodStep === 1){        methodA();    }else{        pairMethodStep = 2;    }}熵递减算法的说明:给一个全局变量,叫做总步数pairMethodStep ,初始值为2。任一个方法执行时,做完自己的事情后,把pairMethodStep减成1。然后,判断此时的pairMethodStep,如果是1, 就调用另一个方法;如果是0了,就不再继续调用了,而是把pairMethodStep恢复成2。我们分析一下代码执行的过程。主动的方法执行前,pairMethodStep的值是2,它做完自己的事后,把pairMethodStep的值变成了1,紧跟着就会执行被动的方法;被动的方法执行前,pairMethodStep的值是1,被动的方法做完自己的事情后,把pairMethodStep的值减成了0,不会再调用另一个方法了(不会发生死循环了),而仅仅是把pairMethodStep还原成2。目的达到。熵递减算法,能完美地解决两个函数相互调用的问题。js函数互相调用碰到的问题项目场景两个函数互相调用时(当一个系统比较大时,尤其是涉及到一些复杂的算法时,很有可能会碰到死循环的情况发生,造成系统的CPU飙升)12345678        function a1() {            console.log("a1");            b1();        }        function b1() {            console.log("b1");            a1();        }问题描述会进入死循环原因分析就类似for循环,或者递归函数如果没有退出条件就会一直执行解决方案1234567891011121314151617181920        let flagNum = 1;          function a1() {            flagNum--;            console.log("a1");            if (flagNum === 0) {                b1();            } else {                flagNum = 1;            }        }        function b1() {            flagNum--;            console.log("b1");            if (flagNum === 0) {                a1();            } else {                flagNum = 1;            }        }总结以上为个人经验,希望能给大家一个参考转载自https://www.jb51.net/article/279298.htm
  • [问题求助] 预约会议后的回调函数缺少对应的返回参数
    返回的对象里,对应param属性里缺少confBaseInfo属性,这个属性里会有对应的会议号等信息
  • [技术干货] 基于Java+SpringBoot+vue+element驾校管理系统设计和实现-转载
     一、前言介绍:         随着社会的发展和科学技术的进步,互联网技术越来越受欢迎。网络传播的生活方式逐渐受到广大人民群众的喜爱。越来越多的互联网爱好者开始在互联网上满足他们的基本需求,同时逐渐进入各个用户的生活起居。互联网具有许多优点,例如便利性,速度,高效率和低成本。因此,类似于驾校管理,满足用户工作繁忙的需求,不仅是方便用户随时查看信息的途径,而且还能提高管理效率。本文首先以驾校管理过程的基本问题作为研究对象。在开发系统之前,我们对现有状况进行了详细的调查和分析。最后,我们利用计算机技术开发了一套完整合适的驾校管理。该系统的实现主要优势是:该系统主要采用计算机技术开发,它方便快捷;系统可以通过管理员界面查看系统所涉及的驾校管理所有信息管理。          驾校管理软件是一款方便、快捷、实用的信息服务查询软件。随着智能网络在全球市场的不断普及以及各种智能平台的使用,作为中国主流智能的技术开发系统,自然需要这样的软件来满足更多用户的需求和体验。系统的开发与人们的日常需求相关,如通过管理系统获取到个人中心、学员管理、驾校教练管理、预约教练管理、预约教练管理、评价教练管理、考试成绩管理、考试通知管理、报考信息管理、练车通知管理、预约练车管理、驾考套餐管理、报名信息管理、车辆信息管理等信息详细情况,了解最新资讯信息等。  二、主要技术:  2.1  Spring Boot框架介绍:         Spring框架是Java平台上的一种开源应用框架,提供具有控制反转特性的容器。尽管Spring框架自身对编程模型没有限制,但其在Java应用中的频繁使用让它备受青睐,以至于后来让它作为EJB(EnterpriseJavaBeans)模型的补充,甚至是替补。Spring框架为开发提供了一系列的解决方案,比如利用控制反转的核心特性,并通过依赖注入实现控制反转来实现管理对象生命周期容器化,利用面向切面编程进行声明式的事务管理,整合多种持久化技术管理数据访问,提供大量优秀的Web框架方便开发等等。Spring框架具有控制反转(IOC)特性,IOC旨在方便项目维护和测试,它提供了一种通过Java的反射机制对Java对象进行统一的配置和管理的方法。Spring框架利用容器管理对象的生命周期,容器可以通过扫描XML文件或类上特定Java注解来配置对象,开发者可以通过依赖查找或依赖注入来获得对象。Spring框架具有面向切面编程(AOP)框架,SpringAOP框架基于代理模式,同时运行时可配置;AOP框架主要针对模块之间的交叉关注点进行模块化。Spring框架的AOP框架仅提供基本的AOP特性,虽无法与AspectJ框架相比,但通过与AspectJ的集成,也可以满足基本需求。Spring框架下的事务管理、远程访问等功能均可以通过使用SpringAOP技术实现。Spring的事务管理框架为Java平台带来了一种抽象机制,使本地和全局事务以及嵌套事务能够与保存点一起工作,并且几乎可以在Java平台的任何环境中工作。  2.2 MYSQL数据库:                 数据库是系统开发过程中不可或缺的一部分。 在WEB应用方面,MySQL AB开发了一个具有很大优势的MySQL关系数据库管理系统。 MySQL可以将数据存储在不同的表中,这非常灵活,并且还可以提高系统在实际应用中的速度。 数据库访问最常用于标准SQL语言,MySQL用于SQL语言,因此它具有高度兼容性。数据库的操作是必不可少的,包括对数据库表的增加、删除、修改、查询等功能。现如今,数据库可以分为关系型数据库和非关系型数据库,Mysql属于关系性数据库,Mysql数据库是一款小型的关系型数据库,它以其自身特点:体积小、速度快、成本低等,Mysql数据库是目前最受欢迎的开源数据库。          在WEB应用技术中, Mysql数据库支持不同的操作系统平台,虽然在不同平台下的安装和配置都不相同,但是差别也不是很大,Mysql在Windows平台下两种安装方式,二进制版和免安装版。安装完Mysql数据库之后,需要启动服务进程,相应的客户端就可以连接数据库,客户端可通过命令行或者图形界面工具登录数据库。  三、系统设计: 3.1 系统架构设计: ​  3.2 登录时序图设计: ​  四、功能截图:  4.1 登录注册:  4.2 前端页面: 4.2.1 系统首页:   4.2.2 教练模块:   4.2.3 考试须知:   4.2.4 预约练车:   4.2.5 车辆信息:  4.2.6 系统公告信息:  4.2.7 个人中心模块:   4.2.8 用户后台模块:  4.3 后端管理:  4.3.1 教练管理:  4.3.2 考试通知:  4.3.3 报名管理:  4.3.4 车辆信息:  4.3.5 系统公告等  五、代码实现:     /**  * 登录相关  */ @RequestMapping("users") @RestController public class UserController{          @Autowired     private UserService userService;          @Autowired     private TokenService tokenService;       /**      * 登录      */     @IgnoreAuth     @PostMapping(value = "/login")     public R login(String username, String password, String captcha, HttpServletRequest request) {         UserEntity user = userService.selectOne(new EntityWrapper<UserEntity>().eq("username", username));         if(user==null || !user.getPassword().equals(password)) {             return R.error("账号或密码不正确");         }         String token = tokenService.generateToken(user.getId(),username, "users", user.getRole());         return R.ok().put("token", token);     }          /**      * 注册      */     @IgnoreAuth     @PostMapping(value = "/register")     public R register(@RequestBody UserEntity user){ //        ValidatorUtils.validateEntity(user);         if(userService.selectOne(new EntityWrapper<UserEntity>().eq("username", user.getUsername())) !=null) {             return R.error("用户已存在");         }         userService.insert(user);         return R.ok();     }       /**      * 退出      */     @GetMapping(value = "logout")     public R logout(HttpServletRequest request) {         request.getSession().invalidate();         return R.ok("退出成功");     }          /**      * 密码重置      */     @IgnoreAuth     @RequestMapping(value = "/resetPass")     public R resetPass(String username, HttpServletRequest request){         UserEntity user = userService.selectOne(new EntityWrapper<UserEntity>().eq("username", username));         if(user==null) {             return R.error("账号不存在");         }         user.setPassword("123456");         userService.update(user,null);         return R.ok("密码已重置为:123456");     }          /**      * 列表      */     @RequestMapping("/page")     public R page(@RequestParam Map<String, Object> params,UserEntity user){         EntityWrapper<UserEntity> ew = new EntityWrapper<UserEntity>();         PageUtils page = userService.queryPage(params, MPUtil.sort(MPUtil.between(MPUtil.allLike(ew, user), params), params));         return R.ok().put("data", page);     }       /**      * 列表      */     @RequestMapping("/list")     public R list( UserEntity user){            EntityWrapper<UserEntity> ew = new EntityWrapper<UserEntity>();           ew.allEq(MPUtil.allEQMapPre( user, "user"));          return R.ok().put("data", userService.selectListView(ew));     }       /**      * 信息      */     @RequestMapping("/info/{id}")     public R info(@PathVariable("id") String id){         UserEntity user = userService.selectById(id);         return R.ok().put("data", user);     }          /**      * 获取用户的session用户信息      */     @RequestMapping("/session")     public R getCurrUser(HttpServletRequest request){         Long id = (Long)request.getSession().getAttribute("userId");         UserEntity user = userService.selectById(id);         return R.ok().put("data", user);     }       /**      * 保存      */     @PostMapping("/save")     public R save(@RequestBody UserEntity user){ //        ValidatorUtils.validateEntity(user);         if(userService.selectOne(new EntityWrapper<UserEntity>().eq("username", user.getUsername())) !=null) {             return R.error("用户已存在");         }         userService.insert(user);         return R.ok();     }       /**      * 修改      */     @RequestMapping("/update")     public R update(@RequestBody UserEntity user){ //        ValidatorUtils.validateEntity(user);         UserEntity u = userService.selectOne(new EntityWrapper<UserEntity>().eq("username", user.getUsername()));         if(u!=null && u.getId()!=user.getId() && u.getUsername().equals(user.getUsername())) {             return R.error("用户名已存在。");         }         userService.updateById(user);//全部更新         return R.ok();     }       /**      * 删除      */     @RequestMapping("/delete")     public R delete(@RequestBody Long[] ids){         userService.deleteBatchIds(Arrays.asList(ids));         return R.ok();     } }      @Configuration public class InterceptorConfig extends WebMvcConfigurationSupport{          @Bean     public AuthorizationInterceptor getAuthorizationInterceptor() {         return new AuthorizationInterceptor();     }          @Override     public void addInterceptors(InterceptorRegistry registry) {         registry.addInterceptor(getAuthorizationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");         super.addInterceptors(registry);     }          /**      * springboot 2.0配置WebMvcConfigurationSupport之后,会导致默认配置被覆盖,要访问静态资源需要重写addResourceHandlers方法      */     @Override     public void addResourceHandlers(ResourceHandlerRegistry registry) {         registry.addResourceHandler("/**")         .addResourceLocations("classpath:/resources/")         .addResourceLocations("classpath:/static/")         .addResourceLocations("classpath:/admin/")         .addResourceLocations("classpath:/front/")           .addResourceLocations("classpath:/public/");         registry.addResourceHandler("/upload/**").addResourceLocations("file:D:/work/");           super.addResourceHandlers(registry);     } }  ———————————————— 版权声明:本文为CSDN博主「java李杨勇」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/weixin_39709134/article/details/128781905 
  • [技术干货] Object.assign详解
    一、Object.assign是什么?       首先了解下Object.assign()是什么。我们先看看ES6官方文档是怎么介绍的?       Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。       简单来说,就是Object.assign()是对象的静态方法,可以用来复制对象的可枚举属性到目标对象,利用这个特性可以实现对象属性的合并。 二、用法:       Object.assign(target, ...sources)   参数:target--->目标对象       source--->源对象   返回值:target,即目标对象 三、使用示例: 1、目标对象和源对象无重名属性 var target={name:'guxin',age:18}; var source={state:'single'} var result=Object.assign(target,source); console.log(target,target==result); 我们可以看到source上的state属性合并到了target对象上。如果只是想将两个或多个对象的属性合并到一起,不改变原有对象的属性,可以用一个空的对象作为target对象。像下面这样: var result=Object.assign({},target,source); 2、目标对象和源对象有重名属性 上面的示例目标对象和源对象是没有重名属性的,那么如果他们有重名属性又会怎样呢?是后面的属性覆盖前面的还是前面的属性覆盖后面的呢?我们接下来看下一个例子: var target={name:'guxin',age:18} var source={state:'signle',age:22} var result=Object.assign(target,source) console.log(target)  可以看到如果有同名属性的话,后面的属性值会覆盖前面的属性值。 3、有多个源对象 前面的示例都是只有一个源对象,那么如果有多个源对象情况会不会不同呢?我们继续看下面的例子:         var target={name:'guxin',age:18}         var source1={state:'signle',age:22}         var source2={mood:'happy',age:25}         var result=Object.assign(target,source1,source2)         console.log(target) 可以看到有多个源对象情况也是和一个源对象一样的。没有同名的属性会直接复制到目标对象上,同名的属性后面的属性值会覆盖前面的同名属性值。  四、注意事项: 1、Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象,继承属性和不可枚举属性是不能拷贝的。 2、针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。 3、目标对象自身也会改变 4、异常会打断后续拷贝任务  五、兼容性 目前IE浏览器不兼容Object.assign(),如果需要兼容IE的话最好不要直接使用这个方法。  六、与$.extend()的比较 我们通过一个简单的示例来比较两者有什么不同,          var target={name:'guxin',age:18}         var source1={state:'signle',age:22}         var source2={mood:'happy',age:25}         var result=Object.assign(target,source1,source2)         console.log(target,'assign')         var targetObj={name:'guxin',age:18}         var sourceObj1={state:'signle',age:22}         var sourceObj2={mood:'happy',age:25}         var result=$.extend(targetObj,sourceObj1,sourceObj2)         console.log(targetObj,'extend') 可以看到两者得到的结果是一样的。所以,我认为这两个方法,除了兼容性应该是一样的。 ———————————————— 原文链接:https://blog.csdn.net/guxin_duyin/article/details/88916106 
  • [技术干货] javascript中将xml转为json的方法
    这篇文章将为大家详细讲解有关javascript中将xml转为json的方法,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。javascript中将xml转换为json字符串的方法:首先通过XML字符串或请求XML文件来获取XML的DOM对象;然后通过遍历和递归来获取子元素的nodeValue值;最后拼接出JSON字符串即可。利用JavaScript将XML转换为JSON首先通过XML字符串来生成XML的DOM对象:/**  * 通过传入xml的内容字符串来解析xml  * @param xmlString xml字符串  * @returns xml的Document对象  */ function getXmlDocumentByXmlString(xmlString) {     var xmlDoc = null;     if (window.DOMParser) {         var parser = new DOMParser();         xmlDoc = parser.parseFromString(xmlString, "text/xml");     } else {         //IE         xmlDoc = new ActiveXObject("Microsoft.XMLDOM");         xmlDoc.async = "false";         xmlDoc.loadXML(xmlString);     }     return xmlDoc; }或者通过请求XML文件来获取XML的DOM对象:/**  * 通过传入xml文件路径来解析xml文档  * @param xmlFilePath xml文档路径,如:files/test.xml  * @returns xml的Document对象  */ function getXmlDocumentByFilePath(xmlFilePath) {     //xmlDocument对象     var xmlDoc = null;     //xmlhttp对象     var xmlhttp = null;     if (window.XMLHttpRequest) {         //IE7+, FireFox, Chrome, Opera, Safari         xmlhttp = new XMLHttpRequest();     } else {         //IE6, IE5         xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");     }     xmlhttp.open("GET", xmlFilePath, false);     xmlhttp.send();     xmlDoc = xmlhttp.responseXML;     return xmlDoc; }接下来就是重点的部分了,通过遍历和递归获取子元素的nodeValue,来拼接出JSON字符串,实现将XML转换成JSON字符串:/**  * 将XML的Document对象转换为JSON字符串  * @param xmlDoc xml的Document对象  * @return string  */ function convertToJSON(xmlDoc) {     //准备JSON字符串和缓存(提升性能)     var jsonStr = "";     var buffer = new Array();     buffer.push("{");     //获取xml文档的所有子节点     var nodeList = xmlDoc.childNodes;     generate(nodeList);     /**      * 中间函数,用于递归解析xml文档对象,并附加到json字符串中      * @param node_list xml文档的的nodeList      */     function generate(node_list) {         for (var i = 0; i < node_list.length; i++) {             var curr_node = node_list[i];             //忽略子节点中的换行和空格             if (curr_node.nodeType == 3) {                 continue;             }             //如果子节点还包括子节点,则继续进行遍历             if (curr_node.childNodes.length > 1) {                 buffer.push("\"" + curr_node.nodeName + "\": {");                 generate(curr_node.childNodes);             } else {                 var firstChild = curr_node.childNodes[0];                 if (firstChild != null) {                     //nodeValue不为null                     buffer.push("\"" + curr_node.nodeName + "\":\"" + firstChild.nodeValue + "\"");                 } else {                     //nodeValue为null                     buffer.push("\"" + curr_node.nodeName + "\":\"\"");                 }             }             if (i < (node_list.length - 2)) {                 buffer.push(",");             } else {                 break;             }         }         //添加末尾的"}"         buffer.push("}");     }     jsonStr = buffer.join("");     return jsonStr; }使用方式:通过getXmLDocumentByFilePath(xmlFilePath)或者getXmlDocumentByXmlString(xmlString)获取XML的Document对象,然后通过调用convertToJSON(xmlDocument)传入xml的Ducument对象即可得到转换后的JSON字符串。适用范围:不含有attribute的任意XML文档。关于“javascript中将xml转为json的方法”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。原文链接:https://m.yisu.com/zixun/446969.html
  • [技术干货] vue中async和await异步编程
    async&await是Promise的语法糖,使用他的目的就是用同步的写法,写异步的操作。async 是“异步”的简写, async 用于申明一个异步的 function,await 可以认为是 async wait 的简写,await 用于等待一个异步方法执行完成。当我们函数中需要返回值是promise对象时通常用async和await简化。          async作为一个关键字放到函数之前,表示函数是异步的函数,异步函数也就意味着该函数的执行不会阻塞后面代码的执行,等async 函数返回一个promise 对象。  1.async async可以作用在任何方法前, 返回值是一个Promise对象  async function a(){  } console.log(a());   var b = (async () => {       }) console.log(b()); async函数内部return的返回值, 会成为then回调函数的参数  async function a() {     return 123 } a().then(res=>{     console.log(res); }) async作用的方法,如果内部出现报错,可以被promise的catch方法捕获  async function a() {     console.log(a);     let a = 123; } a().catch(err=>{     console.log(err); }) 2. await await只能作用在async修饰的方法中,不能单独使用,如果使用报错、  function a(){     await //报错:await is not defined     console.log(123); } a() 正常情况 await后面跟着一个Promise对象,返回的是Promise对象的成功后结果  async function a(){     var b = await new Promise((resolve,reject)=>{         resolve('123')     })     return b } a().then(res=>{     console.log(res); }) ———————————————— 原文链接:https://blog.csdn.net/qq_44858608/article/details/124332167 
  • [技术干货] async和await用法介绍
     一;async 1.函数前面加上 async 关键字,则该函数会返回一个结果为 promise 的对象。 2. async 函数返回 promise 对象的状态。 2.1:如果返回的是一个非 Promise 类型的数据, 则async 函数返回 promise 的状态 为 fulfilled 成功。 2.2:如果返回的是一个 Promise对象,则 async 函数返回 promise 的状态由返回的Promise对象的状态决定。 2.3:如果 throw Errow 抛出异常,则 async 函数返回 promise 的状态为 rejected 失败。 二;await 1.await 右侧的表达式一般为 promise 对象。 2.await 是在等一个表达式的结果,等一个返回值(回调成功的内容) 3.如果 await 右侧是一个 promise 对象,则 await 返回的是 promise 成功的值。 注意: 1.await 必须写在 async 函数中,但 async 函数中可以没有 await。 2.如果 await 的 promise 失败了,就会抛出异常,该异常需要通过 try catch 捕获处理。 3.如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。  三;async 和 await 结合发送 ajax   1.封装一个 发送GET请求的 promise  function sendAjax(url) {             return new Promise((resolve, reject) => {                 const xhr = new XMLHttpRequest()                 xhr.open('GET', url)                 xhr.send()                 // 处理返回结果                 xhr.onreadystatechange = function () {                     if (xhr.readyState == 4) {                         // 判断状态码                         if (xhr.readyState >= 200 && xhr.readyState < 300) {                             resolve(xhr.response)                         }else {                             reject(xhr.status)                         }                     }                 }             })         } 2.async和await结合发送ajax请求  // 触发点击事件发送ajax请求         let btn = document.querySelector('#btn')         btn.addEventListener('click', async function () {             try {                 let res = await sendAjax('get请求地址')                 console.log(res);             } catch (error) {                 console.log(error);             }         }) ———————————————— 原文链接:https://blog.csdn.net/Allurewuhui/article/details/123434870 
总条数:115 到第 页
上滑加载中