这篇文章仍然是分享前端一些常见功能的实现以及原理,主要是帮助大家扩展前端知识,主要的内容如下,下面的一些场景大家可能会在项目开发中遇到,也算是给大家提供解决问题的思路以及参考!
- 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;
通过源码可以看到:
- 它接收两个参数:一个是必传的业务上
Promise
实例;一个是可选的额外错误信息。 - 它的返回值永远是一个
Promise
,其泛型是一个length
为 2 的数组,第一项是错误信息、第二项是请求结果。 - 内部实现是通过调用传入的
Promise
实例,在then
和catch
中分别处理成功和失败最后将其结果放到数组并返回。在处理错误的时候如果有传入第二个参数则将其和错误信息通过Object.assign()
合并后放到数组中返回。 其实该库就是将Promise
的then
和catch
封装了一下,然后统一返回。
三、获取 URL 中的参数
1. 如何获取 URL 中的参数
假设我们有以下 URL:
js
https://www.example.com?name=Tom&age=18&gender=male
我们可以通过以下步骤来获取 URL 中的参数:
- 使用
window.location.search
:获取 URL 中的查询字符串,它包含了以问号?
开头的参数列表
js
const search = window.location.search; // "?name=Tom&age=18&gender=male"
- 使用
URLSearchParams
构造函数:将查询字符串解析为一个可迭代的对象,它包含了所有参数的键值对
js
const params = new URLSearchParams(search); // URLSearchParams { 'name' => 'Tom', 'age' => '18', 'gender' => 'male' }
- 使用
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()
函数,使其更加灵活和易用。如果将你想要继续了解的话,以下是一些可以考虑的改进:
- 支持多个参数名
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"
- 支持默认值:有时候,我们需要设置参数的默认值,以避免在参数不存在时返回
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"
- 支持 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());
迭代器返回的结果具有value
和done
属性,这使得我们可以使用迭代器来确保异步任务的顺序执行。
因此,我们的代码可以用迭代器进行如下组织:
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 请求拿到同样的结果,这样下来就可以不用每个组件都去调用接口。