分享web开发知识

注册/登录|最近发布|今日推荐

主页 IT知识网页技术软件开发前端开发代码编程运营维护技术分享教程案例
当前位置:首页 > 网页技术

.4-浅析webpack源码之convert-argv模块

发布时间:2023-09-06 01:30责任编辑:顾先生关键词:webpack

  上一节看了一眼预编译的总体代码,这一节分析convert-argv模块是如何解析入口函数并返回options。 

生成默认配置文件名数组

module.exports = function(yargs, argv, convertOptions) { ???var options = []; ????// webapck -d ???// 生成map映射文件,告知模块打包地点 ???if(argv.d) { /* ... */ } ???// webpack -p ???// 压缩文件 ???if(argv.p) { /* ... */ } ???// 配置文件加载标记 ???var configFileLoaded = false; ???// 配置文件加载后的载体 ???var configFiles = []; ???// 排序 ???var extensions = Object.keys(interpret.extensions).sort(function(a, b) { ???????return a === ".js" ? -1 : b === ".js" ? 1 : a.length - b.length; ???}); ???// 指定所有默认配置文件名 ???var defaultConfigFiles = ["webpack.config", "webpackfile"].map(function(filename) { ???????return extensions.map(function(ext) { ???????????return { ???????????????path: path.resolve(filename + ext), ???????????????ext: ext ???????????}; ???????}); ???}).reduce(function(a, i) { ???????return a.concat(i); ???}, []); ???????// more code...}

  函数内部,首先判断了argv.d与argv.p属性是否存在,这个属性来源于命令行参数,即webpack -d -p,测试如图:

  

  

  因为懒得加,所以直接跳过,进入到第二阶段,生成默认配置文件名数组。

  这里引入了一个小的模块interpret,调用Object.keys(interpret.extensions)返回一系列文件扩展名的数组,如图:

  

  由于获取到的数组为乱序,所以这里首先进行排序,规则为.js放在第一位,后面的按长度从小到大,结果是这样:

  

  

  接下来是两个map与一个reduce的调用,首先两个map会返回一个数组,包含两个对象数组,对象包含path、ext两个属性,path代表路径+文件名+后缀,ext就是后缀,调用map后会得到如下数组 (截取部分):

  

  

  最后调用reduce方法将二维数组扁平化为一维数组,图就不截了。

  

定义配置文件路径与后缀

  

  有了默认列表,第二步就是尝试获取对应的配置文件:

var i;// 从命令行读取--config// argv.config => config.jsif(argv.config) { ???var getConfigExtension = function getConfigExtension(configPath) { ???????for(i = extensions.length - 1; i >= 0; i--) { ???????????var tmpExt = extensions[i]; ???????????if(configPath.indexOf(tmpExt, configPath.length - tmpExt.length) > -1) { ???????????????return tmpExt; ???????????} ???????} ???????return path.extname(configPath); ???}; ???var mapConfigArg = function mapConfigArg(configArg) { ???????// 获取文件绝对路径 ???????var resolvedPath = path.resolve(configArg); ???????// 获取文件后缀 ???????var extension = getConfigExtension(resolvedPath); ???????return { ???????????path: resolvedPath, ???????????ext: extension ???????}; ???}; ???// 包装成数组 统一处理单、多配置文件情况 ???var configArgList = Array.isArray(argv.config) ? argv.config : [argv.config]; ???configFiles = configArgList.map(mapConfigArg);}// 如果未指定配置文件 尝试匹配默认文件名else { ???for(i = 0; i < defaultConfigFiles.length; i++) { ???????var webpackConfig = defaultConfigFiles[i].path; ???????// 检测路径中是否存在对应文件 ???????if(fs.existsSync(webpackConfig)) { ???????????configFiles.push({ ???????????????path: webpackConfig, ???????????????ext: defaultConfigFiles[i].ext ???????????}); ???????????break; ???????} ???}}

  这里的代码比较简单,如果调用了--config自定义配置文件,该指令后面的会被当成参数传给argv.config。

  存在argv.config则会对文件名与合法后缀数组进行匹配,检测出配置文件的后缀包装成对象返回。

  如果不指定配置文件,会进入else代码段开始遍历默认配置文件数组,fs.existsSync检测当前路径是否存在该文件,有就当成配置文件包装返回。

获取配置文件输出模块并做简单处理

  

  上一步只是代表接确定了配置文件的绝对路径,这个文件并不一定是有效且存在的。

  这一步会获取到配置文件的输出并简单处理:

if(configFiles.length > 0) { ???var registerCompiler = function registerCompiler(moduleDescriptor) { ???????// ... ???}; ???var requireConfig = function requireConfig(configPath) { ???????// 获取到modules.exports输出的内容 ???????var options = require(configPath); ???????// 二次处理 ???????options = prepareOptions(options, argv); ???????return options; ???}; ???// 本例中configFiles => [{path:‘d:\\workspace\\node_modules\\webpack\\bin\\config.js‘,ext:‘.js‘}] ???configFiles.forEach(function(file) { ???????// interpret.extensions[.js]为null ???????// 这里直接跳出 ???????registerCompiler(interpret.extensions[file.ext]); ???????// 这里的options是convert-argv.js开头声明的数组 ???????options.push(requireConfig(file.path)); ???}); ???// 代表配置文件成功加载 ???configFileLoaded = true;}

  这里的处理情况有两个:

1、根据后缀名二次处理

2、将路径传进一个prepareOptions模块处理

  这个模块内容十分简单,可以看一下:

"use strict";module.exports = function prepareOptions(options, argv) { ???argv = argv || {}; ???// 判断是否通过export default输出 ???options = handleExport(options); ???// 非数组 ???if(Array.isArray(options)) { ???????options = options.map(_options => handleFunction(_options, argv)); ???} else { ???????// 当options为函数时 ???????options = handleFunction(options, argv); ???} ???return options;};function handleExport(options) { ???const isES6DefaultExported = ( ???????typeof options === "object" && options !== null && typeof options.default !== "undefined" ???); ???options = isES6DefaultExported ? options.default : options; ???return options;}function handleFunction(options, argv) { ???if(typeof options === "function") { ???????options = options(argv.env, argv); ???} ???return options;}

  这里针对多配置(数组)与单配置进行了处理,判断了模块输出的方式(ES6、CMD)以及输出的类型(对象、函数),最后返回处理后的配置对象并标记配置文件已被加载。

终极处理函数

  

  接下来就是最后一个阶段:

if(!configFileLoaded) { ???return processConfiguredOptions({});} else if(options.length === 1) { ???return processConfiguredOptions(options[0]);} else { ???return processConfiguredOptions(options);}function processConfiguredOptions(options) { ???// 非法输出类型 ???if(options === null || typeof options !== "object") { ???????console.error("Config did not export an object or a function returning an object."); ???????process.exit(-1); // eslint-disable-line ???} ???// promise检测 ???if(typeof options.then === "function") { ???????return options.then(processConfiguredOptions); ???} ???// export default检测 ???if(typeof options === "object" && typeof options.default === "object") { ???????return processConfiguredOptions(options.default); ???} ???// 数组 ???if(Array.isArray(options) && argv["config-name"]) { /* ... */ } ???// 数组 ???if(Array.isArray(options)) { /* ... */ } ????else { ???????// 单配置 ???????processOptions(options); ???} ???if(argv.context) { ???????options.context = path.resolve(argv.context); ???} ???// 设置默认上下文为进程当前绝对路径 ???if(!options.context) { ???????options.context = process.cwd(); ???} ???// 跳过 ???if(argv.watch) { /* ... */ } ???if(argv["watch-aggregate-timeout"]) { /* ... */ } ???if(typeof argv["watch-poll"] !== "undefined") { /* ... */ } ???if(argv["watch-stdin"]) { /* ... */ } ???return options;}

  这里根据不同的情况传入空对象、单配置对象、多配置数组。

  在函数的开头又再次检测了合法性、promise、ES6模块输出方法,由于本例只有一个配置对象,所以直接进processOptions函数,这个函数很长,简化后源码如下:

function processOptions(options) { ???// 是否存在output.filename ???var noOutputFilenameDefined = !options.output || !options.output.filename; ???function ifArg(name, fn, init, finalize) { /* ... */ } ???function ifArgPair(name, fn, init, finalize) { /* ... */ } ???function ifBooleanArg(name, fn) { /* ... */ } ???function mapArgToBoolean(name, optionName) { /* ... */ } ???function loadPlugin(name) { /* ... */ } ???function ensureObject(parent, name) { /* ... */ } ???function ensureArray(parent, name) { /* ... */ }function bindRules(arg) { /* ... */ } ???bindRules("module-bind"); ???bindRules("module-bind-pre"); ???bindRules("module-bind-post"); ???var defineObject; ???// 中间穿插大量ifArgPair、ifArg、ifBooleanArg等 ???mapArgToBoolean("cache"); ???function processResolveAlias(arg, key) { /* ... */ } ???processResolveAlias("resolve-alias", "resolve"); ???processResolveAlias("resolve-loader-alias", "resolveLoader"); ???mapArgToBoolean("bail"); ???mapArgToBoolean("profile"); ???// 无输出文件名配置 ???if (noOutputFilenameDefined) { /* ... */ } ???// 处理命令参数 ???if (argv._.length > 0) { /* ... */ } ???// 无入口文件配置 ???if (!options.entry) { /* ... */ }}

  首先看一下里面的工具函数:

1、ifArgpair、ifArg

function ifArgPair(name, fn, init, finalize) { ???// 直接进入ifArg函数 ???// content => argv[name]的数组元素 ???// idx => 索引 ???ifArg(name, function(content, idx) { ???????// 字符"="索引 ???????var i = content.indexOf("="); ???????if (i < 0) { ???????????// 无等号的字符 ???????????return fn(null, content, idx); ???????} else { ???????????// 传入=号左边与右边的字符 ???????????return fn(content.substr(0, i), content.substr(i + 1), idx); ???????} ???}, init, finalize);}// init => 构造函数// finalize => 析构函数function ifArg(name, fn, init, finalize) { ???if (Array.isArray(argv[name])) { ???????if (init) { init(); } ???????argv[name].forEach(fn); ???????if (finalize) { finalize(); } ???} else if (typeof argv[name] !== "undefined" && argv[name] !== null) { ???????if (init) { init(); } ???????fn(argv[name], -1); ???????if (finalize) { finalize(); } ???}}

2、ifBooleanArg

// 当argv[name]不为false时才执行fn函数function ifBooleanArg(name, fn) { ???ifArg(name, function(bool) { ???????if (bool) { fn(); } ???});}

3、mapArgToBoolean

function mapArgToBoolean(name, optionName) { ???ifArg(name, function(bool) { ???????// argv[name]为true时 ???????if (bool === true) ???????????options[optionName || name] = true; ???????// argv[name]为false时 ???????else if (bool === false) ???????????options[optionName || name] = false; ???});}

4、ensureObject、ensureArray

// 保证指定属性为对象function ensureObject(parent, name) { ???if (typeof parent[name] !== "object" || parent[name] === null) { ???????parent[name] = {}; ???}}// 保证指定属性为数组function ensureArray(parent, name) { ???if (!Array.isArray(parent[name])) { ???????parent[name] = []; ???}}

  剩下的bindRules负责处理rules,loadPlugin负责加载插件,processResolveAlias处理别名,单独分节讲吧,不然这一节要长到令人发指。

  因为配置文件只有entry和out,所以基本上大部分属性都是undefined和false,在ifArg的if与else if中log能进入的name,发现只有4个:

  

  对应的执行函数分别为:

// output参数ifBooleanArg("output-pathinfo", function() { ???ensureObject(options, "output"); ???options.output.pathinfo = true;});// 热重载ifBooleanArg("hot", function() { ???ensureArray(options, "plugins"); ???var HotModuleReplacementPlugin = require("../lib/HotModuleReplacementPlugin"); ???options.plugins.push(new HotModuleReplacementPlugin());});// 默认加载loaderOptionsPlugin插件ifBooleanArg("debug", function() { ???ensureArray(options, "plugins"); ???var LoaderOptionsPlugin = require("../lib/LoaderOptionsPlugin"); ???options.plugins.push(new LoaderOptionsPlugin({ ???????debug: true ???}));});// 默认加载代码压缩插件ifBooleanArg("optimize-minimize", function() { ???ensureArray(options, "plugins"); ???var UglifyJsPlugin = require("../lib/optimize/UglifyJsPlugin"); ???var LoaderOptionsPlugin = require("../lib/LoaderOptionsPlugin"); ???options.plugins.push(new UglifyJsPlugin({ ???????// devtool参数 ???????sourceMap: options.devtool && (options.devtool.indexOf("sourcemap") >= 0 || options.devtool.indexOf("source-map") >= 0) ???})); ???options.plugins.push(new LoaderOptionsPlugin({ ???????minimize: true ???}));});

  可以看到,webpack会默认加载3个插件,一个是处理loader的LoaderOptionsPlugin插件,一个是代码压缩插件UglifyJsPlugin,还有一个就是热重载插件。3个插件的加载别的章节讲,output-pathinfo在之前的config-yargs.js中被定义,默认值是false,而ifBooleanArg在传入值为false时不会执行回调。

  最后剩下3个代码块:

 ???// 无输出文件名配置 ???if (noOutputFilenameDefined) { /* ... */ } ???// 处理命令参数 ???if (argv._.length > 0) { /* ... */ } ???// 无入口文件配置 ???if (!options.entry) { /* ... */ }

  由于指令没有传任何参数,所以argv._是一个空数组,中间的可以跳过。

  所以只需要看其余两个,首先看简单的无入口文件配置的情况,即配置文件没有entry属性:

if (!options.entry) { ???// 存在配置文件 但是没有入口函数 ???if (configFileLoaded) { ???????console.error("Configuration file found but no entry configured."); ???} ???// 未找到配置文件 ????else { ???????console.error("No configuration file found and no entry configured via CLI option."); ???????console.error("When using the CLI you need to provide at least two arguments: entry and output."); ???????console.error("A configuration file could be named ‘webpack.config.js‘ in the current directory."); ???} ???console.error("Use --help to display the CLI options."); ???// 退出进程 ???process.exit(-1); // eslint-disable-line}

  可以看出这是必传参数,根据是否找到对应的配置文件报不同的错误。

  另一种情况是不存在ouput或output.filename属性:

if (noOutputFilenameDefined) { ???ensureObject(options, "output"); ???// convertOptions来源于第三个参数 ???// module.exports = function(yargs, argv, convertOptions) {...} ???// var options = require("./convert-argv")(yargs, argv) ???// 只传了两个参数 所以跳过 ???if (convertOptions && convertOptions.outputFilename) { ???????options.output.path = path.resolve(path.dirname(convertOptions.outputFilename)); ???????options.output.filename = path.basename(convertOptions.outputFilename); ???} ????// 尝试从命令参数获取output.filename ???// 命令的最后一个参数会被当成入口文件名 ???else if (argv._.length > 0) { ???????options.output.filename = argv._.pop(); ???????options.output.path = path.resolve(path.dirname(options.output.filename)); ???????options.output.filename = path.basename(options.output.filename); ???} ???// 老套的报错 不解释 ????else if (configFileLoaded) { ???????throw new Error("‘output.filename‘ is required, either in config file or as --output-filename"); ???} else { ???????console.error("No configuration file found and no output filename configured via CLI option."); ???????console.error("A configuration file could be named ‘webpack.config.js‘ in the current directory."); ???????console.error("Use --help to display the CLI options."); ???????process.exit(-1); // eslint-disable-line ???}}

  可以看出,output.filename虽然是必须的,但是不一定需要在配置文件中,有两个方式可以传入。

  一个是作为convert-argv.js的第三个参数传入,由于在之前解析时默认只传了两个,这里会跳过,暂时不清楚传入地点。

  另外一个是在命令中传入,测试代码:

  

  

  至此,该模块全部解析完毕,输出options如图所示:

  

  真是累……

.4-浅析webpack源码之convert-argv模块

原文地址:http://www.cnblogs.com/QH-Jimmy/p/8023612.html

知识推荐

我的编程学习网——分享web前端后端开发技术知识。 垃圾信息处理邮箱 tousu563@163.com 网站地图
icp备案号 闽ICP备2023006418号-8 不良信息举报平台 互联网安全管理备案 Copyright 2023 www.wodecom.cn All Rights Reserved