扩展你的前端知识库,毫无废话(二)!

这篇文章仍然是分享前端一些常见功能的实现以及原理,主要是帮助大家扩展前端知识,主要的内容如下,下面的一些场景大家可能会在项目开发中遇到,也算是给大家提供解决问题的思路以及参考!

  • vite项目自动配置路由
  • await-to-js的源码解读
  • 获取 URL 中的参数
  • 如何终止forEach循环
  • forEach不能胜任异步任务
  • 多个相同组件重复请求

一、vite项目自动配置路由

1. 痛点分析

  • 项目过程中,你是否每加一个页面,都要添加路由的的烦恼?
  • 你是否想每加一个.vue文件,自动生成路由文件呢?

使用插件 unplugin-vue-router ,就可以解决你的这些烦恼。

2. 使用以及配置

js 复制代码
// vite.config.ts
import VueRouter from 'unplugin-vue-router/vite'

export default defineConfig({
  plugins: [
    VueRouter({
      routesFolder: 'src/views',
      exclude: ['**/components/*.vue'],
      extensions: ['.vue'],
    }),
    // ⚠️ 必须要放到Vue()之前
    Vue(),
  ],
})

具体的配置如下:

js 复制代码
VueRouter({
  // 自动生成路由的文件夹
  routesFolder: 'src/pages',

  // 生成路由的扩展名
  extensions: ['.vue'],

  // 要从路线生成中排除的文件列表
  // 例如['**/__*']将排除以'__'开头的所有文件和文件夹
  //例如['**/__*/**/*']将排除以'__'开头的文件夹中的所有文件
  //例如['**/*。component.vue']将排除以`.component.vue`的结尾的组件
  exclude: [],

  // 生成类型的路径
  // 可以通过设置 false 来禁用.
  dts: './typed-router.d.ts',

  // 自定义 <route> 模块
  routeBlockLang: 'json5',

  // 更改页面组件的导入方式,可以是"异步"、"同步"或函数:
  // (filepath: string) => 'async' | 'sync'
  importMode: 'async',
})
js 复制代码
// src/router/index.js   把vue-router替换为vue-router/auto
import { createRouter, createWebHashHistory } from 'vue-router/auto';

const router = createRouter({
  history: createWebHashHistory(import.meta.env.BASE_URL),
});

export default router;

3. 效果展示

文件夹的结构如下:

js 复制代码
src/views/
├── index.vue
├── about.vue
└── users.vue

这将生成以下路由:

  • /:->渲染 index.vue 组件
  • /about:->渲染 about.vue 组件
  • /users:->渲染 users.vue 组件

该插件更详细的用法请参考 这篇文章

二、await-to-js的源码解读

作者对该库的定义是: Async await wrapper for easy error handling. 它的用途也是用来解决在日常开发中使用 async + await 捕获错误时需要频繁定义 try catch 的问题。

1. 用法

js 复制代码
import to from 'await-to-js';

const [ err, res ] = await to(somePromise({userId: 'demoId', name: 'demoName'}));  

if (err) return console.error(err)
console.info(res)

它的用法也很简单,将业务上的请求传进去,然后会返回报错信息和结果,这里就省去了自己定义 try catch

2. 源码分析

js 复制代码
/**  
 * @param { Promise } promise  
 * @param { Object= } errorExt - Additional Information you can pass to the err object  
 * @return { Promise }  
 */  
export function to<T, U = Error> (  
  promise: Promise<T>,  
  errorExt?: object  
): Promise<[U, undefined] | [null, T]> {  
  return promise  
    .then<[null, T]>((data: T) => [null, data])  
    .catch<[U, undefined]>((err: U) => {  
      if (errorExt) {  
        const parsedError = Object.assign({}, err, errorExt);  
        return [parsedError, undefined];  
      }  
  
      return [err, undefined];  
    });  
}  
  
export default to;

通过源码可以看到:

  1. 它接收两个参数:一个是必传的业务上 Promise 实例;一个是可选的额外错误信息。
  2. 它的返回值永远是一个 Promise,其泛型是一个 length 为 2 的数组,第一项是错误信息、第二项是请求结果。
  3. 内部实现是通过调用传入的 Promise 实例,在 thencatch 中分别处理成功和失败最后将其结果放到数组并返回。在处理错误的时候如果有传入第二个参数则将其和错误信息通过 Object.assign() 合并后放到数组中返回。 其实该库就是将 Promisethencatch 封装了一下,然后统一返回。

三、获取 URL 中的参数

1. 如何获取 URL 中的参数

假设我们有以下 URL:

js 复制代码
https://www.example.com?name=Tom&age=18&gender=male  

我们可以通过以下步骤来获取 URL 中的参数:

  1. 使用 window.location.search:获取 URL 中的查询字符串,它包含了以问号 ? 开头的参数列表
js 复制代码
const search = window.location.search; // "?name=Tom&age=18&gender=male"  
  1. 使用 URLSearchParams 构造函数:将查询字符串解析为一个可迭代的对象,它包含了所有参数的键值对
js 复制代码
const params = new URLSearchParams(search); // URLSearchParams { 'name' => 'Tom', 'age' => '18', 'gender' => 'male' }  
  1. 使用 get() 方法来获取指定参数的值
js 复制代码
const name = params.get('name'); // "Tom"  
const age = params.get('age'); // "18"  
const gender = params.get('gender'); // "male"  

以上就是获取 URL 中参数的基本方法。但是,我们其实还需要考虑一些边界情况和异常处理。

2. 边界情况处理

  • 如果 URL 中没有任何参数,window.location.search 返回空字符串
js 复制代码
const search = window.location.search; // ""  
  • 如果 URL 中的参数名重复,URLSearchParams 对象会将它们合并为一个键值对
js 复制代码
const search = "?name=Tom&age=18&gender=male&name=Jerry";  
const params = new URLSearchParams(search); // URLSearchParams { 'name' => ['Tom', 'Jerry'], 'age' => '18', 'gender' => 'male' }  

我们可以使用 getAll() 方法来获取重复参数的所有值

js 复制代码
const names = params.getAll('name'); // ['Tom', 'Jerry']  
  • 如果需要获取的参数不存在,params.get() 方法会返回 null
js 复制代码
const hobby = params.get('hobby'); // null  

3. 封装函数

为了方便使用,我们可以将上述方法封装为一个通用的函数 getUrlParams(),它接收两个参数:URL 和参数名,返回对应的参数值。如果参数不存在,则返回 null

js 复制代码
function getUrlParams(url, key) {  
  const search = new URL(url).search;  
  const params = new URLSearchParams(search);  
  return params.get(key);  
}  

我们可以通过以下方式来使用 getUrlParams() 函数:

js 复制代码
const url = 'https://www.example.com?name=Tom&age=18&gender=male';  
const name = getUrlParams(url, 'name'); // "Tom"  
const hobby = getUrlParams(url, 'hobby'); // null  

4. 进一步完善

不过除了处理边界情况和异常情况,我们还可以进一步完善 getUrlParams() 函数,使其更加灵活和易用。如果将你想要继续了解的话,以下是一些可以考虑的改进:

  1. 支持多个参数名
js 复制代码
function getUrlParams(url, keys) {  
  const search = new URL(url).search;  
  const params = new URLSearchParams(search);  
  const result = {};  
  keys.forEach(key => {  
    result[key] = params.get(key);  
  });  
  return result;  
}  
   
const url = 'https://www.example.com?productId=123&productName=apple';  
const { productId, productName } = getUrlParams(url, ['productId', 'productName']);  
// productId: "123"  
// productName: "apple"  
  1. 支持默认值:有时候,我们需要设置参数的默认值,以避免在参数不存在时返回 null

为了实现这个功能,我们可以将参数名和默认值作为一个对象传递给 getUrlParams() 函数,它会返回一个包含对应参数值的对象。如果参数不存在,则返回默认值。

js 复制代码
function getUrlParams(url, options) {  
  const search = new URL(url).search;  
  const params = new URLSearchParams(search);  
  const result = {};  
  Object.keys(options).forEach(key => {  
    const defaultValue = options[key];  
    result[key] = params.get(key) || defaultValue;  
  });  
  return result;  
}  
   
const url = 'https://www.example.com?title=Hello%20World';  
const { title, description } = getUrlParams(url, { title: 'Default Title', description: 'Default Description' });  
// title: "Hello World"  
// description: "Default Description"  
  1. 支持 URL 对象:为了提高代码复用性,我们可以将 getUrlParams() 函数改为接收一个 URL 对象而非字符串。这样,我们就可以在不同的地方直接使用 window.location 而不需要额外的处理。
js 复制代码
function getUrlParams(urlObj, options) {  
  const search = urlObj.search;  
  const params = new URLSearchParams(search);  
  const result = {};  
  Object.keys(options).forEach(key => {  
    const defaultValue = options[key];  
    result[key] = params.get(key) || defaultValue;  
  });  
  return result;  
}  
   
const { title, description } = getUrlParams(window.location, { title: 'Default Title', description: 'Default Description' });  
// title: "当前页面的标题"  
// description: "Default Description"  

四、如何终止forEach循环

其实这个问题没啥意义,但是由于某些面试官会问到,因此在这里提供几种方法终止 forEach 循环,首先我们需要知道 return 和 break 都无法终止 forEach 循环

1. 抛出错误

js 复制代码
const array = [ -3, -2, -1, 0, 1, 2, 3 ]

try {
  array.forEach((it) => {
    if (it >= 0) {
      console.log(it) // 输出:0
      throw Error(`We've found the target element.`)
    }
  })
} catch (err) {
  
}

2. 将数组长度设置成0

js 复制代码
const array = [ -3, -2, -1, 0, 1, 2, 3 ]

array.forEach((it) => {
  if (it >= 0) {
    console.log(it) // 输出:0
    array.length = 0
  }
})

3. 使用splice移除数组元素

js 复制代码
const array = [ -3, -2, -1, 0, 1, 2, 3 ]

array.forEach((it, i) => {
  if (it >= 0) {
    console.log(it) // 输出:0
    array.splice(i + 1, array.length - i)
  }
})

五、forEach不能胜任异步任务

1. 问题复现

下面来看一段使用 forEach 遍历角色列表并执行异步操作的代码:

js 复制代码
async function processRoles() {
    let roles = ['admin', 'editor', 'employee'];
    roles.forEach(async role => {
            const result = await performTask(role);
            console.log(result);
    });
    console.log('任务处理完成');
}

function performTask(role) {
    return new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => {
                resolve(`已处理角色:${role}`);
        }, Math.random() * 1000);
    });
}

processRoles();

期望的输出结果:

复制代码
已处理角色:admin
已处理角色:editor
已处理角色:employee
任务处理完成

然而,实际的输出结果却是不确定的,可能会类似于以下情况:

复制代码
任务处理完成
已处理角色:employee
已处理角色:admin
已处理角色:editor

😕 那有没有办法教教它乖乖排队呢?让我们看看该如何处理。

2. forEach的原理

forEach 凭啥"随心所欲",要理解这个问题的原因,我们需要了解 forEach 方法的底层实现方式。

js 复制代码
for (var i = 0; i < length; i++) {
  if (i in array) {
    var element = array[i];
    callback(element, i, array);
  }
}

forEach方法直接遍历数组并执行回调函数,无法保证异步任务的执行顺序。如果后面的任务执行时间较短,就可能在前面的任务之前完成执行。

3. 解决方案

为了解决这个问题,我们可以采用更可靠的方法,即使用 for...of 循环来确保异步任务按照预期顺序执行。

js 复制代码
async function processRoles() {
    let roles = ['admin', 'editor', 'employee'];
    for (const role of roles) {
        const result = await performTask(role);
        console.log(result);
    }
    console.log('任务处理完成');
}

通过使用for...of循环,我们可以确保异步任务按照预期顺序执行,避免了不确定性。

4. 使用迭代器

for...of循环实际上是基于迭代器(Iterator)的遍历方式。对于数组来说,它是一种可迭代对象,可以通过迭代器进行遍历。

我们可以通过以下方式获取数组的迭代器:

js 复制代码
let roles = ['admin', 'editor', 'employee'];
let iterator = roles[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

迭代器返回的结果具有valuedone属性,这使得我们可以使用迭代器来确保异步任务的顺序执行。

因此,我们的代码可以用迭代器进行如下组织:

js 复制代码
async function processRoles() {
    let roles = ['admin', 'editor', 'employee'];
    let iterator = roles[Symbol.iterator]();
    let res = iterator.next();
    while(!res.done) {
        let value = res.value;
        console.log(value);
        console.log(await performTask(value));
        res = iterator.next();
    }
	console.log('任务处理完成');
}
processRoles()

通过以上例子重新认识生成器(Generator)作为迭代器的特性,我们可以更深入地理解了for...of循环的原理和工作方式。

六、多个相同组件重复请求

在页面开发的时候, 如果一个页面中有多个相同的组件, 那么该组件每次 onMounted 的时候都会去调用对应的 api,就会造成类似这样的后果多次请求了同一个api,这里只针对相同参数的请求做处理!

1. 方式一

使用单例模式,基本思路:

  • 有缓存拿缓存
  • 没缓存判断如果是第 1 个请求的,就去请求远端
  • 如果不是第 1 个请求的,就等
js 复制代码
let cache = null;
let count = 0;

async function delay(ms = 200) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

export async function getSignature() {
    if (cache) { return cache; }

    if (count++) {
        // 如果有计数说明自己不是第 1 个,就等。注意这里判断的是加之前的 count
        // 循环里最好再加个超时判断
        while (!cache) { await delay(); }
    } else {
        // 是第 1 个就去请求
        // 如果这里有可能会抛异常,抛异常也不要漏了 count--
        // (这个示例代码没做容错,自己加)
        cache = await fetchSignature();
    }

    count--;    // 记得减回去,方便以后如果要刷新 cache 的时候用
    return cache;
}

2. 方式二

同样使用的是单例模式, 不过这里是异步单例模式 关于异步单例模式,我觉得可以参考这篇文章高级异步模式 - Promise 单例

js 复制代码
 function getSomething() {
     return new Promise(resolve => {
         const result = fetch('http://hn.algolia.com/api/v1/search?query=vue').then(val => resolve(val.json()))
     })
 }

 let myfetch = null
 async function getData() {
     if (myfetch) return myfetch
     myfetch = getSomething()
     console.log(345);
     try {
         const result = await myfetch   // 这里一定要 await myfetch
         console.log(456);
         myfetch = null
         return result
     } catch (err) {
         console.err('err')
     }
 }

 const result1 = getData()
 const result2 = getData()
 const result3 = getData()
 const result4 = getData()

 console.log(123);
 console.log(result1, 'result1');        // Promise {<pending>} 'result1'
 console.log(result2, 'result2');        // Promise {<pending>} 'result2'
 console.log(result3, 'result3');        // Promise {<pending>} 'result3'
 console.log(result4, 'result4');        // Promise {<pending>} 'result4'
 setTimeout(() => {
     console.log(result1, 'result1');     // Promise {<fulfilled>: {...}} 'result1'
     console.log(result2, 'result2');     // Promise {<fulfilled>: {...}} 'result2'
     console.log(result3, 'result3');     // Promise {<fulfilled>: {...}} 'result3'
     console.log(result4, 'result4');     // Promise {<fulfilled>: {...}} 'result4'
 }, 1500)

第一次调用的时候, 没有myfetch , 就去调用接口, 此时的 myfetch 是 pending 状态,因为是多个组件同时调用 getData() 函数, 但是我们在第一次的时候已经给 myfetch 赋值了,所以调用 getData() 函数返回的都是 pending 状态的 myfetch , 等到第一个请求的结果拿到了, 那么其他组件也就拿到了这个结果,所以就不用每个组件都去走一次 http 请求拿到同样的结果,这样下来就可以不用每个组件都去调用接口。

相关推荐
前端大卫4 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘20 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare21 分钟前
浅浅看一下设计模式
前端
Lee川24 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端