-
Notifications
You must be signed in to change notification settings - Fork 2
/
search.xml
573 lines (573 loc) · 271 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[你需要知道的HTML知识]]></title>
<url>%2Fposts%2F52887%2F</url>
<content type="text"><![CDATA[前端三大件:HTML+CSS+JS 今天我们就来说一说HTML,可能很多人觉得这个太简单了,就是平常写网页的一堆元素。然而越是基础的东西人们越容易忽略,所以特意梳理了下相关知识,希望加深对它的理解。 如果你觉得本文对你有所帮助,欢迎猛戳 :star: Github(梳理前端知识体系全集) 是什么HTML(HyperText Markup Language)超文本标记语言。顾名思义,它是一门语言,用来标记文档结构的语言。就像你写 word 一样,有各种格式和大纲,HTML描述了网页文档的结构,标记各种区块。 版本如果你很早以前就接触过 html,那你肯定知道下面的写法: 12345<!-- HTML4.01 --><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><!-- XHTML --><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 上面分别是 HTML4 和 XHTML 的声明部分,其中的DTD规定了可用的元素和属性,这样浏览器才能正确地渲染网页。HTML4/4.01(SGML)时代,语法较为随意,比如标签可以不闭合/大写/属性可为空等,所以各大浏览器会有语法容错机制,自动补全标签闭合等。到了后来,大家觉得这并不是浏览器该做的事情,所以有更为严格的XHTML(xml):必须小写/闭合/属性带引号等等。但是XHTML愈加严格繁琐的语法让人不耐烦了,所以 HTML4 的下一个版本也即HTML5横空出世,轻松的语法,更多语义化的元素以及表单增强等等。 12<!-- HTML 5 声明 --><!DOCTYPE html> 注:HTML5是主流和未来,所以下文内容均是以 HTML5 为参考。 元素HTML 文档由各种元素和属性组成,描述了网页的结构。 常见元素HTML文档元素从上至下包括: doctype:文档声明 head部分:包含页面设定,标题,引用资源等 meta title style link script base body部分:网页呈现出来的部分 div/section/article/aside/header/main/footer p span/em/strong/i table/thead/tbody/th/tr/td a form/input/select/button/textarea 元素分类按照默认样式分类 块级 block: 独占一行或多行,可以设置宽高及对齐等属性 行内 inline:不占独立区域,靠自身内容支撑结构,和相邻元素和睦相处,宽高无效,但水平方向可以设置padding和margin 内联块级 inline-block:和其它inline元素同行显示,同时也可以设置宽高/margin/padding(水平和垂直) block inline inline-block 独占一行,自上而下的排列 自左向右排序,宽度不够的时候换行 和其他 inline 元素同行显示 可设置宽度,默认是 auto(margin+border+padding+content=父级元素的宽度) 设定具体的宽度是不起作用的,由文字内容决定 可以设置宽度,未指定时靠内容撑开 可设置高度,默认是 0,靠内容撑开 不生效 可以设置高度,未指定时靠内容撑开 padding/margin 两个方向均可改变元素位置 水平方向 padding/margin 可改变元素位置 padding/margin 两个方向均可改变元素位置 按照元素类别HTML5 中的每个元素都可以归为零个或多个类别,这些类别将具有相似特征的元素分组在一起。w3c中使用的主要类别如下: Metadata content(元数据元素)是可以被用于说明其他内容的表现或行为,或在当前文档和其他文档之间建立联系的元素。 Flow content(流元素)是在应用程序和文档的主体部分中使用的大部分元素。 Sectioning content(区块型元素)是用于定义标题及页脚范围的元素。 Heading content(标题型元素)定义一个区块/章节的标题。 Phrasing content(语句型元素)用于标记段落级文本的元素。 Embedded content(嵌入型元素)引用或插入到文档中其他资源的元素。 Interactive content(交互型元素)专门用于与用户交互的元素。 元素的嵌套你可能听说过以下常见的元素的规则: 123456789<!-- 块级元素可以包含内联元素 --><div><span></span></div><!-- 块级元素可以包含某些块级元素 --><section><div></div></section> <!--正确--><p><div></div></p> <!--错误--><!-- form/a 不能再嵌套自身 --><a><a></a></a><!-- 内联元素一般不能嵌套块级元素 --><body><a><div></div></a><body> <!--HTML4中不合法(但浏览器支持)/但HTML5中是合法的--> 其中关于 HTML4 的嵌套规则可以参考这里, 而 HTML5 中对元素重新做了分类,嵌套关系根据元素的content model进行合法确定。比如上面的a>div在 HTML5 中就是合法的。参考 HTML5 中的a定义,它的内容模型定义为transparent(透明),透明的意思就是在计算合法性的时候,会忽略a本身,嵌套关系需要根据a的父标签来决定。请看下面嵌套: 12345678910111213<!--第一种嵌套--><div> <a> <div></div> </a></div><!--第二种嵌套--><p> <a> <div></div> </a></p> 第一种是合法嵌套,因为相当于div嵌套div,而第二种则不合法,因为相当于p嵌套div,浏览器会进行猜测解析,不妨在浏览器测试一下哦。 语义化先来看两段 html 代码: 12345678910111213141516<!--使用万能div--><div class="header"></div><div class="left"></div><div class="container"> <div class="content"></div></div><div class="footer"></div><!--不使用div--><header></header><nav></nav><main> <article></article> <aside></aside></main><footer></footer> 对于上面两种写法,第二种就是 HTML 语义化。可能有人觉得这两种写法没什么太大区别呀,结构都很清晰,但是如果脱了 css 这层外衣,你觉得哪种写法更容易理解呢?所谓 HTML 语义化,就是用最恰当的元素标签标记内容结构。 为什么需要语义化呢? 在没有 CSS 样式的条件下,也能很好地呈现出内容结构、代码结构; 语义化 HTML 会使 HTML 结构变的清晰,有利于维护代码和添加样式; 方便其他设备解析(如屏幕阅读器、盲人阅读器、移动设备)以意义的方式来渲染网页; 提升搜索引擎优化(SEO)的效果。和搜索引擎建立良好沟通,有助于爬虫抓取更多的有效信息:爬虫依赖于标签来确定上下文和各个关键字的权重; 便于团队开发和维护,语义化更具可读性,是下一步吧网页的重要动向,遵循 W3C 标准的团队都遵循这个标准,可以减少差异化。 通常语义化 HTML 会使代码变的更少,使页面加载更快。 那怎么写语义化的 HTML 呢? 尽可能少的使用无语义的万能标签div和span; 在语义不明显时,既可以使用div或者p时,尽量用p, 因为p在默认情况下有上下间距,对兼容特殊终端有利; 不要使用纯样式标签,如:b、font、u等,改用css设置。 需要强调的文本,可以包含在strong或者em标签中(浏览器预设样式,能用 CSS 指定就不用他们),strong默认样式是加粗(不要用 b),em是斜体(不用 i); 使用表格时,标题要用caption,表头用thead,主体部分用tbody包围,尾部用tfoot包围。表头和一般单元格要区分开,表头用th,单元格用td; 表单域要用fieldset标签包起来,并用legend标签说明表单的用途; 每个input标签对应的说明文本都需要使用label标签,并且通过为input设置id属性,在lable标签中设置for=someld来让说明文本和相对应的input关联起来。 浏览器默认样式现代浏览器对于 HTML 元素都提供了了默认的样式,比如 body 默认有一定的 padding,下拉框/按钮等都有默认的外观。然而,这些默认的样式某些情况下会带来问题,比如我们想要定制下拉框的外观,那就需要花费精力去处理默认样式,提高了定制成本。 解决的方向大概有两个: 干掉默认样式:覆盖重置(css reset) 统一默认样式:修改统一 css reset的话,可以在网络上找到一些简单的代码或者简单的通过以下来重置样式: 1234html * { margin: 0; padding: 0 ...;} 又或者通过统一的样式来处理,比如normalize.css]]></content>
<categories>
<category>前端之路</category>
</categories>
<tags>
<tag>HTML</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入理解跨域及常见解决方法]]></title>
<url>%2Fposts%2F25206%2F</url>
<content type="text"><![CDATA[什么是跨域?浏览器的同源策略在解释跨域的概念之前,先让我们来了解下浏览器的同源策略,这也是为什么会有跨域的由来。 同源策略是一项约定,是浏览器的行为,限制了从同一个源下的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。 所谓同源是指 协议+域名+端口 三者都相同,不满足这个条件即为非同源,即使两个不同域名指向同一 IP 地址。 当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。 不同域之间相互请求资源,就算作跨域。 协议 子域名 主域名 端口号 资源地址 http:// www. abc.com :8080 /scripts/jquery.js https:// cdn. abc.com :3000 /b.js 同源策略限制的内容: Cookie、LocalStorage、IndexedDB 等存储性内容 DOM 节点 AJAX 请求发送后,响应结果被浏览器拦截(即请求发送了,服务器响应了) 注意:有三个标签是允许跨域加载资源的: <img src=XXX> <link href=XXX> <script src=XXX> 总结一下就是: 同源策略是浏览器的一种安全行为,是为了阻止一个域下的文档或脚本读取另一个域下的资源污染自身,所以拦截了响应。 这也是为什么表单提交可以跨域(因为没有获取新的内容)。 常用的解决方案1.JSONP (json with padding)1) 原理利用<script>标签不受跨域限制,将回调函数名作为参数附带在请求中,服务器接受到请求后,进行特殊处理:把接收到的函数名和需要给它的数据拼接成一个字符串返回,客户端会调用相应声明的函数,对返回的数据进行处理。 2) 示例封装 jsonp 请求 12345678910111213141516171819202122232425262728293031<script> function jsonp({ url, params, cb }) { return new Promise((resolve, reject) => { let script = document.createElement('script'); window[cb] = function(data) { resolve(data); document.body.removeChild(script); }; params = { ...params, cb }; let arrs = []; for (let key in params) { arrs.push(`${key}=${params[key]}`); } script.src = `${url}?${arrs.join('&')}`; document.body.appendChild(script); }); } jsonp({ url: 'http://localhost:3000/say', params: { wd: 'Iloveyou' }, cb: 'show' }).then(data => { console.log(data); });</script> 上述代码向http://localhost:3000/say?wd=Iloveyou&cb=show发起请求,服务器返回show('我不爱你'),因而前台将会调用 show 方法。 123456789101112131415let express = require('express');let app = express();app.get('/say', function(req, res) { let { wd, cb } = req.query; console.log(wd); console.log(cb); res.end(`${cb}('我不爱你')`);});var server = app.listen(3000, function() { var host = server.address().address; var port = server.address().port; console.log('应用实例,访问地址为 http://%s:%s', host, port);}); 3) 优缺点简单兼容性好,解决主流浏览器跨域数据访问。缺点是仅支持GET方法,且需要服务器做支持才能实现。 2.CORSCORS(cross-origin resource share)跨域资源共享 只是在 HTTP 协议内部,新增了若干个 header 字段 ,来制定 跨域资源共享 的实现规则。 目前所有浏览器都支持该功能(IE8+:IE8/9 需要使用 XDomainRequest 对象来支持 CORS)),CORS 也已经成为主流的跨域解决方案。浏览器会自动进行 CORS 通信,实现 CORS 通信的关键在于后端。只要后端实现了 CORS,就实现了跨域。根据浏览器发送的请求可以分为两种情况。 1) 简单请求若请求满足所有下述条件,则该请求可视为“简单请求”: 使用下列方法之一: GET HEAD POST Fetch 规范定义了对 CORS 安全的首部字段集合,不得人为设置该集合之外的其他首部字段。该集合为: Accept Accept-Language Content-Language Content-Type (需要注意额外的限制) DPR Downlink Save-Data Viewport-Width Width Content-Type 的值仅限于下列三者之一: text/plain multipart/form-data application/x-www-form-urlencoded 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。 请求中没有使用 ReadableStream 对象。 对于简单请求,只服务端设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带 cookie 请求:前后端都需要设置。 需注意的是:由于同源策略的限制,所读取的 cookie 为跨域请求接口所在域的 cookie,而非当前页。如果想实现当前页 cookie 的写入,可参考 nginx 反向代理中设置 proxy_cookie_domain 和 NodeJs 中间件代理中 cookieDomainRewrite 参数的设置。 1234567891011121314<script> let xhr = new XMLHttpRequest(); document.cookie = 'name=chen'; xhr.withCredentials = true; xhr.open('POST', 'http://localhost:4000', true); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response); } } }; xhr.send();</script> 12345678910111213141516171819202122232425262728const http = require('http');const server = http.createServer((request, response) => { if (request.url === '/') { if (request.method === 'GET') { response.writeHead(200, { 'Access-Control-Allow-Origin': 'http://localhost:3000' }); response.end("{name:'chen',password:'test'}"); } if (request.method === 'POST') { response.writeHead(200, { 'Access-Control-Allow-Origin': 'http://localhost:3000', 'Access-Control-Allow-Credentials': true, // 此处设置的cookie还是http://localhost:4000的而非http://localhost:3000,因为后端也不能跨域写cookie(nginx反向代理可以实现), // 但只要http://localhost:4000中写入一次cookie认证,后面的跨域接口都能从http://localhost:4000中获取cookie,从而实现所有的接口都能跨域访问 'Set-Cookie': 'l=a123456;Path=/;Domain=http://localhost:3000;HttpOnly' // HttpOnly的作用是让js无法读取cookie }); response.end('true'); } } response.end('false');});server.listen(4000, () => { console.log('the server is running at http://localhost:4000');}); 2) 复杂请求不符合以上条件的请求就肯定是复杂请求了。复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检 请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。 复杂请求例子: 1234567891011121314151617// index.html<script> let xhr = new XMLHttpRequest(); document.cookie = 'name=chen'; xhr.withCredentials = true; xhr.open('GET', 'http://localhost:4000/getData', true); xhr.setRequestHeader('name', 'chen'); xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response); console.log(xhr.getResponseHeader('name')); } } }; xhr.send();</script> 1234567891011121314151617181920212223242526272829303132333435363738394041let express = require('express');let app = express();let whiteList = ['http://localhost:3000'];app.use(function(req, res, next) { let origin = req.headers.origin; if (whiteList.includes(origin)) { // 设置哪个源可以访问我 res.setHeader('Access-Control-Allow-Origin', origin); // 允许携带哪个头访问我 res.setHeader('Access-Control-Allow-Headers', 'name'); // 允许哪个方法访问我 res.setHeader('Access-Control-Allow-Methods', 'PUT'); // 允许携带cookie res.setHeader('Access-Control-Allow-Credentials', true); // 预检的存活时间 res.setHeader('Access-Control-Max-Age', 6); // 允许返回的头 res.setHeader('Access-Control-Expose-Headers', 'name'); if (req.method === 'OPTIONS') { // res.end(); } } next();});app.put('/getData', function(req, res) { console.log(req.headers); res.setHeader('name', 'jw'); res.end("i don't love you");});app.get('/getData', function(req, res) { console.log(req.headers); res.end('i love u');});app.use(express.static(__dirname));app.listen(4000, () => { console.log('serve at 4000');}); 上述代码由http://localhost:3000/index.html向http://localhost:4000/跨域请求,正如我们上面所说的,后端是实现 CORS 通信的关键,需要对引起跨域的因素在 OPTION 中进行相应的处理。 3.ngnix 反向代理1) 跨域原理同源策略是浏览器的安全策略,不是 HTTP 协议的一部分。而服务器端调用 HTTP 接口只是使用 HTTP 协议,不会执行 JS 脚本,不需要同源策略,因此也就不存在跨越问题。 2) 实现思路通过nginx配置一个代理服务器(域名与 domain1 相同,端口不同)做跳板机,反向代理访问 domain2 接口,并且可以顺便修改 cookie 中 domain 信息,方便当前域 cookie 写入,实现跨域登录。 3) 示例代码nginx 相关配置: 123456789101112131415161718192021 server { listen 80; server_name www.domain1.com; location / { root [前端代码路径]; index index.html; }} server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里的domain add_header Access-Control-Allow-Origin http://www.domain1.com; add_header Access-Control-Allow-Credentials true; } } 前端代码: 123456789// index.html<script> var xhr = new XMLHttpRequest(); // 前端开关:浏览器是否读写cookie xhr.withCredentials = true; // 访问nginx中的代理服务器 xhr.open('get', 'http://www.domain1.com:81/?user=admin', true); xhr.send();</script> Nodejs 后台: 123456789101112131415// server.jsvar http = require('http');var server = http.createServer();var qs = require('querystring');server.on('request', function(req, res) { var params = qs.parse(req.url.substring(2)); // 向前台写cookie res.writeHead(200, { 'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:脚本无法读取 }); res.write(JSON.stringify(params)); res.end();});server.listen('8080');console.log('Server is running at port 8080...'); 4.Nodejs 中间件代理1)原理原理和上面的 nginx 大致相同,都是利用服务器之间无需遵守同源策略,通过一个代理服务器,实现请求的转发以及设置 CORS。 2) 实现思路node + express + http-proxy-middleware 搭建 proxy 服务器 3) 示例代码前端代码: 12345678<script> var xhr = new XMLHttpRequest(); // 前端开关:浏览器是否读写cookie xhr.withCredentials = true; // 访问nginx中的代理服务器 xhr.open('get', 'http://www.domain1.com:4000/?user=admin', true); xhr.send();</script> 中间件: 123456789101112131415161718192021222324var express = require('express');var proxy = require('http-proxy-middleware');var app = express();app.use( '/', proxy({ target: 'http://www.domain2.com:8080', changeOrigin: true, // 修改响应头信息,实现跨域并允许带cookie onProxyReq: function(proxyRes, req, res) { res.setHeader('Access-Control-Allow-Origin', 'http://www.domain1.com'); res.setHeader('Access-Control-Allow-Credentials', 'true'); }, // 修改响应中的cookie域 cookieDomainRewrite: 'www.domain1.com' // false 为不修改 }));app.listen(81, () => { console.log('Proxy server is running at port 81...');}); 后台接口: 123456789101112131415161718var express = require('express');var app = express();var qs = require('querystring');app.use(function(req, res, next) { var params = qs.parse(req.url.substring(2)); res.setHeader( 'Set-Cookie', 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' ); res.write(JSON.stringify(params)); res.end();});app.listen(8080, () => { console.log('Server is running at port 8080...');}); 参考资料 九种跨域方式实现原理(完整版) 前端常见跨域解决方案(全) 深入跨域问题(4) - 利用代理解决跨域]]></content>
<categories>
<category>前端之路</category>
</categories>
<tags>
<tag>浏览器跨域</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redux核心概念词汇表]]></title>
<url>%2Fposts%2F1659%2F</url>
<content type="text"><![CDATA[这是 Redux 的核心概念词汇表以及这些核心概念的类型签名。 State1type State = any; State (也称为 state tree) 是一个宽泛的概念,但是在 Redux API 中,通常是指一个唯一的 state 值,由 store 管理且由 getState() 方法获得。它表示了 Redux 应用的全部状态,通常为一个多层嵌套的对象。 约定俗成,顶层 state 或为一个对象,或像 Map 那样的键-值集合,也可以是任意的数据类型。然而你应尽可能确保 state 可以被序列化,而且不要把什么数据都放进去,导致无法轻松地把 state 转换成 JSON。 Action1type Action = Object; Action 是一个普通对象,用来表示即将改变 state 的意图。它是将数据放入 store 的唯一途径。无论是从 UI 事件、网络回调,还是其他诸如 WebSocket 之类的数据源所获得的数据,最终都会被 dispatch 成 action。 约定俗成,action 必须拥有一个 type 域,它指明了需要被执行的 action type。Type 可以被定义为常量,然后从其他 module 导入。比起用 Symbols 表示 type,使用 String 是更好的方法,因为 string 可以被序列化。 除了 type 之外,action 对象的结构其实完全取决于你自己。如果你感兴趣的话,请参考 Flux Standard Action ,了解如何构建 action。 还有就是请看后面的 异步 action。 Reducer1type Reducer<S, A> = (state: S, action: A) => S; Reducer (也称为 reducing function) 函数接受两个参数:之前累积运算的结果和当前被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。 Reducer 并不是 Redux 特有的函数 —— 它是函数式编程中的一个基本概念,甚至大部分的非函数式语言比如 JavaScript,都有一个内置的 reduce API。对于 JavaScript,这个 API 是 Array.prototype.reduce(). 在 Redux 中,累计运算的结果是 state 对象,而被累积的值是 action。Reducer 由上次累积的结果 state 与当前被累积的 action 计算得到一个新 state。这些 Reducer 必须是纯函数,而且当输入相同时返回的结果也会相同。它们不应该产生任何副作用。正因如此,才使得诸如热重载和时间旅行这些很棒的功能成为可能。 Reducer 是 Redux 之中最重要的概念。 不要在 reducer 中有 API 调用 dispatch 函数12type BaseDispatch = (a: Action) => Action;type Dispatch = (a: Action | AsyncAction) => any; dispatching function (或简言之 dispatch function) 是一个接收 action 或者异步 action的函数,该函数要么往 store 分发一个或多个 action,要么不分发任何 action。 我们必须分清一般的 dispatch function 以及由 store 实例提供的没有 middleware 的 base dispatch function 之间的区别。 Base dispatch function 总是同步地把 action 与上一次从 store 返回的 state 发往 reducer,然后计算出新的 state。它期望 action 会是一个可以被 reducer 消费的普通对象。 Store123456type Store = { dispatch: Dispatch; getState: () => State; subscribe: (listener: () => void) => () => void; replaceReducer: (reducer: Reducer) => void;}; Store 维持着应用的 state tree 对象。 因为应用的构建发生于 reducer,所以一个 Redux 应用中应当只有一个 Store。 dispatch(action) 是上述的 base dispatch function。 getState() 返回当前 store 的 state。 subscribe(listener) 注册一个 state 发生变化时的回调函数。 replaceReducer(nextReducer) 可用于热重载和代码分割。通常你不需要用到这个 API。 详见完整的 store API reference。 异步 Action1type AsyncAction = any; 异步 action 是一个发给 dispatching 函数的值,但是这个值还不能被 reducer 消费。在发往 base dispatch() function 之前,middleware 会把异步 action 转换成一个或一组 action。异步 action 可以有多种 type,这取决于你所使用的 middleware。它通常是 Promise 或者 thunk 之类的异步原生数据类型,虽然不会立即把数据传递给 reducer,但是一旦操作完成就会触发 action 的分发事件。]]></content>
<categories>
<category>Redux</category>
</categories>
<tags>
<tag>redux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redux三大基础概念(Action、Reducer和Store)]]></title>
<url>%2Fposts%2F45864%2F</url>
<content type="text"><![CDATA[本文源码地址:GitHub 传送门 Action什么是Action? Action 是把数据从应用(这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷(payload)。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。来看几个例子: 123{ type: 'ADD_TODO', text: 'Go to swimming pool' }{ type: 'TOGGLE_TODO', index: 1 }{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' } 它其实就是一个普通 JavaScript 对象,用来描述发生了什么。按照约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 actions。 1import { ADD_TODO, REMOVE_TODO } from '../actionTypes'; 除了 type 字段外,action 对象的结构完全由你自己决定。参照 Flux 标准 Action 获取关于如何构造 action 的建议。 注意:应该尽量减少在 action 中传递的数据 Action 创建函数Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。 在 Redux 中 action 创建函数只是简单的返回一个 action 对象,然后通过 store 的dispatch()方法发起一次 dispatch 过程。 12345678function addTodo(text) { return { type: ADD_TODO, text };}// dispatchstore.dispatch(addTodo('some text')); 而在 传统的 Flux 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,像这样: 1234567function addTodoWithDispatch(text) { const action = { type: ADD_TODO, text }; dispatch(action);} Reducer让我们来看一下Reducer的签名: 1type Reducer<S, A> = (state: S, action: A) => S; Reducer: n.[助剂] 还原剂。还记得化学实验课上用的各种试剂吗?它们通过各种反应生成不同的产物。类似的,上面提到的Action只是描述了 有事情发生 这一事实,而没有描述如何发生,应用将如何更新 state。Reducer正是为此,它描述了事情将如何发生,它本质上是一个纯函数,接受旧的 state 和 action,并根据 action 的 type 对旧的 state 加以处理,返回一个新的 state。(注意:reducer 不会修改 state,而是返回一个新的对象,保持纯净) State 结构在 Redux 中,所有的的 state 都被保存在一个单一的对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来? 以 todo 应用为例,需要保存两种不同的数据: 当前选中的任务过滤条件; 完整的任务列表。 通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。 12345678910111213{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ]} 处理 Reducer 关系时的注意事项开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 todosById: { id -> todo } 和 todos: array 是比较好的方式,本文中为了保持示例简单没有这样处理。 Action 处理确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。 1(previousState, action) => newState; 之所以将这样的函数称之为 reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作: 修改传入参数; 执行有副作用的操作,如 API 请求和路由跳转; 调用非纯函数,如 Date.now() 或 Math.random()。 在React 官方文档里有介绍如何执行有副作用的操作。现在只需要谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。 明白了这些之后,就可以开始编写 reducer,并让它来处理之前定义过的 action。 我们将以指定 state 的初始状态作为开始。Redux 首次执行时,state 为 undefined,此时我们可借机设置并返回应用的初始 state。 12345function todoApp(state = initialState, action) { // 这里暂不处理任何 action, // 仅返回传入的 state。 return state;} 现在可以处理 SET_VISIBILITY_FILTER。需要做的只是改变 state 中的 visibilityFilter。 12345678910function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }); default: return state; }} 注意: 不要修改 state。 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state。 处理多个 Action还有两个 action 需要处理。就像我们处理 SET_VISIBILITY_FILTER 一样,我们引入 ADD_TODO 和 TOGGLE_TODO 两个 actions 并且扩展我们的 reducer 去处理 ADD_TODO. 1234567891011121314151617181920212223242526272829import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters} from './actions'...function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) default: return state }} 如上,不直接修改 state 中的字段,而是返回新对象。新的 todos 对象就相当于旧的 todos 在末尾加上新建的 todo。而这个新的 todo 又是基于 action 中的数据创建的。 最后,TOGGLE_TODO 的实现也很好理解: 1234567891011case TOGGLE_TODO: return Object.assign({}, state, { todos: state.todos.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) }) 我们需要修改数组中指定的数据项而又不希望导致突变, 因此我们的做法是在创建一个新的数组后, 将那些无需修改的项原封不动移入, 接着对需修改的项用新生成的对象替换。(译者注:Javascript 中的对象存储时均是由值和指向值的引用两个部分构成。此处突变指直接修改引用所指向的值, 而引用本身保持不变。) 如果经常需要这类的操作,可以选择使用帮助类 React-addons-update,updeep,或者使用原生支持深度更新的库 Immutable。最后,时刻谨记永远不要在克隆 state 前修改它。 拆分 Reducer这里的 todos 和 visibilityFilter 的更新看起来是相互独立的。有时 state 中的字段是相互依赖的,需要认真考虑,但在这个案例中我们可以把 todos 更新的业务逻辑拆分到一个单独的函数里: 123456789101112131415161718192021222324252627282930313233343536373839404142function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ]; case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }); } return todo; }); default: return state; }}function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }); case ADD_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }); case TOGGLE_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }); default: return state; }} 注意 todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。 下面深入探讨一下如何做 reducer 合成。能否抽出一个 reducer 来专门管理 visibilityFilter?当然可以: 首先引用, 让我们使用 ES6 对象结构 去声明 SHOW_ALL: 1const { SHOW_ALL } = VisibilityFilters; 接下来: 12345678function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter; default: return state; }} 现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined, 子 reducer 将负责返回它们的默认值。 123456789101112131415161718192021222324252627282930313233343536373839function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ]; case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }); } return todo; }); default: return state; }}function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter; default: return state; }}function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) };} 注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。 现在看起来好多了!随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。 最后,Redux 提供了 combineReducers() 工具类来做上面 todoApp 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 todoApp: 12345678import { combineReducers } from 'redux';const todoApp = combineReducers({ visibilityFilter, todos});export default todoApp; 注意上面的写法和下面完全等价: 123456export default function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) };} 你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价: 12345const reducer = combineReducers({ a: doSomethingWithA, b: processB, c: c}); 1234567function reducer(state = {}, action) { return { a: doSomethingWithA(state.a, action), b: processB(state.b, action), c: c(state.c, action) };} combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。 ES6 用户使用注意combineReducers 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export 暴露出每个 reducer 函数,然后使用 import * as reducers 得到一个以它们名字作为 key 的 object: import { combineReducers } from 'redux';import \* as reducers from './reducers';const todoApp = combineReducers(reducers); 由于 import * 还是比较新的语法,为了避免困惑,我们不会在本文档中使用它。但在一些社区示例中你可能会遇到它们。 Store前面我们知道了action用来描述发生了什么,和使用reducers来根据 action 更新 state. Store 就是把它们联系到一起的对象。Store 有以下职责: 维持应用的 state; 提供 getState() 方法获取 state; 提供 dispatch(action) 方法更新 state; 通过 subscribe(listener) 注册监听器; 通过 subscribe(listener) 返回的函数注销监听器。 再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。 根据已有的 reducer 来创建 store 是非常容易的。在前一个章节中,我们使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传递 createStore()。 123import { createStore } from 'redux';import todoApp from './reducers';let store = createStore(todoApp); createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。 1let store = createStore(todoApp, window.STATE_FROM_SERVER); 发起 Actions现在我们已经创建好了 store ,让我们来验证一下! 123456789101112131415161718192021222324import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters} from './actions';// 打印初始状态console.log(store.getState());// 每次 state 更新时,打印日志// 注意 subscribe() 返回一个函数用来注销监听器const unsubscribe = store.subscribe(() => console.log(store.getState()));// 发起一系列 actionstore.dispatch(addTodo('Learn about actions'));store.dispatch(addTodo('Learn about reducers'));store.dispatch(addTodo('Learn about store'));store.dispatch(toggleTodo(0));store.dispatch(toggleTodo(1));store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED));// 停止监听 state 更新unsubscribe(); Output: 1234567891011121314151617181920212223242526272829$ node .\dist\index.js{ visibilityFilter: 'SHOW_ALL', todos: [] }{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Learn about actions', completed: false } ] }{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Learn about actions', completed: false }, { text: 'Learn about reducers', completed: false } ] }{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Learn about actions', completed: false }, { text: 'Learn about reducers', completed: false }, { text: 'Learn about store', completed: false } ] }{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Learn about actions', completed: true }, { text: 'Learn about reducers', completed: false }, { text: 'Learn about store', completed: false } ] }{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Learn about actions', completed: true }, { text: 'Learn about reducers', completed: true }, { text: 'Learn about store', completed: false } ] }{ visibilityFilter: 'SHOW_COMPLETED', todos: [ { text: 'Learn about actions', completed: true }, { text: 'Learn about reducers', completed: true }, { text: 'Learn about store', completed: false } ] }]]></content>
<categories>
<category>Redux</category>
</categories>
<tags>
<tag>redux</tag>
<tag>flux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Redux介绍]]></title>
<url>%2Fposts%2F28774%2F</url>
<content type="text"><![CDATA[是什么(What)?Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性。Redux 除了和 React 一起用外,还支持其它 UI 库。 它体小精悍(只有 2kB,包括依赖)。 为什么(Why)? 现代 JavaScript 应用中,需要管理比任何时候都要多的 state (状态)。管理不断变化的 state 非常困难,state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。正是这样,Redux 试图让 state 的变化变得可预测。 怎么样(How)?应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。 看个简单的例子! 123456789101112131415161718192021222324252627282930313233343536373839import { createStore } from 'redux';/** * 这是一个 reducer,形式为 (state, action) => state 的纯函数。 * 描述了 action 如何把 state 转变成下一个 state。 * * state 的形式取决于你,可以是基本类型、数组、对象、 * 甚至是 Immutable.js 生成的数据结构。惟一的要点是 * 当 state 变化时需要返回全新的对象,而不是修改传入的参数。 * * 下面例子使用 `switch` 语句和字符串来做判断,但你可以写帮助类(helper) * 根据不同的约定(如方法映射)来判断,只要适用你的项目即可。 */function counter(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; }}// 创建 Redux store 来存放应用的状态。// API 是 { subscribe, dispatch, getState }。let store = createStore(counter);// 可以手动订阅更新,也可以事件绑定到视图层。store.subscribe(() => console.log(store.getState()));// 改变内部 state 惟一方法是 dispatch 一个 action。// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行store.dispatch({ type: 'INCREMENT' });// 1store.dispatch({ type: 'INCREMENT' });// 2store.dispatch({ type: 'DECREMENT' });// 1 你应该把要做的修改变成一个普通对象,这个对象被叫做 action,而不是直接修改 state。然后编写专门的函数来决定每个 action 如何改变应用的 state,这个函数被叫做 reducer。 如果你以前使用 Flux,那么你只需要注意一个重要的区别。Redux 没有 Dispatcher 且不支持多个 store。相反,只有一个单一的 store 和一个根级的 reduce 函数(reducer)。随着应用不断变大,你应该把根级的 reducer 拆成多个小的 reducers,分别独立地操作 state 树的不同部分,而不是添加新的 stores。这就像一个 React 应用只有一个根级的组件,这个根组件又由很多小组件构成。 核心概念Redux 本身很简单。 当使用普通对象来描述应用的 state 时。例如,todo 应用的 state 可能长这样: 12345678910{ todos: [{ text: 'Eat food', completed: true }, { text: 'Exercise', completed: false }], visibilityFilter: 'SHOW_COMPLETED'} 这个对象就像 Model,区别是它并没有 setter(修改器方法)。因此其它的代码不能随意修改它,造成难以复现的 bug。 要想更新 state 中的数据,你需要发起一个 action。Action 就是一个普通 JavaScript 对象(注意到没,这儿没有任何魔法?)用来描述发生了什么。下面是一些 action 的示例: 123{ type: 'ADD_TODO', text: 'Go to swimming pool' }{ type: 'TOGGLE_TODO', index: 1 }{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' } 强制使用 action 来描述所有变化带来的好处是可以清晰地知道应用中到底发生了什么。如果一些东西改变了,就可以知道为什么变。action 就像是描述发生了什么的指示器。最终,为了把 action 和 state 串起来,开发一些函数,这就是 reducer。再次地,没有任何魔法,reducer 只是一个接收 state 和 action,并返回新的 state 的函数。 对于大的应用来说,不大可能仅仅只写一个这样的函数,所以我们编写很多小函数来分别管理 state 的一部分: 12345678910111213141516171819202122function visibilityFilter(state = 'SHOW_ALL', action) { if (action.type === 'SET_VISIBILITY_FILTER') { return action.filter; } else { return state; }}function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return state.concat([{ text: action.text, completed: false }]); case 'TOGGLE_TODO': return state.map((todo, index) => action.index === index ? { text: todo.text, completed: !todo.completed } : todo ); default: return state; }} 再开发一个 reducer 调用这两个 reducer,进而来管理整个应用的 state: 123456function todoApp(state = {}, action) { return { todos: todos(state.todos, action), visibilityFilter: visibilityFilter(state.visibilityFilter, action) };} 这差不多就是 Redux 思想的全部。注意到没我们还没有使用任何 Redux 的 API。Redux 里有一些工具来简化这种模式,但是主要的想法是如何根据这些 action 对象来更新 state,而且 90% 的代码都是纯 JavaScript,没用 Redux、Redux API 和其它魔法。 三大原则单一数据源整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。 这让同构应用开发变得非常容易。来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中。由于是单一的 state tree ,调试也变得非常容易。在开发中,你可以把应用的 state 保存在本地,从而加快开发速度。此外,受益于单一的 state tree ,以前难以实现的如“撤销/重做”这类功能也变得轻而易举。 1234567891011121314151617console.log(store.getState())/* 输出{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ]}*/ State 只读唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。 这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。 Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。 123456789store.dispatch({ type: 'COMPLETE_TODO', index: 1});store.dispatch({ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_COMPLETED'}); 使用纯函数来执行修改为了描述 action 如何改变 state tree ,你需要编写 [reducers][3]。 Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。 123456789101112131415161718192021222324252627282930313233343536function visibilityFilter(state = 'SHOW_ALL', action) { switch (action.type) { case 'SET_VISIBILITY_FILTER': return action.filter; default: return state; }}function todos(state = [], action) { switch (action.type) { case 'ADD_TODO': return [ ...state, { text: action.text, completed: false } ]; case 'COMPLETE_TODO': return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: true }); } return todo; }); default: return state; }}import { combineReducers, createStore } from 'redux';let reducer = combineReducers({ visibilityFilter, todos });let store = createStore(reducer);]]></content>
<categories>
<category>Redux</category>
</categories>
<tags>
<tag>redux</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 8: 延迟加载,生产部署和 SSL]]></title>
<url>%2Fposts%2F25434%2F</url>
<content type="text"><![CDATA[TO BE DONE.]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 7: 相关数据和令牌更新]]></title>
<url>%2Fposts%2F32171%2F</url>
<content type="text"><![CDATA[TO BE DONE.]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 6: 响应式表单和自定义验证]]></title>
<url>%2Fposts%2F5650%2F</url>
<content type="text"><![CDATA[TO BE DONE.]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 5: 动画和模板驱动表单]]></title>
<url>%2Fposts%2F7096%2F</url>
<content type="text"><![CDATA[TO BE DONE.]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 4: 访问控制,管理和详情页面]]></title>
<url>%2Fposts%2F48469%2F</url>
<content type="text"><![CDATA[TO BE DONE.]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 3: 抓取和展示数据]]></title>
<url>%2Fposts%2F65062%2F</url>
<content type="text"><![CDATA[在本系列的第 2 部分我们已经介绍身份认证,授权,功能规划以及数据建模。 接下来本系列的第 3 部分将介绍如何通过 Node API 从 MongoDB 检索数据以及通过 Angular 在前段显示和过滤数据: API:获取 Events 数据 Angular:获取 Events 数据 Angular: 创建 Utility 服务 Angular: 创建过滤/排序服务 Angular: 首页活动事件列表 API:获取 Events 数据让我们继续上次的内容。我们的数据库中已经有数据,所以现在是用 API 检索数据的时候了。我们将从编写四个 API 开始,它们将分别从 MongoDB 获取如下数据: 将要发生的公开活动列表 包含所有活动的列表(需要 admin 权限) 活动详情(需要身份认证) 活动的回复列表(需要身份认证) 打开server/api.js, 让我们开始吧! GET Future Public Events获取未来的公开的活动列表数据: api/events123456789101112131415161718192021222324252627// server/api.js.../* |-------------------------------------- | API Routes |-------------------------------------- */ const _eventListProjection = 'title startDatetime endDatetime viewPublic'; // GET list of public events starting in the future app.get('/api/events', (req, res) => { Event.find({viewPublic: true, startDatetime: { $gte: new Date() }}, _eventListProjection, (err, events) => { let eventsArr = []; if (err) { return res.status(500).send({message: err.message}); } if (events) { events.forEach(event => { eventsArr.push(event); }); } res.send(eventsArr); }); }); ... GET All Public and Private Events获取所有的活动列表(包含过去和将来的公开或私密活动): api/events/admin12345678910111213141516171819// server/api.js ... // GET list of all events, public and private (admin only) app.get('/api/events/admin', jwtCheck, adminCheck, (req, res) => { Event.find({}, _eventListProjection, (err, events) => { let eventsArr = []; if (err) { return res.status(500).send({message: err.message}); } if (events) { events.forEach(event => { eventsArr.push(event); }); } res.send(eventsArr); }); }); ... 注意,这里添加了jwtCheck和adminCheck中间件对请求进行身份校验。只有管理员才能通过列表和所有活动进行交互,虽然普通用户也可以并只限于通过直接链接查看私密活动信息!(简单起见,本系列不会继续深入细化数据权限校验,不过你可以自己研究研究,这并不是什么难事) GET Event Details获取特定活动信息: api/event/:id12345678910111213141516// server/api.js ... // GET event by event ID app.get('/api/event/:id', jwtCheck, (req, res) => { Event.findById(req.params.id, (err, event) => { if (err) { return res.status(500).send({message: err.message}); } if (!event) { return res.status(400).send({message: 'Event not found.'}); } res.send(event); }); }); ... GET RSVPs for an Event获取特定活动的回复信息: api/event/:eventId/rsvps12345678910111213141516171819// server/api.js ... // GET RSVPs by event ID app.get('/api/event/:eventId/rsvps', jwtCheck, (req, res) => { Rsvp.find({eventId: req.params.eventId}, (err, rsvps) => { let rsvpsArr = []; if (err) { return res.status(500).send({message: err.message}); } if (rsvps) { rsvps.forEach(rsvp => { rsvpsArr.push(rsvp); }); } res.send(rsvpsArr); }); }); ... Angular:获取 Events 数据Node API 已经就绪了,我们只需在 Angular 中请求它们然后显示返回的数据就可以啦。 添加 HttpClientModule 模块首先我们需要在根模块导入HttpClientModule,因为我们需要通过 HTTP(s)服务调用后台 API: 123456789101112...import { HttpClientModule } from '@angular/common/http';...@NgModule({ ... imports: [ ..., HttpClientModule ], ...})... 创建 API 服务现在我们将创建一个 API 服务专注于从 Node API 返回特定的数据。通过 CLI 在src/app/core文件夹生成api.service.ts: api.service.ts12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667import { HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';import { Injectable } from '@angular/core';import { Observable, throwError as ObservableThrowError } from 'rxjs';import { catchError, retryWhen } from 'rxjs/operators';import { AuthService } from '../auth/auth.service';import { ENV } from './env.config';import { EventModel } from './models/event.model';import { RsvpModel } from './models/rsvp.model';@Injectable()export class ApiService { constructor(private http: HttpClient, private auth: AuthService) {} private get _authHeader(): string { return `Bearer ${this.auth.accessToken}`; } private get _setAuthHeader(): any { return { headers: new HttpHeaders().set('Authorization', this._authHeader) }; } private _handleError(err: HttpErrorResponse | any): Observable<any> { const errorMsg = err.message || 'Error: Unable to complete request.'; if (err.message && err.message.indexOf('No JWT present') > -1) { this.auth.login(); } return ObservableThrowError(errorMsg); } // GET list of public , future events getEvent$(): Observable<EventModel[]> { return this.http .get<EventModel[]>(`${ENV.BASE_API}events`) .pipe(catchError(error => this._handleError(error))); } // GET all event - private and public (admin only) getAdminEvent$(): Observable<Array<EventModel>> { return this.http .get<Array<EventModel>>( `${ENV.BASE_API}events/admin`, this._setAuthHeader ) .pipe(catchError(error => this._handleError(error))); } // GET event by id (login required) getEventById$(id: string): Observable<EventModel> { return this.http .get<EventModel>(`${ENV.BASE_API}event/${id}`, this._setAuthHeader) .pipe(catchError(error => this._handleError(error))); } // GET RSVPs by event ID (login required) getRsvpsByEventId$(eventId: string): Observable<RsvpModel[]> { return this.http .get<Array<RsvpModel>>( `${ENV.BASE_API}event/${eventId}/rsvps`, this._setAuthHeader ) .pipe(catchError(error => this._handleError(error))); } 我们会发出未经身份验证和身份验证的请求,因此我们将导入HttpClient和HttpHeaders(以添加带有访问令牌的授权头部)以及HttpErrorResponse。如果在尝试发出经过身份验证的请求时没有发现 JWT,我们还需要AuthService来提示登录。 我们将使用 API 调用创建流,因此我们将从 RxJS 导入Observable和ObservableThrowError以及可链式调用的 catchError操作符。我们需要环境配置中的 ENV 来获得适当的 API uri。最后,为了声明事件流的类型,我们需要前面创建的模型(EventModel和RsvpModel)。 为了发出经过身份验证的请求,我们需要使用存储在本地存储中的访问令牌设置一个授权头,这个访问令牌来自我们在第 2 部分中创建的身份认证服务。我们将创建一个名为_authHeader 的访问器方法,使用当前存储的访问令牌返回必要的授权值。如果在会话期间以静默方式更新身份验证(稍后我们将实现静默的令牌更新),则令牌可能会更改,因此我们将在每个请求时从服务中获取它,以确保其有效性。 最后,我们需要处理 API 错误信息。成功调用则将返回响应作为主体(在我们的示例中,返回 JSON)。如果调用失败,则检查错误消息,并在必要时提示重新登录,取消可观察对象,并在发生其他错误时生成错误提示。 API 服务注入为了使 API 服务能在整个应用中使用,需要在根模块注入依赖: 12345678910111213// src/app/app.module.ts...import { ApiService } from './core/api.service';...@NgModule({ ... providers: [ ..., ApiService ], ...})... 创建加载组件由于我们将进行异步 API 调用,所以最好也有一个加载状态。或者,我们可以使用route resolve来防止在返回必要的 API 数据之前加载路由,但这可能会让应用程序在导航时显得迟缓。相反,我们可以显示一个带有非常简单的组件的加载图标: 1$ ng g component core/loading --is --it --flat 现在需要一个合适的加载图标,你可以从loading.io选择你喜欢的。然后将它放在src/assets/images。接着编辑我们的loading.component.ts: 12345678910111213141516171819202122// src/app/core/loading.component.tsimport { Component } from '@angular/core';@Component({ selector: 'app-loading', template: ` <img src="/assets/images/loading.svg" /> `, styles: [ ` :host { display: block; } img { display: block; margin: 20px auto; width: 50px; } ` ]})export class LoadingComponent {} 我们的加载组件就完成了,简单吧!让我们试着把回调组件的Loading...字样替换成我们的加载组件: 12<!-- src/app/pages/callback/callback.component.html --><app-loading></app-loading> 你会发现,当成功登陆跳转时,将会显示我们的加载图标。 Angular: 创建 Utility 服务在开始构建组件之前,让我们先创建一个可以在整个开发过程中利用的的实用程序服务。 1$ ng g service core/utils Utils123456789101112131415161718192021222324252627282930313233343536373839404142434445464748// src/app/core/utils.service.tsimport { Injectable } from '@angular/core';import { DatePipe } from '@angular/common';@Injectable()export class UtilsService { constructor(private datePipe: DatePipe) {} isLoaded(loading: boolean): boolean { return loading === false; } eventDates(start, end): string { // Display single-day events as "Jan 7, 2018" // Display multi-day events as "Aug 12, 2017 - Aug 13, 2017" const startDate = this.datePipe.transform(start, 'mediumDate'); const endDate = this.datePipe.transform(end, 'mediumDate'); if (startDate === endDate) { return startDate; } else { return `${startDate} - ${endDate}`; } } eventDatesTimes(start, end): string { // Display single-day events as "1/7/2018, 5:30 PM - 7:30 PM" // Display multi-day events as "8/12/2017, 8:00 PM - 8/13/2017, 10:00 AM" const _shortDate = 'M/d/yyyy'; const startDate = this.datePipe.transform(start, _shortDate); const startTime = this.datePipe.transform(start, 'shortTime'); const endDate = this.datePipe.transform(end, _shortDate); const endTime = this.datePipe.transform(end, 'shortTime'); if (startDate === endDate) { return `${startDate}, ${startTime} - ${endTime}`; } else { return `${startDate}, ${startTime} - ${endDate}, ${endTime}`; } } eventPast(eventEnd): boolean { // Check if event has already ended const now = new Date(); const then = new Date(eventEnd.toString()); return now >= then; }} 这里我们使用了 Angular 内置的DatePipe,所以我们需要导入它,并且在根模块里注入依赖。 isLoaded()用于检查传入值是否严格等于false,我们打算在每个组件里添加一个loading属性用于同步 API 调用的的状态,因为loading值可能会为undefined,所以通过这个公用方法可以防止显示错误的 UI。 然后eventDates()和eventDatesTimes()用于转换时间的显示格式。而eventPast()用于检查是否是过去的时间,从而告诉我们活动已经过期。 为了全局使用这个通用的服务,在根模块注入相应依赖。 123456789101112131415// src/app/app.module.ts...import { DatePipe } from '@angular/common';import { UtilsService } from './core/utils.service';...@NgModule({ ..., providers: [ ..., DatePipe, UtilsService ], ...})... Angular: 创建过滤/排序服务对于获取的数据数组,我们需要添加一些方法来组织它们。就如同 AngularJS 里面的内置过滤器,比如filter和orderBy。虽然 Angular 使用pipes来转换数据,但是并不提供用于过滤或排序的开箱即用管道。(原因) 出于性能和影响最小化的考虑,我们将不创建自定义管道来实现筛选或排序功能。这只会重新引入 Angular 团队试图通过删除这些过滤器来解决的问题。相反,正确的方法是使用服务。 我们将会添加几个全局通用的服务方法用于搜索,过滤和排序: $ ng g service core/filter-sort 打开src/app/core/filter-sort.service.ts: filter-sort12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import { Injectable } from '@angular/core';import { DatePipe } from '@angular/common';@Injectable()export class FilterSortService { constructor(private datePipe: DatePipe) {} private _objArrayCheck(array: any[]): boolean { // Checks if the first item in the array is an object // (assumes same-shape for all array items) // Necessary because some arrays passed in may have // models that don't match {[key: string]: any}[] // This check prevents uncaught reference errors const item0 = array[0]; const check = !!( array.length && item0 !== null && Object.prototype.toString.call(item0) === '[object Object]' ); return check; } search( array: any[], query: string, excludeProps?: string | string[], dateFormat?: string ) { // Match query to strings and Date objects / ISO UTC strings // Optionally exclude properties from being searched // If matching dates, can optionally pass in date format string if (!query || !this._objArrayCheck(array)) { return array; } const lQuery = query.toLowerCase(); const isoDateRegex = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/; // ISO UTC const dateF = dateFormat ? dateFormat : 'medium'; const filteredArray = array.filter(item => { for (const key in item) { if (item.hasOwnProperty(key)) { if (!excludeProps || excludeProps.indexOf(key) === -1) { const thisVal = item[key]; if ( // Value is a string and NOT a UTC date typeof thisVal === 'string' && !thisVal.match(isoDateRegex) && thisVal.toLowerCase().indexOf(lQuery) !== -1 ) { return true; } else if ( // Value is a Date object or UTC string (thisVal instanceof Date || thisVal.toString().match(isoDateRegex)) && // https://angular.io/api/common/DatePipe // Matching date format string passed in as param (or default to 'medium') this.datePipe .transform(thisVal, dateF) .toLowerCase() .indexOf(lQuery) !== -1 ) { return true; } } } } }); return filteredArray; } noSearchResults(arr: any[], query: string): boolean { // Check if array searched by query returned any results return !!(!arr.length && query); } orderByDate(array: any[], prop: string, reverse?: boolean) { // Order an array of objects by a date property // Default: ascending (1992->2017 | Jan->Dec) if (!prop || !this._objArrayCheck(array)) { return array; } const sortedArray = array.sort((a, b) => { const dateA = new Date(a[prop]).getTime(); const dateB = new Date(b[prop]).getTime(); return !reverse ? dateA - dateB : dateB - dateA; }); return sortedArray; }} _objArrayCheck()方法,用于确保我们尝试搜索或排序的数组包含对象。如果没有,就会产生未捕获的引用错误,因此我们希望有一种方法来防止这种情况。 search()方法接受要筛选的对象数组、要搜索的查询、要从搜索中排除的任何可选属性(单个属性字符串或属性数组),以及可选的日期格式字符串。dateFormat应该是Angular DatePipe中的一种格式。这允许用户搜索原始数据中可读性差得多的日期。开发可以确定他们想要查询的格式。例如,如果转换 UTC 日期字符串或 JavaScript 日期对象,用户可以查询 Jan 并接收数据中实际值为 2017-01-07T15:00:00.000Z 的结果。 如果查询是falsey,我们将返回未经过滤的数组。否则,我们将把查询设置为小写,因为我们的搜索应该不区分大小写(我们将对查询的值执行相同的操作)。由于 UTC 日期在 JavaScript 中被识别为字符串而不是日期,因此我们将使用正则表达式将其与其他字符串区分开来。如果没有传递dateFormat参数,我们将默认为medium(例如,2010 年 9 月 3 日,12:05:08 PM)。 接下来,我们将使用数组方法filter()对数组进行筛选。我们将遍历数组中每个对象中的每个属性,首先确保对象包含hasOwnProperty()方法中的属性。如果键不匹配excludeProps中传递的任何内容,将检查与query匹配的值. 这对于不同的值类型是不同的。搜索处理字符串、JavaScript 日期对象和 UTC 字符串。如果我们想确保搜索不查询某些属性,我们将确保在调用组件中的方法时将它们作为excludedProps传入。 noSearchResults()方法只接受一个数组和一个查询,如果该数组为空且有查询,则返回 true。 orderByDate()方法接受一个对象数组、包含要排序的日期值的属性和一个可选的反向参数,以将排序顺序从升序更改为降序。如果没有传递属性,则返回未排序的数组。 在根模块注入依赖: 12345678910111213// src/app/app.module.ts...import { FilterSortService } from './core/filter-sort.service';...@NgModule({ ..., providers: [ ..., FilterSortService ], ...})... 现在我们可以在组件中根据日期搜索和排序活动事件列表。 Angular: 首页活动事件列表组件应该获取和显示事件列表。我们已经创建了 Node API 来返回该数据,并实现了 API 服务来获取它。现在我们需要在组件里订阅这些数据并显示在页面。 为了使用ngModel指令,我们需要在根模块导入FormsModule: 12345678910111213// src/app/app.module.ts...import { FormsModule } from '@angular/forms';...@NgModule({ ..., imports: [ ..., FormsModule ], ...})... 接着,更新我们的主页组件,让它可以显示公开的活动信息。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970// src/app/pages/home/home.component.tsimport { Component, OnInit, OnDestroy } from '@angular/core';import { Title } from '@angular/platform-browser';import { ApiService } from './../../core/api.service';import { UtilsService } from './../../core/utils.service';import { FilterSortService } from './../../core/filter-sort.service';import { Subscription } from 'rxjs';import { EventModel } from './../../core/models/event.model';@Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss']})export class HomeComponent implements OnInit, OnDestroy { pageTitle = 'Events'; eventListSub: Subscription; eventList: EventModel[]; filteredEvents: EventModel[]; loading: boolean; error: boolean; query: ''; constructor( private title: Title, public utils: UtilsService, private api: ApiService, public fs: FilterSortService ) {} ngOnInit() { this.title.setTitle(this.pageTitle); this._getEventList(); } private _getEventList() { this.loading = true; // Get future, public events this.eventListSub = this.api.getEvents$().subscribe( res => { this.eventList = res; this.filteredEvents = res; this.loading = false; }, err => { console.error(err); this.loading = false; this.error = true; } ); } searchEvents() { this.filteredEvents = this.fs.search( this.eventList, this.query, '_id', 'mediumDate' ); } resetQuery() { this.query = ''; this.filteredEvents = this.eventList; } ngOnDestroy() { this.eventListSub.unsubscribe(); }} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768<!-- src/app/pages/home/home.component.html --><h1 class="text-center">{{ pageTitle }}</h1><app-loading *ngIf="loading"></app-loading><ng-template [ngIf]="utils.isLoaded(loading)"> <ng-template [ngIf]="eventList"> <ng-template [ngIf]="eventList.length"> <!-- Search events --> <label class="sr-only" for="search">Search</label> <div class="search input-group mb-3"> <div class="input-group-prepend"> <div class="input-group-text">Search</div> </div> <input id="search" type="text" class="form-control" [(ngModel)]="query" (keyup)="searchEvents()" /> <span class="input-group-append"> <button class="btn btn-danger" (click)="resetQuery()" [disabled]="!query" > &times; </button> </span> </div> <!-- No search results --> <p *ngIf="fs.noSearchResults(filteredEvents, query)" class="alert alert-warning" > No events found for <em class="text-danger">{{ query }}</em>, sorry! </p> <!-- Events listing --> <section class="list-group"> <a *ngFor="let event of fs.orderByDate(filteredEvents, 'startDatetime')" [routerLink]="['/event', event._id]" class="list-group-item list-group-item-action flex-column align-items-start" > <div class="d-flex w-100 justify-content-between"> <h5 class="mb-1" [innerHTML]="event.title"></h5> <small >{{ utils.eventDates(event.startDatetime, event.endDatetime) }}</small > </div> </a> </section> </ng-template> <!-- No upcoming public events available --> <p *ngIf="!eventList.length" class="alert alert-info"> No upcoming public events available. </p> </ng-template> <!-- Error loading events --> <p *ngIf="error" class="alert alert-danger"> <strong>Oops!</strong> There was an error retrieving event data. </p></ng-template> 小结我们已经介绍了如何使用 Node API 从数据库中获取数据,以及如何在 Angular 中操作和显示数据。在本系列教程的下一部分中,我们将处理访问管理、显示管理事件列表以及开发带有选项卡子组件的事件详细信息页面。 系列索引 Angular 实战系列 - Part 1: MEAN 配置 & Angular 架构 Angular 实战系列 - Part 2: 身份验证和数据建模 Angular 实战系列 - Part 3: 抓取和展示数据(你现在在这里) Angular 实战系列 - Part 4: 访问控制,管理和详情页面 Angular 实战系列 - Part 5: 动画和模板驱动表单 Angular 实战系列 - Part 6: 响应式表单和自定义验证 Angular 实战系列 - Part 7: 相关数据和令牌更新 Angular 实战系列 - Part 8: 延迟加载,生产部署和 SSL]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 2: 身份验证和数据建模]]></title>
<url>%2Fposts%2F64307%2F</url>
<content type="text"><![CDATA[在本系列的第 1 部分我们已经介绍了如何配置云托管的 MongoDB, Node 服务器,还有 Angular 前端项目。 接下来本系列的第 2 部分将介绍身份验证、授权、功能模块规划和数据建模: Angular:身份验证 角色授权 规划功能模块 数据建模 Angular:身份验证继续第 1 部分的内容,现在添加身份验证模块,它包含: 登录和注销 用户信息和令牌管理 会话持久性 使用访问令牌对 HTTP 请求进行授权 安装 Auth0.js首先安装 Auth0 依赖,用于和之前注册的 Auth0 账号进行交互: 1$ npm install auth0-js@latest --save 动态环境配置创建一个文件来存储关于应用程序环境的信息。我们目前在localhost:4200上进行开发,但是最终将部署在节点服务器上,在生产环境中,它会运行在反向代理上。我们需要确保开发环境不会破坏生产环境,反之亦然。 创建src/app/core文件夹,然后添加一个名为env.config.ts的文件: 12345678910111213// src/app/core/env.config.tsconst _isDev = window.location.port.indexOf('4200') > -1;const getHost = () => { const protocol = window.location.protocol; const host = window.location.host; return `${protocol}//${host}`;};const apiURI = _isDev ? 'http://localhost:8083/api/' : `/api/`;export const ENV = { BASE_URI: getHost(), BASE_API: apiURI}; 上述代码检测主机环境并设置应用程序的基础 URI 和基础 API URI。在需要检测和使用这些 uri 的地方,可以引入ENV变量。 另一种方法是配置environments/environment.*.ts。。 安全认证设定创建src/app/auth/auth.config.ts文件用于存储 Auth0 认证相关的配置信息: 123456789101112131415161718// src/app/auth/auth.config.tsimport { ENV } from './../core/env.config';interface AuthConfig { CLIENT_ID: string; CLIENT_DOMAIN: string; AUDIENCE: string; REDIRECT: string; SCOPE: string;}export const AUTH_CONFIG: AuthConfig = { CLIENT_ID: '[AUTH0_CLIENT_ID]', CLIENT_DOMAIN: '[AUTH0_CLIENT_DOMAIN]', // e.g., you.auth0.com AUDIENCE: '[YOUR_AUTH0_API_AUDIENCE]', // e.g., http://localhost:8083/api/ REDIRECT: `${ENV.BASE_URI}/callback`, SCOPE: 'openid profile'}; 这些配置信息可以在你的 Auth0 账号里找到。 身份认证服务AuthService将会负责前端的身份验证逻辑,用 CLI 为生成模板: 1$ ng g service auth/auth --spec false 打开该文件并添加: auth.service123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111// src/app/auth/auth.service.tsimport { Injectable } from '@angular/core';import { Router } from '@angular/router';import { BehaviorSubject } from 'rxjs';import { AUTH_CONFIG } from './auth.config';import * as auth0 from 'auth0-js';@Injectable()export class AuthService { // Create Auth0 web auth instance private _auth0 = new auth0.WebAuth({ clientID: AUTH_CONFIG.CLIENT_ID, domain: AUTH_CONFIG.CLIENT_DOMAIN, responseType: 'token', redirectUri: AUTH_CONFIG.REDIRECT, audience: AUTH_CONFIG.AUDIENCE, scope: AUTH_CONFIG.SCOPE }); accessToken: string; userProfile: any; expiresAt: number; // Create a stream of logged in status to communicate throughout app loggedIn: boolean; loggedIn$ = new BehaviorSubject<boolean>(this.loggedIn); loggingIn: boolean; constructor(private router: Router) { // If app auth token is not expired, request new token if (JSON.parse(localStorage.getItem('expires_at')) > Date.now()) { this.renewToken(); } } setLoggedIn(value: boolean) { // Update login status subject this.loggedIn$.next(value); this.loggedIn = value; } login() { // Auth0 authorize request this._auth0.authorize(); } handleAuth() { // When Auth0 hash parsed, get profile this._auth0.parseHash((err, authResult) => { if (authResult && authResult.accessToken) { window.location.hash = ''; this._getProfile(authResult); } else if (err) { console.error(`Error authenticating: ${err.error}`); } this.router.navigate(['/']); }); } private _getProfile(authResult) { this.loggingIn = true; // Use access token to retrieve user's profile and set session this._auth0.client.userInfo(authResult.accessToken, (err, profile) => { if (profile) { this._setSession(authResult, profile); } else if (err) { console.warn(`Error retrieving profile: ${err.error}`); } }); } private _setSession(authResult, profile?) { this.expiresAt = authResult.expiresIn * 1000 + Date.now(); // Store expiration in local storage to access in constructor localStorage.setItem('expires_at', JSON.stringify(this.expiresAt)); this.accessToken = authResult.accessToken; this.userProfile = profile; // Update login status in loggedIn$ stream this.setLoggedIn(true); this.loggingIn = false; } private _clearExpiration() { // Remove token expiration from localStorage localStorage.removeItem('expires_at'); } logout() { // Remove data from localStorage this._clearExpiration(); // End Auth0 authentication session this._auth0.logout({ clientId: AUTH_CONFIG.CLIENT_ID, returnTo: ENV.BASE_URI }); } get tokenValid(): boolean { // Check if current time is past access token's expiration return Date.now() < JSON.parse(localStorage.getItem('expires_at')); } renewToken() { // Check for valid Auth0 session this._auth0.checkSession({}, (err, authResult) => { if (authResult && authResult.accessToken) { this._getProfile(authResult); } else { this._clearExpiration(); } }); }} 上面代码使用auth.config里的配置实例化了一个WebAuth对象,并且提供了一个 RxJS 的BehaviorSubject身份验证状态事件流,使得我们可以在整个应用中订阅它。 构造函数在初始化时检查应用程序身份验证状态:如果用户没有从之前的会话中退出 Angular 应用程序(令牌还没有过期),将会调用renewToken()的方法来验证他们在身份验证服务器上的 Auth0 会话是否仍然有效。如果是,我们会接收一个新的访问令牌。 login()方法使用WebAuth发起授权身份验证请求。Auth0 登录授权页面会显示给用户,然后用户可以进行登录。 当用户成功验证,应用的回调页面会接收到一个access_token和令牌过期时间(expiresIn)。handleAuth()方法使用 Auth0 的parseHash()回调方法来获取用户的概要文件(_getProfile()),并且通过本地存储保存令牌,过期时间,概要文件设置会话信息(_setSession()),同时调用setLoggedIn()同步用户验证状态,以便应用程序中的任何组件知道用户已经登陆了。 接着,我们创建了一些通用方法(_clearExpiration),用于从本地存储中轻松清除过期信息。 logout()方法清除本地存储的过期信息,并通过 Auth0 的 API 注销当前会话,并且重定向到我们指定的页面(受页)。 tokenValid()访问器,用于检查当前日期时间是否小于令牌过期日期时间。 最后,我们将实现renewToken()方法,如果用户的身份验证会话仍然处于活动状态,则使用 Auth0 checkSession()方法从 Auth0 请求一个新的访问令牌。如果没有会话活动,我们将不做任何事情。我们不希望在这里产生任何错误或日志,因为没有会话并不意味着出了什么问题。 AuthService 全局实例我们需要全局注册 AuthService 的单一实例,因此将在app.module.ts里注入依赖: app.module12345678910111213// src/app/app.module.ts...import { AuthService } from './auth/auth.service';...@NgModule({ ... providers: [ ..., AuthService ], ...})... 回调组件接下来,我们将创建一个回调组件。通过验证后应用程序会被重定向到此。这个组件负责接收处理身份验证信息,然后显示一条加载消息,直到散列解析完成,Angular 应用程序重定向回主页。 还记得我们之前已经将http://localhost:4200/callback和http://localhost:8083/callback添加到 Auth0 允许的客户端回调地址。 1$ ng g component pages/callback AuthService服务的handleAuth()方法必须在该组件的构造方法里调用,以便在应用初始化时运行。 1234<!-- src/app/pages/callback/callback.component.html --><div> Loading...</div> 然后添加回调路由: 123456789101112// src/app/app-routing.module.ts...import { CallbackComponent } from './pages/callback/callback.component';const routes: Routes = [ ... { path: 'callback', component: CallbackComponent }];... 在 HeaderComponent 添加登陆和注销在 Header 组件里添加 AuthService 服务: 1234567891011// src/app/header/header.component.ts...import { AuthService } from './../auth/auth.service';...export class HeaderComponent implements OnInit { ... constructor( ..., public auth: AuthService) { } ...} 在组件模板里添加相应元素: 123456789101112131415161718<!-- src/app/header/header.component.html --><header id="header" class="header"> <div class="header-page bg-primary"> ... <div class="header-page-authStatus"> <span *ngIf="auth.loggingIn">Logging in...</span> <ng-template [ngIf]="!auth.loggingIn"> <a *ngIf="!auth.loggedIn" (click)="auth.login()">Log In</a> <span *ngIf="auth.loggedIn && auth.userProfile"> {{ auth.userProfile.name }} <span class="divider">|</span> <a (click)="auth.logout()">Log Out</a> </span> </ng-template> </div> ... </div></header> 相应样式这里就不占用篇幅了,具体请参照源码。 我们现在可以登录我们的应用程序了! 通过单击“登录”链接并进行身份验证。登录之后,可以在 Header 的右上角看到名字和退出链接。 你可以试着关闭浏览器并重新打开它,你会发现登录状态是持久的(除非令牌已经过期,或者你点击了注销) 角色授权对于我们的应用来说,只有是admin的用户才可以创建,更新和删除活动信息,其他普通用户只能回复活动。为了实现这些,我们需要给用户分配角色在 Node.js 的 API 和 Angular 应用里完成相应的逻辑代码。 首先来看看大概的步骤: 使用 Auth0 规则创建我们的用户角色,然后将它们添加到 ID(客户端用户信息)和 access (API)令牌。 实现 Node.js API 中间件以保证只有admin角色的用户可以访问相应 API。 Angular 中利用用户角色信息对路由和功能模块进行保护。 快上车! 使用 Auth0 规则进行管理授权所谓Rules是 Auth0 提供的一个拓展,它实际上是一个 Javascript 方法,每次进行用户身份认证的时候都会执行。 进入我们的Auth0并选择创建一条Set roles to a user模板的 rule: Assign Admin to specified user12345678910111213141516171819202122232425262728293031323334353637383940// set me as 'admin' role, and all others to 'user'// save app_metadata to ID and tokensfunction (user, context, callback) { // Roles should only be set to verified users. if (!user.email || !user.email_verified) { return callback(new UnauthorizedError('Please verify your email before logging in.')); } user.app_metadata = user.app_metadata || {}; // You can add a Role based on what you want // In this case I check domain const addRolesToUser = function(user,cb) { if(user.email && user.email === '[MY_REGISTERED_ACCOUNT_EMAIL]'){ cb(null,['admin']); }else{ cb(null,['user']); } }; addRolesToUser(user,function(err,roles){ if(err){ callback(err); }else{ user.app_metadata.roles =roles; auth0.users.updateAppMetadata(user.user_id,user.app_metadata) .then(function(){ // add metadata to both ID token and access token var namespace = 'http://yourapp.com/roles'; var userRoles = user.app_metadata.roles; context.idToken[namespace] = userRoles; context.accessToken[namespace] = userRoles; callback(null,user,context); }) .catch(function(err){ callback(err); }); } });} 简单起见,我们只给自己的账号分配admin角色,其他账号都是普通user。 注意,上面代码还检查确保用户邮件必须是已经通过验证了。 namespace标识符可以是任何非 auth0 的 HTTP 或 HTTPS URL,并且不必指向实际的资源。Auth0 执行 OIDC 关于附加声明的建议,并且会静默排除任何没有名称空间的声明。[了解更多] 现在,你可以在我们的 RSVP 程序进行登陆,登陆成功后,可以在Auth0 的用户查看用户的Metadata,你应该看到app_metadata大概如下: 123{ "roles": ["admin"]} 在客户端接收的 ID 和访问令牌, 会附带如下的键值对: 1"http://myapp.com/roles": ["admin"] Node API 管理员中间件现在我们的 Auth0 身份验证已经可以提供角色支持,接下来利用它来保护需要管理员访问的 API 路由。 打开config.js并添加我们在上面设置的namespace: 12345// server/config.jsmodule.exports = { ..., NAMESPACE: 'http://yourapp.com/roles'}; 添加中间件代码来确认用户是否经过身份验证,以及是否具有访问 API 的管理员权限。 12345678910111213141516171819// server/api.js...module.exports = function(app, config) { // Authentication middleware const jwtCheck = jwt({ ... }); // Check for an authenticated admin user const adminCheck = (req, res, next) => { const roles = req.user[config.NAMESPACE] || []; if (roles.indexOf('admin') > -1) { next(); } else { res.status(401).send({message: 'Not authorized for admin access'}); } }... express-jwt包默认将解码后的令牌添加到req.user。adminCheck中间件查找这个属性,并在数组中查找 admin 的值。如果找到,则继续请求。如果没有,则返回 401 未授权状态,并显示一条简短的错误消息。 Angular 应用中的管理员授权同样,我们需要在前端添加相应的管理员授权检测代码,我们需要修改AuthService服务。 首先,添加同样的namespace: 1234567891011// src/app/auth/auth.config.ts...interface AuthConfig { ..., NAMESPACE: string;};export const AUTH_CONFIG: AuthConfig = { ..., NAMESPACE: 'http://yourapp.com/roles'}; 接着在auth.service.ts检查和保存管理员授权信息: 1234567891011121314151617181920212223242526// src/app/auth/auth.service.ts...export class AuthService { ... isAdmin: boolean; ... private _setSession(authResult, profile) { ... // If initial login, set profile and admin information if (profile) { ... this.isAdmin = this._checkAdmin(profile); } // Update login status in loggedIn$ stream ... } private _checkAdmin(profile) { // Check if the user has admin role const roles = profile[AUTH_CONFIG.NAMESPACE] || []; return roles.indexOf('admin') > -1; } ... 首先我们添加了一个属性:isAdmin: boolean,用来标识用户的管理员状态。另外,我们更新了_setSession方法,在用户通过验证后,检查了用户的角色信息并同步isAdmin。 至此,在后端 Node API 路由和 Angular 应用中都已经实现了权限校验。 规划功能模块数据库、Angular 应用程序、身份验证和 Node API 基本结构已经搭建好了。现在是时候进行功能规划和数据建模了。在直接编写 API 和业务逻辑之前,规划应用程序的数据结构非常重要。 让我们从更高层次的角度思考一下 RSVP 应用程序的预期功能,然后我们将推断数据库模型应该是什么样子的。 活动事件 在首页显示可参加的公开活动事件列表,并且可以进行搜索。这些活动必须发生在将来,而不是已经过期的。 管理员可以看到所有活动事件的列表,包括公开/私有/过去/将来的活动。 活动详情页面,已登陆用户可以回复参与活动,并且可以查看别人的回复。 活动只能被管理员创建更新和删除。 删除一个活动会同时清除所有相关联的回复。 公开的活动可以显示在首页,但是私有的活动也可以直接通过链接访问。 活动回复和活动的 ID 活动属性 活动 ID (数据库自动生成) 活动标题 地点 开始日期和时间 结束日期和时间 活动描述 可见性(公开/私有) 活动回复 任何已认证的用户可以参与回复将要发生的活动,不管是公开还是私有。 用户不可以添加和更新一个已经结束的活动。 用户可以修改他们现有的回复,但不能删除它们。 回复的属性 回复 ID 用户 ID 名字 活动 ID 是否出席 额外出席人数(如果参加) 评论 用户 用户应该能够在他们的个人资料中查看所有的已回复列表 用户数据不存储在 MongoDB 中,由 Auth0 托管。 用户通过用户 ID 与他们的 RSVPs 相关联。 用户只能更新自己的回复。 管理员可以对活动进行增删改查。 数据建模我们已经对应用的功能有了大致的了解,接下来需要在服务端和客户端建立必要的数据模型。 创建 Mongoose Schema通过 mongoose 进行 MongoDB 对象建模。每个 mongoose 模式会映射到一个 MongoDB 集合,并定义该集合中文档对象的原型。 在server下新建models文件夹,添加Event.js和Rsvp.js: Event1234567891011121314151617181920// server/models/Event.js/* |-------------------------------------- | Event Model |-------------------------------------- */const mongoose = require('mongoose');const Schema = mongoose.Schema;const eventSchema = new Schema({ title: { type: String, required: true }, location: { type: String, required: true }, startDatetime: { type: Date, required: true }, endDatetime: { type: Date, required: true }, description: String, viewPublic: { type: Boolean, required: true }});module.exports = mongoose.model('Event', eventSchema); MongoDB 会自动生成对象 ID。 Rsvp1234567891011121314151617181920// server/models/Rsvp.js/* |-------------------------------------- | Rsvp Model |-------------------------------------- */const mongoose = require('mongoose');const Schema = mongoose.Schema;const rsvpSchema = new Schema({ userId: { type: String, required: true }, name: { type: String, required: true }, eventId: { type: String, required: true }, attending: { type: Boolean, required: true }, guests: Number, comments: String});module.exports = mongoose.model('Rsvp', rsvpSchema); 在 Node API 中,我们将利用它们从 MongoDB 中 12345678910// server/api.js/* |-------------------------------------- | Dependencies |-------------------------------------- */...const Event = require('./models/Event');const Rsvp = require('./models/Rsvp');... Angular 应用中的模型同样在前端 Angular 应用里我们也需要定义 Event 和 RSVP 模型,用来接受从 Node API 检索回来的数据。通过 CLI 创建两个 Class: 12$ ng g class core/models/event.model$ ng g class core/models/rsvp.model 打开生成的文件并添加: Event Model123456789101112// src/app/core/models/event.model.tsexport class EventModel { constructor( public title: string, public location: string, public startDatetime: Date, public endDatetime: Date, public viewPublic: boolean, public description?: string, public _id?: string ) {}} RSVP Model123456789101112// src/app/core/models/rsvp.model.tsexport class RsvpModel { constructor( public userId: string, public name: string, public eventId: string, public attending: boolean, public guests?: number, public comments?: string, public _id?: string ) {}} 在 MongoDB 里创建和初始化 Collections为了查询数据库,我们准备在 MongoDB 里创建必要的 collection 和一些原始数据。这一切都将通过之前提到的 MongoBooster 来完成: 创建 Collection通过 MongoBooster 连接到我们托管的 MyLab 数据库,并创建events和rsvps两个 collections. 添加原始数据打开 Mongo shell: Add Events12345678910db.getCollection("events").insert([{ "title": "Test Event Past", "location": "Home", "description": "This event took place in the past.", "startDatetime": ISODate("2018-05-05T06:00:00.000+08:00"), "endDatetime": ISODate("2018-05-05T08:00:00.000+08:00"), "viewPublic": true},...]) Add RSVP12345678910db.getCollection("rsvps").insert([{ "userId": "[Auth0_USER_ID]",--auth0|5c3dc9607493d4385206e45 "eventId": "[Event_Object_ID]",--5c3ed83c22a9361ec0ac215d "attending": true, "comments": "i will attend on time.", "guests": 5, "name": "chen zhuang"},...]) 记得替换上面相应的数据,userId对应 Auth0 上的已认证的用户,eventId对应我们已经插入的原始 event 数据。 小结在 Angular 实战系列的第 2 部分中,我们已经介绍了 MEAN 应用程序的身份验证和授权、功能规划和数据建模。在本系列教程的第 3 部分中,我们将使用 Node API 从数据库中获取数据,并使用 Angular 显示数据,完成过滤和排序。 系列索引 Angular 实战系列 - Part 1: MEAN 配置 & Angular 架构 Angular 实战系列 - Part 2: 身份验证和数据建模(你现在在这里) Angular 实战系列 - Part 3: 抓取和展示数据 Angular 实战系列 - Part 4: 访问控制,管理和详情页面 Angular 实战系列 - Part 5: 动画和模板驱动表单 Angular 实战系列 - Part 6: 响应式表单和自定义验证 Angular 实战系列 - Part 7: 相关数据和令牌更新 Angular 实战系列 - Part 8: 延迟加载,生产部署和 SSL]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 实战系列 - Part 1: MEAN 配置 & Angular 架构]]></title>
<url>%2Fposts%2F30078%2F</url>
<content type="text"><![CDATA[本系列教程的第一部分将介绍如何为实际的 Angular 应用程序设置云托管的 MongoDB 数据库、Node 服务器和前端。 介绍:我们打算做什么? 配置 Angular 应用 配置托管的 MongoDB Auth0 的配置 Node.js 服务器设置 Angular: 创建 HomeComponent Angular: 布局和全局组件 介绍:我们打算做什么?本系列教程将教你如何构建一个真实的基于MEAN技术栈的应用程序,涵盖从构思和数据建模到生产部署的所有内容。 为了了解生产级的 JavaScript web 应用程序开发的来龙去脉,我们将会构建一个围绕事件的应用程序。通过 RSVP 应用程序,管理员能够发布、更新和删除事件信息;其他用户能够回复事件。这个 RSVP 应用程序的功能将包括以下: 身份验证和角色授权(客户端和服务端) 使用 API 进行 CRUD 操作 搜索和过滤 模版驱动的表单 具有自定义验证的响应式表单 简单的动画 延迟加载 在 VPS 上使用 nginx 和 SSL 进行生产部署 废话不多说,开始吧! 配置 Angular 应用整个系列我们将使用Angular CLI 进行构建开发,所以保证你已经全局安装了 CLI: 1$ npm install -g @angular/cli Angular CLI: 6.1.5 Node: 10.7.0 Angular: 6.1.10 创建 Angular 项目安装好 CLI 后,打开终端并进入你想创建项目的路径,执行下面的命令: 1ng new mean-rsvp --routing --style scss CLI 会生成一个带有路由模块和 SCSS 支持的 Angular 项目。一旦项目依赖安装完成,我们就可以着手开发了。 添加 Title 服务为了能够在路由时动态地改变页面标题,我们需要使用 Angular 内置的Title服务。这是因为我们创建的是一个单页应用,<title>标签并不在我们的 Angular 应用程序内,所以我们无法对它进行操作。 app.module12345678910// src/app/app.module.tsimport {BrowserModule,Title} from '@angular/platform-browser'...@NgModule({ ..., providers:[ Title ], ...}) 添加 Bootstrap打开src/index.html并添加Bootstrap样式 CDN: 1234567891011121314<!-- src/index.html -->...<head> ... <title>RSVP</title> ... <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" /></head>... 或者通过angular.json配置styles引入 bootstrap 样式(此处略)。 全局 SCSS现在我们将添加一些 SCSS 来管理应用程序的全局样式,包含基本的布局和媒体查询。 首先在src/assets下创建新的scss文件夹,然后将src/styles.scss移动到这个新建的路径。接着修改 angular.json 中对应项目的styles: 12345... "styles": [ "assets/scss/styles.scss" ], ... 做完这些,项目的样式将管理在在 assets 文件夹中。 BASE STYLES123456789101112131415161718192021222324/* src/assets/scss/_base.scss *//*-------------------- BASICS--------------------*/body { min-width: 320px;}/*-- Cursor --*/a,input[type=button],input[type=submit],button { cursor: pointer;}/*-- Link Buttons --*/.../*-- Forms --*/.../*-- Helpers --*/... Bootstrap 提供了大量样式,_base.scss则提供了一些基本的帮助和改进样式。 VARIABLES AND PARTIALS创建新的文件夹src/assets/scss/partials,添加_layout_vars.scss文件: 1234567/* src/assets/scss/partials/_layout.vars.scss *//*-------------------- LAYOUT VARIABLES--------------------*/$padding-screen-small: 3%;$padding-screen-large: 1.5% 3%; 再添加_responsive.partial.scss文件: 12345678910111213141516/* src/assets/scss/partials/_responsive.partial.scss *//*-------------------- RESPONSIVE--------------------*//*-- Variables --*/$large: 'screen and (min-width: 768px)';/*-- Mixins --*/@mixin mq($mqString) { @media #{$mqString} { @content; }} 文件包含了一个$large变量,其中包含一个用于大屏幕大小的媒体查询,以及一个mq() 混合,用于在 SCSS 中轻松定位媒体查询。如果有必要,我们可以随着应用程序的增长向该文件添加更多变量。 IMPORT GLOBAL SCSS最后,我们整合这些创建的样式,以导入项目中。还记得前面项目样式放在assets/scss/styles.scss吗, 所以只需: 1234567/* src/assets/scss/styles.scss */// partials@import 'partials/layout.vars';@import 'partials/responsive.partial';// global styles@import 'base'; 配置托管的 MongoDBMongoDB是一个开源的文档数据库。为了提高速度和易用性,我们将在应用程序的数据库中使用mLab的免费云托管 MongoDB 部署。我们还将通过MongoBooster连接管理 MongoDB。 具体的账号注册,数据库创建连接管理这里就不再赘述,相信电脑前聪明的你很快就能搞定! Auth0 的配置到了这里说明你已经配置好数据库啦,真棒!接下来,我们的 Angular 应用程序和 Node API 将使用 IDaaS(身份即服务)平台Auth0进行身份验证和路由授权。 注册免费账号我们需要一个 Auth0 帐户来管理身份验证。你可以在这里注册一个免费帐户。接下来,设置一个 Auth0 应用程序和 API,这样 Auth0 就可以与 Angular 应用程序和 Node API 进行交互。 配置应用程序 进入 Dashboard,创建新的应用程序,如RSVP MEAN App,并选择SPA. 切换到Settings页面,Allowed Callback URLs:添加http://localhost:8083/callback和http://localhost:4200/callback. Allowed Web Origins,添加http://localhost:8083 和 http://localhost:4200. Allowed Logout URLs, 添加 http://localhost:4200. 高级设置的 OAuth 下的 JsonWebToken Signature Algorithm,确保设置为 RS256. 我们在回调 url 中添加了两个端口,并允许 web 源,因为我们将在开发期间从这两个端口运行和测试应用程序。端口 4200 是 Angular CLI 服务于 Angular 应用的端口。端口 8083 是我们的 Node API 和服务器使用的端口:为了测试产品构建,这是必要的。项目部署后,我们将替换这些设置为生产环境。 配置 API 切换到 API,创建 API 配置。输入 API 的名称(例如:RSVP 表示 API)。 将标识符设置为 API 端点 URL。此标识符是授权调用的audience参数。在我们的应用程序中,是http://localhost:8083/api/。 签名算法配置为RS256。 Node.js 服务器设置紧接着就是我们的 Node 服务器和 API 了。 安装依赖项在我们的 Angular 项目根目录下,运行下面命令: 1$ npm install express body-parser express-jwt jwks-rsa method-override mongoose cors --save 服务端文件结构在项目根目录下创建server文件夹并在其中添加两个文件:server/api.js和server/config.js,同时在根目录下创建server.js文件。至此文件结构大概如下: 12345678...server/ |- api.js |- config.jssrc/...server.js... 配置项打开server/config.js,添加如下配置: 12345module.exports = { AUTH0_DOMAIN: '[YOUR_AUTH0_DOMAIN]', // e.g. suchenrain.auth0.com AUTH0_API_AUDIENCE: '[YOUR_AUTH0_API]', // e.g. `http://localhost:8083/api/` MONGO_URI: 'mongodb://[USER]:[PWD]@[DS######].mlab.com:[PORT]/[DB_NAME]'}; 记住将上述的相应值替换成你的配置,这些配置可以在 Auth0 和 mLab 账号里找到。 Node Server打开server.js,添加如下配置: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384// server.js/* |-------------------------------------- | Dependencies |-------------------------------------- */// Modulesconst express = require('express');const path = require('path');const bodyParser = require('body-parser');const mongoose = require('mongoose');const methodOverride = require('method-override');const cors = require('cors');// Configconst config = require('./server/config');/* |-------------------------------------- | MongoDB |-------------------------------------- */mongoose.connect(config.MONGO_URI);const monDb = mongoose.connection;monDb.on('error', function() { console.error( 'MongoDB Connection Error. Please make sure that', config.MONGO_URI, 'is running.' );});monDb.once('open', function callback() { console.info('Connected to MongoDB:', config.MONGO_URI);});/* |-------------------------------------- | App |-------------------------------------- */const app = express();app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));app.use(methodOverride('X-HTTP-Method-Override'));app.use(cors());// Set portconst port = process.env.PORT || '8083';app.set('port', port);// Set static path to Angular app in dist// Don't run in devif (process.env.NODE_ENV !== 'dev') { app.use('/', express.static(path.join(__dirname, './dist')));}/* |-------------------------------------- | Routes |-------------------------------------- */require('./server/api')(app, config);// Pass routing to Angular app// Don't run in devif (process.env.NODE_ENV !== 'dev') { app.get('*', function(req, res) { res.sendFile(path.join(__dirname, '/dist/index.html')); });}/* |-------------------------------------- | Server |-------------------------------------- */app.listen(port, () => console.log(`Server running on localhost:${port}`)); 请注意,有几个部分是与环境相关的。对于开发,我们希望能够利用 Angular CLI 提供和监视文件的功能,而不需要每次检查工作时都构建一个完整的项目。为了便于实现这一点,我们将从开发中分离 Node.js 服务器和 Angular 前端开始。 这样,我们就可以在localhost:8083上运行 Node API,而 Angular 应用程序在localhost:4200上运行。对于生产环境,我们希望 Node 服务器运行 API 并使用静态路径来提供前端服务。 API 路由打开api.js文件并编辑: 1234567891011121314151617181920212223242526272829303132333435363738394041// server/api.js/* |-------------------------------------- | Dependencies |-------------------------------------- */const jwt = require('express-jwt');const jwks = require('jwks-rsa');/* |-------------------------------------- | Authentication Middleware |-------------------------------------- */module.exports = function(app, config) { // Authentication middleware const jwtCheck = jwt({ secret: jwks.expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json` }), audience: config.AUTH0_API_AUDIENCE, issuer: `https://${config.AUTH0_DOMAIN}/`, algorithm: 'RS256' }); /* |-------------------------------------- | API Routes |-------------------------------------- */ // GET API root app.get('/api/', (req, res) => { res.send('API works'); });}; Auth0 API 结合express-jwt和jwks-rsa一起使用,我们可以在必要时实现对特定 API 路由进行保护。实现这一点,我们可以通过向希望保护的路由添加jwtCheck中间件函数。 启动项目为了方便开发,全局安装nodemon来监视 Node 服务器的变化,而不需要在更新之后重新启动:npm install nodemon -g 开发阶段,我们会经常性的修改项目,所以打算使用分开的终端窗口来启动Angular App和Node API。 123456789# Angular App => http://localhost:4200$ ng serve# Node API => http://localhost:8083/api# Windows:$ SET NODE_ENV=dev$ nodemon server# OR Mac:$ NODE_ENV=dev nodemon server Angular: 创建 HomeComponent运行下面的命令,添加一个主页面组件: 1$ ng g component pages/home 将新创建的 Home 组件添加到路由: 12345678910// src/app/app-routing.module.tsimport {HomeComponent} from './pages/home/home.componet';const routes: Routes = [ { path: '', component: HomeComponent }];... 使用 Title 服务前面我们已经在Angular App Setup全局注入了Title服务,现在我们可以直接使用它: 123456789101112131415// src/app/pages/home/home.component.ts...import { Title } from '@angular/platform-browser';...export class HomeComponent implements OnInit { pageTitle = 'Events'; constructor(private title: Title) { } ngOnInit() { this.title.setTitle(this.pageTitle); }} 上述代码导入了 Title 服务,然后添加一个名为 pageTitle 的属性,默认值为Events。然后我们将 Title 服务传递给构造函数,在ngOnInit()生命周期方法中,我们将使用Title. settitle()方法将文档标题更改为本地pageTitle的值。通过将这个标题存储在属性中,我们还可以在组件的模板中使用它来设置标题: 12<!-- src/app/pages/home/home.component.html --><h1 class="text-center">{{ pageTitle }}</h1> 文档标题和标题现在应该显示在浏览器中。我们已经有了路由和 home 组件,接下来我们可以开始 Angular 应用的全局布局了。 Angular: 布局和全局组件接下来我们将会设置 Angular 应用的布局和全局元素,比如页眉、导航和页脚。我们希望应用程序可以在任何大小的浏览器中工作,因此我们将实现非画布导航。为此,我们需要向根应用程序组件AppComponent添加一些标记和功能,以及创建一个页眉和页脚。 12$ ng g component header$ ng g component footer 整个项目将会忽略相应的测试代码.spec.ts。 Header Component打开生成的HeaderComponent: 12345678910111213141516171819202122232425262728// src/app/header/header.component.tsimport { Component, OnInit, Output, EventEmitter } from '@angular/core';import { Router, NavigationStart } from '@angular/router';import { filter } from 'rxjs/operators';@Component({ selector: 'app-header', templateUrl: './header.component.html', styleUrls: ['./header.component.scss']})export class HeaderComponent implements OnInit { @Output() navToggled = new EventEmitter(); navOpen = false; constructor(private router: Router) {} ngOnInit() { // If nav is open after routing, close it this.router.events .pipe(filter(event => event instanceof NavigationStart && this.navOpen)) .subscribe(event => this.toggleNav()); } toggleNav() { this.navOpen = !this.navOpen; this.navToggled.emit(this.navOpen); }} HeaderComponent包含了一个导航链接和折叠开关,我们通过@Output声明一个EventEmitter用于和父组件进行交互,通知折叠按钮的闭合。 navOpen属性默认是闭合的,所以我们在组件的ngOnInit()钩子中通过观察路由事件,在路由开始时,闭合菜单面板。 当用户点击折叠按钮,会调用toggleNav()方法,它改变折叠状态,并向父组件传递新的状态,通知折叠按钮的变化。 header.component.html相关模板: HTML123456789101112131415161718192021222324<!-- src/app/header/header.component.html --><header id="header" class="header"> <div class="header-page bg-primary"> <a class="toggle-offcanvas bg-primary" (click)="toggleNav()" ><span></span ></a> <h1 class="header-page-siteTitle"> <a routerLink="/">RSVP</a> </h1> </div> <nav id="nav" class="nav" role="navigation"> <ul class="nav-list"> <li> <a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }" >Events</a > </li> </ul> </nav></header> CSS1234567891011121314/* src/app/header/header.component.scss *//*-------------------- HEADER--------------------*/@import '../../assets/scss/partials/layout.vars';/*-- Navigation --*/.../*-- Hamburger toggle --*/.../*-- Header and title --*/... 详细代码请查阅源码,这个文件提供了nav和header的样式,以及将折叠图标动画成X和back的 CSS 样式。值得注意的是,当访问当前组件外部的类时,可以使用特殊的选择器:host-context(.ancestor-class)来访问组件的封装之外的类并向上访问树。 Footer Component我们的底部非常简单,打开footer.component.html和footer.component.scss: HTML1234<!-- src/app/footer/footer.component.html --><p class="text-center"> MIT 2018</p> CSS12345678910111213/* src/app/footer/footer.component.scss *//*-------------------- FOOTER--------------------*/:host { display: block; padding-bottom: 10px;}p { font-size: 12px; margin-bottom: 0;} 上面把底部的 margin/padding(边距/填充) 移到宿主元素,这样段落边距就不会影响下一步窗口高度的计算。 App Component现在我们可以在根组件里使用 Header 和 Footer 了。打开app.component.ts: TS1234567891011121314151617181920212223242526272829303132333435// src/app/app.component.tsimport { Component, OnInit } from '@angular/core';import { fromEvent } from 'rxjs';import { debounceTime } from 'rxjs/operators';@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss']})export class AppComponent implements OnInit { navOpen: boolean; minHeight: string; private _initWinHeight = 0; constructor() {} ngOnInit() { fromEvent(window, 'resize') .pipe(debounceTime(200)) .subscribe(event => this._resizeFn(event)); this._initWinHeight = window.innerHeight; this._resizeFn(null); } navToggledHandler(e: boolean) { this.navOpen = e; } private _resizeFn(e) { const winHeight: number = e ? e.target.innerHeight : this._initWinHeight; this.minHeight = `${winHeight}px`; }} 上面创建了一个navOpen属性来存储HeaderComponent导航面板的状态。navToggledHandler将处理子组件 Header 发出的navToggled事件,并同步更新navOpen的值。同时,观察订阅窗口大小调整事件,调用_resizeFn()处理程序,以确保布局画布的高度与浏览器视图的高度匹配。 我们也可以通过 layout canvas 元素上设置height: 100vh样式来达到同样的效果,但是由于在移动浏览器中与 vh 不一致,所以采用了 JS 代码的方式。 打开app.component.html编辑模板: 123456789101112131415161718192021<!-- src/app/app.component.html --><div class="layout-overflow"> <div class="layout-canvas" [ngClass]="{ 'nav-open': navOpen, 'nav-closed': !navOpen }" [style.min-height]="minHeight" > <!-- HEADER --> <app-header (navToggled)="navToggledHandler($event)"></app-header> <!-- CONTENT --> <div id="layout-view" class="layout-view"> <router-outlet></router-outlet> </div> <!-- FOOTER --> <app-footer></app-footer> </div> <!-- /.layout-canvas --></div><!-- /.layout-overflow --> 上面使用了几个布局容器来管理导航面板,同时通过navOpen属性来动态添加/移除样式。还记得之前 Header 组件里提到的:host-context()吗,Header 组件的样式里就利用了上面的nav-open等这些类。 利用[style.min-height]可以动态改变元素的高度。注意这是一个 DOM 属性,而不是 HTML 属性。注意到其中的差别是很重要的。请务必通读绑定语法:HTML 属性 vs. DOM 属性 最后是app.component.scss,具体请查阅源码。 至此,我们已经完成了项目的基础结构和全局组件,可以进一步开发了。 总结这一部分介绍了 MEAN 技术栈应用程序所需的软件和工具的设置以及依赖关系。还建立了 Angular 前端的基本布局和架构。在 Angular 系列的下一部分中,我们将讨论身份验证和授权、功能模块规划和数据建模。 系列索引 Angular 实战系列 - Part 1: MEAN 配置 & Angular 架构(你现在在这里) Angular 实战系列 - Part 2: 身份验证和数据建模 Angular 实战系列 - Part 3: 抓取和展示数据 Angular 实战系列 - Part 4: 访问控制,管理和详情页面 Angular 实战系列 - Part 5: 动画和模板驱动表单 Angular 实战系列 - Part 6: 响应式表单和自定义验证 Angular 实战系列 - Part 7: 相关数据和令牌更新 Angular 实战系列 - Part 8: 延迟加载,生产部署和 SSL]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular实战系列文章汇总页]]></title>
<url>%2Fposts%2F6845%2F</url>
<content type="text"><![CDATA[Angular 实战系列将会带领你构建和部署一个完整的 JavaScript 应用程序,基于托管的MongoDB,Express,Angular以及Node.js技术栈(MEAN).GitHub源码:mean-rsvp GitHub repo 系列索引 Angular 实战系列 - Part 1: MEAN 配置 & Angular 架构 Angular 实战系列 - Part 2: 身份验证和数据建模 Angular 实战系列 - Part 3: 抓取和展示数据 Angular 实战系列 - Part 4: 访问控制,管理和详情页面 Angular 实战系列 - Part 5: 动画和模板驱动表单 Angular 实战系列 - Part 6: 响应式表单和自定义验证 Angular 实战系列 - Part 7: 相关数据和令牌更新 Angular 实战系列 - Part 8: 延迟加载,生产部署和 SSL]]></content>
<categories>
<category>Angular</category>
<category>Angular实战系列</category>
</categories>
<tags>
<tag>Angular实战</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 7 (formerly Angular 2) - The Complete Guide | Part 4]]></title>
<url>%2Fposts%2F55060%2F</url>
<content type="text"><![CDATA[Routingsetting up and loading routesnavigationrouter linksprogrammaticlly styling active router linksPassing and Retrieving parametersRoute Observables Query Parameters and Fragments]]></content>
<categories>
<category>Angular 2</category>
</categories>
<tags>
<tag>angular 7</tag>
<tag>udemy</tag>
<tag>the complete guide to angular 2</tag>
<tag>routing</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 7 (formerly Angular 2) - The Complete Guide | Part 3]]></title>
<url>%2Fposts%2F5461%2F</url>
<content type="text"><![CDATA[Using Services & Dependency InjectionService Creationlogging.service.ts12345export class LoggingService { logStatusChange(status: string) { console.log('new status:' + status); }} usage: demo.component.ts12345678910@Component({ ...})export class DemoComponent{ // it means demo component need a loggingService instance, and angular injector will give us // this instance from demo component providers / appcomponent providers / appmodule providers constructor(private logSvc: LoggingService){ }} Hierarchical Injector(service instance)There are 3 places where we can provide a service. AppModule: Same instance of service is available Application-wide. in our whole app in all components in all directives in all other services. AppComponent: Same instance of Service is available for all components(but not for other services) Any other component: Same instance of service is available for the component and all its child components. The instances don’t propagate up, they only go down that tree of components. in a lowest level it will actually even overwrite if we were to provide the same service on a hight level Injecting Services into Servicesservice can be injected into other service by using a specific metadata @Injectable. Let’s say you have a serveice A and target service, if you want inject A into target then you should add the @Injectable to target. Cross-Component Communication Through ServiceBy using event emiter service, components could talk to each other. logging.service.ts123export class LoggingService { statusChanged = new EventEmitter<string>();} component A12345678@Component({...})export class AComponent{ constructor(private logSvc: LoggingService){} onSetTo(){ this.logSvc.statusChanged.emit('changed'); }} component B123456789101112@Component({...})export class BComponent{ constructor(private logSvc: LoggingService){ this.logSvc.statusChanged.subscribe((status: string)=>{ alert('New status'+ status); }) } onSetTo(status: string){ this.logSvc.statusChanged.emit(status); }} Application-Wide services in Angular 6+If you’re using Angular 6+ (check your package.json to find out), you can provide application-wide services in a different way. Instead of adding a service class to the providers[] array in AppModule , you can set the following config in@Injectable() : 12@Injectable({providedIn: 'root'})export class MyService { ... } This is exactly the same as: 1export class MyService { ... } and 1234567import { MyService } from './path/to/my.service';@NgModule({ ... providers: [MyService]})export class AppModule { ... } Using this new syntax is completely optional, the traditional syntax (using providers[] ) will still work. The “new syntax” does offer one advantage though: Services can be loaded lazily by Angular (behind the scenes) and redundant code can be removed automatically. This can lead to a better performance and loading speed - though this really only kicks in for bigger services and apps in general.]]></content>
<categories>
<category>Angular 2</category>
</categories>
<tags>
<tag>angular 7</tag>
<tag>angular 2</tag>
<tag>udemy</tag>
<tag>the complete guide to angular 2</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 7 (formerly Angular 2) - The Complete Guide | Part 2]]></title>
<url>%2Fposts%2F54676%2F</url>
<content type="text"><![CDATA[Components & Databinding Deep DiveUsually we splitting Apps into Components. so how does component communicate with each other? Property BindingGenerally all properties or components are only accessible inside these components not from outside. But you can expose property to the world by adding @Input decorator, so that any parent component is now able to pass data to our exposed properties downstream. Binding to custom properties Syntax 12345// component@Input() customPropertyName: Type;// template<selector [customPropertyName]="expression"> Alias 12345// component@Input('alias') customPropertyName: Type;// template<selector [alias]="expression"> Event BindingThe other direction(upstream) what if we have a component and something changes in there and we want inform the parent component. Binding to custom events Syntax 12345678910111213141516171819// child component@Output() someEvent: new EventEmitter<dataType>();onOtherEvent(){ this.someEvent.emit(data)}// child template<element (otherEvent)="onOtherEvent()">//==================================================// host template<selector (someEvent)="someEvented($event)"></selector>// parent componentsomeEvented(data: dataType){} Alias 1234@Output('alias') someEvent: new EventEmitter<dataType>();...<selector (alias)="someEvented($event)"></selector>.... View Encapsulation Emulated: 0: default behavior. Use shimmed CSS that emulates the native behavior. Native: 1: Use shadow roots. This works only if natively available on the platform. None: 2 ShadowDom: 3: Use Shadow DOM to encapsulate styles. usage: 1234@Component({ ... encapsulations: ViewEncapsulation.ShadowDom}) Local References and @ViewChildIn template place a local reference on element then it will hold a reference to this element. you can use it directly only in current template context.usage: 12<element #someRef ></element><button (click)='demo(someRef.value)'></button> if you want getting access to the template&DOM, using @ViewChild. 123@ViewChild('someRef') someEle: ElementRef;...someEle.nativeElement.value; ng-content and @ContentChildprojecting content into component dynamically. such as Tabs 1234567891011121314151617// component 'demo' template code<div class="panel-heading"></div><div class="panel-body"> <ng-content></content></div>// another component template<demo> <p #projected >this is the content i want project into demo component</p></demo>// final html we will see<div class="panel-heading"></div><div class="panel-body"> <p>this is the content i want project into demo component</p></div> A question: how demo component get access to the projected content? The answer is @ContentChild 1234// same as above...// demo component code@ContentChild('projected') somePropertyName: ElementRef; Lifecycle ngOnChanges: called after a bound input property changes. ngOnInit: called once the component is initialized. ngDoCheck: called during every change detection run. ngAfterContentInit: called after content(ng-content) has been projected into view. ngAfterContentChecked: called every time the projected content has been checked ngAfterViewInit: called after the component’s view(and child views) has been initialized. ngAfterViewChecked: called every time the view(and child views) have been checked. ngOnDestroy: called once the component is about to be destroyed. Directives Deep DiveCreate Attribute DirectiveappBasicHighlight.directive.ts1234567891011import { Directive, ElementRef } from '@angular/core';@Directive({ selector: '[appBasicHighlight]'})export class BasicHighlightDirective { constructor(private elementRef: ElementRef) {} ngOnInit() { this.elementRef.nativeElement.style.backgroundColor = 'green'; }} usage: 1<p appBasicHighlight></p> Enhancing directive using the Renderer: better-highlight.directive.ts1234567891011import { Directive, Renderer2 } from '@angular/core';@Directive({ selector: '[appBetterHighlight]'})export class BetterHighlightDirective { constructor(private elRef: ElementRef, private renderer: Renderer2) {} ngOnInit() { this.renderer.setStyle(this.elRef.nativeElement, 'background-color', 'blue'); }} usage: 1<p appBetterHighlight></p> WhyAngular is not limited to running in the browser and for example also works with service worker. it is a better practice to use the renderer for dom access instead of accessing the native element directly. HostListener and HostBindingHostListener: Listen to Host Events.HostBinding: Bind to the Host properties. 1234567891011121314151617181920212223import { Directive, RendererV2, ElementRef, HostListener, HostBinding } from '@angular/core';@Directive({ selector: 'hoverHighlight'})export class HoverHighlightDirective { @HostBinding('style.backgroundColor') backgroundColor: string = 'transparent'; constructor(private elRef: ElementRef, private renderer: RendererV2) {} ngOnInit() {} @HostListener('mouseenter') mouseover(eventData: Event) { this.render.setStyle(this.elRef.nativeElement, 'background-color', 'blue', false, false); //this.backgroundColor = 'blue'; } @HostListener('mouseleave') mouseleave(eventData: Event) { this.render.setStyle(this.elRef.nativeElement, 'background-color', 'transparent', false, false); //this.backgroundColor = 'transparent'; }} Binding to Directive Properties12345678910111213141516171819202122232425262728import { Directive, RendererV2, ElementRef, HostListener, HostBinding, Input } from '@angular/core';@Directive({ selector: 'hoverHighlight'})export class HoverHighlightDirective { @Input() defaultColor: string = 'transparent'; @Input() hightlightColor: string = 'blue'; // using alias see usage 2 @Input('hoverHighlight') hightlightColor: string = 'blue'; @HostBinding('style.backgroundColor') backgroundColor: string; constructor(private elRef: ElementRef, private renderer: RendererV2) {} ngOnInit() { this.backgroundColor = this.defaultColor; } @HostListener('mouseenter') mouseover(eventData: Event) { this.backgroundColor = this.hightlightColor; } @HostListener('mouseleave') mouseleave(eventData: Event) { this.backgroundColor = this.defaultColor; }} template12345<p hoverHighlight [defaultColor]="'yellow'" [highlightColor]="'red'">just a demo here!</p>// short cut - no square bracket<p hoverHighlight defaultColor="yellow" highlightColor="red">just a demo here!</p>// usage 2 note alias!<p [hoverHighlight]="'red'" [defaultColor]="'yellow'">just a demo here!</p> what happens behind Structural DirectiveAs you know angular structual directive prefix with a star(*). ng-template is a element that itself not rendered and it define a template for angular to use(determine which to render). 12345<div *ngIf="onlyOdd"><p>something</p></div>// will be transformed to property binding as below<ng-template [ngIf]="onlyOdd"> <div><p>something</p></div></ng-template> Create Structual Directive like *ngIfwe will create our owned structual directive with the same functionality as *ngIf. 123456789101112131415@Directive({ selector: 'appUnless'})export class UnlessDirective { @Input() set appUnless(condition: boolean) { if (!condition) { this.vcRef.createEmbeddedView(this.templateRef); } else { this.vcRef.clear(); } } constructor(private templateRef: TemplateRef<any>, private vcRef: ViewContainerRef) {}} usage: 12345<div *appUnless="false"><p>some thing.</p></div>// which will be transformed to below<ng-template [appUnless]="false"> <div><p>some thing.</p></div></ng-template>]]></content>
<categories>
<category>Angular 2</category>
</categories>
<tags>
<tag>angular 7</tag>
<tag>angular 2</tag>
<tag>udemy</tag>
<tag>the complete guide to angular 2</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular 7 (formerly Angular 2) - The Complete Guide | Part 1]]></title>
<url>%2Fposts%2F4782%2F</url>
<content type="text"><![CDATA[Component Selector12345678910# component selector## element'app-demo'usage: <app-demo></app-demo>## attribute'[app-demo]'usage: <div app-demo></div>## class'.app-demo'usage: <div class='app-demo'></div> Data Binding String Interpolation(插值)Syntax: 1{{ expression }} expression represent a String value or something that can be converted to String or a function that returns what we metioned above. Property Binding(属性绑定)Syntax: 1<element [property]="expression"></element> property represent the bindable html element property.expression represent a String value or something that can be converted to String or a function that returns what we metioned above. String Interpolation vs Property BindingIn most cases they are equivalent: 123<p>{{ someText }}</p># equals to<p [innerText]="someText"></p> when should you use which of the two: string interpolation: if you want output something in your template print some text. property binding: if you want to change some property be that of an aged female element or a directive or a component. Don’t mix property binding and string interpolation. {{ expression }} won’t work between "". Event BindingSyntax: 1// use parentheses <element (event)="func()"></element> event the bindable event name, e.g. click keyup mouseenterfunc the callback function when event triggered. Two-Way-DatabindingSyntax: 1// use square brackets and parentheses <element [(ngModel)]="model"></element> ngModel the built-in angular directive.model the view model a property difined in our typescript code.Important : For Two-Way-Binding (covered in the next lecture) to work, you need to enable the ngModel directive. This is done by adding the FormsModule to the imports[] array in the AppModule.You then also need to add the import from @angular/forms in the app.module.ts file:import { FormsModule } from '@angular/forms'; Directives(指令)Directives are Instructions(说明) in the DOM! Structural Directivethis type of directive will change the structure of our dom: add or remove elements. Built in directives prefix with *ng. *ngIf : output data conditionally syntax: <element *ngIf="expression"></element> usage: when expression eval as true then add this element to dom otherwise remove it. *ngIf else: syntax: 1<element *ngIf="expression; else someRef"> <ng-template #someRef></ng-template></element> usage: when expression eval as false then add the corresponding template to dom. *ngFor syntax: <element *ngFor="let x of someArray ; let i = index"></element> usage: iterate each item of someArray as the template context. Tip: Performance improvement when using ngFor to loop over an array in templates, use it with a trackBy function which will return an unique identifier for each item. Attribute DirectiveUnlike structural directives, attribute directives only change the element they were placed on. Built in directives ngStyle syntax: <element [ngStyle]="styleObject"></element> styleObject: e.g. {backgroundColor: getColor()} or {'background-color': someVarible} ngClass syntax: <element [ngClass]="{className: expression}"></element> className: CSS class name expression: expression evaluated as boolean. True add the class above. 123456<div [ngClass]="{'class1 class2':expression}"></div><div [ngClass]="{class3: expression1, class4: expression2}"></div><div [ngClass]="{'class-with-dash': expression}"></div>// [class.<class-name>]='truthy expression'<div [class.text-success]="expression"></div> Debugging Syntax Error: error message details in console. Logic Error: debug source code directly or through source map. Augury: you can install this extension tool to help analyse the code.]]></content>
<categories>
<category>Angular 2</category>
</categories>
<tags>
<tag>angular 7</tag>
<tag>angular 2</tag>
<tag>udemy</tag>
<tag>the complete guide to angular 2</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Angular Library系列 - 使用CLI创建自己的库]]></title>
<url>%2Fposts%2F8912%2F</url>
<content type="text"><![CDATA[随着Angular 6的发布, Angular CLI在一定程度上可以说是有了很大的提升。Angular CLI集成ng-packagr构建生成 Angular 库就是其中之一。ng-packagr 是由 David Herges 创建的一个很棒的工具,用于将你的库代码转换成官方约定的Angular 包格式。 接下来的内容中,你将体验创建一个自己的 Angular 库的具体过程。并且,为了避免陷入不必要的麻烦,创建的过程中我会着重强调一些实用的规则。 相应的GitHub 代码 介绍通过ng new Angular CLI 为我们创建了一个新的工作区(workspace). 在这个 Angular workspace中我们将创建两个项目: A library Project这是包含我们想要创建的组件和服务的库。这些代码是我们可以发布到npm等第三方库提供商的。 An application project这个项目用来测试展示我们的库。有时候此项目用作库的文档说明或者用例演示。 这里还会有 Angular CLI 默认为我们创建的 e2e 测试项目,不过本文中我们将忽略它。 现在我们已经对我们的 Angular workspace 有了一个大概的认识,接下来是本教程一些具体的目标: 目标 使用 Angular CLI 创建一个与我们打算创建的库名称一样的工作区(workspace):example-ng6-lib 创建名为example-ng6-lib-app的应用 创建名为example-ng6-lib库 使用enl作为库前缀 在example-ng6-lib-app里导入example-ng6-lib库并使用 Angular 6在写作本文的时候,Angular 6 还是刚出来不久,所以和以前的版本会有些细小的出入。 Angular CLI 的版本号开始和 Angular 保持一致:从 1.7 跳跃到 6.0.0 Angular CLI 的配置文件将使用angular.json替换原有的angular-cli.json 现在 CLI 生成的工作区(workspace)同时支持多个项目 创建 Angular 工作区我们的第一个目标是创建一个名为example-ng6-lib的Angular 工作区。由于 CLI 现有的工作方式,我们需要以一种循环的方式来做这件事。我们需要创建一个名为example-ng6-lib-app的工作空间,然后将其重命名为example-ng6-lib(因为 CLI 默认会生成与工作区同名的 application 和 e2e 项目): 1234ng new example-ng6-lib-apprename example-ng6-lib-app example-ng6-libcd example-ng6-libng serve 如果想要支持 IE, 需要做额外的一些工作:Angular 和 IE Angular 6 配置:angular.json在我们进行下一步创建库之前,先让我们快速的看一下新的 Angular 配置文件:angular.json 相较于旧的 angular-cli.json, 配置内容变化许多。 最主要的是新增的projects对象,每个项目对应一个条目 12345678"projects": { "example-ng6-lib-app": { ... }, "example-ng6-lib-app-e2e": { ... }}, 到目前为止,我们只有两个 project: example-ng6-lib-app: 这是我们打算用来测试库的应用项目 example-ng6-lib-app-e2e:这是默认的端到端测试项目(暂时忽略) 请记住,我们告诉 CLI 创建名为:example-ng6-lib-app的工作空间. 然后,CLI 我们创建了一个名为example-ng6-lib-app的默认应用程序。这让我们有机会将库项目命名为:example-ng6-lib。一旦我们创建了我们的库,对应的条目将添加到这个 projects 对象中。 Notes:使用库+app来命名工作区,然后将其重命名为库的名称。 创建库模块现在让我们在工作区创建名为example-ng6-lib的库。 1ng generate library example-ng6-lib --prefix=enl 上面我们使用了--prefix选项,使得库元素特定的前缀。否则将默认使用 lib 作为前缀。 generate library 具体做了什么呢? 在angular.json文件中添加example-ng6-lib project条目 在package.json中为 ng-packagr 添加相应的依赖 在tsconfig.json文件里为 example-ng6-lib 添加构建路径的引用 在projects/example-ng6-lib路径下创建初始源代码 我们可以再具体深入地看看各自做了啥? example-ng6-lib project in angular.json仔细瞧瞧angular.json,你会发现projects下面多了一条记录:example-ng6-lib 这里有几个值得注意的元素: root指向库项目的根文件夹。 sourceRoot指向存放库的实际源代码的根路径。 projectType项目类型:library 和application. prefixCLI 生成组件的选择器前缀标识符 architect包含很多个部分比如build,test和lint,这些设置告诉 Angular CLI 如何处理相应的 process.请注意,在构建的部分,使用的是ng-packagr. ng-packagr dependency in package.json当我们创建库的时候 Angular CLI 自己就已经意识到它需要用到ng-packagr,所以它在我们的工作区 package.json 里面添加了相应的devDependencies: 1"ng-packagr": "^3.0.0", build path in tsconfig.json当我们想测试我们的 example-ng6-lib 的时候,我们希望能像引入第三方库一样使用它,而不是作为应用的一组文件集。通常,当我们在项目中使用第三方类库时,我们通过npm install将库安装到node_modules文件夹内。 尽管如此,example-ng6-lib 并不会安装到node_modules,而是将被构建到我们工作空间的dist文件夹的子文件夹中。Angular CLI 把这个文件夹添加到tsconfig.json中使得它可以作为一个库导入到应用中。 12345678"paths": { "example-ng6-lib": [ "dist/example-ng6-lib" ], "example-ng6-lib/*": [ "dist/example-ng6-lib/*" ]} example-ng6-lib 源代码src文件夹位于projects/example-ng6-lib. Angular CLI 默认为新库创建了一个包含服务和组件的模块,此外还有另外一些文件: package.json这是针对库的特定 package.json 文件。当库作为一个npm包发布时,这个文件一会一同发布,所以当别人通过 npm 使用我们的库时,就会安装这个文件里指定的依赖。 public_api.ts这就是所谓的入口文件。它定义了我们库的哪些部分是外部可见的。你可能会问:这不是模块中的export做的事情吗?的确是的,但这可能比那更复杂一点。我们会在后面再讨论这个。 ng-package.jsonng-packagr的配置文件。如果是在以前,我们需要熟悉它的内容。但是现在,新的 Angular CLI 足以告诉 ng-packagr 在哪里找到我们的入口文件以及在哪里构建我们的库。 库构建在使用我们新创建的库之前,我们需要对它进行编译: 1ng build --prod example-ng6-lib 编译后的库文件将会放在example-ng6-lib-app\dist\example-ng6-lib 使用--prod选项可以确保我们更早地发现预编译错误。 在项目应用中使用库构建一个库的核心思想之一就是,我们通常有一个和库一起构建的应用程序,以便测试它。 这里example-ng6-lib-app将会使用我们的库。 导入 example-ng6-lib 模块修改AppModule模块:src\app\app.module.ts 把ExampleNg6LibModule添加到imports数组中。可能你使用的 IDE 会提示你引入相应的模块文件,但是请不要相信它!我们应该通过库名称导入它: 1import { ExampleNg6LibModule } from "example-ng6-lib"; 这是可行的,因为当以名称导入库时,Angular CLI 首先查找tsconfig.json paths,然后是 node_modules。 NOTE:在测试应用程序中,使用名称而不是单独的文件来导入库。 至此,app.module.ts 文件应该差不多像这样: 使用 example-ng6-lib 组件简单起见,我们在 AppComponent 模板里面使用类库默认创建的组件。 我们替换 AppComponent 模板的下半部分代码: 1<enl-example-ng6-lib></enl-example-ng6-lib> 修改后的src\app\app.component.html 最后运行ng serve查看效果。 拓展我们的库到目前为止我们已经知道如何构建并运行测试我们的类库。接下来给我们的类库添加一个新的组件。 下面是将要进行的步骤: 在类库中创建新的组件 将创建的组件在模块文件中导出 将新的组件添加到入口文件总 重新编译我们的类库 在测试应用中使用新的组件 创建类库组件通过--project选项告诉 Angular CLI 我们想要给指定的类库项目创建新的组件。现在创建一个简单的名叫foo的类库组件: 1ng generate component foo --project=example-ng6-lib CLI 自动帮我们创建了这个组件并将它添加到类库模块文件(projects\example-ng6-lib\src\lib\example-ng6-lib.module.ts)的declarations数组中. 从类库模块中导出组件]]></content>
<categories>
<category>Angular</category>
<category>Library</category>
</categories>
<tags>
<tag>angular 6</tag>
<tag>angular library</tag>
</tags>
</entry>
<entry>
<title><![CDATA[谷歌云搭建免费VPN]]></title>
<url>%2Fposts%2F37388%2F</url>
<content type="text"><![CDATA[仅供技术学习探索,请合理科学上网,遵纪守法。 # 前面Google Cloud Platform 是 Google 提供的云端服务,目前提供 365 天免费的 300 美金优惠试用,并且超过 365 天或者赠金已消费完,没有你的允许它也不会继续扣款,以下是我在上面搭建个人 VPN 的过程,欢迎参考:D 整体大概流程:申请免费谷歌云服务 => 创建虚拟实例 => 在实例搭建SSR或L2TP/IPsecVPN 服务 # 申请谷歌云服务 # 准备工作 能够科学上网(搭建过程需要爬梯) 谷歌账号 一张双币信用卡(VISA/JCB/MasterCard) # 账号申请申请地址: https://cloud.google.com/free/?hl=zh-cn 国家和地区资料填写,我们在右侧栏可以看到 12 个月有效期的 300 刀赠额说明。国家地区选择真实所在地,这里我选择中国,勾选同意条款,点击同意并继续: 接下来进行个人资料和信用卡资料的填写,个人资料和信用卡资料如实填写就好了。账号类型选择个人,个人资料和信用卡信息填写完成之后直接点击开始免费试用选项即可。 接着你的信用卡账户消费 1 美金,这是谷歌为了确认你不是机器人用于验证账户的, 5 分钟内你会收到账户撤销 1 美金的消费操作,1 美元会返回你的账户,整个过程完全免费。另外除非你主动确认升级成付费账号,300 美金的赠款消费完毕以后并不会直接从你的信用卡账户续费扣款。 # 创建实例直接访问 https://console.cloud.google.com/compute/instances 或登录 GCP 控制台后, 点击计算引擎 - 创建实例. 如图: 12345678# 可免费使用1年,价值300美金。# Google Cloud Platform计算引擎是按小时收费,网络按流量收费.# 我们可以算一算:# 注册赠送了300美金, 一年免费使用期.# 主机选的是微型(最低配置机型), $5/月.# 还剩300-5*12=$240, 用于抵扣流量的费用.# 谷歌云服务器出口大陆流量1T以内价格约为0.23$/1G.# 那么每个月可用流量 = 240/12/0.23 ≈ 86G, 一般日常使用绝对够用。 名称: 随便填 地区: 建议选 asia-east1-c, asia-east1-a, asia-east1-b, asia-east1-c 机房都在中国台湾彰化县, 实测 c 区更好. 机器类型: 选微型(一个共享 vCPU) 0.6G 内存, 一般加速上网, 看视频, 玩游戏都够用了. 不够再换 启动磁盘: 推荐选 CentOS 7. 防火墙允许 HTTP 和 HTTPS 本文命令都是基于 CentOS 7, 如选 Debian, Ubuntu 等其他系统, 命令会稍有不同 # 外部静态 IP两种方式创建外部静态 IP: 在创建实例阶段,选择网络并将外部 IP 设置为静态和保存 VPC 网络=>外部 IP 地址修改相应实例地址类型为静态 # IPsec 服务器搭建# 防火墙规则VPC 网络=>防火墙规则, 建立新的规则: 12345# 名称:ipsecvpn# 流量方向: 输入# 相符时执行的动作:允许# 目标 IP范围: 0.0.0.0/0# 设定指定通讯: udp:500;udp:4500;esp # 设定 VPC IP 转发VPC 网络=>default=>编辑=>动态传送模式设置为全域=>保存 # 负载均衡网络服务=>负载平衡=>建立负载平衡=>UDP 负载平衡=>从网际网路到我的 VM=>继续 1234567##后端设定# 区域:刚才实例创建所选区域# 后端:选择创建的现有实例##前端设定# IP: 临时# 端口:500-4500 最后填入名称并建立。 # 配置 ipsec vpn 服务将使用setup-ipsec-vpn搭建 IPsec VPN 服务器 回到 VM 实例列表,通过 SSH 连接到虚拟机器:运行yum update并重启。(可选步骤,推荐) 1234wget https://git.io/vpnsetup-centos -O vpnsetup.shvim vpnsetup.sh# [替换设置相应的值: YOUR_IPSEC_PSK, YOUR_USERNAME 和 YOUR_PASSWORD]sudo sh vpnsetup.sh 管理VPN用户 在默认情况下,将只创建一个用于 VPN 登录的用户账户。如果你需要添加,更改或者删除用户,请阅读本文档。 首先,IPsec PSK (预共享密钥) 保存在文件/etc/ipsec.secrets 中。如果要更换一个新的 PSK,可以编辑此文件。所有的 VPN 用户将共享同一个 IPsec PSK。 1%any %any : PSK "你的 IPsec 预共享密钥" 对于IPsec/L2TP,VPN 用户账户信息保存在文件 /etc/ppp/chap-secrets。该文件的格式如下: 123"你的 VPN 用户名 1" l2tpd "你的 VPN 密码 1" _"你的 VPN 用户名 2" l2tpd "你的 VPN 密码 2" _... ... 你可以添加更多用户,每个用户对应文件中的一行。不要 在用户名,密码或 PSK 中使用这些字符:\ " ' 对于 IPsec/XAuth ("Cisco IPsec"), VPN 用户账户信息保存在文件 /etc/ipsec.d/passwd。该文件的格式如下: 123你的 VPN 用户名 1:你的 VPN 密码 1 的加盐哈希值:xauth-psk你的 VPN 用户名 2:你的 VPN 密码 2 的加盐哈希值:xauth-psk... ... 这个文件中的密码以加盐哈希值的形式保存。该步骤可以借助比如 openssl 工具来完成: 123# 以下命令的输出为:你的 VPN 密码 1 的加盐哈希值openssl passwd -1 '你的 VPN 密码 1' 最后,如果你更换了新的 PSK,则需要重启服务。对于添加,更改或者删除 VPN 用户,一般不需重启。 12service ipsec restartservice xl2tpd restart 至此,搭建完成。 VPN连接测试 配置 IPsec/L2TP VPN 客户端配置 IPsec/XAuth (“Cisco IPsec”) VPN 客户端 iPhone设定=>VPN=添加配置图 windowsto be done linux # SSR 的搭建]]></content>
<categories>
<category>工具</category>
</categories>
<tags>
<tag>科学上网</tag>
<tag>vpn</tag>
</tags>
</entry>
<entry>
<title><![CDATA[配置全局及自定义主题样式]]></title>
<url>%2Fposts%2F1616%2F</url>
<content type="text"><![CDATA[先说问题吧。 最近在看 primeng 的源码,框架是支持主题切换的,主题样式使用 sass,主题的切换是通过动态替换全局主题 css 文件来实现的。 12345changeTheme(event: Event, theme: string) { let themeLink: HTMLLinkElement = <HTMLLinkElement> document.getElementById('theme-css'); themeLink.href = 'assets/components/themes/' + theme + '/theme.css'; event.preventDefault();} 所以想知道在使用 Angular CLI 构建项目过程中这些theme css如何生成? primeng 源码本身貌似是使用了 gulp,但是 Angular CLI 本身应该也可以做到,一探究竟。 Angular CLI: 6.1.1Node: 10.7.0OS: win32 x64Angular:… 通过在angular.json文件的项目build选项中配置styles,我们可以添加更多的全局样式: 12345678910"architect": { "build":{ "builder": "@angular-devkit/build-angular:browser", "options": { "styles": [ "node_modules/fullcalendar/dist/fullcalendar.min.css", "node_modules/font-awesome/css/font-awesome.min.css", "src/styles.css", { "input": "src/resources/themes/cruze/theme.scss","bundleName": "assets/themes/cruze","lazy": true } ], 其中bundleName指定了input里面的样式编译后输出的目录,如果不使用这种对象格式,则会全部一起合并打包生成styles.js (或者 styles.css)lazy: true 表示只生成该样式,但并不引入到index.html文件中。否则,会自动在index.html中引入生成的文件。 使用 –extract-css build 或者 prod 模式则会生成.css 所以如果使用ng run build --extract-css, 上述配置将会把 resouces 下相应的 theme.scss 编译成 css 并放入 assets/themes 目录下。 另外一个问题,如果使用生产模式编译,最终生成的 css 会附带 hash 串。通过--output-hashing可以设置输出 hash 的模式,可能的值有: none: no hashing performed media: only add hashes to files processed via [url|file]-loaders bundles: only add hashes to the output bundles all: add hashes to both media and bundles none is the default for the development target.all is the default for the production target. 目前想到的解决方法:在 build 完成后,使用脚本去修改名称。类似 rename.js123// rename.js file - location, right next to package.jsonvar fs = require('fs');fs.rename('./dist/main.bundle.js', './dist/myproject.js'); 和 package.json123"scripts":{ "build-rename":"ng build && node rename.js"}]]></content>
<categories>
<category>Angular CLI</category>
</categories>
<tags>
<tag>theme</tag>
<tag>style</tag>
</tags>
</entry>
<entry>
<title><![CDATA[angular-cli配置相关索引]]></title>
<url>%2Fposts%2F25741%2F</url>
<content type="text"><![CDATA[有关angular cli使用及配置的文章的一个快速目录,当作是一个索引。 CLI中配置编译额外的styles(第三方库样式,自定义主题样式等)]]></content>
<categories>
<category>Angular</category>
<category>Angular CLI</category>
</categories>
<tags>
<tag>Angular CLI</tag>
</tags>
</entry>
<entry>
<title><![CDATA[windows命令行工具cmder]]></title>
<url>%2Fposts%2F53829%2F</url>
<content type="text"><![CDATA[To be honest: linux 或者 Mac 的命令行工具要比 windows 的好用很多。但是 windows 下也不乏一些利器,比如今天要说到的cmder,其虽没oh-my-zsh那么逆天的存在,却也甚是好用,至少要比 Windows 原生 Cmd 好出了天际。 IntroductionLovely console emulator package for Windows 官网: http://cmder.net/ 就像它自己说的,它将 conemu,msysgit 和 clink 打包在一起,让你无需配置就能使用一个真正干净的 Linux 终端!还附带了漂亮的 monokai 配色主题。作为一个压缩档的存在, 可即压即用。你甚至可以放到 USB 就可以随时带着走,连调整过的设定都会放在这个目录下,不会用到系统机码(Registry),所以也很适合放在 Dropbox / Google Drive / OneDrive 共享于多台电脑。 Install下载的时候,会有两个版本,分别是 mini 与 full 版;唯一的差别在于有没有内建 msysgit 工具,这是 Git for Windows 的标准配备;全安装版 cmder 自带了 msysgit, 除了 git 本身这个命令之外, 里面可以使用大量的 linux 命令;比如 grep, curl(没有 wget); 像 vim, grep, tar, unzip, ssh, ls, bash, perl 对于爱折腾的 Coder 更是痛点需求。 下载完成后,解压到某个目录直接运行就可以了。 Configue配置环境变量系统变量添加: 12Key: CMDER_HOMEValue: absolute path PATH 添加 _%CMDER_HOME%_ 添加到右键菜单: 123# win + x 再 a 以管理员身份打开cmd# 执行Cmder.exe /REGISTER ALL 修改提示符 lambda(λ)change the λ replace it in the ff files: 123- cmder\vendor\profile.ps1- cmder\vendor\clink.lua- cmder\vendor\git-for-windows\etc\profile.d\git-prompt.sh]]></content>
<categories>
<category>工具集</category>
<category>cmd</category>
</categories>
<tags>
<tag>cmder</tag>
<tag>cmd</tag>
<tag>windows命令行工具</tag>
</tags>
</entry>
<entry>
<title><![CDATA[小狗钱钱笔记]]></title>
<url>%2Fposts%2F57458%2F</url>
<content type="text"><![CDATA[You should always have your own judgement. preface 富裕是我们与生俱来的的权利。 生活为我们提供了众多的财富,而金钱只是其中之一。 成功的故事很少有精彩的翻版,但其中的道理可以帮助我们找出最佳的出路。 个人的经历是无法效仿的,但最基本的真理却可以。 故事这种形式会让人们以为书缺乏深度,其实一个能打动人的故事要远比一幅美丽的图画更值得我们花费笔墨去描述。 一切都是可能的,忽视是一种认输。并非困难使我们放弃,而是因为我们放弃,才显得如此困难。 我也不认为钱是人生命中最重要的东西。可是,假如我们缺钱的话,钱就会变得格外重要。 第一章 会说话的钱钱十个我想变得富有的原因(wish list): 所在的城市有个真正的安身之所 自己的车 带父母和爱的人去旅游 保障家人的生活(有所爱,有所依) 将来给自己的孩子更好的条件 开一家科技公司 自己的书房 开一家自己的点心店 和爱的人养育好多个孩子 帮助身边的人,让生活更和谐美好 金钱有一些秘密和规律,要想了解这些秘密和规律,前提条件是,你自己必须真的有这个愿望。 第二章 梦想储蓄罐和梦想相册每一个有所成就的人的成功秘诀就是他曾梦想着有成功的那一天,他们不停地想象着自己实现梦想时地情形。 从愿望清单里选出 3 个你最想实现的愿望,并且每天都把它从头到尾看一遍,不断提醒自己想得到什么。 大多数人并不清楚自己想要的是什么。他们只知道,自己想得到更多的东西。我们的愿望也是一样,我们必须确切地知道自己心里渴望的是什么才行。当然,还要为此付出努力。 同样的,技术上我到底想学什么呢?我是不是也可以运用一样的方法来知道呢。 有时候不需要完全明白这种方法为什么有效,也不必管它为什么有效,关键是它有效。 好奇是好的,但是你绝不能因为好奇而阻碍你去做一件事情。太多的人做事犹豫不决,就是因为他们觉得没有完全弄懂一样东西。而真正付诸实践要比纯粹的思考聪明多了。要想学会游泳,最好就是下水去玩呢。 如果你只是带着试试看的心态,那么你最后只会以失败而告终。你会一事无成,尝试纯粹是一种借口。你还没有做,就已经给自己想好了退路了,不能试验。你只有两个选择,做或者不做。 第三章 一个很能挣钱的男孩你能否挣到钱,最关键的因素并不在于你是不是有一个好点子,你有够聪明也不是主要原因,决定因素是你的自信程度。 树立自信,成功日记。每天都把做成功的事情记录进去,任何小事都可以,每次至少 5 条。可能你不确定这件或那件事情是否真的可以算作成果,这种情况下,你永远应该做出肯定的回答。过于自信比不够自信要好的多。 12345678910111213# 2018-07-221.开始认真阅读小狗钱钱,并且做了笔记。2.明白了鹅,金蛋和梦想储蓄罐,并且大概知道如何平衡他们的关系。3.没有叫外卖,走了比较远的地方吃午饭和晚饭。4.今天也把衣服手洗了。5.坚持写了日记。# 2018-07-231.今天也重复了10分钟。2.今天也坚持写了日记。3.认真完成了理财训练营的第一个任务。4.鼓励了小伙伴。5.今天也按时洗了衣服冲了凉。 一个人把精力都集中在自己所能做的,知道的和拥有的东西上的那一天起,他的成功就已经拉开了序幕。 第四章 表哥的挣钱之道你最好想清楚,你喜欢做什么,然后再考虑你怎么用它来挣钱。 不去寻找机会的人,最多不过是在走运的时候得到天上掉下来的馅饼。告诉你我为什么能够挣到这么多钱,那是因为我自己有一个公司。 情况顺利的时候,人人都能挣到钱。只有在逆境时,一切才见分晓。 第五章 钱钱以前的主人三件很重要的事情: 你应该在自己遇到困难的时候,仍然坚持自己的意愿。当一切正常的时候,每个人都能做到这一点。可是当真正的困难出现的时候,才见分晓。困难总是在不断地出现。尽管如此,你要每天不间断地去做对你的未来意义重大的事情。为此花费的时间不超过 10 分钟。但是就是这 10 分钟会让一切变得不同。这 10 分钟就是你用来改变自己的最好方式。因为要想情况能向着有利于自己的方向转化,首先必须先改变自己。 在事情进展的非常顺利的情况下,也应该坚持做这些事情。因此你应该每天在固定的时间里,有规律地做这些事情。 72 小时规定。当你决定做一件事情地时候,你必须在 72 小时之内完成它,否则你很可能就永远不会再做了。 第六章 负债4 个忠告: 撕毁信用卡(使用信用卡时往往产生更多消费支出) 尽可能少地偿还贷款 应当将不用于生活的那部分钱中地一半存起来,另一半用于还债,最好不要申请消费贷款。 每次借债前问自己,这真的有必要吗? 第七章 在金先生家可是请你告诉我,你为什么不能因为做了一件自己喜欢地事情而挣到钱呢? 鹅与金蛋的故事:你的存款就是你的鹅,产生的利息就是你的金蛋。1/2 的收入变成我的鹅,40%放入梦想储蓄罐,剩下的 10%用来花。 当你定下了大目标的时候,就意味着你必须付出别人多得多的努力。 第八章 陶穆太太理财投资,这是一件非常有趣的事情。 我只和自己尊重的人打交道。 第九章 adventure就是找到跟你最合得来的人。 和善的顾问。 等候是世界上最愚蠢的事情,我们应该想想如何利用这段时间。 长大吧,小鹅,长大吧。 假如我没有了我的鹅,我就总是得为了赚钱而工作,但是一旦我有了属于自己的鹅,我的钱就会自动为我工作了。 第十一章幸运其实只是充分准备加上努力工作的结果。 勇敢的人也会害怕,一个人虽然害怕却仍然敢于前进,这才叫做勇敢。 重要的原则:你干得活最多只占报酬的一半,另一半是因为你的想法和实施这个想法的勇气。 第十二章只有你自己才能强迫自己去做。 我生命中最美好的事物的出现,是因为我做了我不敢做的事。 最珍贵的礼物是我们自己争取来的。克服了丢面子的恐惧,世界就会向你敞开大门! 第十三章 巨大灾难是,是因为你会像他一样有能力达到你计划的目标;不是,是因为你不会变得和金先生完全一样,二十拥有自己的个性。 最重要的是,你要确定你想要的是什么。 这不难确定。 可并非所有的人都愿意做出必要的努力,因为他们不想付出代价。 坚持成功日记。当你记成功日记的时候,你会对自身、对世界,还有对成功的规律做更深入的思考,你就会越来越多地了解自己和自己地愿望,这会使你有能力去理解别人。 每当你觉得有些事情不好办地时候,你可以做一件事。你就翻一翻成功日记本,你会从过去地事情中找到未来你也有能力完成任何事情地证据。 你不认为你比自己原本想象地要能干地多吗? 恐惧总是在我们设想事情会如何不顺地时候出现,我们对失败地可能性想的越多,就越害怕。而当你朝着积极的目标去思考地时候,就不会心生畏惧。 第十四章 Money magician (金钱魔法师)咒语: 确定自己喜欢获得财务上地成功。 自信,有想法,做自己喜欢地事情。 把钱分成日常开销、梦想目标和金鹅账户三部分。 进行明智地投资。 享受生活。 股票: 第十五章 Lecture(演讲)如果你没有做今天这件事情,你就永远不会知道,给自己一些压力之后,你能够做到些什么。一个人觉得最引以为豪地事情,往往是那些做起来最艰难的事情。 一种感觉涌上心头,似乎我和钱钱的关系不久将会发生一些变化。但不管将来发生什么事情,我不再会感到不安。 第十六章 投资当你把股票实际卖出的时候,才会有损失。 72公式 第十八章 结局重要的是,你能不能听到并且理解我说的话。就像你现在写的这本书,有一些读这本书的人听不到你说的话,于是就没有任何改变。另一些人读过之后开始聪明的理财,他们会拥有更幸福更富有的生活。 不要为失去的东西而忧伤,而要对拥有它的时光心存感激。 自力更生:D It’s not the years in your life that count. It’s the life in your years.]]></content>
<categories>
<category>理财</category>
</categories>
<tags>
<tag>理财知识</tag>
<tag>小狗钱钱</tag>
</tags>
</entry>
<entry>
<title><![CDATA[python 环境管理]]></title>
<url>%2Fposts%2F44663%2F</url>
<content type="text"><![CDATA[建立正常运作的 Python 环境是一个具有挑战性的主题。本文的目的在于如何帮助 python 开发者更好的管理相应的环境。 Why为什么需要管理多个 python 环境呢?你可能会问:安装最新的不就好了吗? 别着急,先看下面几个问题: 你开发了一个应用程序,它之前运行得很好。但是现在你试着运行它,它不工作了。也许其中一个包不再与程序的其他部分兼容(所谓的”breaking changes”)。可能的解决方案是设置一个新的环境,其中包含相应的 Python 版本和与应用程序完全兼容的软件包。 你可能和别人协同开发,你想确认你的程序能在其他成员的电脑上也正常运行。如果他们的 python 环境和你的不一样?该怎么办呢? Python 开发环境由特定的 Python 版本和一些包组成。因此,如果想要开发或使用具有不同 Python 或软件包版本需求的应用程序,你就需要设置不同的环境。 那么,如何做呢? How有两个流行的 python 环境设置工具: PIP (一个 python 包管理工具,’Pip Installs Packages’的缩写)和virtualenv (创建隔离环境的一个工具) Conda(一个包和环境管理工具) 在接下来我们介绍Conda的使用,因为它有如下优点: 清晰的结构:目录简洁有条理 透明的文件管理:所有文件都包含在安装文件夹。 灵活性:它包含很多包(PIP 包也可安装到 Conda 环境) 多用途:不仅是管理 Python 环境和包,还可以用于管理其他语言比如 R(统计计算的编程语言) Conda 版本的选择目前有 3 种安装包: Anaconda (free) Miniconda (free) Anaconda Enterprise platform (商用)) 这里我们只讨论前两种,它们都会安装 Conda 以及设置默认环境(base environment)。区别在于,Anaconda 会安装超过 150 个系统包(比如用于统计和机器学习的包),同时它还会安装一个图形化的工具 Anadaconda Navigator。而对于迷你的 Miniconda,它需要更少的磁盘空间,因为它并不会安装额外的软件包。关于选择 Python 2.7 还是 3.x 安装包,取决你的实际需求,并且这会成为默认根环境的 python 版本。 Managing environments(环境管理)window 为例,打开Anaconda Prompt 添加新的环境 1conda create --name py27 python=2.7 激活特定环境/切换回根环境 12345# 激活conda activate py27#conda deactivate conda 信息 1234conda info# 已有环境信息conda env list Managing Packages(包管理)Package channels(包通道)通道里包含依赖包的路径信息,Conda 通过这些信息寻找依赖包。在 Conda 的安装过程中,(Conda 的开发人员)通道是默认设置的,因此没有任何进一步的修改,这些是 Conda 将开始搜索包裹的位置。 通道以分层的顺序存在。最高优先级的通道是 Conda 检查的第一个通道,寻找需要的包。可以改变这个顺序,并向它添加通道(并设置它们的优先级) 在通道列表中添加一个通道作为最低优先级是一个很好的做法。通过这种方式,你可以包含“特殊的”软件包,它们不是默认设置的(连续体通道)的一部分。因此,在最后你可以得到所有的缺省包——不存在通过较低优先级通道重写它们的风险——以及需要的“特殊”选项。 要安装一个无法在这些默认通道中找到的软件包,你可以在这里搜索特定的软件包。注意:并不是所有的包都可以在所有平台上使用。 12345678# 添加最低优先级的通道conda config --append channels newchannel# 添加最高优先级的通道conda config --prepend channels newchannel# 通道列表conda config --get channels 值得注意的是,如果多个通道都包含了同一个包的不同版本,那么通道的层次优先级决定了最终将会被安装的包版本,即使优先级高的通道包含更旧的版本。 搜索/安装/移除 包 12345678910111213141516171819202122# 已安装包的信息conda list# 搜索指定包conda search -f seaborn# 从通道列表里的最高级通道安装包# 如果未指定版本,默认安装最新可用的版本conda install seabornconda install seaborn=0.7.0# 从指定通道里安装包,yamlconda install -c mychannel yaml# 更新所有已安装的包conda update# 更新指定的包conda update seaborn# 移除指定的包conda remove seaborn 阻止包更新(固定包版本) 在当前环境的目录的conda-meta文件夹下创建一个名为pinned文件,往这个文件添加你要阻止更新的包列表。比如说,强制seaborn使用 0.7.x 分支,锁定yaml固定使用 0.1.7 版本: 12seaborn 0.7.*yaml ==0.1.7 更新某个环境的Python版本 Python 本身也是一个包。 首先,查看可用的版本。 1conda search -f python 替换当前的 python 版本 1conda install python=3.4.2 要将 Python 版本更新到其分支的最新版本(例如,将 3.4.2 升级到 3.4.4 的 3.4.4),运行: 1conda update python 添加PIP包在本文开始的时候,我建议使用 Conda 作为包和环境管理器(而不是 PIP)。正如我上面提到的,PIP 包也可以作为一个包安装到 Conda 环境中。 因此,如果一个包在 Conda 通道不可用时,你可以尝试从PyPI 包索引中安装它。可以通过使用 pip 命令来做到这一点(默认情况下,Conda 安装程序可以使用这个命令,因此你可以在任何活动环境中应用它)。例如,如果你想要安装 lightgbm 包(它是一个梯度增强框架),运行: 1pip install lightgbm]]></content>
<categories>
<category>python</category>
</categories>
<tags>
<tag>python 环境管理</tag>
<tag>conda</tag>
</tags>
</entry>
<entry>
<title><![CDATA[GitHub的三种协议比较]]></title>
<url>%2Fposts%2F65521%2F</url>
<content type="text"><![CDATA[Primary differences between SSH and HTTPS. This post is specifically about accessing Git repositories on GitHub. Protocols to choose from when cloning:plain Git, aka git://github.com/ Does not add security beyond what Git itself provides. The server is not verified. If you clone a repository over git://, you should check if the latest commit’s hash is correct. You cannot push over it. (But see “Mixing protocols” below.) HTTPS, aka https://github.com/ HTTPS will always verify the server automatically, using certificate authorities. (On the other hand, in the past years several certificate authorities have been broken into, and many people consider them not secure enough. Also, some important HTTPS security enhancements are only available in web browsers, but not in Git.) Uses password authentication for pushing, and still allows anonymous pull. Downside: You have to enter your GitHub password every time you push. Git can remember passwords for a few minutes, but you need to be careful when storing the password permanently – since it can be used to change anything in your GitHub account. If you have two-factor authentication enabled, you will have to use a personal access token instead of your regular password. HTTPS works practically everywhere, even in places which block SSH and plain-Git protocols. In some cases, it can even be a little faster than SSH, especially over high-latency connections. HTTP, aka http://github.com/ Doesn’t work with GitHub anymore, but is offered by some other Git hosts. Works practically everywhere, like HTTPS. But does not provide any security – the connection is plain-text. SSH, aka `[email protected]:orssh://[email protected]/` Uses public-key authentication. You have to generate a keypair (or “public key”), then add it to your GitHub account. Using keys is more secure than passwords, since you can add many to the same account (for example, a key for every computer you use GitHub from). The private keys on your computer can be protected with passphrases. On the other hand, since you do not use the password, GitHub does not require two-factor auth codes either – so whoever obtains your private key can push to your repositories without needing the code generator device. However, the keys only allow pushing/pulling, but not editing account details. If you lose the private key (or if it gets stolen), you can just remove it from your GitHub account. A minor downside is that authentication is needed for all connections, so you always need a GitHub account – even to pull or clone. You also need to carefully verify the server’s fingerprint when connecting for the first time. Many people skip that and just type “yes”, which is insecure. (Note: This description is about GitHub. On personal servers, SSH can use passwords, anonymous access, or various other mechanisms.) Mixing protocolsGloballyYou can clone everything over git://, but tell Git to push over HTTPS. [url "https://github.com/"] pushInsteadOf = git://github.com/ Likewise, if you want to clone over git:// or HTTPS, but push over SSH: [url "[email protected]:"] pushInsteadOf = git://github.com/ pushInsteadOf = https://github.com/ These go to your git config file – sometimes ~/.config/git/config, or ~/.gitconfig, or just run git config --edit --global. Per-repositoryYou can also set different pull and push URLs for every remote separately, by changing remote.name.pushUrl in the repository’s own .git/config: [remote "origin"] url = git://nullroute.eu.org/~grawity/rwho.git pushUrl = ssh://sine/pub/git/rwho.git [相关阅读] 在最新的官方 Git 客户端正式版 2.18 中添加了对 Git wire 协议 v2 的支持,并引入了一些性能与 UI 改进的新特性。 在 Git 的核心团队成员 Brandon Williams 公开宣布这一消息前几周,Git 协议 v2 刚刚合并至 Git 的 master 主干分支。Git wire 协议定义了 Git 客户端与服务端如何对于 clone、fetch 和 push 等操作进行通信。按 Williams 所说,新版本协议的目标是提升性能,并使其能够更好的适应未来的改进。新版本协议的主要驱动力是使 Git 服务端能够对各种 ref(分支与 tag)进行过滤操作。这就意味着,Git 服务器无需将代码库中所有的 ref 一次性发送给客户端,再由客户端进行过滤。在大型的代码库中可能会存在不计其数的 ref,即使某些 ref 是客户端无需使用的,也不得不加载多达数个 MB 的 ref 数据。在使用 v2 协议之后,Git 服务器将根据客户端所需的操作类型,对 ref 进行过滤之后再将列表发送至客户端。Williams 举了一个例子,如果开发者所更新的分支仅比其远程分支落后几个提交,或是仅仅检查本地分支是否已更新,则完全没有必要在服务端传递整个 ref 列表,这对于时间和带宽都是一种浪费。Williams 表示,基于 Google 内部对协议 v2 的使用,在访问例如 Chrome 这种包含了超过 50 万个分支和 tag 的大型仓库时,比起使用 v1 协议可达到三倍速以上。此外,通过使用新版本协议,更便于实现某些新的特性,例如按需选取 ref,以及拉取和推送 symref 等等。 支持协议 v2 的 Git 客户端仍然可以与尚未支持 v2 的旧版本服务端进行通信。这要感谢当初在设计时决定通过一个独立的通道发送 v2 所必须的额外信息。旧版本的服务端会直接忽略这个额外的通道,并返回 ref 的完整列表。 为了让开发者能够自行选择协议的版本,Git 现在添加了一个新的 -c命令行选项,如以下示例: 1git -c protocol.version=2 ls-remote 如果希望默认使用 v2 协议,可以修改 Git 的配置: 1git config --global protocol.version=2 Git 2.18 中的另一个新特性是通过序列化的 commit graph 改善性能。简单来说,就是新版本的 Git 可以将 commit graph 的结构保存在某个文件中,并附加一些额外的元数据,以加速图形的加载。在进行获取列表,对提交历史进行过滤,以及计算合并的 base 等操作时,会表现得非常高效。这项功能是由微软的团队所实现的,该团队的成员 Derrick Stole 表示,对于大型代码库,例如 Linux kernel 或 Git 本身的代码库进行这类操作时,速度可提升 75–99%。Git 的 commit graph 仍然是一项处于实验性阶段的功能,因为某些 Git 特性无法很好地与 commit graph 相配合,例如浅克隆、对象替换,以及 commit graft 等等。如果不打算使用这些特性,可以通过运行 git config core.commitGraph true 命令启用 commit graph。 读者可在官方发布说明中了解 Git 2.18 的完整特性。]]></content>
<categories>
<category>git 系列</category>
<category>git 协议</category>
</categories>
<tags>
<tag>git 协议</tag>
</tags>
</entry>
<entry>
<title><![CDATA[词法作用域和动态作用域]]></title>
<url>%2Fposts%2F7035%2F</url>
<content type="text"><![CDATA[作用域是个语言无关的概念,记录下词法作用域和动态作用域的基本知识,加深下对这个概念的理解,此外,还会特别讨论下 JavaScript 的词法作用域。 to be done…]]></content>
<categories>
<category>you-dont-know-JS</category>
</categories>
<tags>
<tag>你不知道的Javascript</tag>
<tag>作用域</tag>
</tags>
</entry>
<entry>
<title><![CDATA[推荐几款很实用的 Chrome 插件]]></title>
<url>%2Fposts%2F33559%2F</url>
<content type="text"><![CDATA[个人很喜欢 Chrome 浏览器,简洁+速度快+方便,相信众多开发人员亦是如此。今天给大家推荐下我自己常用的一些 Chrome 插件,都是精挑细选留下来的,相信会大大提高你的工作效率。 在 Chrome Store 直接搜索添加安装即可(科学上网是必须的) Momentum高逼格,气质瞬间提升!新打开一个 Tab 时不再单调,给你清新的感觉,奉上今日份的图,自己感受下。 OneTab你一定遇到过这样的场景:chrome 打开了好多 tab,很多是有用的但现在没用到,又不舍得关,这时候内存又耗掉许多。怎么办呢?嗯,你可以使用 OneTab 来解决这一切,点击 OneTab,会直接把所有的 tab 回收,接着你可以一键还原某一天的 tab,为 Chrome 贴身打造,很好用哦。 Octotree有了这个插件,你可以直接在 Chrome 侧边栏生成的项目树结构查看别人的 Github 开源项目,非常方便。给大家看下查看我的博客的正确方式。 Save to pocket如果你经常看 Medium 或者其它一些博客,看到好的文章,然后装个这个插件就可以直接保存到 Pocket,通过手机上的 Pocket 客户端进行同步,不管你在坐地铁,还是在上厕所都可以利用这些碎片时间消化保存的知识,比利用这些时间刷知乎和微博好多了,强烈推荐大学生或者工作经验没多久迫切需要学习的人。 Page ruler顾名思义,就是用来度量页面元素的位置和大小等信息,设计师必备工具。 Web Paint这款插件可以让你在网页上随意涂鸦哦,这在给别人演示网页的时候灰常有用。 Care your eyes最重要的总是放在最后,当然还是身体啦!长时间盯着屏幕可不好吧,所以你需要保护好自己的眼睛哦。那就需要Care your eyes了,它可以帮助你有选择性的针对某些网页设置保护色,缓解眼部疲劳。]]></content>
<categories>
<category>工具集</category>
<category>chrome</category>
</categories>
<tags>
<tag>chrome 插件</tag>
<tag>珍藏</tag>
</tags>
</entry>
<entry>
<title><![CDATA[TODO LIST]]></title>
<url>%2Fposts%2F1787%2F</url>
<content type="text"><![CDATA[近期脑子里的想法或者接下来要做的事情。 the complete guide to angular 7 | Udemy Hexo 代码高亮样式 MacPanel,目前只是修改了现有 codeblock 的样式,不方便 - 可以使用钩子,模块功能化 refer to macpanel 参考 construct angular ui library deerlu cheat sheets cmder nvm for windows]]></content>
<categories>
<category>TODO</category>
</categories>
<tags>
<tag>plan</tag>
<tag>to do list</tag>
</tags>
</entry>
<entry>
<title><![CDATA[vs code 常用快捷键]]></title>
<url>%2Fposts%2F27698%2F</url>
<content type="text"><![CDATA[VS Studio Code常用快捷键汇总,持续更新中… 快捷键 Alt + ↑/↓ 当前所在行 上/下移 Shift +Alt + ↑/↓ 将当前行向上/下复制一行 Alt + ←/→ 光标历史 后退/前进 Ctrl + Enter 另起一行 Ctrl + Shift + Enter 当前行之前插入新的一行 Ctrl + Shift + K 删除当前行]]></content>
<categories>
<category>vs code</category>
</categories>
<tags>
<tag>vs-code</tag>
<tag>shortcut</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Ice cream]]></title>
<url>%2Fposts%2F55816%2F</url>
<content type="text"><![CDATA[也许世界杯只是一面镜子,照出的是当下人们所要面对的信息环境。 这是收到的一条推送信息。原本并不打算写什么,习惯了睁一只眼闭一只眼的姿态,心知肚明便好。大概最近重温了《冰菓》的缘故,因而也想呐喊一声(冰菓= ice cream ≈ i scream)。 还拿世界杯来说。无论你看球与否,你多少还是能听到或看到这样的讯息: 冰岛足球队都是兼职球员,教练是牙医,守门员是导演,甚至还是警察。 某国因为世界杯全民放假 12 天 我,煤吸,现在慌的一比。 … 然而现实却是冰岛虽然人口少,但专业程度非常高。驻某国记者证实并未有世界杯放假的消息。段子的背后,是盲目的情绪宣泄… 一件事情的发生,必然有着特定的原因。虚假歪曲的背后,无非是利益在作祟。随处可见的营销号,总是擅长利用底层生物性情绪,获取更多的粉丝和流量。传递真实信息和价值,并不是吸引关注的最有效手段,忽悠才是。因为很大一部分人,除了时间和注意力可以浪费,好像并没有耐心可以浪费。 21 世纪以来,信息的数量呈爆炸性增长,这也是大数据兴起的原因。看上去这是无比自由和信息丰富的互联网时代,每天打开手机和电脑你都有看不完的消息,但其实你所得到的有用的信息并不多。 相较九十年代,很多事情看起来变的简单许多,实际上则是更难了,因为要获取真实和深度的信息,就需要个人有足够多的耐心和识别能力。我并不认为很多人具备这样的素质,包括我。大部分的人已经习惯于被动地接收别处 push 过来的信息,很少 fetch 自己想要的信息,因为根本不知道自己想要的是什么。 所以不要随便翻开一篇文章就信以为真,更不要成为一个只会看段子的复读机。 以上。]]></content>
<categories>
<category>杂谈</category>
</categories>
<tags>
<tag>深度信息</tag>
</tags>
</entry>
<entry>
<title><![CDATA[日常 10 分钟提升记忆力和创造力]]></title>
<url>%2Fposts%2F32469%2F</url>
<content type="text"><![CDATA[日常 10 分钟提升记忆力和创造力翻译自:This 10-Minute Routine Will Increase Your Memory And Creativity 当你训练你的创造力时,你会潜在地训练你的记忆力。当你训练你的记忆时,你会潜在地训练你的创造性思维! 托尼 · 布詹 记忆和想象是密不可分的。事实上,你的记忆是想象出来的。它既不具体也不客观。相反,你的记忆是你如何去存储、连接并解释一个人、地方或事物。 随着你的想象力越来越丰富,你的记忆力也会越来越好。很少有人学会如何利用自己的长期记忆。因为大部分人的记忆策略很差,使得他们学习效率低下,缺乏创造力,对自己缺乏信心。 如果你不记得某件事,那意味着你并没有学会它。 一旦你的记忆力变得强大,你的学习就会更快地从意识转换到潜意识层次。一旦学习成为潜意识,你就有了更多的控制权和自主权。 当某物成为潜意识时,你就可以用它来创造一个更强大的现实。正如拿破仑·希尔所说:“潜意识将通过最直接、最实用的方法转化为它的物理对等物”。 再说一遍,记忆就是想象。如果你不把某件事和另一件事联系起来,你就记不起来了。所有的学习都是简单的联想—— 把你现在知道的和你还不知道的联系起来。 在这篇短文中,你将学到一个简单而有效的策略: 提升你的记忆力和想象力 能让你更有效地引导你的潜意识 创造出你所追求的东西(或者在你追求之上的东西!) 这里有一个简单的开始步骤: 睡前十分钟你所遇到的所有信息都包含在你的潜意识里。潜意识永远不会忘记任何事情。——Taryn Crimi 对于世界上许多最成功的人来说,在他们睡觉的时候,有意识地指引他们潜意识的运作是很常见的。 例如,托马斯·爱迪生每天晚上临睡前都会花一点时间,让他的潜意识思考、扭曲、组织并为他的创造性问题创造出强有力的解决方案。 他对这个过程非常清醒的。就像他说的:“不要在没有请求你的潜意识的情况下睡觉。” 爱迪生是一个记忆机器。他是个贪婪的读者,他几乎记住了他读过的所有东西。他能记住几乎所有他读过的东西,因为他懂得记忆、想象和潜意识之间的相互作用。 因为他理解记忆和创造力之间的联系——这两者都是简单的联系——使他能够成为世界上最伟大的发明家之一。他的想象力和建立新关系的能力是爆炸性的。 如果没有强大的记忆力,你不可能有真正的创造力。没有创造力,你就无法拥有强大的记忆。二者齐头并进。 这是如何运作的呢? 首先,记忆不是意志力的问题。意志力是一种线性和野蛮的方法来完成某件事。想象力和创造力是更强大的方法,它们也是非线性的,可以用很多不同的方式延伸。 凯文·霍斯利(Kevin Horsley)在他的《无限回忆》(Unlimited Memory)一书中写道:“埃米尔·库伊(Emile Coue)指出,‘当想象力和意志发生冲突时,想象力总是占上风。’如果是你的意志迫使你去记住,而你的想象力并未参与其中,你将只有零记忆和零回忆。你的想象力是你所有记忆力的源泉。” 最健康的记忆是灵活的,不是固定的。你可以改变、变更、操作和扩展它们。你越是富有想象力地试图去记住某件事,就越容易记住它。 这里给个例子: 假设现在你正试图从你读过的书中记住一些东西 你所需要做的就是把这个想法或概念与你生活中相关的东西联系起来 你想把它变成一个可视化的东西 你越是能将你的感觉与经历过的东西联系起来,它就越能深入你的长期记忆 一旦深入你的长期记忆中,它就变成了潜意识,因而比你必须有意识地专注于的东西要强大得多 当某些东西变得无意识时,它开始将自己与你大脑的其他区域、其他想法和其他记忆联系起来 每当你回忆起一段记忆,你的整个大脑启动并连结新的连接。 那么托马斯·爱迪生在晚上睡觉前的几分钟做了什么呢?他在可视化。他把他读到的所有想法与他过去的经历和记忆联系在一起。他的记忆力之所以很好,是因为他对这个过程非常有想象力。 你在记忆方面越有想象力和创造力,记忆就会变得越潜意识化。因此,你不想对如何连接思想施加任何限制,甚至你会觉得如果加以限制将会变得有点奇怪。 研究发现玩耍对神经可塑性是非常有益的,神经可塑性是指大脑通过一生形成新的神经连接来重组自身的能力。因此,玩耍对记忆也很有好处。 说一个变得富有想象力的例子: 假设你现在想记住记忆、想象和潜意识是如何一起工作的 你可以把你的记忆想象成一个巨大的漂浮团——是什么让你想到巨大的漂浮团呢?因为我首先想到的是神奇宝贝-加斯特利 然后你把它和想象联系起来——这让我想起独角兽 所以我马上就想象着口袋妖怪骑在独角兽上 然后你把记忆和想象与潜意识的想法联系起来——这让我想起了那本书《思考致富》(Think and Grow Rich) 所以现在我有了加斯特利骑在独角兽上读着一本《思考致富》的画面 为了让这种体验更加身临其境,你可以利用你所有的感官,比如嗅觉、味觉和触觉 — 在这个例子中,加斯特利闻起来像化学实验室里的某种奇怪的东西,而独角兽闻起来像一碗幸运符(一种谷类食品); 而且,当他们从你身边经过时,你可以感受到加斯特利的烟雾,感觉就像一层厚厚的湿润的薄雾,你可以品尝幸运护身符。 创造力就是把事物联系在一起。不可避免地,托马斯·爱迪生很可能在思考各种各样的想法并试图将它们联系在一起。他在睡觉前给自己几分钟时间做这件事,然后他就睡过了。 在他睡觉的时候,他的潜意识开始连接、组织、综合、塑造和创造。 醒来后的十分钟研究证实,大脑,尤其是前额叶皮层,在睡眠后最活跃,最容易产生创造力。相反,随着时间的推移,大脑的分析部分变得更加活跃。这项研究观察了早上和晚上的核磁共振扫描结果,发现早晨大脑中有更多的联系——这是创造过程的关键因素。 当你睡觉时,你的潜意识一直在松散地走神,建立起上下文和时间的联系。这样,你的大脑和大脑就会为创造和学习做好准备。 爱迪生当然醒得很早,甚至在早上的第一件事就是写日记。他整个上午都在进行创造性活动。超过 3500 本爱迪生的笔记本已经被发现。 这些笔记本充满了有趣的观察和见解——许多都是与不相关的项目有关的,有着一连串的联想和联系。连续的草图——有些是粗略的,有些是精确地执行的——跨越了大量的技术和发明。 换句话说,爱迪生经常在早晨以一种意识流的方式写日记。他会让自己的大脑把看似不相关的想法联系在一起,在适当的过程中,他经常会产生灵感和创造性的联系。 例如,在一篇日志中,他预测了与潜在的飞行相关的想法(莱特兄弟成功之前的几十年),然后在电话中得到了 ah-ha 的想法。 这是他日记的开始,日期为 1877 年 5 月 26 日: 12345"发现..."如果你非常仔细地观察任何印刷品,你会发现印刷非常模糊,而且你会看到两种类型的图像... 其中一幅是蓝色或紫外线="很好= 今天早上5点,电话变得更加完美。" 前国际象棋神童、太极世界冠军乔希·韦茨金(Josh Waitzkin)在接受蒂姆·菲里斯(Tim Ferriss)采访时解释了他每天早上的例行活动,利用他睡觉时的潜意识突破和联系。 在 18 岁至 44 岁的人群中,有 80%的人会在起床后 15 分钟内查看自己的智能手机,与他们不同的是,Waitzkin 会去一个安静的地方,做一些冥想,然后拿起他的日记。 在他的日记中,他沉思了几分钟。因此,Waitzkin 并没有像大多数检查通知的人那样关注输入,而是关注输出。这就是他如何利用他的清晰、学习和创造力的更高境界——他称之为“结晶的智慧”。 总结“与潜意识有关的创造性努力的可能性是惊人的和不可估量的。他们使人敬畏。”——拿破仑·希尔 把这一切拼凑在一起: 花大量的时间学习和阅读。 学习方法越多越好 —— 体验式学习和“情境式”学习往往比单纯的阅读更有效。 当你学习的时候,试着用一种富有想象力的方式把你正在学习的东西和你已经知道的东西联系起来。 有明确的目标和你想解决的问题 —— 正如达伦•哈代(Darren Hardy)曾经说过的,“你的生活可以根据你试图解决的问题的规模来衡量。” 在睡觉前花几分钟想象一下你想要完成的想法、概念、挑战、经历 — 也要想象你项目的完工和目标的达成。 当你睡觉的时候,你的潜意识将会有一些强大和强大的东西在工作,因为你在你的大脑中创造了许多不同的联系,不管你在想什么。 当你早上醒来的第一件事是喝一大杯水,去一个安静的地方,开始以一种意识流的方式写日记。 一旦你开始有了创造性的想法,坚持继续写——通常是想法之后的想法才是最重要的。 所有这些都与心理学家所说的顿悟能力有关,这是你在一生中可以掌握的东西,就像记忆和创造力一样。 顿悟的科学定义包含三个关键要素: 抛弃旧的无效思想(即打破精神定势); 形成新的、有效的思想(形成新的小说联想) 体会“啊哈”的强烈刺激感 你可以变得非常擅长获得顿悟或创造性突破。不过,你需要接受新的体验。这显然是非常罕见的。研究表明,随着年龄的增长,人们越来越不愿意接受新的体验。 你需要开发一个强大的记忆和想象策略。你需要变得更有趣,更自由地学习和记住事情。 你的记忆不是客观的,而是主观的。每当记忆被回忆并与新事物相关联时,它就会被创造和重新创造。健康的记忆是灵活的。 因为你的记忆力是灵活的和极富想象力的,你有更多的创造性控制来决定自己成为什么样的人,而不是可能会成为什么样。 你可以成为一个强大的创意思考者。你可以变得非常聪明。你可以变得富有成效的。你可以在困难的领域迅速发展专业技能。 然而问题是:你愿意花时间学习这些技能吗?还是你会让你头脑和记忆中无价的工具白白浪费掉? 准备升级了吗?Confidence doesn’t create success. Instead, successful behavior creates confidence.]]></content>
<categories>
<category>翻译分享</category>
</categories>
<tags>
<tag>翻译</tag>
<tag>好文分享</tag>
</tags>
</entry>
<entry>
<title><![CDATA[今天开心吗?]]></title>
<url>%2Fposts%2F30460%2F</url>
<content type="text"><![CDATA[字前:今天我们要讨论的是开心的事。 几个小时以前。 是的,又开始下雨了。 也就是突然间那么一下,整个人莫名就陷入失落的怪圈。宛如好容易堆积起来的扑克楼,倏地就呼啦啦坍塌在你面前。究其原因,大概是体内累积的各种负面情绪伴随着雨水突然发作罢。雨势渐弱,失落感并未散去,反倒藉着周遭的霓虹愈演愈烈,人也开始变得焦躁,最终的结果是我生气了。于是索性漫步在街头,雨并没有下大的意思,所以心情也逐渐缓和。 回过头来,细想生气的缘由。雨天吗,还是因为堵车?答案是否定的,我并不特别讨厌堵车的下雨天。就算讨厌,也不到生气的程度。讨厌的大概是望着窗外时间随雨水流逝,脚底却并未挪动半步的无力感。出于牢骚,便向臭味相投的友人团吐了几句,因为我觉得他们肯定会站出来爱护我的。的确,他们真的站出来了,只不过最后的结论是我爱护他们胜过自己。 然而,关注点并不在于这个结论(因为这并不意外),而在于其实每个人并没有你看到的那样开心。我想绝大多数人并不愿意把自己认为不好的一面展现在他人面前,即便是至亲:另一半甚至父母。并不是说虚伪,大概如同人生而孤独一般,下意识地自我保护在作祟罢了。 对于有意识的人来说,总会有藏秘密的地方。不过大多是不开心的秘密,因为开心是藏不住的。可是藏起来的东西并不会像酒一样收藏起时光的味道,久了必然是会露出马脚,以无法预见的形式:倒塌的扑克楼? 自然而然,想到要消除扑克楼的不安定因素。信息爆炸的时代,我想没有谁能置身于互联之外。或多或少,无论是庸人自扰还是树欲静而风不止,总会有点滴不开心。滴水成流,粒米成箩,时光最擅积少成多,而众人皆不以为意。不开心无可避免,犹如不可能每张扑克牌都恰到好处,这时候需要的是些许的修正,而不是一跳而过,因为你永远敌不过时间。 进一步的,你还可以选择开心。因为开心越多,不开心也会变得开心。还没有 800 字吗!所以再编个说个开心的故事吧。 故事开始 很久没见面和说话的朋友,就叫 A 君吧。不知道谁提议的,反正最后终于约好见面了。我问晚上吃啥?A 君说无所谓。我说,那好,我订,晚上见。 最后我还是预定了 X。老实说,这家的菜做得并不好。但是吃饭这回事,不在于菜。谈天说地,没有顾忌,怎么样都是愉快的。 饭余,去 DQ 吃了个冰激凌,忘记聊了些啥乱七八糟的。到了该回家的时候,我们各自打道回府。到家时收到 A 君的消息:”今天开心吗?” 忽然间就觉得很暖。也许一个人并不开心,所以 A 君很努力很尽心地陪了你一段时间,最后很期待地问你:”你今天开心吗?”很简单的几个字,然而用心的是一个过程。 故事编完了。 大概就是:开心其实很简单,用心的过程。 所以,你今天开心吗? 是的,我很开心,因为有800个字了:)]]></content>
<categories>
<category>杂谈</category>
<category>由感</category>
</categories>
<tags>
<tag>下雨天</tag>
<tag>用心</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git系列-远程仓库]]></title>
<url>%2Fposts%2F38902%2F</url>
<content type="text"><![CDATA[相信到了这里,你已经可以建立自己的 git 仓库并进行基本的版本管理了。然而这仅限于你自己的机器,这是远远不够的。还记得建立 git 仓库的时候我们介绍了git clone从别处拷贝仓库,这里的别处就是我们要介绍的远程仓库。所谓的远程仓库是指托管在因特网或其他网络中的项目的版本库,你可以有好几个远程仓库,通常有些仓库对你只读,有些则可以读写。 与他人协作涉及管理远程仓库以及根据需要推送或拉取数据。 管理远程仓库包括了解如何添加远程仓库、移除无效的远程仓库、管理不同的远程分支并定义它们是否被跟踪等等。 查看已经配置的远程仓库如果想查看当前已经配置的远程仓库服务器,可以运行 git remote 命令。 它会列出指定的每一个远程服务器的简写。 如果是克隆的仓库,那么至少应该能看到 origin - 这是 Git 给你克隆的仓库服务器的默认名字: 123$ pwd/c/Hexo/gitpost/project$ git remote 上述是新建的仓库,所以还没有配置远程仓库地址。 添加远程仓库运行 git remote add <shortname> <url> 添加一个新的远程 Git 仓库,同时指定一个你可以轻松引用的简写: 12345678$ git remote add gitdemo https://github.com/suchenrain/gitpost.git$ git remotegitdemo$ git remote -vgitdemo https://github.com/suchenrain/gitpost.git (fetch)gitdemo https://github.com/suchenrain/gitpost.git (push)]]></content>
<categories>
<category>git系列</category>
<category>git remote</category>
</categories>
<tags>
<tag>git remote</tag>
<tag>远程仓库</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git系列索引]]></title>
<url>%2Fposts%2F6331%2F</url>
<content type="text"><![CDATA[就是 git 相关的快速索引。 Git 概念先行 工作区、暂存区和版本库 Git 正常操作 配置 Git 创建版本库 基本玩法 撤销 远程仓库 打标签 git别名 分支管理 创建分支 一些好用的工具 储藏队列 子模块 Git 常用命令清单]]></content>
<categories>
<category>git系列</category>
</categories>
<tags>
<tag>git教程</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git系列—撤销操作]]></title>
<url>%2Fposts%2F12438%2F</url>
<content type="text"><![CDATA[在任何一个阶段,都有可能想要撤消某些操作。方法的选择取决你的情况:是否已提交。如果是,你又想如何修复。 未提交重置工作区日常开发中,你可能会遇到工作区的文件搞的一团乱麻,这时你会想回到之前的状态:编辑前的干净状态,上次暂存的状态,上次储藏队列的状态。 干净状态所谓的干净状态即最近一次提交时的状态。本质上是将版本库中版本恢复到工作区和暂存区中。即重置。 12345678910111213141516# 重置暂存区,与上一次commit保持一致$ git reset$ git reset HEAD# 重置当前分支HEAD指针为指定commit,并重置暂存区,工作区不变$ git reset [commit]# 重置暂存区与工作区,与上一次commit保持一致$ git reset --hard$ git reset --hard HEAD# 重置当前分支HEAD指针为指定commit,并重置暂存区与工作区$ git reset --hard [commit]# 重置当前HEAD为指定commit,但保持暂存区和工作区不变$ git reset --keep [commit] fatal: Cannot do hard reset with paths.如果只是重置暂存区,reset 可以使用 paths。而有--hard时不能使用。 如果你只是想恢复部分特定文件,那么: 1234567891011121314# 重置暂存区和工作区的单个指定文件,与上一次commit保持一致$ git checkout head test.md# 约等同于$ git reset test.md # 注意此处 额外重置了HEAD指针$ git checkout test.md# 重置暂存区与工作区的lib文件夹下内容,与上一次commit保持一致$ git checkout head lib# 重置暂存区与工作区,与上一次commit保持一致$ git checkout head . # 等同于reset --hard# 恢复某个commit的指定paths到暂存区和工作区(不会更新HEAD)$ git checkout [commit] [paths] 上次暂存状态某种情况下,你先将一部分修改进行了暂存,而在后来你想将工作区回退至暂存的状态。即将暂存区中的版本恢复至工作区。 12345# 恢复暂存区的指定文件到工作区$ git checkout [file]# 恢复暂存区的指定paths到工作区$ git checkout [paths] 储藏队列除了工作区,暂存区和版本库,还有一个特殊的储存队列。 当你正在做一项复杂的工作时, 发现了一个和当前工作不相关但是又很讨厌的 bug. 你这时想先修复 bug 再做手头的工作, 那么就可以用 git stash 来保存当前的工作状态, 等你修复完 bug 后,执行’反储藏’(unstash)操作就可以回到之前的工作里。 1$ git stash <message> 这条命令将目前工作区的修改保存到 stash 中,这会将工作区和暂存区重置回当前分支上次提交时的状态。当你修复完 bug 后, 你可以用git stash apply来回复到以前的工作状态。 12345678# 使用顶部的stash$ git stash apply# 使用特定stash$ git stash apply stash@{1}# 查看stashes$ git stash list 已提交如果你已经做了一个提交(commit),但是你马上后悔了。这里有两种截然不同的方法去处理这个问题: 新建提交去修正错误的提交(适合代码已发布) 修改旧提交 新建提交创建一个新的,撤消(revert)了前期修改的提交(commit)是很容易的; 只要把出错的提交(commit)的名字(reference)做为参数传给命令: git revert就可以了; 下面这条命令就演示了如何撤消最近的一个提交: 1$ git revert HEAD 这样就创建了一个撤消了上次提交(HEAD)的新提交, 你就有机会来修改新提交(new commit)里的提交注释信息. 你也可撤消更早期的修改, 下面这条命令就是撤消“上上次”(next-to-last)的提交: 1$ git revert HEAD^ 在这种情况下,git 尝试去撤消老的提交,然后留下完整的老提交前的版本. 如果你最近的修改和要撤消的修改有重叠(overlap),那么就会被要求手工解决冲突(conflicts), 就像解决合并(merge)时出现的冲突一样。 修改旧提交如果你刚刚做了某个提交(commit), 但是你又想马上修改这个提交; git commit --amend能让你修改刚才的这个提交(HEAD commit). 这个命令会默认使用刚才的提交信息将暂存区中的文件进行提交并覆盖刚才的那个旧提交。]]></content>
<categories>
<category>git系列</category>
</categories>
<tags>
<tag>git</tag>
<tag>git系列</tag>
<tag>撤销</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git系列—记录变更到版本库中]]></title>
<url>%2Fposts%2F20456%2F</url>
<content type="text"><![CDATA[到了这里相信你的机器上已经有了一个 Git 仓库,并且工作区里也签出了工作副本。通常,当项目达到想要记录的状态时,我们开始对文件进行更改并将这些变更的快照提交到存储库中。 工作目录里的文件无非两种状态:已追踪或者未追踪。已追踪文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录;它们可以是「未修改」、「已修改」、「已暂存(staged)」。简而言之,追踪的文件就是 Git 所知道的文件。 工作目录中除已追踪文件以外的所有其它文件都属于未追踪文件,它们既不存在于上次快照的记录中,也没有放入暂存区。 初次克隆某个仓库的时候,工作目录中的所有文件都属于已追踪文件,并处于未修改状态。 编辑过工作区的某些文件之后,由于自上次提交后对它们做了修改,Git 将它们标记为已修改文件。 我们逐步将这些修改过的文件放入暂存区(add),然后提交所有暂存了的修改(commit),如此反复。所以使用 Git 时文件的生命周期大致如下: 检查当前文件状态123$ git statusOn branch masternothing to commit, working tree clean 这说明现在的工作目录相当干净。换句话说,所有已跟踪文件在上次提交后都未被更改过。 此外,上面的信息还表明,当前目录下没有出现任何处于未跟踪状态的新文件,否则 Git 会在这里列出来。 最后,该命令还显示了当前所在分支,并告诉你这个分支同远程服务器上对应的分支没有偏离。 新建一个文件。 如果之前并不存在这个文件,使用 git status 命令,你将看到一个新的未跟踪文件: 123456789$ echo 'i hate u' > test.md$ git statusOn branch masterUntracked files: (use "git add <file>..." to include in what will be committed) test.mdnothing added to commit but untracked files present (use "git add" to track) Git 不会自动将新的文件纳入跟踪范围,除非你明明白白地告诉它“我需要跟踪该文件”, 这样的处理让你不必担心将生成的二进制文件或其它不想被跟踪的文件包含进来。 跟踪新文件通过使用命令 git add 开始跟踪一个文件。 1$ git add test.md 此时再运行 git status 命令,会看到 README 文件已被跟踪,并处于暂存状态: 123456$ git statusOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) new file: test.md 只要在 Changes to be committed 这行下面的,就说明是已暂存状态。git add 命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件。 暂存已修改文件修改一个已被跟踪的文件 readme.md, 然后运行 git status 命令: 12345678910111213$ echo 'add a line' > readme.md$ git statusOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) new file: test.mdChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: readme.md 文件 readme.md 出现在 Changes not staged for commit 这行下面,说明已追踪文件的内容发生了变化,但还没有放到暂存区。 要暂存这次更新,需要运行 git add 命令。 这是个多功能命令:可以用它开始跟踪新文件,或者把已追踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态等。 将这个命令理解为“添加内容到下一次提交中”而不是“将一个文件添加到项目中”要更加合适。 12345678$ git add readme.md$ git statusOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) modified: readme.md new file: test.md 现在更改后的 readme.md 也已经暂存了。不过在提交记录之前,我们再次对 readme.md 进行修改并查看状态: 1234567891011121314$ echo 'add another line' >> readme.md$ git statusOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) modified: readme.md new file: test.mdChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: readme.md 见鬼了? 你会发现 readme.md 文件同时出现在暂存区和非暂存区。 这怎么可能呢? 好吧,实际上 Git 只不过暂存了你运行 git add 命令时的版本, 如果你现在提交,readme.md 的版本是你最后一次运行 git add 命令时的那个版本,而不是你运行 git commit 时,在工作目录中的当前版本。 所以,运行了 git add 之后又作了修订的文件,需要重新运行 git add 把最新版本重新暂存起来。 状态简览git status 命令的输出十分详细,同时也有些啰嗦。 通过 git status -s 命令或 git status –short 命令,输出将更加简洁: 123456$ git status -sM License.txtA lib/file3.zc M readme.mdMM test.md?? lib/file4.zc 新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记。 你可能注意到了 M 有两个可以出现的位置,出现在右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件被修改了并放入了暂存区。 例如,上面的状态报告显示: 在工作区新添加了 lib/file4.zc 文件且未追踪 lib/file3.zc 为工作区新添加文件并且放入了暂存区。 readme.md 文件在工作区被修改了但是还没有将修改后的文件放入暂存区 License.txt 文件被修改了并将修改后的文件放入了暂存区 test.md 在工作区被修改并提交到暂存区后又在工作区中被修改了,所以在暂存区和工作区都有该文件被修改了的记录。 如何理解上述短格式? 短格式使用两位字母的状态码XY。 对于合并冲突的路径,X 和 Y 显示合并的每一边的修改状态。对于那些没有合并冲突的路径,X 显示了索引(暂存区)的状态,并且 Y 显示了工作树(工作目录)的状态。对于未被追踪的路径,XY 是??其他状态码可以解释如下: ‘’ = unmodified M = modified A = added D = deleted R = renamed C = copied U = updated but unmerged 12345678910111213141516171819202122232425X Y Meaning------------------------------------------------- [AMD] not updatedM [ MD] updated in indexA [ MD] added to indexD deleted from indexR [ MD] renamed in indexC [ MD] copied in index[MARC] index and work tree matches[ MARC] M work tree changed since index[ MARC] D deleted in work tree[ D] R renamed in work tree[ D] C copied in work tree-------------------------------------------------D D unmerged, both deletedA U unmerged, added by usU D unmerged, deleted by themU A unmerged, added by themD U unmerged, deleted by usA A unmerged, both addedU U unmerged, both modified-------------------------------------------------? ? untracked! ! ignored------------------------------------------------- 所以可以用如下表格描述上面的输出,比如 readme.md,它在工作区被修改了,但暂存区无变化,说明文件在工作区被修改但未放入暂存区。再比如 lib/file4.zc,它在工作区和暂存区均未知状态,说明是工作区新的未追踪文件。 暂存区状态 工作区状态 文件 M License.txt A lib/file3.zc M readme.md M M test.md ? ? lib/file4.zc 更多请参阅git status 忽略特定文件一般我们总会有些文件不需要纳入 Git 的管理,也不希望它们总出现在未追踪文件列表。 它们通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。 在这种情况下,我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件规则。 来看一个实际的例子: 123$ cat .gitignore*.[oa]*~ 第一行告诉 Git 忽略所有以 .o 或 .a 结尾的文件。一般这类对象文件和存档文件都是编译过程中出现的。 第二行告诉 Git 忽略所有以波浪符(~)结尾的文件,许多文本编辑软件(比如 Emacs)都用这样的文件名保存副本。 此外,你可能还需要忽略 log,tmp 或者 pid 目录,以及自动生成的文档等等。 要养成一开始就设置好 .gitignore 文件的习惯,以免将来误提交这类无用的文件。 文件 .gitignore 的格式规范如下: 所有空行或者以 # 开头的行都会被 Git 忽略。 可以使用标准的 glob 模式匹配。 匹配模式可以以(/)开头防止递归。 匹配模式可以以(/)结尾指定目录。 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。 所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号()匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。 使用两个星号() 表示匹配任意中间目录,比如a/**/z 可以匹配 a/z, a/b/z 或 a/b/c/z等。 我们再看一个 .gitignore 文件的例子: 1234567891011121314151617# 不要追蹤檔名為 .a 結尾的檔案*.a# 但是要追蹤 lib.a,即使上面已指定忽略所有的 .a 檔案!lib.a# 只忽略根目錄下的 TODO 檔案,不包含子目錄下的 TODO/TODO# 忽略 build/ 目錄下所有檔案build/# 忽略 doc/notes.txt,但不包含 doc/server/arch.txtdoc/*.txt# 忽略所有在 doc/ 目錄底下的 .pdf 檔案doc/**/*.pdf 可以看出规范的应用顺序为从上到下,所以在制定.gitignore 文件时,尽量把粗粒度规则写在上面,细粒度的放在下面。 Tip GitHub 在 https://github.com/github/gitignore 中针对几十种项目和程序语言维护了一个很完整、好用的 .gitignore 范例文件列表,你可以试试。 一般来说,一个 git 仓库会在根目录有这样一个.gitignore 文件, 它依次作用于整个仓库目录。然而,子目录也可以有自己的.gitignore 文件。这些嵌套的.gitignore 文件中的规则只适用于它们所在目录下的文件。 更多详情可以参见man gitignore。 查看已暂存和未暂存的修改git status告诉你的只是哪些文件发生了什么样的变化,如果你想知道具体的更改,那么你需要使用git diff命令。通常它用于回答两个问题: 当前做的哪些更改还没有暂存?(git diff) 有哪些更新已经暂存起来准备好了下次提交?(git diff --staged) 假设我们项目当前状态如下: 123456789101112$ git statusOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) modified: readme.mdChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: test.md 要查看尚未暂存的文件更新了哪些部分,不加参数直接输入 git diff: 12345678910$ git diffdiff --git a/test.md b/test.mdindex 8e86c6b..e77b92e 100644--- a/test.md+++ b/test.md@@ -1,3 +1,4 @@ i hate u i like u i love u+i hate you 此命令比较的是工作目录中当前文件和暂存区域快照之间的差异, 也就是修改之后还没有暂存起来的变化内容。 若要查看已暂存的将要添加到下次提交里的内容,可以用 git diff –cached 命令。(Git 1.6.1 及更高版本还允许使用 git diff –staged,效果是相同的,但更好记些。) 123456789$ git diff --stageddiff --git a/readme.md b/readme.mdindex f7ec434..f44d5bd 100644--- a/readme.md+++ b/readme.md@@ -1,2 +1,3 @@ add a line add another line+test1 小结git diff (查看工作区和暂存区的差异)git diff --staged (查看暂存区和版本库的差异) Git Diff 的插件版本 git 默认使用 git diff 来分析文件差异。 但是,如果你喜欢通过图形化的方式或其它格式输出方式的话,可以使用 git difftool 命令来用 Araxis ,emerge 或 vimdiff 等软件输出 diff 分析结果。 使用 git difftool –tool-help 命令来看你的系统支持哪些 Git Diff 插件。 提交更新到此,相信你已经准备好将暂存区的修改提交到版本库了。每次准备提交前,先用 git status 看下,是不是都已暂存起来了, 然后再运行提交命令 git commit: 123456789101112$ git commit# Please enter the commit message for your changes. Lines starting# with '#' will be ignored, and an empty message aborts the commit.## On branch master# Changes to be committed:# modified: readme.md# modified: test.md#~~ 这会启动文本编辑器以便输入本次提交的说明。 (默认会启用 shell 的环境变量 $EDITOR 所指定的软件,一般都是 vim 或 emacs。输入你的提交信息,保存退出完成提交。 另外,你也可以在 commit 命令后添加 -m 选项,将提交信息与命令放在同一行,如下所示: 123$ git commit -m 'first commit'[master 643c866] first commit 2 files changed, 2 insertions(+) 跳过使用暂存区域尽管使用暂存区域的方式可以精心准备要提交的细节,但有时候这么做略显繁琐。 Git 提供了一个跳过使用暂存区域的方式, 只要在提交的时候,给 git commit 加上 -a 选项,Git 就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add 步骤: 1234567891011$ git status -s M readme.md?? demofile.zc$ git commit -a -m 'skip staged step'[master be44449] skip staged step 1 file changed, 1 insertion(+)# untracked files remain$ git status -s?? demofile.zc 只是针对 tracked 的文件,未追踪的文件还是在工作区! 移除文件假设由于不小心将某文件提交了版本库,那么如何移除呢? 要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。 可以用 git rm 命令完成此项工作,并连带从工作目录中删除指定的文件,这样以后就不会出现在未跟踪文件清单中了。 如果只是简单地从工作目录中手工删除文件,运行 git status 时就会在 “Changes not staged for commit” 部分(也就是 未暂存清单)看到: 12345678910$ rm test.md$ git statusOn branch masterChanges not staged for commit: (use "git add/rm <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) deleted: test.mdno changes added to commit (use "git add" and/or "git commit -a") 然后再运行 git rm 记录此次移除文件的操作: 123456789$ git rm test.mdrm 'test.md'$ git statusOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) deleted: test.md 下一次提交时,该文件就不再纳入版本管理了。 如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母)。 这是一种安全特性,用于防止误删还没有添加到快照的数据,这样的数据不能被 Git 恢复。 另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。 换句话说,你想让文件保留在磁盘,但是并不想让 Git 继续跟踪。 当你忘记添加 .gitignore 文件,不小心把一个很大的日志文件或一堆 .a 这样的编译生成文件添加到暂存区时,这一做法尤其有用。 为达到这一目的,使用 –cached 选项: 1$ git rm --cached test.md git rm 命令后面可以列出文件或者目录的名字,也可以使用 glob 模式。 比方说: 1$ git rm log/\*.log 注意到星号 * 之前的反斜杠 \, 因为 Git 有它自己的文件模式扩展匹配方式,所以我们不用 shell 来帮忙展开。 此命令删除 log/ 目录下扩展名为 .log 的所有文件。 类似的比如: 1$ git rm \*~ 删除以 ~ 结尾的所有文件。 移动文件(重命名)要在 Git 中对文件改名,可以这么做: $ git mv file_from file_to 它会恰如预期般正常工作。 实际上,即便此时查看状态信息,也会明白无误地看到关于重命名操作的说明: 1234567$ git mv README.md README$ git statusOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) renamed: README.md -> README 其实,运行 git mv 就相当于运行了下面三条命令: 123$ mv README.md README$ git rm README.md$ git add README 如此分开操作,Git 也会意识到这是一次改名,所以不管何种方式结果都一样。 两者唯一的区别是,mv 是一条命令而另一种方式需要三条命令,直接用 git mv 轻便得多。 参考:[1]. Git Basics - Recording Changes to the Repository]]></content>
<categories>
<category>git系列</category>
</categories>
<tags>
<tag>git</tag>
<tag>git系列</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git系列—理解Git的工作区、暂存区和版本库]]></title>
<url>%2Fposts%2F55466%2F</url>
<content type="text"><![CDATA[Git 目录创建了版本库后。首先,让我们来看一看 Git 仓库的目录结构。作为演示,我们初始化了一个叫做 project 的 Git 仓库。使用tree查看项目结构如下: 123456789101112131415$ cd C:/Hexo/gitpost/project$ tree -C -F -a -L 2.|-- .git/ # 隐藏文件夹(**git版本库**)| |-- COMMIT_EDITMSG #| |-- HEAD # 这个git项目当前处在哪个分支里| |-- config # 项目的配置信息,git config命令会改动它(仓库级别)| |-- description # 项目的描述信息| |-- hooks/ # 系统默认钩子脚本目录| |-- index # 索引文件(**暂存区**)| |-- info/ #| |-- logs/ # 各个refs的历史信息| |-- objects/ # Git本地仓库的所有对象 (commits, trees, blobs, tags)| `-- refs/ # 标识项目里的每个分支指向了哪个提交(commit)`-- readme.md # 项目文件(**工作区**) 以上使用 git version 2.15.0.windows.1 其他版本或略有差异。 接下来我们就可以来介绍工作区,暂存区和版本库的概念了。 Git 工作区、暂存区和版本库 工作区:(Working Directory)就是你在电脑里能看到的目录。存储着你现在签出(checkout)来用来编辑的文件. 当你在项目的不同分支间切换时, 工作目录里的文件经常会被替换和删除. 所有历史信息都保存在 ‘.git 目录’中 ; 工作目录只用来临时保存签出(checkout) 文件的地方, 你可以编辑工作目录的文件直到下次提交(commit)为止. 暂存区:英文叫 stage, 或 index。一般存放在 “.git 目录下” 下的 index 文件(.git/index)中,所以我们把暂存区有时也叫作索引(index)。 版本库:(Repository)工作区有一个隐藏目录.git,这个不算工作区,而是 Git 的版本库。 图中左侧为工作区,右侧为版本库。在版本库中标记为 index 的区域是暂存区(stage, index),标记为 master 的是 master 分支所代表的目录树。 图中我们可以看出此时 HEAD 实际是指向 master 分支的一个游标。所以图示的命令中出现 HEAD 的地方可以用 master 来替换。 图中的 objects 标识的区域为 Git 的对象库,实际位于 .git/objects 目录下,里面包含了创建的各种对象及内容。 当对工作区修改(或新增)的文件执行 git add 命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新的对象中,而该对象的 ID 被记录在暂存区的文件索引中。 当执行提交操作(git commit)时,暂存区的目录树写到版本库(对象库)中,master 分支会做相应的更新。即 master 指向的目录树就是提交时暂存区的目录树。 当执行 git reset HEAD 命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。 当执行 git rm --cached <file> 命令时,会直接从暂存区删除文件,工作区则不做出改变。 当执行 git checkout . 或者 git checkout -- <file> 命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。 当执行 git checkout HEAD . 或者 git checkout HEAD <file> 命令时,会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。 小结暂存区是 Git 非常重要的概念,弄明白了暂存区,就弄明白了 Git 的很多操作到底干了什么。]]></content>
<categories>
<category>git系列</category>
</categories>
<tags>
<tag>git系列</tag>
<tag>git目录</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git系列—创建版本库]]></title>
<url>%2Fposts%2F7310%2F</url>
<content type="text"><![CDATA[既然我们现在把一切都设置好了,那么我们需要一个 Git 仓库。有两种方法可以得到它: 使用git init将现有的本地非版本控制目录转换成 Git 仓库 使用git clone从别处拷贝已存在的 Git 仓库 现有目录中初始化 Git 仓库 如果你需要对现存的未版本控制化的项目进行版本控制,你可以通过如下命令: 123//项目所在根目录$ cd <path/to/project>$ git init 初始化后,会在<project>目录下会出现一个名为 .git 的目录,所有 Git 需要的数据和资源都存放在这个目录中。此时这个.git 只是一个最原始的 Git 仓库骨架,还没有任何项目文件被 git 所追踪。如果想要对这些项目文件开始版本控制,你需要追踪这些文件并初始化一次提交。 123//追踪所有文件$ git add *$ git commit -m 'initial project version' 这样,项目的所有文件就已经在版本追踪中了。 新建一个指定目录作为 Git 仓库 12//新建newrepo仓库$ git init <newrepo> 这会新生成一个 newrepo 目录,并在其中出现一个名为 .git 的目录,所有 Git 需要的数据和资源都存放在这个目录中。 克隆已有的 Git 仓库相较其他版本控制工具比如 Subversion 的 checkout,git clone 不仅仅只是拷贝一份工作副本,它还包含了所有的数据:每个文件的所有历史版本。 克隆仓库的命令格式为: 1$ git clone <repo> 如果我们需要克隆到指定的目录,可以使用以下命令格式: 1$ git clone <repo> <directory> 如果你想克隆指定分支: 1$ git clone -b <branch> <url> 克隆含有子模块的仓库: 123456# 自动初始化并更新仓库中的每一个子模块$ git clone --recursive <repo># 或者 通过$ git submodule init$ git submodule update <repo>可以有多种传输协议形式:https,local,ssh和git,这在之后会介绍。 参考:[1]. Getting a Git Repository[2]. Git 创建仓库]]></content>
<categories>
<category>git系列</category>
<category>git clone</category>
</categories>
<tags>
<tag>git</tag>
<tag>git系列</tag>
<tag>git clone</tag>
<tag>git init</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git系列—Git配置详解]]></title>
<url>%2Fposts%2F46722%2F</url>
<content type="text"><![CDATA[Git 的配置文件Git 自带一个 git config 的工具来帮助设置控制 Git 外观和行为的配置变量。 这些变量存储在三个不同的位置,针对三个级别System,User和Repository: /etc/gitconfig 文件(一般是 Git 的安装目录): 包含系统上每一个用户及他们仓库的通用配置。 如果使用带有 --system 选项的 git config 时,它会从此文件读写配置变量。 ~/.gitconfig 或 ~/.config/git/config 文件(当前用户主目录):只针对当前用户。 可以传递 --global 选项让 Git 读写此文件。 当前使用仓库的 Git 目录中的 config 文件(就是 .git/config):针对该仓库。 你可以通过git config -l --show-origin来查看当前目录下的 git 配置信息(配置来源+配置): 1234567891011121314151617181920212223242526$ git config -l --show-originfile:"C:\\ProgramData/Git/config" core.symlinks=falsefile:"C:\\ProgramData/Git/config" core.autocrlf=truefile:"C:\\ProgramData/Git/config" core.fscache=truefile:"C:\\ProgramData/Git/config" rebase.autosquash=truefile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" http.sslcainfo=C:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crtfile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" http.sslbackend=opensslfile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" diff.astextplain.textconv=astextplainfile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" filter.lfs.clean=git-lfs clean -- %ffile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" credential.helper=managerfile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" user.name=John Doefile:C:/Users/Administrator/.gitconfig [email protected]:C:/Users/Administrator/.gitconfig user.name=suchenxiaoyufile:C:/Users/Administrator/.gitconfig gui.recentrepo=C:/WorkSpace/Centerfile:.git/config core.repositoryformatversion=0file:.git/config core.filemode=falsefile:.git/config core.bare=falsefile:.git/config core.logallrefupdates=truefile:.git/config remote.origin.url=https://github.com/suchenrain/workspace.gitfile:.git/config remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*file:.git/config branch.master.remote=originfile:.git/config branch.master.merge=refs/heads/masterfile:.git/config gui.wmstate=zoomedfile:.git/config gui.geometry=443x321+-964+107 649 330file:.git/config branch.dev.remote=originfile:.git/config branch.dev.merge=refs/heads/dev 你会发现其中user.name在 global 和 repository 级别同时存在,此时将使用 repository 里的配置。即配置优先级:Repository > Global > System。 初次使用 Git 前的配置 用于提交的用户信息当安装完 Git 应该做的第一件事就是设置你的用户名称与邮件地址。 这样做很重要,因为每一个 Git 的提交都会使用这些信息,并且它会写入到你的每一次提交中,不可更改: 12git config --global user.name "suchenxiaoyu"git config --global user.email [email protected] 当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有 --global 选项的命令来进行配置。 这里的用户信息并不是你的 git 账户信息,这些信息是用于每一次的提交。当然,你也可以使用你的 git 账号信息作为用户信息,这并没有什么不可以。 文本编辑器设置 Git 默认使用的文本编辑器, 一般可能会是 Vi 或者 Vim。如果你有其他偏好,比如 Emacs 的话,可以重新设置: 1git config --global core.editor emacs 如果是在 windows 上,假设你想设置 Notepad++: 1git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -nosession" 差异分析工具还有一个比较常用的是,在解决合并冲突时使用哪种差异分析工具。比如要改用 vimdiff 的话: 1git config --global merge.tool vimdiff Git 可以理解 kdiff3,tkdiff,meld,xxdiff,emerge,vimdiff,gvimdiff,ecmerge,和 opendiff 等合并工具的输出信息。 当然,你也可以指定使用自己开发的工具,具体请参考后续文章。 查看配置信息要检查已有的配置信息,可以使用 git config –list 命令: 1234567891011$ git config --list或者$ git config -luser.name=suchenxiaoyuuser.email=suchenxiaoyu@example.comcolor.status=autocolor.branch=autocolor.interactive=autocolor.diff=auto... 有时候会看到重复的变量名,那就说明它们来自不同的配置文件(比如 /etc/gitconfig 和 ~/.gitconfig),不过最终 Git 实际采用的是最后一个。 直接查阅某个环境变量的设定,只要把特定的名字跟在后面即可,像这样: 12$ git config user.namesuchenxiaoyu 参考:[1]. Git 安装配置[2]. First-Time Git Setup]]></content>
<categories>
<category>git系列</category>
<category>git config</category>
</categories>
<tags>
<tag>git</tag>
<tag>git系列</tag>
<tag>git config</tag>
</tags>
</entry>
<entry>
<title><![CDATA[git command cheat sheet]]></title>
<url>%2Fposts%2F40446%2F</url>
<content type="text"><![CDATA[更新日志 2018-06-04 git log 2018-06-03 git add + git commit 2018-05-31 git status 2018-05-24 git init 2018-05-23 git config 2018-05-15 初稿 关于实话说 git 是笔者用过的并觉得最好的版本控制系统,相较于 Microsoft 的 TFS 以及 SVN,git 都要优雅许多。然而接触它这么久,还是不敢说已经完全掌握并理解它,但这并不妨碍我对它的热爱。以前也记录过使用的一些总结,后来由于某种原因丢失了,所以想温故而知新。 注意:本文的关注点在于如何使用 git 所提供的命令上,诸如环境安装,配置此类问题均不在范畴。 git initgit-init - 用于创建空的 Git 仓库或者重新初始化一个已存在的 Git 仓库。 以下均假设未设置 Git 相关参数环境变量,如GIT_DIR,GIT_TEMPLATE_DIR etc. 以下在git version 2.13.0.windows.1运行测试通过123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475# 新建git仓库$ git init # 将当前目录初始化为Git仓库$ git init [directory] # 在当前路径新建`directory`目录并初始化为Git仓库$ pwd/c/WorkBench/suchenrain/temptest$ git init test1Initialized empty Git repository in C:/WorkBench/suchenrain/temptest/test1/.git/$ git init [path] # 在指定的路径`path`下初始化Git仓库$ git init to/test # 相对路径即 ./to/testInitialized empty Git repository in C:/WorkBench/suchenrain/temptest/to/test/.git/$ git init /to/test # 根路径(Git安装路径)Initialized empty Git repository in C:/WorkSpace/Develop/Git/to/test/.git/-------------------------------------------------------------------------------------# 裸仓库$ git init --bare # 只生成Git版本库,没有工作区(适用于git服务器)$ git init --bare bareInitialized empty Git repository in C:/WorkBench/suchenrain/temptest/bare/$ cd bare$ tree -F -L 1.|-- HEAD|-- config|-- description|-- hooks/|-- info/|-- objects/`-- refs/-------------------------------------------------------------------------------------# 分离版本库(指定git版本库的路径,而不是默认的./.git。常用于移动已有仓库的版本库)$ git init --separate-git-dir fake srcInitialized empty Git repository in C:/WorkBench/suchenrain/temptest/fake/$ tree -F -a -L 2.|-- fake/ # git版本库| |-- HEAD| |-- config| |-- description| |-- hooks/| |-- info/| |-- objects/| `-- refs/`-- src/ # 工作区 `-- .git # 版本库路径信息文件$ vi src/.gitgitdir: C:/WorkBench/suchenrain/temptest/fake # 版本库路径为fake~~~-------------------------------------------------------------------------------------# 仓库访问权限$ git init --shared[=(false|true|umask|group|all|world|everybody|0xxx)]# false等价于umask 这是默认设置,表示使用系统默认的文件权限# group (or true) 仓库组可写。Note that the umask still applies to the other permission bits(e.g. if umask is 0022, using group will not remove read privileges from other (non-group) users)# all (or world or everybody)Same as group, but make the repository readable by all users.# 0xxx# u-user g-group o-others# rwx# 4 stands for "read",# 2 stands for "write",# 1 stands for "execute", and# 0 stands for "no permission."$ git init --shared=0660 testShared git clone12345678910111213141516# 克隆仓库到当前目录下$ git clone <repo># 克隆仓库到指定目录$ git clone <repo> <directory># 克隆仓库并签出指定分支(默认为master分支)$ git clone -b <branch> <repo># 克隆带有子模块的仓库,正常命令只会包含子模块的空目录# 自动初始化并更新仓库中的每一个子模块$ git clone --recursive <repo># 或者$ git submodule init$ git submodule update git config以下在git version 2.15.0.windows.1 运行测试通过123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960# 查看所有配置$ git config -l, --list# 查看配置并显示配置来源$ git config --show-origin -l-------------------------------------------------------------------------------------# 设置配置$ git config --system user.name "John Doe" # 设置system级别用户名$ git config --global user.name "suchenxiaoyu" # 设置global级别用户名-------------------------------------------------------------------------------------# 读取配置$ git config user.name # 读取最终生效的配置$ git config --get user.name # 读取最终生效的配置suchenxiaoyu$ git config --get-all user.name # 读取所有配置John Doesuchenxiaoyu-------------------------------------------------------------------------------------# 添加配置$ git config --add user.name test # 添加repository级别配置$ git config --global --add user.name test # 添加global级别配置(增加一条配置不管存在与否)$ git config --show-origin --get-all user.name # 读取user.name的所有配置值并显示来源file:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" John Doefile:C:/Users/Administrator/.gitconfig suchenxiaoyufile:C:/Users/Administrator/.gitconfig testfile:.git/config test-------------------------------------------------------------------------------------# 移除配置$ git config --global --unset user.name # 移除global级别用户名(适合只有一条记录的配置)warning: user.name has multiple values$ git config --global --unset-all user.name # 移除global级别用户名的所有配置$ git config --show-origin --get-all user.namefile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" John Doefile:.git/config test-------------------------------------------------------------------------------------$ git config --unset user.name #移除repository级别user.name$ git config --show-origin --get-all user.namefile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" John Doe #user.name只剩system级别配置-------------------------------------------------------------------------------------$ git config --show-origin --get-regexp user # 根据正则获取配置信息,如 获取所有级别包含user的配置file:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" user.name John Doefile:C:/Users/Administrator/.gitconfig user.email [email protected]:C:/Users/Administrator/.gitconfig xuser.name hahafile:.git/config user.name tefile:.git/config user.hello tefile:.git/config xuser.name haha-------------------------------------------------------------------------------------$ git config --remove-section user # remove a section: name# (移除repository级别的用户节点 user.name,user.email,etc.)$ git config --show-origin --get-regexp userfile:"C:\\Program Files\\Git\\mingw64/etc/gitconfig" user.name John Doefile:C:/Users/Administrator/.gitconfig user.email [email protected]:C:/Users/Administrator/.gitconfig xuser.name hahafile:.git/config xuser.name haha git statusgit status 用于查看当前工作区文件状态。 以下在git version 2.17.1.windows.2运行测试通过1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859# 查看工作区当前状态$ git status# 短格式-输出两位字母状态码$ git status -s, --short-------------------------------------------------------------------------------------# 附加输出已暂存的具体变化,一个-v相当于 diff --cached# 两个-v 相当于再附加上未暂存的具体变化$ git status -v -vOn branch masterChanges to be committed: (use "git reset HEAD <file>..." to unstage) modified: test.mdChanges not staged for commit: (use "git add <file>..." to update what will be committed) (use "git checkout -- <file>..." to discard changes in working directory) modified: readme.mdChanges to be committed:diff --git c/test.md i/test.mdindex e77b92e..23ff8f0 100644--- c/test.md+++ i/test.md@@ -2,3 +2,4 @@ i hate u i like u i love u i hate you+i like you--------------------------------------------------Changes not staged for commit:diff --git i/readme.md w/readme.mdindex d8ac117..4094f29 100644--- i/readme.md+++ w/readme.md@@ -3,3 +3,4 @@ add another line test1 test2 test3+test4-------------------------------------------------------------------------------------# 简洁状态下也显示分支信息(搭配 -s)$ git status -s -b## master M readme.mdM test.md-------------------------------------------------------------------------------------# 显示已经隐藏起来的条目数量$ git stash # 隐藏当前工作目录的变化Saved working directory and index state WIP on master: be44449 skip staged step$ git status --show-stashOn branch masternothing to commit, working tree cleanYour stash currently has 1 entry git addadd 命令用于将工作区变化放入暂存区。 以下在git version 2.17.1.windows.2运行测试通过1234567891011121314151617181920212223242526# 特定文件$ git add readme.md# 指定目录下(包括子目录)的所有变化$ git add lib = git add lib/# 当前目录下所有变化$ git add .# 暂存已忽略的文件$ git add -f temp.dll# 进入交互模式$ git add -i [path] staged unstaged path 1: unchanged +1/-0 lib/file4.zc 2: unchanged +1/-0 readme.md 3: +2/-0 +1/-0 test.md*** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: helpWhat now># 交互式地选择补丁(等同于交互模式下选择patch子命令)$ git add -p, -patch 交互模式 进入交互模式后默认输出 status 子命令的结果,并且处于交互命令循环中。此时光标以一个>结尾,可以通过键入选项数字或者类似s,sta等使选择唯一的字母。 主命令循环有 6 个子命令: status用于显示暂存区和版本库及工作区的变化 staged - 显示暂存区版本库当前版本的变化,即将会提交的变化。 unstaged - 显示工作区与暂存区的变化,即可以暂存的变化。 1234 staged unstaged path1: unchanged +1/-0 lib/file4.zc2: unchanged +1/-0 readme.md3: +2/-0 +1/-0 test.md # 已暂存的变化(新增2行内容),未暂存的变化(新增一行内容) update用于暂存选择的变化。 123456789*** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: helpWhat now> 2 staged unstaged path 1: unchanged +1/-0 lib/file4.zc 2: unchanged +1/-0 readme.md 3: +2/-0 +1/-0 test.mdUpdate>> 注意此时已经在 update 子命令下>>,你可以通过如下方式选择你需要暂存的变化: 1,2 以,分隔多个选择 1-3或 1-进行范围选择 -2取消选中 选择完之后,已选中的变化会以*高亮显示。空行回车即可暂存所选中的变化。 以下在git version 2.17.1.windows.2运行测试通过12345678910111213141516Update>> 3 staged unstaged path 1: unchanged +1/-0 lib/file4.zc 2: unchanged +1/-0 readme.md* 3: +2/-0 +1/-0 test.mdUpdate>>updated 1 path*** Commands *** 1: status 2: update 3: revert 4: add untracked 5: patch 6: diff 7: quit 8: helpWhat now> 1 staged unstaged path 1: unchanged +1/-0 lib/file4.zc 2: unchanged +1/-0 readme.md 3: +3/-0 nothing test.md # 工作区变化已经全部暂存 revert类似 update 命令,用于将选中的已暂存变化回退至版本库中的当前版本。如果是新路径,即将新追踪的文件回退至未追踪状态。 add untracked类似 update 和 revert,用于追踪新文件。 patch类似 update 但是更细致化,当你选择需要暂存的变化后,会将暂存区与工作区的差异(即将要放入暂存区的变化)依次显示并询问你的动作。这实际上相当于更加细粒度的暂存你选择的那些变化。 可用的动作: y - stage this hunk(暂存当前变化块) n - do not stage this hunk(不暂存) q - quit; do not stage this hunk or any of the remaining ones(退出 patch) a - stage this hunk and all later hunks in the file(暂存所有) d - do not stage this hunk or any of the later hunks in the file(不暂存当前文件的此处及之后的变化块) g - select a hunk to go to(跳转至其它变化块) / - search for a hunk matching the given regex(搜索指定的变化块) j - leave this hunk undecided, see next undecided hunk(跳过当前变化块,跳至下一个未决定的变化块) J - leave this hunk undecided, see next hunk(跳过当前变化块,跳至下一个变化块) k - leave this hunk undecided, see previous undecided hunk(跳过当前变化块,跳至前一个未决定的变化块) K - leave this hunk undecided, see previous hunk(跳过当前变化块,跳至前一个的变化块) s - split the current hunk into smaller hunks(将当前变化块分割成更小的变化块,细粒度) e - manually edit the current hunk(手动编辑当前变化块+ -操作) ? - print h diff用于查看将要提交的内容(暂存区与版本库当前版本)。 git commit用于将暂存区的变化更新到版本库中,在新的提交中储存暂存区的当前内容,以及来自用户描述更改的日志消息。 以下在git version 2.17.1.windows.2运行测试通过123456789101112131415161718192021222324252627282930313233343536# 自动暂存所有被修改或删除的已追踪文件并提交# (未追踪文件不受影响)# 用于跳过手动stage$ git commit -a, --all# 交互式地选择补丁提交$ git commit -p, --patch# 交互式模式$ git commit --interactive# 复用指定的提交信息$ git commit -C <commit>, --reuse-message=<commit>$ git commit -c <commit>, --reedit-message=<commit> # 可以另外再编辑# 指定作者信息(override)$ git commit [email protected]# 指定提交信息 (-m 与 -c,-C及-F互斥)$ git commit -m 'commit message'$ git commit -m 'first message' -m 'second message' # 最终以段落形式串联$ git log -1commit fe9e1ce441873311a532239a3cca709acbd2d59f (HEAD -> master)Author: suchenxiaoyu <[email protected]>Date: Mon Jun 4 18:20:11 2018 +0800 first second# 替换/修正上一次提交$ git commit --amend # 默认使用上一次提交的所有信息$ git commit --amend -m 'replace message'# 预提交(模拟提交)$ git commit --dry-run git log用于查看历史提交日志。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283# 查看最近2次提交并显示内容差异$ git log -p -2# 提交的简略统计信息$ git log --statcommit 817c17b5d37a6f1f44b0d5a87466895a2b327cfa (HEAD -> master)Author: suchenxiaoyu <[email protected]>Date: Mon Jun 4 18:20:11 2018 +0800 test readme.md | 1 + 1 file changed, 1 insertion(+)# 只显示 --stat 中最后的行数修改添加移除统计$ git log --shortstat---------------------------------------------------------------# 仅在提交信息后显示已修改的文件清单$ git log --name-only# 显示新增、修改、删除的文件清单(状态码)$ git log --name-status# 仅显示 SHA-1 的前几个字符$ git log --abbrev-commit# 使用较短的相对时间显示(比如,“2 weeks ago”)$ git log --relative-date# ASCII图形(显示分支与合并)$ git log --graph---------------------------------------------------------------# 仅显示最近的n条提交$ git log -n# 仅显示指定时间之前/后的提交$ git log --since,--after=<time format>$ git log --until,--before=<time format>$ git log --since=2.days # 两天前开始的提交$ git log --since="2018-06-02"$ git log --since="3 days 5 minutes ago" # 距现在3天零5分钟内的提交# 指定作者的提交$ git log --author=gitster# 指定提交者的提交$ git log --committer=commiter# 提交说明中包含指定关键字的提交(多个关键字时需要使用--all-match,否则是或关系)$ git log --grep=<text>$ git log --grep=skip --grep=staged --all-match # 同时包含'skip'和'staged'# 显示添加或移除了某些字符串的提交$ git log -Slove # 显示新加或者删除'love'改动的提交# 只关心特定文件或目录的历史提交$ git log -- lib/file1.zc # 特定文件$ git log -- lib # 特定目录---------------------------------------------------------------# 指定日志展示的格式(oneline | short | full |fuller)$ git log --pretty=oneline817c17b5d37a6f1f44b0d5a87466895a2b327cfa (HEAD -> master) testbe444493d956aee03d387276a883358ebe77410e skip staged step05cb82feb2f77d9d83642b61c14835c35b138258 skip stage step643c8668bf7857ce49ebf7a4d778a3c69623881e first commit0f6c323da12fd853d640146f12dd70b8d6d516d2 demo 182af2fec9c052826abbb642b26948d0ff932dd93 test84b6ea98f62b7d06b98b4a94c515ae6a548376f2 initial project version---------------------------------------------------------------# 自定义日志格式$ git log --pretty=format:"%h - %an, %ar : %s"$ git log --pretty="%h , %s" # 简写形式817c17b - suchenxiaoyu, 38 minutes ago : testbe44449 - suchenxiaoyu, 4 days ago : skip staged step643c866 - suchenxiaoyu, 4 days ago : first commit0f6c323 - John Doe, 4 days ago : demo 182af2fe - John Doe, 5 days ago : test84b6ea9 - suchenxiaoyu, 11 days ago : initial project version--------------------------------------------------------------- git log --pretty=format 常用的选项: 123456789101112131415%H 提交对象(commit)的完整哈希字串%h 提交对象的简短哈希字串%T 树对象(tree)的完整哈希字串%t 树对象的简短哈希字串%P 父对象(parent)的完整哈希字串%p 父对象的简短哈希字串%an 作者(author)的名字%ae 作者的电子邮件地址%ad 作者修订日期(可以用 --date= 选项定制格式)%ar 作者修订日期,按多久以前的方式显示%cn 提交者(committer)的名字%ce 提交者的电子邮件地址%cd 提交日期%cr 提交日期,按多久以前的方式显示%s 提交说明 作者指的是实际作出修改的人,提交者指的是最后将此工作成果提交到仓库的人。(分布式 Git) 1234# 显示从2018/6/1开始到1分钟之前这段时间内由'suchenxiaoyu'对文件'readme.md'的非合并提交$ git log --pretty="%h - %s" --author=suchenxiaoyu --since="2018-06-01" \> --before="a minute ago" --no-merges -- readme.md817c17b - test References Git Reference Pro Git Git Community Book]]></content>
<categories>
<category>git系列</category>
<category>git命令</category>
</categories>
<tags>
<tag>git</tag>
<tag>git系列</tag>
<tag>git命令</tag>
</tags>
</entry>
<entry>
<title><![CDATA[angular 6.0发布啦]]></title>
<url>%2Fposts%2F64464%2F</url>
<content type="text"><![CDATA[翻译自:version-6-of-angular-now-available Angular 6.0.0 已经发布啦! 这是一个重要的版本,因为我们更多地将关注点从Angular自身底层框架转移到了相关的工具链上,如何让Angular的快速开发在以后变得更加简单。 作为这次新版本发布的一部分, 我们升级了框架包的主版本(@angular/core, @angular/common, @angular/compiler, etc), Angular CLI, 以及 Angular Material + CDK。为了区分交叉兼容性,我们更改了主版本号,所以现在发布的都是6.0.0版本。而这些项目的小版本和补丁版本将根据项目的需要发布。 你可以通过我们的changelogs来查看全部变更列表: framework, material+cdk, cli. ng updateng update <package> 是一个CLI的新命令。它会分析你项目的 package.json文件并且利用Angular的相关知识智能地为你推荐更新. 实际例子请查阅update guide.不仅可以通过ng update 来帮助你使用依赖包的合适版本,及保持包的同步, 第三方也可以通过schematics提供更新脚本 . 如果你的一个依赖包提供了ng update schematic, 它们可以在需要进行破坏更改时自动更新代码! ng update 不会取代你的包管理器, 而是借助于npm或者yarn来管理依赖项。除了更新依赖项和对等的依赖项之外, 如果有需要,ng update会对你的项目做相应的转换。 举个例子,ng update @angular/core 会更新所有的Angualr框架包,连同RxJS和TypeScrit。并且它会运行这些依赖项的任何可用的schematics,从而保证项目最新。作为这一命令的一部分,我们将自动将RxJS -compat安装到应用程序中,以使更加顺利的使用RxJS v6。 我们期望在未来的几个月里会看到更多的库和包添加ng update的schematics,而且我们已经从企业组件库团队那里听说过,他们计划使用ng update来通过自动化的方式来推动重要的变化,以节省开发人员的时间。 Learn more about how the ng update command works. 想创建属于你自己的ng update schematic, 请移步 package.json of rxjs 以及 collection.json. ng add另一个新的CLI命令ng add <package>使您的项目更容易添加新的功能。ng add将使用你的包管理器来下载新的依赖项,并调用安装脚本(实现为schematic),该脚本可以通过配置更改更新项目,添加额外的依赖项(例如,poly填充),或者使用特定于包的初始化代码。 在你的新ng new应用程序中尝试以下几点: ng add @angular/pwa — 通过添加应用程序清单和service worker,将应用程序转换为PWA。 ng add @ng-bootstrap/schematics — 添加 ng-bootstrap 到你的项目中。 ng add @angular/material — 安装和设置 Angular Material和主题,并将新的开始组件注册到ng generate ng add @clr/angular@next — 从虚拟机安装和设置Clarity ng add @angular/elements — 添加必须的document-register-element.js polyfill(填充)以及angular elements相关依赖 (see below) 因为ng add是建立在schematics和npm注册表之上的,我们希望相关库和社区能够帮助我们构建一个丰富的ng add支持包生态系统。 查阅 Angular Material’s ng-add schematic 例子来开始构建你自己的 ng-add schematics. Angular ElementsAngular Elements的第一个版本集中于允许你通过将其作为定制元素注册在现有的angular应用程序中引导angular组件。我们在angular.io上广泛使用这一点并作为我们的内容管理系统的一部分,从而允许通过嵌入的HTML对功能进行动态引导。这代替了在静态html内容中手工引导的angular组件的需要。 更多请查阅an example of registering a component as a custom element 或更多关于 Angular Elements. 油管视频 Angular Elements Quick Start Angular Material + CDK Components最大的变化就是用于显示分层数据的新的树组件。下面的模式来自数据表组件, CDK提供了核心树指令。使用Angular Material所提供的和Material设计一样的样式。 我们最近讨论了这个组件,所以请查看更多信息。这些新的树组件都有样式(Material’s mat-tree)和没有样式的版本(CDK’s cdk-tree)。 在树组件的旁边,我们也有新的徽章和底板组件。标识符有助于显示一些有用的信息,比如未读项计数。bottom -sheets是一种特殊类型的以移动为中心的对话框,它从viewport的底部出现,通常用于在操作之后呈现一个选项列表。 @angular/cdk/overlay包是cdk中最强大的基础设施之一。随着版本6的发布,这个包现在包含了新的定位逻辑,可以帮助弹出窗口在所有情况下智能地保持在屏幕上。 Angular Material Starter Components 一旦您运行了ng add @angular/material以向现有应用程序添加材料,您还将能够生成3个新的启动组件。 Material Sidenav现在,您可以生成一个starter组件,包括带有应用程序名称和边导航的工具栏。该组件基于断点响应。 Run: 1ng generate @angular/material:material-nav --name=my-nav 这将创建这个starter组件: Material Dashboard现在,您可以生成一个包含动态网格列表的starter dashboard组件。 Run: 1ng generate @angular/material:material-dashboard --name=my-dashboard 这将创建这个starter组件: Material Data Table您可以生成一个启动数据表组件,它预先配置了一个数据源来进行排序和分页。 Run: 1ng generate @angular/material:material-table --name=my-table 这将创建这个starter组件: 了解更多Angular Material Schematics. CLI WorkspacesCLI v6现在支持包含多个项目的工作空间,例如多个应用程序或库。CLI项目现在将使用angular.json替.angular-cli.json用于构建和项目配置。 每个CLI工作区都有项目,每个项目都有目标,每个目标都可以有配置。 1234567891011121314151617181920{ "projects": { "my-project-name": { "projectType": "application", "architect": { "build": { "configurations": { "production": {}, "demo": {}, "staging": {}, } }, "serve": {}, "extract-i18n": {}, "test": {}, } }, "my-project-name-e2e": {} },} 了解更多新版配置文件new configuration file Library Support我们的CLI中最需要的特性之一是支持创建和构建库,我们可以很自豪地介绍: 1ng generate library <name> 该命令将在CLI工作区中创建一个库项目,并将其配置为测试和构建。 Learn more about creating libraries with the Angular CLI Tree Shakable Providers为了使你的应用程序更小,我们已经从模块引用服务的模式转换为服务引用模块。这意味着允许我们只将模块中注入的服务捆绑到你的代码库中。 Beforeapp.module.ts12345@NgModule({ ... providers: [MyService]})export class AppModule {} my-service.ts123456import { Injectable } from '@angular/core';@Injectable()export class MyService { constructor() { }} After我们的NgModule中不需要引用。 my-service.ts12345678import { Injectable } from '@angular/core';@Injectable({ providedIn: 'root',})export class MyService { constructor() { }} Read more about Dependency Injection Animations Performance Improvements我们已经更新了动画的实现,将不再需要web 动画填充库. 这意味着你可以从你的程序中移除这些填充库并减少大约47KB的包大小, 同时增加Safari的动画性能。 RxJS v6Angular已经更新到使用v6的RxJS。RxJS是一个独立的项目,在几周前发布了v6。RxJS v6带来了几个主要的更改,以及向后兼容包RxJS -compat,它将使您的应用程序正常工作。 RxJS已经被重新整理,使它更容易被树摇(tree-shakable),确保只有你使用的RxJS片段包含在最终的生产包中。 如果您使用ng update,您的应用程序应该可以继续工作,但是您可以了解更多关于5.5 to 6.0 migration. Long Term Support (LTS)我们正在扩大对所有主要产品的长期支持。 之前我们宣布只有v4和v6是LTS版本,但是为了使从一个专业升级到下一个更容易,并且给更大的项目更多的时间来计划更新,我们决定从v4开始扩展对所有主要版本的长期支持。 每个主要的版本将被支持18个月,大约6个月的积极开发,然后是12个月的关键补丁和安全补丁 了解更多关于如何Angular versions and releases. How to update to 6.0.0访问update.angular.io 来获取关于更新应用程序的信息和指南。 更新通常遵循3个步骤,并将利用新的ng update工具。 Update @angular/cli Update your Angular framework packages Update other dependencies 让开发人员轻松地了解最新版本对我们来说非常重要,所以让我们知道您在评论中对这个版本的看法! What about Ivy?在ng-conf我们提到了一个叫做Ivy的新项目 — 我们的下一代渲染管道。Ivy目前正在开发中,并不是6.0版的一部分。我们将在接下来的几个月里宣布一项关于Ivy的选择预览。关注这个博客的最新信息。]]></content>
<categories>
<category>Angular</category>
</categories>
<tags>
<tag>angular</tag>
<tag>ng-conf</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Hexo Next主题使用汇总(不定期更新)]]></title>
<url>%2Fposts%2F27331%2F</url>
<content type="text"><![CDATA[写在前面记录在使用Next主题时遇到的一些点: 关于多选项卡Tabs Tabs在看关于markdown的语法时,突然想到可能会用到的tabs。但是发现GitHub Flavored Markdown好像没有tabs相关的语法,不过因为在gitbook里有使用过tabs的markdown语法,所以去gitbook的在线编辑页面研究了一下。最终gitbook生成的相应源代码如下:12345678{% tabs %}{% tab title="First Tab" %}first{% endtab %}{% tab title="second" %}second{% endtab %}{% endtabs %} 其中tabs和tab是gitbook内部支持的markdown拓展语法,所以很无奈在hexo中并不能直接使用。 很不甘心,所以想利用hexo的标签插件自定义类似的快速标签。在查看hexo及next源码时,发现Next其实内置了tabs标签,然而在官方文档里并没有提到。其使用方法如下:12345678{% tabs %}<!-- tab first tab@heart --> first<!-- endtab --><!-- tab second tab--> second<!-- endtab -->{% endtabs %} 上面的代码效果:]]></content>
<categories>
<category>Hexo</category>
<category>Next</category>
</categories>
<tags>
<tag>Hexo</tag>
<tag>Next</tag>
<tag>主题</tag>
</tags>
</entry>
<entry>
<title><![CDATA[markdown语法汇总]]></title>
<url>%2Fposts%2F15918%2F</url>
<content type="text"><![CDATA[markdown 相关语法的总结 注意:这里介绍的只是通用的 markdown 语法,一些平台特有的语法的比如 GFM(GitHub Flavored Markdown)的@等将不涉及。 Headers(标题)使用#来标记标题级别,语法如下: 1234567891011# 这是一级标题## 这是二级标题### 这是三级标题#### 这是四级标题##### 这是五级标题###### 这是六级标题 效果:这是一级标题这是二级标题这是三级标题这是四级标题这是五级标题这是六级标题 Strikethrough(删除线)就像这样~~like this~~或者<del>直接使用html</del>html(不推荐) i hate you um…i like you actually Emphasis(强调)1234567_文字斜体__也是文字斜体_**文字粗体****也是文字粗体**_你**可以**组合它们_ 效果:文字斜体也是文字斜体 文字粗体也是文字粗体 你可以组合它们 <hr>分割线123456---------- 三个以上短线\_\_\_ 三个下划线 效果: 超链接12345678910[连接名称](address , alt)[我是链接名](https://suchenrain.github.io, '我是标题')[<i class="fa fa-refresh"></i> 点我刷新](/sonfilename/)另一种超链接写法:[链接名][链接代号][here][3]然后在别的地方定义 3 这个详细链接信息[3]: https://suchenrain.github.io "suchenrain"直接展示链接的写法:<https://suchenrain.github.io> 我是链接名点我刷新 herehttps://suchenrain.github.io 键盘键1<kbd>Ctrl+[</kbd> and <kbd>Ctrl+]</kbd> 键盘键Ctrl+[ and Ctrl+] 代码块语法如下 12345678910111213`var example=true` //内联样式//换行并缩进 4 格....if(isAwesome){return true}//使用 3 个反引号,忽略括号(`) if (isAwesome){ return true } (`)//指定代码高亮语言(`javascript) if (isAwesome){ return true } (`) 效果: var example=true //内联样式 //换行并缩进 4 格 if(isAwesome){ return true } //使用 3 个反引号 123if (isAwesome){ return true} //指定代码高亮语言 123if (isAwesome) { return true;} Lists(列表)无序列表(Unordered)使用 -/*/+ 加一个空格 12345678910111213141516//层次使用一个 Tab 或者两个空格缩进- Item 1- Item 2 - Item 2a - Item 2b* Item 1* Item 2 - Item 2a - Item 2b- Item 1- Item 2 - Item 2a - Item 2b 效果: Item 1 Item 2 Item 2a Item 2b Item 1 Item 2 Item 2a Item 2b Item 1 Item 2 Item 2a Item 2b 有序列表(Ordered)使用 数字 加一个英文句点+一个空格,即:数字+.+空格 数字可以不用有序,会自动排序 123451.Item 11.Item 21.Item 31.Item 3a1.Item 3b Item 1 Item 2 Item 3 Item 3a Item 3b 图片(Image)12345![Alt text](src 'Optional title')# 图片+指定链接(其实就是链接结合图片)[![Alt text](src 'sub title')](link) 段落(Paragraph)以一个空行开始,以一个空行结束,中间的就是一个段落。 123456-----假装我是空行------我是一段话。我真的是一段话。-----假装我是空行------ 效果: 我是一段话。我真的是一段话。 块引用(Blockquotes)12345678> 给引用的文本开始位置都加一个 '>',> 便可组成一个块引用。在块引用中,可以结合> 其他 markdown 元素一块使用,比如列表。> **强调**> 也可以只在第一行加大于号,其他位置不加。>> - 块引用里使用列表,需要和上面的内容隔开一个空行> - 记得加空格哦。 效果(以下效果仅供参考…因为我修改了样式): 给引用的文本开始位置都加一个 ‘>’,便可组成一个块引用。在块引用中,可以结合其他 markdown 元素一块使用,比如列表。强调也可以只在第一行加大于号,其他位置不加。 块引用里使用列表,需要和上面的内容隔开一个空行 记得加空格哦。 Task Lists(任务清单)1234- [x] [links](), **formatting**, and <del>tags</del> supported- [x] list syntax required (any unordered or ordered list supported)- [x] this is a complete item- [ ] this is an incomplete item 效果: links, formatting, and tags supported list syntax required (any unordered or ordered list supported) this is a complete item this is an incomplete item Table(表格)You can create tables by assembling a list of words and dividing them with hyphens - (for the first row), and then separating each column with a pipe |: 1234567891011121314example 1:| First Header | Second Header || --------------------------- | ---------------------------- || Content from cell 1 | Content from cell 2 || Content in the first column | Content in the second column |example 2:| Item | Value || -------- | ----- || Computer | $1600 || Phone | $12 || Pipe | $1 | 效果:example 1 First Header Second Header Content from cell 1 Content from cell 2 Content in the first column Content in the second column example 2 Item Value Computer $1600 Phone $12 Pipe $1 忽略 markdown 语法使用\来忽略 markdown 语法 12\`\`\`Let's rename \*our-new-project\* to \*our-old-project\*. ```Let’s rename *our-new-project* to *our-old-project*. 参考文献:[1]. 掌握这几种 Markdown 语法你就够了[2]. Mastering Markdown]]></content>
<categories>
<category>markdown</category>
</categories>
<tags>
<tag>markdown</tag>
<tag>syntax</tag>
</tags>
</entry>
<entry>
<title><![CDATA[写在前面]]></title>
<url>%2Fposts%2F53323%2F</url>
<content type="text"><![CDATA[阅读是一种习惯。同样,写作也是。 思维是个调皮的家伙,时刻都在改变。你永远不知道下一秒他将会是什么样子。也许是一闪灵光,也许是思索千百次后的一次总结。时间在走,你也在走。哪天等你回想起的时候,却忘了它的模样。像相机一样,只是想把某一刻的思维定格在时光里。这样,回头望的时候就知道当时的心情了。 历史总是惊人的相似,因为文字记录下来的总是你会遇到或将要遇见的。文字是个伟大的发明,思维的传承离不开文字。所以我喜欢文字胜于话语,这大概是我比较沉闷的一个原因。 记录是个思考且费时的过程,很多时候都很难坚持或被忽视。这也是为什么我以前写的东西都丢失的原因,还是没有写作的习惯。阅读很简单,可是坚持阅读很难,更何况坚持写作。业精于勤荒于嬉,坚持。 写的比较乱,可能是心没静下来。反正就是坚持坚持再坚持!]]></content>
<categories>
<category>前言</category>
</categories>
<tags>
<tag>前言</tag>
</tags>
</entry>
<entry>
<title><![CDATA[下雨了]]></title>
<url>%2Fposts%2F28203%2F</url>
<content type="text"><![CDATA[没了阳光,影子也不复存在。 藏在心里的东西不会像酒一样收藏起时光的味道,遇上雨天,发霉也变得容易。上一次的日记大概也是去年的这个时候,其实应该是年记了,毕竟一年才文艺这么一回。就像是路上的一次小憩,整理好下一段旅程的情绪。放假前总是那么开心,睡着也会笑醒。毕竟没人会讨厌放假,心情也变得愉悦。夏目里说只要有想见的人,就不会孤单。或者说,内心有所期盼,天空也会变得明朗。 之前很喜欢雨天,在爱忧伤的年纪的时候。最近并不喜欢,可以说是讨厌,毕竟放假。其实无所谓喜欢不喜欢,不衬心情罢了。人的孤独寂寞,大抵因为内心的喜欢还在流浪。再怎么浪,也没能浪到她的心上。不过,黑夜给了你黑色的眼睛,也给了你黎明的希望。情由心生,心若晴朗,便是晴天。 昨天吹着昨天的风,今天听着今天的雨,明天追着明天的阳光。 下雨了,可是雨天之上总会是晴天。 2015.10.07 鱼缸里的四尾鱼]]></content>
<categories>
<category>朝花夕拾</category>
</categories>
<tags>
<tag>下雨了</tag>
<tag>旧时光</tag>
<tag>青春</tag>
</tags>
</entry>
</search>