Webpack 4 和單頁應用入門

github:github.com/fenivana/we…javascript

webpack 更新到了 4.0,官網尚未更新文檔。所以把教程更新一下,方便你們用起 webpack 4。php

webpack

寫在開頭

先說說爲何要寫這篇文章,最初的緣由是組裏的小朋友們看了 webpack 文檔後,表情都是這樣的:摘自 webpack 一篇文檔的評論區)css

WTF

和這樣的:html

You Couldn't Handle Me

是的,即便是外國佬也在吐槽這文檔不是人能看的。回想起當年本身啃 webpack 文檔的血與淚的往事,以爲有必要整一個教程,可讓你們看完後愉悅地搭建起一個 webpack 打包方案的項目。前端

官網新的 webpack 文檔如今寫的很詳細了,能看英文的小夥伴能夠直接去看官網。vue

可能會有人問 webpack 到底有什麼用,你不能上來就糊我一臉代碼讓我立刻搞,我照着搞了一遍結果根本沒什麼用,都是騙人的。因此,在說 webpack 以前,我想先談一下前端打包方案這幾年的演進歷程,在什麼場景下,咱們遇到了什麼問題,催生出了應對這些問題的工具。瞭解了需求和目的以後,你就知道何時 webpack 能夠幫到你。我但願我用完以後很爽,大家用完以後也是。java

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

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

隨後的幾年,人們開始嘗試在一個頁面裏作更多的事情。容器的顯示,隱藏,切換。用 css 寫的彈層,圖片輪播等等。但若是一個頁面內不能向服務器請求數據,能作的事情畢竟有限的,代碼的量也能維持在頁面交互邏輯範圍內。這時候不少人開始突破一個頁面能作的事情的範圍,使用隱藏的 iframe 和 flash 等做爲和服務器通訊的橋樑,新世界的大門慢慢地被打開,在一個頁面內和服務器進行數據交互,意味着之前須要跳轉多個頁面的事情如今能夠用一個頁面搞定。但因爲 iframe 和 flash 技術過於 tricky 和複雜,並沒能獲得普遍的推廣。python

直到 Google 推出 Gmail 的時候(2004 年),人們意識到了一個被忽略的接口,XMLHttpRequest, 也就是咱們俗稱的 AJAX, 這是一個使用方便的,兼容性良好的服務器通訊接口。今後開始,咱們的頁面開始玩出各類花來了,前端一會兒出現了各類各樣的庫,PrototypeDojoMooToolsExt JSjQuery…… 咱們開始往頁面裏插入各類庫和插件,咱們的 js 文件也就爆炸了。react

隨着 js 能作的事情愈來愈多,引用愈來愈多,文件愈來愈大,加上當時大約只有 2Mbps 左右的網速,下載速度還不如 3G 網絡,對 js 文件的壓縮和合並的需求愈來愈強烈,固然這裏面也有把代碼混淆了不容易被盜用等其餘因素在裏面。JSMinYUI CompressorClosure CompilerUglifyJS 等 js 文件壓縮合並工具陸陸續續誕生了。壓縮工具是有了,但咱們得要執行它,最簡單的辦法呢,就是 windows 上搞個 bat 腳本,mac / linux 上搞個 bash 腳本,哪幾個文件要合併在一塊的,哪幾個要壓縮的,發佈的時候運行一下腳本,生成壓縮後的文件。

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

  • 庫和插件爲了要給他人調用,確定要找個地方註冊,通常就是在 window 下申明一個全局的函數或對象。難保哪天用的兩個庫在全局用一樣的名字,那就衝突了。
  • 庫和插件若是還依賴其餘的庫和插件,就要告知使用人,須要先引哪些依賴庫,那些依賴庫也有本身的依賴庫的話,就要先引依賴庫的依賴庫,以此類推。

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

var b = require('./b')
var c = require('./c')
複製代碼

那麼變量 bc 會是什麼呢?那就是 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)
})
複製代碼

相應的導出語法也是異步回調方式,好比 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 也沒閒着。什麼 lesssassstylus 的 css 預處理器橫空出世,說能幫咱們簡化 css 的寫法,自動給你加 vendor prefix。html 在這期間也出現了一堆模板語言,什麼 handlebarsejsjade,能夠把 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),好比下圖:

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 規範,並且在 npm 3 以前,這些包有依賴項的話也是不能用的。後來有個 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
│   ├── shared                公共函數庫
│   └── 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 中的 devDependenciesdependencies 字段,把記錄的包的相應版本下載下來。

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

{
  "eslintConfig": {
    "extends": "enough",
    "env": {
      "browser": true,
      "node": true
    }
  }
}
複製代碼

業界最有名的語法規範是 airbnb 出品的,但它規定的太死板了,好比不容許使用 for-offor-in 等。感興趣的同窗能夠參照 這裏 安裝使用。

eslint-loader 用於在 webpack 編譯的時候檢查代碼,若是有錯誤,webpack 會報錯。

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

Visual Studio Code 須要安裝 ESLint 擴展

atom 須要安裝 linterlinter-eslint 這兩個插件,裝好後重啓生效。

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

WebStorm ESLint Config

寫幾個頁面

咱們寫一個最簡單的 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:

// 引入 router
import router from './router'

// 啓動 router
router.start()
複製代碼

src/router.js:

// 引入頁面文件
import foo from './views/foo'
import bar from './views/bar'

const 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) {
    // 首頁
    if (path === '/') path = '/foo'
    // 建立頁面實例
    const view = new routes[path]()
    // 調用頁面方法,把頁面加載到 document.body 中
    view.mount(document.body)
  }
}

// 導出 router 實例
export default new Router()
複製代碼

src/views/foo/index.js:

// 引入 router
import router from '../../router'

// 引入 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 頁面
      router.go('/bar')
    })
  }
}
複製代碼

src/views/bar/index.js:

// 引入 router
import router from '../../router'

// 引入 html 模板,會被做爲字符串引入
import template from './index.html'

// 引入 css, 會生成 <style> 塊插入到 <head> 頭中
import './style.css'

// 導出類
export default class {
  mount(container) {
    document.title = 'bar'
    container.innerHTML = template
    container.querySelector('.bar__gofoo').addEventListener('click', () => {
      // 調用 router.go 方法加載 /foo 頁面
      router.go('/foo')
    })
  }
}
複製代碼

藉助 webpack 插件,咱們能夠 import html, css 等其餘格式的文件,文本類的文件會被儲存爲變量打包進 js 文件,其餘二進制類的文件,好比圖片,能夠本身配置,小圖片做爲 Data URI 打包進 js 文件,大文件打包爲單獨文件,咱們稍後再講這塊。

其餘的 src 目錄下的文件你們本身瀏覽,拷貝一份到本身的工做目錄,等會打包時會用到。

頁面代碼這樣就差很少搞定了,接下來咱們進入 webpack 的安裝和配置階段。如今咱們尚未講 webpack 配置因此頁面還沒法訪問,等會弄好 webpack 配置後再看頁面實際效果。

安裝 webpack 和 Babel

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

npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev
複製代碼

webpack 即 webpack 核心庫。它提供了不少 API, 經過 Node.js 腳本中 require('webpack') 的方式來使用 webpack。

webpack-cli 是 webpack 的命令行工具。讓咱們能夠不用寫打包腳本,只需配置打包配置文件,而後在命令行輸入 webpack-cli --config webpack.config.js 來使用 webpack, 簡單不少。webpack 4 以前命令行工具是集成在 webpack 包中的,4.0 開始 webpack 包自己再也不集成 cli。

webpack-serve 是 webpack 提供的用來開發調試的服務器,讓你能夠用 http://127.0.0.1:8080/ 這樣的 url 打開頁面來調試,有了它就不用配置 nginx 了,方便不少。

html-webpack-plugin, html-loader, css-loader, style-loader 等看名字就知道是打包 html 文件,css 文件的插件,你們在這裏可能會有疑問,html-webpack-pluginhtml-loader 有什麼區別,css-loaderstyle-loader 有什麼區別,咱們等會看配置文件的時候再講。

file-loaderurl-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 還有 其餘配置文件

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

{
  "babel": {
    "presets": ["env"]
  }
}
複製代碼

打包時 babel 會讀取 package.jsonbabel 字段的內容,而後執行相應的轉換。

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

配置 webpack

包都裝好了,接下來總算能夠進入正題了。咱們來建立 webpack 配置文件 webpack.config.js,注意這個文件是在 node.js 中運行的,所以不支持 ES6 的 import 語法。咱們來看文件內容:

const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const history = require('connect-history-api-fallback')
const convert = require('koa-connect')

// 使用 WEBPACK_SERVE 環境變量檢測當前是不是在 webpack-server 啓動的開發環境中
const dev = Boolean(process.env.WEBPACK_SERVE)

module.exports = {
  /* webpack 執行模式 development:開發環境,它會在配置文件中插入調試相關的選項,好比 moduleId 使用文件路徑方便調試 production:生產環境,webpack 會將代碼作壓縮等優化 */
  mode: dev ? 'development' : 'production',

  /* 配置 source map 開發模式下使用 cheap-module-eval-source-map, 生成的 source map 能和源碼每行對應,方便打斷點調試 生產模式下使用 hidden-source-map, 生成獨立的 source map 文件,而且不在 js 文件中插入 source map 路徑,用於在 error report 工具中查看 (好比 Sentry) */
  devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map',

  // 配置頁面入口 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',

      /* 由於和 webpack 4 的兼容性問題,chunksSortMode 參數須要設置爲 none https://github.com/jantimon/html-webpack-plugin/issues/870 */
      chunksSortMode: 'none'
    })
  ]
}

/* 配置開發時用的服務器,讓你能夠用 http://127.0.0.1:8080/ 這樣的 url 打開頁面來調試 而且帶有熱更新的功能,打代碼時保存一下文件,瀏覽器會自動刷新。比 nginx 方便不少 若是是修改 css, 甚至不須要刷新頁面,直接生效。這讓像彈框這種須要點擊交互後纔會出來的東西調試起來方便不少。 由於 webpack-cli 沒法正確識別 serve 選項,使用 webpack-cli 執行打包時會報錯。 所以咱們在這裏判斷一下,僅當使用 webpack-serve 時插入 serve 選項。 issue:https://github.com/webpack-contrib/webpack-serve/issues/19 */
if (dev) {
  module.exports.serve = {
    // 配置監聽端口,默認值 8080
    port: 8080,

    // add: 用來給服務器的 koa 實例注入 middleware 增長功能
    add: app => {
      /* 配置 SPA 入口 SPA 的入口是一個統一的 html 文件,好比 http://localhost:8080/foo 咱們要返回給它 http://localhost:8080/index.html 這個文件 */
      app.use(convert(history()))
    }
  }
}
複製代碼

走一個

配置 OK 了,接下來咱們就運行一下吧。咱們先試一下開發環境用的 webpack-serve:

./node_modules/.bin/webpack-serve webpack.config.js
複製代碼

執行時須要指定配置文件。

上面的命令適用於 Mac / Linux 等 * nix 系統,也適用於 Windows 上的 PowerShell 和 bash/zsh 環境(Windows Subsystem for Linux, Git BashBabunMSYS2 等)。安利一下 Windows 同窗使用 Ubuntu on Windows,能夠避免不少跨平臺的問題,好比設置環境變量。

若是使用 Windows 的 cmd.exe,請執行:

node_modules\.bin\webpack-serve webpack.config.js
複製代碼

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

命令執行後,控制檯顯示:

「wdm」: Compiled successfully。
複製代碼

這就表明編譯成功了,咱們能夠在瀏覽器打開 http://localhost:8080/ 看看效果。若是有報錯,那多是什麼地方沒弄對?請本身仔細檢查一下~

咱們能夠隨意更改一下 src 目錄下的源代碼,保存後,瀏覽器裏的頁面應該很快會有相應變化。

要退出編譯,按 ctrl+c

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

./node_modules/.bin/webpack-cli
複製代碼

不須要指定配置文件,默認讀取 webpack.config.js

執行腳本的命令有點麻煩,所以,咱們能夠利用 npm,把命令寫在 package.json 中:

{
  "scripts": {
    "dev": "webpack-serve webpack.config.js",
    "build": "webpack-cli"
  }
}
複製代碼

package.json 中的 scripts 對象,能夠用來寫一些腳本命令,命令不須要前綴目錄 ./node_modules/.bin/,npm 會自動尋找該目錄下的命令。咱們能夠執行:

npm run dev
複製代碼

來啓動開發環境。

執行

npm run build
複製代碼

來打包生產環境的代碼。

進階配置

上面的項目雖然能夠跑起來了,但有幾個點咱們尚未考慮到:

  • 設置靜態資源的 url 路徑前綴
  • 各個頁面分開打包
  • 第三方庫和業務代碼分開打包
  • 輸出的 entry 文件加上 hash
  • 開發環境關閉 performance.hints
  • 配置 favicon
  • 開發環境容許其餘電腦訪問
  • 打包時自定義部分參數
  • webpack-serve 處理路徑帶後綴名的文件的特殊規則
  • 代碼中插入環境變量
  • 簡化 import 路徑
  • 優化 babel 編譯後的代碼性能
  • 使用 webpack 自帶的 ES6 模塊處理功能
  • 使用 autoprefixer 自動建立 css 的 vendor prefixes

那麼,讓咱們在上面的配置的基礎上繼續完善,下面的代碼咱們只寫出改變的部分。代碼在 examples/advanced 目錄。

設置靜態資源的 url 路徑前綴

如今咱們的資源文件的 url 直接在根目錄,好比 http://127.0.0.1:8080/index.js, 這樣作緩存控制和 CDN 不是很方便,所以咱們給資源文件的 url 加一個前綴,好比 http://127.0.0.1:8080/assets/index.js. 咱們來修改一下 webpack 配置:

{
  output: {
    publicPath: '/assets/'
  }
}
複製代碼

webpack-serve 也須要修改:

if (dev) {
  module.exports.serve = {
    port: 8080,
    host: '0.0.0.0',
    dev: {
      /* 指定 webpack-dev-middleware 的 publicpath 通常狀況下與 output.publicPath 保持一致(除非 output.publicPath 使用的是相對路徑) https://github.com/webpack/webpack-dev-middleware#publicpath */
      publicPath: '/assets/'
    },
    add: app => {
      app.use(convert(history({
        index: '/assets/' // index.html 文件在 /assets/ 路徑下
      })))
    }
  }
}
複製代碼

各個頁面分開打包

這樣瀏覽器只需加載當前頁面所需的代碼。

webpack 可使用異步加載文件的方式引用模塊,咱們使用 async/ awaitdynamic import 來實現:

src/router.js:

// 將 async/await 轉換成 ES5 代碼後須要這個運行時庫來支持
import 'regenerator-runtime/runtime'

const routes = {
  // import() 返回 promise
  '/foo': () => import('./views/foo'),
  '/bar.do': () => import('./views/bar.do')
}

class Router {
  // ...

  // 加載 path 路徑的頁面
  // 使用 async/await 語法
  async load(path) {
    // 首頁
    if (path === '/') path = '/foo'

    // 動態加載頁面
    const View = (await routes[path]()).default

    // 建立頁面實例
    const view = new View()

    // 調用頁面方法,把頁面加載到 document.body 中
    view.mount(document.body)
  }
}
複製代碼

這樣咱們就不須要在開頭把全部頁面文件都 import 進來了。

由於 import() 尚未正式進入標準,須要使用 babel-preset-stage-2 來支持:

npm install babel-preset-stage-2 --save-dev
複製代碼

package.json 改一下:

{
  "babel": {
    "presets": [
      "env",
      "stage-2"
    ]
  }
}
複製代碼

而後修改 webpack 配置:

{
  output: {
    /* 代碼中引用的文件(js、css、圖片等)會根據配置合併爲一個或多個包,咱們稱一個包爲 chunk。 每一個 chunk 包含多個 modules。不管是不是 js,webpack 都將引入的文件視爲一個 module。 chunkFilename 用來配置這個 chunk 輸出的文件名。 [chunkhash]:這個 chunk 的 hash 值,文件發生變化時該值也會變。使用 [chunkhash] 做爲文件名能夠防止瀏覽器讀取舊的緩存文件。 還有一個佔位符 [id],編譯時每一個 chunk 會有一個id。 咱們在這裏不使用它,由於這個 id 是個遞增的數字,增長或減小一個chunk,均可能致使其餘 chunk 的 id 發生改變,致使緩存失效。 */
    chunkFilename: '[chunkhash].js',
  }
}
複製代碼

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

這樣更新業務代碼時能夠藉助瀏覽器緩存,用戶不須要從新下載沒有發生變化的第三方庫。 Webpack 4 最大的改進即是自動拆分 chunk, 若是同時知足下列條件,chunk 就會被拆分:

  • 新的 chunk 能被複用,或者模塊是來自 node_modules 目錄
  • 新的 chunk 大於 30Kb(min+gz 壓縮前)
  • 按需加載 chunk 的併發請求數量小於等於 5 個
  • 頁面初始加載時的併發請求數量小於等於 3 個

通常狀況只需配置這幾個參數便可:

{
  plugins: [
    // ...

    /* 使用文件路徑的 hash 做爲 moduleId。 雖然咱們使用 [chunkhash] 做爲 chunk 的輸出名,但仍然不夠。 由於 chunk 內部的每一個 module 都有一個 id,webpack 默認使用遞增的數字做爲 moduleId。 若是引入了一個新文件或刪掉一個文件,可能會致使其餘文件的 moduleId 也發生改變, 那麼受影響的 module 所在的 chunk 的 [chunkhash] 就會發生改變,致使緩存失效。 所以使用文件路徑的 hash 做爲 moduleId 來避免這個問題。 */
    new webpack.HashedModuleIdsPlugin()
  ],

  optimization: {
    /* 上面提到 chunkFilename 指定了 chunk 打包輸出的名字,那麼文件名存在哪裏了呢? 它就存在引用它的文件中。這意味着一個 chunk 文件名發生改變,會致使引用這個 chunk 文件也發生改變。 runtimeChunk 設置爲 true, webpack 就會把 chunk 文件名所有存到一個單獨的 chunk 中, 這樣更新一個文件只會影響到它所在的 chunk 和 runtimeChunk,避免了引用這個 chunk 的文件也發生改變。 */
    runtimeChunk: true,

    splitChunks: {
      /* 默認 entry 的 chunk 不會被拆分 由於咱們使用了 html-webpack-plugin 來動態插入 <script> 標籤,entry 被拆成多個 chunk 也能自動被插入到 html 中, 因此咱們能夠配置成 all, 把 entry chunk 也拆分了 */
      chunks: 'all'
    }
  }
}
複製代碼

webpack 4 支持更多的手動優化,詳見: https://gist.github.com/sokra/1522d586b8e5c0f5072d7565c2bee693

但正如 webpack 文檔中所說,默認配置已經足夠優化,在沒有測試的狀況下不要盲目手動優化。

輸出的 entry 文件加上 hash

上面咱們提到了 chunkFilename 使用 [chunkhash] 防止瀏覽器讀取錯誤緩存,那麼 entry 一樣須要加上 hash。 但使用 webpack-serve 啓動開發環境時,entry 文件是沒有 [chunkhash] 的,用了會報錯。 所以咱們只在執行 webpack-cli 時使用 [chunkhash]

{
  output: {
    filename: dev ? '[name].js' : '[chunkhash].js'
  }
}
複製代碼

這裏咱們使用了 [name] 佔位符。解釋它以前咱們先了解一下 entry 的完整定義:

{
  entry: {
    NAME: [FILE1, FILE2, ...]
  }
}
複製代碼

咱們能夠定義多個 entry 文件,好比你的項目有多個 html 入口文件,每一個 html 對應一個或多個 entry 文件。 而後每一個 entry 能夠定義由多個 module 組成,這些 module 會依次執行。 在 webpack 4 以前,這是頗有用的功能,好比以前提到的第三方庫和業務代碼分開打包,在之前,咱們須要這麼配置:

{
  entry {
    main: './src/index.js',
    vendor: ['jquery', 'lodash']
  }
}
複製代碼

entry 引用文件的規則和 import 是同樣的,會尋找 node_modules 裏的包。而後結合 CommonsChunkPlugin 把 vendor 定義的 module 從業務代碼分離出來打包成一個單獨的 chunk。 若是 entry 是一個 module,咱們能夠不使用數組的形式。

在 simple 項目中,咱們配置了 entry: './src/index.js',這是最簡單的形式,轉換成完整的寫法就是:

{
  entry: {
    main: ['./src/index.js']
  }
}
複製代碼

webpack 會給這個 entry 指定名字爲 main

看到這應該知道 [name] 的意思了吧?它就是 entry 的名字。

有人可能注意到官網文檔中還有一個 [hash] 佔位符,這個 hash 是整個編譯過程產生的一個總的 hash 值,而不是單個文件的 hash 值,項目中任何一個文件的改動,都會形成這個 hash 值的改變。[hash] 佔位符是始終存在的,但咱們不但願修改一個文件致使全部輸出的文件 hash 都改變,這樣就沒法利用瀏覽器緩存了。所以這個 [hash] 意義不大。

開發環境關閉 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: 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:指定文件輸出名 [hash] 爲源文件的hash值,[ext] 爲後綴。 */
              name: '[hash].[ext]'
            }
          }
        ]
      },

      // 圖片文件的加載配置增長一個 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, 很是大。

開發環境容許其餘電腦訪問

const internalIp = require('internal-ip')

module.exports.serve = {
  host: '0.0.0.0',
  hot: {
    host: {
      client: internalIp.v4.sync(),
      server: '0.0.0.0'
    }
  },
  
  // ...
}
複製代碼

打包時自定義部分參數

在多人開發時,每一個人可能須要有本身的配置,好比說 webpack-serve 監聽的端口號,若是寫死在 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/',

  serve: {
    port: 8090
  }
}
複製代碼
  • local.js: 我的本地環境,在 dev.js 基礎上修改部分參數。
const config = require('./dev')
config.serve.port = 8070
module.exports = config
複製代碼

package.json 修改 scripts:

{
  "scripts": {
    "local": "npm run webpack-serve --config=local",
    "dev": "npm run webpack-serve --config=dev",
    "webpack-serve": "webpack-serve webpack.config.js",
    "build": "webpack-cli"
  }
}
複製代碼

webpack 配置修改:

// ...
const url = require('url')

const config = require('./config/' + (process.env.npm_config_config || 'default'))

module.exports = {
  // ...
  
  output: {
    // ...
    publicPath: config.publicPath
  }
  
  // ...
}

if (dev) {
  module.exports.serve = {
    host: '0.0.0.0',
    port: config.serve.port,
    dev: {
      publicPath: config.publicPath
    },
    add: app => {
      app.use(convert(history({
        index: url.parse(config.publicPath).pathname
      })))
    }
  }
}
複製代碼

這裏的關鍵是 npm run 傳進來的自定義參數能夠經過 process.env.npm_config_* 得到。參數中若是有 - 會被轉成 _

還有一點,咱們不須要把本身我的用的配置文件提交到 git,因此咱們在 .gitignore 中加入:

config/*
!config/default.js
!config/dev.js
複製代碼

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

可能有同窗注意到了 webpack-cli 能夠經過 --env 的方式從命令行傳參給腳本,遺憾的是 webpack-cli 不支持

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

當處理帶後綴名的請求時,好比 http://localhost:8080/bar.do ,connect-history-api-fallback 會認爲它應該是一個實際存在的文件,就算找不到該文件,也不會 fallback 到 index.html,而是返回 404。但在 SPA 應用中這不是咱們但願的。

幸虧有一個配置選項 disableDotRule: true 能夠禁用這個規則,使帶後綴的文件當不存在時也能 fallback 到 index.html

module.exports.serve = {
  // ...
  add: app => {
    app.use(convert(history({
      // ...
      disableDotRule: true,
      htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'] // 須要配合 disableDotRule 一塊兒使用
    })))
  }
}
複製代碼

代碼中插入環境變量

在業務代碼中,有些變量在開發環境和生產環境是不一樣的,好比域名、後臺 API 地址等。還有開發環境可能須要打印調試信息等。

咱們可使用 DefinePlugin 插件在打包時往代碼中插入須要的環境變量。

// ...
const pkgInfo = require('./package.json')

module.exports = {
  // ...
  plugins: [
    new webpack.DefinePlugin({
      DEBUG: 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.versionJSON.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 自帶的 ES6 模塊處理功能

咱們目前的配置,babel 會把 ES6 模塊定義轉爲 CommonJS 定義,但 webpack 本身能夠處理 importexport, 並且 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')()
  ]
}
複製代碼

使用 webpack 打包多頁面應用(Multiple-Page Application)

多頁面網站一樣能夠用 webpack 來打包,以便使用 npm 包,import()code splitting 等好處。

MPA 意味着並沒不是一個單一的 html 入口和 js 入口,而是每一個頁面對應一個 html 和多個 js。那麼咱們能夠把項目結構設計爲:

├── dist
├── package.json
├── node_modules
├── src
│   ├── components
│   ├── shared
|   ├── favicon.png
│   └── pages                 頁面放這裏
|       ├── foo               編譯後生成 http://localhost:8080/foo.html
|       |    ├── index.html
|       |    ├── index.js
|       |    ├── style.css
|       |    └── pic.png
|       └── bar                        http://localhost:8080/bar.html
|           ├── index.html
|           ├── index.js
|           ├── style.css
|           └── baz                    http://localhost:8080/bar/baz.html
|               ├── index.html
|               ├── index.js
|               └── style.css
└── webpack.config.js
複製代碼

這裏每一個頁面的 index.html 是個完整的從 <!DOCTYPE html> 開頭到 </html> 結束的頁面,這些文件都要用 html-webpack-plugin 處理。index.js 是每一個頁面的業務邏輯,做爲每一個頁面的入口 js 配置到 entry 中。這裏咱們須要用 glob 庫來把這些文件都篩選出來批量操做。爲了使用 webpack 4 的 optimization.splitChunksoptimization.runtimeChunk 功能,我寫了 html-webpack-include-sibling-chunks-plugin 插件來配合使用。還要裝幾個插件把 css 壓縮並放到 <head> 中。

npm install glob html-webpack-include-sibling-chunks-plugin uglifyjs-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin --save-dev
複製代碼

webpack.config.js 修改的地方:

// ...
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackIncludeSiblingChunksPlugin = require('html-webpack-include-sibling-chunks-plugin')
const glob = require('glob')

const dev = Boolean(process.env.WEBPACK_SERVE)
const config = require('./config/' + (process.env.npm_config_config || 'default'))

const entries = glob.sync('./src/**/index.js')
const entry = {}
const htmlPlugins = []
for (const path of entries) {
  const template = path.replace('index.js', 'index.html')
  const chunkName = path.slice('./src/pages/'.length, -'/index.js'.length)
  entry[chunkName] = dev ? [path, template] : path
  htmlPlugins.push(new HtmlWebpackPlugin({
    template,
    filename: chunkName + '.html',
    chunksSortMode: 'none',
    chunks: [chunkName]
  }))
}

module.exports = {
  entry,

  output: {
    path: resolve(__dirname, 'dist'),
    // 咱們不定義 publicPath,不然訪問 html 時須要帶上 publicPath 前綴
    filename: dev ? '[name].js' : '[chunkhash].js',
    chunkFilename: '[chunkhash].js'
  },

  optimization: {
    runtimeChunk: true,
    splitChunks: {
      chunks: 'all'
    },
    minimizer: dev ? [] : [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: true
      }),
      new OptimizeCSSAssetsPlugin()
    ]
  },

  module: {
    rules: [
      // ...
      
      {
        test: /\.css$/,
        use: [dev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      },
      
      // ...
    ]
  },

  plugins: [
    // ...
    
    /* 這裏不使用 [chunkhash] 由於從同一個 chunk 抽離出來的 css 共享同一個 [chunkhash] [contenthash] 你能夠簡單理解爲 moduleId + content 生成的 hash 所以一個 chunk 中的多個 module 有本身的 [contenthash] */
    new MiniCssExtractPlugin({
      filename: '[contenthash].css',
      chunkFilename: '[contenthash].css'
    }),

    // 必須放在html-webpack-plugin前面
    new HtmlWebpackIncludeSiblingChunksPlugin(),

    ...htmlPlugins
  ],

  // ...
}
複製代碼

entryhtmlPlugins 會經過遍歷 pages 目錄生成,好比:

entry:

{
  'bar/baz': './src/pages/bar/baz/index.js',
  bar: './src/pages/bar/index.js',
  foo: './src/pages/foo/index.js'
}
複製代碼

在開發環境中,爲了可以修改 html 文件後網頁可以自動刷新,咱們還須要把 html 文件也加入 entry 中,好比:

{
  foo: ['./src/pages/foo/index.js', './src/pages/foo/index.html']
}
複製代碼

這樣,當 foo 頁面的 index.js 或 index.html 文件改動時,都會觸發瀏覽器刷新該頁面。雖然把 html 加入 entry 很奇怪,但放心,不會致使錯誤。記得不要在生產環境這麼作,否則致使 chunk 文件包含了無用的 html 片斷。

htmlPlugins:

[
  new HtmlWebpackPlugin({
    template: './src/pages/bar/baz/index.html',
    filename: 'bar/baz.html',
    chunksSortMode: 'none',
    chunks: ['bar/baz']
  },

  new HtmlWebpackPlugin({
    template: './src/pages/bar/index.html',
    filename: 'bar.html',
    chunksSortMode: 'none',
    chunks: ['bar']
  },

  new HtmlWebpackPlugin({
    template: './src/pages/foo/index.html',
    filename: 'foo.html',
    chunksSortMode: 'none',
    chunks: ['foo']
  }
]
複製代碼

代碼在 examples/mpa 目錄。

總結

經過這篇文章,我想你們應該學會了 webpack 的正確打開姿式。雖然我沒有說起如何用 webpack 來編譯 Reactvue.js, 但你們能夠想到,無非是安裝一些 loader 和 plugin 來處理 jsxvue 格式的文件,那時難度就不在於 webpack 了,而是代碼架構組織的問題了。具體的你們本身去摸索一下。

版權許可

知識共享許可協議
本做品採用 知識共享署名 - 非商業性使用 4.0 國際許可協議 進行許可。

相關文章
相關標籤/搜索