内容简介:程序的本质就是偷懒(误)。如果有个脚手架模版工程,我们直接使用最熟练的通过对
程序的本质就是偷懒(误)。 为了偷懒
为了减少重复劳动,提高开发效率,现在许多的框架和库都会自带CLI工具,如 vue-cli
、 create-react-app
、 egg-init
等,可以快速地创建项目工程,而不需要从头开始搭建,或者从一个已有的项目中 copy
过来,然后去删减一堆的东西。
设计思路
如果有个脚手架模版工程,我们直接使用最熟练的 copy
就能快速创建一个工程功能了,但是,如果我们需要为这些工程动态配置一些东西,以及脚手架模版越来越多,这时候就需要有个 工具 来进行管理了(其实就是通过工具来进行 copy
)。
通过对 vue-cli
和 create-react-app
研究发现,它们在思路上基本都是相同:
CLI
当然,不同的命令行工具会有不同的实现, vue-cli
会通过交互式命令来获取工程配置,通过 git clone
来拉取模版进行初始化,而 create-react-app
将模版拉取、初始化以及工具命令放到了 react-scripts
里面。
下面将会以自己写的 easyapp 为例进行分析。
核心库
- chalk : 控制终端输出字符串的样式。
- commander : 命令行核心库,提供了用户命令行输入和参数解析的强大功能,可以简化命令行开发。
- cross-spawn : 跨平台处理子进程系统命令。
-
download-git-repo
: 通过
git方式下载 repository 。 -
fs-extra
: 增强
Node.js的fs模块。 -
inquirer
:
Node.js命令行交互工具,提供通用的命令行用户界面集合,用于和用户进行交互。 - npm-check-updates : 检查 packages 是否需要更新。
- ora : 提供 loading 的样式。
-
request
:
Node.js的 http 请求库。 -
semver
:
semver版本规范,提供版本的判断。 -
validate-npm-package-name
: 校验是否符合
npm package的命名规范。
CLI命令
Commands: create create <project-name> Options: -v, --version Show version number 复制代码
目前只有核心命令 create
, 接受一个参数作为工程名字。
Node.js
中,一个可执行的命令,是通过 package.json
中的 bin
字段来实现的。
"bin": {
"easyapp": "bin/easyapp.js"
},
复制代码
在执行 easyapp
命令时,实际上执行的是 bin
目录下的 easyapp.js
文件。
解析获取命令行参数
首先 checkNodeVersion
函数对当前Node版本进行校验,然后是定义 create
的命令和参数解析,当命令行输入的命令为 create
时,执行 create
函数。
// src/index.ts
import program from 'commander'
import { version } from './package.json'
import create from './src/commands/create'
import list from './src/commands/list'
import { checkNodeVersion } from './src/utils/check-version'
// 校验 Node 版本
checkNodeVersion()
// 定义
program
.version(version, '-v, --version')
.command('create [name]')
.description('create project')
.action(async (name: string) => {
await create(name)
})
program.parse(process.argv)
if (program.args.length < 1) {
program.help()
}
复制代码
create 函数
create
函数是核心方法,该方法实现了:
isValidPackageName createAppDir isSafeDirectory getProjectInfo download generate install
import chalk from 'chalk'
import ora from 'ora'
import path from 'path'
import generate from '../utils/generate'
import download from '../utils/download'
import install from '../utils/install'
import {
isSafeDirectory,
isValidPackageName,
createAppDir,
getProjectInfo
} from '../utils'
const { red, green } = chalk
async function create(name: string): Promise<void> {
// 校验create命令接收的参数 - name 是否合法
if (!isValidPackageName(name)) process.exit(1)
// 判断是否存在以 name 命名的目录,如果无则创建
createAppDir(name)
// 判断该目录是否为合法的目录
if (!isSafeDirectory(name)) process.exit(1)
const root = path.resolve(name)
// 从交互式命令行界面获取工程配置参数
const { template, ...projectInfo } = await getProjectInfo(name)
console.log()
console.log()
const spinner = ora('Downloading please wait...')
spinner.start()
try {
// 根据交互式命令行选择的模版名称下载拉取模版
await download(`${template.path}#${template.version}`, `./${name}`)
} catch (error) {
console.log()
console.log(
red(`Failed to download template ${template}: ${error.message}.`)
)
process.exit(1)
}
// 初始化和处理下载的模版
generate(name, projectInfo)
spinner.succeed(`${green('Template download successfully!')}`)
spinner.start('Installing packages. This might take a couple of minutes.')
// 安装依赖
await install(name)
spinner.succeed(`${green('All packages installed successfully!')}`)
console.log()
console.log(green(`Success! Created ${name} at ${root}`))
console.log()
}
export default create
复制代码
交互式命令行获取配置参数
通过 inquirer
来实现交互式的命令行,主要获取 name
、 version
、 description
、 repository
、 author
、 license
和 template
,用于之后选择下载的模版和初始化。
export async function getProjectInfo(name: string): Promise<inquirer.Answers> {
const question = getQuestion(name)
const answers = await inquirer.prompt(question)
return answers
}
function getQuestion(name: string): inquirer.Questions {
const author = getGitAuthor()
const choices = Object.keys(TEMPLATE).map(
(name: string): inquirer.ChoiceType => ({
name,
value: TEMPLATE[name]
})
)
return [
{
type: 'input',
name: 'name',
message: 'Project name',
default: name,
filter(value: string): string {
return value.trim()
}
},
{
type: 'input',
name: 'version',
message: 'Project version',
default: '0.1.0',
filter(value: string): string {
return value.trim()
}
},
{
type: 'input',
name: 'description',
message: 'Project description',
filter(value: string): string {
return value.trim()
}
},
{
type: 'input',
name: 'repository',
message: 'Repository',
filter(value: string): string {
return value.trim()
}
},
{
type: 'input',
name: 'author',
message: 'Author',
default: `${author.name} <${author.email}>`,
filter(value: string): string {
return value.trim()
}
},
{
type: 'input',
name: 'license',
message: 'License',
default: 'MIT',
filter(value: string): string {
return value.trim()
}
},
{
type: 'list',
name: 'template',
message: 'Please select a template for the project',
choices,
default: choices[0]
},
{
type: 'confirm',
name: 'confirm',
message: 'Is this ok?',
default: true
}
]
}
复制代码
下载脚手架模版
使用 download-git-repo
进行下载。
import downloadRepo from 'download-git-repo'
export default async function download<T>(
repository: string,
destination: string
): Promise<T> {
return new Promise(
(resolve, reject): void => {
downloadRepo(
repository,
destination,
{ clone: true },
(error: Error, data: any): void => {
if (error) {
reject(error)
} else {
resolve(data)
}
}
)
}
)
}
复制代码
初始化模版
export default function generate(name: string, packageInfo: PackageInfo): void {
const packageFile = path.resolve(name, 'package.json')
const readmeFile = path.resolve(name, 'README.md')
try {
// 读取模版的 package.json
const data = fs.readFileSync(packageFile, 'utf-8')
// 将 package.json 解析成 json 对象
const pkg = JSON.parse(data)
// 将获取到的工程配置重新赋值给package.json
pkg.name = packageInfo.name
pkg.version = packageInfo.version
pkg.description = packageInfo.description
pkg.author = packageInfo.author
pkg.license = packageInfo.license
pkg.repository = { type: 'git', url: packageInfo.repository }
pkg.bugs = { url: `${packageInfo.repository}/issues` }
pkg.homepage = `${packageInfo.repository}#readme`
if (pkg.module) pkg.module = `dist/${packageInfo.name}.mjs`
if (pkg['umd:main']) pkg['umd:main'] = `dist/${packageInfo.name}.js`
if (pkg.main) pkg.main = `dist/${packageInfo.name}.js`
// 将解析过的 package.json 重新写到 package.json 文件中
fs.writeFileSync(packageFile, JSON.stringify(pkg, null, 2), 'utf-8')
// 同时生成 README.md 文件
fs.writeFileSync(
readmeFile,
`# ${packageInfo.name}${os.EOL}${packageInfo.description}`,
'utf-8'
)
} catch (error) {
console.log(red(`Fail to generate: ${error.message}`))
process.exit(1)
}
}
复制代码
安装依赖
export default async function install(name: string): Promise<void> {
const command = getPackageManager()
const root = path.resolve(name)
const args = []
// 根据 package.json 中的依赖包,判断是否需要进行版本更新升级
await ncu.run({
jsonUpgraded: true,
packageManager: 'npm',
silent: true,
packageFile: `./${name}/package.json`
})
// 是否使用 yarn
if (command === 'yarn') {
args.push('--cwd', root)
}
args.push('--silent')
try {
// 子进程中执行 yarn / npm install
spawn.sync(command, args, { stdio: 'ignore', cwd: root })
} catch (error) {
console.log(` ${cyan(command)} has failed.`)
}
}
复制代码
结语
以上基本上就是一个CLI基本的实现过程,总结下,其实就是获取配置、下载模版、初始化模版和安装依赖。后面可以自己再进行扩展,比如命令行界面的优化、添加新的命令、列举模版、缓存模版,等等。
至此,脚手架命令行工具的原理和 easyapp
的实现已经介绍完毕。
项目github地址: github.com/Chersquwn/e…
原文链接: 脚手架命令行工具实现揭秘
欢迎大家 star。
参考
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Flash ActionScript 3.0 动画高级教程
Keith Peters / 苏金国、荆涛 / 人民邮电出版社 / 2010-1 / 65.00元
《Flash ActionScript 3.0 动画高级教程》是介绍Flash 10 ActionScript动画高级技术的经典之作,是作者在这一领域中多年实践经验的结晶。书中不仅涵盖了3D、最新绘图API以及Pixel Bender等Flash 10 ActionScript特性,深入介绍了碰撞检测、转向、寻路等Flash游戏开发技术,还通过实例具体讲解了等角投影和数值积分的基本理论和应用。 ......一起来看看 《Flash ActionScript 3.0 动画高级教程》 这本书的介绍吧!