首页
小游戏
壁纸
留言
视频
友链
关于
Search
1
上海市第八人民医院核酸检测攻略(时间+预约+报告)-上海
299 阅读
2
上海烟花销售点一览表2022-上海
241 阅读
3
新款的 Thinkbook 16+ 值不值得买?-知乎热搜
219 阅读
4
如何看待网传小米 MIUI 13 内置国家反诈中心 APP?-知乎热搜
214 阅读
5
窦唯到底厉害在哪里?-知乎热搜
192 阅读
免费代理IP
免费翻墙节点
文章聚合
掘金
知乎
IT之家
本地宝
观察者网
金山词霸
搜韵网
新华网
其他
登录
/
注册
Search
标签搜索
知乎热搜
IT之家热榜
广州
深圳
北京
观察者网头条
前端
上海
后端
知乎日报
Android
iOS
人工智能
阅读
工具资源
杭州
诗词日历
每日一句
郑州
设计
看啥
累计撰写
129,720
篇文章
累计收到
46
条评论
首页
栏目
免费代理IP
免费翻墙节点
文章聚合
掘金
知乎
IT之家
本地宝
观察者网
金山词霸
搜韵网
新华网
其他
页面
小游戏
壁纸
留言
视频
友链
关于
搜索到
1775
篇与
的结果
2022-10-20
不用防抖和节流,用更底层的方式解决JS的重复请求-掘金
欢迎转载,评论,然后注明来源和github地址即可。如果你认为本文或本工具对你有帮助,请点赞收藏或给项目star,这对我真的很重要。ヾ(•ω•`)o项目 github 地址你或许在项目中遇到过这样的情况。 成员A成员B都用得上一个后端接口api,但它们互相不知道对方什么时候请求这个接口,因此导致一打开页面,同一个接口竟然重复请求了多次。 由于用户手抖,又因为成员忘记做请求的loading防误触处理,导致一个接口被用于疯狂请求,最终数据乱套,页面不可用。 SPA单页面应用,多个页面甚至是多个组件可能有同样的数据请求,完全可以共享的数据却不得不重复请求,影响页面加载效率。 想要用节流或者防抖解决上面的问题,但是后端返回数据的时间浮动太大,导致不知道应该设置多长的时间。 这些请求浪费,实际上都有调用异步函数(async function)的参与的;因此,它们虽不是async function的问题,但却可以利用async function的特点来解决。async function本质上是一个Promise。因此只要利用好Promise的特性,就能解决这些问题。once-init 正是为解决这些问题而生。它从 Promise 的定义出发,用 Promise 的基础功能彻底地阻止了异步请求浪费的发生。我用它做了两件事: 缓存请求的返回值; 缓存Promise请求本身; 原理once-init 的核心思想是缓存和执行队列;缓存返回值实现缓存返回值并不困难,只要写一个单例模式就好了。下面是一个缓存的单例模式的简单示例;class OnceInit { cache = undefined; func; constructor(asyncFunc) { this.func = asyncFunc; } async init() { if (typeof this.cache !== 'undefined') { return this.cache; } else { const res = await this.func(); this.cache = res; return res; } } } // 使用 const oiFoo = new OnceInit(someAsyncFunc); await oiFoo.init(); 如果缓存已经有值,返回缓存的值; 如果缓存没有值,执行异步函数;执行完毕后,更新缓存; 这是一个简易的解决方案,它大概能解决10%的异步函数相关的问题,因为在第一次执行Promise完成之后,就不会再进行请求,也就不会产生浪费了;但是,它没有解决多个Promise同时发生的情况。假设开发人员同一时间多次调用init,如果第一次调用的Promise还没有完成,cache也还没有初始化,就会导致同一时间的所有调用依旧创建新的Promise。甚至有可能因为多次请求,不断的变化cache,你甚至没有办法确定最后cache的值是不是你最后一次请求的返回值。如果要解决这个问题,就需要利用Promise的特性,同一时间,同一个async function,只允许同一个Promise处在pending状态。缓存 Promise 如果Promise正在执行,就不创建新的Promise;直接返回正在执行的Promise的返回值; 如果没有Promise正在执行,就创建并缓存新的Promise; Promise执行结束之后,删除缓存的Promise; class OnceInit { cache = undefined; func; promise = undefined; constructor(asyncFunc) { this.func = asyncFunc; } async init() { if (typeof this.cache !== 'undefined') { return this.cache; } else { if (this.promise) { return await this.promise; } else { const promise = this.func(); promise.finally(() => { this.promise = undefined; }) this.promise = promise; const res = await promise; this.cache = res; return res; } } } } 通过这种方式,就能避免promise同一时间重复执行。这也是once-init这个库的核心思想。当然这个简单实现还有很多问题需要解决。 如果想要刷新缓存怎么办; 如果asyncFunc需要参数怎么办; 怎样提供Typescript支持; 不过这些问题 once-init 都已经解决了。如果你读过我的上一篇文章https://juejin.cn/post/7046667393405304868,就知道我在年初就写了一个库封装这个想法。然而很多伙伴都对我的实现提出了有效的建议,经过这一段时间的打磨,并在实际生产环境中试用了一段时间后,现在终于推出了它的v1.0.0正式版本了。once-init 🗼 Makes asynchronous function execution manageable.封装可控的 async function。你可以让同一个 async function 不会在同一时间内被执行两次,以防止发出重复的请求。你可以让第二次执行 async function ,直接返回第一次执行的结果,而不是重复执行函数。解决大量的相同请求的问题。详细且精确的 Typescript 检查。安装npm install once-init 简介once-init 的核心思想是缓存和执行队列;使用// 0. 引入once-init import oi from "once-init"; // 1. 创建一个异步函数 async function foo() { // do something, for example, request backend data. const res = await axios.get("xxx.com"); return res; } // 2. 用once-init封装这个异步函数 const oiFoo = oi(foo); // 3. 执行封装后的函数 oiFoo.init(); 用例一个结合axios的简单示例import oi from "once-init"; axios.get = oi(axios.get).refresh; 只用一行,就能在调用axios.get的时候就能阻止同一时间的重复请求了。不用 once-init// 我们假设 axios.get("xxx.com") 返回的值是一个递增的数字,即第1次请求,会返回1,第2次请求会返回2,第n次请求会返回n。 await foo(); // 返回 1 await foo(); // 返回 2 await foo(); // 返回 3 使用 once-init// once-init 会将重复执行重定向到第一次执行的结果上;(第一次执行后会缓存执行结果,类似单例模式) await oiFoo.init(); // 返回 1 await oiFoo.init(); // 返回 1 await oiFoo.init(); // 返回 1 这意味着无论重复执行 oiFoo.init 多少次,foo 都只会执行第一次,返回第一次执行的结果;(就像缓存一样)await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] await Promise.all([oiFoo.init(), oiFoo.init(), oiFoo.init()]); // 返回 [1, 1, 1] // 通常,如果你只会使用到init,你可以直接把 oiFoo 定义成 init 函数 const oiFoo = oi(foo).init; await oiFoo(); 如果你不使用缓存,只是希望防止同一时间发出重复请求,你可以使用refresh:// refresh和init在同一时间执行多次,都会阻止重复执行,多余的async function会返回第一次的结果; await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [1, 1, 1] // 但refresh如果当前没有其它重复的async function在执行,会刷新结果,并同时刷新缓存(影响到下一次init的返回); await Promise.all([oiFoo.refresh(), oiFoo.refresh(), oiFoo.refresh()]); // 返回 [2, 2, 2] await oiFoo.init(); // 返回 2 once-init 会区分参数,如果传入的异步函数有参,那么传入不同的参数将被视为两个不同的异步函数,不会共享缓存和执行队列;(使用lodash.isEqual判断参数是否相等)下面这个复杂用例将会给你提供灵感:// 假设 xxx.com/+ 会返回正数, xxx.com/- 会返回负数,两者有独立的缓存,且绝对值都递增 async function foo(op: "+" | "-") { const res = await axios.get(`xxx.com/${op}`); return res; } const oiFoo = oi(foo); await oiFoo.init("-"); // 返回 -1 await oiFoo.refresh("-"); // 返回 -2 await oiFoo.refresh("-"); // 返回 -3 await oiFoo.refresh("+"); // 返回 1 await oiFoo.init("-"); // 返回 -3 更多问题和api请到项目 github 地址中查看,如果有任何问题,也请在下面评论留言或到github提交issue(热烈欢迎到github提交issue捏)。以上文章来自[掘金]-[Xmo]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-20
🌼 细数那些惊艳一时的 CSS 属性-掘金
以上文章来自[掘金]-[CatWatermelon]本程序使用github开源项目RSSHub提取聚合!
2022年10月20日
0 阅读
0 评论
0 点赞
2022-10-19
用好 TypeScript,请再深入一些-掘金
TypeScript 已经成为前端编程语言的事实标准。但我从大量的 Code Review 和面试经历中发现,真正能深入使用 TypeScript 的开发其实并不多。如果你不知道 ReturnType 的作用和实现,或许这篇文章也适合你。当然,我们花大量时间去学习一门语言或技术并非为了追求奇技淫巧,而是出于务实的态度。如果你看过大量使用 any 的 TypeScript 的代码,你肯定会感叹“那还不如不用”。在此种情况下,使用 TypeScript 的成本大于它所带来的收益。如何充分利用好 TypeScript 的语言特性,帮助自己写出更健壮、类型提示更友好的代码?那就需要"再深入一些"。一个简单的例子假设有以下的 TypeScript 代码,你会如何添加类型信息呢?function prop(obj, key) { return obj[key]; } 上面的函数接收一个对象和一个 key,返回对应的属性值。我们也许可以尝试这样写:function prop(obj: {}, key: string) { return obj[key]; } 我们限定了 obj 必须是个对象,key 必须是一个字符串,但返回值呢?假如我们这样使用该函数:const todo = { id: 1, text: "Buy milk", due: new Date(2016, 11, 31), }; const id = prop(todo, "id"); // any const text = prop(todo, "text"); // any const due = prop(todo, "due"); // any 因为 JavaScript 是高度动态的语言,我们没办法预知 obj 的具体类型和key的值,也就没办法知道返回值的类型,所以 TypeScript 将返回值推断为 any。如果我们想要得到更精确的返回值类型,那该如何解?想一想:上面我们用 {} 来约束 obj 必须是一个对象,那你知道在 TypeScript 中,{}、object、Object 三种类型的区别吗,它们分别用在什么场景?keyof 操作符keyof 是 TypeScript 2.1 版本增加的操作符,我们用它来解决上面的问题。interface Todo { id: number; text: string; due: Date; } type TodoKeys = keyof Todo; // "id" | "text" | "due" 从上面的示例代码中可以看到,keyof 作用于类型 Todo,可以得到 Todo 中所有 key 的联合类型,即上面的 "id" | "text" | "due"。有了 keyof 的助力,我们可以改写 prop 方法:function prop(obj: T, key: K) { return obj[key]; } 上面的函数签名中,key 的类型 K 不再是任意的字符串,而是要求必须存在于 obj 对象属性名的联合类型中。想一想:停下来思考一下 K extends keyof T 这种写法,尝试用自己的话描述它的含义。如果说不清楚,原因很可能是对 extends 关键字的理解不够清晰。上面的代码中,obj 的类型是 T,key 的类型是 K,那么返回值 obj[key] 的类型会被推断为 T[K],在 TypeScript 中这被称为 Indexed Access Types。现在我们重新通过 prop 函数来访问 todo 的属性:const todo = { id: 1, text: "Buy milk", due: new Date(2016, 11, 31), }; const id = prop(todo, "id"); // number const text = prop(todo, "text"); // string const due = prop(todo, "due"); // Date 可以看到,现在返回值类型能被正确推断,而不是笼统的 any 类型。另外,正因为我们限定 K 必须存在于 obj 属性名的联合类型中,所以传入其他 key 值,TS 会报错,代码会更健壮:const none = prop(todo, 'none'); // [ts] Argument of type 'none' is not assignable to parameter of type '"id" | "text" | "due"'. 想一想:如何用刚学的这些知识点,来给 JavaScript 语言内置的 Object.entries() 方法补充类型签名?聪明的你可以打开 VSCode 看一看。第二个简单的例子你觉得下面的代码存在什么问题?interface Point { x: number; y: number; } interface FrozenPoint { readonly x: number; readonly y: number; } function freezePoint(p: Point): FrozenPoint { return Object.freeze(p); } const origin = freezePoint({ x: 0, y: 0 }); // Error! Cannot assign to 'x' because it // is a constant or a read-only property. origin.x = 42; 表面上看我们确实实现了想要的效果,我们冻结了一个对象,修改冻结对象的属性会报错,一切似乎都没问题。但真的吗?至少存在两个缺陷: 我们需要定义两个 interface,并且每当 Point 类型有修改,对应的 FrozenPoint 也要修改,这样做不但修改成本高,也很容易遗漏导致出错; 对于每一个需要 Object.freeze 的对象,我们都要封装一个类似 freezePoint 这样的函数来得到正确的返回值类型,实在太繁琐。 那解法又是什么?映射类型TypeScript 中的映射类型允许你通过映射已有类型的每个属性来创建新的类型。我们直接来看 Object.freeze() 方法的类型签名:freeze(o: T): Readonly; 这里的 Readonly 返回类型就是一个映射类型,它的定义如下:type Readonly = { readonly [P in keyof T]: T[P]; }; 在讲述过第一个例子后,你对上面代码中的 keyof T,T[P] 写法应该不再陌生,剩余的关键点是属性方括号中的 in 操作符,正是它告诉我们这是一个映射类型。[P in keyof T]: T[P] 意味着类型 T 的每一个 P 属性类型都应该转换为 T[P] 类型。如果没有 readonly 修饰符,这种转换后的类型和之前是一样的。想一想:readonly 修饰符可以约束属性是只读的,那你知道 TypeScript 中还有哪些属性修饰符吗?如果还是不能理解上面的代码,可以看看下面的转换过程(转换过程只是为了解释,并非 TypeScript 中的具体算法):// 泛型参数 T 我们传入了上面定义的 Point 类型,得到 ReadonlyPoint 类型 type ReadonlyPoint = Readonly; // 等价于 type ReadonlyPoint = { readonly [P in keyof Point]: Point[P]; }; // 进一步,我们展开 keyof Point type ReadonlyPoint = { readonly [P in "x" | "y"]: Point[P]; }; // P 代表了 x 和 y 属性,我们可以分别声明,进而去掉映射类型语法: type ReadonlyPoint = { readonly x: Point["x"]; readonly y: Point["y"]; }; // 最后我们可以用 number 代替 T[P] 形式的查询类型: type ReadonlyPoint = { readonly x: number; readonly y: number; }; 而这就是我们最初代码中定义的 FrozenPoint 类型。 从上面的代码中,我们得到的 ReadonlyPoint 和 FrozenPoint 类型是完全一样的。但使用 Readonly 映射类型避免了上文中提到的两个问题。想一想:Readonly 映射类型是 TS 的内置类型,除此之外,TS 还定义了哪些映射类型?你能整理出所有的内置映射类型并弄明白它们的作用和实现吗?回到最初的问题文章开头我们有提到 ReturnType,它的作用和实现是什么?这次我们不卖关子,直接给出代码。它的用法:type A = ReturnType string>; // string type B = ReturnType () => any[]>; // () => any[] type C = ReturnType; // number type D = ReturnType; // boolean 它的定义:/** * Obtain the return type of a function type */ type ReturnType any> = T extends ( ...args: any[] ) => infer R ? R : any; 从用法中我们可以看到,ReturnType 接收一个函数类型参数,然后推断出该函数类型的返回值类型,比如它推断出 Math.random 函数的返回值类型是 number。想一想:为什么是 ReturnType,而不是 ReturnType ?我们再来看看它的实现。乍看之下你可能会觉得很复杂,因为它涉及到了 Typescript 这门语言中最难的部分,也就是所谓的类型编程。Typescript 的类型编程本身是图灵完备的,比如你可以使用三元运算符来决定使用哪个类型。T extends U ? X : Y 上面的表达式,在 TS 中被称为条件类型。条件类型使用了熟悉的 ... ? ... : ... 语法,它在 Javascript 中被用于条件表达式。T, U, X 和 Y 代表了任意类型。 其中 T extends U 描述了类型关系测试。如果条件满足,类型 X 被选择,否则类型 Y 被选择。如果使用人类语言,条件类型可以描述如下:如果类型 T 可以赋值给 U,选择类型 X,否则选择类型 Y。此时我们重新回过头来看 ReturnType 的实现,是不是更容易理解了?ReturnType 的泛型参数我们约束为函数类型,如果 T 真满足约束,那么我们可以通过 infer 关键字推断函数返回值类型 R,否则就返回 any 类型。想一想:我们既然可以通过条件类型推断函数的返回值类型,那是不是同样可以推断函数的入参类型呢?聪明的你可以在 VSCode 里试试 Parameters 类型。既然我们已经知道了映射类型和条件类型,那么两者结合起来怎么样?type NonNullablePropertyKeys = { [P in keyof T]: null extends T[P] ? never : P; }[keyof T]; 你能按照第二个例子中的推导过程,推导以下代码中 NonNullableUserPropertyKeys 的最终类型吗?type User = { name: string; email: string | null; }; type NonNullableUserPropertyKeys = NonNullablePropertyKeys; 想一想: NonNullablePropertyKeys 的定义中用到了 never 关键字,你知道它的含义吗?另外,你能用自己的话说清楚 any 和 unknown 类型的区别吗?结语这篇文章并不想就 TypeScript 的某个主题深入讲解,而是提出这样一个事实:TypeScript 的类型使用远比想象中复杂。为什么会这样?因为 JavaScript 是动态的,很多类型只能通过文中提到一些方法来描述。当然并不是说你掌握了 keyof 操作符、映射类型、条件类型就万事大吉了,如果你真的想使用好 TypeScript,必须再深入一些,彻底掌握 TS 的类型编程。我这里推荐两份学习资料: 我之前翻译的 TypeScript Evolution 系列,讲解了很多 TypeScript 2.0 以来的重要特性,文章中的例子也都来自于此,掘金专栏:https://juejin.cn/column/7026521661352607781 神光的 "TypeScript 类型体操通关秘籍" 小册,小册地址:https://juejin.cn/book/7047524421182947366 当你能轻松回答所有“想一想”中的问题,你会感受到成长的喜悦,祝你学习愉快!参考资料: https://mariusschulz.com/blog/keyof-and-lookup-types-in-typescript https://mariusschulz.com/blog/conditional-types-in-typescript https://mariusschulz.com/blog/mapped-types-in-typescript 以上文章来自[掘金]-[chaosflutter]本程序使用github开源项目RSSHub提取聚合!
2022年10月19日
0 阅读
0 评论
0 点赞
2022-10-19
想写出复用性强的组件?快来试试 Storybook 吧!-掘金
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情前言最近笔者通过学习和使用 Storybook 时,记录了一些使用心得,写下这篇文章和大家分享一下,笔者主要通过官方文档进行学习:Storybook官方文档简介Storybook 是 UI 组件的开发环境,它允许开发者浏览组件库,查看每个组件的不同状态,以及交互地开发和测试组件。Storybook 可帮助你记录组件以供重用,并自动对组件进行可视化测试以防止出现错误。开始在对 Storybook 有了大致的了解之后,下面就让我们把 Storybook 启动起来吧!Storybook 需要安装到已经设置了框架的项目中。它不适用于空项目。这里我使用的是 React,首先我们进入空项目安装 React,这里推荐使用官方脚手架安装:npx create-react-app my-app 安装完成后进入项目目录:cd my-app 之后再安装 Storybook,命令如下:npx storybook init 安装完成后将 Storybook 启动:npm run storybook 启动成功后,页面如下:在该项目的 src 下,我们可以看到 stories 文件夹,里面有一些后缀为 .stories.jsx 的文件,分别对应着页面中的 Button、Header 和 Page 组件。这些 story 以格式 (CSF) 编写——这是一种基于 ES6 模块的标准,用于编写组件示例。组件我们以 Button 组件为例进行分析,默认情况下,页面上的 Button 组件如下图:可以看到 Canvas 部分用来实时显示组件,Controls 用来控制组件,例如可以改变按钮的背景颜色、大小等。当我们点击按钮时,在 Actions 中会打印点击事件,如下图显示:现在我们大致清楚了 Storybook 是可以对组件进行一些调试的,那么如果我们需要设计一些可以高度复用的组件,该怎么通过 Storybook 来设计呢?对于开发者来说,设计出高度复用的组件可以为以后的开发节约大量的时间,写出更优质的组件,这样的组件具有高内聚、低耦合的特点,在大型项目中写出这样的组件则显得尤为重要!在需求中,设计按钮组件时,有固定因素和可变因素,例如按钮的大小,颜色以及按钮的内容等等都是可变因素;固定因素有比如按钮的背景图片。保留固定元素将可变因素进行动态转化可以达到组件高度重用的目的。在 Button.stories.jsx 中,我对默认代码进行了修改,通过 args 对组件 Button 进行传参,为了方便理解,其中 args 只包括了 primary 和 label 两个参数(即是否为主要按钮和按钮内容),代码如下:import React from 'react'; import { Button } from './Button'; // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export export default { title: 'Example/Button', component: Button }; // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args const Template = (args) => ; export const Primary = Template.bind({}); // More on args: https://storybook.js.org/docs/react/writing-stories/args Primary.args = { primary: true, label: 'Button' }; Template.bind({}) 是一个标准 JavaScript 技术用于制作函数的副本。我们复制它,Template 让每个导出的 story 都可以在其上设置自己的属性。通过将 args 引入组件的 story 中,不仅可以减少代码量,而且还减少了数据重复。在 Button.jsx 中,我们将传入的参数进行解构,这里直接解构出 primary 和 label,对其做了类型约束:import React from 'react'; import PropTypes from 'prop-types'; import './button.css'; /** * Primary UI component for user interaction */ export const Button = ({ primary, label }) => { const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; return ( {label} ); }; Button.propTypes = { /** * Is this the principal call to action on the page? */ primary: PropTypes.bool, /** * Button contents */ label: PropTypes.string.isRequired }; Button.defaultProps = { primary: false }; 页面如下图:可以看到,Controls 中只保留了 primary 和 label,这是因为我只对这两个参数进行了相关操作(定义、传递以及类型约束)。通过这样的方法,把可变因素当作参数进行传递,组件的复用性是不是大大提高了呢,其实还可以进一步提高,从上述代码来看 Button 组件的样式是写在其他文件的,我们可以把样式写在 Button.jsx 中,这样一个文件就对应了一个组件,便于管理。样式编写我使用的是 Emotion,能够实现 css in js,官方地址如下:Emotion,推荐使用 @emotion/styled通过这种方法来编写组件,追求高复用性,养成一种编写优质代码的习惯,是不是非常的nice呢!总结以上就是笔者在阅读文档以及使用 Storybook 后的一些记录,也是笔者对 Storybook 的一些个人见解,如有不足欢迎大家指出,如果大家觉得还可以的话,不要忘了点赞哟~,谢谢!以上文章来自[掘金]-[codinglin]本程序使用github开源项目RSSHub提取聚合!
2022年10月19日
0 阅读
0 评论
0 点赞
2022-10-19
前端的焦虑,你想过30岁以后的前端路怎么走吗?-掘金
曾几何时,我总会很庆幸自己进了前端这个行业。因为在这个职业范畴里面,我如鱼得水,成长很快,成就感满满。然而,随着年龄和工龄的增长,渐渐发现自己的瓶颈越来越明显了,我感觉自己似乎碰到了前端的天花板。原因何在 1.从客观原因来看,前端相对于后端的入门门槛确实低了不少。公司对前端的需求量虽然很旺盛,但是对前端的技术能力要求却不是很高,特别是一些小公司或者不是技术驱动的公司。这给人一种错觉,好像只需要懂一些js,会一般的html+css就能完成前端的工作。也由于这种原因,前端总是处于技术鄙视链的最底层。 2.从主观原因来说,前端平时基本都是和页面和看得到的UI打交道居多,对于后端的服务,数据存储,运维,部署等等懂得的不多,也导致了领导我们的往往都是后端。在大多数的情况下,你基本很难看到前端去统筹大局,统领前后端。 3.从个人原因来总结,前端经验上去了,工作年限上去了,但是职级却没有上去。归根结底,主要是因为自己的后端知识薄弱,前端深度不够。还有前端管理的职位僧多粥少导致的。居于上述的原因,前端的天花板来得比别的技术栈更早。这也是导致我们焦虑的主要原因。既然有原因,那就可以找相应的解决方法。 解决方法 1.对症下药,哪里缺乏补哪里。前端的进阶,总离不开对后端的认知。我们不能把自己限死在前端这个范畴里面。业务驱动技术,而不是技术引导业务。不懂数据库,补数据库。不懂服务端,补服务端。幸好现在有nodeJs这个利器。我们完全可以借用nodejs,去切入后端的世界,了解和学习后端的知识。做到不受语言的限制,学习应用,也就能突破自己的瓶颈。除了node,php也是一个不错的选择。 2.主动创造条件。很多时候,选择比努力更重要。如果你发现你在一个地方再怎么努力也改变不了现状,这个时候你就应该出去别的地方看看,或者想想怎样改变现状。如果你无法升管理,那你可以尝试去别的地方当管理;如果你总是厌倦天天的无止境的切图和coding,但是又有很多想法,转岗去尝试当产品也是一个选择。 3.大前端和全栈是以后前端的一个趋势,懂后端的前端,懂各端的前端更加具有竞争力,以后可以往这个方向靠拢。现在脑补一下前端知识体系的脑图。 接下来再总结一下前端以后的路怎么走。选择一:前端——高级前端——全栈——前端架构师(前端专家)选择这条路的童鞋,最好就是技术迷,热爱前端,对技术有说不出的热情。喜欢专研,不管现在,还是将来,都乐于接受新事物新知识。这条路的优点:一直都能呆在自己喜欢的领域,踏踏实实的敲代码,薪水也能不断提高。这条路的缺点:30多岁还要各种敲代码,难免要被其他人管着,疲于各种公司的需求。选择二:前端——高级前端——前端主管——前端经理这条路,可能是大部分前端,都渴望走的路,都会理所当然的以为自己以后会走上的路。这个时候问题来了?哪里来这么多的前端主管和前端经理给你啊?这条路的优点:一步一脚印,人生不断往上爬。成为高富帅,赢取白富美,登上事业的高峰。这条路的缺点:就拿广州来说,不要说前端经理,就是前端主管这个职位,估计也没有多少公司是存在的。很多人上到前端经理也算到顶了。这里是想说明一点,路是有的,但是选择很少。万一有一天你要跳槽了,你真的不一定能找到下一间公司,又能当会前端主管的。我所在的公司,当得上主管或者组长这个职位的人,真的一只手就可以数完。ps:本人其实也想走这条路,但是我很唠叨的再强调一遍,30几岁之后,你未必能找到喜欢的公司的这个职位。僧多粥少啊。最后的结果会沦为,继续当码农。选择三:前端——高级前端——转后台——高级后台——后台经理这也是不少有实力的前端走的一条路。毕竟,在大多的公司,在大多的时候,都是后台统领着前台。说一句不好听的话,前端是一个习惯被领导的职位。后台引导统筹项目的开发,估计大家都看得多了。前端统领后台,统筹项目开发你听过没有(除了张云龙)?很少。至少我是没接触过的。这条路的优点:华丽转岗,前后通杀,也能走出一辈子码农的死循环,当上经理,做管理层。这条路的缺点:前端转后台,这明显不是一条好走的路,需要熬很多苦,学很多后台的东西,再慢慢成长起来。简单概括就是成本高,前期很辛苦。熬过了,上路了,就有机会走上更高的台阶;熬不过了,浪费了青春,继续当个二流的后台开发,继续码农。选择四:前端——高级前端——转产品——产品经理——高级产品经理这条路本人觉得也是一条不错的出路。在这个最好又最坏的年代,人人都是产品经理。在前端界打滚了这么多年,自然有不少产品的基础和思想。所以前端转产品,也是一条相对不会很吃力的路。这条路的优点:有一定的基础,产品经理需求量大,以后的选择很多。这条路的缺点:半路出家,前期也会很吃力地转型,转产品需要自身很有想法。懒于思考的人儿不适合。选择五:前端——高级前端——其他行业,创业等等这条路就是现在的我,总是憧憬着以后有一份不错的生意,然后有白富美,有车有楼,财务自由的一条路。这条路的优点:未知性很大,不用再整天敲代码,可能还真的很赚钱。这条路的缺点:正因为未知性太大,所以前途未卜。选择走这条路的童鞋,要早早地想好要干什么,干的事情需要具备什么技能,趁早学。总结:学无止境,祝大家都能突破自己的瓶颈。可能还有其他的路,欢迎补充。Alone381原文:https://juejin.cn/post/6844903615681806344以上文章来自[掘金]-[耀_]本程序使用github开源项目RSSHub提取聚合!
2022年10月19日
0 阅读
0 评论
0 点赞
2022-10-19
🔥🔥 CSS 手撕 10 个手机快捷方式,咱家 UI 都笑出猪叫了-掘金
思路分析这个位置信息的图标可能大家第一眼有点看不出来,实际上它是一个 3 个边都是 50% 的圆角的正方形 。还是不懂?没关系,我们看看图:这么看就简单了,我们设置正方形的三个角的 border-radius 为 50%,注意如果有边框的话,我们要把边框的宽度加上。width: 30px; height: 30px; border-radius: 16px 16px 16px 0; transform: rotate(-45deg); 这样水滴形状就画好了,水滴里面的圆可以使用伪元素 ::after 进行添加,然后通过绝对定位 absolute 的方式将它移至正中间。由于这里我们用到了绝对定位,因此不能通过 flex 布局 水平垂直居中了,但是也有更方便的方式,你还记得怎么通过 定位 + translate 的方式进行 水平垂直居中 吗?position: absolute; top:50%; left: 50%; transform: translate(-50%, -50%); 通过 top: 50% 和 left: 50% 后,元素不会如我们预期移至正中央:因此需要 translate(-50%, -50%) 将圆水平方向和垂直方向都“扯回”半个身位。2. 震动震动是个好东西,人人都该拥有它。震动就是“嗡嗡嗡”,重在怎么体现震动。效果预览思路分析.shock::before { content: ''; position: absolute; top: 20px; left: -30px; width: 40px; height: 8px; background:linear-gradient(135deg, transparent, transparent 45%, #008000, transparent 55%, transparent 100%), linear-gradient(45deg, transparent, transparent 45%, #008000, transparent 55%, transparent 100%); background-size: 1em 1em; background-repeat: repeat-x, repeat-x; transform: rotate(90deg); } 震动的两条波浪线,可以通过 线性渐变 实现。我们将两个相反的线性渐变进行 横向平铺 ,重复多个,然后遮住另一半就可以实现震动的样式啦。3. 免打扰免打扰一般会是月亮图标,当然也有静音式样的。效果预览思路分析实际上难点就是这个月亮图标了,很多人以为只能一步到位的画圆,实际上我们可以通过两个圆进行叠加,通过一些 hack 手段实现效果,我们看看示意图:一个黄色的小圆和一个灰色的大圆进行叠加,此时由于大圆是灰色的,因此明显的能看出实现原理,那如果灰色大圆是白色的呢?是不是就天衣无缝了。4. 悬浮导航悬浮导航一般是手机状的,至于长啥样那真是各种各样,我手机的悬浮导航图标是长下图这样的,有没有和我一样的小伙伴?效果预览思路分析这个图标难点就在圆弧的绘制,有的小伙伴可能平时没有画过,一时半会不知道怎么实现,实际上它就是一个 背景色和底色相同的半圆 ,给它加上 一部分 边框实现的。有的小伙伴会说:啊,半圆我也不会画啊!行行行,我教我教。实际上我们平时 border-radius: 50% 的写法都是缩写,这个大家懂吧?不懂的看图,画图老费劲了。那如果我们要得到一个半圆,是不是让另一半圆消失就行了?怎么消失呢?很简单,把它们的 y 轴半径变成 0 就好了。height: 6px; width: 10px; border-radius: 50% / 100% 100% 0 0; border: 3px solid grey; 知道怎么画半圆后,我们通过 伪元素 ::before 画半圆,通过 伪元素 ::after 画点,通过绝对定位的方式移到指定位置,就大功告成啦。5. 屏幕录制想当前,屏幕录制是没有快捷入口的,想要进行屏幕录制特别麻烦,不得不说这个东西真好用。屏幕录制一般是个摄像机的样子。效果预览思路分析屏幕录制图标的难点实际上就一个 —— 圆角梯形。是真的头疼,但是也不是没有办法。多的不说,看图,一图胜千言。没错,还是 hack 手段,反正用户也看不出。6. 手电筒手电筒就是个手电筒,玩成花也是手电筒样。效果预览思路分析难点还是圆角梯形,圆角梯形的画法上一条提到了,不再赘述。手电筒柄的椭圆可以通过 长方形 + 圆角 实现,圆中圆是通过伪元素定位实现的。7. 扫一扫扫一扫大家都不陌生吧,看吐了都。效果预览思路分析一开始看到这个图标头有点大,但是知道原理后就容易多了。那剩余中间一个扫描线定位一下就完事了,记得用上面提到的 absolute + translate ,OK?8. 录音机录音机是个好东西,人人都该拥有它。一般的录音机图标是个音频频率式样的,不知道小伙伴们的长啥样?效果预览思路分析这。。录音机图标啊,就是长短不一的圆角长方形,我们唯一能给它点属性加持的,就是通过 CSS 变量 来做了。 给每条线都加上个 变量 ,然后 CSS 部分通过类名匹配到所有该标签,统一进行样式处理。.recorder div { width: 4px; height: calc(var(--lens) * 5px); background-color: grey; border-radius: 2px; } 有的小伙伴会说:啊,我直接 6 个样式跨跨写来写不是也行吗,又不多。话虽如此,但是以后我不要 6 根了,我只要 5 根,或者我要 7 根,是不是又要改 CSS 代码 又要改 HTML 代码了?通过 CSS 变量的方式就不会有这种顾虑,除此之外,CSS 变量的用武之地多的很。9. 深色模式这个图标我一看到就觉得眼熟。开玩笑,我一看到就想起了 mix-blend-mode:difference 属性,为啥记得这么清楚呢,因为那天我上班路上刷文章刷到的,当天写主题切换就用上了,美滋滋。效果预览这里用到了一个特殊的属性:mix-blend-mode:difference 。mix-blend-mode:difference 属性描述了元素的内容应该与元素的直系父元素的内容和元素的背景如何混合。其中,difference 表示差值。关于 mix-blend-mode:difference 的更多用法,我推荐大家一篇文章:谈谈一些有趣的 CSS 题目(十七)-- 不可思议的颜色混合模式 mix-blend-mode - 掘金 (juejin.cn)10. 超级省电不知道大家平时有没有用过超级省电?反正我没用过。效果预览思路分析超级省电图标的难点闪电的绘制,不得不说这个闪电真难画。我们看看图:闪电都画好了,电池就是两个 圆角长方形 而已。码上掘金代码片段结束语最后的最后,梦醒了,醒来时,我的嘴角还带着笑,人有点恍惚,猛地,我瞪大了眼睛 —— 原来我们家没有 UI。哦,世间悲欢与我无关,我只觉得他们吵闹。本文就到此结束了,希望大家都有 UI 💪💪。如果文中有不对的地方,或是大家有不同的见解,欢迎指出 🙏🙏。如果大家觉得所有收获,欢迎一键三连💕💕。以上文章来自[掘金]-[CatWatermelon]本程序使用github开源项目RSSHub提取聚合!
2022年10月19日
0 阅读
0 评论
0 点赞
2022-10-18
花一个小时整了个ikun的篮球空间-掘金
比赛场上总有一个要赢,那个人为什么不能是我呢?背景最近打篮球,打着打着,突发奇想,想整个篮球空间网站,用来放一些教程视频、训练方案、篮球战术、篮球规则以及我个人的一些总结。 因此,借着周末时光,快速整了个网站。下面废话少说, 做个简洁总结。建站初心空闲时间归纳整理篮球方面的知识和技巧。极速搭建网站申请域名和实名认证现阶段整网站,用 github 或者 vercel 等自带的域名,都不太好用。 建议用国内云厂商注册个便宜域名,不用注册贵的,一年就几块钱。实名认证这个需要花点时间,我是之前已经实名认证过了,有模板可用。开发到上线 使用 vitepress 快速开发网站 使用 vercel 快速部署网站,并在 vercel 上关联 ba.godkun.top 域名。 网站主要内容如下图所示:主要包含 6 个内容 教程视频: 收集一些还不错的网上教程视频 训练方案: 收集一些还可以的训练方案 篮球战术: 收集一些好的篮球战术 篮球规则: 整理些篮球规则 篮球装备: 整理些篮球装备 ikun 篮球空间: 我个人的一些篮球总结 目前只是把几大类确定了,内容还没补充。后面空闲时间慢慢整理完善。一起建设欢迎喜欢篮球的小伙伴,进入下面地址https://ba.godkun.top/ikun/%E4%BA%A4%E6%B5%81%E7%BE%A4.html加入 ikun 篮球交流空间群 ,一起交流篮球技术。总结这是一篇没有阅读难度的文章,记录了我整的一个小网站,周末愉快!附文章只在掘金、 github 和公众号上同步 掘金地址:https://juejin.cn/user/2101921962531469/posts github:https://github.com/godkun 公众号: 元语言 版权声明:本文系我原创,未经许可,不可转载和二次创作。以上文章来自[掘金]-[码上有你]本程序使用github开源项目RSSHub提取聚合!
2022年10月18日
0 阅读
0 评论
0 点赞
2022-10-18
组员大眼瞪小眼,forEach处理异步任务遇到的坑-掘金
以上文章来自[掘金]-[zz]本程序使用github开源项目RSSHub提取聚合!
2022年10月18日
0 阅读
0 评论
0 点赞
2022-10-18
全网都在搞的评论区、主页等显示IP地址功能-掘金
从 Http header 中可以获取到客户端在请求头中携带的IP地址头部名词的解释 X-Forwarded-For:Web 服务器获取访问用户的真实 IP 地址,若存在代理,最左边是最原始客户端的 IP 地址; 在node.js中koa为例子在中间件函数中, 获取header里的x-forwarded-for子段export default (ctx, next) => { const { method, body, query, header: { 'x-forwarded-for': ip }, } = ctx.request; console.log(ip, 'ip地址’) } nginx的配置如果你使用的nginx代理你的服务, 其实上面node里获取的真实ip那个子段, 依赖于nginx的转发。就是这行。 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;子段释义$proxy_add_x_forwarded_for 记录真实的客户端IP地址,但是服务器中的日志需要有这个变量。参考链接 nginx官方文档 https://nginx.org/en/docs/http/ngx_http_proxy_module.html#var_proxy_add_x_forwarded_fornginx配置转发伪代码server { listen 80; ... location /api/ { ... // 代理真实IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; ... proxy_set_header Connection "upgrade"; proxy_pass http://127.0.0.1:8001/; proxy_redirect off; } } ip地址换地理位置注册高德地图开放平台, 进入控制台https://lbs.amap.com/创建一个应用在应用 创建一个apikey apikey创建好了 使用IP定位API文档链接 https://lbs.amap.com/api/webservice/guide/api/ipconfig如下, 传你的apiKey和ip地址即可换取省市区及经纬度https://restapi.amap.com/v3/ip?key=XXX&ip=你的ip地址 返回示例{ "status": "1", // 1为成功 "info": "OK", "infocode": "10000", "province": "浙江省", "city": "杭州市", "adcode": "330100", "rectangle": "119.8824799,29.95931271;120.5552208,30.52048536" } node中使用koa中间件中使用const fetch = require('node-fetch'); export default async(ctx, next) => { const { method, body, query, header: { 'x-forwarded-for': ip }, } = ctx.request; const info = await getLocation(ip); console.log(info, '省市信息’) // { // "status": "1", // "info": "OK", // "infocode": "10000", // "province": "浙江省", // "city": "杭州市", // "adcode": "330100", // "rectangle": "119.8824799,29.95931271;120.5552208,30.52048536" // } await next() } /* * ip获取省市信息 * @param {} $ip * @returns 省市信息 */ async function getLocation($ip) { // 处理一下ip const ip = $ip.replace(/::ffff:/g, '').split(',')[0]; // 调用高德地图的 const ipResult = await fetch(`https://restapi.amap.com/v3/ip?key=310d88b1f76599ee6a4b0bd50ba6bbd8&ip=${ip}`).then((res) => res.json()).catch(() => { }); if (data && data.status === '1') { return ipResult; } return null; } 其他ip换取地理信息方式github发现一个离线的解析ip的库Ip2regionip2region v2.0 - 是一个离线IP地址定位库和IP定位数据管理框架,10微秒级别的查询效率,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现。 github链接 (https://github.com/lionsoul2014/ip2region/blob/master/v1.0) https://github.com/lionsoul2014/ip2region存储方式 & 业务分析存储方式1、存Redis每次获取ip先从redis里找, 没有再去查询第三方API2、 搞个ip和地理信息对应表每次获取ip先从对应表, 没有再去查询第三方API业务功能分析评论里的IP信息例如评论这种功能 , ip和评论绑定,是快照存(后续变动了不影响之前的评论)。 这种每次从ip和地理信息对应表 查也可以。 如果 要求不高, 不考虑后续扩展, 直接加个子段 把地理信息冗余到业务表里吧。 主页的IP信息主页的位置信息要实时变的。加个中间件 每次都去获取当前ip 判断地址变没变?按一定策略, 在某个接口 加个异步方法,低频率判断。比如在主页的某个接口,加一个方法,异步判断, 变化了,就更新到用户表。总结两步1、异步判断ip地址变化。2、异步更新到用户表。结尾是个有趣的功能,快去加到你的项目里吧🏄🏼♂️🏄🏄♀ developer!希望掘金也快点出这个功能, 掘友们@掘金产品经理以上文章来自[掘金]-[浏览器API调用工程师]本程序使用github开源项目RSSHub提取聚合!
2022年10月18日
0 阅读
0 评论
0 点赞
2022-10-18
还不会用 TS 封装 Axios?我教你啊-掘金
Axios 的二次封装是一项基础工作,主要目的就是将一些常用的功能进行封装,简化后续网络请求的发送。JS 版本的封装大家都已经非常熟悉了,可以信手拈来。但是使用 TypeScript 对 Axios 进行封装,稍微就复杂了些。主要是由于 TS 引入了类型系统,带来了一些类型的束缚。对于 TS 不太熟悉的小伙伴就容易绕晕。因此本文适合的阅读对象:熟悉 axios 封装,但在 TypeScript 中不知该如何下手。明确我们的封装目标:能用上TypeScript 带来的好处,类型检查(安全性) 和语法提示(便捷性) 。本文将从泛型入手,然后了解 Axios 中的部分类型,延续 JS 版本的极简风,教你封装出一个可用的清爽版 Axios。初始化项目使用 create-vue 脚手架初始化项目:npm create vue ts-axios // 或者 pnpm create vue ts-axios 选择添加 TypeScript :之后进入项目目录并安装依赖:npm install // 或者 pnpm install 安装其他依赖示例会用到一些组件,所以安装下组件库:pnpm add axios pnpm add ant-design-vue 接口 mock使用插件 mock 一个登录接口和一个用户信息接口,方便测试请求。上篇文章《一个登录案例包学会 Pinia》介绍了在 vite 中 mock 数据的简单用法,本文不再赘述。安装插件并进行配置:pnpm add vite-plugin-mock mockjs -D // vite.config.ts import vue from '@vitejs/plugin-vue' import { viteMockServe } from 'vite-plugin-mock' export default defineConfig({ plugins: [ vue(), viteMockServe() ], }) 建立 mock 文件,一个登录接口返回 token,一个用户信息接口返回用户名等信息,还有一个模拟出现业务错误的接口。// mock/user.ts export default [ // 用户登录 { url: "/api/user/login", method: "post", response: (res) => { return { code: 0, message: 'success', data: { token: "Token" } } } }, // 获取用户信息 { url: "/api/user/info", method: "get", response: (res) => { return { code: 0, message: 'success', data: { id: "2467751560226270", username: "昆吾kw", avatar: "https://p3-passport.byteimg.com/img/user-avatar/3745b7eb198f2357155cd88eb7930f35~180x180.awebp", description: "前端开发", } } } }, // 一个失败的请求 { url: "/api/error", method: "get", response: (res) => { return { code: 1, message: '密码错误', data: null } } } ] 泛型泛型的英文是Generic,意思是“通用的”,“一般的“。在程序设计,泛型中可以理解为 “泛指的类型”,不确定的类型,或者通用的类型。理解泛型泛型的概念很像函数中的参数。为了更方便的理解什么是泛型,可以用函数参数做一个类比。函数在定义时,我们并不知道参数的值到底是多少。只有当函数调用时,才能确定。function print(a) { console.log(a) } print('hello') print('world') 泛型的含义也是如此。只不过,参数不确定的是值,泛型不确定的是类型。泛型的使用场景有很多种,比如泛型函数,泛型类,泛型接口。我们只看泛型函数。如何定义泛型函数呢?如下:function add(a:T, b:T): T { return a + b } 这段代码定义了一个 add 函数,在函数名后来使用了一对尖括号来声明泛型,使用 T(Type 的缩写)来表示一个泛型。之后参数的类型,就不再具体指定是 string 还是 number ,亦或是其他具体的类型,而是用 T 表示。函数的返回值类型同样使用 T 表示。这样就声明了一个泛型函数。接着就是泛型函数的调用:add(1, 2) add('a', 'b') 和普通函数的调用一样,只不过在函数名后面再次使用一对尖括号,来传递了具体的类型。通过和参数的类比,详细大家能体会到泛型的含义了。所谓泛型,就是在定义时不确定,而在使用时确定的一种类型。泛型的好处就是提高了代码的复用性,减少了代码的冗余。有了对泛型的初步认识后,下面就可以去进入正题,去看看 TypeScript 如何封装 Axios 了。本文的思路我们不再按照创建实例、配置参数、设置拦截器、封装公共请求方法、封装 API 的顺序去实现封装代码。而是打破常规,从类型入手。Axios 中的重要类型查看第三方库的类型声明Axios 提供了完备的类型声明。声明文件可以直接去看 node_modules/axios/index.d.ts 这个文件:也可以在编辑器中,按 ctrl,同时鼠标点击 axios 模块跳转过去:要封装好 axios,需要先明白这几个类型是干啥用的。从一个请求方法入手,看常用的类型TypeScript 中 class 不仅是类,还可以是类类型。作为前者,它是一个值。作为后者,它是一个类型。下面的 Axios 就是一个类类型,它声明了一些成员的类型。我们看其中的核心请求方法,request,get ,post :export class Axios { // ...... request(config: AxiosRequestConfig): Promise; get(url: string, config?: AxiosRequestConfig): Promise; post(url: string, data?: D, config?: AxiosRequestConfig): Promise; // ...... } 熟悉 axios 的朋友都知道,axios.get,axios.post 这些方法其实是对 axios.request 的一层封装。所以我们就看 request 方法的声明。request 方法有三个泛型,T ,R 和 D,接收一个 AxiosRequestConfig 类型的参数作为配置对象,返回值的类型是一个接收泛型R 的 Promise 类型。request(config: AxiosRequestConfig): Promise; 这三个泛型,T 的含义是什么?要去看 R。R 的含义是什么?要去看它的默认类型 AxiosResponse :export interface AxiosResponse { data: T; status: number; statusText: string; headers: RawAxiosResponseHeaders | AxiosResponseHeaders; config: AxiosRequestConfig; request?: any; } 看到这个结构,我们知道了,原来 AxiosResponse 就是在设置响应拦截器中用到的那个 response 对象的类型。同时也就知道了泛型 T 就是服务器返回的数据的类型。因为服务器究竟返回什么类型,代码是不知道的,所以它的默认类型是 any。回到头来再来看 request 方法的定义,现在就能明白了,它接收的第一个泛型 T 就是将来服务器返回数据的类型,R 就是这个数据经过 axios 包装一层得到的 response 对象的类型,而 request 方法的返回值是一个 Promise,其值就是成功态的 R,也就是 response对象。第三个泛型 D,这里就不再说了。你可以用同样的方法,找出它又是谁的类型,然后发在评论区。AxiosRequestConfig 类型先看下它的类型声明:export interface AxiosRequestConfig { url?: string; method?: Method | string; baseURL?: string; headers?: RawAxiosRequestHeaders; // ..... } 相信大家能看出来,它其实就是 axios 的配置对象的类型。在设置请求拦截器和封装公共请求方法时会用到。AxiosInstance该类型为 axios.create 方法创建出的实例的类型。后面直接用到。AxiosError该类型为请求发送过程中出现错误产生的错误对象的类型:export class AxiosError extends Error { constructor( message?: string, code?: string, config?: AxiosRequestConfig, request?: any, response?: AxiosResponse ); config?: AxiosRequestConfig; code?: string; request?: any; response?: AxiosResponse; isAxiosError: boolean; status?: number; toJSON: () => object; cause?: Error; // ...... } 我们会用到其中的 response 属性,它表示响应对象,需要根据它的 HTTP 状态码做一些处理。知道了上面这几个类型,接下来就可以着手封装 Axios 了。开始封装先提前打个预防针,因为封装的很简单,没有用到类,没有过多的处理业务逻辑。估计看完后你会感觉索然无味。毕竟我们最初的目的就是用上 TypeScript 的类型能力:类型检查和代码提示。而且我相信,太复杂的封装,可能会阻碍对类型系统的学习,反而得不偿失。新建一个 src/utils/request.ts 文件,在这个文件中对 Axios 进行封装。Result 类型从服务器返回的数据的类型通常长这样子:{ code: 0, message: 'ok', data: {} } 定义一个接口 Result 来表示它,其中 data 的类型不确定,所以就要用到泛型了:/* 服务器返回数据的的类型,根据接口文档确定 */ export interface Result { code: number, message: string, data: T } 这是一个泛型接口。和泛型函数差不多,在定义时声明一个泛型,用这个泛型去规范成员的类型。将来使用该接口的时候,再去确定泛型的具体类型。接下来就可以写代码了。创建实例这个大家都非常熟悉了,不再赘述。import axios from 'axios' import type { AxiosInstance } from 'axios' const service: AxiosInstance = axios.create({ baseURL: '/api', timeout: 30000 }) 请求拦截器这个大家也很熟悉,主要在这里处理请求发送前的一些工作,比如给 HTTP Header 添加 token ,开启 Loading 效果,设置取消请求等。import type { AxiosError, AxiosRequestConfig } from 'axios' import { message as Message } from 'ant-design-vue' /* 请求拦截器 */ service.interceptors.request.use((config: AxiosRequestConfig) => { // 伪代码 // if (token) { // config.headers.Authorization = `Bearer ${token}`; // } return config }, (error: AxiosError) => { Message.error(error.message); return Promise.reject(error) }) 响应拦截器响应拦截器大家也很熟悉了。简单说一下做了哪些事情: 根据自定义错误码判断请求是否成功,然后只将组件会用到的数据,也就是上面的 Result 中的 data 返回 如果错误码判断请求失败,此时为业务错误,比如用户名不存在等,在这里进行提示 如果网络错误,则进入第二个回调函数中,根据不同的状态码设置不同的提示消息进行提示 import type { AxiosError, AxiosResponse } from 'axios' import { message as Message } from 'ant-design-vue' /* 响应拦截器 */ service.interceptors.response.use((response: AxiosResponse) => { const { code, message, data } = response.data // 根据自定义错误码判断请求是否成功 if (code === 0) { // 将组件用的数据返回 return data } else { // 处理业务错误。 Message.error(message) return Promise.reject(new Error(message)) } }, (error: AxiosError) => { // 处理 HTTP 网络错误 let message = '' // HTTP 状态码 const status = error.response?.status switch (status) { case 401: message = 'token 失效,请重新登录' // 这里可以触发退出的 action break; case 403: message = '拒绝访问' break; case 404: message = '请求地址错误' break; case 500: message = '服务器故障' break; default: message = '网络连接故障' } Message.error(message) return Promise.reject(error) }) 公共请求方法这里是本文的重点,也是 TS 封装 Axios 的重点。使用 TS 无非要获得良好的代码提示,我们在调用接口时编辑器能提示出该接口的返回值有哪些。/* 导出封装的请求方法 */ export const http = { get(url: string, config?: AxiosRequestConfig) : Promise { return service.get(url, config) }, post(url: string, data?: object, config?: AxiosRequestConfig) :Promise { return service.post(url, data, config) }, put(url: string, data?: object, config?: AxiosRequestConfig) :Promise { return service.put(url, data, config) }, delete(url: string, config?: AxiosRequestConfig) : Promise { return service.delete(url, config) } } 我们以 get 方法为例,看下为什么这么封装?get(url: string, config?: AxiosRequestConfig) : Promise { return service.get(url, config) } 上文说过,axios 的 requet,get 这些方法,返回的都是一个 AxiosResponse 类型的数据。默认的响应拦截器返回的是完整的 response 对象,类型为 AxiosResponse 。经过我们的修改,现在响应拦截器只返回 response.data.data,对应的类型就是 AxiosResponse 中的 T。所以这里的 service.get 方法,其实际返回类型应为 T,但是编辑器并不知道,它仍然认为它返回的是:所以我们要手动帮助编辑器“修正”类型提示,也就是不依靠 TS 的类型推导,而是主动设置返回类型为 Promise :get(url: string, config?: AxiosRequestConfig) : Promise { return service.get(url, config) } 这样编辑器就准确识别类型了:这里的泛型 T 表示的服务器返回数据的类型,具体返回什么类型,要到接口调用时才知道。所以下面来到 API 层,去封装请求接口。为什么要封装公共请求方法之所以封装这一层公共请求方法,主要目的就是为了帮编译器正确识别类型。方法就是手动设置返回类型。如果直接使用 axios 实例的请求方法,就需要在每次调用时都指定一遍,当接口有几十上百个时,多少会在增加一些工作量。所以就在这里多加一层,提前将类型制定好。API 层准备两个文件,一个写类型,一个写接口。在 api/user/types.ts 文件中,写接口需要的参数的类型,和接口返回数据的类型。这里的类型,要根据接口文档而定。/* 登录接口参数类型 */ export interface LoginData { username: string, password: string, } /* 登录接口返回值类型 */ export interface LoginRes { token: string } /* 用户信息接口返回值类型 */ export interface UserInfoRes { id: string, username: string, avatar: string, description: string, } 在 api/user/index.ts 文件中,封装组件用到的接口。import request, { http } from '@/utils/request' import type { LoginData, LoginRes, UserInfoRes} from './types' /** * 登录 */ export function login(data: LoginData) { return http.post('/user/login', data); } /** * 获取登录用户信息 */ export function getUserInfo() { return http.get('/user/info') } 以 login 方法为例进行说明。它接收 LoginData 类型的参数作为请求参数。在调用接口时,通过泛型指定了该接口的返回值类型:测试Axios 代码提示测试App.vue 组件中有一个登录表单,在这里测试登录接口是否正常。方法login 可以推导出当前返回值的类型:返回值 res 的类型也能推导出:也可以正确使用类型提示:然后再测试下调用请求用户信息的接口:如我们所愿,现在 Axios 经过 TypeScript 的封装,可以给出友好的类型提示,这对于开发体验和效率都有很好的提升。响应拦截器提示测试测试业务处理错误的请求,假设此次请求用户密码输入错误:关闭开发服务,测试无网络连接下的请求发送:小结本文示例代码已上传到仓库。开始只是想说说 TypeScript 封装 Axios 的一个简单实现版本。但没想到最后说了不老少内容,包括: TypeScript 泛型的概念 第三方库如何看类型声明 如何利用类型封装 Axios 其中封装 Axios 的过程个人感觉有点啰嗦了,主要是理清类型的作用,不太容易用文字说明,希望大家能理解。如果有帮助,还请一键三连,感谢支持~以上文章来自[掘金]-[昆吾kw]本程序使用github开源项目RSSHub提取聚合!
2022年10月18日
0 阅读
0 评论
0 点赞
1
2
...
178