1. 前言
脚手架大家一定都不陌生,比如我们经常使用的 vue-cli、create-react-app,它可以帮助我们快速的初始化一个项目,无需从零配置,极大的方便我们的开发。到这里你可能会疑惑,既然市面上有成熟的脚手架,为什么需要写一个属于自己的脚手架呢。因为公共脚手架虽然强大,但并不能满足我们的实际开发需求。
例如项目中已有的沉淀,项目架构、接口请求的统一处理、换肤、业务组件、eslint配置等,这些想要用到新项目中,只能通过复制粘贴,会存在以下弊端:
重复性劳动,繁琐且浪费时间 已有项目沉淀分散在各处,很容易有所遗漏 项目间的配置差异很可能会被忽略 人工操作永远都有可能犯错,建新项目时,总要花时间去排错
如果我们自己开发一套脚手架,定制自己的模板,复制粘贴的人工流程就会转换为 cli 的自动化流程, 还可以通过维护不同的模板以适应不同业务需求。既然要开发一套脚手架,站在巨人肩膀上显然省事多了,我们先来看看业界知名脚手架Vue CLI是如何实现的。
2.Vue CLI原理分析
Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,提供:
通过 @vue/cli 实现的交互式的项目脚手架。 通过 @vue/cli + @vue/cli-service-global 实现的零配置原型开发。 一个运行时依赖 (@vue/cli-service),该依赖: 可升级; 基于 webpack 构建,并带有合理的默认配置; 可以通过项目内的配置文件进行配置; 可以通过插件进行扩展。 一个丰富的官方插件集合,集成了前端生态中最好的工具。
一套完全图形化的创建和管理 Vue.js 项目的用户界面。
2.1
全局 vue 执行命令存放在哪里
2.2
vue命令是从哪里注册的
找到源码目录中的package.json,我们会看到如下代码:
2.3
依赖包分析
包名 | 用途 |
---|---|
commander | 完整的 node.js 命令行解决方案, Commander 负责将参数解析为选项和命令参数 |
shelljs | 用来执行shell命令 |
inquirer | 通用交互式命令行用户界面的集合 |
semver | 语义化版本控制 |
chalk | 设置终端字符串样式 |
2.4
脚手架都做了哪些事情
2.4.1 HTML 和静态资源
html文件是一个会被 html-webpack-plugin 处理的模板。在构建过程中,资源链接会被自动注入。另外,Vue CLI 也会自动注入 resource hint (preload/prefetch、manifest 和图标链接 (当用到 PWA 插件时) 以及构建过程中处理的 JavaScript 和 CSS 文件的资源链接。
2.4.2 CSS 相关
Vue CLI生成项目支持 PostCSS、CSS Modules 和包含 Sass、Less、Stylus 在内的预处理器,你可以在创建项目的时候选择预处理器。
2.4.3 webpack 相关
Vue CLI基于 webpack 构建,并带有合理的默认配置,可以通过项目内的配置文件进行配置,还可以通过插件进行扩展。
2.4.4 模式与环境变量
模式是 Vue CLI 项目中一个重要的概念。默认情况下,一个 Vue CLI 项目有三个模式:
development 模式用于 vue-cli-service servetest 模式用于 vue-cli-service test:unitproduction 模式用于 vue-cli-service build 和 vue-cli-service test:e2e你可以通过传递 –mode 选项参数为命令行覆写默认的模式。
2.4.5 构建目标
当你运行 vue-cli-service build 时,你可以通过 –target 选项指定不同的构建目标。它允许你将相同的源代码根据不同的用例生成不同的构建。
通过以上对Vue CLI 的分析,我们就对脚手架工具提供的构建集成能力有了一个大概的了解。这有助于我们在使用具体工具时快速定位问题的边界,当我们自己设计脚手架的时候,我们也可以参照和借鉴,可以适用于我们业务的有:
通过命令行与用户交互 根据用户的选择生成对应的文件,实现的零配置原型开发
基于 vite 构建,并带有合理的默认配置; 预定义业务模板,根据用户选择生成 业务模板基础支持: HTML 和静态资源处理 内置css预处理器 内置vite配置,可以直接修改vite配置文件 内置test、pre、pro三种模式,并生成对应的配置文件
按照上述总结,让我们一步一步编写自己的脚手架吧,首先是通过命令行与用户交互,那么我们需要有一个可执行命令的名字,也是脚手架的名字,这里我们就叫做dt-fe-cli
3.脚手架实现
3.1
命令行工具编写
3.1.1 初始化项目
我们的脚手架叫做dt-fe-cli,创建dt-fe-cli文件夹,执行npm init -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-cli create xxx可以创建一个项目 …
3.1.3 create命令
create接受一个项目名作为参数,这里还提供了额外选项-f, –force,此选项代表如果本地已经存在同名文件夹,是否覆写。命令行解决方案需要依赖第三方库commander
importcreatefrom'../lib/create.mjs'program.command('create').description('createanewprojectpoweredbydt-fe-cli').option("-f,--force","overwritetargetdirectoryifitexists").action((projectName,options)=>{create(projectName,options)})
3.1.4 create方法设计与实现
执行create命令后,如何创建项目呢,我们公司的项目都是托管在内部gitlab上面的,所以直接使用git clone去拉取模板项目,这里需要依赖第三方库shelljs,那么这里就需要首先判断git是否存在,不存在提示并退出。之前我们还写了一个额外选项,用来表示如果本地已经存在同名文件夹,是否覆写。若没有此选项,还需要交互式的询问,这里需要依赖第三方库inquirer。
create.mjs:
importchalkfrom'chalk'importfsefrom'fs-extra'importshelljsfrom'shelljs'importpathfrom'path'importinquirerfrom'inquirer'asyncfunctioncreate(projectName,options){consttargetDirectory=path.join(process.cwd(),projectName)try{//判断是否存在git,不存在则提示并退出if(!shelljs.which('git')){console(chalk.red('Sorry,dt-fe-clirequiresgit'));return}constisExist=awaitfse.pathExists(targetDirectory)//判断目录下是否存在同名文件夹if(isExist){if(options.force){awaitfse.remove(targetDirectory);}else{const{isOverwrite}=awaitnewinquirer.prompt([{name:"isOverwrite",//与返回值对应type:"list",message:"Targetdirectoryalreadyexists.Pickanaction:",choices:[{name:"Overwrite",value:true},{name:"Cancel",value:false},],},]);//移除同名文件夹if(isOverwrite){console.log('removeexistingdirectory...')awaitfse.remove(targetDirectory);}else{return;}}}//项目类型const{projectType}=awaitnewinquirer.prompt([{name:"projectType",//与返回值对应type:"list",message:"Pleaseselectprojecttype:",choices:[{name:"pc",value:'pc'},{name:"h5",value:'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\nSuccessfullycreatedproject${chalk.cyan(projectName)}`);console.log(`\r\ncd${chalk.cyan(projectName)}`);console.log("gitinit");console.log("pnpminstall");console.log("pnpmdev");}})}catch(error){console.log(error);}}
3.1.5 node版本检查
package.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('YouareusingNode'+process.version+',butthisversionof'+id+'requiresNode'+wanted+'.\nPleaseupgradeyourNodeversion.'))process.exit(1)}}checkNodeVersion(requiredVersion,'dt-fe-cli')
到这里,脚手架的基本功能就已经开发完毕了,剩下的就是我们的项目模板了
3.2
模版设计支持功能
3.2.1 TypeScript
使用 Vite3 构建,Vite3 天然支持引入 .ts 文件
3.2.2 打包自动上传CDN
const{execSync}=require('child_process');const{loadEnv}=require('vite')constenv=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.3 commit 校验
npminstallhusky--save-devnpmpkgsetscripts.prepare="huskyinstall"npmrunpreparenpxhuskyadd.husky/pre-commit"npmrunlint"gitadd.husky/pre-commit
3.2.4 eslint校验
ESLint通用配置的部分这里就不再赘述了,这里介绍一下我们业务里面自定义的ESLint插件。eslint校验大家都很熟悉,市面上也有很多eslint插件,但随着项目不断迭代发展,我们团队的编码规范使用现有的eslint插件已经无法满足了,需要自己创建插件,并融入到cli的模板当中。
► 创建插件
开始创建插件的最简单方法是使用 Yeoman 生成器。生成器将指导您设置插件的骨架
npmi-gyogenerator-eslintyoeslint:plugin
以上命令会生成如下目录
.├──README.md├──lib│├──index.js│└──rules├──package.json└──tests└──lib└──rules
插件可以在 ESLint 中使用的额外规则。为此,插件必须导出一个包含规则 ID 到规则的键值映射的规则对象,举个简单的例子,我们想创建一条不允许使用console.log的规则
► 创建规则
yoeslint:rule
此命令会在lib/rules文件夹下创建一个新的js文件,一个规则对应一个可导出的 node 模块
"usestrict";//-------------------------------------------------------//RuleDefinition//-------------------------------------------------------/**@type{import('eslint').Rule.RuleModule}*/module.exports={meta:{type:"suggestion",docs:{description:"disallowunnecessarysemicolons",recommended:true,url:"https://eslint.org/docs/rules/no-extra-semi"},fixable:"code",schema:[]//nooptions},create(context){return{//callbackfunctions};}};
上面这段代码是一个规则的源码文件的基本格式,一个规则的源文件输出一个对象,它由 meta 和 create 两部分组成。
⚫meta(对象)包含规则的元数据,如规则类型、文档、可接受参数的schema等等
⚫create (function) 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST (ESTree 定义的 AST) 时,用来访问节点的方法。
核心其实在于create方法,我们若想知道如何编写create方法,首先要明白其原理,那就是 ESLint 是如何分析我们所编写的代码呢?相信大家对此也都有所了解,没错,就是AST (Abstract Syntax Tree(抽象语法树))
► 插件原理
ESLint 解析器将代码转换为 ESLint 可以评估的抽象语法树。默认情况下,ESLint 使用内置的 Espree 解析器,它与标准的 JavaScript 运行时和版本兼容,然后去拦截检测是否符合我们规定的书写方式,最后让其展示报错、警告或正常通过。ESLint 的核心就是规则(rules),而定义规则的核心就是利用 AST 来做校验,那就让我们看一下代码 AST 中会表现为什么样子。
► 编写规则
"usestrict";//-------------------------------------------------------//RuleDefinition//-------------------------------------------------------/**@type{import('eslint').Rule.RuleModule}*/module.exports={meta:{type:"suggestion",fixable:"code",schema:[],//nooptions},create(context){return{//key是selector'CallExpressionMemberExpression':(node)=>{const{property,object}=node;//如果在AST中匹配到了console.log,就用context.report()来发布警告或错误if(object.name==='console'&&property.name==='log'){context.report({node,message:'console.logisforbidden.'});}}};}};
至此,包含一条规则(禁止使用console.log)的 ESLint 插件就编写完成了,接下来将此项目发布到npm平台就可以在项目模板中下载使用了
最后
本文介绍了如何从零编写一个我们自己的脚手架,并且可以根据不同业务场景区分模版,把业务已有的积累沉淀进去,以上便是本次分享的全部内容,希望对你有所帮助 ^_^
作者|马春键
本文来自博客园,作者:古道轻风,转载请注明原文链接:https://www.cnblogs.com/88223100/p/Frontend-Engineering-Practice—Developing-Enterprise-Level-CLI.html