资讯专栏INFORMATION COLUMN

webpack源码分析之四:plugin

yhaolpz / 453人阅读

摘要:流程划分纵观整个打包过程,可以流程划分为四块。核心类关系图功能实现模块通过将源码解析为树并拆分,以及直至基础模块。通过的依赖和切割文件构建出含有和包含关系的对象。通过模版完成主入口文件的写入,模版完成切割文件的写入。

前言

插件plugin,webpack重要的组成部分。它以事件流的方式让用户可以直接接触到webpack的整个编译过程。plugin在编译的关键地方触发对应的事件,极大的增强了webpack的扩展性。它的出现让webpack从一个面向过程的打包工具,变成了一套完整的打包生态系统。

功能分析 Tapable

既然说到了事件流,那么就得介绍Tapable了,Tapable是webpack里面的一个小型库,它允许你自定义一个事件,并在触发后访问到触发者的上下文。当然他也支持异步触发,多个事件同步,异步触发。本次实现用的是较早的v0.1.9版,具体文档可查看tapable v0.19文档

在webpack内使用,如SingleEntryPlugin中

compiler.plugin("make",function(compilation,callback){
   compilation.addEntry(this.context, new SingleEntryDependency({request: this.entry}), this.name, callback);
})

在compiler内部触发。

 this.applyPluginsParallel("make",compilation, err => {
     /* do something */
 })

解析入口文件时,通过EntryOptionPlugin解析entry类型并实例化SingleEntryPlugin, SingleEntryPlugin在调用compilation的addEntry函数开启编译。这种观察者模式的设计,解耦了compiler, compilation,并使它们提供的功能更加纯粹,进而增加扩展性。

流程划分

纵观整个打包过程,可以流程划分为四块。

初始化

构建

封装

文件写入

模块划分

接入plugin后,webpack对parse,resolve,build,writeSource等功能的大规模重构。
目前拆分模块为

Parser模块,负责编译module。

Resolver模块,负责对文件路径进行解析。

ModuleFactory模块,负责完成module的实例化。

Module模块,负责解析出modules依赖,chunk依赖。构建出打包后自身module的源码。

Template模块,负责提供bundle,chunk模块文件写入的模版。

Compilation模块,负责文件编译细节,构建并封装出assets对象供Compiler模块进行文件写入。

Compiler模块,负责实例化compilation,bundle文件的写入。监听modules的变化,并重新编译。

核心类关系图

功能实现 Parser模块

通过exprima将源码解析为AST树,并拆分statements,以及expression直至Identifier基础模块。

解析到CallExpression时触发call事件。

解析到MemberExpression,Identifier时触发expression事件。

提供evaluateExpression函数,订阅Literal,ArrayExpression,CallExpression,ConditionalExpression等颗粒化的事件供evaluateExpression调用。

 case "CallExpression":
            //do something
            this.applyPluginsBailResult("call " + calleeName, expression);
            //do something
            break;
 case "MemberExpression":
            //do something
            this.applyPluginsBailResult("expression " + memberName, expression);
            //do something
            break;
 case "Identifier":
            //do something
            this.applyPluginsBailResult("expression " + idenName, expression);
               //do something
            break;           
 this.plugin("evaluate Literal", (expr) => {})
 this.plugin("evaluate ArrayExpression", (expr) => {})
 this.plugin("evaluate CallExpression", (expr) => {})
 ...

如需要解析require("a"),require.ensure(["b"],function(){})的时候,注册plugin去订阅"call require",以及"call require.ensure",再在回调函数调用evaluateExpression解析expression。

Resolver模块

封装在enhanced-resolve库,提供异步解析文件路径,以及可配置的filestream能力。在webpack用于缓存文件流以及以下三种类型模块的路径解析。

普通的module模块

带context的module模块

loader模块

用法如

ResolverFactory.createResolver(Object.assign({
            fileSystem: compiler.inputFileSystem,
            resolveToContext: true
        }, options.resolve));

具体配置可去查看github文档

ModuleFactory模块

子类有NormalModuleFactory,ContextModuleFactory。常用的NormalModuleFactory功能如下

实例化module之前,调用Resolver模块解析出module和preloaders的绝对路径。

通过正则匹配module文件名,匹配出rules内的loaders,并和preloaders合并。

实例化module

这里主要是使用async库的parallel函数并行的解析loaders和module的路径,并整合运行结果。

async.parallel([
                (callback) => {
                    this.requestResolverArray( context, loader, resolver, callback)
                },
                (callback) => {
                    resolver.normal.resolve({}, context, req, function (err, result) {
                        callback(null, result)
                    });
                },
            ], (err, result) => {
                    let loaders = result[0];
                const resource = result[1];
                //do something
            })

async模块是一整套异步编程的解决方案。async官方文档

Module模块

运行loaders数组内的函数,支持同步,异步loaders,得到编译前源码。

源码交由Parser进行解析,分析出modules依赖和blocks切割文件依赖

提供替换函数,将源码替换,如require("./a")替换为__webpack_require__(1)

一个编译好的module对象包含modules依赖ModuleDependency和blocks依赖RequireEnsureDependenciesBlock,loaders,源码_source,其数据结构如下:

{
  chunks: [],
  id: null,
  parser: 
   Tapable {
     _plugins: 
      { "evaluate Literal": [Array],
        "evaluate ArrayExpression": [Array],
        "evaluate CallExpression": [Array],
        "call require": [Array],
        "call require:commonjs:item": [Array],
        "call require.ensure": [Array] },
     options: {},
     scope: { declarations: [] },
     state: { current: [Circular], module: [Circular] },
     _currentPluginApply: undefined },
  fileDependencies: 
   [ "/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js" ],
  dependencies: 
   [ ModuleDependency {
       request: "./module!d",
       range: [Array],
       class: [Function: ModuleDependency],
       type: "cms require" },
     ModuleDependency {
       request: "./assets/test",
       range: [Array],
       class: [Function: ModuleDependency],
       type: "cms require" } ],
  blocks: 
   [ RequireEnsureDependenciesBlock {
       blocks: [],
       dependencies: [Array],
       requires: [Array],
       chunkName: "",
       beforeRange: [Array],
       afterRange: [Array] } ],
  loaders: [],
  request: "/Users/zhujian/Documents/workspace/webpack/simple-webpack/example/a.js",
  fileName: "a.js",
  requires: [ [ 0, 7 ], [ 23, 30 ] ],
  context: "/Users/zhujian/Documents/workspace/webpack/simple-webpack/example",
  built: true,
  _source: 
   RawSource {
     _result: 
      { source: "require("./module!d");
require("./assets/test");
require.ensure(["./e","./b"], function () {
    console.log(1)
    console.log(1)
    console.log(1)
    console.log(1)
    require("./m");
    require("./e");
});
" },
     _source: "require("./module!d");
require("./assets/test");
require.ensure(["./e","./b"], function () {
    console.log(1)
    console.log(1)
    console.log(1)
    console.log(1)
    require("./m");
    require("./e");
});
" 
             } 
     }
Compilation模块

通过entry和context,获取到入口module对象,并创建入口chunk。

通过module的modules依赖和blocks切割文件构建出含有chunk和modules包含关系的chunk对象。

给modules和chunks的排序并生成id,触发一系列optimize相关的事件(如CommonsChunkPlugin就是使用optimize-chunks事件进行开发),最终构建出有文件名和源码映射关系的assets对象

一个典型的含有切割文件的多入口entry的assets对象数据结构如下:

assets: 
   { "0.bundle.js": 
      Chunk {
        name: "",
        parents: [Array],
        modules: [Array],
        id: 0,
        source: [Object] },
     "main.bundle.js": 
      Chunk {
        name: "main",
        parents: [],
        modules: [Array],
        id: 1,
        entry: true,
        chunks: [Array],
        blocks: true,
        source: [Object] },
     "multiple.bundle.js": 
      Chunk {
        name: "multiple",
        parents: [],
        modules: [Array],
        id: 2,
        entry: true,
        chunks: [Array],
        source: [Object] } 
  }
Compiler模块

解析CLI, webpack配置获取options对象,初始化resolver,parser对象。

实例化compilation对象,触发make 并行事件调用compilation对象的addEntry开启编译。

获取到assets对象,通过触发before-emit事件开启文件写入。通过JsonMainTemplate模版完成主入口bundle文件的写入,JsonpChunkTemplate模版完成chunk切割文件的写入。 使用async.forEach管理异步多文件写入的结果。

监听modules的变化,并重新编译。

考虑到多入口entry的可能,make调用的是并行异步事件

this.applyPluginsParallel("make", compilation, err => {
    //do something
    compilation.seal(err=>{})
    //do something
}
代码实现

本人的简易版webpack实现simple-webpack

总结

相信大家都有设计过业务/开源代码,很多情况是越往后写,越难维护。一次次的定制化的需求,将原有的设计改的支离破碎。这个时候可以试试借鉴webpak的思想,充分思考并抽象出稳定的基础模块,划分生命周期,将模块之间的业务逻辑,特殊需求交由插件去解决。

完。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/96472.html

相关文章

  • 关于 webpack 你可能忽略的细节(附源码分析

    摘要:本篇的主要目标是通过实际问题来介绍中容易被人忽略的细节以及源码分析以最新发布的版本的源码为例并且提供几种解决方案。探究原因及源码分析这里以最新发布的版本的源码作为分析。解决方案加参数基于上面简要的分析,我们来尝试下参数的作用。 注:本篇不是入门教程,入门请直接查看官方文档。本篇的主要目标是通过实际问题来介绍 webpack 中容易被人忽略的细节, 以及源码分析(以最新发布的 relea...

    mtunique 评论0 收藏0
  • webpack源码plugin机制

    摘要:调用的目的是为了注册你的逻辑指定一个绑定到自身的事件钩子。这个对象在启动时被一次性建立,并配置好所有可操作的设置,包括,和。对象代表了一次资源版本构建。一个对象表现了当前的模块资源编译生成资源变化的文件以及被跟踪依赖的状态信息。 引言 在上一篇文章Tapable中介绍了其概念和一些原理用法,和这次讨论分析webpack plugin的关联很大。下面从实现一个插件入手。 demo插件 f...

    glumes 评论0 收藏0
  • vue-loader 源码解析系列之 整体分析

    摘要:笔者系贡献者之一官方说明简单来说就是将文件变成,然后放入浏览器运行。部分首先分析部分从做右到左,也就是被先后被和处理过了。源码解析之二源码解析之三写作中源码解析之四写作中作者博客作者微博 笔者系 vue-loader 贡献者(#16)之一 官方说明 vue-loader is a loader for Webpack that can transform Vue components ...

    icattlecoder 评论0 收藏0
  • webpack 源码分析(四)——complier模块

    摘要:源码分析四模块上一篇我们看到,通过对命令行传入的参数和配置文件里的配置项做了转换包装,然后传递给的模块去编译。这一篇我们来看看做了些什么事。在上面的分析中,我们看到最核心的其实就是实例,接下来我们就看下它的类的内部逻辑。 webpack 源码分析(四)——complier模块 上一篇我们看到,webpack-cli 通过 `yargs 对命令行传入的参数和配置文件里的配置项做了转换包装...

    tianlai 评论0 收藏0
  • webpack源码分析(一)-流程分析

    摘要:先上一张流程图一般打包文件是通过调用这实际上等同于通过调用源码如下将用户本地的配置文件拼接上内置的参数初始化对象编辑器对象,包含所有主环境相关内容注册插件和用户配置的插件触发和上注册的事件注册内置插件源码如下注册触发钩子触发钩子触发钩子 先上一张流程图showImg(https://segmentfault.com/img/bVbeW6Q?w=851&h=497);一般webpack打...

    codecraft 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<