前端開發(fā)者必須知道的日常小技巧!
共 64285字,需瀏覽 129分鐘
·
2024-04-19 09:20
點擊上方 前端Q,關注公眾號
回復加群,加入前端Q技術交流群
作者:明遠湖之魚
https://juejin.cn/post/7301947438885191695
這篇文章收錄了本人在前端學習實踐中遇到的一些問題及解決,可供前端新人進行學習和參考,下面先展示一些可能有用的文檔/文章/網(wǎng)站:
文檔:
【Chrome 擴展開發(fā)文檔】:wizardforcel.gitbooks.io/chrome-doc/…[1]
【Docker — 從入門到實踐】:yeasy.gitbook.io/docker\_prac…[2]
文章:
1 、關于package.json里面,尖角號(^)和波浪線(~)的區(qū)別
在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時選擇一個合適的版本控制方式來使得項目的依賴關系更加穩(wěn)定。
2 、執(zhí)行npm install命令時發(fā)生版本沖突問題
// 執(zhí)行npm install 命令發(fā)現(xiàn)報錯: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: [email protected]
npm ERR! Found: [email protected]
npm ERR! node_modules/react
npm ERR! react@"^17.0.2" from the root project
npm ERR! peer react@">=16.9.0" from [email protected]
npm ERR! node_modules/antd
npm ERR! antd@"^4.21.2" from the root project
npm ERR! peer antd@"4.x" from @ant-design/[email protected]
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)
這個報錯是因為依賴樹出現(xiàn)了問題,可能是由于部分依賴的版本沖突導致的。
你可以嘗試以下幾種方法來解決這個問題:
-
清空 node_modules 和 package-lock.json 文件,重新執(zhí)行 npm install 命令。 -
使用 npm install --legacy-peer-deps 命令替代 npm install 命令,這條命令會忽略 peerDependencies 的版本限制。 -
更新 package.json 中的依賴版本號,使其符合 SemVer 規(guī)范。 -
更換包管理器為 yarn 或 pnpm,并嘗試再次執(zhí)行安裝命令。
如果以上方法不能解決問題,建議檢查一下項目中 package.json 中的依賴是否正確,并且檢查網(wǎng)絡連接狀態(tài)是否正常。
3、快速定位到某個組件對應的位置步驟
1、項目跑起來后,在地址欄里找到組件關鍵字:
2、復制項目路由文件夾的相對路徑:
3、結(jié)合組件關鍵字與路由相對路徑,便可快速定位到改組件的路由:
5、在路由文件里便可清晰地看到組件定義的位置了:
4、關于CSS的模塊化
在CSS中,模塊化可以通過多種方式實現(xiàn)。以下是幾種常見的方法:
-
命名約定:通過在樣式規(guī)則中使用特定的命名前綴或后綴來標識該樣式規(guī)則屬于哪個模塊。例如,如果您的網(wǎng)站包含一個名為“頭部”的模塊,您可以使用“-header”后綴來標識所有與該模塊相關的樣式規(guī)則。
-
BEM(塊、元素、修飾符)方法:這是一種廣泛使用的CSS命名約定,它基于組件化設計的思想。使用BEM,每個模塊都被視為一個獨立的塊(block),其中包含了多個元素(element),并且可以有零個或多個修飾符(modifier)。例如,一個名為“頭部”的模塊可以定義一個塊元素“頭部__logo”,以及一個帶有修飾符的塊元素“頭部--transparent”。缺點:這種方式和方面的那種命名約定方式是比較傳統(tǒng)的解決方案,但是隨著應用規(guī)模的增大,命名沖突和代碼重復的問題也越來越明顯,增加了開發(fā)的復雜性和維護難度。
-
CSS模塊(CSS Modules):它是一種官方的CSS模塊化解決方案,它利用Webpack、Vite等打包工具,將CSS樣式表歸檔為模塊,并自動管理CSS類名的作用域和命名。這使得CSS代碼更易于維護和擴展,并且避免了全局污染和命名沖突的問題。在Vue框架的CSS作用域中,采取的是CSS模塊(CSS Modules)中的局部作用域(Local Scope)方式。index.module.less 也是一種基于 CSS Modules 的 CSS 模塊化方式,它可以在 React 項目中使用,可能產(chǎn)生不靈活的問題,比如如果想聲明某個選擇器在全局范圍內(nèi)生效,只能使用偽類:global。缺點:使用 CSS Modules 需要借助打包工具,并且需要保證每個組件的類名唯一,否則會影響樣式的正確性。此外,CSS Modules 學習成本相對于其他方式較高,需要理解一些額外的語法和配置。
-
CSS-in-JS:這是一種將CSS樣式作為JavaScript對象嵌入到組件中的方法。使用CSS-in-JS,您可以將不同模塊的樣式定義在同一個文件或同一個組件內(nèi),并以動態(tài)方式根據(jù)組件狀態(tài)或其他條件應用它們。常見的CSS-in-JS庫包括Styled Components、Emotion等。缺點:雖然 CSS-in-JS 可以實現(xiàn)組件化的樣式定義,并且能夠更好地利用 JavaScript 的編程能力,但是需要在項目中引入額外的庫和插件,增加了代碼的復雜性和學習成本。
以上是幾種常見的CSS模塊化方式,每種方式都有其優(yōu)缺點和適用場景。選擇合適的方式可以讓您的代碼更具擴展性、可維護性和重用性,提高開發(fā)效率并減少錯誤。
5、關于定義類型時的命名規(guī)范
規(guī)范:大寫I開頭,每個單詞首字母都大寫,如果類型是數(shù)組,后面加上Item,如:
export interface IOperateInfoItem {
action: string
name: string
createTime: string
type: string
docnumber: number
}
6、git clone到本地的項目執(zhí)行npm install命令報錯
產(chǎn)生原因:權(quán)限問題
解決辦法:blog.csdn.net/qq\_34488939…[7]
(主要就是給node_global文件夾加權(quán)限)
之后若仍然安裝失敗報錯,仔細看會發(fā)現(xiàn)并不是上述那個報錯,而是安裝某些包時報錯,因為存在預依賴,所以執(zhí)行npm i -f 強制安裝即可:
7、關于寫注釋的技巧
以雙斜杠這種方式寫注釋時:
導致如果其他地方用到這個變量,鼠標放上去不會有注釋提示:
但如果以/** */這種方式注釋時:
則如果其他地方用到這個變量,鼠標放上去會有注釋提示:
8、泛型在接口類型定義時的應用
對于一些請求,接口返回的數(shù)據(jù)總有相同的字段,比如下面這種請求分頁返回的data總會有current、page、records、searchCount、size、total等幾個字段,但是records里面的字段可能就要具體情況具體定義。因此對于這種情況,可以采用泛型,將data定義為PagesuccessResponse,里面的records為泛型數(shù)組,然后便可以具體情況具體定義了:
9、關于企業(yè)項目的自動化部署流程
使用GitLab的Webhook功能來監(jiān)聽代碼庫中的變化,并自動觸發(fā)部署流程。具體實現(xiàn)步驟如下:
1、在GitLab項目設置中的Webhooks選項中添加一個新的Webhook。將Webhook的URL地址指定為部署服務器上的一個接收請求的腳本。
2、編寫部署服務器上的腳本,在接收到GitLab Webhook的請求時,解析請求中的數(shù)據(jù),并根據(jù)解析結(jié)果觸發(fā)相應的自動化部署流程。部署流程可以包括測試、構(gòu)建、部署等多個步驟,可以使用Jenkins或Ansible等自動化部署工具來實現(xiàn)。
3、完成上述步驟后,每當GitLab代碼庫中發(fā)生變化時,部署服務器就會自動接收到Webhook請求并觸發(fā)自動化部署流程。這樣就可以實現(xiàn)自動化部署的目的,提高開發(fā)效率和部署質(zhì)量。
10、git clone倉庫項目時遇到權(quán)限問題及解決
如下圖,第一次git clone某倉庫時遇到權(quán)限問題:
解決方案:在本地生成git密碼,添加到倉庫中:
要在本地生成Git密鑰,請按照以下步驟操作:
-
打開命令行或終端窗口。 -
輸入以下命令: ssh-keygen -t rsa -b 4096 -C "[email protected]"。 -
按Enter鍵,將使用默認文件名和位置生成密鑰。如果您希望使用不同的文件名或位置,請根據(jù)需要進行更改。 -
然后系統(tǒng)會提示您輸入一個密碼以保護您的密鑰。如果您不想添加密碼,可以直接按Enter鍵。 -
最后,將在指定位置生成兩個文件:公鑰(id_rsa.pub)和私鑰(id_rsa)。
將公鑰內(nèi)容粘貼到里面即可:
此時便能成功git clone項目。
11、關于使用a標簽時要注意的點
使用a標簽時,一般除了設置href屬性,還要設置 target="_blank",rel="noopener noreferrer"這兩個屬性。
target="_blank" 用于在新窗口或者新標簽頁中打開鏈接,而不是在當前頁面打開鏈接。
rel="noopener noreferrer" 是一個安全屬性,主要用于保護用戶隱私安全。其中 noreferrer 指示瀏覽器在導航到目標資源時不要發(fā)送 Referer header(即告知目標站點來自哪個網(wǎng)站的信息),從而保護了用戶瀏覽器的信息不被泄露。而 noopener 指示瀏覽器在接下來的新頁面中取消對原頁面的引用,防止被惡意頁面通過 window.opener 訪問到原頁面中的權(quán)限,從而防止跨窗口腳本攻擊。
這兩個屬性的組合使用可以有效預防一些潛在安全問題,建議在開發(fā)過程中養(yǎng)成使用的習慣。
12、關于px與rem之間的自動轉(zhuǎn)化(使用postcss-pxtorem)
安裝依賴:
pnpm install postcss-pxtorem
新建 postcss.config.js文件:
export default {
plugins: {
'postcss-pxtorem': {
// 基準屏幕寬度
rootValue: 192,
// rem的小數(shù)點后位數(shù)
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;
},
},
},
};
在根節(jié)點文件中引入:
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';
};
// 初始化時,即頁面掛載前就要執(zhí)行一次,防止頁面第一次加載時產(chǎn)生抖動
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)轉(zhuǎn)換為 rem 單位,從而實現(xiàn)響應式布局。該插件的原理是通過遍歷 CSS 樣式文件中的每個規(guī)則,在其中檢測并轉(zhuǎn)換出現(xiàn)的像素單位,并根據(jù)約定的轉(zhuǎn)換比例將其轉(zhuǎn)換為 rem 單位。通常情況下,該插件會將視口寬度作為參考,以便在不同設備上獲得一致的 UI 顯示效果。
例如,在默認設置下,當 CSS 中出現(xiàn) font-size: 16px; 的樣式規(guī)則時,該插件將自動將其轉(zhuǎn)換為 font-size: 1rem;,根據(jù)默認的轉(zhuǎn)換比例(1px = 1/16rem)計算而得。這樣可以確保在不同屏幕尺寸和分辨率下,UI 元素的大小和間距能夠自適應地調(diào)整,提高網(wǎng)站或應用的可訪問性和用戶體驗。
13、關于調(diào)試修改antd design組件樣式的技巧
我們使用到antd design組件時,需要改變默認樣式,如果我們想改變某個組件的樣式,則首先需要找到某個組件標簽的類名,一般在控制臺通過鼠標選擇查找到,對于一些需要觸發(fā)才能顯示的元素,有兩種情況:hover觸發(fā)或者組件本身有類似open:true/false(類似Dropdown組件,展開或收起通過open這個屬性觸發(fā))
此時若想全局改變,則需要在樣式文件里面屬下類似下面的代碼即可:
: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,再去改變樣式:
此時可以看到標簽已經(jīng)被掛載了類名:(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、關于根據(jù)設計稿制作網(wǎng)頁時的屏幕適配、縮放操作適配問題
14-1 不要使用設計稿的決定定位
我們還原設計稿時,對于分出的每個組件的最外層的container,我們不要去給它設置固定高度和寬度,設置max-width即可 width: 100%; max-width: 1920px; ,其余由子元素撐開即可:
設置container里面的子元素時,記得不要用設計稿的絕對定位,因為那個是基于整個網(wǎng)頁決定定位的,可能造成頁面布局崩潰,在container里面以container為父盒子平鋪即可。
14-2 關于莫名其妙的滾動條(涉及元素的默認寬度)
如果沒有設置寬度,元素的默認寬度是100%。這意味著元素會填充其父元素的整個寬度。( 一些元素(如<button>)具有自己的默認寬度 ), 像下面這樣:
當元素設置偏移后(left值或right值不為0),則會導致盒子溢出父盒子,致使整個頁面出現(xiàn)滾動條:
此時可以用calc()計算確定盒子的寬度,防止上面情況的發(fā)生:
如果不是元素的默認寬度導致莫名其妙出現(xiàn)的滾動條,那么排查方法一般是先在根組件中依次刪掉,看問題出現(xiàn)在哪個組件中,確定好之后再在組件里面刪元素,看問題出現(xiàn)在哪個元素中。(一般是固定寬度過寬的元素導致的)
14-3 關于瀏覽器的12px限制
對于一些里面有文字的div,如果給這些div設置固定寬高,在頁面縮小時,由于瀏覽器字體的12px限制,可能會使文字溢出div盒子,此時可以采取兩種方案解決:
1、不給div設置寬高,設置padding,使里面的文字撐開div,防止溢出:
2、利用媒體查詢,強制縮放文字:(采用這種方法時,記得給文字多套一層盒子,因為縮放是整個元素一起縮放的):
14-4 關于縮小屏幕時的處理(涉及到meta的viewport)
是一種描述網(wǎng)頁視口的 meta 元素。
在移動設備上,網(wǎng)頁通常需要適應不同的屏幕大小和分辨率。那么,在這種情況下,網(wǎng)頁應該如何表現(xiàn)呢?viewport 元素就是來解決這個問題的。
具體而言,width=device-width 表示網(wǎng)頁的寬度應該等于設備的寬度,而 initial-scale=1.0 表示網(wǎng)頁的初始縮放比例為 100%。這個設置對于確保在移動設備上展示的網(wǎng)頁可以正確響應用戶的手勢操作非常重要。
除了上面提到的兩個屬性之外,viewport 元素還有其他一些常用的屬性,例如:
-
height:設置 viewport 的高度; -
user-scalable:設置是否允許用戶縮放網(wǎng)頁; -
minimum-scale 和 maximum-scale:設置用戶可以縮放的最小和最大值。
綜上所述,viewport 元素是一種非常重要的網(wǎng)頁元信息,可以幫助網(wǎng)頁在移動設備上正確展示,并提供更加友好的用戶體驗。
如果去掉<meta name="viewport" content="width=device-width, initial-scale=1.0">,在移動設備上打開網(wǎng)頁的時候,網(wǎng)頁會自動進行縮放,導致網(wǎng)頁中的元素變得很小。在沒有移動端的設計稿時,不失為一種防止在移動端上布局樣式崩潰的方法。
如果沒有設置寬度,元素的默認寬度是100%。這意味著元素會填充其父元素的整個寬度。一些元素(如<button>)具有自己的默認寬度), 像下面這樣:
當元素設置偏移后(left值或right值不為0),則會導致盒子溢出父盒子,致使整個頁面出現(xiàn)滾動條:
此時可以用calc()計算確定盒子的寬度,防止上面情況的發(fā)生:
15、一個使用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盒子外面記得再包一層盒子,然后用定位慢慢調(diào)位置。
16、關于項目中的多語言切換
多語言切換在很多場景中會用到,尤其類似官網(wǎng)的這種場景:
步驟如下:
1、封裝一個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);
}
}
};
2、利用 React Context 實現(xiàn)一個狀態(tài)管理庫,使得所有組件都能輕易地獲取到當前的狀態(tài)(即語言類型),檢測到狀態(tài)改變即可重新渲染:
import React, { createContext, useContext, ComponentType, ComponentProps } from 'react';
/** 創(chuàng)建context組合useState狀態(tài)Store */
function createStore<T extends object>(store: () => T) {
// eslint-disable-next-line
const ModelContext: any = {};
/** 使用model */
function useModel<K extends keyof T>(key: K) {
return useContext(ModelContext[key]) as T[K];
}
/** 當前的狀態(tài) */
let currentStore: T;
/** 上一次的狀態(tài) */
let prevStore: T;
/** 創(chuàng)建狀態(tài)注入組件 */
function StoreProvider(props: { children: React.ReactNode }) {
currentStore = store();
/** 如果有上次的context狀態(tài),做一下淺對比,
* 如果狀態(tài)沒變,就復用上一次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;
/** 遍歷狀態(tài),遞歸形成多層級嵌套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);
}
/** 獲取當前狀態(tài), 方便在組件外部使用,也不會引起頁面更新 */
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-names
return 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-ignore
return 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 實現(xiàn)的狀態(tài)管理庫,提供了 createStore 方法來創(chuàng)建一個狀態(tài) Store,通過 useModel 方法獲取對應狀態(tài)的值,在組件中使用 connectModel 方法連接對應的 Model 和組件,并且通過 StoreProvider 組件將狀態(tài)注入整個應用中。其中狀態(tài)的變化通過判斷前后兩次狀態(tài)是否相同來避免無意義的重新渲染,使用了淺比較的方法來判斷狀態(tài)是否相同。
3、根據(jù)瀏覽器API設置默認語言,創(chuàng)建sessionStorage,如果切換語言則改變sessionStorage存儲的值,同時負責將語言文件引入以便和狀態(tài)管理器一起發(fā)揮作用:
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';
// 根據(jù)瀏覽器api獲取當前語言
const getBrowserLanguage = () => {
// 獲取瀏覽器語言字符串
const languageString = navigator.language || navigator.languages[0];
// 將語言字符串拆分成語言和地區(qū)
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 的狀態(tài)變量,用于存儲用戶當前所選擇的語言類型。默認值為 localeStorage.getItem() 或者 'default'。然后使用 useMemo 函數(shù),根據(jù)當前的語言類型從語言包 localesMap 中獲取對應的翻譯文本。如果當前語言類型為 falsy 值,則使用默認語言 'default' 的翻譯文本。最后使用 useMemoizedFn 函數(shù),定義一個 setLocale 方法,用于修改當前語言類型。如果傳入的是一個函數(shù),則先根據(jù)當前語言類型執(zhí)行該函數(shù),得到要修改的新語言類型,然后將該語言類型存儲到本地存儲中,并修改當前的語言類型變量。最后將 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;
4、根據(jù)第二步、第三步中創(chuàng)建一個用于管理語言狀態(tài)的全局狀態(tài)管理庫,并導出相關方法供外部使用:
import createStore from './createStore';
import locales from './modules/locales';
const store = () => ({
locales: locales(),
});
const contextResult = createStore(store);
export const { useModel, StoreProvider, getModel, connectModel } = contextResult;
5、在組件中實現(xiàn)切換語言、使用相應狀態(tài)的語言包:
17、關于基于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() 方法是一個異步方法,所以你需要在回調(diào)函數(shù)中處理異常。你可以在then的第二個參數(shù)中傳入回調(diào)函數(shù),處理接口報錯的情況。例如:
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()方法向攔截器數(shù)組中添加一個攔截器,可以通過forEach()方法對所有的攔截器進行遍歷和執(zhí)行。
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 函數(shù)
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;
},
});
// 處理請求攔截器--遍歷所有的請求攔截器,并執(zhí)行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}`);
}
// 處理響應攔截器--遍歷所有的響應攔截器,并執(zhí)行onFulfilled()方法,將返回值賦值給response
responseInterceptors.forEach((interceptor) => {
response = interceptor.onFulfilled?.(response) ?? response;
});
return response.json() as Promise<T>;
}
這段代碼是一個封裝了攔截器的 fetch 請求函數(shù),通過調(diào)用 request 函數(shù)可以發(fā)送請求,并對請求和響應進行攔截和處理。
具體來說,定義了一個 IRequestOptions 接口來表示請求參數(shù),指定了請求方法和請求頭等參數(shù);定義了一個 Interceptor 類型來表示攔截器,其中包括 onFulfilled 和 onRejected 兩個方法,分別表示請求成功和請求失敗后的處理函數(shù);定義了一個 InterceptorManager 類來管理攔截器數(shù)組,其中包括 use 添加攔截器和 forEach 遍歷攔截器的方法。
在 request 函數(shù)中,先創(chuàng)建了請求攔截器和響應攔截器,使用 use 方法添加攔截器,并在請求攔截器中處理請求,在響應攔截器中處理響應。最后返回處理后的響應數(shù)據(jù)。
18、關于代理服務
18-1 vite中配置代理解決跨域訪問的方法(用于本地跨域訪問)
對于生產(chǎn)環(huán)境的接口地址,我們進行請求時一般要配置代理以解決跨域問題:
本地進行請求時:
server: {
open: true,
proxy: {
'/uis': {
target: 'http://subs-global.xiongmaoboshi.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, ''),
// 由于網(wǎng)站部署在后端的OSS(云服務器)上,不經(jīng)過前端的node服務,前端無法通過nginx配置代理實現(xiàn)跨域訪問
// 所以對于線上的生產(chǎn)環(huán)境,需要后端開啟訪問白名單,允許前端的域名訪問
// 但是本地開發(fā)環(huán)境,由于沒有后端,所以需要通過vite的代理配置來實現(xiàn)跨域訪問
// 但是這里有個問題,就是代理配置的headers中的Origin,必須和請求的Origin一致,否則會報錯(403Forbidden)
// 雖然我們在這里設置了代理的headers,但是打開控制臺會看到請求的headers中,Origin并沒有被設置仍然是本地http://127.0.0.1:5173
// 但實質(zhì)上,vite代理服務器幫我們轉(zhuǎn)發(fā)請求的時候,Origin已經(jīng)被設置為了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 內(nèi)部集成的開發(fā)服務器和 Connect 中間件框架,通過在開發(fā)服務器上設置代理服務器,將請求轉(zhuǎn)發(fā)到另一個服務器上。代理服務器不是瀏覽器,不受同源策略的限制,因此可以向任意域名下的接口發(fā)起請求。具體來說,開發(fā)服務器通過監(jiān)聽端口接收來自瀏覽器的請求,當收到符合代理規(guī)則的請求時,會將請求轉(zhuǎn)發(fā)到目標服務器上,并將響應返回給瀏覽器。代理服務器在轉(zhuǎn)發(fā)請求的同時,可以修改請求頭、請求體、目標 URL 等信息,從而幫助開發(fā)者解決跨域、請求重定向、統(tǒng)一接口前綴等問題。
在本例中,使用了 http-proxy-middleware 庫,該庫封裝了 Connect 中間件的代理功能,并在處理請求前進行了路徑重寫,將請求路徑中的前綴 /uis 替換為 /api,以便將請求發(fā)送到目標服務器的正確接口上。
18-3 nginx代理解決跨域(用于部署在自己的服務器上的情況,否則需要后端開啟訪問白名單)
vite中配置代理解決跨域,一般是用于本地訪問的。如若需要上線后跨域訪問,則可以使用nginx作反向代理,從而實現(xiàn)跨域請求。配置如下:
server {
server_name book-waves.com;
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;
}
}
18-4 設置環(huán)境變量判斷是本地開發(fā)環(huán)境還是線上生產(chǎn)環(huán)境
在上面的敘述中我們知道,在本地是通過啟用vite的代理服務器來實現(xiàn)跨域訪問的,在線上是通過后端設置訪問白名單來實現(xiàn)跨域訪問的。我們必須設置一個環(huán)境變量判斷是本地開發(fā)環(huán)境還是線上生產(chǎn)環(huán)境,因為它們的請求接口不同:
import { paramsType, resType } from './type';
import { request } from '@/utils/request';
export async function feedbackSubmit(params: paramsType): Promise<resType> {
// 本地時,由于有vite的代理服務,我們只需要在請求時,把這里的請求路徑改為'/uis/ns/sendEmail'即可,因為會被代理服務轉(zhuǎn)發(fā)到線上的地址
// 但是線上時,由于沒有代理服務,所以我們需要在請求時,把這里的請求路徑改為'http://subs-global.xiongmaoboshi.com/uis/ns/sendEmail',因為沒有代理服務,所以不會被轉(zhuǎn)發(fā)到線上的地址
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 需要配置代理的情況
瀏覽器的同源策略限制了前端頁面向不同域名的接口發(fā)起請求,這導致某些情況下需要使用代理服務器來轉(zhuǎn)發(fā)請求。一般來說,這種情況包括以下幾種:
-
使用第三方 API 或服務:例如,使用第三方地圖 API 服務,需要向 API 服務提供商的域名下的接口發(fā)起請求,而這與前端頁面所在的域名不同。 -
開發(fā)環(huán)境與生產(chǎn)環(huán)境不同:在開發(fā)環(huán)境中,前端頁面通常運行在本地的開發(fā)服務器上,而后端服務則運行在遠程服務器上。在這種情況下,由于開發(fā)服務器與后端服務器的域名不同,因此需要使用代理服務器將請求轉(zhuǎn)發(fā)到正確的后端服務端點。 -
部分接口需要登錄認證:在某些情況下,服務端需要對接口進行訪問控制,需要用戶先在頁面進行登錄認證。這時,前端頁面需要先向自己的域名下的登錄接口發(fā)起請求進行認證,獲得認證信息后,再使用代理服務器將包含認證信息的請求轉(zhuǎn)發(fā)到相應的接口上。
18-6 代理帶來安全性問題及解決
代理可能帶來安全性問題(誰都可以請求接口)。因此在某些情況下,服務端需要對接口進行訪問控制,需要用戶先在頁面進行登錄認證(例如使用用戶名和密碼登錄、驗證碼二次驗證)。這時,前端頁面需要先向自己的域名下的登錄接口發(fā)起請求進行認證,獲得認證信息后,再使用代理服務器將包含認證信息的請求轉(zhuǎn)發(fā)到相應的接口上。(使用 token 進行認證):
對于這類接口,通常會在用戶成功登錄后,后端會生成一個 token 并返回給前端,前端保存這個 token 在客戶端,并在后續(xù)的請求中攜帶這個 token,以便服務器能夠?qū)φ埱筮M行認證。服務器收到帶有 token 的請求后,會驗證 token 是否合法,以此決定是否允許請求訪問相應的資源。****
這種方式的優(yōu)點是服務器不需要為每個訪問請求進行單獨的 cookie-session 保存,整個流程的 stateless 特點也使得服務器可以更輕松地進行水平擴展以支持高并發(fā)。
18-7 關于token的攜帶及設置
Token 通常在請求頭的 Authorization 字段中攜帶,其格式為 Bearer <token>,其中 <token> 是后端認證生成的令牌。這種方式被稱為 Bearer Token 認證協(xié)議,其實現(xiàn)方式如下所示:
Authorization: Bearer <token>
其中 Bearer 是認證協(xié)議類型,類似于 Basic 和 Digest,可以指定其他類型的認證方式。<token> 是后端生成的認證令牌,通常為隨機字符串,可以是 JSON Web Token (JWT) 、OAuth Token 等多種形式。
前端在發(fā)送請求時,需要將 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、關于環(huán)境變量
19-1 環(huán)境變量的概念
系統(tǒng)的環(huán)境變量是指操作系統(tǒng)中設置的全局變量,它們是指定操作系統(tǒng)和其他應用程序在運行時所需的一些參數(shù)和路徑的變量。
常見的環(huán)境變量包括:
-
PATH:指定可執(zhí)行文件所在的路徑,當用戶輸入一個命令時,系統(tǒng)會在PATH中指定的路徑中查找可執(zhí)行文件。 -
HOME:指定當前用戶的主目錄路徑。 -
TEMP / TMP:指定臨時文件的存放路徑。 -
LANG / LC_ALL:指定系統(tǒng)的語言環(huán)境。
用戶也可以自己創(chuàng)建自定義的環(huán)境變量來存儲一些自己需要的參數(shù)和配置信息。在Windows操作系統(tǒng)中,可以通過“系統(tǒng)變量”和“用戶變量”來設置環(huán)境變量。在Linux或Unix系統(tǒng)中,可以使用“export”命令來設置環(huán)境變量。
使用環(huán)境變量能夠提高應用程序的可移植性和靈活性,因為不同的操作系統(tǒng)和應用程序都可以通過環(huán)境變量來適應不同的配置和需求。
19-2 環(huán)境變量在前端代碼編寫中發(fā)揮的作用
后端寫的接口,在開發(fā)環(huán)境、生產(chǎn)環(huán)境的url可能是不同的,作為前端,我們調(diào)用接口時,要判斷當前是開發(fā)環(huán)境還是生產(chǎn)環(huán)境來選擇調(diào)用不同的接口。像下面這樣:
import { paramsType, resType } from './type';
import { request } from '@/utils/request';
export async function feedbackSubmit(params: paramsType): Promise<resType> {
// 本地時,由于有vite的代理服務,我們只需要在請求時,把這里的請求路徑改為'/uis/ns/sendEmail'即可,因為會被代理服務轉(zhuǎn)發(fā)到線上的地址
// 但是線上時,由于沒有代理服務,所以我們需要在請求時,把這里的請求路徑改為'http://subs-global.xiongmaoboshi.com/uis/ns/sendEmail',因為沒有代理服務,所以不會被轉(zhuǎn)發(fā)到線上的地址
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環(huán)境與瀏覽器環(huán)境訪問環(huán)境變量的區(qū)別
先說結(jié)論:瀏覽器本身并不直接支持訪問系統(tǒng)環(huán)境變量,Node.js可以訪問環(huán)境變量。
瀏覽器是運行在用戶操作系統(tǒng)之上的應用程序,它是通過操作系統(tǒng)提供的API和驅(qū)動程序來與系統(tǒng)硬件通信的。
系統(tǒng)環(huán)境變量是系統(tǒng)級別的配置信息,它們是指定操作系統(tǒng)和其他應用程序在運行時所需的一些參數(shù)和路徑的變量。由于環(huán)境變量可能涉及到系統(tǒng)級別的安全問題,因此瀏覽器不能直接訪問它們,以避免存在安全漏洞。
此外,不同的操作系統(tǒng)所使用的環(huán)境變量的名稱和取值也可能會存在差異。因此,瀏覽器并不能像Node.js一樣直接訪問操作系統(tǒng)的環(huán)境變量。
作為替代方案,瀏覽器提供了一些本地存儲機制(如localStorage和sessionStorage),以及一些瀏覽器擴展API(如Chrome的chrome.storage和Firefox的browser.storage),開發(fā)者可以使用這些API來存儲和讀取瀏覽器級別的配置信息和用戶設置,從而實現(xiàn)類似的功能。
Node.js是一個基于JavaScript的服務器端開發(fā)平臺,由于其運行在服務器端而非瀏覽器中,可以直接使用底層操作系統(tǒng)提供的API來訪問系統(tǒng)環(huán)境變量。
在Node.js中,環(huán)境變量使用process.env屬性進行管理。process對象是Node.js內(nèi)置對象的一個全局對象,它提供了對當前進程的相關信息以及控制進程操作的方法。process.env屬性是一個表示當前操作系統(tǒng)環(huán)境變量的鍵值對集合。
但是利用vite構(gòu)建的web應用程序,其控制臺輸入console.log(process.env),是能打印出東西的:
在Vite開發(fā)環(huán)境下,并不是直接運行在瀏覽器中的,而是先通過Node.js對代碼進行預處理,并將代碼轉(zhuǎn)換為瀏覽器可執(zhí)行的JavaScript文件,因此在Vite開發(fā)環(huán)境下,可以通過Node.js提供的process對象來訪問系統(tǒng)環(huán)境變量。
很多前端框架(如React和Vue.js)在開發(fā)環(huán)境下都會集成類似于Vite、Webpack等打包工具,這些打包工具可以在編譯代碼時將環(huán)境變量注入到應用程序中,從而在應用程序中使用環(huán)境變量。這些前端框架一般都提供了自己的方式來獲取環(huán)境變量,一般是通過在代碼中讀取process.env對象中的變量來實現(xiàn)。在開發(fā)環(huán)境下,也可以在控制臺中打印出process.env對象,但是這并不是直接訪問操作系統(tǒng)的環(huán)境變量,而是打印出了當前應用程序中注入的環(huán)境變量。在生產(chǎn)環(huán)境下,由于安全的原因,通常不建議在控制臺中暴露環(huán)境變量信息。
19-4 借助cross-env手動設置環(huán)境變量
在vite中,自帶了【環(huán)境變量和模式[8]】的配置,幫助我們手動設置一些環(huán)境變量,但是這些配置卻顯得不是很好用,因此我們可以借助cross-env這個包來優(yōu)雅靈活地手動設置環(huán)境變量。
安裝依賴:
pnpm install cross-env
此時便可以在package.json中設置我們的環(huán)境變量:
此時控制臺打印環(huán)境變量的值,便可以看到環(huán)境變量被注入了:
20、使用vite-plugin-html向html模板注入內(nèi)容
Github地址:github.com/vbenjs/vite…[9]
在有些時候,我們的網(wǎng)頁要做出一些seo的配置,如title、description、keywords等,如果我們想后臺自定義這些內(nèi)容,則需要借助vite-plugin-html插件,調(diào)用相關接口獲取內(nèi)容向html文件注入。步驟如下:
1、安裝依賴:
2、同時,因fetch 函數(shù)是在瀏覽器環(huán)境中全局定義的,所以在瀏覽器環(huán)境中可以直接使用。但是,在 vite.config.ts 中使用 fetch 函數(shù)時,可能還未加載到瀏覽器環(huán)境中,所以需要特別處理才能在 vite.config.ts 中使用。需要使用 node-fetch 這個第三方模塊將 fetch 函數(shù)兼容到 node.js 環(huán)境中,這樣就可以在 vite.config.ts 中直接使用 fetch 函數(shù):
3、在 vite.config.ts 文件中添加如下代碼將 fetch 函數(shù)兼容到 node.js 環(huán)境:
import fetch from 'node-fetch'
(global as any).fetch = fetch
4、在 vite.config.ts 中書寫接口調(diào)用函數(shù)來獲取內(nèi)容:
// 接口返回數(shù)據(jù)的類型
interface IHtmlHeadContent {
seo: {
title: string;
description: string;
keywords: string;
};
}
async function getHtmlHeadContent(): Promise<IHtmlHeadContent> {
let url = '';
// 判斷是否是生產(chǎn)環(huán)境
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;
}
5、向html文件中注入:
plugins: [
react(),
createHtmlPlugin({
minify: true,
/**
* 需要注入 index.html ejs 模版的數(shù)據(jù)
*/
inject: {
data: {
title: (await getHtmlHeadContent()).seo.title,
description: (await getHtmlHeadContent()).seo.description,
keywords: (await getHtmlHeadContent()).seo.keywords,
},
},
}),
],
6、在html文件中獲取到注入的內(nèi)容:
<title><%- title %></title>
<meta name="description" content="<%= description %>" />
<meta name="keywords" content="<%= keywords %>" />
21、關于antd design的Form獲取實例來設置表單回顯
如果現(xiàn)在想實現(xiàn)一個回顯需求,設置被Form包裹的Input標簽和TextArea標簽的初始值,如果通過下面這樣,通過ref獲取標簽實例再去設置是不可行的:
const emailTitleRef = useRef<InputRef>(null)
const emailMsgRef = useRef<HTMLDivElement>(null)
<Form>
<Form.Item label='郵件主題' name='emailTitle' rules={[{ required: true }]}>
<Input
placeholder='請輸入郵件主題 - 注意長度和語言'
onChange={e => setEmailTitle(e.target.value)}
ref={emailTitleRef}
/>
</Form.Item>
<Form.Item label='郵件正文' name='emailContent'>
<TextArea
placeholder='請輸入郵件正文 - 僅支持「文本」或「圖片」'
disabled={!!emailImageList[0]}
onChange={e => setEmailContent(e.target.value)}
ref={emailMsgRef}
/>
</Form.Item>
<Form.Item label=' ' name='loadImage'>
<>
<Button
icon={<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 }]}>
<Input
placeholder='請輸入郵件主題 - 注意長度和語言'
onChange={e => setEmailTitle(e.target.value)}
/>
</Form.Item>
<Form.Item label='郵件正文' name='emailContent'>
<TextArea
placeholder='請輸入郵件正文 - 僅支持「文本」或「圖片」'
disabled={!!emailImageList[0]}
onChange={e => setEmailContent(e.target.value)}
/>
</Form.Item>
<Form.Item label=' ' name='loadImage'>
<>
<Button
icon={<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 }]}>
<Input
placeholder='請輸入郵件主題 - 注意長度和語言'
onChange={e => setEmailTitle(e.target.value)}
/>
</Form.Item>
<Form.Item label='郵件正文' name='emailContent'>
<TextArea
placeholder='請輸入郵件正文 - 僅支持「文本」或「圖片」'
disabled={!!emailImageList[0]}
onChange={e => setEmailContent(e.target.value)}
/>
</Form.Item>
<Form.Item label=' ' name='loadImage'>
<>
<Button
icon={<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,
})
針對可動態(tài)增減表單項這種情況,可通過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'>
<Checkbox
onChange={e => setIsWelfareId(e.target.checked)}
checked={isWelfareId}
>
福利ID
</Checkbox>
</Form.Item>
{fields.map((field, index) => (
<Form.Item key={field.key} name={[field.name]}>
<>
<Input
value={welfareType[index]}
disabled={!isWelfareId || componentType === 1}
onChange={event => handleGetWelfareId(index, event.target.value)}
/>
{!isWelfare || componentType === 1 ? (
<DeleteOutlined />
) : (
<DeleteOutlined
onClick={() => {
remove(field.name)
const welfareIdList = welfareIds
welfareIdList.splice(index, 1)
setWelfareIds(welfareIdList)
}}
/>
)}
</>
</Form.Item>
))}
<Form.Item name='welfareIdAdd'>
<Button
onClick={() => {
add()
setWelfareIdNum(welfareIdNum + 1)
}}
disabled={!isWelfareId || componentType === 1}
>
<PlusOutlined />
</Button>
</Form.Item>
</>
)
}}
</Form.List>
</Form>
// 給Form.List傳值
welfareTypeInstance?.setFieldsValue({
welfareType: welfareIdList,
})
往期推薦
TypeScript很麻煩,不想使用!
不要再寫滿屏import導入啦!
京東一面:瀏覽器跨標簽頁通信的方式都有什么?
最后
歡迎加我微信,拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...
點個在看支持我吧
參考資料
往期推薦
最后
歡迎加我微信,拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...
https://wizardforcel.gitbooks.io/chrome-doc/content/index.html: https://wizardforcel.gitbooks.io/chrome-doc/content/index.html
[2]https://yeasy.gitbook.io/docker_practice: https://yeasy.gitbook.io/docker_practice
[3]https://interview.poetries.top/: https://interview.poetries.top/
[4]https://juejin.cn/post/7152697551760654349: https://juejin.cn/post/7152697551760654349
[5]https://juejin.cn/post/7101596844181962788: https://juejin.cn/post/7101596844181962788
[6]https://www.jq22.com/: https://www.jq22.com/
[7]https://blog.csdn.net/qq_34488939/article/details/121146658: https://blog.csdn.net/qq_34488939/article/details/121146658
[8]https://cn.vitejs.dev/guide/env-and-mode.html#html-env-replacement: https://cn.vitejs.dev/guide/env-and-mode.html#html-env-replacement
[9]https://github.com/vbenjs/vite-plugin-html: https://github.com/vbenjs/vite-plugin-html
