構建的核心是資源管理。簡單說,構建就是把前端工程師開發的源代碼進行編譯、壓縮、打包等一系列操作,最終產出可以直接上線或者可供后端工程師的資源。
構建可以划分為純前端構建和前后端協作構建。
這兩個不是專業術語,如果你有更合適的稱謂,歡迎指正。
所謂純前端構建,就是說不涉及后端模板的構建,經過構建之后的前端代碼可以直接上線。這種情形下大多是數據驅動UI的web應用,模板只負責提供空白的容器和基礎的靜態資源,UI的文檔結構交由前端JavaScript實現。這個過程可以使用一些框架,比如近期較流行的React、Vue等;也使用較輕量級的JavaScript模板工具,比如的underscore template、jsmart、Mustache等;甚至可以直接拼接字符串。
前后端協作構建與純前端構建唯一的不同是加入了對后端模板的依賴,這也是目前絕大多數web應用的工作模式。這種模式下,構建工具要額外處理模板中對靜態資源的引用地址。我們在淺析前端工程化一文中提到的便是前后端協作的構建模式,也是本文將要討論的方向。
下面我們細化資源管理的每個關鍵點,共同探討一下前端工程中構建環節的工作內容和面臨的問題。
1. 基本功能
如果是純前端構建(不涉及后端模板),在資源管理方面,編譯工具需要完成的事情包括:
- 代碼審查。包括eslint、sass審查;
- 預編譯。包括es6/7語法轉譯、sass預編譯css、spirit圖片生成;
- 資源嵌入。小於某個尺寸(比如10kb)的靜態資源替換為base64,以減少一次http請求;
- 依賴分析。掃描模塊依賴關系並產出,整個流程大致可以規划為:
是否合並這一步由用戶選擇,比如項目中使用requirejs作為前端模塊化方案,考慮到緩存、異步等問題可能選擇不合並。這種情況下往往需要在本次構建產出的資源依賴表的基礎上進行二次構建,后續會詳細說明。
- uglify和compress;
- hash指紋 。利用md5算法對比靜態資源更改,給文件名加上hash指紋,並產出資源定位表;
- release。將構建后的文件產出到指定目錄,這個目錄通常是本地的,經本地測試通過后可以push到線上服務器。同時,release階段會產出資源定位表,以便模板構建和二次構建中使用;
- 模板構建。根據release階段產出的資源定位表,將模板中靜態資源的引用地址更新為構建后的絕對路徑。
2.整體流程
一套完整的構建流程如下圖所示:
具體構建流程中的各個行為並不是嚴格按照上圖的前后順序進行,可以自行安排。
上圖中提到的各個構建行為中,代碼審查、預編譯、uglify&compress、hash指紋實現較容易,各構建模式中沒有差異,本文便不再贅述。而依賴打包管理和模板構建是需要額外配置並且方案不唯一,下面詳細探討這兩個行為的具體內容。
3. 依賴管理
依賴管理之所以方案不唯一是因為每個項目可能會采用不同的客戶端模塊加載方案(AMD/CMD/CommonJS)。構建平台本身不應該面向某一種方案,而應該是可配置的。
比如某個項目客戶端的模塊化方案采用AMD,使用requirejs作為模塊加載器,那么在構建平台產出上述資源依賴表之后,還需要對requirejs進行配置,這個階段我們可以稱為二次構建。
二次構建要用到依賴分析之后的資源依賴表和release之后產出資源定位表。假設資源依賴表格式如下:
{
'a.js': {
deps:['b.js','c.js']
}
'c.js': {
deps: ['d.js']
}
'e.js': {
deps: ['f.js']
}
}
資源定位表的格式如下:
{
'src/js/a.js': 'release\js\a.sdf43n.js',
'src/js/b.js': 'release\js\b.sdf43n.js',
'src/js/c.js': 'release\js\c.sdf43n.js',
'src/js/d.js': 'release\js\d.sdf43n.js',
'src/js/e.js': 'release\js\e.sdf43n.js',
'src/js/f.js': 'release\js\f.sdf43n.js',
}
在二次構建階段,requirejs根據資源依賴表和資源定位表需要作出如下配置:
requirejs.config({
baseUrl: '/',
path: {
'a.js': 'release\js\a.sdf43n.js',
'b.js': 'release\js\b.sdf43n.js',
//...
},
shim: {
'c.js': {
deps: ['d.js']
}
//...
}
});
雖然同步依賴的文件原則上應該打包成一個文件,但是構建平台不應該制定一些條律,所以在requirejs配置完成之后,構建平台應該還需要提供是否壓縮打包的配置項。
此外,如果開發階段使用es6語法,客戶端使用AMD方案的話,在二次構建之前還需要將es6 module模塊編譯為AMD規范,這一步在預編譯階段完成。
4. 模板構建
模板構建的核心問題是如何同步更新靜態資源的引用地址。除此之外,模板中往往還包含一些由Controller輸出的動態數據,在構建過程中需要謹慎處理各模板引擎的語法。
目前對模板的處理分為兩種模式:由前端負責構建和由后端負責構建。
這兩種模式不僅僅是分工的不同,同時涉及開發方案的不同。
4.1 模板由前端構建
如果模板由前端構建工具進行編譯,交到后端開發者手里的模板中對靜態資源的引用地址是已經更新后的url,后端開發者不需要對模板進行額外操作便可以直接進行下一步流程(測試、部署)。
但是前端構建工具必須謹慎處理模板引擎的語法,以免造成“誤傷”。后端模板引擎多種多樣,前端構建工具很難做到百分百覆蓋。所以通常情況下需要對模板中靜態資源的url添加額外標識位,以處理文本的方式識別標識位並進行替換。比如:
<script src='[__static-start__]src/js/index.js[/__static-end__]'></script>
上述代碼中將index.js
的url用類ubb格式的開閉標簽包裹起來。假設經編譯后index.js
的資源定位表如下:
{
'src/js/index.js': 'static.daojia.com/js/index.sfdf232.js'
}
前端構建工具首先獲取模板文件的文本內容,正則出[__static-start__]src/js/index.js[/__static-end__]
,然后替換為static.daojia.com/js/index.sfdf232.js
。
以上的操作往往非常耗時,並且不能保證百分百正確率。
4.2 模板由后端構建
模板有后端構建的意思是,后端開發人員對模板引擎進行擴展,書寫一個模板引擎語法的資源尋址function。比如php使用smarty模板引擎實現一個cdn
方法:
<?php
/*
* Smarty plugin
* -------------------------------------------------------------
* File: function.cdn.php
* Type: function
* Name: cdn
* Purpose: transform internal cdn path to online format
* -------------------------------------------------------------
*/
function smarty_function_cdn($params, Smarty_Internal_Template $smarty){
//...
}
還可以針對不同類型的靜態資源擴展對應的cdn尋址方法:
<?php
/*
* Smarty plugin
* -------------------------------------------------------------
* File: function.js.php
* Type: function
* Name: js
* Purpose: print script tag by url
* -------------------------------------------------------------
*/
require_once('function.cdn.php');
function smarty_function_js($params, $smarty){
//...
}
然后前端開發人員在編寫smarty模板時便可以使用如下語法引入靜態資源:
{js url="/src/js/index.js"}
前端構建工具只需要處理靜態資源即可,后端模板在部署上線后會將index.js
的url更新為線上地址。
模板交由后端構建的優點是可以對每種模板引擎有針對性的處理,而且是很工程化的構建方式;缺點是需要額外書寫尋址function,但是這個缺點相對於優點來說微不足道。
4.3 小結
綜上所述,模板前端構建和后端構建的對比如下:
根據表格的對比數據可以看出模板后端構建相比前端構建有很大優勢。但是作為構建平台,應該同時支持兩種模式。所以在開發構建平台的時候,開發者應該提供前端構建的功能接口,由用戶選擇是否采用。
5. 總結
本文簡單講述構建平台在不同開發模式下對應的構建方案。以上內容是筆者的一些經驗和思考,肯定會有不足和錯誤之處,歡迎大家反饋,共同探討。