1.前言脚手架大家一定不陌生,比如我们经常使用的vue-cli和create-react-app,可以帮助我们快速初始化一个项目,而不用从头开始配置,极大的方便了我们的开发。说到这里,你可能会疑惑,既然市面上有成熟的脚手架,为什么还要自己写脚手架。因为公共脚手架虽然功能强大,但是并不能满足我们实际的开发需求。比如项目中已有的沉淀,项目架构,接口请求的统一处理,换肤,业务组件,eslint配置等,如果要在新项目中使用这些,只能复制粘贴,会有缺点:重复劳动,繁琐浪费时间现有项目分散在各处,很容易漏掉一些东西。项目之间的配置差异可能会被忽略。手动操作总是会出错。在构建新项目时,总是需要时间来解决问题。如果我们自己开发一套脚手架,自定义自己的模板,手工复制粘贴的过程就会转化为cli的自动化过程。您还可以维护不同的模板以满足不同的业务需求。既然要开发一套脚手架,站在巨人的肩膀上显然要容易很多。下面我们就来看看业界知名的脚手架VueCLI是如何实现的。2、VueCLI原理分析VueCLI是一套完整的基于Vue.js的快速开发系统,提供:通过@vue/cli实现的交互式项目脚手架。通过@vue/cli+@vue/cli-service-global进行零配置原型设计。运行时依赖项(@vue/cli-service)是:可升级;建立在具有合理默认值的webpack之上;可通过项目内配置文件进行配置;可通过插件扩展。丰富的官方插件集合,集成了前端生态系统中最好的工具。用于创建和管理Vue.js项目的全图形用户界面。2.1全局的vue执行命令存放在哪里?以mac为例,使用命令wherevue查找vue命令所在位置。找到位置后,查看目录分析源码。2.2vue命令在哪里注册?在源码目录下找到package.json,我们会看到如下代码:可以看到bin字段指定了可执行文件的命令名和可执行文件的路径。npm安装依赖时,如果在依赖的package.json中指定了bin信息,则会创建一个全局软链接指向该命令对应的可执行文件。具体可以参考npm官方文档:package.json中bin的使用说明。2.3依赖包分析包名用途Commander一个完整的node.js命令行解决方案,Commander负责将参数解析为选项和命令参数shelljs用于执行shell命令Inquirer通用交互式命令行用户界面的集合设置终端字符串样式2.4脚手架有什么作用?2.4.1HTML和静态资源html文件是一个模板,将由html-webpack-plugin处理。资源链接在构建过程中自动注入。此外,VueCLI还会自动注入资源提示(预加载/预取、清单和图标链接(当使用PWA插件时)和资源链接到构建过程中处理的JavaScript和CSS文件。2.4.2CSS相关VueCLI生成的项目支持PostCSS、CSSModules和预处理器包括Sass、Less、Stylus,创建项目时可以选择预处理器2.4.3webpack相关VueCLI基于webpack构建,具有合理的默认配置,可通过配置进行配置2.4.4模式和环境变量模式是VueCLI项目中的一个重要概念,默认情况下,一个VueCLI项目有三种模式:开发模式使用vue-cli-serviceservetest模式用于vue-cli-servicetest:unitproduction模式,用于vue-cli-servicebuild和vue-cli-servicetest:e2e可以通过--mode选项参数进行命令行覆盖默认模式。2.4.5卜ildtarget当你运行vue-cli-servicebuild时,你可以通过--target选项指定不同的构建目标。它允许您根据不同的用例从相同的源代码生成不同的构建。通过以上对VueCLI的分析,我们对脚手架工具提供的构建集成能力有了一个大概的了解。这有助于我们在使用特定工具时快速定位问题的边界。我们自己设计脚手架的时候,也可以参考借鉴。可以应用于我们业务的是:通过命令行与用户交互,根据用户的选择生成相应的文件,实现零配置原型开发需要修改的部分是:基于vite构建,配合合理默认配置;预定义业务模板,根据用户选择生成业务模板基础支持:内置HTML和静态资源处理css预处理器内置vite配置,内置三种模式可直接修改vite配置文件test、pre、pro,并生成相应的配置文件。根据上面的总结,让我们一步步写自己的脚手架。首先,通过命令行和User交互,然后我们需要有一个可执行的命令名,也就是脚手架的名字,这里我们称之为dt-fe-cli3。脚手架实现3.1命令行工具编写3.1.1初始化项目我们的脚手架名为dt-fe-cli,创建一个dt-fe-cli文件夹,执行npminit-y初始化仓库,生成package.json文件。在dt-fe-cli文件夹下创建一个bin文件夹,在里面创建一个cli.mjs文件。这个文件作为我们搭建脚手架的入口,需要配置在package.json的bin字段中。{"name":"@auto/dt-fe-cli","version":"0.0.1","bin":{"dt-fe-cli":"bin/cli.mjs"}}所以我们脚手架的入口已经准备好了,下面继续写脚手架的功能。3.1.2指令dt-fe-cli是一个全局指令,同时提供了很多指令。dt-fe-cli--version可以查看dt-fe-cli的版本dt-fe-cli--help可以查看帮助文档dt-fe-clicreatexxx可以创建项目...3.1.3create命令create接受一个项目名作为参数,这里还提供了额外的选项-f、--force。该选项表示如果本地已经存在同名文件夹是否覆盖。命令行方案需要依赖第三方库commanderimportcreatefrom'../lib/create.mjs'program.command('create').description('createanewprojectpoweredbydt-fe-cli').option("-f,--force","如果存在则覆盖目标目录").action((projectName,options)=>{create(projectName,options)})3.1.4创建方法设计与实现执行完create命令后,如何创建工程呢?我们公司的项目都是托管在内部的gitlab上,所以直接用gitclone拉取模板项目。这里需要依赖第三方库shelljs,所以这里需要先判断git是否存在,不提示退出。我们之前还写了一个附加选项,用来表示如果本地已经存在同名文件夹是否覆盖。如果没有这个选项,则需要交互式查询,这需要第三方图书馆查询器。create.mjs:从'chalk'导入粉笔从'fs-extra'导入fse从'shelljs'导入shelljs从'path'导入路径从'inquirer'异步函数创建(projectName,options){consttargetDirectory=path。join(process.cwd(),projectName)try{//判断git是否存在,不存在提示退出if(!shelljs.which('git')){console(chalk.red('Sorry,dt-fe-cli需要git'));return}constisExist=awaitfse.pathExists(targetDirectory)//判断目录下是否存在同名文件夹if(isExist){if(options.force){awaitfse.remove(targetDirectory);}else{const{isOverwrite}=awaitnewinquirer.prompt([{name:"isOverwrite",//对应返回值type:"list",message:"目标目录已存在。选择一个动作:",choices:[{名称:“覆盖”,值:真},{名称:“取消”,值:假},],},]);//删除同名文件夹if(isOverwrite){console.log('删除现有目录...')awaitfse.remove(targetDirectory);}else{返回;}}}//项目类型const{projectType}=awaitnewinquirer.prompt([{name:"projectType",//与返回值对应type:"list",message:"Pleaseselectprojecttype:",choices:[{名称:“pc”,值:'pc'},{名称:“h5”,值:'h5'},],},]);constPROJECT_MAP={pc:'pc.git',h5:'h5.git'}//安装依赖项目shelljs.exec(`gitclone${PROJECT_MAP[projectType]}${projectName}`,async(code,stdout,stderr)=>{if(code===0){progress.start()try{//删除原有.gitawaitfse.remove(path.join(process.cwd(),projectName,'.git'))}catch(error){console.log(error)}progress.succeed()console.log(`\r\n成功创建项目${chalk.cyan(项目名称)}`);console.log(`\r\ncd${chalk.cyan(projectName)}`);console.log("gitinit");console.log("pnpm安装");console.log("pnpmdev");}})}catch(error){console.log(error);}}3.1.5node版本检查执行create命令后,创建的项目会去gitlab拉取代码,下载我们自定义的模板,目前我使用的模板都是Vite3创建的,Vite3需要Node.js版本14.18+,16+,所以在使用脚手架的时候可以先检查当前的Node.js版本是否合规,不合规会抛出异常.当前依赖的Node.js版本需要在package.json的engines字段中配置,判断当前Node.js版本是否符合第三方库semverpackage.json:{"engines":{"node":">=14.18.0"},}cli.mjs:import{readFile}from'fs/promises'importsemverSatisfiesfrom'semver/functions/satisfies.js'const{engines:{node:requiredVersion},version}=JSON。parse(awaitreadFile(newURL('../package.json',import.meta.url)))functioncheckNodeVersion(wanted,id){if(!semverSatisfies(process.version,wanted,{includePrerelease:true})){console.log(chalk.red('你正在使用Node'+process.version+',但是这个版本的'+id+'需要Node'+wanted+'。\n请升级你的Node版本。'))process.exit(1)}}checkNodeVersion(requiredVersion,'dt-fe-cli')至此,脚手架的基本功能已经开发完成,剩下的就是我们的工程模板了。3.2模板设计支持功能3.2.1TypeScript使用Vite3Build,Vite3自然支持导入.ts文件3.2.2自动打包上传CDNconst{execSync}=require('child_process');const{loadEnv}=require('vite')常量环境=process.argv[2]const{VITE_BASE_URL}=loadEnv(env,process.cwd(),'')constprefix=`${env}${VITE_BASE_URL}`execSync(`vitebuild--mode${env}--base=https://cdn.com/${prefix}`);uploadCDN({Dir:`dist/assets`,Prefix:prefix})3.2.3commitchecknpminstallhusky--save-devnpmpkgsetscripts.prepare="huskyinstall"npmrunpreparepxhuskyadd.husky/pre-commit"npmrunlint"gitadd.husky/pre-commit3.2.4eslint验证ESLint通用配置部分不再赘述。这里介绍下我们业务中自定义ESLint插件eslint验证。每个人都熟悉它。市面上也有很多eslint插件。但是随着项目的迭代开发,使用现有的eslint插件已经不能满足我们团队的编码标准了。你需要自己创建一个插件,并将其集成到cli的模板中。创建插件开始创建插件的最简单方法是使用Yeoman生成器。生成器将引导您设置插件的骨架npmi-gyogenerator-eslintyoeslint:plugin上面的命令将生成以下目录。├──README.md├──lib│├──index.js│└──rules├──package.json└──tests└──lib└──规则插件可以在ESLint中使用额外的规则。为此,插件必须导出一个规则对象,该对象包含规则ID到规则的键值映射。举个简单的例子,我们想创建一个不允许console.log的规则。创建一条规则yoeslint:rule该命令会在lib/rules文件夹下新建一个js文件,一条规则对应一个可导出节点模块"usestrict";//--------------------------------------------------------//规则定义//-----------------------------------------------------/**@type{import('eslint').Rule.RuleModule}*/module.exports={meta:{type:"suggestion",docs:{description:"disallowunnecessarysemicolons",推荐:true,url:"https://eslint.org/docs/rules/no-extra-semi"},fixable:"code",schema:[]//无选项},create(context){return{//回调函数};}};上面的代码是一个常规源代码文件的基本格式。一个常规的源文件输出一个对象,它由两部分组成:meta和create。meta(object)包含规则的元数据,如规则类型、文档、可接受参数的schema等create(function)返回一个对象,包含ESLint遍历JavaScript代码的抽象语法树AST(ESTree定义的ASTree),用于访问节点的方法。核心其实在于create方法。要想知道create方法怎么写,首先要了解它的原理,即ESLint是如何分析我们写的代码的?这个相信大家都知道,没错,就是AST(抽象语法树(AbstractSyntaxTree))插件原理ESLint解析器将代码转换成ESLint可以求值的抽象语法树。默认情况下,ESLint使用内置的Espree解析器,兼容标准的JavaScript运行时和版本,然后拦截检测是否符合我们规定的写法,最后让它显示错误、警告或正常通过。ESLint的核心是规则,而定义规则的核心是使用AST来进行校验,那么让我们看看代码AST会是什么样子。从上图可以看出,console.log对应AST中的类型为ExpressionStatement(表达式语句),表达式类型为CallExpression(调用表达式),被调用者类型为MemberExpression(成员表达式),被调用对象名为console,属性名为log,根据以上信息,我们可以改进create方法,写规则“usestrict”;//---------------------------------------------------//规则定义//---------------------------------------------------/**@type{import('eslint').Rule.RuleModule}*/module.exports={meta:{type:"suggestion",fixable:"code",schema:[],//没有选项},create(context){return{//key是选择器'CallExpressionMemberExpression':(node)=>{const{property,object}=node;//如果console.log在AST中匹配,使用context.report()发出警告或错误if(object.name==='console'&&property.name==='log'){context.report({node,message:'console.log被禁止。'});}}};}};这里写了一个包含规则(禁止使用console.log)的ESLint插件,然后将项目发布到npm平台,可以在项目模板中下载使用。后面这篇文章介绍如何从零开始写自己的脚手架,并且可以根据不同的业务场景区分模板,把已有的业务积累沉淀进去。以上就是本次分享的全部内容,希望对大家有所帮助^_^作者简介马春健,主机厂事业部,技术部2021年加入汽车之家,目前在主机厂工作工厂事业部-技术部-数字技术与系统团队-前端开发组,主要负责数字前端业务、前端前端技术探索等工作