原文轉自:http://www.infoq.com/cn/articles/talk-front-end-integrated-solution-part2javascript
InfoQ編輯注:本文來自前端工程師張雲龍的博客,由做者本人推薦至InfoQ進行分享。目前本系列已經發布了三個部分,本處分享的是第二部分,前端開發體系建設日記。建議在閱讀本文前先閱讀本文做者和其團隊以前分享的《前端工程精粹》系列一、二、三。php
上週寫了一篇 文章 介紹前端集成解決方案的基本理論,不少同窗看過以後大呼不過癮。css
乾貨
fuck things在哪裏!html
本打算繼續完善理論鏈,造成前端工程的知識結構。但鑑於現在的快餐文化,po主決定仍是先寫一篇實戰介紹,讓你們看到前端工程體系能爲團隊帶來哪些好處,調起你們的胃口再說。前端
ps: 寫完才發現這篇文章真的很是很是長,涵蓋了前端開發中的不少方面,但願你們能有耐心看完,相信必定會有所斬獲。。。java
新到松鼠團隊的次日,小夥伴 @nino 找到我說node
nino: 視頻項目打算從新梳理一下,但願能引入新的技術體系,解決現有的一些問題。
po主不由暗喜,好機會,這是我專業啊,藍翔技校-前端集成解決方案學院-自動化系-打包學專業的文憑不是白給的,因而自信滿滿的對nino說,有什麼需求儘管提!
nino: 個人需求並很少,就這麼幾條~~
我倒吸一口涼氣,但表面故做鎮定的說:恩,確實很少,讓咱們先來看看第一個需求。。。
還沒等我說完,nino打斷我說
nino: 橋豆麻袋(稍等),還有一個最重要的需求!
松鼠公司的松鼠瀏覽器你知道吧,恩,它有不少個版本的樣子。 我但願代碼發佈後能按照版本部署,不要彼此覆蓋。 舉個例子,代碼部署結構多是這樣的: release/ - public/ - 項目名 - 1.0.0/ - 1.0.1/ - 1.0.2/ - 1.0.2-alpha/ - 1.0.2-beta/ 讓歷史瀏覽器瀏覽歷史版本,沒事還能作個灰度發佈,ABTest啥的,多好! 此外,咱們未來會有多個項目使用這套開發模式,但願能共用一些組件或者模 塊,產品也會公佈一些api模塊給第三方使用,因此共享模塊功能也要加上。
總的來講,還要追加兩個部署需求:
nino: 怎麼樣,不算複雜吧,這個項目很趕,3天搞定怎麼樣?
我凝望着會議室白板上的這些需求,正打算爭辯什麼,一扭頭髮現nino已經不見了。。。正在沮喪之際,小夥伴 @hinc 過來找我,跟他大概講了一下nino的需求,正想跟他抱怨工期問題時,hinc卻說
hinc: 恩,這正是咱們須要的開發體系,不過我這裏還有一個需求。。。
3天時間,13項前端技術元素,靠譜麼。。。
一覺醒來,輕鬆了許多,但還有任務在身,不敢有半點怠慢。整理一下昨天的需求,咱們來作一個簡單的劃分。
這樣一套規範、框架、工具和倉庫的開發體系,服從我以前介紹的 前端集成解決方案 的描述。前端界天天都團隊在設計和實現這類系統,它們實際上是有規律可循的。百度出品的 fis 就是一個能幫助快速搭建前端集成解決方案的工具。使用fis我應該能夠在3天以內完成這些任務。
ps: 這不是一篇關於fis的軟文,若是這樣的一套系統基於grunt實現相信會有很是大量的開發工做,3天完成幾乎是不可能的任務。
不幸的是,如今fis官網所介紹的 並非 fis,而是一個叫 fis-plus 的項目,該項目並不像字面理解的那樣是fis的增強版,而是在fis的基礎上定製的一套面向百度前端團隊的解決方案,以php爲後端語言,跟smarty有較強的綁定關係,有着 19項 技術要素,密切配合百度現行技術選型。絕大多數非百度前端團隊都很難完整接受這19項技術選型,尤爲是其中的部署、框架規範,跟百度前端團隊相關開發規範、部署規範、以及php、smarty等有着較深的綁定關係。
所以若是你的團隊用的不是 php後端 && smarty模板 && modjs模塊化框架 && bingo框架 的話,請查看fis的文檔,或許不會有那麼多困惑。
ps: fis是一個構建系統內核,很好的抽象了前端集成解決方案所需的通用工具需求,自己不與任何後端語言綁定。而基於fis實現的具體解決方案就會有具體的規範和技術選型了。
言歸正傳,讓咱們基於 fis 開始實踐這套開發體系吧!
前端開發體系設計第一步要定義開發概念。開發概念是指針對開發資源的分類概念。開發概念的確立,直接影響到規範的定製。好比,傳統的開發概念通常是按照文件類型劃分的,因此傳統前端項目會有這樣的目錄結構:
這樣確實很直接,任何智力健全的人都知道每一個文件該放在哪裏。可是這樣的開發概念劃分將給項目帶來較高的維護成本,併爲項目臃腫埋下了工程隱患,理由是:
ps: 除非你的團隊只有1-2我的,你的項目只有不多的代碼量,並且不用關心性能和將來的維護問題,不然,以文件爲依據設計的開發概念是應該被拋棄的。
以我我的的經驗,更傾向於具備必定語義的開發概念。綜合前面的需求,我爲這個開發體系肯定了3個開發資源概念:
ps: 開發概念越簡單越好,前面提到的fis-plus也有相似的開發概念,有組件或模塊(widget),頁面(page),測試數據(test),非模塊化靜態資源(static)。有的團隊在模塊之中又劃分出api模塊和ui模塊(組件)兩種概念。
基於開發概念的確立,接下來就要肯定目錄規範了。我一般會給每種開發資源的目錄取一個有語義的名字,三種資源咱們能夠按照概念直接定義目錄結構爲:
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 存放頁面以及非模塊化資源
託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一般是部署後的路徑,遷移以後就可能失效了。
解耦開發規範和部署規範是前端開發體系的設計重點。
好了,去吃個午餐,下午繼續。。。
我準備了一個樣例項目:
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.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的配置系統很是靈活,除了 文檔 中提到的配置節點,其餘配置用戶能夠隨便定義使用。好比配置的roadmap是系統保留的,而name、version都是用戶本身隨便指定的。fis系統保留的配置節點只有6個,包括:
完成第一份配置以後,咱們來看一下效果。
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 project fis release --dest ../release
獲得:
至此,咱們已經基本解決了開發和部署直接的目錄規範問題,這裏我須要加快一些步伐,把其餘目錄的部署規範也配置好,獲得一個相對比較完整的結果:
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');
我構造了一個相對完整的目錄結構,而後進行了一次構建,效果還不錯:
無論部署規則多麼複雜都不用擔憂,有fis強大的資源定位系統幫你在開發規範和部署規範之間創建聯繫,設計開發體系不在受制於工具的實現能力。
你能夠盡情發揮想象力,設計出最優雅最合理的開發規範和最高效最貼合公司運維要求的部署規範,最終用fis的roadmap.path功能將它們鏈接起來,實現完美轉換。
fis的roadmap功能實際上提供了項目代碼與部署規範解耦的能力。
從前面的例子能夠看出,開發使用相對路徑便可,fis會在構建時會根據fis-conf.js中的配置完成開發路徑到部署路徑的轉換工做。這意味着在fis體系下開發的模塊將具備自然的可移植性,既能知足不一樣項目的不一樣部署需求,又能容許開發中使用相對路徑進行資源定位,工程師再不用把部署路徑寫到代碼中了。
愉快的一天就這麼過去了,睡覺!
每到週五老是很是愜意的感受,無論這一週多麼辛苦,週五就是一個解脫,更況且今天仍是個特別的日子——情人節!
昨天主要解決了開發概念、開發目錄規範、部署目錄規範以及初步的fis-conf.js配置。今天要進行前端開發體系設計的關鍵任務——模塊化框架。
模塊化框架是前端開發體系中最爲核心的環節。
模塊化框架肩負着模塊管理、資源加載、性能優化(按需,請求合併)等多種重要職責,同時它也是組件開發的基礎框架,所以模塊化框架設計的好壞直接影響到開發體系的設計質量。
很遺憾的說,如今市面上已有的模塊化框架都沒能很好的處理模塊管理、資源加載和性能優化三者之間的關係。這倒不是框架設計的問題,而是由前端領域語言特殊性決定的。框架設計者通常在思考模塊化框架時,一般站在純前端運行環境角度考慮,基本功能都是用原生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 ... };
因爲框架設計是在「純前端實現」的約束條件下,使得模塊化框架對於依賴的分析必須在模塊資源加載完成以後才能作出識別。這將引發兩個性能相關的問題:
第一個問題還好,尤爲是在gzip下差很少多少字節,可是要配置js壓縮器保留require函數不壓縮。第二個問題就比較麻煩了,雖然seajs有seajs-combo插件能夠必定程度上減小請求,但仍然不能很好的解決這個問題。舉個例子,有以下seajs模塊依賴關係樹:
ps: 圖片來源 @raphealguo
採用seajs-combo插件以後,靜態資源請求的效果是這樣的:
工做過程是
雖然combo能夠在依賴層級上進行合併,但完成page.js的請求仍須要4個。不少團隊在使用seajs的時候,爲了不這樣的串行依賴請求問題,會本身實現打包方案,將全部文件直接打包在一塊兒,放棄了模塊化的按需加載能力,也是一種無奈之舉。
緣由很簡單
以純前端方式來實現模塊依賴加載不能同時解決性能優化問題。
歸根結底,這樣的結論是由前端領域語言的特色決定的。前端語言缺乏三種編譯能力,前面講目錄規範和部署規範時其實已經提到了一種能力,就是「資源定位的能力」,讓工程師使用開發路徑定位資源,編譯後可轉換爲部署路徑。其餘語言編寫的程序幾乎都沒有web這種物理上分離的資源部署策略,並且大多具都有相似'getResource(path)'這樣的函數,用於在運行環境下定位當初的開發資源,這樣無論項目怎麼部署,只要getResource函數運行正常就好了。惋惜前端語言沒有這樣的資源定位接口,只有url這樣的資源定位符,它指向的其實並非開發路徑,而是部署路徑。
這裏能夠簡單列舉出前端語言缺乏三種的語言能力:
之後我會在完善前端開發體系理論的時候在詳細介紹這三種語言能力的必要性和原子性,這裏就暫時不展開說明了。
fis最核心的編譯思想就是圍繞這三種語言能力設計的。
要兼顧性能的同時解決模塊化依賴管理和加載問題,其關鍵點在於
不能運行時去分析模塊間的依賴關係,而要讓框架提早知道依賴樹。
瞭解了緣由,咱們就要本身動手設計模塊化框架了。不要懼怕,模塊化框架其實很簡單,思想、規範都是通過不少前輩總結的結果,咱們只要聽從他們的設計思想去實現就行了。
參照已有規範,我定義了三個模塊化框架接口:
利用構建工具創建模塊依賴關係表,再將關係表注入到代碼中,調用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
從而實現按需加載和請求合併兩項性能優化需求。
根據這樣的設計思路,我請 @hinc 幫忙實現了這個框架,我告訴他,deps裏不但會有js,還會有css,因此也要兼容一下。hinc果真是執行能力很是強的小夥伴,僅一個下午的時間就搞定了框架的實現,咱們給這個框架取名爲 scrat.js,僅有393行。
前面提到fis具備資源依賴聲明的編譯能力。所以只要工程師按照fis規定的書寫方式在代碼中聲明依賴關係,就能在構建的最後階段自動得到fis系統整理好的依賴樹,而後對依賴的數據結構進行調整、輸出,知足框架要求就搞定了!fis規定的資源依賴聲明方式爲:在html中聲明依賴,在js中聲明依賴,在css中聲明依賴。
接下來,我要寫一個配置,將依賴關係表注入到代碼中。fis構建是分流程的,具體構建流程能夠看這裏。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]);
咱們準備一下項目代碼,看看構建的時候發生了什麼:
執行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 插件完成。配置爲:
//在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配置進行模塊定義了。
最煎熬的一天終於過去了,睡一覺,擁抱一下週末。
週末的天氣很是好哇,一覺睡到中午才起,這麼好的天氣寫碼豈不是很loser?!
竟然浪費了一天,剩下的時間很少了,今天要抓緊啊!!!
讓咱們來回顧一下已經完成了哪些工做:
剩下的幾個需求中有些是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; };
編譯後獲得:
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 階段處理非標準語言,這個階段能夠把非標準的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的內置的各類編譯功能,咱們離目標又近了許多:
剩下兩個,咱們能夠經過擴展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-conf.js不能同步升級,這是很是嚴重的問題。grunt的gruntfile.js也是如此。若是說有一個項目用了某套grunt配置感受很爽,那麼下個項目也想用這套方案,複製gruntfile.js是必須的操做,項目用的多了,同步gruntfile的成本就變得很是高了。
所以,fis提供了一種「包裝」的能力,它容許你將fis做爲內核,包裝出一個新的命令行工具,這個工具內置了一些fis的配置,而且把全部命令行調用的參數傳遞給fis內核去處理。
我準備把這套系統打包爲一個新的工具,給它取名爲 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項技術問題,並提供一套完整的集成解決方案,而你的團隊使用的時候,只有兩個步驟:
使用新工具的命令、參數幾乎和fis徹底同樣:
scrat release [options] scrat server start scrat install <name@version> [options]
而scrat這個工具所內置的配置將變成規範文檔描述給團隊同窗,這套系統要比grunt那種鬆散的構建系統組成方式更容易被多個團隊、多個項目同時共享。
熬了一個通宵,基本算是完成了。。。
終於到了週一,交付了一個新的開發工具——scrat,及其使用 文檔。
然而,過去的三天,爲了構造這套前端開發體系,都寫了哪些代碼呢?
一共 960行 代碼,用了4人/天。
不能否認,爲大規模前端團隊設計集成解決方案須要花費很是多的心思。
若是說只是實現一個簡單的編譯+壓縮+文件監+聽自動刷新的常規構建系統,基於fis應該不超過1小時就能完成一個,但要實踐完整的前端集成解決方案,確實須要點時間。
如以前一篇 文章 所講,前端集成解決方案有8項技術要素,除了組件倉庫,其餘7項對於企業級前端團隊來講,應該都須要完整實現的。即使暫時不想實現,也會隨着業務發展而被迫慢慢完善,這個完善過程是普適的。
對於前端集成解決方案的實踐,能夠總結出這些設計步驟:
咱們能夠看看業界已有團隊提出的各類解決方案,無不以這種思路來設計和發展的:
縱觀這些公司出品的前端集成解決方案,深刻剖析其中的框架、規範、工具和流程,均可以發現一些共通的影子,設計思想異曲同工,不約而同的朝着一種方向前進,那就是前端集成解決方案。嘗試將前端工程孤立的技術要素整合起來,解決常見的領域問題。
或許有人會問,不就是寫頁面麼,用得着這麼複雜?
在這裏我不能給出確定或者否認的答覆。
由於單純從語言的角度來講,html、js、css(甚至有人認爲css是數據結構,而非語言)確實是最簡單最容易上手的開發語言,不用模塊化、不用工具、不用壓縮,任何人均可以快速上手,完成一兩個功能簡單的頁面。因此說,在通常狀況下,前端開發很是簡單。
在規模很小的項目中,前端技術要素彼此不會直接產生影響,所以無需集成解決方案。
但正是因爲前端語言這種靈活鬆散的特色,使得前端項目規模在達到必定規模後,工程問題凸顯,成爲發展瓶頸,各類技術要素彼此之間開始出現關聯,要用模塊化開發,就必須對應某個模塊化框架,用這個框架就必須對應某個構建工具,要用這個工具,就必須對應某個包管理工具……這個時候,完整實踐前端集成解決方案就成了不二選擇。
當前端項目達到必定規模後,工程問題將成爲主要瓶頸,原來孤立的技術要素開始彼此產生影響,須要有人從比較高的角度去梳理、尋找適合本身團隊的集成解決方案。
因此會出現一些框架或工具在小項目中使用的好好的,一旦放到團隊裏使用就很是困難的狀況。
前端入門雖易工程不易,且行寫珍惜!