前端测试工具列表

QA

综合测试

  • Jest
  • polly.js(拦截,重放)
  • storybook
  • F2eTest
  • sinonjs

E2E测试(end to end)

  • cypress
  • jsdom
  • browsersync
  • selenium-webdriver
  • puppeteer
  • KarmaJs
  • NightWatch

模拟测试

  • json-server
  • nock(http转发)

UI测试

  • BackTopJs

性能测试

  • autocanno
  • wrk
  • jarvis

接口测试

  • mocha
  • mock server worker
  • superTest
  • AVA

单元测试

  • enzyme
  • react-testing-library
  • istanbul

断言库

  • jasmine
  • chai

单元测试框架

  • better-assert
  • should.js
  • expect.js
  • chai.js
  • Jasmine.js
  • nodejs本身集成

单元测试流程

  1. before 单个测试用例开始前
  2. beforeEach 每个测试用例开始前
  3. it 定义测试用例,并利用断言库进行设置chai如:expect(x).to.Equal(true);异步mocha
  4. after
  5. afterEach

vue项目组件总结

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
Number.prototype.toFx = function(n){
var result = this.toString()
var ary = result.split(".")
if(ary.length < 2){
result += "."
for(var i=0;i<n;i++){
result += '0'
}
return result
}
var integer = ary[0];
var decimal = ary[1];
if(decimal.length == n){
return result
}
if(decimal.length < n){
for(var i=0; i<n-decimal.length;i++){
result += '0'
}
return result
}
return integer + "." + decimal.slice(0,n)
}

/**
* 自定义指令,方便项目在进行绑定的时候。
*/
Vue.config.silent = true;

Vue.directive('cblur',{
bind:function(el,binding){
document.body.addEventListener('click',function(e){
var parent = e.target;
while(parent && parent !== document.body){
// 首先弹出内容被点击,不应该隐藏
if($(parent).hasClass(binding.arg)){
e.preventDefault()
return
}
parent = parent.parentNode
}
if(e.target == el){ // 当然触发按钮也不应该触发隐藏事件
e.preventDefault()
return
}
$('.'+binding.arg).css('display','none');
})
}
})

Vue.component('b-calc',{
template:"#_calc",
delimiters: ['${', '}'],
props:[
"coininfo"
],
data:function(){
return {
calc_list:{},
calculator:{
show:false,
initial:false, //初始化与否
coin:'grin',
input_value:1,
pps_value:'',
pps_unit:'',
coin_result:'',
cny_result:'',
cny:'',
power: 0,
elec_fee: 0,
timer:''
},
}
},
watch:{
coininfo:function(val){
this.fetch_coin();
}
},
methods:{
fetch_coin(){
var that = this
if(this.coininfo != undefined || this.coininfo != null){
that.initCoinInfo(this.coininfo)
}else{
$.get('/pool_status').done(function(res){
var res = JSON.parse(res)
that.initCoinInfo(res)
})
}
},
initCoinInfo(res,calc_list){
var calc_list = {}
for(var i=0;i<res.data.data.length;i++){
var data = res.data.data[i]
var target = $('.b-tr-coin[data-coin="'+data.coin+'"]')
var coin_calc={
coin:data.coin.toUpperCase(),
pps_value:data.pps_value,
pps_unit:data.pps_unit,
cny:data.cny
}
calc_list[data.coin.toUpperCase()] = coin_calc
}
this.calc_list = calc_list

if(!this.calculator.initial){
this.calculator.initial = true
for(var key in calc_list){
this.setAndInitial(calc_list[key])
break
}
}
},
// 计算
_list_toggle_slide(select){
$(select).stop().slideToggle()
},
_list_hide_slide(list,curr,e){
if($(e.target).hasClass(curr)) return
$(list).stop().slideUp()
},
// 显示计算器
showCalculator(){
this.calculator.show = true
},
// 隐藏计算器
closeCalculator(){
this.calculator.show = false
this.calculator.power = 0
this.calculator.elec_fee = 0
for(var key in this.calc_list){
this.setAndInitial(this.calc_list[key])
break
}
},
// 设置计算器参数并计算
setAndInitial(target){
this.calculator.coin = target.coin
this.calculator.pps_value = target.pps_value
this.calculator.cny = target.cny
this.calculator.pps_unit = target.pps_unit.toUpperCase()
// 计算每日币数及价格
this.calculator.coin_result = this.calculator.pps_value
this.calculator.cny_result = Number(this.calculator.coin_result*target.cny).toFx(3)
},
// 切换计算币种
switchCalcCoin(e){
var coin = $(e.currentTarget).attr('data-coin')
// 重新定义币种单位
this.calculator.coin = coin
this.calculator.input_value = 1
this.calculator.power = 0
this.calculator.pps_value = this.calc_list[coin].pps_value
this.calculator.pps_unit = this.calc_list[coin].pps_unit.toUpperCase()
this.calculator.cny = this.calc_list[coin].cny
$('.coin_list').stop().slideUp()
this.shake_fixed()
},
shake_fixed(){
window.clearTimeout(this.calculator.timer)
this.calculator.timer = window.setTimeout(()=>{
if(!this.calculator.input_value){
this.calculator.input_value = 1
}
if(!this.calculator.power || this.calculator.power === 0){
this.calculator.power = 0
}
if(!this.calculator.elec_fee || this.calculator.elec_fee === 0){
this.calculator.elec_fee = 0
}
this.calculate()
},700)
},
// 计算当前结果
calculate(){
// 算力 this.calculator.input_value
// 电费 this.calculator.elec_fee
// 功率 this.calculator.power
// 当前币种单位 this.calculator.pps_unit
var coin = this.calculator.input_value*this.calculator.pps_value
var elec_fee = this.calculator.elec_fee*this.calculator.power*24
if(this.calculator.cny === 0){
this.calculator.coin_result = Number(coin).toFx(3)
this.calculator.cny_result = Number(-elec_fee).toFx(3)
}else{
var coin_fee = elec_fee / this.calculator.cny
this.calculator.coin_result = Number(coin-coin_fee).toFx(3)
this.calculator.cny_result = Number(this.calculator.coin_result*this.calculator.cny).toFx(3)
}
}
},
created:function(){
this.fetch_coin()
},
mounted:function(){
var that = this
$('body').on('click',(e)=>{
that._list_hide_slide('.coin_list','unit_selected',e);
})
}
})

/**
* [bee-select]
* @param Object options
* @example
* {
* id:'icon的类',
* options:{
* key1:'item',
* key2:'item'
* }
* }
*
*/

Vue.component('bee-select',{
template:
"<div class='selection' :ref='options.id'>"+
"<div class='selected' @click.stop='show_list'>"+
"<span class='iconwrap'><span class='default' :class='[options.id,currentCls]'></span></span>"+
"<span class='now'>{{selected}}</span>"+
"</div>"+
"<ul class='option-list'>"+
"<li class='option-item' v-for='(val,key,index) in options.options' :data-option='key' :key='index' @click.stop='select'>"+
"<span class='icon' v-if='options.show_icon' :class='key'></span>"+
"<span class='key' v-if='options.show_key'>{{key}}</span>"+
"<span class='val'> {{val}} </span>"+
"</li>"+
"</ul>"+
"</div>",
props:{
options: {
tyep:Object,
required:true
},
cb:{
type:Function,
required:true
},
init:{
type:Function,
required:false
}
},
watch:{
'options':{
handler: function (val, oldVal) {
this.selected = val.selected || val.options[Object.keys(this.options.options)[0]]
},
deep: true
}
},
data:function(){
return {
selected:'',
currentCls:''
}
},
methods:{
show_list:function(p){
$(this.$refs[this.options.id]).find('.selected').css('border-color','#00bc8d')
$(this.$refs[this.options.id]).find('.option-list').stop().slideToggle(200)
},
select:function(e){
this.selected = this.currentCls = e.currentTarget.dataset.option
$(this.$refs[this.options.id]).find('.option-list').stop().scrollTop(0).hide()
this.cb(e.currentTarget.dataset.option)
}
},
mounted:function(){
this.selected = this.options.selected || this.options.options[Object.keys(this.options.options)[0]]
if(!!this.init && typeof this.init === 'function'){
this.init(this.selected)
}
var that = this
var preventScroll = function(dom){
if(navigator.userAgent.indexOf('Firefox') >= 0){ //firefox
dom.addEventListener('DOMMouseScroll',function(e){
dom.scrollTop += e.detail > 0 ? 38 : -38;
e.preventDefault();
},false);
}else{
dom.onmousewheel = function(e){
e = e || window.event;
dom.scrollTop += e.wheelDelta > 0 ? -38 : 38;
return false
};
}
};
preventScroll($(that.$refs[that.options.id]).find('.option-list').get(0))
$('body').on('click',function(){
$(that.$refs[that.options.id]).find('.option-list').stop().slideUp(200).scrollTop(0)
$(that.$refs[that.options.id]).find('.selected').css('border-color','#e0e0e0')
})
}
})

/**
* [bee-count]
* @param count Number
* @param start String
* @param end String
* @param cb Function
*/

Vue.component('bee-count',{
template:"<span class='b-count' @click='notRun&&cb(run)' >${content}</span>",
props:['count','start','end','cb'],
delimiters:['${','}'],
data:function(){
return {
timer:'',
content:'',
notRun:true
}
},
methods:{
run:function(){
this.notRun = false
var time = Number(this.count)
this.content = time + '秒'
var that = this
function run_time(){
that.timer = window.setTimeout(function(){
that.content = --time + '秒'
if(time == 0){
that.content = that.end
window.clearTimeout(that.timer)
that.notRun = true
return
}
window.clearTimeout(that.timer)
run_time()
},1000)
}
run_time()
},
stop:function(){
window.clearTimeout(this.timer)
this.content = this.start
}
},
mounted:function(){
this.content = this.start
}
})

Vue.component('bee-switch',{
template:
'<button class="b-switch" :class="[b_switch?\'b-switch-on\':\'\',size]" :data-switch="[b_switch?\'1\':\'2\']" @click="swicth_change">'+
'</button>',
props:{
size:{
type: String,
required: true
},
status:{
type:Boolean,
required:true
},
cb:{
type:Function,
required: true
}
},
watch:{
status:function(val){
this.b_switch = val
}
},
data:function(){
return {
b_switch:false
}
},
methods:{
swicth_change:function(e){
this.$emit('change')
}
},
mounted:function(){
this.b_switch = this.status
}
})

Vue.component('bee-alert',{
template:'<transition name="tips"><div class="tip_wraper" v-show="isshow"><span id="tip-content">'+
'<span v-if=" type == \'error\'" class="icon icon-m_del"><span class="path1"></span><span class="path2"></span><span class="path3"></span></span>'+
'<span v-if="type == \'correct\'" class="icon icon-tip_confirm"></span>'+
'<span class="text">${msg}</span>&nbsp!'+
'</span></div></transition>',
props:["type","msg","duration"],
delimiters:['${','}'],
data(){
return {
isshow:false
}
},
methods:{
show(){
this.isshow = true
setTimeout(this.hide,this.duration)
},
hide(){
this.isshow = false
setTimeout(this.remove,700)
}
}
})

$.fn.extend({
/**
* 执行强制重绘
*/
reflow: function () {
return this.each(function () {
return this.clientLeft;
});
},

/**
* 设置 transition 时间
* @param duration
*/
transition: function (duration) {
if (typeof duration !== 'string') {
duration = duration + 'ms';
}

return this.each(function () {
this.style.webkitTransitionDuration = duration;
this.style.transitionDuration = duration;
});
},

/**
* transition 动画结束回调
* @param callback
* @returns {transitionEnd}
*/
transitionEnd: function (callback) {
var events = [
'webkitTransitionEnd',
'transitionend',
];
var i;
var _this = this;

function fireCallBack(e) {
if (e.target !== this) {
return;
}

callback.call(this, e);

for (i = 0; i < events.length; i++) {
_this.off(events[i], fireCallBack);
}
}

if (callback) {
for (i = 0; i < events.length; i++) {
_this.on(events[i], fireCallBack);
}
}

return this;
},
transformOrigin: function (transformOrigin) {
return this.each(function () {
this.style.webkitTransformOrigin = transformOrigin;
this.style.transformOrigin = transformOrigin;
});
},
transform: function (transform) {
return this.each(function () {
this.style.webkitTransform = transform;
this.style.transform = transform;
});
},
});

var TouchHandler = {
touches: 0,
isAllow: function (e) {
var allow = true;

if (
TouchHandler.touches &&
[
'mousedown',
'mouseup',
'mousemove',
'click',
'mouseover',
'mouseout',
'mouseenter',
'mouseleave',
].indexOf(e.type) > -1
) {
// 触发了 touch 事件后阻止鼠标事件
allow = false;
}

return allow;
},
register: function (e) {
if (e.type === 'touchstart') {
// 触发了 touch 事件
TouchHandler.touches += 1;
} else if (['touchmove', 'touchend', 'touchcancel'].indexOf(e.type) > -1) {
// touch 事件结束 500ms 后解除对鼠标事件的阻止
setTimeout(function () {
if (TouchHandler.touches) {
TouchHandler.touches -= 1;
}
}, 500);
}
},

start: 'touchstart mousedown',
move: 'touchmove mousemove',
end: 'touchend mouseup',
cancel: 'touchcancel mouseleave',
unlock: 'touchend touchmove touchcancel',
};

(function () {
var Ripple = {
delay: 200,
show: function (e, $ripple) {
// 鼠标右键不产生涟漪
if (e.button === 2) {
return;
}
// 点击位置坐标
var tmp;
if ('touches' in e && e.touches.length) {
tmp = e.touches[0];
} else {
tmp = e;
}
var touchStartX = tmp.pageX;
var touchStartY = tmp.pageY;
// 涟漪位置
var offset = $ripple.offset();
var center = {
x: touchStartX - offset.left,
y: touchStartY - offset.top,
};
var height = $ripple.innerHeight();
var width = $ripple.innerWidth();
var diameter = Math.max(
Math.pow((Math.pow(height, 2) + Math.pow(width, 2)), 0.5), 48
);
// 涟漪扩散动画
var translate =
'translate3d(' + (-center.x + width / 2) + 'px, ' + (-center.y + height / 2) + 'px, 0) ' +
'scale(1)';
// 涟漪的 DOM 结构
$('<div class="b-ripple-wave" style="' +
'width: ' + diameter + 'px; ' +
'height: ' + diameter + 'px; ' +
'margin-top:-' + diameter / 2 + 'px; ' +
'margin-left:-' + diameter / 2 + 'px; ' +
'left:' + center.x + 'px; ' +
'top:' + center.y + 'px;">' +
'</div>')
// 缓存动画效果
.data('translate', translate)
.prependTo($ripple)
.reflow()
.transform(translate);
},
hide: function (e, element) {
var $ripple = $(element || this);
$ripple.children('.b-ripple-wave').each(function () {
removeRipple($(this));
});
$ripple.off('touchmove touchend touchcancel mousemove mouseup mouseleave', Ripple.hide);
},
};

function removeRipple($wave) {
if (!$wave.length || $wave.data('isRemoved')) {
return;
}
$wave.data('isRemoved', true);

var removeTimeout = setTimeout(function () {
$wave.remove();
}, 400);

var translate = $wave.data('translate');

$wave
.addClass('b-ripple-wave-fill')
.transform(translate.replace('scale(1)', 'scale(1.01)'))
.transitionEnd(function () {
clearTimeout(removeTimeout);

$wave
.addClass('b-ripple-wave-out')
.transform(translate.replace('scale(1)', 'scale(1.01)'));

removeTimeout = setTimeout(function () {
$wave.remove();
}, 700);

setTimeout(function () {
$wave.transitionEnd(function () {
clearTimeout(removeTimeout);
$wave.remove();
});
}, 0);
});
}

function showRipple(e) {
if (!TouchHandler.isAllow(e)) {
return;
}
TouchHandler.register(e);
// Chrome 59 点击滚动条时,会在 document 上触发事件
if (e.target === document) {
return;
}
var $ripple;
var $target = $(e.target);
// 获取含 .b-ripple 类的元素
if ($target.hasClass('b-ripple')) {
$ripple = $target;
} else {
$ripple = $target.parents('.b-ripple').eq(0);
}
if ($ripple.length) {
if (e.type === 'touchstart') {
var hidden = false;

// toucstart 触发指定时间后开始涟漪动画
var timer = setTimeout(function () {
timer = null;
Ripple.show(e, $ripple);
}, Ripple.delay);

var hideRipple = function (hideEvent) {
// 如果手指没有移动,且涟漪动画还没有开始,则开始涟漪动画
if (timer) {
clearTimeout(timer);
timer = null;
Ripple.show(e, $ripple);
}

if (!hidden) {
hidden = true;
Ripple.hide(hideEvent, $ripple);
}
};

// 手指移动后,移除涟漪动画
var touchMove = function (moveEvent) {
if (timer) {
clearTimeout(timer);
timer = null;
}
hideRipple(moveEvent);
};
$ripple
.on('touchmove', touchMove)
.on('touchend touchcancel', hideRipple);
} else {
Ripple.show(e, $ripple);
$ripple.on('touchmove touchend touchcancel mousemove mouseup mouseleave', Ripple.hide);
}
}
}
// 初始化绑定的事件
$(document)
.on(TouchHandler.start, showRipple)
.on(TouchHandler.unlock, TouchHandler.register);
})();

function tips(status,msg,delay=1000){
var props = {
type:status,
msg:msg,
duration:delay
}

var vm = new Vue({
render:function(h){
return h("bee-alert",{props})
}
}).$mount()

$('#tip-box').append(vm.$el)

var comp = vm.$children[0]
comp.remove = function(){
$('#tip-box').get(0).removeChild(vm.$el)
comp.$destroy()
}
comp.show()
}

// function tips(status,msg,delay=1000){
// $("#tip-content .text").text(msg)
// $("#tip-content .icon").removeClass('show')
// if(status=='correct'){
// $("#tip-content .icon-tip_confirm").addClass('show')
// }else if(status=='error'){
// $("#tip-content .icon-m_del").addClass('show')
// }
// $("#tip-content").stop(true).addClass(function(){
// $('#tip-box').addClass('hi')
// return 'shift'
// }).fadeIn(300).delay(delay).removeClass('shift').fadeOut(200,function(){
// $('#tip-box').removeClass('hi')
// })
// }

var bee_header = new Vue({
el:'#b_navtop',
delimiters: ['${', '}'],
data:function(){
return {
header:{
showSide:false,
logined:'',
},
language:{
id:'language',
show_key:false,
options:{
cn:'简体中文',
en:'English'
},
selected:''
},
miner_part:'',
mine:{
name:'',
score:'',
grade:''
},
observe:false,
navbarWhite:false //导航栏颜色是否已经是白色
}
},
watch:{
'header.logined':function(val,old){
try {
if( bee !== null){
bee.logined = val
}
} catch (err) {}

try {
if( b_account !== null){
b_account.mine = this.mine
}
} catch (err) {}

try {
if( b_miner !== null){
b_miner.mine = this.mine
}
} catch (err) {}
}
},
methods:{
/* <导航栏方法> */
showOrHide:function(){
if(!this.header.showSide){
$('.b-side-bar').css({'marginLeft':'0'})
$('.b-side-back').fadeIn(200)
this.header.showSide = true
}else{
$('.b-side-back').fadeOut(200)
this.header.showSide = false
$('.b-side-bar').css({'marginLeft':'100%'})
}
},
showTime:function(status,ele){
if(status=='in'){
$(ele).stop().slideDown()
}else{
$(ele).stop().slideUp()
}
},
show_personal:function(p){
if(p=='in'){
$('.action_list').stop().slideDown()
}else{
$('.action_list').stop().slideUp()
}
},
switch_language(l){
if(l == 'cn'){
$.cookie('language','zh',{ path: '/',expires:30})
}else if(l == 'en'){
$.cookie('language','en',{ path: '/',expires:30})
}
location.reload();
},
sideSlide:function(ele){
$(ele).slideToggle()
$('.b-mobile-item').removeClass('checked')
$(ele).parent().addClass('checked')
},
showNav:function(){
if($(window).scrollTop() > 460 ){
$('#goTop').stop().fadeIn()
}else if($(window).scrollTop() <= 460 ){
$('#goTop').stop().fadeOut()
}
},
goTop:function(duration){
var journey = $(window).scrollTop()
var speed = 5
var step = (journey/duration)*5
topTurn(null,step,speed)
function topTurn(timer,step,speed){
window.clearTimeout(timer)
if($(window).scrollTop() <= step){
$(window).scrollTop(0)
return
}
var timer = window.setTimeout(function(){
$(window).scrollTop($(window).scrollTop()-step)
topTurn(timer,step,speed)
},speed)
}
},
fetch_login_info(){
$.ajax({
url:'/getLoginInfo',
type:'get',
success:(res)=>{
res = JSON.parse(res)
if(res.code == 0){
this.mine.name = res.data.name
this.mine.score = res.data.score || 0
this.mine.grade = res.data.grade || 0
this.mine.phone = res.data.phone
this.mine.email = res.data.email
this.mine.update = 9
this.mine.ready = true
this.header.logined = true
}else{
this.header.logined = false
}
}
})
},
IEVersion() {
//取得浏览器的userAgent字符串
var userAgent = navigator.userAgent;
//判断是否IE浏览器
var isIE = userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1;
if (isIE) {
var reIE = new RegExp("MSIE (\\d+\\.\\d+);");
reIE.test(userAgent);
var fIEVersion = parseFloat(RegExp["$1"]);
if (fIEVersion < 10 || !isSupportPlaceholder()) {
return true;
}
} else {
return false;
}
},
/* </导航栏方法> */
init:function(){
var that = this
var scrollTimer = 0
$(window).on('scroll',function(e){
window.clearTimeout(scrollTimer)
scrollTimer = setTimeout(that.showNav,150)
})
var index = window.location.href.lastIndexOf('/')
var _href = window.location.href.slice(index+1)
var homeReg = /^home[.]*/i
var observeReg = /^observe[.]*/i
if(observeReg.test(_href) || homeReg.test(_href)){
$('.minerCenter .miner_list').show()
$(window).on('hashchange',function(){
that.miner_part = window.location.hash.slice(1)
})
}
if(observeReg.test(_href)){
that.observe = true
}
}
},
created(){
if(this.IEVersion()){
alert('浏览器版本过低,蜜蜂矿池敬请您使用Chrome、Firefox浏览器进行访问!')
}
if($.cookie('language')=='en'){
this.language.selected = 'English'
}else{
this.language.selected = '简体中文'
}
this.init()
this.fetch_login_info()
}
})

npm发布之version和tag

使用nrm管理仓库源

  1. 安装nrm
    yarn global add nrm

  2. nrm添加registry
    nrm add npm https://registry.npmjs.org/

  3. 使用仓库源和测试仓库员
    nrm use npm
    nrm test npm

  4. 查看可用仓库源
    nrm ls
    nrm use npm

上面操作设置了 registry

npm adduser

假设没有设置:

npm adduser –registry https://registry.npmjs.org/

这里默认你已经切换源了,且在上面新建用户的时候添加了账户

npm login

按照提示输入即可

具体操作npm·publish

npm publish

version 和 tag

  1. version

发布到npm后,每一个版本号都对应了其资源文件,而且是不可修改的。npm中的版本号类似于git中的tag。

  1. tag

npm中也有个tag的概念,真是混乱。一般情况下,我们可以不指定tag,这时默认就会用latest这个tag,所有发布或者安装都是最新的正式版。而指定tag之后,我们可以在这个tag上发布一个更新的版本,用户安装的时候如果也指定这个tag,则会安装这个tag下的最新版。因此,npm中的tag类似于git中的branch。 next这个tag默认是测试版本

总结一下

version一旦发布,是不可变的;
而tag更像一个渠道,只要用户选择了这个渠道,就可以一直更新这个渠道的最新版。

切换tag

npm dist-tag ls

latest: 1.0.0
next: 1.0.0-alpha.0

如果我们不小心发布了一个1.0.0-alpha.1到latest,那么我们会得到:

latest: 1.0.0-alpha.1
next: 1.0.0-alpha.0

npm view user-picker versions

[ ‘0.1.0’,
‘1.0.0’,
‘1.0.0-alpha.0’,
‘1.0.0-alpha.1’ ]

npm dist-tag add user-picker@version tag

把原来的1.0.0设置成最新的正式版

npm dist-tag add my-package@1.0.0 latest

把1.0.0-alpha.1更新到最新的测试版

npm dist-tag add my-package@1.0.0-alpha.1 next

已经被废弃的命令,撤销已经发布的某个版本,虽然可以撤销但是该版本号也不能再重新发布

npm unpublish user-picker@1.20.1

Typescript基础

1.1 ts基本类型及用法

1
2
3
4
5
6
7
8
const flag: boolean = true;

const flag: number[] = true;

enum colors {
red = 0,
blue = 1
}

1.2 ts特殊类型及用法

  • any: 任意类型,TS忽略类型检查,应当避免使用。
  • unknown: 所有类型都可以分配给unknown,必须判断类型后才可以继续使用。
  • never:用于永远不可能的场景, 用于错误检查或者收敛条件类型。

unknown用法

1
2
3
4
5
6
7
8
9
10
11
12
// eg1
const a: unknown = 1;
const a2: number = a; //error: Type 'unknown' is not assignable to type 'number'.
const a3: number = typeof a === 'number' ? a : 0;

// eg2
function getEval(){
return 'eval'
}
const eval: unknown = getEval();
(eval as string).toString();
(eval as string).toLowerCase();

1.3 断言

断言关键字有 !as<>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// eg: !
let num: number;
const init = () => num = 1;
init();
console.log( num * 2 ) //error: fixed by `let num!: number`
console.log( num! * 2 )

// eg: as
const box = document.querySelector('checkbox') as HTMLInputElement;
console.log(box.checked);

const start = { code: 200 } as const;
start.code = 300; // error: cannot assign to 'code' because it is a read-only property

// eg: <>
const button = document.querySelector('button');
console.log(<HTML>)

1.4 类型守卫

is关键字

1
2
3
4
5
6
7
class A {}
class B {}
const list = [new A(), new B(), new B()];
function isA(item: A|B): item is A {
return item instanceof A
}
cosnt result = list.filter(item => isA(item))

查找类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface initProps{
id: number;
name: string;
}

interface user extends initProps{
age: number;
unit: string;
grade: number;
}

interface student {
name: user['name'],
grade: user['grade'],
}

in操作符

1
2
3
4
5
6
7
8
interface User {
age: number;
name: string;
}

type keyInUser = {
[k in 'name' | 'age']: User[k];
}

typeof & keyof 关键字

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
const option = {
time: '2018-03',
value: 77
}

// eg: typeof
type OptionValueType = typeof option;
const otherOption: OptionValueType = {
time: '2012-03',
value: 23
}

// eg: keyof
type OptionKeyType = keyof typeof option;

function getOptionVal<T extends OptionKeyType>(key: T): typeof option[T] {
return option[key]
}
const getOptionVal2 = <T extends OptionKeyType>(key: T): typeof option[T]{
return option[key]
}

type Validator<T> = {
[K in keyof T]?: null extends T[K]? someFunc<T[K] | null | undefined> : someFunc<T[K]>
}

type ValTypeToString<T> = {
[key in keyof T]: string
}

const optionString: ValTypeToString<OptionValueType> = {
time: '2012-03',
value: '23'
}

2.1 泛型常用命名

  • T(Type): 最常用类型参数
  • K(Key): 对象的键类型
  • V(Value): 对象的值类型
  • P(Property): 对象的属性类型
  • R(Result): 类型推导的结果类型

2.2 泛型常用关键字及操作符用法

  • & 表示类型交集
  • | 表示类型并集
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
interface PersonType {
age: number;
address: string;
}

interface UserType<T> {
unit: string;
profile: T;
}

const user: UserType<PersonType> = {
unit: '北极研究所',
profile: {
age: 23,
address: '北极'
}
}

type UserType1 = Pick<UserType<PersonType>, 'unit'>
// 等于 : type UserType1 = {
// unit: string;
// }

type UserType2 = Partial<UserType<PersonType>>
// 等于 : type UserType2 = {
// unit?: string;
// married?: boolean;
// hasChild?: boolean;
// profile?: PersonType;
// }

type UserType3 = Omit<UserType2, 'unit' | 'married'>
// 等于 : type UserType3 = {
// hasChild?: boolean;
// profile?: PersonType;
// }

type UserType4 = Exclude<'unit' | 'married' | 'hasChild' | 'profile', 'unit' | 'married'>
// 等于 : type UserType4 = "hasChild" | "profile"


type UserType5 = Extract<'unit' | 'married' | 'hasChild' | 'profile', 'unit' | 'married'>
// 等于 : type UserType5 = "unit" | "married"

Nodejs模块

Nodejs模块机制

Node 一切(独立 JS 文件)皆模块,模块之间互相隔离互不影响,通过引用来互相调用。

一个模块本质是一个模块对象,通过 module.exports(exports 只是 module.exports 的一个引用)对外暴露接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ./util.js
const calcArea = (width, height) => width * height

exports.area = calcArea

// ./index.js
const area = require("./utils.js")

const logInfo = (rect) => {
const { width, height } = rect
console.log(`Input rect's rect is ${area(width, height)}`)
}

module.exports = { logInfo }

console.log(module)

index.js打印出的模块信息

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
Module {
id: '.',
path: '/Users/attacki/Desktop/mine/attacki/source/_posts/backend/nodejs/test',
exports: { logInfo: [Function: logInfo] },
filename: '/Users/attacki/Desktop/mine/attacki/source/_posts/backend/nodejs/test/index.js',
loaded: false,
children: [
Module {
id: '/Users/attacki/Desktop/mine/attacki/source/_posts/backend/nodejs/test/utils.js',
path: '/Users/attacki/Desktop/mine/attacki/source/_posts/backend/nodejs/test',
exports: [Object],
filename: '/Users/attacki/Desktop/mine/attacki/source/_posts/backend/nodejs/test/utils.js',
loaded: true,
children: [],
paths: [Array]
}
],
paths: [
'/Users/attacki/Desktop/mine/attacki/source/_posts/backend/nodejs/test/node_modules',
'/Users/attacki/Desktop/mine/attacki/source/_posts/backend/nodejs/node_modules',
'/Users/attacki/Desktop/mine/attacki/source/_posts/backend/node_modules',
'/Users/attacki/Desktop/mine/attacki/source/_posts/node_modules',
'/Users/attacki/Desktop/mine/attacki/source/node_modules',
'/Users/attacki/Desktop/mine/attacki/node_modules',
'/Users/attacki/Desktop/mine/node_modules',
'/Users/attacki/Desktop/node_modules',
'/Users/attacki/node_modules',
'/Users/node_modules',
'/node_modules'
]
}

commonJS机制

1
2
3
4
5
6
7
8
9
10
11
12
13
纸篓子 = [
'1. Node 源码有一坨依赖,大部分是 C/C++ 底层',
'2. Node 启动入口是 node_main.cc 的 main 函数',
'3. 入口函数找到 node.cc 的 3 个 Start,依次调用',
'4. node.cc 的第一个 Start 初始化了 v8,调用第二个 Start',
'5. 第二个 Start 让 v8 准备了引擎实例,调用第三个 Start',
'6. 第三个 Start:',
' 6.1 首先准备了 v8 的上下文 Context',
' 6.2 其次准备了 Node 的启动环境,对各种需要的变量做整理',
' 6.3 再把 Node 原生模块和我们的 JS 代码都加载进来运行',
' 6.4 最后把主持人 libuv 请上场,执行 JS 里的各种任务',
'7. libuv 没活干了,就一层层来退出进程、收拾场地,退出程序',
]

加载内部模块的 Loader

首先回到 6.3 的 LoadEnvironment,在 src/node.cc 2115 行,精简如下:

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
void LoadEnvironment(Environment* env) {
// 1. 首先载入 loader.js 和 node.js 拿到 JS 文件内容(字符串),通过 GetBootstrapper 解析
// 注意这两个 JS 是会被 node_js2c 编译成字符串数组,存储到 node_javascript.cc 里面,这里只是源码而已
<String> loaders_name = FIXED_STRING(env->isolate(), "internal/bootstrap/loaders.js");
<Function> loaders_bootstrapper = GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name);
Local<String> node_name = FIXED_STRING(env->isolate(), "internal/bootstrap/node.js");
<Function> node_bootstrapper = GetBootstrapper(env, NodeBootstrapperSource(env), node_name);
// 2. 创建各种 bindings,后面会丢到 JS 函数中用
// ...
// 3. 拼装 loaders 的函数参数数组,分别是 process 和后面的 binding function
// 注意这里的几个参数跟下文的 loaders.js 是有对应关系的
Local<Value> loaders_bootstrapper_args[] = {
env->process_object(),
get_binding_fn,
get_linked_binding_fn,
get_internal_binding_fn
};
// 4. 通过 ExecuteBootstrapper 来陆续启动内部模块的 loader 和 node.js
// 其中启动的时候,会传入环境参数、loader 函数体,以及上面拼好的参数数组
ExecuteBootstrapper(env, loaders_bootstrapper.ToLocalChecked(),
arraysize(loaders_bootstrapper_args),
loaders_bootstrapper_args,
&bootstrapped_loaders)

// 5. 拼装 node.js 的函数参数数组,分别是 process 和后面的 bootstrapper
Local<Value> node_bootstrapper_args[] = {
env->process_object(),
bootstrapper,
bootstrapped_loaders
};
// 6. 启动 node.js
ExecuteBootstrapper(env, node_bootstrapper.ToLocalChecked(),
arraysize(node_bootstrapper_args),
node_bootstrapper_args,
&bootstrapped_node)
}

在注释 1 的位置,JS 源码经过 GetBootstrapper 后,会定义成一个可以执行的 C++ 函数,也就是 loaders_bootstrapper,它是 Local 类型的 Function,在 v8 引擎里面,可以通过 call 直接执行它对应的 JS 函数,可以理解为 v8 里面调用 C++ 函数,来运行一段 JS 代码,另外在执行这个 JS 代码的时候,可以对 JS 里面的函数传入 C++ 构造的一些对象或者函数,这样就达到让被执行的 JS 函数,它里面也能调用到 C++ 层面的函数的目的。

也就是到了注释 4,通过执行 JS 代码来启动模块的 loader,我们看下 ExecuteBootstrapper 的代码,在 src/node.cc 2094 行,精简如下:

1
2
3
4
static bool ExecuteBootstrapper(Environment* env, Local<Function> bootstrapper, int argc, Local<Value> argv[], Local<Value>* out) {
bootstrapper->Call(
env->context(), Null(env->isolate()), argc, argv).ToLocal(out);
}

核心就是 bootstrapper->Call(),来执行 bootstrapper 函数,实际上就是执行 internal/bootstrap/loaders.js 的 JS 函数表达式 (function(){ }),同时对它传入 C++ 生成的 process 对象和 bindings 函数,也就是 loaders_bootstrapper_args 里面的:

1
2
3
4
5
6
{
env->process_object(), # 对应 process
get_binding_fn, # 对应 GetBinding
get_linked_binding_fn, # 对应 GetLinkedBinding
get_internal_binding_fn # 对应 GetInternalBinding
}

GetBinding GetLinkedBindingGetInternalBinding 里面,又是各自通过 get_builtin_module get_internal_moduleget_linked_module 来找到对应的模块以及进行一些初始化工作,这些代码都在 src/node.cc 里面,可以发现它们也都是通过 FindModule 函数来遍历查找的,关于模块注册和查找我们不再往上面继续深究,继续回来到 loaders_bootstrapper_args 的几个参数,我们此时执行 internal/bootstrap/loaders.js,对它传入这 4 个参数,看下简版的 loaders.js 代码:

1
2
3
4
5
6
(function bootstrapInternalLoaders(process,
getBinding, getLinkedBinding, getInternalBinding) {
function NativeModule(id) {}

return loaderExports;
});

发现它所接收的参数刚好是 4 个,跟 loaders_bootstrapper_args 里的参数一一对应,同时这个函数里面,有一个 NativeModule 的函数对应,望名生义,应该就是原生模块了,整个 Loaders 函数执行后,还会返回一个 loaderExports 对象,这个对 internal/bootstrap/loaders.js 是有用的。

翻译下,node.cc 里面从 C++ 层面把 loader.js 的源码拎过来解析执行,同时对它传入几个 C++ 对象,这样就可以从 loaders.js 里面以 getBinding 的形式获取原生模块了,费了这么大力气,终于可以来更新下纸箱子了:

1
2
3
4
纸箱子 = [
'6.3.1 Node 底层环境均已 Ready,准备装载 JS 模块',
'6.3.2 node.cc 加载 loaders.js,对 JS 函数传入 process、binding 等 C++ 接口',
]

internal/bootstrap/loaders.js 的文档非常详实,我简单翻译下:

首先它是 Node 启动的前置条件:

  • loaders 的作用是创建内部模块,以及用来 binding 的 loaders,来给内置模块使用
  • 我们自己写的代码,包括 node_modules 下的三方模块,都由 lib/internal/modules/cjs/loader.js 和 lib/internal/modules/esm/* (ES Modules) 接管处理
  • loaders.js 本身最终会被编译,编译后被 node.cc 所调用,等到它生效后,才会去继续调用 bootstrap/node.js 也就是说,要等到 loaders 启动之后 Nodejs 才算是真正启动

其次,它把 C++ binding 能力挂载到了 process 对象上:

  • process.binding(): 是 C++ binding loader, 从用户这可以直接访问
  • process._linkedBinding(): 目的是让 C++ 作为扩展被项目嵌入进来引用,本质是 C++ binding
  • internalBinding(): 私有内部(internal) C++ binding loader, 用户无权访问,只给 NativeModule.require() 使用

再次,它提供了内部原生模块的 loader 能力:

  • NativeModule: 一个迷你的模块系统,用来加载 Node 的核心 JS 模块
    • 这些模块在 lib/**/*.js deps/**/*.js 里面
    • 这些核心模块会被 node_javascript.cc 编译成 node 二进制文件,这样没有 I/O 开销,加载更快
    • 这个类还允许核心模块访问 lib/internal/* deps/internal/* 里的模块和 internalBinding(),也允许核心模块通过 require 加载它,即便它不是一个 CommonJS 的模块
  • process.moduleLoadList 则是按照加载顺序,记录了 bindings 和已经 load 的模块

最后,binding 和 loader 的能力,都被放到了 loaderExports 里面,作为函数执行的返回值,以 CommonJS 的方式暴露出去,可以这样理解:

1
module.exports = { internalBinding, NativeModule }

再来更新下纸箱子:

1
2
3
4
5
纸箱子 = [
'6.3.1 Node 底层环境均已 Ready,准备装载 JS 模块',
'6.3.2 node.cc 加载 loaders.js,对 JS 函数传入 process、binding 等 C++ 接口',
'6.3.2.1 loaders.js 封装了原生模块的加载,同时把加载能力和 internalBinding 也暴露出去',
]

我们再稍微的看下 internal/bootstrap/loaders.js 的源码,它里面一共分为三部分:

首先是往 process 上挂 binding:

1
2
3
4
5
6
process.binding = function binding(module) {
mod = bindingObj[module] = getBinding(module);
};
process._linkedBinding = function _linkedBinding(module) {
mod = bindingObj[module] = getLinkedBinding(module);
}

然后就是声明 NativeModule,实现代码编译等操作:

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
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.script = null;
}

// require 时代码拎过来组装编译,再把 exports 丢出去,缓存代码都略去不表
NativeModule.require = function (id) {
const nativeModule = new NativeModule(id);
nativeModule.compile();
return nativeModule.exports;
};

// contextify 这个模块的作用就是执行 JS 代码
const { ContextifyScript } = process.binding('contextify');

NativeModule.prototype.compile = function () {
// 拿到传入模块的源码,包裹成 CommonJS 的样子
let source = NativeModule.getSource(id);
source = NativeModule.wrap(source);

// ContextifyScript 类上面主要有 RunInContext、RunInThisContext 两个方法
const script = new ContextifyScript(
source, this.filename, 0, 0,
cache, false, undefined
);
const fn = script.runInThisContext(-1, true, false);
const requireFn = this.id.startsWith('internal/deps/') ?
NativeModule.requireForDeps :
NativeModule.require;
fn(this.exports, requireFn, this, process);
};

// internal 这些内部模块不会暴露给用户使用,代码略去不表
NativeModule.requireForDeps = function (id) {
return NativeModule.require(`internal/deps/${id}`);
};
NativeModule.wrapper = ['(function (exports, require, module, process) {', '\n});'];
NativeModule.wrap = (script) => (NativeModule.wrapper[0] + script + NativeModule.wrapper[1])
NativeModule._source = getBinding('natives');
NativeModule.getSource = function (id) {
return NativeModule._source[id];
};

最后,来把 loaderExports 暴露出去:

1
2
3
4
5
6
7
8
9
10
11
12
let internalBinding = function internalBinding(module) {
let mod = bindingObj[module];
if (typeof mod !== 'object') {
mod = bindingObj[module] = getInternalBinding(module);
moduleLoadList.push(`Internal Binding ${module}`);
}
return mod;
};

const loaderExports = { internalBinding, NativeModule }

return loaderExports

NativeModule 的工作产出,我们来举个简单例子,比如加载 internal/steam.js,源码大概是:

1
2
3
4
5
6
7
const { Buffer } = require('buffer');
const Stream = module.exports = require('internal/streams/legacy');
Stream.Readable = require('_stream_readable');
Stream.Writable = require('_stream_writable');
Stream.Duplex = require('_stream_duplex');
Stream.Transform = require('_stream_transform');
Stream.PassThrough = require('_stream_passthrough');

那么通过 internal/bootstrap/loaders.js 的 loaderExports 中 NativeModule 加载之后,实际是这样的代码在 v8 里面运行:

1
2
3
4
5
6
7
8
9
(function (exports, require, module, process) {
const { Buffer } = require('buffer');
const Stream = module.exports = require('internal/streams/legacy');
Stream.Readable = require('_stream_readable');
Stream.Writable = require('_stream_writable');
Stream.Duplex = require('_stream_duplex');
Stream.Transform = require('_stream_transform');
Stream.PassThrough = require('_stream_passthrough');
})

运行时,里面的 require,在 NativeModule 里面是有区分的,对于 internal 走 requireForDeps,其他模块就是 require:

1
2
3
4
const requireFn = this.id.startsWith('internal/deps/') ?
NativeModule.requireForDeps :
NativeModule.require;
fn(this.exports, requireFn, this, process)

总而言之,作为 native 模块的 loader,internal/bootstrap/loaders.js 依然可以看做是准备工作,主要负责原生模块的加载,那我们在项目中写的 JS 是怎么加载进来的呢?比如 server.js 是怎么被加载进去的呢?

真正要让 JS 代码运行起来,还需要 internal/bootstrap/node.js 的赞助:

1
2
3
4
5
(function bootstrapNodeJSCore(process,
{ _setupProcessObject, _setupNextTick, _setupPromises, ... },
{ internalBinding, NativeModule }) {
startup();
});

先忽略它第二个通过解构拿到的一坨参数组成的参数对象,我们看第三个参数 { internalBinding, NativeModule } 其实就是我们之前 internal/bootstrap/loaders.js 的 loaderExports 所传下来的参数,也就是 binding 能力和 NativeModule 的加载能力,有了这两个能力,我们看下它 startup() 所做的主要事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function startup() {
// 通过 NativeModule 拿到 cjs/loader 这个用来加载外部(用户)的 JS loader
const CJSModule = NativeModule.require('internal/modules/cjs/loader');
preloadModules();
// 调用 CJSModule 的 runMain 方法,让代码运行起来
CJSModule.runMain();
}

function preloadModules() {
const {
_preloadModules
} = NativeModule.require('internal/modules/cjs/loader');
_preloadModules(process._preload_modules);
}

startup();

那么接下来的事情,自然就发生在了 internal/modules/cjs/loader 里面了,其实我们可以这样来测试下调用栈,在本地写一个 test.js,里面放上:

1
require('./notexist.js')

我们调用一个不存在的 JS 模块, node test.js 跑一下,会报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 代码终止在了 583 行,抛出错误
internal/modules/cjs/loader.js:583
throw err;
^

Error: Cannot find module './notexist.js'
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:581:15)
at Function.Module._load (internal/modules/cjs/loader.js:507:25)
at Module.require (internal/modules/cjs/loader.js:637:17)
at require (internal/modules/cjs/helpers.js:20:18)
at Object.<anonymous> (/Users/black/Downloads/node-10.x/bind.js:1:75)
at Module._compile (internal/modules/cjs/loader.js:689:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
at Module.load (internal/modules/cjs/loader.js:599:32)
at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
at Function.Module._load (internal/modules/cjs/loader.js:530:3)

调用栈由下向上,依次调用,比如 require(‘./notexist.js’) 就是调用到了internal/modules/cjs/loader _load 方法,我们一一对应整理下来就是:

1
2
3
4
5
6
7
8
9
10
|- loader.js 530 行 _load
|- loader.js 538 行 tryModuleLoad
|- loader.js 599 行 load
|- loader.js 700 行 _extensions
|- loader.js 275 行 _compile
|- bind.js 1 行 匿名函数
|- helpers.js 20 行 require
|- loader.js 637 行 require
|- loader.js 507 行 _load
|- loader.js 581 行 _resolveFilename

具体代码的行数大家不用计较,因为 Node 版本不同,跟我们读的源码不一定能对上,但是函数名基本是可以对上的,来把 internal/modules/cjs/loader 代码精简一下,删减到了 50 行,其实跟 NativeModule 差不多,我们找到 Module.runMain 从上向下看:

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
function Module(id, parent) {
this.id = id;
this.exports = {};
}

Module.wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
Module.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];

Module.runMain = function() {
Module._load(process.argv[1], null, true);
};

Module._load = function(request, parent, isMain) {
var module = new Module(filename, parent);

tryModuleLoad(module, filename);
};

function tryModuleLoad(module, filename) {
module.load(filename);
}

Module.prototype.load = function(filename) {
Module._extensions['.js'](this, filename);
};

Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
};

Module.prototype._compile = function(content, filename) {
var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var require = makeRequireFunction(this);

compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
};

Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};

Module._resolveFilename = function(request, parent, isMain, options) {};

那么我们平时手写的 JS 代码,包括 node_modules 下的代码,就会被这个 CJS Loader 给接管了,拿到代码后的第一件事就是对它包裹一个函数表达式,传入一些变量,我们可以在 node 命令行模式下,输入 require('module').wrap.toString()require('module').wrapper 来查看到包裹的方法:

我们可以拿一段 webpack 源代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ChunkRenderError.js
const WebpackError = require('./WebpackError')

class ChunkRenderError extends WebpackError {
constructor(chunk, file, error) {
super()

this.name = 'ChunkRenderError'
this.error = error
this.message = error.message
this.details = error.stack
this.file = file
this.chunk = chunk
Error.captureStackTrace(this, this.constructor)
}
}

module.exports = ChunkRenderError

在 require 的时候,经过 CJS Loader 的编译,就编程了这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function (exports, require, module, __filename, __dirname) {
const WebpackError = require('./WebpackError')

class ChunkRenderError extends WebpackError {
constructor(chunk, file, error) {
super()

this.name = 'ChunkRenderError'
this.error = error
this.message = error.message
this.details = error.stack
this.file = file
this.chunk = chunk
Error.captureStackTrace(this, this.constructor)
}
}

module.exports = ChunkRenderError
})

于是我们很直观的得到两个结论:

  • internal/bootstrap/loaders.jsinternal/modules/cjs/loader 都是 Loader,但作用不同,前者是加载 Native 模块,后者加载我们项目中的 JS 模块,且后者依赖前者
  • 前者是非 CommonJS 的 Loader,后者是 CommonJS 的 Loader

扒了这一圈,我们就可以来回答文章开头的问题了:Node 里面的模块规范还是 CommonJS 么?

CommonJS 与 Node Modules

上面提到,虽然基于 CommonJS 来实现模块管理,但 Node 的 modules 体系演化至今,已经自成一套,跟 CommonJS 虽有大量血缘关系,但也确实有不同之处,最明显的,Node 里面 require('./index') 的依赖查找有后缀名的优先级,分别是 .js > .json > .node,同时一个模块的入口文件路径,是在 package.json 的 main 里面定义,以及 Node 里面依赖的模块统一在 node_modules 下面管理,这也是 Node 所独有的,还有其他比较大的差异之处,比如:

  1. CommonJS 为 require 定义了 main 和 paths 两个静态属性,而 Node 不支持 require.paths, 且暴露了额外的 cache 属性和 resolve() 方法,可以 node 命令行打印 require
  2. CommonJS 的 module 对象有 id 和 uri,而 Node 里面增加了 children/exports/filename/loaded/parent 属性,以及 require() 方法,它的接口通过 exports 和 module.exports 对外暴露,而在 CommonJS 里面,暴露模块 API 的唯一办法就是对 exports 对象增加方法或者属性,module.exports 在 CommonJS 里面不存在。

总而言之,就像 Node 社区所说,CommonJS is dead,Node 里的 modules 体系已经不再是严格意义的 CommonJS,只是大家对这个叫法习惯了,现在依然用 CommonJS 来代指 Node 里面的模块规范,而事实上,Node 社区的开发者已经抛弃 CommonJS 而去,只不过里面的大量血液仍源于 CommonJS。