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
相关产品推荐
相关产品推荐

