# 如何自定义开发一个 loader?

编写一个解决实际问题的 Loader。

该 Loader 名叫 comment-require-loader,作用是把 JavaScript 代码中的注释语法// @require '../style/index.css'转换成require('../style/index.css');

该 Loader 的使用场景是去正确加载针对 Fis3 (opens new window) 编写的 JavaScript,这些 JavaScript 中存在通过注释的方式加载依赖的 CSS 文件。

该 Loader 的使用方法如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['comment-require-loader'],
        // 针对采用了 fis3 CSS 导入语法的 JavaScript 文件通过 comment-require-loader 去转换
        include: [path.resolve(__dirname, 'node_modules/imui')]
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

该 Loader 的实现非常简单,完整代码如下:

function replace(source) {
  // 使用正则把 // @require '../style/index.css' 转换成 require('../style/index.css');
  return source.replace(/(\/\/ *@require) +(('|").+('|")).*/, 'require($2);')
}

module.exports = function(content) {
  return replace(content)
}
1
2
3
4
5
6
7
8

# 如何使用本地自己开发的 loader?

在开发 Loader 的过程中,为了测试编写的 Loader 是否能正常工作,需要把它配置到 Webpack 中后,才可能会调用该 Loader。 在前面的章节中,使用的 Loader 都是通过 Npm 安装的,要使用 Loader 时会直接使用 Loader 的名称,代码如下:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader']
      }
    ]
  }
}
1
2
3
4
5
6
7
8
9
10

如果还采取以上的方法去使用本地开发的 Loader 将会很麻烦,因为你需要确保编写的 Loader 的源码是在 node_modules 目录下。 为此你需要先把编写的 Loader 发布到 Npm 仓库后再安装到本地项目使用。

解决以上问题的便捷方法有两种,分别如下:

Npm link 专门用于开发调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤如下:

  1. 确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  2. 在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
  3. 在项目根目录下执行 npm link loader-name,把第 2 步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第 1 步中的 package.json 文件中配置的模块名称。

链接好 Loader 到项目后你就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

# ResolveLoader

2-7 其它配置项 (opens new window) 中曾介绍过 ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules

假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:

module.exports = {
  resolveLoader: {
    // 去哪些目录下寻找 Loader,有先后顺序之分
    modules: ['node_modules', './loaders/']
  }
}
1
2
3
4
5
6

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,如果找不到,会再去 ./loaders/ 目录下寻找。

# 如何自定义开发一个插件?

该插件的名称取名叫 EndWebpackPlugin,作用是在 Webpack 即将退出时再附加一些额外的操作,例如在 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。 同时该插件还能区分 Webpack 构建是否执行成功。使用该插件时方法如下:

// 使用插件
const EndWebpackPlugin = require('./plugins/end-webpack-plugin')
module.exports = {
  plugins: [
    // 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
    new EndWebpackPlugin(
      () => {
        // Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
      },
      (err) => {
        // Webpack 构建失败,err 是导致错误的原因
        console.error(err)
      }
    )
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

要实现该插件,需要借助两个事件:

  • done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
  • failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;

实现该插件非常简单,完整代码如下:

// ./plugins/end-webpack-plugin
class EndWebpackPlugin {
  constructor(doneCallback, failCallback) {
    // 存下在构造函数中传入的回调函数
    this.doneCallback = doneCallback
    this.failCallback = failCallback
  }

  apply(compiler) {
    compiler.plugin('done', (stats) => {
      // 在 done 事件中回调 doneCallback
      this.doneCallback(stats)
    })
    compiler.plugin('failed', (err) => {
      // 在 failed 事件中回调 failCallback
      this.failCallback(err)
    })
  }
}
// 导出插件
module.exports = EndWebpackPlugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

从开发这个插件可以看出,找到合适的事件点去完成功能在开发插件时显得尤为重要。 在 5-1 工作原理概括 (opens new window) 中详细介绍过 Webpack 在运行过程中广播出常用事件,你可以从中找到你需要的事件。

# Compiler 和 Compilation 的区别

在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

# 文件监听工作原理

在 Webpack 中监听一个文件发生变化的原理是定时的去获取这个文件的最后编辑时间,每次都存下最新的最后编辑时间,如果发现当前获取的和最后一次保存的最后编辑时间不一致,就认为该文件发生了变化。 配置项中的 watchOptions.poll 就是用于控制定时检查的周期,具体含义是每隔多少毫秒检查一次。

当发现某个文件发生了变化时,并不会立刻告诉监听者,而是先缓存起来,收集一段时间的变化后,再一次性告诉监听者。 配置项中的 watchOptions.aggregateTimeout 就是用于配置这个等待时间。 这样做的目的是因为我们在编辑代码的过程中可能会高频的输入文字导致文件变化的事件高频的发生,如果每次都重新执行构建就会让构建卡死。

对于多个文件来说,原理相似,只不过会对列表中的每一个文件都定时的执行检查。 但是这个需要监听的文件列表是怎么确定的呢? 默认情况下 Webpack 会从配置的 Entry 文件出发,递归解析出 Entry 文件所依赖的文件,把这些依赖的文件都加入到监听列表中去。 可见 Webpack 这一点还是做的很智能的,不是粗暴的直接监听项目目录下的所有文件。

由于保存文件的路径和最后编辑时间需要占用内存,定时检查周期检查需要占用 CPU 以及文件 I/O,所以最好减少需要监听的文件数量和降低检查频率。

# 如何优化文件监听

开启监听模式时,默认情况下会监听配置的 Entry 文件和所有其递归依赖的文件。 在这些文件中会有很多存在于 node_modules 下,因为如今的 Web 项目会依赖大量的第三方模块。 在大多数情况下我们都不可能去编辑 node_modules 下的文件,而是编辑自己建立的源码文件。 所以一个很大的优化点就是忽略掉 node_modules 下的文件,不监听它们。相关配置如下:

module.export = {
  watchOptions: {
    // 不监听的 node_modules 目录下的文件
    ignored: /node_modules/
  }
}
1
2
3
4
5
6

采用这种方法优化后,你的 Webpack 消耗的内存和 CPU 将会大大降低。

有时你可能会觉得 node_modules 目录下的第三方模块有 bug,想修改第三方模块的文件,然后在自己的项目中试试。 在这种情况下如果使用了以上优化方法,我们需要重启构建以看到最新效果。 但这种情况毕竟是非常少见的。

除了忽略掉部分文件的优化外,还有如下两种方法:

  • watchOptions.aggregateTimeout 值越大性能越好,因为这能降低重新构建的频率。
  • watchOptions.poll 值越大越好,因为这能降低检查的频率。

但两种优化方法的后果是会让你感觉到监听模式的反应和灵敏度降低了。

# 如何 node_modules 中第三方库的 bug

# 第一种思路

第一种思路是将第三方库中有问题的文件 copy 一份进行修复,放在项目目录里面(非 node_modules),然后通过构建工具 resolve.alias 能力重定向到修复后的位置。

resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径。例如使用以下配置:

// Webpack alias 配置
resolve:{
  alias:{
    'components': './src/components/'
  }
}
1
2
3
4
5
6

当你通过 import Button from 'components/button' 导入时,实际上被 alias 等价替换成了 import Button from './src/components/button'

以上 alias 配置的含义是把导入语句里的 components 关键字替换成 ./src/components/

这样做可能会命中太多的导入语句,alias 还支持 $ 符号来缩小范围到只命中以关键字结尾的导入语句:

resolve:{
  alias:{
    'vue$': '/path/to/vue.min.js'
  }
}
1
2
3
4
5

vue$ 只会命中以 vue 结尾的导入语句,即只会把 import 'vue' 关键字替换成 import '/path/to/vue.min.js'

# 第二种思路

另一种是通过 patch-package 记录 node_modules 更改记录,生成 patches 目录,然后通过项目的 post-install 脚本在团队中同步这个更改。

这里采用后者来实现第三方库的临时 patch,当然这也适合其他第三方库问题的临时修复。

// 1. 安装
yarn add patch-package postinstall-postinstall
// 2. 修改 node_modules 代码后执行,package-name是出现bug的包名
yarn patch-package package-name
// 3. package.json 中 scripts 增加:
{
  "postinstall": "package-name"
}
1
2
3
4
5
6
7
8

# 前端如何实现条件编译

条件编译,是指 用同一套代码和同样的编译构建过程,根据设置的条件,选择性地编译指定的代码,从而输出不同程序的过程。一般用在 C++、Java、C#这样的编译执行语言。对于前端 javascript、typescript、vue、css、scss、html 等代码,我们也可以使用基于webpackjs-conditional-compile-loader插件实现类似的条件编译功能

场景

  • 代码需要根据运行环境,运行不同的代码
  • 项目交付给多个客户使用,而某些客户会有一些定制模块
  • 前端为了调试,引用了很多 mock 数据

js-conditional-compile-loader (opens new window)是一个 webpack 的 loader 插件,它支持 js 等各种代码文件(只要是文本文件都可以用,如 javascript 的 js 文件、typescript 的 ts 文件、vue 文件、css、scss、less 文件等等),只需在 webpack 的 rules 针对文件类型把它作为最先加载的 loader 即可

const conditionalCompiler = {
  loader: 'js-conditional-compile-loader',
  options: {
    isDebug: process.env.NODE_ENV === 'development', // 可选,默认
    //用法:/* IFDEBUG */...js code.../* FIDEBUG */
    envTest: process.env.ENV_CONFIG === 'test', // 自定义属性
    //用法:/* IFTRUE_evnTest */ ...js code... /* FITRUE_evnTest */
    CLIENT1: process.env.npm_config_client1 // CLIENT1,client1可以自定义
    //用法:/* IFCLIENT1 */ ...js code... /* FICLIENT1 */ ,执行命令: `npm run build --client1`条件为true
  }
}

module.exports = {
  // others...
  module: {
    rules: [
      {
        test: /\.vue$/,
        use: ['vue-loader', conditionalCompiler]
      },
      {
        test: /\.js$/,
        include: [resolve('src'), resolve('test')],
        use: [
          //step-2
          'babel-loader?cacheDirectory',
          //step-1
          conditionalCompiler
        ]
      }
      // others...
    ]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

例如:

/* IFTRUE_CLIENT1 */
var aliCode = require('./ali/alibaba-business.js')
aliCode.doSomething()
/* FITRUE_CLIENT1 */
$state.go('win', { dir: menu.winId /*IFDEBUG , reload: true FIDEBUG*/ })
1
2
3
4
5

执行npm run build,条件为 false,去除 client1 包含的 js 代码

$state.go('win', { dir: menu.winId /*IFDEBUG , reload: true FIDEBUG*/ })
1

执行npm run build --client1,条件为 true,打包 client1 包含的 js 代码

var aliCode = require('./ali/alibaba-business.js')
aliCode.doSomething()
$state.go('win', { dir: menu.winId /*IFDEBUG , reload: true FIDEBUG*/ })
1
2
3

# webpack 与 Grunt 和 Gulp 有什么不同?

Grunt 和 Gulp 是任务运行器,它们可以自动化执行各种任务,如压缩、编译、测试等。

而 Webpack 则是一个模块打包器,专门用于前端资源的打包和管理。

相比之下,Webpack 更加专注于前端资源的处理,而 Grunt 和 Gulp 则更加通用。

# webpack 的构建流程是什么?

Webpack 的构建流程主要包括以下步骤:

  • 首先,Webpack 读取配置文件(webpack.config.js),确定需要处理的入口文件和输出文件;
  • 然后,Webpack 通过加载器(Loader)将入口文件中的模块转换为 CommonJS 格式;
  • 接着,Webpack 使用插件(Plugin)进行代码压缩、优化等操作;
  • 最后,Webpack 将处理后的模块打包成一个或多个 bundle 文件。

# webpack 中的长缓存是什么?如何实现长缓存优化?

长缓存是指通过在资源文件中添加版本号或其他标识符来避免浏览器缓存过期的问题。当资源文件发生变化时,可以通过修改版本号或其他标识符来强制浏览器重新下载资源文件。

Webpack 可以通过在配置文件中设置 output.filename 和 output.chunkFilename 来添加版本号或其他标识符来实现长缓存优化。

# webpack 如何实现模块热更新?如何实现?

模块热更新是指在不刷新整个页面的情况下只更新发生变化的模块的代码。

Webpack 可以通过 Hot Module Replacement(HMR)来实现模块热更新。在配置文件中设置 entry 选项时,可以指定多个入口文件,这样当其中一个入口文件发生变化时,Webpack 只会重新打包该入口文件及其依赖的模块,而不会重新加载整个页面。同时,开发者还可以使用 React Hot Loader 等插件来进一步优化 React 组件的热更新性能。

# HappyPack 和 thread-loader 有什么区别

HappyPack 和 thread-loader 都是用于加速前端构建的工具,但它们有一些区别。

实现方式:HappyPack 是通过多进程方式加载 Loader 来实现加速的,而 thread-loader 则是通过多线程方式加载资源来实现加速的。

适用场景:HappyPack 适用于使用 Loader 进行代码转换、压缩等操作的场景,而 thread-loader 则适用于需要进行大量资源加载的场景。

性能优化:HappyPack 通过多进程并行处理的方式,可以充分利用系统资源,提高构建速度。而 thread-loader 则通过多线程并行处理的方式,可以加快资源加载速度,提高页面响应速度。

# webpack4、5 有什么区别

Webpack 4 和 Webpack 5 在以下方面存在区别:

  • 性能:Webpack 5 相对于 Webpack 4 有更好的性能表现,尤其是在构建速度和 Tree Shaking 方面。
  • 模块联邦:Webpack 5 引入了模块联邦的概念,可以让多个 Webpack 构建的应用程序共享模块,从而减少了代码冗余。
  • 持久性缓存:Webpack 5 引入了持久性缓存,通过使用持久性哈希来生成文件名,可以更好地利用浏览器缓存,从而提高应用程序的加载速度。
  • 解析器:Webpack 5 支持 WebAssembly 模块、JSON 模块和 TypeScript 模块的解析。
  • 构建输出:Webpack 5 支持输出多个 bundle,通过设置 output.chunkFilename 参数来实现。
  • 移除插件:Webpack 5 移除了一些不常用的插件,例如 UglifyJsWebpackPlugin 和 CommonsChunkPlugin。

# 微前端跟模块联邦是什么,有什么区别

微前端是一种设计风格,它将整体结构分解成更小的组件,然后可以在单个页面上组装。这种设计风格使得前端开发更加模块化和可重用,提高了可扩展性和维护性。微前端通过使用独立的 JavaScript 应用程序动态地从另一个应用程序加载代码,并在此过程中共享依赖关系,从而实现了不同应用程序之间的模块化联邦。

模块联邦则是通过 webpack 将应用打包为 js 文件,暴露出方法。其他应用引入该 js 文件,从而构建成完整的应用程序,且不需要重新编译部署,直接使用最新的版本。

以下是微前端和模块联邦的一个简单举例说明:

微前端:

假设有一个大型 Web 应用程序,它由多个独立的子应用程序组成。每个子应用程序都是一个独立的前端项目,使用不同的技术栈和框架开发。这些子应用程序通过微前端架构进行集成,每个子应用程序都是一个独立的 JavaScript 应用程序,通过动态加载和共享代码实现模块化联邦。

例如,主应用程序可能是一个 React 应用程序,而其中一个子应用程序可能是一个 Vue.js 应用程序。通过微前端架构,主应用程序可以动态加载 Vue.js 子应用程序的代码,并在页面上呈现该子应用程序。同时,Vue.js 子应用程序也可以通过微前端架构动态加载其他子应用程序的代码,实现不同应用程序之间的模块化联邦。

模块联邦:

假设有两个独立的前端项目 A 和 B。A 项目使用 React 框架开发,而 B 项目使用 Vue.js 框架开发。由于两个项目的技术栈不同,它们不能直接集成在一起。

通过模块联邦的方式,可以将 A 项目的功能以 JavaScript 模块的形式暴露出来,并在 B 项目中引入该模块。这样,B 项目就可以使用 A 项目的功能,而不需要关心 A 项目的技术栈和实现细节。同时,如果 A 项目更新了代码或添加了新功能,B 项目也可以直接使用最新的代码,而不需要重新编译和部署整个应用程序。

# 微前端和模块联邦有什么优缺点呢

微前端和模块联邦都有各自的优缺点,具体如下:

微前端的优点:

  • 可以与时俱进,不断引入新技术/新框架,提高开发效率、质量、用户体验。
  • 可以很好的实现应用和服务的隔离,互相之间几乎没有影响,可以很好的支持团队引入新技术和新框架。
  • 局部/增量升级,可以逐步替换老旧的大型单体前端,降低风险。
  • 代码简洁、解耦、更易维护,每个小模块的代码库要比一个单体前端的代码库小很多,处理起来更简单方便。
  • 独立部署,可以缩减每次部署涉及的范围,降低风险。

微前端的缺点:

  • 浏览器和框架的支持不够,需要更多的 polyfills,可能影响到用户页面的加载体验。
  • 重写现有的前端应用,需要在整个前端应用上把它们全部转换成 Web Components。
  • 系统架构复杂,当应用被拆分为一个又一个的组件时,组件间的通讯会成为一个特别大的麻烦。

模块联邦的优点:

  • 可以将多个独立的构建组成一个应用程序,不同的构建可以独立的开发与部署。
  • 利用模块联邦可以在一定程度上去实现微前端。

模块联邦的缺点:

  • 对开发者的技术要求较高,需要具备模块化开发、动态加载等方面的技术能力。
  • 模块联邦的实现和维护相对复杂,需要投入更多的时间和精力。

# webpack 的 Loader 和 Plugin 有什么区别?如何使用?

Loader 和 Plugin 都是 Webpack 的扩展工具,但它们的作用和使用方式有所不同。

Loader 主要用于处理各种类型的文件,如 JS、CSS、图片等。它们在 Webpack 打包过程中对每个文件进行转换和加载,然后输出到输出目录中。例如,使用 babel-loader 可以将 ES6 代码转换为 ES5 代码,使用 style-loader 可以将 CSS 代码注入到 HTML 文件中。

Plugin 主要用于完成更复杂的任务,如资源管理、代码分割、热更新等。它们可以在 Webpack 打包过程中的任意阶段进行干预,对资源进行修改、优化等操作。例如,使用 HtmlWebpackPlugin 可以生成 HTML 文件,使用 TerserPlugin 可以压缩 JavaScript 代码。

使用 Loader 和 Plugin 需要在 webpack.config.js 文件中进行配置。例如,要使用 babel-loader 和 style-loader,可以在 module 字段中添加相应的配置;要使用 HtmlWebpackPlugin 和 TerserPlugin,可以在 plugins 字段中添加相应的实例。

# webpack5 如何处理图片资源

在 Webpack 5 中,不再需要使用特定的加载器来处理图片,因为 Webpack 5 已经内置了对图片的支持。

在 Webpack 5 的配置文件中,可以通过 optimization.image 选项来配置图片优化相关的设置。

使用mini-css-extract-plugin插件来提取CSS中的背景图像,并使用css-minimizer-webpack-plugin插件来优化和压缩CSS文件

以下是一个示例的Webpack配置文件:

const path = require('path');  
const MiniCssExtractPlugin = require('mini-css-extract-plugin');  
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');  
const CdnUploadPlugin = require('webpack-cdn-upload'); // CDN上传插件  
  
module.exports = {  
  // ...其他配置项...  
  mode: 'production', // 确保在production模式下进行构建  
  module: {  
    rules: [  
      {  
        test: /\.css$/,  
        use: [  
          MiniCssExtractPlugin.loader,  
          'css-loader',  
        ],  
      },  
      {  
        test: /\.(png|jpe?g|gif|svg)$/i,  
        type: 'asset/resource', // 使用Webpack 5内置的asset模块处理图片  
        generator: {  
          filename: '[path][name].[ext]',  
          dataUrlCondition: {  
            maxSize: 10 * 1024, // 小于10KB的图片自动转为Base64  
          },  
        },  
      },  
    ],  
  },  
  optimization: {  
    minimize: true, // 开启代码压缩  
    minimizer: [  
      '...', // 其他压缩插件,如果有的话  
      new CssMinimizerPlugin(), // 使用CssMinimizerPlugin进行CSS压缩  
    ],  
  },  
  plugins: [  
    new MiniCssExtractPlugin({  
      filename: '[name].css',  
      chunkFilename: '[id].css',  
    }),  
    new CdnUploadPlugin({ // CDN上传插件配置  
      // CDN配置项...  
    }),  
  ],  
  output: {  
    path: path.resolve(__dirname, 'dist'), // 输出目录配置,根据您的实际情况进行调整  
    publicPath: process.env.NODE_ENV === 'production' ? process.env.BASE_URL : '/', // 根据环境变量设置publicPath  
  },  
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
最近更新: 几秒前