当前位置: 首页 > 后端技术 > Node.js

从零开始搭建一个节点脚手架工具(一)

时间:2023-04-03 10:27:31 Node.js

下一篇传送门:从零开始搭建一个节点脚手架工具(二)前言在实际开发中,我们总会遇到各种各样的脚手架工具。从vue-cli,create-react-app,从头开始搭建项目结构,到保存代码片段的smallsnippets,都为我们的开发带来了很多便利。这些脚手架工具各有优缺点。比如vue-cli只支持创建Vue项目,定制化程度低,而snippets过于轻量,不支持多人协作开发。因此,在学习了node之后,我打算做一个符合自己需求的node脚手架工具。贴上脚手架工具的github地址供参考:YOSO:Youonlysetoncetypescript我选择的开发语言是typescript。众所周知,javascript是一种动态类型的语言,这使得我们在开发时不太关注对象的类型。这确实带来了一些方便,有时可以使代码简洁,但有时也会带来麻烦。比如很久以前写的一个函数,如果没有好的注释,你可能记不住这个函数的输入输出是什么。或者在重构代码的时候,给函数加了一个参数,一不小心很容易错过某个调用。特别是对于大型项目,这增加了程序员的负担。Typescript是javascript的超集,支持javascript的所有语法,可以编译成纯javascript运行。它的主要特性之一是静态类型。与动态类型相比,除了需要更多类型代码外,它还有更多优势。检测错误typescript的第一个优点是可以在编译时检测到类型错误,而不是等到上线后才发现。编程规范typescript的另一个优点是它加强了编程规范。它提供了一种定义接口的简单方法。系统模块可以抽象为由typescript定义的接口。一种更抽象的设计形式是将大型系统构建为具有清晰接口的模块。代码可读性类型注解对于提高代码可读性也有很大的帮助。同时,利用typedoc等工具,还可以轻松生成文档。决定使用typescript之后,开发的时候还有几点需要注意。首先,既然决定使用typescript,就一定要充分利用它的优势。写类型的时候,不要一直用any,定义多少接口就用多少,否则会是负担。考虑到复用性,可以在减速文件中定义常用的类型,方便参考。在typescript中导入npm包时,有时会出现Couldnotfindadeclarationfileformodule的错误。这是因为大多数JavaScript库没有打字稿类型定义。为了解决这个问题,DefinitelyTyped应运而生。这是一个高质量的typescript类型定义存储库,您可以通过npminstall@types/jquery--save-dev将类型定义添加到库中。如果在DefinitelyTyped中没有找到你想要的库,不要使用import方法,改用require引用,也可以避免报错。在正式开发设计之前,必须先做好设计。整个项目的结构我参考了nest-cli的结构,主要包括:bin文件夹下的入口文件commands文件夹,存放模块化的命令文件,用于接收和解析输入的命令,以及命令在nest-cli的文件夹如下。命令├──abstract.command.ts├──add.command.ts├──command.input.ts├──command.loader.ts├──generate.command.ts├──index.ts├──info.command.ts├──new.command.ts└──update.command.tsactions文件夹,存放模块化动作文件,用于处理和执行命令。nest-cli中的actions文件夹如下。actions├──abstract.action.ts├──add.action.ts├──generate.action.ts├──index.ts├──info.action.ts├──new.action.ts└──update.action.ts.gitignore、tsconfig.json、npm相关文件等,这些文件应该遵循行业规范。可以参考我的项目脚手架工具的工作流程如下。命令行处理会用到commander、inquirer等第三方库。另外,我选择使用react+ink来开发ui,让交互界面更好用。加载模板时,我选择从github仓库加载,这需要了解github的相关API,记录用户的github仓库地址。模板引擎我选择了Mozilla的nunjucks。之所以没有选择比较出名的handlebars、pug、ejs等模板引擎,是因为这些模板引擎主要是针对html语言的。为了防止xss攻击,会有很多转义处理。如果要生成js等文件作为脚手架工具,那就比较麻烦了。而且nunjucks默认是不做转义的,各方面都差不多的时候更适合我们的脚手架工具。最后,对于文件操作,node自带的fs工具可以完成。开始开发项目初始化完成,设计完成后,就可以开始正式的开发了。第一步是完成一些琐碎的事情,给项目起个名字。在命名之前,可以使用npminfoname查看是否有重名。新建一个git仓库存放项目代码,并在本地创建相应的目录连接远程仓库。使用npminit命令初始化并生成package.json文件。因为我们要使用typescript进行开发,所以需要安装typescript。然后创建一个tsconfig.json文件,在tsconfig.json中设置typescript相关的配置项。这时候可以运行tsc看看js文件是否可以正常编译。然后根据我们的设计,创建这些我们需要的文件夹,并在bin文件夹中新建一个入口文件,一般和输入的命令同名,我这里叫yoso.ts。然后在package.json中添加配置项。"scripts":{"build":"tec"}"bin":{"yoso":"bin/yoso.js"}需要注意的是,虽然我们使用typescript进行开发,但实际上最终还是会编译成js要运行,我们发送的npm包也必须是编译好的js代码。所以这里虽然创建了bin/yoso.ts,但是yoso.js还是写在了package.json里面。然后进入yoso.ts文件,写:#!/usr/bin/envnodeconsole.log("yoso!")#!/usr/bin/env开头的node不能少,指定的路径节点环境。保存,然后就可以测试了。调试和发送包你当然可以选择在构建之后运行测试,但这确实很愚蠢。要测试ts代码,您可以选择使用ts-node。安装npminstall-Dts-node后,将脚本"scripts"添加到package.json:{..."start":"ts-nodebin/yoso.ts"}然后运行??npmrunstart或npmstart,如果你看到输出yoso!,就成功了。那你可以发个包试试。去npm注册一个账号,然后返回npmlogin。发送包前,先将package.json中的version修改为0.0.1,然后每次发送都要修改版本号。0开头的代表测试版本。然后编译ts文件,使用tsc命令修改.npmignore文件,忽略ts文件,但包含d.ts文件。我是这样写的,仅供参考。.idea/.gitignoretsconfig.json#docdoc/#testtest/#source**/*.ts*.ts#definitions!**/*.d.ts!*.d.ts编译完成后,使用npmpublish命令发送包。为了防止发包前忘记编译,建议添加npmhook命令脚本:{..."prepublish":"npmrunbuild","postpublish":"npmrunbuild:clear","build:clear":"find./actions./bin./commands./utils./ui./component-typef-name'*.d.ts'-delete&find./actions./bin./commands./utils./ui./component-typef-not-name'*.ts*'-delete",...}这样每次publish前都会自动编译,编译后的js文件发布后自动清除,这点很省心。发送包成功后,可以全局安装npminstall-gyoso,然后运行命令yoso。如果正常,可以看到输出。但是全局调试不需要每次都发一个包。编译后可以使用npmlink命令创建全局软链接,然后全局使用yoso命令即可。模块化然后我们可以编写模块化命令。这里使用了commander框架,可以先去github主页看例子。我在这里发布最简单的命令。program.command('exec').alias('ex').description('执行给定的远程cmd').option("-e,--exec_mode","使用哪个执行模式").action(function(cmd,options){});如果有多个选项和多个这样的命令,代码放在一起会变得很不清晰。利于维护。因此,要模块化开发,我们需要将每一个命令进行抽象,将每一个执行的动作提取出来。然后在加载文件中,加载命令和相应的动作。参考nest-cli,我们可以写一个简单的init命令。首先在actions中创建一个动作抽象文件。//abstract.action.tsinterfaceInput{name:string;值:布尔值|string;}exportabstractclassAbstractAction{publicabstractasynchandle(inputs?:Input[],options?:Input[]):Promise;}Input的类型可以自己定义,也可以写在外部的类型声明文件中。然后在命令文件夹中创建命令的抽象。//abstract.command.tsimport{CommanderStatic}from'commander';import{AbstractAction}from'../actions/abstract.action';exportabstractclassAbstractCommand{constructor(protectedaction:AbstractAction){}publicabstractload(program:CommanderStatic):void;}然后你可以写一个简单的command继承这个抽象,这里叫做init.command.ts。//init.command.tsimport{Command,CommanderStatic}from"commander";import{AbstractCommand}from"./abstract.command";exportclassInitCommandextendsAbstractCommand{publicload(program:CommanderStatic){program.command("init[tpl][path]").alias("i").description("InitFilesFromGit,example:tplinitdemosrc").action(async(tpl:string,path:string)=>{让输入:any={path,tpl};awaitthis.action.handle(inputs);});}}自己定义类型,最好不要写any。然后在actions中写一个继承自abstract.action.ts的简单动作实例,在动作入口文件index.ts中引入。//init.action.tsimport{AbstractAction}来自“./abstract.action”;exportclassInitActionextendsAbstractAction{publicasynchandle(inputs:any){console.log(inputs.tpl);控制台日志(输入路径);}}至此,command实例和action实例都写好了,接下来要做的就是加载command和对应的action。在命令中创建command.loader.ts并在索引中导入它。//command.loader.tsimport{CommanderStatic}来自“commander”;从“../actions”导入{InitAction};从“./init.command”导入{InitCommand};导出类CommandLoader{publicstaticload(program:CommanderStatic):void{newInitCommand(newInitAction()).load(program);}}最后,修改之前创建的bin/yoso.ts文件。#!/usr/bin/envnodeimport*ascommanderfrom'commander';import{CommanderStatic}from'commander';import{CommandLoader}from'../commands';constbootstrap=()=>{const程序:CommanderStatic=指挥官;program.version(require('../package.json').version);CommandLoader.load(程序);commander.parse(process.argv);如果(!program.args.length){程序。输出帮助();}};自举();大功告成,测试新的init命令npmstartinittpl-pathout-path看是否输出了tpl路径和output路径。成功后可以在action中丰富脚手架的操作。下一篇传送门:从零开始搭建一个节点脚手架工具(二)