webpack原理与实践

本文详细介绍了webpack的模块化演进,包括文件划分、命名空间和IIFE的优缺点,以及模块加载的问题和解决方案。重点讲解了webpack的打包过程、配置文件、Loader和Plugin的使用,以及Dev Server、模块热更新(HMR)、SourceMap等功能。同时,讨论了webpack项目优化,如Tree Shaking、Code Splitting和项目中遇到的问题及解决策略。
摘要由CSDN通过智能技术生成

webpack原理

模块化演进

1. 文件划分方式

<html lang="en">
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 直接使用全局成员
foo()// 可能命名冲突
data = [] // 数据可能会被修改
</script>
</html>

缺点:

  • 模块直接在全局工作,大量模块成员污染全局作用域
  • 没有私有空间,所有模块内的成员都可以在模块外部被访问或者修改
  • 一旦模块增多,容易产生命名冲突
  • 无法管理模块与模块之间的依赖关系
  • 在维护过程中也很难分辨每个成员所属的模块

2. 命名空间方式

只允许暴露一个全局对象,其他模块挂载在全局对象上

// module-a.js
window.moduleA = {
	method1: function() {
		console.log("method1");
	}
}
// module-b.js
window.moduleA = {
	data: 1,
	method1: function() {
		console.log("method1");
	}
}

只解决了命名冲突,其他问题依旧存在

3. IIFE

在2的基础上,变成立即执行函数,为模块提供私有空间

// module-a.js
(function () {
	var name = "module-a";
	function method1() {}
	window.moduleA = {
		method1: method1
	}
})()

// module-b.js
(function ($) { // 通过参数明显表明这个模块的依赖
	// 私有成员,通过闭包访问
	var name = "module-b";
	function method1() {}
	window.moduleB = {
		method1: method1
	}
})(jQuery)

解决了命名冲突和全局作用域污染以及模块之间的依赖关系

以上三种解决方式,只解决了模块的组织问题,模块加载的问题并未解决

模块加载的问题

通过在html里面script加载模块,这种方式让模块加载不受控制;
在出现模块未引入页面,或者引入的模块已经移除等情况,会影响程序

理想的方式: 在页面中引入一个JS入口文件,其余用到的模块可以通过代码控制按需加载

模块化规范

目前通过约定实现模块化,为了统一不同开发者,不同项目之间的差异,需要制定一个行业标准去规范模块化的实现方式。

  • 一个统一的模块化标准规范
  • 一个可以自动加载模块的基础库
  1. CommonJS规范:(同步模块)
    Node.js所遵循的模块规范,该规范约定一个文件就是一个模块,每个模块都有单独的作用域;通过module.exports 导出成员,require函数载入模块。
  2. AMD: 异步模块
// 定义模块
define(['jquery', './module2.js'], function($, module2) {
	// return 导出模块
	return {
		start: function() {}
	}
})
// 载入模块
require(['./modules/module1.js'], function(module1){
	module1.start()
})
  1. CMD

  2. 模块化标准规范
    Node.js环境中,遵循CommonJS规范组织模块
    浏览器环境,遵循ES Modules规范(es6)

模块打包工具

  • ES Modules 模块系统本身存在环境兼容问题,尽管如今主流浏览器的最新版本都支持这一特性,但目前无法保证用户的浏览器使用情况
  • 模块化的方式划分出来的模块文件过多,而前端应用又运行在浏览器中,每一个文件都需要单独从服务器请求回来,零散的模块文件必然会导致浏览器的频繁发送网络请求,影响应用的工作效率。
  • 随着应用日益复杂,前端应用开发过程中,除了Js代码需要模块化,HTML和CSS这些资源文件也需要被模块化,也应该看作前端应用中的模块,只不过种类和用途跟JS不同。
  1. 编译:(babel 转译代码新特性) 解决环境兼容问题
  2. 打包:模块打包(在开发阶段必要,组织代码)
  3. 不同种类资源的模块打包(1,2,3webpack)(1,2 gulp)

webpack静态模块打包器(打包工具),发展成对整个前端项目的构建系统

webpack

  • webpack作为一个模块打包工具,本身可以实现模块化打包,通过webpack可以将零散的JS代码打包到一个JS文件中
  • 对于环境兼容,webpack可以在打包过程中通过Loader对其实现编译转换,然后再进行打包
  • 对于不同类型的前端模块,webpack支持在JS中以模块化的方式载入任意类型的资源文件;例如通过webpack在js中加载css文件,被加载的css文件将会通过style标签的方式工作
  • 其他
  • 还具备代码拆分能力,能够将应用中的所有模块按需分块打包;不用担心全部代码打包到一起,产生单个文件过大,导致加载慢的问题。

webpack打包过程详解

webpack版本: 5.61.0核心模块
webpack-cli 用于在命令行中调用webpack, (它所提供的命令行程序存在于node_modules/.bin文件中)

npx webpack
在执行过程中,webpack默认会自动从src/index.js文件开始打包
然后根据代码中的模块导入操作,自动将所有用到的模块打包到一起
在根目录下生成一个dist目录,打包结果就会存在dist/main.js中
在html文件中引入打包后的文件

// bundle.js
(() => { // webpackBootstrap(打包到同一个文件,并且提供基础代码,让模块与模块之间的依赖关系还可以保持)
	var __webpack_modules__ = ({
	// key 模块路径
	 "./node_modules/babel-loader/lib/index.js??clonedRuleSet-2[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./src/App.vue?vue&type=script&lang=js&":
	 ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
// eval执行打包过的模块字符串代码
// __webpack_require__.r r函数的作用是为了给导出对象添加标记
/*
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {"default": () => (__WEBPACK_DEFAULT_EXPORT__) });
var _compB_vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/compB.vue");
const __WEBPACK_DEFAULT_EXPORT__ = ({
    name: 'App',
    components: {
        CompB: _compB_vue__WEBPACK_IMPORTED_MODULE_0__["default"]
    },
    data: function data() { 
        return {
            message: '',    
            messageFromBus: '',      
            sender: ''    
        };  
    }, 
    mounted: function mounted() {    
        var _this = this;    
        this.$bus.$on('sendMessage', function (obj) {     
            // 通过eventBus监听sendMessage事件           
            var sender = obj.sender,          
            message = obj.message;      
            _this.sender = sender;      
            _this.messageFromBus = message;    
        });  
    },  
    methods: {    
        sendMessage: function sendMessage() {      
            this.$bus.$emit('sendMessage', {        
                // 通过eventBus触发sendMessage事件             
                sender: this.$options.name,        
                message: this.message      
            });    
        }  
    }
});
*/
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n/* harmony import */ var _compB_vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./compB.vue */ \"./src/compB.vue\");\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({\n  name: 'App',\n  components: {\n    CompB: _compB_vue__WEBPACK_IMPORTED_MODULE_0__[\"default\"]\n  },\n  data: function data() {\n    return {\n      message: '',\n      messageFromBus: '',\n      sender: ''\n    };\n  },\n  mounted: function mounted() {\n    var _this = this;\n\n    this.$bus.$on('sendMessage', function (obj) {\n      // 通过eventBus监听sendMessage事件     \n      var sender = obj.sender,\n          message = obj.message;\n      _this.sender = sender;\n      _this.messageFromBus = message;\n    });\n  },\n  methods: {\n    sendMessage: function sendMessage() {\n      this.$bus.$emit('sendMessage', {\n        // 通过eventBus触发sendMessage事件     \n        sender: this.$options.name,\n        message: this.message\n      });\n    }\n  }\n});\n\n//# sourceURL=webpack:///./src/App.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-2%5B0%5D.rules%5B0%5D.use%5B0%5D!./node_modules/vue-loader/lib/index.js??vue-loader-options");

/***/ }),
	});
	// The module cache
	var __webpack_module_cache__ = {};
	// The require function
	function __webpack_require__(moduleId) {
 		// Check if module is in cache
 		var cachedModule = __webpack_module_cache__[moduleId];
 		if (cachedModule !== undefined) {
 			return cachedModule.exports;
		}
 		// Create a new module (and put it into the cache)
 		var module = __webpack_module_cache__[moduleId] = {
 			// no module.id needed
 			// no module.loaded needed
 			exports: {}
 		};
 	
 		// Execute the module function
 		// 传入创建的模块,模块导出对象和require, 为了让函数内部可以导入和导出对象
 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
 	
 		// Return the exports of the module
		return module.exports;
 	}
})()

	// 用立即执行函数给__webpack_require__增加一些数据和工具函数
 	 	/* webpack/runtime/make namespace object */
 	(() => {
 		// define __esModule on exports 在导出对象上添加__esModule标记
 		__webpack_require__.r = (exports) => {
 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 			}
 			Object.defineProperty(exports, '__esModule', { value: true });
 		};
 	})();
	// startup
 	// Load entry module and return exports
 	// This entry module can't be inlined because the eval devtool is used.
 	var __webpack_exports__ = __webpack_require__("./src/main.js");

webpack配置文件

注: webpack4以后的版本支持零配置的方式直接启动打包,修改配置则在项目根目录下新建webpack.config.js文件作为webpack配置文件;
webpack.config.js运行在node.js环境,需要以commonJS的方式导入文件,导出成员,可以使用ode.js内置模块


  1. 配置文件支持智能提示: 类型注释
// ./webpack.config.js
// 只是为了导入webpack配置对象的类型,目的是为了标注config对象的类型
//运行webpack时先注释掉这里
import {Configuration} from 'webpack';
/**
 * @type {Configuration}
 */
const config = {
}
module.exports = config;

这些代码只是给vscode用的,运行webpack时先注释掉import语句,因为node.js环境默认还不支持import语法

  1. webpack工作模式:针对不同环境的三组预设配置
    **production模式:**启动内置优化插件,自动优化打包结果,打包速度偏慢;
    **development模式:**自动优化打包速度,添加一些调试过程中的辅助插件便于更好的调试错误;
    **none模式:**运行最原始的打包,不做额外处理,这种模式一般在需要分析我们的打包结果时会用到。

修改工作模式的方式:
通过cli --mode 参数传入;
配置文件设置mode属性

Loader实现特殊资源加载

webpack内部默认只能处理JS代码,也就是说,会把打包过程中的所有文件当作JS代码去解析。

下图为引入css文件时打包结果,css语法不符合js语法所以报错
在这里插入图片描述
webpack是使用Loader去处理每一个模块的
通过npm先去安装Loader,在配置文件中添加对应的配置

module.exports = {
	module: {
		rules: [
			{
                test: /\.(s[ca]ss|css)$/,
                use: ['style-loader', 'css-loader', 'sass-loader']
                // 从后向前执行loader,css-loader是将css模块加载到JS代码中,并不会使用这个模块,style-loader是将css-loader的处理结果通过style标签引入html页面中
            },
		]
}

在JS中加载其他资源

在入口文件中通过import去加载其他资源
原因是: 假设页面开发上的某个局部功能,用到一个样式模块和图片文件;
若是将这些资源文件单独引入Html中,然后再到js中添加对应的逻辑代码;如果后期局部功能不用了

  1. 需要同时删除js代码和html中的文件引入;
  2. webpack,所有资源的加载都是由JS代码控制,后期只需维护JS代码一条线

开发一个Loader(思路)

实现一个能加载.md文件 (markdown语法)的loader

markdown -> markdown-loader ->html

分析: 该markdown-loader需将.md 文件加载到JS代码中,

  1. 直接在markdown-loader中返回,由marked 解析markdown语法,变成html字符串,
  2. 在markdown-loader中返回marked 解析markdown语法后的html,之后可以由其他的Loader去处理这里得到的结果(html-loader)

实现:

  1. 可以在项目根目录实现一个markdown-loader.js文件,之后再发布到npm上去;
  2. 安装marked,将markdown解析为html的模块
  3. 安装完成后,在markdown-loader.js中导入该模块去解析
// ./markdown-loader.js
module.exports = source => {
    console.log(source);
    return 'hello loader~'
}

在这里插入图片描述

// ./markdown-loader.js
module.exports = source => {
    console.log(source);
    // 必须返回一个JS代码字符串,因为bundle中通过eval去执行返回结果
    return "console.log('hello loader~')";
}
// 打包之后bundle.js中的模块
var __webpack_modules__ = ({

/***/ "./readme.md": (() => {
// 打包结果就是将Loader返回的结果拼接至此
eval("console.log('hello loader~')\n\n//# sourceURL=webpack:///./readme.md?");
 }),

/***/ "./src/main.js":
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _readme_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../readme.md */ \"./readme.md\");\n/* harmony import */ var _readme_md__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_readme_md__WEBPACK_IMPORTED_MODULE_0__");;
 })
});

在这里插入图片描述

// 非模块内使用CommonJS标准
const {marked} = require('marked');
module.exports = source => {
    var html = marked(source);
    console.log(source);
    html = JSON.stringify(html); // 转义内部的换行符等
    return `export default ${html}`;
    // return `module.exports = ${html}`;
}

另外一种实现方式

const {marked} = require('marked');
module.exports = source => {
    var html = marked(source);
   	return html;
}

plugin

插件最常见的应用场景:

  • 实现自动在打包之前清除dist目录(上次打包的结果)【clean-webpack-plugin】

  • 自动生成应用所需的html文件【html-webpack-plugin】

  • 根据不同环境为代码注入类似API地址这种可能变化的部分

  • 拷贝不需要参与打包的资源文件到输出目录

  • 压缩webapck打包完成后输出的文件

  • 自动发布打包结果到服务器实现自动部署

  • 【clean-webpack-plugin】: 每次完成打包之前,自动清理dist目录,每次打包过后,dist目录只会存在那些必要文件
    在这里插入图片描述

  • 【Html-webpack-plugin】
    在这里插入图片描述
    在这里插入图片描述
    html-webpack-plugin插件除了自定义输出文件内容,同时输出多个html文件也是一个非常常见的需求(再去添加一个html-webpack-plugin的实例,更改filename)

  • 【copy-webpack-plugin】
    在这里插入图片描述

module.exports = {
plugins: [
        new CopyWebpackPlugin(['public']), // 需要拷贝的目录或路径
        new HtmlWebpackPlugin({
            template: './index.html',
            title: '',
            // 和模板设置是一样的
            meta: {}
        })
    ]
}

开发一个插件

** plugin插件机制:**webpack几乎为每一个环节都埋下了一个钩子,开发插件时,就可以通过往这些不同的钩子上去挂载不同的任务,扩展webpack的能力。

功能: 自动清除webpack打包结果中的注释

分析:

  • webpack要求我们的插件必须是一个函数或者是一个包含apply方法的对象。

  • 这里把它定义成一个类型,在这个类型当中去定义一个apply方法,在使用这个插件时,就可以通过这个类型来去创建一个实例对象。

  • compiler: 包含我们此次构建过程当中所有的配置信息
    通过这个对象去注册钩子函数的

  • 任务执行的时机: webpack要生成的bundle.js文件内容明确过后
    这个钩子会在webpack即将向输出目录输出文件时执行
    在这里插入图片描述
    方案:
    通过compiler对象的hooks属性访问到emit钩子,再通过tap方法注册一个钩子函数;
    tap方法接受两个参数:

  • 第一个是插件的名称

  • 第二个是要挂载到这个钩子上的函数, 该函数可以接受一个compilation的对象参数(可以理解为此次运行打包的上下文,所有打包过程产生的结果都会放到这个对象当中),可以通过compilation.assets去获取即将要写入输出目录的资源文件信息

class RemoveCommentsPlugin {
    apply(compiler) {
        compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {
            for(const name in compilation.assets) {
                if (name.endsWith('.js')) {
                    // source()返回文件内容
                    const content = compilation.assets[name].source();
                    // 正则取消注释
                    const noComments = content.replace(/\/\*{2,}\/\s?/g,'');
                    // 重新赋值,webpack要求格式
                    compilation.assets[name] = {
                        source: () => noComments,
                        size: () => noComments.length
                    }
                }
            }
        })
    }
}

module.exports = RemoveCommentsPlugin;

compiler 钩子
Compiler 模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例。 它扩展(extends)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。

在为 webpack 开发插件时,你可能需要知道每个钩子函数是在哪里调用的。想要了解这些内容,请在 webpack 源码中搜索 hooks..call。

监听(watching)
Compiler 支持可以监控文件系统的 监听(watching) 机制,并且在文件修改时重新编译。 当处于监听模式(watch mode)时, compiler 会触发诸如 watchRun, watchClose 和 invalid 等额外的事件。 通常在 开发环境 中使用, 也常常会在 webpack-dev-server 这些工具的底层调用, 由此开发人员无须每次都使用手动方式重新编译。 还可以通过 CLI 进入监听模式。

compiler hooks

仔细去过一遍插件的官方说明,查看还有没有其他特别用法;具体需求,提炼你关键字去搜索对应插件

webpack运行机制和核心工作原理

webpack在整个打包过程中:

  • 通过Loader处理特殊类型的资源的加载
  • 通过Plugin实现各种自动化的构建任务,如自动压缩,自动发布
  1. 根据配置,找到指定的入口文件,根据代码中出现的imort或者require之类的语句解析这个文件所依赖的一些资源模块,然后分别去解析每个资源模块的依赖,最终形成了整个项目中所有用到的文件之间的依赖关系树
  2. webpack会递归这个依赖树,找到每一个节点对应的资源文件
  3. 然后根据配置选项中的Loader配置,去加载模块,将加载后的结果放入bundle.js中,从而实现整个项目的打包。
  4. 对于项目中一些无法通过JS代码表示的一些资源模块,如图片和字体,一般loader会将他们单独作为资源文件拷贝到输出目录,然后将这个资源文件对应的访问路径作为这个模块的导出成员暴露给外部

整个工作过程的细节,需要深入以上的每一个环节,落实到代码层面,有针对性的查阅源代码

在这里插入图片描述

  • webpack Cli 启动打包流程
    wbpack-cli作用就是将cli参数和webpack配置文件中的配置整合,得到一个完整的配置对象;(入口文件 bin/cli.js文件)
  • 解析 cli参数【通过命令行传入的参数】
  • 将命令行参数转换为webpack的配置选项对象【首先为传递过来的命令行参数设置了一些默认值,然后判断命令行参数中是否制定了具体的配置文件路径,指定了就去加载指定的配置文件,否则需要根据默认配置文件的加载规则去找到一个具体的配置文件,将配置文件中的配置和cli参数里面的配置合并到一起,重复优先使用cli参数,最终得到一个完整的配置选项】
  1. 就开始加载webpack的核心模块,传入配置选项,创建compiler对象【最核心的对象,负责完成整个项目的构建工作】(入口文件lib/webpack.js)

webpack.js文件导出的就是一个用于去创建compiler对象的一个函数【首先校验了一下从外部传递过来的option参数是否符合要求;判断options类型,创建数组对应的多入口和对象对应的单一入口的compiler对象;之后webpack去注册我们在配置当中的每一个插件(webpack生命周期就开始了,必须先注册,才能确保每一个插件中的钩子函数能被命中);判断是否启用监视模式,启动构建(compile.watch || compile.run);】

// webpack-cli  核心代码

// webpack webpack.js
const getValidateSchema = memoize(() => require("./validateSchema"));
const createMultiCompiler = (childOptions, options) => {	
	// 1. 数组通过map去调用单个createCompiler去生成多个compiler
	const compilers = childOptions.map(options => createCompiler(options));
	const compiler = new MultiCompiler(compilers, options);
	for (const childCompiler of compilers) {
		if (childCompiler.options.dependencies) {
			compiler.setDependencies(
				childCompiler,
				childCompiler.options.dependencies
			);
		}
	}
	return compiler;
};

const createCompiler = rawOptions => {
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
	const compiler = new Compiler(options.context);
	// 1. 将配置信息绑定到compiler上
	compiler.options = options;
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);
	// 2. 注册已配置的插件
	if (Array.isArray(options.plugins)) {
		for (const plugin of options.plugins) {
			if (typeof plugin === "function") {
				plugin.call(compiler, compiler);
			} else {
				plugin.apply(compiler);
			}
		}
	}
	applyWebpackOptionsDefaults(options);
	// 3. 触发特定的Hook
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();
	new WebpackOptionsApply().process(options, compiler);
	compiler.hooks.initialize.call();
	return compiler;
};
const webpack = 
	(options, callback) => {
		const create = () => {
			//1. 首先校验了一下从外部传递过来的option参数是否符合要求
			if (!webpackOptionsSchemaCheck(options)) {
				getValidateSchema()(webpackOptionsSchema, options);
			}
			/** @type {MultiCompiler|Compiler} */
			let compiler;
			let watch = false;
			/** @type {WatchOptions|WatchOptions[]} */
			let watchOptions;
			//2. 判断options类型
			if (Array.isArray(options)) {
			// 2.1 options是数组--多路打包
			// 也就是说webpack支持同时开启多路打包,配置数组中每一个成员就是一个独立的配置选项
				/** @type {MultiCompiler} */
				compiler = createMultiCompiler(
					options,
					/** @type {MultiCompilerOptions} */ (options)
				);
				watch = options.some(options => options.watch);
				watchOptions = options.map(options => options.watchOptions || {});
			} else {
			// 2.2 options是对象 -- 单线打包
				const webpackOptions = /** @type {WebpackOptions} */ (options);
				/** @type {Compiler} */
				// 创建compiler
				compiler = createCompiler(webpackOptions);
				watch = webpackOptions.watch;
				watchOptions = webpackOptions.watchOptions || {};
			}
			return { compiler, watch, watchOptions };
		};
		if (callback) {
			try {
				const { compiler, watch, watchOptions } = create();
				// 3. 配置选项中是否启用监视模式
				if (watch) {
					// 3.1 监视模式调用compiler.watch方法启动构建
					compiler.watch(watchOptions, callback);
				} else {
					// 3.2 非监视模式,compiler.run启动构建
					compiler.run((err, stats) => {
						compiler.close(err2 => {
							callback(err || err2, stats);
						});
					});
				}
				return compiler;
			} catch (err) {
				process.nextTick(() => callback(err));
				return null;
			}
		} else {
			const { compiler, watch } = create();
			if (watch) {
				util.deprecate(
					() => {},
				)();
			}
			return compiler;
		}
	}
);
// compiler.run方法,在webpack/lib/compiler.js
run(callback) {
const run = () => {
	// 1. 先是触发了beforeRun 和run两个钩子
			this.hooks.beforeRun.callAsync(this, err => {
				if (err) return finalCallback(err);

				this.hooks.run.callAsync(this, err => {
					if (err) return finalCallback(err);

					this.readRecords(err => {
						if (err) return finalCallback(err);
	// 2. 调用compile方法,真正去编译项目
						this.compile(onCompiled);
					});
				});
			});
		};
}
// compile方法
compile(callback) {
		const params = this.newCompilationParams();
		this.hooks.beforeCompile.callAsync(params, err => {
			if (err) return callback(err);

			this.hooks.compile.call(params);
		// 1. 内部主要就是创建了一个compilation对象(理解为一次构建的上下文对象,包含了这次构建过程中全部的资源和一些额外的信息)
			const compilation = this.newCompilation(params);

			const logger = compilation.getLogger("webpack.Compiler");

			logger.time("make hook");
			// 2. 整个构建过程当中最核心的make阶段,主体目标:根据entry配置找到入口模块,开始依次递归除所有依赖,形成依赖关系树,然后将递归到的每个模块交给不同Loader处理
			// 此处采用事件触发的机制,让外部去监听这个make事件的地方开始执行
			this.hooks.make.callAsync(compilation, err => {
			});
			// 全局搜索用到make.tap的地方去分析
		});
	}

在这里插入图片描述

Dev Server

提高开发效率

理想的开发环境:

  • 【必须能使用Http服务运行而不是文件形式预览】(1. 更接近生产环境;2. 项目可能需要使用ajax之类的API,以文件形式访问会产生诸多问题)
  • 【在修改完代码后,webpack能够自动完成构建,浏览器可以即时显示最新 的运行结果】(减少开发过程中的额外重复操作)
  • 【能提供sourceMap支持。快速定位错误,调试应用】(运行过程出现的错误可以快速定位到源代码中的位置,而不是打包结果中的位置)

webpack实现了以上的功能,所以只需增强使用webpack的开发体验。

webpack提供的watch模式

watch模式下:webpack完成初次构建后,项目中的源文件会被监视,一旦发生任何改动,webpack都会自动重新运行打包任务。
具体用法:启动webpack时,添加一个--watch的cli参数,webpack就会以监视模式启动运行,在打包完成后,cli不会立即退出,他会等待文件变化再次工作,直到手动结束或出现不可控错误。

browserSync:浏览器实现webpack打包后,文件更新自动刷新浏览器
由它代替server的启动
原理: webpack 监视源代码的变化,自动打包的dist目录当中,dist目录又会被browserSync监听,dist一旦变化就会自动刷新浏览器。【自动编译,自动刷新浏览器 】在这里插入图片描述

webpack-dev-server

**webpack提出的一款开发工作,它提供了一个开发服务器,并且将自动编译和自动刷新浏览器等一系列功能全部集成,**目的是提高开发者的开发效率。

在这里插入图片描述
webpack-dev-server为了提高工作效率,并没有把打包结果存入磁盘中,而是暂时存放在内存中。
在这里插入图片描述

webpack-dev-server配置选项

  1. devServer
module.exports = {
	devServer: {
		contentBase: ['public'] // 额外的静态资源路径
		// "webpack-dev-server": "^4.4.0"的版本
		static: {
			directory: ''
		}
	}
}

静态资源访问: webpack-dev-server默认会将构建结果和输出文件全部作为开发服务器的资源文件,只要通过webpack打包能够输出的文件都可以被直接访问。但如果还有一些没有参与打包的静态文件也需要作为开发服务器的资源被访问,就需要额外配置通过webpack-dev-server。

注: 之前把未参与打包的静态文件通过copy-webpack-plugin复制到输出目录,实际使用webpack时,都会把这个插件留在上线前的那一次打包中使用,开发过程一般不会用它。

  1. proxy

本地开发服务器,应用在开发阶段独立运行在webpack-dev-server提供的localhost本地的一个端口,后端服务处于另一个地址,部署后又会和后端地址同源 。

问题:在实际生产环境中能够直接访问的API,回到开发环境后,再次访问这些API就会产生跨域请求问题。
解决:解决这种开发阶段跨域请求问题最好的办法,就是在开发服务器中配置一个后端API的代理服务,也就是把后端接口服务代理到本地的开发服务地址。

module.exports = {
	devServer: {
		proxy: {
			'/api': {
				target: 'https://lalal.com',
				pathRewrite: {'^/api': ''} // 代理路径重写,正则替换
				changeOrigin: true // 确保请求的主机名是lalal.com
			}
		}
	}
}

模块热更新(HMR)

浏览器自动刷新导致页面状态丢失。希望实现的是页面不刷新的情况下,代码
也可以及时更新到浏览器页面中。

HMR全称 Hot Module Replacement “模块热替换”,可以实现只将修改过后的模块实施替换到应用中,而不必刷新整个应用。

开启HMR:HMR已经集成在webpack模块中,不需再单独安装模块;在运行webpack-dev-server命令时,通过–hot参数去开启这个特性。或者在配置文件中添加对应配置。
这里需要配置两个地方:

  • devServer的hot属性设置为true
  • 需要载入一个插件,是webpack内置插件,hotModuleReplacementPlugin

此时就可以实现css模块的热更新了,但js模块依旧会刷新整个页面
原因:

  • HMR不像其他的一些特性可以开箱即用,它需要手动通过代码去处理当模块更新过后,该如何把更新后的模块去替换到页面中。
  • 样式文件的修改可以直接热更新,是因为样式文件是经过Loader处理的,在style-loader中就已经自动处理了样式文件的热更新,不需要我们额外手动处理。
  • 样式可以自动处理,是因为样式模块更新后,只需把更新后的css即时替换到页面中,它就可以覆盖掉之前的样式,从而实现更新。
  • 脚本手动处理,是因为JS模块的编写是没有任何规律的,有可能导出的是对象,字符串,函数等,webpack不知道如何去处理更新后的模块,也就没有办法实现一个可以通用所有情况的模块热替换方案。
  • 在使用框架时,项目中的每个文件就有了规律,框架内部都会实现通用的替换操作。
module.exports = {
	devServer: {
        // 开启hmr特性,如果资源不支持hmr会回退自动刷新
        hot: true,
        // 只使用hmr,不会回退到自动刷新
        // hotOnly: true
    },
    plugins: [
		new webpack.HotModuleReplacementPlugin()
	]
}

HMR热替换API

HMR 为JS 提供了用于去处理热替换的API。

// main.js
// 第一个参数是模块路径,第二个参数是模块更新时的处理函数
module.hot.accept('./index', () => {
	// 当./index.js更新,自动执行该函数
	console.log('update');
})
// main.js 
// 处理一个输入框的热更新
import {create} from '../Hmr/create';

const edit = create();
document.body.appendChild(edit);

let old = edit;
module.hot.accept('../Hmr/create', () => {
    console.log(old.value)
    const value = old.value;
    document.body.removeChild(old);
    old = create();
    old.value = value;
    document.body.appendChild(old);
})
// create.js
export const create = function() {
    let input = document.createElement('input');
    console.log('1223')
    return input;
}

常见问题:

  1. 热替换的处理函数中出错,HMR失效,也会回退导致页面自动刷新。【这时可以使用hotOnly】
  2. module.hot.accept()是HMR插件提供的方法,代码中可以判断module.hot的存在来判断是否添加了插件,防止报错
  3. 添加的额外代码在打包时清除掉有关HMR的配置,打包结果中会自动清除我们额外添加的处理热更新的代码。

SourceMap【开发调试】

需要编译的前端项目如何调试,报错?

(.map)映射转换后的代码和源代码之间的关系
并且可以通过sourceMap文件(.map)逆向解析

配置以后可以在浏览器中调试源代码

// jquery-3.4.1.min.map
{
	// sourceMap版本
	"version": 3,
	// 转换之前源文件的名称,多个文件打包生成一个文件的情况所以是数组
	"sources": ["jquery-3.4.1.js"],
	// 源代码中使用的成员名称,压缩代码时,会将开发过程中一些有意义的变量名替换为一些简短的字符,增大代码的压缩比例
	"names": [""],
	// base64 vrq编码形式的字符串,转换前后代码中字符的映射关系
	"mappings": ""
}
// 一般会在转换过后的代码中添加一行注释来引入sourceMap文件
// jquery-3.4.1.min.js
// 根据下面的注释去请求对应文件的.map文件
// # sourceMappingURL = jquery-3.4.1.min.map

**不同的值会明显影响到构建(build)和重新构建(rebuild)的速度: **
在这里插入图片描述

  1. Eval模式
    eval是JS中执行字符串代码的方法;eval(’’)语句在浏览器中运行是运行在临时虚拟机中,可以通过sourceURL声明这段代码所属的文件路径,这样这段代码就执行在指定的文件下
    在这里插入图片描述
// webpack.config.js
module.exports = {
	devtool: 'eval'
}
// bundle.js  
// eval by default in mode: "development"
var __webpack_modules__ = ({
"./readme.md":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval("...;\n\n//# sourceURL=webpack:///./readme.md?");
// source-map模式
/**
"./readme.md":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ("...");

}),
*/
}),

eval - 每个模块都使用 eval() 执行,并且都有 //# sourceURL。此选项会非常快地构建。主要缺点是,由于会映射到转换后的代码,而不是映射到原始代码(没有从 loader 中获取 source map),所以不能正确的显示行数。eval by default in mode: “development”

  1. eval-source-map
    同eval,不过它会额外生成sourceMap文件,可以定位到源文件代码对应的行列
    每个模块使用 eval() 执行,并且 source map 转换为 DataUrl 后添加到 eval() 中。初始化 source map 时比较慢,但是会在重新构建时提供比较快的速度,并且生成实际的文件。行数能够正确映射,因为会映射到原始代码中。它会生成用于开发环境的最佳品质的 source map。

  2. eval-cheap-source-map - 类似 eval-source-map,每个模块使用 eval() 执行。这是 “cheap(低开销)” 的 source map,因为它没有生成列映射(column mapping),只是映射行数。它会忽略源自 loader 的 source map,并且仅显示转译后的代码,就像 eval devtool。

  3. eval-cheap-module-source-map - 类似 eval-cheap-source-map,并且,在这种情况下,源自 loader 的 source map 会得到更好的处理结果。然而,loader source map 会被简化为每行一个映射(mapping)。

3和4中,4这种名字中带有module的模式,解析出来的源代码是没有经过Loader加工的;3名字中不带module的模式,解析出来的源代码是经过Loader加工的结果。若要还原一模一样的源代码就要选择带module的。

  1. inline-source-map
  2. hidden-source-map - 与 source-map 相同,但不会为 bundle 添加引用注释。如果你只想 source map 映射那些源自错误报告的错误堆栈跟踪信息,但不想为浏览器开发工具暴露你的 source map,这个选项会很有用。
  3. nosources-source-map - 创建的 source map 不包含 sourcesContent(源代码内容)。它可以用来映射客户端上的堆栈跟踪,而无须暴露所有的源代码。你可以将 source map 文件部署到 web 服务器。
    在这里插入图片描述
    注:

1-4都是适用于开发模式;
none和source-map,5-7适用于生产模式;
cheap表示只有行信息
sourceMap不是webpack特有的功能,webpack支持sourceMap

webpack项目优化

Tree Shaking

webpack 生产模式下自动开启tree shaking。

效果:删除未引用代码,如一个模块导出多个对象,但只使用了一个模块,或着写在return下一行的代码,以及注释。

注: Tree-shaking 并不是指wbepack中的某一个配置选项,而是一组功能搭配使用过后实现的效果。【usedExports,minimize,】

optimization属性:集中配置webpack优化功能的。

module.exports = {
	optimization: {
        // 模块只导出被使用的成员
        usedExports: true,
        // 开启压缩,会压缩掉未使用的代码
        minimize: true,
        // 尽可能的合并每一个模块到一个函数中,提升运行效率,缩小打包体积
        concatenateModules: true,
    },
}

在这里插入图片描述

结合babel-loader的问题

Tree-shaking实现的前提是ES Modules,也就是最终交给webpack打包的代码,必须是使用ESModules的方式来组织的模块化。

webpack打包之前会将模块交给不同的Loader去处理,最后将Loader处理过后的结果打包到一起。为了兼容性会使用babel-loader去转换新特性,而babel-loader有可能把ES Moudle处理成CommonJS【取决于是否配置使用转换ESModule的插件(babel基于插件机制)】.

预设的插件集合preset-env中就含有将EsModule转换为CommonJS的插件,
webpack在打包时拿到转换后的CommonJS代码,tree-shaking就失效。

			{
              test: /\.m?js$/,
               use: [
                   {
                       loader: 'babel-loader',
                       options: {
                       // 预设的插件集合
                           presets: ['@babel/preset-env']
                       }
                   }
               ]
           },

实际打包时,按照上面的设置去打包,开启usedExports;
结果发现usedExports生效了,依然是未导出。也就说明tree-shaking未失效。【最新版的babel默认关闭了EsModule转换的插件】

						options: {
						
                            presets: [
                            // 默认 {modules: auto},根据环境判断是否开启
                                ['@babel/preset-env', {modules: 'commonjs'}]
                            ]
                        }
// bundel.js
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "create": () => (/* binding */ create)
/* harmony export */ });
/* unused harmony export shaking */
var create = function create() {
  var input = document.createElement('input');
  console.log('1223');
  return input; // 测试tree-shaking

  console.log("112");
};
var shaking = function shaking() {
  return console.log("tree-shakiing");
};

/***/ })

强制开启EsModule的转换插件

/***/ ((__unused_webpack_module, exports) => {



Object.defineProperty(exports, "__esModule", ({
  value: true
}));
exports.shaking = exports.create = void 0;

var create = function create() {
  var input = document.createElement('input');
  console.log('1223');
  return input; // 测试tree-shaking

  console.log("112");
};

exports.create = create;

var shaking = function shaking() {
  return console.log("tree-shakiing");
};

exports.shaking = shaking;

/***/ })

在不确定babel是否默认启用了模块转换的插件,可以通过配置强制不适用.

presets: [
                                ['@babel/preset-env', {modules: false}]
                            ]

babel有关的源码部分

// @babel/preset-env/src/index.js

SideEffects

tree-shaking只能用于移除没有用到的代码成员;
sideEffects移除整个没用到的模块;

对全局产生副作用的不可以删除,对模块内部产生副作用的可以删除

模块的副作用 :模块执行的时候除了导出成员,是否还做了其他的事情。

webpack.config.js中的sideEffects用来开启这个功能。
package.json中 sideEffects:false表示忽略项目中所有的副作用 ,也就是可以移除所有的副作用,也可以是数组,表示保留副作用的路径。

打包结果将模块都打包进了bundle.js,此时开启Tree-shaking,没有用到的部分也会移除掉,但是由于模块包含一些副作用代码,导致tree-shaking过后,模块并不会被完全移除掉,只是导出成员会被移除掉。而模块内的副作用代码服务于模块,但其实整个模块都没有被使用到,那副作用代码也没必要留下。

// webpack.config.js
optimization: {
        // 模块只导出被使用的成员
        usedExports: true,
        // 开启压缩,会压缩掉未使用的代码
        // minimize: true,
        sideEffects: true
    },
// package.json
{
	sideEffects: false
}

Code Splitting 在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
多入口打包
在这里插入图片描述
默认会将输出的bundle引入html,多入口时,就会引入其他不需要的bundle,所以需要指定chunks[],指定使用某个bundle
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

splitChunks动态打包策略

在这里插入图片描述
按照这种写法,webpack会自动按需载入
在这里插入图片描述
这种动态打包产生的包名字是数字,以下可以去添加名字
在这里插入图片描述
而且相同的魔法注释名称会被打包到一个文件中。

webpack自行搭建

webpack中遇到的问题

模板文件用到插件配置的属性值时报错

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title><%=HtmlWebpackPlugin.options.title %></title>
    </head>
    <body>
        <script></script>
        <div id="app"></div>
    </body>
</html>

模板文件用到插件配置的属性值时报错
在这里插入图片描述
这个问题只是把HtmlWebpackPlugin的名字换成htmlWebpackPlugin,目前也不知道啥意思。

mode: none时的报错

打包成功,页面在浏览器中运行报错ReferrenceError: process is not defined

提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。
如果 mode 未通过配置或 CLI 赋值,CLI 将使用可能有效的 NODE_ENV 值作为 mode。

总结:

  1. Loader:负责完成项目中各种资源模块的加载,实现项目整体模块化
  2. plugin是用来去解决除资源模块打包以外的其他自动化工作
  3. webpack为每一个工作环节都预留了合适的钩子,扩展时只需找到合适的时机去做合适的事情,这种插件机制又叫面向切面编程aop;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值