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

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

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

問:這篇文章的出處?
答:此篇文章知識來自付費視頻(連接在文章末尾),文章由本身獨立撰寫,已得到講師受權並首發於掘金前端

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

若是你以爲寫的不錯,請給我點一個star,原博客地址:原文地址node

PWA配置

PWA全稱Progressive Web Application(漸進式應用框架),它能讓咱們主動緩存文件,這樣用戶離線後依然可以使用咱們緩存的文件打開網頁,而不至於讓頁面掛掉,實現這種技術須要安裝workbox-webpack-plugin插件。react

若是你的谷歌瀏覽器尚未開啓支持PWA,請開啓它再進行下面的測試。 jquery

安裝插件

$ 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目錄的打包結果以下:webpack

|-- 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。ios

改寫index.js

須要判斷瀏覽器是否支持PWA,支持的時候咱們才進行註冊,註冊的.js文件爲咱們打包後的service-worker.js文件。git

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的實際結果以下圖所示

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後, 咱們能夠在瀏覽器中看到,咱們已經成功請求到了咱們的數據。

如何使用接口路徑重寫

如今依然假設有這樣一個場景: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'
        }
      }
    }
  }
}
複製代碼

一樣,咱們打包後在瀏覽器中能夠看到,咱們的測試接口的數據已經成功拿到了。

其餘常見配置的含義

轉發到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在瀏覽器中運行,結果以下:

思考:如今只有三個頁面,即咱們要配置三個入口+三個對應的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.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

使用其餘模塊的類型定義文件

若是咱們要使用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-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 的一個配置參數(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.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閱讀整理而來,觀看視頻請支持正版。

相關文章
相關標籤/搜索