脚手架核心开发流程
痛点分析
- 创建项目组件时,存在大量重复代码拷贝,快速复用已有沉淀
- 协同开发时,由于git操作不规范导致分支混乱,操作耗时,制定标准的操作规范,并集成到脚手架
- 发布上线耗时,容易出现各种错误,制定标准的上线流程和规范并集成到脚手
需求分析
- 通用到研发脚手架
- 通用到项目/组件创建能力
- 模版支持定制,定制后能够发布生效
- 模版支持快速接入,极地到接入成本
- 通用到项目/组件发布能力
- 发布过程自动完成标准的git操作
- 发布成功后自动删除开发分支并创建tag
- 发布后自动完成云构建,CDN,域名绑定
- 发布过程支持测试/正式两种模式
脚手架设计图
脚手架拆包策略
- 核心流程
- 命令
- 初始化
- 发布
- 模型层
- command命令
- project项目
- component组件
- npm模型
- git仓库
- 支撑模块
- git操作
- 云构建
- 工具方法
- api请求
- git api
脚手架拆包原则
- 核型模块 core
- 命令模块 command
- 模型模块 models
- 工具模块 utils
core模块技术方案
- 准备阶段
- 命令注册
- 命令执行
命令执行流程
- 准备阶段
- 命令注册
- 命令执行
设计技术点
核型库
- import-local
- commander
工具库
- npmlogs
- fs-extra
- semver
- colors
- user-home
- dotenv
- root-check
开始写代码
- 1.首先创建一个
lerna
项目:learn init intest-cli-dev
- 2.根目录下创建core,utils,models,commands文件夹,并修改lerna.json配置如下:
{ "packages": [ "core/*", "utils/*", "models/*", "commands/*" ], "version": "1.0.1" }
- 3.将原项目packages下的core移入到创建的core目录下,同时将packages下的utils下,修改core项目为cli,如下图:
- 4.在
cli/bin/index.js
代码如下:
#!/usr/bin/env node
const utils = require('intest-cli-dev-utils')
// console.log('111')
// console.log(utils)
const importLocal = require('import-local')
const npmlog = require('npmlog')
if(importLocal(__filename)) {
npmlog.info('cli', '正在使用本地版本')
} else {
require('../lib')(process.argv.slice(2))
}
import-local的作用当全局node_modules和本地node_modules中,存在相同的库,则优先加载本地node_modules中的库 npmlog打印工具
5.检查版本号开发 在
core/cli/lib/index.js
下写入代码如下:const pkg = require('../package.json') function checkPkgVersion() { const { version } = pkg // log.notice('cli', version) }
此时我们可以获取到配置文件package.json
的版本号,那么现在我们需要考虑require
加载资源的方式
require
可加载的资源文件有.js
, .json
, .node
- 加载
.js
时需要我们必须在js文件中,使用module.exports=any
或者exports.
输出 - 加载
.json
时会使用JSON.parse
进行转译从而得到一个json对象 .node
是一个c++插件
如果我们加一个txt文件,暂且命名为test.txt
,其内容如下:
module.exports = function test() {
console.log('11')
}
我们使用require('./test.txt')(),这个时候代码依然会执行,这是为什么呢?原来我们是使用require的时候,如果加载的文件不是上述三种格式的文件,会默认当成js文件进行解析
- npmlog的使用
- 1.使用lerna创建
lerna create intest-cli-dev-log
,项目会创建在core下,移入到utils下并进入当前目录,安装npmlognpm i npmlog -S
,代码如下:'use strict'; const log = require('npmlog') log.addLevel('success', 2000, { fg: 'green' }) // 降级处理 log log.level = process.env.LOG_LEVEL || 'info' //c 添加前缀 log.heading = 'intest' module.exports = log
- 2.如何在项目使用
intest-cli-dev-log
模块呢?- 首先在
core/cli/package.json
下,自动引入,注意file:../../utils/log是其相对路径目录,如下:{ ... "dependencies": { ... "intest-cli-dev-log": "file:../../utils/log", } }
- 然后在
core/cli
下npm link
,此时就可以在core/cli
中使用了。
- 首先在
- 1.使用lerna创建
6.检查Node版本 依然在
core/cli/lib/index.js
下创建方法checkNodeVersion方法,通过semver
库(用于检测版本号的库),通过process.version
获取node版本,以及代码中设置的最对版本进行判断,代码如下:const LOW_NODE_VERSION = '8.0.0' function checkNodeVersion() { // 1.获取当前node版本好 const currentVersion = process.version // 比对最低版本 const result = semver.gt(currentVersion, LOW_NODE_VERSION) // console.log(chalk.red(result)) if (!result) { throw new Error(chalk.red('当前版本过低,请升级node版本。 ') + chalk.blue(`最低版本: ${LOW_NODE_VERSION}`)) } }
6.检查root启动 我们可以使用
process.getuid()
来获取uid,如果返回值是0则说明说超级管理员,否则是一般用户。开发中一般是使用一般用户进行开发,如果需要降级,可以使用root-check
这个库进行开发,使用也很简单,代码如下:function checkRoot() { const rootCheck = require('root-check') // 使用之后可降级为用户权限 rootCheck() }
7.用户主目录检查功能开发
const chalk = require('chalk) const pathExists = reuqire('path-exists').sync // 判读目录是否存在 const userHome = require('user-home') // 获取当前用户主目录 function checkUserHome() { if (!userHome || !pathExists(userHome)) { throw new Error(chalk.red('当前登陆用户主目录不存在!')) } }
8.入参数检查和debug模式开发 入口参数就是我们在终端上输入的指令例如:
vue create project
,我们可以得到create和project参数
我们如何得到入口参数呢?这里我们使用minimist
这个库来获取入口参数,代码如下:
const args = require('minimist')(process.argv.slice(2))
const log = require('intest-cli-dev-log')
function checkInputArgs() {
checkArgs()
}
// 设置debug
function checkArgs() {
if(args.debug) {
process.env.LOG_LEVEL = 'verbose'
} else {
process.env.LOG_LEVEL = 'info'
}
log.level = process.env.LOG_LEVEL
}
9.环境变量检查功能开发和npm全局更新功能开发 我们可以使用
dotenv
进行读取.env
的配置内容,具体方法可以参考dotenv官方文档 具体代码如下:const dotEnv = require('dotenv) function checkEnv() { const dotEnvPath = path.resolve(userHome, '.env')// 获取本地.env文件内容 if (pathExists(dotEnvPath)) { config = dotEnv.config({ path: dotEnvPath }) } else { log.error('找不到.env') } createDefaultConfig() // 设置默认配置 } // 设置默认的环境变量 function createDefaultConfig() { const cliConfig = { home: userHome, } if(process.env.CLI_HOME) { cliConfig['cli_home'] = path.join(userHome, process.env.CLI_HOME) } else { cliConfig['cli_home'] = path.join(userHome, DEFAULT_CLI_HOME) } // console.log(cliConfig) process.env['CLI_HOME_PATH'] = cliConfig['cli_home'] }
9.通用npm API封装 实现这个功能需要如下步骤:
- 1.获取当前版本号和模块名
- 2.调用npm API获取说有的版本号和模块名
3.提取所有版本版本号,比对那些版本号是大于当前版本号的
- 使用
lerna
创建get-npm-info
模块lerna create intest-cli-dev-get-npm-info
,并移入到utils/目录下 - 在
core/cli/package.json
下配置 如下,并进行软连接npm link
{ ... "dependencies": { ... "intest-cli-dev-log": "file:../../utils/log", "get-npm-info": "file:../../utils/get-npm-info", } }
get-npm-info
模块下lib/index.js写入如下代码:
'use strict'; const axios = require('axios') const urlJoin = require('url-join') const semver = require('semver') function getNpmInfo(npmName, registry) { // TODO if (!npmName) return const registryUrl = registry || getDefaultRegistry() const res = urlJoin(registryUrl, npmName) return axios.get(res).then(resp => { if (resp.status === 200) { return resp.data } else { return null } }) .catch(err => { return Promise.reject(err) }) } function getDefaultRegistry(isOrigin = false) { return isOrigin ? 'http://registry.npmjs.org/' : 'http://registry.npm.taobao.org/' } async function getNpmVersion(npmName, registry) { const data = await getNpmInfo(npmName, registry); if (data) { // console.log(Object.keys(data.versions)) const r = Object.keys(data.versions) return r } else { return [] } } function getSemverVersion(baseVersion, versionList) { return versionList .filter(version => semver.satisfies(version,`^${baseVersion}`)) .sort((a,b)=>semver.gt(b,a)) } async function getNpmSemverVersion(baseVersion, npmName, registry) { const versionList = await getNpmVersion(npmName, registry) const newVersions = getSemverVersion(baseVersion, versionList) if(newVersions && newVersions.length) return newVersions[0] return } async function getNpmLatest(npmName, registry) { const versions = await getNpmVersion(npmName, registry) if(versions) { return versions.sort((a,b)=>semver.gt(a,b))[0] } return null } module.exports = { getNpmInfo, getNpmVersion, getNpmSemverVersion, getDefaultRegistry, getNpmLatest };
- 使用
- 4.获取最新的版本号,提示用户更新到该版本号
在core/cli/lib/index.js下写入如下代码:
const { getNpmSemverVersion } = require('get-npm-info') async function checkGlobalUpdate() { // 1.获取当前版本号和模块名 const { version, name } = pkg // 2.调用npm API获取说有的版本号和模块名 const lastVersion = await getNpmSemverVersion(version, name) // 3.提取所有版本版本号,比对那些版本号是大于当前版本号的 // 4.获取最新的版本号,提示用户更新到该版本号 if(lastVersion && semver.gt(lastVersion,version)) log.warn(chalk.yellow(`请手动更新${name} 当前版本:${version} 最新版本:${lastVersion}`)) }
commander到使用
快速实现一个commander脚手架,代码如下:
function registryCommand() {
program
.name(Object.keys(pkg.bin)[0])
.usage('<command> [options]')
.version(pkg.version)
.option('-d, --debug', '是否开启调试模式', false)
.option('-tp, --targerPath <targetPath>', '是否指定本地调试文件路径', '')
program
.command('init [projectName]')
.option('-f,--force', '是否强制初始化项目')
.action(exec)
// 开启debug模式
program.on('option:debug', function(){
if (program.debug) {
process.env.LOG_LEVEL = 'verbose'
} else {
process.env.LOG_LEVEL = 'info'
}
log.level = process.env.LOG_LEVEL
log.verbose('test')
})
// 监听targetPath,并设置环境变量
program.on('option:targerPath', function() {
// console.log(this.targerPath)
process.env.CLI_TARGET_PATH = this.targerPath
})
// 对未知命令进行监听
program.on('command:*', function(obj) {
// 获取所有注册的命令
const allRegistryCommand = program.commands.map(cmd=>cmd.name())
console.log(chalk.red('未知命令:' + obj[0]))
})
program.parse(process.argv)
// 后面调用
// console.log(program.args)
if(program.args && !program.args.length){
program.outputHelp()
}
}