最近用Webpack+npm scripts+Mongodb+Nodejs+React寫了個后台項目,在用Webpack構建過程中遇到了許多坑,就寫出來分享一下。
構建工具五花八門,想當年剛學會Grunt,Grunt就被淘汰了,取而代之的是Gulp,其任務流式的機制,有着邏輯清晰,靈活多變的特點,而且容易上手,相比Grunt真的要少寫太多配置文件代碼了,立馬就學的風聲水起,剛熟練Gulp,Webpack又如構建工具界的一顆新星冉冉升起,其獨特的模塊打包機制和各種各樣好用的loader,讓無數Coder為之青睞,加之和React,ES6的完美配合,博主又立馬放棄Gulp,抱着懷疑的心態嘗試純用Webpack構建一個項目(注意:Webpack和Gulp並不是沖突的,曾在項目中結合使用過,但博主決定試一試完全不用Gulp是否可行),結果當然是肯定的,Gulp有的東西(壓縮,合並,MD5)等等,你幾乎都可以用Webpack來實現一遍,再配上npm scripts,簡直如虎添翼。
首先我簡單介紹一下npm scripts。先來看一段代碼。
{
"name": "app",
"version": "0.0.1",
"private": true,
"main": "./bin/www",
"scripts": {
"clean": "rm -rf client/dist/*",
"copy": "rsync -a --exclude=*.html --exclude=*.jsx ./client/src/*.* ./client/dist",
"start": "./bin/www",
"server": "node server.js",
"build": "npm run clean && webpack --config webpack.config.pro.js && npm run copy && node qiniu.js"
},
"dependencies": {
"babel-runtime": "^6.11.6",
"body-parser": "~1.15.1",
"bootstrap-sass": "^3.3.7",
"classnames": "^2.2.5",
"cookie-parser": "~1.4.3",
"debug": "~2.2.0",
"ejs": "~2.4.1",
"express": "~4.13.4",
"mongoose": "^4.6.4",
"morgan": "~1.7.0",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-redux": "^4.4.5",
"react-router": "^2.8.1",
"redux": "^3.6.0",
"serve-favicon": "~2.3.0"
},
"devDependencies": {
"autoprefixer": "^6.5.1",
"babel-core": "^6.17.0",
"babel-loader": "^6.2.5",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-preset-es2015": "^6.16.0",
"babel-preset-react": "^6.16.0",
"css-loader": "^0.25.0",
"cssnano": "^3.7.7",
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0",
"html-webpack-plugin": "^2.24.0",
"node-sass": "^3.10.1",
"postcss-loader": "^1.0.0",
"qiniu": "^6.1.13",
"react-hot-loader": "^3.0.0-beta.6",
"sass-loader": "^4.0.2",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.2",
"webpack-dev-server": "^1.16.2",
"webpack-md5-hash": "0.0.5"
}
}
做前端的童鞋們不可能不接觸這個配置文件package.json,是npm幫助我們管理依賴的重要配置文件,其中的scripts那一塊,就是npm scripts的使用方式啦,凡是在npm的scripts屬性中配置的鍵值對,都可以通過npm run xxx【xxx為鍵名】來執行對應的值里面的命令,比如:npm run server,就會執行node server.js,npm scripts支持bash shell。是不是有點熟悉?你可以把多個命令配置在一個鍵名下,通過&&符號連接,這樣執行完第一個,就會執行第二個,以此類推,直到最后一個執行完畢就結束運行,如果你想同時並行執行,可以用一個&符號,不過貌似只有bash支持,你可以通過npm-run-all插件或者parallelshell插件來做到並行執行。
廢話太多了,接下來開始說Webpack,不得不再廢話一句,我們以前使用gulp的時候一般也會配置兩套任務流,開發環境和生產環境,webpack當然也可以做到,只不過不是像gulp那樣用任務的方式自由組合,而是寫兩個配置文件。
webpack.config.pro.js
webpack.config.dev.js
我們先來說一說開發環境,webpack.config.dev.js這個配置文件。這個配置文件里面使用了webpack-dev-server,webpack-dev-serve類似gulp里面的browserSync,可以創建一個前端服務器,具有代碼變動監測,自動刷新頁面,熱替換等功能。這里我把webpack-dev-server的配置文件單獨拿出來,寫了一個server.js,我們可以通過node server.js來執行這個文件,這個文件會創建一個dev server,並注入webpack.config.dev.js的配置來開啟服務器。
contentBase屬性相當於browserSync里面的baseDir,是一個服務器的運行文件目錄。
hot這個屬性跟熱替換有關,先不說。
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server'); var config = require('./webpack.config.dev'); new WebpackDevServer(webpack(config), { contentBase: ['./client/src'], stats: { colors: true }, hot: true, historyApiFallback: true, headers: { 'Access-Control-Allow-Origin': '*' } }).listen(3001, 'localhost', function(err, result) { if (err) { return console.log(err); } console.log('Listening at http://localhost:3001/'); });
下面我們再看一下webpack.config.dev.js這個文件。
var path = require('path'); var webpack = require('webpack'); var autoprefixer = require('autoprefixer'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var source_dir = './client/src'; module.exports = { cache: true, context: __dirname, devtool: 'cheap-module-eval-source-map', entry: { vendors: [ 'webpack-dev-server/client?http://0.0.0.0:3001', 'classnames', 'echarts', 'immutable', 'isomorphic-fetch', 'jwt-decode', 'lodash', 'react', 'react-addons-css-transition-group', 'react-dom', 'react-motion', 'react-redux', 'react-router', 'react-select', 'redux', 'redux-logger', 'redux-thunk', 'reselect' ], business: [ 'babel-polyfill', source_dir + '/router', 'webpack/hot/dev-server' ] }, output: { path: '/', filename: 'scripts/[name].js' }, module: { loaders: [{ test: /\.js[x]?$/, include: /client\/src/, loader: 'babel' }, { test: /\.scss$/, include: /(client\/src\/containers|client\/src\/components)/, loader: ExtractTextPlugin.extract('style', 'css?modules&sourceMap&localIdentName=[name]__[local]-[hash:base64:5]!postcss!sass?outputStyle=expanded&sourceMap') }, { test: /\.(scss|css)$/, include: /client\/src\/assets\/styles/, loader: ExtractTextPlugin.extract('style', 'css!postcss!sass?outputStyle=expanded&sourceMap') }, { test: /\.(png|jpe?g|gif)$/, include: /client\/src\/assets\/images/, loader: 'url?limit=2048&name=/images/[name].[hash:8].[ext]' }, { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, include: /client\/src\/assets\/images/, loader: 'url?limit=2048&minetype=image/svg+xml&name=/images/[name].[hash:8].[ext]' }, { test: /\.(eot|ttf|woff|woff2|svg)$/, include: /client\/src\/assets\/fonts/, loader: 'url?limit=2048&name=/fonts/[name].[hash:8].[ext]' }] }, postcss: function() { return [ autoprefixer({ browsers: ['last 2 versions'] }) ]; }, plugins: [ new webpack.optimize.CommonsChunkPlugin('vendors', 'scripts/vendors.js', Infinity), new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), new ExtractTextPlugin('styles/main.css', { allChunks: true }), new webpack.DefinePlugin({ ENV: JSON.stringify(require('./config.client.dev')) }) ], resolve: { extensions: ['', '.js', '.jsx'] } };
有點懵逼?沒關系,我們一一道來。
context這個屬性是配置文件的上下文環境,我們用node的__dirname就行了。
devtool是用來配置使用哪種sourceMap的,這里我就不多說了,看官方文檔,要注意的是不要在生產環境配置這個屬性,會導致文件巨大,而且生產環境不是用來調試的,不需要sourceMap。
entry是最關鍵的屬性,它的值是一個對象,對象的鍵名是文件名,值可以是字符串或者數組,可以將一個或多個js文件合並到一個文件中,以鍵名指定的文件名命名,最后輸出。這里我們一般會把業務邏輯代碼和框架庫的代碼分開來,這樣每次改動業務代碼重新編譯就不會去編譯體積較大的框架和庫文件了,提高了編譯效率,不過前提是你要使用CommonsChunkPlugin這個插件,將框架、庫文件單獨輸出合並成一個vendors文件,並在頁面中引入。
webpack-dev-server/client?http://localhost:3001,引入這個的目的是什么?這個就是啟用server自動刷新必須要添加的模塊,當然你也可以用--inline模式,但是api方式不支持inline模式,所以必須要把這個模塊加在你的所有業務邏輯文件之前。這樣你的代碼一改,你發現了什么?自動刷新了吧?哈哈哈!
有了自動刷新,還不滿足,我們想要熱替換,什么是熱替換?就是在不刷新頁面的情況下,改變代碼就自動改變頁面對應的內容。大大節省開發時間(不過這個功能還處於測試階段)。我這里將它和react hot loader結合使用。
這里出現第一個坑,在新版的hot loader里面,如果你把hot-loader加在babel-loader前面,會出現一個錯誤,Module build failed:The Webpack loader is now exported separately.如果你的loader是不穩定版的,我建議新建一個.babelrc文件。
{
"presets": ["react", "es2015"], "plugins": ["react-hot-loader/babel", "transform-runtime"] }
然后在文件中作如上配置,除了這個配置,你還需要加上如下配置:
- webpack/hot/dev-server,加在你的業務邏輯js文件后面,這里有第二個坑,如果你打包了多個入口,需要在每個入口都加上一個webpack/hot/dev-server,否則不起作用。
- 在plugins模塊中加入new webpack.HotModuleReplacementPlugin()
- 並在之前的那個server.js中將hot屬性設為true
這里我附上一個官方的Troubleshooting鏈接。
https://github.com/gaearon/react-hot-loader/blob/master/docs/Troubleshooting.md
這里有個地方要注意一下,很多用gulp轉來用webpack的新手會有個困擾,開發環境下我們編譯的文件去哪里了?以前我們用gulp的時候一般會生成一個.tmp臨時文件夾,但是webpack好像你找來找去沒有找到,在哪里呢?其實webpack給你放到內存里了,你是不會在磁盤中找到這些文件的,如果你想查看這些文件,可以在瀏覽器中輸入像如下的路徑。
http://localhost:3001/webpack-dev-server
這樣你就能看到那些編譯過的文件了,或者你也可以使用chrome的開發者工具 -> sources里面也可以看到。
至於配置文件中的其他loader和plugin我就不一一講述了,官方文檔和網上的帖子一大堆,童鞋們自己去研究吧。
怎么樣,開發環境很簡單吧?那么下面我們來配置生產環境,生產環境去掉一些調試的工具,加上一些編譯優化的工具。
下面是生產環境的配置文件webpack.config.pro.js。
var path = require('path'); var webpack = require('webpack'); var autoprefixer = require('autoprefixer'); var ExtractTextPlugin = require('extract-text-webpack-plugin'); var WebpackMd5Hash = require('webpack-md5-hash'); var HtmlWebpackPlugin = require('html-webpack-plugin'); var source_dir = './client/src'; var config = require('./config.server'); module.exports = { cache: true, context: __dirname, entry: { vendors: [ 'classnames', 'echarts', 'immutable', 'isomorphic-fetch', 'jwt-decode', 'lodash', 'react', 'react-addons-css-transition-group', 'react-dom', 'react-motion', 'react-redux', 'react-router', 'react-select', 'redux', 'redux-logger', 'redux-thunk', 'reselect' ], business: [ 'babel-polyfill', source_dir + '/router' ] }, output: { path: 'client/dist', publicPath: config.qn_access.origin, filename: 'scripts/[name].[chunkhash:8].js' }, module: { loaders: [{ test: /\.js[x]?$/, include: /client\/src/, loader: 'babel' }, { test: /\.scss$/, include: /(client\/src\/containers|client\/src\/components)/, loader: ExtractTextPlugin.extract('style', 'css?modules&sourceMap&localIdentName=[name]__[local]-[hash:base64:5]!postcss!sass?outputStyle=expanded&sourceMap') }, { test: /\.(scss|css)$/, include: /client\/src\/assets\/styles/, loader: ExtractTextPlugin.extract('style', 'css!postcss!sass?outputStyle=expanded&sourceMap') }, { test: /\.(png|jpe?g|gif)$/, include: /client\/src\/assets\/images/, loader: 'url?limit=2048&name=/images/[name].[hash:8].[ext]' }, { test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, include: /client\/src\/assets\/images/, loader: 'url?limit=2048&minetype=image/svg+xml&name=/images/[name].[hash:8].[ext]' }, { test: /\.(eot|ttf|woff|woff2|svg)$/, include: /client\/src\/assets\/fonts/, loader: 'url?limit=2048&name=/fonts/[name].[hash:8].[ext]' }] }, postcss: function() { return [ autoprefixer({ browsers: ['last 2 versions'] }) ]; }, plugins: [ new WebpackMd5Hash(), new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') }, ENV: JSON.stringify(require('./config.client.pro')) }), new ExtractTextPlugin('styles/main.[contenthash:8].css', { allChunks: true }), new webpack.optimize.CommonsChunkPlugin('vendors', 'scripts/vendors.[chunkhash:8].js', Infinity), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({ sourceMap: false, mangle: false, compress: { warnings: false } }), new HtmlWebpackPlugin({ title: '奇速后台', template: 'client/src/template.html' }) ], resolve: { extensions: ['', '.js', '.jsx'] } };
很多人知道output是用來指定輸出路徑的模塊,path是用來指定輸出文件的目錄,publicPath主要是給很多插件提供的路徑,比如替換靜態資源路徑,在開發環境下我們用不到,可以不配置,filename是輸出的文件名,這里出現第三個坑,filename不單單可以指定文件名,也可以在文件名前面加路徑,但它會成為path的子路徑,path會是所有插件的上下文輸出路徑,所有插件的輸出路徑都會繼承這個父路徑,在開發環境下,我們配置了服務器的目錄並加載這個配置文件后,path是相對於服務器地址的,一般是/,直接丟到服務器根路徑就行了,會輸出文件到根目錄(在內存中)。而生產環境中,path的路徑是真實的輸出路徑,會在服務器上產生文件,我這邊是輸出到了client/dist。
生產環境下我們需要壓縮JS,很簡單:
new webpack.optimize.UglifyJsPlugin({
mangle: false, sourceMap: false, compress: { warnings: false } })
這邊的mangle屬性表示是否要混淆形參的名字,壓縮過js的都知道壓縮后的js的參數會被轉化成a,b,c,d這些簡單的無語義的字母,如果不是對js有嚴苛的大小要求,這里可以把它關閉,因為在合並入一些第三方插件的時候,第三方插件代碼的不規范,會導致壓縮后出現無法定位調試的錯誤,或者你也可以手動指定一些不要混淆的代碼,比如module.exports。sourceMap一定要關閉,只在開發調試環境有用,生產環境會產生大量無用的映射代碼。
ExtractTextPlugin用來把sass、less、css文件單獨導出,怎么使用這里不多介紹了,但是這里有一個css中引用圖片路徑的坑:
我們首先明確一下,不管是grunt中的usemin,gulp中的useref,rev,還是webpack中的loader,無非在做兩件事情,第一件事都是幫助我們輸出文件到指定路徑,第二件事改變引用的地方的路徑,使之可以正確找到(有的可能只具備輸出,不具備改路徑,有的可能只是用來替換路徑,不具備輸出,有的都具備)。
以開發環境為例子,現在假設我們使用了url-loader,它是同時具備輸出和改引用路徑功能的,我配置了大於2kb的圖片會被作為md5過的獨立文件輸出,並改變原引用路徑,假設現在url-loader設置的路徑是 images/[name].[hash:8].[ext],這個路徑在js中import或者require圖片會正確輸出到如下地址,同時js或jsx也能正確找到圖片:
[主機地址:端口]/images/xxx.[md5].png
現在我要在css中引用一張圖片(background-image),同時ExtractTextPlugin是這么配置的,ExtractTextPlugin.extract('style', 'css!postcss!sass?outputStyle=expanded&sourceMap'),編譯時,ExtractTextPlugin中的css-loader會去css里面找有沒有匹配的后綴,比如png,然后應用url-loader,最后css中的路徑變成了:
background-image:url('images/xxx.[md5].png')
圖片正確輸出到了內存中,說明輸出文件確實是繼承了path的根目錄來輸出的,這點毋庸置疑,如下圖。

但是這個background-image的路徑確是有問題的,在我調試的時候,css文件就會以相對路徑去找圖片,結果如下:
[主機地址:端口]/styles/images/xxx.[md5].png,果然出了404錯誤
這顯然不是我們想要的結果,所以url-loader這邊我們還是要使用絕對路徑 /images/[name].[hash:8].[ext],這樣就不會被css的輸出路徑所影響。
如果你任性,就是想用相對路徑,那么這時候publicPath的作用就體現了,其實本質上是用來替換主機地址,變為cdn地址的:
ExtractTextPlugin.extract('style', 'css!postcss!sass?outputStyle=expanded&sourceMap', { publicPath: [你的cdn地址] }),或者在output里面設置全局的也可以,這樣配置后編譯出來的結果如下:
[cdn]/images/xxx.[md5].png
這才是我們想要看到的。
另外像autoprefixer這些插件就不介紹了,gulp能做的,webpack有過之而無不及。
最后更改文件md5碼相同的問題,這里引用一篇文章。
http://www.cnblogs.com/ihardcoder/p/5623411.html
看完我相信你就懂了,用上WebpackMd5Hash這個插件,妥妥的解決了問題。
最后最后,如果你想要替換靜態html中的css和js引用路徑,可以使用HtmlWebpackPlugin這個插件,自動生成模板文件,包括引入rev過地址的js和css,還自帶了壓縮模板文件的功能,也可以使用各種模板引擎,ejs,jade等等。
什么?你還想要替換html中的<img src="" />的靜態路徑?正常來講做react開發,不可能發生這種事情,不過萬一你用了Angular呢?是吧?那么也有辦法:
<img src="<%= require('/圖片路徑') %>" />,這樣就可以替換html中的靜態資源了。
結語:長江后浪推前浪,前浪死在沙灘上,最早從grunt開始學,到gulp,再到webpack,webpack2,構建工具層出不窮,學習這些構建工具是我們邁向前端工程化模塊化必須要走的路,前端也不過是在走后端的老路,不管學什么,能不能用上,學習本身是不會做無用功的,你學習的每一樣東西,即使淘汰了,也會對你今后學習其他東西有幫助,重要的不是只知道工具的規則,更要知道為什么這么用,它是如何實現的。
