摄影艺术居

摄影艺术居

解锁创新潜力:Farm 核心 API 带你打造个性化构建工具

admin 150 171

本文开始将开始Farm框架原理性解析的第一篇,带领大家从0到1到最后打造出属于自己的编译器。

如果还有不知道Farm的同学,在这里再给大家简单的介绍一下Farm是一个基于Rust实现的极速构建引擎,帮助您更快地构建Web程序和JavaScript库。

Farm仓库相关链接,希望大家可以多多支持,如果大家对Farm感兴趣,也非常希望大家可以参与进来,一同打造下一代Web构建工具

Farm相关链接,希望大家多多鼓励和支持

Github仓库

文档

FarmCLI

第一篇文章带大家创建基于Farmapi来打造一套个性化的构建工具,带大家先从cli命令行开始,不仅仅可以像其他构建工具一样比如vite,使用vitevitebuildvitepreview命令来快速启动一个项目,我们也可以针对不同功能,业务来封装属于自己的命令

那么什么是cli呢?,以及针对web项目的cli和传统意义上的cli有什么区别呢

CLI是CommandLineInterface的缩写,即命令行界面。它是一种在计算机上与用户进行交互的方式,用户通过键入文本命令来执行各种操作,而不是通过图形用户界面(GUI)。而针对Web构建工具的CLI通常是一组命令行工具,用于辅助开发人员在项目中执行各种任务。这些任务可以包括启动服务器运行项目、构建项目、预览项目,分析等。开发Cli使用的工具

开发cli也有一些好用的工具,这里为大家介绍一些作者最常用的工具。

commander:用于解析命令和参数,可以给不同的命令配置不同的处理函数,函数会接收到这个伴随命令传入的参数,可以说是非常好用了。

citty:优雅的CLI构建器由unjs开发,函数式编程,各个环节逻辑清晰

Oclif:是一个用于构建CLI的框架,它提供了一组强大的工具和约定,可以帮助你快速构建出具有一致性和专业性的CLI工具。它支持命令的组合、自动生成帮助文档、命令插件化等功能。

cac:是一个用于构建CLI应用程序的JavaScript库。也是目前比较常用的一款

ora:控制台的loading效果

minimist解析命令行参数

inquirer:可以在控制台和用户一问一答,交互式的处理。

实现功能

本文会从头搭建项目,带领大家一步一步完成整个cli项目的构建与发布,以下是我们要实现的几个通用命令,因为本文只是cli的建设搭建所以不会对具体核心api以及功能实现展开讲解,以后Farm团队都会一点一点以本文为起点解析所有功能,请大家拭目以待。

以下是Farm目前所有实现的一些主要命令,本文带大家主要完成start和build命令

farmstart启动开发服务器

farmbuild项目打包编译

farmwatch监听项目打包编译,修改代码后实时进行编译

farmpreview启动预览服务器来预览生产环境产物

farmclean清除增量构建写入磁盘的缓存

项目初始化

接下来就正式开始创建我们的项目,那么我们选择citty来开发脚手架,至于为什么选择citty因为不仅citty基于mri(轻量parser解析器)mri比其他以前常用的这是minimist和yargs-parser的都要快很多,并且使用了更加现代化的函数式编程思想,提供了defineCommand、runMain等函数API,语法更加简洁直观。而很多其他工具使用的是基于原型或对象的API方式.

初始化项目

ts复制代码pnpminit

安装项目所需依赖,尽量做到轻量,功能全面,代码清晰简洁

ts复制代码pnpmadd@farmfe/corecitty-D

在中设置bin字段,在的bin字段指定一个可执行文件的路径,npm会为该文件创建一个软链接,添加到系统环境变量PATH中。这样在终端就可以直接执行该命令。

ts复制代码{"name":"personal-next-cli","version":"1.0.0","main":"./dist/","types":"./dist/","bin":{"next":"./bin/"}}

在项目中设置bin目录,新增一个文件使用shebang语法,在入口文件的第一行加上了!/usr/bin/("Imnextpersonalcli");

然后终端中执行pnpminstall-g或者npmlink将包安装到全局环境后,bin中配置的命令就被链接到了系统的PATH变量中。这样在任何目录下执行该命令都是可行的,方便使用。我们执行next之后,就会运行当前bin链接的js文件

ts复制代码PSD:\adny\personal-clinext//命令行输入Imnextpersonalcli//输出

所以我们只需要把("Imnextpersonalcli");这句话换成我们需要运行的文件就可以完成我们的第一步了

那么先来介绍一下citty提供的主要方法及其作用:

defineCommand

defineCommand是citty中定义命令的核心API。它接收一个对象作为参数,该对象描述了命令的元数据、参数配置和执行逻辑。返回一个命令定义对象。

ts复制代码constmain=defineCommand({meta:{/**/},//命令元数据解析例如versionargs:{/**/},//命令参数配置run({args}){/**/}//命令执行逻辑})

runMain

runMain用于运行一个命令定义对象。它会根据用户输入的参数执行相应的命令逻辑,并自动生成帮助信息。这是运行CLI应用的入口点。

ts复制代码constmain=defineCommand({meta:{/**/},//命令元数据解析例如versionargs:{/**/},//命令参数配置run({args}){/**/}//命令执行逻辑})runMain(main)//运行命令

showUsage

呈现使用方法并打印到控制台

ts复制代码PSD:\adny\personal-clinext--helpUSAGEnext-personal-cli[OPTIONS]start|build|watch|preview|cleanOPTIONS-h,--helpShowthishelpmessageCOMMANDSstartCompiletheprojectindevmodeandserveitwithdevserverbuildCompiletheprojectinproductionmodewatchWatchfilechangeandrecompilepreviewCompiletheprojectinwatchmodewithpreviewservercleanCleanupthecachebuiltincrementallyUsenext-personal-clicommand--helpformoreinformationaboutacommand.

parseArgs

parseArgs用于解析命令行参数,并应用默认值。返回解析后的参数对象。

ts复制代码constargs=parseArgs(['--foo','bar'],{})//{_:[],foo:'bar'}

常用的命令我们只需要前两个(defineCommand,runMain)就够了

然后@farmfe/core作为farm的核心包,提供了很多的编译方面的api在后续文章我们都会为大家介绍,@farmfe/core也内置了针对cli所需要的所有api包括start,build,watch,clean,preview和其他更加底层的api例如compiler,wathcer,server等等

编写代码

目录结构如下

ts复制代码--

先来看入口文件src/,先定义所有命令citty使用defineCommand定义入口,meta定义元数据,比如我们可以传入脚手架的版本号或者描述,run方法代表每次执行命令之后执行的函数,subCommands定义子命令。

ts复制代码importfsfrom'node:fs'import{defineCommand,runMain,showUsage}from'citty'import{args}from'./'constpackageJsonFile=('./','utf8');const{version,description}=packageJsonFileasany;constmain=defineCommand({meta:{name:'next-personal-cli',version,description,},args,asyncrun(ctx){if(){awaitshowUsage()return}},subCommands:{start:()=import("./commands/").then((r)=),build:()=import("./commands/").then((r)=),watch:()=import("./commands/").then((r)=),preview:()=import("./commands/").then((r)=),clean:()=import("./commands/").then((r)=),},});runMain(main);

arg属性代表每一个命令传递的参数,可以传递类型,别名,默认值,还有描述,比如第一个config参数,那么比如在我们运行build的时候就可以使用来指定我们需要运行的配置文件,farm默认接收一个文件会把定义的配置文件和命令行参数进行合并,然后解析出最终交给compiler的参数对象

ts复制代码exportconstargs:any={config:{type:'string',alias:'c',description:'Usethisconfigfile(ifargumentisusedbutvalueisunspecified,)',},mode:{type:'string',alias:'m',description:'Setenvmode',default:'development',},base:{type:'string',description:'Publicbasepath',},clearScreen:{type:'boolean',description:'Allow/disableclearscreenwhenlogging',default:true,},help:{type:'boolean',alias:'h',description:'Showthishelpmessage',},}
start

那么先来看一下start子命令,farm提供了开箱即用的一些命令,也包含了所有编译,开发服务器等api的使用方式,在start方法中就可以使用farm提供的start方法,传递不同的参数

ts复制代码import{start}from'@farmfe/core'import{startArgsasargs}from"../";exportdefaultdefineCommand({meta:{name:"start",description:"Compiletheprojectindevmodeandserveitwithdevserver",},args,run({args}){constresolveOptions=resolveCommandOptions(args);constconfigPath=getConfigPath();constdefaultOptions:any={compilation:{lazyCompilation:},server:resolveOptions,clearScreen:,configPath,mode:};start(defaultOptionsasany)},});

先来设计一下start子命令所需要的参数


base:配置devserver的publicpath


port:支持命令行传递不同端口号


host:支持命令行传递host主机


open:支持默认自动打开浏览器功能

start的命令行参数一般情况下,我们会可能需要配置base相当于开发服务器publicpath,port开发服务器端口号,host开发服务器主机ip地址,open是否默认启动浏览器。我们先简单的使用这几个

ts复制代码exportconststartArgs:any={base:{type:'string',description:'Publicbasepath',}host:{type:"number",description:"specifyhost",},port:{type:"number",description:"specifyport",},open:{type:"boolean",description:"openbrowserafterserverstarted",},}

然后我们就可以开始测试start命令了,我们简单的使用tsc进行打包,然后bin文件链接打包之后的dist目录。

先定义文件

json复制代码{"compilerOptions":{"noImplicitAny":true,"sourceMap":true,"target":"esnext","inlineSources":true,"esModuleInterop":true,"allowSyntheticDefaultImports":true,"skipDefaultLibCheck":true,"skipLibCheck":true,"rootDir":"src","outDir":"dist","declaration":false,"noUnusedLocals":false,"noUnusedParameters":false,"resolveJsonModule":true,"lib":["ESNext","DOM"],"moduleResolution":"node","module":"esnext"},"exclude":["node_modules","src/templates",],"include":["src/**/*.ts"],}

然后运行tsc之后会生成dist目录,我们在bin/中导入这个文件

shell复制代码#!/usr/bin/envnodeimport'../dist/'

中已经配置了bin属性的指向,所以当我们运行nextstart他就会执行这个文件,接下来新建一个playground目录,新建一个。


然后我们进入到playground目录运行nextstart--port6542。


成功运行,说明我们start方法就已经运行成功了,那么我们start命令就完成了,大家根据公司业务或者其他想法,也可以添加其他的命令行参数。

Build

接下来开始build命令,其实功能也是一样,浅浅设计一下build命令


outDir:支持编译打包之后的输出目录


entry:支持编译的入口文件


format:如果是node环境需要打包出不同的输出格式比如esm,cjs


targetEnv:要同时支持web环境和node环境的打包


minify:是否需要压缩minify


sourcemap:是否输出生成sourcemap


treeShaking:以及是否需要treeShaking

那么需要传递的命令行参数就是

ts复制代码exportconstbuildArgs:any={//输出目录outDir:{type:'string',alias:'o',description:'Outputdirectory',},//入口文件input:{type:'string',alias:'i',description:'Inputfile',},//构建环境target:{type:'string',alias:'t',description:'transpiletargetEnvnode,browser',},//格式化输出esm/cjsformat:{type:'string',alias:'f',description:'Outputformatesm,cjs',},//是否开启压缩minify:{type:'boolean',description:'codecompressionatbuildtime',},//是否开始sourcemapsourcemap:{type:'boolean',description:'outputsourcemapsforbuild'},//是否开启树摇treeShaking:{type:'boolean',description:'Eliminateuselesscodewithoutsideeffects',},}

然后开始编写build的subCommands,把格式化后的参数传递给farm提供的build方法

javascript复制代码import{defineCommand}from"citty";import{build}from'@farmfe/core'exportdefaultdefineCommand({meta:{name:"build",description:"Compiletheprojectinproductionmode",},args,run({args}){constconfigPath=getConfigPath();constdefaultOptions={compilation:{watch:,output:{path:args?.outDir,targetEnv:args?.target,format:args?.format},input:{index:args?.input},sourcemap:,minify:,treeShaking:},mode:,configPath};build(defaultOptions)},});

这样我们在执行nextbuild之后他就会自动执行这个命令


接下来在测试一下我们定义的build命令行参数

shell复制代码


然后目录就会生成build文件夹,并且打包的是node环境下的产物


Farm一致性扩展create命令

其他命令原理也是一样的,这样就完成了一个构建工具基本的cli启动和构建的能力。为了扩展cli的能力,在带领大家写一个创建模板的子命令,首先在中添加create子命令

ts复制代码subCommands:{create:()=import("./commands/").then((r)=),},

commands目录下新建文件整个功能的实现也非常简单,我们提供好已知的项目模板,只要根据不同的命令行参数就可以解析到不同的template最后把模板复制到我们传递的目录中就可以了,我们使用prompts这个包来进行交互不仅提供命令行参数的能力,也提供模板交互来让我们手动进行选择,我们为了简化操作就只提供一种选择框架的交互整个代码逻辑也非常简单,

我们需要先安装prompts交互和picocolors颜色包

ts复制代码pnpmaddpromptspicocolors-D

代码逻辑如下,createArgs我们可以传递template参数来指定使用vue或者react等其他框架或者根据prompts来进行手动选择,先输入项目名称,然后选择需要使用的框架,然后就成功啦。

ts复制代码import{defineCommand}from"citty";importfsfrom'node:fs'importpromptsfrom'prompts';importcolorsfrom'picocolors';import{createArgsasargs}from"../";import{copyTemplate,formatTargetDir,getConfigPath,isEmpty,pkgFromUserAgent,resolveCommandOptions}from"../";exportdefaultdefineCommand({meta:{name:"createtemplate",description:"createanewprojectfromatemplatewithfarm",},args,run({args}){createFarm(args)},});asyncfunctioncreateFarm(args:any){constcwd=();constDEFAULT_TARGET_NAME='farm-project';constargProjectName=formatTargetDir(args._[0]);constargFramework=||;lettargetDir=argProjectName||DEFAULT_TARGET_NAME;letresult:IResultType={};constskipInstall=args['skip-install']????true;try{result=awaitprompts([{type:argProjectName?null:'text',name:'projectName',message:'Projectname:',initial:DEFAULT_TARGET_NAME,onState:(state)={targetDir=formatTargetDir()||DEFAULT_TARGET_NAME;}},{type:()=!(targetDir)||isEmpty(targetDir)?null:'confirm',name:'overwrite',message:()=(targetDir==='.'?'Currentdirectory':`Targetdirectory"${targetDir}"`)+`?`},{type:(_,{overwrite}:{overwrite?:boolean})={if(overwrite===false){thrownewError(('❌')+'Operationcancelled');}returnnull;},name:'overwriteChecker'},{type:argFramework?null:'select',name:'framework',message:'Selectaframework:',initial:0,choices:[{title:('React'),value:'react'},{title:('Vue'),value:'vue'},]},],{onCancel:()={thrownewError(('❌')+'Operationcancelled');}});}catch(cancelled){();return;}const{framework=argFramework,packageManager}=result;awaitcopyTemplate(targetDir,{framework,projectName:targetDir,packageManager});(('TemplatecopiedSuccessfully!'));}

接下来我们试一下效果,要提前把template文件夹放到目录中,发布的时候可以在中放到files里


我们定义好的templates目录


然后在任意目录下输入

ts复制代码nextcreate


选择完之后就能看到项目被正确的创建了


总结

本文从头到尾详细介绍了如何使用Farm构建一个个性化的CLI工具,旨在帮助开发者快速构建适用于特定项目或场景的命令行界面工具。通过本文的学习,读者不仅可以了解到CLI工具的重要性和特点,还能够掌握使用citty构建CLI工具的方法。

在项目初始化和结构部分,我们学习了如何使用citty初始化项目,配置的bin字段,并创建了项目的目录结构,为后续各个子命令的实现做好了准备。接着,我们逐步实现了start、build等子命令,展示了如何利用Farm提供的API来完成具体功能。整个脚手架的命令都已经完成本文只带大家体验了start和build命令。其他的watch,preview和clean完整的功能可以看githubnext-personal-cli仓库代码

在最后,我们还介绍了如何添加交互式命令create,并使用prompts包来与用户进行交互,以便根据用户的选择创建新项目。这一部分为CLI工具增添了更多的交互性和可定制性,使其更加灵活和易用。

我们甚至可以基于cli扩展各种各样的插件命令来快速创建插件,Farm框架也提供了更多核心、功能颗粒度更加细致的API,使得开发者可以根据自身需求定制更加完善的功能。不仅仅是cli,无论是扩展现有功能、引入新的特性,还是开发更专业化的工具,都可以在Farm的基础上灵活实现。这种灵活性和可扩展性为开发者提供了丰富的选择,使得他们可以根据项目需求和个人偏好,打造出更符合实际场景的定制化工具。因此,无论是构建简单的CLI工具还是复杂的Web应用,Farm都能够提供强大支持,并为开发者带来更大的创造空间。

本文实现代码功能逻辑并不复杂,适合大家在步入前端工程化开头的一课,这是我们实现Farm的第一课,后面的代码会循序渐进。请大家拭目以待,毫无保留的教给大家