webpack

202 阅读3分钟

概述

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

首先我们创建一个文件夹,执行npm init 初始化工程后,执行

npm install webpack webpack-cli --save-dev

在工程根目录创建一个index.js

console.log('hello webpack')
npx 执行

npx是npm5.2版开始新增的命令,应用直接调用工程内部已经安装的模块

在此之前我们需要在package.json的scripts配置,然后执行npm run xx

执行

npx webpack --mode=development ./index.js
dist/main.js

执行完成了以后我们发现工程中多了一个dist/main.js,查看其代码发现一句console.log最终经过webpack打包生成了一段很长的代码

 (function(modules) {
 	
 	var installedModules = {};

 	function __webpack_require__(moduleId) {

 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};

 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 		module.l = true;

 		return module.exports;
 	}

 	__webpack_require__.m = modules;

 	__webpack_require__.c = installedModules;

 	__webpack_require__.d = function(exports, name, getter) {
 		if(!__webpack_require__.o(exports, name)) {
 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
 		}
 	};

 	__webpack_require__.r = function(exports) {
 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 		}
 		Object.defineProperty(exports, '__esModule', { value: true });
     };
     
 	__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;
 	};

 	__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;
 	};
 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

 	__webpack_require__.p = "";

 	return __webpack_require__(__webpack_require__.s = "./index.js");
 })

 ({
"./index.js":

 (function(module, exports) {

eval("console.log('hello webpack')\n\n//# sourceURL=webpack:///./index.js?");

 })

 });

我们来看一下这段代码 首先它是一个立即执行函数,内部有一个 __webpack_require__ 方法

modules参数为模块对象,包含了所有的模块,key是模块的路径,value是模块的代码

(function(modules){
	function __webpack_require__(){
    }
})({})
__webpack_require__
 (function(modules) {
 	//缓存对象,已经解析的模块存储在installedModules
 	var installedModules = {};

 	function __webpack_require__(moduleId) {
		//判断当前的模块id是否在缓存中,如果在则直接返回代码
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
        //创建当前的模块对象,写入缓存
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};
		//执行当前模块代码
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 		module.l = true;

 		return module.exports;
 	}
	//启动程序
 	return __webpack_require__(__webpack_require__.s = "./index.js");
 })

 ({
 	//modules对象
 });
依赖

刚刚这个例子我们的index.js只是单纯的console语句,实际场景下我们的项目的js是相互依赖的,我们跑一个依赖的情况观察下__webpack_require__ 是如何执行多个文件的

在工程根目录创建一个item.js

export const item = ['A','B','C']

在工程根目录创建一个list.js

import {item} from './item'
export const list = [1,2,3]

修改index.js

import {list} from'./list.js'
console.log(list)
console.log('hello webpack')

执行 npx webpack --mode=development ./index.js 我们观察dist/main.js modules对象

 ({

 "./index.js":
 (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _list_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./list.js */ \"./list.js\");\n\nconsole.log(_list_js__WEBPACK_IMPORTED_MODULE_0__[\"list\"])\nconsole.log('hello webpack')\n\n//# sourceURL=webpack:///./index.js?");

 }),

 "./item.js":
 (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"item\", function() { return item; });\nconst item = ['A','B','C']\n\n//# sourceURL=webpack:///./item.js?");

 }),

 "./list.js":
 (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"list\", function() { return list; });\n/* harmony import */ var _item__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./item */ \"./item.js\");\n\nconst list = [1,2,3]\n\n//# sourceURL=webpack:///./list.js?");

 })

 });

"./index.js"通过__webpack_require__ 调用了 "./list.js"

"./list.js"通过__webpack_require__ 调用了 "./item.js"

结合 __webpack_require__ 函数中的

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

我们可以得出结论:webpack通过将模块放入一个对象中,key为文件路径,value是一个函数,内部是其代码,对象传入自执行函数内,内部通过__webpack_require__ 函数来递归解析

webpack.config.js

webpack推荐创建一个webpack.config.js的文件来定义配置

const path = require('path')

module.exports = {
	//入口
    entry: './index.js',
    //出口
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js'
      }
}

执行

npx webpack --mode=development

成功之后在dist文件夹下生成了一个index.bundle.js

loader

loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript,或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件!

举个栗子

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js'
    },
    module: {
        rules: [
            { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader"}
        ]
      }
}

运行这个配置,webpack会调用babel-loader将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。这样我们就可以在项目中使用ES6 了。那么这个babel-loader是什么?

我们写一个简单的loader

修改index.js

console.log('hello webpack')

工程根目录下新建loader1.js

const loaderUtils = require('loader-utils')
module.exports = function (source) {
    console.log('>>>>>>loader1<<<<<<<<')
    //通过loader-utils模块获取webpack中对应options
    const optionsName = loaderUtils.getOptions(this).name || ''
    return source.replace(/hello/g, optionsName)
}

工程根目录下新建loader2.js

module.exports = function (source) {
    console.log('>>>>>>loader2<<<<<<<<')
    return source.replace(/webpack/g, '世界')
}

修改webpack.config.js

const path = require('path')
module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js'
    },
    module: {
        rules: [
            { 
                test: /\.js$/, 
                exclude: /node_modules/, 
                use: [{
                    loader: './loader1.js',
                    options: {
                        name: '您好'
                    }
                }, './loader2.js'],
            }
        ]
      }
}

运行 npx webpack --mode=development

node控制台输出

>>>>>>loader2<<<<<<<<
>>>>>>loader1<<<<<<<<

浏览器控制台输出

您好 世界

由此可以看到

loader其实是一个函数,根据test的匹配规则,将.js的文件读取出来作为参数传递给函数loader2,函数loader2处理完成,将内容作为参数传递给函数loader1,函数loader1最终返回代码

webpack通过倒序迭代loader函数(每个loader是职责单一的函数),最终返回处理好的代码

插件

webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。