webpack系列之模块化原理

·  阅读 326

前言

在阅读本文之前需要对以下概念有相当清晰认知: 请前往 模块化

  • 1、模块化是什么?
  • 2、有哪些模块化标准?

webpack是什么?webpack是一款模块加载器兼打包工具。

  • 作为一款模块加载器,它能把所有的资源文件(JS、JSX、CSS、CoffeeScript、Less、Sass、Image等)都作为模块来使用和处理。
  • 作为一个模块打包工具,主要功能是打包资源文件并整合到一个包中,我们在开发时,只需要引用一个包文件,就能加载预先设计好的模块功能,同时允许以指定模块化规范输出。

那么作为一款模块加载器。在浏览器运行时,我想弄明白以下几个问题:

  • 1、webpack如何实现对各种不同JS模块规范的兼容处理?
  • 2、webpack如何实现对js类型文件之外的模块化处理的?

作为一个模块打包工具,在浏览器运行时,我想知道:

  • 1、webpack将打包资源文件并整合到一个包中之后,是如何按指定模块化规范输出的?

本文以打包结果为导向,对比我们的源码,探讨在浏览器端运行时,webpack输出的文件是如何处理js文件,css文件、图片、字体等,以及按需加载,以达到模块化的管理。

[一] 初始化webpack项目

1.1 新建一个文件夹webpack-demo,安装依赖。

npm init -y
npm install webpack webpack-cli -D
复制代码

1.2 配置打包的输入(entry)与输出(output)

新建webpack.config.js

module.exports = {
    entry:'./src/index.js', // 输入输出
    output: {
        path: path.resolve(__dirname, "./dist"), // 打包后的目录
        filename:"bundle.js"
    }
}  
复制代码

1.3 配置打包命令

在package.json增加

{
    scripts:{
      "build": "webpack --config webpack.config.js"
    }
}
复制代码

1.4 新建入口文件

src/index.js随便写点什么

const foo = function(){
    console.log('this is foo')
}
export default foo
复制代码

1.5 执行打包命令 npm run build

可以在根目录下找到dist/bundle.js

【二】分析bundle.js

将bundle.js copy 过来删除部分注释之后大致内容如下:

 (function(modules) { // webpackBootstrap
 	// 1. 定义一个模块缓存对象
 	var installedModules = {};

 	// 2. require 函数的实现
 	function __webpack_require__(moduleId) {

 		// 2.1 首先会检查模块缓存
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		// 2.2 缓存不存在时,创建并缓存一个新的模块对象,
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};

 		// 2.3 调用模块函数
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 		// 2.4 标记模块为已加载
 		module.l = true;

 		// 2.5 返回输出模块
 		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 = "";


 	// 3. 加载入口文件
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })({
    "./src/index.js":
    (function(module, __webpack_exports__, __webpack_require__) {
        "use strict";
        eval("__webpack_require__.r(__webpack_exports__);\nvar foo = function foo() {\n  console.log(\"this is foo\");\n};\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (foo);\n\n//# sourceURL=webpack:///./src/index.js?");
     })
})

复制代码

2.1 整体结构

webpack 作为模块加载器,打包结果bundle.js中的内容分为两部分,运行时代码和我们的源代码。原理是通过IIFE作为启动器,自执行函数体是webpack管理各个模块代码在浏览器端运行时所需的代码。自执行函数的参数就是我们的源代码。

(function(modules) { // webpackBootstrap
    statements
    // 此处 require 入口模块
})(
    // modules对象集合
    {
       path1:function(){ /*xxx.js源码*/ },
       path2:function(){ /*xxx.js源码*/ }     
    }
)
复制代码

简单来理解,webpack模块化机制是通过自执行函数(IIFE)启动,然后通过webpack自定义的 exports 和 require 来实现的各个依赖模块化管理。

2.2 require的实现

__webpack_require__函数 主要实现逻辑如下

  • 2.2.1 首先会检查模块缓存,如果有则返回缓存模块的export对象,即module.exports
  • 2.2.2 缓存不存在时,创建一个新的模块对象并放入缓存
  • 2.2.3 调用模块函数,同时将新的模块对象和__webpack_require__作为参数传入
  • 2.2.4 标记模块为已加载
  • 2.2.5 返回输出模块 exports对象

webpack_require 挂载一些方法和属性

-注释描述
webpack_require.m// expose the modules object暴露传进来的modules对象
webpack_require.c// expose the module cache暴露已经缓存起来的模块 installedModules(可能还未加载完)
webpack_require.d// define getter function for harmony exports将具体操作绑定到属性a的getter方法上
webpack_require.r// define __esModule on exports用于给__webpack_exports__添加一个 __esModule 为 true 的属性,表示这是一个 ES6 module
webpack_require.t//
webpack_require.n// getDefaultExport function for compatibility with non-harmony modules用于判断__webpack_exports__是否为es模块,当__esModule为true的时候,标识__webpack_exports__为es模块,那么module[default]默认返回export default对应的变量,否则返回直接返回export
webpack_require.o// Object.prototype.hasOwnProperty.call判断是否自有属性
webpack_require.p// __webpack_public_path__Webpack 配置中的 publicPath,用于加载被分割出去的异步代码
webpack_require.s// Load entry module and return exports保存了启动模块路径

2.3 export的实现(Es6 module)

2.3.1 源文件

// ========== 源文件 ========== start
// src/module-es6.js
export let moduleValue = "moduleValue"
export default function () {
  console.log("this is es6-module")
}

// src/index.js
import moduleDefault, { moduleValue } from "./module-es6.js";
// ========== 源文件 ========== end
复制代码

2.3.2 编译打包结果

// ========== 编译打包结果 ========== start
'./src/index.js':
(function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _module_es6_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( \"./src/module-es6.js\");\n\nconsole.log(_module_es6_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]);\n\n//# sourceURL=webpack:///./src/index.js?");

}),

'./src/module-es6.js':
/*! exports provided: moduleValue, default */
(function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"moduleValue\", function() { return moduleValue; });\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return f; });\nvar moduleValue = \"moduleValue\";\n\nfunction f() {\n  console.log(\"this is es6-module\");\n}\n\n//# sourceURL=webpack:///./src/module-es6.js?");
})
// ========== 编译打包结果 ========== end
复制代码

2.3.3 分析打包结果

// 2.3.3.1 先从module-es6.js 导出对象来分析(即 导出过程__webpack_require__函数的返回值 module.exports)
{
    default: f(), // 对应 export default 关键字
    moduleValue: 'moduleValue', // 对应 export 关键字
    __esModule:true  // 用于标识 es6模块
}

// index.js 中通过import引入es6模块 
// 2.3.3.2 import 实际上通过__webpack_require__函数引入
var _module_es6_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( \"./src/module-es6.js\")

// 2.3.3.3 对应变量的使用也转化为导出对象的属性访问
_module_es6_js__WEBPACK_IMPORTED_MODULE_0__['default']
_module_es6_js__WEBPACK_IMPORTED_MODULE_0__['moduleValue']
复制代码

2.4 export的实现(commonJs module)

2.4.1 源文件

// module-commonjs.js
exports.moduleValue1 = "moduleValue1";
exports.moduleValue2 = "moduleValue2";

// src.js
import moduleDefault, { moduleValue1, moduleValue2 } from "./module-commonjs.js";
console.log(moduleDefault);
console.log(moduleValue1, moduleValue2);
复制代码

2.4.2 编译打包结果

// ========== 编译打包结果 ========== start
"./src/index.js":

/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _module_commonjs_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module-commonjs.js */ \"./src/module-commonjs.js\");\n/* harmony import */ var _module_commonjs_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_module_commonjs_js__WEBPACK_IMPORTED_MODULE_0__);\n\nconsole.log(_module_commonjs_js__WEBPACK_IMPORTED_MODULE_0___default.a);\nconsole.log(_module_commonjs_js__WEBPACK_IMPORTED_MODULE_0__[\"moduleValue1\"], _module_commonjs_js__WEBPACK_IMPORTED_MODULE_0__[\"moduleValue2\"]);\n\n//# sourceURL=webpack:///./src/index.js?");
}),

"./src/module-commonjs.js":
(function(module, exports) {
eval("// module-commonjs.js\nexports.moduleValue1 = \"moduleValue1\";\nexports.moduleValue2 = \"moduleValue2\";\n\n//# sourceURL=webpack:///./src/module-commonjs.js?");
})
// ========== 编译打包结果 ========== end
复制代码

2.4.3 分析

// ========== 分析 ========== start
// 2.4.3.1、先从结果来看module-commonjs.js 导出对象(即 __webpack_require__函数的返回值 module.exports)
{
    moduleValue1: 'moduleValue1', 
    moduleValue2: 'moduleValue2'
}

// index.js 中通过import引入common js模块 
// 2.4.3.2 对应变量的使用也转化为 导出对象的属性访问
_module_commonjs_js__WEBPACK_IMPORTED_MODULE_0___default.a  // 通过__webpack_require__.n 获取默认模块
_module_es6_js__WEBPACK_IMPORTED_MODULE_0__['moduleValue1']
_module_es6_js__WEBPACK_IMPORTED_MODULE_0__['moduleValue2']

// ========== 分析 ========== end
复制代码

当然模块化标准不只是 CommoJs 和 ES Harmony,还有AMD、CMD等,由于篇幅原因就不在展开

【三】实现按需加载(引入异步模块)原理

3.1 引入异步模块module-dynamic

举个简单例子

// module-dynamic.js
export default function (msg) {
  console.log(msg);
}


// index.js
import("./module-dynamic").then((show) => {
  // 执行 show 函数
  show("Webpack");
});
复制代码

3.2 执行打包命令 npm run build

重新构建后会输出两个文件,分别是执行入口文件 bundle.js 和异步加载文件 0.bundle.js

3.2.1 0.bundle.js 内容如下

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{
    "./src/module-dynamic.js":
    (function(module, __webpack_exports__, __webpack_require__) {

    "use strict";
    eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function (msg) {\n  console.log(msg);\n});\n\n//# sourceURL=webpack:///./src/module-dynamic.js?");
    })
}]);
复制代码

3.2.2 重新构建之后bundle.js 内容如下

(function(){
      /***
   * webpackJsonp 用于从异步加载的文件中安装模块。
   * 把 webpackJsonp 挂载到全局是为了方便在其它文件中调用。
   *
   * @param chunkIds 异步加载的文件中存放的需要安装的模块对应的 Chunk ID
   * @param moreModules 异步加载的文件中存放的需要安装的模块列表
  
   */
// install a JSONP callback for chunk loading
function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];
    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    // 收集模块,将所有“chunkIds”标记为已加载
    var moduleId, chunkId, i = 0, resolves = [];
    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        
        if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    // 将异步模块内容插入到主文件模块对象 modules 中,以便后续直接使用
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonpFunction) parentJsonpFunction(data);

    while(resolves.length) {
        resolves.shift()();
    }

};

// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
    "main": 0
};

// script path function
function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + chunkId + ".bundle." + "f26a295c3de9e9af4369" + ".js"
}


 /**
   * 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件
   * @param chunkId 需要异步加载的 Chunk 对应的 ID
   * @returns {Promise}
   */
__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];


    // JSONP chunk loading for javascript
  // 去检查 chunkId 对应的 Chunk 是否安装成功,安装成功时才会存在于 installedChunks 中
    var installedChunkData = installedChunks[chunkId];
    if(installedChunkData !== 0) { // 0 means "already installed".

        // a Promise means "currently loading".
        // installedChunkData 不为空且不为0表示该 Chunk 正在网络加载中
        if(installedChunkData) {
            promises.push(installedChunkData[2]);
        } else {
            // setup Promise in chunk cache
            var promise = new Promise(function(resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            var script = document.createElement('script');
            var onScriptComplete;

            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }
            script.src = jsonpScriptSrc(chunkId);

            // create error before stack unwound to get useful stacktrace later
            var error = new Error();
            onScriptComplete = function (event) {
                // avoid mem leaks in IE.
                // 防止内存泄露
                script.onerror = script.onload = null;
                clearTimeout(timeout);
                var chunk = installedChunks[chunkId];
                if(chunk !== 0) {
                    if(chunk) {
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                        error.name = 'ChunkLoadError';
                        error.type = errorType;
                        error.request = realSrc;
                        chunk[1](error);
                    }
                    installedChunks[chunkId] = undefined;
                }
            };
            // 设置最长加载时间
            var timeout = setTimeout(function(){
                onScriptComplete({ type: 'timeout', target: script });
            }, 120000);
            script.onerror = script.onload = onScriptComplete;
            document.head.appendChild(script);
        }
    }
    return Promise.all(promises);
};

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

   "./src/index.js":
    (function(module, exports, __webpack_require__) {
eval("__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./module-dynamic */ \"./src/module-dynamic.js\")).then(function (show) {\n  // 执行 show 函数\n  show(\"Webpack\");\n});\n\n//# sourceURL=webpack:///./src/index.js?");

 })
})
复制代码

对比发现,旧的bundle.js 和引入异步的 bundle.js 非常相似,区别在于:

  • 1、定义了一个对象 installedChunks 作用是缓存动态模块。
  • 2、定义了一个辅佐函数jsonScriptSrc(),作用是根据chunkId 生产动态模块的URL
  • 2、多了两个核心方法,一个 __webpack_require__.e 函数用于加载被分割出去的,需要异步加载的chunk对应的文件,并返回一个promise对象,另一个webpackJsonCallback函数用于处理异步模块的安装;
  • 3、定义了一个 webpackJsonp=[] 全局变量,用于存储需要动态导入的模块。
  • 4、重写window.webpackJsonp数组的push方法为webpackJsonCallback,当执行webpackJsonp.push时,即通过webpackJsonCallback来实现异步文件加载完成之后模块的安装。

3.2.3 异步模块实现原理

index.js打包结果来分析

  1. index.js 通过编译之后,import函数实际通过调用__webpack_require__.e 并传入需要异步加载模块的chunkId(通过jsonpScriptSrc函数映射到实际文件加载路径),动态创建script,注册script onload事件,最后返回一个promise对象

  2. 待异步模块加载完成之后,执行刚加载完成的脚本, 而window[“webpackJsonp”].push实际指向的是webpackJsonpCallback 函数,完成以下三个事情:

  • 收集模块,将所有“chunkIds”标记为已加载
  • 将刚加载异步模块内容插入到主文件模块对象 modules 中,以便后续直接使用
  • rosolve 更新上一步返回的promise对象的状态
  1. 然后通过__webpack_require__访问到更新之后主文件模块对象 modules,获取到异步模块,继续执行后续代码逻辑

当然可以通过魔法注释方式指定输出文件名等

import(
  /* webpackChunkName: "my-chunk-name" */
  /* webpackMode: "lazy" */
  /* webpackExports: ["default", "named"] */
  './module-dynamic.js'
);
复制代码

此時,0.bundle.xxx.js 將被替换为 my-chunk-name.bundle.xxx.js

【四】webpack 如何处理非js文件

4.1 处理css

webpack 本身不具备处理除了js 以外的文件能力,css 文件需要通过插件、loader加载处理。有两种方案

  • 将css直接打包进js。之后通过js 动态创建style标签插入页面
  • 一般我们都不会把大段的css放进页面中,会将其放在独立的文件中,然后在我们的页面通过link中引用。

4.1.1 借助 style-loader 插入页面

安装依赖:style-loader css-loader

// 1、配置webpack .config.js
// 2、执行npm run build
// 3、查看打包结果 bunld.js
"./node_modules/css-loader/dist/cjs.js!./src/css/main.css":
(function(module, __webpack_exports__, __webpack_require__) {
     // ... css-loader加载main.css处理细节(包含css代码中可能又引入其它css文件)
 }),
 "./node_modules/css-loader/dist/runtime/api.js":
 (function(module, exports, __webpack_require__) {
    // ... 省略 css-loader 运行时
     }),
    
     "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":

     (function(module, exports, __webpack_require__) {
        // ... 省略 style-loader 运行时
     }), 
 
     "./src/css/main.css":
     (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    eval("__webpack_require__.r(__webpack_exports__); var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(\"./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js\"); var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__); var _node_modules_css_loader_dist_cjs_js_main_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(\"./node_modules/css-loader/dist/cjs.js!./src/css/main.css\");            var options = {};options.insert = \"head\";options.singleton = false;var update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_main_css__WEBPACK_IMPORTED_MODULE_1__[\"default\"], options); __webpack_exports__[\"default\"] = (_node_modules_css_loader_dist_cjs_js_main_css__WEBPACK_IMPORTED_MODULE_1__[\"default\"].locals || {});");
    
     })
// 4、分析打包结果 loader执行顺序自右向左
// 4.1 通过 css-loader 运行时api 加载main.css文件得到符合CommonJS 规范的对象
// 4.1 通过 style-loader 运行时api 将上一步得到css模块对象通过style标签动态插入html中
复制代码
  • css-loader 的作用是将 css 转换成 commonjs 对象,也就是样式代码会被放到 js 里面去了。
  • style-loader 插入样式是一个动态的过程,查看打包后的 html 源码并不会看到 html 有 style 样式的。style-loader 是代码运行时动态的创建 style 标签,然后将 css style 插入到 style 标签里面去

4.1.2 借助mini-css-extract-plugin 提取出单独的css文件

安装依赖:mini-css-extract-plugin css-loader

// 1、 配置webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, "css-loader"], // 从右向左解析原则
      }
     ]
  },
  plugins: [
      new MiniCssExtractPlugin({
      // 这里的配置和webpackOptions.output中的配置相似
      // 即可以通过在名字前加路径,来决定打包后的文件存在的路径
      filename: "css/[name].[hash].css",
      chunkFilename: "css/[id].[hash].css",
    })
  ]
}

// 2、index.js 引入main.css
import './main.css'

// 3、执行npm run build 分析打包文件
// 3.1 打包结果中增加一个 css/main.xxx.css 文件
// 3.2 index.html 头部通过link链接该路径下的css文件
复制代码

4.2 处理图片、字体

wepack处理图片、字体方案是通过url-loader,file-loader完成模块化转换的。大致原理如下:

借助url-loader,通过修改 webpack 配置让小于 10k 的图片或者字体文件在构建阶段自动转化为base64。如果图片较多,会发很多http请求,会降低页面性能。url-loader会将引入的图片编码,生成dataURl。相当于把图片数据翻译成一串字符。再把这串字符打包到文件中,最终只需要引入这个文件就能访问图片了。当然,如果图片较大,编码会消耗性能。因此url-loader提供了一个limit参数,小于limit字节的文件会被转为DataURl,大于limit的还会使用file-loader进行copy。

4.2.1 使用url-loader转base64编码

"./src/images/不你没懂.png":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (\"data:image/png;base64,......省略base64编码\");\n)})
复制代码

4.2.1 使用file-loader复制一份

file-loader本身并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置为他生成一个唯一的文件名。大致输出结果如下:

"./src/images/不你没懂.png":
(function(module, __webpack_exports__, __webpack_require__) {
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (__webpack_require__.p + \"img/不你没懂.adb0f5d9.png\");\n\n//# sourceURL=webpack:///./src/images/%E4%B8%8D%E4%BD%A0%E6%B2%A1%E6%87%82.png?");
})
复制代码

通过与__webpack_require__.p 拼接访问到实际文件路径

【五】补充——引入非模块

上面介绍了 webpack 打包视一切资源为模块的情况,但实际使用场景中,仍有一些不支持模块化规范的模块通过全局变量来使用,但我们希望它可以作为模块内容导出。

5.1 举个例子

// hello.js
var Hello = window.Hello = function(){
    console.log('hello from global Hello() function');
};

// index.js
var Hello = require('./hello.js');
Hello();
复制代码

打包执行之后可以看到,由于hello.js并没有导出模块,导致入口文件index.js在引用的时候报错,找不到Hello这个函数

5.2 使用exports-loader将某些全局变量作为模块内容导出

npm install exports-loader@1 --save-dev

import Hello from "exports-loader?exports=Hello!./hello.js";
复制代码

5.3 查看打包结果

"./node_modules/exports-loader/dist/cjs.js?exports=Hello!./src/hello.js":
(function(module, __webpack_exports__, __webpack_require__) {
    //.. 省略部分打包内容
  __webpack_require__.d(__webpack_exports__, \"Hello\", function() { return Hello; }
})
复制代码

【六】 输出指定模块化规范

webpack 提供了这几个字段来满足不同模块化方案输出

  • output.library: 指定导出的模块名。支持string或者object,它的值被如何使用会根据output.libraryTarget的取值不同而不同
  • output.libraryTarget: 支持string,作用是控制 webpack 打包的内容是如何暴露的,默认为var
暴露方式
暴露一个变量var、assign
通过对象属性暴露this、window、global、commonjs
模块定义系统commonjs2、amd、umd
  • output.libraryExport: 指定模块的某个属性作为导出对象

不你没懂.png

分类:
前端
标签: