前端集成解決方案要求:
- 模塊化開發。最好能像寫nodejs一樣寫js,很舒服。css最好也能來個模塊化管理!
- 性能要好。模塊那么多,得有按需加載,請求不能太多。
- 組件化開發。一個組件的js、css、模板最好都在一個目錄維護,維護起來方便。
- 用 handlebars 作為前端模板引擎。這個模板引擎不錯,logic-less(輕邏輯)。
- 用 stylus 寫css挺方便,給我整一個。
- 圖片base64嵌入。有些小圖可能要以base64的形式嵌入到頁面、js或者css中使用。嵌入之前記得壓縮圖片以減小體積。
- js/css/圖片壓縮應該都沒問題吧。
- 要能與公司的ci平臺集。工具里最好別依賴什么系統庫,ci的機器未必支持。
- 開發體驗要好。文件監聽,瀏覽器自動刷新(livereload)一個都不能少。
- 我們用nodejs作為服務器,本地要能預覽,最好再能抓取線上數據,方便調試。
- 代碼發布后能按照版本部署,不要彼此覆蓋。
- 允許第三方引用項目公共模塊
- 支持原有公用組件模塊,采用component 作為發布規范
簡單劃分 - 規范
- 開發規范
- 模塊化開發,js模塊化,css模塊化,像nodejs一樣編碼
- 組件化開發,js,css,handlebars維護在一起
- 部署規范
- 采用nodejs后端,基本部署規范應該參考 [express]
(http://expressjs.com/) 項目部署 - 按版本號做費覆蓋式發布
- 公共模塊可發布給第三方共享
- 采用nodejs后端,基本部署規范應該參考 [express]
- 開發規范
- 框架
- js模塊化框架,支持請求合并,按需加載等性能優化點
- 工具
- 可以編譯stylus為css
- 支持js、css、圖片壓縮
- 允許圖片壓縮后以base64編碼形式嵌入到css、js或html中
- 與ci平臺集成
- 文件監聽、瀏覽器自動刷新
- 本地預覽、數據模擬
- 倉庫
- 支持component模塊安裝和使用
百度出品的 fis 就是一個能幫助快速搭建前端集成解決方案的工具,不幸的是,現在fis官網所介紹的 并不是 fis,而是一個叫 fis-plus 的項目,該項目并不像字面理解的那樣是fis的加強版,而是在fis的基礎上定制的一套面向百度前端團隊的解決方案,以php為后端語言,跟smarty有較強的綁定關系,有著 19項
技術要素,密切配合百度現行技術選型。絕大多數非百度前端團隊都很難完整接受這19項技術選型,尤其是其中的部署、框架規范,跟百度前端團隊相關開發規范、部署規范、以及php、smarty等有著較深的綁定關系。
因此如果你的團隊用的不是 php后端 && smarty模板&& modjs模塊化框架
&& bingo框架的話,請查看 fis的文檔,或許不會有那么多困惑。
ps: fis是一個構建系統內核,很好的抽象了前端集成解決方案所需的通用工具需求,本身不與任何后端語言綁定。而基于fis實現的具體解決方案就會有具體的規范和技術選型了。
0. 開發概念定義
前端開發體系設計第一步要定義開發概念。開發概念是指針對開發資源的分類概念。開發概念的確立,直接影響到規范的定制。比如,傳統的開發概念一般是按照文件類型劃分的,所以傳統前端項目會有這樣的目錄結構:
- js:放js文件
- css:放css文件
- images:放圖片文件
- html:放html文件
這樣確實很直接,任何智力健全的人都知道每個文件該放在哪里。但是這樣的開發概念劃分將給項目帶來較高的維護成本,并為項目臃腫埋下了工程隱患,理由是:
如果項目中的一個功能有了問題,維護的時候要在js目錄下找到對應的邏輯修改,再到css目錄下找到對應的樣式文件修改一下,如果圖片不對,還要再跑到images目錄下找對應的開發資源。
images下的文件不知道哪些圖片在用,哪些已經廢棄了,誰也不敢刪除,文件越來越多。
ps: 除非你的團隊只有1-2個人,你的項目只有很少的代碼量,而且不用關心性能和未來的維護問題,否則,以文件為依據設計的開發概念是應該被拋棄的。
以我個人的經驗,更傾向于具有一定語義的開發概念。綜合前面的需求,我為這個開發體系確定了3個開發資源概念: - 模塊化資源:js模塊、css模塊或組件
- 頁面資源:網站html或后端模板頁面,引用模塊化框架,加載模塊
- 非模塊化資源:并不是所有的開發資源都是模塊化的,比如提供模塊化框架的js本身就不能是一個模塊化的js文件。嚴格上講,頁面也屬于一種非模塊化的靜態資源。
ps: 開發概念越簡單越好,前面提到的fis-plus也有類似的開發概念,有組件或模塊(widget),頁面(page),測試數據(test),非模塊化靜態資源(static)。有的團隊在模塊之中又劃分出api模塊和ui模塊(組件)兩種概念
1. 開發目錄設計
基于開發概念的確立,接下來就要確定目錄規范了。我通常會給每種開發資源的目錄取一個有語義的名字,三種資源我們可以按照概念直接定義目錄結構為:
project
- modules 存放模塊化資源
- pages 存放頁面資源
- static 存放非模塊化資源
這樣劃分目錄確實直觀,但希望能使用component倉庫資源,因此我決定將模塊化資源目錄命名為components,得到:
project
- components 存放模塊化資源
- pages 存放頁面資源
- static 存放非模塊化資源
模塊資源分為項目模塊和公共模塊,以及hinc提到過希望能從component安裝一些公共組件到項目中,因此,一個components目錄還不夠,想到nodejs用node_modules作為模塊安裝目錄,因此我在規范中又追加了一個 component_modules 目錄,得到:
project
- component_modules 存放外部模塊資源
- components 存放項目模塊資源
- pages 存放頁面資源
- static 存放非模塊化資源
大多數項目采用nodejs作為后端,express是比較常用的nodejs的server框架,express項目通常會把后端模板放到 views 目錄下,把靜態資源放到 public下。為了迎合這樣的需求,我將page、static兩個目錄調整為 views
和 public,規范又修改為:
project
- component_modules 存放外部模塊資源
- components 存放項目模塊資源
- views 存放頁面資源
- public 存放非模塊化資源
考慮到頁面也是一種靜態資源,而public這個名字不具有語義性,與其他目錄都有概念沖突,不如將其與views目錄合并,views目錄負責存放頁面和非模塊化資源比較合適,因此最終得到的開發目錄結構為:
project
- component_modules 存放外部模塊資源
- components 存放項目模塊資源
- views 存放頁面以及非模塊化資源
2. 部署目錄設計
一個完整的部署結果應該是這樣的目錄結構:
由于還要部署一些可以被第三方使用的模塊,public下只有項目名的部署還不夠,應改把模塊化文件單獨發布出來,得到這樣的部署結構:
由于 component_modules 這個名字太長了,如果部署到這樣的路徑下,url會很長,這也是一個優化點,因此最終決定部署結構為:
插一句,并不是所有團隊都會有這么復雜的部署要求,這和松鼠團隊的業務需求有關,但我相信這個例子也不會是最復雜的。每個團隊都會有自己的運維需求,前端資源部署經常牽連到公司技術架構,因此很多前端項目的開發目錄結構會和部署要求保持一致。這也為項目間模塊的復用帶來了成本,因為代碼中寫的url通常是部署后的路徑,遷移之后就可能失效了。
解耦開發規范和部署規范是前端開發體系的設計重點。
3. 配置fis連接開發規范和部署規范
樣例項目:
project
- views
- logo.png
- index.html
- fis-conf.js
- README.md
fis-conf.js是fis工具的配置文件,接下來我們就要在這里進行構建配置了。雖然開發規范和部署規范十分復雜,但好在fis有一個非常強大的 roadmap.path 功能,專門用于分類文件、調整發布結構、指定文件的各種屬性等功能實現。
所謂構建,其核心任務就是將文件按照某種規則進行分類(以文件后綴分類,以模塊化/非模塊化分類,以前端/后端代碼分類),然后針對不同的文件做不同的構建處理。
閑話少說,我們先來看一下基本的配置,在 fis-conf.js 中添加代碼:
fis.config.set('roadmap.path', [ {
reg : '**.md', //所有md后綴的文件
release : false //不發布
}
]);
以上配置,使得項目中的所有md后綴文件都不會發布出來。release是定義file對象發布路徑的屬性,如果file對象的release屬性為false,那么在項目發布階段就不會被輸出出來。
在fis中,roadmap.pah是一個數組數據,數組每個元素是一個對象,必須定義 reg
屬性,用以匹配項目文件路徑從而進行分類劃分,reg屬性的取值可以是路徑通配字符串或者正則表達式。fis有一個內部的文件系統,會給每個源碼文件創建一個 fis.File 對象,創建File對象時,按照roadmap.path的配置逐個匹配文件路徑,匹配成功則把除reg之外的其他屬性賦給File對象,fis中各種處理環節及插件都會讀取所需的文件對象的屬性值,而不會自己定義規范。有關roadmap.path的工作原理可以看這里 以及 這里。ok,讓md文件不發布很簡單,那么views目錄下的按版本發布要求怎么實現呢?其實也是非常簡單的配置:
fis.config.set('roadmap.path', [
{
reg : '**.md', //所有md后綴的文件
release : false //不發布
},
{
//正則匹配【/views/**】文件,并將views后面的路徑捕獲為分組1
reg : /^\/views\/(.*)$/i, //發布到 public/proj/1.0.0/分組1 路徑下
release : '/public/proj/1.0.0/$1'
}
]);
roadmap.path數組的第二元素據采用正則作為匹配規則,正則可以幫我們捕獲到分組信息,在release屬性值中引用分組是非常方便的。正則匹配 + 捕獲分組,成為目錄規范配置的強有力工具:

在上面的配置中,版本號被寫到了匹配規則里,這樣非常不方便工程師在迭代的過程中升級項目版本。我們應該將版本號、項目名稱等配置獨立出來管理。好在roadmap.path還有讀取其他配置的能力,修改上面的配置,我們得到:
fis的配置系統非常靈活,除了 文檔 中提到的配置節點,其他配置用戶可以隨便定義使用。比如配置的roadmap是系統保留的,而name、version都是用戶自己隨便指定的。fis系統保留的配置節點只有6個,包括:
project(系統配置)
modules(構建流程配置)
settings(插件配置)
roadmap(目錄規范與域名配置)
deploy(部署目標配置)
pack(打包配置)
完成第一份配置之后,我們來看一下效果。
cd project
fis release --dest ../release
進入到項目目錄,然后使用fis release命令,對項目進行構建,用 --dest <path>
參數指定編譯結果的產出路徑,可以看到部署后的結果:
ps: fis release會將處理后的結果發布到源碼目錄之外的其他目錄里,以保持源碼目錄的干凈。
fis系統的強大之處在于當你調整了部署規范之后,fis會識別所有資源定位標記,將他們修改為對應的部署路徑。
fis的文件系統設計決定了配置開發規范的成本非常低。fis構建核心有三個超級正則,用于識別資源定位標記,把用戶的開發規范和部署規范通過配置完整連接起來,具體實現可以看這里。
不止html,fis為前端三種領域語言都準備了資源定位識別標記,更多文檔可以看這里:在html中定位資源,在js中定位資源,在css中定位資源
接下來,我們修改一下項目版本配置,再發布一下看看效果:
fis.config.set('version', '1.0.1');
再次執行:
cd projectfis release --dest ../release
至此,我們已經基本解決了開發和部署直接的目錄規范問題,這里我需要加快一些步伐,把其他目錄的部署規范也配置好,得到一個相對比較完整的結果:
我構造了一個相對完整的目錄結構,然后進行了一次構建,效果還不錯:

不管部署規則多么復雜都不用擔心,有fis強大的資源定位系統幫你在開發規范和部署規范之間建立聯系,設計開發體系不在受制于工具的實現能力。
你可以盡情發揮想象力,設計出最優雅最合理的開發規范和最高效最貼合公司運維要求的部署規范,最終用fis的roadmap.path功能將它們連接起來,實現完美轉換。
fis的roadmap功能實際上提供了項目代碼與部署規范解耦的能力。
從前面的例子可以看出,開發使用相對路徑即可,fis會在構建時會根據fis-conf.js中的配置完成開發路徑到部署路徑的轉換工作。這意味著在fis體系下開發的模塊將具有天然的可移植性,既能滿足不同項目的不同部署需求,又能允許開發中使用相對路徑進行資源定位,工程師再不用把部署路徑寫到代碼中了。
模塊化框架###
模塊化框架是前端開發體系中最為核心的環節。
模塊化框架肩負著模塊管理、資源加載、性能優化(按需,請求合并)等多種重要職責,同時它也是組件開發的基礎框架,因此模塊化框架設計的好壞直接影響到開發體系的設計質量。
很遺憾的說,現在市面上已有的模塊化框架都沒能很好的處理模塊管理、資源加載和性能優化三者之間的關系。這倒不是框架設計的問題,而是由前端領域語言特殊性決定的。框架設計者一般在思考模塊化框架時,通常站在純前端運行環境角度考慮,基本功能都是用原生js實現的,因此一個模塊化開發的關鍵問題不能被很好的解決。這個關鍵問題就是依賴聲明。
以 seajs 為例(無意冒犯),seajs采用運行時分析的方式實現依賴聲明識別,并根據依賴關系做進一步的模塊加載。比如如下代碼:
define(function(require) {
var foo = require("foo"); //...
});
當seajs要執行一個模塊的factory函數之前,會先分析函數體中的require
書寫,具體代碼在這里和這里,大概的代碼邏輯如下:
Module.define = function (id, deps, factory) {
... //抽取函數體的字符串內容
var code = factory.toString(); //設計一個正則,分析require語句
var reg = /\brequire\s*\(([.*]?)\)/g;
var deps = []; //掃描字符串,得到require所聲明的依賴
code.replace(reg, function(m, $1){
deps.push($1);
});
//加載依賴,完成后再執行factory ...
};
由于框架設計是在“純前端實現”的約束條件下,使得模塊化框架對于依賴的分析必須在模塊資源加載完成之后才能做出識別。這將引起兩個性能相關的問題:
1.require被強制為關鍵字而不能被壓縮。否則factory.toString()的分析將沒有意義。
2.依賴加載只能串行進行,當一個模塊加載完成之后才知道后面的依賴關系。
第一個問題還好,尤其是在gzip下差不多多少字節,但是要配置js壓縮器保留require函數不壓縮。第二個問題就比較麻煩了,雖然seajs有seajs-combo插件可以一定程度上減少請求,但仍然不能很好的解決這個問題。舉個例子,有如下seajs模塊依賴關系樹:
ps: 圖片來源 @raphealguo
采用seajs-combo插件之后,靜態資源請求的效果是這樣的:
http://www.example.com/page.js
http://www.example.com/a.js,b.js
http://www.example.com/c.js,d.js,e.js
http://www.example.com/f.js
工作過程是
- 框架先請求了入口文件page.js
- 加載完成后分析factory函數,發現依賴了a.js和b.js,然后發起一個combo請求,加載a.js和b.js
- a.js和b.js加載完成后又進一步分析源碼,才發現還依賴了c.js、d.js和e.js,再發起請求加載這三個文件
- 完成c、d、e的加載之后,再分析,發現f.js依賴,再請求
- 完成f.js的請求,page.js的依賴全部滿足,執行它的factory函數。
雖然combo可以在依賴層級上進行合并,但完成page.js的請求仍需要4個。很多團隊在使用seajs的時候,為了避免這樣的串行依賴請求問題,會自己實現打包方案,將所有文件直接打包在一起,放棄了模塊化的按需加載能力,也是一種無奈之舉。
原因很簡單以純前端方式來實現模塊依賴加載不能同時解決性能優化問題。
歸根結底,這樣的結論是由前端領域語言的特點決定的。前端語言缺少三種編譯能力,前面講目錄規范和部署規范時其實已經提到了一種能力,就是“資源定位的能力”,讓工程師使用開發路徑定位資源,編譯后可轉換為部署路徑。其他語言編寫的程序幾乎都沒有web這種物理上分離的資源部署策略,而且大多具都有類似'getResource(path)'這樣的函數,用于在運行環境下定位當初的開發資源,這樣不管項目怎么部署,只要getResource函數運行正常就行了。可惜前端語言沒有這樣的資源定位接口,只有url這樣的資源定位符,它指向的其實并不是開發路徑,而是部署路徑。
這里可以簡單列舉出前端語言缺少三種的語言能力:
**
資源定位的能力:使用開發路徑進行資源定位,項目發布后轉換成部署路徑
依賴聲明的能力:聲明一個資源依賴另一個資源的能力
資源嵌入的能力:把一個資源的編譯內容嵌入到另一個文件中
**
以后我會在完善前端開發體系理論的時候在詳細介紹這三種語言能力的必要性和原子性,這里就暫時不展開說明了。
fis最核心的編譯思想就是圍繞這三種語言能力設計的。
要兼顧性能的同時解決模塊化依賴管理和加載問題,其關鍵點在于
不能運行時去分析模塊間的依賴關系,而要讓框架提前知道依賴樹。
了解了原因,我們就要自己動手設計模塊化框架了。不要害怕,模塊化框架其實很簡單,思想、規范都是經過很多前輩總結的結果,我們只要遵從他們的設計思想去實現就好了。
參照已有規范,我定義了三個模塊化框架接口:
**
模塊定義接口:define(id, factory);
異步加載接口:require.async(ids, callback);
框架配置接口:require.config(options);
**
利用構建工具建立模塊依賴關系表,再將關系表注入到代碼中,調用require.config接口讓框架知道完整的依賴樹,從而實現require.async在異步加載模塊時能提前預知所有依賴的資源,一次性請求回來。
以上面的page.js依賴樹為例,構建工具會生成如下代碼:
當執行require.async('page.js', fn);語句時,框架查詢config.deps表,就能知道要發起一個這樣的combo請求:
http://www.example.com/f.js,c.js,d.js,e.js,a.js,b.js,page.js
從而實現按需加載和請求合并兩項性能優化需求。
根據這樣的設計思路,我請 @hinc 幫忙實現了這個框架,我告訴他,deps里不但會有js,還會有css,所以也要兼容一下。hinc果然是執行能力非常強的小伙伴,僅一個下午的時間就搞定了框架的實現,我們給這個框架取名為 scrat.js,僅有393行。
前面提到fis具有資源依賴聲明的編譯能力。因此只要工程師按照fis規定的書寫方式在代碼中聲明依賴關系,就能在構建的最后階段自動獲得fis系統整理好的依賴樹,然后對依賴的數據結構進行調整、輸出,滿足框架要求就搞定了!fis規定的資源依賴聲明方式為:在html中聲明依賴,在js中聲明依賴,在css中聲明依賴。
接下來,我要寫一個配置,將依賴關系表注入到代碼中。fis構建是分流程的,具體構建流程可以看這里。fis會在postpackager階段之前創建好完整的依賴樹表,我就在這個時候寫一個插件來處理即可。編輯fis-conf.js
我們準備一下項目代碼,看看構建的時候發生了什么:

執行fis release查看命令行輸出,可以看到consolog.log的內容為:
可以看到js和同名的css自動建立了依賴關系,這是fis默認進行的依賴聲明。有了這個表,我們就可以把它注入到代碼中了。我們為頁面準備一個替換用的鉤子,比如約定為FRAMEWORK_CONFIG
,這樣用戶就可以根據需要在合適的地方獲取并使用這些數據。模塊化框架的配置一般都是寫在非模塊化文件中的,比如html頁面里,所以我們應該只針對views目錄下的文件做這樣的替換就可以。所以我們需要給views下的文件進行一個標記,只有views下的html或js文件才需要進行依賴樹數據注入,具體的配置為:
我在views/index.html中寫了這樣的代碼:
執行 fis release -d ../release之后,得到構建后的內容為:
在調用 require.async('components/foo/foo.js')
之際,模塊化框架已經知道了這個foo.js依賴于bar.js、bar.css以及foo.css,因此可以發起兩個combo請求去加載所有依賴的js、css文件,完成后再執行回調。
現在模塊的id有一些問題,因為模塊發布會有版本號信息,因此模塊id也應該攜帶版本信息,從前面的依賴樹生成配置代碼中我們可以看到模塊id其實也是文件的一個屬性,因此我們可以在roadmap.path中重新為文件賦予id屬性,使其攜帶版本信息:
重新構建項目,我們得到了新的結果:
所有id都會被修改為我們指定的模式,這就是以文件為中心的編譯系統的威力。
以文件對象為中心構建系統應該通過配置指定文件的各種屬性。插件并不自己實現某種規范規定,而是讀取file對象的對應屬性值,這樣插件的職責單一,規范又能統一起來被用戶指定,為完整的前端開發體系設計奠定了堅實規范配置的基礎。
接下來還有一個問題,就是模塊名太長,開發中寫這么長的模塊名非常麻煩。我們可以借鑒流行的模塊化框架中常用的縮短模塊名手段——別名(alias)——來降低開發中模塊引用的成本。此外,目前的配置其實會針對所有文件生成依賴關系表,我們的開發概念定義只有components和component_modules目錄下的文件才是模塊化的,因此我們可以進一步的對文件進行分類,得到這樣配置規范:
然后我們為一些模塊id建立別名:
再次構建,在注入的代碼中就能看到alias字段了:
這樣,代碼中的 require('foo');就等價于 require('proj/1.0.5/foo/foo.js')了。
還剩最后一個小小的需求,就是希望能像寫nodejs一樣開發js模塊,也就是要求實現define的自動包裹功能,這個可以通過文件編譯的 postprocessor 插件完成。配置為:
所有在components目錄和component_modules目錄下的js文件都會被包裹define,并自動根據roadmap.path中的id配置進行模塊定義了。
回顧
剩下的幾個需求中有些是fis默認支持的,比如base64內嵌功能,圖片會先經過編譯流程,得到壓縮后的內容fis再對其進行base64化的內嵌處理。由于fis的內嵌功能支持任意文件的內嵌,所以,這個語言能力擴展可以同時解決前端模板和圖片base64內嵌需求,比如我們有這樣的代碼:
project
- components
- foo
- foo.js
- foo.css
- foo.handlebars
- foo.png
無需配置,既可以在js中嵌入資源,比如 foo.js 中可以這樣寫:
//依賴聲明
var bar = require('../bar/bar.js');
//把handlebars文件的字符串形式嵌入到js中
var text = __inline('foo.handlebars');
var tpl = Handlebars.compile(text);
exports.render = function(data){
return tpl(data);
};
//把圖片的base64嵌入到js中
var data = __inline('foo.png');
exports.getImage = function(){
var img = new Image();
img.src = data;
return img;
};
編譯后得到:
支持stylus也非常簡單,fis在 parser 階段處理非標準語言,這個階段可以把非標準的js(coffee/前端模板)、css(less/sass/stylus)、html(markdown)語言轉換為標準的js、css或html。處理之后那些文件還能和標準語言一起經歷預處理、語言能力擴展、后處理、校驗、測試、壓縮等階段。
所以,要支持stylus的編譯,只要在fis-conf.js中添加這樣的配置即可:
這樣我們項目中的*.styl后綴的文件都會被編譯為css內容,并且會在后面的流程中被當做css內容處理,比如壓縮、csssprite等。
文件監聽、自動刷新都是fis內置的功能,fis的release命令集合了所有編譯所需的參數,
這些參數是可以隨意組合的,比如我們想文件監聽、自動刷新,則使用:
fis release -wL
壓縮、打包、文件監聽、自動刷新、發布到output目錄,則使用:
fis release -opwLd output
構建工具不需要那么多命令,或者develop、release等不同狀態的配置文件,應該從命令行切換編譯參數,從而實現開發/上線構建模式的切換。
另外,fis是命令行工具,各種內置的插件也是完全獨立無環境依賴的,可以與ci平臺直接對接,并在各個主流操作系統下運行正常。
利用fis的內置的各種編譯功能,我們離目標又近了許多:
剩下兩個,我們可以通過擴展fis的命令行插件來實現。fis有11個編譯流程擴展點,還有一個命令行擴展點。要擴展命令行插件很簡單,只要我們將插件安裝到與fis同級的node_modules目錄下即可。比如:
node_modules - fis - fis-command-say
那么執行 fis say這個命令,就能調用到那個fis-command-say插件了。剩下的這個component模塊安裝,我就利用了這個擴展點,結合component開源的 component-installer 包,我就可以把component整合當前開發體系中,這里我們需要創建一個npm包來提供擴展,而不能直接在fis-conf.js中擴展命令行,插件代碼我就不貼了,可以看 這里。
眼前我們有了一個差不多100行的fis-conf.js文件,還有幾個插件,如果我把這樣一個零散的系統交付團隊使用,那么大家使用的步驟差不多是這樣的:
安裝fis,npm install -g fis
安裝component安裝用的命令行插件,npm insatll -g fis-command-component
安裝stylus編譯插件,npm install -g fis-parser-stylus
下載一份配置文件,fis-conf.js,修改里面的name、version配置
這種情況讓團隊用起來會有很多問題。首先,安裝過程太過麻煩,其次如果項目多,那么fis-conf.js不能同步升級,這是非常嚴重的問題。grunt的gruntfile.js也是如此。如果說有一個項目用了某套grunt配置感覺很爽,那么下個項目也想用這套方案,復制gruntfile.js是必須的操作,項目用的多了,同步gruntfile的成本就變得非常高了。
因此,fis提供了一種“包裝”的能力,它允許你將fis作為內核,包裝出一個新的命令行工具,這個工具內置了一些fis的配置,并且把所有命令行調用的參數傳遞給fis內核去處理。
我準備把這套系統打包為一個新的工具,給它取名為 scrat,也是一只松鼠。這個新工具的目錄結構是這樣的:
將這個npm包發布出來,我們就有了一個全新的開發工具,這個工具可以解決前面說的13項技術問題,并提供一套完整的集成解決方案,而你的團隊使用的時候,只有兩個步驟:
- 安裝這個工具,npm install -g scrat
- 項目配置只有兩項,name和version
使用新工具的命令、參數幾乎和fis完全一樣:
scrat release [options]
scrat server start
scrat install <name@version> [options]
而scrat這個工具所內置的配置將變成規范文檔描述給團隊同學,這套系統要比grunt那種松散的構建系統組成方式更容易被多個團隊、多個項目同時共享。
總結
不可否認,為大規模前端團隊設計集成解決方案需要花費非常多的心思。
如果說只是實現一個簡單的編譯+壓縮+文件監+聽自動刷新的常規構建系統,基于fis應該不超過1小時就能完成一個,但要實踐完整的前端集成解決方案,確實需要點時間。
如之前一篇 文章 所講,前端集成解決方案有8項技術要素,除了組件倉庫,其他7項對于企業級前端團隊來說,應該都需要完整實現的。即便暫時不想實現,也會隨著業務發展而被迫慢慢完善,這個完善過程是普適的。
對于前端集成解決方案的實踐,可以總結出這些設計步驟:
- 設計開發概念,定義開發資源的分類(模塊化/非模塊化)
- 設計開發目錄,降低開發、維護成本(開發規范)
- 根據運維和業務要求,設計部署規范(部署規范)
- 設計工具,完成開發目錄和部署目錄的轉換(開發-部署轉換)
- 設計模塊化框架,兼顧性能優化(開發框架)
- 擴展工具,支持開發框架的構建需求(框架構建需求)
- 流程整合(開發、測試、聯調、上線等流程接入)
我們可以看看業界已有團隊提出的各種解決方案,無不以這種思路來設計和發展的:
- seajs開發體系,支付寶團隊前端開發體系,以 spm 為構建和包管理工具
- fis-plus,百度絕大多數前端團隊使用的開發體系,以fis為構建工具內核,以lights為包管理工具
- edp,百度ecomfe前端開發體系,以 edp 為構建和包管理工具
- modjs,騰訊AlloyTeam團隊出品的開發體系
- yeoman,google出品的解決方案,以grunt為構建工具,bower為包管理工具
縱觀這些公司出品的前端集成解決方案,深入剖析其中的框架、規范、工具和流程,都可以發現一些共通的影子,設計思想殊途同歸,不約而同的朝著一種方向前進,那就是前端集成解決方案。嘗試將前端工程孤立的技術要素整合起來,解決常見的領域問題。
或許有人會問,不就是寫頁面么,用得著這么復雜?
在這里我不能給出肯定或者否定的答復。
因為單純從語言的角度來說,html、js、css(甚至有人認為css是數據結構,而非語言)確實是最簡單最容易上手的開發語言,不用模塊化、不用工具、不用壓縮,任何人都可以快速上手,完成一兩個功能簡單的頁面。所以說,在一般情況下,前端開發非常簡單。
在規模很小的項目中,前端技術要素彼此不會直接產生影響,因此無需集成解決方案。
但正是由于前端語言這種靈活松散的特點,使得前端項目規模在達到一定規模后,工程問題凸顯,成為發展瓶頸,各種技術要素彼此之間開始出現關聯,要用模塊化開發,就必須對應某個模塊化框架,用這個框架就必須對應某個構建工具,要用這個工具,就必須對應某個包管理工具……這個時候,完整實踐前端集成解決方案就成了不二選擇。
當前端項目達到一定規模后,工程問題將成為主要瓶頸,原來孤立的技術要素開始彼此產生影響,需要有人從比較高的角度去梳理、尋找適合自己團隊的集成解決方案。
所以會出現一些框架或工具在小項目中使用的好好的,一旦放到團隊里使用就非常困難的情況。
不知道fouber是怎么處理業務模塊與公共模塊的。比如5位開發承擔了站點的不同模塊開發(可以理解為5個頁面)。頁面都調用了公共的登錄模塊,發布的時候是獨自發布登錄的模塊呢?還是用一個公共的頁面片(類似apache的include)。如果是獨立發布,那登錄模塊改版,就涉及到5個人都要發。如果是用公共的頁面片,看你的文章也沒有體現出來。處理公共的模塊與業務,一直覺得很頭疼。編寫上還好說,發布最頭疼,除了要考慮同事間的協作,還得考慮性能
對于大型網站的維護,你這個需求其實很常見,而且也很迫切——我們希望系統由多個獨立的子系統組織形成,由多個團隊維護,每個團隊的代碼不用與其他團隊的業務代碼產生構建依賴,均能獨立構建獨立上線。
這些需求,也是通過基于表的靜態資源管理實現的。在百度的做法:
1. 目錄結構規劃
site是站點目錄,common、user、news、photo都是一個 子系統
(為例避免混淆,我們并不稱它為模塊,而是叫做子系統),每個這樣的子系統就是一個 git/svn 倉庫,由一個獨立的團隊維護,最終將所有子系統構建之后部署在一起,就得到了完整的站點。
以common子系統為例,其內部目錄結構大致為:
每個子系統都有一個 fis-conf.js
是我們的構建工具配置,你可以把它理解成是 gulpfile/gruntfile,widget放的是UI組件或js/css模塊,page放的是頁面,lib放的是第三方類庫以及一些非模塊化資源。
每個子系統構建之后,會產生一張資源表,比如上面這個common子系統,構建之后會得到一個 common-map.json的文件,這個文件中記錄了common子系統中所有js/css/模板的部署路徑和依賴關系,我在其他文章中也時常介紹過。
然后,給你展示一下我們這個common子系統構建之后的目錄結構:
每個子系統經過構建之后都會得到 static、template、map三個目錄,三個目錄中都以子系統名稱命名了一個目錄,里面存放了靜態資源、模板和資源表。
這樣,我們把所有子系統都構建一遍部署在一起后,就得到了這樣的一個完整的站點部署結果:
做到了以下幾點:
- 子系統的部署相互之間是不覆蓋的
- 每個子系統都會發布產生static、template、map三個目錄,其中static用于靜態資源部署,可以單獨拿出來部署到CDN源服務器,template和map要一起部署到webserver
當我們有了這樣的開發和部署規范以后,我們才方便談『如何跨子系統使用資源,去除構建依賴,并保證資源更新』。
這一切都是因為我們有資源表和基于表的資源管理框架?。。?/strong>
由于每個子系統會產生一張表,記錄了子系統內資源的依賴、打包(子系統內也可以實現資源合并打包,并記錄在表)和發布信息,這樣我們需要跨子系統調用資源的時候,就可以這樣做(假設我們在photo子系統中需要使用common子系統和user子系統的資源):
在photo子系統的js文件中跨子系統依賴資源:
/** * 系統:photo *
文件:widget/header/header.js
*/
var $ = require('common:lib/jquery');
var login = require('user:widget/login');...
在photo子系統的css文件中跨子系統依賴資源(@require是fis自定義的依賴聲明標識,用于構建工具識別):
/** * 系統:photo * 文件:widget/footer/footer.css * * @require common:lib/font-awesome * */.footer .fa { font-size: 12px;}
在photo子系統的后端模板文件中跨子系統調用組件(widget是擴展的模板引擎功能,類似include):
{*
系統:photo
文件:page/index/index.tpl*
}
<div class="photo-page-index">
<div class="photo-page-index_header">
{% widget id="common:widget/header" %}
</div>
<div class="photo-page-index_user-info">
{% widget id="user:widget/info" %}
</div>
</div>
我們修改了一下id的規范,在前面加上了 子系統名:
這樣的標識,表示是跨子系統的資源調用,比如需要common子系統的header組件,其id就是 common:widget/header
。
代碼設計成這樣之后,最后的關鍵就是寫資源管理框架了。
因為資源表中記錄了資源的依賴關系,資源依賴關系會帶上子系統的名稱前綴,所以,資源管理框架只需根據這個id去不同的map文件中查找資源即可,一般對于這樣的大型系統,我們都是把資源管理框架實現為服務端模板引擎的擴展,可以減少前端的負擔,有關這個資源管理框架的實現思路介紹,我其實還沒有完整發文介紹過,也是近期在醞釀的事情,并不復雜,只是要在這里全部展開多少有點不合適。
有了資源表、跨子系統資源依賴聲明規范、資源管理框架和一個簡單的構建工具之后,就完成你問題的要求了,構建依賴被消除的原因是資源管理框架讓依賴變成了運行時的。模板在服務端拼裝的過程中,資源管理框架實時讀取map目錄下的資源表,跨子系統找到資源的發布路徑、打包合并情況,然后動態生成資源的加載代碼,插入到html中;而每個子系統獨立上線之后,會更新資源表,讓新的部署立即生效,而不需要依賴構建解決資源更新問題。