脚手架项目和组件初始化开发
完成 weilai-cli
脚手架安装项目/组件功能开发
本期主要是 installTemplate/安装模板
部分的内容
模板安装架构设计
- 首先安装模板之前,我们需要做一些准备操作,例如:
判断目录是否为空、创建的项目类型、以及收集一些必要信息
; - 其次需要根据选择的项目类型选择下载相应的项目模板;
- 最后我们需要对下载的模板进行安装操作以及做一些特殊处理。
项目和组件初始化
初始化的时候,首先要判断 type
类型,是标准模板还是自定义模板。然后根据不同的类型执行不同的安装方法。示例代码如下:
async installTemplate() {
if(this.templateInfo) {
if(this.templateInfo.type === TEMPLATE_TYPE_NORMAL) {
// 标准安装
await this.installNormalTemplate()
} else if(this.templateInfo.type === TEMPLATE_TYPE_CUSTOM) {
// 自定义安装
await this.installCustomTemplate()
} else {
throw new Error('项目模板信息类型无法识别')
}
} else {
throw new Error('项目模板信息不存在')
}
}
标准安装
标准安装就是按照脚手架的拷贝和渲染流程处理的模板;
首先我们需要确保模板目录和目标目录是否是存在的,如果存在,就从模板目录把模板拷贝到目标目录,并且使用 ejs
进行模板渲染的处理;
最后会为模板进行依赖安装和执行启动命令。
示例代码如下:
async installNormalTemplate() {
log.verbose('安装标准模板')
log.verbose('templateNpm', this.templateNpm)
// 拷贝模板代码到当前目录
const spinner = spinnerStart('正在安装模板...')
const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
const targetPath = process.cwd()
await sleep()
try {
fsExtra.ensureDirSync(templatePath) // 确保目录存在
fsExtra.ensureDirSync(targetPath) // 确保目录存在
fsExtra.copySync(templatePath, targetPath) // 拷贝到 targetPath 目录下
} catch (e) {
throw e
} finally {
spinner.stop(true)
log.success('模板安装成功')
}
const templateIgnore = this.templateInfo.ignore || []
const ignore = ['**/node_modules/**', ...templateIgnore]
await this.ejsRender({ignore})
const { installCommand, startCommand } = this.templateInfo
let installCmdRet, startCmdRet
// 依赖安装
await this.execCommand(installCommand, '依赖安装失败')
// 启动命令执行
await this.execCommand(startCommand, '启动命令执行失败')
}
自定义安装
自定义安装就是模板自身提供拷贝和渲染流程处理的模板;
首先需要判断 Package
是否存在,如果存在,则获取 rootFilePath
也就是默认的执行文件,一般指向 package.json
里面的 main、bin
的配置;
其次确认该文件是否存在,存在则继续,不存在则终止;
最后通过 spawan
开启一个子进程去执行该文件,并且把收集到的信息组合成一个配置项传入,信息有 targetPath、sourcePath、templateInfo、projectInfo
;
示例代码如下:
async installCustomTemplate() {
log.verbose('安装自定义模板')
log.verbose('templateNpm', this.templateNpm)
if(await this.templateNpm.exists()) {
const rootFile = this.templateNpm.getRootFile()
log.verbose('rootFile', rootFile)
if(fs.existsSync(rootFile)) {
log.notice('开始执行自定义模板安装')
const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
const options = {
targetPath: process.cwd(),
sourcePath: templatePath,
templateInfo: this.templateInfo,
projectInfo: this.projectInfo
}
const code = `require('${rootFile}')(${JSON.stringify(options)})`
await spawnAsync('node', ['-e', code], { stdio: 'inherit', cwd: process.cwd()})
log.success('自定义模板安装成功')
} else {
throw new Error('自定义模板入口文件不存在!')
}
}
}
ejs 渲染模板
模板渲染,如果你经历过前后端没有分离的时代,那么你一定会有了解,例如:php, jsp
等;
简单来说,就是通过一些特殊的标签,对模板进行动态编辑;
ejs 标签如下所示:
<% '脚本' 标签,用于流程控制,无输出。
<%_ 删除其前面的空格符
<%= 输出数据到模板(输出是转义 HTML 标签)
<%- 输出非转义的数据到模板
<%# 注释标签,不执行、不输出内容
<%% 输出字符串 '<%'
%> 一般结束标签
-%> 删除紧随其后的换行符
_%> 将结束标签后面的空格符删除
脚手架模板渲染,首先通过 glob
获取目标目录下的所有文件,并且过滤掉一些不需要渲染的文件或文件夹,然后通过循环逐一对每个文件进行渲染,渲染后复写该文件。
示例代码如下:
async ejsRender(options) {
const cwd = process.cwd()
return new Promise((resolve1, reject1) => {
glob('**', {
cwd: cwd,
ignore: options.ignore || '',
nodir: true
}, (err, files) => {
if(err) {
reject1(err)
}
Promise.all(files.map(file => {
const filePath = path.join(cwd, file)
return new Promise((resolve2, reject2) => {
ejs.renderFile(filePath, this.projectInfo, {}, (err, result) => {
if(err) {
reject2(err)
}
fsExtra.writeFileSync(filePath, result)
resolve2(result)
})
})
})).then(() => {
resolve1()
}).catch(err => {
reject1(err)
})
})
})
}
命令执行
命令执行的原理依旧是使用 spawn
通过对命令字符串的拆分并且去验证它是否是合法的命令。
示例代码如下:
async execCommand(command, errMsg) {
if(command) {
const cmdOptions = command.split(' ')
const cmd = this.checkCommand(cmdOptions[0])
const args = cmdOptions.splice(1)
const ret = await spawnAsync(cmd, args, {
stdio: 'inherit',
cwd: process.cwd()
})
if(ret !== 0) {
throw new Error(errMsg)
}
return ret
}
throw new Error(`命令不存在`)
}
ejs 和 require 源码学习过程和感悟
吃不下了吃不下了,太顶了,我选择狗带,滑稽脸。