webpack 快速熟悉

webpack

核心模块webpack cli模块webpack-cli

查看webpack 命令

js 复制代码
yarn webpack --version

DEMO2

webpack4版本之后,支持零配置 按照约定将src/index.js 打包到dist/main.js 如果需要自定义,就要添加配置文件

mode目前只有三张模式:development/production/none mode默认是生产模式production,会优化打包结果(代码压缩、

也可通过cli设置mode,开发环境,会优化打包速度,在代码中添加调试过程中需要的辅助[devtool],方便调试:sourceMap

js 复制代码
yarn webpack --mode development

设置mode为none模式,最原始的打包,不会做额外的处理

js 复制代码
yarn webpack --mode none

模块Id是数组下标

js 复制代码
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _hello_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
/* harmony import */ var _hello_md__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_hello_md__WEBPACK_IMPORTED_MODULE_0__);


console.log(_hello_md__WEBPACK_IMPORTED_MODULE_0___default.a);


/***/ }),
/* 1 */
/***/ (function(module, exports) {

console.log("hello loader")

/***/ })
/******/ ]);

设置mode为development 入参是map,key是文件路径,即模块id

js 复制代码
/******/ ({

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

eval("console.log(\"hello loader\")\n\n//# sourceURL=webpack:///./src/hello.md?");

/***/ }),

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

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _hello_md__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello.md */ \"./src/hello.md\");\n/* harmony import */ var _hello_md__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_hello_md__WEBPACK_IMPORTED_MODULE_0__);\n\n\nconsole.log(_hello_md__WEBPACK_IMPORTED_MODULE_0___default.a);\n\n\n//# sourceURL=webpack:///./src/main.js?");

/***/ })

/******/ });

DEMO3--原理解读

设置mode为none模式,方便理解

生成了一个立即执行函数IIFE,工作入口

js 复制代码
/******/ (function(modules) { // webpackBootstrap --工作入口
/******/ 	// The module cache
/******/ 	var installedModules = {}; // 缓存模块
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {// 第一次moduleId为0
/******/
/******/ 		// 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是立即执行函数传进来的函数数组参数
/******/ 		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;
/******/  }
/******/
/******/ 	// Load entry module and return exports 
/******/  // 开始调用__webpack_require__函数,初始化调用参数数组的第一个元素,这里才开始加载模块
/******/ 	return __webpack_require__(__webpack_require__.s = 0);          
/******/ })
/************************************************************************/
/******/ ([
  /***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/***/ }),
/******/ ]);

立即执行函数是入口,参数是数组,数组中每一个函数 第一执行 webpack_require(0) // moduleId为0

js 复制代码
/******/ 	// Load entry module and return exports 
/******/  // 开始调用__webpack_require__函数,初始化调用参数数组的第一个元素,这里才开始加载模块
/******/ 	return __webpack_require__(__webpack_require__.s = 0);       

webpack_require 函数实现

js 复制代码
/******/ 	function __webpack_require__(moduleId) { // 第一次moduleId为0
/******/
/******/ 		// 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, // 数组下标为模块id
/******/ 			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;
/******/ 	}

加载模块

js 复制代码
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

执行模块里面的函数,使用严格模式,调用__webpack_require__.r函数

js 复制代码
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);

// 这里调用__webpack_require__,参数是1,该模块用来加载依赖的第一个模块
/* harmony import */ var _head_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


const head = Object(_head_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

document.body.append(head);


/***/ }),

调用__webpack_require__.r函数,添加标识,用来标识是__esModule

js 复制代码
/******/ 	// 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 });
/******/ 	};

接下该模块内部加载依赖的模块,它依赖第一个模块,(从这里可以看出,不能自己引用自己

js 复制代码
/* harmony import */ var _head_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

开始调用该模块

js 复制代码
const head = Object(_head_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

webpackBootstrap作用 处理各模块依赖关系,保持原有状态

DEM4-资源文件打包

loader是webpack实现前端模块化的核心 webpack默认把所有的文件当js文件处理,内部加载器只认识js文件 不能直接处理css文件,需要使用css-loader、style-loader 多个loader,是从后往前执行

js 复制代码
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader']
      }
    ]
  }

css-loader的作用就是把css文件转换成js模块

js 复制代码
var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default()(function(i){return i[1]});
// Module
___CSS_LOADER_EXPORT___.push([module.i, "body {\n  background-color: blue;\n}\n", ""]);
// Exports
/* harmony default export */ __webpack_exports__["default"] = (___CSS_LOADER_EXPORT___);

style-loader作用就是把css-loader转换的结果通过标签的形式追加到页面上

loader是webpack实现前端模块化的核心,通过不同的loader,可以加载任何资源文件

file-loader

文件加载器,把资源文件拷贝到打包目录

url-loader

Data Urls是特殊的url 协议,它可以用来直接表示一个文件,这样就不会发送 一般的url是需要服务器上有一个文件,通过请求这个地址,得到这个文件 格式 data:[][;base64], data: -- 协议 -- 媒体类型和编码 -- 文件内容 使用这样的协议,就不会发送http请求,浏览器能解析,输入到浏览器地址即可

eg: data:text/html;charset=UTF-8,

html content

输入到浏览器地址,浏览器能解析这是一个html

data:image/png;base64,ivshdsdbhdjbhbudhjdhvb....

webpack中可以使用url-loader来处理这种data url

js 复制代码
{
  test: /.png$/,
  use: 'url-loader'
}

这样dist目录就不会有图片文件了,导出的data url 的base64编码

Data URLS使用场景 本地小文件使用Data URL,可以减少请求次数 本地大文件单独提取存放,提高加载速度

js 复制代码
{
  test: /.png$/,
  use: {
    loader: 'url-loader',
    options: {
      // 10 KB ,对10KB以内的png文件会使用data urls来转换成base64,
      // 超出还是使用file-loader,因此file-loader包不能remove
      limit: 10 * 1024 
    }
  }
}

Webpack 常用loader分类

编译转换类,作用就是把资源模块转换成js代码 比如foo.css文件,通过css-loader,打包到bundle.js中

文件操作类,作用就是把加载的资源文件拷贝到打包目录,导出文件的访问路径

代码检查类,作用就是校验我们的代码(通过/不通过),统一代码风格,提高代码质量, 这种加载器不会修改生产环境的代码,比如eslint-loader

webpack 默认都支持esModule/AMD/commonJS/CMD,无需配置额外的loader

需要配置library字段

demo7-webpack-es2015

webpack完成模块打包工作,默认会处理es module中import/export, 无需额外的loader来处理es module中import/export 但是webpack并不会编译转换es6其他的特性 比如:箭头函数是不会转换成普通函数

js 复制代码
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (() => {
  const ele = document.createElement('h2');

  ele.textContent = 'Hello world';
  ele.addEventListener('click', () => {
    alert('hello webpack');
  });
  return ele;
});

如果需要对es6特性转换,就需要安装编译转换类的loader babel-loader,依赖babel的核心模块@babel/core, 还需要用于编译转换的插件集合@babel/preset-env,

js 复制代码
{
  test: /.js$/,
  use: 'babel-loader'
}

babel-loader是一个代码转换平台,真正转换的是babel插件,因此要配置插件

js 复制代码
{
  test: /.js$/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env']
    }
  }
}

webpack只是打包工具,不会处理es6或者更高版本的新特性, 加载器可以用来编译转换代码

webpack 模块打包方式

1.遵循ES Modules 标准的import 声明 2.遵循CommonJS标准的require函数

js 复制代码
// CommonJs中的require导入ES Module,对于ES Module中默认导出,需要对导入的对象的default属性获取
const createHeading = require('./heading.js').default;

3.遵循 AMD 标准的define函数和require函数

webpack兼容多种模块化标准,尽量避免使用在一个项目中混合使用,混用会降低可维护性

Loader 加载的非js也会触发资源加载 1.样式代码中的 @import 指令 和 url 函数 2.HTTML代码中图片标签的src属性

html中也会有引用资源文件

js 复制代码
// foot.html
<footer>
  <img src="better.png" alt="bet">
</footer>
js 复制代码
import footHtml from './foot.html';
document.write(footHtml);

webpack默认只认识js文件,需要html-loader加载html模块

html-loader默认只会处理img标签的src属性,如果需要处理其他标签属性

js 复制代码
{
  test: /.html$/,
  use: {
    loader: 'html-loader',
    options: {
      attrs: ['img:src', 'a:href']
    }
  }
}

webpack 核心工作原理(webpack只识别js模块,其他资源都是通过loader来转换成js代码)

项目中一般有各种类型的资源文件js/css/png/json/jpg/scss等, webpack首先通过webpack配置找到打包入口文件 webpack根据代码中的import / require 语句推断解析依赖的模块 解析每个资源模块对应的依赖,建立项目中所有文件的依赖关系的树, webpack会递归依赖树,找到每个节点对应的资源文件,通过配置文件的rules属性 找到对应文件的加载器来加载该模块,将加载结果放到bundle.js中,实现整个项目打包, Loader机制是webpack的核心,可以用来处理各种资源文件,如果没有Loader,webpack 只是js代码合并工具

Loader 核心工作原理 --demo8-手写loader

loader工作原理,就是将资源文件从输入到输出的转换,最终转换成js代码 webpack中对于同一个资源可以一次使用多个Loader 比如:css-loader -> style-loader

js 复制代码
{
  test: /.md$/,
  use: [
    'html-loader', 
    'markdown-loader', // 
  ]
}

资源文件 --> markdown-loader接收资源文件内容,转换成html字符串 --> html-loader将md文件转换成js代码(比如 return module.exports = ${JSON.stringify(html)}) A资源类型的Loader加载A资源, Loader的入参就是A资源的内容,最后一个Loader必须返回js代码,否则webpack 不能识别

webpack插件

目的是增强Webpack自动化能力 loader是专注实现各种资源模块加载(非js资源文件转成js模块),也可以不兼容js模块转成兼容的js模块的loader

Plugin是用来解决除了资源加载以外的其他的自动化工作 eg: 1.每次重新打包,清除上一次打包结果的 dist 目录 2.拷贝不需要参与打包的静态文件至输出目录 3.压缩打包的输出代码

Webpack + Plugin 实现了大多前端工程化工作

常用插件

*clean-webpack-plugin // 自动清除输出目录插件

webpack打包都是覆盖打包目录(dist目录内的文件),同名文件会被覆盖,对于未重名的文件就会一直积累在dist目录 因此,需要在每次打包前清理dist目录,clean-webpack-plugin很好满足了这次需求

js 复制代码
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const config = {
  ...
  plugins: [
    new CleanWebpackPlugin()
  ]
}

*html-webpack-plugin // 自动生成使用bundle.js的html

2个问题 1.发布时,需要发布dist目录的所有打包结果和根目录下html文件 2.webpack打包的结果bundle.js,需要手动引入到html中,上线需要确保script标签引用路径是正确的 一旦,配置发生变化,打包文件名发生变化,需要手动去修改html中script标签引用路径

导致这个问题的根因是硬编码产生的

需要通过webpack输出Html文件到dist目录,自动注入引用打包结果bundle.js文件到html

配合html-webpack-plugin定制html模板

1.定制title; 2.增加标签; 3.增加参数,html通过<%= htmlWebpackPlugin.options.title %>; 4.指定html模板; 5.多实例,每个HtmlWebpackPlugin实例对象生成都会生成一个html,多个生成多个html,多页面应用

js 复制代码
const HtmlWebpackPlugin = require('html-webpack-plugin');
const config = {
  ...
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack plugin title',
      meta: {
        viewport: 'width=device-width'
      },
      template: './index.html' // 指定html模板
    }), // script标签的路径是output中${publicPath}/${filename}
    // index_2.html
    new HtmlWebpackPlugin({
      filename: 'index_2.html';
    })
  ]
}

copy-webpack-plugin 项目中经常有不需要参与构建的静态文件,比如html模板引用的本地favicon.icon图标文件等,这类文件不需要参与打包

最常用的插件 1.clean-webpack-plugin 2.html-webpack-plugin 3.copy-webpack-plugin

Plugin相比于Loader,Plugin拥有更宽的能力范围

Plugin是通过webpack的钩子机制实现

webpack中Plugin必须是一个函数或者是一个包含apply方法的对象

js 复制代码
// 通过钩子机制实现自定义插件,
// 实现删除bundle.js中的注释
class MyPlugin {
  apply(compiler) {
    console.log('MyPlugin 启动');
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // compilation => 此次打包的上下文
      // compilation.assets => 打包的之后的文件
      for(const name in compilation.assets) {
        // console.log('name', name);
        // console.log('name', compilation.assets[name].source());
        if (name.endsWith('.js')) {
          const contents = compilation.assets[name].source();
          const withoutComments = contents.replace(/\/\*\*+\*\//g, '');
          compilation.assets[name] = {
            source: () => withoutComments,
            size: () => withoutComments.length // webpack内部要求的,需要返回文件大小
          }
        }
      }
    })
  }
}

Plugin插件:通过在webpack打包的生命周期的钩子中挂载函数实现扩展

Webpack 开发体验

编写源码 -> Webpack打包 -> 运行应用 -> 刷新浏览器

优化 1.http server 运行 2.自动编译 + 自动刷新 3.source map支持

watch模式:监听文件变化,自动重新打包

js 复制代码
yarn webpack --watch // 监视模式运行打包,需要手动刷新浏览器

编译之后,自动刷新浏览器 browser-sync工具可以实现

js 复制代码
npm i browser-sync --dev // 安装browser-sync工具

// 使用browser-syncy启动http server
browser-sync dist --file "**/*"

优化: 1.自动编译,自动刷新

缺点: 1、操作麻烦,同时需要使用两个工具 2.效率降低,webpack会不断将文件写入磁盘

webpack-dev-server

集成了上述功能 1.提供了用于开发的http server 2.集成了自动编译自动刷新浏览器等功能

特性:使用webpack-dev-server开发,为提高工作效率,并不会把打包文件写入dist目录(磁盘中), 而是把打包结果暂时存放在内存当中,内部的http server从内存中读取出来,大大减少磁盘读写操作 提高构建效率

webpack dev server 默认只会将开发构建文件作为开发服务器的资源文件,如果serve也需要其他资源文件,需要增加配置

js 复制代码
  devServer: {
    contentBase: './public' // 指定额外的资源路径
  },

配置代理服务器 比如生产环境api www.prod.com/api

开发环境 http://localhost:8080/index.html

开发中调用www.prod.com/api会出现跨域问题

CORS:跨域资源共享,需要api支持跨域

前后端同源部署(域名、协议、端口一致),没有必要开启cors

问题:开发阶段接口跨域问题s

API服务器 开发服务器

markdown 复制代码
            浏览器

就是将接口服务代理到本地开发服务器地址

js 复制代码
proxy: {
  '/api': {
    // http://localhost:8080/api/users -> https://api.github.com/api/users(不对,多了api)
    target: 'https://api.github.com',
    // https://api.github.com/api/users -> https://api.github.com/users
    pathRewrite: {
      '^/api': '' //将/api替换为空
    },
    // 不能使用localhost:8080作为请求Github的主机名,原因去查http host
    changeOrigin: true
  }
}

Source Map(源代码地图) 缘由:webpack打包之后运行代码与源代码之间差异很大,导致调试应用、错误信息无法快速定位 调试和报错都是编译打包之后的代码运行的

Source Map作用就是映射编译打包代码和源代码 编译打包的代码通过SourceMap文件逆向解析得到源代码

SourceMap文件是一种json格式的.map后缀的文件,以jquery-3.4.1.js为例

js 复制代码
{
  "version": 3, // source map使用的标准的版本,
  "sources": ["jquery-3.4.1.js"], // 源代码的文件名称,可能是多个文件,编译打包成一个文件,因此是数组
  "names": [ // 表示源代码使用的一些成员名称,开发阶段,会将有意义的变量名压缩替换成简短的字符
    "global",
    "factory",
    "module",
    "exports",
    "document",
    "w",
    ...
  ],
  "mappings": ";CAaA,SAAYA..." // 核心属性,base64-ZRQ编码,表示转换后的字符和转换前字符的对应关系
}

source map文件只是用来调试和定位错误的,对生产环境没有声明意义

使用source map格式(以jquery-3.4.1.js为例)

js 复制代码
... // 打包之后的代码

//# sourceMappingURL = jquery.3.4.1.min.map

webpack 配置 source map

devtool配置source map 总共可以支持12种,每种方式的效率和效果各不相同 eval eval是js的一个函数

js 复制代码
eval('console.log(123)'); // 运行在临时的虚拟机中 VMxx:1
eval('console.log(123) //# sourceURL=./foo/bar.js');  // 指定运行的路径 ./foo/bar.js(其实还是在虚拟机中) ,只是一个标识

使用devtool设置eval模式,打包后模块代码在eval中,在eval函数内部指定文件位置 //# sourceURL=webpack:///./src/main.js?, 就能定位到错位打包后模块文件

js 复制代码
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _head_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);\n\n\nconst head = Object(_head_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n\nconsole.log1(1234567)\n\ndocument.body.append(head);\n\n\n//# sourceURL=webpack:///./src/main.js?");

/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (() => {\n  const ele = document.createElement('h2');\n\n  ele.textContent = 'Hello world';\n  ele.addEventListener('click', () => {\n    alert('hello webpack');\n  });\n  return ele;\n});\n\n//# sourceURL=webpack:///./src/head.js?");

/***/ })
/******/ ]);

在main模块中设置错误信息,错误定位信息 main.js:7 Uncaught TypeError: console.log1 is not a function at eval (main.js:7) at Module. (bundle.js:92) at webpack_require (bundle.js:20) at bundle.js:84 at bundle.js:87

js 复制代码
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _head_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);


const head = Object(_head_js__WEBPACK_IMPORTED_MODULE_0__["default"])();

console.log1(1234567)

document.body.append(head);

这种模式不会生成source map文件,构建速度是最快的,不能定位到源代码的行列信息,只能定位到文件路径名称

demo12

eval-source-map:相比eval生成sourcemap,不仅可以定位到文件名称,还可以定位到编译之后的代码行列信息

cheap-eval-source-map:生成了sourcemap,仅可以编译之后的代码定位到行,没有列

cheap-module-eval-source-map:生成了sourcemap,仅可以源代码代码定位到行,没有列

js 复制代码
  'eval',
  'cheap-eval-source-map',
  'eval-source-map',
  'cheap-source-map',
  'cheap-module-source-map',
  'inline-cheap-source-map',
  'inline-cheap-module-source-map',
  'source-map',
  'inline-source-map', // sourcemap没有单独文件,sourcemap在打包之后代码最后一行
  'hidden-source-map', // 生成了source map 文件,但是没有引用到源代码中,因此在开发环境中看不到效果
                       // 第三包生成source map 但不引用它们,出现问题再引用
  'nosources-source-map' // 生成了source map 文件,但是定位信息点进去看不到源代码

...其他模式就是将这几个模式组合

eval - 是否使用eval执行模块代码 cheap - source map 是否包含行信息 module - 是否能够得到Loader处理之前的源代码

选择sourcemap经验

开发环境:cheap-module-eval-source-map // 定位到行、文件模块 代码每行不超过80字符 代码经过loader转换后差异较大 首次打包速度无要求,重写打包相对较快

生产环境:none,不生成sourcemap,因为source map会暴露代码到生产环境, 可以适当选择nosources-source-map

自动刷新问题 自动刷新导致页面状态丢失

期望页面不刷新的前提下,模块也可以及时更新

HMR Hot Module Replacement

模块热替换/热更新 应用程序运行过程中实时替换某个模块,应用运行状态不受影响

自动刷新导致页面状态丢失,热替换只将修改的模块实时替换至应用中

集成在webpack-dev-server cli命令

js 复制代码
webpack-dev-server --hot // 开发HMR

webpack插件new webpack.HotModuleReplacementPlugin()也开启HMR

webpack中的HMR需要手动处理模块热替换

样式文件的热更新开箱即用:样式文件是通过loader处理,style-laoder自动处理样式文件的热更新

样式文件更新后会直接替换之前的文件,会覆盖之前的样式,从而实现样式文件热更新

js模块是没有规律,导出的可能是对象、函数、字符串等,对导出的成员的使用也是不同,所以webpack面对这些毫无规律的js模块,webpack无法处理你更新的模块,无法实现所有情况的js模块的热更新方案

框架下的开发,每种文件是有规律, 通过脚手架创建的项目内部都集成了HMR方案

我们需要手动处理JS模块更新后的热替换 new webpack.HotModuleReplacementPlugin(), 开启插件才能使用HMR API HMR API处理热替换

js模块的热替换

js 复制代码
import createHead from './head';

const head = createHead();

document.body.append(head);

console.log(head);
let lastHead = head;
// HMR API处理./head模块热更新
module.hot.accept('./head', () => {
  // console.log('head模块更新了,需要这里手动处理热替换逻辑12');
  // console.log(head);
  const value = lastHead.innerHTML;
  document.body.remove(lastHead);
  const newHead = createHead();
  newHead.innerHTML = value;
  document.body.append(newHead);
  lastHead = newHead;
})

// 图片的热替换

js 复制代码
module.hot.accept('./test', () => {
  img.src = png;
});

HMR注意事项 1.处理HMR的代码报错会导致自动刷新,导致错误不易被发现

配置hotOnly不会自动刷新

  1. 没有启用HMR的情况,HMR API 报错

使用HMR API

JS 复制代码
if (module.hot) {
  // 处理热替换
}

处理热替换的代码, 开发环境打包会去掉热替换代码

js 复制代码
if (false) {

}

生产环境,压缩过后会直接删掉

生产环境优化

sourcemap、HMR额外的代码提高开发效率,对生产环境没什么作用

生产环境注重运行效率 开发环境注重开发效率

mode(模式)来为不同的工作环境创建不同的配置

方式 1.配置文件根据不同导出不同配置 --小项目 2.一个环境对应一个配置文件 --大项目 使用命令--config来使用配置文件

js 复制代码
yarn webpack --config webpack.prod.js

webpack.config.js也可以导出一个函数,接收两个参数 env是运行cli传递的环境名参数,argv是运行cli传递的所有参数

js 复制代码
module.exports = (env, argv) => {
  const config = {
    mode: "development", // 设置打包模式:开发环境development,生产环境production,none
    entry: './src/main.js', // 设置打包入库文件,如果是相对路径,./不能省略
    output: {
      filename: 'bundle.js', // 指定打包输出文件名称
      path: path.join(__dirname, 'dist'), // 必须为绝对路径,不如输入output,会报错
    },
    plugins: [
      new CleanWebpackPlugin(),
      new HtmlWebpackPlugin({
        title: 'webpack plugin title',
        meta: {
          viewport: 'width=device-width'
        },
        template: './index.html' // 启用模板html
      }), // script标签的路径是output中${publicPath}/${filename}
      // 生成index_2.html
      new HtmlWebpackPlugin({
        filename: 'index_2.html'
      }),
      // 需要拷贝的文件或者文件目录的数组
      new CopyWebpackPlugin([
        // 'public/**',支持通配符、文件目录、文件的相对路径
      ])
    ]
  };
  
  if (env === 'production') {
    config.mode = 'production';
    config.devtool = false;
    config.plugins = [
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['plugin'])
    ]
  }
}

DefinePlugin

为代码注入全局成员

js 复制代码
plugins: [
  new webpack.DefinePlugin({
    //这里value需要代码片段,因此打包之后变成console.log(https://api.com);
    // API_BASE: 'https://api.com',

    
    API_BASE: '"https://api.com"',
    API_BASE: JSON.stringify('https://api.com'),
  })
]

Tree Shaking(摇树)

摇掉代码中未引用部分(dead-code) 生产环境自动启用

js 复制代码
optimization: {
  usedExports: true, // 开启tree shaking,不导出未使用的模块,打包代码还在
  minimize: true, // 压缩代码,去除未使用的代码
}

concatenateModules

尽可能将所有模块合并输出到一个函数 既提升了运行效率,有减少了代码体积 Scope Hoisting

optimization: { usedExports: true, // 开启tree shaking,不导出未使用的模块,打包代码还在 concatenateModules: true, // 压缩代码,去除未使用的代码 }

Tree Shaking & babel

使用babel-loader 会导致tree shaking 失效 Tree shaking前提是ES Module 由webpack打包的代码必须使用ESM 未处理esm新特性,可能会选择 babel-loader: ESM -> CommonJS,因此tree saking 会失效

babel插件集合@babel/preset-env会把esm的部分转换成commonjs,webpack再去打包,就会是以commonjs 的方式组织的代码打包

**在最新的版本的babel插件关闭了esm转换插件,转换之后还是esm的代码,tree shaking还是可以正常工作

sideEffects 副作用

一般用于npm包标记是否有副作用 production模式下自动开启

设置两个配置webpack.config.js、package.json webpack中配置

js 复制代码
optimization: {
  sideEffects: true // 开启功能
}

package.json 配置sideEffects: false标识当前项目中代码没有副作用 打包过后的代码就会移除未使用的代码 a.js b.js c.js

index.js 导出a.js,b.js,c.js

main.js 只导入使用a.js,

配置开启sideEffects副作用,就不会把b.js和c.js代码打包进去

使用sideEffects前提,确保你的代码真的没有副作用 extend.js

js 复制代码
Number.prototype.add = function(size) {
  let result = this + '';
  while(result.length < size) {
    result = '0' + result;
  }
  return result;
}

// 8.add(2) // 8 --> 008

在main.js 中

js 复制代码
import './extend.js'; // 只是导入文件
8.add(2);

此时 extend.js并不会被打包到代码里 添加的add方法不能使用 我们项目css文件很多情况都是 import './index.css' 有时出现打包代码没有包含css文件,默认开启副作用,css代码没被打包进去 解决这个问题,在package.json中配置

js 复制代码
sideEffects: [
  "./src/extend.js",
  "*.css"
]

打包结果就会包含这些代码

Code Splitting 分包/代码分割

webpack默认把所有代码都打包到一起bundle.js

所有模块打包到一起和分包是互斥的

场景 当模块越来越大时, 1.并不是每个模块在启动时都是必要的 2.分包,按需加载,打包成多个bundle

webpack配置分包 1.多入口打包 multi entry 多页应用程序 2.动态导入(esm的动态导入)

多入口打包

一个页面对应一个打包入口

js 复制代码
  entry: {
    a: './src/a.js',
    b: './src/b.js'
  }, // 设置打包入库文件,如果是相对路径,./不能省略
  output: {
    filename: '[name].bundle.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'a页面',
      template: './src/a.html', // 启用模板html
      filename: 'a.html',
      chunks: ['a']  // 配置chunks,否则打包文件都会被引入
    }), 
    new HtmlWebpackPlugin({
      title: 'b页面',
      template: './src/b.html', // 启用模板html
      filename: 'b.html',
      chunks: ['b']
    }), 
  ]

公共的部分单独提取 不同的入口中肯定会有公共模块

js 复制代码
optimization: {
  splitChunks: {
    chunks: 'all'
  }
}, // 开启公共模块单独提取chunk,会生成一个单独的bundle
动态导入

动态导入的模块会被自动分包,

按需加载模块

动态导入无需配置webpack,执行按照esm中动态导入模块就可以,webpack会自动处理分包,按需加载

对于单页面应用开发,react/vue开发框架,路由映射组件就可以使用动态导入的方式实现动态加载

js 复制代码
import('./common').then(mod => {
  console.log('mod', mode);
})

按照动态导入,打包成的文件名称是序号,如果需要重命名 可以使用魔法注释

js 复制代码
import(/* webpackChunkName: common */'./common').then(mod => {
  console.log('mod', mode);
})

动态导入的模块打包的文件名就会说common 配置相同的chunkName会被打包到一起

MiniCssExtractPlugin (css文件超过150kb才考虑使用)

提取CSS文件插件,实现单独提取css模块到一个文件中 npm i mini-css-extract-plugin --dev

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

js 复制代码
const MiniCssExtractPlugin = require('mini-css-rxtract-plugin');

....
module: {
  rules: [
    {
      test: /.css$/,
      use: [
        MiniCssExtractPlugin.loader 
        'css-loader'
      ]
    }
  ]
},
plugins: [
  new MiniCssExtractPlugin({
    filename: '[name].bundle.css'
  })
]

单独提出的css文件在生产环境下打包,没有被压缩,因为webpack内部的压缩是针对js文件压缩

OptimizeCssAssetsWebpackPlugin 压缩css

npm i optimize-css-assets-webpack-plugin --dev

js 复制代码
const OptimizeCssAssetsWebpackPlugin = require('mini-css-rxtract-plugin');
// 配置在optimization中minimizer数组中,这样配置使得内置的js压缩插件失效
// 因此要配置TerserWebpackPlugin

const TerserWebpackPlugin = require('terser-webpack-plugin');

...
optimization: {
  minimizer: [
    new OptimizeCssAssetsWebpackPlugin(),
    new TerserWebpackPlugin()
  ]
}

输出文件名Hash

substitutions

如果客户端文件缓存时间设置很长,文件名相同,就会一直是缓存, 文件名称改变,就可以请求新的文件

生产模式下,文件名使用hash

hash

plugins: [ new MiniCssExtractPlugin({ filename: '[name]-[hash].bundle.css' }) // x项目基本的hash,即项目中任何文件改变,生成的文件名(hash)都会变化 ]

chunkhash\] // chunk hash,哪个文件变化,打包的哪个文件名变化(hash) \[contenthash\] // 文件级别的hash,不同的文件不同的hash值 ---最适合解决文件缓存问题 指定hash长度 \[contenthash:8\] 指定8位长度的hash

相关推荐
vipbic4 小时前
关于Vue打包的遇到模板引擎解析的引号问题
前端·webpack
妮妮喔妮7 小时前
Webpack 有哪些特性?构建速度?如何优化?
前端·webpack·node.js
ST.J7 小时前
webpack笔记
前端·笔记·webpack
webYin21 小时前
vue2 打包生成的js文件过大优化
前端·vue.js·webpack
!执行21 小时前
webpack 相关配置
webpack
醉方休1 天前
vite与webpack对比
前端·webpack·devops
wallflower20201 天前
🚀 从 Webpack 到 Vite:企业级前端构建、代码分割与懒加载优化完全指南
webpack·vite
一枚前端小能手1 天前
🚀 Webpack打包慢到怀疑人生?这6个配置让你的构建速度起飞
前端·javascript·webpack
全栈技术负责人2 天前
webpack性能优化指南
webpack·性能优化·devops
和雍2 天前
webpack5 创建一个 模块需要几步?
javascript·面试·webpack