在現行的軟體架構中,前端和後端是分離的,即前端只專注於頁面渲染,而後臺專注於業務邏輯,前端和後端是兩個不同的工種,而前後端互動最常見的方式就是通過介面。
前後端分離架構
在正式說明前後臺架構分離之前,我們來看一下多年之前,傳統軟體開發的架構模式。
為什麼要前後端分離
還記得零幾年我上大學的時候,在初學 Java Web 開發時,課本上介紹的還是 JSP + Servlet 這種很傳統的架構模式,這時候前端和後端業務邏輯程式碼都在一個工程裡面,還沒有分離開來,這種開發模式屬於 Model1 模式,雖然實現了邏輯功能和顯示功能的分離,但是由於檢視層和控制層都是由 JSP 頁面實現的,即檢視層和控制層並沒有實現分離。
隨著學習的深入以及漸漸流行的企業應用開發,我們漸漸的擯棄這種技術選型,並開始在專案中使用了若干開源框架,常用的框架組合有 Spring +Struts/Spring MVC + Hibernate/Mybatis 等等,由於框架的優越性以及良好的封裝性使得這套開發框架組合迅速成為各個企業開發中的不二之選,這些框架的出現也減少了開發者的重複編碼工作,簡化開發,加快開發進度,降低維護難度,隨之而火熱的是這套技術框架背後的開發模式,即 MVC 開發模式,它是為了克服 Model1 存在的不足而設計的。
MVC 的具體含義是:Model + View + Controller,即模型+檢視+控制器,
- Model 模型層: 它常常使用 JavaBean 來編寫,它接受檢視層請求的資料,然後進行相應的業務處理並返回最終的處理結果,它負擔的責任最為核心,並利用 JavaBean 具有的特性實現了程式碼的重用和擴充套件以及給維護帶來了方便。
- View 檢視層: 代表和使用者互動的介面,負責資料的採集和展示,通常由 JSP 實現。
- Controller 控制層: 控制層是從使用者端接收請求,然後將請求傳遞給模型層並告訴模型層應該呼叫什麼功能模組來處理該請求,它將協調檢視層和模型層之間的工作,起到中間樞紐的作用,它一般交由 Servlet 來實現。
MVC的工作流程如下圖所示。
同時,專案開發在進行模組分層時也會劃分為三層:控制層,業務層,持久層。控制層負責接收引數,呼叫相關業務層,封裝資料,以及路由並將資料渲染到 JSP 頁面,然後在 JSP 頁面中將後臺的資料展現出來,相信大家對這種開發模式都十分熟悉,不管是企業開發或者是個人專案的搭建,這種開發模式都是大家的首選,不過,隨著開發團隊的擴大和專案架構的不斷演進,這套開發模式漸漸有些力不從心。
接下來,我們來分析下這套開發模式的痛點。
痛點一:JSP 效率問題
首先,JSP 必須要在 Servlet 容器中執行(例如 Tomcat,jetty 等),在請求 JSP 時也需要進行一次編譯過程,最後被譯成 Java 類和 class 檔案,這些都會佔用 PermGen 空間,同時也需要一個新的類載入器載入,JSP 技術與 Java 語言和 Servlet 有強關聯,在解耦上無法與模板引擎或者純 html 頁面相媲美。其次每次請求 JSP 後得到的響應都是 Servlet 通過輸出流輸出的 html 頁面,效率上也沒有直接使用 html 高。由於 JSP 與 Servlet 容器的強關聯,在專案優化時也無法直接使用 Nginx 作為 JSP 的 web 伺服器,效能提升不高。
痛點二:人員分工不明
在這種開發模式下的工作流程通常是:設計人員給出頁面原型設計後,前端工程師只負責將設計圖切成 html 頁面,之後則需要由後端開發工程師來將 html 轉為 JSP 頁面進行邏輯處理和資料展示。在這種工作模式下,人為出錯率較高,後端開發人員任務更重,修改問題時需要雙方協同開發,效率低下,一旦出現問題後,前端開發人員面對的是充滿標籤和表示式的 JSP 頁面,後端人員在面對樣式或者互動的問題時本就造詣不高的前端技術也會捉襟見肘。
在某些緊急情況下也會出現前端人員除錯後端程式碼,後端開發人員除錯前端程式碼這些讓人捧腹的現象,分工不明確,且溝通成本大,一旦某些功能需要返工則需要前後端開發人員,這種情況下,對於前後端人員的後期技術成長也不利,後端追求的是高併發、高可用、高效能、安全、架構優化等,前端追求的是模組化、元件整合、速度流暢、相容性、使用者體驗等等,但是在 MVC 這種開發模式下顯然會對這些技術人員都有一定的掣肘。
痛點三:不利於專案迭代
專案初期,為了快速上線應用,選擇使用這種開發模式來進行 Java Web 專案的開發是非常正確的選擇,此時流量不大,使用者量也不高,並不會有非常苛刻的效能要求,但是隨著專案的不斷成長,使用者量和請求壓力也會不斷擴大,對於網際網路專案的效能要求是越來越高,如果此時的前後端模組依舊耦合在一起是非常不利於後續擴充套件的。舉例說明一下,為了提高負載能力,我們會選擇做叢集來分擔單個應用的壓力,但是模組的耦合會使得效能的優化空間越來越低,因為單個專案會越來越大,不進行合理的拆分無法做到最好的優化,又或者在發版部署上線的時候,明明只改了後端的程式碼,前端也需要重新發布,或者明明只改了部分頁面或者部分樣式,後端程式碼也需要一起釋出上線,這些都是耦合較嚴重時常見的不良現象,因此原始的前後端耦合在一起的架構模式已經逐漸不能滿足專案的演進方向,需要需找一種解耦的方式替代當前的開發模式。
痛點四:不滿足業務需求
隨著公司業務的不斷髮展,僅僅只有瀏覽器端的 Web 應用已經逐漸顯得有些不夠用了,目前又是移動網際網路急劇增長的時代,手機端的原生 App 應用已經非常成熟,隨著 App 軟體的大量普及越來越多的企業也加入到 App 軟體開發當中來,為了儘可能的搶佔商機和提升使用者體驗,你所在的公司可能也不會把所有的開發資源都放在 web 應用上,而是多端應用同時開發,此時公司的業務線可能就是如下的幾種或者其中一部分:
瀏覽器端的 Web 應用、iOS 原生 App、安卓端原生 App、微信小程式等等,可能只是開發其中的一部分產品,但是除了 web 應用能夠使用傳統的 MVC 模式開發外,其他的都無法使用該模式進行開發,像原生 App 或者微信小程式都是通過呼叫 RESTful api 的方式與後端進行資料互動。
隨著網際網路技術的發展,更多的技術框架被提了出來,其中最革命性的就是前後端分離概念的提出。
什麼是前後端分離
何為前後端分離,我認為應該從以下幾個方面來理解。
前後端分離是一種專案開發模式
當業務變得越來越複雜或者產品線越來越多,原有的開發模式已經無法滿足業務需求,當端上的產品越來越多,展現層的變化越來越快、越來越多,此時就應該進行前後端分離分層抽象,簡化資料獲取過程,比如目前比較常用的就是前端人員自行實現跳轉邏輯和頁面互動,後端只負責提供介面資料,二者之間通過呼叫 RESTful api 的方式來進行資料互動,如下圖所示:
此時就不會出現 HTML 程式碼需要轉成 JSP 進行開發的情況,前端專案只負責前端部分,並不會摻雜任何後端程式碼,這樣的話程式碼不再耦合。同時,前端專案與後端專案也不會再出現耦合嚴重的現象,只要前後端協商和定義好介面規範及資料互動規範,雙方就可以並行開發,互不干擾,業務也不會耦合,兩端只通過介面來進行互動。
在 MVC 模式開發專案時,往往後端過重,“控制權”也比較大,既要負責處理業務邏輯、許可權管理等後端操作,也需要處理頁面跳轉等邏輯,在前後端分離的模式中,後端由原來的大包大攬似的獨裁者變成了介面提供者,而前端也不僅僅是原來那樣僅處理小部分業務,頁面跳轉也不再由後端來處理和決定,整個專案的控制權已經由後端過渡至前端來掌控,前端需要處理的更多。
前端專案和後端專案隔離開來、互不干涉,通過介面和資料規範來完成專案功能需求,這也是目前比較流行的一種開發方式。
前後端分離是一種人員分工
在前後端分離的架構模式下,後臺負責資料提供,前端負責顯示互動,在這種開發模式下,前端開發人員和後端開發人員分工明確,職責劃分十分清晰,雙方各司其職,不會存在邊界不清晰的地方,並且從業人員也各司其職。
前端開發人員包括 Web 開發人員、原生 App 開發人員,後端開發則是指 Java 開發人員(以 Java 語言為例),不同的開發人員只需要注重自己所負責的專案即可。後端專注於控制層(RESTful API)、服務層 、資料訪問層,前端專注於前端控制層、 檢視層,不會再出現前端人員需要維護部分後端程式碼,或者後端開發人員需要去除錯樣式等等職責不清和前後端耦合的情況,我們通過兩張專案開發流程簡圖來對比:
此時,開發過程中會存在前後端耦合的情況,如果出現問題前端需要返工、後端也需要返工,開發效率會有所影響。現在,前後端分離後流程簡圖如下:
前後端分離後,伺服器端開發人員和前端開發人員各幹各的,大家互不干擾,。在設計完成後,Web 端開發人員、App 端開發人員、後端開發人員都可以投入到開發工作當中,能夠做到並行開發,前端開發人員與後端開發人員職責分離,即使出現問題,也是修復各自的問題不會互相影響和耦合,開發效率高且滿足企業對於多產品線的開發需求。
前後端分離是一種架構模式
前後端分離後,各端應用可以獨立打包部署,並針對性的對部署方式進行優化,不再是前後端一個統一的工程最終打成一個部署包進行部署。以 Web 應用為例,前端專案部署後,不再依賴於 Servlet 容器,可以使用吞吐量更大的 Nginx 伺服器,採用動靜分離的部署方式,既提升了前端的訪問體驗,也減輕了後端伺服器的壓力,再進一步優化的話,可以使用頁面快取、瀏覽器快取等設定,也可以使用 CDN 等產品提升靜態資源的訪問效率。對於後端服務而言,可以進行叢集部署提升服務的響應效率,也可以進一步的進行服務化的拆分等等。前後端分離後的獨立部署維護以及針對性的優化,可以加快整體響應速度和吞吐量。
前端發展歷程
當我們去了解某個事物的時候,首先我們需要去了解它的歷史,才能更好的把握它的未來。
原始時代
世界上第一款瀏覽器 NCSAMosaic ,是網景公司(Netscape)在1994年開發出來的,它的初衷是為了方便科研人員查閱資料、文件(這個時候的文件大多是圖片形式的)。那個時代的每一個互動,按鈕點選、表單提交,都需要等待瀏覽器響應很長時間,然後重新下載一個新頁面。
同年 PHP(超文字前處理器) 指令碼語言被開發出來,開啟了資料嵌入模板的 MVC 模式,同時期比較類似的做法有以下幾種:
- PHP 直接將資料內嵌到 HTML 中。
- ASP 的 ASPX,在 HTML 中嵌入 C# 程式碼。
- Java 的 JSP 直接將資料嵌入到網頁中。
這個時期,瀏覽器的開發者,以後臺開發人員居多,大部分前後端開發是一體的,大致開發流程是:後端收到瀏覽器的請求 ---> 傳送靜態頁面 ---> 傳送到瀏覽器。即使是有專門的前端開發,也只是用 HTML 寫寫頁面模板、CSS 給頁面排個好看點的版式。在這一時期,前端的作用有限,往往只是切圖仔的角色。
鐵器時代
1995年,網景公司的一位叫布蘭登·艾奇的大佬,希望開發出一個類似 Java 的指令碼語言,用來提升瀏覽器的展示效果,增強動態互動能力。結果大佬喝著啤酒抽著煙,十來天就把這個指令碼語言寫出來了,功能很強大,就是語法一點都不像 Java。這樣就漸漸形成了前端的雛形:HTML 為骨架,CSS 為外貌,JavaScript 為互動。
同時期微軟等一些公司也針對自家瀏覽器開發出了自己的指令碼語言。瀏覽器五花八門,雖然有了比較統一的 ECMA 標準,但是瀏覽器先於標準在市場上流行開來,成為了事實標準。導致,現在前端工程師還要在做一些政府古老專案的時候,還要去處理瀏覽器相容(萬惡的 IE 系列)。
不管怎麼說,前端開發也算是能寫點邏輯程式碼了,不再是隻能畫畫頁面的低端開發了。隨著1998年 AJax 的出現,前端開發從 Web1.0邁向了Web2.0,前端從純內容的靜態展示,發展到了動態網頁,富互動,前端資料處理的新時期。這一時期,比較知名的兩個富互動動態的瀏覽器產品是。
- Gmail(2004年)
- Google 地圖(2005年)
由於動態互動、資料互動的需求增多,還衍生出了jQuery(2006) 這樣優秀的跨瀏覽器的 js 工具庫,主要用於 DOM 操作,資料互動。有些古老的專案,甚至近幾年開發的大型專案現在還在使用 jQuery,以至於 jQuery 庫現在還在更新,雖然體量上已經遠遠不及 React、Vue 這些優秀的前端庫。
資訊時代
自 2003 以後,前端發展渡過了一段比較平穩的時期,各大瀏覽器廠商除了按部就班的更新自己的瀏覽器產品之外,沒有再作妖搞點其他事情。但是我們程式設計師們耐不住寂寞啊,工業化推動了資訊化的快速到來,瀏覽器呈現的資料量越來越大,網頁動態互動的需求越來越多,JavaScript 通過操作 DOM 的弊端和瓶頸越來越明顯(頻繁的互動操作,導致頁面會很卡頓),僅僅從程式碼層面去提升頁面效能,變得越來越難。於是優秀的大佬們又幹了點驚天動地的小事兒:
- 2008 年,谷歌 V8 引擎釋出,終結微軟 IE 時代。
- 2009 年 AngularJS 誕生、Node誕生。
- 2011 年 ReactJS 誕生。
- 2014 年 VueJS 誕生。
其中,V8 和 Node.JS 的出現,使前端開發人員可以用熟悉的語法糖編寫後臺系統,為前端提供了使用同一語言的實現全棧開發的機會(JavaScript不再是一個被嘲笑只能寫寫頁面互動的指令碼語言)。React、Angular、Vue 等 MVVM 前端框架的出現,使前端實現了專案真正的應用化(SPA單頁面應用),不再依賴後臺開發人員處理頁面路由 Controller,實現頁面跳轉的自我管理。同時也推動了前後端的徹底分離(前端專案獨立部署,不再依賴類似的 template 檔案目錄)。
至於為啥 MVVM 框架能提升前端的渲染效能,這裡簡單的說一下原理,因為大量的 DOM 操作是效能瓶頸的罪魁禍首,那通過一定的分析比較演算法,實現同等效果下的最小 DOM 開銷是可行的。React、Vue 這類框架大都是通過這類思想實現的,具體實現可以去看一下相關資料。前後端分離也導致前端的分工發生了一些變化。
而後端開發更加關注資料服務,前端則負責展示和互動。當然相應的學習成本也越來越大,Node.JS的出現也使得前端前後端一起開發成為可能,好多大公司在 2015 年前後就進行了嘗試,用 Node.JS 作為中間資料轉接層,讓後端更加專注於資料服務和治理。
前端模組化發展歷程
自 2009 年 5 月 Node.js 釋出以來,前端能幹的事情越來越多。短短 10 來年的時間,前端便從刀耕火種的年代走向了模組化、工程化的時代。各種前端框架百家爭鳴,前端贏來了真正屬於自己的時代。
原始時代
時間回到 2009年,記得那時候還沒有流行前後端分離,很多專案還是混在一起,而那時候的前端開發人員大多數也都是“切圖仔”。前端完成靜態頁面,由服務端同事完成資料的嵌入,也就是所謂的套頁面操作,每當有類似的功能,都會回到之前的頁面去複製貼上,由於處於不同的頁面,類名需要更換,但是換湯不換藥。
久而久之,重複程式碼越來越多,但凡改動一個小的地方,都需要改動很多程式碼,顯得極不方便,也不利於大規模的進行工程化開發。雖然市面上也慢慢出現了 Angular、 Avalon 等優秀的前端框架,但是考慮到 SEO 和維護人員並不好招,很多公司還是選擇求穩,用套頁面的形式製作網頁,這對前端的工程化、模組化是一個不小的阻礙。
構建工具的出現
不過,隨著 Node 被大力推崇,市面上湧現出大量的構建工具,如 Npm Scripts、Grunt、Gulp、FIS、Webpack、Rollup、Parcel等等。構建工具解放了我們的雙手,幫我們處理一些重複的機械勞動。
舉個簡單的例子:我們用 ES6 寫了一段程式碼,需要在瀏覽器執行。但是由於瀏覽器廠商對瀏覽器的更新非常保守,使得很多 ES6 的程式碼並不能直接在瀏覽器上執行。這個時候我們總不能手動將 ES6 程式碼改成 ES5 的程式碼。於是乎就有了下面的轉換。
//編譯前
[1,2,3].map(item => console.log(item))
//編譯後
[1, 2, 3].map(function (item) {
return console.log(item);
});
//程式碼壓縮後
[1,2,3].map(function(a){return console.log(a)});
就是做了上述的操作,才能使得我們在寫前端程式碼的時候,使用最新的 ECMAScript 語法,並且儘可能的壓縮程式碼的體積,使得瀏覽器載入靜態指令碼時能更加快速。
傳統的模組化
隨著 Ajax 的流行,前端工程師能做的事情就不只是“切圖” 這麼簡單,現在前端工程師能做的越來越多,開始出現了明確的分工,並且能夠與服務端工程師進行資料聯調。這裡說的傳統模組化還不是後現代的模組化,早期的模組化是不借助任何工具的,純屬由 JavaScript 完成程式碼的結構化。在傳統的模組化中我們主要是將一些能夠複用的程式碼抽成公共方法,以便統一維護和管理,比如下面程式碼。
function show(id) {
document.getElementById(id).setAttribute('style', "display: block")
}
function hide(id) {
document.getElementById(id).setAttribute('style', "display: none")
}
然後,我們將這些工具函式封裝到一個 JS 指令碼檔案裡,在需要使用它們的地方進行引入。
<script scr="./utils.js"></script>
但是,這種做法會衍生出兩個很大的問題,一個是全域性變數的汙染,另一個是人工維護模組之間的依賴關係會造成程式碼的混亂。
例如,當我們的專案有十幾個甚至幾十個人維護的時候,難免會有人在公用元件中新增新的方法,比如 show 這個方法一旦被覆蓋了,使用它的人會得到和預期不同的結果,這樣就造成的全域性變數的汙染。另一個問題,因為真實專案中的公用指令碼之間的依賴關係是比較複雜的,比如 c 指令碼依賴 b 指令碼,a 指令碼依賴 b 指令碼,那麼我們在引入的時候就要注意必須要這樣引入。
<script scr="c.js"></script>
<script scr="b.js"></script>
<script scr="a.js"></script>
要這樣引入才能保證 a 指令碼的正常執行,否則就會報錯。對於這類問題,我們該如何解決這樣的問題呢?
全域性變數的汙染
解決這個問題有兩種,先說說治標不治本的方法,我們通過團隊規範開發文件,比如說我有個方法,是在購物車模組中使用的,可以如下書寫。
var shop.cart.utils = {
show: function(id) {
document.getElementById(id).setAttribute('style', "display: block")
},
hide: function(id) {
document.getElementById(id).setAttribute('style', "display: none")
}
}
這樣就能比較有效的避開全域性變數的汙染,把方法寫到物件裡,再通過物件去呼叫。專業術語上這叫名稱空間的規範,但是這樣模組多了變數名會比較累贅,一寫就是一長串,所以我叫它治標不治本。
還有一種比較專業的方法技術通過立即執行函式完成閉包封裝,為了解決封裝內變數的問題,立即執行函式是個很好的辦法,這也是早期很多開發正在使用的方式,如下所示。
(function() {
var Cart = Cart || {};
function show (id) {
document.getElementById(id).setAttribute('style', "display: block")
}
function hide (id) {
document.getElementById(id).setAttribute('style', "display: none")
}
Cart.Util = {
show: show,
hide: hide
}
})();
上述程式碼,通過一個立即執行函式,給予了模組的獨立作用域,同時通過全域性變數配置了我們的模組,達到了模組化的目的。
當前的模組化方案
先來說說 CommonJS 規範,在 Node.JS 釋出之後,CommonJS 模組化規範就被用在了專案開發中,它有幾個概念給大家解釋一下。
- 每個檔案都是一個模組,它都有屬於自己的作用域,內部定義的變數、函式都是私有的,對外是不可見的;
- 每個模組內部的 module 變數代表當前模組,這個變數是一個物件;
- module 的 exports 屬性是對外的介面,載入某個模組其實就是在載入模組的 module.exports 屬性;
- 使用 require 關鍵字載入對應的模組,require 的基本功能就是讀入並執行一個 JavaScript 檔案,然後返回改模組的 exports 物件,如果沒有的話會報錯的;
下面來看一下示例,我們就將上面提到過的程式碼通過 CommonJS 模組化。
module.exports = {
show: function (id) {
document.getElementById(id).setAttribute('style', "display: block")
},
hide: function (id) {
document.getElementById(id).setAttribute('style', "display: none")
}
}
// 也可以輸出單個方法
module.exports.show = function (id) {
document.getElementById(id).setAttribute('style', "display: block")
}
// 引入的方式
var utils = require('./utils')
// 使用它
utils.show("body")
除了 CommonJS 規範外,還有幾個現在只能在老專案裡才能看到的模組化模式,比如以 require.js 為代表的 AMD(Asynchronous Module Definition) 規範 和 玉伯團隊寫的 sea.js 為代表的 CMD(Common Module Definition) 規範。
AMD 的特點:是一步載入模組,但是前提是一開始就要將所有的依賴項載入完全。CMD 的特點是:依賴延遲,在需要的時候才去載入。
AMD
首先,我們來看一下如何通過 AMD 規範的 require.js 書寫上述模組化程式碼。
define(['home'], function(){
function show(id) {
document.getElementById(id).setAttribute('style', "display: block")
}
function hide(id) {
document.getElementById(id).setAttribute('style', "display: none")
}
return {
show: show,
hide: hide
};
});
// 載入模組
require(['utils'], function (cart){
cart.show('body');
});
require.js 定義了一個函式 define,它是全域性變數,用來定義模組,它的語法規範如下:
define(id, dependencies, factory)
- id:它是可選引數,用於標識模組;
- dependencies:當前模組所依賴的模組名稱陣列,如上述模組依賴 home 模組,這就解決了之前說的模組之間依賴關係換亂的問題,通過這個引數可以將前置依賴模組載入進來;
- factory:模組初始化要執行的函式或物件。
require([dependencies], function(){})
然後,在其他檔案中使用 require 進行引入,第一個引數為需要依賴的模組陣列,第二個引數為一個回撥函式,當前面的依賴模組被載入成功之後,回撥函式會被執行,載入進來的模組將會以引數的形式傳入函式內,以便進行其他操作。
CMD
sea.js 和 require.js 解決的問題其實是一樣的,只是執行的機制不同,遵循的是就近依賴,來看下使用CMD方式實現的模組化程式碼。
define(function(require, exports, module) {
function show(id) {
document.getElementById(id).setAttribute('style', "display: block")
}
exports.show = show
});
<script type="text/javascript" src="sea.js"></script>
<script type="text/javascript">
// 引入模組通過seajs.use,然後可以在回撥函式內使用上面模組匯出的方法
seajs.use('./utils.js',function (show) {
show('#box');
});
</script>
首先是引入 sea.js 庫,定義和匯出模組分別是 define() 和 exports,可以在定義模組的時候通過 require 引數手動引入需要依賴的模組,使用模組通過 seajs.use。
ES6
ES6 提出了最新的模組化方案,並且引入了類的機制,讓 JavaScript 從早期的表單驗證指令碼語言搖身一變成了一個物件導向的語言了。ES6 的模組化使用的是 import/export 關鍵字來實現匯入和匯出,並且自動採用的是嚴格模式(use strict),考慮到都是執行在模組之中,所以 ES6 實際上把整個語言都升到了嚴格模式。
在 ES6 中每一個模組即是一個檔案,在檔案中定義變數、函式、物件在外部是無法獲取的。如果想要獲取模組內的內容,就必須使用 export 關鍵字來對其進行暴露。我們把之前的公用指令碼用 ES6 的形式再重構一遍。
// utils.js
const show = () => {
document.getElementById(id).setAttribute('style', 'display: block');
}
const hide = () => {
document.getElementById(id).setAttribute('style', 'display: none');
}
export {
show,
hide
}
// 或者直接丟擲方法
export const show = (id) => {
document.getElementById(id).setAttribute('style', 'display: block');
}
export const hide = (id) => {
document.getElementById(id).setAttribute('style', 'display: none');
}
// 外部引入模組
import { show, hide } from './utils'
可以發現,ES6 的寫法更加清晰。具備了物件導向和麵向函式的特徵,可讀性更強。