xunm的前端blog

记录学习的点滴


  • Home

  • Archives

  • Tags

Submodule介绍

Posted on 2019-08-07

Nginx实践

Posted on 2019-07-28
  1. Nginx下载地址

  2. proxy_pass
    nginx反向代理主要通过proxy_pass来配置,将你项目的开发机地址填写到proxy_pass后面,正常的格式为proxy_pass URL即可

    1
    2
    3
    4
    5
    6
    server {
    listen 80;
    location / {
    proxy_pass http://10.10.10.10:20186;
    }
    }
  3. Upstream模块实现负载均衡

  • ip_hash指令

  • server指令

  • upstream指令及相关变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 修改nginx.conf
    worker_processes 1;
    events {
    worker_connections 1024;
    }
    http {
    upstream firstdemo {
    server 39.106.145.33;
    server 47.93.6.93;
    }
    server {
    listen 8080;
    location / {
    proxy_pass http://firstdemo;
    }
    }
    }
  • worker_processes

    • 工作进程数,和CPU核数相同
  • worker_connections

    • 每个进程允许的最大连接数
  • upstream模块

    • 负载均衡就靠它
    • 语法格式:upstream name {}
    • 里面写的两个server分别对应着不同的服务器
  • server模块

    • 实现反向代理
    • listen监督端口号
    • location / {}访问根路径
    • proxy_pass http://firstdemo,代理到firstdemo里两个服务器上
      上面修改了nginx.conf之后,别忘了最重要的一步重启nginx
      通过ip_hash使得用户第一次访问到其中一台服务器后,下次再访问的时候就直接访问该台服务器就好了
      1
      2
      3
      4
      5
      upstream firstdemo {
      ip_hash;
      server 39.106.145.33;
      server 47.93.6.93;
      }

ip_hash它的作用是如果第一次访问该服务器后就记录,之后再访问都是该服务器了,这样比如第一次访问是33服务器,那之后再访问也会分配为33服务器访问了

参考文章:
谁说前端不需要懂-Nginx反向代理与负载均衡

浏览器输入url到服务器响应全过程

Posted on 2019-07-23

浏览器输入url到服务器响应全过程

当在浏览器地址栏中输入一串域名“https://www.google.com”时候,都发生了什么呢?

  1. 首先浏览器会去缓存系统找是否有301永久重定向的缓存,如果有就直接跳转重定向的网址
  2. 如果未找到,就会去浏览器找缓存系统,即这个网页上次访问响应头设置了expires和max-age强制缓存
  3. 如果强制缓存未命中,会去DNS寻址,先去windows下 C:\Windows\System32\drivers\etc中hosts文件找对应域名是否配置相关ip地址映射,找到了就返回对应的ip地址
  4. 如果未找到DNS客户端会去DNS缓存系统中去找,找到了就返回对应的ip地址
  5. 没找到会去查询就近的DNS服务器,找到了就返回对应的ip地址,因为DNS服务器是分布式的数据库,所以就近的DNS服务器没找到对应的ip地址会去找更上层的DNS服务器,直到找到返回ip地址,未找到返回找不到网址
  6. DNS寻址拿到域名对应的ip地址过后,浏览器通过http协议三次握手与服务端建立tcp/ip连接
  7. http协议的三次握手过程大概是:
    TCP三次握手
  • 第一次握手:主机A发送位码为syn=1,随机产生seq number=0的数据包到服务器,主机B由syn=1知道,A要求建立联机;
  • 第二次握手:主机B收到请求后要确认联机信息,向A发送ack number=(主机A的seq+1),syn=1,ack=1,随机产生seq=7654321的包
  • 第三次握手:主机A收到后检查ack number是否正确,即第一次发送的seq number+1,以及位码ack是否为1,若正确,主机A会再发送ack number=(主机B的seq+1),ack=1,主机B收到后确认seq值与ack=1则连接建立成功。
    完成三次握手,主机A与主机B开始传送数据。

常见面试题

Posted on 2019-07-22

常见面试题

1. 如何实现ajax请求
  • 通过实例化一个XMLHttpRequest对象得到一个实例
  • 调用实例的open方法为这次 ajax请求设定相应的http方法、相应的地址和以及是否异步,当然大多数情况下我们都是选异步
  • 以异步为例,之后调用send方法ajax请求,这个方法可以设定需要发送的报文主体
  • 然后通过 监听readystatechange事件,通过这个实例的readyState属性来判断这个ajax请求的状态,其中分为0,1,2,3,4这四种 状态,当状态为4的时候也就是接收数据完成的时候,这时候可以通过实例的status属性判断这个请求是否成功
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var xhr = new XMLHttpRequest();
    xhr.open('get', 'aabb.php', true);
    xhr.send(null);
    xhr.onreadystatechange = function() {
    if(xhr.readyState==4) {
    if(xhr.status==200) {
    console.log(xhr.responseText);
    }
    }
    }
javascript有哪几种数据类型

六种基本数据类型

  • undefined
  • null
  • string
  • boolean
  • number
  • symbol(ES6)

一种引用数据类型

  • Object
    什么闭包,闭包有什么用
    闭包是在某个作用域内定义的函数,它可以访问这个作用域内的所有变量。闭包作用域链通常包括三个部分:
  1. 函数本身作用域。
  2. 闭包定义时的作用域。
  3. 全局作用域。

闭包常见用途:

  1. 创建特权方法用于访问控制
  2. 事件处理程序及回调

JavaScript有哪几种方法定义函数

  1. 函数声明
  2. 函数表达式
  3. ES6箭头函数
客户端存储localStorage和SessionStorage
  • localStorage有效期为永久,sessionStorage有效期为顶层窗口关闭前
  • 同源文档可以读取并修改localStorage数据,sessionStorage只允许同一个窗口下的文档访问,如通过iframe引入的同源文档。
  • Storage对象通常被当做普通javascript对象使用:通过设置属性来存取字符串值,Storage对象的API:setItem(key, value)设置,getItem(key)读取,removeItem(key)删除,clear()删除所有数据,length表示已存储的数据项数目,key(index)返回对应索引的key
    1
    2
    3
    4
    5
    // 枚举所有存储的键值对
    for (var i = 0, len = localStorage.length; i < len; ++i ) {
    var name = localStorage.key(i);
    var value = localStorage.getItem(name);
    }
cookie及其操作
  • cookie是web浏览器存储的少量数据,最早设计为服务器端使用,作为HTTP协议的扩展出现。cookie数据会自动在浏览器和服务器之间传输。
  • 通过读写cookie检测是否支持
  • document.cookie setItem参数有key,value,max-age,path,domain
    link与@import的区别
  1. link是HTML方式, @import是CSS方式
  2. link最大限度支持并行下载,@import过多嵌套导致串行下载,出现FOUC
  3. link可以通过rel=”alternate stylesheet”指定候选样式
  4. 浏览器对link支持早于@import,可以使用@import对老浏览器隐藏样式
  5. @import必须在样式规则之前,可以在css文件中引用其他文件
  6. 总体来说:link优于@import
MVVM

mvvm由以下三个内容组成

  • View:界面
  • Model:数据模型
  • ViewModel:作为桥梁负责沟通View和Model

在jQuery时期,如果需要刷新UI时,需要先取到对应的DOM再更新UI,这样数据和业务的逻辑就和页面有强耦合。
在MVVM中,UI是通过数据驱动的,数据一旦改变就会相应的刷新对应的UI,UI如果改变,也会改变对应的数据。这种方式就可以在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel只关心数据和业务的处理,不关心View如何处理数据,这种情况下,View和Model都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个ViewModel中,让多个View复用这个ViewModel。
在MVVM中,最核心的也就是数据双向绑定,例如Angluar的脏数据检测,Vue中的数据劫持。

脏数据检测
数据劫持

Vue内部使用了Object.definpropty() 中的get和set方法监听数据。
在解析模板代码时,会实例化属性的watcher对象,并且把它挂在到Dep.target属性上。
通过触发属性的get取值函数把相关的watcher实例添加到dep的subs属性中。
当数据发生变化时,触发属性的set存值函数,调用dep对象的notify方法,主要内容是遍历相关的watcher实例,调用其对应的update方法,更新视图。

面向过程和面向对象的区别?

面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个调用就可以了。
面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。

箭头函数表达式和普通函数的区别

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或 new.target。
更短的函数、不绑定this、通过 call 或 apply 调用第一个参数忽略、不绑定arguments、箭头函数不能用作构造器,和 new一起用会抛出错误、箭头函数没有prototype属性、箭头函数不能用作生成器

vdom是什么?为何会存在vdom?

不用vdom遇到的问题:

  • DOM操作是“昂贵”的,js运行效率高
  • 尽量减少DOM操作,而不是“推到重来”
  • 项目越复杂,影响就越严重
  • vdom即可解决这个问题
    问题解答:
  • virtual dom,虚拟DOM
  • 用JS模拟DOM结构
  • DOM操作非常“昂贵”
  • 将DOM对比操作放在JS层,提高效率
    vdom如何应用,核心API是什么?
  • h(‘<标签名>’, {…属性…}, […子元素…])
  • h(‘<标签名>’, {…属性…}, ‘…’)
  • patch(container, vnode)
  • patch(vnode, newVnode)
    问题解答:
  • 如何使用?可用snabbdom的用法来举例
  • 核心API:h函数返回vnode、patch函数打补丁
    介绍一下diff算法?
    问题解答:
  • 知道什么是diff算法,是linux的基础命令
  • vdom中应用diff算法是为了找出要更新的节点
  • diff实现,patch(container, vnode) patch(vnode, newVnode)
  • 核心逻辑,createElement和updateChildren
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function createElement(vnode) {
    var tag = vnode.tag
    var attrs = vnode.attrs || {}
    var children = vnode.children || []
    if (!tag) {
    return null;
    }
    // 创建元素
    var elem = document.createElement(tag)
    // 属性
    var attrName
    for (attrName in attrs) {
    if (attrs.hasOwnProperty(attrName)) {
    elem.setAttribute(attrName, attrs[attrName])
    }
    }
    // 子元素
    children.forEach(function(childVnode) {
    // 递归调用createElement创建子元素
    elem.appendChild(createElement(childVnode))
    })
    return elem;
    }

vuex源码注释

Posted on 2019-07-13
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
import applyMixin from "./mixin";
import devtoolPlugin from "./plugins/devtool";
import ModuleCollection from "./module/module-collection";
import { forEachValue, isObject, isPromise, assert, partial } from "./util";

let Vue; // bind on install

export class Store {
constructor(options = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
// To allow users to avoid auto-installation in some cases,
// this code should be placed here. See #731
if (!Vue && typeof window !== "undefined" && window.Vue) {
install(window.Vue);
}

if (process.env.NODE_ENV !== "production") {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`);
assert(
typeof Promise !== "undefined",
`vuex requires a Promise polyfill in this browser.`
);
assert(
this instanceof Store,
`store must be called with the new operator.`
);
}

const { plugins = [], strict = false } = options;

// store internal state
this._committing = false;
this._actions = Object.create(null);
this._actionSubscribers = [];
this._mutations = Object.create(null);
this._wrappedGetters = Object.create(null);
this._modules = new ModuleCollection(options);
this._modulesNamespaceMap = Object.create(null);
this._subscribers = [];
this._watcherVM = new Vue();

// bind commit and dispatch to self
const store = this;
const { dispatch, commit } = this;
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload);
};
this.commit = function boundCommit(type, payload, options) {
return commit.call(store, type, payload, options);
};

// strict mode
this.strict = strict;

const state = this._modules.root.state;

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root);

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state);

// apply plugins
plugins.forEach(plugin => plugin(this));

const useDevtools =
options.devtools !== undefined ? options.devtools : Vue.config.devtools;
if (useDevtools) {
devtoolPlugin(this);
}
}

get state() {
return this._vm._data.$$state;
}

set state(v) {
if (process.env.NODE_ENV !== "production") {
assert(
false,
`use store.replaceState() to explicit replace store state.`
);
}
}

commit(_type, _payload, _options) {
// check object-style commit
const { type, payload, options } = unifyObjectStyle(
_type,
_payload,
_options
);

const mutation = { type, payload };
const entry = this._mutations[type];
if (!entry) {
if (process.env.NODE_ENV !== "production") {
console.error(`[vuex] unknown mutation type: ${type}`);
}
return;
}
this._withCommit(() => {
entry.forEach(function commitIterator(handler) {
handler(payload);
});
});
this._subscribers.forEach(sub => sub(mutation, this.state));

if (process.env.NODE_ENV !== "production" && options && options.silent) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
"Use the filter functionality in the vue-devtools"
);
}
}

dispatch(_type, _payload) {
// check object-style dispatch
const { type, payload } = unifyObjectStyle(_type, _payload);

const action = { type, payload };
const entry = this._actions[type];
if (!entry) {
if (process.env.NODE_ENV !== "production") {
console.error(`[vuex] unknown action type: ${type}`);
}
return;
}

try {
this._actionSubscribers
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state));
} catch (e) {
if (process.env.NODE_ENV !== "production") {
console.warn(`[vuex] error in before action subscribers: `);
console.error(e);
}
}

const result =
entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload);

return result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state));
} catch (e) {
if (process.env.NODE_ENV !== "production") {
console.warn(`[vuex] error in after action subscribers: `);
console.error(e);
}
}
return res;
});
}

subscribe(fn) {
return genericSubscribe(fn, this._subscribers);
}

subscribeAction(fn) {
const subs = typeof fn === "function" ? { before: fn } : fn;
return genericSubscribe(subs, this._actionSubscribers);
}

watch(getter, cb, options) {
if (process.env.NODE_ENV !== "production") {
assert(
typeof getter === "function",
`store.watch only accepts a function.`
);
}
return this._watcherVM.$watch(
() => getter(this.state, this.getters),
cb,
options
);
}

replaceState(state) {
this._withCommit(() => {
this._vm._data.$$state = state;
});
}

/**
* @description
* 注册模块 `myModule`
* store.registerModule('myModule', {})
* 注册嵌套模块 `nested/myModule`
* store.registerModule(['nested', 'myModule'], {})
* 之后就可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态
*/
registerModule(path, rawModule, options = {}) {
if (typeof path === "string") path = [path];
// 对参数做断言,Array.isArray(path) && path.length > 0为真
if (process.env.NODE_ENV !== "production") {
assert(Array.isArray(path), `module path must be a string or an Array.`);
assert(
path.length > 0,
"cannot register the root module by using registerModule."
);
}
// 调用this._modules.register注册模块的方法
this._modules.register(path, rawModule);
installModule(
this,
this.state,
path,
this._modules.get(path),
options.preserveState
);
// 重置store更新getters...
resetStoreVM(this, this.state);
}

unregisterModule(path) {
if (typeof path === "string") path = [path];

if (process.env.NODE_ENV !== "production") {
assert(Array.isArray(path), `module path must be a string or an Array.`);
}
// 卸载模块
this._modules.unregister(path);
this._withCommit(() => {
const parentState = getNestedState(this.state, path.slice(0, -1));
Vue.delete(parentState, path[path.length - 1]);
});
// 重置store
resetStore(this);
}

hotUpdate(newOptions) {
this._modules.update(newOptions);
resetStore(this, true);
}

_withCommit(fn) {
const committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
}
}

function genericSubscribe(fn, subs) {
if (subs.indexOf(fn) < 0) {
subs.push(fn);
}
return () => {
const i = subs.indexOf(fn);
if (i > -1) {
subs.splice(i, 1);
}
};
}

function resetStore(store, hot) {
store._actions = Object.create(null);
store._mutations = Object.create(null);
store._wrappedGetters = Object.create(null);
store._modulesNamespaceMap = Object.create(null);
const state = store.state;
// init all modules
installModule(store, state, [], store._modules.root, true);
// reset vm
resetStoreVM(store, state, hot);
}

function resetStoreVM(store, state, hot) {
const oldVm = store._vm;

// bind store public getters
store.getters = {};
const wrappedGetters = store._wrappedGetters;
const computed = {};
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure enviroment.
computed[key] = partial(fn, store);
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
});
});

// use a Vue instance to store the state tree
// suppress warnings just in case the user has added
// some funky global mixins
const silent = Vue.config.silent;
Vue.config.silent = true;
store._vm = new Vue({
data: {
$$state: state
},
computed
});
Vue.config.silent = silent;

// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store);
}

if (oldVm) {
if (hot) {
// dispatch changes in all subscribed watchers
// to force getter re-evaluation for hot reloading.
store._withCommit(() => {
oldVm._data.$$state = null;
});
}
Vue.nextTick(() => oldVm.$destroy());
}
}

// 递归安装模块
function installModule(store, rootState, path, module, hot) {
// 如果path是[],则是跟组件
const isRoot = !path.length;
// 获取名字空间
const namespace = store._modules.getNamespace(path);
// 注册名字空间集合
if (module.namespaced) {
// 安装命名空间时发现有这个命名空间了,则报错重复命名空间
if (
store._modulesNamespaceMap[namespace] &&
process.env.NODE_ENV !== "production"
) {
console.error(
`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
"/"
)}`
);
}
// 这里以path是['nested', 'myModule']举例,
// 就是store._modulesNamespaceMap['nested/myModule'] = new Module({state: {}})
store._modulesNamespaceMap[namespace] = module;
}

// 不是根模块时,设置状态
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1));
const moduleName = path[path.length - 1];
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state);
});
}

const local = (module.context = makeLocalContext(store, namespace, path));

module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key;
registerMutation(store, namespacedType, mutation, local);
});

module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key;
const handler = action.handler || action;
registerAction(store, type, handler, local);
});

module.forEachGetter((getter, key) => {
const namespacedType = namespace + key;
registerGetter(store, namespacedType, getter, local);
});

module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot);
});
}

/**
* make localized dispatch, commit, getters and state
* if there is no namespace, just use root ones
*/
function makeLocalContext(store, namespace, path) {
const noNamespace = namespace === "";

const local = {
dispatch: noNamespace
? store.dispatch
: (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options);
const { payload, options } = args;
let { type } = args;

if (!options || !options.root) {
type = namespace + type;
if (
process.env.NODE_ENV !== "production" &&
!store._actions[type]
) {
console.error(
`[vuex] unknown local action type: ${
args.type
}, global type: ${type}`
);
return;
}
}

return store.dispatch(type, payload);
},

commit: noNamespace
? store.commit
: (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options);
const { payload, options } = args;
let { type } = args;

if (!options || !options.root) {
type = namespace + type;
if (
process.env.NODE_ENV !== "production" &&
!store._mutations[type]
) {
console.error(
`[vuex] unknown local mutation type: ${
args.type
}, global type: ${type}`
);
return;
}
}

store.commit(type, payload, options);
}
};

// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
});

return local;
}

function makeLocalGetters(store, namespace) {
const gettersProxy = {};

const splitPos = namespace.length;
Object.keys(store.getters).forEach(type => {
// skip if the target getter is not match this namespace
if (type.slice(0, splitPos) !== namespace) return;

// extract local getter type
const localType = type.slice(splitPos);

// Add a port to the getters proxy.
// Define as getter property because
// we do not want to evaluate the getters in this time.
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
});
});

return gettersProxy;
}

function registerMutation(store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = []);
entry.push(function wrappedMutationHandler(payload) {
handler.call(store, local.state, payload);
});
}

function registerAction(store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = []);
entry.push(function wrappedActionHandler(payload, cb) {
let res = handler.call(
store,
{
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
},
payload,
cb
);
if (!isPromise(res)) {
res = Promise.resolve(res);
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit("vuex:error", err);
throw err;
});
} else {
return res;
}
});
}

function registerGetter(store, type, rawGetter, local) {
if (store._wrappedGetters[type]) {
if (process.env.NODE_ENV !== "production") {
console.error(`[vuex] duplicate getter key: ${type}`);
}
return;
}
store._wrappedGetters[type] = function wrappedGetter(store) {
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
);
};
}

function enableStrictMode(store) {
store._vm.$watch(
function() {
return this._data.$$state;
},
() => {
if (process.env.NODE_ENV !== "production") {
assert(
store._committing,
`do not mutate vuex store state outside mutation handlers.`
);
}
},
{ deep: true, sync: true }
);
}

// 获取嵌套状态
function getNestedState(state, path) {
return path.length ? path.reduce((state, key) => state[key], state) : state;
}

// 统一对象样式
function unifyObjectStyle(type, payload, options) {
if (isObject(type) && type.type) {
options = payload;
payload = type;
type = type.type;
}

if (process.env.NODE_ENV !== "production") {
assert(
typeof type === "string",
`expects string as the type, but found ${typeof type}.`
);
}

return { type, payload, options };
}

// call Vue.use(vuex) 安装vuex
export function install(_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== "production") {
console.error(
"[vuex] already installed. Vue.use(Vuex) should be called only once."
);
}
return;
}
Vue = _Vue;
applyMixin(Vue);
}

node网站部署

Posted on 2019-07-10

node网站部署

1.安装nvm (文档 https://github.com/nvm-sh/nvm;https://www.jianshu.com/p/e21e3783304f)

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.34.0/install.sh | bash
安装链接可能不是最新,具体以官方文档为准

2.安装node指定版本

nvm install 8.11.3
nvm use 8

3.安装cnpm (npm淘宝镜像)

npm install -g cnpm –registry=https://registry.npm.taobao.org

4.安装pm2

npm install -g pm2

5.跳转至项目目录(本文以/home/www/node/yhfhyy为例)安装所需依赖

cd /home/www/node/yhfhyy
cnpm i
cnpm i nuxt (新增nuxt服务端渲染框架)

6.编辑pm2.json

vim pm2.json
将cmd 属性 改为项目实际路径;如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"apps": [
{
"name": "yhfhyy",
"script": "production.js",
"cwd": "/home/www/node/yhfhyy",
"exec_mode": "fork",
"max_memory_restart": "1G",
"autorestart": true,
"node_args": [],
"args": [],
"env": {
"NODE_ENV": "production"
},
"watch": [
"client",
"services"
]
}
]
}

7.设置启动端口

vim src/config/config.js
设置 port 属性不冲突即可(没有就新增,有就修改,默认值为8360)

1
2
3
4
module.exports = {
workers: 1,
port: '8450'
};

8.启动

pm2 start pm2.json

9.nginx 代理设置

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
server {
listen 80;
server_name www.xxx.com;
root /usr/share/nginx/node/bf.limingwei.name;

#node端口号
set $node_port 8450;

index index.js index.html index.htm;

location / {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:$node_port$request_uri;
proxy_redirect off;
}

location ~ /static/ {
root /usr/share/nginx/node/bf.limingwei.name/www/;
add_header Cache-Control no-store;
}
}

JSONP原理

Posted on 2019-06-30

JSONP原理

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
var count = 0;
function noop() {}
function jsonp(url, opts, fn) {
// 容错,第二个参数传函数的情况
if (typeof opts === 'function') {
fn = opts;
opts = {};
}
if (!opts) opts = {};
var prefix = opts.prefix || '__jp';
// 使用提供的回调名称,否则通过递增我们的计数器来生成唯一的名称
var id = opts.name || (prefix + (count++));
var param = opts.param || 'callback';
var timeout = null != opts.timeout ? opts.timeout: 60000;
var enc = encodeURIComponent;
var target = document.getElementsByTagName('script')[0] || document.head;
var script;
var timer;
if (timeout) {
timer = setTimeout(function() {
cleanup();
if (fn) fn(new Error('Timeout'));
}, timeout);
}
function cleanup() {
if (script.parentNode) script.parentNode.removeChilde(script);
window[id] = noop;
if (timer) clearTimeout(timer);
}
function cancel() {
if (window[id]) {
cleanup();
}
}
window[id] = function(data) {
cleanup()
if (fn) fn(null, data);
}

url += (~url.indexOf('?') ? '&': '?') + param + '=' + enc(id);
url = url.replace('?&', '?');
// create script
script = document.createElement('script');
script.src = url;
target.parentNode.insertBefore(script, target);
return cancel;
}

发布订阅模块实现

Posted on 2019-06-12

发布订阅模式

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>event demo</title>
</head>
<body>
<script src="./event.js"></script>
<script>
const event = new EventEmitter();
/************先发布后订阅************/
event.trigger('click', 1);
event.listen('click', function(a) {
console.log(a, '--------------'); // 输出:1
});
/************先订阅后发布************/
event.listen('sayhello', function(a) {
console.log(a); // 输出:1
});
event.trigger('sayhello', 'hello');
/************使用命名空间************/
event.create('namespace1').listen('click', function(a) {
console.log(a); // 输出:namespace1
})
event.create('namespace1').trigger('click', 'namespace1');

event.create('namespace2').listen('click', function(a) {
console.log(a); // 输出:namespace2
})
event.create('namespace2').trigger('click', 'namespace2');
</script>
</body>
</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
class EventEmitter {
constructor() {
this.namespaceCache = {};
this.offlineStack= [];
this.cache = {};
}
_each(ary, fn) {
let result;
let n;
let i = ary.length;
while ((n = ary[--i])) {
result = fn.call(n, i, n);
}
return result;
}
_listen(key, fn) {
if (!this.cache[key]) this.cache[key] = [];
this.cache[key].push(fn);
}
_trigger(key, ...args) {
const stack = this.cache[key];
if (!stack || !stack.length) return;
return this._each(stack, function() {
return this(...args);
});
}
_remove(key, fn) {
if (!this.cache[key]) return;
if (!fn) this.cache[key] = [];
let i = this.cache[key].length;
while (this.cache[key][--i]) {
if (this.cache[key][i] === fn) {
this.cache[key].splice(i, 1);
}
}
}
create(namespace = "default") {
return this.namespaceCache[namespace]
? this.namespaceCache[namespace]
: (this.namespaceCache[namespace] = new EventEmitter());
}
remove(key, fn) {
this._remove(key, fn);
}
trigger(...args) {
const fn = () => {
return this._trigger(...args);
};
if (this.offlineStack) {
return this.offlineStack.push(fn);
}
return fn();
}
listen(key, fn, last) {
this._listen(key, fn);
if (this.offlineStack === null) return;
if (last === "last") {
this.offlineStack.length && this.offlineStack.pop()();
} else {
this._each(this.offlineStack, function() {
this();
});
}
this.offlineStack = null;
}
one(key, fn, last) {
this._remove(key);
this.listen(key, fn, last);
}
}

盘点那些年踩过的坑

Posted on 2019-06-10

盘点那些年踩过的坑(>﹏<)

前言


最近在找工作,所有想趁现在不是太忙复盘下以前踩过的坑,也是为了提醒自己不要在这些问题上迷糊,提高自己的工作效率。因为是第一次写,所有难免写得不好,望各位大哥,小姐姐轻拍砖,有问题的地方也请不吝赐教,感谢。


1. 微信h5页面中打开本地app,如果没有跳转下载页面

详细解决方案: H5 唤醒APP小记

2. 点击时,屏幕键盘遮挡输入

Element.scrollIntoView()方法将调用它的元素滚动到浏览器窗口的可见区域

3. 进入页面自动聚焦输入框,并弹出软键盘
4. 安卓手机不支持手机批量上传图片

Android端通过input框focus()方法,setTimeOut,trigge()方法勉强能实现需求,iOS端不行,最后我采用的方法是在上一个页面点击跳转之前触发focus方法(),这里的弹出软键盘是动作带来的就解决了这个问题。

5. iOS系统手机拍照图片Canvas压缩上传后图片旋转的bug

使用到了EXIF.js插件,它会读取图片的信息,然后返回一个值,这个值就是图片的旋转位置,通过修正图片的旋转角度最后在调用Canvas的toDataURL方法压缩图片,微信公众号也可以直接调用JSSDK避开这个问题。
详细解决方案: 解决IOS系统手机拍照图片Canvas压缩上传后图片旋转的bug

6. 微信小程序利用canvas生成海报分享图片

详细解决方案:微信小程序利用canvas生成海报分享图片

7. webpack性能优化
  • 图片资源优化

    • url-loader
    • file-loader
    • image-webpack-loader
  • happypack

    8.老生常谈的跨域问题
  • JSONP跨域(原理是利用script脚本src属性可以跨域的特性)
    详细解决方案:JSONP 教程
    JSONP的缺点:JSONP不提供错误处理。如果动态插入的代码正常运行,你可以得到返回,但是如果失败了,那么什么都不会发生。

  • Electron项目接口出现了403问题,file协议跨域
    打包成web版本,通过本地electron加载加载远程资源的方案绕过跨域限制。这个方案应该有问题,应该是在Electron的BrowserWindow模块中配置webSecurity:false解决。

    1
    2
    3
    4
    5
    6
    7
    mainWindow = new BrowserWindow({
    //...
    webPreferences: {
    webSecurity: false,
    //...
    },
    });

    详细解决方案:electron如何跨域?electron 跨域方案之禁用 webSecurity

    9. 角色权限控制

    通过vue-router的addRoutes API动态添加路由配置,解决了侧边栏菜单渲染的问题,
    功能点的权限控制由后端通过前端传token去判断用户的权限,通过添加路由的全局导航守卫拦截页面,控制访问权限。
    详细解决方案:手摸手,带你用vue撸后台 系列二(登录权限篇)

    10. 真机调试方案

    我在工作中使用过的有:Fiddler、TBS、微信开发者工具

JavaScript设计模式与开发实践笔记

Posted on 2019-05-28

JavaScript设计模式与开发实践笔记

第16章 状态模式

状态模式的关键是区分事物内部的状态,事物内部的改变往往会带来事物的行为改变。

场景
有一个电灯,电灯上面只有一个开关。当电灯开着的时候,此时按下开关,电灯会切换到关闭状态;再按一次开关,电灯又被打开。同一个开关按钮,不同状态下表现的行为不一样。

现在用代码描述这个场景,Light代表一个电灯类,light是从Light创建出的实例,且拥有两个属性,用state记录电灯的状态,用button记录开关按钮。

16.1.1 第一个示例:电灯程序

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
class Light {
constructor() {
this.state = 'off';
this.button = null;
}
// 接下来定义init方法,该方法负责在页面创建一个真实的button节点,假设这个button是电灯开关按钮,
// 当button的onclick事件被触发时,就是电灯开关被按下的时候,代码如下:
init() {
const button = document.createElement('button');
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
this.button.addEventListener('click', this::buttonWasPressed)
}
// 当开关被按下时,程序会调用this.buttonWasPressed方法,开关按下之后的所有行为,都被封装在这个方法里,代码如下:
buttonWasPressed() {
if (this.state === 'off') {
console.log('开灯');
this.state = 'on';
} else if (this.state === 'on') {
console.log('关灯');
this.state = 'off';
}
}
}
const light = new Light();
light.init();

上面的代码虽然设计得很好,逻辑既简单又缜密,但是不利于扩展。想想世界上灯泡并非一种,如果引进新的灯泡就需要改动原先的代码。综述上面的例子犯了一下缺点:

  1. 很明显buttonWasPressed方法是违反开放-封闭原则的,每次新增或者修改light的状态,都需要改动buttonWasPressed方法中的代码,这使得buttonWasPressed成为了一个非常不稳定的方法。
  2. 所以跟状态有关的行为,都被封装在buttonWasPressed方法里,如果以后电灯又增加了强光,超强光,终极强光,那我们无法预料到这个方法将膨胀到什么地方。
  3. 状态的切换非常不明显,仅仅表现为对state变量的赋值,比如this.state='on'
  4. 状态之间的切换关系,不过是往buttonWasPressed方法里堆砌if、else语句,增加或者修改了一个状态可能需要改变若干个操作,这使得buttonWasPressed更加难以阅读和维护。

16.1.2 状态模式改进电灯程序

通常我们谈到封装,一般会优先封装对象的行为,而不是对象的状态。但在状态模式中刚好相反,状态模式的关键是把事物的每种状态都封装成单独的类,跟此种状态有关的行为都被封装在这个类的内部,所有button被按下的时候,只需要在上下文中,把这个请求委托给当前的状态对象即可,该状态对象会负责渲染它自身的行为。

下面进入状态莫斯的代码编写阶段,首先将定义3个状态类,分别是OffLightState、WeakLightState、StrongLightState。这三个类都有一个原型方法buttonWasPressed,代表在各自状态下,按钮被按下时将发生的行为,代码如下:

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
// OffLightState 
class OffLightState {
constructor(light) {
this.light = light;
}
buttonWasPressed() {
// offLightState对应的行为
console.log('弱光');
// 切换状态到weakLightState
this.light.setState(this.light.weakLightState);
}
}
// weakLightState
class WeakLightState {
constructor(light) {
this.light = light;
}
buttonWasPressed() {
// weakLightState对应的行为
console.log('强光');
// 切换状态到strongLightState
this.light.setState(this.light.strongLightState);
}
}
class StrongLightState {
constructor(light) {
this.light = light;
}
buttonWasPressed() {
// strongLightState对应的行为
console.log('关灯');
// 切换状态到offLightState
this.light.setState(this.light.strongLightState);
}
}

接下来改写Light类,现在不再使用一个字符串来记录当前的状态,而是使用更加立体化的状态对象。我们在Light类的构造函数里为每个状态类都创建一个状态对象,这样一来我们可以很明显地看到电灯一共有多少种状态,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Light {
constructor() {
this.offLightState = new OffLightState(this);
this.weakLightState = new WeakLightState(this);
this.strongLightState = new StrongLightState(this);
this.button = null;
}
// 在button按钮被按下的事件里,Context也不再直接进行任何实质性的操作,而是通过this.currState::buttonWasPressed()将请求委托给当前持有的状态对象去执行
init() {
const button = document.createElement('button');
button.innerHTML = '开关';
this.button = document.body.appendChild(button);
// 设置当前状态
this.currState = this.offLightState;
this.button.addEventListener('click', this.currState::buttonWasPressed)
}
// 通过这个方法来切换light对象的状态。
setState(newState) {
this.currState = newState;
}
}
// 现在可以进行一些测试:
const light = new Light();
light.init();

使用状态模式的好处是可以使每一种状态和它对应的行为之间的关系局部化,这些行为被分散和封装在各自对应的状态类中,便于阅读和管理代码。

16.2 状态模式的定义

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

我们以逗号分隔,把这句话分为两部分来看。第一部分的意思是将状态封装成独立的类,并将请求委托给当前的状态对象,当对象的内部状态改变时,会带来不同的行为变化。电灯的例子足以说明这一点,在off和on这两种不同的状态下,我们点击同一个按钮,得到的行为反馈是截然不同的。
第二部分是从客户的角度来看,我们使用的对象,在不同的状态下具有截然不同的行为,这个对象看起来是从不同的类中实例化来的,实际上这是使用了委托的效果。

16.3 状态模式的通用结构

16.4 缺少抽象类的变通方式

定义一个State的抽象父类,把共同的功能放进抽象父类中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class State {
constructor(light) {
this.light = light;
}
buttonWasPressed() {
throw new Error('父类的buttonWasPressed方法必须被重写')
}
}
class SuperStrongLightState extends State {
buttonWasPressed() {
console.log('关灯');
this.light.setState(this.light.offLightState);
}
}

16.5 另一个状态模式示例—文件上传

16.5.1 更复杂的切换条件

相对于电灯的例子,文件上传不同的地方在于,现在我们将面临更加复杂的条件切换关系。在电灯的例子中,电灯的状态总是从关到开再到关,或者从关到弱光、弱光到强光,强光再到关。看起来总是遵循规矩的A->B->C->A,所以即使不使用状态模式来编写电灯的程序,而是使用原始的if、else来控制状态的切换,我们也不至于在逻辑编写中迷失自己,因为状态的切换总死遵循一些简单的规律,而文件上传的状态切换要复杂得多,控制文件上传的流程需要两个节点,第一个用于暂停和继续上传,第二个用于删除文件。

现在看看文件在不同的状态下,点击这两个按钮将分别发生什么行为。

  1. 文件在扫描状态中,是不能进行任何操作的,既不能暂停也不能删除文件,只能等待扫描完成。扫描完成之后,根据文件的md5值判断,若确认该文件已经文在于服务器,则直接跳到上传完成状态。如果该文件的大小超过允许上传的最大值,或者该文件已经损坏,则跳转到上传失败状态。剩下的情况才进入上传中状态。
  2. 上传过程中可以点击暂停按钮来暂停上传,暂停后点击同一个按钮会继续上传。
  3. 扫描和上传过程中,点击删除按钮无效,只有在暂停、上传完成、上传失败之后,才能删除文件。

16.5.2 一些准备工作

上传是一个异步过程,所有控件会不停的调用JavaScript提供的一个全局函数window.external.upload,来通知JavaScript的上传进度,把控件当前的文件状态作为参数state塞进window.external.upload。setTimeout负责模拟上传进度,window.external.upload在此例中只负责打印log:

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
window.external.upload = function(state) {
// 可能为sign、uploading、done、error
console.log(state);
}
// 另外我们需要在页面放置一个用于上传的插件对象:
const plugin = (function() {
const plugin = document.createElement('embed');
plugin.style.display = 'none';
plugin.type = 'application/txftn-wbkit';
plugin.sign = function() {
console.log('开始文件扫描');
}
plugin.pause = function() {
console.log('暂停文件扫描');
}
plugin.uploading = function() {
console.log('开始文件上传');
}
plugin.del = function() {
console.log('删除文件上传');
}
plugin.done = function() {
console.log('文件上传完成');
}
document.body.appendChild(plugin);
return plugin;
})();

16.5.3 开始编写代码

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
class Upload {
constructor(fileName) {
this.plugin = plugin;
this.fileName = fileName;
this.button1 = null;
this.button2 = null;
// 设置初始状态为waiting
this.state = 'sign';
}
// init方法会进行写初始化工作,包括创建页面中的一些节点。在这些节点里,起主要作用的是两个用于控制上传流程的按钮,第一个按钮用于暂停和继续上传,第二个按钮用于删除文件:
init() {
this.dom = document.createElement('div');
this.dom.innerHTML =
`<span>文件名称:${this.fileName}</span>\
<button data-action="button1">扫描中</button>\
<button data-action="button2">删除</button>`;
document.body.appendChild(this.dom);
// 第一个按钮
this.button1 = this.dom.querySelector('[data-action="button1"]');
// 第二个按钮
this.button2 = this.dom.querySelector('[data-action="button2"]');
this.bindEvent();
}
// 接下来需要给两个按钮分别绑定点击事件
bindEvent() {
this.button1.addEventListener('click', () => {
// 扫描状态下,任何操作无效
if (this.state === 'sign') {
console.log('扫描中,点击无效...');
}
// 上传中,点击切换到暂停
else if (this.state === 'uploading') {
this.changeState('pause');
}
// 暂停中,点击切换到上传中
else if (this.state === 'pause') {
this.changeState('uploading');
}
else if (this.state === 'done') {
console.log('文件已完成上传,点击无效');
}
else if (this.state === 'error') {
console.log('文件上传失败,点击无效');
}
});
this.button2.addEventListener('click', () => {
if (this.state === 'done' || this.state === 'error' || this.state === 'pause') {
// 上传完成、上传失败和暂停状态下可以删除
this.changeState('del');
}
else if (this.state === 'sign') {
console.log('文件正在扫描中,不能删除');
}
else if (this.state === 'uploading') {
console.log('文件正在上传中,不能删除');
}
});
}
changeState(state) {
switch(state) {
case 'sign':
this.plugin.sign();
this.button1.innerHTML = '扫描中,任何操作无效';
break;
case 'uploading':
this.plugin.uploading();
this.button1.innerHTML = '正在上传,点击暂停';
break;
case 'pause':
this.plugin.pause();
this.button1.innerHTML = '已暂停,点击继续上传';
break;
case 'done':
this.plugin.done();
this.button1.innerHTML = '上传完成';
break;
case 'error':
this.button1.innerHTML = '上传失败';
break;
case 'del':
this.plugin.del();
this.dom.parentNode.removeChild(this.dom);
console.log('删除完成');
break;
}
this.state = state;
}
}
// 测试代码:
const uploadObj = new Upload('JavaScript设计模式与开发实践');
uploadObj.init();
// 插件调用JavaScript方法
window.external.upload = function(state) {
uploadObj.changeState(state);
}
// 模拟上传进度
// 文件开始扫描
window.external.upload('sign');
// 1秒后开始上传
setTIimeout(function() {
window.external.upload('uploading');
}, 1000);
// 5秒后上传完成
etTIimeout(function() {
window.external.upload('done');
}, 5000);

16.5.4 状态模式重构文件上传程序

第一步仍然是提供window.external.upload函数,在页面中模拟创建上传插件,这部分代码没有改变:

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
window.external.upload = function(state) {
// 可能为sign、uploading、done、error
console.log(state);
}
const plugin = (function() {
const plugin = document.createElement('embed');
plugin.style.display = 'none';
plugin.type = 'application/txftn-wbkit';
plugin.sign = function() {
console.log('开始文件扫描');
}
plugin.pause = function() {
console.log('暂停文件扫描');
}
plugin.uploading = function() {
console.log('开始文件上传');
}
plugin.del = function() {
console.log('删除文件上传');
}
plugin.done = function() {
console.log('文件上传完成');
}
document.body.appendChild(plugin);
return plugin;
})();

第二部,改造Upload构造函数,在构造函数中为每种状态子类都创建一个实例对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Upload {
constructor(fileName) {
this.plugin = plugin;
this.fileName = fileName;
this.button1 = null;
this.button2 = null;
// 设置初始状态为waiting
this.signState = new SignState(this);
this.uploadingState = new UpladState(this);
this.pauseState = new PauseState(this);
this.doneState = new DoneState(this);
this.errorState = new ErrorState(this);
// 设置当前状态
this.currState = this.signState;
}
}

第三步,init方法无需改变,仍然负责往页面中创建跟上传流程有关的DOM节点,并开始绑定按钮的事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
init() {
this.dom = document.createElement('div');
this.dom.innerHTML =
`<span>文件名称:${this.fileName}</span>\
<button data-action="button1">扫描中</button>\
<button data-action="button2">删除</button>`;
document.body.appendChild(this.dom);
// 第一个按钮
this.button1 = this.dom.querySelector('[data-action="button1"]');
// 第二个按钮
this.button2 = this.dom.querySelector('[data-action="button2"]');
this.bindEvent();
}

第四步,负责具体的按钮事件实现,在点击了按钮之后,Context并不做任何具体的操作,而是把请求委托给当前的状态类来执行:

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
bindEvent() {
this.button1.addEventListener('click', this.currState::clickHandler1);
this.button2.addEventListener('click', this.currState::clickHandler2);
}
sign() {
this.plugin.sign();
this.currenState = this.signState;
}
uploading() {
this.button1.innerHTML = '正在上传,点击暂停';
this.plugin.uploading();
this.currState = this.uploadingState;
}
pause() {
this.button1.innerHTML = '已暂停,点击继续上传';
this.plugin.pause();
this.currState = this.pauseState;
}
done() {
this.button1.innerHTML = '上传完成';
this.plugin.done();
this.currState = this.doneState;
}
error() {
this.button1.innerHTML = '上传失败';
this.currState = this.errorState;
}
del() {
this.plugin.del();
this.dom.parentNode.removeChild(this.dom);
}

第五步,使用StateFactory编写各个状态类的实现。

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
class State {
constructor(uploadObj) {
this.uploadObj = uploadObj;
}
clickHandler1() {
throw new Error('子类必须重写父类的clickHanler1方法');
}
clickHanlder2() {
throw new Error('子类必须重写父类的clickHandler2方法');
}
}
class SignState extends State {
clickHandler1() {
console.log('扫描中,点击无效...');
}
clickHandler2() {
console.log('文件正在上传中,不能删除');
}
}
class UploadingState extends State {
clickHandler1() {
this.uploadObj.pause();
}
clickHandler2() {
console.log('文件正在上传中,不能删除');
}
}
class PauseState extends State {
clickHandler1() {
this.uploadObj.uploading);
}
clickHandler2() {
this.uploadObj.del();
}
}
class ErrorState extends State {
clickHandler1() {
console.log('文件上传失败,点击无效');
}
clickHandler2() {
this.uploadObj.del();
}
}
// 测试
const uploadObj = new Upload('JavaScript设计模式与开发实践');
uploadObj.init();
window.external.upload = function(state) {
uploadObj[state]();
}
// 模拟上传进度
window.external.upload('sign');
setTimeout(function() {
// 1秒后开始上传
window.external.upload('uploading');
}, 1000);
setTimeout(function() {
// 5秒后上传完成
window.external.upload('done');
}, 5000);

16.6 状态模式的优缺点

状态模式的优点如下:

  1. 状态模式定义了状态与行为之间的关系,并将它们封装在一个类里。通过增加新的状态类,很容易增加新的状态和转换。
  2. 避免Context无限膨胀,状态切换的逻辑被分布在状态类中,也去掉了Context中原本过多的条件分支。
  3. 用对象代替字符串来记录当前的状态,使得状态的切换更加一目了然。
  4. Context中的请求动作和状态类中的封装行为可以非常容易地独立变化而互不影响。

状态模式的缺点是会在系统中定义许多状态类,且逻辑分散,不能一眼看出整个状态机的逻辑。

16.7 状态模式的性能优化点

  1. state对象的创建销毁时机
  2. 利用亨元模式使得各个Context共享一个state对象

16.8 状态模式和策略模式的关系

相同点:他们都有上下文、一些策略或者状态类,上下文把请求委托给这些类来执行。
区别:使用场景不一样,策略模式多用于客户封装了一系列算法,在需要的时候切换使用;而状态模式中状态和状态对应的行为是早已封装好的,状态之间的切换也是早就规定好了,”改变行为”这件事发生在状态模式内部,对客户来说不需要了解其实现细节。

12
xunm

xunm

个人前端成长记录

13 posts
© 2019 xunm
Powered by Hexo
Theme - NexT.Muse