实现思路
按我的理解,脚手架实际就是种对模板文件进行读写的操作,所以大致的步骤有三个
- 通过命令行接收传递的参数
- 通过fs读取模板目录下的文件内容
- 将读取的文件内容写入到项目目录下
开始准备
首先需要初始化一个脚手架项目初始结构
yarn init --yes
然后我们需要创建一个cli.js作为入口执行文件,并且需要定义头部#!/usr/bin/env node
,并在package.json中定义bin字段的值为该入口文件
命令行发起询问
如何通过命令行接收用户输入的参数,这里我们可以使用inquirer模块发起命令行询问
const inquirer = require('inquirer')
// prompt 方法接收一个对象数组参数,每一个对象都可以发起一次询问
inquirer.prompt([
{
type: 'input', // 类型
name: 'name', // 接收变量的参数名
message: 'project name', // 命令行提示
default: 'default key' // 默认值
}
])
读取模板目录的文件内容
prompt方法返回一个promise,在then方法的回调函数中可以接收用户输入的参数
inquirer.prompt([...])
.then((config) => {
// 读取和写入逻辑在这里编写
})
读取luoji
想要读取模板文件内容,我们需要知道模板目录的文件路径,这里我们需要使用到path和fs模块
const tmplDir = path.join(__dirname, 'templatePath')
// 然后通过fs.readdir读取目录下的所有文件
fs.readdir(tmplDir, (err, file) => {
// err是错误信息
// file是读取的文件的子路径
// 读取文件内容
let result = fs.readFileSync(path.join(tmplDir, file))
// 写操作,请看下一节内容
})
将文件写入项目目录下
读取模板目录的文件内容后,我们需要通过写操作将内容写入到项目中,同样的,我们需要知道项目目录的文件路径
const destDir = process.cwd() // cwd 可以获取工作路径
// 然后通过fs.writeFileSync将文件写入项目中
fs.readdir(tmplDir, (err, file) => {
// err是错误信息
// file是读取的文件的子路径
// 读取文件内容
let result = fs.readFileSync(path.join(tmplDir, file))
// 写操作
fs.writeFileSync(path.join(destDir, file))
})
当所有文件写入完成,那么脚手架的工作就完成了
问题与优化
遇到的问题一
在实现上述逻辑的时候,我发现,对于简单的模板结构,的确是没问题的。但是遇到文件目录嵌套的情况,上面的读写操作则无法完成,这是由于readdir只能读取到路径下的文件的目录,但无法读取对应路径下目录内的文件
解决思路
所以这里我首先想到的是通过递归读取来实现获取模板目录下所有文件
遇到的问题二
解决了获取模板内所有文件的路径后,我又发现,在进行写入文件到项目的过程的时候,对于未生成的文件夹进行直接的文件写入是会报错的。
解决思路
在进行写的操作的时候,我们需要对未生成的文件夹进行创建,然后再进行写的操作
优化
为了更好的处理两个问题,我将两个问题的解决方案进行结合,大体思路是,判断当前文件是否是文件类型,是则进行写的操作。否则,在项目中新建对应文件夹,并对目录进行循环,进行递归操作
function readAndWriteFiles(src, dest, pathName, config) {
let stat = fs.statSync(path.join(src, pathName))
if (stat.isDirectory()) {
// 是文件夹,在输出目录下创建文件夹,并进行递归读写
// 在项目下创建对应文件夹
if (pathName) {
// console.log(`mkdir: ${pathName}`)
fs.mkdirSync(path.join(dest, pathName))
}
let list = fs.readdirSync(path.join(src, pathName))
list.forEach(fileName => {
readAndWriteFiles(src, dest, path.join(pathName, fileName), config)
})
return
}
// 普通文件,直接读写
// console.log(`write: ${pathName}`)
let result = fs.readFileSync(path.join(src, pathName), 'utf-8')
fs.writeFileSync(path.join(dest, pathName), result)
}
发布和使用
最后可以将编写的脚手架发布到npm或者link到本地,这里就简单介绍一下link到本地的操作
link
// 在脚手架根目录下运行
yarn link
使用
// 在项目根路径下运行,脚手架名称
<cli name>
源代码
#!/usr/bin/env node
// 需要定义 #!/usr/bin/env node 头
// 这是执行入口文件
const fs = require('fs')
const path = require('path')
const deConfig = {
name: process.cwd().split('\\').reverse()[0]
}
const inquirer = require('inquirer')
const ejs = require('ejs')
// 脚手架工作流程:
// 发起命令行询问并获取对于的配置参数
// 获取模板文件信息
// 通过模板引擎渲染文件并写入项目目录中
function writeRenderFile(src, dest, config) {
ejs.renderFile(src, config, (err, result) => {
if (err) throw err
// 写入项目目录下
fs.writeFileSync(dest, result)
})
}
/**
*
* @param {string} src 源文件根路径
* @param {string} dest 项目根路径
* @param {string} pathName 文件路径
* @param {object} config 配置
*/
function readAndWriteFiles(src, dest, pathName, config) {
let stat = fs.statSync(path.join(src, pathName))
if (stat.isDirectory()) {
// 是文件夹,在输出目录下创建文件夹,并进行递归读写
// 在项目下创建对应文件夹
if (pathName) {
// console.log(`mkdir: ${pathName}`)
fs.mkdirSync(path.join(dest, pathName))
}
let list = fs.readdirSync(path.join(src, pathName))
list.forEach(fileName => {
readAndWriteFiles(src, dest, path.join(pathName, fileName), config)
})
return
}
// 普通文件,直接读写
// console.log(`write: ${pathName}`)
let result = fs.readFileSync(path.join(src, pathName), 'utf-8')
fs.writeFileSync(path.join(dest, pathName), result)
}
// inquirer 一个发起命令行询问的工具
inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'project name',
default: deConfig.name
}
]).then((config) => {
const tmplDir = path.join(__dirname, 'templates')
const destDir = process.cwd()
readAndWriteFiles(tmplDir, destDir, '', config)
})
本内容仅供学习总结参考