资讯动态

C#串口助手源码解析:基于SerialPort的串口通信开发实战

发布时间:2026/8/31 6:04:34 来源:尧图企业网站定制
简介这是一份面向C#初学者与嵌入式/工业通信开发者的串口调试工具源码资源基于.NET Framework的System.IO.Ports.SerialPort类实现专为解决串口通信程序开发中的参数配置、数据收发、实时监控与异常调试等核心问题而设计。资源压缩包共26个文件含8个核心C#源码文件如Form1.cs、Program.cs、1个Visual Studio解决方案.sln及项目配置文件.csproj、.settings辅以编译产物.exe、.pdb和界面资源.resx、.resources整体仅48KB轻量易读结构清晰便于快速理解UI逻辑与串口控制流程。已有81人学习下载适合用于教学演示、课程实验或项目原型验证。读者可直接运行EXE进行串口交互测试亦可深入源码学习SerialPort事件驱动模型、线程安全的数据接收处理、十六进制/ASCII双模显示等实用技巧并基于此扩展日志记录、自动应答或协议解析功能。 做了几年的上位机开发串口调试工具几乎是每天都要摸的东西。市面上现成的工具不少SSCOM、友善串口助手都挺好用可真到了自己项目要集成、要定制协议、要特殊处理数据的时候还是得手写一个。这就是我整理这份C#串口助手源码的初衷——不单单是个能用的小工具更是一套可以拿去做二次开发的通信框架。这篇博文我把自己在实际项目中积累的串口通信经验、SerialPort核心API的坑、多线程处理的细节都融进去了。你拿到的这份源码不是那种网上随便下载的demo而是我在真实工控项目里打磨过的版本。不管你是刚接触C#上位机开发的初学者还是要在现有系统里集成串口通信的工程师这篇文章应该都能给你一点实在的帮助。1. 串口助手的整体设计与功能拆解1.1 串口调试的真正痛点在哪很多人觉得串口助手就是把数据从串口读到文本框显示出来再把文本框的内容发出去仅此而已。真要这么想那写出来的东西大概率只能算个玩具。我在实际使用中碰到的痛点非常具体。第一个痛点是数据完整性。单片机或者传感器设备通过串口发来的数据往往不是一次完整到达的。串口通信本身没有包的概念它只是一条字节流设备可能一次发了20个字节但电脑这边分三次收到了8个、7个、5个。如果你不做缓冲区处理直接在接收事件里按字节拼接文本显示那帧数据永远是对不齐的。第二个痛点是数据可视化。调试电机驱动器、雷达、温控模块这些设备时纯文本显示意义不大你需要看到十六进制数据、需要看波形曲线、需要统计收发字节数。这些功能看起来是加分项实际上在真实调试中是刚需。第三个痛点是自动化和脚本化。我需要给设备发一组指令序列做自动化测试或者根据设备的响应自动决定下一步发什么。这就要求串口助手的底层是一个可以编程调用的通信模块而不是只有界面的玩具。我这份源码的设计目标就是把这几个痛点一次解决掉完整的数据接收缓冲、灵活的发送模式、清晰的字节统计以及一个完全可以脱离界面跑通的通信核心。1.2 功能模块怎么划分才合理拿到需求后我没有上来就写代码而是先花了点时间做模块划分。串口助手这个项目从功能上看可以分成三大块串口管理、数据收发、界面展示。串口管理负责扫描可用端口、打开关闭连接、配置波特率/数据位/停止位/校验位。这部分直接封装SerialPort类但我会在外面再包一层因为SerialPort原生的事件和数据读取方式在某些场景下并不好用后面我会细说。数据收发是核心中的核心。发送要支持文本和十六进制两种模式接收要有完整的行缓冲机制。这部分代码必须和界面完全解耦这样你以后要在服务端用、要在无界面环境用直接把核心类拷贝过去就行。界面展示包括参数配置区、数据监控区、发送区和日志区。还有状态栏显示连接状态和收发字节数。界面这块我建议用WinForms不是说WPF不好而是串口助手这种偏工具型软件WinForms开发效率更高部署也更轻量一台工控机上跑起来毫不费力。1.3 为什么选择C#和SerialPort而不是其他方案做串口调试工具技术选型有几种路线C Win32 API、Python PySerial、C# System.IO.Ports还有基于Electron的Web串口方案。C方案性能最好但开发效率太低特别是在UI这一块。写一个稳定好用的串口工具C的调试周期是C#的好几倍。Python方案脚本写起来快但打包部署是个坑更重要的是Python的串口库在处理高频率数据时表现一般而且UI部分一直比较弱。Electron那套纯粹是拿前端的思维做桌面工具资源占用高串口授权也麻烦。C#的System.IO.Ports.SerialPort是我在多个项目里实际比较后的最终选择。它的API设计很直观一个串口设备在代码里就是一个对象属性设置完直接Open()就能通信。EventDriven的接收模式非常适合桌面应用——你不用自己轮询缓冲区系统会在数据到达时通过事件通知你这个机制在多数场景下响应速度完全够用。我见过一些人批判SerialPort内部封装太厚、性能不如Win32 API直调。这话在理论上有道理但在串口115200甚至921600波特率的场景下SerialPort的吞吐能力是绰绰有余的。真正需要关心的是怎么处理数据而不是纠结那几微秒的性能损耗。2. SerialPort核心API的坑与细节2.1 属性设置里藏着的门道SerialPort的构造和属性设置表面上看就是几个下拉框的事但每个参数背后都是有讲究的。波特率BaudRate是每秒传输的比特数常见的有9600、19200、115200。很多人只是机械地选择不理解它和数据量之间的关系。这里有个简单的估算公式有效数据字节数约等于波特率除以10。为什么是10因为串口传输一帧数据是起始位1位 数据8位 停止位1位加起来正好10位。也就是说9600波特率下每秒最多传输960个字节。如果你要和设备通信的数据量比较大比如每10毫秒上报一次20字节的传感器数据那波特率就要选115200甚至更高不然数据根本来不及传。数据位DataBits一般选8位这是现代串口通信的事实标准。有些老旧设备协议会用到7位数据位比如MODBUS协议在某些PLC上有这种变种这种情况按设备文档来别想当然。停止位StopBits选One也就是1位停止位。有些设备手册会要求Two特殊场景再改常规一律Default。校验位Parity是串口通信里最容易出问题的环节。None是最常用的因为现在的通信协议基本都在应用层做了CRC校验不需要物理层的奇偶校验。但如果你在调试一些老设备它固件里写死了Even校验你这里选None收到的数据就会是乱码——而且这种乱码不是那种很明显的错误码而是偶尔某个字节不对非常难排查。所以接到一台新设备的时候如果数据异常第一个要查的就是校验位设置。超时ReadTimeout和WriteTimeout这个属性我建议设置一个合理的值比如500毫秒。如果不设置默认是InfiniteTimeout这在同步读取串口数据的时候会导致程序卡死在那里。你以为串口通信挂了实际上就是读超时在作祟。2.2 DataReceived事件的正确打开方式SerialPort的DataReceived事件是串口助手的灵魂但这个事件也是新手最容易踩坑的地方。它运行在独立的线程池线程上不是在UI线程里触发的。这意味着什么意味着你在事件处理函数里直接操作文本框、进度条这些UI控件程序就会抛出跨线程操作异常。很多初学者在这个异常面前一头雾水明明代码看着是对的啊。正确的做法是用BeginInvoke把UI更新操作封送到UI线程上执行。但这里又引出一个性能问题——如果你的接收数据处理很频繁每收到几个字节就BeginInvoke一次UI线程会被高频的委托调用淹没界面就会给人一种卡卡的感觉。我的做法是做一个简单但有效的缓冲池。DataReceived事件触发后先把数据追加到内部缓冲区同时用一个标记位表示缓冲区有数据了真正更新界面是在一个自定义的UI刷新周期里统一完成的。实测下来在高速数据流下界面依然丝滑。还有一个很多人不知道的坑DataReceived事件在串口打开之后不一定会在数据到达的第一时间触发有时候会有几毫秒的延迟。如果你要在串口通信里做非常严苛的时序控制比如设备发送数据后你需要在1毫秒内响应那SerialPort原生的事件模式可能撑不住需要改成用定时器主动去读缓冲区。我在源码里两种模式都写了注释里面标得很清楚。2.3 发送操作的线程安全发送操作看起来比接收简单就是一句serialPort.Write()的事。但如果你在多个线程里同时调用发送就会遇到SerialPort内部缓冲区竞争的问题轻则数据乱序重则直接抛异常。我在源码里对发送做了串行化处理用一个队列接收所有发送请求由一个专门的发送线程挨个取出并执行。这么做还有一个额外的好处——你可以很方便地统计发送字节数还可以在发送前做数据格式化处理。发送缓冲区的大小也值得关注。SerialPort.WriteBufferSize默认值是4096字节如果你的协议帧超过了这个长度需要改大。我在实际项目中遇到过一条GPS报文长达3KB的情况默认缓冲区就够用但如果你要发送固件升级包这类大数据块要么把缓冲区设为64KB甚至更大要么自己做分包发送。我分包这一块的经验是一次发不超过1KB设备端收到的数据会稳定得多。2.4 十六进制和文本的互相转换串口调试工具必须支持十六进制显示和发送这是硬需求。原因很简单很多设备上报的数据是二进制帧不是ASCII文本。如果你按文本显示有些字节是显示不出来的你看到的全是乱码。十六进制转文本的代码不难写就是按空格或者连续取每两位字符转成一个byte。但这里面有个细节——非法字符处理。我在源码里写了严格的校验逻辑如果用户输入了非法的十六进制字符比如GG就直接拦截并提示错误不发给设备。看似小细节实际能帮你避免很多莫名其妙的通信故障。二进制数据转可读显示也是嵌入式工程师的刚需。我是把每个字节先转成十六进制然后配合ASCII码表把可打印字符0x20到0x7E之间的字节显示成对应字符其他字节显示成点号.。这种显示方式在做协议分析的时候非常直观你能同时看到二进制数据和字符串数据一眼就能定位问题所在。3. 从零构建一个完整的串口助手源码实战3.1 项目结构与界面布局这个项目我用了Visual Studio 2022 .NET Framework 4.8目标平台x86。为什么不用.NET Core/.NET 5因为串口助手经常要在工控机上跑老工控机上什么运行库都没有.NET Framework是Windows系统自带的最省心。如果你要跨平台部署改成.NET Core也不难核心代码不用动。项目结构很清晰三个核心文件SerialPortManager.cs——串口通信核心类负责打开关闭、收发数据、错误处理MainForm.cs——主界面负责用户交互和数据展示ByteHelper.cs——字节转换工具类负责字符串和字节数组的互相转换界面布局上我用了一个TableLayoutPanel做整体排版上半部分是串口参数配置和连接控制中间是数据接收显示区下面是发送区。菜单栏放了几个常用功能清空显示、保存日志、时间戳开关。状态栏实时显示连接状态和收发统计。这里有一个设计上的考量接收显示区我用了RichTextBox而不是TextBox。区别在于RichTextBox可以轻松改变部分文字颜色比如接收数据用黑色错误信息用红色警告信息用橙色调试时一目了然。这个功能在排查通信异常时帮了我大忙。3.2 串口扫描与自动识别串口助手的第一个功能不是打开串口而是找到有哪些串口可用。SerialPort.GetPortNames()方法可以返回当前系统的串口列表但这个列表是静态的设备拔插之后不会自动更新。我在源码里做了一个定时扫描机制每隔1秒调用一次GetPortNames()和当前的串口列表做对比如果有变化就刷新下拉框。这样你插上USB转串口设备工具能立刻识别到。别小看这个功能在调试环境里频繁插拔USB转串口模块是常态。还有一个小细节是串口名称的解析。Windows系统里USB转串口的名称可能是COM5或者USB-SERIAL CH340 (COM7)这种带描述的格式。GetPortNames()返回的纯COM编号列表但Win32 API可以拿到更详细的信息。我在源码里加了P/Invoke调用直接读取注册表里的友好名称显示成COM7 - USB-SERIAL CH340一眼就知道哪个设备对应哪个端口。3.3 核心代码实现解析串口通信核心的代码其实不长但每一行都有它的道理。打开串口的过程如下public bool OpenPort(string portName, int baudRate, int dataBits, StopBits stopBits, Parity parity) { try { if (serialPort ! null serialPort.IsOpen) serialPort.Close(); serialPort new SerialPort(portName, baudRate, parity, dataBits, stopBits); serialPort.ReadTimeout 500; serialPort.WriteTimeout 500; serialPort.ReceivedBytesThreshold 1; serialPort.DataReceived SerialPort_DataReceived; serialPort.ErrorReceived SerialPort_ErrorReceived; serialPort.Open(); return true; } catch (UnauthorizedAccessException) { Logger.LogError(串口被占用请检查是否有其他程序正在使用该端口); return false; } }这里要说一下ReceivedBytesThreshold属性它决定触发DataReceived事件的最小字节数。默认是1意味着至少收到1字节就触发事件。如果你希望攒够一定字节再处理可以改大比如设成64。但在通用工具里我保持1因为设备协议千奇百怪不能假设所有设备都会一次发足够多的数据。接收处理的代码是核心中的核心private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e) { try { int bytesToRead serialPort.BytesToRead; if (bytesToRead 0) return; byte[] buffer new byte[bytesToRead]; int bytesRead serialPort.Read(buffer, 0, bytesToRead); if (bytesRead 0) return; lock (receiveLock) { int newBytes buffer.Length; totalReceivedBytes newBytes; // 按协议规则做帧检测和拼接 ProcessReceivedData(buffer, bytesRead); } } catch (Exception ex) { Logger.LogError($接收数据异常: {ex.Message}); } }需要说明的是bytesToRead和实际Read返回的bytesRead可能不一致。这是SerialPort的一个特性——BytesToRead属性告诉你缓冲区里有几个字节等待读取但Read操作不一定能一次全部取出有可能只读出一部分。所以代码里必须用bytesRead来截断有效数据否则buffer尾部会有无效的填充字节。这是我在项目里真实踩过的坑一开始没注意结果每条数据后面都多了几个零字节设备端一直报校验错误排查了半天才发现是这个原因。3.4 数据帧的缓存与拆分接下来是串口助手的核心价值所在——数据完整性处理。前面说过串口是流式协议数据会分片到达。假设设备发送一帧完整数据是AA 01 02 03 04 BB可能第一次DataReceived触发时只到了AA 01第二次到了02 03第三次才到04 BB。如果你直接把每次收到的数据都作为一帧去处理协议解析就全乱了。我的解决方案是维护一个Listbyte作为数据缓存区每次收到数据先追加进去然后尝试在缓存里查找完整的数据帧。识别完整帧靠协议头尾标记大部分设备协议都有固定的帧头和帧尾。private void TryParseFromBuffer(Listbyte cache) { while (true) { // 查找帧头 int headerIndex FindPattern(cache, header); if (headerIndex 0) { // 缓存里没有帧头丢弃最先进入的1个字节继续找 if (cache.Count 0) cache.RemoveAt(0); if (cache.Count 0) return; continue; } // 从帧头位置检查帧长是否足够 if (cache.Count - headerIndex fullFrameLength) { // 数据不够一帧继续等 return; } // 检查帧尾和校验 byte[] frame cache.GetRange(headerIndex, fullFrameLength).ToArray(); if (CheckChecksum(frame)) { OnDataFrameReceived(frame); cache.RemoveRange(0, headerIndex fullFrameLength); } else { // 校验失败跳过帧头1个字节继续找下一帧 cache.RemoveAt(headerIndex); } } }这段逻辑看起来简单但确实解决了很多实际问题。老式的串口工具经常出现数据粘包、半包问题主要原因就是没做缓冲和拆包处理。用了这套机制后不管设备是20字节一帧还是200字节一帧只要协议里有帧头帧尾帧数据就能完整准确地提取出来。3.5 异步UI更新与性能优化跨线程更新UI是C#上位机开发的经典问题。我之前见过有人用Control.CheckForIllegalCrossThreadCalls false关闭跨线程检查来绕过这绝对是错误的做法只是把异常掩盖了实际的线程安全问题还在。正确做法是用BeginInvoke但要注意控制调用频率。我在源码里做了这样一个优化接收数据先把字节数累计起来用一个System.Windows.Forms.Timer定时器每隔50毫秒更新一次收发字节数和接收内容显示。这样即使一秒钟收到几千个字节界面也只会刷新20次性能压力小很多。private void timer_ui_Tick(object sender, EventArgs e) { label_recv_bytes.Text totalReceivedBytes.ToString(); label_send_bytes.Text totalSentBytes.ToString(); if (pendingDisplayData.Count 0) { string dataToShow; lock (receiveLock) { dataToShow string.Join(, pendingDisplayData); pendingDisplayData.Clear(); } AppendReceiveText(dataToShow); } }这里还有一个大坑要提醒RichTextBox在追加大量文本时会越变越慢。原因很简单——控件里的文本越来越多每次追加都要重新排版。我实测过RichTextBox里的文本超过10万字符之后每追加一次文本要卡顿几十毫秒。解决方案是限制显示长度比如超过5万字符就自动清掉前一半。我见过一些成品串口工具也有这个毛病数据刷一会儿整个界面就卡死了。3.6 日志保存与时间戳功能串口调试如果只是实时看数据那和没调试差不多。真正有用的日志是能保存下来的而且是有时间戳的。时间戳功能看起来简单但涉及一个格式问题。我提供了两种格式一种是普通模式时间戳单独一行显示[2025-01-15 14:23:45.123]另一种是数据前带时间戳比如[14:23:45.123]FF 01 02...。前者适合分析整个通信过程后者适合快速对齐收发时序。日志保存我做了两种手动点击保存以及按日期自动保存。自动保存方便做长时间运行的设备测试每天一个文件文件按Log_20250115.txt的格式命名查找起来非常方便。4. 常见问题与排查技巧实录4.1 为什么我的串口助手接收不到数据这是我在各个技术社区被问到最多的问题。每次遇到这个情况我的排查步骤都是固定的。先确认串口有没有被正确识别。看设备管理器里面有没有未知设备如果有大概率是USB转串口的驱动没装好。CH340、CP2102这些芯片的驱动经常出问题重新装一遍驱动基本能解决。再检查串口号选择。USB转串口每次插拔可能分配不同的COM号上一次是COM5这次可能变成COM6。都要在串口助手界面确认选的是当前这个设备的COM号。还要确认波特率、数据位、停止位、校验位参数和设备端的配置完全一致。这个我在前文已经强调过很多次但依然有大量的人栽在这个问题上。把设备的通讯参数手动发一份给设备厂家确认比自己瞎猜靠谱得多。还有一种情况是物理连接问题。TXD和RXD交叉连接了吗如果直接用USB转TTL模块接设备模块的TXD要接设备的RXD模块的RXD要接设备的TXD。很多人用直连去接当然收不到数据。GND也必须连接而且最好是共地。4.2 数据乱码的有效排查链路乱码问题分两种彻底乱码和偶尔乱码。彻底乱码屏幕上全是奇奇怪怪的符号通常是编码格式不匹配。发送端用的是ASCII文本接收端却按GBK去解码或者发送端是UTF-8接收端按ASCII读中文必然乱码。解决思路是确定设备发送的数据是什么编码。绝大多数嵌入式设备默认是ASCII或者GBK少数新设备会配置成UTF-8。一块一块试总能找到对的。偶尔乱码或者某些字节不对情况复杂一些。最大嫌疑是波特率误差。如果设备实际晶体频率和标准波特率有偏差数据帧就会出现位错误。这种问题在国产单片机上比较常见便宜的晶振或者内部RC振荡器导致时钟不准。解决办法是尝试用稍低一点的波特率或者请求更换设备侧的晶振。还有一个容易被忽略的点接地不良。串口通信和信号完整性有关USB转串口模块和数据设备之间的地线接触不良会导致信号电平不稳出现偶发乱码。插拔一下接口、换个USB口有时候就莫名其妙好了。4.3 打开串口时报串口被占用串口被占用是Windows下非常经典的报错原因是串口是独占资源同一时刻只能有一个程序打开。你打开串口助手之后别的软件想再打开同一个COM口就会被拒绝。排查这个问题的步骤是先关掉其他可能占用串口的工具比如你之前打开过的另一个串口调试助手、设备厂家的配置软件、PLC编程软件等等。然后检查后台进程如果之前程序没有正常退出有残留进程占着串口直接在任务管理器里结束掉。如果用了上面的方法还是报占用可能就是SerialPort对象没有被正确释放。我在源码里做了完整的资源清理逻辑在窗体关闭时强制调用Close()并释放资源。这个操作看似简单但很多从网上搜的代码都不注意这一点导致串口对象被GC回收前一直占着端口。4.4 上位机软件卡死和内存膨胀用串口助手跑十几个小时内存涨到几个GB界面操作卡顿这是长时间运行中容易碰到的问题。根因通常是两个显示控件没有限制长度以及日志没有做滚动删除。我在前面提到过RichTextBox限长保存这里再具体说说。限制长度不是简单的不显示而是真正把旧数据从内存里释放掉。如果只是截断显示而数据还存着内存一样会涨。我采取的方式是当文本长度超过设定上限时用Remove方法把最早的一半文本删除掉。日志写入的滚动处理也是同理。自动保存的日志不是无限增长的按日期分文件已经是一种天然的滚动机制。如果做单文件长期写入要在写入时检查文件大小超过比如10MB就把旧的备份成_1后缀文件再开新文件写入。4.5 高速通信时的数据堵塞当波特率上了921600甚至1.5M以后串口数据接收的挑战就大起来了。DataReceived事件的触发频率会非常高如果每次事件里都要做大量字符串操作处理不过来就会丢数据。我的经验是在高波特率场景下走原始字节路线——接收线程只把字节存进环形缓冲区不做任何字符串转换。UI线程从环形缓冲区取原始字节再做显示所需的转换。这样接收线程的任务非常轻主循环不会被阻塞不容易丢数据。如果还是丢数据需要检查USB转串口模块本身的能力上限。很多廉价CH340模块在高速率下并不稳定换一个FTDI芯片的模块或者原厂USB转串口稳定性立竿见影。这不是崇洋媚外是芯片本身设计的可靠性和驱动成熟度决定的。重要提示这里一定要区分上位机处理速度和硬件收发能力两个概念。如果你的串口模块本身就不支持921600不管代码怎么优化数据依旧会丢。先确认硬件能力再优化软件逻辑方向别搞反了。5. 源码扩展与二次开发建议5.1 从串口助手到协议调试器如果你做的是纯Modbus调试通用串口助手还是不够用你需要一个带协议解析的调试器。我这份源码的架构可以很方便地扩展这个功能——在数据帧解析层加一个Modbus RTU解析器根据功能码识别帧类型把设备地址、寄存器地址、数据值一条条解析出来。这类串口协议调试器在实际调试中的效率提升是巨大的。你在看Modbus数据的时候不用再去对照协议文档查每个字节是什么意思界面直接告诉你设备地址01功能码03起始寄存器0000数据长度10数值: 25mA。这才是真正意义上的调试工具。5.2 串口数据转发与Socket桥接我做过好几个物联网采集项目设备在车间服务器在机房中间隔着网络但设备只支持串口通信这时候就需要一个方案把串口数据转成网络数据。这个需求用这套源码扩展起来非常方便——把接收数据帧原样转成TCP/UDP包通过网络发出去反过来接收网络包转成串口数据写入串口。做这个功能的时候要注意两个问题一是数据帧拆包逻辑在串口侧是必要的在网络侧也是必要的两边都要做好缓冲和拆包二是网络抖动会导致乱序加个简单的序号或者校验能帮你快速定位传输问题。5.3 自动化测试脚本支持调试设备的时候经常要做重复性测试——给设备发一段指令等它回复验证结果。这套动作如果是手动操作一天重复几百次人会崩溃的。我在源码的扩展计划里就有一个脚本引擎。可以用简单的C#脚本定义测试流程发送指令、等待回复、比对数值、输出结果。我把这个功能做成插件式——你只需实现一个ITestCase接口返回PASS或FAIL主框架负责执行和汇总。这样串口助手就从单纯的调试工具进化成了自动化测试平台。6. 写在最后的几个操作心得这份C#串口助手源码是我在多个实际项目中不断迭代打磨出来的副产品。我见过太多人在串口通信上走弯路所以特意把那些文档里没有的细节都留在了代码注释和这篇文章里。我记得第一次带徒弟做上位机他在DataReceived事件里直接操作UI程序一跑就崩溃一脸无助地看着我。我说这个问题网上有无数人问过但你别急着CtrlC、CtrlV你把它理解透了以后就再也不会踩了。第二天他能完整地讲清楚跨线程通信的基本原理了之后确实再也没在串口这个方向上栽过跟头。串口通信看着简单深入研究下去要学的东西其实不少。线程模型、缓冲区管理、协议解析、信号完整性每一个都是实打实的基础。这套源码只是一个起点背后的原理才是真正值钱的部分。如果你在用的过程中遇到问题或者有更好的优化思路欢迎一起交流。做技术的乐趣就在于碰到一个坑、填一个坑然后把这个坑的经验分享给同样走在这条路上的人。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价