Пусть NodeJS сияет в вашем проекте

Node.js
Пусть NodeJS сияет в вашем проекте

В последние годы поNodeJSВесенний ветерок, передняя часть пережила волну перетасовки развития. Он совершил качественный скачок в эффективности и качестве front-end разработки. Можно сказатьNodeJSЭто уже незаменимый навык во фронтенде. Но на самом деле большинство интерфейсов устанавливаются локальноNodeJSиспользование может быть ограниченоnode -vа такжеnpm😂. фактическиNodeJSКак настоящий серверный язык, его можно использовать при разработкеNodeJSМощные модули и многочисленныеnpmпакет, чтобы служить себе.

написать впереди

Примечание. В этой статье не будут обсуждаться некоторые базовые вещи, надеюсь, вы привнесете свои собственные.ES6+грамматика,NodeJSбазовый, простойLinuxзнание операции. Кроме того, эта статья не будет посвящена деталям реализации технологии, а в основном предоставит некоторые идеи и направления. Для более глубокого использования друзья все еще могут раскопать его самостоятельно.И этот пост будет немного длинным 🎫

Быстро создавайте модули

В этой части я использовалУскорить разработку проектов VueЭто было упомянуто в , но та версия была написана относительно просто (грубо), как Демо, меня это не очень удовлетворило, поэтому я переделал ее. Это скрипт, если говорить прямоNodeJSДля замены нам нужно сгенерировать файлы и код, который нужно скопировать и вставить. Создайте модуль с помощью следующей команды.

В масштабных проектах, особенно в мидл и бэкенд проектах, при разработке нового бизнес-модуля много копий и вставок могут быть неразделимы Например, многие модули в мидл и бэкенд проектах могут быть стандартнымиCURDмодуль, в том числе列表,新增,详情,编辑эти страницы. Тогда это значит что много дублирующегося кода.После каждого копирования и вставки возникает куча заморочек типа модификация,модификация,удаление и удаление.Самое главное что при копировании и вставке легко забыть какая часть и забудьте изменить его, в результате чего проект является грубым и тратит много времени. Тогда наше требование — передать ту часть, которая была вручную скопирована и вставлена ​​вNodeделать, стремиться时间和质量Получите двойную защиту.

доVueЭтот модуль был написан в проекте, затем следующийdemoмы беремVueпроект в качестве примера,адрес проекта.

Предварительная подготовка

  • Разделение файловой структуры: файлы просмотра, файлы маршрутизации,ControlerКак положить файл должен быть четко разделен. Это как основа, которую можно разделить в соответствии с бизнесом вашего собственного проекта.Наш каталог проектов разделен следующим образом

      vue-base-template
      │   config                            // webpack配置config/q其他一些config文件
      │   scripts                           // 帮助脚本文件 ===> 这里存放我们的项目脚本文件
      │   │   template                      // 模块文件
      │   │   build-module.js               // build构建脚本
      │   │   
      └───src                               // 业务逻辑代码
      │   │   api                           // http api 层
      │   └── router                        // 路由文件
      │   │     │  modules                  // 业务路由文件夹  ==> 业务模块路由生成地址
      │   │           │ module.js           // 制定模块
      │   │   store                         // vuex
      │   └── views                         // 视图文件
      │   │     │  directory                // 抽象模块目录
      │   │     │      │  module            // 具体模块文件夹
      │   │     │      │    │ index.vue     // 视图文件
      │   │   global.js                     // 全局模块处理
      │   │   main.js                       // 入口文件
    

    Бизнес-модуль, который я в основном прохожу抽象模块+具体模块Способ деления:

    • Абстрактный модуль: это означает, что нет конкретной функциональной страницы, но есть сводка по ряду бизнес-модулей, что эквивалентно каталогу, содержащему каждый конкретный модуль.
    • Конкретный модуль: относится к модулю с определенными функциональными страницами, включая каждую конкретную страницу.

    Этот метод разделения очень гибкий, в основном в соответствии с их собственными потребностями.

  • Пользовательские файлы шаблонов: в основном используются для создания шаблонов для таких файлов, как.vueтакой файл

  • Технические приготовления:

    Теоретически нам нужно использовать следующееnpmМодуль, как минимум надо знать для чего это

    • dotenv: Управление файлом конфигурации
    • inquirer: взаимодействие с командной строкой
    • chalk: Украсить вывод командной строки
  • Создать процесс

    Процесс очень простой, и я не умею рисовать, так что давайте заставим его работать. Перекрашу, когда будет время 😂

начать мастурбировать

Поскольку у меня появилась идея, этот сценарий я делаю уже в третий раз. От первого единственного простого до настоящего идеального функционирования. Больше всего мне кажется, что у этой штуки нет конца в развитии, каждый раз я нахожу разные точки спроса и каждый раз нахожу части, которые можно оптимизировать. Скрипты могут быть простыми или сложными для ваших собственных проектов. Очень волшебно, для меня должен быть четвертый и пятый рефакторинг.

Для последующего обслуживания я поместил все скрипты, связанные сNodeJSКод размещается в корневом каталогеscriptsв папке

scripts                             // 帮助脚本文件 ===> 这里存放我们的项目脚本文件
└───template                        // template管理文件夹
│   │   index.js                    // 模块文件处理中心
│   │   api.template.js             // api模块文件
│   │   route.template.js           // route模块文件
│   │   template.vue                // view模块文件
│   build-module.js                 // 创建脚本入口文件
│   │   
|   .env.local                      // 本地配置文件
│   │                               
│   util.js                         // 工具文件

Давайте поговорим о роли этих файлов по частям (предупреждение о большом количестве кода)

  • build-module.js: файл входа, скрипт, взаимодействующий с пользователем. Получите три основные переменные, которые нам нужны, задав вопросы目录(抽象模块), 模块(具体模块), 注释. Если это первый раз запускать этот скрипт, то будут проблемы связанные с конфигурацией.После первой настройки последующее использование не будет запрашиваться.Если вы хотите изменить его, вы можете изменить его самостоятельно..env.localдокумент. Подробно описывать здесь не буду, большая часть пояснений написана в комментариях.
const inquirer = require('inquirer')
const path = require('path')
const { Log, FileUtil, LOCAL , ROOTPATH} = require('./util')
const { buildVueFile, buildRouteFile, buildApiFile, RouteHelper } = require('./template')
const EventEmitter = require('events');
// file options
const questions = [
  {
    type: 'input',
    name: 'folder',
    message: "请输入所属目录名称(英文,如果检测不到已输入目录将会默认新建,跳过此步骤将在Views文件夹下创建新模块):"
  },
  {
    type: 'input',
    name: 'module',
    message: "请输入模块名称(英文)",
    // 格式验证
    validate: str => ( str !== '' && /^[A-Za-z0-9_-]+$/.test(str))
  },
  {
    type: 'input',
    name: 'comment',
    message: "请输入模块描述(注释):"
  },
]
// local configs 
const configQuestion = [
  {
    type: 'input',
    name: 'AUTHOR',
    message: "请输入作者(推荐使用拼音或者英文)",
    // 格式验证
    validate: str => ( str !== '' && /^[\u4E00-\u9FA5A-Za-z]+$/.test(str)),
    when: () => !Boolean(process.env.AUTHOR)
  },
  {
    type: 'input',
    name: 'Email',
    message: "请输入联系方式(邮箱/电话/钉钉)"
  }
]
// Add config questions if local condfig does not exit
if (!LOCAL.hasEnvFile()) {
  questions.unshift(...configQuestion)
}
// 获取已经完成的答案
inquirer.prompt(questions).then(answers => {
  // 1: 日志打印
  Log.logger(answers.folder == '' ? '即将为您' : `即将为您在${answers.folder}文件夹下` + `创建${answers.module}模块`)
  // 2: 配置文件的相关设置
  if (!LOCAL.hasEnvFile()) {
    LOCAL.buildEnvFile({
      AUTHOR: answers.AUTHOR,
      Email: answers.Email
    })
  }
  // 3: 进入文件和目录创建流程
  const {
    folder, // 目录
    module, // 模块
    comment // 注释
  } = answers
  buildDirAndFiles(folder, module, comment)
})
// 事件处理中心
class RouteEmitter extends EventEmitter {}
// 注册事件处理中心
const routeEmitter = new RouteEmitter() 
routeEmitter.on('success', value => {
  // 创建成功后正确退出程序
  if (value) {
    process.exit(0)
  }
})
// module-method map
// create module methods
const generates = new Map([
  // views部分
  // 2019年6月12日17:39:29 完成
  ['view', (folder, module, isNewDir , comment) => {
    // 目录和文件的生成路径
    const folderPath = path.join(ROOTPATH.viewsPath,folder,module)
    const vuePath = path.join(folderPath, '/index.vue')
    // vue文件生成
    FileUtil.createDirAndFile(vuePath, buildVueFile(module, comment), folderPath)
  }],
  // router is not need new folder
  ['router', (folder, module, isNewDir, comment) => {
    /**
     * @des 路由文件和其他的文件生成都不一样, 如果是新的目录那么生成新的文件。
     * 但是如果module所在的folder 已经存在了那么就对路由文件进行注入。
     * @reason 因为我们当前项目的目录分层结构是按照大模块来划分, 即src下一个文件夹对应一个router/modules中的一个文件夹
     * 这样做使得我们的目录结构和模块划分都更加的清晰。
     */
    if (isNewDir) {
      // 如果folder不存在 那么直接使用module命名 folder不存在的情况是直接在src根目录下创建模块
      const routerPath = path.join(ROOTPATH.routerPath, `/${folder || module}.js`)
      FileUtil.createDirAndFile(routerPath, buildRouteFile(folder, module, comment))
    } else {
      // 新建路由helper 进行路由注入
      const route = new RouteHelper(folder, module, routeEmitter)
      route.injectRoute()
    }
  }],
  ['api', (folder, module, isNewDir, comment) => {
    // inner module will not add new folder
    // 如果当前的模块已经存在的话那么就在当前模块的文件夹下生成对应的模块js
    const targetFile = isNewDir ? `/index.js` : `/${module}.js`
    // 存在上级目录就使用上级目录  不存在上级目录的话就是使用当前模块的名称进行创建
    const filePath = path.join(ROOTPATH.apiPath, folder || module)
    const apiPath = path.join(filePath, targetFile)
    FileUtil.createDirAndFile(apiPath, buildApiFile(comment), filePath)
  }]
])
/**
 * 通过我们询问的答案来创建文件/文件夹
 * @param {*} folder 目录名称
 * @param {*} module 模块名称
 * @param {*} comment 注释
 */
function buildDirAndFiles (folder, module, comment) {
  let _tempFloder = folder || module // 临时文件夹 如果当前的文件是
  let isNewDir
  // 如果没有这个目录那么就新建这个目录
  if (!FileUtil.isPathInDir(_tempFloder, ROOTPATH.viewsPath)) {
    rootDirPath = path.join(ROOTPATH.viewsPath, _tempFloder)
    // create dir for path
    FileUtil.createDir(rootDirPath)
    Log.success(`已创建${folder ? '目录' : "模块"}${_tempFloder}`)
    isNewDir = true
  } else {
    isNewDir = false
  }
  // 循环操作进行
  let _arrays = [...generates]
  _arrays.forEach((el, i) => {
    if (i < _arrays.length) {
      el[1](folder, module, isNewDir, comment)
    } else {
      Log.success("模块创建成功!")
      process.exit(1)
    }
  })
}

Примечание. Здесь я использовалgeneratesэтоMapДля управления всеми операциями, т. к. предыдущая версия написана так, мне лень менять, можно также использовать двумерный массив или объект для управления, а также сохранить выбор условий записи.

  • template: управляет файлами шаблонов, используемыми сгенерированными файлами (vue文件,路由文件, api文件), мы смотрим только наroute.template.js, другие части могут относиться к проекту
/*
 * @Author: _author_
 * @Email: _email_
 * @Date: _date_
 * @Description: _comment_
 */
export default [
  {
    path: "/_mainPath",
    component: () => import("@/views/frame/Frame"),
    redirect: "/_filePath",
    name: "_mainPath",
    icon: "",
    noDropdown: false,
    children: [
      {
        path: "/_filePath",
        component: () => import("@/views/_filePath/index"),
        name: "_module",
        meta: {
          keepAlive: false
        }
      }
    ]
  }
]

существуетtemplateсамый важный изindex.jsТеперь этот файл в основном содержитФайл шаблона считывается и регенерируется, чтобы сгенерировать нужную нам строку шаблона, а также конкретный код маршрутизации, который нам нужен..template/index.js, Генерация шаблонов файлов в основном осуществляется путем чтения каждого файла шаблона и преобразования его в строку, а также преобразованияУказанная строка заменяется ожидаемой переменной, а затем возвращает новую строку для использования в make-файле.

const fs = require('fs')
const path = require('path')
const os = require('os')
const readline = require('readline')
const {Log, DateUtil, StringUtil , LOCAL, ROOTPATH} = require('../util')
/**
 * 替换作者/时间/日期等等通用注释
 * @param {*string} content 内容
 * @param {*string} comment 注释
 * @todo 这个方法还有很大的优化空间
 */
const _replaceCommonContent = (content, comment) => {
  if (content === '') return ''
  // 注释对应列表 comments =  [ [文件中埋下的锚点, 将替换锚点的目标值] ]
  const comments = [
    ['_author_', LOCAL.config.AUTHOR],
    ['_email_', LOCAL.config.Email],
    ['_comment_', comment],
    ['_date_', DateUtil.getCurrentDate()]
  ]
  comments.forEach(item => {
    content = content.replace(item[0], item[1])
  })
  return content
}
/**
 * 生成Vue template文件
 * @param {*} moduleName 模块名称
 * @returns {*string}
 */
module.exports.buildVueFile = (moduleName, comment) => {
  const VueTemplate = fs.readFileSync(path.resolve(__dirname, './template.vue'))
  const builtTemplate = StringUtil.replaceAll(VueTemplate.toString(), "_module_", moduleName)
  return _replaceCommonContent(builtTemplate, comment)
}
/**
 * @author: etongfu
 * @description: 生成路由文件
 * @param {string} folder 文件夹名称 
 * @param {string} moduleName 模块名称
 * @returns  {*string}
 */
module.exports.buildRouteFile = (folder,moduleName, comment) => {
  const RouteTemplate = fs.readFileSync(path.resolve(__dirname, './route.template.js')).toString()
  // 因为路由比较特殊。路由模块需要指定的路径。所以在这里重新生成路由文件所需要的参数。
  const _mainPath = folder || moduleName
  const _filePath = folder == '' ? `${moduleName}` : `${folder}/${moduleName}`
  // 进行替换
  let builtTemplate = StringUtil.replaceAll(RouteTemplate, "_mainPath", _mainPath) // 替换模块主名称
  builtTemplate = StringUtil.replaceAll(builtTemplate, "_filePath", _filePath) // 替换具体路由路由名称
  builtTemplate = StringUtil.replaceAll(builtTemplate, "_module", moduleName) // 替换模块中的name
  return _replaceCommonContent(builtTemplate, comment)
}

/**
 * @author: etongfu
 * @description: 生成API文件
 * @param {string}  comment 注释
 * @returns:  {*}
 */
module.exports.buildApiFile = comment => {
  const ApiTemplate = fs.readFileSync(path.resolve(__dirname, './api.template.js')).toString()
  return _replaceCommonContent(ApiTemplate, comment)
}

инъекция маршрута: Когда входной каталог уже существует, новый файл каталога не будет создан. В это время маршрут нового модуля будет введен в файл маршрута существующего каталога. Эффект следующий

Здесь мы проходимRouteHelperЧтобы завершить операцию внедрения маршрута нового модуля в существующий файл маршрута, в основном черезпоток,readline (читать построчно)быть реализованным.

Далее идет часть сухих товаров: при выполнении внедрения маршрута сначала найдите наш целевой файл маршрутизации через параметры, а затем передайтеgenerateRouter()Чтобы сплайсировать для генерации маршрутов, нам нужно внедрить. пройти черезinjectRouteметод начинает вводить маршруты, вinjectRouteСначала мы генерируем имя как_rootфайл с временным путем и создать файл на основе этого путиwriteStream, то по старому адресу файла маршрутизацииrootСоздаватьreadStreamи черезreadlineИнтерфейс чтения-записи считывает исходный файл маршрута и использует массив для сбора данных каждой строки старого маршрута. Начать обход после прочтенияtempэтот массив и найти первыйchildrenтогда поставьgenerateRouter()Массив, возвращаемый методом, вставляется в эту позицию. Наконец-то закончили шитьtempПеребирать запись построчноwriteStreamсередина. Наконец-то поставил оригиналrootфайл удалить, поставить_rootпереименован вroot. Процесс внедрения маршрута завершен. Общий процесс такой, друзья, которые не знают деталей кода, могут написать мне в приват 😁.


/**
 * @author: etongfu
 * @description: 路由注入器
 * @param {string}  dirName
 * @param {string}  moduleName
 * @param {event}  event
 * @returns:  {*}
 */
module.exports.RouteHelper = class {
  constructor (dirName, moduleName, event) {
    // the dir path for router file
    this.dirName = dirName
    // the path for router file
    this.moduleName = moduleName
    // 事件中心
    this.event = event
    // route absolute path
    this.modulePath = path.join(ROOTPATH.routerPath, `${dirName}.js`)
  }
  /**
   * Generate a router for module
   * The vue file path is @/name/name/index
   * The default full url is http:xxxxx/name/name
   * @param {*} routeName url default is router name
   * @param {*string} filePath vue file path default is ${this.dirName}/${this.moduleName}/index
   * @returns {*Array} A string array for write line
   */
  generateRouter (routeName = this.moduleName, filePath = `${this.dirName}/${this.moduleName}/index`) {
    let temp = [
      `      // @Author: ${LOCAL.config.AUTHOR}`,
      `      // @Date: ${DateUtil.getCurrentDate()}`,
      `      {`,
      `        path: "/${this.dirName}/${routeName}",`,
      `        component: () => import("@/views/${filePath}"),`,
      `        name: "${routeName}"`,
      `      },`
    ]
    return temp
  }
  /**
   * add router to file
   */
  injectRoute () {
    try {
      const root = this.modulePath
      const _root = path.join(ROOTPATH.routerPath, `_${this.dirName}.js`)
      // temp file content
      let temp = []
      // file read or write
      let readStream = fs.createReadStream(root)
      // temp file
      let writeStream = fs.createWriteStream(_root)
      let readInterface = readline.createInterface(
        {
          input: readStream
        // output: writeStream
        }
      )
      // collect old data in file
      readInterface.on('line', (line) => {
        temp.push(line)
      })
      // After read file and we begin write new router to this file
      readInterface.on('close', async () => {
        let _index
        temp.forEach((line, index) => {
          if (line.indexOf('children') !== -1) {
            _index = index + 1
          }
        })
        temp = temp.slice(0, _index).concat(this.generateRouter(), temp.slice(_index))
        // write file
        temp.forEach((el, index) => {
          writeStream.write(el + os.EOL)
        })
        writeStream.end('\n')
        // 流文件读写完毕
        writeStream.on('finish', () => {
          fs.unlinkSync(root)
          fs.renameSync(_root, root)
          Log.success(`路由/${this.dirName}/${this.moduleName}注入成功`)
          //emit 成功事件
          this.event.emit('success', true)
        })
      })
    } catch (error) {
      Log.error('路由注入失败')
      Log.error(error)
    }
  }
}

Я сам не очень доволен дизайном маршрутизации инъекций.Если есть лучший способ, пожалуйста, дайте мне знать.

  • .env.local: файл конфигурации, который создается при первом использовании скрипта. Ничего особенного, просто запишите локальные элементы конфигурации.
AUTHOR = etongfu
Email = 13583254085@163.com
  • util.js: Различные инструменты и методы, в том числеdate, file, fs, string, Log, ROOTPATHДождитесь инструментов и методов, я выложу часть кода из-за ограниченного места, вы можете просмотреть весь код в проекте
const chalk = require('chalk')
const path = require('path')
const dotenv = require('dotenv')
const fs = require('fs')
// 本地配置相关
module.exports.LOCAL = class  {
  /**
   * env path
   */
  static get envPath () {
    return path.resolve(__dirname, './.env.local')
  }
  /**
   * 配置文件
   */
  static get config () {
    // ENV 文件查找优先查找./env.local
    const ENV = fs.readFileSync(path.resolve(__dirname, './.env.local')) || fs.readFileSync(path.resolve(__dirname, '../.env.development.local'))
    // 转为config
    const envConfig = dotenv.parse(ENV)
    return envConfig
  }
  /**
   * 创建.env配置文件文件
   * @param {*} config 
   * @description 创建的env文件会保存在scripts文件夹中
   */
  static buildEnvFile (config = {AUTHOR: ''}) {
    if (!fs.existsSync(this.envPath)) {
      // create a open file
      fs.openSync(this.envPath, 'w')
    }
    let content = ''
    // 判断配置文件是否合法
    if (Object.keys(config).length > 0) {
      // 拼接内容
      for (const key in config) {
        let temp = `${key} = ${config[key]}\n`
        content += temp
      }
    }
    // write content to file
    fs.writeFileSync(this.envPath, content, 'utf8')
    Log.success(`local env file ${this.envPath} create success`)
  }
  /**
   * 检测env.loacl文件是否存在
   */
  static hasEnvFile () {
    return fs.existsSync(path.resolve(__dirname, './.env.local')) || fs.existsSync(path.resolve(__dirname, '../.env.development.local'))
  }
}

// 日志帮助文件
class Log {
  // TODO
}
module.exports.Log = Log

// 字符串Util
module.exports.StringUtil = class {
    // TODO
}
// 文件操作Util
module.exports.FileUtil = class {
  // TODO
  /**
   * If module is Empty then create dir and file
   * @param {*} filePath .vue/.js 文件路径
   * @param {*} content 内容
   * @param {*} dirPath 文件夹目录
   */
  static createDirAndFile (filePath, content, dirPath = '') {
    try {
      // create dic if file not exit
      if (dirPath !== '' && ! fs.existsSync(dirPath)) {
        // mkdir new dolder
        fs.mkdirSync(dirPath)
        Log.success(`created ${dirPath}`)
      }
      if (!fs.existsSync(filePath)) {
        // create a open file
        fs.openSync(filePath, 'w')
        Log.success(`created ${filePath}`)
      }
      // write content to file
      fs.writeFileSync(filePath, content, 'utf8')
    } catch (error) {
      Log.error(error)
    }
  }
}
// 日期操作Util
module.exports.DateUtil = class {
  // TODO
}

существуетUtilЧасть файла, на которую следует обратить внимание, может быть.envгенерация файла и чтение этой части иFileUtilсерединаcreateDirAndFile, это метод, который мы используем для создания папок и файлов, используяnodeФайловая система завершена. знакомыйAPIТрудности не будет.

UtilВ файле есть одинROOTPATHОбратите внимание, что это относится к нашемуМаршрутизация, представления, конфигурация корневого каталога API, я предлагаю не записывать эту конфигурацию, потому что, если в вашем проекте есть несколько записей или подпроектов, они могут измениться. Вы также можете выбрать другие способы настройки.

// root path
const reslove = (file = '.') => path.resolve(__dirname, '../src', file)
const ROOTPATH = Object.freeze({
  srcPath: reslove(),
  routerPath: reslove('router/modules'),
  apiPath: reslove('api'),
  viewsPath: reslove('views')
})
module.exports.ROOTPATH = ROOTPATH
  • предварительный просмотр

Таким образом, мы можем быстро создавать модули через командную строку, эффект выглядит следующим образом.

бегать

  • Суммировать

Хотя эти вещи можно делать путем копирования и вставки, надежность и надежность выполнения их с помощью машин будут значительно улучшены.Наша фронтенд-команда теперь полностью использует скрипты для создания новых модулей, и скрипты постоянно обновляются. Время, сэкономленное таким скриптом для команды в большом проекте, очень значительное., предлагаю вам тоже написать этот скрипт, когда у вас будет время сэкономить драгоценное время для команды или себя😁

выполнять механические задачи

Во время разработки большая часть работы была механической и неинтересной. Мне жаль, что они не используют эти вещи

SSH-релиз

Примечание. Если команда развертываетCI/CD, эту часть можно игнорировать напрямую.

После моего наблюдения многие front-end программисты не понимаютLinuxЭксплуатация, иногда нужно найти коллег для помощи при выпуске теста, если другой коллегаLinuxЕсли основа не очень хорошая, они могут потерять много времени для них двоих. Сегодня, написав скрипт, все наши коллеги могут самостоятельно выпустить тест. Этот файл также находится в папке проекта.scriptsв папке

Предварительная подготовка

  • Настройте первойWebСервер, если нет, то можете прочитать мою предыдущую статьюРуководство по публикации сервера Vue

  • Требуемые технологии - это все пакеты инструментов, здесь нет никаких сложностей.

    • inquirer: взаимодействие с командной строкой
    • chalk: Украсить вывод командной строки
    • ora: Командная строкаloading
    • shelljs: воплощать в жизньshellЗаказ
    • node-ssh: node SSH
    • node-ssh: node SSH
    • zip-local: zipкомпрессия

начать мастурбировать

потому что он опубликован на другом сервере, потому чтоdevelopment/stage/productionВсе это должны быть разные серверы, поэтому нам нужен файл конфигурации для управления серверами.deploy.config.js

module.exports = Object.freeze({
  // development
  development: {
    SERVER_PATH: "xxx.xxx.xxx.xx", // ssh地址
    SSH_USER: "root", // ssh 用户名
    SSH_KEY: "xxx", // ssh 密码 / private key文件地址
    PATH: '/usr/local' // 操作开始文件夹 可以直接指向配置好的地址
  },
  // stage
  stage: {
    SERVER_PATH: "",
    SSH_USER: "",
    SSH_KEY: "",
    PATH: ''
  },
  // production
  production: {
    SERVER_PATH: "",
    SSH_USER: "",
    SSH_KEY: "",
    PATH: ''
  }
})

После того, как конфигурационный файл настроен, приступаем к написанию скрипта, сначала определимся с процессом.

  1. пройти черезinquirerЗадайте вопрос, это пример кода, вопрос относительно прост, включая фактическое использование发布平台и так далее для разных целей выпуска.
  2. Проверьте файл конфигурации, потому что файл конфигурации является точным.
  3. компрессияdistфайл, черезzip-localработать. очень простой
  4. пройти черезnode-sshподключиться к серверу
  5. Выполните удаление и резервное копирование (резервная копия еще не записана) старых файлов на сервере.
  6. передачаSSHизputFileМетод начинает загрузку локального файла на сервер.
  7. выполнить на сервереunzipЗаказ. 8: Релиз завершен 🎈

Ниже сухой код

const fs = require('fs')
const path = require('path')
const ora = require('ora')
const zipper = require('zip-local')
const shell = require('shelljs')
const chalk = require('chalk')
const CONFIG = require('../config/release.confg')
let config
const inquirer = require('inquirer')
const node_ssh = require('node-ssh')
let SSH = new node_ssh()
// loggs
const errorLog = error => console.log(chalk.red(`*********${error}*********`))
const defaultLog = log => console.log(chalk.blue(`*********${log}*********`))
const successLog = log => console.log(chalk.green(`*********${log}*********`))
// 文件夹位置
const distDir = path.resolve(__dirname, '../dist')
const distZipPath = path.resolve(__dirname, '../dist.zip')
// ********* TODO 打包代码 暂时不用 需要和打包接通之后进行测试 *********
const compileDist = async () => {
  // 进入本地文件夹
  shell.cd(path.resolve(__dirname, '../'))
  shell.exec(`npm run build`)
  successLog('编译完成')
}
// ********* 压缩dist 文件夹 *********
const zipDist =  async () => {
  try {
    if(fs.existsSync(distZipPath)) {
      defaultLog('dist.zip已经存在, 即将删除压缩包')
      fs.unlinkSync(distZipPath)
    } else {
      defaultLog('即将开始压缩zip文件')
    }
    await zipper.sync.zip(distDir).compress().save(distZipPath);
    successLog('文件夹压缩成功')
  } catch (error) {
    errorLog(error)
    errorLog('压缩dist文件夹失败')
  }
}
// ********* 连接ssh *********
const connectSSh = async () =>{
  defaultLog(`尝试连接服务: ${config.SERVER_PATH}`)
  let spinner = ora('正在连接')
  spinner.start()
  try {
    await SSH.connect({
      host: config.SERVER_PATH,
      username: config.SSH_USER,
      password: config.SSH_KEY
    })
    spinner.stop()
    successLog('SSH 连接成功')
  } catch (error) {
    errorLog(err)
    errorLog('SSH 连接失败');
  }
}
// ********* 执行清空线上文件夹指令 *********
const runCommond = async (commond) => {
  const result = await SSH.exec(commond,[], {cwd: config.PATH})
  defaultLog(result)
}
const commonds = [`ls`, `rm -rf *`]
// ********* 执行清空线上文件夹指令 *********
const runBeforeCommand = async () =>{
  for (let i = 0; i < commonds.length; i++) {
    await runCommond(commonds[i])
  }
}
// ********* 通过ssh 上传文件到服务器 *********
const uploadZipBySSH = async () => {
  // 连接ssh
  await connectSSh()
  // 执行前置命令行
  await runBeforeCommand()
  // 上传文件
  let spinner = ora('准备上传文件').start()
  try {
    await SSH.putFile(distZipPath, config.PATH + '/dist.zip')
    successLog('完成上传')
    spinner.text = "完成上传, 开始解压"
    await runCommond('unzip ./dist.zip')
  } catch (error) {
    errorLog(error)
    errorLog('上传失败')
  }
  spinner.stop()
}
// ********* 发布程序 *********
/**
 * 通过配置文件检查必要部分
 * @param {*dev/prod} env 
 * @param {*} config 
 */
const checkByConfig = (env, config = {}) => {
  const errors = new Map([
    ['SERVER_PATH',  () => {
      // 预留其他校验
      return config.SERVER_PATH == '' ? false : true
    }],
    ['SSH_USER',  () => {
      // 预留其他校验
      return config.SSH_USER == '' ? false : true
    }],
    ['SSH_KEY',  () => {
      // 预留其他校验
      return config.SSH_KEY == '' ? false : true
    }]
  ])
  if (Object.keys(config).length === 0) {
    errorLog('配置文件为空, 请检查配置文件')
    process.exit(0)
  } else {
    Object.keys(config).forEach((key) => {
      let result = errors.get(key) ? errors.get(key)() : true
      if (!result) {
        errorLog(`配置文件中配置项${key}设置异常,请检查配置文件`)
        process.exit(0)
      }
    })
  }
  
}
// ********* 发布程序 *********
const runTask = async () => {
  // await compileDist()
  await zipDist()
  await uploadZipBySSH()
  successLog('发布完成!')
  SSH.dispose()
  // exit process
  process.exit(1)
}
// ********* 执行交互 *********
inquirer.prompt([
  {
    type: 'list',
    message: '请选择发布环境',
    name: 'env',
    choices: [
      {
        name: '测试环境',
        value: 'development'
      },
      {
        name: 'stage正式环境',
        value: 'production'
      },
      {
        name: '正式环境',
        value: 'production'
      }
    ]
  }
]).then(answers => {
  config = CONFIG[answers.env]
  // 检查配置文件
  checkByConfig(answers.env, config)
  runTask()
})

Предварительный просмотр эффекта

На данный момент каждый может с радостью опубликовать код и опубликовать его безболезненно. Профессиональное тестирование не займет более30s

Крюк после упаковки

Я планирую написать статью об этом. Потому что этот пост немного длинный. . .

Суммировать

Написание этих сценариев может занять некоторое время, но после завершения команда значительно повысит свою эффективность и качество, что позволит разработчикам больше сосредоточиться на бизнесе и технологиях. В то же время нельзя игнорировать экономию времени, к такому выводу я пришел после командного эксперимента. Если раньше подготовка к копированию и вставке на ранней стадии разработки модуля могла занимать более получаса, то теперь подготовку модуля на ранней стадии и статическую разработку страницы списка можно выполнить за 10 минут. После написания сценария релиза каждый коллега может самостоятельно выпустить тестовую среду (не у всех есть официальное разрешение), и времени на это уходит очень мало. Они на самом деле отражаются в ежедневном развитии. Кроме тогоNodeОкружение все установлено, зря его использовать не нужно (белая проституция 😁😁😁), так же вы можете сами расходиться с мыслями,Не двигайте кирпичи, которые можно переместить с помощью кода.

образец кода

исходный адресСтавьте ⭐, если считаете это полезным