最近用Webpack+npm scripts+Mongodb+Nodejs+React寫了個後臺項目,在用Webpack構建過程當中遇到了許多坑,就寫出來分享一下。css
構建工具五花八門,想當年剛學會Grunt,Grunt就被淘汰了,取而代之的是Gulp,其任務流式的機制,有着邏輯清晰,靈活多變的特色,並且容易上手,相比Grunt真的要少寫太多配置文件代碼了,立馬就學的風聲水起,剛熟練Gulp,Webpack又如構建工具界的一顆新星冉冉升起,其獨特的模塊打包機制和各類各樣好用的loader,讓無數Coder爲之青睞,加之和React,ES6的完美配合,博主又立馬放棄Gulp,抱着懷疑的心態嘗試純用Webpack構建一個項目(注意:Webpack和Gulp並非衝突的,曾在項目中結合使用過,但博主決定試一試徹底不用Gulp是否可行),結果固然是確定的,Gulp有的東西(壓縮,合併,MD5)等等,你幾乎均可以用Webpack來實現一遍,再配上npm scripts,簡直如虎添翼。html
首先我簡單介紹一下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插件來作到並行執行。node
廢話太多了,接下來開始說Webpack,不得再也不廢話一句,咱們之前使用gulp的時候通常也會配置兩套任務流,開發環境和生產環境,webpack固然也能夠作到,只不過不是像gulp那樣用任務的方式自由組合,而是寫兩個配置文件。react
webpack.config.pro.jswebpack
webpack.config.dev.jsgit
咱們先來講一說開發環境,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的配置來開啓服務器。github
contentBase屬性至關於browserSync裏面的baseDir,是一個服務器的運行文件目錄。web
hot這個屬性跟熱替換有關,先不說。chrome
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"] }
而後在文件中做如上配置,除了這個配置,你還須要加上以下配置:
這裏我附上一個官方的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,構建工具層出不窮,學習這些構建工具是咱們邁向前端工程化模塊化必需要走的路,前端也不過是在走後端的老路,無論學什麼,能不能用上,學習自己是不會作無用功的,你學習的每同樣東西,即便淘汰了,也會對你從此學習其餘東西有幫助,重要的不是隻知道工具的規則,更要知道爲何這麼用,它是如何實現的。