likes
comments
collection
share

Node.js开发Iconfont链接同步云服务器的命令行工具

作者站长头像
站长
· 阅读数 3

前言

在日常的前端开发当中,我们通常脱离不了图标库的使用,而在众多图标库中,iconFont是被运用最广泛的。其作用简单来说,就是可以把图标转换成字体文件,再通过文本展示出来。

其使用方法也非常简单,官方支持生成cdn链接,提供给使用方通过<link>标签的形式引入。

同时官方也给过提醒:在线链接服务仅供平台体验和调试使用,平台不承诺服务的稳定性,企业客户需下载字体包自行发布使用并做好备份,对于企业级的网站来说,将生成的字体包加载到自己的服务器中,是最稳妥的方式。除此以外,将字体文件部署在自己的网站中,可以最大限度的复用DNS查询,从而提升资源访问的速度。

因此,下面我将用node打造一个命令行工具,实现cdn链接到服务器的快速同步;

工具&前置知识

  • readline: Node.js中实现控制台输入输出的模块
  • ssh2-sftp-client: 使用javascript编写的Promise风格的SSH SFTP操作工具包

具体实现

一、 使用readline实现交互功能

import process from "process"
import readline from "readline"

// 创建readline接口实例
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

rl.question("🔗 请输入下载的iconfont链接: ", (link) => {
  rl.question("📁 请输入保存的文件名: ", (name) => {
    // TODO: 具体实现...
    rl.close()
  })
})

Node.js开发Iconfont链接同步云服务器的命令行工具

二、 将iconfont服务器的文件下载至本地

基于第一步操作,拿到用户输入的iconfont链接与要保存的文件名,通过node内置的https模块下载对应的css文件并解析下载css文件中的url(FONT_URL) format(***)中的FONT_URL;

核心代码如下:

import {
  DOMAIN, // 域名
  LOCAL_TEMP_DIRECTORY, // 本地临时目录
  REMOTE_DIRECTORY // 上传目录
} from "env.js"

const localFiles = [] // 需要上传到服务器的文件资源

// 下载字体文件到本地
function downloadFile(url, destination) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      const file = fs.createWriteStream(destination)

      res.pipe(file)

      file.on("finish", () => {
        file.close(() => {
          console.log(
            `✅ File ${path.basename(destination)} downloaded successfully`
          )
          localFiles.push(destination)
          resolve()
        })
      })

      file.on("error", (err) => {
        fs.unlink(destination, () => {}) // 删除文件
        reject(
          `❌ Error occurred during ${path.basename(destination)} download: ${err}`
        )
      })
    })
  })
}

// 写入修改后的css文件并上传到服务器
function writeFileAndUpload(data) {
  return new Promise((resolve, reject) => {
    const cssFilePath = path.resolve(
      __dirname,
      `${LOCAL_TEMP_DIRECTORY}${name}.css`
    )
    fs.writeFile(cssFilePath, data, (err) => {
      if (err) {
        reject(`❌ Error occurred during writing CSS file: ${err}`)
      } else {
        console.log(`✅ CSS file written successfully`)
        localFiles.push(cssFilePath)
        resolve()
      }
    })
  })
}

  // 格式化url
  if (!link.startsWith("http:") && !link.startsWith("https:")) {
    link = "https:" + link
  }
  https.get(link, (response) => {
    let data = ""

    response.on("data", (chunk) => {
      data += chunk
    })

    response.on("end", () => {

      // 获取`url(FONT_URL) format(***)`中的FONT_URL
      const urlRegex =
        /url\(['"]?([^'"\)]+)['"]?\) format\(['"]?([^'"\)]+)['"]?\)/g
      let match
      const downloadPromises = [] // 收集需要下载的字体文件
      while ((match = urlRegex.exec(data)) !== null) {
        let url = match[1].replace(/\?t=\d+$/, "") // 去除参数部分
        const extension = path.extname(url)
        const filename = `${globalFilename}${extension}`
        const destination = path.resolve(
          __dirname,
          `${LOCAL_TEMP_DIRECTORY}${filename}`
        )

        // 校验url是否为base64 (彩色icon url为base64)
        if (!url.startsWith("data:")) {
          // 将css文件中原本的url替换为服务器url
          data = data.replace(
            match[1],
            `${DOMAIN}${REMOTE_DIRECTORY}${filename}`
          )
          if (!url.startsWith("http:") && !url.startsWith("https:")) {
            url = "https:" + url
          }
          downloadPromises.push(downloadFile(url, destination))
        }
      }

至此,我们已经将iconfont提供的css文件以及相关的字体文件下载到了LOCAL_TEMP_DIRECTORY中,整个同步工作已经完成了2/3。

三、 基于ssh2-sftp-client实现本地文件上传服务器

在进行最后一步上传服务器之前,我们先介绍一下ssh2-sftp-client,它是基于ssh2开发的Promise风格的文件传输工具集,提供了操作远端服务器文件的大部分方法。

同时,这里插播两条关于FTP与SSH的知识点:

  • FTP(文件传输协议)是用于在计算机网络上的客户端和服务器之间传输计算机文件的标准网络协议。它运行在TCP/IP协议的基础上,并且在更安全的SSH2协议出现之前就已经存在。FTP在两个通信通道中运作:命令通道使用众所周知的端口21,数据通道使用在连接设置期间进行协商的不同端口。FTP并未对数据传输提供加密,这使得它容易受到窃听和其他安全风险的影响,因此出现了更安全的替代方案,比如SSH2。

  • SSH2(安全外壳协议2)是用于在不安全网络上安全运行网络服务的安全网络协议。它提供了安全的加密连接,用于数据传输和远程访问。SSH2使用了强大的加密和认证方法,因此成为了安全的文件传输和远程系统管理的首选。SSH2采用了客户端-服务器模型,所有的流量都会进行加密,包括会话、终端、文件传输和端口转发。SSH2运行在标准的TCP/IP协议之上,为数据传输提供了高级的安全性。

进入实战:

// ftp链接配置
const REMOTE_SERVER_INFO = {
  host: "127.0.0.1",
  port: "1234",
  username: "yourUsername",
  password: "yourPassword"
}
sftp
  .connect(REMOTE_SERVER_INFO) // 创建ftp链接
  .then(() => {
    const uploadPromises = localFiles.map((localFilePath) => {
      const remoteFileName = path.basename(localFilePath)
      return sftp.put(localFilePath, REMOTE_DIRECTORY + remoteFileName) // 上传文件
    })
    return Promise.all(uploadPromises)
  })
  .then(() => {
    console.log("✅ Files uploaded successfully")
    return sftp.end() // 记得关闭链接
  })
  .catch((err) => {
    console.error(err, "❌ Error occurred during upload")
    return sftp.end()
  })

至此,同步iconfont文件的最后一步工作完成,具体交互参考下图:

Node.js开发Iconfont链接同步云服务器的命令行工具

Node.js开发Iconfont链接同步云服务器的命令行工具

完整代码如下:

import fs from "fs"
import https from "https"
import path, { dirname } from "path"
import process from "process"
import readline from "readline"
import { fileURLToPath } from "url"
import Client from "ssh2-sftp-client"

import {
  DOMAIN, // 域名
  LOCAL_TEMP_DIRECTORY, // 本地临时目录
  REMOTE_DIRECTORY, // 上传目录
  REMOTE_SERVER_INFO // ssh配置
} from "../../.config/env.js"

let sftp = new Client()

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
})

const localFiles = [] // 需要上传到服务器的文件资源

// 下载文件到本地
function downloadFile(url, destination) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      const file = fs.createWriteStream(destination)

      res.pipe(file)

      file.on("finish", () => {
        file.close(() => {
          console.log(
            `✅ File ${path.basename(destination)} downloaded successfully`
          )
          localFiles.push(destination)
          resolve()
        })
      })

      file.on("error", (err) => {
        fs.unlink(destination, () => {}) // 删除文件
        reject(
          `❌ Error occurred during ${path.basename(destination)} download: ${err}`
        )
      })
    })
  })
}

// 写入修改后的css文件并上传到服务器
function writeFileAndUpload(data) {
  return new Promise((resolve, reject) => {
    const cssFilePath = path.resolve(
      __dirname,
      `${LOCAL_TEMP_DIRECTORY}${globalFilename}.css`
    )
    fs.writeFile(cssFilePath, data, (err) => {
      if (err) {
        reject(`❌ Error occurred during writing CSS file: ${err}`)
      } else {
        console.log(`✅ CSS file written successfully`)
        localFiles.push(cssFilePath)
        resolve()
      }
    })
  })
    .then(() => {
      return sftp.connect(REMOTE_SERVER_INFO)
    })
    .then(() => {
      const uploadPromises = localFiles.map((localFilePath) => {
        const remoteFileName = path.basename(localFilePath)
        return sftp.put(localFilePath, REMOTE_DIRECTORY + remoteFileName)
      })

      return Promise.all(uploadPromises)
    })
    .then(() => {
      console.log("✅ Files uploaded successfully")
      return sftp.end()
    })
    .catch((err) => {
      console.error(err, "❌ Error occurred during upload")
      return sftp.end()
    })
}
let globalFilename = "iconfont"
rl.question("🔗 请输入下载的iconfont链接: ", (link) => {
  rl.question("📁 请输入保存的文件名: ", (name) => {
    globalFilename = name

    // 格式化url
    if (!link.startsWith("http:") && !link.startsWith("https:")) {
      link = "https:" + link
    }
    https.get(link, (response) => {
      let data = ""

      response.on("data", (chunk) => {
        data += chunk
      })

      response.on("end", () => {
        const urlRegex =
          /url\(['"]?([^'"\)]+)['"]?\) format\(['"]?([^'"\)]+)['"]?\)/g
        let match
        const downloadPromises = []
        while ((match = urlRegex.exec(data)) !== null) {
          let url = match[1].replace(/\?t=\d+$/, "") // 去除参数部分
          const extension = path.extname(url)
          const filename = `${globalFilename}${extension}`
          const destination = path.resolve(
            __dirname,
            `${LOCAL_TEMP_DIRECTORY}${filename}`
          )

          // 校验url是否为base64 (彩色icon url为base64)
          if (!url.startsWith("data:")) {
            data = data.replace(
              match[1],
              `${DOMAIN}${REMOTE_DIRECTORY}${filename}`
            )
            if (!url.startsWith("http:") && !url.startsWith("https:")) {
              url = "https:" + url
            }
            downloadPromises.push(downloadFile(url, destination))
          }
        }

        Promise.all(downloadPromises)
          .then(() => {
            return writeFileAndUpload(data)
          })
          .then(() => {
            rl.close()
          })
          .catch((err) => {
            console.error(err)
            rl.close()
          })
      })
    })
    rl.close()
  })
})

源码地址:github.com/waylonzheng…