webpack 快速入門 系列 —— 初步認識 webpack


其他章節請看:

webpack 快速入門 系列

初步認識 webpack

webpack 是一種構建工具

webpack 是構建工具中的一種。

所謂構建,就是將資源轉成瀏覽器可以識別的。比如我們用 less、es6 寫代碼,瀏覽器不能識別 less,也不支持 es6 的某些語法,這時我們可以通過構建工具將源碼轉成瀏覽器可以識別的 css 和 js。

webpack 是一種模塊化解決方案

以前,前端只需要寫幾個html、css、js就能完成工作,現在前端做的項目更加復雜,在性能、體驗、開發效率等其他方面,都對我們前端提出了更高的要求。

為了能按質按量的完成老板交代的任務,我們只能站在巨人的肩膀上,也就是引入第三方模塊(或稱為庫、框架、包),然后快速組裝我們的項目。

於是這就出現了一個項目依賴多個模塊的場景,只有這些模塊能相互通信,十分融洽的在一起,我們才能集中於一處發力把項目做好。

問題在於這些模塊不能很好的相處。如何理解?我們可以簡化上面的場景:現在我們有三個模塊,moduleA 要使用 moduleB,moduleB 要使用 moduleC。如果需要我們自己維護這三個模塊之間的依賴關系,可能就是有一點點麻煩;如果要維護數十個、上百個模塊之間的依賴關系呢,可能就很困難了。

於是就出現了各種模塊化解決方案。有人曾說 jQuery 之后前端最偉大的發明就是 requirejs,它是一個模塊化開發的庫;而 webpack 就是一種優秀的模塊化解決方案。

webpack 官方定義

webpack 是一個現代 JavaScript 應用程序的靜態模塊打包工具 —— 官方定義

模塊才是 webpack 的核心,所以下文先談談模塊,再分析 webpack 模塊化解決方案的原理。

淺談模塊

早期 js 是沒有模塊的概念,都是全局作用域,我們可能會這么寫代碼:

// a.js
var name = 'ph';
var age = '18';

// b.js
var name = 'lj';
var age = '21';

如果 html 頁面同時引入 a.js 和 b.js,變量 name 和 age 就會相互覆蓋。

為了避免覆蓋,我們使用命名空間,可能會這么寫:

// a.js
var nameSpaceA = {
  name: 'ph',
  age: '18'
}

// b.js
var nameSpaceB = {
  name: 'lj',
  age: '21'
}

雖然不會相互覆蓋,但模塊內部的變量沒有得到保護,a 模塊仍然可以更改 b 模塊的變量。於是我們使用函數作用域:

// a.js
var nameSpaceA = (function(){
  var name = 'ph';
  var age = '18';
  return {
    name: name,
    age: age,
  }
}())

// b.js
var nameSpaceB = (function(){
  var name = 'lj';
  var age = '21';
  return {
    name: name,
    age: age,
  }
}())

這里使用了函數作用域、立即執行函數和命名空間,這就是早期模塊的實現方式。更通俗的做法,例如 jQuery 會這么做:

// a.js
(function(window){
  var name = 'ph';
  var age = '18';
  window.nameSpaceA = {
    name: name,
    age: age,
  }
}(window))

之后又出現了各種模塊的規范,比如 AMD,代表實現是 requirejs、CommonJS,它的流行得益於 Node 采用了這種方式等等。

終於 es6 帶着官方的模塊語法(import和export)來了。

模塊化

模塊化就是將復雜的系統拆分到不同的模塊來編寫。帶來的好處有:

  • 重用。將一些通用的功能提取出來作為模塊,需要使用該功能的地方只需要通過特定方式引入即可。
  • 解耦。將一個1萬行的文件(模塊)分解成10個1千行的文件,模塊之間通過暴露的接口進行通信。
  • 作用域封裝。模塊之間不會相互影響。比如2個模塊都有變量count,變量count不會被對方模塊影響。

webpack 模塊化解決方案的原理

下面我們通過一個項目,從代碼層面上看一下 webpack 模塊化解決方案的原理。

首先初始化項目,並安裝依賴包。

// 創建項目
> mkdir webpack-example1
// 進入項目目錄。有的控制台可能是: cd webpack-example1
> cd .\webpack-example1\
// 使用 npm 初始化項目(會自動生成 package.json)
> npm init -y
// 安裝依賴包。雖然現在有 webpack 5,但筆者使用的是 webpack 4
// 因為有些構建功能所需要的 npm 包暫時不支持 webpack 5。
> npm i -D webpack@4
// 不安裝 webpack-cli,運行時會報錯,會提示需要安裝 webpack-cli
> npm i -D webpack-cli@3

接着在 webpack-example1/src 文件夾下創建三個模塊,模塊之間的關系是 index 依賴 b,b 依賴 c,內容如下:

// index.js
import './b.js'
console.log('moduleA')

// b.js
import './c.js'
console.log('moduleB')

// c.js
console.log('moduleC')

執行 npx webpack,會將我們的腳本 src/index.js 作為入口起點,然后會生成 dist/main.js:

// webpack 默認是生產模式,這里通過參數指定為開發模式
webpack-example1> npx webpack --mode development
Hash: cb88f1c065314d7a6a2c
Version: webpack 4.46.0
Time: 73ms
Built at: 2021-05-10 4:06:03 ├F10: PM┤
  Asset      Size  Chunks             Chunk Names
main.js  4.81 KiB    main  [emitted]  main
Entrypoint main = main.js
[./src/b.js] 39 bytes {main} [built]
[./src/c.js] 22 bytes {main} [built]
[./src/index.js] 39 bytes {main} [built]

Tip:Node 8.2/npm 5.2.0 以上版本提供的 npx 命令,可以運行 webpack 二進制文件(即 ./node_modules/.bin/webpack)

webpack-example1> .\node_modules\.bin\webpack
// 等於
webpack-example1> npx webpack

生成的 dist/main.js 就是打包后的文件(現在無需詳細的看 main.js 的內容):

/******/ (function(modules) { // webpackBootstrap
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ 		}
/******/ 	};
/******/
/******/ 	// define __esModule on exports
/******/ 	__webpack_require__.r = function(exports) {
/******/ 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ 		}
/******/ 		Object.defineProperty(exports, '__esModule', { value: true });
/******/ 	};
/******/
/******/ 	// create a fake namespace object
/******/ 	// mode & 1: value is a module id, require it
/******/ 	// mode & 2: merge all properties of value into the ns
/******/ 	// mode & 4: return value when already ns object
/******/ 	// mode & 8|1: behave like require
/******/ 	__webpack_require__.t = function(value, mode) {
/******/ 		if(mode & 1) value = __webpack_require__(value);
/******/ 		if(mode & 8) return value;
/******/ 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ 		var ns = Object.create(null);
/******/ 		__webpack_require__.r(ns);
/******/ 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ 		return ns;
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/
/******/ 	// Load entry module and return exports
/******/ 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })
/************************************************************************/
/******/ ({

/***/ "./src/b.js":
/*!******************!*\
  !*** ./src/b.js ***!
  \******************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _c_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./c.js */ \"./src/c.js\");\n/* harmony import */ var _c_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_c_js__WEBPACK_IMPORTED_MODULE_0__);\n\r\nconsole.log('moduleB')\n\n//# sourceURL=webpack:///./src/b.js?");

/***/ }),

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

eval("console.log('moduleC')\n\n//# sourceURL=webpack:///./src/c.js?");

/***/ }),

/***/ "./src/index.js":
/*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ \"./src/b.js\");\n\r\nconsole.log('moduleA')\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

/******/ });

只需要知道 main.js 與我們的源碼是等價的。我們可以通過 node 運行 main.js 驗證這個結論:

> node dist/main.js
moduleC
moduleB
moduleA

輸出了三句文案。

Tip:你也可以創建一個 html 頁面,通過 src 引用 dist/main.js,然后在瀏覽器的控制台下驗證,輸出內容應該也是這三句文案。

接着我們來看一下 webpack 模塊化解決方案的原理。在此之前我們先優化一下 main.js,核心代碼如下:

(function(modules){
    // 模塊緩存
    var installedModules = {};
    // 定義的 require() 方法,用於加載模塊
    // 與 nodejs 中的 require() 類似
    function __webpack_require__(moduleId) {
      // 如果緩存中有該模塊,直接返回
      if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
      }
          // 創建一個新的模塊,並放入緩存
      var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      };
          
      // 執行模塊函數
      // 並將 __webpack_require__ 作為參數傳入模塊,模塊就能調用其他模塊
      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

      // 標記此模塊已經被加載
      module.l = true;

      // 返回模塊的 exports
      return module.exports;
    }
    ...
    // 加載入口模塊
    return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
    // b 模塊
    "./src/b.js": (function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _c_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./c.js */ \"./src/c.js\");\n/* harmony import */ var _c_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_c_js__WEBPACK_IMPORTED_MODULE_0__);\n\r\nconsole.log('moduleB')\n\n//# sourceURL=webpack:///./src/b.js?");
    }),
    // c 模塊
    "./src/c.js": (function(module, exports) {
        eval("console.log('moduleC')\n\n//# sourceURL=webpack:///./src/c.js?");
    }),
    // index 模塊
    "./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _b_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./b.js */ \"./src/b.js\");\n\r\nconsole.log('moduleA')\n\n//# sourceURL=webpack:///./src/index.js?");
    })
});

很顯然,main.js 是一個立即執行函數。立即執行函數的實參是一個對象,里面包含了所有的模塊,key 可以理解成模塊名,value 則是准備就緒的模塊。如果模塊還需要引入其他模塊,比如 index.js 依賴於 b.js,則會有形參 webpack_require

現在我們大致理解了 webpack 模塊化解決方案的原理:

  1. 根據入口文件分析所有依賴的模塊,組裝好,封裝到一個對象中
  2. 將封裝好的對象作為參數傳給匿名函數執行
  3. 定義加載模塊的方法(webpack_require
  4. 加載並執行入口模塊(即入口文件)
  5. 依次加載執行依賴的其他模塊

Tip:webpack 又被稱為打包神器,筆者認為打包就是將多個模塊整成一個;你也可以賦予打包其他含義,比如構建。

核心概念

webpack 中的核心概念有:

  • entry。指定 webpack 的入口,可以指定單入口或多入口
  • output。打包后輸出的相關配置,例如指定輸出目錄等
  • mode。開發模式或生產模式
  • loader
  • plugin

前3個比較簡單,loader 和 plugin 單獨介紹

entry、output 和 mode 放在 loader 中一起介紹。

loader

根據 webpack 官方定義,webpack 在沒有特殊配置的情況下,只識別 javascript。但我們的前端除了 javascript,還有 css、圖片等其他資源。所以 webpack 提供了 loader 幫我們解決這個問題。

loader 是文件加載器,用於對模塊的源代碼進行轉換,實現的是文件的轉義和編譯。例如需要將 es6 轉成 es5,或者需要在 javascript 中引入 css 文件,就需要使用它。可以將它看作成翻譯官

下面我們就使用 loader 處理 css 文件。

首先我們得創建 webpack 配置文件(webpack-example1/webpack.config.js),這樣我們可以通過配置指定 loader、插件(plugin)等其他功能,更加靈活:

const path = require('path');

module.exports = {
  // 給 webpack 指定入口
  entry: './src/index.js',
  // 輸出
  output: {
    // 文件名
    filename: 'main.js',
    // 指定輸出的路徑。即當前文件所處目錄的 dist 文件夾
    path: path.resolve(__dirname, 'dist')
  },
  // loader 放這里
  module: {
    rules: [
      {
        // 匹配所有 .css 結尾的文件
        test: /\.css$/i,
        // 先經過 css-loader 處理,會將 css 文件翻譯成 webpack 能識別的
        // 接着用 style-loader 處理,也就是將 css 注入 DOM。
        use: ["style-loader", "css-loader"]
      },
    ]
  },
  // 指定為開發模式。webpack 提供了開發模式和生產模式
  // 如果不指定 mode,打包時會在控制台提示缺省 mode,並默認指定為生產模式
  mode: 'development'
};

Tip:配置文件參考 webpack v4 使用一個配置文件css-loader

安裝相關依賴包:

// 特意指定版本,否則可能由於不兼容而安裝失敗
> npm i -D css-loader@5 style-loader@2

在 src 下創建 a.css 和 index.html:

// a.css
body{color:red;}

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='../dist/main.js'></script>
</head>
<body>
    <p>我是紅色嗎</p>
</body>
</html>

設置一個運行 webpack 的快捷方式,需要修改 package.json 文件,在 npm scripts 中添加一個 npm 命令:

{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    // 新增
    "build": "webpack"
  },
}

運行 webpack 重新打包:

// 自定義命令通過”npm run + 命令“即可運行
> npm run build

最后通過瀏覽器打開 index.html,就可以看到頁面有紅色文字”我是紅色嗎“。

可能你會疑惑:為什么要在 index.js 中引入 a.css?其實你通過 c.js 引入 a.css 也是相同效果。

上文我們分析 webpack 原理時,知道 webpack 首先從入口文件開始,分析所有依賴的模塊,最后打包生成一個文件,生成的這個文件與我們的源碼是等價的。所以 a.css 必須要在依賴模塊中,否則最終生成的這個文件就不會包含 a.css。

換句話說,如果我們的資源需要被 webpack 打包處理,那么該資源就得出現在依賴中。

Tip:webpack 中一切皆模塊。webpack 除了能導入 js 文件,也能把 css、圖片等其他資源都當作模塊處理,只是需要相應的 loader 翻譯一下即可。

plugin

loader 用於轉換某些類型的模塊,而插件則可以用於執行范圍更廣的任務。包括:打包優化,資源管理,注入環境變量。

插件(plugin)可以幫助用戶直接觸及到編譯過程。plugin 強調一個事件監聽的能力,能在 webpack 內部監聽一些事件,並且能改變一些文件打包后輸出的結果。

目前我們需要自己創建一個 html 頁面,然后引用打包后的資源,感覺不是很方便,於是我們可以使用 html-webpack-plugin 這個包通過 plugin 簡化這一過程。

首先安裝依賴包 npm i -D html-webpack-plugin@4

接着給 webpack.config.js 增加兩處代碼:

// 增加 +
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  // +
  plugins: [
    new HtmlWebpackPlugin()
  ]
};

再次打包,會發現 build 文件夾下多出了一個文件(index.html),內容如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  <meta name="viewport" content="width=device-width, initial-scale=1"></head>
  <body>
  <script src="main.js"></script></body>
</html>

該文件自動引入打包后的資源(main.js)。瀏覽器訪問這個頁面(build/index.html),發現控制台正常輸出,但頁面是空白的。

如果我們需要在這個 html 頁面中增加一些內容,比如一句話,可以配置一個模板。

修改 webpack.config.js,指定模板為 src/index.html:

plugins: [
  new HtmlWebpackPlugin({
      // 指定模板
      template: 'src/index.html'
  })
],

修改模板(src/index.html)內容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=`, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <p>請查看控制台</p>
</body>
</html>

重新打包后:

> npm run build

> build  
> webpack
// 打包會生成一個hash。以后會使用到。
Hash: 0751d9e63f9e32eac13d
// webpack 的版本是 4.46.0
Version: webpack 4.46.0
// 構建所花費的時間
Time: 396ms
Built at: 2021-05-11 7:56:19 ├F10: PM┤
// 下面3行4列是一個表格
// Asset,打包輸出的資源(index.html 和 main.js)
// Size,輸出資源。 main.js 的大小是 17.3Kb
// Chunks,main [發射]
// Chunk Names,main
     Asset       Size  Chunks             Chunk Names
index.html  307 bytes          [emitted]
   main.js   17.3 KiB    main  [emitted]  main
Entrypoint main = main.js
[./node_modules/css-loader/dist/cjs.js!./src/a.css] 314 bytes {main} [built]
[./src/a.css] 322 bytes {main} [built]
[./src/b.js] 58 bytes {main} [built]
[./src/c.js] 22 bytes {main} [built]
[./src/index.js] 68 bytes {main} [built]
    + 2 hidden modules
Child HtmlWebpackCompiler:
     1 asset
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html] 560 bytes {HtmlWebpackPlugin_0} [built]

生成的 html 文件(dist/index.html)內容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=`, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <p>請查看控制台</p>
<script src="main.js"></script></body>
</html>

這樣,重新生成的 html 頁面就以我們的文件為模板,並自動引入打包后的資源。

webpack-dev-server

webpack-dev-server 提供了一個簡單的 web server,方便我們調試。

在 loader 這個示例上繼續做如下修改:

// 安裝依賴包
> npm i -D webpack-dev-server@3

// 修改配置文件 webpack.config.js
module.exports = {
  devServer: {
    // 默認打開瀏覽器
    open: true,
    // 告訴服務器從哪個目錄中提供內容
    // serve(服務) 所有來自項目根路徑下 dist/ 目錄的文件
    contentBase: path.join(__dirname, 'dist'),
    // 開啟壓縮 gzip
    compress: true,
    // 端口號
    port: 9000,
  },
};

// 修改 package.json,增加自定義命令
"scripts": {
  // +
  "dev": "webpack-dev-server"
},

執行 npm run dev 就會默認打開瀏覽器,頁面就是 src/index.html。

啟動 devServer 不會打包輸出產物,也就是不會生成 dist 目錄,而是存在於內存中。

修改 src 中的 html、js,保存后瀏覽器會自動刷新並顯示最新效果,十分方便。

:之前運行 npm run dev 報錯,后來將 webpack-cli 從版本4改成版本3,然后就能正常啟動服務了。

學習建議

不要執着於 API 和命令 —— API 當然也是需要看的哈。

因為 webpack 迭代速度比較快,api 也會相應的更新,以后 webpack 配置也會更簡單好用。

其他章節請看:

webpack 快速入門 系列


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM