从 npm run build 学习 webpack(中)

261 阅读6分钟

前情回顾

在上篇 npm run build(上) 中, 介绍了

  • npm run build,找执行命令 webpack
  • 通过 package.jsonbin字段获取到 webpack 命令映射的执行文件 webpack.js
  • webpack.js文件中执行 runCli()方法,用来载入 webpack-cli 命令的映射文件 cli.js
  • cli.js执行 runCLI()方法,实例化 new WebpackCli(),并执行run()方法。
    • 实例化
      • this.color = createColors()返回一个对象,包含各种方法,如red()bgRed()等,用来在终端生成彩色文字。
      • this.logger = getLogger()自定义warn、error、info、success的log方法,增加“[webpack-cli]”前缀。
      • this.program = programcommander实例挂载到this.program上。初始化program名字,配置错误写入时使用this.logger.error,并配置错误输出格式
    • run()方法
      • 利用this.program.action方法来响应终端命令。如果没有传入命令,默认命令为build
      • 然后执行loadCommanByName()方法,如果判断是build命令,执行makeCommand()方法。

继续学习

makeCommand()方法

上一节中学习了loadCommanByName()中,执行了makeCommand()方法。那么该方法如何使用呢?

参数:接受三个参数

  1. commandOptions:一些命令的配置对象,其中包括name,usage,description,alias,dependecies字段。
  2. options:注册option参数
  3. actioncommander.action回调函数

作用

  1. this.program.commands中包含了所有已注册的命令,通过commandOptions中name,alias字段,判断命令是否注册过。注册过不重复注册

  2. 没有注册,初始化注册。注册描述,用法,别名等 image.png

  3. 检测commandOptionsdependencies依赖是否已经安装。

    • 安装了:跳过
    • 没安装,并且是查询该命令的help,记录未全部安装,并跳过。(查询帮助不需要安装依赖)
    • 没安装
      • 依赖webpack但是WEBPACK_PACKAGE_IS_CUSTOM提供自定义依赖,跳过
      • 依赖dev-server但是WEBPACK_DEV_SERVER_PACKAGE_IS_CUSTOM提供自定义依赖,跳过
      • 否则执行doInstall()安装

checkPackageExists() 方法来判断是否安装

  • 如果是 yarn pnp 管理包,直接默认已安装。
  • 否则从当前文件的目录node_modules开始向上逐级查找,直到顶层"/"
  • 还没找到,再从全局包安装路径require("module").globalPaths中查找

image.png

doInstall() 方法逻辑

  • 参数:
    • packageName:依赖的 npm 包名
    • options.preMessage:预处理提示信息,这里提示使用某命令,需要安装对应哪个依赖
  • 逻辑
    • 获取本地包管理工具(npm,pnpm,yarn)。
      • 首先通过查询xx-lock.json文件返回对应包管理工具
      • 否则利用 cross-spwan sync() 方法,启动一个进程执行 xx -version 命令,检测管理工具
    • 执行option.preMessage()提示将安装依赖包
    • 利用readline.createInterface创建可读流实例,调用question()方法询问是否安装依赖。
      • 用户输入“Y”或者“yes”,使用sync()方法安装
      • 否则退出
  1. 接下来处理第二个参数options
  • 如果是函数类型options
    • 在请求help但是有依赖没有安装完成。对command.description修改,提示要安装哪些依赖
    • 否则获取options执行结果,赋值给options
  • 遍历options,利用makeOption()函数,将options每一项处理成标准的配置,new Option()创建option对象,然后注册command.addOption

image.png 5. 将第三个参数,作为command.action的回调函数。

回顾一下makeCommand通过program.commands获取全部的注册命令,遍历全部命令查找aliasname是否有与传入的第一个参数commandOptions中的aliasname相同,来判断是否注册过。没有注册过,利用commandOptions参数中的值,把aliasdescrptionnameusage都初始化。检测commandOptions.dependeces依赖是否安装,检测方法就是通过node_modeules逐层查找,没有再到全局安装路径下查找。没安装就利用cross-spwansync方法,执行安装命令。然后再把第二个参数的options中的每一项格式化后,绑定到command上。最后把第三个参数绑定到command.action上。

执行 build 命令时,在 loadCommanByName() 中是如何执行 makeCommand() 的?

在了解了makeCommand()就是在初始化注册一个命令,我们看看在buildloadCommandByName()中给makeCommand()传了什么参数

image.png

参数1(commandOptions):buildCommandOptions

image.png

参数2(options):传入的是一个函数。通过loadWeabpack()加载webpack。然后执行getBuiltInOptions返回options

  1. loadWeabpack()函数加载weabpack loadWeabpack()使用tryRequireThenImport('weabpack', true)加载weabpack

    而在tryRequireThenImport()中根据moduleType类型:

    • 如果是commonjs使用require()加载。
    • 如果是esm使用动态加载import()
    • 如果都不是,即unknown优先用require()报错了,在catch中使用import()加载
    • 最后在判断是不是通过export.default导出的,是就是把export.default作为结果返回,否则直接返回结果

image.png

  1. getBuiltInOptions()函数获取初始options

通过this.weabpack.cli.getArguments()获取到初始options,整理成与builtInFlags数据结构相同的类型。然后与builtInFlags合并数组,作为结果返回。

builtInFlags定义的一个常量,数据结构如下:

interface WebpackCLIBuiltInFlag {
  name: string;
  alias?: string;
  type?: (
    value: string,
    previous: Record<string, BasicPrimitive | object>,
  ) => Record<string, BasicPrimitive | object>;
  configs?: Partial<FlagConfig>[];
  negative?: boolean;
  multiple?: boolean;
  valueName?: string;
  description: string;
  describe?: string;
  negatedDescription?: string;
  defaultValue?: string;
  helpLevel: "minimum" | "verbose";
}

image.png

  1. this.weabpack.cli.getArguments()结果是什么?

首先要找到cli.getArguments,在加载模块时是根据package.jsonmain字段来确定入口文件。指向了lib/index.js文件。

image.png

lib/index.js文件中返回一个对象,其中cli字段,是返回的cli.js模块,因此getArguments()也就在这个文件中

image.png

getArguments()主要是利用traverse()../schemas/weabpackOptions.json整理并收集到flag对象中。最后遍历flag再处理一遍数据结构。最终返回flag

image.png

  • traverse()根据 type 字段进行递归
    • object类型,properties字段是一个对象,遍历每一项递归
    • array类型,如果items是数组,遍历递归每一项。否则将整个items递归一次
    • 其他类型如果有anyOfallOfoneOf遍历每一项递归
    • 递归时会利用addFlag()方法,将含有enum字段,或者type字段是stringnumberboolean其中之一,或者instanceof字段是regexp。收集到flag对象中。
      • flag对象以路径为key,值如下:
      flags[name] = {
        configs: [],
        description: undefined,
        simpleType: undefined,
        multiple: undefined
      };
      
      • 其中configs会将本次的argConfig放入其中。argConfig结构如下
      // 将有 enum字段,instanceof 是RegExp,或 type是boolean,string,number返回对象{type:xxx,value?:xx}。其他返回 undefined
      const argConfigBase = schemaToArgumentConfig(path[0].schema);
      const argConfig = {
      	...argConfigBase,
      	multiple,
      	// 优先返回 schema.cli.description,其次 schema.description
      	description: getDescription(path),
        // path第一个就是当前的对象
      	path: path[0].path
      };
      
  • for循环,为flag中每项初始化时,descriptionsimpleTypemultiple三个字段赋值
    • description:把config中每一项description合并到一起
    • simpleTypeconfig中所有都是相同type则返回type,否则返回string
    • multipleconfig有一项multipletrue,就是true

image.png

总结下,第二个参数到底做了什么?首先利用loadWeabpack()加载weabpackloadWeabpack()利用tryRequireThenImport()兼容commonjsesm不同模块化的加载方式。然后执行getBuiltInOptions()获取build options,在getBuiltInOptions()中,将初始builtInFlagsthis.weabpack.cli.getArguments()结果合并。最终返回一个如下的数组对象。

image.png

image.png

参数3(action):触发命令的回调函数

前面都是build命令的前菜,接下来就来到了build的主要命令。这个篇幅比较长,留到下一篇详细介绍