從今天開始,學習Webpack,減少對腳手架的依賴(下)

汪圖南發表於2019-05-29

問:這篇文章適合哪些人?
答:適合沒接觸過Webpack或者瞭解不全面的人。

問:這篇文章的目錄怎麼安排的?
答:先介紹背景,由背景引入Webpack的概念,進一步介紹Webpack基礎、核心和一些常用配置案例、優化手段,Webpack的plugin和loader確實非常多,短短2w多字還只是覆蓋其中一小部分。

問:這篇文章的出處?
答:此篇文章知識來自付費視訊(連結在文章末尾),文章由自己獨立撰寫,已獲得講師授權並首發於掘金

上一篇:從今天開始,學習Webpack,減少對腳手架的依賴(上)

如果你覺得寫的不錯,請給我點一個star,原部落格地址:原文地址

PWA配置

PWA全稱Progressive Web Application(漸進式應用框架),它能讓我們主動快取檔案,這樣使用者離線後依然能夠使用我們快取的檔案開啟網頁,而不至於讓頁面掛掉,實現這種技術需要安裝workbox-webpack-plugin外掛。

如果你的谷歌瀏覽器還沒有開啟支援PWA,請開啟它再進行下面的測試。

安裝外掛

$ npm install workbox-webpack-plugin -D
複製程式碼

webpack.config.js檔案配置

// PWA只有線上上環境才有效,所以需要在webpack.prod.js檔案中進行配置
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const prodConfig = {
  // 其它配置
  plugins: [
    new MiniCssExtractPlugin({}),
    new WorkboxWebpackPlugin.GenerateSW({
      clientsClaim: true,
      skipWaiting: true
    })
  ]
}
module.exports = merge(commonConfig, prodConfig);
複製程式碼

以上配置完畢後,讓我們使用npm run build打包看一看生成了哪些檔案,dist目錄的打包結果如下:

|-- dist
|   |-- index.html
|   |-- main.f28cbac9bec3756acdbe.js
|   |-- main.f28cbac9bec3756acdbe.js.map
|   |-- precache-manifest.ea54096f38009609a46058419fc7009b.js
|   |-- service-worker.js
複製程式碼

我們可以程式碼塊高亮的部分,多出來了precache-manifest.xxxxx.js檔案和service-worker.js,就是這兩個檔案能讓我們實現PWA。

改寫index.js

需要判斷瀏覽器是否支援PWA,支援的時候我們才進行註冊,註冊的.js檔案為我們打包後的service-worker.js檔案。

console.log('hello,world');
if('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js').then((register) => {
    console.log('註冊成功');
  }).catch(error => {
    console.log('註冊失敗');
  })
}
複製程式碼

PWA實際效果

npm run dev後,我們利用webpack-dev-server啟動了一個小型的伺服器,然後我們停掉這個伺服器,重新整理頁面,PWA的實際結果如下圖所示

從今天開始,學習Webpack,減少對腳手架的依賴(下)

WebpackDevServer請求轉發

在這一小節中,我們要學到的技能有:

  • 如何進行介面代理配置
  • 如何使用介面路徑重寫
  • 其他常見配置的介紹

假設我們現在有這樣一個需求:我有一個URL地址(http://www.dell-lee.com/react/api/header.json),我希望我請求的時候,請求的地址是/react/api/header.json,能有一個什麼東西能自動幫我把請求轉發到http://www.dell-lee.com域名下,那麼這個問題該如何解決呢?可以使用 Webpack 的webpack-dev-server這個外掛來解決,其中需要配置proxy屬性。

如何進行介面代理配置

既然我們要做請求,那麼安裝axios來發請求再合適不過了,使用如下命令安裝axios:

$ npm install axios --save-dev
複製程式碼

因為我們的請求代理只能在開發環境下使用,線上的生產環境,需要走其他的代理配置,所以我們需要在webpack.dev.js中進行代理配置

const devConfig = {
  // 其它配置
  devServer: {
    contentBase: './dist',
    open: false,
    port: 3000,
    hot: true,
    hotOnly: true,
    proxy: {
      '/react/api': {
        target: 'http://www.dell-lee.com'
      }
    }
  }
}
複製程式碼

以上配置完畢後,我們在index.js檔案中引入axios模組,再做請求轉發。

import axios from 'axios';

axios.get('/react/api/header.json').then((res) => {
  let {data,status} = res;
  console.log(data);
})
複製程式碼

使用npm run dev後, 我們可以在瀏覽器中看到,我們已經成功請求到了我們的資料。

從今天開始,學習Webpack,減少對腳手架的依賴(下)

如何使用介面路徑重寫

現在依然假設有這樣一個場景:http://www.dell-lee.com/react/api/header.json這個後端介面還沒有開發完畢,但後端告訴我們可以先使用http://www.dell-lee.com/react/api/demo.json 這個測試介面,等介面開發完畢後,我們再改回來。解決這個問題最佳辦法是,程式碼中的地址不能變動,我們只在proxy代理中處理即可,使用pathRewrite屬性進行配置。

const devConfig = {
  // 其它配置
  devServer: {
    contentBase: './dist',
    open: false,
    port: 3000,
    hot: true,
    hotOnly: true,
    proxy: {
      '/react/api': {
        target: 'http://www.dell-lee.com',
        pathRewrite: {
          'header.json': 'demo.json'
        }
      }
    }
  }
}
複製程式碼

同樣,我們打包後在瀏覽器中可以看到,我們的測試介面的資料已經成功拿到了。

從今天開始,學習Webpack,減少對腳手架的依賴(下)

其他常見配置的含義

轉發到https: 一般情況下,不接受執行在https上,如果要轉發到https上,可以使用如下配置

module.exports = {
  //其它配置
  devServer: {
    proxy: {
      '/react/api': {
        target: 'https://www.dell-lee.com',
        secure: false
      }
    }
  }
}
複製程式碼

跨域: 有時候,在請求的過程中,由於同源策略的影響,存在跨域問題,我們需要處理這種情況,可以如下進行配置。

module.exports = {
  //其它配置
  devServer: {
    proxy: {
      '/react/api': {
        target: 'https://www.dell-lee.com',
        changeOrigin: true,
      }
    }
  }
}
複製程式碼

代理多個路徑到同一個target: 代理多個路徑到同一個target,可以如下進行配置

module.exports = {
  //其它配置
  devServer: {
    proxy: [{
      context: ['/vue/api', '/react/api'],
      target: 'http://www.dell-lee.com'
    }]
  }
}
複製程式碼

多頁打包

現在流行的前端框架都推行單頁引用(SPA),但有時候我們不得不相容一些老的專案,他們是多頁的,那麼如何進行多頁打包配置呢? 現在我們來思考一個問題:多頁運用,即 多個入口檔案+多個對應的html檔案 ,那麼我們就可以配置 多個入口+配置多個html-webpack-plugin 來進行。

場景:假設現在我們有這樣三個頁面:index.html, list.html, detail.html,我們需要配置三個入口檔案,新建三個.js檔案。

webpack.common.js中配置多個entry並使用html-webpack-plugin來生成對應的多個.html頁面。 HtmlWebpackPlugin引數說明

  • template:代表以哪個HTML頁面為模板
  • filename:代表生成頁面的檔名
  • chunks:代表需要引用打包後的哪些.js檔案
module.exports = {
  // 其它配置
  entry: {
    index: './src/index.js',
    list: './src/list.js',
    detail: './src/detail.js',
  },
  plugins: [
    new htmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new htmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'list.html',
      chunks: ['list']
    }),
    new htmlWebpackPlugin({
      template: 'src/index.html',
      filename: 'detail.html',
      chunks: ['detail']
    }),
    new cleanWebpackPlugin()
  ]
}
複製程式碼

src目錄下新建三個.js檔案,名字分別是:index.jslist.jsdetail.js,它們的程式碼如下:

// index.js程式碼
document.getElementById('root').innerHTML = 'this is index page!'

// list.js程式碼
document.getElementById('root').innerHTML = 'this is list page!'

// detail.js程式碼
document.getElementById('root').innerHTML = 'this is detail page!'
複製程式碼

執行npm run build進行打包:

$ npm run build
複製程式碼

打包後的dist目錄:

|-- dist
|   |-- detail.dae2986ea47c6eceecd6.js
|   |-- detail.dae2986ea47c6eceecd6.js.map
|   |-- detail.html
|   |-- index.ca8e3d1b5e23e645f832.js
|   |-- index.ca8e3d1b5e23e645f832.js.map
|   |-- index.html
|   |-- list.5f40def0946028db30ed.js
|   |-- list.5f40def0946028db30ed.js.map
|   |-- list.html
複製程式碼

隨機選擇list.html在瀏覽器中執行,結果如下:

從今天開始,學習Webpack,減少對腳手架的依賴(下)

思考:現在只有三個頁面,即我們要配置三個入口+三個對應的html,如果我們有十個入口,那麼我們也要這樣做重複的勞動嗎?有沒有什麼東西能幫助我們自動實現呢?答案當然是有的!

我們首先定義一個makeHtmlPlugins方法,它接受一個 Webpack 配置項的引數configs,返回一個plugins陣列

const makeHtmlPlugins = function (configs) {
  const htmlPlugins = []
  Object.keys(configs.entry).forEach(key => {
    htmlPlugins.push(
      new htmlWebpackPlugin({
        template: 'src/index.html',
        filename: `${key}.html`,
        chunks: [key]
      })
    )
  })
  return htmlPlugins
}
複製程式碼

通過呼叫makeHtmlPlugins方法,它返回一個htmlplugins陣列,把它和原有的plugin進行合併後再複製給configs

configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs));
module.exports = configs;
複製程式碼

以上配置完畢後,打包結果依然還是一樣的,請自行測試,以下是webpack.commom.js完整的程式碼:

const path = require('path');
const webpack = require('webpack');
const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const miniCssExtractPlugin = require('mini-css-extract-plugin');
const optimizaCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const configs = {
  entry: {
    index: './src/index.js',
    list: './src/list.js',
    detail: './src/detail.js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          { 
            loader: miniCssExtractPlugin.loader,
            options: {
              hmr: true,
              reloadAll: true
            }
          },
          'css-loader'
        ]
      },
      { 
        test: /\.js$/, 
        exclude: /node_modules/, 
        loader: [
          {
            loader: "babel-loader"
          },
          {
            loader: "imports-loader?this=>window"
          }
        ] 
      }
    ]
  },
  plugins: [
    new cleanWebpackPlugin(),
    new miniCssExtractPlugin({
      filename: '[name].css'
    }),
    new webpack.ProvidePlugin({
      '$': 'jquery',
      '_': 'lodash'
    })
  ],
  optimization: {
    splitChunks: {
      chunks: 'all'
    },
    minimizer: [
      new optimizaCssAssetsWebpackPlugin()
    ]
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname,'../dist')
  }
}
const makeHtmlPlugins = function (configs) {
  const htmlPlugins = []
  Object.keys(configs.entry).forEach(key => {
    htmlPlugins.push(
      new htmlWebpackPlugin({
        template: 'src/index.html',
        filename: `${key}.html`,
        chunks: [key]
      })
    )
  })
  return htmlPlugins
}
configs.plugins = configs.plugins.concat(makeHtmlPlugins(configs))
module.exports = configs
複製程式碼

如何打包一個庫檔案(Library)

在上面所有的 Webpack 配置中,幾乎都是針對業務程式碼的,如果我們要打包釋出一個庫,讓別人使用的話,該怎麼配置?在下面的幾個小節中,我們將來講一講該怎麼樣打包一個庫檔案,並讓這個庫檔案在多種場景能夠使用。

建立一個全新的專案

步驟:

  • 建立library專案
  • 使用npm init -y進行配置package.json
  • 新建src目錄,建立math.js檔案、string.js檔案、index.js檔案
  • 根目錄下建立webpack.config.js檔案
  • 安裝webpackwebpack-cli:::

按上面的步驟走完後,你的目錄大概看起來是這樣子的:

|-- src
|   |-- index.js
|   |-- math.js
|   |-- string.js
|-- webpack.config.js
|-- package.json
複製程式碼

初始化package.json

// 初始化後,改寫package.json
{
  "name": "library",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "MIT"
}

複製程式碼

建立src目錄,並新增檔案

src目錄下新建math.js,它的程式碼是四則混合運算的方法,如下:

export function add(a, b) {
  return a + b;
}
export function minus(a, b) {
  return a - b;
}
export function multiply(a, b) {
  return a * b;
}
export function division(a, b) {
  return a / b;
}
複製程式碼

src目錄下新建string.js,它有一個join方法,如下:

export function join(a, b) {
  return a + '' + b;
}
複製程式碼

src目錄下新建index.js檔案,它引用math.jsstring.js並匯出,如下:

import * as math from './math';
import * as string from './string';

export default { math, string };
複製程式碼

新增webpack.config.js

因為我們是要打包一個庫檔案,所以mode只配置為生產環境(production)即可。

在以上檔案新增完畢後,我們來配置一下webpack.config.js檔案,它的程式碼非常簡單,如下:

const path = require('path');
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'library.js',
    path: path.resolve(__dirname, 'dist')
  }
}
複製程式碼

安裝Webpack

因為涉及到 Webpack 打包,所以我們需要使用npm instll進行安裝:

$ npm install webpack webpack-cli -D
複製程式碼

進行第一次打包

使用npm run build進行第一次打包,在dist目錄下會生成一個叫library.js的檔案,我們要測試這個檔案的話,需要在dist目錄下新建index.html

$ npm run build
$ cd dist
$ touch index.html
複製程式碼

index.html中引入library.js檔案:

<script src="./library.js"></script>
複製程式碼

至此,我們已經基本把專案目錄搭建完畢,現在我們來考慮一下,可以在哪些情況下使用我們打包的檔案:

  • 使用ES Module語法引入,例如import library from 'library'
  • 使用CommonJS語法引入,例如const library = require('library')
  • 使用AMDCMD語法引入,例如require(['library'], function() {// todo})
  • 使用script標籤引入,例如<script src="library.js"></script>

針對以上幾種使用場景,我們可以在output中配置library和libraryTarget屬性(注意:這裡的library和libraryTarget和我們的庫名字library.js沒有任何關係,前者是Webpack固有的配置項,後者只是我們隨意取的一個名字)

const path = require('path');
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist'),
    library: 'library',
    libraryTarget: 'umd'
  }
}
複製程式碼

配置屬性說明:

  • library:這個屬性指,我們庫的全域性變數是什麼,類似於jquery中的$符號
  • libraryTarget: 這個屬性指,我們庫應該支援的模組引入方案,umd代表支援ES ModuleCommomJSAMD以及CMD

在配置完畢後,我們再使用npm run build進行打包,並在瀏覽器中執行index.html,在console控制檯輸出library這個全域性變數,結果如下圖所示:

從今天開始,學習Webpack,減少對腳手架的依賴(下)

以上我們所寫的庫非常簡單,在實際的庫開發過程中,往往需要使用到一些第三方庫,如果我們不做其他配置的話,第三方庫會直接打包進我們的庫檔案中。

如果使用者在使用我們的庫檔案時,也引入了這個第三方庫,就造成了重複引用的問題,那麼如何解決這個問題呢?可以在webpack.config.js檔案中配置externals屬性。

string.js檔案的join方法中,我們使用第三方庫lodash中的_join()方法來進行字串的拼接。

import _ from 'lodash';
export function join(a, b) {
  return _.join([a, b], ' ');
}
複製程式碼

在修改完畢string.js檔案後,使用npm run build進行打包,發現lodash直接打包進了我們的庫檔案,造成庫檔案積極臃腫,有70.8kb。

$ npm run build
Built at: 2019-04-05 00:47:25
     Asset      Size  Chunks             Chunk Names
library.js  70.8 KiB       0  [emitted]  main
複製程式碼

針對以上問題,我們可以在webpack.config.js中配置externals屬性,更多externals的用法請點選externals

const path = require('path');
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  externals: ['lodash'],
  output: {
    filename: 'library.js',
    path: path.resolve(__dirname, 'dist'),
    library: 'library',
    libraryTarget: 'umd'
  }
}
複製程式碼

配置完externals後,我們再進行打包,它的打包結果如下,我們可以看到我們的庫檔案又變回原來的大小了,證明我們的配置起作用了。

$ npm run build
Built at: 2019-04-05 00:51:22
     Asset      Size  Chunks             Chunk Names
library.js  1.63 KiB       0  [emitted]  main
複製程式碼

如何釋出並使用我們的庫檔案

在打包完畢後,我們如何釋出我們的庫檔案呢,以下是釋出的步驟

  • 註冊npm賬號
  • 修改package.json檔案的入口,修改為:"main": "./dist/library.js"
  • 執行npm adduser新增賬戶名稱
  • 執行npm publish命令進行釋出
  • 執行npm install xxx來進行安裝

為了維護npm倉庫的乾淨,我們並未實際執行npm publish命令,因為我們的庫是無意義的,釋出上去屬於垃圾程式碼,所以請自行嘗試釋出。另外自己包的名字不能和npm倉庫中已有的包名字重複,所以需要在package.json中給name屬性起一個特殊一點的名字才行,例如"name": "why-library-2019"

TypeScript配置

隨著TypeScript的不斷髮展,相信未來使用TypeScript來編寫 JS 程式碼將變成主流形式,那麼如何在 Webpack 中配置支援TypeScript呢?可以安裝ts-loadertypescript來解決這個問題。

新建一個專案webpack-typescript

新建立一個專案,命名為webpack-typescript,並按如下步驟處理:

  • 使用npm init -y初始化package.json檔案,並在其中新增build Webpack打包命令
  • 新建webpack.config.js檔案,並做一些簡單配置,例如entryoutput
  • 新建src目錄,並在src目錄下新建index.ts檔案
  • 新建tsconfig.json檔案,並做一些配置
  • 安裝webpackwebpack-cli
  • 安裝ts-loadertypescript

按以上步驟完成後,專案目錄大概如下所示:

|-- src
|   |-- index.ts
|-- tsconfig.json
|-- webpack.config.js
|-- package.json
複製程式碼

package.json中新增好打包命令命令:

"scripts": {
  "build": "webpack"
},
複製程式碼

接下來我們需要對webpack.config.js做一下配置:

const path = require('path');
module.exports = {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.(ts|tsx)?$/,
        use: {
          loader: 'ts-loader'
        }
      }
    ]
  },
  entry: {
    main: './src/index.ts'
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
複製程式碼

tsconfig.json裡面進行typescript的相關配置,配置項的說明如下

  • module: 表示我們使用ES6模組
  • target: 表示我們轉換成ES5程式碼
  • allowJs: 允許我們在.ts檔案中通過import語法引入其他.js檔案
{
  "compilerOptions": {
    "module": "ES6",
    "target": "ES5",
    "allowJs": true
  }
}
複製程式碼

src/index.ts檔案中書寫TypeScript程式碼,像下面這樣

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return 'hello, ' + this.greeting;
  }
}

let greeter = new Greeter('why');
console.log(greeter.greet());
複製程式碼

打包測試

  • 執行npm run build進行打包
  • 在生成dist目錄下,新建index.html,並引入打包後的main.js檔案
  • 在瀏覽器中執行index.html

從今天開始,學習Webpack,減少對腳手架的依賴(下)

使用其他模組的型別定義檔案

如果我們要使用lodash庫,必須安裝其對應的型別定義檔案,格式為@types/xxx

安裝lodash對應的typescript型別檔案:

$ npm install lodash @types/lodash -D
複製程式碼

安裝完畢後,我們在index.ts中引用lodash,並使用裡面的方法:

import * as _ from 'lodash'

class Greeter {
  greeting: string
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return _.join(['hello', this.greeting], '**');
  }
}

let greeter = new Greeter('why');
console.log(greeter.greet());
複製程式碼

打包測試

使用npm run build,在瀏覽器中執行index.html,結果如下:

從今天開始,學習Webpack,減少對腳手架的依賴(下)

Webpack效能優化

打包分析

在進行 Webpack 效能優化之前,如果我們知道我們每一個打包的檔案有多大,打包時間是多少,它對於我們進行效能優化是很有幫助的,這裡我們使用webpack-bundle-analyzer來幫助我們解決這個問題。

首先需要使用如下命令去安裝這個外掛:

$ npm install webpack-bundle-analyzer --save-dev
複製程式碼

安裝完畢後,我們需要在webpack.prod.js檔案中做一點小小的改動:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const prodConfig = {
  // 其它配置項
  mode: 'production',
  plugins: [
    new BundleAnalyzerPlugin()
  ]
}
複製程式碼

配置完畢後,我們執行npm run build命令來檢視打包分析結果,以下打包結果僅供參考:

從今天開始,學習Webpack,減少對腳手架的依賴(下)

縮小檔案的搜尋範圍

首先我們要弄明白 Webpack 的一個配置引數(Resolve)的作用:它告訴了 Webpack 怎麼去搜尋檔案,它同樣有幾個屬性需要我們去理解:

  • extensions:它告訴了 Webpack 當我們在匯入模組,但沒有寫模組的字尾時應該如何去查詢模組。
  • mainFields:它告訴了 Webpack 當我們在匯入模組,但並沒有寫模組的具體名字時,應該如何去查詢這個模組。
  • alias:當我們有一些不得不引用的第三方庫或者模組的時候,可以通過配置別名,直接引入它的.min.js檔案,這樣可以庫內的直接解析
  • 其它includeexcludetest來配合loader進行限制檔案的搜尋範圍

extensions引數

就像上面所說的那樣,extensions它告訴了 Webpack 當我們在匯入模組,但沒有寫模組的字尾時,應該如何去查詢模組。這種情況在我們開發中是很常見的,一個情形可能如下所示:

// 書寫了模組字尾
import main from 'main.js'

// 沒有書寫模組字尾
import main from 'main'
複製程式碼

像上面那樣,我們不寫main.js.js字尾,是因為 Webpack 會預設幫我們去查詢一些檔案,我們也可以去配置自己的檔案字尾配置:

extensions引數應儘可能只配置主要的檔案型別,不可為了圖方便寫很多不必要的,因為每多一個,底層都會走一遍檔案查詢的工作,會損耗一定的效能。

module.exports = {
  // 其它配置
  resolve: {
    extensions: ['.js', '.json', '.vue']
  }
}
複製程式碼

如果我們像上面配置後,我們可以在程式碼中這樣寫:

// 省略 .vue檔案擴充套件
import BaseHeader from '@/components/base-header';

// 省略 .json檔案擴充套件
import CityJson from '@/static/city';
複製程式碼

mainFields引數

mainFields引數主要應用場景是,我們可以不寫具體的模組名稱,由 Webpack 去查詢,一個可能的情形如下:

// 省略具體模組名稱
import BaseHeader from '@components/base-header/';

// 以上相當於這一段程式碼
import BaseHeader from '@components/base-header/index.vue';
// 或者這一段
import BaseHeader from '@components/base-header/main.vue';
複製程式碼

我們也可以去配置自己的mainFields引數:

同extensions引數類似,我們也不建議過多的配置mainFields的值,原因如上。

module.exports = {
  // 其它配置
  resolve: {
    extensions: ['.js', '.json', '.vue'],
    mainFields: ['main', 'index']
  }
}
複製程式碼

alias引數

alias引數更像一個別名,如果你有一個目錄很深、檔名很長的模組,為了方便,配置一個別名這是很有用的;對於一個龐大的第三方庫,直接引入.min.js而不是從node_modules中引入也是一個極好的方案,一個可能得情形如下:

通過別名配置的模組,會影響Tree Shaking,建議只對整體性比較強的庫使用,像lodash庫不建議通過別名引入,因為lodash使用Tree Shaking更合適。

// 沒有配置別名之前
import main from 'src/a/b/c/main.js';
import React from 'react';

// 配置別名之後
import main from 'main.js';
import React from 'react';
複製程式碼
// 別名配置
const path = require('path');
module.exports = {
  // 其它配置
  resolve: {
    extensions: ['.js', '.json', '.vue'],
    mainFields: ['main', 'index'],
    alias: {
      main: path.resolve(__dirname, 'src/a/b/c'),
      react: path.resolve(__dirname, './node_modules/react/dist/react.min.js')
    }
  }
}
複製程式碼

Tree Shaking去掉冗餘的程式碼

Tree Shaking配置我們已經在上面講過,配置Tree Shaking也很簡單。

module.exports = {
  // 其它配置
  optimization: {
    usedExports: true
  }
}
複製程式碼

如果你對Tree Shaking還不是特別理解,請點選Tree Shaking閱讀更多。

DllPlugin減少第三方庫的編譯次數

對於有些固定的第三方庫,因為它是固定的,我們每次打包,Webpack 都會對它們的程式碼進行分析,然後打包。那麼有沒有什麼辦法,讓我們只打包一次,後面的打包直接使用第一次的分析結果就行。答案當然是有的,我們可以使用 Webpack 內建的DllPlugin來解決這個問題,解決這個問題可以分如下的步驟進行:

  • 把第三方庫單獨打包在一個xxx.dll.js檔案中
  • index.html中使用xxx.dll.js檔案
  • 生成第三方庫的打包分析結果儲存在xxx.manifest.json檔案中
  • npm run build時,引入已經打包好的第三方庫的分析結果
  • 優化

單獨打包第三方庫

為了單獨打包第三方庫,我們需要進行如下步驟:

  • 根目錄下生成dll資料夾
  • build目錄下生成一個webpack.dll.js的配置檔案,並進行配置。
  • package.json檔案中,配置build:dll命令
  • 使用npm run build:dll進行打包

生成dll資料夾:

$ mkdir dll
複製程式碼

build資料夾下生成webpack.dll.js:

$ cd build
$ touch webpack.dll.js
複製程式碼

建立完畢後,需要在webpack.dll.js檔案中新增如下程式碼:

const path = require('path');
module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  }
}
複製程式碼

最後需要在package.json檔案中新增新的打包命令:

{
  // 其它配置
  "scripts": {
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
    "build:dll": "webpack --config ./build/webpack.dll.js"
  }
}
複製程式碼

使用npm run build:dll打包結果,你的打包結果看起來是下面這樣的:

|-- build
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.dll.js
|   |-- webpack.prod.js
|-- dll
|   |-- vendors.dll.js
|-- src
|   |-- index.html
|   |-- index.js
|-- package.json
複製程式碼

引用xxx.dll.js檔案

在上一小節中我們成功拿到了xxx.dll.js檔案,那麼如何在index.html中引入這個檔案呢?答案是需要安裝add-asset-html-webpack-plugin外掛:

$ npm install add-asset-html-webpack-plugin -D
複製程式碼

webpack.common.js中使用add-asset-html-webpack-plugin外掛:

const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const configs = {
  // 其它配置
  plugins: [
    new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    })
  ]
}
module.exports = configs;
複製程式碼

我們將第三方庫全域性暴露了一個vendors變數,現引入xxx.dll.js檔案結果如下所示:

從今天開始,學習Webpack,減少對腳手架的依賴(下)

生成打包分析檔案

webpack.dll.js中使用 Webpack 內建的DllPlugin外掛,進行打包分析:

const path = require('path');
const webpack = require('webpack');
module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash', 'jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, '../dll/[name].manifest.json')
    })
  ]
}
複製程式碼

引用打包分析檔案

webpack.common.js中使用 Webpack 內建的DllReferencePlugin外掛來引用打包分析檔案:

const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
module.exports = {
  // 其它配置
  plugins: [
    new cleanWebpackPlugin(),
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
    })
  ]
}
複製程式碼

優化

現在我們思考一個問題,我們目前是把lodashjquery全部打包到了vendors檔案中,那麼如果我們要拆分怎麼辦,拆分後又該如何去配置引入?一個可能的拆分結果如下:

const path = require('path');
const webpack = require('webpack');
module.exports = {
  mode: 'production',
  entry: {
    vendors: ['lodash'],
    jquery: ['jquery']
  },
  output: {
    filename: '[name].dll.js',
    path: path.resolve(__dirname, '../dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]',
      path: path.resolve(__dirname, '../dll/[name].manifest.json')
    })
  ]
}
複製程式碼

根據上面的拆分結果,我們需要在webpack.common.js中進行如下的引用配置:

const htmlWebpackPlugin = require('html-webpack-plugin');
const cleanWebpackPlugin = require('clean-webpack-plugin');
const addAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const path = require('path');
const configs = {
  // ... 其他配置
  plugins: [
    new cleanWebpackPlugin(),
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
    new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    }),
     new addAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/jquery.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
    }),
    new webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, '../dll/jquery.manifest.json')
    })
  ]
}
module.exports = configs;
複製程式碼

我們可以發現:隨著我們引入的第三方模組越來越多,我們不斷的要進行 Webpack 配置檔案的修改。對於這個問題,我們可以使用Node的核心模組fs來分析dll資料夾下的檔案,進行動態的引入,根據這個思路我們新建一個makePlugins方法,它返回一個 Webpack 的一個plugins陣列:

const makePlugins = function() {
  const plugins = [
    new cleanWebpackPlugin(),
    new htmlWebpackPlugin({
      template: 'src/index.html'
    }),
  ];

  // 動態分析檔案
  const files = fs.readdirSync(path.resolve(__dirname, '../dll'));
  files.forEach(file => {
    // 如果是xxx.dll.js檔案
    if(/.*\.dll.js/.test(file)) {
      plugins.push(
        new addAssetHtmlWebpackPlugin({
          filepath: path.resolve(__dirname, '../dll', file)
        })
      )
    }
    // 如果是xxx.manifest.json檔案
    if(/.*\.manifest.json/.test(file)) {
      plugins.push(
        new webpack.DllReferencePlugin({
          manifest: path.resolve(__dirname, '../dll', file)
        })
      )
    }
  })
  return plugins;
}
configs.plugins = makePlugins(configs);
module.exports = configs;
複製程式碼

使用npm run build:dll進行打包第三方庫,再使用npm run build打包,打包結果如下:

本次試驗,第一次打包時間為1100ms+,後面的打包穩定在800ms+,說明我們的 Webpack效能優化已經生效。

|-- build
|   |-- webpack.common.js
|   |-- webpack.dev.js
|   |-- webpack.dll.js
|   |-- webpack.prod.js
|-- dist
|   |-- index.html
|   |-- jquery.dll.js
|   |-- main.1158fa9f961c50aaea21.js
|   |-- main.1158fa9f961c50aaea21.js.map
|-- dll
|   |-- jquery.dll.js
|   |-- jquery.manifest.json
|   |-- vendors.dll.js
|   |-- vendors.manifest.json
|-- src
|   |-- index.html
|   |-- index.js
|-- package.json
|-- postcss.config.js
複製程式碼

小結:Webpack 效能優化是一個長久的話題,本章也僅僅只是淺嘗輒止,後續會有關於 Webpack 更加深入的解讀部落格,敬請期待(立個flag)。

編寫自己的Loader

在我們使用 Webpack 的過程中,我們使用了很多的loader,那麼那些loader是哪裡來的?我們能不能寫自己的loader然後使用? 答案當然是可以的,Webpack 為我們提供了一些loader的API,通過這些API我們能夠編寫出自己的loader並使用。

如何編寫及使用自己的Loader

場景: 我們需要把.js檔案中,所有出現Webpack is good!,改成Webpack is very good!。實際上我們需要編寫自己的loader,所以我們有如下的步驟需要處理:

  • 新建webpack-loader專案
  • 使用npm init -y命令生成package.json檔案
  • 建立webpack.config.js檔案
  • 建立src目錄,並在src目錄下新建index.js
  • 建立loaders目錄,並在loader目錄下新建replaceLoader.js
  • 安裝webpackwebpack-cli

按上面的步驟新建後的專案目錄如下:

|-- loaders
|   | -- replaceLoader.js
|-- src
|   | -- index.js
|-- webpack.config.js
|-- package.json
複製程式碼

首先需要在webpack.config.js中新增下面的程式碼:

const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [path.resolve(__dirname, './loaders/replaceLoader.js')]
      }
    ]
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
複製程式碼

隨後在package.json檔案新增build打包命令:

// 其它配置
"scripts": {
  "build": "webpack"
}
複製程式碼

接下來在src/index.js檔案中新增一行程式碼:這個檔案使用最簡單的例子,只是列印一句話。

console.log('Webpack is good!');
複製程式碼

最後就是在loader/replaceLoader.js編寫我們自己loader檔案中的程式碼:

  • 編寫loader時,module.exports是固定寫法,並且它只能是一個普通函式,不能寫箭頭函式(因為需要this指向自身)
  • source是打包檔案的原始檔內容
const loaderUtils = require('loader-utils');
module.exports = function(source) {
  return source.replace('good', 'very good');
}
複製程式碼

使用我們的loader: 要使用我們的loader,則需要在modules中寫loaderresolveLoader它告訴了 Webpack 使用loader時,應該去哪些目錄下去找,預設是node_modules,做了此項配置後,我們就不用去顯示的填寫其路徑了,因為它會自動去loaders資料夾下面去找。

const path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  resolveLoader: {
    modules: ['node_modules', './loaders']
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [{
          loader: 'replaceLoader',
          options: {
            name: 'wanghuayu'
          }
        }]
      }
    ]
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}
複製程式碼

最後我們執行npm run build,在生成的dist目錄下開啟main.js檔案,可以看到檔案內容已經成功替換了,說明我們的loader已經使用成功了。

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no static exports found */
/***/ (function(module, exports) {

eval("console.log('Webpack is very good!');\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });
複製程式碼

如何向Loader傳參及返回多個值

問題:

  • 我們如何返回多個值?
  • 我們如何向自己的Loader傳遞引數?

如何返回多個值

Webpack 的 API允許我們使用callback(error, result, sourceMap?, meta?)返回多個值,它有四個引數:

  • Error || Null :錯誤型別, 沒有錯誤傳遞null
  • result :轉換後的結果
  • sourceMap:可選引數,處理分析後的sourceMap
  • meta: 可選引數,元資訊

返回多個值,可能有如下情況:

// 第三,第四個引數是可選的。
this.callback(null, result);
複製程式碼

如何傳遞引數

我們知道在使用loader的時候,可以寫成如下的形式:

// options裡面可以傳遞一些引數
{
  test: /\.js$/,
  use: [{
    loader: 'replaceLoader',
    options: {
      word: 'very good'
    }
  }]
}
複製程式碼

再使用options傳遞引數後,我們可以使用官方提供的loader-utils來獲取options引數,可以像下面這樣寫:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  var options = loaderUtils.getOptions(this);
  return source.replace('good', options.word)
}
複製程式碼

如何在Loader中寫非同步程式碼

在上面的例子中,我們都是使用了同步的程式碼,那麼如果我們有必須非同步的場景,該如何實現呢?我們不妨做這樣的假設,先寫一個setTimeout

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  var options = loaderUtils.getOptions(this);
  setTimeout(() => {
    var result = source.replace('World', options.name);
    return this.callback(null, result);
  }, 0);
}
複製程式碼

如果你執行了npm run build進行打包,那麼一定會報錯,解決辦法是:使用this.async()主動標識有非同步程式碼:

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  var options = loaderUtils.getOptions(this);
  var callback = this.async();
  setTimeout(() => {
    var result = source.replace('World', options.name);
    callback(null, result);
  }, 0);
}
複製程式碼

至此,我們已經掌握瞭如何編寫、如何引用、如何傳遞引數以及如何寫非同步程式碼,在下一小節當中我們將學習如何編寫自己的plugin

編寫自己的Plugin

loader一樣,我們在使用 Webpack 的過程中,也經常使用plugin,那麼我們學習如何編寫自己的plugin是十分有必要的。 場景:編寫我們自己的plugin的場景是在打包後的dist目錄下生成一個copyright.txt檔案

plugin基礎

plugin基礎講述了怎麼編寫自己的plugin以及如何使用,與建立自己的loader相似,我們需要建立如下的專案目錄結構:

|-- plugins
|   -- copyWebpackPlugin.js
|-- src
|   -- index.js
|-- webpack.config.js
|-- package.json
複製程式碼

copyWebpackPlugins.js中的程式碼:使用npm run build進行打包時,我們會看到控制檯會輸出hello, my plugin這段話。

plugin與loader不同,plugin需要我們提供的是一個類,這也就解釋了我們必須在使用外掛時,為什麼要進行new操作了。

class copyWebpackPlugin {
  constructor() {
    console.log('hello, my plugin');
  }
  apply(compiler) {

  }
}
module.exports = copyWebpackPlugin;
複製程式碼

webpack.config.js中的程式碼:

const path = require('path');
// 引用自己的外掛
const copyWebpackPlugin = require('./plugins/copyWebpackPlugin.js');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    // new自己的外掛
    new copyWebpackPlugin()
  ]
}
複製程式碼

如何傳遞引數

在使用其他plugin外掛時,我們經常需要傳遞一些引數進去,那麼我們如何在自己的外掛中傳遞引數呢?在哪裡接受呢?
其實,外掛傳參跟其他外掛傳參是一樣的,都是在建構函式中傳遞一個物件,外掛傳參如下所示:

const path = require('path');
const copyWebpackPlugin = require('./plugins/copyWebpackPlugin.js');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
  plugins: [
    // 向我們的外掛傳遞引數
    new copyWebpackPlugin({
      name: 'why'
    })
  ]
}
複製程式碼

plugin的建構函式中呼叫:使用npm run build進行打包,在控制檯可以列印出我們傳遞的引數值why

class copyWebpackPlugin {
  constructor(options) {
    console.log(options.name);
  }
  apply(compiler) {

  }
}
module.exports = copyWebpackPlugin;
複製程式碼

如何編寫及使用自己的Plugin

  • apply函式是我們外掛在呼叫時,需要執行的函式
  • apply的引數,指的是 Webpack 的例項
  • compilation.assets打包的檔案資訊

我們現在有這樣一個需求:使用自己的外掛,在打包目錄下生成一個copyright.txt版權檔案,那麼該如何編寫這樣的外掛呢? 首先我們需要知道plugin的鉤子函式,符合我們規則鉤子函式叫:emit,它的用法如下:

class CopyWebpackPlugin {
  constructor() {
  }
  apply(compiler) {
    compiler.hooks.emit.tapAsync('CopyWebpackPlugin', (compilation, cb) => {
      var copyrightText = 'copyright by why';
      compilation.assets['copyright.txt'] = {
        source: function() {
          return copyrightText
        },
        size: function() {
          return copyrightText.length;
        }
      }
      cb();
    })
  }
}
module.exports = CopyWebpackPlugin;
複製程式碼

使用npm run build命名打包後,我們可以看到dist目錄下,確實生成了我們的copyright.txt檔案。

|-- dist
|   |-- copyright.txt
|   |-- main.js
|-- plugins
|   |-- copyWebpackPlugin.js
|-- src
|   |-- index.js
|-- webpack.config.js
|-- package.json
複製程式碼

我們開啟copyright.txt檔案,它的內容如下:

copyright by why
複製程式碼

本篇部落格由慕課網視訊從基礎到實戰手把手帶你掌握新版Webpack4.0閱讀整理而來,觀看視訊請支援正版。

相關文章