-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path聊聊毕业设计系列2.html
1240 lines (859 loc) · 104 KB
/
聊聊毕业设计系列2.html
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
<!DOCTYPE html>
<html style="display: none;" lang="zh">
<head>
<meta charset="utf-8">
<!--
© Material Theme
https://github.com/viosey/hexo-theme-material
Version: 1.5.2 -->
<script>
window.materialVersion = "1.5.2"
// Delete localstorage with these tags
window.oldVersion = [
'codestartv1',
'1.3.4',
'1.4.0',
'1.4.0b1',
'1.5.0'
]
</script>
<!-- dns prefetch -->
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="https://cdn1.lncld.net"/>
<!-- Meta & Info -->
<meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<!-- Title -->
<title>
聊聊毕业设计系列 --- 系统实现 |
黄明照--一个在路上慢慢行走的前端人
</title>
<!-- Favicons -->
<link rel="icon shortcut" type="image/ico" href="/img/favicon.png">
<link rel="icon" href="/img/favicon.png">
<meta name="format-detection" content="telephone=no"/>
<meta name="description" itemprop="description" content="黄明照、黄明照的个人博客、黄明照的个人网站、一个在路上慢慢行走的前端人">
<meta name="keywords" content="黄明照、黄明照的个人博客、黄明照的个人网站、一个在路上慢慢行走的前端人,Mongoose,Express,毕业设计,Vue,MongoDB,Node.js,Sockit.io">
<meta name="theme-color" content="#0097A7">
<!-- Disable Fucking Bloody Baidu Tranformation -->
<meta http-equiv="Cache-Control" content="no-transform" />
<meta http-equiv="Cache-Control" content="no-siteapp" />
<!--[if lte IE 9]>
<link rel="stylesheet" href="/css/ie-blocker.css">
<script src="/js/ie-blocker.zhCN.js"></script>
<![endif]-->
<!-- Import lsloader -->
<script>(function(){window.lsloader={jsRunSequence:[],jsnamemap:{},cssnamemap:{}};lsloader.removeLS=function(a){try{localStorage.removeItem(a)}catch(b){}};lsloader.setLS=function(a,c){try{localStorage.setItem(a,c)}catch(b){}};lsloader.getLS=function(a){var c="";try{c=localStorage.getItem(a)}catch(b){c=""}return c};versionString="/*"+(window.materialVersion||"unknownVersion")+"*/";lsloader.clean=function(){try{var b=[];for(var a=0;a<localStorage.length;a++){b.push(localStorage.key(a))}b.forEach(function(e){var f=lsloader.getLS(e);if(window.oldVersion){var d=window.oldVersion.reduce(function(g,h){return g||f.indexOf("/*"+h+"*/")!==-1},false);if(d){lsloader.removeLS(e)}}})}catch(c){}};lsloader.clean();lsloader.load=function(f,a,b,d){if(typeof b==="boolean"){d=b;b=undefined}d=d||false;b=b||function(){};var e;e=this.getLS(f);if(e&&e.indexOf(versionString)===-1){this.removeLS(f);this.requestResource(f,a,b,d);return}if(e){var c=e.split(versionString)[0];if(c!=a){console.log("reload:"+a);this.removeLS(f);this.requestResource(f,a,b,d);return}e=e.split(versionString)[1];if(d){this.jsRunSequence.push({name:f,code:e});this.runjs(a,f,e)}else{document.getElementById(f).appendChild(document.createTextNode(e));b()}}else{this.requestResource(f,a,b,d)}};lsloader.requestResource=function(b,e,a,c){var d=this;if(c){this.iojs(e,b,function(h,f,g){d.setLS(f,h+versionString+g);d.runjs(h,f,g)})}else{this.iocss(e,b,function(f){document.getElementById(b).appendChild(document.createTextNode(f));d.setLS(b,e+versionString+f)},a)}};lsloader.iojs=function(d,b,g){var a=this;a.jsRunSequence.push({name:b,code:""});try{var f=new XMLHttpRequest();f.open("get",d,true);f.onreadystatechange=function(){if(f.readyState==4){if((f.status>=200&&f.status<300)||f.status==304){if(f.response!=""){g(d,b,f.response);return}}a.jsfallback(d,b)}};f.send(null)}catch(c){a.jsfallback(d,b)}};lsloader.iocss=function(f,c,h,a){var b=this;try{var g=new XMLHttpRequest();g.open("get",f,true);g.onreadystatechange=function(){if(g.readyState==4){if((g.status>=200&&g.status<300)||g.status==304){if(g.response!=""){h(g.response);a();return}}b.cssfallback(f,c,a)}};g.send(null)}catch(d){b.cssfallback(f,c,a)}};lsloader.iofonts=function(f,c,h,a){var b=this;try{var g=new XMLHttpRequest();g.open("get",f,true);g.onreadystatechange=function(){if(g.readyState==4){if((g.status>=200&&g.status<300)||g.status==304){if(g.response!=""){h(g.response);a();return}}b.cssfallback(f,c,a)}};g.send(null)}catch(d){b.cssfallback(f,c,a)}};lsloader.runjs=function(f,c,e){if(!!c&&!!e){for(var b in this.jsRunSequence){if(this.jsRunSequence[b].name==c){this.jsRunSequence[b].code=e}}}if(!!this.jsRunSequence[0]&&!!this.jsRunSequence[0].code&&this.jsRunSequence[0].status!="failed"){var a=document.createElement("script");a.appendChild(document.createTextNode(this.jsRunSequence[0].code));a.type="text/javascript";document.getElementsByTagName("head")[0].appendChild(a);this.jsRunSequence.shift();if(this.jsRunSequence.length>0){this.runjs()}}else{if(!!this.jsRunSequence[0]&&this.jsRunSequence[0].status=="failed"){var d=this;var a=document.createElement("script");a.src=this.jsRunSequence[0].path;a.type="text/javascript";this.jsRunSequence[0].status="loading";a.onload=function(){d.jsRunSequence.shift();if(d.jsRunSequence.length>0){d.runjs()}};document.body.appendChild(a)}}};lsloader.tagLoad=function(b,a){this.jsRunSequence.push({name:a,code:"",path:b,status:"failed"});this.runjs()};lsloader.jsfallback=function(c,b){if(!!this.jsnamemap[b]){return}else{this.jsnamemap[b]=b}for(var a in this.jsRunSequence){if(this.jsRunSequence[a].name==b){this.jsRunSequence[a].code="";this.jsRunSequence[a].status="failed";this.jsRunSequence[a].path=c}}this.runjs()};lsloader.cssfallback=function(e,c,b){if(!!this.cssnamemap[c]){return}else{this.cssnamemap[c]=1}var d=document.createElement("link");d.type="text/css";d.href=e;d.rel="stylesheet";d.onload=d.onerror=b;var a=document.getElementsByTagName("script")[0];a.parentNode.insertBefore(d,a)};lsloader.runInlineScript=function(c,b){var a=document.getElementById(b).innerText;this.jsRunSequence.push({name:c,code:a});this.runjs()}})();</script>
<!-- Import queue -->
<script>function Queue(){this.dataStore=[];this.offer=b;this.poll=d;this.execNext=a;this.debug=false;this.startDebug=c;function b(e){if(this.debug){console.log("Offered a Queued Function.")}if(typeof e==="function"){this.dataStore.push(e)}else{console.log("You must offer a function.")}}function d(){if(this.debug){console.log("Polled a Queued Function.")}return this.dataStore.shift()}function a(){var e=this.poll();if(e!==undefined){if(this.debug){console.log("Run a Queued Function.")}e()}}function c(){this.debug=true}}var queue=new Queue();</script>
<!-- Import CSS -->
<style id="material_css"></style><script>if(typeof window.lsLoadCSSMaxNums === "undefined")window.lsLoadCSSMaxNums = 0;window.lsLoadCSSMaxNums++;lsloader.load("material_css","/css/material.min.css?Z7a72R1E4SxzBKR/WGctOA==",function(){if(typeof window.lsLoadCSSNums === "undefined")window.lsLoadCSSNums = 0;window.lsLoadCSSNums++;if(window.lsLoadCSSNums == window.lsLoadCSSMaxNums)document.documentElement.style.display="";}, false)</script>
<style id="style_css"></style><script>if(typeof window.lsLoadCSSMaxNums === "undefined")window.lsLoadCSSMaxNums = 0;window.lsLoadCSSMaxNums++;lsloader.load("style_css","/css/style.min.css?MKetZV3cUTfDxvMffaOezg==",function(){if(typeof window.lsLoadCSSNums === "undefined")window.lsLoadCSSNums = 0;window.lsLoadCSSNums++;if(window.lsLoadCSSNums == window.lsLoadCSSMaxNums)document.documentElement.style.display="";}, false)</script>
<!-- Config CSS -->
<!-- Other Styles -->
<style>
body, html {
font-family: Roboto, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
overflow-x: hidden !important;
}
code {
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
}
a {
color: #00838F;
}
.mdl-card__media,
#search-label,
#search-form-label:after,
#scheme-Paradox .hot_tags-count,
#scheme-Paradox .sidebar_archives-count,
#scheme-Paradox .sidebar-colored .sidebar-header,
#scheme-Paradox .sidebar-colored .sidebar-badge{
background-color: #0097A7 !important;
}
/* Sidebar User Drop Down Menu Text Color */
#scheme-Paradox .sidebar-colored .sidebar-nav>.dropdown>.dropdown-menu>li>a:hover,
#scheme-Paradox .sidebar-colored .sidebar-nav>.dropdown>.dropdown-menu>li>a:focus {
color: #0097A7 !important;
}
#post_entry-right-info,
.sidebar-colored .sidebar-nav li:hover > a,
.sidebar-colored .sidebar-nav li:hover > a i,
.sidebar-colored .sidebar-nav li > a:hover,
.sidebar-colored .sidebar-nav li > a:hover i,
.sidebar-colored .sidebar-nav li > a:focus i,
.sidebar-colored .sidebar-nav > .open > a,
.sidebar-colored .sidebar-nav > .open > a:hover,
.sidebar-colored .sidebar-nav > .open > a:focus,
#ds-reset #ds-ctx .ds-ctx-entry .ds-ctx-head a {
color: #0097A7 !important;
}
.toTop {
background: #757575 !important;
}
.material-layout .material-post>.material-nav,
.material-layout .material-index>.material-nav,
.material-nav a {
color: #757575;
}
#scheme-Paradox .MD-burger-layer {
background-color: #757575;
}
#scheme-Paradox #post-toc-trigger-btn {
color: #757575;
}
.post-toc a:hover {
color: #00838F;
text-decoration: underline;
}
</style>
<!-- Theme Background Related-->
<style>
body{
background-color: #fff;
}
/* blog_info bottom background */
#scheme-Paradox .material-layout .something-else .mdl-card__supporting-text{
background-color: #fff;
}
</style>
<!-- Fade Effect -->
<style>
.fade {
transition: all 800ms linear;
-webkit-transform: translate3d(0,0,0);
-moz-transform: translate3d(0,0,0);
-ms-transform: translate3d(0,0,0);
-o-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
opacity: 1;
}
.fade.out{
opacity: 0;
}
</style>
<!-- Import Font -->
<!-- Import Roboto -->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
<!-- Import Material Icons -->
<style id="material_icons"></style><script>if(typeof window.lsLoadCSSMaxNums === "undefined")window.lsLoadCSSMaxNums = 0;window.lsLoadCSSMaxNums++;lsloader.load("material_icons","/css/material-icons.css?pqhB/Rd/ab0H2+kZp0RDmw==",function(){if(typeof window.lsLoadCSSNums === "undefined")window.lsLoadCSSNums = 0;window.lsLoadCSSNums++;if(window.lsLoadCSSNums == window.lsLoadCSSMaxNums)document.documentElement.style.display="";}, false)</script>
<!-- Import jQuery -->
<script>lsloader.load("jq_js","/js/jquery.min.js?qcusAULNeBksqffqUM2+Ig==", true)</script>
<!-- WebAPP Icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="黄明照--一个在路上慢慢行走的前端人">
<meta name="msapplication-starturl" content="http://www.huangmingzhao.cn/聊聊毕业设计系列2.html">
<meta name="msapplication-navbutton-color" content="#0097A7">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="黄明照--一个在路上慢慢行走的前端人">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="apple-touch-icon" href="/img/favicon.png">
<!-- Site Verification -->
<!-- RSS -->
<!-- The Open Graph protocol -->
<meta property="og:url" content="http://www.huangmingzhao.cn/聊聊毕业设计系列2.html">
<meta property="og:type" content="blog">
<meta property="og:title" content="聊聊毕业设计系列 --- 系统实现 | 黄明照--一个在路上慢慢行走的前端人">
<meta property="og:image" content="/img/favicon.png">
<meta property="og:description" content="黄明照、黄明照的个人博客、黄明照的个人网站、一个在路上慢慢行走的前端人">
<meta property="og:article:tag" content="Mongoose"> <meta property="og:article:tag" content="Express"> <meta property="og:article:tag" content="毕业设计"> <meta property="og:article:tag" content="Vue"> <meta property="og:article:tag" content="MongoDB"> <meta property="og:article:tag" content="Node.js"> <meta property="og:article:tag" content="Sockit.io">
<meta property="article:published_time" content="Sun Aug 26 2018 11:53:01 GMT+0800">
<meta property="article:modified_time" content="Sun Aug 26 2018 23:53:17 GMT+0800">
<!-- The Twitter Card protocol -->
<meta name="twitter:card" content="summary_large_image">
<!-- Add canonical link for SEO -->
<link rel="canonical" href="http://www.huangmingzhao.cn/聊聊毕业设计系列2.html" />
<!-- Structured-data for SEO -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"mainEntityOfPage": "http://www.huangmingzhao.cn/聊聊毕业设计系列2.html",
"headline": "聊聊毕业设计系列 --- 系统实现",
"datePublished": "Sun Aug 26 2018 11:53:01 GMT+0800",
"dateModified": "Sun Aug 26 2018 23:53:17 GMT+0800",
"author": {
"@type": "Person",
"name": "黄明照",
"image": {
"@type": "ImageObject",
"url": "/img/avatar1.jpg"
},
"description": "Be A Hero To Myself"
},
"publisher": {
"@type": "Organization",
"name": "黄明照--一个在路上慢慢行走的前端人",
"logo": {
"@type":"ImageObject",
"url": "/img/favicon.png"
}
},
"keywords": ",Mongoose,Express,毕业设计,Vue,MongoDB,Node.js,Sockit.io黄明照、黄明照的个人博客、黄明照的个人网站、一个在路上慢慢行走的前端人",
"description": "黄明照、黄明照的个人博客、黄明照的个人网站、一个在路上慢慢行走的前端人",
}
</script>
<!-- Analytics -->
<script>
var _hmt = _hmt || [];
(function() {var hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?16de35e801aa6d41f4001b2445157db7';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
<!-- Custom Head -->
</head>
<body id="scheme-Paradox" class="lazy">
<div class="material-layout mdl-js-layout has-drawer is-upgraded">
<!-- Main Container -->
<main class="material-layout__content" id="main">
<!-- Top Anchor -->
<div id="top"></div>
<!-- Hamburger Button -->
<button class="MD-burger-icon sidebar-toggle">
<span class="MD-burger-layer"></span>
</button>
<!-- Post TOC -->
<!-- Back Button -->
<!--
<div class="material-back" id="backhome-div" tabindex="0">
<a class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon"
href="#" onclick="window.history.back();return false;"
target="_self"
role="button"
data-upgraded=",MaterialButton,MaterialRipple">
<i class="material-icons" role="presentation">arrow_back</i>
<span class="mdl-button__ripple-container">
<span class="mdl-ripple"></span>
</span>
</a>
</div>
-->
<!-- Left aligned menu below button -->
<button id="post-toc-trigger-btn"
class="mdl-button mdl-js-button mdl-button--icon">
<i class="material-icons">format_list_numbered</i>
</button>
<ul class="post-toc-wrap mdl-menu mdl-menu--bottom-left mdl-js-menu mdl-js-ripple-effect" for="post-toc-trigger-btn" style="max-height:80vh; overflow-y:scroll;">
<ol class="post-toc"><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#效果展示"><span class="post-toc-number">1.</span> <span class="post-toc-text">效果展示</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#github"><span class="post-toc-number">2.</span> <span class="post-toc-text">github</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#前言"><span class="post-toc-number">3.</span> <span class="post-toc-text">前言</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#MongoDB"><span class="post-toc-number">4.</span> <span class="post-toc-text">MongoDB</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#用户身份认证实现"><span class="post-toc-number">5.</span> <span class="post-toc-text">用户身份认证实现</span></a><ol class="post-toc-child"><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#介绍"><span class="post-toc-number">5.1.</span> <span class="post-toc-text">介绍</span></a></li><li class="post-toc-item post-toc-level-2"><a class="post-toc-link" href="#实现思路"><span class="post-toc-number">5.2.</span> <span class="post-toc-text">实现思路</span></a></li></ol></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#七牛云接入"><span class="post-toc-number">6.</span> <span class="post-toc-text">七牛云接入</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#路由权限模块"><span class="post-toc-number">7.</span> <span class="post-toc-text">路由权限模块</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#账号模块"><span class="post-toc-number">8.</span> <span class="post-toc-text">账号模块</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#实时消息推送"><span class="post-toc-number">9.</span> <span class="post-toc-text">实时消息推送</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#评论模块"><span class="post-toc-number">10.</span> <span class="post-toc-text">评论模块</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#一些思考"><span class="post-toc-number">11.</span> <span class="post-toc-text">一些思考</span></a></li><li class="post-toc-item post-toc-level-1"><a class="post-toc-link" href="#总结"><span class="post-toc-number">12.</span> <span class="post-toc-text">总结</span></a></li></ol>
</ul>
<!-- Layouts -->
<!-- Post Module -->
<div class="material-post_container">
<div class="material-post mdl-grid">
<div class="mdl-card mdl-shadow--4dp mdl-cell mdl-cell--12-col">
<!-- Post Header(Thumbnail & Title) -->
<!-- Paradox Post Header -->
<!-- Random Thumbnail -->
<div class="post_thumbnail-random mdl-card__media mdl-color-text--grey-50">
<script type="text/ls-javascript" id="post-thumbnail-script">
var randomNum = Math.floor(Math.random() * 19 + 1);
$('.post_thumbnail-random').attr('data-original', '/img/random/material-' + randomNum + '.png');
$('.post_thumbnail-random').addClass('lazy');
</script>
<p class="article-headline-p">
聊聊毕业设计系列 --- 系统实现
</p>
</div>
<!-- Paradox Post Info -->
<div class="mdl-color-text--grey-700 mdl-card__supporting-text meta">
<!-- Author Avatar -->
<div id="author-avatar">
<img src="/img/avatar1.jpg" width="44px" height="44px" alt="Author Avatar"/>
</div>
<!-- Author Name & Date -->
<div>
<strong>黄明照</strong>
<span>8月 26, 2018</span>
</div>
<div class="section-spacer"></div>
<!-- Favorite -->
<!--
<button id="article-functions-like-button" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon btn-like">
<i class="material-icons" role="presentation">favorite</i>
<span class="visuallyhidden">favorites</span>
</button>
-->
<!-- Qrcode -->
<!-- Tags (bookmark) -->
<button id="article-functions-viewtags-button" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon">
<i class="material-icons" role="presentation">bookmark</i>
<span class="visuallyhidden">bookmark</span>
</button>
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="article-functions-viewtags-button">
<li class="mdl-menu__item">
<a class="post_tag-link" href="/tags/Express/">Express</a></li><li class="mdl-menu__item"><a class="post_tag-link" href="/tags/MongoDB/">MongoDB</a></li><li class="mdl-menu__item"><a class="post_tag-link" href="/tags/Mongoose/">Mongoose</a></li><li class="mdl-menu__item"><a class="post_tag-link" href="/tags/Node-js/">Node.js</a></li><li class="mdl-menu__item"><a class="post_tag-link" href="/tags/Sockit-io/">Sockit.io</a></li><li class="mdl-menu__item"><a class="post_tag-link" href="/tags/Vue/">Vue</a></li><li class="mdl-menu__item"><a class="post_tag-link" href="/tags/毕业设计/">毕业设计</a>
</ul>
<!-- Share -->
<button id="article-fuctions-share-button" class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon">
<i class="material-icons" role="presentation">share</i>
<span class="visuallyhidden">share</span>
</button>
<ul class="mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="article-fuctions-share-button">
<!-- Leancloud Views -->
<a class="post_share-link" href="#">
<li class="mdl-menu__item">
<span id="/聊聊毕业设计系列2.html" class="leancloud-views_num" data-flag-title="聊聊毕业设计系列 --- 系统实现">
浏览量
</span>
</li>
</a>
<!-- Share Weibo -->
<a class="post_share-link" href="http://service.weibo.com/share/share.php?appkey=&title=聊聊毕业设计系列 --- 系统实现&url=http://www.huangmingzhao.cn/聊聊毕业设计系列2.html&pic=http://www.huangmingzhao.cn/img/favicon.png&searchPic=false&style=simple" target="_blank">
<li class="mdl-menu__item">
分享到微博
</li>
</a>
<!-- Share Twitter -->
<a class="post_share-link" href="https://twitter.com/intent/tweet?text=聊聊毕业设计系列 --- 系统实现&url=http://www.huangmingzhao.cn/聊聊毕业设计系列2.html&via=黄明照" target="_blank">
<li class="mdl-menu__item">
分享到 Twitter
</li>
</a>
<!-- Share Facebook -->
<a class="post_share-link" href="https://www.facebook.com/sharer/sharer.php?u=http://www.huangmingzhao.cn/聊聊毕业设计系列2.html" target="_blank">
<li class="mdl-menu__item">
分享到 Facebook
</li>
</a>
<!-- Share Google+ -->
<a class="post_share-link" href="https://plus.google.com/share?url=http://www.huangmingzhao.cn/聊聊毕业设计系列2.html" target="_blank">
<li class="mdl-menu__item">
分享到 Google+
</li>
</a>
<!-- Share LinkedIn -->
<!-- Share QQ -->
<a class="post_share-link" href="http://connect.qq.com/widget/shareqq/index.html?site=黄明照--一个在路上慢慢行走的前端人&title=聊聊毕业设计系列 --- 系统实现&summary=黄明照、黄明照的个人博客、黄明照的个人网站、一个在路上慢慢行走的前端人&pics=http://www.huangmingzhao.cn/img/favicon.png&url=http://www.huangmingzhao.cn/聊聊毕业设计系列2.html" target="_blank">
<li class="mdl-menu__item">
分享到 QQ
</li>
</a>
<!-- Share Telegram -->
</ul>
</div>
<!-- Post Content -->
<div id="post-content" class="mdl-color-text--grey-700 mdl-card__supporting-text fade out">
<h1 id="效果展示"><a href="#效果展示" class="headerlink" title="效果展示"></a>效果展示</h1><p><img src="https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195" alt="管理系统"></p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/26/16576a8711c14cbc?w=341&h=569&f=gif&s=48073144" alt="WebApp"></p>
<h1 id="github"><a href="#github" class="headerlink" title="github"></a>github</h1><p><a href="https://github.com/ishareme/moment-server" target="_blank" rel="noopener">moment-server github地址</a></p>
<p><a href="https://github.com/ishareme/moment" target="_blank" rel="noopener">moment github地址</a></p>
<p><a href="https://github.com/ishareme/moment-manage" target="_blank" rel="noopener">moment-manage github地址</a></p>
<h1 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h1><p>在上一篇文章中,主要是对项目做了介绍,并且对系统分析和系统设计做了大概的介绍。那么接下来这篇文章会对系统的实现做介绍,主要是选择一些比较主要的模块或者说可拿出来与大家分享的模块。好了,接入正题吧~~</p>
<h1 id="MongoDB"><a href="#MongoDB" class="headerlink" title="MongoDB"></a>MongoDB</h1><p>服务端这边使用的是Express框架,数据库使用的是MongoDB,通过Mongoose模块来操作数据库。这边主要是想下对MongoDB做个介绍,当然看官了解的话直接往下划<del>~</del>~</p>
<p>在项目开始前要确保电脑是否安装mongoDB,<a href="https://www.mongodb.com/download-center?jmp=nav#atlas" target="_blank" rel="noopener">下载点我</a>,<a href="https://robomongo.org/download" target="_blank" rel="noopener">图像化工具Robo 3T 点我</a>,下载好具体怎么配置还请问度娘或Google吧,本文不做介绍了哈。注意:安装完mongoDB的时候进行项目时要把lib目录下的mongod服务器打开哈~~</p>
<p>MongoDB 是一个基于分布式文件存储的数据库,是一个介于关系型数据库和非关系型数据库之间的开源产品,它是功能最为丰富的非关系型数据库,也是最像关系型数据库的。但是和关系型数据库不同,MongoDB没有表和行的概念,而是一个面向<code>集合、文档</code>的数据库。其中的文档是一个键值对,采用BSON(Binary Serialized Document Format),BSON是一种类似于JSON的二进制形式的存储格式,并且BSON具有表示数据类型的扩展,因此支持的数据非常丰富。MongoDB有两个很重要的数据类型就是<code>内嵌文档和数组</code>,而且在数组内可以嵌入其他文档,这样一条记录就能表示非常复杂的关系。</p>
<p>Mongoose是在node.js异步环境下对MongoDB进行简便操作的对象模型工具,能从数据库提取任何信息,可以用面向对象的方法来读写数据,从而使操作MongoDB数据库非常便捷。Mongoose中有三个非常重要的概念,便是Schema(模式),Model(模型),Entity(实体)。</p>
<ol>
<li><p>Schema: 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力,创建它的过程如同关系型数据库建表的过程,如下:</p>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">//Schema</span><br><span class="line">const mongoose = require('mongoose');</span><br><span class="line">const Schema = mongoose.Schema;</span><br><span class="line"></span><br><span class="line">const UserSchema = new Schema({</span><br><span class="line"> token: String,</span><br><span class="line"> is_banned: {type: Boolean, default: false}, //是否禁言</span><br><span class="line"> enable: { type: Boolean, default: true }, //用户是否有效</span><br><span class="line"> is_actived: {type: Boolean, default: false}, //邮件激活</span><br><span class="line"> username: String,</span><br><span class="line"> password: String,</span><br><span class="line"> email: String, //email唯一性</span><br><span class="line"> code: String,</span><br><span class="line"> email_time: {type: Date},</span><br><span class="line"> phone: {type: String},</span><br><span class="line"> description: { type: String, default: "这个人很懒,什么都没有留下..." },</span><br><span class="line"> avatar: { type: String, default: "http://p89inamdb.bkt.clouddn.com/default_avatar.png" },</span><br><span class="line"> bg_url: { type: String, default: "http://p89inamdb.bkt.clouddn.com/FkagpurBWZjB98lDrpSrCL8zeaTU"},</span><br><span class="line"> ip: String,</span><br><span class="line"> ip_location: { type: Object },</span><br><span class="line"> agent: { type: String }, // 用户ua</span><br><span class="line"> last_login_time: { type: Date },</span><br><span class="line"> .....</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
</li>
<li><p>Model: 由Schema发布生成的模型,具有抽象属性和行为的数据库操作对象</p>
</li>
</ol>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">//生成一个具体User的model并导出</span><br><span class="line">const User = mongoose.model("User", UserSchema); //第一个参数是集合名,在数据库中会把Model名字字母全部变小写和在后面加复数s</span><br><span class="line"></span><br><span class="line">//执行到这个时候你的数据库中就有了 users 这个集合</span><br><span class="line"></span><br><span class="line">module.exports = User;</span><br></pre></td></tr></table></figure>
<ol>
<li>Entity: 由Model创建的实体,他的操作也会影响数据库,但是它操作数据库的能力比Model弱<figure class="highlight plain"><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><span class="line">const newUser = new UserModel({ //UserModel 为导出来的 User</span><br><span class="line"> email: req.body.email,</span><br><span class="line"> code: getCode(),</span><br><span class="line"> email_time: Date.now()</span><br><span class="line"> });</span><br></pre></td></tr></table></figure>
</li>
</ol>
<p>Mongoose中有一个东西个人感觉非常主要,那便是<code>populate</code>,通过populate他可以很方便的与另一个集合建立关系。如下,user集合可以与article集合、user集合本身进行关联,根据其内嵌文档的特性,这样子他便可以内嵌子文档,子文档中有可以内嵌子文档,这样子它返回的数据就会异常的丰富。<br><figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">const user = await UserModel.findOne({_id: req.query._id, is_actived: true}, {password: 0}).populate({</span><br><span class="line"> path: 'image_article',</span><br><span class="line"> model: 'ImageArticle',</span><br><span class="line"> populate: {</span><br><span class="line"> path: 'author',</span><br><span class="line"> model: 'User'</span><br><span class="line"> }</span><br><span class="line"> }).populate({</span><br><span class="line"> path: 'collection_film_article',</span><br><span class="line"> model: 'FilmArticle',</span><br><span class="line"> }).populate({</span><br><span class="line"> path: 'following_user',</span><br><span class="line"> model: 'User',</span><br><span class="line"> }).populate({</span><br><span class="line"> path: 'follower_user',</span><br><span class="line"> model: 'User',</span><br><span class="line"> }).exec();</span><br></pre></td></tr></table></figure></p>
<p>服务端主要是操作数据库,对数据库进行增删改查(CRUD)等操作。项目中的接口,Mongoose的各种方法这边就不对其做详细介绍,大家可以查看<a href="https://mongoosejs.com/docs/guide.html" target="_blank" rel="noopener">Mongoose文档</a>。</p>
<h1 id="用户身份认证实现"><a href="#用户身份认证实现" class="headerlink" title="用户身份认证实现"></a>用户身份认证实现</h1><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>本系统的用户身份认证机制采用的是<code>JSON Web Token(JWT)</code>,它是一种轻量的认证规范,也用于接口的认证。我们知道,HTTP协议是一种无状态的协议,这便意味着每个请求都是独立的,当用户提供了用户名和密码来对我们的应用进行用户认证,那么在下一次请求的时候,用户需要再进行一次用户的认证才可以,因为根据HTTP协议,我们并不能知道是哪个用户发出的请求,本系统采用了token的鉴权机制。这个token必须要在每次请求时传递给服务端,它应该保存在请求头里,另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。</p>
<p>在用户身份认证这一块有很多方法,最常见的像cookie ,session。那么他们三之间又有什么区别,这里有两篇文章介绍的挺全面。</p>
<ul>
<li><a href="http://www.52im.net/thread-1525-1-1.html" target="_blank" rel="noopener">正确理解HTTP短连接中的Cookie、Session和Token</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/38227861" target="_blank" rel="noopener">小白必读:闲话HTTP短连接中的Session和Token</a></li>
</ul>
<p>token 与 session的区别在于,它不同于传统的session认证机制,它不需要在服务端去保留用户的认证信息或其会话的信息。系统一旦比较大,都会采用机器集群来做负载均衡,这需要多台机器,由于session是保存在服务端,那么就要 去考虑用户到底是在哪一台服务器上进行登录的,这便是一个很大的负担。</p>
<p>那么就有人想问了,你这个系统这么小,为什么不使用传统的session机制呢?哈~因为之前自己的项目一般都是使用session做登录,没使用过token,想尝试尝试入入坑~~哈哈哈~</p>
<h2 id="实现思路"><a href="#实现思路" class="headerlink" title="实现思路"></a>实现思路</h2><p>JWT主要的实现思路如下:</p>
<ol>
<li><p>在用户登录成功的时候创建token保存于数据库中,并返回给客户端。</p>
</li>
<li><p>客户端之后的每一次请求都要带上token,在请求头里加入Authorization,并加上token.</p>
</li>
<li><p>在服务端进行验证token的有效性,在有效期内返回200状态码,token过期则返回401状态码</p>
</li>
</ol>
<p>如下图所示: </p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/19/16551e5acf4fb44e?w=685&h=563&f=png&s=59915" alt="JWT请求图"></p>
<center>JWT请求图</center>
<p>在node中主要用了<code>jsonwebtoken</code>这个模块来创建JWT,jsonwebtoken的使用请查看<a href="https://segmentfault.com/a/1190000009494020" target="_blank" rel="noopener">jsonwebtoken文档</a>。项目中创建token的中间件createToken如下</p>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * createToken.js</span><br><span class="line"> */</span><br><span class="line">const jwt = require('jsonwebtoken'); // 引入jsonwebtoken模块</span><br><span class="line">const secret = '我是密钥'</span><br><span class="line"></span><br><span class="line">//登录时:核对用户名和密码成功后,应用将用户的id(user_id)作为JWT Payload的一个属性</span><br><span class="line">module.exports = function(user_id){</span><br><span class="line"> const token = jwt.sign({</span><br><span class="line"> user_id: user_id</span><br><span class="line"> }, secret, { //密钥</span><br><span class="line"> expiresIn: '24h' //过期时间设置为24h。那么decode这个token的时候得到的过期时间为:创建token的时间+设置的值</span><br><span class="line"> });</span><br><span class="line"> return token;</span><br><span class="line">};</span><br></pre></td></tr></table></figure>
<p>return 出来的 token 类似<code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0.Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM</code>。我们仔细看这字符串,分为三段,分别被 “.” 隔开。现在我们分别对前两段进行base64解码如下:<br><figure class="highlight plain"><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><span class="line">eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ===> {"alg":"HS256","typ":"JWT"} 其中 alg是加密算法名字,typ是类型</span><br><span class="line"></span><br><span class="line">eyJ1c2VyX2lkIjoiYWRtaW4iLCJpYXQiOjE1MzQ2ODQwNzAsImV4cCI6MTUzNDc3MDQ3MH0 ===> {"user_id":"admin","iat":1534684070,"exp":1534770470} 其中 name是我们储存的内容,iat创建的时间戳,exp到期时间戳。</span><br><span class="line"></span><br><span class="line">Y3kaglqW9Fpe1YxF_uF7zwTV224W4W97MArU0aI0JgM ===> 最后一段是由前面两段字符串,HS256加密后得到。所以前面的任何一个字段修改,都会导致加密后的字符串不匹配。</span><br></pre></td></tr></table></figure></p>
<p>当我们根据用户的id创建获取到token之后,我们需要把token返回到客户端,客户端对其在本地(localStorage)保存, 客户端之后的每一次请求都要带上token,在请求头里加入Authorization,并加上token,服务端进行验证token的有效性。那么我们如何验证token的有效性呢? 所以我们需要checkToken这个中间件来检测token的有效性。<br><figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * checkToken</span><br><span class="line"> */</span><br><span class="line">const jwt = require('jsonwebtoken');</span><br><span class="line">const secret = '我是密钥'</span><br><span class="line"></span><br><span class="line">module.exports = async ( req, res, next ) => {</span><br><span class="line"> const authorization = req.get('Authorization');</span><br><span class="line"> if (!authorization) {</span><br><span class="line"> res.status(401).end(); //接口需要认证但是有没带上token,返回401未授权状态码</span><br><span class="line"> return</span><br><span class="line"> }</span><br><span class="line"> const token = authorization.split(' ')[1];</span><br><span class="line"> try {</span><br><span class="line"> let tokenContent = await jwt.verify(token, secret); //如果token过期或验证失败,将抛出错误</span><br><span class="line"> next(); //执行下一个中间件</span><br><span class="line"> } catch (err) {</span><br><span class="line"> console.log(err)</span><br><span class="line"> res.status(401).end(); //token过期或者验证失败返回401状态码</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p>
<p>那么现在咱们只要在需要用户认证的接口上,在操作数据之前,加上checkToken中间件即可,如下调用:<br><figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">//更新用户信息</span><br><span class="line">router.post('/updateUserInfo', checkToken, User.updateUserInfo) </span><br><span class="line"></span><br><span class="line">//如果checkToken检测不成功,它便返回401状态码,不会对User.updateUserInfo做任何操作, 只有检测token成功,才能处理User.updateUserInfo</span><br></pre></td></tr></table></figure></p>
<p>我们如何保证每次请求都能在请求头里加入Authorization,并加上token,这就要用到Axios的请求拦截,并且也用到了它的响应拦截,因为在服务端返回401状态码之后应要执行登出操作,清楚本地token的存储,具体代码如下:<br><figure class="highlight js"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">//request拦截器</span></span><br><span class="line">instance.interceptors.request.use(</span><br><span class="line"> config => {</span><br><span class="line"> <span class="comment">//每次发送请求之前检测本地是否存有token,都要放在请求头发送给服务器</span></span><br><span class="line"> <span class="keyword">if</span>(localStorage.getItem(<span class="string">'token'</span>)){</span><br><span class="line"> <span class="keyword">if</span> (config.url.indexOf(<span class="string">'upload-z0.qiniup.com/putb64'</span>) > <span class="number">-1</span>){</span><br><span class="line"> config.headers.Authorization = config.headers[<span class="string">'UpToken'</span>]; <span class="comment">//加上七牛云上传token</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">else</span> {</span><br><span class="line"> config.headers.Authorization = <span class="string">`token <span class="subst">${localStorage.getItem(<span class="string">'token'</span>)}</span>`</span>.replace(<span class="regexp">/(^\")|(\"$)/g</span>, <span class="string">''</span>); <span class="comment">//加上系统接口token</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'config'</span>,config)</span><br><span class="line"> <span class="keyword">return</span> config;</span><br><span class="line"> },</span><br><span class="line"> err => {</span><br><span class="line"> <span class="built_in">console</span>.log(<span class="string">'err'</span>,err)</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.reject(err);</span><br><span class="line"> }</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">//response拦截器</span></span><br><span class="line">instance.interceptors.response.use(</span><br><span class="line"> response => {</span><br><span class="line"> <span class="keyword">return</span> response;</span><br><span class="line"> },</span><br><span class="line"> error => { <span class="comment">//默认除了2XX之外的都是错误的,就会走这里</span></span><br><span class="line"> <span class="keyword">if</span>(error.response){</span><br><span class="line"> <span class="keyword">switch</span>(error.response.status){</span><br><span class="line"> <span class="keyword">case</span> <span class="number">401</span>:</span><br><span class="line"> <span class="built_in">console</span>.log(error.response)</span><br><span class="line"> store.dispatch(<span class="string">'ADMIN_LOGINOUT'</span>); <span class="comment">//可能是token过期,清除它</span></span><br><span class="line"> router.replace({ <span class="comment">//跳转到登录页面</span></span><br><span class="line"> path: <span class="string">'/login'</span>,</span><br><span class="line"> query: { <span class="attr">redirect</span>: <span class="string">'/dashboard'</span> } <span class="comment">// 将跳转的路由path作为参数,登录成功后跳转到该路由</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">Promise</span>.reject(error.response);</span><br><span class="line"> }</span><br><span class="line">);</span><br></pre></td></tr></table></figure></p>
<p>其中的if else 是因为本系统的图片,音视频是放在七牛云,上传需要七牛云上传base64图片的时候token是放在请求头的,正常的图片上传不是放在请求头,所以这边对token做了区分,如何接入七牛云也会在下面模块介绍到。</p>
<h1 id="七牛云接入"><a href="#七牛云接入" class="headerlink" title="七牛云接入"></a>七牛云接入</h1><p>本系统的图片,音视频是放在七牛云,所以需要接入七牛云。七牛云分了两种情况,正常图片和音视频的上传和base64图片的上传,因为七牛云在对他们两者上传的<code>Content-Type</code>和<code>domain(域)</code>有所不同,正常图片和音视频的Content-Type是<code>headers: {'Content-Type':'multipart/form-data'}</code>domain是<code>domain='https://upload-z0.qiniup.com'</code>,而base64图片的上传则是<code>headers:{'Content-Type':'application/octet-stream'}</code>domain是<code>domain='https://upload-z0.qiniup.com/putb64/-1'</code>,所以他们请求的时候token放的地方不同,base64就像上面所说的放在请求头<code>Authorization</code>中,而正常的放在<code>form-data</code>中。在服务端通过接口请求来获取七牛云上传token,客户端获取到七牛云token,通过不同方案将token带上。</p>
<ol>
<li>base64的上传: <code>headers:{'Content-Type':'application/octet-stream'}</code> 和 <code>domain='https://upload-z0.qiniup.com/putb64/-1'</code>,token放在请求头<code>Authorization</code>中。</li>
<li>正常图片和音视频的上传: <code>headers: {'Content-Type':'multipart/form-data'}</code>和<code>domain='https://upload-z0.qiniup.com'</code>,token 放在 <code>form-data</code>中。</li>
</ol>
<p>服务端通过<code>qiniu</code>这个模块进行创建token,服务端代码如下:</p>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">/**</span><br><span class="line"> * 构建一个七牛云上传凭证类</span><br><span class="line"> * @class QN</span><br><span class="line"> */</span><br><span class="line">const qiniu = require('qiniu') //导入qiniu模块</span><br><span class="line">const config = require('../config')</span><br><span class="line">class QN {</span><br><span class="line"> /**</span><br><span class="line"> * Creates an instance of qn.</span><br><span class="line"> * @param {string} accessKey -七牛云AK</span><br><span class="line"> * @param {string} secretKey -七牛云SK</span><br><span class="line"> * @param {string} bucket -七牛云空间名称</span><br><span class="line"> * @param {string} origin -七牛云默认外链域名,(可选参数)</span><br><span class="line"> */</span><br><span class="line"> constructor (accessKey, secretKey, bucket, origin) {</span><br><span class="line"> this.ak = accessKey</span><br><span class="line"> this.sk = secretKey</span><br><span class="line"> this.bucket = bucket</span><br><span class="line"> this.origin = origin</span><br><span class="line"> }</span><br><span class="line"> /**</span><br><span class="line"> * 获取七牛云文件上传凭证</span><br><span class="line"> * @param {number} time - 七牛云凭证过期时间,以秒为单位,如果为空,默认为7200,有效时间为2小时</span><br><span class="line"> */</span><br><span class="line"> upToken (time) {</span><br><span class="line"> const mac = new qiniu.auth.digest.Mac(this.ak, this.sk)</span><br><span class="line"> const options = {</span><br><span class="line"> scope: this.bucket,</span><br><span class="line"> expires: time || 7200</span><br><span class="line"> }</span><br><span class="line"> const putPolicy = new qiniu.rs.PutPolicy(options)</span><br><span class="line"> const uploadToken = putPolicy.uploadToken(mac)</span><br><span class="line"> return uploadToken</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">exports.QN = QN;</span><br><span class="line"></span><br><span class="line">exports.upToken = () => {</span><br><span class="line"> return new QN(config.qiniu.accessKey, config.qiniu.secretKey, config.qiniu.bucket, config.qiniu.origin).upToken() //每次调用都创建一个token</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">//获取七牛云token接口</span><br><span class="line">const {upToken} = require('../utils/qiniu')</span><br><span class="line"></span><br><span class="line">app.get('/api/uploadToken', (req, res, next) => {</span><br><span class="line"> const token = upToken()</span><br><span class="line"> res.send({</span><br><span class="line"> status: 1,</span><br><span class="line"> message: '上传凭证获取成功',</span><br><span class="line"> upToken: token,</span><br><span class="line"> })</span><br><span class="line"> })</span><br></pre></td></tr></table></figure>
<p>由于正常图片和音视频的上传和base64图片的上传,因为七牛云在对他们两者上传的<code>Content-Type</code>和<code>domain(域)</code>有所不同,所以的token请求存放的位置有所不同,因此要区分,客户端调用上传代码如下:</p>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">//根据获取到的上传凭证uploadToken上传文件到指定域</span><br><span class="line"> //正常图片和音视频的上传</span><br><span class="line"> uploadFile(formdata, domain='https://upload-z0.qiniup.com',config={headers:{'Content-Type':'multipart/form-data'}}){</span><br><span class="line"> console.log(domain)</span><br><span class="line"> console.log(formdata)</span><br><span class="line"> return instance.post(domain, formdata, config)</span><br><span class="line"> },</span><br><span class="line"> //base64图片的上传</span><br><span class="line"> //根据获取到的上传凭证uploadToken上传base64到指定域</span><br><span class="line"> uploadBase64File(base64, token, domain = 'https://upload-z0.qiniup.com/putb64/-1', config = {</span><br><span class="line"> headers: {</span><br><span class="line"> 'Content-Type': 'application/octet-stream',</span><br><span class="line"> },</span><br><span class="line"> }){</span><br><span class="line"> const pic = base64.split(',')[1];</span><br><span class="line"> config.headers['UpToken'] = `UpToken ${token}`</span><br><span class="line"> return instance.post(domain, pic, config)</span><br><span class="line"> },</span><br></pre></td></tr></table></figure>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">function upload(Vue, data, callbackSuccess, callbackFail) {</span><br><span class="line"> //获取上传token之后处理</span><br><span class="line"> Vue.prototype.axios.getUploadToken().then(res => {</span><br><span class="line"> if (typeof data === 'string'){ //如果是base64</span><br><span class="line"> const token = res.data.upToken</span><br><span class="line"> Vue.prototype.axios.uploadBase64File(data, token).then(res => {</span><br><span class="line"> if (res.status === 200){</span><br><span class="line"> callbackSuccess && callbackSuccess({</span><br><span class="line"> data: res.data,</span><br><span class="line"> result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> }).catch((error) => {</span><br><span class="line"> callbackFail && callbackFail({</span><br><span class="line"> error</span><br><span class="line"> })</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> else if (data instanceof FormData){ //如果是FormData</span><br><span class="line"> data.append('token', res.data.upToken)</span><br><span class="line"> data.append('key', `moment${Date.now()}${Math.floor(Math.random() * 100)}`)</span><br><span class="line"> Vue.prototype.axios.uploadFile(data).then(res => {</span><br><span class="line"> if (res.status === 200){</span><br><span class="line"> callbackSuccess && callbackSuccess({</span><br><span class="line"> data: res.data,</span><br><span class="line"> result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}`</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> }).catch((error) => {</span><br><span class="line"> callbackFail && callbackFail({</span><br><span class="line"> error</span><br><span class="line"> })</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> else {</span><br><span class="line"> const formdata = new FormData() //如果不是formData 就创建formData</span><br><span class="line"> formdata.append('token', res.data.upToken)</span><br><span class="line"> formdata.append('file', data.file || data)</span><br><span class="line"> formdata.append('key', `moment${Date.now()}${Math.floor(Math.random() * 100)}.${data.file.type.split('/')[1]}`)</span><br><span class="line"> // 获取到凭证之后再将文件上传到七牛云空间</span><br><span class="line"> console.log('formdata',formdata)</span><br><span class="line"> Vue.prototype.axios.uploadFile(formdata).then(res => {</span><br><span class="line"> console.log('res',res)</span><br><span class="line"> if (res.status === 200){</span><br><span class="line"> callbackSuccess && callbackSuccess({</span><br><span class="line"> data: res.data,</span><br><span class="line"> result_url: `http://p89inamdb.bkt.clouddn.com/${res.data.key}` //返回的图片链接</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> }).catch((error) => {</span><br><span class="line"> console.log(error)</span><br><span class="line"> callbackFail && callbackFail({</span><br><span class="line"> error</span><br><span class="line"> })</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> })</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">export default upload</span><br></pre></td></tr></table></figure>
<h1 id="路由权限模块"><a href="#路由权限模块" class="headerlink" title="路由权限模块"></a>路由权限模块</h1><p>系统的后台管理面向的是合作作者和管理员,涉及到两种角色,故此要做权限管理。不同的权限对应着不同的路由,同时侧边栏的菜单也需根据不同的权限,异步生成,不同于以往的服务端直接返回路由表,由前端动态生成,接下来介绍下登录和权限验证的思路:</p>
<ol>
<li><p>登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后前端会根据token再去拉取一个getAdminInfo的接口来获取用户的详细信息(如用户权限,用户名等等信息)。</p>
</li>
<li><p>权限验证:通过token获取用户对应的role,动态根据用户的role算出其对应有权限的路由,通过vue-router的beforeEach进行全局前置守卫再通过router.addRoutes动态挂载这些路由。</p>
</li>
</ol>
<p>代码有点多,这边就直接放流程图哈~~</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/19/16552aedaded68fa?w=700&h=628&f=png&s=64946" alt="权限路由流程图"></p>
<center>权限路由流程图</center>
<p>最近正好也在公司做中后台项目,公司的中后台项目的这边是由服务端生成路由表,前端进行直接渲染,毕竟公司的一整套业务比较成熟。但是我们会在想能不能由前端维护路由表,这样不用到时候项目迭代,前端每增加页面都要让服务端兄弟配一下路由和权限,当然前提可能是项目比较小的时候。</p>
<h1 id="账号模块"><a href="#账号模块" class="headerlink" title="账号模块"></a>账号模块</h1><p>账号模块是业务中最为基础的模块,承担着整个系统所有的账号相关的功能。系统实现了用户注册、用户登录、密码修改、找回密码功能。</p>
<p>系统的账号模块使用了邮件服务,针对普通用户的注册采用了邮件服务来发送验证码,以及密码的修改等操作都采用了邮件服务。在node.js中主要采用了Nodemailer,Nodemailer是一个简单易用的Node.js邮件发送组件,它的使用可以<a href="https://user-gold-cdn.xitu.io/2018/8/19/16552bfb4463e4dc" target="_blank" rel="noopener">摸我摸我摸我</a>,通过此模块进行邮件的发送。你们可能会问,为什么不用短信服务呢?哈~因为短信服务要钱,哈哈哈</p>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">/*</span><br><span class="line">* email 邮件模块</span><br><span class="line">*/</span><br><span class="line"></span><br><span class="line">const nodemailer = require('nodemailer');</span><br><span class="line">const smtpTransport = require('nodemailer-smtp-transport');</span><br><span class="line">const config = require('../config')</span><br><span class="line"></span><br><span class="line">const transporter = nodemailer.createTransport(smtpTransport({</span><br><span class="line"> host: 'smtp.qq.com',</span><br><span class="line"> secure: true,</span><br><span class="line"> port: 465, // SMTP 端口</span><br><span class="line"> auth: {</span><br><span class="line"> user: config.email.account,</span><br><span class="line"> pass: config.email.password //这里密码不是qq密码,是你设置的smtp授权码</span><br><span class="line"> }</span><br><span class="line">}));</span><br><span class="line"></span><br><span class="line">let clientIsValid = false;</span><br><span class="line">const verifyClient = () => {</span><br><span class="line"> transporter.verify((error, success) => {</span><br><span class="line"> if (error) {</span><br><span class="line"> clientIsValid = false;</span><br><span class="line"> console.warn('邮件客户端初始化连接失败,将在一小时后重试');</span><br><span class="line"> setTimeout(verifyClient, 1000 * 60 * 60);</span><br><span class="line"> } else {</span><br><span class="line"> clientIsValid = true;</span><br><span class="line"> console.log('邮件客户端初始化连接成功,随时可发送邮件');</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">};</span><br><span class="line">verifyClient();</span><br><span class="line"></span><br><span class="line">const sendMail = mailOptions => {</span><br><span class="line"> if (!clientIsValid) {</span><br><span class="line"> console.warn('由于未初始化成功,邮件客户端发送被拒绝');</span><br><span class="line"> return false;</span><br><span class="line"> }</span><br><span class="line"> mailOptions.from = '"ShineTomorrow" <admin@momentin.cn>'</span><br><span class="line"> transporter.sendMail(mailOptions, (error, info) => {</span><br><span class="line"> if (error) return console.warn('邮件发送失败', error);</span><br><span class="line"> console.log('邮件发送成功', info.messageId, info.response);</span><br><span class="line"> });</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line">exports.sendMail = sendMail;</span><br></pre></td></tr></table></figure>
<p>账号的注册先是填写email,填写好邮箱之后会通过Nodemailer发送一封含有有效期的验证码邮件,之后填写验证码、昵称和密码即可完成注册,并且为了安全考虑,对密码采用了安全哈希算法(Secure Hash Algorithm)进行加密。账号的登录以账号或者邮箱号加上密码进行登录,并且采用上文所说的JSON Web Token(JWT)身份认证机制,从而实现用户和用户登录状态数据的对应。</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/19/16552c1407819375?w=426&h=756&f=png&s=98007" alt="邮件长这样"></p>
<center>我的邮件长这样👆(可自己写邮件模板)</center>
<h1 id="实时消息推送"><a href="#实时消息推送" class="headerlink" title="实时消息推送"></a>实时消息推送</h1><p>当用户被人关注、评论被他人回复和点赞等一些社交性的操作的时候,在数据存储完成后,服务端应需要及时向用户推送消息来提醒用户。消息推送模块采用了<code>Socket.io</code>来实现,socket.io封装了websocket,不支持websocket的情况还提供了降级AJAX轮询,功能完备,设计优雅,是开发实时双向通讯的不二手段。</p>
<p>通过 socket.io,用户每打开一个页面,这个页面都会和服务端建立一个连接。在服务端可以通过连接的socket的id属性来匹配到一个建立连接的页面。所以用户的ID和socket的id,是一对多的关系,即一个用户可能在登录后打开多个页面。而socket.io没有提供从服务端向某个用户单独发送消息的功能,更没有提供向某个用户打开的所有页面推送消息的功能。但是socket.io提供了room的概念,即群组。在建立websocket时,客户端可以选择加入某个room,如果这个room没有存在则自动新建一个,否则直接加入,服务端可以向某个room中的所有客户端推送消息。</p>
<p>根据这个特性,设计将用户的ID作为room的名字,当某个用户打开页面建立连接时,会选择加入以自己用户ID为名字的room。这样,在用户ID为名字的 room中,加入的都是用户自己打开的页面建立的连接。从而向某个用户推送消息,可以直接通过向以此用户的ID为名字的room发送消息,这样就会推送到用户打开的所有页面。</p>
<p>有了想法后我们就开始鲁吧~,在服务端中<code>socket.io</code>在客户端中使用<code>vue-socket.io</code>, 服务端代码如下:</p>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">/*</span><br><span class="line">* app.js中</span><br><span class="line">*/</span><br><span class="line">const server = require('http').createServer(app);</span><br><span class="line">const io = require('socket.io')(server);</span><br><span class="line">global.io = io; //全局设上io值, 因为在其他模块要用到</span><br><span class="line">io.on('connection', function (socket) {</span><br><span class="line"> // setTimeout(()=>{</span><br><span class="line"> // socket.emit('nodeEvent', { hello: 'world' });</span><br><span class="line"> // }, 5000)</span><br><span class="line"> socket.on('login_success', (data) => { //接受客户端触发的login_success事件</span><br><span class="line"> //使用user_id作为房间号</span><br><span class="line"> socket.join(data.user_id);</span><br><span class="line"> console.log('login_success',data);</span><br><span class="line"> });</span><br><span class="line">});</span><br><span class="line">io.on('disconnect', function (socket) {</span><br><span class="line"> socket.emit('user disconnected');</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">server.listen(config.port, () => {</span><br><span class="line"> console.log(`The server is running at http://localhost:${config.port}`);</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line">/*</span><br><span class="line">* 某业务模块</span><br><span class="line">*/</span><br><span class="line">//例如某文章增加评论</span><br><span class="line">io.in(newMusicArticle.author.user_id._id).emit('receive_message', newMessage); //实时通知客户端receive_message事件</span><br><span class="line">sendMail({ //发送邮件</span><br><span class="line"> to: newMusicArticle.author.user_id.email,</span><br><span class="line"> subject: `Moment | 你有未读消息哦~`,</span><br><span class="line"> text: `啦啦啦,我是卖报的小行家~~ 🤔`,</span><br><span class="line"> html: emailTemplate.comment(sender, newMusicArticle, content, !!req.body.reply_to_id)</span><br><span class="line">})</span><br></pre></td></tr></table></figure>
<p>客服端代码:<br><figure class="highlight plain"><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></pre></td><td class="code"><pre><span class="line"><script></span><br><span class="line"> export default {</span><br><span class="line"> name: 'App',</span><br><span class="line"> data () {</span><br><span class="line"> return {</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> sockets:{</span><br><span class="line"> connect(){</span><br><span class="line"> },</span><br><span class="line"> receive_message(val){ //接受服务端触发的事件,进行客户端实时更新数据</span><br><span class="line"> if (val){</span><br><span class="line"> console.log('服务端实时通信', val)</span><br><span class="line"> this.$notify(val.content)</span><br><span class="line"> console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)')</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> mixins: [mixin],</span><br><span class="line"> mounted(){</span><br><span class="line"> if (!!JSON.parse(window.localStorage.getItem('user_info'))){</span><br><span class="line"> this.$socket.emit('login_success', { //通知服务端login_success 事件, 传入id</span><br><span class="line"> user_id: JSON.parse(window.localStorage.getItem('user_info'))._id</span><br><span class="line"> })</span><br><span class="line"> }</span><br><span class="line"> },</span><br><span class="line"> }</span><br><span class="line"></script></span><br></pre></td></tr></table></figure></p>
<h1 id="评论模块"><a href="#评论模块" class="headerlink" title="评论模块"></a>评论模块</h1><p>评论模块是为了移动端WebApp下的文章下为用户提供关于评论的一些操作。系统实现了对文章的评论,评论的点赞功能,热门评论置顶以及评论的回复功能。在评论方面存在着各种各样的安全性问题,比如XSS攻击(Cross Site Scripting,跨站脚本攻击)以及敏感词等问题。预防XSS攻击使用了<code>xss</code>模块, 敏感词过滤使用<code>text-censor</code>模块。</p>
<h1 id="一些思考"><a href="#一些思考" class="headerlink" title="一些思考"></a>一些思考</h1><ol>
<li>接口数据问题</li>
</ol>
<p>在开发的时候经常会遇到这个问题,接口数据问题。有时候服务端返回的数据并不是我们想要的数据,前端要对数据进行再一步的处理。</p>
<p>例如服务端返回的某个字段为null或者服务端返回的数据结构太深,前端需要不断去判断数据结构是否真的返回了正确的东西,而不是个null 或者undefined~</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/26/165750eb5f3cf2bf?w=763&h=370&f=png&s=108610" alt=""></p>
<p>我们前端都要这么去处理过滤:<br><figure class="highlight plain"><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><span class="line"><div class="author"></span><br><span class="line"> 文 / {{(musicArticleInfo.author && musicArticleInfo.author.user_id) ? musicArticleInfo.author.user_id.username : '我叫这个名字'}}</span><br><span class="line"></div></span><br></pre></td></tr></table></figure></p>
<p>这就引出了一个思考:</p>
<p>对数据的进一步封装处理,必然渲染性能方面会存在问题,而且我们要时刻担心数据返回的问题。如果应用到公司的业务,我们应该如何处理呢 ?</p>
<ol>
<li>页面性能优化和SEO问题</li>
</ol>
<p>首屏渲染问题一直是单页应用的痛点,那么除了常用的性能优化,我们还有什么方法优化的吗 ? 这个项目虽然面向的是移动端用户,可能不存在SEO问题,如果做成pc端的话,像文章这类的应用,SEO都是必须品。<br><br><br><br><br><br><br>对于上面提出的问题,node的出现让我们看到了解决方案,那就常说的<strong>Node中间层</strong>,当然本项目中是不存在Node中间层,而是直接作为后端语言处理数据库。</p>
<p>由于大部分的公司后端要么是php要么是java,一般不把node直接作为后端语言,如果有使用到node,一般是作为一个中间层的形式存在。</p>
<p>对于第一个问题的解决:我们可以在中间层做接口转发,在转发的过程中做数据处理。而不用担心数据返回的问题。</p>
<p>对于第二个问题的解决:有了Node中间层的话,那么我们可以把首屏渲染的任务交给nodejs去做,次屏的渲染依然走之前的浏览器渲染。<br><br><br><br><br><br><br>有Node中间层的话,新的架构如下:</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/26/1657571ab2ef3d3d?w=872&h=375&f=png&s=15216" alt="新架构"></p>
<p>前后端的职能:</p>
<p><img src="https://user-gold-cdn.xitu.io/2018/8/26/165757633f790cfb?w=859&h=469&f=png&s=23919" alt=""></p>
<h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>已经毕业一段时间了,写文章是为了回顾。本人水平一般,见谅见谅。这个产品的实现,一个人扛,在其中充当了各种角色,要有一点点产品思维,要有一点点设计的想法,要会数据库设计,要会后端开发,挺繁琐的。最难的点个人感觉还是数据库设计,数据库要一开始就要设计的很完整,不然到后面的添添补补,就会很乱很乱,当然这个基础是产品要非常清晰,刚开始自己心中对产品可能是个模糊的定义,想想差不多是那样,于是乎就开始搞<del>导致于后面数据库设计的不是很满意。由于时间关系,现在的产品中有些小模块还没完成,但是大部分的功能结构已经完成,算是个成型的产品,当然是一个没有经过测试的产品哈哈哈哈,要是有测试的话,那就哈哈哈哈你懂得 ~</del>。</p>
<p>前路漫漫,吾将上下而求索~</p>
<p><br></p>
<p><strong>完</strong></p>
<p><strong>谢谢~~</strong></p>
</div>
<!-- Post Comments -->
<div id="comment" style='padding:10px;' class="vcomment"></div>
<script src="//cdn1.lncld.net/static/js/3.0.4/av-min.js"></script>
<script src="//unpkg.com/valine/dist/Valine.min.js"></script>
<script>
var GUEST_INFO = ['nick','mail','link'];
var guest_info = 'nick,mail,link'.split(',').filter(function(item){
return GUEST_INFO.indexOf(item) > -1
});
var notify = 'false' == true;
var verify = 'false' == true;
new Valine({
el: '.vcomment',
notify: notify,
verify: verify,
appId: "ztvc2Jdss3JEXFT7bmDXiR6u-gzGzoHsz",
appKey: "SaIXu97lk5KQeyTiQ6VmgDLF",
placeholder: "Just go go",
pageSize:'10',
avatar:'identicon',
lang:'zh-cn'
});
</script>
</div>
<!-- Post Prev & Next Nav -->
<nav class="material-nav mdl-color-text--grey-50 mdl-cell mdl-cell--12-col">
<!-- Prev Nav -->
<a href="/MongoDB-Mongoose.html" id="post_nav-newer" class="prev-content">
<button class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon mdl-color--white mdl-color-text--grey-900" role="presentation">
<i class="material-icons">arrow_back</i>
</button>
新篇
</a>
<!-- Section Spacer -->
<div class="section-spacer"></div>
<!-- Next Nav -->
<a href="/聊聊毕业设计系列1.html" id="post_nav-older" class="next-content">
旧篇
<button class="mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon mdl-color--white mdl-color-text--grey-900" role="presentation">
<i class="material-icons">arrow_forward</i>
</button>
</a>
</nav>
</div>
</div>
<!-- Overlay For Active Sidebar -->
<div class="sidebar-overlay"></div>
<!-- Material sidebar -->
<aside id="sidebar" class="sidebar sidebar-colored sidebar-fixed-left" role="navigation">
<div id="sidebar-main">
<!-- Sidebar Header -->
<div class="sidebar-header header-cover" style="background-image: url(/img/sidebar_header.png);">
<!-- Top bar -->
<div class="top-bar"></div>
<!-- Sidebar toggle button -->
<button type="button" class="sidebar-toggle mdl-button mdl-js-button mdl-js-ripple-effect mdl-button--icon" style="display: initial;" data-upgraded=",MaterialButton,MaterialRipple">
<i class="material-icons">clear_all</i>
<span class="mdl-button__ripple-container">
<span class="mdl-ripple">
</span>
</span>
</button>
<!-- Sidebar Avatar -->
<div class="sidebar-image">
<img src="/img/avatar1.jpg" alt="黄明照's avatar">
</div>
<!-- Sidebar Email -->
<a data-toggle="dropdown" class="sidebar-brand" href="#settings-dropdown">
beheroto@gmail.com
<b class="caret"></b>
</a>
</div>
<!-- Sidebar Navigation -->
<ul class="nav sidebar-nav">
<!-- User dropdown -->
<li class="dropdown">
<ul id="settings-dropdown" class="dropdown-menu">
<li>
<a href="mailto: beheroto@gmail.com" target="_blank" title="Email Me">
<i class="material-icons sidebar-material-icons sidebar-indent-left1pc-element">email</i>
Email Me
</a>
</li>
<li>
<a href="https://github.com/ishareme" target="_blank" title="Github">
<i class="material-icons sidebar-material-icons sidebar-indent-left1pc-element">star</i>
Github
</a>
</li>
</ul>
</li>
<!-- Homepage -->
<li id="sidebar-first-li">
<a href="/">
<i class="material-icons sidebar-material-icons">home</i>
主页
</a>
</li>
<!-- Archives -->
<li class="dropdown">
<a href="#" class="ripple-effect dropdown-toggle" data-toggle="dropdown">
<i class="material-icons sidebar-material-icons">inbox</i>
归档
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>
<a class="sidebar_archives-link" href="/archives/2018/10/">十月 2018<span class="sidebar_archives-count">1</span></a></li><li><a class="sidebar_archives-link" href="/archives/2018/08/">八月 2018<span class="sidebar_archives-count">2</span></a></li><li><a class="sidebar_archives-link" href="/archives/2018/03/">三月 2018<span class="sidebar_archives-count">3</span></a></li><li><a class="sidebar_archives-link" href="/archives/2018/02/">二月 2018<span class="sidebar_archives-count">6</span></a></li><li><a class="sidebar_archives-link" href="/archives/2018/01/">一月 2018<span class="sidebar_archives-count">1</span></a></li><li><a class="sidebar_archives-link" href="/archives/2017/10/">十月 2017<span class="sidebar_archives-count">3</span></a></li><li><a class="sidebar_archives-link" href="/archives/2017/08/">八月 2017<span class="sidebar_archives-count">3</span></a></li><li><a class="sidebar_archives-link" href="/archives/2017/07/">七月 2017<span class="sidebar_archives-count">2</span></a>
</ul>
</li>
<!-- Categories -->
<li class="dropdown">
<a href="#" class="ripple-effect dropdown-toggle" data-toggle="dropdown">
<i class="material-icons sidebar-material-icons">chrome_reader_mode</i>
分类
<b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li>
<a class="sidebar_archives-link" href="/categories/渣技术/">渣技术<span class="sidebar_archives-count">18</span></a></li><li><a class="sidebar_archives-link" href="/categories/闲言语/">闲言语<span class="sidebar_archives-count">1</span></a>
</ul>
</li>
<!-- Pages -->
<li>
<a href="https://github.com/ishareme" title="Github">
<i class="material-icons sidebar-material-icons">star</i>
Github
</a>
</li>
<li>
<a href="/links" title="Links">
<i class="material-icons sidebar-material-icons">group</i>
Links
</a>
</li>
<li>
<a href="/gallery" title="Gallery">
<i class="material-icons sidebar-material-icons">photo_camera</i>
Gallery
</a>
</li>
<li class="divider"></li>
<li>
<a href="/tags" title="Tags">
<i class="material-icons sidebar-material-icons">bookmark_border</i>
Tags
</a>
</li>
<li>
<a href="/timeline" title="Timeline">
<i class="material-icons sidebar-material-icons">timeline</i>
Timeline
</a>
</li>
<li>
<a href="http://ougxgesj3.bkt.clouddn.com/%E9%BB%84%E6%98%8E%E7%85%A7-%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E5%B7%A5%E7%A8%8B%E5%B8%88.pdf" title="About">
<i class="material-icons sidebar-material-icons">person</i>
About
</a>
</li>
<!-- Article Number -->
<li>
<a href="/archives">
文章总数
<span class="sidebar-badge">21</span>
</a>
</li>
</ul>
<!-- Sidebar Footer -->
<!--
I'm glad you use this theme, the development is no so easy, I hope you can keep the copyright, I will thank you so much.
If you still want to delete the copyrights, could you still retain the first one? Which namely "Theme Material"
It will not impact the appearance and can give developers a lot of support :)
很高兴您使用并喜欢该主题,开发不易 十分谢谢与希望您可以保留一下版权声明。
如果您仍然想删除的话 能否只保留第一项呢?即 "Theme Material"
它不会影响美观并可以给开发者很大的支持和动力。 :)
-->
<!-- Sidebar Divider -->
<div class="sidebar-divider"></div>
<!-- Theme Material -->
<!-- Help & Support -->
<!--
-->
<!-- Feedback -->
<!--
-->
<!-- About Theme -->
<!--
-->
</div>
<!-- Sidebar Image -->
</aside>
<!-- Footer Top Button -->
<div id="back-to-top" class="toTop-wrap">
<a href="#top" class="toTop">
<i class="material-icons footer_top-i">expand_less</i>
</a>
</div>
<!--Footer-->
<footer class="mdl-mini-footer" id="bottom">
<!-- Paradox Footer Left Section -->
<div class="mdl-mini-footer--left-section sns-list">
<!-- Twitter -->
<!-- Facebook -->
<!-- Google + -->
<!-- Weibo -->
<a href="https://weibo.com/3018998921/profile?rightmod=1&wvr=6&mod=personinfo" target="_blank">
<button class="mdl-mini-footer--social-btn social-btn footer-sns-weibo">
<span class="visuallyhidden">Weibo</span>
</button><!--
--></a>
<!-- Instagram -->
<!-- Tumblr -->
<!-- Github -->
<a href="https://github.com/ishareme" target="_blank">
<button class="mdl-mini-footer--social-btn social-btn footer-sns-github">
<span class="visuallyhidden">Github</span>
</button><!--
--></a>
<!-- LinkedIn -->
<a href="https://www.linkedin.com/in/%E6%98%8E%E7%85%A7-%E9%BB%84-79042b105/" target="_blank">
<button class="mdl-mini-footer--social-btn social-btn footer-sns-linkedin">
<span class="visuallyhidden">LinkedIn</span>
</button><!--
--></a>
<!-- Zhihu -->
<a href="https://www.zhihu.com/people/ishareme/activities" target="_blank">
<button class="mdl-mini-footer--social-btn social-btn footer-sns-zhihu">
<span class="visuallyhidden">Zhihu</span>
</button><!--
--></a>