• [技术干货] Babel解决ES6不能被所有浏览器解析问题【转】
    一、简介1.ES6的某些高级语法在浏览器环境甚至是Node.js环境中无法执行。2.Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行执行。二、安装BabelBabel提供babel-cli工具,用于命令行转码安装:npm i -g babel-cli检查安装:babel --version三、Babel的使用1)安装Node.js环境:cid:link_02)进入项目,初始化项目:npm init -y3)创建文件 src/example.js4)安装转码器,在项目中安装:npm i --save-dev babel-preset-env 或 npm i --save-dev babel-preset-es20155)创建文件并配置:.babelrc {"presets":["env","es2015"],"plugins":[]}注:Babel的配置文件是.babelrc,存放在项目的根目录下,该文件用来设置转码规则和插件。四、文件转化文件:babel src/index.js -o dist/index.js注:-o(--out-file):输出的意思文件夹:babel src -d dist注:-d(--out-dir):指定输入目录实时监控:babel src -w -d dist注:-w:watch监控五、自定义脚本1)改写package.json{ // ... "scripts":{ // ... "build": "babel src\\index.js -o dist\\index.js" },}2)转码时,执行下面命令mkdir distnpm run build  转载自https://www.cnblogs.com/yulingzhiling/p/15844424.html
  • [技术干货] Vue中的$nextTick有什么作用?【转】
    一、NextTick是什么官方对其的定义在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM什么意思呢?我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新举例一下Html结构<div id="app"> {{ message }} </div>构建一个vue实例const vm = new Vue({  el: '#app',  data: {    message: '原始值'  }})修改messagethis.message = '修改后的值1'this.message = '修改后的值2'this.message = '修改后的值3'这时候想获取页面最新的DOM节点,却发现获取到的是旧值console.log(vm.$el.textContent) // 原始值这是因为message数据在发现变化的时候,vue并不会立刻去更新Dom,而是将修改数据的操作放在了一个异步操作队列中如果我们一直修改相同数据,异步操作队列还会进行去重等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM的更新为什么要有nexttick举个例子{{num}}for(let i=0; i<100000; i++){    num = i}如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略二、使用场景如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()第一个参数为:回调函数(可以获取最近的DOM结构)第二个参数为:执行函数上下文// 修改数据vm.message = '修改后的值'// DOM 还没有更新console.log(vm.$el.textContent) // 原始的值Vue.nextTick(function () {  // DOM 更新了  console.log(vm.$el.textContent) // 修改后的值})组件内使用 vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上this.message = '修改后的值'console.log(this.$el.textContent) // => '原始的值'this.$nextTick(function () {    console.log(this.$el.textContent) // => '修改后的值'})$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情this.message = '修改后的值'console.log(this.$el.textContent) // => '原始的值'await this.$nextTick()console.log(this.$el.textContent) // => '修改后的值'三、实现原理源码位置:/src/core/util/next-tick.jscallbacks也就是异步操作队列callbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一次01112131415161718192021222324252627282930export function nextTick(cb?: Function, ctx?: Object) {  let _resolve;  // cb 回调函数会经统一处理压入 callbacks 数组  callbacks.push(() => {    if (cb) {      // 给 cb 回调函数执行加上了 try-catch 错误处理      try {        cb.call(ctx);      } catch (e) {        handleError(e, ctx, 'nextTick');      }    } else if (_resolve) {      _resolve(ctx);    }  });  // 执行异步延迟函数 timerFunc  if (!pending) {    pending = true;    timerFunc();  }  // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用  if (!cb && typeof Promise !== 'undefined') {    return new Promise(resolve => {      _resolve = resolve;    });  }}timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:Promise.then、MutationObserver、setImmediate、setTimeout通过上面任意一种方法,进行降级操作131415161718192021222324252627282930313233343536export let isUsingMicroTask = falseif (typeof Promise !== 'undefined' && isNative(Promise)) {  //判断1:是否原生支持Promise  const p = Promise.resolve()  timerFunc = () => {    p.then(flushCallbacks)    if (isIOS) setTimeout(noop)  }  isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && (  isNative(MutationObserver) ||  MutationObserver.toString() === '[object MutationObserverConstructor]')) {  //判断2:是否原生支持MutationObserver  let counter = 1  const observer = new MutationObserver(flushCallbacks)  const textNode = document.createTextNode(String(counter))  observer.observe(textNode, {    characterData: true  })  timerFunc = () => {    counter = (counter + 1) % 2    textNode.data = String(counter)  }  isUsingMicroTask = true} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {  //判断3:是否原生支持setImmediate  timerFunc = () => {    setImmediate(flushCallbacks)  }} else {  //判断4:上面都不行,直接用setTimeout  timerFunc = () => {    setTimeout(flushCallbacks, 0)  }}无论是微任务还是宏任务,都会放到flushCallbacks使用这里将callbacks里面的函数复制一份,同时callbacks置空依次执行callbacks里面的函数12345678function flushCallbacks () {  pending = false  const copies = callbacks.slice(0)  callbacks.length = 0  for (let i = 0; i < copies.length; i++) {    copies[i]()  }}小结:把回调函数放入callbacks等待执行将执行函数放到微任务或者宏任务中事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调转载自https://www.cnblogs.com/smileZAZ/p/18027869
  • [技术干货] pinia初学习
    两种写法定义pinia第一种:对象形式不需要写ref state直接就是响应式数据import { defineStore } from "pinia" export const useCounterStore = defineStore("useCounterStore ", { state:() =>{ return{ } }, actions:{ }, getters:{ } })第二种: 匿名函数形式需要写ref定义state 不然不是响应式 用了ref 在actions和getters中使用的时候就需要.valueimport { defineStore } from "pinia" import { ref, computed } from "vue" export const useCounterStore = defineStore("useCounterStore ", () => { let num = ref(1) // 自动识别为state const dobuleNum = computed(() => { // 使用了computed 自动识别为计算属性 return num.value * 2 }) function addNum(){ //自动识别为actions pinia没有muctions了 actions就可以同步异步都可以 } return { num, dobuleNum,addNum } })state重置状态将state的状态重置到初始值let data = useCountPinia() data.$reset() //重置同时修改多个状态// 第一种方法 接受一个对象 let data = useCountPinia() data.$patch({ name:xxx, num:xxx }) // 第二种方法:接受一个函数 data.$patch((state)=>{ state.name = xxxx state.num = xxx })替换整个state通过 store.$state = {}来替换let data = useCountPinia() data.$state = { xxx }订阅状态在vue中可以使用watch来对vuex的数据进行监听 但是如果想在js中使用的话就要借助$subscribevuex中也有$subscribe 具体看文档吧pinia中的$subscribe写法如下import { useCountPinia } from "@/store/index" const data = useCountPinia() data.$subscribe((mutation,state)=>{ // mutation是记录state变化信息的对象 state是state对象 // 修改state之后会触发此回调函数 }) // 分析mutation和state是什么 // mutaton: { "type": "patch function", // 修改的类型 /** "patch function"通过$patch传入函数修改 "patch object" 通过$patch传入对象修改 "direct" 直接修改state */ "storeId": "useCounterStore ", // 库的id "events": [ // 存储变化的数据信息 {...} ] } // state 是一个Proxy对象 是state的代理对象 那么 watch 和 subscribe 捕获数据变化的区别是什么?watch只会捕获新旧值不同的情况subscribe不仅会捕获新旧值不同 只要是进行修改 就会捕获二、Getters箭头函数没有this所以需要使用接收参数state来实现getters: { // 自动将返回类型推断为数字 doubleCount(state) { return state.counter * 2 }, // 返回类型必须明确设置 doublePlusOne(): number { return this.counter * 2 + 1 }, doubleNum:(state) =>{ return state.num *2 } },传递参数给getter没有办法直接获取 但是可以再返回一个函数 在这个函数中去接收 doubleNum:(state) =>{ return (value) =>{ // 接收的参数 return state.num + value } }<h1> {{store.doubleNum('我是传参')}} </h1>需要注意的是 当使用了传参之后 getter则不再缓存 只是您调用的参数三、ActionsActions相当于组件中的methods 可以使用actions进行定义export const useStore = defineStore('main',() =>{ state:()=>{ return { num:0 } }, actions:{ add(){ this.num ++ } } })actions可以是异步的 代替了vuex 中的mutations订阅actionsxxxxxxx四、plugins用于补充扩展store。本质其实就是一个函数,可以有以下操作向Store添加新属性定义Store时添加新选项为Store添加新方法包装现有方法更改甚至取消操作实现本地存储等副作用仅适用于特定Store在mian.js文件中使用pinia.use()将插件添加到pinia实例中。下面举例为所有的store添加一个静态属性//main文件 import { createPinia } from "pinia" // 为安装此插件后创建的每个store添加一个名为 `level` 的属性 function SecretPiniaPlugin() { return { level:'1.982' } } // 将插件提供给pinia const pinia = createPinia() pinia.use(SecretPiniaPlugin) // 在另一个文件中 const store = useStore() store.level // 'the cake is a lie'添加新属性有两种写法// 第一种 const pinia = createPinia() pinia.use(()=>{ return { name:'我是第一种' } }) // 第二种 const pinia = createPinia() pinia.use(({store}) =>{ store.name = "我是第二种" })转载自https://www.cnblogs.com/zxl327/p/17911621.html
  • [技术干货] 为什么需要使用$nextTick?【转】
    首先我们来看看官方对于$nextTick的定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。由于vue的试图渲染是异步的,生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM其实并未进行渲染,而此时进行DOM操作是徒劳的,所以一定要将DOM操作的js代码放到Vue.nextTick()的回调函数中。除了在created()钩子函数中使用之外咱们还会遇到很多种需要使用到Vue.nextTick()的场景,如下所示:咱们日常生活中常常会遇上上述场景,当我们点击按钮更新数据时候,如下示例:<template> <div> <input type="text" v-if = "isShow" ref="input"/> <button @click="handleClick">点击显示输入框,并且获取输入框焦点</button> </div> </template> <script> export default { data() { return { isShow: false } }, methods : { handleClick () { this.isShow = true this.$refs.input.focus() //控制栏会报错,因为还没有这个dom } } } </script>点击控制栏显示效果:控制栏报错,提示没有获取到dom元素;所以现在Vue.nextTick()派上了用场,Vue.nextTick() 方法的作用正是等待上一次事件循环执行完毕,并在下一次事件循环开始时再执行回调函数。这样可以保证回调函数中的 DOM 操作已经被 Vue.js 进行过更新,从而避免了一些潜在的问题,如下代码所示:<template> <div> <input type="text" v-if = "isShow" ref="input"/> <button @click="handleClick">点击显示输入框,并且获取输入框焦点</button> </div> </template> <script> export default { data() { return { isShow: false } }, methods : { handleClick () { this.isShow = true this.$nextTick(()=>{ this.$refs.input.focus() }) } } } </script>加上this.$nextTick后就能够使得输入框获取到焦点;总而言之Vue.nextTick()就是下次 DOM 更新渲染后执行延迟回调函数。在日常开发中,我们在修改数据之后使用这个方法,就可以获取更新后的 DOM的同时进行在对DOM进行相对应操作的 js代码;2.$nextTick如何实现的?JS是单线程执行的,所有的同步任务都是在主线程上执行的,形成了一个执行栈,从上到下依次执行,异步代码会放在任务队列里面。•同步任务在主线程里执行,当浏览器第一遍过滤html文件的时候可以执行完;(在当前作用域直接执行的所有内容,包括执行的方法、new出来的对象)•异步任务耗费时间较长或者性能较差的,浏览器执行到这些的时候会将其丢到异步任务队列中,不会立即执行同时异步任务分为宏任务(如setTimeout、setInterval、postMessage、setImmediate等)和微任务(Promise、process.nextTick等),浏览器执行这两种任务的优先级不同;会优先执行微任务队列的代码,微任务队列清空之后再执行宏任务的队列,这样循环往复;JS自上向下进行代码的编译执行,遇到同步代码压入JS执行栈执行后出栈,遇到异步代码放入任务队列,当JS执行栈清空,去执行异步队列中的回调函数,先去执行微任务队列,当微任务队列清空后,去检测执行宏任务队列中的回调函数,直至所有栈和队列清空整体流程如下图所示:接下来让我们看看nextTick的源码~vue将nextTick的源码放在了vue/core/util/next-tick.js中。如下图所示:我们把这个文件拆成三个部分来看:1.nextTick定义函数我们将nextTick函数单独拿出来,callbacks是一个回调队列,其实调用nextTick就是往这个数组里面传执行任务,callbacks新增回调函数之后执行timerFunc函数,pending是用来限制同一个事件循环内只能执行一次的pending锁;const callbacks = [] // 回调队列 let pending = false // export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { // cb 回调函数会经统一处理压入 callbacks 数组 if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) // 执行异步延迟函数 timerFunc if (!pending) { pending = true timerFunc() } // $flow-disable-line // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }2.timerFunc函数 做了四个判断,先后尝试当前环境是否能够使用原生的Promise.then、MutationObserver和setImmediate,不断的降级处理,如果以上三个都不支持,则最后就会直接使用setTimeOut,主要操作就是将flushCallbacks中的函数放入微任务或者宏任务,等待下一个事件循环开始执行;宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,使用宏任务;但是,各种宏任务之间也有效率的不同,需要根据浏览器的支持情况,使用不同的宏任务;export let isUsingMicroTask = false let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { //是否支持Promise const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { //是否支持MutationObserver let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = () => { //是否支持setImmediate setImmediate(flushCallbacks) } } else { // Fallback to setTimeout. timerFunc = () => { //上面都不行,直接使用setTimeout setTimeout(flushCallbacks, 0) } }3.flushCallbacks函数flushCallbacks函数只有几行,也很好理解,将pending锁置为false,同时将callbacks数组复制一份之后再将callbacks置为空,接下来将复制出来的callbacks数组的每个函数依次进行执行,简单来说它的主要作用就是用来执行callbacks中的回调函数;function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } }值得注意的是,$nextTick 并不是一个真正意义上的微任务microtask,而是利用了事件循环机制来实现异步更新。因此,它的执行时机相对于微任务可能会有所延迟,但仍能保证在 DOM 更新后尽快执行回调函数。总的来说,nextTick就是1.将传入的回调函数放入callbacks数组等待执行,定义pending判断锁保证一个事件循环中只能调用一次timerFunc函数;2.根据环境判断使用异步方式,调用timerFunc函数调用flushCallbacks函数依次执行callbacks中的回调函数;3.个人小结nextTick可避免数据更新后导致DOM的数据不一致的问题,提供了更稳定的异步更新机制,解决了created钩子函数DOM未渲染会造成的异步数据渲染问题,但如果过多的使用nextTick会导致事件循环中任务数量和回调函数增多,有可能出现可怕的回调地狱,导致性能下降,同时过度依赖nextTick也会降低代码的可读性,所以大家还是"按需加载"的好~作者:京东保险 卓雅倩来源:京东云开发者社区 转载请注明来源
  • [技术干货] Vue自动生成组件名【转】
    unplugin-generate-component-name一款用于以文件夹名或者setup标签写入名字来自动生成Vue组件名的插件。项目地址功能💚 支持 Vue 3 开箱即用。⚡️ 支持 Vite、Webpack、Rspack、Vue CLI、Rollup、esbuild 等,由 unplugin 提供支持。🪐 支持文件夹名称和 Setup extend 两种模式。🦾 完全支持 TypeScript。安装12345# Yarn$ yarn add unplugin-generate-component-name -D# pnpm$ pnpm i unplugin-generate-component-name -DVite 配置12345678// vite.config.tsimport GenerateComponentName from 'unplugin-generate-component-name/vite'export default defineConfig({  plugins: [    GenerateComponentName({ /* options */ }),  ],})Rollup 配置12345678// rollup.config.jsimport GenerateComponentName from 'unplugin-generate-component-name/rollup'export default {  plugins: [    GenerateComponentName({ /* options */ }),  ],}Webpack 配置1234567// webpack.config.jsmodule.exports = {  /* ... */  plugins: [    require('unplugin-generate-component-name/webpack').default({ /* options */ }),  ],}使用方法(开箱即用)文件夹名称你可以使用index组件所在的文件夹名作为组件的名字。自动生成 Vue 组件名称在Vue中,我们可以使用unplugin-generate-component-name插件自动基于目录名称生成组件名称。这个插件使得在大型代码库中找到和管理组件更加容易和直观。例如,假设我们有一个Vue组件名为Index.vue,此组件在Home目录中。通过unplugin-generate-component-name插件,此组件会自动命名为Home。123456src/home/├── index.vue // 组件名称是 Home├── about.vue└── users/    ├── index.vue // 组件名称是 Users    └── info.vueSetup Extend在 <script setup> 标签中,我们设置了 name 属性为 "Home"。这显式地将组件命名为 "Home",unplugin-generate-component-name 插件会使用这个名字而不是 "Index"。1234567891011<template>  <!-- 你的组件标记 --></template><script setup name="Home">  // 你的脚本逻辑</script><style>  <!-- 你的组件样式 --></style>选项1234567891011121314type GenComponentName = (opt: {    filePath: string;    dirname: string;    originalName: string;    attrName: string | undefined;}) => string;interface PattenOptions {    include?: string | RegExp | (string | RegExp)[];    exclude?: string | RegExp | (string | RegExp)[];    genComponentName: GenComponentName;}interface Options extends Omit<PattenOptions, 'genComponentName'> {    enter?: PattenOptions[];}includeinclude 选项能够明确说明哪些文件应被包含在组件名称的自动创建过程中。excludeexclude 选项则用于指明哪些文件应排除在组件名称的自动创建过程之外。enter在 Options 接口中,有一个 enter 选项。enter 是一个数组,该数组中每个对象都被视作一种特定的规则用于处理不同的文件路径。每一种规则都可以包含 include 和 exclude 选项,用以指明哪些文件是应特别对待的。你也可以要求对 genComponentName 函数进行特定的设置,以达到自定义组件名称生成的效果。下面是一个例子:123456789101112131415161718// vite.config.tsimport GenerateComponentName from 'unplugin-generate-component-name/vite'export default defineConfig({  plugins: [     GenerateComponentName({      include: ['**/*.vue'],      enter: [{        include: ["**/*index.vue"],        genComponentName: ({ attrName, dirname }) => attrName ?? dirname      }, {        exclude: ['**/*.index.vue'],        include: ["src/components/**/*.vue"],        genComponentName: ({ dirname, originalName }) => `${dirname}-${originalName}`      }]    }),  ],});在这个例子中,unplugin-generate-component-name 插件被配置为处理所有的 .vue 文件。在 enter 选项中有两个对象适用于不同的文件路径:第一个对象覆盖所有以"index.vue" 结尾的文件。genComponentName 函数返回组件名称。如果 script setup 标签 中已经指定了 名称 ,那么优先级将会很高; 如果没有,文件夹名称(dirname )就将会使用。第二个对象排除了所有以"index.vue" 结尾的文件, 仅包括 "src/components/" 目录下的 .vue 文件。genComponentName 函数用来以 ${dirname}-${originalName} 的格式生成组件名。例如,对于一个名为 src/component/Button 中的 MyButton.vue 文件,它将会是 Button-MyButton 。本文转载于:https://juejin.cn/post/7314301236098269236
  • [技术干货] 工程化第一步这个package.json要真的搞明白才行【转】
    工程化最开始就是package.json开始的,很多人学了很多年也没搞清楚这个为什么这么神奇,其实有些字段是在特定场景才有效的,那每个属性的适用场景和作用是什么,又牵扯很多知识点,今天先解读一些常见的属性,关注我,后期在遇到特定场景也会再逐步的补充这些属性,只有真正清楚知道每个自动的属性和场景你才能真正使用它得心应手,也才能真正掌握并帮助你解决你的问题。创建一个package.json 你可以使用npm init 按指令创建,也可以通过npm init -y来快速创建,当然也可以手动来创建,那现在我们创建一个。 package.json{ "name": "package-demo", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC"}这个是原装的属性,当然还有一些额外的场景才有的,接下来我们来逐步分析:常用属性这些属性没有没有太复杂,但还是要仔细看看每个都可以做什么,适用于什么场景,如何去使用和配置namepackage.json 中最重要字段之一就是名称name 字段,有人的说为什么我没有配置也没有影响我项目的正常运行呢?这个是因为你没有发布npm 包,这个name就是你发布到npm的包名,如果你不打算发布你的包到npm上这个是可以省略的。当然这个名称也不是随便乱起的,它也有一定的规则,否则有警告的如下:1字符串与 "^(?:(?:@(?:[a-z0-9-*~][a-z0-9-*._~]*)?/[a-z0-9-._~])|[a-z0-9-~])[a-z0-9-._~]*$" 的模式不匹配。name的规则如下:名称必须少于或等于 214 个字符。 这包括范围包的范围。范围包的名称可以以点或下划线开头。 没有范围是不允许的。新包的名称中不得包含大写字母。该名称最终成为 URL、命令行参数和文件夹名称的一部分。 因此,名称不能包含任何非 URL 安全字符version这个是版本,跟name 字段差不多,基本是一起的,因为它们共同构成一个假定完全唯一的标识符,如果你不发布包也是这个省略这个字段的,当然这个字段值也不是随意写的,它完全基于符合语义化版本(Semantic Versioning)来管理依赖的版本,所以版本号的更新需要符合语义化版本规范,简单总结:可以被 semver 包可解析的,当然你也可以使用这个semver 进行版本的操作,具体可以看我的另一篇版本号文章版本号keywords这个也是发布到npm 上才有用的,它的作用也是别人可以通过搜索来查找你的包,或是通过npm search xxx开查找你的包 如下,我有两个运行时的包处理react preact 的className的,可以看看查出了什么:是不是几个包的信息,所以这个是搜索查找用的,如果你不发布这个也可以不写的homepage项目主页的 url。随便找个npm 包,可以看一下,该包的右侧有个地址就是这个字段进行配置的bugs这个就是要需要作者给个地址,比如你写的包有问题,别人怎么给你提问题,联系到你 通常我们设置的issue地址,那如何打开呢,可以通过如下方式:npm bugs [<pkgname> [<pkgname> ...]]license指定一个许可证,以便人们知道他们如何被允许使用它,以及你对其施加的任何限制,常见的开源协议是 MIT 和 BSD,如果我们不确定我们应该如何写,可以看一下网上这个图说的比较清晰借用一下:author它表示项目的作者信息,它既可以是一个字符串,又可以是一个对象"author":{  "name": "lvz",  "email": "xxxx@xx.com",  "url": "https://xxxxx"}或者使用字符串的形式也可以"author": "lvz <xxxxx@xx.com> (https://xxxxx)"contributorscontributors 它表示的是贡献者,是一个数组,当然这个也有两种写法"contributors": [  "lvz1 <xxxxx@xx.com> (https://xxxxx)" ]或者对象的写法"contributors":[    {      "name": "lvz1",      "email": "xxxx@xx.com",      "url": "https://xxxxx"    }]funding如果你不开源,对资金没有要求或有其它渠道可以不使用,这个是在npm 6.13.0添加了funding命令,针对开源者,让维护 npm 的开发人员,为有意愿的捐赠者指明捐赠平台。在 package.json 文件中添加了一个funding字段,指向在线捐赠服务的 url,如 Patreon、Open 或者其他支付网站,并可以通过npm fund命令列出这些捐赠平台及其 url。"funding": [    {      "type": "patreon",      "url": "http://XXXXXX"    },    "http://XXXXX",    {          "type": "open",      "url": "https://XXXXXX"    }  ]files项目在进行 npm 发布时,可以通过 files 指定需要跟随一起发布的内容来控制 npm 包的大小,避免安装时间太长,比如我们包体有很多文件,但我们只想把dist放进去,其他src,test文件并不想放在包体中,可以使用这个字段但无论设置如何,总是会包含某些文件:当然除了files 也有其它方式可以来忽略文件进入包体,我们先看一下我们什么也不处理的文件格式添加.gitignore文件12src/*example/*我们发布之后在看一下文件结构,只有index.js 也就是main字段指向的index.js还有package.json两个文件添加.npmignore 文件12src/*example同样我们发布之后看一下文件结构,如下图所示:.npmignore在程序包的根目录下,它不会覆盖“files”字段,但在子目录中,它将覆盖。该.npmignore文件的工作方式与一样 .gitignore。如果有.gitignore文件但.npmignore缺少文件,.gitignore则将使用的内容repository简单点说就是配置仓库地址,这个配置后,可以在npm 包首页看到仓库地址入口,可以进行点击查看12345"repository": {   "type": "git",   "url": "https://github.com/xxxxx",   "directory": "xxxx/xxx" }当然你也可以直接写字符串1"repository":"https://github.com/xxxxx/xxxx"config配置对象可用于设置在升级后持续存在的包脚本中使用的配置参数123"config": {  "type": "chrome"},我们在index.js中进行打印一下1console.log(process.env.npm_package_config_type)此时如果你直接node index.js 你会发现打印的是个undefined,我们在将其放在scripts中:123"scripts": {   "start": "node index.js" },再执行就可以打印出值了,这样我们就可以在脚本中使用了这个环境变量了。依赖配置项这个是我们这次的重点之一,其实有些朋友经常分不清,这些到底怎么回事,我们先整体分析一下,我们安装一个包有两种,一种是当前项目package.json 已有的,一种是安装包中package.json的依赖,那针对这个我们做个实验再总结一下看看它们都是什么妖魔鬼怪。我们在介绍version这个字段的时候,讲解了如何安装一个指定范围的npm包,如果有疑问可以进去查看哈,这里就不再赘述了背景 有两个项目,一个是project-demo,一个是package-demo,包名是lucky-package-demo,project-demo会依赖lucky-package-demo这个包。 如下project-demo 中的package.json123"dependencies": {    "lucky-package-demo": "*"}devDependencies简单理解就是这个包是开发环境用到了,生产环境并不需要它,比如我们的webpack,vite,或是eslint等我们在package-demo 中安装 vite123"devDependencies": {    "vite": "4.3.0"}然后我们在project-demo更新lucky-package-demo后,可以看到project-demo的node_modules中发现react 和 react-demo 已经被安装了,虽然暂时我们并没有去使用这两个包,也就是说dependencies中的包将会被安装peerDependencies也叫同等依赖,它主要用于确保多个模块在同一个主模块的上下文中使用,并共享依赖的版本,简单来说就是project-demo项目中已经安装react了,package-demo中我没必要在dependencies依赖这个react了。从而避免项目中和依赖包中出现重复安装包所导致的包版本不相容、打包了多份不同版本的库等问题,我们举几个常遇到的问题来解释一下这个属性。我有一个组件库,依赖了react,项目中我也要使用react并且已经安装好了,组件库和项目可以共享react但有个问题,我项目更新了,react的版本和组件库react的版本不一致了,还是共享,如何避免冲突安装方式:我们可以配合一些属性使用,在依赖包中,因为我们不会在项目包中安装,可以放在devDependencies中,我们又想告诉项目包我们需要什么范围内的版本,就需要设置peerDependencies,如下:package-demo中如下配置1234567"devDependencies": {    "react": "17.0.2",    "vite": "4.3.0"},"peerDependencies": {    "react":">16.8.0 <18.0.0"}如果我们在project-demo中安装的是 react:"17.0.1",此时是满足我们依赖包中peerDependencies中对react的版本范围要求的,安装很顺利,项目和依赖包会共享17.0.1的react的包,也不会存在任何警告。12345dependencies:- lucky-package-demo+ lucky-package-demo 0.0.10- react+ react 17.0.1 (18.2.0 is available)但如果此时我们将project-demo中react缓存16.8.0此时会有如下提示,因为此时16.8.0 是不满足peerDependencies中react版本范围要求的,此时你手动处理一下,要么安装正确的版本,要么通知作者更新一下peerDependencies12345678910dependencies:- lucky-package-demo+ lucky-package-demo 0.0.10- react+ react 16.8.0 (18.2.0 is available) WARN  Issues with peer dependencies found.└─┬ lucky-package-demo 0.0.10  └── ✕ unmet peer react@">16.8.0 <18.0.0": found 16.8.0peerDependenciesMeta如果 peerDependencies 中指定的包尚未安装,npm 将发出警告,如下配置将对等依赖标记为可选,如果用户没有安装对等依赖,npm不会发出警告12345"peerDependenciesMeta":{   "react":{     "optional":true   } }在project-demo中安装lucky-package-demo,可以发现node_modules中并不存在react的包,控制台也没有任何的警告出现1npm i -S lucky-package-demo如果设置成optional:false中,在project-demo中安装lucky-package-demo后将会自动安装符合范围的react的包bundleDependenciesnpm的命令 -B, --save-bundle语法:npm i -B [package-name]这个属性使用的较少,通过在 bundleDependencies 数组中指定包名称,在发布包时,包名的数组会被打包进去,有的说我试过但发布包什么也没有呢?因为单单配置bundleDependencies这个是没有效果的,需要再在依赖包中安装devDependencies或dependencies中才能将其打包到node_modules中。举例说明:在package-demo 中进行如下安装和配置,并在project-demo中npm i -S lucky-package-demo安装,你会发现project-demo中的node_modules中lucky-package-demo的node_modules中是bundleDependencies中配置的react-runtime-clsx包123456"dependencies": {    "react-runtime-clsx": "0.0.1" },"bundleDependencies": [    "react-runtime-clsx"]当然你可以在package-demo跟目录执行npm pack,和安装lucky-package-demo一样,可以看看目录结构:123456project-demo----node_modules-------lucky-package-demo-----------node_modules---------------react-runtime-clsx //这个是bundleDependencies配置的---------------clsx  这个是react-runtime-clsx的依赖optionalDependencies通过npm 命令 -O, --save-optionalnpm i -O [package-name]optionalDependencies 用于定义可选依赖项,和 dependencies 非常类似,主要的差别在于:在 optionalDependencies 中的依赖包安装报错甚至找不到时不会影响到包管理器的安装行为12345npm i -O react-runtime-clsx// package-demo package.json 如下:"optionalDependencies": {    "react-runtime-clsx": "0.0.1"}optionalDependencies 中的条目将覆盖 dependencies 中的同名条目,因此通常最好只放在一个位置overrides如果需要对依赖项的依赖项进行特定更改,例如将依赖项的版本替换为已知的安全问题,将现有依赖项替换为分支,或者确保在任何地方都使用相同版本的包,则可以添加覆盖。package-demo中package.json 配置123"optionalDependencies": {    "react-runtime-clsx": "0.0.1"  }具体的大家可以看一下文档overrides文档 )项目中的依赖项目中我们也会有依赖,有些是辅助开发的工具,有些是项目引用的代码包,因为项目我们是不会发布它作为npm包来使用的,它作为的是一个工程,所以在依赖上是有区分的,你是不是也有下面的困惑呢?我随便装的并不影响到开发和构建,似乎这两者没有区别,安装哪里都一样呢?感觉是对的,其实项目中这两者并没有明显的划分,只是我们通常是将开发中使用的安装到devDependencies,运行使用的放在dependencies,其实这两个都是安装了的,无论你放哪都可以使用,所以也有部分刚接触时很困惑,因为这个针对的是npm发布包有用的,在项目中还是按规范了养成习惯,清楚每个的含义,对我们做npm包的发布是有用的哈本文转载于:https://juejin.cn/post/7315606159742058530
  • [技术干货] 关于vite的跨域问题【转】
    报错Access to XMLHttpRequest at '<http://localhost:3000/player>' from origin '<http://localhost:4000/>' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.解决vue需要配置自定义代理规则进行跨域访问配置跨域官方文档:cid:link_0在vite.config.ts修改://vite.config.ts export default defineConfig({ //...... server: { //...... port: 4000, //vite的默认端口(别和后端冲突了) proxy: { "/api": { //代理的请求 target: "http://localhost:8000", //后端的地址 changeOrigin: true, //开启跨域访问 rewrite: (path) => path.replace(/^\/api/,''), //重写前缀(如果后端本身就有api这个通用前缀,那么就不用重写) }, }, }, })发起请求的地方:axios.defaults.baseURL ='/api'; //配置前缀 axios.get('info') //这里会发送到http://localhost:4000/info生产环境配置跨域生产环境配置跨域,还需要编辑nginx的配置文件,在server对象中再添加一个location对象(别忘了上一个对象末尾的分号;)        server { //...... location /api/ { proxy_pass http://localhost:8000/; //后端的地址 } }转载自https://www.cnblogs.com/korin5/p/17931093.html
  • [技术干货] 前端无感知刷新token & 超时自动退出【转】
    前端无感知刷新token&超时自动退出一、token的作用因为http请求是无状态的,是一次性的,请求之间没有任何关系,服务端无法知道请求者的身份,所以需要鉴权,来验证当前用户是否有访问系统的权限。以oauth2.0授权码模式为例:每次请求资源服务器时都会在请求头中添加 Authorization: Bearer access_token 资源服务器会先判断token是否有效,如果无效或过期则响应 401 Unauthorize。此时用户处于操作状态,应该自动刷新token保证用户的行为正常进行。刷新token:使用refresh_token获取新的access_token,使用新的access_token重新发起失败的请求。二、无感知刷新token方案2.1 刷新方案当请求出现状态码为 401 时表明token失效或过期,拦截响应,刷新token,使用新的token重新发起该请求。如果刷新token的过程中,还有其他的请求,则应该将其他请求也保存下来,等token刷新完成,按顺序重新发起所有请求。2.2 原生AJAX请求2.2.1 http工厂函数function httpFactory({ method, url, body, headers, readAs, timeout }) { const xhr = new XMLHttpRequest() xhr.open(method, url) xhr.timeout = isNumber(timeout) ? timeout : 1000 * 60​ if(headers){ forEach(headers, (value, name) => value && xhr.setRequestHeader(name, value)) } const HTTPPromise = new Promise((resolve, reject) => { xhr.onload = function () { let response;​ if (readAs === 'json') { try { response = JSONbig.parse(this.responseText || null); } catch { response = this.responseText || null; } } else if (readAs === 'xml') { response = this.responseXML } else { response = this.responseText }​ resolve({ status: xhr.status, response, getResponseHeader: (name) => xhr.getResponseHeader(name) }) }​ xhr.onerror = function () { reject(xhr) } xhr.ontimeout = function () { reject({ ...xhr, isTimeout: true }) }​ beforeSend(xhr)​ body ? xhr.send(body) : xhr.send()​ xhr.onreadystatechange = function () { if (xhr.status === 502) { reject(xhr) } } })​ // 允许HTTP请求中断 HTTPPromise.abort = () => xhr.abort()​ return HTTPPromise;}2.2.2 无感知刷新token// 是否正在刷新token的标记let isRefreshing = false​// 存放因token过期而失败的请求let requests = []​function httpRequest(config) { let abort let process = new Promise(async (resolve, reject) => { const request = httpFactory({...config, headers: { Authorization: 'Bearer ' + cookie.load('access_token'), ...configs.headers }}) abort = request.abort try { const { status, response, getResponseHeader } = await request​ if(status === 401) { try { if (!isRefreshing) { isRefreshing = true // 刷新token await refreshToken()​ // 按顺序重新发起所有失败的请求 const allRequests = [() => resolve(httpRequest(config)), ...requests] allRequests.forEach((cb) => cb()) } else { // 正在刷新token,将请求暂存 requests = [ ...requests, () => resolve(httpRequest(config)), ] } } catch(err) { reject(err) } finally { isRefreshing = false requests = [] } } } catch(ex) { reject(ex) } }) process.abort = abort return process}​// 发起请求httpRequest({ method: 'get', url: 'http://127.0.0.1:8000/api/v1/getlist' })2.3 Axios 无感知刷新token// 是否正在刷新token的标记let isRefreshing = false​let requests: ReadonlyArray<(config: any) => void> = []​// 错误响应拦截axiosInstance.interceptors.response.use((res) => res, async (err) => { if (err.response && err.response.status === 401) { try { if (!isRefreshing) { isRefreshing = true // 刷新token const { access_token } = await refreshToken()​ if (access_token) { axiosInstance.defaults.headers.common.Authorization = `Bearer ${access_token}`;​ requests.forEach((cb) => cb(access_token)) requests = []​ return axiosInstance.request({ ...err.config, headers: { ...(err.config.headers || {}), Authorization: `Bearer ${access_token}`, }, }) }​ throw err }​ return new Promise((resolve) => { // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行 requests = [ ...requests, (token) => resolve(axiosInstance.request({ ...err.config, headers: { ...(err.config.headers || {}), Authorization: `Bearer ${token}`, }, })), ] }) } catch (e) { isRefreshing = false throw err } finally { if (!requests.length) { isRefreshing = false } } } else { throw err }})三、长时间无操作超时自动退出当用户登录之后,长时间不操作应该做自动退出功能,提高用户数据的安全性。3.1 操作事件操作事件:用户操作事件主要包含鼠标点击、移动、滚动事件和键盘事件等。特殊事件:某些耗时的功能,比如上传、下载等。3.2 方案用户在登录页面之后,可以复制成多个标签,在某一个标签有操作,其他标签也不应该自动退出。所以需要标签页之间共享操作信息。这里我们使用 localStorage 来实现跨标签页共享数据。在 localStorage 存入两个字段:当有操作事件时,将当前时间戳存入 lastActiveTime。当有特殊事件时,将特殊事件名称存入 activeEvents ,等特殊事件结束后,将该事件移除。设置定时器,每1分钟获取一次 localStorage 这两个字段,优先判断 activeEvents 是否为空,若不为空则更新 lastActiveTime 为当前时间,若为空,则使用当前时间减去 lastActiveTime 得到的值与规定值(假设为1h)做比较,大于 1h 则退出登录。3.3 代码实现const LastTimeKey = 'lastActiveTime'const activeEventsKey = 'activeEvents'const debounceWaitTime = 2 * 1000const IntervalTimeOut = 1 * 60 * 1000​export const updateActivityStatus = debounce(() => { localStorage.set(LastTimeKey, new Date().getTime())}, debounceWaitTime)​/** * 页面超时未有操作事件退出登录 */export function timeout(keepTime = 60) { document.addEventListener('mousedown', updateActivityStatus) document.addEventListener('mouseover', updateActivityStatus) document.addEventListener('wheel', updateActivityStatus) document.addEventListener('keydown', updateActivityStatus)​ // 定时器 let timer;​ const doTimeout = () => { timer && clearTimeout(timer) localStorage.remove(LastTimeKey) document.removeEventListener('mousedown', updateActivityStatus) document.removeEventListener('mouseover', updateActivityStatus) document.removeEventListener('wheel', updateActivityStatus) document.removeEventListener('keydown', updateActivityStatus)​ // 注销token,清空session,回到登录页 logout() }​ /** * 重置定时器 */ function resetTimer() { localStorage.set(LastTimeKey, new Date().getTime())​ if (timer) { clearInterval(timer) }​ timer = setInterval(() => { const isSignin = document.cookie.includes('access_token') if (!isSignin) { doTimeout() return }​ const activeEvents = localStorage.get(activeEventsKey) if(!isEmpty(activeEvents)) { localStorage.set(LastTimeKey, new Date().getTime()) return } const lastTime = Number(localStorage.get(LastTimeKey))​ if (!lastTime || Number.isNaN(lastTime)) { localStorage.set(LastTimeKey, new Date().getTime()) return }​ const now = new Date().getTime() const time = now - lastTime​ if (time >= keepTime) { doTimeout() } }, IntervalTimeOut) }​ resetTimer()}​// 上传操作function upload() { const current = JSON.parse(localStorage.get(activeEventsKey)) localStorage.set(activeEventsKey, [...current, 'upload']) ... // do upload request ... const current = JSON.parse(localStorage.get(activeEventsKey)) localStorage.set(activeEventsKey, Array.isArray(current) ? current.filter((item) => itme !== 'upload'))}本文转载于:https://juejin.cn/post/7320044522910269478
  • [技术干货] 按钮防连点终极解决方案 【转】
    引言在日常前端开发中,我们经常会面对一个让人头疼的问题:按钮被用户点击了两次以上,导致出现重复提交表单或者发送重复的请求。这个问题常见而且恼人。为了解决这个问题,我们需要一个又简单又实用的方法,可以在不搞乱原有代码的情况下,有效地防止按钮被连续点击。背景随着网页应用变得越来越复杂,用户在页面上的交互也变得越来越频繁。这就使得按钮被不小心点击多次的情况变得非常普遍。一般的解决方法存在一些问题,比如改动原有代码太多,不够灵活等。因此,我们需要一种更好的、通用的按钮防连点方法。挑战在解决按钮被连点的问题时,我们要面临一些挑战。首先,解决方法得适应各种情况,比如表单提交、异步请求等。其次,我们需要确保解决方法不会让我们的原有代码变得混乱,同时还要具备足够的灵活性。最后,我们希望不修改原有代码的情况下提供按钮防连点。目标本文的目标是为大家提供一个简单易用的按钮防连点解决方案。我们会深入讲解方案的设计原理和实现细节,并且会附上完整的源码解析。通过学习本文,你将能够理解这个解决方案的原理,同时学会如何在实际项目中应用这个方法。希望通过这篇文章,按钮防连点问题不再让你感到头疼,反而变得得心应手。防连点原理概述在开始实现我们的按钮防连点终极解决方案之前,让我们首先理解一下连点问题的本质以及为什么传统的解决方案可能存在一些问题。连点问题的本质按钮连点问题的核心在于,用户在短时间内多次点击按钮,导致触发相同的操作。这可能引发一系列不良后果,比如重复提交表单、重复发送请求等。为了解决这个问题,我们需要一种机制来在用户点击按钮后一段时间内禁用按钮,防止其再次触发相同的操作。常规解决方案的局限性传统的解决方案往往通过在点击按钮后添加禁用状态,然后在一段时间后再启用按钮,来防止连点问题。然而,这种方法存在一些局限性。首先,它可能需要修改原有的按钮组件,使得在多个地方应用时不够灵活。其次,由于采用了定时器等机制,可能导致在某些情况下并不准确,或者在异步操作中存在问题。给按钮加指令创建一个可复用的Vue自定义指令,该指令能够动态地管理按钮的状态,以防止用户在短时间内多次点击按钮。关键之处在于,我们还将支持外部传递参数,以自定义按钮的禁用时间。创建可复用的防连点按钮组件首先,我们需要创建一个按钮组件,该组件可以接受我们的自定义指令。这样,我们就可以在需要的按钮上应用这个指令。123456789101112131415161718<template>  <button v-prevent-duplicate-clicks="2000" @click="handleClick">防连点按钮</button></template><script>import preventDuplicateClicks from '@/path-to-your-file/preventDuplicateClicks';export default {  directives: {    preventDuplicateClicks,  },  methods: {    handleClick() {      // 处理按钮点击事件的业务逻辑    },  },};</script>在上述代码中,我们创建了一个按钮组件,通过 v-prevent-duplicate-clicks 指令来防止按钮的连点行为。并且,我们通过传递参数 "2000" 指定了按钮禁用的时间为2秒。指令文件现在,创建一个名为 preventDuplicateClicks.js 的文件,该文件包含我们的自定义指令。123456789101112131415161718// preventDuplicateClicks.jsconst preventDuplicateClicks = {  mounted(el, binding) {    const { value } = binding;    el.addEventListener('click', () => {      if (!el.disabled) {        el.disabled = true;        setTimeout(() => {          el.disabled = false;        }, value || 1000); // 默认1秒后恢复按钮点击      }    });  },};export default preventDuplicateClicks;在上述代码中,我们定义了一个名为 preventDuplicateClicks 的自定义指令,它在按钮被点击时阻止多次点击。通过 setTimeout 来实现按钮在一定时间后恢复点击。此外,我们在指令上支持了外部参数的传递,用于自定义按钮的禁用时间。以为估计是80%的前端的解决方案,我在面试中也问过类似的问题,很多人能给出指令的写法已经很好了。有如下问题:侵入已有代码:如果是老的项目,都要添加这个功能,要去批量改,很麻烦;禁用时间错误:我们假设给的参数是2秒,一个接口请求超过2秒, 用户在请求响应之前依然会重复发起请求。如果接口几十毫秒就返回(是异常,让重试),用户也得等2秒;那么有没有更好的解决方案了,肯定是有的,我们来分析以上两个问题:侵入代码:我们通过重写已有组件可以做到,假设用的el-button,我们在全局把el-button覆盖成我们自己的即可禁用时间错误:既然设置是错的,那我们就不设置,按钮肯定都有一个onClick的函数,我们只要在onClick执行之前设置禁用,执行后启用即可。终极解决方案有了以上的分析,我们直接贴代码吧,我司用的vue + ant design vue,其他组件库,类似写法吧。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354<script>import { Button as AntButton } from 'ant-design-vue'export default {  name: 'AButton',  props: {    ...AntButton.props,    delay: {      type: Number,      default: 300    }  },  data() {    return {      customLoading: false,      ownDisabled: false    }  },  computed: {    allProps() {      return Object.assign({}, this.$props, {        loading: this.customLoading || this.loading,        type: this.$props.type || 'primary'      })    }  },  methods: {    async handler (...arg) {      if (this.ownDisabled) return      this.customLoading = true      this.ownDisabled = true      const { click: preClick } = this.$listeners || {}      const ret = preClick(...arg)      try {        await Promise.resolve(ret)      } finally {        this.customLoading = false        let timer = setTimeout(() => {          this.ownDisabled = false          clearTimeout(timer)          timer = null        }, this.delay)      }    }  },  render() {    return (      <AntButton props={this.allProps} onClick={this.handler}>        {this.$slots?.default}      </AntButton>    )  }}</script>需要注意的是,使用的时候 onClick 需要返回promise,这算一个很容易遵守的约定吧。另外加个默认延迟300ms,目的我记得好像是为了避免路由跳转时的问题,大家可以自己调整。本文转载于:https://juejin.cn/post/7304946949941608475
  • [技术干货] 5分钟搞定vue3函数式弹窗【转】
    前言最近接到一个需求,需要在一些敏感操作进行前要求输入账号和密码,然后将输入的账号和密码加到接口请求的header里面。如果每个页面都去手动导入弹窗组件,在点击按钮后弹出弹窗。再拿到弹窗返回的账号密码后去请求接口也太累了,那么有没有更简单的实现方式呢?函数式弹窗的使用场景首先我们来看看什么是函数式弹窗?函数式弹窗是一种使用函数来创建弹窗的技术。它可以简化弹窗的使用,只需要在需要弹窗的地方调用函数就可以了。那么这里使用函数式弹窗就能完美的解决我们的问题。我们只需要封装一个showPasswordDialog函数,调用该函数后会弹出一个弹窗。该函数会返回一个resolve后的值就是账号密码的Promise。然后在http请求拦截器中加一个needValidatePassword字段,拦截请求时如果该字段为true,就await调用showPasswordDialog函数。拿到账号和密码后塞到请求的header里面。这样就我们就只需要在发起请求的地方加一个needValidatePassword: true配置就行了。先来实现一个弹窗组件这个是简化后template中的代码,和Element Plus官网中的demo代码差不多,没有什么说的。<template> <el-dialog :model-value="visible" title="账号和密码" @close="handleClose"> <!-- 省略账号、密码表单部分... --> <el-button type="primary" @click="submitForm()">提交</el-button> </el-dialog> </template>这个是简化后的script代码,大部分和Element Plus官网的demo代码差不多。需要注意的是我们这里将close关闭事件和confirm确认事件定义在了props中,而不是在emits中,因为后面函数式组件会通过props将这两个回调传入进来。具体的我们下面会讲。<script setup lang="ts"> interface Props { visible: boolean; close?: () => void; confirm?: (data) => void; } const props = defineProps<Props>(); const emit = defineEmits(["update:visible"]); const submitForm = async () => { // 省略validate表单校验的代码 // 这里的data为表单中输入的账号密码 props.confirm?.(data); handleClose(); }; const handleClose = () => { emit("update:visible", false); props.close?.(); }; </script>再基于弹窗组件实现函数式弹窗createApp函数和app.mount方法createApp函数会创建和返回一个vue的应用实例,也就是我们平时常说的app,该函数接受两个参数。第一个参数为接收一个组件,也就是我们平时写的vue文件。第二个参数为可选的对象,这个对象会传递给第一个参数组件的props。举个例子:import MyComponent from "./MyComponent" const app = createApp(MyComponent, { visible: true })在这个例子中我们基于MyComponent组件生成了一个app应用实例,如果MyComponent组件的props中有定义visible,那么visible就会被赋值为true。调用createApp函数创建的这个应用实例app实际就是在内存中创建的一个对象,并没有渲染到浏览器的dom上面。这个时候我们就要调用应用实例app暴露出来的mount方法将这个组件挂载到真实的dom上面去。mount方法接收一个“容器”参数,用于将组件挂载上去,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串。比如下面这个例子是将组件挂载到body上面:app.mount(document.body)app提供了很多方法和属性,详见 vue官网。封装一个showPasswordDialog函数首先我们来看看期望如何使用showPasswordDialog函数?我们希望showPasswordDialog函数返回一个Promise,resolve的值就是弹窗中输入的表单。例如,我们可以使用以下代码使用showPasswordDialog函数:try { // 调用这个就会弹出弹窗 const res: RuleForm = await showPasswordDialog(); // 这个res就是输入的账号密码 console.log("res", res); } catch (error) { console.log(error); }具体如何实现showPasswordDialog函数?经过上面的介绍我们知道了可以调用createApp函数传入指定组件生成app,然后使用app.mount方法将这个组件挂载到指定的dom上面去。那么现在思路就清晰了,我们只需要将我们前面实现的弹窗组件作为第一个参数传递给createApp函数。第二个参数传入一个对象给弹窗组件的props,用以控制打开弹窗和注册弹窗关闭和确认的事件回调。下面是实现的showPasswordDialog函数import { App, createApp } from "vue"; import PasswordDialog from "./index.vue"; // 这个index.vue就是我们前面实现的弹窗组件 export async function showPasswordDialog(): Promise<RuleForm> { return new Promise((resolve, reject) => { let mountNode = document.createElement("div"); let dialogApp: App<Element> | undefined = createApp(PasswordDialog, { visible: true, close: () => { if (dialogApp) { dialogApp.unmount(); document.body.removeChild(mountNode); dialogApp = undefined; reject("close"); } }, confirm: (res: RuleForm) => { resolve(res); dialogApp?.unmount(); document.body.removeChild(mountNode); dialogApp = undefined; }, }); document.body.appendChild(mountNode); dialogApp.mount(mountNode); }); }在这个showPasswordDialog函数中我们先创建了一个div元素,再将弹窗组件传递给了createApp函数生成一个dialogApp的实例。然后将创建的div元素挂载到body上面,再调用mount方法将我们的弹窗组件挂载到创建的div元素上,至此我们实现了通过函数式调用将弹窗组件渲染到body中。现在我们再来看看传入到createApp函数的第二个对象参数,我们给这个对象分别传入了visible属性、close和confirm回调方法,分别会赋值给弹窗组件props中的visible、close、confirm。弹窗组件中触发关闭事件时会调用props.close?.(),实际这里就是在调用我们传入的close回调方法。在这个方法中我们调用了实例的unmount方法卸载组件,然后将创建的弹窗组件dom从body中移除,并且返回一个reject的Promise。当我们将账号和密码输入完成后,会调用props.confirm?.(ruleForm),这里的ruleForm就是我们表单中的账号和密码。实际这里就是在调用我们传入的confirm回调方法,接下来同样也是卸载组件和移除弹窗组件生成的dom,并且返回一个resolve值为账号密码表单的Promise。总结这篇文章主要介绍了如何创建函数式弹窗:创建一个常规的弹窗组件,有点不同的是close和confirm事件不是定义在emits中,而是作为回调定义在props中。创建一个showPasswordDialog函数,该函数返回一个Promise,resolve的值就是我们弹窗中输入的表单。调用createApp函数将步骤一的弹窗组件作为第一个参数传入,并且第二个对象参数中传入属性visible为true打开弹窗和注入弹窗close关闭和confirm确认的回调。使用者只需await调用showPasswordDialog就可以打开弹窗和拿到表单中填入的账号和密码。转载自https://www.cnblogs.com/heavenYJJ/p/17955954
  • [技术干货] 有了Composition API后,有些场景或许你不需要pinia了【转】
    前言日常开发时有些业务场景功能很复杂,如果将所有代码都写在一个vue组件中,那个vue文件的代码量可能就几千行了,维护极其困难。这时我们就需要将其拆分为多个组件,拆完组件后就需要在不同组件间共享数据和业务逻辑。有的小伙伴会选择将数据和业务逻辑都放到pinia中,这样虽然可以解决问题。但是如果将所有的复杂的业务都放在pinia中,那么pinia就会变得很乱。将数据和业务逻辑都封装到hooks中这时你还有另外一个选择,使用Composition API将数据和业务逻辑都抽取到hooks中。state状态的定义和更新以及具体的业务逻辑全部由hooks内部维护,组件只负责使用hooks暴露出的state状态和方法。下面是我们封装的hooks:export const useStore = () => { const count = ref(0); const doubleCount = computed(() => { return count.value * 2; }); function increment() { count.value = count.value + 1; } function decrement() { count.value = count.value - 1; } return { count, doubleCount, increment, decrement, }; };组件只需要使用hooks中暴露出的状态count和doubleCount,以及方法increment和decrement,无需关注具体的内部逻辑是如何实现的。上面的封装其实是有问题的,如果我们将组件拆为两个,分别为CountValue.vue(显示count的值)和CountBtn.vue(修改count变量值)。CountValue.vue组件代码如下:<template> <p>count的值是{{ count }}</p> <p>doubleCount的值是{{ doubleCount }}</p> </template> <script setup lang="ts"> import { useStore } from "./store"; const { count, doubleCount } = useStore(); </script>CountBtn.vue组件代码如下:<template> <button @click="decrement">count--</button> <button @click="increment">count++</button> </template> <script setup lang="ts"> import { useStore } from "./store"; const { decrement, increment } = useStore(); </script>由于我们的count变量是在useStore函数中定义的,所以每调用一次useStore函数都会重新定义一个count变量。在我们这里CountValue和CountBtn组件都在setup中调用了useStore函数,通过useStore函数拿到的就不是同一个count变量。这样就会导致我们在CountBtn中修改了count变量的值,但是CountValue组件中显示的count变量的值一直没变。多个组件同时调用hooks如何共享同一份state状态要解决上面的问题其实很简单,问题的原因是因为每次调用useStore函数都会生成一个新的count变量。那我们就不将count变量的定义写在useStore函数中,只需要将count变量的定义写在useStore函数的外面就可以了。下面是优化后的hooks:import { computed, ref } from "vue"; // 将count的定义放在外面 const count = ref(0); const doubleCount = computed(() => { return count.value * 2; }); export const useStore = () => { function increment() { count.value = count.value + 1; } function decrement() { count.value = count.value - 1; } return { count, doubleCount, increment, decrement, }; };我们将count变量定义放在了useStore函数的外面,这样CountValue和CountBtn组件中调用useStore拿到的count变量都是我们在useStore函数外面定义的count变量。总结这篇文章介绍了在多个组件中需要复用状态和业务逻辑的情况时,我们可以不将这些状态和业务逻辑写到pinia中,而是使用Composition API将状态和业务逻辑封装成一个hooks。为了多个组件同时调用hooks时能够共用同一个state状态,我们需要将定义的变量写在useStore函数外面。转载自https://www.cnblogs.com/heavenYJJ/p/17982518
  • [技术干货] Vue中的$attrs你真的会用吗?【转】
    先来看一个业务需求:项目经常会遇到产品经理要求你做某组件一样的功能,还要在它的基础上增加东西。如何只用少量代码高效的二次封装组件呢?例如我要做一个element-ui的input组件进行封装,以下是封装要求:对el-input组件增加某些定制功能调整el-input的原有css样式封装后不得更改原有el-input的所有功能文章最后会给出element-ui的input组件二次封装的示例。先来介绍一下attrs吧在 Vue2 中,attr 是指组件接收的 HTML 特性(attribute),通过 props 的方式传递给子组件。而在 Vue3 中,attr 的概念被引入了 Composition API 中,并且被用于管理各种配置项。下面介绍一些 attr 的使用技巧:Vue2 中使用 attr使用 v-bind指令绑定 HTML 属性在 Vue2 中,如果想将父组件传递的 HTML 属性传递给子组件进行使用,可以在子组件中通过 props 接收参数,并使用 v-bind 指令将其绑定到子组件的 HTML 元素上。例如:12345678910111213<template>  <div class="demo-component" :style="styleObject">{{ message }}</div></template><script>export default {  name: "DemoComponent",  props: {    message: String,    styleObject: Object,  },};</script>在父组件中使用该组件时,可以通过 v-bind 指令将 HTML 特性传递给子组件:123<template>  <demo-component message="Hello, world!" :style-object="{ color: 'red' }"></demo-component></template>使用 $attrs 对象传递所有未被 props 所接收的特性在 Vue2 中,可以通过 $attrs 对象获取父组件传递给子组件但未被 props 所接收的特性,从而实现组件复用和扩展的目的。例如:12345678910111213<template>  <div class="demo-component" :style="styleObject" v-bind="$attrs">{{ message }}</div></template><script>export default {  name: "DemoComponent",  props: {    message: String,    styleObject: Object,  },};</script>在父组件使用该组件时,可以像平常传递普通的 HTML 特性一样,同时还可以传递一些自定义的特性:1234567<template>  <demo-component    message="Hello, world!"    :style-object="{ color: 'red' }"    custom-attribute="something"  ></demo-component></template>在子组件中,可以通过 this.$attrs 属性获取父组件传递给子组件但未被 props 所接收的特性:1console.log(this.$attrs.customAttribute); // 输出:somethingVue3 中使用 attr在 Vue3 中,可以通过 setup 函数中的第二个参数 context 来访问该组件的配置选项,其中包括了所有未被 props 所接收的特性:12345678910111213141516<template>  <div class="demo-component" :style="styleObject" v-bind="$attrs">{{ message }}</div></template><script>export default {  name: "DemoComponent",  props: {    message: String,    styleObject: Object,  },  setup(props, context) {    console.log(context.attrs.customAttribute); // 输出:something  },};</script>在 setup 函数中,通过 context.attrs 获取父组件传递给子组件但未被 props 所接收的特性。除了 $attrs,Vue3 中还引入了 $props 对象,它是一个由 props 组成的响应式对象,在组件内部通过解构赋值可以快速地访问 props 的属性值:1234567891011121314151617<template>  <div class="demo-component" :style="styleObject">{{ message }}</div></template><script>export default {  name: "DemoComponent",  props: {    message: String,    styleObject: Object,  },  setup(props) {    const { message, styleObject } = props;    console.log(message, styleObject); // 输出:Hello, world! { color: 'red' }  },};</script>在 setup 函数中,通过解构赋值可以快速地访问 props 的属性值。利用 $attrs 和 $listeners 可以在二次封装 element-ui 组件时非常方便地传递组件属性和事件。示例代码下面以 el-input 组件为例,演示一下vue2中如何高效地二次封装 element-ui 组件,从而达到只用少量代码在原有组件上升级的效果:1234567891011121314151617181920<template>  <el-input v-bind="$attrs" v-on="$listeners" :class="{ 'is-invalid': isError }">    <template v-if="isError" #append>      <i class="el-input__icon el-icon-circle-close"></i>    </template>  </el-input></template><script>export default {  name: "MyInput",  inheritAttrs: false,  props: {    isError: Boolean, // 是否显示错误提示  },};</script><style scoped lang="scss">//这是写自己的样式内容</style>解释一下上面代码的内容吧:使用 v-bind="$attrs" 将父级组件所有的未被 props 所接收的特性绑定到 el-input 组件上。使用 v-on="$listeners" 将父级组件传递给当前组件的所有事件监听器绑定到 el-input 组件上。在模板中可以很方便地使用 isError 属性来扩展组件,并且不需要在父组件中再次定义。需要注意的是,由于 element-ui 组件本身也包含了一些默认的属性和事件,因此需要在组件中设置 inheritAttrs: false,以避免传递 element-ui 组件自带的属性和事件。本文转载于:https://juejin.cn/post/7221357811288260664
  • 你敢信?比 setTimeout 还快 80 倍的定时器【转】
    起因很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说:在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。在HTML Standard规范中也有提到更具体的:Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。用如下代码做个测试:12345678910111213141516171819202122232425let a = performance.now();setTimeout(() => {  let b = performance.now();  console.log(b - a);  setTimeout(() => {    let c = performance.now();    console.log(c - b);    setTimeout(() => {      let d = performance.now();      console.log(d - c);      setTimeout(() => {        let e = performance.now();        console.log(e - d);        setTimeout(() => {          let f = performance.now();          console.log(f - e);          setTimeout(() => {            let g = performance.now();            console.log(g - f);          }, 0);        }, 0);      }, 0);    }, 0);  }, 0);}, 0);在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。探索假设就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,在 MDN 文档的角落里有一些线索:如果想在浏览器中实现 0ms 延时的定时器,可以参考这里所说的window.postMessage()。这篇文章里的作者给出了这样一段代码,用postMessage来实现真正 0 延迟的定时器:12345678910111213141516171819202122232425(function () {  var timeouts = [];  var messageName = 'zero-timeout-message';  // 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。  function setZeroTimeout(fn) {    timeouts.push(fn);    window.postMessage(messageName, '*');  }  function handleMessage(event) {    if (event.source == window && event.data == messageName) {      event.stopPropagation();      if (timeouts.length > 0) {        var fn = timeouts.shift();        fn();      }    }  }  window.addEventListener('message', handleMessage, true);  // 把 API 添加到 window 对象上  window.setZeroTimeout = setZeroTimeout;})();由于postMessage的回调函数的执行时机和setTimeout类似,都属于宏任务,所以可以简单利用postMessage和addEventListener('message')的消息通知组合,来实现模拟定时器的功能。这样,执行时机类似,但是延迟更小的定时器就完成了。再利用上面的嵌套定时器的例子来跑一下测试:全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。测试从理论上来说,由于postMessage的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。设计一个实验方法,就是分别用postMessage版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。实验代码:1234567891011121314151617181920212223242526272829303132333435363738394041424344function runtest() {  var output = document.getElementById('output');  var outputText = document.createTextNode('');  output.appendChild(outputText);  function printOutput(line) {    outputText.data += line + '\n';  }  var i = 0;  var startTime = Date.now();  // 通过递归 setZeroTimeout 达到 100 计数  // 达到 100 后切换成 setTimeout 来实验  function test1() {    if (++i == 100) {      var endTime = Date.now();      printOutput(        '100 iterations of setZeroTimeout took ' +        (endTime - startTime) +        ' milliseconds.'      );      i = 0;      startTime = Date.now();      setTimeout(test2, 0);    } else {      setZeroTimeout(test1);    }  }  setZeroTimeout(test1);  // 通过递归 setTimeout 达到 100 计数  function test2() {    if (++i == 100) {      var endTime = Date.now();      printOutput(        '100 iterations of setTimeout(0) took ' +        (endTime - startTime) +        ' milliseconds.'      );    } else {      setTimeout(test2, 0);    }  }}实验代码很简单,先通过setZeroTimeout也就是postMessage版本来递归计数到 100,然后切换成 setTimeout计数到 100。直接放结论,这个差距不固定,在 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在硬件更好的台式机上,甚至能到 200 倍以上。Performance 面板只是看冷冰冰的数字还不够过瘾,打开 Performance 面板,看看更直观的可视化界面中,postMessage版的定时器和setTimeout版的定时器是如何分布的。这张分布图非常直观的体现出了上面所说的所有现象,左边的postMessage版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。而右边的setTimeout版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。作用也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。12345678910111213141516171819const channel = new MessageChannel();const port = channel.port2;// 每次 port.postMessage() 调用就会添加一个宏任务// 该宏任务为调用 scheduler.scheduleTask 方法channel.port1.onmessage = scheduler.scheduleTask;const scheduler = {  scheduleTask() {    // 挑选一个任务并执行    const task = pickTask();    const continuousTask = task();    // 如果当前任务未完成,则在下个宏任务继续执行    if (continuousTask) {      port.postMessage(null);    }  },};React 把任务切分成很多片段,这样就可以通过把任务交给postMessage的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。为什么不用执行时机更靠前的微任务呢?关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。总结可以了解如下几个知识点:setTimeout的 4ms 延迟历史原因,具体表现。如何通过postMessage实现一个真正 0 延迟的定时器。postMessage定时器在 React 时间切片中的运用。为什么时间切片需要用宏任务,而不是微任务。本文转载于:https://juejin.cn/post/7229520942668824633
  • [技术干货] 使用vue实现滑动滚动条来加载数据
    在vuejs中,我们经常使用axios来请求数据,但是有时候,我们请求的数据量很大,那么我们如何实现滑动滚动条来加载数据呢,接下来小编就给大家介绍一下在vuejs中如何实现滑动滚动条来动态加载数据,需要的朋友可以参考下实现思路首先,我们需要在vuejs中引入axios然后,我们需要从vue中,引入onMounted,onUnmounted生命周期钩子函数 然后,我们需要在onMounted函数中,进行监听 而在onUnmounted函数中,我们需要取消监听,解绑编写事件处理函数handleScroll, 获取变量scrollTop是滚动条滚动时,距离顶部的距离,获取变量scrollHeight是滚动条的总高度,获取变量clientHeight是滚动条可视区域的高度当滚动条到达底部,并且距离底部小于10px时,加载数据,也就是请求axios数据,页码++,重新加载数据函数为了防止用户频繁触发下拉滑动滚动条,往往需要添加一个函数防抖,在指定的时间内,只执行最后一次事件处理函数,避免频繁请求数据,给服务器造成压力代码实现 <template>     <div>           <div>             <el-button type="primary"  @click="handleBtnGetJoke">请求数据</el-button>             <el-button type="danger" @click="handleBtnClearData" v-if="aDatas.length > 0?true:false">清空数据</el-button>           </div>             <div>             <ul v-if="aDatas.length > 0?true:false">                 <li class="joke-list" v-for="item in aDatas"                                     :key="item.hashId">{{ item.content }}                 </li>                 <div class="loading" v-if="aDatas.length > 0?true:false">                      <el-button size="mini"  type="primary" @click="handleBtnLoading" >加载</el-button>                 </div>             </ul>            </div>     </div> </template>  <script setup> import axios from "axios"; import { ref,onMounted,onUnmounted  } from "vue";  let aDatas = ref([]); let page = ref(1); let pagesize = ref(20);  onMounted(() => {     // 获取数据     handleBtnGetJoke();     window.addEventListener('scroll', debounce(handleScroll,500)); })  onUnmounted(() => {     window.removeEventListener('scroll', handleScroll); })  // 事件处理函数 function handleScroll() {     // 变量scrollTop是滚动条滚动时,距离顶部的距离     const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;     // 变量scrollHeight是滚动条的总高度     const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;      // 变量clientHeight是滚动条可视区域的高度     const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;      // 当滚动条到达底部,并且距离底部小于10px时,加载数据        if (scrollTop + clientHeight - scrollHeight <= 10) {         page.value++;         handleBtnGetJoke();     } }  // 函数的防抖 function  debounce(method, duration) {          var timer = null;          return function(){             var that = this,                 args = arguments;             // 在本次调用之间的一个间隔时间内若有方法在执行,则终止该方法的执行             if(timer) {               clearTimeout(timer);             }             // 开始执行本次调用             timer = setTimeout(function(){               method.apply(that,args);             }, duration)           }    }  async function handleBtnGetJoke() {     const params = {         key: 'aa1b7ad08be2a09a4e0d2d46897e2dc8',         page: page.value,         pagesize:pagesize.value,         time: 1418816972     }      const response =  await axios.get('/api/joke/content/text.php',{params})     console.log(response);     if(response.status == 200) {         const { data } = response.data.result;         console.log(data);         aDatas.value = aDatas.value.concat(data);         if(page.value*pagesize.value >= data.length) {             alert("没有更多数据了")         }      } }  // 加载数据,叠加 function handleBtnLoading() {     page.value++;     handleBtnGetJoke();  }  // 清空数据 function handleBtnClearData() {     aDatas.value = []; }  </script>  <style scoped> .joke-list {     list-style-type: decimal;     list-style-position: outside;     padding: 5px 0;     border-bottom: dashed 1px #ccc; }  .loading {     margin: 0 auto;     text-align:center; } </style> 其中核心防抖函数如下所示,实现方式也很简单,就是利用定时器,在规定的时间内,如果再次触发,则清除定时器,重新开始计时。只执行最后一次 // 函数的防抖 function  debounce(method, duration) {          var timer = null;          return function(){             var that = this,                 args = arguments;             // 在本次调用之间的一个间隔时间内若有方法在执行,则终止该方法的执行             if(timer) {               clearTimeout(timer);             }             // 开始执行本次调用             timer = setTimeout(function(){               method.apply(that,args);             }, duration)           }    } 至于怎么样判断数据是否加载完毕,到最后一页每次在请求完成数据的时候去判断一下当前的 page × pagesize是否已经大于等于接口返回的total值就行了,也可以是pageNum 等于total 的时候,就说明已经没有数据了,可以提示用户了if(page.value*pagesize.value >= data.length) {             alert("没有更多数据了") } 总结其实这个功能很简单,但是写起来还是有点麻烦,因为涉及到异步请求,所以需要判断数据是否加载完毕,还要判断是否最后一页,还要判断是否还有数据,还要判断是否需要提示用户没有更多数据了,所以代码量还是挺多的,但是写完之后,感觉还是挺有成就感的。什么上拉,下拉刷新,下拉加载更多,其实原理都差不多,都是利用了防抖函数,然后利用定时器,在规定的时间内,如果再次触发,则清除定时器,重新开始计时。 实现方式都差不多转载自https://www.jb51.net/javascript/302873mq0.htm
  • [交流吐槽] 啥时候支持vue
    啥时候支持vue希望能尽快支持吧,java已经在用了