GitChat 作者:Meathill
原文:用 Vue 改造 Bootstrap,漸進提升項目框架
關注微信公眾號:「GitChat 技術雜談」 一本正經的講技術
【不要錯過文末彩蛋】
前言
Vue 橫空出世,以迅雷不及掩耳之勢橫掃前端界,儼然有當年 jQuery 之勢。我認為 Vue 成功的關鍵在於三點:
-
學習曲線平緩,有點經驗的前端基本上一天就能看完文檔,然后就可以上手操作。
-
上升空間很大,組件化/路由/Vuex/Ajax,生態完整,架構強壯,用它構建中大型項目也很容易。
-
API 設計優雅,並且和標准很友好。
但是在我看來,很多 Vue UI 組件庫反倒走在一條錯誤的道路上:過分追大求全。比如說,第一個組件多半是 Grid,CSS 能搞定的事情為什么要做成組件?前端本來就是 HTML/CSS/JS 的集合,我們理應把合適的技術用在合適的地方。
當然,如果你是想把一切都塞進 JS 的“JS 至上黨”,這樣做也行。不過,不是每個人都要從頭開始做一個新產品,很多同學正在維護一個老產品,如何把新技術應用到老產品里,讓它老樹開新花,也是本文的主旨。
所以接下來,我會介紹這些內容:
-
前端框架很多,為什么要使用 Vue?
-
一個基於 jQuery + Bootstrap 的后台項目。
-
對其進行有限的改造,讓它能漸進地獲得提升。
-
CSS 的歸 CSS,JS 的歸 JS。
適合的讀者
-
初中級前端,希望學習 Vue 和組件式開發。
-
后端,用過 Bootstrap,想升級改造框架。
名詞及約定
-
ES6 = ES2015
-
MVVM Model-View-ViewModel,一種現代化的 UI 架構體系,非常適合 HTML + CSS 這樣的標記型語言,經實踐證明可以大大提升開發效率。本文中大部分和 Vue 互相指代。
-
響應式 在 CSS 領域,我們可以簡單理解成手機端和桌面端有不同的呈現;在 MVVM 框架(本文上,它指界面根據數據自動刷新顯示。
Vue 實現響應式的基礎是 ES5 中的 Object.defineProperty()
方法,它把普通的屬性讀寫改成 getter/setter,以便注入其它操作。大部分現代化瀏覽器都已經支持這個方法,但一些古老的瀏覽器如 IE8 不支持(現在還在使用 IE8 的人你也很難指望他們會升級),如果你要使用 Vue,請確保你的項目不會跑在這些平台上。
本文使用 Bootstrap 4.0.0-alpha.6,Vue 2.4.0+。
作者介紹
大家好,我叫翟路佳,花名“肉山”,這個名字跟 Dota 沒關系,從高中起伴隨我到現在。
我熱愛編程,喜歡學習,喜歡分享,從業十余年,投入的比較多,學習積累到的也比較多,對前端方方面面都有所了解,希望能與大家分享。
我興趣愛好比較廣泛,尤其喜歡旅游,歡迎大家相互交流。
你可以在這里找到我:
版權許可
本書采用“保持署名—非商用”創意共享4.0許可證。
只要保持原作者署名和非商用,您可以自由地閱讀、分享、修改本書。
反饋
如果您對於文中的內容有任何疑問,請在評論或 Issue 中告訴我。亦可發郵件給我:meathill[at]gmail.com。謝謝。
前端框架發展簡史
對前端框架不感興趣的同學可以跳過這一章,並不會影響后面的理解。
這章主要解決這些問題:
-
為什么要切換到 Vue 上?
-
jQuery 真的過時了么?
-
除了 Vue 以外,我還有其它選擇么?
我之前還做過一次視頻分享 jQuery, Backbone, Vue,也是通過分析 JS 開發的歷史,對比三個時期最具代表性的框架。感興趣的同學可以看一下。
史前文明
有位名人說過:“一個人的命運啊,當然要靠自我奮斗,但是也要考慮到歷史的行程。”這句話放在 Web 技術上,其實也非常正確。
發明 HTML 的目的是為了方便閱讀文獻,所以它身上有很多印刷業的影子。后來大家嫌排版沒有其它工具做得漂亮,於是又發明了 CSS。結果憑借着極高的傳播效率,市面上又缺少競品,急速普及,被大家拿來干各種事情。
在那個撥號上網的洪荒年代,瀏覽器還非常初級,與服務器進行數據交互的唯一方式就是提交表單。用戶填寫完成之后,交給服務器處理,如果內容合規當然好,如果不合規就麻煩了,必須打回來重填。所以很容易想象:當用戶填完100+選項,按下提交按鈕,等待幾十秒甚至幾分鍾之后,反饋回來的信息卻是:“您的用戶名不能包含大寫字母”,他會有多么崩潰多么想殺人。為了提升用戶體驗,網景公司的布蘭登·艾克用了大約10天時間,開發出 JavaScript 的原型,從此,這門注定改變世界的語言就誕生了。
石器時代:jQuery
這個時候瀏覽器還處於非常嚴重的分裂狀態。IE 自恃有 Windows 護身,真是想怎么搞怎么搞。另一邊的網景后來的 Firefox 空有一腔報國熱血,但是怎么都干不過 IE。后來干脆全捐給開源社區,曲線救國,用免費開源的群眾戰爭和微軟打。
這種環境受苦的還是網頁開發者。同一個頁面,經常要寫兩套代碼,然后尋找各式各樣的 Hack 方法,費時費力。於是 jQuery 出現后大家覺得真是“春風十里,不如你”。
jQuery 的口號是:“Write less, do more“,為 Web 開發做出了卓越的貢獻:
-
提供統一的操作接口。你不需要擔心代碼跑在什么瀏覽器上,自有 jQuery 來適配。
-
封裝了很多常用接口。比如增刪樣式,只需要
.addClass(className)
就可以,比原始方法簡單很多。 -
用組合模式減少錯誤。HTML 結構多變,JS 操作 DOM 節點很容易發生錯誤,尤其早期切頁面和寫 JS 的很可能不是同一個人。用 jQuery 你至少不用擔心會報錯。
就這樣,jQuery 憑借着出色的工程設計,俘獲了大量開發者的心,是現在最普及的框架。
鐵器時代:Backbone
jQuery 治下的 DOM 操作實在太簡單了,選擇器 + 修改,還不用擔心報錯。這就好比說,鄰居有個熱心大哥,買了輛車,說你出門盡管找他。然后他還真的來者不拒,笑臉相迎,以至於小區里的人出門都不開車也不打車也不騎車,都坐他的車。
開始總是好的,但隨着環境變化,老的優勢也可能會變成問題。就像一個小區只有一輛車肯定不夠一樣,開發者對 DOM 操作過分依賴,會導致 JS 代碼和 HTML 嚴重耦合,牽一發而動全身,維護成本大幅升高。
另外,經過幾年發展,包括 Flash 等軟件不懈探索,此時的 Web 已經不僅僅要提供能閱讀的圖文信息,越來越多的 RIA(Rich Internet Application,富互聯網應用) 涌現出來,大家都在竭盡所能地把桌面軟件搬上互聯網。這對前端開發提出了更高的要求。
Backbone 給出了自己的答案。它是一個 MVP 框架,充分利用了現代 Web 技術,包括 jQuery。它大大減少了操作 DOM 的需求,轉而教大家使用模板。它以數據為視角來組織代碼,告訴大家:原來 JS 還可以這么寫。
使用 Backbone 編寫的應用在工程性方面提升巨大,維護成本大大降低,后期增刪改功能都變得相對容易。開發中大型軟件也變得可行,開發者感受到技術架構帶來的價值,更加主動的放眼看世界,果然還有更值得我們學習的東西。
蒸汽動力:Knockout,Angular1
MVC 是 Smalltalk 提出的編程模型,實際上,那個時期的概念跟今天相距甚遠:主要輸入設備是鍵盤,鼠標指針在屏幕上移動都需要開發者自己寫,更沒有“元素-點擊事件”這種極其抽象的東西。所以它里面的 Model-View-Controller 不能用現在的概念去套。(感興趣的同學可以去看下擴展閱讀里頭兩篇文章。)
隨着 UI 技術的發展,接下來出現的是 MVP 模型。它里面的 P(Presenter)已經是 UI 控件了,所以和 Web 技術非常接近。Backbone 就是這樣架構的框架。
接下來,UI 技術進一步發展,HTML 這種標記語言如日中天,於是微軟最早提出了 MVVM 的概念,並且將它應用在自家產品中。它的模式是這樣的:
(圖片來源: https://erazerbrecht.wordpress.com/2015/10/13/mvvm-entityframework/)
View 視圖和 ViewModel 雙向數據綁定,View 既是數據展示窗口,也是接受用戶操作的輸入來源。ViewModel 除了負責渲染邏輯以外,還負責全局數據管理,以及和真正的數據源 Model 進行交互。MVVM 非常吸引人,因為它在 MVP 的基礎上又進行了一次抽象,並且提供雙向數據綁定,所以開發維護效率巨高,代碼量可能只有 MVP 的 1/10,但功能一致甚至更強。
最早在 Web 中引入這套模式的是 Knockout。用英語的句式來說:它是如此之早以至於它甚至支持 IE 6 和 Firefox 3.5……於是這也讓它背上了沉重的歷史包袱:
-
它必須顯式的聲明數據綁定,顯式到語法啰哩吧嗦。
-
賦值時必須使用
.get(key)
.set(key, val)
這種語法,甚至a.get('key1').get('key2').get('key3').set('key4', val)
這樣。
另一個嘗試來自 Angular1。它不需要這么復雜的語法,而是采用“臟查詢”的方式,即當某個可能導致頁面重新渲染的操作產生后,檢查所有備案過的變量,如果有改變的,就重新渲染。這種做法的問題就是慢,在運算能力充足的桌面電腦上感覺不明顯,但是在移動端就會很慢。桌面端也會慢,所以它只能追蹤1000個變量。
但是它的開發效率的確很高,在企業級占據霸主地位,幾乎已經是事實標准。
這兩個框架有些生不逢時,因為瀏覽器整體環境的限制,它們選擇了不夠完善的實現方案。但是 Angular 至少證明了這個方向可行,而且效果很好,於是他們注定要被后浪拍在沙灘上。
電子時代:Vue,React,Angular2/4/5
接下來便輪到我們的主角登場了。不過與它一起登場的還有另外兩名選手,都是背景深厚實力不凡的大 Boss,我先介紹它們。
React
React 是 Facebook 研發的框架。它最大的特點是使用虛擬 DOM 作為實際 DOM 的影子,這樣當它判斷是否需要重新渲染時,就不用費力的與 DOM 交互,而只要從 JS 內存中取出虛擬 DOM 做 diff 即可。這樣做可以大大提升檢查效率,提高渲染速度。
但它本質上仍然在執行臟檢查,所以實際效率不太高,或者說,有明顯的天花板;只是面對越來越強的計算能力,這些損失浪費的表現越來越不明顯。但是虛擬 DOM 也帶來另外一個好處:如果重寫它的實現機制,可以在任意場合實現任意類型的渲染,包括移動 App。於是便誕生了 React Native 這個項目,可以直接把基於 React 開發的 Web 項目轉譯成原生應用。
React 背靠 Facebook 這座大山,國內也有阿里支持,社區異常活躍,生態異常豐富。又能編譯出原生應用,這是它最大的優勢。
不過 React 也有缺點,導致我沒有選擇它:
-
要使用 React 必須使用丑陋的 JSX,我認為這是反標准的。
-
它的生態雖然豐富,但是官方並沒有引導,沒有全家桶,各種實現參差不齊。
-
設計上為照顧大規模企業級開發,里面有眾多新概念,學習曲線非常陡峭。
-
夾帶私貨,如今 Apache 基金會已經禁止使用 React。
Angular 2/4/5
Angular 吸取了第一代的教訓,在 Angular 2 里重寫了底層邏輯,也使用虛擬 DOM 來判斷是否需要重新渲染。所以 React 的優勢它基本也有。之后的 v4、v5都是在 v2 基礎上的改進,並沒有特別大的變化。
Angular 的學習曲線就更陡峭了,應該說它從一開始就沒打算走群眾路線,而是直接奔着大而全的企業級開發框架去做的(因為 v1 就是這樣做的並且取得了成功)——相對來說 React 表現的還有點扭捏。
另外 Angular 為了能夠更好的對接大規模團隊開發,選擇 TypeScript 作為開發語言,一方面使得學習成本更高;另一方面也和標准化漸行漸遠。這也是我不推薦它的原因。
Vue
與前面兩個競品不同,Vue 是個人作品,而且在 2013 年才開始開發。但我選擇它推薦它並不是因為什么挑戰大公司霸權的情懷,而是它的確設計得很棒。
-
雙向綁定效率高
問世的晚,歷史包袱就少。Vue 使用 ES5 新增的
Object.defineProperty()
方法,將對象的屬性轉化為getter/setter
,這樣我們習以為常的this.a=1
賦值語句實際上就被改寫成this.set('a', 1)
,而這個操作對開發者來說是完全無感的!這樣我們一方面可以正常寫代碼,另一方面還可以輕松的享受到雙向綁定,並且是高效的沒有多余動作的綁定,任何有點點潔癖或者強迫症的人都會覺得很舒服吧! -
模塊化減緩學習曲線
初入門時,我們只需要學習 Vue 就好。如果只想實現簡單的“數據<=>視圖”映射,區區幾行代碼就足夠了,非常簡單。甚至可以直接拿來替換 jQuery。
之后隨着項目增大,使用加深,可以慢慢的開始使用組件,使用路由,使用全局狀態管理。這一系列進化都在平緩的進行。
-
貼近標准
與大公司喜歡夾帶私貨,搞自有標准不同,Vue 是標准友好的。甚至連 Vue 控件,寫出來都跟普通 HTML 一樣。作為個人開發者,或者小公司開發人員,我不願意介入大公司之間的角逐,我見過妖魔橫行的年代,我希望標准一統天下。
小結
在代表先進生產力的 MVVM 框架里,我最終選了 Vue 作為新的主攻框架。我也把它推薦給大家。
框架入門
這一章會簡單介紹 Bootstrap 和 Vue 的入門知識,如果您已經了解,可以跳到下一章。
Bootstrap 簡介
Bootstrap 是 Twitter 的兩位前端工程師搞出來的前端框架。它包含了大量 UI 組件,可以覆蓋到很多開發場景,大大提升開發效率。
Bootstrap 經過幾輪升級,現在處於 v4-alpha.6 階段,作為一個免費開源產品,原作者太忙,alpha 快兩年了,還沒 beta,不知道什么時候能正式版。不過經過我一段時間的使用,我覺得目前這個版本基本上沒啥大問題,還是推薦大家使用。
作為最流行的前端框架,有很多人基於它做出了很多值得學習的項目,很多都是開源免費的,也一並推薦給大家:
-
CoreUI 基於 Bootstrap 做的后台類模板,免費,有多種框架配置
-
Start Bootstrap 另一套免費模板
Bootstrap 的文檔非常詳細,我就不湊字數了。將來文章發布后若有問題再補充吧。
Vue 入門
Vue 的文檔非常棒,尤其還有中文版,學習起來輕松愉快。
這里我也不再重復官方已有的內容了,大家自己看就好。說一下我理解中,從 jQuery 向 Vue 轉換時需要注意的東西。
MVVM 對數據的抽象
jQuery 里幾乎沒有數據抽象。你面對的就是一個雖然錯綜復雜,但是總能找到聯系的 DOM 樹,只要你有耐心,總能把它改成你想要的樣子。
MVP 就抽象出數據層和視圖層,但是還要我們手動更新視圖;MVVM 比 MVP 的抽象更進一步,只要操作數據。所以我們必須要理解它的抽象,並且習慣它的抽象。
在這個體系里,我們應避免直接操作 DOM,因為一切都是數據的映射。舉個例子,一個新聞列表,在傳統開發模式中,是無數個 DOM 操作的結果;而在 Vue 里,就是通過模板把數據映射成 HTML。對后台類產品而言,這很好理解也很好實現,因為后台可以抽象成用戶與數據的交互,然后還原成數據的展示和修改,繼而直接對應到屏幕上的組件上。
在寫 Vue 應用的時候,我們需要注意,哪些數據是業務數據,即要拿來跟后端數據進行交互的;哪些數據是界面數據,即用來切換頁面狀態,和業務無關。但是基本上,我們不需要直接操作 DOM。
組件復用
組件是 Vue 最值得注意的強大特性。組件化和組件復用將大大提升我們的開發效率。
使用組件主要有兩種方式:
-
注冊全局組件。這種方式很簡單,有點類似 jQuery 插件,我們只要引用組件就好,然后就可以在模板中使用特定的組件標簽。比較適合已有項目,可以在不怎么改動的前提下接入應用 Vue。
-
使用局部組件。這種方式要復雜一些,而且也有幾種不同的實現,如果同時要加載組件模板和組件樣式,可能還要用 webpack + vue-loader。
因為這篇文章就是“漸進式改造項目”,所以根據項目現狀選擇合適的方法很有必要。
ES6 與生態
這個其實不是 jQuery 和 Vue 的差別,只是在眼下這個時間點,ES6 已經實裝到絕大部分瀏覽器里,所以我們無論是看文檔、看教程都會看到大量 ES6 的內容。至於整個前端生態,基於 Node.js 開發的各種工具也已經普及到方方面面,使用 webpack + 各種 loader 已經成了默認功課。
所以,那個用 <script src="/path/to/jquery.js">
引入 jQuery,然后就可以在頁面當中任意使用 jQuery 相關技術的年代,其實已經過去了。
目標改造項目簡介
這個項目,本來有機會成為一個真實的項目,但造化弄人,所以它現在只是一個虛擬的項目。
這個項目是這樣的:
-
一個后台產品,主要面向公司內部員工,提供對公司產品的維護。
-
后台已經工作了5年,現在仍在正常工作,不能下線。
-
后台不斷有新需求,不太可能另起爐灶開發。
-
后台使用 Bootstrap + jQuery 搭建,是一個前后端分離結構,每個子頁面加載完成后,會有一個組件管理器對子頁面進行掃描,初始化組件,並且完成數據加載。
-
公司的角度,對 Vue 即不支持也不反對,只要求必須保證開發效率和工作效率。
-
雖然這個產品很重要,但是公司不會投入更多的人力,從頭到尾都只有原先的幾個人來做。
現在,作為一個有追求有夢想,並且想在跳槽的時候有更多加分的前端開發者,我要對它進行 Vuerify 改造了!
第0步:分析
好的,啰嗦半天半天終於進入實戰了。不過,在動手之前我還要多說兩句。所謂謀定而后動,想清楚再動手,肯定比頭腦一熱上去猛干要好。
一個項目能跑5年,說明其設計大體上還是過硬的。所以一開始最好不要動其根本,先找比較獨立的一塊兒來弄最為合適。
我們看一眼 Bootstrap 的文檔,首先 布局(Layout) 和 內容(Content) 的部分就不需要組件化了,CSS 搞得定的事情就不勞 JS 動手了。
組件(Component)里面,有很多也是純 CSS(比如Badge) 或者可以進行純 CSS 改造的(比如 Navs),這些我們先不管,即使改造也等將來用 CSS 改造或者直接放到大組件里。還有像 Tooltip 這樣,雖然帶着一點 JS,但實際上對 JS 的需求很低,我們也不用管它,只要 jQuery 一日不死,這些舊臣都能盡忠報國。
看來看去,最值得拿來入手的應該是 彈窗(Modal) 和 通知(Alert) 之類和主要業務邏輯關系不大,自身可定制性比較強(JS 比較復雜)的組件。
好的,就拿彈窗來試刀吧!
篇幅所限,我沒法展示對每一個組件的重構/改造,所以本文中的幾個篇章都有特定的目的,主要在於介紹一門或幾個技術。希望大家舉一反三,將它們應用到日常工作中。
第一步:彈窗 Modal
本章的目的在於領大家入門。
彈窗除了用作通知以外,在后台產品當中還經常用來對特定屬性進行修改,如下圖所示:
對於這樣的需求,在之前的產品中我是這么做的,首先,HTML 部分就按照 Bootstrap 的標准套路來寫:
<div class="modal fade" id="sales-editor"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">修改職位</h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <form action="/api/sales/" data-action="/api/sales/{{id}}" method="POST" id="sales-editor-form"> <input type="hidden" name="user_id" value=""> <div class="form-group"> <label for="type">職位</label> <select class="form-control" name="type" id="type"> <option value="1">區域總監</option> <option value="2">商務經理</option> <option value="3">商務助理</option> </select> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-primary" form="sales-editor-form"><i class="fa fa-check"></i> 保存</button> <button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-times"></i> 取消</button> </div> </div> </div> </div>
表格模板是這樣的(只寫單行吧,其它部分意義不大):
<tr> <td>{{name}}</td> <td><button type="button" class="btn btn-primary edit-button" data-id="{{id}}">編輯</button></td> </tr>
這兩部分沒什么好解釋的。接下來 JS 的部分就稍顯啰嗦:
// 通過后端接口取出來所有商務數據 let allSales = fetchAllSales(); // 編輯按鈕的點擊事件,每次點擊,除了打開新窗口,還要把表單里面的幾個值修改一下 $('#table').on('click', '.edit-button', event => { let id = $(this).data('id'); let sales = allSales[id]; $('#sales-editor').modal('show') .find('[name=user_id]').val(id) .end().find('form').attr(function () { return $(this).data('action').replace('{{id}}', id); }) .end().find('select').val(sales.type); });
很明顯,正如前文所寫的那樣,jQuery 用起來很方便,可以在 DOM 節點之間自由穿梭。但是因為它沒有抽象,所以任何操作都要手工編寫,會造成 JS 和 HTML 深度耦合,不僅不方便維護,新功能開發的時候也要寫很多多余的代碼。
接下來我們要用 Vue 實現同樣地功能,應該怎么寫呢?
首先我們可以觀察一下 Bootstrap 里 Modal 的實現機制,很明顯,它是通過修改 .modal
元素的 display
屬性和增刪 show
樣式來實現的。那很簡單,我們知道,Vue 里,可以通過 :prop="someValue"
把 vm 的值 someValue
綁定到 DOM 節點的 prop
屬性上,所以控制彈窗的打開關閉就很簡單:
<div class="modal fade" :class="isShow ? 'show' : ''" :style="isShow ? 'display:block' : ''" id="sales-editor" @click.self="close"> <!-- 內容 --> </div> <div class="modal-backdrop fade" :class="isShow ? 'show' : ''" v-show="isShow"></div>
let app = new Vue({ el: '#app', data: { isShow: false }, methods: { show() { this.isShow = true; }, close() { this.isShow = false; } } });
(這里只列出重點代碼,其它因為不影響大局就沒有寫。具體可以參考 codepen oegbvK)
這樣修改了之后,我發現打開關閉已經正常工作了,但是沒有動畫,看起來不夠炫酷。沒關系,我們審查元素,發現是 .show
樣式增加的過渡效果,opacity 0 <-> 1,而同時設置 display
屬性會使得它缺失過渡效果。那很簡單,我們可以用 watch
觀察選項,偵聽 isShow
的變化,在它變化后,間隔一點點時間,再觸發動畫。關閉的動畫就要偵聽 transitionend
事件了,在關閉動畫完成后再隱藏這部分元素。修改后的代碼是這樣的:
<div class="modal fade" :class="isShowClass ? 'show' : ''" :style="isShow ? 'display:block' : ''" id="sales-editor" @click.self="close" @transitionend="hide"> <!-- 內容 --> </div> <div class="modal-backdrop fade" :class="isShowClass ? 'show' : ''" v-show="isShow"></div>
let app = new Vue({ el: '#app', data: { isShow: false, isShowClass: false }, methods: { show() { this.isShow = true; }, hide() { this.isShow = false; }, close() { this.isShowClass = false; } }, watch: { isShow(value) { setTimeout(() => { this.nextShow = value; }, 50); } } });
填入表單
進展的還算順利,我們現在已經可以正常開關彈窗了。
接下來我要把表單中的其它屬性填進去。對於 Vue 來說,這就太簡單了,因為 MVVM 框架最擅長的就是把數據和 UI 進行雙向綁定,只用非常少的代碼。
<form :action="'/api/sales/' + id" method="POST" id="sales-editor-form"> <input type="hidden" name="user_id" :value="id"> <div class="form-group"> <label for="type">職位</label> <select class="form-control" name="type" id="type" v-modal="type"> <option value="1">區域總監</option> <option value="2">商務經理</option> <option value="3">商務助理</option> </select> </div> </form>
三兩下清潔溜溜。接下來改一下編輯按鈕的響應事件:
let modal = new Vue(....); // 把 Vue 組件弄出來備好 $('#table').on('click', '.edit-button', event => { let id = $(this).data('id'); let sales = allSales[id]; modal.id = id; modal.type = sales.type; modal.isShow = true; });
代碼量一下少了好多,真是舒爽!
不過還沒完,因為還要把數據提交給服務器,並且適當的顯示結果,所以我們繼續工作。這次要偵聽表單的提交事件,把它攔截下來,將數據用 Ajax 的方式提交,並且顯示結果。
<form @submit.prevent="submit"> <div class="alert" :class="result-status" v-if="result !== null">{{result}}</div>
// Vue Modal 組件 data: { result: null, message: '' } methods: { submit(event) { $.ajax(event.target.action, { dataType: 'json', data: { type: this.type } }) .then( response => { this.result = response.code; this.message = response.message; this.$emit('saved', this.id, this.type); }) .catch( err => { this.result = 500; this.message = err.message; }); } } // 外面的老列表代碼 let allSales = fetchAllSales(); let modal = new Vue(....); modal.on('saved', (id, type) => { allSales[id].type = type; });
在這段代碼中,我偵聽到 submit
事件,然后把用戶選擇的數據通過 Ajax 上傳給服務器。當服務器返回時,再根據返回值,直接在窗口中顯示成功或失敗提示。接下來,將完成操作的消息廣播出去。外界接到消息后,可以進行下一步的處理。
這里大家可以看到我在偵聽事件的 @submit
后面增加了 .prevent
,如果你留心的話,上一節還有用到 @click.self
,這可不是 jQuery 的事件命名空間,而是 Vue 的事件修飾符。這是一種語法糖,因為我們經常需要在事件處理函數里進行 .preventDefault()
之類的操作,Vue 干脆把它們進行封裝,並以顯式聲明的方式提供給我們。
另外,我在 Ajax 請求當中使用了 Promise 的寫法,如果你對 JS 的異步開發還不甚了解,推薦你看我的另一篇文章:JavaScript 開發全攻略
小結
初戰告捷!
在這一節中,我們對一段老的,依賴 jQuery 和 Bootstrap 的代碼進行了重構,使得它現在支持 Vue,並以 Vue 的方式打開關閉以及與服務器交互。
可能會有同學問:這樣做,是不是太簡單了……
沒錯,千里之行始於足下。如果我們從頭開發一個項目,大可以直接搬來一套 UI 框架,比如 Element.ui,然后全都按照他的標准來寫。但當我們面對着一個已經上線提供服務的產品,就不得不謹慎行事,通過一點一滴的積累,把它逐步往我們希望的目標改造。
不過大家可以放心:
-
這個改造只是入門,我們不會就此收手。
-
“MVVM 最先進 UI 架構”的優勢還會保持很長時間,甚至在新的交互方式(比如語言,讀心)普及之前不會被取代,我們大可以把這個改造過程拉長到3~6個月,甚至一年,而不用擔心半路又要改到其它方向。
-
當我們學會並施行組件化開發,開發效率會大增。
第二步:用戶登錄
本章的目的在於教授大家 Vuex 的用法,以及逐步用 Vue 接管全局。
一段時間過去,我用這種看起來很土的方式改造了一部分代碼,現在我希望更進一步,正好最近幾個需求都處在驗收階段,我有一些時間可以做想做的事。
那就改造用戶系統吧!
之所以選擇這個方向,是考慮到用戶登錄與用戶信息管理是另一個全局功能,和具體業務無關,但又在程序入口,通過對它的改造,我們有機會去觸動更基礎的部分,對將來徹底轉向 Vue 有很大幫助。
說干就干,我翻查老代碼,發現現在的處理方式,是在頁面初始化的時候,向服務器發起一個請求,驗證用戶身份。
-
如果已經登錄,獲取用戶的信息,比如名字,可以看到的導航菜單等。
-
如果沒有登錄,則跳到登錄頁。
這個過程其實還有點復雜,復雜就復雜在需要根據用戶身份更新導航菜單。不過也正和我意,總得從外圍向內核滲透么不是。
首先,修改整個 index.html,把左側導航列表改成 Vue 模板:
<ul id="navbar-side-inner" class="nav side-nav"> <li class="nav-item"> <a href="#/dashboard"><i class="fa fa-dashboard"></i><span>首頁</span></a> </li> <li class="sidebar-nav-item" :class="invisible ? '' : 'hidden'" :id="'parent-' + item.link + item.subId" class="nav-item" v-for="item in sidebar"> <template v-if="item.sub"> <a href="javascript:void(0);" data-parent="#navbar-side-inner" data-toggle="collapse" class="accordion-toggle" :data-target="'#' + item.sub-id" v-if="item.sub"> <i class="fa" :class="'fa-' + item.icon" v-if="item.icon"></i> <span>{{item.title}}</span> <span class="caret" v-if="item.sub"></span> </a> <ul class="nav collapse" :id="item.subId"> <li :id="item.link" :class="invisible ? '' : 'hidden'" v-for="item in sub"> <a :href="item.link"> <i class="fa" :class="'fa-' + item.icon" v-if="item.icon"></i> <span>{{item.title}}</span> <button type="button" class="eye-edit-button" v-if="editing"><i class="fa fa-eye"></i></button> </a> </li> {{/each}} </ul> </template> <a :href="item.link" v-else> <i class="fa" :class="'fa-' + item.icon" v-if="item.icon"></i> <span>{{item.title}}</span> <button type="button" class="eye-edit-button" v-if="editing" v-if="editing"><i class="fa fa-eye"></i></button> </a> </li> </ul>
如果你仔細看上面這段的代碼,你會發現,它最多支持二級菜單,並且一二級菜單的 HTML 結構是完全一致的,這里為接下來組件一章埋下了伏筆。不過這一章我們姑且放過它。
然后把右上角的用戶身份部分也改成 Vue 模板:
<li class="nav-item dropdown me"> <a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> <span class="username">{{username}}</span> <span class="caret"></span> </a> <div class="dropdown-menu" role="menu"> <a href="#/my/profile/" class="dropdown-item"><i class="fa fa-user fa-fw"></i> 我的賬戶</a></li> <a href="#/my/finance/" class="dropdown-item"><i class="fa fa-yen fa-fw"></i> 財務管理</a> <a href="page/my/changepwd.html" class="popup" title="修改密碼" data-confirm="保存" data-cancel="取消"><i class="fa fa-fw fa-lock"></i> 修改密碼</a> <a href="#/my/settings"><i class="fa fa-cog fa-fw"></i> 設置</a> <div class="divider"></div> <a href="#/user/logout"><i class="fa fa-fw fa-sign-out"></i> 退出</a> </ul> </li>
模板告一段落。無論使用何種框架,保持對與 DOM 的解耦是很重要的。做的好的話,這個時候就能提現出效果了,此時我們直接用一個大的 Vue 實例去控制整個后台 UI,與原先的框架進行雙軌制管理。
// 原先的框架 var me = new tp.model.DIYUser() var profile = new tp.view.Me({ el: '.me', model: me }); var body = new tp.view.Body({ el: 'body', model: me }); var sidebarEditor = new tp.view.SidebarEditor({ el: '#navbar-side', model: me }); var ranger = new tp.component.DateRanger({ el: '.date-range' }); var search = new tp.view.Search({ el: '.global-search' }); // 新的框架 let app = new App({ el: 'body' });
因為用戶有關的部分都准備交給 Vue 處理,所以上面的 profile
和 sidebarEditor
都要干掉。還好里面都是處理界面邏輯的代碼,干掉它們沒什么大礙,最后就變成:
// 原先的框架 var body = new tp.view.Body({ el: 'body', model: me }); var ranger = new tp.component.DateRanger({ el: '.date-range' }); var search = new tp.view.Search({ el: '.global-search' }); // 新的框架 let app = new App({ el: 'body', data: { sidebar: null, me: null } });
使用 Vuex 保存用戶狀態
還記得我前面對比 React 時說過的話么?React 生態雖然豐富,但缺少官方指定庫,所以質量參差不齊。Vue 在這方面做的比較好,官方有幾個推薦產品組成全家桶。
雖然現在的改造東一榔頭西一棒槌,但放長遠來看,這套系統一定會整體遷移到 Vue,並且實現組件化。所以,為了能在組件間共享狀態,我決定從現在開始就使用 Vuex。
Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
如果你想了解 Vuex,建議去看官方文檔,非常短小精悍,不超過1個小時你就能看完。
上一章里,我為老框架建立了一個接班人,准備逐步把工作交給它。接下來,我就會用 Vuex 構建一個全局數據中心,方便將來在系統內部存取數據。這次為了擴展考慮,我使用了 Vuex 的模塊概念,即把不同的數據放在不同的模塊里,方便管理。另外,從現在起,我會肆無忌憚地使用新技術,使用 Vue 開發的功能會通過 Webpack 打包到 JS 里(不包含原先的內容),加載到頁面中。
// index.js import Vue from 'vue'; import Vuex from 'vuex'; import { API } from 'config'; import user from './user/'; import mutations from './mutations'; import actions from './actions'; Vue.use(Vuex); export default { state: { API: API, _token: '', }, mutations, modules: { user } }; // user.js import mutations from './mutations'; export default { state: { id: 0, name: '', fullname: '', email: '', sidebar: null }, mutations }
然后回到入口 JS,在里面添加用戶身份檢查:
import base from './store/'; let store = new Vuex.Store(base); let app = new Vue({ el: 'body', store }); let headers = new Headers(); headers.append('Accept', 'application/json'); fetch(API + 'login', { mode: 'cors', headers: headers, credentials: 'include' }) .then( response => { return [response.status, response.json()]; }) .then( (status, json) => { if (status === 401) { router.push('/login'); return; } store.commit(UserMutations.SET_USER_INFO, json.user); }) .catch( err => { router.push('/error/500'); });
這里我用了 Fetch API,從服務器請求用戶身份。如果服務器返回 401 錯誤,就說明用戶沒有登錄,然后就通過路由跳轉到登錄界面;否則的話,把用戶信息記入 store
完成操作。
全局登錄狀態檢查
用戶登錄狀態也是后台產品比較關心的內容。因為后台有很嚴格的權限控制,不同權限的人看到的內容是不同的,甚至連菜單都是不同的。用戶登錄狀態都保存在服務器,前端是不知道的,所以,很可能用戶登錄已經超時了,但是前端框架仍然傻乎乎的向后台發起請求。這個時候,后台必須能從服務器端返回的信息里看到端倪,並做出正確的處理,比如跳轉到登錄頁。
以前用 jQuery,我們可以用全局的響應函數 .ajaxError()
來處理。如今我們正在做去 jQuery 化,所以這個工作也一樣移交給 Vue 一族來處理。
Vue 生態里有個遠程資源組件,叫做 Vue Resouce,可以用來取代 jQuery.ajax。於是我把它也加入項目中,並且利用它的中間件來做全局登錄狀態檢查:
Vue.use(VueResource);
Vue.http.interceptors.push( (request, next) => {
// 所有的頭 request.headers.set('Accept', 'application/json'); request.credentials = true; next( response => { if (response.status === 401 || (!response.ok && response.status === 0)) { router.push({ name: 'Login', params: { fetch: true, redirectTo: location.hash.substr(1) } }); } }); });
不過需要注意的是,只有通過 Vue-Resource 發起的請求才會被中間件過濾,所以其它由 jQuery 發起的請求仍然繼續用以前的策略處理。
第三步:組件化
組件是 Vue 最重要的特性。簡單來說,任何 Vue 實例都可以視作組件;一個組件可以由多個組件組合而成;所以一個應用,無論規模,都可以視作若干個組件組成的整體。
為了接下來的內容,我建議你先讀完 Vue 組件的文檔。
組件的目的是提高代碼的復用性。舉個例子,前面我們實現了 Modal 組件的 Vue 化,似乎蠻簡單的,效果還不錯。但是當另一個頁面當中也需要這樣的 Modal,麻煩就來了。完全復制一遍當然可以,但這明顯不是最好的做法。
這種時候,我們就該把它做成組件。因為整個產品框架仍然處於混合運行的狀態,所以單文件組件 暫時還不容易接入,我們還是選擇全局組件吧。
Vue.component('my-modal', { template: '', props: { isShow: { type: Boolean, default: false } }, data() { return { isShowClass: false, result: null, message: '' } } methods: { submit(event) { // 提交 }, show() { this.isShow = true; }, hide() { this.isShow = false; }, close() { this.isShowClass = false; } }, watch: { isShow(value) { setTimeout(() => { this.nextShow = value; }, 50); } } })
為防止你沒有仔細閱讀文檔,我還是把一些要點提一下。首先,注冊全局組件要用 Vue.component
方法,注冊出來的組件通過在 HTML 里寫標簽來使用,如:
<my-modal> </my-modal>
其次,組件的 data
屬性必須是一個函數,然后返回一個包含要用到數據的對象。
最后,任何組件在用的時候都會變成“子組件”,所以這個時候向它傳值必須通過 prop
屬性。在我們的例子中,負責控制 Modal 顯示/隱藏的屬性是 isShow
,原先通過 modal.isShow = true;
即可將它彈出。如今我們需要把它暴露給父組件,所以放在 props
里。
接下來,我還要調整一下它的模板。既然是組件,必須考慮復用,所以內部表單肯定不能寫死了。Vue 給我們提供了 Slot(插槽)作為插入外部內容的手段,所以模板可以改成這樣:
<!-- 前面不變 --> <div class="modal-body"> <slot @submit.prevent="submit"></slot> </div> <!-- 后面也不變 -->
模板可以放在任何地方,比如統一堆在 index.html 里面,用 <script type="text/x-template"></script>
包裹;也可以寫在組件的 template
屬性里,只要能訪問到,都不是問題。
這樣,我們就可以在別的地方使用這個 Modal 組件了:
<div class="隨便什么容器" id="some-vue-app"> <my-modal @saved="onModalSaved"> <form action="/api/some/" method="post"> <!-- 表單內容 --> </form> </my-modal> </div>
不過使用 Vue 組件的一定是其它 Vue 實例,如果要混合使用的話,也要用 Vue 實例作為中介。
單文件組件
單文件組件是更好的選擇。它更容易被復用、被修改、被測試。
不過單文件組件更依賴對整個前端工具體系的掌握,你必須會用 Webpack,會配置各種 loader,對於一些初學者可能會比較困難。所以我建議不要着急上單文件組件,干什么事都應該循序漸進,先把 Vue 用好用熟練,解決掉日常用到的問題,再找機會切換到單文件模式就好。
之前的工作當然不是白費的,Vue 的單文件組件也可以正常使用 import、export,所以之前寫好的組件可以直接放進來。
比如我們的 Modal,完成之后,將來如果要謄到單文件組件中,只需要在里面引用就好:
<template> .... </template> <script src="./my-modal.js"></script>
第四步:路由
改造路由本身其實並不困難。真正的難點在於,之前的路由,無論是基於 Backbone.Router 還是基於 Page.js,都是偵聽 URL 變化,然后調用回調函數來處理。而 Vue 官方插件 Vue-Router,則是直接實例化頁面組件。
這樣導致我們很難漸進式的遷移功能頁,必須小心翼翼的把兩個路由分開,比如 /new/path/to/feature/
交給新路由,其它的交給老路由,等將來徹底遷移重構完畢再把 new
去掉。
本身路由的寫法直接看官方文檔就好了,大約1個小時就能讀完,這里就不在贅述。
第五步:使用其它 UI 框架
經過不懈的努力,后台項目的改造告一段落。大部分組件都被重構成基於 Vue,有些還被很好的重構成可復用的組件,用在其它項目中。如今,雖然用的還是 Bootstrap,但已經可以把 jQuery 從依賴中拿掉了。
整個系統基於 Vue 全家桶開發,使用 Webpack + Babel 管理,既時髦又高效。我們對 Vue 開發也很熟悉了,日常開發不在話下。
不過從這個時刻起,我們也不需要像以前那樣謹小慎微,土啦吧唧的以“夠用就行”的標准來寫組件。項目重構完成之后,因為 Vue 單文件組件出色的解耦特性,引用外部組件庫也是個不錯的選擇。而且從提升技術的角度,我還是強烈推薦大家用一用別人的框架。
我這方面的經驗也不多,就推薦兩個吧:
-
Element UI 由餓了么團隊開發維護的組件庫。
-
iView 一套基於 Vue.js 的高質量 UI 組件庫。
推薦這兩款組件庫的原因除了它們本身質量不錯,都是國人開發,中文文檔豐富也是重要原因。
總結
其實,動類似的腦筋的人,我肯定不是第一個。比如 VueStrap 和 VueStrap 這兩個項目,但是它代碼寫的實在太差,功能也不完備,用起來各種不爽,所以我干脆重寫了一些。
Vue 比較讓我欣賞的一點也是如此:實現組件庫很簡單,覺得別的庫不合適,自己搞也很快。
甚至我認為這才是正道:大部分基礎組件,可以依賴 CSS 來實現,又簡單又可靠;部分復雜的功能,自己實現有針對性並且能全把握的大型組件。這也是我寫作這篇文章的原因。
擴展閱讀
https://blog.csdn.net/GitChat/article/details/78086852