莘县网站开发,建阅读网站,wordpress搜索全站,深圳网站建设注意事项前端开发tips 关于package.json里面#xff0c;尖角号#xff08;^#xff09;和波浪线#xff08;~#xff09;的区别 在package.json里面#xff0c;我们可以使用尖角号#xff08;^#xff09;和波浪线#xff08;~#xff09;来表示不同的包版本。这些符号通常被…前端开发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-cmsnpm install
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: panda1.0.0
npm ERR! Found: react17.0.2
npm ERR! node_modules/react
npm ERR! react^17.0.2 from the root project
npm ERR! peer react16.9.0 from antd4.24.8
npm ERR! node_modules/antd
npm ERR! antd^4.21.2 from the root project
npm ERR! peer antd4.x from ant-design/pro-card1.0.6
npm ERR! node_modules/ant-design/pro-card
npm ERR! ant-design/pro-card1.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_emailexample.com。按Enter键将使用默认文件名和位置生成密钥。如果您希望使用不同的文件名或位置请根据需要进行更改。然后系统会提示您输入一个密码以保护您的密钥。如果您不想添加密码可以直接按Enter键。最后将在指定位置生成两个文件公钥id_rsa.pub和私钥id_rsa。 将公钥内容粘贴到里面即可 此时便能成功git clone项目。 关于使用a标签时要注意的点 使用a标签时一般除了设置href属性还要设置 target“_blank”relnoopener 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.FragmentApp //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触发或者组件本身有类似opentrue/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 元素就是来解决这个问题的。 具体而言widthdevice-width 表示网页的宽度应该等于设备的宽度而 initial-scale1.0 表示网页的初始缩放比例为 100%。这个设置对于确保在移动设备上展示的网页可以正确响应用户的手势操作非常重要。 除了上面提到的两个属性之外viewport 元素还有其他一些常用的属性例如 height设置 viewport 的高度user-scalable设置是否允许用户缩放网页minimum-scale 和 maximum-scale设置用户可以缩放的最小和最大值。 综上所述viewport 元素是一种非常重要的网页元信息可以帮助网页在移动设备上正确展示并提供更加友好的用户体验。 如果去掉 meta nameviewport contentwidthdevice-width, initial-scale1.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 ISessionStorageT {key: string;defaultValue: T;
}
// 重新封装的sessionStorage
export class StorageT implements ISessionStorageT {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 Storagestring(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 createStoreT extends object(store: () T) {// eslint-disable-next-lineconst ModelContext: any {};/** 使用model */function useModelK 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 getContextV, 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 getModelK extends keyof T(key: K): T[K] {return currentStore[key];}/** 连接Model注入到组件中 */function connectModelSelected, K extends keyof T(key: K, selector: (state: T[K]) Selected) {// eslint-disable-next-line func-namesreturn function P, C extends ComponentType(WarpComponent: C,): ComponentTypeOmitComponentPropsC, 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 ComponentTypeOmitComponentPropsC, keyof Selected;};}return {useModel,connectModel,StoreProvider,getModel,};
}export default createStore;/** 浅对比对象 */
function ShallowT(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 StorageILocale(locale, undefined as unknown as ILocale);export default () {const [locale, _setLocale] useStateILocale(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 requestT(url: string, options: IRequestOptions {}): PromiseT {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): PromiseresType {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 InterceptorT {onFulfilled?: (value: T) T | PromiseT;onRejected?: (error: any) any;
}// 定义拦截器管理类--用于管理多个拦截器可以通过use()方法向拦截器数组中添加一个拦截器可以通过forEach()方法对所有的拦截器进行遍历和执行。
class InterceptorManagerT {private interceptors: ArrayInterceptorT;constructor() {this.interceptors [];}use(interceptor: InterceptorT) {this.interceptors.push(interceptor);}forEach(fn: (interceptor: InterceptorT) void) {this.interceptors.forEach((interceptor) {if (interceptor) {fn(interceptor);}});}
}// 添加拦截器的 request 函数
export async function requestT(url: string, options: IRequestOptions {}): PromiseT {const requestInterceptors new InterceptorManagerIRequestOptions();const responseInterceptors new InterceptorManagerany();// 添加请求拦截器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 PromiseT;
}这段代码是一个封装了拦截器的 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): PromiseresType {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): PromiseresType {// 本地时由于有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): PromiseresType {// 本地时由于有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(): PromiseIHtmlHeadContent {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 namedescription content% description % /
meta namekeywords content% keywords % /关于antd design的Form获取实例来设置表单回显 如果现在想实现一个回显需求设置被Form包裹的Input标签和TextArea标签的初始值如果通过下面这样通过ref获取标签实例再去设置是不可行的 const emailTitleRef useRefInputRef(null)
const emailMsgRef useRefHTMLDivElement(null)FormForm.Item label邮件主题 nameemailTitle rules{[{ required: true }]}Inputplaceholder请输入邮件主题 - 注意长度和语言onChange{e setEmailTitle(e.target.value)}ref{emailTitleRef}//Form.ItemForm.Item label邮件正文 nameemailContentTextAreaplaceholder请输入邮件正文 - 仅支持「文本」或「图片」disabled{!!emailImageList[0]}onChange{e setEmailContent(e.target.value)}ref{emailMsgRef}//Form.ItemForm.Item label nameloadImageButtonicon{UploadOutlined /}typeprimarydisabled{!!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邮件主题 nameemailTitle rules{[{ required: true }]}Inputplaceholder请输入邮件主题 - 注意长度和语言onChange{e setEmailTitle(e.target.value)}//Form.ItemForm.Item label邮件正文 nameemailContentTextAreaplaceholder请输入邮件正文 - 仅支持「文本」或「图片」disabled{!!emailImageList[0]}onChange{e setEmailContent(e.target.value)}//Form.ItemForm.Item label nameloadImageButtonicon{UploadOutlined /}typeprimarydisabled{!!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邮件主题 nameemailTitle rules{[{ required: true }]}Inputplaceholder请输入邮件主题 - 注意长度和语言onChange{e setEmailTitle(e.target.value)}//Form.ItemForm.Item label邮件正文 nameemailContentTextAreaplaceholder请输入邮件正文 - 仅支持「文本」或「图片」disabled{!!emailImageList[0]}onChange{e setEmailContent(e.target.value)}//Form.ItemForm.Item label nameloadImageButtonicon{UploadOutlined /}typeprimarydisabled{!!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 namewelfareType{(fields, { add, remove }) {// 获取传过来的值const welfareType welfareTypeInstance.getFieldValue(welfareType)return (Form.Item label福利类型 namewelfareIdCheckCheckboxonChange{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 namewelfareIdAddButtononClick{() {add()setWelfareIdNum(welfareIdNum 1)}}disabled{!isWelfareId || componentType 1}PlusOutlined //Button/Form.Item/)}}/Form.List
/Form// 给Form.List传值
welfareTypeInstance?.setFieldsValue({welfareType: welfareIdList,
})