webpack 快速入门 系列 —— 初步认识 webpack
阅读原文时间:2021年05月11日阅读:5

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 [email protected]
// 不安装 webpack-cli,运行时会报错,会提示需要安装 webpack-cli
> npm i -D [email protected]

接着在 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 [email protected] [email protected]

在 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 [email protected]

接着给 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 [email protected]

// 修改配置文件 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 配置也会更简单好用。