在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 命令发现报错: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)
这个报错是因为依赖树出现了问题,可能是由于部分依赖的版本冲突导致的。
你可以尝试以下几种方法来解决这个问题:
如果以上方法不能解决问题,建议检查一下项目中 package.json 中的依赖是否正确,并且检查网络连接状态是否正常。
在CSS中,模块化可以通过多种方式实现。以下是几种常见的方法:
以上是几种常见的CSS模块化方式,每种方式都有其优缺点和适用场景。选择合适的方式可以让您的代码更具扩展性、可维护性和重用性,提高开发效率并减少错误。
规范:大写I开头,每个单词首字母都大写,如果类型是数组,后面加上Item,如:
export interface IOperateInfoItem {
action: string
name: string
createTime: string
type: string
docnumber: number
}
产生原因:权限问题
解决办法: 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密码,添加到仓库中:
要在本地生成Git密钥,请按照以下步骤操作:
ssh-keygen -t rsa -b 4096 -C "[email protected]"
。将公钥内容粘贴到里面即可:
此时便能成功git clone项目。
使用a标签时,一般除了设置href属性,还要设置 target=“_blank”,rel="noopener noreferrer"这两个属性。
target=“_blank” 用于在新窗口或者新标签页中打开链接,而不是在当前页面打开链接。
rel=“noopener noreferrer” 是一个安全属性,主要用于保护用户隐私安全。其中 noreferrer 指示浏览器在导航到目标资源时不要发送 Referer header(即告知目标站点来自哪个网站的信息),从而保护了用户浏览器的信息不被泄露。而 noopener 指示浏览器在接下来的新页面中取消对原页面的引用,防止被恶意页面通过 window.opener 访问到原页面中的权限,从而防止跨窗口脚本攻击。
这两个属性的组合使用可以有效预防一些潜在安全问题,建议在开发过程中养成使用的习惯。
安装依赖:
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(
,
);
在App.tsx中:
import HomePage from '@/pages/homePage';
import styles from './app.module.less';
function App() {
return (
);
}
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组件时,需要改变默认样式,如果我们想改变某个组件的样式,则首先需要找到某个组件标签的类名,一般在控制台通过鼠标选择查找到,对于一些需要触发才能显示的元素,有两种情况: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;
}
}
}
}
我们还原设计稿时,对于分出的每个组件的最外层的container,我们不要去给它设置固定高度和宽度,设置max-width即可 width: 100%; max-width: 1920px; ,其余由子元素撑开即可:
设置container里面的子元素时,记得不要用设计稿的绝对定位,因为那个是基于整个网页决定定位的,可能造成页面布局崩溃,在container里面以container为父盒子平铺即可。
如果没有设置宽度,元素的默认宽度是100%。这意味着元素会填充其父元素的整个宽度。( 一些元素(如 )具有自己的默认宽度 ), 像下面这样:
当元素设置偏移后(left值或right值不为0),则会导致盒子溢出父盒子,致使整个页面出现滚动条:
此时可以用calc()计算确定盒子的宽度,防止上面情况的发生:
如果不是元素的默认宽度导致莫名其妙出现的滚动条,那么排查方法一般是先在根组件中依次删掉,看问题出现在哪个组件中,确定好之后再在组件里面删元素,看问题出现在哪个元素中。(一般是固定宽度过宽的元素导致的)
对于一些里面有文字的div,如果给这些div设置固定宽高,在页面缩小时,由于浏览器字体的12px限制,可能会使文字溢出div盒子,此时可以采取两种方案解决:
是一种描述网页视口的 meta 元素。
在移动设备上,网页通常需要适应不同的屏幕大小和分辨率。那么,在这种情况下,网页应该如何表现呢?viewport 元素就是来解决这个问题的。
具体而言,width=device-width 表示网页的宽度应该等于设备的宽度,而 initial-scale=1.0 表示网页的初始缩放比例为 100%。这个设置对于确保在移动设备上展示的网页可以正确响应用户的手势操作非常重要。
除了上面提到的两个属性之外,viewport 元素还有其他一些常用的属性,例如:
综上所述,viewport 元素是一种非常重要的网页元信息,可以帮助网页在移动设备上正确展示,并提供更加友好的用户体验。
如果去掉
,在移动设备上打开网页的时候,网页会自动进行缩放,导致网页中的元素变得很小。在没有移动端的设计稿时,不失为一种防止在移动端上布局样式崩溃的方法。
如果没有设置宽度,元素的默认宽度是100%。这意味着元素会填充其父元素的整个宽度。一些元素(如)具有自己的默认宽度), 像下面这样:
当元素设置偏移后(left值或right值不为0),则会导致盒子溢出父盒子,致使整个页面出现滚动条:
此时可以用calc()计算确定盒子的宽度,防止上面情况的发生:
<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盒子外面记得再包一层盒子,然后用定位慢慢调位置。
多语言切换在很多场景中会用到,尤其类似官网的这种场景:
步骤如下:
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);
}
}
};
import React, { createContext, useContext, ComponentType, ComponentProps } from 'react';
/** 创建context组合useState状态Store */
function createStore(store: () => T) {
// eslint-disable-next-line
const ModelContext: any = {};
/** 使用model */
function useModel(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-line
let keys: any[] = Object.keys(currentStore);
let i = 0;
const length = keys.length;
/** 遍历状态,递归形成多层级嵌套Context */
function getContext(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(key: K): T[K] {
return currentStore[key];
}
/** 连接Model注入到组件中 */
function connectModel(key: K, selector: (state: T[K]) => Selected) {
// eslint-disable-next-line func-names
return function (
WarpComponent: C,
): ComponentType, keyof Selected>> {
const Connect = (props: P) => {
const val = useModel(key);
const state = selector(val);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return React.createElement(WarpComponent, {
...props,
...state,
});
};
return Connect as unknown as ComponentType, keyof Selected>>;
};
}
return {
useModel,
connectModel,
StoreProvider,
getModel,
};
}
export default createStore;
/** 浅对比对象 */
function Shallow(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 组件将状态注入整个应用中。其中状态的变化通过判断前后两次状态是否相同来避免无意义的重新渲染,使用了浅比较的方法来判断状态是否相同。
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('locale', undefined as unknown as ILocale);
export default () => {
const [locale, _setLocale] = useState(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;
export interface IRequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: { [key: string]: string };
body?: BodyInit;
}
// 添加泛型
export async function request(url: string, options: IRequestOptions = {}): Promise {
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()方法,将返回值赋值给options
requestInterceptors.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()方法,将返回值赋值给response
responseInterceptors.forEach((interceptor) => {
response = interceptor.onFulfilled?.(response) ?? response;
});
return response.json() as Promise<T>;
}
这段代码是一个封装了拦截器的 fetch 请求函数,通过调用 request 函数可以发送请求,并对请求和响应进行拦截和处理。
具体来说,定义了一个 IRequestOptions 接口来表示请求参数,指定了请求方法和请求头等参数;定义了一个 Interceptor 类型来表示拦截器,其中包括 onFulfilled 和 onRejected 两个方法,分别表示请求成功和请求失败后的处理函数;定义了一个 InterceptorManager 类来管理拦截器数组,其中包括 use 添加拦截器和 forEach 遍历拦截器的方法。
在 request 函数中,先创建了请求拦截器和响应拦截器,使用 use 方法添加拦截器,并在请求拦截器中处理请求,在响应拦截器中处理响应。最后返回处理后的响应数据。
对于生产环境的接口地址,我们进行请求时一般要配置代理以解决跨域问题:
本地进行请求时:
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;
}
原理:
利用了 Vite 内部集成的开发服务器和 Connect 中间件框架,通过在开发服务器上设置代理服务器,将请求转发到另一个服务器上。代理服务器不是浏览器,不受同源策略的限制,因此可以向任意域名下的接口发起请求。具体来说,开发服务器通过监听端口接收来自浏览器的请求,当收到符合代理规则的请求时,会将请求转发到目标服务器上,并将响应返回给浏览器。代理服务器在转发请求的同时,可以修改请求头、请求体、目标 URL 等信息,从而帮助开发者解决跨域、请求重定向、统一接口前缀等问题。
在本例中,使用了 http-proxy-middleware 库,该库封装了 Connect 中间件的代理功能,并在处理请求前进行了路径重写,将请求路径中的前缀 /uis 替换为 /api,以便将请求发送到目标服务器的正确接口上。
vite中配置代理解决跨域,一般是用于本地访问的。如若需要上线后跨域访问,则可以使用nginx作反向代理,从而实现跨域请求。配置如下:
server {
server_name book-waves
gzip 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;
}
}
在上面的叙述中我们知道,在本地是通过启用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;
}
浏览器的同源策略限制了前端页面向不同域名的接口发起请求,这导致某些情况下需要使用代理服务器来转发请求。一般来说,这种情况包括以下几种:
代理可能带来安全性问题(谁都可以请求接口)。因此在某些情况下,服务端需要对接口进行访问控制,需要用户先在页面进行登录认证(例如使用用户名和密码登录、验证码二次验证)。这时,前端页面需要先向自己的域名下的登录接口发起请求进行认证,获得认证信息后,再使用代理服务器将包含认证信息的请求转发到相应的接口上。(使用 token 进行认证):
对于这类接口,通常会在用户成功登录后,后端会生成一个 token 并返回给前端,前端保存这个 token 在客户端,并在后续的请求中携带这个 token,以便服务器能够对请求进行认证。服务器收到带有 token 的请求后,会验证 token 是否合法,以此决定是否允许请求访问相应的资源。****
这种方式的优点是服务器不需要为每个访问请求进行单独的 cookie-session 保存,整个流程的 stateless 特点也使得服务器可以更轻松地进行水平扩展以支持高并发。
Token 通常在请求头的 Authorization 字段中携带,其格式为 Bearer
,其中
是后端认证生成的令牌。这种方式被称为 Bearer Token 认证协议,其实现方式如下所示:
Authorization: Bearer
其中 Bearer 是认证协议类型,类似于 Basic 和 Digest,可以指定其他类型的认证方式。
是后端生成的认证令牌,通常为随机字符串,可以是 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
}
})
系统的环境变量是指操作系统中设置的全局变量,它们是指定操作系统和其他应用程序在运行时所需的一些参数和路径的变量。
常见的环境变量包括:
用户也可以自己创建自定义的环境变量来存储一些自己需要的参数和配置信息。在Windows操作系统中,可以通过“系统变量”和“用户变量”来设置环境变量。在Linux或Unix系统中,可以使用“export”命令来设置环境变量。
使用环境变量能够提高应用程序的可移植性和灵活性,因为不同的操作系统和应用程序都可以通过环境变量来适应不同的配置和需求。
后端写的接口,在开发环境、生产环境的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;
}
先说结论:浏览器本身并不直接支持访问系统环境变量,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对象,但是这并不是直接访问操作系统的环境变量,而是打印出了当前应用程序中注入的环境变量。在生产环境下,由于安全的原因,通常不建议在控制台中暴露环境变量信息。
在vite中,自带了【环境变量和模式】的配置,帮助我们手动设置一些环境变量,但是这些配置却显得不是很好用,因此我们可以借助cross-env这个包来优雅灵活地手动设置环境变量。
安装依赖:
pnpm install cross-env
此时便可以在package.json中设置我们的环境变量:
此时控制台打印环境变量的值,便可以看到环境变量被注入了:
Github地址: https://github.com/vbenjs/vite-plugin-html
在有些时候,我们的网页要做出一些seo的配置,如title、description、keywords等,如果我们想后台自定义这些内容,则需要借助vite-plugin-html插件,调用相关接口获取内容向html文件注入。步骤如下:
import fetch from 'node-fetch'
(global as any).fetch = fetch
// 接口返回数据的类型
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;
}
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,
},
},
}),
],
<title><%- title %>title>
<meta name="description" content="<%= description %>" />
<meta name="keywords" content="<%= keywords %>" />
如果现在想实现一个回显需求,设置被Form包裹的Input标签和TextArea标签的初始值,如果通过下面这样,通过ref获取标签实例再去设置是不可行的:
const emailTitleRef = useRef(null)
const emailMsgRef = useRef(null)
setEmailTitle(e.target.value)}
ref={emailTitleRef}
/>
<>
}
type='primary'
disabled={!!emailContent}
onClick={handleUploadImage}
>
上传图片
{emailImageList[0] && (
{emailImageList[0]}
)}
>
// 不起作用
// emailTitleRef.current.input.defaultValue = cnTitle || enTitle
// emailMsgRef.current.input.defaultValue = cnMsg || enMsg
这是因为Form包裹后,里面的组件变成了受控组件,只能通过Form提供的方法Form.useForm去获取整个表单的实例,再通过这个实例去设置子项的值:
const emailFillingInstance = Form.useForm(null)[0]
setEmailTitle(e.target.value)}
/>
<>
}
type='primary'
disabled={!!emailContent}
onClick={handleUploadImage}
>
上传图片
{emailImageList[0] && (
{emailImageList[0]}
)}
>
// 设置值--起作用了
emailFillingInstance?.setFieldsValue({
emailTitle: cnTitle || enTitle,
emailContent: cnMsg || enMsg,
})
也可以给实例传递泛型:
const [emailFillingInstance] = Form.useForm<{ emailTitle: string; emailContent: string }>()
setEmailTitle(e.target.value)}
/>
<>
}
type='primary'
disabled={!!emailContent}
onClick={handleUploadImage}
>
上传图片
{emailImageList[0] && (
{emailImageList[0]}
)}
>
// 设置值--起作用了
emailFillingInstance?.setFieldsValue({
emailTitle: cnTitle || enTitle,
emailContent: cnMsg || enMsg,
})
针对可动态增减表单项这种情况,可通过getFieldValue方法获取传入的值:
const [welfareTypeInstance] = Form.useForm<{ welfareType: string[] }>()
{(fields, { add, remove }) => {
// 获取传过来的值
const welfareType = welfareTypeInstance.getFieldValue('welfareType')
return (
<>
setIsWelfareId(e.target.checked)}
checked={isWelfareId}
>
福利ID
{fields.map((field, index) => (
<>
handleGetWelfareId(index, event.target.value)}
/>
{!isWelfare || componentType === 1 ? (
) : (
{
remove(field.name)
const welfareIdList = welfareIds
welfareIdList.splice(index, 1)
setWelfareIds(welfareIdList)
}}
/>
)}
>
))}
>
)
}}
// 给Form.List传值
welfareTypeInstance?.setFieldsValue({
welfareType: welfareIdList,
})