1.umi4发布,有哪些技术亮点?
2.react umi+dva开发基本流程(1)
3.Umi 4 RC 发布
4.UmiJS 学习笔记 1 - 快速上手
5.umi3源码解析之核心Service类初始化
umi4发布,有哪些技术亮点?
前言
在业务调整后,我负责的多个中后台系统的研发范式不统一,导致上手成本高,资源调配困难。为了解决这个问题,我们考虑采用应用框架实现范式的侧面积源码统一下调。近期,我们关注了开源应用框架,包括umi,发现其4.0版本的发布带来了技术亮点。
原文链接: umijs.org/docs/introduc...
在深入研究后,我们发现了一个有趣的功能:import all from 'umi'。这意味着所有能力都可从umi中导入获取,并支持通过插件扩展此功能。接下来,我们将通过源码分析来探讨如何实现这一功能。
源码分析 umi从 demo 说起
首先,我们以创建的Umi项目为例进行分析。在页面中通过import { useModel } from '@umijs/max' 来使用useModel方法。这让我们追踪到 @umijs/max 实际上导出了umi包的全部方法。
继续深入,我们查看了Umi的入口文件,发现其未实际导出useModel方法。由此推测,@umijs/max可能通过某种机制在构建时注入了这些方法。
webpack构建时
根据Umi官方说明,@umijs/max是一个插件集,内置了数据流管理插件。通过配置文件.umirc.ts证实了这一点。进一步分析,我们发现构建过程中的关键在于@umijs/max基于Umi插件协议修改了webpack配置,引入了alias别名@@/exports。
通过进一步查看源码,android 7.0 源码下载我们发现@@/exports最终指向了/.umi/exports文件,这是Umi项目中生成的临时文件。
模板
在Umi项目中,.umi目录下存放的是临时文件,那么这个/.umi/exports文件是如何生成的呢?答案在packages/preset-umi/src/features/tmpFiles/tmpFiles.ts中。这里向插件register了一个临时文件生产方法hooks,供applyPlugins使用,实现项目dev或build时的文件生成。
总结,Umi通过上述机制实现了"import all from '@umijs/max'"。
接着,我们分析了类似实现的vitekit。同样地,从官方使用案例入手,我们发现vitekit利用了依赖包的方式,而非alias别名。在.vitekit-package中间接引用了@vitekit/framework-vue。
vite构建时
vitekit的实现依赖于构建时自动引入依赖包,这得益于其源码/vitekit/src/node.ts中对构建配置的处理。
综上,umi和vitekit采用相似的实现思路,通过构建时插件协议或依赖包管理来支持扩展导入功能。
手写实践
为了验证理解,我们基于上述原理,编写了一个简单的demo。首先,我们创建了临时文件exports,并修改了webpack配置引入alias别名。接着,准备了一个测试demo,展示了函数的导入与使用。最后,代刷源码4.7执行pnpm start启动项目,验证了临时文件的生成与页面输出,确保实现符合预期。
总结,通过分析和实践,我们深入理解了Umi和vitekit实现"import all"功能的原理,以及构建时插件和依赖包管理在其中的作用。对于想要理解和实现类似功能的开发者,本文提供的方法和步骤提供了参考。
react umi+dva开发基本流程(1)
认识UMI,一个企业级的React应用框架,官网地址:umijs.org/zh/guide/.它以路由为核心,支持类Next.js的约定式路由,以及各种进阶功能,如路由级别的按需加载。同时,UMI配备了完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求,已拥有超过个插件。
作为蚂蚁金服的底层前端框架,UMI已服务于超过个应用,包括Java、Node、H5无线、离线(Hybrid)应用、纯前端资产应用、CMS应用等。UMI旨在为内部及外部用户提供高效、稳定的前端解决方案。
使用UMI搭建项目的步骤如下:
1. 全局安装环境
2. 构建项目并创建src目录
3. 创建页面或路由组件
4. 运行项目
5. 构建生产环境
这些步骤涵盖了基本的页面构建和项目启动。
在UMI中,区块链源码评级pages中的js组件并列,文件名即为路由路径。通过导航标签可实现路由切换。
路由传参有三种形式:params、query、state。接收参数时,根据传参形式进行对应处理。
嵌套路由时,构建_layout.js用于展示子组件。通过{ props.children}展示子组件。
HTML模版定义包括定义title、meta等设置,构建document.ejs。全局公共的css编写,构建global.css,无需引入,所有pages组件均可用。
Umi 4 RC 发布
经过数月精心开发,备受期待的Umi 4 RC版终于与开发者们见面了。这次升级带来了显著的变化,尽管开发周期较长,但团队努力将对开发者的影响降到最低。以下是Umi 4 RC版的主要亮点:新官网与文档: Umi 4的官网焕然一新,包含基础配置、API、升级教程等文档,同时,插件文档整合到Umi官网,方便用户查阅。
MFSU V3 & 默认开启: MFSU的最新版本在性能和稳定性上有了显著提升,Umi 4默认启用此功能,修复图片源码且支持独立运行。
双构建引擎和 ESMi: 提供Vite和Webpack两种构建模式,允许开发者按需选择,同时在Vite模式下支持ESMi的客户端使用。
Webpack 5: Umi 4默认采用Webpack 5并启用物理缓存,提升构建效率。
React Router 6: Umi 4采用React Router 6的新路由结构,简化了路由配置,便于扩展和约定式请求。
最佳实践升级: 优化了官方插件,提供更一致的风格和体验。
依赖预打包: 加强了对依赖的安全性和稳定性的管理,包括对核心库的锁定。
Umi Pro: 内部Bigfish框架的对外版本,为开发者和企业提供了另一种集中化框架的选择。
Low Import 研发模式: 降低开发者的import负担,通过lowImport实现无import使用相关功能。
强约束功能集成: 提供API支持代码校验和约束功能,方便团队协作。
Import All From Umi: 解决了黑盒问题,提供无副作用的依赖管理。
源码编译与依赖编译: 提供多种编译选项,针对不同场景选择最合适的工具。
jsMinifier和cssMinifier: 默认使用esbuild进行压缩,同时支持其他压缩工具。
应用元数据: 收集项目详细信息,为插件开发者和统计需求提供便利。
微生成器: 学习自modern.js,支持按需启用和定制配置。
贴心改进: 包括新增plugin.ts文件和优先使用本地umi.js代码等便捷功能。
Umi 4 RC版还在不断优化中,未来还会发布更多功能和改进。请尝试新版本,并关注官方文档中关于ant-design-pro从Umi 3到4的升级指南。为了帮助大家顺利过渡,我们还准备了交流群和升级教程。祝大家新年快乐,虎年吉祥! 交流群二维码链接:点击获取二维码UmiJS 学习笔记 1 - 快速上手
UmiJS,中文读作乌米,是一个被称为“插件化的企业级前端应用框架”的工具。它的核心基于路由,支持配置式和约定式两种路由方式,旨在提供完整的项目开发流程支持,涵盖从源代码开发到构建的各个环节,适用于各种类型的项目开发。UmiJS在阿里巴巴和网易等公司中得到了广泛应用,凭借其全面的功能和实战验证的可靠性,备受信赖。
要快速上手Umi,有两种途径可供选择。首先,你可以手动创建项目文件,通过自定义文件结构来启动项目。另一种更为便捷的方式是利用Umi的脚手架工具,这能简化初始化过程。只需在浏览器中访问 http://localhost:,即可启动本地的Umi服务器,正式进入项目开发阶段。现在,你可以开始构建并探索你的项目了。
umi3源码解析之核心Service类初始化
前言
umi是一个插件化的企业级前端应用框架,在开发中后台项目中应用颇广,确实带来了许多便利。借着这个契机,便有了我们接下来的“umi3源码解析”系列的分享,初衷很简单就是从源码层面上帮助大家深入认知umi这个框架,能够更得心应手的使用它,学习源码中的设计思想提升自身。该系列的大纲如下:
开辟鸿蒙,今天要解析的就是第一part,内容包括以下两个部分:
邂逅umi命令,看看umidev时都做了什么?
初遇插件化,了解源码中核心的Service类初始化的过程。
本次使用源码版本为?3.5.,地址放在这里了,接下来的每一块代码笔者都贴心的为大家注释了在源码中的位置,先clone再食用更香哟!
邂逅umi命令该部分在源码中的路径为:packages/umi
首先是第一部分umi命令,umi脚手架为我们提供了umi这个命令,当我们创建完一个umi项目并安装完相关依赖之后,通过yarnstart启动该项目时,执行的命令就是umidev
那么在umi命令运行期间都发生了什么呢,先让我们来看一下完整的流程,如下图:
接下来我们对其几个重点的步骤进行解析,首先就是对于我们在命令行输入的umi命令进行处理。
处理命令行参数//packages/umi/src/cli.tsconstargs=yParser(process.argv.slice(2),{ alias:{ version:['v'],help:['h'],},boolean:['version'],});if(args.version&&!args._[0]){ args._[0]='version';constlocal=existsSync(join(__dirname,'../.local'))?chalk.cyan('@local'):'';console.log(`umi@${ require('../package.json').version}${ local}`);}elseif(!args._[0]){ args._[0]='help';}解析命令行参数所使用的yParser方法是基于yargs-parser封装,该方法的两个入参分别是进程的可执行文件的绝对路径和正在执行的JS文件的路径。解析结果如下:
//输入umidev经yargs-parser解析后为://args={ //_:["dev"],//}在解析命令行参数后,对version和help参数进行了特殊处理:
如果args中有version字段,并且args._中没有值,将执行version命令,并从package.json中获得version的值并打印
如果没有version字段,args._中也没有值,将执行help命令
总的来说就是,如果只输入umi实际会执行umihelp展示umi命令的使用指南,如果输入umi--version会输出依赖的版本,如果执行umidev那就是接下来的步骤了。
提问:您知道输入umi--versiondev会发什么吗?
运行umidev
//packages/umi/src/cli.tsconstchild=fork({ scriptPath:require.resolve('./forkedDev'),});process.on('SIGINT',()=>{ child.kill('SIGINT');process.exit(0);});//packages/umi/src/utils/fork.tsif(CURRENT_PORT){ process.env.PORT=CURRENT_PORT;}constchild=fork(scriptPath,process.argv.slice(2),{ execArgv});child.on('message',(data:any)=>{ consttype=(data&&data.type)||null;if(type==='RESTART'){ child.kill();start({ scriptPath});}elseif(type==='UPDATE_PORT'){ //setcurrentusedportCURRENT_PORT=data.portasnumber;}process.send?.(data);});本地开发时,大部分脚手架都会采用开启一个新的线程来启动项目,umi脚手架也是如此。这里的fork方法是基于node中child_process.fork()方法的封装,主要做了以下三件事:
确定端口号,使用命令行指定的端口号或默认的,如果该端口号已被占用则prot+=1
开启子进程,该子进程独立于父进程,两者之间建立IPC通信通道进行消息传递
处理通信,主要监听了RESTART重启和UPDATE_PORT更新端口号事件
接下来看一下在子进程中运行的forkedDev.ts都做了什么。
//packages/umi/src/forkedDev.ts(async()=>{ try{ //1、设置NODE_ENV为developmentprocess.env.NODE_ENV='development';//2、InitwebpackversiondeterminationandrequirehookinitWebpack();//3、实例化Service类,执行run方法constservice=newService({ cwd:getCwd(),//umi项目的根路径pkg:getPkg(process.cwd()),//项目的package.json文件的路径});awaitservice.run({ name:'dev',args,});//4、父子进程通信letclosed=false;process.once('SIGINT',()=>onSignal('SIGINT'));process.once('SIGQUIT',()=>onSignal('SIGQUIT'));process.once('SIGTERM',()=>onSignal('SIGTERM'));functiononSignal(signal:string){ if(closed)return;closed=true;//退出时触发插件中的onExit事件service.applyPlugins({ key:'onExit',type:service.ApplyPluginsType.event,args:{ signal,},});process.exit(0);}}catch(e:any){ process.exit(1);}})();设置process.env.NODE_ENV的值
initWebpack(接下来解析)
实例化Service并run(第二part的内容)
处理父子进程通信,当父进程监听到SIGINT、SIGTERM等终止进程的信号,也通知到子进程进行终止;子进程退出时触发插件中的onExit事件
initWebpack
//packages/umi/src/initWebpack.tsconsthaveWebpack5=(configContent.includes('webpack5:')&&!configContent.includes('//webpack5:')&&!configContent.includes('//webpack5:'))||(configContent.includes('mfsu:')&&!configContent.includes('//mfsu:')&&!configContent.includes('//mfsu:'));if(haveWebpack5||process.env.USE_WEBPACK_5){ process.env.USE_WEBPACK_5='1';init(true);}else{ init();}initRequreHook();这一步功能是检查用户配置确定初始化webpack的版本。读取默认配置文件.umirc和config/config中的配置,如果其中有webpack5或?mfsu等相关配置,umi就会使用webpack5进行初始化,否则就使用webpack4进行初始化。这里的mfsu是webpack5的模块联邦相关配置,umi在3.5版本时已经进行了支持。
初遇插件化该部分在源码中的路径为:packages/core/src/Service
说起umi框架,最先让人想到的就是插件化,这也是框架的核心,该部分实现的核心源码就是Service类,接下来我们就来看看Service类的实例化和init()的过程中发生了什么,可以称之为插件化实现的开端,该部分的大致流程如下
该流程图中前四步,都是在Service类实例化的过程中完成的,接下来让我们走进Service类。
Service类的实例化//packages/core/src/Service/Service.tsexportdefaultclassServiceextendsEventEmitter{ constructor(opts:IServiceOpts){ super();this.cwd=opts.cwd||process.cwd();//当前工作目录//repoDirshouldbetherootdirofrepothis.pkg=opts.pkg||this.resolvePackage();//package.jsonthis.env=opts.env||process.env.NODE_ENV;//环境变量//在解析config之前注册babelthis.babelRegister=newBabelRegister();//通过dotenv将环境变量中的变量从.env或.env.local文件加载到process.env中this.loadEnv();//1、getuserconfigconstconfigFiles=opts.configFiles;this.configInstance=newConfig({ cwd:this.cwd,service:this,localConfig:this.env==='development',configFiles});this.userConfig=this.configInstance.getUserConfig();//2、getpathsthis.paths=getPaths({ cwd:this.cwd,config:this.userConfig!,env:this.env,});//3、getpresetsandpluginsthis.initialPresets=resolvePresets({ ...baseOpts,presets:opts.presets||[],userConfigPresets:this.userConfig.presets||[],});this.initialPlugins=resolvePlugins({ ...baseOpts,plugins:opts.plugins||[],userConfigPlugins:this.userConfig.plugins||[],});}}Service类继承自EventEmitter用于实现自定义事件。在Service类实例化的过程中除了初始化成员变量外主要做了以下三件事:
1、解析配置文件
//packages/core/src/Config/Config.tsconstDEFAULT_CONFIG_FILES=[//默认配置文件'.umirc.ts','.umirc.js','config/config.ts','config/config.js',];//...if(Array.isArray(opts.configFiles)){ //配置的优先读取this.configFiles=lodash.uniq(opts.configFiles.concat(this.configFiles));}//...getUserConfig(){ //1、找到configFiles中的第一个文件constconfigFile=this.getConfigFile();this.configFile=configFile;//潜在问题:.local和.env的配置必须有configFile才有效if(configFile){ letenvConfigFile;if(process.env.UMI_ENV){ //1.根据UMI_ENV添加后缀eg:.umirc.ts-->.umirc.cloud.tsconstenvConfigFileName=this.addAffix(configFile,process.env.UMI_ENV,);//2.去掉后缀eg:.umirc.cloud.ts-->.umirc.cloudconstfileNameWithoutExt=envConfigFileName.replace(extname(envConfigFileName),'',);//3.找到该环境下对应的配置文件eg:.umirc.cloud.[ts|tsx|js|jsx]envConfigFile=getFile({ base:this.cwd,fileNameWithoutExt,type:'javascript',})?.filename;}constfiles=[configFile,//eg:.umirc.tsenvConfigFile,//eg:.umirc.cloud.tsthis.localConfig&&this.addAffix(configFile,'local'),//eg:.umirc.local.ts].filter((f):fisstring=>!!f).map((f)=>join(this.cwd,f))//转为绝对路径.filter((f)=>existsSync(f));//clearrequirecacheandsetbabelregisterconstrequireDeps=files.reduce((memo:string[],file)=>{ memo=memo.concat(parseRequireDeps(file));//递归解析依赖returnmemo;},[]);//删除对象中的键值require.cache[cachePath],下一次require将重新加载模块requireDeps.forEach(cleanRequireCache);this.service.babelRegister.setOnlyMap({ key:'config',value:requireDeps,});//requireconfigandmergereturnthis.mergeConfig(...this.requireConfigs(files));}else{ return{ };}}细品源码,可以看出umi读取配置文件的优先级:自定义配置文件?>.umirc>config/config,后续根据UMI_ENV尝试获取对应的配置文件,development模式下还会使用local配置,不同环境下的配置文件也是有优先级的
例如:.umirc.local.ts>.umirc.cloud.ts>.umirc.ts
由于配置文件中可能require其他配置,这里通过parseRequireDeps方法进行递归处理。在解析出所有的配置文件后,会通过cleanRequireCache方法清除requeire缓存,这样可以保证在接下来合并配置时的引入是实时的。
2、获取相关绝对路径
//packages/core/src/Service/getPaths.tsexportdefaultfunctiongetServicePaths({ cwd,config,env,}:{ cwd:string;config:any;env?:string;}):IServicePaths{ letabsSrcPath=cwd;if(isDirectoryAndExist(join(cwd,'src'))){ absSrcPath=join(cwd,'src');}constabsPagesPath=config.singular?join(absSrcPath,'page'):join(absSrcPath,'pages');consttmpDir=['.umi',env!=='development'&&env].filter(Boolean).join('-');returnnormalizeWithWinPath({ cwd,absNodeModulesPath:join(cwd,'node_modules'),absOutputPath:join(cwd,config.outputPath||'./dist'),absSrcPath,//srcabsPagesPath,//pagesabsTmpPath:join(absSrcPath,tmpDir),});}这一步主要获取项目目录结构中node_modules、dist、src、pages等文件夹的绝对路径。如果用户在配置文件中配置了singular为true,那么页面文件夹路径就是src/page,默认是src/pages
3、收集preset和plugin以对象形式描述
在umi中“万物皆插件”,preset是对于插件的描述,可以理解为“插件集”,是为了方便对插件的管理。例如:@umijs/preset-react就是一个针对react应用的插件集,其中包括了plugin-access权限管理、plugin-antdantdUI组件等。
//packages/core/src/Service/Service.tsthis.initialPresets=resolvePresets({ ...baseOpts,presets:opts.presets||[],userConfigPresets:this.userConfig.presets||[],});this.initialPlugins=resolvePlugins({ ...baseOpts,plugins:opts.plugins||[],userConfigPlugins:this.userConfig.plugins||[],});在收集preset和plugin时,首先调用了resolvePresets方法,其中做了以下处理:
3.1、调用getPluginsOrPresets方法,进一步收集preset和plugin并合并
//packages/core/src/Service/utils/pluginUtils.tsgetPluginsOrPresets(type:PluginType,opts:IOpts):string[]{ constupperCaseType=type.toUpperCase();return[//opts...((opts[type===PluginType.preset?'presets':'plugins']asany)||[]),//env...(process.env[`UMI_${ upperCaseType}S`]||'').split(',').filter(Boolean),//dependencies...Object.keys(opts.pkg.devDependencies||{ }).concat(Object.keys(opts.pkg.dependencies||{ })).filter(isPluginOrPreset.bind(null,type)),//userconfig...((opts[type===PluginType.preset?'userConfigPresets':'userConfigPlugins']asany)||[]),].map((path)=>{ returnresolve.sync(path,{ basedir:opts.cwd,extensions:['.js','.ts'],});});}这里可以看出收集preset和plugin的来源主要有四个:
实例化Service时的入参
process.env中指定的UMI_PRESETS或UMI_PLUGINS
package.json中dependencies和devDependencies配置的,需要命名规则符合?/^(@umijs\/|umi-)preset-/这个正则
解析配置文件中的,即入参中的userConfigPresets或userConfigPresets
3.2、调用pathToObj方法:将收集的plugin或preset以对象的形式输出
//输入umidev经yargs-parser解析后为://args={ //_:["dev"],//}0umi官网中提到过:每个插件都会对应一个id和一个key,id是路径的简写,key是进一步简化后用于配置的唯一值。便是在这一步进行的处理
形式如下:
//输入umidev经yargs-parser解析后为://args={ //_:["dev"],//}1思考:为什么要将插件以对象的形式进行描述?有什么好处?
执行run方法,初始化插件在Service类实例化完毕后,会立马调用run方法,run()执行的第一步就是执行init方法,init()方法的功能就是完成插件的初始化,主要操作如下:
遍历initialPresets并init
合并initpresets过程中得到的plugin和initialPlugins
遍历合并后的plugins并init
这里的initialPresets和initialPlugins就是上一步收集preset和plugin得到的结果,在这一步要对其逐一的init,接下来我们看一下init的过程中做了什么。
Initplugin
//输入umidev经yargs-parser解析后为://args={ //_:["dev"],//}2这段代码主要做了以下几件事情:
getPluginAPI方法:newPluginAPI时传入了Service实例,通过pluginAPI实例中的registerMethod方法将register方法添加到Service实例的pluginMethods中,后续返回pluginAPI的代理,以动态获取最新的register方法,以实现边注册边使用。
//输入umidev经yargs-parser解析后为:/