1. 为什么是C# + HALCON:这套组合的定位与架构思路
1.1 核心需求拆解
我第一次把C#和HALCON放在一起做视觉项目,大概是在一个PCB板定位项目上。当时需求很明确:产线上有两台相机,要抓拍工件上的mark点,算偏移量,再把结果通过modbus TCP发给PLC调整机械手抓取位置。前期调研时对比过OpenCV、VisionPro、HALCON几套方案,最终选了HALCON,原因很直接:shape-based matching的鲁棒性、算子库里沉淀了几十年的工业测量经验、文档和例程完整,而且对相机SDK的兼容度做得比较好。
语言这一层,我选了C#而不直接用HALCON自带的HDevelop或C++,主要是考虑到几个现实问题:上位机界面要做数据展示、参数配置、用户权限管理,这些用C#的WinForm或WPF来做效率远高于C++;团队里维护软件的工程师不一定都是视觉算法出身,C#的开发门槛低,后续交接成本也小。HALCON本身提供.NET接口,C#可以直接调用它的HOperatorSet类,这就形成了一个分工明确的架构:HALCON聚焦图像处理和算子计算,C#负责业务流程、界面交互和硬件调度。
这套组合真正能称为“成熟模板”,关键不在于跑通一个demo,而在于把链路打通:相机取流、图像预处理、模板匹配或测量、结果判定、数据输出、异常恢复、日志记录。每一个环节在HDevelop里单独验证都很简单,但放进C#的上位机程序里,就要考虑线程、内存释放、卡顿、连续运行稳定性一类的问题。顺序颠倒一下,很多项目会在联调阶段反复返工,所以模板从一开始就要围绕“工程可用”来搭,不是围绕“算法能出结果”来搭。
1.2 整体架构与技术选型
我搭的这种C# + HALCON视觉项目模板,整体结构通常分四层:
- 界面层:WinForm或WPF,负责参数录入、图像显示、结果查看、报警提示。
- 业务逻辑层:承担流程控制,比如触发信号来了调相机采集、采集完调视觉处理、处理完把数据写到PLC或数据库。
- 视觉算法层:封装HALCON算子,对外只暴露“传入图像、返回结果”的方法,不把HOperatorSet的细节泄露到上层。
- 硬件抽象层:相机、光源、PLC、IO板卡的驱动和通讯统一封装,方便换硬件时不影响上层代码。
选型上还要考虑HALCON的版本和C#的.NET框架版本匹配。就我目前用的版本组合,HALCON 20.11以上对.NET Core 3.1和.NET 5/6的支持已经很稳,如果项目不出差,我用.NET Framework 4.8配合HALCON的x64运行时,部署起来省心,老工业电脑也兼容性好。新项目我倾向上.NET 6及以上的WinForm,启动快、依赖清爽,但在没有Visual Studio环境的目标机器上发布时,记得带上对应运行时。
这套模板的优势在于:视觉算法和业务代码解耦后,项目里换算法、换相机、换PLC通讯协议,各自都只影响自己那一层。以前接一个项目要一两周,用模板之后,大部分时间花在标定和调试参数上,整个软件框架基本是搬过去直接用的。
1.3 模板化设计能给项目带来什么
一个“成熟视觉项目”的开发流程,很大一部分是重复劳动:图像采集要写、ROI选择要写、结果显示要写、参数保存要写。每开一个新项目,如果从零开始,就免不了把上一套代码里的bug带过来一遍。模板化的最大价值是沉淀。
我的模板里会固定几个约定:所有视觉参数用统一的数据结构保存(比如模板文件路径、匹配分数阈值、角度范围、缩放范围);所有结果封装成统一的Result类,里面包含位置坐标、分数、耗时、图像路径、判定结果;所有日志走同一个接口,生产环境出问题能按时间线快速回溯。这些约定一旦固定下来,哪怕项目功能差异很大,调试路径都是一样的。
从代码复用率来看,模板搭好以后,视觉项目里大约六成代码是直接复用,两成改改配置,两成写新逻辑。开发周期缩短,稳定性反而更好——因为被反复用过的代码,边界条件早就磨平了。接下来我先把环境搭建和工程配置讲清楚,这部分卡住的话,后面全都是空谈。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境搭建:从HALCON许可到C#引用的完整链路
2.1 HALCON版本与安装方式选择
HALCON的安装分两种:试用版和正式授权版。试用版必须在联网状态下启动,每次打开HDevelop都要验证license,功能绝大部分能用,跑项目验证算法完全没问题;正式授权又分本地加密狗和license文件两种,生产环境部署时用license文件更灵活,只要把license路径配置好就行。
从安装过程看,官方安装包会同时装三样东西:HDevelop(交互式开发环境)、运行时(Runtime)、不同语言的接口文档和示例。目前HALCON的安装界面很傻瓜化,一路Next就可以,但有一个地方要留意:选择安装位数时务必统一用64位。工业相机SDK、HALCON、C#编译目标、外部DLL,任何一环出现32位/64位混用,运行时会报莫名其妙的BadImageFormatException或找不到DLL。
HALCON版本迭代中,比较关键的是版本号跨度不要太大。比如我项目里用的20.11,如果要升级到24.x,不光DLL路径从C:\Program Files\MVTec\HALCON-20.11\bin\x64-win64变成新版本对应路径,有些算子参数和标定文件格式也变过。所以上线之后尽量锁版本,不要为“尝鲜”升级。
2.2 在C#项目中正确引入HALCON
安装完成后,新建一个C# WinForm项目,引入HALCON有两条路:
- 直接添加引用
halcondotnet.dll,在项目里using HalconDotNet;,然后就能用HImage、HObject、HOperatorSet这些类。 - 从NuGet包管理器搜索
MVTec.Halcon(官方没有统一发布,通常是内部源或手动引用),个人建议还是手动引用本地DLL,路径最可控。
这里有个新手常踩的坑:halcondotnet.dll通常在...\bin\dotnet35或...\bin\dotnet\目录下,不同版本位置不一样。你要选和项目目标框架匹配的那个版本,比如.NET Framework 4.8,我一般选dotnet35目录下的DLL,实测兼容;.NET 6则用dotnet目录下的。引用之后记得在项目的App.config或运行时环境里配置HALCON根目录,或者更省事的办法:把HALCONROOT和HALCONARCH环境变量设好。
如果程序启动时报找不到hdevengine.dll这类原生DLL,不是引用配置错了,而是运行时环境变量没生效。在程序入口处可以显示设置:
csharp复制Environment.SetEnvironmentVariable("HALCONROOT", @"C:\Program Files\MVTec\HALCON-20.11");
Environment.SetEnvironmentVariable("HALCONARCH", "x64-win64");
这两行不用也行,前提是安装时勾选了“加入系统PATH”。但工业现场电脑有时会装精简系统,环境变量可能被优化掉了,主动设置一次最稳。
2.3 许可证与运行时分发
正式发布的视觉软件,目标电脑上不需要装完整的HALCON开发环境,但需要装Runtime。Runtime安装包在安装目录下叫HALCON-xx-Runtime.exe之类,或者说你可以直接把开发机的bin目录和license文件一起打包。要更干净的做法:在安装工程里加一个自定义操作,静默安装Runtime:
code复制HALCON-20.11-Runtime.exe /S /VERYSILENT /SUPPRESSMSGBOXES
license文件一般叫license.dat,路径默认读的是%HALCONROOT%\license\。如果程序在客户那边报“License not found”或者“No more licenses available”,先检查这个文件是否在正确目录,再看有没有被杀毒软件隔离。我在部署阶段遇到过好几次,HALCON的license加密机制偶发会被安全软件误判,要在白名单里加上安装目录。
一句话总结环境配置的经验:开发机上跑通只算第一步,部署包要做到“放到任何一台干净没装过HALCON的机器上,双击就能跑”,才叫真正的模板合格。
3. 核心环节:HALCON模板匹配在C#中的落地
3.1 模板匹配的基本流程
HALCON里模板匹配有个标准套路,不管你是做定位、对位还是检测,都是同一个骨架,HDevelop里顺手写下来就是:
c复制* 读取图像
read_image (Image, 'test.png')
* 选定模板区域
gen_rectangle1 (ROI, 100, 100, 300, 300)
reduce_domain (Image, ROI, ImageReduced)
* 创建形状模板
create_shape_model (ImageReduced, 'auto', -10, 20, 'auto', 'auto', 'ignore_local_polarity', 5, ModelID)
* 保存模板文件
write_shape_model (ModelID, 'model.shm')
创建模板后,在实际程序里就可以用find_shape_model在任意图像中搜索目标:
c复制find_shape_model (Image, ModelID, -10, 20, 0.7, 1, 0.5, 'least_squares', 0, 0.9, Row, Column, Angle, Score)
这里每个参数都有实际含义:角度起始-10和角度范围20表示允许匹配到的目标在-10°到+10°之间旋转;最小分数0.7是匹配相似度的阈值;最大重叠0.5决定多个目标时允许的重叠程度;显示结果时要把匹配到的坐标、角度、分数输出到界面。
在C#里调用时,最容易被忽略的是内存问题。HALCON对象(如图像、区域、轮廓)在托管代码里虽然会被GC回收,但HALCON底层原生内存不完全是GC管理的,频繁创建HImage和HObject又不释放,跑几个小时内存就往上蹿。我的习惯是每次处理完,对临时对象调用Dispose(),或者用using语句,配合定期GC.Collect()——虽然不推荐强行调用GC,但工业软件连续运行场景里,我实测定期回收是有效的。
3.2 提高模板匹配成功率的几个关键参数
很多朋友在项目里问得最多的就是“模板匹配老找不到”或者“找到了但位置不稳”,这往往不是代码问题,是参数和模板质量的问题。
先说模板本身。模板创建之前,图像要做合适的预处理,尤其光照不稳的场景,可以考虑先高斯滤波或均值滤波去掉噪点,再做invert_image把极性统一。在工业现场,最好的模板图像是实际生产环境下拍出来的,而不是用实验室干净图。因为模板里的背景噪声、边缘模糊程度如果和现场不一致,匹配分数会明显下降。
再说创建模板时的参数。num_levels(金字塔层数)不建议一律填auto。大目标在低分辨率层就能锁定,小目标需要更多的金字塔细节,如果给的层数太多,小目标会在顶层被抹掉导致匹配失败。optimization参数用auto没问题,contrast建议根据目标与背景的对比度调节,对比度低的场景选'low'能提高检出率。
角度范围也要贴合实际。好多人为了“保险”把角度范围设成-180到180,这会带来两个问题:一是搜索空间大,耗时翻几倍;二是模板对称性不强时,容易把相似形状误判成目标。正确做法是依据机械安装场景,限位转机只有±5°,你设成-10到10就够了。
匹配阶段还有几个细节:
MinScore不要设得太低。0.7算是个分界线,低于0.6的时候误匹配概率会明显上升,宁可漏检也别乱检。Greediness是“搜索贪婪程度”,取值0到1,越大越快但越容易跳过正确解。调试时可以先用0.3或0.5,稳定后逐步调高到0.8以上提升速度。SubPixel选择'least_squares'虽然耗一点时间,但精度比'interpolation'高不少,定位类项目建议保留。
这些参数在HDevelop里调好后,直接导出C#代码,再结合项目结构调整,比在C#里盲改参数靠谱得多。我一般习惯在HDevelop里把流程和参数全部确定,再导到C#工程里封装,这样能把算法调试时间和业务代码调试分开。
3.3 图像采集与显示循环的C#封装
图像从相机到HALCON,一般是通过HALCON自带的接口,比如HFramegrabber。使用前需要在HDevelop里先open_framegrabber测试相机型号、接口、参数,确认能取到图,再拷贝到C#。
csharp复制HFramegrabber grabber = new HFramegrabber(
"GigEVision", 0, 0, 0, 0, 0, 0, "default",
-1, "default", -1, "false",
"camera_serial", "0", 0, -1);
HImage image = grabber.GrabImage();
这样取到的HImage对象可以直接传给find_shape_model。但要注意,GigE相机触发模式如果设成软触发,每次采集前要调用一次grabber.SetFramegrabberParam("trigger", "software")再TriggerSoftware(),不然图像不会更新。
显示循环是另一个容易卡死的地方。直接在UI线程里GrabImage会阻塞界面,鼠标都拖不动。正确的做法是采集线程和显示分离:后台线程不断抓图、执行视觉处理,把结果封装好后通过Invoke或BeginInvoke安全传到UI线程更新HWindowControl。千万别在UI线程里执行视觉算法,HALCON的算子重起来能卡几毫秒到几十毫秒,界面必然无响应。
我后来直接做了个VisualProcessor类,把“取图 → 处理 → 回调结果”整个流程包成事件驱动模式:
csharp复制public class VisualProcessor
{
private System.Threading.Thread workerThread;
private bool isRunning = false;
private HImage snapshot;
public event Action<VisualResult> ProcessFinished;
public void Start() { /* 启动采集与处理线程 */ }
public void Stop() { /* 停止并释放资源 */ }
private void WorkerLoop()
{
while (isRunning)
{
HImage image = grabber.GrabImage();
// 调用模板匹配
HTuple row, col, angle, score;
HOperatorSet.FindShapeModel(image, modelID, -10, 20, 0.7,
1, 0.5, "least_squares", 0, 0.9, out row, out col,
out angle, out score);
// 封装结果并触发事件
ProcessFinished?.Invoke(new VisualResult(row, col, angle, score));
}
}
}
这样上层界面只关心ProcessFinished事件,逻辑清晰,也方便以后把视觉处理扩展成独立服务。
4. 上位机功能扩展:从测量到缺陷检测
4.1 测量类算子的C#调用
模板匹配解决的是“找没找到”和“在哪”的问题,测量则是“尺寸是否合格”。HALCON的测量算子体系也很成熟:measure_pos、measure_pairs、metrology_model是常用的三类。在C#里应用比较典型的是对卡尺测量直线间距、圆的直径或者圆孔的位置度。
用metrology_model做测量时,流程是:
create_metrology_model创建一个计量模型。add_metrology_object_circle_measure添加要测量的圆,指定圆心初值和半径初值。apply_metrology_model执行测量,得到精确尺寸。get_metrology_object_result取出测量结果,比如半径、直径、圆心坐标、拟合误差。
这段逻辑在HDevelop里调试好导到C#里,注意的点是:添加计量对象时给的初值如果偏差太大,测量会失败或拟合到错误边缘。所以生产流程里,测量之前通常先跑一遍模板匹配锁定目标位置,再用锁定的坐标作为计量模型的初值,这样稳定性非常明显。
csharp复制HTuple model = new HTuple();
HOperatorSet.CreateMetrologyModel(out model);
HTuple circleHandle = new HTuple();
HOperatorSet.AddMetrologyObjectCircleMeasure(model, row, col, radius,
20, 5, 1.5, 30, new HTuple(), new HTuple(), out circleHandle);
HOperatorSet.ApplyMetrologyModel(image, model);
HTuple measuredRadius;
HOperatorSet.GetMetrologyObjectResult(model, circleHandle, "all",
"result_type", "radius", out measuredRadius);
这套组合拳在许多3C装配场景里用得很广,比如手机摄像头模组的同心度检测、结构件定位孔的孔径测量。要注意把“标定”也纳入模板,生产现场的像素当量通常是每像素对应多少毫米,换镜头或调焦距后,一定要重新标定,否则测量结果形同虚设。
4.2 基于深度学习的缺陷检测
传统方法做缺陷检测,难点在于缺陷形态千变万化,靠固定阈值和几何特征很难覆盖全。HALCON从17版开始就带深度学习模块,训练分类、分割、目标检测模型。DLTool是离线训练工具,训练好的模型通过HOperatorSet推断,流程是:
- 用
read_dl_model读取训练好的.hdlm模型。 create_dl_dataset创建推理数据集。apply_dl_model执行推理,得到分类标签或分割区域。- 解析结果,把置信度和缺陷区域显示出来。
C#里最省心的做法是:在DLTool里完成标注、训练、评估,然后导出模型文件,再写C#调用。训练环节其实不需要懂太多机器学习原理,但标注质量决定了模型上限——缺陷样本不够、标注边界不精确,得出来的模型在实际线上会有明显误检。我见过一个电池表面划痕检测项目,训练时只用了几百张图,现场一跑误检率超过20%,后来补到五千多张,并且保证样本里有不同角度、不同亮度下的划痕,误检率才压到3%以下。
深度学习模型在推理阶段占用的CPU或GPU资源和传统算子不是一个量级。CPU推理一张512×512图可能要50~100ms,显卡上可能只要10ms。如果产线节拍要求高,建议在C#里把模型推理放到单独的线程,同时考虑用GPU推理来换取更低的延迟。HALCON对CUDA的支持很透明,配置好显卡驱动和CUDA库,apply_dl_model会自动走GPU。
4.3 数据管理与其他功能
一个视觉项目除了“看”,还要“记”。保存检测结果和原始图像,是生产追溯的基本要求。我在模板里固定了数据保存的结构:
- 每次检测结果记录一条JSON,包含时间戳、产品条码、检测项结论、测量值、图像编号。
- 原始图像按日期归档,保存位置按
/data/2025-06-10/000123.bmp这种格式。 - 所有记录写入SQLite或SQL Server,具体看现场要求,SQLite适合单机,SQL Server适合车间多台设备集中管理。
图像保存方面有个细节:HALCON的write_image保存时格式选择要注意,保存成JPG会压缩、有损,常规检测我全用BMP或PNG,而确实需要留底且尺寸大时,用TIFF可以带LZW无损压缩。还有,保存前最好把检测结果绘叠加到图像上再存一份“标注图”,这样事后查问题直接看图,不用翻数据库。
如果现场需要把结果传给PLC或MES,模板里封装了Modbus TCP、TCP/IP Socket、OPC UA几种通讯方式。Modbus TCP在PLC场景最常见,把检测结果写到特定寄存器区;MES对接一般用Socket发JSON文本,或者用OPC UA读写节点。通讯失败要设计重试机制和报警,产线上通讯偶发超时很正常,不做好重试和提示,半夜机器一停就是事故。
5. 工程化要点:程序集结构、多线程与安装包
5.1 把视觉逻辑封装成类库
模板的工程结构我建议拆成多个程序集,而不是全堆在WinForm项目里。比如:
Vision.Core:所有HALCON相关代码,包括相机取流、模板匹配、测量、深度学习推理。Vision.Models:数据模型,比如DetectResult、CameraConfig、ModelParam。Equipment.Hardware:相机、PLC、IO板卡的驱动封装。Equipment.App:WinForm界面工程。
这样的好处是,换界面框架(比如WinForm换WPF)时,Vision.Core和Equipment.Hardware可以直接复用;写单元测试时也不用启动界面。再进一步,把Vision.Core设计成不依赖具体UI,内部通过事件或回调把中间过程传出来,调试时甚至可以开一个控制台工程来调用视觉处理逻辑,方便批量回归。
我在工程里还会单独放一个HalconHelper.cs,把常用操作用静态方法包起来,比如加载模板、设置匹配参数、图像转HObject、保存结果叠加图。这层封装既减少重复代码,也避免后续人员直接乱用底层算子造成管理混乱。
5.2 多线程与相机控制的坑
多线程在C# + HALCON里要格外小心。HALCON对象不是线程安全的,比如同一个HImage对象在一个线程里被读取、另一个线程里被Dispose,程序会直接崩或出现未知异常。我的处理原则是:每个线程里创建和使用独立的HALCON对象,线程间只传递托管数据结构(坐标、分数、图像路径),绝不传递HImage或HObject引用。
相机控制也容易踩坑。GigE工业相机的采集线程和视觉处理线程分离后,如果相机断线重连逻辑没做好,系统会陷入死循环或一直报错。我用过的相机SDK里,断线一般会抛异常或返回错误码,模板里针对这个做了事件通知:检测到相机异常,先停采集线程,释放相机对象,等三秒自动重连。重连成功后重新拉流,这种策略比手动重启软件要实用。
另外提醒一个细节:HALCON的open_framegrabber一旦打开相机,默认图像格式可能是RGB或YUV,但模板匹配和测量大多只需要灰度图。可以设置grabber.SetFramegrabberParam("color_space", "gray")减少图像转换开销,如果接口不提供灰度采集,就在程序里用ConvertImageType或Rgb1ToGray转一下,别拿RGB图直接跑模板匹配,浪费内存和算力。
5.3 WinForm安装包制作
这里直接回答一个很多人搜过的问题:C#的WinForm如何制作安装包?我推荐用Visual Studio的“Setup Project”插件来生成MSI安装包,部署简单,客户双击就装完。步骤是:
- 在解决方案里新建“Setup Project”。
- 将主项目的“Primary output”添加到Application Folder。
- 把HALCON运行时的关键DLL、
license.dat、模型文件、相机SDK的DLL一并加进安装包。 - 设置快捷方式到桌面和开始菜单。
- 配置“启动条件”,检查.NET运行时和HALCON运行时是否已安装,没装则提示或自动引导安装。
安装包制作时最常见的坑是DLL缺失或路径不对。HALCON的DLL依赖性很强,halcondotnet.dll只是托管壳,真正干活的是hdevengine*.dll、hxl.dll、hacio*.dll这些原生库。最简单稳妥的部署方案是:把整个HALCON的bin\x64-win64目录通通带进安装包,同时在系统PATH里添加这个目录,防止各种“无法加载DLL”的问题。
我在模板里还做了个部署检查小工具:启动时扫描关键DLL、license文件、模型文件是否完整,并写入日志。这样客户报“启动不了”,我能第一时间判断是不是文件缺失,省去大量远程来回。
6. 常见问题速查与调试经验
6.1 必须收藏的几类报错和对应处理
这几类问题是我项目里反复遇到、也反复帮人排查的,整理成了速查表:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 启动即报“HALCON license not found” | license路径不对或文件丢失 | 确认HALCONROOT\license\license.dat存在;环境变量是否正确 |
| 报“Image acquisition failed”或采集超时 | 相机被占用、网线松动、相机掉电 | 重启程序前先释放相机;检查网络丢包;相机供电 |
| 匹配找不到目标 | 模板质量差、参数约束太苛刻、光照变化大 | 重新截取模板;调整MinScore和角度范围;预处理增强 |
| 测量结果跳动大 | 边缘干扰、像素当量不准确 | 收缩测量区域;增强滤波;重新标定 |
| .NET程序报“BadImageFormatException” | 目标平台位数混用 | 统一x64或x86,安装包与开发机一致 |
| 程序连续运行几小时后内存暴涨 | HALCON对象未及时释放 | 检查临时HImage/HObject生命周期,显式Dispose |
6.2 调试工具与排查方法
HALCON的调试能力很强,但不少C#程序员只会在代码里加断点,忽略了HDevelop这座宝库。我建议做法:先在HDevelop里把视觉流程跑通,再用export功能导出C#代码,最后嵌到模板工程里。一旦线上出问题,拿故障图像回HDevelop里跑一遍,能快速定位是算法问题还是数据问题。
C#侧排查时,我习惯每一步都记录耗时。模板匹配阶段、测量阶段、图像保存阶段分别计时,哪一步突然变慢,问题多半出在哪一步。比如匹配阶段从2ms涨到20ms,优先怀疑图像分辨率变了或目标区域复杂度增加;图像保存变慢,优先怀疑硬盘IO被占满或文件越写越大。
现场日志要保留多份:程序运行日志、视觉参数日志、图像保存记录各存一份。运行日志用log4net或NLog,视觉参数日志在每次参数修改时自动记录变更前后值,图像则按照上面说的按日期归档。这样即使不现场去,也能从日志还原问题。
6.3 从“能跑”到“稳定跑”的进阶建议
先把流程跑通,再把细节抠稳。这是我从大量项目里得出来的经验。很多团队做视觉项目,目标只是“上线演示能跑”,结果现场一开机连续运转就崩。模板项目真正成熟,要过三关:
第一关叫“连续运行关”。程序不关机连续跑48小时以上,看内存是否稳定、相机是否掉线、匹配是否偶发漏检。第二关叫“环境干扰关”。光照变化、工件批次差异、传送带振动都可能会影响视觉稳定性,测试时要在不同时间段、不同环境下多试几次。第三关叫“异常恢复关”。人为制造相机断开、PLC通讯超时、模板文件被删等故障,看程序能不能自动恢复或给出完善提示,而不是直接崩溃。
我个人在实际操作中的体会是,模板给项目的最大帮助不是省了那几周开发时间,而是让团队在“遇到问题知道去哪里查”这件事上形成了统一口径。C# + HALCON的组合本身不复杂,复杂的是现场环境千奇百怪,而模板就像一个兜底框架,把那些容易出问题的边缘情况提前封堵住了。
最后再分享一个小技巧:每次做完一个项目,回头把新踩的坑和新增的公共代码回收到模板里,记一句“为什么这么做”的注释。两三个项目之后,你会发现自己这个模板越来越顺,新项目基本是填空,而不是从头造轮子。这套玩法我持续了多个项目,实用价值非常高。
