目前團隊中新的 Web 項目基本都採用了 Vue 或 React ,加上 RN,這些都屬於比較重量級的框架,然而對於小型 Web 頁面,又顯得過大。早期的一些項目則使用了較原始的 HTML 頁面構建技術,但業務邏輯基本沒法複用。
近半年作過幾個小型 Web 頁面,在不斷學習前端知識的同時,也在重構並摸索小型 Web 項目可能的更好解決方案。本文則對以前的工做進行一次總體描述。css
目標和定位html
單論小型 Web 頁面,其相對於 Vue/React 等項目最大不一樣是不須要支持 SPA 這種比較重的形式,以 MVP(Minimum Viable Product) 爲原則,小頁面只要知足需求,作到夠用便可。因此在對現有原始 Web 頁面進行重構時,會將如下兩個方面做爲最高優先級:前端
不斷提升項目的重用性、可維護性;vue
不斷提升前端性能,這裏主要是加載性能;ios
對於第一點,組件化代碼結構是當前最可行的思路;對於第二點,在作到第一點的前提下,極少的第三方依賴,良好的打包方法,是必需要作到的。nginx
項目結構演化歷程es6
本文所描述的小型 Web 頁項目結構和打包方法是通過若干次項目重構才獲得的。ajax
初版 vuex
初版項目基本上以最原始的 HTML+JS+CSS 爲基準。爲了讓項目代碼更好維護,首先考慮到的是有兩個點須要作:json
使頁面內容具備維護性,須要採用 JS 模板;
因爲業務複雜,分工較細,接口繁多,須要將數據接入層 DAL
(Data Access Layer) 單獨分離出來;
對於第一個問題,最後選擇 Mustache
庫。緣由是它語法極簡,容易學習,同時該類型語法有廣大用戶羣體,固然一樣流行的還有 underscore
/ejs
類型的模板語法。爲了保證內容頁面的無邏輯性和簡單,故 Mustache
的高級版 Handlebars
未被使用。
第二個問題是對公司業務和項目代碼有所瞭解後所下的結論。至關因而對現有代碼的重構,主要目的是進行職責分離,將複雜多變的接口隔離出來,讓剩下的代碼專心解決業務問題。
開始敲代碼後,才發現另外一個比較嚴重的問題:我須要把管理內容模板的代碼單獨分離出來,使其不會影響主要的業務邏輯,因而想到了 MVP(Model View Presenter) 模式。簡單講,這個就是 MVVM 模式去掉 View-ViewModel 雙向數據綁定後的一個弱化版。以下圖所示:
在小型 Web 頁面中,通常是沒有 Model 層的。頁面中的 Presenter 部分只負責經過參數控制界面的渲染,並以組件的方式對外公開 View 層事件。按照這個思路,初版項目結構就基本出來了,見下圖:
初版項目結構已經足以應付小型 Web 頁面的需求了,同時也不會帶來較多的複雜性。可是原始 Web 頁面天生就不利於模塊化開發,同時存在一個根本性問題:
代碼解耦會使項目文件結構清晰,職責分離,有利於維護;
打包結果須要將相關代碼壓縮到單個文件中,便於提升加載性能;
在 Web 頁面開發中,這二者造成一個悖論。因此須要引入一個打包機制,將項目代碼和打包文件進行解耦。年老的 Gulp 和 Grunt 就不看了,現存項目中用的較多的是 FIS3 和 Webpack 1.x。前者國產,使用起來也很是方便,後者難度高一些,可是跟其餘國外開源項目同樣,他們總能把一個軟件的 50% —— 文檔作的很好。(其實還有不少能夠吐槽的地方,但看了以後感受相對更踏實)
在看了 Webpack 2.x 的文檔以後,基本就肯定使用該打包機制了,它有以下優勢讓人慾罷不能:
原生支持 import
語法。這樣就完全擺脫文件結構很差管理的問題了,面向對象、模塊化什麼的通通均可以引進來,終於能夠舒舒服服寫代碼了;
支持 Tree Shaking。原本這是 rollup 打包機制獨有的特色,如今 Webpack 也有了;
Webpack 的配置文件雖然複雜,但瞭解以後再配合插件機制,會發現它潛力很大,使用也至關靈活;
在引入 Webpack 2.x 後,不一樣功能均以單個文件的形式進行分開,各模塊之間接口也變得很是明確,但還有待改進。
加入打包機制後,JavaScript 文件已經解耦的不錯了,可是模板還都放在了首頁中,樣式也都放在了一個文件中,依賴關係混亂,不方便管理。改良它的一個好方法是參考其餘優秀項目,好比 Vue 就有一套很好的項目組織結構,直接借鑑就好了。新的項目主要變化以下:
真正將組件分離出來。組件內容採用 Mustache
模板,樣式採用 Less
語法,JS 部分則控制組件的渲染邏輯,儘可能不關聯業務邏輯,三者合一就至關於一個 .vue 文件了。經過修改 Webpack 配置或使用合適的插件,該方式能夠一樣支持其餘模板和 CSS 語法,好比 ejs
或者 SCSS
;
選擇支持多頁面入口,而沒有采用路由功能。這樣能夠簡化 SPA 中複雜的 URL 結構,同時打包結果也不用附帶路由邏輯。這樣還有一個好處是後期引入簡單版的 SSR 也會很方便,路由就是 nginx 的事兒了;
對於該部分項目結構的詳細描述直接看下文。結構圖以下:
須要說明的是,圖中的 State Store
其實目前是沒有的,放在這裏主要是爲了好看 :)。後期若是把 vuex/MobX/Redux 之類的加進去了,那就完整了,目前由於業務邏輯很簡單,狀態什麼的暴力解決就好了。而 app.js 則處理項目中公共的業務邏輯,讓頁面入口解脫出來專心處理內容。
項目目錄結構以下:
----build # Webpack 配置文件 ... ----src --------assets # 資源文件
--------components ------------GoodsInfo # 商品信息組件 GoodsInfo.mst # 組件模板,採用 Mustache 語法 GoodsInfo.js # 組件渲染和操做邏輯。通常業務無關 GoodsInfo.less # 組件樣式
------------RiskPromt ... ------------ShareHeader ... ------------SharePanel ... utils.js # 業務無關,視圖層相關的輔助方法集合
--------dal # 數據接入層 index.js # 入口文件。集中管理請求接口和僞數據 getInfoById.js # 接口請求實現 getInfoById.json # 接口返回僞數據,在 index.js 中可生成 mock 方法
--------Main # 默認頁面入口 Main.html # 頁面模板 Main.js # 頁面業務邏輯 Main.less # 頁面樣式
--------MainBanner # 帶有底部 Banner 的頁面入口 ... app.js # 抽取多頁面共有的業務邏輯,好比分享功能的具體實現 common.js # 應用級的輔助方法集合 common.less package.json README.md
第三方依賴
在 Webpack 2+ 的幫助下,項目選用了以下開源第三方庫做爲基礎依賴:
es6-promise
:採用 Promise 的方式可使代碼更清晰更好維護;
axios
:Vue 官方推薦的 vue-resource 替代品;
mustache
:項目所用的模板庫
另外還使用了團隊維護的 SDK:
@zz/zz-jssdk
:提供 Web 頁面和轉轉 App 客戶端的交互接口
@zz/perf
:性能統計工具
因爲 axios
官方堅持不集成非標準的 jsonp
請求,對於現存部分只支持 jsonp
請求的接口,還須要引入 jsonp
第三方開源庫。
以上是項目文件依賴。開發依賴中,所用的第三方庫基本都是 Webpack 相關,包括 Less
文件的解析模塊。項目沒有引入 babel-polyfill
進行 ES6 語法的開發,由於容易產生沒必要要的額外打包代碼。
在 Webpack 的語義下,全部的項目文件都是一種資源,供 JavaScript 使用,因此處理任何資源時,只要配置好合適的 loader 便可。該部分則對項目中不一樣類型文件的加載和解析規則配置進行了簡要描述。這裏不會講解 Webpack 配置細節,相關內容請查看官方文檔。
對於通常資源文件的加載,採用 file-loader
便可。對於圖片文件,採用推薦的 url-loader
。該加載器有一個選項是,若是圖片小於指定值,會將其轉化爲 DataUri
嵌入到打包文件中,以減小額外 HTTP 請求,項目設置指定值爲經常使用的 10K。規則以下:
樣式文件
項目中樣式文件默認採用 Less
,主要用到該庫的兩個特性:
能夠方便的使用 CSS 變量,典型的好比定義通用像素大小;
層次化的樣式描述方式;
Webpack 配置同時保留了 css 文件的加載能力,後期還能夠加入對 SCSS
文件支持。規則以下:
同一個項目中,因爲 CSS
/LESS
/SCSS
文件之間具備依賴關係,因此強烈推薦採用同一種技術實現。對於單個組件,不大可能像 Vue 同樣寫個 Webpack Loader 支持 .vue 類型的組件格式。樣式文件的加載須要在對應的 .js 文件中顯式引入 .less 文件,好比:
項目模板默認採用 Mustache
,在 Webpack 的支持下,模板內容被單獨放在一個文件中,並以 .mst
做爲自定義後綴,文件內容依然是 HTML 格式,只是根標籤爲 <template>
。Webpack 中選用 html-loader
對其進行解析,規則以下:
對於 Mustache
模板的自動解析和加載,網上有開源的 mustache-loader
實現,但其關注度實在過低,而 html-loader
足以達到所需功能:
加載 .mst 文件,並壓縮內容;
將文件中 img:src
等相對路徑屬性自動替換爲絕對目標地址;
對於其餘模板語言一樣可使用這種方法,就能夠在項目中靈活的使用不一樣的模板庫了。不過須要注意的是,同一個項目中最好只使用一種模板語言,方便管理,同時不會增長打包文件大小。
將 .mst 模板加載到頁面中和 .less 方法差很少。在對應的 .js 文件中顯式引入,而後用 extractTemplate
方法提取出模板內容便可:
這種顯式引入的方式有一個好處是,能夠手動控制不一樣的模板和樣式。在實際產品需求中,內容和樣式改變是很頻繁的,而功能邏輯的變化相對要慢一些,這樣經過 js 引用不一樣版本的模板和樣式就會比較靈活。若是能把這一套管理機制抽象出來單獨進行配置也是很不錯的。
頁面文件在 Webpack 中也是以模板的角色存在的,解析方式和模板同樣,規則見上文。因爲是頁面入口文件,在 Webpack 中還須要使用 HtmlWebpackPlugin
插件進行配置。以下配置中,項目存在兩個不一樣的頁面入口,因此須要兩個 HtmlWebpackPlugin
實例:
因爲用戶每次進入 Web 頁面都會加載首頁,因此首頁越小越節省流量。參考 Vue 項目的 index.html
就會發現裏面基本只有一個骨架,具體內容都在組件中。但項目配置自己不會對這點進行假設,因此即便在首頁中寫入全部內容也是可行的。
項目的主要打包配置前文已經介紹差很少了,其餘具體配置參看官方文檔便可。採用該項目結構的最後打包結果,全部部署文件包括圖片加起來沒有超過 130K。在瀏覽器中,由於 gzip 的緣由,全頁面加載網絡流量不到 70K。
前文已經提到過,把數據請求單獨做爲一個層主要是爲了分離出複雜多變的數據請求接口,還有一個好處是接口 mock 數據也能夠在這裏統一處理。
一個項目中可能在不少地方都會請求同一個接口。對於單個接口請求,可能有不一樣方法,好比用 ajax、fetch、jsonp、axios 甚至 jQuery 庫;有的是 GET,有的是 POST;有的還須要帶 cookie,其餘卻不須要;返回數據的格式也許還不是統一的。而 JavaScript 邏輯只關心輸入和輸出,把這些請求細節都放在另一個地方單獨維護,會使主要業務邏輯更加簡潔。在項目中使用時,只須要以 Promise
的形式調用方法便可。接口封裝的示例代碼以下:
先後端協同開發時,須要首先定好接口,給出 mock 數據示例。因此在 DAL
層把 mock 數據封裝好,會節省不少工做。在項目中會將 mock 數據直接保存成 .json 格式的文件,而後在 DAL 入口文件中經過 import
導入,再使用一個工廠方法來對外提供 mock
方法,便可使用 mock 數據了。下面是入口文件中相關代碼:
有了 DAL
層對各請求接口的聚合,在其餘地方使用就比較簡單了,直接上代碼:
小型 Web 頁面的組件和 .vue
文件結構相似,只是分紅了三個文件:
樣式。內容和使用方式是基本同樣的;
模板。後者 Vue
有本身的模板語法,前者則用的 Mustache
,也可支持其餘模板。若是 Vue
的模板加載器單獨分離出來,那理論上也是能夠拿過來使用的;
控制邏輯。JS 邏輯部分則有些不同,Vue
框架有着本身獨特完美的雙向綁定機制,其接口和生命週期也是圍繞它來設計的(這裏只針對 .vue
文件進行討論,類 React
使用方式很大程度是爲了方便拉取用戶而設定)。小型 Web 頁面由於簡單,因此重心都放在了組件初始化和渲染上;
組件在小型 Web 頁面中定位是很明確的,即只針對頁面呈現和交互,因此對外接口的設計也不復雜。若是組件採用的是 MVC 模式,那就很難討論,由於 Controller 自己就是「老大」,可能有不少行爲。Presenter 和 ViewModel 則相對簡單,它們的區別只是內在機制不一樣,對外是行爲是差很少的。這裏不考慮大型 Web 頁面,小型 Web 頁中組件的接口默認就兩種:接受純數據參數(props
);對外公佈事件接口。相比於更高級的 Vue,少了一個 Slot
插槽功能。
使用組件的方式很直接,看代碼:
組件中一個 init
方法並不能搞定所有需求,由於項目中 init
方法不只包含了組件渲染邏輯,還有事件綁定邏輯。當組件數據內容更新時,還須要抽取出一個 render
或 update
方法單獨進行調用來更新界面。這不像 Vue 自帶雙向數據綁定神器,因此要麻煩點。
使用組件提供的事件也很簡單,代碼以下:
這裏事件句柄的參數採用了 (Object data, Event e)
的形式。其中 data
表示事件來源,它能夠是被點擊對象的 ViewModel
,或者簡單點,直接是被點擊對象所表明的的原始數據;e
則是 HTML 的事件參數。
組件內部綁定到具體的模板前文已經示例說明過了。在渲染組件內容時,還須要處理參數內容,並將其渲染到頁面指定地方。這裏直接上代碼:
在構造器中,首先定義 props
參數的格式,並給上默認值。在 init
方法中,則將 data 中的參數賦值給 props
,這裏通常是會有數據轉化處理邏輯。
最後直接進行組件渲染。能夠發現,若是想要使用其餘模板引擎,是很容易替換的。若是採用 SSR 服務端渲染組件,那能夠各類模板庫全放進來,一個工廠方法就能夠進行自動化處理。
組件的參數被取名爲 props
,徹底是仿造 Vue
/React
。由於它們的功能和定位基本是同樣的,並且官方推薦的最佳實踐這裏也基本都推薦。具體這樣作的幾點思路以下:
小項目作不到 Vue
/React
的參數驗證功能,但顯式表示 props
參數有自描述文檔的做用,須要哪些參數及其類型一目瞭然;
構造器中同時給出了 props
默認值,無參數時組件有默認展現形式;
參數只有一個 data
對象。Vue
推薦參數都用基本類型,但內容龐大時,屬性繁多,分割成更小組件也不會減小多少使用的複雜性;
props
中的每個屬性不能是對象,只能是 Integer
、String
、Boolean
、Array
等基本類型;
將事件的觸發封裝到組件中也是爲了減小業務的複雜性。不少 Web 項目中都是直接操做頁面內容,用戶交互、內容處理、業務邏輯都耦合在了一塊兒,這裏組件將用戶交互封裝起來,同時對外提供事件接口。代碼以下:
組件內部保存一個事件回調句柄 clickCallback
,組件初始化時對用戶點擊事件進行數據綁定,並觸發這個回調。
本文簡單描述了小型 Web 頁面的定位,經過對小型 Web 頁面的摸索和演化解釋了當前項目結構的設計思路,並對其細節進行了詳細描述,重點介紹了數據接入層和組件化開發。
當前的項目並非最終形態,而只是一個 α 版本的雛形,還有不少地方值得改善:
針對首屏時間進行優化,好比支持 SSR;
繼續改善打包部署方案,靈活支持多頁面部署,達到或接近離線應用的效果;
一些好的 ES6 語法很值得支持,須要找到一個方法在打包層面上漸進式的引入特定語法;
基於 Promise 的語法值得大面積採用,這是代碼層面須要考慮的;
Webpack 挺好,但還不夠好,但願插件能更成熟更豐富;
可能還有不少點沒考慮到,不過實際需求永遠是最高優先級。只要不斷的重構和改善,軟件就會一直有生命力~