目錄
上周寫了一篇?文章?介紹前端集成解決方案的基本理論,很多同學(xué)看過之后大呼不過癮。
干貨?
fuck things?在哪里!
本打算繼續(xù)完善理論鏈,形成前端工程的知識結(jié)構(gòu)。但鑒于如今的快餐文化,po主決定還是先寫一篇實戰(zhàn)介紹,讓大家看到前端工程體系能為團隊帶來哪些好處,調(diào)起大家的胃口再說。
ps: 寫完才發(fā)現(xiàn)這篇文章真的非常非常長,涵蓋了前端開發(fā)中的很多方面,希望大家能有耐心看完,相信一定會有所斬獲。。。
新到松鼠團隊的第二天,小伙伴?@nino?找到我說
nino: 視頻項目打算重新梳理一下,希望能引入新的技術(shù)體系,解決現(xiàn)有的一些問題。
po主不禁暗喜,好機會,這是我專業(yè)啊,藍翔技校-前端集成解決方案學(xué)院-自動化系-打包學(xué)專業(yè)的文憑不是白給的,于是自信滿滿的對nino說,有什么需求盡管提!
nino: 我的需求并不多,就這么幾條~~
我倒吸一口涼氣,但表面故作鎮(zhèn)定的說:恩,確實不多,讓我們先來看看第一個需求。。。
還沒等我說完,nino打斷我說
nino: 橋豆麻袋(稍等),還有一個最重要的需求!
松鼠公司的松鼠瀏覽器你知道吧,恩,它有很多個版本的樣子。
我希望代碼發(fā)布后能按照版本部署,不要彼此覆蓋。
舉個例子,代碼部署結(jié)構(gòu)可能是這樣的:
release/
- public/
- 項目名
- 1.0.0/
- 1.0.1/
- 1.0.2/
- 1.0.2-alpha/
- 1.0.2-beta/
讓歷史瀏覽器瀏覽歷史版本,沒事還能做個灰度發(fā)布,ABTest啥的,多好!
此外,我們將來會有多個項目使用這套開發(fā)模式,希望能共用一些組件或者模
塊,產(chǎn)品也會公布一些api模塊給第三方使用,所以共享模塊功能也要加上。
總的來說,還要追加兩個部署需求:
nino: 怎么樣,不算復(fù)雜吧,這個項目很趕,
3天
搞定怎么樣?
我凝望著會議室白板上的這些需求,正打算爭辯什么,一扭頭發(fā)現(xiàn)nino已經(jīng)不見了。。。正在沮喪之際,小伙伴?@hinc?過來找我,跟他大概講了一下nino的需求,正想跟他抱怨工期問題時,hinc卻說
hinc: 恩,這正是我們需要的開發(fā)體系,不過我這里還有一個需求。。。
3天時間,13項前端技術(shù)元素,靠譜么。。。
一覺醒來,輕松了許多,但還有任務(wù)在身,不敢有半點怠慢。整理一下昨天的需求,我們來做一個簡單的劃分。
這樣一套規(guī)范、框架、工具和倉庫的開發(fā)體系,服從我之前介紹的?前端集成解決方案?的描述。前端界每天都團隊在設(shè)計和實現(xiàn)這類系統(tǒng),它們其實是有規(guī)律可循的。百度出品的?fis?就是一個能幫助快速搭建前端集成解決方案的工具。使用fis我應(yīng)該可以在3天之內(nèi)完成這些任務(wù)。
ps: 這不是一篇關(guān)于fis的軟文,如果這樣的一套系統(tǒng)基于grunt實現(xiàn)相信會有非常大量的開發(fā)工作,3天完成幾乎是不可能的任務(wù)。
不幸的是,現(xiàn)在fis官網(wǎng)所介紹的?并不是?fis,而是一個叫?fis-plus?的項目,該項目并不像字面理解的那樣是fis的加強版,而是在fis的基礎(chǔ)上定制的一套面向百度前端團隊的解決方案,以php為后端語言,跟smarty有較強的綁定關(guān)系,有著?19項
?技術(shù)要素,密切配合百度現(xiàn)行技術(shù)選型。絕大多數(shù)非百度前端團隊都很難完整接受這19項技術(shù)選型,尤其是其中的部署、框架規(guī)范,跟百度前端團隊相關(guān)開發(fā)規(guī)范、部署規(guī)范、以及php、smarty等有著較深的綁定關(guān)系。
因此如果你的團隊用的不是?php后端
?&&?smarty模板
?&&?modjs模塊化框架
?&&?bingo框架
?的話,請查看?fis的文檔,或許不會有那么多困惑。
ps: fis是一個構(gòu)建系統(tǒng)內(nèi)核,很好的抽象了前端集成解決方案所需的通用工具需求,本身不與任何后端語言綁定。而基于fis實現(xiàn)的具體解決方案就會有具體的規(guī)范和技術(shù)選型了。
言歸正傳,讓我們基于?fis?開始實踐這套開發(fā)體系吧!
前端開發(fā)體系設(shè)計第一步要定義開發(fā)概念。開發(fā)概念是指針對開發(fā)資源的分類概念。開發(fā)概念的確立,直接影響到規(guī)范的定制。比如,傳統(tǒng)的開發(fā)概念一般是按照文件類型劃分的,所以傳統(tǒng)前端項目會有這樣的目錄結(jié)構(gòu):
這樣確實很直接,任何智力健全的人都知道每個文件該放在哪里。但是這樣的開發(fā)概念劃分將給項目帶來較高的維護成本,并為項目臃腫埋下了工程隱患,理由是:
ps: 除非你的團隊只有1-2個人,你的項目只有很少的代碼量,而且不用關(guān)心性能和未來的維護問題,否則,以文件為依據(jù)設(shè)計的開發(fā)概念是應(yīng)該被拋棄的。
以我個人的經(jīng)驗,更傾向于具有一定語義的開發(fā)概念。綜合前面的需求,我為這個開發(fā)體系確定了3個開發(fā)資源概念:
ps: 開發(fā)概念越簡單越好,前面提到的fis-plus也有類似的開發(fā)概念,有組件或模塊(widget),頁面(page),測試數(shù)據(jù)(test),非模塊化靜態(tài)資源(static)。有的團隊在模塊之中又劃分出api模塊和ui模塊(組件)兩種概念。
基于開發(fā)概念的確立,接下來就要確定目錄規(guī)范了。我通常會給每種開發(fā)資源的目錄取一個有語義的名字,三種資源我們可以按照概念直接定義目錄結(jié)構(gòu)為:
project
- modules 存放模塊化資源
- pages 存放頁面資源
- static 存放非模塊化資源
這樣劃分目錄確實直觀,但結(jié)合前面hinc說過的,希望能使用component倉庫資源,因此我決定將模塊化資源目錄命名為components
,得到:
project
- components 存放模塊化資源
- pages 存放頁面資源
- static 存放非模塊化資源
而nino又提到過模塊資源分為項目模塊和公共模塊,以及hinc提到過希望能從component安裝一些公共組件到項目中,因此,一個components目錄還不夠,想到nodejs用node_modules作為模塊安裝目錄,因此我在規(guī)范中又追加了一個?component_modules
?目錄,得到:
project
- component_modules 存放外部模塊資源
- components 存放項目模塊資源
- pages 存放頁面資源
- static 存放非模塊化資源
nino說過今后大多數(shù)項目采用nodejs作為后端,express是比較常用的nodejs的server框架,express項目通常會把后端模板放到?views
?目錄下,把靜態(tài)資源放到?public
?下。為了迎合這樣的需求,我將page、static兩個目錄調(diào)整為?views
?和?public
,規(guī)范又修改為:
project
- component_modules 存放外部模塊資源
- components 存放項目模塊資源
- views 存放頁面資源
- public 存放非模塊化資源
考慮到頁面也是一種靜態(tài)資源,而public
這個名字不具有語義性,與其他目錄都有概念沖突,不如將其與views
目錄合并,views目錄負責(zé)存放頁面和非模塊化資源比較合適,因此最終得到的開發(fā)目錄結(jié)構(gòu)為:
project
- component_modules 存放外部模塊資源
- components 存放項目模塊資源
- views 存放頁面以及非模塊化資源
托nino的福,咱們的部署策略將會非常復(fù)雜,根據(jù)要求,一個完整的部署結(jié)果應(yīng)該是這樣的目錄結(jié)構(gòu):
release
- public
- 項目名
- 1.0.0 1.0.0版本的靜態(tài)資源都構(gòu)建到這里
- 1.0.1 1.0.1版本的靜態(tài)資源都構(gòu)建到這里
- 1.0.2 1.0.2版本的靜態(tài)資源都構(gòu)建到這里
...
- views
- 項目名
- 1.0.0 1.0.0版本的后端模板都構(gòu)建到這里
- 1.0.1 1.0.1版本的后端模板都構(gòu)建到這里
- 1.0.2 1.0.2版本的后端模板都構(gòu)建到這里
...
由于還要部署一些可以被第三方使用的模塊,public下只有項目名的部署還不夠,應(yīng)改把模塊化文件單獨發(fā)布出來,得到這樣的部署結(jié)構(gòu):
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版本的靜態(tài)資源都構(gòu)建到這里
- 1.0.1 1.0.1版本的靜態(tài)資源都構(gòu)建到這里
- 1.0.2 1.0.2版本的靜態(tài)資源都構(gòu)建到這里
...
- views
- 項目名
- 1.0.0 1.0.0版本的后端模板都構(gòu)建到這里
- 1.0.1 1.0.1版本的后端模板都構(gòu)建到這里
- 1.0.2 1.0.2版本的后端模板都構(gòu)建到這里
...
由于?component_modules
?這個名字太長了,如果部署到這樣的路徑下,url會很長,這也是一個優(yōu)化點,因此最終決定部署結(jié)構(gòu)為:
release
- public
- c 模塊化資源都部署到這個目錄下
- 公共模塊
- 版本號
- 項目名
- 版本號
- 項目名
- 版本號 非模塊化資源都部署到這個目錄下
- views
- 項目名
- 版本號 后端模板都構(gòu)建到這個目錄下
插一句,并不是所有團隊都會有這么復(fù)雜的部署要求,這和松鼠團隊的業(yè)務(wù)需求有關(guān),但我相信這個例子也不會是最復(fù)雜的。每個團隊都會有自己的運維需求,前端資源部署經(jīng)常牽連到公司技術(shù)架構(gòu),因此很多前端項目的開發(fā)目錄結(jié)構(gòu)會和部署要求保持一致。這也為項目間模塊的復(fù)用帶來了成本,因為代碼中寫的url通常是部署后的路徑,遷移之后就可能失效了。
解耦開發(fā)規(guī)范和部署規(guī)范是前端開發(fā)體系的設(shè)計重點。
好了,去吃個午飯,下午繼續(xù)。。。
我準備了一個樣例項目:
project
- views
- logo.png
- index.html
- fis-conf.js
- README.md
fis-conf.js
是fis工具的配置文件,接下來我們就要在這里進行構(gòu)建配置了。雖然開發(fā)規(guī)范和部署規(guī)范十分復(fù)雜,但好在fis有一個非常強大的?roadmap.path?功能,專門用于分類文件、調(diào)整發(fā)布結(jié)構(gòu)、指定文件的各種屬性等功能實現(xiàn)。
所謂構(gòu)建,其核心任務(wù)就是將文件按照某種規(guī)則進行分類(以文件后綴分類,以模塊化/非模塊化分類,以前端/后端代碼分類),然后針對不同的文件做不同的構(gòu)建處理。
閑話少說,我們先來看一下基本的配置,在?fis-conf.js
?中添加代碼:
fis.config.set('roadmap.path', [
{
reg : '**.md', //所有md后綴的文件
release : false //不發(fā)布
}
]);
以上配置,使得項目中的所有md后綴文件都不會發(fā)布出來。release是定義file對象發(fā)布路徑的屬性,如果file對象的release屬性為false,那么在項目發(fā)布階段就不會被輸出出來。
在fis中,roadmap.pah是一個數(shù)組數(shù)據(jù),數(shù)組每個元素是一個對象,必須定義?reg
?屬性,用以匹配項目文件路徑從而進行分類劃分,reg屬性的取值可以是路徑通配字符串或者正則表達式。fis有一個內(nèi)部的文件系統(tǒng),會給每個源碼文件創(chuàng)建一個?fis.File?對象,創(chuàng)建File對象時,按照roadmap.path的配置逐個匹配文件路徑,匹配成功則把除reg之外的其他屬性賦給File對象,fis中各種處理環(huán)節(jié)及插件都會讀取所需的文件對象的屬性值,而不會自己定義規(guī)范。有關(guān)roadmap.path的工作原理可以看這里?以及?這里。
ok,讓md文件不發(fā)布很簡單,那么views目錄下的按版本發(fā)布要求怎么實現(xiàn)呢?其實也是非常簡單的配置:
fis.config.set('roadmap.path', [
{
reg : '**.md', //所有md后綴的文件
release : false //不發(fā)布
},
{
//正則匹配【/views/**】文件,并將views后面的路徑捕獲為分組1
reg : /^\/views\/(.*)$/i,
//發(fā)布到 public/proj/1.0.0/分組1 路徑下
release : '/public/proj/1.0.0/$1'
}
]);
roadmap.path數(shù)組的第二元素據(jù)采用正則作為匹配規(guī)則,正則可以幫我們捕獲到分組信息,在release屬性值中引用分組是非常方便的。正則匹配 + 捕獲分組,成為目錄規(guī)范配置的強有力工具:
執(zhí)行? 在調(diào)用? 現(xiàn)在模塊的id有一些問題,因為模塊發(fā)布會有版本號信息,因此模塊id也應(yīng)該攜帶版本信息,從前面的依賴樹生成配置代碼中我們可以看到模塊id其實也是文件的一個屬性,因此我們可以在roadmap.path中重新為文件賦予id屬性,使其攜帶版本信息: 重新構(gòu)建項目,我們得到了新的結(jié)果: you see?所有id都會被修改為我們指定的模式,這就是以文件為中心的編譯系統(tǒng)的威力。 以文件對象為中心構(gòu)建系統(tǒng)應(yīng)該通過配置指定文件的各種屬性。插件并不自己實現(xiàn)某種規(guī)范規(guī)定,而是讀取file對象的對應(yīng)屬性值,這樣插件的職責(zé)單一,規(guī)范又能統(tǒng)一起來被用戶指定,為完整的前端開發(fā)體系設(shè)計奠定了堅實規(guī)范配置的基礎(chǔ)。 接下來還有一個問題,就是模塊名太長,開發(fā)中寫這么長的模塊名非常麻煩。我們可以借鑒流行的模塊化框架中常用的縮短模塊名手段——別名(alias)——來降低開發(fā)中模塊引用的成本。此外,目前的配置其實會針對所有文件生成依賴關(guān)系表,我們的開發(fā)概念定義只有components和component_modules目錄下的文件才是模塊化的,因此我們可以進一步的對文件進行分類,得到這樣配置規(guī)范: 然后我們?yōu)橐恍┠Kid建立別名: 再次構(gòu)建,在注入的代碼中就能看到alias字段了: 這樣,代碼中的? 還剩最后一個小小的需求,就是希望能像寫nodejs一樣開發(fā)js模塊,也就是要求實現(xiàn)define的自動包裹功能,這個可以通過文件編譯的?postprocessor?插件完成。配置為: 所有在components目錄和component_modules目錄下的js文件都會被包裹define,并自動根據(jù)roadmap.path中的id配置進行模塊定義了。 最煎熬的一天終于過去了,睡一覺,擁抱一下周末。 周末的天氣非常好哇,一覺睡到中午才起,這么好的天氣寫碼豈不是很loser?! 居然浪費了一天,剩下的時間不多了,今天要抓緊啊?。?! 讓我們來回顧一下已經(jīng)完成了哪些工作: 剩下的幾個需求中有些是fis默認支持的,比如base64內(nèi)嵌功能,圖片會先經(jīng)過編譯流程,得到壓縮后的內(nèi)容fis再對其進行base64化的內(nèi)嵌處理。由于fis的內(nèi)嵌功能支持任意文件的內(nèi)嵌,所以,這個語言能力擴展可以同時解決前端模板和圖片base64內(nèi)嵌需求,比如我們有這樣的代碼: 無需配置,既可以在js中嵌入資源,比如 foo.js 中可以這樣寫: 編譯后得到:
fis release -d ../release
?之后,得到構(gòu)建后的內(nèi)容為:<!doctype html>
<html>
<head>
<title>hello</title>
</head>
<body>
<script type="text/javascript" src="https://atts.w3cschool.cn/attachments/image/cimg/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')
?之際,模塊化框架已經(jīng)知道了這個foo.js依賴于bar.js、bar.css以及foo.css,因此可以發(fā)起兩個combo請求去加載所有依賴的js、css文件,完成后再執(zhí)行回調(diào)。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="https://atts.w3cschool.cn/attachments/image/cimg/logo.png"/>
<script type="text/javascript" src="https://atts.w3cschool.cn/attachments/image/cimg/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>
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
}
]);
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]);
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');
了。//在postprocessor對所有js后綴的文件進行內(nèi)容處理:
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;
});
2014年02月15日 - 超晴
2014年02月16日 - 小雨
模塊化開發(fā),js模塊化,css模塊化,像nodejs一樣的模塊化開發(fā)組件化開發(fā),js、css、handlebars維護在一起采用nodejs后端,基本部署規(guī)范應(yīng)該參考?express?項目部署按版本號做非覆蓋式發(fā)布公共模塊可發(fā)布給第三方共享js模塊化框架,支持請求合并,按需加載等性能優(yōu)化點project
- components
- foo
- foo.js
- foo.css
- foo.handlebars
- foo.png
//依賴聲明
var bar = require('../bar/bar.js');
//把handlebars文件的字符串形式嵌入到j(luò)s中
var text = __inline('foo.handlebars');
var tpl = Handlebars.compile(text);
exports.render = function(data){
return tpl(data);
};
//把圖片的base64嵌入到j(luò)s中
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文件的字符串形式嵌入到j(luò)s中
var text = "<h1>{{title}}</h1>";
var tpl = Handlebars.compile(text);
exports.render = function(data){
return tpl(data);
};
//把圖片的b
更多建議: