需要达到的效果
- 实现在命令行中可以直接运行代码,要上传到npm上
- lb-cli install 安装命令
- lb-cli config 查看配置文件
- 等功能
1、先进行初始化 npm init -y 创建package.json
2、新建src文件夹,表示脚手架的源文件,创建入口main.js
main.js主要功能是打印我们的命令行
3、新建bin文件夹/www文件,表示使用那个核心文件,运行的话就是运行的www文件,www文件又会去引用某一个主模块或者主文件
bin/www文件
#! /usr/bin/env node // 声明这是一个可执行文件,是可以运行的,并且在node环境下执行
require('../src/main.js'); // 执行的时候就会自动去找这个文件
4、因为有些会用es6语法或者node版本很低,还有可能会用到import,默认在在node里是不能直接运行的,那我们要用到babel进行转译 npm install babel-cli babel-env -D下载babel-cli进行转译,可以实时的把源文件打包成输出文件,下载babel-env,可以根据当前环境转译出特定的代码
npm install babel-cli babel-env babel-core -D
5、包安装完了之后会自动生成一个.babelrc文件,它编译ES6语法的时候靠得就是这个.babelrc,这个文件会自动生成一些配置,可以用它的,也可以我们自己配一下
{
"presets": [ //预设,告诉它用哪个包去转
["env",{
"targets":{// 转成哪个版本的
"node":"current"// 版本号是多少(current:当前版本)
}
}],
"react",
"stage-0"
]
}
- 如果后面运行是报Cannot use import statement outside a module,可能是bable安装没有成功,或者配置不对
6、创建一个命令 ,将src下的文件打包到dist文件夹目录下
- 打开package.json,在scripts下增加一个脚本
{
"name": "lb-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts":{
// 用bable转译src文件夹输出到dist
"compile":"bable src -d dist"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-env": "^2.4.1"
}
}
- 这样我们就可以运行npm run compile指令,把src下的文件打包到dist文件夹下
npm run compile
7、还可以加一个命令watch,可以监控src文件夹,一有改变可以产出新的文件
{
"script":{
"watch":"npm run compile -- --watch" // --代表要给命令传一个监控的参数
}
}
运行
npm run watch
8、www文件会去引用某一个主模块或者主文件(映射命令行工具,配置lb-cli指令能被node识别)
8.1、在package.json加个参数bin,和src同级,用来描述运行的命令是哪一个
{
"bin":{
"lb-cli":"./bin/www" // lb-cli指令可以自定义,./bin/www是指定执行文件路径
}
}
8.2、在根目录下运行npm link 链接到npm全局目录中去,在本地开发npm模块的时候,我们可以使用npm link命令,将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试
npm link // 运行完这个命令后,lb-cli就可以全局执行了
- 如果报错了
9、打印帮助,lb-cli --help(类似于node --help)
npm install commander // 安装命令行管家工具,可以帮我们配命令行的一些名字
安装完成后,在src下的main文件内导入并命名为program
import program from 'commander' //报错的话 var program = require('commander')
program.version('V1.0','-v --version').parse(process.argv) // 配置版本号 ,指令。后半句是所有用户的参数都在argv里
配置完成后就能看到下面的效果了
把package.json中的一些变量引入进来,在src下建一个常量文件夹util,在里面建一个constants.js
配置constants.js
// 这个文件存放用户所需要的常量,用起来比较方便
const { version } = require('../../package.json') //导入package.json的版本号
export const VERSION = version //导出
在main.js导入版本号
import { VERSION } from './utils/constants' // 导入版本号
program.version(VERSION,'-v --version').parse(process.argv) // 版本号
10、配置几个常用的指令
lb-cli config set a 1
lb-cli install
在main.js中增加配置
program.command("install") //指令
.description("install template") //描述
.alias("i") // 指令缩写
.action(()=>{
// 这里面写功能逻辑
})
如果是很多功能的话,可把把所有指令封装一个对象循环处理,在上一段代码前写一个映射表
let actionMap = {
install:{
alias:'i', // 缩写
description:'install template', // 描述
examples:[ // 例子
'lb-cli i',
'lb-cli install'
]
},
config:{
alias:'c',
description:'config .lbclirc', // 插件的描述文件,后面用到再创建
examples:[
'lb-cli config set <k> <v>', // 设置key和value
'lb-cli config get <k>',
'lb-ci config remove <k>'
]
},
// 最后一些必须写*,表示其他的找不到指令了
"*":{
description:'not found' ,
examples:[]
}
}
修改配置代码,改为循环生成配置,加上这段代码后,就可以
Object.keys(actionMap).forEach(action=>{ // 遍历对象,action为键值对的key
program.command(action) //指令
.description(actionMap[action].description) //描述
.alias(actionMap[action].alias) // 指令缩写
.action(()=>{
// 这里面写功能逻辑
console.log(action)
})
})
把用法也打印出来,监听一个时间,当用户敲-h或者–help时打印下使用方法
function help(){
console.log('以下是使用例子')
Object.keys(actionMap).forEach(action=>{ // 遍历对象,action为键值对的key
actionMap[action].examples.forEach(examples=>{
// 打印使用方法例子
console.log(examples)
})
})
}
program.on('-h',help)
program.on('--help',help)
11、把每个指令的功能单独抽离()
11.1 在src下创建index.js
index.js主要功能是根据用户敲的指令执行具体执行的代码文件
// 命令行的命令拿到后,这里是主流程控制
let apply = ()=>{
}
export default apply
11.2 在main.js中引进来
import main from './index.js' // 导入功能文件
// ...
// 在循环生成配置时
Object.keys(actionMap).forEach(action=>{ // 遍历对象,action为键值对的key
program.command(action) //指令
.description(actionMap[action].description) //描述
.alias(actionMap[action].alias) // 指令缩写
.action(()=>{
// 判断一下你当前用的是什么操作
if(action==='config'){
// 实现可以更改配置文件
main(action,...process.argv.slice(3)) //将指令的参数传过去,因为参数个数不确定,所以用node方法解一下
}else if(action=='install'){
// 下载脚手架
main(acticon) // 安装的时候不需要传参
}
})
})
11.3 在index.js中对内容进行一些分发,指定到每一个文件进行处理。在src下创建文件config.js和install.js
- config.js
// 专门管理.lbclirc文件(在当前的用户目录下)
let config = (action,k,v)=>{
console.log(action,k,v)
}
export default config
- install.js
let install = ()=>{
}
export default install
11.4 封装公共方法
- 在utils文件夹下创建common.js
1、因为bable-env 会将import转化为require,会将 export default 转换为module.export ={default:xxx},在导出的时候,多了一层对象结构,所以这里统一封装一个方法进行处理
export let betterRequire = (abspath)=>{
let module = require(absPath)
if(module.default){
return module.default
}
return module
}
- 在index.js中使用
// 命令行的命令拿到后,这里是主流程控制
import {betterRequire} from './utils/common' // 导入公共方法封装文件
import {resolve} from 'path' // 导入nodejs自带的路径插件
let apply = ()=>{
betterRequire(resolve(_dirname,`./${action}`))(...args) // 转成绝对路径,调用方法,把参数传入
}
export default apply
12 编写lb-cli config set key value的真正逻辑(本质上就是找到.lbclirc文件修改里面的参数)
在constants.js文件增加用户根目录常量
// 通过node方法找到用户根目录,由于macos和windows要取的值不一样,要判断一下(windows统一叫win32)
const HOME = process.env[process.platform==='win32'?'USERPROFILE':'HOME']
export const RC = `${HOME}/.lbclirc`
// RC配置下载模板的地方,给github的api用来拉项目[github的vue模板地址](https://api.github.com/orgs/vuejs-templates/repos)
export const DEFAULTS = {
registry:'zhufeng-template', // 希望仓库从哪来
type:'USER', // 表示定义在个人目录(users)下还是用户目录(orgs)下
}
在utils文件夹下创建rc.js
// 导入RC配置文件,DEFAULT默认配置
import {RC,DEFAULTS} from './constants'
// 定义几个公共的工具方法
export let set = ()=>{
}
export let get = ()=>{
}
export let remove = ()=>{
}
export let getAll = ()=>{
}
在config.js文件下引入,并用switch判断
import {set,get,romove,getAll} from './utils/rc'
let config = (action,k,v)=>{
switch(action)
case 'get':
if(k){
get(k)
}else{
getAll()// 没有键值取全部
}
break
case 'set'
set(k,v)
break
case 'remove'
remove(k)
break
}
对rc.js文件的方法进行真正的状态操作,因为要获取文件进行读写操作,所以设计到异步,在所有方法前加async ,
安装包npm install ini,a=b格式在代码里面叫ini,这个包可以帮我们将key等于value转成对象,也可以把对象转成这种格式,它有两个方法,decode和encode类似于JSON.parse和JSON.stringify
// 导入RC配置文件,DEFAULT默认配置
import {RC,DEFAULTS} from './constants'
// 导入key/valut和对象转换方法
import {decode,encode} from 'ini'
// 导入node默认包util的promise化方法
import {promisify} from 'util'
// 导入node默认包fs的读文件方法fs
import fs from 'fs'
// 定义方法,promisify可以把这个方法编程promise方法
let exists = promisify(fs.exists) // 是否存在
let readFile = promisify(fs.readFile) // 写
let writeFile = promisify(fs.writeFile) // 读
// 定义几个公共的工具方法
export let set = async (k)=>{
let has =await exists(RC) // 判断文件是否存在
let opts //定义一个变量用来存获取到的文件
if(has){
opts = await readFile(RC,'utf8')
opts = decode(opts) // 用ini的decode方法解下码,将key/valut转为对象
return opts
}
return ''
}
export let get = async ()=>{
let has =await exists(RC) // 判断文件是否存在
let opts //定义一个变量用来存获取到的文件
if(has){
opts = await readFile(RC,'utf8')
opts = decode(opts) // 用ini的decode方法解下码,将key/valut转为对象
Object.assign(opts,{[k]:v})
}else{
opts = Object.assign(DEFAULTS,{[k]:v})
}
await writeFile(RC,encode(opts),'utf8')
}
export let remove = async ()=>{
let has =await exists(RC) // 判断文件是否存在
let opts //定义一个变量用来存获取到的文件
if(has){
opts = await readFile(RC,'utf8')
opts = decode(opts) // 用ini的decode方法解下码,将key/valut转为对象
delete opts[k]
await writeFile(RC,encode(opts),'utf8')
}
}
export let getAll = async ()=>{
let has =await exists(RC) // 判断文件是否存在
let opts //定义一个变量用来存获取到的文件
if(has){
opts = await readFile(RC,'utf8')
opts = decode(opts) // 用ini的decode方法解下码,将key/valut转为对象
return opts
}
return {}
}
因为rc文件的方法都是promise,所以在config.js使用这些方法的时候也要加上await
import {set,get,romove,getAll} from './utils/rc'
let config = async (action,k,v)=>{
switch(action)
case 'get':
if(k){
let key = await get(k)
console.log(key)
}else{
let obj = await getAll()// 没有键值取全部
Object.keys(obj).forEach(key=>{
console.log(`${key}=${obj[key]}`)
})
}
break
case 'set'
await set(k,v)
break
case 'remove'
await remove(k)
break
}
13 编写lb-cli istall的真正逻辑(本质上就是下载模板)
修改install.js
let install = ()=>{
// 下载模板 选择模板使用
// 通过配置文件,获取模板信息(有哪些模板)
let list = await repoList()
list = list.map(({name})=name) // 只要name属性
console.log(list) // 出来的是模板数组
}
export default install
核心功能是怎么去写repolist方法以及版本号tagList方法(怎么获取这个模板列表信息,和版本号列表)
- 在utils下创建一个远程仓库的获取方式文件git.js
export let repoList = async()=>{
}
因为在node里面下载要请求别人的接口,所以要安装一个爬虫模块 npm install request,并在git.js导入
import request from 'request' // 导入爬虫模块
import {getAll} from './rc' // 拿到我们所有的config参数
// 封装一个请求方法
let fetch = async (url)=>{
// 封装一个promise
return new Promise((resolve,reject)=>{
let config = {
url,
methods:'get',
headers:{
'user-agent':'' // 下载git的资源必须加这个参数,这个是git的规定
}
}
// 参数是固定的,根据request模块文档来的
request(config,(err,response,body)=>{
if(err){
reject(err)
}
resolve(JSON.parse(body)) // 返回的是一个字符串类型,需要转成json
})
})
}
// 配置下载版本号列表
export let tagList = async(repo)=>{
// 获取所有config配置
let config = await getAll()
// 动态配置模板下载地址
let api = `https://api.github.com/repos/${config.registry}/${repo}/tags`
return await fetch(api)
}
// 配置下载地址列表
export let repoList = async()=>{
// 获取所有config配置
let config = await getAll()
// 动态配置模板下载地址
let api = `https://api.github.com/${config.type}/${config.registry}/repos`
return await fetch(api)
}
在install.js导入地址列表方法,这一步走完就可以下载模板了
- 为了命令行界面有进度的感觉,可以在下载模板时增加一个命令行的动画效果,安装命令行动画插件 npm install ora
- 为了让用户可以选择模板,安装让用户选择模板的包 npm install inquirer
import {repoList, tagList} from './utils/git' // 导入地址列表方法和版本号列表方法
import ora from 'ora' // 导入命令行动画插件
import inquirer from 'inquirer' // 导入用户选择模板插件
let install = ()=>{
// 下载模板 选择模板使用
//创建命令行动画
let loading = ora('fetching template......')
//运行动画
loading.start()
// 通过配置文件,获取模板信息(有哪些模板)
let list = await repoList()
//关闭动画
loading.succeed()
list = list.map(({name})=name) // 只要name属性
console.log(list) // 出来的是模板数组
// inquirer.prompt方法返回的是一个promise
let answer = await inquirer.prompt([
{
type:'list',
name:'project',
choices:list,
questions:'pleace choice template(请选择模板)'
}
])
console.log(answer.project)
// 项目名字
let project = answer.project
// 获取当前项目版本号tag
//创建命令行动画
let loading = ora('fetching tag(获取版本)......')
//运行动画
loading.start()
// 通过配置文件,获取版本信息(有哪些版本)
list = await tagList()
//关闭动画
loading.succeed()
list = list.map(({name})=name) // 只要name属性
// inquirer.prompt方法返回的是一个promise
let answer = await inquirer.prompt([
{
type:'list',
name:'tag',
choices:list,
questions:'pleace choice tag(请选择模板)'
}
])
let tag = answer.tag
}
export default install
模板和版本都选择完成以后就是下载了,需要安装下载git仓库包 npm install download-git-repo。
如果要设置缓存区的话(设置缓存可以在第二次下载的时候直接从缓存区拿文件,节省了下载的时间,并且lb-cli init指令是将已经下载的文件拷贝到需要的位置,也需要缓存),需要配置缓存区目录,需要在utils文件夹下的constants.js文件定义下载缓存目录,这里就设置为用户根目录下的.template文件夹,还需要在git.js中封装下载方法
- 加在constants.js文件最后面
// 下载目录
export const DOWNLOAD = `${HOME}/.template`
- 在git.js代码增加
// 导入git下载模板插件
import downLoadGit from 'download-git-repo'
// 导入本地缓存区地址
import {DOWNLOAD} from './utils/constants'
// ...
// 封装git下载方法,有两个参数,从哪下载,存放到哪
export let download = async (src,dest)=>{
return new Promise((resolve,reject)=>{
downLoadGit(src,dest,(err)=>{
if(err){
reject()
}
resolve()
})
})
}
// 封装git下载到本地方法,需要两个参数,一个是项目名,一个是版本号
export let downloadLocal = async(project,version)=>{
let conf = await getAll()
let api = `${conf.registry}/${project)`
if(version){
api += `#{version}` // 如果有版本号,以哈希的方式拼上版本号
}
return await download(api,DOWNLOAD+'/'+project) // 下载地址,存放地址和文件名
}
- 在install.js代码编辑
import {repoList, tagList, downloadLocal} from './utils/git' // 导入地址方法和版本号方法和下载git模板到本地方法
import ora from 'ora' // 导入命令行动画插件
import inquirer from 'inquirer' // 导入用户选择模板插件
let install = ()=>{
// 下载模板 选择模板使用
//创建命令行动画
let loading = ora('fetching template......')
//运行动画
loading.start()
// 通过配置文件,获取模板信息(有哪些模板)
let list = await repoList()
//关闭动画
loading.succeed()
list = list.map(({name})=name) // 只要name属性
console.log(list) // 出来的是模板数组
// inquirer.prompt方法返回的是一个promise
let answer = await inquirer.prompt([
{
type:'list',
name:'project',
choices:list,
questions:'pleace choice template(请选择模板)'
}
])
console.log(answer.project)
// 项目名字
let project = answer.project
// 获取当前项目版本号tag
//创建命令行动画
let loading = ora('fetching tag(获取版本)......')
//运行动画
loading.start()
// 通过配置文件,获取版本信息(有哪些版本)
list = await tagList()
//关闭动画
loading.succeed()
list = list.map(({name})=name) // 只要name属性
// inquirer.prompt方法返回的是一个promise
let answer = await inquirer.prompt([
{
type:'list',
name:'tag',
choices:list,
questions:'pleace choice tag(请选择模板)'
}
])
let tag = answer.tag
// 调用下载到本地方法,下载文件
//创建命令行动画
let loading = ora('fetching project(下载工程中)......')
//运行动画
loading.start()
await downloadLocal(project,tag)
console.log('下载功能')
//关闭动画
loading.succeed()
}
export default install
14 到这里,一个最基础的脚手架基本上就完成了,
其他的一些功能
- 比如vue会使用模板引擎
- 比如vue init 可以把当前下载好的模板生成到项目目录中
- 比如卸载vue uninstall
- 比如选择技术等…