前言
2022年曾经过了四分之一还多了,之前说好的每个月一片文章如同也没有让本人兑现。最近公司在做一些前端工程化相干的货色,尽管筹备做组件库的事件被领导给毙了,不过在这之前写了一个脚手架的工具,毕竟当初这个环境下,脚手架工具泛滥,所以当然也要写一写玩玩。
- 前言
- 最终成果
- 反对性能
- 开发
-
初始化我的项目
- 设置我的项目入口
- 其余花里胡哨的东东
- 我的项目模板
- 公布
- 总结
- 参考
- 结语
最终成果
反对性能
- 自主抉择web端或挪动端;
- 自主抉择我的项目框架react或vue;
- 自主抉择是否设置近程git地址;
- 反对在我的项目模板中自定义变量替换;
- 自主抉择是否主动装置依赖,可抉择npm、cnpm、yarn;
- 反对应用update命令在线更新;
- 自主抉择已存在文件目录是否笼罩;
开发
初始化我的项目
那么接下来就开始开发了,首先咱们来新建一个我的项目文件夹就叫new-cli
吧,在我的项目文件夹中新建package.json
文件,设置罕用的字段,设置实现后如下:
{
"name": "new-cli",
"version": "1.0.0",
"description": "a react project cli, help you create a react project quickly",
"bin": {
"new-cli": "bin/www.js"
},
"dependencies": {
"boxen": "^5.1.2",
"chalk": "^4.1.2",
"commander": "^9.1.0",
"consolidate": "^0.16.0",
"cross-spawn": "^7.0.3",
"download-git-repo": "^3.0.2",
"ejs": "^3.1.6",
"fs-extra": "^10.0.1",
"inquirer": "^8.2.1",
"metalsmith": "^2.4.2",
"ora": "^5.4.1",
"figlet": "^1.5.2",
"semver": "^7.3.5",
"shelljs": "^0.8.5"
},
"repository": {
"type": "git",
"url": "https://github.com/BoWang816/new-cli.git"
},
"keywords": [
"cli",
"react"
],
"author": "恪晨",
"publishConfig": {
"registry": "公有仓库地址"
},
"engines": {
"node":"^12.20.0 || >=14"
}
}
通过以上设置当前,咱们的脚手架名字就叫new-cli,也就是说到时候装置的时候就是通过npm install -g new-cli
进行装置。bin上面设置的名称就是为了设置脚手架执行的命令,并且是从bin/www.js文件作为了入口文件;dependencies
中为咱们须要的我的项目依赖,值得注意的是像boxen、chalk、figlet这一类的依赖包在最新版本中曾经不反对requier形式引入了所以这里咱们须要装置低版本的包;publishConfig
中能够设置到时候须要公布的npm地址,如果你搭建了npm私服则通过设置registry就能够公布到你的私服了。
设置我的项目入口
建好package.json
当前咱们就开始建入口文件,也就是bin上面的www.js,事实上你的入口文件搁置在根目录也是能够的,能够依据本人的爱好,当然如果搁置在了根目录,则bin上面就要改为new-cli: './www.js'
。www.js中次要是引入commander、inquirer等工具包,进行脚手架工具的初始化。因为www.js将要作为一个node脚本来运行,因而须要在最上方申明环境:#! /usr/bin/env node
,我写的这个脚手架中波及到了init、update、help这三个命令,并且help是commander自身就反对的,这里只是做了一点定制化。
-
初始化init命令、update命令、help命令
首先须要引入commander,应用它的program,
const {program} = require("commander");
,脚手架工具的主体就是它了,咱们初始化相干的命令:#! /usr/bin/env node // 引入commander const {program} = require("commander"); // 初始化init命令, project-name就是你的项目名称与我的项目文件夹名称 program.command("init <project-name>") // init命令形容 .description("create a new project name is <project-name>") // init命令参数项,因为后续会设置反对笼罩文件夹,所以这里提供一个-f参数 .option("-f, --force", "overwrite target directory if it exists") // init命名执行后做的事件 .action(() => { console.log('doSomething'); }); program.command("update") .description("update the cli to latest version") // update命令执行后做的事件,自动检测更新 .action(async () => { // await checkUpdate(); console.log('update'); }); program.on("--help", () => { // 监听--help命令,输入一个提醒 console.log(figlet.textSync("new-cli", { font: "Standard", horizontalLayout: 'full', verticalLayout: 'fitted', width: 120, whitespaceBreak: true })); }); // 这个肯定不能忘,且必须在最初!!! program.parse(process.argv);
通过设置以上内容,其实咱们就能够应用根本的命令了。本地调试的形式有两种,一种是通过npm link
命令将咱们写的脚手架工具间接链接到本地的全局npm中,一种则是间接通过node bin/www.js
间接执行这个js文件,这里咱们应用后者就能够了。
-
扩大init命令
接下来咱们就须要扩大init命名,也就是在action做一些事件了。首先,咱们提供了-f的参数选项,目标是为了在初始化我的项目的时候检测到有同名文件夹则进行笼罩,因而在初始化我的项目的第一步咱们就须要检测以后门路下是否存在同名的文件夹,并且在没有设置-f的时候给出提示信息,同时在设置了-f后给出二次提醒,批准笼罩则开始初始化我的项目。因而action函数中将要执行的以下内容,这里咱们就须要引入chalk,paht,fs-extray以及后续咱们本人写的create。
const chalk = require("chalk");
const path = require("path");
const fs = require('fs-extra');
const figlet = require('figlet');
const create = require('../utils/create');
program
.command("init <project-name>")
.description("create a new project name is <project-name>")
.option("-f, --force", "overwrite target directory if it exists")
.action(async (projectName, options) => {
const cwd = process.cwd();
// 拼接到指标文件夹
const targetDirectory = path.join(cwd, projectName);
// 如果指标文件夹已存在
if (fs.existsSync(targetDirectory)) {
if (!options.force) {
// 如果没有设置-f则提醒,并退出
console.error(chalk.red(`Project already exist! Please change your project name or use ${chalk.greenBright(`new-cli create ${projectName} -f`)} to create`))
return;
}
// 如果设置了-f则二次询问是否笼罩原文件夹
const {isOverWrite} = await inquirer.prompt([{
name: "isOverWrite",
type: "confirm",
message: "Target directory already exists, Would you like to overwrite it?",
choices: [
{name: "Yes", value: true},
{name: "No", value: false}
]
}]);
// 如需笼罩则开始执行删除原文件夹的操作
if (isOverWrite) {
const spinner = ora(chalk.blackBright('The project is Deleting, wait a moment...'));
spinner.start();
await fs.removeSync(targetDirectory);
spinner.succeed();
console.info(chalk.green("✨ Deleted Successfully, start init project..."));
console.log();
// 删除胜利后,开始初始化我的项目
// await create(projectName);
console.log('init project overwrite');
return;
}
console.error(chalk.green("You cancel to create project"));
return;
}
// 如果以后门路中不存在同名文件夹,则间接初始化我的项目
// await create(projectName);
console.log('init project');
});
咱们再来查看当初的成果:
-
创立create办法
在上一步操作中,咱们笼罩同名文件后,应用了
await create(projectName)
办法开始初始化我的项目,接下来咱们开始开发create办法。在根目录新建一个文件夹叫utils
,当然你能够随便叫lib或者✨点赞
都行,在utils上面新建一个文件叫create.js
,在这个文件中,咱们将设置下载初始化我的项目中一些问题询问的执行。内容次要有以下:const inquirer = require("inquirer"); const chalk = require("chalk"); const path = require("path"); const fs = require("fs"); const boxen = require("boxen"); const renderTemplate = require("./renderTemplate"); const downloadTemplate = require('./download'); const install = require('./install'); const setRegistry = require('./setRegistry'); const {baseUrl, promptList} = require('./constants'); const go = (downloadPath, projectRoot) => { return downloadTemplate(downloadPath, projectRoot).then(target => { //下载模版 return { downloadTemp: target } }) } module.exports = async function create(projectName) { // 校验项目名称合法性,项目名称仅反对字符串、数字,因为后续这个名称会用到我的项目中的package.json以及其余很多中央,所以不能存在特殊字符 const pattern = /^[a-zA-Z0-9]*$/; if (!pattern.test(projectName.trim())) { console.log(`\n${chalk.redBright('You need to provide a projectName, and projectName type must be string or number!\n')}`); return; } // 询问 inquirer.prompt(promptList).then(async answers => { // 指标文件夹 const destDir = path.join(process.cwd(), projectName); // 下载地址 const downloadPath = `direct:${baseUrl}/${answers.type}-${answers.frame}-template.git#master` // 创立文件夹 fs.mkdir(destDir, {recursive: true}, (err) => { if (err) throw err; }); console.log(`\nYou select project template url is ${downloadPath} \n`); // 开始下载 const data = await go(downloadPath, destDir); // 开始渲染 await renderTemplate(data.downloadTemp, projectName); // 是否须要主动装置依赖,默认否 const {isInstall, installTool} = await inquirer.prompt([ { name: "isInstall", type: "confirm", default: "No", message: "Would you like to help you install dependencies?", choices: [ {name: "Yes", value: true}, {name: "No", value: false} ] }, // 抉择了装置依赖,则应用哪一个包管理工具 { name: "installTool", type: "list", default: "npm", message: 'Which package manager you want to use for the project?', choices: ["npm", "cnpm", "yarn"], when: function (answers) { return answers.isInstall; } } ]); // 开始装置依赖 if (isInstall) { await install({projectName, installTool}); } // 是否设置了仓库地址 if (answers.setRegistry) { setRegistry(projectName, answers.gitRemote); } // 我的项目下载胜利 downloadSuccessfully(projectName); }); }
-
在
create.js
文件中,咱们首先判断了初始化的项目名称是否蕴含特殊字符,如果蕴含则给出谬误提醒,并终止我的项目初始化。如果项目名称非法,则开始询问用户须要的我的项目模板:
咱们将这些询问的list抽离为常量,同时也将模板的地址抽离为常量,因而须要在utils文件夹下建设一个constants.js
的文件,外面的内容如下:/** * constants.js * @author kechen * @since 2022/3/25 */ const { version } = require('../package.json'); const baseUrl = 'https://github.com/BoWangBlog'; const promptList = [ { name: 'type', message: 'Which build tool to use for the project?', type: 'list', default: 'webpack', choices: ['webpack', 'vite'], }, { name: 'frame', message: 'Which framework to use for the project?', type: 'list', default: 'react', choices: ['react', 'vue'], }, { name: 'setRegistry', message: "Would you like to help you set registry remote?", type: 'confirm', default: false, choices: [ {name: "Yes", value: true}, {name: "No", value: false} ] }, { name: 'gitRemote', message: 'Input git registry for the project: ', type: 'input', when: (answers) => { return answers.setRegistry; }, validate: function (input) { const done = this.async(); setTimeout(function () { // 校验是否为空,是否是字符串 if (!input.trim()) { done('You should provide a git remote url'); return; } const pattern = /^(http(s)?:\/\/([^\/]+?\/){2}|git@[^:]+:[^\/]+?\/).*?.git$/; if (!pattern.test(input.trim())) { done( 'The git remote url is validate', ); return; } done(null, true); }, 500); }, } ]; module.exports = { version, baseUrl, promptList }
其中version为咱们的脚手架版本号,baseUrl为我的项目模板下载的根底地址,promptList为询问用户的问题列表,promptList的具体写法是依据
inquirer.prompt()
办法来写的,具体的怎么写前面我都会将官网文档地址附上,大家能够本人施展。 - 通过
inquirer.prompt()
获取到用户反馈的后果当前,咱们会拿到相干的字段值,而后去拼接出下载的我的项目模板地址,接下来就是开始下载我的项目模板了。这里咱们写了go函数和renderTemplate俩个函数,一个用于下载我的项目模板一个用于渲染我的项目模板(因为波及到变量的替换)。go函数中其实是应用了从内部引入的downloadTemplate办法,因而咱们须要去关注downloadTemplate与renderTemplate办法,也就是接下来要讲的重点了。
-
-
创立download办法
在
utils
文件夹下,新建一个名称为download.js
的文件,文件内容如下:/** * 下载 * download.js * @author kechen * @since 2022/3/25 */ const download = require('download-git-repo') const path = require("path") const ora = require('ora') const chalk = require("chalk"); const fs = require("fs-extra"); module.exports = function (downloadPath, target) { target = path.join(target); return new Promise(function (resolve, reject) { const spinner = ora(chalk.greenBright('Downloading template, wait a moment...\r\n')); spinner.start(); download(downloadPath, target, {clone: true}, async function (err) { if (err) { spinner.fail(); reject(err); console.error(chalk.red(`${err}download template failed, please check your network connection and try again`)); await fs.removeSync(target); process.exit(1); } else { spinner.succeed(chalk.greenBright('✨ Download template successfully, start to config it: \n')); resolve(target); } }) }) }
该文件中,咱们应用了
download-git-repo
这个第三方的工具库,用于下载我的项目模板,因为download-git-repo的返回后果是下载胜利或者失败,咱们在应用异步的形式的时候如果间接应用会存在问题,因而这里封装为promise,当err的时候给用户抛出异样提醒,胜利则将指标文件夹门路返回用于后续应用。在create.js
中咱们应用了go函数,在go函数执行胜利后会返回一个data,外面拿到了我的项目要下载到具体的文件夹的门路,其实次要是为了获取在download中的promise的resolve后果,拿到指标文件夹的门路后,其实我的项目模板曾经下载到了该文件夹中,就能够开始renderTemplate了。 - 创立renderTemplate办法
在utils
文件夹下,新建一个文件叫renderTemplate.js
,该函数的次要目标是为了将初始化的我的项目中设置的变量进行替换,次要应用了metalSmith
和consolidate
这两个第三方的包,通过遍历初始化我的项目中的文件,将其转换为ejs模板,并替换相干的变量。这个办法是参考了vww-cli的形式,通过读取我的项目模板中的ask.ts
文件,获取我的项目模板中自定义的询问列表,而后再进行文件模板引擎渲染替换相干设置好的变量,次要内容如下:
/**
* 渲染模板
* renderTemplate.js
* @author kechen
* @since 2022/3/24
*/
const MetalSmith = require('metalsmith');
const {render} = require('consolidate').ejs;
const {promisify} = require('util');
const path = require("path");
const inquirer = require('inquirer');
const renderPro = promisify(render);
const fs = require('fs-extra');
module.exports = async function renderTemplate(result, projectName) {
if (!result) {
return Promise.reject(new Error(`有效的目录:${result}`))
}
await new Promise((resolve, reject) => {
MetalSmith(__dirname)
.clean(false)
.source(result)
.destination(path.resolve(projectName))
.use(async (files, metal, done) => {
const a = require(path.join(result, 'ask.ts'));
// 读取ask.ts文件中设置好的询问列表
let r = await inquirer.prompt(a);
Object.keys(r).forEach(key => {
// 将输出内容前后空格革除,不然装置依赖时package.json读取会报错
r[key] = r[key]?.trim() || '';
})
const m = metal.metadata();
const tmp = {
...r,
// 将应用到的name全副转换为小写字母
name: projectName.trim().toLocaleLowerCase()
}
Object.assign(m, tmp);
// 实现后删除模板中的文件
if (files['ask.ts']) {
delete files['ask.ts'];
await fs.removeSync(result);
}
done()
})
.use((files, metal, done) => {
const meta = metal.metadata();
// 须要替换的文件的后缀名汇合
const fileTypeList = ['.ts', '.json', '.conf', '.xml', 'Dockerfile', '.json'];
Object.keys(files).forEach(async (file) => {
let c = files[file].contents.toString();
// 找到我的项目模板中设置好的变量进行替换
for (const type of fileTypeList) {
if (file.includes(type) && c.includes('<%')) {
c = await renderPro(c, meta);
files[file].contents = Buffer.from(c);
}
}
});
done()
})
.build((err) => {
err ? reject(err) : resolve({resolve, projectName});
})
});
};
通过renderTemplate办法,咱们根本就实现咱们脚手架的次要性能了。咱们就能够实现应用init命令创立我的项目了。这里我遇到一个问题,就是在删除ask.ts文件的时候,如果前面不加await fs.removeSync(result);
这个文件就无奈删除,然而加上按理说又不合理,具体起因没有找到,有晓得的敌人能够留言解释一下,非常感激。至此,咱们初始化我的项目的性能曾经实现,接下来就是一些扩大了。
-
创立setRegistry办法
在
utils
文件夹下,新建一个文件叫setRegistry.js
,次要是为了帮忙用户初始化我的项目的git地址,在用户创立是抉择是否须要主动设置我的项目仓库地址,如果设置了我的项目地址,则这里会主动初始化git,并设置我的项目地址,具体内容如下:/** * 设置仓库地址 * setRegistry.js * @author kechen * @since 2022/3/28 */ const shell = require("shelljs"); const chalk = require("chalk"); module.exports = function setRegistry(projectName, gitRemote) { shell.cd(projectName); if (shell.exec('git init').code === 0) { if (shell.exec(`git remote add origin ${gitRemote}`).code === 0) { console.log(chalk.green(`✨ \n Set registry Successfully, now your local gitRemote is ${gitRemote} \n`)); return; } console.log(chalk.red('Failed to set.')); shell.exit(1); } };
-
创立install办法
在
utils
文件夹下,新建一个文件叫install.js
,次要是为了帮忙用户主动装置依赖,次要内容如下:/** * 装置依赖 * install.js * @author kechen * @since 2022/3/22 */ const spawn = require("cross-spawn"); module.exports = function install(options) { const cwd = options.projectName || process.cwd(); return new Promise((resolve, reject) => { const command = options.installTool; const args = ["install", "--save", "--save-exact", "--loglevel", "error"]; const child = spawn(command, args, {cwd, stdio: ["pipe", process.stdout, process.stderr]}); child.once("close", code => { if (code !== 0) { reject({ command: `${command} ${args.join(" ")}` }); return; } resolve(); }); child.once("error", reject); }); };
-
创立checkUpdate办法
在
utils
文件夹下,新建一个文件叫checkUpdate.js
,次要是为了帮忙用户自动检测并进行脚手架更新,次要内容如下:/** * 查看更新 * checkUpdate.js * @author kechen * @since 2022/3/23 */ const pkg = require('../package.json'); const shell = require('shelljs'); const semver = require('semver'); const chalk = require('chalk'); const inquirer = require("inquirer"); const ora = require("ora"); const updateNewVersion = (remoteVersionStr) => { const spinner = ora(chalk.blackBright('The cli is updating, wait a moment...')); spinner.start(); const shellScript = shell.exec("npm -g install new-cli"); if (!shellScript.code) { spinner.succeed(chalk.green(`Update Successfully, now your local version is latestVersion: ${remoteVersionStr}`)); return; } spinner.stop(); console.log(chalk.red('\n\r Failed to install the cli latest version, Please check your network or vpn')); }; module.exports = async function checkUpdate() { const localVersion = pkg.version; const pkgName = pkg.name; const remoteVersionStr = shell.exec( `npm info ${pkgName}@latest version`, { silent: true, } ).stdout; if (!remoteVersionStr) { console.log(chalk.red('Failed to get the cli version, Please check your network')); process.exit(1); } const remoteVersion = semver.clean(remoteVersionStr, null); if (remoteVersion !== localVersion) { // 检测本地装置版本是否是最新版本,如果不是则询问是否自动更新 console.log(`Latest version is ${chalk.greenBright(remoteVersion)}, Local version is ${chalk.blackBright(localVersion)} \n\r`) const {isUpdate} = await inquirer.prompt([ { name: "isUpdate", type: "confirm", message: "Would you like to update it?", choices: [ {name: "Yes", value: true}, {name: "No", value: false} ] } ]); if (isUpdate) { updateNewVersion(remoteVersionStr); } else { console.log(); console.log(`Ok, you can run ${chalk.greenBright('wb-cli update')} command to update latest version in the feature`); } return; } console.info(chalk.green("Great! Your local version is latest!")); };
这里须要留神的是,因为脚手架是全局装置的,波及到权限的问题,因而在mac下须要应用
sudo new-cli update
进行更新,而在windows中须要以管理员身份关上命令行工具执行new-cli update
进行更新。到这里,咱们的脚手架根本就实现啦。
其余花里胡哨的东东
次要性能根本就是下面这些啦,另外咱们须要加一个我的项目创立胜利之后的提醒,在上文的create.js
中最初面有一个downloadSuccessfully的办法,其实就是创立胜利后的提醒,次要内容如下:
const downloadSuccessfully = (projectName) => {
const END_MSG = `${chalk.blue("🎉 created project " + chalk.greenBright(projectName) + " Successfully")}\n\n 🙏 Thanks for using wb-cli !`;
const BOXEN_CONFIG = {
padding: 1,
margin: {top: 1, bottom: 1},
borderColor: 'cyan',
align: 'center',
borderStyle: 'double',
title: '🚀 Congratulations',
titleAlignment: 'center'
}
const showEndMessage = () => process.stdout.write(boxen(END_MSG, BOXEN_CONFIG))
showEndMessage();
console.log('👉 Get started with the following commands:');
console.log(`\n\r\r cd ${chalk.cyan(projectName)}`);
console.log("\r\r npm install");
console.log("\r\r npm run start \r\n");
}
具体的实现成果就是这样的,这里我是截了之前做好的图。
我的项目模板
咱们须要创立一个我的项目模板,外面须要在根目录下蕴含一个ask.ts
文件,其余的就和失常我的项目一样就好了,aks.ts的文件内容示例如下,
/**
* demo
* aks.ts
* @author kechen
* @since 2022/3/24
*/
module.exports = [
{
name: 'description',
message: 'Please enter project description:',
},
{
name: 'author',
message: 'Please enter project author:',
},
{
name: 'apiPrefix',
message: 'Please enter project apiPrefix:',
default: 'api/1.0',
// @ts-ignore
validate: function (input) {
const done = this.async();
setTimeout(function () {
// 校验是否为空,是否是字符串
if (!input.trim()) {
done(
'You can provide a apiPrefix, or not it will be default【api/1.0】',
);
return;
}
const pattern = /[a-zA-Z0-9]$/;
if (!pattern.test(input.trim())) {
done(
'The apiPrefix is must end with letter or number, like default 【api/1.0】',
);
return;
}
done(null, true);
}, 300);
},
},
{
name: 'proxy',
message: 'Please enter project proxy:',
default: 'https://www.test.com',
// @ts-ignore
validate: function (input) {
const done = this.async();
setTimeout(function () {
// 校验是否为空,是否是字符串
if (!input.trim()) {
done(
'You can provide a proxy, or not it will be default【https://www.test.com】',
);
return;
}
const pattern =
/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-.,@?^=%&:/~+#]*[\w\-@?^=%&/~+#])?/;
if (!pattern.test(input.trim())) {
done(
'The proxy is must end with letter or number, like default 【https://www.test.com】',
);
return;
}
done(null, true);
}, 300);
},
},
];
这里我设置了四个变量别离是description、author、apiPrefix、proxy,在应用时只须要通过<%= var %>
这种形式就能够了,var能够是你在ask.ts中设置的任何变量,具体应用demo如下,当然要替换的文件类型必须是在下面咱们提到的renderTemplate函数中设置了后缀名的文件才能够。应用这种形式,你就能够在我的项目模板中自在增加变量,且不须要更新脚手架工具。
{
"name": "xasrd-fe-mobile",
"description": "<%= description %>",
"private": true,
"author": "<%= author %>"
}
至此,咱们的脚手架就全副开发实现啦,接下来就是怎么公布到npm或者npm私服了。
公布
在下面咱们讲过,如果须要公布的npm私服,则须要在package.json
中配置publishConfig并指向npm私服的地址,公布的时候则须要通过以下命令进行公布:
-
私服npm公布
- 登陆私服
npm login --registry=http://xxxxx
xxxxx为你的私服地址 - 公布
npm publish
- 登陆私服
-
官网npm公布
- 间接
npm login
,再npm publish
- 前提是你的npm源指向的官网npm
- 间接
-
通过github action主动触发npm公布
- 具体请参考:具体记录开发一个npm包,封装前端罕用的工具函数
当然须要留神的是,公布的时候,package.json中的version版本号不能反复哈!!!
总结
到这里,咱们就残缺的开发了一个比较简单前端脚手架工具,并能够公布应用了。其实具体的做法并不是很难,有很多第三方的工具包能够用,当然因为这个工具的交互相对来说比较简单,各位也能够本人奇思妙想,做一些更加花里胡哨的性能进行扩大。示例的demo就不放啦,根本所有的内容都在下面提到了,大家能够自由发挥。当然基于这套我本人也写了一个地址是https://www.npmjs.com/package/wb-fe-cli,不过因为最近切实没工夫,所以我的项目模板还没有,临时还不能残缺的跑起来,后续会缓缓更新的。
参考
- inquirer
- boxen
- vee-cli
- 具体记录开发一个npm包,封装前端罕用的工具函数
结语
最初心愿看完本篇文章后,对你有所帮忙,勤快的话能够本人入手手写一写啦。另外心愿大家可能关注一下我的Github,哈哈哈哈,带你们看贪吃蛇!
也能够关注一下GridManager这个好用的表格插件,反对React、Vue,十分好用哦!
下期,我将会给大家带来一些我罕用的Mac软件的介绍,可能帮忙你在日常开发与工作中大大晋升工作效率!!!能够先预览一下 恪晨的Mac软件举荐。
发表回复