【nodejs】脚手架从零开始搭建JBD

🛎️脚手架编写


脚手架框架

  • bin

    • www.js
  • src

    • contant.js

    • create.js

    • main.js

  • package-lock.json

  • package.json

在这里插入图片描述


🛠️插件安装

devDependencies & dependencies

脚本名称脚本作用
commander读取版本,设定选项(option),开发命令行工具
consolidateexpress中的模板引擎可以覆盖其他模板
download-git-repo可以通过git的方式下载模板到本地
ejs模板库,json生成html和consolidate配合使用
inquirer命令行交互
metalsmith批量处理模板
ora优化加载等待的交互
chalk美化终端
ncp判断文件是否存在
axioshttp库发送请求

在这里插入图片描述

npm i

编写bin文件

bin文件下创建 www.js 文件

输入

#! /usr/bin/env node

require("../src/main.js")
  • 回到 package.json 添加如下启动项
  "bin": {
    "xiu": "./bin/www.js"
  },

⭐编写src文件


🚀main.js

版本命令行生成以及文件路径选择都在这里编写

const {version} = require('../package.json');
const path = require('path')
const program  = require('commander');

编写文件创建和帮助指令

const mapActions = {
    create: { 
        alias: "c",
        description:"create a project",
        examples:["xiu create <project-name>"]
    },
    config: { 
        alias:"conf",
        description:"config project variable",
        examples:["xiu config set<k><v>","xiu config get <k>"]
    },
    "*": { 
        alias:"",
        description:"command not found",
        examples:[]
    }
};
  • 完成上一步操作后,我们需要逐步进行命令选择

  • 需要使用 Reflect 中的 ownkeys进行每条命令的遍历

  • 最后使用 forEach进行循环操作

Reflect.ownKeys(mapActions).forEach((action) => {
    program
        .command(action)
        .alias(mapActions[action].alias)
        .description(mapActions[action].description)
        .action(() => {
        if(action === "*") 
            console.log(mapActions[action].description) 
        else{
            require(path.resolve(__dirname,action))(...process.argv.slice(3));
        }
    });
});

command :命令行(对应mapActions中的每一个动作)

alias :别称,也就是mapActions中的alias

description:同样对应mapActions中的description

因为这边是对mapActions进行循环遍历,所以每一个在mapActions中的属性都需要遍历到

  • 最后一行的判断
help事件
program.on("--help",() => {
    console.log("\nExamples:");
    Reflect.ownKeys(mapActions).forEach((action)=> {
        mapActions[action].examples.forEach((example)=> {
            console.log(example)
        });
    });
});
  • 执行help的时候输出mapActions和命令行自带的option

  • 在这里插入图片描述

版本显示
program.version(version).parse(process.argv);
main代码
console.log("welcome xiu");

const {version} = require('../package.json');
const path = require('path')
const program  = require('commander');
const mapActions = {
    create: { 
        alias: "c",
        description:"create a project",
        examples:["xiu create <project-name>"]
    },
    config: { 
        alias:"conf",
        description:"config project variable",
        examples:["xiu config set<k><v>","xiu config get <k>"]
    },
    "*": { 
        alias:"",
        description:"command not found",
        examples:[]
    }
};
Reflect.ownKeys(mapActions).forEach((action) => {
    program
        .command(action)
        .alias(mapActions[action].alias)
        .description(mapActions[action].description)
        .action(() => {
        if(action === "*") 
            console.log(mapActions[action].description) 
        else{
            require(path.resolve(__dirname,action))(...process.argv.slice(3));
        }
    });
});
// help event
program.on("--help",() => {
    console.log("\nExamples:");
    Reflect.ownKeys(mapActions).forEach((action)=> {
        mapActions[action].examples.forEach((example)=> {
            console.log(example)
        });
    });
});
program.version(version).parse(process.argv);

💡create.js

如果纯复刻,建议先去下面把 constant.js写了再回来

模板的选择模板的复制终端的选择终端的样式都在这里实现

const axios = require('axios')
const ora = require('ora')
const Inquirer = require('inquirer')
const path = require('path')
// 包装 
const {promisify} = require('util')
let downLoadGitRepo = require('download-git-repo')
downLoadGitRepo = promisify(downLoadGitRepo) // 装成ES6
// 复制
let ncp = require('ncp')
ncp = promisify(ncp)
// 复杂选择
const fs = require('fs')
const metalSmith = require('metalsmith')
let {render} = require('consolidate').ejs
render = promisify(render)
const {downloadDirectory} = require('./constant')
const download = require('download-git-repo')
// 美化终端
const chalk = require('chalk')

获取仓库信息

// 获取仓库信息
const fetchRepoList = async() => {
    const {data} = await axios.get("/*请求地址*/ https://api/orgs/repos")
    return data
}

抓取版本列表

// 抓取版本(tag)列表
const fetchTagList = async(repo) => {
    const {data} = await axios.get("/*请求地址*/" `https://api/orgs/${repo}/tags`)
    return data
}

下载项目

const downLoad = async(repo, tag) => {
    let api = `xiu/${repo}`
    if(tag) {
        api += `#${tag}`
    }
    const tempdest = `${downloadDirectory}/${repo}`
    await downLoadGitRepo(api,tempdest)
    return tempdest
}

repo 就是仓库下的模板名称

tag 就是每个模板的版本号

编写加载项

再完成上面几步之前会有一个加载的过程,重复的加载我们可以封装来完成

const waitFnLoading = (fn,message) => async(...args) => {
    // loading 加载
    const spinner = ora(message)
    spinner.start()
    let repos = await fn(...args)
    spinner.succeed();
    return repos
}

这里有两个函数体变量

(fn,message) , async(…args)

前者用来接收执行的函数和发出的提示

后者用来对执行函数自带参数进行调用

  • 接下来要做的就是导出下载模块

导出下载模块

交互选择
    let repos = await waitFnLoading(fetchRepoList,'fetch template...')()
    // 交互选择
    repos = repos.map(item => item.name)
    const {repo} = await Inquirer.prompt({
        name: 'repo',
        type: 'list',
        message: 'please choise a template',
        choices : repos, // 选择列表
    });
获取对应的版本号
let tags = await waitFnLoading(fetchTagList,'fetch template tag...')(repo)
    tags = tags.map((item) => item.name)
    const {tag} = await Inquirer.prompt({
        name: 'repo',
        type: 'list',
        message: 'please choise a tags for template',
        choices : tags, // 选择列表
    });

交互选择 和 获取对应的版本号 本质上没有差别

都是通过 inquirer 交互页面查看每个版本进行选择

不同的地方是:

异步调用的 加载项不同:

let repos = await waitFnLoading(fetchRepoList,'fetch template...')<mark>()</mark>

let tags = await waitFnLoading(fetchTagList,'fetch template tag...')<mark>(repo)</mark>
  • 下载的项目首先是临时存放到本地,之后再对存放的内容的文件名称和当前路径下的文件匹配有无重复最后实现模板复制到当前路径下。

  • 在一些简单模板下没有ask.js 但是在绝大部分的复杂模板下有 ask.js文件,这就需要我们对ask.js文件进行访问和重编写

这里需要使用到 metalSmith 对模板的内容进行批量处理

 // 下载项目 返回临时的存放目录
    const result = await waitFnLoading(downLoad,'downloading...')(repo,tag)
    if(!fs.existsSync(path.join(result,'ask.js'))) {
        await ncp(result,path.resolve(proname))  
    } else {
        // 复杂模板需要选择
        await new Promise((resolve,reject) => {
            metalSmith(__dirname)
                .source(result)
                .destination(path.resolve(proname))
                .use(async(files,metal,done) => {
                    // files 现在就是所有的文件
                    const args = require(path.join(result,'ask.js'))
                    // 选择
                    const obj = await Inquirer.prompt(args)
                    const meta = metal.metadata()
                    Object.assign(meta,obj)
                    delete files["ask.js"]
                    done()
                })
                .use((files,metal,done)=>{
                    const obj = metal.metadata()
                    Reflect.ownKeys(files).forEach(async(file)=>{
                        if(file.includes("js")|| file.includes("json")) {
                            let content = files[file].contents.toString()
                            if(content.includes("<%")) {
                                content = await render(content, obj)
                                files[file].contents = Buffer.from(content) // 渲染
                            }
                        }
                    })
                    done()
                }).build(err => {
                    if(err) {
                        reject()
                    }else {
                        resolve()
                    }
                })
        })
    }
  • done() 相当于 node.js的中间件 next()
const result = await waitFnLoading(downLoad,'downloading...')(repo,tag)
  • 这一行代码获取到的result 就是对应模板和版本号之后的结果
if(!fs.existsSync(path.join(result,'ask.js'))) {
        await ncp(result,path.resolve(proname))  
    } else {}
  • 这里的 if…else… 是对文件中是否存在 ask.js 进行判断

  • 有就是复杂模板 需要重编写

            metalSmith(__dirname)
                .source(result)
                .destination(path.resolve(proname))
                .use(async(files,metal,done) => {
                    // files 现在就是所有的文件
                    const args = require(path.join(result,'ask.js'))
                    // 选择
                    const obj = await Inquirer.prompt(args)
                    const meta = metal.metadata()
                    Object.assign(meta,obj)
                    delete files["ask.js"]
                    done()
                })
  • 这一处代码就是对模板下的所有文件匹配找到ask.js然后遍历执行里面所有的问题最后删除 delete files[“ask.js”]
.use((files,metal,done)=>{
                    const obj = metal.metadata()
                    Reflect.ownKeys(files).forEach(async(file)=>{
                        if(file.includes("js")|| file.includes("json")) {
                            let content = files[file].contents.toString()
                            if(content.includes("<%")) {
                                content = await render(content, obj)
                                files[file].contents = Buffer.from(content) // 渲染
                            }
                        }
                    })
                    done()
                })
  • 使用 metalSmith下的use 对刚刚遍历执行的问题中找到以‘<%’开头的选项就是要用户选择或是填写的

  • 这里说明一下,前面删除的 ask.js 为什么这里还可以访问?

  • 原因在于同样都是中间件,中间件之间是可以互相访问变量内容的,所以删除的ask.js在use里同样算是变量未删除。

成功退出

没什么好说的,直接复制改改就好了

    console.log(`
${chalk.green('thanks to use my CLI')}
${chalk.white.bold.bgBlue('success download')}
----------------------------------
⭕ ${chalk.red('version of this')}${chalk.white(repo,tag)}${chalk.white('buy me a coofee')}
design by ${chalk.bgBlue.yellow('wuchanghua')}™️
----------------------------------
${chalk.bgWhite.blue('如果你足够充满智慧,加入我,和我一起创造 QQ:1453346832 Email: 1453346832@qq.com')}
${chalk.green('Finish to 100%')}
${chalk.green('Welcome')}
`)

在这里插入图片描述

create代码

const axios = require('axios')
const ora = require('ora')
const Inquirer = require('inquirer')
const path = require('path')
// 包装 
const {promisify} = require('util')
let downLoadGitRepo = require('download-git-repo')
downLoadGitRepo = promisify(downLoadGitRepo) // 装成ES6
// 复制
let ncp = require('ncp')
ncp = promisify(ncp)
// 复杂选择
const fs = require('fs')
const metalSmith = require('metalsmith')
let {render} = require('consolidate').ejs
render = promisify(render)
const {downloadDirectory} = require('./constant')
const download = require('download-git-repo')
// 美化终端
const chalk = require('chalk')

// 获取仓库信息
const fetchRepoList = async() => {
    const {data} = await axios.get("/*请求地址*/ https://api/orgs/repos")
    return data
}
// 抓取版本(tag)列表
const fetchTagList = async(repo) => {
    const {data} = await axios.get("/*请求地址*/" `https://api/orgs/${repo}/tags`)
    return data
}
// 下载项目
const downLoad = async(repo, tag) => {
    let api = `xiu/${repo}`
    if(tag) {
        api += `#${tag}`
    }
    const tempdest = `${downloadDirectory}/${repo}`
    await downLoadGitRepo(api,tempdest)
    return tempdest
}
const waitFnLoading = (fn,message) => async(...args) => {
    // loading 加载
    const spinner = ora(message)
    spinner.start()
    let repos = await fn(...args)
    spinner.succeed();
    return repos
}
module.exports = async(proname) => {
    let repos = await waitFnLoading(fetchRepoList,'fetch template...')()
    // 交互选择
    repos = repos.map(item => item.name)
    const {repo} = await Inquirer.prompt({
        name: 'repo',
        type: 'list',
        message: 'please choise a template',
        choices : repos, // 选择列表
    });
    // 获取对应的版本号
    let tags = await waitFnLoading(fetchTagList,'fetch template tag...')(repo)
    tags = tags.map((item) => item.name)
    const {tag} = await Inquirer.prompt({
        name: 'repo',
        type: 'list',
        message: 'please choise a tags for template',
        choices : tags, // 选择列表
    });
    // 下载项目 返回临时的存放目录
    const result = await waitFnLoading(downLoad,'downloading...')(repo,tag)
    if(!fs.existsSync(path.join(result,'ask.js'))) {
        await ncp(result,path.resolve(proname))  
    } else {
        // 复杂模板需要选择
        await new Promise((resolve,reject) => {
            metalSmith(__dirname)
                .source(result)
                .destination(path.resolve(proname))
                .use(async(files,metal,done) => {
                    // files 现在就是所有的文件
                    const args = require(path.join(result,'ask.js'))
                    // 选择
                    const obj = await Inquirer.prompt(args)
                    const meta = metal.metadata()
                    Object.assign(meta,obj)
                    delete files["ask.js"]
                    done()
                })
                .use((files,metal,done)=>{
                    const obj = metal.metadata()
                    Reflect.ownKeys(files).forEach(async(file)=>{
                        if(file.includes("js")|| file.includes("json")) {
                            let content = files[file].contents.toString()
                            if(content.includes("<%")) {
                                content = await render(content, obj)
                                files[file].contents = Buffer.from(content) // 渲染
                            }
                        }
                    })
                    done()
                }).build(err => {
                    if(err) {
                        reject()
                    }else {
                        resolve()
                    }
                })
        })
    }
    console.log(`
${chalk.green('thanks to use my CLI')}
${chalk.white.bold.bgBlue('success download')}
----------------------------------
⭕ ${chalk.red('version of this')}${chalk.white(repo,tag)}${chalk.white('buy me a coofee')}
design by ${chalk.bgBlue.yellow('wuchanghua')}™️
----------------------------------
${chalk.bgWhite.blue('如果你足够充满智慧,加入我,和我一起创造 QQ:1453346832 Email: 1453346832@qq.com')}
${chalk.green('Finish to 100%')}
${chalk.green('Welcome')}
`)
}

💡constant.js

  • 在create中用到的地址下载,你会发现无法复刻到本地原因在于 电脑系统版本不同,临时文件存放路径不同

  • 所以这里可以单独对版本进行判断

const downloadDirectory = `${process.env[process.platform === 'darwin' ? 'Home' : 'USERPROFILE']}/.template`
module.exports = {
    downloadDirectory
}
  • 如果是 ‘darwin’ 那就是 mac 如果不是 其他的都是Windows

🎈完结

祝福你也可以完成自己的脚手架

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
1. 安装nodejs 首先,需要在官方网站 https://nodejs.org/en/ 下载并安装nodejs。根据自己的操作系统选择相应的包进行安装。 2. 安装npm npm是nodejs的官方包管理工具。nodejs安装完成后,npm也会随之安装。可以在终端中输入以下命令进行验证: ``` npm -v ``` 如果输出了npm的版本号,则说明npm安装成功。 3. 创建第一个nodejs应用 接下来,我们创建第一个nodejs应用。在终端中进入到想要创建应用的目录中,然后输入以下命令: ``` mkdir myapp cd myapp npm init ``` 执行npm init命令会创建一个package.json文件,其中包含了应用的基本信息和依赖信息。 4. 创建入口文件 在myapp目录下,创建一个名为index.js的文件,作为应用的入口文件。将以下代码添加到index.js文件中: ``` console.log("Hello World!"); ``` 5. 运行应用 在终端中进入myapp目录,然后输入以下命令: ``` node index.js ``` 如果输出了“Hello World!”,则说明应用运行成功。 6. 安装第三方模块 nodejs有丰富的第三方模块可供使用。可以使用npm安装第三方模块。例如,安装一个用于处理http请求的模块: ``` npm install request --save ``` --save参数表示将模块信息添加到package.json文件中的dependencies字段中。 7. 使用第三方模块 在index.js文件中,引入已安装的模块并使用它。例如,使用request模块发起一个http请求: ``` const request = require('request'); request('https://www.baidu.com', function (error, response, body) { console.log(body); }); ``` 8. 学习nodejs API nodejs API文档详细介绍了nodejs提供的各种模块和函数。可以在官网上查看文档并学习使用。例如,学习使用fs模块读写文件: ``` const fs = require('fs'); fs.writeFile('message.txt', 'Hello Node.js', (err) => { if (err) throw err; console.log('The file has been saved!'); }); fs.readFile('message.txt', 'utf8', (err, data) => { if (err) throw err; console.log(data); }); ``` 以上就是nodejs从零开始学习的基本步骤。除此之外,还可以学习使用Express框架、WebSocket、数据库连接等高级应用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

THIM

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值