脚手架项目和组件初始化开发

完成 weilai-cli 脚手架安装项目/组件功能开发

本期主要是 installTemplate/安装模板 部分的内容

模板安装架构设计

  1. 首先安装模板之前,我们需要做一些准备操作,例如:判断目录是否为空、创建的项目类型、以及收集一些必要信息
  2. 其次需要根据选择的项目类型选择下载相应的项目模板;
  3. 最后我们需要对下载的模板进行安装操作以及做一些特殊处理。

项目和组件初始化

初始化的时候,首先要判断 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 源码学习过程和感悟

吃不下了吃不下了,太顶了,我选择狗带,滑稽脸。

Copyright © imooc-lego (2020 - present) all right reserved,powered by GitbookFile Modify: 2021-06-27 08:04:57

results matching ""

    No results matching ""