-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
2301 lines (2166 loc) · 95.4 KB
/
atom.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"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://jiangrubin.github.io</id>
<title>Rubin's Blog</title>
<updated>2024-08-20T11:57:33.042Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="https://jiangrubin.github.io"/>
<link rel="self" href="https://jiangrubin.github.io/atom.xml"/>
<subtitle>温故而知新</subtitle>
<logo>https://jiangrubin.github.io/images/avatar.png</logo>
<icon>https://jiangrubin.github.io/favicon.ico</icon>
<rights>All rights reserved 2024, Rubin's Blog</rights>
<entry>
<title type="html"><![CDATA[前端领域的 IoC 理念]]></title>
<id>https://jiangrubin.github.io/post/lBW2YQBoU/</id>
<link href="https://jiangrubin.github.io/post/lBW2YQBoU/">
</link>
<updated>2023-09-27T14:37:32.000Z</updated>
<content type="html"><![CDATA[<h2 id="背景">背景</h2>
<p>前端应用在不断壮大的过程中,内部模块间的依赖可能也会随之越来越复杂,模块间的 低复用性 导致应用 难以维护,不过我们可以借助计算机领域的一些优秀的编程理念来一定程度上解决这些问题,接下来要讲述的 IoC 就是其中之一。</p>
<h2 id="什么是-ioc">什么是 IoC</h2>
<p>IoC 的全称叫做 Inversion of Control,可翻译为「控制反转」或「依赖倒置」,它主要包含了三个准则:</p>
<ol>
<li>高层次的模块不应该依赖于低层次的模块,它们都应该依赖于抽象</li>
<li>抽象不应该依赖于具体实现,具体实现应该依赖于抽象</li>
<li>面向接口编程,而不要面向实现编程</li>
</ol>
<p>概念总是抽象的,所以下面将以一个例子来解释上述的概念:</p>
<p>假设需要构建一款应用叫 App,它包含一个路由模块 Router 和一个页面监控模块 Track,一开始可能会这么实现:</p>
<pre><code class="language-js">// app.js
import Router from './modules/Router';
import Track from './modules/Track';
class App {
constructor(options) {
this.options = options;
this.router = new Router();
this.track = new Track();
this.init();
}
init() {
window.addEventListener('DOMContentLoaded', () => {
this.router.to('home');
this.track.tracking();
this.options.onReady();
});
}
}
// index.js
import App from 'path/to/App';
new App({
onReady() {
// do something here...
},
});
</code></pre>
<p>嗯,看起来没什么问题,但是实际应用中需求是非常多变的,可能需要给路由新增功能(比如实现 <code>history</code> 模式)或者更新配置(启用 history, new Router({ mode: 'history' }))。这就不得不在 App 内部去修改这两个模块,这是一个 <code>INNER BREAKING</code> 的操作,而对于之前测试通过了的 App 来说,也必须重新测试。</p>
<p>很明显,这不是一个好的应用结构,高层次的模块 App 依赖了两个低层次的模块 Router 和 Track,对低层次模块的修改都会影响高层次的模块 App。那么如何解决这个问题呢,解决方案就是接下来要讲述的 依赖注入(Dependency Injection)。</p>
<h2 id="依赖注入">依赖注入</h2>
<p>所谓的依赖注入,简单来说就是把高层模块所依赖的模块通过传参的方式把依赖「注入」到模块内部,上面的代码可以通过依赖注入的方式改造成如下方式:</p>
<pre><code class="language-js">// app.js
class App {
constructor(options) {
this.options = options;
this.router = options.router;
this.track = options.track;
this.init();
}
init() {
window.addEventListener('DOMContentLoaded', () => {
this.router.to('home');
this.track.tracking();
this.options.onReady();
});
}
}
// index.js
import App from 'path/to/App';
import Router from './modules/Router';
import Track from './modules/Track';
new App({
router: new Router(),
track: new Track(),
onReady() {
// do something here...
},
});
</code></pre>
<p>可以看到,通过依赖注入解决了上面所说的 <code>INNER BREAKING</code> 的问题,可以直接在 App 外部对各个模块进行修改而不影响内部。</p>
<p>是不是就万事大吉了?理想很丰满,但现实却是很骨感的,没过两天产品就给你提了一个新需求,给 App 添加一个分享模块 <code>Share</code>。这样的话又回到了上面所提到的 <code>INNER BREAKING</code> 的问题上:你不得不对 App 模块进行修改加上一行 <code>this.share = options.share</code>,这明显不是我们所期望的。</p>
<p>虽然 App 通过依赖注入的方式在一定程度上解耦了与其他几个模块的依赖关系,但是还不够彻底,其中的 this.router 和 this.track 等属性其实都还是对「具体实现」的依赖,明显违背了 IoC 思想的准则,那如何进一步抽象 App 模块呢。</p>
<pre><code class="language-js">class App {
static modules = []
constructor(options) {
this.options = options;
this.init();
}
init() {
window.addEventListener('DOMContentLoaded', () => {
this.initModules();
this.options.onReady(this);
});
}
static use(module) {
Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);
}
initModules() {
App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));
}
}
</code></pre>
<p>经过改造后 App 内已经没有「具体实现」了,看不到任何业务代码了,那么如何使用 App 来管理我们的依赖呢:</p>
<pre><code class="language-js">// modules/Router.js
import Router from 'path/to/Router';
export default {
init(app) {
app.router = new Router(app.options.router);
app.router.to('home');
}
};
// modules/Track.js
import Track from 'path/to/Track';
export default {
init(app) {
app.track = new Track(app.options.track);
app.track.tracking();
}
};
// index.js
import App from 'path/to/App';
import Router from './modules/Router';
import Track from './modules/Track';
App.use([Router, Track]);
new App({
router: {
mode: 'history',
},
track: {
// ...
},
onReady(app) {
// app.options ...
},
});
</code></pre>
<p>可以发现 App 模块在使用上也非常的方便,通过 <code>App.use()</code> 方法来「注入」依赖,在 <code>./modules/some-module.js</code> 中按照一定的「约定」去初始化相关配置,比如此时需要新增一个 <code>Share</code> 模块的话,无需到 App 内部去修改内容:</p>
<pre><code class="language-js">// modules/Share.js
import Share from 'path/to/Share';
export default {
init(app) {
app.share = new Share();
app.setShare = data => app.share.setShare(data);
}
};
// index.js
App.use(Share);
new App({
// ...
onReady(app) {
app.setShare({
title: 'Hello IoC.',
description: 'description here...',
// some other data here...
});
}
});
</code></pre>
<p>直接在 App 外部去 use 这个 Share 模块即可,对模块的注入和配置极为方便。</p>
<p>那么在 App 内部到底做了哪些工作呢,首先从 App.use 方法说起:</p>
<pre><code class="language-js">class App {
static modules = []
static use(module) {
Array.isArray(module) ? module.map(item => App.use(item)) : App.modules.push(module);
}
}
</code></pre>
<p>可以很清楚的发现,App.use 做了一件非常简单的事情,就是把依赖保存在了 App.modules 属性中,等待后续初始化模块的时候被调用。</p>
<p>接下来我们看一下模块初始化方法 <code>this.initModules()</code> 具体做了什么事情:</p>
<pre><code class="language-js">class App {
initModules() {
App.modules.map(module => module.init && typeof module.init == 'function' && module.init(this));
}
}
</code></pre>
<p>可以发现该方法同样做了一件非常简单的事情,就是遍历 App.modules 中所有的模块,判断模块是否包含 init 属性且该属性必须是一个函数,如果判断通过的话,该方法就会去执行模块的 init 方法并把 App 的实例 this 传入其中,以便在模块中引用它。</p>
<p>从这个方法中可以看出,要实现一个可以被 App.use() 的模块,就必须满足两个「约定」:</p>
<ol>
<li>模块必须包含 init 属性</li>
<li>init 必须是一个函数</li>
</ol>
<p>这其实就是 IoC 思想中对「面向接口编程 而不要面向实现编程」这一准则的很好的体现。App 不关心模块具体实现了什么,只要满足对 接口 init 的「约定」就可以了。</p>
<p>此时回看 <code>Router</code> 模块的实现,就可以很容易理解为什么要这么写了。</p>
<pre><code class="language-js">// modules/Router.js
import Router from 'path/to/Router';
export default {
init(app) {
app.router = new Router(app.options.router);
app.router.to('home');
}
};
</code></pre>
<h2 id="总结">总结</h2>
<p>App 模块此时应该称之为「容器」比较合适了,跟业务已经没有任何关系了,它仅仅只是提供了一些方法来辅助管理注入的依赖和控制模块如何执行。</p>
<p>控制反转(Inversion of Control)是一种「思想」,依赖注入(Dependency Injection)则是这一思想的一种具体「实现方式」,而这里的 App 则是辅助依赖管理的一个「容器」。</p>
<!-- https://juejin.cn/post/6844903750843236366?searchId=20240816155056A2F697E62358988CFF2D -->]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Quill 实践分享]]></title>
<id>https://jiangrubin.github.io/post/j2dJS15tF/</id>
<link href="https://jiangrubin.github.io/post/j2dJS15tF/">
</link>
<updated>2022-02-24T12:40:27.000Z</updated>
<content type="html"><![CDATA[<h2 id="前言">前言</h2>
<p>由于 Quill 在近期项目中的实践,特此写了这篇文章。从使用场景、基本原理,再到对 Quill 的扩展,跟大家分享 Quill 富文本编辑器的那些事儿。</p>
<h2 id="使用场景">使用场景</h2>
<ul>
<li>文章博客</li>
<li>聊天框</li>
<li>评论</li>
<li>...</li>
</ul>
<figure data-type="image" tabindex="1"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646029986997.png" alt="" loading="lazy"></figure>
<h2 id="基本原理">基本原理</h2>
<p>Quill 有两个核心的概念 Delta 和 Parchment Blot。</p>
<h3 id="delta">Delta</h3>
<p>Delta 是一种特定 JSON 格式的数据模型,用来描述内容的变化。所以有了 Delta 数据,就能知道编辑器的内容。可以参考 Quill 文档中 <a href="https://quilljs.com/docs/delta/">Delta 的描述</a>。</p>
<figure data-type="image" tabindex="2"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646640171736.png" alt="" loading="lazy"></figure>
<pre><code class="language-js">{
"ops": [
{ "insert": "Hello " },
{ "insert": "World", "attributes": { "bold": true } },
{ "insert": "\n" }
]
}
</code></pre>
<h3 id="parchment">Parchment</h3>
<blockquote>
<p>Parchment is Quill's document model. It is a parallel tree structure to the DOM tree, and provides functionality useful for content editors, like Quill. A Parchment tree is made up of Blots, which mirror a DOM node counterpart.</p>
</blockquote>
<p>Parchment 和 Blot 是对 DOM 的模拟。Blot 相当于 Node,它包含了很多 Quill 富文本操作需要的 API ,这些是原生 DOM API 没有的。</p>
<figure data-type="image" tabindex="3"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646644176394.png" alt="" loading="lazy"></figure>
<h2 id="扩展能力">扩展能力</h2>
<p>扩展性是编辑器的重要能力。</p>
<p>在使用编辑器时不仅仅有图文,往往还有个性化的需求,比如以下场景。</p>
<h3 id="超链接卡片">超链接卡片</h3>
<p>比如插入知乎这样的超链接卡片。</p>
<figure data-type="image" tabindex="4"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646125091924.png" alt="" loading="lazy"></figure>
<h3 id="插入表情">插入表情</h3>
<p>比如在编辑器中插入表情,类似微信的聊天框。</p>
<figure data-type="image" tabindex="5"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646126965417.png" alt="" loading="lazy"></figure>
<h3 id="提及功能">提及功能</h3>
<p>社区评论中常见的提及(@Mention)功能。</p>
<figure data-type="image" tabindex="6"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646484269913.png" alt="" loading="lazy"></figure>
<h2 id="扩展实现">扩展实现</h2>
<p>扩展 Quill 的方式:</p>
<ul>
<li>通过自定义 Blot 格式来扩展编辑器的内容。</li>
<li>通过自定义模块来扩展编辑器的功能。</li>
</ul>
<h3 id="如何插入表情">如何插入表情</h3>
<p>我们从如何插入表情入手,一起看看怎么在 Quill 中插入自定义的内容。</p>
<ul>
<li>自定义工具栏按钮</li>
<li>自定义 EmojiBlot</li>
<li>注册 EmojiBlot</li>
<li>调用 Quill 的 API 插入表情</li>
</ul>
<h4 id="自定义工具栏按钮">自定义工具栏按钮</h4>
<p>参考微信的表情选择面板,所以需要我们自定义 emoji 工具栏按钮。</p>
<pre><code class="language-js">// emoji-tool.vue
<template>
<span class="editor-tool-emoji">
<el-popover ref="popover">
<span v-for="(item, i) of emojis" :key="i" @click="onSelect(item)">
<img :src="item.src_ios" :alt="item.code_cn" />
</span>
</el-popover>
<img src="../assets/emoji.png" v-popover:popover />
</span>
</template>
<script>
// [{ "code": "[Smile]", "code_cn": "[微笑]", "src_ios": "https://emojipedia-us.s3.amazonaws.com/content/2021/02/14/emojipedia_wechat_ios_802_smile.png", "src_android": "" }]
import emojis from '../assets/wechat-emojis.json'
export default {
data () {
return {
emojis,
}
},
methods: {
onSelect (item) {
this.$emit('select', item)
this.$refs.popover.doClose()
}
}
}
</script>
</code></pre>
<p>在自定义的 toolbar 中使用 emoji 工具栏按钮。</p>
<pre><code class="language-js"><template>
<div id="editor">
<div id="editor-toolbar">
<emoji-tool />
</div>
<div id="editor-container"></div>
</div>
</template>
</code></pre>
<pre><code class="language-js">this.quill = new Quill('#editor-container', {
theme: 'snow',
modules: {
toolbar: '#editor-toolbar',
}
})
</code></pre>
<p>效果如下:</p>
<figure data-type="image" tabindex="7"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646210316370.png" alt="" loading="lazy"></figure>
<h4 id="自定义-emojiblot">自定义 EmojiBlot</h4>
<p>Quill 中的 Blot 是一个普通的 ES6 Class,由于表情和图片的差别就在于:</p>
<p>Quill 内置的图片格式不支持自定义宽高,而我们要插入的表情是需要特定的宽高的。</p>
<p>因此我们可以基于 Quill 内置的 ImageBlot 来扩展。</p>
<pre><code class="language-js">// emoji.js
import Quill from 'quill'
const ImageBlot = Quill.import('formats/image')
// 扩展 Quill 内置的 ImageBlot
export default class EmojiBlot extends ImageBlot {
static tagName = 'img' // 自定义内容的标签名
static blotName = 'emoji' // 自定义 Blot 的名字(必须全局唯一)
static className = 'emoji'
// 创建自定义内容的 DOM 节点
static create(value) {
const node = super.create(value)
node.setAttribute('src', value.src)
node.setAttribute('alt', value.alt)
node.setAttribute('style', 'vertical-align: bottom;pointer-events: none;')
if (value.width !== undefined) {
node.setAttribute('width', value.width)
}
if (value.height !== undefined) {
node.setAttribute('height', value.height)
}
return node
}
// 返回 ops 数据
static value(node) {
return {
src: node.getAttribute('src'),
alt: node.getAttribute('alt'),
width: node.getAttribute('width'),
height: node.getAttribute('height'),
}
}
}
</code></pre>
<h4 id="注册-emojiblot">注册 EmojiBlot</h4>
<p>有了 EmojiBlot,要将其插入 Quill 编辑器中,还需要将这个类注册到 Quill 中。</p>
<pre><code class="language-js">// index.vue
import Quill from 'quill'
import EmojiBlot from './formats/emoji'
Quill.register({
'formats/emoji': EmojiBlot
}, true)
</code></pre>
<h4 id="调用-quill-的-api-插入表情">调用 Quill 的 API 插入表情</h4>
<p>EmojiBlot 注册到 Quill 中之后,Quill 就能认识它了,也就可以调用 Quill 的 API 将其插入到编辑器中。</p>
<pre><code class="language-js">onSelectEmoji (data) {
const { index } = this.quill.getSelection(true)
this.quill.insertEmbed(index, 'emoji', {
width: '20px',
height: '20px',
src: data.src_ios,
alt: data.code_cn
})
this.quill.setSelection(index + 1)
}
</code></pre>
<h3 id="最终效果">最终效果</h3>
<figure data-type="image" tabindex="8"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/1646568963970.png" alt="" loading="lazy"></figure>
<p>demo 源码地址:<a href="https://github.com/jiangrubin/quill-demo">https://github.com/jiangrubin/quill-demo</a></p>
<h2 id="结语">结语</h2>
<p>最后再说一下对 Quill 使用后的感受。在开箱即用方面 Quill 做的不错,简单易用文档清晰,对一些基本场景都能够覆盖。但在个性化定制扩展方面,需要通过操作 DOM 的方式来实现,对习惯了 Vue 或 React 的开发方式,确实不太友好。</p>
<p>在写这篇文章时,发现了几款不错的富文本编辑器:</p>
<ul>
<li><a href="https://tiptap.dev/">Tiptap</a> 基于 ProseMirror 封装的,专为 vue.js 打造,设计优雅,体验流畅舒服的现代富文本编辑器。</li>
<li><a href="https://editorjs.io/">Editor.js</a> Next generation block styled editor 块样式编辑器 - 知识库内容编辑的未来。</li>
</ul>
<h2 id="资料">资料</h2>
<ul>
<li><a href="https://quilljs.com/docs/quickstart/">Quill Document</a></li>
<li><a href="https://github.com/quilljs/parchment/">Quill Parchment</a></li>
</ul>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Web 深色模式适配方案]]></title>
<id>https://jiangrubin.github.io/post/o7gyttwcg/</id>
<link href="https://jiangrubin.github.io/post/o7gyttwcg/">
</link>
<updated>2021-08-13T07:29:40.000Z</updated>
<content type="html"><![CDATA[<h2 id="前言">前言</h2>
<p>随着苹果发布 iOS 13 正式版,推出了备受期待的深色模式(Dark Mode)。因该特性在浏览时提供更好的可视性和沉浸感。支持深色模式已然成为现代移动应用和网站的一个潮流趋势。</p>
<p>那么就跟随趋势,开始进行深色模式的适配吧。</p>
<h2 id="技术方案">技术方案</h2>
<h3 id="css-媒体查询">CSS 媒体查询</h3>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/prefers-color-scheme">prefers-color-scheme</a> 是一种用于检测用户是否有将系统的主题色设置为亮色或者暗色的 CSS 媒体特性。利用其设置不同主题模式下的 CSS 样式,浏览器会自动根据当前系统主题加载对应的 CSS 样式。light 适配浅色主题,dark 适配深色主题,no-preference 表示获取不到主题时的适配方案。</p>
<pre><code class="language-css">@media (prefers-color-scheme: light) {
.content {
color: #333;
background-color: #fff;
}
}
@media (prefers-color-scheme: dark) {
.content {
color: #fff;
background-color: #292934;
}
}
</code></pre>
<p>先来看一下效果,将系统设置为浅色外观:</p>
<figure data-type="image" tabindex="1"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/7ec0b9e89a7b851ceb9d364853675f02.png" alt="" loading="lazy"></figure>
<figure data-type="image" tabindex="2"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/4f383204f22a0f6863803b19cabf23b6.png" alt="" loading="lazy"></figure>
<p>然后将系统设置为深色外观:</p>
<figure data-type="image" tabindex="3"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/e261b5862392b75b2c85bd5ccc1b329f.png" alt="" loading="lazy"></figure>
<figure data-type="image" tabindex="4"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/37722e9a926ffea5f2763683b35dc352.png" alt="" loading="lazy"></figure>
<h3 id="css-变量">CSS 变量</h3>
<p>在项目中,往往需要写大量的 CSS 样式类名,如果把样式根据不同的外观模式各写一份,其工作量可想而知。而通过将不同外观模式下的颜色定义为 CSS 变量。在外观模式切换时,只需修改颜色变量即可。</p>
<pre><code class="language-css">// 示例代码
:root {
--white: #fff;
--gray: #333;
--text-color: var(--gray);
}
@media (prefers-color-scheme: light) {
--text-color: var(--gray);
}
@media (prefers-color-scheme: dark) {
--text-color: var(--white);
}
.container {
color: var(--text-color);
}
</code></pre>
<h3 id="windowmatchmedia">Window.matchMedia</h3>
<p>浏览器提供了 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/matchMedia">window.matchMedia</a> 方法,可以用来查询指定的媒体查询字符串解析后的结果。</p>
<pre><code class="language-js">if (window.matchMedia('prefers-color-scheme: dark').matches) {
// 深色模式做什么
} else {
// 浅色模式做什么
}
</code></pre>
<p>另外还可以监听系统外观模式的状态:</p>
<pre><code class="language-js">window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
if (e.matches) {
// 系统开启深色模式后做什么
} else {
// 系统开启浅色模式后做什么
}
});
</code></pre>
<h2 id="项目实践">项目实践</h2>
<p>以 vue-cli 搭建的应用为例。</p>
<h3 id="组件库定制主题">组件库定制主题</h3>
<p>项目中大都引用第三方开源组件库,组件库一般会使用 Sass、Less 等 CSS 预处理器定义颜色变量作为组件的基础色值,可以修改基础色值来自定义主题和深色模式适配。</p>
<p>例如使用 vant 组件库,代码如下:</p>
<pre><code class="language-css">:root {
--white: #fff;
--gray-1: #4E4C56;
--gray-2: #f8f8f8;
--dark-1: #292934;
--dark-2: #23232B;
}
@media (prefers-color-scheme: light) {
--text-color: var(--gray-1);
--background-color: var(--gray-2);
--card-background-color: var(--white);
}
@media (prefers-color-scheme: dark) {
--text-color: var(--white);
--background-color: var(--dark-2);
--card-background-color: var(--dark-1);
}
// 覆盖 vant less 样式变量
@text-color: var(--text-color);
@tabbar-background-color: var(--card-background-color);
</code></pre>
<blockquote>
<p>更多关于 vant 定制主题的内容,可查阅官方<a href="https://vant-contrib.gitee.io/vant/#/zh-CN/theme">文档</a>。</p>
</blockquote>
<h3 id="图片显示">图片显示</h3>
<p>如果一张图是暗色调,在明亮模式色彩对比度强、观看流畅,但在暗黑模式下便会存在和背景色对比度弱,不方便查看。所以需要在不同模式下显示不同的图片。实现方式就是使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/picture">picture</a> 元素。</p>
<pre><code class="language-html"><picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/25423296/163456776-7f95b81a-f1ed-45f7-b7ab-8fa810d529fa.png" />
<img src="https://user-images.githubusercontent.com/25423296/163456779-a8556205-d0a5-45e2-ac17-42d089e3c3f8.png" />
</picture>
</code></pre>
<p><video controls src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/2022-05-23.14.33.49.mov" width="100%"></video></p>
<!-- 在项目中除了颜色的适配外,一些 icon 图标、svg、图片也需要根据外观模式切换而改变。那么只使用 CSS 条件规则很难实现这些需求。所以可以使用 window.matchMedia 结合 vuex,创建一个全局变量以便每个组件去使用。
```js
// App.vue
mounted () {
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
this.$store.commit('SET_THEME_MODE', e.matches ? 'dark' : 'light')
})
},
```
```js
export default new Vuex.Store({
state: {
themeMode: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
},
getters: {
themeMode (state) {
return state.themeMode
}
},
mutations: {
SET_THEME_MODE (state, mode) {
state.themeMode = mode
}
}
})
``` -->
<h3 id="用户设置">用户设置</h3>
<p>应用应该允许用户主动去选择外观模式,设置后外观模式将不再跟随系统设置。</p>
<figure data-type="image" tabindex="5"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/54fa9fb0c6c49aceae2ba923300972ff.png" alt="" loading="lazy"></figure>
<p>具体实现的核心代码如下:</p>
<pre><code class="language-js">function getThemeMode () {
return localStorage.getItem('THEME_MODE')
}
function setThemeMode (value) {
localStorage.setItem('THEME_MODE', value)
}
export default new Vuex.Store({
state: {
themeMode: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
},
getters: {
themeMode (state) {
return getThemeMode() || state.themeMode
}
},
mutations: {
SET_THEME_MODE (state, mode) {
state.themeMode = mode
}
}
})
</code></pre>
<p>在根节点上添加 theme-mode 属性,使应用初次加载时,也可渲染当前设置的外观模式效果。</p>
<pre><code class="language-js"><template>
<div id="app" :theme-mode="themeMode"></div>
</template>
<script>
export default {
computed: {
themeMode () {
return this.$store.getters.themeMode
}
},
mounted () {
window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
this.$store.commit('SET_THEME_MODE', e.matches ? 'dark' : 'light')
})
},
}
</script>
</code></pre>
<pre><code class="language-css">:root {
--white: #fff;
--gray-1: #4E4C56;
--gray-2: #f8f8f8;
--dark-1: #292934;
--dark-2: #23232B;
}
.theme-mode-light {
--text-color: var(--gray-1);
--background-color: var(--gray-2);
--card-background-color: var(--white);
}
.theme-mode-dark {
--text-color: var(--white);
--background-color: var(--dark-2);
--card-background-color: var(--dark-1);
}
#app[theme-mode=light] {
&:extend(.theme-mode-light);
}
#app[theme-mode=dark] {
&:extend(.theme-mode-dark);
}
@media (prefers-color-scheme: light) {
:root {
&:extend(.theme-mode-light);
}
}
@media (prefers-color-scheme: dark) {
:root {
&:extend(.theme-mode-dark);
}
}
</code></pre>
<h2 id="总结">总结</h2>
<p>本文介绍了深色模式适配方案和项目中会遇到的问题。如果有错误的地方或者有更好的解决想法,欢迎留言讨论。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Element Table 组件封装]]></title>
<id>https://jiangrubin.github.io/post/mOXUMW6uC/</id>
<link href="https://jiangrubin.github.io/post/mOXUMW6uC/">
</link>
<updated>2021-04-10T02:48:09.000Z</updated>
<content type="html"><![CDATA[<h2 id="前言">前言</h2>
<p>使用配置描述数据来替代 vue 模板的组件写法,基础能力完全与 <a href="https://element.eleme.cn/#/zh-CN/component/table">el-table</a> 组件保持一致。并提供了一些方便和自定义的 api,加快书写。</p>
<h2 id="代码实现">代码实现</h2>
<pre><code class="language-js"><script>
import { formatDate } from 'element-ui/lib/utils/date-util';
function isObject (obj) {
return Object.prototype.toString.call(obj) === '[object Object]';
};
function isNil (val) {
return val === undefined || val === null;
};
function getEnumValue (value, _enum) {
const valueEnum = _enum || undefined;
return valueEnum && valueEnum[value] ? valueEnum[value] : value;
};
export default {
name: 'ElProTable',
props: {
data: {
type: Array,
default: () => ([])
},
columns: {
type: Array,
default: () => ([])
},
loading: Boolean
},
mounted () {
this.mountTableMethods();
},
methods: {
// 在 this 上挂载 table 组件实例方法
mountTableMethods() {
const ElTableMethodKeys = ['clearSelection', 'toggleRowSelection', 'toggleAllSelection', 'toggleRowExpansion', 'setCurrentRow', 'clearSort', 'clearFilter', 'doLayout', 'sort'];
Object.entries(this.$refs.table).forEach(([key, item]) => {
if (ElTableMethodKeys.includes(key)) {
this[key] = item;
}
});
},
generateValue ({ prop, valueType, enum: _enum }) {
if (!prop) return null;
return (scoped) => {
const row = scoped.row || {};
let value = row[prop];
switch (valueType) {
case 'date':
value = formatDate(value);
break;
case 'dateTime':
value = formatDate(value, 'yyyy-MM-dd HH:mm:ss');
break;
case 'text':
default: value = getEnumValue(value, _enum);
};
return value;
};
},
renderColumns (h, columns) {
const $scopedSlots = this.$scopedSlots;
return columns.map((item, i) => {
if (!isObject(item)) return null;
const key = !isNil(item.prop) ? item.prop : i;
const { render, renderHeader, slot, slotHeader, columns: _columns, ...props } = item;
const scopedSlots = {
default: render ? (scoped) => render(h, scoped) : $scopedSlots[slot] || this.generateValue(item),
header: renderHeader ? (scoped) => renderHeader(h, scoped) : $scopedSlots[slotHeader] || null
};
// 通过递归处理多级表头的情况
return <el-table-column key={ key } { ...{ props } } scopedSlots={ scopedSlots }>
{ Array.isArray(_columns) ? this.renderColumns(h, _columns) : null }
</el-table-column>;
});
}
},
render (h) {
return (
<div class="el-pro-table">
<el-table
ref="table"
{ ...{ props: this.$attrs } }
{ ...{ on: this.$listeners } }
data={ this.data }
v-loading={ this.loading }
>
{ this.renderColumns(h, this.columns) }
</el-table>
</div>
);
}
};
</script>
</code></pre>
<h2 id="基本使用">基本使用</h2>
<figure data-type="image" tabindex="1"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/31992c4ad447.png" alt="" loading="lazy"></figure>
<pre><code class="language-js"><template>
<el-pro-table :data="dataSource" :columns="columns" />
</template>
<script>
export default {
data () {
return {
columns: [
{
prop: 'name',
label: 'Name'
},
{
prop: 'age',
label: 'Age'
},
{
prop: 'address',
label: 'Address'
}
],
dataSource: [
{ name: 'Yu Lou', age: 32, address: 'New York No. 1 Lake Park' },
{ name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park' },
{ name: 'Joe Black', age: 32, address: 'Sidney No. 1 Lake Park' },
]
}
}
}
</script>
</code></pre>
<h2 id="使用-valuetype">使用 valueType</h2>
<p>封装了一些常用的值类型来减少重复的 render 操作,通过在 columns 数据项中配置一个 valueType 即可展示格式化的数据。</p>
<figure data-type="image" tabindex="2"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/74950AD8-AFFA-4267-926F-ADAA1736977D.png" alt="" loading="lazy"></figure>
<pre><code class="language-js"><template>
<el-pro-table :data="dataSource" :columns="columns" />
</template>
<script>
export default {
data () {
return {
columns: [
{
prop: 'title',
label: 'Title'
},
{
prop: 'createTime',
label: 'Time',
valueType: 'dateTime'
},
{
prop: 'state',
label: 'State'
},
],
dataSource: [
{ title: 'title 1', createTime: 1616495706550, state: 'open' },
{ title: 'title 2', createTime: 1616405700550, state: 'closed' },
]
}
}
}
</script>
</code></pre>
<h2 id="使用-enum">使用 enum</h2>
<p>通过在 columns 数据项中配置 enum 对象,将状态值转为对应的描述。</p>
<figure data-type="image" tabindex="3"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/28D3F346-49B6-4592-98D2-5DC6588E081D.png" alt="" loading="lazy"></figure>
<pre><code class="language-js"><template>
<el-pro-table :data="dataSource" :columns="columns" />
</template>
<script>
export default {
data () {
return {
columns: [
{
prop: 'title',
label: 'Title'
},
{
prop: 'state',
label: 'State',
enum: {
open: '未解决',
closed: '已解决'
}
},
],
dataSource: [
{ title: 'title 1', state: 'open' },
{ title: 'title 2', state: 'closed' },
]
}
}
}
</script>
</code></pre>
<h2 id="自定义列">自定义列</h2>
<figure data-type="image" tabindex="4"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/5FD717E8-A446-42E7-A9FB-71ADB6F1E7CD.png" alt="" loading="lazy"></figure>
<h3 id="render-写法">render 写法</h3>
<p>通过给 columns 数据的项,设置一个函数 render,可以自定义渲染当前列,包括渲染自定义组件,它基于 Vue 的 Render 函数。</p>
<p>render 函数传入两个参数,第一个是 h,第二个是对象,包含 row、column 和 $index,分别指当前行数据,当前列数据,当前是第几行。</p>
<pre><code class="language-js"><template>
<el-pro-table :data="dataSource" :columns="columns" />
</template>
<script>
export default {
data () {
return {
columns: [
{
prop: 'name',
label: 'Name'
},
{
prop: 'age',
label: 'Age'
},
{
prop: 'address',
label: 'Address'
},
{
label: 'Action',
render (h, { row }) {
return (
<div>
<el-button type="text">Invite { row.name }</el-button>
<el-button type="text">Delete</el-button>
</div>
)
}
}
],
dataSource: [
{ name: 'Yu Lou', age: 32, address: 'New York No. 1 Lake Park' },
{ name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park' },
{ name: 'Joe Black', age: 32, address: 'Sidney No. 1 Lake Park' },
]
}
}
}
</script>
</code></pre>
<h3 id="slot-scope-写法">slot-scope 写法</h3>
<p>在 columns 的某列声明 slot 后,就可以在 Table 的 slot 中使用 slot-scope。</p>
<p>slot-scope 的参数有 3 个:当前行数据 row,当前列数据 column,当前行序号 $index。</p>
<pre><code class="language-js"><template>
<el-pro-table :data="dataSource" :columns="columns">
<template slot-scope="{ row }" slot="action">
<el-button type="text">Invite {{ row.name }}</el-button>
<el-button type="text">Delete</el-button>
</template>
</el-pro-table>
</template>
<script>
export default {
data () {
return {
columns: [
{
prop: 'name',
label: 'Name'
},
{
prop: 'age',
label: 'Age'
},
{
prop: 'address',
label: 'Address'
},
{
label: 'Action',
slot: 'action'
}
],
dataSource: [
{ name: 'Yu Lou', age: 32, address: 'New York No. 1 Lake Park' },
{ name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park' },
{ name: 'Joe Black', age: 32, address: 'Sidney No. 1 Lake Park' },
]
}
}
}
</script>
</code></pre>
<h2 id="自定义表头">自定义表头</h2>
<figure data-type="image" tabindex="5"><img src="https://fastly.jsdelivr.net/gh/jiangrubin/image-host@master/24847B3C-8FCC-4080-958C-7CAD1291A155.png" alt="" loading="lazy"></figure>
<h3 id="render-写法-2">render 写法</h3>
<p>通过给 columns 数据的项,设置一个函数 renderHeader,可以自定义渲染当前列表头,包括渲染自定义组件,它基于 Vue 的 Render 函数。</p>
<p>renderHeader 函数传入两个参数,第一个是 h,第二个是对象,包含 column 和 $index,分别指当前列数据,当前是第几行。</p>
<pre><code class="language-js"><template>
<el-pro-table :data="tableData" :columns="columns" />
</template>
<script>
export default {
data () {
return {
search: '',
columns: [
{
prop: 'date',
label: 'Date'
},
{
prop: 'name',
label: 'Name'
},
{
align: 'right',
renderHeader: (h, scoped) => {
return <el-input value={ this.search } on-input={ this.onInput } size="small" placeholder="Search" />
},
render (h) {
return <el-button size="small">Edit</el-button>
}
}
],
dataSource: [
{ date: '2016-05-03', name: 'Yu Lou' },
{ date: '2016-05-02', name: 'Jim Green' },
{ date: '2016-05-04', name: 'Joe Black' }
]
}
},
computed: {
tableData () {