webpack是目前一個很熱門的前端打包工具,官網說得很清楚,webpack的出現就是要把requirejs幹掉。同時它還提供了十分便利的本地開發的環境。網上並不容易找到一個講解得比較詳細完整的教程,本文結合實踐經驗,總結一套可用的開發和上線的配置和流程。javascript
首先,Require JS有什麼問題php
博主先是使用了RequireJs,後來又轉了webpack,綜合比較,requirejs確實存在一些缺點:css
須要把全部的依賴模塊寫在require函數裏面,當模塊不少的時候,看起來逼格就不高了,感覺以下: html
而webpack既兼容requirejs的寫法,也兼容commonjs的寫法,也就是說,使用webpack你既能夠繼續像上面那樣寫,也能夠像node那樣寫,感覺以下:前端
var modules = { signHandler: require("module/sign-log"), chatHandler: require("module/chat-win"), mapHandler: require("lib/map"), util: require("lib/util") };
能夠在須要的時候再去require,而不是搞個大括號把所有的模塊一會兒寫到一塊兒。(模塊的導出用module.exports = ....)java
固然這兩種寫法不只是感光上的區別,邏輯上也有區別。用中括號加載的模塊一般webpack是動態去加載,而沒有中括號是和主文件打包在一塊兒的。node
例若有一個彈框模塊,用在登錄註冊,而且全部頁面都有登錄註冊,因此這是一個全部頁面的通用模塊。若是頁面的其它模塊都沒調到通用模塊裏面的東西的話,用RequireJs沒什麼問題。可是實際狀況上不是這樣的,例如util模塊既會被登錄註冊的模塊調用,也會被不少其它模塊調用。這個時候合併壓縮就有問題了:合併後的通用模塊如common-app.js會帶上util的代碼、另一個頁面的例如detail.js也會帶上util的代碼,之後一改util.js裏面的東西,就會一併改動其它全部用到util的頁面js,就得從新打全部js的版本號。這樣不管對佈署上線,仍是對於用戶的緩存來講都是不利的。
react
webpack能夠把幾個文件的通用模塊抽出來單獨做爲一個模塊common-chunk.js,引用的時候每一個頁面先引一個common-chunk.js,再引一個該頁面本身的js文件如detail.js,原detail.js裏面和其它js文件共用的模塊已經被提取到common-chunk.js裏面。webpack
這個問題是這樣的,假設個人聊天模塊文件有500Kb這麼大,並不但願一刷頁面就加載,而是用戶點了聊天再去加載。這個聊天模塊有一個入口文件和其它幾個模塊文件,我合併壓縮了入口文件,須要有一個輸出文件,而入口文件define的模塊名和壓縮優化後的輸出文件的路徑確定是要不同的,可是壓縮以後他並不會自動去改變輸出文件的模塊名。這樣就致使你要手動去改一下壓縮文件的模塊名,否則會require不到。我以前找了一下,沒有找到解決方案,因此採起了一個壓縮兩次的比較笨拙的方法。nginx
而webpack有一個文件束chunkFile的概念,它會自動去把須要異步加載的文件變成一個chunkFile,而後觸發加載的時候再去加載chunkFile。
webpack自己有一些插件和第三方的插件,能夠在本地開一個webpack-dev-server,文件一保存的時候就會自動打包編譯js/css/less/sass等。
使用RequireJs雖然看起來缺點比較多,可是使用RequireJs也有webpack不具有的優勢,那就是RequireJs開發的時候在瀏覽器裏面,每一個模塊都是單獨一個文件,跟本地文件保持一致,而webpack是把主文件和該文件都用到的模塊都打包成了一個文件,這樣在調試的時候就須要你去搜索找到要調試的位置,而使用requireJs直接根據第幾行就能夠了。不過,考慮到使用webpack能夠搭建一個很方便的本地開發環境,因此這個缺點也不是很明顯。
用一句歸納就是:寫一個配置文件,而後執行下webpack,就能夠把生成的文件輸出,可壓縮帶版本號,同時生成一個source-map文件,這個文件包含了每一個模塊的js和css的實際(帶版本號)路徑,根據這個路徑就能夠把html裏面的js/css等換成真實的路徑。
webpack是一個打包的工具,它有一個重要的概念,就是把js/css/image/coffee都當成地位相等的資源,你能夠在js裏面require一個css,也能夠require一個image。可是這種模式比較適用於React等框架,都是用js控制。
webpack的其它幾個重要概念:
上面說到,各類各樣的資源均可以在webpack裏面加載,而這些資源都須要相應的加載器,webpack才能識別,而後解析成正常的瀏覽器認識的資源。
換句話說,你能夠給webpack加載各類各樣的資源:css/less/sass/png/babel等,而後在代碼裏面進行管理。
例如要加一個sass的loader,須要先安裝:
npm install sass-loader node-sass
而後在配置文件添加一個loader:
{
test: /\.sass$/,
loaders: ["style", "css", "sass"]
},
這樣當你require(「hello.sass」)的時候,webpack就能處理這種.sass結尾的文件。這樣子有兩個好處,一個是webpack可以自動編譯sass爲css,另外一個是require進來的style,webpack會把它解析成一個object,這個object的key就是類名,就能夠在js使用樣式的類名,這種比較適合相似於react的開發模式。
上面提到的,會把動態加載的文件生成一個個的chunk,在配置文件的output裏面加一行:
chunkFilename: "bundle-[id].js"
就會根據id區分不一樣動態加載的chunk文件,而這些chunk文件名對於咱們來講是可有可無,由於這個是webpack管理的,開發者無需關心叫什麼又是怎麼加載的。
這是webpack的一個插件,能夠在本地開一個靜態服務,用來做爲本地開發的重要工具。具體步驟就是html裏面引用的資源用一個假的域名,如develop.com:
<script src="//develop.com/site/app-init.js"></script>
而後再把develop.com綁到本地迴路:
127.0.0.1 fedren.com
這樣請求就打到了本地的80端口。同時在本地開一個nginx監聽在80端口,nginx收到80端口的請求後,再把請求轉發到webpack的服務(默認是8080端口)。這樣就可以實現本地開發,下文會具體介紹。
下面一步步介紹怎麼配置和使用webpack
首先,npm init建立一個node的配置文件package.json,而後安裝webpack:
npm install webpack sudo npm install webpack -g //安裝一個全局的命令
再建立一個webpack.config.js文件,加入最基本的配置:
module.exports = { // The standard entry point and output config //每一個頁面的js文件 entry: { home: "js/home", detail: "js/detail" }, output: { path: "assets", //打包輸出目錄 publicPath: "/static/build/", //webpack-dev-server訪問的路徑 filename: "[name].js", //輸出文件名 chunkFilename: "bundle-[id].js" //輸出chunk文件名 } };
工程的js都放到js目錄下,一個叫home.js,另外一個叫detail.js,輸出到assets目錄,publicPath是爲webpack-dev-server所使用
而後在當前目錄執行webpack,發現webpack報錯了:
ERROR in Entry module not found: Error: Cannot resolve module 'js/home' in /Users/yincheng/code/blog-webpack
找不到js/home的模塊,只要在配置裏面加一句resolve:
resolve: { modulesDirectories: ['.'] }
告訴webpack全部模塊的啓始目錄由當前目錄開始,再執行下webpack就能夠正常輸出了:
到目前爲此,當前工程的目錄結構就是這樣的了:
接下來,建立html:home.html,裏面引入js文件,"static/build"即爲上面定義的publicPath:
<body> <p>home.html</p> <script src="//develop.com/static/build/home.js"></script> </body>
注意咱們用了一個develop.com的域名,把這個域名綁到本地迴路:
127.0.0.1 develop.com
而後配置nginx,打開nginx.conf,加多一個server:
server { listen 80; server_name payment-admin.com; charset utf-8; #工程路徑 root /Users/yincheng/code/demo; autoindex on; autoindex_exact_size on; location ~* /.+\.[a-z]+$ { proxy_set_header x-request-filename $request_filename; # webpack的服務 proxy_pass http://127.0.0.1:8080; } }
啓動nginx或者重啓下nginx
而後再裝一個webpack-dev-server:
npm install webpack-dev-server --save-dev sudo npm install webpack-dev-server -g
而後啓動webpack-dev-server,執行:
webpack-dev-sever --port=8080 //不加port參數,默認就爲8080端口
而後就能夠訪問:http://develop.com/html/home.html
這個時候,只要一改變home.js的內容,webpack-dev-server就會自動打包新的文件 ,一刷新頁面,就是最新的修改了。這樣就實現了最基本的本地開發,無論你用的jsp/php,都不須要把js/css往服務器上傳。 注意webpack-dev-server是在內存生成的文件,你在本地是找不到static/build目錄的,只有執行了webpack打包纔會輸出文件到assets目錄。一個爲上面配置裏的publicPath,另外一個爲path。
引入樣式文件——首先建立css/home.css:
body{ color: #f00; }
而後在js裏面引入這個css文件:
require("css/home.css");
一保存以後,會發現webpack-dev-server報錯了:
ERROR in ./css/home.css Module parse failed: /Users/yincheng/code/blog-webpack/css/home.css Unexpected token (1:4) You may need an appropriate loader to handle this file type.
根據提示,咱們須要加裝一個css loader,讓webpack可以處理css文件,更改webpack.config.js,加入一個loader:
module.exports = { entry: ..., output: ..., resolve: ..., module: { loaders: [ { test: /\.css$/, loader: "style-loader!css-loader" }, ] } };
固然要先安裝一下:npm install style-loader css-loader --save-dev,而後再重啓下webpack-dev-server,就能夠加載樣式了,咱們發現webpack是把樣式動態插到了head標籤的style裏面,可是通常並不但願直接寫到head裏面,而是獨立的一個css文件,這個時候藉助一個分離css的插件就能夠了:
npm install extract-text-webpack-plugin --save-dev
同時把配置文件的loader改一下:
var ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { module: { loaders: [ // Extract css files { test: /\.css$/, loader: ExtractTextPlugin.extract("style-loader", "css-loader") }, ] }, plugins: [ new ExtractTextPlugin("[name].css") ] };
就會生成和js相同路徑和名字的css文件,在home.html裏面引入css文件:
<link rel="stylesheet" href="//develop.com/static/build/home.css"></link>
你也能夠加載各類各樣的loader,如加載一個sass/less loader,require一個sass/less文件後就能夠寫sass/less了,webpack會把它編譯成和上面同樣普通的css文件,讀者能夠本身試試,還能夠再裝一個png/jpg的loader,指定一個小於多少個k的圖片的參數,webpack就會把小於指定尺寸的圖片轉成base64的格式。各類loader的安裝查一查就有了。
到這裏一個最基本的本地開發環境就已經搭起來了。接下來討論自動刷新
上面一保存js/css的時候,webpack server就會自動打包,刷新頁面的時候就是最新的修改。這個刷新只要使用webpack的hot模式就能夠自動實現,即一保存就自動打包刷新。將上面運行webpack-dev-server的命令再加多兩個參數,按照官方文檔的方式:
webpack-dev-server --port=8383 --hot --inline
若是沒有意外,在你的電腦上將會報錯:
ERROR in multi home Module not found: Error: Cannot resolve module 'webpack/hot/dev-server' in /Users/yincheng/code/blog-webpack @ multi home
這個問題困惑了筆者很久,由於在node_modules裏面是有這個"webpack/hot/dev-server"的,其實只要認真看下上面的提示,就會發現它並非說在node_modules裏面,而是在當前工程目錄裏,因此把node_modules裏的webpack文件夾拷一份到外面就能夠正常運行了。(若是你又配了個context的參數的話,那就根據提示拷到context指定的目錄)
使用hot模式,只要一保存js/css就能夠自動刷新了,這個功能確實很方便。若是不寫參數,也能夠把它寫在配置文件裏面:
var hotModuleReplacementPlugin = require("webpack/lib/HotModuleReplacementPlugin"); module.exports = { plugins: [ new ExtractTextPlugin("[name].css"), new hotModuleReplacementPlugin() ], devServer: { historyApiFallback: true, hot: true, inline: true, progress: true } };
而後運行server就不用帶上後面那兩個參數了。
如上文提到,webpack能夠將幾個js的公共模塊提取成一個chunk,須要藉助一個commonChunkPlugin,在上面的plugins再添加一個:
var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin"); plugins: [ new CommonsChunkPlugin({ //minChunks: 3, name: "common-app.chunk", chunks: ["home", "detail", "list"] }) ]
這樣就能夠把home、detail、list三個js和css用到的公共模塊提取到common-app.chunk.js和common-app.chunk.css這兩個文件了。注意頁面要先引入這兩個文件,而後再引入具體頁面的js,webpack在common chunk裏面定義了它的require函數。如上面的home.html:
<script src="//develop.com/static/build/common-app.chunk.js"></script> <script src="//develop.com/static/build/home.js"></script>
能夠指定一個minChunk的參數,指定模塊至少被require幾回才能提取出來,默認是3
還能夠定義兩個commonChunk,例如在詳情頁、列表頁和首頁都有搜索的模塊,而其它頁面沒有搜索的模塊,也就是說除了全部頁面都有的公共模塊如登錄註冊外,還有一個搜索的公共模塊有三個頁面要用到。若是都用一個common chunk,會把搜索的也放進來,但其它不少頁面並不須要用到。這個時候須要加多一個common chunk:
plugins: [ new CommonsChunkPlugin({ name: "search-app.chunk", chunks: ["search-app-init", "home", "detail", "list"] }), new CommonsChunkPlugin({ name: "common-app.chunk", chunks: ["home", "detail", "search-map", "search-app.chunk", "sell", "about", "blog"] }) ]
注意要把search-app.chunk也寫到下面那個全部頁面的chunk裏面,不然webpack會定義兩個同樣的require函數,頁面的模塊也會跟着混亂,一刷頁面就報錯。頁面引用js的順序就變成了:
<script src="//develop.com/static/build/common-app.chunk.js"></script> <script src="//develop.com/static/build/search-app.chunk.js"></script> <script src="//develop.com/static/build/home.js"></script>
壓縮只須要要在plugins裏面再添加一個用來壓縮的插件:
var webpack = require("webpack"); plugins: [ new webpack.optimize.UglifyJsPlugin() ]
這樣執行webpack輸出的js/css就是壓縮的
版本號就是在輸出帶上hash的替換符,以下:
module.exports = { output: { path: "assets", publicPath: "/static/build/", filename: "[name]-[chunkhash].js", chunkFilename: "bundle-[chunkhash].js" }, plugins: [ new ExtractTextPlugin("[name]-[contenthash].css") ], }
其中js用的是webpack的chunkhash,而css用的是contenthash,contenthash是根據內容生成的hash。若是不用contenthash,那麼一改js,css的版本號也會跟着改變,這個就有問題了。webpack還有另一個自帶的叫作"[hash]",這個hash是全部文件都用的同一個哈希,也就是說某個文件改了,全部文件的版本號都會跟着改,因此通常不用這個。
運行webpack,若是報了下面這個錯誤:
ERROR in chunk detail [entry] [name]-[chunkhash].js Cannot use [chunkhash] for chunk in '[name]-[chunkhash].js' (use [hash] instead)
那你就把plugins裏面的熱替換插件註釋掉就行了,上線的config不須要熱替換:
plugins: [ //new hotModuleReplacementPlugin(), ],
成功執行後,就會在設定的output目錄下面輸出加上版本號的文件:
. ├── detail-d19e4614a1c4f3c1581b.js ├── home-11198f8526424e8c58ce10a2799793e3.css └── home-5ec13a52eea2a6faf96a.js
有了版本號以後,下一步是要把html裏面的js/css換成帶版本號的路徑
以前在html裏的路徑是test.com,如今要把它換成cdn且帶版本號的路徑,也就是說,目標是要把下面的引入:
<script src="//develop.com/static/build/home.js"></script>
替換成下面的引入,並把新生成的html輸出到built目錄
<script src="//cdn.mycdn.com/test/home-5ec13a52eea2a6faf96a.js"></script>
目測沒有現成符合格式的插件能夠用,能夠自已用node寫一個,不費事。
首先要知道全部文件的對應的版本號,能夠用AssetsPlugin,生成source-map:
var AssetsPlugin = require('assets-webpack-plugin'); output: { publicPath: "//cdn.mycdn.com/static/build/" }, plugins: [ new AssetsPlugin({filename: './source-map.json', prettyPrint: true}), ]
執行webpack以後,就會生成source-map.json,打開這個文件:
{ "detail": { "js": "//cdn.mycdn.com/static/build/detail-c8a2c82ebe2e48e06564.js" }, "home": { "js": "//cdn.mycdn.com/static/build/home-380af86bfeb6fcb477a4.js", "css": "//cdn.mycdn.com/static/build/home-11198f8526424e8c58ce10a2799793e3.css" } }
根據develop.com開頭的以及最後面的home.js/home.css,就能夠在上面找到對應的路徑名。筆者寫了個腳本,能夠實現這個功能,詳見:version-control-replace-html
到這裏,整個流程就基本完成了。還有一些優化的步驟
webpack對於每一個模塊都是用id標誌,而不是用模塊的名字,只是爲了節省空間。還能夠再節省,就是用它自帶的occurrence-order插件將最經常使用的模塊靠前,這樣能夠再節省一點點空間,由於id是從0開始排的,從一位數到n位數。
new webpack.optimize.OccurenceOrderPlugin()
在上面用了common-chunk的插件,抽離公共模塊,在這個common-chunk.js裏,webpack會定義每一個模塊加載的src,以便於加載那些須要動態加載的chunk,以下:
script.src = __webpack_require__.p + "" + chunkId + "-" + {"0":"0cb48ff1ab1d1156015d","5":"e9e7f761f306c648ccef","6":"cbbdf8e3ad1aba34ced0"}[chunkId] + ".js";
從上面能夠看出它會把版本號也寫在裏面,這樣就致使一個問題,每改一個js文件,它的版本號就會變化,就會致使common chunk裏面的內容發生變化,因此它的版本號也得跟着變,也就是說改了一個文件,影響了兩個文件。因此須要把它抽出來,有個插件已經作了這樣的事情,叫作ChunkManifestPlugin:
var ChunkManifestPlugin = require('chunk-manifest-webpack-plugin'); plugins: [ new ChunkManifestPlugin({ filename: "chunk-manifest.json", manifestVariable: "webpackManifest" }) ]
傳兩個參數,一個是輸出文件名,另外一個是變量名,用於上面的script.src,執行webpack後,它會把上面script.src的那一坨東西放到chunk-manifest.json,而後在頁面寫一個內聯的script,定義一個全局變量window.webpackManifest,值爲manifest.json裏面的內容。筆者已在上面的替換版本號的腳本作處理,只需在頁面合適的地方寫上一行:
<!--%webpack manifest%-->
就會把這行替換成一個script標籤。
在上面寫了兩個common chunk,在生成的兩個chunk文件裏面,你會發現大量的的重複代碼,已經失去了公共模塊的做用,這個問題能夠用一個MoveToParentMergingPlugin解決,它會把search-app用到的common-app的模塊所有移到了common-app,search-app就不會重複common-app的內容了。
上面提到,只要一保存css/js,webpack-dev-server就會自動保存和刷新,可是html/jsp沒辦法(若是你用react開發,能夠用react-hot-loader),其實能夠手動解決這個問題。打開node_modules/webpack-dev-server/client/index.js這個文件,能夠發現webpack是用的sockjs實現自動刷新的。瀏覽器使用sockjs建立socket客戶端,鏈接到webpack的服務,保存更改的時候,服務就向瀏覽器的socket發送消息,接收到這個消息後客戶端就調window.location.reload刷新頁面。因此能夠模仿這個過程,在本地另開一個服務,監聽html的修改,而後向瀏覽器端發送刷新頁面的消息。
具體來講,首先在上面的node_modules/webpack-dev-server/client/index.js這個文件最後面再添加一個socket鏈接:
/*自定義reload window*/ var reload = new SockJS("http://localhost:9999/reload"); reload.onopen = function(){ console.log("customer reload start......."); } reload.onclose = function(){ console.log("customer reload close......."); } reload.onmessage = function(_msg){ var msg = JSON.parse(_msg.data); if(msg.type === "reload"){ console.log("customer reload window now"); window.location.reload(); } }
這個9999端口的server就是下面要在本地監聽的一個socket服務。在開這個socket服務以前,須要先在本地開一個監聽文件修改的服務,而後再向這個socket服務發送消息。監聽的服務比較好寫,有現成的node包能夠用:chokidar,使用也很是簡單。監聽到修改以後就能夠執行上傳服務器的命令,而後(使用進程間的通訊)再向socket服務發送一個須要刷新的消息,再傳遞給瀏覽器的scoket,如上面的代碼,一收到消息就刷新頁面。具體代碼查看github
除了優化,在使用中會遇到的一些問題:
有時候會引入外部的庫,這些庫可能會用umd的require模式,判斷是要用requirejs仍是commonjs或是寫個全局的函數:
/* CommonJS */ if (typeof require === 'function' && typeof module === 'object' && module && typeof exports === 'object' && exports) module['exports'] = init(require("ByteBuffer")); /* AMD */ else if (typeof define === 'function' && define["amd"]) define("lib/chat/ProtoBuf", ["./ByteBuffer"], init); /* Global */ else(global["dcodeIO"] = global["dcodeIO"] || {})["ProtoBuf"] = init(global["dcodeIO"]["ByteBuffer"]);
這個的問題就在於,只要頁面上有require出現,webpack就會去打包,無論你是寫if裏面還click事件裏面。由於像上面說的,webpack會把異步加載的文件打包成一個boundle文件,同時也會把非異步的打包到一塊兒。像上面那樣寫,它會重複打包,生成好多個bundle。只要加多一個umdREquirePlugin,webpack就能正常打包了。
webpack是一個打包的工具,它並非像requireJs那樣能夠支持直接require一個外部資源。
例如我要require谷歌地圖:https://maps.googleapis.com/maps/api/js,打包的時候webpack會給出一個warning,說加載不到這個外部資源,運行代碼的時候會報錯,提示沒有這個模塊。
另一個問題是,我須要if else判斷,若是是中國的環境就加載中國域名的谷歌地圖:http://ditu.google.cn/maps/api/js 不然就加載上面的,使用webpack是沒辦法作到的, 使用requireJs就能夠很簡單地直接require一下就行。
但其實這個問題很好解決只要本身寫一個動態加載script的函數就行了,一個兼容性很好的版本:
function loadScript(url, callback){ var script = document.createElement("script") script.type = "text/javascript"; if (script.readyState){ //IE script.onreadystatechange = function(){ if (script.readyState == "loaded" || script.readyState == "complete"){ script.onreadystatechange = null; callback(); } }; } else { //Others script.onload = function(){ callback(); }; } script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
詳見:The best way to load external JavaScript
webpack雖然是一個利器,可是坑也很多,目前遇到過的不太好解決的問題:
使用chunkhash有兩個問題,一個是css改變以後,js的版本號也會跟着改變,即便js沒有修改,可是比較這兩個js文件的時候,你會發現這兩個版本號不同的文件內容是徹底如出一轍的。由於chunkhash不是根據文件內容算的hash值。第二個問題是,相同的代碼在不一樣人的機器上打的包的版本號不同。若是使用一些根據文件內容打版本號的插件,如webpack-md5-hash,這個插件是用文件內容做一個md5的計算得出一個版本號,這樣能夠解決上面的兩個問題,可是又引起了新的問題,這個md5的時不時就會出現打的版本號不惟一的狀況,文件內容不一樣、版本號相同,並且這個機率還不小。因此最後仍是放棄了使用這個插件,而後又嘗試了另一個使用sha算法計算,可是這個改了一個文件會使幾個文件的版本號也發生變化。如今仍是使用chunkhash
上文提到,webpack的模塊是用id標誌的,每一個模塊對就一個id,例如util對應2,可是這個id不是固定不變的,在n次修改和打包以後,util的id可能會變成了3,這個就比較坑了,給增量上線形成了阻力,即單獨上一個html有風險。由於在common-chunk裏面,util的id是上次打包的時候定的,可是你此次打包util的id變了,而你只想上home.html,在home.html裏面引的home.js裏面使用到的util的id對不上common-chunk裏面的,致使不能在home裏面正常地加載util這個模塊。一個臨時的解決辦法是,home.js不要使用common-chunk,全部的模塊都打包到home.js裏面就不會有這個問題。
綜上對於webpack的介紹基本說完了,後續會繼續研究webpack的打包方式和怎麼樣寫一個webpack的插件。若是上面有什麼不合理或能夠優化的地方還請指出。