开发一个自己的npm模块(个人脚手架)

需要达到的效果

  • 实现在命令行中可以直接运行代码,要上传到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文件增加用户根目录常量

github的vue模板地址

// 通过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
  • 比如选择技术等…
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值