原文: A production-ready realtime SaaS with webpack
作者: Matt Krick
翻譯: 黃祺(pinqy520)
譯者說:
最近在看meatier,某個meteor的替代方案,解決了meteor的幾個問題:技術方案陳舊(畢竟三年了)、編譯擴展不方便、學習成本高等等。它直接采用現在流行技術,并且使用webpack進行打包,降低了編譯擴展這塊的學習成本。
雖然這篇文章是去年年底的,但是關于webpack配置這塊依然值得學習(也可能會很快過時吧,哈哈)。
最后,第一次進行翻譯,如有錯誤歡迎指出。
我是Meteor的忠實粉絲,它讓一切都socket化(socketize)[1],編譯(transpile)樣式,然后為server生成一些代碼。然而有時候,可能需要更靈活一點。
像標準的SaaS[2],我需要一個可以從CDN獲取的小跳轉頁,一個提供實時websocket連接的門戶首頁,很容易在橫向和縱向進行擴展
最終結果就是Meatier[3],我將其放到github上了:http://github.com/mattkrick/meatier,他解決了很多問題:
- JWT(JSON Web Tokens)[4]
- sockets
- 使用redux做client-side緩存
- 將socket狀態存于redux的state中
- 優化和實時數據庫更新
但是在本文重心在于webpack,因為有100篇webpack 101指導,卻并沒有一篇webpack 201(講的都不深入?)。
在本項目中,使用了webpack 2 Beta,雖然還有一些bug,但是并不常見
構建production的webpack設置
我假設你已經設置好了一個開發用的webpack配置,如果你不知道如何創建一個開發配置......
使用路由來拆分你的頁面
第一步先處理路由,然后讓你的同步組件異步處理。參考這個例子。
export default store => {
return {
onEnter: requireNoAuth(store),
path: 'signup',
getComponent: async (location, cb) => {
let component = await System.import('./Signup');
cb(null, component)
}
}
}
這里的路由并不是指的jsx,而是一個在redux store中的函數(注:類似于一個reducer專門用來處理路由),用來更好的對ruduer進行代碼拆分(更多信息在下篇博客當中),重點在于System.import
中,它將創建一個promise來傳輸這個模塊,那么在webpack 2中,它將會變成一個獨立的可以被動態加載的模塊[5]。
為client編寫production-ready的webpack配置
好,現在webpack知道如何最好的來分割你的代碼了,這能節約流量。然而,有時候可能不會這么完美。比如說:在某種情況下,節約額外的5kb流量,并不比增加一次額外的http請求收益更高。所以我們要使用AggressiveMergingPlugin
,它能平衡你的請求大小比例。還有MinChunkSizePlugin
插件,可以用來設置閾值(例如50000)來限制一些小的chunks(注:代碼塊,可以理解為模塊)。
接下來是優化用戶第二次訪問的體驗(如果用戶會第二次訪問你的網站?),我們需要盡可能多的利用用戶的瀏覽器緩存,只傳輸少量的流量(意味著更少的錢和更快的速度)。我們首先拆分vendor包,因為有可能你不想BUG產生的次數和你更新React的次數一樣多。
然后是進行地址更新,當客戶端讀取一個資源的時候,在、瀏覽器看到一個文件名(URL地址),如果它能夠解析到一個本地緩存,那瀏覽器就不會發送請求。也就是說:假設你的文件叫app.js
,你更新了這個文件之后,瀏覽器并不會重新下載它,除非你關閉了緩存。為了解決這個問題,當文件改變時,將會給每個文件分配一個hash值(放到文件名里)。
output: {
filename: '[name]_[chunkhash].js',
chunkFilename: '[name]_[chunkhash].js',
...
},
現在,我們不能在HTML中靜態的請求一個app.js
(為了解決上面那個問題),我們需要請求一個在每次build之后文件名都會變化的文件,可以用AssetsPlugin
來創建一個查詢表,來存儲資源和hash后的名稱。
new AssetsPlugin({
path: path.join(root, 'build'),
filename: 'assets.json'
}),
然后,在生成HTML之前,我們就需要assets.json
,給src賦值assets.app.js
(assets
是require('assets.json')
,assets.app.js
是hash后的app.js
的地址,同理assets.vendor.js
和assets.manifest.js
也一樣)。
https://github.com/mattkrick/meatier/blob/master/src/server/Html.js
現在問題來了,如果一個chunk改變了,那也會改變其他chunk的hash值(因為webpack按照文件名進行獲取不同的模塊,那么一個文件的hash值變了,其他文件因為引用的模塊路徑有變化,也會變化),導致你其他的chunk也會刷新(被瀏覽器重新下載)。為了避免這種問題,NamedModulesPlugin
能夠將webpack模塊編號,替換成一個實際的路徑。只需要在沒有參數的情況下調用它,在下一次編譯中,所有的chunk都會由區分開的hash值。一般情況下,你不會想要客戶端知道你的實際路徑,這就是為啥我們要使用HashedModuleIdsPlugin
,它會增加一些額外的流量,但是能讓人安心。
現在,唯一會被刷新的chunk只有你改變過代碼的那個,并且會帶有webpack runtime(webpack自帶的一些代碼)。但是,這個webpack runtime已經被打包進了上一個公共的chunk中,可能叫vendor.js
。圍繞這一點,我們需要抽取出webpack runtime,因此相比于要刷新兩個文件,可以只更新一個你想要的文件。
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest'],
minChunks: Infinity
}),
因為現在最新的公共chunk一直都是有webpack runtime的,它將會在CommonsChunkPlugin
中被提取出來。需要注意的是,它并沒有相應的入口chunk,并且minChunks
被設置成無限的,所以沒有東西會被加進去。
為了得到HTML的內容,我們需要創建另外一個HTTP請求,但是額外的請求會造成額外的浪費,所以,我們將其嵌入其中。我們將從assets.json
中獲取路徑,然后將其中的內容讀取成字符串(打印到HTML模板)。
https://github.com/mattkrick/meatier/blob/master/src/server/createSSR.js
const assets = require('../../build/assets.json');
const readFile = promisify(fs.readFile);
assets.manifest.text = await readFile(path.join(root, 'build', path.basename(assets.manifest.js)), 'utf-8');
為server編寫production-ready的webpack配置
最后一步比較麻煩,對于CSS文件你有四種選擇:
- 將css提取成樣式表(
<link />
), - 將你組建中的樣式寫成inline-style的
- 將css提取到一系列style標簽中
- 混合方案
我個人比較喜歡提取成樣式表,以消耗額外的HTTP請求為代價(但是它加載的很快,不需要javascript,并且可以對樣式進行瀏覽器緩存)。最理想的情況下,每個stylesheet就是一個chunk。但是現在在服務端渲染中還不可能(作者說:如果他錯了就告訴他),同樣的css可能會被重復多次,將會被壓縮到很小,所以就讓我們優化一個大的樣式表吧。
現在有很多hacky的解決方案去解決這個:node并不知道如何需要require
一個CSS文件的問題。如下幾種:
- 在server端忽略CSS require
- 在每個組件中加入
process.env.BROWSER
環境變量 - 將他們探測出來,放置在webpack的
stats.json
文件中,然后在服務端將它們插入進去。 - 給每個組件一個能被父級訪問的
this.styles
,然后一路被父級訪問,直到創建了一個style
標簽。 - 將styles放到組建的上下文中
然而,我并不喜歡上面任何一個。重寫組件只為了能夠使用服務端渲染,是一個錯誤的想法。因此,我決定在server端生成一個路由的webpack build,因為webpack知道如何處理css文件(node不知道)。它(webpack)能編譯整個路由,并且用它在server端渲染每個頁面。這很容易,將你的target
設置為node
,并且output.libraryTarget: "commonjs2"
。
https://github.com/mattkrick/meatier/blob/master/webpack/webpack.config.server.js
接下來用ExtractTextPlugin
創建你的css文件。然而在webpack 1 和 2 中,出現了一些問題:為了將所有樣式打包成一個文件,我設置allChunks
為true
,結果還是生成了多個CSS文件。為了解決這個問題,我使用了LimitChunkCountPlugin
,將chunk限制設置為1。結果就是,你的網站會使用一個可以被server用來渲染HTML的預處理好的路由,和一個包含網站所有樣式的CSS文件。
為了加快服務端啟動的時間,假設你在服務端使用了babel。你可以用如下選項排除調這個預渲染好的bundle
require('babel-register')({
only(filename) {
return (filename.indexOf('build') === -1 && filename.indexOf('node_modules') === -1);
}
});
結語
好了,現在你知道如何來便寫一個production ready的webpack配置文件了,它將在服務端渲染中工作,并不需要在客戶端中允許javascript。你不需要重寫你的組件來處理樣式,并且,它在開發中依然很快。無論何時,你在開發server端的時候,你都可以運行你的production構建(build),并且你不需要在每次重啟后,重新編譯你的client端(bundle)。
-
譯者注: 『一切編程都是Socket』Socket起源于Unix,而Unix基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。Socket就是該模式的一個實現。所以使用javascript同構變成的方式,把網絡請求都變成類似與讀寫的操作,通過websocket將前后端數據同步。 ?
-
譯者注:可以理解成一個web app ?
-
譯者注:作者寫的一個類似于meteor的webpack構建saas的模板。 ?
-
譯者注:https://jwt.io/ ?
-
譯者注:在webpack2中,遵循ES6的代碼拆分方式,可以使用
System.import
方法,在運行時動態加載es6模塊。webpack把System.import
作為拆分點,然后把請求的模塊放入一個單獨的『塊』(chunk)中。 ?