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

2022-11-16 17:28:31 浏览数 (1)

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

  • 1. 从构建前后产物对比分析webpack做了些什么?
  • 2. webpack构建的基石: tapable@1.1.3源码分析
  • 3. webpack构建整体流程的组织:webpack -> Compiler -> Compilation
  • 4. 创建模块实例,为模块解析准备
  • 5. 路径解析:enhanced-resolve@4.5.0源码分析
  • 6. 模块构建之loader执行:loader-runner@2.4.0源码分析
  • 7. 模块构建之解析_source获取dependencies
  • 8. 从dependency graph 到 chunk graph
  • 9. 从chunk到最终的文件内容到最后的文件输出?
  • 10. webpack中涉及了哪些设计模式呢?

上一节说到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如何收集依赖

0 人点赞