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

相关推荐
理想不理想v2 天前
webpack最基础的配置
前端·webpack·node.js
臣妾没空2 天前
全栈里程碑二:前端基础建设
webpack
Domain-zhuo2 天前
如何利用webpack来优化前端性能?
前端·webpack·前端框架·node.js·ecmascript
初学者7.2 天前
Webpack学习笔记(2)
笔记·学习·webpack
理想不理想v2 天前
webpack如何自定义插件?示例
前端·webpack·node.js
森叶2 天前
【附源码】Electron Windows桌面壁纸开发中的 CommonJS 和 ES Module 引入问题以及 Webpack 如何处理这种兼容
webpack·electron
初学者7.2 天前
Webpack学习笔记(3)
笔记·学习·webpack
Byron Loong2 天前
Python+OpenCV系列:【打卡系统-工具模块设计】工具模块深度揭秘,考勤智能化的核心秘籍!
python·opencv·webpack
初学者7.3 天前
Webpack学习笔记(4)
学习·webpack
理想不理想v4 天前
免登陆是什么?
服务器·前端·javascript·vue.js·webpack