前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >4. 创建模块实例,为模块解析准备

4. 创建模块实例,为模块解析准备

作者头像
tinyant
发布2022-11-16 17:28:31
6900
发布2022-11-16 17:28:31
举报

通过一个demo带你深入进入webpack@4.46.0源码的世界,分析构建原理,专栏地址,共有十篇。


上一节说到normalModuleFactory.create来创建模块实例,下面从该方法开始分析创建模块实例需要哪些准备工作。

NormalModuleFactory

该部分有大段篇幅分析loader的解析,因为会涉及内联loader,因此以解析main.js中的第一个import引入的依赖为例。 该资源的解析是在main.js模块构建之后获取其dependencies,而后基于dependencies进行依赖模块的构建。

在addModuleDependencies -> factory.create (这里的factory也是NormalModuleFactory类型)

此时的create方法的参数如下:

看到dependencies有两个元素,这是前面processModuleDependencies方法分类的结果,指向相同资源路径(这里request="./custom-loaders/custom-inline-loader.js??share-opts!./a?c=d")的dependency构建一次即可。

代码语言:javascript
复制
// NormalModuleFactory.js

create(data, callback) {
   const dependencies = data.dependencies;   
    // dependencies[0]是否缓存过,缓存过则直接返回
    
   const request = dependencies[0].request; //...   
   this.hooks.beforeResolve.callAsync({ /*...*/ }, (err, result) => {
         const factory = this.hooks.factory.call(null);
         factory(result, (err, module) => {/*...*/});
      }
   );
}

constructor(context, resolverFactory, options) {
    //...
    
    // 注意:返回一个函数: 模块工厂用来构造模块实例
    this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
        let resolver = this.hooks.resolver.call(null); // 返回一个函数
        // 执行this.hooks.resolver.tap返回的函数或构造模块需要的信息
        resolver(result, (err, data) => {       
            // hooks.afterResolve、hooks.createModule //...
            createdModule = new NormalModule(result);
            // hooks.module
        });
    }

    // 注意:返回一个函数
    this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
        // ... 关键,获取应用在该模块的真实路径、loaders等信息,收集依赖需要的parser等
        callback(null, { // new NormalModule的入参
           context: context,
           request: loaders.map(loaderToIdent).concat([resource]).join("!"),
           dependencies: data.dependencies,
           userRequest,
           rawRequest: request,
           loaders,
           resource,
           matchResource,
           resourceResolveData,
           settings,
           type,
           parser: this.getParser(type, settings.parser),
           generator: this.getGenerator(type, settings.generator),
           resolveOptions
        });
    })
}

主要两个步骤:

  1. hooks.resolver的目的是解析loader和resource等信息,创建模块实例需要用到
  2. hooks.factory钩子的目的是创建模块实例

注意这两个订阅函数的执行结果是返回一个函数:factroy()、resolver()

resolver(): 收集各种模块构建过程中需要的信息

该部分有大量代码解析loader,下面先介绍下loader的特性。

loader的类型、运行阶段、覆盖特性

loader的类型:

默认是normal,

Rule.enforce,enfoce:可能的值:pre | post 可以强制当前loader作用的阶段,前置还是后置。

另外还有inlined loader,内联loader应用在import/require的路径中,比如

代码语言:javascript
复制
import {logA} from './custom-loaders/custom-inline-loader.js!./a'

运行阶段:

webpack的loaders的执行实际是交个loader-runner这个库,后面会以单独小结分析该库。这里简单说下,所有 loader 依次进入两个阶段:

  • Pitching 阶段:loader 上的 pitch 方法按 post、inline、normal、pre 的顺序调用。Pitching Loader
  • normal阶段:loader 上的正常方法按 pre、normal、inline、post 的顺序执行。模块源代码的转换发生在这个阶段。

覆盖特性:

  • 所有normal loaders 都可以通过请求中(request)的前缀!来省略(覆盖)。
  • 所有normal、pre loaders都可以通过前缀 -! 省略(覆盖)
  • 所有normal、pre、post loaders 都可以通过前缀 !! 省略(覆盖)。

分析: 主要是loaders的匹配和本地路径解析

一共分为五个部分介绍

代码语言:javascript
复制
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
    //...
    
    // 第一部分 -----------------------
    // 获取xxxResolver
    const loaderResolver = this.getResolver("loader");
    const normalResolver = this.getResolver("normal", data.resolveOptions);
    
    let matchResource = undefined; //... 先遗留
    let requestWithoutMatchResource = request;
    
    // 通过request判断是否需要忽略部分loaders
    // 如果request以'-!'开始,则忽略normal、pre loaders
    const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
    // 如果request以'!'开始,则忽略normal loaders
    const noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
    // 如果request以'-!!'开始,则忽略pre、post、normal loaders
    const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
    
    // 获取内联形式的loader信息
    let elements = requestWithoutMatchResource.replace(/^-?!+/, "").replace(/!!+/g, "!").split("!");
    let resource = elements.pop(); // 最后一个是资源路径,排除掉
    
    let resource = elements.pop();
    // loader形式可能是xxx-loader?a=b等,转换为 {loader、options}结构,loader是loader的路径,options是query部分。
    elements = elements.map(identToLoaderRequest);

    // 第二部分 -----------------------
    asyncLib.parallel([callback => (/*..获取内联系形式loader绝对路径.*/),  
            callback => { /*..获取resource绝对路径.*/ }], 
            (err, results) => {
            // 第三部分 -----------------------
            // loader匹配、分类(normal、pre、post)、确定模块的类型,见下面具体分析
            
            // 第四部分 -----------------------
            // 获取normal、pre、post各类型loader的绝对路径
            asyncLib.parallel([/*..useLoadersPost.*/, /*.useLoaders..*/,/*..useLoadersPre.*/],
                (err, results) => {
                    // 连接所有的loaders:post -> inline -> normal -> pre
                    loaders = results[0].concat(loaders, results[1], results[2]);
                    // 第五部分 -----------------------
                    const type = settings.type; // 模块类型
                    const resolveOptions = settings.resolve; // 
                    callback(null, { ... });
                }
            );
        }
    );
});

第一部分: 获取内联loader

  1. 获取 loaderResolver、normalResolver,具体的获取逻辑会在下面介绍三个核心对象 - resolver时介绍
  2. 根据request来判断是否要屏蔽pre、normal、post等类型的loaders(即上面介绍的覆盖特性)。确定了这几个变量noPreAutoLoadersnoAutoLoadersnoPrePostAutoLoaders,后面通过this.ruleSet.exec初步匹配loaders的基础上根据这些变量进行进一步的过滤
  3. request中解析出内联的loaders,存储到elements

第二部分: 获取内联loader本地路径和当前资源的本地路径

代码语言:javascript
复制
asyncLib.parallel([
    callback => (/*..获取内联系形式loader绝对路径.*/), 
    callback => { /*..获取resource绝对路径.*/ }],
    (err, results) => { /*...*/ }
)
  1. 获取内联loaders的绝对路径
  2. 获取resource的绝对路径

示例介绍:

代码语言:javascript
复制
import {logA} from './custom-loaders/custom-inline-loader.js!./a'

比如上面的loader路径和js资源路径都是相对路径,这里会经过loaderResolvernormalResolver来解析为本地路径,需要注意的是,二者的解析路径存在一些差异,因此有两个resolver实例。路径的解析webpack交给了一个单独的库enhanced-resolver,后面会单独介绍该库。

第三部分:匹配非内联loaders

代码语言:javascript
复制
let loaders = results[0];
const resourceResolveData = results[1].resourceResolveData;
resource = results[1].resource;

// 如果内联loader携带了ident,即形式如xxx-loader??xxxident,会从当前所有的loaders(内置 + 用户配置的)
// 查找到相同ident的loader的options,并赋值给该内联loader,
// 如果这个options是对象,则多个loader之间可以共享该对象。
try {
   for (const item of loaders) {
      if (typeof item.options === "string" && item.options[0] === "?") {
         const ident = item.options.substr(1);
         item.options = this.ruleSet.findOptionsByIdent(ident);
         item.ident = ident;
      }
   }
} catch (e) //...

// ... 没有resouce的异常处理

// 原始内联形式的request的重新连接,区别是loaders和resource都改为绝对路径了
const userRequest =
   (matchResource !== undefined ? `${matchResource}!=!` : "") +
   loaders.map(loaderToIdent).concat([resource]).join("!");

let resourcePath = matchResource !== undefined ? matchResource : resource;
let resourceQuery = "";
const queryIndex = resourcePath.indexOf("?");
// resourcePath拆分为path和query两部分
if (queryIndex >= 0) {
   resourceQuery = resourcePath.substr(queryIndex);
   resourcePath = resourcePath.substr(0, queryIndex);
}

// this.ruleSet包含了所有的loaders信息(内置和用户的),根据resourceQuery、resource等信息来获取匹配的loaders
const result = this.ruleSet.exec({ /*...*/ });

const settings = {};
// loaders分类
const useLoadersPost = []; // post loaders
const useLoaders = []; // normal loaders
const useLoadersPre = []; // pre loaders
for (const r of result) {
   if (r.type === "use") {
      // 声明为post,并且request中没有屏post loaders
      if (r.enforce === "post" && !noPrePostAutoLoaders) {
         useLoadersPost.push(r.value);
     // 声明为pre,并且没有屏蔽 pre loaders
      } else if (r.enforce === "pre" && !noPreAutoLoaders && !noPrePostAutoLoaders) {
         useLoadersPre.push(r.value);
     // 没有屏蔽normal loaders
      } else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
         useLoaders.push(r.value);
      }
   } else if (typeof r.value === "object" && r.value !== null && typeof settings[r.type] === "object" && settings[r.type] !== null) {
      settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
   } else {
      settings[r.type] = r.value;
   }
}

这里我们需要关心一下setting.type,在后面会用到;

首先this.ruleSet来内内置规则和用户提供的规则

代码语言:javascript
复制
// NormalModuleFactory.Constructor
this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));

options.defaultRules规则由WebpackOptionsDefaulter中提供的默认规则,在获取parsergenerator时需要用到,作用是确定模块化类型,该值的可选值参考,用户也可以自己提供该配置来覆盖默认规则。默认的规则如下:

看到后面三个都提供了test,显然不会命中我们的.js文件,也就是如果开发者不主动设置的话,默认的js,ts等文件都会命中第一个规则,会得到 setting.type = "javascript/auto";因此在getParsergetGenerator中的入参type都是该值

这里主要做了三件事情

设置内联loader的ident和options

对loaders进行匹配并且根据屏蔽规则确定最终可以应用的loaders,并根据normal、pre、post loader进行分类,分类的目的是为了后面按照这个顺序在加上内联loaders组装成最终的loaders

代码语言:javascript
复制
// results[0]:post loaders
// loaders: 内联loaders,从request中解析出来
// results[0]: normal loaders
// results[0]: pre loaders
// 类型:post -> inline -> normal -> pre
loaders = results[0].concat(loaders, results[1], results[2]);

设置setting.type确定模块类型

我们的demo在这里的loader分类结果如下:

代码语言:javascript
复制
// loaders: 内联loaders
[{
    "loader": "/Users/.../src/simple/custom-loaders/custom-inline-loader.js",
    "options": { "a": "b" },
    "ident": "share-opts"
}]

// useLoaders: normal loaders
[{
    "options": { "a": "b" },
    "ident": "share-opts",
    "loader": "./src/simple/custom-loaders/custom-normal-loader"
}]

// useLoadersPost: post loaders
[{ "loader": "./src/simple/custom-loaders/custom-post-loader", "options": undefined }]

// useLoadersPre: pre loaders
[{ "loader": "./src/simple/custom-loaders/custom-pre-loader", "options": undefined  }]

这里的内联loader:custom-inline-loader通过在后面追加??share-opts共享了在webpack.config.js中配置的custom-normal-loader提供的options,二者具有相同的ident(identifier的缩写)

除了内联loader的路径是本地路径外(因为在上面已经解析过了),其余都是原始路径,比如我们示例中使用的相对路径,这里依然是相对路径。在下一个部分会被loaderResolver解析为本地路径

第四部分: 获取非内联loaders本地路径

代码语言:javascript
复制
asyncLib.parallel([ /*..useLoadersPost.*/, 
    /*.useLoaders..*/,
    /*..useLoadersPre.*/], 
    (err, results) => { /*...*/ })

在第二部分获取了内联loader的本地路径,经过第三部分确定了最终被应用的prenormalpost loaders,这里就是对第三部分获取的非内联loader通过loaderResolver进行本地路径的获取。

useLoadersPost: 存储post loaders;useLoaders: 存储normal loaders;useLoadersPre: 存储pre loaders

上面的pre、normal、post loaders的路径被转为了本地路径(下面示例省略了中间部分)

代码语言:javascript
复制
[
    [{
        "loader": "/Users/.../src/simple/custom-loaders/custom-post-loader.js"
    }],
    [{
        "options": {
            "a": "b"
        },
        "ident": "share-opts",
        "loader": "/Users/.../src/simple/custom-loaders/custom-normal-loader.js"
    }],
    [{
        "loader": "/Users/.../src/simple/custom-loaders/custom-pre-loader.js"
    }]
]

第五部分: 获取parser、generator等核心对象

确认了资源的本地路径以及最终需要应用的loaders后,另外还另外获取两个核心对象parsergenerator,会在三个核心对象段落中介绍

代码语言:javascript
复制
const type = settings.type;
const resolveOptions = settings.resolve;
callback(null, {
   context: context,
   request: loaders.map(loaderToIdent).concat([resource]).join("!"),
   dependencies: data.dependencies,
   userRequest,
   rawRequest: request,
   loaders,
   resource,
   matchResource,
   resourceResolveData,
   settings,
   type,
   parser: this.getParser(type, settings.parser),
   generator: this.getGenerator(type, settings.generator),
   resolveOptions
});

注意原先的request被赋给了rawRequest,request被重新更改为所有loaders和当前resource路径的拼接后的字符串,如下

代码语言:javascript
复制
'/Users/.../src/simple/custom-loaders/custom-post-loader.js!/Users/.../src/simple/custom-loaders/custom-inline-loader.js??share-opts!/Users/.../src/simple/custom-loaders/custom-normal-loader.js??share-opts!/Users/.../src/simple/custom-loaders/custom-pre-loader.js!/Users/.../src/simple/a.js?c=d'

然后就是调用callback进入到回调函数中创建NormalModule实例,如下

代码语言:javascript
复制
resolver(result, (err, data) => {
     //...
     new NormalModule(...)
})

三个核心对象的获取:parser | generator | resolver

三个核心对象 parser、generator、resolver

下面的createParsercraeteGenerator都用到了上面解析出的type: javascript/auto

代码语言:javascript
复制
class NormalModuleFactory extends Tapable {
    getParser(type, parserOptions) {
        // 缓存中是否创建过 this.parserCache,有则返回
        // 没有则调用createParser,并缓存到parserCache
    }
    createParser(type, parserOptions = {}) {
       const parser = this.hooks.createParser.for(type).call(parserOptions);
       this.hooks.parser.for(type).call(parser, parserOptions);
       return parser
    }
    
    getGenerator(type, generatorOptions) {
        // 缓存中是否创建过 this.generatorCache,有则返回
        // 没有则调用createGenerator,并缓存到generatorCache
    }
    createGenerator(type, generatorOptions = {}) {
        const generator = this.hooks.createGenerator.for(type).call(generatorOptions);
        this.hooks.generator.for(type).call(generator, generatorOptions);
        return generator;
    }
    
    getResolver(type, resolveOptions) { /*...*/ }
}

parser: 通过解析ast收集依赖、generator: 最终产物的代码生成

WebpackOptionsApply中注册了JavascriptModulesPlugin插件

代码语言:javascript
复制
// WebpackOptionsApply.js 
new JavascriptModulesPlugin().apply(compiler);

JavascriptModulesPlugin注册hooks.createParserhooks.createGenerator钩子

代码语言:javascript
复制
// JavascriptModulesPlugin.js
// apply方法中注册了下面两个钩子

const Parser = require("./Parser");
const JavascriptGenerator = require("./JavascriptGenerator");

normalModuleFactory.hooks.createParser.for("javascript/auto").tap("JavascriptModulesPlugin", options => {
    return new Parser(options, "auto");
});
// "javascript/dynamic"
// "javascript/esm"

normalModuleFactory.hooks.createGenerator.for("javascript/auto").tap("JavascriptModulesPlugin", () => {
    return new JavascriptGenerator();
});
// "javascript/dynamic"
// "javascript/esm"

当前案例中的createParsercreateGenerator入参typejavascript/auto

resolver:路径解析器

代码语言:javascript
复制
// Compiler.js
const ResolverFactory = require("./ResolverFactory");

createNormalModuleFactory() {
   const normalModuleFactory = new NormalModuleFactory(
      this.options.context,
      this.resolverFactory, // new ResolverFactory()
      this.options.module || {}
   );
    //...
}
代码语言:javascript
复制
// NormalModuleFactory.js
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
    //...
    const loaderResolver = this.getResolver("loader");
    const normalResolver = this.getResolver("normal", data.resolveOptions);
   //...
}

getResolver(type, resolveOptions) { // type: normal、loader
   return this.resolverFactory.get(
      type,
      resolveOptions || EMPTY_RESOLVE_OPTIONS
   );
}

webpack/lib/ResolverFactory

代码语言:javascript
复制
const Factory = require("enhanced-resolve").ResolverFactory;

get(type, resolveOptions) {
    // 是否缓存过,巧妙的是缓存的key,JSON.stringify
    // 调用_create创建resolver
   const newResolver = this._create(type, resolveOptions);
   //...
}

_create(type, resolveOptions) {
   const originalResolveOptions = Object.assign({}, resolveOptions);
   resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions);
   const resolver = Factory.createResolver(resolveOptions);
   //...
}

这里实际会进入到enhanced-resolve库中进行创建工作,具体创建工作和使用后面会单独出一节介绍。

小结

主要的步骤如下

  • 获取loaders信息、resource本地路径、parser、generator等信息。
    • 解析出内联loader
    • 解析内联loader和resource本地路径
    • 通过this.ruleSet匹配所有的非内联loader
    • 解析非内联loader路径为本地路径
    • 获取parser、generator:hooks.createParser、hooks.createGenerator
  • 创建模块实例(new NormalModule(...))

NormalModule

NormalModuleFactory.create创建完NormalModule实例后,会调用module.build进行模块的真正的构建。

为什么说是真正的构建,因为之前都是准备工作,并没有获取模块内容和内容解析相关的工作。现在才开始获取原始资源内容,执行loaders,解析ast收集依赖等工作。

build()

代码语言:javascript
复制
// NormalModule.js
build(options, compilation, resolver, fs, callback) {
    //...
    this._source = null;
    this._ast = null;

    return this.doBuild(options, compilation, resolver, fs, err => {
        this._cachedSources.clear();
        // 如果配置了module.noParse,会校验阻止命中模块的parse,直接返回
        //...
       const result = this.parser.parse(this._ast || this._source.source(), { /*...*/ }, (err, result) => {/*...*/ });
    });
}

noParse的作用:

防止 webpack 解析任何匹配给定正则表达式的文件。被忽略的文件不应调用 import、require、define 或任何其他导入机制。当忽略大型库时,这可以提高构建性能。

主要步骤:

  1. 调用this.doBuild来应用loaders获取_source
  2. 获取完_source后调用parser.parse来解析_source对应的ast来收集依赖(Dependency)

doBuild()

代码语言:javascript
复制
const { getContext, runLoaders } = require("loader-runner");

doBuild(options, compilation, resolver, fs, callback) {
    // 创建loader的执行上下文,提供了部分api和属性共loader使用
    const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);

    // 调用loader-runner库执行各种loader,在loader-runner会被转为数组返回
    runLoaders({ /*...*/ }, (err, result) => {
            // result.result是最后一个loader返回的结果
            const source = result.result[0]; // resouce指向的资源的内容
            // 显然是sourceMap,在开发模式下帮忙debug时很有帮助
            const sourceMap = result.result.length >= 1 ? result.result[1] : null;
            // 可以用来提供 ast,比如有些loader会需要用到ast,可以在这里返回,
            // 这样在后面parse的时候可以复用该ast,避免多做一次工作
            const extraInfo = result.result.length >= 2 ? result.result[2] : null;
            
            // 创建source对象
            this._source = this.createSource(this.binary ? asBuffer(source) : asString(source),
                resourceBuffer,
                sourceMap
            );
            
            this._ast = (typeof extraInfo === "object" && extraInfo !== null && extraInfo.webpackAST !== undefined) 
                ? extraInfo.webpackAST : null;
            
            //...
        }
    );
}

显示调用createLoaderContext方法创建执行loader时的上下文(loader函数执行时的this指向,该上下文包含了很多API,如日志(getLogger),错误,警告(emitError、emitWarning)等收集,文件输出(emitFile)等),然后调用由loader-runner库提供的runLoaders方法,后面有单独一节会对loader-runner库进行源码分析。

看下Source类型

如果loader返回的source是Buffer类型的,则使用RawSource,如果loader返回了sourceMap并且webpack中提供了devtool即需要生成sourceMap,会返回SourceMapSource,否则会生成OrinalSource

其中 RawSourceSourceMapSouceOriginalSource可以理解为是平级的,都是在loaders转换完资源之后的初始Source

而初始Source ->ReplaceSource -> CachedSource 三者存在递进关系。最终输出的文件内容和原始内容是有很大出入的,被做了很多修改(替换、插入等操作),这些都是ReplaceSource实现的,而ReplaceSource内部有一个_source指向了初始SourceRepalceSouce提供修改能力,后面在介绍代码生成的时候看到这一部分。当应用修改生成最终Source时会再次升级为CachedSource,提供缓存能力。

doBuild的主要功能就是执行匹配的loaders生成初始Source,然后进入回调中调用this.parser.parse,该函数的主要工作就是收集各种Dependencty,这部分具体解析会单独作为一个小节讲解

小结

  1. runLoaders: 获取_source
  2. this.parser.parse: 收集依赖

总结

  • NormalModuleFactory.create
代码语言:txt
复制
- 调用normalResolver获取资源的`本地路径`
- 获取内联loaders并匹配(过滤)需要应用的loaders,调用loaderResovler来获取loader的`本地路径`
- 构造一个NormalModule实例NormalModule.build
代码语言:txt
复制
- doBuild 执行loaders(runLoaders)获取原始\_source
- 调用this.parser.parse收集`Dependency

引出三个大方向,下面每个点都会以一个单独小结来讲解

  1. resolver是如何解析路径的
  2. runLoaders的执行过程
  3. parser.parse如何收集依赖
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-11-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • NormalModuleFactory
  • resolver(): 收集各种模块构建过程中需要的信息
    • loader的类型、运行阶段、覆盖特性
      • 分析: 主要是loaders的匹配和本地路径解析
        • 第一部分: 获取内联loader
        • 第二部分: 获取内联loader本地路径和当前资源的本地路径
        • 第三部分:匹配非内联loaders
        • 第四部分: 获取非内联loaders本地路径
        • 第五部分: 获取parser、generator等核心对象
      • 三个核心对象的获取:parser | generator | resolver
        • parser: 通过解析ast收集依赖、generator: 最终产物的代码生成
        • resolver:路径解析器
      • 小结
      • NormalModule
        • build()
          • doBuild()
            • 小结
            • 总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档