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

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

  • 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 请求拿到同样的结果,这样下来就可以不用每个组件都去调用接口。

相关推荐
小牛itbull6 分钟前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress
请叫我欧皇i14 分钟前
html本地离线引入vant和vue2(详细步骤)
开发语言·前端·javascript
533_17 分钟前
[vue] 深拷贝 lodash cloneDeep
前端·javascript·vue.js
guokanglun23 分钟前
空间数据存储格式GeoJSON
前端
GIS瞧葩菜26 分钟前
局部修改3dtiles子模型的位置。
开发语言·javascript·ecmascript
zhang-zan1 小时前
nodejs操作selenium-webdriver
前端·javascript·selenium
ZBY520311 小时前
【Vue】 npm install amap-js-api-loader指南
javascript·vue.js·npm
猫爪笔记1 小时前
前端:HTML (学习笔记)【2】
前端·笔记·学习·html
brief of gali1 小时前
记录一个奇怪的前端布局现象
前端
前端拾光者2 小时前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化