[{"data":1,"prerenderedAt":1686},["ShallowReactive",2],{"navigation":3,"post-\u002Fposts\u002F2023\u002Ffont2svg-solution":20,"surroundPosts-\u002Fposts\u002F2023\u002Ffont2svg-solution":1673},[4,8,12,16],{"title":5,"path":6,"stem":7},"首页","\u002F","00.index",{"title":9,"path":10,"stem":11},"文章","\u002Fposts","01.posts",{"title":13,"path":14,"stem":15},"动态","\u002Fmoments","02.moments",{"title":17,"path":18,"stem":19},"关于","\u002Fabout","09.about",{"id":21,"title":22,"body":23,"class":1655,"cover":1655,"coverSize":1655,"date":1656,"description":252,"draft":1657,"extension":1658,"hideComments":1657,"location":1655,"meta":1659,"navigation":223,"path":1660,"readingTime":1661,"seo":1666,"sitemap":1667,"stem":1668,"tags":1669,"time":1655,"weather":1655,"__hash__":1672},"posts\u002Fposts\u002F2023\u002F20230719.font2svg-solution.md","Font2svg 特殊字体渲染方案",{"type":24,"value":25,"toc":1649},"minimark",[26,58,62,65,68,73,76,80,88,92,95,98,101,105,108,112,115,119,122,126,129,133,136,140,143,147,150,154,158,161,164,167,171,174,177,180,183,186,189,192,195,198,201,204,207,210,213,216,219,225,229,246,1072,1075,1078,1081,1084,1229,1245,1251,1452,1455,1458,1476,1479,1494,1497,1500,1503,1506,1509,1512,1515,1518,1585,1609,1612,1615,1618,1621,1624,1627,1630,1633,1636,1639,1642,1645],[27,28,29,47,55],"blockquote",{},[30,31,32,33,40,41,46],"p",{},"2023-09-12 发表于 ",[34,35,39],"a",{"href":36,"rel":37},"https:\u002F\u002Fmp.weixin.qq.com\u002Fs\u002FeAlceV6H0JO019m8TozwxA",[38],"nofollow","哔哩哔哩技术公众号","、",[34,42,45],{"href":43,"rel":44},"https:\u002F\u002Fwww.bilibili.com\u002Fread\u002Fcv26464950\u002F",[38],"哔哩哔哩技术专栏"," 等处。",[30,48,49,50],{},"GitHub 开源地址：",[34,51,54],{"href":52,"rel":53},"https:\u002F\u002Fgithub.com\u002Ffont2svg",[38],"Font2svg",[30,56,57],{},"注：开源版方案和文中略有差别，但更通用。API 服务已经完成，前端组件的开源版还没搞完。",[59,60,61],"h2",{"id":61},"背景介绍",[30,63,64],{},"在 Web 开发中，经常会需要在页面中引入一些特殊字体，这些字体通常不在系统字体库的范围内，并且动辄 4~5MB，甚至有些字体超过 10MB，会影响用户加载体验，尤其在手机端使用移动网络的情况下。",[30,66,67],{},"针对不同的业务场景，通常会有以下几种解决方案：",[69,70,72],"h4",{"id":71},"一使用切图","一、使用切图",[30,74,75],{},"当需要显示特殊字体的文案为固定文案时，直接使用切图可能是最常见的一种解决方案，无论是透明的 PNG 切图，还是用矢量的 SVG，都能在便捷度和加载体验上有不错的体验。当然，使用 PNG 的时候需要注意高分辨率屏幕的适配，使用 1 倍图在高分辨率的屏幕下肉眼很容易看出模糊。",[69,77,79],{"id":78},"二使用原始字体","二、使用原始字体",[30,81,82,83,87],{},"当需要显示特殊字体的文案是动态文案时，就没办法直接用文字切图来实现了。通常在对用户加载体验没有特别要求的场景下，可直接通过 ",[84,85,86],"code",{},"@font-face"," 来直接引入特殊字体的文件进来。缺点也显而易见，正如前文所说，动辄 4~5MB，甚至超过 10MB 的字体文件，会比较影响用户的加载体验，在首屏需要显示的场景下，用户往往会先看到先渲染默认字体，再闪现成特殊字体的情况。",[69,89,91],{"id":90},"三使用裁剪压缩后的字体","三、使用裁剪\u002F压缩后的字体",[30,93,94],{},"字体裁剪技术，可通过省略字体变体（如某些字体会包含多个粗细）、裁剪字体字符集（省略不常用的字符）、通过其他压缩算法（如 woff、woff2）等方式来降低字体体积。当文案内容相对可控的情况下，用该方案可比直接加载原始字体要节省更多体积。但若字符裁剪过多，会导致缺少部分字符，在显示时出现字体不一致的情况；若字符裁剪过少，体积仍会较大。并且通常情况下，裁剪后的字符数也是远大于用户实际需要渲染的字符数，还是会有非常大的不必要的下载流量。",[59,96,97],{"id":97},"实现原理",[30,99,100],{},"在动态渲染特殊字体文案的场景下，为了提升用户的加载体验，我们研发了一个创新的解决方案：「Font2svg」。在介绍实现原理前，先简单介绍下字体相关的基础知识。",[69,102,104],{"id":103},"字符character","字符（Character）",[30,106,107],{},"在计算机中，字符是任意单个文本元素，例如字母、数字、标点符号、汉字甚至 Emoji 表情等。字符通常是构建文本的基本单位。",[69,109,111],{"id":110},"字符集character-set","字符集（Character Set）",[30,113,114],{},"字符集是一组预定义的字符的集合。它是对字符进行分类和组织的方式，以便在计算机系统中能够使用。会为每一个字符分配一个唯一的 ID，叫做 Code Point（码点、内码），常见字符集：ASCII、GB2312、GBK、Unicode 等。",[69,116,118],{"id":117},"字符编码character-encoding","字符编码（Character Encoding）",[30,120,121],{},"字符编码是一种将字符映射到特定二进制数的规则，以便在计算机中存储。通常字符集和字符编码都是成对出现，如 ASCII、GB2312、GBK 等，都是既代表了字符集，也代表了对应的字符编码。而 Unicode 比较特殊，有多种字符编码，如 UTF-8、UTF-16 等。",[69,123,125],{"id":124},"字体家族font-family","字体家族（Font Family）",[30,127,128],{},"字体家族是指具有相似设计特征的一组字体。这些字体共享类似的外观，但在细节上可能略有不同，如粗细、斜体等。例如：「思源宋体」就是一个字体家族。",[69,130,132],{"id":131},"字体样式font-style","字体样式（Font Style）",[30,134,135],{},"字体样式是字体家族中特定字体的变体，它定义了字体的外观。例如：Light、Regular、Bold、Heavy 等。",[69,137,139],{"id":138},"字型font","字型（Font）",[30,141,142],{},"字型是一个字体家族下的特定样式的字体，例如: 思源宋体-Bold",[69,144,146],{"id":145},"字形glyph","字形（Glyph）",[30,148,149],{},"字形是指一个单独字符在特定的字体（字型）中的具体外观或形状。每个字符都有对应的字形，它们描述了字符的细节和轮廓。如下图，就是「哔」这个字符在不同的字体下不同的字形：",[151,152],"post-image",{"filename":153},"01.png",[69,155,157],{"id":156},"字体度量metrics","字体度量（Metrics）",[30,159,160],{},"字体度量是指字体中字符的尺寸和位置信息。它包括了字符的宽度、高度、上沿线（ascender）、下沿线（descender）、基线（baseline）等数据。字体度量在排版中非常重要，它决定了字符之间的间距和对齐方式，确保文本在屏幕或打印上的合理布局。",[151,162],{"filename":163},"02.png",[151,165],{"filename":166},"03.png",[69,168,170],{"id":169},"轮廓outline","轮廓（Outline）",[30,172,173],{},"字体轮廓是字形的矢量图形表示，一个字形由若干轮廓组成，每个轮廓由若干条线段或贝塞尔曲线闭合而成。根据字体格式不同，使用的贝塞尔曲线的阶数也有区别。TrueType（TTF）字体采用二次贝塞尔曲线（3 点控制），而 OpenType（OTF）字体采用三次贝塞尔曲线（4 点控制）。",[30,175,176],{},"一个轮廓组成的字母 C：",[151,178],{"filename":179},"04.gif",[30,181,182],{},"多个轮廓组成的字母 B：",[151,184],{"filename":185},"05.gif",[30,187,188],{},"二次贝塞尔曲线：",[151,190],{"filename":191},"06.gif",[30,193,194],{},"三次贝塞尔曲线：",[151,196],{"filename":197},"07.gif",[30,199,200],{},"本方案的原理本质上还是利用 SVG 来渲染特殊字体，通过上面的介绍，我们知道，字体的轮廓由若干线段和贝塞尔曲线组成，得知了线段的坐标和贝塞尔曲线的控制点，我们就可以绘制出轮廓的 SVG，而有了 SVG 我们就可以用来显示。因此我们可以通过解析字体文件，按需获取对应字符的 SVG 来避免加载整个字体文件。如何去唯一表示一个特定字符呢？那就是通过字符的 Unicode 编码。如下图所示，我们在渲染「哔哩哔哩乾杯」这几个字符的时候，只需要根据「哔」、「哩」、「乾」、「杯」这四个字符的 Unicode 编码去获取他们的 SVG，然后组合起来显示即可。",[151,202],{"filename":203},"08.png",[30,205,206],{},"考虑到性能，如果每个字符都实时去请求服务端生成 SVG 的话，并发小的时候没影响，并发大了的话，这个字体转 SVG 的服务就会成为瓶颈。不过由于这些 SVG 生成之后就几乎不会变，所以可以预先全部生成好，放到 CDN 上，每次直接根据 Unicode 编码去 CDN 上获取指定字符的 SVG 即可。这样既避免了服务端的压力，还利用了 CDN 的特性，可以让用户更快的获取到所需的资源。",[30,208,209],{},"「字体转 SVG」只是第一步，在实际渲染中，还需要修改 SVG 的样式，例如字体颜色、下划线等，因此我们还做了一个前端的组件，用来对获取到的 SVG 进行样式的处理以还原字体的样式。这个组件实现了根据所需字符「动态加载 SVG」以及「SVG 复原字体样式」，在使用起来只需要传入字体名称、所需显示的字符、一些字体样式等，大大减少了前端接入的复杂度。",[30,211,212],{},"整体流程如下图，页面开发阶段，在 Font2svg 后台预先上传字体文件，解析所有字符的 SVG，并预先上传至对象存储\u002FCDN 上。在页面加载时，通过前端组件传入需要渲染的字符和对应字体的配置，从 CDN 拉取特定字体特定字符的 SVG，并根据样式对字符的 SVG 进行二次编辑，复原字体上色、描边、下划线等样式。",[151,214],{"filename":215},"09.png",[30,217,218],{},"后台效果预览：",[220,221],"video-player",{":autoplay":222,":controls":222,":loop":222,"autoPlay":223,"filename":224},"true",true,"10.mp4",[69,226,228],{"id":227},"如何解析字体并生成字符的-svg","如何解析字体并生成字符的 SVG",[30,230,231,232,237,238,241,242,245],{},"字体解析服务通过 Python 实现，借助 ",[34,233,236],{"href":234,"rel":235},"https:\u002F\u002Fgithub.com\u002Frougier\u002Ffreetype-py",[38],"freetype"," 这个库对字体进行解析，遍历字体中的每一个字符，每个字符的字形（glyph）包含了轮廓（outline）和度量（metrics）数据，通过 ",[84,239,240],{},"outline.decompose"," 方法，可以将字符的轮廓数据分解为一系列的路径段，其中每个路径段都是一个点序列，表示字符的一部分。这些路径段可以是直线段、二次贝塞尔曲线或三次贝塞尔曲线。通过这些路径段的类型和点坐标，再通过 ",[84,243,244],{},"svgpathtools"," 这个库绘制成 SVG 的路径。",[247,248,253],"pre",{"className":249,"code":250,"language":251,"meta":252,"style":252},"language-python shiki shiki-themes material-theme-lighter github-light github-dark","outline.decompose(\n    context=None,\n    move_to=self.callbackMoveTo,\n    line_to=self.callbackLineTo,\n    conic_to=self.callbackConicTo,\n    cubic_to=self.callbackCubicTo,\n)\n\ndef callbackMoveTo(self, *args):\n    self._verbose(\"MoveTo \", len(args), self.vectorsToPoints(args))\n    self._lastX, self._lastY = args[0].x, args[0].y\n\ndef callbackLineTo(self, *args):\n    self._verbose(\"LineTo \", len(args), self.vectorsToPoints(args))\n    line = Line(self.lastXyToComplex(), self.vectorToComplex(args[0]))\n    self.svg_paths.append(line)\n    self._lastX, self._lastY = args[0].x, args[0].y\n\ndef callbackConicTo(self, *args):\n    self._verbose(\"ConicTo\", len(args), self.vectorsToPoints(args))\n    curve = QuadraticBezier(self.lastXyToComplex(), self.vectorToComplex(args[0]), self.vectorToComplex(args[1]))\n    self.svg_paths.append(curve)\n    self._lastX, self._lastY = args[1].x, args[1].y\n\ndef callbackCubicTo(self, *args):\n    self._verbose(\"CubicTo\", len(args), self.vectorsToPoints(args))\n    curve = CubicBezier(\n        self.lastXyToComplex(),\n        self.vectorToComplex(args[0]),\n        self.vectorToComplex(args[1]),\n        self.vectorToComplex(args[2]),\n    )\n    self.svg_paths.append(curve)\n    self._lastX, self._lastY = args[2].x, args[2].y\n","python","",[84,254,255,275,293,313,330,347,364,370,376,406,457,508,513,533,573,614,635,676,681,701,741,796,816,857,862,882,922,934,947,967,986,1006,1012,1031],{"__ignoreMap":252},[256,257,260,264,268,272],"span",{"class":258,"line":259},"line",1,[256,261,263],{"class":262},"su5hD","outline",[256,265,267],{"class":266},"sP7_E",".",[256,269,271],{"class":270},"slqww","decompose",[256,273,274],{"class":266},"(\n",[256,276,278,282,286,290],{"class":258,"line":277},2,[256,279,281],{"class":280},"s99_P","    context",[256,283,285],{"class":284},"smGrS","=",[256,287,289],{"class":288},"s39Yj","None",[256,291,292],{"class":266},",\n",[256,294,296,299,301,305,307,311],{"class":258,"line":295},3,[256,297,298],{"class":280},"    move_to",[256,300,285],{"class":284},[256,302,304],{"class":303},"s_hVV","self",[256,306,267],{"class":266},[256,308,310],{"class":309},"skxfh","callbackMoveTo",[256,312,292],{"class":266},[256,314,316,319,321,323,325,328],{"class":258,"line":315},4,[256,317,318],{"class":280},"    line_to",[256,320,285],{"class":284},[256,322,304],{"class":303},[256,324,267],{"class":266},[256,326,327],{"class":309},"callbackLineTo",[256,329,292],{"class":266},[256,331,333,336,338,340,342,345],{"class":258,"line":332},5,[256,334,335],{"class":280},"    conic_to",[256,337,285],{"class":284},[256,339,304],{"class":303},[256,341,267],{"class":266},[256,343,344],{"class":309},"callbackConicTo",[256,346,292],{"class":266},[256,348,350,353,355,357,359,362],{"class":258,"line":349},6,[256,351,352],{"class":280},"    cubic_to",[256,354,285],{"class":284},[256,356,304],{"class":303},[256,358,267],{"class":266},[256,360,361],{"class":309},"callbackCubicTo",[256,363,292],{"class":266},[256,365,367],{"class":258,"line":366},7,[256,368,369],{"class":266},")\n",[256,371,373],{"class":258,"line":372},8,[256,374,375],{"emptyLinePlaceholder":223},"\n",[256,377,379,383,387,390,393,396,399,403],{"class":258,"line":378},9,[256,380,382],{"class":381},"sbsja","def",[256,384,386],{"class":385},"sGLFI"," callbackMoveTo",[256,388,389],{"class":266},"(",[256,391,304],{"class":392},"smCYv",[256,394,395],{"class":266},",",[256,397,398],{"class":284}," *",[256,400,402],{"class":401},"sFwrP","args",[256,404,405],{"class":266},"):\n",[256,407,409,412,414,417,419,423,427,429,431,435,437,439,442,445,447,450,452,454],{"class":258,"line":408},10,[256,410,411],{"class":303},"    self",[256,413,267],{"class":266},[256,415,416],{"class":270},"_verbose",[256,418,389],{"class":266},[256,420,422],{"class":421},"sjJ54","\"",[256,424,426],{"class":425},"s_sjI","MoveTo ",[256,428,422],{"class":421},[256,430,395],{"class":266},[256,432,434],{"class":433},"sptTA"," len",[256,436,389],{"class":266},[256,438,402],{"class":270},[256,440,441],{"class":266},"),",[256,443,444],{"class":303}," self",[256,446,267],{"class":266},[256,448,449],{"class":270},"vectorsToPoints",[256,451,389],{"class":266},[256,453,402],{"class":270},[256,455,456],{"class":266},"))\n",[256,458,460,462,464,467,469,471,473,476,479,482,485,489,492,495,497,499,501,503,505],{"class":258,"line":459},11,[256,461,411],{"class":303},[256,463,267],{"class":266},[256,465,466],{"class":309},"_lastX",[256,468,395],{"class":266},[256,470,444],{"class":303},[256,472,267],{"class":266},[256,474,475],{"class":309},"_lastY",[256,477,478],{"class":284}," =",[256,480,481],{"class":262}," args",[256,483,484],{"class":266},"[",[256,486,488],{"class":487},"srdBf","0",[256,490,491],{"class":266},"].",[256,493,494],{"class":309},"x",[256,496,395],{"class":266},[256,498,481],{"class":262},[256,500,484],{"class":266},[256,502,488],{"class":487},[256,504,491],{"class":266},[256,506,507],{"class":309},"y\n",[256,509,511],{"class":258,"line":510},12,[256,512,375],{"emptyLinePlaceholder":223},[256,514,516,518,521,523,525,527,529,531],{"class":258,"line":515},13,[256,517,382],{"class":381},[256,519,520],{"class":385}," callbackLineTo",[256,522,389],{"class":266},[256,524,304],{"class":392},[256,526,395],{"class":266},[256,528,398],{"class":284},[256,530,402],{"class":401},[256,532,405],{"class":266},[256,534,536,538,540,542,544,546,549,551,553,555,557,559,561,563,565,567,569,571],{"class":258,"line":535},14,[256,537,411],{"class":303},[256,539,267],{"class":266},[256,541,416],{"class":270},[256,543,389],{"class":266},[256,545,422],{"class":421},[256,547,548],{"class":425},"LineTo ",[256,550,422],{"class":421},[256,552,395],{"class":266},[256,554,434],{"class":433},[256,556,389],{"class":266},[256,558,402],{"class":270},[256,560,441],{"class":266},[256,562,444],{"class":303},[256,564,267],{"class":266},[256,566,449],{"class":270},[256,568,389],{"class":266},[256,570,402],{"class":270},[256,572,456],{"class":266},[256,574,576,579,581,584,586,588,590,593,596,598,600,603,605,607,609,611],{"class":258,"line":575},15,[256,577,578],{"class":262},"    line ",[256,580,285],{"class":284},[256,582,583],{"class":270}," Line",[256,585,389],{"class":266},[256,587,304],{"class":303},[256,589,267],{"class":266},[256,591,592],{"class":270},"lastXyToComplex",[256,594,595],{"class":266},"(),",[256,597,444],{"class":303},[256,599,267],{"class":266},[256,601,602],{"class":270},"vectorToComplex",[256,604,389],{"class":266},[256,606,402],{"class":270},[256,608,484],{"class":266},[256,610,488],{"class":487},[256,612,613],{"class":266},"]))\n",[256,615,617,619,621,624,626,629,631,633],{"class":258,"line":616},16,[256,618,411],{"class":303},[256,620,267],{"class":266},[256,622,623],{"class":309},"svg_paths",[256,625,267],{"class":266},[256,627,628],{"class":270},"append",[256,630,389],{"class":266},[256,632,258],{"class":270},[256,634,369],{"class":266},[256,636,638,640,642,644,646,648,650,652,654,656,658,660,662,664,666,668,670,672,674],{"class":258,"line":637},17,[256,639,411],{"class":303},[256,641,267],{"class":266},[256,643,466],{"class":309},[256,645,395],{"class":266},[256,647,444],{"class":303},[256,649,267],{"class":266},[256,651,475],{"class":309},[256,653,478],{"class":284},[256,655,481],{"class":262},[256,657,484],{"class":266},[256,659,488],{"class":487},[256,661,491],{"class":266},[256,663,494],{"class":309},[256,665,395],{"class":266},[256,667,481],{"class":262},[256,669,484],{"class":266},[256,671,488],{"class":487},[256,673,491],{"class":266},[256,675,507],{"class":309},[256,677,679],{"class":258,"line":678},18,[256,680,375],{"emptyLinePlaceholder":223},[256,682,684,686,689,691,693,695,697,699],{"class":258,"line":683},19,[256,685,382],{"class":381},[256,687,688],{"class":385}," callbackConicTo",[256,690,389],{"class":266},[256,692,304],{"class":392},[256,694,395],{"class":266},[256,696,398],{"class":284},[256,698,402],{"class":401},[256,700,405],{"class":266},[256,702,704,706,708,710,712,714,717,719,721,723,725,727,729,731,733,735,737,739],{"class":258,"line":703},20,[256,705,411],{"class":303},[256,707,267],{"class":266},[256,709,416],{"class":270},[256,711,389],{"class":266},[256,713,422],{"class":421},[256,715,716],{"class":425},"ConicTo",[256,718,422],{"class":421},[256,720,395],{"class":266},[256,722,434],{"class":433},[256,724,389],{"class":266},[256,726,402],{"class":270},[256,728,441],{"class":266},[256,730,444],{"class":303},[256,732,267],{"class":266},[256,734,449],{"class":270},[256,736,389],{"class":266},[256,738,402],{"class":270},[256,740,456],{"class":266},[256,742,744,747,749,752,754,756,758,760,762,764,766,768,770,772,774,776,779,781,783,785,787,789,791,794],{"class":258,"line":743},21,[256,745,746],{"class":262},"    curve ",[256,748,285],{"class":284},[256,750,751],{"class":270}," QuadraticBezier",[256,753,389],{"class":266},[256,755,304],{"class":303},[256,757,267],{"class":266},[256,759,592],{"class":270},[256,761,595],{"class":266},[256,763,444],{"class":303},[256,765,267],{"class":266},[256,767,602],{"class":270},[256,769,389],{"class":266},[256,771,402],{"class":270},[256,773,484],{"class":266},[256,775,488],{"class":487},[256,777,778],{"class":266},"]),",[256,780,444],{"class":303},[256,782,267],{"class":266},[256,784,602],{"class":270},[256,786,389],{"class":266},[256,788,402],{"class":270},[256,790,484],{"class":266},[256,792,793],{"class":487},"1",[256,795,613],{"class":266},[256,797,799,801,803,805,807,809,811,814],{"class":258,"line":798},22,[256,800,411],{"class":303},[256,802,267],{"class":266},[256,804,623],{"class":309},[256,806,267],{"class":266},[256,808,628],{"class":270},[256,810,389],{"class":266},[256,812,813],{"class":270},"curve",[256,815,369],{"class":266},[256,817,819,821,823,825,827,829,831,833,835,837,839,841,843,845,847,849,851,853,855],{"class":258,"line":818},23,[256,820,411],{"class":303},[256,822,267],{"class":266},[256,824,466],{"class":309},[256,826,395],{"class":266},[256,828,444],{"class":303},[256,830,267],{"class":266},[256,832,475],{"class":309},[256,834,478],{"class":284},[256,836,481],{"class":262},[256,838,484],{"class":266},[256,840,793],{"class":487},[256,842,491],{"class":266},[256,844,494],{"class":309},[256,846,395],{"class":266},[256,848,481],{"class":262},[256,850,484],{"class":266},[256,852,793],{"class":487},[256,854,491],{"class":266},[256,856,507],{"class":309},[256,858,860],{"class":258,"line":859},24,[256,861,375],{"emptyLinePlaceholder":223},[256,863,865,867,870,872,874,876,878,880],{"class":258,"line":864},25,[256,866,382],{"class":381},[256,868,869],{"class":385}," callbackCubicTo",[256,871,389],{"class":266},[256,873,304],{"class":392},[256,875,395],{"class":266},[256,877,398],{"class":284},[256,879,402],{"class":401},[256,881,405],{"class":266},[256,883,885,887,889,891,893,895,898,900,902,904,906,908,910,912,914,916,918,920],{"class":258,"line":884},26,[256,886,411],{"class":303},[256,888,267],{"class":266},[256,890,416],{"class":270},[256,892,389],{"class":266},[256,894,422],{"class":421},[256,896,897],{"class":425},"CubicTo",[256,899,422],{"class":421},[256,901,395],{"class":266},[256,903,434],{"class":433},[256,905,389],{"class":266},[256,907,402],{"class":270},[256,909,441],{"class":266},[256,911,444],{"class":303},[256,913,267],{"class":266},[256,915,449],{"class":270},[256,917,389],{"class":266},[256,919,402],{"class":270},[256,921,456],{"class":266},[256,923,925,927,929,932],{"class":258,"line":924},27,[256,926,746],{"class":262},[256,928,285],{"class":284},[256,930,931],{"class":270}," CubicBezier",[256,933,274],{"class":266},[256,935,937,940,942,944],{"class":258,"line":936},28,[256,938,939],{"class":303},"        self",[256,941,267],{"class":266},[256,943,592],{"class":270},[256,945,946],{"class":266},"(),\n",[256,948,950,952,954,956,958,960,962,964],{"class":258,"line":949},29,[256,951,939],{"class":303},[256,953,267],{"class":266},[256,955,602],{"class":270},[256,957,389],{"class":266},[256,959,402],{"class":270},[256,961,484],{"class":266},[256,963,488],{"class":487},[256,965,966],{"class":266},"]),\n",[256,968,970,972,974,976,978,980,982,984],{"class":258,"line":969},30,[256,971,939],{"class":303},[256,973,267],{"class":266},[256,975,602],{"class":270},[256,977,389],{"class":266},[256,979,402],{"class":270},[256,981,484],{"class":266},[256,983,793],{"class":487},[256,985,966],{"class":266},[256,987,989,991,993,995,997,999,1001,1004],{"class":258,"line":988},31,[256,990,939],{"class":303},[256,992,267],{"class":266},[256,994,602],{"class":270},[256,996,389],{"class":266},[256,998,402],{"class":270},[256,1000,484],{"class":266},[256,1002,1003],{"class":487},"2",[256,1005,966],{"class":266},[256,1007,1009],{"class":258,"line":1008},32,[256,1010,1011],{"class":266},"    )\n",[256,1013,1015,1017,1019,1021,1023,1025,1027,1029],{"class":258,"line":1014},33,[256,1016,411],{"class":303},[256,1018,267],{"class":266},[256,1020,623],{"class":309},[256,1022,267],{"class":266},[256,1024,628],{"class":270},[256,1026,389],{"class":266},[256,1028,813],{"class":270},[256,1030,369],{"class":266},[256,1032,1034,1036,1038,1040,1042,1044,1046,1048,1050,1052,1054,1056,1058,1060,1062,1064,1066,1068,1070],{"class":258,"line":1033},34,[256,1035,411],{"class":303},[256,1037,267],{"class":266},[256,1039,466],{"class":309},[256,1041,395],{"class":266},[256,1043,444],{"class":303},[256,1045,267],{"class":266},[256,1047,475],{"class":309},[256,1049,478],{"class":284},[256,1051,481],{"class":262},[256,1053,484],{"class":266},[256,1055,1003],{"class":487},[256,1057,491],{"class":266},[256,1059,494],{"class":309},[256,1061,395],{"class":266},[256,1063,481],{"class":262},[256,1065,484],{"class":266},[256,1067,1003],{"class":487},[256,1069,491],{"class":266},[256,1071,507],{"class":309},[30,1073,1074],{},"除了 SVG 的路径之外，还有一个很重要的内容，就是 viewBox。因为在字体中，表达一个字形，除了轮廓之外，还有一个很重要的 metrics 信息，metrics 信息没有还原好，在字体排版的时候就会出现问题。如果以轮廓本身的大小作为容器的话，一些未占满空间的文字，或者一些标点符号，在排版时，就会被缩放的很大；而如果以字体的上沿线和下沿线来计算一个方形容器显示的话，对于中文字符问题不大，但对于半角字符，其中间的间隙就会显得太大，如下图：",[151,1076],{"filename":1077},"11.png",[30,1079,1080],{},"因此，我们需要从 Metrics 中提取每个字形的基本信息。以横排为例，取每个字形自己的宽度来绘制 viewBox，高度还是通过上下沿线相减来计算。这样在排版时就可以得到想要的效果了：",[151,1082],{"filename":1083},"12.png",[247,1085,1087],{"className":249,"code":1086,"language":251,"meta":252,"style":252},"def calcViewBox(self, metrics):\n    view_box_min_x = 0\n    view_box_min_y = -self.face.ascender\n    view_box_width = metrics.horiAdvance\n    view_box_height = self.face.ascender - self.face.descender\n    return f\"{view_box_min_x} {view_box_min_y} {view_box_width} {view_box_height}\"\n",[84,1088,1089,1107,1117,1139,1153,1184],{"__ignoreMap":252},[256,1090,1091,1093,1096,1098,1100,1102,1105],{"class":258,"line":259},[256,1092,382],{"class":381},[256,1094,1095],{"class":385}," calcViewBox",[256,1097,389],{"class":266},[256,1099,304],{"class":392},[256,1101,395],{"class":266},[256,1103,1104],{"class":401}," metrics",[256,1106,405],{"class":266},[256,1108,1109,1112,1114],{"class":258,"line":277},[256,1110,1111],{"class":262},"    view_box_min_x ",[256,1113,285],{"class":284},[256,1115,1116],{"class":487}," 0\n",[256,1118,1119,1122,1124,1127,1129,1131,1134,1136],{"class":258,"line":295},[256,1120,1121],{"class":262},"    view_box_min_y ",[256,1123,285],{"class":284},[256,1125,1126],{"class":284}," -",[256,1128,304],{"class":303},[256,1130,267],{"class":266},[256,1132,1133],{"class":309},"face",[256,1135,267],{"class":266},[256,1137,1138],{"class":309},"ascender\n",[256,1140,1141,1144,1146,1148,1150],{"class":258,"line":315},[256,1142,1143],{"class":262},"    view_box_width ",[256,1145,285],{"class":284},[256,1147,1104],{"class":262},[256,1149,267],{"class":266},[256,1151,1152],{"class":309},"horiAdvance\n",[256,1154,1155,1158,1160,1162,1164,1166,1168,1171,1173,1175,1177,1179,1181],{"class":258,"line":332},[256,1156,1157],{"class":262},"    view_box_height ",[256,1159,285],{"class":284},[256,1161,444],{"class":303},[256,1163,267],{"class":266},[256,1165,1133],{"class":309},[256,1167,267],{"class":266},[256,1169,1170],{"class":309},"ascender",[256,1172,1126],{"class":284},[256,1174,444],{"class":303},[256,1176,267],{"class":266},[256,1178,1133],{"class":309},[256,1180,267],{"class":266},[256,1182,1183],{"class":309},"descender\n",[256,1185,1186,1190,1193,1195,1198,1201,1204,1207,1210,1212,1214,1217,1219,1221,1224,1226],{"class":258,"line":349},[256,1187,1189],{"class":1188},"sVHd0","    return",[256,1191,1192],{"class":381}," f",[256,1194,422],{"class":425},[256,1196,1197],{"class":487},"{",[256,1199,1200],{"class":262},"view_box_min_x",[256,1202,1203],{"class":487},"}",[256,1205,1206],{"class":487}," {",[256,1208,1209],{"class":262},"view_box_min_y",[256,1211,1203],{"class":487},[256,1213,1206],{"class":487},[256,1215,1216],{"class":262},"view_box_width",[256,1218,1203],{"class":487},[256,1220,1206],{"class":487},[256,1222,1223],{"class":262},"view_box_height",[256,1225,1203],{"class":487},[256,1227,1228],{"class":425},"\"\n",[30,1230,1231,1232,40,1235,40,1238,40,1241,1244],{},"在有了 ",[84,1233,1234],{},"path",[84,1236,1237],{},"viewBox",[84,1239,1240],{},"width",[84,1242,1243],{},"height"," 等信息之后，我们就可以直接生成能表达具体字形所需要的 SVG 文件了。",[30,1246,1247,1248,1250],{},"还有一个特殊字符，那就是空格，他是不包含任何轮廓的，解析轮廓的时候结果是空的。在生成 SVG 的时候，如果 path 是为空会失败，这时候可以生成一个和 ",[84,1249,1237],{}," 同样大小的透明度为 0 的轮廓用来占位。",[247,1252,1254],{"className":249,"code":1253,"language":251,"meta":252,"style":252},"path = (\n    Path(*self.svg_paths).scaled(1, -1)\n    if len(self.svg_paths) > 0\n    else parse_path(\n        f\"M 0,{-self.face.ascender} L {metrics.horiAdvance},{-self.face.ascender} L {metrics.horiAdvance},{-self.face.descender} L 0,{-self.face.descender} Z\"\n    )\n)\n",[84,1255,1256,1266,1300,1323,1333,1444,1448],{"__ignoreMap":252},[256,1257,1258,1261,1263],{"class":258,"line":259},[256,1259,1260],{"class":262},"path ",[256,1262,285],{"class":284},[256,1264,1265],{"class":266}," (\n",[256,1267,1268,1271,1273,1276,1278,1280,1282,1285,1288,1290,1292,1294,1296,1298],{"class":258,"line":277},[256,1269,1270],{"class":270},"    Path",[256,1272,389],{"class":266},[256,1274,1275],{"class":284},"*",[256,1277,304],{"class":303},[256,1279,267],{"class":266},[256,1281,623],{"class":309},[256,1283,1284],{"class":266},").",[256,1286,1287],{"class":270},"scaled",[256,1289,389],{"class":266},[256,1291,793],{"class":487},[256,1293,395],{"class":266},[256,1295,1126],{"class":284},[256,1297,793],{"class":487},[256,1299,369],{"class":266},[256,1301,1302,1305,1307,1309,1311,1313,1315,1318,1321],{"class":258,"line":295},[256,1303,1304],{"class":1188},"    if",[256,1306,434],{"class":433},[256,1308,389],{"class":266},[256,1310,304],{"class":303},[256,1312,267],{"class":266},[256,1314,623],{"class":309},[256,1316,1317],{"class":266},")",[256,1319,1320],{"class":284}," >",[256,1322,1116],{"class":487},[256,1324,1325,1328,1331],{"class":258,"line":315},[256,1326,1327],{"class":1188},"    else",[256,1329,1330],{"class":270}," parse_path",[256,1332,274],{"class":266},[256,1334,1335,1338,1341,1343,1346,1348,1350,1352,1354,1356,1358,1361,1363,1366,1368,1371,1373,1375,1377,1379,1381,1383,1385,1387,1389,1391,1393,1395,1397,1399,1401,1403,1405,1407,1409,1411,1413,1415,1417,1420,1422,1425,1427,1429,1431,1433,1435,1437,1439,1441],{"class":258,"line":332},[256,1336,1337],{"class":381},"        f",[256,1339,1340],{"class":425},"\"M 0,",[256,1342,1197],{"class":487},[256,1344,1345],{"class":284},"-",[256,1347,304],{"class":303},[256,1349,267],{"class":266},[256,1351,1133],{"class":309},[256,1353,267],{"class":266},[256,1355,1170],{"class":309},[256,1357,1203],{"class":487},[256,1359,1360],{"class":425}," L ",[256,1362,1197],{"class":487},[256,1364,1365],{"class":270},"metrics",[256,1367,267],{"class":266},[256,1369,1370],{"class":309},"horiAdvance",[256,1372,1203],{"class":487},[256,1374,395],{"class":425},[256,1376,1197],{"class":487},[256,1378,1345],{"class":284},[256,1380,304],{"class":303},[256,1382,267],{"class":266},[256,1384,1133],{"class":309},[256,1386,267],{"class":266},[256,1388,1170],{"class":309},[256,1390,1203],{"class":487},[256,1392,1360],{"class":425},[256,1394,1197],{"class":487},[256,1396,1365],{"class":270},[256,1398,267],{"class":266},[256,1400,1370],{"class":309},[256,1402,1203],{"class":487},[256,1404,395],{"class":425},[256,1406,1197],{"class":487},[256,1408,1345],{"class":284},[256,1410,304],{"class":303},[256,1412,267],{"class":266},[256,1414,1133],{"class":309},[256,1416,267],{"class":266},[256,1418,1419],{"class":309},"descender",[256,1421,1203],{"class":487},[256,1423,1424],{"class":425}," L 0,",[256,1426,1197],{"class":487},[256,1428,1345],{"class":284},[256,1430,304],{"class":303},[256,1432,267],{"class":266},[256,1434,1133],{"class":309},[256,1436,267],{"class":266},[256,1438,1419],{"class":309},[256,1440,1203],{"class":487},[256,1442,1443],{"class":425}," Z\"\n",[256,1445,1446],{"class":258,"line":349},[256,1447,1011],{"class":266},[256,1449,1450],{"class":258,"line":366},[256,1451,369],{"class":266},[69,1453,1454],{"id":1454},"怎么设置字体样式",[30,1456,1457],{},"在前面的步骤中，我们为每个字形预先生成好了对应的 SVG 文件，但我们知道，SVG 文件是一个静态的文件，其字体颜色等样式是固定的，在前端使用的时候，如何去动态设置颜色等样式呢？",[30,1459,1460,1461,1464,1465,1467,1468,1471,1472,1475],{},"我们在前端去加载 SVG 字体文件的时候，并不是直接通过 ",[84,1462,1463],{},"img"," 标签的方式来引入 SVG，而是在前端组件中去下载 SVG 文件内容并解析 DOM 对象，然后根据需要去修改对应的属性或添加额外的样式，例如修改 ",[84,1466,1234],{}," 的 ",[84,1469,1470],{},"fill"," 属性来设置颜色，修改完样式后再把修改后的 SVG 标签直接插入到文档中。还有一些字体全局的样式信息，例如下划线的位置、粗细等，在不同的字体中，也是有不一样的配置的。我们需要想办法把这些信息也通过 SVG 传递过来。解决方法也很简单，我们在 SVG 的根节点上添加一个自定义的属性，将所需传递的信息转成 ",[84,1473,1474],{},"JSON"," 格式然后塞到这个属性里，前端在解析 SVG 的 DOM 时，把这个属性中的数据取出来解析并渲染就可以了。",[151,1477],{"filename":1478},"13.png",[30,1480,1481,1482,1485,1486,1489,1490,1493],{},"上面截图中，",[84,1483,1484],{},"underline"," 和 ",[84,1487,1488],{},"underlineThickness"," 就分别代表了下划线位置和粗细信息，在解析到这两个信息后再做一些转换处理，可通过 ",[84,1491,1492],{},"::before"," 伪元素来绘制下划线：",[151,1495],{"filename":1496},"14.png",[59,1498,1499],{"id":1499},"实践案例",[69,1501,1502],{"id":1502},"页面接入",[30,1504,1505],{},"创作中心每周会给 UP 主推送荣誉周报，页面中首屏有非常大的篇幅显示的是本周关键词，效果如下图：",[151,1507],{"filename":1508},"15.jpg",[30,1510,1511],{},"这个场景有两处使用了特殊字体「方正 FW 筑紫黑」，一个是卡片标题「本周关键词」这几个字，这个是固定标题，用传统的切图方式也是 OK 的，但下面的关键词是根据 UP 主上周的投稿、评论、弹幕、播放等数据由服务端动态下发的，这个就无法通过切图来实现了，需要穷举的关键词特别多。",[30,1513,1514],{},"在使用本方案之前，该页面使用的是直接引入字体文件来设置字体。由于该页面加载资源较多，在加载时不可避免会出现字体闪烁、图片资源加载过程页面抖动的问题，在手机上通过 4G 网络访问时尤其明显。为了避免这种情况，该页面渲染之前加入了一个 Loading 页，在 Loading 页上加载所需的所有资源，包括图片、字体文件等。",[30,1516,1517],{},"使用本方案优化前后的数据对比如下：",[1519,1520,1521,1539],"table",{},[1522,1523,1524],"thead",{},[1525,1526,1527,1530,1533,1536],"tr",{},[1528,1529],"th",{},[1528,1531,1532],{},"渲染文字所需资源",[1528,1534,1535],{},"首屏加载时长",[1528,1537,1538],{},"页面跳失率",[1540,1541,1542,1557,1571],"tbody",{},[1525,1543,1544,1548,1551,1554],{},[1545,1546,1547],"td",{},"优化前",[1545,1549,1550],{},"2855KB",[1545,1552,1553],{},"2268ms",[1545,1555,1556],{},"5.47%",[1525,1558,1559,1562,1565,1568],{},[1545,1560,1561],{},"优化后",[1545,1563,1564],{},"45KB",[1545,1566,1567],{},"1791ms",[1545,1569,1570],{},"3.11%",[1525,1572,1573,1576,1579,1582],{},[1545,1574,1575],{},"对比",[1545,1577,1578],{},"🔽 下降 98.4%",[1545,1580,1581],{},"🔽 下降 21%",[1545,1583,1584],{},"🔽 下降 2.36PP",[1586,1587,1588,1597,1603],"ul",{},[1589,1590,1591,1592,1596],"li",{},"渲染文字所需资源：优化前字体文件为 2855KB，优化后 JS 组件库为 37KB，4 个 SVG 为 8KB，总共 45KB 的资源，",[1593,1594,1595],"strong",{},"下降了 98.4%","。",[1589,1598,1599,1600,1596],{},"首屏加载时长：从 2268ms 下降至 1791ms，",[1593,1601,1602],{},"下降了 21%",[1589,1604,1605,1606,1596],{},"页面跳失率：从 5.47% 降低至 3.11%，",[1593,1607,1608],{},"下降了 2.36PP",[30,1610,1611],{},"最终本方案不仅通过降低首屏加载时长提升了用户的加载体验，同时也获得了页面跳失率下降的业务结果。",[69,1613,1614],{"id":1614},"失败兜底",[30,1616,1617],{},"尽管本方案所需加载的资源量相比直接引入字体包要小很多，但请求数量会根据字符数有所上升，在极端情况下，网络失败会导致资源请求失败；另外，一些字体包含的字符数有限，在请求这个字体本身就不存在的字符的时候，也会出现请求失败的情况。在缺失 SVG 的情况下如何进行兜底的显示也是我们需要考虑的内容。",[30,1619,1620],{},"在组件侧获取不到对应的 SVG 时，可通过系统默认字体直接在字符位置进行渲染作为兜底，与浏览器默认的处理方式一致。具体效果如下：",[151,1622],{"filename":1623},"16.png",[30,1625,1626],{},"由于兜底的默认字体与目标字体不同，他们的容器大小和下划线位置、下划线粗细等设置均有可能不一样，所以在开启了下划线的情况下，会出现下划线高度和粗细不一致的现象，如下图：",[151,1628],{"filename":1629},"17.png",[30,1631,1632],{},"为了解决这个问题，我们在渲染下划线的时候，对于兜底的默认字体，不采用 css 样式来绘制下划线，而是与 SVG 一样，通过伪元素并且使用目标字体的下划线设置来进行绘制，最终效果如下图：",[151,1634],{"filename":1635},"18.png",[59,1637,1638],{"id":1638},"总结",[30,1640,1641],{},"在本文中，我们深入探讨了 Font2svg 方案的技术原理和实现细节。我们通过将字体转换成 SVG 进行渲染，降低了用户渲染特殊字体动态文字所需下载的文件大小，提高了加载速度，从而优化了特殊字体在 Web 页面中的加载体验。",[30,1643,1644],{},"这套方案在动态渲染特殊字体文案的场景下具有广泛的应用前景，不只是在 Web 端，在客户端上也同样适用。它能为设计师和开发者提供更灵活、高效的特殊字体渲染方案，让设计师不会再因为字体包体积而放弃使用一些艺术字体，同时也让开发者不再为字体包的大小而头疼。",[1646,1647,1648],"style",{},"html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .slqww, html code.shiki .slqww{--shiki-light:#6182B8;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s39Yj, html code.shiki .s39Yj{--shiki-light:#39ADB5;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .smCYv, html code.shiki .smCYv{--shiki-light:#E53935;--shiki-light-font-style:italic;--shiki-default:#24292E;--shiki-default-font-style:inherit;--shiki-dark:#E1E4E8;--shiki-dark-font-style:inherit}html pre.shiki code .sFwrP, html code.shiki .sFwrP{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#24292E;--shiki-default-font-style:inherit;--shiki-dark:#E1E4E8;--shiki-dark-font-style:inherit}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sptTA, html code.shiki .sptTA{--shiki-light:#6182B8;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}",{"title":252,"searchDepth":277,"depth":277,"links":1650},[1651,1652,1653,1654],{"id":61,"depth":277,"text":61},{"id":97,"depth":277,"text":97},{"id":1499,"depth":277,"text":1499},{"id":1638,"depth":277,"text":1638},null,"2023-07-19",false,"md",{},"\u002Fposts\u002F2023\u002Ffont2svg-solution",{"text":1662,"minutes":1663,"time":1664,"words":1665},"21 min read",20.145,1208700,4029,{"title":22,"description":252},{"loc":1660},"posts\u002F2023\u002F20230719.font2svg-solution",[1670,1671],"技术","前端","ExIjK3gmZDqEqp_xAilxKHvuQ01bwCJnvj3dq-s3k6o",[1674,1680],{"title":1675,"path":1676,"stem":1677,"date":1678,"description":1679,"children":-1},"关于成长的思考","\u002Fposts\u002F2023\u002Fthoughts-about-growth","posts\u002F2023\u002F20230728.thoughts-about-growth","2023-07-28","最近参加了公司的一个管理培训计划，上了很多次课，也做了很多课堂及课后作业，也参加了导师对于最后演讲答辩的指导，以及最后的导师点评和分享环节。",{"title":1681,"path":1682,"stem":1683,"date":1684,"description":1685,"children":-1},"使用 Cloudflare 加速博客海外访问速度","\u002Fposts\u002F2023\u002Fuse-cloudflare-speed-up-overseas-traffic","posts\u002F2023\u002F20230518.use-cloudflare-speed-up-overseas-traffic","2023-05-18","这几天对 hadb.me 博客又做了一次迁移和优化。",1777579138378]