《深入淺出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 webpack@<version> -D
# 最新的體驗版
npm install webpack@beta -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 extract-text-webpack-plugin@next

使用 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,便於調試
  • watchwatchOptions:用於監聽文件改動,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
    • 可用之前說的 UglifyPluginParallelUglifyPlugin 插件進行配置,也可以啓動 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 內置的 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:當前配置使用的所有插件列表
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章