webpack 2 打包實戰

寫在開頭

先說說為什麼要寫這篇文章, 最初的原因是組裡的小朋友們看了webpack文檔後, 表情都是這樣的: (摘自webpack一篇文檔的評論區)

和這樣的:

是的, 即使是外國佬也在吐槽這文檔不是人能看的. 回想起當年自己啃webpack文檔的血與淚的往事, 覺得有必要整一個教程, 可以讓大家看完後愉悅地搭建起一個webpack打包方案的項目。

可能會有人問webpack到底有什麼用, 你不能上來就糊我一臉代碼讓我馬上搞, 我照著搞了一遍結果根本沒什麼naizi用, 都是騙人的. 所以, 在說webpack之前, 我想先談一下前端打包方案這幾年的演進歷程, 在什麼場景下, 我們遇到了什麼問題, 催生出了應對這些問題的工具. 了解了需求和目的之後, 你就知道什麼時候webpack可以幫到你. 我希望我用完之後很爽, 你們用完之後也是。

先說說前端打包方案的黑暗歷史

在很長的一段前端歷史裡, 是不存在打包這個說法的. 那個時候頁面基本是純靜態的或者服務端輸出的, 沒有AJAX, 也沒有jQuery. 那個時候的JavaScript就像個玩具, 用處大概就是在側欄弄個時鐘, 用media player放個mp3之類的腳本, 代碼量不是很多, 直接放在<script>標籤里或者弄個js文件引一下就行, 日子過得很輕鬆愉快.

隨後的幾年, 人們開始嘗試在一個頁面里做更多的事情. 容器的顯示, 隱藏, 切換. 用css寫的彈層, 圖片輪播等等. 但如果一個頁面內不能向伺服器請求數據, 能做的事情畢竟有限的, 代碼的量也能維持在頁面交互邏輯範圍內. 這時候很多人開始突破一個頁面能做的事情的範圍, 使用隱藏的iframe和flash等作為和伺服器通信的橋樑, 新世界的大門慢慢地被打開, 在一個頁面內和伺服器進行數據交互, 意味著以前需要跳轉多個頁面的事情現在可以用一個頁面搞定. 但由於iframe和flash技術過於tricky和複雜, 並沒能得到廣泛的推廣.

直到Google推出Gmail的時候(2004年), 人們意識到了一個被忽略的介面, XMLHttpRequest,也就是我們俗稱的AJAX, 這是一個使用方便的, 兼容性良好的伺服器通信介面. 從此開始, 我們的頁面開始玩出各種花來了, 前端一下子出現了各種各樣的庫, Prototype JavaScript framework , Dojo Toolkit,MooTools,Sencha Ext JS,jQuery... 我們開始往頁面里插入各種庫和插件, 我們的js文件也就爆炸了...

隨著js能做的事情越來越多, 引用越來越多, 文件越來越大, 加上當時大約只有2Mbps左右的網速, 下載速度還不如3G網路, 對js文件的壓縮和合併的需求越來越強烈, 當然這裡面也有把代碼混淆了不容易被盜用等其他因素在裡面. JSMin,YUI Compressor,Closure Compiler, UglifyJS 等js文件壓縮合併工具陸陸續續誕生了. 壓縮工具是有了, 但我們得要執行它, 最簡單的辦法呢, 就是windows上搞個bat腳本, mac/linux上搞個bash腳本, 哪幾個文件要合併在一塊的, 哪幾個要壓縮的, 發布的時候運行一下腳本, 生成壓縮後的文件.

基於合併壓縮技術, 項目越做越大, 問題也越來越多, 大概就是以下這些問題:

  • 庫和插件為了要給他人調用, 肯定要找個地方註冊, 一般就是在window下申明一個全局的函數或對象. 難保哪天用的兩個庫在全局用同樣的名字, 那就衝突了.
  • 庫和插件如果還依賴其他的庫和插件, 就要告知使用人, 需要先引哪些依賴庫, 那些依賴庫也有自己的依賴庫的話, 就要先引依賴庫的依賴庫, 以此類推...

恰好就在這個時候(2009年), 隨著後端JavaScript技術的發展, 人們提出了CommonJS的模塊化規範, 大概的語法是: 如果a.js依賴b.js和c.js, 那麼就在a.js的頭部, 引入這些依賴文件:

var b = require(./b)var c = require(./c)

那麼變數b和c會是什麼呢? 那就是b.js和c.js導出的東西, 比如b.js可以這樣導出:

exports.square = function(num) { return num * num}

然後就可以在a.js使用這個square方法:

var n = b.square(2)

如果c.js依賴d.js, 導出的是一個Number, 那麼可以這樣寫:

var d = require(./d)module.exports = d.PI // 假設d.PI的值是3.14159

那麼a.js中的變數c就是數字3.14159, 具體的語法規範可以查看Node.js的文檔.

但是CommonJS在瀏覽器內並不適用. 因為require()的返回是同步的, 意味著有多個依賴的話需要一個一個依次下載, 堵塞了js腳本的執行. 所以人們就在CommonJS的基礎上定義了Asynchronous Module Definition (AMD)規範(2011年), 使用了非同步回調的語法來並行下載多個依賴項, 比如作為入口的a.js可以這樣寫:

require([./b, ./c], function(b, c) { var n = b.square(2) console.log(c) // 3.14159})

相應的導出語法也是非同步回調方式, 比如c.js依賴d.js, 就寫成這樣:

define([./d], function(d) { return d.PI})

可以看到, 定義一個模塊是使用define()函數, define()和require()的區別是, define()必須要在回調函數中返回一個值作為導出的東西, require()不需要導出東西, 因此回調函數中不需要返回值, 也無法作為被依賴項被其他文件導入, 因此一般用於入口文件, 比如頁面中這樣載入a.js:

<script src="js/require.js" data-main="js/a"></script>

以上是AMD規範的基本用法, 更詳細的就不多說了(反正也淘汰了~), 有興趣的可以看這裡.

js模塊化問題基本解決了, css和html也沒閑著. 什麼Less,sass,stylus的css預處理器橫空出世, 說能幫我們簡化css的寫法, 自動給你加vendor prefix. html在這期間也出現了一堆模板語言, 什麼handlebars,ejs,jade, 可以把ajax拿到的數據插入到模板中, 然後用innerHTML顯示到頁面上.

托AMD和CSS預處理和模板語言的福, 我們的編譯腳本也洋洋洒洒寫了百來行. 命令行腳本有個不好的地方, 就是windows和mac/linux是不通用的, 如果有跨平台需求的話, windows要裝個可以執行bash腳本的命令行工具, 比如msys(目前最新的是msys2), 或者使用php或python等其他語言的腳本來編寫, 對於非全棧型的前端程序員來說, 寫bash/php/python還是很生澀的. 因此我們需要一個簡單的打包工具, 可以利用各種編譯工具, 編譯/壓縮js, css, html, 圖片等資源. 然後Grunt產生了(2012年), 配置文件格式是我們最愛的js, 寫法也很簡單, 社區有非常多的插件支持各種編譯, lint, 測試工具. 一年多後另一個打包工具gulp誕生了, 擴展性更強, 採用流式處理效率更高.

依託AMD模塊化編程, SPA(Single-page application)的實現方式更為簡單清晰, 一個網頁不再是傳統的類似word文檔的頁面, 而是一個完整的應用程序. SPA應用有一個總的入口頁面, 我們通常把它命名為index.html, app.html, main.html, 這個html的<body>一般是空的, 或者只有總的布局(layout), 比如下圖:

布局會把header, nav, footer的內容填上, 但main區域是個空的容器. 這個作為入口的html最主要的工作是載入啟動SPA的js文件, 然後由js驅動, 根據當前瀏覽器地址進行路由分發, 載入對應的AMD模塊, 然後該AMD模塊執行, 渲染對應的html到頁面指定的容器內(比如圖中的main). 在點擊鏈接等交互時, 頁面不會跳轉, 而是由js路由載入對應的AMD模塊, 然後該AMD模塊渲染對應的html到容器內.

雖然AMD模塊讓SPA更容易地實現, 但小問題還是很多的:

  • 不是所有的第三方庫都是AMD規範的, 這時候要配置shim, 很麻煩.

  • 雖然RequireJS支持插件的形式通過把html作為依賴載入, 但html裡面的<img>的路徑是個問題, 需要使用絕對路徑並且保持打包後的圖片路徑和打包前的路徑不變, 或者使用html模板語言把src寫成變數, 在運行時生成.

  • 不支持動態載入css, 變通的方法是把所有的css文件合併壓縮成一個文件, 在入口的html頁面一次性載入.

  • SPA項目越做越大, 一個應用打包後的js文件到了幾MB的大小. 雖然r.js支持分模塊打包, 但配置很麻煩, 因為模塊之間會互相依賴, 在配置的時候需要exclude那些通用的依賴項, 而依賴項要在文件里一個個檢查.

  • 所有的第三方庫都要自己一個個的下載, 解壓, 放到某個目錄下, 更別提更新有多麻煩了. 雖然可以用npm包管理工具, 但npm的包都是CommonJS規範的, 給後端Node.js用的, 只有部分支持AMD規範, 而且在npm3.0之前, 這些包有依賴項的話也是不能用的. 後來有個Bower包管理工具是專門的web前端倉庫, 這裡的包一般都支持AMD規範.

  • AMD規範定義和引用模塊的語法太麻煩, 上面介紹的AMD語法僅是最簡單通用的語法, API文檔裡面還有很多變異的寫法, 特別是當發生循環引用的時候(a依賴b, b依賴a), 需要使用其他的語法解決這個問題. 而且npm上很多前後端通用的庫都是CommonJS的語法. 後來很多人又開始嘗試使用ES6模塊規範, 如何引用ES6模塊又是一個大問題.

  • 項目的文件結構不合理, 因為grunt/gulp是按照文件格式批量處理的, 所以一般會把js, html, css, 圖片分別放在不同的目錄下, 所以同一個模塊的文件會散落在不同的目錄下, 開發的時候找文件是個麻煩的事情. code review時想知道一個文件是哪個模塊的也很麻煩, 解決辦法比如又要在imgs目錄下建立按模塊命名的文件夾, 裡面再放圖片.

到了這裡, 我們的主角webpack登場了(2012年)(此處應有掌聲).

和webpack差不多同期登場的還有Browserify. 這裡簡單介紹一下Browserify, Browserify的目的是讓前端也能用CommonJS的語法require(module)來載入js. 它會從入口js文件開始, 把所有的require()調用的文件打包合併到一個文件, 這樣就解決了非同步載入的問題. 那麼Browserify有什麼不足之處導致我不推薦使用它呢? 主要原因有下面幾點:

  • 最主要的一點, Browserify不支持把代碼打包成多個文件, 在有需要的時候載入. 這就意味著訪問任何一個頁面都會全量載入所有文件.
  • Browserify對其他非js文件的載入不夠完善, 因為它主要解決的是require()js模塊的問題, 其他文件不是它關心的部分. 比如html文件里的img標籤, 它只能轉成Data URI的形式, 而不能替換為打包後的路徑.

  • 因為上面一點Browserify對資源文件的載入支持不夠完善, 導致打包時一般都要配合gulp或grunt一塊使用, 無謂地增加了打包的難度.

  • Browserify只支持CommonJS模塊規範, 不支持AMD和ES6模塊規範, 這意味舊的AMD模塊和將來的ES6模塊不能使用.

基於以上幾點, Browserify並不是一個理想的選擇. 那麼webpack是否解決了以上的幾個問題呢? 廢話, 不然介紹它幹嘛. 那麼下面章節我們用實戰的方式來說明webpack是怎麼解決上述的問題的.

上手先搞一個簡單的SPA應用

一上來步子太大容易扯到蛋, 讓我們先弄個最簡單的webpack配置來熱一下身.

安裝Node.js

webpack是基於我大Node.js的打包工具, 上來第一件事自然是先安裝Node.js了,傳送門->.

初始化一個項目

我們先隨便找個地方, 建一個文件夾叫simple, 然後在這裡面搭項目. 完成品在examples/simple目錄, 大家搞的時候可以參照一下. 我們先看一下目錄結構:

├── dist 打包輸出目錄, 只需部署這個目錄到生產環境├── package.json 項目配置信息├── node_modules npm安裝的依賴包都在這裡面├── src 我們的源代碼│ ├── components 可以復用的模塊放在這裡面│ ├── index.html 入口html│ ├── index.js 入口js│ ├── libs 不在npm和git上的庫扔這裡│ └── views 頁面放這裡└── webpack.config.js webpack配置文件

打開命令行窗口, cd到剛才建的simple目錄. 然後執行這個命令初始化項目:

npm init

命令行會要你輸入一些配置信息, 我們這裡一路按回車下去, 生成一個默認的項目配置文件package.json.

給項目加上語法報錯和代碼規範檢查

我們安裝eslint, 用來檢查語法報錯, 當我們書寫js時, 有錯誤的地方會出現提示.

npm install eslint eslint-config-enough eslint-loader --save-dev

npm install可以一條命令同時安裝多個包, 包之間用空格分隔. 包會被安裝進node_modules目錄中.

--save-dev會把安裝的包和版本號記錄到package.json中的devDependencies對象中, 還有一個--save, 會記錄到dependencies對象中, 它們的區別, 我們可以先簡單的理解為打包工具和測試工具用到的包使用--save-dev存到devDependencies, 比如eslint, webpack. 瀏覽器中執行的js用到的包存到dependencies, 比如jQuery等. 那麼它們用來幹嘛的?

因為有些npm包安裝是需要編譯的, 那麼導致windows/mac/linux上編譯出的可執行文件是不同的, 也就是無法通用, 因此我們在提交代碼到git上去的時候, 一般都會在.gitignore里指定忽略node_modules目錄和裡面的文件, 這樣其他人從git上拉下來的項目是沒有node_modules目錄的, 這時我們需要運行

npm install

它會讀取package.json中的devDependencies和dependencies欄位, 把記錄的包的相應版本下載下來.

這裡eslint-config-enough是配置文件, 它規定了代碼規範, 要使它生效, 我們要在package.json中添加內容:

{ "eslintConfig": { "extends": "enough", "env": { "browser": true, "node": true } }}

業界最有名的語法規範是airbnb出品的, 但它規定的太死板了, 比如不允許使用for-of和for-in等. 感興趣的同學可以參照這裡安裝使用.

eslint-loader用於在webpack編譯的時候檢查代碼, 如果有錯誤, webpack會報錯.

項目里安裝了eslint還沒用, 我們的IDE和編輯器也得要裝eslint插件支持它.

Visual Studio Code需要安裝ESLint擴展

atom需要安裝linter和linter-eslint這兩個插件, 裝好後重啟生效.

WebStorm需要在設置中打開eslint開關:

寫幾個頁面

我們寫一個最簡單的SPA應用來介紹SPA應用的內部工作原理. 首先, 建立src/index.html文件, 內容如下:

<!DOCTYPE html><html> <head> <meta charset="utf-8"> </head> <body> </body></html>

它是一個空白頁面, 注意這裡我們不需要自己寫<script src="index.js"></script>, 因為打包後的文件名和路徑可能會變, 所以我們用webpack插件幫我們自動加上.

然後重點是src/index.js:

// 引入作為全局對象儲存空間的global.js, js文件可以省略後綴import g from ./global// 引入頁面文件import foo from ./views/fooimport bar from ./views/barconst routes = { /foo: foo, /bar: bar}// Router類, 用來控制頁面根據當前URL切換class Router { start() { // 點擊瀏覽器後退/前進按鈕時會觸發window.onpopstate事件, 我們在這時切換到相應頁面 // https://developer.mozilla.org/en-US/docs/Web/Events/popstate window.addEventListener(popstate, () => { this.load(location.pathname) }) // 打開頁面時載入當前頁面 this.load(location.pathname) } // 前往path, 會變更地址欄URL, 並載入相應頁面 go(path) { // 變更地址欄URL history.pushState({}, , path) // 載入頁面 this.load(path) } // 載入path路徑的頁面 load(path) { // 創建頁面實例 const view = new routes[path]() // 調用頁面方法, 把頁面載入到document.body中 view.mount(document.body) }}// new一個路由對象, 賦值為g.router, 這樣我們在其他js文件中可以引用到g.router = new Router()// 啟動g.router.start()

現在我們還沒有講webpack配置所以頁面還無法訪問, 我們先從理論上講解一下, 等會弄好webpack配置後再實際看頁面效果. 當我們訪問localhost:8100/foo的時候, 路由會載入 ./views/foo/index.js文件, 我們來看看這個文件:

// 引入全局對象import g from ../../global// 引入html模板, 會被作為字元串引入import template from ./index.html// 引入css, 會生成<style>塊插入到<head>頭中import ./style.css// 導出類export default class { mount(container) { document.title = foo container.innerHTML = template container.querySelector(.foo__gobar).addEventListener(click, () => { // 調用router.go方法載入 /bar 頁面 g.router.go(/bar) }) }}

藉助webpack插件, 我們可以import html, css等其他格式的文件, 文本類的文件會被儲存為變數打包進js文件, 其他二進位類的文件, 比如圖片, 可以自己配置, 小圖片作為Data URI打包進js文件, 大文件打包為單獨文件, 我們稍後再講這塊.

其他的 src 目錄下的文件大家自己瀏覽, 拷貝一份到自己的工作目錄, 等會打包時會用到.

頁面代碼這樣就差不多搞定了, 接下來我們進入webpack的安裝和配置階段.

安裝webpack和Babel

我們把webpack和它的插件安裝到項目:

npm install webpack webpack-dev-server html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev

webpack-dev-server是webpack提供的用來開發調試的伺服器, 讓你可以用 127.0.0.1:8080/ 這樣的url打開頁面來調試, 有了它就不用配置nginx了, 方便很多.

html-webpack-plugin, html-loader,css-loader,style-loader等看名字就知道是打包html文件, css文件的插件, 大家在這裡可能會有疑問, `html-webpack-plugin`和`html-loader`有什麼區別, css-loader和style-loader有什麼區別, 我們等會看配置文件的時候再講.

file-loader和url-loader是打包二進位文件的插件, 具體也在配置文件章節講解.

接下來, 為了能讓不支持ES6的瀏覽器(比如IE)也能照常運行, 我們需要安裝babel, 它會把我們寫的ES6源代碼轉化成ES5, 這樣我們源代碼寫ES6, 打包時生成ES5.

npm install babel-core babel-preset-env babel-loader --save-dev

這裡babel-core顧名思義是babel的核心編譯器.babel-preset-env是一個配置文件, 我們可以使用這個配置文件轉換ES2015/ES2016/ES2017到ES5, 是的, 不只ES6哦. babel還有其他配置文件. 如果只想用ES6, 可以安裝babel-preset-es2015:

npm install babel-preset-es2015 --save-dev

但是光安裝了babel-preset-env, 在打包時是不會生效的, 需要在package.json加入babel配置:

{ "babel": { "presets": [ "env" ] }}

打包時babel會讀取package.json中babel欄位的內容, 然後執行相應的轉換.

如果使用babel-preset-es2015, 這裡相應的也要修改為:

{ "babel": { "presets": [ "es2015" ] }}

babel-loader是webpack的插件, 我們下面章節再說.

配置webpack

包都裝好了, 接下來, 總算可以進入正題了, 是不是有點心累...呵呵. 我們來創建webpack配置文件webpack.config.js, 注意這個文件是在node.js中運行的, 因此不支持ES6的import語法. 我們來看文件內容:

const { resolve } = require(path)const HtmlWebpackPlugin = require(html-webpack-plugin)module.exports = { // 配置頁面入口js文件 entry: ./src/index.js, // 配置打包輸出相關 output: { // 打包輸出目錄 path: resolve(__dirname, dist), // 入口js的打包輸出文件名 filename: index.js }, module: { /* 配置各種類型文件的載入器, 稱之為loader webpack當遇到import ... 時, 會調用這裡配置的loader對引用的文件進行編譯 */ rules: [ { /* 使用babel編譯ES6/ES7/ES8為ES5代碼 使用正則表達式匹配後綴名為.js的文件 */ test: /.js$/, // 排除node_modules目錄下的文件, npm安裝的包不需要編譯 exclude: /node_modules/, /* use指定該文件的loader, 值可以是字元串或者數組. 這裡先使用eslint-loader處理, 返回的結果交給babel-loader處理. loader的處理順序是從最後一個到第一個. eslint-loader用來檢查代碼, 如果有錯誤, 編譯的時候會報錯. babel-loader用來編譯js文件. */ use: [babel-loader, eslint-loader] }, { // 匹配.html文件 test: /.html$/, /* 使用html-loader, 將html內容存為js字元串, 比如當遇到 import htmlString from ./template.html template.html的文件內容會被轉成一個js字元串, 合併到js文件里. */ use: html-loader }, { // 匹配.css文件 test: /.css$/, /* 先使用css-loader處理, 返回的結果交給style-loader處理. css-loader將css內容存為js字元串, 並且會把background, @font-face等引用的圖片, 字體文件交給指定的loader打包, 類似上面的html-loader, 用什麼loader同樣在loaders對象中定義, 等會下面就會看到. */ use: [style-loader, css-loader] }, { /* 匹配各種格式的圖片和字體文件 上面html-loader會把html中<img>標籤的圖片解析出來, 文件名匹配到這裡的test的正則表達式, css-loader引用的圖片和字體同樣會匹配到這裡的test條件 */ test: /.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(?.+)?$/, /* 使用url-loader, 它接受一個limit參數, 單位為位元組(byte) 當文件體積小於limit時, url-loader把文件轉為Data URI的格式內聯到引用的地方 當文件大於limit時, url-loader會調用file-loader, 把文件儲存到輸出目錄, 並把引用的文件路徑改寫成輸出後的路徑 比如 views/foo/index.html中 <img src="smallpic.png"> 會被編譯成 <img src_="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAA..."> <img src="largepic.png"> 會被編譯成 <img src="/f78661bef717cf2cc2c2e5158f196384.png"> */ use: [ { loader: url-loader, options: { limit: 10000 } } ] } ] }, /* 配置webpack插件 plugin和loader的區別是, loader是在import時根據不同的文件名, 匹配不同的loader對這個文件做處理, 而plugin, 關注的不是文件的格式, 而是在編譯的各個階段, 會觸發不同的事件, 讓你可以干預每個編譯階段. */ plugins: [ /* html-webpack-plugin用來打包入口html文件 entry配置的入口是js文件, webpack以js文件為入口, 遇到import, 用配置的loader載入引入文件 但作為瀏覽器打開的入口html, 是引用入口js的文件, 它在整個編譯過程的外面, 所以, 我們需要html-webpack-plugin來打包作為入口的html文件 */ new HtmlWebpackPlugin({ /* template參數指定入口html文件路徑, 插件會把這個文件交給webpack去編譯, webpack按照正常流程, 找到loaders中test條件匹配的loader來編譯, 那麼這裡html-loader就是匹配的loader html-loader編譯後產生的字元串, 會由html-webpack-plugin儲存為html文件到輸出目錄, 默認文件名為index.html 可以通過filename參數指定輸出的文件名 html-webpack-plugin也可以不指定template參數, 它會使用默認的html模板. */ template: ./src/index.html }) ], /* 配置開發時用的伺服器, 讓你可以用 http://127.0.0.1:8080/ 這樣的url打開頁面來調試 並且帶有熱更新的功能, 打代碼時保存一下文件, 瀏覽器會自動刷新. 比nginx方便很多 如果是修改css, 甚至不需要刷新頁面, 直接生效. 這讓像彈框這種需要點擊交互後才會出來的東西調試起來方便很多. */ devServer: { // 配置監聽埠, 因為8080很常用, 為了避免和其他程序衝突, 我們配個其他的埠號 port: 8100, /* historyApiFallback用來配置頁面的重定向 SPA的入口是一個統一的html文件, 比如 http://localhost:8010/foo 我們要返回給它 http://localhost:8010/index.html 這個文件 配置為true, 當訪問的文件不存在時, 返回根目錄下的index.html文件 */ historyApiFallback: true }}

走一個

配置OK了, 接下來我們就運行一下吧. 我們先試一下開發環境用的webpack-dev-server:

./node_modules/.bin/webpack-dev-server -d --hot

上面的命令適用於Mac/Linux等*nix系統, 也適用於Windows上的PowerShell和bash/zsh環境(Bash on Wbuntu on Windows, Git Bash,Babun,MSYS2等).

如果使用Windows的cmd.exe, 請執行:

node_modules.binwebpack-dev-server -d --hot

我在這裡安利Windows同學使用Bash on Ubuntu on Windows, 可以避免很多跨平台的問題, 比如設置環境變數.

npm會把包的可執行文件安裝到./node_modules/.bin/目錄下, 所以我們要在這個目錄下執行命令.

-d參數是開發環境(Development)的意思, 它會在我們的配置文件中插入調試相關的選項, 比如打開debug, 打開sourceMap, 代碼中插入源文件路徑注釋.

--hot開啟熱更新功能, 參數會幫我們往配置里添加HotModuleReplacementPlugin插件, 雖然可以在配置里自己寫, 但有點麻煩, 用命令行參數方便很多.

命令執行後, 控制台的最後一行應該是

webpack: bundle is now VALID.

這就代表編譯成功了, 我們可以在瀏覽器打開localhost:8100/foo 看看效果. 如果有報錯, 那可能是什麼地方沒弄對? 請自己仔細檢查一下~

我們可以隨意更改一下src目錄下的源代碼, 保存後, 瀏覽器里的頁面應該很快會有相應變化.

要退出編譯, 按ctrl+c.

開發環境編譯試過之後, 我們試試看編譯生產環境的代碼, 命令是:

./node_modules/.bin/webpack -p

-p參數會開啟生產環境模式, 這個模式下webpack會將代碼做壓縮等優化.

大家可能會發現, 執行腳本的命令有點麻煩. 因此, 我們可以利用npm的特性, 把命令寫在package.json中:

{ "scripts": { "dev": "webpack-dev-server -d --hot --env.dev", "build": "webpack -p" }}

package.json中的scripts對象, 可以用來寫一些腳本命令, 命令不需要前綴目錄 ./node_modules/.bin/, npm會自動尋找該目錄下的命令. 我們可以執行

npm run dev

來啟動開發環境.

執行

npm run build

來打包生產環境的代碼.

進階配置

上面的項目雖然可以跑起來了, 但有幾個點我們還沒有考慮到:

  • 指定靜態資源的url路徑前綴

  • 各個頁面分開打包

  • 打包時區分開發環境和生產環境

  • 輸出的entry文件加上hash

  • 第三方庫和業務代碼分開打包

  • 開發環境關閉performance.hints

  • 配置favicon

  • 開發環境允許其他電腦訪問

  • 打包時自定義部分參數

  • webpack-dev-server處理帶後綴名的文件的特殊規則

  • 代碼中插入環境變數

  • 簡化import路徑

  • 優化babel編譯後的代碼性能

  • 使用webpack 2自帶的ES6模塊處理功能

  • 使用autoprefixer自動創建css的vendor prefixes

  • * 編譯前清空dist目錄
  • 那麼, 讓我們在上面的配置的基礎上繼續完善, 下面的代碼我們只寫出改變的部分. 代碼在examples/advanced目錄.

指定靜態資源的url路徑前綴

現在我們的資源文件的url直接在根目錄, 比如127.0.0.1:8100/index.js, 這樣做緩存控制和CDN都不方便, 我們需要給資源文件的url加一個前綴, 比如127.0.0.1:8100/assets/i這樣. 我們來修改一下webpack配置:

{ output: { publicPath: /assets/ }, devServer: { // 指定index.html文件的url路徑 historyApiFallback: { index: /assets/ } }}

各個頁面分開打包

這樣瀏覽器只需載入當前訪問的頁面的代碼.

webpack可以使用非同步載入文件的方式引用模塊, webpack 1的API是require.ensure(), webpack 2開始支持TC39的dynamic import. 我們這裡就使用新的import()來實現頁面分開打包非同步載入. 話不多說, 上代碼

src/index.js:

load(path) { import(./views + path + /index.js).then(module => { // export default ... 的內容通過module.default訪問 const View = module.default const view = new View() view.mount(document.body) })}

這樣我們就不需要在開頭把所有頁面文件都import進來了.

因為import()還沒有正式進入標準, 因此babel和eslint需要插件來支持它:

npm install babel-eslint babel-preset-stage-2 --save-dev

package.json改一下:

{ "babel": { "presets": [ "env", "stage-2" ] }, "eslintConfig": { "parser": "babel-eslint", "extends": "enough", "env": { "browser": true, "node": true } }}

然後修改webpack配置:

{ output: { /* import()載入的文件會被分開打包, 我們稱這個包為chunk, chunkFilename用來配置這個chunk輸出的文件名. [id]: 編譯時每個chunk會有一個id. [chunkhash]: 這個chunk的hash值, 文件發生變化時該值也會變. 文件名加上該值可以防止瀏覽器讀取舊的緩存文件. */ chunkFilename: [id].js?[chunkhash], }}

打包時區分開發環境和生產環境

如果webpack.config.js導出的是一個function, 那麼webpack會執行它, 並把返回的結果作為配置對象.

module.exports = (options = {}) => { return { // 配置內容 }}

該function接受一個參數, 這個參數的值是由命令行傳入的. 比如當我們在命令行中執行:

webpack --env.dev --env.server localhost

那麼options值為 `{ dev: true, server: localhost }`

該參數對 webpack-dev-server 命令同樣有效.

我們修改一下package.json, 給dev腳本加上env.dev:

{ "scripts": { "dev": "webpack-dev-server -d --hot --env.dev", }}

輸出的entry文件加上hash

上面我們提到了chunkFilename可以加上[chunkhash]防止瀏覽器讀取錯誤緩存, 那麼entry同樣需要加上hash. 但使用webpack-dev-server啟動開發環境時, entry文件是沒有[chunkhash]的, 用了會報錯. 因此我們需要利用上面提到的區分開發環境和生產環境的功能, 只在打包生產環境代碼時加上[chunkhash]

module.exports = (options = {}) => { return { /* 這裡entry我們改用對象來定義 屬性名在下面的output.filename中使用, 值為文件路徑 */ entry: { index: ./src/index, }, output: { /* entry欄位配置的入口js的打包輸出文件名 [name]作為佔位符, 在輸出時會被替換為entry里定義的屬性名, 比如這裡會被替換為"index" [chunkhash]是打包後輸出文件的hash值的佔位符, 把?[chunkhash]跟在文件名後面可以防止瀏覽器使用緩存的過期內容, 這裡, webpack會生成以下代碼插入到index.html中: <script type="text/javascript" src="/assets/index.js?d835352892e6aac768bf"></script> 這裡/assets/目錄前綴是output.publicPath配置的 options.dev是命令行傳入的參數. 這裡是由於使用webpack-dev-server啟動開發環境時, 是沒有[chunkhash]的, 用了會報錯 因此我們不得已在使用webpack-dev-server啟動項目時, 命令行跟上--env.dev參數, 當有該參數時, 不在後面跟[chunkhash] */ filename: options.dev ? [name].js : [name].js?[chunkhash], } }}

有人可能注意到官網文檔中還有一個[hash]佔位符, 這個hash是整個編譯過程產生的一個總的hash值, 而不是單個文件的hash值, 項目中任何一個文件的改動, 都會造成這個hash值的改變. [hash]佔位符是始終存在的, 但我們不希望修改一個文件導致所有輸出的文件hash都改變, 這樣就無法利用瀏覽器緩存了. 因此這個[hash]意義不大.

第三方庫和業務代碼分開打包

這樣更新業務代碼時可以藉助瀏覽器緩存, 用戶不需要重新下載沒有發生變化的第三方庫.

我們的思路是, 入口的html文件引兩個js, vendor.js和index.js。vendor.js用來引用第三方庫, 比如這兒我們引入一個第三方庫來做路由, 我們先安裝它:

npm install spa-history --save

然後在vendor.js中, 我們引用一下它:

import spa-history/PathHistory

我們import它但不需要做什麼, 這樣webpack打包的時候會把這個第三方庫打包進vendor.js.

然後在src/index.js中, 我們使用它:

import PathHistory from spa-history/PathHistoryconst history = new PathHistory({ change(location) { // 使用import()將載入的js文件分開打包, 這樣實現了僅載入訪問的頁面 import(./views + location.path + /index.js).then(module => { // export default ... 的內容通過module.default訪問 const View = module.default const view = new View() view.mount(document.body) }) }})history.hookAnchorElements()history.start()

頁面foo和bar的js和html文件因為路由的改變也要做些微調.

src/views/foo/index.js

import template from ./index.htmlimport ./style.cssexport default class { mount(container) { document.title = foo container.innerHTML = template }}

src/views/foo/index.html:

<div class="foo"> <h1>Page Foo</h1> <a href="/bar">goto bar</a> <p> <img src="smallpic.png"> </p> <p> <img src="/views/foo/largepic.png"> </p></div>

src/views/bar/index.js:

import template from ./index.htmlimport ./style.cssexport default class { mount(container) { document.title = bar container.innerHTML = template }}

src/views/bar/index.html:

<div class="bar"> <h1>Page Bar</h1> <a href="/foo">goto foo</a></div>

然後最重要的webpack的配置需要修改一下:

// 引入webpack, 等會需要用const webpack = require(webpack)module.exports = (options = {}) => { return { // entry中加入vendor entry: { vendor: ./src/vendor, index: ./src/index }, plugins: [ /* 使用CommonsChunkPlugin插件來處理重複代碼 因為vendor.js和index.js都引用了spa-history, 如果不處理的話, 兩個文件里都會有spa-history包的代碼, 我們用CommonsChunkPlugin插件來使共同引用的文件只打包進vendor.js */ new webpack.optimize.CommonsChunkPlugin({ /* names: 將entry文件中引用的相同文件打包進指定的文件, 可以是新建文件, 也可以是entry中已存在的文件 這裡我們指定打包進vendor.js 但這樣還不夠, 還記得那個chunkFilename參數嗎? 這個參數指定了chunk的打包輸出的名字, 我們設置為 [id].js?[chunkhash] 的格式. 那麼打包時這個文件名存在哪裡的呢? 它就存在引用它的文件中. 這就意味著被引用的文件發生改變, 會導致引用的它文件也發生改變. 然後CommonsChunkPlugin有個附加效果, 會把所有chunk的文件名記錄到names指定的文件中. 那麼這時當我們修改頁面foo或者bar時, vendor.js也會跟著改變, 而index.js不會變. 那麼怎麼處理這些chunk, 使得修改頁面代碼而不會導致entry文件改變呢? 這裡我們用了一點小技巧. names參數可以是一個數組, 意思相當於調用多次CommonsChunkPlugin, 比如: plugins: [ new webpack.optimize.CommonsChunkPlugin({ names: [vendor, manifest] }) ] 相當於 plugins: [ new webpack.optimize.CommonsChunkPlugin({ names: vendor }), new webpack.optimize.CommonsChunkPlugin({ names: manifest }) ] 首先把重複引用的庫打包進vendor.js, 這時候我們的代碼里已經沒有重複引用了, chunk文件名存在vendor.js中, 然後我們在執行一次CommonsChunkPlugin, 把所有chunk的文件名打包到manifest.js中. 這樣我們就實現了chunk文件名和代碼的分離. 這樣修改一個js文件不會導致其他js文件在打包時發生改變, 只有manifest.js會改變. */ names: [vendor, manifest] }) ] }}

開發環境關閉performance.hints

我們注意到運行開發環境是命令行會報一段warning:

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (250 kB).This can impact web performance.

這是說建議每個輸出的js文件的大小不要超過250k. 但開發環境因為包含了sourcemap並且代碼未壓縮所以一般都會超過這個大小, 所以我們可以在開發環境把這個warning關閉.

webpack配置中加入:

{ performance: { hints: options.dev ? false : warning }}

配置favicon

在src目錄中放一張favicon.png, 然後src/index.html的<head>中插入:

<link rel="icon" type="image/png" href="favicon.png">

修改webpack配置:

{ module: { rules: [ { test: /.html$/, use: [ { loader: html-loader, options: { /* html-loader接受attrs參數, 表示什麼標籤的什麼屬性需要調用webpack的loader進行打包. 比如<img>標籤的src屬性, webpack會把<img>引用的圖片打包, 然後src的屬性值替換為打包後的路徑. 使用什麼loader代碼, 同樣是在module.rules定義中使用匹配的規則. 如果html-loader不指定attrs參數, 默認值是img:src, 意味著會默認打包<img>標籤的圖片. 這裡我們加上<link>標籤的href屬性, 用來打包入口index.html引入的favicon.png文件. */ attrs: [img:src, link:href] } } ] }, { /* 匹配favicon.png 上面的html-loader會把入口index.html引用的favicon.png圖標文件解析出來進行打包 打包規則就按照這裡指定的loader執行 */ test: /favicon.png$/, use: [ { // 使用file-loader loader: file-loader, options: { // name: 指定文件輸出名 // [name]是源文件名, 不包含後綴. [ext]為後綴. [hash]為源文件的hash值, // 這裡我們保持文件名, 在後面跟上hash, 防止瀏覽器讀取過期的緩存文件. name: [name].[ext]?[hash] } } ] }, // 圖片文件的載入配置增加一個exclude參數 { test: /.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(?.+)?$/, // 排除favicon.png, 因為它已經由上面的loader處理了. 如果不排除掉, 它會被這個loader再處理一遍 exclude: /favicon.png$/, use: [ { loader: url-loader, options: { limit: 10000 } } ] } ] }}

其實html-webpack-plugin接受一個favicon參數, 可以指定favicon文件路徑, 會自動打包插入到html文件中. 但它有個bug, 打包後的文件名路徑不帶hash, 就算有hash, 它也是[hash], 而不是[chunkhash], 導致修改代碼也會改變favicon打包輸出的文件名. issue中提到的favicons-webpack-plugin倒是可以用, 但它依賴PhantomJS, 非常大.

開發環境允許其他電腦訪問

webpack配置devServer.host為`0.0.0.0`即可.

打包時自定義部分參數

在多人開發時, 每個人可能需要有自己的配置, 比如說webpack-dev-server監聽的埠號, 如果寫死在webpack配置里, 而那個埠號在某個同學的電腦上被其他進程佔用了, 簡單粗暴的修改webpack.config.js會導致提交代碼後其他同學的埠也被改掉.

還有一點就是開發環境/測試環境/生產環境的部分webpack配置是不同的, 比如publicPath在生產環境可能要配置一個CDN地址.

我們在根目錄建立一個文件夾config, 裡面創建3個配置文件:

  • default.js: 生產環境

module.exports = { publicPath: http://cdn.example.com/assets/}

  • dev.js: 默認開發環境

module.exports = { publicPath: /assets/, devServer: { port: 8100, proxy: { /api/auth/: { target: http://api.example.dev, changeOrigin: true, pathRewrite: { ^/api: } }, /api/pay/: { target: http://pay.example.dev, changeOrigin: true, pathRewrite: { ^/api: } } } }}

  • local.js: 個人本地環境, 在dev.js基礎上修改部分參數.

const config = require(./dev)config.devServer.port = 8200module.exports = config

package.json修改scripts:

{ "scripts": { "local": "npm run dev --config=local", "dev": "webpack-dev-server -d --hot --env.dev --env.config dev", "build": "rimraf dist && webpack -p" }}

webpack配置修改:

// ...const url = require(url)module.exports = (options = {}) => { const config = require(./config/ + (process.env.npm_config_config || options.config || default)) return { // ... devServer: config.devServer ? { host: 0.0.0.0, port: config.devServer.port, proxy: config.devServer.proxy, historyApiFallback: { index: url.parse(config.publicPath).pathname } } : undefined, }}

這裡的關鍵是npm run傳進來的自定義參數可以通過process.env.npm_config_*獲得. 參數中如果有-會被轉成_

--env.*傳進來的參數可以通過options.*獲得. 我們優先使用npm run指定的配置文件. 這樣我們可以在命令行覆蓋scripts中指定的配置文件:

npm run dev --config=CONFIG_NAME

local命令就是這樣做的.

這樣, 當我們執行npm run dev時使用的是dev.js, 執行npm run local使用local.js, 執行npm run build使用default.js.

config.devServer.proxy用來配置後端api的反向代理, ajax /api/auth/*的請求會被轉發到api.example.dev/auth/*, /api/pay/*的請求會被轉發到api.example.dev/pay/*.

changeOrigin會修改HTTP請求頭中的Host為target的域名, 這裡會被改為api.example.dev

pathRewrite用來改寫URL, 這裡我們把/api前綴去掉.

還有一點, 我們不需要把自己個人用的配置文件提交到git, 所以我們在.gitignore中加入:

config/*!config/default.js!config/dev.js

把config目錄排除掉, 但是保留生產環境和dev默認配置文件.

webpack-dev-server處理帶後綴名的文件的特殊規則

當處理帶後綴名的請求時, 比如 localhost:8100/bar.do , webpack-dev-server會認為它應該是一個實際存在的文件, 就算找不到該文件, 也不會fallback到index.html, 而是返回404. 但在SPA應用中這不是我們希望的. 幸好webpack-dev-server有一個配置選項disableDotRule: true可以禁用這個規則, 使帶後綴的文件當不存在時也能fallback到index.html

historyApiFallback: { index: url.parse(config.publicPath).pathname, disableDotRule: true}

代碼中插入環境變數

在業務代碼中, 有些變數在開發環境和生產環境是不同的, 比如域名, 後台API地址等. 還有開發環境可能需要列印調試信息等.

我們可以使用DefinePlugin插件在打包時往代碼中插入需要的環境變數,

// ...const pkgInfo = require(./package.json)module.exports = (options = {}) => { const config = require(./config/ + (process.env.npm_config_config || options.config || default)).default return { // ... plugins: [ new webpack.DefinePlugin({ DEBUG: Boolean(options.dev), VERSION: JSON.stringify(pkgInfo.version), CONFIG: JSON.stringify(config.runtimeConfig) }) ] }}

DefinePlugin插件的原理很簡單, 如果我們在代碼中寫:

console.log(DEBUG)

它會做類似這樣的處理:

console.log(DEBUG).replace(DEBUG, true)

最後生成:

console.log(true)

這裡有一點需要注意, 像這裡的VERSION, 如果我們不對pkgInfo.version做JSON.stringify(),

console.log(VERSION)

然後做替換操作:

console.log(VERSION).replace(VERSION, 1.0.0)

最後生成:

console.log(1.0.0)

這樣語法就錯誤了. 所以, 我們需要JSON.stringify(pkgInfo.version)轉一下變成"1.0.0", 替換的時候才會帶引號.

還有一點, webpack打包壓縮的時候, 會把代碼進行優化, 比如:

if (DEBUG) { console.log(debug mode)} else { console.log(production mode)}

會被編譯成:

if (false) { console.log(debug mode)} else { console.log(production mode)}

然後壓縮優化為:

console.log(production mode)

簡化import路徑

文件a引入文件b時, b的路徑是相對於a文件所在目錄的. 如果a和b在不同的目錄, 藏得又深, 寫起來就會很麻煩:

import b from ../../../components/b

為了方便, 我們可以定義一個路徑別名(alias):

resolve: { alias: { ~: resolve(__dirname, src) }}

這樣, 我們可以以`src`目錄為基礎路徑來`import`文件:

import b from ~/components/b

html中的<img>標籤沒法使用這個別名功能, 但html-loader有一個root參數, 可以使 / 開頭的文件相對於root目錄解析.

{ test: /.html$/, use: [ { loader: html-loader, options: { root: resolve(__dirname, src), attrs: [img:src, link:href] } } ]}

那麼, <img src="/favicon.png">就能順利指向到src目錄下的favicon.png文件, 不需要關心當前文件和目標文件的相對路徑.

PS: 在調試<img>標籤的時候遇到一個坑, html-loader會解析<!-- -->注釋中的內容, 之前在注釋中寫的

<!--大於10kb的圖片, 圖片會被儲存到輸出目錄, src會被替換為打包後的路徑<img src="/assets/f78661bef717cf2cc2c2e5158f196384.png">-->

之前因為沒有加root參數, 所以`/`開頭的文件名不會被解析, 加了root導致編譯時報錯, 找不到該文件. 大家記住這一點.

優化babel編譯後的代碼性能

babel編譯後的代碼一般會造成性能損失, babel提供了一個loose選項, 使編譯後的代碼不需要完全遵循ES6規定, 簡化編譯後的代碼, 提高代碼執行效率:

package.json:

{ "babel": { "presets": [ [ "env", { "loose": true } ], "stage-2" ] }}

但這麼做會有兼容性的風險, 可能會導致ES6源碼理應的執行結果和編譯後的ES5代碼的實際結果並不一致. 如果代碼沒有遇到實際的效率瓶頸, 官方不建議使用loose模式.

使用webpack 2自帶的ES6模塊處理功能

我們目前的配置, babel會把ES6模塊定義轉為CommonJS定義, 但webpack自己可以處理import和export, 而且webpack處理import時會做代碼優化, 把沒用到的部分代碼刪除掉. 因此我們通過babel提供的modules: false選項把ES6模塊轉為CommonJS模塊的功能給關閉掉.

package.json:

{ "babel": { "presets": [ [ "env", { "loose": true, "modules": false } ], "stage-2" ] }}

使用autoprefixer自動創建css的vendor prefixes

css有一個很麻煩的問題就是比較新的css屬性在各個瀏覽器里是要加前綴的, 我們可以使用autoprefixer工具自動創建這些瀏覽器規則, 那麼我們的css中只需要寫:

:fullscreen a { display: flex}

autoprefixer會編譯成:

:-webkit-full-screen a { display: -webkit-box; display: flex}:-moz-full-screen a { display: flex}:-ms-fullscreen a { display: -ms-flexbox; display: flex}:fullscreen a { display: -webkit-box; display: -ms-flexbox; display: flex}

首先, 我們用npm安裝它:

npm install postcss-loader autoprefixer --save-dev

autoprefixer是postcss的一個插件, 所以我們也要安裝postcss的webpack loader.

修改一下webpack的css rule:

{ test: /.css$/, use: [style-loader, css-loader, postcss-loader]}

然後創建文件postcss.config.js:

module.exports = { plugins: [ require(autoprefixer)() ]}

編譯前清空dist目錄

不清空的話上次編譯生成的文件會遺留在dist目錄中, 我們最好先把目錄清空一下. macOS/Linux下可以用rm -rf dist搞定, 考慮到跨平台的需求, 我們可以用rimraf:

npm install rimraf --save-dev

package.json修改一下:

{ "scripts": { "build": "rimraf dist && webpack -p --env.config production" },}

傳統的多頁面網站(MPA)能否用webpack打包?

對於多頁面網站, 我們最多的是用Grunt或Gulp來打包, 因為這種簡單的頁面對模塊化編程的需求不高. 但如果你喜歡上使用import來引入庫, 那麼我們仍然可以使用webpack來打包.

MPA意味著並沒不是一個單一的html入口和js入口, 而是每個頁面對應一個html和多個js. 那麼我們可以把項目結構設計為:

├── dist├── package.json├── node_modules├── src│ ├── components│ ├── libs| ├── favicon.png| ├── vendor.js 所有頁面公用的第三方庫│ └── pages 頁面放這裡| ├── foo 編譯後生成 http://localhost:8100/foo.html| | ├── index.html| | ├── index.js| | ├── style.css| | └── pic.png| └── bar http://localhost:8100/bar.html| ├── index.html| ├── index.js| ├── style.css| └── baz http://localhost:8100/bar/baz.html| ├── index.html| ├── index.js| └── style.css└── webpack.config.js

這裡每個頁面的index.html是個完整的從<!DOCTYPE html>開頭到</html>結束的頁面, 這些文件都要用html-webpack-plugin處理. index.js是每個頁面的業務邏輯, 全部作為入口js配置到entry中. 頁面公用的第三方庫仍然打包進vendor.js. 這裡我們需要用glob庫來把這些文件都篩選出來批量操作.

npm install glob --save-dev

webpack.config.js修改的地方:

// ...const glob = require(glob)module.exports = (options = {}) => { // ... const entries = glob.sync(./src/**/index.js) const entryJsList = {} const entryHtmlList = [] for (const path of entries) { const chunkName = path.slice(./src/pages/.length, -/index.js.length) entryJsList[chunkName] = path entryHtmlList.push(new HtmlWebpackPlugin({ template: path.replace(index.js, index.html), filename: chunkName + .html, chunks: [manifest, vendor, chunkName] })) } return { entry: Object.assign({ vendor: ./src/vendor }, entryJsList), // ... plugins: [ ...entryHtmlList, // ... ] }}

代碼在examples/mpa目錄.

其他問題

為什麼不使用webpack.config.babel.js

部分同學可能知道webpack可以讀取webpack.config.babel.js, 它會先調用babel將文件編譯後再執行. 但這裡有兩個坑:

1. 由於我們的package.json中的babel配置指定了modules: false, 所以babel並不會轉碼import, 這導致編譯後的webpack配置文件仍然無法在node.js中執行, 解決方案是package.json不指定modules: false, 而在babel-loader中的options中配置babel. 這樣webpack.config.babel.js會使用package.json的babel配置編譯, 而webpack編譯的js會使用babel-loader指定的配置編譯.

{ test: /.js$/, exclude: /node_modules/, use: [ { loader: babel-loader, options: { presets: [ [env, { loose: true, modules: false }], stage-2 ] } }, eslint-loader ]}

2. postcss的配置不支持先用babel轉碼, 這導致了我們的配置文件格式的不統一.

綜上, 還是只在src目錄中的文件使用ES6模塊規範會比較方便一點.

總結

通過這篇文章, 我想大家應該學會了webpack的正確打開姿勢. 雖然我沒有提及如何用webpack來編譯React和vue.js, 但大家可以想到, 無非是安裝一些loader和plugin來處理jsx和vue格式的文件, 那時難度就不在於webpack了, 而是代碼架構組織的問題了. 具體的大家自己去摸索一下. 以後有時間我會把腳手架整理一下放到github上, 供大家參考.

幾個腳手架

Vue Boilerplate

Vue SSR Boilerplate

版權許可

本作品採用知識共享署名-非商業性使用 4.0 國際許可協議進行許可.
推薦閱讀:

徹底解決Webpack打包性能問題
資源表+載入器的方案為什麼沒有webpack的本地打包方案流行?
webpack到底怎麼用?
iView 發布後台管理系統 iview-admin,沒錯,它就是你想要的
基於gulp+webpack構建基礎

TAG:前端框架 | 前端开发 | webpack |