webpack

195 阅读17分钟

说说对 webpack 的理解?解决了什么问题?

webpack 赋予了前端工程化的能力

webpack 最初的目标是实现前端项目的模块化,目的是更高效地管理和维护项目中的每一个资源。

  • 最早是通过文件划分的形式实现模块化,即将每个功能及其相关状态数据各自单独放到不同的 js 文件中;
  • 约定每个文件是一个独立的模块,然后将 js 文件引入到 html,一个 script 标签对应一个模块;
  • 弊端:1. 模块都是在全局中工作,大量模块污染了环境,且模块之间没有依赖关系,维护困难,也没有私有空间

早期解决方式:

  1. 命名空间方式:每个模块只暴露一个全局对象,模块的所有内容都挂载到这个对象中,但是一万没有解决依赖等其他问题;
  2. 使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖声明

这些都不是理想的解决方式,理想的解决方式是在页面中引入一个 js 入口文件,其余用到的模块可以通过代码控制,按需加载

从后端渲染的 jsp、php 到前端原生 js,再到 jQuery,再到三大框架 react、vue、angular; 从 js 到后面的 es6、7、8、9、10,再到 ts; css 到 less、sass 等; 前端开发已经十分复杂,会遇到以下的问题:

  • 需要模块化的方式来开发;
  • 需要使用一些高级特性来提高开发效率、安全性,eg. es6+、ts、sass 和 less 等
  • 实时监听文件的变化并反映到浏览器中,提高开发效率;
  • 不仅仅 js 需要模块化,html、css 这些资源文件也面临模块化的问题;
  • 开发完成后,还需要对代码进行压缩、合并和混淆等优化

webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具,主要功能:

  1. 模块化和依赖管理,提升代码可维护性:将每个依赖项模块化,webpack 帮助管理这些依赖项,让每个依赖项能够在正确的时间、正确的地点被正确地引用;支持不同种类的前端模块类型,统一的模块化方案,不同类型的资源文件加载都可以通过代码控制,可以避免用户加载永远用不到的代码
  2. 代码转换:将 es6+语法编译为 es5、将 sass 编译成 css 、将 ts 编译成 js 等
  3. 文件优化,压缩 html、css、js 代码,压缩合并图片等等
  4. 自动刷新,监听本地源代码的变化,自动重新构建、刷新浏览器
  5. 代码校验,在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过;
  6. 自动发布,更新代码后,自动构建出线上发布代码并传输给发布系统
  7. 代码分割,提取多个页面的公共代码,提取首屏不需要执行的部分代码让其异步加载;
  8. 模块合并,对于采用模块化的项目,将其中的多个模块和文件合并成一个文件,减少 http 请求,提高性能

编译:loader、babel-loader、less-loader 分发:打包 bundle、混淆、压缩

常见构建工具及其对比

npm script

在 package.json 中 script 字段是一个对象,每一个属性对应一段 shell 脚本;

优点:内置,无需安装其他依赖;

缺点:功能太简单,不能方便地管理多个任务之间的依赖

glup

优点: 相当于 grunt 的加强版,增加了监听文件、读写文件、流式处理的功能

缺点: 集成度不高,要写很多配置才能用,

webpack

在 webpack 中一切皆模块,通过 loader 转换文件,通过 plugin 注入钩子,最后输出由多个模块组合成的文件

优点:

  • 专注一处理模块化的项目,能做到开箱即用、一步到位;
  • 可以通过 plugin 扩展,完整好用又不失灵活;
  • 使用场景不局限于 web 开发;
  • 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
  • 良好的开发体验
  • 配置有很大的灵活性

webpack 的构建流程?

流程概括

是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化流程:从配置文件和 Shell 语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,通过执行对象的 run 方法开始执行编译;
  3. 确定入口:根据配置中的 entry 找出所有入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译:在经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再将每个 chunk 转换成一个单独的文件加入到输出列表中,这是可以修改输出内容的最后机会;
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中;

在以上过程中,webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 api 改变 webpack 的运行结果。

流程细节

构建流程可以分为以下三个阶段:

  1. 初始化:启动构建,读取、合并配置参数,加载 plugin,实例化 Compiler;
  2. 编译:从 entry 出发,针对每个 module 串行调用对应的 loader 去翻译文件的内容,再找到该 module 依赖的 module,递归地进行编译处理;
  3. 输出: 将编译后的 module 组合成从 chunk,将 chunk 转换成文件,输出到文件系统中。

如果只执行一次构建,上述过程按照顺序执行一次;

如果开启监听模式,则在文件发生变化时会重复执行 2、3

初始化阶段

会发生的事件:

配置

配置文件默认下为 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,初始化时定义了很多钩子函数

编译构建流程

根据配置中的 entry 找出所有的入口文件

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

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

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

compile 编译

执行了 run 方法后,首先会触发 compile,主要是构建一个 Compilation 对象

该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象

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 内容的过程

build module 完成模块编译

这里主要调用配置的 loaders,将我们的模块转成标准的 JS 模块

在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析

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

webpack 如何实现打包?

1. 解析入口文件,获取所有依赖项

首先确定入口文件的地址,通过该地址可以:

  1. 获取其文件内容;
  2. 获取其依赖模块的相对地址(结合入口文件的绝对地址就可以得到依赖模块的绝对地址,进而读取依赖模块的内容);

依赖关系中的模块表示:

  • code:文件解析内容;
  • dependencies:依赖数组,是所有依赖模块的相对路径;
  • filename:文件的绝对路径;
// 模块
'src/entry': {
  code: '', // 文件解析后内容
  dependencies: ["./message.js"], // 依赖项
}

2. 递归解析所有的依赖项,生成一个依赖关系图

模块定义为filename: {}

因为 dependencies 是每个依赖项的相对路径,但解析文件时需要绝对路径,所以 mapping 中存放相对路径到绝对路径的映射

// 模块
'src/entry': {
  code: '', // 文件解析后内容
  dependencies: ["./message.js"], // 依赖项
  mapping:{
    "./message.js": "src/message.js"
  }
}

则依赖关系图为:

// graph 依赖关系图
let graph = {
  // entry 模块
  'src/entry.js': {
    code: '',
    dependencies: ['./src/message.js'],
    mapping: {
      './message.js': 'src/message.js',
    },
  },
  // message 模块
  'src/message.js': {
    code: '',
    dependencies: [],
    mapping: {},
  },
};

总结:

  • 当项目运行时,通过入口文件获取其代码内容,执行其代码,当遇到import时,通过 mapping 映射出其对应的绝对路径,从而读取模块内容;
  • 因为每个模块的绝对路径 filename 是唯一的,所以在将模块加入到依赖图graph中时,仅仅需要判断graph[filename]是否存在,如果存在就不需要二次加入,这样就剔除了模块的重复打包;

3. 使用依赖图,返回一个可以在浏览器运行的 js 文件

IIFE(立即执行函数)能解决全局变量污染的问题

创建一个 IIFE 用于在浏览器上直接运行,并将依赖关系图作为参数传给此立即执行函数;

4. 输出到 dist/bundle.js

webpack 怎么做到的混淆?

Webpack 中最常用的压缩和混淆的 js 插件就是 UglifyJsPlugin,对 css 使用的比较多的工具则是 cssnano。 www.tgf21.com/post/aa6507…

webpack 核心概念

入口(entry)

指示 webpack 应该使用哪个模块,来作为构建其内部依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

默认值是 ./src/index.js

单入口

只会生成一个 chunk,chunk 名称是 main

// 单个入口语法简写
module.exports = {
  entry: './path/to/my/entry/file.js',
};

// 单个入口完全写法
module.exports = {
  entry: {
    main: './path/to/my/entry/file.js',
  },
};

// 也可以将一个文件路径数组传递给 entry 属性,这将创建一个所谓的 "multi-main entry"。在你想要一次注入多个依赖文件,并且将它们的依赖关系绘制在一个 "chunk" 中时,这种方式就很有用。
module.exports = {
  entry: ['./src/file_1.js', './src/file_2.js'],
  output: {
    filename: 'bundle.js',
  },
};

多入口

chunk 名称是 object 中键值对的 键名

module.exports = {
  entry: {
    app: './src/app.js',
    adminApp: './src/adminApp.js',
  },
};

输出(output)

告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js,其他生成文件默认放置在 ./dist 文件夹中。

const path = require('path');

module.exports = {
  entry: './path/to/my/entry/file.js',
  output: {
    path: path.resolve(__dirname, 'dist'), // 输出文件路径
    filename: 'my-first-webpack.bundle.js', // 输出文件名称
  },
};

loader

webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中。

webpack 的其中一个强大的特性就是能通过 import 导入任何类型的模块(例如 .css 文件),其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是很有必要的,因为这可以使开发人员创建出更准确的依赖关系图。

loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS 文件!

module.rules 是个数组,其中每一项描述如何处理部分文件;

const path = require('path');

module.exports = {
  output: {
    filename: 'my-first-webpack.bundle.js',
  },
  module: {
    // test :识别出哪些文件会被转换。
    // use :定义出在进行转换时,应该使用哪个 loader。
    rules: [
      { test: /\.txt$/, use: 'raw-loader' },
      {
        test: /\.css$/,
        use: [
          // [style-loader](/loaders/style-loader)
          { loader: 'style-loader' },
          // [css-loader](/loaders/css-loader)
          {
            loader: 'css-loader',
            options: {
              modules: true,
            },
          },
          // [sass-loader](/loaders/sass-loader)
          { loader: 'sass-loader' },
        ],
      },
    ],
    // loader 从右到左(或从下到上)地取值(evaluate)/执行(execute)。
  },
};

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:test 和 use。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用) raw-loader 转换一下。”

插件(plugin)

在 webpack 构建流程中,特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情

loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建一个插件实例。

const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件

module.exports = {
  module: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
  },
  plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};

在上面的示例中,html-webpack-plugin 为应用程序生成一个 HTML 文件,并自动将生成的所有 bundle 注入到此文件中。

  • plugins 插件(数组):在 webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果 or 做想做的事情

开发插件

在开发插件时,还需要注意以下两点: 只要能拿到 Compiler 或者 Compilation 对象,就能广播新的事件,所以在新开发的插件中也能广播事件,为其他插件监听使用

模式(mode)

通过选择 development, production 或 none 之中的一个,来设置 mode 参数,你可以启用 webpack 内置在相应环境下的优化。其默认值为 production。

# 因为每次打包文件后,dist 文件夹中的 js 文件名都带有不同的 hash,为了避免每次都在 html 中手动修改
# 会自动在 html 中加入 script 标签
npm install html-webpack-plugin -D
module.exports = {
  mode: 'production',
};

浏览器兼容性(browser compatibility)

Webpack 支持所有符合 ES5 标准 的浏览器(不支持 IE8 及以下版本)。webpack 的 import() 和 require.ensure() 需要 Promise。如果你想要支持旧版本浏览器,在使用这些表达式之前,还需要 提前加载 polyfill。

环境(environment)

Webpack 5 运行于 Node.js v10.13.0+ 的版本。

整体流程

Webpack 在启动后会从 Entry 配置的 Module 开始,递归解析 Entry 依赖的所有 Module 每找到一个 Module ,就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module 这些模块会以 Entry 为单位进行分组, Entry 及其 所有依赖的 Module 被分到一个组也就是 Chunk 。最后, Webpack 会将所有 Chunk 转换成文件输出。在整个流程中, Webpack 会在恰当的时机执行 Plugin 里定义的逻辑

模块

模块是一组与特定功能相关的代码,它封装了实现细节,公开了一个公共 api,与其他模块结合以构建更大的应用程序; 模块化:为了实现更高级别的抽象

由于前后端代码扮演的角色不同,侧重点也不一样:

  • 浏览器端的 js 要经历从一个服务器端分发到多个客户端执行;而服务器端 js 则是相同的代码需要多次执行;
  • 浏览器端的 js 的瓶颈在于带宽;服务器端 js 的平静在于 cpu 内存等资源;
  • 加载速度不同:浏览器端的 js 需要通过网络加载代码;服务器端 js 需要从磁盘中加载;

所以,前后端的模块定义也不同

服务器端的模块:CommonJS(eg. node),是同步导入,但因为文件都在本地,即使卡住主线程影响也不大;如果在浏览器端用同步导入,会造成很差的用户体验。

浏览器端的模块:

  • AMD(异步模块定义):浏览器中模块的异步模型;
  • UMD(通用模块定义):本质上是,一段 js 代码放在库的顶部,可以让任何加载程序、任何环境加载他们;
  • ES6 module:定义了异步导入和导出模块的语义,会编译成 require/exports来执行,最常用

模块化

将一个复杂系统分解为多个模块,以方便编码。

旧的模块化方案:

  1. 命名空间,缺点:
  • 可能存在命名空间冲突的问题;
  • 无法合理地管理项目依赖和版本;
  • 无法方便地控制依赖的加载顺序;
  • 随着项目变大,这种方式难以维护;

CommonJS

require() 同步加载依赖的其他模块; module.exports导出需要暴露的接口;

// a.js
module.exports = { foo, bar };

// b.js
const { foo, bar } = require('./a.js');

特点

  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块是同步加载的,即只有加载完成,才能执行后面的操作
  • 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存
  • require 返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值

优点

  • 代码可复用于 Node 环境,例如做同构应用
  • npm 发布的很多第三方包都采用了 CommonJS 规范;

缺点

  • 无法直接运行在浏览器中,需要通过工具转换成 es5;

AMD

核心思想

Asynchronous ModuleDefinition(AMD),异步模块定义,

采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行

采用 amd 导入及导出的代码如下:

// 定义一个模块
define('module', ['dep'], function (dep) {
  return exports;
});

// 导入和使用
require(['module'], function (module) {});

优点:

  • 无需代码转换,可直接在浏览器中运行;
  • 可异步加载模块;
  • 可并行加载多个依赖;
  • 代码可运行在浏览器和 Node 中

缺点:

js 运行环境没有原生支持,需要先导入实现了 amd 的库

ES6 module

是 ECMA 提出的 js 规范 模块功能主要由两个命令构成:

export:用于规定模块的对外接口 import:用于输入其他模块提供的功能

export

一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export 关键字输出该变量

// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;

或;
// 建议使用下面写法,这样能瞬间确定输出了哪些变量
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export { firstName, lastName, year };

输出函数或类

export function multiply(x, y) {
  return x * y;
}

通过 as 可以进行输出变量的重命名

function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

import

使用 export 命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import 命令加载这个模块

// main.js
import { firstName, lastName, year } from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}

同样如果想要输入变量起别名,通过 as 关键字

import { lastName as surname } from './profile.js';

当加载整个模块的时候,需要用到星号*

// circle.js
export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}

// main.js
import * as circle from './circle';
console.log(circle); // {area:area,circumference:circumference}

输入的变量都是只读的,不允许修改,但是如果是对象,允许修改属性

import { a } from './xxx.js';

a.foo = 'hello'; // 合法操作
a = {}; // Syntax Error : 'a' is read-only;

不过建议即使能修改,但我们不建议。因为修改之后,我们很难差错

import 后面我们常接着 from 关键字,from 指定模块文件的位置,可以是相对路径,也可以是绝对路径

import { a } from './a';

在编译阶段,import 会提升到整个模块的头部,首先执行

foo();

import { foo } from 'my_module';

多次重复执行同样的导入,只会执行一次

import 'lodash';
import 'lodash';

上面的情况,大家都能看到用户在导入模块的时候,需要知道加载的变量名和函数,否则无法加载

如果不需要知道变量名或函数就完成加载,就要用到 export default 命令,为模块指定默认输出

// export-default.js
export default function () {
  console.log('foo');
}

加载该模块的时候,import 命令可以为该函数指定任意名字

// import-default.js
import customName from './export-default';
customName(); // 'foo'

动态加载

允许您仅在需要时动态加载模块,而不必预先加载所有模块,这存在明显的性能优势

这个新功能允许您将 import()作为函数调用,将其作为参数传递给模块的路径。 它返回一个 promise,它用一个模块对象来实现,让你可以访问该对象的导出

import('/modules/myModule.mjs').then((module) => {
  // Do something with the module.
});

复合写法

如果在一个模块之中,先输入后输出同一个模块,import 语句可以与 export 语句写在一起

export { foo, bar } from 'my_module';

// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };

同理能够搭配 as、*搭配使用

核心思想

是 ECMA 提出的 js 模块化规范,在语言层面实现了模块化

优点

  • 浏览器和 node 原生支持;
  • 可以在编译时就完成模块加载(由于编译加载,使得静态分析成为可能)

缺点

需要转换成 es5 才能运行

应用场景

如今,ES6 模块化已经深入我们日常项目开发中,像 vue、react 项目搭建项目,组件化开发处处可见,其也是依赖模块化实现

vue 组件

<template>
  <div class="App">组件化开发 ---- 模块化</div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String,
  },
};
</script>

react 组件

function App() {
  return <div className="App">组件化开发 ---- 模块化</div>;
}

export default App;

scss 等语言可以实现样式文件的模块化

loader & plugin 区别?编写他们的思路?

  • loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
  • plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事

两者在运行时机上的区别:

  • loader 运行在打包文件之前; loader,实质是一个转换器,将 A 文件进行编译形成 B 文件,操作的是文件,比如将 A.scss 或 A.less 转变为 B.css,单纯的文件转换过程
  • plugins 在整个编译周期都起作用 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果

编写 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; // 同步
};

一般在编写 loader 的过程中,保持功能单一,避免做多种功能

如 less 文件转换成 css 文件也不是一步到位,而是 less-loader、css-loader、style-loader 几个 loader 的链式调用才能完成转换

编写 plugin

由于 webpack 基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务

在之前也了解过,webpack 编译会创建两个核心对象:

  • 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 事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容