You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React JSON密钥校验工具:如何显示缺失密钥的具体文件名?

问题描述

我正在开发一个支持上传JSON文件并校验文件是否缺失特定密钥的React应用。目前应用已接近完善,但无法指明具体哪个文件缺失了密钥,当前提示信息为:

admin-page is missing in 1 file(s) you uploaded

我希望将其改为如下形式:

admin-page is missing in english.json(english.json为文件名)

我的组件代码如下:

import { Box, Button, TextField } from '@mui/material';
import { useState } from 'react';
import './App.css';
import { compareKeys, getKeys } from './service/keysService.ts';

function App() {
  const [files, setFiles] = useState<File[]>([]);
  const [missingKeys, setMissingKeys] = useState<{ [key: string]: number }>({});
  const [fileContents, setFileContents] = useState<string[]>([]);

  const handleFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
    const uploadedFiles = e.target.files as FileList;
    setFiles((prevFiles) => [...prevFiles, ...uploadedFiles]);
    Array.from(uploadedFiles).forEach((file) => {
      const reader = new FileReader();
      reader.onload = () => {
        const content = reader.result?.toString();
        setFileContents((prevContents) => [
          ...prevContents,
          reader.result as string ?? "",
        ]);
      };
      reader.readAsText(file);
    });
  };

  const handleCompareKeys = () => {
    compareKeys(files, setMissingKeys, fileContents);
  };

  return (
    <Box sx={{
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      "flexDirection": "column"
    }}>
      <input type="file" onChange={handleFiles} multiple />
      <Button variant="contained" onClick={handleCompareKeys}>
        Compare Keys
      </Button>
      <Box sx={{
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
      }}>
        {files.map((file, index) => (
          <TextField
            key={index}
            sx={{
              width: "50%",
            }}
            label={file.name}
            value={fileContents[index] || ''}
            multiline
            rows={10}
            margin="normal"
            InputProps={{
              readOnly: true,
            }}
          />
        ))}
      </Box>
      <Box>
        {Object.keys(missingKeys).length > 0 && (
          <div>
            <h2>Missing Keys:</h2>
            <ul>
              {Object.entries(missingKeys)
                .sort(([key1, count1], [key2, count2]) =>
                  key1.localeCompare(key2)
                )
                .map(([key, count]) => (
                  <li key={key}>
                    {key} is missing in {count} file(s) you uploaded
                  </li>
                ))}
            </ul>
          </div>
        )}
      </Box>
    </Box>
  );
} 

export default App;

处理逻辑的服务代码如下:

export const getKeys = (obj: { [x: string]: any }): string[] => {
    const keys = [];
    for (const key of Object.keys(obj)) {
        keys.push(key);
        if (typeof obj[key] === "object") {
            keys.push(
                ...getKeys(obj[key]).map((subkey: any) => `${key}.${subkey}`)
            );
        }
    }
    return keys;
};

export const compareKeys = (
    files: File[],
    setMissingKeys: (missingKeys: { [key: string]: number }) => void,
    fileContents: string[]
) => {
    if (files.length < 2) {
        console.log("Please upload at least two files");
        return;
    }

    const readers = files.map((file) => new FileReader());
    const objects: any[] = [];

    readers.forEach((reader, i) => {
        reader.onload = () => {
            try {
                const data = reader.result;
                let obj = null;
                if (typeof data === "string") {
                    if (data != null && typeof data === "string") {
                        obj = JSON.parse(data);
                    } else {
                        console.log("Invalid file format");
                    }
                }
                objects[i] = obj;
                if (objects.length === files.length) {
                    const allKeys = new Set(
                        objects.map((obj) => getKeys(obj)).flat()
                    );

                    const missingKeysObj: { [key: string]: number } = {};
                    for (const key of allKeys) {
                        let count = 0;
                        for (const keys of objects.map((obj) => getKeys(obj))) {
                            if (!keys.includes(key)) {
                                count++;
                            }
                        }
                        if (count > 0) {
                            missingKeysObj[key] = count;
                        }
                    }
                    setMissingKeys(missingKeysObj);
                }
            } catch (e) {
                console.log(`Error parsing JSON in file ${files[i].name}:`, e);
            }
        };

        reader.readAsText(files[i]);
    });
};

同时附上getKeys函数的测试用例:

import { getKeys, compareKeys } from "./service/keysService.ts";

describe('getKeys', () => {
    it('returns an array of keys from an object', () => {
        const obj = {
            foo: 'bar',
            baz: {
                qux: 'quux',
            },
        };

        const result = getKeys(obj);

        expect(result).toEqual(['foo', 'baz', 'baz.qux']);
    });

    it('handles nested objects correctly', () => {
        const obj = {
            foo: {
                bar: {
                    baz: 'qux',
                },
            },
        };

        const result = getKeys(obj);

        expect(result).toEqual(['foo', 'foo.bar', 'foo.bar.baz']);
    });

    it('returns an empty array for an empty object', () => {
        const obj = {};

        const result = getKeys(obj);

        expect(result).toEqual([]);
    });

    it('handles arrays correctly', () => {
        const obj = {
            foo: ['bar', 'baz'],
        };

        const result = getKeys(obj);

        expect(result).toEqual(['foo', 'foo.0', 'foo.1']);
    });
});

我需要实现捕获具体文件名的功能,同时希望获得提升代码可读性的建议。


解决方案

一、修改状态结构,关联文件名与内容

当前fileContents是单纯的字符串数组,无法对应到具体文件名。我们需要把它改成包含文件名和内容的对象数组:

// App组件中修改状态定义
const [fileRecords, setFileRecords] = useState<{ name: string; content: string }[]>([]);
const [missingKeys, setMissingKeys] = useState<{ [key: string]: string[] }>({}); // 改为存储文件名列表

二、更新文件上传逻辑

修改handleFiles,存储文件名和对应内容:

const handleFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
  const uploadedFiles = e.target.files as FileList;
  Array.from(uploadedFiles).forEach((file) => {
    const reader = new FileReader();
    reader.onload = () => {
      const content = reader.result?.toString() ?? "";
      setFileRecords(prev => [...prev, { name: file.name, content }]);
    };
    reader.readAsText(file);
  });
};

注:这里可以移除原来的files状态,因为fileRecords已经包含了文件名信息。

三、修改compareKeys函数,返回缺失密钥的文件名列表

调整服务端逻辑,让它追踪每个密钥缺失的具体文件:

export const compareKeys = (
    fileRecords: { name: string; content: string }[],
    setMissingKeys: (missingKeys: { [key: string]: string[] }) => void,
) => {
    if (fileRecords.length < 2) {
        console.log("请至少上传两个文件");
        return;
    }

    try {
        // 解析所有JSON文件并关联文件名
        const parsedFiles = fileRecords.map(record => ({
            name: record.name,
            keys: getKeys(JSON.parse(record.content))
        }));

        // 获取所有唯一密钥
        const allKeys = new Set(parsedFiles.flatMap(f => f.keys));

        const missingKeysObj: { [key: string]: string[] } = {};
        for (const key of allKeys) {
            const missingFiles = parsedFiles
                .filter(f => !f.keys.includes(key))
                .map(f => f.name);
            
            if (missingFiles.length > 0) {
                missingKeysObj[key] = missingFiles;
            }
        }

        setMissingKeys(missingKeysObj);
    } catch (e) {
        if (e instanceof Error) {
            console.log("解析JSON时出错:", e.message);
        }
    }
};

四、更新组件渲染逻辑,显示具体文件名

修改App组件中handleCompareKeys和渲染缺失密钥的部分:

const handleCompareKeys = () => {
  compareKeys(fileRecords, setMissingKeys);
};

// 渲染部分替换原有的Missing Keys区块
<Box>
  {Object.keys(missingKeys).length > 0 && (
    <div>
      <h2>缺失密钥:</h2>
      <ul>
        {Object.entries(missingKeys)
          .sort(([key1], [key2]) => key1.localeCompare(key2))
          .map(([key, fileNames]) => (
            <li key={key}>
              {key} 缺失于: {fileNames.join(", ")}
            </li>
          ))}
      </ul>
    </div>
  )}
</Box>

// 更新文件预览部分
<Box sx={{
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  flexWrap: "wrap" // 添加换行,避免文件过多时横向溢出
}}>
  {fileRecords.map((record, index) => (
    <TextField
      key={index}
      sx={{
        width: "45%",
        margin: "0 10px 20px"
      }}
      label={record.name}
      value={record.content}
      multiline
      rows={10}
      margin="normal"
      InputProps={{
        readOnly: true,
      }}
    />
  ))}
</Box>

代码可读性提升建议

  • 明确类型定义:给getKeys的参数添加更精确的类型,比如Record<string, any>,避免使用{ [x: string]: any };为parsedFiles定义接口,比如interface ParsedFile { name: string; keys: string[] }。
  • 拆分复杂函数:把compareKeys中的解析逻辑、密钥对比逻辑拆分成独立函数,比如parseFileRecords、findMissingKeys,提高可维护性。
  • 错误提示UI化:不要只在控制台打印错误,添加一个状态来存储错误信息,在页面上展示给用户。
  • 使用useCallback优化性能:对于handleFiles和handleCompareKeys,使用useCallback包裹,避免每次渲染都创建新函数。
  • 避免重复解析JSON:当前代码在handleFiles和compareKeys中都解析了一次JSON,改为在handleFiles时就解析并存储密钥,减少重复计算。
  • 添加文件去重:上传文件时检查是否已存在相同文件名的文件,避免重复上传。
  • 注释关键逻辑:在getKeys的递归逻辑、compareKeys的对比逻辑处添加注释,说明代码意图。

内容的提问来源于stack exchange,提问作者Navro

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 23:10:02