为什么要弄个脚手架
对于我个人,经常写些demo,或者写一个新项目的时候,要么就是把以前的项目模板复制一份,要么就是重新搭建一份,显得比较麻烦,浪费时间,所以就有了搭建一个能满足自己需要的脚手架。
脚手架的效果
这是一个基本的脚手架,init一个项目,输入项目名称,版本号等信息,然后从git仓库拷贝一份自己需要的项目模板。类似vue的vue-cli或者react的create-react-app,只是这个比较简单.
基本思路参考下图
这部分参考了掘金@张国钰大佬的思路.
项目结构
主要3个,一个bin文件夹,放执行命令的入口文件
lib文件夹,放项目的主要文件,package.json不多说
这项目主要用到的几个包
commander: 命令行工具
download-git-repo: 用来下载远程模板
ora: 显示loading动画
chalk: 修改控制台输出内容样式
log-symbols: 显示出 √ 或 × 等的图标
inquirer.js:命令交互
metalsmith:处理项目模板
handlebars:模板引擎
使用commander.js命令行工具
修改package.json的bin执行入口,
"bin": { "lz": "./bin/www" },
"lz"这个命令可以自己选择,然后在bin文件加创建名为www的文件,
#! /usr/bin/env node require('../lib/index.js');
其中
#! /usr/bin/env node
不能少,这个主要指定当前脚本由node.js进行解析
在lib创建一个index.js文件,
const program = require('commander') program.version('1.0.0') .usage('<command> [项目名称]') .command('init', '创建新项目') .parse(process.argv);
为方便测试,先链接到全局环境
npm link
执行下命令感受下
lz init hello
正常来说,应该就报错了,错误堆栈大概就是确实www-init文件,
这是因为
commander支持git风格的子命令处理,可以根据子命令自动引导到以特定格式命名的命令执行文件,文件名的格式是[command]-[subcommand],例如:
macaw hello => macaw-hello
macaw init => macaw-init
所以我们 执行www文件的init,所以要在bin创建一个www-init文件,在lib创建个init.js文件
www-init
#! /usr/bin/env node require('../lib/init.js');
init.js 完整代码
const program = require('commander') const path = require('path') const fs = require('fs') const glob = require('glob') // npm i glob -D const download = require('../lib/download.js') const inquirer = require('inquirer') const chalk = require('chalk') const generator = require('../lib/generator') const logSymbols = require("log-symbols"); program.usage('<project-name>') // 根据输入,获取项目名称 let projectName = process.argv[2]; if (!projectName) { // project-name 必填 // 相当于执行命令的--help选项,显示help信息,这是commander内置的一个命令选项 program.help() return } const list = glob.sync('*') // 遍历当前目录 let next = undefined; let rootName = path.basename(process.cwd()); if (list.length) { // 如果当前目录不为空 if (list.some(n => { const fileName = path.resolve(process.cwd(), n); const isDir = fs.statSync(fileName).isDirectory(); return projectName === n && isDir })) { console.log(`项目${projectName}已经存在`); return; } rootName = projectName; next = Promise.resolve(projectName); } else if (rootName === projectName) { rootName = '.'; next = inquirer.prompt([ { name: 'buildInCurrent', message: '当前目录为空,且目录名称和项目名称相同,是否直接在当前目录下创建新项目?', type: 'confirm', default: true } ]).then(answer => { return Promise.resolve(answer.buildInCurrent ? '.' : projectName) }) } else { rootName = projectName; next = Promise.resolve(projectName) } next && go() function go() { next .then(projectRoot => { if (projectRoot !== '.') { fs.mkdirSync(projectRoot) } return download(projectRoot).then(target => { return { name: projectRoot, root: projectRoot, downloadTemp: target } }) }) .then(context => { return inquirer.prompt([ { name: 'projectName', message: '项目的名称', default: context.name }, { name: 'projectVersion', message: '项目的版本号', default: '1.0.0' }, { name: 'projectDescription', message: '项目的简介', default: `A project named ${context.name}` } ]).then(answers => { return { ...context, metadata: { ...answers } } }) }) .then(context => { //删除临时文件夹,将文件移动到目标目录下 return generator(context); }) .then(context => { // 成功用绿色显示,给出积极的反馈 console.log(logSymbols.success, chalk.green('创建成功:)')) console.log(chalk.green('cd ' + context.root + '\nnpm install\nnpm run dev')) }) .catch(err => { // 失败了用红色,增强提示 console.log(err); console.error(logSymbols.error, chalk.red(`创建失败:${err.message}`)) }) }
init.js都做了什么呢?