在这里插入图片描述

写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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐