普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月20日首页

JSON转TypeScript接口核心JS实现

作者 滕青山
2026年2月19日 23:34

JSON转TypeScript接口核心JS实现

这篇只讲核心 JS 逻辑:一段 JSON 是如何一步步变成 TypeScript 接口代码的。

在线工具网址:see-tool.com/json-to-typ…
工具截图:
工具截图.png

1)状态与入口函数

先看最核心的状态和入口:

const jsonInput = ref('')
const outputData = ref('')
const interfaceName = ref('RootObject')
const useType = ref(false)
const optionalProps = ref(true)
const errorMessage = ref('')

const convert = () => {
  const input = jsonInput.value.trim()
  if (!input) {
    outputData.value = ''
    errorMessage.value = ''
    return
  }

  try {
    const parsed = JSON.parse(input)
    if (parsed === null || (typeof parsed !== 'object' && !Array.isArray(parsed))) {
      errorMessage.value = '根节点必须是对象或数组'
      outputData.value = ''
      return
    }
    const rootName = interfaceName.value.trim() || 'RootObject'
    outputData.value = jsonToTypeScript(parsed, rootName)
    errorMessage.value = ''
  } catch (error) {
    errorMessage.value = `JSON 解析失败:${error.message}`
    outputData.value = ''
  }
}

这里做了三件事:输入清洗、JSON 解析、调用生成器。只要这一步跑通,工具就能稳定输出。

2)类型名生成:先合法,再去重

JSON 字段名常常不规范,比如有空格、短横线、数字开头,所以类型名要先标准化:

const toPascalCase = (value, fallback = 'Item') => {
  const cleaned = String(value || '').replace(/[^A-Za-z0-9_$\s]/g, ' ').trim()
  if (!cleaned) return fallback
  const parts = cleaned.split(/\s+/).filter(Boolean)
  const combined = parts.map(part => part.charAt(0).toUpperCase() + part.slice(1)).join('')
  return /^[A-Za-z_$]/.test(combined) ? combined : fallback
}

const normalizeInterfaceName = (value, fallback = 'RootObject') => {
  if (!value) return fallback
  return toPascalCase(value, fallback) || fallback
}

嵌套对象会不断生成新接口名,所以还要处理重名:

const interfaceNames = new Set()
const processedNames = new Set()

const generateInterfaceName = (base, suffix = '') => {
  const baseNormalized = normalizeInterfaceName(base, 'RootObject')
  const suffixNormalized = suffix ? toPascalCase(suffix) : ''
  const name = `${baseNormalized}${suffixNormalized}`

  if (!interfaceNames.has(name) && !processedNames.has(name)) {
    interfaceNames.add(name)
    return name
  }

  let counter = 2
  while (interfaceNames.has(`${name}${counter}`) || processedNames.has(`${name}${counter}`)) {
    counter += 1
  }
  const finalName = `${name}${counter}`
  interfaceNames.add(finalName)
  return finalName
}

3)类型推断:递归处理对象和数组

真正的核心在 inferType

const inferType = (value, key, parentName) => {
  if (value === null) return 'null'

  if (Array.isArray(value)) {
    if (value.length === 0) return 'any[]'
    const elementTypes = new Set(value.map(item => inferType(item, 'Item', parentName)))
    const types = Array.from(elementTypes)
    const inner = types.length === 1 ? types[0] : `(${types.join(' | ')})`
    return `${inner}[]`
  }

  if (typeof value === 'object') {
    const nestedName = generateInterfaceName(parentName, key)
    processObject(value, nestedName)
    return nestedName
  }

  if (typeof value === 'string') return 'string'
  if (typeof value === 'number') return 'number'
  if (typeof value === 'boolean') return 'boolean'
  return 'any'
}

比如 [{ id: 1 }, { id: "2" }] 会得到 (RootItem | RootItem2)[] 或联合类型形式,保证类型信息不丢。

4)对象转声明文本

推断完类型后,要拼成最终代码:

const formatPropertyKey = key => {
  const value = String(key)
  const identifier = /^[A-Za-z_$][A-Za-z0-9_$]*$/
  return identifier.test(value) ? value : JSON.stringify(value)
}

const processObject = (obj, name) => {
  if (processedNames.has(name)) return
  processedNames.add(name)

  const optionalMark = optionalProps.value ? '?' : ''
  const lines = []
  lines.push(useType.value ? `export type ${name} = {` : `export interface ${name} {`)

  Object.entries(obj).forEach(([key, value]) => {
    const tsType = inferType(value, key, name)
    const propertyKey = formatPropertyKey(key)
    lines.push(`  ${propertyKey}${optionalMark}: ${tsType};`)
  })

  lines.push('}')
  interfaces.push(lines.join('\n'))
}

如果根节点本身是数组,再补一行根类型:

if (Array.isArray(json)) {
  const arrayType = inferType(json, 'Item', baseName)
  const rootLine = `export type ${baseName} = ${arrayType};`
  return interfaces.length ? `${interfaces.join('\n\n')}\n\n${rootLine}` : rootLine
}

5)实时转换触发

输入实时更新,但不希望每敲一个字都立即解析,所以用了防抖:

let debounceTimer = null
const scheduleConvert = () => {
  if (debounceTimer) clearTimeout(debounceTimer)
  debounceTimer = setTimeout(() => {
    convert()
  }, 400)
}

watch(jsonInput, scheduleConvert)
watch(interfaceName, scheduleConvert)
watch([useType, optionalProps], convert)

最终效果就是:输入 JSON、改根接口名、切换 interface/type、切换可选属性,结果区都会自动刷新。

6)完整思路总结

这套实现本质是三层:

  1. 输入层:解析 JSON、处理错误
  2. 推断层:递归判断数据类型、生成嵌套接口名
  3. 输出层:拼接 TypeScript 声明文本

把这三层拆开后,代码可读性会很高,读者也能很容易定位:哪里在“解析”、哪里在“推断”、哪里在“输出”。

昨天以前首页

文件16进制查看器核心JS实现

作者 滕青山
2026年2月13日 00:45

文件16进制查看器核心JS实现

本文将介绍基于 Vue 3 和 Nuxt 3 实现的“文件16进制查看器”的核心技术方案。该工具主要用于在浏览器端直接查看任意文件(包括二进制文件)的十六进制编码,所有文件处理均在前端完成,不涉及后端上传。

在线工具网址:see-tool.com/file-hex-vi…
工具截图:
在这里插入图片描述

1. 核心工具函数 (utils/file-hex-viewer.js)

我们将核心的文件处理和格式化逻辑封装在 utils/file-hex-viewer.js 中,主要包括文件大小格式化、二进制转换十六进制字符串以及文件名生成。

1.1 文件大小格式化 (formatFileSize)

用于将字节数转换为人类可读的格式(如 KB, MB)。

export function formatFileSize(bytes, units = ['Bytes', 'KB', 'MB', 'GB', 'TB']) {
  if (!Number.isFinite(bytes) || bytes < 0) return `0 ${units[0] || 'Bytes'}`
  if (bytes === 0) return `0 ${units[0] || 'Bytes'}`

  const k = 1024
  const index = Math.floor(Math.log(bytes) / Math.log(k))
  const value = Math.round((bytes / Math.pow(k, index)) * 100) / 100
  const unit = units[index] || units[units.length - 1] || 'Bytes'
  return `${value} ${unit}`
}

1.2 二进制转十六进制 (bytesToHex)

这是本工具的核心转换函数。它接收一个 Uint8Array,并根据传入的 format 参数(支持 spacenospaceuppercase)生成对应的十六进制字符串。对于 space 格式,每16个字节会自动换行,方便阅读。

export function bytesToHex(uint8Array, format = 'space') {
  if (!uint8Array || !uint8Array.length) return ''
  const useUppercase = format === 'uppercase'
  const useSpace = format === 'space'
  let hexString = ''

  for (let i = 0; i < uint8Array.length; i++) {
    // 将每个字节转换为2位十六进制字符串
    let hex = uint8Array[i].toString(16).padStart(2, '0')
    
    if (useUppercase) {
      hex = hex.toUpperCase()
    }
    
    if (useSpace) {
      hexString += `${hex} `
      // 每16个字节插入一个换行符
      if ((i + 1) % 16 === 0) {
        hexString += '\n'
      }
    } else {
      hexString += hex
    }
  }

  return hexString.trim()
}

1.3 导出文件名生成 (buildHexFileName)

根据原文件名和当前的格式设置,生成导出文件的名称(后缀为 .hex.HEX)。

export function buildHexFileName(originalName, format = 'space') {
  if (!originalName) return `file${format === 'uppercase' ? '.HEX' : '.hex'}`
  const lastDot = originalName.lastIndexOf('.')
  const baseName = lastDot > 0 ? originalName.slice(0, lastDot) : originalName
  const extension = format === 'uppercase' ? '.HEX' : '.hex'
  return `${baseName}${extension}`
}

2. 文件读取与处理逻辑

在前端实现十六进制查看器的核心是利用 HTML5 的 FileReader API 读取文件内容为 ArrayBuffer,然后转换为 Uint8Array 进行处理。

const processFile = (file) => {
  const reader = new FileReader()
  
  reader.onload = (event) => {
    try {
      const buffer = event.target.result
      const bytes = new Uint8Array(buffer)
      // 调用工具函数生成 Hex 字符串
      const hex = bytesToHex(bytes, 'space') 
      // 更新视图...
    } catch (error) {
      console.error('Process failed:', error)
    }
  }
  
  reader.onerror = () => {
    console.error('Read error')
  }
  
  // 读取文件为 ArrayBuffer
  reader.readAsArrayBuffer(file)
}

3. 导出与下载功能

为了让用户可以将十六进制编码保存到本地,我们利用 Blob 对象和 URL.createObjectURL 创建临时的下载链接,实现纯前端下载。

const downloadHexFile = (hexContent, originalName, format) => {
  if (!hexContent) return

  const fileName = buildHexFileName(originalName, format)
  // 创建包含 Hex 内容的 Blob
  const blob = new Blob([hexContent], { type: 'text/plain' })
  const url = URL.createObjectURL(blob)
  
  // 创建临时链接并触发下载
  const link = document.createElement('a')
  link.href = url
  link.download = fileName
  document.body.appendChild(link)
  link.click()
  
  // 清理
  document.body.removeChild(link)
  URL.revokeObjectURL(url)
}

总结

该方案的核心在于通过 utils/file-hex-viewer.js 封装纯粹的格式化和转换逻辑,并结合浏览器原生的 FileReaderBlob API 完成文件的读取与导出,实现了一个轻量级且高效的纯前端文件十六进制查看工具。

❌
❌