這幾天幫朋友忙,用了一周時間,高仿了一個釘釘審批流。
這個東西會有不少朋友有類似需求,就分享出來,希望能有所幫助。為了方便朋友的使用,設(shè)計制作的時候,盡量做到節(jié)點配置可定制,減少集成成本。如果您的項目有審批流需求,這個項目可以直接拿過去使用。
React初學者也可以把本項目當做研讀案例,學習并快速上手React項目。通過研讀項目代碼,您可以學到如何設(shè)計一個react項目架構(gòu),輔助理解react設(shè)計哲學,學習css-in-js在項目中的使用,并理解其優(yōu)勢。理解Redux這種immutable的狀態(tài)管理好處等。
本文章只包含審批流設(shè)計部分,不包含表單的設(shè)計,表單的設(shè)計請參考作者另一個可視化前端項目RxDrag:
項目地址:github.com/codebdy/rxd…
演示地址:rxdrag.vercel.app
相關(guān)文章:
《實戰(zhàn),一個高擴展、可視化低代碼前端,詳實、完整》
《挑戰(zhàn)零代碼:可視化邏輯編排》
項目地址:github.com/codebdy/din…
演示地址:dingflow.vercel.app/
運行快照:
這個項目非常典型,它足夠小,不至于讓文章太長;另外,它足夠完整,涵蓋了一個設(shè)計器的大部分內(nèi)容,比如狀態(tài)管理、物料管理、屬性面板、撤銷重做、畫布縮放、皮膚切換、多語言管理、文件的導入導出等。
設(shè)計制作一個項目的時候,最好適當提高自己的要求,從利他的角度思考,比如:能夠方便發(fā)布獨立npm包,方便第三方引用;要考慮,代碼怎么寫,別人容易讀。這樣的要求,能讓你設(shè)計的代碼結(jié)構(gòu)更合理,擴展性更好。時間久了,代碼會越來越優(yōu)雅。本項目也是這個思路下完成的,希望作者代碼能夠越來越好!
項目畫布的css大部分復(fù)制了這個項目:github.com/StavinLi/Wo…
飲水思源,有了這個項目的借鑒,節(jié)省了大量時間,在此對項目作者深表謝意。
本文的代碼取自項目代碼倉庫,但是為了理解的方便,做了少許簡化。
分兩部分理解界面布局,第一部分整體布局,理解了這部分,就知道自己業(yè)務(wù)相關(guān)的組件如何插入編輯器,能夠理解作者這么設(shè)計代碼架構(gòu)是為了提高擴展性,方便第三方引入;第二部分是畫布繪制,該項目以div樹的方式組織審批流節(jié)點,理解了這部分有助于理解后面的數(shù)據(jù)結(jié)構(gòu)。
項目代碼有兩個主要目錄:example 和 workflow-editor。workflow-editor 是編輯器核心,未來要作為獨立的npm package來發(fā)布;example 是演示如何使用workflow-editor來把審批流集成入自己的項目。
上圖把頁面劃分為3個區(qū)域,workflow-editor 包含全部③區(qū)域和②區(qū)域的部分通用組件;example包含全部①區(qū)域的內(nèi)容跟部分②區(qū)域的定制內(nèi)容,并引用③的內(nèi)容。
點擊一個畫布(也就是區(qū)域③)中的節(jié)點,會彈出屬性設(shè)置面板,屬性面板包含④⑤兩部分:
彈出這個面板的抽屜(drawer)和它的標題④,包含在workflow-editor目錄中,它內(nèi)部的組件,就是⑤區(qū)域是在example中定義,通過接口注入進去的。
綜上,編輯器通用的功能在workflow-editor中定義,差異化部分通過接口注入。
畫布區(qū)是通過嵌套的div實現(xiàn)的,連線、箭頭是通過css的border、偽類before跟after實現(xiàn)的,這些css細節(jié)請參看源碼,這里只介紹div的嵌套結(jié)構(gòu)。
像這樣一組不含條件的普通節(jié)點:
它的div結(jié)構(gòu)是這樣的:
在一條直線路徑上的節(jié)點,就這樣層層嵌套,結(jié)束節(jié)點除外,它最后面。
如果加上條件分支,同一級別的條件分支是水平排列的div,分支內(nèi)部的路徑再次循環(huán)嵌套:
只要明白這些節(jié)點是一棵div樹,不是扁平結(jié)構(gòu)就可以了。
UI雖然是樹形結(jié)構(gòu),但是項目內(nèi)部的數(shù)據(jù)結(jié)構(gòu)可以是樹形,也可以是扁平的。
扁平的意思是,所用節(jié)點存在一個數(shù)組或者map里,通過parentId跟childIds等信息描述樹形關(guān)系。
因為這個項目是幫朋友做的,他的后端是樹形結(jié)構(gòu),跟div的結(jié)構(gòu)一致。如果這個項目提供一個編輯器組件WorkflowEditor,這個組件要有value跟onChange屬性,如果是扁平結(jié)構(gòu),onChange的時要轉(zhuǎn)一下,如果做成受控組件,性能可能會有問題。
所以,最后選擇了樹形數(shù)據(jù)結(jié)構(gòu):
export enum NodeType {
//開始節(jié)點
start="start",
//審批人
approver="approver",
//抄送人?
notifier="notifier",
//處理人?
audit="audit",
//路由(條件節(jié)點),下面包含分支節(jié)點
route="route",
//分支節(jié)點
branch="branch",
}
//審批流節(jié)點
export interface IWorkFlowNode<Config=unknown>{
id: string
//名稱
name?: string
//string可以用于自定義節(jié)點,暫時用不上
nodeType: NodeType | string
//描述
desc?: string
//子節(jié)點
childNode?: IWorkFlowNode
//配置
config?: Config
}
//條件根節(jié)點,下面包含各分支節(jié)點
export interface IRouteNode extends IWorkFlowNode {
//分支節(jié)點
conditionNodeList: IBranchNode[]
}
//條件分支的子節(jié)點,分支節(jié)點
export interface IBranchNode extends IWorkFlowNode {
//條件配置部分還沒定義,可能會放入config
}
//審批流,代表一張審批流圖
export interface IWorkflow {
//審批流Id
flowId: string;
//審批流名稱
name?:string;
//開始節(jié)點
childNode: IWorkFlowNode;
}
如果是扁平結(jié)構(gòu),狀態(tài)管理作者會首選Recoil,用起來簡單,代碼量小。但是,因為數(shù)據(jù)結(jié)構(gòu)定義的樹形,要是用Recoil做狀態(tài)管理,需要扁平化處理,會出現(xiàn)上文說的轉(zhuǎn)換問題。所以,最終選擇了Redux作為狀態(tài)管理工具。
作者只會基礎(chǔ)的Redux庫,所以代碼會略顯繁瑣一點,即便這樣,還是不想選mobx。因為這么小的編輯器項目,mobx的撤銷、重做的工作量,要比Redux大。用Mobx的話,一般要采用comand模式做撤銷重做,每個Command有正負操作,挺繁瑣,工作量也大。而immutable的操作方式,可以保留狀態(tài)快照,易于回溯,很容易就能完成撤銷、重做功能。
狀態(tài)定義:
//操作快照,用于撤銷、重做
export interface ISnapshot {
//開始節(jié)點
startNode: IWorkFlowNode,
//是否校驗過
validated?: boolean,
}
//錯誤消息
export interface IErrors {
[nodeId: string]: string | undefined
}
//狀態(tài)
export interface IState {
//是否被修改,該標識用于提示是否需要保存
changeFlag: boolean,
//撤銷快照列表
undoList: ISnapshot[],
//重做快照列表
redoList: ISnapshot[],
//開始節(jié)點
startNode: IWorkFlowNode,
//被選中的節(jié)點,用于彈出屬性面板
selectedId?: string,
//是否校驗過,如果校驗過,后面加入的節(jié)點會自動校驗
validated?: boolean,
//校驗錯誤
errors: IErrors,
}
Redux處理這些樹形結(jié)構(gòu)的狀態(tài),需要遞歸處理,具體參看reducers部分代碼。
引擎(Engine)在作者的項目里是老演員了,這里依然扮演了一個重要角色,全名EditorEngine。編輯器的絕大多數(shù)業(yè)務(wù)邏輯,都在這部分實現(xiàn),主要功能就是操作Redux store。源碼文件在src/workflow-editor/classes目錄下。
物料就是節(jié)點的定義,包括節(jié)點的圖標、顏色、缺省配置等信息。把這些信息獨立出來的好處,是讓代碼更容易擴展,方便后期添加新的節(jié)點類型。作者自己開源低代碼前端RxDrag,也用了類似的設(shè)計方式,不過比這里的擴展性還要好,可以支持物料的熱加載。這個項目比較簡單,沒有熱加載需求,做到這種程度就夠用了。
物料定義代碼:
//國際化翻譯函數(shù),外部注入,這里使用的是@rxdrag/locales的實現(xiàn)(通過react hooks轉(zhuǎn)了一下)
export type Translate=(msg: string)=> string | undefined
//物料上下文
export interface IContext {
//翻譯
t: Translate
}
//節(jié)點物料
export interface INodeMaterial<Context extends IContext=IContext> {
//顏色
color: string
//標題
label: string
//圖標
icon?: React.ReactElement
//默認配置
defaultConfig?: { nodeType: NodeType | string }
//創(chuàng)建一個默認節(jié)點,跟defaultCofig只選一個
createDefault?: (context: Context)=> IWorkFlowNode
//從物料面板隱藏,比如發(fā)起人節(jié)點、條件分支內(nèi)的分支節(jié)點
hidden?: boolean
}
審批流節(jié)點相對比較固定,目前只有四個主要節(jié)點類型。后面有可能會有擴展,但是頻率會非常低。所以物料雖然定義了接口,但是實現(xiàn)基本上還是以預(yù)定義實現(xiàn)為主。預(yù)定義節(jié)點代碼:
export const defaultMaterials: INodeMaterial[]=[
//發(fā)起人節(jié)點
{
//標題,引擎會通過國際化t函數(shù)翻譯
label: "promoter",
//顏色
color: "rgb(87, 106, 149)",
//引擎會直接去defaultConfig來生成一個節(jié)點,會克隆一份defaultConfig數(shù)據(jù)保證immutable
defaultConfig: {
//默認配置,可以把類型上移一層,但是如果增加其它默認屬性的話,不利于擴展
nodeType: NodeType.start,
},
//不在物料板顯示
hidden: true,
},
//審批人節(jié)點
{
color: "#ff943e",
label: "approver",
icon: sealIcon,
defaultConfig: {
nodeType: NodeType.approver,
},
},
//通知人節(jié)點
{
color: "#4ca3fb",
label: "notifier",
icon: notifierIcon,
defaultConfig: {
nodeType: NodeType.notifier,
},
},
{
color: "#fb602d",
label: "dealer",
icon: dealIcon,
defaultConfig: {
nodeType: NodeType.audit,
},
},
//條件節(jié)點
{
color: "#15bc83",
label: "routeNode",
icon: routeIcon,
//條件分支內(nèi)部的分支節(jié)點需要動態(tài)創(chuàng)建ID,所以通過函數(shù)來實現(xiàn)
createDefault: ({ t })=> {
return {
id: createUuid(),
nodeType: NodeType.route,
conditionNodeList: [
{
id: createUuid(),
nodeType: NodeType.branch,
name: t?.("condition") + "1"
},
{
id: createUuid(),
nodeType: NodeType.branch,
name: t?.("condition") + "2"
}
]
}
},
},
//分支節(jié)點
{
label: "condition",
color: "",
defaultConfig: {
nodeType: NodeType.branch,
},
//不在物料板顯示
hidden: true,
},
]
這份配置代碼保存在引擎(EditorEngine)中,渲染畫布跟物料面板會使用這些配置。物料面板是指這里:
就是點擊“添加”按鈕彈出的選擇面板。
跟物料相關(guān)的還有一些內(nèi)容:節(jié)點的內(nèi)容區(qū)①;校驗規(guī)則、校驗后的錯誤消息②;節(jié)點配置面板③。
這些內(nèi)容根據(jù)物料的不同而不同,并且跟具體業(yè)務(wù)強相關(guān)。就是說,不同的項目,這些內(nèi)容是不一樣的。如果要把編輯器跟具體項目集成,那么這部分內(nèi)容就要做成可注入的。
把要注入的內(nèi)容抽出來,獨立定義為物料UI(IMaterialUI),具體代碼:
//物料UI配置
export interface IMaterialUI<FlowNode extends IWorkFlowNode, Config=any, Context extends IContext=IContext> {
//節(jié)點內(nèi)容區(qū)
viewContent?: (node: FlowNode, context: Context)=> React.ReactNode
//屬性面板設(shè)置組件
settersPanel?: React.FC<{ value: Config, onChange: (value: Config)=> void }>
//校驗失敗返回錯誤消息,成功返回ture
validate?: (node: FlowNode, context: Context)=> string | true | undefined
}
//物料UI的一個map,用于組件間通過props傳遞物料UI,key是節(jié)點類型
export interface IMaterialUIs {
[nodeType: string]: IMaterialUI<any> | undefined
}
在example目錄(該目錄放具體項目強相關(guān)內(nèi)容),依據(jù)這個物料UI約定,定義業(yè)務(wù)相關(guān)的ui元素,注入進設(shè)計器。目前的實現(xiàn):
export const materialUis: IMaterialUIs={
//發(fā)起人物料UI
[NodeType.approver]: {
//節(jié)點內(nèi)容區(qū),只實現(xiàn)了空邏輯,具體過幾天實現(xiàn)
viewContent: (node: IWorkFlowNode<IApproverSettings>, { t })=> {
return <ContentPlaceholder secondary text={t("pleaseChooseApprover")} />
},
//屬性面板
settersPanel: ApproverPanel,
//校驗,目前僅實現(xiàn)了空校驗,其它校驗過幾天實現(xiàn)
validate: (node: IWorkFlowNode<IApproverSettings>, { t })=> {
if (!node.config) {
return (t("noSelectedApprover"))
}
return true
}
},
//辦理人節(jié)點
[NodeType.audit]: {
//節(jié)點內(nèi)容區(qū)
viewContent: (node: IWorkFlowNode<IAuditSettings>, { t })=> {
return <ContentPlaceholder secondary text={t("pleaseChooseDealer")} />
},
//屬性面板
settersPanel: AuditPanel,
//校驗函數(shù)
validate: (node: IWorkFlowNode<IApproverSettings>, { t })=> {
if (!node.config) {
return t("noSelectedDealer")
}
return true
}
},
//條件分支節(jié)點的分支子節(jié)點
[NodeType.branch]: {
//節(jié)點內(nèi)容區(qū)
viewContent: (node: IWorkFlowNode<IConditionSettings>, { t })=> {
return <ContentPlaceholder text={t("pleaseSetCondition")} />
},
//屬性面板
settersPanel: ConditionPanel,
//校驗函數(shù)
validate: (node: IWorkFlowNode<IApproverSettings>, { t })=> {
if (!node.config) {
return t("noSetCondition")
}
return true
}
},
//通知人節(jié)點
[NodeType.notifier]: {
viewContent: (node: IWorkFlowNode<INotifierSettings>, { t })=> {
return <ContentPlaceholder text={t("pleaseChooseNotifier")} />
},
settersPanel: NotifierPanel,
},
//發(fā)起人節(jié)點
[NodeType.start]: {
viewContent: (node: IWorkFlowNode<IStartSettings>, { t })=> {
return <ContentPlaceholder text={t("allMember")} />
},
settersPanel: StartPanel,
},
}
這份代碼游離于設(shè)計器之外,要根據(jù)具體項目的業(yè)務(wù)規(guī)則進行修改,這里并沒有完全完成。
多語言使用的是@rxdrag/locales,相關(guān)的react封裝在src/workflow-editor/react-locales目錄下。沒有@rxdrag/react-lacales,因為react版本跟朋友項目的react版本不兼容。
通過鉤子useTranslate拿到t函數(shù),把t函數(shù)注入到引擎供物料定義等場景使用。
項目其他部分的翻譯,直接使用useTranslate實現(xiàn)。多語言資源系統(tǒng)預(yù)定義了一部分,也可以通過編輯器的props傳入locales,補充或覆蓋已有的多語言資源。
引擎訂閱Redux store的數(shù)據(jù)變化,通過一系列鉤子來把這些數(shù)據(jù)變化推送給相應(yīng)的react組件,這些鉤子在目錄src/workflow-editor/hooks下。這些鉤子,相當于是狀態(tài)的監(jiān)聽器。
比如起始節(jié)點的監(jiān)聽,它hook代碼是這樣:
//獲取起始節(jié)點
export function useStartNode() {
const [startNode, setStartNode]=useState<IWorkFlowNode>()
const engine=useEditorEngine()
//引擎起始節(jié)點變化事件處理函數(shù)
const handleStartNodeChange=useCallback((startNode: IWorkFlowNode)=> {
setStartNode(startNode)
}, [])
useEffect(()=> {
//訂閱起始節(jié)點變化事件
const unsub=engine?.subscribeStartNodeChange(handleStartNodeChange)
return unsub
}, [handleStartNodeChange, engine])
//初始化時,先拿到最新數(shù)據(jù)
useEffect(()=> {
setStartNode(engine?.store.getState().startNode)
}, [engine?.store])
return startNode
}
現(xiàn)在redux有很多輔助庫,用上這些輔助庫的話可能不太需要這些鉤子了,作者不是很熟悉這些庫,代碼量也不大,就這么寫了。如果是大一點的項目,優(yōu)先考慮的是Recoil,也就沒有動力再去研究這些輔助庫了。
antd5支持css-in-js了,雖然跟mui相比,在這方面還有不小差距,但是勉強夠用了。主題皮膚的切換,就是基于antd的這個特性。
通過props把antd的theme token傳入設(shè)計器,設(shè)計器根據(jù)這個,使用styled-components庫定義符合相應(yīng)主題的組件。
antd的theme token屬性用不了全部,為了簡化接口,摘了一部分有用的獨立出來,沒有直接使用token的好處是,以后擴展自己的配色方案更方便些。接口定義:
//只是摘取了antd token的一些屬性,后面還可以再根據(jù)需要擴展
export interface IThemeToken {
colorBorder?: string;
colorBorderSecondary?: string;
colorBgContainer?: string;
colorText?: string;
colorTextSecondary?: string;
colorBgBase?: string;
colorPrimary?: string;
}
//styled-components 的typescript使用
export interface IDefaultTheme{
token?: IThemeToken
mode?: 'dark' | 'light'
}
在編輯器最外層加一個styled-components的主題配置:
import { ThemeProvider } from "styled-components";
...
export const FlowEditorScopeInner=memo((props: {
mode?: 'dark' | 'light',
themeToken?: IThemeToken,
children?: React.ReactNode,
materials?: INodeMaterial[],
materialUis?: IMaterialUIs,
})=> {
...
const theme: { token: IThemeToken, mode?: 'dark' | 'light' }=useMemo(()=> {
return {
token: themeToken || token,
mode
}
}, [mode, themeToken, token])
...
return <ThemeProvider theme={theme}>
...
</ThemeProvider>
})
添加typescript的聲明文件styled.d.ts用于IDE的智能提示,文件代碼:
// import original module declarations
import 'styled-components';
import { IDefaultTheme } from './theme';
// and extend them!
declare module 'styled-components' {
export interface DefaultTheme extends IDefaultTheme {
}
}
給IDE(作者用的VSCode)安裝styled-components相關(guān)插件(作者用的是vscode-styled-components)。然后就可以在代碼中使用這些主題信息來定義組件樣式了:
編輯器外部傳入不同theme mode,來切換不同的皮膚主題,具體效果請參考在線演示。
BTW,最近網(wǎng)上在傳閱一篇文章,那個誰誰誰不用css-in-js了,說是影響性能等等。看了后有兩個困惑:
1、什么時候前端的性能變得那么重要了,顯示器有能力展示出這種性能差異嗎?人類真的能識別并感受到這種性能差異嗎?
2、css-in-js如火如荼,使用面也夠逛,如果一點優(yōu)點看不到,不妨問問自己,為什么看不到它的優(yōu)點,是不是觸到了自己的知識盲點?
歡迎明白的大佬留言指點。
整個審批流編輯器獨立在目錄src/workflow-editor中,以后會抽時間把這個目錄發(fā)布為一個單獨的npm package。
編輯器對外提供兩個組件:FlowEditorScope,F(xiàn)lowEditorCanvas。
前者負責接收各種配置資源,比如物料、物料ui、多語言資源、主題定義等,根據(jù)這個些配置生成一個EditorEngine對象,并把這個對象通過context下發(fā)。
理論上,F(xiàn)lowEditorScope內(nèi)的所有子組件,都可以通過EditorEngine來操作編輯器。FlowEditorCanvas是畫布區(qū),流程圖的所有UI,都在這里面。
通常思路,會把這兩個合并為一個FlowEditor組件,外部只引用一次就可以。這樣的話,集成的靈活性會喪失一些。這里保持分開,使用方法請參考expample目錄。
FlowEditorCanvas 通過context拿到資源,所以沒有props,除了className跟style。
FlowEditorScope的定義如下:
export const FlowEditorScope=memo((props: {
//當前主題模式
mode?: 'dark' | 'light',
//主題定義
themeToken?: IThemeToken,
children?: React.ReactNode,
//當前語言
lang?: string,
//多語言資源
locales?: ILocales,
//自定義物料
materials?: INodeMaterial[],
//所有物料的Ui配置,包括自定義物料跟預(yù)定義物料
materialUis?: IMaterialUIs,
})=> {
//實現(xiàn)代碼省略
...
})
以前做導出,直接做一個a標簽,模擬a標簽的點擊觸發(fā)下載動作,導入是用file組件。現(xiàn)在可以使用window.showOpenFilePicker跟window.showSaveFilePicker直接打開、保存文件。文件操作代碼在src/workflow-editor/utils目錄下。
導入導出JSON功能,基于這個通用方法,封裝成兩個鉤子:useImport、useExport。在src/workflow-editor/hooks目錄下,代碼比較簡單,讀者自行翻看吧。
釘釘審批流設(shè)計的挺經(jīng)典,足夠簡潔,能適應(yīng)絕大多數(shù)審批場景。只是有些用戶體驗方面的細節(jié),不是非常完美,這方面作者做了一點優(yōu)化。具體的優(yōu)化點有以下三處:
原版的zoom工具欄是隱形浮動的,在這個位置:
這種隱形工具欄,在畫布滾動時,有時會跟畫布元素重疊,出現(xiàn)這樣的效果:
這種效果用戶也能明白,但是總感覺有種廉價感。
所以,這部分作者做成了浮動工具條,當畫布沒有滾動的時候,跟原版一樣是隱形的,當畫布滾動時,就會浮現(xiàn)出來,元素重疊時變成這樣的效果:
具體運作,請參考在線演示。
原版的畫布滾動,只能通過點擊滾動條實現(xiàn),每次移動畫布都要去找滾動條,用起來十分不便,這個也是作者最在意的地方。希望實現(xiàn)的效果是,鼠標懸浮在畫布空白處,鼠標光標顯示grab(展開的手掌)效果,鼠標按下時顯示未grabbing(抓取的小手)效果,拖動時直接移動畫布。有了這個功能,會極大提高用戶體驗。
在線演示已經(jīng)實現(xiàn)了這個效果。實現(xiàn)代碼在src/workflow-editor/FlowEditor/FlowEditorCanvas.tsx文件中。
一個編輯器,如果有撤銷、重做功能,能夠非常有效的防止用戶誤操作,提高用戶體驗。原版中不存在這個功能,作者決定加上。使用immutable的狀態(tài)管理方式,加這樣的功能非常簡單,增加不了多少工作量。
在畫布左側(cè)跟縮放工具欄對稱的地方,加了一個迷你工具欄:
畫布滾動的時候,這個工具欄同樣會浮現(xiàn)出來:
具體實現(xiàn)方式,請參考源碼。
zoom實現(xiàn)方式是基于transform:scale(x) css樣式實現(xiàn)的,放大畫布時,會出現(xiàn)畫布內(nèi)的元素超出滾動區(qū)域的問題,為了解決這個問題,加了css樣式:transform-origin: 50% 0px 0px ,但是這又出現(xiàn)了一個新問題,就是每次縮放畫布,畫布會閃爍一下,滾回起始點。
這個問題作者很在意,但是由于css樣式不是很熟悉,這個問題一直沒解決,有解決方案的朋友歡迎留言指點,十分感謝。
本文介紹了用React模仿釘釘審批流的大致原理,內(nèi)容偏架構(gòu)方面,細節(jié)介紹不多,畢竟篇幅所限,不明的地方歡迎聯(lián)系作者。
文章對代碼的表達還是有限,很多細節(jié)未能說明白,后期如果有朋友需要的話,可以考慮錄個視頻來講解代碼。
原文鏈接:https://juejin.cn/post/7263858443329191996
段時間有分享過Vue和React系列聊天實例精選,今天要給小伙伴們分享的是如何基于AngularJs全家桶技術(shù)來開發(fā)一個移動端聊天實例。
angular+angular-cli+angular-router+ngrx/store+rxjs等技術(shù)開發(fā)實現(xiàn)的仿微信app聊天室實例項目。實現(xiàn)了下拉刷新、右鍵菜單、發(fā)送消息、表情(動圖),圖片、視頻預(yù)覽,紅包打賞等功能。
https://angular.cn/
https://github.com/angular/angular
<script src="https://lf6-cdn-tos.bytescm.com/obj/cdn-static-resource/tt_player/tt.player.js?v=20160723"></script>
<div class="weChatIM__panel clearfix">
<div class="we__chatIM-wrapper flexbox flex__direction-column">
<!-- 頂部 -->
<header-bar></header-bar>
<!-- 主頁面 -->
<div class="wcim__container flex1">
<router-outlet></router-outlet>
</div>
<!-- 底部 -->
<tab-bar></tab-bar>
</div>
</div>
/*
* angular/router路由配置
*/
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
// 引入路由驗證
import { Auth } from '../views/auth/auth'
// 引入頁面組件
import { NotFoundComponent } from '../components/404'
import { IndexComponent } from '../views/index'
import { GroupChatComponent } from '../views/chat/group-chat'
// ...
export const routes: Routes=[
{
path: '', redirectTo: 'index', pathMatch: 'full',
data: { showHeader: true, showTabBar: true },
},
// 首頁、聯(lián)系人、我
{
path: 'index', component: IndexComponent, canActivate: [Auth],
data: { showHeader: true, showTabBar: true },
},
{
path: 'chat/group-chat', component: GroupChatComponent, canActivate: [Auth]
},
// 404
{
path: '**', component: NotFoundComponent,
},
// ...
];
@NgModule({
// imports: [RouterModule.forRoot(routes)],
imports: [RouterModule.forRoot(routes, { useHash: true })], //開啟hash模式
exports: [RouterModule],
providers: [Auth]
})
export class AppRoutingModule {}
AngularJs中是通過 ngrx/store 來進行狀態(tài)管理。這里不作詳細介紹,想了解用法去官網(wǎng)查閱資料。
https://ngrx.io/
https://github.com/ngrx/platform
export class LoginComponent implements OnInit {
private formField={
tel: '',
pwd: ''
}
handleSubmit(){
let that=this
if(!this.formField.tel){
wcPop({ content: '手機號不能為空!', style: 'background:#eb5a5c;color:#fff;', time: 2 });
}else if(!checkTel(this.formField.tel)){
wcPop({ content: '手機號格式不正確!', style: 'background:#eb5a5c;color:#fff;', time: 2 });
}else if(!this.formField.pwd){
wcPop({ content: '密碼不能為空!', style: 'background:#eb5a5c;color:#fff;', time: 2 });
}else{
this.store.dispatch(new actions.setToken(getToken(64)))
this.store.dispatch(new actions.setUser(this.formField.tel))
wcPop({
content: '登錄成功,跳轉(zhuǎn)中...', style: 'background:#378fe7;color:#fff;', time: 2, shadeClose: false,
end: function () {
that.router.navigate(['/index'])
}
});
}
}
}
「vue+mint-ui仿微信移動端app聊天實例」
?? 最后
如果覺得這篇文章對你有幫助,點個「關(guān)注/轉(zhuǎn)發(fā)」,讓更多的人也能看到你的分享!
著vue3.x越來越穩(wěn)定及vite2.0的快速迭代推出,加上很多大廠相繼推出了vue3的UI組件庫,在2021年必然受到開發(fā)者的再一次熱捧。
Vue3迭代更新頻繁,目前star高達20.2K+。
// 官網(wǎng)地址
https://v3.vuejs.org/
Vitejs目前的star達到15.7K+。
// 官網(wǎng)地址
https://vitejs.dev/
vue3-webchat 基于vue3.x+vuex4+vue-router4+element-plus+v3layer+v3scroll等技術(shù)架構(gòu)的仿微信PC端界面聊天實例。
以上是仿制微信界面聊天效果,同樣也支持QQ皮膚。
大家看到的所有彈窗功能,均是自己開發(fā)的vue3.0自定義彈窗V3Layer組件。
前段時間有過一篇詳細的分享,這里就不作介紹了。感興趣的話可以去看看。
vue3.0系列:Vue3自定義PC端彈窗組件V3Layer
為了使得項目效果一致,所有頁面的滾動條均是采用vue3.0自定義組件實現(xiàn)。
v3scroll 一款輕量級的pc桌面端模擬滾動條組件。支持是否原生滾動條、自動隱藏、滾動條大小/層疊/顏色等功能。
大家感興趣的話,可以去看看這篇分享。
Vue3.0系列:vue3定制美化滾動條組件v3scroll
/**
* Vue3.0項目配置
*/
const path=require('path')
module.exports={
// 基本路徑
// publicPath: '/',
// 輸出文件目錄
// outputDir: 'dist',
// assetsDir: '',
// 環(huán)境配置
devServer: {
// host: 'localhost',
// port: 8080,
// 是否開啟https
https: false,
// 編譯完是否打開網(wǎng)頁
open: false,
// 代理配置
// proxy: {
// '^/api': {
// target: '<url>',
// ws: true,
// changeOrigin: true
// },
// '^/foo': {
// target: '<other_url>'
// }
// }
},
// webpack配置
chainWebpack: config=> {
// 配置路徑別名
config.resolve.alias
.set('@', path.join(__dirname, 'src'))
.set('@assets', path.join(__dirname, 'src/assets'))
.set('@components', path.join(__dirname, 'src/components'))
.set('@layouts', path.join(__dirname, 'src/layouts'))
.set('@views', path.join(__dirname, 'src/views'))
}
}
// 引入餓了么ElementPlus組件庫
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
// 引入vue3彈窗組件v3layer
import V3Layer from '../components/v3layer'
// 引入vue3滾動條組件v3scroll
import V3Scroll from '@components/v3scroll'
// 引入公共組件
import WinBar from '../layouts/winbar.vue'
import SideBar from '../layouts/sidebar'
import Middle from '../layouts/middle'
import Utils from './utils'
const Plugins=app=> {
app.use(ElementPlus)
app.use(V3Layer)
app.use(V3Scroll)
// 注冊公共組件
app.component('WinBar', WinBar)
app.component('SideBar', SideBar)
app.component('Middle', Middle)
app.provide('utils', Utils)
}
export default Plugins
項目中主面板毛玻璃效果(虛化背景)
<!-- //虛化背景(毛玻璃) -->
<div class="vui__bgblur">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="100%" class="blur-svg" viewBox="0 0 1920 875" preserveAspectRatio="none">
<filter id="blur_mkvvpnf"><feGaussianBlur in="SourceGraphic" stdDeviation="50"></feGaussianBlur></filter>
<image :xlink:href="store.state.skin" x="0" y="0" width="100%" height="100%" externalResourcesRequired="true" xmlns:xlink="http://www.w3.org/1999/xlink" style="filter:url(#blur_mkvvpnf)" preserveAspectRatio="none"></image>
</svg>
<div class="blur-cover"></div>
</div>
vue3.0中使用全局路由鉤子攔截登錄狀態(tài)。
router.beforeEach((to, from, next)=> {
const token=store.state.token
// 判斷當前路由地址是否需要登錄權(quán)限
if(to.meta.requireAuth) {
if(token) {
next()
}else {
// 未登錄授權(quán)
V3Layer({
content: '還未登錄授權(quán)!', position: 'top', layerStyle: 'background:#fa5151', time: 2,
onEnd: ()=> {
next({ path: '/login' })
}
})
}
}else {
next()
}
})
如上圖:聊天編輯框部分支持文字+emoj表情、在光標處插入表情、多行文本內(nèi)容。
編輯器抽離了一個公共的Editor.vue組件。
<template>
<div
ref="editorRef"
class="editor"
contentEditable="true"
v-html="editorText"
@click="handleClick"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
style="user-select:text;-webkit-user-select:text;">
</div>
</template>
另外還支持粘貼截圖發(fā)送,通過監(jiān)聽paste事件,判斷是否是圖片類型,從而發(fā)送截圖。
editorRef.value.addEventListener('paste', function(e) {
let cbd=e.clipboardData
let ua=window.navigator.userAgent
if(!(e.clipboardData && e.clipboardData.items)) return
if(cbd.items && cbd.items.length===2 && cbd.items[0].kind==="string" && cbd.items[1].kind==="file" &&
cbd.types && cbd.types.length===2 && cbd.types[0]==="text/plain" && cbd.types[1]==="Files" &&
ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49){
return;
}
for(var i=0; i < cbd.items.length; i++) {
var item=cbd.items[i]
// console.log(item)
// console.log(item.kind)
if(item.kind=='file') {
var blob=item.getAsFile()
if(blob.size===0) return
// 讀取圖片記錄
var reader=new FileReader()
reader.readAsDataURL(blob)
reader.onload=function() {
var _img=this.result
// 返回圖片給父組件
emit('pasteFn', _img)
}
}
}
})
還支持拖拽圖片至聊天區(qū)域進行發(fā)送。
<div class="ntMain__cont" @dragenter="handleDragEnter" @dragover="handleDragOver" @drop="handleDrop">
// ...
</div>
const handleDragEnter=(e)=> {
e.stopPropagation()
e.preventDefault()
}
const handleDragOver=(e)=> {
e.stopPropagation()
e.preventDefault()
}
const handleDrop=(e)=> {
e.stopPropagation()
e.preventDefault()
// console.log(e.dataTransfer)
handleFileList(e.dataTransfer)
}
// 獲取拖拽文件列表
const handleFileList=(filelist)=> {
let files=filelist.files
if(files.length >=2) {
v3layer.message({icon: 'error', content: '暫時支持拖拽一張圖片', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
return false
}
for(let i=0; i < files.length; i++) {
if(files[i].type !='') {
handleFileAdd(files[i])
}else {
v3layer.message({icon: 'error', content: '目前不支持文件夾拖拽功能', shade: true, layerStyle: {background:'#ffefe6',color:'#ff3838'}})
}
}
}
大家如果感興趣可以自己去試試哈。
ok,基于vue3+element-plus開發(fā)仿微信/QQ聊天實戰(zhàn)項目就分享到這里。
基于vue3.0+vant3移動端聊天實戰(zhàn)|vue3聊天模板實例
*請認真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。