脚手架的基本原理
点击查看脚手架系列文章总览【正在更新】
个人网站:www.dengzhanyong.com
关注公众号【前端筱园】,不错过每一篇文章
初始化项目
1. 创建项目文件
mkdir steamed-cli
cd steamed-cli
2. 使用lerna初始化项目
lerna init
项目默认结构如下:
C:.
| .git
│ lerna.json
│ package.json
│
└─packages
在此基础上需要进行一些改动:
-
删除
packages
目录,packages
是用来放置所有包的目录,包较多的情况下,最好多建几个文件夹将他们进行分类 -
新建文件夹
commands
(命令) 、core
(核心包)、models
(类)、utils
(工具方法) -
修改
lerna.json
配置中的packages
属性,此属性指定包的位置{ "packages": [ "core/*", "commands/*", "models/*", "utils/*" ], "version": "1.0.0" }
-
新建
.gitignore
文件,添加git
忽略文件配置**/node_modules
-
新建
README.md
文件,对脚手架进行介绍及使用说明
整体流程
整个脚手架的开发分成三个阶段进行:准备阶段、注册阶段、执行阶段
- 准备阶段:在执行命令前需要做的一些事,如:检查node版本,检查脚手架更新,检查用户目录、权限等
- 注册阶段:解析用户输入的命令(command)和参数(options),这里的参数主要针对于全局参数。
- 执行阶段:安装/更新执行相应的package,也可直接执行本地文件。
准备阶段
创建入口 package
现在就正式开始脚手架的开发,首先需要一个入口。创建一个名叫 @steamed/cli
的包
lerna create @steamed/cli ./core
# 在最后可以加上一个路径,会将此包创建到指定的目录下
一个包的目录结构如下:
cli:.
│ package.json
│ README.md
│
├─lib
│ cli.js
│
└─__tests__
cli.test.js
为了保持统一的规范,最好将 lib 下的 cli.js
改为 index.js
。
@steamed/cli
这个包是整个脚手架的入口,需要创建一个 /bin/index.js
文件作为入口,并在 packages.json
中配置 bin
属性。
{
"name": "@steamed/cli",
"version": "1.0.0",
"bin": {
"st": "./bin/index.js"
}
}
测试流程
在 core/cli
下执行 npm link
,创建其软链接,使得我们可以在本地使用设置的命令进行调试。
在 core/cli/bin/index.js
下可以先写一句打印 console.log('hello steamed-cli')
,然后在命令行中执行 st
命令,如果正常输入,则说明到目前一切正常。
如何获取输入的命令参数?
需要根据用户输入不同的命令来做不同的事,使用 process.argv
可以获取到输入的所有内容,其值为一个数组。
st init procejectName
[
'C:\\Program Files\\nodejs\\node.exe',
'C:\\Users\\DZY26\\AppData\\Roaming\\npm\\node_modules\\@steamed\\cli\\bin\\index.js',
'init',
'myname'
]
- 第一项是
node
的安装目录 - 第二项是所执行的文件路径
- 后面的内容就是所传入的参数
bin/index.js 只做一件事
对于输入命令后需要做的事情我们都放到 lib/index.js
中去做,在 bin/index.js
只做一件事。
- import-local:允许全局安装的包使用其自身的本地安装版本(如果可用)
如果存在本地版本,则执行本地脚手架。如果不存在,则执行 lib/index.js
中的内容
#! /usr/bin/env nodeconst importLocal = require('import-local');if (importLocal(__filename)) { console.log('执行本地脚手架');} else { require('../lib/index')(process.argv.slice(2))}
功能开发
本阶段的开发需要用到以下的 npm
包,
- semver:用户版本号规范校验,版本号比较等
- colors:对打印的日志设置颜色
- userHome:获取用户主目录
- npmlog:用户输入日志信息
- root-check:尝试降级具有root权限的进程的权限,如果失败,则阻止访问
- dotenv:可以将本地
.env
文件中的环境变量配置到全局环境变量中。
工具包开发
@steamed/log
功能:对
npmlog
进行封装,自定义打印级别和样式位置:/utils/log
** npmlog
打印级别**
在不同的场景下, 可以使用 npmlog
打印出不同类型的信息,包括:silly
,verbose
,info
,timing
,http
,notice
,warn
,error
,silent
默认打印级别为 info
,即大于等于 info
级别的信息才会被真正的打印。
主要作用
debug 信息一般使用 verbose
级别打印,因此默认情况下是不会展示debug信息的。为了更好的调试,定位问题,可以通过传入 --debug
参数使得可以看到debug信息,实现原理就是将 npmlog
的打印级别设置为 verbose
。
完整代码:
'use strict';const log = require('npmlog');log.level = process.env.STEAMED_CLI_LOG_LEVEL || 'info'; // 设置log 的打印级别,默认为info,可以在环境变量中拿到自定义级别设置log.heading = 'Steamed'; // 自定义log头部信息,一般为脚手架的名字log.headingStyle = { bg: 'white', fg: 'black' } // 自定义头部样式module.exports = log;
@steamed/get-npm-info
功能:获取
npm
信息、版本等功能位置:/utils/get-npm-info
完整代码:
'use strict';const axios = require('axios');const semver = require('semver');// 获取默认的npm源function getDefaultRegistry(origin = true) { return origin ? 'https://registry.npmjs.org/' : 'https://registry.npm.taobao.org/'}// 获取包的所有历史版本号async function getNpmVersions(npmName, registry) { const npmRegistry = registry || getDefaultRegistry(); return axios.get(`${npmRegistry}${npmName}`) .then((res) => { if (res.status === 200) { return Object.keys(res.data.versions); } return []; }) .catch(() => { return []; })}// 获取最新版本async function getLatestVersion(npmName, registry) { const versions = (await getNpmVersions(npmName, registry)) .sort((a, b) => semver.gte(a, b) ? -1 : 1); if (versions[0]) { return versions[0] } else { throw new Error('检查更新失败'); }}module.exports = { getDefaultRegistry, getNpmVersions, getLatestVersion};
准备阶段功能实现
在准备阶段需要按照下面的顺序依次完成每个功能。
检查当前版本
这里指的是脚手架当前版本,此版本号就是 package.json
中的 version
的值。
const pkg = require('../package.json'); // 检查当前版本function checkPkgVersion() { log.info('cli-version', pkg.version);}
检查node版本
需要检查用户当前使用的 node
版本是否满足脚手架的最低版本要求,以保证脚手架的所有功能可以正常使用。
新建一个 constant.js
文件,用户配置一些常量配置信息。
const LOWEST_NODE_VERSION = '12.0.0'; // 最低node版本const STEAMED_CLI_HOME_PATH = '.steamed'; // 脚手架主目录名称const STEAMED_CLI_LOG_LEVEL = 'info'; // log 的打印等级module.exports = { LOWEST_NODE_VERSION, STEAMED_CLI_HOME_PATH, STEAMED_CLI_LOG_LEVEL}
const semver = require('semver');const { LOWEST_NODE_VERSION } = require('./constant');// 检查node版本function checkNodeVersion() { const currentVersion = process.version; // 获取当前使用的版本 const lowestVersion = LOWEST_NODE_VERSION; if (!semver.gte(currentVersion, lowestVersion)) { throw new Error(`steamed-cli 需要node的最低版本为${lowestVersion},当前node.js版本为${currentVersion}`); }}
检查root权限
需要检查用户是否有root权限,如果没有,则会进行降级处理,此功能在 root-check
中已实现。
import rootCheck from 'root-check';// 检查root权限function checkRoot() { rootCheck();}
检查用户主目录
检查用户主目录是否存在
const userHome = require('user-home');// 检查用户主目录function checkUserHome() { if (!userHome || !fs.existsSync(userHome)) { throw new Error(colors.red('当前用户主目录不存在!')); } else { process.env.STEAMED_CLI_USER_HOME = userHome; }}
检查入参
- 用户需要执行命令,则至少需要输入一个命令或参数。
- 检查用户输入的参数是否包含
-d
或--debug
,如果存在,则表示开启debug
模式,需要将log
的打印等级设置为verbose
const log = require('@steamed/log');const { STEAMED_CLI_LOG_LEVEL } = require('./constant');// 检查用户主目录function checkArgv(argv) { if (argv.length < 0) { throw new Error('请输入命令') } if (argv.includes('-d') || argv.includes('--debug')) { log.level = 'verbose'; } else { log.level = STEAMED_CLI_LOG_LEVEL; } process.env.LOG_LEVEL = log.level; // 将log等级存到环境变量中,以便其他地方使用}
检查环境变量
将主目录下的 .env
文件中环境变量会被注入到 process.env 中
const { STEAMED_CLI_HOME_PATH } = require('./constant');const userHome = require('user-home');// 检查环境变量function checkEnv() { const dotnev = require('dotenv'); const envPath = path.resolve(userHome, '.env'); // 获取主目录下的 .env 文件 dotnev.config({ path: envPath }); // .env 中的环境变量会被注入到 process.env 中 process.env.STEAMED_CLI_HOME_PATH = path.resolve(userHome, STEAMED_CLI_HOME_PATH);}
检查版本更新
检查本地脚手架版本是否是最新版本,如果不是,则提示用户及时更新脚手架
const { getLatestVersion } = require('@steamed/get-npm-info');// 检查更新async function checkCliUpdate() { const lastVersion = await getLatestVersion(pkg.name); if (lastVersion && !semver.gte(pkg.version, lastVersion)) { log.warn('steamed更新', `发现新版本${lastVersion},当前版本${pkg.version},请及时更新!`); }}
打印报错信息
在上面的流程中,每一步都有可能会出错,可能是第三方组件内部报错,也可能是我们自己写的 throw new Error()
,这对这些错误信息,我们可以在最外层通过 try catch
处理。
async function index() { try { checkPkgVersion(); checkNodeVersion(); checkRoot(); checkUserHome(); checkEnv(); await checkCliUpdate(); } catch (e) { log.error(e.message); // 只打印关键信息 if (log.level === 'verbose') { // 如果是debug模式,则打印出完整的错误信息栈,以便于定位问题 console.log(e); } }}
完整代码地址 Github
:https://github.com/DengZhanyong/steamed
点击查看脚手架系列文章总览
个人网站:www.dengzhanyong.com
关注公众号【前端筱园】,不错过每一篇文章