> 原文出處:https://github.com/fouber/blog/issues/2
> 作者:[fouber](https://github.com/fouber)
目錄
[TOC=2,2]
# 前端開發體系建設日記
上周寫了一篇?[文章](https://github.com/fouber/blog/issues/1)?介紹前端集成解決方案的基本理論,很多同學看過之后大呼不過癮。
> 干貨?~~fuck things~~?在哪里!
本打算繼續完善理論鏈,形成前端工程的知識結構。但鑒于如今的快餐文化,po主決定還是先寫一篇實戰介紹,讓大家看到前端工程體系能為團隊帶來哪些好處,調起大家的胃口再說。
> ps: 寫完才發現這篇文章真的非常非常長,涵蓋了前端開發中的很多方面,希望大家能有耐心看完,相信一定會有所斬獲。。。
## 2014年02月12日 - 晴
新到松鼠團隊的第二天,小伙伴?[@nino](https://github.com/ninozhang)?找到我說
> nino: 視頻項目打算重新梳理一下,希望能引入新的技術體系,解決現有的一些問題。
po主不禁暗喜,好機會,這是我專業啊,藍翔技校-前端集成解決方案學院-自動化系-打包學專業的文憑不是白給的,于是自信滿滿的對nino說,有什么需求盡管提!
> nino: 我的需求并不多,就這么幾條~~
1. 模塊化開發。最好能像寫nodejs一樣寫js,很舒服。css最好也能來個模塊化管理!
2. 性能要好。模塊那么多,得有按需加載,請求不能太多。
3. 組件化開發。一個組件的js、css、模板最好都在一個目錄維護,維護起來方便。
4. 用?[handlebars](http://handlebarsjs.com/)?作為前端模板引擎。這個模板引擎不錯,logic-less(輕邏輯)。
5. 用?[stylus](http://learnboost.github.io/stylus/)?寫css挺方便,給我整一個。
6. 圖片base64嵌入。有些小圖可能要以base64的形式嵌入到頁面、js或者css中使用。嵌入之前記得壓縮圖片以減小體積。
7. js/css/圖片壓縮應該都沒問題吧。
8. 要能與公司的ci平臺集。工具里最好別依賴什么系統庫,ci的機器未必支持。
9. 開發體驗要好。文件監聽,瀏覽器自動刷新([livereload](http://www.livereload.com/))一個都不能少。
10. 我們用nodejs作為服務器,本地要能預覽,最好再能抓取線上數據,方便調試。
我倒吸一口涼氣,但表面故作鎮定的說:恩,確實不多,讓我們先來看看第一個需求。。。
還沒等我說完,nino打斷我說
> nino: 橋豆麻袋(稍等),還有一個最重要的需求!
~~~
松鼠公司的松鼠瀏覽器你知道吧,恩,它有很多個版本的樣子。
我希望代碼發布后能按照版本部署,不要彼此覆蓋。
舉個例子,代碼部署結構可能是這樣的:
release/
- public/
- 項目名
- 1.0.0/
- 1.0.1/
- 1.0.2/
- 1.0.2-alpha/
- 1.0.2-beta/
讓歷史瀏覽器瀏覽歷史版本,沒事還能做個灰度發布,ABTest啥的,多好!
此外,我們將來會有多個項目使用這套開發模式,希望能共用一些組件或者模
塊,產品也會公布一些api模塊給第三方使用,所以共享模塊功能也要加上。
~~~
總的來說,還要追加兩個部署需求:
1. 按版本部署,采用非覆蓋式發布
2. 允許第三方引用項目公共模塊
> nino: 怎么樣,不算復雜吧,這個項目很趕,`3天`搞定怎么樣?
我凝望著會議室白板上的這些需求,正打算爭辯什么,一扭頭發現nino已經不見了。。。正在沮喪之際,小伙伴?[@hinc](https://github.com/hinc)?過來找我,跟他大概講了一下nino的需求,正想跟他抱怨工期問題時,hinc卻說
> hinc: 恩,這正是我們需要的開發體系,不過我這里還有一個需求。。。
1. 我們之前積累了一些業務可以共用的模塊,放在了公司內的gitlab上,采用?[component](http://component.io/)?作為發布規范,能不能再加上這個組件倉庫的支持?
3天時間,13項前端技術元素,靠譜么。。。
## 2014年02月13日 - 多云
一覺醒來,輕松了許多,但還有任務在身,不敢有半點怠慢。整理一下昨天的需求,我們來做一個簡單的劃分。
* 規范
* 開發規范
* 模塊化開發,js模塊化,css模塊化,像nodejs一樣編碼
* 組件化開發,js、css、handlebars維護在一起
* 部署規范
* 采用nodejs后端,基本部署規范應該參考?[express](http://expressjs.com/)?項目部署
* 按版本號做非覆蓋式發布
* 公共模塊可發布給第三方共享
* 框架
* js模塊化框架,支持請求合并,按需加載等性能優化點
* 工具
* 可以編譯stylus為css
* 支持js、css、圖片壓縮
* 允許圖片壓縮后以base64編碼形式嵌入到css、js或html中
* 與ci平臺集成
* 文件監聽、瀏覽器自動刷新
* 本地預覽、數據模擬
* 倉庫
* 支持component模塊安裝和使用
這樣一套規范、框架、工具和倉庫的開發體系,服從我之前介紹的?[前端集成解決方案](https://github.com/fouber/blog/issues/1)?的描述。前端界每天都團隊在設計和實現這類系統,它們其實是有規律可循的。百度出品的?[fis](https://github.com/fis-dev/fis/wiki)?就是一個能幫助快速搭建前端集成解決方案的工具。使用fis我應該可以在3天之內完成這些任務。
> ps: 這不是一篇關于fis的軟文,如果這樣的一套系統基于grunt實現相信會有非常大量的開發工作,3天完成幾乎是不可能的任務。
不幸的是,現在fis官網所介紹的?**并不是**?fis,而是一個叫?[fis-plus](https://github.com/fex-team/fis-plus)?的項目,該項目并不像字面理解的那樣是fis的加強版,而是在fis的基礎上定制的一套面向百度前端團隊的解決方案,以php為后端語言,跟smarty有較強的綁定關系,有著?`19項`?技術要素,密切配合百度現行技術選型。絕大多數非百度前端團隊都很難完整接受這19項技術選型,尤其是其中的部署、框架規范,跟百度前端團隊相關開發規范、部署規范、以及php、smarty等有著較深的綁定關系。
因此如果你的團隊用的不是?`php后端`?&&?`smarty模板`?&&?`modjs模塊化框架`?&&?`bingo框架`?的話,請查看?[fis的文檔](https://github.com/fis-dev/fis/wiki),或許不會有那么多困惑。
> ps: fis是一個構建系統內核,很好的抽象了前端集成解決方案所需的通用工具需求,本身不與任何后端語言綁定。而基于fis實現的具體解決方案就會有具體的規范和技術選型了。
言歸正傳,讓我們基于?[fis](https://github.com/fis-dev/fis/)?開始實踐這套開發體系吧!
### 0\. 開發概念定義
前端開發體系設計第一步要定義開發概念。開發概念是指針對開發資源的分類概念。開發概念的確立,直接影響到規范的定制。比如,傳統的開發概念一般是按照文件類型劃分的,所以傳統前端項目會有這樣的目錄結構:
* js:放js文件
* css:放css文件
* images:放圖片文件
* html:放html文件
這樣確實很直接,任何智力健全的人都知道每個文件該放在哪里。但是這樣的開發概念劃分將給項目帶來較高的維護成本,并為項目臃腫埋下了工程隱患,理由是:
1. 如果項目中的一個功能有了問題,維護的時候要在js目錄下找到對應的邏輯修改,再到css目錄下找到對應的樣式文件修改一下,如果圖片不對,還要再跑到images目錄下找對應的開發資源。
2. images下的文件不知道哪些圖片在用,哪些已經廢棄了,誰也不敢刪除,文件越來越多。
> ps: 除非你的團隊只有1-2個人,你的項目只有很少的代碼量,而且不用關心性能和未來的維護問題,否則,以文件為依據設計的開發概念是應該被拋棄的。
以我個人的經驗,更傾向于具有一定語義的開發概念。綜合前面的需求,我為這個開發體系確定了3個開發資源概念:
* 模塊化資源:js模塊、css模塊或組件
* 頁面資源:網站html或后端模板頁面,引用模塊化框架,加載模塊
* 非模塊化資源:并不是所有的開發資源都是模塊化的,比如提供模塊化框架的js本身就不能是一個模塊化的js文件。嚴格上講,頁面也屬于一種非模塊化的靜態資源。
> ps: 開發概念越簡單越好,前面提到的fis-plus也有類似的開發概念,有組件或模塊(widget),頁面(page),測試數據(test),非模塊化靜態資源(static)。有的團隊在模塊之中又劃分出api模塊和ui模塊(組件)兩種概念。
### 1\. 開發目錄設計
基于開發概念的確立,接下來就要確定目錄規范了。我通常會給每種開發資源的目錄取一個有語義的名字,三種資源我們可以按照概念直接定義目錄結構為:
~~~
project
- modules 存放模塊化資源
- pages 存放頁面資源
- static 存放非模塊化資源
~~~
這樣劃分目錄確實直觀,但結合前面hinc說過的,希望能使用component倉庫資源,因此我決定將模塊化資源目錄命名為`components`,得到:
~~~
project
- components 存放模塊化資源
- pages 存放頁面資源
- static 存放非模塊化資源
~~~
而nino又提到過模塊資源分為項目模塊和公共模塊,以及hinc提到過希望能從component安裝一些公共組件到項目中,因此,一個components目錄還不夠,想到nodejs用node_modules作為模塊安裝目錄,因此我在規范中又追加了一個?`component_modules`?目錄,得到:
~~~
project
- component_modules 存放外部模塊資源
- components 存放項目模塊資源
- pages 存放頁面資源
- static 存放非模塊化資源
~~~
nino說過今后大多數項目采用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\. 部署目錄設計
托nino的福,咱們的部署策略將會非常復雜,根據要求,一個完整的部署結果應該是這樣的目錄結構:
~~~
release
- public
- 項目名
- 1.0.0 1.0.0版本的靜態資源都構建到這里
- 1.0.1 1.0.1版本的靜態資源都構建到這里
- 1.0.2 1.0.2版本的靜態資源都構建到這里
...
- views
- 項目名
- 1.0.0 1.0.0版本的后端模板都構建到這里
- 1.0.1 1.0.1版本的后端模板都構建到這里
- 1.0.2 1.0.2版本的后端模板都構建到這里
...
~~~
由于還要部署一些可以被第三方使用的模塊,public下只有項目名的部署還不夠,應改把模塊化文件單獨發布出來,得到這樣的部署結構:
~~~
release
- public
- component_modules 模塊化資源都部署到這個目錄下
- module_a
- 1.0.0
- module_a.js
- module_a.css
- module_a.png
- 1.0.1
- 1.0.2
...
- 項目名
- 1.0.0 1.0.0版本的靜態資源都構建到這里
- 1.0.1 1.0.1版本的靜態資源都構建到這里
- 1.0.2 1.0.2版本的靜態資源都構建到這里
...
- views
- 項目名
- 1.0.0 1.0.0版本的后端模板都構建到這里
- 1.0.1 1.0.1版本的后端模板都構建到這里
- 1.0.2 1.0.2版本的后端模板都構建到這里
...
~~~
由于?`component_modules`?這個名字太長了,如果部署到這樣的路徑下,url會很長,這也是一個優化點,因此最終決定部署結構為:
~~~
release
- public
- c 模塊化資源都部署到這個目錄下
- 公共模塊
- 版本號
- 項目名
- 版本號
- 項目名
- 版本號 非模塊化資源都部署到這個目錄下
- views
- 項目名
- 版本號 后端模板都構建到這個目錄下
~~~
插一句,并不是所有團隊都會有這么復雜的部署要求,這和松鼠團隊的業務需求有關,但我相信這個例子也不會是最復雜的。每個團隊都會有自己的運維需求,前端資源部署經常牽連到公司技術架構,因此很多前端項目的開發目錄結構會和部署要求保持一致。這也為項目間模塊的復用帶來了成本,因為代碼中寫的url通常是部署后的路徑,遷移之后就可能失效了。
> 解耦開發規范和部署規范是前端開發體系的設計重點。
好了,去吃個午飯,下午繼續。。。
### 3\. 配置fis連接開發規范和部署規范
我準備了一個樣例項目:
~~~
project
- views
- logo.png
- index.html
- fis-conf.js
- README.md
~~~
`fis-conf.js`是fis工具的配置文件,接下來我們就要在這里進行構建配置了。雖然開發規范和部署規范十分復雜,但好在fis有一個非常強大的?[roadmap.path](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#roadmappath)?功能,專門用于分類文件、調整發布結構、指定文件的各種屬性等功能實現。
> 所謂構建,其核心任務就是將文件按照某種規則進行分類(以文件后綴分類,以模塊化/非模塊化分類,以前端/后端代碼分類),然后針對不同的文件做不同的構建處理。
閑話少說,我們先來看一下基本的配置,在?`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](https://github.com/fex-team/fis-kernel/blob/master/lib/file.js)?對象,創建File對象時,按照roadmap.path的配置逐個匹配文件路徑,匹配成功則把除reg之外的其他屬性賦給File對象,fis中各種處理環節及插件都會讀取所需的文件對象的屬性值,而不會自己定義規范。有關roadmap.path的工作原理可以看[這里](https://github.com/fex-team/fis-kernel/blob/master/lib/file.js#L156)?以及?[這里](https://github.com/fex-team/fis-kernel/blob/master/lib/uri.js#L48-L70)。
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屬性值中引用分組是非常方便的。正則匹配 + 捕獲分組,成為目錄規范配置的強有力工具:
[](https://box.kancloud.cn/2015-09-06_55ec10705d574.png)
在上面的配置中,版本號被寫到了匹配規則里,這樣非常不方便工程師在迭代的過程中升級項目版本。我們應該將版本號、項目名稱等配置獨立出來管理。好在roadmap.path還有讀取其他配置的能力,修改上面的配置,我們得到:
~~~
//開發部署規范配置
fis.config.set('roadmap.path', [
{
reg : '**.md', //所有md后綴的文件
release : false //不發布
},
{
reg : /^\/views\/(.*)$/i,
//使用${xxx}引用fis.config的其他配置項
release : '/public/${name}/${version}/$1'
}
]);
//項目配置,將name、version獨立配置,統管全局
fis.config.set('name', 'proj');
fis.config.set('version', '1.0.0');
~~~
fis的配置系統非常靈活,除了?[文檔](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI)?中提到的配置節點,其他配置用戶可以隨便定義使用。比如配置的roadmap是系統保留的,而name、version都是用戶自己隨便指定的。fis系統保留的配置節點只有6個,包括:
1. [project](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#projectcharset)(系統配置)
2. [modules](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#modulesparser)(構建流程配置)
3. [settings](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#settings)(插件配置)
4. [roadmap](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#roadmappath)(目錄規范與域名配置)
5. [deploy](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#deploy)(部署目標配置)
6. [pack](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#pack)(打包配置)
完成第一份配置之后,我們來看一下效果。
~~~
cd project
fis release --dest ../release
~~~
進入到項目目錄,然后使用fis release命令,對項目進行構建,用?`--dest <path>`?參數指定編譯結果的產出路徑,可以看到部署后的結果:
[](https://box.kancloud.cn/2015-09-06_55ec1072735f1.png%3Fv)
> ps: fis release會將處理后的結果發布到源碼目錄之外的其他目錄里,以保持源碼目錄的干凈。
fis系統的強大之處在于當你調整了部署規范之后,fis會識別所有資源定位標記,將他們修改為對應的部署路徑。
[](https://box.kancloud.cn/2015-09-06_55ec1073e6817.png)
fis的文件系統設計決定了配置開發規范的成本非常低。fis構建核心有三個超級正則,用于識別資源定位標記,把用戶的開發規范和部署規范通過配置完整連接起來,具體實現可以看[這里](https://github.com/fex-team/fis-kernel/blob/master/lib/compile.js#L376-L465)。
> 不止html,fis為前端三種領域語言都準備了資源定位識別標記,更多文檔可以看這里:[在html中定位資源](https://github.com/fex-team/fis/wiki/%E5%AE%9A%E4%BD%8D%E8%B5%84%E6%BA%90#%E5%9C%A8html%E4%B8%AD%E5%AE%9A%E4%BD%8D%E8%B5%84%E6%BA%90),[在js中定位資源](https://github.com/fex-team/fis/wiki/%E5%AE%9A%E4%BD%8D%E8%B5%84%E6%BA%90#%E5%9C%A8js%E4%B8%AD%E5%AE%9A%E4%BD%8D%E8%B5%84%E6%BA%90),[在css中定位資源](https://github.com/fex-team/fis/wiki/%E5%AE%9A%E4%BD%8D%E8%B5%84%E6%BA%90#%E5%9C%A8css%E4%B8%AD%E5%AE%9A%E4%BD%8D%E8%B5%84%E6%BA%90)
接下來,我們修改一下項目版本配置,再發布一下看看效果:
~~~
fis.config.set('version', '1.0.1');
~~~
再次執行:
~~~
cd project
fis release --dest ../release
~~~
得到:
[](https://box.kancloud.cn/2015-09-06_55ec107721e0d.png)
至此,我們已經基本解決了開發和部署直接的目錄規范問題,這里我需要加快一些步伐,把其他目錄的部署規范也配置好,得到一個相對比較完整的結果:
~~~
fis.config.set('roadmap.path', [
{
//md后綴的文件不發布
reg : '**.md',
release : false
},
{
//component_modules目錄下的代碼,由于component規范,已經有了版本號
//我將它們直接發送到public/c目錄下就好了
reg : /^\/component_modules\/(.*)$/i,
release : '/public/c/$1'
},
{
//項目模塊化目錄沒有版本號結構,用全局版本號控制發布結構
reg : /^\/components\/(.*)$/i,
release : '/public/c/${name}/${version}/$1'
},
{
//views目錄下的文件發布到【public/項目名/版本】目錄下
reg : /^\/views\/(.*)$/,
release : '/public/${name}/${version}/$1'
},
{
//其他文件就不屬于前端項目了,比如nodejs的后端代碼
//不處理這些文件的資源定位替換(useStandard: false)
//也不用對這些資源進行壓縮(useOptimizer: false)
reg : '**',
useStandard : false,
useOptimizer : false
}
]);
fis.config.set('name', 'proj');
fis.config.set('version', '1.0.2');
~~~
我構造了一個相對完整的目錄結構,然后進行了一次構建,效果還不錯:
[](https://box.kancloud.cn/2015-09-06_55ec107846f37.png)
不管部署規則多么復雜都不用擔心,有fis強大的資源定位系統幫你在開發規范和部署規范之間建立聯系,設計開發體系不在受制于工具的實現能力。
> 你可以盡情發揮想象力,設計出最優雅最合理的開發規范和最高效最貼合公司運維要求的部署規范,最終用fis的roadmap.path功能將它們連接起來,實現完美轉換。
>
> fis的roadmap功能實際上提供了項目代碼與部署規范解耦的能力。
從前面的例子可以看出,開發使用相對路徑即可,fis會在構建時會根據fis-conf.js中的配置完成開發路徑到部署路徑的轉換工作。這意味著在fis體系下開發的模塊將具有天然的可移植性,既能滿足不同項目的不同部署需求,又能允許開發中使用相對路徑進行資源定位,工程師再不用把部署路徑寫到代碼中了。
愉快的一天就這么過去了,睡覺!
## 2014年02月14日 - 陰轉多云
每到周五總是非常愜意的感覺,不管這一周多么辛苦,周五就是一個解脫,更何況今天還是個特別的日子——情人節!
昨天主要解決了開發概念、開發目錄規范、部署目錄規范以及初步的fis-conf.js配置。今天要進行前端開發體系設計的關鍵任務——模塊化框架。
> 模塊化框架是前端開發體系中最為核心的環節。
模塊化框架肩負著模塊管理、資源加載、性能優化(按需,請求合并)等多種重要職責,同時它也是組件開發的基礎框架,因此模塊化框架設計的好壞直接影響到開發體系的設計質量。
很遺憾的說,現在市面上已有的模塊化框架都沒能很好的處理模塊管理、資源加載和性能優化三者之間的關系。這倒不是框架設計的問題,而是由前端領域語言特殊性決定的。框架設計者一般在思考模塊化框架時,通常站在純前端運行環境角度考慮,基本功能都是用原生js實現的,因此一個模塊化開發的關鍵問題不能被很好的解決。這個關鍵問題就是`依賴聲明`。
以?[seajs](http://seajs.org/)?為例(無意冒犯),seajs采用運行時分析的方式實現依賴聲明識別,并根據依賴關系做進一步的模塊加載。比如如下代碼:
~~~
define(function(require) {
var foo = require("foo");
//...
});
~~~
當seajs要執行一個模塊的factory函數之前,會先分析函數體中的`require`書寫,具體代碼在[這里](https://github.com/seajs/seajs/blob/master/src/module.js#L286)和[這里](https://github.com/seajs/seajs/blob/master/src/util-deps.js),大概的代碼邏輯如下:
~~~
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模塊依賴關系樹:
[](https://box.kancloud.cn/2015-09-06_55ec107a6159d.png)
> ps: 圖片來源?[@raphealguo](http://weibo.com/guozhiw)
采用seajs-combo插件之后,靜態資源請求的效果是這樣的:
1. [http://www.example.com/page.js](http://www.example.com/page.js)
2. [http://www.example.com/a.js,b.js](http://www.example.com/a.js,b.js)
3. [http://www.example.com/c.js,d.js,e.js](http://www.example.com/c.js,d.js,e.js)
4. [http://www.example.com/f.js](http://www.example.com/f.js)
工作過程是
1. 框架先請求了入口文件page.js
2. 加載完成后分析factory函數,發現依賴了a.js和b.js,然后發起一個combo請求,加載a.js和b.js
3. a.js和b.js加載完成后又進一步分析源碼,才發現還依賴了c.js、d.js和e.js,再發起請求加載這三個文件
4. 完成c、d、e的加載之后,再分析,發現f.js依賴,再請求
5. 完成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.config({
deps : {
'page.js' : [ 'a.js', 'b.js' ],
'a.js' : [ 'c.js' ],
'b.js' : [ 'd.js', 'e.js' ],
'c.js' : [ 'f.js' ],
'd.js' : [ 'f.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](http://www.example.com/f.js,c.js,d.js,e.js,a.js,b.js,page.js)
從而實現`按需加載`和`請求合并`兩項性能優化需求。
根據這樣的設計思路,我請?[@hinc](https://github.com/hinc)?幫忙實現了這個框架,我告訴他,deps里不但會有js,還會有css,所以也要兼容一下。hinc果然是執行能力非常強的小伙伴,僅一個下午的時間就搞定了框架的實現,我們給這個框架取名為?[scrat.js](https://github.com/scrat-team/scrat.js/blob/master/scrat.js),僅有393行。
前面提到fis具有資源依賴聲明的編譯能力。因此只要工程師按照fis規定的書寫方式在代碼中聲明依賴關系,就能在構建的最后階段自動獲得fis系統整理好的依賴樹,然后對依賴的數據結構進行調整、輸出,滿足框架要求就搞定了!fis規定的資源依賴聲明方式為:[在html中聲明依賴](https://github.com/fex-team/fis/wiki/%E5%9C%A8html%E4%B8%AD%E5%A3%B0%E6%98%8E%E4%BE%9D%E8%B5%96),[在js中聲明依賴](https://github.com/fex-team/fis/wiki/%E5%9C%A8js%E4%B8%AD%E5%A3%B0%E6%98%8E%E4%BE%9D%E8%B5%96),[在css中聲明依賴](https://github.com/fex-team/fis/wiki/%E5%9C%A8css%E4%B8%AD%E5%A3%B0%E6%98%8E%E4%BE%9D%E8%B5%96)。
接下來,我要寫一個配置,將依賴關系表注入到代碼中。fis構建是分流程的,具體構建流程可以看[這里](https://github.com/fex-team/fis/wiki/%E8%BF%90%E8%A1%8C%E5%8E%9F%E7%90%86)。fis會在`postpackager`階段之前創建好完整的依賴樹表,我就在這個時候寫一個插件來處理即可。
編輯`fis-conf.js`
~~~
//postpackager插件接受4個參數,
//ret包含了所有項目資源以及資源表、依賴樹,其中包括:
// ret.src: 所有項目文件對象
// ret.pkg: 所有項目打包生成的額外文件
// reg.map: 資源表結構化數據
//其他參數暫時不用管
var createFrameworkConfig = function(ret, conf, settings, opt){
//創建一個對象,存放處理后的配置項
var map = {};
//依賴樹數據
map.deps = {};
//遍歷所有項目文件
fis.util.map(ret.src, function(subpath, file){
//文件的依賴數據就在file對象的requires屬性中,直接賦值即可
if(file.requires && file.requires.length){
map.deps[file.id] = file.requires;
}
});
console.log(map.deps);
};
//在modules.postpackager階段處理依賴樹,調用插件函數
fis.config.set('modules.postpackager', [createFrameworkConfig]);
~~~
我們準備一下項目代碼,看看構建的時候發生了什么:
[](https://box.kancloud.cn/2015-09-06_55ec107b47c7c.png)
執行fis release查看命令行輸出,可以看到consolog.log的內容為:
~~~
{
deps: {
'components/bar/bar.js': [
'components/bar/bar.css'
],
'components/foo/foo.js': [
'components/bar/bar.js',
'components/foo/foo.css'
]
}
}
~~~
可以看到js和同名的css自動建立了依賴關系,這是fis默認進行的依賴聲明。有了這個表,我們就可以把它注入到代碼中了。我們為頁面準備一個替換用的鉤子,比如約定為`__FRAMEWORK_CONFIG__`,這樣用戶就可以根據需要在合適的地方獲取并使用這些數據。模塊化框架的配置一般都是寫在非模塊化文件中的,比如html頁面里,所以我們應該只針對views目錄下的文件做這樣的替換就可以。所以我們需要給views下的文件進行一個標記,只有views下的html或js文件才需要進行依賴樹數據注入,具體的配置為:
~~~
fis.config.set('roadmap.path', [
{
reg : '**.md',
release : false
},
{
reg : /^\/component_modules\/(.*)$/i,
release : '/public/c/$1'
},
{
reg : /^\/components\/(.*)$/i,
release : '/public/c/${name}/${version}/$1'
},
{
reg : /^\/views\/(.*)$/,
//給views目錄下的文件加一個isViews屬性標記,用以標記文件分類
//我們可以在插件中拿到文件對象的這個值
isViews : true,
release : '/public/${name}/${version}/$1'
},
{
reg : '**',
useStandard : false,
useOptimizer : false
}
]);
var createFrameworkConfig = function(ret, conf, settings, opt){
var map = {};
map.deps = {};
fis.util.map(ret.src, function(subpath, file){
if(file.requires && file.requires.length){
map.deps[file.id] = file.requires;
}
});
//把配置文件序列化
var stringify = JSON.stringify(map, null, opt.optimize ? null : 4);
//再次遍歷文件,找到isViews標記的文件
//替換里面的__FRAMEWORK_CONFIG__鉤子
fis.util.map(ret.src, function(subpath, file){
//有isViews標記,并且是js或者html類文件,才需要做替換
if(file.isViews && (file.isJsLike || file.isHtmlLike)){
var content = file.getContent();
//替換文件內容
content = content.replace(/\b__FRAMEWORK_CONFIG__\b/g, stringify);
file.setContent(content);
}
});
};
fis.config.set('modules.postpackager', [createFrameworkConfig]);
//項目配置
fis.config.set('name', 'proj'); //將name、version獨立配置,統管全局
fis.config.set('version', '1.0.3');
~~~
我在`views/index.html`中寫了這樣的代碼:
~~~
<!doctype html>
<html>
<head>
<title>hello</title>
</head>
<body>
<script type="text/javascript" src="scrat.js"></script>
<script type="text/javascript">
require.config(__FRAMEWORK_CONFIG__);
require.async('components/foo/foo.js', function(foo){
//todo
});
</script>
</body>
</html>
~~~
執行?`fis release -d ../release`?之后,得到構建后的內容為:
~~~
<!doctype html>
<html>
<head>
<title>hello</title>
</head>
<body>
<script type="text/javascript" src="/public/proj/1.0.3/scrat.js"></script>
<script type="text/javascript">
require.config({
"deps": {
"components/bar/bar.js": [
"components/bar/bar.css"
],
"components/foo/foo.js": [
"components/bar/bar.js",
"components/foo/foo.css"
]
}
});
require.async('components/foo/foo.js', function(foo){
//todo
});
</script>
</body>
</html>
~~~
在調用?`require.async('components/foo/foo.js')`?之際,模塊化框架已經知道了這個foo.js依賴于bar.js、bar.css以及foo.css,因此可以發起兩個combo請求去加載所有依賴的js、css文件,完成后再執行回調。
現在模塊的id有一些問題,因為模塊發布會有版本號信息,因此模塊id也應該攜帶版本信息,從前面的依賴樹生成配置代碼中我們可以看到模塊id其實也是文件的一個屬性,因此我們可以在roadmap.path中重新為文件賦予id屬性,使其攜帶版本信息:
~~~
fis.config.set('roadmap.path', [
{
reg : '**.md',
release : false,
isHtmlLike : true
},
{
reg : /^\/component_modules\/(.*)$/i,
//追加id屬性
id : '$1',
release : '/public/c/$1'
},
{
reg : /^\/components\/(.*)$/i,
//追加id屬性,id為【項目名/版本號/文件路徑】
id : '${name}/${version}/$1',
release : '/public/c/${name}/${version}/$1'
},
{
reg : /^\/views\/(.*)$/,
//給views目錄下的文件加一個isViews屬性標記,用以標記文件分類
//我們可以在插件中拿到文件對象的這個值
isViews : true,
release : '/public/${name}/${version}/$1'
},
{
reg : '**',
useStandard : false,
useOptimizer : false
}
]);
~~~
重新構建項目,我們得到了新的結果:
~~~
<!doctype html>
<html>
<head>
<title>hello</title>
</head>
<body>
<img src="/public/proj/1.0.4/logo.png"/>
<script type="text/javascript" src="/public/proj/1.0.4/scrat.js"></script>
<script type="text/javascript">
require.config({
"deps": {
"proj/1.0.4/bar/bar.js": [
"proj/1.0.4/bar/bar.css"
],
"proj/1.0.4/foo/foo.js": [
"proj/1.0.4/bar/bar.js",
"proj/1.0.4/foo/foo.css"
]
}
});
require.async('proj/1.0.4/foo/foo.js', function(foo){
//todo
});
</script>
</body>
</html>
~~~
you see?所有id都會被修改為我們指定的模式,這就是以文件為中心的編譯系統的威力。
> 以文件對象為中心構建系統應該通過配置指定文件的各種屬性。插件并不自己實現某種規范規定,而是讀取file對象的對應屬性值,這樣插件的職責單一,規范又能統一起來被用戶指定,為完整的前端開發體系設計奠定了堅實規范配置的基礎。
接下來還有一個問題,就是模塊名太長,開發中寫這么長的模塊名非常麻煩。我們可以借鑒流行的模塊化框架中常用的縮短模塊名手段——別名(alias)——來降低開發中模塊引用的成本。此外,目前的配置其實會針對所有文件生成依賴關系表,我們的開發概念定義只有components和component_modules目錄下的文件才是模塊化的,因此我們可以進一步的對文件進行分類,得到這樣配置規范:
~~~
fis.config.set('roadmap.path', [
{
reg : '**.md',
release : false,
isHtmlLike : true
},
{
reg : /^\/component_modules\/(.*)$/i,
id : '$1',
//追加isComponentModules標記屬性
isComponentModules : true,
release : '/public/c/$1'
},
{
reg : /^\/components\/(.*)$/i,
id : '${name}/${version}/$1',
//追加isComponents標記屬性
isComponents : true,
release : '/public/c/${name}/${version}/$1'
},
{
reg : /^\/views\/(.*)$/,
isViews : true,
release : '/public/${name}/${version}/$1'
},
{
reg : '**',
useStandard : false,
useOptimizer : false
}
]);
~~~
然后我們為一些模塊id建立別名:
~~~
var createFrameworkConfig = function(ret, conf, settings, opt){
var map = {};
map.deps = {};
//別名收集表
map.alias = {};
fis.util.map(ret.src, function(subpath, file){
//添加判斷,只有components和component_modules目錄下的文件才需要建立依賴樹或別名
if(file.isComponents || file.isComponentModules){
//判斷一下文件名和文件夾是否同名,如果同名則建立一個別名
var match = subpath.match(/^\/components\/(.*?([^\/]+))\/\2\.js$/i);
if(match && match[1] && !map.alias.hasOwnProperty(match[1])){
map.alias[match[1]] = file.id;
}
if(file.requires && file.requires.length){
map.deps[file.id] = file.requires;
}
}
});
var stringify = JSON.stringify(map, null, opt.optimize ? null : 4);
fis.util.map(ret.src, function(subpath, file){
if(file.isViews && (file.isJsLike || file.isHtmlLike)){
var content = file.getContent();
content = content.replace(/\b__FRAMEWORK_CONFIG__\b/g, stringify);
file.setContent(content);
}
});
};
fis.config.set('modules.postpackager', [createFrameworkConfig]);
~~~
再次構建,在注入的代碼中就能看到alias字段了:
~~~
require.config({
"deps": {
"proj/1.0.5/bar/bar.js": [
"proj/1.0.5/bar/bar.css"
],
"proj/1.0.5/foo/foo.js": [
"proj/1.0.5/bar/bar.js",
"proj/1.0.5/foo/foo.css"
]
},
"alias": {
"bar": "proj/1.0.5/bar/bar.js",
"foo": "proj/1.0.5/foo/foo.js"
}
});
~~~
這樣,代碼中的?`require('foo');`?就等價于?`require('proj/1.0.5/foo/foo.js');`了。
還剩最后一個小小的需求,就是希望能像寫nodejs一樣開發js模塊,也就是要求實現define的自動包裹功能,這個可以通過文件編譯的?[postprocessor](https://github.com/fex-team/fis/wiki/%E9%85%8D%E7%BD%AEAPI#modulespostprocessor)?插件完成。配置為:
~~~
//在postprocessor對所有js后綴的文件進行內容處理:
fis.config.set('modules.postprocessor.js', function(content, file){
//只對模塊化js文件進行包裝
if(file.isComponents || file.isComponentModules){
content = 'define("' + file.id +
'", function(require,exports,module){' +
content + '});';
}
return content;
});
~~~
所有在components目錄和component_modules目錄下的js文件都會被包裹define,并自動根據roadmap.path中的id配置進行模塊定義了。
最煎熬的一天終于過去了,睡一覺,擁抱一下周末。
## 2014年02月15日 - 超晴
周末的天氣非常好哇,一覺睡到中午才起,這么好的天氣寫碼豈不是很loser?!
## 2014年02月16日 - 小雨
居然浪費了一天,剩下的時間不多了,今天要抓緊啊!!!
讓我們來回顧一下已經完成了哪些工作:
* 規范
* 開發規范
* ~~模塊化開發,js模塊化,css模塊化,像nodejs一樣的模塊化開發~~
* ~~組件化開發,js、css、~~handlebars維護在一起
* 部署規范
* ~~采用nodejs后端,基本部署規范應該參考?[express](http://expressjs.com/)?項目部署~~
* ~~按版本號做非覆蓋式發布~~
* ~~公共模塊可發布給第三方共享~~
* 框架
* ~~js模塊化框架,支持請求合并,按需加載等性能優化點~~
* 工具
* 可以編譯stylus為css
* 支持js、css、圖片壓縮
* 允許圖片壓縮后以base64編碼形式嵌入到css、js或html中
* 與ci平臺集成
* 文件監聽、瀏覽器自動刷新
* 本地預覽、數據模擬
* 倉庫
* 支持component模塊安裝和使用
剩下的幾個需求中有些是fis默認支持的,比如base64內嵌功能,圖片會先經過編譯流程,得到壓縮后的內容fis再對其進行base64化的內嵌處理。由于fis的內嵌功能支持任意文件的內嵌,所以,這個語言能力擴展可以同時解決前端模板和圖片base64內嵌需求,比如我們有這樣的代碼:
~~~
project
- components
- foo
- foo.js
- foo.css
- foo.handlebars
- foo.png
~~~
無需配置,既可以[在js中嵌入資源](https://github.com/fex-team/fis/wiki/%E5%9C%A8js%E4%B8%AD%E5%B5%8C%E5%85%A5%E8%B5%84%E6%BA%90),比如 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;
};
~~~
編譯后得到:
~~~
define("proj/1.0.5/foo/foo.js", function(require,exports,module){
//依賴聲明
var bar = require('proj/1.0.5/bar/bar.js');
//把handlebars文件的字符串形式嵌入到js中
var text = "<h1>{{title}}</h1>";
var tpl = Handlebars.compile(text);
exports.render = function(data){
return tpl(data);
};
//把圖片的base64嵌入到js中
var data = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoA...';
exports.getImage = function(){
var img = new Image();
img.src = data;
return img;
};
});
~~~
支持stylus也非常簡單,fis在?[parser](https://github.com/fouber/blog/issues/2)?階段處理非標準語言,這個階段可以把非標準的js(coffee/前端模板)、css(less/sass/stylus)、html(markdown)語言轉換為標準的js、css或html。處理之后那些文件還能和標準語言一起經歷預處理、語言能力擴展、后處理、校驗、測試、壓縮等階段。
所以,要支持stylus的編譯,只要在fis-conf.js中添加這樣的配置即可:
~~~
//依賴開源的stylus包
var stylus = require('stylus');
//編譯插件只負責處理文件內容
var stylusParser = function(content, file, conf){
return stylus(content, conf).render();
};
//配置編譯流程,styl后綴的文件經過編譯插件函數處理
fis.config.set('modules.parser.styl', stylusParser);
//告訴fis,styl后綴的文件,被當做css處理,編譯后后綴也是css
fis.config.set('roadmap.ext.styl', 'css');
~~~
這樣我們項目中的*.styl后綴的文件都會被編譯為css內容,并且會在后面的流程中被當做css內容處理,比如壓縮、csssprite等。
文件監聽、自動刷新都是fis內置的功能,fis的release命令集合了所有編譯所需的參數,
~~~
fis release -h
Usage: release [options]
Options:
-h, --help output usage information
-d, --dest <names> release output destination
-m, --md5 [level] md5 release option
-D, --domains add domain name
-l, --lint with lint
-t, --test with unit testing
-o, --optimize with optimizing
-p, --pack with package
-w, --watch monitor the changes of project
-L, --live automatically reload your browser
-c, --clean clean compile cache
-r, --root <path> set project root
-f, --file <filename> set fis-conf file
-u, --unique use unique compile caching
--verbose enable verbose output
~~~
這些參數是可以隨意組合的,比如我們想文件監聽、自動刷新,則使用:
~~~
fis release -wL
~~~
壓縮、打包、文件監聽、自動刷新、發布到output目錄,則使用:
~~~
fis release -opwLd output
~~~
> 構建工具不需要那么多命令,或者develop、release等不同狀態的配置文件,應該從命令行切換編譯參數,從而實現開發/上線構建模式的切換。
另外,fis是命令行工具,各種內置的插件也是完全獨立無環境依賴的,可以與ci平臺直接對接,并在各個主流操作系統下運行正常。
利用fis的內置的各種編譯功能,我們離目標又近了許多:
* 規范
* 開發規范
* ~~模塊化開發,js模塊化,css模塊化,像nodejs一樣的模塊化開發~~
* ~~組件化開發,js、css、handlebars維護在一起~~
* 部署規范
* ~~采用nodejs后端,基本部署規范應該參考express項目部署~~
* ~~按版本號做非覆蓋式發布~~
* ~~公共模塊可發布給第三方共享~~
* 框架
* ~~js模塊化框架,支持請求合并,按需加載等性能優化點~~
* 工具
* ~~可以編譯stylus為css~~
* ~~支持js、css、圖片壓縮~~
* ~~允許圖片壓縮后以base64編碼形式嵌入到css、js或html中~~
* ~~與ci平臺集成~~
* ~~文件監聽、瀏覽器自動刷新~~
* 本地預覽、數據模擬
* 倉庫
* 支持component模塊安裝和使用
剩下兩個,我們可以通過擴展fis的命令行插件來實現。fis有11個編譯流程擴展點,還有一個命令行擴展點。要擴展命令行插件很簡單,只要我們將插件安裝到與fis同級的node_modules目錄下即可。比如:
~~~
node_modules
- fis
- fis-command-say
~~~
那么執行?`fis say`?這個命令,就能調用到那個fis-command-say插件了。剩下的這個component模塊安裝,我就利用了這個擴展點,結合component開源的?[component-installer](https://www.npmjs.org/package/component-installer)?包,我就可以把component整合當前開發體系中,這里我們需要創建一個npm包來提供擴展,而不能直接在fis-conf.js中擴展命令行,插件代碼我就不貼了,可以看?[這里](https://github.com/scrat-team/scrat-command-install/blob/master/index.js)。
眼前我們有了一個差不多100行的fis-conf.js文件,還有幾個插件,如果我把這樣一個零散的系統交付團隊使用,那么大家使用的步驟差不多是這樣的:
1. 安裝fis,`npm install -g fis`
2. 安裝component安裝用的命令行插件,`npm insatll -g fis-command-component`
3. 安裝stylus編譯插件,`npm install -g fis-parser-stylus`
4. 下載一份配置文件,fis-conf.js,修改里面的name、version配置
這種情況讓團隊用起來會有很多問題。首先,安裝過程太過麻煩,其次如果項目多,那么fis-conf.js不能同步升級,這是非常嚴重的問題。grunt的gruntfile.js也是如此。如果說有一個項目用了某套grunt配置感覺很爽,那么下個項目也想用這套方案,復制gruntfile.js是必須的操作,項目用的多了,同步gruntfile的成本就變得非常高了。
因此,fis提供了一種“包裝”的能力,它允許你將fis作為內核,包裝出一個新的命令行工具,這個工具內置了一些fis的配置,并且把所有命令行調用的參數傳遞給fis內核去處理。
我準備把這套系統打包為一個新的工具,給它取名為?`scrat`,也是一只[松鼠](http://image.baidu.com/i?tn=baiduimage&ct=201326592&lm=-1&cl=2&fr=ala1&word=scrat)。這個新工具的目錄結構是這樣的:
~~~
scrat
- bin
- scrat
- node_modules
- fis
- fis-parser-handlebars
- fis-lint-jshint
- scrat-command-install
- scrat-command-server
- scrat-parser-stylus
- index.js
- package.json
~~~
其中,index.js的內容為:
~~~
//require一下fis模塊
var fis = module.exports = require('fis');
//聲明命令行工具名稱
fis.cli.name = 'scrat';
//定義插件前綴,允許加載scrat-xxx-xxx插件,或者fis-xxx-xxx插件,
//這樣可以形成scrat自己的插件系統
fis.require.prefixes = [ 'scrat', 'fis' ];
//把前面的配置都寫在這里統一管理
//項目中就不用再寫了
fis.config.merge({...});
~~~
將這個npm包發布出來,我們就有了一個全新的開發工具,這個工具可以解決前面說的13項技術問題,并提供一套完整的集成解決方案,而你的團隊使用的時候,只有兩個步驟:
1. 安裝這個工具,npm install -g scrat
2. 項目配置只有兩項,name和version
使用新工具的命令、參數幾乎和fis完全一樣:
~~~
scrat release [options]
scrat server start
scrat install <name@version> [options]
~~~
而scrat這個工具所內置的配置將變成規范文檔描述給團隊同學,這套系統要比grunt那種松散的構建系統組成方式更容易被多個團隊、多個項目同時共享。
> 熬了一個通宵,基本算是完成了。。。
## 2014年02月17日 - 多云
終于到了周一,交付了一個新的開發工具——[scrat](https://github.com/scrat-team/scrat/),及其使用?[文檔](https://github.com/scrat-team/scrat/#%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E)。
然而,過去的三天,為了構造這套前端開發體系,都寫了哪些代碼呢?
* 基于fis的一套[規范及插件配置](https://github.com/scrat-team/scrat/blob/master/index.js),274行;
* scrat install命令行[插件](https://github.com/scrat-team/scrat-command-install/blob/master/index.js),用于安裝component模塊,74行;
* scrat server命令行[插件](https://github.com/scrat-team/scrat-command-server/blob/master/index.js),用于啟動nodejs的服務器,203行
* 編譯stylus的[插件](https://github.com/scrat-team/scrat-parser-stylus/blob/master/index.js),10行
* 編譯handlebars的[插件](https://github.com/fouber/fis-parser-handlebars/blob/master/index.js),6行
* 一個模塊化框架?[scrat.js](https://github.com/scrat-team/scrat.js/blob/master/scrat.js),393行
一共?`960行`?代碼,用了4人/天。
## 總結
> 不可否認,為大規模前端團隊設計集成解決方案需要花費非常多的心思。
如果說只是實現一個簡單的編譯+壓縮+文件監+聽自動刷新的常規構建系統,基于fis應該不超過1小時就能完成一個,但要實踐完整的前端集成解決方案,確實需要點時間。
如之前一篇?[文章](https://github.com/fouber/blog/issues/1)?所講,前端集成解決方案有8項技術要素,除了組件倉庫,其他7項對于企業級前端團隊來說,應該都需要完整實現的。即便暫時不想實現,也會隨著業務發展而被迫慢慢完善,這個完善過程是普適的。
對于前端集成解決方案的實踐,可以總結出這些設計步驟:
1. 設計開發概念,定義開發資源的分類(模塊化/非模塊化)
2. 設計開發目錄,降低開發、維護成本(開發規范)
3. 根據運維和業務要求,設計部署規范(部署規范)
4. 設計工具,完成開發目錄和部署目錄的轉換(開發-部署轉換)
5. 設計模塊化框架,兼顧性能優化(開發框架)
6. 擴展工具,支持開發框架的構建需求(框架構建需求)
7. 流程整合(開發、測試、聯調、上線等流程接入)
我們可以看看業界已有團隊提出的各種解決方案,無不以這種思路來設計和發展的:
* [seajs開發體系](http://seajs.org/),支付寶團隊前端開發體系,以?[spm](https://github.com/spmjs/spm)?為構建和包管理工具
* [fis-plus](https://github.com/fex-team/fis-plus),百度絕大多數前端團隊使用的開發體系,以fis為構建工具內核,以[lights](https://www.npmjs.org/package/lights)為包管理工具
* [edp](https://github.com/ecomfe/edp),百度ecomfe前端開發體系,以?[edp](https://github.com/ecomfe/edp)?為構建和包管理工具
* [modjs](http://madscript.com/modjs/),騰訊AlloyTeam團隊出品的開發體系
* [yeoman](http://yeoman.io/),google出品的解決方案,以grunt為構建工具,bower為包管理工具
縱觀這些公司出品的前端集成解決方案,深入剖析其中的框架、規范、工具和流程,都可以發現一些共通的影子,設計思想殊途同歸,不約而同的朝著一種方向前進,那就是前端集成解決方案。嘗試將前端工程孤立的技術要素整合起來,解決常見的領域問題。
> 或許有人會問,不就是寫頁面么,用得著這么復雜?
在這里我不能給出肯定或者否定的答復。
因為單純從語言的角度來說,html、js、css(甚至有人認為css是數據結構,而非語言)確實是最簡單最容易上手的開發語言,不用模塊化、不用工具、不用壓縮,任何人都可以快速上手,完成一兩個功能簡單的頁面。所以說,在一般情況下,前端開發非常簡單。
> 在規模很小的項目中,前端技術要素彼此不會直接產生影響,因此無需集成解決方案。
但正是由于前端語言這種靈活松散的特點,使得前端項目規模在達到一定規模后,工程問題凸顯,成為發展瓶頸,各種技術要素彼此之間開始出現關聯,要用模塊化開發,就必須對應某個模塊化框架,用這個框架就必須對應某個構建工具,要用這個工具,就必須對應某個包管理工具……這個時候,完整實踐前端集成解決方案就成了不二選擇。
> 當前端項目達到一定規模后,工程問題將成為主要瓶頸,原來孤立的技術要素開始彼此產生影響,需要有人從比較高的角度去梳理、尋找適合自己團隊的集成解決方案。
所以會出現一些框架或工具在小項目中使用的好好的,一旦放到團隊里使用就非常困難的情況。
前端入門雖易工程不易,且行寫珍惜!