本篇主要介绍 webpack 中的 externals 如何使用,顺带浅析一下 webpack 内部实现。
Externals 对于 Externals 就不过多描述了,蹭下热度,把这个问题交给 AI 小助手。
使用 Externals 项目目录 在 codesandbox 中预览项目,run build or build-externals
|--pages/ | |--A.js |--App.js |--index.js |--webpack.config.js
import React from "react" ;import { get } from "lodash" ;import { Button } from "antd" ;const A = ( ) => { const obj = { a : 1 }; return ( <div > <Button onClick ={() => { console.log(get(obj, "a"), "use loadsh-es"); }} > click </Button > </div > ); }; export default A;
{ externals : { lodash : "_" , }, }
<script src ="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js" > </script >
产物对比
[{"url":"https://i1.wp.com/gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl58mc6j30zk0kcnic.jpg","alt":""},{"url":"https://i1.wp.com/gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl3e932j30zk0kb7q5.jpg","alt":""}]
加载更多
[{"url":"https://i1.wp.com/gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl45hc3j30q4160qmy.jpg","alt":""},{"url":"https://i1.wp.com/gzw.sinaimg.cn/mw2000/0085UwQ9gy1heagl0fzd6j30rg19g4pw.jpg","alt":""}]
加载更多
API 介绍 { externalsType : 'var' externals : { "module-name" : "global-variable-name" } }
externalsType - 模块类型 模块类型,例如 amd、module(esm) 。默认 var,从 全局 当中取变量,具体看模块资源 export 的方式,其他类型可在 externals 中自定义。
const jq = $;jq ('.my-element' ).animate ();
externals externals 支持多种类型,这里简单介绍对象方式的配置
externals : { "module-name" : "global-variable-name" , "lodash" : "_" , }
key => module-name webpack 编译时会替换 引入模块 路径中匹配的 module-name,将其替换为 global-variable-name
value => global-variable-name 模块资源中暴露的变量名称,一般资源文件在结尾处都会导出,例如 lodash 编译后的 lodash.min.js 中(或 lodash.js,此类文件未经压缩方便查看),正常情况下 变量名称 和 包名一致。
CDN / 资源文件 externals 配置的是全局变量名称,所以需要我们在 index.html 文件中引入相关资源来确保变量存在,若资源有依赖性需要确保引入的顺序
<script src ="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js" > </script >
Features: externalsType = script
写法1: externalsType : 'script' ,externals : { "module-name" : ["library" , "global-variable-name" ], react : ['https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js' , 'React' ], 'react-dom' : ['https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.profiling.min.js' , 'ReactDOM' ], lodash : ['https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js' , '_' ], },
写法2: 与 string 语法类似,你可以使用${externalsType} ${libraryName}语法在数组的第一项中指定外部库类型,例如:
externals : { "module-name" : ["${externalsType} ${libraryName}" , "global-variable-name" ], react : ['script https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.production.min.js' , 'React' ], 'react-dom' : ['script https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.profiling.min.js' , 'ReactDOM' ], lodash : ['script https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js' , '_' ], }
lodash 只会在 visible 为 true 时引入资源。
const Test = React .lazy (() => import ('./Test' ));<React.Suspense fallback ={ <div > loading...</div > }> {this.state.visible && <Test /> } </React.Suspense >
把玩链接,run start-externals
externalsType = module 当 type 为 module 时,此时我们可以为所欲为了,直接使用 esm,不需要考虑各个模块的变量直接引入资源即可,确保资源为 esm 导出的即可。
externals : { react : 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm' , 'react-dom' : 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm' , lodash : "https://cdn.jsdelivr.net/npm/lodash@4.17.21/+esm" , } externalsType : "module" ,experiments : { outputModule : true , },
import * as __WEBPACK_EXTERNAL_MODULE_lodash__ from 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/+esm' ;const lodash = __WEBPACK_EXTERNAL_MODULE_jquery__['default' ];lodash.xx
浅析 externals 是如何替换模块的? 在配置了 Externals 时,webpack 会注册 ExternalsPlugin,插件中会在 webpack 提供的钩子内订阅事件(normalModuleFactory.hooks.factorize),并在 callback 中重定义了当前 module 的代码生成逻辑,跳过常规模块的 resolve 和 module create 等流程。
Webpack 大致流程
Source code ExternalsPlugin class ExternalsPlugin { apply (compiler): { compiler.hooks .compile .tap ("ExternalsPlugin" , ({ normalModuleFactory } ) => { new ExternalModuleFactoryPlugin (this .type , this .externals ).apply ( normalModuleFactory ); }); } }
ExternalModuleFactoryPlugin 在 normalModuleFactory.hooks.factorize 钩子中判断当前模块是否为扩展模块
class ExternalModuleFactoryPlugin { apply (normalModuleFactory ) { normalModuleFactory.hooks .factorize .tapAsync ( "ExternalModuleFactoryPlugin" , (resolveData, callback ) => { const dependency = resolveData.dependencies [0 ]; if (typeof externals === "object" ) { if ( Object .prototype .hasOwnProperty .call ( resolvedExternals, dependency.request ) ) { callback ( null , new ExternalModule ( externalConfig, type || globalType, dependency.request ) ); } } } ) } }
ExternalModule 重新定义当前 module 的构建和代码生成等(以 ExternalsType = script 为例)
class ExternalModule extends Module { codeGeneration ({ runtimeTemplate, moduleGraph, chunkGraph, runtime, concatenationScope } ) { const { request, externalType } = this ._getRequestAndExternalType (); switch (externalType) { default : { const sourceData = this ._getSourceData ( request, externalType, runtimeTemplate, moduleGraph, chunkGraph, runtime ); let sourceString = sourceData.expression ; if (sourceData.init ) sourceString = `${sourceData.init} \n${sourceString} ` ; let data = undefined ; if (sourceData.chunkInitFragments ) { data = new Map (); data.set ("chunkInitFragments" , sourceData.chunkInitFragments ); } const sources = new Map (); if (this .useSourceMap || this .useSimpleSourceMap ) { sources.set ( "javascript" , new OriginalSource (sourceString, this .identifier ()) ); } else { sources.set ("javascript" , new RawSource (sourceString)); } let runtimeRequirements = sourceData.runtimeRequirements ; if (!concatenationScope) { if (!runtimeRequirements) { runtimeRequirements = RUNTIME_REQUIREMENTS ; } else { const set = new Set (runtimeRequirements); set.add (RuntimeGlobals .module ); runtimeRequirements = set; } } return { sources, runtimeRequirements : runtimeRequirements || EMPTY_RUNTIME_REQUIREMENTS , data }; } } } build (options, compilation, resolver, fs, callback ) { this .buildMeta = { async : false , exportsType : undefined }; this .buildInfo = { strict : true , topLevelDeclarations : new Set (), module : compilation.outputOptions .module }; const { request, externalType } = this ._getRequestAndExternalType (); this .buildMeta .exportsType = "dynamic" ; let canMangle = false ; this .clearDependenciesAndBlocks (); switch (externalType) { case "script" : case "promise" : this .buildMeta .async = true ; break ; } this .addDependency (new StaticExportsDependency (true , canMangle)); callback (); } }
在构建时根据 Type 修改对应 module 实例 build 属性,例如 script 为异步加载所以添加 buildMeta.async 为 true,编译时则会对该模块在引入部分代码做修改。 在生成阶段(seal)针对不同 Type 生成不同的运行时代码
type 为 script 时的 Module 代码生成逻辑。
const getSourceForScriptExternal = (urlAndGlobal, runtimeTemplate ) => { if (typeof urlAndGlobal === "string" ) { urlAndGlobal = extractUrlAndGlobal (urlAndGlobal); } const url = urlAndGlobal[0 ]; const globalName = urlAndGlobal[1 ]; return { init : "var __webpack_error__ = new Error();" , expression : `new Promise(${runtimeTemplate.basicFunction( "resolve, reject" , [ `if(typeof ${globalName} !== "undefined") return resolve();` , `${RuntimeGlobals.loadScript} (${JSON .stringify( url )} , ${runtimeTemplate.basicFunction("event" , [ `if(typeof ${globalName} !== "undefined") return resolve();` , "var errorType = event && (event.type === 'load' ? 'missing' : event.type);" , "var realSrc = event && event.target && event.target.src;" , "__webpack_error__.message = 'Loading script failed.\\n(' + errorType + ': ' + realSrc + ')';" , "__webpack_error__.name = 'ScriptExternalLoadError';" , "__webpack_error__.type = errorType;" , "__webpack_error__.request = realSrc;" , "reject(__webpack_error__);" ])} , ${JSON .stringify(globalName)} );` ] )} ).then(${runtimeTemplate.returningFunction( `${globalName} ${propertyAccess(urlAndGlobal, 2 )} ` )} )` , runtimeRequirements : RUNTIME_REQUIREMENTS_FOR_SCRIPT }; };
var __webpack_error__ = new Error ();module .exports = new Promise ((resolve, reject ) => { if (typeof _ !== "undefined" ) return resolve (); __webpack_require__.l ("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" , (event ) => { if (typeof _ !== "undefined" ) return resolve (); var errorType = event && (event.type === 'load' ? 'missing' : event.type ); var realSrc = event && event.target && event.target .src ; __webpack_error__.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')' ; __webpack_error__.name = 'ScriptExternalLoadError' ; __webpack_error__.type = errorType; __webpack_error__.request = realSrc; reject (__webpack_error__); }, "_" ); }).then (() => (_));
const getSourceForDefaultCase = (optional, request, runtimeTemplate ) => { const variableName = request[0 ]; const objectLookup = propertyAccess (request, 1 ); return { init : optional ? checkExternalVariable (variableName, request.join ("." ), runtimeTemplate) : undefined , expression : `${variableName} ${objectLookup} ` }; };
Module id ((module ) => { module .exports = _; })
NormalModule
构建流程
调用 handleModuleCreate
,根据文件类型构建module
子类
调用 loader-runner 仓库的runLoaders
转译module
内容,通常是从各类资源类型转译为 JavaScript 文本
调用 acorn 将 JS 文本解析为AST
遍历 AST,触发各种钩子 a. 在HarmonyExportDependencyParserPlugin
插件监听exportImportSpecifier
钩子,解读 JS 文本对应的资源依赖 b. 调用module
对象的addDependency
将依赖对象加入到module
依赖列表中
AST 遍历完毕后,调用module.handleParseResult
处理模块依赖
对于module
新增的依赖,调用handleModuleCreate
,控制流回到第一步
所有依赖都解析完毕后,构建阶段结束
class NormalModule extends Module { _doBuild (..., callback ) { runLoaders (...); } build (options, compilation, resolver, fs, callback ) { return this ._doBuild (...) } }
默认情况下,模块会在 NormalModuleFactory 中的 factorize.tapAsync 来创建,由于其定义了 stage: 100,总会在其他事件完成后执行,若其他订阅事件的 callback 带参返回则会终止发布事件(tapable.tapAsync ),跳过常规模块创建流程(NormalModule)。
class NormalModuleFactory extends ModuleFactory { constructor ( ) { this .hooks .factorize .tapAsync ( { name : "NormalModuleFactory" , stage : 100 }, () => { this .hooks .resolve .callAsync (..., () => { createdModule = new NormalModule ( (createData) ); }) } ) } create ( ) { this .hooks .factorize .callAsync ( ..., (err, module ) => { const factoryResult = { module , }; callback (null , factoryResult); }) } }
class Compilation { _factorizeModule ({ factory } ) { factory.create ({...}, (err, result ) => { callback (null , factoryResult ? result : result.module ); }) } }
externals 和 alias 之间有影响吗? ‒ 不会,从执行顺序上看 externals 在 normalModuleFactory.hooks.factorize 中执行,时机会早于 resolve,此时 alias 后的路径在 resolveData.creatData(afterResolve) 中,将在创建 module 时将其带入。 ‒ 在创建 module 之前的阶段(resolve 等)中对 模块 路径(request)的判断应遵循代码,之后(after afterResolve)遵循模块真实路径。
Resolve 阶段 import lodash from "我被Alias了" ;console .log (lodash);
resolve : { alias : { 我被Alias 了: 'lodash' , }, },
Resolve 后 class AliasTest { apply (compiler ) { normalModuleFactory.hooks .afterResolve .tap ('AliasTest-afterResolve' , (resolveData ) => { if (resolveData.request .includes ('Alias' )) { console .log (`${resolveData.request} (request) | -- afterResolve.resolveData -- | ${resolveData.context} (context)` ); console .log (resolveData.createData ?.request ); console .log ('----- afterResolve --- \n' ); } }) normalModuleFactory.hooks .createModule .tap ('AliasTest-createModule' , (createData, resolveData ) => { if (resolveData.request .includes ('Alias' )) { console .log (`${createData.request} (request) | -- createModule.createData -- | ${createData.context} (context)` ); console .log (`${resolveData.request} (request) | -- createModule.resolveData -- | ${resolveData.context} (context)` ); console .log ('----- createModule ---- \n' ); } }) }); } }
AliasPlugin.js
Resolve class NormalModuleFactory extends ModuleFactory { constructor ( ) { this .hooks .resolve .tapAsync (..., (resolveData ) => { Object .assign (resolveData.createData , { request : stringifyLoadersAndResource ( allLoaders, resourceData.resource ), userRequest, rawRequest : request, }); }) } }
Module class NormalModuleFactory extends ModuleFactory { constructor ( ) { this .hooks .afterResolve .callAsync (resolveData, (err, result ) => { const createData = resolveData.createData ; this .hooks .createModule .callAsync ( createData, resolveData, (err, createdModule ) => { if (!createdModule) { createdModule = new NormalModule (createData); } createdModule = this .hooks .module .call ( createdModule, createData, resolveData ); return callback (null , createdModule); } ); }); } }
基本上获取路径都会去取 request,例如 IgnorePlugin 插件,在 beforeResolve 阶段处理。此时的路径为代码内路径(例如:”我被Alias了”)。 怎么判断插件是拿什么匹配的?一般涉及路径都接收多种类型,例如函数,console 即可。
class IgnorePlugin { checkIgnore (resolveData ) { if ( this .options .checkResource (resolveData.request , resolveData.context ) ) { return false ; } } apply (compiler ) { compiler.hooks .normalModuleFactory .tap ("IgnorePlugin" , nmf => { nmf.hooks .beforeResolve .tap ("IgnorePlugin" , this .checkIgnore ); }); } }
new webpack.IgnorePlugin ({ resourceRegExp : /我被Alias了/ , })
https://mp.weixin.qq.com/s/SbJNbSVzSPSKBe2YStn2Zw https://mp.weixin.qq.com/s?__biz=Mzg3OTYwMjcxMA==&mid=2247484088&idx=1&sn=41bf509a72f2cbcca1521747bf5e28f4&chksm=cf00bfc1f87736d76681e1e1db39deb1fa3121686bcd45a42709fa58aaba6d7ffba6d7fb4975&scene=178&cur_album_id=1856066636768722949#rd https://mp.weixin.qq.com/s/tXkGx6Ckt9ucT2o8tNM-8w