Webpack 之常見見招拆招

lucius0發表於2019-03-24

前端的發展,大致的發展路線可以看黃玄的JavaScript 模組化七日談。從最初的全域性汙染式的注入到ES6模組化,打包工具的不斷迭代替換。主要的原因都是因為前端發展越來越複雜龐大所導致。

本篇文章主要是來談談 webpack 在我們平時的開發工作中起到什麼作用,以及我們該如何靈活的應用它來成為我們的利器。大多數情況下我不會說明怎麼使用,因為這樣會導致篇幅太多不容易閱覽,所以具體的配置還是得自己閱覽官方文件。

背景

如今的前端百花齊放,不再像以前那樣直接操作 DOM 然後壓縮扔到伺服器上去。看似沒啥問題,但是不斷的重複勞動力導致開發效率低下。

React、Vue、Angular2。Typescript、Flow、CoffeeScript、ES6。SASS、LESS。分別為前端框架JS超集/JS新標準CSS前處理器。以上的這些無法直接的在瀏覽器上跑,都需要轉換為 ES5/CSS 才可以。(注:ES6 可以在支援 ES6 語法的瀏覽器上執行,如Chrome)

構建工具

無論什麼構建工具,它們做的內容都是大同小異:程式碼轉換、檔案優化、程式碼分割、模組合併、自動重新整理、程式碼校驗、自動分佈。歷史上的構建工具都是基於Node.js開發的。有GruntGulpFis3RollupBrowserify 等等。更具體可以參考前端構建:3類13種熱門工具的選型參考

至於它們之間優劣性以及為什麼選擇webpack在網上有很多相關的資料可以參考,在這裡就不再贅述了。

開始

基礎配置

/// webpack.config.js
const path = require('path');
module.exports = {
  // js 執行入口檔案
  entry: './main.js',
  output: {
    // 將所有依賴的模組合併輸出到一個 bundle.js 檔案
    filename: 'bundle.js',
    // 將輸出檔案都放到 dist 目錄下
    path: path.resolve(__dirname, './dist'),
  }
};
複製程式碼

執行 webpack --config webpack.config.js,則會在dist資料夾生成bundle.js檔案,這就是最基本的 webpack 配置。更多配置檢視官網webpack

Loader

Loader 主要是用於將模組程式碼轉換為可在瀏覽器執行的程式碼。可以理解為翻譯機。如將 Less 轉換為 CSS,Typescript 轉換為 Javascript 等。

Plugin

Plugin 主要是擴充套件 webpack 的功能,增強 webpack 的靈活性。如extract-text-webpack-plugin,可以將包中的文字提取到單獨的檔案中,從bundle.js提取 css 到單獨的檔案出來等。

DevServer

webpack-dev-server,可以幫我們解決上面沒提到但是在開發中遇到的痛點。

  • 提供 HTTP 服務而不是使用本地檔案預覽;
  • 監聽檔案的變化並自動重新整理網頁,做到實時預覽:
  • 支援 Source Map,以方便除錯。

見招拆招

交待完 webpack 的基礎也是重要的功能之後,我們從工作中開始,見招拆招,也就是說我們平時需要做什麼,webpack 能幫我們做什麼。

見招 - ES6

ES6的出現引入了新的語法,提高了開發效率。但是目前仍有很多瀏覽器對其標準支援不全。所以我們需要將其轉換為 ES5 以及對新 API 打 polyfill。才能正常的使用。

拆招 - Babel

Babel 是 JS 編譯器,主要功能就是將 ES6 轉為 ES5,詳看 What is Babel? · Babel。在專案根目錄建立.babelrc

{
  // plugins 告訴 Babel 要使用哪些外掛,這些外掛可以控制如何轉換程式碼 。 
  "plugins": [
    [
      "transform-runtime",
      {
        "polyfill": false
      }
    ],
  ],
  // presets屬性告訴 Babel要轉換的原始碼使用了哪些新的語法特性,一個 Presets對一組新語法的特性提供了支援,多個 Presets 可以疊加。
  "presets": [
    [
      // 除此之外,還有往上的標準如 ES2016等 以及 Env,其中 Env 包含ES 標準的最新特性
      "es2015", 
      {
        "modules": false
      }
    ],
    // 社群提出卻還未入標準的新特性,有stage0 - stage4,被納入的可能性依次增加
    "stage-2",
    // 特定應用場景語法特性
    "react"
  ]
}
複製程式碼

在瞭解 Babel 後,下一步就是配置 Webpack。

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['babel-loader'],
      }
    ]
  }
}
複製程式碼

見招 - Typescript

Typescript 是 Javascript 型別的超集,它可以編譯成純 Javascript。TypeScript—JavaScript的超集

拆招 - Typescript

Typescript 官方提供了能將 Typescript 轉換成 JavaScript 的編譯器。執行安裝npm i -g typescript,然後在根目錄新建配置編譯選項tsconfig.json

{
  "compilerOptions": {
    "module": "commonjs", // 編譯出的程式碼採用的模組規範
    "target": "es5", // 編譯出的程式碼採用 ES 的哪個版本
    "sourceMap": true // 輸出 Source Map 以方便除錯
  },
  "exclude": [
    "node_modules"
  ]
}
複製程式碼

配置完tsconfig.json,我們就可以配置 Webpack。

module.exports = {
  ...
  resolve: {
    extensions: ['.ts']
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader'
      }
    ]
  },
  devtool: 'source-map',
}
複製程式碼

見招 - SASS/LESS

SASS 和 LESS 都是 CSS 的前處理器,它們都是可以方便的管理程式碼,抽離樣式公共部分,通過邏輯來書寫更加靈活的樣式程式碼,從而提高效率。關於他們更多的資訊可以Sass: Syntactically Awesome Style SheetsGetting started | Less.js去檢視。

拆招 - SASS-LOADER / LESS-LOADER

安裝完sass-loaderless-loader之後,直接配置Webpack。

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss/, 或 /\.less/
        use: ['style-loader', 'css-loader', 'sass-loader'] 或 ['style-loader', 'css-loader', 'less-loader']
      }
    ]
  }
}
複製程式碼

其處理流程如下:

  1. 通過 loader 將 sass/less 檔案轉換為 css 程式碼,再將其交給 css-loader 處理;
  2. css-loader 會找出 css 程式碼中匯入語句如@importurl(),同時支援 css modules、壓縮 css 等功能,然後交給 style-loader 處理;
  3. style-loader 會講 css 轉換為字串注入 js 程式碼中。

見招 - React

React 中主要是因為其程式碼中使用了 JSX 和 Class 特性,因此我們需要將其轉換為瀏覽器能識別的 JavaScript 程式碼。

拆招 - Babel

我們需要依賴 babel-preset-react來完成語法上的轉換。所以我們還需要配置.babelrc,加入 React Preset。

"presets": [
  "react"
]
複製程式碼

其實這樣就可以了。但是我們有時候會使用 React + Typescript 組合來提高我們開發效率。在上面我們提到 Typescript 的開發,我們這次來修改其配置檔案tsconfig.json

{
  "compilerOptions": {
    "jsx": "react" // 開啟 JSX,支援 React
  }
}
複製程式碼

至於 Webpack 的配置,其實不用太多的改動,只需要支援下/\.tsx/字尾檔案就行。


見招 - Vue

Vue 沒有 React 那樣會內建專屬語法,但它和 React 一樣,都推崇元件化和由資料驅動的思想。話不多說,直接拆招。

拆招 - vue-loader

解析 vue 主要需要 vue-loadervue-template-compilervue-loader 主要事用來解析和轉換.vue檔案,提取出其中的邏輯程式碼、樣式程式碼以及 html 模板 template,再分別將它們交給對應的 Loader 去處理,如 template 則就是由 vue-template-compiler 去處理的。

/// webpack
module: {
  rules: [
    {
      test: /\.vue$/,
      use: ['vue-loader'],
    },
  ]
}
複製程式碼

同樣,假如我們需要 Vue + Typescript 組合呢?從 Vue 2.5 開始,就提供了對TS 的支援。配置 tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "es2015", // 用於使 Tree Shaking 優化生效
    "moduleResolution": "node",
  }
}
複製程式碼

除此之外還需要在宣告檔案 vue-shims.d.ts 定義 vue 型別:

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
複製程式碼

修改 webpack 配置檔案。

module: {
  rules: [
    {
      test: /\.ts$/,
      loader: 'ts-loader',
      exclude: /node_modules/,
      options: {
        appendTsSuffixTo: [/\.vue$/],
      }
    }
  ]
}
複製程式碼

到這裡為止,我們就可以通過 webpack 來進行我們的開發工作了。但是實際專案中有很多的痛點,例如程式碼檢查,熱更新,CDN釋出等。我們不可能每次都手動的來配置,這樣太繁瑣太浪費時間了。接下來我們通過 webpack 來優化我們的開發體驗。

見招 - 監聽更新

當我們在開發階段,肯定會在期間不斷地修改原始碼。但是我們不可能每一次修改就手動編譯然後重新整理頁面,這明顯浪費我們的時間跟精力。於是就有了自動化監聽更新,原理就是監聽本地原始碼包括樣式,一旦發生變化時,就會自動構建然後重新整理瀏覽器。

拆招 - webpack

通過 webpack 開啟監聽模式,一般有兩種方式:

  • 配置webpack.config.js設定watch: true;
  • 執行 webpack 時,可以帶上引數,如 webpack --watch

它的工作原理就是通過 aggregateTimeout 設定等待時間,到該時間時就會去檢查編輯後的檔案的最後編輯時間從而達到監聽的目的。

見招 - 自動重新整理瀏覽器

在上面我們提到了監聽更新,但是更新完後瀏覽器應該有所表現,不然手動重新整理瀏覽器的行為也是蠻愚蠢的。所以當我們監聽到的檔案一旦發生了修改,瀏覽器就要主動去重新整理瀏覽器。

拆招 - webpack-dev-server

我們使用 webpack-dev-server 模組啟動 webpack 模組時,webpack 模組的監聽模式預設會被開啟。webpack 模組會在檔案發生變化時通知 webpack-dev-server 模組。

通過 webpack-dev-server 啟動時,有以下兩種方式可以實現自動重新整理:

  • webpack-dev-server(預設):向要開發的網頁注入代理客戶端程式碼,通過代理客戶端去重新整理整個頁面;
  • webpack-dev-server --inline false:將要開發的網頁裝進一個 iframe 中,通過重新整理 iframe 去看到最新效果。

見招 - 模組熱替換

在上面提到的更新後重新整理是會重新整理整個頁面,這樣的體驗不好。所以 webpack-dev-server 還支援模組熱替換,就是在不重新整理整個頁面的情況下只替換修改的檔案,這樣不但快捷,而且資料也不會丟失。

拆招 - webpack-dev-server

實現模組熱替換也有兩種方式:

  • webpack-dev-server-hot
  • HotModuleReplacementPlugin(推薦)

見招 - 檢查程式碼

當我們的專案越來越龐大時,特別是多人協作開發,會導致一個問題就是程式碼會有多種風格導致可讀性下降。因此我們需要在提交之前執行自動化檢查,讓專案成員強制遵守統一的程式碼風格,同時也可以分析出潛在的問題。

拆招 - **lint 及 husky

**lint 這裡指的是針對不同的語言使用不同的 lint 檢查工具。

  • eslint:用來檢查 JavaScript,配置 .eslintrc 來新增規則,再結合 eslint-loader 就可以通過 webpack 來執行程式碼檢查;
  • tslint:用來檢查 TypeScript,配置 tslint.json 來新增規則,再結合 tslint-loader 就可以通過 webpack 來執行程式碼檢查;
  • stylelint:用來檢查樣式檔案,如 SCSS、Less等,配置.stylelintrc 來新增規則,再結合 stylelint-webpack-plugin 就可以通過 webpack 來執行程式碼檢查;

上面通過整合到 webpack 存在個問題,就是在開發過程中構建速度會變慢很多。所以我們建議在提交的時候通過 Git Hook 來執行我們的程式碼檢查,如huskyhusky 會通過 Npm Script Hook 自動配置好 Git Hook,然後我們只需要在 package.json 新增 script 指令碼,其中 precommitprepush 只需要其中一個就好了,配置如下:

{
  "scripts": {
    // 在執行 git commit 前會執行的指令碼 
    "precommit": "npm run lint",
    //在執行 git push 前會執行的指令碼 
    "prepush": "lint",
    // 呼叫 eslint、stylelint 等工具檢查程式碼
    "lint": "eslint && stylelint"
  }
}
複製程式碼

其他

除了上面這些,我們可能還需要需要以下的配置: 載入圖片 - file-loader:將 JavaScript 和 CSS 中匯入圖片的路徑替換成正確的路徑,並同時將其輸出到對應位置; - url-loader:將檔案的內容經過 base 64 編碼後注入JavaScript 或 CSS 中。

載入SVG - raw-loader:可以將文字檔案內容讀取出來,注入到 JavaScript 或 CSS 中。 - svg-inline-loader:跟 raw-loader 一樣,但是增加了對 svg 壓縮的功能。

優化

區分環境

區分環境的好處我就不多解釋了,這裡主要是用到了 webpack自帶(當程式碼出現process時,webpack會將其模組打包進去)的 process 模組。使用方法也很簡單 process.env.NODE_ENV 就行了。

壓縮程式碼

上線後我們除了GZIP對其檔案進行壓縮,我們還需要對檔案本身進行壓縮排而減少網路傳輸流量和提高網頁載入速度。這裡的檔案壓縮就是用到了UglifyJsPlugin 外掛。詳情配置可以檢視官方,需要注意的是,記得區分環境如 source-map 等。

壓縮 CSS

壓縮 CSS,用一款基於 PostCSS 的壓縮工具 cssnanocss-loader已經內建其模組了,只需要開啟 css-loaderminimize 選項即可。

CDN加速

這裡不是說要通過前端來做 CDN 加速的事,而是當我們上傳靜態資源時,靜態資源需要通過 CDN 服務提供的 URL 地址去訪問,而我們要做的,就是在生成頁面時,將我們的靜態資源替換為CDN的地址。 我們所說的靜態資源主要分為兩種,入口 HTML 檔案以及 JS、CSS、圖片等靜態資源。前者的處理方法是存在伺服器而非CDN,並且伺服器不對其做快取處理,這樣就可以保證每次請求的入口檔案都是最新的;後者則會上傳 CDN 服務上,做快取處理。 簡單的來說就是入口 HTML 檔案是在每一次請求都是最新的,那麼其請求的 靜態資源的 Hash 值也有可能會更新,那麼只要發生變換,則去請求新的靜態資源就行了。

那麼問題來了,怎麼做才能每次打包新的 HTML 檔案時,其請求的靜態資源的也會跟隨變化呢?webpack 及其外掛提供了其功能,分別為:

  • output.publicPath 中設定 Javascript 的地址;
  • css-loader.publicPath 中設定被 CSS 匯入的資源的地址;
  • Webplugin.stylePublicPath 中設定 CSS 檔案的地址。

提取公共程式碼

webpack 有個專門用於提取多個 Chunk 中公共部分的外掛 CommonsChunkPlugin,用法如下:

const ComrnonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({
  // 從 a、b chunk 提取共同的程式碼模組
  chunks: ['a', 'b'],
  // 將其封裝到 common 新 chunk
  name: 'common',
}) 

複製程式碼

按需載入

在這裡只針對 Vue、React 來說。目前比較流行的做法就是在路由上做處理。

Vue vue-router 通過 vue 的動態元件 & 非同步元件 — Vue.js,就可以實現按需載入了,如:

resolve => require(['./Test'], resolve)
複製程式碼

React react-router 還可以配合 react-loadable,實現路由按需載入,如:

function asyncLoad (loader) {
  return Loadable({ loader });
}
asyncLoad(() => import('./Test'));
複製程式碼

分析報告

webpack 自帶分析功能webpack --profile --json > stats.json,也可以安裝視覺化分析工具webpack-bundle-analyzer更加直觀的觀察專案的情況。

最後

本篇的大多內容是閱覽完《深入淺出 webpack》後的總結。之所以想總結,是因為 webpack 的配置給人的感覺就是配置麻煩很瑣碎。因此就有了這個想法,對知識點的查漏補缺,同時也是一次對知識點的梳理。這篇文章目前主要梳理常用的一些配置、外掛以及優化。當然這也只是冰山一角,更多的還需要自己去查閱官方文件,不同版本也會有不同的差異性。之後遇到問題,我也會持續記錄下來。

相關文章