前言
hellow,大家好。最近刚写完一个react项目,又想写一个练练手,可是我突然发现一个问题。那就是又要从零构建一个完整的项目环境,避免不了要重复搬砖搬砖还是搬砖,确实挺麻烦,可能得花费20多分钟。啥?我项目都还没写就花了我半小时?气愤之下,我立刻想了个好主意,我能不能用node写个脚手架呢?就像vue react它们的脚手架一样,直接运行vue create xxx 就直接把它那套模板搬过来了,可是我的明显是基于他们之上再结合自己日常项目需求所需的额外的包来搭建我们的项目。现在很多公司都有自己的脚手架,无非是根据自己公司的项目来定制的一套模板而已,并且在此基础之上添加了一些脚本命令,再生成特定的文件夹和文件一系列操作。就比如创建一个store,我们可能需要下面这样的结构
启程
脚本是如何执行的?
大家在用脚手架创建项目时有没有这样的困惑,为啥敲个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 后面有用到哦
// 配置--help指令执行后的输出
const program = require('commander')
const helpOptions = () => {// 增加自己的optionsprogram.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 installconst command = process.platform === 'win32' ? 'yarn.cmd' : 'yarn';await commandSpawn(command, ['install'], { cwd: `./${project}` })//3.运行项目 执行yarn startcommandSpawn(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
自动创建组件
每次创建组件时,既要创建文件夹又要创建俩文件,如下
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编译生成模板 需要下载ejscreateDir 判断路径是否存在并创建文件夹 错误则返回falsewriteToFile 写入内容至对应文件
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 startcommandSpawn(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
}
大功告成
写完了,让我们来看看效果吧默认组件文件夹是这样的
acr addcpn Header
acr addcpn Header -d src/views/home
执行前
|