资讯专栏INFORMATION COLUMN

【中高级前端必备】手摸手教你撸一个脚手架

caoym / 3156人阅读

摘要:管理文件当前用户目录下文件的增删改查是配置文件是默认的配置发布将本脚手架发布至上。

脚手架

vue-cli, create-react-appreact-native-cli 等都是非常优秀的脚手架,通过脚手架,我们可以快速初始化一个项目,无需自己从零开始一步步配置,有效提升开发体验。尽管这些脚手架非常优秀,但是未必是符合我们的实际应用的,我们可以定制一个属于自己的脚手架(或公司通用脚手架),来提升自己的开发效率。

更多优质文章可戳: https://github.com/YvetteLau/...

脚手架的作用

减少重复性的工作,不需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。

可以根据交互动态生成项目结构和配置文件。

多人协作更为方便,不需要把文件传来传去。

实现的功能

在开始之前,我们需要明确自己的脚手架需要哪些功能。vue init template-name project-namecreate-react-app project-name。我们这次编写的脚手架(eos-cli)具备以下能力(脚手架的名字爱叫啥叫啥,我选用了Eos黎明女神):

eos init template-name project-name 根据远程模板,初始化一个项目(远程模板可配置)

eos config set 修改配置信息

eos config get [] 查看配置信息

eos --version 查看当前版本号

eos -h

大家可以自行扩展其它的 commander,本篇文章旨在教大家如何实现一个脚手架。

效果展示

初始化一个项目

修改.eosrc文件,从 vuejs-template 下载模板

需要使用的第三方库

babel-cli/babel-env: 语法转换

commander: 命令行工具

download-git-repo: 用来下载远程模板

ini: 格式转换

inquirer: 交互式命令行工具

ora: 显示loading动画

chalk: 修改控制台输出内容样式

log-symbols: 显示出 √ 或 × 等的图标

关于这些第三方库的说明,可以直接npm上查看相应的说明,此处不一一展开。

初始化项目

创建一个空项目(eos-cli),使用 npm init 进行初始化。

安装依赖
npm install babel-cli babel-env chalk commander download-git-repo ini inquirer log-symbols ora
目录结构
├── bin
│   └── www             //可执行文件
├── dist
    ├── ...             //生成文件
└── src
    ├── config.js       //管理eos配置文件
    ├── index.js        //主流程入口文件
    ├── init.js         //init command
    ├── main.js         //入口文件
    └── utils
        ├── constants.js //定义常量
        ├── get.js       //获取模板
        └── rc.js        //配置文件
├── .babelrc             //babel配置文件
├── package.json
├── README.md
babel 配置

开发使用了ES6语法,使用 babel 进行转义,

.bablerc
{
    "presets": [
        [
            "env",
            {
                "targets": {
                    "node": "current"
                }
            }
        ]
    ]
}
eos 命令

node.js 内置了对命令行操作的支持,package.json 中的 bin 字段可以定义命令名和关联的执行文件。在 package.json 中添加 bin 字段

package.json
{
    "name": "eos-cli",
    "version": "1.0.0",
    "description": "脚手架",
    "main": "index.js",
    "bin": {
        "eos": "./bin/www"
    },
    "scripts": {
        "compile": "babel src -d dist",
        "watch": "npm run compile -- --watch"
    }
}
www 文件

行首加入一行 #!/usr/bin/env node 指定当前脚本由node.js进行解析

#! /usr/bin/env node
require("../dist/main.js");
链接到全局环境

开发过程中为了方便调试,在当前的 eos-cli 目录下执行 npm link,将 eos 命令链接到全局环境。

启动项目
npm run watch
处理命令行

利用 commander 来处理命令行。

main
import program from "commander";
import { VERSION } from "./utils/constants";
import apply from "./index";
import chalk from "chalk";

/**
 * eos commands
 *    - config
 *    - init 
 */

let actionMap = {
    init: {
        description: "generate a new project from a template",
        usages: [
            "eos init templateName projectName"
        ]
    },
    config: {
        alias: "cfg",
        description: "config .eosrc",
        usages: [
            "eos config set  ",
            "eos config get ",
            "eos config remove "
        ]
        
    },
    //other commands
}

// 添加 init / config 命令
Object.keys(actionMap).forEach((action) => {
    program.command(action)
    .description(actionMap[action].description)
    .alias(actionMap[action].alias) //别名
    .action(() => {
        switch (action) {
            case "config": 
                //配置
                apply(action, ...process.argv.slice(3));
                break;
            case "init":
                apply(action, ...process.argv.slice(3));
                break;
            default:
                break;
        }
    });
});

function help() {
    console.log("
Usage:");
    Object.keys(actionMap).forEach((action) => {
        actionMap[action].usages.forEach(usage => {
            console.log("  - " + usage);
        });
    });
    console.log("
");
}
program.usage(" [options]");
// eos -h 
program.on("-h", help);
program.on("--help", help);
// eos -V   VERSION 为 package.json 中的版本号
program.version(VERSION, "-V --version").parse(process.argv);

// eos 不带参数时
if (!process.argv.slice(2).length) {
    program.outputHelp(make_green);
}
function make_green(txt) {
    return chalk.green(txt); 
}
下载模板

download-git-repo 支持从 Github、Gitlab 下载远程仓库到本地。

get.js
import { getAll } from "./rc";
import downloadGit from "download-git-repo";

export const downloadLocal = async (templateName, projectName) => {
    let config = await getAll();
    let api = `${config.registry}/${templateName}`;
    return new Promise((resolve, reject) => {
        //projectName 为下载到的本地目录
        downloadGit(api, projectName, (err) => {
            if (err) {
                reject(err);
            }
            resolve();
        });
    });
}
init 命令 命令行交互

在用户执行 init 命令后,向用户提出问题,接收用户的输入并作出相应的处理。命令行交互利用 inquirer 来实现:

inquirer.prompt([
    {
        name: "description",
        message: "Please enter the project description: "
    },
    {
        name: "author",
        message: "Please enter the author name: "
    }
]).then((answer) => {
    //...
});

视觉美化

在用户输入之后,开始下载模板,这时候使用 ora 来提示用户正在下载模板,下载结束之后,也给出提示。

import ora from "ora";
let loading = ora("downloading template ...");
loading.start();
//download
loading.succeed(); //或 loading.fail();

index.js
import { downloadLocal } from "./utils/get";
import ora from "ora";
import inquirer from "inquirer";
import fs from "fs";
import chalk from "chalk";
import symbol from "log-symbols";

let init = async (templateName, projectName) => {
    //项目不存在
    if (!fs.existsSync(projectName)) {
        //命令行交互
        inquirer.prompt([
            {
                name: "description",
                message: "Please enter the project description: "
            },
            {
                name: "author",
                message: "Please enter the author name: "
            }
        ]).then(async (answer) => {
            //下载模板 选择模板
            //通过配置文件,获取模板信息
            let loading = ora("downloading template ...");
            loading.start();
            downloadLocal(templateName, projectName).then(() => {
                loading.succeed();
                const fileName = `${projectName}/package.json`;
                if(fs.existsSync(fileName)){
                    const data = fs.readFileSync(fileName).toString();
                    let json = JSON.parse(data);
                    json.name = projectName;
                    json.author = answer.author;
                    json.description = answer.description;
                    //修改项目文件夹中 package.json 文件
                    fs.writeFileSync(fileName, JSON.stringify(json, null, "	"), "utf-8");
                    console.log(symbol.success, chalk.green("Project initialization finished!"));
                }
            }, () => {
                loading.fail();
            });
        });
    }else {
        //项目已经存在
        console.log(symbol.error, chalk.red("The project already exists"));
    }
}
module.exports = init;
config 配置
eos config set registry vuejs-templates

config 配置,支持我们使用其它仓库的模板,例如,我们可以使用 vuejs-templates 中的仓库作为模板。这样有一个好处:更新模板无需重新发布脚手架,使用者无需重新安装,并且可以自由选择下载目标。

config.js
// 管理 .eosrc 文件 (当前用户目录下)
import { get, set, getAll, remove } from "./utils/rc";

let config = async (action, key, value) => {
    switch (action) {
        case "get":
            if (key) {
                let result = await get(key);
                console.log(result);
            } else {
                let obj = await getAll();
                Object.keys(obj).forEach(key => {
                    console.log(`${key}=${obj[key]}`);
                })
            }
            break;
        case "set":
            set(key, value);
            break;
        case "remove":
            remove(key);
            break;
        default:
            break;
    }
}

module.exports = config;
rc.js

.eosrc 文件的增删改查

import { RC, DEFAULTS } from "./constants";
import { decode, encode } from "ini";
import { promisify } from "util";
import chalk from "chalk";
import fs from "fs";

const exits = promisify(fs.exists);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

//RC 是配置文件
//DEFAULTS 是默认的配置
export const get = async (key) => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, "utf8");
        opts = decode(opts);
        return opts[key];
    }
    return "";
}

export const getAll = async () => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, "utf8");
        opts = decode(opts);
        return opts;
    }
    return {};
}

export const set = async (key, value) => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, "utf8");
        opts = decode(opts);
        if(!key) {
            console.log(chalk.red(chalk.bold("Error:")), chalk.red("key is required"));
            return;
        }
        if(!value) {
            console.log(chalk.red(chalk.bold("Error:")), chalk.red("value is required"));
            return;
        }
        Object.assign(opts, { [key]: value });
    } else {
        opts = Object.assign(DEFAULTS, { [key]: value });
    }
    await writeFile(RC, encode(opts), "utf8");
}

export const remove = async (key) => {
    const exit = await exits(RC);
    let opts;
    if (exit) {
        opts = await readFile(RC, "utf8");
        opts = decode(opts);
        delete opts[key];
        await writeFile(RC, encode(opts), "utf8");
    }
}
发布

npm publish 将本脚手架发布至npm上。其它用户可以通过 npm install eos-cli -g 全局安装。
即可使用 eos 命令。

项目地址

本项目完整代码请戳: https://github.com/YvetteLau/...

编写本文,虽然花费了一定时间,但是在这个过程中,我也学习到了很多知识,谢谢各位小伙伴愿意花费宝贵的时间阅读本文,如果本文给了您一点帮助或者是启发,请不要吝啬你的赞和Star,您的肯定是我前进的最大动力。
https://github.com/YvetteLau/...

参考文章:

[1] npm依赖文档(https://www.npmjs.com/package...

关注公众号,加入技术交流群。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106017.html

相关文章

  • 前端之从零开始系列

    摘要:只有动手,你才能真的理解作者的构思的巧妙只有动手,你才能真正掌握一门技术持续更新中项目地址求求求源码系列跟一起学如何写函数库中高级前端面试手写代码无敌秘籍如何用不到行代码写一款属于自己的类库原理讲解实现一个对象遵循规范实战手摸手,带你用撸 Do it yourself!!! 只有动手,你才能真的理解作者的构思的巧妙 只有动手,你才能真正掌握一门技术 持续更新中…… 项目地址 https...

    Youngdze 评论0 收藏0
  • PHP--摸手,教你撸一个会自动补全的命令行工具1

    摘要:前言一次在使用的时候,发现使用命令行的时候有些关键字会自动提示。介绍随着,等框架的流行,命令行工具越来越流行,但是很多时候命令太多,根本无法记住所有参数,或者参数太长输入太不方便。下文,我们一起来优化这个工具。备注不支持自动补全 前言 一次在使用symfony的时候,发现使用命令行的时候有些关键字会自动提示。 showImg(https://segmentfault.com/img/b...

    Chiclaim 评论0 收藏0
  • 手摸教你用canvas实现给图片添加平铺水印

    摘要:最近项目中遇到一个需求,需要把一张图片加上平铺的水印类似这样的效果首先想到的是用完成这种功能,因为我之前也没有接触过,所以做这个功能的时候,就是一步一步的摸索中学习,过程还是挺的,接下来跟我一步步来实现这个功能以及发现一些的坑吧。 最近项目中遇到一个需求,需要把一张图片加上平铺的水印 类似这样的效果showImg(https://segmentfault.com/img/remote/...

    崔晓明 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<