.先展現示例
TabPage
實際應用示例
尾部貼完整代碼,若只需要實現功能,直接復制代碼即可。
1.首先分析 頁面需求,需要添加 標簽 ,點擊標簽傳遞參數。委托思路。
第一步:分析標簽結構
標簽頁面:展示圖標。文本信息。關閉按鈕。
public class TabHeader : FlowLayoutPanel
受限聲明一個控件,繼承 FlowLayoutPanel 繼承此控件主要是關鍵代碼系統(tǒng)控件已經都具有,不用自己造輪子,按照真正開發(fā)思路,還是可以繼承 control控件自定義分割,重新封裝。根據個人喜好。
標簽頁 制作:
public class TabHeaderItem : WenControl
聲明一個標簽頁 繼承WenControl : 此控件是個人封裝的基礎控件,若要了解可以查看往期文章。或者開源搜索 WenSkin。可以找到相關介紹。
private TabHeader owner;
public TabHeaderItem(TabHeader owner, string path)
{
this.owner=owner;
this.Path=path;
this.Width=120;
this.Height=30;
WenButton button=new WenButton()
{
Size=new Size(16, 16),
Image=Properties.Resources.close,
ImageSize=new Size(14, 14),
TextImageRelation=TextImageRelation.Overlay,
Location=new Point(this.Width - 16 - 4, 7),
};
button.Click +=Button_Click;
this.Controls.Add(button);
this.Paint +=FileItemControl_Paint;
this.MouseLeave +=TabHeaderItem_MouseLeave;
this.MouseEnter +=TabHeaderItem_MouseEnter;
}
聲明構造函數,部分細節(jié)后續(xù)代碼注意講解。
此處最主要是 尺寸信息 當然可以直接 size=new size();個人習慣。
聲明一個關閉按鈕。可以采用gdi畫,也可以直接添加一個圖片按鈕。
private void Button_Click(object sender, EventArgs e)
{
owner.Controls.Remove(this);
this.Dispose();
}
文中就直接調用往期封裝好的按鈕控件。若需要了解可以跳轉往期文章查看。講解了關于按鈕 封裝。
this.Paint +=FileItemControl_Paint;
畫標簽 的文字和 內容,可以直接重寫 也可以使用委托畫,個人喜好。
private void FileItemControl_Paint(object sender, PaintEventArgs e)
{
var recImage=new Rectangle(4, 4, 22, 22);
var recStr=new Rectangle(28, 4, this.Width - 28 - 4 - 22 - 2, 22);
Graphics g=e.Graphics.SetGDIHigh();
g.DrawImage(Properties.Resources.file, recImage);
g.DrawString(FileText, this.Font, Brushes.White, recStr, ControlHelper.StringConters);
}
this.MouseLeave +=TabHeaderItem_MouseLeave;
this.MouseEnter +=TabHeaderItem_MouseEnter;
鼠標事件處理
public void ReBackColor()
{
if (owner.selectedItem==this)
{
this.BackColor=Color.FromArgb(63, 63, 70);
}
else
{
this.BackColor=Color.Transparent;
}
}
private void TabHeaderItem_MouseEnter(object sender, EventArgs e)
{
this.BackColor=Color.FromArgb(62, 62, 64);
}
private void TabHeaderItem_MouseLeave(object sender, EventArgs e)
{
ReBackColor();
}
主要是鼠標移動到指定位置改變顏色,離開后顏色會變化。直接修改背景顏色即可。
可以設置一個選中顏色
需要在主體中明一個選中選線。便于后續(xù)比較改變背景顏色。
#region 私有屬性
private TabHeaderItem selectedItem;
#endregion
若需要暴露在 全部 可以將私有屬性改為公有。然后聲明變化。
接下來就是 添加標簽代碼。
文中有一個需求就是最新 添加標簽在最前端,并選中。
public void Add(string path)
{
TabHeaderItem f=new TabHeaderItem(this, path);
foreach (TabHeaderItem item in this.Controls)
{
if (path==item.Path)
{
this.Controls.SetChildIndex(item, 0);
re(item);
return;
}
}
f.Click +=(s, e)=>
{
re(f);
};
this.Controls.Add(f);
re(f);
this.Controls.SetChildIndex(f, 0);
void re(TabHeaderItem item)
{
ItemChanged?.Invoke(this, new TabEventArgs(path));
var ci=selectedItem;
selectedItem=item;
ci?.ReBackColor();
item.ReBackColor();
}
}
若有其他需求可直接更改即可。
#region 委托
public delegate void TabEventHandler(object sender, TabEventArgs e);
public class TabEventArgs : EventArgs
{
public TabEventArgs(string path)
{
Path=path;
}
public string Path { get; set; }
}
public event TabEventHandler ItemChanged;
#endregion
至此,完整解決一個tab標簽。
完整代碼塊
麻不燒的Github
配合著源碼,用心看完這篇文章,你便領悟了封裝的精髓,麻雀雖小,五臟俱全。
前記
業(yè)務代碼之外的代碼,我想稱之為增值代碼。
什么意思?
作為一個程序員,你應該除了完成領導安排的任務,你還應該有一些自己的時間,用來“玩”一些比較有意思的事情。
當現有框架、庫滿足不了我們需求的時候,我們應該嘗試去自己造一些工具。也正是這些你所實現的,成就了他人,造就了自己。
不信,你且想一想,他人會關心你寫的具體的業(yè)務邏輯代碼嗎?我想他們更關心的是,你寫的插件,是如何使用的吧,以及方不方便他們借此完成他們自己業(yè)務代碼。
再通俗一點,他們不會記住你,但是他們會記住你的Api,因而憶起你。
還有很重要的一點,所有的技術,都是服務于業(yè)務的,否則,就是扯皮。
背景
入職新公司以來,一直忙于開發(fā)業(yè)務,過程中,多處用到了領導寫的牛逼工具。說實話,內心由衷的佩服,簡直就是解放生產力,放到古代,就是要被封神滴。
舉個例子:
領導花了一段時間,研究出了一個自動表單生成器。之前手寫一個表單配置頁,加上表單驗證,可能需要半天,甚至更久。
現在呢?所有的表單、樣式及驗證,都可以通過代碼配置實現,二十分鐘可能就完成了。
由此,我悟出了一個道理:
重復地做一件事,不如用心地做“一件事”。
我想,你肯定也想成為他人口中的那個男人,但整天活在自己的世界里,你可能一時并不知道該如何去做,這里我想告訴你:
成長的一個關鍵性因素,就是來自于模仿。
對的,你可以先嘗試著去閱讀下他人的代碼,看看別人的實現方式,再者可以去github上溜一圈,優(yōu)秀項目太多了,仿著寫去唄。
我自己是一名從事了多年開發(fā)的web前端老程序員,目前辭職在做自己的web前端私人定制課程,今年年初我花了一個月整理了一份最適合2019年學習的web前端學習干貨,各種框架都有整理,送給每一位前端小伙伴,想要獲取的可以關注我的頭條號并在后臺私信我:前端,即可免費獲取。
只要你想學,你就一定能學會,只不過是實現的方式好與壞而已,這些是需要后期不斷完善的。
鑒于本篇文章快要跑題了,不再多述,進入正題...
正文
1.組件和插件的區(qū)別與聯(lián)系
區(qū)別
聯(lián)系
這里不做過多闡述,有興趣可以參考下勞卜大大的這篇文章,寫的很通俗易懂。
2.實現插件的必備因素
基礎
你需要清楚的知道vue的一些高階知識點以及相關內容,比如
技巧
以下這個技巧是今天開發(fā)的時候悟出來的,目測很有用:
別著急開發(fā),先想著如何在開發(fā)中使用你的插件
什么意思?順著我的思路捋下去
因為我想實現一個全局toast插件,大概用法
this.$toast('那個男人') // todo
光彈出文案不行,應該有一個控制彈出方向的變量
this.$toast('那個男人',{ position:'topCenter' })
全局toast的狀態(tài)應該有多種,比如常見的成功、錯誤、警告、普通...
// 成功success // 錯誤error // 警告warning // 普通info this.$toast('那個男人',{ position:'topCenter', type:'success' })
應該有一個時間變量去控制多長時間自動消失toast
this.$toast('那個男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast })
會不會存在一種業(yè)務場景,我們不需要自動消失toast
this.$toast('那個男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast autoClose: false })
如果我想在toast結束后,觸發(fā)一些回調動作,比如刪除成功toast后刷新列表頁面
this.$toast('那個男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast autoClose: true, callback () { ... } })
toast的內容,可能會很長,因此應該有兩個變量分別控制toast寬度和高度
this.$toast('那個男人',{ position:'topCenter', type:'success', closeTime: 3 // 控制3秒后消失toast autoClose: true, callback () { ... }, width:300, height:80 })
至此,基礎功能應該都涵蓋了,這個時候你要去考慮一些內建的問題
配置項多應該怎么解決-默認值
默認給個type唄,比如我的項目中默認的type是info,當我在使用的時候,沒有傳入type時,默認為info
因為大部分的toast場景都是短暫的停留在頁面,所以autoClose設置為true
又因為大部分的toast文案比較短,所以我的默認toast長寬設置為300、80應該足夠了
...
以上默認配置,都可以在使用的時候,傳入參數覆蓋默認參數
針對不同的狀態(tài),toast圖標、顏色、標題之間有什么聯(lián)系?
本地存一個map映射配置表,根據傳入的type,我就可以準確的知道圖標、顏色、標題應該是什么
總結幾點:
實現
上文提到過,組件可以暴露數據給插件,對于這句話
我的理解是,組件是靜態(tài)的,只是對外暴露一些參數入口props。插件,讓我們可以動態(tài)的往其中注入一些自定義參數。具體的實現,還是在組件當中完成。
于是乎:我寫了一個靜態(tài)組件,通過props定義上文提到的相關變量
先看下script部分
再來看下html部分
可以看到,內部實現其實很簡單,無非就是通過外部傳入的props,控制內部的展示細節(jié)而已
到這里靜態(tài)組件基本已經完成了(css樣式代碼不在這里貼了)
注意:
靜態(tài)組件怎么變成插件使用呢?
這里不再做過多闡述,vue封裝插件的常用方法主要有以下四種,有疑惑的話,建議觀閱vue開發(fā)插件,當然我覺得你應該還需要去了解下Vue.extend的用法,插件的實現離不開它哦。
看下關鍵部分:該文件也是我們后期webpack打包(build)的入口文件
該文件內容涉及到的知識點,也是開發(fā)一個vue插件最核心的內容。里面的每一行代碼,都充滿了殺機~
至此,關于插件實現部分基本已經全部完成。
3.如何將自己的插件上傳到npm上去
這里的話,網上的教程有很多,我理解你只需要了解以下幾行代碼的作用,就足夠了
// webpack.config.js module.exports={ entry: process.env.NODE_ENV==='development' ? './src/main.js' : './src/index.js', output: { path: path.resolve(__dirname, './dist'), publicPath: '/dist/', filename: 'build.js', libraryTarget: 'umd' }, ... // package.json { "name": "mbs-toast", "description": "a toast plugin base on Vue2", "version": "1.0.0", "author": "xxx", "license": "MIT", "private": false, "main": "dist/build.js", "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", ...
這里我用的模版是自己在官方webpack-simple模版的基礎上做了一些定制化的,里面為了方便我平時開發(fā),加入了scss、eslint,這樣的話,后面就不用每次手動install了,有興趣的可以看下README,定制一份屬于自己的腳手架模板
在你了解了上述背景后,你只需要執(zhí)行以下幾步即可實現皆大歡喜
順利的話,現在你已經可以在正式項目中,通過
npm install -S xxx 安裝你的私有包了
最后在你的入口文件注冊你的插件
import toastPlugin from 'xxx'Vue.use(toastPlugin) // 這里Vue.use的第二個參數,可以通過全局配置,做一些自定義配置,有需要的自行前往學習
到這里,所有的一切,已塵埃落定
你可以在代碼中愉快的使用了
this.$toast('塵埃落定', { callback () { console.log('hello world') }, type: 'success', // position: 'topRight', autoClose: false})
最后
我在寫這個插件之前,在Github上看到一個大神封裝的插件。四個字描述下,嘆為觀止,有興趣的一定要去看下,我相信愛學習的你,一定會收獲滿滿。同時在開發(fā)該插件時,一些樣式及動畫,也做了相應的參考。
該插件的源碼已經上傳mbs-toast,方便大家參考。同時,上述提到的form表單生成器,我也嘗試著自己實現了一遍,有興趣的可以一起加入哦。所有的插件以及組件目前都匯總在麻不燒的Github里,文檔和README正在不斷完善中~
碼字不易,且行且珍惜!
源自:https://juejin.im/post/5dc42069f265da4d3962a8e4
聲明:文章著作權歸作者所有,如有侵權,請聯(lián)系小編刪除。
小希這次帶來了進階版的Vue3 + Vite項目框架的封裝搭建
本文主要的切入點有
一般情況下,項目開發(fā)只有一個入口,只需要配置一個入口,一個項目
但有時多個同業(yè)務、同類型的項目,有很多可以復用的業(yè)務,組件,工具類等,就可以放在同一個代碼庫里進行維護,不用新建多個代碼庫
每個項目都有自己獨立的入口,可以獨立打包并進行部署,低耦合,不會相互影響,同時還可以復用相同的組件,業(yè)務等,可以大大地提高開發(fā)效率和后期的維護
如上圖所示,有兩個項目,分別是app1,app2,每個項目都有自己獨有的main.ts入口文件,App.vue文件,以及路由,倉庫pinia,組件等,同時也有共有的組件,utils工具類等
在package.json中,將下圖單入口的配置
改為
{
"scripts": {
"dev:app1": "vite serve src/app1/ --config ./vite.config.ts",
"dev:app2": "vite serve src/app2/ --config ./vite.config.ts",
"build:app1": "vue-tsc && vite build",
"build:app2": "vue-tsc && vite build"
},
這樣配置可實現項目的獨立運行,獨立打包
在vite.config.ts中配置:
/* 項目名稱 */
//采用這種方式可以動態(tài)獲取項目名稱,當然,如果項目少可以手動配置
let appName=process.env.npm_lifecycle_event
appName=appName.slice(appName.indexOf(':') + 1) //app1、app2
export default defineConfig({
root: `./src/${appName}/`,
build: {
rollupOptions: {
input: {
[appName]: path.resolve(__dirname, `src/${appName}/index.html`)
},
output: {
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]',
}
}
}
})
打包構建
pnpm build:app1
pnpm build:app2
基于多入口打包,也就是一個代碼庫同時維護多個同類型的項目情況下,可以通過配置實現自動化生成項目基礎模板,這樣,當需要在代碼庫新建一個新項目時,可以通過命令行快速創(chuàng)建
這個插件用來詢問用戶輸入項目名稱,這是一個比較在處理命令行交互比較常見的庫
主要用于實現命令行交互式界面。幫助我們與用戶進行交互式交流
它有幾個特點:提供錯誤反饋,詢問問題,解析輸入,驗證答案
詳細可參考 命令行交互工具inquirer
安裝
pnpm add inquirer@^8.0.0 -S
在package.json里添加
"scripts": {
"init-app": "node ./src/utils/initApp/index.ts"
}
當執(zhí)行這個命令時,會自動去執(zhí)行,在本地utils文件夾下的initApp文件里的js腳本,在src目錄下會自動生成一個新的文件夾(項目)
在utils下新增initApp文件夾以及index.ts和temlate
在index.ts添加以下代碼
#!/usr/bin/env node
console.log('您正在創(chuàng)建項目')
const path=require('path')
const fs=require('fs')
const inquirer=require('inquirer')
const stat=fs.stat
const targetDir=path.resolve(__dirname, './template')
//復制文件目錄
const copyFile=(targetDir, resultDir)=> {
// 讀取文件、目錄
fs.readdir(targetDir, function (err, paths) {
if (err) {
throw err
}
paths.forEach(function (p) {
const target=path.join(targetDir, '/', p)
const res=path.join(resultDir, '/', p)
let read
let write
stat(target, function (err, statsDta) {
if (err) {
throw err
}
if (statsDta.isFile()) {
read=fs.createReadStream(target)
write=fs.createWriteStream(res)
read.pipe(write)
} else if (statsDta.isDirectory()) {
fs.mkdir(res, function () {
copyFile(target, res)
})
}
})
})
})
}
const question=[
{
type: 'input',
name: 'name',
message: '請輸入項目名稱:'
}
]
const createProject=()=> {
// 詢問用戶問題
inquirer
.prompt(question)
.then(({ name })=> {
// name 為輸入的項目名稱
name=name.trim()
if (!name) {
console.log('項目目錄不能為空')
// 如果輸入空,繼續(xù)詢問
createProject()
return false
}
// 目標路徑,要放在module目錄下
const resultDir=path.resolve(__dirname, '../../', name)
// fs.access()方法用于測試文件是否存在
fs.access(resultDir, function (err, data) {
if (err) {
// 創(chuàng)建文件
fs.mkdir(resultDir, function (err, data) {
if (err) {
throw err
}
// 復制模版文件
copyFile(targetDir, resultDir)
})
console.log(`${name} 項目已創(chuàng)建成功`)
} else {
console.log(`${name} 項目目錄已存在,請輸入其他名稱`)
// 不存在,繼續(xù)詢問
createProject()
}
})
})
.catch((err)=> {
console.log(err)
})
}
createProject()
注:此代碼copy==> Vue3項目框架搭建封裝,一次學習,終身受益【萬字長文,滿滿干貨】
在temlate文件夾下新增項目所需要的文件目錄,main.ts以及App.vue是必須的,因為它是獨立的項目
app3項目自動生成
數據存儲在緩存(內存)中,優(yōu)點讀寫更快,可以保存任意的js類型數據和對象,比如當我們刷新瀏覽器的時候,數據會丟失,所以需要實現pinia持久化
安裝
pnpm add pinia-plugin-persist
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persist'
const pinia=createPinia()
pinia.use(piniaPersist)
createApp({})
.use(pinia)
.mount('#app')
// store/use-user-store.ts
import { defineStore } from 'pinia'
export const useUserStore=defineStore('storeUser', {
state: ()=> {
return {
firstName: 'S',
lastName: 'L',
accessToken: 'xxxxxxxxxxxxx'
}
},
actions: {
setToken (value: string) {
this.accessToken=value
}
},
persist: {
enabled: true,
//這里可以單獨給每個字段配置存儲的形式sessionStorage/localStorage
//paths配置state里的字段,不同的數據采取不同的存儲方式
strategies: [
{ storage: sessionStorage, paths: ['firstName', 'lastName'] },
{ storage: localStorage, paths: ['accessToken'] },
],
}
})
strategies 字段說明:
屬性 | 描述 |
key | 自定義存儲的 key,默認是 store.$id |
storage | 可以指定localStorage/sessionStorage,或者自定義存儲類型,默認為 sessionStorage |
paths | state 中的字段名,按組打包儲存 |
也可以自定義存儲類型,更多具體配置戳pinia-plugin-persist插件官網地址
源碼解析
核心是通過 store.$subscribe去監(jiān)聽倉庫數據,當倉庫數據發(fā)生變化時會觸發(fā)回調,更改本地緩存數據,當刷新后就會從本地緩存取出相關的數據
import { PiniaPluginContext } from 'pinia'
type Store=PiniaPluginContext['store']; //pinia插件上下文
type PartialState=Partial<Store['$state']>;
//調用函數將倉庫數據存儲到本地
export const updateStorage=(strategy: PersistStrategy, store: Store)=> {
const storage=strategy.storage || sessionStorage //可以自定義存儲類型,默認為sessionStorage
const storeKey=strategy.key || store.$id //可以自定義存儲的 key,默認是 store.$id
//判斷是否有配置paths,如果沒有就緩存一整個倉庫中的state
if (strategy.paths) {
//遍歷paths里面的字段,并通過 store.$state[key]獲取相應的數據
const partialState=strategy.paths.reduce((finalObj, key)=> {
finalObj[key]=store.$state[key]
return finalObj
}, {} as PartialState)
//存儲到本地
storage.setItem(storeKey, JSON.stringify(partialState))
} else {
storage.setItem(storeKey, JSON.stringify(store.$state))
}
}
export default ({ options, store }: PiniaPluginContext): void=> {
//判斷enabled是否為true
if (options.persist?.enabled) {
const defaultStrat: PersistStrategy[]=[{
key: store.$id,
storage: sessionStorage,
}]
const strategies=options.persist?.strategies?.length ? options.persist?.strategies : defaultStrat
strategies.forEach((strategy)=> {
const storage=strategy.storage || sessionStorage
const storeKey=strategy.key || store.$id
//根據key判斷是否在本地緩存中,如果在刷新后會從本地緩存中將數據賦給pinia倉庫的state
const storageResult=storage.getItem(storeKey)
// 如果本地中存在同步數據,更新倉庫state數據
//(比如瀏覽器刷新后會進行判斷,如果有數據會賦值給pinia倉庫的state,實現pinia持久化)
if (storageResult) {
store.$patch(JSON.parse(storageResult))
updateStorage(strategy, store)
}
})
//通過$subscribe監(jiān)聽state,倉庫數據更改會觸發(fā)回調同步更改本地數據
store.$subscribe(()=> {
strategies.forEach((strategy)=> {
updateStorage(strategy, store)
})
})
}
}
通過顯示進度條的形式,來提高用戶體驗,可用在進入/離開路由時觸發(fā)動畫,也可在發(fā)接口時使用
安裝
pnpm add nprogress -S
只需調用start()和done()即可控制進度條。
NProgress.start();
NProgress.done();
切換路由
router.beforeEach((to, from, next)=> {
NProgress.start()
next()
})
router.afterEach(()=> {
NProgress.done()
})
發(fā)請求時
// axios請求攔截器
axios.interceptors.request.use(
config=> {
NProgress.start() // 設置加載進度條(開始..)
return config
},
error=> {
return Promise.reject(error)
}
)
// axios響應攔截器
axios.interceptors.response.use(
function(response) {
NProgress.done() // 設置加載進度條(結束..)
return response
},
function(error) {
return Promise.reject(error)
}
)
其它詳細配置請戳官網:ricostacruz.com/nprogress/
PostCSS 是一種 JavaScript 工具,可將你的 CSS 代碼轉換為抽象語法樹 (AST),然后提供 API(應用程序編程接口)用于使用 JavaScript 插件對其進行分析和修改。
Autoprefixer主要功能是解析CSS并使用Can I Use中的值向CSS規(guī)則添加供應商前綴。以兼容各種瀏覽器,部分CSS屬性需要加上不同的前綴以兼容不同的瀏覽器。通過配置Autoprefixer,自動為CSS屬性添加對應瀏覽器的前綴。
postcss-px-to-viewport 用于將單位為 px 的尺寸轉換為視口單位(vw, vh, vmin, vmax)
下面用到Autoprefixer和postcss-px-to-viewport這兩個插件進行viewport適配
安裝
pnpm add postcss-px-to-viewport -D
pnpm add autoprefixer -D
創(chuàng)建postcss.config.js并配置
// postcss.config.js
module.exports=()=> {
return {
plugins: {
autoprefixer: {},
'postcss-px-to-viewport': {
unitToConvert: 'px', // 需要轉換的單位,默認為"px"
viewportWidth: 1920, // 設計稿的視口寬度
unitPrecision: 5, // 單位轉換后保留的精度
propList: ['*'], // 能轉化為vw的屬性列表
viewportUnit: 'vw', // 希望使用的視口單位
fontViewportUnit: 'vw', // 字體使用的視口單位
selectorBlackList: [], // 需要忽略的CSS選擇器,不會轉為視口單位,使用原有的px等單位。
minPixelValue: 1, // 設置最小的轉換數值,如果為1的話,只有大于1的值會被轉換
mediaQuery: false, // 媒體查詢里的單位是否需要轉換單位
replace: true, // 是否直接更換屬性值,而不添加備用屬性
exclude: undefined, // 忽略某些文件夾下的文件或特定文件,例如 'node_modules' 下的文件
include: undefined, // 如果設置了include,那將只有匹配到的文件才會被轉換
landscape: false, // 是否添加根據 landscapeWidth 生成的媒體查詢條件 @media (orientation: landscape)
landscapeUnit: 'vw', // 橫屏時使用的單位
landscapeWidth: 1920 // 橫屏時使用的視口寬度
}
}
}
}
效果如下
不同視口寬度,界面會響應性變化
作者:小希學前端
鏈接:https://juejin.cn/post/7327965216826032154
*請認真填寫需求信息,我們會在24小時內與您取得聯(lián)系。