通過參考網上的一些構建方法,當然也在開發過程中進行了一番實踐,最終搭建了一套適用於當前多頁應用的構建方案,當然該方案還處於draft版本,會在后續的演進過程中不斷的優化。
個人覺得該方案的演進過程相對於成果而言更值得記錄。但在此之前,我們先簡單介紹下項目的整體架構,這樣大家會更明白為什么要采用這樣的構建方式。下圖列出了用戶在瀏覽器中輸入url到頁面ready的過程,可以看出這是一個典型的服務端直出架構,其中作為前端工程師的我們主要關注點是放在瀏覽器端以及Node層。在Node層,我們對koa的進行了封裝,並采用了類似於eggjs的MVC架構,同時使用pug作為模板引擎,技術棧其實並不復雜。
V0.0.1:使用webpack對前端資源進行構建
在構建過程中要做什么事呢?相信不同的人有不同的見解:
- 對靜態資源進行壓縮,減少傳輸字節;
- 為避免瀏覽器讀取了舊的緩存文件,需要為靜態資源添加MD5戳;
- 為CSS屬性自動添加vendor prefix;
- ......
上面所列出的事項也是我們在構建過程中所需要考慮的。在項目早期的構建方案中,我們選擇使用webpack作為構建工具,原因其實很簡單:功能強大、用的人多,所以一拍腦袋就選擇它了。當然,webpack也確實不負眾望,通過它,我們可以像寫Node一樣直接引入其他的文件,在使用前期確實給我們帶來了很多的便捷。
但是我們的項目畢竟和webpack主流的使用場景,如React、Vue等項目還是有很大的不同之處,在使用webpack的過程中陸續出現了一些水土不服的地方,雖然都最后都通過一些方式解決了,但是這也促使我們在思考,是否有其他更合適的方案。
問題一:
如何讓webpack打包所有資源文件?
解決方法:
webpack會將entry作為入口起點,找到所有依賴項並對其進行構建。由於webpack只認識JavaScript文件,所以對於非JavaScript文件需要使用loader將其轉為webpack能夠處理的模塊。所以說,如果某個資源文件需要被webpack構建,那么這個資源文件必須是從entry可達的。對我們的項目來說,最理想的情況是以pug模板文件作為入口,但是由於pug模板文件需要的數據是從server獲取的,而在構建階段是不可知的,所以,只能退而求其次,使用JavaScript文件作為入口。
對於不同類型的文件,我們采用了不同的策略:
- 對於圖片,我們使用了webpack提供的require.context方法,它可以讓我們使用正則的方式來引入相應的模塊。所以我們添加了下述文件,並將其置於webpack的entry屬性中。
require.context('./public', true, /\.*\.(jpg|png|jpeg|gif|ico)$/i)
- 對於css文件,當然也可以采用上述的方式,但是這會將所有的css編譯到一個文件中,導致生成的文件過大,從而使頁面加載耗時較長。所以我們只能選擇在相應的JavaScript文件中添加 import './xxxx.less' 來告訴webpack:你需要構建xxxx less/css文件了。這樣做功能是實現了,但是卻並不優雅,同時也會使JavaScript變得不純粹,也沒辦法被其他不用webpack作為構建工具的項目所重用了。
問題二:
在Node從server獲取數據后,會將pug模板渲染成html並將其發送回瀏覽器端,那么在模板中如何保持對靜態資源的引用呢?因為構建工具會為所有的資源文件添加MD5戳,所以我們在編碼時並不知道確切的文件名是什么。
解決方法:
利用webpack-manifest-plugin插件,它會在webpack構建完成后生成一個manifest.json文件,該文件會列出原始文件名與構建后的文件名之間的匹配關系。通過將manifest.json作為參數傳入pug的render方法中,這樣在pug模板中就可以通過類似於 img(src=manifest["logo.png"]) 的方式來保持對靜態資源的引用。
V1.0.0-beta:使用gulp+webpack對前端資源進行構建
問題總是有方法解決的,但是這樣寫起來總有一些別扭,也感覺很不優雅,這致使我們思考webpack是否真的適用於我們的項目。通過一番討論,最終決定嘗試使用gulp+webpack的方式進行構建。
- 利用webpack構建JavaScript資源,可以方便的利用webpack模塊的思想,使得JavaScript之間相互引用變得簡單、便捷;
- 利用gulp構建css, images等其他資源。
使用webpack對JavaScript進行構建時,為了不至於每次添加一個新文件,都要修改webpack的配置,所以我們寫了一個方法將所有的JavaScript都放入webpack的entry屬性中。
function entries(globPath) { const files = glob.sync(globPath); let key, name, ext, entries = {}; for (let file of files) { ext = path.extname(file); name = path.basename(file, ext); if (name.startsWith('_')) { continue; } entries[name] = path.join(__dirname, file); } return entries }
使用時,只需要在globPath中輸入指定的js路徑就可以了,如
let webpackConfig = { entry: entries('./public/**/*.js'), }
而在實際的開發中我們發現,有一些JavaScript文件存在的目的就是被其他文件引用,例如一些helper方法,它們是不會作為webpack的entry存在的,所以我們在entries方法中只尋找不是以下划線(_)開頭的JavaScript文件,因此對於一些只有helper方法的JavaScript文件,只需要將其文件名以_開頭,這樣就不會被添加到webpack的entry屬性中了,這也算是該構建方案中的一個小彩蛋~~~
在使用gulp對其他資源的構建過程中,我們用到了很多成熟的gulp插件,其中我認為比較重要的是gulp-rev和gulp-rev-replace。其中gulp-rev插件用來為css、images等資源文件添加MD5戳並生成相應的manifest.json文件,同時搭配使用gulp-rev-replace插件將pug、css等文件中存在於manifest.json里的文件名進行替換,這樣我們在pug模板中引用資源文件就可以直接寫 img(src=logo.png) 而不需要像以前那么復(chou)雜(lou)了。
再說一個題外話,其實在使用gulp-rev+gulp-rev-replace插件之前,我們嘗試使用過gulp-md5-plus插件。gulp-md5-plus插件使用起來超級方便,它可以添加MD5戳,也可以替換文件名,但是該插件暫不支持添加自定義前綴的功能,而這個功能對我們確實必須的。因為在生產環境中,所有的資源文件會放到CDN上,而測試環境中的資源文件則放在對應的測試服務器上,所以說,同樣的一張圖片,在測試環境的地址可能是public/images/logo.png,而生產環境卻是//cdn.demo.com/images/logo.png,所以我們需要支持自定義前綴功能,並使用類似下述的代碼為其添加前綴 var prefix = process.env.NODE_ENV === 'production' ? '//cdn.demo.com/': '/public/' 。
寫在最后:
上述構建方案也許不是那么完美,但是對於當前的項目來說,確是一個相對而言較為合適的。當然也不排除后續該方案會被其他更優雅的方案所替代。但是通過這么多次的嘗試、重構,才真正體會到了什么叫“適合的才是最好的😄”。