关于前端工程化的一些理解,以及配置gulp自动化和自定义一个前端脚手架

关于前端工程化

前端工程化是什么

工程化是使用软件工程的技术和方法来进行前端的开发流程、技术、工具、经验等规范化、标准化,其以提高效率、降低成本、质量保证为目的。工程化不等于某个具体的工具,工程化是指项目整体的规划和架构,工具只是落地实施的手段。

工程化主要解决的问题

  • 传统语言或语法的弊端,如需要使用ES6+的新特性,但是存在兼容问题,亦或是使用Less/Sass/PostCSS时,运行环境不能支持支持
  • 无法使用模块化/组件化,来提高项目的可维护性,但运行环境不能直接支持
  • 重复的机械式工作,部署上线前需要手动压缩代码及资源文件、部署过程需要手动上传代码到服务器
  • 代码风格统一、质量保证,多人协作开发时无法硬性统一大家的代码风格,从仓库中pull回来的代码质量无法保证
  • 依赖后端服务接口支持,需要等待后端服务接口提前完成
  • 整体依赖后端项目,如以前的jsp开发,MVC在java端,前端只输出页面模版,需依赖java端的稳定,才能输出前端页面

工程化的一般流程

  • 创建项目:使用脚手架工具CLI自动搭建,创建项目结构、创建特定类型文件
  • 编码:格式化代码、校验代码风格、编译/构建/打包
  • 预览/测试:Web Server热更新、Mock模拟接口、Live Reloading、HMR、Source Map定位源代码
  • 提交:Git Hooks提交前做源码检查、Lint-Staged、持续集成
  • 部署:CI/CD、自动发布

一些常用的工程化手段

利用gulp实现自动化构建
1.必备模块
  • gulp:gulp主文件
  • gulp-babel|@babel/core|@babel/preset-env:编译es6+文件
  • browser-sync:热更新依赖
  • del:删除文件夹
  • gulp-clean-css|gulp-htmlmin|gulp-uglify:压缩打包后的css|html|js文件
  • gulp-if:用来判断文件类型,调用对应的gulp插件来压缩文件
  • gulp-imagemin:拷贝媒体文件
  • gulp-sass:编译sass文件
  • gulp-swig:编译swig模板
  • gulp-useref:合并css|js文件
  • gulp-load-plugins:不用依次引入gulp插件,直接使用plugins.xxx使用gulp插件
2.安装并引入项目依赖
# 安装依赖
$ yarn install

gulpfile.js

// 引入gulp读写流
const { src, dest, parallel, series, watch } = require('gulp')
// 引入自动加载gulp插件依赖
const loadPlugins = require('gulp-load-plugins')
const plugins = loadPlugins()
3.编写开发阶段逻辑
3.1 页面热更新

gulpfile.js

const browserSync = require('browser-sync')
const bs = browserSync.create()

// 实时监听sass、js、html变化,更新页面
const serve = () => {
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)

  // 媒体文件只在更新后重载,开发阶段不进行实时监控编译,以免浪费性能
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**',
  ], bs.reload)

  bs.init({
    notify: false,
    files: 'temp/**',
    server: {
      baseDir: ['temp', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules',
      },
    },
  })
}
3.2 css、js、html打包

将css、js、html文件打包到临时目录temp,以便useref压缩拷贝到dist

此处为防止useref在压缩合并代码时,在同一个目录下同时进行读写产生错误,先将打包的html、js、css代码暂存在temp目录下,然后通过useref拷贝到dist下

gulpfile.js

// sass编译打包
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('temp'))
}

// js编译打包
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('temp'))
}

// html编译打包,设置swig缓存为false,防止页面不刷新
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } }))
    .pipe(dest('temp'))
}
3.3 组合任务实现开发阶段热更新

gulpfile.js

// 编译css、js、html编译可以同步进行
const compile = parallel(style, script, page)
// dev开发启动需要在编译完成后
const dev = series(compile, serve)

module.exports = {
  dev,
}
4.编写打包构建
4.1 编写清除插件

清除插件的作用在于删除之前的dist和temp文件
gulpfile.js

const del = require('del')

const clean = () => {
  return del(['dist', 'temp'])
}
4.2 编译打包媒体文件

gulpfile.js

// 编译图片文件
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 编译字体文件
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 编译public下的文件
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}
4.3 压缩打包后的css、js、html文件,并拷贝到dist目录

gulpfile.js

const useref = () => {
  return src('temp/**.html')
    .pipe(plugins.useref({
      searchPath: ['dist', '.']
    }))
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({ 
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true,
    })))
    .pipe(dest('dist'))
}
4.4 组合实现build上线任务
  • build之前需要先将之前的dist、temp文件先清除掉,打包前先执行清除,为了方便开发中清除旧包,此处将clean也作为单独任务导出
  • 清除任务完成之后同步执行编译各种文件
  • 在编译css、js、html时,先完成编译任务,然后执行压缩任务
    gulpfile.js
const build = series(
  clean,
  parallel(
    series(compile, useref),
    image,
    font,
    extra,
  ))
module.exports = {
  build,
  clean,
}
5.使用
yarn gulp clean
yarn gulp build
yarn gulp dev

or

将三种任务在package.json中配置成script

package.json

"scripts": {
  "clean": "gulp clean",
  "dev": "gulp dev",
  "build": "gulp build",
}
yarn clean
yarn build
yarn dev

完整gulp配置代码

const { src, dest, parallel, series, watch } = require('gulp')

const del = require('del')
const browserSync = require('browser-sync')
const loadPlugins = require('gulp-load-plugins')

const plugins = loadPlugins()
const bs = browserSync.create()

const data = {
  menus: [
    {
      name: 'Home',
      icon: 'aperture',
      link: 'index.html'
    },
    {
      name: 'Features',
      link: 'features.html'
    },
    {
      name: 'About',
      link: 'about.html'
    },
    {
      name: 'Contact',
      link: '#',
      children: [
        {
          name: 'Twitter',
          link: 'https://twitter.com/'
        },
        {
          name: 'About',
          link: 'https://weibo.com/'
        },
      ]
    }
  ],
  pkg: require('./package.json'),
  date: new Date()
}

const clean = () => {
  return del(['dist', 'temp'])
}

// sass编译打包
const style = () => {
  return src('src/assets/styles/*.scss', { base: 'src' })
    .pipe(plugins.sass({ outputStyle: 'expanded' }))
    .pipe(dest('temp'))
}

// js编译打包
const script = () => {
  return src('src/assets/scripts/*.js', { base: 'src' })
    .pipe(plugins.babel({ presets: ['@babel/preset-env'] }))
    .pipe(dest('temp'))
}

// html编译打包
const page = () => {
  return src('src/*.html', { base: 'src' })
    .pipe(plugins.swig({ data, defaults: { cache: false } }))
    .pipe(dest('temp'))
}

// 编译图片文件
const image = () => {
  return src('src/assets/images/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 编译字体文件
const font = () => {
  return src('src/assets/fonts/**', { base: 'src' })
    .pipe(plugins.imagemin())
    .pipe(dest('dist'))
}

// 编译public下的文件
const extra = () => {
  return src('public/**', { base: 'public' })
    .pipe(dest('dist'))
}

const serve = () => {
  watch('src/assets/styles/*.scss', style)
  watch('src/assets/scripts/*.js', script)
  watch('src/*.html', page)

  // 文件更新后,重载
  watch([
    'src/assets/images/**',
    'src/assets/fonts/**',
    'public/**',
  ], bs.reload)

  bs.init({
    notify: false,
    files: 'temp/**', // 监听该文件夹下的文件变化,自动更新页面
    server: {
      baseDir: ['temp', 'src', 'public'],
      routes: {
        '/node_modules': 'node_modules',
      },
    },
  })
}

const useref = () => {
  return src('temp/**.html')
    .pipe(plugins.useref({
      searchPath: ['dist', '.']
    }))
    .pipe(plugins.if(/\.js$/, plugins.uglify()))
    .pipe(plugins.if(/\.css$/, plugins.cleanCss()))
    .pipe(plugins.if(/\.html$/, plugins.htmlmin({ 
      collapseWhitespace: true,
      minifyCSS: true,
      minifyJS: true,
    })))
    .pipe(dest('dist'))
}

const compile = parallel(style, script, page)
const build = series(
  clean,
  parallel(
    series(compile, useref),
    image,
    font,
    extra,
  ))
const dev = series(compile, serve)

module.exports = {
  build,
  dev,
  clean,
}
脚手架工具

前端工程化的发起者,创建项目基础结构、提供项目规范和约定,避免重复工作,其主要意义在于

  • 避免重复工作,不必从零开始搭建初始项目,提高开发效率
  • 自定义项目参数,根据项目实际需要来生成对应的初始项目接口与依赖
  • 便于多人协作,统一代码风格规范
  • 迭代更新方便,只需要迭代更新代码库中项目模板,即可下载最新的项目

常用的脚手架工具

  • 服务于特定框架的脚手架:create-react-app、vue-cli(此处不做讨论)
  • 通用型脚手架:Yeoman
  • 项目开发过程中创定特定类型的文件:Plop

动手开发一个脚手架

1.必备模块
  • commander:参数解析,自动化–help
  • inquirer:交互式命令行工具,实现命令行的选择功能
  • download-git-repo:在git库中下载模板
  • ora:提供远程下载时的loading状态展示
  • axios:下载模板
  • ejs:渲染模板到目标目录
2.工程创建
2.1 创建项目
# 初始化项目
$ yarn init
# 安装各种依赖包
$ yarn install
2.2 项目结构
├── README.md // 项目自述文档
├── bin
│   └── cli.js // 命令行文件
├── lib
│   ├── create.js // 创建模板
│   ├── main.js // 脚手架入口文件
│   └── utils
│       ├── constants.js // 公共常量
│       └── utils.js // 工具文件
└── package.json
2.3 链接全局包

在package.json中设置命令路径

"bin": {
  "yhzzy-cli": "./bin/cli.js"
}

cli.js添加node环境执行命令,并使用main.js作为入口

#!/usr/bin/env node
require('../lib/main')

将yhzzy-cli命令链接到全局,之后在main.js中实现脚手架逻辑

yarn link
3.设置commander
3.1 设置版本号信息

utils/constants.js

const { name, version } = require('../../package.json')

module.exports = {
  name,
  version,
}

main.js

const { program } = require('commander')
const { version } = require('./utils/constants')

program.version(version)
  .parse(process.argv)
3.2 配置指令命令

main.js

const actions = {
  create: {
    description: 'create a project',
    alias: 'cr',
    examples: [
      'yh-cli create <template-name>'
    ],
  },
}

Object.keys(actions).forEach(action => {
  program
    .command(action)
    .description(actions[action].description)
    .alias(actions[action].alias)
    .action(() => {
      require(path.resolve(__dirname, action))(...process.argv.slice(3))
    })
})
3.3 配置help命令

main.js

program.on('--help', () => {
  console.log('Examples')
  Object.keys(actions).forEach(action => {
    (actions[action].examples || []).forEach(ex => {
      console.log(`${ex}`)
    })
  })
})
4.编写create命令

create命令的作用:根据用户选择的模板选项,从git仓库中下载对应的模板到本地目录,并渲染生成到执行命令的目标目录

4.1 设置问题选项

utils/constants.js文件中添加以下代码,commonQuestions为不同模板共同的问题

const commonQuestions = [
  {
    type: 'input',
    name: 'name',
    message: 'Project name?',
    default: process.cwd().split('/').pop(),
  },
  {
    type: 'input',
    name: 'version',
    message: 'Project version?',
    default: '0.1.0',
  },
  {
    type: 'input',
    name: 'author',
    message: 'Who is the author?',
  },
  {
    type: 'input',
    name: 'description',
    message: 'Project description?',
    default: 'this is a template',
  },
  {
    type: 'confirm',
    name: 'private',
    message: 'Is private?',
    default: false,
  },
  {
    type: 'list',
    name: 'license',
    message: 'please choice a license',
    choices: ['MIT', 'ISC', 'Apache'],
  },
]
const questions = {
  'template-vue-cli': [
    ...commonQuestions,
    {
      type: 'input',
      name: 'displayName',
      message: 'Display for webpack title name?',
      default: process.cwd().split('/').pop(),
    },
  ],
  'template-nm-cli': [
    ...commonQuestions,
  ],
}

module.exports = {
  questions,
}
4.2 配置create命令

main.js

const actions = {
  create: {
    description: 'create a project',
    alias: 'cr',
    examples: [
      'yhzzy-cli create || yhzzy-cli cr'
    ],
  },
}
4.3 配置下载仓库地址信息

create.js

const path = require('path')
const axios = require('axios')

// 模板列表
let repos = []

// 模板下载地址
const templatePath = path.resolve(__dirname, '../templates')

// 获取仓库地址
const fetchRepoList = async () => {
  const { data } = await axios.get('https://api.github.com/users/yhzzy/repos')
  return data
}
// 获取最新版本信息
const fetchReleasesLatest = async (repo) => {
  const { data } = await axios.get(`https://api.github.com/repos/yhzzy/${repo}/releases/latest`)
  return data
}
4.4 编写下载模板及写入模板的方法

create.js

const fs = require('fs')
const ejs = require('ejs')
const { promisify } = require('util')
const { loading, isDirectory } = require('../lib/utils/utils')
const downLoadGitRepo = require('download-git-repo')
const downLoadGit = promisify(downLoadGitRepo)

// 下载模板
const download = async (path, repo) => {
  const repoPath = `yhzzy/${repo}`
  await downLoadGit(repoPath, path)
}

// 获取当前已下载模板版本号
const getTemplateVersion = dir => {
  const packageJson = fs.readFileSync(path.join(dir, 'package.json'))
  return JSON.parse(packageJson)
}

// 往目标目录中写入模板文件并完成渲染
const writeTemplateFile = (tempDir, destDir, answers, file) => {
  // 判断是否媒体文件,如果是媒体文件则直接复制过去
  const isMedia = tempDir.split('/').pop() === 'img'
  if (isMedia) {
    const sourceFile = path.join(tempDir, file)
    const destPath = path.join(destDir, file)
    const readStream = fs.createReadStream(sourceFile)
    const writeStream = fs.createWriteStream(destPath)
    return readStream.pipe(writeStream)
  }
  ejs.renderFile(path.join(tempDir, file), answers, (err, result) => {
    if (err) throw err
    fs.writeFileSync(path.join(destDir, file), result)
  })
}

// 读取模板文件目录并完成写入并渲染
const writeTemplateFiles = async (tempDir, destDir, answers) => {
  fs.readdir(tempDir, (err, files) => {
    if (err) throw err
    files.forEach(async (file) => {
      // 判断复制的文件是否为文件夹
      const isDir = isDirectory(path.join(tempDir, file))
      if (isDir) {
        // 判断目标文件夹下是否有此名称文件夹
        const destDirHasThisDir = isDirectory(path.join(destDir, file))
        // 如果没有此名称文件夹则新建文件夹
        if (!destDirHasThisDir) {
          fs.mkdirSync(path.join(destDir, file))
        }
        writeTemplateFiles(path.join(tempDir, file), path.join(destDir, file), answers)
      } else {
        writeTemplateFile(tempDir, destDir, answers, file)
      }
    })
  })
}
4.5 编写选项逻辑

根据用户选择的选项进行对应的模板下载,如果本地已存在模板,则和git仓库中的模板进行版本对比,有新版本发布时从新下载模板,否则不进行模板下载操作
create.js

const del = require('del')
const inquirer = require('inquirer')
const { questions } = require('./utils/constants')

module.exports = async (projectName) => {
  repos = await loading(fetchRepoList, 'fetching repo list')()
  repos = repos.filter(item => item.name.split('-')[0] === 'template')
  inquirer.prompt([
    {
      type: 'list',
      name: 'templateType',
      message: 'please choice a template to create project',
      choices: repos,
    },
  ])
  .then(answer => {
    const { templateType } = answer
    inquirer.prompt([
      ...questions[templateType],
    ])
    .then(async (answers) => {
      // 判断是否存在模板文件夹,不存在则新建文件夹
      const templatesFolder = isDirectory(templatePath)
      if (!templatesFolder) {
        fs.mkdirSync(templatePath)
      }
      const downloadPath = path.join(templatePath, templateType)
      const destDir = process.cwd()
      // 判断模板文件夹中是否存在需要下载的模板文件夹
      if (fs.existsSync(downloadPath)) {
        // 获取模板最新的发布版本号,然后和当前已下载的模板进行比对,如果版本已更新则更新本地模板文件,如果版本一致则不进行下载模板操作
        const { name } = await loading(fetchReleasesLatest, `view the latest ${templateType} version in guthub now...`)(templateType)
        const { releaseVersion } = getTemplateVersion(downloadPath)
        if (name !== releaseVersion) {
          del(downloadPath, {
            force: true,
          })
          await loading(download, 'download the template now...')(downloadPath, templateType)
        }
      } else {
        await loading(download, 'download the template now...')(downloadPath, templateType)
      }
      writeTemplateFiles(downloadPath, destDir, answers)
    })
  })
}
5.使用
# global install
yarn global add yhzzy-cli
# global installed or local link
yhzzy-cli cr

完整脚手架代码
bin/cli.js

#!/usr/bin/env node
require('../lib/main')

utils/utils.js

const fs = require('fs')
const ora = require('ora')

/**
 * 
 * @param {*} fn 执行的方法
 * @param {*} msg 提示语言
 * @returns 
 */
const loading  = (fn, msg) => async (...args) => {
  const spinner = ora(msg)
  spinner.start()
  const res = await fn(...args)
  spinner.succeed()
  return res
}

/**
 * 
 * @param {*} dir 文件路径
 * @returns 
 */
const isDirectory = dir => {
  try{
    return fs.statSync(dir).isDirectory()
  } catch(e) {
    return false
  }
}

module.exports = {
  loading,
  isDirectory,
}

utils/constants.js

const { name, version } = require('../../package.json')
const commonQuestions = [
  {
    type: 'input',
    name: 'name',
    message: 'Project name?',
    default: process.cwd().split('/').pop(),
  },
  {
    type: 'input',
    name: 'version',
    message: 'Project version?',
    default: '0.1.0',
  },
  {
    type: 'input',
    name: 'author',
    message: 'Who is the author?',
  },
  {
    type: 'input',
    name: 'description',
    message: 'Project description?',
    default: 'this is a template',
  },
  {
    type: 'confirm',
    name: 'private',
    message: 'Is private?',
    default: false,
  },
  {
    type: 'list',
    name: 'license',
    message: 'please choice a license',
    choices: ['MIT', 'ISC', 'Apache'],
  },
]
const questions = {
  'template-vue-cli': [
    ...commonQuestions,
    {
      type: 'input',
      name: 'displayName',
      message: 'Display for webpack title name?',
      default: process.cwd().split('/').pop(),
    },
  ],
  'template-nm-cli': [
    ...commonQuestions,
  ],
}

module.exports = {
  name,
  version,
  questions,
}

lib/main.js

const path = require('path')
const fs = require('fs')
const { program } = require('commander')

const { version } = require('./utils/constants')

const actions = {
  create: {
    description: 'create a project',
    alias: 'cr',
    examples: [
      'yhzzy-cli create || yhzzy-cli cr'
    ],
  },
}

Object.keys(actions).forEach(action => {
  program
    .command(action)
    .description(actions[action].description)
    .alias(actions[action].alias)
    .action(() => {
      require(path.resolve(__dirname, action))(...process.argv.slice(3))
    })
})

program.on('--help', () => {
  console.log('Examples')
  Object.keys(actions).forEach(action => {
    (actions[action].examples || []).forEach(ex => {
      console.log(`${ex}`)
    })
  })
})

program.version(version)
  .parse(process.argv)

lib/create.js

const path = require('path')
const fs = require('fs')
const del = require('del')
const axios = require('axios')
const inquirer = require('inquirer')
const ejs = require('ejs')
const { promisify } = require('util')
const { loading, isDirectory } = require('../lib/utils/utils')
const { questions } = require('./utils/constants')
const downLoadGitRepo = require('download-git-repo')
const downLoadGit = promisify(downLoadGitRepo)

// 模板列表
let repos = []

// 模板下载地址
const templatePath = path.resolve(__dirname, '../templates')

// 获取仓库地址
const fetchRepoList = async () => {
  const { data } = await axios.get('https://api.github.com/users/yhzzy/repos')
  return data
}
// 获取最新版本信息
const fetchReleasesLatest = async (repo) => {
  const { data } = await axios.get(`https://api.github.com/repos/yhzzy/${repo}/releases/latest`)
  return data
}

// 下载模板
const download = async (path, repo) => {
  const repoPath = `yhzzy/${repo}`
  await downLoadGit(repoPath, path)
}

// 获取当前已下载模板版本号
const getTemplateVersion = dir => {
  const packageJson = fs.readFileSync(path.join(dir, 'package.json'))
  return JSON.parse(packageJson)
}

// 往目标目录中写入模板文件并完成渲染
const writeTemplateFile = (tempDir, destDir, answers, file) => {
  // 判断是否媒体文件,如果是媒体文件则直接复制过去
  const isMedia = tempDir.split('/').pop() === 'img'
  if (isMedia) {
    const sourceFile = path.join(tempDir, file)
    const destPath = path.join(destDir, file)
    const readStream = fs.createReadStream(sourceFile)
    const writeStream = fs.createWriteStream(destPath)
    return readStream.pipe(writeStream)
  }
  ejs.renderFile(path.join(tempDir, file), answers, (err, result) => {
    if (err) throw err
    fs.writeFileSync(path.join(destDir, file), result)
  })
}

// 读取模板文件目录并完成写入并渲染
const writeTemplateFiles = async (tempDir, destDir, answers) => {
  fs.readdir(tempDir, (err, files) => {
    if (err) throw err
    files.forEach(async (file) => {
      // 判断复制的文件是否为文件夹
      const isDir = isDirectory(path.join(tempDir, file))
      if (isDir) {
        // 判断目标文件夹下是否有此名称文件夹
        const destDirHasThisDir = isDirectory(path.join(destDir, file))
        // 如果没有此名称文件夹则新建文件夹
        if (!destDirHasThisDir) {
          fs.mkdirSync(path.join(destDir, file))
        }
        writeTemplateFiles(path.join(tempDir, file), path.join(destDir, file), answers)
      } else {
        writeTemplateFile(tempDir, destDir, answers, file)
      }
    })
  })
}

module.exports = async () => {
  repos = await loading(fetchRepoList, 'fetching repo list')()
  repos = repos.filter(item => item.name.split('-')[0] === 'template')
  inquirer.prompt([
    {
      type: 'list',
      name: 'templateType',
      message: 'please choice a template to create project',
      choices: repos,
    },
  ])
  .then(answer => {
    const { templateType } = answer
    inquirer.prompt([
      ...questions[templateType],
    ])
    .then(async (answers) => {
      // 判断是否存在模板文件夹,不存在则新建文件夹
      const templatesFolder = isDirectory(templatePath)
      if (!templatesFolder) {
        fs.mkdirSync(templatePath)
      }
      const downloadPath = path.join(templatePath, templateType)
      const destDir = process.cwd()
      // 判断模板文件夹中是否存在需要下载的模板文件夹
      if (fs.existsSync(downloadPath)) {
        // 获取模板最新的发布版本号,然后和当前已下载的模板进行比对,如果版本已更新则更新本地模板文件,如果版本一致则不进行下载模板操作
        const { name } = await loading(fetchReleasesLatest, `view the latest ${templateType} version in guthub now...`)(templateType)
        const { releaseVersion } = getTemplateVersion(downloadPath)
        if (name !== releaseVersion) {
          del(downloadPath, {
            force: true,
          })
          await loading(download, 'download the template now...')(downloadPath, templateType)
        }
      } else {
        await loading(download, 'download the template now...')(downloadPath, templateType)
      }
      writeTemplateFiles(downloadPath, destDir, answers)
    })
  })
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值