本文github倉庫地址: https://github.com/Rynxiao/webpack-tutorial ,裏面包括了本教程的全部代碼。javascript
【若是你以爲這篇文章寫得不錯,麻煩給本倉庫一顆星:-D】css
webpack is a module bundler.
webpack takes modules with dependencies and generates static assets representing those modules.html
簡單的歸納就是:webpack是一個模塊打包工具,處理模塊之間的依賴同時生成對應模塊的靜態資源。前端
圖中已經很清楚的反應了幾個信息:java
webpack --config webpack.config.js
)npm install -g webpack
npm install webpack // 處理相似以下調用 import webpack from "webpack"; var webpack = require("webpack");
建議安裝淘寶的npm鏡像,這樣下載npm包會快上不少,具體作法:node
// 方式一 npm install xx --registry=https://registry.npm.taobao.org/ // 方式二:安裝淘寶提供的npm工具 npm install -g cnpm cnpm install xx // 方式三 // 在用戶主目錄下,找到.npmrc文件,加上下面這段配置 registry=https://registry.npm.taobao.org/
建立配置文件(webpack.config.js
,執行webpack命令的時候,默認會執行這個文件)react
module.export = { entry : 'app.js', output : { path : 'assets/', filename : '[name].bundle.js' }, module : { loaders : [ // 使用babel-loader解析js或者jsx模塊 { test : /\.js|\.jsx$/, loader : 'babel' }, // 使用css-loader解析css模塊 { test : /\.css$/, loader : 'style!css' }, // or another way { test : /\.css$/, loader : ['style', 'css'] } ] } };
說明一: webpack.config.js
默認輸出一個webpack
的配置文件,與CLI
方式調用相同,只是更加簡便
說明二: 執行webpack
命令便可以運行配置,先決條件,全局安裝webpack
,項目安裝各模塊loader
說明三: entry
對應須要打包的入口js
文件,output
對應輸出的目錄以及文件名,module
中的loaders
對應解析各個模塊時須要的加載器jquery
一個簡單的例子webpack
basic/app.js
git
require('./app.css'); document.getElementById('container').textContent = 'APP';
basic/app.css
* { margin: 0; padding: 0; } #container { margin: 50px auto; width: 50%; height: 200px; line-height: 200px; border-radius: 5px; box-shadow: 0 0 .5em #000; text-align: center; font-size: 40px; font-weight: bold; }
basic/webpack.config.js
/** * webpack打包配置文件 */ module.exports = { // 若是你有多個入口js,須要打包在一個文件中,那麼你能夠這麼寫 // entry : ['./app1.js', './app2.js'] entry : './app.js', output : { path : './assets/', filename : '[name].bundle.js' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] } };
basic/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>basic webpack</title> </head> <body> <div id="container"></div> <script src="./assets/main.bundle.js"></script> </body> </html>
在basic
文件夾執行webpack
,打包信息以下
生成main.bundle.js
文件,chunk
名稱爲main
,也是webpack
默認生成的chunk
名
4.1.1webpack
的多入口配置
上例的簡單配置中,只有一個入口文件,那麼若是對應於一個頁面須要加載多個打包文件或者多個頁面想同時引入對應的打包文件的時候,應該怎麼作?
entry : { app1 : './app1.js', app2 : './app2.js' }
在multi-entry
文件夾執行webpack
,打包信息以下
可見生成了兩個入口文件,以及各自對應的chunk
名
4.2.1 output.publicPath
output: { path: "/home/proj/cdn/assets/[hash]", publicPath: "http://cdn.example.com/assets/[hash]/" }
引用一段官網的話:
The publicPath specifies the public URL address of the output files when referenced in a browser. For loaders that embed
<script>
or<link>
tags or reference assets like images, publicPath is used as the href or url() to the file when it’s different then their location on disk (as specified by path).
大體意思就是:publicPath
指定了你在瀏覽器中用什麼地址來引用你的靜態文件,它會包括你的圖片、腳本以及樣式加載的地址,通常用於線上發佈以及CDN部署的時候使用。
好比有下面一段配置:
var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry : './app.js', output : { path : './assets/', filename : '[name].bundle.js', publicPath : 'http://rynxiao.com/assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new HtmlWebpackPlugin({ filename: './index-release.html', template: path.resolve('index.template'), inject: 'body' }) ] };
其中我將publicPath
設置成了http://rynxiao.com/assets/
,其中設置到了插件的一些東西,這點下面會講到,總之這個插件的做用是生成了上線發佈時候的首頁文件,其中script
中引用的路徑將會被替換。以下圖:
4.2.2 output.chunkFilename
各個文件除了主模塊之外,還可能生成許多額外附加的塊,好比在模塊中採用代碼分割就會出現這樣的狀況。其中chunkFilename
中包含如下的文件生成規則:
[id] 會被對應塊的id替換.
[name] 會被對應塊的name替換(或者被id替換,若是這個塊沒有name).
[hash] 會被文件hash替換.
[chunkhash] 會被塊文件hash替換.
例如,我在output中以下設置:
output : { path : './assets/', filename : '[name].[hash].bundle.js', chunkFilename: "chunk/[chunkhash].chunk.js" }
同時我修改了一下basic/app.js
中的文件
require('./app.css'); require.ensure('./main.js', function(require) { require('./chunk.js'); }); document.getElementById("container").textContent = "APP";
其中對應的chunk.js
就會生成帶有chunkhash
的chunk
文件,以下圖:
這在作給文件打版本號的時候特別有用,當時如何進行hash
替換,下面會講到
4.2.3 output.library
這個配置做爲庫發佈的時候會用到,配置的名字即爲庫的名字,一般能夠搭配libraryTarget
進行使用。例如我給basic/webpack.config.js
加上這樣的配置:
output : { // ... library : 'testLibrary' // ... }
那麼實際上生成出來的main.bundle.js
中會默認帶上如下代碼:
var testLibrary = (//....之前的打包生成的代碼); // 這樣在直接引入這個庫的時候,就能夠直接使用`testLibrary`這個變量
4.2.4 output.libraryTarget
規定了以哪種方式輸出你的庫,好比:amd/cmd/或者直接變量,具體包括以下
"var"
- 以直接變量輸出(默認library方式) var Library = xxx (default)
"this"
- 經過設置this
的屬性輸出 this["Library"] = xxx
"commonjs"
- 經過設置exports
的屬性輸出 exports["Library"] = xxx
"commonjs2"
- 經過設置module.exports
的屬性輸出 module.exports = xxx
"amd"
- 以amd方式輸出
"umd"
- 結合commonjs2/amd/root
例如我以umd
方式輸出,如圖:
loader
中!
表明的含義
require("!style!css!less!bootstrap/less/bootstrap.less");
// => the file "bootstrap.less" in the folder "less" in the "bootstrap"
// module (that is installed from github to "node_modules") is
// transformed by the "less-loader". The result is transformed by the
// "css-loader" and then by the "style-loader".
// If configuration has some transforms bound to the file, they will not be applied.
表明加載器的流式調用,例如:
{ test : /\.css|\.less$/, loader : 'style!css!less' }
就表明了先使用less加載器來解釋less文件,而後使用css加載器來解析less解析後的文件,依次類推
loaders
中的include
與exclude
include
表示必需要包含的文件或者目錄,而exclude
的表示須要排除的目錄
好比咱們在配置中通常要排除node_modules
目錄,就能夠這樣寫
{ test : /\.js$/, loader : 'babel', exclude : nodeModuleDir }
官方建議:優先採用include,而且include最好是文件目錄
module.noParse
使用了noParse
的模塊將不會被loaders
解析,因此當咱們使用的庫若是太大,而且其中不包含require
、define
或者相似的關鍵字的時候(由於這些模塊加載並不會被解析,因此就會報錯),咱們就可使用這項配置來提高性能。
例以下面的例子:在basic/
目錄中新增no-parse.js
var cheerio = require('cheerio'); module.exports = function() { console.log(cheerio); }
webpack.config.js
中新增以下配置:
module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ], noParse : /no-parse.js/ }
當執行打包後,在瀏覽器中打開index.html
時,就會報錯require is not defined
resolve.alias
爲模塊設置別名,可以讓開發者指定一些模塊的引用路徑。對一些常常要被import或者require的庫,如react,咱們最好能夠直接指定它們的位置,這樣webpack能夠省下很多搜索硬盤的時間。
例如咱們修改basic/app.js
中的相關內容:
var moment = require("moment"); document.getElementById("container").textContent = moment().locale('zh-cn').format('LLLL');
加載一個操做時間的類庫,讓它顯示當前的時間。使用webpack --profile --colors --display-modules
執行配置文件,獲得以下結果:
其中會發現,打包總共生成了104個隱藏文件,其中一半的時間都在處理關於moment
類庫相關的事情,好比尋找moment
依賴的一些類庫等等。
在basic/webpack.config.js
加入以下配置,而後執行配置文件
resolve : { alias : { moment : 'moment/min/moment-with-locales.min.js' } }
有沒有發現打包的時間已經被大大縮短,而且也只產生了兩個隱藏文件。
配合module.noParse
使用
module.noParse
參看上面的解釋
noParse: [/moment-with-locales/]
執行打包後,效果以下:
是否是發現打包的時間進一步縮短了。
配合externals
使用
externals
參看下面的解釋
Webpack 是如此的強大,用其打包的腳本能夠運行在多種環境下,Web 環境只是其默認的一種,也是最經常使用的一種。考慮到 Web 上有不少的公用 CDN 服務,那麼 怎麼將 Webpack 和公用的 CDN 結合使用呢?方法是使用 externals 聲明一個外部依賴。
externals: { moment: true }
固然了 HTML 代碼裏須要加上一行
<script src="//apps.bdimg.com/libs/moment/2.8.3/moment-with-locales.min.js"></script>
執行打包後,效果以下:
resolve.extensions
resolve : { extensions: ["", ".webpack.js", ".web.js", ".js", ".less"] }
這項配置的做用是自動加上文件的擴展名,好比你有以下代碼:
require('style.less'); var app = require('./app.js');
那麼加上這項配置以後,你能夠寫成:
require('style'); var app = require('./app');
當咱們想在項目中require一些其餘的類庫或者API,而又不想讓這些類庫的源碼被構建到運行時文件中,這在實際開發中頗有必要。此時咱們就能夠經過配置externals參數來解決這個問題:
//webpack.config.js module.exports = { externals: { 'react': 'React' }, //... }
externals對象的key是給require時用的,好比require('react'),對象的value表示的是如何在global(即window)中訪問到該對象,這裏是window.React。
同理jquery的話就能夠這樣寫:'jquery': 'jQuery',那麼require('jquery')便可。
HTML中注意引入順序便可:
<script src="react.min.js" /> <script src="bundle.js" />
提供了一些方式來使得代碼調試更加方便,由於打包以後的代碼是合併之後的代碼,不利於排錯和定位。其中有以下幾種方式,參見官網devtool
例如,我在basic/app.js
中增長以下配置:
require('./app.css'); // 新增hello.js,顯然在文件夾中是不會存在hello.js文件的,這裏會報錯 require('./hello.js'); document.getElementById("container").textContent = "APP";
執行文件,以後運行index.html
,報錯結果以下:
給出的提示實在main.bundle.js第48行,點進去看其中的報錯以下:
從這裏你徹底看不出到底你程序的哪一個地方出錯了,而且這裏的行數還算少,當一個文件出現了上千行的時候,你定位bug
的時間將會更長。
增長devtool
文件配置,以下:
module.exports = { devtool: 'eval-source-map', // .... };
執行文件,以後運行index.html
,報錯結果以下:
這裏發現直接定位到了app.js
,而且報出了在第二行出錯,點擊去看其中的報錯以下:
發現問題定位一目瞭然。
5.1.1 Commonjs採用require.ensure
來產生chunk
塊
require.ensure(dependencies, callback); //static imports import _ from 'lodash' // dynamic imports require.ensure([], function(require) { let contacts = require('./contacts') })
這一點在output.chunkFileName
中已經作過演示,能夠去查看
5.1.2 AMD採用require
來產生chunk
塊
require(["module-a", "module-b"], function(a, b) { // ... });
5.1.3 將項目APP代碼與公共庫文件單獨打包
咱們在basic/app.js
中添加以下代碼
var $ = require('juqery'), _ = require('underscore'); //.....
而後咱們在配置文件中添加vendor
,以及運用代碼分離的插件對生成的vendor
塊從新命名
var webpack = require("webpack"); module.exports = { entry: { app: "./app.js", vendor: ["jquery", "underscore", ...], }, output: { filename: "bundle.js" }, plugins: [ new webpack.optimize.CommonsChunkPlugin(/* chunkName= */"vendor", /* filename= */"vendor.bundle.js") ] };
運行配置文件,效果以下:
5.1.4 抽取多入口文件的公共部分
咱們從新創建一個文件夾叫作common
,有以下文件:
// common/app1.js console.log("APP1");
// common/app2.js console.log("APP2");
打包以後生成的app1.bundle.js
、app2.bundle.js
中會存在許多公共代碼,咱們能夠將它提取出來。
// common/webpack.config.js /** * webpack打包配置文件 * 抽取公共部分js */ var webpack = require('webpack'); module.exports = { entry : { app1 : './app1.js', app2 : './app2.js' }, output : { path : './assets/', filename : '[name].bundle.js' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new webpack.optimize.CommonsChunkPlugin("common.js") ] };
抽取出的公共js爲common.js
,如圖
查看app1.bundle.js
,發現打包的內容基本是咱們在模塊中所寫的代碼,公共部分已經被提出到common.js
中去了
5.1.5 抽取css文件,打包成css bundle
默認狀況下以require('style.css')
狀況下導入樣式文件,會直接在index.html
的<head>
中生成<style>
標籤,屬於內聯。若是咱們想將這些css文件提取出來,能夠按照下面的配置去作。
// extract-css/app1.js require('./app1.css'); document.getElementById("container").textContent = "APP"; // extract-css/app2.js require('./app2.css'); document.getElementById("container").textContent = "APP1 APP2"; // extract-css/app1.css * { margin: 0; padding: 0; } #container { margin: 50px auto; width: 50%; height: 200px; line-height: 200px; border-radius: 5px; box-shadow: 0 0 .5em #000; text-align: center; font-size: 40px; font-weight: bold; } // extract-css/app2.css #container { background-color: #f0f0f0; } // extract-css/webpack.config.js /** * webpack打包配置文件 * 抽取公共樣式(沒有chunk) */ var webpack = require('webpack'); var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry : { app1 : './app1.js', app2 : './app1.js' }, output : { path : './assets/', filename : '[name].bundle.js' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : ExtractTextPlugin.extract("style-loader", "css-loader") } ] }, plugins : [ new ExtractTextPlugin("[name].css") ] };
獲得的效果以下圖:
若是包含chunk文件,而且chunk文件中也由於了樣式文件,那麼樣式文件會嵌入到js中
css合併到一個文件
// ... module.exports = { // ... plugins: [ new ExtractTextPlugin("style.css", { allChunks: true }) ] }
效果如圖:
若是包含chunk文件,而且chunk文件中也由於了樣式文件,樣式文件不會嵌入到js中,而是直接輸出到style.css
配合CommonsChunkPlugin一塊兒使用
// ... module.exports = { // ... plugins: [ new webpack.optimize.CommonsChunkPlugin("commons", "commons.js"), new ExtractTextPlugin("[name].css") ] }
效果圖以下:
線上發佈時爲了防止瀏覽器緩存靜態資源而改變文件版本,這裏提供兩種作法:
5.2.1 使用HtmlWebpackPlugin
插件
// version/webpack.config.js /** * webpack打包配置文件 * 文件打版本,線上發佈 */ var path = require('path'); var HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry : './app.js', output : { path : './assets/', filename : '[name].[hash].bundle.js', publicPath : 'http://rynxiao.com/assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new HtmlWebpackPlugin({ filename: './index-release.html', template: path.resolve('index.template'), inject: 'body' }) ] };
生成的效果以下:
每次打包以後都會生成文件hash,這樣就作到了版本控制
5.2.2 自定義插件給文件添加版本
// version/webpack.config.version.js /** * webpack打包配置文件 * 文件打版本,線上發佈,自定義插件方式 */ var path = require('path'); var fs = require('fs'); var cheerio = require('cheerio'); module.exports = { entry : './app.js', output : { path : './assets/', filename : '[name].[hash].bundle.js', publicPath : 'http://rynxiao.com/assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ function() { this.plugin("done", function(stats) { fs.writeFileSync( path.join(__dirname, "stats.json"), JSON.stringify(stats.toJson()) ); fs.readFile('./index.html', function(err, data) { var $ = cheerio.load(data.toString()); $('script[src*=assets]').attr('src','http://rynxiao.com/assets/main.' + stats.hash +'.bundle.js'); fs.writeFile('./index.html', $.html(), function(err) { !err && console.log('Set has success: '+ stats.hash) }) }) }); } ] };
效果如圖:
能夠達到一樣的效果,可是stats暫時只能拿到hash值,由於咱們只能考慮在hash上作版本控制,好比咱們能夠建hash目錄等等
好比有以下場景:咱們用到 Pen 這個模塊, 這個模塊對依賴一個 window.jQuery, 可我手頭的 jQuery 是 CommonJS 語法的,而 Pen 對象又是生成好了綁在全局的, 但是我又須要經過 require('pen') 獲取變量。 最終的寫法就是作 Shim 處理直接提供支持:
作法一:
{test: require.resolve('jquery'), loader: 'expose?jQuery'}, // 輸出jQuery到全局 {test: require.resolve('pen'), loader: 'exports?window.Pen'} // 將Pen做爲一個模塊引入
作法二:
new webpack.ProvidePlugin({ $: "jquery", jQuery: "jquery", "window.jQuery": "jquery" })
This plugin makes a module available as variable in every module.
The module is required only if you use the variable.
Example: Make $ and jQuery available in every module without writing require("jquery").
Loader 是支持鏈式執行的,如處理 sass 文件的 loader,能夠由 sass-loader、css-loader、style-loader 組成,由 compiler 對其由右向左執行,第一個 Loader 將會拿到需處理的原內容,上一個 Loader 處理後的結果回傳給下一個接着處理,最後的 Loader 將處理後的結果以 String 或 Buffer 的形式返回給 compiler。當然也是但願每一個 loader 只作該作的事,純粹的事,而不但願一籮筐的功能都集成到一個 Loader 中。
官網給出了兩種寫法:
// Identity loader module.exports = function(source) { return source; };
// Identity loader with SourceMap support module.exports = function(source, map) { this.callback(null, source, map); };
第一種爲基礎的寫法,採用return
返回, 是由於是同步類的 Loader 且返回的內容惟一。若是你寫loader有依賴的話,一樣的你也能夠在頭部進行引用,好比:
// Module dependencies. var fs = require("fs"); module.exports = function(source) { return source; };
而第二種則是但願多個loader
之間鏈式調用,將上一個loader
返回的結果傳遞給下一個loader
。
案例
好比我想開發一個es6-loader,專門用來作以.es6
文件名結尾的文件處理,那麼咱們能夠這麼寫
// loader/es6-loader.js // 固然若是我這裏不想將這個loader所返回的東西傳遞給下一個laoder,那麼我 // 能夠在最後直接返回return source // 這裏改變以後,我直接能夠扔給babel-loader進行處理 module.exports = function(source, map) { // 接收es6結尾文件,進行source改變 source = "console.log('I changed in loader');" // 打印傳遞進來的參數 console.log("param", this.query); // ... 咱們還能夠作一些其餘的邏輯處理 this.callback(null, source, map); }; // loader/loader1.es6 let a = 1; console.log(a); // loader/app.js // 向loader中傳遞參數 require('./es6-loader?param1=p1!./loader1.es6'); document.getElementById("container").textContent = "APP";
執行webpack打包命令,在控制檯會打印出param的值,如圖:
在執行完成以後,打開index.html
,在控制檯打印出「I changed in loader」,而不是1
進階
能夠去閱讀如下這篇文章 如何開發一個 Webpack loader
插件基本的結構
插件是能夠實例化的對象,在它的prototype上必須綁定一個apply
方法。這個方法會在插件安裝的時候被Webpack compiler
進行調用。
function HelloWorldPlugin(options) { // Setup the plugin instance with options... } HelloWorldPlugin.prototype.apply = function(compiler) { compiler.plugin('done', function() { console.log('Hello World!'); }); }; module.exports = HelloWorldPlugin;
安裝一個插件,將其添加到配置中的plugins
數組中。
var HelloWorldPlugin = require('hello-world'); var webpackConfig = { // ... config settings here ... plugins: [ new HelloWorldPlugin({options: true}) ] };
執行效果如圖:
這裏只做簡單的引入,日常通常都不須要本身寫插件,若是想進一步瞭解,能夠去看官網例子
// 1.全局安裝webpack-dev-server cnpm install -g webpack-dev-server // 2. 設置一個文件啓動目錄,運行 webpack-dev-server --content-base basic/ // 3. 在瀏覽器輸入localhost:8080
// auto-refresh/app.js document.getElementById("container").textContent = "APP APP HOT "; console.log("OK"); // auto-refresh/server.js var webpack = require('webpack'); var config = require('./webpack.config.js'); var WebpackDevServer = require("webpack-dev-server"); var compiler = webpack(config); new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: true, noInfo: false, historyApiFallback: true }).listen(8080, 'localhost', function (err, result) { if (err) { console.log(err); } console.log('Listening at localhost:3000'); }); // auto-refresh/webpack.config.js /** * webpack打包配置文件 */ var webpack = require('webpack'); module.exports = { entry : [ 'webpack-dev-server/client?http://127.0.0.1:8080', // WebpackDevServer host and port 'webpack/hot/only-dev-server', './app.js' ], output : { path : './assets/', filename : '[name].bundle.js', publicPath : './assets/' }, module : { loaders : [ { test : /\.js$/, loader : 'react-hot!babel' }, { test : /\.css$/, loader : 'style!css' } ] }, plugins : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin(), new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"development"' }), ] }; // auto-refresh/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>basic webpack</title> </head> <body> <div id="container"></div> <script src="./assets/main.bundle.js"></script> </body> </html> // 運行 node server.js // 瀏覽器輸入:localhost:8080
// 1. 安裝babel-core、babel-preset-es2015以及babel-loader // 2. 項目根目錄下配置.babelrc文件 { "presets": ["es2015"] } // 3. 將webpack.config.js從新命名爲webpack.config.babel.js // 4.運行webpack --config webpack.config.babel.js // 說明node 版本5.0以上,babel-core版本6以上須要如此配置
這是一個 Webpack 支持,但文檔裏徹底沒有提到的特性 (應該立刻就會加上)。只要你把配置文件命名成 webpack.config.[loader].js ,Webpack 就會用相應的 loader 去轉換一遍配置文件。因此要使用這個方法,你須要安裝 babel-loader 和 babel-core 兩個包。記住你不須要完整的 babel 包。
其餘辦法(未成功)
1.在上述的方案中,其實不須要從新命名就能夠直接運行webpack,可是今天試了一直不成功 2.{ test : /\.js|jsx$/, loader : 'babel', query: { //添加兩個presents 使用這兩種presets處理js或者jsx文件 presets: ['es2015', 'react'] } }