Github项目链接:github.com/DoubleD0721…
Background
在之前的需求中接触到了React开发,开始学习后感觉可以实现相关功能的初步交付了,觉得自己可以了。之前使用的json format工具一直充斥着广告,实在受不了决定自己手搓一个json formatter的插件,方便自己后面开发的过程中的便捷。
这篇文档就记录这个插件的实现过程,整个插件的代码在 -> Git仓库,后面在Json Formatter的基础上,也扩展了RGBA和Hex的互转、Url的Encode和Decode功能。
React基础介绍
这里主要是介绍一些react的基础语法和hooks,并使用kotlin语法做对比,方便理解(因为作者本身是Android开发)。如果已经对语法和hooks比较熟悉,可以直接跳转到 Solution 目录下。
基础语法
- 空值合并运算符
?? 是空值合并运算符,当左侧表达式为 null 或 undefined 时,返回右侧表达式的值。
const value1: string | null = null;
const result1 = value1 ?? 'Default Value';
console.log(result1);
const value2: string | null = 'Actual Value';
const result2 = value2 ?? 'Default Value';
console.log(result2);
fun main() {
val value1: String? = null
val result1 = value1 ?: "Default Value"
println(result1) // 输出: Default Value
val value2: String? = "Actual Value"
val result2 = value2 ?: "Default Value"
println(result2) // 输出: Actual Value
}
- 可选链操作符
?. 是可选链操作符,当对象属性可能为 null 或 undefined 时,可安全地访问对象的属性或方法。
interface Person {
address?: {
city?: string;
};
}
const person: Person = {};
const city = person.address?.city;
console.log(city); // 输出: undefined
class Address(var city: String? = null)
class Person(var address: Address? = null)
fun main() {
val person = Person()
val city = person.address?.city
println(city) // 输出: null
}
- 类型判断
类型断言用于告诉编译器某个变量的具体类型,有两种语法形式:尖括号语法和 as 语法。
const value: any = "Hello";
const length1 = (value as string).length;
const length2 = (<string>value).length;
console.log(length1); // 输出: 5
console.log(length2); // 输出: 5
fun main() {
val value: Any = "Hello"
val str1 = value as String
val str2 = value as? String
println(str1.length) // 输出: 5
println(str2?.length) // 输出: 5
}
- 解构赋值
解构赋值可以从数组或对象中提取值并赋给变量。
// 数组解构
const numbers = [1, 2, 3];
const [a, b, c] = numbers;
console.log(a, b, c); // 输出: 1 2 3
// 对象解构
const person = { name: "Alice", age: 30 };
const { name, age } = person;
console.log(name, age); // 输出: Alice 30
data class Person(val name: String, val age: Int)
fun main() {
val person = Person("Alice", 30)
val (name, age) = person
println("$name $age") // 输出: Alice 30
// 数组解构
val numbers = arrayOf(1, 2, 3)
val (a, b, c) = numbers
println("$a $b $c") // 输出: 1 2 3
}
- 匿名函数
const add = (a: number, b: number) => a + b;
console.log(add(1, 2)); // 输出: 3
fun main() {
val add = { a: Int, b: Int -> a + b }
println(add(1, 2)) // 输出: 3
}
常用Hooks函数
useState
用于在函数组件中添加和管理状态。它返回一个数组,第一个元素是当前状态值,第二个元素是用于更新该状态的函数。
const UseStateExample = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button
onClick={() => setCount(count + 1)}
>Increment</button>
</div>
);
};
useContext
用于在函数组件中访问 React 上下文(Context)的值,避免通过props层层传递数据。
const ThemeContext = createContext<string>('light');
const UseContextExample: React.FC = () => {
const { theme } = useContext(ThemeContext);
return (
<div>
<p>Current theme: {theme}</p>
</div>
);
};
useEffect
处理函数组件中的副作用操作,例如数据获取、订阅、手动修改 DOM 等。可以在组件挂载、更新、卸载时执行相应的逻辑。类似swift UI的@State
const UseEffectExample = () => {
const [data, setData] = useState<string | null>(null);
useEffect(() => {
// 组件挂载时发起网络请求
const fetchData = async () => {
try {
const response = await fetch('xxx');
const jsonData = await response.json();
setData(jsonData.title);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
// 组件卸载时执行清理操作
return () => {
console.log('Component unmounted');
};
}, []);
return (
<div>
{data ? <p>Data: {data}</p> : <p>Loading...</p>}
</div>
);
};
useRef
创建一个可变的引用对象,在组件的整个生命周期内保持不变。常用于获取 DOM 元素、存储不需要触发重新渲染的变量。
const UseRefExample = () => {
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
</div>
);
};
useCallback
返回一个记忆化的回调函数,只有当依赖项发生变化时,才会重新创建该函数。常用于优化子组件的性能,避免不必要的重新渲染。
const ChildComponent: React.FC<{ onClick: () => void }> = ({ onClick }) => {
return <button onClick={onClick}>Click me</button>;
};
const UseCallbackExample: React.FC = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<ChildComponent onClick={handleClick} />
</div>
);
};
useMemo
返回一个记忆化的值,只有当依赖项发生变化时,才会重新计算该值。常用于优化性能,避免不必要的计算。
const UseMemoExample = () => {
const [num1, setNum1] = useState(1);
const [num2, setNum2] = useState(2);
// 使用 useMemo 记忆化计算结果
const sum = useMemo(() => {
console.log('Calculating sum...');
return num1 + num2;
}, [num1, num2]);
return (
<div>
<p>Sum: {sum}</p>
<button onClick={() => setNum1(num1 + 1)}>Increment Num1</button>
<button onClick={() => setNum2(num2 + 1)}>Increment Num2</button>
</div>
);
};
Solution
初始化项目
创建项目
在terminal中使用create-react-app来初始化一个项目
npx create-react-app json-formatter
初始化manifest
打开创建的项目,修改manifest中的信息
{
"manifest_version": 3,
"name": "Json Formatter",
"version": "1.0",
"description": "A simple Chrome extension for formatting JSON",
"permissions": ["scripting"],
"action": {
"default_width": 800,
"default_height": 600
},
"background": {
"service_worker": "background.js"
},
"web_accessible_resources": [
{
"resources": ["index.html"],
"matches": ["<all_urls>"]
}
]
}
这里由于我希望点击插件按钮后,可以进入到一个新的页面而不是弹出一个弹窗,所以需要在background.js中实现这个操作
chrome.action.onClicked.addListener((tab) => {
chrome.tabs.create({
// 指向构建后的 index.html
url: chrome.runtime.getURL('index.html')
});
});
运行项目
在terminal中输入npm start来运行项目,成功后会打开对应的前端页面。后面和我们编写Lynx项目一样,修改项目,点击保存,网页会进行热更新,实时看到修改后的样式.
打包和发布
编写完所有的逻辑后,执行npm run build对项目进行打包,打包完成后会发现项目的目录里增加了一个build的文件夹。
通过以下几个步骤在chrome上加载自己的插件:
- 打开 Chrome 浏览器,访问 chrome://extensions。
- 开启右上角的
「开发者模式」。 - 点击
「加载已解压的扩展程序」,选择项目的 build 目录。
实现
创建好项目后,可以通过点击插件按钮打开一个新的网页,对应的网页内容就是App.jsx中的内容,现在开始实现内部的具体功能
初始化
首先需要新建一个组件来作为Json Formatter的功能容器,新建components文件,并在里面新建json_formatter目录并新建一个json_formatter.jsx用来实现能力框架
const JsonFormatter = () => {
const [inputJson, setInputJson] = useState('');
return (
<div>
<h1>Json Formatter</h1>
<div>
<textarea
value={inputJson}
onChange={(e) => setInputJson(e.target.value)}
placeholder="Enter JSON here"
className="json-formatter-textarea"
wrap="off"
/>
<button className="clear-button" onClick={clearInput}>Clear</button>
</div>
);
};
export default JsonFormatter;
然后在App.jsx中加上我们的json组件
import JsonFormatter from './components/json_formatter';
function App() {
const [selectedTab, setSelectedTab] = useState('');
return (
<div className="App">
<JsonFormatter />
</div>
);
}
export default App;
这样,我们就可以在页面上看到一个Json的输入框,样式可以通过新建scss文件进行调整
功能实现
基础能力
首先我们需要划分我们需要的功能都有哪些,按照市面上常用的json formatter工具,重点的功能有以下几个:
- 格式化json
- 压缩json
- 转义json
- 去除转义
每个功能对应着一个button,每个button的点击事件都是对useState定义的inputJson进行修改,下面对这四个功能进行简单的实现。
- 格式化json
const formatJson = () => {
try {
const parsedJson = JSON.parse(inputJson);
const formatted = JSON.stringify(parsedJson, null, 2);
setInputJson(formatted);
} catch (e) {
}
};
- 压缩json
const minifyJson = () => {
try {
const parsedJson = JSON.parse(inputJson);
const minifiedJson = JSON.stringify(parsedJson);
setInputJson(minifiedJson);
} catch (e) {
}
};
- 转义json
const escape = (str) => {
return str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
const escapeJson = () => {
try {
const escapedJson = escape(inputJson);
setInputJson(escapedJson);
} catch (e) {
}
};
- 去除转义
const unescape = (str) => {
return str
.replace(/\\\\/g, '\\')
.replace(/\\"/g, '"')
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t');
}
const unescapeJson = () => {
try {
const unescapedJson = unescape(inputJson);
setInputJson(unescapedJson);
success();
} catch (e) {
fail(e.message || 'Unescape JSON format');
}
}
错误提示
在实现完这些能力后,我们还需要一个可以提示用户错误信息的组件,将error信息展示给用户,可以让用户根据错误调整json格式。
const [error, setError] = useState(null);
const fail = (message) => {
setError(message);
}
const success = () => {
setError(null);
}
return <>
{error && <text className="error-message">{error}</text>}
</>
新定义一个error的state,当点击button后发生错误走入了catch逻辑后,调用fail函数可以为error赋值,在成功后删掉error的信息,这样就可以保证在失败的时候展示一个error信息,在成功后隐藏error信息。
Copy到剪切板
用户在处理好json后,应该需要一个button来copy到剪切板,这样就可以避免用户手动选择全部再点击复制了,首先实现最基础的copy能力。
const copyToClipboard = () => {
if (inputJson) {
navigator.clipboard.writeText(inputJson);
} else {
}
};
一般在用户点击完copy后不知道自己到底有没有copy成功,这样我们需要一个toast来给用户提示,实现的方法也是通过useState来控制toast的展示,然后使用useEffect来实现过x秒后自动隐藏
const [toast, setToast] = useState(null);
useEffect(() => {
if (toast) {
setTimeout(() => {
setToast(null);
}, 3000);
}
}, [toast])
return <>
{toast && <div className="toast">{toast}</div>}
</>
.toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background-color: #333;
color: white;
padding: 10px 20px;
border-radius: 5px 5px 5px 5px;
opacity: 0;
animation: fadeInOut 3s ease-in-out;
z-index: 9999;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateX(-50%) translateY(100%); }
10% { opacity: 1; transform: translateX(-50%) translateY(0); }
90% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(100%); }
}
组装
完成上面的能力实现后,我们可以将各个功能进行组装,完成我们对json format能力的实现。
import { useEffect, useState } from 'react';
import './json_formatter.styles.scss';
const escape = (str) => {
return str
.replace(/\\/g, '\\\\')
.replace(/"/g, '\\"')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
}
const unescape = (str) => {
return str
.replace(/\\\\/g, '\\')
.replace(/\\"/g, '"')
.replace(/\\n/g, '\n')
.replace(/\\r/g, '\r')
.replace(/\\t/g, '\t');
}
const JsonFormatter = () => {
const [inputJson, setInputJson] = useState('');
const [error, setError] = useState(null);
const [toast, setToast] = useState(null);
const success = () => {
setError(null);
}
const fail = (message) => {
setError(message);
}
const copyToClipboard = () => {
if (inputJson) {
navigator.clipboard.writeText(inputJson);
setToast('Copied to clipboard');
} else {
setToast('No content to copy');
}
};
useEffect(() => {
if (toast) {
setTimeout(() => {
setToast(null);
}, 3000);
}
}, [toast])
const formatJson = () => {
try {
const parsedJson = JSON.parse(inputJson);
const formatted = JSON.stringify(parsedJson, null, 2);
setInputJson(formatted);
success();
} catch (e) {
fail(e.message || 'Invalid JSON format');
}
};
const minifyJson = () => {
try {
const parsedJson = JSON.parse(inputJson);
const minifiedJson = JSON.stringify(parsedJson);
setInputJson(minifiedJson);
success();
} catch (e) {
fail(e.message || 'Minify JSON format');
}
};
const escapeJson = () => {
try {
const escapedJson = escape(inputJson);
setInputJson(escapedJson);
success();
} catch (e) {
fail(e.message || 'Escape JSON format');
}
};
const unescapeJson = () => {
try {
const unescapedJson = unescape(inputJson);
setInputJson(unescapedJson);
success();
} catch (e) {
fail(e.message || 'Unescape JSON format');
}
}
return (
<div>
<h1>Json Formatter</h1>
<div className="editor-container">
<textarea
value={inputJson}
onChange={(e) => setInputJson(e.target.value)}
placeholder="Enter JSON here"
className="json-formatter-textarea"
wrap="off"
/>
</div>
{error && <text className="error-message">{error}</text>}
<div className="button-container">
<button onClick={copyToClipboard} className="button">Copy to Clipboard</button>
<button onClick={formatJson} className="button">Format JSON</button>
<button onClick={minifyJson} className="button">Minify JSON</button>
<button onClick={escapeJson} className="button">Escape JSON</button>
<button onClick={unescapeJson} className="button">Unescape JSON</button>
</div>
{toast && <div className="toast">{toast}</div>}
</div>
);
};
export default JsonFormatter;
最终效果
进阶功能
现在展示的只是一个简单的用户输入一大串字符串,点击各个button可以进行json的相关操作,但是还有提升用户体验的方式,比如对json的key和value进行不同颜色的区分。相比于现在的全部以黑色展示,以颜色区分用户能更加的直观获取到json中的各个数据。下面将介绍一些针对这个模块的一些优化体验的能力实现
高亮key/value
首先我们先对json的key和value进行不同颜色的区分,首先我们需要定义各种颜色,包括对value的各种类型进行区分,比如string和boolean类型的颜色进行区分。
首先我们先定义各个颜色的style。
.key {
color: #d73a49;
}
.string {
color: #032f62;
}
.number {
color: #005cc5;
}
.boolean {
color: #6f42c1;
}
.null {
color: #d23632;
}
接着我们给编辑框引入一个可编辑的状态,只有点击编辑按钮才可以开始编辑,当用户点击format按钮后,就进入不可编辑的状态。
新创建一个isEditing的状态,同时新增一个button来进入可编辑状态,在原来的textarea的基础上扩展成只有可编辑的时候才展示textarea,当不可编辑的时候展示pre标签,从而可以展示高亮的json文本。
import { useEffect, useState } from 'react';
import './json_formatter.styles.scss';
const JsonFormatter = () => {
// 已有代码
const [isEditing, setIsEditing] = useState(true);
// 已有代码
return (
<div>
<h1>Json Formatter</h1>
<div className="editor-container">
{isEditing ? (
<textarea
value={inputJson}
onChange={(e) => setInputJson(e.target.value)}
placeholder="Enter JSON here"
className="json-formatter-textarea"
wrap="off"
/>
) : (
<pre
className="json-formatter-pre"
dangerouslySetInnerHTML={{ __html: inputJson }}
/>
)}
</div>
{error && <text className="error-message">{error}</text>}
<div className="button-container">
{ /* Other buttons */ }
<button
onClick={() => {
if (isEditing) {
return;
}
setIsEditing(true)
}}
className={`button ${isEditing ? 'disabled': ''}`}
>Start Editing</button>
</div>
{toast && <div className="toast">{toast}</div>}
</div>
);
};
export default JsonFormatter;
下面开始实现高亮能力,主要是用来匹配各个字符串,然后判断字符串的类型分配对应的主题,然后通过spac标签,将匹配好的主题通过这种方式进行展示。
const highlightJson = (json) => {
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, (match) => {
let cls = 'number';
if (/^"/.test(match)) {
cls = /:$/.test(match) ? 'key' : 'string';
} else if (/true|false/.test(match)) {
cls = 'boolean';
} else if (/null/.test(match)) {
cls = 'null';
}
return `<span class="${cls}">${match}</span>`;
});
};
最终效果:
折叠key/value
如果文件过长,用户可能需要拉好久才能看到自己想要的数据,这样的话我们需要可以折叠key的能力,让用户可以隐藏掉自己不希望看到的数据。
首先,我们需要解析以及format好的json数据,然后通过递归的方式搭建对应的JsonTree,每一个JsonNode都有一个标签用于提示折叠/展开状态,以及内部Node的数量。每个节点都有一个expanded状态,用于记录是否需要被折叠。
import { useState } from 'react';
import './json_formatter.styles.scss';
const JsonTree = ({ data, depth = 0 }) => {
const [expanded, setExpanded] = useState(true);
const isObject = typeof data === 'object' && data !== null;
const isArray = Array.isArray(data);
if (!isObject) {
let valueClass = 'json-value';
if (typeof data === 'number') {
valueClass = 'number';
} else if (typeof data === 'string') {
valueClass = 'string';
} else if (typeof data === 'boolean') {
valueClass = 'boolean';
} else if (data === null) {
valueClass = 'null';
}
return <span className={valueClass}>{JSON.stringify(data)}</span>;
}
const toggleExpand = () => {
setExpanded(!expanded);
};
const keys = Object.keys(data);
if (keys.length === 0) {
return isArray ? <span className="json-value">[]</span> : <span className="json-value">{{}}</span>;
}
return (
<div className="json-node">
<div className="json-header" onClick={toggleExpand}>
{isArray ? '[' : '{'}
<span className="toggle-icon">{expanded ? '▼' : '▶'}</span>
{isArray ? `] (${keys.length})` : `} (${keys.length})`}
</div>
{expanded && (
<div className="json-children" style={{ marginLeft: `${depth * 5}px` }}>
{keys.map((key, index) => (
<div key={index} className="json-item">
<span className="key">"{key}"</span>:{" "}
<JsonTree data={data[key]} depth={depth + 1} />
{index < keys.length - 1 && <span className="json-comma">,</span>}
</div>
))}
</div>
)}
</div>
);
};
export default JsonTree;
接着我们需要在现有的button中新增一个button用来展示这种情况,当用户点击后,可以把格式化好的json编程可折叠的样式进行展示。
const [isViewing, setIsViewing] = useState(false);
let parsedData;
try {
parsedData = JSON.parse(inputJson);
} catch (e) {
parsedData = null;
}
<div className="editor-container">
{isEditing ? (
<textarea
value={inputJson}
onChange={(e) => setInputJson(e.target.value)}
placeholder="Enter JSON here"
className="json-formatter-textarea"
wrap="off"
/>
) : isViewing && parsedData ? (
<div className="json-tree">
<JsonTree data={parsedData} />
</div>
) : (
<pre
className="json-formatter-pre"
dangerouslySetInnerHTML={{ __html: highlightJson(inputJson) }}
/>
)}
<button className="clear-button" onClick={clearInput}>Clear</button>
</div>
最终效果
总结
通过上面的代码,我们实现了一个简单的json format工具,在此基础上,我们可以扩展其他的能力,比如常用到的RGBA和Hex颜色的互转,Url的encode和decode等操作,整个项目的代码在 Git仓库 中,可以参考相关的实现(or 指出我整个实现过程中存在的问题),感谢大家!