Flutter for OpenHarmony Web开发助手App实战:JS格式化
写JavaScript代码的时候,经常会遇到压缩过的或者格式混乱的代码。今天我们来实现一个JS格式化工具,让代码变得整洁易读。

写JavaScript代码的时候,经常会遇到压缩过的或者格式混乱的代码。今天我们来实现一个JS格式化工具,让代码变得整洁易读。
功能需求分析
JS格式化器要解决的核心问题是:把一行或者格式混乱的JavaScript代码,转换成有缩进、有换行的标准格式。
这个功能看起来简单,但要做好不容易。需要考虑:
语法识别:识别出代码中的关键字、括号、分号等。
缩进处理:根据代码层级自动添加缩进。
换行规则:在合适的位置添加换行。
保留注释:格式化时不能丢失注释内容。
完整实现代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
class JsFormatterPage extends StatefulWidget {
const JsFormatterPage({Key? key}) : super(key: key);
State<JsFormatterPage> createState() => _JsFormatterPageState();
}
class _JsFormatterPageState extends State<JsFormatterPage> {
final TextEditingController _inputController = TextEditingController();
final TextEditingController _outputController = TextEditingController();
int _indentSize = 2;
bool _useTabs = false;
void initState() {
super.initState();
_inputController.text = '''function hello(name){if(name){console.log("Hello, "+name);}else{console.log("Hello, World!");}}const arr=[1,2,3,4,5];const result=arr.map(x=>x*2).filter(x=>x>5);''';
}
void dispose() {
_inputController.dispose();
_outputController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('JS格式化'),
actions: [
IconButton(
icon: const Icon(Icons.copy),
onPressed: _copyOutput,
tooltip: '复制结果',
),
IconButton(
icon: const Icon(Icons.swap_vert),
onPressed: _swapInputOutput,
tooltip: '交换输入输出',
),
],
),
body: Column(
children: [
// 工具栏
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
color: Colors.grey[100],
child: Row(
children: [
Text('缩进大小:', style: TextStyle(fontSize: 14.sp)),
SizedBox(width: 8.w),
DropdownButton<int>(
value: _indentSize,
items: [2, 4, 8].map((size) {
return DropdownMenuItem(
value: size,
child: Text('$size空格'),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() => _indentSize = value);
}
},
),
SizedBox(width: 16.w),
Row(
children: [
Checkbox(
value: _useTabs,
onChanged: (value) {
setState(() => _useTabs = value ?? false);
},
),
Text('使用Tab', style: TextStyle(fontSize: 14.sp)),
],
),
const Spacer(),
ElevatedButton.icon(
onPressed: _formatCode,
icon: const Icon(Icons.auto_fix_high),
label: const Text('格式化'),
),
],
),
),
// 输入输出区域
Expanded(
child: Row(
children: [
// 输入区
Expanded(
child: Container(
color: Colors.grey[50],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(12.w),
color: Colors.orange,
child: Row(
children: [
Icon(Icons.input, color: Colors.white, size: 20.sp),
SizedBox(width: 8.w),
Text(
'输入代码',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
IconButton(
icon: const Icon(Icons.clear, color: Colors.white),
onPressed: () => _inputController.clear(),
tooltip: '清空',
),
],
),
),
Expanded(
child: TextField(
controller: _inputController,
maxLines: null,
expands: true,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 13.sp,
),
decoration: InputDecoration(
hintText: '粘贴或输入JavaScript代码...',
border: InputBorder.none,
contentPadding: EdgeInsets.all(16.w),
),
),
),
],
),
),
),
// 分隔线
Container(width: 2.w, color: Colors.grey[300]),
// 输出区
Expanded(
child: Container(
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.all(12.w),
color: Colors.green,
child: Row(
children: [
Icon(Icons.output, color: Colors.white, size: 20.sp),
SizedBox(width: 8.w),
Text(
'格式化结果',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.bold,
),
),
],
),
),
Expanded(
child: TextField(
controller: _outputController,
maxLines: null,
expands: true,
readOnly: true,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 13.sp,
),
decoration: InputDecoration(
hintText: '格式化后的代码将显示在这里...',
border: InputBorder.none,
contentPadding: EdgeInsets.all(16.w),
),
),
),
],
),
),
),
],
),
),
],
),
);
}
void _formatCode() {
final input = _inputController.text.trim();
if (input.isEmpty) {
Get.snackbar('提示', '请输入要格式化的代码',
snackPosition: SnackPosition.BOTTOM);
return;
}
try {
final formatted = _formatJavaScript(input);
_outputController.text = formatted;
} catch (e) {
Get.snackbar('错误', '格式化失败: $e',
snackPosition: SnackPosition.BOTTOM);
}
}
String _formatJavaScript(String code) {
final indent = _useTabs ? '\t' : ' ' * _indentSize;
final buffer = StringBuffer();
int level = 0;
bool inString = false;
String stringChar = '';
bool inComment = false;
bool inLineComment = false;
for (int i = 0; i < code.length; i++) {
final char = code[i];
final nextChar = i < code.length - 1 ? code[i + 1] : '';
// 处理字符串
if ((char == '"' || char == "'") && !inComment && !inLineComment) {
if (!inString) {
inString = true;
stringChar = char;
} else if (char == stringChar && (i == 0 || code[i - 1] != '\\')) {
inString = false;
}
buffer.write(char);
continue;
}
// 处理注释
if (!inString) {
if (char == '/' && nextChar == '/' && !inComment) {
inLineComment = true;
buffer.write(char);
continue;
}
if (char == '/' && nextChar == '*' && !inLineComment) {
inComment = true;
buffer.write(char);
continue;
}
if (char == '*' && nextChar == '/' && inComment) {
inComment = false;
buffer.write(char);
i++;
buffer.write(code[i]);
continue;
}
if (char == '\n' && inLineComment) {
inLineComment = false;
buffer.write(char);
buffer.write(indent * level);
continue;
}
}
if (inString || inComment || inLineComment) {
buffer.write(char);
continue;
}
// 处理大括号和缩进
if (char == '{') {
buffer.write(' {\n');
level++;
buffer.write(indent * level);
} else if (char == '}') {
level = level > 0 ? level - 1 : 0;
buffer.write('\n');
buffer.write(indent * level);
buffer.write('}');
} else if (char == ';') {
buffer.write(';\n');
buffer.write(indent * level);
} else if (char == ',') {
buffer.write(', ');
} else if (char == '\n' || char == '\r') {
// 跳过原有的换行
continue;
} else if (char == ' ' && (nextChar == ' ' || buffer.toString().endsWith(' '))) {
// 跳过多余空格
continue;
} else {
buffer.write(char);
}
}
// 清理多余的空行
return buffer.toString()
.replaceAll(RegExp(r'\n\s*\n\s*\n'), '\n\n')
.trim();
}
void _copyOutput() {
if (_outputController.text.isEmpty) {
Get.snackbar('提示', '没有可复制的内容',
snackPosition: SnackPosition.BOTTOM);
return;
}
Clipboard.setData(ClipboardData(text: _outputController.text));
Get.snackbar('成功', '已复制到剪贴板',
snackPosition: SnackPosition.BOTTOM,
duration: const Duration(seconds: 2));
}
void _swapInputOutput() {
final temp = _inputController.text;
_inputController.text = _outputController.text;
_outputController.text = temp;
}
}
格式化算法核心
格式化的核心是遍历代码字符串,根据不同的字符做不同的处理:
遇到左大括号:添加换行,增加缩进层级。
遇到右大括号:减少缩进层级,添加换行。
遇到分号:添加换行。
遇到逗号:添加空格。
这是最基础的规则,实际实现时还要考虑字符串和注释的特殊情况。
字符串处理
字符串内部的特殊字符不应该被格式化:
bool inString = false;
String stringChar = '';
if ((char == '"' || char == "'") && !inComment) {
if (!inString) {
inString = true;
stringChar = char;
} else if (char == stringChar && code[i - 1] != '\\') {
inString = false;
}
}
用一个标志位记录当前是否在字符串内部。进入字符串时设为true,遇到匹配的引号时设为false。
还要注意转义字符,\"不应该被当作字符串结束。
注释处理
JavaScript有两种注释:单行注释和多行注释。
单行注释:从//开始到行尾。
if (char == '/' && nextChar == '/') {
inLineComment = true;
}
if (char == '\n' && inLineComment) {
inLineComment = false;
}
多行注释:从/*开始到*/结束。
if (char == '/' && nextChar == '*') {
inComment = true;
}
if (char == '*' && nextChar == '/') {
inComment = false;
}
在注释内部的字符都原样输出,不做格式化处理。
缩进管理
使用一个level变量记录当前的缩进层级:
int level = 0;
if (char == '{') {
level++;
buffer.write(indent * level);
}
if (char == '}') {
level = level > 0 ? level - 1 : 0;
}
每遇到一个左大括号,层级加1。遇到右大括号,层级减1。
输出时根据层级添加相应数量的缩进字符。
缩进字符选择
用户可以选择使用空格还是Tab:
final indent = _useTabs ? '\t' : ' ' * _indentSize;
空格的数量也可以自定义,常见的是2个或4个空格。
这个选择很重要,不同的团队有不同的代码规范。
空格和换行优化
格式化后的代码可能有多余的空格和空行:
// 跳过多余空格
if (char == ' ' && (nextChar == ' ' || buffer.toString().endsWith(' '))) {
continue;
}
// 清理多余空行
return buffer.toString()
.replaceAll(RegExp(r'\n\s*\n\s*\n'), '\n\n')
.trim();
连续的空格只保留一个,连续的空行最多保留一个。
工具栏功能
提供了几个实用的选项:
缩进大小选择:2、4、8空格可选。
DropdownButton<int>(
value: _indentSize,
items: [2, 4, 8].map((size) {
return DropdownMenuItem(
value: size,
child: Text('$size空格'),
);
}).toList(),
onChanged: (value) {
setState(() => _indentSize = value!);
},
)
使用Tab选项:勾选后用Tab代替空格。
格式化按钮:触发格式化操作。
输入输出交换
有时候需要对格式化后的代码再次格式化,交换功能很方便:
void _swapInputOutput() {
final temp = _inputController.text;
_inputController.text = _outputController.text;
_outputController.text = temp;
}
一键把输出区的内容移到输入区,省去了复制粘贴的麻烦。
错误处理
格式化过程中可能出错,需要捕获异常:
try {
final formatted = _formatJavaScript(input);
_outputController.text = formatted;
} catch (e) {
Get.snackbar('错误', '格式化失败: $e',
snackPosition: SnackPosition.BOTTOM);
}
如果代码有语法错误,格式化可能失败。给用户一个友好的提示,而不是让应用崩溃。
性能考虑
对于很长的代码,逐字符处理可能会慢。可以考虑:
分块处理:把代码分成多个块,并行处理。
缓存结果:相同的输入不重复格式化。
异步处理:使用compute函数在独立线程处理。
final formatted = await compute(_formatJavaScript, input);
这样UI线程不会被阻塞,界面保持流畅。
功能扩展方向
语法高亮:给格式化后的代码添加颜色。
压缩功能:反向操作,把代码压缩成一行。
语法检查:检测代码中的语法错误。
美化选项:更多的格式化规则,比如运算符两边是否加空格。
支持其他语言:CSS、HTML、JSON等。
实战经验
做这个功能时,最大的挑战是处理各种边界情况。比如字符串里包含引号、注释里包含代码、正则表达式等。
一开始我的算法很简单,只处理了大括号和分号。但测试时发现很多情况下格式化结果不对。
后来逐步完善,加入了字符串识别、注释识别、转义字符处理等。虽然还不能处理所有情况,但已经能应对大部分常见代码了。
小结
JS格式化器通过字符遍历和规则匹配,把混乱的代码整理成规范的格式。核心是正确识别代码结构,合理添加缩进和换行。
虽然我们的实现比较简单,但已经能满足日常使用。如果要做得更完善,可以考虑使用专业的JavaScript解析器,比如Babel的parser。
记住:工具不需要完美,能解决80%的问题就很有价值了。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐
所有评论(0)