-
Notifications
You must be signed in to change notification settings - Fork 545
/
index.html
2505 lines (2376 loc) · 182 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="generator" content="pandoc">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Growth 实战篇 Django版 – </title>
<style type="text/css">code{white-space: pre;}</style>
<style type="text/css">
div.sourceCode { overflow-x: auto; }
table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
margin: 0; padding: 0; vertical-align: baseline; border: none; }
table.sourceCode { width: 100%; line-height: 100%; }
td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
td.sourceCode { padding-left: 5px; }
code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
code > span.dt { color: #902000; } /* DataType */
code > span.dv { color: #40a070; } /* DecVal */
code > span.bn { color: #40a070; } /* BaseN */
code > span.fl { color: #40a070; } /* Float */
code > span.ch { color: #4070a0; } /* Char */
code > span.st { color: #4070a0; } /* String */
code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
code > span.ot { color: #007020; } /* Other */
code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
code > span.fu { color: #06287e; } /* Function */
code > span.er { color: #ff0000; font-weight: bold; } /* Error */
code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
code > span.cn { color: #880000; } /* Constant */
code > span.sc { color: #4070a0; } /* SpecialChar */
code > span.vs { color: #4070a0; } /* VerbatimString */
code > span.ss { color: #bb6688; } /* SpecialString */
code > span.im { } /* Import */
code > span.va { color: #19177c; } /* Variable */
code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code > span.op { color: #666666; } /* Operator */
code > span.bu { } /* BuiltIn */
code > span.ex { } /* Extension */
code > span.pp { color: #bc7a00; } /* Preprocessor */
code > span.at { color: #7d9029; } /* Attribute */
code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
</style>
<link rel="stylesheet" href="style.css">
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
<meta name="viewport" content="width=device-width">
</head>
<body>
<h1>全栈增长工程师实战</h1>
<p>By <a href="https://www.phodal.com/">Phodal</a>(Follow me: <a href="http://weibo.com/phodal">微博</a>、<a href="https://www.zhihu.com/people/phodal">知乎</a>、<a href="https://segmentfault.com/u/phodal">SegmentFault</a>)
</p>
<p><strong>阅读过程中遇到任何问题,请以issue的形式提出来,这样可以帮助其他读者来发现这个问题。</strong></p>
<p>GitHub: <a href="https://github.com/phodal/growth-in-action-django">全栈增长工程师实战</a></p>
<p>PDF、Mobi、Epub版本下载地址:<a href="https://github.com/phodal/growth-in-action/releases">https://github.com/phodal/growth-in-action/releases</a></p>
<p>微信公众号</p>
<p><img src="http://articles.phodal.com/qrcode.jpg" alt=""/></p>
<p>我的其他电子书:</p>
<ul>
<li>《<a href="https://github.com/phodal/growth-ebook">全栈增长工程师指南</a>》</li>
<li>《<a href="https://github.com/phodal/designiot">一步步搭建物联网系统</a>》</li>
<li>《<a href="https://github.com/phodal/github-roam">GitHub 漫游指南</a>》</li>
<li>《<a href="https://github.com/phodal/repractise">RePractise</a>》</li>
</ul>
<div style="width:800px">
<iframe src="http://ghbtns.com/github-btn.html?user=phodal&repo=growth-in-action-django&type=watch&count=true"
allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
</div>
<h2>全栈增长工程师实战目录</h2>
<nav id="TOC">
<ul>
<li><a href="#序如何成为全栈增长工程师">序:如何成为全栈增长工程师?</a></li>
<li><a href="#phodals-idea实战指南">Phodal’s Idea实战指南</a><ul>
<li><a href="#关于作者">关于作者</a></li>
<li><a href="#先成为全栈工程师">先成为全栈工程师</a></li>
<li><a href="#再成为增长工程师">再成为增长工程师</a></li>
</ul></li>
<li><a href="#全栈增长工程师实战">全栈增长工程师实战</a><ul>
<li><a href="#准备工作和工具">准备工作和工具</a></li>
</ul></li>
<li><a href="#深入浅出django">深入浅出Django</a><ul>
<li><a href="#django简介">Django简介</a><ul>
<li><a href="#django应用架构">Django应用架构</a></li>
</ul></li>
<li><a href="#django-helloworld">Django hello,world</a><ul>
<li><a href="#安装django">安装Django</a></li>
<li><a href="#创建项目">创建项目</a></li>
<li><a href="#django后台">Django后台</a></li>
<li><a href="#第一次提交">第一次提交</a></li>
</ul></li>
</ul></li>
<li><a href="#三步创建博客应用">三步创建博客应用</a><ul>
<li><a href="#tasking">Tasking</a></li>
<li><a href="#创建blogpostapp">创建BlogpostAPP</a><ul>
<li><a href="#生成app">生成APP</a></li>
<li><a href="#创建model">创建Model</a></li>
<li><a href="#配置url">配置URL</a></li>
</ul></li>
<li><a href="#创建view">创建View</a><ul>
<li><a href="#创建博客列表页">创建博客列表页</a></li>
<li><a href="#创建博客详情页">创建博客详情页</a></li>
</ul></li>
<li><a href="#测试">测试</a><ul>
<li><a href="#测试首页">测试首页</a></li>
<li><a href="#测试详情页">测试详情页</a></li>
</ul></li>
</ul></li>
<li><a href="#自动化测试与持续集成">自动化测试与持续集成</a><ul>
<li><a href="#编写自动化测试">编写自动化测试</a><ul>
<li><a href="#selenium与第一个ui测试">Selenium与第一个UI测试</a></li>
</ul></li>
<li><a href="#搭建持续集成">搭建持续集成</a><ul>
<li><a href="#jenkins创建任务">Jenkins创建任务</a></li>
<li><a href="#创建shell">创建shell</a></li>
</ul></li>
</ul></li>
<li><a href="#更完善的博客系统">更完善的博客系统</a><ul>
<li><a href="#静态页面">静态页面</a><ul>
<li><a href="#安装-flatpages">安装 flatpages</a></li>
<li><a href="#创建模板">创建模板</a></li>
</ul></li>
<li><a href="#评论功能">评论功能</a></li>
<li><a href="#sitemap">Sitemap</a><ul>
<li><a href="#站点地图介绍">站点地图介绍</a></li>
<li><a href="#创建首页的sitemap">创建首页的Sitemap</a></li>
<li><a href="#创建静态页面的sitemap">创建静态页面的Sitemap</a></li>
<li><a href="#创建博客的sitemap">创建博客的Sitemap</a></li>
<li><a href="#提交到搜索引擎">提交到搜索引擎</a></li>
</ul></li>
</ul></li>
<li><a href="#样式与ui美化">样式与UI美化</a><ul>
<li><a href="#响应式设计">响应式设计</a><ul>
<li><a href="#引入前端框架">引入前端框架</a></li>
</ul></li>
<li><a href="#页面美化">页面美化</a><ul>
<li><a href="#添加导航">添加导航</a></li>
<li><a href="#添加标语">添加标语</a></li>
<li><a href="#优化列表">优化列表</a></li>
<li><a href="#添加footer">添加footer</a></li>
</ul></li>
</ul></li>
<li><a href="#应用api">应用API</a><ul>
<li><a href="#博客列表">博客列表</a><ul>
<li><a href="#django-rest-framework">Django REST Framework</a></li>
<li><a href="#创建博客列表api">创建博客列表API</a></li>
<li><a href="#测试-api">测试 API</a></li>
</ul></li>
<li><a href="#自动完成">自动完成</a><ul>
<li><a href="#搜索api">搜索API</a></li>
<li><a href="#页面实现">页面实现</a></li>
</ul></li>
<li><a href="#跨域支持">跨域支持</a><ul>
<li><a href="#添加跨域支持">添加跨域支持</a></li>
</ul></li>
</ul></li>
<li><a href="#创建移动应用">创建移动应用</a><ul>
<li><a href="#helloworld">hello,world</a><ul>
<li><a href="#构建应用">构建应用</a></li>
</ul></li>
<li><a href="#博客列表页">博客列表页</a><ul>
<li><a href="#列表页">列表页</a></li>
<li><a href="#详情页">详情页</a></li>
</ul></li>
<li><a href="#profile">Profile</a><ul>
<li><a href="#json-web-tokens">Json Web Tokens</a></li>
<li><a href="#登录表单">登录表单</a></li>
<li><a href="#profile-1">Profile</a></li>
</ul></li>
<li><a href="#创建博客">创建博客</a></li>
</ul></li>
<li><a href="#移动单页面应用">移动单页面应用</a><ul>
<li><a href="#移动设备处理">移动设备处理</a></li>
<li><a href="#前后端分离">前后端分离</a><ul>
<li><a href="#riot.js">Riot.js</a></li>
<li><a href="#reactivejs构建服务">ReactiveJS构建服务</a></li>
<li><a href="#创建博客列表页-1">创建博客列表页</a></li>
<li><a href="#博客详情页">博客详情页</a></li>
<li><a href="#添加导航-1">添加导航</a></li>
</ul></li>
</ul></li>
<li><a href="#配置管理">配置管理</a><ul>
<li><a href="#local-settings">local settings</a></li>
</ul></li>
</ul>
</nav>
<h1 id="序如何成为全栈增长工程师">序:如何成为全栈增长工程师?</h1>
<h1 id="phodals-idea实战指南">Phodal’s Idea实战指南</h1>
<h2 id="关于作者">关于作者</h2>
<p>黄峰达(Phodal Huang)是一个创客、工程师、咨询师和作家。他毕业于西安文理学院电子信息工程专业,现作为一个咨询师就职于 ThoughtWorks 深圳。长期活跃于开源软件社区 GitHub,目前专注于物联网和前端领域。</p>
<p>作为一个开源软件作者,著有 Growth、Stepping、Lan、Echoesworks 等软件。其中开源学习应用 Growth,广受读者和用户好评,可在 APP Store 及各大 Android 应用商店下载。</p>
<p>作为一个技术作者,著有《自己动手设计物联网》(电子工业出版社)、《全栈应用开发:精益实践》(电子工业出版社,正在出版)。并在 GitHub 上开源有《Growth: 全栈增长工程师指南》、《GitHub 漫游指南》等七本电子书。</p>
<p>作为技术专家,他为英国 Packt 出版社审阅有物联网书籍《Learning IoT》、《Smart IoT》,前端书籍《Angular 2 Serices》、《Getting started with Angular》等技术书籍。</p>
<p>他热爱编程、写作、设计、旅行、hacking,你可以从他的个人网站:<a href="https://www.phodal.com/" class="uri">https://www.phodal.com/</a> 了解到更多的内容。</p>
<p>其它相关信息:</p>
<ul>
<li>微博:<a href="http://weibo.com/phodal" class="uri">http://weibo.com/phodal</a></li>
<li>GitHub: <a href="https://github.com/phodal" class="uri">https://github.com/phodal</a></li>
<li>知乎:<a href="https://www.zhihu.com/people/phodal" class="uri">https://www.zhihu.com/people/phodal</a></li>
<li>SegmentFault:<a href="https://segmentfault.com/u/phodal" class="uri">https://segmentfault.com/u/phodal</a></li>
</ul>
<p>当前为预览版,在使用的过程中遇到任何问题请及时与我联系。阅读过程中的问题,不妨在GitHub上提出来: <a href="https://github.com/phodal/fe/issues">Issues</a></p>
<p>阅读过程中遇到语法错误、拼写错误、技术错误等等,不妨来个Pull Request,这样可以帮助到其他阅读这本电子书的童鞋。</p>
<p>我的电子书:</p>
<ul>
<li>《<a href="https://github.com/phodal/github-roam">GitHub 漫游指南</a>》</li>
<li>《<a href="https://github.com/phodal/fe">我的职业是前端工程师</a>》</li>
<li>《<a href="https://github.com/phodal/serverless">Serverless 架构应用开发指南</a>》</li>
<li>《<a href="https://github.com/phodal/growth-ebook">Growth: 全栈增长工程师指南</a>》</li>
<li>《<a href="https://github.com/phodal/ideabook">Phodal’s Idea实战指南</a>》</li>
<li>《<a href="https://github.com/phodal/designiot">一步步搭建物联网系统</a>》</li>
<li>《<a href="https://github.com/phodal/repractise">RePractise</a>》</li>
<li>《<a href="https://github.com/phodal/growth-in-action">Growth: 全栈增长工程师实战</a>》</li>
</ul>
<p>我的微信公众号:</p>
<figure>
<img src="./images/wechat.jpg" alt="作者微信公众号:phodal-weixin" /><figcaption>作者微信公众号:phodal-weixin</figcaption>
</figure>
<p>支持作者,可以加入作者的小密圈:</p>
<figure>
<img src="./images/xiaomiquan.jpg" alt="小密圈" /><figcaption>小密圈</figcaption>
</figure>
<p>或者转账:</p>
<p><img src="./images/alipay.png" alt="支付宝" /> <img src="./images/wechat-pay.png" alt="微信" /></p>
<p>记得我们在《<a href="http://mp.weixin.qq.com/s?src=3&timestamp=1463835081&ver=1&signature=z1onJvKn4TSrUmXm384CQUF1IZBVsLShsQ4DpmumN6xY0Gm5RR9XKdbf6ELzdRqg-mxdtxceTg-4-KrhYHZQC6wiSEWsP64vh0sl2Je4G16hnS6MsuZaD-u01HAENCSKoMhQiw0tu2y3-tSJsOML0w==">RePractise前端篇: 前端演进史</a>》中提到技术在最近十几年的飞速发展,当然最主要的就是:技术的复杂度不断地从应用层抽象到了框架层。虽说:</p>
<blockquote>
<p>技术的复杂度同力一样不会消失,也不会凭空产生,它总是从一个物体转移到另一个物体或一种形式转为另一种形式。</p>
</blockquote>
<p>然而这也意味着成为一个全栈工程师,比以往的任何一个时间要容易得多。这也意味着一个全栈工程师也可以很快地成为一个Growth Hacking(中文:增长黑客)。所以,我们开始谈论如何成为一名<code>全栈增长工程师</code>。</p>
<h2 id="先成为全栈工程师">先成为全栈工程师</h2>
<p>在电子书《<a href="http://mp.weixin.qq.com/s?src=3&timestamp=1463835463&ver=1&signature=z1onJvKn4TSrUmXm384CQUF1IZBVsLShsQ4DpmumN6xzPP-WG-vZxJgzeXdGcPSFn9Erm6laV3FgnEMuiqMnHP0TadjpLl4tYHPhFr-yKWi35U*tGi-RKIdwGc2ylN9bA2Ph*KAl5w5CJRlw2LI9*g==">全栈增长工程师指南</a>》中,我们提到过成为全栈增长工程师的技术基础,但是没有并没有谈论到如何成为这样的全栈工程师——这是一个漫长的过程。</p>
<p>早期,当我们有一个想法的时候,我们会去搭建一个网站——如以WordPress作为CMS,以RoR、Django来开发应用等等。随后,我们将我们的网站推向市场,发现市场有点反应。</p>
<p>接着,我们不断地开发出一些新的功能——如CMS的留言、Sitemap等等。在这个过程中,我们会开发一些API来满足我们的需求。</p>
<p>在一个新的阶段里,我们开始推出移动应用。基于先前的API,我们不断地构建出了不同的API。或以单体应用的形式出现,或以微服务的形式产生出新的API。</p>
<p>然后,我们发现并不是所有的移动用户都愿意去下载我们的API。于是,我们推出了SPA(单页面应用),以此来迎接那些移动设备用户。</p>
<p>最后,我们的业务逐渐稳定了下来。我们开始了一些优化工作,或者如Facebook一样优化PHP,推出HHVM。或者如Netflix一样使用微服务解耦系统。又或者,我们使用新的架构对我们的系统进行重新的设计。</p>
<p>在整个过程中,我们将学习到如何去做网站后台、移动应用、API设计、前端单页面应用等等。从这种意义上来说,全栈工程师非常match初创企业所需要的技术要求。</p>
<h2 id="再成为增长工程师">再成为增长工程师</h2>
<p>Growth整一个系列:APP、社区、电子书《全栈增长工程师指南》、电子书《全栈增长工程师实战》算是我对Growth Hacking的一个研究。不过,对于一个人来说这工作量还是蛮大的——在完成两本电子书后,我们将继续研究。在这一个过程中,我发现一些很有意思的东西——只有开发出用户想要的东西,这个过程才容易实践起来的。</p>
<p>增长可以分为两部分:一个是自身的增长,一个是用户的增长。两者实际上是一种相互促进的关系,当我们的能力增长到一定的程度,我们才能推进用户的增长。相用户增长到一定的程度,也会推进我们的技能增长。</p>
<p>只是要在技术、数据分析、用户分析、创新等等有所突破,看上去好像不是一件容易的事。只是对于大部分的全栈工程师来说,实现技术、数据抓取和分析是一件容易的事。要实现对数据的敏感是一种很难的事,但是可视化过后的数据就一样了。对于用户的行为分析也是类似的,只是因为我们缺乏一些有效的练习。</p>
<p>更让人惊讶的是创新也是可以练习的,每次我们遇到一个问题的时候,就是我们离创新最近的时候——难道不是吗?当你遇到一个难解的问题,就是你开拓一个新的能力的时候。</p>
<p>好好享受这个学习的过程吧!</p>
<h1 id="全栈增长工程师实战">全栈增长工程师实战</h1>
<h2 id="准备工作和工具">准备工作和工具</h2>
<p>在开始写代码之前你需要保证你有一些Python基础,如果没有的话,请参阅其他相关书籍来一起学习。</p>
<p>并且你还需要在你的计算机上安装:</p>
<ul>
<li>Python环境及其包管理工具pip。</li>
<li>Firefox浏览器——用于运行功能测试。</li>
<li>Git版本控制器——用于代码版本控制。</li>
<li>一个开发工具。(PS: 在这里笔者使用的是PyCharm的社区版)</li>
</ul>
<h1 id="深入浅出django">深入浅出Django</h1>
<h2 id="django简介">Django简介</h2>
<p>Django是一个高级的Python Web开发框架,它的目标是使得开发复杂的、数据库驱动的网站变得更加简单。</p>
<p>由于Django最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的。所以,我们可以发现在使用Django的很多网站里,都是用于作为CMS(内容管理系统)来使用的。使用Django的一些比较知名的网站如下图所示:</p>
<figure>
<img src="./images/who-use-django.jpg" alt="使用Django的网站" /><figcaption>使用Django的网站</figcaption>
</figure>
<p>Django是一个MTV框架,其架构模板看上去与传统的MVC架构并没有太大的区别。其对比如下表所示:</p>
<table>
<thead>
<tr class="header">
<th>传统的MVC架构</th>
<th>Django 架构</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Model</td>
<td>Model(Data Access Logic)</td>
</tr>
<tr class="even">
<td>View</td>
<td>Template(Presentation Logic)</td>
</tr>
<tr class="odd">
<td>View</td>
<td>View(Business Logic)</td>
</tr>
<tr class="even">
<td>Controller</td>
<td>Django itself</td>
</tr>
</tbody>
</table>
<p>在Django中View只用来描述你要看到的内容,Template才是最后用于显示的内容。而在MVC架构中,这只相当于是View层。它的核心包含下面的四部分:</p>
<ul>
<li>一个 对象关系映射,作为数据模型和关系性数据库间的媒介(Model层);</li>
<li>一个基于正则表达式的URL分发器(即MVC中的Controller);</li>
<li>一个用于处理HTTP请求的系统,含web模板系统(View层);</li>
</ul>
<p>其核心框架还包含:</p>
<ul>
<li>一个轻量级的、独立的Web服务器,只用于开发和测试。</li>
<li>一个表单序列化及验证系统,用于将HTML表单转换成适用于数据库存储的数据。</li>
<li>一个缓存框架,并且可以从几种缓存方式中选择。</li>
<li>中间件支持,能对请求处理的各个阶段进行处理。</li>
<li>内置的分发系统允许应用程序中的组件采用预定义的信号进行相互间的通信。</li>
<li>一个序列化系统,能够生成或读取采用XML或JSON表示的Django模型实例。</li>
<li>一个用于扩展模板引擎的能力的系统。</li>
</ul>
<h3 id="django应用架构">Django应用架构</h3>
<p>Django的每一个模块在内部都称之为APP,在每个APP里都有自己的三层结构。如下图所示:</p>
<figure>
<img src="./images/django_app_arch.jpg" alt="Django 应用架构" /><figcaption>Django 应用架构</figcaption>
</figure>
<p>这样做不仅可以在开发的时候更容易理解系统,而且可以提高代码的可复用性——因为每一个APP都是独立的应用,在下次使用时我们只需要简单的复制和粘贴。</p>
<p>说了这么多,还不如从一个hello,world开始。</p>
<h2 id="django-helloworld">Django hello,world</h2>
<h3 id="安装django">安装Django</h3>
<p>安装Django之前,我们可以用virtualenv工具来创建一个虚拟的Python运行环境。环境问题是一个很复杂的问题,在我们使用Python的过程中,我们会不断地安装一些库,而这些库可能会有不同的版本。并且在安装Python库的过程中,我们会遇到权限问题——即我们需要超级用户的权限才能将库安装到系统的环境之下。随后在这个软件的生涯中,我们还需要保证这个项目所依赖的模块不会发生变动。而这些都是很棘手的一些事,这时候我们就需要创建一个虚拟的运行环境,而virtualenv就是这样的一个工具。</p>
<h4 id="virtualenv">virtualenv</h4>
<p>安装Python包我们需要用到pip命令,它是Python语言中的一个包管理工具。如果你没有安装的话,可以使用下面的命令来安装:</p>
<pre><code>curl https://bootstrap.pypa.io/get-pip.py | python</code></pre>
<p>在不同的Python环境中,我们可能需要使用不同的pip,如下所示是笔者使用的Python3的pip命令pip3</p>
<pre><code>$ pip3 install virtualenv</code></pre>
<p>如果是Python2.7的话,对应会有:</p>
<pre><code>$ pip install virtualenv</code></pre>
<p>需要注意的是这将会安装到Python所在的目录,如我的目录是:</p>
<pre><code>$ /usr/local/bin/virtualenv</code></pre>
<p>有的可能会是:</p>
<pre><code>$ /usr/local/share/python3/virtualenv</code></pre>
<p>在创建我们的这个虚拟环境之前,我们可以创建一个存储所有virtualenv的目录:</p>
<pre><code>$ mkdir somewhere/virtualenvs</code></pre>
<p>现在,我们就可以创建一个新的虚拟环境:</p>
<pre><code>$ virtualenv somewhere/virtualenvs/<project-name> --no-site-packages</code></pre>
<p>如果你想使用不同的Python版本的话,那么需要指定Python版本的路径</p>
<pre><code>$ virtualenv --distribute -p /usr/local/bin/python3.3 somewhere/virtualenvs/<project-name></code></pre>
<p>通过到相应的目录下执行激活就可以使用这个虚拟环境了:</p>
<pre><code>$ cd somewhere/virtualenvs/<project-name>/bin
$ source activate</code></pre>
<p>停止使用只需要执行下面的命令即可:</p>
<pre><code>$ deactivate</code></pre>
<h4 id="安装django-1">安装Django</h4>
<p>准备了这么久我们终于要开始安装Django了,执行:</p>
<pre><code>$ pip install django</code></pre>
<p>开始下最新版本的Django,如下所示:</p>
<pre><code>Collecting django
Downloading Django-1.9.4-py2.py3-none-any.whl (6.6MB)
94% |██████████████████████████████▎ | 6.2MB 251kB/s eta 0:00:02</code></pre>
<p>等下载完后,就会开始安装Django。安装完后,我们就可以使用Django自带的django-admin命令。django-admin是Django自带的一个管理任务的命令行工具。</p>
<p>通过这个命令,我们不仅仅可以用它来创建项目、创建app、运行服务、数据库迁移,还可以执行各种SQL工具等等。django-admin用法如下:</p>
<pre><code>$ django-admin <command> [options]</code></pre>
<p>下面是django-admin自带的一些命令:</p>
<pre><code>[django]
check
compilemessages
createcachetable
dbshell
diffsettings
dumpdata
flush
inspectdb
loaddata
makemessages
makemigrations
migrate
runfcgi
runserver
shell
sql
sqlall
sqlclear
sqlcustom
sqldropindexes
sqlflush
sqlindexes
sqlinitialdata
sqlmigrate
sqlsequencereset
squashmigrations
startapp
startproject
syncdb
test
testserver
validate</code></pre>
<p>现在,让我们来看看这个强大的工具。</p>
<h3 id="创建项目">创建项目</h3>
<p>在这些命令中startproject可以用于创建项目,在这里我们的项目名是blog,那么我们的命令如下:</p>
<p>$ django-admin startproject blog</p>
<p>这个命令将创建下面的文件内容,而这些是Django项目的一些必须文件。</p>
<pre><code>.
├── blog
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── manage.py</code></pre>
<p>blog目录对应的就是blog这个项目,将会放置这个项目的一些相关配置:</p>
<ol type="1">
<li>settings.py包含了这个项目的相关配置。如数据库环境、启用的插件等等。</li>
<li>urls.py即URL Dispatcher的配置,指明了某个URL应该指向某个函数来处理。</li>
<li>wsgi.py用于部署。WSGI(Python Web Server Gateway Interface,Web服务器网关接口)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。</li>
<li>__init__.py指明了这是一个Python模块。</li>
</ol>
<p>manage.py 会在每个Django项目中自动生成,它可以和django-admin做类似的事。如我们可以用manage.py来启动测试环境的服务器:</p>
<p>$ python manage.py runserver</p>
<pre><code>Performing system checks...
System check identified no issues (0 silenced).
You have unapplied migrations; your app may not work properly until they are applied.
Run 'python manage.py migrate' to apply them.
March 24, 2016 - 03:07:34
Django version 1.9.4, using settings 'blog.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Not Found: /
[24/Mar/2016 03:07:35] "GET / HTTP/1.1" 200 1767
Not Found: /favicon.ico
[24/Mar/2016 03:07:36] "GET /favicon.ico HTTP/1.1" 404 1934</code></pre>
<p>现在,我们只需要在浏览器中打开<a href="http://127.0.0.1:8000/" class="uri">http://127.0.0.1:8000/</a>,便可以访问我们的应用程序。</p>
<h3 id="django后台">Django后台</h3>
<p>Django很适合CMS的另外一个原因,就是它自带了一个后台管理系统。为了启用这个后台管理系统,我们需要配置我们的数据库,并创建相应的超级用户。如下所示的是settings.py中的默认数据库配置:</p>
<pre><code># Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}</code></pre>
<p>上面的配置中我们使用的是SQLite3作为数据库,并使用了当前目录下的<code>db.sqlite3</code>作为数据库文件。Django内建支持下面的一些数据库:</p>
<pre><code>'django.db.backends.postgresql_psycopg2'
'django.db.backends.mysql'
'django.db.backends.sqlite3'
'django.db.backends.oracle'</code></pre>
<p>如果我们想使用别的数据库,可以在网上寻找相应的解决方案,如用于支持使用MongoDB的django-nonrel项目。不同的数据库有不同的配置,如下所示的是使用PostgreSQL的配置。</p>
<pre><code>DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'mydatabase',
'USER': 'mydatabaseuser',
'PASSWORD': 'mypassword',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}</code></pre>
<p>接着,我们就可以运行数据库迁移,只需要运行相应的脚本即可:</p>
<p>$ python manage.py migrate</p>
<pre><code>Operations to perform:
Apply all migrations: sessions, admin, auth, contenttypes
Running migrations:
Rendering model states... DONE
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying sessions.0001_initial... OK
(growth-django)</code></pre>
<p>在上面的过程中,我们会创建相应的数据库模型,并依据迁移脚本来创建一些相应的数据,如默认的配置等等。</p>
<p>最后,我们可以创建一个相应的超级用户来登陆后台。</p>
<p>$ python manage.py createsuperuser</p>
<pre><code>Username (leave blank to use 'fdhuang'): root
Email address: [email protected]
Password:
Password (again):
Superuser created successfully.</code></pre>
<p>输入相应的用户名和密码,即可完成创建。然后访问 <a href="http://127.0.0.1:8000/admin" class="uri">http://127.0.0.1:8000/admin</a>,输入上面的用户名和密码就可以来到后台:</p>
<figure>
<img src="./images/django-backend.jpg" alt="Django后台" /><figcaption>Django后台</figcaption>
</figure>
<h3 id="第一次提交">第一次提交</h3>
<p>在创建完应用后,我们就可以进行第一次提交,通常初次提交的提交信息(commit message)是<code>init project</code>。如果在那之前,你没有执行<code>git init</code>来初始化git的话,那么我们就需要去执行这个命令。</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> init</code></pre></div>
<p>它将返回类似于下面的结果</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">Initialized</span> empty Git repository in /Users/fdhuang/test/helloworld/.git/</code></pre></div>
<p>即初始化了一个空的Git项目,然后我们就可以执行<code>add</code>来添加上面的内容:</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> add .</code></pre></div>
<p>需要注意的是上面的数据库文件不应该添加到项目里,所以我们应该执行reset命令来重置这个状态:</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> reset db.sqlite3</code></pre></div>
<p>这时我们会将其变成下面的状态:</p>
<figure>
<img src="./images/first-commit.png" alt="第一次提交前的reset" /><figcaption>第一次提交前的reset</figcaption>
</figure>
<p>上面的绿色文件代表这几个文件都被添加了进去,蓝色则代表未添加的文件。为了避免手误产生一些问题,我们需要添加一个名为<code>.gitignore</code>文件用于将一些文件名加入忽略名单,如下是常用的python项目的<code>.gitignore</code>文件中的内容:</p>
<pre><code>*.pyc
*.db
*.sqlite3</code></pre>
<p>当我们添加完这个文件,git就会识别这个文件,并忽略原来的那些文件,如下图所示:</p>
<figure>
<img src="./images/git-ignore.png" alt="添加完gitignore文件后的效果" /><figcaption>添加完gitignore文件后的效果</figcaption>
</figure>
<p>我们只需要添加这个文件即可:</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> add .gitignore</code></pre></div>
<p>如果你之前已经不小心添加了一些不应该添加的文件,那么可以执行下面的命令来重置其状态:</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> reset .</code></pre></div>
<p>然后再执行添加命令。</p>
<p>最后,我们就可以在本地提交我们的代码了:</p>
<pre><code>git commit -m "init project"</code></pre>
<p>如果你是将代码托管在GitHub上的话,那么你就可以执行<code>git push</code>来将代码提交到服务器上。</p>
<h1 id="三步创建博客应用">三步创建博客应用</h1>
<h2 id="tasking">Tasking</h2>
<p>在我们不了解Django的时候,要对这样一个任务进行Tasking,有点困难。不过,我们还是可以简单地看看是应该如何去做:</p>
<ul>
<li>生成APP。对于大部分主流的Web框架来说,它们都可以手动地生成一些脚手架,如Ruby语言中的Ruby On Rails、Node.js中的Express等等。</li>
<li>创建对应的Model,即其在数据库中存储的模型与我们在代码中要使用的模型。</li>
<li>创建程序对应的View,用于处理数据。</li>
<li>创建程序的Template,用于显示数据。</li>
<li>编写测试来保证功能。</li>
</ul>
<p>对于其他应用来说也是差不多的。</p>
<h2 id="创建blogpostapp">创建BlogpostAPP</h2>
<h3 id="生成app">生成APP</h3>
<p>现在我们可以开始创建我们的APP,使用下面的代码来创建:</p>
<p>$ django-admin startapp blogpost</p>
<p>会在blogpost目录下,生成下面的文件:</p>
<pre><code>.
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py</code></pre>
<h3 id="创建model">创建Model</h3>
<p>现在,我们需要来创建博客的Model即可。对于一篇基本的博客来说,它会包含下面的几部分内容:</p>
<ul>
<li>标题</li>
<li>作者</li>
<li>链接(中文更需要一个好的链接)</li>
<li>内容</li>
<li>发布日期</li>
</ul>
<p>我们就可以按照上面的内容来创建我们的Blogpost model:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.db <span class="im">import</span> models
<span class="im">from</span> django.db.models <span class="im">import</span> permalink
<span class="kw">class</span> Blogpost(models.Model):
title <span class="op">=</span> models.CharField(max_length<span class="op">=</span><span class="dv">100</span>, unique<span class="op">=</span><span class="va">True</span>)
author <span class="op">=</span> models.CharField(max_length<span class="op">=</span><span class="dv">100</span>, unique<span class="op">=</span><span class="va">True</span>)
slug <span class="op">=</span> models.SlugField(max_length<span class="op">=</span><span class="dv">100</span>, unique<span class="op">=</span><span class="va">True</span>)
body <span class="op">=</span> models.TextField()
posted <span class="op">=</span> models.DateField(db_index<span class="op">=</span><span class="va">True</span>, auto_now_add<span class="op">=</span><span class="va">True</span>)
<span class="kw">def</span> <span class="fu">__unicode__</span>(<span class="va">self</span>):
<span class="cf">return</span> <span class="st">'</span><span class="sc">%s</span><span class="st">'</span> <span class="op">%</span> <span class="va">self</span>.title
<span class="at">@permalink</span>
<span class="kw">def</span> get_absolute_url(<span class="va">self</span>):
<span class="cf">return</span> (<span class="st">'view_blog_post'</span>, <span class="va">None</span>, { <span class="st">'slug'</span>: <span class="va">self</span>.slug })</code></pre></div>
<p>上面的<code>get_absolute_url</code>方法就是用于返回博客的链接。之所以使用手动而不是自动生成,是因为自动生成不靠谱,而且不利</p>
<p>然后在Admin注册这个Model</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.contrib <span class="im">import</span> admin
<span class="im">from</span> blogpost.models <span class="im">import</span> Blogpost
<span class="kw">class</span> BlogpostAdmin(admin.ModelAdmin):
exclude <span class="op">=</span> [<span class="st">'posted'</span>]
prepopulated_fields <span class="op">=</span> {<span class="st">'slug'</span>: (<span class="st">'title'</span>,)}
admin.site.register(Blogpost, BlogpostAdmin)</code></pre></div>
<p>接着我们需要先将<code>blogpost</code>这个APP添加到配置文件<code>blog/blog/settings.py</code>的<code>INSTALLED_APPS</code>字段中:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">INSTALLED_APPS <span class="op">=</span> [
<span class="st">'blogpost.apps.BlogpostConfig'</span>,
<span class="st">'django.contrib.admin'</span>,
...
]</code></pre></div>
<p>然后做数据库迁移:</p>
<pre class="shelln"><code>python manage.py migrate</code></pre>
<p>这时会提示:</p>
<pre class="shell"><code>Operations to perform:
Apply all migrations: admin, contenttypes, auth, sessions
Running migrations:
No migrations to apply.
Your models have changes that are not yet reflected in a migration, and so won't be applied.
Run 'manage.py makemigrations' to make new migrations, and then re-run 'manage.py migrate' to apply them.</code></pre>
<p>是因为我们忘记了先运行</p>
<pre class="shell"><code>python manage.py makemigrations</code></pre>
<p>进入后台,我们就可以看到BLOGPOST的一栏里,就可以对其进行相关的操作。</p>
<figure>
<img src="./images/django-admin-ui.png" alt="Django后台界面" /><figcaption>Django后台界面</figcaption>
</figure>
<p>点击Blogpost的Add后,我们就会进入如下的添加博客界面:</p>
<figure>
<img src="./images/admin-blog.png" alt="Django添加博客" /><figcaption>Django添加博客</figcaption>
</figure>
<p>实际上,这样做的意义是将删除(Delete)、修改(Update)、添加(Create)这些内容交给用户后台来做,当然它也不需要在View/Template层来做。在我们的Template层中,我们只需要关心如何来显示这些数据。</p>
<p>现在,我们可以执行一次新的代码提交——因为现在的代码可以正常工作。这样出现问题时,我们就可以即时的返回上一版本的代码。</p>
<pre><code>git add .
git commit -m "create blogpost model"</code></pre>
<p>然后再进行下一步地操作。</p>
<h3 id="配置url">配置URL</h3>
<p>现在,我们就可以在我们的<code>urls.py</code>里添加相应的route来访问页面,代码如下所示:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.conf <span class="im">import</span> settings
<span class="im">from</span> django.conf.urls <span class="im">import</span> include, url
<span class="im">from</span> django.conf.urls.static <span class="im">import</span> static
<span class="im">from</span> django.contrib <span class="im">import</span> admin
urlpatterns <span class="op">=</span> [
(<span class="vs">r'^$'</span>, <span class="st">'blogpost.views.index'</span>),
url(<span class="vs">r'^blog/(?P<slug>[^\.]+).html'</span>, <span class="st">'blogpost.views.view_post'</span>, name<span class="op">=</span><span class="st">'view_blog_post'</span>),
url(<span class="vs">r'^admin/'</span>, include(admin.site.urls))
]</code></pre></div>
<p>在上面的代码里,我们创建了两个route:</p>
<ul>
<li>指向首页,其view是index</li>
<li>指向博客详情页,其view是view_post</li>
</ul>
<p>指向博客详情页的URL正则<code>r'^blog/(?P<slug>[^\.]+).html</code>,会将形如blog/hello-world.html中的hello-world提取出来作为参数传给view_post方法。</p>
<p>接着,我们就可以创建两个view。</p>
<h2 id="创建view">创建View</h2>
<h3 id="创建博客列表页">创建博客列表页</h3>
<p>对于我们的首页来说,我们可以简单的只显示五篇博客,所以我们所需要做的就是从我们的Blogpost对象中,取出前五个结果即可。代码如下所示:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.shortcuts <span class="im">import</span> render, render_to_response, get_object_or_404
<span class="im">from</span> blogpost.models <span class="im">import</span> Blogpost
<span class="kw">def</span> index(request):
<span class="cf">return</span> render_to_response(<span class="st">'index.html'</span>, {
<span class="st">'posts'</span>: Blogpost.objects.<span class="bu">all</span>()[:<span class="dv">5</span>]
})</code></pre></div>
<p>Django的render_to_response方法可以根据一个给定的上下文字典渲染一个给定的目标,并返回渲染后的HttpResponse。即将相应的值,如这里的Blogpost.objects.all()[:5],填入相应的index.html中,再返回最后的结果。</p>
<p>首先,我们需要创建一个templates文件夹,然后在setting.py的TEMPLATES字段将该目录指定为默认目录</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"> TEMPLATES <span class="op">=</span> [
{
<span class="st">'BACKEND'</span>: <span class="st">'django.template.backends.django.DjangoTemplates'</span>,
<span class="st">'DIRS'</span>: [<span class="st">'templates/'</span>],
<span class="st">'APP_DIRS'</span>: <span class="va">True</span>,
<span class="st">'OPTIONS'</span>: {
<span class="st">'context_processors'</span>: [
<span class="st">'django.template.context_processors.debug'</span>,
<span class="st">'django.template.context_processors.request'</span>,
<span class="st">'django.contrib.auth.context_processors.auth'</span>,
<span class="st">'django.contrib.messages.context_processors.messages'</span>,
],
},
},
]</code></pre></div>
<p>另外,在templates目录下我们需要新建base.html, index.html和blogpost_detail.html三个模板。</p>
<div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html">{% load staticfiles %}
<span class="kw"><html></span>
<span class="kw"><head></span>
<span class="kw"><meta</span><span class="ot"> charset=</span><span class="st">"utf-8"</span><span class="kw">></span>
<span class="kw"><meta</span><span class="ot"> http-equiv=</span><span class="st">"X-UA-Compatible"</span><span class="ot"> content=</span><span class="st">"IE=edge"</span><span class="kw">></span>
<span class="kw"><meta</span><span class="ot"> name=</span><span class="st">"viewport"</span><span class="ot"> content=</span><span class="st">"width=device-width, initial-scale=1"</span><span class="kw">></span>
<span class="kw"><title></span>{% block head_title %}Welcome to my blog{% endblock %}<span class="kw"></title></span>
<span class="kw"><link</span><span class="ot"> rel=</span><span class="st">"stylesheet"</span><span class="ot"> type=</span><span class="st">"text/css"</span><span class="ot"> href=</span><span class="st">"{% static 'css/bootstrap.min.css' %}"</span><span class="kw">></span>
<span class="kw"><link</span><span class="ot"> rel=</span><span class="st">"stylesheet"</span><span class="ot"> type=</span><span class="st">"text/css"</span><span class="ot"> href=</span><span class="st">"{% static 'css/styles.css' %}"</span><span class="kw">></span>
<span class="kw"></head></span>
<span class="kw"><body</span><span class="ot"> data-twttr-rendered=</span><span class="st">"true"</span><span class="ot"> class=</span><span class="st">"bs-docs-home"</span><span class="kw">></span>
<span class="kw"><header</span><span class="ot"> class=</span><span class="st">"navbar navbar-static-top bs-docs-nav"</span><span class="ot"> id=</span><span class="st">"top"</span><span class="ot"> role=</span><span class="st">"banner"</span><span class="kw">></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"container"</span><span class="kw">></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"navbar-header"</span><span class="kw">></span>
<span class="kw"><button</span><span class="ot"> class=</span><span class="st">"navbar-toggle collapsed"</span><span class="ot"> type=</span><span class="st">"button"</span><span class="ot"> data-toggle=</span><span class="st">"collapse"</span>
<span class="ot"> data-target=</span><span class="st">".bs-navbar-collapse"</span><span class="kw">></span>
<span class="kw"><span</span><span class="ot"> class=</span><span class="st">"sr-only"</span><span class="kw">></span>切换视图<span class="kw"></span></span>
<span class="kw"><span</span><span class="ot"> class=</span><span class="st">"icon-bar"</span><span class="kw">></span></span>
<span class="kw"><span</span><span class="ot"> class=</span><span class="st">"icon-bar"</span><span class="kw">></span></span>
<span class="kw"><span</span><span class="ot"> class=</span><span class="st">"icon-bar"</span><span class="kw">></span></span>
<span class="kw"></button></span>
<span class="kw"><a</span><span class="ot"> href=</span><span class="st">"/"</span><span class="ot"> class=</span><span class="st">"navbar-brand"</span><span class="kw">></span>Growth博客<span class="kw"></a></span>
<span class="kw"></div></span>
<span class="kw"><nav</span><span class="ot"> class=</span><span class="st">"collapse navbar-collapse bs-navbar-collapse"</span><span class="ot"> role=</span><span class="st">"navigation"</span><span class="kw">></span>
<span class="kw"><ul</span><span class="ot"> class=</span><span class="st">"nav navbar-nav"</span><span class="kw">></span>
<span class="kw"><li></span>
<span class="kw"><a</span><span class="ot"> href=</span><span class="st">"/pages/about/"</span><span class="kw">></span>关于我<span class="kw"></a></span>
<span class="kw"></li></span>
<span class="kw"><li></span>
<span class="kw"><a</span><span class="ot"> href=</span><span class="st">"/pages/resume/"</span><span class="kw">></span>简历<span class="kw"></a></span>
<span class="kw"></li></span>
<span class="kw"></ul></span>
<span class="kw"><ul</span><span class="ot"> class=</span><span class="st">"nav navbar-nav navbar-right"</span><span class="kw">></span>
<span class="kw"><li><a</span><span class="ot"> href=</span><span class="st">"/admin"</span><span class="ot"> id=</span><span class="st">"loginLink"</span><span class="kw">></span>登入<span class="kw"></a></li></span>
<span class="kw"></ul></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"col-sm-3 col-md-3 pull-right"</span><span class="kw">></span>
<span class="kw"><form</span><span class="ot"> class=</span><span class="st">"navbar-form"</span><span class="ot"> role=</span><span class="st">"search"</span><span class="kw">></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"input-group"</span><span class="kw">></span>
<span class="kw"><input</span><span class="ot"> type=</span><span class="st">"text"</span><span class="ot"> id=</span><span class="st">"typeahead-input"</span><span class="ot"> class=</span><span class="st">"form-control"</span><span class="ot"> placeholder=</span><span class="st">"Search"</span><span class="ot"> name=</span><span class="st">"search"</span><span class="ot"> data-provide=</span><span class="st">"typeahead"</span><span class="kw">></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"input-group-btn"</span><span class="kw">></span>
<span class="kw"><button</span><span class="ot"> class=</span><span class="st">"btn btn-default search-button"</span><span class="ot"> type=</span><span class="st">"submit"</span><span class="kw">><i</span><span class="ot"> class=</span><span class="st">"glyphicon glyphicon-search"</span><span class="kw">></i></button></span>
<span class="kw"></div></span>
<span class="kw"></div></span>
<span class="kw"></form></span>
<span class="kw"></div></span>
<span class="kw"></nav></span>
<span class="kw"></div></span>
<span class="kw"></header></span>
<span class="kw"><main</span><span class="ot"> class=</span><span class="st">"bs-docs-masthead"</span><span class="ot"> id=</span><span class="st">"content"</span><span class="ot"> role=</span><span class="st">"main"</span><span class="kw">></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"container"</span><span class="kw">></span>
<span class="kw"><div</span><span class="ot"> id=</span><span class="st">"carbonads-container"</span><span class="kw">></span>
THE ONLY FAIR IS NOT FAIR <span class="kw"><br></span>
ENJOY CREATE <span class="er">&</span> SHARE
<span class="kw"></div></span>
<span class="kw"></div></span>
<span class="kw"></main></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"container"</span><span class="ot"> id=</span><span class="st">"container"</span><span class="kw">></span>
{% block content %}
{% endblock %}
<span class="kw"></div></span>
<span class="kw"><footer</span><span class="ot"> class=</span><span class="st">"footer"</span><span class="kw">></span>
<span class="kw"><div</span><span class="ot"> class=</span><span class="st">"container"</span><span class="kw">></span>
<span class="kw"><p</span><span class="ot"> class=</span><span class="st">"text-muted"</span><span class="kw">></span>@Copyright Phodal.com<span class="kw"></p></span>
<span class="kw"></div></span>
<span class="kw"></footer></span>
<span class="kw"><script</span><span class="ot"> src=</span><span class="st">"{% static 'js/jquery.min.js' %}"</span><span class="kw">></script></span>
<span class="kw"><script</span><span class="ot"> src=</span><span class="st">"{% static 'js/bootstrap.min.js' %}"</span><span class="kw">></script></span>
<span class="kw"><script</span><span class="ot"> src=</span><span class="st">"{% static 'js/bootstrap3-typeahead.min.js' %}"</span><span class="kw">></script></span>
<span class="kw"><script</span><span class="ot"> src=</span><span class="st">"{% static 'js/main.js' %}"</span><span class="kw">></script></span>
<span class="kw"></body></span>
<span class="kw"></html></span></code></pre></div>
<p>在我们的index.html中,我们就可以拿到前五篇博客。我们只需要遍历出posts,拿出每个post相应的值,就可以完成列表页。</p>
<div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html">{% extends 'base.html' %}
{% block title %}Welcome to my blog{% endblock %}
{% block content %}
<span class="kw"><h1></span>Posts<span class="kw"></h1></span>
{% for post in posts %}
<span class="kw"><h2><a</span><span class="ot"> href=</span><span class="st">"{{ post.get_absolute_url }}"</span><span class="kw">></span>{{ post.title }}<span class="kw"></a></h2></span>
<span class="kw"><p></span>{{post.posted}} - By {{post.author}}<span class="kw"></p></span>
<span class="kw"><p></span>{{post.body}}<span class="kw"></p></span>
{% endfor %}
{% endblock %}</code></pre></div>
<p>在上面的模板里,我们还取出了博客的链接用于跳转到详情页。</p>
<h3 id="创建博客详情页">创建博客详情页</h3>
<p>依据上面拿到的slug,我们就可以创建对应的详情页的view,代码如下所示:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">def</span> view_post(request, slug):
<span class="cf">return</span> render_to_response(<span class="st">'blogpost_detail.html'</span>, {
<span class="st">'post'</span>: get_object_or_404(Blogpost, slug<span class="op">=</span>slug)
})</code></pre></div>
<p>这里的<code>get_object_or_404</code>将会根据slug来获取相应的博客,如果取不出相应的博客就会返回404。因此,我们的详情页和上面的列表页也是类似的。</p>
<div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html">{% extends 'base.html' %}
{% block head_title %}{{ post.title }}{% endblock %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<span class="kw"><h2></span>{{ post.title }}<span class="kw"></a></h2></span>
<span class="kw"><p></span>{{post.posted}} - By {{post.author}}<span class="kw"></p></span>
<span class="kw"><p></span>{{post.body}}<span class="kw"></p></span>
{% endblock %}</code></pre></div>
<p>随后,我们就可以再提交一次代码了。</p>
<h2 id="测试">测试</h2>
<p>TDD虽然是一个非常好的实践,但是那是对于那些已经习惯写测试的人来说。如果你写测试的经历非常少,那么我们就可以从写测试开始。</p>
<p>在这里我们使用的是Django这个第三方框架来完成我们的工作,所以我们并不对这个框架的功能进行测试。虽然有些时候正是因为这些第三方框架的问题而导致的Bug,但是我们仅仅只是使用一些基础的功能。这些基础的功能也已经在他们的框架中测试过了。</p>
<h3 id="测试首页">测试首页</h3>
<p>先来做一个简单的测试,即测试我们访问首页的时候,调用的函数是上面的index函数</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.core.urlresolvers <span class="im">import</span> resolve
<span class="im">from</span> django.http <span class="im">import</span> HttpRequest
<span class="im">from</span> django.test <span class="im">import</span> TestCase
<span class="im">from</span> blogpost.views <span class="im">import</span> index, view_post
<span class="kw">class</span> HomePageTest(TestCase):
<span class="kw">def</span> test_root_url_resolves_to_home_page_view(<span class="va">self</span>):
found <span class="op">=</span> resolve(<span class="st">'/'</span>)
<span class="va">self</span>.assertEqual(found.func, index)</code></pre></div>
<p>但是这样的测试看上去没有多大意义,不过它可以保证我们的route可以和我们的URL对应上。在编写完测试后,我们就可以命令提示行中运行:</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">python</span> manage.py test</code></pre></div>
<p>来查看测试的结果:</p>
<pre><code>Creating test database for alias 'default'...
.
----------------------------------------------------------------------
Ran 1 test in 0.031s
OK
Destroying test database for alias 'default'...
(growth-django)</code></pre>
<p>运行通过,现在我们可以进行下一个测试了——我们可以测试页面的标题是不是我们想要的结果:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"> <span class="kw">def</span> test_home_page_returns_correct_html(<span class="va">self</span>):
request <span class="op">=</span> HttpRequest()
response <span class="op">=</span> index(request)
<span class="va">self</span>.assertIn(b<span class="st">'<title>Welcome to my blog</title>'</span>, response.content)</code></pre></div>
<p>这里我们需要去请求相应的页面来获取页面的标题,并用assertIn方法来断言返回的首页的html中含有<code><title>Welcome to my blog</title></code>。</p>
<h3 id="测试详情页">测试详情页</h3>
<p>同样的我们也可以用测试是否调用某个函数的方法,来看博客的详情页的route是否正确?</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogpostTest(TestCase):
<span class="kw">def</span> test_blogpost_url_resolves_to_blog_post_view(<span class="va">self</span>):
found <span class="op">=</span> resolve(<span class="st">'/blog/this_is_a_test.html'</span>)
<span class="va">self</span>.assertEqual(found.func, view_post)</code></pre></div>
<p>与上面测试首页不一样的是,在我们的Blogpost测试中,我们需要创建数据,以确保这个流程是没有问题的。因此我们需要用<code>Blogpost.objects.create</code>方法来创建一个数据,然后访问相应的页面来看是否正确。</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">def</span> test_blogpost_create_with_view(<span class="va">self</span>):
Blogpost.objects.create(title<span class="op">=</span><span class="st">'hello'</span>, author<span class="op">=</span><span class="st">'admin'</span>, slug<span class="op">=</span><span class="st">'this_is_a_test'</span>, body<span class="op">=</span><span class="st">'This is a blog'</span>,
posted<span class="op">=</span>datetime.now)
response <span class="op">=</span> <span class="va">self</span>.client.get(<span class="st">'/blog/this_is_a_test.html'</span>)
<span class="va">self</span>.assertIn(b<span class="st">'This is a blog'</span>, response.content)</code></pre></div>
<p>或许你会疑惑这个数据会不会被注入到数据库中,请看运行测试时返回的结果的第一句:</p>
<pre><code>Creating test database for alias 'default'...</code></pre>
<p>Django将会创建一个数据库用于测试。</p>
<p>同理,我们也可以为首页添加一个相似的测试:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">def</span> test_blogpost_create_with_show_in_homepage(<span class="va">self</span>):
Blogpost.objects.create(title<span class="op">=</span><span class="st">'hello'</span>, author<span class="op">=</span><span class="st">'admin'</span>, slug<span class="op">=</span><span class="st">'this_is_a_test'</span>, body<span class="op">=</span><span class="st">'This is a blog'</span>,
posted<span class="op">=</span>datetime.now)
response <span class="op">=</span> <span class="va">self</span>.client.get(<span class="st">'/'</span>)
<span class="va">self</span>.assertIn(b<span class="st">'This is a blog'</span>, response.content)</code></pre></div>
<p>我们用同样的方法创建了一篇博客,然后在首页测试返回的内容中是否含有<code>This is a blog</code>。</p>
<h1 id="自动化测试与持续集成">自动化测试与持续集成</h1>
<p>在上一章最后,我们写的测试可以算得上是单元测试,接着我们可以写一些自动化测试。</p>
<h2 id="编写自动化测试">编写自动化测试</h2>
<p>接着我们就可以用Selenium来做自动化测试。这是ThoughtWorks出品的一个强大的基于浏览器的开源自动化测试工具,它通常用来编写Web 应用的自动化测试。</p>
<h3 id="selenium与第一个ui测试">Selenium与第一个UI测试</h3>
<p>先让我们来看一个自动化测试的例子:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.test <span class="im">import</span> LiveServerTestCase
<span class="im">from</span> selenium <span class="im">import</span> webdriver
<span class="kw">class</span> HomepageTestCase(LiveServerTestCase):
<span class="kw">def</span> setUp(<span class="va">self</span>):
<span class="va">self</span>.selenium <span class="op">=</span> webdriver.Firefox()
<span class="va">self</span>.selenium.maximize_window()
<span class="bu">super</span>(HomepageTestCase, <span class="va">self</span>).setUp()
<span class="kw">def</span> tearDown(<span class="va">self</span>):
<span class="va">self</span>.selenium.quit()
<span class="bu">super</span>(HomepageTestCase, <span class="va">self</span>).tearDown()
<span class="kw">def</span> test_visit_homepage(<span class="va">self</span>):
<span class="va">self</span>.selenium.get(
<span class="st">'</span><span class="sc">%s%s</span><span class="st">'</span> <span class="op">%</span> (<span class="va">self</span>.live_server_url, <span class="st">"/"</span>)
)
<span class="va">self</span>.assertIn(<span class="st">"Welcome to my blog"</span>, <span class="va">self</span>.selenium.title)</code></pre></div>
<p>在setUp——即开始的时候,我们会用selenium启动一个Firefox浏览器的进程,并执行maximize_window来将窗口最大化。在tearDown——即结束的时候,我们就会关闭这个浏览器的进程。我们的主要测试代码就在<code>test_visit_homepage</code>这个方法里,我们在里面访问首页,并判断标题是不是<code>Welcome to my blog</code>。</p>
<p>运行上面的测试就会启动一个浏览器,并且会在浏览器上进行相应的操作。如下图所示:</p>
<figure>
<img src="./images/selenium-demo.jpg" alt="Selenium Demo" /><figcaption>Selenium Demo</figcaption>
</figure>
<p>这时你可能会产生一些疑惑,这些内容我们不是已经测试过了么?两者从测试看是差不多的,但是从流程上看来说并不是如些。下图是页面渲染的时间线:</p>
<figure>
<img src="./images/page-timing-overview.png" alt="页面渲染时间线" /><figcaption>页面渲染时间线</figcaption>
</figure>
<p>请求从浏览器传到服务器要有一系列的过程,如重定向、缓存、DNS等等,最后直至返回对应的Response。我们用Django的测试框架只能实现到这一步,随后页面请请求对应的静态资料,再对页面进行渲染,在这个过程中页面的内容会发生一些变化。</p>
<p>为了避免页面的内容被替换掉,那么我们就需要对这部分内容进行测试。</p>
<p>如下的代码也是可以用于测试页面内容的代码:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogpostDetailCase(LiveServerTestCase):
<span class="kw">def</span> setUp(<span class="va">self</span>):
Blogpost.objects.create(
title<span class="op">=</span><span class="st">'hello'</span>,
author<span class="op">=</span><span class="st">'admin'</span>,
slug<span class="op">=</span><span class="st">'this_is_a_test'</span>,
body<span class="op">=</span><span class="st">'This is a blog'</span>,
posted<span class="op">=</span>datetime.now
)
<span class="va">self</span>.selenium <span class="op">=</span> webdriver.Firefox()
<span class="va">self</span>.selenium.maximize_window()
<span class="bu">super</span>(BlogpostDetailCase, <span class="va">self</span>).setUp()
<span class="kw">def</span> tearDown(<span class="va">self</span>):
<span class="va">self</span>.selenium.quit()
<span class="bu">super</span>(BlogpostDetailCase, <span class="va">self</span>).tearDown()
<span class="kw">def</span> test_visit_blog_post(<span class="va">self</span>):
<span class="va">self</span>.selenium.get(
<span class="st">'</span><span class="sc">%s%s</span><span class="st">'</span> <span class="op">%</span> (<span class="va">self</span>.live_server_url, <span class="st">"/blog/this_is_a_test.html"</span>)
)
<span class="va">self</span>.assertIn(<span class="st">"hello"</span>, <span class="va">self</span>.selenium.title)</code></pre></div>
<p>虽然在这里我们要测试的只是页面的标题,而实际上我们要测试的是页面的元素是否存在。</p>
<p>同样的,我们也可以对博客的内容进行测试。这些稍有不同的是,我们更多地是要测试用户的行为,如我们在首页点击某个链接,那么我应该中转到对应的博客详情页,如下代码所示:</p>
<div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogpostFromHomepageCase(LiveServerTestCase):
<span class="kw">def</span> setUp(<span class="va">self</span>):
Blogpost.objects.create(
title<span class="op">=</span><span class="st">'hello'</span>,
author<span class="op">=</span><span class="st">'admin'</span>,
slug<span class="op">=</span><span class="st">'this_is_a_test'</span>,
body<span class="op">=</span><span class="st">'This is a blog'</span>,
posted<span class="op">=</span>datetime.now
)
<span class="va">self</span>.selenium <span class="op">=</span> webdriver.Firefox()
<span class="va">self</span>.selenium.maximize_window()
<span class="bu">super</span>(BlogpostFromHomepageCase, <span class="va">self</span>).setUp()
<span class="kw">def</span> tearDown(<span class="va">self</span>):
<span class="va">self</span>.selenium.quit()
<span class="bu">super</span>(BlogpostFromHomepageCase, <span class="va">self</span>).tearDown()
<span class="kw">def</span> test_visit_blog_post(<span class="va">self</span>):
<span class="va">self</span>.selenium.get(
<span class="st">'</span><span class="sc">%s%s</span><span class="st">'</span> <span class="op">%</span> (<span class="va">self</span>.live_server_url, <span class="st">"/"</span>)
)
<span class="va">self</span>.selenium.find_element_by_link_text(<span class="st">"hello"</span>).click()
<span class="va">self</span>.assertIn(<span class="st">"hello"</span>, <span class="va">self</span>.selenium.title)</code></pre></div>
<p>需要注意的是,如果我们的单元测试如果可以测试到页面的内容——即没有使用JavaScript对页面的内容进行修改,那么我们应该使用单元测试即可。如测试金字塔所说,越底层的测试越快。</p>
<p>在我们编写完这些测试后,我们就可以搭建好相应的持续集成来运行这些测试了。</p>
<h2 id="搭建持续集成">搭建持续集成</h2>
<p>这里我们将使用Jenkins来完成这部分的工作,它是一个用Java编写的开源的持续集成工具。</p>
<blockquote>
<p>它提供了软件开发的持续集成服务。它运行在Servlet容器中(例如Apache Tomcat)。它支持软件配置管理(SCM)工具(包括AccuRev SCM、CVS、Subversion、Git、Perforce、Clearcase和和RTC),可以执行基于Apache Ant和Apache Maven的项目,以及任意的Shell脚本和Windows批处理命令。</p>
</blockquote>
<p>要使用Jenkins,只需要从Jenkins的主页上(<a href="https://jenkins.io/" class="uri">https://jenkins.io/</a>)下载最新的 jenkins.war文件。然后运行</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -jar jenkins.war</code></pre></div>
<p>便可以启动:</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">Running</span> from: /Users/fdhuang/repractise/growth-ci/jenkins.war
<span class="ex">webroot</span>: <span class="va">$user</span>.home/.jenkins
<span class="ex">May</span> 12, 2016 10:55:18 PM org.eclipse.jetty.util.log.JavaUtilLog info
<span class="ex">INFO</span>: Logging initialized @489ms
<span class="ex">May</span> 12, 2016 10:55:18 PM winstone.Logger logInternal
<span class="ex">INFO</span>: Beginning extraction from war file
<span class="ex">May</span> 12, 2016 10:55:20 PM org.eclipse.jetty.util.log.JavaUtilLog warn
<span class="ex">WARNING</span>: Empty contextPath
<span class="ex">May</span> 12, 2016 10:55:20 PM org.eclipse.jetty.util.log.JavaUtilLog info
<span class="ex">INFO</span>: jetty-9.2.z-SNAPSHOT
<span class="ex">May</span> 12, 2016 10:55:20 PM org.eclipse.jetty.util.log.JavaUtilLog info
<span class="ex">INFO</span>: NO JSP Support for /, did not find org.eclipse.jetty.jsp.JettyJspServlet
<span class="ex">Jenkins</span> home directory: /Users/fdhuang/.jenkins found at: <span class="va">$user</span>.home/.jenkins
<span class="ex">May</span> 12, 2016 10:55:21 PM org.eclipse.jetty.util.log.JavaUtilLog info
<span class="ex">INFO</span>: Started w.@68c34b0<span class="dt">{/,file:/Users/fdhuang/.jenkins/war/,AVAILABLE}{/Users/fdhuang/.jenkins/war}</span>
<span class="ex">May</span> 12, 2016 10:55:21 PM org.eclipse.jetty.util.log.JavaUtilLog info
<span class="ex">INFO</span>: Started ServerConnector@733a9ac6<span class="dt">{HTTP/1.1}{0.0.0.0:8080}</span></code></pre></div>
<p>接着,打开<a href="http://0.0.0.0:8080/" class="uri">http://0.0.0.0:8080/</a>就可以进行后续的安装,如下图所示:</p>
<figure>
<img src="./images/jenkins-install.jpg" alt="Jenkins安装过程" /><figcaption>Jenkins安装过程</figcaption>
</figure>
<p>慢慢等其安装完成:</p>
<figure>
<img src="./images/jenkins-getting-started.jpg" alt="Jenkins安装完成" /><figcaption>Jenkins安装完成</figcaption>
</figure>
<p>等安装完成后,我们就可以开始使用Jenkins来创建我们的任务了。</p>
<h3 id="jenkins创建任务">Jenkins创建任务</h3>
<p>在首页,我们会看到“开始创建一个新任务”的提示,点击它。</p>
<p>源码管理中选择Git,并填入我们代码的地址:</p>
<pre><code>[https://github.com/phodal/growth-in-action-python-code](https://github.com/phodal/growth-in-action-python-code)</code></pre>
<p>如下图所示:</p>
<figure>
<img src="./images/jenkins-repo-setup.jpg" alt="Jenkins设计Repo" /><figcaption>Jenkins设计Repo</figcaption>
</figure>
<p>然后就是构建触发器,一共有五种类型的触发器,意思也很容易理解:</p>
<ul>
<li>触发远程构建 (例如,使用脚本)</li>
<li>Build after other projects are built</li>
<li>Build periodically</li>
<li>Build when a change is pushed to GitHub</li>
<li>Poll SCM</li>
</ul>
<p>在这里,我们要使用的是GitHub这个,它的原理是:</p>
<blockquote>
<p>This job will be triggered if jenkins will receive PUSH GitHub hook from repo defined in scm section</p>
</blockquote>
<p>即Jenkins在监听GitHub上对应的PUSH hook,当发生代码提交时,就会运行我们的测试。</p>
<p>由于,我们暂时不需要一些特殊的<code>构建环境</code>配置,我们就可以将这个放空。接着,我们就可以配置<code>构建</code>了。</p>
<h3 id="创建shell">创建shell</h3>
<p>在这里我们需要添加的构建步骤是:<code>execute shell</code>,先让我们写一个简单的安装依赖的shell</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">virtualenv</span> --distribute -p /usr/local/bin/python3.5 growth-django
<span class="bu">source</span> growth-django/bin/activate
<span class="ex">pip</span> install -r requirements.txt</code></pre></div>
<p>然后在保存后,我们可以尝试立即构建这个项目:</p>
<figure>
<img src="./images/build-console-ouput.jpg" alt="控制台输出" /><figcaption>控制台输出</figcaption>
</figure>
<p>在编写shell的过程中,我们要经过一些尝试,在这其中会经历一些失败的情形——即使是大部分有相关经验的程序员。如下图就是一次编写构建脚本引起的构建失败的例子:</p>
<figure>
<img src="./images/jenkins-failure-setup.jpg" alt="Jenkins失败的构建" /><figcaption>Jenkins失败的构建</figcaption>
</figure>
<p>最后,我们就得到下面的一个shell脚本,我们就可以将其变成相应的运行CI的脚本。以便于它可以在其他环境中使用:</p>
<div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="co">#!/usr/bin/env bash</span>
<span class="ex">virtualenv</span> --distribute -p /usr/local/bin/python3.5 growth-django
<span class="bu">source</span> growth-django/bin/activate
<span class="ex">pip</span> install -r requirements.txt
<span class="ex">python</span> manage.py test
<span class="ex">python</span> manage.py test test</code></pre></div>
<p>记得给你的shell文件,加上执行的标志:</p>
<pre><code>chmod u+x ./scripts/ci.sh</code></pre>
<p>最后,我们就可以修改CI上相应的构建环境的配置。</p>
<h1 id="更完善的博客系统">更完善的博客系统</h1>
<p>在Django框架中,内置了很多应用在它的“contrib”包中,这些包括:</p>
<ul>
<li>一个可扩展的认证系统</li>
<li>动态站点管理页面</li>
<li>一组产生RSS和Atom的工具</li>
<li>一个灵活的评论系统</li>
<li>产生Google站点地图(Google Sitemaps)的工具</li>
<li>防止跨站请求伪造(cross-site request forgery)的工具</li>
<li>一套支持轻量级标记语言(Textile和Markdown)的模板库</li>
<li>一套协助创建地理信息系统(GIS)的基础框架</li>
</ul>
<p>这意味着,我们可以直接用Django一些内置的组件来完成很多功能,先让我们来看看怎么完成一个简单的评论功能。</p>
<h2 id="静态页面">静态页面</h2>
<p>Django带有一个可选的“flatpages”应用,可以让我们存储简单的“扁平化(flat)”页面在数据库中,并且可以通过Django的管理界面以及一个Python API来处理要管理的内容。这样的一个静态页面,一般包含下面的几个属性:</p>
<ul>
<li>标题</li>
<li>URL</li>
<li>内容(Content)</li>
<li>Sites</li>
<li>自定义模板(可选)</li>
</ul>
<p>为了使用它来创建静态页面,我们需要在数据库中存储对应的映射关系,并创建对应的静态页面。</p>