从Dynamsoft条形码阅读器迁移到IronBarcode
大多数从Dynamsoft条形码阅读器迁移到IronBarcode的开发者属于以下两类之一:选择Dynamsoft因为其声誉,然后发现以相机为中心的API与其文档处理使用场景不匹配的人,以及那些运行在气隙或Docker环境中而许可证服务器依赖性引起生产事故的人。
如果您属于第一组,则迁移将移除外部 PDF 渲染库、逐页渲染循环和错误代码许可模式。 如果您属于第二组,迁移将从您的Docker或VPC配置中移除InitLicense网络调用、离线许可证内容捆绑包和刷新周期以及出站网络策略。 无论哪种方式,迁移后代码库都会变短。
本指南坦诚地说明了您的损失:如果您的应用处理实时摄像头帧,Dynamsoft的捕获视觉管道是为这种工作负载量身定制的,IronBarcode不是合适的替代品。 本迁移指南适用于服务器端文件处理、文档工作流以及许可证服务器访问存在问题的环境。
步骤 1:交换NuGet包
dotnet remove package Dynamsoft.DotNet.BarcodeReader.Bundle
dotnet add package BarCode
如果您的项目中还添加了专门用于 Dynamsoft 的 PDF 渲染库(最常见的是 PdfiumViewer),也可以将其删除:
# Remove if added only for Dynamsoft PDF support
dotnet remove package PdfiumViewer
dotnet remove package PdfiumViewer.Native.x86_64.v8-xfa
步骤 2:替换许可证初始化
这是最直接的简化之处。 Dynamsoft模式要求每次启动时都进行错误代码检查和异常处理:
之前 — Dynamsoft:
using Dynamsoft.License;
using Dynamsoft.Core;
// Must run before any barcode operations
int errorCode = LicenseManager.InitLicense("YOUR-DYNAMSOFT-KEY", out string errorMsg);
if (errorCode != (int)EnumErrorCode.EC_OK)
throw new InvalidOperationException($"License validation failed [{errorCode}]: {errorMsg}");Imports Dynamsoft.License
Imports Dynamsoft.Core
' Must run before any barcode operations
Dim errorCode As Integer = LicenseManager.InitLicense("YOUR-DYNAMSOFT-KEY", errorMsg)
If errorCode <> CType(EnumErrorCode.EC_OK, Integer) Then
Throw New InvalidOperationException($"License validation failed [{errorCode}]: {errorMsg}")
End If之后IronBarcode:
// NuGet: dotnet add package BarCode
using IronBarCode;
// Local validation — no network call, no error code
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";Imports IronBarCode
' Local validation — no network call, no error code
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"在ASP.NET Core应用中,将此添加到builder.Build():
IronBarCode.License.LicenseKey = Environment.GetEnvironmentVariable("IRONBARCODE_KEY")
?? "YOUR-LICENSE-KEY";Imports IronBarCode
IronBarCode.License.LicenseKey = If(Environment.GetEnvironmentVariable("IRONBARCODE_KEY"), "YOUR-LICENSE-KEY")在Docker或Kubernetes环境中,在您的部署清单中设置IRONBARCODE_KEY环境变量。不需要出站网络规则。
步骤 3:替换命名空间导入
在所有源文件中查找并替换:
grep -r "using Dynamsoft\." --include="*.cs" .
替换所有出现的项:
// Before
using Dynamsoft.CVR;
using Dynamsoft.DBR;
using Dynamsoft.License;
using Dynamsoft.Core;
// After
using IronBarCode;Imports IronBarCode代码迁移示例
基本文件读取
最基本的操作——从图像文件中读取条形码。
之前 — Dynamsoft:
using Dynamsoft.CVR;
using Dynamsoft.DBR;
public string ReadBarcodeFromFile(CaptureVisionRouter router, string imagePath)
{
CapturedResult result = router.Capture(imagePath, PresetTemplate.PT_READ_BARCODES);
BarcodeResultItem[] items = result.GetDecodedBarcodesResult()?.GetItems();
if (items == null || items.Length == 0)
return null;
return items[0].GetText();
}Imports Dynamsoft.CVR
Imports Dynamsoft.DBR
Public Function ReadBarcodeFromFile(router As CaptureVisionRouter, imagePath As String) As String
Dim result As CapturedResult = router.Capture(imagePath, PresetTemplate.PT_READ_BARCODES)
Dim items As BarcodeResultItem() = result.GetDecodedBarcodesResult()?.GetItems()
If items Is Nothing OrElse items.Length = 0 Then
Return Nothing
End If
Return items(0).GetText()
End Function之后IronBarcode:
// NuGet: dotnet add package BarCode
using IronBarCode;
public string ReadBarcodeFromFile(string imagePath)
{
var results = BarcodeReader.Read(imagePath);
return results?.FirstOrDefault()?.Value;
}Imports IronBarCode
Public Function ReadBarcodeFromFile(imagePath As String) As String
Dim results = BarcodeReader.Read(imagePath)
Return results?.FirstOrDefault()?.Value
End Function路由器实例消失了。 BarcodeReader.Read是静态的。 .Value。 使用LINQ对results的空检查更简洁。
读取多个条形码
之前 — Dynamsoft:
using Dynamsoft.CVR;
using Dynamsoft.DBR;
public List<string> ReadAllBarcodes(CaptureVisionRouter router, string imagePath)
{
SimplifiedCaptureVisionSettings settings = router.GetSimplifiedSettings(
PresetTemplate.PT_READ_BARCODES);
settings.BarcodeSettings.ExpectedBarcodesCount = 0; // 0 = find all
router.UpdateSettings(PresetTemplate.PT_READ_BARCODES, settings);
CapturedResult result = router.Capture(imagePath, PresetTemplate.PT_READ_BARCODES);
BarcodeResultItem[] items = result.GetDecodedBarcodesResult()?.GetItems();
var values = new List<string>();
if (items != null)
{
foreach (var item in items)
values.Add(item.GetText());
}
return values;
}Imports Dynamsoft.CVR
Imports Dynamsoft.DBR
Public Function ReadAllBarcodes(router As CaptureVisionRouter, imagePath As String) As List(Of String)
Dim settings As SimplifiedCaptureVisionSettings = router.GetSimplifiedSettings(PresetTemplate.PT_READ_BARCODES)
settings.BarcodeSettings.ExpectedBarcodesCount = 0 ' 0 = find all
router.UpdateSettings(PresetTemplate.PT_READ_BARCODES, settings)
Dim result As CapturedResult = router.Capture(imagePath, PresetTemplate.PT_READ_BARCODES)
Dim items As BarcodeResultItem() = result.GetDecodedBarcodesResult()?.GetItems()
Dim values As New List(Of String)()
If items IsNot Nothing Then
For Each item In items
values.Add(item.GetText())
Next
End If
Return values
End Function之后IronBarcode:
using IronBarCode;
public List<string> ReadAllBarcodes(string imagePath)
{
var options = new BarcodeReaderOptions
{
ExpectMultipleBarcodes = true,
MaxParallelThreads = 4
};
return BarcodeReader.Read(imagePath, options)
.Select(r => r.Value)
.ToList();
}Imports IronBarCode
Public Function ReadAllBarcodes(imagePath As String) As List(Of String)
Dim options As New BarcodeReaderOptions With {
.ExpectMultipleBarcodes = True,
.MaxParallelThreads = 4
}
Return BarcodeReader.Read(imagePath, options) _
.Select(Function(r) r.Value) _
.ToList()
End Function从字节(内存映像)读取
之前 — Dynamsoft:
using Dynamsoft.CVR;
using Dynamsoft.Core;
using Dynamsoft.DBR;
// Requires width, height, stride, and pixel format — low-level buffer API
public string ReadFromBuffer(CaptureVisionRouter router, byte[] rawPixels, int width, int height)
{
var imageData = new ImageData
{
Bytes = rawPixels,
Width = width,
Height = height,
Stride = width * 3, // assuming 24bpp RGB
Format = EnumImagePixelFormat.IPF_RGB_888
};
CapturedResult result = router.Capture(imageData, PresetTemplate.PT_READ_BARCODES);
return result.GetDecodedBarcodesResult()?.GetItems()?.FirstOrDefault()?.GetText();
}Imports Dynamsoft.CVR
Imports Dynamsoft.Core
Imports Dynamsoft.DBR
' Requires width, height, stride, and pixel format — low-level buffer API
Public Function ReadFromBuffer(router As CaptureVisionRouter, rawPixels As Byte(), width As Integer, height As Integer) As String
Dim imageData As New ImageData With {
.Bytes = rawPixels,
.Width = width,
.Height = height,
.Stride = width * 3, ' assuming 24bpp RGB
.Format = EnumImagePixelFormat.IPF_RGB_888
}
Dim result As CapturedResult = router.Capture(imageData, PresetTemplate.PT_READ_BARCODES)
Return result.GetDecodedBarcodesResult()?.GetItems()?.FirstOrDefault()?.GetText()
End Function之后IronBarcode:
using IronBarCode;
// Pass PNG/JPEG/BMP bytes directly — no pixel format or stride calculation
public string ReadFromImageBytes(byte[] imageBytes)
{
return BarcodeReader.Read(imageBytes)?.FirstOrDefault()?.Value;
}Imports IronBarCode
Public Function ReadFromImageBytes(imageBytes As Byte()) As String
Return BarcodeReader.Read(imageBytes)?.FirstOrDefault()?.Value
End Function如果您的应用程序之前已将图像字节转换为 Dynamsoft 的原始像素缓冲区,则您可以将原始编码图像字节(PNG、JPEG、BMP)直接传递给IronBarcode,而无需先解码为原始像素。
PDF条形码读取——移除渲染循环
这通常是迁移过程中代码量减少最多的部分。 移除整个 PdfiumViewer 渲染循环,并用单个调用代替。
之前 — Dynamsoft 与 PdfiumViewer:
// Requires: Dynamsoft.DotNet.BarcodeReader.Bundle + PdfiumViewer + PdfiumViewer.Native.*
using Dynamsoft.CVR;
using Dynamsoft.DBR;
using PdfiumViewer;
using System.Drawing.Imaging;
public List<string> ReadBarcodesFromPdf(CaptureVisionRouter router, string pdfPath)
{
var allBarcodes = new List<string>();
using (var pdfDoc = PdfDocument.Load(pdfPath))
{
for (int page = 0; page < pdfDoc.PageCount; page++)
{
using var image = pdfDoc.Render(page, 300, 300, true);
using var ms = new MemoryStream();
image.Save(ms, ImageFormat.Png);
CapturedResult result = router.Capture(ms.ToArray(),
PresetTemplate.PT_READ_BARCODES);
var items = result.GetDecodedBarcodesResult()?.GetItems();
if (items != null)
{
foreach (var item in items)
allBarcodes.Add(item.GetText());
}
}
}
return allBarcodes;
}Imports Dynamsoft.CVR
Imports Dynamsoft.DBR
Imports PdfiumViewer
Imports System.Drawing.Imaging
Public Function ReadBarcodesFromPdf(router As CaptureVisionRouter, pdfPath As String) As List(Of String)
Dim allBarcodes As New List(Of String)()
Using pdfDoc As PdfDocument = PdfDocument.Load(pdfPath)
For page As Integer = 0 To pdfDoc.PageCount - 1
Using image = pdfDoc.Render(page, 300, 300, True)
Using ms As New MemoryStream()
image.Save(ms, ImageFormat.Png)
Dim result As CapturedResult = router.Capture(ms.ToArray(), PresetTemplate.PT_READ_BARCODES)
Dim items = result.GetDecodedBarcodesResult()?.GetItems()
If items IsNot Nothing Then
For Each item In items
allBarcodes.Add(item.GetText())
Next
End If
End Using
End Using
Next
End Using
Return allBarcodes
End Function之后IronBarcode:
using IronBarCode;
public List<string> ReadBarcodesFromPdf(string pdfPath)
{
return BarcodeReader.Read(pdfPath)
.Select(r => r.Value)
.ToList();
}Imports IronBarCode
Public Function ReadBarcodesFromPdf(pdfPath As String) As List(Of String)
Return BarcodeReader.Read(pdfPath) _
.Select(Function(r) r.Value) _
.ToList()
End Function页面循环、PdfDocument、300 DPI渲染步骤、MemoryStream以及每页捕获调用全部消失。 IronBarcode在内部处理 PDF 页面。
如果您需要读取带有选项的 PDF 文件(例如,用于读取密集或难以识别的条形码):
using IronBarCode;
public List<string> ReadBarcodesFromPdfAccurate(string pdfPath)
{
var options = new BarcodeReaderOptions
{
Speed = ReadingSpeed.Balanced,
ExpectMultipleBarcodes = true
};
return BarcodeReader.Read(pdfPath, options)
.Select(r => r.Value)
.ToList();
}Imports IronBarCode
Public Function ReadBarcodesFromPdfAccurate(pdfPath As String) As List(Of String)
Dim options As New BarcodeReaderOptions With {
.Speed = ReadingSpeed.Balanced,
.ExpectMultipleBarcodes = True
}
Return BarcodeReader.Read(pdfPath, options) _
.Select(Function(r) r.Value) _
.ToList()
End Function离线/物理隔离部署
如果您的代码中包含离线许可模式,请将其完全删除:
之前 — Dynamsoft 离线许可证:
using Dynamsoft.License;
using Dynamsoft.Core;
// Dynamsoft offline: fetch license bundle on a connected machine, persist it,
// then replay it on the offline machine via InitLicenseFromLicenseContent.
int errorCode = LicenseManager.InitLicenseFromLicenseContent(
licenseContent,
out string errorMsg);
if (errorCode != (int)EnumErrorCode.EC_OK)
throw new InvalidOperationException($"Offline license failed: {errorMsg}");Imports Dynamsoft.License
Imports Dynamsoft.Core
' Dynamsoft offline: fetch license bundle on a connected machine, persist it,
' then replay it on the offline machine via InitLicenseFromLicenseContent.
Dim errorMsg As String
Dim errorCode As Integer = LicenseManager.InitLicenseFromLicenseContent(licenseContent, errorMsg)
If errorCode <> CType(EnumErrorCode.EC_OK, Integer) Then
Throw New InvalidOperationException($"Offline license failed: {errorMsg}")
End If之后IronBarcode:
// Remove all of the above. Replace with:
IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY";IronBarCode.License.LicenseKey = "YOUR-LICENSE-KEY"没有许可证内容包需要获取和刷新。 没有连接机器引导步骤。密钥在本地验证。
Docker 配置
如果您之前有网络出口规则或代理配置以允许出站HTTPS到Dynamsoft的许可证端点:
# Before: Docker or Kubernetes egress policy
# Required: Allow outbound HTTPS to Dynamsoft licence endpoints
# After: Remove that egress rule.
# IronBarcode does not require outbound network access for license validation.
# Set license via environment variable
env:
- name: IRONBARCODE_KEY
valueFrom:
secretKeyRef:
name: ironbarcode-license
key: key
实例管理清理
Dynamsoft使用一个基于CaptureVisionRouter构建的实例API。 如果您的代码在服务类、字段初始化程序或DI注册中创建路由器实例,这些全部消失。
之前——Dynamsoft实例管理:
using Dynamsoft.CVR;
using Dynamsoft.DBR;
using Dynamsoft.License;
using Dynamsoft.Core;
public class BarcodeService : IDisposable
{
private readonly CaptureVisionRouter _router;
public BarcodeService()
{
int errorCode = LicenseManager.InitLicense("KEY", out string errorMsg);
if (errorCode != (int)EnumErrorCode.EC_OK)
throw new InvalidOperationException(errorMsg);
_router = new CaptureVisionRouter();
var settings = _router.GetSimplifiedSettings(PresetTemplate.PT_READ_BARCODES);
settings.BarcodeSettings.ExpectedBarcodesCount = 0;
_router.UpdateSettings(PresetTemplate.PT_READ_BARCODES, settings);
}
public string[] ReadFile(string path)
{
CapturedResult result = _router.Capture(path, PresetTemplate.PT_READ_BARCODES);
var items = result.GetDecodedBarcodesResult()?.GetItems();
return items?.Select(i => i.GetText()).ToArray() ?? Array.Empty<string>();
}
public void Dispose()
{
_router?.Dispose();
}
}Imports Dynamsoft.CVR
Imports Dynamsoft.DBR
Imports Dynamsoft.License
Imports Dynamsoft.Core
Public Class BarcodeService
Implements IDisposable
Private ReadOnly _router As CaptureVisionRouter
Public Sub New()
Dim errorCode As Integer = LicenseManager.InitLicense("KEY", errorMsg:=Nothing)
If errorCode <> CType(EnumErrorCode.EC_OK, Integer) Then
Throw New InvalidOperationException(errorMsg)
End If
_router = New CaptureVisionRouter()
Dim settings = _router.GetSimplifiedSettings(PresetTemplate.PT_READ_BARCODES)
settings.BarcodeSettings.ExpectedBarcodesCount = 0
_router.UpdateSettings(PresetTemplate.PT_READ_BARCODES, settings)
End Sub
Public Function ReadFile(path As String) As String()
Dim result As CapturedResult = _router.Capture(path, PresetTemplate.PT_READ_BARCODES)
Dim items = result.GetDecodedBarcodesResult()?.GetItems()
Return If(items?.Select(Function(i) i.GetText()).ToArray(), Array.Empty(Of String)())
End Function
Public Sub Dispose() Implements IDisposable.Dispose
_router?.Dispose()
End Sub
End Class之后IronBarcode静态API:
// NuGet: dotnet add package BarCode
using IronBarCode;
public class BarcodeService
{
// No constructor initialization — license set once at app startup
// No Dispose — no instance to clean up
public string[] ReadFile(string path)
{
var options = new BarcodeReaderOptions { ExpectMultipleBarcodes = true };
return BarcodeReader.Read(path, options)
.Select(r => r.Value)
.ToArray();
}
}Imports IronBarCode
Public Class BarcodeService
' No constructor initialization — license set once at app startup
' No Dispose — no instance to clean up
Public Function ReadFile(path As String) As String()
Dim options As New BarcodeReaderOptions With {.ExpectMultipleBarcodes = True}
Return BarcodeReader.Read(path, options) _
.Select(Function(r) r.Value) _
.ToArray()
End Function
End Class该类失去了它的构造函数,它的_router字段。 如果此服务在DI中注册为singleton或scoped service来管理路由器生命周期,该注册可以简化或服务可以成为一组静态方法。
阅读速度与超时时间的关系
Dynamsoft使用一个以毫秒为单位的Timeout,针对摄像头帧率进行了优化。 IronBarcode使用一个ReadingSpeed枚举:
| Dynamsoft 设置 | IronBarcode等效产品 |
|---|---|
settings.Timeout = 100 (摄像头管线) | Speed = ReadingSpeed.Faster |
| 低超时时间(优先考虑速度) | Speed = ReadingSpeed.Balanced |
| 更高的超时时间(优先考虑准确性) | Speed = ReadingSpeed.Detailed |
| 最高精度,无时间压力 | Speed = ReadingSpeed.ExtremeDetail |
对于大多数文档处理工作流中,吞吐量比小于100毫秒的响应时间更重要,ReadingSpeed.Balanced是正确的默认设置:
var options = new BarcodeReaderOptions
{
Speed = ReadingSpeed.Balanced,
ExpectMultipleBarcodes = true,
MaxParallelThreads = 4
};Dim options As New BarcodeReaderOptions With {
.Speed = ReadingSpeed.Balanced,
.ExpectMultipleBarcodes = True,
.MaxParallelThreads = 4
}常见迁移问题
BarcodeResultItem.GetText() vs result.Value
访问器从方法改为属性:
// Before
string value = item.GetText();
// After
string value = result.Value;' Before
Dim value As String = item.GetText()
' After
Dim value As String = result.ValueBarcodeResultItem.GetFormatString() vs result.Format
Dynamsoft通过GetFormatString()以字符串形式返回格式。 IronBarcode通过result.Format上公开它:
// Before
if (item.GetFormatString() == "QR_CODE")
Console.WriteLine("Found QR code");
// After
if (result.Format == BarcodeEncoding.QRCode)
Console.WriteLine("Found QR code");
// For logging without enum comparison — .ToString() works on both
Console.WriteLine($"Format: {result.Format}");' Before
If item.GetFormatString() = "QR_CODE" Then
Console.WriteLine("Found QR code")
End If
' After
If result.Format = BarcodeEncoding.QRCode Then
Console.WriteLine("Found QR code")
End If
' For logging without enum comparison — .ToString() works on both
Console.WriteLine($"Format: {result.Format}")空结果与空集合
当未找到条形码时,Dynamsoft的null。 IronBarcode返回一个空集合。 更新空值检查:
// Before: null check required
CapturedResult result = router.Capture(path, PresetTemplate.PT_READ_BARCODES);
var items = result.GetDecodedBarcodesResult()?.GetItems();
if (items != null && items.Length > 0)
Process(items[0].GetText());
// After: null-safe but also correct to check Count
var results = BarcodeReader.Read(path);
if (results.Any())
Process(results.First().Value);Imports System.Linq
' Before: null check required
Dim result As CapturedResult = router.Capture(path, PresetTemplate.PT_READ_BARCODES)
Dim items = result.GetDecodedBarcodesResult()?.GetItems()
If items IsNot Nothing AndAlso items.Length > 0 Then
Process(items(0).GetText())
End If
' After: null-safe but also correct to check Count
Dim results = BarcodeReader.Read(path)
If results.Any() Then
Process(results.First().Value)
End IfSimplifiedCaptureVisionSettings to BarcodeReaderOptions
GetSimplifiedSettings / Read:
// Before
var settings = router.GetSimplifiedSettings(PresetTemplate.PT_READ_BARCODES);
settings.BarcodeSettings.ExpectedBarcodesCount = 0;
settings.Timeout = 500;
router.UpdateSettings(PresetTemplate.PT_READ_BARCODES, settings);
var result = router.Capture(path, PresetTemplate.PT_READ_BARCODES);
// After
var options = new BarcodeReaderOptions
{
Speed = ReadingSpeed.Balanced,
ExpectMultipleBarcodes = true
};
var results = BarcodeReader.Read(path, options);' Before
Dim settings = router.GetSimplifiedSettings(PresetTemplate.PT_READ_BARCODES)
settings.BarcodeSettings.ExpectedBarcodesCount = 0
settings.Timeout = 500
router.UpdateSettings(PresetTemplate.PT_READ_BARCODES, settings)
Dim result = router.Capture(path, PresetTemplate.PT_READ_BARCODES)
' After
Dim options As New BarcodeReaderOptions With {
.Speed = ReadingSpeed.Balanced,
.ExpectMultipleBarcodes = True
}
Dim results = BarcodeReader.Read(path, options)迁移清单
运行以下搜索,查找所有需要更新的 Dynamsoft 参考资料:
grep -r "using Dynamsoft\." --include="*.cs" .
grep -r "LicenseManager.InitLicense\|EnumErrorCode\|EC_OK" --include="*.cs" .
grep -r "new CaptureVisionRouter\|router\.Capture\|PresetTemplate" --include="*.cs" .
grep -r "BarcodeResultItem\|GetDecodedBarcodesResult\|GetFormatString" --include="*.cs" .
grep -r "GetSimplifiedSettings\|UpdateSettings\|SimplifiedCaptureVisionSettings" --include="*.cs" .
grep -r "router\.Dispose\|InitLicenseFromLicenseContent" --include="*.cs" .
逐场比赛进行分析:
using Dynamsoft.*→using IronBarCodeLicenseManager.InitLicense(key, out errorMsg)+ 错误检查 →IronBarCode.License.LicenseKey = "key"new CaptureVisionRouter()→ 移除(静态API,无实例)router.Capture(path, PresetTemplate.PT_READ_BARCODES)→BarcodeReader.Read(path)router.Capture(imageData, ...)(原始像素缓冲) →BarcodeReader.Read(imageBytes)- 每页PDF渲染循环 +
router.Capture(pageBytes, ...)→BarcodeReader.Read(pdfPath) BarcodeResultItem.GetText()→result.ValueBarcodeResultItem.GetFormatString()→result.FormatGetSimplifiedSettings(...)+UpdateSettings(...)→new BarcodeReaderOptions { ... }router.Dispose()→ 移除LicenseManager.InitLicenseFromLicenseContent(...)→ 完全移除- 如果添加 PdfiumViewer NuGet包仅仅是为了支持 Dynamsoft PDF 处理,则将其移除。
- 移除Dynamsoft许可端点的Docker/Kubernetes网络出口规则
- 在部署配置中设置
IRONBARCODE_KEY环境变量

Curtis Chau 拥有卡尔顿大学的计算机科学学士学位,专注于前端开发,精通 Node.js、TypeScript、JavaScript 和 React。他热衷于打造直观且美观的用户界面,喜欢使用现代框架并创建结构良好、视觉吸引力强的手册。