前端开发tips
-
关于package.json里面,尖角号(^)和波浪线(~)的区别
在package.json里面,我们可以使用尖角号(^)和波浪线(~)来表示不同的包版本。这些符号通常被用于定义一个包所需的最小版本或允许更新的版本范围。
尖角号()通常用于指定主要版本号不变,允许安装新的次要版本和修补程序版本。例如,如果我们在package.json中指定"dependencies"对象的某个包的版本为"1.2.3",那么npm将会安装大于等于1.2.3版本,并且小于2.0.0版本的最新版本。
波浪线()通常用于指定包的修补程序版本不变,允许安装新的次要版本和主要版本。例如,如果我们在package.json中指定"dependencies"对象的某个包的版本为"1.2.3",那么npm将会安装大于等于1.2.3版本并且小于1.3.0版本的最新版本。
总的来说,使用尖角号和波浪线都是为了保证依赖的包可以兼容而又能够及时更新。建议在定义package.json时选择一个合适的版本控制方式来使得项目的依赖关系更加稳定。
-
执行npm install命令时发生版本冲突问题
// 执行npm install 命令发现报错:D:\StudySoft\VsCode\code\CODE_Projects\new-cms>npm install npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: panda@1.0.0 npm ERR! Found: react@17.0.2 npm ERR! node_modules/react npm ERR! react@"^17.0.2" from the root project npm ERR! peer react@">=16.9.0" from antd@4.24.8 npm ERR! node_modules/antd npm ERR! antd@"^4.21.2" from the root project npm ERR! peer antd@"4.x" from @ant-design/pro-card@1.0.6 npm ERR! node_modules/@ant-design/pro-card npm ERR! @ant-design/pro-card@"1.0.6" from the root project npm ERR! 1 more (react-dom)
这个报错是因为依赖树出现了问题,可能是由于部分依赖的版本冲突导致的。
你可以尝试以下几种方法来解决这个问题:
- 清空 node_modules 和 package-lock.json 文件,重新执行 npm install 命令。
- 使用 npm install --legacy-peer-deps 命令替代 npm install 命令,这条命令会忽略 peerDependencies 的版本限制。
- 更新 package.json 中的依赖版本号,使其符合 SemVer 规范。
- 更换包管理器为 yarn 或 pnpm,并尝试再次执行安装命令。
如果以上方法不能解决问题,建议检查一下项目中 package.json 中的依赖是否正确,并且检查网络连接状态是否正常。
-
快速定位到某个组件对应的位置步骤
- 项目跑起来后,在地址栏里找到组件关键字:
- 复制项目路由文件夹的相对路径:
- 结合组件关键字与路由相对路径,便可快速定位到改组件的路由:
- 在路由文件里便可清晰地看到组件定义的位置了:
-
关于CSS的模块化
在CSS中,模块化可以通过多种方式实现。以下是几种常见的方法:
- 命名约定:通过在样式规则中使用特定的命名前缀或后缀来标识该样式规则属于哪个模块。例如,如果您的网站包含一个名为“头部”的模块,您可以使用“-header”后缀来标识所有与该模块相关的样式规则。
- BEM(块、元素、修饰符)方法:这是一种广泛使用的CSS命名约定,它基于组件化设计的思想。使用BEM,每个模块都被视为一个独立的块(block),其中包含了多个元素(element),并且可以有零个或多个修饰符(modifier)。例如,一个名为“头部”的模块可以定义一个块元素“头部__logo”,以及一个带有修饰符的块元素“头部–transparent”。缺点:这种方式和方面的那种命名约定方式是比较传统的解决方案,但是随着应用规模的增大,命名冲突和代码重复的问题也越来越明显,增加了开发的复杂性和维护难度。
- CSS模块(CSS Modules):它是一种官方的CSS模块化解决方案,它利用Webpack、Vite等打包工具,将CSS样式表归档为模块,并自动管理CSS类名的作用域和命名。这使得CSS代码更易于维护和扩展,并且避免了全局污染和命名冲突的问题。在Vue框架的CSS作用域中,采取的是CSS模块(CSS Modules)中的局部作用域(Local Scope)方式。index.module.less 也是一种基于 CSS Modules 的 CSS 模块化方式,它可以在 React 项目中使用,可能产生不灵活的问题,比如如果想声明某个选择器在全局范围内生效,只能使用伪类:global。缺点:使用 CSS Modules 需要借助打包工具,并且需要保证每个组件的类名唯一,否则会影响样式的正确性。此外,CSS Modules 学习成本相对于其他方式较高,需要理解一些额外的语法和配置。
- CSS-in-JS:这是一种将CSS样式作为JavaScript对象嵌入到组件中的方法。使用CSS-in-JS,您可以将不同模块的样式定义在同一个文件或同一个组件内,并以动态方式根据组件状态或其他条件应用它们。常见的CSS-in-JS库包括Styled Components、Emotion等。缺点:虽然 CSS-in-JS 可以实现组件化的样式定义,并且能够更好地利用 JavaScript 的编程能力,但是需要在项目中引入额外的库和插件,增加了代码的复杂性和学习成本。
以上是几种常见的CSS模块化方式,每种方式都有其优缺点和适用场景。选择合适的方式可以让您的代码更具扩展性、可维护性和重用性,提高开发效率并减少错误。
-
关于定义类型时的命名规范
规范:大写I开头,每个单词首字母都大写,如果类型是数组,后面加上Item,如:
export interface IOperateInfoItem {action: stringname: stringcreateTime: stringtype: stringdocnumber: number }
-
git clone到本地的项目执行npm install命令报错
产生原因:权限问题
解决办法: https://blog.csdn.net/qq_34488939/article/details/121146658
(主要就是给node_global文件夹加权限)
之后若仍然安装失败报错,仔细看会发现并不是上述那个报错,而是安装某些包时报错,因为存在预依赖,所以执行npm i -f 强制安装即可:
-
关于写注释的技巧
以双斜杠这种方式写注释时:
导致如果其他地方用到这个变量,鼠标放上去不会有注释提示:
但如果以/** */这种方式注释时:
则如果其他地方用到这个变量,鼠标放上去会有注释提示:
-
泛型在接口类型定义时的应用
对于一些请求,接口返回的数据总有相同的字段,比如下面这种请求分页返回的data总会有current、page、records、searchCount、size、total等几个字段,但是records里面的字段可能就要具体情况具体定义。因此对于这种情况,可以采用泛型,将data定义为PagesuccessResponse,里面的records为泛型数组,然后便可以具体情况具体定义了:
-
关于企业项目的自动化部署流程
使用GitLab的Webhook功能来监听代码库中的变化,并自动触发部署流程。具体实现步骤如下:
-
在GitLab项目设置中的Webhooks选项中添加一个新的Webhook。将Webhook的URL地址指定为部署服务器上的一个接收请求的脚本。
-
编写部署服务器上的脚本,在接收到GitLab Webhook的请求时,解析请求中的数据,并根据解析结果触发相应的自动化部署流程。部署流程可以包括测试、构建、部署等多个步骤,可以使用Jenkins或Ansible等自动化部署工具来实现。
-
完成上述步骤后,每当GitLab代码库中发生变化时,部署服务器就会自动接收到Webhook请求并触发自动化部署流程。这样就可以实现自动化部署的目的,提高开发效率和部署质量。
-
-
git clone仓库项目时遇到权限问题及解决
如下图,第一次git clone某仓库时遇到权限问题:
解决方案:在本地生成git密码,添加到仓库中:
要在本地生成Git密钥,请按照以下步骤操作:
- 打开命令行或终端窗口。
- 输入以下命令:
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
。 - 按Enter键,将使用默认文件名和位置生成密钥。如果您希望使用不同的文件名或位置,请根据需要进行更改。
- 然后系统会提示您输入一个密码以保护您的密钥。如果您不想添加密码,可以直接按Enter键。
- 最后,将在指定位置生成两个文件:公钥(id_rsa.pub)和私钥(id_rsa)。
将公钥内容粘贴到里面即可:
此时便能成功git clone项目。
-
关于使用a标签时要注意的点
使用a标签时,一般除了设置href属性,还要设置 target=“_blank”,rel="noopener noreferrer"这两个属性。
target=“_blank” 用于在新窗口或者新标签页中打开链接,而不是在当前页面打开链接。
rel=“noopener noreferrer” 是一个安全属性,主要用于保护用户隐私安全。其中 noreferrer 指示浏览器在导航到目标资源时不要发送 Referer header(即告知目标站点来自哪个网站的信息),从而保护了用户浏览器的信息不被泄露。而 noopener 指示浏览器在接下来的新页面中取消对原页面的引用,防止被恶意页面通过 window.opener 访问到原页面中的权限,从而防止跨窗口脚本攻击。
这两个属性的组合使用可以有效预防一些潜在安全问题,建议在开发过程中养成使用的习惯。
-
关于px与rem之间的自动转化(使用postcss-pxtorem)
安装依赖:
pnpm install postcss-pxtorem
新建 postcss.config.js文件:
export default {plugins: {'postcss-pxtorem': {// 基准屏幕宽度rootValue: 192,// rem的小数点后位数unitPrecision: 2,propList: ['*'],exclude: function (file) {// console.log('postcss-pxtorem', file)// if (file.indexOf('node_modules') > -1 || file.indexOf('cms.module') > -1) {// console.log('postcss-pxtorem', file)// }return file.indexOf('node_modules') > -1;},},}, };
在根节点文件中引入:
import React from 'react'; import ReactDOM from 'react-dom/client'; import App from '@/App'; import '@/assets/global.less'; const onResize = () => {let width = document.documentElement.clientWidth;if (width > 1920) {width = 1920;}document.documentElement.style.fontSize = width / 10 + 'px'; }; // 初始化时,即页面挂载前就要执行一次,防止页面第一次加载时产生抖动 onResize(); window.addEventListener('resize', onResize); ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<React.Fragment><App /></React.Fragment>, );
在App.tsx中:
import HomePage from '@/pages/homePage'; import styles from './app.module.less'; function App() {return (<div className={styles.content}><HomePage></HomePage></div>); }export default App;
在src\app.module.less中:
.content {margin: 0 auto;max-width: 1920px; }
分出的每个组件的最外层的container的:
此后,便可以直接写设计稿的px单位大小,最大宽度设置为1920px,若超过这个宽度会居中,若小于这个宽度会缩小。
postcss-pxtorem 是一个 PostCSS 插件,用于将 CSS 中的像素单位(px)转换为 rem 单位,从而实现响应式布局。该插件的原理是通过遍历 CSS 样式文件中的每个规则,在其中检测并转换出现的像素单位,并根据约定的转换比例将其转换为 rem 单位。通常情况下,该插件会将视口宽度作为参考,以便在不同设备上获得一致的 UI 显示效果。
例如,在默认设置下,当 CSS 中出现 font-size: 16px; 的样式规则时,该插件将自动将其转换为 font-size: 1rem;,根据默认的转换比例(1px = 1/16rem)计算而得。这样可以确保在不同屏幕尺寸和分辨率下,UI 元素的大小和间距能够自适应地调整,提高网站或应用的可访问性和用户体验。
-
关于调试修改antd design组件样式的技巧
我们使用到antd design组件时,需要改变默认样式,如果我们想改变某个组件的样式,则首先需要找到某个组件标签的类名,一般在控制台通过鼠标选择查找到,对于一些需要触发才能显示的元素,有两种情况:hover触发或者组件本身有类似open:true/false(类似Dropdown组件,展开或收起通过open这个属性触发)
此时若想全局改变,则需要在样式文件里面属下类似下面的代码即可:
:global {.ant-dropdown .ant-dropdown-menu {box-shadow: 0px 7.41667px 22.25px rgba(54, 88, 255, 0.15);border-radius: 14.8333px;padding: 20px 10px 20px 10px;display: flex;flex-direction: column;justify-content: center;} }
此时若想只改变某个地方的该组件,则需要给该组件加上rootClassName,再去改变样式:
此时可以看到标签已经被挂载了类名:(rootClassName+生成的哈希(用于样式隔离,原理类似vue的scoped方法)
此时再去修改样式即可:
.dropdown {:global {.ant-dropdown-menu {box-shadow: 0px 7.41667px 22.25px rgba(54, 88, 255, 0.15);border-radius: 14.8333px;padding: 20px 10px 20px 10px;display: flex;flex-direction: column;justify-content: center;li {padding: 4.8px 36px 4.8px 36px !important;}}} }
-
关于根据设计稿制作网页时的屏幕适配、缩放操作适配问题
14-1 不要使用设计稿的决定定位
我们还原设计稿时,对于分出的每个组件的最外层的container,我们不要去给它设置固定高度和宽度,设置max-width即可 width: 100%; max-width: 1920px; ,其余由子元素撑开即可:
设置container里面的子元素时,记得不要用设计稿的绝对定位,因为那个是基于整个网页决定定位的,可能造成页面布局崩溃,在container里面以container为父盒子平铺即可。
14-2 关于莫名其妙的滚动条(涉及元素的默认宽度)
如果没有设置宽度,元素的默认宽度是100%。这意味着元素会填充其父元素的整个宽度。( 一些元素(如 )具有自己的默认宽度 ), 像下面这样:
当元素设置偏移后(left值或right值不为0),则会导致盒子溢出父盒子,致使整个页面出现滚动条:
此时可以用calc()计算确定盒子的宽度,防止上面情况的发生:
如果不是元素的默认宽度导致莫名其妙出现的滚动条,那么排查方法一般是先在根组件中依次删掉,看问题出现在哪个组件中,确定好之后再在组件里面删元素,看问题出现在哪个元素中。(一般是固定宽度过宽的元素导致的)
14-3 关于浏览器的12px限制
对于一些里面有文字的div,如果给这些div设置固定宽高,在页面缩小时,由于浏览器字体的12px限制,可能会使文字溢出div盒子,此时可以采取两种方案解决:
-
不给div设置宽高,设置padding,使里面的文字撑开div,防止溢出:
-
利用媒体查询,强制缩放文字:(采用这种方法时,记得给文字多套一层盒子,因为缩放是整个元素一起缩放的):
14-4 关于缩小屏幕时的处理(涉及到meta的viewport)
是一种描述网页视口的 meta 元素。
在移动设备上,网页通常需要适应不同的屏幕大小和分辨率。那么,在这种情况下,网页应该如何表现呢?viewport 元素就是来解决这个问题的。
具体而言,width=device-width 表示网页的宽度应该等于设备的宽度,而 initial-scale=1.0 表示网页的初始缩放比例为 100%。这个设置对于确保在移动设备上展示的网页可以正确响应用户的手势操作非常重要。
除了上面提到的两个属性之外,viewport 元素还有其他一些常用的属性,例如:
- height:设置 viewport 的高度;
- user-scalable:设置是否允许用户缩放网页;
- minimum-scale 和 maximum-scale:设置用户可以缩放的最小和最大值。
综上所述,viewport 元素是一种非常重要的网页元信息,可以帮助网页在移动设备上正确展示,并提供更加友好的用户体验。
如果去掉
<meta name="viewport" content="width=device-width, initial-scale=1.0">
,在移动设备上打开网页的时候,网页会自动进行缩放,导致网页中的元素变得很小。在没有移动端的设计稿时,不失为一种防止在移动端上布局样式崩溃的方法。如果没有设置宽度,元素的默认宽度是100%。这意味着元素会填充其父元素的整个宽度。一些元素(如
<button>
)具有自己的默认宽度), 像下面这样:当元素设置偏移后(left值或right值不为0),则会导致盒子溢出父盒子,致使整个页面出现滚动条:
此时可以用calc()计算确定盒子的宽度,防止上面情况的发生:
-
-
一个使用grid布局的案例
<div className={styles.innerface}><div className={styles.imageList}>{fourthImgs.innerfaceImgs.map((imgSrc, index) => (<div className={styles.item} key={index}><img src={imgSrc} alt="" /></div>))}</div></div>
.innerface {width: 1920px;height: 1024px;position: absolute;top: 3750px;left: 50%;transform: translate(-50%, 0);display: flex;justify-content: center;align-items: center;.imageList {display: grid;grid-template-columns: repeat(8, 1fr);grid-template-rows: repeat(3, auto);gap: 10px;width: 100%;height: 100%;opacity: 0.15;.item:nth-child(8n + 1),.item:nth-child(8n) {img {width: calc(50%);height: calc((100%) - 10px);}}.item:nth-child(8n) {text-align: right;}.item:not(:nth-child(8n + 1)):not(:nth-child(8n)) {position: relative;img {position: absolute;width: calc((100%));height: calc((100%) - 10px);}}.item:nth-child(8n + 2) {img {left: -75px;}}.item:nth-child(8n + 3) {img {left: -45px;}}.item:nth-child(8n + 4) {img {left: -15px;}}.item:nth-child(8n + 5) {img {right: -15px;}}.item:nth-child(8n + 6) {img {right: -45px;}}.item:nth-child(8n + 7) {img {right: -75px;}}}}
效果如下:
核心:img盒子外面记得再包一层盒子,然后用定位慢慢调位置。
-
关于项目中的多语言切换
多语言切换在很多场景中会用到,尤其类似官网的这种场景:
步骤如下:
-
封装一个Storage类及一些相关的类型和方法,方便我们操作和处理sessionStorage .
export const localStorageKey = 'com.drpanda.chatgpt.';interface ISessionStorage<T> {key: string;defaultValue: T; } // 重新封装的sessionStorage export class Storage<T> implements ISessionStorage<T> {key: string;defaultValue: T;constructor(key: string, defaultValue: T) {this.key = localStorageKey + key;this.defaultValue = defaultValue;}setItem(value: T) {sessionStorage.setItem(this.key, JSON.stringify(value));}getItem(): T {const value = sessionStorage[this.key] && sessionStorage.getItem(this.key);if (value === undefined) return this.defaultValue;try {return value && value !== 'null' && value !== 'undefined' ? (JSON.parse(value) as T) : this.defaultValue;} catch (error) {return value && value !== 'null' && value !== 'undefined' ? (value as unknown as T) : this.defaultValue;}}removeItem() {sessionStorage.removeItem(this.key);} }/** 管理token */ export const tokenStorage = new Storage<string>('authToken', '');/** 只清除当前项目所属的本地存储 */ export const clearSessionStorage = () => {for (const key in sessionStorage) {if (key.includes(localStorageKey)) {sessionStorage.removeItem(key);}} };
-
利用 React Context 实现一个状态管理库,使得所有组件都能轻易地获取到当前的状态(即语言类型),检测到状态改变即可重新渲染:
import React, { createContext, useContext, ComponentType, ComponentProps } from 'react';/** 创建context组合useState状态Store */ function createStore<T extends object>(store: () => T) {// eslint-disable-next-lineconst ModelContext: any = {};/** 使用model */function useModel<K extends keyof T>(key: K) {return useContext(ModelContext[key]) as T[K];}/** 当前的状态 */let currentStore: T;/** 上一次的状态 */let prevStore: T;/** 创建状态注入组件 */function StoreProvider(props: { children: React.ReactNode }) {currentStore = store();/** 如果有上次的context状态,做一下浅对比,* 如果状态没变,就复用上一次context的value指针,避免context重新渲染*/if (prevStore) {for (const key in prevStore) {if (Shallow(prevStore[key], currentStore[key])) {currentStore[key] = prevStore[key];}}}prevStore = currentStore;// eslint-disable-next-linelet keys: any[] = Object.keys(currentStore);let i = 0;const length = keys.length;/** 遍历状态,递归形成多层级嵌套Context */function getContext<V, K extends keyof V>(key: K, val: V, children: React.ReactNode): JSX.Element {const Context = ModelContext[key] || (ModelContext[key] = createContext(val[key]));const currentIndex = ++i;/** 返回嵌套的Context */return React.createElement(Context.Provider,{value: val[key],},currentIndex < length ? getContext(keys[currentIndex], val, children) : children,);}return getContext(keys[i], currentStore, props.children);}/** 获取当前状态, 方便在组件外部使用,也不会引起页面更新 */function getModel<K extends keyof T>(key: K): T[K] {return currentStore[key];}/** 连接Model注入到组件中 */function connectModel<Selected, K extends keyof T>(key: K, selector: (state: T[K]) => Selected) {// eslint-disable-next-line func-namesreturn function <P, C extends ComponentType>(WarpComponent: C,): ComponentType<Omit<ComponentProps<C>, keyof Selected>> {const Connect = (props: P) => {const val = useModel(key);const state = selector(val);// eslint-disable-next-line @typescript-eslint/ban-ts-comment// @ts-ignorereturn React.createElement(WarpComponent, {...props,...state,});};return Connect as unknown as ComponentType<Omit<ComponentProps<C>, keyof Selected>>;};}return {useModel,connectModel,StoreProvider,getModel,}; }export default createStore;/** 浅对比对象 */ function Shallow<T>(obj1: T, obj2: T) {if (obj1 === obj2) return true;for (const key in obj1) {if (obj1[key] !== obj2[key]) return false;}return true; }
上面这段代码是一个使用 React Context 实现的状态管理库,提供了 createStore 方法来创建一个状态 Store,通过 useModel 方法获取对应状态的值,在组件中使用 connectModel 方法连接对应的 Model 和组件,并且通过 StoreProvider 组件将状态注入整个应用中。其中状态的变化通过判断前后两次状态是否相同来避免无意义的重新渲染,使用了浅比较的方法来判断状态是否相同。
-
根据浏览器API设置默认语言,创建sessionStorage,如果切换语言则改变sessionStorage存储的值,同时负责将语言文件引入以便和状态管理器一起发挥作用:
import enUS from '@/locales/en-US'; import esES from '@/locales/es-ES'; import { Storage } from '@/common/storage'; import { useMemo, useState } from 'react'; import { useMemoizedFn } from 'tools';// 根据浏览器api获取当前语言 const getBrowserLanguage = () => {// 获取浏览器语言字符串const languageString = navigator.language || navigator.languages[0];// 将语言字符串拆分成语言和地区const [language, region] = languageString.split('-');// 返回语言return language; };const localesMap = { enUS, esES, default: getBrowserLanguage() === 'es' ? esES : enUS };type ILocale = 'enUS' | 'esES' | 'default'; /** 管理user */ export const localeStorage = new Storage<ILocale>('locale', undefined as unknown as ILocale);export default () => {const [locale, _setLocale] = useState<ILocale>(localeStorage.getItem() || 'default');const locales = useMemo(() => (locale ? localesMap[locale] : localesMap.default), [locale]);const setLocale = useMemoizedFn((value: ILocale | ((value: ILocale) => ILocale)) => {if (typeof value === 'function') {value = value(locale!);}localeStorage.setItem(value);_setLocale(value);});return {...locales,locale,setLocale,}; };
在上面默认导出的自定义Hook 中,首先使用 useState 定义了一个名为 locale 的状态变量,用于存储用户当前所选择的语言类型。默认值为 localeStorage.getItem() 或者 ‘default’。然后使用 useMemo 函数,根据当前的语言类型从语言包 localesMap 中获取对应的翻译文本。如果当前语言类型为 falsy 值,则使用默认语言 ‘default’ 的翻译文本。最后使用 useMemoizedFn 函数,定义一个 setLocale 方法,用于修改当前语言类型。如果传入的是一个函数,则先根据当前语言类型执行该函数,得到要修改的新语言类型,然后将该语言类型存储到本地存储中,并修改当前的语言类型变量。最后将 locales、locale 和 setLocale 包装成一个对象返回。
语言文件如下:
import { ILocales } from '../types'; import home from './home'; import second from './second'; import third from './third'; import forth from './forth'; import fifth from './fifth'; import contact from './contact';const enUS: ILocales = {home,second,third,forth,fifth,contact, };export default enUS;
-
根据第二步、第三步中创建一个用于管理语言状态的全局状态管理库,并导出相关方法供外部使用:
import createStore from './createStore'; import locales from './modules/locales';const store = () => ({locales: locales(), });const contextResult = createStore(store);export const { useModel, StoreProvider, getModel, connectModel } = contextResult;
-
在组件中实现切换语言、使用相应状态的语言包:
-
-
关于基于fetch封装的请求方法(包含添加拦截器)
export interface IRequestOptions {method?: 'GET' | 'POST' | 'PUT' | 'DELETE';headers?: { [key: string]: string };body?: BodyInit; } // 添加泛型 export async function request<T>(url: string, options: IRequestOptions = {}): Promise<T> {const response = await fetch(url, {method: options.method || 'GET',headers: options.headers || {'Content-Type': 'application/json',},body: options.body,});if (!response.ok) {throw new Error(`Request failed with status code ${response.status}`);}const data = (await response.json()) as T;return data; }
此时便可在其它地方使用了:
import { paramsType, resType } from './type'; import { request } from '@/utils/request';export async function feedbackSubmit(params: paramsType): Promise<resType> {const data: resType = await request('https://api.example.com/data', {method: 'POST',body: JSON.stringify(params),});return data; }
注意:
上面的feedbackSubmit请求方法是一个异步请求,如果向下面这样:
setLoading(true); try {feedbackSubmit(contactMsg).then((res) => {if (res.code === 0) {message.success(contact.status.success);} else if (res.code === 101) {message.error(contact.status.throttle);} else {message.error(contact.status.fail);}setLoading(false);}); } catch {message.error(contact.status.fail);setLoading(false);return; }
如果接口报错,那么应该是 feedbackSubmit() 方法抛出了一个错误,并且没有被处理。在此情况下,try catch 是不能捕捉到这个错误的,因为它只能处理同步异常。而 feedbackSubmit() 方法是一个异步方法,所以你需要在回调函数中处理异常。你可以在then的第二个参数中传入回调函数,处理接口报错的情况。例如:
setLoading(true); feedbackSubmit(contactMsg).then((res) => {if (res.code === 0) {message.success(contact.status.success);} else if (res.code === 101) {message.error(contact.status.throttle);} else {message.error(contact.status.fail);}setLoading(false);}).catch(() => {message.error(contact.status.fail);setLoading(false);});
附:添加拦截器的代码:
export interface IRequestOptions {method?: 'GET' | 'POST' | 'PUT' | 'DELETE';headers?: { [key: string]: string };body?: BodyInit; }// 定义拦截器的接口 interface Interceptor<T> {onFulfilled?: (value: T) => T | Promise<T>;onRejected?: (error: any) => any; }// 定义拦截器管理类--用于管理多个拦截器,可以通过use()方法向拦截器数组中添加一个拦截器,可以通过forEach()方法对所有的拦截器进行遍历和执行。 class InterceptorManager<T> {private interceptors: Array<Interceptor<T>>;constructor() {this.interceptors = [];}use(interceptor: Interceptor<T>) {this.interceptors.push(interceptor);}forEach(fn: (interceptor: Interceptor<T>) => void) {this.interceptors.forEach((interceptor) => {if (interceptor) {fn(interceptor);}});} }// 添加拦截器的 request 函数 export async function request<T>(url: string, options: IRequestOptions = {}): Promise<T> {const requestInterceptors = new InterceptorManager<IRequestOptions>();const responseInterceptors = new InterceptorManager<any>();// 添加请求拦截器requestInterceptors.use({onFulfilled: (options) => {// 处理请求console.log('请求拦截器:处理请求');return options;},onRejected: (error) => {console.log('请求拦截器:处理错误', error);return error;},});// 添加响应拦截器responseInterceptors.use({onFulfilled: (response) => {// 处理响应console.log('响应拦截器:处理响应');return response.json();},onRejected: (error) => {console.log('响应拦截器:处理错误', error);return error;},});// 处理请求拦截器--遍历所有的请求拦截器,并执行onFulfilled()方法,将返回值赋值给optionsrequestInterceptors.forEach(async (interceptor) => {options = await interceptor.onFulfilled?.(options) ?? options;});let response = await fetch(url, {method: options.method || 'GET',headers: options.headers || {'Content-Type': 'application/json',},body: options.body,});if (!response.ok) {throw new Error(`Request failed with status code ${response.status}`);}// 处理响应拦截器--遍历所有的响应拦截器,并执行onFulfilled()方法,将返回值赋值给responseresponseInterceptors.forEach((interceptor) => {response = interceptor.onFulfilled?.(response) ?? response;});return response.json() as Promise<T>; }
这段代码是一个封装了拦截器的 fetch 请求函数,通过调用 request 函数可以发送请求,并对请求和响应进行拦截和处理。
具体来说,定义了一个 IRequestOptions 接口来表示请求参数,指定了请求方法和请求头等参数;定义了一个 Interceptor 类型来表示拦截器,其中包括 onFulfilled 和 onRejected 两个方法,分别表示请求成功和请求失败后的处理函数;定义了一个 InterceptorManager 类来管理拦截器数组,其中包括 use 添加拦截器和 forEach 遍历拦截器的方法。
在 request 函数中,先创建了请求拦截器和响应拦截器,使用 use 方法添加拦截器,并在请求拦截器中处理请求,在响应拦截器中处理响应。最后返回处理后的响应数据。
-
关于代理服务
18-1 vite中配置代理解决跨域访问的方法(用于本地跨域访问)
对于生产环境的接口地址,我们进行请求时一般要配置代理以解决跨域问题:
本地进行请求时:
server: {open: true,proxy: {'/uis': {target: 'http://subs-global.xiongmaoboshi.com',changeOrigin: true,rewrite: (path) => path.replace(/^/api/, ''),// 由于网站部署在后端的OSS(云服务器)上,不经过前端的node服务,前端无法通过nginx配置代理实现跨域访问// 所以对于线上的生产环境,需要后端开启访问白名单,允许前端的域名访问// 但是本地开发环境,由于没有后端,所以需要通过vite的代理配置来实现跨域访问// 但是这里有个问题,就是代理配置的headers中的Origin,必须和请求的Origin一致,否则会报错(403Forbidden)// 虽然我们在这里设置了代理的headers,但是打开控制台会看到请求的headers中,Origin并没有被设置仍然是本地http://127.0.0.1:5173// 但实质上,vite代理服务器帮我们转发请求的时候,Origin已经被设置为了http://subs-global.xiongmaoboshi.com了,只是控制台没有显示出来headers: {Origin: 'http://subs-global.xiongmaoboshi.com',},},},// // 放在这里是设置全局的了,没必要,我们只需要设置代理的时候,才需要设置// headers: {// Origin: 'http://subs-global.xiongmaoboshi.com',// },},
配置好代理后,便可在本地进行请求该地址了:
import { paramsType, resType } from './type'; import { request } from '@/utils/request';export async function feedbackSubmit(params: paramsType): Promise<resType> {const data: resType = await request('/uis/xxx/xxx', {method: 'POST',body: JSON.stringify(params),});return data; }
18-2 vite中配置代理解决跨域的原理
原理:
利用了 Vite 内部集成的开发服务器和 Connect 中间件框架,通过在开发服务器上设置代理服务器,将请求转发到另一个服务器上。代理服务器不是浏览器,不受同源策略的限制,因此可以向任意域名下的接口发起请求。具体来说,开发服务器通过监听端口接收来自浏览器的请求,当收到符合代理规则的请求时,会将请求转发到目标服务器上,并将响应返回给浏览器。代理服务器在转发请求的同时,可以修改请求头、请求体、目标 URL 等信息,从而帮助开发者解决跨域、请求重定向、统一接口前缀等问题。
在本例中,使用了 http-proxy-middleware 库,该库封装了 Connect 中间件的代理功能,并在处理请求前进行了路径重写,将请求路径中的前缀 /uis 替换为 /api,以便将请求发送到目标服务器的正确接口上。
18-3 nginx代理解决跨域(用于部署在自己的服务器上的情况,否则需要后端开启访问白名单)
vite中配置代理解决跨域,一般是用于本地访问的。如若需要上线后跨域访问,则可以使用nginx作反向代理,从而实现跨域请求。配置如下:
server {server_name book-wavesgzip on;location / {root /web-project/bookwaves-web;index index.html index.htm;try_files $uri $uri /index.html;}location /uis {proxy_pass http://subs-global.xiongmaoboshi.com; } }
18-4 设置环境变量判断是本地开发环境还是线上生产环境
在上面的叙述中我们知道,在本地是通过启用vite的代理服务器来实现跨域访问的,在线上是通过后端设置访问白名单来实现跨域访问的。我们必须设置一个环境变量判断是本地开发环境还是线上生产环境,因为它们的请求接口不同:
import { paramsType, resType } from './type'; import { request } from '@/utils/request';export async function feedbackSubmit(params: paramsType): Promise<resType> {// 本地时,由于有vite的代理服务,我们只需要在请求时,把这里的请求路径改为'/uis/ns/sendEmail'即可,因为会被代理服务转发到线上的地址// 但是线上时,由于没有代理服务,所以我们需要在请求时,把这里的请求路径改为'http://subs-global.xiongmaoboshi.com/uis/ns/sendEmail',因为没有代理服务,所以不会被转发到线上的地址let url = '';if (process.env.NODE_ENV === 'development') {url = '/uis/ns/sendEmail';} else {// 项目上线后申请了https证书,所以这里的地址需要改为https,否则会报错url = 'https://subs-global.xiongmaoboshi.com/uis/ns/sendEmail';}const data: resType = await request(url, {method: 'POST',body: JSON.stringify(params),});return data; }
18-5 需要配置代理的情况
浏览器的同源策略限制了前端页面向不同域名的接口发起请求,这导致某些情况下需要使用代理服务器来转发请求。一般来说,这种情况包括以下几种:
- 使用第三方 API 或服务:例如,使用第三方地图 API 服务,需要向 API 服务提供商的域名下的接口发起请求,而这与前端页面所在的域名不同。
- 开发环境与生产环境不同:在开发环境中,前端页面通常运行在本地的开发服务器上,而后端服务则运行在远程服务器上。在这种情况下,由于开发服务器与后端服务器的域名不同,因此需要使用代理服务器将请求转发到正确的后端服务端点。
- 部分接口需要登录认证:在某些情况下,服务端需要对接口进行访问控制,需要用户先在页面进行登录认证。这时,前端页面需要先向自己的域名下的登录接口发起请求进行认证,获得认证信息后,再使用代理服务器将包含认证信息的请求转发到相应的接口上。
18-6 代理带来安全性问题及解决
代理可能带来安全性问题(谁都可以请求接口)。因此在某些情况下,服务端需要对接口进行访问控制,需要用户先在页面进行登录认证(例如使用用户名和密码登录、验证码二次验证)。这时,前端页面需要先向自己的域名下的登录接口发起请求进行认证,获得认证信息后,再使用代理服务器将包含认证信息的请求转发到相应的接口上。(使用 token 进行认证):
对于这类接口,通常会在用户成功登录后,后端会生成一个 token 并返回给前端,前端保存这个 token 在客户端,并在后续的请求中携带这个 token,以便服务器能够对请求进行认证。服务器收到带有 token 的请求后,会验证 token 是否合法,以此决定是否允许请求访问相应的资源。****
这种方式的优点是服务器不需要为每个访问请求进行单独的 cookie-session 保存,整个流程的 stateless 特点也使得服务器可以更轻松地进行水平扩展以支持高并发。
18-7 关于token的携带及设置
Token 通常在请求头的 Authorization 字段中携带,其格式为
Bearer <token>
,其中<token>
是后端认证生成的令牌。这种方式被称为 Bearer Token 认证协议,其实现方式如下所示:Authorization: Bearer <token>
其中 Bearer 是认证协议类型,类似于 Basic 和 Digest,可以指定其他类型的认证方式。
<token>
是后端生成的认证令牌,通常为随机字符串,可以是 JSON Web Token (JWT) 、OAuth Token 等多种形式。前端在发送请求时,需要将 Authorization 字段设置为对应的 token 值,以便后端可以从请求头中解析出 token 并进行认证。例如,在 JavaScript 中可以使用 fetch API 或者 axios 库设置请求头:
// 使用 fetch API const token = 'your_token_here' fetch('/api/some-resource', {headers: {Authorization: 'Bearer ' + token} })// 使用 axios const token = 'your_token_here' axios.get('/api/some-resource', {headers: {Authorization: 'Bearer ' + token} })
-
关于环境变量
19-1 环境变量的概念
系统的环境变量是指操作系统中设置的全局变量,它们是指定操作系统和其他应用程序在运行时所需的一些参数和路径的变量。
常见的环境变量包括:
- PATH:指定可执行文件所在的路径,当用户输入一个命令时,系统会在PATH中指定的路径中查找可执行文件。
- HOME:指定当前用户的主目录路径。
- TEMP / TMP:指定临时文件的存放路径。
- LANG / LC_ALL:指定系统的语言环境。
用户也可以自己创建自定义的环境变量来存储一些自己需要的参数和配置信息。在Windows操作系统中,可以通过“系统变量”和“用户变量”来设置环境变量。在Linux或Unix系统中,可以使用“export”命令来设置环境变量。
使用环境变量能够提高应用程序的可移植性和灵活性,因为不同的操作系统和应用程序都可以通过环境变量来适应不同的配置和需求。
19-2 环境变量在前端代码编写中发挥的作用
后端写的接口,在开发环境、生产环境的url可能是不同的,作为前端,我们调用接口时,要判断当前是开发环境还是生产环境来选择调用不同的接口。像下面这样:
import { paramsType, resType } from './type'; import { request } from '@/utils/request';export async function feedbackSubmit(params: paramsType): Promise<resType> {// 本地时,由于有vite的代理服务,我们只需要在请求时,把这里的请求路径改为'/uis/ns/sendEmail'即可,因为会被代理服务转发到线上的地址// 但是线上时,由于没有代理服务,所以我们需要在请求时,把这里的请求路径改为'http://subs-global.xiongmaoboshi.com/uis/ns/sendEmail',因为没有代理服务,所以不会被转发到线上的地址let url = '';if (process.env.NODE_ENV === 'development') {url = '/uis/ns/sendEmail';} else {// 项目上线后申请了https证书,所以这里的地址需要改为https,否则会报错url = 'https://subs-global.xiongmaoboshi.com/uis/ns/sendEmail';}const data: resType = await request(url, {method: 'POST',body: JSON.stringify(params),});return data; }
19-3 关于node环境与浏览器环境访问环境变量的区别
先说结论:浏览器本身并不直接支持访问系统环境变量,Node.js可以访问环境变量。
浏览器是运行在用户操作系统之上的应用程序,它是通过操作系统提供的API和驱动程序来与系统硬件通信的。
系统环境变量是系统级别的配置信息,它们是指定操作系统和其他应用程序在运行时所需的一些参数和路径的变量。由于环境变量可能涉及到系统级别的安全问题,因此浏览器不能直接访问它们,以避免存在安全漏洞。
此外,不同的操作系统所使用的环境变量的名称和取值也可能会存在差异。因此,浏览器并不能像Node.js一样直接访问操作系统的环境变量。
作为替代方案,浏览器提供了一些本地存储机制(如localStorage和sessionStorage),以及一些浏览器扩展API(如Chrome的chrome.storage和Firefox的browser.storage),开发者可以使用这些API来存储和读取浏览器级别的配置信息和用户设置,从而实现类似的功能。.
Node.js是一个基于JavaScript的服务器端开发平台,由于其运行在服务器端而非浏览器中,可以直接使用底层操作系统提供的API来访问系统环境变量。
在Node.js中,环境变量使用process.env属性进行管理。process对象是Node.js内置对象的一个全局对象,它提供了对当前进程的相关信息以及控制进程操作的方法。process.env属性是一个表示当前操作系统环境变量的键值对集合。
但是利用vite构建的web应用程序,其控制台输入console.log(process.env),是能打印出东西的:
在Vite开发环境下,并不是直接运行在浏览器中的,而是先通过Node.js对代码进行预处理,并将代码转换为浏览器可执行的JavaScript文件,因此在Vite开发环境下,可以通过Node.js提供的process对象来访问系统环境变量。
很多前端框架(如React和Vue.js)在开发环境下都会集成类似于Vite、Webpack等打包工具,这些打包工具可以在编译代码时将环境变量注入到应用程序中,从而在应用程序中使用环境变量。这些前端框架一般都提供了自己的方式来获取环境变量,一般是通过在代码中读取process.env对象中的变量来实现。在开发环境下,也可以在控制台中打印出process.env对象,但是这并不是直接访问操作系统的环境变量,而是打印出了当前应用程序中注入的环境变量。在生产环境下,由于安全的原因,通常不建议在控制台中暴露环境变量信息。
19-4 借助cross-env手动设置环境变量
在vite中,自带了【环境变量和模式】的配置,帮助我们手动设置一些环境变量,但是这些配置却显得不是很好用,因此我们可以借助cross-env这个包来优雅灵活地手动设置环境变量。
安装依赖:
pnpm install cross-env
此时便可以在package.json中设置我们的环境变量:
此时控制台打印环境变量的值,便可以看到环境变量被注入了:
-
使用vite-plugin-html向html模板注入内容
Github地址: https://github.com/vbenjs/vite-plugin-html
在有些时候,我们的网页要做出一些seo的配置,如title、description、keywords等,如果我们想后台自定义这些内容,则需要借助vite-plugin-html插件,调用相关接口获取内容向html文件注入。步骤如下:
- 安装依赖:
- 同时,因fetch 函数是在浏览器环境中全局定义的,所以在浏览器环境中可以直接使用。但是,在 vite.config.ts 中使用 fetch 函数时,可能还未加载到浏览器环境中,所以需要特别处理才能在 vite.config.ts 中使用。需要使用 node-fetch 这个第三方模块将 fetch 函数兼容到 node.js 环境中,这样就可以在 vite.config.ts 中直接使用 fetch 函数:
- 在 vite.config.ts 文件中添加如下代码将 fetch 函数兼容到 node.js 环境:
import fetch from 'node-fetch' (global as any).fetch = fetch
- 在 vite.config.ts 中书写接口调用函数来获取内容:
// 接口返回数据的类型 interface IHtmlHeadContent {seo: {title: string;description: string;keywords: string;}; } async function getHtmlHeadContent(): Promise<IHtmlHeadContent> {let url = '';// 判断是否是生产环境if (process.env.NODE_ENV === 'development') {url = 'https://www.book-waves.com/dev/home/data.json';} else {url = 'https://www.book-waves.com/home/data.json';}const response = await fetch(url);const data = await response.json();return data as IHtmlHeadContent; }
- 向html文件中注入:
plugins: [react(),createHtmlPlugin({minify: true,/*** 需要注入 index.html ejs 模版的数据*/inject: {data: {title: (await getHtmlHeadContent()).seo.title,description: (await getHtmlHeadContent()).seo.description,keywords: (await getHtmlHeadContent()).seo.keywords,},},}), ],
- 在html文件中获取到注入的内容:
<title><%- title %></title> <meta name="description" content="<%= description %>" /> <meta name="keywords" content="<%= keywords %>" />
-
关于antd design的Form获取实例来设置表单回显
如果现在想实现一个回显需求,设置被Form包裹的Input标签和TextArea标签的初始值,如果通过下面这样,通过ref获取标签实例再去设置是不可行的:
const emailTitleRef = useRef<InputRef>(null) const emailMsgRef = useRef<HTMLDivElement>(null)<Form><Form.Item label='邮件主题' name='emailTitle' rules={[{ required: true }]}><Inputplaceholder='请输入邮件主题 - 注意长度和语言'onChange={e => setEmailTitle(e.target.value)}ref={emailTitleRef}/></Form.Item><Form.Item label='邮件正文' name='emailContent'><TextAreaplaceholder='请输入邮件正文 - 仅支持「文本」或「图片」'disabled={!!emailImageList[0]}onChange={e => setEmailContent(e.target.value)}ref={emailMsgRef}/></Form.Item><Form.Item label=' ' name='loadImage'><><Buttonicon={<UploadOutlined />}type='primary'disabled={!!emailContent}onClick={handleUploadImage}>上传图片</Button>{emailImageList[0] && (<div className={styles.upLoad}><PaperClipOutlined />{emailImageList[0]}<DeleteOutlined onClick={handleRemoveImage} /></div>)}</></Form.Item> </Form>// 不起作用 // emailTitleRef.current.input.defaultValue = cnTitle || enTitle // emailMsgRef.current.input.defaultValue = cnMsg || enMsg
这是因为Form包裹后,里面的组件变成了受控组件,只能通过Form提供的方法Form.useForm去获取整个表单的实例,再通过这个实例去设置子项的值:
const emailFillingInstance = Form.useForm(null)[0]<Form form={emailFillingInstance}><Form.Item label='邮件主题' name='emailTitle' rules={[{ required: true }]}><Inputplaceholder='请输入邮件主题 - 注意长度和语言'onChange={e => setEmailTitle(e.target.value)}/></Form.Item><Form.Item label='邮件正文' name='emailContent'><TextAreaplaceholder='请输入邮件正文 - 仅支持「文本」或「图片」'disabled={!!emailImageList[0]}onChange={e => setEmailContent(e.target.value)}/></Form.Item><Form.Item label=' ' name='loadImage'><><Buttonicon={<UploadOutlined />}type='primary'disabled={!!emailContent}onClick={handleUploadImage}>上传图片</Button>{emailImageList[0] && (<div className={styles.upLoad}><PaperClipOutlined />{emailImageList[0]}<DeleteOutlined onClick={handleRemoveImage} /></div>)}</></Form.Item> </Form>// 设置值--起作用了 emailFillingInstance?.setFieldsValue({emailTitle: cnTitle || enTitle,emailContent: cnMsg || enMsg, })
也可以给实例传递泛型:
const [emailFillingInstance] = Form.useForm<{ emailTitle: string; emailContent: string }>()<Form form={emailFillingInstance}><Form.Item label='邮件主题' name='emailTitle' rules={[{ required: true }]}><Inputplaceholder='请输入邮件主题 - 注意长度和语言'onChange={e => setEmailTitle(e.target.value)}/></Form.Item><Form.Item label='邮件正文' name='emailContent'><TextAreaplaceholder='请输入邮件正文 - 仅支持「文本」或「图片」'disabled={!!emailImageList[0]}onChange={e => setEmailContent(e.target.value)}/></Form.Item><Form.Item label=' ' name='loadImage'><><Buttonicon={<UploadOutlined />}type='primary'disabled={!!emailContent}onClick={handleUploadImage}>上传图片</Button>{emailImageList[0] && (<div className={styles.upLoad}><PaperClipOutlined />{emailImageList[0]}<DeleteOutlined onClick={handleRemoveImage} /></div>)}</></Form.Item> </Form>// 设置值--起作用了 emailFillingInstance?.setFieldsValue({emailTitle: cnTitle || enTitle,emailContent: cnMsg || enMsg, })
针对可动态增减表单项这种情况,可通过getFieldValue方法获取传入的值:
const [welfareTypeInstance] = Form.useForm<{ welfareType: string[] }>()<Form disabled={!isWelfare || componentType === 1} form={welfareTypeInstance}><Form.List name='welfareType'>{(fields, { add, remove }) => {// 获取传过来的值const welfareType = welfareTypeInstance.getFieldValue('welfareType')return (<><Form.Item label='福利类型' name='welfareIdCheck'><CheckboxonChange={e => setIsWelfareId(e.target.checked)}checked={isWelfareId}>福利ID</Checkbox></Form.Item>{fields.map((field, index) => (<Form.Item key={field.key} name={[field.name]}><><Inputvalue={welfareType[index]}disabled={!isWelfareId || componentType === 1}onChange={event => handleGetWelfareId(index, event.target.value)}/>{!isWelfare || componentType === 1 ? (<DeleteOutlined />) : (<DeleteOutlinedonClick={() => {remove(field.name)const welfareIdList = welfareIdswelfareIdList.splice(index, 1)setWelfareIds(welfareIdList)}}/>)}</></Form.Item>))}<Form.Item name='welfareIdAdd'><ButtononClick={() => {add()setWelfareIdNum(welfareIdNum + 1)}}disabled={!isWelfareId || componentType === 1}><PlusOutlined /></Button></Form.Item></>)}}</Form.List> </Form>// 给Form.List传值 welfareTypeInstance?.setFieldsValue({welfareType: welfareIdList, })