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

2,079 阅读7分钟

前言

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

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

启程

脚本是如何执行的?

大家在用脚手架创建项目时有没有这样的困惑,为啥敲个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

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

自动创建组件

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

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

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
}

大功告成

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

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

acr addcpn Header

image.png

image.png

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

acr addcpn Header -d src/views/home

执行前 image.png
执行后

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

END

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

npm install acr-cli -g

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

QQ图片20200210181218.jpg