webpack5七进七出-工程化必备技能

804 阅读24分钟

Webpack5入门到精通

安装

npm install webpack webpack-cli –g # 全局安装
npm install webpack webpack-cli –D # 局部安装

webpack命令指定某个文件为入口文件

npx webpack --entry ./src/main.js --output-path ./build
//或者改为:
“scripts”: {
    "build":webpack --entry ./src/main.js --output-path ./build
}

或者在根目录新建webpack.config.js

const path = require('path')
module.exports = {
    entry:'./src/main.js', //入口文件
    output:{ //出口
        filename:"bundle.js", //输入文件的名字
        path:path.resolve(__dirname,'./build')
    }
}

更改指定配置文件

例如将webpack.config.js更改为wk.config.js

“scripts”: {
    "build":webpack --config ./wk.config.js
}

webpack是如何将项目打包?

  1. webpack在处理应用程序时,根据命令或者配置文件找到入口文件。
  2. 接着会生成一个依赖关系图(包含App中所需要的所有模块,JS、CSS、图片、字体)
  3. 然后遍历图结构,根据不同模块所依赖的loader,打包成一个个模块。

Loader

Loader是用于特定的模块类型进行转换。

css-loader的使用

css-loader安装:npm install css-loader -D

方式1:内联方式

import "style-loader!css-loader!../css/index.css"

方式2:CLI方式

//此方式webpack5已经弃用
"scripts":{
"build":"webpack --module-bind 'css=css-loader' --config wk.config.js"
}

方式3:loader配置方式

module.exports ={
	module:{ //多个属性
		rules:[ //存放Rule对象
            {
               test:/\.css$/i, //匹配资源 
               //loader:"css-loader" (方式一),
                use:[  
                    //{(方式二)
                   		 //loader:"css-loader",
                   		 //options:{}
                    // }
                    
                    "css-loader" (方式三) 
                ]
            }
        ] 
	}
}
style-loader

css-loader负责将.css文件进行解析,style-loader将css插入到页面中。

安装: npm install style-loader -D

rules:[
	{
        test:/\.css$/,
        use:[
        "style-loader","css-loader" //loader处理是从下往上,从右往左,从后往前 进行处理
        ]
	}
]
预处理器

例如:安装less. npm install less -D

方式1:npx less ./src/css/component.less > component.css

方式2:

module:{
	rules:[
	{
        test:/\.less$/,
        use:[
            "style-loader",
            "css-loader",
       		"less-loader"
        ]
	}
	]
}

SASS 同上

浏览器兼容性

查看node_modules是否安装过browserslist?

npm i browserslist

执行browserslist:

npx browserslist  ">1%,last 2 version, not dead"

“ , ” 或者 “ or ” 或者 “换行” 并集关系

“ and ” 交集关系

“ not ” 取反

npx browserslist 命令会默认执行 .browserslistrc

方式1:

package.json:

"browserslist":{
	">1%",
	"last 2 version",
	"not dead"
}

方式2:

创建文件 ".browserslistrc"

>1%
last 2 version
not dead
PostCSS工具

通过JS转换CSS样式,进行CSS适配,比如自动添加浏览器的前缀,css样式重置。

第一步:查找PostCSS在构建中的扩展,比如webpack中的postcss- loader;

第二步:选择可以添加PostCSS相关的插件

安装:

npm install postcss -D

如果需要在命令行运行postcss需要安装:

npm install postcss-cli -D

例如对单个css文件进行打包:

npx postcss -o result.css  ./src/css/test.css

如果需要将css文件中的样式添加前缀,直接运行上面的命令会出现提示:You did not set any plugins ... 。因此需要安装autoprefixer

npm install autoprefixer -D

然后运行(对单个文件进行处理):

npx postcss --use autoprefixer -o result.css ./src/css/test.css

此时查看result.css:

:-webkit-full-screen {
  font-size: 12px;
}

:-ms-fullscreen {
  font-size: 12px;
}

:fullscreen {
  font-size: 12px;
}

.content {
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
  transition: all 2s ease;
  
}

webpack 中使用postcss

安装postcss-loader:

npm install postcss-loader -D

配置webpack.config.js

rules:[
	{
        test:/\.css$/,
        use:[
        "style-loader",
        "css-loader",
        {
        	loader:"postcss-loader",
        	options:{
        		postcssOptions:{
        			plugins:[
        				require("autoprefixer")
        			]
        		}
        	}
        }
        ]
	}
]
postcss-preset-env

在配置post-loader时,配置插件并不需要使用autoprefixer

安装:

npm install postcss-preset-env -D

使用

rules:[
	{
        test:/\.css$/,
        use:[
        "style-loader",
        "css-loader",
        {
        	loader:"postcss-loader",
        	options:{
        		postcssOptions:{
        			plugins:[
        				//require("autoprefixer"), 可以删除掉
        				//require("postcss-preset-env")或者
                        "postcss-preset-env"
        			]
        		}
        	}
        }
        ]
	}
]

建议写法:

在根目录下创建 postcss.config.js

module.exports = {
    plugins:[
        require("postcss-preset-env")
    ]
}

当在Css文件中@import的新的Css文件。需要添加:

rules:[
	{
        test:/\.css$/,
        use:[
        "style-loader",
        {
        loader:"css-loader",
         options:{
         importLoaders:1  //与后面loader个数有关
         }
        },
      	"postcss-loader"
        ]
	}
]
file-loader

处理jpg、png等格式的图片,需要安装file-loader

安装:

npm install file-loader -D

wk.config.js的rules进行配置:

{
	test:/\.(png|jpe?g|gif|svg)$/,
	use:"file-loader"
}

需要注意的是在JS文件到如图片的时候(require)需要 “.default”

设置打包之后的图片名称:

{
	test:/\.(png|jpe?g|gif|svg)$/,
	use:{
	loader:"file-loader",
	options:{
		name:"img/[name].[hash:6].[ext]",  //hash:截取6位
		//outputPath:"img"  指定输出目录
		}
	},
}

常用placeholder:

常用placeholder

url-loader

将较小的文件,转换成base64文件,将文件嵌入到打包好的JS文件中。

  {
        test:/\.(png|jpg|jpeg|gif|svg)$/,
        use:[
          {
            loader:'url-loader',
            options:{
              name:"img/[name].[hash:6].[ext]",
              // outputPath:'img'
              limit:100 * 1024 //小于此大小的文件转换为base64
            }
          }
        ]
      }
asset module type

需要将上述的依赖中卸载 file-loader 和 url-loader

npm uninstall  file-loader url-loader 

然后在package.json中配置

  {
        test:/\.(png|jpg|jpeg|gif|svg)$/,
       	type:"asset/resource",
      	//type:"asset/inline" 将文件导入到jS文件中
       generator:{ //自定义名称01
          filename:"img/[name].[hash:6][ext]"
        }
      }

自定义名称02 (二选一):

output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './build'),
    assetModuleFilename:"img/[name].[hash:6][ext]" //自定义文件名称
  },

按照文件大小去设置文件转换(base64 / 路径. ):

需要配置:

  {
        test:/\.(png|jpg|jpeg|gif|svg)$/,
		type:"asset",
       generator:{ 
          filename:"img/[name].[hash:6][ext]"
        },
        parser:{
        	dataUrlCondition:{
        	maxSize:100 * 1024
        	}
        }
      }

加载字体文件:

需要在package.json中配置:

 {
        test:/\.ttf|eot|woff2?$/i,
        type:"asset/resource",
        generator:{
          filename:"font/[name].[hash:6][ext]"
        }
  }

Plugin

Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等。

Plugin

CleanWebpackPlugin

安装:

 npm install clean-webpack-plugin -D

配置:

package.json:

const { CleanWebpackPlugin} = require("clean-webpack-plugin")

module.exports = {
     plugins:[
    new CleanWebpackPlugin()
  ]
}
 
HtmlWebpackPlugin

HTML文件是编写在根目录下的,而最终打包的dist文件夹中是没有index.html文件的,在进行项目部署的时,必然也是需要有对应的入口文件index.html。因此需要动态创建index.html

安装:

npm install html-webpack-plugin -D

配置:

package.json:

const HtmlWebpackPlugin = require("html-webpack-plugin")
plugins:[
    new HtmlWebpackPlugin({
      title:"thunder webpack", //作用于html模板中:htmlWebapckPlugin.options.title
      template: './public/index.html',
    })
  ]
DefinePlugin

在定义的模板中还有一个Base_Url变量:

<link rel="icon" href="<%= BASE_URL %>favicon.ico">

DefinePlugin允许在编译时创建配置的全局常量,是一个webpack内置的插件(不需要单独安装)

引入

const { DefinePlugin } = require('webpack');

配置:

plugins:[
    new DefinePlugin({
      BASE_URL:'"./"'
    })
]

CopyWebpackPlugin

将文件复制到指定的文件夹中.

安装:

npm install copy-webpack-plugin -D

在webpack.config.js中进行配置:

导入:

const CopyWebpackPlugin = require("copy-webpack-plugin")

配置

plugins:[
		new CopyWebpackPlugin(
		{
			patterns:[
				from:'public', //设置从哪一个源中开始复制
                //to:''  //复制到的位置,可以省略,会默认复制到打包的目录下;
				globOptions:{ //设置一些额外的选项,其中可以编写需要忽略的文件
				ignore:[
            	  "**/index.html", //也不需要复制,因为我们已经通过HtmlWebpackPlugin完成了index.html的生成;
            	  "**/.DS_Store",//mac目录下回自动生成的一个文件;
            	  "**/abc.txt"
          		  ]
				}
			]
		})
]

mode

打包模式:

development / production(丑化过的JS代码)

在webapck.config.js配置:

module.exports = {
  mode: 'development', //模式
  ...
  ...
};

devtool

修改打包之后的代码,默认是 env

module.exports = {
  devtool:"source-map",//还有配置eval
  ...
  ...
};

模块化打包原理

创建文件

format.js

const dataFormate = (data) => {
    return '2020-12-12'
}

const priceFormate = (proce) => {
    return "100.00"
}

module.exports = {
    dataFormate,
    priceFormate
}

main.js

export function sum(a,b) {
    return a+b 
}

export function mul(a,b) {
    return a+b 
}
commonJS打包源码

commonJS 打包之后的代码,一行行分析通俗易懂

common_index.js

const { dataFormate,priceFormate} = require('./js/format')

bundle.js

//定义一个对象
// 形式为:key,value
var __webpack_modules__ = {
  './src/js/format.js': function (module) {
    const dataFormate = (data) => {
      return '2020-12-12';
    };

    const priceFormate = (proce) => {
      return '100.00';
    };
    module.exports = {
      dataFormate,
      priceFormate,
    };
  },
};

// The module cache
//作为加载模块的缓存
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
  // 1.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
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
  // Return the exports of the module
  return module.exports;
}

var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
//具体开始执行代码的逻辑
!(function () {
  const { dataFormate, priceFormate } =
    __webpack_require__('./src/js/format.js');
})();
ES打包源码

ES打包之后的源码,一行行分析通俗易懂

es_index.js

import {sum,mul} from './js/main'

bundles.js

//1、定义了一个对象,对象里面做了模块的映射
var __webpack_modules__ = {
  /***/ './src/js/main.js': /***/ function (
    __unused_webpack_module,
    __webpack_exports__,
    __webpack_require__,
  ) {
    __webpack_require__.r(__webpack_exports__);
    /* harmony export */ __webpack_require__.d(__webpack_exports__, {
      /* harmony export */ sum: function () {
        return /* binding */ sum;
      },
      /* harmony export */ mul: function () {
        return /* binding */ mul;
      },
      /* harmony export */
    });
    function sum(a, b) {
      return a + b;
    }

    function mul(a, b) {
      return a + b;
    }

    /***/
  },
};

// 2.The module cache
var __webpack_module_cache__ = {};

// 3.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
  __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

  // Return the exports of the module
  return module.exports;
}

/* webpack/runtime/define property getters */
!(function () {
  // define getter functions for harmony exports
  //给__webpack_require__这个函数添加一个属性:d -> 值为function
  __webpack_require__.d = function (exports, definition) {
    for (var key in definition) {
      if (
        __webpack_require__.o(definition, key) &&
        !__webpack_require__.o(exports, key)
      ) {
        Object.defineProperty(exports, key, {
          enumerable: true,
          get: definition[key],
        });
      }
    }
  };
})();

/* webpack/runtime/hasOwnProperty shorthand */
!(function () {
  __webpack_require__.o = function (obj, prop) {
    return Object.prototype.hasOwnProperty.call(obj, prop);
  };
})();

/* webpack/runtime/make namespace object */
!(function () {
  // 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 });
  };
})();

/***********************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!(function () {
  /*!*************************!*\
  !*** ./src/es_index.js ***!
  \*************************/
  __webpack_require__.r(__webpack_exports__);
  /* harmony import */ var _js_main__WEBPACK_IMPORTED_MODULE_0__ =
    __webpack_require__(/*! ./js/main */ './src/js/main.js');
})();

认识source-map

当代码报错需要调试时(debug),调试转换后的代码是很困难的。这时ource-map是从已转换的代码,映射到原始的源文件中,史浏览器可以重构原始源并在调试器中显示重建的原始源。

source-map文件结构

version:当前使用的版本,也就是最新的第三版;

sources:从哪些文件转换过来的source-map和打包的代码(最初始的文件);

names:转换前的变量和属性名称(因为我目前使用的是development模式,所以不需要保留转换前的名 称); mappings:source-map用来和源文件映射的信息(比如位置信息等),一串base64 VLQ(veriablelength quantity可变长度值)编码;

file:打包后的文件(浏览器加载的文件);

sourceContent:转换前的具体代码信息(和sources是对应的关系); psourceRoot:所有的sources相对的根目录;

不生成source-map的几个值

配置一eval

module.exports = {
  mode: 'development', //模式,
  devtool:"eval",//配置eval
  ...
  ...
};

生成对应的文件信息

eval(
   '......sourceURL=webpack://thunder/./src/index.js?',
        );

配置二默认不填写

source-map

需要在package.json中配置:

module.exports = {
   ...
  devtool:"source-map",
    ...
    ...
};

打包完成后在bundle.js中会有这么一行注释*//# sourceMappingURL=bundle.js.map*指向source-map文件。

eval-source-map

生成sourcemap,但是source-map是以DataUrl添加到eval函数的后面

inline-source-map

会生成sourcemap,但是source-map是以DataUrl添加到bundle文件的后面

cheap-source-map

会生成sourcemap,但是会更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping),在开发中,我们可以定位到错误。

cheap-module-source-map

类似于cheap-source-map,但是对源自loader的sourcemap处理会更好。如果loader对我们的源码进行了特殊的处理,此时我们需要用cmsm

当然我们需要安装一些loader比如时候ES6转换成ES5

npm install @babel/core babel-loader @babel/preset-env -D

进行配置:

module.exports = {
	...
  devtool: 'cheap-module-source-map', 
	...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
	...
};
cheap-source-map和cheap-module-source-map对比

cheap-source-map和cheap-module-source-map的区别:

建议使用cheap-module-source-map

hidden-sorce-map

将boundle.js 中的注释

//# sourceMappingURL=bundle.js.map

删除掉(大可不用)

nosources-source-map

会生成sourcemap,但是生成的sourcemap只有错误信息的提示,不会生成源代码文件(大可不用);

提示信息:

点击错误信息,无法查看源码:

多个值的组合

组合的规则

  1. inline-|hidden-|eval:三个值时三选一;

  2. nosources:可选值;

  3. cheap可选值,并且可以跟随module的值;

[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map

开发阶段:推荐使用 source-map或者cheap-module-source-map。这分别是vue和react使用的值,可以获取调试信息,方便快速开发;

测试阶段:推荐使用 source-map或者cheap-module-source-map。测试阶段我们也希望在浏览器下看到正确的错误提示;

发布阶段:false、缺省值(不写)

认识babel

如果您需要直接运行babel的相关命令需要安装babel-cli

npm install @babel/cli -D

例如:

npx babel src --out-dir result
箭头函数转换
npm install @babel/plugin-transform-arrow-functions -D

例如:

npx babel src --out-dir result --plugins=@babel/plugin-transform-arrow-functions
const转换成var
npm install @babel/plugin-transform-block-scoping -D

例如:

npx babel src --out-dir result --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-block-scoping
预设
npm install @babel/preset-env -D

例如:

npx babel src --out-dir result --presets=@babel/preset-env

当然您也可以在webpack.config.js中进行配置

module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: [
              "@babel/plugin-transform-arrow-functions",//箭头函数
              "@babel/plugin-transform-block-scoping" //const 转换成var
            ], 
            ====>或直接使用“预设”
			presets:[
              	 "@babel/preset-env"
            ]
             
          },
        },
      },
    ],
  },

需要注意的是 .browserslistrc 文件非常重要~

babel编译器原理

一张图,两个链接(github / 相关资料(rb8u )).

options => presets 属性设置
use: {
          loader: 'babel-loader',
          options: {
            presets:[
              ["@babel/preset-env",{
                targets:["chrome 88"],
                esmodules:true
              }]
            ]
          },
        },

内部的适配权重高于. browserslistrc,建议不要采用。

Babel的配置文件

两种文件配置的方式:

babel.config.json (或者.js , .cjs , .mjs) 文件;

.babelrc.json (或者.babelrc , .js , .cjs , .mjs)文件;

在根目录创建 babel.config.js

module.exports = {
    presets:[
        "@babel/preset-env"
    ]
}

将webpack.config.js中的关于babel-loader的options配置注释,在进行测试

认识polyfill

安装

npm install core-js regenerator-runtime --save

配置babel.config.js

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        //配置polyfill
        // useBuiltIns:false, //不适用任何polyfill
        // useBuiltIns:"usage", //代码中需要那些polyfill,就引入polyfill
        useBuiltIns: 'entry', //默认不会生效,需要在入口文件中引入 ==》 index.js
        corejs: 3,
      },
    ],
  ],
};

useBuiltIns: 设置 以什么样的方式来使用polyfill;

corejs: 设置corejs发版本,目前使用较多的是3.x的版本;

为了防止和node_modules中的模块冲突,建议在webpack.config.js中配置:

 rules: [
      {
        test: /\.js$/,
        exclude:/node_modules/, //预防在依赖中也存在babel-loader从而引发的冲突
        use: {
          loader: 'babel-loader',
          options: {
          },
        },
      },
    ],

使用 “entry”时需要在入口文件顶部配置:

import "core-js/stable";
import "regenerator-runtime"

这样会根据browserslist 目标导入所有的polyfill,但是包的体积也会变大

React中jsx支持

在写React代码时需要安装相关依赖:

npm install react react-dom --save

创建react_main.js 文件作为webpack的入口文件

import React, { Component } from 'react';
import ReactDom from 'react-dom';
export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      message: 'thunder',
    };
  }
  render() {
    return <div>{this.state.message}</div>;
  }
}
ReactDom.render(<App></App>, document.getElementById('app'));

此时打包时是不能正常打包的会报错~,需要安装babel ==> @babel/preset-react -D

npm install @babel/preset-react -D

在babel.config.js中配置:

module.exports = {
  presets: [
    [
      '@babel/preset-env',
      {
        useBuiltIns: 'entry'
        corejs: 3,
      },
    ],
    ['@babel/preset-react'],  //重点
  ],
};

webpack编译typeScript

全局安装typescript

npm install typescript -g

测试文件index.ts:

const message: string = 'hello TypeScript';

const foo = (info: string) => {
  console.log(info);
};

foo(message);


export {}

编译指令:

tsc index.ts
ts-loader

安装:

npm intall ts-loader -D

然后在webpack.config.js中进行配置

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude:/node_modules/, //预防在依赖中也存在babel-loader从而引发的冲突
        use: {
          loader: 'babel-loader'
          },
        },
      },
      {
          test:/\.ts$/,
          use:"ts-loader"
      }
    ],
  },

此时在进行打包的时候会出现报错:

原因是缺少tsconfig.json文件

创建tsconfig.json文件命令:

tsc --init

打包:

npm run build
babel编译ts

直接修改webpack.config.js中的配置文件:

{
          test:/\.ts$/,
          exclude:/node_modules/, 
          use:"babel-loader"
      }

当然我们还需要安装预设:

npm install @babel/preset-typescript -D

然后在babel.config.js在配置

 presets: [
    ...
    ['@babel/preset-react'],
    ['@babel/preset-typescript'], //
  ],

npm run build

ts-loader和babel-loader选择

ts-loader:

  1. 直接编译typeScript,只能将ts转换成js;
  2. 如果希望在这个过程中添加对应的polyfill,ts-loader不支持;
  3. 我们需要借助babel来完成polyfill的填充功能。

babel-loader:

  1. 直接编译typeScript,只能将ts转换成js,并且可以实现polyfill的功能;
  2. 但是在编译的过程中,不会对类型错误进行检测。

推荐使用

在package.json中配置

 "scripts": {
    "build": "npm run type-check & webpack --config ./wk.config.js",
    "type-check":"tsc --noEmit"
  },

直接运行 npm run build

或者:

  "scripts": {
    "build": "webpack --config ./wk.config.js",
    "type-check":"tsc --noEmit",
    "type-check-watch":"tsc --noEmit --watch"
  },

首先运行 npm run type-check-watch,此时终端会启动代码调试的功能,调试完毕后(Found 0 errors.Watch for file changes

在运行 npm run build

ESLint

安装
npm install eslint -D

初始化eslint配置文件

npx eslint --init

检测

npx eslint ./src/index.js

相关配置略

webpack中配置eslint-loader

安装:

npm install eslint-loader -D

打包:

npm run build
vscode中使用esLint

webpack中加载vue

安装Vue:

npm install vue 

安装 vue-loader

npm install vue-loader -D

安装 template

npm install vue-template-compiler -D

在webpack.config.js中配置:

const VueLoaderPlugin = require('vue-loader/lib/plugin');

...
 module: {
    rules: [
      {
        test: /\.less$/,
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              importLoaders: 2,
            },
          },
          'postcss-loader',
          'less-loader',
        ],
      },
      {
        test: /\.js$/,
        exclude: /node_modules/, 
        use: ['babel-loader'],
      },
      {
        test: /\.vue$/,
        use: 'vue-loader',
      },
    ],
  },
   plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'thunder webpack',
      template: './public/index.html',
    }),
    new DefinePlugin({
      BASE_URL: '"./" ',
    }),
    new VueLoaderPlugin(),
  ],

打包:

npm run build

webpack的DevServer

完成自动编译,webpack提供了几种可选的方式:

  1. pwebpack watch mode
  2. webpack-dev-server
  3. webpack-dev-middleware
watch

避免每次在修改代码之后,我们需要重新执行npm run build因此需要实时监听代码的变化。

方式一:

在package.json中配置:

 "scripts": {
    "watch":"webpack --watch"
  },
npm run watch

方式二:

直接在webpack.config.js中配置:

module.exports = {
  watch: true,
  ...
};
npm run build
webpack-dev-server

上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的.

它的功能就是

安装:

npm install webpack-dev-server -D

在package.json中配置:

"scripts": {
 	...
    "serve":"webpack serve"
  },
npm run serve

webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中

webpack-dev-middleware

默认请情况下,当我们npm run serve的时候,会默认启动一个服务,我们可以根据自已的需求去自定义一个服务.

  1. webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server(见server.js代码)
  2. webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行 更多自定义设置

安装:

npm install webpack-dev-middleware express

在根目录创建server.js , 直接上代码。

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');

const app = express();

const config = require('./webpack.config');

const compiler = webpack(config);

const middleware = webpackDevMiddleware(compiler);

app.use(middleware);

app.listen(3000, () => {
  console.log('开启3000端口');
});

热更新(HMR)

模块热替换是指在 应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面。

优势:

  • 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失;
  • 只更新需要变化的内容,节省开发的时间;
  • 修改了css、js源代码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式;

不开启HMR的情况下,当修改了源代码之后,整个页面会自动刷新,使用的是live reloading;不能再watch / webpack-dev-middleware 情况下使用

webpack.config.js中添加配置:

devServer: {
    hot: true,
 }

指定某个模块下面编写如下代码:

if (module.hot) {
  module.hot.accept('./math.js', () => {
      console.log('math模块发生了更新');
    });
}
React-HMR

回顾首先安装react:

npm install react react-dom

安装相关babel

npm install @babel/core babel-loader @babel/preset-env @babel/preset-react -D

创建babel.config.js

module.exports = {
  presets: [['@babel/preset-env'], ['@babel/preset-react']],
};

配置webpack.config.js

 module:{
    rules:[
        {
            test:/\.jsx?$/i,
            use:"babel-loader"
        }
    ]
  },

创建jsx文件编写相关代码:

import React, { Component } from 'react'
export default class App extends Component {
    constructor(props) {
        super(props)
    
        this.state = {
             name:'thunder'
        }
    }
    render() {
        return (
            <div>
                {this.state.name}
            </div>
        )
    }
}

在入口文件中引入

import React from 'react'
import  ReactDOM  from 'react-dom'
import ReactApp from './App.jsx'
...
...
ReactDOM.render(<ReactApp></ReactApp>,document.getElementById("app"))

在index.html中添加dom元素

<body>
    <div id="app"></div>
</body>

安装依赖:

npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh

在babel.config.js中添加配置:

module.exports = {
  ...
  
  plugins:[
    ["react-refresh/babel"]
  ]
};

在webpack.config.js中添加

const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin")

...

 plugins: [
  	...
    new ReactRefreshWebpackPlugin()
  ],

运行

npm run serve
Vue-HMR

创建.vue 文件

安装相关依赖

npm install vue
npm install vue-loader vue-template-compiler -D
npm install style-loader css-loader -D

配置webpack.config.js

...

const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
  ...
  
  module: {
    rules: [
    ...
      {
        test: /\.vue$/i,
        use: 'vue-loader',
      },
      {
        test:/\.css$/i,
        use:[
          "style-loader",
          "css-loader"
        ]
      }
    ],
  },
  plugins: [
   ...
   
    new VueLoaderPlugin(),
  ],
};

HMR的原理

webpack-dev-server会创建两个服务:

提供静态资源的服务(express)和Socket服务(net.Socket);express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);

HMR Socket Server,是一个socket的长连接:

长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk);通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新;

原理图:

webpack对路径的处理

output中的path

path:作为webpack打包之后的输出目录,将静态资源的js,css等输入到指定文件夹中如:dist、build等

const path = require('path');
output: {
   ... 
    path: path.resolve(__dirname, './build'),
   ...
  },
output中的publicPath

该属性是指定index.html文件打包引用的一个基本路径

  • 它的默认值是一个空字符串,所以我们打包后引入js文件时,路径是 bundle.js;
  • 如果将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名+路径去请求对应的资源;
  • 如果希望在本地直接打开html文件来运行,会将其设置为 ./,路径时 ./bundle.js,可以根据相对路径去查找资源;
devServer中的publicPath
devserver中的directory

作用是如果打包 后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容:

  1. 在index.html中,需要依赖一个 abc.js 文件,这个文件我们存放在 public文件 中;
  2. 在index.html中,应该如何去引入这个文件呢
    1. 比如代码是这样的<script src="./abc/tc/abc.js"></script>
    2. 是这样打包后浏览器是无法通过相对路径去找到这个文件夹的;
    3. 所以代码是这样的:<script src="/abc.js"></script>;(存在着疑惑)
    4. 设置directory即可;

需要注意的是随着webpack5的更新devserver配置发生了很大的变化 正如第三点,在webpack5.65版本配置如下,可以正常运行,按照webpack5.65以前的版本属性contentBase:path.resolve(__dirname, './abc'),inde.html中配置:<script src="./tc/abc.js"></script>可以正常运行。经过多次测试directory需要在路径前添加./abc/tc/abc.js

目录结构:

index.html:

...
<script src="./abc/tc/abc.js"></script>
...

webpack.config.json

...
output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './build'),
    publicPath: '/abc',
  },
  devServer: {
    hot: true,
    static:{
      directory:path.resolve(__dirname, './abc'),
      publicPath: '/abc',
    },
   
  },
...
devServer中的watchFiles

监听文件的变化,动态更新打包之后的代码,从而不会刷新整个页面.

配置如下:

devServer: {
    hot: true,
    watchFiles:'*',
    ...
  },
      

详细配置见官网: watchFiles

devServer中的Gzip压缩

配置:

devServer: {
   ...
    compress:true, //开启Gzip 压缩
    ...
  },

devServer中的Proxy

设置代理来解决跨域访问的问题

配置:

  devServer: {
    ...
    proxy:{
      '/api':{
          target:'url',
          pathRewrite:{ //重写路径,例如将'/api'替换为空. http://192.168.100.2/api/demo -->  //http://192.168.100.2/demo
              '/api':"",
          secure:false, //跳过HTTPs证书验证
          changeOrigin:true,
          historyApiFallback: true//解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误。
          }
      }
    }
    ...
  },
devServer中的historyApiFallback

解决SPA页面在路由跳转之后,进行页面刷新 时,返回404的错误。

配置:

 devServer: {
 	 ...
    historyApiFallback: true
	...
  },
devServer.resolve中配置extensions

解析文件的后缀名,导入时将后缀删除

配置:

module.exports = {
...
 resolve:{
  extensions:['js','mjs',".json",'.jsx','.vue']
 },
...
}
devServer.resolve中配置alias

配置别名

配置:

module.exports = {
...
 resolve:{
  alias:{
      "@":path.resolve(__dirname,'./src')
    }
 },
...
}

webpack的环境分离

根目录创建 config 文件夹,分别创建 webpack.common.js 、webpack.dev.js 、 webpack.prod.js、path.js

webpack.common.js :

const path = require('path');

module.exports = function (env) {
  const isProduction = env.production;
  return {
      // 配置绝对路径,默认使用 Node.js 进程的当前工作目录
      // context:path.resolve(__dirname,"../"),
     //entry:写上的相对路径,并不是相对于文件所在的路径,而是相对context配置的路径
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, '../build'),
    },
  };
};

最终实现截图:

path.js
const path = require('path');


const appDir = process.cwd();

const resolveApp = (relativePath) => path.resolve(appDir, relativePath);

module.exports = resolveApp;

webpack.common.js
const resolveApp = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

const { merge } = require("webpack-merge");

const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");

const commonConfig = {
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
    path: resolveApp("./build"),
  },
  resolve: {
    extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
    alias: {
      "@": resolveApp("./src"),
      pages: resolveApp("./src/pages"),
    },
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/i,
        use: "babel-loader",
      },
      {
        test: /\.vue$/i,
        use: "vue-loader",
      },
      {
        test: /\.css/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html",
    }),
    new VueLoaderPlugin(),
  ]
};

module.exports = function(env) {
  const isProduction = env.production;
  process.env.NODE_ENV = isProduction ? "production": "development";

  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig, config);

  return mergeConfig;
};

webpack.dev.js
const resolveApp = require('./paths');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');

const isProduction = false;



module.exports = {
  mode: "development",
  devServer: {
    hot: true,
    hotOnly: true,
    compress: true,
    contentBase: resolveApp("./why"),
    watchContentBase: true,
    proxy: {
      "/tc": {
        target: "http://localhost:8888",
        pathRewrite: {
          "^/tc": ""
        },
        secure: false,
        changeOrigin: true
      }
    },
    historyApiFallback: {
      rewrites: [
        {from: /abc/, to: "/index.html"} //当访问	params为/abc时会直接返回index.html
      ]
    }
  },
  plugins: [
    // 开发环境
    new ReactRefreshWebpackPlugin(),
  ]
}
webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const isProduction = true;

module.exports = {
  mode: "production",
  plugins: [
    // 生成环境
    new CleanWebpackPlugin({}),
  ]
}
package.json
  "scripts": {
    "build": "webpack --config ./config/webpack.prod.js",
    "serve": "webpack serve --config ./config/webpack.dev.js",
    "build2": "webpack --config ./config/webpack.common.js --env production",
    "serve2": "webpack serve --config ./config/webpack.common.js --env development"
  },

webpack 手动分离代码

​ 将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件,默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载, 就会影响首页的加载速度,代码分离可以分出出更小的bundle,以及控制资源加载优先级,提供代码的加载性能。

代码分离有三种:

  1. 入口起点:使用entry配置手动分离代码;

  2. 防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;

  3. 动态导入:通过模块的内联函数调用来分离代码;

入口起点及共享

配置一个index.js和main.js的入口,们分别有自己的代码逻辑,假如我们的index.js和main.js都依赖两个库:lodash、dayjs,

如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs,事实上我们可以对他们进行共享.

基本配置如下:

  entry: {
    main: {import:'./src/main.js',dependOn:shared},
    index: {import:'./src/index.js',dependOn:"lodash"},
    lodash: 'lodash',
    dayjs:"dayjs",
    shared:["lodash","dayjs"]
  },
  output: {
    filename: '[name].bundle.js',
    path: resolveApp('./build'),
  },

额外配置:

 optimization: {
    minimize: true,
    //对代码进行压缩的相关操作
    minimizer: [
      new TerserPlugin({
        extractComments: false,//删除注释
      }),
    ],
  },
SplitChunks

另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的

基础配置:

cacheGroups:用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包

test属性:匹配符合规则的包;

name属性:拆分包的name属性;

filename属性:拆分包的名称,可以自己使用placeholder属性;

动态导入(dynamic import)

代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式:

  1. 使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;

  2. 使用webpack遗留的 require.ensure,目前已经不推荐使用;

动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;

通常会在output中,通过 chunkFilename 属性来命名;

默认情况下我们获取到的 [name] 是和id的名称保持一致的,

修改name的值,可以通过magic comments(魔法注释)的方式;

场景:

路由懒加载

chunkIds

optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成。

image.png

有三个比较常见的值:

  1. natural:按照数字的顺序使用id;

  2. named:development下的默认值,一个可读的名称的id;

  3. deterministic:确定性的,在不同的编译中不变的短数字id,有益于长期缓存。在生产模式中会默认开启

  4. 最佳实践:

    1. 开发过程中,我们推荐使用named;
    2. 打包过程中,我们推荐使用deterministic;
webpack懒加载

demo

element.js:


const element = document.createElement('div')

element.innerHTML = "Hello thunder"

export default element

index.js:

const button = document.createElement('button');
button.innerHTML = '点击我吧~';

button.addEventListener('click', () => {
    //魔法注释
  import(/*webpackChunkName: 'elemtn' */ './element.js').then(
    ({ defalut: element }) => {
      document.body.appendChild(element);
    },
  );
});

document.body.appendChild(button);

如果页面进入的时候需要下载某文件,点击按钮加载已经下载的文件:

需要将魔法注释设置为:

/* webpackPrefetch:true */	

/*webpackPreload:true*/

Prefetch VS Preload

  1. preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  2. preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  3. preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻
runtime
  • 配置runtime相关的代码是否抽取到一个单独的chunk中:
  1. runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;

  2. 比如我们的component、bar两个通过import函数相关的代码加载,就是通过runtime代码完成的;

  • 抽离出来后,有利于浏览器缓存的策略:
  1. 比如我们修改了业务代码(main),那么runtime和component、bar的chunk是不需要重新加载的;
  2. 比如我们修改了component、bar的代码,那么main中的代码是不需要重新加载的;
  • 设置的值:
  1. true/multiple:针对每个入口打包一个runtime文件;
  2. single:打包一个runtime文件;
  3. 对象:name属性决定runtimeChunk的名称;

配置如下:

module.exports = {
	...
	optimization:{
		...
		// true/multiple
      // single
      // object: name
      runtimeChunk: {
        name: function(entrypoint) {
          return `tc-${entrypoint.name}`
        }
      }
	}
    ...
}
第三方包CND配置

项目中使用到某些库,但是需要cnd加载,需要添加配置:

 externals:{
    lodash:"_", //暴露出的全局对象
    dayjs:"dayjs"
  },

在打包后的index.html中添加对应的CND链接

例如jQuery:

index.html

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>

webpack.config.js

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

import $ from 'jquery';

$('.my-element').animate(/* ... */);
shimming

比如我们现在依赖一个第三方的库,这个第三方的库本身依赖axios,但是默认没有对axios进行导入(认 为全局存在axios),那么我们就可以通过ProvidePlugin来实现shimming的效果;

代码:

const webpack = require('webpack');
......
  plugins: [
  ...
    //当在代码走遇到某一个变量找不到时,可以通过ProvidePlugin,自动找到对应的库
    new webpack.ProvidePlugin({
      axios: 'axios', //变量名称 : 库
      get: ['axios', 'get'],
    }),
  ],

webpack并不推荐随意的使用shimming

CSS文件抽取

MiniCssExtractPlugin将css提取到一个独立的css文件中,该插件需要在webpack4+才可以使用

npm install mini-css-extract-plugin -D
plugins: [
   ...
  new MinCssExtractPlugin({
    filename:"css/[name].[hash:8].css",
    chunkFilename:"css/[name].[contenthash:8].css"
  })
  ]
 module: {
    rules: [
	...
      {
        test: /\.css/i,
        use: [MinCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
Hash命名

webpack打包后的文件名配置:

  1. [ext] :目标文件/资源的文件扩展名。
  2. [name] :文件/资源的基本名称。
  3. [hash]:指定生成文件内容哈希值的哈希方法。

文件进行命名的时候,会使用placeholder,placeholder中有几个属性比较相似:

  • hash、chunkhash、contenthash
  • hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制);

注意:

  • hash值的生成和整个项目有关系:
  1. 比如我们现在有两个入口index.js和main.js;
  2. 它们分别会输出到不同的bundle文件中,并且在文件名称中我们有使用hash;
  3. 这个时候,如果修改了index.js文件中的内容,那么hash会发生变化;
  4. 那就意味着两个文件的名称都会发生变化
  • chunkhash可以有效的解决上面的问题,它会根据不同的入口进行借来解析来生成hash值:
  1. 比如我们修改了index.js,那么main.js的chunkhash是不会发生改变的;
  • contenthash表示生成的文件hash名称,只和内容有关系:
  1. 比如我们的index.js,引入了一个style.css,style.css有被抽取到一个独立的css文件中;
  2. 这个css文件在命名时,如果我们使用的是chunkhash;
  3. 那么当index.js文件的内容发生变化时,css文件的命名也会发生变化;
  4. 这个时候我们可以使用contenthash;

认识DLL库

DLL全称是动态链接库(Dynamic Link Library) ,是为软件在Windows中实现共享函数库的一种实现方式;

webpack中也有内置的DLL的功能,它指的是我们可以共享,并且不经常改变的代码,抽取成一个共享库;

DLL库的使用分为两步:

  1. 打包一个DLL库;
  2. 项目中引入DLL库;

注意:在升级到webpack4之后,React和Vue脚手架都移除了DLL库(下面的vue作者的回复)

搭建一个webpack环境:

npm init 

安装相关依赖:

npm install webpack webpack-cli react react-dom
npm i clean-webpack-plugin -S-D

新建配置文件webpack.dell.js

const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

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

module.exports = {
  entry: {
    react: ['react', 'react-dom'],
  },
  output: {
    path: path.resolve(__dirname, './dll'),
    filename: 'dll_[name].js',
    library: 'dll_[name]',
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        extractComments: false, //隐藏注释
      }),
    ],
  },
  plugins: [
    new CleanWebpackPlugin(),
    new webpack.DllPlugin({
      name: 'dll_[name]',
      path: path.resolve(__dirname, './dll/[name].manifest.json'),
    })
  ],
};

修改package.json文件:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dll": "webpack --config ./webpack.dll.js"
  }

运行:

npm run dll 

结果:

会在根目录中生成dll文件夹

实践

保留上方的dll文件,webpack.dll.js 建议单独配置.

安装:add-asset-html-webpack-plugin

npm i add-asset-html-webpack-plugin -D

The plugin will add the given JS or CSS file to the files Webpack knows about, and put it into the list of assets html-webpack-plugin injects into the generated html. Add the plugin to your config, providing it a filepath

webpack.common.js 配置插件:

plugins: [
      new HtmlWebpackPlugin({
        template: "./index.html",
      }),
      new VueLoaderPlugin(),
      new webpack.DllReferencePlugin({        manifest:resolveApp("./dll/react.manifest.json"),
        context: resolveApp("./"),
      }),
      new AddAssetHtmlPlugin({
        filepath: resolveApp("./dll/dll_react.js"),
      })
    ],

resolveApp获取绝对路径打印得:

resolveApp("./") : E:\mydata\webpack\webpack的DLL的使用 resolveApp("./dll/dll_react.js") : E:\mydata\webpack\webpack的DLL的使用\dll\react.manifest.json

打包

"scripts": {
  "build": "webpack --config ./config/webpack.common.js --env production",
 },

npm run build

打包的目录如下:

webpack中terser的使用

定义:
  • Terser是一个JavaScript的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集;
  • 早期使用 uglify-js来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的 语法
  • Terser是从 uglify-es fork 过来的,并且保留它原来的大部分API以及适配 uglify-es和uglify-js@3等
  • Terser可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小

安装:

npm install terser
npx

命令行npx使用:

npx terser [input files] [options]

举例:

npx terser js/file1.js -o foo.min.js -c ...Compress option -m ...Mangle option
  • Compress option:

    • arrows:class或者object中的函数,转换成箭头函数;

    • arguments:将函数中使用 arguments[index]转成对应的形参名称;

    • dead_code:移除不可达的代码(tree shaking);

  • Mangle option

  • toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换);

  • keep_classnames:默认值是false,是否保持依赖的类名称;

  • keep_fnames:默认值是false,是否保持原来的函数名称;

例如:

npx terser ./src/abc.js -o abc.min.js -c 
arrows,arguments=true,dead_code -m 
toplevel=true,keep_classnames=true,keep_fnames=true 
TerserPlugin

修改webpack.prod.js

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

module.exports = {
  mode: 'production',
  externals: {
    lodash: '_',
    dayjs: 'dayjs',
  },
  optimization: {
    minimize: true,//改为false,关闭默认的minimize
    minimizer: [
      new TerserPlugin({
        extractComments: false, // 提取注释
        parallel: true, // 多核
        terserOptions: {
          compress: {
            //压缩
            arguments: true,
            dead_code: true,
          },
          mangle: true,
          toplevel: true,
          keep_classnames: true,
          keep_fnames: true,
        },
      }),
    ],
  },
  plugins: [
    // 生成环境
    new CleanWebpackPlugin({}),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:6].css',
    }),
  ],
};

更多配置详见webpack官网

CSS压缩

安装插件:

npm install css-minimizer-webpack-plugin -D

修改webpack.prod.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');



module.exports = {
  mode: 'production',
    
 	...
    
  plugins: [
    // 生成环境
    new CleanWebpackPlugin({}),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:6].css',
    }),
    new CssMinimizerPlugin()
  ],
};
Scope Hoisting

功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快;

  • 默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:
    • 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
    • Scope Hoisting可以将函数合并到一个模块中来运行;
  • 使用Scope Hoisting非常的简单,webpack已经内置了对应的模块:
    • 在production模式下,默认这个模块就会启用;
    • 在development模式下,我们需要自己来打开该模块
const webpack = require('webpack');

plugins: [
	...
  webpack.optimize.ModuleConcatenationPlugin(),
  ],

对比如下:

作用域闭包

作用域提升

Tree Shaking

定义
  • 最早的想法起源于LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进 行函数式编程时,尽量使用纯函数的原因之一);
  • 后来Tree Shaking也被应用于其他的语言,比如JavaScript、Dart;
  • JavaScript的Tree Shaking:
    • JavaScript进行Tree Shaking是源自打包工具rollup
    • Tree Shaking依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);
    • webpack2正式内置支持了ES2015模块,和检测未使用模块的能力;
    • 在webpack4正式扩展了这个能力,并且通过 package.json的 sideEffects属性作为标记,告知webpack在编译时, 哪里文件可以安全的删除掉;
    • webpack5中,也提供了对部分CommonJS的tree shaking的支持;github.com/webpack/cha…
webpack中使用Tree Shaking
  • webpack实现Tree Shaking 采用了两种不同的方案:
    • usedExports: 通过表计某些函数是否被使用,之后通过Terser来进行优化;

    • sideEffects:跳过整个模块/文件,直接查看该文件是否含有副作用.

usedExports

修改配置文件 --- webpack.prod.js:

module.exports = {
  mode: 'development', //为了方便演示需要改为开发环境
  devtool:"source-map",
  externals: {
    lodash: '_',
    dayjs: 'dayjs',
  },
  optimization: {
    usedExports: true,
    minimize: false, //关闭minimize,防止进行terser优化
    minimizer: [
      new TerserPlugin({
        extractComments: false, // 提取注释
        parallel: true, // 多核
        terserOptions: {
          compress: {
            //压缩
            arguments: true,
            dead_code: true,
          },
          mangle: true,
          toplevel: true,
          keep_classnames: true,
          keep_fnames: true,
        },
      }),
    ],
  },
  plugins: [
    // 生成环境
    new CleanWebpackPlugin({}),
    new MiniCssExtractPlugin({
      filename: 'css/[name].[contenthash:6].css',
    }),
    new CssMinimizerPlugin(),
    //new webpack.optimize.ModuleConcatenationPlugin(),  疑问点
  ],
};

测试代码:

math.js:

export function sum(num1, num2) {
  return num1 + num2;
}

export function mul(num1, num2) {
  return num1 - num2;
}

main.js

import { sum } from './math';

sum(1, 2);

打包(webpack --config ./config/webpack.common.js --env production )完成后:

可以看到注释中明确的标记了某个函数没有使用**(usedExports作用)**, 此时打开 minimize:true(开启 terser)

​ 未出现关于 mul 任何代码。

上述配置文件中提到,开启作用域提升ModuleConcatenationPlugin的情况下,我多次测试不会出现 unused harmony export mul 相关信息.

开启ModuleConcatenationPlugin,打包信息如下首先开启usedExports:true , minimize:false

都为true 时:

对此有一些疑惑,不知道如何进行判断。希望评论区有前辈们的回复...

因此 usedExports必须结合Terser使用,将未使用的函数从代码中删除, 默认在production模式中是开启的

sideEffects

添加测试文件formate.js:

export const dataFormate = (data, type) => {
    console.log(data);
}	

在main.js中引入

import { sum } from './math';
import './formate.js';

sum(1, 2);

在package.json中添加配置:

{
  "name": "thunder",
  "version": "1.0.0",
  "description": "",
  "sideEffects": false,  //添加sideEffects:false
  "main": "index.js",
  "scripts": {
    "build": "webpack --config ./config/webpack.common.js --env production",
    "watch": "webpack --watch",
    "serve": "webpack serve --config ./config/webpack.dev.js --env development"
  },
  }

npm run build 后(关闭terser)

sideEffects:true时 两者对比

后者依然加载了formate函数

sideEffects 还可以改写为数组配置如下:

{

	"sideEffects":[

		"./src/format.js",

		"**.css"
	]

}

再次打包时会忽略数组中的文件,保留其副作用.

需要注意的是: 如果直接将值改为false,css文件将不会打包,为了解决这个问题需要在config添加配置:

 {
          test: /\.css/i,
          use: [
            isProduction ? MiniCssExtractPlugin.loader: "style-loader", 
            "css-loader"],
            sideEffects: true,  //保留副作用的模块 ,react脚手架
        },

总结在生产环境中配置tree shaking.

在optimization中配置usedExports为true,来帮助Terser进行优化;

在package.json中配置sideEffects,直接对模块进行优化. 需要在module中添加sideEffects:true

CSS TreeShaking

安装: npm install purgecss-webpack-plugin glob -D

webpack.prod.js文件:

...
const PurgeCssPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');
const resolveApp = require('./paths');
...

module.exports = {
	...
	
	plugins:[
		...
		new PurgeCssPlugin({
      paths: glob.sync(`${resolveApp('./src')}/**/*`, { nodir: true }),//同步匹配文件夹中 的所有文件 nodir: 			true:不是文件夹,而是文件
      safelist: function () {
        return {
          standard: ['body'],//那些标签不会被清除
        };
      },
    }),
	]
}

demo 测试:

main.js(入口文件):

import { sum } from './math';
import './formate.js';
import './style.css';

sum(1, 2);

style.css:

body {
    width: 100%;
    height: 100%;
}

需要注意的是首先需要将 standard: ['body'] 注释, 打包:

打包后的css文件为空, 此时我们需要加上standard属性, body对应的css才会保留.

demo测试2:

修改main.js:

import { sum } from './math';
import './formate.js';
import './style.css';

sum(1, 2);

const titleDiv = document.createElement('div');
titleDiv.className = 'title';
document.body.appendChild(titleDiv);

const h2El = document.createElement('h2');
document.body.appendChild(h2El);

修改style.css

body {
    width: 100%;
    height: 100%;
}
.title {
    font-size: 20px;
    color: #fff;
    text-align: center;
    line-height: 50px;
    background-color: #f00;
}
  h2 {
      color:'#f00';
  }

打包:

main.css:

/*!*****************************************************************!*\
  !*** css ./node_modules/css-loader/dist/cjs.js!./src/style.css ***!
  \*****************************************************************/body{height:100%;width:100%}

疑惑(待讨论): 查阅相关资料有些coder得到的结果,动态创建El,并且css 文件中含有此属性,safelist属性 并未包含此css, 打包后的文件仍然有相关的样式. 经过多次测试发现只有safelist属性含有此属性才会保留. 再次我并没有花费太长的时间,插件的作用就是对css TreeShaking,但是我们会将css提供很多base样式, 因此我更相信前者.

HTTP压缩

定义:HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式,

压缩流程:
  • 第一步: HTTP数据在服务器发送前就已经被压缩了;(可以在webpack中完成)
  • 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式;
  • 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器;
webpack 对文件压缩

安装插件: npm install compression-webpack-plugin -D

配置webpack.config.prod.js

const compressPlugin = require('compression-webpack-plugin');

module.exports = {
    
    ...
    
    plugins:[
        ...
        new compressPlugin({ // gzip压缩
      threshold:0, //文件大小大于0B的时候才会被压缩,受minRation压缩比例的限制
      test:/\.js$|\.css$|\.html$/, //压缩js,css,html
      minRatio:0.8, //压缩比例 默认 0.8
      algorithm: 'gzip', //压缩算法 默认 gzip
    })
    ]
}

npm run build 打包:

HTML 文件中代码中的压缩

安装插件:npm install html-webpack-plugin --save -dev

文件配置 webpack.config.common.js

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
    ...
    
    plugins: [
      new HtmlWebpackPlugin({
        template: "./index.html",
        inject:true,//注入相关js css 文件 可选择属性: false(不注入) / head(头部) / body(body标签)
        catche: true,//开启页面缓存,页面不发生改变时, 使用之前的缓存
        // minify:  true // 默认为false, 开启(true)后会对html进行压缩
        minify:isProduction?{
            //是否移除注释
            removeComments:false,
            removeRedundantAttributes:true,//是否移除多余的属性: 添加input标签添加属性type="text"(默认属性)打包后会将此属性删除
            removeEmptyAttributes:true,//是否移除空属性
            collapseWhitespace:true, //是否折叠空白
            removeStyleLinkTypeAttributes:true,//是否移除link标签中的type属性
            minifyCSS:true,//是否压缩style标签中的css
            // minifyJS:true,//是否压缩script标签中的js
            minifyJS: { //丑化标签中的js
              mangle: {
                toplevel: true,
              }
            }
        }:false// 默认为false, 开启(true)后会对html进行压缩
      }),
   	...
 
    ],
}

更多配置详细见官网 , 基本配置见代码注释, 此配置是对打包后的index.html 进行压缩,截图略

InlineChunkHtmlPlugin

辅助将一些chunk出来的模块,内联到html中,减少不必要的请求

安装:npm install react-dev-utils -D

在webpack.config.js中进行配置:

const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    plugins: {
        new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/]),
    }
}

需要在common.js中配置:

optimization:{
 runtimeChunk: {
        name: function(entrypoint) {
          return `runtime`
        }
      }
}

打包:

webpack自定义npm包(简述)

测试文件:

webpack.config.js:

const path = require('path');
module.exports = {
    mode:'development',
  entry: './index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    //AMD / CommonJS    
    libraryTarget: 'umd',
    library:"myLibrary",
    globalObject: 'this'
  },
};

入口index.js

import * as math from './lib/math';
import * as format from './lib/formate';

export {
    math,
    format,
}

lib- formate、math

//formate
export function dataFormate() {
    return 'dataFormate';
}
//math
export function sum(num1, num2) {
  return num1 + num2;
}

export function mul(num1, num2) {
  return num1 + num2;
}

目录结构:

image.png

  1. npm init
  2. npm login
  3. npm publish

webpack的打包分析

插件时间分析

npm install --save-dev speed-measure-webpack-plugin

webpack.common.js:

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

module.exports = function(env) {
  const isProduction = env.production;
  process.env.NODE_ENV = isProduction ? "production" : "development";

  const config = isProduction ? prodConfig : devConfig;
  const mergeConfig = merge(commonConfig(isProduction), config);

  return  smp.wrap(mergeConfig);
};

特别注意的是,此插件已经很久不维护了,目前支持到webpack4.

打包过程中遇到的相关问题请到github,查找相关的解决办法.

打包文件分析

配置:script命令 package.json:

"scripts": {
  ...
    "stats": "webpack --config ./config/webpack.common.js --env production --profile --json=stats.json"
  },

npm run stats:

在根目录中会生成stats.json 文件

assets": [
    {
      "type": "asset",
      "name": "js/runtime.c518c4.bundle.js",
      "size": 5102,
      "emitted": true,
      "comparedForEmit": false,
      "cached": false,
      "info": {
        "immutable": true,
        "chunkhash": "c518c4",
        "javascriptModule": false,
        "related": {
          "sourceMap": "js/runtime.c518c4.bundle.js.map",
          "gzipped": "js/runtime.c518c4.bundle.js.gz"
        },
        "size": 5102
      }
        ...
      ]
   ...

其次 还可以去专门的网站上进行分析

webpack.github.io/analyse/

上传state.json文件得

插件分析

安装:npm install webpack-bundle-analyzer -D

配置(webpack.prod.js)

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
    plugins: [
   	...
    new BundleAnalyzerPlugin()
  ],
}

打包完毕后会启动Analyzer服务.

webpack源码解析(重中之重)

webpack-cli的启动流程

执行webpack相关命令时,首先来到node_modules下的webpack>bin>webpack.js

首先定义了cli对象,该对象installed属性执行了isInstalled属性,来判断node_modules是否包含webpack-cli. If函数判断是否安装,如果没有安装会有相对的提示信息. 相反会执行runCli函数,该函数require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));这一段代码实际上找到node_modules>webpack-cli>package.json中

 "bin": {
    "webpack-cli": "bin/cli.js"
  },

并且导入cli.js,执行真正的runCLI(process.argv);

回头看runClick的代码const runCLI = require("../lib/bootstrap");.

创建了cli实例,执行了run方法, 继续深入 lib>webpack-cli.js, 找到代码929(webpack:5.65.0)行,run函数: 1. 初始化内置/外置命令 2.loadCommandByName函数中调用makeCommand()函数完成相关命令和启动的操作. 3. runWebpack 和 createCompiler 完整最终的Command合并

流程图如下:

webpack源码解析

主要会提及到两个问题: 1. plugins是什么时候注册的,什么时候运行的? 2. compiler和compilation 的区别

webpack源码 => lib => webpack.js:

webpack函数接受options和callback两个函数

create(){
    ...
	if(Array.isArray(options)) {
        	compiler = createMultiCompiler(
					options
				);
        ...
    }else {
        ...
        compiler = createCompiler(webpackOptions); //核心
    }
return { compiler, watch, watchOptions };

}
if(callback) {
	...
    compiler.run(() =>{})
    return compiler;
}else {
 return compiler;
}

create函数中核心是创建compiler , createCompiler函数(65行左右)主要做了以下几件事:

  1. 创建compiler对象
  2. 对nodeJS中的fs模块封装,同时挂载到compiler对象下
  3. 注册所有Plugins插件(注册不代表执行)
  4. 调用钩子函数 environment 和 afterEnvironment的call()
  5. 处理config文件中的plugins和rules

代码:

const createCompiler = rawOptions => {
	const options = getNormalizedWebpackOptions(rawOptions);
	applyWebpackOptionsBaseDefaults(options);
	const compiler = new Compiler(options.context, options);
	new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
	}).apply(compiler);
	//注册所有插件
	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);

	//调用钩子函数 environment 和 afterEnvironment的call()
	compiler.hooks.environment.call();
	compiler.hooks.afterEnvironment.call();

	//process用于处理config文件中的plugins和rules
	new WebpackOptionsApply().process(options, compiler);
	compiler.hooks.initialize.call();
	return compiler;
};

WebpackOptionsApply类 主要是将传入的属性转成webpack的login注入到webpack的生命周期中 将内置的plugin进行导入(所有plugin实事上贯穿webpack的整个构建流程)

回头看compiler 类,该类定义了webpack生命周期的所有hooks, 每一hook都可以注册(tap)任意事件,当进行编译的到某个生命周期, 会调用call方法调用已经注册的时间,因此Plugins可以在任何阶段进行调用,取决于在那一阶段注册了事件, 我们主要看run方法:

run(callback) {
	const finalCallback = (err,stats) =>{}
	const onComiled = (err, compilation) => {}
	const run = () => {... 	this.compile(onCompiled);}
	if (this.idle) {
			this.cache.endIdle(err => {
				if (err) return finalCallback(err);
				this.idle = false;
				run();
			});
		} else {
			run();
		}
}

内部的run函数执行 compile 方法开始编译

compile(callback) {
	this.hooks.beforeCompile.callAsync(params, err => {
			...
            this.hooks.compile.call(params);
			const compilation = this.newCompilation(params);
			const logger = compilation.getLogger("webpack.Compiler");
				..
			//make的call
			this.hooks.make.callAsync(compilation, err => {
					...
				this.hooks.finishMake.callAsync(compilation, err => {
					
                        ...
				});
			});
		});
}

compile 在webpack构建的时候所有阶段compiler都是一个全局存在的对象,(before - run - beforeCompiler - compile - make - finishMake - afterCompiler - done),只要是webpack的编译,都会先创建一个compiler

compilation**是到准备编译模块(比如main.js)才会创建compilation对象,主要存在于compile - make(之前)阶段

比如: watch -> 源代码发生改变就需要重新编译模块comPiler 可以继续使用,如果修改了webpack的配置, 那么需要重新执行npm run build*, Compilation 需要创建一个新的compilation对象*

那么什么时候编译的相关compilation呢?

结论: 模块编译发生在make阶段:hooks.make.callAsyncWebpack.js : new WebpackOptionsApply().process(options, compiler)找到WebpackOptionsApply.js(335行左右)入口处理 new EntryOptionPlugin().apply(compiler); 来到EntryOptionPlugin.js文件:静态方法applyEntryOption:

static applyEntryOption(compiler, context, entry) {
    console.log(context) //E:\mydata\webpack\webpack-5.72.0\itc
		if (typeof entry === "function") {
			const DynamicEntryPlugin = require("./DynamicEntryPlugin");
			new DynamicEntryPlugin(context, entry).apply(compiler);
		} else {
			const EntryPlugin = require("./EntryPlugin");
			console.log(37, entry);  // { main: { import: [ './src/main.js' ] } }
			for (const name of Object.keys(entry)) {
				const desc = entry[name];
				const options = EntryOptionPlugin.entryDescriptionToOptions(
					compiler,
					name,
					desc
				);
				for (const entry of desc.import) {
					new EntryPlugin(context, entry, options).apply(compiler);
				}
			}
		}
	}

entry参数就是指配置中的入口文件 , 我们重点关注EntryPlugin.js: new EntryPlugin()

class EntryPlugin {
	constructor(context, entry, options) {
		this.context = context;
		this.entry = entry;
		this.options = options || "";
	}

	apply(compiler) {
		compiler.hooks.compilation.tap(
			"EntryPlugin",
			(compilation, { normalModuleFactory }) => {
				compilation.dependencyFactories.set(
					EntryDependency,
					normalModuleFactory
				);
			}
		);
		const { entry, options, context } = this;
		const dep = EntryPlugin.createDependency(entry, options);
		compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { //注册make hooks
			compilation.addEntry(context, dep, options, err => {
				callback(err);
			});
		});

	...
}

我们终于找到make.tapAsync, 因此当开始编译执行到make生命周期时会将所有的模块进行编译

接下来我们分析Compilation.js:Compilation中的addEntry :

	addEntry(context, entry, optionsOrName, callback) {
		const options =
			typeof optionsOrName === "object"
				? optionsOrName
				: { name: optionsOrName };
        // context : E:\mydata\webpack\webpack-5.72.0\itc 根目录
        // entry : EntryDependency {_parentModule: undefined,_parentDependenciesBlock: undefined,_parentDependenciesBlockIndex: -1,weak: false,optional: false,	..._loc: { name: 'main' },request: './src/main.js',userRequest: './src/main.js',range: undefined,assertions: undefined} 入口文件
        //options : {name: 'main',filename: undefined,runtime: undefined,layer: undefined,dependOn: undefined,baseUri: undefined,publicPath: undefined,chunkLoading: undefined,asyncChunks: undefined,wasmLoading: undefined,library: undefined} 相关配置
		this._addEntryItem(context, entry, "dependencies", options, callback);
	}

进入当前文件下的 _addEntryItem 方法:

_addEntryItem(context, entry, target, options, callback) {
		const { name } = options;
		let entryData =
			name !== undefined ? this.entries.get(name) : this.globalEntry;
    
		...
        
		this.hooks.addEntry.call(entry, options);

		this.addModuleTree(
			{
				context,
				dependency: entry,
				contextInfo: entryData.options.layer
					? { issuerLayer: entryData.options.layer }
					: undefined
			},
			(err, module) => {
				if (err) {
					this.hooks.failedEntry.call(entry, options, err);
					return callback(err);
				}
				this.hooks.succeedEntry.call(entry, options, module);
				return callback(null, module);
			}
		);
	}

最终的目的是将所有的模块组合成一个模块树.

addModuleTree({ context, dependency, contextInfo }, callback) {
		...
		this.handleModuleCreation(
			{
				factory: moduleFactory,
				dependencies: [dependency],
				originModule: null,
				contextInfo,
				context
			},
			(err, result) => {
				if (err && this.bail) {
					callback(err);
					this.buildQueue.stop();
					this.rebuildQueue.stop();
					this.processDependenciesQueue.stop();
					this.factorizeQueue.stop();
				} else if (!err && result) {
					callback(null, result);
				} else {
					callback();
				}
			}
		);
	}

所有模块最终的结构是转换成模块图, factorizeModule将所有的相关方法添加到队列中,执行addModule,将所有的模块全部添加到模块队列中, 当监听到hook的时候在处理对应的模块

handleModuleCreation(
		{
			factory,
			dependencies,
			originModule,
			contextInfo,
			context,
			recursive = true,
			connectOrigin = recursive
		},
		callback
	) {
    
		const moduleGraph = this.moduleGraph;

		const currentProfile = this.profile ? new ModuleProfile() : undefined;

		this.factorizeModule(
			{
				currentProfile,
				factory,
				dependencies,
				factoryResult: true,
				originModule,
				contextInfo,
				context
			},
			(err, factoryResult) => {
				...
                this.addModule(){
                    this._handleModuleBuildAndDependencies();
                } 
			}
		);
	}

最终调用**_handleModuleBuildAndDependencies()**开始真正的构建(buildModule),将所有的构建模块添加到buildquene队列中:

_handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
    ...
    /**
    * buildModule(module, callback) {this.buildQueue.add(module, callback);}
    */
		this.buildModule(module, err => {
			if (creatingModuleDuringBuildSet !== undefined) {
				creatingModuleDuringBuildSet.delete(module);
			}
			if (err) {
				if (!err.module) {
					err.module = module;
				}
				this.errors.push(err);

				return callback(err);
			}

			...

			this.processModuleDependencies(module, err => {
				if (err) {
					return callback(err);
				}
				callback(null, module);
			});
		});
	}

每次在buildQuene.add添加模块的时候通过_ensureProcessing函数监测处理进度,然后通过this.hooks.added.call(item);将需要打包的文件添加到队列中,最终会调用 _buildModule, 最终执行module.build开始构建,最终通过hooks.succeedModule生命周期构建完成

module.build 中的build方法继承自NormalModule.js下的build

以上就是compilation的编译过程

对于以上的解析有很大的不足. 对于webpack的源码,咨询很多的开发者,很多人回复是不难. 强烈建议下载一份源代码按照我上面的步骤去阅读. 后续我会继续更新,添加更加详细的注解

相关截图(来源 coderwhy)

自定义loader

loader是干啥的?它是用于对模块的源代码进行转换(处理),比如css-loader、styleloader、babel-loader等

搭建webpack基础环境, 创建loaders文件夹创建tc-loader01.js:

module.exports = function (content, sourceMap) {
		
    console.log(content,'这是我的loader'); //content:值函数的上下文 console.log('hello main.js'); 这是我的loader
  return content;
};

创建webpack.config.js:

const path = require('path');
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './build'),
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'tc-loader01.js',
      },
    ],
  },
  resolveLoader: {
    //默认配置为 node_modules. 数组的含义: 如果node_modules中没有找到就去下一个路径去找
    modules: ['node_modules', './loaders'],
  },
};

pitch-loader

为了方便测试 , 创建 tc-loader02.js , tc-loader03.js

修改webpack.config.js

rules: [
      {
        test: /\.js$/,
        use: ['tc-loader01.js','tc-loader02.js','tc-loader03.js']
      },
    ],

npm run build 打印得:

pitch
pitch02
pitch03
console.log('hello main.js'); 这是我的loader03
console.log('hello main.js'); 这是我的loader02
console.log('hello main.js'); 这是我的loader

对于打印的顺序可以来到源码:loader-runner库: 比较熟悉的runLoaders方法,实际上会执行iteratePitchingLoaders函数,该函数就是遍历Pitchloaders其中会对loaderContext.loaderIndex++,因此优先打印pitch. 最后调用loadLoader函数方法,其中执行

loaderContext.loaderIndex--;
iterateNormalLoaders(options, loaderContext, args, callback);

倒序打印normalLoader

那么如何更改loader的执行顺序呢?

答案:设置enforce

enforce

enforce一共有四种方式:

  • 默认所有的loader都是normal;
  • 在行内设置的loader是inline(在前面将css加载时讲过,import 'loader1!loader2!./test.js');
  • 也可以通过enforce设置 pre(前置) 和 post(后置);

在Pitching和Normal它们的执行顺序分别是:

  • post, inline, normal, pre;
  • pre, normal, inline, post;
同步loader和异步loader
//异步loader
module.exports = function(content) {
  console.log(content,'这是我的async loader');

  const callback = this.async();
  setTimeout(() => {
    callback(null,content)
  },2000)
}


//同步loaders
module.exports = function (content, sourceMap) {

    console.log(content,'这是我的loader');
    //同步的loader, 两种方法返回数据
  // 1. return content;
  this.callback(null, content, sourceMap); // 2. 
};

module.exports.pitch = function() {
  console.log('pitch');
}
loader参数处理

修改webpack.config.js:

 module: {
    rules: [
      {
        test: /\.js$/i,
        use:{
          loader: 'tc-loader01.js',
          options:{
            name:'itc',
            age:18
          }
        }

      },
    ],
  },

tc-loader01.js:

module.exports = function (content) {
  console.log(content, '这是我的async loader');

  //传入的参数
  const options = this.getOptions();
  console.log(options, 'options'); //{ name: 'itc', age: 18 } options
  const callback = this.async();
  setTimeout(() => {
    callback(null, content);
  }, 2000);
};
参数校验

安装依赖schema-utils: npm install schema-utils -D

相关参数同上

添加配置文件loader01:

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "The name of the user"
    },
    "age": {
      "type": "number",
      "description": "The age of the user"
    }
  },
    //附加其他参数
   "additionalProperties": true 
}

修改tc-loader01.js:

const schema = require('../tc-schema/loader01.json')
//异步loader
module.exports = function (content) {
  console.log(content, '这是我的async loader');
  //传入的参数
  const options = this.getOptions();
  validate(schema,options,{
      name:'loader01',
  })
 
  const callback = this.async();
  setTimeout(() => {
    callback(null, content);
  }, 2000);
};

如果填入的类型与type不符, 报错信息会以description为准

自定义babel-loader

安装依赖: npm install @babel/core @babel/preset-env -D

添加tcbabel-loader.js文件

const babel = require('@babel/core');

module.exports = function (content) {
    //设置为异步的loader
    const callback = this.async();
    //获取传入的参数
    const options = this.getOptions();

    //对源代码进行转换
    babel.transform(content,options,(err,result)=>{
        if (err) {
            callback(err)
        }else {
            callback(null,result.code)
        }
    })
}

修改webpack.config.js 配置:

const path = require('path');
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './build'),
  },
  module: {
    rules: [
      {
        test: /\.js$/i,
        use: {
          loader: 'tcbabel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
    ],
  },
  resolveLoader: {
    //默认配置为 node_modules
    modules: ['node_modules', './loaders'],
  },
};

自定义md.loader

安装依赖:npm install marked

创建doc.md文件:

# 学习webpack
```js
console.log("Hello Loader");

const message = "Hello World";
console.log(message);

const foo = () => {
  console.log("foo");
}

foo();
```

创建文件tcmd-loader.js:

const marked = require('marked')

module.exports = function(content) {
    const htmlContent = marked.parse(content)
    return htmlContent;
}

配置webpack.config.js:

...
 {
        test: /\.md$/i,
        use: [
          "tcmd-loader"
        ]
      },
...

main.js中引入md文件, npm run build之后会发现报错, 提示You may need an additional loader to handle the result of these loaders.

这时我们需要安装npm install html-loader -D ,修改配置文件:

use: [
          "html-loader",
          "tcmd-loader"
     ]

npm run build 打包成功

我们可以将html添加到body中, 修改main.js

import code from './doc.md'

let a = 10
console.log('hello main.js');

document.body.innerHTML = code

在次打包运行html:

那么如何将code 高亮呢?

安装依赖:npm install css-loader style-loader highlight.js

修改tcmd-loader.js:

const marked = require('marked')
const hljs = require('highlight.js')


module.exports = function(content) {
    marked.setOptions({
        highlight: function(code, lang, callback) {
            return hljs.highlightAuto(code).value
        }
      });
    const htmlContent = marked.parse(content)

    //如果不是用html-loader, 手动转换需要如下配置:
    const innerContent = "`"+ htmlContent + "`";
    const moduleCode = `var code=${innerContent}; export default code;`;

    return moduleCode;
}

添加code样式main.js:

...
import "highlight.js/styles/default.css";
...

修改webpack.config.js 添加loader:

{
        test: /\.css$/i,
        use: [
          'style-loader',
          'css-loader',
        ]
      }

npm run build :

自定义Plugins

认识tapable

​ webpack中有两个重要的类CompilerCompilation, 他们通过注入的方式来监听webpack的生命周期, 插件的注入离不开Hook, 其中webpack是创建了Tapable库中各种的Hook实例.

安装:npm install tapable -D

同步Hook(测试代码):

const { SyncHook, SyncBailHook, SyncLoopHook,SyncWaterfallHook } = require('tapable');

class TCLearnTabable {
  constructor() {
    this.counter = 0;
    this.hooks = {
      //syncHook: new SyncHook(['name', 'age']),
      //bail: 在某一个事件监听函数中, 如果有返回值,那么后续监听的事件就不会执行了
      // syncHook: new SyncBailHook(['name', 'age']),
      //loop: 在某个事件监听函数中,如果返回值为true,那么这个回调函数会一直循环执行,返回undefined就停止执行
    //   syncHook: new SyncLoopHook(['name', 'age']),
    //waterfall:第一次监听的函数返回值,会作为下一个监听函数的第一个参数
    syncHook: new SyncWaterfallHook(['name', 'age']),

    };
    this.hooks.syncHook.tap('event1', (name, age) => {
      if (this.counter++ < 3) {
        console.log('event1', name, age);
        return 'event1';
      }
    });
    
    this.hooks.syncHook.tap('event2', (name, age) => {
      console.log('event2', name, age);
    });
  }
  emit() {
    this.hooks.syncHook.call('why', 'age');
  }
}

const lt = new TCLearnTabable();
lt.emit();

详细说明建议查看github文档

异步Hook(测试代码):

const {
  AsyncSeriesHook,
  AsyncParallelHook
} = require('tapable');

class TCLearnTabable {
  constructor() {
    this.counter = 0;
    this.hooks = {
        //series:在一个hook中,监听了两次事件(两个回调函数),这两个回调是串行执行
      // asyncHook: new AsyncSeriesHook(['name', 'age']),
      //parallel:子啊一个hook中,监听了多个事件, 是并行执行订单
      asyncHook: new AsyncParallelHook(['name', 'age']),
    };
    this.hooks.asyncHook.tapAsync('event1',(name,age,callBack) => {
      setTimeout(() => {
        console.log('event1', name, age);
        callBack();
      },2000)
  })


  this.hooks.asyncHook.tapAsync('event2',(name,age,callBack) => {
    setTimeout(() => {
      console.log('event2', name, age);
      callBack();
    },2000)
})

this.hooks.asyncHook.tapPromise("eventPromise",(name,age) => {
  return new Promise((resolve,reject) => {
    setTimeout(() => {
      console.log('eventPromise', name, age);
      resolve();
    },2000)
  })
})
  }
  emit() {
    this.hooks.asyncHook.promise("thunder",30).then((result) => {
      console.log("第二次程序执行完成");
    })
  }
}
const lt = new TCLearnTabable();
lt.emit();

/*并行
event1 thunder 30
event2 thunder 30      
eventPromise thunder 30
第二次程序执行完成 
*/

自定义plugin

插件- 打包后自动上传

const { NodeSSH } = require('node-ssh');

class AutoUploadPlugin {
  constructor(options) {
    this.ssh = new NodeSSH();
    this.options = options;
  }
  apply(compiler) {
    compiler.hooks.afterEmit.tapAsync(
      'AutoUpLoadPlugin',
      async (compilation, callback) => {
        console.log('自动上传文件');
        //1.获取输出的文件夹
        const outputPath = compilation.outputOptions.path;
        console.log(outputPath);
        //2.链接服务器ssh
        await this.connectServer();
        //3.删除文件中的内容
        const serverDir = this.options.remotePath;
        await this.ssh.execCommand(`rm -rf ${serverDir}/*`);

        //4.上传文件到服务器(ssh链接)
        await this.uploadFiles(outputPath, serverDir);

        //5.关闭ssh链接
        this.ssh.dispose();
        callback();
      },
    );
  }

  async connectServer() {
    try {
      await this.ssh.connect({
        host: this.options.host,
        username: this.options.username,
        password: this.options.password,
      });
      console.log('ssh链接成功');
    } catch (err) {
      console.log(err);
    }
  }

  async uploadFiles(localPath, remotePath) {
    const status = await this.ssh.putDirectory(localPath, remotePath, {
      recursive: true, //递归上传
      concurrency: 10, //并发上传
    });
    console.log('传送到服务器', status ? '成功' : '失败');
  }
}

module.exports = AutoUploadPlugin;

React_App

创建react项目:npx create-react-app test_app

显示配置文件:npm run eject

待补充

Vue_App

待补充

Gulp(凉了)

什么是Gulp?

gulp 也是一种自动化构建工具, 他是基于文件Stream的构建流 ,将文件压缩压缩、整合、移动.

gulp可以定义一系列任务,等待任务被执行 . 而 webpack是一个模块化打包工具,可以使用各种各样的loader加载不同的模块,还可以使用各种plugins插件在webpack的打包生命周期中完成其他任务. gulp相对webpack 更加简单、易用,更适合编写自动化的任务.当然最重要的差别是gulp不支持模块化.

基本使用(单任务执行)
  1. 安装npm install gulp -g

  2. 创建gulpfile.js,创建一个任务:

    const { task } = require('gulp');
    
    //定义任务
    const foo = (cb) => {
      console.log('foo');
    
      cb();
    };
    //gulp 4版本以前的写法
    task('bar', (cb) => {
      console.log('bar');
      cb();
    });
    
    module.exports = {
      foo,
    };
    
    //默认任务 npx gulp
    module.exports.default = (cb) => {
        console.log("default, task");
        cb()
    };
    
    
  3. 运行:

    npx gulp foo
    
  4. 结果:

多任务执行

代码:

const {series,parallel} = require('gulp');

const task1 = (cb) => {
  setTimeout(() => {
    console.log(1);
    cb();
  }, 2000);
};

const task2 = (cb) => {
  setTimeout(() => {
    console.log(2);
    cb();
  }, 2000);
};

const task3 = (cb) => {
  setTimeout(() => {
    console.log(3);
    cb();
  }, 2000);
};

const seriesTasks = series(task1,task2,task3)//串行执行
const parallelTask = parallel(task1,task2,task3)//并行执行

const composeTask = series(parallelTask,seriesTasks)
module.exports = {
    seriesTasks,parallelTask,composeTask
}

打印结果

  1. console.log(seriesTasks)

  1. console.log(parallelTask)

  1. console.log(composeTask)

Gulp的文件匹配和监听

创建相关文件,编写代码:

/ => src => main.js

gulpfile.js

安装相关依赖:

npm install @babel/core @babel/preset-env gulp-babel gulp-terser -D

编写代码

gulpfile.js:

const { src, dest,watch } = require('gulp');
const babel = require('gulp-babel');
const terser = require('gulp-terser');

const jsTask = () => {
    // src('./src/main.js')
  return src('./src/*.js')
    .pipe(babel({ presets: ['@babel/preset-env'] }))
    .pipe(terser({ mangle: { toplevel: true } }))
    .pipe(dest('./dist'));
};
watch("./src/**/*.js", jsTask);
module.exports = {
  jsTask,
};

gulp-babel: ES6 => ES5

gulp-terser: 丑化及压缩

watch:监听文件变化,同步更新

需要注意的是,watch可以监听多个任务,用上一届提到的API替换即可

运行

npx gulp jsTask

案例练习

Rollup

Rollup是一个JavaScript的模块化打包工具,可以帮助我们编译小的代码到一个大的、复杂的代码中,比如一个库或者一个应用程序;

Rollup也是一个模块化的打包工具,但是Rollup主要是针对ES Module进行打包的;

vue、react、dayjs源码本身都是基于rollup的

打包命令: (-f : 文件格式, -o : 输出位置)

  1. commonJS : npx rollup ./src/main.js -f cjs -o dist/bundle.js
  2. iife(立即执行函数): npx rollup ./src/main.js -f iife -o dist/bundle.browser.js
  3. amd: npx rollup ./src/main.js -f iife -o dist/bundle.amd.js
  4. umd : npx rollup ./src/main.js -f umd --name uitils -o dist/bundle.js
基本使用

安装:npm install rollup -D

main.js:

const message = 'Hello World';
console.log(message);

export const abc = () => {
  console.log(66767);
};

运行打包命令即可

配置文件说明

修改package.json文件:

"scripts": {
 	...
    "build": "rollup -c"
  },
  

安装相关依赖

npm install @babel/core @babel/preset-env @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup rollup-plugin-terser rollup-plugin-postcss rollup-plugin-vue vue-template-compiler rollup-plugin-serve rollup-plugin-livereload rollup-plugin-replace -D

npm install lodash sass vue (测试)

基础配置:

新建src => main.js:

import { dateFormat } from './utils/formate.js';
import _ from 'lodash';

const message = 'Hello World';
console.log(message);

console.log(dateFormat());
console.log(_.join(["abc","cba"]));
export const abc = () => {
  console.log(66767);
};

新建 utils => formate.js:

export const dateFormat = () => {
    return 1111
}

基础配置文件rollup.config.js:

module.exports = {
    input: 'src/main.js',
    output:[{ //多文件输出
        format:"umd",
        name:"utils",
        file:"dist/itc.util.js"
    },
    {
        format:'cjs',
        file:"dist/itc.util.cjs.js"
    },
    {
        format:'es',
        file:"dist/itc.util.es.js"
    },
    {
        format:"amd",
        file:"dist/itc.util.amd.js"
    },
    {
        format:"iife",
        name:"itc",
        file:"dist/itc.util.iife.js"
    }
]
}

新建index.html 运行, 引入itc.util.js文件,运行提示为 Cannot read properties of undefined (reading 'join'),这时我们需要引入lodashJS库

<body>
    <script src="./node_modules/lodash/lodash.js"></script>
    <script src="./dist/itc.util.js"></script>
    <script>
        console.log(utils.abc());
    </script>
</body>

其他配置:

因为上述我们引入了babel所以新建babel.config.js:

module.exports = {
  presets: ['@babel/preset-env'],
};

新建 styles => style.css

body {
    background-color: red;
}

新建vue => app.vue

<template>
    <div>   
            wo shi App
    </div>
</template>

<script>
    export default {
        name:"app"
    }
</script>

<style  scoped>

</style>

更新main.js

import { dateFormat } from './utils/formate.js';
import _ from 'lodash';
import './css/style.css';
import Vue from 'vue';
import VueApp from './vue/app.vue';

new Vue({
  render: (h) => h(VueApp),
}).$mount('app');
const message = 'Hello World';
console.log(message);

console.log(dateFormat());
console.log(_.join(['abc', 'cba']));
export const abc = () => {
  console.log(66767);
};

external中不包含vue, 会出现process is not defined为了解决这个问题需要安装:npm install rollup-plugin-replace -D

更新配置文件replace({'process.env.NODE_ENV':JSON.stringify('production')}),传递环境变量

配置serve: 打包完成之后,自动打开浏览器,详情见配置文件

配置文件监听:两种方式:一种是在package.json中配置"rollup -c -w" 另一种是配置在config.js中配置watch 详情见配置文件

配置livereload:配置实时刷新(修改文件之后刷新页面)

更新rollup.config.js:

import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import {terser} from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
import vue from 'rollup-plugin-vue';

import replace from 'rollup-plugin-replace';
import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';

export default {
  input: 'src/main.js',
  output: {
    format: 'umd',
    name: 'utils',
    file: 'dist/itc.util.js',
    globals: { //在html文件中必须引入对应的JS文件
        lodash: '_',
        vue: 'Vue',
      },
  },
  watch:{
    include: 'src/**',
    exclude: 'node_modules/**'
  },
  external: ['lodash','vue'], //排除node_modules中的模块 这个非常重要,如果不排除,打包后的JS会包含相关依赖

  plugins: [
    commonjs(), //解决某个插件中使用了 CJS 导出的模块
    resolve(), //引用node_modules值的模块
    replace({
      "process.env.NODE_ENV": JSON.stringify("production")
    }),
    babel({
        babelHelpers: 'bundled',//辅助函数
        exclude: 'node-modules/**',
    }), //转译es6语法
    postcss(), //转译css
    vue(), //转译vue
    terser(), //压缩
    serve({
      open: true,//自动打开浏览器
      port:8080,//端口
      contentBase: '.',//服务器根目录,哪一个文件夹
    }),
     livereload(),//实时刷新
  ],
};


环境分离

修改package.json:

 "build": "rollup -c --environment NODE_ENV:production",
 "serve": "rollup -c --environment NODE_ENV:development"

修改配置文件

...

const isProduction = process.env.NODE_ENV === 'production';
const plugins = [
  commonjs(), //解决某个插件中使用了 CJS 导出的模块
  resolve(), //引用node_modules值的模块
  replace({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
  }),
  babel({
    babelHelpers: 'bundled', //辅助函数
    exclude: 'node-modules/**',
  }), //转译es6语法
  postcss(), //转译css
  vue(), //转译vue
];

if (isProduction) {
  plugins.push(terser()); //压缩
} else {
  const devPlugins = [
    serve({
      open: true, //自动打开浏览器
      port: 8080, //端口
      contentBase: '.', //服务器根目录,哪一个文件夹
    }),
    livereload(), //实时刷新
  ];
  plugins.push(...devPlugins);
}
export default {
 ...
  plugins
};

Vite