-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeed.xml
2024 lines (1911 loc) · 405 KB
/
feed.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Xheldon Blog</title>
<description>The Answer to Life, the Universe and Everything is...</description>
<link>https://www.xheldon.com</link>
<atom:link href="https://www.xheldon.com/feed.xml" rel="self" type="application/rss+xml" />
<pubDate>Tue, 07 Jan 2025 14:32:39 +0000</pubDate>
<lastBuildDate>Tue, 07 Jan 2025 14:32:39 +0000</lastBuildDate>
<generator>Hexo v7.3.0</generator>
<item>
<title>Jekyll 迁移到 Hexo 问题记录</title>
<description><h2 id="前言">前言</h2>
<p>我是一名前端开发人员,因此对其他的一些脚本语言如 Ruby 并不熟悉。刚开始写博客的时候使用了 Github Pages 作为博客平台,它默认使用的是 Jeklly 框架,想着内容比框架更重要,因此也就用着了,这一用就是 10 年。</p>
<p>期间换过一些主题,后来锁定了 Hux 提供的主题,简洁大方好看,而且是开源的:</p>
<p><a href="https://huangxuan.me/" target="_blank"> 黄玄的博客 | Hux Blog</a></p>
<p>我针对这个主题做了很多的定制化内容,如自定义右侧内容、自定义数据、使用 Notion 作为数据源渲染等。</p>
<p>但随着苹果的 M 系列芯片的发布,我越来越难以处理 Ruby 在 Intel 和 Apple 芯片之间的差异,举个例子来说,我构建的时候需要特定使用 x86 架构的指令才能偶然正确 build,这还是我不能随便动任意一个依赖的前提下:</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-built_in">arch</span> -x86_64 bundle <span class="hljs-built_in">exec</span> jekyll server --trace --config=_config.dev.yml --ssl-key local.xheldon.cn.key --ssl-cert local.xheldon.cn.pem<br></code></pre></td></tr></table></figure>
<p>因此我意识到如果再不尽快更换框架,我可能将来就完全无法发布博客了。</p>
<h2 id="技术选型">技术选型</h2>
<p>这一节没有什么好说的,你基本可以认为,Hexo 是 Jekyll 的 JavaScript 实现。里面的很多概念,95% 的都相同,因此迁移上手无难度。</p>
<p>更重要的是,Hexo 中也有人做了 Hux 的博客主题模板,因此我就直接拿来用了,在这个过程中简单记录一下过程。</p>
<h2 id="迁移过程">迁移过程</h2>
<h3 id="插件迁移">插件迁移</h3>
<p>这个属于比较容易的,在 Jekyll 中的插件,在 Hexo 中我是用辅助函数实现,如下是我处理来自 Notion 的 Bookmark 的标签的函数(路径是 <code>_plugins/add-attribute.rb</code>:)</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><code class="hljs ruby"><span class="hljs-keyword">module</span> <span class="hljs-title class_">Jekyll</span><br> <span class="hljs-keyword">class</span> <span class="hljs-title class_">RenderBookMarkBlock</span> &lt; <span class="hljs-title class_ inherited__">Liquid::Block</span><br> <span class="hljs-keyword">def</span> <span class="hljs-title function_">initialize</span>(<span class="hljs-params">tag_name, attr, tokens</span>)<br> <span class="hljs-variable language_">super</span><br> <span class="hljs-comment"># 普通的链接没有 yid 和 bid</span><br> attrs = attr.scan(<span class="hljs-regexp">/url\=\&quot;(.*)\&quot;\stitle\=\&quot;(.*)\&quot;\simg\=\&quot;(.*)\&quot;\syid\=\&quot;(.*)\&quot;\sbid\=\&quot;(.*)\&quot;/</span>)<br> <span class="hljs-keyword">if</span> !attrs.empty?<br> <span class="hljs-variable">@url</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]<br> <span class="hljs-variable">@title</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>]<br> <span class="hljs-variable">@img</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">2</span>]<br> <span class="hljs-variable">@yid</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">3</span>]<br> <span class="hljs-variable">@bid</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">4</span>]<br> <span class="hljs-variable">@firstChar</span> = <span class="hljs-variable">@title</span>.empty? ? <span class="hljs-string">&quot;&quot;</span> : (<span class="hljs-variable">@title</span>)[<span class="hljs-number">0</span>].upcase<br> <span class="hljs-variable">@error</span> = <span class="hljs-string">&quot;&quot;</span><br> <span class="hljs-keyword">else</span><br> attrs = attr.scan(<span class="hljs-regexp">/url\=\&quot;(.*)\&quot;\stitle\=\&quot;(.*)\&quot;\simg\=\&quot;(.*)\&quot;/</span>)<br> <span class="hljs-variable">@url</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]<br> <span class="hljs-variable">@title</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>]<br> <span class="hljs-variable">@img</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">2</span>]<br> <span class="hljs-variable">@firstChar</span> = <span class="hljs-variable">@title</span>.empty? ? <span class="hljs-string">&quot;&quot;</span> : (<span class="hljs-variable">@title</span>)[<span class="hljs-number">0</span>].upcase<br> <span class="hljs-variable">@error</span> = <span class="hljs-string">&quot;&quot;</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">def</span> <span class="hljs-title function_">render</span>(<span class="hljs-params">context</span>)<br> <span class="hljs-variable">@desc</span> = <span class="hljs-variable language_">super</span><br> <span class="hljs-keyword">if</span> !<span class="hljs-variable">@yid</span>.<span class="hljs-literal">nil</span>? &amp;&amp; !<span class="hljs-variable">@yid</span>.empty?<br> <span class="hljs-string">&quot;&lt;p class=&#x27;embed-responsive embed-responsive-16by9&#x27;&gt;&lt;iframe src=&#x27;https://www.youtube.com/embed/<span class="hljs-subst">#&#123;<span class="hljs-variable">@yid</span>&#125;</span>?rel=0&#x27; title=&#x27;YouTube video player&#x27; frameborder=&#x27;0&#x27; allow=&#x27;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&#x27; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;&quot;</span><br> <span class="hljs-keyword">elsif</span> !<span class="hljs-variable">@bid</span>.<span class="hljs-literal">nil</span>? &amp;&amp; !<span class="hljs-variable">@bid</span>.empty?<br> <span class="hljs-string">&quot;&lt;p class=&#x27;embed-responsive embed-responsive-16by9&#x27; style=&#x27;border-bottom: 1px solid #ddd;&#x27;&gt;&lt;iframe src=&#x27;//player.bilibili.com/player.html?bvid=<span class="hljs-subst">#&#123;<span class="hljs-variable">@bid</span>&#125;</span>&amp;high_quality=1&amp;as_wide=1&#x27; scrolling=&#x27;no&#x27; border=&#x27;0&#x27; frameborder=&#x27;no&#x27; framespacing=&#x27;0&#x27; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;&quot;</span><br> <span class="hljs-keyword">else</span><br> <span class="hljs-string">&quot;&lt;p&gt;&lt;a class=&#x27;link-bookmark&#x27; href=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@url</span>&#125;</span>&#x27; target=&#x27;_blank&#x27;&gt;&lt;span data-bookmark-img=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@img</span>&#125;</span>&#x27; data-bookmark-title=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@firstChar</span>&#125;</span>&#x27;&gt;&lt;img src=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@img</span>&#125;</span>&#x27;/&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;<span class="hljs-subst">#&#123;<span class="hljs-variable">@title</span>&#125;</span>&lt;/span&gt;&lt;span&gt;<span class="hljs-subst">#&#123;<span class="hljs-variable">@desc</span>&#125;</span>&lt;/span&gt;&lt;span&gt;<span class="hljs-subst">#&#123;<span class="hljs-variable">@url</span>&#125;</span>&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&quot;</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br><br><span class="hljs-title class_">Liquid</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:Template</span>.register_tag(<span class="hljs-string">&#x27;render_bookmark&#x27;</span>, <span class="hljs-title class_">Jekyll</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:RenderBookMarkBlock</span>)<br></code></pre></td></tr></table></figure>
<p>而在 Hexo 中我是这么写的(路径是 <code>scripts/liquid.js</code>):</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs javascript">hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">tag</span>.<span class="hljs-title function_">register</span>(<span class="hljs-string">&#x27;render_bookmark&#x27;</span>, <span class="hljs-keyword">function</span>(<span class="hljs-params">args, content</span>) &#123;<br> <span class="hljs-keyword">const</span> [url, title, img, yid, bid] = args.<span class="hljs-title function_">map</span>(getValue);<br> <span class="hljs-keyword">const</span> firstChar = title ? title[<span class="hljs-number">0</span>].<span class="hljs-title function_">toUpperCase</span>() : <span class="hljs-string">&#x27;&#x27;</span>;<br> <span class="hljs-keyword">const</span> strip_html = hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">helper</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;strip_html&#x27;</span>).<span class="hljs-title function_">bind</span>(hexo);<br> <span class="hljs-keyword">const</span> trim = hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">helper</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;trim&#x27;</span>).<span class="hljs-title function_">bind</span>(hexo);<br> <span class="hljs-keyword">if</span> (yid) &#123;<br> <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;p class=&#x27;embed-responsive embed-responsive-16by9&#x27;&gt;&lt;iframe src=&#x27;https://www.youtube.com/embed/<span class="hljs-subst">$&#123;yid&#125;</span>?rel=0&#x27; title=&#x27;YouTube video player&#x27; frameborder=&#x27;0&#x27; allow=&#x27;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&#x27; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;`</span><br> &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (bid) &#123;<br> <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;p class=&#x27;embed-responsive embed-responsive-16by9&#x27; style=&#x27;border-bottom: 1px solid #ddd;&#x27;&gt;&lt;iframe src=&#x27;//player.bilibili.com/player.html?bvid=<span class="hljs-subst">$&#123;bid&#125;</span>&amp;high_quality=1&amp;as_wide=1&#x27; scrolling=&#x27;no&#x27; border=&#x27;0&#x27; frameborder=&#x27;no&#x27; framespacing=&#x27;0&#x27; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;`</span>;<br> &#125;<br> <span class="hljs-keyword">return</span> <span class="hljs-string">`&lt;p&gt;&lt;a class=&#x27;link-bookmark&#x27; href=&#x27;<span class="hljs-subst">$&#123;url&#125;</span>&#x27; target=&#x27;_blank&#x27;&gt;&lt;span data-bookmark-img=&#x27;<span class="hljs-subst">$&#123;img&#125;</span>&#x27; data-bookmark-title=&#x27;<span class="hljs-subst">$&#123;firstChar&#125;</span>&#x27;&gt;&lt;img src=&#x27;<span class="hljs-subst">$&#123;img&#125;</span>&#x27;/&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; <span class="hljs-subst">$&#123;title&#125;</span>&lt;/span&gt;&lt;span&gt; <span class="hljs-subst">$&#123;strip_html(trim(content))&#125;</span>&lt;/span&gt;&lt;span&gt; <span class="hljs-subst">$&#123;url&#125;</span>&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;`</span>;<br>&#125;, &#123;<br> <span class="hljs-attr">ends</span>: <span class="hljs-literal">true</span>,<br>&#125;);<br></code></pre></td></tr></table></figure>
<h3 id="permalink-问题">Permalink 问题</h3>
<p>不知道为什么,我在 Hexo 中设置了 <code>:category/:name.html</code> 但是依然给我生成的是 <code>life/2024-life-xxx.html</code>(使用 <code>-</code>目录分割) 而预期应该是 <code>life/xxx.html</code>,看源码 <code>name</code> 使用了 <code>slug</code>,的 <code>basename</code>,但是 <code>slug</code> 生成逻辑是基于 folder 路径然后加上 - 的,因此我只能自己手动修改文件名。Jekyll 中,文件名前的日期格式,如 <a href="http://2024-02-12-xxx.md">2024-02-12-xxx.md</a> 中,2024-02-12 会被忽略,title 直接就是 xxx,但是在 Hexo 中 title 在 post 中读取的 title front matter,所以只能写一个 filter 插件来最终确定 permalink 地址:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * permalink 中的 name 不符合预期,对于 _posts/life/2015/xxx.md 来说,在文档中 :name 表示的是 xxx,但是实际是 life-2015-xxx</span><br><span class="hljs-comment"> */</span><br>hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">filter</span>.<span class="hljs-title function_">register</span>(<span class="hljs-string">&#x27;post_permalink&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">data</span>) &#123;<br> <span class="hljs-comment">// 在这里修改 post.name 的值</span><br> <span class="hljs-keyword">const</span> arr = data.<span class="hljs-title function_">split</span>(<span class="hljs-string">&#x27;/&#x27;</span>).<span class="hljs-title function_">filter</span>(<span class="hljs-title class_">Boolean</span>);<br> <span class="hljs-keyword">const</span> categories = arr[<span class="hljs-number">0</span>];<br> <span class="hljs-keyword">const</span> name = arr[<span class="hljs-number">1</span>];<br> <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">$&#123;categories&#125;</span>/<span class="hljs-subst">$&#123;name.split(<span class="hljs-string">&#x27;-&#x27;</span>).filter(<span class="hljs-built_in">Boolean</span>).slice(<span class="hljs-number">3</span>).join(<span class="hljs-string">&#x27;-&#x27;</span>)&#125;</span>`</span>;<br>&#125;);<br></code></pre></td></tr></table></figure>
<p>另外,permalink 不能是纯数字,得用字符串:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2025/jekyll-2-hexo/147800e8-7a61-8006-a660-e8ecfbe7da9a.webp' alt='' title=''></p>
<p>不然报错(看 .endsWith 就知道为什么报错啦):</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2025/jekyll-2-hexo/147800e8-7a61-8032-8e51-d6528f277306.webp' alt='' title=''></p>
<h3 id="ejs-模板语法问题">EJS 模板语法问题</h3>
<p>ejs 模板嵌套的时候,与 Jekyll 的 liquid 语法不同的是,它不能在模板中自定义 front-matter。也就是说,模板之间的传参,只能外层到内层,不能从内到外。比如我 index 的 layout 是 page,同时还设置了 fornt-matter,但是此 front-matter 无法被 page.ejs 这个模板读取到,因此我只能每个用到的地方都往下传参。</p>
<h3 id="markdown-语法渲染问题">Markdown 语法渲染问题</h3>
<p>Hexo 自带的 marked 的 markdown 渲染跟 jekyll 的 karmarkdown 渲染有出入,前者会把诸如 ## h2 (换行+空行)段落 中的空行+段落给忽略,而后者不会。</p>
<p>虽然无伤大雅,但是这样会导致我在首页的时候,内容摘要会少一个空格导致不完全一致,而我希望尽可能的完全一致,这样 SEO 才不会降权,才能让搜索引擎不会认为我的内容有巨大改动。</p>
<p>于是我使用了 markdown-it 进行处理,安装 hexo-renderer-markdown-it 即可。</p>
<h3 id="liquid-排序问题">Liquid 排序问题</h3>
<p>我 Jekyll 的 liquid 的排序是这么用的: <code>&#123;&#123; tags | split:'`**`SEPARATOR`**`' | sort &#125;&#125;</code> :</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2025/jekyll-2-hexo/0389dcbb-8bb7-413c-a968-4a77a8b76b71.webp' alt='' title=''></p>
<p>问题是,Liquid 的排序在 sort 部分相同后,会按照后面的 href 字符串排序的,因此继承过来:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2025/jekyll-2-hexo/a24e2d73-ab8b-4b5d-a57d-7e0f2f5f0a66.webp' alt='' title=''></p>
<h3 id="mermaid-语法问题">Mermaid 语法问题</h3>
<p>mermaid 不能被 highlight 高亮,需要在 hexo 的 config 中排除掉:</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">exclude_languages:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">mermaid</span><br></code></pre></td></tr></table></figure>
<h3 id="markdown-无法使用-ejs-语法问题">Markdown 无法使用 EJS 语法问题</h3>
<p>暂时没管,曲线绕。</p>
<h3 id="分页生成问题">分页生成问题</h3>
<p>hexo-generator-index 的 pagination 生成的是 page/2 page/3 但是 jekyll 中的格式是 page2, page3 所以复制了该插件源码放到本地进行修改(位于 <code>scripts/pagination.js</code>):</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-meta">&#x27;use strict&#x27;</span>;<br><span class="hljs-keyword">const</span> pagination = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;hexo-pagination&#x27;</span>);<br>hexo.<span class="hljs-property">config</span>.<span class="hljs-property">index_generator</span> = <span class="hljs-title class_">Object</span>.<span class="hljs-title function_">assign</span>(<br> &#123;<br> <span class="hljs-attr">per_page</span>:<br> <span class="hljs-keyword">typeof</span> hexo.<span class="hljs-property">config</span>.<span class="hljs-property">per_page</span> === <span class="hljs-string">&#x27;undefined&#x27;</span> ? <span class="hljs-number">10</span> : hexo.<span class="hljs-property">config</span>.<span class="hljs-property">per_page</span>,<br> <span class="hljs-attr">order_by</span>: <span class="hljs-string">&#x27;-date&#x27;</span>,<br> &#125;,<br> hexo.<span class="hljs-property">config</span>.<span class="hljs-property">index_generator</span><br>);<br>hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">generator</span>.<span class="hljs-title function_">register</span>(<span class="hljs-string">&#x27;index&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">locals</span>) &#123;<br> <span class="hljs-keyword">const</span> config = <span class="hljs-variable language_">this</span>.<span class="hljs-property">config</span>;<br> <span class="hljs-keyword">const</span> posts = locals.<span class="hljs-property">posts</span>.<span class="hljs-title function_">sort</span>(config.<span class="hljs-property">index_generator</span>.<span class="hljs-property">order_by</span>);<br> posts.<span class="hljs-property">data</span>.<span class="hljs-title function_">sort</span>(<span class="hljs-function">(<span class="hljs-params">a, b</span>) =&gt;</span> (b.<span class="hljs-property">sticky</span> || <span class="hljs-number">0</span>) - (a.<span class="hljs-property">sticky</span> || <span class="hljs-number">0</span>));<br> <span class="hljs-keyword">const</span> paginationDir = config.<span class="hljs-property">pagination_dir</span> || <span class="hljs-string">&#x27;page&#x27;</span>;<br> <span class="hljs-keyword">const</span> path = config.<span class="hljs-property">index_generator</span>.<span class="hljs-property">path</span> || <span class="hljs-string">&#x27;&#x27;</span>;<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_">pagination</span>(path, posts, &#123;<br> <span class="hljs-attr">perPage</span>: config.<span class="hljs-property">index_generator</span>.<span class="hljs-property">per_page</span>,<br> <span class="hljs-attr">layout</span>: [<span class="hljs-string">&#x27;index&#x27;</span>, <span class="hljs-string">&#x27;archive&#x27;</span>],<br> <span class="hljs-attr">format</span>: paginationDir + <span class="hljs-string">&#x27;%d/&#x27;</span>,<br> <span class="hljs-attr">data</span>: &#123;<br> <span class="hljs-attr">__index</span>: <span class="hljs-literal">true</span>,<br> &#125;,<br> &#125;);<br>&#125;);<br></code></pre></td></tr></table></figure>
<hr />
<h3 id="删除线实现问题">删除线实现问题</h3>
<p>默认删除线是 s 标签,但是 jekyll 是 del,用 after_render:html 直接替换(位于 <code>scripts/tag-del.js</code>):</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs javascript">hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">filter</span>.<span class="hljs-title function_">register</span>(<span class="hljs-string">&#x27;after_render:html&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">str</span>) &#123;<br> <span class="hljs-keyword">return</span> str.<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/&lt;s&gt;/g</span>, <span class="hljs-string">&#x27;&lt;del&gt;&#x27;</span>).<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/&lt;\/s&gt;/g</span>, <span class="hljs-string">&#x27;&lt;/del&gt;&#x27;</span>);<br>&#125;);<br></code></pre></td></tr></table></figure>
<hr />
<h3 id="构建后多余文件问题">构建后多余文件问题</h3>
<p>有些文件是多余的,构建后删除掉:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2025/jekyll-2-hexo/147800e8-7a61-8077-911f-f5d55f484628.webp' alt='' title=''></p>
<p>具体有:</p>
<ul>
<li>
<p>categories/*</p>
</li>
<li>
<p>i_dont_wanna_use_default_archives/*</p>
</li>
<li>
<p>i_dont_wanna_use_default_tags/*</p>
</li>
<li>
<p>less/*</p>
</li>
</ul>
<h3 id="rss-问题">RSS 问题</h3>
<p>我使用了自定义的标签样式来渲染来自 Notion Bookmark 的,以期望跟 Notion 的 Bookmark 一样好看,但是如此一来,RSS 阅读器如 Reeder 就无法正确渲染出样式了,因此我处理了以下,在 Jekyll 中使用的是模板语法处理函数,也即在构建的时候动态替换掉自定义的样式:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-variable language_">module</span> <span class="hljs-title class_">Jekyll</span><br> <span class="hljs-variable language_">module</span> <span class="hljs-title class_">BookmarkFilter</span><br> def <span class="hljs-title function_">bookmark_filter</span>(input)<br> input.<span class="hljs-title function_">gsub</span>(<span class="hljs-regexp">/^\&lt;p\&gt;\&lt;a\s+class=\&quot;link-bookmark\&quot;\shref=(.*)\starget=\&quot;_blank\&quot;\&gt;\&lt;span\&gt;(.*)\&lt;\/span\&gt;\&lt;span\&gt;\&lt;span\&gt;(.*)\&lt;\/span\&gt;\&lt;span\&gt;\n(.*)\n\&lt;\/span\&gt;\&lt;span\&gt;(.*)\&lt;\/span\&gt;\&lt;\/span\&gt;\&lt;\/a\&gt;\&lt;\/p\&gt;$/</span>, <span class="hljs-string">&#x27;&lt;p&gt;&lt;a href=\1 target=&quot;_blank&quot;&gt;\3&lt;/a&gt;&lt;/p&gt;&#x27;</span>);<br> end<br> end<br> end<br> <br> <span class="hljs-title class_">Liquid</span>::<span class="hljs-title class_">Template</span>.<span class="hljs-title function_">register_filter</span>(<span class="hljs-title class_">Jekyll</span>::<span class="hljs-title class_">BookmarkFilter</span>)<br></code></pre></td></tr></table></figure>
<p>而在 Hexo 中是在构建之后打补丁的方式处理(位于 <code>scripts/rss-gene.js</code>):</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;fs&#x27;</span>);<br><span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;path&#x27;</span>);<br><span class="hljs-keyword">const</span> ejs = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;ejs&#x27;</span>);<br><span class="hljs-keyword">const</span> rootDate = <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>();<br><br><span class="hljs-keyword">function</span> <span class="hljs-title function_">getDate</span>(<span class="hljs-params">_date</span>) &#123;<br> <span class="hljs-keyword">const</span> date = _date ? <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(_date) : rootDate;<br> <span class="hljs-comment">// 获取各个部分</span><br> <span class="hljs-keyword">const</span> days = [<span class="hljs-string">&#x27;Sun&#x27;</span>, <span class="hljs-string">&#x27;Mon&#x27;</span>, <span class="hljs-string">&#x27;Tue&#x27;</span>, <span class="hljs-string">&#x27;Wed&#x27;</span>, <span class="hljs-string">&#x27;Thu&#x27;</span>, <span class="hljs-string">&#x27;Fri&#x27;</span>, <span class="hljs-string">&#x27;Sat&#x27;</span>];<br> <span class="hljs-keyword">const</span> months = [<br> <span class="hljs-string">&#x27;Jan&#x27;</span>,<br> <span class="hljs-string">&#x27;Feb&#x27;</span>,<br> <span class="hljs-string">&#x27;Mar&#x27;</span>,<br> <span class="hljs-string">&#x27;Apr&#x27;</span>,<br> <span class="hljs-string">&#x27;May&#x27;</span>,<br> <span class="hljs-string">&#x27;Jun&#x27;</span>,<br> <span class="hljs-string">&#x27;Jul&#x27;</span>,<br> <span class="hljs-string">&#x27;Aug&#x27;</span>,<br> <span class="hljs-string">&#x27;Sep&#x27;</span>,<br> <span class="hljs-string">&#x27;Oct&#x27;</span>,<br> <span class="hljs-string">&#x27;Nov&#x27;</span>,<br> <span class="hljs-string">&#x27;Dec&#x27;</span>,<br> ];<br> <span class="hljs-keyword">const</span> dayName = days[date.<span class="hljs-title function_">getDay</span>()];<br> <span class="hljs-keyword">const</span> day = <span class="hljs-title class_">String</span>(date.<span class="hljs-title function_">getDate</span>()).<span class="hljs-title function_">padStart</span>(<span class="hljs-number">2</span>, <span class="hljs-string">&#x27;0&#x27;</span>);<br> <span class="hljs-keyword">const</span> month = months[date.<span class="hljs-title function_">getMonth</span>()];<br> <span class="hljs-keyword">const</span> year = date.<span class="hljs-title function_">getFullYear</span>();<br> <span class="hljs-keyword">const</span> hours = <span class="hljs-title class_">String</span>(date.<span class="hljs-title function_">getHours</span>()).<span class="hljs-title function_">padStart</span>(<span class="hljs-number">2</span>, <span class="hljs-string">&#x27;0&#x27;</span>);<br> <span class="hljs-keyword">const</span> minutes = <span class="hljs-title class_">String</span>(date.<span class="hljs-title function_">getMinutes</span>()).<span class="hljs-title function_">padStart</span>(<span class="hljs-number">2</span>, <span class="hljs-string">&#x27;0&#x27;</span>);<br> <span class="hljs-keyword">const</span> seconds = <span class="hljs-title class_">String</span>(date.<span class="hljs-title function_">getSeconds</span>()).<span class="hljs-title function_">padStart</span>(<span class="hljs-number">2</span>, <span class="hljs-string">&#x27;0&#x27;</span>);<br><br> <span class="hljs-comment">// 获取时区偏移(以分钟为单位)</span><br> <span class="hljs-keyword">const</span> timezoneOffset = -date.<span class="hljs-title function_">getTimezoneOffset</span>();<br> <span class="hljs-keyword">const</span> sign = timezoneOffset &gt;= <span class="hljs-number">0</span> ? <span class="hljs-string">&#x27;+&#x27;</span> : <span class="hljs-string">&#x27;-&#x27;</span>;<br> <span class="hljs-keyword">const</span> offsetHours = <span class="hljs-title class_">String</span>(<br> <span class="hljs-title class_">Math</span>.<span class="hljs-title function_">floor</span>(<span class="hljs-title class_">Math</span>.<span class="hljs-title function_">abs</span>(timezoneOffset) / <span class="hljs-number">60</span>)<br> ).<span class="hljs-title function_">padStart</span>(<span class="hljs-number">2</span>, <span class="hljs-string">&#x27;0&#x27;</span>);<br> <span class="hljs-keyword">const</span> offsetMinutes = <span class="hljs-title class_">String</span>(<span class="hljs-title class_">Math</span>.<span class="hljs-title function_">abs</span>(timezoneOffset) % <span class="hljs-number">60</span>).<span class="hljs-title function_">padStart</span>(<span class="hljs-number">2</span>, <span class="hljs-string">&#x27;0&#x27;</span>);<br><br> <span class="hljs-comment">// 格式化为 RFC 2822 格式的字符串</span><br> <span class="hljs-keyword">return</span> <span class="hljs-string">`<span class="hljs-subst">$&#123;dayName&#125;</span>, <span class="hljs-subst">$&#123;day&#125;</span> <span class="hljs-subst">$&#123;month&#125;</span> <span class="hljs-subst">$&#123;year&#125;</span> <span class="hljs-subst">$&#123;hours&#125;</span>:<span class="hljs-subst">$&#123;minutes&#125;</span>:<span class="hljs-subst">$&#123;seconds&#125;</span> <span class="hljs-subst">$&#123;sign&#125;</span><span class="hljs-subst">$&#123;offsetHours&#125;</span><span class="hljs-subst">$&#123;offsetMinutes&#125;</span>`</span>;<br>&#125;<br><br>hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">generator</span>.<span class="hljs-title function_">register</span>(<span class="hljs-string">&#x27;xml&#x27;</span>, <span class="hljs-keyword">function</span> (<span class="hljs-params">locals</span>) &#123;<br> <span class="hljs-comment">// 仿照 Liquid 内置的日期格式写法</span><br> <span class="hljs-comment">// 注意如果前面不加这个 \uFEFF 则不会被识别为 xml</span><br> <span class="hljs-keyword">const</span> template =<br> <span class="hljs-string">&#x27;\uFEFF&#x27;</span> +<br> <span class="hljs-string">`&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;</span><br><span class="hljs-string"> &lt;rss version=&quot;2.0&quot; xmlns:atom=&quot;http://www.w3.org/2005/Atom&quot;&gt;</span><br><span class="hljs-string"> &lt;channel&gt;</span><br><span class="hljs-string"> &lt;title&gt;Xheldon Blog&lt;/title&gt;</span><br><span class="hljs-string"> &lt;description&gt;The Answer to Life, the Universe and Everything is...&lt;/description&gt;</span><br><span class="hljs-string"> &lt;link&gt;https://www.xheldon.com&lt;/link&gt;</span><br><span class="hljs-string"> &lt;atom:link href=&quot;https://www.xheldon.com/feed.xml&quot; rel=&quot;self&quot; type=&quot;application/rss+xml&quot; /&gt;</span><br><span class="hljs-string"> &lt;pubDate&gt;&lt;%= getDate() %&gt;&lt;/pubDate&gt;</span><br><span class="hljs-string"> &lt;lastBuildDate&gt;&lt;%= getDate() %&gt;&lt;/lastBuildDate&gt;</span><br><span class="hljs-string"> &lt;generator&gt;Hexo v&lt;%= version %&gt;&lt;/generator&gt;</span><br><span class="hljs-string"> &lt;% for (post of posts.sort((a, b) =&gt; (new Date(b.date).getTime()) - (new Date(a.date).getTime())).slice(0, 10)) &#123; %&gt;</span><br><span class="hljs-string"> &lt;item&gt;</span><br><span class="hljs-string"> &lt;title&gt;&lt;%= post.title %&gt;&lt;/title&gt;</span><br><span class="hljs-string"> &lt;description&gt;&lt;%= bookmark_filter(post.content) %&gt;&lt;/description&gt;</span><br><span class="hljs-string"> &lt;pubDate&gt;&lt;%= getDate(post.date) %&gt;&lt;/pubDate&gt;</span><br><span class="hljs-string"> &lt;link&gt;&lt;%= post.permalink %&gt;&lt;/link&gt;</span><br><span class="hljs-string"> &lt;guid isPermaLink=&quot;true&quot;&gt;&lt;%= post.permalink %&gt;&lt;/guid&gt;</span><br><span class="hljs-string"> &lt;% for (tag of post.tags.data) &#123; %&gt;</span><br><span class="hljs-string"> &lt;category&gt;&lt;%= tag.name %&gt;&lt;/category&gt;</span><br><span class="hljs-string"> &lt;% &#125; %&gt;</span><br><span class="hljs-string"> &lt;% for (cat of post.categories.data) &#123; %&gt;</span><br><span class="hljs-string"> &lt;category&gt;&lt;%- escape_html(cat.name) %&gt;&lt;/category&gt;</span><br><span class="hljs-string"> &lt;% &#125; %&gt;</span><br><span class="hljs-string"> &lt;/item&gt;</span><br><span class="hljs-string"> &lt;% &#125; %&gt;</span><br><span class="hljs-string"> &lt;/channel&gt;</span><br><span class="hljs-string"> &lt;/rss&gt;`</span>;<br><br> <span class="hljs-keyword">const</span> bookmark_filter = hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">helper</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;bookmark_filter&#x27;</span>).<span class="hljs-title function_">bind</span>(hexo);<br> <span class="hljs-keyword">const</span> escape_html = hexo.<span class="hljs-property">extend</span>.<span class="hljs-property">helper</span>.<span class="hljs-title function_">get</span>(<span class="hljs-string">&#x27;escape_html&#x27;</span>).<span class="hljs-title function_">bind</span>(hexo);<br><br> <span class="hljs-keyword">const</span> data = &#123;<br> <span class="hljs-attr">posts</span>: locals.<span class="hljs-property">posts</span>.<span class="hljs-title function_">toArray</span>(),<br> getDate,<br> <span class="hljs-attr">version</span>: hexo.<span class="hljs-property">version</span>,<br> escape_html,<br> bookmark_filter,<br> &#125;;<br><br> <span class="hljs-keyword">const</span> jsonContent = ejs.<span class="hljs-title function_">render</span>(template, data);<br><br> <span class="hljs-keyword">const</span> outputPath = path.<span class="hljs-title function_">join</span>(<span class="hljs-string">&#x27;source/_posts&#x27;</span>, <span class="hljs-string">&#x27;feed.xml&#x27;</span>);<br> fs.<span class="hljs-title function_">writeFileSync</span>(outputPath, jsonContent, &#123; <span class="hljs-attr">encoding</span>: <span class="hljs-string">&#x27;utf8&#x27;</span> &#125;);<br><br> <span class="hljs-keyword">return</span> &#123;<br> <span class="hljs-attr">path</span>: <span class="hljs-string">&#x27;feed.xml&#x27;</span>,<br> <span class="hljs-attr">data</span>: jsonContent,<br> &#125;;<br>&#125;);<br></code></pre></td></tr></table></figure>
<h3 id="service-worker-问题">Service-Worker 问题</h3>
<p>移除了 service worker,因为每次构建,页面的 tags 部分一定会变,导致 html 页面一定会更新,会导致经常需要手动刷新页面,不胜其烦,因此直接移除了。</p>
<h3 id="其他问题">其他问题</h3>
<ul>
<li>
<p>有些文件是需要的但是没放进入,构建后放入,如 ads.txt 等。</p>
</li>
<li>
<p><code>layout</code> 值是 <code>post</code> 类型的文章, <code>page.path</code> 的值不以 <code>/</code> 开头,这点要注意。</p>
</li>
</ul>
<h2 id="后话">后话</h2>
<p>基本就是这么多,使用 BeyondCompare 逐行对比后基本可以最小化变化的迁移过去。</p>
</description>
<pubDate>Tue, 07 Jan 2025 00:00:00 +0000</pubDate>
<link>https://www.xheldon.com/tech/jekyll-2-hexo.html</link>
<guid isPermaLink="true">https://www.xheldon.com/tech/jekyll-2-hexo.html</guid>
<category>使用体验</category>
<category>折腾</category>
<category>总结</category>
<category>教程</category>
<category>技巧</category>
<category>JavaScript</category>
<category>技术</category>
<category>框架</category>
<category>tech</category>
</item>
<item>
<title>赛博朋克 2024:我正在使用的工具们</title>
<description><h2 id="前言">前言</h2>
<p>2024 年底了,趁着微信的内容助推券即将过期之际,总结一下本年度我日常开发过程中会频繁使用到的各种效率工具,软硬件均有,仅供参考。</p>
<p caption='AI 生成图'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80d5-99b3-da02d3eacde4.webp' alt='AI 生成图' title='AI 生成图'></p>
<h2 id="付费订阅软件">付费订阅软件</h2>
<h3 id="crusor">Crusor</h3>
<p>AI 编程,首当其冲(首当其冲:比喻首先受到攻击或遭遇灾难。此处用典——作者注)的是 Cursor。</p>
<p>在与 Windsurf、Github Copilot 的对比中,新代码生成能力基本不相上下,但是 Cursor 修改/重构已有代码它比其他 AI 代码辅助工具高出一个次元。全程你基本只需要按 Tab 键,它会自动将光标(Cursor)定位到需要重构/修改的地方,你只需要按 Tab 接受,然后继续按 Tab 就会跳到下一个需要重构/修改的地方,而它的 Composer/Agent 也是与 AI 结对编程的开创者,实力毋庸置疑。</p>
<p caption='使用 Cursor 的日常编程界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8053-865f-cbdc451a31ad.webp' alt='使用 Cursor 的日常编程界面' title='使用 Cursor 的日常编程界面'></p>
<h3 id="notion">Notion</h3>
<blockquote style='border-color: ;color: '><p> 我用到的功能免费版即可满足,付费版更多的是协同之用,因此来年我将不再订阅续费。</p></blockquote>
<p>我使用笔记类软件最重要的两点是:「无限制的导入导出」和「全平台」。Notion 做到了,它:</p>
<ol>
<li>
<p>支持 API 功能,因此不用担心跑路数据无法导出(点名私有化格式的印象笔记等一众国内笔记产品);</p>
</li>
<li>
<p>基于 API,配合 Notion-Flow 浏览器插件可以随时随地写基于 Github 的静态博客;</p>
</li>
<li>
<p>它的 database 模块功能,只需要稍微写点服务端代码,即可将其当做网络数据库,如我在博客页面所使用的那样:</p>
</li>
</ol>
<p><a href="https://www.xheldon.cn/subscribe/" target="_blank"> 订阅&付费软件 - Xheldon Blog</a></p>
<p caption='Notion Flow 插件界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80ed-b18b-fd360fdc9098.webp' alt='Notion Flow 插件界面' title='Notion Flow 插件界面'></p>
<h3 id="滴答清单">滴答清单</h3>
<p>之前买断了 Things,但是在与日历的配合过程中很割裂,被朋友推荐了嘀嗒清单。</p>
<p>日历、任务、列表视图非常方便,任务详情支持富文本,这可能也是它为付费订阅而不是付费买断的原因,因为需要持续的服务器存储成本支出。</p>
<p caption='滴答清单'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-807e-aa46-e617dece267c.webp' alt='滴答清单' title='滴答清单'></p>
<h3 id="1password">1Password</h3>
<p>密码管理软件当之无愧的老大,在各种场景可以解放双手无需再输入任何密码,支持各种格式的存储如 API Token、信用卡信息、SSH 信息、恢复代码纯文本、WiFi 账号密码。</p>
<p>而它与系统的深度结合,更将「不输密码」发挥到了极致。当然,在 iOS 平台你需要使用系统自带的键盘才可以调起,这也是我不在手机上使用任何第三方输入法的最主要的原因。</p>
<p>而安全方面,我更相信商业软件。</p>
<p caption='1Password'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80d7-aea0-cecb9be5a8ab.webp' alt='1Password' title='1Password'></p>
<h3 id="youtube-premium">Youtube Premium</h3>
<p>把它算效率工具的原因,是因为我一般用它学习新东西,有不明白的用它搜一搜,总会有相关内容可以学到。最近的如 AI 相关,远点的如 Swift 相关等,甚至性爱技巧也有博主蹭流量来分享经验(当然是自媒体,真假自辨)。不过上面也有一些垃圾内容,网上冲浪注意甄别。</p>
<p>另外,作为 Premium 会员的「福利」,YouTube Music 也可以免费使用,你既可以将 Apple Music 播放列表导入(需要第三方服务),也可以自己上传音乐文件。其与 Apple Music 相比,好处有:</p>
<ol>
<li>
<p>YouTube Music 的资料库不会擅自用在线曲库的音乐替换你上传的音乐。</p>
</li>
<li>
<p>YouTube Music 的资料库可以允许你最多上传 10 万首音乐。</p>
</li>
<li>
<p>YouTube Music 即使你退订了 YouTube Premium,也依然可以使用你上传的音乐,算是一个音乐网盘。</p>
</li>
</ol>
<p caption='我的 YouTube Music'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/161800e8-7a61-8089-b694-c40de53f4c15.webp' alt='我的 YouTube Music' title='我的 YouTube Music'></p>
<p>因此,本着消费降级的原则,我已经在 2024 年底,Apple Music 到期之际,退订 Apple Music,订阅 YouTube Premium 了。</p>
<p caption='我的 YouTube 主页'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15f800e8-7a61-801c-bdb9-dade7616324d.webp' alt='我的 YouTube 主页' title='我的 YouTube 主页'></p>
<h2 id="付费买断软件">付费买断软件</h2>
<h3 id="popclip">Popclip</h3>
<p>右手0帧起手进行搜索&amp;翻译&amp;各种操作,无需左手&amp;键盘。</p>
<p>我的使用场景是,划选文本后调起 OpenAI Translator,然后在 OpenAI Translator 中设置好任务(下面说)。</p>
<p caption='PopClip 主页'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-809a-b407-fedb0270ab0b.webp' alt='PopClip 主页' title='PopClip 主页'></p>
<h3 id="alfred-5">Alfred 5</h3>
<p>从 4 带买断直接升上来的,主要是应用启动器和配合 OpenAITranslate 进行问 AI 操作,写了个自定义的脚本,调起方法同上。</p>
<p caption='Alfred 主界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8013-ba20-fde096aa3bf9.webp' alt='Alfred 主界面' title='Alfred 主界面'></p>
<p caption='Alfred 自定义脚本界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/161800e8-7a61-800e-a495-cd4e85400abd.webp' alt='Alfred 自定义脚本界面' title='Alfred 自定义脚本界面'></p>
<h3 id="bettermouse">Bettermouse</h3>
<p>我一直在用罗技的游戏鼠标,罗技有 G HUB 软件,但是该软件有 N 多问题:</p>
<ul>
<li>任务栏中,只有黑色 icon,这在一众白色 icon 中很扎眼。</li>
</ul>
<p caption='特立独行的 G Hub 软件'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80ea-8bc1-f301acb45cdd.webp' alt='特立独行的 G Hub 软件' title='特立独行的 G Hub 软件'></p>
<ul>
<li>
<p>交互逻辑奇葩。我想打开软件,需要点击任务栏,然后点启动 G HUB 才能启动,而一般软件的交互是点击直接打开主窗口,右键才显示交互菜单。最不济的,起码给一个可选的设置让用户来决定交互。</p>
</li>
<li>
<p>操作卡顿。启动需要半分钟,偶尔还无法启动,一直卡在动画播放界面。</p>
</li>
<li>
<p>文案描述古怪。请告诉我,如果我想把鼠标放入板载,这个选项此时我应该点击「开启」,还是保持现状即可?</p>
</li>
</ul>
<p caption='离谱的文案描述'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-807e-bc82-e1b1057cf7b7.webp' alt='离谱的文案描述' title='离谱的文案描述'></p>
<p>而 Bettermouse 除了可以自定义鼠标移动速度外,还可以设置平滑滚动、右键拖拽滑动等高级交互(如 Figma 中你想滚动页面需要按住 cmd 后鼠标左键按下移动,而 Bettermouse 可以让你按下右键即可拖拽页面等)。</p>
<p caption='BetterMouse 软件设置'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80d2-b103-c9c785b85f43.webp' alt='BetterMouse 软件设置' title='BetterMouse 软件设置'></p>
<h3 id="magnet">Magnet</h3>
<p>很多年前买的窗口管理工具,跟着我的 Apple ID 安装在了一台又一台 Mac 上。快捷键调整窗口屏占比,非~常~方~便~。市面上有挺多类似功能的软件,不过对我来说这款已经足够了,而且经过多个版本迭代后功能更丰富了,完全没有替换的动力。</p>
<p caption='Magnet 软件设置'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8051-81d6-d51835aaeb7b.webp' alt='Magnet 软件设置' title='Magnet 软件设置'></p>
<h3 id="cleanshotx">CleanshotX</h3>
<blockquote style='border-color: ;color: '><p> 大版本买断</p></blockquote>
<p>最强截图/录屏工具,中间还付费升级过一次大版本,不多介绍。最喜欢的功能是:截图后一键复制到粘贴板、截图后一键固定在屏幕上(最常用)。使用场景就是有些东西你看一眼记不住(抱歉年龄越大我大脑的 context 容量越低了),得反复来回切换,截屏固定后免去此烦恼~</p>
<p caption='置顶截图'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-808f-90e9-c31105514fac.webp' alt='置顶截图' title='置顶截图'></p>
<h3 id="surge-quantumultx">Surge/QuantumultX</h3>
<blockquote style='border-color: ;color: '><p> 大版本买断</p></blockquote>
<p>因为 Clash 内核不再更新放弃了 ClashX 小猫咪,跟朋友拼了个 Mac Surge 5人车。手机上使用 QuantumultX。</p>
<p caption='使用 Surge 当网关'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80f7-8620-c8391a537a25.webp' alt='使用 Surge 当网关' title='使用 Surge 当网关'></p>
<h3 id="xpic(夹带私货)">xPic(夹带私货)</h3>
<blockquote style='border-color: ;color: '><p> 此为私货推荐,我开发的,哈哈</p></blockquote>
<p>工作中需要压缩图片和转换图片,而一些在线工具不是无法达到预期,就是上传下载太麻烦,又或者有隐私问题。因此我开发了一个工具 xPic,支持图片和视频的压缩、格式转换,以及图片序列帧的合成、视频转 Gif 图。</p>
<p>同时因为工作中用到了 SVGA,还支持了 SVGA 的便捷预览。软件依然有 bug,还在内测中。</p>
<p caption='xPic SVGA 预览'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8072-a02d-f69887f17f73.webp' alt='xPic SVGA 预览' title='xPic SVGA 预览'></p>
<h2 id="免费-开源软件">免费/开源软件</h2>
<h3 id="openai-translator">OpenAI Translator</h3>
<p>本身是一个翻译工具没什么值得推荐的,不过它支持 PopClip 调起(原理是向软件注册的一个 Unix 套接字发送数据,命令为:<code>curl -d &quot;$1&quot; --unix-socket /tmp/openai-translator.sock http://openai-translator</code> 即可唤起),因此可以快速配合 PopClip 进行问 AI 操作,Alfred 同理。</p>
<p caption='OpenAI Translator 界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8071-9f06-d2abc7033bb2.webp' alt='OpenAI Translator 界面' title='OpenAI Translator 界面'></p>
<p>我新加了一个任务为:</p>
<p caption='OpenAI Translator 设置界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80fe-8c06-fdfbe61d1174.webp' alt='OpenAI Translator 设置界面' title='OpenAI Translator 设置界面'></p>
<h3 id="微信输入法">微信输入法</h3>
<p>我没有在 iPhone 上使用微信输入法,仅在公司/家里的 Mac 上使用,共享词库很不错。之前一直用搜狗输入法,但不知道是搜狗被腾讯收购后被有意降智逼用户转移还是其他什么原因,搜狗输入法经常会出现一些我完全没有输入过的词,而不是最普通最常用的词。比如我输入 <code>zhuomian</code>,给我第一个候选词是「卓面」而不是「桌面」。</p>
<p>另外一个尴尬的是,我承认用键盘战斗的时候输出过「屄」这个字,不知怎的,搜狗输入法仿佛发现了我的癖好,即使我手动特意多输入了几遍 bi 的同音字以期望能够通过「频率调词」来将「屄」这个候选词移动到后面,但总是失败。或者当时成功将其移动到第二页候选词后,过段时间又失败回到了第一个候选词(我发誓这段时间内我没有输入过这个字)。</p>
<p>另一个问题与之有关,无论任何时间(即使我刚调教过后)我输入 <code>vi</code> 后,也许是输入法以为我想输入 <code>bi</code> (毕竟键盘上 <code>v</code> 和 <code>b</code> 挨着,这么判断很正常),此时它会忽略我曾经有意调教过的 <code>bi</code> 的候选词顺序,优先给我展示「屄」这个字,这让我在投屏的时候大为尴尬。</p>
<p>之所以没有在 iPhone 上使用的原因,是因为手机上调起 1Password 的自动填充/验证码自动填充只有原生键盘可以做到。</p>
<p caption='微信输入法设置界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80e8-9e42-c558615951bf.webp' alt='微信输入法设置界面' title='微信输入法设置界面'></p>
<h3 id="orbstack">OrbStack</h3>
<p>原生的 Docker/Linux/k8s 管理工具,界面优雅,操作方便,内存占用低,免费的完全够用。比 Docker Desktop 不知道高到哪里去了,这里有一个官方的对比:</p>
<p><a href="https://docs.orbstack.dev/compare/docker-desktop" target="_blank"> OrbStack vs. Docker Desktop · OrbStack Docs</a></p>
<p caption='OrbStack 界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8025-bacc-d810a6a81df6.webp' alt='OrbStack 界面' title='OrbStack 界面'></p>
<h3 id="immersive-translate">Immersive translate</h3>
<p>AI 翻译插件,阅读网页的时候配合快捷键可以全文翻译,也可以只翻译鼠标悬浮下的内容,支持自己提供 API;支持翻译 Youtube 字幕,比谷歌自带的机翻不知道高到哪里去了。</p>
<p>(PS:谷歌搞大模型为什么不先把自家的全部文档用 AI 翻译一遍而依旧采用机翻痛苦难读?)</p>
<p caption='Immersive translate 官网'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8087-8c69-f9aa44bfb9be.webp' alt='Immersive translate 官网' title='Immersive translate 官网'></p>
<h3 id="warp">Warp</h3>
<p>杀手锏功能:像编辑文本一样在终端输入 bash 命令。</p>
<p>直接在 iTerm2、终端等,输入命令有诸多限制,如 <code>vim</code> 快捷键等无法使用等。如果不是这个功能,我绝不会使用 Wrap,因为它出了很多对我来说完全没必要的功能:AI、协同等。</p>
<p caption='Wrap 界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8003-bec7-c767006042fb.webp' alt='Wrap 界面' title='Wrap 界面'></p>
<h3 id="syntax-highlight">Syntax Highlight</h3>
<blockquote style='border-color: ;color: '><p> 开源</p></blockquote>
<p>按下空格,即可预览文件,丰富的格式支持,非~常~方~便~</p>
<p caption='Syntax Highlight 界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8096-af2a-e71c652a4594.webp' alt='Syntax Highlight 界面' title='Syntax Highlight 界面'></p>
<h3 id="monitorcontrol">MonitorControl</h3>
<blockquote style='border-color: ;color: '><p> 开源</p></blockquote>
<p>顾名思义,显示器亮度调节软件,支持快捷键,很方便。</p>
<p caption='MonitorControl 界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8089-9377-d223950afe0c.webp' alt='MonitorControl 界面' title='MonitorControl 界面'></p>
<h2 id="破解软件">破解软件</h2>
<h3 id="tableplus">TablePlus</h3>
<p>偶尔连接服务器数据库查看使用,使用频率极低(毕竟我不是专业运维/服务端),界面好看,操作简单。</p>
<p caption='TablePlus 界面'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8039-8cd7-c4d3be294f4a.webp' alt='TablePlus 界面' title='TablePlus 界面'></p>
<h2 id="硬件">硬件</h2>
<h3 id="g309-鼠标">G309 鼠标</h3>
<p>四个字总结:非常喜欢。</p>
<p>清脆的手感、蓝牙无线双模(蓝牙聊胜于无)、无线连接指哪儿打哪儿的准确性(回报率)、微微拱起的背部、垂直的两侧,加上随赠的防滑贴纸,一切都很完美。</p>
<p>注:之前我用 G403 和 G502 有线都是非对称鼠标(右键水平位置更低),使用 G309 对称鼠标后,左右键在同一水平位置,中指在右键感觉容易误触,需要有意抬起来一些,习惯了就好。</p>
<p caption='小手友好的 G309(虽然我是大手)'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8015-b435-d15e54c1bd29.webp' alt='小手友好的 G309(虽然我是大手)' title='小手友好的 G309(虽然我是大手)'></p>
<h3 id="apple-watch-表带">Apple Watch 表带</h3>
<p>淘宝某神秘店铺买的表带,号称原装,拆封不退。只有原价的 1/3 ~ 1/2 的价格,买来后确实跟原装没有任何区别。店铺还有一些是散装无包装的原装/99新/95新的货,看评论基本都可以确认是苹果工厂流出/瑕疵货或者官退货,我为了出二手所以买了带原包装的,但店铺有很多99新散装表带。</p>
<p caption='各种表带'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-806d-a1b5-eb2218516b61.webp' alt='各种表带' title='各种表带'></p>
<h3 id="airpods-4-降噪版">AirPods 4 降噪版</h3>
<p>出了 AirPods Pro 2,因为它带着确实不舒服,且带了快两年了,想换新的了。于是入了 4 代降噪版。降噪效果我个人使用对比与 Pro 2 代还是有一定差距,但没有差太多,唯一的意外是居然不能用耳机上下滑动耳机柄调节音量,其他的很满意。</p>
<p caption='AirPods 4 降噪版'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-8069-8274-f67ac1401a27.webp' alt='AirPods 4 降噪版' title='AirPods 4 降噪版'></p>
<h3 id="小米-ih-电饭煲-s1"><strong>小米 IH 电饭煲 S1</strong></h3>
<p>淘汰了用了8 年的美的电饭煲(虽然它也没坏),用着不错,唯一震惊的是它煮粥(稀饭)要一个半小时。</p>
<p caption='小米电饭锅'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-804b-9e0a-c48863912c40.webp' alt='小米电饭锅' title='小米电饭锅'></p>
<h3 id="美利达探索者x平把公路车">美利达探索者X平把公路车</h3>
<p>虽然不算电子产品,但也算作为程序员生活的一部分,也加上吧。通勤用,每天上下班总共20分钟,很方便~</p>
<p caption='探索者 X 在雁栖湖'><img src='https://static.xheldon.cn/img/in-post/2024/the-tools-i-used-in-2024/15a800e8-7a61-80ba-bf2e-e14e392280c4.webp' alt='探索者 X 在雁栖湖' title='探索者 X 在雁栖湖'></p>
<h2 id="后记">后记</h2>
<p>欢迎大家分享自己的工具~</p>
</description>
<pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate>
<link>https://www.xheldon.com/life/the-tools-i-used-in-2024.html</link>
<guid isPermaLink="true">https://www.xheldon.com/life/the-tools-i-used-in-2024.html</guid>
<category>生活</category>
<category>经验</category>
<category>工具</category>
<category>AI</category>
<category>life</category>
</item>
<item>
<title>如何让 TP-Link 关闭 DHCP 后拨号上网</title>
<description><blockquote style='border-color: ;color: '><p> 因为已经配置好了网络,所以一些页面我就用文字代替图片,毕竟为了写博客再重置一遍网络有点麻烦。</p></blockquote>
<h2 id="前言">前言</h2>
<p>我买的 TP-Link XDR-5480 有一个奇怪的设定:当你关闭了路由器的 DHCP 服务后,它的全部口(包括 SPF 转的口),都会变成 Lan 口,这就导致无法再光猫桥接+路由拨号上网了,而且你的 Lan 口 IP 无法指定,都会从前端路由(也就是光猫)获取。</p>
<p>此问题老款的 TP-Link 路由器反而没有,可以随意关闭不影响 Wan 口。也许新款 TP-Link 的产品经理觉得如果这样的话,就白白浪费了一个 Wan 口,加上新款的路由器全部都是 Wan/Lan 口混插,所以才把这个问题暴露出来了。</p>
<p>因为前几天 Mac mini 发布了 M4 款,体型更小,能耗更低,因此网络上又刮起了「Mac mini 当软路由+ Mac Surge 掌管家庭网络」的邪风。因为我当前的网络架构已经很稳定了,大概有一年多没折腾新东西,所以心痒难耐;又一想,我的 Mac Studio 也一直常年不关机,不就也可以跟 mini 一样当一个软路由吗?又加上网友们鼓吹的 Surge(以下均指 Mac Surge)接管 DHCP 服务可以全屋无感魔法上网以及实时「优雅」的查看各个设备的连接情况。本着「优雅永不过时」的态度,我又开始着手琢磨如何解决这个 TP-Link 的问题。</p>
<p>网上搜了一圈,如:</p>
<p><a href="https://cn.v2ex.com/t/1053641" target="_blank"> tp link 的路由器如何能在关闭 dhcp 服务的同时让 wan 口能够拨号上网? - V2EX</a></p>
<p>以及:</p>
<p><a href="https://www.chiphell.com/forum.php?mod=viewthread&tid=2521336&highlight=&mobile=no" target="_blank"> 请教各位大佬关于华硕和tplink路由器关闭dhcp设置的问题 - 电脑讨论(新) - Chiphell - 分享与交流用户体验</a></p>
<h2 id="现状">现状</h2>
<blockquote style='border-color: ;color: '><p> 注意:请提前在 TP-Link(以下均指我的 XDR-5480 型号)的设置界面备份你的路由设置,防止你操作失误,无法访问路由器的情况下重置,路由配置丢失的情况,如下图导出配置即可。注意,这里导出的配置不会涉及端口设置和网络,这个产品设计很好的解决了你设置错网络重置然后导入配置依然是错误配置无法访问网络的情况,给 TP-Link 的产品经理点赞。</p></blockquote>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-80e6-9e16-eb66a08e2ff7.webp' alt='' title=''></p>
<h3 id="网络拓扑图:">网络拓扑图:</h3>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-8003-89a9-e3f05024c8b5.webp' alt='' title=''></p>
<h3 id="目标">目标</h3>
<p>不改动当前任何设备的连接方式、网段的情况下(怕被骂),使用 Mac Studio 的 Surge 作为网关掌管 DHCP 服务。</p>
<h3 id="难点">难点</h3>
<ol>
<li>
<p>TP-Link 关闭了自身的 DHCP 服务后,会弹窗告诉你所有的口(包括 SPF 的口)都变成 Lan 口,无法再自定义端口地址。</p>
</li>
<li>
<p>TP-Link 关闭了自身的 DHCP 服务后,会弹窗提示你目前所有 Lan 口的 IP 地址,都是从前端路由(也就是光猫)获取,这也无可厚非,毕竟它此时是一个纯 AP,但这也导致了 Suerge 还是能感知到光猫上存在的 DHCP 服务的。</p>
</li>
<li>
<p>Mac Surge 作为网关的话,它会自己开启一个 DHCP 服务,同时要求网络上没有其他 DHCP 服务,否则会造成冲突掉线的情况。</p>
</li>
<li>
<p>根据第 2 点和第 3 点可得,你需要同时关闭光猫的 DHCP 和路由器的 DHCP 服务。但是!此操作如果你没有提前在光猫/路由器上设置好 IP 与 Mac 地址绑定的话,就无法再通过 WiFi 连接二者而只能重置路由器和光猫了!(别问我为什么知道的)所以不能关闭光猫的 DHCP 服务。</p>
</li>
</ol>
<h2 id="解决办法">解决办法</h2>
<p>基于上面的目标和难点,「朋友们,你们记一下,我作如下部署调整」:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-80ce-8790-d3cef3ef7d8f.webp' alt='' title=''></p>
<h3 id="步骤-1:关闭光猫的电源">步骤 1:关闭光猫的电源</h3>
<blockquote style='border-color: ;color: '><p> 其实只要断开光猫与 TP-Link 的连接就可以,我这里让关闭电源,只是防止大家可能意外连上光猫的 WiFi,如果你选择忘记光猫 WiFi,那么不关闭电源只断开与 TP-Link 的连接即可。</p></blockquote>
<p>我们先不设置光猫,防止它的 DHCP 服务被 Surge 检测到导致 Surge 无法开启 DHCP 服务。而且因为已经关闭了光猫与 TP-Link 的连接,当下一步关闭 TP-Link 的 DHCP 服务的时候,TP-Link 不会再自己检测前端路由器的 DHCP 服务而改变自己的 Lan 口 IP 了。</p>
<h3 id="步骤-2:关闭路由器的-dhcp-服务">步骤 2:关闭路由器的 DHCP 服务</h3>
<p>在设置里面关了就行了,这里我们选择「关闭」,后续等设置成功后,我们再改为「自动」。改为自动的是为了网络健壮性,我后面说。</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-804e-a69d-edb6a6eaabc1.webp' alt='' title=''></p>
<h3 id="步骤-3:开启-surge-dhcp">步骤 3:开启 Surge DHCP</h3>
<p>这一步在 Surge 的「概览」和「设备」tab 都可以找到入口:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-8091-83b9-e144224f35d5.webp' alt='' title=''></p>
<p>它会提示你 Surge 会将你的 Mac 的 IP 地址改为手动并固定,我这里就是 <code>192.168.5.100</code>,具体的教程网上一大把,你既然看到这里,肯定知道怎么设置,这里就不啰嗦了。</p>
<p>这里有个点需要注意的是,它会显示路由器的地址是 <code>192.168.5.1</code> 地址池是从 <code>192.168.5.100</code> 到 <code>192.168.5.200</code> 我这里把地址池改成了 <code>192.168.5.2</code> 到 <code>192.168.5.254</code> 范围更大一些。</p>
<h3 id="步骤-4:找到新的路由器地址">步骤 4:找到新的路由器地址</h3>
<p>设置完 Surge 后,你再访问路由器的 <code>192.168.5.1</code> 会发现已经无法访问了!不要紧张,因为此时你的 Mac Studio 是 DHCP 服务,它「居然」会给你的 TP-Link 路由器再分配一个 IP 地址(至于为什么会这样,我也不知道)。你点击「设备」后,找到名字是「Hostname Unsuitable for Printing」的(不确定一定是这个),在地址栏访问,就会发现熟悉的路由器界面又回来了!我这里的新的路由器 IP 地址是 <code>192.168.5.114</code> 。</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-809d-894e-fbe831126b06.webp' alt='' title=''></p>
<h3 id="步骤-5:修改路由器的-lan-口并拨号">步骤 5:修改路由器的 Lan 口并拨号</h3>
<p>此时你是无法上网的,因为你的光猫都没有通电!到这一步,你可以把光猫通电了,等到几分钟,然后在路由器的设置界面,你仍然会发现 <code>Wan 口已经断开连接</code> 再怎么点都没用。</p>
<p>但是不要放弃!你想一想,你现在的 Studio 的作用,跟你折腾软路由的情况是不是类似?当时是光猫负责拨号,软路由插到主路由/或者光猫上,负责 DHCP 服务。而现在,我的朋友,你将拨号设备后移到了路由器,你的 Studio 连接了主路由,因此你的 Studio 就变成了软路由,将 DHCP 服务放到了自身上。</p>
<p>因此关键是这一步:你需要再在设置里,手动指定 TP-Link Lan 口的 IP 地址为 <code>192.168.5.1</code> 即可(否则整个网络上都没有拥有 <code>192.168.5.1</code> 这个 IP 的设备了)!</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-80e0-8bb3-f208b1f9c874.webp' alt='' title=''></p>
<p>手动指定完 Lan 口 IP 后(虽然叫 Lan 口,但是它连接了光猫,是可以拨号的!),此时我们再回到上网设置,点击「连接」,也许需要点个 4、5 次,因为首次拨号比较久,但是最后会成功的。</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-8092-a5bc-cc3e8f728305.webp' alt='' title=''></p>
<h3 id="步骤-6:结尾">步骤 6:结尾</h3>
<p>至此,你的设置就完成了,现在你需要手动将已连接的设备右键,点击「将 Surge 作为其网关」,然后将设备重新连接网络即可:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-8044-b913-da4aafc3219f.webp' alt='' title=''></p>
<p>另外可以再设置一下「新设备都将 Surge 作为默认网关」就行了:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-80fc-9142-f5520e0a2756.webp' alt='' title=''></p>
<p>此时你的全部设备均把 Surge 当做网关,可以查看他们的实时流量。</p>
<p>还记得我在第 2 步说的,把 TP-Link 的 DHCP 服务关闭的设置吗?此时你可以再回到 TP-Link,把 DHCP 服务改为「自动」,这么做,是为了万一的万一,**Surge 的 DHCP 服务崩溃了,或者 Mac 坏掉了,你和家里的设备仍然可以访问访问网络,而你,我的朋友,你不会挨骂!**因为 TP-Link 的这个设置会「自动」探测局域网的 DHCP 服务,如果没有就开启自己的 DHCP,如果有就关闭:</p>
<p caption=''><img src='https://static.xheldon.cn/img/in-post/2024/how-to-pppoe-after-close-tp-link-dhcp/134800e8-7a61-805c-98c5-e3895ef7dfe0.webp' alt='' title=''></p>
<p>注:如果你点了「关」并保存,此时你的路由器的地址又变为了 <code>192.168.5.114</code> 。而且无法修改(因为你既连了光猫,又关了 DHCP,触发了上面难点中的 1 和 2 的问题),那又需要重新从头(关闭光猫电源)走一遍流程。</p>
<h2 id="结尾">结尾</h2>
<p>至此就结束了。在这个过程中我遇到了一些奇怪的问题,也做过一些失败的尝试,比如你猜我为什么会想到关闭光猫电源设置好路由器后再打开这个方法?</p>
<p>祝大家 WiFi 自由!</p>
</description>
<pubDate>Mon, 04 Nov 2024 00:00:00 +0000</pubDate>
<link>https://www.xheldon.com/tech/how-to-pppoe-after-close-tp-link-dhcp.html</link>
<guid isPermaLink="true">https://www.xheldon.com/tech/how-to-pppoe-after-close-tp-link-dhcp.html</guid>
<category>折腾</category>
<category>软路由</category>
<category>网络</category>
<category>路由器</category>
<category>技巧</category>
<category>技术</category>
<category>旁路由</category>
<category>千兆</category>
<category>tech</category>
</item>
<item>
<title>如何使用 Notion Flow 模块转换</title>
<description><p>一周前,我构建了 Notion Flow 浏览器扩展:</p>
<p><a href="https://twitter.com/_Xheldon/status/1770466495560294583" target="_blank"> Xheldon on Twitter / X</a></p>
<p>而刚刚更新的 0.4.1 版本:</p>
<p><a href="https://notion-flow.xheldon.com/blog/2024/03/31/0.4.1" target="_blank"> 0.4.1 | Notion Flow</a></p>
<p>支持了兼容 AWS S3 API 的自建 OSS 服务,如 Cloudflare R2:</p>
<p><a href="https://www.cloudflare.com/zh-cn/developer-platform/r2/" target="_blank"> Cloudflare R2 | 零出口费用分布式对象存储 | Cloudflare | Cloudflare</a></p>
<p>本篇文章简单介绍一下我是如何使用这个浏览器扩展用于我的 Github Jekyll 博客的。</p>
<hr />
<p>Jekyll 静态博客是基于 Ruby 构建的,支持插件。所以我自己写了几个插件(Jekyll 博客的插件位于 <code>_plugins</code> 目录下,写好 ruby 文件后,丢到该目录下,重启服务即可)来处理 Liquid 模板语言,而内容就是来自 Notion Flow 转换的 Notion 内容。如处理 bookmark 的插件内容如下:</p>
<figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><code class="hljs ruby"><span class="hljs-keyword">module</span> <span class="hljs-title class_">Jekyll</span><br> <span class="hljs-keyword">class</span> <span class="hljs-title class_">RenderBookMarkBlock</span> &lt; <span class="hljs-title class_ inherited__">Liquid::Block</span><br> <span class="hljs-keyword">def</span> <span class="hljs-title function_">initialize</span>(<span class="hljs-params">tag_name, attr, tokens</span>)<br> <span class="hljs-variable language_">super</span><br> <span class="hljs-comment"># 普通的链接没有 yid 和 bid</span><br> attrs = attr.scan(<span class="hljs-regexp">/url\=\&quot;(.*)\&quot;\stitle\=\&quot;(.*)\&quot;\simg\=\&quot;(.*)\&quot;\syid\=\&quot;(.*)\&quot;\sbid\=\&quot;(.*)\&quot;/</span>)<br> <span class="hljs-keyword">if</span> !attrs.empty?<br> <span class="hljs-comment"># 外部的 video 链接,youtube、bilibili(如本文上一篇博客就是)</span><br> <span class="hljs-variable">@url</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]<br> <span class="hljs-variable">@title</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>]<br> <span class="hljs-variable">@img</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">2</span>]<br> <span class="hljs-variable">@yid</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">3</span>]<br> <span class="hljs-variable">@bid</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">4</span>]<br> <span class="hljs-variable">@firstChar</span> = (<span class="hljs-variable">@title</span>)[<span class="hljs-number">0</span>].upcase<br> <span class="hljs-variable">@error</span> = <span class="hljs-string">&quot;&quot;</span><br> <span class="hljs-keyword">else</span><br> <span class="hljs-comment"># 正常和 notion 一样的 bookmark(如本文上面三个链接就是)</span><br> attrs = attr.scan(<span class="hljs-regexp">/url\=\&quot;(.*)\&quot;\stitle\=\&quot;(.*)\&quot;\simg\=\&quot;(.*)\&quot;/</span>)<br> <span class="hljs-variable">@url</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]<br> <span class="hljs-variable">@title</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>]<br> <span class="hljs-variable">@img</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">2</span>]<br> <span class="hljs-variable">@firstChar</span> = (<span class="hljs-variable">@title</span>)[<span class="hljs-number">0</span>].upcase<br> <span class="hljs-variable">@error</span> = <span class="hljs-string">&quot;&quot;</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">def</span> <span class="hljs-title function_">render</span>(<span class="hljs-params">context</span>)<br> <span class="hljs-variable">@desc</span> = <span class="hljs-variable language_">super</span><br> <span class="hljs-keyword">if</span> !<span class="hljs-variable">@yid</span>.<span class="hljs-literal">nil</span>? &amp;&amp; !<span class="hljs-variable">@yid</span>.empty?<br> <span class="hljs-string">&quot;&lt;p class=&#x27;embed-responsive embed-responsive-16by9&#x27;&gt;&lt;iframe src=&#x27;https://www.youtube.com/embed/<span class="hljs-subst">#&#123;<span class="hljs-variable">@yid</span>&#125;</span>?rel=0&#x27; title=&#x27;YouTube video player&#x27; frameborder=&#x27;0&#x27; allow=&#x27;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&#x27; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;&quot;</span><br> <span class="hljs-keyword">elsif</span> !<span class="hljs-variable">@bid</span>.<span class="hljs-literal">nil</span>? &amp;&amp; !<span class="hljs-variable">@bid</span>.empty?<br> <span class="hljs-string">&quot;&lt;p class=&#x27;embed-responsive embed-responsive-16by9&#x27; style=&#x27;border-bottom: 1px solid #ddd;&#x27;&gt;&lt;iframe src=&#x27;//player.bilibili.com/player.html?bvid=<span class="hljs-subst">#&#123;<span class="hljs-variable">@bid</span>&#125;</span>&amp;high_quality=1&amp;as_wide=1&#x27; scrolling=&#x27;no&#x27; border=&#x27;0&#x27; frameborder=&#x27;no&#x27; framespacing=&#x27;0&#x27; allowfullscreen&gt;&lt;/iframe&gt;&lt;/p&gt;&quot;</span><br> <span class="hljs-keyword">else</span><br> <span class="hljs-string">&quot;&lt;p&gt;&lt;a class=&#x27;link-bookmark&#x27; href=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@url</span>&#125;</span>&#x27; target=&#x27;_blank&#x27;&gt;&lt;span data-bookmark-img=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@img</span>&#125;</span>&#x27; data-bookmark-title=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@firstChar</span>&#125;</span>&#x27;&gt;&lt;img src=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@img</span>&#125;</span>&#x27;/&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;<span class="hljs-subst">#&#123;<span class="hljs-variable">@title</span>&#125;</span>&lt;/span&gt;&lt;span&gt;<span class="hljs-subst">#&#123;<span class="hljs-variable">@desc</span>&#125;</span>&lt;/span&gt;&lt;span&gt;<span class="hljs-subst">#&#123;<span class="hljs-variable">@url</span>&#125;</span>&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&quot;</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br><br><span class="hljs-comment"># 上传的 video</span><br><span class="hljs-keyword">module</span> <span class="hljs-title class_">Jekyll</span><br> <span class="hljs-keyword">class</span> <span class="hljs-title class_">RenderVideoBlock</span> &lt; <span class="hljs-title class_ inherited__">Liquid::Block</span><br> <span class="hljs-keyword">def</span> <span class="hljs-title function_">initialize</span>(<span class="hljs-params">tag_name, attr, tokens</span>)<br> <span class="hljs-variable language_">super</span><br> attrs = attr.scan(<span class="hljs-regexp">/caption\=\&quot;(.*)\&quot;\simg\=\&quot;(.*)\&quot;\ssuffix\=\&quot;(.*)\&quot;/</span>)<br> <span class="hljs-variable">@caption</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]<br> <span class="hljs-variable">@img</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">1</span>]<br> <span class="hljs-variable">@suffix</span> = attrs[<span class="hljs-number">0</span>][<span class="hljs-number">2</span>]<br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">def</span> <span class="hljs-title function_">render</span>(<span class="hljs-params">context</span>)<br> text = <span class="hljs-variable language_">super</span><br> <span class="hljs-string">&quot;&lt;p caption=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@caption</span>&#125;</span>&#x27;&gt;&lt;video controls muted&gt;&lt;source src=&#x27;<span class="hljs-subst">#&#123;<span class="hljs-variable">@img</span>&#125;</span>&#x27; type=&#x27;video/<span class="hljs-subst">#&#123;<span class="hljs-variable">@suffix</span>&#125;</span>&#x27; /&gt;&lt;/video&gt;&lt;/p&gt;&quot;</span><br> <span class="hljs-keyword">end</span><br> <span class="hljs-keyword">end</span><br><span class="hljs-keyword">end</span><br><br><span class="hljs-title class_">Liquid</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:Template</span>.register_tag(<span class="hljs-string">&#x27;render_bookmark&#x27;</span>, <span class="hljs-title class_">Jekyll</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:RenderBookMarkBlock</span>)<br><span class="hljs-title class_">Liquid</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:Template</span>.register_tag(<span class="hljs-string">&#x27;render_video&#x27;</span>, <span class="hljs-title class_">Jekyll</span><span class="hljs-symbol">:</span><span class="hljs-symbol">:RenderVideoBlock</span>)<br></code></pre></td></tr></table></figure>
<p>这段的逻辑是如果遇到 Notion 的 bookmark 模块链接是 Youtube、Bilibili,则转成嵌入视频的 HTML(iframe),否则转成类似于 Notion bookmark 的 HTML(需要配合 CSS 实现)。</p>
<p>所以我使用 Notion Flow 将 Notion 内容转换成 Markdown 格式的同时,自定义了 bookmark 等模块的转换规则,以让博客能够显示 Youtube、Bilibili 和与 Notion 一样的 bookmark 样式内容,如下:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-attr">video</span>: <span class="hljs-keyword">function</span> <span class="hljs-title function_">video</span>(<span class="hljs-params">block</span>) &#123;<br> <span class="hljs-keyword">if</span> (block.<span class="hljs-property">type</span> === <span class="hljs-string">&#x27;file&#x27;</span>) &#123;<br> <span class="hljs-comment">// 用户自己上传的 video 文件,用默认 video 插件处理</span><br> <span class="hljs-keyword">return</span> <span class="hljs-string">`&#123;% render_video caption=&quot;<span class="hljs-subst">$&#123;block.caption&#125;</span>&quot; img=&quot;<span class="hljs-subst">$&#123;block.url&#125;</span>&quot; suffix=&quot;<span class="hljs-subst">$&#123;block.suffix&#125;</span>&quot; %&#125;\n![<span class="hljs-subst">$&#123;block.caption&#125;</span>](<span class="hljs-subst">$&#123;block.url&#125;</span>)\n&#123;% endrender_video %&#125;\n`</span>;<br> &#125; <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (block.<span class="hljs-property">type</span> === <span class="hljs-string">&#x27;external&#x27;</span>) &#123;<br> <span class="hljs-comment">// 外部链接、youtube 和 bilibili 视频链接,用 bookmark 处理</span><br> <span class="hljs-keyword">return</span> <span class="hljs-string">`&#123;% render_bookmark url=&quot;<span class="hljs-subst">$&#123;block.url&#125;</span>&quot; title=&quot;<span class="hljs-subst">$&#123;</span></span><br><span class="hljs-subst"><span class="hljs-string"> block.caption || <span class="hljs-string">&#x27;&#x27;</span></span></span><br><span class="hljs-subst"><span class="hljs-string"> &#125;</span>&quot; img=&quot;&quot; yid=&quot;<span class="hljs-subst">$&#123;block.yid&#125;</span>&quot; bid=&quot;<span class="hljs-subst">$&#123;</span></span><br><span class="hljs-subst"><span class="hljs-string"> block.bid</span></span><br><span class="hljs-subst"><span class="hljs-string"> &#125;</span>&quot; %&#125;&#123;% endrender_bookmark %&#125;\n`</span>;<br> &#125;<br>&#125;<br></code></pre></td></tr></table></figure>
<p>这里需要注意(我不太懂 ruby), Liquid 模板的标签之间,必须有文本内容(你可以不用),否则,ruby 插件无法生成 HTML。即:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs javascript">&#123;% render_video %&#125;这里必须有任意内容!&#123;% endrender_video %&#125;<br></code></pre></td></tr></table></figure>
<p>这样在 ruby 插件中,<code>super</code> 变量拿到的就是「这里必须有任意内容!」这句话(你可以不使用该变量)。如果没有这段内容,则插件压根不会返回任何内容。</p>
</description>
<pubDate>Sun, 31 Mar 2024 00:00:00 +0000</pubDate>
<link>https://www.xheldon.com/tech/how-i-use-notion-flow.html</link>
<guid isPermaLink="true">https://www.xheldon.com/tech/how-i-use-notion-flow.html</guid>
<category>教程</category>
<category>技巧</category>
<category>工作流</category>
<category>技术</category>
<category>Jekyll</category>
<category>Notion</category>
<category>tech</category>
</item>
<item>
<title>【视频】使用 Notion Flow 简化你的博客发布流程</title>
<description><p>本文分享使用 Notion Flow 来简化你的博客发布流程的视频,具体官网见:</p>
<p><a href="https://notion-flow.xheldon.com" target="_blank"> Notion Flow | Notion Flow</a></p>
<p>Youtube:</p>
<p class='embed-responsive embed-responsive-16by9'><iframe src='https://www.youtube.com/embed/aPitTcsruhM?rel=0' title='YouTube video player' frameborder='0' allow='accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture' allowfullscreen></iframe></p>
<p>Bilibili:</p>
<p class='embed-responsive embed-responsive-16by9' style='border-bottom: 1px solid #ddd;'><iframe src='//player.bilibili.com/player.html?bvid=BV1Ar421h7tM&high_quality=1&as_wide=1' scrolling='no' border='0' frameborder='no' framespacing='0' allowfullscreen></iframe></p>
</description>
<pubDate>Thu, 21 Mar 2024 00:00:00 +0000</pubDate>
<link>https://www.xheldon.com/tech/use-notion-flow.html</link>
<guid isPermaLink="true">https://www.xheldon.com/tech/use-notion-flow.html</guid>
<category>折腾</category>
<category>教程</category>
<category>工作流</category>
<category>技术</category>
<category>插件</category>
<category>视频</category>
<category>Notion</category>
<category>tech</category>
</item>
<item>
<title>让 VSCode 更好用的设置——前端开发角度</title>
<description><p class='content-callout' style='background: rgb(241, 241, 239); color: ;'><span class='content-callout-icon'>☝🏻</span><span>后面计划出一期视频说明,因为有些设置的效果需要演示才能看出差异,而我又懒得制作动图在博客中了。</span></p>
<h2 id="前言">前言</h2>
<p>刚开始学习前端的时候,还没有 VSCode,那时我用的是 WebStorm(当时是学生,所以用的盗版)。开箱即用的体验让人爱不释手。后来由于办公电脑配置的下沉,以及它对 4K 及多显示器卡顿问题的长久不解决,再加上周围同事的影响, 最终一击是「配置同步」让我最终切换到 VSCode 。</p>
<p>在适应了没有单独的悬浮搜索框这一史诗级割裂之后,很快就摸索出了我个人认为好用的配置,下面就详细得说一说。如果有人觉得自己的设置比我的更好的,欢迎在下方留言然后附上原因和效果截图。</p>
<p class='content-callout' style='background: rgb(241, 241, 239); color: ;'><span class='content-callout-icon'>📖</span><span>默认的设置我基本不说了(除非非常好用),我就说我对于默认配置的修改部分。VSCode 中大部分配置都能修改,比如「是否在右侧小地图位置显示光标行」这种的都能,非常好。</span></p>
<h2 id="样式">样式</h2>
<h3 id="主题-字体">主题/字体</h3>
<p>主题是 One Dark Pro:</p>
<p><a href="https://marketplace.visualstudio.com/items?itemName=zhuangtongfa.Material-theme" target="_blank"> marketplace.visualstudio.com</a></p>
<p>字体是 Fira Code:</p>
<p><a href="https://github.com/tonsky/FiraCode?tab=readme-ov-file#download--install" target="_blank"> GitHub - tonsky/FiraCode: Free monospaced font with programming ligatures</a></p>
<p>Fira Code 是官方推荐字体,<a href="https://code.visualstudio.com/docs/getstarted/tips-and-tricks#:~:text=zoomLevel%22%3A%205-,Font%20ligatures,-%22editor.fontFamily%22">内部也在使用</a>。</p>
<p>Fira Code 对一些符号的变体支持非常好看,如 <code>===</code> 和 <code>&lt;=</code> 等(有些需要手动启用字符集和变体):</p>
<p caption='Fira Code 字体'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/3371f847-fdd8-410c-82e1-b1d63cd91035.webp' alt='Fira Code 字体' title='Fira Code 字体'></p>
<p>很多人不习惯 Fira Code 默认的 <code>&amp;</code> 符号,这可以通过配置来禁用它的变体,具体可以参看其 Github 的介绍,我的设置是:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br> <span class="hljs-attr">&quot;workbench.colorTheme&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;One Dark Pro&quot;</span><span class="hljs-punctuation">,</span><br> <span class="hljs-attr">&quot;editor.fontFamily&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&#x27;Fira Code&#x27;, Monaco, &#x27;Courier New&#x27;, monospace&quot;</span><span class="hljs-punctuation">,</span><br> <span class="hljs-attr">&quot;editor.fontLigatures&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&#x27;ss01&#x27;, &#x27;ss02&#x27; off, &#x27;ss03&#x27;, &#x27;ss04&#x27;, &#x27;ss05&#x27;, &#x27;ss07&#x27;, &#x27;cv29&#x27;, &#x27;cv28&#x27;, &#x27;cv13&#x27;&quot;</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure>
<p>另外,行高是 1.4,字号是 13。</p>
<h2 id="编辑器">编辑器</h2>
<p>最主要的就是编辑器设置了,好的编辑器当然是为了提高编码效率,下面逐个说说。</p>
<h3 id="渲染空白字符">渲染空白字符</h3>
<p caption='Editor: Render Whitespace'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/9175365e-0357-4b2f-abf7-cdf20062b2ca.webp' alt='Editor: Render Whitespace' title='Editor: Render Whitespace'></p>
<p>这个我是使用默认的 selection,即只在划选的时候,如果内容有空白符(空格)才会显示出来,否则不显示,不然影响美观。 <code>boundary</code> 的设置是总是显示,不好看:</p>
<p caption='选区渲染空白符号'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/12c4e471-e6fc-4b6d-aed7-61d4db16cd18.webp' alt='选区渲染空白符号' title='选区渲染空白符号'></p>
<h3 id="自动添加-删除配对括号">自动添加/删除配对括号</h3>
<p caption='Auto Closing 设置'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/49d09c46-5456-441a-9f72-fccc3a5d761e.webp' alt='Auto Closing 设置' title='Auto Closing 设置'></p>
<p>这个几个设置使用场景是,如果你输入一个起始括号,如 <code>(&#123;[</code> 会自动在后面给你生成一个 <code>)&#125;]</code> ,删除的设置也是同理。默认是插入的时候配对,删除的时候也同步配对删除。</p>
<h3 id="括号着色(池)">括号着色(池)</h3>
<p caption='Bracket Pair Colorization'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/899eeaba-8737-4f07-8e22-9480f915fcbc.webp' alt='Bracket Pair Colorization' title='Bracket Pair Colorization'></p>
<p>第一个打开后,你的各个括号就会有颜色(而不是白色)。第二个打开后,每种类型的括号,拥有自己独立的一套颜色配置(其实也会不同的括号颜色重复,但不再是按不同括号的显示顺序,而是同种括号的显示顺序来着色了——我的理解和测试)。</p>
<h3 id="矩形选区">矩形选区</h3>
<p caption='Column Selection'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/c64604e0-b420-4845-86e2-bac8d40aaa3d.webp' alt='Column Selection' title='Column Selection'></p>
<p>默认情况从上往下选择,如果经过某行的行首和行尾,是选中整行的:</p>
<p caption='默认选中效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/d97e73d0-91ae-4e0a-befb-c708ceae1bc0.webp' alt='默认选中效果' title='默认选中效果'></p>
<p>如果这个开关打开后,就变成了鼠标划选是一个矩形选区(根据鼠标位置,而不是代码位置进行选择),常用场景是同时编辑多行类似缩进的内容,如 JSON 的键等:</p>
<p caption='列选择'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/99dcc5e1-65bd-4048-bf8a-b06443bc7745.webp' alt='列选择' title='列选择'></p>
<p caption='列选择的一个应用场景'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/85a0ac88-34a6-4843-adfd-ea95f01c1806.gif' alt='列选择的一个应用场景' title='列选择的一个应用场景'></p>
<p>多说一句,在终端中选中的时候按下 Opt 键,也是这个效果。</p>
<h3 id="复制内容的时候带语法高亮">复制内容的时候带语法高亮</h3>
<p caption='Copy With Syntax Highlighting'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/2e507e99-524a-41c0-978c-42cb1bcbebca.webp' alt='Copy With Syntax Highlighting' title='Copy With Syntax Highlighting'></p>
<p>有些富文本编辑器,没有特殊处理,因此在直接复制 VSCode 中的代码到富文本编辑器的时候,会将颜色也带上,这通常不是预期。此设置可以让你复制出来的内容不带颜色。</p>
<h3 id="拖拽">拖拽</h3>
<p caption='Drag And Drop'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/319dae0d-0369-4469-abd9-ed2e0c36649c.webp' alt='Drag And Drop' title='Drag And Drop'></p>
<p>我写码这么多年,几乎没有使用「拖拽」来实现移动代码块的操作,因此建议取消。第二个按住 shift 后拖拽文件到 VSCode,如果是媒体文件则松手后只会显示文件名,如果不按住 shift 则会打开媒体文件,多一个功能挺好的,以备不时之需(这个默认是打开的)。</p>
<h3 id="空选区复制当前行">空选区复制当前行</h3>
<p caption='Empty Selection Clipboard'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/32f95652-6320-4347-8c9e-b7f03dbecd79.webp' alt='Empty Selection Clipboard' title='Empty Selection Clipboard'></p>
<p>如果选区只是光标,没有选中任何内容,此时进行复制操作会选中当前行。复制当前行更简单了(默认开启)。</p>
<h3 id="自动折叠">自动折叠</h3>
<p caption='Folding'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/96927f79-74ed-4949-b3e0-7cff488269ed.webp' alt='Folding' title='Folding'></p>
<p>代码折叠肯定是需要的。突出显示折叠范围也是需要的(会跟鼠标在那一行一行的效果,当前行高亮),不然不知道当前行是否折叠了。最后一个是自动折叠 import 部分,我觉得没必要。</p>
<p>折叠我个人喜欢始终显示,因为这个功能太常用了,我经常需要先 hover 到位置,看哪行是被折叠了,然后再点打开折叠,效率太低。我喜欢一眼看到哪些地方被折叠的,所以需要设置成 always:</p>
<p caption='Show Folding Controls'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/0a382e11-522d-490a-a086-703b291ef90e.webp' alt='Show Folding Controls' title='Show Folding Controls'></p>
<h3 id="括号-缩进参考线">括号/缩进参考线</h3>
<p caption='(缩进/括号)参考线'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b537a5b7-b989-4346-837b-919a4705599c.webp' alt='(缩进/括号)参考线' title='(缩进/括号)参考线'></p>
<p>如下图,不过我没测试出什么是「缩进参考线」,先打开吧。</p>
<p caption='图中高亮的括号'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/f8b0495e-2bd6-4dd5-9eb1-36a33821f1e8.webp' alt='图中高亮的括号' title='图中高亮的括号'></p>
<h3 id="hover-时浮窗出现的位置">hover 时浮窗出现的位置</h3>
<p caption='Hover 位置'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/da26e746-9e8b-4a3a-9b24-2a38abae431d.webp' alt='Hover 位置' title='Hover 位置'></p>
<p>一般情况我们看代码是从上往下看的,这个设置 hover 代码后浮窗出现在上方,挡住了内容,还得移动一下鼠标让浮窗消失再出现,建议取消。</p>
<p caption='始终显示提示在下方更合适'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/6df3c907-16b9-4cba-b7f3-daa4a5fd4532.webp' alt='始终显示提示在下方更合适' title='始终显示提示在下方更合适'></p>
<h3 id="悬浮出提示">悬浮出提示</h3>
<p caption='消失延迟其实不需要'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/6b18c5da-1193-4e89-b49f-b7775dcdb192.webp' alt='消失延迟其实不需要' title='消失延迟其实不需要'></p>
<p>鼠标移出一般就是不想让它显示,直接设置为 0。</p>
<h3 id="鼠标缩放字体">鼠标缩放字体</h3>
<p caption='完全没用的功能…'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/2e05860a-01cb-45ff-9d3b-230d8871ebd6.webp' alt='完全没用的功能…' title='完全没用的功能…'></p>
<p>经常误触,关了。</p>
<h3 id="编辑器区域顶部-padding">编辑器区域顶部 padding</h3>
<p caption='统一视觉间隔'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/07183c4a-3dcc-412d-a7bb-f5167443874d.webp' alt='统一视觉间隔' title='统一视觉间隔'></p>
<p>我设置为 2。底部 padding 就没必要了。</p>
<p caption='优雅,永不过时'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/9f1cd71d-f579-44c6-8b23-58f558dfeeaf.webp' alt='优雅,永不过时' title='优雅,永不过时'></p>
<h3 id="滚动条">滚动条</h3>
<p>水平滚动条为 6 宽度,竖直为 25(默认水平 12,竖直 14):</p>
<p caption='Scrollbar'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/2b7e8382-836f-4fe4-a7b5-c88e24332838.webp' alt='Scrollbar' title='Scrollbar'></p>
<p>我个人是不喜欢滚动到范围外,会导致明明一屏显示完全的内容,出现滚动条,所以最后一个 Scroll Beyond Last Line 关了。</p>
<p caption='滚动条显示信息'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/361fa784-0d98-4055-a56e-58678f0c3a31.webp' alt='滚动条显示信息' title='滚动条显示信息'></p>
<p>这里要说下为什么竖直滚动条调大为 20,因为在那个区域其实不只是滚动条,还含有三个信息:</p>
<ol>
<li>
<p>滚动条右侧亮黄色的是编辑器警告信息。</p>
</li>
<li>
<p>滚动条中间暗黄色块是匹配的搜索项(含全局搜索和当前编辑器搜索)。其中,暗黄色块也可能是灰色(表示光标选中的部分和类似内容),也可能是淡粉色,表示光标选中的的内容的声明处。</p>
</li>
<li>
<p>占滚动条整行的蓝色线是光标所在的行。</p>
</li>
<li>
<p>滚动条左侧的绿色部分是代码变动的部分。其中,也可能是淡黄色,表示修改部分(如果启用了 git 的话)。</p>
</li>
</ol>
<p>可以看到这部分的信息显示很丰富,所以调宽一点。</p>
<h3 id="平滑滚动">平滑滚动</h3>
<p caption='动画,优雅'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/190452c5-f985-47b4-b238-000edce28c4b.webp' alt='动画,优雅' title='动画,优雅'></p>
<p>强烈建议开启,这样在滚动的时候就可以知道你大概滚动了多少行,而不是突然跳过去,「不知道滚动到哪里去了」。</p>
<h3 id="滚动吸顶">滚动吸顶</h3>
<p caption='吸顶,好用'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/37741a88-7ba7-481a-9fe5-06ed276af9d3.webp' alt='吸顶,好用' title='吸顶,好用'></p>
<p>滚动的时候可能需要查看超出当前屏幕的作用域,打开该选项即可。另外,水平滚动的时候会把该 sticky 的函数滚走,我倾向于不滚动它,所以把最后一个选项取消。</p>
<p caption='左右滚动不跟随'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b7b7e17c-ac89-4d69-b1c2-648f0e582a40.webp' alt='左右滚动不跟随' title='左右滚动不跟随'></p>
<h3 id="光标">光标</h3>
<p caption='Cursor Blinking'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/5736c4cc-e07c-40f1-ae31-e1c16e458a00.webp' alt='Cursor Blinking' title='Cursor Blinking'></p>
<p>第一个是光标闪烁的淡入淡出,第二个是你在点击不同位置的时候,光标是从上一个位置动画移动到点击位置的,可以让你知道本次点击光标位置相对上一个编辑位置是哪里,信息更丰富了。</p>
<h3 id="查找">查找</h3>
<p caption='编辑器右上角查找小部件'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/eb174538-47bb-44b8-ba84-54df4a37555e.webp' alt='编辑器右上角查找小部件' title='编辑器右上角查找小部件'></p>
<p>这个建议关掉,搜索的时候,如果不关,会在文件顶部凭空产生一些距离导关闭搜索框的时候编辑器跳动一下,难受。</p>
<p caption='空白,不优雅'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/83dcf800-6ec9-4a8a-8b49-2e69e419bd72.webp' alt='空白,不优雅' title='空白,不优雅'></p>
<p>不过该选项打开后可能会遮挡住编辑器内容,自己取舍(一般顶部都是 import 后的换行内容,挡住也无所谓)。</p>
<p caption='没空白,优雅'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/6878a013-d547-4ccb-9792-32e03a97e9d4.webp' alt='没空白,优雅' title='没空白,优雅'></p>
<h3 id="自动带入搜索小组件">自动带入搜索小组件</h3>
<p caption='自动带入,优雅'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/f671a305-d7c8-4ae0-b91c-5aa094bbd3a6.webp' alt='自动带入,优雅' title='自动带入,优雅'></p>
<p>这个建议关掉。我经常会使用搜索,然后搜索后选中某个内容后再搜索(非选中内容),此时编辑器自作聪明的把我选中的内容给带到搜索框中,导致我之前搜索的内容没了,很烦。</p>
<h3 id="缩略图">缩略图</h3>
<p caption='右侧小地图'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b8986040-1b86-4bbd-a99b-1b7d551c8b28.webp' alt='右侧小地图' title='右侧小地图'></p>
<p>编辑器右侧的缩略图我始终显示出来,它的作用一般是让我知道我当前处于编辑的哪个位置,以及相对于某个函数、组件,我所处的位置,因此我需要缩略图不滚动,同时仅渲染色块即可,不用将每行都渲染出来。</p>
<h3 id="建议">建议</h3>
<p caption='建议预览'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/1cbc255e-2784-461d-a723-ce5f325130a2.webp' alt='建议预览' title='建议预览'></p>
<p>这个开关建议关闭(默认),因为可能跟 copilot 建议弄混淆,如图是 copilot 的建议:</p>
<p caption='copilot 建议'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/89587ee7-0329-43e7-9a68-814e55bd8e07.webp' alt='copilot 建议' title='copilot 建议'></p>
<p>而这个是预览的建议:</p>
<p caption='整个一没必要咱就是说'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/26edace2-f059-4f51-a873-47a743946229.webp' alt='整个一没必要咱就是说' title='整个一没必要咱就是说'></p>
<p caption='最近建议'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/203628d4-4e0c-4f9a-8289-31ab679750dc.webp' alt='最近建议' title='最近建议'></p>
<p>这个选项默认是 first,即始终使用默认选择第一个建议,但是我经常遇到的问题是,在 CSS 中,我输入 <code>wid</code> 当然预期是 <code>width</code>,但是它会给我建议是 <code>widow</code> 我当然不用这个属性,但每次都是排在第一个,我就每次需要通过箭头来切换,所以此处建议修改成「最近使用」,类似与输入法的「动态调频」:</p>
<p caption='css 最近建议'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/a1edc147-031b-4347-be78-8c6ec3c71bd7.webp' alt='css 最近建议' title='css 最近建议'></p>
<h2 id="工作台">工作台</h2>
<h3 id="命令提示框">命令提示框</h3>
<p caption='命令建议'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/a62c1989-314d-4675-abbb-cb9f09534480.webp' alt='命令建议' title='命令建议'></p>
<p>有时候会经常反复输入一个命令,所以打开这个历史命令列表很有用。除此之外,保留输入内容也很有用,比如以 toggle 开头的命令(如 Toggle Screen Capture Mode)。</p>
<p>注意,如果输入内容后按了 esc 导致输入框消失,下次再次唤起不会保留输入内容,只有选择了一个命令执行后,再次唤起,才会保留上次输入的内容。</p>
<h3 id="目录树">目录树</h3>
<p caption='目录树滚动'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/ff187f9a-5588-40fb-927d-89e701495d94.webp' alt='目录树滚动' title='目录树滚动'></p>
<p>一般动画我都会打开因为「优雅永不过时」。这个设置也影响「设置」界面的滚动(之前对编辑器设置平滑滚动不会影响「设置」界面和目录树界面的滚动效果 )。</p>
<h3 id="快速打开记录历史">快速打开记录历史</h3>
<p caption='快速打开带入上次记录'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b4633d53-e671-437c-a4e9-241fe722c953.webp' alt='快速打开带入上次记录' title='快速打开带入上次记录'></p>
<p>按下 cmd + p 会出现 quick open 输入框,记住历史挺好的。另外还有个选项是失焦是否自动消失,大部分场景下需要自动消失,偶尔不需要,先保持默认自动消失了。</p>
<h3 id="工作台减少动画效果">工作台减少动画效果</h3>
<p caption='绝不减少动画'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/24d1bc51-76bf-415b-9c79-561bc6eb7caf.webp' alt='绝不减少动画' title='绝不减少动画'></p>
<p>我的 64G 内存 M1 Max,不需要减少动画(默认是 auto,根据系统配置自动适应,适用于多台电脑间配置同步的问题)。</p>
<h3 id="字体平滑">字体平滑</h3>
<p caption='字体平滑'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/093ce91b-5349-48a0-b563-2a44726f3bf2.webp' alt='字体平滑' title='字体平滑'></p>
<p>类似于 css 中的 <code>-webkit-font-smoothing: antialiased;</code> ,default 用于在大多数非 retina 屏上显示最清晰的字体(次像素级),antialiased 是像素级平滑,可能会导致字体更细,见图:</p>
<p caption='default 设置'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/81378425-4fd6-4954-8d27-5317c822b237.webp' alt='default 设置' title='default 设置'></p>
<p caption='antialiased 设置'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/e24ea63a-7536-48bd-8dec-302b335224a9.webp' alt='antialiased 设置' title='antialiased 设置'></p>
<p>这个设置虽然是在「工作台」块,但是也影响编辑器区域。可以看到开启了 antialiased 的时候,无论是编辑器区域还是工作台区域,字体都更暗(对比度更弱)、更细了。我喜欢后者,所以开启了。</p>
<p>注意,这个「次像素级」,并不是说比像素还小的级别,而是指「还没到像素」的级别,意思是更低级,而不是更高级。</p>
<h3 id="目录树-sticky">目录树 sticky</h3>
<p caption='目录树滚动吸顶'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/ed8074f7-0e31-4a1f-80f8-1618d2264e73.webp' alt='目录树滚动吸顶' title='目录树滚动吸顶'></p>
<p>非常好用,滚动的时候可以知道当前的滚动路径,唯一美中不足的是如果能加个 box-shadow 阴影就好了,不然不太好区分的:</p>
<p caption='目录树吸顶效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/5a7c2102-2730-47a0-b225-738a42d86342.webp' alt='目录树吸顶效果' title='目录树吸顶效果'></p>
<p>sticky 的最大级数也可以修改,默认是 7,足够了(编辑器 sticky 默认是 5 级)。</p>
<p>注意,此设置也同样适用于「设置」界面(原来设置界面属于工作台,而不是编辑器):</p>
<p caption='设置项界面也归它管'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/031c3170-cd43-4775-8401-a95e6c474a75.webp' alt='设置项界面也归它管' title='设置项界面也归它管'></p>
<p>目录树的缩进我改成 14 了,参考线我喜欢始终显示,不然同级文件太多,不好找:</p>
<p caption='目录树缩进'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/7a67020e-c5a4-4114-9ac0-ff3afcb5cb61.webp' alt='目录树缩进' title='目录树缩进'></p>
<h3 id="目录导航">目录导航</h3>
<p caption='目录导航显示 icon'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/eae4d00a-68d6-4c64-bab8-34df8fc7f458.webp' alt='目录导航显示 icon' title='目录导航显示 icon'></p>
<p>目录导航还是需要的,但是不需要文件/文件夹 icon,这样可以显著的和文件内的数组、类进行区分,非常好用:</p>
<p caption='面包屑显示效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/57e4fefd-4c76-42cd-b79c-d3d49eb2d1df.webp' alt='面包屑显示效果' title='面包屑显示效果'></p>
<h3 id="修改过的-tab">修改过的 tab</h3>
<p>与此相关的有多个,如在修改后未保存的文件上方显示高亮线:</p>
<p caption='高亮修改的 tab'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/e4a60c98-ec6f-452d-84ea-e0835ed963bf.webp' alt='高亮修改的 tab' title='高亮修改的 tab'></p>
<p>默认显示的是点,此选项打开后,会点和线同时显示,重启编辑器会只显示上方蓝色线(可能是 bug,其实应该不用重启编辑器也能生效)。</p>
<p>效果:</p>
<p caption='修改过的 tab 效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/aea54cb4-158e-4662-a6b5-285fc56fd836.webp' alt='修改过的 tab 效果' title='修改过的 tab 效果'></p>
<p>因为「点」也占用一部分的 tab 空间,会导致无法显示更多 tab 内容信息,所以建议打开该选项。</p>
<h3 id="鼠标导航">鼠标导航</h3>
<p caption='鼠标前进后退'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/254a8290-8b0a-4ebc-ac36-d453a855719d.webp' alt='鼠标前进后退' title='鼠标前进后退'></p>
<p>这是个默认选项,但是我也说一下,对于有左侧按键(右手),也即 4、5 按键的鼠标而言,的鼠标直接就可以用来导航,非常好用。</p>
<h3 id="tab-固定">tab 固定</h3>
<p caption='允许 tab 固定,好用'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b8c68b52-f2ec-4fb4-ac39-1fd1af2c9ebd.webp' alt='允许 tab 固定,好用' title='允许 tab 固定,好用'></p>
<p>固定后的 tab 默认出现在编辑器组的左侧,但是如果将其单独排成一行会更直观,与非固定的 tab 区分开,效果如下:</p>
<p caption='tab 固定效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b6868fe5-65b6-4c6b-bd81-3bd15fada494.webp' alt='tab 固定效果' title='tab 固定效果'></p>
<p>注意,默认情况下,固定的 tab 是无法通过鼠标中键或者 cmd + w 关闭的(按下会打开非固定 tab 而不是关闭固定 tab),此行为可以修改:</p>
<p caption='cmd + w 效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/0259919c-dd23-44f5-89e6-026c2140a424.webp' alt='cmd + w 效果' title='cmd + w 效果'></p>
<h3 id="tab-关闭按钮">tab 关闭按钮</h3>
<p caption='隐藏关闭按钮'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/c1e66c49-5cab-4d42-9c04-713a6530bb50.webp' alt='隐藏关闭按钮' title='隐藏关闭按钮'></p>
<p>一直使用左手 cmd + w 关闭 tab,所以此选项可以取消。另外,我其实更习惯双击 tab 关闭,但是官方回复不会做,见:</p>
<p><a href="https://github.com/Microsoft/vscode/issues/52628#issuecomment-420887497" target="_blank"> Allow to double click on a tab to close it · Issue #52628 · microsoft/vscode</a></p>
<h3 id="tab-wrap">tab wrap</h3>
<p caption='tab wrap'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b72dfdff-c447-457d-89de-8ca3a682281d.webp' alt='tab wrap' title='tab wrap'></p>
<p>如果打开 tab 较多,滚动 tab 的时候就会比较费劲,无法掌控全局,所以我喜欢 wrap tab,效果如下:</p>
<p caption='wrap 效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/3738c007-8e6d-4397-a3fb-0a5210402ed2.webp' alt='wrap 效果' title='wrap 效果'></p>
<p>比较尴尬的一点是,wrap 效果产生的多行 tab,可能跟上面提到的「修改 tab 上方蓝色高亮」搞的比较混乱(蓝色的线不知道是上面 tab 的还是下面 tab 的,得反应一下不直观)。是在 tab 显示更多内容,还是更直观,自己取舍:</p>
<p caption='高亮修改 + tab 高亮效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/bba12f32-b355-4550-aba5-c59950ed66fc.webp' alt='高亮修改 + tab 高亮效果' title='高亮修改 + tab 高亮效果'></p>
<h3 id="tab-高度">tab 高度</h3>
<p caption='紧凑 tab 布局'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/3224c016-4adb-41d2-82f8-83391960259d.webp' alt='紧凑 tab 布局' title='紧凑 tab 布局'></p>
<p>紧凑布局有利于掌控全局+不占地方。</p>
<h3 id="双击-tab-关闭(?)"><s>双击 tab 关闭(?)</s></h3>
<p caption='没懂这个设置'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/3385a990-b192-4aec-963a-ee5996faf19d.webp' alt='没懂这个设置' title='没懂这个设置'></p>
<p>看字面意思这个选项是官方号称不会做的「双击 tab 关闭」(如上面所言),但即使我关闭了可能会冲突的「双击 tab 自动扩展编辑器组」,该设置依然不生效,不知是不是我理解有误还是 bug。</p>
<h3 id="原生-tab">原生 tab</h3>
<p>与此相关的有两个:</p>
<p caption='原生 tab'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/09ad718f-b4c3-40a3-8c92-8e399613540a.webp' alt='原生 tab' title='原生 tab'></p>
<p>第一个设置,启用后,可以将多个项目窗口,合并到一个窗口。「窗口」选项中会出现「合并所有窗口」的选项,这样可以在一个窗口中来回切换多个项目,非常好用:</p>
<p caption='合并所有窗口'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/8d13f532-cc6a-49a6-9d2c-88aabf5904e3.webp' alt='合并所有窗口' title='合并所有窗口'></p>
<p>但是,这样的话就无法使用自定义的标题(其实我觉得也么啥用),自定义标题是这样的:</p>
<p caption='自定义标题栏效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/1d16fa15-9ee4-4647-8458-1103a659a165.webp' alt='自定义标题栏效果' title='自定义标题栏效果'></p>
<p>第一个设置如果打开了,那第二个就无效了,无论设置为 native 和 custom。如果第一个设置不打开,第二个设置设置为 native,那就没有「合并所有窗口」,也没有「自定义标题栏」(不知道这个设置意义何在)。</p>
<h3 id="目录树拖放">目录树拖放</h3>
<p caption='最好禁用拖拽文件'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/f4f866b8-c0be-456f-8336-2a229c532f61.webp' alt='最好禁用拖拽文件' title='最好禁用拖拽文件'></p>
<p>我经常误触,然后导致上百个修改…所以关了。</p>
<h3 id="搜索结果自动折叠">搜索结果自动折叠</h3>
<p caption='少于 10 个的文件夹展开'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/5aab9aef-806d-42a0-ab7c-7d58ae4b4849.webp' alt='少于 10 个的文件夹展开' title='少于 10 个的文件夹展开'></p>
<p>默认总是展开,但是如果搜索结果过多(通常是因为你还没有输入完成),此时展开是没有必要的,而且会耽误你掌控全局。</p>
<p>另外,如果你没有在搜索栏中加入「排除的文件」,那么也可能出现海量搜索结果,如 NextJS 项目的 .next 目录等,因此此设置也是必要的。</p>
<p>需要注意的是,这个「展开」、「折叠」的 10 个文件限制,指的是搜索结果中,出现在某个文件夹下的文件数量,而不是整个搜索结果的文件夹数量:</p>
<p caption='多余 10 个的文件夹折叠'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/67c50309-319c-474a-addf-bbd33925d827.webp' alt='多余 10 个的文件夹折叠' title='多余 10 个的文件夹折叠'></p>
<p>因此,如果某个文件夹下,出现符合搜索结果的文件过多(文件夹被折叠),通常你就需要检查是否需要提供更多搜索信息了。</p>
<h3 id="搜索框自动填入选择内容">搜索框自动填入选择内容</h3>
<p caption='全局搜索自动带入选择内容'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/42995472-6453-4e61-881e-d7e0ae5da443.webp' alt='全局搜索自动带入选择内容' title='全局搜索自动带入选择内容'></p>
<p>通常你选中一个内容后想搜索它,因此「Seed On Focus」此选项让你可以节省一个 cmd + v 的操作。</p>
<p>注意,这有区别于「搜索小组件」中的 选中后聚焦到搜索时自动带入。因为在编辑器中你去选中内容,然后聚焦到搜索小组件,不一定是为了搜索,还可能只是为了简单在当前编辑器高亮选中的相同内容以便于查看,但是此时选中后聚焦到搜索小组件,就自动替换成选中内容了,很多时候不符合预期。</p>
<p>而如果你在选中内容后,聚焦到搜索视图(右侧),那大概率是为了搜索内容。</p>
<p>另外搜索结果我会想知道它所处的行号,以确定它在其文档中的位置,所以显示行号也是很有必要的。</p>
<p>最后的 Smart Case 算是一个小技巧,如果都用小写,就表示自己记不太清搜索名字了,如果很确定搜索内容(如驼峰的函数名)的某个字母是大写,那么就区分大小写进行搜索,非常好用。</p>
<p>除此之外,如果能记住上次输入的内容,其实记住也是选中状态,如果不符合自己的输入预期,直接输入内容即可,对自己即将想要搜索的内容没有影响,而如果之前搜索的内容还有用,那岂不是更好?↓</p>
<p caption='注意与搜索小组件的差别'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/b84b5d58-94cb-448f-bd1a-cc129aac13bd.webp' alt='注意与搜索小组件的差别' title='注意与搜索小组件的差别'></p>
<h3 id="搜索忽略全局的-ignore">搜索忽略全局的 ignore</h3>
<p caption='全局忽略设置'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/21a936ed-29c9-4613-b6d4-5ad910fc04f0.webp' alt='全局忽略设置' title='全局忽略设置'></p>
<p>git 有个全局默认的 ignore,打开该选项可以在搜索的时候将其中列出的文件、文件夹忽略掉,通常是有必要的。</p>
<p>另外还有个在父级目录中启用 ignore,没明白什么意思,可能是多级 git 管理吧,我也勾选上了,既然都 ignore 了嘛:</p>
<p caption='统统勾上'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/fa5ad841-87ee-460f-ad01-d1c7a34f2553.webp' alt='统统勾上' title='统统勾上'></p>
<h3 id="调试和测试">调试和测试</h3>
<p>恕本人技术水平有限,VSCode 的调试和测试功能用的较少,只用来调试过诸如 NextJS 这类的 NodeJS 应用,使用起来跟 Chrome 差不多。因为用的少,所以没发现有什么痛点,所以没有什么配置可以优化的,这里就不说了。</p>
<h3 id="文件修改效果">文件修改效果</h3>
<p caption='实线比「装订线」好看'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/ef52ba2e-5ff7-4f8f-963d-32da4f899fe6.webp' alt='实线比「装订线」好看' title='实线比「装订线」好看'></p>
<p>在显示行号那一列,可以设置是实线还是「装订线」来显示差异,如:</p>
<p caption='实在不知道装订线存在的意义'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/4c009676-4e9a-4c90-8eab-5ceffae9593d.webp' alt='实在不知道装订线存在的意义' title='实在不知道装订线存在的意义'></p>
<p>我更喜欢实线,所以这两个选项都取消了。</p>
<h3 id="取消-git-提交按钮">取消 git 提交按钮</h3>
<p caption='移除多余的 UI 按钮'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/6343ab7e-7fc4-4cdc-8b0d-b65eae20b0b7.webp' alt='移除多余的 UI 按钮' title='移除多余的 UI 按钮'></p>
<p>说实话,左侧的这个提交按钮我从来没用过,都是使用命令行操作的 git,所以这个选项我取消了。</p>
<p>同理,这个按钮(看起来是 github copilot 的按钮,自动生成提交注释),我也取消了,尤其是对于公司项目,强制要求输入内容带上需求/bug 卡片编号的时候,这个智能写提效信息就更没用了:</p>
<p caption='移除自动写提交信息'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/111f48be-42e0-4f6a-b490-7dc832b15045.webp' alt='移除自动写提交信息' title='移除自动写提交信息'></p>
<h2 id="扩展">扩展</h2>
<h3 id="取消通知">取消通知</h3>
<p caption='取消全部扩展通知'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/caa23ff0-089b-4035-90e1-c914e44c61ac.webp' alt='取消全部扩展通知' title='取消全部扩展通知'></p>
<p>我不需要任何扩展告诉我应该怎么做,如果有需要,我会主动找它。</p>
<h2 id="终端">终端</h2>
<h3 id="右键行为">右键行为</h3>
<p caption='终端邮件默认居然是选中+菜单'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/8c75e65f-ae00-49f1-982f-82c682b4a2f9.webp' alt='终端邮件默认居然是选中+菜单' title='终端邮件默认居然是选中+菜单'></p>
<p>一般是鼠标左键选中后,右键出上下文操作。但 VSCode 默认行为居然是选中内容(单词)后出右键,可以,但没必要。</p>
<h3 id="终端最大行数">终端最大行数</h3>
<p caption='最大记录行'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/67d6cb52-ce20-40d2-ab50-548b7a4aab41.webp' alt='最大记录行' title='最大记录行'></p>
<p>这个其实改不改都行,我偶然情况下需要看很久之前的 log 信息,加上我的 64G 内存,调大点无所谓。</p>
<h3 id="终端滚动动画">终端滚动动画</h3>
<p caption='奇怪的动画,关了'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/7bd7b784-c3e5-456a-9aea-f0374dfa62b0.webp' alt='奇怪的动画,关了' title='奇怪的动画,关了'></p>
<p>虽然我喜欢动画(优雅),但是很奇怪,在终端的动画滚动似乎有点惯性,很难掌控滚动量,跟编辑器或者工作台内的滚动效果有很大差异,所以我关了。</p>
<h2 id="css-less-sass">CSS/Less/Sass</h2>
<h3 id="lint-重复属性警告">lint 重复属性警告</h3>
<p caption='需要设置三次'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/067e3056-d82f-4910-9e47-519259d54577.webp' alt='需要设置三次' title='需要设置三次'></p>
<p>这个很有必要,有时候你是从外面复制多个属性值粘贴(常见的是从浏览器检查元素的 style 上复制),然后去除重复的属性。</p>
<h2 id="git">Git</h2>
<h3 id="自动-stash">自动 Stash</h3>
<p caption='少操作一次'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/4fee6e57-9088-4a39-bbfe-c86ea5c9beb2.webp' alt='少操作一次' title='少操作一次'></p>
<p>如图,描述的很清楚了,建议开启,少一步操作。</p>
<h2 id="第三方扩展">第三方扩展</h2>
<p>其实没什么好说的,毕竟都装扩展了,肯定是有自己的需求才会装的,所以按照自己的需求配置即可。</p>
<h3 id="gitlens">GitLens</h3>
<p>不过有些插件,是可以关闭付费推荐的,对,说的就是 <code>GitLens</code> ,在我(意外)查看 git 分支合入情况的时候,会触发到付费功能的提示,这个可以关闭(感谢插件开发者的大度):</p>
<p caption='关掉 GitLens 付费功能提醒'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/28974458-b244-4ed9-919e-affe90c409fe.webp' alt='关掉 GitLens 付费功能提醒' title='关掉 GitLens 付费功能提醒'></p>
<h3 id="one-dark-pro">One Dark Pro</h3>
<p caption='高亮部分代码'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/a1ac82b3-7b1c-4fc4-9327-2cd04796bbee.webp' alt='高亮部分代码' title='高亮部分代码'></p>
<p>这个我喜欢,更显著的看到方法、函数名:</p>
<p caption='效果'><img src='https://static.xheldon.cn/img/in-post/2023/make-vscode-great-forever/d6235ab1-a532-4188-be52-2f29908b31e5.webp' alt='效果' title='效果'></p>
<h2 id="后记">后记</h2>
<p>说了这么多设置,适合自己的才是最重要的,祝大家高效工作,早点下班!</p>
</description>
<pubDate>Thu, 21 Dec 2023 00:00:00 +0000</pubDate>
<link>https://www.xheldon.com/life/make-vscode-great-forever.html</link>
<guid isPermaLink="true">https://www.xheldon.com/life/make-vscode-great-forever.html</guid>
<category>生活</category>
<category>折腾</category>
<category>教程</category>
<category>技巧</category>
<category>JavaScript</category>
<category>工作流</category>
<category>VSCode</category>
<category>设置</category>
<category>life</category>
</item>
<item>
<title>使用 Apple TV 收看北京联通 IPTV</title>
<description><h2 id="前言">前言</h2>
<p>之前在 <a href="/life/the-way-to-watching-tv.html">这篇博客</a> 讲了现在家庭观影的方案,其中提到的一个点是使用网络上别人抓取到的 IPTV 的节目地址(m3u 后缀),放入 iPlayTV 中,可以直接播。但是这个节目地址过一段时间就失效了,这是因为联通 IPTV 服务器会时不时的会更新一下节目的播放地址,而 Apple TV 上填写的地址是固定的,无法及时更新,因此本文就来解决这个问题。</p>
<p class='content-callout' style='background: rgb(253, 235, 236); color: ;'><span class='content-callout-icon'>🚧</span><span>**使用前必读:**本人为北京联通宽带,**光猫使用桥接方式,路由器拨号,软路由 R4S 以旁路由**的方式进行连接。本教程针对该拓扑方案进行介绍,其他网络拓扑如「光猫桥接 + R4S 当主路由进行拨号,或者光猫拨号 + R4S 旁路由」等,也可以实现,但是其中关键的 R4S 设置也许跟本教程有些许的差异,建议多搜索一下,领会其中的精神,不用完全跟我的一样。</span></p>
<p class='content-callout' style='background: rgb(251, 243, 219); color: ;'><span class='content-callout-icon'>🚧</span><span>同上,如果你是使用光猫直接拨号,你可以直接从光猫接根线出来到旁/主路由上进行组播变单播;也可以直接在支持的设备上使用组播地址进行播放,就不用像我这么麻烦了。具体还要看你家的网络拓扑结构而定。</span></p>
<p class='content-callout' style='background: rgb(251, 236, 221); color: ;'><span class='content-callout-icon'>🚧</span><span>本教程仅针对北京联通 IPTV 测试成功,其他地区可能有所差异,如果发现文本中有跟地域强相关的内容,请根据你所在的地区替换对应内容。</span></p>
<p class='content-callout' style='background: rgb(244, 238, 238); color: ;'><span class='content-callout-icon'>🚧</span><span>在修改设置前,请提前先备份所有路由器、设备的设置,以防万一。</span></p>
<p class='content-callout' style='background: rgb(251, 243, 219); color: ;'><span class='content-callout-icon'>🚧</span><span>我使用的软路由是 R4S,有两个网口。如果你是跟我一样的网络拓扑,请保证你的软路由至少有两个或以上网口,因为一个需要连主路由,另一个需要连光猫。</span></p>
<p class='content-callout' style='background: rgba(244, 240, 247, 0.8); color: ;'><span class='content-callout-icon'>🚧</span><span>我家的光猫网段是:192.168.1.x,主路由网段是 192.168.5.x;光猫地址是 192.168.1.1,R4S 的地址是 192.168.5.2,主路由 Lan 口地址是 192.168.5.1,路由器 Wan 口地址是 192.168.1.2。</span></p>
<h2 id="确定是否支持白嫖">确定是否支持白嫖</h2>
<p class='content-callout' style='background: rgb(231, 243, 248); color: ;'><span class='content-callout-icon'>🚧</span><span>目前北京地区的 IPTV 没有增加鉴权,但是从我看到的信息来看其他地区的一些运营商有对 IPTV 进行加密鉴权,即必须用运营商给的 IPTV 盒子的 Mac 地址连接(盒子起到解密的作用),才能实现脱离盒子使用软路由进行局域网任意设备的播放。具体如何实现比较复杂,本教程不介绍这个场景。</span></p>
<h3 id="提前下载-vlc">提前下载 VLC</h3>
<p>光猫桥接,路由器拨号的方式上网后,连接光猫后是无法上网的,因此请先提前在电脑上下载好 VLC 播放软件,以供一会儿进行测试,VLC 播放器地址在这里下载:</p>
<p><a href="https://www.videolan.org/vlc/" target="_blank"> Official download of VLC media player, the best Open Source player - VideoLAN</a></p>
<h3 id="连接光猫">连接光猫</h3>
<p>将<strong>电脑使用有线的方式连接光猫</strong>的 IPTV 口(如果光猫上没有发现 IPTV 口,则表示光猫支持混插,即接口不区分宽带和 IPTV 口,插任意一个口都行),然后在 VLC 软件中:</p>
<ul>
<li>
<p>打开 File-Open Network。</p>
</li>
<li>
<p>点击下面的 Open RTP/UDP Stream。</p>
</li>
<li>
<p>Protocol 选 RTP,Mode 选 Multicast,IP Address 填:<code>239.3.1.241</code> (或者 <code>rtp://239.3.1.241</code> ,具体哪个忘了)端口填 <code>8000</code> 。</p>
</li>
</ul>
<p>点 Open 之后,如果可以看到北京卫视,说明你可以免费白嫖。</p>
<p caption='VLC RTP 播放'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/ea9168ee-6086-42df-b6b4-269900eb5592.webp' alt='VLC RTP 播放' title='VLC RTP 播放'></p>
<p class='content-callout' style='background: rgb(251, 243, 219); color: ;'><span class='content-callout-icon'>💡</span><span>这里的 `rtp://239.3.1.241:8000` 就是北京卫视的组播地址,将来该地址可能有变化,具体准确的地址,可以到 `https://raw.githubusercontent.com/qwerttvv/Beijing-IPTV/master/IPTV-Unicom.m3u` 这里,找到任意一个 rtp 路径后面的 ip 地址进行测试。</span></p>
<h2 id="基本概念解释">基本概念解释</h2>
<p>不想了解的直接跳到下一节。</p>
<h3 id="iptv">IPTV</h3>
<p>根据维基百科对 IPTV 的解释:</p>
<p class='content-callout' style='background: rgb(241, 241, 239); color: ;'><span class='content-callout-icon'>📖</span><span>网路协定电视(英语:Internet Protocol Television,缩写:IPTV)是宽频电视的一种。IPTV 是用宽频网络作为介质传送电视信息的一种系统,将广播节目透过宽频上的网际协议(Internet Protocol, IP)向订户传递数码电视服务。由于需要使用网路,IPTV 服务供应商经常会一并提供连接互联网及 IP 电话等相关服务,也可称为“三重服务”或“三合一服务”(Triple Play)。IPTV 是数位电视的一种,因此普通电视机需要配合相应的机顶盒接收频道,也因此供应商通常会向客户同时提供随选视讯服务。虽透过宽频网路及网际协议,但 IPTV 不一定透过网际网路,为传输品质会通过局域网传输。</span></p>
<p>有此可知,一般情况下 IPTV 都是宽带提供商提供的服务,通过它可以看电视。</p>
<h3 id="组播">组播</h3>
<p>一种 IPTV 实现播放的技术手段,英文名叫「multicast」,也译为多播。具体概念不用弄太清楚,了解到它相比于单播:</p>
<p><strong>优势在于:</strong></p>
<ul>
<li>
<p>不占用互联网带宽。</p>
</li>
<li>
<p>IPTV 盒子起到认证的作用,IPTV 运营商由于是对一个组进行广播,因此对自己的服务器压力较小。</p>
</li>
</ul>
<p>劣势<strong>在于:</strong></p>
<ul>
<li>必须有线连接光猫 IPTV 口(有些光猫支持混插,即不区分 IPTV 口还是宽带口)才能用,因此只能连接 IPTV 盒子的设备使用,不能使用 WiFi 让家里任意设备观看网络电视。</li>
</ul>
<h3 id="单播">单播</h3>
<p>另一种较老的 IPTV 实现播放的技术手段,早期 IPTV 用户不多的时候使用该方案,相比于组播来说,彼之缺点就是其之优点,反之亦然,即:</p>
<p><strong>优势在于:</strong></p>
<ul>
<li>接入后,局域网支持 WiFi ,以供任意设备播放。</li>
</ul>
<p><strong>劣势在于:</strong></p>
<ul>
<li>
<p>跟服务器 1 对 1 连接,服务器压力较大,用户多的时候播放会比较卡顿。</p>
</li>
<li>
<p>占用宽带的带宽,直接使用互联网连接进行的播放(就跟现在看直播一样)。</p>
</li>
</ul>
<h3 id="udpxy">udpxy</h3>
<blockquote style='border-color: ;color: '><p> udpxy 服务器是**一款 UDP 流转 HTTP 流的代理服务器**,可以将 IP 直播流转化为 HTTP 流,方便在各种终端上播放。</p></blockquote>
<p>当无法直接获取到组播地址的时候,用来将组播地址转为单播地址,如组播地址是:a:b, d:e(a、d 为 ip 地址,b、e 为端口);转换成单播地址后就是统一的地址如 z/a/b,z/d/e。播放器监听这个地址 z 即可。</p>
<h3 id="m3u">m3u</h3>
<blockquote style='border-color: ;color: '><p> **M3U**(MP3 URL 的缩写)是一种播放多媒体列表的文件格式,它的设计初衷是为了播放音频文件,比如 MP3,但是越来越多的软件现在用做播放视频文件列表的格式。</p></blockquote>
<p>m3u 文件就是一个里面含有全部播放组播地址的文本文件,在 Apple TV 或者电脑上读取这个文件地址,就可以播放其中的视频地址。</p>
<h3 id="epg">EPG</h3>
<p>包含节目单信息,上一步知道 m3u 地址后,里面都是一个个的 ip 地址,那如何知道每个 ip 地址是哪个频道呢?这个时候就需要一个 EPG 进行配对了,EPG 含有每个地址的节目信息,甚至是频道的简单介绍,EPG 通常会随着视频信号一起广播。</p>
<h2 id="网络拓扑">网络拓扑</h2>
<p caption='网络拓扑-主路由拨号'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/0349a1d5-caeb-4233-b36b-8bc1c7db7565.webp' alt='网络拓扑-主路由拨号' title='网络拓扑-主路由拨号'></p>
<h2 id="上手实操">上手实操</h2>
<p class='content-callout' style='background: rgb(251, 243, 219); color: ;'><span class='content-callout-icon'>🚧</span><span>动手之前先按照开头所述检查是否支持白嫖。</span></p>
<h3 id="设置软路由">设置软路由</h3>
<p><strong>安装 udpxy 和 luci-udpxy</strong></p>
<p>这一步就是常规的操作了,UI 安装最方便,如图(安装完成后先别启用,最后一步再启用):</p>
<p caption='软件安装界面'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/2be1ca35-90a8-4936-89cd-5458557ac064.webp' alt='软件安装界面' title='软件安装界面'></p>
<p><strong>新建/配置网络接口</strong></p>
<p>这一步为了让软路由识别到来自光猫的数据。</p>
<ul>
<li>在 <code>网络-接口</code> 中新建一个接口,随便起个名字叫 <code>IPTV</code> :</li>
</ul>
<p caption='新建 IPTV 接口'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/a938e2a7-9723-40f5-8b86-3ed151ba12c5.webp' alt='新建 IPTV 接口' title='新建 IPTV 接口'></p>
<p>注意箭头的部分,我已经新建好了所以括号中有 IPTV 字样,刚新建的时候是没有的。其中 eth1 是我的 Lan 口,接的是主路由;eth0 是另一个接口,接的即是光猫(IPTV 口);这里我曾经修改过,默认情况下,eht0 是 Lan 口,eth1 是 Wan 口,不重要,这一步是将 Wan 口用作 IPTV 口。</p>
<ul>
<li>配置「IPTV」接口的网关跃点与防火墙设置:</li>
</ul>
<p caption='配置网关跃点'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/d1df1f84-7240-4eb9-872b-6871f9c895a6.webp' alt='配置网关跃点' title='配置网关跃点'></p>
<p caption='防火墙配置到 wan 上'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/c0e4032b-f066-4fcf-b906-dae85a37d070.webp' alt='防火墙配置到 wan 上' title='防火墙配置到 wan 上'></p>
<ul>
<li>配置 Wan 口的网关跃点:</li>
</ul>
<p caption='Wan 口网关跃点'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/21347824-ca57-4715-9fe9-cd7b6be0f282.webp' alt='Wan 口网关跃点' title='Wan 口网关跃点'></p>
<ul>
<li>配置 Lan 口 IGMP 嗅探:</li>
</ul>
<p caption='IGMP 嗅探'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/b075aaec-042b-4f9d-9437-27ea4d17211e.webp' alt='IGMP 嗅探' title='IGMP 嗅探'></p>
<ul>
<li>配置网络防火墙</li>
</ul>
<p caption='网络防火墙配置'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/577418eb-c3f6-43be-b73b-b1cf3ab5dcea.webp' alt='网络防火墙配置' title='网络防火墙配置'></p>
<p caption='防火墙配置2'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/b3ce719b-a05d-41ef-ab4a-12eb866178a8.webp' alt='防火墙配置2' title='防火墙配置2'></p>
<p><strong>配置 udpxy 服务</strong></p>
<ul>
<li>如图配置即可,注意这里的 eth0 是 Wan 口,别搞错了:</li>
</ul>
<p caption='打开 UDPXY'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/bcd40c05-ed7f-43fd-b833-4cee94948150.webp' alt='打开 UDPXY' title='打开 UDPXY'></p>
<p>最后尝试打开 [<a href="http://192.168.5.2:4022/status">http://192.168.5.2:4022/status</a>]([object Object]) 验证 udpxy 服务是否启动成功:</p>
<p caption='看到这个就表示成功了'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/189e9cb4-80c9-4b5d-8f57-722660e66145.webp' alt='看到这个就表示成功了' title='看到这个就表示成功了'></p>
<p>之后,将之前使用 VLC 打开的地址 <code>rtp://239.3.1.241:8000</code> 改成 <code>http://192.168.2.1:4022/rtp/239.3.1.241:8000</code> 再尝试打开(不用点下面的 Open RTP/UDP Stream,直接在 URL 中打开)。</p>
<p caption='尝试将 RTP 变 HTTP 播放'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/582e015c-4260-4181-a81c-5eafc97a1132.webp' alt='尝试将 RTP 变 HTTP 播放' title='尝试将 RTP 变 HTTP 播放'></p>
<p>然后双击播放即可:</p>
<p caption='成功画面↑'><img src='https://static.xheldon.cn/img/in-post/2023/iptv-for-apple-tv-in-beijing/215aa895-e673-419a-a4fc-b94603af8baa.webp' alt='成功画面↑' title='成功画面↑'></p>
<h3 id="使用播放软件">使用播放软件</h3>
<p>我是在 Apple TV 4K 看电视的,试了几个播放软件,这里简单说说:</p>
<ul>
<li>
<p>iPlayTV 不能播放,不知道为什么。</p>
</li>
<li>
<p>Fileball 上下换台没问题,但是一选台就闪退。</p>
</li>
<li>
<p>最终选择了 IIVA 同一个开发者的 app:「TV+」,港区售价 38 港币(购买第二天就限免,尴尬)。</p>
</li>
</ul>
<h2 id="如何实现自动更新地址">如何实现自动更新地址</h2>
<p>联通的 IPTV 节目播放地址,每隔一段时间都会变一次,有时候变的是端口,有时候变的是 ip 地址,这个时候再用这个办法播放就失效了,怎么办呢?</p>
<p>网上有好心人,经过一系列复杂监听操作,比如这个:</p>
<p><a href="https://blog.friskit.me/2020/05/31/bjunicom-network.html" target="_blank"> 光纤入户光猫改桥接+内网转发IPTV=任意设备看电视直播 - Botian's Blog</a></p>
<p>获取到了 IPTV 电视盒子与联通服务器通信的数据,拿到了它的地址,因此我们直接使用即可,如:</p>
<p><a href="https://github.com/qwerttvv/Beijing-IPTV/blob/master/README.md" target="_blank"> github.com</a></p>
<p>但是这里有个点是,你家里的软路由地址需要跟这位好心人的地址是一样的(192.168.123.1),然后将 udpxy 端口改为 23234,这样你就可以直接将这个地址放到 TV+ 里面即可:</p>
<p><a href="https://raw.githubusercontent.com/qwerttvv/Beijing-IPTV/master/IPTV-Unicom.m3u" target="_blank"> raw.githubusercontent.com</a></p>
<p>否则,你就只能主动监听这个文件的变动,然后更新。</p>
<p>这里我花十分钟在 Vercel 部署了一个 API 服务,同时启用了 Vercel 的 Corn Jobs 服务,可以定时执行函数,来检测变动,代码如下,你也可以自己部署一套:</p>
<figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">import</span> type &#123; <span class="hljs-title class_">NextApiRequest</span>, <span class="hljs-title class_">NextApiResponse</span> &#125; <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;next&#x27;</span>;<br><br>type <span class="hljs-title class_">ResponseData</span> = &#123;<br> <span class="hljs-attr">msg</span>: string,<br>&#125;;<br><br><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 该函数用来将网友通过 IPTV 盒子抓包获取的联通单播地址,转成自己的单播地址</span><br><span class="hljs-comment"> * 该函数每天 3 点触发一次,定时检测网友的单播地址是否有更新,使用 vercel corn 任务进行</span><br><span class="hljs-comment"> * <span class="hljs-doctag">TODO:</span> 该函数未做鉴权,任何人都可以手动触发检测,为了防止滥用可以加上鉴权,但是 corn 似乎没这个功能</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">handler</span>(<span class="hljs-params"></span><br><span class="hljs-params"> request: NextApiRequest,</span><br><span class="hljs-params"> response: NextApiResponse&lt;ResponseData&gt;</span><br><span class="hljs-params"></span>) &#123;<br> <span class="hljs-comment">// Note: 步骤</span><br> <span class="hljs-comment">// 1. 获取网友通过监听盒子数据包抓取的(自己搞比较费劲,直接用现成的了)联通 IPTV 永久地址(rtp 协议的组播地址,多用户通用),获取其内容</span><br> <span class="hljs-comment">// 2. 添加本地 udpxy 转发地址</span><br> <span class="hljs-comment">// 3. 获取之前的 github gist 内容以对比二者</span><br> <span class="hljs-comment">// 4. 有差异,则更新 github gist 内容</span><br> <span class="hljs-comment">// 5. 没有,则不做操作</span><br> <span class="hljs-comment">// Note: 环境变量,自己在 Vercel 中设置好</span><br> <span class="hljs-keyword">const</span> token = process.<span class="hljs-property">env</span>.<span class="hljs-property">GITHUB_TOKEN</span>;<br> <span class="hljs-keyword">const</span> gist = process.<span class="hljs-property">env</span>.<span class="hljs-property">GIST_URL</span>;<br> <span class="hljs-keyword">const</span> id = process.<span class="hljs-property">env</span>.<span class="hljs-property">GIST_ID</span>;<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_">fetch</span>(<br> <span class="hljs-string">&#x27;https://raw.githubusercontent.com/qwerttvv/Beijing-IPTV/master/IPTV-Unicom.m3u&#x27;</span><br> )<br> .<span class="hljs-title function_">then</span>(<span class="hljs-keyword">async</span> (res) =&gt; &#123;<br> <span class="hljs-keyword">if</span> (!res.<span class="hljs-property">ok</span>) &#123;<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;获取源地址异常&#x27;</span>);<br> <span class="hljs-keyword">return</span> response.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>(&#123;<br> <span class="hljs-attr">msg</span>: <span class="hljs-string">&#x27;获取源地址异常&#x27;</span>,<br> &#125;);<br> &#125;<br> <span class="hljs-keyword">const</span> src = <span class="hljs-keyword">await</span> res.<span class="hljs-title function_">text</span>();<br> <span class="hljs-comment">// Note: 替换网友的本地单播地址为我的,其实你也可以将自己家的路由器网段设置成跟网友的一样(192.168.123.x),udpxy 端口转发设置成跟网友一样(23234),你就可以直接使用该地址了</span><br> <span class="hljs-keyword">const</span> newGist = src.<span class="hljs-title function_">replace</span>(<br> <span class="hljs-regexp">/http\:\/\/192\.168\.123\.1\:23234/g</span>,<br> <span class="hljs-string">&#x27;http://192.168.5.2:4022&#x27;</span><br> );<br> response.<span class="hljs-title function_">setHeader</span>(<span class="hljs-string">&#x27;Content-Type&#x27;</span>, <span class="hljs-string">&#x27;text/html; charset=utf-8&#x27;</span>);<br> <span class="hljs-comment">// Note: 获取 gist 的 raw 内容,需要加个 cache-bust 否则每次请求会被缓存</span><br> <span class="hljs-keyword">return</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">`<span class="hljs-subst">$&#123;gist&#125;</span>?cache-bust=<span class="hljs-subst">$&#123;<span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">100000</span>)&#125;</span>`</span>, &#123;<br> <span class="hljs-attr">headers</span>: &#123;<br> <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">$&#123;token&#125;</span>`</span>,<br> &#125;,<br> &#125;)<br> .<span class="hljs-title function_">then</span>(<span class="hljs-keyword">async</span> (pre) =&gt; &#123;<br> <span class="hljs-keyword">const</span> preGist = <span class="hljs-keyword">await</span> pre.<span class="hljs-title function_">text</span>();<br> <span class="hljs-comment">// console.log(&#x27;preGist:&#x27;, preGist);</span><br> <span class="hljs-keyword">if</span> (<span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(newGist) !== <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(preGist)) &#123;<br> <span class="hljs-comment">// Note: 更新 Gist</span><br> <span class="hljs-keyword">const</span> files = &#123;<br> <span class="hljs-string">&#x27;IPTV.m3u&#x27;</span>: &#123;<br> <span class="hljs-attr">content</span>: newGist,<br> &#125;,<br> &#125;;<br> <span class="hljs-keyword">return</span> <span class="hljs-title function_">fetch</span>(<span class="hljs-string">`https://api.github.com/gists/<span class="hljs-subst">$&#123;id&#125;</span>`</span>, &#123;<br> <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;PATCH&#x27;</span>,<br> <span class="hljs-attr">headers</span>: &#123;<br> <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">$&#123;token&#125;</span>`</span>,<br> <span class="hljs-string">&#x27;Content-Type&#x27;</span>: <span class="hljs-string">&#x27;text/plain&#x27;</span>,<br> &#125;,<br> <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>(&#123; files &#125;),<br> &#125;)<br> .<span class="hljs-title function_">then</span>(<span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> &#123;<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<br> <span class="hljs-string">`更新成功: <span class="hljs-subst">$&#123;gist&#125;</span>?cache-bust=<span class="hljs-subst">$&#123;<span class="hljs-built_in">Math</span>.floor(</span></span><br><span class="hljs-subst"><span class="hljs-string"> <span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">1000000</span></span></span><br><span class="hljs-subst"><span class="hljs-string"> )&#125;</span>`</span><br> );<br> <span class="hljs-keyword">return</span> response.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>(&#123;<br> <span class="hljs-attr">msg</span>: <span class="hljs-string">`更新成功: <span class="hljs-subst">$&#123;gist&#125;</span>?cache-bust=<span class="hljs-subst">$&#123;<span class="hljs-built_in">Math</span>.floor(</span></span><br><span class="hljs-subst"><span class="hljs-string"> <span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">1000000</span></span></span><br><span class="hljs-subst"><span class="hljs-string"> )&#125;</span>`</span>,<br> &#125;);<br> &#125;)<br> .<span class="hljs-title function_">catch</span>(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> &#123;<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`更新失败: <span class="hljs-subst">$&#123;e&#125;</span>`</span>);<br> <span class="hljs-keyword">return</span> response.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>(&#123;<br> <span class="hljs-attr">msg</span>: <span class="hljs-string">`更新失败: <span class="hljs-subst">$&#123;e&#125;</span>`</span>,<br> &#125;);<br> &#125;);<br> &#125;<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<br> <span class="hljs-string">`未变化: <span class="hljs-subst">$&#123;gist&#125;</span>?cache-bust=<span class="hljs-subst">$&#123;<span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">1000000</span>)&#125;</span>`</span><br> );<br> <span class="hljs-keyword">return</span> response.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>(&#123;<br> <span class="hljs-attr">msg</span>: <span class="hljs-string">`未变化: <span class="hljs-subst">$&#123;gist&#125;</span>?cache-bust=<span class="hljs-subst">$&#123;<span class="hljs-built_in">Math</span>.floor(</span></span><br><span class="hljs-subst"><span class="hljs-string"> <span class="hljs-built_in">Math</span>.random() * <span class="hljs-number">1000000</span></span></span><br><span class="hljs-subst"><span class="hljs-string"> )&#125;</span>`</span>,<br> &#125;);<br> &#125;)<br> .<span class="hljs-title function_">catch</span>(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> &#123;<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`获取自己的 gist 失败: <span class="hljs-subst">$&#123;e&#125;</span>`</span>);<br> <span class="hljs-keyword">return</span> response.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>(&#123;<br> <span class="hljs-attr">msg</span>: <span class="hljs-string">`获取自己的 gist 失败: <span class="hljs-subst">$&#123;e&#125;</span>`</span>,<br> &#125;);<br> &#125;);<br> &#125;)<br> .<span class="hljs-title function_">catch</span>(<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> &#123;<br> <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">`获取别人的源失败: <span class="hljs-subst">$&#123;e&#125;</span>`</span>);<br> <span class="hljs-keyword">return</span> response.<span class="hljs-title function_">status</span>(<span class="hljs-number">200</span>).<span class="hljs-title function_">json</span>(&#123;<br> <span class="hljs-attr">msg</span>: <span class="hljs-string">`获取别人的源失败: <span class="hljs-subst">$&#123;e&#125;</span>`</span>,<br> &#125;);<br> &#125;);<br>&#125;<br></code></pre></td></tr></table></figure>
<p>Corn Jobs 服务配置:</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs json"><span class="hljs-punctuation">&#123;</span><br> <span class="hljs-attr">&quot;crons&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><br> <span class="hljs-punctuation">&#123;</span><br> <span class="hljs-attr">&quot;path&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;/api/get&quot;</span><span class="hljs-punctuation">,</span><br> <span class="hljs-attr">&quot;schedule&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;0 15 * * *&quot;</span><br> <span class="hljs-punctuation">&#125;</span><br> <span class="hljs-punctuation">]</span><br><span class="hljs-punctuation">&#125;</span><br></code></pre></td></tr></table></figure>
<p>拉取该文件后若有更新,会自动更新 gist 文件:</p>
<p><a href="https://gist.githubusercontent.com/Xheldon/73bf97cb5ac5db2f5237264556b20951/raw/ea44694028a38baefff04ea46c02795e448d76f0/IPTV.m3u" target="_blank"> gist.githubusercontent.com</a></p>
<p>这样,我只需要在 TV+ 中写死这个 gist 地址,即可在网友更新这个地址的时候,自动更新了。</p>
<h2 id="参考链接">参考链接</h2>
<p><a href="https://blog.lishun.me/iptvhelper-guide" target="_blank"> 单线融合IPTV到家庭局域网最简单的方法:路由+桥接混合模式</a></p>
<p><a href="https://blog.friskit.me/2020/05/31/bjunicom-network.html" target="_blank"> 光纤入户光猫改桥接+内网转发IPTV=任意设备看电视直播 - Botian's Blog</a></p>
<p><a href="https://www.haoyizebo.com/posts/6a0c2301/" target="_blank"> 北京联通白嫖 IPTV</a></p>
</description>
<pubDate>Mon, 30 Oct 2023 00:00:00 +0000</pubDate>
<link>https://www.xheldon.com/life/iptv-for-apple-tv-in-beijing.html</link>
<guid isPermaLink="true">https://www.xheldon.com/life/iptv-for-apple-tv-in-beijing.html</guid>
<category>生活</category>
<category>经验</category>
<category>Apple</category>
<category>折腾</category>
<category>苹果</category>
<category>网络</category>
<category>路由器</category>
<category>教程</category>
<category>life</category>
</item>
<item>
<title>TeslaMate 使用指南</title>
<description><p class='content-callout' style='background: rgb(251, 236, 221); color: ;'><span class='content-callout-icon'>💡</span><span>本指南需要有一丁点的编程知识,知道什么是终端、什么是命令行。</span></p>
<p class='content-callout' style='background: rgba(244, 240, 247, 0.8); color: ;'><span class='content-callout-icon'>💡</span><span>本教程使用 Docker 安装 TeslaMate,如果你是在软路由环境,可能需要做一些额外操作如端口映射等,浏览器才能访问。而我的 Mac 电脑常年不关机,因此装在了 Mac 系统下的 Docker 上。</span></p>
<p class='content-callout' style='background: rgb(231, 243, 248); color: ;'><span class='content-callout-icon'>💡</span><span>有点遗憾的是,TeslaMate 不能获取车辆的历史信息,因此你只能查看安装 TeslaMate 后的车辆行驶数据,且 TeslaMate 的服务不能关闭,否则无法记录到相关行驶数据。</span></p>
<h2 id="前言">前言</h2>
<p>「TeslaMate」,简单翻译过来就是「特斯拉伴侣」,它是一款开源软件,可以获取车辆上报给特斯拉服务器的数据,然后使用 Grafana 这款 Web 数据可视化仪表盘工具显示出来。</p>
<p>TeslaMate 的仓库:</p>
<p><a href="https://github.com/adriankumpf/teslamate#teslamate" target="_blank"> GitHub - adriankumpf/teslamate: A self-hosted data logger for your Tesla 🚘</a></p>
<p>TeslaMate 的文档:</p>
<p><a href="https://docs.teslamate.org/docs/installation/docker" target="_blank"> Docker install | TeslaMate</a></p>
<hr />
<p>特斯拉的工程师们会收集这些数据进行车辆的大数据分析、软件优化、电池充电优化等,而我们个人车主获取这些信息则可以更好的了解自己爱车的一些详细数据,如历史行程、每日行驶里程数、耗电情况、充电效率等。</p>
<p>之所以有这篇博文是因为 TeslaMate 的文档只说了如何安装(很简单),但是并没有告诉你安装完成后如何配置才能看到想要的仪表盘,而网上的一些内容农场靠着 SEO 技巧,排名靠前的也都是复制粘贴官网内容来的,要找到想要的信息是有点困难的,我目前没看到有任何一篇讲从如何安装到成品的文章,所以本文从头讲起。</p>
<p>先上一张成果图(用户可以自定义面板):</p>
<p caption='TeslaMate 成果图'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/03d97d52-796e-4e46-83be-3ec3b3013cea.webp' alt='TeslaMate 成果图' title='TeslaMate 成果图'></p>
<h2 id="一、安装-docker">一、安装 Docker</h2>
<p>如开头所述,我使用 Docker 进行安装,TeslaMate 的文档要求是安装 Docker 和 Docker Compose(别管是什么,装就完了),而我们只需要安装 Docker Desktop 即可将这二者都装了,Docker Desktop 下载在这里:</p>
<p><a href="https://www.docker.com/products/docker-desktop/" target="_blank"> Docker Desktop: The #1 Containerization Tool for Developers | Docker</a></p>
<p>安装后启动,然后随便找个目录(TeslaMate 的全部文件后续都会在这个目录,不要删除),将官方给的 <code>docker-compose.yml</code> 文件放入其中,我这里放到了 <code>~/Developer/Docker/TeslaMate</code> 下,<code>docker-compose.yml</code> 内容如下:</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><code class="hljs yaml"><span class="hljs-attr">version:</span> <span class="hljs-string">&#x27;3&#x27;</span><br><br><span class="hljs-attr">services:</span><br> <span class="hljs-attr">teslamate:</span><br> <span class="hljs-attr">image:</span> <span class="hljs-string">teslamate/teslamate:latest</span><br> <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br> <span class="hljs-attr">environment:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">ENCRYPTION_KEY=</span> <span class="hljs-comment">#设置 TeslaMate API 加密密码,注意等号后面不要有空格</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_USER=teslamate</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_PASS=</span> <span class="hljs-comment">#设置安全数据库密码,注意等号后面不要有空格</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_NAME=teslamate</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_HOST=database</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">MQTT_HOST=mosquitto</span><br> <span class="hljs-attr">ports:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-number">4000</span><span class="hljs-string">:4000</span><br> <span class="hljs-attr">volumes:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">./import:/opt/app/import</span><br> <span class="hljs-attr">cap_drop:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">all</span><br><br> <span class="hljs-attr">database:</span><br> <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:15</span><br> <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br> <span class="hljs-attr">environment:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_USER=teslamate</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_PASSWORD=</span> <span class="hljs-comment">#设置数据库密码,注意等号后面不要有空格</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">POSTGRES_DB=teslamate</span><br> <span class="hljs-attr">volumes:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">teslamate-db:/var/lib/postgresql/data</span><br><br> <span class="hljs-attr">grafana:</span><br> <span class="hljs-attr">image:</span> <span class="hljs-string">teslamate/grafana:latest</span><br> <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br> <span class="hljs-attr">environment:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_USER=teslamate</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_PASS=</span> <span class="hljs-comment">#设置 grafana 的数据库密码,注意等号后面不要有空格</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_NAME=teslamate</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">DATABASE_HOST=database</span><br> <span class="hljs-attr">ports:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-number">3000</span><span class="hljs-string">:3000</span><br> <span class="hljs-attr">volumes:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">teslamate-grafana-data:/var/lib/grafana</span><br><br> <span class="hljs-attr">mosquitto:</span><br> <span class="hljs-attr">image:</span> <span class="hljs-string">eclipse-mosquitto:2</span><br> <span class="hljs-attr">restart:</span> <span class="hljs-string">always</span><br> <span class="hljs-attr">command:</span> <span class="hljs-string">mosquitto</span> <span class="hljs-string">-c</span> <span class="hljs-string">/mosquitto-no-auth.conf</span><br> <span class="hljs-comment"># ports:</span><br> <span class="hljs-comment"># - 1883:1883</span><br> <span class="hljs-attr">volumes:</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">mosquitto-conf:/mosquitto/config</span><br> <span class="hljs-bullet">-</span> <span class="hljs-string">mosquitto-data:/mosquitto/data</span><br><br><span class="hljs-attr">volumes:</span><br> <span class="hljs-attr">teslamate-db:</span><br> <span class="hljs-attr">teslamate-grafana-data:</span><br> <span class="hljs-attr">mosquitto-conf:</span><br> <span class="hljs-attr">mosquitto-data:</span><br></code></pre></td></tr></table></figure>
<p>注意有些服务的密码设置是需要跟另一个服务密码一致的,所以我建议将上述的密码都设置成一样,省的麻烦。另外尤其需要注意的是 <code>=</code> 后面不要有空格。之后打开终端执行(每行复制到终端后按回车):</p>
<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash"><span class="hljs-comment"># 假设你放上述文件的目录为 ~/Developer/Docker/TeslaMate</span><br><span class="hljs-built_in">cd</span> ~/Developer/Docker/TeslaMate<br>docker compose up -d<br></code></pre></td></tr></table></figure>
<p>完成后,打开 Docker Desktop,应该是这个样子:</p>
<p caption='安装完成后截图'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/cd39c66f-60b7-4d3d-81bd-f5ac50260d28.webp' alt='安装完成后截图' title='安装完成后截图'></p>
<h2 id="二、登陆特斯拉账号授权">二、登陆特斯拉账号授权</h2>
<p>上一步完成无错误后,确保全部的服务都处于 Running 状态,(可以点击上面截图的各个服务名字查看对应 log,看有无类似与 error 之类的报错),在浏览器中打开 <code>localhost:4000</code> 后,发现会要求你输入 API Token 和 Refresh Token,这两个获取方式官方文档有写,在这里:</p>
<p><a href="https://docs.teslamate.org/docs/faq/#how-to-generate-your-own-tokens" target="_blank"> Frequently Asked Questions | TeslaMate</a></p>
<p>这里我使用第三种方式「Tesla Auth (macOS, Linux, Windows) 」点击以下链接查看适用于自己系统的版本,下载对应文件(是一个可执行文件):</p>
<p><a href="https://github.com/adriankumpf/tesla_auth#download" target="_blank"> GitHub - adriankumpf/tesla_auth: Securely generate API tokens for third-party access to your Tesla.</a></p>
<p>直接双击打开(或者在终端打开)的话,如果是 MacOS 会提醒你该执行文件可能有危险,去 <code>系统设置-隐私与安全性</code>中,点击「仍要打开」即可。打开后会弹出一个窗口,让你登陆特斯拉账号,登陆完成后,页面会显示出特斯拉的 API Token 和 Refresh Token(截图我就不放了),将其复制到 <code>localhost:4000</code> 页面中的对应位置,即可成功登陆,登陆成功的界面是这样的:</p>
<p caption='TeslaMate 授权成功界面'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/8d41bc35-475f-499d-b2d3-c4812e9a72e9.webp' alt='TeslaMate 授权成功界面' title='TeslaMate 授权成功界面'></p>
<p>到这就算是鉴权成功了,接下来配置 Grafana。</p>
<h2 id="三、grafana-基本概念">三、Grafana 基本概念</h2>
<p>Grafana 是一款非常强大的 Web 数据可视化仪表盘工具,使用相对比较复杂,而且我没有研究它汉化的方法。好在 Tesla 的数据字段比较简单,也用不着汉化。这里首先简单介绍一下相关概念,方便后续的自定义。</p>
<h3 id="dashboard-和-panel">Dashboard 和 Panel</h3>
<p>Dashboard 就是一个显示各种数据的界面,可以显示不同的 Panel,Panel 就是一个查询数据库获得数据后,将数据可视化的一个个模块,如下每个红框就是一个 Panel:</p>
<p caption='N 个 Panel 组成了一个 Dashboard'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/6b5bfe32-8214-4994-a6ce-9b3a1874e11e.webp' alt='N 个 Panel 组成了一个 Dashboard' title='N 个 Panel 组成了一个 Dashboard'></p>
<p>Panel 可以在不同的 Dashboard 之间复制,如下:</p>
<p caption='Panel 复制'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/2522b63e-34e9-44e2-a0ba-2d74901ea53b.webp' alt='Panel 复制' title='Panel 复制'></p>
<p>点击复制后,就可以在另一个 Dashboard 中粘贴,点击 Dashboard 右上角的 Add Panel 按钮(或者首次新建一个 Dashboard 的时候)(如果没在上一步对 Panel 点 Copy,下图中的第四个「Paste from Clipboard 」就不会出现)就会出现下面的新建 Panel:</p>
<p caption='粘贴刚刚复制的 Panel'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/37d13b9a-d6b0-4e19-9bd9-b41712ea5e05.webp' alt='粘贴刚刚复制的 Panel' title='粘贴刚刚复制的 Panel'></p>
<h3 id="variable(变量)和-json-model">Variable(变量)和 JSON Model</h3>
<p><strong>Variable</strong></p>
<p>每个 Dashboard 可以设置供 Panel 使用的 Variable,而 Panel 如何使用 Variable 呢?Panel 的数据是通过 SQL 查询查出来的,而 SQL 的部分语法是 grafana 的 SQL 模板语法,下面是查询海拔的 SQL 语法:</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">SELECT</span><br> $__time(<span class="hljs-type">date</span>),<br> ROUND(convert_m(elevation, <span class="hljs-string">&#x27;$alternative_length_unit&#x27;</span>)) <span class="hljs-keyword">AS</span> elevation_[[alternative_length_unit]]<br><span class="hljs-keyword">FROM</span><br> positions<br><span class="hljs-keyword">WHERE</span><br> car_id <span class="hljs-operator">=</span> $car_id <span class="hljs-keyword">AND</span><br> $__timeFilter(<span class="hljs-type">date</span>)<br><span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span><br> <span class="hljs-type">date</span> <span class="hljs-keyword">ASC</span><br></code></pre></td></tr></table></figure>
<p>其中的带 <code>$</code> 符号的就是预设的 Variable,有些是内置的如 <code>$__timeFilter</code>,有些是自定义的如 <code>$alternative_length_unit</code>,自定义的变量就是 Dashboard 配置的 Variable。Dashboard 的 Variable 在 Dashboard 右上角的 Dashboard Setting 里配置:</p>
<p caption='Variable 配置'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/f624e3f9-bea2-4293-82e0-e751886acb3b.webp' alt='Variable 配置' title='Variable 配置'></p>
<p><strong>JSON Model</strong></p>
<p>有时候你会发现,从一个 Dashboard 复制了一个 Panel 粘贴到新 Dashboard 后,Panel 并不能正常显示数据,就是因为你没有将源 Panel 用到的 Variable 从源 Dashboard 中复制到新 Dashboard 中,导致 SQL 语法报错,数据无法查询出来。但是如果 Dashboard 的 Variable 很多的话,一个一个复制又非常麻烦,怎么办呢?此时可以通过 JSON Model 来解决。</p>
<p>JSON Model 其实就是 Dashboard 配置的 JSON 格式,因此,你只需要复制某个 Dashboard 对应字段的内容,就可以复制相应的内容到目标 Dashboard。Variable 在 JSON Model 中对应的字段是 <code>templating</code> ,复制其内的 <code>list</code> 中的对应项到目标 Dashboard 的 JSON Model 对应 <code>templating</code> 字段的 <code>list</code> 字段,作为其项即可(<strong>记得点击 Save Changes 后再点击 Save dashboard</strong>):</p>
<p caption='直接复制对应 JSON'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/1ebb8257-1f17-48ee-9406-5849d320dea5.webp' alt='直接复制对应 JSON' title='直接复制对应 JSON'></p>
<p>Grafana 还有很多其他复杂的功能,不过对于我们特斯拉数据可视化来说,了解这么多即可。</p>
<h2 id="四、配置-grafana">四、配置 Grafana</h2>
<p>访问 <code>localhost:3000</code> 后首先需要登录,首次默认用户密码都是 <code>admin</code> ,首次登陆成功后会让你设置密码,设置后进入是这个界面:</p>
<p caption='Grafana 默认 Dashboard'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/de518aa9-edce-48d8-94db-fbf8a7a48f85.webp' alt='Grafana 默认 Dashboard' title='Grafana 默认 Dashboard'></p>
<p>这里,TeslaMate 已经给我们配置好了各种 Dashboard,点击左侧的这个地方可以打开文件夹查看文件夹目录下的各个 Dashboard:</p>
<p caption='查看 TeslaMate 预设的 Dashboard'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/c36ebc9b-bda2-4ab6-933d-2da603f9ccf7.webp' alt='查看 TeslaMate 预设的 Dashboard' title='查看 TeslaMate 预设的 Dashboard'></p>
<p caption='各种 Dashboard'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/58bbe182-7bbf-4814-97dd-c8406525a659.webp' alt='各种 Dashboard' title='各种 Dashboard'></p>
<p>随便点开一个,如 <code>Drive Stats</code>,可以查看相应 Dashboard 中的各种 Panel:</p>
<p caption='Drive Stats Dashboard'><img src='https://static.xheldon.cn/img/in-post/2023/the-use-of-teslamate/29c1c887-e773-40b5-9ea1-fb8c3cc703dd.webp' alt='Drive Stats Dashboard' title='Drive Stats Dashboard'></p>