index.html 182 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="generator" content="pandoc">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
  7. <title>Growth 实战篇 Django版 – </title>
  8. <style type="text/css">code{white-space: pre;}</style>
  9. <style type="text/css">
  10. div.sourceCode { overflow-x: auto; }
  11. table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode {
  12. margin: 0; padding: 0; vertical-align: baseline; border: none; }
  13. table.sourceCode { width: 100%; line-height: 100%; }
  14. td.lineNumbers { text-align: right; padding-right: 4px; padding-left: 4px; color: #aaaaaa; border-right: 1px solid #aaaaaa; }
  15. td.sourceCode { padding-left: 5px; }
  16. code > span.kw { color: #007020; font-weight: bold; } /* Keyword */
  17. code > span.dt { color: #902000; } /* DataType */
  18. code > span.dv { color: #40a070; } /* DecVal */
  19. code > span.bn { color: #40a070; } /* BaseN */
  20. code > span.fl { color: #40a070; } /* Float */
  21. code > span.ch { color: #4070a0; } /* Char */
  22. code > span.st { color: #4070a0; } /* String */
  23. code > span.co { color: #60a0b0; font-style: italic; } /* Comment */
  24. code > span.ot { color: #007020; } /* Other */
  25. code > span.al { color: #ff0000; font-weight: bold; } /* Alert */
  26. code > span.fu { color: #06287e; } /* Function */
  27. code > span.er { color: #ff0000; font-weight: bold; } /* Error */
  28. code > span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
  29. code > span.cn { color: #880000; } /* Constant */
  30. code > span.sc { color: #4070a0; } /* SpecialChar */
  31. code > span.vs { color: #4070a0; } /* VerbatimString */
  32. code > span.ss { color: #bb6688; } /* SpecialString */
  33. code > span.im { } /* Import */
  34. code > span.va { color: #19177c; } /* Variable */
  35. code > span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
  36. code > span.op { color: #666666; } /* Operator */
  37. code > span.bu { } /* BuiltIn */
  38. code > span.ex { } /* Extension */
  39. code > span.pp { color: #bc7a00; } /* Preprocessor */
  40. code > span.at { color: #7d9029; } /* Attribute */
  41. code > span.do { color: #ba2121; font-style: italic; } /* Documentation */
  42. code > span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
  43. code > span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
  44. code > span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
  45. </style>
  46. <link rel="stylesheet" href="style.css">
  47. <!--[if lt IE 9]>
  48. <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
  49. <![endif]-->
  50. <meta name="viewport" content="width=device-width">
  51. </head>
  52. <body>
  53. <h1>全栈增长工程师实战</h1>
  54. <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>)
  55. </p>
  56. <p><strong>阅读过程中遇到任何问题,请以issue的形式提出来,这样可以帮助其他读者来发现这个问题。</strong></p>
  57. <p>GitHub: <a href="https://github.com/phodal/growth-in-action-django">全栈增长工程师实战</a></p>
  58. <p>PDF、Mobi、Epub版本下载地址:<a href="https://github.com/phodal/growth-in-action/releases">https://github.com/phodal/growth-in-action/releases</a></p>
  59. <p>微信公众号</p>
  60. <p><img src="http://articles.phodal.com/qrcode.jpg" alt=""/></p>
  61. <p>我的其他电子书:</p>
  62. <ul>
  63. <li>《<a href="https://github.com/phodal/growth-ebook">全栈增长工程师指南</a>》</li>
  64. <li>《<a href="https://github.com/phodal/designiot">一步步搭建物联网系统</a>》</li>
  65. <li>《<a href="https://github.com/phodal/github-roam">GitHub 漫游指南</a>》</li>
  66. <li>《<a href="https://github.com/phodal/repractise">RePractise</a>》</li>
  67. </ul>
  68. <div style="width:800px">
  69. <iframe src="http://ghbtns.com/github-btn.html?user=phodal&repo=growth-in-action-django&type=watch&count=true"
  70. allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
  71. </div>
  72. <h2>全栈增长工程师实战目录</h2>
  73. <nav id="TOC">
  74. <ul>
  75. <li><a href="#序如何成为全栈增长工程师">序:如何成为全栈增长工程师?</a></li>
  76. <li><a href="#phodals-idea实战指南">Phodal’s Idea实战指南</a><ul>
  77. <li><a href="#关于作者">关于作者</a></li>
  78. <li><a href="#先成为全栈工程师">先成为全栈工程师</a></li>
  79. <li><a href="#再成为增长工程师">再成为增长工程师</a></li>
  80. </ul></li>
  81. <li><a href="#全栈增长工程师实战">全栈增长工程师实战</a><ul>
  82. <li><a href="#准备工作和工具">准备工作和工具</a></li>
  83. </ul></li>
  84. <li><a href="#深入浅出django">深入浅出Django</a><ul>
  85. <li><a href="#django简介">Django简介</a><ul>
  86. <li><a href="#django应用架构">Django应用架构</a></li>
  87. </ul></li>
  88. <li><a href="#django-helloworld">Django hello,world</a><ul>
  89. <li><a href="#安装django">安装Django</a></li>
  90. <li><a href="#创建项目">创建项目</a></li>
  91. <li><a href="#django后台">Django后台</a></li>
  92. <li><a href="#第一次提交">第一次提交</a></li>
  93. </ul></li>
  94. </ul></li>
  95. <li><a href="#三步创建博客应用">三步创建博客应用</a><ul>
  96. <li><a href="#tasking">Tasking</a></li>
  97. <li><a href="#创建blogpostapp">创建BlogpostAPP</a><ul>
  98. <li><a href="#生成app">生成APP</a></li>
  99. <li><a href="#创建model">创建Model</a></li>
  100. <li><a href="#配置url">配置URL</a></li>
  101. </ul></li>
  102. <li><a href="#创建view">创建View</a><ul>
  103. <li><a href="#创建博客列表页">创建博客列表页</a></li>
  104. <li><a href="#创建博客详情页">创建博客详情页</a></li>
  105. </ul></li>
  106. <li><a href="#测试">测试</a><ul>
  107. <li><a href="#测试首页">测试首页</a></li>
  108. <li><a href="#测试详情页">测试详情页</a></li>
  109. </ul></li>
  110. </ul></li>
  111. <li><a href="#自动化测试与持续集成">自动化测试与持续集成</a><ul>
  112. <li><a href="#编写自动化测试">编写自动化测试</a><ul>
  113. <li><a href="#selenium与第一个ui测试">Selenium与第一个UI测试</a></li>
  114. </ul></li>
  115. <li><a href="#搭建持续集成">搭建持续集成</a><ul>
  116. <li><a href="#jenkins创建任务">Jenkins创建任务</a></li>
  117. <li><a href="#创建shell">创建shell</a></li>
  118. </ul></li>
  119. </ul></li>
  120. <li><a href="#更完善的博客系统">更完善的博客系统</a><ul>
  121. <li><a href="#静态页面">静态页面</a><ul>
  122. <li><a href="#安装-flatpages">安装 flatpages</a></li>
  123. <li><a href="#创建模板">创建模板</a></li>
  124. </ul></li>
  125. <li><a href="#评论功能">评论功能</a></li>
  126. <li><a href="#sitemap">Sitemap</a><ul>
  127. <li><a href="#站点地图介绍">站点地图介绍</a></li>
  128. <li><a href="#创建首页的sitemap">创建首页的Sitemap</a></li>
  129. <li><a href="#创建静态页面的sitemap">创建静态页面的Sitemap</a></li>
  130. <li><a href="#创建博客的sitemap">创建博客的Sitemap</a></li>
  131. <li><a href="#提交到搜索引擎">提交到搜索引擎</a></li>
  132. </ul></li>
  133. </ul></li>
  134. <li><a href="#样式与ui美化">样式与UI美化</a><ul>
  135. <li><a href="#响应式设计">响应式设计</a><ul>
  136. <li><a href="#引入前端框架">引入前端框架</a></li>
  137. </ul></li>
  138. <li><a href="#页面美化">页面美化</a><ul>
  139. <li><a href="#添加导航">添加导航</a></li>
  140. <li><a href="#添加标语">添加标语</a></li>
  141. <li><a href="#优化列表">优化列表</a></li>
  142. <li><a href="#添加footer">添加footer</a></li>
  143. </ul></li>
  144. </ul></li>
  145. <li><a href="#应用api">应用API</a><ul>
  146. <li><a href="#博客列表">博客列表</a><ul>
  147. <li><a href="#django-rest-framework">Django REST Framework</a></li>
  148. <li><a href="#创建博客列表api">创建博客列表API</a></li>
  149. <li><a href="#测试-api">测试 API</a></li>
  150. </ul></li>
  151. <li><a href="#自动完成">自动完成</a><ul>
  152. <li><a href="#搜索api">搜索API</a></li>
  153. <li><a href="#页面实现">页面实现</a></li>
  154. </ul></li>
  155. <li><a href="#跨域支持">跨域支持</a><ul>
  156. <li><a href="#添加跨域支持">添加跨域支持</a></li>
  157. </ul></li>
  158. </ul></li>
  159. <li><a href="#创建移动应用">创建移动应用</a><ul>
  160. <li><a href="#helloworld">hello,world</a><ul>
  161. <li><a href="#构建应用">构建应用</a></li>
  162. </ul></li>
  163. <li><a href="#博客列表页">博客列表页</a><ul>
  164. <li><a href="#列表页">列表页</a></li>
  165. <li><a href="#详情页">详情页</a></li>
  166. </ul></li>
  167. <li><a href="#profile">Profile</a><ul>
  168. <li><a href="#json-web-tokens">Json Web Tokens</a></li>
  169. <li><a href="#登录表单">登录表单</a></li>
  170. <li><a href="#profile-1">Profile</a></li>
  171. </ul></li>
  172. <li><a href="#创建博客">创建博客</a></li>
  173. </ul></li>
  174. <li><a href="#移动单页面应用">移动单页面应用</a><ul>
  175. <li><a href="#移动设备处理">移动设备处理</a></li>
  176. <li><a href="#前后端分离">前后端分离</a><ul>
  177. <li><a href="#riot.js">Riot.js</a></li>
  178. <li><a href="#reactivejs构建服务">ReactiveJS构建服务</a></li>
  179. <li><a href="#创建博客列表页-1">创建博客列表页</a></li>
  180. <li><a href="#博客详情页">博客详情页</a></li>
  181. <li><a href="#添加导航-1">添加导航</a></li>
  182. </ul></li>
  183. </ul></li>
  184. <li><a href="#配置管理">配置管理</a><ul>
  185. <li><a href="#local-settings">local settings</a></li>
  186. </ul></li>
  187. </ul>
  188. </nav>
  189. <h1 id="序如何成为全栈增长工程师">序:如何成为全栈增长工程师?</h1>
  190. <h1 id="phodals-idea实战指南">Phodal’s Idea实战指南</h1>
  191. <h2 id="关于作者">关于作者</h2>
  192. <p>黄峰达(Phodal Huang)是一个创客、工程师、咨询师和作家。他毕业于西安文理学院电子信息工程专业,现作为一个咨询师就职于 ThoughtWorks 深圳。长期活跃于开源软件社区 GitHub,目前专注于物联网和前端领域。</p>
  193. <p>作为一个开源软件作者,著有 Growth、Stepping、Lan、Echoesworks 等软件。其中开源学习应用 Growth,广受读者和用户好评,可在 APP Store 及各大 Android 应用商店下载。</p>
  194. <p>作为一个技术作者,著有《自己动手设计物联网》(电子工业出版社)、《全栈应用开发:精益实践》(电子工业出版社,正在出版)。并在 GitHub 上开源有《Growth: 全栈增长工程师指南》、《GitHub 漫游指南》等七本电子书。</p>
  195. <p>作为技术专家,他为英国 Packt 出版社审阅有物联网书籍《Learning IoT》、《Smart IoT》,前端书籍《Angular 2 Serices》、《Getting started with Angular》等技术书籍。</p>
  196. <p>他热爱编程、写作、设计、旅行、hacking,你可以从他的个人网站:<a href="https://www.phodal.com/" class="uri">https://www.phodal.com/</a> 了解到更多的内容。</p>
  197. <p>其它相关信息:</p>
  198. <ul>
  199. <li>微博:<a href="http://weibo.com/phodal" class="uri">http://weibo.com/phodal</a></li>
  200. <li>GitHub: <a href="https://github.com/phodal" class="uri">https://github.com/phodal</a></li>
  201. <li>知乎:<a href="https://www.zhihu.com/people/phodal" class="uri">https://www.zhihu.com/people/phodal</a></li>
  202. <li>SegmentFault:<a href="https://segmentfault.com/u/phodal" class="uri">https://segmentfault.com/u/phodal</a></li>
  203. </ul>
  204. <p>当前为预览版,在使用的过程中遇到任何问题请及时与我联系。阅读过程中的问题,不妨在GitHub上提出来: <a href="https://github.com/phodal/fe/issues">Issues</a></p>
  205. <p>阅读过程中遇到语法错误、拼写错误、技术错误等等,不妨来个Pull Request,这样可以帮助到其他阅读这本电子书的童鞋。</p>
  206. <p>我的电子书:</p>
  207. <ul>
  208. <li>《<a href="https://github.com/phodal/github-roam">GitHub 漫游指南</a>》</li>
  209. <li>《<a href="https://github.com/phodal/fe">我的职业是前端工程师</a>》</li>
  210. <li>《<a href="https://github.com/phodal/serverless">Serverless 架构应用开发指南</a>》</li>
  211. <li>《<a href="https://github.com/phodal/growth-ebook">Growth: 全栈增长工程师指南</a>》</li>
  212. <li>《<a href="https://github.com/phodal/ideabook">Phodal’s Idea实战指南</a>》</li>
  213. <li>《<a href="https://github.com/phodal/designiot">一步步搭建物联网系统</a>》</li>
  214. <li>《<a href="https://github.com/phodal/repractise">RePractise</a>》</li>
  215. <li>《<a href="https://github.com/phodal/growth-in-action">Growth: 全栈增长工程师实战</a>》</li>
  216. </ul>
  217. <p>我的微信公众号:</p>
  218. <figure>
  219. <img src="./images/wechat.jpg" alt="作者微信公众号:phodal-weixin" /><figcaption>作者微信公众号:phodal-weixin</figcaption>
  220. </figure>
  221. <p>支持作者,可以加入作者的小密圈:</p>
  222. <figure>
  223. <img src="./images/xiaomiquan.jpg" alt="小密圈" /><figcaption>小密圈</figcaption>
  224. </figure>
  225. <p>或者转账:</p>
  226. <p><img src="./images/alipay.png" alt="支付宝" /> <img src="./images/wechat-pay.png" alt="微信" /></p>
  227. <p>记得我们在《<a href="http://mp.weixin.qq.com/s?src=3&amp;timestamp=1463835081&amp;ver=1&amp;signature=z1onJvKn4TSrUmXm384CQUF1IZBVsLShsQ4DpmumN6xY0Gm5RR9XKdbf6ELzdRqg-mxdtxceTg-4-KrhYHZQC6wiSEWsP64vh0sl2Je4G16hnS6MsuZaD-u01HAENCSKoMhQiw0tu2y3-tSJsOML0w==">RePractise前端篇: 前端演进史</a>》中提到技术在最近十几年的飞速发展,当然最主要的就是:技术的复杂度不断地从应用层抽象到了框架层。虽说:</p>
  228. <blockquote>
  229. <p>技术的复杂度同力一样不会消失,也不会凭空产生,它总是从一个物体转移到另一个物体或一种形式转为另一种形式。</p>
  230. </blockquote>
  231. <p>然而这也意味着成为一个全栈工程师,比以往的任何一个时间要容易得多。这也意味着一个全栈工程师也可以很快地成为一个Growth Hacking(中文:增长黑客)。所以,我们开始谈论如何成为一名<code>全栈增长工程师</code>。</p>
  232. <h2 id="先成为全栈工程师">先成为全栈工程师</h2>
  233. <p>在电子书《<a href="http://mp.weixin.qq.com/s?src=3&amp;timestamp=1463835463&amp;ver=1&amp;signature=z1onJvKn4TSrUmXm384CQUF1IZBVsLShsQ4DpmumN6xzPP-WG-vZxJgzeXdGcPSFn9Erm6laV3FgnEMuiqMnHP0TadjpLl4tYHPhFr-yKWi35U*tGi-RKIdwGc2ylN9bA2Ph*KAl5w5CJRlw2LI9*g==">全栈增长工程师指南</a>》中,我们提到过成为全栈增长工程师的技术基础,但是没有并没有谈论到如何成为这样的全栈工程师——这是一个漫长的过程。</p>
  234. <p>早期,当我们有一个想法的时候,我们会去搭建一个网站——如以WordPress作为CMS,以RoR、Django来开发应用等等。随后,我们将我们的网站推向市场,发现市场有点反应。</p>
  235. <p>接着,我们不断地开发出一些新的功能——如CMS的留言、Sitemap等等。在这个过程中,我们会开发一些API来满足我们的需求。</p>
  236. <p>在一个新的阶段里,我们开始推出移动应用。基于先前的API,我们不断地构建出了不同的API。或以单体应用的形式出现,或以微服务的形式产生出新的API。</p>
  237. <p>然后,我们发现并不是所有的移动用户都愿意去下载我们的API。于是,我们推出了SPA(单页面应用),以此来迎接那些移动设备用户。</p>
  238. <p>最后,我们的业务逐渐稳定了下来。我们开始了一些优化工作,或者如Facebook一样优化PHP,推出HHVM。或者如Netflix一样使用微服务解耦系统。又或者,我们使用新的架构对我们的系统进行重新的设计。</p>
  239. <p>在整个过程中,我们将学习到如何去做网站后台、移动应用、API设计、前端单页面应用等等。从这种意义上来说,全栈工程师非常match初创企业所需要的技术要求。</p>
  240. <h2 id="再成为增长工程师">再成为增长工程师</h2>
  241. <p>Growth整一个系列:APP、社区、电子书《全栈增长工程师指南》、电子书《全栈增长工程师实战》算是我对Growth Hacking的一个研究。不过,对于一个人来说这工作量还是蛮大的——在完成两本电子书后,我们将继续研究。在这一个过程中,我发现一些很有意思的东西——只有开发出用户想要的东西,这个过程才容易实践起来的。</p>
  242. <p>增长可以分为两部分:一个是自身的增长,一个是用户的增长。两者实际上是一种相互促进的关系,当我们的能力增长到一定的程度,我们才能推进用户的增长。相用户增长到一定的程度,也会推进我们的技能增长。</p>
  243. <p>只是要在技术、数据分析、用户分析、创新等等有所突破,看上去好像不是一件容易的事。只是对于大部分的全栈工程师来说,实现技术、数据抓取和分析是一件容易的事。要实现对数据的敏感是一种很难的事,但是可视化过后的数据就一样了。对于用户的行为分析也是类似的,只是因为我们缺乏一些有效的练习。</p>
  244. <p>更让人惊讶的是创新也是可以练习的,每次我们遇到一个问题的时候,就是我们离创新最近的时候——难道不是吗?当你遇到一个难解的问题,就是你开拓一个新的能力的时候。</p>
  245. <p>好好享受这个学习的过程吧!</p>
  246. <h1 id="全栈增长工程师实战">全栈增长工程师实战</h1>
  247. <h2 id="准备工作和工具">准备工作和工具</h2>
  248. <p>在开始写代码之前你需要保证你有一些Python基础,如果没有的话,请参阅其他相关书籍来一起学习。</p>
  249. <p>并且你还需要在你的计算机上安装:</p>
  250. <ul>
  251. <li>Python环境及其包管理工具pip。</li>
  252. <li>Firefox浏览器——用于运行功能测试。</li>
  253. <li>Git版本控制器——用于代码版本控制。</li>
  254. <li>一个开发工具。(PS: 在这里笔者使用的是PyCharm的社区版)</li>
  255. </ul>
  256. <h1 id="深入浅出django">深入浅出Django</h1>
  257. <h2 id="django简介">Django简介</h2>
  258. <p>Django是一个高级的Python Web开发框架,它的目标是使得开发复杂的、数据库驱动的网站变得更加简单。</p>
  259. <p>由于Django最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的。所以,我们可以发现在使用Django的很多网站里,都是用于作为CMS(内容管理系统)来使用的。使用Django的一些比较知名的网站如下图所示:</p>
  260. <figure>
  261. <img src="./images/who-use-django.jpg" alt="使用Django的网站" /><figcaption>使用Django的网站</figcaption>
  262. </figure>
  263. <p>Django是一个MTV框架,其架构模板看上去与传统的MVC架构并没有太大的区别。其对比如下表所示:</p>
  264. <table>
  265. <thead>
  266. <tr class="header">
  267. <th>传统的MVC架构</th>
  268. <th>Django 架构</th>
  269. </tr>
  270. </thead>
  271. <tbody>
  272. <tr class="odd">
  273. <td>Model</td>
  274. <td>Model(Data Access Logic)</td>
  275. </tr>
  276. <tr class="even">
  277. <td>View</td>
  278. <td>Template(Presentation Logic)</td>
  279. </tr>
  280. <tr class="odd">
  281. <td>View</td>
  282. <td>View(Business Logic)</td>
  283. </tr>
  284. <tr class="even">
  285. <td>Controller</td>
  286. <td>Django itself</td>
  287. </tr>
  288. </tbody>
  289. </table>
  290. <p>在Django中View只用来描述你要看到的内容,Template才是最后用于显示的内容。而在MVC架构中,这只相当于是View层。它的核心包含下面的四部分:</p>
  291. <ul>
  292. <li>一个 对象关系映射,作为数据模型和关系性数据库间的媒介(Model层);</li>
  293. <li>一个基于正则表达式的URL分发器(即MVC中的Controller);</li>
  294. <li>一个用于处理HTTP请求的系统,含web模板系统(View层);</li>
  295. </ul>
  296. <p>其核心框架还包含:</p>
  297. <ul>
  298. <li>一个轻量级的、独立的Web服务器,只用于开发和测试。</li>
  299. <li>一个表单序列化及验证系统,用于将HTML表单转换成适用于数据库存储的数据。</li>
  300. <li>一个缓存框架,并且可以从几种缓存方式中选择。</li>
  301. <li>中间件支持,能对请求处理的各个阶段进行处理。</li>
  302. <li>内置的分发系统允许应用程序中的组件采用预定义的信号进行相互间的通信。</li>
  303. <li>一个序列化系统,能够生成或读取采用XML或JSON表示的Django模型实例。</li>
  304. <li>一个用于扩展模板引擎的能力的系统。</li>
  305. </ul>
  306. <h3 id="django应用架构">Django应用架构</h3>
  307. <p>Django的每一个模块在内部都称之为APP,在每个APP里都有自己的三层结构。如下图所示:</p>
  308. <figure>
  309. <img src="./images/django_app_arch.jpg" alt="Django 应用架构" /><figcaption>Django 应用架构</figcaption>
  310. </figure>
  311. <p>这样做不仅可以在开发的时候更容易理解系统,而且可以提高代码的可复用性——因为每一个APP都是独立的应用,在下次使用时我们只需要简单的复制和粘贴。</p>
  312. <p>说了这么多,还不如从一个hello,world开始。</p>
  313. <h2 id="django-helloworld">Django hello,world</h2>
  314. <h3 id="安装django">安装Django</h3>
  315. <p>安装Django之前,我们可以用virtualenv工具来创建一个虚拟的Python运行环境。环境问题是一个很复杂的问题,在我们使用Python的过程中,我们会不断地安装一些库,而这些库可能会有不同的版本。并且在安装Python库的过程中,我们会遇到权限问题——即我们需要超级用户的权限才能将库安装到系统的环境之下。随后在这个软件的生涯中,我们还需要保证这个项目所依赖的模块不会发生变动。而这些都是很棘手的一些事,这时候我们就需要创建一个虚拟的运行环境,而virtualenv就是这样的一个工具。</p>
  316. <h4 id="virtualenv">virtualenv</h4>
  317. <p>安装Python包我们需要用到pip命令,它是Python语言中的一个包管理工具。如果你没有安装的话,可以使用下面的命令来安装:</p>
  318. <pre><code>curl https://bootstrap.pypa.io/get-pip.py | python</code></pre>
  319. <p>在不同的Python环境中,我们可能需要使用不同的pip,如下所示是笔者使用的Python3的pip命令pip3</p>
  320. <pre><code>$ pip3 install virtualenv</code></pre>
  321. <p>如果是Python2.7的话,对应会有:</p>
  322. <pre><code>$ pip install virtualenv</code></pre>
  323. <p>需要注意的是这将会安装到Python所在的目录,如我的目录是:</p>
  324. <pre><code>$ /usr/local/bin/virtualenv</code></pre>
  325. <p>有的可能会是:</p>
  326. <pre><code>$ /usr/local/share/python3/virtualenv</code></pre>
  327. <p>在创建我们的这个虚拟环境之前,我们可以创建一个存储所有virtualenv的目录:</p>
  328. <pre><code>$ mkdir somewhere/virtualenvs</code></pre>
  329. <p>现在,我们就可以创建一个新的虚拟环境:</p>
  330. <pre><code>$ virtualenv somewhere/virtualenvs/&lt;project-name&gt; --no-site-packages</code></pre>
  331. <p>如果你想使用不同的Python版本的话,那么需要指定Python版本的路径</p>
  332. <pre><code>$ virtualenv --distribute -p /usr/local/bin/python3.3 somewhere/virtualenvs/&lt;project-name&gt;</code></pre>
  333. <p>通过到相应的目录下执行激活就可以使用这个虚拟环境了:</p>
  334. <pre><code>$ cd somewhere/virtualenvs/&lt;project-name&gt;/bin
  335. $ source activate</code></pre>
  336. <p>停止使用只需要执行下面的命令即可:</p>
  337. <pre><code>$ deactivate</code></pre>
  338. <h4 id="安装django-1">安装Django</h4>
  339. <p>准备了这么久我们终于要开始安装Django了,执行:</p>
  340. <pre><code>$ pip install django</code></pre>
  341. <p>开始下最新版本的Django,如下所示:</p>
  342. <pre><code>Collecting django
  343. Downloading Django-1.9.4-py2.py3-none-any.whl (6.6MB)
  344. 94% |██████████████████████████████▎ | 6.2MB 251kB/s eta 0:00:02</code></pre>
  345. <p>等下载完后,就会开始安装Django。安装完后,我们就可以使用Django自带的django-admin命令。django-admin是Django自带的一个管理任务的命令行工具。</p>
  346. <p>通过这个命令,我们不仅仅可以用它来创建项目、创建app、运行服务、数据库迁移,还可以执行各种SQL工具等等。django-admin用法如下:</p>
  347. <pre><code>$ django-admin &lt;command&gt; [options]</code></pre>
  348. <p>下面是django-admin自带的一些命令:</p>
  349. <pre><code>[django]
  350. check
  351. compilemessages
  352. createcachetable
  353. dbshell
  354. diffsettings
  355. dumpdata
  356. flush
  357. inspectdb
  358. loaddata
  359. makemessages
  360. makemigrations
  361. migrate
  362. runfcgi
  363. runserver
  364. shell
  365. sql
  366. sqlall
  367. sqlclear
  368. sqlcustom
  369. sqldropindexes
  370. sqlflush
  371. sqlindexes
  372. sqlinitialdata
  373. sqlmigrate
  374. sqlsequencereset
  375. squashmigrations
  376. startapp
  377. startproject
  378. syncdb
  379. test
  380. testserver
  381. validate</code></pre>
  382. <p>现在,让我们来看看这个强大的工具。</p>
  383. <h3 id="创建项目">创建项目</h3>
  384. <p>在这些命令中startproject可以用于创建项目,在这里我们的项目名是blog,那么我们的命令如下:</p>
  385. <p>$ django-admin startproject blog</p>
  386. <p>这个命令将创建下面的文件内容,而这些是Django项目的一些必须文件。</p>
  387. <pre><code>.
  388. ├── blog
  389. │   ├── __init__.py
  390. │   ├── settings.py
  391. │   ├── urls.py
  392. │   └── wsgi.py
  393. └── manage.py</code></pre>
  394. <p>blog目录对应的就是blog这个项目,将会放置这个项目的一些相关配置:</p>
  395. <ol type="1">
  396. <li>settings.py包含了这个项目的相关配置。如数据库环境、启用的插件等等。</li>
  397. <li>urls.py即URL Dispatcher的配置,指明了某个URL应该指向某个函数来处理。</li>
  398. <li>wsgi.py用于部署。WSGI(Python Web Server Gateway Interface,Web服务器网关接口)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。</li>
  399. <li>__init__.py指明了这是一个Python模块。</li>
  400. </ol>
  401. <p>manage.py 会在每个Django项目中自动生成,它可以和django-admin做类似的事。如我们可以用manage.py来启动测试环境的服务器:</p>
  402. <p>$ python manage.py runserver</p>
  403. <pre><code>Performing system checks...
  404. System check identified no issues (0 silenced).
  405. You have unapplied migrations; your app may not work properly until they are applied.
  406. Run &#39;python manage.py migrate&#39; to apply them.
  407. March 24, 2016 - 03:07:34
  408. Django version 1.9.4, using settings &#39;blog.settings&#39;
  409. Starting development server at http://127.0.0.1:8000/
  410. Quit the server with CONTROL-C.
  411. Not Found: /
  412. [24/Mar/2016 03:07:35] &quot;GET / HTTP/1.1&quot; 200 1767
  413. Not Found: /favicon.ico
  414. [24/Mar/2016 03:07:36] &quot;GET /favicon.ico HTTP/1.1&quot; 404 1934</code></pre>
  415. <p>现在,我们只需要在浏览器中打开<a href="http://127.0.0.1:8000/" class="uri">http://127.0.0.1:8000/</a>,便可以访问我们的应用程序。</p>
  416. <h3 id="django后台">Django后台</h3>
  417. <p>Django很适合CMS的另外一个原因,就是它自带了一个后台管理系统。为了启用这个后台管理系统,我们需要配置我们的数据库,并创建相应的超级用户。如下所示的是settings.py中的默认数据库配置:</p>
  418. <pre><code># Database
  419. # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
  420. DATABASES = {
  421. &#39;default&#39;: {
  422. &#39;ENGINE&#39;: &#39;django.db.backends.sqlite3&#39;,
  423. &#39;NAME&#39;: os.path.join(BASE_DIR, &#39;db.sqlite3&#39;),
  424. }
  425. }</code></pre>
  426. <p>上面的配置中我们使用的是SQLite3作为数据库,并使用了当前目录下的<code>db.sqlite3</code>作为数据库文件。Django内建支持下面的一些数据库:</p>
  427. <pre><code>&#39;django.db.backends.postgresql_psycopg2&#39;
  428. &#39;django.db.backends.mysql&#39;
  429. &#39;django.db.backends.sqlite3&#39;
  430. &#39;django.db.backends.oracle&#39;</code></pre>
  431. <p>如果我们想使用别的数据库,可以在网上寻找相应的解决方案,如用于支持使用MongoDB的django-nonrel项目。不同的数据库有不同的配置,如下所示的是使用PostgreSQL的配置。</p>
  432. <pre><code>DATABASES = {
  433. &#39;default&#39;: {
  434. &#39;ENGINE&#39;: &#39;django.db.backends.postgresql_psycopg2&#39;,
  435. &#39;NAME&#39;: &#39;mydatabase&#39;,
  436. &#39;USER&#39;: &#39;mydatabaseuser&#39;,
  437. &#39;PASSWORD&#39;: &#39;mypassword&#39;,
  438. &#39;HOST&#39;: &#39;127.0.0.1&#39;,
  439. &#39;PORT&#39;: &#39;5432&#39;,
  440. }
  441. }</code></pre>
  442. <p>接着,我们就可以运行数据库迁移,只需要运行相应的脚本即可:</p>
  443. <p>$ python manage.py migrate</p>
  444. <pre><code>Operations to perform:
  445. Apply all migrations: sessions, admin, auth, contenttypes
  446. Running migrations:
  447. Rendering model states... DONE
  448. Applying contenttypes.0001_initial... OK
  449. Applying auth.0001_initial... OK
  450. Applying admin.0001_initial... OK
  451. Applying admin.0002_logentry_remove_auto_add... OK
  452. Applying contenttypes.0002_remove_content_type_name... OK
  453. Applying auth.0002_alter_permission_name_max_length... OK
  454. Applying auth.0003_alter_user_email_max_length... OK
  455. Applying auth.0004_alter_user_username_opts... OK
  456. Applying auth.0005_alter_user_last_login_null... OK
  457. Applying auth.0006_require_contenttypes_0002... OK
  458. Applying auth.0007_alter_validators_add_error_messages... OK
  459. Applying sessions.0001_initial... OK
  460. (growth-django)</code></pre>
  461. <p>在上面的过程中,我们会创建相应的数据库模型,并依据迁移脚本来创建一些相应的数据,如默认的配置等等。</p>
  462. <p>最后,我们可以创建一个相应的超级用户来登陆后台。</p>
  463. <p>$ python manage.py createsuperuser</p>
  464. <pre><code>Username (leave blank to use &#39;fdhuang&#39;): root
  465. Email address: h@phodal.com
  466. Password:
  467. Password (again):
  468. Superuser created successfully.</code></pre>
  469. <p>输入相应的用户名和密码,即可完成创建。然后访问 <a href="http://127.0.0.1:8000/admin" class="uri">http://127.0.0.1:8000/admin</a>,输入上面的用户名和密码就可以来到后台:</p>
  470. <figure>
  471. <img src="./images/django-backend.jpg" alt="Django后台" /><figcaption>Django后台</figcaption>
  472. </figure>
  473. <h3 id="第一次提交">第一次提交</h3>
  474. <p>在创建完应用后,我们就可以进行第一次提交,通常初次提交的提交信息(commit message)是<code>init project</code>。如果在那之前,你没有执行<code>git init</code>来初始化git的话,那么我们就需要去执行这个命令。</p>
  475. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> init</code></pre></div>
  476. <p>它将返回类似于下面的结果</p>
  477. <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>
  478. <p>即初始化了一个空的Git项目,然后我们就可以执行<code>add</code>来添加上面的内容:</p>
  479. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> add .</code></pre></div>
  480. <p>需要注意的是上面的数据库文件不应该添加到项目里,所以我们应该执行reset命令来重置这个状态:</p>
  481. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> reset db.sqlite3</code></pre></div>
  482. <p>这时我们会将其变成下面的状态:</p>
  483. <figure>
  484. <img src="./images/first-commit.png" alt="第一次提交前的reset" /><figcaption>第一次提交前的reset</figcaption>
  485. </figure>
  486. <p>上面的绿色文件代表这几个文件都被添加了进去,蓝色则代表未添加的文件。为了避免手误产生一些问题,我们需要添加一个名为<code>.gitignore</code>文件用于将一些文件名加入忽略名单,如下是常用的python项目的<code>.gitignore</code>文件中的内容:</p>
  487. <pre><code>*.pyc
  488. *.db
  489. *.sqlite3</code></pre>
  490. <p>当我们添加完这个文件,git就会识别这个文件,并忽略原来的那些文件,如下图所示:</p>
  491. <figure>
  492. <img src="./images/git-ignore.png" alt="添加完gitignore文件后的效果" /><figcaption>添加完gitignore文件后的效果</figcaption>
  493. </figure>
  494. <p>我们只需要添加这个文件即可:</p>
  495. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> add .gitignore</code></pre></div>
  496. <p>如果你之前已经不小心添加了一些不应该添加的文件,那么可以执行下面的命令来重置其状态:</p>
  497. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">git</span> reset .</code></pre></div>
  498. <p>然后再执行添加命令。</p>
  499. <p>最后,我们就可以在本地提交我们的代码了:</p>
  500. <pre><code>git commit -m &quot;init project&quot;</code></pre>
  501. <p>如果你是将代码托管在GitHub上的话,那么你就可以执行<code>git push</code>来将代码提交到服务器上。</p>
  502. <h1 id="三步创建博客应用">三步创建博客应用</h1>
  503. <h2 id="tasking">Tasking</h2>
  504. <p>在我们不了解Django的时候,要对这样一个任务进行Tasking,有点困难。不过,我们还是可以简单地看看是应该如何去做:</p>
  505. <ul>
  506. <li>生成APP。对于大部分主流的Web框架来说,它们都可以手动地生成一些脚手架,如Ruby语言中的Ruby On Rails、Node.js中的Express等等。</li>
  507. <li>创建对应的Model,即其在数据库中存储的模型与我们在代码中要使用的模型。</li>
  508. <li>创建程序对应的View,用于处理数据。</li>
  509. <li>创建程序的Template,用于显示数据。</li>
  510. <li>编写测试来保证功能。</li>
  511. </ul>
  512. <p>对于其他应用来说也是差不多的。</p>
  513. <h2 id="创建blogpostapp">创建BlogpostAPP</h2>
  514. <h3 id="生成app">生成APP</h3>
  515. <p>现在我们可以开始创建我们的APP,使用下面的代码来创建:</p>
  516. <p>$ django-admin startapp blogpost</p>
  517. <p>会在blogpost目录下,生成下面的文件:</p>
  518. <pre><code>.
  519. ├── __init__.py
  520. ├── admin.py
  521. ├── apps.py
  522. ├── migrations
  523. │   └── __init__.py
  524. ├── models.py
  525. ├── tests.py
  526. └── views.py</code></pre>
  527. <h3 id="创建model">创建Model</h3>
  528. <p>现在,我们需要来创建博客的Model即可。对于一篇基本的博客来说,它会包含下面的几部分内容:</p>
  529. <ul>
  530. <li>标题</li>
  531. <li>作者</li>
  532. <li>链接(中文更需要一个好的链接)</li>
  533. <li>内容</li>
  534. <li>发布日期</li>
  535. </ul>
  536. <p>我们就可以按照上面的内容来创建我们的Blogpost model:</p>
  537. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.db <span class="im">import</span> models
  538. <span class="im">from</span> django.db.models <span class="im">import</span> permalink
  539. <span class="kw">class</span> Blogpost(models.Model):
  540. 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>)
  541. 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>)
  542. 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>)
  543. body <span class="op">=</span> models.TextField()
  544. 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>)
  545. <span class="kw">def</span> <span class="fu">__unicode__</span>(<span class="va">self</span>):
  546. <span class="cf">return</span> <span class="st">&#39;</span><span class="sc">%s</span><span class="st">&#39;</span> <span class="op">%</span> <span class="va">self</span>.title
  547. <span class="at">@permalink</span>
  548. <span class="kw">def</span> get_absolute_url(<span class="va">self</span>):
  549. <span class="cf">return</span> (<span class="st">&#39;view_blog_post&#39;</span>, <span class="va">None</span>, { <span class="st">&#39;slug&#39;</span>: <span class="va">self</span>.slug })</code></pre></div>
  550. <p>上面的<code>get_absolute_url</code>方法就是用于返回博客的链接。之所以使用手动而不是自动生成,是因为自动生成不靠谱,而且不利</p>
  551. <p>然后在Admin注册这个Model</p>
  552. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.contrib <span class="im">import</span> admin
  553. <span class="im">from</span> blogpost.models <span class="im">import</span> Blogpost
  554. <span class="kw">class</span> BlogpostAdmin(admin.ModelAdmin):
  555. exclude <span class="op">=</span> [<span class="st">&#39;posted&#39;</span>]
  556. prepopulated_fields <span class="op">=</span> {<span class="st">&#39;slug&#39;</span>: (<span class="st">&#39;title&#39;</span>,)}
  557. admin.site.register(Blogpost, BlogpostAdmin)</code></pre></div>
  558. <p>接着我们需要先将<code>blogpost</code>这个APP添加到配置文件<code>blog/blog/settings.py</code>的<code>INSTALLED_APPS</code>字段中:</p>
  559. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">INSTALLED_APPS <span class="op">=</span> [
  560. <span class="st">&#39;blogpost.apps.BlogpostConfig&#39;</span>,
  561. <span class="st">&#39;django.contrib.admin&#39;</span>,
  562. ...
  563. ]</code></pre></div>
  564. <p>然后做数据库迁移:</p>
  565. <pre class="shelln"><code>python manage.py migrate</code></pre>
  566. <p>这时会提示:</p>
  567. <pre class="shell"><code>Operations to perform:
  568. Apply all migrations: admin, contenttypes, auth, sessions
  569. Running migrations:
  570. No migrations to apply.
  571. Your models have changes that are not yet reflected in a migration, and so won&#39;t be applied.
  572. Run &#39;manage.py makemigrations&#39; to make new migrations, and then re-run &#39;manage.py migrate&#39; to apply them.</code></pre>
  573. <p>是因为我们忘记了先运行</p>
  574. <pre class="shell"><code>python manage.py makemigrations</code></pre>
  575. <p>进入后台,我们就可以看到BLOGPOST的一栏里,就可以对其进行相关的操作。</p>
  576. <figure>
  577. <img src="./images/django-admin-ui.png" alt="Django后台界面" /><figcaption>Django后台界面</figcaption>
  578. </figure>
  579. <p>点击Blogpost的Add后,我们就会进入如下的添加博客界面:</p>
  580. <figure>
  581. <img src="./images/admin-blog.png" alt="Django添加博客" /><figcaption>Django添加博客</figcaption>
  582. </figure>
  583. <p>实际上,这样做的意义是将删除(Delete)、修改(Update)、添加(Create)这些内容交给用户后台来做,当然它也不需要在View/Template层来做。在我们的Template层中,我们只需要关心如何来显示这些数据。</p>
  584. <p>现在,我们可以执行一次新的代码提交——因为现在的代码可以正常工作。这样出现问题时,我们就可以即时的返回上一版本的代码。</p>
  585. <pre><code>git add .
  586. git commit -m &quot;create blogpost model&quot;</code></pre>
  587. <p>然后再进行下一步地操作。</p>
  588. <h3 id="配置url">配置URL</h3>
  589. <p>现在,我们就可以在我们的<code>urls.py</code>里添加相应的route来访问页面,代码如下所示:</p>
  590. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.conf <span class="im">import</span> settings
  591. <span class="im">from</span> django.conf.urls <span class="im">import</span> include, url
  592. <span class="im">from</span> django.conf.urls.static <span class="im">import</span> static
  593. <span class="im">from</span> django.contrib <span class="im">import</span> admin
  594. urlpatterns <span class="op">=</span> [
  595. (<span class="vs">r&#39;^$&#39;</span>, <span class="st">&#39;blogpost.views.index&#39;</span>),
  596. url(<span class="vs">r&#39;^blog/(?P&lt;slug&gt;[^\.]+).html&#39;</span>, <span class="st">&#39;blogpost.views.view_post&#39;</span>, name<span class="op">=</span><span class="st">&#39;view_blog_post&#39;</span>),
  597. url(<span class="vs">r&#39;^admin/&#39;</span>, include(admin.site.urls))
  598. ]</code></pre></div>
  599. <p>在上面的代码里,我们创建了两个route:</p>
  600. <ul>
  601. <li>指向首页,其view是index</li>
  602. <li>指向博客详情页,其view是view_post</li>
  603. </ul>
  604. <p>指向博客详情页的URL正则<code>r'^blog/(?P&lt;slug&gt;[^\.]+).html</code>,会将形如blog/hello-world.html中的hello-world提取出来作为参数传给view_post方法。</p>
  605. <p>接着,我们就可以创建两个view。</p>
  606. <h2 id="创建view">创建View</h2>
  607. <h3 id="创建博客列表页">创建博客列表页</h3>
  608. <p>对于我们的首页来说,我们可以简单的只显示五篇博客,所以我们所需要做的就是从我们的Blogpost对象中,取出前五个结果即可。代码如下所示:</p>
  609. <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
  610. <span class="im">from</span> blogpost.models <span class="im">import</span> Blogpost
  611. <span class="kw">def</span> index(request):
  612. <span class="cf">return</span> render_to_response(<span class="st">&#39;index.html&#39;</span>, {
  613. <span class="st">&#39;posts&#39;</span>: Blogpost.objects.<span class="bu">all</span>()[:<span class="dv">5</span>]
  614. })</code></pre></div>
  615. <p>Django的render_to_response方法可以根据一个给定的上下文字典渲染一个给定的目标,并返回渲染后的HttpResponse。即将相应的值,如这里的Blogpost.objects.all()[:5],填入相应的index.html中,再返回最后的结果。</p>
  616. <p>首先,我们需要创建一个templates文件夹,然后在setting.py的TEMPLATES字段将该目录指定为默认目录</p>
  617. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"> TEMPLATES <span class="op">=</span> [
  618. {
  619. <span class="st">&#39;BACKEND&#39;</span>: <span class="st">&#39;django.template.backends.django.DjangoTemplates&#39;</span>,
  620. <span class="st">&#39;DIRS&#39;</span>: [<span class="st">&#39;templates/&#39;</span>],
  621. <span class="st">&#39;APP_DIRS&#39;</span>: <span class="va">True</span>,
  622. <span class="st">&#39;OPTIONS&#39;</span>: {
  623. <span class="st">&#39;context_processors&#39;</span>: [
  624. <span class="st">&#39;django.template.context_processors.debug&#39;</span>,
  625. <span class="st">&#39;django.template.context_processors.request&#39;</span>,
  626. <span class="st">&#39;django.contrib.auth.context_processors.auth&#39;</span>,
  627. <span class="st">&#39;django.contrib.messages.context_processors.messages&#39;</span>,
  628. ],
  629. },
  630. },
  631. ]</code></pre></div>
  632. <p>另外,在templates目录下我们需要新建base.html, index.html和blogpost_detail.html三个模板。</p>
  633. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html">{% load staticfiles %}
  634. <span class="kw">&lt;html&gt;</span>
  635. <span class="kw">&lt;head&gt;</span>
  636. <span class="kw">&lt;meta</span><span class="ot"> charset=</span><span class="st">&quot;utf-8&quot;</span><span class="kw">&gt;</span>
  637. <span class="kw">&lt;meta</span><span class="ot"> http-equiv=</span><span class="st">&quot;X-UA-Compatible&quot;</span><span class="ot"> content=</span><span class="st">&quot;IE=edge&quot;</span><span class="kw">&gt;</span>
  638. <span class="kw">&lt;meta</span><span class="ot"> name=</span><span class="st">&quot;viewport&quot;</span><span class="ot"> content=</span><span class="st">&quot;width=device-width, initial-scale=1&quot;</span><span class="kw">&gt;</span>
  639. <span class="kw">&lt;title&gt;</span>{% block head_title %}Welcome to my blog{% endblock %}<span class="kw">&lt;/title&gt;</span>
  640. <span class="kw">&lt;link</span><span class="ot"> rel=</span><span class="st">&quot;stylesheet&quot;</span><span class="ot"> type=</span><span class="st">&quot;text/css&quot;</span><span class="ot"> href=</span><span class="st">&quot;{% static &#39;css/bootstrap.min.css&#39; %}&quot;</span><span class="kw">&gt;</span>
  641. <span class="kw">&lt;link</span><span class="ot"> rel=</span><span class="st">&quot;stylesheet&quot;</span><span class="ot"> type=</span><span class="st">&quot;text/css&quot;</span><span class="ot"> href=</span><span class="st">&quot;{% static &#39;css/styles.css&#39; %}&quot;</span><span class="kw">&gt;</span>
  642. <span class="kw">&lt;/head&gt;</span>
  643. <span class="kw">&lt;body</span><span class="ot"> data-twttr-rendered=</span><span class="st">&quot;true&quot;</span><span class="ot"> class=</span><span class="st">&quot;bs-docs-home&quot;</span><span class="kw">&gt;</span>
  644. <span class="kw">&lt;header</span><span class="ot"> class=</span><span class="st">&quot;navbar navbar-static-top bs-docs-nav&quot;</span><span class="ot"> id=</span><span class="st">&quot;top&quot;</span><span class="ot"> role=</span><span class="st">&quot;banner&quot;</span><span class="kw">&gt;</span>
  645. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;container&quot;</span><span class="kw">&gt;</span>
  646. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;navbar-header&quot;</span><span class="kw">&gt;</span>
  647. <span class="kw">&lt;button</span><span class="ot"> class=</span><span class="st">&quot;navbar-toggle collapsed&quot;</span><span class="ot"> type=</span><span class="st">&quot;button&quot;</span><span class="ot"> data-toggle=</span><span class="st">&quot;collapse&quot;</span>
  648. <span class="ot"> data-target=</span><span class="st">&quot;.bs-navbar-collapse&quot;</span><span class="kw">&gt;</span>
  649. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;sr-only&quot;</span><span class="kw">&gt;</span>切换视图<span class="kw">&lt;/span&gt;</span>
  650. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;icon-bar&quot;</span><span class="kw">&gt;&lt;/span&gt;</span>
  651. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;icon-bar&quot;</span><span class="kw">&gt;&lt;/span&gt;</span>
  652. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;icon-bar&quot;</span><span class="kw">&gt;&lt;/span&gt;</span>
  653. <span class="kw">&lt;/button&gt;</span>
  654. <span class="kw">&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/&quot;</span><span class="ot"> class=</span><span class="st">&quot;navbar-brand&quot;</span><span class="kw">&gt;</span>Growth博客<span class="kw">&lt;/a&gt;</span>
  655. <span class="kw">&lt;/div&gt;</span>
  656. <span class="kw">&lt;nav</span><span class="ot"> class=</span><span class="st">&quot;collapse navbar-collapse bs-navbar-collapse&quot;</span><span class="ot"> role=</span><span class="st">&quot;navigation&quot;</span><span class="kw">&gt;</span>
  657. <span class="kw">&lt;ul</span><span class="ot"> class=</span><span class="st">&quot;nav navbar-nav&quot;</span><span class="kw">&gt;</span>
  658. <span class="kw">&lt;li&gt;</span>
  659. <span class="kw">&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/pages/about/&quot;</span><span class="kw">&gt;</span>关于我<span class="kw">&lt;/a&gt;</span>
  660. <span class="kw">&lt;/li&gt;</span>
  661. <span class="kw">&lt;li&gt;</span>
  662. <span class="kw">&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/pages/resume/&quot;</span><span class="kw">&gt;</span>简历<span class="kw">&lt;/a&gt;</span>
  663. <span class="kw">&lt;/li&gt;</span>
  664. <span class="kw">&lt;/ul&gt;</span>
  665. <span class="kw">&lt;ul</span><span class="ot"> class=</span><span class="st">&quot;nav navbar-nav navbar-right&quot;</span><span class="kw">&gt;</span>
  666. <span class="kw">&lt;li&gt;&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/admin&quot;</span><span class="ot"> id=</span><span class="st">&quot;loginLink&quot;</span><span class="kw">&gt;</span>登入<span class="kw">&lt;/a&gt;&lt;/li&gt;</span>
  667. <span class="kw">&lt;/ul&gt;</span>
  668. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;col-sm-3 col-md-3 pull-right&quot;</span><span class="kw">&gt;</span>
  669. <span class="kw">&lt;form</span><span class="ot"> class=</span><span class="st">&quot;navbar-form&quot;</span><span class="ot"> role=</span><span class="st">&quot;search&quot;</span><span class="kw">&gt;</span>
  670. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;input-group&quot;</span><span class="kw">&gt;</span>
  671. <span class="kw">&lt;input</span><span class="ot"> type=</span><span class="st">&quot;text&quot;</span><span class="ot"> id=</span><span class="st">&quot;typeahead-input&quot;</span><span class="ot"> class=</span><span class="st">&quot;form-control&quot;</span><span class="ot"> placeholder=</span><span class="st">&quot;Search&quot;</span><span class="ot"> name=</span><span class="st">&quot;search&quot;</span><span class="ot"> data-provide=</span><span class="st">&quot;typeahead&quot;</span><span class="kw">&gt;</span>
  672. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;input-group-btn&quot;</span><span class="kw">&gt;</span>
  673. <span class="kw">&lt;button</span><span class="ot"> class=</span><span class="st">&quot;btn btn-default search-button&quot;</span><span class="ot"> type=</span><span class="st">&quot;submit&quot;</span><span class="kw">&gt;&lt;i</span><span class="ot"> class=</span><span class="st">&quot;glyphicon glyphicon-search&quot;</span><span class="kw">&gt;&lt;/i&gt;&lt;/button&gt;</span>
  674. <span class="kw">&lt;/div&gt;</span>
  675. <span class="kw">&lt;/div&gt;</span>
  676. <span class="kw">&lt;/form&gt;</span>
  677. <span class="kw">&lt;/div&gt;</span>
  678. <span class="kw">&lt;/nav&gt;</span>
  679. <span class="kw">&lt;/div&gt;</span>
  680. <span class="kw">&lt;/header&gt;</span>
  681. <span class="kw">&lt;main</span><span class="ot"> class=</span><span class="st">&quot;bs-docs-masthead&quot;</span><span class="ot"> id=</span><span class="st">&quot;content&quot;</span><span class="ot"> role=</span><span class="st">&quot;main&quot;</span><span class="kw">&gt;</span>
  682. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;container&quot;</span><span class="kw">&gt;</span>
  683. <span class="kw">&lt;div</span><span class="ot"> id=</span><span class="st">&quot;carbonads-container&quot;</span><span class="kw">&gt;</span>
  684. THE ONLY FAIR IS NOT FAIR <span class="kw">&lt;br&gt;</span>
  685. ENJOY CREATE <span class="er">&amp;</span> SHARE
  686. <span class="kw">&lt;/div&gt;</span>
  687. <span class="kw">&lt;/div&gt;</span>
  688. <span class="kw">&lt;/main&gt;</span>
  689. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;container&quot;</span><span class="ot"> id=</span><span class="st">&quot;container&quot;</span><span class="kw">&gt;</span>
  690. {% block content %}
  691. {% endblock %}
  692. <span class="kw">&lt;/div&gt;</span>
  693. <span class="kw">&lt;footer</span><span class="ot"> class=</span><span class="st">&quot;footer&quot;</span><span class="kw">&gt;</span>
  694. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;container&quot;</span><span class="kw">&gt;</span>
  695. <span class="kw">&lt;p</span><span class="ot"> class=</span><span class="st">&quot;text-muted&quot;</span><span class="kw">&gt;</span>@Copyright Phodal.com<span class="kw">&lt;/p&gt;</span>
  696. <span class="kw">&lt;/div&gt;</span>
  697. <span class="kw">&lt;/footer&gt;</span>
  698. <span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/jquery.min.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span>
  699. <span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/bootstrap.min.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span>
  700. <span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/bootstrap3-typeahead.min.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span>
  701. <span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/main.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span>
  702. <span class="kw">&lt;/body&gt;</span>
  703. <span class="kw">&lt;/html&gt;</span></code></pre></div>
  704. <p>在我们的index.html中,我们就可以拿到前五篇博客。我们只需要遍历出posts,拿出每个post相应的值,就可以完成列表页。</p>
  705. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html">{% extends &#39;base.html&#39; %}
  706. {% block title %}Welcome to my blog{% endblock %}
  707. {% block content %}
  708. <span class="kw">&lt;h1&gt;</span>Posts<span class="kw">&lt;/h1&gt;</span>
  709. {% for post in posts %}
  710. <span class="kw">&lt;h2&gt;&lt;a</span><span class="ot"> href=</span><span class="st">&quot;{{ post.get_absolute_url }}&quot;</span><span class="kw">&gt;</span>{{ post.title }}<span class="kw">&lt;/a&gt;&lt;/h2&gt;</span>
  711. <span class="kw">&lt;p&gt;</span>{{post.posted}} - By {{post.author}}<span class="kw">&lt;/p&gt;</span>
  712. <span class="kw">&lt;p&gt;</span>{{post.body}}<span class="kw">&lt;/p&gt;</span>
  713. {% endfor %}
  714. {% endblock %}</code></pre></div>
  715. <p>在上面的模板里,我们还取出了博客的链接用于跳转到详情页。</p>
  716. <h3 id="创建博客详情页">创建博客详情页</h3>
  717. <p>依据上面拿到的slug,我们就可以创建对应的详情页的view,代码如下所示:</p>
  718. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">def</span> view_post(request, slug):
  719. <span class="cf">return</span> render_to_response(<span class="st">&#39;blogpost_detail.html&#39;</span>, {
  720. <span class="st">&#39;post&#39;</span>: get_object_or_404(Blogpost, slug<span class="op">=</span>slug)
  721. })</code></pre></div>
  722. <p>这里的<code>get_object_or_404</code>将会根据slug来获取相应的博客,如果取不出相应的博客就会返回404。因此,我们的详情页和上面的列表页也是类似的。</p>
  723. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html">{% extends &#39;base.html&#39; %}
  724. {% block head_title %}{{ post.title }}{% endblock %}
  725. {% block title %}{{ post.title }}{% endblock %}
  726. {% block content %}
  727. <span class="kw">&lt;h2&gt;</span>{{ post.title }}<span class="kw">&lt;/a&gt;&lt;/h2&gt;</span>
  728. <span class="kw">&lt;p&gt;</span>{{post.posted}} - By {{post.author}}<span class="kw">&lt;/p&gt;</span>
  729. <span class="kw">&lt;p&gt;</span>{{post.body}}<span class="kw">&lt;/p&gt;</span>
  730. {% endblock %}</code></pre></div>
  731. <p>随后,我们就可以再提交一次代码了。</p>
  732. <h2 id="测试">测试</h2>
  733. <p>TDD虽然是一个非常好的实践,但是那是对于那些已经习惯写测试的人来说。如果你写测试的经历非常少,那么我们就可以从写测试开始。</p>
  734. <p>在这里我们使用的是Django这个第三方框架来完成我们的工作,所以我们并不对这个框架的功能进行测试。虽然有些时候正是因为这些第三方框架的问题而导致的Bug,但是我们仅仅只是使用一些基础的功能。这些基础的功能也已经在他们的框架中测试过了。</p>
  735. <h3 id="测试首页">测试首页</h3>
  736. <p>先来做一个简单的测试,即测试我们访问首页的时候,调用的函数是上面的index函数</p>
  737. <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
  738. <span class="im">from</span> django.http <span class="im">import</span> HttpRequest
  739. <span class="im">from</span> django.test <span class="im">import</span> TestCase
  740. <span class="im">from</span> blogpost.views <span class="im">import</span> index, view_post
  741. <span class="kw">class</span> HomePageTest(TestCase):
  742. <span class="kw">def</span> test_root_url_resolves_to_home_page_view(<span class="va">self</span>):
  743. found <span class="op">=</span> resolve(<span class="st">&#39;/&#39;</span>)
  744. <span class="va">self</span>.assertEqual(found.func, index)</code></pre></div>
  745. <p>但是这样的测试看上去没有多大意义,不过它可以保证我们的route可以和我们的URL对应上。在编写完测试后,我们就可以命令提示行中运行:</p>
  746. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">python</span> manage.py test</code></pre></div>
  747. <p>来查看测试的结果:</p>
  748. <pre><code>Creating test database for alias &#39;default&#39;...
  749. .
  750. ----------------------------------------------------------------------
  751. Ran 1 test in 0.031s
  752. OK
  753. Destroying test database for alias &#39;default&#39;...
  754. (growth-django)</code></pre>
  755. <p>运行通过,现在我们可以进行下一个测试了——我们可以测试页面的标题是不是我们想要的结果:</p>
  756. <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>):
  757. request <span class="op">=</span> HttpRequest()
  758. response <span class="op">=</span> index(request)
  759. <span class="va">self</span>.assertIn(b<span class="st">&#39;&lt;title&gt;Welcome to my blog&lt;/title&gt;&#39;</span>, response.content)</code></pre></div>
  760. <p>这里我们需要去请求相应的页面来获取页面的标题,并用assertIn方法来断言返回的首页的html中含有<code>&lt;title&gt;Welcome to my blog&lt;/title&gt;</code>。</p>
  761. <h3 id="测试详情页">测试详情页</h3>
  762. <p>同样的我们也可以用测试是否调用某个函数的方法,来看博客的详情页的route是否正确?</p>
  763. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogpostTest(TestCase):
  764. <span class="kw">def</span> test_blogpost_url_resolves_to_blog_post_view(<span class="va">self</span>):
  765. found <span class="op">=</span> resolve(<span class="st">&#39;/blog/this_is_a_test.html&#39;</span>)
  766. <span class="va">self</span>.assertEqual(found.func, view_post)</code></pre></div>
  767. <p>与上面测试首页不一样的是,在我们的Blogpost测试中,我们需要创建数据,以确保这个流程是没有问题的。因此我们需要用<code>Blogpost.objects.create</code>方法来创建一个数据,然后访问相应的页面来看是否正确。</p>
  768. <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>):
  769. Blogpost.objects.create(title<span class="op">=</span><span class="st">&#39;hello&#39;</span>, author<span class="op">=</span><span class="st">&#39;admin&#39;</span>, slug<span class="op">=</span><span class="st">&#39;this_is_a_test&#39;</span>, body<span class="op">=</span><span class="st">&#39;This is a blog&#39;</span>,
  770. posted<span class="op">=</span>datetime.now)
  771. response <span class="op">=</span> <span class="va">self</span>.client.get(<span class="st">&#39;/blog/this_is_a_test.html&#39;</span>)
  772. <span class="va">self</span>.assertIn(b<span class="st">&#39;This is a blog&#39;</span>, response.content)</code></pre></div>
  773. <p>或许你会疑惑这个数据会不会被注入到数据库中,请看运行测试时返回的结果的第一句:</p>
  774. <pre><code>Creating test database for alias &#39;default&#39;...</code></pre>
  775. <p>Django将会创建一个数据库用于测试。</p>
  776. <p>同理,我们也可以为首页添加一个相似的测试:</p>
  777. <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>):
  778. Blogpost.objects.create(title<span class="op">=</span><span class="st">&#39;hello&#39;</span>, author<span class="op">=</span><span class="st">&#39;admin&#39;</span>, slug<span class="op">=</span><span class="st">&#39;this_is_a_test&#39;</span>, body<span class="op">=</span><span class="st">&#39;This is a blog&#39;</span>,
  779. posted<span class="op">=</span>datetime.now)
  780. response <span class="op">=</span> <span class="va">self</span>.client.get(<span class="st">&#39;/&#39;</span>)
  781. <span class="va">self</span>.assertIn(b<span class="st">&#39;This is a blog&#39;</span>, response.content)</code></pre></div>
  782. <p>我们用同样的方法创建了一篇博客,然后在首页测试返回的内容中是否含有<code>This is a blog</code>。</p>
  783. <h1 id="自动化测试与持续集成">自动化测试与持续集成</h1>
  784. <p>在上一章最后,我们写的测试可以算得上是单元测试,接着我们可以写一些自动化测试。</p>
  785. <h2 id="编写自动化测试">编写自动化测试</h2>
  786. <p>接着我们就可以用Selenium来做自动化测试。这是ThoughtWorks出品的一个强大的基于浏览器的开源自动化测试工具,它通常用来编写Web 应用的自动化测试。</p>
  787. <h3 id="selenium与第一个ui测试">Selenium与第一个UI测试</h3>
  788. <p>先让我们来看一个自动化测试的例子:</p>
  789. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.test <span class="im">import</span> LiveServerTestCase
  790. <span class="im">from</span> selenium <span class="im">import</span> webdriver
  791. <span class="kw">class</span> HomepageTestCase(LiveServerTestCase):
  792. <span class="kw">def</span> setUp(<span class="va">self</span>):
  793. <span class="va">self</span>.selenium <span class="op">=</span> webdriver.Firefox()
  794. <span class="va">self</span>.selenium.maximize_window()
  795. <span class="bu">super</span>(HomepageTestCase, <span class="va">self</span>).setUp()
  796. <span class="kw">def</span> tearDown(<span class="va">self</span>):
  797. <span class="va">self</span>.selenium.quit()
  798. <span class="bu">super</span>(HomepageTestCase, <span class="va">self</span>).tearDown()
  799. <span class="kw">def</span> test_visit_homepage(<span class="va">self</span>):
  800. <span class="va">self</span>.selenium.get(
  801. <span class="st">&#39;</span><span class="sc">%s%s</span><span class="st">&#39;</span> <span class="op">%</span> (<span class="va">self</span>.live_server_url, <span class="st">&quot;/&quot;</span>)
  802. )
  803. <span class="va">self</span>.assertIn(<span class="st">&quot;Welcome to my blog&quot;</span>, <span class="va">self</span>.selenium.title)</code></pre></div>
  804. <p>在setUp——即开始的时候,我们会用selenium启动一个Firefox浏览器的进程,并执行maximize_window来将窗口最大化。在tearDown——即结束的时候,我们就会关闭这个浏览器的进程。我们的主要测试代码就在<code>test_visit_homepage</code>这个方法里,我们在里面访问首页,并判断标题是不是<code>Welcome to my blog</code>。</p>
  805. <p>运行上面的测试就会启动一个浏览器,并且会在浏览器上进行相应的操作。如下图所示:</p>
  806. <figure>
  807. <img src="./images/selenium-demo.jpg" alt="Selenium Demo" /><figcaption>Selenium Demo</figcaption>
  808. </figure>
  809. <p>这时你可能会产生一些疑惑,这些内容我们不是已经测试过了么?两者从测试看是差不多的,但是从流程上看来说并不是如些。下图是页面渲染的时间线:</p>
  810. <figure>
  811. <img src="./images/page-timing-overview.png" alt="页面渲染时间线" /><figcaption>页面渲染时间线</figcaption>
  812. </figure>
  813. <p>请求从浏览器传到服务器要有一系列的过程,如重定向、缓存、DNS等等,最后直至返回对应的Response。我们用Django的测试框架只能实现到这一步,随后页面请请求对应的静态资料,再对页面进行渲染,在这个过程中页面的内容会发生一些变化。</p>
  814. <p>为了避免页面的内容被替换掉,那么我们就需要对这部分内容进行测试。</p>
  815. <p>如下的代码也是可以用于测试页面内容的代码:</p>
  816. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogpostDetailCase(LiveServerTestCase):
  817. <span class="kw">def</span> setUp(<span class="va">self</span>):
  818. Blogpost.objects.create(
  819. title<span class="op">=</span><span class="st">&#39;hello&#39;</span>,
  820. author<span class="op">=</span><span class="st">&#39;admin&#39;</span>,
  821. slug<span class="op">=</span><span class="st">&#39;this_is_a_test&#39;</span>,
  822. body<span class="op">=</span><span class="st">&#39;This is a blog&#39;</span>,
  823. posted<span class="op">=</span>datetime.now
  824. )
  825. <span class="va">self</span>.selenium <span class="op">=</span> webdriver.Firefox()
  826. <span class="va">self</span>.selenium.maximize_window()
  827. <span class="bu">super</span>(BlogpostDetailCase, <span class="va">self</span>).setUp()
  828. <span class="kw">def</span> tearDown(<span class="va">self</span>):
  829. <span class="va">self</span>.selenium.quit()
  830. <span class="bu">super</span>(BlogpostDetailCase, <span class="va">self</span>).tearDown()
  831. <span class="kw">def</span> test_visit_blog_post(<span class="va">self</span>):
  832. <span class="va">self</span>.selenium.get(
  833. <span class="st">&#39;</span><span class="sc">%s%s</span><span class="st">&#39;</span> <span class="op">%</span> (<span class="va">self</span>.live_server_url, <span class="st">&quot;/blog/this_is_a_test.html&quot;</span>)
  834. )
  835. <span class="va">self</span>.assertIn(<span class="st">&quot;hello&quot;</span>, <span class="va">self</span>.selenium.title)</code></pre></div>
  836. <p>虽然在这里我们要测试的只是页面的标题,而实际上我们要测试的是页面的元素是否存在。</p>
  837. <p>同样的,我们也可以对博客的内容进行测试。这些稍有不同的是,我们更多地是要测试用户的行为,如我们在首页点击某个链接,那么我应该中转到对应的博客详情页,如下代码所示:</p>
  838. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogpostFromHomepageCase(LiveServerTestCase):
  839. <span class="kw">def</span> setUp(<span class="va">self</span>):
  840. Blogpost.objects.create(
  841. title<span class="op">=</span><span class="st">&#39;hello&#39;</span>,
  842. author<span class="op">=</span><span class="st">&#39;admin&#39;</span>,
  843. slug<span class="op">=</span><span class="st">&#39;this_is_a_test&#39;</span>,
  844. body<span class="op">=</span><span class="st">&#39;This is a blog&#39;</span>,
  845. posted<span class="op">=</span>datetime.now
  846. )
  847. <span class="va">self</span>.selenium <span class="op">=</span> webdriver.Firefox()
  848. <span class="va">self</span>.selenium.maximize_window()
  849. <span class="bu">super</span>(BlogpostFromHomepageCase, <span class="va">self</span>).setUp()
  850. <span class="kw">def</span> tearDown(<span class="va">self</span>):
  851. <span class="va">self</span>.selenium.quit()
  852. <span class="bu">super</span>(BlogpostFromHomepageCase, <span class="va">self</span>).tearDown()
  853. <span class="kw">def</span> test_visit_blog_post(<span class="va">self</span>):
  854. <span class="va">self</span>.selenium.get(
  855. <span class="st">&#39;</span><span class="sc">%s%s</span><span class="st">&#39;</span> <span class="op">%</span> (<span class="va">self</span>.live_server_url, <span class="st">&quot;/&quot;</span>)
  856. )
  857. <span class="va">self</span>.selenium.find_element_by_link_text(<span class="st">&quot;hello&quot;</span>).click()
  858. <span class="va">self</span>.assertIn(<span class="st">&quot;hello&quot;</span>, <span class="va">self</span>.selenium.title)</code></pre></div>
  859. <p>需要注意的是,如果我们的单元测试如果可以测试到页面的内容——即没有使用JavaScript对页面的内容进行修改,那么我们应该使用单元测试即可。如测试金字塔所说,越底层的测试越快。</p>
  860. <p>在我们编写完这些测试后,我们就可以搭建好相应的持续集成来运行这些测试了。</p>
  861. <h2 id="搭建持续集成">搭建持续集成</h2>
  862. <p>这里我们将使用Jenkins来完成这部分的工作,它是一个用Java编写的开源的持续集成工具。</p>
  863. <blockquote>
  864. <p>它提供了软件开发的持续集成服务。它运行在Servlet容器中(例如Apache Tomcat)。它支持软件配置管理(SCM)工具(包括AccuRev SCM、CVS、Subversion、Git、Perforce、Clearcase和和RTC),可以执行基于Apache Ant和Apache Maven的项目,以及任意的Shell脚本和Windows批处理命令。</p>
  865. </blockquote>
  866. <p>要使用Jenkins,只需要从Jenkins的主页上(<a href="https://jenkins.io/" class="uri">https://jenkins.io/</a>)下载最新的 jenkins.war文件。然后运行</p>
  867. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">java</span> -jar jenkins.war</code></pre></div>
  868. <p>便可以启动:</p>
  869. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">Running</span> from: /Users/fdhuang/repractise/growth-ci/jenkins.war
  870. <span class="ex">webroot</span>: <span class="va">$user</span>.home/.jenkins
  871. <span class="ex">May</span> 12, 2016 10:55:18 PM org.eclipse.jetty.util.log.JavaUtilLog info
  872. <span class="ex">INFO</span>: Logging initialized @489ms
  873. <span class="ex">May</span> 12, 2016 10:55:18 PM winstone.Logger logInternal
  874. <span class="ex">INFO</span>: Beginning extraction from war file
  875. <span class="ex">May</span> 12, 2016 10:55:20 PM org.eclipse.jetty.util.log.JavaUtilLog warn
  876. <span class="ex">WARNING</span>: Empty contextPath
  877. <span class="ex">May</span> 12, 2016 10:55:20 PM org.eclipse.jetty.util.log.JavaUtilLog info
  878. <span class="ex">INFO</span>: jetty-9.2.z-SNAPSHOT
  879. <span class="ex">May</span> 12, 2016 10:55:20 PM org.eclipse.jetty.util.log.JavaUtilLog info
  880. <span class="ex">INFO</span>: NO JSP Support for /, did not find org.eclipse.jetty.jsp.JettyJspServlet
  881. <span class="ex">Jenkins</span> home directory: /Users/fdhuang/.jenkins found at: <span class="va">$user</span>.home/.jenkins
  882. <span class="ex">May</span> 12, 2016 10:55:21 PM org.eclipse.jetty.util.log.JavaUtilLog info
  883. <span class="ex">INFO</span>: Started w.@68c34b0<span class="dt">{/,file:/Users/fdhuang/.jenkins/war/,AVAILABLE}{/Users/fdhuang/.jenkins/war}</span>
  884. <span class="ex">May</span> 12, 2016 10:55:21 PM org.eclipse.jetty.util.log.JavaUtilLog info
  885. <span class="ex">INFO</span>: Started ServerConnector@733a9ac6<span class="dt">{HTTP/1.1}{0.0.0.0:8080}</span></code></pre></div>
  886. <p>接着,打开<a href="http://0.0.0.0:8080/" class="uri">http://0.0.0.0:8080/</a>就可以进行后续的安装,如下图所示:</p>
  887. <figure>
  888. <img src="./images/jenkins-install.jpg" alt="Jenkins安装过程" /><figcaption>Jenkins安装过程</figcaption>
  889. </figure>
  890. <p>慢慢等其安装完成:</p>
  891. <figure>
  892. <img src="./images/jenkins-getting-started.jpg" alt="Jenkins安装完成" /><figcaption>Jenkins安装完成</figcaption>
  893. </figure>
  894. <p>等安装完成后,我们就可以开始使用Jenkins来创建我们的任务了。</p>
  895. <h3 id="jenkins创建任务">Jenkins创建任务</h3>
  896. <p>在首页,我们会看到“开始创建一个新任务”的提示,点击它。</p>
  897. <p>源码管理中选择Git,并填入我们代码的地址:</p>
  898. <pre><code>[https://github.com/phodal/growth-in-action-python-code](https://github.com/phodal/growth-in-action-python-code)</code></pre>
  899. <p>如下图所示:</p>
  900. <figure>
  901. <img src="./images/jenkins-repo-setup.jpg" alt="Jenkins设计Repo" /><figcaption>Jenkins设计Repo</figcaption>
  902. </figure>
  903. <p>然后就是构建触发器,一共有五种类型的触发器,意思也很容易理解:</p>
  904. <ul>
  905. <li>触发远程构建 (例如,使用脚本)</li>
  906. <li>Build after other projects are built</li>
  907. <li>Build periodically</li>
  908. <li>Build when a change is pushed to GitHub</li>
  909. <li>Poll SCM</li>
  910. </ul>
  911. <p>在这里,我们要使用的是GitHub这个,它的原理是:</p>
  912. <blockquote>
  913. <p>This job will be triggered if jenkins will receive PUSH GitHub hook from repo defined in scm section</p>
  914. </blockquote>
  915. <p>即Jenkins在监听GitHub上对应的PUSH hook,当发生代码提交时,就会运行我们的测试。</p>
  916. <p>由于,我们暂时不需要一些特殊的<code>构建环境</code>配置,我们就可以将这个放空。接着,我们就可以配置<code>构建</code>了。</p>
  917. <h3 id="创建shell">创建shell</h3>
  918. <p>在这里我们需要添加的构建步骤是:<code>execute shell</code>,先让我们写一个简单的安装依赖的shell</p>
  919. <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
  920. <span class="bu">source</span> growth-django/bin/activate
  921. <span class="ex">pip</span> install -r requirements.txt</code></pre></div>
  922. <p>然后在保存后,我们可以尝试立即构建这个项目:</p>
  923. <figure>
  924. <img src="./images/build-console-ouput.jpg" alt="控制台输出" /><figcaption>控制台输出</figcaption>
  925. </figure>
  926. <p>在编写shell的过程中,我们要经过一些尝试,在这其中会经历一些失败的情形——即使是大部分有相关经验的程序员。如下图就是一次编写构建脚本引起的构建失败的例子:</p>
  927. <figure>
  928. <img src="./images/jenkins-failure-setup.jpg" alt="Jenkins失败的构建" /><figcaption>Jenkins失败的构建</figcaption>
  929. </figure>
  930. <p>最后,我们就得到下面的一个shell脚本,我们就可以将其变成相应的运行CI的脚本。以便于它可以在其他环境中使用:</p>
  931. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="co">#!/usr/bin/env bash</span>
  932. <span class="ex">virtualenv</span> --distribute -p /usr/local/bin/python3.5 growth-django
  933. <span class="bu">source</span> growth-django/bin/activate
  934. <span class="ex">pip</span> install -r requirements.txt
  935. <span class="ex">python</span> manage.py test
  936. <span class="ex">python</span> manage.py test test</code></pre></div>
  937. <p>记得给你的shell文件,加上执行的标志:</p>
  938. <pre><code>chmod u+x ./scripts/ci.sh</code></pre>
  939. <p>最后,我们就可以修改CI上相应的构建环境的配置。</p>
  940. <h1 id="更完善的博客系统">更完善的博客系统</h1>
  941. <p>在Django框架中,内置了很多应用在它的“contrib”包中,这些包括:</p>
  942. <ul>
  943. <li>一个可扩展的认证系统</li>
  944. <li>动态站点管理页面</li>
  945. <li>一组产生RSS和Atom的工具</li>
  946. <li>一个灵活的评论系统</li>
  947. <li>产生Google站点地图(Google Sitemaps)的工具</li>
  948. <li>防止跨站请求伪造(cross-site request forgery)的工具</li>
  949. <li>一套支持轻量级标记语言(Textile和Markdown)的模板库</li>
  950. <li>一套协助创建地理信息系统(GIS)的基础框架</li>
  951. </ul>
  952. <p>这意味着,我们可以直接用Django一些内置的组件来完成很多功能,先让我们来看看怎么完成一个简单的评论功能。</p>
  953. <h2 id="静态页面">静态页面</h2>
  954. <p>Django带有一个可选的“flatpages”应用,可以让我们存储简单的“扁平化(flat)”页面在数据库中,并且可以通过Django的管理界面以及一个Python API来处理要管理的内容。这样的一个静态页面,一般包含下面的几个属性:</p>
  955. <ul>
  956. <li>标题</li>
  957. <li>URL</li>
  958. <li>内容(Content)</li>
  959. <li>Sites</li>
  960. <li>自定义模板(可选)</li>
  961. </ul>
  962. <p>为了使用它来创建静态页面,我们需要在数据库中存储对应的映射关系,并创建对应的静态页面。</p>
  963. <h3 id="安装-flatpages">安装 flatpages</h3>
  964. <p>为此我们需要添加两个应用到<code>settings.py</code>文件的<code>INSTALLED_APPS</code>中:</p>
  965. <ul>
  966. <li><code>django.contrib.sites</code>——“sites”框架,它用于将对象和功能与特定的站点关联。同时,它还是域名和你的Django 站点名称之间的对应关系所保存的位置,即我们需要在这个地方设置我们的网站的域名。</li>
  967. <li><code>django.contrib.flatpages</code>,即上文说到的内容。</li>
  968. </ul>
  969. <p>在添加<code>django.contrib.sites</code>的时候,我们需要创建一个<code>SITE_ID</code>。通过这个值等于1,除非我们打算用这个框架去管理多个站点。代码如下所示:</p>
  970. <pre><code>SITE_ID = 1
  971. INSTALLED_APPS = (
  972. &#39;django.contrib.admin&#39;,
  973. &#39;django.contrib.auth&#39;,
  974. &#39;django.contrib.contenttypes&#39;,
  975. &#39;django.contrib.sessions&#39;,
  976. &#39;django.contrib.messages&#39;,
  977. &#39;django.contrib.staticfiles&#39;,
  978. &#39;django.contrib.sites&#39;,
  979. &#39;django.contrib.flatpages&#39;,
  980. &#39;blogpost&#39;
  981. )</code></pre>
  982. <p>接着,还添加对应的中间件<code>django.contrib.flatpages.middleware.FlatpageFallbackMiddleware</code>到<code>settings.py</code>文件的<code>MIDDLEWARE_CLASSES</code>中。</p>
  983. <p>然后,我们需要创建对应的URL来管理所有的静态页面。下面的代码是将静态页面都放在pages路径下,即如果我们有一个about的页面,那么对应的URL会变成 http://localhost/pages/about/。</p>
  984. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">url(<span class="vs">r&#39;^pages/&#39;</span>, include(<span class="st">&#39;django.contrib.flatpages.urls&#39;</span>)),</code></pre></div>
  985. <p>当然我们也可以将其配置为类似于 http://localhost/about/ 这样的URL:</p>
  986. <pre><code>urlpatterns += [
  987. url(r&#39;^(?P&lt;url&gt;.*/)$&#39;, views.flatpage),
  988. ]</code></pre>
  989. <p>最后,我们还需要做一个数据库迁移:</p>
  990. <pre><code>Operations to perform:
  991. Apply all migrations: contenttypes, auth, admin, sites, blogpost, sessions, flatpages, django_comments
  992. Running migrations:
  993. Rendering model states... DONE
  994. Applying flatpages.0001_initial... OK</code></pre>
  995. <h3 id="创建模板">创建模板</h3>
  996. <p>接着,我们可以在<code>templates</code>目录下创建<code>flatpages</code>文件,用于存放我们的模板文件,下面是一个简单的模板:</p>
  997. <pre><code>{% extends &#39;base.html&#39; %}
  998. {% block title %}关于我{% endblock %}
  999. {% block content %}
  1000. &lt;div&gt;
  1001. &lt;h2&gt;关于博客&lt;/h2&gt;
  1002. &lt;p&gt;一方面,找到更多志同道合的人;另一方面,扩大影响力。&lt;/p&gt;
  1003. &lt;p&gt;内容包括&lt;/p&gt;
  1004. &lt;ul&gt;
  1005. &lt;li&gt;成长记录&lt;/li&gt;
  1006. &lt;li&gt;技术笔记&lt;/li&gt;
  1007. &lt;li&gt;生活思考&lt;/li&gt;
  1008. &lt;li&gt;个人试验&lt;/li&gt;
  1009. &lt;/ul&gt;
  1010. &lt;/div&gt;
  1011. {% endblock %}</code></pre>
  1012. <p>当我们完成模板后,我们就需要登录后台,并添加对应的静态页面的配置:</p>
  1013. <figure>
  1014. <img src="./images/admin-flatpages-create.jpg" alt="管理员界面创建flatpage" /><figcaption>管理员界面创建flatpage</figcaption>
  1015. </figure>
  1016. <p>然后从高级选项中填写我们的静态页面的路径,我们就可以完成静态页面的创建。如下图所示:</p>
  1017. <figure>
  1018. <img src="./images/flatpages-advance-option.png" alt="flatpage高级选项" /><figcaption>flatpage高级选项</figcaption>
  1019. </figure>
  1020. <p>最后,还要有个链接加到首页的导航中:</p>
  1021. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw">&lt;li&gt;</span>
  1022. <span class="kw">&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/pages/about/&quot;</span><span class="kw">&gt;</span>关于我<span class="kw">&lt;/a&gt;</span>
  1023. <span class="kw">&lt;/li&gt;</span></code></pre></div>
  1024. <p>下面让我们为我们的博客添加一个简单的评论功能吧!</p>
  1025. <h2 id="评论功能">评论功能</h2>
  1026. <p>在早期的Django版本(1.6以前)中,Comments是自带的组件,但是后来它被从标准组件中移除了。因此,我们需要安装comments这个包:</p>
  1027. <pre><code>pip install django-contrib-comments</code></pre>
  1028. <p>再把它及它的版本添加到<code>requirements.txt</code>,如下所示:</p>
  1029. <pre><code>django==1.9.4
  1030. selenium==2.53.1
  1031. fabric==1.10.2
  1032. djangorestframework==3.3.3
  1033. djangorestframework-jwt==1.7.2
  1034. django-cors-headers==1.1.0
  1035. django-contrib-comments==1.7.1</code></pre>
  1036. <p>接着,将<code>django.contrib.sites</code>和<code>django_comments</code>添加到<code>INSTALLED_APPS</code>,如下:</p>
  1037. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">INSTALLED_APPS <span class="op">=</span> (
  1038. <span class="st">&#39;django.contrib.admin&#39;</span>,
  1039. <span class="st">&#39;django.contrib.auth&#39;</span>,
  1040. <span class="st">&#39;django.contrib.contenttypes&#39;</span>,
  1041. <span class="st">&#39;django.contrib.sessions&#39;</span>,
  1042. <span class="st">&#39;django.contrib.messages&#39;</span>,
  1043. <span class="st">&#39;django.contrib.staticfiles&#39;</span>,
  1044. <span class="st">&#39;django.contrib.sites&#39;</span>,
  1045. <span class="st">&#39;django_comments&#39;</span>,
  1046. <span class="st">&#39;rest_framework&#39;</span>,
  1047. <span class="st">&#39;blogpost&#39;</span>
  1048. )</code></pre></div>
  1049. <p>然后做一下数据库迁移我们就可以完成对其的初始化:</p>
  1050. <pre><code>Operations to perform:
  1051. Apply all migrations: contenttypes, admin, blogpost, auth, sites, sessions, django_comments
  1052. Running migrations:
  1053. Rendering model states... DONE
  1054. Applying sites.0001_initial... OK
  1055. Applying django_comments.0001_initial... OK
  1056. Applying django_comments.0002_update_user_email_field_length... OK
  1057. Applying django_comments.0003_add_submit_date_index... OK
  1058. Applying sites.0002_alter_domain_unique... OK
  1059. (growth-django)</code></pre>
  1060. <p>然后再添加URL到urls.py:</p>
  1061. <pre><code>url(r&#39;^comments/&#39;, include(&#39;django_comments.urls&#39;)),</code></pre>
  1062. <p>现在,我们就可以登录后台,来创建对应的评论,但是这是时候评论是不会显示到页面上的。所以我们需要对我们的博客详情页的模板进行修改,在其中添加一句:</p>
  1063. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html">{% render_comment_list for post %}</code></pre></div>
  1064. <p>用于显示对应博客的评论,最近我们的模板文件如下面的内容所示:</p>
  1065. <pre><code>{% extends &#39;base.html&#39; %}
  1066. {% load comments %}
  1067. {% block head_title %}{{ post.title }}{% endblock %}
  1068. {% block title %}{{ post.title }}{% endblock %}
  1069. {% block content %}
  1070. &lt;div class=&quot;mdl-card mdl-shadow--2dp&quot;&gt;
  1071. &lt;div class=&quot;mdl-card__title&quot;&gt;
  1072. &lt;h2 class=&quot;mdl-card__title-text&quot;&gt;&lt;a href=&quot;{{ post.get_absolute_url }}&quot;&gt;{{ post.title }}&lt;/a&gt;&lt;/h2&gt;
  1073. &lt;/div&gt;
  1074. &lt;div class=&quot;mdl-card__supporting-text&quot;&gt;
  1075. {{post.body}}
  1076. &lt;/div&gt;
  1077. &lt;div class=&quot;mdl-card__actions&quot;&gt;
  1078. {{post.posted}} - By {{post.author}}
  1079. &lt;/div&gt;
  1080. &lt;/div&gt;
  1081. {% render_comment_list for post %}
  1082. {% endblock %}</code></pre>
  1083. <p>遗憾的是,当我们刷新页面的时候,页面报错了,原因如下所示:</p>
  1084. <figure>
  1085. <img src="./images/site_id_issue.jpg" alt="SITE_ID报错" /><figcaption>SITE_ID报错</figcaption>
  1086. </figure>
  1087. <p>我们还需要定义一个<code>SITE_ID</code>,添加下面的代码到<code>settings.py</code>文件中即可:</p>
  1088. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">SITE_ID <span class="op">=</span> <span class="dv">1</span></code></pre></div>
  1089. <p>然后,我们就可以从后台创建评论:</p>
  1090. <figure>
  1091. <img src="./images/create-comment-backend.jpg" alt="后台创建评论" /><figcaption>后台创建评论</figcaption>
  1092. </figure>
  1093. <h2 id="sitemap">Sitemap</h2>
  1094. <p>我们在之前的文章中提到过SEO的重要性,这里只是简单地对Sitemap的内容进行展开。</p>
  1095. <h3 id="站点地图介绍">站点地图介绍</h3>
  1096. <p>Sitemap译为站点地图,它用于告诉搜索引擎他们网站上有哪些可供抓取的网页。常见的Sitemap的形式是以xml出现了,如下是我博客的sitemap.xml的一部分内容:</p>
  1097. <div class="sourceCode"><pre class="sourceCode xml"><code class="sourceCode xml"><span class="kw">&lt;?xml</span> version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;<span class="kw">?&gt;</span>
  1098. <span class="kw">&lt;urlset</span><span class="ot"> xmlns=</span><span class="st">&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;</span><span class="kw">&gt;</span>
  1099. <span class="kw">&lt;url&gt;</span>
  1100. <span class="kw">&lt;loc&gt;</span>https://www.phodal.com/blog/mezzanine-add-new-page/<span class="kw">&lt;/loc&gt;</span>
  1101. <span class="kw">&lt;lastmod&gt;</span>2014-08-03<span class="kw">&lt;/lastmod&gt;</span>
  1102. <span class="kw">&lt;changefreq&gt;</span>Monthly<span class="kw">&lt;/changefreq&gt;</span>
  1103. <span class="kw">&lt;priority&gt;</span>0.2<span class="kw">&lt;/priority&gt;</span>
  1104. <span class="kw">&lt;/url&gt;</span>
  1105. <span class="kw">&lt;/urlset&gt;</span></code></pre></div>
  1106. <p>从上面的内容中,我们可以发现它包含了下面的一些XML标签:</p>
  1107. <ul>
  1108. <li>urlset,封装该文件,并指明当前协议的标准。</li>
  1109. <li>url,每个URL实体的父标签。</li>
  1110. <li>loc,指明页面的URL</li>
  1111. <li>lastmod(可选),内容最后的修改时间</li>
  1112. <li>changefreq(可选),内容的修改频率,用于告知搜索引擎抓取频率。它包含的值有:<code>always</code>、<code>hourly</code>、<code>daily</code>、<code>weekly</code>、<code>monthly</code>、<code>yearly</code>、<code>never</code></li>
  1113. <li>priority(可选),范围是从0.0~1.0,搜索引擎用于对你网站在搜索结果的排序,即内部的优先级排序。需要注意的是如果你把所有页面的优先级设置为1,那么它就和没有设置的效果是一样的。</li>
  1114. </ul>
  1115. <p>从上面的内容中,我们可以发现:</p>
  1116. <blockquote>
  1117. <p>站点地图能够提供与其中列出的网页相关的宝贵元数据:元数据是网页的相关信息,例如网页的最近更新时间、网页的更改频率以及网页相较于网站中其他网址的重要程度。 ——内容来自 Google Sitemap帮助文档。</p>
  1118. </blockquote>
  1119. <p>现在,我们一共有三种类型的页面:</p>
  1120. <ul>
  1121. <li>首页,通常来说首页的priority应该是最高的,而它的<code>changefreq</code>可以设置为<code>daily</code>、<code>weekly</code>,这取决于你的博客的更新频率。如果你是做一些UGC(用户生成内容)的网站,那么你应该设置为<code>always</code>、<code>hourly</code>。</li>
  1122. <li>动态生成的博客详情页,这些内容一般很少进行改变,所以这的changefreq会比较低,如<code>yearly</code>或者<code>monthly</code>——并且没有高的必要性,它会导致搜索引擎一直抓取你的内容。这会对服务器造成一定的压力,并且无助于你网站的排名。</li>
  1123. <li>静态页面,如About页面,它可以有一个高的<code>priority</code>,但是它的<code>changefreq</code>也不一定很高。</li>
  1124. </ul>
  1125. <p>下面就让我们从首页说起。</p>
  1126. <h3 id="创建首页的sitemap">创建首页的Sitemap</h3>
  1127. <p>与上面创建静态页面时一样,我们也需要添加<code>django.contrib.sitemaps</code>到<code>INSTALLED_APPS</code>中。</p>
  1128. <p>然后,我们需要指定一个URL规则。通常来说,这个URL是叫sitemap.xml——一个约定俗成的标准。我们需要创建一个sitemaps对象来存储所有的sitemaps:</p>
  1129. <pre><code>url(r&#39;^sitemap\.xml$&#39;, sitemap, {&#39;sitemaps&#39;: sitemaps}, name=&#39;django.contrib.sitemaps.views.sitemap&#39;)</code></pre>
  1130. <p>由于,我们使用的视图处理方法是<code>django.contrib.sitemaps.views.sitemap</code>,代码如下所示:</p>
  1131. <pre><code>
  1132. @x_robots_tag
  1133. def sitemap(request, sitemaps, section=None,
  1134. template_name=&#39;sitemap.xml&#39;, content_type=&#39;application/xml&#39;):
  1135. req_protocol = request.scheme
  1136. req_site = get_current_site(request)</code></pre>
  1137. <p>在这个方法里,它指定了默认模板的位置,即在<code>template</code>目录中。</p>
  1138. <p>现在,我们需要创建几种不同类型的sitemap,如下是首页的Sitemap,它继承自Django的Sitemap类:</p>
  1139. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> PageSitemap(Sitemap):
  1140. priority <span class="op">=</span> <span class="fl">1.0</span>
  1141. changefreq <span class="op">=</span> <span class="st">&#39;daily&#39;</span>
  1142. <span class="kw">def</span> items(<span class="va">self</span>):
  1143. <span class="cf">return</span> [<span class="st">&#39;main&#39;</span>]
  1144. <span class="kw">def</span> location(<span class="va">self</span>, item):
  1145. <span class="cf">return</span> reverse(item)</code></pre></div>
  1146. <p>它定义了自己的priority是最高的1.0,同时每新频率为<code>daily</code>。然后在items里面去取它所要获取的URL,即<code>urls.py</code>中对应的<code>name</code>的<code>main</code>的URL。在这里我们只返回了<code>main</code>一个值,依据于下面的location方法中的<code>reverse</code>,它找到了main对应的URL,即首页。</p>
  1147. <p>最后结合首页sitemap.xml的<code>urls.py</code>代码如下所示:</p>
  1148. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> sitemap.sitemaps <span class="im">import</span> PageSitemap
  1149. sitemaps <span class="op">=</span> {
  1150. <span class="st">&quot;page&quot;</span>: PageSitemap
  1151. }
  1152. urlpatterns <span class="op">=</span> patterns(<span class="st">&#39;&#39;</span>,
  1153. url(<span class="vs">r&#39;^$&#39;</span>, blogpostViews.index, name<span class="op">=</span><span class="st">&#39;main&#39;</span>),
  1154. url(<span class="vs">r&#39;^blog/(?P&lt;slug&gt;[^\.]+).html&#39;</span>, <span class="st">&#39;blogpost.views.view_post&#39;</span>, name<span class="op">=</span><span class="st">&#39;view_blog_post&#39;</span>),
  1155. url(<span class="vs">r&#39;^comments/&#39;</span>, include(<span class="st">&#39;django_comments.urls&#39;</span>)),
  1156. url(<span class="vs">r&#39;^admin/&#39;</span>, include(admin.site.urls)),
  1157. url(<span class="vs">r&#39;^pages/&#39;</span>, include(<span class="st">&#39;django.contrib.flatpages.urls&#39;</span>)),
  1158. url(<span class="vs">r&#39;^sitemap\.xml$&#39;</span>, sitemap, {<span class="st">&#39;sitemaps&#39;</span>: sitemaps}, name<span class="op">=</span><span class="st">&#39;django.contrib.sitemaps.views.sitemap&#39;</span>)
  1159. ) <span class="op">+</span> static(settings.STATIC_URL, document_root<span class="op">=</span>settings.STATIC_ROOT)</code></pre></div>
  1160. <p>除此,我们还需要创建自己的<code>sitemap.xml</code>模板——自带的系统模板比较简单。</p>
  1161. <div class="sourceCode"><pre class="sourceCode xml"><code class="sourceCode xml"><span class="kw">&lt;?xml</span> version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;<span class="kw">?&gt;</span>
  1162. <span class="kw">&lt;urlset</span><span class="ot"> xmlns=</span><span class="st">&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;</span><span class="kw">&gt;</span>
  1163. {% spaceless %}
  1164. {% for url in urlset %}
  1165. <span class="kw">&lt;url&gt;</span>
  1166. <span class="kw">&lt;loc&gt;</span>{{ url.location }}<span class="kw">&lt;/loc&gt;</span>
  1167. {% if url.lastmod %}<span class="kw">&lt;lastmod&gt;</span>{{ url.lastmod|date:&quot;Y-m-d&quot; }}<span class="kw">&lt;/lastmod&gt;</span>{% endif %}
  1168. {% if url.changefreq %}<span class="kw">&lt;changefreq&gt;</span>{{ url.changefreq }}<span class="kw">&lt;/changefreq&gt;</span>{% endif %}
  1169. {% if url.priority %}<span class="kw">&lt;priority&gt;</span>{{ url.priority }}<span class="kw">&lt;/priority&gt;</span>{% endif %}
  1170. <span class="kw">&lt;/url&gt;</span>
  1171. {% endfor %}
  1172. {% endspaceless %}
  1173. <span class="kw">&lt;/urlset&gt;</span></code></pre></div>
  1174. <p>最后,我们访问<a href="http://localhost:8000/sitemap.xml" class="uri">http://localhost:8000/sitemap.xml</a>,我们就可以获取到我们的<code>sitemap.xml</code>:</p>
  1175. <pre><code>&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
  1176. &lt;urlset xmlns=&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;&gt;
  1177. &lt;url&gt;
  1178. &lt;loc&gt;http://www.phodal.com/&lt;/loc&gt;
  1179. &lt;changefreq&gt;daily&lt;/changefreq&gt;
  1180. &lt;priority&gt;1.0&lt;/priority&gt;
  1181. &lt;/url&gt;
  1182. &lt;/urlset&gt;</code></pre>
  1183. <p>下一步,我们仍可以直接创建出对应的静态页面的Sitemap。</p>
  1184. <h3 id="创建静态页面的sitemap">创建静态页面的Sitemap</h3>
  1185. <p>相似的,我们也需要从items方法中,定义出我们所要创建页面的对象。</p>
  1186. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="im">from</span> django.contrib.sitemaps <span class="im">import</span> Sitemap
  1187. <span class="im">from</span> django.core.urlresolvers <span class="im">import</span> reverse
  1188. <span class="im">from</span> django.apps <span class="im">import</span> apps <span class="im">as</span> django_apps
  1189. <span class="kw">class</span> FlatPageSitemap(Sitemap):
  1190. priority <span class="op">=</span> <span class="fl">0.8</span>
  1191. <span class="kw">def</span> items(<span class="va">self</span>):
  1192. Site <span class="op">=</span> django_apps.get_model(<span class="st">&#39;sites.Site&#39;</span>)
  1193. current_site <span class="op">=</span> Site.objects.get_current()
  1194. <span class="cf">return</span> current_site.flatpage_set.<span class="bu">filter</span>(registration_required<span class="op">=</span><span class="va">False</span>)</code></pre></div>
  1195. <p>只不过这个方法可能会稍微麻烦一些,我们需要从数据库中取出当前的站点。再取出当前站点中的flatpage集合,对过滤那些不需要注册的页面,即代码中的<code>registration_required=False</code>。</p>
  1196. <p>最后再将这个对象放入sitemaps即可:</p>
  1197. <pre><code>from sitemap.sitemaps import PageSitemap, FlatPageSitemap
  1198. sitemaps = {
  1199. &quot;page&quot;: PageSitemap,
  1200. &#39;flatpages&#39;: FlatPageSitemap
  1201. }</code></pre>
  1202. <p>现在,我们可以完成博客的Sitemap了。</p>
  1203. <h3 id="创建博客的sitemap">创建博客的Sitemap</h3>
  1204. <p>同上面一样的是,我们依然需要在items方法中返回所有的博客内容。并且在lastmod中,返回这篇博客的发表日期——以免他们返回的是同一个日期:</p>
  1205. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogSitemap(Sitemap):
  1206. changefreq <span class="op">=</span> <span class="st">&quot;never&quot;</span>
  1207. priority <span class="op">=</span> <span class="fl">0.5</span>
  1208. <span class="kw">def</span> items(<span class="va">self</span>):
  1209. <span class="cf">return</span> Blogpost.objects.<span class="bu">all</span>()
  1210. <span class="kw">def</span> lastmod(<span class="va">self</span>, obj):
  1211. <span class="cf">return</span> obj.posted</code></pre></div>
  1212. <p>最近我们的Sitemap.xml,如下所示:</p>
  1213. <div class="sourceCode"><pre class="sourceCode xml"><code class="sourceCode xml"><span class="kw">&lt;?xml</span> version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;<span class="kw">?&gt;</span>
  1214. <span class="kw">&lt;urlset</span><span class="ot"> xmlns=</span><span class="st">&quot;http://www.sitemaps.org/schemas/sitemap/0.9&quot;</span><span class="kw">&gt;</span>
  1215. <span class="kw">&lt;url&gt;</span>
  1216. <span class="kw">&lt;loc&gt;</span>http://www.phodal.com/about/<span class="kw">&lt;/loc&gt;</span>
  1217. <span class="kw">&lt;priority&gt;</span>0.8<span class="kw">&lt;/priority&gt;</span>
  1218. <span class="kw">&lt;/url&gt;</span>
  1219. <span class="kw">&lt;url&gt;</span>
  1220. <span class="kw">&lt;loc&gt;</span>http://www.phodal.com/<span class="kw">&lt;/loc&gt;</span>
  1221. <span class="kw">&lt;changefreq&gt;</span>daily<span class="kw">&lt;/changefreq&gt;</span>
  1222. <span class="kw">&lt;priority&gt;</span>1.0<span class="kw">&lt;/priority&gt;</span>
  1223. <span class="kw">&lt;/url&gt;</span>
  1224. <span class="kw">&lt;url&gt;</span>
  1225. <span class="kw">&lt;loc&gt;</span>http://www.phodal.com/blog/hello.html<span class="kw">&lt;/loc&gt;</span>
  1226. <span class="kw">&lt;lastmod&gt;</span>2016-03-24<span class="kw">&lt;/lastmod&gt;</span>
  1227. <span class="kw">&lt;changefreq&gt;</span>never<span class="kw">&lt;/changefreq&gt;</span>
  1228. <span class="kw">&lt;priority&gt;</span>0.5<span class="kw">&lt;/priority&gt;</span>
  1229. <span class="kw">&lt;/url&gt;</span>
  1230. <span class="kw">&lt;/urlset&gt;</span></code></pre></div>
  1231. <h3 id="提交到搜索引擎">提交到搜索引擎</h3>
  1232. <p>这里我们以Google Webmaster为例简单的介绍一下如何使用各种站长工具来提交sitemap.xml。</p>
  1233. <p>我们可以登录Google的Webmaster:<a href="https://www.google.com/webmasters/tools/home?hl=zh-cn" class="uri">https://www.google.com/webmasters/tools/home?hl=zh-cn</a>,然后点击添加属性来创建一个新的网站:</p>
  1234. <figure>
  1235. <img src="./images/add-property.png" alt="添加网站" /><figcaption>添加网站</figcaption>
  1236. </figure>
  1237. <p>这时候Google需要确认这个网站是你的,所以它提供几种方法来验证,除了下面的推荐方法:</p>
  1238. <figure>
  1239. <img src="./images/google-add-website.png" alt="推荐的验证方式" /><figcaption>推荐的验证方式</figcaption>
  1240. </figure>
  1241. <p>我们可以使用下面的这一些方法:</p>
  1242. <figure>
  1243. <img src="./images/google-addition-method.png" alt="备选的难方法" /><figcaption>备选的难方法</figcaption>
  1244. </figure>
  1245. <p>我个人比较喜欢用HTML Tag的方式来实现</p>
  1246. <figure>
  1247. <img src="./images/html-tag.png" alt="HTML标签验证" /><figcaption>HTML标签验证</figcaption>
  1248. </figure>
  1249. <p>在我们完成验证之后,我们就可以在后台手动提交Sitemap.xml了。</p>
  1250. <figure>
  1251. <img src="./images/google-add-sitemap.png" alt="提交Sitemap.xml" /><figcaption>提交Sitemap.xml</figcaption>
  1252. </figure>
  1253. <p>点击上方的<strong>添加/测试站点地图</strong>即可。</p>
  1254. <h1 id="样式与ui美化">样式与UI美化</h1>
  1255. <p>我们的前端样式实在是太丑了,让我们想办法来美化一下它们吧——这时候我们就需要一个前端框架来帮助我们做这件事。这里的前端框架并不是指那种MV*框架,而是UI框架。</p>
  1256. <h2 id="响应式设计">响应式设计</h2>
  1257. <p>考虑到易学程度,以其响应式设计的问题,我们决定用Bootstrap来作为这里的前端框架。Bootstrap是Twitter推出的一个用于前端开发的开源工具包,似乎也是当前“最受欢迎”的前端框架。它提供了全面、美观的文档。你能在这里找到关于 HTML 元素、HTML 和 CSS 组件、jQuery 插件方面的所有详细文档。并且我们能在 Bootstrap 的帮助下通过同一份代码快速、有效适配手机、平板、PC 设备。</p>
  1258. <p>它是一个支持响应式设计的框架,即页面的设计与开发应当根据用户行为以及设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相应的响应和调整。如下图所示:</p>
  1259. <figure>
  1260. <img src="./images/responsive-design.png" alt="响应式设计" /><figcaption>响应式设计</figcaption>
  1261. </figure>
  1262. <p>我们在不同的设计上看到的是不同的布局,这会依据我们的设备大小做出调整——使用媒体查询(media queries)实现。</p>
  1263. <h3 id="引入前端框架">引入前端框架</h3>
  1264. <p>下好Bootstrap,将里面的内容复制到<code>static/</code>目录,如下所示:</p>
  1265. <pre><code>.
  1266. ├── css
  1267. │   ├── bootstrap-theme.css
  1268. │   ├── bootstrap-theme.css.map
  1269. │   ├── bootstrap-theme.min.css
  1270. │   ├── bootstrap-theme.min.css.map
  1271. │   ├── bootstrap.css
  1272. │   ├── bootstrap.css.map
  1273. │   ├── bootstrap.min.css
  1274. │   ├── bootstrap.min.css.map
  1275. │   └── styles.css
  1276. ├── fonts
  1277. │   ├── glyphicons-halflings-regular.eot
  1278. │   ├── glyphicons-halflings-regular.svg
  1279. │   ├── glyphicons-halflings-regular.ttf
  1280. │   ├── glyphicons-halflings-regular.woff
  1281. │   └── glyphicons-halflings-regular.woff2
  1282. └── js
  1283. ├── bootstrap.js
  1284. ├── bootstrap.min.js
  1285. └── npm.js</code></pre>
  1286. <p>它包含了JavaScript、CSS还有字体,需要注意的一点是bootstrap依赖于jquery。因此,我们需要下载jquery并放到这个目录里。然后在我们的head里引入这些css</p>
  1287. <pre><code>&lt;head&gt;
  1288. &lt;title&gt;{% block head_title %}Welcome to my blog{% endblock %}&lt;/title&gt;
  1289. &lt;link rel=&quot;stylesheet&quot; type=&quot;text/css&quot; href=&quot;{% static &#39;css/bootstrap.min.css&#39; %}&quot;&gt;
  1290. &lt;/head&gt;</code></pre>
  1291. <p>在我们的body结尾的地方:</p>
  1292. <pre><code>&lt;script src=&quot;{% static &#39;js/jquery.min.js&#39; %}&quot;&gt;&lt;/script&gt;
  1293. &lt;script src=&quot;{% static &#39;js/bootstrap.min.js&#39; %}&quot;&gt;&lt;/script&gt;
  1294. &lt;/body&gt;
  1295. &lt;/html&gt;</code></pre>
  1296. <p>在这里,将Script放在body的尾部有利于用户打开页面的速度。而对于一些纯前端的框架来说,它们就需要放在页面开始的地方。</p>
  1297. <h2 id="页面美化">页面美化</h2>
  1298. <p>现在,我们就可以创建一个导航了。</p>
  1299. <h3 id="添加导航">添加导航</h3>
  1300. <p>根据Bootstrap的官方文档的Demo,我们可以创建对应的导航。</p>
  1301. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw">&lt;header</span><span class="ot"> class=</span><span class="st">&quot;navbar navbar-static-top bs-docs-nav&quot;</span><span class="ot"> id=</span><span class="st">&quot;top&quot;</span><span class="ot"> role=</span><span class="st">&quot;banner&quot;</span><span class="kw">&gt;</span>
  1302. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;container&quot;</span><span class="kw">&gt;</span>
  1303. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;navbar-header&quot;</span><span class="kw">&gt;</span>
  1304. <span class="kw">&lt;button</span><span class="ot"> class=</span><span class="st">&quot;navbar-toggle collapsed&quot;</span><span class="ot"> type=</span><span class="st">&quot;button&quot;</span><span class="ot"> data-toggle=</span><span class="st">&quot;collapse&quot;</span>
  1305. <span class="ot"> data-target=</span><span class="st">&quot;.bs-navbar-collapse&quot;</span><span class="kw">&gt;</span>
  1306. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;sr-only&quot;</span><span class="kw">&gt;</span>切换视图<span class="kw">&lt;/span&gt;</span>
  1307. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;icon-bar&quot;</span><span class="kw">&gt;&lt;/span&gt;</span>
  1308. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;icon-bar&quot;</span><span class="kw">&gt;&lt;/span&gt;</span>
  1309. <span class="kw">&lt;span</span><span class="ot"> class=</span><span class="st">&quot;icon-bar&quot;</span><span class="kw">&gt;&lt;/span&gt;</span>
  1310. <span class="kw">&lt;/button&gt;</span>
  1311. <span class="kw">&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/&quot;</span><span class="ot"> class=</span><span class="st">&quot;navbar-brand&quot;</span><span class="kw">&gt;</span>Growth博客<span class="kw">&lt;/a&gt;</span>
  1312. <span class="kw">&lt;/div&gt;</span>
  1313. <span class="kw">&lt;nav</span><span class="ot"> class=</span><span class="st">&quot;collapse navbar-collapse bs-navbar-collapse&quot;</span><span class="ot"> role=</span><span class="st">&quot;navigation&quot;</span><span class="kw">&gt;</span>
  1314. <span class="kw">&lt;ul</span><span class="ot"> class=</span><span class="st">&quot;nav navbar-nav&quot;</span><span class="kw">&gt;</span>
  1315. <span class="kw">&lt;li&gt;</span>
  1316. <span class="kw">&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/pages/about/&quot;</span><span class="kw">&gt;</span>关于我<span class="kw">&lt;/a&gt;</span>
  1317. <span class="kw">&lt;/li&gt;</span>
  1318. <span class="kw">&lt;li&gt;</span>
  1319. <span class="kw">&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/pages/resume/&quot;</span><span class="kw">&gt;</span>简历<span class="kw">&lt;/a&gt;</span>
  1320. <span class="kw">&lt;/li&gt;</span>
  1321. <span class="kw">&lt;/ul&gt;</span>
  1322. <span class="kw">&lt;ul</span><span class="ot"> class=</span><span class="st">&quot;nav navbar-nav navbar-right&quot;</span><span class="kw">&gt;</span>
  1323. <span class="kw">&lt;li&gt;&lt;a</span><span class="ot"> href=</span><span class="st">&quot;/admin&quot;</span><span class="ot"> id=</span><span class="st">&quot;loginLink&quot;</span><span class="kw">&gt;</span>登入<span class="kw">&lt;/a&gt;&lt;/li&gt;</span>
  1324. <span class="kw">&lt;/ul&gt;</span>
  1325. <span class="kw">&lt;/nav&gt;</span>
  1326. <span class="kw">&lt;/div&gt;</span>
  1327. <span class="kw">&lt;/header&gt;</span></code></pre></div>
  1328. <p>它在桌面下的效果大致如下图所示:</p>
  1329. <figure>
  1330. <img src="./images/bootstrap-nav-desktop.png" alt="桌面浏览器下的Bootstrap导航" /><figcaption>桌面浏览器下的Bootstrap导航</figcaption>
  1331. </figure>
  1332. <p>而在移动浏览器下则是这样的效果:</p>
  1333. <figure>
  1334. <img src="./images/nav-in-mobile.png" alt="移动设备上的导航" /><figcaption>移动设备上的导航</figcaption>
  1335. </figure>
  1336. <p>当我们点击右上角的菜单按钮时,会出现我们的菜单</p>
  1337. <figure>
  1338. <img src="./images/nav-in-mobile-with-click.png" alt="点击导航后的结果" /><figcaption>点击导航后的结果</figcaption>
  1339. </figure>
  1340. <h3 id="添加标语">添加标语</h3>
  1341. <p>接着,我们可以快速的创建一个标语:</p>
  1342. <pre><code>&lt;main class=&quot;bs-docs-masthead&quot; id=&quot;content&quot; role=&quot;main&quot;&gt;
  1343. &lt;div class=&quot;container&quot;&gt;
  1344. &lt;div id=&quot;carbonads-container&quot;&gt;
  1345. THE ONLY FAIR IS NOT FAIR &lt;br&gt;
  1346. ENJOY CREATE &amp; SHARE
  1347. &lt;/div&gt;
  1348. &lt;/div&gt;
  1349. &lt;/main&gt;</code></pre>
  1350. <p>这里的代码都比较简单,我想也不需要太多的解释。</p>
  1351. <h3 id="优化列表">优化列表</h3>
  1352. <p>接着,我们可以简单的对首页的博客列表做一个优化,方法比较简单:</p>
  1353. <ul>
  1354. <li>为博客列表添加一个<code>row</code>的class,表示它可以滚动</li>
  1355. <li>在每一篇博客里添加<code>col-sm-4</code>的class,在不同的大小下会有不同的布局</li>
  1356. </ul>
  1357. <p>代码如下所示:</p>
  1358. <pre><code>{% extends &#39;base.html&#39; %}
  1359. {% block title %}Welcome to my blog{% endblock %}
  1360. {% block content %}
  1361. &lt;h2&gt;博客&lt;/h2&gt;
  1362. &lt;div class=&quot;row&quot;&gt;
  1363. {% if posts %}
  1364. {% for post in posts %}
  1365. &lt;div class=&quot;col-sm-4&quot;&gt;
  1366. &lt;h2&gt;&lt;a href=&quot;{{ post.get_absolute_url }}&quot;&gt;{{ post.title }}&lt;/a&gt;&lt;/h2&gt;
  1367. {{post.body | slice:&quot;:80&quot;}}
  1368. {{post.posted}} - By {{post.author}}
  1369. &lt;/div&gt;
  1370. {% endfor %}
  1371. {% else %}
  1372. &lt;p&gt;There are no posts.&lt;/p&gt;
  1373. {% endif %}
  1374. &lt;/div&gt;
  1375. {% endblock %}</code></pre>
  1376. <p>它在桌面和自动设备上的效果如下图所示:</p>
  1377. <figure>
  1378. <img src="./images/desktop-blogposts.png" alt="桌面设备效果" /><figcaption>桌面设备效果</figcaption>
  1379. </figure>
  1380. <figure>
  1381. <img src="./images/mobile-blogposts.png" alt="移动设备效果" /><figcaption>移动设备效果</figcaption>
  1382. </figure>
  1383. <h3 id="添加footer">添加footer</h3>
  1384. <p>最后,我们可以在页面的最下方添加一个footer,来做一些版权声明:</p>
  1385. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw">&lt;footer</span><span class="ot"> class=</span><span class="st">&quot;footer&quot;</span><span class="kw">&gt;</span>
  1386. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;container&quot;</span><span class="kw">&gt;</span>
  1387. <span class="kw">&lt;p</span><span class="ot"> class=</span><span class="st">&quot;text-muted&quot;</span><span class="kw">&gt;</span>@Copyright Phodal.com<span class="kw">&lt;/p&gt;</span>
  1388. <span class="kw">&lt;/div&gt;</span>
  1389. <span class="kw">&lt;/footer&gt;</span></code></pre></div>
  1390. <p>它拥有一些简单的样式,来将footer固定在页面的最下方:</p>
  1391. <div class="sourceCode"><pre class="sourceCode css"><code class="sourceCode css"><span class="fl">.footer</span> <span class="kw">{</span>
  1392. <span class="kw">position:</span> <span class="dt">absolute</span><span class="kw">;</span>
  1393. <span class="kw">bottom:</span> <span class="dt">0</span><span class="kw">;</span>
  1394. <span class="kw">width:</span> <span class="dt">100%</span><span class="kw">;</span>
  1395. <span class="co">/* Set the fixed height of the footer here */</span>
  1396. <span class="kw">height:</span> <span class="dt">60px</span><span class="kw">;</span>
  1397. <span class="kw">background-color:</span> <span class="dt">#f5f5f5</span><span class="kw">;</span>
  1398. <span class="kw">}</span>
  1399. <span class="fl">.footer</span> <span class="fl">.container</span> <span class="kw">{</span>
  1400. <span class="kw">width:</span> <span class="dt">auto</span><span class="kw">;</span>
  1401. <span class="kw">max-width:</span> <span class="dt">680px</span><span class="kw">;</span>
  1402. <span class="kw">padding:</span> <span class="dt">0</span> <span class="dt">15px</span><span class="kw">;</span>
  1403. <span class="kw">}</span>
  1404. <span class="fl">.footer</span> <span class="fl">.container</span> <span class="fl">.text-muted</span> <span class="kw">{</span>
  1405. <span class="kw">margin:</span> <span class="dt">20px</span> <span class="dt">0</span><span class="kw">;</span>
  1406. <span class="kw">}</span></code></pre></div>
  1407. <h1 id="应用api">应用API</h1>
  1408. <p>在下一章开始之前,我们先来搭建一下API平台,不仅仅可以提供一些额外的功能,还可以为我们的APP提供API。</p>
  1409. <h2 id="博客列表">博客列表</h2>
  1410. <h3 id="django-rest-framework">Django REST Framework</h3>
  1411. <p>在这里,我们需要用到一个名为Django REST Framework的RESTful API库。通过这个库,我们可以快速创建我们所需要的API。</p>
  1412. <p>Django REST Framework 这个名字很直白,就是基于 Django 的 REST 框架。因此,首先我们仍是要安装这个库:</p>
  1413. <pre><code>pip install djangorestframework</code></pre>
  1414. <p>然后把它添加到<code>INSTALLED_APPS</code>中:</p>
  1415. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">INSTALLED_APPS <span class="op">=</span> (
  1416. ...
  1417. <span class="st">&#39;rest_framework&#39;</span>,
  1418. )</code></pre></div>
  1419. <p>如下所示:</p>
  1420. <pre><code>INSTALLED_APPS = (
  1421. &#39;django.contrib.admin&#39;,
  1422. &#39;django.contrib.auth&#39;,
  1423. &#39;django.contrib.contenttypes&#39;,
  1424. &#39;django.contrib.sessions&#39;,
  1425. &#39;django.contrib.messages&#39;,
  1426. &#39;django.contrib.staticfiles&#39;,
  1427. &#39;rest_framework&#39;,
  1428. &#39;blogpost&#39;
  1429. )</code></pre>
  1430. <p>接着我们可以在我们的API中创建一个URL,用于匹配它的授权机制。</p>
  1431. <pre><code>urlpatterns = [
  1432. ...
  1433. url(r&#39;^api-auth/&#39;, include(&#39;rest_framework.urls&#39;, namespace=&#39;rest_framework&#39;))
  1434. ]</code></pre>
  1435. <p>不过这个API,目前并没有多大的用途。只有当我们在制作一些需要权限验证的接口时,它才会突显它的重要性。</p>
  1436. <h3 id="创建博客列表api">创建博客列表API</h3>
  1437. <p>为了方便我们继续展开后面的内容,我们先来创建一个博客列表API。参考Django REST Framework的官方文档,我们可以很快地创建出下面的Demo:</p>
  1438. <pre><code>from django.contrib.auth.models import User
  1439. from rest_framework import serializers, viewsets
  1440. from blogpost.models import Blogpost
  1441. class BlogpsotSerializer(serializers.HyperlinkedModelSerializer):
  1442. class Meta:
  1443. model = Blogpost
  1444. fields = (&#39;title&#39;, &#39;author&#39;, &#39;body&#39;, &#39;slug&#39;)
  1445. class BlogpostSet(viewsets.ModelViewSet):
  1446. queryset = Blogpost.objects.all()
  1447. serializer_class = BlogpsotSerializer</code></pre>
  1448. <p>在上面这个例子中,API由两个部分组成:</p>
  1449. <ul>
  1450. <li>ViewSets,用于定义视图的展现形式——如返回哪些内容,需要做哪些权限处理</li>
  1451. <li>Serializers,用于定义API的表现形式——如返回哪些字段,返回怎样的格式</li>
  1452. </ul>
  1453. <p>我们在我们的URL中,会定义相应的规则到ViewSet,而ViewSet则通过<code>serializer_class</code>找到对应的<code>Serializers</code>。我们将Blogpost的所有对象赋予queryset,并返回这些值。在BlogpsotSerializer中,我们定义了我们要返回的几个字段:<code>title</code>、<code>author</code>、<code>body</code>、<code>slug</code>。</p>
  1454. <p>接着,我们可以在我们的<code>urls.py</code>配置URL。</p>
  1455. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">...
  1456. <span class="im">from</span> rest_framework <span class="im">import</span> routers
  1457. <span class="im">from</span> blogpost.api <span class="im">import</span> BlogpostSet
  1458. apiRouter <span class="op">=</span> routers.DefaultRouter()
  1459. apiRouter.register(<span class="vs">r&#39;blogpost&#39;</span>, BlogpostSet)
  1460. urlpatterns <span class="op">=</span> [,
  1461. ...
  1462. url(<span class="vs">r&#39;^api/&#39;</span>, include(apiRouter.urls)),
  1463. ] <span class="op">+</span> static(settings.STATIC_URL, document_root<span class="op">=</span>settings.STATIC_ROOT)</code></pre></div>
  1464. <p>我们使用默认的Router来配置我们的URL,即DefaultRouter,它提供了一个非常简单的机制来自动检测URL规则。因此,我们只需要注册好我们的url——<code>blogpost</code>以及它值<code>BlogpostSet</code>即可。随后,我们再为其定义一个根URL即可:</p>
  1465. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">url(<span class="vs">r&#39;^api/&#39;</span>, include(apiRouter.urls))</code></pre></div>
  1466. <h3 id="测试-api">测试 API</h3>
  1467. <p>现在,我们可以访问<a href="http://127.0.0.1:8000/api/" class="uri">http://127.0.0.1:8000/api/</a>来访问我们现在的API。由于Django REST Framework提供了一个UI机制,所以我们可以在网页上直接看到我们所有的API:</p>
  1468. <figure>
  1469. <img src="./images/django-rest-framework-api-lists.png" alt="Django REST Framework列表" /><figcaption>Django REST Framework列表</figcaption>
  1470. </figure>
  1471. <p>然后,点击页面中的<a href="http://127.0.0.1:8000/api/blogpost/" class="uri">http://127.0.0.1:8000/api/blogpost/</a>,我们就可以访问博客相关的API了,如下图所示:</p>
  1472. <figure>
  1473. <img src="./images/drf-blogppost-set-list.png" alt="博客API" /><figcaption>博客API</figcaption>
  1474. </figure>
  1475. <p>在页面上显示了所有的博客内容,在页面的下面有一个表单可以先让我们来创建数据:</p>
  1476. <figure>
  1477. <img src="./images/api-post-form.png" alt="创建博客的表单" /><figcaption>创建博客的表单</figcaption>
  1478. </figure>
  1479. <p>直接在表单中添加数据,我们就可以完成数据创建了。</p>
  1480. <p>当然,我们也可以直接用命令行工具来测试,执行:</p>
  1481. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">curl</span> -i http://127.0.0.1:8000/api/blogpost/</code></pre></div>
  1482. <p>即可返回相应的结果:</p>
  1483. <figure>
  1484. <img src="./images/curl-api.png" alt="CuRL API" /><figcaption>CuRL API</figcaption>
  1485. </figure>
  1486. <h2 id="自动完成">自动完成</h2>
  1487. <p>AutoComplete是一个很有意思的功能,特别是当我们的文章很多的时候,我们可以让读者有机会能搜索到相应的功能。以Google为例,Google在我们输入一些关键字的时候,会向我们推荐一些比较流行的词条可以让我们选择。</p>
  1488. <figure>
  1489. <img src="./images/google-autocomplete.png" alt="Google AutoComplete" /><figcaption>Google AutoComplete</figcaption>
  1490. </figure>
  1491. <p>同样的,我们也可以实现一个同样的效果用于我们的博客搜索:</p>
  1492. <figure>
  1493. <img src="./images/autocomplete-example.png" alt="自动完成" /><figcaption>自动完成</figcaption>
  1494. </figure>
  1495. <p>当我们输入某一些关键字的时候,就会出现文章的标题,随后我们只需要点击相应的标题即可跳转到文章。</p>
  1496. <h3 id="搜索api">搜索API</h3>
  1497. <p>为了实现这个功能我们需要对之前的博客API做一些简单的改造——可以支持搜索博客标题。这里我们需要稍微扩展一下我们的博客API即可:</p>
  1498. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">class</span> BlogpostSet(viewsets.ModelViewSet):
  1499. permission_classes <span class="op">=</span> (permissions.IsAuthenticatedOrReadOnly,)
  1500. serializer_class <span class="op">=</span> BlogpsotSerializer
  1501. search_fields <span class="op">=</span> <span class="st">&#39;title&#39;</span>
  1502. <span class="kw">def</span> <span class="bu">list</span>(<span class="va">self</span>, request):
  1503. queryset <span class="op">=</span> Blogpost.objects.<span class="bu">all</span>()
  1504. search_param <span class="op">=</span> <span class="va">self</span>.request.query_params.get(<span class="st">&#39;title&#39;</span>, <span class="va">None</span>)
  1505. <span class="cf">if</span> search_param <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:
  1506. queryset <span class="op">=</span> Blogpost.objects.<span class="bu">filter</span>(title__contains<span class="op">=</span>search_param)
  1507. serializer <span class="op">=</span> BlogpsotSerializer(queryset, many<span class="op">=</span><span class="va">True</span>)
  1508. <span class="cf">return</span> Response(serializer.data)</code></pre></div>
  1509. <p>我们添加了一个名为<code>search_fields</code>的变量,顾名思义就是定义搜索字段。接着我们覆写了ModelViewSet的list方法,它是用于列出(list)所有的结果。我们会尝试在我们的请求中获取搜索参量,如果没有的话我们就返回所有的结果。如果搜索的参数中含有标题,则从所有博客中过滤出标题中含有搜索标题中的内容,再返回这些结果。如下是一个搜索的URL:<a href="http://127.0.0.1:8000/api/blogpost/?format=json&amp;title=test" class="uri">http://127.0.0.1:8000/api/blogpost/?format=json&amp;title=test</a>,我们搜索标题中含有<code>test</code>的内容。</p>
  1510. <p>同时,我们还需要为我们的apiRouter设置一个basename,即下面代码中最后的<code>Blogpost</code></p>
  1511. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">apiRouter.register(<span class="vs">r&#39;blogpost&#39;</span>, BlogpostSet, <span class="st">&#39;Blogpost&#39;</span>)</code></pre></div>
  1512. <h3 id="页面实现">页面实现</h3>
  1513. <p>接着,我们就可以在页面上实现这个功能。在这里我们使用一个名为<a href="https://github.com/bassjobsen/Bootstrap-3-Typeahead">Bootstrap-3-Typeahead</a>的插件来实现,下载这个插件以及它对应的CSS:<a href="https://github.com/bassjobsen/typeahead.js-bootstrap-css" class="uri">https://github.com/bassjobsen/typeahead.js-bootstrap-css</a>,并添加到<code>base.html</code>中,然后创建一个<code>main.js</code>文件负责相关的逻辑处理。</p>
  1514. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/jquery.min.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span>
  1515. <span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/bootstrap.min.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span>
  1516. <span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/bootstrap3-typeahead.min.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span>
  1517. <span class="kw">&lt;script</span><span class="ot"> src=</span><span class="st">&quot;{% static &#39;js/main.js&#39; %}&quot;</span><span class="kw">&gt;&lt;/script&gt;</span></code></pre></div>
  1518. <p>接着我们需要在页面上创建对应的UI,我们可以直接在<code>登录</code>后面添加这个搜索按钮:</p>
  1519. <pre><code>&lt;nav class=&quot;collapse navbar-collapse bs-navbar-collapse&quot; role=&quot;navigation&quot;&gt;
  1520. &lt;ul class=&quot;nav navbar-nav&quot;&gt;
  1521. &lt;li&gt;
  1522. &lt;a href=&quot;/pages/about/&quot;&gt;关于我&lt;/a&gt;
  1523. &lt;/li&gt;
  1524. &lt;li&gt;
  1525. &lt;a href=&quot;/pages/resume/&quot;&gt;简历&lt;/a&gt;
  1526. &lt;/li&gt;
  1527. &lt;/ul&gt;
  1528. &lt;ul class=&quot;nav navbar-nav navbar-right&quot;&gt;
  1529. &lt;li&gt;&lt;a href=&quot;/admin&quot; id=&quot;loginLink&quot;&gt;登录&lt;/a&gt;&lt;/li&gt;
  1530. &lt;/ul&gt;
  1531. &lt;div class=&quot;col-sm-3 col-md-3 pull-right&quot;&gt;
  1532. &lt;form class=&quot;navbar-form&quot; role=&quot;search&quot;&gt;
  1533. &lt;div class=&quot;input-group&quot;&gt;
  1534. &lt;input type=&quot;text&quot; id=&quot;typeahead-input&quot; class=&quot;form-control&quot; placeholder=&quot;Search&quot; name=&quot;search&quot; data-provide=&quot;typeahead&quot;&gt;
  1535. &lt;div class=&quot;input-group-btn&quot;&gt;
  1536. &lt;button class=&quot;btn btn-default search-button&quot; type=&quot;submit&quot;&gt;&lt;i class=&quot;glyphicon glyphicon-search&quot;&gt;&lt;/i&gt;&lt;/button&gt;
  1537. &lt;/div&gt;
  1538. &lt;/div&gt;
  1539. &lt;/form&gt;
  1540. &lt;/div&gt;
  1541. &lt;/nav&gt;</code></pre>
  1542. <p>我们主要是使用input标签,标签上对应有一个id</p>
  1543. <pre><code>&lt;input type=&quot;text&quot; id=&quot;typeahead-input&quot; class=&quot;form-control&quot; placeholder=&quot;Search&quot; </code></pre>
  1544. <p>对应于这个ID,我们就可以开始编写我们的功能了:</p>
  1545. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="at">$</span>(document).<span class="at">ready</span>(<span class="kw">function</span> () <span class="op">{</span>
  1546. <span class="at">$</span>(<span class="st">&#39;#typeahead-input&#39;</span>).<span class="at">typeahead</span>(<span class="op">{</span>
  1547. <span class="dt">source</span><span class="op">:</span> <span class="kw">function</span> (query<span class="op">,</span> process) <span class="op">{</span>
  1548. <span class="cf">return</span> <span class="va">$</span>.<span class="at">get</span>(<span class="st">&#39;/api/blogpost/?format=json&amp;title=&#39;</span> <span class="op">+</span> query<span class="op">,</span> <span class="kw">function</span> (data) <span class="op">{</span>
  1549. <span class="cf">return</span> <span class="at">process</span>(data)<span class="op">;</span>
  1550. <span class="op">}</span>)<span class="op">;</span>
  1551. <span class="op">},</span>
  1552. <span class="dt">updater</span><span class="op">:</span> <span class="kw">function</span> (item) <span class="op">{</span>
  1553. <span class="cf">return</span> item<span class="op">;</span>
  1554. <span class="op">},</span>
  1555. <span class="dt">displayText</span><span class="op">:</span> <span class="kw">function</span> (item) <span class="op">{</span>
  1556. <span class="cf">return</span> <span class="va">item</span>.<span class="at">title</span><span class="op">;</span>
  1557. <span class="op">},</span>
  1558. <span class="dt">afterSelect</span><span class="op">:</span> <span class="kw">function</span> (item) <span class="op">{</span>
  1559. <span class="va">location</span>.<span class="at">href</span> <span class="op">=</span> <span class="st">&#39;http://localhost:8000/blog/&#39;</span> <span class="op">+</span> <span class="va">item</span>.<span class="at">slug</span> <span class="op">+</span> <span class="st">&quot;.html&quot;</span><span class="op">;</span>
  1560. <span class="op">},</span>
  1561. <span class="dt">delay</span><span class="op">:</span> <span class="dv">500</span>
  1562. <span class="op">}</span>)<span class="op">;</span>
  1563. <span class="op">}</span>)<span class="op">;</span></code></pre></div>
  1564. <p>$(document).ready()方法可以是在DOM完成加载后,运行其中的函数。接着我们开始监听<code>#typeahead-input</code>,对应的便是id为<code>typeahead-input</code>的元素。可以看到在这其中有五个对象:</p>
  1565. <ul>
  1566. <li>source,即搜索的来源,我们返回的是我们搜索的URL。</li>
  1567. <li>updater,即每次更新要做的事</li>
  1568. <li>displayText,显示在页面上的内容,如在这里我们返回的是博客的标题</li>
  1569. <li>afterSelect,每用户选中某一项后做的事,这里我们直接中转到对应的博客。</li>
  1570. <li>delay,延时500ms。</li>
  1571. </ul>
  1572. <p>虽然我们使用的是插件来完成我们的功能,但是总体的处理逻辑是:</p>
  1573. <ol type="1">
  1574. <li>监听我们的输入文本</li>
  1575. <li>获取API的返回结果</li>
  1576. <li>对返回结果进行处理——如高亮输入文本、显示到页面上</li>
  1577. <li>处理用户点击事件</li>
  1578. </ol>
  1579. <h2 id="跨域支持">跨域支持</h2>
  1580. <p>当我们想为其他的网页提供我们的API时,可能会报错——原因是不支持跨域请求。为了方便我们下一章更好的展开,内容我们在这里对跨域进行支持。</p>
  1581. <h3 id="添加跨域支持">添加跨域支持</h3>
  1582. <p>有一个名为<code>django-cors-headers</code>的插件用于实现对跨域请求的支持,我们只需要安装它,并进行一些简单的配置即可。</p>
  1583. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">pip</span> install django-cors-headers</code></pre></div>
  1584. <p>安装过程如下:</p>
  1585. <pre><code>Collecting django-cors-headers
  1586. Downloading django-cors-headers-1.1.0.tar.gz
  1587. Building wheels for collected packages: django-cors-headers
  1588. Running setup.py bdist_wheel for django-cors-headers ... done
  1589. Stored in directory: /Users/fdhuang/Library/Caches/pip/wheels/b0/75/89/7b17f134fc01b74e10523f3128e45b917da0c5f8638213e073
  1590. Successfully built django-cors-headers
  1591. Installing collected packages: django-cors-headers
  1592. Successfully installed django-cors-headers-1.1.0</code></pre>
  1593. <p>我们还需要添加到<code>django-cors-headers=1.1.0</code>到<code>requirements.txt</code>文件中,以及添加到<code>settings.py</code>中:</p>
  1594. <pre><code>INSTALLED_APPS = (
  1595. ...
  1596. &#39;corsheaders&#39;,
  1597. ...
  1598. )</code></pre>
  1599. <p>以及对应的中间件:</p>
  1600. <pre><code>MIDDLEWARE_CLASSES = (
  1601. ...
  1602. &#39;corsheaders.middleware.CorsMiddleware&#39;,
  1603. &#39;django.middleware.common.CommonMiddleware&#39;,
  1604. ...
  1605. )</code></pre>
  1606. <p>同时还有对应的配置:</p>
  1607. <pre><code>CORS_ALLOW_CREDENTIALS = True</code></pre>
  1608. <p>现在,让我们进行下一步,开始APP吧!</p>
  1609. <h1 id="创建移动应用">创建移动应用</h1>
  1610. <p>依照国际惯例,我们还将用Ionic 2继续创建hello,world。</p>
  1611. <h2 id="helloworld">hello,world</h2>
  1612. <p>开始之前我们需要先安装Ionic的命令行工具,后面我们需要用这个工具来创建工程。</p>
  1613. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">npm</span> install -g ionic@beta</code></pre></div>
  1614. <p>如果没有意外,我们将安装成功,然后可以使用<code>ionic</code>命令:</p>
  1615. <p>它自带了一系列的工具来加速我们的开发,这些工具可以在后面的章节中学习到。</p>
  1616. <pre><code>Available tasks: (use --help or -h for more info)
  1617. start .......... Starts a new Ionic project in the specified PATH
  1618. serve .......... Start a local development server for app dev/testing
  1619. platform ....... Add platform target for building an Ionic app
  1620. run ............ Run an Ionic project on a connected device
  1621. emulate ........ Emulate an Ionic project on a simulator or emulator
  1622. build .......... Build (prepare + compile) an Ionic project for a given platform.
  1623. plugin ......... Add a Cordova plugin
  1624. resources ...... Automatically create icon and splash screen resources (beta)
  1625. Put your images in the ./resources directory, named splash or icon.
  1626. Accepted file types are .png, .ai, and .psd.
  1627. Icons should be 192x192 px without rounded corners.
  1628. Splashscreens should be 2208x2208 px, with the image centered in the middle.
  1629. upload ......... Upload an app to your Ionic account
  1630. share .......... Share an app with a client, co-worker, friend, or customer
  1631. lib ............ Gets Ionic library version or updates the Ionic library
  1632. setup .......... Configure the project with a build tool (beta)
  1633. io ............. Integrate your app with the ionic.io platform services (alpha)
  1634. security ....... Store your app&#39;s credentials for the Ionic Platform (alpha)
  1635. push ........... Upload APNS and GCM credentials to Ionic Push (alpha)
  1636. package ........ Use Ionic Package to build your app (alpha)
  1637. config ......... Set configuration variables for your ionic app (alpha)
  1638. browser ........ Add another browser for a platform (beta)
  1639. service ........ Add an Ionic service package and install any required plugins
  1640. add ............ Add an Ion, bower component, or addon to the project
  1641. remove ......... Remove an Ion, bower component, or addon from the project
  1642. list ........... List Ions, bower components, or addons in the project
  1643. info ........... List information about the users runtime environment
  1644. help ........... Provides help for a certain command
  1645. link ........... Sets your Ionic App ID for your project
  1646. hooks .......... Manage your Ionic Cordova hooks
  1647. state .......... Saves or restores state of your Ionic Application using the package.json file
  1648. docs ........... Opens up the documentation for Ionic
  1649. generate ....... Generate pages and components</code></pre>
  1650. <p>现在,我们就可以用第一个命令<code>start</code>来创建我们的项目。</p>
  1651. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">ionic</span> start growth-blog-app --v2</code></pre></div>
  1652. <p>在这个过程中,它将下载Ionic 2项目的基础项目,并执行安装命令。</p>
  1653. <pre><code>Creating Ionic app in folder /Users/fdhuang/repractise/growth-blog-app based on tabs project
  1654. Downloading: https://github.com/driftyco/ionic2-app-base/archive/master.zip
  1655. [=============================] 100% 0.0s
  1656. Downloading: https://github.com/driftyco/ionic2-starter-tabs/archive/master.zip
  1657. [=============================] 100% 0.0s
  1658. Installing npm packages...</code></pre>
  1659. <p>然后到<code>growth-blog-app</code>目录,我们会看到类似于下面的内容:</p>
  1660. <pre><code>.
  1661. ├── README.md
  1662. ├── app
  1663. │   ├── app.js
  1664. │   ├── pages
  1665. │   │   ├── page1
  1666. │   │   │   ├── page1.html
  1667. │   │   │   ├── page1.js
  1668. │   │   │   └── page1.scss
  1669. │   │   ├── page2
  1670. │   │   │   ├── page2.html
  1671. │   │   │   ├── page2.js
  1672. │   │   │   └── page2.scss
  1673. │   │   ├── page3
  1674. │   │   │   ├── page3.html
  1675. │   │   │   ├── page3.js
  1676. │   │   │   └── page3.scss
  1677. │   │   └── tabs
  1678. │   │   ├── tabs.html
  1679. │   │   └── tabs.js
  1680. │   └── theme
  1681. │   ├── app.core.scss
  1682. │   ├── app.ios.scss
  1683. │   ├── app.md.scss
  1684. │   ├── app.variables.scss
  1685. │   └── app.wp.scss
  1686. ├── config.xml
  1687. ├── gulpfile.js
  1688. ├── hooks
  1689. │   ├── README.md
  1690. │   └── after_prepare
  1691. │   └── 010_add_platform_class.js
  1692. ├── ionic.config.json
  1693. ├── package.json
  1694. └── www
  1695. └── index.html</code></pre>
  1696. <p>在这2.0版本的Ionic,页面开始以目录来划分,一个页面路径下有自己的<code>html</code>、<code>js</code>、<code>scss</code>。</p>
  1697. <ul>
  1698. <li><code>tabs</code>负责这些页面间跳转</li>
  1699. <li><code>theme</code>则负责系统相应样式的修改</li>
  1700. <li><code>config.xml</code>带有相应的Cordova配置</li>
  1701. <li><code>hooks</code>则对系统添加和编译时进行一些预处理</li>
  1702. <li><code>ionic.config.json</code>则是ionic的一些相关配置选项</li>
  1703. <li><code>package.json</code>则存放相应的node.js的包的依赖</li>
  1704. <li><code>www</code>目录用于存放出最后构建出来的内容,以及一些静态资源</li>
  1705. </ul>
  1706. <p>由于Angular 2.0使用的是Typescript,所以在这里我们将用typescript进行展示,因此我们的执行命令变成~~:</p>
  1707. <pre><code>ionic start growth-blog-app --v2 --ts</code></pre>
  1708. <p><code>--ts</code>表示使用的是<code>typescript</code>来创建项目,安装的过程是一样的,不一样的是后面写的代码。</p>
  1709. <p>执行相应的启动serve命令,我们就可以开始我们的项目了:</p>
  1710. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">ionic</span> serve</code></pre></div>
  1711. <p>这时候Ionic将做一些额外的事,才能启动我们的服务,如:</p>
  1712. <ul>
  1713. <li>删除<code>www/build</code>目录下的文件</li>
  1714. <li>编译SASS到CSS</li>
  1715. <li>编译文件到HTML</li>
  1716. <li>编译字体</li>
  1717. <li>等等</li>
  1718. </ul>
  1719. <p>最后,它将启动一个Web服务,URL为<a href="http://localhost:8100" class="uri">http://localhost:8100</a></p>
  1720. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"> <span class="ex">Running</span> <span class="st">&#39;serve:before&#39;</span> gulp task before serve
  1721. [<span class="ex">20</span>:59:16] Starting <span class="st">&#39;clean&#39;</span>...
  1722. [<span class="ex">20</span>:59:16] Finished <span class="st">&#39;clean&#39;</span> after 6.07 ms
  1723. [<span class="ex">20</span>:59:16] Starting <span class="st">&#39;watch&#39;</span>...
  1724. [<span class="ex">20</span>:59:16] Starting <span class="st">&#39;sass&#39;</span>...
  1725. [<span class="ex">20</span>:59:16] Starting <span class="st">&#39;html&#39;</span>...
  1726. [<span class="ex">20</span>:59:16] Starting <span class="st">&#39;fonts&#39;</span>...
  1727. [<span class="ex">20</span>:59:16] Starting <span class="st">&#39;scripts&#39;</span>...
  1728. [<span class="ex">20</span>:59:16] Finished <span class="st">&#39;scripts&#39;</span> after 43 ms
  1729. [<span class="ex">20</span>:59:16] Finished <span class="st">&#39;html&#39;</span> after 51 ms
  1730. [<span class="ex">20</span>:59:16] Finished <span class="st">&#39;fonts&#39;</span> after 54 ms
  1731. [<span class="ex">20</span>:59:16] Finished <span class="st">&#39;sass&#39;</span> after 738 ms
  1732. <span class="ex">7.6</span> MB bytes written (5.62 seconds)
  1733. [<span class="ex">20</span>:59:22] Finished <span class="st">&#39;watch&#39;</span> after 6.62 s
  1734. [<span class="ex">20</span>:59:22] Starting <span class="st">&#39;serve:before&#39;</span>...
  1735. [<span class="ex">20</span>:59:22] Finished <span class="st">&#39;serve:before&#39;</span> after 3.87 μs
  1736. <span class="ex">Running</span> live reload server: http://localhost:35729
  1737. <span class="ex">Watching</span>: www/**/*, !www/lib/**/*
  1738. √ <span class="ex">Running</span> dev server: http://localhost:8100
  1739. <span class="ex">Ionic</span> server commands, enter:
  1740. <span class="ex">restart</span> or r to restart the client app from the root
  1741. <span class="ex">goto</span> or g and a url to have the app navigate to the given url
  1742. <span class="ex">consolelogs</span> or c to enable/disable console log output
  1743. <span class="ex">serverlogs</span> or s to enable/disable server log output
  1744. <span class="ex">quit</span> or q to shutdown the server and exit
  1745. <span class="ex">ionic</span> $</code></pre></div>
  1746. <p>接着,就可以打开相应的Web页面,如下图所示:</p>
  1747. <figure>
  1748. <img src="./images/ionic-web-view.jpg" alt="Ionic Web预览界面" /><figcaption>Ionic Web预览界面</figcaption>
  1749. </figure>
  1750. <h3 id="构建应用">构建应用</h3>
  1751. <p>由于Ionic是基于Cordova的,我们需要安装Cordova业完成后续的工作。</p>
  1752. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="fu">sudo</span> npm install -g cordova</code></pre></div>
  1753. <p>为了构建不同的平台的应用,我们就需要添加不同的平台,如:</p>
  1754. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">ionic</span> platform add android</code></pre></div>
  1755. <p>上面的命令可以为项目添加Android平台的支持,过程如下面的日志所示:</p>
  1756. <pre><code>Adding android project...
  1757. Creating Cordova project for the Android platform:
  1758. Path: platforms/android
  1759. Package: io.ionic.starter
  1760. Name: V2_Test
  1761. Activity: MainActivity
  1762. Android target: android-23
  1763. Android project created with cordova-android@5.1.1
  1764. Running command: /Users/fdhuang/repractise/growth-blog-app/hooks/after_prepare/010_add_platform_class.js /Users/fdhuang/repractise/growth-blog-app</code></pre>
  1765. <p>最后,再执行<code>run</code>就可以在对应的平台上运行,如:</p>
  1766. <pre><code>ionic run android</code></pre>
  1767. <h2 id="博客列表页">博客列表页</h2>
  1768. <p>现在,让我们来结合我们的博客APP,做一个相应的展示博客的APP。在这一步我们所要做的事情比较简单:</p>
  1769. <ul>
  1770. <li>获取博客列表API</li>
  1771. <li>渲染博客列表</li>
  1772. </ul>
  1773. <h3 id="列表页">列表页</h3>
  1774. <p>在上一个章节里我们已经有了一个博客详细的API,我们只需要获取这个API并显示即可。不过,让我们简单地熟悉一下显示数据的这部分内容:</p>
  1775. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw">&lt;ion-navbar</span> <span class="er">*navbar</span><span class="kw">&gt;</span>
  1776. <span class="kw">&lt;ion-title&gt;</span>博客<span class="kw">&lt;/ion-title&gt;</span>
  1777. <span class="kw">&lt;/ion-navbar&gt;</span>
  1778. <span class="kw">&lt;ion-content</span><span class="ot"> class=</span><span class="st">&quot;blog-list&quot;</span><span class="kw">&gt;</span>
  1779. <span class="kw">&lt;ion-item</span> <span class="er">*ngFor</span><span class="ot">=</span><span class="st">&quot;#blogpost of blogposts&quot;</span><span class="kw">&gt;</span>
  1780. <span class="kw">&lt;h1</span> <span class="er">*ngIf</span><span class="ot">=</span><span class="st">&quot;blogpost&quot;</span><span class="kw">&gt;</span>
  1781. {{blogpost.title}}
  1782. <span class="kw">&lt;/h1&gt;</span>
  1783. <span class="kw">&lt;p</span> <span class="er">*ngIf</span><span class="ot">=</span><span class="st">&quot;blogpost&quot;</span><span class="kw">&gt;</span>
  1784. {{blogpost.body}}
  1785. <span class="kw">&lt;/p&gt;</span>
  1786. <span class="kw">&lt;/ion-item&gt;</span>
  1787. <span class="kw">&lt;/ion-content&gt;</span></code></pre></div>
  1788. <p>上面是一个基本的详情页的模板,其中定义了一系列的Ionic自定义标签,如:</p>
  1789. <ul>
  1790. <li><ion-navbar> 显示在导航栏中的内容</li>
  1791. <li><ion-content> 显示APP的内容</li>
  1792. <li><ion-item> 即将博客展示成每一项</li>
  1793. </ul>
  1794. <p>而从上面的内容中,我们可以看到:我们在ngFor中遍历了blogposts,然后显示每篇文章的标题和内容。对应的代码也就比较简单了:</p>
  1795. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="im">import</span> <span class="op">{</span>Page<span class="op">}</span> <span class="im">from</span> <span class="st">&#39;ionic-angular&#39;</span><span class="op">;</span>
  1796. @<span class="at">Page</span>(<span class="op">{</span>
  1797. <span class="dt">templateUrl</span><span class="op">:</span> <span class="st">&#39;build/pages/blog/list/index.html&#39;</span><span class="op">,</span>
  1798. <span class="dt">providers</span><span class="op">:</span> [BlogpostServices]
  1799. <span class="op">}</span>)
  1800. <span class="im">export</span> <span class="kw">class</span> BlogList <span class="op">{</span>
  1801. <span class="kw">public</span> blogposts<span class="op">;</span>
  1802. <span class="at">constructor</span>() <span class="op">{</span>
  1803. <span class="op">}</span>
  1804. <span class="op">}</span></code></pre></div>
  1805. <p>但是我们要去哪里获取博客的值呢,我们先看看改造后的BlogList的Controller:</p>
  1806. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="im">import</span> <span class="op">{</span>Page<span class="op">}</span> <span class="im">from</span> <span class="st">&#39;ionic-angular&#39;</span><span class="op">;</span>
  1807. <span class="im">import</span> <span class="op">{</span>BlogpostServices<span class="op">}</span> <span class="im">from</span> <span class="st">&#39;../../../services/BlogpostServices&#39;</span><span class="op">;</span>
  1808. @<span class="at">Page</span>(<span class="op">{</span>
  1809. <span class="dt">templateUrl</span><span class="op">:</span> <span class="st">&#39;build/pages/blog/list/index.html&#39;</span><span class="op">,</span>
  1810. <span class="dt">providers</span><span class="op">:</span> [BlogpostServices]
  1811. <span class="op">}</span>)
  1812. <span class="im">export</span> <span class="kw">class</span> BlogList <span class="op">{</span>
  1813. <span class="kw">private</span> blogListService<span class="op">;</span>
  1814. <span class="kw">public</span> blogposts<span class="op">;</span>
  1815. <span class="at">constructor</span>(<span class="dt">blogpostServices</span><span class="op">:</span>BlogpostServices) <span class="op">{</span>
  1816. <span class="kw">this</span>.<span class="at">blogListService</span> <span class="op">=</span> blogpostServices<span class="op">;</span>
  1817. <span class="kw">this</span>.<span class="at">initService</span>()<span class="op">;</span>
  1818. <span class="op">}</span>
  1819. <span class="kw">private</span> <span class="at">initService</span>() <span class="op">{</span>
  1820. <span class="kw">this</span>.<span class="va">blogListService</span>.<span class="at">getBlogpostLists</span>().<span class="at">subscribe</span>(
  1821. data <span class="op">=&gt;</span> <span class="op">{</span><span class="kw">this</span>.<span class="at">blogposts</span> <span class="op">=</span> <span class="va">JSON</span>.<span class="at">parse</span>(<span class="va">data</span>.<span class="at">_body</span>)<span class="op">;},</span>
  1822. err <span class="op">=&gt;</span> <span class="va">console</span>.<span class="at">log</span>(<span class="st">&#39;Error: &#39;</span> <span class="op">+</span> <span class="va">JSON</span>.<span class="at">stringify</span>(err))<span class="op">,</span>
  1823. () <span class="op">=&gt;</span> <span class="va">console</span>.<span class="at">log</span>(<span class="st">&#39;Get Blogpost&#39;</span>)
  1824. )<span class="op">;</span>
  1825. <span class="op">}</span>
  1826. <span class="op">}</span></code></pre></div>
  1827. <p>我们初始化了一个blogListService,然后我们调用这个服务去获取博客列表。</p>
  1828. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"> <span class="kw">this</span>.<span class="va">blogListService</span>.<span class="at">getBlogpostLists</span>().<span class="at">subscribe</span>(
  1829. data <span class="op">=&gt;</span> <span class="op">{</span><span class="kw">this</span>.<span class="at">blogposts</span> <span class="op">=</span> <span class="va">JSON</span>.<span class="at">parse</span>(<span class="va">data</span>.<span class="at">_body</span>)<span class="op">;},</span>
  1830. err <span class="op">=&gt;</span> <span class="va">console</span>.<span class="at">log</span>(<span class="st">&#39;Error: &#39;</span> <span class="op">+</span> <span class="va">JSON</span>.<span class="at">stringify</span>(err))<span class="op">,</span>
  1831. () <span class="op">=&gt;</span> <span class="va">console</span>.<span class="at">log</span>(<span class="st">&#39;Get Blogpost&#39;</span>)
  1832. )<span class="op">;</span></code></pre></div>
  1833. <p>当我们获取到数据的时候,我们就解析这个数据,并将这个值赋予blogposts。如果这其中遇到什么错误,就会显示相应的错误信息。</p>
  1834. <p>现在,让我们创建一个获取博客的服务:</p>
  1835. <pre><code>import {Inject} from &#39;angular2/core&#39;;
  1836. import {Http} from &#39;angular2/http&#39;;
  1837. import &#39;rxjs/add/operator/map&#39;;
  1838. export class BlogpostServices {
  1839. private http;
  1840. constructor(@Inject(Http) http:Http) {
  1841. this.http = http
  1842. }
  1843. getBlogpostLists() {
  1844. var url = &#39;http://127.0.0.1:8000/api/blogpost/?format=json&#39;;
  1845. return this.http.get(url).map(res =&gt; res);
  1846. }
  1847. }</code></pre>
  1848. <p>我们将通过这个API来获取相关的数据,并将数据返回到BlogList类中。接着将更新blogposts的值,并重新渲染页面。</p>
  1849. <h3 id="详情页">详情页</h3>
  1850. <p>在我们的博客API中,每个内容都对应有一个id,如下所示:</p>
  1851. <pre><code>{
  1852. &quot;title&quot;: &quot;这是一个标题&quot;,
  1853. &quot;author&quot;: &quot;Phodal2&quot;,
  1854. &quot;body&quot;: &quot;这是一个测试的内容&quot;,
  1855. &quot;slug&quot;: &quot;this-is-a-test&quot;,
  1856. &quot;id&quot;: 3
  1857. }</code></pre>
  1858. <p>我们只需要访问这个id,就可以获取这个结果,如:<a href="http://localhost:8000/api/blogpost/3/" class="uri">http://localhost:8000/api/blogpost/3/</a></p>
  1859. <p>因此,我们所需要做的就是:</p>
  1860. <ul>
  1861. <li>在渲染博客列表的时候,为每一项赋予一个ID</li>
  1862. <li>点击某一项时,将跳转到详情页,并去获取相应的API的数据,并渲染到页面上。</li>
  1863. </ul>
  1864. <p>好了,我们可以用ionic的生成命令来创建博客详情页。</p>
  1865. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">ionic</span> g page blog-detail --ts</code></pre></div>
  1866. <p>它将在app/pages目录下,生成下面的内容:</p>
  1867. <div class="sourceCode"><pre class="sourceCode bash"><code class="sourceCode bash"><span class="ex">app/pages/blog-detail/</span>
  1868. ├── <span class="ex">blog-detail.html</span>
  1869. ├── <span class="ex">blog-detail.ts</span>
  1870. └── <span class="ex">blog-detail.scss</span></code></pre></div>
  1871. <p>我们可以遵循之前添加Django App的习惯,先添加Router。因此我们可以在<code>app.ts</code>添加新的Route:</p>
  1872. <pre><code>const ROUTES = [
  1873. {path: &#39;/app/blog/:id&#39;, component: BlogDetailPage}
  1874. ];
  1875. @App({
  1876. template: &#39;&lt;ion-nav [root]=&quot;rootPage&quot;&gt;&lt;/ion-nav&gt;&#39;,
  1877. config: {}
  1878. })
  1879. @RouteConfig(ROUTES)
  1880. export class MyApp {
  1881. rootPage:any = TabsPage;
  1882. constructor(platform:Platform) {
  1883. this.rootPage = TabsPage;
  1884. this.initializeApp(platform)
  1885. }
  1886. private initializeApp(platform:Platform) {
  1887. platform.ready().then(() =&gt; {
  1888. StatusBar.styleDefault();
  1889. });
  1890. }
  1891. }</code></pre>
  1892. <p>我们用RouteConfig来关联我们的URL和App Component。</p>
  1893. <p>同上面的博客列表页面一样,我们也可以直接添加我们的API服务。有所区别的是,我们需要依据id去获取我们的博客内容。</p>
  1894. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript">
  1895. <span class="at">getBlogpostDetail</span>(id) <span class="op">{</span>
  1896. <span class="kw">var</span> url <span class="op">=</span> <span class="st">&#39;http://localhost:8000/api/blogpost/&#39;</span> <span class="op">+</span> id <span class="op">+</span> <span class="st">&#39;?format=json&#39;</span><span class="op">;</span>
  1897. <span class="cf">return</span> <span class="kw">this</span>.<span class="va">http</span>.<span class="at">get</span>(url).<span class="at">map</span>(res <span class="op">=&gt;</span> res)<span class="op">;</span>
  1898. <span class="op">}</span></code></pre></div>
  1899. <p>和之前的博客列表一样,我们需要几乎一样的方法来获取数据:</p>
  1900. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="im">import</span> <span class="op">{</span>Page<span class="op">,</span> NavController<span class="op">,</span> NavParams<span class="op">}</span> <span class="im">from</span> <span class="st">&#39;ionic-angular&#39;</span><span class="op">;</span>
  1901. <span class="im">import</span> <span class="op">{</span>BlogpostServices<span class="op">}</span> <span class="im">from</span> <span class="st">&quot;../../services/BlogpostServices&quot;</span><span class="op">;</span>
  1902. @<span class="at">Page</span>(<span class="op">{</span>
  1903. <span class="dt">templateUrl</span><span class="op">:</span> <span class="st">&#39;build/pages/blog-detail/blog-detail.html&#39;</span><span class="op">,</span>
  1904. <span class="dt">providers</span><span class="op">:</span> [BlogpostServices]
  1905. <span class="op">}</span>)
  1906. <span class="im">export</span> <span class="kw">class</span> BlogDetailPage <span class="op">{</span>
  1907. <span class="kw">private</span> navParams<span class="op">;</span>
  1908. <span class="kw">private</span> blogServices<span class="op">;</span>
  1909. <span class="kw">private</span> blogpost<span class="op">;</span>
  1910. <span class="at">constructor</span>(<span class="kw">public</span> <span class="dt">nav</span><span class="op">:</span>NavController<span class="op">,</span> <span class="dt">navParams</span><span class="op">:</span>NavParams<span class="op">,</span> <span class="dt">blogServices</span><span class="op">:</span>BlogpostServices) <span class="op">{</span>
  1911. <span class="kw">this</span>.<span class="at">nav</span> <span class="op">=</span> nav<span class="op">;</span>
  1912. <span class="kw">this</span>.<span class="at">navParams</span> <span class="op">=</span> navParams<span class="op">;</span>
  1913. <span class="kw">this</span>.<span class="at">blogServices</span> <span class="op">=</span> blogServices<span class="op">;</span>
  1914. <span class="kw">this</span>.<span class="at">initService</span>()<span class="op">;</span>
  1915. <span class="op">}</span>
  1916. <span class="kw">private</span> <span class="at">initService</span>() <span class="op">{</span>
  1917. <span class="kw">let</span> id <span class="op">=</span> <span class="kw">this</span>.<span class="va">navParams</span>.<span class="at">get</span>(<span class="st">&#39;id&#39;</span>)<span class="op">;</span>
  1918. <span class="kw">this</span>.<span class="va">blogServices</span>.<span class="at">getBlogpostDetail</span>(id).<span class="at">subscribe</span>(
  1919. data <span class="op">=&gt;</span> <span class="op">{</span>
  1920. <span class="kw">this</span>.<span class="at">blogpost</span> <span class="op">=</span> <span class="va">JSON</span>.<span class="at">parse</span>(<span class="va">data</span>.<span class="at">_body</span>)<span class="op">;</span>
  1921. <span class="va">console</span>.<span class="at">log</span>(<span class="kw">this</span>.<span class="at">blogpost</span>)<span class="op">;</span>
  1922. <span class="op">},</span>
  1923. err <span class="op">=&gt;</span> <span class="va">console</span>.<span class="at">log</span>(<span class="st">&#39;Error: &#39;</span> <span class="op">+</span> <span class="va">JSON</span>.<span class="at">stringify</span>(err))<span class="op">,</span>
  1924. () <span class="op">=&gt;</span> <span class="va">console</span>.<span class="at">log</span>(<span class="st">&#39;Get Blogpost&#39;</span>)
  1925. )<span class="op">;</span>
  1926. <span class="op">}</span>
  1927. <span class="op">}</span></code></pre></div>
  1928. <p>现在我们几乎已经完成了博客详情页的工作,我们可以直接通过URL来访问博客详情页:<a href="http://localhost:8100/#/app/blog/1" class="uri">http://localhost:8100/#/app/blog/1</a>。结果如下图所示:</p>
  1929. <figure>
  1930. <img src="./images/blog-detail-page.png" alt="访问博客详情页" /><figcaption>访问博客详情页</figcaption>
  1931. </figure>
  1932. <p>不过,这时候我们的列表页并没有和详情页关联到一起。我们还需要做一些额外的工作:</p>
  1933. <ul>
  1934. <li>在列表页的每一项中添加对点击事件的处理</li>
  1935. </ul>
  1936. <p>在我们的模板页里ion-item里添加一个click事件,这个事件将调用navigate函数,并把博客id传到这个函数里。</p>
  1937. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw">&lt;ion-item</span> <span class="er">*ngFor</span><span class="ot">=</span><span class="st">&quot;#blogpost of blogposts&quot;</span> <span class="er">(click)</span><span class="ot">=</span><span class="st">&quot;navigate(blogpost.id)&quot;</span><span class="kw">&gt;</span>
  1938. <span class="kw">&lt;h1</span> <span class="er">*ngIf</span><span class="ot">=</span><span class="st">&quot;blogpost&quot;</span><span class="kw">&gt;</span>
  1939. {{blogpost.title}}
  1940. <span class="kw">&lt;/h1&gt;</span>
  1941. <span class="kw">&lt;p</span> <span class="er">*ngIf</span><span class="ot">=</span><span class="st">&quot;blogpost&quot;</span><span class="kw">&gt;</span>
  1942. {{blogpost.body}}
  1943. <span class="kw">&lt;/p&gt;</span>
  1944. <span class="kw">&lt;/ion-item&gt;</span></code></pre></div>
  1945. <p>随后在我们的博客详情页的初始化里,我们要初始化一个NavController:</p>
  1946. <pre class="javscript"><code>constructor(nav: NavController, blogpostServices:BlogpostServices) {
  1947. this.blogListService = blogpostServices;
  1948. this.nav = nav;
  1949. this.initService();
  1950. }</code></pre>
  1951. <p>接着,在navigate里我们只需要将BlogDetailPage页面及参数push给navController,并交由它来渲染页面。</p>
  1952. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="at">navigate</span>(id)<span class="op">{</span>
  1953. <span class="kw">this</span>.<span class="va">nav</span>.<span class="at">push</span>(BlogDetailPage<span class="op">,</span> <span class="op">{</span>
  1954. <span class="dt">id</span><span class="op">:</span> id
  1955. <span class="op">}</span>)<span class="op">;</span>
  1956. <span class="op">}</span></code></pre></div>
  1957. <p>现在,我们可以试试从首页跳转到这个博客详情页。</p>
  1958. <h2 id="profile">Profile</h2>
  1959. <p>现在,我们要做一个更有意思的东西了。不过这个内容是为后面的创建文章提供一个技术基础。在用户授权这一部分,我们使用不同的技术来实现,如Cookies、HTTP基本认证等等。而在手机端继续Cookie来进行用户授权,不是一件简单的事。因此我们就需要JSON Web Tokens,这是一种基于token 的认证方案。</p>
  1960. <h3 id="json-web-tokens">Json Web Tokens</h3>
  1961. <p>同样,为了实现这部分功能,我们仍然可以使用其他框架来帮助我们完成基础功能。这里我们就用到了一个名为<code>djangorestframework-jwt</code>的库,从它的名字上我们就可以知道,它就是基于Django REST Framework之上的JWT实现。还是继续使用pip来安装这个库,记得把它添加到<code>requirements.txt</code>中。</p>
  1962. <pre><code>pip install djangorestframework-jwt</code></pre>
  1963. <p>接着,我们需要在我们的URL中配置用于获取token的API即可使用。</p>
  1964. <pre><code>urlpatterns = patterns(
  1965. &#39;&#39;,
  1966. # ...
  1967. url(r&#39;^api-token-auth/&#39;, &#39;rest_framework_jwt.views.obtain_jwt_token&#39;),
  1968. )</code></pre>
  1969. <p>在我们完成了上面的步骤之后,我们可以用<code>curl</code>命令或者Chrome浏览器的Postman来做测试:</p>
  1970. <ul>
  1971. <li>向服务器发送我们的用户名和密码,获取对应的Token。</li>
  1972. </ul>
  1973. <p>如下是<code>curl</code>创建的请求,在这其中我们发送了我们的用户和密码。</p>
  1974. <pre><code>curl -H &quot;Content-Type: application/json&quot; -X POST -d &#39;{&quot;username&quot;:&quot;admin&quot;,&quot;password&quot;:&quot;root&quot;}&#39; http://localhost:8000/api-token-auth</code></pre>
  1975. <p>然后服务端给我们返回了对应的Token,它可以用于后面的创建文章、获取用户信息等等的功能。下面是一个Token的示例:</p>
  1976. <pre><code>{&quot;token&quot;:&quot;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImhAcGhvZGFsLmNvbSIsInVzZXJfaWQiOjIsInVzZXJuYW1lIjoiYWRtaW4iLCJleHAiOjE0NjQ4NzQ1MDZ9.B5LEeIlGDTGggD6dh9akGRKx0Hk09wjylQRLas6kjGM&quot;}</code></pre>
  1977. <h3 id="登录表单">登录表单</h3>
  1978. <p>现在,我们要先做的一件事就是,创建一个用于登录的表单。</p>
  1979. <pre><code>&lt;form #loginCreds=&quot;ngForm&quot; (ngSubmit)=&quot;login(loginCreds.value)&quot;&gt;
  1980. &lt;ion-item&gt;
  1981. &lt;ion-label&gt;Username&lt;/ion-label&gt;
  1982. &lt;ion-input type=&quot;text&quot; ngControl=&quot;username&quot;&gt;&lt;/ion-input&gt;
  1983. &lt;/ion-item&gt;
  1984. &lt;ion-item&gt;
  1985. &lt;ion-label&gt;Password&lt;/ion-label&gt;
  1986. &lt;ion-input type=&quot;password&quot; ngControl=&quot;password&quot;&gt;&lt;/ion-input&gt;
  1987. &lt;/ion-item&gt;
  1988. &lt;div padding&gt;
  1989. &lt;button block type=&quot;submit&quot;&gt;登录&lt;/button&gt;
  1990. &lt;/div&gt;
  1991. &lt;/form&gt;</code></pre>
  1992. <p>我们创建一个名为<code>loginCreds</code>的ngForm,在我们提交的时候我们就调用login方法,并把其中的值(username、password)传过去。而在我们的代码里,我们所要做的就是和上面一样将数据post到之前的Auth API的地址:</p>
  1993. <pre><code>constructor(http: Http, nav:NavController) {
  1994. this.nav = nav;
  1995. this.http = http;
  1996. this.local.get(&#39;id_token&#39;).then(
  1997. (data) =&gt; {
  1998. this.user = this.jwtHelper.decodeToken(data).username;
  1999. }
  2000. );
  2001. }
  2002. login(credentials) {
  2003. this.contentHeader = new Headers({&quot;Content-Type&quot;: &quot;application/json&quot;});
  2004. this.http.post(this.LOGIN_URL, JSON.stringify(credentials), {headers: this.contentHeader})
  2005. .map(res =&gt; res.json())
  2006. .subscribe(
  2007. data =&gt; this.authSuccess(data.token),
  2008. err =&gt; console.log(err)
  2009. );
  2010. }
  2011. authSuccess(token) {
  2012. this.local.set(&#39;id_token&#39;, token);
  2013. this.user = this.jwtHelper.decodeToken(token).username;
  2014. }</code></pre>
  2015. <p>在我们成功的获取到Token的时候,保存这个Token,并调用jwtHelper来解码Token,并从中获取我们的username。</p>
  2016. <p>同时,对于我们来说要登出就是一件容易的,删除这个token,将用户名清空。</p>
  2017. <pre><code>logout() {
  2018. this.local.remove(&#39;id_token&#39;);
  2019. this.user = null;
  2020. }</code></pre>
  2021. <h3 id="profile-1">Profile</h3>
  2022. <p>当我们获取到这个Token,我们也可以顺便获取用户的用户名、邮件等等的信息给用户。我们所要做的就是再获取一次API,但是在获取这次API的时候,我们需要上传我们的Token。因此我们需要一个简单的AuthHelper来帮助我们。</p>
  2023. <h4 id="authhttp">AuthHttp</h4>
  2024. <p>虽然我们要做的仅仅只是在我们的Header中,添加一个字段,它的值就是Token的值。但是这部分的逻辑交给Angular2-JWT来做可能会好一点,它提供了一个AuthHTTP方法可以让每次请求都带上这个Header。首先我们需要安装这个库:</p>
  2025. <pre><code>npm install angular2-jwt</code></pre>
  2026. <p>然后在我们<code>app.ts</code>中添加这个provider,并指明它的header前缀是JWT。</p>
  2027. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript">@<span class="at">App</span>(<span class="op">{</span>
  2028. <span class="dt">template</span><span class="op">:</span> <span class="st">&#39;&lt;ion-nav [root]=&quot;rootPage&quot;&gt;&lt;/ion-nav&gt;&#39;</span><span class="op">,</span>
  2029. <span class="dt">config</span><span class="op">:</span> <span class="op">{},</span> <span class="co">// http://ionicframework.com/docs/v2/api/config/Config/</span>
  2030. <span class="dt">providers</span><span class="op">:</span> [
  2031. <span class="at">provide</span>(AuthHttp<span class="op">,</span> <span class="op">{</span>
  2032. <span class="dt">useFactory</span><span class="op">:</span> (http) <span class="op">=&gt;</span> <span class="op">{</span>
  2033. <span class="kw">var</span> authConfig <span class="op">=</span> <span class="kw">new</span> <span class="at">AuthConfig</span>(<span class="op">{</span><span class="dt">headerPrefix</span><span class="op">:</span> <span class="st">&#39;JWT&#39;</span><span class="op">}</span>)<span class="op">;</span>
  2034. <span class="cf">return</span> <span class="kw">new</span> <span class="at">AuthHttp</span>(authConfig<span class="op">,</span> http)<span class="op">;</span>
  2035. <span class="op">},</span>
  2036. <span class="dt">deps</span><span class="op">:</span> [Http]
  2037. <span class="op">}</span>)
  2038. ]
  2039. <span class="op">}</span>)</code></pre></div>
  2040. <p>现在,我们就可以用和Http一样的方式去获取用户信息。</p>
  2041. <h4 id="获取用户信息">获取用户信息</h4>
  2042. <p>现在我们所需要做的就是发出我们的API去获取用户的信息:</p>
  2043. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="at">authSuccess</span>(token) <span class="op">{</span>
  2044. <span class="kw">this</span>.<span class="va">local</span>.<span class="at">set</span>(<span class="st">&#39;id_token&#39;</span><span class="op">,</span> token)<span class="op">;</span>
  2045. <span class="kw">this</span>.<span class="at">user</span> <span class="op">=</span> <span class="kw">this</span>.<span class="va">jwtHelper</span>.<span class="at">decodeToken</span>(token).<span class="at">username</span><span class="op">;</span>
  2046. <span class="kw">let</span> <span class="dt">params</span><span class="op">:</span>URLSearchParams <span class="op">=</span> <span class="kw">new</span> <span class="at">URLSearchParams</span>()<span class="op">;</span>
  2047. <span class="va">params</span>.<span class="at">set</span>(<span class="st">&#39;username&#39;</span><span class="op">,</span> <span class="kw">this</span>.<span class="at">user</span>)<span class="op">;</span>
  2048. <span class="kw">this</span>.<span class="va">authHttp</span>.<span class="at">request</span>(<span class="st">&#39;http://localhost:8000/api/user/&#39;</span><span class="op">,</span> <span class="op">{</span>
  2049. <span class="dt">search</span><span class="op">:</span> params
  2050. <span class="op">}</span>)
  2051. .<span class="at">map</span>(res <span class="op">=&gt;</span> <span class="va">res</span>.<span class="at">text</span>())
  2052. .<span class="at">subscribe</span>(
  2053. data <span class="op">=&gt;</span> <span class="kw">this</span>.<span class="va">local</span>.<span class="at">set</span>(<span class="st">&#39;user_info&#39;</span><span class="op">,</span> <span class="va">JSON</span>.<span class="at">stringify</span>(<span class="va">JSON</span>.<span class="at">parse</span>(data)[<span class="dv">0</span>]))<span class="op">,</span>
  2054. err <span class="op">=&gt;</span> <span class="va">console</span>.<span class="at">log</span>(err)
  2055. )<span class="op">;</span>
  2056. <span class="op">}</span></code></pre></div>
  2057. <p>只是我们的API似乎还不支持这样的功能。它的实现方式和我们之前的AutoComplete是一样的,也是搜索用户名:</p>
  2058. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python"><span class="kw">def</span> <span class="bu">list</span>(<span class="va">self</span>, request):
  2059. search_param <span class="op">=</span> <span class="va">self</span>.request.query_params.get(<span class="st">&#39;username&#39;</span>, <span class="va">None</span>)
  2060. <span class="cf">if</span> search_param <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:
  2061. queryset <span class="op">=</span> User.objects.<span class="bu">filter</span>(username__contains<span class="op">=</span>search_param)
  2062. serializer <span class="op">=</span> UserSerializer(queryset, many<span class="op">=</span><span class="va">True</span>)
  2063. <span class="cf">return</span> Response(serializer.data)</code></pre></div>
  2064. <p>然后再显示这些数据:</p>
  2065. <pre><code>&lt;div *ngIf=&quot;auth.authenticated()&quot;&gt;
  2066. &lt;div padding&gt;
  2067. &lt;h1&gt;Welcome, {{ user }}&lt;/h1&gt;
  2068. &lt;h2 *ngIf=&quot;user_info&quot;&gt;Last login {{user_info.last_login}}&lt;/h2&gt;
  2069. &lt;button block (click)=&quot;logout()&quot;&gt;Logout&lt;/button&gt;
  2070. &lt;/div&gt;
  2071. &lt;/div&gt;</code></pre>
  2072. <p>不过由于我们Django自带的用户管理模块只有这点信息,我们也就只能显示这些信息了。下一步,我们就可以实现在我们的APP里去创建博客。</p>
  2073. <h2 id="创建博客">创建博客</h2>
  2074. <p>在我们开始在APP端实现这个功能之前,我们先要实现一个高级点的用户授权管理——即只有用户登录或者用户的请求中带有Token的时候,我们才能创建博客。于是在这里我们所要做的就是实现一个<code>IsAuthenticatedOrReadOnly</code>,来判断用户是否有权限,如果没有的话,那么只让用户看到博客的内容。代码如下所示:</p>
  2075. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">SAFE_METHODS <span class="op">=</span> [<span class="st">&#39;GET&#39;</span>, <span class="st">&#39;HEAD&#39;</span>, <span class="st">&#39;OPTIONS&#39;</span>]
  2076. <span class="kw">class</span> IsAuthenticatedOrReadOnly(BasePermission):
  2077. <span class="co">&quot;&quot;&quot;</span>
  2078. <span class="co"> The request is authenticated as a user, or is a read-only request.</span>
  2079. <span class="co"> &quot;&quot;&quot;</span>
  2080. <span class="kw">def</span> has_permission(<span class="va">self</span>, request, view):
  2081. <span class="cf">if</span> (request.method <span class="kw">in</span> SAFE_METHODS <span class="kw">or</span>
  2082. request.user <span class="kw">and</span>
  2083. request.user.is_authenticated()):
  2084. <span class="cf">return</span> <span class="va">True</span>
  2085. <span class="cf">return</span> <span class="va">False</span>
  2086. <span class="kw">class</span> BlogpsotSerializer(serializers.HyperlinkedModelSerializer):
  2087. <span class="kw">class</span> Meta:
  2088. model <span class="op">=</span> Blogpost
  2089. fields <span class="op">=</span> (<span class="st">&#39;title&#39;</span>, <span class="st">&#39;author&#39;</span>, <span class="st">&#39;body&#39;</span>, <span class="st">&#39;slug&#39;</span>, <span class="st">&#39;id&#39;</span>)
  2090. <span class="co"># ViewSets define the view behavior.</span>
  2091. <span class="kw">class</span> BlogpostSet(viewsets.ModelViewSet):
  2092. permission_classes <span class="op">=</span> (permissions.IsAuthenticatedOrReadOnly,)
  2093. queryset <span class="op">=</span> Blogpost.objects.<span class="bu">all</span>()
  2094. serializer_class <span class="op">=</span> BlogpsotSerializer</code></pre></div>
  2095. <p>接着,我们可以创建一个Modal来做这个工作。对于我们的博客表单来说,和登录没有太大的区别。</p>
  2096. <pre><code> &lt;form #blogpostForm=&quot;ngForm&quot; (ngSubmit)=&quot;create(blogpostForm.value)&quot;&gt;
  2097. &lt;ion-item&gt;
  2098. &lt;ion-label&gt;标题&lt;/ion-label&gt;
  2099. &lt;ion-input type=&quot;text&quot; ngControl=&quot;title&quot;&gt;&lt;/ion-input&gt;
  2100. &lt;/ion-item&gt;
  2101. &lt;ion-item&gt;
  2102. &lt;ion-label&gt;作者&lt;/ion-label&gt;
  2103. &lt;ion-input type=&quot;text&quot; ngControl=&quot;author&quot;&gt;&lt;/ion-input&gt;
  2104. &lt;/ion-item&gt;
  2105. &lt;ion-item&gt;
  2106. &lt;ion-label&gt;URL&lt;/ion-label&gt;
  2107. &lt;ion-input type=&quot;text&quot; ngControl=&quot;slug&quot;&gt;&lt;/ion-input&gt;
  2108. &lt;/ion-item&gt;
  2109. &lt;ion-item&gt;
  2110. &lt;ion-label&gt;内容&lt;/ion-label&gt;
  2111. &lt;ion-textarea type=&quot;text&quot; ngControl=&quot;body&quot;&gt;&lt;/ion-textarea&gt;
  2112. &lt;/ion-item&gt;
  2113. &lt;div padding&gt;
  2114. &lt;button block type=&quot;submit&quot;&gt;创建&lt;/button&gt;
  2115. &lt;/div&gt;
  2116. &lt;/form&gt;</code></pre>
  2117. <p>稍有不同的是在我们的标题栏里会有一个关闭按钮。</p>
  2118. <pre><code>&lt;ion-toolbar&gt;
  2119. &lt;ion-title&gt;
  2120. 创建博客
  2121. &lt;/ion-title&gt;
  2122. &lt;ion-buttons start&gt;
  2123. &lt;button (click)=&quot;close()&quot;&gt;
  2124. &lt;span primary showWhen=&quot;ios&quot;&gt;取消&lt;/span&gt;
  2125. &lt;ion-icon name=&quot;md-close&quot; showWhen=&quot;android,windows&quot;&gt;&lt;/ion-icon&gt;
  2126. &lt;/button&gt;
  2127. &lt;/ion-buttons&gt;
  2128. &lt;/ion-toolbar&gt;</code></pre>
  2129. <p>对于我们的实现代码来说,也是类似的,除了我们在发表成功的时候做的事情不一样——关闭这个Modal。</p>
  2130. <pre><code> close() {
  2131. this.viewCtrl.dismiss();
  2132. }
  2133. create(value) {
  2134. this.contentHeader = new Headers({&quot;Content-Type&quot;: &quot;application/json&quot;});
  2135. this.authHttp.post(&#39;http://127.0.0.1:8000/api/blogpost/&#39;, JSON.stringify(value), {headers: this.contentHeader})
  2136. .map(res =&gt; res.json())
  2137. .subscribe(
  2138. data =&gt; this.postSuccess(data),
  2139. err =&gt; console.log(err)
  2140. );
  2141. }
  2142. postSuccess(data) {
  2143. this.close()
  2144. }</code></pre>
  2145. <p>同时,我们需要在我们的首页里添加这样的一个入口。</p>
  2146. <pre><code> &lt;button fab fab-bottom fab-right (click)=&quot;createBlog()&quot; calm&gt;
  2147. &lt;ion-icon name=&quot;add&quot;&gt;&lt;/ion-icon&gt;
  2148. &lt;/button&gt;</code></pre>
  2149. <p>以及它的处理逻辑:</p>
  2150. <pre><code> createBlog() {
  2151. let modal = Modal.create(CreateBlogModal);
  2152. this.nav.present(modal)
  2153. }</code></pre>
  2154. <h1 id="移动单页面应用">移动单页面应用</h1>
  2155. <p>为了实现在移动设备上的访问,这里就以riot.js为例做一个简单的Demo。不过,首先我们需要在后台判断用户是来自于某种设备,再对其进行特殊的处理。</p>
  2156. <h2 id="移动设备处理">移动设备处理</h2>
  2157. <p>幸运的是我们又找到了一个库名为<code>django_mobile</code>,可以根据用户的User-Agent来区别设备,并为其分配一个移动设备专用的模板。因此,我们需要安装这个库:</p>
  2158. <pre><code>pip install django_mobile</code></pre>
  2159. <p>并将<code>'django_mobile.middleware.MobileDetectionMiddleware'</code>和<code>'django_mobile.middleware.SetFlavourMiddleware'</code>添加MIDDLEWARE_CLASSES中:</p>
  2160. <pre><code>MIDDLEWARE_CLASSES = (
  2161. &#39;django.contrib.sessions.middleware.SessionMiddleware&#39;,
  2162. &#39;corsheaders.middleware.CorsMiddleware&#39;,
  2163. &#39;django.middleware.common.CommonMiddleware&#39;,
  2164. &#39;django.middleware.csrf.CsrfViewMiddleware&#39;,
  2165. &#39;django.contrib.auth.middleware.AuthenticationMiddleware&#39;,
  2166. &#39;django.contrib.auth.middleware.SessionAuthenticationMiddleware&#39;,
  2167. &#39;django.contrib.messages.middleware.MessageMiddleware&#39;,
  2168. &#39;django.middleware.clickjacking.XFrameOptionsMiddleware&#39;,
  2169. &#39;django.contrib.flatpages.middleware.FlatpageFallbackMiddleware&#39;,
  2170. &#39;django_mobile.middleware.MobileDetectionMiddleware&#39;,
  2171. &#39;django_mobile.middleware.SetFlavourMiddleware&#39;
  2172. )</code></pre>
  2173. <p>修改Template配置,添加对应的loader和context_processor,如下所示的内容即是修改完后的结果:</p>
  2174. <pre><code>TEMPLATES = [
  2175. {
  2176. &#39;BACKEND&#39;: &#39;django.template.backends.django.DjangoTemplates&#39;,
  2177. &#39;DIRS&#39;: [
  2178. &#39;templates/&#39;
  2179. ],
  2180. &#39;OPTIONS&#39;: {
  2181. &#39;context_processors&#39;: [
  2182. &#39;django.template.context_processors.debug&#39;,
  2183. &#39;django.template.context_processors.request&#39;,
  2184. &#39;django.contrib.auth.context_processors.auth&#39;,
  2185. &#39;django.contrib.messages.context_processors.messages&#39;,
  2186. &#39;django_mobile.context_processors.flavour&#39;
  2187. ],
  2188. },
  2189. },
  2190. ]
  2191. # dirty fixed for https://github.com/gregmuellegger/django-mobile/issues/72
  2192. TEMPLATE_LOADERS = TEMPLATES[0][&#39;OPTIONS&#39;][&#39;loaders&#39;]</code></pre>
  2193. <p>我们在LOADERS中添加了<code>'django_mobile.loader.Loader'</code>,在<code>context_processors</code>中添加了<code>django_mobile.context_processors.flavour</code>。</p>
  2194. <p>然后在template目录中创建<code>template/mobile/index.html</code>文件,即可。</p>
  2195. <h2 id="前后端分离">前后端分离</h2>
  2196. <p>为了方便我们讲述模块化,也不改变系统原有架构,我决定挖个大坑使用Riot.js来展示这一部分的内容。</p>
  2197. <h3 id="riot.js">Riot.js</h3>
  2198. <p>Riot拥有创建现代客户端应用的所有必需的成分:</p>
  2199. <ul>
  2200. <li>“响应式” 视图层用来创建用户界面</li>
  2201. <li>用来在各独立模块之间进行通信的事件库</li>
  2202. <li>用来管理URL和浏览器回退按钮的路由器(Router)</li>
  2203. </ul>
  2204. <p>等等。</p>
  2205. <p>接着让我们引入riot.js这个库,顺便也引入rxjs吧:</p>
  2206. <pre><code>&lt;script src=&quot;{% static &#39;js/mobile/riot+compiler.min.js&#39; %}&quot;&gt;&lt;/script&gt;
  2207. &lt;script src=&quot;{% static &#39;js/mobile/rx.core.min.js&#39; %}&quot;&gt;&lt;/script&gt;</code></pre>
  2208. <h3 id="reactivejs构建服务">ReactiveJS构建服务</h3>
  2209. <p>由于我们所要做的服务比较简单,并且我们也更愿意使用Promise来加载API服务,因此我们引入了这个库来加速我们的开发。下面是我们用于获取博客API的代码:</p>
  2210. <pre><code>var responseStream = function (blogId) {
  2211. var url = &#39;/api/blogpost/?format=json&#39;;
  2212. if(blogId) {
  2213. url = &#39;/api/blogpost/&#39; + blogId + &#39;?format=json&#39;;
  2214. }
  2215. return Rx.Observable.create(function (observer) {
  2216. jQuery.getJSON(url)
  2217. .done(function (response) {
  2218. observer.onNext(response);
  2219. })
  2220. .fail(function (jqXHR, status, error) {
  2221. observer.onError(error);
  2222. })
  2223. .always(function () {
  2224. observer.onCompleted();
  2225. });
  2226. });
  2227. };</code></pre>
  2228. <p>当我们想访问特定博客的时候,我们就传博客ID进去——这时会使用<code>'/api/blogpost/' + blogId + '?format=json'</code>作为URL。接着我们创建了自己定制的事件流——使用jQuery去获取API:</p>
  2229. <ul>
  2230. <li>成功的时候(done),我们将用onNext()来通知观察者</li>
  2231. <li>失败的时候(fail),我们就调用onError()来通知观察者</li>
  2232. <li>不论成功或者失败,都会执行always</li>
  2233. </ul>
  2234. <p>在使用的时候,我们只需要调用其<code>subscribe</code>方法即可:</p>
  2235. <pre><code>responseStream().subscribe(function (response) {
  2236. })</code></pre>
  2237. <h3 id="创建博客列表页-1">创建博客列表页</h3>
  2238. <p>现在,我们可以修改原生的博客模板,将其中的container内容变为:</p>
  2239. <pre><code>&lt;div class=&quot;container&quot; id=&quot;container&quot;&gt;
  2240. &lt;blog&gt;&lt;/blog&gt;
  2241. &lt;/div&gt;</code></pre>
  2242. <p>接着,我们可以创建一个<code>blog.tag</code>文件,添加加载这个文件:`</p>
  2243. <pre><code>&lt;script src=&quot;{% static &#39;riot/blog.tag&#39; %}&quot; type=&quot;riot/tag&quot;&gt;&lt;/script&gt;</code></pre>
  2244. <p>为了调用这个tag的内容,我们需要在我们的<code>main.js</code>加上一句:</p>
  2245. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="va">riot</span>.<span class="at">mount</span>(<span class="st">&quot;blog&quot;</span>)<span class="op">;</span></code></pre></div>
  2246. <p>随后我们可以在我们的tag文件中,来对blog的内容进行操作。</p>
  2247. <div class="sourceCode"><pre class="sourceCode html"><code class="sourceCode html"><span class="kw">&lt;blog</span><span class="ot"> class=</span><span class="st">&quot;row&quot;</span><span class="kw">&gt;</span>
  2248. <span class="kw">&lt;div</span><span class="ot"> class=</span><span class="st">&quot;col-sm-4&quot;</span><span class="ot"> each=</span><span class="st">{</span><span class="ot"> opts</span> <span class="er">}</span><span class="kw">&gt;</span>
  2249. <span class="kw">&lt;h2&gt;&lt;a</span><span class="ot"> href=</span><span class="st">&quot;#/blogDetail/{id}&quot;</span><span class="ot"> onclick=</span><span class="st">{</span><span class="ot"> parent.click</span> <span class="er">}</span><span class="kw">&gt;</span>{ title }<span class="kw">&lt;/a&gt;&lt;/h2&gt;</span>
  2250. { body }
  2251. { posted } - By { author }
  2252. <span class="kw">&lt;/div&gt;</span>
  2253. <span class="kw">&lt;script&gt;</span>
  2254. <span class="kw">var</span> self <span class="op">=</span> <span class="kw">this</span><span class="op">;</span>
  2255. <span class="kw">this</span>.<span class="at">on</span>(<span class="st">&#39;mount&#39;</span><span class="op">,</span> <span class="kw">function</span> (id) <span class="op">{</span>
  2256. <span class="at">responseStream</span>().<span class="at">subscribe</span>(<span class="kw">function</span> (response) <span class="op">{</span>
  2257. <span class="va">self</span>.<span class="at">opts</span> <span class="op">=</span> response<span class="op">;</span>
  2258. <span class="va">self</span>.<span class="at">update</span>()<span class="op">;</span>
  2259. <span class="op">}</span>)
  2260. <span class="op">}</span>)
  2261. <span class="at">click</span>(event)
  2262. <span class="op">{</span>
  2263. <span class="kw">this</span>.<span class="at">unmount</span>()<span class="op">;</span>
  2264. <span class="va">riot</span>.<span class="at">route</span>(<span class="st">&quot;blogDetail/&quot;</span> <span class="op">+</span> <span class="va">event</span>.<span class="va">item</span>.<span class="at">id</span>)<span class="op">;</span>
  2265. <span class="op">}</span>
  2266. <span class="kw">&lt;/script&gt;</span>
  2267. <span class="kw">&lt;/blog&gt;</span></code></pre></div>
  2268. <p>在Riot中,变量默认是以opts的方式传递起来的,因此我们也遵循这个方式。在模板方面,我们遍历每个博客取出其中的内容:</p>
  2269. <pre><code>&lt;div class=&quot;col-sm-4&quot; each={ opts }&gt;
  2270. &lt;h2&gt;&lt;a href=&quot;#/blogDetail/{id}&quot; onclick={ parent.click }&gt;{ title }&lt;/a&gt;&lt;/h2&gt;
  2271. { body }
  2272. { posted } - By { author }
  2273. &lt;/div&gt;</code></pre>
  2274. <p>而博客的数据需要依赖于我们监听<code>mount</code>事件才会去获取——即我们加载了这个tag。</p>
  2275. <pre><code>this.on(&#39;mount&#39;, function (id) {
  2276. responseStream().subscribe(function (response) {
  2277. self.opts = response;
  2278. self.update();
  2279. })
  2280. })</code></pre>
  2281. <p>在这个页面中,还有一个单击事件<code>onclick={ parent.click }</code>,即当我们点击某个博客的标题时执行的函数:</p>
  2282. <pre><code>click(event)
  2283. {
  2284. this.unmount();
  2285. riot.route(&quot;blog/&quot; + event.item.id);
  2286. }</code></pre>
  2287. <p>我们将卸载当前的tag,然后加载blogDetail的内容。</p>
  2288. <h3 id="博客详情页">博客详情页</h3>
  2289. <p>在我们加载之前,我们需要先配置好blogDetail。我们仍然使用正则表达式<code>blogDetail/*</code>来获取博客的id:</p>
  2290. <pre><code>riot.route.base(&#39;#&#39;);
  2291. riot.route(&#39;blog/*&#39;, function(id) {
  2292. riot.mount(&quot;blogDetail&quot;, {id: id})
  2293. });
  2294. riot.route.start();</code></pre>
  2295. <p>然后将由相应的tag来执行:</p>
  2296. <pre><code>&lt;blogDetail class=&quot;row&quot;&gt;
  2297. &lt;div class=&quot;col-sm-4&quot;&gt;
  2298. &lt;h2&gt;{ opts.title }&lt;/h2&gt;
  2299. { opts.body }
  2300. { opts.posted } - By { opts.author }
  2301. &lt;/div&gt;
  2302. &lt;script&gt;
  2303. var self = this;
  2304. this.on(&#39;mount&#39;, function (id) {
  2305. responseStream(this.opts.id).subscribe(function (response) {
  2306. self.opts = response;
  2307. self.update();
  2308. })
  2309. })
  2310. &lt;/script&gt;
  2311. &lt;/blogDetail&gt;</code></pre>
  2312. <p>同样的,我们也将去获取这篇博客的内容,然后显示。</p>
  2313. <h3 id="添加导航-1">添加导航</h3>
  2314. <p>在上面的例子里,我们少了一部分很重要的内容就是在页面间跳转,现在就让我们来创建<code>navbar.tag</code>吧。</p>
  2315. <p>首先,我们需要重新规则一下route,在系统初始化的时候我们将使用的路由是blog,在进入详情页的时候,我们用blog/*。</p>
  2316. <div class="sourceCode"><pre class="sourceCode javascript"><code class="sourceCode javascript"><span class="va">riot</span>.<span class="va">route</span>.<span class="at">base</span>(<span class="st">&#39;#&#39;</span>)<span class="op">;</span>
  2317. <span class="va">riot</span>.<span class="at">route</span>(<span class="st">&#39;blog/*&#39;</span><span class="op">,</span> <span class="kw">function</span> (id) <span class="op">{</span>
  2318. <span class="va">riot</span>.<span class="at">mount</span>(<span class="st">&quot;blogDetail&quot;</span><span class="op">,</span> <span class="op">{</span><span class="dt">id</span><span class="op">:</span> id<span class="op">}</span>)
  2319. <span class="op">}</span>)<span class="op">;</span>
  2320. <span class="va">riot</span>.<span class="at">route</span>(<span class="st">&#39;blog&#39;</span><span class="op">,</span> <span class="kw">function</span> () <span class="op">{</span>
  2321. <span class="va">riot</span>.<span class="at">mount</span>(<span class="st">&quot;blog&quot;</span>)
  2322. <span class="op">}</span>)<span class="op">;</span>
  2323. <span class="va">riot</span>.<span class="va">route</span>.<span class="at">start</span>()<span class="op">;</span>
  2324. <span class="va">riot</span>.<span class="at">route</span>(<span class="st">&quot;blog&quot;</span>)<span class="op">;</span></code></pre></div>
  2325. <p>然后将我们的navbar标签放在<code>blog</code>和<code>blogDetail</code>中,如下所示:</p>
  2326. <pre><code>&lt;blogDetail class=&quot;row&quot;&gt;
  2327. &lt;navbar title=&quot;{ opts.title }&quot;&gt;&lt;/navbar&gt;
  2328. &lt;div class=&quot;col-sm-4&quot;&gt;
  2329. &lt;h2&gt;{ opts.title }&lt;/h2&gt;
  2330. { opts.body }
  2331. { opts.posted } - By { opts.author }
  2332. &lt;/div&gt;
  2333. &lt;/blogDetail&gt;
  2334. 当我们到了博客详情页,我们将把标题作为参数传给title。接着,我们在navbar中我们就可以创造一个breadcrumb导航了:
  2335. </code></pre>
  2336. <navbar>
  2337. <ol class="breadcrumb">
  2338. <pre><code> &lt;li&gt;&lt;a href=&quot;#/&quot; onclick={parent.clickTitle}&gt;Home&lt;/a&gt;&lt;/li&gt;
  2339. &lt;li if=&quot;opts.title&quot;&gt;{ opts.title} &lt;/li&gt;
  2340. &lt;/ol&gt;</code></pre>
  2341. <p></navbar></p>
  2342. <pre><code>
  2343. 最后可以在我们的blogDetail标签中添加一个点击事件来跳转到首页:
  2344. </code></pre>
  2345. <p>clickTitle(event) { self.unmount(true); riot.route(“blog”); } ```</p>
  2346. <h1 id="配置管理">配置管理</h1>
  2347. <h2 id="local-settings">local settings</h2>
  2348. <p>作为一个开源项目,我们在这方面做得并不是特别好——当然是有意如此的。不过,这里我们还是做一些简单的介绍。对于我们的项目来说,我们需要一些额外的配置,如我们的数据库中的<code>DATABASES</code>、<code>DEFAULT_AUTHENTICATION_CLASSES</code>、<code>CORS_ORIGIN_ALLOW_ALL</code>、<code>SECRET_KEY</code>应该在不同的环境中都有不同的配置。</p>
  2349. <p>我们可以一个创建<code>local_settings.py</code>,在里面放置一些关键的服务器相关的配置,如:</p>
  2350. <div class="sourceCode"><pre class="sourceCode python"><code class="sourceCode python">
  2351. <span class="co"># </span><span class="al">SECURITY</span><span class="co"> </span><span class="al">WARNING</span><span class="co">: keep the secret key used in production secret!</span>
  2352. SECRET_KEY <span class="op">=</span> <span class="st">&#39;hpi!zb8!(j%40)r55@+_5k*^9qcjf9sx0o_it*jlp3=x9^2ak@&#39;</span>
  2353. <span class="co"># </span><span class="al">SECURITY</span><span class="co"> </span><span class="al">WARNING</span><span class="co">: don&#39;t run with debug turned on in production!</span>
  2354. DEBUG <span class="op">=</span> <span class="va">True</span>
  2355. TEMPLATE_DEBUG <span class="op">=</span> <span class="va">True</span>
  2356. <span class="co"># Database</span>
  2357. <span class="co"># https://docs.djangoproject.com/en/1.7/ref/settings/#databases</span>
  2358. DATABASES <span class="op">=</span> {
  2359. <span class="st">&#39;default&#39;</span>: {
  2360. <span class="st">&#39;ENGINE&#39;</span>: <span class="st">&#39;django.db.backends.sqlite3&#39;</span>,
  2361. <span class="st">&#39;NAME&#39;</span>: <span class="st">&#39;db.sqlite3&#39;</span>,
  2362. }
  2363. }
  2364. REST_FRAMEWORK <span class="op">=</span> {
  2365. <span class="st">&#39;DEFAULT_PERMISSION_CLASSES&#39;</span>: (
  2366. ),
  2367. <span class="st">&#39;DEFAULT_AUTHENTICATION_CLASSES&#39;</span>: (
  2368. <span class="st">&#39;rest_framework.authentication.SessionAuthentication&#39;</span>,
  2369. <span class="st">&#39;rest_framework.authentication.BasicAuthentication&#39;</span>,
  2370. <span class="st">&#39;rest_framework_jwt.authentication.JSONWebTokenAuthentication&#39;</span>,
  2371. ),
  2372. }
  2373. CORS_ORIGIN_ALLOW_ALL <span class="op">=</span> <span class="va">True</span></code></pre></div>
  2374. <p>接着,我们只需要在我们的主<code>settiings.py</code>中引用即可。</p>
  2375. </body>
  2376. </html>