李思睿,鄭大翔,李志芳
(海南醫(yī)學(xué)院,海南 ???571199)
847520303@qq.com;2505663420@qq.com;19143212@qq.com
當(dāng)前絕大多數(shù)的醫(yī)院信息系統(tǒng)都采用三層架構(gòu)的開(kāi)發(fā)方式,將整個(gè)應(yīng)用程序劃分為界面層、業(yè)務(wù)邏輯層、數(shù)據(jù)訪問(wèn)層。在界面層,通常結(jié)合DevExpress控件庫(kù)進(jìn)行開(kāi)發(fā),其內(nèi)容雖然豐富,但加載速度較慢,且樣式較為單一。處理業(yè)務(wù)邏輯時(shí),因?yàn)榉謱有枰褂弥虚g層方式(數(shù)據(jù)訪問(wèn)層)訪問(wèn)數(shù)據(jù)庫(kù),需要對(duì)數(shù)據(jù)進(jìn)行各種轉(zhuǎn)換和計(jì)算,降低了系統(tǒng)性能。當(dāng)用戶訪問(wèn)量增大,處于高并發(fā)時(shí),會(huì)導(dǎo)致系統(tǒng)響應(yīng)慢,經(jīng)常出現(xiàn)未響應(yīng)狀態(tài),甚至崩潰。三層架構(gòu)實(shí)際上不只是三層,隨著業(yè)務(wù)復(fù)雜度的提升,少則劃分四五層,多至八九層。層級(jí)之間缺乏統(tǒng)一的標(biāo)準(zhǔn),不同的開(kāi)發(fā)者對(duì)各個(gè)層級(jí)的理解不一致,當(dāng)有新功能需求時(shí),需要開(kāi)發(fā)者在各個(gè)層級(jí)進(jìn)行相應(yīng)開(kāi)發(fā),同時(shí)接口方面也存在銜接不一致的問(wèn)題。
隨著互聯(lián)網(wǎng)技術(shù)的快速發(fā)展,架構(gòu)技術(shù)數(shù)不勝數(shù)。國(guó)內(nèi)外IT公司在業(yè)務(wù)層次追求強(qiáng)大功能的同時(shí),更加注重用戶體驗(yàn)和系統(tǒng)性能。在開(kāi)發(fā)層次,采用團(tuán)隊(duì)協(xié)作方式,利用工程化思想,采取前后端分離的開(kāi)發(fā)模式,前端負(fù)責(zé)對(duì)數(shù)據(jù)的渲染,關(guān)注與用戶的交互;后端處理業(yè)務(wù)邏輯,為前端提供數(shù)據(jù)接口。前端工程化是當(dāng)今主流的項(xiàng)目開(kāi)發(fā)方案。本文通過(guò)對(duì)現(xiàn)有醫(yī)院信息系統(tǒng)的研究,采用解耦的編程思想,設(shè)計(jì)并實(shí)現(xiàn)了藥房藥庫(kù)管理系統(tǒng)。系統(tǒng)以藥品管理規(guī)范化、科學(xué)化為目標(biāo),提高了醫(yī)院藥房藥庫(kù)整體的工作效率,促進(jìn)了醫(yī)院信息化發(fā)展。系統(tǒng)主要包括藥房管理、藥庫(kù)管理、門(mén)診藥房、數(shù)據(jù)統(tǒng)計(jì)、后臺(tái)管理功能模塊。
系統(tǒng)采用前后端分離的架構(gòu)模式,前端主要運(yùn)用HTML、CSS制作靜態(tài)頁(yè)面,結(jié)合JavaScript實(shí)現(xiàn)頁(yè)面動(dòng)態(tài)化并對(duì)數(shù)據(jù)進(jìn)行渲染展示,讓頁(yè)面運(yùn)行更加流暢,追求更高的用戶體驗(yàn);后端處理業(yè)務(wù)邏輯,提供數(shù)據(jù)接口,實(shí)現(xiàn)服務(wù)的高性能和高并發(fā)。前端通過(guò)Axios調(diào)用后端的Api接口,對(duì)后端返回?cái)?shù)據(jù)進(jìn)行處理并渲染。這種開(kāi)發(fā)方式降低了前后端代碼之間的耦合性,架構(gòu)體系清晰,便于代碼的上線部署和后續(xù)維護(hù)。
系統(tǒng)的桌面應(yīng)用程序基于Electron,它是由GitHub開(kāi)發(fā)的一款跨平臺(tái)桌面應(yīng)用開(kāi)發(fā)框架。Electron將Google的Chromium和Node.js合并到一個(gè)運(yùn)行環(huán)境中,所以能使用Node.js中幾乎所有的模塊,通過(guò)JavaScript操作系統(tǒng)原生的Api。Electron有且僅只有一個(gè)主進(jìn)程(Main Process),Main Process通過(guò)BrowserWindow實(shí)例創(chuàng)建頁(yè)面并通過(guò)Chromium展示,每個(gè)獨(dú)立的Web頁(yè)面都運(yùn)行在它自身的渲染進(jìn)程(Render Process)中,能夠完美地使用Vue.js,在各個(gè)渲染進(jìn)程中獨(dú)立開(kāi)發(fā)Web頁(yè)面。Electron原理如圖1所示。
圖1 Electron原理圖示Fig.1 Diagram of Electron principle
在傳統(tǒng)開(kāi)發(fā)中,頁(yè)面都是靜態(tài)渲染的,頁(yè)面的更新必須要直接操作DOM,大量DOM操作不僅消耗性能且十分煩瑣。Vue.js通過(guò)MVVM模式實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)視圖,實(shí)際上是從數(shù)據(jù)劫持到發(fā)布訂閱的一種模式。通過(guò)Object.defineProperty這個(gè)Api在對(duì)象上定義一個(gè)新屬性或者修改現(xiàn)有的屬性,使用該Api下的get方法讀取屬性獲取data,使用set方法寫(xiě)入屬性監(jiān)聽(tīng)data的變化。組件中data一旦發(fā)生變化,就會(huì)根據(jù)修改后的data重新渲染視圖,由MVVM去自動(dòng)更新DOM,開(kāi)發(fā)者就無(wú)須直接操作DOM,節(jié)約了開(kāi)發(fā)成本,提高了程序的性能。
雖然MVVM能幫助開(kāi)發(fā)者自動(dòng)更新視圖,減少了對(duì)DOM的直接操作,但實(shí)際上也是要對(duì)DOM進(jìn)行操作的,依然非常耗時(shí)。但Virtual Dom先用執(zhí)行速度更快的JS來(lái)模擬DOM結(jié)構(gòu),在多次的DOM操作中計(jì)算出最小的變更,避免了一些毫無(wú)意義的操作,最后操作DOM,其本質(zhì)上是DOM和JS之間的一個(gè)緩存。
Virtual Dom通過(guò)diff算法比較DOM操作前后的差別,計(jì)算出最小變更。對(duì)于操作前后的DOM樹(shù),diff算法只對(duì)它們之間同級(jí)比較,若兩棵樹(shù)的tag不相同,刪除重建,不再進(jìn)行下一層級(jí)的對(duì)比。若一棵樹(shù)的tag和key都相同,則認(rèn)為是相同的節(jié)點(diǎn),同樣不再進(jìn)行下一層級(jí)的對(duì)比。此算法的時(shí)間復(fù)雜度為O(n),執(zhí)行算法時(shí)的工作量是十分理想的。
在修改model中的data時(shí),重新編譯渲染。在編譯過(guò)程中會(huì)將template模板轉(zhuǎn)變成一個(gè)render函數(shù),通過(guò)render函數(shù)生成Virtual Dom,調(diào)用vm._render方法生成一個(gè)新的虛擬節(jié)點(diǎn)(Vnode)。對(duì)原來(lái)的Vnode和新生成的Vnode進(jìn)行diff算法計(jì)算出最小變更。這個(gè)過(guò)程中會(huì)實(shí)例化一個(gè)Watcher,Watcher是Dep實(shí)例中的一個(gè)對(duì)象,Watcher會(huì)調(diào)用Dep下的Notify方法遍歷Dep的Watcher實(shí)例數(shù)組,通過(guò)對(duì)應(yīng)的update方法來(lái)更新視圖。Vue.js使用了Virtual Dom,避免了回流和重繪的DOM操作,提高了性能。Virtual Dom流程相關(guān)的模板渲染過(guò)程如圖2所示。
圖2 模板渲染過(guò)程Fig.2 Template rendering process
組件化設(shè)計(jì)的優(yōu)勢(shì)如下:(1)組件標(biāo)準(zhǔn)化。每個(gè)組件都有統(tǒng)一的標(biāo)準(zhǔn),有了標(biāo)準(zhǔn)和規(guī)范,各個(gè)組件才能更好地結(jié)合在一起使用。(2)功能分治。將系統(tǒng)的各個(gè)功能都封裝到不同的組件中,目的是讓每個(gè)組件可以獨(dú)立進(jìn)行開(kāi)發(fā),達(dá)到解耦。(3)組件復(fù)用。當(dāng)系統(tǒng)中因?yàn)槟硞€(gè)功能的改變導(dǎo)致組件不可以使用時(shí),對(duì)組件進(jìn)行回收,更換一個(gè)新的組件,或者對(duì)原先組件進(jìn)行優(yōu)化。(4)組件組合。組件之間通過(guò)組合的方式,構(gòu)成一個(gè)功能或者一類(lèi)功能模塊。組件化設(shè)計(jì)最終的目的是達(dá)到高效、協(xié)作以及復(fù)用。
模塊是指在系統(tǒng)開(kāi)發(fā)中,用自執(zhí)行函數(shù)將多個(gè)函數(shù)包裹起來(lái)形成閉包,自執(zhí)行函數(shù)通過(guò)return將內(nèi)部函數(shù)暴露,這種方式也稱(chēng)單例設(shè)計(jì)模式。模塊化就是把一個(gè)或多個(gè)函數(shù)封裝到一個(gè)JS中,各個(gè)JS之間互不影響,提供方通過(guò)export的方式進(jìn)行暴露,使用方通過(guò)import的方式進(jìn)行導(dǎo)入。模塊化最終的目的是解決開(kāi)發(fā)中的命名沖突,避免因各個(gè)JS文件中存在同名變量導(dǎo)致的變量同名覆蓋問(wèn)題;同時(shí)解決了各個(gè)JS之間的依賴(lài)問(wèn)題,并達(dá)到代碼的復(fù)用,提高函數(shù)的可維護(hù)性,從而提高整體項(xiàng)目的開(kāi)發(fā)效率,降低維護(hù)成本。
一般情況下,組件只需要操作自己的私有數(shù)據(jù)就能夠滿足某一功能的需求,但有些功能比較復(fù)雜,需要多個(gè)組件共同完成。父組件可以通過(guò)屬性傳遞的方式向子組件傳遞狀態(tài)(數(shù)據(jù)),子組件使用props接收父組件傳遞的數(shù)據(jù),通過(guò)this.$emit調(diào)用父組件的事件。父組件通過(guò)event.$on綁定自定義事件,子組件使用event.$emit調(diào)用自定義事件。但是,在組件之間不是父子或兄弟這種簡(jiǎn)單關(guān)系,而是相隔多層的祖孫關(guān)系或者沒(méi)有任何關(guān)系,甚至多個(gè)組件之間共用一個(gè)狀態(tài)的情況下,使用上述組件通信方式就顯得特別笨拙。
采用Vuex對(duì)組件的全局狀態(tài)進(jìn)行統(tǒng)一管理,實(shí)現(xiàn)了組件之間的數(shù)據(jù)共享。第一步,組件改變數(shù)據(jù)后通過(guò)執(zhí)行$store.dispatch()方法觸發(fā)action。第二步,在action處理異步或同步操作后執(zhí)行$store.commit()方法觸發(fā)mutation。第三步,在mutation中處理同步操作后更新?tīng)顟B(tài)(數(shù)據(jù)),存儲(chǔ)在state中。組件可以通過(guò)$store.getters()方法獲取state中的全局對(duì)象,也可以通過(guò)import將mapGetters輔助函數(shù)導(dǎo)入并映射到組件的計(jì)算屬性(computed)中,在computed中使用擴(kuò)展運(yùn)算符...mapGetters將獲取的全局對(duì)象解構(gòu)。
Vuex可以集中管理組件間共享的狀態(tài)(數(shù)據(jù)),充分利用Vue.js的細(xì)粒度數(shù)據(jù)響應(yīng)機(jī)制來(lái)進(jìn)行高效的狀態(tài)管理。存儲(chǔ)的數(shù)據(jù)是響應(yīng)式的,能夠保證在實(shí)時(shí)存儲(chǔ)、視圖更新的操作后,存儲(chǔ)數(shù)據(jù)與頁(yè)面數(shù)據(jù)保持同步。各步操作采用模塊化的方法,分工明確,提高了開(kāi)發(fā)效率,易于后期維護(hù)。Vuex組件狀態(tài)管理如圖3所示。
圖3 Vuex組件狀態(tài)管理Fig.3 Vuex component status management
為避免用戶密碼信息泄露,危害系統(tǒng)安全,系統(tǒng)采用md5加密算法。md5是哈希算法中的一種信息摘要算法,不同長(zhǎng)度的用戶密碼都會(huì)被加密成固定長(zhǎng)度的32 位字符,并存儲(chǔ)在數(shù)據(jù)庫(kù)中。md5加密具有雪崩效應(yīng),明文的一丁點(diǎn)修改,都會(huì)造成密文的大幅變化,且密文到明文具有不可逆性。
但如果用戶密碼比較簡(jiǎn)單,通過(guò)彩虹表仍可能被快速破解,故將明文密碼拆解為三部分,結(jié)合密鑰在特定部分加Salt,再進(jìn)行多次md5加密。Salt具有動(dòng)態(tài)性,用戶名具有唯一性,可將用戶名作為Salt(如明文拆解1+username+明文拆解2+密鑰+明文拆解3)。這樣處理前后的密文截然不同,即使Salt和密鑰泄露,彩虹表也無(wú)法使用,只能根據(jù)Salt和密鑰對(duì)彩虹表進(jìn)行重新生成,且處理后的密碼具有超長(zhǎng)位數(shù)和復(fù)雜度。在原始密碼只是純數(shù)字的情況下,分布列范圍至少為10的20次方;如果是數(shù)字加字母的密碼,其分布列范圍則達(dá)到了62的20次方,幾乎不可能破解。
普通的接口只需要攜帶指定參數(shù)就可以訪問(wèn)后端服務(wù),多次惡意的訪問(wèn)會(huì)導(dǎo)致服務(wù)器繁忙,甚至崩潰。采用JSON Web Token的認(rèn)證機(jī)制,用戶登錄成功后會(huì)自動(dòng)生成一個(gè)JWT Token,返回并存儲(chǔ)到Cookie中,用戶訪問(wèn)接口時(shí),附帶在請(qǐng)求頭中協(xié)同用戶提交的參數(shù)一起發(fā)送到服務(wù)端,服務(wù)端對(duì)發(fā)來(lái)的Token用密鑰解析,驗(yàn)證請(qǐng)求是否合法以及判斷用戶的身份。
JSON Web Token包括JWT頭部(Header)、JWT主體(Playload)、JWT簽名(Veify Signature)。Header規(guī)定了所采用的加密算法和Token的類(lèi)型,Playload中包含需要傳遞的數(shù)據(jù),Veify Signature對(duì)Playload中的數(shù)據(jù)進(jìn)行運(yùn)算,返回一串字符串,再使用Header中規(guī)定的加密算法進(jìn)行加密,生成加密字符串。
本系統(tǒng)采用用戶名和姓名作為主體數(shù)據(jù),加密算法為HMAC SHA256,Token類(lèi)型為JWT類(lèi)型,自設(shè)密鑰。服務(wù)端接收到用戶發(fā)來(lái)的Token信息,使用密鑰進(jìn)行解密,判斷Token有效期及Token對(duì)應(yīng)的用戶身份,身份驗(yàn)證通過(guò)允許訪問(wèn)接口。JWT驗(yàn)證過(guò)程如圖4所示。
圖4 JWT驗(yàn)證過(guò)程Fig.4 JWT validation process
JWT驗(yàn)證過(guò)程:(1)可以指定Secure來(lái)確保Token只在Https協(xié)議下傳輸,防止傳輸過(guò)程中被竊聽(tīng)。(2)在Cookie中設(shè)置HttpOnly,禁止通過(guò)JS獲取Cookie信息,一定程度上防御了XSS攻擊。(3)服務(wù)端可以檢查用戶的Refer和Origin,在Response中缺少Access-Control-Allow-Origin字段或者原始資源URI不明的情況下,可以拒絕處理該請(qǐng)求,防止惡意請(qǐng)求,一定程度上防御了CSRF攻擊,減輕服務(wù)器的壓力。
(1)設(shè)置授權(quán)機(jī)制。對(duì)授權(quán)碼采用md5加密方式,只有設(shè)置授權(quán)碼的管理員和被授予權(quán)限的用戶才知道真正的授權(quán)碼。
(2)防御跨站腳本攻擊(XSS)。對(duì)輸入文本的特殊字符進(jìn)行轉(zhuǎn)義,例如將“<”變?yōu)椤?lt”,這樣被非法嵌入JavaScript的腳本就不能運(yùn)行。
(3)防御跨偽造攻擊(CSRF)。重要請(qǐng)求接口采用POST請(qǐng)求方式,對(duì)重要操作增加了驗(yàn)證措施。
(4)采用路由守衛(wèi)的導(dǎo)航模式。通過(guò)函數(shù)鉤子beforeEach對(duì)路由進(jìn)行全局前置守衛(wèi),對(duì)登錄后才能訪問(wèn)的頁(yè)面設(shè)置了攔截操作,登錄后服務(wù)端設(shè)置Token信息,只有Token存在時(shí)才能訪問(wèn)。
系統(tǒng)用戶主要是醫(yī)務(wù)人員,群體較為固定。根據(jù)業(yè)務(wù)需求,功能模塊包括藥房管理、藥庫(kù)管理、門(mén)診藥房、數(shù)據(jù)統(tǒng)計(jì)、后臺(tái)管理等。(1)藥房管理實(shí)現(xiàn)了藥品報(bào)損、效期管理、藥品請(qǐng)領(lǐng);(2)藥庫(kù)管理實(shí)現(xiàn)了藥品入庫(kù)、藥品出庫(kù)、藥品盤(pán)點(diǎn)、庫(kù)存查詢(xún)、藥品移庫(kù);(3)門(mén)診藥房如圖5所示,實(shí)現(xiàn)了處方發(fā)藥、狀態(tài)查詢(xún)、退藥處理;(4)數(shù)據(jù)統(tǒng)計(jì)包括訪問(wèn)量、年度藥房藥庫(kù)銷(xiāo)售金額、當(dāng)前庫(kù)存量、分類(lèi)藥品銷(xiāo)量排行、年度藥房報(bào)損藥品金額、年度藥品報(bào)損原因、年度盤(pán)盈盤(pán)虧等統(tǒng)計(jì);(5)后臺(tái)管理包括票據(jù)管理、調(diào)價(jià)管理、用戶管理、權(quán)限管理。
圖5 門(mén)診藥房功能模塊Fig.5 Functional modules of outpatient pharmacy
系統(tǒng)采用可視化面板(GUI)對(duì)代碼進(jìn)行測(cè)試,自動(dòng)生成測(cè)試報(bào)告,在4G環(huán)境下只需1.59 秒就可以加載完成,但ECharts和Element-UI的依賴(lài)體積過(guò)大,在一定程度上影響了系統(tǒng)的性能。同時(shí)在資源模塊中,出現(xiàn)對(duì)打包后的JS文件和部分圖片文件發(fā)出警告的問(wèn)題。針對(duì)這些問(wèn)題我們做了如下優(yōu)化:
(1)在依賴(lài)方面,通過(guò)import方式導(dǎo)入的所有依賴(lài)項(xiàng)都會(huì)被打包合并到一個(gè)文件中,導(dǎo)致文件體積過(guò)大。使用config.set方法創(chuàng)建白名單,白名單中的依賴(lài)項(xiàng)將不會(huì)被合并打包,而在window全局對(duì)象查找使用;在public下的index.html文件中添加以link、script的src方式引用CDN資源的配置項(xiàng),通過(guò)修改Webpack的externals節(jié)點(diǎn)加載外部的CND資源。但完整引入Element-UI依賴(lài)項(xiàng)導(dǎo)致依賴(lài)包體積過(guò)大,采用按需加載的方式將所需的組件從Element-UI中解構(gòu)出來(lái),僅對(duì)使用的組件進(jìn)行打包。
(2)在路由方面,使用了路由懶加載技術(shù)。安裝babel插件,用箭頭函數(shù)的形式,通過(guò)webpackChunkName對(duì)路由進(jìn)行分組,打包到不同的JS中,避免了一次性加載全部路由,只有當(dāng)用戶訪問(wèn)的時(shí)候才進(jìn)行加載。通過(guò)Vue.js的內(nèi)置組件Keep-alive對(duì)路由進(jìn)行緩存,避免了重復(fù)渲染,提高了頁(yè)面的加載效率,增加了用戶體驗(yàn)感。
(3)在圖片方面,將較大的圖片壓縮成base64格式,以減少圖片體積。對(duì)于大量的圖片,可以通過(guò)v-lazy指令對(duì)圖片進(jìn)行懶加載,圖片分多批次加載,用戶訪問(wèn)時(shí)才觸發(fā)下一次加載。
(4)在代碼層面,進(jìn)行組件通信時(shí),通過(guò)event.$on生成自定義事件,框架提供的默認(rèn)事件在組件生命周期結(jié)束時(shí)會(huì)被自動(dòng)回收,而開(kāi)發(fā)者定義的自定義事件會(huì)一直存在。本系統(tǒng)在組件的生命周期beforeDestory函數(shù)中通過(guò)evnent.$off銷(xiāo)毀自定義事件。此外,還要對(duì)全局變量進(jìn)行回收,避免內(nèi)存泄漏。系統(tǒng)分配給應(yīng)用程序的內(nèi)存資源是有限的,當(dāng)應(yīng)用程序向系統(tǒng)申請(qǐng)內(nèi)存時(shí),系統(tǒng)會(huì)在堆內(nèi)存中開(kāi)辟一塊內(nèi)存空間,如果應(yīng)用程序中的全局變量、自定義事件沒(méi)有被回收和銷(xiāo)毀,久而久之,內(nèi)存就會(huì)被占滿,發(fā)生內(nèi)存泄漏的現(xiàn)象,導(dǎo)致軟件卡頓直至崩潰。
(5)對(duì)于首屏加載過(guò)慢的問(wèn)題或頁(yè)面組件過(guò)多難以一次性加載的情況,采用異步組件的方式,只有用戶觸發(fā)了該組件,才會(huì)對(duì)該組件進(jìn)行渲染。對(duì)加載過(guò)的組件同樣進(jìn)行緩存處理,避免二次加載造成資源浪費(fèi)。
本文以藥房藥庫(kù)管理系統(tǒng)為例,運(yùn)用前端工程化思想,采用前后端分離架構(gòu),前端采用了Vue.js,以組件和模塊的方式對(duì)視圖部分進(jìn)行開(kāi)發(fā);后端采用了Node.js,負(fù)責(zé)處理業(yè)務(wù)邏輯,提供數(shù)據(jù)接口,并結(jié)合MVVM模式、組件化、模塊化等解決方案,達(dá)到組件間高效、協(xié)作、復(fù)用的效果,模塊間互不影響,視圖與數(shù)據(jù)分離,細(xì)化了開(kāi)發(fā)者的分工協(xié)作,并從用戶隱私安全、接口安全、系統(tǒng)安全三大角度進(jìn)行了分析。提出組件狀態(tài)的管理機(jī)制以及錯(cuò)誤信息的控制措施,從整體上提升了項(xiàng)目可維護(hù)性和拓展性,同時(shí)提高了開(kāi)發(fā)效率,降低了開(kāi)發(fā)成本。解決了傳統(tǒng)醫(yī)院信息系統(tǒng)分層開(kāi)發(fā)方式的系統(tǒng)性能差、維護(hù)難、開(kāi)發(fā)效率低下、層級(jí)間協(xié)作不一致的問(wèn)題,提高了醫(yī)院的藥品管理水平,促進(jìn)了醫(yī)院的信息化發(fā)展。