关于前端:Webpack常见面试题总结

7次阅读

共计 21848 个字符,预计需要花费 55 分钟才能阅读完成。

一、谈谈你对 Webpack 的了解

1.1 背景

Webpack 的指标是实现前端我的项目的模块化,从而更高效地治理和保护我的项目中的每一个资源。在晚期的前端我的项目中,咱们通过文件划分的模式来实现模块化,也就是将每个性能及其相干状态数据各自独自放到不同的 JS 文件中。约定每个文件是一个独立的模块,而后再将这些 js 文件引入到页面,一个 script 标签对应一个模块,而后再调用模块化的成员。比方:

<script src="module-a.js"></script>
<script src="module-b.js"></script>

但这种模块化开发的弊病也非常显著,模块都是在全局中工作,大量模块成员净化了环境,模块与模块之间并没有依赖关系、保护艰难、没有公有空间等问题。随后,就呈现了命名空间形式,规定每个模块只裸露一个全局对象,而后模块的内容都挂载到这个对象中。

window.moduleA = {method1: function () {console.log('moduleA#method1')
  }
}

不过,这种形式也没有解决第一种形式的依赖等问题。接着,有呈现了应用立刻执行函数为模块提供公有空间,通过参数的模式作为依赖申明。

(function ($) {
  var name = 'module-a'

  function method1 () {console.log(name + '#method1')
    $('body').animate({margin: '200px'})
  }
    
  window.moduleA = {method1: method1}
})(jQuery)

上述的形式晚期解决模块的形式,然而依然存在一些没有解决的问题。例如,咱们是用过 script 标签在页面引入这些模块的,这些模块的加载并不受代码的管制,工夫一久保护起来也非常的麻烦。

除了模块加载的问题以外,还须要规定模块化的标准,现在风行的则是 CommonJS、ES Modules。

特地是随着前端我的项目的越来越大,前端开发也变得非常的简单,咱们常常在开发过程中会遇到如下的问题:

  • 须要通过模块化的形式来开发
  • 应用一些高级的个性来放慢咱们的开发效率或者安全性,比方通过 ES6+、TypeScript 开发脚本逻辑,通过 sass、less 等形式来编写 css 款式代码
  • 监听文件的变动来并且反映到浏览器上,进步开发的效率
  • JavaScript 代码须要模块化,HTML 和 CSS 这些资源文件也会面临须要被模块化的问题
  • 开发实现后咱们还须要将代码进行压缩、合并以及其余相干的优化

而 Webpack 的呈现,就是为了解决以上问题的。总的来说,Webpack 是一个模块打包工具,开发者能够很方面应用 Webpack 来治理模块依赖,并编译输出模块们所须要的动态文件。

1.2 Webpack

Webpack 是一个用于古代 JavaScript 应用程序的动态模块打包工具,能够很方面的治理模块的恶依赖。

1.2.1 动态模块

此处的动态模块指的是开发阶段,能够被 Webpack 间接援用的资源(能够间接被获取打包进 bundle.js 的资源)。当 Webpack 解决应用程序时,它会在外部构建一个依赖图,此依赖图对应映射到我的项目所需的每个模块(不再局限 js 文件),并生成一个或多个 bundle,如下图。

1.2.2 Webpack 作用

  • 编译代码能力,提高效率,解决浏览器兼容问题
  • 模块整合能力,进步性能,可维护性,解决浏览器频繁申请文件的问题
  • 万物皆可模块能力,我的项目维护性加强,反对不同品种的前端模块类型,对立的模块化计划,所有资源文件的加载都能够通过代码管制。

二、说说 webpack 的构建流程

webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会播送事件,插件只须要监听它所关怀的事件,就能退出到这条 webpack 机制中,去扭转 Webpack 的运作。

从启动到完结会顺次经验三大流程:

  • 初始化阶段:从配置文件和 Shell 语句中读取与合并参数,并初始化须要应用的插件和配置插件等执行环境所须要的参数。
  • 编译构建阶段:从 Entry 收回,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译解决。
  • 输入阶段:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输入到文件系统。

2.1 初始化阶段

初始化阶段次要是从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。配置文件默认下为 webpack.config.js,也或者通过命令的模式指定配置文件,次要作用是用于激活 webpack 的加载项和插件。上面是 webpack.config.js 文件配置,内容分析如下正文:

var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');

module.exports = {
  // 入口文件,是模块构建的终点,同时每一个入口文件对应最初生成的一个 chunk。entry: './path/to/my/entry/file.js',// 文件门路指向(可放慢打包过程)。resolve: {
    alias: {'react': pathToReact}
  },
  // 生成文件,是模块构建的起点,包含输入文件与输入门路。output: {path: path.resolve(__dirname, 'build'),
    filename: '[name].js'
  },
  // 这里配置了解决各模块的 loader,包含 css 预处理 loader,es6 编译 loader,图片解决 loader。module: {
    loaders: [
      {
        test: /\.js$/,
        loader: 'babel',
        query: {presets: ['es2015', 'react']
        }
      }
    ],
    noParse: [pathToReact]
  },
  // webpack 各插件对象,在 webpack 的事件流中执行对应的办法。plugins: [new webpack.HotModuleReplacementPlugin()
  ]
};

webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins。实现上述步骤之后,则开始初始化 Compiler 编译对象,该对象掌控者 webpack 申明周期,不执行具体的工作,只是进行一些调度工作。

class Compiler extends Tapable {constructor(context) {super();
        this.hooks = {beforeCompile: new AsyncSeriesHook(["params"]),
            compile: new SyncHook(["params"]),
            afterCompile: new AsyncSeriesHook(["compilation"]),
            make: new AsyncParallelHook(["compilation"]),
            entryOption: new SyncBailHook(["context", "entry"])
            // 定义了很多不同类型的钩子
        };
        // ...
    }
}

function webpack(options) {var compiler = new Compiler();
  ...// 查看 options, 若 watch 字段为 true, 则开启 watch 线程
  return compiler;
}
...

在下面的代码中,Compiler 对象继承自 Tapable,初始化时定义了很多钩子函数。

2.2 编译构建

用上一步失去的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 办法开始执行编译。而后,依据配置中的 entry 找出所有的入口文件,如下。

module.exports = {entry: './src/file.js'}

初始化实现后会调用 Compiler 的 run 来真正启动 webpack 编译构建流程,次要流程如下:

  • compile:开始编译
  • make:从入口点剖析模块及其依赖的模块,创立这些模块对象
  • build-module:构建模块
  • seal:封装构建后果
  • emit:把各个 chunk 输入到后果文件

1,compile 编译

执行了 run 办法后,首先会触发 compile,次要是构建一个 Compilation 对象。该对象是编译阶段的次要执行者,次要会顺次下述流程:执行模块创立、依赖收集、分块、打包等次要工作的对象。

2,make 编译模块

当实现了上述的 compilation 对象后,就开始从 Entry 入口文件开始读取,次要执行_addModuleChain()函数,源码如下:

_addModuleChain(context, dependency, onModule, callback) {
   ...
   // 依据依赖查找对应的工厂函数
   const Dep = /** @type {DepConstructor} */ (dependency.constructor);
   const moduleFactory = this.dependencyFactories.get(Dep);
   
   // 调用工厂函数 NormalModuleFactory 的 create 来生成一个空的 NormalModule 对象
   moduleFactory.create({dependencies: [dependency]
       ...
   }, (err, module) => {
       ...
       const afterBuild = () => {
        this.processModuleDependencies(module, err => {if (err) return callback(err);
         callback(null, module);
           });
    };
       
       this.buildModule(module, false, null, null, err => {
           ...
           afterBuild();})
   })
}

_addModuleChain 中接管参数 dependency 传入的入口依赖,应用对应的工厂函数 NormalModuleFactory.create 办法生成一个空的 module 对象。回调中会把此 module 存入 compilation.modules 对象和 dependencies.module 对象中,因为是入口文件,也会存入 compilation.entries 中。随后,执行 buildModule 进入真正的构建模块 module 内容的过程。

3,build module 实现模块编译

这个过程的次要调用配置的 loaders,将咱们的模块转成规范的 JS 模块。在用 Loader 对一个模块转换完后,应用 acorn 解析转换后的内容,输入对应的形象语法树(AST),以不便 Webpack 前面对代码的剖析。

从配置的入口模块开始,剖析其 AST,当遇到 require 等导入其它模块语句时,便将其退出到依赖的模块列表,同时对新找出的依赖模块递归剖析,最终搞清所有模块的依赖关系。

2.3 输入阶段

seal 办法次要是要生成 chunks,对 chunks 进行一系列的优化操作,并生成要输入的代码。Webpack 中的 chunk,能够了解为配置在 entry 中的模块,或者是动静引入的模块。

依据入口和模块之间的依赖关系,组装成一个个蕴含多个模块的 Chunk,再把每个 Chunk 转换成一个独自的文件退出到输入列表。在确定好输入内容后,依据配置确定输入的门路和文件名即可。

output: {path: path.resolve(__dirname, 'build'),
        filename: '[name].js'
}

在 Compiler 开始生成文件前,钩子 emit 会被执行,这是咱们批改最终文件的最初一个机会。整个过程如下图所示。

三、Webpack 中常见的 Loader

3.1 Loader 是什么

Loader 实质就是一个函数,在该函数中对接管到的内容进行转换,返回转换后的后果。因为 Webpack 只意识 JavaScript,所以 Loader 就成了翻译官,对其余类型的资源进行转译的预处理工作。

默认状况下,在遇到 import 或者 load 加载模块的时候,webpack 只反对对 js 文件打包。像 css、sass、png 等这些类型的文件的时候,webpack 则无能为力,这时候就须要配置对应的 loader 进行文件内容的解析。在加载模块的时候,执行程序如,如下图所示。

对于配置 Loader 的形式,有常见的三种形式:

  • 配置形式(举荐):在 webpack.config.js 文件中指定 loader
  • 内联形式:在每个 import 语句中显式指定 loader
  • Cli 形式:在 shell 命令中指定它们

3.1 配置形式

对于 Loader 的配置,咱们通常是写在 module.rules 属性中,属性介绍如下:

  • rules 是一个数组的模式,因而咱们能够配置很多个 loader。
  • 每一个 loader 对应一个对象的模式,对象属性 test 为匹配的规定,个别状况为正则表达式。
  • 属性 use 针对匹配到文件类型,调用对应的 loader 进行解决。

上面是一个 module.rules 的示例代码:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [{ loader: 'style-loader'},
          {
            loader: 'css-loader',
            options: {modules: true}
          },
          {loader: 'sass-loader'}
        ]
      }
    ]
  }
};

3.2 Loader 个性

从上述代码能够看到,在解决 css 模块的时候,use 属性中配置了三个 loader 别离解决 css 文件。因为 loader 反对链式调用,链中的每个 loader 会解决之前已解决过的资源,最终变为 js 代码。程序为相同的程序执行,即上述执行形式为 sass-loader、css-loader、style-loader。

除此之外,loader 的个性还有如下:

  • Loader 能够是同步的,也能够是异步的
  • Loader 运行在 Node.js 中,并且可能执行任何操作
  • 除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还能够在 module.rules 中应用 loader 字段间接援用一个模块
  • 插件 (plugin) 能够为 loader 带来更多个性
  • Loader 可能产生额定的任意文件

能够通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。用户当初能够更加灵便地引入细粒度逻辑,例如:压缩、打包、语言翻译和更多其余个性。

3.3 罕用 Loader

在页面开发过程中,除了须要导入一些场景 js 文件外,还须要配置响应的 loader 进行加载。WebPack 常见的 Loader 如下:

  • style-loader:将 css 增加到 DOM 的内联款式标签 style 里,而后通过 dom 操作去加载 css。
  • css-loader : 容许将 css 文件通过 require 的形式引入,并返回 css 代码。
  • less-loader: 解决 less,将 less 代码转换成 css。
  • sass-loader: 解决 sass,将 scss/sass 代码转换成 css。
  • postcss-loader:用 postcss 来解决 css。
  • autoprefixer-loader: 解决 css3 属性前缀,已被弃用,倡议间接应用 postcss。
  • file-loader: 散发文件到 output 目录并返回相对路径。
  • url-loader: 和 file-loader 相似,然而当文件小于设定的 limit 时能够返回一个 Data Url。
  • html-minify-loader: 压缩 HTML
  • babel-loader : 用 babel 来转换 ES6 文件到 ES。
  • awesome-typescript-loader:将 TypeScript 转换成 JavaScript,性能优于 ts-loader。
  • eslint-loader:通过 ESLint 查看 JavaScript 代码。
  • tslint-loader:通过 TSLint 查看 TypeScript 代码。
  • cache-loader: 能够在一些性能开销较大的 Loader 之前增加,目标是将后果缓存到磁盘里

上面以 css-loader 为例子,来阐明 Loader 的应用过程。首先,咱们在我的项目中装置 css-loader 插件。

npm install --save-dev css-loader

而后将规定配置到 module.rules 中,比方:

rules: [
  ...,
 {
  test: /\.css$/,
    use: {
      loader: "css-loader",
      options: {// 启用 / 禁用 url() 解决
     url: true,
     // 启用 / 禁用 @import 解决
     import: true,
        // 启用 / 禁用 Sourcemap
        sourceMap: false
      }
    }
 }
]

四、Webpack 中常见的 Plugin

4.1 根底

Plugin 就是插件,基于事件流框架 Tapable,插件能够扩大 Webpack 的性能,在 Webpack 运行的生命周期中会播送出许多事件,Plugin 能够监听这些事件,在适合的机会通过 Webpack 提供的 API 扭转输入后果。

Webpack 中的 Plugin 也是如此,Plugin 赋予其各种灵便的性能,例如打包优化、资源管理、环境变量注入等,它们会运行在 Webpack 的不同阶段(钩子 / 生命周期),贯通了 Webpack 整个编译周期。


应用的时候,通过配置文件导出对象中 plugins 属性传入 new 实例对象即可。

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 装置
const webpack = require('webpack'); // 拜访内置的插件
module.exports = {
  ...
  plugins: [new webpack.ProgressPlugin(),
    new HtmlWebpackPlugin({template: './src/index.html'}),
  ],
};

4.2 个性

Plugin 从实质上来说,就是一个具备 apply 办法 Javascript 对象。apply 办法会被 webpack compiler 调用,并且在整个编译生命周期都能够拜访 compiler 对象。

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {apply(compiler) {compiler.hooks.run.tap(pluginName, (compilation) => {console.log('webpack 构建过程开始!');
    });
  }
}

module.exports = ConsoleLogOnBuildWebpackPlugin;

咱们能够应用下面的形式,来获取 Plugin 在编译生命周期钩子。如下:

  • entry-option:初始化 option
  • compile:真正开始的编译,在创立 compilation 对象之前
  • compilation:生成好了 compilation 对象
  • make:从 entry 开始递归剖析依赖,筹备对每个模块进行 build
  • after-compile:编译 build 过程完结
  • emit:在将内存中 assets 内容写到磁盘文件夹之前
  • after-emit:在将内存中 assets 内容写到磁盘文件夹之后
  • done:实现所有的编译过程
  • failed:编译失败的时候

4.3 常见的 Plugin

Weebpack 中,常见的 plugin 有如下一些:

  • define-plugin:定义环境变量 (Webpack4 之后指定 mode 会主动配置)
  • ignore-plugin:疏忽局部文件
  • html-webpack-plugin:简化 HTML 文件创建 (依赖于 html-loader)
  • web-webpack-plugin:可不便地为单页利用输入 HTML,比 html-webpack-plugin 好用
  • uglifyjs-webpack-plugin:不反对 ES6 压缩 (Webpack4 以前)
  • terser-webpack-plugin: 反对压缩 ES6 (Webpack4)
  • webpack-parallel-uglify-plugin: 多过程执行代码压缩,晋升构建速度
  • mini-css-extract-plugin: 拆散款式文件,CSS 提取为独立文件,反对按需加载 (代替 extract-text-webpack-plugin)
  • serviceworker-webpack-plugin:为网页利用减少离线缓存性能
  • clean-webpack-plugin: 目录清理
  • ModuleConcatenationPlugin: 开启 Scope Hoisting
  • speed-measure-webpack-plugin: 能够看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)
  • webpack-bundle-analyzer: 可视化 Webpack 输入文件的体积 (业务组件、依赖第三方模块)

上面通过 clean-webpack-plugin 来看一下插件的应用办法。首先,须要装置 clean-webpack-plugin 插件。

npm install --save-dev clean-webpack-plugin

而后,引入插件即可应用。

const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
 ...
  plugins: [
    ...,
    new CleanWebpackPlugin(),
    ...
  ]
}

五、Loader 和 Plugin 的区别,以及如何自定义 Loader 和 Plugin

5.1 区别

Loader 实质就是一个函数,在该函数中对接管到的内容进行转换,返回转换后的后果。因为 Webpack 只意识 JavaScript,所以 Loader 就成了翻译官,对其余类型的资源进行转译的预处理工作。

Plugin 就是插件,基于事件流框架 Tapable,插件能够扩大 Webpack 的性能,在 Webpack 运行的生命周期中会播送出许多事件,Plugin 能够监听这些事件,在适合的机会通过 Webpack 提供的 API 扭转输入后果。

  • Loader 运行在打包文件之前,Loader 在 module.rules 中配置,作为模块的解析规定,类型为数组。每一项都是一个 Object,外部蕴含了 test(类型文件)、loader、options (参数)等属性。
  • Plugins 在整个编译周期都起作用,Plugin 在 plugins 中独自配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。

5.2 自定义 Loader

咱们晓得,Loader 实质上来说就是一个函数,函数中的 this 作为上下文会被 webpack 填充,因而咱们不能将 loader 设为一个箭头函数。该函数承受一个参数,为 webpack 传递给 loader 的文件源内容。

同时,函数中 this 是由 webpack 提供的对象,可能获取以后 loader 所须要的各种信息。函数中有异步操作或同步操作,异步操作通过 this.callback 返回,返回值要求为 string 或者 Buffer,如下。

// 导出一个函数,source 为 webpack 传递给 loader 的文件源内容
module.exports = function(source) {const content = doSomeThing2JsString(source);
    
    // 如果 loader 配置了 options 对象,那么 this.query 将指向 options
    const options = this.query;
    
    // 能够用作解析其余模块门路的上下文
    console.log('this.context');
    
    /*
     * this.callback 参数:* error:Error | null,当 loader 出错时向外抛出一个 error
     * content:String | Buffer,通过 loader 编译后须要导出的内容
     * sourceMap:为不便调试生成的编译后内容的 source map
     * ast:本次编译生成的 AST 动态语法树,之后执行的 loader 能够间接应用这个 AST,进而省去反复生成 AST 的过程
     */
    this.callback(null, content); // 异步
    return content; // 同步
}

5.3 自定义 Plugin

Webpack 的 Plugin 是基于事件流框架 Tapable,因为 webpack 基于公布订阅模式,在运行的生命周期中会播送出许多事件,插件通过监听这些事件,就能够在特定的阶段执行本人的插件工作。

同时,Webpack 编译会创立两个外围对象:compiler 和 compilation。

  • compiler:蕴含了 Webpack 环境的所有的配置信息,包含 options,loader 和 plugin,和 webpack 整个生命周期相干的钩子.
  • compilation:作为 Plugin 内置事件回调函数的参数,蕴含了以后的模块资源、编译生成资源、变动的文件以及被跟踪依赖的状态信息。当检测到一个文件变动,一次新的 Compilation 将被创立。

如果须要自定义 Plugin,也须要遵循肯定的标准:

  • 插件必须是一个函数或者是一个蕴含 apply 办法的对象,这样能力拜访 compiler 实例
  • 传给每个插件的 compiler 和 compilation 对象都是同一个援用,因而不倡议批改
  • 异步的事件须要在插件解决完工作时调用回调函数告诉 Webpack 进入下一个流程,不然会卡住

上面是一个自定 Plugin 的模板:

class MyPlugin {
    // Webpack 会调用 MyPlugin 实例的 apply 办法给插件实例传入 compiler 对象
  apply (compiler) {
    // 找到适合的事件钩子,实现本人的插件性能
    compiler.hooks.emit.tap('MyPlugin', compilation => {
        // compilation: 以后打包构建流程的上下文
        console.log(compilation);
        
        // do something...
    })
  }
}

在 emit 事件被触发后,代表源文件的转换和组装曾经实现,能够读取到最终将输入的资源、代码块、模块及其依赖,并且能够批改输入资源的内容。

六、Webpack 热更新

6.1 热更新

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。这个机制能够做到不必刷新浏览器而将新变更的模块替换掉旧的模块。

HMR 的外围就是客户端从服务端拉去更新后的文件,精确的说是 chunk diff (chunk 须要更新的局部),实际上 WDS 与浏览器之间保护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行比照。客户端比照出差别后会向 WDS 发动 Ajax 申请来获取更改内容(文件列表、hash),这样客户端就能够再借助这些信息持续向 WDS 发动 jsonp 申请获取该 chunk 的增量更新。

在 Webpack 中配置开启热模块也十分的简略,只须要增加如下代码即可。

const webpack = require('webpack')
module.exports = {
  // ...
  devServer: {
    // 开启 HMR 个性
    hot: true
    // hotOnly: true
  }
}

须要阐明的是,实现热更新还须要去指定哪些模块产生更新时进行 HRM,因为默认状况下,HRM 只对 css 文件无效。

if(module.hot){module.hot.accept('./util.js',()=>{console.log("util.js 更新了")
    })
}

6.2 实现原理

首先,咱们来看一张图:

下面图中波及了很多不同的概念,如下:

  • Webpack Compile:将 JS 源代码编译成 bundle.js
  • HMR Server:用来将热更新的文件输入给 HMR Runtime
  • Bundle Server:动态资源文件服务器,提供文件拜访门路
  • HMR Runtime:socket 服务器,会被注入到浏览器,更新文件的变动
  • bundle.js:构建输入的文件
  • 在 HMR Runtime 和 HMR Server 之间建设 websocket,即图上 4 号线,用于实时更新文件变动

整个流程,咱们能够将它分为两个阶段:启动阶段和更新阶段。

启动阶段的次要工作是,Webpack Compile 将源代码和 HMR Runtime 一起编译成 bundle 文件,传输给 Bundle Server 动态资源服务器。接下来,咱们重点关注下更新阶段:

当某一个文件或者模块发生变化时,webpack 监听到文件变动对文件从新编译打包,编译生成惟一的 hash 值,这个 hash 值用来作为下一次热更新的标识。而后,依据变动的内容生成两个补丁文件:manifest(蕴含了 hash 和 chundId,用来阐明变动的内容)和 chunk.js 模块。

因为 socket 服务器在 HMR Runtime 和 HMR Server 之间建设 websocket 链接,当文件产生改变的时候,服务端会向浏览器推送一条音讯,音讯蕴含文件改变后生成的 hash 值,如下图的 h 属性,作为下一次热更细的标识。

在浏览器承受到这条音讯之前,浏览器曾经在上一次 socket 音讯中曾经记住了此时的 hash 标识,这时候咱们会创立一个 ajax 去服务端申请获取到变动内容的 manifest 文件。mainfest 文件蕴含从新 build 生成的 hash 值,以及变动的模块,对应上图的 c 属性。浏览器依据 manifest 文件获取模块变动的内容,从而触发 render 流程,实现部分模块更新。

6.3 总结

通过后面的剖析,总结 Webpack 热模块的步骤如下:

  • 通过 webpack-dev-server 创立两个服务器:提供动态资源的服务(express)和 Socket 服务
  • express server 负责间接提供动态资源的服务(打包后的资源间接被浏览器申请和解析)
  • socket server 是一个 websocket 的长连贯,单方能够通信
  • 当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest 文件)和.js 文件(update chunk)
  • 通过长连贯,socket server 能够间接将这两个文件被动发送给客户端(浏览器)
  • 浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对批改的模块进行更新

七、Webpack Proxy 工作原理

7.1 代理

在我的项目开发中不可避免会遇到逾越问题,Webpack 中的 Proxy 就是解决前端跨域的办法之一。所谓代理,指的是在接管客户端发送的申请后转发给其余服务器的行为,webpack 中提供服务器的工具为 webpack-dev-server。

7.1.1 webpack-dev-server

webpack-dev-server 是 webpack 官网推出的一款开发工具,将主动编译和主动刷新浏览器等一系列对开发敌对的性能全副集成在了一起。同时,为了进步开发者日常的开发效率,只实用在开发阶段。在 webpack 配置对象属性中配置代理的代码如下:

// ./webpack.config.js
const path = require('path')

module.exports = {
    // ...
    devServer: {contentBase: path.join(__dirname, 'dist'),
        compress: true,
        port: 9000,
        proxy: {
            '/api': {target: 'https://api.github.com'}
        }
        // ...
    }
}

其中,devServetr 外面 proxy 则是对于代理的配置,该属性为对象的模式,对象中每一个属性就是一个代理的规定匹配。

属性的名称是须要被代理的申请门路前缀,个别为了分别都会设置前缀为 /api,值为对应的代理匹配规定,对应如下:

  • target:示意的是代理到的指标地址。
  • pathRewrite:默认状况下,咱们的 /api-hy 也会被写入到 URL 中,如果心愿删除,能够应用 pathRewrite。
  • secure:默认状况下不接管转发到 https 的服务器上,如果心愿反对,能够设置为 false。
  • changeOrigin:它示意是否更新代理后申请的 headers 中 host 地址。

7.2 原理

proxy 工作原理本质上是利用 http-proxy-middleware 这个 http 代理中间件,实现申请转发给其余服务器。比方上面的例子:

const express = require('express');
const proxy = require('http-proxy-middleware');

const app = express();

app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
app.listen(3000);

// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar

在下面的例子中,本地地址为http://localhost:3000,该浏览器发送一个前缀带有 /api 标识的申请到服务端获取数据,但响应这个申请的服务器只是将申请转发到另一台服务器中。

7.1 跨域

在开发阶段,webpack-dev-server 会启动一个本地开发服务器,所以咱们的利用在开发阶段是独立运行在 localhost 的一个端口上,而后端服务又是运行在另外一个地址上。所以在开发阶段中,因为浏览器同源策略的起因,当本地拜访后端就会呈现跨域申请的问题。

解决这种问题时,只须要设置 webpack proxy 代理即可。当本地发送申请的时候,代理服务器响应该申请,并将申请转发到指标服务器,指标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地,原理图如下:

在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能失常接收数据。

留神:服务器与服务器之间申请数据并不会存在跨域行为,跨域行为是浏览器安全策略限度

八、如何借助 Webpack 来优化性能

作为一个我的项目的打包构建工具,在实现我的项目开发后常常须要利用 Webpack 对前端我的项目进行性能优化,常见的优化伎俩有如下几个方面:

  • JS 代码压缩
  • CSS 代码压缩
  • Html 文件代码压缩
  • 文件大小压缩
  • 图片压缩
  • Tree Shaking
  • 代码拆散
  • 内联 chunk

8.1 JS 代码压缩

terser 是一个 JavaScript 的解释、绞肉机、压缩机的工具集,能够帮忙咱们压缩、美化咱们的代码,让 bundle 更小。在 production 模式下,webpack 默认就是应用 TerserPlugin 来解决咱们的代码的。如果想要自定义配置它,配置办法如下。

const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
    ...
    optimization: {
        minimize: true,
        minimizer: [
            new TerserPlugin({parallel: true              // 电脑 cpu 核数 -1})
        ]
    }
}

TerserPlugin 罕用的属性如下:

  • extractComments:默认值为 true,示意会将正文抽取到一个独自的文件中,开发阶段,咱们可设置为 false,不保留正文
  • parallel:应用多过程并发运行进步构建的速度,默认值是 true,并发运行的默认数量:os.cpus().length – 1
  • terserOptions:设置咱们的 terser 相干的配置:
    compress:设置压缩相干的选项,mangle:设置美化相干的选项,能够间接设置为 true
    mangle:设置美化相干的选项,能够间接设置为 true
    toplevel:底层变量是否进行转换
    keep_classnames:保留类的名称
    keep_fnames:保留函数的名称

8.2 CSS 代码压缩

CSS 压缩通常用于去除无用的空格等,不过因为很难去批改选择器、属性的名称、值等,所以咱们能够应用另外一个插件:css-minimizer-webpack-plugin。配置如下:

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
    // ...
    optimization: {
        minimize: true,
        minimizer: [
            new CssMinimizerPlugin({parallel: true})
        ]
    }
}

8.3 Html 文件代码压缩

应用 HtmlWebpackPlugin 插件来生成 HTML 的模板时候,能够通过配置属性 minify 进行 html 优化,配置如下。

module.exports = {
    ...
    plugin:[
        new HtmlwebpackPlugin({
            ...
            minify:{
                minifyCSS:false, // 是否压缩 css
                collapseWhitespace:false, // 是否折叠空格
                removeComments:true // 是否移除正文
            }
        })
    ]
}

8.4 文件大小压缩

对文件的大小进行压缩,能够无效缩小 http 传输过程中宽带的损耗,文件压缩须要用到 compression-webpack-plugin 插件,配置如下。

new ComepressionPlugin({test:/\.(css|js)$/,  // 哪些文件须要压缩
    threshold:500, // 设置文件多大开始压缩
    minRatio:0.7, // 至多压缩的比例
    algorithm:"gzip", // 采纳的压缩算法
})

8.5 图片压缩

如果咱们对 bundle 包进行剖析,会发现图片等多媒体文件的大小是远远要比 js、css 文件要大的,所以图片压缩在打包方面也是很重要的。配置能够参考如下的形式:

module: {
  rules: [
    {test: /\.(png|jpg|gif)$/,
      use: [
        {
          loader: 'file-loader',
          options: {name: '[name]_[hash].[ext]',
            outputPath: 'images/',
          }
        },
        {
          loader: 'image-webpack-loader',
          options: {
            // 压缩 jpeg 的配置
            mozjpeg: {
              progressive: true,
              quality: 65
            },
            // 应用 imagemin**-optipng 压缩 png,enable: false 为敞开
            optipng: {enabled: false,},
            // 应用 imagemin-pngquant 压缩 png
            pngquant: {
              quality: '65-90',
              speed: 4
            },
            // 压缩 gif 的配置
            gifsicle: {interlaced: false,},
            // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格局
            webp: {quality: 75}
          }
        }
      ]
    },
  ]
} 

8.6 Tree Shaking

Tree Shaking 是一个术语,在计算机中示意打消死代码,依赖于 ES Module 的动态语法分析。在 webpack 实现 Trss shaking 有两种不同的计划:

  • usedExports:通过标记某些函数是否被应用,之后通过 Terser 来进行优化的
  • sideEffects:跳过整个模块 / 文件,间接查看该文件是否有副作用

usedExports 的配置办法很简略,只须要将 usedExports 设为 true 即可,如下。

module.exports = {
    ...
    optimization:{usedExports}
}

而 sideEffects 则用于告知 webpack compiler 在编译时哪些模块有副作用,配置办法是在 package.json 中设置 sideEffects 属性。如果 sideEffects 设置为 false,就是告知 webpack 能够平安的删除未用到的 exports,如果有些文件须要保留,能够设置为数组的模式。

"sideEffecis":["./src/util/format.js",    "*.css" // 所有的 css 文件]

8.7 代码拆散

默认状况下,所有的 JavaScript 代码(业务代码、第三方依赖、临时没有用到的模块)在首页全副都加载,就会影响首页的加载速度。如果能够分出出更小的 bundle,以及管制资源加载优先级,从而优化加载性能。

代码拆散能够通过 splitChunksPlugin 来实现,该插件 webpack 曾经默认装置和集成,只须要配置即可。

module.exports = {   
 ...    
    optimization:{    
        splitChunks:{chunks:"all"}  
        }}

splitChunks 有如下几个属性:

  • Chunks:对同步代码还是异步代码进行解决
  • minSize:拆分包的大小, 至多为 minSize,如何包的大小不超过 minSize,这个包不会拆分
  • maxSize:将大于 maxSize 的包,拆分为不小于 minSize 的包
  • minChunks:被引入的次数,默认是 1

8.8 内联 chunk

能够通过 InlineChunkHtmlPlugin 插件将一些 chunk 的模块内联到 html,如 runtime 的代码(对模块进行解析、加载、模块信息相干的代码),代码量并不大然而必须加载的,比方:

const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
     module.exports = {  
           ...    plugin:[new InlineChunkHtmlPlugin(HtmlWebpackPlugin,[/runtime.+\.js/]}

总结一下,Webpack 对前端性能的优化,次要是通过文件体积大小动手,次要的措施有分包、缩小 Http 申请次数等。

九、进步 Webpack 的构建速度

随着性能和业务代码越来越多,相应的 Webpack 的构建工夫也会越来越久,构建的效率也会越来越低,那如何晋升 Webpack 构建速度,是前端工程化的重要一环。罕用的伎俩有如下一些:

  • 优化 loader 配置
  • 正当应用 resolve.extensions
  • 优化 resolve.modules
  • 优化 resolve.alias
  • 应用 DLLPlugin 插件
  • 应用 cache-loader
  • terser 启动多线程
  • 正当应用 sourceMap

9.1 优化 Loader 配置

在应用 Loader 时,能够通过配置 include、exclude、test 属性来匹配文件,通过 include、exclude 来规定匹配利用的 loader。例如,上面是 ES6 我的项目中配置 babel-loader 的例子:

module.exports = {
  module: {
    rules: [
      {
        // 如果我的项目源码中只有 js 文件就不要写成 /\.jsx?$/,晋升正则表达式性能
        test: /\.js$/,
        // babel-loader 反对缓存转换出的后果,通过 cacheDirectory 选项开启
        use: ['babel-loader?cacheDirectory'],
        // 只对我的项目根目录下的 src 目录中的文件采纳 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
    ]
  },
};

9.2 正当 resolve.extensions

在开发中,咱们会有各种各样的模块依赖,这些模块可能来自第三方库,也可能是本人编写的,resolve 能够帮忙 Webpack 从每个 require/import 语句中,找到须要引入到适合的模块代码。

具体来说,通过 resolve.extensions 是解析到文件时主动增加拓展名,默认状况如下:

module.exports = {
    ...
    extensions:[".warm",".mjs",".js",".json"]
}

当咱们引入文件的时候,若没有文件后缀名,则会依据数组内的值顺次查找。所以,解决配置的时候,不要轻易把所有后缀都写在外面。

9.3 优化 resolve.modules

resolve.modules 用于配置 webpack 去哪些目录下寻找第三方模块,默认值为[‘node_modules’]。所以,在我的项目构建时,能够通过指明寄存第三方模块的绝对路径来缩小寻找的工夫。

module.exports = {
  resolve: {modules: [path.resolve(__dirname, 'node_modules')]   // __dirname 示意当前工作目录
  },
};

9.4 优化 resolve.alias

alias 给一些罕用的门路起一个别名,特地当咱们的我的项目目录构造比拟深的时候,一个文件的门路可能是./../../ 的模式,通过配置 alias 以缩小查找过程。

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

9.5 应用 DLL Plugin 插件

DLL 全称是动态链接库,是为软件在 winodw 种实现共享函数库的一种实现形式,而 Webpack 也内置了 DLL 的性能,为的就是能够共享,不常常扭转的代码,抽成一个共享的库。应用步骤分成两局部:

  • 打包一个 DLL 库
  • 引入 DLL 库

打包一个 DLL 库

Webpack 内置了一个 DllPlugin 能够帮忙咱们打包一个 DLL 的库文件,如下。

module.exports = {
    ...
    plugins:[
        new webpack.DllPlugin({name:'dll_[name]',
            path:path.resolve(__dirname,"./dll/[name].mainfest.json")
        })
    ]
}

引入 DLL 库

首先,应用 webpack 自带的 DllReferencePlugin 插件对 mainfest.json 映射文件进行剖析,获取要应用的 DLL 库。而后,再通过 AddAssetHtmlPlugin 插件,将咱们打包的 DLL 库引入到 Html 模块中。

module.exports = {
    ...
    new webpack.DllReferencePlugin({context:path.resolve(__dirname,"./dll/dll_react.js"),
        mainfest:path.resolve(__dirname,"./dll/react.mainfest.json")
    }),
    new AddAssetHtmlPlugin({
        outputPath:"./auto",
        filepath:path.resolve(__dirname,"./dll/dll_react.js")
    })
}

9.6 正当应用应用 cache-loader

在一些性能开销较大的 loader 之前增加 cache-loader,以将后果缓存到磁盘里,显著晋升二次构建速度。比方:

module.exports = {
    module: {
        rules: [
            {
                test: /\.ext$/,
                use: ['cache-loader', ...loaders],
                include: path.resolve('src'),
            },
        ],
    },
};

须要阐明的是,保留和读取这些缓存文件会有一些工夫开销,所以请只对性能开销较大的 loader 应用此 loader。

9.7 开启多线程

开启多过程并行运行能够进步构建速度,配置如下:

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({parallel: true,          // 开启多线程}),
    ],
  },
};

十、除了 Webpack 外,你还理解哪些模块管理工具

模块化是一种解决简单零碎合成为更好的可治理模块的形式。能够用来宰割、组织和打包利用。每个模块实现一个特定的子性能,所有的模块按某种办法组装起来,成为一个整体。

在前端畛域中,除了 Webpack 外,比拟风行的模块打包工具还包含 Rollup、Parcel、snowpack 和最近风靡的 Vite。

10.1 Rollup

Rollup 是一款 ES Modules 打包器,能够将小块代码编译成大块简单的代码,例如 library 或应用程序。从作用上来看,Rollup 与 Webpack 十分相似。不过相比于 Webpack,Rollup 要玲珑的多。当初很多苦都应用它进行打包,比方:Vue、React 和 three.js 等。

应用之前,能够应用 npm install --global rollup 命令进行装置。Rollup 能够通过命令行接口(command line interface) 配合可选配置文件 (optional configuration file) 来调用,或者能够通过 JavaScript API 来调用。运行 rollup --help 能够查看可用的选项和参数。

上面通过一个简略的例子,阐明如何应用 Rollup 进行打包。首先,新建如下几个文件:

// ./src/messages.js
export default {hi: 'Hello World'}

// ./src/logger.js
export const log = msg => {console.log('---------- INFO ----------')
  console.log(msg)
  console.log('--------------------------')
}

export const error = msg => {console.error('---------- ERROR ----------')
  console.error(msg)
  console.error('---------------------------')
}

// ./src/index.js
import {log} from './logger'
import messages from './messages'
log(messages.hi)

最初,再应用上面的命令打包即可。

npx rollup ./src/index.js --file ./dist/bundle.js

参考:Rollup.js

10.2 Parcel

Parcel,是一款齐全零配置的前端打包器,它提供了“傻瓜式”的应用体验,只需理解简略的命令,就能构建前端应用程序。

应用 Parcel 的流程如下:

  1. 创立目录并应用 npm init -y 初始化package.json
  2. 装置模块npm i parcel-bundler --save-dev
  3. 创立 src/index.html 文件作为入口文件,尽管 Parcel 反对任意文件为打包入口,然而还是举荐咱们应用 HTML 文件作为打包入口,官网理由是 HTML 是浏览器运行的入口,故应该应用 HTML 作为打包入口。

应用之前,须要在 package.json 中配置脚本,如下:

"scripts": {"parcel": "parcel"},

Parcel 跟 Webpack 一样都反对以任意类型文件作为打包入口,但倡议应用 HTML 文件作为入口,如下。

<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="main.js"></script>
</body>
</html>

而后,在 main.js 文件通过 ES Moudle 办法导入其余模块成员。

// ./src/main.js
import {log} from './logger'
log('hello parcel')

// ./src/logger.js
export const log = msg => {console.log('---------- msg ----------')
  console.log(msg)
}

而后,应用如下的命令即可打包:

npx parcel src/index.html

执行命令后,Parcel 不仅打包了利用,同时也启动了一个开发服务器,跟 webpack Dev Server 的成果是一样的。

10.3 Vite

Vite 是 Vue 的作者尤雨溪开发的 Web 开发构建工具,它是一个基于浏览器原生 ES 模块导入的开发服务器,在开发环境下,利用浏览器去解析 import,在服务器端按需编译返回,齐全跳过了打包这个概念,服务器随启随用。同时不仅对 Vue 文件提供了反对,还反对热更新,而且热更新的速度不会随着模块增多而变慢。

Vite 具备以下特点:

  • 疾速的冷启动
  • 即时热模块更新(HMR,Hot Module Replacement)
  • 真正按需编译

Vite 由两局部组成:

  • 一个开发服务器,它基于 原生 ES 模块 提供了丰盛的内建性能,如速度快到惊人的 [模块热更新 HMR。
  • 一套构建指令,它应用 Rollup 打包你的代码,并且它是预配置的,能够输入用于生产环境的优化过的动态资源。

Vite 在开发阶段能够间接启动开发服务器,不须要进行打包操作,也就意味着不须要剖析模块的依赖、不须要编译,因而启动速度十分快。当浏览器申请某个模块的时候,依据须要对模块的内容进行编译,大大缩短了编译工夫。工作原理如下图所示。

在热模块 HMR 方面,当批改一个模块的时候,仅需让浏览器从新申请该模块即可,毋庸像 Webpack 那样须要把该模块的相干依赖模块全副编译一次,因而效率也更高。

正文完
 0