首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
70 阅读
2
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
52 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
42 阅读
4
如何在 Clash for Windows 上配置服务
40 阅读
5
如何在 IOS Shadowrocket 上配置服务
40 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1061
篇与
的结果
2024-10-20
基于 IntersectionObserver 实现一个组件的曝光监控
我们在产品推广过程中,经常需要判断用户是否对某个模块感兴趣。那么就需要获取该模块的曝光量和用户对该模块的点击量,若点击量/曝光量越高,说明该模块越有吸引力。那么如何知道模块对用户是否曝光了呢?之前我们是监听页面的滚动事件,然后通过getBoundingClientRect()现在我们直接使用IntersectionObserver就行了,使用起来简单方便,而且性能上也比监听滚动事件要好很多。 1. IntersectionObserver # 我们先来简单了解下这个 api 的使用方法。IntersectionObserver 有两个参数,new IntersectionObserver(callback, options),callback 是当触发可见性时执行的回调,options 是相关的配置。// 初始化一个对象 const io = new IntersectionObserver( (entries) => { // entries是一个数组 console.log(entries); }, { threshold: [0, 0.5, 1], // 触发回调的节点,0表示元素刚完全不可见,1表示元素刚完全可见,0.5表示元素可见了一半等 }, ); // 监听dom对象,可以同时监听多个dom元素 io.observe(document.querySelector('.dom1')); io.observe(document.querySelector('.dom2')); // 取消监听dom元素 io.unobserve(document.querySelector('.dom2')); // 关闭观察器 io.disconnect(); 在 callback 中的 entries 参数是一个IntersectionObserverEntry类型的数组。主要有 6 个元素: { time: 3893.92, rootBounds: ClientRect { bottom: 920, height: 1024, left: 0, right: 1024, top: 0, width: 920 }, boundingClientRect: ClientRect { // ... }, intersectionRect: ClientRect { // ... }, intersectionRatio: 0.54, target: element } 各个属性的含义:{ time: 触发该行为的时间戳(从打开该页面开始计时的时间戳),单位毫秒 rootBounds: 视窗的尺寸, boundingClientRect: 被监听元素的尺寸, intersectionRect: 被监听元素与视窗交叉区域的尺寸, intersectionRatio: 触发该行为的比例, target: 被监听的dom元素 } 我们利用页面可见性的特点,可以做很多事情,比如组件懒加载、无限滚动、监控组件曝光等。 2. 监控组件的曝光 # 我们利用IntersectionObserver这个 api,可以很好地实现组件曝光量的统计。实现的方式主要有两种: 函数的方式; 高阶组件的方式; 传入的参数:interface ComExposeProps { readonly always?: boolean; // 是否一直有效 // 曝光时的回调,若不存在always,则只执行一次 onExpose?: (dom: HTMLElement) => void; // 曝光后又隐藏的回调,若不存在always,则只执行一次 onHide?: (dom: HTMLElement) => void; observerOptions?: IntersectionObserverInit; // IntersectionObserver相关的配置 } 我们约定整体的曝光量大于等于 0.5,即为有效曝光。同时,我们这里暂不考虑该 api 的兼容性,若需要兼容的话,可以安装对应的 polyfill 版。 2.1 函数的实现方式 # 用函数的方式来实现时,需要业务侧传入真实的 dom 元素,我们才能监听。// 一个函数只监听一个dom元素 // 当需要监听多个元素,可以循环调用exposeListener const exposeListener = (target: HTMLElement, options?: ComExposeProps) => { // IntersectionObserver相关的配置 const observerOptions = options?.observerOptions || { threshold: [0, 0.5, 1], }; const intersectionCallback = (entries: IntersectionObserverEntry[]) => { const [entry] = entries; if (entry.isIntersecting) { if (entry.intersectionRatio >= observerOptions.threshold[1]) { if (target.expose !== 'expose') { options?.onExpose?.(target); } target.expose = 'expose'; if (!options?.always && typeof options?.onHide !== 'function') { // 当always属性为加,且没有onHide方式时 // 则在执行一次曝光后,移动监听 io.unobserve(target); } } } else if (typeof options?.onHide === 'function' && target.expose === 'expose') { options.onHide(target); target.expose = undefined; if (!options?.always) { io.unobserve(target); } } }; const io = new IntersectionObserver(intersectionCallback, observerOptions); io.observe(target); }; 调用起来也非常方便:exposeListener(document.querySelector('.dom1'), { always: true, // 监听的回调永远有效 onExpose() { console.log('dom1 expose', Date.now()); }, onHide() { console.log('dom1 hide', Date.now()); }, }); // 没有always时,所有的回调都只执行一次 exposeListener(document.querySelector('.dom2'), { // always: true, onExpose() { console.log('dom2 expose', Date.now()); }, onHide() { console.log('dom2 hide', Date.now()); }, }); // 重新设置IntersectionObserver的配置 exposeListener(document.querySelector('.dom3'), { observerOptions: { threshold: [0, 0.2, 1], }, onExpose() { console.log('dom1 expose', Date.now()); }, }); 那么组件的曝光数据,就可以在onExpose()的回调方式里进行上报。不过我们可以看到,这里面有很多标记,需要我们处理,单纯的一个函数不太方便处理;而且也没对外暴露出取消监听的 api,导致我们想在卸载组件前也不方便取消监听。因此我们可以用一个 class 类来实现。 2.2 类的实现方式 # 类的实现方式,我们可以把很多标记放在属性里。核心部分跟上面的差不多。class ComExpose { target = null; options = null; io = null; exposed = false; constructor(dom, options) { this.target = dom; this.options = options; this.observe(); } observe(options) { this.unobserve(); const config = { ...this.options, ...options }; // IntersectionObserver相关的配置 const observerOptions = config?.observerOptions || { threshold: [0, 0.5, 1], }; const intersectionCallback = (entries) => { const [entry] = entries; if (entry.isIntersecting) { if (entry.intersectionRatio >= observerOptions.threshold[1]) { if (!config?.always && typeof config?.onHide !== 'function') { io.unobserve(this.target); } if (!this.exposed) { config?.onExpose?.(this.target); } this.exposed = true; } } else if (typeof config?.onHide === 'function' && this.exposed) { config.onHide(this.target); this.exposed = false; if (!config?.always) { io.unobserve(this.target); } } }; const io = new IntersectionObserver(intersectionCallback, observerOptions); io.observe(this.target); this.io = io; } unobserve() { this.io?.unobserve(this.target); } } 调用的方式:// 初始化时自动添加监听 const instance = new ComExpose(document.querySelector('.dom1'), { always: true, onExpose() { console.log('dom1 expose'); }, onHide() { console.log('dom1 hide'); }, }); // 取消监听 instance.unobserve(); 不过这种类的实现方式,在 react 中使用起来也不太方便: 首先要通过useRef()获取到 dom 元素; 组件卸载时,要主动取消对 dom 元素的监听; 2.3 react 中的组件嵌套的实现方式 # 我们可以利用 react 中的useEffect()hook,能很方便地在卸载组件前,取消对 dom 元素的监听。import React, { useEffect, useRef, useState } from 'react'; interface ComExposeProps { children: any; readonly always?: boolean; // 是否一直有效 // 曝光时的回调,若不存在always,则只执行一次 onExpose?: (dom: HTMLElement) => void; // 曝光后又隐藏的回调,若不存在always,则只执行一次 onHide?: (dom: HTMLElement) => void; observerOptions?: IntersectionObserverInit; // IntersectionObserver相关的配置 } /** * 监听元素的曝光 * @param {ComExposeProps} props 要监听的元素和回调 * @returns {JSX.Element} */ const ComExpose = (props: ComExposeProps): JSX.Element => { const ref = useRef(null); const curExpose = useRef(false); useEffect(() => { if (ref.current) { const target = ref.current; const observerOptions = props?.observerOptions || { threshold: [0, 0.5, 1], }; const intersectionCallback = (entries: IntersectionObserverEntry[]) => { const [entry] = entries; if (entry.isIntersecting) { if (entry.intersectionRatio >= observerOptions.threshold[1]) { if (!curExpose.current) { props?.onExpose?.(target); } curExpose.current = true; if (!props?.always && typeof props?.onHide !== 'function') { // 当always属性为加,且没有onHide方式时 // 则在执行一次曝光后,移动监听 io.unobserve(target); } } } else if (typeof props?.onHide === 'function' && curExpose.current) { props.onHide(target); curExpose.current = false; if (!props?.always) { io.unobserve(target); } } }; const io = new IntersectionObserver(intersectionCallback, observerOptions); io.observe(target); return () => io.unobserve(target); // 组件被卸载时,先取消监听 } }, [ref]); // 当组件的个数大于等于2,或组件使用fragment标签包裹时 // 则创建一个新的div用来挂在ref属性 if (React.Children.count(props.children) >= 2 || props.children.type.toString() === 'Symbol(react.fragment)') { return {props.children}; } // 为该组件挂在ref属性 return React.cloneElement(props.children, { ref }); }; export default ComExpose; 调用起来更加方便了,而且还不用手动获取 dom 元素和卸载监听: console.log('expose')} onHide={() => console.log('hide')}> dom1 always Vue 组件实现起来的方式也差不多,不过我 Vue 用的确实比较少,这里就不放 Vue 的实现方式了。 3. 总结 # 现在我们已经基本实现了关于组件的曝光的监听方式,整篇文章的核心全部都在IntersectionObserver上。基于上面的实现方式,我们其实还可以继续扩展,比如在组件即将曝光时踩初始化组件;页面中的倒计时只有在可见时才执行,不可见时则直接停掉等等。IntersectionObserver 还等着我们探索出更多的用法!
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
将leetcode中二叉树的数组结构转为真实的树结构
我们在 LeetCode 上锻炼算法时,关于二叉树的题目,网站上给的都直接是一个数组,那么本地测试的时候,就很不方便。官方文档中的解释是这样的:请问 [1, null, 2, 3] 在二叉树测试用例中代表什么。 1. javascript 版本 # 我们首先定义一个 TreeNode 的结构:function TreeNode(val) { this.val = val; this.left = this.right = null; } 我们用循环的方式,将数组转为二叉树结构。const array2binary = (arr) => { if (!arr || !arr.length) { return null; } let index = 0; const queue = []; const len = arr.length; const head = new TreeNode(arr[index]); queue.push(head); while (index < len) { index++; const parent = queue.shift(); if (arr[index] !== null && arr[index] !== undefined) { const node = new TreeNode(arr[index]); parent.left = node; queue.push(node); } index++; if (arr[index] !== null && arr[index] !== undefined) { const node = new TreeNode(arr[index]); parent.right = node; queue.push(node); } } return head; }; 使用:const root = array2binary([3, 9, 20, null, null, 15, 7]); 最终转换成的树形结构: 2.C++ 版本 # 在 C++中的 vector 数据结构里,只能有一种类型,这里我们用INT_MAX来代替 null,需要转换的数组中的 null 也请用 INT_MAX 来代替。我们的转换程序主要是应用在本地,为了方便本地的调试,因此在使用时,尤其要注意数据的范围,若真的用到了 INT_MAX 对应的数据,请把转换程序中代替 null 的INT_MAX更换为其他数字。官方给的二叉树结构为:struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode() : val(0), left(nullptr), right(nullptr) {} TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} }; 我们的转换程序如下:/** * 将leetcode中的数组转为二叉树 * 因C++中的vector只能是同一个类型,这里用INT_MAX来代替null * @param {vector} nums * @return {TreeNode} */ TreeNode *vectorToTree(vector nums){ int i = 1; bool isLeftTree = true; // 是否是左分支,true为左分支,false时是右分支 if (nums.size() == 0) { return nullptr; } queue myQueue; TreeNode *head = new TreeNode(nums[0]); myQueue.push(head); while (i < nums.size()) { TreeNode *topNode = myQueue.front(); // 这里我们与js版稍微有点区别 // 队列中的节点只要用两次,当isLeftTree指向到右分支时,说明可以取出了 if (!isLeftTree) { myQueue.pop(); } // 我们用INT_MAX来标识nullptr if (nums[i] != INT_MAX) { TreeNode *node = new TreeNode(nums[i]); if (isLeftTree) { topNode->left = node; } else { topNode->right = node; } myQueue.push(node); } isLeftTree = !isLeftTree; i++; } return head; } 使用:vector nums = {3, 9, 20, INT_MAX, INT_MAX, 15, 7}; TreeeNode *root = vectorToTree(nums); 3. 测试 # 我们可以在 leetcode 中选择一个题目来测试下,就选最简单的二叉树前序遍历。把题目中给到的数组数据,转为二叉树数据,然后再调试您的程序吧。 4. 题外话 # 刚开始经同事提示,添加了一个递归的方式,如:// 错误代码,请不要直接使用 const array2binary = (arr, index = 0) => { if (!arr || arr[index] === null || index >= arr.length) { return null; } const node = new TreeNode(arr[index]); node.left = array2binary(arr, index * 2 + 1); node.right = array2binary(arr, index * 2 + 2); return node; }; 这个代码只会在某些情况里正常,并不会适应 leetcode 里所有的二叉树的情况。如在513. 找树左下角的值中的第 2 个示例:输入: [1,2,3,4,null,5,6,null,null,7] 输出: 7 实际的树结构是:若使用最上面的迭代循环方式,是没有问题的,构建出来的树与例题中一样。但若使用这段递归的代码,就会出现问题,发现节点7没了。这是因为:节点 2 的右子节点为 null 时,后续数组不会再用 null 为该空节点(2 右) 来填充他的子节点。使用上面队列结构循环时,我们会跳过(2 右)这个节点,接着处理节点 5。但在递归的方式中,我们通过index * 2 + 1和index * 2 + 2来计算 arr[index]的左右节点,会把节点 7 算到(2 右)空节点的子节点上。从这里开始,后续的节点都会错位。后来又想着怎么给这个递归程序补救一下,不过没找到实现的方式。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
实现一个带有动效的 React 弹窗组件
我们在写一些 UI 组件时,若不考虑动效,就很容易实现,主要就是有无的切换(类似于 Vue 中的 v-if 属性)或者可见性的切换(类似于 Vue 中的 v-show 属性)。 1. 没有动效的弹窗 # 在 React 中,可以这样来实现:interface ModalProps { open: boolean; onClose?: () => void; children?: any; } const Modal = ({open. onClose, children}: ModalProps) => { if (!open) { return null; } return createPortal( {children} x , document.body); }; 使用方式:const App = () => { const [open, setOpen] = useState(false); return ( setOpen(true)}>show modal setOpen(false)}> modal content ); }; 我们在这里就是使用open属性来控制展示还是不展示,但完全没有渐变的效果。若我们想实现 fade, zoom 等动画效果,还需要对此进行改造。 2. 自己动手实现有动效的弹窗 # 很多同学在自己实现动效时,经常是展示的时候有动效,关闭的时候没有动效。都是动效的时机没有控制好。这里我们先自己来实现一下动效的流转。刚开始我实现的时候,动效只有开始状态和结束状态,需要很多的变量和逻辑来控制这个动效。后来我参考了react-transition-group组件的实现,他是将动效拆分成了几个部分,每个部分分别进行控制。 展开动效的顺序:enter -> enter-active -> enter-done; 关闭动效的顺序:exit -> exit-active -> exit-done; 动效过程在enter-active和exit-active的过程中。我们再通过一个变量 active 来控制是关闭动效是否已执行关闭,参数 open 只控制是执行展开动效还是关闭动效。当 open 和 active 都为 false 时,才销毁弹窗。const Modal = ({ open, children, onClose }) => { const [active, setActive] = useState(false); // 弹窗的存在周期 if (!open && !active) { return null; } return ReactDOM.createPortal( {children} x , document.body, ); }; 这里我们接着添加动效过程的变化:const [aniClassName, setAniClassName] = useState(''); // 动效的class // transition执行完毕的监听函数 const onTransitionEnd = () => { // 当open为rue时,则结束状态为'enter-done' // 当open未false时,则结束状态为'exit-done' setAniClassName(open ? 'enter-done' : 'exit-done'); // 若open为false,则动画结束时,弹窗的生命周期结束 if (!open) { setActive(false); } }; useEffect(() => { if (open) { setActive(true); setAniClassName('enter'); // setTimeout用来切换class,让transition动起来 setTimeout(() => { setAniClassName('enter-active'); }); } else { setAniClassName('exit'); setTimeout(() => { setAniClassName('exit-active'); }); } }, [open]); Modal 组件完整的代码如下:const Modal = ({ open, children, onClose }) => { const [active, setActive] = useState(false); // 弹窗的存在周期 const [aniClassName, setAniClassName] = useState(''); // 动效的class const onTransitionEnd = () => { setAniClassName(open ? 'enter-done' : 'exit-done'); if (!open) { setActive(false); } }; useEffect(() => { if (open) { setActive(true); setAniClassName('enter'); setTimeout(() => { setAniClassName('enter-active'); }); } else { setAniClassName('exit'); setTimeout(() => { setAniClassName('exit-active'); }); } }, [open]); if (!open && !active) { return null; } return ReactDOM.createPortal( {children} x , document.body, ); }; 动效的流转过程已经实现了,样式也要一起写上。比如我们要实现渐隐渐现的 fade 效果:.enter { opacity: 0; } .enter-active { transition: opacity 200ms ease-in-out; opacity: 1; } .enter-done { opacity: 1; } .exit { opacity: 1; } .exit-active { opacity: 0; transition: opacity 200ms ease-in-out; } .exit-done { opacity: 0; } 如果是要实现放大缩小的 zoom 效果,修改这几个 class 就行。一个带有动效的弹窗就已经实现了。使用方式:const App = () => { const [open, setOpen] = useState(false); return ( setOpen(true)}>show modal setOpen(false)}> modal content ); }; 点击链接自己实现动效的 React 弹窗 demo查看效果。类似地,还有 Toast 之类的,也可以这样实现。 3. react-transition-group # 我们在实现动效的思路上借鉴了 react-transition-group 中的CSSTransition组件。CSSTransition已经帮我封装好了动效展开和关闭的过程,我们在实现弹窗时,可以直接使用该组件。这里有一个重要的属性:unmountOnExit,表示在动效结束后,卸载该组件。const Modal = ({ open, onClose }) => { // http://reactcommunity.org/react-transition-group/css-transition/ // in属性为true/false,true为展开动效,false为关闭动效 return createPortal( {children} x , document.body, ); }; 在使用 CSSTransition 组件后,Modal 的动效就方便多了。 4. 总结 # 至此已把待动效的 React Modal 组件实现出来了。虽然 React 中没有类似 Vue 官方定义的标签,不过我们可以自己或者借助第三方组件来实现。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
如何使用 jest 和 lint-staged 只检测发生改动的文件
我们现在在推进 EPC 的过程中,单元测试是必备的技能,在本地的 Git commit 之前进行单测非常有必要,总不能把所有的单测的压力都放在流水线上。毕竟在流水线运行单测的成本还是挺高的,从 push 上去触发流水线,到感知单测的结果,至少需要好几分钟的时间。因此我们有必要在 git commit 进行一些单测的检测。不过若我们每次在 commit 之前都完整地运行所有的单测用例,一个是没必要,再一个是耗时很长。那应该怎么只运行有变动的文件的单测用例呢? 1. 使用 husky 和 lint-staged # 我们接下来要使用 husky 和 lint-staged 组件,来实现在 commit 之前检测只发生变动的文件。 husky可以让我们很方便地设置 pre-commit 的钩子; lint-staged组件能够在 Git commit 提交之前,获取上次 commit 到现在所有发生变动的文件。我们可以利用这个特性来运行 jest; 1.1 配置 husky 和 lint-staged # 首先我们来安装和配置这两个组件。$npm i husky lint-staged --save-dev $npm set-script prepare "husky install" $npm run prepare $npx husky add .husky/pre-commit "npx lint-staged" 运行完上述 4 个命令后,然后在package.json中配置 lint-staged:{ "scripts": { "test:staged": "jest --bail --findRelatedTests" }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx}": ["npm run test:staged"] } } 在 lint-staged 中支持node-glob通配符的配置,同时也支持配置多个,若发生变动的文件路径满足配置,则触发后面的命令。上面通配符的意思是:src 目录中任意路径的任意以.js 或.jsx 或.ts 或.tsx 结尾的文件。 1.2 配置 jest # 我们在上面的 test:staged 命令配置上了 jest。 bail: 只要遇到运行失败的单测用例即退出; findRelatedTests: 检测指定的文件路径; 其他更多的参数,可以直接查阅官方文档Jest CLI Options。很多公共的数据,我们可以直接在jest.config.js中进行配置:module.exports = { roots: ['/src'], // 查找src目录中的文件 collectCoverage: true, // 统计覆盖率 coverageDirectory: 'coverage', // 覆盖率结果输出的文件夹 coverageThreshold: { // 所有文件总的覆盖率要求 global: { branches: 60, functions: 60, lines: 60, statements: 60, }, // 匹配到的单个文件的覆盖率要求 // 这里也支持通配符的配置 './src/**/*.{ts,tsx}': { branches: 40, functions: 40, lines: 40, statements: 40, }, }, // 匹配单测用例的文件 testMatch: ['/src/**/__tests__/**/*.{js,jsx,ts,tsx}', '/src/**/*.{spec,test}.{js,jsx,ts,tsx}'], // 当前环境是jsdom还是node testEnvironment: 'jsdom', // 设置别名,若不设置,运行单测时会不认识@符号 moduleNameMapper: { '^@/(.*)$': '/src/$1', }, }; 上面两个配置完成后,当本次 commit 发生变动的文件满足要求,至少有 1 个文件满足时,则就会执行npm run test:staged。我们先运行下所有的测试用例:可以看到,我们实际上有 2 个源文件,3 个测试文件。不过 add 相关的已经在上次 commit 提交过了,本次提交时,只有 uitils 和 utils.test 有变动。$git add . $git ci -m 'test(utils): only test utils changed file' 从给出的测试报告能看出来,当前只检测了发生变动的 utils 文件: 2. 覆盖率的要求 # 我们在上面通过 jest.config.js 中的coverageThreshold属性,设置了全局覆盖率和单个文件的覆盖率。我们再在代码新增几个文件,但不配置对应的测试文件。然后运行时发现,如果没有对应的测试文件,就不会检查该文件的覆盖率。我这里特意把概率设置 100%,然后 math.js 没有对应的测试文件:从运行的测试结果来,这里只检测了有测试文件的 utils.js,并没有检测到 math.js。这里我们就要新增一个属性了collectCoverageFrom:{ collectCoverageFrom: ["src/**/*.{js,jsx,ts,tsx}"], } 这时,我们再运行单测时,就能把所有符合要求的文件,都纳入到覆盖率的考核里了。 3. collectCoverageFrom 的坑 # 我们在使用 commit 提交时,会触发lint-staged,按我们在第 1 节说的,应该只运行发生变动的实例,覆盖率也只输出当前运行的实例。但实际上并不是如此,若配置了collectCoverageFrom,无论是怎样运行单测,他都会输出所有符合要求的文件的覆盖率数据:从黄色框框中可以看到,我们本次只提交了 utils.js,按说应该只运行和计算 utils.js 的单测覆盖率即可,但实际上会把所有的覆盖率都输出出来,然后大部分数据为 0,无法满足设置的覆盖率的要求。但我们又不能不设置 collectCoverageFrom 属性,最后我的解决办法是:排除法:{ collectCoverageFrom: ['!src/**/*.d.ts', '!src/**/*{.json,.snap,.less,.scss}'], } 这样我们在 commit 提交时,就能满足只从被检测的文件中提取覆盖率。 4. 总结 # 工欲兴其事,必先利其器。当我们提前把配置搭建完成后,就可以进行开发啦。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
如何在 nodejs 的原生http服务中获取请求参数
在 nodejs 中,之前一直推荐的是url.parse方法来解析参数,不过这个方法已经不推荐了,现在推荐的是 WHATWG 网址的 API。因为网上找到的都还是之前的方法,这里特此记录下,在 nodejs 中,如何使用 URL 类 或 URLSearchParams 类来获取请求的参数。 1. 创建一个服务 # 我们先用http.createServer来创建一个简单的 http 服务:const http = require('http'); http .createServer((req, res) => { res.end('hello world'); }) .listen(8080, '127.0.0.1'); 在 nodejs 中起一个服务还是很简单的。 2. URL 的使用 # 我们在获取参数前,先讲解下URL的使用方式。URL 在浏览器和 nodejs 都可以支持,在 nodejs 中,是在 8.0.0 版本添加上的。我们在 nodejs 的 http 服务中,可以通过req.url获取到请求的路径和携带的参数:const url = '/api?a=1&b=2&c=ccc'; // req.url 2.1 解析 url # 那么我们可以通过 URL 类来解析这个 url。但要注意的是: 若只给 URL 传入一个参数,这个 url 必须是绝对地址,即需要携带协议,若 http://, https://, file://等;若不确定第一个参数中是否携带协议和主机信息,我们可以使用第二个参数进行补充。 当第 1 个参数含有协议和主机,则直接使用;否则会使用第 2 个参数中的信息进行补全。我们来测试几个:new URL('/api?a=1'); // Uncaught TypeError: Failed to construct 'URL': Invalid URL new URL('//www.qq.com'); // Invalid URL 解析错误 new URL('//www.qq.com', 'htts://example.com'); // 正确解析,origin: https://www.xiabingbao.com new URL('http://www.qq.com', 'https://example.com'); // 正确解析,还是使用自己的http协议, http://www.qq.com new URL('/api?a=1', 'https://example.com'); // 正确解析,href: https://example.com/api?a=1 可以看到第 1 个参数若是绝对地址,则直接使用;否则会将第 2 个参数与第 1 个参数拼接后,再进行解析。 2.2 解析参数 # 那么该如何解析参数呢?参数已经在实例的searchParams属性中,这个属性也是URLSearchParams的实例。const uu = new URL('/api?a=1&b=2&c=ccc', 'https://example.com'); const { searchParams } = uu; searchParams.get('a'); // '1' searchParams.get('b'); // '2' searchParams.get('c'); // 'ccc' searchParams.get('d'); // null searchParams.has('a'); // 是否有参数a [...searchParams.values()]; // 所有数值的集合 ['1', '2', 'ccc'] [...searchParams.keys()]; // 所有key的集合['a', 'b', 'c'] searchParams.forEach(console.log); // 循环所有的数据 searchParams.getAll('a'); // 获取所有的参数a ['1'] searchParams 中的keys(), values(), entries()等属于迭代器的方法。无法直接获取到一个 json 结构的数据,那么如何构造呢? 2.3 如何序列化参数 # 这里没有直接的方法来获取,我们需要通过迭代器的循环来自己实现,关于迭代器的知识,请参考https://es6.ruanyifeng.com/#docs/iterator:const obj = {}; for (let [key, value] of searchParams) { obj[key] = value; } console.log(obj); 这样就可以得到一个 object 类型的数据了。 3. nodejs 中的使用 # 我们在上面已经了解了 URL 的使用方法,在 nodejs 中怎么使用呢?const http = require('http'); http .createServer((req, res) => { const { searchParams } = new URL(req.url, 'https://example.com'); const username = searchParams.get('username') || 'wenzi'; res.end(`${username}, hello world\n`); }) .listen(8080, '127.0.0.1'); 解析完毕。当我们向接口发起请求并携带 username 参数时,接口就会对应的数据,这就说明解析成功了。
2024年10月20日
3 阅读
0 评论
0 点赞
1
...
45
46
47
...
213