前端必學(xué)之完美組件封裝原則(適用react、vue)
前言
此文總結(jié)了我多年組件封裝經(jīng)驗(yàn),以及拜讀 antd、element-plus、vant、fusion等多個(gè)知名組件庫(kù)所提煉的完美組件封裝的經(jīng)驗(yàn);是一個(gè)開發(fā)者在封裝項(xiàng)目組件,公共組件等場(chǎng)景時(shí)非常有必要遵循的一些原則,希望和大家一起探討,也希望世界上少一些半吊子組件
下面以react為例,但是思路是相通的,在vue上也適用
1. 基本屬性綁定原則
任何組件都需要繼承className, style 兩個(gè)屬性
import classNames from 'classnames';
export interface CommonProps {
/** 自定義類名 */
className?: string;
/** 自定義內(nèi)斂樣式 */
style?: React.CSSProperties;
}
export interface MyInputProps extends CommonProps {
/** 值 */
value: any
}
const MyInput = forwardRef((props: MyInputProps, ref: React.LegacyRef<HTMLDivElement>) => {
const { className, ...rest } = props;
const displayClassName = classNames('chc-input', className);
return (
<div ref={ref} {...rest} className={displayClassName}>
<span></span>
</div>
);
});
export default ChcInput2. 注釋使用原則
- 原則上所有的
props和ref屬性類型都需要有注釋 - 且所有屬性(
props和ref屬性)禁用// 注釋內(nèi)容語(yǔ)法注釋,因?yàn)榇俗⑨尣粫?huì)被ts識(shí)別,也就是鼠標(biāo)懸浮的時(shí)候不會(huì)出現(xiàn)對(duì)應(yīng)注釋文案 - 常用的注視參數(shù)
@description描述,@version新屬性的起始版本,@deprecated廢棄的版本,@default默認(rèn)值 - 面向國(guó)際化使用的組件一般描述語(yǔ)言推薦使用英文
bad ?
interface MyInputsProps {
// 自定義class
className?: string
}
const test: MyInputsProps = {}
test.className
應(yīng)該使用如下注釋方法
after good ?
interface MyInputsProps {
/** custom class */
className?: string
/**
* @description Custom inline style
* @version 2.6.0
* @default ''
*/
style?: React.CSSProperties;
/**
* @description Custom title style
* @deprecated 2.5.0 廢棄
* @default ''
*/
customTitleStyle?: React.CSSProperties;
}
const test: MyInputsProps = {}
test.className
3. export暴露
- 組件
props類型必須export導(dǎo)出 - 如有
useImperativeHandle則ref類型必須export導(dǎo)出 - 組件導(dǎo)出
funtion必須有名稱 - 組件
funtion一般export default默認(rèn)導(dǎo)出
在沒(méi)有名稱的組件報(bào)錯(cuò)時(shí)不利于定位到具體的報(bào)錯(cuò)組件
bad ?
interface MyInputProps {
....
}
export default (props: MyInputProps) => {
return <div></div>;
};after good ?
// 暴露 MyInputProps 類型
export interface MyInputProps {
....
}
funtion MyInput(props: MyInputProps) {
return <div></div>;
};
// 也可以自己掛載一個(gè)組件名稱
if (process.env.NODE_ENV !== 'production') {
MyInput.displayName = 'MyInput';
}
export default MyInputindex.ts
export * from './input'
export { default as MyInput } from './input';當(dāng)然如果目標(biāo)組件沒(méi)有暴露相關(guān)的類型,可以通過(guò)ComponentProps和ComponentRef來(lái)分別獲取組件的props和ref屬性
type DialogProps = ComponentProps<typeof Dialog> type DialogRef = ComponentRef<typeof Dialog>
4. 入?yún)㈩愋图s束原則
入?yún)㈩愋捅仨氉裱唧w原則
- 確定入?yún)㈩愋偷目赡芮闆r下,切忌不可用基本類型一筆帶過(guò)
- 公共組件一般不使用枚舉作為入?yún)㈩愋?,因?yàn)檫@樣在使用者需要引入此枚舉才可以不報(bào)錯(cuò)
- 部分?jǐn)?shù)值類型的參數(shù)需要描述最大和最小值
bad ?
interface InputProps {
status: string
}after good ?
interface InputProps {
status: 'success' | 'fail'
}bad ?
interface InputProps {
/** 總數(shù) */
count: number
}after good ?
interface InputProps {
/** 總數(shù) 0-999 */
count: number
}5. class和style定義規(guī)則
- 禁用 CSS module 因?yàn)榇祟悓懛〞?huì)讓使用者無(wú)法修改組件內(nèi)部樣式;vue 的話可以用 scoped 標(biāo)簽來(lái)防止樣式重復(fù) 也可以實(shí)現(xiàn)父親可修改組件內(nèi)部樣式。
- 書寫組件時(shí),內(nèi)部的
class一定要加上統(tǒng)一的前綴來(lái)區(qū)分組件內(nèi)外class,避免和外部的 class 類有重復(fù)。 - class 類的名稱需要語(yǔ)意化。
- 組件內(nèi)部的所有 class 類都可以被外部使用者改變
- 禁用 important,不到萬(wàn)不得已不用行內(nèi)樣式
- 可以為顏色相關(guān) CSS 屬性留好 CSS 變量,方便外部開發(fā)主題切換
bad ?
import styles from './index.module.less'
export default funtion MyInput(props: MyInputProps) {
return (
<div className={styles.input_box}>
<span className={styles.detail}>21312312</span>
</div>
);
};after good ?
import './index.less'
const prefixCls = 'my-input' // 統(tǒng)一的組件內(nèi)部前綴
export default funtion MyInput(props: MyInputProps) {
return (
<div className={`${prefixCls}-box`}>
<span className={`${prefixCls}-detail`}>21312312</span>
</div>
);
};after good ?
.my-input-box {
height: 100px;
background: var(--my-input-box-background, #000);
}6. 繼承透?jìng)髟瓌t
書寫組件時(shí)如果進(jìn)行了二次封裝切忌不可將傳入的屬性一個(gè)一個(gè)提取然后綁定,這有非常大的局限性,一旦你基礎(chǔ)的組件更新了或者需要增加使用的參數(shù)則需要再次去修改組件代碼
bad ?
import { Input } from '某組件庫(kù)'
export interface MyInputProps {
/** 值 */
value: string
/** 限制 */
limit: number
/** 狀態(tài) */
state: string
}
const MyInput = (props: Partail<MyInputProps>) => {
const { value, limit, state } = props
// ...一些處理
return (
<Input value={value} limit={limit} state={state} />
)
}
export default MyInput以extends繼承基礎(chǔ)組件的所有屬性,并用...rest 承接所有傳入的屬性,并綁定到我們的基準(zhǔn)組件上。
after good ?
import { Input, InputProps } from '某組件庫(kù)'
export interface MyInputProps extends InputProps {
/** 值 */
value: string
}
const MyInput = (props: Partial<MyInputProps>) => {
const { value, ...rest } = props
// ...一些處理
return (
<Input value={value} {...rest} />
)
}
export default MyInput7.事件配套原則
任何組件內(nèi)部操作導(dǎo)致UI視圖改變都需要有配套的事件,來(lái)給使用者提供全量的觸發(fā)鉤子,提高組件的可用性
bad ?
export default funtion MyInput(props: MyInputProps) {
// ...省略部分代碼
const [open, setOpen] = useState(false)
const [showDetail, setShowDetail] = useState(false)
const currClassName = classNames(className, {
`${prefixCls}-box`: true,
`${prefixCls}-open`: open, // 是否采用打開樣式
})
const onCheckOpen = () => {
setOpen(!open)
}
const onShowDetail = () => {
setShowDetail(!showDetail)
}
return (
<div className={currClassName} style={style} onClick={onCheckOpen}>
<span onClick={onShowDetail}>{showDetail ? '123' : '...'}</span>
</div>
);
};所有組件內(nèi)部會(huì)影響外部UI改變的事件都預(yù)留了鉤子
after good ?
export default funtion MyInput(props: MyInputProps) {
const { onChange, onShowChange } = props
// ...省略部分代碼
const [open, setOpen] = useState(false)
const [showDetail, setShowDetail] = useState(false)
// ...省略部分代碼
const currClassName = classNames(className, {
`${prefixCls}-box`: true,
`${prefixCls}-open`: open, // 是否采用打開樣式
})
const onCheckOpen = () => {
setOpen(!open)
onChange?.(!open) // 實(shí)現(xiàn)組件內(nèi)部open改變的事件鉤子
}
const onShowDetail = () => {
setShowDetail(!showDetail)
onShowChange?.(!showDetail) // 實(shí)現(xiàn)組件詳情展示改變的事件鉤子
}
return (
<div className={currClassName} style={style} onClick={onCheckOpen}>
<span onClick={onShowDetail}>{showDetail ? '123' : '...'}</span>
</div>
);
};8. ref綁定原則
任何書寫的組件在有可能綁定ref情況下都需要暴露有ref屬性,不然使用者一旦掛載ref則會(huì)導(dǎo)致控制臺(tái)報(bào)錯(cuò)警告。
- 原創(chuàng)組件:useImperativeHandle 或 直接ref綁定組件根節(jié)點(diǎn)
interface ChcInputRef {
/** 值 */
setValidView: (isShow?: boolean) => void,
/** 值 */
field: Field
}
const ChcInput = forwardRef<ChcInputRef, MyProps>((props, ref) => {
const { className, ...rest } = props;
useImperativeHandle(ref, () => ({
setValidView(isShow = false) {
setIsCheckBalloonVisible(isShow);
},
field
}), []);
return (
<div className={displayClassName}>
...
</div>
);
});
export default ChcInputconst ChcInput = forwardRef((props: MyProps, ref: React.LegacyRef<HTMLDivElement>) => {
const { className, ...rest } = props;
const displayClassName = classNames('chc-input', className);
return (
<div ref={ref} className={displayClassName}>
<span></span>
...
</div>
);
});
export default ChcInput- 二次封裝組件:則直接ref綁定在原基礎(chǔ)組件上 或 組件根節(jié)點(diǎn)
import { Input } from '某組件庫(kù)'
const ChcInput = forwardRef((props: InputProps, ref: React.LegacyRef<Input>) => {
const { className, ...rest } = props;
const displayClassName = classNames('chc-input', className);
return <Input ref={ref} className={displayClassName} {...rest} />;
});
export default ChcInput9. 自定義擴(kuò)展性原則
在組件封裝時(shí),遇到組件內(nèi)部會(huì)用一些固定邏輯來(lái)渲染UI或者計(jì)算時(shí),最好預(yù)留一個(gè)使用者可以隨意自定義的入口,而不是只能死板采用組件內(nèi)部邏輯,這樣可以
- 增加組件的擴(kuò)展靈活性
- 減少迭代修改
bad ?
export default funtion MyInput(props: MyInputProps) {
const { value } = props
const detailText = useMemo(() => {
return value.split(',').map(item => `組件內(nèi)部復(fù)雜的邏輯:${item}`).join('\n')
}, [value])
return (
<div>
<span>{detailText}</span>
</div>
);
};after good ?
export default funtion MyInput(props: MyInputProps) {
const { value, render } = props
const detailText = useMemo(() => {
// render 用戶自定義渲染
return render ? render(value) : value.split(',').map(item => `組件內(nèi)部復(fù)雜的邏輯:${item}`).join('\n')
}, [value])
return (
<div>
<span>{detailText}</span>
</div>
);
};同理復(fù)雜的ui渲染也可以采用用戶自定義傳入render方法的方式進(jìn)行擴(kuò)展
10. 受控與非受控模式原則
對(duì)于react組件,我們往往都會(huì)要求組件在設(shè)計(jì)時(shí)需要包含受控和非受控兩個(gè)模式。
- 非受控: 的情況可以實(shí)現(xiàn)更加方便的使用組件
- 受控: 的情況可以實(shí)現(xiàn)更加靈活的使用組件,以增加組件的可用性
bad ?(只有一種受控模式)
import classNames from 'classnames';
const prefixCls = 'my-input'
export default funtion MyInput(props: MyInputProps) {
const { value, className, style, onChange } = props
const currClassName = classNames(className, {
`${prefixCls}-box`: true,
`${prefixCls}-open`: value, // 是否采用打開樣式
})
const onCheckOpen = () => {
onChange?.(!value)
}
return (
<div className={currClassName} style={style} onClick={onCheckOpen}>
<span>12312</span>
</div>
);
};after good ?
import classNames from 'classnames';
const prefixCls = 'my-input'
export default funtion MyInput(props: MyInputProps) {
const { value, defaultValue = true, className, style, onChange } = props
// 實(shí)現(xiàn)非受控模式
const [open, setOpen] = useState(value || defaultValue)
useEffect(() => {
if(typeof value !== 'boolean') return
setOpen(value)
}, [value])
const currClassName = classNames(className, {
`${prefixCls}-box`: true,
`${prefixCls}-open`: open, // 是否采用打開樣式
})
const onCheckOpen = () => {
onChange?.(!open)
// 非受控模式下 組件內(nèi)部自身處理
if(typeof value !== 'boolean') {
setOpen(!open)
}
}
return (
<div className={currClassName} style={style} onClick={onCheckOpen}>
<span>12312</span>
</div>
);
};11. 最小依賴原則
所有組件封裝都要遵循最小依賴原則,在條件允許的情況下,簡(jiǎn)單的方法需要引入新的依賴的情況下采用手寫方式。這樣避免開發(fā)出非常依賴融于的組件或組件庫(kù)
bad ?
import { useLatest } from 'ahooks' // 之前組件庫(kù)無(wú)ahooks, 會(huì)引入新的依賴!
import classNames from 'classnames';
const ChcInput = forwardRef((props: InputProps, ref: React.LegacyRef<Input>) => {
const { className, ...rest } = props;
const displayClassName = classNames('chc-input', className);
const funcRef = useLatest(func); // 解決回調(diào)內(nèi)無(wú)法獲取最新state問(wèn)題
return <div className={displayClassName} {...rest}></div>;
});
export default ChcInputafter good ?
// hooks/index.tsx
import { useRef } from 'react';
export function useLatest(value) {
const ref = useRef(value);
ref.current = value;
return ref;
}
...
// 組件
import { useLatest } from '@/hooks' // 之前組件庫(kù)無(wú)ahooks引入新的依賴!
import classNames from 'classnames';
const ChcInput = forwardRef((props: InputProps, ref: React.LegacyRef<Input>) => {
const { className, ...rest } = props;
const displayClassName = classNames('chc-input', className);
const funcRef = useLatest(func); // 解決回調(diào)內(nèi)無(wú)法獲取最新state問(wèn)題
return <div className={displayClassName} {...rest}></div>;
});
export default ChcInput當(dāng)然依賴包是否引入也要參考當(dāng)時(shí)的使用情況,比如如果ahooks在公司內(nèi)部基本都會(huì)使用,那這個(gè)時(shí)候引入也無(wú)妨。
12. 功能拆分,單一職責(zé)原則
如果一個(gè)組件內(nèi)部能力很強(qiáng)大,可能包含多個(gè)功能點(diǎn),不建議將所有能力都只在組件內(nèi)部體現(xiàn),可以將這些功能拆分成其他的公共組件, 一個(gè)組件只處理一個(gè)功能點(diǎn)(單一職責(zé)原則),提高功能的復(fù)用性和靈活性。
當(dāng)然業(yè)務(wù)組件除外,業(yè)務(wù)組件可以在組件內(nèi)實(shí)現(xiàn)多個(gè)組件的整合完成一個(gè)業(yè)務(wù)能力的單一職責(zé)。
bad ?
const MyShowPage = forwardRef((props: MyTableProps, ref: React.LegacyRef<Table>) => {
const { data, imgList, ...rest } = props;
return (
<div>
<Table ref={ref} data={data} {...rest}>
{/* 表格顯示相關(guān)功能封裝 ...省略一堆代碼 */}
</Table>
<div>
{/* 圖例相關(guān)功能封裝 ...省略一堆代碼 */}
</div>
</div>
)
});將表格和圖例兩個(gè)功能點(diǎn)拆分成單獨(dú)的兩個(gè)公共組件
after good ?
const MyShowPage = forwardRef((props: MyTableProps, ref: React.LegacyRef<Table>) => {
const { data, imgList, ...rest } = props;
return (
<div>
{/* 表格組件只處理表格內(nèi)容 */}
<MyTable ref={ref} data={data} {...rest}></Table>
{/* 圖片組件只處理圖片展示能力 */}
<MyImg data={imgList}>
</div>
)
});
當(dāng)然如果完全沒(méi)有復(fù)用價(jià)值的組件或功能點(diǎn)也是沒(méi)必要拆分的。
13. 業(yè)務(wù)組件去業(yè)務(wù)化
我們?cè)诜庋b業(yè)務(wù)組件的時(shí)候,切忌不可將相關(guān)復(fù)雜的業(yè)務(wù)邏輯以及運(yùn)算放到組件外面由使用者去實(shí)現(xiàn),在組件內(nèi)部只是一些簡(jiǎn)單的封裝;這很難達(dá)到業(yè)務(wù)組件的價(jià)值最大化,組件的通用性會(huì)降低,使用心智負(fù)擔(dān)也會(huì)加大。
比如:有個(gè)table組件,負(fù)責(zé)將傳入的數(shù)據(jù)進(jìn)行一個(gè)業(yè)務(wù)渲染和展示:
bad ?
const MyTable = forwardRef((props: MyTableProps, ref: React.LegacyRef<Table>) => {
const { data, ...rest } = props;
return (
<Table ref={ref} data={data} {...rest}>
<Table.Column dataIndex="test1" title="標(biāo)題1"/>
<Table.Column dataIndex="test2" title="標(biāo)題2"/>
<Table.Column dataIndex="data" title="值"/>
</Table>
)
});但是有一個(gè)業(yè)務(wù)是當(dāng)數(shù)據(jù)的type=1時(shí),data的值要乘2展示,則上面的組件使用者只能這樣使用:
const res = [...]
const data = useMemo(() => {
return res.map(item => ({
...item,
data: item.type === 1 ? item.data*2 : item.data
}))
}, [res])
return (
<MyTable data={data}/>
)顯然這樣的封裝在使用者這邊會(huì)有一些心智負(fù)擔(dān),假如一個(gè)不熟悉業(yè)務(wù)的人來(lái)開發(fā)很容易會(huì)遺漏,所以這個(gè)時(shí)候需要業(yè)務(wù)組件去業(yè)務(wù)化,降低使用者的門檻
after good ?
const MyTable = forwardRef((props: MyTableProps, ref: React.LegacyRef<Table>) => {
const { data, ...rest } = props;
const dataRender = (item: ListItem) => {
return item.type === 1 ? item.data*2 : item.data
}
return (
<Table ref={ref} data={data} {...rest}>
<Table.Column dataIndex="test1" title="標(biāo)題1"/>
<Table.Column dataIndex="test2" title="標(biāo)題2"/>
<Table.Column dataIndex="data" title="值" render={dataRender}/>
</Table>
)
});使用者無(wú)需關(guān)心業(yè)務(wù)也可以順利圓滿完成任務(wù):
const res = [...]
return (
<MyTable data={res}/>
)14. 最大深度擴(kuò)展性
當(dāng)組件傳入的數(shù)據(jù)可能會(huì)有樹形等有深度的格式,而組件內(nèi)部也會(huì)針對(duì)其渲染出有遞歸深度的UI時(shí),需要考慮到使用者對(duì)于數(shù)據(jù)深度的不可控性,組件內(nèi)部需要預(yù)留好無(wú)限深度的可能
如下渲染組件方式只有一層的深度,很有局限性
bad ?
interface Columns extends TableColumnProps {
columns: TableColumnProps[]
}
const MyTable = forwardRef((props: MyTableProps, ref: React.LegacyRef<Table>) => {
const { data, columns = [], ...rest } = props;
const renderColumn = useMemo(() => {
return columns.map(item => {
return item.columns ? (
<Table.Column {...item}>
{item.columns.map(column => <Table.Column {...column}/>)}
</Table.Column>
) : <Table.Column {...item}/>
})
}, [columns])
return (
<Table ref={ref} data={data} {...rest}>
{renderColumn}
</Table>
)
});after good ?
interface Columns extends TableColumnProps {
columns: Columns[] // 改變?yōu)槔^承自己
}
const MyTable = forwardRef((props: MyTableProps, ref: React.LegacyRef<Table>) => {
const { data, columns = [], ...rest } = props;
return (
<Table ref={ref} data={data} {...rest}>
{/* 采用外部組件 */}
<MyColumn columns={columns}/>
</Table>
)
});
const MyColumn = (props: MyColumnProps) => {
const { columns = [] } = props
return (
item.columns ? (
<Table.Column {...item}>
{/* 遞歸渲染數(shù)據(jù),實(shí)現(xiàn)數(shù)據(jù)的深度無(wú)限性 */}
<MyColumn columns={item.columns}/>
</Table.Column>
) :
<Table.Column {...item}/>
)
}15. 多語(yǔ)言可配制化
- 組件內(nèi)部所有的語(yǔ)言都需要可以修改,兼容多語(yǔ)言的使用場(chǎng)景
- 默認(rèn)推薦英文
- 內(nèi)部語(yǔ)言變量較多時(shí)可以統(tǒng)一暴露一個(gè)例如
strings對(duì)象參數(shù),其內(nèi)部可以傳入所有可以替換文案的key
bad ?
const prefixCls = 'my-input' // 統(tǒng)一的組件內(nèi)部前綴
export default funtion MyInput(props: MyInputProps) {
const { title = '標(biāo)題' } = props;
return (
<div className={`${prefixCls}-box`}>
<span className={`${prefixCls}-title`}>{title}</span>
<span className={`${prefixCls}-detail`}>詳情</span>
</div>
);
};after good ?
const prefixCls = 'my-input' // 統(tǒng)一的組件內(nèi)部前綴
export default funtion MyInput(props: MyInputProps) {
const { title = 'title', detail = 'detail' } = props;
return (
<div className={`${prefixCls}-box`}>
<span className={`${prefixCls}-title`}>{title}</span>
<span className={`${prefixCls}-detail`}>{detail}</span>
</div>
);
};16.異常捕獲和提示
- 對(duì)于用戶傳入意外的參數(shù)可能帶來(lái)錯(cuò)誤時(shí)要控制臺(tái) console.error 提示
- 不要直接在組件內(nèi)部 throw error,這樣會(huì)導(dǎo)致用戶的白屏
- 缺少某些參數(shù)或者參數(shù)不符合要求但不會(huì)導(dǎo)致報(bào)錯(cuò)時(shí)可以使用 console.warn 提示
bad ?
export default funtion MyCanvas(props: MyCanvasProps) {
const { instanceId } = props;
useEffect(() => {
initDom(instanceId)
}, [])
return (
<div>
<canvas id={instanceId} />
</div>
);
};after good ?
export default funtion MyCanvas(props: MyCanvasProps) {
const { instanceId } = props;
useEffect(() => {
if(!instanceId){
console.error('missing instanceId!')
return
}
initDom(instanceId)
}, [])
return (
<div>
<canvas id={instanceId} />
</div>
);
};17. 語(yǔ)義化原則
組件的命名,組件的api,方法,包括內(nèi)部的變量定義都要遵循語(yǔ)義化的原則,嚴(yán)格按照其代表的功能來(lái)命名。
到此這篇關(guān)于前端必學(xué)之完美組件封裝原則的文章就介紹到這了,更多相關(guān)前端組件封裝原則內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react中如何對(duì)自己的組件使用setFieldsValue
react中如何對(duì)自己的組件使用setFieldsValue問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03
React中的setState使用細(xì)節(jié)和原理解析(最新推薦)
這篇文章主要介紹了React中的setState使用細(xì)節(jié)和原理解析(最新推薦),前面我們有使用過(guò)setState的基本使用, 接下來(lái)我們對(duì)setState使用進(jìn)行詳細(xì)的介紹,需要的朋友可以參考下2022-12-12
基于react框架使用的一些細(xì)節(jié)要點(diǎn)的思考
下面小編就為大家?guī)?lái)一篇基于react框架使用的一些細(xì)節(jié)要點(diǎn)的思考。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-05-05
React?Fiber?樹思想解決業(yè)務(wù)實(shí)際場(chǎng)景詳解
這篇文章主要為大家介紹了React?Fiber?樹思想解決業(yè)務(wù)實(shí)際場(chǎng)景詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12

