社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
每当你发现自己和大多数人站在一边,就是时候停下来思考了。—— 马克·吐恩
因为这部分内容稍有些复杂,所以讲解之前先贴出github地址和视频讲解地址:
视频讲解,请搜索微信公众号 《JavaScript全栈》
相信大家在工作中都有如下经历:
开发新项目,很多逻辑比如:项目架构、接口请求、状态管理、国际化、换肤等之前项目就已经存在,这时,我们选择“信手拈来”,ctrl + c,ctrl + v 二连,谈笑间,新项目搭建完成,无非是要改改一些文件和包名;
项目增加某个模块时,复制一个已有模块,改改名字,新的模块就算创建成功了;
项目的规范要无时无刻不在同事耳边提及,就算有规范文档,你还需要苦口婆心。
使用复制粘贴有以下缺点:
重复性工作,繁琐而且浪费时间
copy过来的模板容易存在无关的代码
项目中有很多需要配置的地方,容易忽略一些配置点
人工操作永远都有可能犯错,建新项目时,总要花时间去排错
框架也会不断迭代,人工建项目不知道最新版本号是多少,使用的依赖都是什么版本,很容易bug一大堆。
承受过以上一些痛苦的同学应该不少,怎么去解决这些问题呢?我觉得,脚手架能够规避很多认为操作的问题,因为脚手架能够根据你事先约定的规范,创建项目,定义新的模块,打包,部署等等都能够在一个命令敲击后搞定,提升效率的同时降低了入职员工的培训成本,所以,我推荐大家考虑考虑为团队打造一个脚手架!
库名 | 描述 |
---|---|
commander | 处理控制台命令 |
chalk | 五彩斑斓的控制台 |
semver | 版本检测提示 |
fs-extra | 更友好的fs操作 |
inquirer | 控制台询问 |
execa | 执行终端命令 |
download-git-repo | git远程仓库拉取 |
脚手架可以为我们做很多事情,比如项目的创建、项目模块的新增、项目打包、项目统一测试、项目发布等,我先与大家聊聊最初始的功能:项目创建。
上图向大家展示了创建项目和项目中创建模块的脚手架大致工作流程,下图更详细描述了基于模板创建的过程:
思路很简单,接下来我们就通过代码示例,为大家详细讲解。
项目结构如图
在package.json中指明你的包通过怎样软链接的形式启动:bin
指定,因为是package.json包,所以我们一定要注意了dependencies、devDependencies和peerDependencies的区别,我这里不做展开。
{
"name": "awesome-test-cli",
"version": "1.0.0",
"description": "合一带大家开发脚手架工具",
"main": "index.js",
"bin": {
"awesome-test": "bin/main.js"
},
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [
"scaffold",
"efficient",
"react"
],
"author": "walker",
"license": "ISC",
"engines": {
"node": ">=8.9"
},
"dependencies": {
"chalk": "^2.4.2",
"commander": "^3.0.0",
"download-git-repo": "^2.0.0",
"execa": "^2.0.4",
"fs-extra": "^8.1.0",
"import-global": "^0.1.0",
"inquirer": "^6.5.1",
"lru-cache": "^5.1.1",
"minimist": "^1.2.0",
"nunjucks": "^3.2.0",
"ora": "^3.4.0",
"request-promise-native": "^1.0.7",
"semver": "^6.3.0",
"string.prototype.padstart": "^3.0.0",
"valid-filename": "^3.1.0",
"validate-npm-package-name": "^3.0.0"
}
}复制代码
接下来编写/bin/main.js
入口文件,主要的操作就是通过commander
处理控制台命令,根据不同参数处理不同的逻辑.
// 开始处理命令
const program = require('commander')
const minimist = require('minimist')
program
.version(require('../package').version)
.usage('<command> [options]')
// 创建命令
program
.command('create <app-name>')
.description('create a new project')
.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
.option('-d, --default', 'Skip prompts and use default preset')
.action((name, cmd) => {
const options = cleanArgs(cmd)
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow('n ⚠️ 检测到您输入了多个名称,将以第一个参数为项目名,舍弃后续参数哦'))
}
require('../lib/create')(name, options)
})
复制代码
将真正的处理逻辑放在 lib
中,这样一来,我们后面希望添加更多命令或操作更友好。接下来我们编写 lib/create
文件,该文件主要处理文件名合法检测,文件是否存在等配置,检测无误,执行项目创建逻辑,该逻辑我们放在 lib/Creator
文件中处理。
async function create (projectName, options) {
const cwd = options.cwd || process.cwd()
// 是否在当前目录
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName
const targetDir = path.resolve(cwd, projectName || '.')
const result = validatePackageName(name)
// 如果所输入的不是合法npm包名,则退出
if (!result.validForNewPackages) {
console.error(chalk.red(`不合法的项目名: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('❌ ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('⚠️ ' + warn))
})
exit(1)
}
// 检查文件夹是否存在
if (fs.existsSync(targetDir)) {
if (options.force) {
await fs.remove(targetDir)
} else {
await clearConsole()
if (inCurrent) {
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
])
if (!ok) {
return
}
} else {
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `目标文件夹 ${chalk.cyan(targetDir)} 已经存在,请选择:`,
choices: [
{ name: '覆盖', value: 'overwrite' },
{ name: '取消', value: false }
]
}
])
if (!action) {
return
} else if (action === 'overwrite') {
console.log(`nRemoving ${chalk.cyan(targetDir)}...`)
await fs.remove(targetDir)
}
}
}
}
await clearConsole()
// 前面完成准备工作,正式开始创建项目
const creator = new Creator(name, targetDir)
await creator.create(options)
}
module.exports = (...args) => {
return create(...args).catch(err => {
stopSpinner(false)
error(err)
})
}复制代码
通过以上操作,完成了创建项目前的准备工作,接下来正式进行创建,创建操作通过一下代码开始
const creator = new Creator(name, targetDir)
await creator.create(options)复制代码
创建逻辑我们放在另外文件中 /lib/Creator
,该文件中我们主要进行的操作有:
拉取远程模板;
询问项目创建相关配置,比如:项目名、项目版本、操作人等;
将拉取的模板文件拷贝到创建项目文件夹中,生成readme文档;
安装项目所需依赖;
创建git仓库,完成项目创建。
const chalk = require('chalk')
const execa = require('execa')
const inquirer = require('inquirer')
const EventEmitter = require('events')
const loadRemotePreset = require('../lib/utils/loadRemotePreset')
const writeFileTree = require('../lib/utils/writeFileTree')
const copyFile = require('../lib/utils/copyFile')
const generateReadme = require('../lib/utils/generateReadme')
const {installDeps} = require('../lib/utils/installDeps')
const {
defaults
} = require('../lib/options')
const {
log,
error,
hasYarn,
hasGit,
hasProjectGit,
logWithSpinner,
clearConsole,
stopSpinner,
exit
} = require('../lib/utils/common')
module.exports = class Creator extends EventEmitter {
constructor(name, context) {
super()
this.name = name
this.context = context
this.run = this.run.bind(this)
}
async create(cliOptions = {}, preset = null) {
const { run, name, context } = this
if (cliOptions.preset) {
// awesome-test create foo --preset mobx
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else {
preset = await this.resolvePreset(defaults.presets.default, cliOptions.clone)
}
await clearConsole()
log(chalk.blue.bold(`Awesome-test CLI v${require('../package.json').version}`))
logWithSpinner(`✨`, `正在创建项目 ${chalk.yellow(context)}.`)
this.emit('creation', { event: 'creating' })
stopSpinner()
// 设置文件名,版本号等
const { pkgVers, pkgDes } = await inquirer.prompt([
{
name: 'pkgVers',
message: `请输入项目版本号`,
default: '1.0.0',
},
{
name: 'pkgDes',
message: `请输入项目简介`,
default: 'project created by awesome-test-cli',
}
])
// 将下载的临时文件拷贝到项目中
const pkgJson = await copyFile(preset.tmpdir, preset.targetDir)
const pkg = Object.assign(pkgJson, {
version: pkgVers,
description: pkgDes
})
// write package.json
log()
logWithSpinner('
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!