作為從grunt、gulp一路走來的老碼農,一開始用webpack的時候我是很抗拒的。但由於核心庫使用了vue,而webpack又是vue的最佳拍檔(vue作者專門為其寫了vue-loader),所以用webpack來構建項目就成了自然而然的事情。經過一段時間的摸索,各個流程都跑通了,「從入門到放棄」的那點事也就都不算事了。
webpack是以模塊為中心的打包工具,但由於其日漸豐富的插件,能做的事情已經很多了,從開發環境搭建到上線構建,幾乎可以一條龍包辦了。事實上我在最近的項目中,連gulp都省了,完全用webpack就完成了所有的工作。下面一一道來。
本地server
在前后端分離的大前提下,前端本地已不需要起后端服務了,那為什么還要起一個本地server呢?這個server主要提供以下支持:
-
靜態資源訪問
-
代碼熱更新(實時刷新瀏覽器)
-
模擬請求數據
-
代理http請求
本地server是由webpack插件webpack-dev-middleware提供的,它是基於express的,所以還需要把express也裝上。配置代碼相當簡單:
var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: config.output.publicPath, stats: { colors: true, chunks: false } }); app.use(devMiddleware);
通常我們把它保存為dev-server.js,然后配合npm script來啟動這個server,在package.json中寫好:
"scripts": { "dev": "node ./build/dev-server.js" }
然后一行命令就可以啟動了:npm run dev
需要注意的一點事,當我們起了本地server之后,webpack打包后的文件並不寫入到硬盤上,而是保存在內存中。所以你並不會在目錄下看到生成的文件,但是瀏覽器已經能夠按照路徑進行訪問了,這樣開發環境下的編譯速度就大大加快。
獨立vue組件
用了webpack之后,寫vue組件最爽的就是可以寫成單獨的.vue文件了,在一個文件中寫好所有的樣式、模板、js邏輯。然后vue-loader就會幫我們編譯成標准的vue組件。
令人高興的是,vue2.0引入了虛擬DOM來提高性能,vue-loader也會將我們的模板編譯成虛擬DOM來使用,你也不必費勁再去寫render函數了。
編譯ES6
之前也說過項目已經完全用ES6了,所以我們在開發環境需要用babel進行編譯,我們寫ES6代碼的地方有兩個,一個是js文件,另一個是.vue文件中的。
js文件的在webpack配置文件中配置loader即可:
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/ }
而.vue文件中的,vue-loader已經天然幫我們做了,所以不需要任何設置。
babel的配置項,寫在.babelrc中放在根目錄下即可。
編譯sass
前端進入編譯時代,css當然也是少不了的。項目中使用了sass來編寫css代碼,所以也需要在開發環境進行編譯。需要編譯的有兩個地方,一個是外鏈的.scss文件,另一個是.vue文件中的。
對於.scss文件,我們還是用loader來處理,安裝sass-loader,然后在webpack配置文件中配好:
{ test: /\.scss$/, loader: ExtractTextPlugin.extract('style', 'css!sass') }
至於.vue文件中的,vue-loader早幫我們做好了工作,只需在<style>標簽加上lang屬性就可以了,例如編譯sass
<style lang="sass"> /*此處可以寫sass代碼啦*/ </style>
代碼檢查
在開發階段進行代碼檢查也是一項必要工作,檢查ES6當然eslint是標配,在配置文件中可以通過preLoaders來配置:
preLoaders: [ { test: /\.(js|vue)$/, exclude: /node_modules/, loader: 'eslint-loader' } ]
這樣在js文件和.vue文件中的代碼都會用eslint規則進行檢查。eslint的配置同樣寫在.eslintrc文件中放在根目錄下。
代碼熱更新
編譯工作就是以上這些了。當我們開始正式敲代碼之后,還有一個功能是夢寐以求的,那就是代碼熱更新。即編輯器保存代碼后,瀏覽器實時刷新。這個特性在webpack中叫做”模塊熱替換(hot module repacement)“,使用webpack-hot-middleware這個插件來完成。
這個插件跟我們以前用的live-reload不同,它不會刷新瀏覽器頁面,而是把模塊進行熱替換。這樣的好處是,應用的當前狀態還能保持,但是代碼已經更新了。比以前爽了不止一點半點。
模擬數據
前后端分離開發中,還有重要的一環,就是前端mock數據。在后端接口開發完成之前,我們可以通過自己模擬的數據完成調試。
事實上mock數據並不需要webpack提供,而是通過我們的本地server,寫express中間件的方式。需要以下兩步:
第一步,根目錄下建一個mock目錄,用於放置假數據,每個接口一個js文件,為了便於express使用,文件的格式如下:
module.exports = { api: '/api/mock/banner', response: function (req, res) { res.json({ success: true, data: { name: 'test' } }); } }
第二步,在dev-server中把mock的數據掛載到express上,代碼如下:
var mockDir = path.resolve(__dirname, '../mock'); fs.readdirSync(mockDir).forEach(function (file) { var mock = require(path.resolve(mockDir, file)); app.use(mock.api, mock.response); });
這樣當我們請求的路徑中含有/api/mock字樣時,就會返回我們mock的數據啦。當后端的接口開發完成時,我們就可以把路徑中的mock去掉,從而去請求后端的接口。
代理請求
由於我們起了本地server,ajax路徑是相對的,所以請求會打到本地的這個server上。當我們需要調試后端接口時,就得把請求轉發到后端服務器。為了避免跨域的麻煩,通常需要配置http代理。
我使用了http-proxy-middleware這個中間件,這也是和webpack無關的,它也是一個express中間件。配置方法如下:
app.use(proxy('/api', { target: { host: 'localhost', protocol: 'http:', port: 5050 }, logLevel: 'debug' }));
我們和后端協商好,所有異步請求都帶"/api"前綴,這樣我就把請求轉發到后端服務器了。此處我把host填了localhost,意味着我本地起了一個后端服務,事實上可以指向任何一台可以提供接口的服務器。
這個proxy中間件有一個小坑,就是路徑必須向我這樣分開寫。如果拼在一起不會生效,我目前沒找到原因。
移動端遠程調試
出於平台兼容性的考慮,移動端調試我選擇了weinre。用過weinre的同學應該知道,我們需要在頁面上嵌一個<script>標簽才能使用weinre。不熟悉的同學可以參考我之前寫的這篇:http://www.cnblogs.com/lvdabao/p/3436620.html
所以,我們需要對當前運行的環境進行判斷,如果是開發環境才嵌入這個標簽。我用了一個叫做DefinePlugin的插件來在通過webpack在頁面全局定義變量,用法如下:
new webpack.DefinePlugin({ LOCAL_IP: JSON.stringify(localIP), __ENV__: JSON.stringify('dev'), WEINRE_RUN: JSON.stringify(process.argv[2]=='weinre') })
這樣,在頁面初始化的時候,我們就可以根據當前的環境來決定是否嵌入這個<script>啦,如下:
if(__ENV__ == 'dev' && WEINRE_RUN){ document.write('<script src="http://'+LOCAL_IP+':8080/target/target-script-min.js#anonymous"></script>'); }
插入<script>標簽
我們的js打包好后,下一步就是把它引入到html文件了。由於我們通常會用md5作為文件名來控制js版本,所以html中的文件地址就需要每次構建后進行更新。
webpack中是通過HtmlWebpackPlugin這個插件來實現的,它的特點在於,每次構建基於一個html模板來生成新的html文件,所以並不是替換js路徑。在webpack的plugins選項中如下配置:
new HtmlWebpackPlugin({ filename: path.resolve(__dirname, '../static/dist/index.html'), chunks: ['vendor', 'components', 'app'], template: path.resolve(__dirname, '../static/src/index.html'), inject: true })
代碼壓縮
在gulp時代,我們是通過配置gulp任務來完成js的壓縮。在webpack中,是通過插件來完成的,相同的道理,在plugins中進行如下配置:
new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })
另外,webpack可以進行sourcemap配置,用於幫助映射文件,開啟代碼如下:devtool: 'source-map'
打包結果分析
在打包完成之后,我們經常會有這個心理:生成的文件完全不可讀,打包的結果到底對不對,有沒有重復打包,有沒有冗余的東西。
這個時候就需要一個分析工具了,webpack有這樣一個插件,能以可視化的方式展示打包結果,為你提供分析需求。它就是BundleAnalyzerPlugin,相當好用,我上一張我的截圖:
上線構建
以上大部分是開發環境需要的配置,當我們本地測試完畢需要上線時,就需要真正生成文件了。
我們一般會准備兩個或者多個(如何有多個環境,如test環境)webpack配置文件,用於不同的環境。在線上服務器跑的配置文件我們一般會修改輸出地址,如果需要配cdn的話,把output中的publicPath填成cdn地址即可。然后直接運行webpack編譯命令,而不是像本地server那樣起。
同樣我們也會寫一個npm script,像這樣:
"scripts": { "dev": "node ./build/dev-server.js", "build": "webpack --config ./build/prod.webpack.config.js" }
我對webpack的應用大概就是這么多了,本想大致介紹,沒想到一貼就是這么多代碼~~其實webpack的精髓我本篇並未提及,那就是打包策略的選擇,不同的項目架構需要不同的打包方式,而這個過程,則充滿了挑戰,你懂的。
(本文始發於我的微信公眾號(doctor-programmer),整理重新編輯后發於博客,有興趣的同學歡迎關注我的公眾號,右邊掃碼去吧~)