Webpack是一個module bundler。利用webpack就可以開心的管理我們的前端程式碼了, 使用npm管理module, 輸出source maps, 將大的javscript檔案拆成多個檔案, 幫我們前置處理像是jsx, coffeescript, sass, ES6....
如果只用了幾個<script src='...'>
那可能看不出webpack效益, 不過若web app很巨大,或是要長的很大, 那麼用webpack效益就會很明顯。
參考這篇, 翻譯與整理。
官網的內容很多, 先縮小一下範圍, 假設一下我的使用情境: 這次我想使用webpack, 管理我的SPA, 做一個可以很快速產出prototyping的環境。
我的SPA會大量用到react, boostrap和jQuery, SPA和REST API有可能會放在相同的Express server上, 我的Express server開發的時候可以hot-reload, 當我改變我的sass或是js程式碼的時候, 就會有webpack幫我在記憶體自動編譯,然後瀏覽器就會重新載入顯示更改過後的結果, 當我想要上線的時候, 透過webpack可以幫我的前端程式碼最佳化。
.
├── app
│ ├── main.js
│ └── main.scss
├── package.json
├── public
│ └── index.html
├── server
│ └── bundle.js
├── server.js
├── webpack.config.js
└── webpack.production.config.js
其中:
app
: SPA主資料夾app/main.js
: 我們app的進入點(entry point)public
: 依照express的慣例, 這裡放static filepublic/index.html
: 主要就是引用我們編好的js程式碼server/bundle.js
: bundler主程式server.js
: express server 和 proxywebpack.config.js
: 開發時期用的webpack設定檔webpack.production.config.js
: 要產出上線的webapck設定檔一開始, server.js
主要就是區分上線或是prototype, 並且提供存取index.html的服務:
var express = require('express');
var path = require('path');
var app = express();
var isProduction = process.env.NODE_ENV === 'production';
var port = isProduction ? 8080 : 5000;
var publicPath = path.resolve(__dirname, 'public');
app.use(express.static(publicPath));
app.listen(port, function(){
console.log('Server running on port ' + port);
});
public/index.html
單純就是引用bundle好的js檔案, 這裡我們設定bundle好的js檔案放在public/bundle
下:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prototype</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="build/bundle.js"></script>
</body>
</html>
參考官網設定, 編寫webpack.config.js
:
var path = require('path');
var webpack = require('webpack');
var nodeModulePath = path.resolve(__dirname, 'node_modules');
var buildPath = path.resolve(__dirname, 'public', 'build');
var mainPath = path.resolve(__dirname, 'app', 'main.js');
var config = module.exports = {
devtool: 'eval'
};
把相關路徑先做個整理, 先設定devtool: 'eval'
方便之後使用devtool作debug。
再來設定要bundle的進入點:
config.entry= [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
mainPath
];
我們pass給entry
一個陣列, 那麼所有的module都會在開始的時候載入, 然後匯出最後一個(mainPath
),
我們使用webpack-dev-server監看改變,webpack-dev-server就是一個epxress+socket.io server, 當我們js或style檔案改變的時候, webpack-dev-server就會幫我們動態更新頁面, 因此要設定webpack/hot/dev-server
和 webpack-dev-server/client?http://localhost:8080
這兩個特殊的entry point。
設定我們開發環境的輸出:
config.output= {
path: buildPath,
filename: 'bundle.js',
publicPath: '/build'
};
這裡是說, 我們把bundle好的js檔案輸出到buildPath
路徑的bundle.js
, 不過我們這邊借用webpack-dev-server, 所有的bundle都是在記憶體執行, 沒有實際輸出到檔案, 因此這個設定是沒有作用的, 不過這兩個設定拿掉webpack會報錯, 因此需要加著。
所有關於webpack的檔案應該都要過publicPath
(build path), 這裡代表都會經過http://localhost:5000/build
, 這樣方便proxy來處理。
再來設定loaders:
config.module = {};
config.module.loaders= [
{
test: /\.(js|jsx)$/,
loader: 'babel',
exclude: [nodeModulePath]
},
{
test: /\.css$/,
loader: 'style!css'
},
{
test: /\.scss$/,
loader: 'style!css!sass'
},
{
test: /\.(png|woff|woff2|eot|ttf|svg)$/,
loader: 'url-loader?limit=100000'
}
];
Loaders就很像其他build工具(例如gulp)裏面的tasks
用來轉換檔案, 引用的loader名字可以簡寫例如babel-loader
可以簡寫為babel
。Loader是可串聯的, 串聯利用!
來表示, 例如style!css!sass
表示style-loader處理完換css-loader處理,最後sass-loader處理。
上面的意思這樣就很好理解了, 只要js檔案(除了node_modules
)都會先利用babel-loader前置處理轉換ES6程式碼, 剩下類推。 babel-es6支援 react的JSX語法處理, 因此我們的.jsx或是js檔案都使用babel-loader來前置處理。
loders可參考 Loader列表
記得要安裝相關loader套件:
$ npm i -save-dev babel-loader style-loader css-loader url-loader sass-loader
config.plugins= [
new webpack.HotModuleReplacementPlugin(),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
_: 'lodash',
React: 'react'
})
];
Webpack的能力可以靠著Plugins來擴充, HotModuleReplacementPlugin
啟動Hot Module Replacement功能, ProvidePlugin
幫我們自動載入一些module, 例如上面設定後, 我們不再需要每個檔案用到jQuery時都要var $ = require('jquery');
, 直接使用就成了,例如:
$('#item').show();
var Webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var webpackConfig = require('../webpack.config.js');
var path = require('path');
var fs = require('fs');
var mainPath = path.resolve(__dirname, '..', 'app', 'main.js');
module.exports = function(){
var bundleStart = null; //bundle起始時間
var compiler = Webpack(webpackConfig); //注入webpack設定檔
//編譯開始時的顯示訊息
compiler.plugin('compile', function(){
console.log('Bundling....');
bundleStart = Date.now();
});
//編譯完成後的顯示訊息
compiler.plugin('done', function(){
console.log('Bundleed in ' + (Date.now() - bundleStart) + 'ms!');
});
var bundler = new WebpackDevServer(compiler, {
publicPath: '/build/',
hot: true, //啟動hot replacement
//以下為終端機顯示設定
quiet: false,
noInfo: true,
stats: {
color: true
}
});
//webpakc-dev-server 監聽port為8080
bundler.listen(8080, 'localhost', function(){
console.log('Building project, please wait....');
});
};
只要是http://locahost:5000/build
的內容, 都會透過proxy轉送到這個http://localhost:8080/build
這個webpack-dev-server來處理, webpack-dev-server就會依照我們的webpack.config.js設定檔, 將檔案前置處理並bundle好後回傳(在記憶體中未寫入檔案)。
修改一下我們原來的server.js
, 加入http-proxy
, 利用proxy將要bundle的檔案透過proxy轉給webpack-dev-server:
var express = require('express');
var path = require('path');
var httpProxy = require('http-proxy');
var proxy = httpProxy.createProxyServer();
var app = express();
var isProduction = process.env.NODE_ENV === 'production';
var port = isProduction ? 8080 : 5000;
var publicPath = path.resolve(__dirname, 'public');
app.use(express.static(publicPath));
//如果不是production mode, 就啟動webpack-dev-server
if (!isProduction){
var bundle = require('./server/bundle.js');
bundle();
//所有localhost:5000/build的請求都proxy到webpack-dev-server
app.all('/build/*', function(req, res){
proxy.web(req, res, {
target: 'http://localhost:8080'
});
});
}
//catch proxy errors!
proxy.on('error', function(e){
console.log('Could not connect to proxy, please try again...');
});
app.listen(port, function(){
console.log('Server running on port ' + port);
});
另外寫一個webpack.production.config.js
:
...
config.entry= mainPath ;
config.output= {
path: buildPath,
filename: 'bundle.js'
};
...
config.plugins= [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
_: 'lodash',
React: 'react'
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
];
與原來webpack.config.js
的差別在於, 我們拿掉了所有有關webpack-dev-server的設定,
另外設定了UglifyJSPlugin
的選項把warnning訊息取消掉。
require('../node_modules/bootstrap/dist/css/bootstrap.min.css');
require('./main.scss');
require('bootstrap'); /* bootrstap.min.js */
在我們的bundle進入點app/main.js
中加入對應引用, webpack就可以幫我們處理對應的前置處理與打包了! 不僅js檔案, css,scss都可以引用。
修改一下package.json
, 這裡我加上了簡單的scripts:
...
"scripts": {
"dev": "rm -r public/build; node server",
"build": "rm -r public/build; NODE_ENV=production webpack -p --config webpack.production.config.js",
},
...
不管是啟動開發或是production的bundle, 都先把public/build
的內容物清除, 執行npm run dev
就啟動了有webpack-dev-server的環境, 這樣只要修改任何的js或css檔案都會即時的前置編譯並在瀏覽器即時顯示修改結果。
執行npm run build
就會使用webpack.production.config.js
設定檔做編譯, 實際的將bundle好的檔案寫入到public/build
去。
完整的結果我放在這裡。
webpack with practical examples
gulp、webpackでjqueryプラグイン(scrollmagic、tweenmax等)を使う時の設定