zl程序教程

您现在的位置是:首页 >  Java

当前栏目

【架构师(第十八篇)】脚手架之项目模板的安装

2023-02-18 15:37:10 时间

执行命令

utils\utils\lib\index.js 模块下新建两个方法

/**
 * @description: 封装一个 spawn 方法,兼容 mac 和 windows
 * windows : cp.spawn('cmd',['/c','node','-e',code],{})
 * mac : cp.spawn('node', ['-e', code],{})
 * @param {*} command 'cmd'
 * @param {*} args ['/c','node','-e',code]
 * @param {*} options {}
 * @return {*} cp.spawn(cmd, cmdArgs, options)
 */
function spawn(command, args, options = {}) {
  const cp = require('child_process');
  const win32 = process.platform === 'win32';
  const cmd = win32 ? 'cmd' : command;
  const cmdArgs = win32 ? ['/c'].concat(command, args) : args;
  return cp.spawn(cmd, cmdArgs, options);
}

/**
 * @description: 异步执行命令
 * @param {*} 参数同上
 * @return {*}
 */
function execAsync(command, args, options = {}) {
  return new Promise((resolve, reject) => {
    const p = spawn(command, args, options)
    p.on('error', e => {
      reject(e)
    })
    p.on('exit', c => {
      resolve(c)
    })
  })
}

module.exports = {
  spawn,
  execAsync
};

模板安装

commands\init\lib\index.js

在命令的执行阶段添加安装模板的方法。

/**
   * @description: 命令的执行阶段
   * @param {*}
   * @return {*}
   */
  async exec() {
    try {
      // 1.准备阶段
      const projectInfo = await this.prepare();
      if (projectInfo) {
        this.projectInfo = projectInfo;
        log.verbose('?? ~ 项目详情', projectInfo);
        // 2.下载模板
        await this.downloadTemplate()
        // 3.安装模板
        await this.installTemplate()
      }
    } catch (error) {
      log.error(error.message);
    }
  }

安装模板,根据模板类型安装标准模板或者自定义模板。

const TEMPLATE_TYPE_NORMAL = 'normal';
const TEMPLATE_TYPE_CUSTOM = 'custom';
/**
   * @description: 安装模板
   * @param {*}
   * @return {*}
   */
  async installTemplate() {
    // 安装标准模板
    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("无法解析的项目类型")
    }
  }

封装的执行命令的方法。

/**
   * @description: 执行命令
   * @param {*} cmd 命令
   * @param {*} message 执行后提示的消息
   * @return {*}
   */
  async execCommand(cmdInfo, message) {
    if (cmdInfo) {
      // npm install ==> [npm,install]
      const installCmd = cmdInfo.split(' ')
      // npm
      const cmd = checkCommand(installCmd[0])
      if (!cmd) {
        throw new Error("命令不存在")
      }
      // [install]
      const args = installCmd.slice(1)
      const installResult = await execAsync(cmd, args, {
        stdio: 'inherit',
        cwd: process.cwd()
      })
      if (installResult === 0) {
        log.warn(message)
      }
    }
  }

安装标准模板。

/**
   * @description: 安装标准模板
   * @param {*}
   * @return {*}
   */
  async installNormalTemplate() {
    const spinner = spinnerStart('模板安装中,请稍候...')
    await sleep()
    try {
      // 获取模板缓存路径
      // console.log('✅✅✅ ~ ', this.templateNpm);
      // 获取模板所在目录
      const templatePath = path.resolve(this.templateNpm.cacheFilePath, 'template')
      // 获取当前目录
      const targetPath = process.cwd()
      // 确保目录存在 不存在会创建
      fse.ensureDirSync(templatePath);
      fse.ensureDirSync(targetPath);
      // 拷贝模板到当前目录
      fse.copySync(templatePath, targetPath)
    } catch (error) {
      log.error(error.message)
    } finally {
      spinner.stop(true)
      log.warn('模板安装成功')
    }
    // ejs 模板渲染
    const ignore = ['node_modules/**', 'public/**']
    const ops = {
      ignore
    }
    await this.ejsRender(ops)
    const { installCommand, startCommand } = this.templateInfo
    // 依赖安装
    await this.execCommand(installCommand, '依赖安装成功')
    // 启动命令
    await this.execCommand(startCommand, '项目启动成功')
  }

安装自定义模板,暂时没有开发到这里。

  /**
   * @description: 安装自定义模板
   * @param {*}
   * @return {*}
   */
  async installCustomTemplate() {
    console.log('✅✅✅ ~ 安装自定义模板');
  }

检查命令,防止产生意外情况,命令必须是以下几种之一才会执行。

const WHITE_COMMAND = ['npm', 'cnpm', 'yarn']

/**
 * @description: 检查命令是否在白名单中
 * @param {*} cmd
 * @return {*}
 */
function checkCommand(cmd) {
  if (WHITE_COMMAND.includes(cmd)) {
    return cmd
  }
  return null
}

修改模板代码

使用 ejs 修改 hzw-cli-dev-template-vue3 这个模板 template 目录下的 package.json 文件

 "name": "<%= className %>",
 "version": "<%= version %>",

然后修改外层 package.json 的版本号 npm publish 发布新版本到 npm

PS: 因为发布到 npm 的版本号必须是正常的版本号,所以才需要嵌套一层,将外层作为 npm 模块,内层作为模板。

commands\init\lib\index.js

需要在 getInfo 这个方法中将 projectName 转换为链接线格式 project-name

if (info.project) {
  // kebab-case 这个库如果是大写字母开头会多出一个 - , 所以使用 replace 把第一个 - 给去掉
  info.project = require('kebab-case')(info.project).replace(/^-/, '')
}

使用 ejs 进行模板渲染

安装 ejsglob

lerna add ejs commands/init
lerna add glob commands/init

commands\init\lib\index.js

 /**
   * @description: ejs 模板渲染
   * @param {*}
   * @return {*}
   */
  ejsRender(options) {
    return new Promise((resolve, reject) => {
      // 遍历文件列表
      glob("**", {
        cwd: process.cwd(),
        nodir: true,
        ignore: options.ignore || []
      }, (err, files) => {
        if (err) {
          reject(err)
        }
        // 对文件列表使用 ejs 进行渲染
        Promise.all(files.map((file) => {
          const filePath = path.join(process.cwd(), file)
          return new Promise((resolve1, reject1) => {
            ejs.renderFile(filePath, this.projectInfo, {}, (err, res) => {
              if (err) {
                reject1(err)
              }
              // 将源文件替换成 ejs 渲染后的文件
              fse.writeFileSync(filePath, res)
              resolve1(res)
            })
          })
        }))
          .then(() => resolve(files))
          .catch((err) => reject(err))
      })
    })
  }

init 命令直接传入项目名称功能支持

/**
   * @description: 选择创建项目或者组件 获取项目的基本信息 return Object
   * @param {*}
   * @return {*} 项目的基本信息
   */
  async getInfo() {
    let info = {};
    // 选择创建项目或者组件;
    const {
      type
    } = await inquirer.prompt({
      type: 'list',
      message: '请选择初始化类型',
      name: 'type',
      default: TYPE_PROJECT,
      choices: [{
        name: '项目',
        value: TYPE_PROJECT,
      },
      {
        name: '组件',
        value: TYPE_COMPONENT,
      },
      ],
    });
    log.verbose('type', type);
    // 获取项目的基本信息;
    if (type === TYPE_COMPONENT) { }
    const isValidateName = (a) => {
      const reg =
        /^[a-zA-Z]+([-][a-zA-Z0-9]|[_][a-zA-Z0-9]|[a-zA-Z0-9])*$/;
      return reg.test(a)
    }
    // 是否在执行init 命令的时候就传入了正确的项目名称
    const isTrueName = isValidateName(this.projectName)
    console.log('?? ~ InitCommand ~ this.projectName', this.projectName);
    console.log('?? ~ InitCommand ~ isTrueName', isTrueName);

    const promptArr = [{
      type: 'input',
      message: '请输入项目版本号',
      name: 'version',
      default: '1.0.0',
      validate: (a) => {
        return !!semver.valid(a) || '请输入合法的版本号';
      },
      filter: (a) => {
        if (!!semver.valid(a)) {
          return semver.valid(a);
        }
        return a;
      },
    },
    {
      type: 'list',
      message: '请选择项目模板',
      name: 'template',
      default: 'vue3',
      choices: this.createTemplateChoices()
    },]
    if (type === TYPE_PROJECT) {
      const projectPrompt = {
        type: 'input',
        message: '请输入项目名称',
        name: 'project',
        validate: (a) => {
          if (isValidateName(a)) {
            return true;
          }
          return '要求英文字母开头,数字或字母结尾,字符只允许使用 - 以及 _ ';
        },
      }
      if (isTrueName) {
        info.project = this.projectName
      } else {
        promptArr.unshift(projectPrompt)
      }
      const answers = await inquirer.prompt(promptArr);
      info = {
        ...info,
        ...answers,
        type,
      };
    }

    // 将项目名称改成连接线形式
    if (info.project) {
      // kebab-case 这个库如果是大写字母开头会多出一个 - , 所以使用 replace 把第一个 - 给去掉
      info.className = require('kebab-case')(info.project).replace(/^-/, '')
    }
    return info;
  }