天天看點

《深入淺出Webpack》學習筆記

入門

先簡單地提下子產品化的思想。

子產品化

簡單來說就是将複雜的系統分解成各個簡單的子子產品,便于開發和維護。

一般 JavaScript 子產品化規範有 CommonJS,AMD 和 ES6 中的子產品化。

CommonJS

其核心思想就是利用

require

來同步加載依賴的子產品,通過

module.exports

來暴露子產品的接口。

優點在于

  • 代碼可在 node.js 環境下運作
  • NPM 包中大部分子產品都支援 CommonJS

缺點在于:無法在浏覽器環境下運作,要想運作必須通過工具轉換。

AMD

異步子產品定義(Asynchronous Module Definition) 是以異步的方式加載子產品,主要用于解決浏覽器的子產品化加載,比如像 require.js.

優點

  • 可無需轉換即可在浏覽器環境中運作
  • 可異步加載子產品
  • 可并行加載多個子產品
  • 可在node和浏覽器環境中運作

缺點:浏覽器沒有原生支援 AMD,若要使用需要導入相應的包。

ES6 的子產品化

它是 ECMA 提出的 JavaScript 子產品化規範,是在語言層面上實作的。他将逐漸取代 CommonJS 和 AMD 規範。但是目前仍然無法運作在大部分 JavaScript 運作環境中,是以需要轉換工具轉成 ES5 後才能運作。

tip: CSS 樣式檔案中也開始支援子產品化,比如

SASS, SCSS, less

不僅這些子產品化規範需要轉換工具才能運作,還有目前流行 React 中 JSX 文法和 vue 中的 template 也需要進行轉換,還有目前流行 es6,typescript 也需要轉換。是以随着項目越來越複雜和技術越來越新,不可避免的需要出現建構工具來統一的管理這些轉換工具。

常見的建構工具

建構工具主要的工作如下:

  • 代碼轉換
  • 檔案優化:壓縮靜态資源檔案
  • 分割代碼,将公共代碼分離出來
  • 子產品合并
  • 實作熱更新
  • 自動校驗
  • 實作自動釋出

主要的建構工具有(誕生的時間排序):

  • Npm Script:屬于 NPM 内置的功能,在 package.json 檔案中的 script 對象中配置,其中每個屬性對應一個 shell 指令。
  • Grunt:通過執行任務的方式進行建構。
  • Gulp:一種基于流的自動化建構工具,除了可以管理和執行任務,還能監聽檔案,讀寫檔案。
  • Webpack:專注于構模組化塊化項目,在 webpack 裡,一切檔案都被視為子產品。通過 loader 轉換檔案,通過 Plugin 注入鈎子,最後輸出有多個子產品組合而成的檔案。
  • Rollup:和 webpack 類似,他的亮點就是可以 Tree shaking,以減小輸出檔案大小和提高運作性能。後期 webpack 也是實作了 Tree Shaking。

選擇 Webpack 的原因

  • 可以為新項目提供一站式的解決方案
  • 有良好的生态和維護團隊
  • 被大量使用,經受住了大家的考驗。

安裝

# 初始化 npm 包
npm init

# 以下三選一
# 穩定版
npm install webpack -D
# 指定版
npm install [email protected]<version> -D
# 最新的體驗版
npm install [email protected] -D
           

請看以下 webpack 使用執行個體(檔案都在根目錄)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app"></div>
<!-- 導入webpack打包輸出的檔案 -->
<script src="./dist/bundle.js"></script>
</body>
</html>
           

show.js

function show(content){
  document.getElementById("app").innerText = "hello, " + content;
}

// 通過 CommonJS 規範導出show函數
module.exports=show;
           

main.js

// 通過 CommonJS 規範導入show函數
const show = require('./show.js')

show("webpack")
           

wepack.config.js

const path = require('path')

module.exports = {
  // javascript 執行入口檔案
  entry: './main.js',
  output: {
    // 将所有依賴的子產品合并輸出到bundle.js檔案中
    filename: "bundle.js",
    // 将輸出檔案都放到此目錄下
    path: path.resolve(__dirname, "./dist")
  }
}
           

然後在根目錄執行打包指令

webpack

會看到生成 dist 目錄,裡面會有打包好的檔案bundle.js。

打開 index.html 檔案便可看到建構效果。

從 Webpack 2版本開始,就已經内置了轉換 CommonJS、ES6、AMD子產品化的功能

簡單的 Loader 使用示例

假如我們需要給頁面添加樣式,建立了

main.css

檔案:

#app{
    color:red;
}
           

為了讓樣式生效,我們還需要在

main.js

中導入:

// 通過 CommonJS 規範導入 main.css
require('main.css')
// 通過 CommonJS 規範導入show函數
const show = require('./show.js')

show("webpack")
           

因為 webpack 原生是不支援非 JavaScript 檔案轉換的,是以直接通過

webpack

打包指令可能會報錯:

ERROR in ./main.css 1:0
Module parse failed: Unexpected character '#' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> #app{
|     color: red;
| }
 @ ./main.js 2:0-21
           

由此需要引出 Loader 概念,它就是用來處理檔案轉換的。如果需要支援 CSS 子產品的轉換,首先需要先安裝相關 Loader:

npm install -D style-loader css-loader

           

之後在

webpack.config.js

檔案中配置:

const path = require('path')

module.exports = {
  // javascript 執行入口檔案
  entry: './main.js',
  output: {
    // 将所有依賴的子產品合并輸出到bundle.js檔案中
    filename: "bundle.js",
    // 将輸出檔案都放到此目錄下
    path: path.resolve(__dirname, "./dest")
  },
  // 配置 Loader
  module: {
    rules: [
      {
        test:/\.css$/,
        // css-loader負責讀取css檔案,再通過 style-loader 注入到js中
        use:['style-loader', 'css-loader?minimize'] 
      }
    ]
  }
}

           

通過在

module.rules

數組中配置一系列規則,以便告知 webpack 根據哪些 Loader 去轉換指定的檔案。其中

use

屬性需要注意以下幾點:

  • Loader 的執行順序是由後向前的。
  • 每個 Loader 傳參方式有兩種:
    • 通過 URL querystring 的方式,比如

      css-loader?minimize

      就是告知 Loader 要開啟 CSS 壓縮。
    • 通過options 方式:
      const path = require('path')
      
      module.exports = {
        // javascript 執行入口檔案
        entry: './main.js',
        output: {
          // 将所有依賴的子產品合并輸出到bundle.js檔案中
          filename: "bundle.js",
          // 将輸出檔案都放到此目錄下
          path: path.resolve(__dirname, "./dest")
        },
        module: {
          rules: [
            {
              test:/\.css$/,
              use:['style-loader', {
                loader:'css-loader',
                options: {
                  minimize:true
                }
              }]
            }
          ]
        }
      }
      
                 

注:webpack3.0 和 css-loader1.0 以上就不支援 minimize 了,為了講解傳參方式,還是用了它。

簡單的 Plugin 使用示例

Plugin 是用來擴充 webpack 功能的,通過在建構流程中注入鈎子實作。比如我們需要将 CSS 從 bundle.js 檔案中分離出來,可以用

extract-text-webpack-plugin

,首先還是先安裝 Plugin:

npm install extract-text-webpack-plugin -D

           

然後在 webpack.config.js 檔案中配置:

const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
let plugins = []
plugins.push(new ExtractTextPlugin({
  // 提取出來的 css 檔案名
  filename: `[name]_[contenthash:8].css`
}))
module.exports = {
  // javascript 執行入口檔案
  entry: './main.js',
  output: {
    // 将所有依賴的子產品合并輸出到bundle.js檔案中
    filename: "bundle.js",
    // 将輸出檔案都放到此目錄下
    path: path.resolve(__dirname, "./dist")
  },
  module: {
    rules: [
      {
        test:/\.css$/,
        use:ExtractTextPlugin.extract({
          fallback:'style-loader',
          use:['css-loader']
        })
      }
    ]
  },
  plugins
}

           

**注:webpack4.0 以上就不再支援 extract-text-webpack-plugin **,可以通過安裝新的版本解決:

npm install --D [email protected]

使用 DevServer

一般我們開發網頁調試有以下幾個需求:

  1. 提供 http 服務而不是不是本地檔案預覽
  2. 監聽檔案内容變化,實時更新頁面
  3. 提供 sourcemap 功能,以便調試頁面

後兩個功能 webpack 原生支援。第一個需求可使用

webpack-dev-server

,它可以啟動 http 服務用于網絡請求,并同時執行 webpack 指令,将建構後的檔案存儲在記憶體中。是以執行

webpack-dev-server

指令是不會建立

dist

目錄,是以在

.index.html

中靜态資源路徑需要将以前的

./dist/bundle.js

修改為

./bundle.js

第二個功能可以通過給

webpack-dev-server

指令加

--hot

參數實作:

也就是常說的熱替換,在不重新重新整理頁面的情況下通過替換舊子產品來更新頁面。

webpack-dev-server --hot

           

第三個功能可以通過給

webpack-dev-server

指令加

--devtool source-map

參數實作:

webpack-dev-server --devtool source-map

           

核心概念

以下幾個核心概念需要清楚:

  • Entry:入口
  • Module:子產品
  • Chunk:代碼塊
  • Loader:子產品轉換器
  • Plugin:擴充插件
  • Output:輸出結果

webpack 的流程:

webpack 啟動時會先找到 Entry 配置裡 Module,去解析 Entry 依賴的所有 Module。每找到一個 Module 就會根據 Loader 裡的相應規則進行轉換,轉換後會再次解析目前 Module 依賴的所有 Module,這些子產品會以 Entry 為機關進行分組,一個 Entry及其依賴的所有 Module 形成一個組,也就是一個 chunk,webpack 會将所有的 chunk 轉換成輸出檔案。在建構中,webpack會在恰當的時機執行 plugin 中的邏輯。

配置

webpack 配置總的來說有兩種方式,一種通過配置檔案

webpack.config.js

,另一種則是通過指令行的方式,如

webpack --devtool map-resource

本章主要講解下以下配置概念:

  • Entry:配置子產品的入口檔案
  • Output:配置建構輸出檔案資訊
  • Module:配置解析子產品的規則
  • Resolve:配置尋找子產品的規則
  • Plugins:配置擴充插件
  • DevServer:配置 DevServer
  • 配置總結

Entry

context

webpack 在處理相對路徑時會以

context

為根目錄,

context

預設為目前配置檔案目錄,淡然也可以通過以下兩種方式修改:

  1. 修改配置檔案webpack.config.js
module.exports={
    context:path.resolve(__dirname, 'app')
}

           
  1. 指令行式改變
webpack --context path

           

Entry 類型

entry可以是三種類型:

  • String:

    './app/entry'

    ,隻輸出一個Chunk,Chunk的名稱是main
  • Array:

    ['./app/entry1', './app/entry2']

    ,隻輸出最後一個元素的 Chunk,Chunk的名稱是main
  • Object:

    {a:'./app/entry-a',b:'./app/entry-b'}

    ,輸出多個Chunk,Chunk的名稱是對象對應的鍵值

動态配置 Entry

當入口檔案是動态時,就需要給 Entry 傳入函數,如下配置:

// 同步函數
entry:()=>{
    return {
        a:'./app/entry-a',
        b:'./app/entry-b'
    }
}
// 或者異步函數
entry:()=>{
    return new Pormise((resolve)=>{
        resolve({
        a:'./app/entry-a',
        b:'./app/entry-b'})
    })
}

           

Output

常見的屬性有:

  • filename

    :配置輸出檔案的名稱
  • path

    :配置輸出檔案的目錄
  • publicPath

    :用于處理靜态資源引用位址

filename 和 path

比如:

output:{
    filename:'bundle.js',
    path:path.resolve(__dirname, './dist')
}

           

filename

除了可以配置靜态名稱也可以配置動态名稱:

  • filename: [id].js

    :chunk的唯一辨別符,從0開始
  • filename: [name].js

    :chunk的名稱
  • filename: [hash].js

    :chunk 唯一辨別hash,可以取hash前幾位,比如取前8位:

    filename: [hash:8].js

  • filename: [chunkhash].js

    :chunk 内容的 hash,可以取hash前幾位,比如取前8位:

    filename: [hash:8].js

其他屬性

  • chunkFilename

  • publicPath

  • crossOriginLoading

  • libraryTarget

    和 library
  • libraryExport

  • 等等…

Module

主要用來配置子產品處理功能,裡面主要有

rules

屬性,Array 類型,用于加載和解析子產品檔案,用于配置 Loader 規則。

rules

元素有以下幾個常用的屬性:

  • test
  • use
  • noParse
  • parser

配置 Loader

通過以下的示例來說明 Loader 配置。

module: {
    rules: [
      {
        // 命中 js 檔案
        // test 也可以接收數組類型,其中每個元素條件之間是“或”的關系
        test:/\.js$/,
        // 用 babel-loader 轉換 JavaScript 檔案
        // ?cacheDirectory 表示傳給 babel-loader 的參數,用于緩存 babel 的便宜結果,加快編譯速度
        // use 數組裡的loader執行順序為從右向左,也可以通過enforce強制設定執行位置。
        use:['babel-loader?cacheDirectory'],
        // 隻命中 src 目錄裡的 JavaScript 檔案,加快 Webpack 的搜尋速度
        include:path.resolve(__dirname,'src'),
        // 通過 exclude 排除 node_modules 目錄下的檔案,
        // 一般來講,include和exclude不必要同時配置,在此為了說明兩者用法,故同時配置
        exclude: path.resolve(__dirname,'node_modules'),
        // 配置哪些内置子產品文法被解析
        parser: {
          amd:false, // 禁用 AMD
          commonjs:false, // 禁用 CommonJS
          system:false, // 禁用 SystemJS
          harmony:false, // 禁用ES6 import/export
          // 等等...
        },
      }
    ],
    // 忽略符合以下規則子產品的解析, noparse 可以是 RegExp,[RegExp],function中的一種
    noParse: /jquery|chartjs/
  }

           

Resolve

用來告知 webpack 按照何種規則來尋找子產品。

可由以下示例來講解:

resolve: {
    // 配置别名
    alias: {
      '@':'./src/components',
    },
    // 決定優先是哪個入口檔案代碼,查找順序從左至右
    mainFields: ['browser','main'],
    // 配置擴充名,當導入檔案的擴充名省略時,從以下數組中依次比對查找,查找順序從左至右
    extensions: ['.ts','.js','.json'],
    // './src/components' 和 'node_modules' 路徑下的子產品導入的相對路徑可以其作為根目錄
    // 比如 './src/components' 的button子產品,直接可以通過 import 'button' 導入
    modules:['./src/components', 'node_modules']
    // 配置第三方子產品檔案名稱描述,也就是 package.json
    descriptionFiles: ['package.json'],
    // 子產品導入時是否必須帶擴充名
    enforceExtension: true,
    // 子產品導入時是否必須帶擴充名,隻針對 node_modules 下的子產品生效
    enforceModuleExtension: false
  },

           

Plugin

Plugin 用于擴充 webpack,配置很簡單,接收數組類型,每個元素都是 Plugin的示例,Plugin的參數由構造函數傳入,比如:

const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
    plugins:[new ExtractTextPlugin({
        // 提取出來的 css 檔案名
        filename: `style.css`
  	})]
}

           

難點在于各自 Plugin 自身的參數配置。

DevServer

DevServer 除了在指令行配置參數,也可以在配置檔案webpack.config.js中配置

module.exports={
    devServer: {
    	host:'127.0.0.1',
        port:8080
  	}
}

           

此配置項僅針對

devServer

指令有效,

webpack

指令會忽略此配置項。

之前也講了它的簡單用法,以下講下其常用的配置:

  • hot

    :用于熱替換
  • host

    :用設定web伺服器位址
  • port

    :用于設定端口号
  • https

    :用于設定https服務,預設是http服務。
  • open

    :在第一次建構完是否自動打開網頁

其他配置項

  • devtool

    :用于設定 source-map,便于調試
  • watch

    watchOptions

    :用于監聽檔案改動,

    webpack-dev-server

    指令自動開啟
  • externals

    :告知以下檔案無需打包。
  • 等等…

實戰

具體配置可檢視書籍或相關Loader介紹。

優化

優化方向有兩個:

  • 優化開發效率(針對開發人員)
    • 提高建構速度
    • 優化開發體驗
  • 優化輸出品質(針對使用者)
    • 減少首屏加載時間
    • 提升流暢度

縮小檔案的搜尋範圍

  • 優化 Loader 配置:針對

    module

    配置
    • 可以添加 noParse 屬性,排除一些非子產品化檔案,像

      noparse:/jquery|chart\.js/

      。確定這些檔案中沒有子產品化操作。
    • 在 rules 屬性中給其 Loader 盡可能添加 include 或 exclude 屬性以縮小解析範圍。
  • resolve

    配置:
    • extensions

      的數組長度盡可能的短,頻率高的字尾名放前面。
    • 盡可能的在

      alias

      中配置依賴子產品的具體位置。
    • 等等…

使用 HappyPack

使用 HappyPack 插件進行多程序建構。它會接管某些 Loader 任務,并行執行這些任務。

使用

parallelUglifyPlugin

插件

webpack 内置了

UglifyPlugin

用于壓縮代碼,

parallelUglifyPlugin

插件可以多程序并行壓縮代碼。

使用自動重新整理優化開發體驗

主要通過配置

watch

實作。

監聽檔案是否發生改變,如果發生改變則在一定時間内重新建構輸出檔案,并通過向網頁中注入代理用戶端代碼的方式來實作自動重新整理功能。

開啟子產品熱替換優化開發體驗

原理:隻需編譯發生變化了的代碼,再将新的輸出子產品替換浏覽器中舊的子產品。

優勢:

  • 建構速度快。
  • 可以在儲存浏覽器狀态的情況下更新頁面。

區分環境

通過

DefinePlugin

插件來定義

process.env.NODE_ENV

的值。

壓縮代碼

JavaScript 壓縮原理:插件通過分析 JavaScript 文法分析樹,按照一定的規則将代碼中的輸出日志、注釋等一些無用代碼去除。

CSS 壓縮原理:了解 CSS 含義,比如會将

color:#ff0000

壓縮成

color:red

,壓縮率可達到 60%。

  • 壓縮 ES5
    • 可用之前說的

      UglifyPlugin

      ParallelUglifyPlugin

      插件進行配置,也可以啟動

      webpack

      時帶上

      --optimize-minimize

      參數,它會自動配置

      UglifyPlugin

      插件的預設參數。
  • 壓縮 ES6
    • 可用

      uglifyESPlugin

      插件進行壓縮,需要注意的是壓縮 ES6 代碼需要運作環境支援ES6文法才有意義。
  • 壓縮 CSS
    • 通過給

      css-loader

      Loader添加

      minimize

      參數進行壓縮:

      use:['css-loader?minimize']

CDN 加速

将不同的靜态資源放在不同域名的CDN下,可以利用

webpack

進行配置

使用 Tree Shaking

Tree Shaking 插件是為了剔除沒有用到的死代碼。

使用 Tree shaking 的前提是子產品化必須是 ES6 文法,因為其導入導出的路徑必須是靜态字元串,不能出現在代碼塊中。像 CommonJS 就不一樣:

require(x+y)

提取公共代碼

為什麼要提取?

因為如果每個頁面都包含大量公共代碼的話,會導緻:

  • 相同的資源被重複加載,浪費使用者流量和伺服器成本
  • 每個頁面需要加載的資源過大,影響頁面渲染

如何提取?

先将所有頁面中用到的基礎依賴庫(如react,react-dom)提取到單獨的一個檔案 base.js 中。然後再從所有頁面中提取出不包含base.js中代碼的公共代碼到common.js檔案中,然後将每個網頁剩餘的代碼單獨包裝成一個檔案。

如圖:

《深入淺出Webpack》學習筆記

提取公共代碼可利用 webpack 内置的 commonsChunkPlugin 插件

分割代碼以按需加載

像現在流行的單頁應用,将所有的功能整合到了一個 HTML 檔案中。其實每次隻是運用到其中的一部分功能,為了使用一部分功能而加載所有資源而使得網頁加載相對緩慢。為了解決此問題,則可以将整個頁面分成一個個小的功能,而根據功能之間的相關程度進行分類,把每一類合并成一個chunk,然後再按需加載每一個Chunk。

可以使用

import(*)

語句實作按需加載。

使用 Prepack

通過深入分析JavaScript代碼邏輯,預先執行代碼邏輯,然後将執行結果輸出,此技術還未成熟。

比如:

// 轉換前
function hello(){
	console.log("hello,world")
}
hello();

// 轉換後
console.log("hello,world")

           

開啟 Scop Hoisting

可以進一步的減少打包體積,通過将多個函數進行合并,但是前提是運用ES6文法的子產品。

可利用 webpack 内置的功能進行開啟

輸出分析

如果需要分析webpack 的詳細輸出資訊,可在指令行添加以下參數

  • --profile

    :記錄建構過程中的耗時資訊
  • --json

    :以 json 格式記錄輸出檔案資訊

如:

webpack --profile --json > state.json

>

為 LINUX 中的管道指令,就是将輸出結果存到

state.json

檔案中

可以通過以下兩種方式來可視化分析

state.json

檔案資訊

  • Webpack Analyse:一個web應用,登入其官網上傳 json 檔案即可分析
  • webpack-bundle-analyzer:一個全局插件,在

    state.json

    所在目錄下輸入

    webpack-bundle-analyzer

    即可分析

    state.json

原理

工作原理概括

核心概念

  • Entry:建構入口。
  • Ouput:建構輸出資訊。
  • Module:子產品,在 webpack 中每個檔案都當作是一個子產品。
  • Chunk:代碼塊,有多個子產品組成,用于代碼合并與分割。
  • Loader:子產品轉換器,Loader 根據需求将原有檔案轉成新的内容。
  • Plugin:Webpack 擴充插件,webpack 在建構的過程中會廣播一些事件,而 Plugin 則通過監聽相應的事件來擴充某些功能。

建構流程

主要分三大階段:

  • 初始化:啟動建構,讀取合并 webpack 配置參數,加載插件,初始化 compiler 對象。
  • 編譯:讀取 Entry 配置中的子產品,根據比對規則調用相應的 Loader 對其進行轉換,并找到其依賴的子產品進行遞歸解析轉換,并确定各個子產品之間的依賴關系。
  • 輸出:将有聯系的多個Module合并成一個chunk,再将每個chunk輸出成一個檔案并儲存在系統中,儲存資訊由 Output 配置決定。

編寫 Loader

簡單來說輸出一個轉換函數。

Npm Link 可用于調試開發本地的 Npm 子產品。

編寫 Plugin

以下有些常用的屬性和function總結如下:

  • compilation.chunk

    :數組類型,存放所有的

    chunk

  • chunk.forEachModule(module=>{})

    :周遊每個

    module

  • module.fileDependencies

    :數組類型,表示目前子產品所依賴的檔案路徑
  • chunk.files

    :數組類型,表示

    chunk

    輸出的檔案,有可能不隻一個檔案。
  • compilation.assets[filename].source()

    :表示

    filename

    輸出檔案的内容
  • compilation.fileDependencies

    :數組類型,表示檔案依賴清單
  • compiler.options.plugins

    :目前配置使用的所有插件清單

繼續閱讀