咱们业务中能够经过Vue-cli脚手架快速生成vue项目,一样咱们也能够开发一款cli脚手架用于快速生成咱们平常提炼出来的业务基础模型/架构。本文将详细讲解脚手架如何开发,所涉及到的技术细节和坑以及各类第三方包的讲解,确保即便是小白同窗也能够照着作出来本身的cli。html
装逼大法!提高逼格!!升职加薪!!!赢娶白富美!!!!你还在等什么?快快开始吧~~~前端
言归正传,首先思考一下咱们的脚手架要帮助咱们作什么事情?好比,这里咱们就实现一个vta-cli
,经过在终端运行一个vta create my-app
就能够初始化咱们日出提炼出来的一套Vue+Ts+ElementUi
的RBAC系统的基础项目架构。有了这个目标,咱们就能够拆解要实现的步骤了:vue
vta
vta create my-app
命令运行后,检查当前文件名的存在与否状况👇下面咱们就一步一步讲解具体实现过程,跟上队别掉队哈~~~node
# 终端建立项目根文件夹并进入到根文件夹 mkdir vta-cli && cd vta-cli # 建立bin和src文件夹 mkdir bin src # bin下建立init.js做为脚本的入口文件 cd bin && touch init.js # 并在init.js中键入以下内容: #!/usr/bin/env node console.log('Hello,my bin!') # 初始化npm的包管理文件, 根目录下执行 # 该命令会询问你不少配置参数,若是不想询问直接在后面加-y参数便可 npm init 复制代码
项目基本的目录文件夹出来了,说下具体的目录做用,bin文件夹用于存放咱们的命令入口文件,init.js做为入口文件(命名随你),src做为咱们真正实现脚本命令逻辑的地方:linux
package.json
文件,在里面添加咱们的脚本命令。打开咱们的package.json
文件,在里面添加bin
字段:{ "bin": { "vta": "bin/init.js" }, } 复制代码
这就是咱们定义了vta这个能够在终端运行的脚本命令,即运行vta
这个命令的时候,程序会去运行咱们配置的bin/init.js
这个脚本文件。其实,根据npm的机制,当install
一个包的时候,会自动去查询其定义的bin命令,并把他添加到node_modules/.bin
文件中去,做为shell的命令能够去执行。所以当你的包安装到局部的项目中,那么其bin中的命令就是局部可运行的,安装到全局中则变成了全局能够运行的命令。ios
说明一下,并非必定要是js文件,其实在linux系统中一切皆文件,是没有后缀名的规定的,至少为了让“人”好识别而已。git
重点强调一下:init.js文件的第一行,必定是第一行,咱们添加了#!/usr/bin/env node
代码,是指定了咱们脚本的运行环境,和自定在咱们运行vta命令的时候添加了node命令做为前缀,即实际运行的是node vta
。github
# 终端运行命令(需在当前项目根目录下) npm link 复制代码
注意,npm link是当咱们当前包link到本地的全局中,就比如如咱们安装依赖时使用了-g
参数把一些包装到了全局环境同样,是用来方便咱们本地开发时测试的,他可让咱们开发的时候自动热更新。若是不清楚npm link的
小伙伴,能够去npm官网查查npm link的
用法再继续往下学习。vue-cli
可是,我想说的时候,不少小伙伴在这块可能会踩坑:typescript
package.json
的配置中把node_modules
等无关的文件夹去掉(或者指定咱们须要的),也能够经过.gitignore
等配置文件忽略掉也能够,或者.npmrc
等。在哪里设置均可以,由于npm配置取值是有一套前后顺序的规则,有兴趣的话能够移步npm文档查阅。这里演示一下如何在package.json
文件的配置:{ "files": [ "./bin", "./src" ], } 复制代码
咱们经过在package.json文件中指定files文件夹目录,即告诉npm咱们实际应该包含的真正文件有哪些,好比咱们只须要bin和src
文件夹,一些默认的文件像package.json
啊,其余的一些基础配置文件啊,即便你不添加,也会被默认包含进来的。这也是当咱们把这个包发布到npm
所须要配置的,也就是须要哪些文件发布到npm仓库上。
注意,也能够经过排除的字段,exclude
。可是,不少时候指定咱们须要哪些文件,可能更为方便哦!再强调一遍,node_modules必定要排除掉,否则npm link会巨慢并且会失败的几率大,当心踩坑~~
# 终端运行 vta # 那么脚本执行后,便会看到终端的输出 # 说明脚本执行成功了 复制代码
再次强调,init.js首行必定要添加沙棒,以下:
#!/usr/bin/env node console.log('运行测试') 复制代码
commander.js
是nodejs命令行界面的一个完整解决方案。能够帮助咱们定义各类命令行命令/参数等等。好比咱们想定义create命令啊,或者-v做为版本号查询的参数等等。那就先看下怎么使用吧:
cnpm install commander -S
复制代码
// 在init.js中引入 const { Command } = require('commander'); // 导入当前根目录下的package.json文件, // 为了获取对应的字段值,好比版本version const package = require('../package'); // 初始化 const program = new Command(); 复制代码
// // 如此, program .version(package.version, '-v, --version', 'display version for vta-cli') .usage('<command> [options]'); // 复制代码
经过调用version方法,定义命令行命令版本的功能,咱们即可以在命令行输入vta -v
获得当前的版本信息。
调用usage方法,是定义的咱们的辅助命令(help)的提示的文案标题,相似于定义table的表头的感受,以下图,当咱们输入vta -h时,就是定义的蓝色框框内展现的部分:
注意,这里version方法
的第三个参数,是咱们定义的说明内容,如上图的红色部分。help
默认也是这个值
/** * 定义vta的参数 */ program .option('-y, --yes', 'run default action') .option('-f, --force', 'force all the question'); /** * 能够经过判断,当用户输入了对应的这些参数时, * 咱们能够作一些操做: */ if (program.force) { // do something.. } 复制代码
经过option方法,定义咱们的命令行参数,比如vta -f
,等同于vta --force
。注意,第一个参数是定义命令行参数,包含一个短的名称(1个字符)和一个长的名称,不能多了。第二个参数,就是定义的说明内容。注意,判断部分的代码,只能使用长的名称,不能判断短的,例如program.f
。
建立子命令是重要的一部分,好比咱们使用vue create my-app
建立项目时, create
就是vue命令的子命令,my-app
是命令参数。这里咱们也定义一个子命令:
/** * 调用command方法,建立一个create命令, * 同时create命令后面必须跟一个命令参数 * 若是你在终端运行vta create不加名称,则会报错提示用户 */ program.command('create <name>') // 定义该命令的描述 .description('create a vta template project') // 为该命令指定一些参数 // 最后咱们均可以解析到这些参数,而后根据参数实现对应逻辑 .option('-f, --force', '忽略文件夹检查,若是已存在则直接覆盖') /** * 最后定义咱们的实现逻辑 * source表示当前定义的name参数 * destination则是终端的cmd对象,能够从中解析到咱们须要的内容 */ .action((source, destination) => { /** * 好比咱们这里把实现逻辑放在了另外一个文件中去实现, * 方便代码解耦, * 由于destination参数比较杂乱,其实仍是在此处先解析该参数对应再传入使用吧 * 能够定义一个解析的工具函数 */ new CreateCommand(source, destination) }); 复制代码
如图,看下destination
对象究竟是什么?仍是满多的内容。咱们须要关注的就是红色框框的这部分,这里就是咱们定义的该命令的全部参数的列表,咱们变量该列表,取图中蓝色的部分的值,解决--
后面的部分,而后做为key到整个cmd对象中取匹配,其值就是用户输入的参数的值。
好比,可能会定义一个解析的工具函数:
/** * parseCmdParams * @description 解析用户输入的参数 * @param {} cmd Cammander.action解析出的cmd对象 * @returns { Object } 返回一个用户参数的键值对象 */ exports.parseCmdParams = (cmd) => { if (!cmd) return {} const resOps = {} cmd.options.forEach(option => { const key = option.long.replace(/^--/, ''); if (cmd[key] && !isFunction(cmd[key])) { resOps[key] = cmd[key] } }) return resOps } 复制代码
上述的解析方法实现方式和咱们vue-cli的差很少。
/** * 切记parse方法的调用,必定要program.parse()方式, * 而不是直接在上面的链式调用以后直接xxx.parse()调用, * 否则就会做为当前command的parse去处理了,从而help命令等都与你的预期不符合了 */ try { program.parse(process.argv); } catch (error) { console.log('err: ', error) } 复制代码
最后必定要解析,不解析是拿不到对应参数program.parse(process.argv)
,也就是不会执行对应的命令等行为的。切记!切记!切记!!!更详细的命令请查询commander文档。
从上面的步骤咱们能够看出,咱们已经定义好了vta create <name>
的命令了,即当咱们运行vta create my-app
命令的时候,就会初始化咱们定义的CreateCommand
类了。下面咱们看看入如何实现这个逻辑:咱们首先建立src/command/CreateCommand.js
这个文件来实现咱们的逻辑:
/** * class 项目建立命令 * * @description * @param {} source 用户提供的文件夹名称 * @param {} destination 用户输入的create命令的参数 */ class Creator { constructor(source, destination, ops = {}) { this.source = source this.cmdParams = parseCmdParams(destination) this.RepoMaps = Object.assign({ repo: RepoPath, // 配置文件中放置的远程地址常量 temp: path.join(__dirname, '../../__temp__'), target: this.genTargetPath(this.source) }, ops); this.gitUser = {}; this.spinner = ora(); this.init(); } // 其余实例方法 // ... } // 最终导出这个class module.exports = Creator; 复制代码
咱们看下这个构造函数咱们用来作了什么事情,首先就是把实例化时传进来的参数赋值给this对象,供后面其余实例方法中去使用。而后定义了RepoMaps
属性设置咱们的一些基础参数,像项目模板的地址repo
、咱们本地cli项目内部临时存放的项目模板的地址temp
、和最终咱们须要把项目安装到的目标地址taregt
。由于项目最终会安装到终端运行的地址下的位置,而你的脚手架包是被安装在其余地址的。
而后定义了gitUser用于存放用户的git信息,后面会经过自动执行命令获取相关的信息,而后最后咱们会把信息塞到package.json文件中。
this.spinner = ora();
就是实例化一个菊花图,当咱们在执行命令的时候能够调用this.spinner方法进行菊花转呀转!
下面咱们来实现这个init初始化的方法吧:
// 初始化函数 async init() { try { // 检查目标路径文件是否正确 await this.checkFolderExist(); // 拉取git上的vue+ts+ele的项目模板 // 存放在临时文件夹中 await this.downloadRepo(); // 把下载下来的资源文件,拷贝到目标文件夹 await this.copyRepoFiles(); // 根据用户git信息等,修改项目模板中package.json的一些信息 await this.updatePkgFile(); // 对咱们的项目进行git初始化 await this.initGit(); // 最后安装依赖、启动项目等! await this.runApp(); } catch (error) { console.log('') log.error(error); exit(1) } finally { this.spinner.stop(); } } 复制代码
从上面代码注释能够看到,咱们的init方法,就是把一系列操做一次调用执行便可。最后先看一下配置文件吧:
exports.InquirerConfig = { // 文件夹已存在的名称的询问参数 folderExist: [{ type: 'list', name: 'recover', message: '当前文件夹已存在,请选择操做:', choices: [ { name: '建立一个新的文件夹', value: 'newFolder' }, { name: '覆盖', value: 'cover' }, { name: '退出', value: 'exit' }, ] }], // 重命名的询问参数 rename: [{ name: 'inputNewName', type: 'input', message: '请输入新的项目名称: ' }] } // 远程Repo地址 // 你们开发阶段,若是没有本身的项目,能够先调用个人这个地址练习 // 也能够随便一个地址练习均可以 exports.RepoPath = 'github:chinaBerg/vue-typescript-admin' 复制代码
后面咱们将看看这一系列方法该如何实现?
首先介绍一下咱们的小菊花吧!咱们在执行各类操做的时候,好比拉模板数据等等,都是会有必定等待实际的,那么这个等待过程,咱们能够在终端有个小菊花转转转,这样 会给用户更好的体验,让用户知道当前脚本在执行加载,如图(最左侧有个小菊花在转转转~~~):
ora就是这一款终端使用的菊花图工具,下面看看如何使用吧!
cnpm install ora -S
复制代码
const ora = require('ora'); // ora参数建立spinner文字内容 // 也能够传递一个对象,设置spinner的周期、颜色等 // 调用start方法启动,最终返回一个实例 const spinner = ora('Loading start') // 开启菊花转转 spinner.start(); // 中止 spinner.stop() // 设置文案,后者菊花的color spinner.text = '正在安装项目依赖文件,请稍后...'; spinner.color = 'green'; // 显示转成功的状态 spinner.succeed('package.json更新完成'); 复制代码
注意,文案的颜色,仍是得靠chalk辅助。后面会介绍chalk。上个图片演示一下实际的运用:
更多详细的用户请查阅ora文档。
chalk是一款可让咱们的控制台打印出各类颜色/背景的内容的工具,由此咱们能够鲜明的区分各类提示内容,以下图(就问你骚不骚???):
# 终端运行 cnpm i chalk -S 复制代码
const chalk = require('chalk'); // 好比,这里定义一个log对象 exports.log = { warning(msg = '') { console.warning(chalk.yellow(`${msg}`)); }, error(msg = '') { console.error(chalk.red(`${msg}`)); }, success(msg = '') { console.log(chalk.green(`${msg}`)); } } 复制代码
好比,上面咱们封装了最简单的log方法,用于打印各类类型的信息时展现带颜色的内容。还有一点,咱们说一下上面提到了的如何配合ora使用吧:
const chalk = require('chalk'); const ora = require('ora'); const spinner = ora('Loading start') // 开启菊花转转 spinner.start(chalk.yellow('打印一个yellow色的文字')); 复制代码
用法比较简单,很少说了,更多用法仍是查阅文档吧!
在详细说明各个步骤实现的方式以前,咱们先说一下在cli中使用的文件操做的库。node自己有fs操做,那么咱们为何还要引入fs-extra库呢?是由于他彻底能够用来取代fs的库,省去了mkdirp``rimraf``ncp
等库等安装引入。用于拷贝、读取、删除等文件操做,并且提供了更多的功能等等。
cnpm install fs-extra
复制代码
具体的api的方法,请查阅文档fs-extra,后面讲解各个步骤具体实现的时候也会说起到。
到了咱们运行vta create my-app
的时候了,这时候咱们就要考虑了,若是当前位置已经存在了同名的文件夹,那么咱们确定是不能直接覆盖的,而是要给用户选择,好比覆盖、从新建立一个新的文件夹、退出,以下图:
而后根据用户的不一样选择做出对于的操做。下面咱们看这个文件夹检查的具体实现:
checkFolderExist() { return new Promise(async (resolve, reject) => { const { target } = this.RepoMaps // 若是create附加了--force或-f参数,则直接执行覆盖操做 if (this.cmdParams.force) { await fs.removeSync(target) return resolve() } try { // 不然进行文件夹检查 const isTarget = await fs.pathExistsSync(target) if (!isTarget) return resolve() const { recover } = await inquirer.prompt(InquirerConfig.folderExist); if (recover === 'cover') { await fs.removeSync(target); return resolve(); } else if (recover === 'newFolder') { const { inputNewName } = await inquirer.prompt(InquirerConfig.rename); this.source = inputNewName; this.RepoMaps.target = this.genTargetPath(`./${inputNewName}`); return resolve(); } else { exit(1); } } catch (error) { log.error(`[vta]Error:${error}`) exit(1); } }) } 复制代码
具体讲解:
vta create my-app
的时候有没有在后面加-f
的参数,若是添加了参数则是告诉咱们忽略检查直接日后走,就是默认覆盖的操做。经过调用fs.removeSync(target);
方法进行移除须要覆盖的文件;await fs.pathExistsSync(target)
逻辑进行判断当前文件夹名称是否已经存在,若是不存在则resolve告诉程序执行文件夹检查成功以后的程序。说到命令行交互,就要提到一个比较程序的库inquirer,这是一个用于node环境下进行命令行交互的库,支持单选、多选、用户输入、confirm询问等等操做。
cnpm i inquirer -S
复制代码
const inquirer = require('inquirer'); // 定义询问的参数 // type表示询问的类型,是单选、多选、确认等等 // name能够理解为当前交互的标识符,其值为交互的结果 const InquirerConfig = { // 文件夹已存在的名称的询问参数 folderExist: [{ type: 'list', name: 'recover', message: '当前文件夹已存在,请选择操做:', choices: [ { name: '覆盖', value: 'cover' }, { name: '建立一个新的文件夹', value: 'newFolder' }, { name: '退出', value: 'exit' }, ] }], // 重命名的询问参数 rename: [{ name: 'inputNewName', type: 'input', message: '请输入新的项目名称: ' }] } // 使用 // 经过当前标识符获取交互的结果 // 好比,以下是一个单选的演示 const { recover } = await inquirer.prompt(InquirerConfig.folderExist); // 若是用户选中的是“覆盖”选项 if (recover === 'cover') { await fs.removeSync(target); return resolve(); // 若是用户选中的是“建立新文件夹”选中 } else if (recover === 'newFolder') { // 再次建立一个用户输入的交互操做 // 让用户输入新的文件夹名称 const { inputNewName } = await inquirer.prompt(InquirerConfig.rename); this.RepoMaps.target = this.genTargetPath(`./${inputNewName}`); return resolve(); // 若是用户选的是“退出”选项 } else { exit(1); } 复制代码
在进行了文件夹监测完成以后,就应该是要下载咱们在git上的项目资源了。下载资源咱们是经过download-git-repo这个库来实现的。
cnpm install download-git-repo -S
复制代码
const path = require('path'); const downloadRepo = require('download-git-repo'); // 下载repo资源 downloadRepo() { // 菊花转起来~ this.spinner.start('正在拉取项目模板...'); const { repo, temp } = this.RepoMaps return new Promise(async (resolve, reject) => { // 若是本地临时文件夹存在,则先删除临时文件夹 await fs.removeSync(temp); /** * 第一个参数为远程仓库地址,注意是类型:做者/库 * 第二个参数为下载到的本地地址, * 后面还能够继续加一个配置参数对象,最后一个是回调函数, */ download(repo, temp, async err => { if (err) return reject(err); // 菊花变成对勾 this.spinner.succeed('模版下载成功'); return resolve() }) }) } 复制代码
主要逻辑就是把资源下载到咱们当前的临时文件夹位置,若是临时文件夹已经存在了那么就先删除临时文件夹。
上面经过git上资源的下载,咱们是下载到了cli目录内的临时文件内,那么咱们还须要把资源移动到咱们指定的位置,而且删除没必要要的资源。因此咱们这边会在utlis里面封装一个公共函数,用于资源的拷贝:
/** * copyFiles 拷贝下载的repo资源 * @param { string } tempPath 待拷贝的资源路径(绝对路径) * @param { string } targetPath 资源放置路径(绝对路径) * @param { Array<string> } excludes 须要排除的资源名称(会自动移除其全部子文件) */ exports.copyFiles = async (tempPath, targetPath, excludes = []) => { const removeFiles = ['./git', './changelogs'] // 资源拷贝 await fs.copySync(tempPath, targetPath) // 删除额外的资源文件 if (excludes && excludes.length) { await Promise.all(excludes.map(file => async () => await fs.removeSync(path.resolve(targetPath, file)) )); } } 复制代码
// 拷贝repo资源 async copyRepoFiles() { const { temp, target } = this.RepoMaps await copyFiles(temp, target, ['./git', './changelogs']); } 复制代码
这里,咱们移除了项目中自己含有的./git
、./changelogs
等文件,由于这些是该git项目须要的内容,而咱们实际是不须要的。
经过上面的操做,咱们已经把资源拷贝到咱们的目标地址了。那么咱们还想自动把package.json中的name、version、author等字段更新成咱们须要的,应该怎么作呢?
/** * updatePkgFile * @description 更新package.json文件 */ async updatePkgFile() { // 菊花转起来! this.spinner.start('正在更新package.json...'); // 获取当前的项目内的package.json文件的据对路径 const pkgPath = path.resolve(this.RepoMaps.target, 'package.json'); // 定义须要移除的字段 // 这些字段自己只是git项目配置的内容,而咱们业务项目是不须要的 const unnecessaryKey = ['keywords', 'license', 'files'] // 调用方法获取用户的git信息 const { name = '', email = '' } = await getGitUser(); // 读取package.json文件内容 const jsonData = fs.readJsonSync(pkgPath); // 移除不须要的字段 unnecessaryKey.forEach(key => delete jsonData[key]); // 合并咱们须要的信息 Object.assign(jsonData, { // 以初始化的项目名称做为name name: this.source, // author字段更新成咱们git上的name author: name && email ? `${name} ${email}` : '', // 设置非私有 provide: true, // 默认设置版本号1.0.0 version: "1.0.0" }); // 将更新后的package.json数据写入到package.json文件中去 await fs.writeJsonSync(pkgPath, jsonData, { spaces: '\t' }); // 中止菊花 this.spinner.succeed('package.json更新完成!'); } 复制代码
这一块,上面代码注释已经写的很是清晰了,看一遍应该就晓得过程逻辑了吧!!!至于其中获取用户git信息的逻辑,后面立刻会讲解到!!!
如今咱们看下如何获取git信息的,咱们定义了一个公共的方法getGitUser:
/** * getGitUser * @description 获取git用户信息 */ exports.getGitUser = () => { return new Promise(async (resolve) => { const user = {} try { const [name] = await runCmd('git config user.name') const [email] = await runCmd('git config user.email') // 移除结尾的换行符 if (name) user.name = name.replace(/\n/g, ''); if (email) user.email = `<${email || ''}>`.replace(/\n/g, '') } catch (error) { log.error('获取用户Git信息失败') reject(error) } finally { resolve(user) } }); } 复制代码
咱们都知道,在终端想查看用户的git信息,那么只须要键入git config user.name
便可,git config user.email
能够获取用户的邮箱。那么咱们一样的在脚本中也执行这样的命令不就能够获取到了吗?
那么剩下的就是如何在终端执行shell命令呢?
node是经过开启一个子进程来执行脚本命令的,child_process说明是node提供的一个开启子进程的方法。因而咱们能够封装一个方法用于执行子进程:
// node的child_process能够开启一个进程执行任务 const childProcess = require('child_process'); /** * runCmd * @description 运行cmd命令 * @param { string } 待运行的cmd命令 */ const runCmd = (cmd) => { return new Promise((resolve, reject) => { childProcess.exec(cmd, (err, ...arg) => { if (err) return reject(err) return resolve(...arg) }) }) } 复制代码
因此上述获取git详情的操做其实就是调用的这个方法,让node开启一个子进程去运行咱们的git命令,而后将结果返回出来。
// 初始化git文件 async initGit() { // 菊花转起来 this.spinner.start('正在初始化Git管理项目...'); // 调用子进程,运行cd xxx的命令进入到咱们目标文件目录 await runCmd(`cd ${this.RepoMaps.target}`); // 调用process.chdir方法,把node进程的执行位置变动到目标目录 // 这步很重要,否则会执行失败(由于执行位置不对) process.chdir(this.RepoMaps.target); // 调用子进程执行git init命令,辅助咱们进行git初始化 await runCmd(`git init`); // 菊花停下来 this.spinner.succeed('Git初始化完成!'); } 复制代码
这一块也是调用的咱们封装的方法执行git命令而已。可是必定要注意、process.chdir(this.RepoMaps.target);
变动进程的执行位置,若是变动目录失败会抛出异常(例如,若是指定的 directory 不存在)。这步操做很是重要,切记!!切记!!!详细能够查阅process.chdir说明
最后咱们就须要自动暗转项目依赖了。本质也是调用子进程执行npm命令就能够了。这里咱们直接指定了使用淘宝的镜像源,小伙伴们也能够扩展,根据用户的选择指定npm、yarn和其余镜像源等等,尽情发挥吧!!!
// 安装依赖 async runApp() { try { this.spinner.start('正在安装项目依赖文件,请稍后...'); await runCmd(`npm install --registry=https://registry.npm.taobao.org`); await runCmd(`git add . && git commit -m"init: 初始化项目基本框架"`); this.spinner.succeed('依赖安装完成!'); console.log('请运行以下命令启动项目吧:\n'); log.success(` cd ${this.source}`); log.success(` npm run serve`); } catch (error) { console.log('项目安装失败,请运行以下命令手动安装:\n'); log.success(` cd ${this.source}`); log.success(` npm run install`); } } 复制代码
vta-cli脚手架git源码地址,有兴趣的小伙伴能够查阅代码实现。也可使用vta-cli快速初始化Vue+Ts+ElementUi
的RBAC后台管理系统的基础架构。安装vta-cli
的方法:
# 安装cli npm i vta-cli -g # 初始化项目 vta create my-app 复制代码
vue-typescript-admin项目模板将会很快完善起来!!!也欢迎小伙伴们一块儿贡献代码哦~~
关于cli开发的讲解,到这就基本结束了!!!上面涵盖了常见的技术实现方案和注意细节,项目能够无痛上手的~~~有兴趣的小伙伴们能够照着封装本身的cli,把业务通用的场景解决方案抽离处理,提高本身的开发效率吧!最后,我是大家的老朋友愣锤,欢迎👏👏点赞👍👍收藏💗💗哦~~~
点赞👍、收藏👋、分享防走丢哦!!!须要的时候能够拿出来对着开发~~~
此处将留做后续更多和脚手架开发相关的优秀库的展现地址,后续会继续更新~~~
本文使用 mdnice 排版