目标驱动 - 逐步逐步做成这个 TUI 程序 起因 前几天逛 0xFFFF 社区，看到一个帖子 - 为什么我折腾这些小众技术？，主要有个回复让我脑子有点想法 -- 「 现在 GitHub 上出现了这么多活跃的 TUI 项目（支持显示全彩全分辨率图片甚至播放视频的 Terminal、IDE、漂亮的 TUI 文件管理器、音乐播放器等等），反倒是计算机届的文艺复兴了。 」 熟悉我开发习惯的就知道，我挺喜欢在终端 Terminal 下完成一些工作，就心想要不我也开发个小小 TUI 程序用用。 写该文的时候，Vim 的创作者 Brarm Moolenaar 去世了，这里 RIP 下，毕竟自大二起就使用着 Vim，这个东西对我开发真是非常便捷，感谢 Brarm Moolenaar 的贡献。 就想着可以做个简单天气查询的 TUI 程序。 因为我目前办公的位置离窗边蛮远，且现在天气热，太阳晒，全都拉上窗帘，我就看不见窗外啥天气了。 某日中午出去吃饭，居然下太阳雨，直接淋成「 落汤鸡 」。 同样的悲催事可不能再遭遇一次，现在离开座位，要出办公楼前，必看看天气预报，决定是否带个雨伞。 一般我看天气的步骤如下： 拿起手机解锁 滑到最左边的天气界面 那我想更懒惰点，反正每一天下来，我对着电脑比对着手机更多，且我每台电脑的终端我都不会关掉的，常驻后台。 不如做个 TUI 程序，这样我想查看天气的时候，步骤就变成了： 快捷键唤起终端 输入命令，按下回车，就可看到天气信息 且这样我每天就可以少拿起手机了。因为怎么说，我有时拿起手机看个天气，有可能不知不觉的刷了「 亿 」会儿手机，那放下手机，回到电脑前继续做些事情，脑子又可能会有点小卡壳，不知道自己刚才在干嘛（如果在敲代码，又可能会忘了某一个逻辑是要怎么做的，要花些时间回忆下）。 不过其实这影响也不大，只是我想着如果把工作任务早点做完，那我摸鱼就摸的更心安理得，好像更不错耶。（￣︶￣）↗ OK 清楚目标了，那就开始吧! 思考下吧！ 理解业务 这个东西的展现形态为一个 Terminal 下即可运行的程序。 运行这个程序，会显示我所在地的天气情况。 问题来了 - 我想要看到什么样的天气信息呢？ 明确下要展示的天气信息： 目前天气情况，即运行该程序那时刻的天气是咋样的，我要知道 未来天气情况，即未来几个小时或几天内的天气情况，我想知道 确定所用技术栈 我作为一个开发者，我掌握着什么样的技术栈，做这个东西用什么技术比较合适，这些要确定下来。 如果是在 Terminal 下运行的程序，那用很多语言都可以完成的，只不过我最近常用一门叫 Rust 的语言。 下面是我问 ChatGPT，用 Rust 做命令行程序的优势是什么，它给出的回答。见仁见智啦！ Rust 作为一种现代的系统级编程语言，拥有许多优势，使其成为开发命令行程序的理想选择。以下是 Rust 在开发命令行程序方面的一些优势： 1. 性能：Rust 是一门性能优秀的语言，它的执行速度接近于 C 和 C++，这使得命令行程序在处理大规模数据和复杂计算时表现出色。 2. 安全性：Rust 的所有权和借用系统能够在编译时检查内存安全性和数据竞争，有效地避免了许多常见的运行时错误和安全漏洞，使得命令行程序更加健壮和安全。 3. 并发支持：Rust 内置了对并发编程的支持，可以轻松地编写多线程的命令行程序，充分利用现代多核处理器的性能优势。 4. 轻量级：Rust 的标准库非常精简，这使得生成的可执行文件相对较小，节省了系统资源，并且适合用于开发轻量级的命令行工具。 5. 跨平台支持：Rust 提供了广泛的平台支持，可以轻松地在不同操作系统上运行编译好的命令行程序，这为开发跨平台工具提供了便利。 6. 依赖管理：Rust 的包管理器 Cargo 提供了简单易用的依赖管理功能，可以方便地引入外部库和工具，加速开发过程。 7. 社区和生态系统：Rust 社区非常活跃，并且拥有丰富的生态系统，你可以轻松地找到许多现成的库和工具，以及丰富的文档和教程，帮助你更高效地开发命令行程序。 综上所述，Rust 的性能、安全性、并发支持和跨平台能力使其成为开发命令行程序的优秀选择。无论是开发简单的工具还是复杂的命令行应用，Rust 都能够提供高效、安全和可靠的解决方案。 接下来就是关键的环节。 把业务需求翻译成技术需求 开发前准备相关资源 我这里的数据是有关天气的，那我就需要一个天气数据源。 这个网上选择很多，我这里用的是心知天气，调用它提供的 API 返回的数据格式为 JSON 类型，便于处理。 梳理出技术需求吧！ 调用心知天气的 API，获取天气数据；Rust 有提供 reqwest 库，可以用来发起网络请求； 能处理返回来的 JSON 数据，Rust 有提供 serde 库，可以用来处理 JSON 数据； 制作成一个命令行程序，要考虑命令行程序的一些特点，比如：命令行参数的处理，以及提供 --help 的选项让使用者清楚该程序的使用方法； 动起手来吧！ 指挥下 ChatGPT 帮我写些代码 (〃∀〃) 我好懒～～～ do { 把我的技术需求输入给 ChatGPT; 它负责具体实现; 我 Code Review 它给我展示的代码; if (我满意了) { break; } } while(我不满意) 最喜欢 Code Review 这件事情。现在平日开发，自己提升最多的一个环节就是在 Code Review 这里。 最终呈现出来的代码就是这个仓库 - weather_cli 代码很简单的，非常直白。 End 文章字数不多，就是叙述我完成这个目标（做一个天气查询的 TUI 程序）的一个过程。我给自己制定的目标去驱动我去完成这个事情，这个过程中，我也算学到了一些新知识（和 ChatGPT 对话学到的）。 再说说我现在开发的一些感受，软件开发需要你是怎么样的? 假如有个任务给你了，你应该： 理解业务，这件事是什么？ 沟通上下游，现在基本都会有的。目前这时代绝非一个靠单打独斗就能做成一些事情的时代啦。 做成这件事，是不是会要对某些编程语言和技术栈熟悉，要清楚是什么，如果有不会的，那就去学得了。要相信自己的学习能力。 在理解业务的前提下，去把业务需求翻译成技术需求，最好梳理出的技术需求是层层递进的，这样自己慢慢实现了一个个技术需求，就能得到一个反馈，清楚自己离那个最终待解决的目标还有多远。 有一点点的算法能力，有些问题的解决额外需要数学知识。本科生推荐必学「 高等数学 && 线性代数 && 数论 && 初高中三角函数知识也可别忘了（如果做 Qt 绘图会用到）」。 注意我这里说的是「 必 」哦，我个人啊，我个人啊，就建议是必学。疑惑的话，我只能说现实就是残酷呀，目前感觉绝大多数程序员的差异化就在这些部份上，不认同的，也请当我乱说。毕竟就我来说啊，数学这部分外的一些什么新框架，什么新技术啦，我都是上 YouTube 上看印度老哥讲解学会的（dddd），或者直接看官方技术文档就行啦。 多喝水！心态好！坐久了就站起来动动！尽可能挖掘出做这件事的乐趣。很难说，就一种开发的感觉，进入「 心流状态 」，这说法合适吗？呀，真不好描述。文笔烂。 总之，Have Fun！ Just For Fun！
写于 2023 年 3 月 24 日。 以剔除 Qt 的 QThread 为目标而诞生的项目。设计的线程基类能搭配类似 Qt 中的信号与槽机制来使用，且利用现代 C++ 提供的互斥量和条件变量，让函数能够同步或异步运行，这样派生类（某业务工作线程）能持有定时器（线程）来做些定时任务（异步）。 cxx_thread 起因 和 记一次「 包轮子 」的经历 的起因一样，只不过这个项目是为了剔除 Qt 的 QThread 而诞生的。 拆分目标 目标 -> 替换掉 QThread 分析下 QThread 这里主要看 QThread 的使用方式，看它如何被使用的。 第一种示例代码 class Worker : public QObject { Q_OBJECT public slots: void doWork(const QString &parameter) { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); } signals: void resultReady(const QString &result); }; class Controller : public QObject { Q_OBJECT QThread workerThread; public: Controller() { Worker *worker = new Worker; worker->moveToThread(&workerThread); connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater); connect(this, &Controller::operate, worker, &Worker::doWork); connect(worker, &Worker::resultReady, this, &Controller::handleResults); workerThread.start(); } ~Controller() { workerThread.quit(); workerThread.wait(); } public slots: void handleResults(const QString &); signals: void operate(const QString &); }; 第一种是在一个继承自 QObject 的子类中，将 QThread 作为成员变量 workerThread，用 moveToThread() 函数将该类所创建的对象移动到 workerThread 中，connect() 函数将信号与槽连接起来，在槽函数中编写需要在线程中处理的逻辑代码。然后再调用 QThread 的 start() 函数来启动 workerThread，这样槽函数就能在一个新的线程中运行。 第二种示例代码 class WorkerThread : public QThread { Q_OBJECT void run() override { QString result; /* ... here is the expensive or blocking operation ... */ emit resultReady(result); } signals: void resultReady(const QString &s); }; void MyObject::startWorkInAThread() { WorkerThread *workerThread = new WorkerThread(this); connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults); connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater); workerThread->start(); } 第二种是写子类继承自 QThread，然后重写 run() 函数，这样就可以在 run() 函数中写线程的逻辑。 需要剔除 QThread 的代码中使用方式为第一种。 Signals and Slots Across Threads Qt 有提供一些信号与槽的连接选项 Qt::ConnectionType，这些选项能控制信号与槽的连接方式，比如是否是异步的，是否是阻塞的等等。 简单梳理需求 信号与槽机制以及管理信号的消息队列（要有生产者和消费者） 槽函数 Async 异步执行或 Sync 同步执行 线程能够持续运行，直至被销毁 实现需求 搜索资料并理解学习 抱着网上可能已经有现成解决方案的心态去互联网上搜索 /doge sigslot -- 提供了如何处理参数传递的思路 std::static_pointer_cast -- 转换类型的方法 Concurrency support library / CPP-Concurrency-In-Action-2ed-2019 -- 有关 C++ 并行（多线程）开发的知识，这里主要用到 std::thread、std::mutex 和 std::condition_variable 。 信号与槽机制 看看需要剔除 QThread 的那份代码，业务线程自创建后便持续存在，等待着主线程发送一个指令（即调用业务线程的公有成员函数），当业务线程接受到主线程某一个指令后，业务线程能根据该指令（被调用的公有函数内部再调用 emit 发送信号函数）去处理相对应的业务逻辑（调用对应的槽函数）。 这样梳理下，即需要有一张关系映射表去将主线程要调用的指令函数和对应的业务逻辑关联起来，即业务线程内部有一张将信号与槽函数关联起来的表。 那时立马想到的是维护一个 std::map 数据结构，即 std::map<Signal, Slot>，其中 Signal 为整型，Slot 为对应的槽函数，这样就能将一个信号与一个槽函数对应起来，但是需要考虑到函数参数传递的问题，以及可能一个信号会对应到多个槽函数的情况，会要把 Slot 设置成一个容器去包含多个槽函数，又要考虑到函数执行顺序的问题，这样就会变得很复杂。 那就换个思路，既然是要将信号与槽函数关联起来，先得有信号 ID 和槽函数。 那么继承基类 ThreadBase 而派生的业务线程类中必须要有定义信号和槽函数。 这里用 enum 枚举类型定义 Signal 信号，一般如下： enum Signal : int { SayHello_Signal, SayGoodBye_Signal, WillDo_Signal, PlanToDo_Signal, AskAQuestion_Signal, GetAQuestion_Signal, AnswerAQuestion_Signal, GetAAnswer_Signal, WantToSleep_Signal, DestroyTimer_Signal }; 设计一个结构体来存储信号以及需要携带的参数数据，命名为 SignalMsg ，如下： struct SignalMsg { public: SignalMsg(int signal, std::shared_ptr<void> msg) : _signal(signal), _msg(msg) {} int GetSignal() const { return _signal; } std::shared_ptr<void> GetMsg() const { return _msg; } private: int _signal = -1; // -1: destroy thread std::shared_ptr<void> _msg; // data required by the slot function }; 在派生类中根据信号与其对应的槽函数所需参数来编写些公有成员函数，让主线程能够通过调用这些公有成员函数来发送消息（信号与参数数据），示例代码如下： void Human::SendWillDoSignal(const std::string& doWhat) { std::shared_ptr<std::string> msgData = std::make_shared<std::string>(doWhat); std::shared_ptr<SignalMsg> signalMsg = std::make_shared<SignalMsg>( WillDo_Signal, std::static_pointer_cast<void>(msgData)); SendSlotFuncSyncRunMsg(std::move(signalMsg)); } void Human::SendPlanToDoSignal(const Plan& plan) { std::shared_ptr<Plan> msgData = std::make_shared<Plan>(plan); std::shared_ptr<SignalMsg> signalMsg = std::make_shared<SignalMsg>( PlanToDo_Signal, std::static_pointer_cast<void>(msgData)); SendSlotFuncSyncRunMsg(std::move(signalMsg)); } 这里使用 std::static_pointer_cast 将其它类型的智能指针转换成 std::shared_ptr<void> 类型的智能指针，便于后续的参数传递。 而槽函数的就一般会在命名后面加上个 Slot 后缀以区别其它函数，如 SayHelloSlot() 。 在一个名为 UserCustomFunction 函数使用 switch case 判断语句和 enum 枚举类型将信号 Signal 与 Slot 槽函数对应起来。 UserCustomFunction 函数为 ThreadBase 基类的纯虚函数，需要在派生类中实现。 示例代码如下： // Human.h virtual void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) override; // Human.cpp void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) { switch (signalMsg->GetSignal()) { case SayHello_Signal: { SayHelloSlot(); break; } case SayGoodBye_Signal: { SayGoodByeSlot(); break; } case WillDo_Signal: { std::string doWhat = *(std::static_pointer_cast<std::string>(signalMsg->GetMsg())); WillDoSlot(doWhat); break; } case PlanToDo_Signal: { auto plan = *(std::static_pointer_cast<Plan>(signalMsg->GetMsg())); PlanToDoSlot(plan); break; } ...... ...... ...... } } 该函数接受一个 SignalMsg 类型的参数，即信号以及携带的参数数据。根据 Siganl 信号的值，去调用对应的槽函数。 这里使用了 std::static_pointer_cast 函数将 std::shared_ptr<void> 类型的参数数据转换成对应的类型，再将参数数据传递给槽函数去处理。 而 UserCustomFunction 函数的调用则是在 ThreadBase 基类的 Process 函数中，如下： void ThreadBase::Process() { std::shared_ptr<SignalMsg> signalMsg; while (1) { { // get signal msg } if (signalMsg->GetSignal() == DestroyThread_Signal) break; UserCustomFunction(signalMsg); } } Process 函数为一个 while 循环，用 std::thread 类型创建一个线程时绑定该 Process 函数，这样该业务线程就能持续运行，就解决掉上述需求中的第三点。 OK，这样生产者和消费者就有了。 生产者就是主线程，通过调用业务线程提供的公有成员函数来发送信号与参数数据。 消费者就是业务线程，在 Process 函数获取到消息，传递给 UserCustomFunction 函数，再由 UserCustomFunction 函数调用对应的槽函数。 这样就实现了主线程与业务线程的通信。 之后就需要有一个消息队列维护这些消息，这里使用 std::queue 类型来存储消息 std::shared_ptr<SignalMsg> 。 通用的变量和函数都放在 ThreadBase 基类中，如下： class ThreadBase { ..... ..... ..... protected: /// Build the relationship between the signal and the slot function /// @param[in] signalMsg - message (signal, data required for slot function) virtual void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) = 0; private: /// Process the message queue void Process(); private: const int DestroyThread_Signal = -1; private: std::unique_ptr<std::thread> _thread; std::queue<std::shared_ptr<SignalMsg>> _signalMsgQueue; }; Process 函数也演变成如下： void ThreadBase::Process() { std::shared_ptr<SignalMsg> signalMsg; while (1) { { if (_signalMsgQueue.empty()) continue; signalMsg = std::move(_signalMsgQueue.front()); _signalMsgQueue.pop(); } if (signalMsg->GetSignal() == DestroyThread_Signal) break; UserCustomFunction(signalMsg); } } 槽函数异步执行或同步执行 业务线程可以持有定时器（线程）去做定时任务（另一些依附该业务而做的事情）。定时器被业务线程所持有，其创建与销毁都要通过调用业务线程提供的 CreateTimer 和 DestroyTimer 。且定时任务不阻塞主线程，不使用 std::thread 中提供的 detach 方法的情况下，定时任务应异步执行。 同时，有时在主线程给业务线程发送一个指令后，须确保该指令对应的业务逻辑被执行后，主线程才可继续执行。 即须确保包含业务逻辑的槽函数是能同步执行的。 使用 C++ 提供的 std::mutex 互斥量和 std::condition_variable 条件变量来实现同步。 SiganlMsg 结构体中增加一个 bool 类型的 _wait 成员变量，用来标识该消息是否需要同步执行，即是否需要等待槽函数执行完毕后再继续执行。 反之，如果 _wait 为 false ，则表示该消息不需要同步执行，即不需要等待槽函数执行完毕后再继续执行，即槽函数异步执行。 更新后头文件代码如下： struct SignalMsg { public: SignalMsg(int signal, std::shared_ptr<void> msg) : _wait(false), _signal(signal), _msg(msg) {} bool GetWait() const { return _wait; } void SetWait(bool wait) { _wait = wait; } int GetSignal() const { return _signal; } std::shared_ptr<void> GetMsg() const { return _msg; } private: bool _wait = false; // async: false, sync: true (default: false) int _signal = -1; // -1: destroy thread std::shared_ptr<void> _msg; // data required by the slot function }; class ThreadBase { ...... ...... ...... public: ...... /// Send a message to the message queue (async) /// @param[in] signalMsg - message (signal, data required for slot function) void SendSlotFuncAsyncRunMsg(std::shared_ptr<SignalMsg> signalMsg); /// Send a message to the message queue (sync) /// @param[in] signalMsg - message (signal, data required for slot function) void SendSlotFuncSyncRunMsg(std::shared_ptr<SignalMsg> signalMsg); protected: /// Build the relationship between the signal and the slot function /// @param[in] signalMsg - message (signal, data required for slot function) virtual void UserCustomFunction(std::shared_ptr<SignalMsg> signalMsg) = 0; private: /// Send a message to the thread queue (async or sync) /// @param[in] wait - async: false, sync: true /// @param[in] signalMsg - message (signal, data required for slot function) void SendMsg(bool wait, std::shared_ptr<SignalMsg> signalMsg); /// Process the message queue void Process(); private: const int DestroyThread_Signal = -1; private: std::unique_ptr<std::thread> _thread; std::mutex _mutex; std::condition_variable _cv; std::queue<std::shared_ptr<SignalMsg>> _signalMsgQueue; bool _syncProcessed = false; }; 对应的 CPP 文件代码如下： void ThreadBase::SendSlotFuncAsyncRunMsg(std::shared_ptr<SignalMsg> signalMsg) { SendMsg(false, std::move(signalMsg)); } void ThreadBase::SendSlotFuncSyncRunMsg(std::shared_ptr<SignalMsg> signalMsg) { SendMsg(true, std::move(signalMsg)); std::unique_lock<std::mutex> lock(_mutex); try { _cv.wait(lock, [this] { return _syncProcessed; }); } catch (...) { // Ensure _syncProcessed is set to true even if an exception is thrown } } void ThreadBase::SendMsg(bool wait, std::shared_ptr<SignalMsg> signalMsg) { if (!_thread) return; signalMsg->SetWait(wait); // Add the message to the queue std::unique_lock<std::mutex> lock(_mutex); _signalMsgQueue.emplace(std::move(signalMsg)); _cv.notify_one(); if (wait) { _syncProcessed = false; // Wait for the message to be processed by the worker thread synchronously _cv.wait(lock, [this] { return _syncProcessed; }); } } void ThreadBase::Process() { std::shared_ptr<SignalMsg> signalMsg; while (1) { { // Wait for a message to be added to the queue std::unique_lock<std::mutex> lock(_mutex); _cv.wait(lock, [this] { return !_signalMsgQueue.empty(); }); if (_signalMsgQueue.empty()) continue; signalMsg = std::move(_signalMsgQueue.front()); _signalMsgQueue.pop(); } if (signalMsg->GetSignal() == DestroyThread_Signal) break; UserCustomFunction(signalMsg); if (signalMsg->GetWait()) { std::lock_guard<std::mutex> lock(_mutex); _syncProcessed = true; _cv.notify_one(); } } } ThreadBase 基类的公有成员函数 ThreadBase::ThreadBase() : _thread(nullptr) {} ThreadBase::~ThreadBase() { DestroyThread(); } bool ThreadBase::CreateThread() { if (!_thread) { _thread = std::make_unique<std::thread>(&ThreadBase::Process, this); } return true; } void ThreadBase::DestroyThread() { if (!_thread) return; // Send a message to the thread queue to destroy the thread { std::lock_guard<std::mutex> lock(_mutex); _signalMsgQueue.emplace(std::make_shared<SignalMsg>( DestroyThread_Signal, std::shared_ptr<void>(nullptr))); _cv.notify_one(); } // Wait for the thread to be destroyed _thread->join(); _thread = nullptr; } std::thread::id ThreadBase::GetThreadId() { return _thread ? _thread->get_id() : std::thread::id(); } std::thread::id ThreadBase::GetCurrentThreadId() { return std::this_thread::get_id(); } CreateThread 在派生类的构造函数中被调用，用来创建线程。 当派生类创建的对象被销毁时，会调用基类的析构函数，从而调用 DestroyThread 来销毁线程。DestroyThread 会向消息队列中发送一个销毁线程的消息，中止 Process 函数的执行，避免线程被销毁后继续执行，从而导致程序崩溃。同时，DestroyThread 会调用 std::thread::join 函数，等待线程执行完毕，再销毁线程。 提供了 GetThreadId 和 GetCurrentThreadId 两个函数，用来获取线程 ID 或当前线程 ID。 std::thread 是 C++11 中引入的一个多线程库，用于创建和管理线程。在使用 std::thread 时，可以使用 get_id() 函数获取线程的 ID 值。 线程 ID 值是一个唯一的标识符，用于识别不同的线程。在多线程程序中，可以使用线程 ID 值来跟踪和调试线程的执行过程。例如，可以使用线程 ID 值来输出线程的执行顺序，或者在发生死锁或其他错误时跟踪线程的状态。 End 其实这类 ThreadBase 的实现网上一大把，但其实都是根据自己需要解决的业务场景而去设计的，没有一个是万能的 /doge，还是需要根据自己的需求来设计。 自己这样造一个轮子，其实感觉思路并不难。前期就能快速有个雏形。 我用 GoogleTest 做单元测试，TDD 开发，写测试，只有测试是通过的才能说明编写的代码能达到我的设计期望。 有了雏形，之后就是不断重构，改命名，改写法，改数据结构等等，中间花费了大量时间去修改，试错。其间过程很混乱，好在有 GoogleTest 能够快速的让我知道我改的对不对，TDD 很方便。 总的来说，重构的过程还是很有意思的，也是学习的过程。 对于 C++11 提供的 Concurrency support library 以及类型转换 cast 这两个知识点也是熟练了些。
写于 2023 年 3 月 17 日。 重新拾起 C++ 开发已经有 8 个月了，自己主导做的一件事情。 三个看法，一是记住 C++ 能不造轮子，就别造轮子了；二是能做单元测试的东西，就要做单元测试；三是好好利用 ChatGPT 之类的东西来优化代码 -/- doge cxx_crud_file 起因 被分配了一个任务，用现今的 C++ 标准改写一个原先用 Qt 编写的代码。就是要去除掉那些以 Q 开头的代码（QString、QVariant、QSetting 和 QFile 等等）。 重新用 C++ 标准改写的这份代码，扩展性就会更强，当然 Qt 的生态也不错，不过是业务需求如此罢了，不展开具体内容。 开始「 模仿 」 这份 Qt 编写的代码中有自己的文件读写工具类 FileTools（是根据实际业务产生的，即各种规定好的读写要求，怎么读取，怎么写入数据等等）。 用 Qt 构建的 FileTools 类， 其代码长度大概为两千多行左右 只不过这个类是基于 Qt 整个语言生态构造的，现在打算要去除 Qt 罢了。 我一开始，就是简单仿造这个 FileTools 类，函数名大致不变，函数内部的处理逻辑也大致不变，入参和返回值的类型由 Qt 的数据结构改成 C++ 标准下的，例如 QString 用 std::string 代替。 但其实，在修改这个 FileTools 类时，发现这其实算不上是个类，只是把功能函数全当作成员函数罢了，要使用这些函数时，就用这个类创建一个对象，通过这个对象来调用所需的功能函数。 大概是刚从前端转到做 C++ 的开发，我一开始也觉得没什么问题，就是 函数式编程 嘛。 但是 C++ 的一大精髓就是 OOP（面向对象开发），这个类弄得更像面向过程的。 且这个类里有蛮多方法其实可以继承复用的，阅读其中的代码，有点乱。 越开发，越能闻到代码传出的那股 「 坏味道 」。 该换思路来做。 开始「 包轮子 」 包包包，有现成的，优秀的，经过试验和测试的功能函数，我是绝对不会重复造轮子的，我是个很懒的开发者 /doge。 测试驱动开发 Test-Driven Development (TDD) VSCode | CMake | C++ | TDD 整个项目使用 GoogleTest 来做单元测试，开发过程中，先编写测试用例，然后再编写代码，最后再运行测试用例，看看是否通过，不通过，则修改代码，直到通过为止。 记住，测试代码应当与你功能代码的「 体量 」差不多大。 UFile 重新拾起 C++ 开发，看了蛮多 C++ 的书籍（《clean C++》、《Effective C++》等），以及看 cppreference 官网内容，感觉要做的文件读写工作。一些功能函数可以基于 C++ 标准提供的 <filesystem> 文件系统库来做。 UFile #ifndef UFILE_HPP_ #define UFILE_HPP_ #include <iostream> #include <string> #include "Tools.hpp" #if defined(__clang__) || defined(__GNUC__) #define CPP_STANDARD __cplusplus #elif defined(_MSC_VER) #define CPP_STANDARD _MSVC_LANG #endif #if CPP_STANDARD >= 201103L && CPP_STANDARD < 201703L #include <cstring> #include <experimental/filesystem> namespace fs = std::experimental::filesystem; #endif #if CPP_STANDARD >= 201703L #include <filesystem> namespace fs = std::filesystem; #endif class UFile { private: fs::path _p; public: explicit UFile(const std::string &path) : _p(path) {} explicit UFile(const fs::path &path) : _p(path) {} std::string path() { return _p.string(); } }; #endif // UFILE_HPP_ 将 <filesystem> 的 path 类作为私有变量使用； 因为要适配 Linux 和 Windows 平台以及不同的 C++ 标准，加个宏定义来做判断。 这样就是一个基类，接下来对各种类型处理的文件类都继承该基类。 TxtFile #ifndef TXTFILE_HPP_ #define TXTFILE_HPP_ #include <fstream> #include "UFile.hpp" class TxtFile : public UFile { private: std::string _data; public: explicit TxtFile(const std::string& path) : UFile(path) {} explicit TxtFile(const fs::path& path) : UFile(path) {} std::string getData() const { return _data; } void setData(const std::string& data) { _data = data; } bool readData() { std::ifstream file(path()); if (!file) { return false; } _data.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); file.close(); return true; } bool writeData() { std::ofstream file(path()); if (!file) { return false; } file << _data; return true; } bool appendWriteData(const std::string& data) { std::ofstream file(path(), std::ios_base::app); if (!file) { return false; } file << data; return true; } }; #endif // TXTFILE_HPP_ 文本文件主要是对字符串的处理，根据需求考虑读取，写入和追加写入这三个功能。 就是调用 <fstream> 这个库中的 std::ifstream 和 std::ofstream 类对文本文件做处理，有个数据流的概念，那三个功能就是对数据流做些处理即可。 IniFile #ifndef INIFILE_HPP_ #define INIFILE_HPP_ #include "UFile.hpp" #include "ini/SimpleIni.h" class IniFile : public UFile { private: CSimpleIniA _ini; public: explicit IniFile(const std::string &path) : UFile(path) {} explicit IniFile(const fs::path &path) : UFile(path) {} ~IniFile() {} bool setup() { _ini.SetUnicode(); SI_Error rc = _ini.LoadFile(path().c_str()); if (rc < 0) return false; return true; } void getFromIni(const std::string &section, const std::string &key, std::string &param, const char *defaultVal) { param = _ini.GetValue(section.c_str(), key.c_str(), defaultVal); } template <typename T> void getFromIni(const std::string &section, const std::string &key, T &param, const T defaultVal) { std::string paramType = typeid(T).name(); std::string tempParam; tempParam = _ini.GetValue(section.c_str(), key.c_str(), std::to_string(defaultVal).c_str()); if (paramType[0] == 'i') param = static_cast<T>(stoi(tempParam)); else if (paramType[0] == 'f') param = static_cast<T>(stof(tempParam)); else if (paramType[0] == 'd') param = static_cast<T>(stod(tempParam)); else if (paramType[0] == 'b') if (tempParam == "false" || tempParam == "0") param = false; else if (tempParam == "true" || tempParam == "1") param = true; } template <typename T> void getFromIni(const std::string &section, const std::string &key, T *param, const T *defaultVal, const int &size) { int index = 0; std::string paramType = typeid(T).name(); if (_ini.GetValue(section.c_str(), key.c_str()) == nullptr) while (index <= size - 1) { param[index] = defaultVal[index]; index++; } else { std::string tempParamArrayStr = _ini.GetValue(section.c_str(), key.c_str()); std::vector<std::string> tempParamArray = split(tempParamArrayStr, " ,\t\n"); if (paramType[0] == 'i') for (int i = 0; i < tempParamArray.size(); ++i) param[index++] = static_cast<T>(stoi(tempParamArray[i])); else if (paramType[0] == 'f') for (int i = 0; i < tempParamArray.size(); ++i) param[index++] = static_cast<T>(stof(tempParamArray[i])); else if (paramType[0] == 'd') for (int i = 0; i < tempParamArray.size(); ++i) param[index++] = static_cast<T>(stod(tempParamArray[i])); while (index <= size - 1) { param[index] = defaultVal[index]; index++; } } } void setToIni(const std::string &section, const std::string &key, const char *fromValue) { _ini.SetValue(section.c_str(), key.c_str(), fromValue); } template <typename T> void setToIni(const std::string &section, const std::string &key, const T fromValue) { std::string valueType = typeid(T).name(); std::string toValue; if (valueType[0] == 'i') toValue = std::to_string(fromValue); else if (valueType[0] == 'f') toValue = std::to_string(fromValue); else if (valueType[0] == 'd') toValue = std::to_string(fromValue); else if (valueType[0] == 'b') if ((bool)fromValue == false) toValue = "false"; else if ((bool)fromValue == true) toValue = "true"; _ini.SetValue(section.c_str(), key.c_str(), toValue.c_str()); } template <typename T> void setToIni(const std::string &section, const std::string &key, const T *fromValueArr, const int &size) { if (size <= 0) return; std::string valueType = typeid(T).name(); std::string toValueArr; if (valueType[0] == 'i') for (int i = 0; i < size; ++i) { toValueArr += std::to_string(fromValueArr[i]); if (i != size - 1) toValueArr += ", "; } else if (valueType[0] == 'f') for (int i = 0; i < size; ++i) { toValueArr += std::to_string(fromValueArr[i]); if (i != size - 1) toValueArr += ", "; } else if (valueType[0] == 'd') for (int i = 0; i < size; ++i) { toValueArr += std::to_string(fromValueArr[i]); if (i != size - 1) toValueArr += ", "; } _ini.SetValue(section.c_str(), key.c_str(), toValueArr.c_str()); } void save() { std::string output; _ini.Save(output); _ini.SaveFile(path().c_str()); } }; #endif // INIFILE_HPP_ *.ini 文件是 Initialization file 的缩写，即为初始化文件，是 Windows 系统配置文件所采用的存储格式，统管 Windows 的各项配置。 用的一开源库 simpleini，再根据实际业务需求封了一层功能函数，能对 ini 文件做读取 getFromIni 和写入 setToIni 操作，用函数重载即可对不同的参数类型做处理。 JsonFile #ifndef JSONFILE_HPP_ #define JSONFILE_HPP_ #include "UFile.hpp" #include "json/json.hpp" using json = nlohmann::json; class JsonFile : public UFile { public: json _data; explicit JsonFile(const std::string &path) : UFile(path) {} explicit JsonFile(const fs::path &path) : UFile(path) {} ~JsonFile() {} bool setup() { std::ifstream file(path()); if (!file) { return false; } file >> _data; file.close(); return true; } void getFromJson(const std::string &key, std::string &param, const std::string &defaultVal) { if (key == "") { param = defaultVal; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) param = temp.at(keyArr[keyArr.size() - 1]); else param = defaultVal; } template <typename T> void getFromJson(const std::string &key, T &param, T defaultVal) { if (key == "") { param = defaultVal; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) param = temp.at(keyArr[keyArr.size() - 1]); else param = defaultVal; } void getFromJson(const std::string &key, std::string *param, const std::string *defaultVal, const int &size) { int index = 0; if (key == "") { for (int i = index; i < size; ++i) param[i] = defaultVal[i]; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) { const json thisKeyArrValue = temp.at(keyArr[keyArr.size() - 1]); for (int i = 0; i < thisKeyArrValue.size(); ++i) param[index++] = thisKeyArrValue[index]; } if (index < size) for (int i = index; i < size; ++i) param[i] = defaultVal[i]; } template <typename T> void getFromJson(const std::string &key, T *param, const T *defaultVal, const int &size) { int index = 0; if (key == "") { for (int i = index; i < size; ++i) param[i] = defaultVal[i]; return; } json temp = _data; std::vector<std::string> keyArr = split(key, "."); for (int i = 0; i < keyArr.size() - 1; ++i) if (temp.contains(keyArr[i])) temp = temp.at(keyArr[i]); if (temp.contains(keyArr[keyArr.size() - 1])) { const json thisKeyArrValue = temp.at(keyArr[keyArr.size() - 1]); for (int i = 0; i < thisKeyArrValue.size(); ++i) param[index++] = thisKeyArrValue[index]; } if (index < size) for (int i = index; i < size; ++i) param[i] = defaultVal[i]; } void save() { std::ofstream file(path()); file << _data; file.flush(); } }; #endif // JSONFILE_HPP_ 也用的一开源库 nlohmann/json，和 IniFile 一样做法啦。 BmpFile #ifndef BMPFILE_HPP_ #define BMPFILE_HPP_ #include "UFile.hpp" #include "bmp/BMP.h" class BmpFile : public UFile { public: explicit BmpFile(const std::string& path) : UFile(path) {} explicit BmpFile(const fs::path& path) : UFile(path) {} ~BmpFile() {} }; #endif // BMPFILE_HPP_ 呃，也用的开源库，是公司的前辈 clone 的，我没找到来源，不过还是日常感谢无私的开源贡献者。 百度百科 - BMP 是英文 Bitmap（位图）的简写，它是 Windows 操作系统中的标准图像文件格式，能够被多种 Windows 应用程序所支持。随着 Windows 操作系统的流行与丰富的 Windows 应用程序的开发，BMP 位图格式理所当然地被广泛应用。这种格式的特点是包含的图像信息较丰富，几乎不进行压缩，但由此导致了它与生俱来的缺点--占用磁盘空间过大。所以，BMP 在单机上比较流行。 这个开源库的接口考虑的很全面了，处理目前的业务够用了，就没做二次封装，不包了，直接用，仅仅是继承于 UFile，便于做路径处理。 BinFile 主要用于二进制文件的处理（读取、写入、追加），和 TxtFile 一样，使用的都是 std::ifstream 和 std::ofstream，打开选项多了个 std::ios::binary，用于指定以二进制方式打开文件，以及需要用到 seekg 来定位文件指针，计算出文件的长度。 #ifndef BINFILE_HPP_ #define BINFILE_HPP_ #include <fstream> #include <vector> #include "UFile.hpp" class BinFile : public UFile { private: std::vector<uint8_t> _data; int _length = 0; public: explicit BinFile(const std::string &path) : UFile(path) {} explicit BinFile(const fs::path &path) : UFile(path) {} void setData(const std::vector<uint8_t> &data) { _data = data; } std::vector<uint8_t> getData() const { return _data; } void setLength(const int &length) { _length = length; } int getLength() const { return _length; } bool readData() { std::ifstream file(path(), std::ios::in | std::ios::binary); if (!file) { return false; } file.seekg(0, std::ios::end); _length = (int)file.tellg(); file.seekg(0, std::ios::beg); _data.resize(_length); file.read((char *)_data.data(), _length); file.close(); return true; } bool writeData() { std::ofstream file(path(), std::ios::out | std::ios::binary); if (!file) { return false; } file.write((const char *)_data.data(), _length); file.close(); return true; } bool appendWriteData(std::vector<uint8_t> data) { std::ofstream file(path(), std::ios::out | std::ios::binary | std::ios::app); if (!file) { return false; } _data.insert(_data.end(), data.begin(), data.end()); _length = (int)_data.size(); file.write((const char *)_data.data(), _length); file.close(); return true; } }; #endif // BINFILE_HPP_ 用来处理以下两种数据： dat .dat 是二进制文件； img 处理图像文件其实也是对二进制的处理； 小造轮子 UString #ifndef USTRING_HPP_ #define USTRING_HPP_ #include <regex> #include <string> #include <vector> class UString { private: std::string _content; public: UString() : _content("") {} explicit UString(const std::string &content) : _content(content) {} ~UString() {} UString args(const std::string &substitution) { std::string upToDateContent = ""; std::string suffix = ""; std::regex percentSign("%([1-9]{1})"); auto content_begin = std::sregex_iterator(_content.begin(), _content.end(), percentSign); auto content_end = std::sregex_iterator(); for (std::sregex_iterator i = content_begin; i != content_end; ++i) { std::smatch match = *i; std::string match_str = match.str(); int index = match_str[1] - '1'; if (index == 0) upToDateContent += match.prefix().str() + substitution; else upToDateContent += match.prefix().str() + "%" + std::to_string(index); suffix = match.suffix(); } upToDateContent += suffix; return UString(upToDateContent); } UString args(const char *substitution) { return args(std::string(substitution)); } template <typename T> UString args(T substitution) { return args(std::to_string(substitution)); } template <class T, class... Args> UString args(T head, Args... rest) { UString result(_content); result = result.args(head); result = result.args(rest...); return result; } operator std::string() { return _content; } const char *c_str() { return _content.c_str(); } friend std::ostream &operator<<(std::ostream &os, const UString &uString) { os << uString._content; return os; } }; #endif // USTRING_HPP_ 其实就是 QString 有一个 arg 的方法（用于字符串的构建）太好用了，软件组舍不得，想保留这个方法。 不过 C++ 标准的 std::string 没有类似的功能函数，那我就浅浅造个小轮子。 造起来不难，小小难点就三个： 正则表达式库 <regex> 的使用，用于字符串的替换； 链式调用，那就函数重载； 传递多个参数，模板接受，然后递归即可，会自动识别对应的同名函数做处理； 要重载一下 << 操作符，否则 std::ostream 输出流识别不了 UString 这个类。 还有一个类型重载，这样就能用 = 操作符，将右值（Ustring 对象）重载为 std::string。 借轮子 Variant 用于替代 Qt 中的 QVariant，处理不同数据类型之间的转换。 前辈在 CSDN 上找的，软件组的大家在实际使用中，根据业务的需求，有做一些修改和 bug 修复。 这个代码太长了，不展示出来，看这里
写于 2023 年 3 月 2 日。 需要重复处理的文件过多了，就别 Ctrl C、Ctrl V 了,写点 Python 最合适了。 代码背景 #pragma once 和 #ifndef 都可用于防止头文件被两次编译。 不过 #pragma once 是依托微软编译器來使用，不支持跨平台。 #ifndef 的写法是 C++ 标准支持的，能够跨平台使用。 因此用 #ifndef 來替换掉原先代码中存在的 #pragma once。 用用 Python 利器 一般处理多文本文件，我就会分成两块： 遍历多文件，这里即 traverse_directory，目录遍历文件的代码可复用； 文件文本处理，这里即 replace_the_marco，文本处理的代码根据需求來做修改； import os def replace_the_marco(cur_file_name: str) -> bool: flag: bool = False basename: str = os.path.basename(cur_file_name) file_name: str = basename.split(".")[0].upper() suffix: str = basename.split(".")[1].upper() macro: str = file_name + "_" + suffix + "_" first_sentence: str = "#ifndef " + macro second_sentence: str = "#define " + macro third_sentence: str = "#endif // " + macro lines: list[str] = [] with open(cur_file_name, mode="r", encoding="utf-8") as file: index: int = 0 # 只找前三行 for line in file: temp_item: str = line.strip() if index < 3: if temp_item == "#pragma once": flag = True index = index + 1 lines.append(line) file.close() if flag: print("\nModify the macro for " + basename + "!\n") with open(cur_file_name, mode="w", encoding="utf-8") as file: for line in lines: if "#pragma once" in line: file.write("%s\n" % first_sentence) file.write("%s\n" % second_sentence) else: file.write("%s" % line) file.write("\n%s\n" % third_sentence) file.close() else: print("The " + basename + " does not need to be modified!\n") return flag def traverse_directory(cur_dir: str): for cur_dir_item in os.listdir(cur_dir): cur_path_name: str = cur_dir + "/" + cur_dir_item if cur_dir_item[-4:] == ".hpp" or cur_dir_item[-2:] == ".h": replace_the_marco(cur_path_name) elif os.path.isdir(cur_path_name): traverse_directory(cur_path_name) if __name__ == "__main__": folder_absolute_path: str = "" traverse_directory(folder_absolute_path) 小坑，遇到一些 Visual Studio 下创建的文件，格式为 ansi，需要转为 utf-8 才可再用上面的脚本处理，分另一个脚本文件处理此过程。这样跑完一个脚本，可以验证一个阶段。 import chardet import codecs import os def ansi_to_utf_8(cur_file_name: str): with open(cur_file_name, "rb") as f: r = f.read() f_charInfo = chardet.detect(r) # 获取文本编码信息 charset = f_charInfo["encoding"] if charset != "utf-8" and charset != "UTF-8-SIG": print(cur_file_name, charset) f = codecs.open(cur_file_name, "r", "ansi") ff = f.read() file_object = codecs.open(cur_file_name, "w", "utf-8") file_object.write(ff) def traverse_directory(cur_dir: str): for cur_dir_item in os.listdir(cur_dir): cur_path_name: str = cur_dir + "/" + cur_dir_item if ( cur_dir_item[-4:] == ".hpp" or cur_dir_item[-2:] == ".h" or cur_dir_item[-4:] == ".cpp" or cur_dir_item[-2:] == ".c" ): ansi_to_utf_8(cur_path_name) elif os.path.isdir(cur_path_name): traverse_directory(cur_path_name) if __name__ == "__main__": folder_absolute_path: str = "" traverse_directory(folder_absolute_path)
水篇博文 /doge。 开头挂个教程 Learning Prompt 开头挂几个视频链接 使用 ChatGPT 的一些例子 超级小華 不高兴就喝水 个人感觉，如果说互联网的出现，将知识从 “实体” 形式转换为 “数字” 形式存储起来，让人们相较过往，更容易接触到各种各样的知识。那么，ChatGPT 这种可对话的 AI，很容易地将知识做一个整合，将整合的内容再反馈给提问者。 Web Searching Skills 过往，一般（说我自己），在计算机方面打算做一个小玩具（桌面计时器啦），会组合几个搜索的关键词，去 Google 上搜索，然后再从搜索结果中找到自己想要的答案。整个过程会花费掉一些时间，而且，搜索结果中的内容，往往不是自己想要的，还需要再花费一些时间去过滤掉不想要的内容。最终还要我自己整合出我所需要的信息。 但现在，我可以直接描述我打算做一个什么东西，ChatGPT 会给我一些尽可能接近的回答，我再继续追问，ChatGPT 返回的答案，会越来越接近我想要的答案。这个过程，相较我自己去搜索，会更加快速。 但我对于 ChatGPT 返回的回答，其所述说的知识准确性也是需要再三确认的。记住，最终做判断还是我们人类本身，AI 只是辅助我们做决策。 网络上有关 ChatGPT 这类 AI 的应用有很多视频或文章，基本现在各大资讯渠道有关这方面的资讯应该不少。我这里只介绍我个人所用到的，谈谈使用感受。 Easy 生成配置环境教程 太爽了，说清楚你什么系统（最好能说版本号就说），你要做什么事情，做这个事情大概会安装什么，做个甲方，给 ChatGPT 提需求，ChatGPT 就能从训练的数据（无数前人配置环境踩过的坑）中找到适合你的配置环境教程。当你想去学习某些新技术，很难再卡在配置环境那一环节了。 小白入门新技术，输出 “Hello World！”。 生成目标脚本 年初，还打算自我增值，学一波 Linux 下的 shell 脚本知识，ChatGPT 一出，我已经失去继续学习的欲望。我只要面向 ChatGPT 编程即可。而且本身 shell 脚本的很多知识就很模块化，将各种 shell 命令根据你所描述的需求去组合并不是一件难事（对 ChatGPT 而言）。不仅是 shell，对于 Python 这门脚本语言来说，ChatGPT 也能生成目标脚本去处理一些文本数据或者做一些自动化操作。 生成函数（函数式编程） 函数式编程中的函数 == 真正的数学函数，其引用透明，即只要使用相同的输入调用函数，将始终得到相同的输出。 这类函数让 ChatGPT 生成真是嘎嘎简单。只要你描述清楚你的需求，ChatGPT 就能生成你想要的函数。 这是我重新拾起 C++ 开发 5 个月后写的一个字符串分割函数（网上找了一份 Linux 下 C 语言，加了个 Windows 的版本，测试能跑就没管了）。 std::vector<std::string> split(const std::string &data, const std::string &separator) { std::vector<std::string> result; if (data == "") return result; #if defined(_MSC_VER) char *next_token = NULL; char *token = strtok_s((char *)data.c_str(), separator.c_str(), &next_token); while (token) { result.push_back(token); token = strtok_s(NULL, separator.c_str(), &next_token); } #elif defined(__GNUC__) char *token = strtok((char *)data.c_str(), separator.c_str()); while (token) { result.push_back(token); token = strtok(NULL, separator.c_str()); } #endif return result; } 这是 ChatGPT 生成的函数，我只是把我上面写的这个函数丢给它，让它帮我优化，就返回一个十分简练的函数，一对比，就很不错。ChatGPT 非常适合写这种和业务逻辑无关的函数。毕竟网络上的相关实现，想来这么多年也是积攒了不少。 std::vector<std::string> split(const std::string &data, const std::string &separator) { std::vector<std::string> result; std::stringstream ss(data); std::string item; while (std::getline(ss, item, separator[0])) { result.push_back(item); } return result; } 数学计算（问的别太夸张） 感觉，就我大学学过的数学相关的知识（高数、线性代数和概率论等等），ChatGPT 都能给我很好的回答。当然，也有可能是因为我所需解决的数学问题相较来说还蛮普通。问的时候最好别想着只要个结果，最好要让它把全部推导过程说清楚，否则那个可信度，自行根据自己的数学知识再做判断。 单元测试（TDD） TDD（Test-Driven Development）是一种软件开发过程，它是由 Kent Beck 在 1999 年提出的。TDD 是一种通过编写测试来驱动开发的过程。在 TDD 过程中，开发者先编写一个失败的单元测试，然后编写一些代码来使单元测试通过，最后重构代码。 接上面的函数式编程，其引用透明的特性，ChatGPT 很容易根据这些函数编写对应的单元测试，测试函数的输入输出是否符合预期。 Still Hard 复杂一点点的业务逻辑 这方面，ChatGPT 还是有点弱，即使你自认为能描述清楚你的业务需求，ChatGPT 也不一定能给你一个满意的答案。毕竟业务这种东西本身就带有主观性，而且，ChatGPT 是通过大量的数据训练出来的一种产物，ChatGPT 本身带的客观性相较来说还是蛮强的。所以，即使 ChatGPT 能生成一些代码，还是需要你自己去根据业务的实际情况去做些调整。 这里放一个我在做的文件读写工具的链接，ChatGPT 能帮我编写不错的文件读写代码（文件读写相关的代码网络上太多了），但是它不理解我在文件读写之上，根据所需的业务代码而确定的接口，那个工具是为了替换 Qt 代码而创建的，但要确保接口（返回值与传递参数）不变，要不然到时整体代码的改动会很大，改的很累。即使我把这个接口规范丢给 ChatGPT，它也不能给我一个满意的答案。这一块还是需要我自己去做调整。 End ChatGPT 的出现，让我感觉这是一个新的 Apple 时刻（iPhone 触屏手机的出现）。未来，使用 ChatGPT 这类 AI 会越来越普遍，而且，这类 AI 会越来越智能。总之，还蛮期待，这会发展成什么样子。 这篇文章会持续更新，看看 ChatGPT 还能做些什么。
起因 年终 -- 工具总结 我真的需要很多的工具吗？ 我的编程习惯貌似差不多定下来了，谨以此文记录下来！ 同时面向对象编程 OOP 有一个基本原则 --- Don't repeat yourself（不要重复你自己，简称 DRY），要不然你想 WET --- Write everything twice。 因为工作上的需求，需要在 Linux 环境下做些工作，上班期间花了些时间配置了两台 Linux 电脑的开发环境，花一个下午折腾所需开发环境。之后那个工作可能会在多个新的 Linux 电脑上进行，我便做了简易的 PPT 记录如何配置开发环境，让后续参与该项目的同事有个参考。 顺带我手头上的电子设备蛮多，我也喜欢折腾电子产品，而现在一些操作习惯也逐渐确立，那肯定于我个人而言，我希望在我每一个电脑（或者 Docker 容器）上都能没什么操作障碍地敲代码，就是我配置的那些快捷键啊，一些我自定义的缩写指令呀这些，都是一样的。一种操作上的自在。 就有整理一个 GitHub 仓库，可以参考下。（不包含 IDE 的快捷键设置，IDE 见仁见智，各有所爱） 顺带一提，编程开发看做什么吧，大部份开发就关注电脑的系统、内存和硬盘这三个就够了。如果你有软件安装失败的情况，那大概率会和这三方面挂钩，有些可能需要考虑显卡或者一些驱动。 Dev Platform 我个人目前在使用中的一些操作系统 Linux Ubuntu 22.04 Kail Linux Arch Linux Windows 10/11 -- WSL2（Ubuntu 20.04） macOS（Unix） Tools Terminal 我很喜欢在终端下完成我的一些工作（只要是敲代码于我而言就是工作）： 用 Vim 写一些 shell 脚本 喜欢开着一个 top 面板看着（如果我当时的工作环境有个副屏的话），可以用来看看软件的使用情况 用 ssh 远程连接一些服务器，同时做些事情 用 Python 来充当计算器（进制计算、移位计算和异或等等） macOS 下的 Terminal 终端就是 iTerm2（以上截图都为其效果） Linux 下的都为其自带的 Terminal 终端即可 Windows 下建议下载其商店的 Terminal 终端来使用，而非选择 Powershell Editor 邪？少儿不宜？看我博客的不强求，上手难度有点大。 就是 Vim + Tmux 这套，这套组合其实蛮老的，也常被一些程序员称其为邪教组合，但是只要你习惯终端开发，可能会对这一套开发组合感兴趣。 VSCode（基本三平台我都会安装），VSCode 的扩展各个平台也会有不同的选择，可看我上面提到的 GitHub 仓库。 Sublime Text （Linux 环境下我才安装，Windows 的界面太丑太拉了，我就不用） Visual Studio （工作用） JetBrains 全家桶（看工作需要安装） Browser Edge（Windows 必备） Chrome（基本三平台都安装） Vivaldi（macOS 安装，也只有 macOS 下的 UI 值得一看，否则其他平台太拉了，看的太臃肿了） Safari（macOS 必有，但我超少用） Commands 我最常使用的 20 个指令，看得出我现在主要的工作都围绕 C++和 Web 这两个领域展开 history | awk '{print $2}' | sort | uniq -c | sort -nr | head -20 2824 git 1964 cls 1009 cd 700 yarn 499 ls 470 npm 254 rm 178 code 117 brew 103 g++ 101 npx 97 ./a.out 88 md 87 vim 83 pnpm 71 mv 67 node 58 cat 54 sudo 43 tree
搜索到一个东西，叫 Katalon Recorder，是一个能通过录制用户操作生成测试脚本的插件（各个主流浏览器都会有，推荐 Firefox，我 Chrome 卡的丫皮） 录制结束可按 play 重新回放你的操作。 当然重点是 Export！ 可以导出蛮多种语言编写形式的脚本，我这里如果是要求 JavaScript，导出格式要求为 WebDriver.io。 欧！熟悉的节点操作，这个 $ 就是 Jquery 那味道了。 So，我只用做个翻译工，把上面代码转换为 async/await 形式的脚本即可。 const { Builder } = require("selenium-webdriver"); const { By } = require("selenium-webdriver"); (async function myFunction() { let driver = await new Builder().forBrowser("chrome").build(); // 测试网址 await driver.get( "不见了～～" ); // 输入账户名 await driver .findElement(By.xpath("//input[@type='text']")) .sendKeys("～～"); // 输入密码 await driver .findElement(By.xpath("//input[@type='password']")) .sendKeys("～～"); // 验证码 await driver .findElement( By.xpath( "//div[@id='userLayout']/div/div[2]/div/form/div/div[3]/div/div[2]/form/div[3]/div/div/div/div/span/span/input" ) ) .sendKeys("～～"); // 点击登陆 const loginButton = await driver.findElement( By.xpath("//button[@type='submit']") ); await loginButton.click(); })();
Selenium 官网链接 https://www.selenium.dev/ 能够多种语言做自动化测试，这里使用的是 JavaScript。 准备 使用之前需要安装两个东西： 下载 Selenium 的 npm 包 npm install selenium-webdriver -g 需要下载 browser drivers 即如果需要在哪个浏览器下进行自动化测试运行，都需要安转其相应版本号的驱动。 目前在 Chrome 浏览器下做自动化测试，在官网给出的链接选择我浏览器对应的版本号即可。 https://chromedriver.storage.googleapis.com/index.html 将下载好的压缩包解压到测试项目中 测试项目 . ├── chromedriver ├── login_test.gif ├── login_test.js ├── login_test.mov ├── node_modules ├── package-lock.json └── package.json 注意：驱动需与脚本在同一级目录！ 登陆测试 查询 Selenium 官方文档学习基本对浏览器的操作，根据文档说明编写脚本。 const { Builder } = require("selenium-webdriver"); const { By } = require("selenium-webdriver"); (async function myFunction() { let driver = await new Builder().forBrowser("chrome").build(); // 测试网址 await driver.get( "它不见了～～～" ); // 输入账户名 await driver .findElement(By.xpath("//input[@type='text']")) .sendKeys("～～"); // 输入密码 await driver .findElement(By.xpath("//input[@type='password']")) .sendKeys("～～"); // 验证码 await driver .findElement( By.xpath( "//div[@id='userLayout']/div/div[2]/div/form/div/div[3]/div/div[2]/form/div[3]/div/div/div/div/span/span/input" ) ) .sendKeys("～～"); // 点击登陆 const loginButton = await driver.findElement( By.xpath("//button[@type='submit']") ); await loginButton.click(); })(); 原本想发测试图，可图泄露东西有点多～～
起因 最近看到 freeCodeCamp 微信公众号的一篇文章的； 那篇文章当时看到一句话 在这篇文章里，我们将使用 React、Tailwind CSS、Netlify 等等行业标准工具创建一个作品集网站。 这句话中有一个关键词 Tailwind CSS ，这是我还没整过的玩意，最近也是在准备一个春招，有遇到面试官提过一下，我当时实话说自己还没用过 doge ，不过记住了这个玩意，想着来摸索下这个项目是如何使用 Tailwind CSS 的，顺带再水篇博文； 动起来！ 那肯定就先按照那个公众号把整个项目弄起来，部署起来，先看看这个跑不跑得起来（不能跑就不看了 doge ，bushi）； 部署后的网站链接：https://kok-s0s-portfolio.netlify.app/ 先清楚它是如何被调用的 Google 一搜 Tailwind CSS creat react app ，ok！ 第一篇就是你了 --> 在 Create React App 中安装 Tailwind CSS 它的官方文档就是写的很详细了（自己看去吧） 阅读它的代码 公众号文章有事先说明 Tailwind 允许我们通过组合类名（classnames）将多种样式轻松应用到 React 项目上，给应用程序一个专业的外观。 个人我肯定先看项目被挂载的主要组件 App.js ； import React from "react"; import About from "./components/About"; import Contact from "./components/Contact"; import Navbar from "./components/Navbar"; import Projects from "./components/Projects"; import Skills from "./components/Skills"; import Testimonials from "./components/Testimonials"; export default function App() { return ( <Navbar /> <About /> <Projects /> <Skills /> <Testimonials /> <Contact /> ); } 接下来就是挺“简朴”的操作，在 Tailwind CSS 官方文档搜索这些组件文件中用到的类名，接下来列出这个项目我搜索过的所有有结果的类名以及该类名的作用： App.js text-gray-400 字体颜色 bg-gray-900 背景颜色 body-font 文档没搜到，大概是使用浏览器默认字体的意思 About.js container 容器 --> 看文档很详细 mx-auto 能使一个容器居中 px-10 添加水平内边距，左右都有 py-20 添加垂直方向的内边距 pr-24 即 padding-right: 6rem; py-2 即 padding-top: 0.5rem; padding-bottom: 0.5rem; px-6 即 padding-left: 1.5rem; padding-right: 1.5rem; 响应式设计 sm 断点 --> 最小宽度 640px，即 @media (min-width: 640px) { ... } md 断点 --> 最小宽度 768px，即 @media (min-width: 768px) { ... } lg 断点 --> 最小宽度 1024px 即 @media (min-width: 1024px) { ... } flex 看文档 flex-row 使用 flex-row 用来沿与文本相同的方向水平放置 flex 子项 --> 横向 flex-col 使用 flex-col 用来沿垂直方向放置 flex 子项 --> 纵向 flex-grow 使用 flex-grow 允许一个 flex 项目放大，以填充任何可用空间 items-center 使用 items-center 沿着容器的交叉轴中心对齐项目，即垂直居中效果 items-start 使用 items-start 将项目沿着容器交叉轴的起点对齐： w-1/2 即宽度为 width: 50% 分数为比例，数字就是具体数值 text-left 文字向左对齐 text-center 文字居中对齐 mb-16 即 margin-bottom: 4rem; mb-0 即 margin-bottom: 0px; title-font text-4xl 即 font-size: 2.25rem; line-height: 2.5rem; text-3xl 即 font-size: 1.875rem; line-height: 2.25rem; mb-4 即 margin-bottom: 1rem; font-medium 即 font-weight: 500; text-white 白色文字，即 --tw-text-opacity: 1; color: rgba(255, 255, 255, var(--tw-text-opacity)); hidden 隐藏，使用 hidden 设置一个元素为 display: none，并从页面布局中移除 inline-block 使用 inline-block 创建一个内联块级元素 mb-8 即 margin-bottom: 2rem; leading-relaxed 即 line-height: 1.625; justify-center 使用 justify-start 让项目沿着容器主轴的起点对齐 inline-flex 使用 inline-flex 来创建一个内联的 flex 容器 bg-green-500 即 --tw-bg-opacity: 1; background-color: rgba(16, 185, 129, var(--tw-bg-opacity)); border-0 即 border-width: 0px; 边框厚度 focus 添加 focus: 前缀，以在 focus 状态时应用一个功能类 outline-none 使用 outline-none 来隐藏焦点元素的默认浏览器轮廓 hover 添加 hover: 前缀，以在 hover 状态时应用一个功能类 bg-green-600 即 --tw-bg-opacity: 1; background-color: rgba(5, 150, 105, var(--tw-bg-opacity)); rounded 即 border-radius: 0.25rem; text-lg 即 font-size: 1.125rem; line-height: 1.75rem; ml-4 即 margin-left: 1rem; max-w-lg 设置最大宽度，即 max-width: 32rem; w-full 即 width: 100%; w-5/6 即 width: 83.333333%; object-cover 即 object-fit: cover; 使用 .object-cover 调整元素内容的大小以覆盖其容器 object-center 使用 object-{side} 功能来指定可替换元素的内容在其容器中的位置。 End 搜完 About.js ，就大概摸索出一些规律和使用方法： 这里面很多规定大都是将“1”分为四份； 很简写 top --> t left --> l right --> r bottom --> b margin --> m padding --> p top、bottom --> y left、right --> x high --> h width --> w 背景 --> bg 文字 --> text 设置最大最小宽度 --> max、min flex 的看多就清楚了 doge 其实英语熟练点就很快上手了； 多敲代码多实践； 开源网站真的有很多不错的代码值得学习； 课外读物 Why I Don't Like Tailwind CSS
doge 为狗头意思 应发布于1月14日（离职） 第一份前端实习工作 --- 历险记？ 记录下这份实习工作的收获和感受 面试进的是某医疗机构的研发部门，人不多，都是为医务人员打工； 做的东西就是医生为患者看病时，管理患者数据的一个大系统： 现在患者看病 --> 小程序； 医生电脑看患者信息 -> 管理后台； 医生手机端APP看患者信息 （时代的一个趋势吧，现在手机太方便了，原先医生都是在电脑或者手记管理患者信息） --> 貌似原计划在安卓、iOS上做APP应用变成在企业微信上做H5页面应用开发（成本低，开发快，满足需求）； 熟悉下 其实原先这家公司早有一个在用着的管理后台系统让医务人员使用，但是时代在进步，大概医疗人员觉得原先那个太丑太拉了不想用了，那这甲方老哥有需求，这不得要做出些改变呀！ 上级那几位研发也确定好了技术选择，基于一个开源框架 Jeecg-Boot 来搭建这个后台。 这个框架是一个低代码平台，能便于前后端（这个项目两个前端，两个后端）快速开发，后端我就不清楚到底有多便捷了，前端倒是真的方便太多了。 头一星期 看 README 做些换色换皮操作，Jeecg-Boot 这个框架源码下载下来有自带的信息 (dddd)； 配置页面路由信息，该框架是基于 Vue2 的，可以在其框架提供的 @/config/router.config.js 做路由配置操作，但更好的是用该框架提供的选择-在线菜单配置； 简单做个页面测试是否可行，OK了继续做下去。。。 之后 切图切图还是切图！ 真的，切图时回忆起高考后当时大家流传着一句话“劝人学医，天打雷劈！”，这句话真的有几分道理，学医的要掌握太多知识了，是一份即吃下功夫的努力和大量时间的工作。 我切的那些图都不怎么难了，但是一个页面数据量巨多； 举个例子，一个患者如果患有糖尿病，现在时代变了，搞多些数字化检测，患者每天需要对自己的血糖做个三次检测，每次检测都在吃饭前后，早餐午餐晚餐，那就有6个数据了，这个检测还不是只做一天那种； 再有，原来看病有时需要考虑些并发症和合并症的问题（一个糖尿病就需要考虑11种并发症），如果患者真的有这些疾病了，又需要结合患者的既往病史考虑这个患者的情况，好家伙，我这就是要不停地切图呀！ 切的人都麻了，好处就是了解很多关于疾病的信息，某些疾病会和某些疾病有所关联，属实是带薪学“医”了。 这个切图经历让我感觉高资历医生其实真的很配拿高福利高工资的。 这个后台用到的技术 基于 Jeecg-Boot 框架搭建的后台系统中前端部分所用技术为： Vue2 主要是页面的制作，不同的数据（文本信息，时间，图片）显示与修改，文件的上传下载等等以及父子组件的一些问题（子组件调用父组件的方法，父组件传递数据给子组件等等） Vuex 用于处理数据的流动，主要是患者的唯一ID，在进入某一个患者的信息档案后，该患者关联的所有子信息档案的患者ID只能为该患者的。 因为是管理，会有一个大列表有着每一位患者的基本信息，这个流程是这样的，医务人员点击某个患者信息后，会进入该患者的详细信息档案（很多很多不同信息档案），就需要将列表中点击的那个患者ID传入这些信息档案中； 做法就是在 store 相关文件下的 mutation-types.js 中设置自己需要的变量，用 export const 导出，在列表入口做个赋值操作并且能打开新窗口显示该患者的信息档案，即相关代码 Vue router.resolve 如下 this.$router.resolve({ path: '...', query }) // query包含患者ID这个参数 而在其它需要使用到这个变量的子信息档案页面中使用 Vue $route.query 的方法获取URI上的患者ID这个参数信息即可。 Vue Router 用 Jeecg-Boot 的在线菜单路由进行配置； UI库 -- Ant Design of Vue Jeecg-Boot 推荐使用的组件库，搭配 Vue2 实现一些功能，主要使用到的组件为 Button 按钮 Grid 栅格 Dropdown 下拉菜单 Checkbox 多选框 DatePicker 日期选择框 Form 表单 信息校验 自定义校验 -> 手机号|身份证号（正则表达式） Input 输入框 Radio 单选框 Select 选择器 Upload 上传 Table 表格 Tabs 标签页 Drawer 抽屉 Message 全局提示 Modal 对话框 axios Jeecg-Boot 基于 axios 内置了大量的请求方法，主要使用为 GET 和 POST ; 调用接口注意点 接口地址（测试和正式） 传参（post方法会用到） 返回值（是否规范？） Jeecg-Boot 自带组件 主要配合数据字典使用，因为医疗存在大量术语，这些术语可能会在多个页面复用，配置成数据字典，能够一份数据在多个页面做调用，主要用于多选框，下拉选择框，单选框和列表Table这些组件上。 解决冲突 01 这年头，小程序大概已经深入人们生活的方方面面了吧，人们离不开小程序。 我后期工作被拉去搞些移动端网页的制作 说说怎么回事吧。下面是我瞎猜乱编阐述（换位思老）甲方为什么会有这种需求的，切勿当真，纯属我个人感觉。 现在我们（医务人员）是能使用电脑管理患者的信息，但是我们单纯只是为了查看患者信息总是用电脑那也不方便是吧，毕竟，电脑不能随身带，那行，正好我们公司不是有个企业微信嘛，要不让研发的人在企业微信弄个类似小程序的功能，反正我们的患者（客户）都在这个企业微信那，让他们为这个患者设置多个信息查询功能，我们好查看患者信息。 弄个流程图说明下，PS：我被拖去切图时，这个网站已经能和企业微信的患者关联起信息了，我的任务被分配的也还是切图。 切的图全是展示数据那种，毕竟移动端，甲方只要求能看患者信息就行了。 这个子标题的重点要来了 这个网站搭建技术，我参与的时候惊讶地发现有很多种不同技术在使用着。 我看着 Git Graph 上显示的 git commit 历史，前几十个commit，网站建站用的是传统三剑客，用 jquery 操控页面的 DOM 节点，使用 ajax 调用接口来获取数据；看着看着，忽然出现了别的技术（网站中途让另一个前端做），用的技术是 Vue2 来制作网页，使用 axios 调用接口。 说真，没工作前，老是在知乎B站刷到说公司中的一个项目所用技术有可能会很杂，我面临的这个杂就是代码大家没有做个规范，代码有些还没基本的格式化（代码加个 tab 也行呀），大家编写风格也有自己的理解等等； 好在这个需求还蛮急的，说让我用什么技术都行，能把活做了就行； 那我就整活了，实习的时候讲道理我还没具体使用过 Vue3 做过一些事情（面试进去的时候我也只会 React 的皮毛 doge），还好那个网站搭建就是一个个普通的 HTML文件做成的，使用接口来对数据做处理，额外可能会用到 localStorage-本地存储空间 来存储一些数据（前面提到的患者唯一ID等等）； 而且我惊讶地发现这个项目从没用过一些组件库，之前的工作人员全都靠自己一手一手地“搭房子”，而这个项目的第一个 commit 时间为 2021/6/10（OK，xiaochuqiangda!） 那行，我就用组件库了，这时候如果是要找代码资源，个人建议优先开源平台搜索； Github 一搜 mobile UI 就出来一大堆； 最后根据UI给出的原型图我选定的是 vant 这个开源组件库； 因为是小程序的主要目的是用于展示患者信息数据，整体很好做； 这其中最开心的一件事情就是我提交过几次代码后，其它两位参与的同事也应该看到过我的代码，会在他们自己做的任务中使用我写过的一些代码片段，也利用 vant 组件库来快速开发，有一种被小认可的感觉； 02 因为前端是最容易发现问题的地方，无论是谁都先把问题递交给我们前端，真的人很麻； 而且那个项目其实大都是新人在做，测试是文科生，也是她第一次做测试，沟通有很大的问题，开发时我经常会被打扰到，开发的思路就断了； 处理做法： 和测试说明什么样的问题（是否组件出现故障，页面布局不合理等等）提给我； 多喝水！ 当我快走了（还剩一周），我那leader终于发现我们这个团队存在的问题（沟通低效），让我们严格遵循企业微信上一个叫TAPD的东西去执行我们各自的工作； 我真的喜欢他当时训诫说的一些话：工作中，直接喊别人解决问题是最低效的沟通，是一种野蛮沟通、问题应该用合适的语句说明出来再将问题给解决问题的那个人、程序员效率最高的时候就是带个耳机，其它先什么都不管，好好码代码，一下就能把工作做完了； 真的工作后，个人认为工作中沟通是要为别人考虑的； 分享无极限～ 分享我在这份前端实习中用过的“一些” 软件篇 Does It ARM? 查看软件是否支持你的 macOS 版本和芯片； 主要是 macOS 系统下的 Github Desktop 偶尔拿来看同事代码的； Postman 用于调试接口，调试出问题截图和后端好老兄battle； Navicat Premium 当时后端好老兄告诉的，用于连接数据库使用的，也可实时更改数据库数据； Pikka 取色工具，一种颜色能够很方便选取它的相邻颜色（eg：蓝色 -> 淡淡蓝色。。。。）； Snipaste 截图工具，有时和同事说话，一图“代”千言； Notion 用于收录记录开发中遇到的问题，问题出现过一次，解决了，那下次应该是要避免出现类似问题； LICEcap gif 动图生产工具，用于录制bug（复现bug）； 一些 PWA 应用 是阅读 VueJs 文档时了解到的 配合一些快捷键能够快速打开文档应用检索信息； Pixelmator Pro P图工具，功能比 PhotoShop 少很多，但是它提供的功能满足我的使用需求，而且当时黑五打半折，是一次性订阅终身，性价比个人感觉比 PhotoShop 高； HazeOver 能让我的视线快速聚焦于当前窗口； Moment 倒计时软件，方便自己给自己任务定个 DDL，比leader给的截止日早一点完成工作，让自己能够更可控自己的时间； 12. VSCode 单独开个标题以示尊重 doge 主题插件 自己有个小习惯，对着屏幕有时眼睛会累，就会根据自己当时的感觉走选一个舒服的主题，这样自我感觉又有些动力编写代码了；doge Community Material Theme ommunity Material Theme Dracula Official freeCodeCamp Dark Theme Material Theme Material Theme Icons Minimalist Product Icon Theme Bootstrap Product Icons Carbon Product Icons file-icons Fluent Icons Material Product Icons Night Owl 夜间最爱！ Noctis Peacock 为每个项目个性化侧边栏颜色； vscode-icons-mac Vue.js Vue VSCode Snippets Vue Language Features (Volar) Vetur React ES7+ React/Redux/React-Native snippets html to JSX JS JSX Snippets 便于写前端代码的插件 Auto Close Tag Auto Rename Tag Babel JavaScript Beautify CSS Formatter CSS Peek ESLint HTML CSS Support HTML Snippets Import Cost JavaScript (ES6) code snippets Live Preview 开发移动端页面时常用，不用来回切换屏幕； Live Server Node Dependencies npm npm Intellisense open in browser Paste JSON as Code Path Autocomplete Path Intellisense px to rem & rpx (cssrem) Quokka.js REST Client 类似 Postman，能够快速测试一些测试接口，测完就删； Search node_modules Thunder Client 这个就可以说是 VSCode 版本的小 Postman，很常用，而且它会自动生成一些各种不同方法调用接口的代码（可以参考，切勿直接拿来用）； 杂 大部分看英文就懂是干啥的吧，一个个都解释很累的，如果我有解释的大概就是我个人很常用的； AppleScript Bash Debug Better Align --> 用于代码对齐 Better C++ Syntax Better Comments Bookmarks 书签，重点说说，其实做前端有时很多代码都是复用的（就只需要改些字段等等），只要我有一段代码需要出现第二次我就给那段代码打个 tag，这个习惯我个人感觉基于 VSCode 的体验不错，其实市面有不少这种需求的软件，其中我了解到的 SnippetsLab 挺好的，不过还没买，这个软件对我来说现在供需不对，其实就是贵，代码收录这个功能很多人都可以做，但是那个软件做的真的好（它的快捷性很强），目前在等优惠中； Bracket Pair Colorizer Code Debugger CodeLLDB CodeSnap 用于生成代码截图； Docker EditorConfig for VS Code Excel to Markdown table Git Extension Pack Git Graph 个人强烈推荐！ GitHub Markdown Preview gitignore highlight-icemode 高亮相同选中； Image preview 图片侧边预览； indent-rainbow 高亮层叠； Jupyter LaTeX Workshop LeetCode 刷题体验很好，关键方便自己本地分类； Stop 停一下！下面是一堆 markdown 插件 个人因为主要在 GitHub 和博客上发布文章，所以 Github 的渲染效果一般就会是我发布文章后的渲染效果；其实在 VSCode 编辑好 markdown 文件，发布在 GitHub 上，既能利用 git 的优势清楚自己所写文章的演变过程，而且 VSCode 搭配这些插件个人感觉体验很好，在此推荐下，当然萝卜青菜各有所爱； Markdown All in One Markdown Checkboxes Markdown Emoji Markdown Paste Markdown Preview Github Styling Markdown Shortcuts markdown-formatter Markdown+Math :emojisense: 分割线 Mithril Emmet Open 能在 VSCode 根据文件的后缀自动用我默认关联的应用来打开该文件； Open in GitHub, Bitbucket, Gitlab, VisualStudio.com! 本地反向到在线 GitHub 项目； PDF Preview Polacode-2020 Prettier - Code formatter Project Manager Qwerty Learner 练英语单词用的，真的； Remote - Containers Remote - SSH Remote - SSH: Editing Configuration Files solidity Tabnine AI 免费的还行啦！ TODO Highlight 真的到下班了，但是如果有不是很紧急的任务还没搞定，会留个 TODO 提醒自己； Toggle Zen mode 禅模式，码 markdown 写文章时必用； Vim VimL (Vim Language, Vim Script) WakaTime 记录自己码代码的各种数据（用时，语言等等）； Word Count CJK 看自己打了多少字； 接下来的 Image2Icon 翻译下就知道了； Translatium - Translator 免费的翻译软件（挺普通的，但够了），关键支持快捷，那就OK； Sketch 向 UI 同事学习学习，熟练度还不够高； GitHub 学生包 如果还有在校读书学生读这个文章，而且自己是有方法去 kexueshangwang 的，就是能稳定访问 GitHub的，我真的强烈推荐去 GitHub 上申请个学生包，享受下在校生的福利； 这是申请入口 我自己目前享用的是 Bootstrap Studio MongoDB HazeOver 一大堆免费的教学视频资源，主要是 educative 这是福利说明 Wireshark 抓包，主要用于通过实际操作来学习计算机网络知识； OBS 偶尔开开直播用的（主游戏，或，没或了） Steam en，劳逸结合！ START 去年 67891011月 玩很多云游戏是免费的，我之前一直用它玩原神和Q飞，现在每周有两小时免费时长； mac下云游戏的体验不错，前提是网速也要很不错； 设备篇 前端主力机子 macbook m1 air 8g 8g 真的个人感觉也就做做前端的工作（事实也是），JetBrains 下任何一个应用打开运行都卡得一批； Keychron K6 茶轴 算是一款 mac 键盘下的一个好选择（个人认为）； 昕科暖桌垫 家里人今年冬天给我买的，大赞； 今年广东那个过年天气哟，我完全不想把手露出来在桌上敲键盘； 强烈推荐大家买个暖桌垫，呵护手部健康 doge；