关于为了少搬砖,而用node手写了一个React脚手架这件事

前言

hellow,大家好。最近刚写完一个react项目,又想写一个练练手,可是我突然发现一个问题。
那就是又要从零构建一个完整的项目环境,避免不了要重复搬砖搬砖还是搬砖,确实挺麻烦,可能得花费20多分钟。
啥?我项目都还没写就花了我半小时?气愤之下,我立刻想了个好主意,我能不能用node写个脚手架呢?就像vue react它们的脚手架一样,直接运行vue create xxx 就直接把它那套模板搬过来了,可是我的明显是基于他们之上再结合自己日常项目需求所需的额外的包来搭建我们的项目。
现在很多公司都有自己的脚手架,无非是根据自己公司的项目来定制的一套模板而已,并且在此基础之上添加了一些脚本命令,再生成特定的文件夹和文件一系列操作
就比如创建一个store,我们可能需要下面这样的结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UpKTw0sB-1661262380513)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c49592f90ee54ad99fe83416019a7821~tplv-k3u1fbpfcp-watermark.image?)]

可是我们很多地方都需要,难不成自己手动建四个文件?可以是可以,但作为合格的搬砖人,是时候运用工具来帮助我们完成了!

启程

脚本是如何执行的?

大家在用脚手架创建项目时有没有这样的困惑,为啥敲个vue create xxx 它就知道帮我创建项目,它是如何运行的呢?我们要怎么实现呢?让我们带着问题一步一步实现吧。
乍看觉得很复杂,其实当我们深入了解的时候就会发现,其实也没有这么难。

初始化

新建一个acr-cli 文件夹 寓意:a auto react cli 自动构建react项目
执行脚本

npm init -y

初始化生成 package.json 文件
新建index.js 入口文件
先下载 commander 模块 辅助我们执行终端命令
先实现一个最简单的命令 acr -V 或者 acr --version 查看我们脚手架版本

index.js

开头这个注释很重要!!这是标记运行脚本,不能省略

#!/usr/bin/env node
const program = require('commander')

// 查看版本号
program.version(require('./package.json').version)

// 解析终端指令
program.parse(process.argv);

package.json

注意添加 bin对象 这段指令!
指定脚本执行的入口文件即后续操作

{
  "name": "acr-cli",
  "version": "1.0.0",
  "description": "a auto create react cli",
  "main": "index.js",
  "bin": {
    "acr": "index.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "react",
    "vue",
    "acr"
  ],
  "author": "kzj",
  "license": "MIT",
  "homepage": "https://github.com/kzj0916",
  "repository": {
    "type": "git",
    "url": "https://github.com/kzj0916"
  },
  "dependencies": {
    "commander": "^6.1.0",
    "download-git-repo": "^3.0.2",
    "ejs": "^3.1.5"
  }
}

修改完成后,执行脚本

npm link

使我们的配置生效
此时运行 acr -V 或者 acr --version 就可以查看我们脚手架版本

实现 --help指令

index.js

#!/usr/bin/env node
const program = require('commander')

const helpOptions = require('./lib/core/help')

// 查看版本号
program.version(require('./package.json').version)

// 配置help指令
helpOptions()


// 解析终端指令
program.parse(process.argv);

lib/core/help.js

注意,这里配置的 -d --dest <dest> 后面有用到哦

// 配置--help指令执行后的输出

const program = require('commander')

const helpOptions = () => {
    // 增加自己的options
    program.option('-a --acr', 'a auto create React cli');
    program.option('-d --dest <dest>', '配置目标路径,例如: -d /src/components')
    program.option('-f --framework <framework>', 'your frameword')
    //  配置其他信息
    program.on('--help', function () {
        console.log("");
        console.log("其它配置:")
        console.log("  other options~");
    })
}

module.exports = helpOptions
   

现在我们 acr --help 指令也完成了,下一步主要是实现自动创建项目

实现自动创建项目

自动创建项目分三步

  1. 下载指定的react模板 --> git clone …
  2. 安装依赖 —> yarn install
  3. 运行项目 —> yarn start

index.js

#!/usr/bin/env node
const program = require('commander')

const helpOptions = require('./lib/core/help')
const createCommands = require('./lib/core/create')

// 查看版本号
program.version(require('./package.json').version)

// 配置help指令
helpOptions()

// 创建其他指令
createCommands()

// 解析终端指令
program.parse(process.argv); 

lib/core/create.js

监听终端执行的脚本,做出相应行动
由于action执行函数体较复杂,为了代码的可读性进行了抽离,在actions封装

const program = require('commander');

const {
    createAction,
} = require('./action')

const createCommands = () => {
    program
        .command('create <project> [others...]')
        .description('自动创建项目')
        .action(createAction)
}

module.exports = createCommands

action.js

根据我们上面的三步规划来运行的
commandSpawn 就是对终端执行做了一些额外的配置,看下面的文件就知道了
需要下载 download-git-repo 帮助我们远程下载git上的代码
我个人的react模板地址reactTmp
大家可以参考下

 const { promisify } = require('util');
const download = promisify(require('download-git-repo'));
const path = require('path');

// 模板地址
const { reacttmp } = require('../config/temp-git-path')
// 执行cmd指令
const { commandSpawn } = require('../utils/terminal')

// callback -> promisify(函数) -> Promise -> async await
// project 所创建的文件名
// 创建项目
const createAction = async (project) => {
    console.log("正在构建项目中~")
    // 1.远程克隆react的模板
    await download(reacttmp, project, { clone: true })
    // 2.初始化依赖下载 判断电脑配置环境 yarn install
    const command = process.platform === 'win32' ? 'yarn.cmd' : 'yarn';
    await commandSpawn(command, ['install'], { cwd: `./${project}` })
    //3.运行项目 执行yarn start
    commandSpawn(command, ['start'], { cwd: `./${project}` });
}
    module.exports = {
    createAction
}

utils/terminal.js

    
const { spawn } = require('child_process');

const commandSpawn = (...args) => {
    return new Promise((resolve, reject) => {
        const childProcess = spawn(...args);
        // 下载执行时的进程打印 成功下载或失败下载
        childProcess.stdout.pipe(process.stdout);
        childProcess.stderr.pipe(process.stderr);
        // 下载执行完毕
        childProcess.on("close", () => {
            resolve();
        })
    })
}

module.exports = {
    commandSpawn
}

运行

acr create demothree

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-44DnXP60-1661262380517)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eca433fb0fd94824a7b48e7c5e88bb20~tplv-k3u1fbpfcp-watermark.image?)]

成功了!竟然什么都帮我们自动构建好了 redux router services 等

自动创建组件

每次创建组件时,既要创建文件夹又要创建俩文件,如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rx8TiIRL-1661262380519)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bd56834c2c6945f5a6fffa6ddd3988c6~tplv-k3u1fbpfcp-watermark.image?)]

干脆写个脚本自动生成好了!说干就干

create.js

这里以自动创建组件为例,下面的基本上是一样的思想就不过多阐述了,具体看看源码

const program = require('commander');

const {
    createAction,
    addComponentAction,
    addPageAndRouteAction,
    addStoreAction,
    addServiceAction
} = require('./action')

const createCommands = () => {
    program
        .command('create <project> [others...]')
        .description('自动创建项目')
        .action(createAction)

    program
        .command('addcpn <name>')
        .description('自动创建组件')
        .action((name) => {
            addComponentAction(name, program.dest || 'src/components');
        })

    program
        .command('addpage <page>')
        .description('自动创建页面')
        .action((page) => {
            addPageAndRouteAction(page, program.dest || 'src/views');
        })

    program
        .command('addstore <store>')
        .description('自动创建store')
        .action((store) => {
            addStoreAction(store, program.dest || 'src/store');
        })

    program
        .command('addserver <serve>')
        .description('自动创建service')
        .action((serve) => {
            addServiceAction(serve, program.dest || 'src/services');
        })
}

module.exports = createCommands

action.js

compile 根据ejs编译生成模板 需要下载ejs
createDir 判断路径是否存在并创建文件夹 错误则返回false
writeToFile 写入内容至对应文件

const { promisify } = require('util');
const download = promisify(require('download-git-repo'));
const path = require('path');

// 模板地址
const { reacttmp } = require('../config/temp-git-path')
// 执行cmd指令
const { commandSpawn } = require('../utils/terminal')
// 编译模板
const { compile, writeToFile, createDir } = require('../utils/utils')

// callback -> promisify(函数) -> Promise -> async await
// project 所创建的文件名
// 创建项目
const createAction = async (project) => {
    console.log("正在构建项目中~")
    // 远程克隆react的模板
    await download(reacttmp, project, { clone: true })
    // 初始化依赖下载 判断电脑配置环境
    const command = process.platform === 'win32' ? 'yarn.cmd' : 'yarn';
    await commandSpawn(command, ['install'], { cwd: `./${project}` })
    //运行项目 执行npm run start
    commandSpawn(command, ['start'], { cwd: `./${project}` });
}

// 创建组件
const addComponentAction = async (name, dest) => {
    // 获取编译成功后的模板内容
    const component = await compile("react-component.ejs", { name, wrapperName: name + 'Wrapper' })
    const style = await compile("react-style.ejs", { wrapperName: name + 'Wrapper' })
    // 写入文件的操作
    const targetDest = path.resolve(dest, name.toLowerCase())
    if (createDir(targetDest)) {
        const componentPath = path.resolve(targetDest, `index.tsx`);
        const stylePath = path.resolve(targetDest, `style.ts`);
        writeToFile(componentPath, component);
        writeToFile(stylePath, style);
    }
}


module.exports = {
    createAction,
    addComponentAction,
}

util.js

const path = require('path');
const fs = require('fs');
const ejs = require('ejs');

// 编译并生成对应模板 templateName模板名 data额外参数
const compile = (templateName, data) => {
    const templatePosition = `../templates/${templateName}`;
    // 获取模板完整路径
    const templatePath = path.resolve(__dirname, templatePosition);

    return new Promise((resolve, reject) => {
        ejs.renderFile(templatePath, { data }, {}, (err, result) => {
            if (err) {
                console.log(err);
                reject(err);
                return;
            }

            resolve(result);
        })
    })
}

const writeToFile = (path, content) => {
    // 判断path是否存在, 如果不存在, 创建对应的文件夹
    return fs.promises.writeFile(path, content);
}


// eg src/components/navbar/header
// 递归生成文件夹
const createDir = (dirPath) => {
    if (!fs.existsSync(dirPath)) {
        if (createDir(path.dirname(dirPath))) {
            fs.mkdirSync(dirPath);
            return true
        }
    } else {
        return true
    }

}

module.exports = {
    compile,
    writeToFile,
    createDir
}

大功告成

写完了,让我们来看看效果吧
默认组件文件夹是这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMF5NRBd-1661262380520)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2ee076e70fc046ba85868d483d5c2ad1~tplv-k3u1fbpfcp-watermark.image?)]

我现在想生成一个header组件 创建组件名需大写!

acr addcpn Header

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IkQ9MY3T-1661262380524)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc8e522c431a41518a9ec33477a68813~tplv-k3u1fbpfcp-watermark.image?)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQEpuNnU-1661262380527)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e7f58d5881664c5d99255651b80bf1cf~tplv-k3u1fbpfcp-watermark.image?)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RHtIRAjb-1661262380533)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7476b38f581c46ec9eb7bbdd27621491~tplv-k3u1fbpfcp-watermark.image?)]

达到我们理想的效果,如果我们想在其他文件夹生成,可以这样写

acr addcpn Header -d src/views/home

执行前

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JmJilESC-1661262380534)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9edda3425325479692822f5de96a960b~tplv-k3u1fbpfcp-watermark.image?)]

执行后

内容也会根据我们的模板生成哦!

END

想要更好体验来接更多功能可以全局安装 acr-cli 我已经发布到npm上了
只需要你执行

npm install acr-cli -g

你就可以任意在你react项目中执行我的脚本啦
arc-cli 源码地址:源码地址在此求颗小星星
如果此文对你有帮助欢迎大家点赞收藏加关注!如有不对之处,望各位大佬不吝赐教。
三连加关注,更新不迷路!

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340