├── .gitignore
├── CNAME
├── LICENSE
├── Makefile
├── README.md
├── api
└── all.json
├── build
├── author.html
├── head.html
├── metadata.xml
├── share.html
├── stats.html
└── title.txt
├── chapters-en
├── big-data.md
├── bookself.md
├── brand.md
├── congee.md
├── echoesworks.md
├── eppsc.md
├── google-map-solr.md
├── growth.md
└── ionic-es.md
├── chapters
├── about.md
├── big-data.md
├── bookshelf.md
├── brand.md
├── congee.md
├── echoesworks.md
├── eppsc.md
├── google-map-solr.md
├── growth.md
├── ionic-es.md
├── lettuce.md
├── lock.md
├── luffa.md
├── moqimobi.md
├── oculus-three.md
├── onmap.md
├── sherlock.md
├── tech-stack.md
├── text2logo.md
└── vmap.md
├── css
└── vendor.css
├── epub.css
├── ideabook.md
├── images
├── alipay.png
├── anti-map-action.jpg
├── badge.png
├── bookshelf.jpg
├── brand-idea-prototype.jpg
├── carrot.png
├── china-geojson.jpg
├── city-with-geo.jpg
├── clean_code.png
├── congee.jpg
├── demo.jpg
├── demo.png
├── eche-editor-screenshot.png
├── echoesworks.jpg
├── elasticsearch_ionic_info_page.jpg
├── elasticsearch_ionic_map.jpg
├── elasticsearch_ionit_map.jpg
├── finally-brand.jpg
├── full-platforms.jpg
├── general-province-city-map.png
├── geopoly2d-small.png
├── gmap-solr.jpg
├── gnu-linux.jpg
├── growth-arch.png
├── growth-full-platforms.jpg
├── hexoarch.png
├── iot-3-layer.jpg
├── iot-layer.jpg
├── iot-server.jpg
├── iphone.jpg
├── lan-struct.png
├── luffa.jpg
├── lz.jpg
├── man-git.png
├── map_with_bg.jpg
├── mpu6050.jpg
├── node.png
├── oculus-vr.jpg
├── onmap-demo.jpg
├── province-hash-with-map.jpg
├── refactor.png
├── sherlock.png
├── start.jpg
├── struct.png
├── tdd.png
├── tech-stack.jpg
├── tips.jpg
├── travis-edit-publish-code.png
├── tree.jpg
├── vmap-click-handler.jpg
├── wechat-pay.png
├── wechat.jpg
└── xiaomiquan.jpg
├── img
├── cover.jpg
└── favicon.ico
├── index.html
├── listings-setup.tex
├── style.css
└── template
└── template.tex
/.gitignore:
--------------------------------------------------------------------------------
1 | ideabook.rtf
2 | ideabook.mobi
3 | ideabook.pdf
4 | ideabook.epub
5 | .idea
6 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | ideabook.phodal.com
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | include_dir=build
2 | source=chapters/*.md
3 | title="Phodal's Idea实战指南"
4 | filename='ideabook'
5 |
6 |
7 | all: html epub rtf pdf mobi
8 |
9 | markdown:
10 | awk 'FNR==1{print ""}{print}' $(source) > $(filename).md
11 |
12 | html: markdown
13 | pandoc -s $(filename).md -t html5 -o index.html -c style.css \
14 | --include-in-header $(include_dir)/head.html \
15 | --include-before-body $(include_dir)/author.html \
16 | --include-before-body $(include_dir)/share.html \
17 | --include-after-body $(include_dir)/stats.html \
18 | --title-prefix $(title) \
19 | --normalize \
20 | --smart \
21 | --toc
22 |
23 | epub: markdown
24 | pandoc -s $(filename).md --normalize --smart -t epub -o $(filename).epub \
25 | --epub-metadata $(include_dir)/metadata.xml \
26 | --epub-stylesheet epub.css \
27 | --epub-cover-image img/cover.jpg \
28 | --title-prefix $(title) \
29 | --normalize \
30 | --smart \
31 | --toc
32 |
33 | rtf: markdown
34 | pandoc -s $(filename).md -o $(filename).rtf \
35 | --title-prefix $(title) \
36 | --normalize \
37 | --smart
38 |
39 | pdf: markdown
40 | # OS X: http://www.tug.org/mactex/
41 | # Then find its path: find /usr/ -name "pdflatex"
42 | # Then symlink it: ln -s /path/to/pdflatex /usr/local/bin
43 | pandoc -s $(filename).md -o $(filename).pdf \
44 | --title-prefix $(title) \
45 | --listings -H listings-setup.tex \
46 | --template=template/template.tex \
47 | --normalize \
48 | --smart \
49 | --toc \
50 | --latex-engine=`which xelatex`
51 |
52 | mobi: epub
53 | # Symlink bin: ln -s /path/to/kindlegen /usr/local/bin
54 | kindlegen $(filename).epub
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 一个全栈增长工程师的练手项目集
2 | ===
3 |
4 | > English version is Ongoing, welcome to join us.
5 |
6 | **你是不是在为提高编程技术而发愁?**
7 |
8 | **你是不是在为找不到合适的练手项目而烦恼?**
9 |
10 | **你是不是在为有合适的项目,但是没有指南而烦恼?**
11 |
12 | 我的其他电子书:
13 |
14 | - 《[一步步搭建物联网系统](https://github.com/phodal/designiot)》
15 | - 《[GitHub 漫游指南](https://github.com/phodal/github-roam)》
16 | - 《[RePractise](https://github.com/phodal/repractise)》
17 | - 《[Growth: 全栈增长工程师指南](https://github.com/phodal/growth-ebook)》
18 | - 《[Growth: 全栈增长工程师实战](https://github.com/phodal/growth-in-action)》
19 |
20 | 欢迎关注我的微信公众号(扫描下面的二维码或搜索 Phodal).
21 |
22 | 
23 |
24 | 目录
25 | ---
26 |
27 | * [分析网站日志,打造访问地图](http://ideabook.phodal.com/#分析网站日志打造访问地图)
28 | * [概况](http://ideabook.phodal.com/#概况)
29 | * [背景](http://ideabook.phodal.com/#背景)
30 | * [ShowCase](http://ideabook.phodal.com/#showcase)
31 | * [Hadoop + Pig + Jython + AmMap + ElasticSearch](http://ideabook.phodal.com/#hadoop-pig-jython-ammap-elasticsearch)
32 | * [步骤](http://ideabook.phodal.com/#步骤)
33 | * [Step 1: 搭建基础设施](http://ideabook.phodal.com/#step-1-搭建基础设施)
34 | * [Step 2: 解析access.log](http://ideabook.phodal.com/#step-2-解析access.log)
35 | * [Step 3: 转换IP为GEO信息](http://ideabook.phodal.com/#step-3-转换ip为geo信息)
36 | * [Step 4: 展示数据到地图上](http://ideabook.phodal.com/#step-4-展示数据到地图上)
37 | * [练习建议](http://ideabook.phodal.com/#练习建议)
38 | * [书籍录入移动应用:条形码扫描](http://ideabook.phodal.com/#书籍录入移动应用条形码扫描)
39 | * [概况](http://ideabook.phodal.com/#概况-1)
40 | * [背景](http://ideabook.phodal.com/#背景-1)
41 | * [ShowCase](http://ideabook.phodal.com/#showcase-1)
42 | * [Ionic + Zxing](http://ideabook.phodal.com/#ionic-zxing)
43 | * [步骤](http://ideabook.phodal.com/#步骤-1)
44 | * [Step 1: ZXing扫描与Douban API](http://ideabook.phodal.com/#step-1-zxing扫描与douban-api)
45 | * [Step 2: 存储数据库](http://ideabook.phodal.com/#step-2-存储数据库)
46 | * [练习建议](http://ideabook.phodal.com/#练习建议-1)
47 | * [制作专属Badge](http://ideabook.phodal.com/#制作专属badge)
48 | * [概况](http://ideabook.phodal.com/#概况-2)
49 | * [背景](http://ideabook.phodal.com/#背景-2)
50 | * [ShowCase](http://ideabook.phodal.com/#showcase-2)
51 | * [SVG与SVGWrite](http://ideabook.phodal.com/#svg与svgwrite)
52 | * [步骤](http://ideabook.phodal.com/#步骤-2)
53 | * [Step 1: 基本图形](http://ideabook.phodal.com/#step-1-基本图形)
54 | * [Step 2: 高级Badge](http://ideabook.phodal.com/#step-2-高级badge)
55 | * [微信文章编辑器](http://ideabook.phodal.com/#微信文章编辑器)
56 | * [概况](http://ideabook.phodal.com/#概况-3)
57 | * [背景](http://ideabook.phodal.com/#背景-3)
58 | * [ShowCase](http://ideabook.phodal.com/#showcase-3)
59 | * [CKEditor + Ractive](http://ideabook.phodal.com/#ckeditor-ractive)
60 | * [步骤](http://ideabook.phodal.com/#步骤-3)
61 | * [Step 1: hello,world](http://ideabook.phodal.com/#step-1-helloworld)
62 | * [Step 2: Require.js模块化](http://ideabook.phodal.com/#step-2-require.js模块化)
63 | * [Step 3: 初始化](http://ideabook.phodal.com/#step-3-初始化)
64 | * [Step 4: 创建对应的View](http://ideabook.phodal.com/#step-4-创建对应的view)
65 | * [练习建议](http://ideabook.phodal.com/#练习建议-2)
66 | * [JavaScript制作Slide框架](http://ideabook.phodal.com/#javascript制作slide框架)
67 | * [概况](http://ideabook.phodal.com/#概况-4)
68 | * [背景](http://ideabook.phodal.com/#背景-4)
69 | * [Showcase](http://ideabook.phodal.com/#showcase-4)
70 | * [需求](http://ideabook.phodal.com/#需求)
71 | * [步骤](http://ideabook.phodal.com/#步骤-4)
72 | * [Step 1: 基本的Slide功能](http://ideabook.phodal.com/#step-1-基本的slide功能)
73 | * [Step 2: 解析Markdown](http://ideabook.phodal.com/#step-2-解析markdown)
74 | * [Step 3: 事件处理](http://ideabook.phodal.com/#step-3-事件处理)
75 | * [Step 4: 解析字幕](http://ideabook.phodal.com/#step-4-解析字幕)
76 | * [Step 5: 进度条](http://ideabook.phodal.com/#step-5-进度条)
77 | * [Step 6: 同步](http://ideabook.phodal.com/#step-6-同步)
78 | * [练习建议](http://ideabook.phodal.com/#练习建议-3)
79 | * [编辑-发布-分离的博客系统](http://ideabook.phodal.com/#编辑-发布-分离的博客系统)
80 | * [概况](http://ideabook.phodal.com/#概况-5)
81 | * [背景: 编辑-发布-开发分离](http://ideabook.phodal.com/#背景-编辑-发布-开发分离)
82 | * [用户场景](http://ideabook.phodal.com/#用户场景)
83 | * [步骤](http://ideabook.phodal.com/#步骤-5)
84 | * [Step 1: 构建工具](http://ideabook.phodal.com/#step-1-构建工具)
85 | * [Step 2: 静态页面生成](http://ideabook.phodal.com/#step-2-静态页面生成)
86 | * [Solr实现多边形地理搜索](http://ideabook.phodal.com/#solr实现多边形地理搜索)
87 | * [概况](http://ideabook.phodal.com/#概况-6)
88 | * [背景](http://ideabook.phodal.com/#背景-5)
89 | * [Showcase](http://ideabook.phodal.com/#showcase-5)
90 | * [Solr](http://ideabook.phodal.com/#solr)
91 | * [步骤](http://ideabook.phodal.com/#步骤-6)
92 | * [Step 1: Solr Flask](http://ideabook.phodal.com/#step-1-solr-flask)
93 | * [Step 2: Google map Polygon](http://ideabook.phodal.com/#step-2-google-map-polygon)
94 | * [一份代码打造跨平台应用](http://ideabook.phodal.com/#一份代码打造跨平台应用)
95 | * [概况](http://ideabook.phodal.com/#概况-7)
96 | * [背景](http://ideabook.phodal.com/#背景-6)
97 | * [ShowCase](http://ideabook.phodal.com/#showcase-6)
98 | * [Ionic & Electron & Cordova](http://ideabook.phodal.com/#ionic-electron-cordova)
99 | * [步骤](http://ideabook.phodal.com/#步骤-7)
100 | * [Step 1: 从Web到混合应用,再到桌面应用](http://ideabook.phodal.com/#step-1-从web到混合应用再到桌面应用)
101 | * [Step 2: 响应式设计](http://ideabook.phodal.com/#step-2-响应式设计)
102 | * [Step 3: 平台特定代码](http://ideabook.phodal.com/#step-3-平台特定代码)
103 | * [未来](http://ideabook.phodal.com/#未来)
104 | * [Ionic与ElasticSearch打造O2O应用](http://ideabook.phodal.com/#ionic与elasticsearch打造o2o应用)
105 | * [概况](http://ideabook.phodal.com/#概况-8)
106 | * [背景](http://ideabook.phodal.com/#背景-7)
107 | * [Showcase](http://ideabook.phodal.com/#showcase-7)
108 | * [构架设计](http://ideabook.phodal.com/#构架设计)
109 | * [步骤](http://ideabook.phodal.com/#步骤-8)
110 | * [Step 1: Django GIS 设置](http://ideabook.phodal.com/#step-1-django-gis-设置)
111 | * [Step 2: 配置Haystack](http://ideabook.phodal.com/#step-2-配置haystack)
112 | * [Step 3: Django Haystack Model创建](http://ideabook.phodal.com/#step-3-django-haystack-model创建)
113 | * [Step 4: 创建search_index](http://ideabook.phodal.com/#step-4-创建search_index)
114 | * [Step 4: Ionic ElasticSearch 创建页面](http://ideabook.phodal.com/#step-4-ionic-elasticsearch-创建页面)
115 | * [Step 5: Ionic ElasticSearch Service](http://ideabook.phodal.com/#step-5-ionic-elasticsearch-service)
116 | * [Step 6: Ionic OpenLayer 地图显示](http://ideabook.phodal.com/#step-6-ionic-openlayer-地图显示)
117 | * [一步步搭建JavaScript框架](http://ideabook.phodal.com/#一步步搭建javascript框架)
118 | * [概况](http://ideabook.phodal.com/#概况-9)
119 | * [背景](http://ideabook.phodal.com/#背景-8)
120 | * [步骤](http://ideabook.phodal.com/#步骤-9)
121 | * [Step 1: 注册npm和bower包](http://ideabook.phodal.com/#step-1-注册npm和bower包)
122 | * [Step 2: 生成Javascript项目框架](http://ideabook.phodal.com/#step-2-生成javascript项目框架)
123 | * [Step 3: 寻找所需要的函数](http://ideabook.phodal.com/#step-3-寻找所需要的函数)
124 | * [Step 4: 整合](http://ideabook.phodal.com/#step-4-整合)
125 | * [Step 5: 测试](http://ideabook.phodal.com/#step-5-测试)
126 | * [练习建议](http://ideabook.phodal.com/#练习建议-4)
127 | * [制作简易Mac OS上的伪锁屏工具](http://ideabook.phodal.com/#制作简易mac-os上的伪锁屏工具)
128 | * [概况](http://ideabook.phodal.com/#概况-10)
129 | * [背景](http://ideabook.phodal.com/#背景-9)
130 | * [步骤](http://ideabook.phodal.com/#步骤-10)
131 | * [Step 1:屏幕截图](http://ideabook.phodal.com/#step-1屏幕截图)
132 | * [Step 2:调节亮度](http://ideabook.phodal.com/#step-2调节亮度)
133 | * [Step 3:全屏图片](http://ideabook.phodal.com/#step-3全屏图片)
134 | * [基于Virtual DOM的测试代码生成](http://ideabook.phodal.com/#基于virtual-dom的测试代码生成)
135 | * [概况](http://ideabook.phodal.com/#概况-11)
136 | * [背景](http://ideabook.phodal.com/#背景-10)
137 | * [ShowCase](http://ideabook.phodal.com/#showcase-8)
138 | * [基本原理](http://ideabook.phodal.com/#基本原理)
139 | * [步骤](http://ideabook.phodal.com/#步骤-11)
140 | * [Step 1: Virtual-dom与HyperScript](http://ideabook.phodal.com/#step-1-virtual-dom与hyperscript)
141 | * [Step 2: 标记DOM变化](http://ideabook.phodal.com/#step-2-标记dom变化)
142 | * [基于Backbone的单页面移动应用](http://ideabook.phodal.com/#基于backbone的单页面移动应用)
143 | * [概况](http://ideabook.phodal.com/#概况-12)
144 | * [背景](http://ideabook.phodal.com/#背景-11)
145 | * [Showcase](http://ideabook.phodal.com/#showcase-9)
146 | * [jQuery + Backbone + UnderScore + Require.JS](http://ideabook.phodal.com/#jquery-backbone-underscore-require.js)
147 | * [步骤](http://ideabook.phodal.com/#步骤-12)
148 | * [Step 1: 使用Require.js管理依赖](http://ideabook.phodal.com/#step-1-使用require.js管理依赖)
149 | * [Step 2: 添加路由](http://ideabook.phodal.com/#step-2-添加路由)
150 | * [Step 3: 创建主页View](http://ideabook.phodal.com/#step-3-创建主页view)
151 | * [Step 4: jQuery Sidr](http://ideabook.phodal.com/#step-4-jquery-sidr)
152 | * [Step 5: Django Tastypie示例](http://ideabook.phodal.com/#step-5-django-tastypie示例)
153 | * [Step 6: RequireJS Plugins](http://ideabook.phodal.com/#step-6-requirejs-plugins)
154 | * [Step 6: 简单的博客](http://ideabook.phodal.com/#step-6-简单的博客)
155 | * [Step 7: 重构](http://ideabook.phodal.com/#step-7-重构)
156 | * [Step 8: 移动CMS滑动](http://ideabook.phodal.com/#step-8-移动cms滑动)
157 | * [Oculus + Node.js + Three.js 打造VR世界](http://ideabook.phodal.com/#oculus-node.js-three.js-打造vr世界)
158 | * [概况](http://ideabook.phodal.com/#概况-13)
159 | * [背景](http://ideabook.phodal.com/#背景-12)
160 | * [Showcase](http://ideabook.phodal.com/#showcase-10)
161 | * [框架: Oculus Rift & Node NMD](http://ideabook.phodal.com/#框架-oculus-rift-node-nmd)
162 | * [步骤](http://ideabook.phodal.com/#步骤-13)
163 | * [Step 1: Node Oculus Services](http://ideabook.phodal.com/#step-1-node-oculus-services)
164 | * [Step 2: Node.js Oculus Hello,World](http://ideabook.phodal.com/#step-2-node.js-oculus-helloworld)
165 | * [Step 3: Node Oculus WebSocket](http://ideabook.phodal.com/#step-3-node-oculus-websocket)
166 | * [Step 4: Oculus Effect + DK2 Control](http://ideabook.phodal.com/#step-4-oculus-effect-dk2-control)
167 | * [Step 5: Three.js KeyHandler](http://ideabook.phodal.com/#step-5-three.js-keyhandler)
168 | * [练习建议](http://ideabook.phodal.com/#练习建议-5)
169 | * [手动制作照片地图](http://ideabook.phodal.com/#手动制作照片地图)
170 | * [概况](http://ideabook.phodal.com/#概况-14)
171 | * [Background:把照片放在地图上](http://ideabook.phodal.com/#background把照片放在地图上)
172 | * [Showcase](http://ideabook.phodal.com/#showcase-11)
173 | * [框架: EXIF & ExifRead & CartoDB](http://ideabook.phodal.com/#框架-exif-exifread-cartodb)
174 | * [步骤](http://ideabook.phodal.com/#步骤-14)
175 | * [Step 1: 解析读取照片信息](http://ideabook.phodal.com/#step-1-解析读取照片信息)
176 | * [Step 2: 上传数据](http://ideabook.phodal.com/#step-2-上传数据)
177 | * [练习建议](http://ideabook.phodal.com/#练习建议-6)
178 | * [D3.js打造技能树](http://ideabook.phodal.com/#d3.js打造技能树)
179 | * [概况](http://ideabook.phodal.com/#概况-15)
180 | * [背景](http://ideabook.phodal.com/#背景-13)
181 | * [Showcase](http://ideabook.phodal.com/#showcase-12)
182 | * [Graphviz](http://ideabook.phodal.com/#graphviz)
183 | * [步骤](http://ideabook.phodal.com/#步骤-15)
184 | * [Step 1: 打造简单的技能树](http://ideabook.phodal.com/#step-1-打造简单的技能树)
185 | * [Step 3: D3.js Tooltipster](http://ideabook.phodal.com/#step-3-d3.js-tooltipster)
186 | * [技术雷达趋势](http://ideabook.phodal.com/#技术雷达趋势)
187 | * [概况](http://ideabook.phodal.com/#概况-16)
188 | * [背景](http://ideabook.phodal.com/#背景-14)
189 | * [Showcase](http://ideabook.phodal.com/#showcase-13)
190 | * [D3.js](http://ideabook.phodal.com/#d3.js)
191 | * [步骤](http://ideabook.phodal.com/#步骤-16)
192 | * [Step 1: Schema与原始代码](http://ideabook.phodal.com/#step-1-schema与原始代码)
193 | * [Step 2: 处理数据](http://ideabook.phodal.com/#step-2-处理数据)
194 | * [文本转Logo](http://ideabook.phodal.com/#文本转logo)
195 | * [概况](http://ideabook.phodal.com/#概况-17)
196 | * [背景](http://ideabook.phodal.com/#背景-15)
197 | * [ShowCase](http://ideabook.phodal.com/#showcase-14)
198 | * [需求说明](http://ideabook.phodal.com/#需求说明)
199 | * [步骤](http://ideabook.phodal.com/#步骤-17)
200 | * [Step 1: Python 文字转Logo实战](http://ideabook.phodal.com/#step-1-python-文字转logo实战)
201 | * [GeoJSON与ElasticSearch实现高级图形搜索](http://ideabook.phodal.com/#geojson与elasticsearch实现高级图形搜索)
202 | * [概况](http://ideabook.phodal.com/#概况-18)
203 | * [Showcase](http://ideabook.phodal.com/#showcase-15)
204 | * [jQuery + Mustache + Leaflet](http://ideabook.phodal.com/#jquery-mustache-leaflet)
205 | * [步骤](http://ideabook.phodal.com/#步骤-18)
206 | * [Step 1: 离线地图与搜索](http://ideabook.phodal.com/#step-1-离线地图与搜索)
207 | * [Step 2: 从地点到地图上显示](http://ideabook.phodal.com/#step-2-从地点到地图上显示)
208 | * [Step 3: 从地图到地点上显示](http://ideabook.phodal.com/#step-3-从地图到地点上显示)
209 |
210 | License
211 | ---
212 |
213 | [](https://www.phodal.com/) [](http://ideas.phodal.com/)
214 |
215 | © 2016 [Phodal Huang](https://www.phodal.com). This code is distributed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 License. See `LICENSE` in this directory.
216 |
217 | [](http://www.xuntayizhan.com/person/ji-ke-ai-qing-zhi-er-shi-dai-wo-dai-ma-bian-cheng-qu-ni-wei-qi-ke-hao-wan/)
218 |
219 |
--------------------------------------------------------------------------------
/api/all.json:
--------------------------------------------------------------------------------
1 | {
2 | "content": [
3 | {
4 | "title": "分析网站日志,打造访问地图",
5 | "path": "chapters/big-data.md"
6 | },
7 | {
8 | "title": "书籍录入移动应用:条形码扫描",
9 | "path": "chapters/bookshelf.md"
10 | },
11 | {
12 | "title": "制作专属Badge",
13 | "path": "chapters/brand.md"
14 | },
15 | {
16 | "title": "微信文章编辑器",
17 | "path": "chapters/congee.md"
18 | },
19 | {
20 | "title": "JavaScript制作Slide框架",
21 | "path": "chapters/echoesworks.md"
22 | },
23 | {
24 | "title": "编辑-发布-分离的博客系统",
25 | "path": "chapters/eppsc.md"
26 | },
27 | {
28 | "title": "Solr实现多边形地理搜索",
29 | "path": "chapters/google-map-solr.md"
30 | },
31 | {
32 | "title": "一份代码打造跨平台应用",
33 | "path": "chapters/growth.md"
34 | },
35 | {
36 | "title": "Ionic与ElasticSearch打造O2O应用",
37 | "path": "chapters/ionic-es.md"
38 | },
39 | {
40 | "title": "一步步搭建JavaScript框架",
41 | "path": "chapters/lettuce.md"
42 | },
43 | {
44 | "title": "基于Virtual DOM的测试代码生成",
45 | "path": "chapters/luffa.md"
46 | },
47 | {
48 | "title": "基于Backbone的单页面移动应用",
49 | "path": "chapters/moqimobi.md"
50 | },
51 | {
52 | "title": "Oculus + Node.js + Three.js 打造VR世界",
53 | "path": "chapters/oculus-three.md"
54 | },
55 | {
56 | "title": "手动制作照片地图",
57 | "path": "chapters/onmap.md"
58 | },
59 | {
60 | "title": "D3.js打造技能树",
61 | "path": "chapters/sherlock.md"
62 | },
63 | {
64 | "title": "技术雷达趋势",
65 | "path": "chapters/tech-stack.md"
66 | },
67 | {
68 | "title": "文本转Logo",
69 | "path": "chapters/text2logo.md"
70 | },
71 | {
72 | "title": "制作简易Mac OS上的伪锁屏工具",
73 | "path": "chapters/lock.md"
74 | },
75 | {
76 | "title": "GeoJSON与ElasticSearch实现高级图形搜索",
77 | "path": "chapters/vmap.md"
78 | }
79 | ],
80 | "source": "http://ideabook.phodal.com/"
81 | }
82 |
--------------------------------------------------------------------------------
/build/author.html:
--------------------------------------------------------------------------------
1 |
Phodal's Idea实战指南
2 | By Phodal (Follow Me: 微博、知乎、SegmentFault)
3 |
4 |
5 | GitHub: Phodal's Idea实战指南
6 |
7 | 我的其他电子书:
8 |
14 |
15 | 微信公众号
16 | 
17 |
18 | 当前为预览版,在使用的过程中遇到任何遇到请及时与我联系。阅读过程中问题,不烦在GitHub上提出来:
19 | Idea Ebook Issues
20 |
21 |
22 | 阅读过程中遇到语法错误、拼写错误、技术错误等等,不烦来个Pull Request,这样可以帮助到其他阅读这本电子书的童鞋。
23 |
24 |
--------------------------------------------------------------------------------
/build/head.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/build/metadata.xml:
--------------------------------------------------------------------------------
1 | IdeaBook
2 | Phodal
3 | Creative Commons Attribution Non-Commercial Share Alike 3.0
4 | zh-CN
--------------------------------------------------------------------------------
/build/share.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
--------------------------------------------------------------------------------
/build/stats.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/build/stats.html
--------------------------------------------------------------------------------
/build/title.txt:
--------------------------------------------------------------------------------
1 | % IdeaBook
2 | % Phodal
--------------------------------------------------------------------------------
/chapters-en/big-data.md:
--------------------------------------------------------------------------------
1 | Analytics website log, build visitors' map
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### Showcase
10 |
11 | ### Hadoop + Pig + Jython + AmMap + ElasticSearch
12 |
13 | Steps
14 | ---
15 |
16 | ### Step 1: setup infrastructure
17 |
18 | ### Step 2: parse access.log
19 |
20 | ### Step 3: convert IP to GEO
21 |
22 | ### Step 4: show in map
23 |
24 | Practise
25 | ---
26 |
--------------------------------------------------------------------------------
/chapters-en/bookself.md:
--------------------------------------------------------------------------------
1 | Book barcode mobile application
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### Showcase
10 |
11 | ### Ionic + Zxing
12 |
13 | Steps
14 | ---
15 |
16 | ### Step 1: ZXing and Douban API
17 |
18 | ### Step 2: Save in database
19 |
20 | ### Step 3: Upload data
21 |
22 | Practise
23 | ---
24 |
--------------------------------------------------------------------------------
/chapters-en/brand.md:
--------------------------------------------------------------------------------
1 | Code generate exclusive badge
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### Showcase
10 |
11 | ### SVG + SVGWrite
12 |
13 | Steps
14 | ---
15 |
16 | ### Step 1: Basic graphics
17 |
18 | ### Step 2: Advanced Background
19 |
20 | ### Step 3: Done
21 |
22 | Practise
23 | ---
24 |
--------------------------------------------------------------------------------
/chapters-en/congee.md:
--------------------------------------------------------------------------------
1 | Custom styles WeChat Public Editor
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### Showcase
10 |
11 | ### CKEditor + Ractive
12 |
13 | Steps
14 | ---
15 |
16 | ### Step 1: hello,world
17 |
18 | ### Step 2: Require.js as module tool
19 |
20 | ### Step 3: initialize
21 |
22 | #### CKEditor initialize
23 |
24 | #### ColorPicker initialize
25 |
26 | ### Step 4: Create view
27 |
28 | Practise
29 | ---
30 |
--------------------------------------------------------------------------------
/chapters-en/echoesworks.md:
--------------------------------------------------------------------------------
1 | Build Web Slide Framework
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### AC
10 |
11 | Steps
12 | ---
13 |
14 | ### Step 1: basic slide function
15 |
16 | ### Step 2: parse markdown
17 |
18 | ### Step 3: event handle
19 |
20 | ### Step 4: parse time subs
21 |
22 | ### Step 5: add processbar
23 |
24 | ### Step 6: sync
25 |
26 | Practise
27 | ---
28 |
--------------------------------------------------------------------------------
/chapters-en/eppsc.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/chapters-en/eppsc.md
--------------------------------------------------------------------------------
/chapters-en/google-map-solr.md:
--------------------------------------------------------------------------------
1 | Solr + Google Map build polygon search
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### Showcase
10 |
11 | Steps
12 | ---
13 |
14 | ### Step 1: Solr Flask
15 |
16 | ### Step 2: Google map Polygon
17 |
18 | Practise
19 | ---
20 |
--------------------------------------------------------------------------------
/chapters-en/growth.md:
--------------------------------------------------------------------------------
1 | A Code build Cross-platform application
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### ShowCase
10 |
11 | ### Ionic & Electron & Cordova
12 |
13 | Steps
14 | ---
15 |
16 | ### Step 1: Web to Hybird app, to desktop app
17 |
18 | ### Step 2: responsive design
19 |
20 | ### Step 3: platform specific code
21 |
22 | #### storage
23 |
24 | #### analytics
25 |
26 | #### auto-update
27 |
28 | #### desktop app
29 |
30 | ### Step 4: future
31 |
32 | Practise
33 | ---
34 |
--------------------------------------------------------------------------------
/chapters-en/ionic-es.md:
--------------------------------------------------------------------------------
1 | Ionic & ElasticSearch build O2O application
2 | ===
3 |
4 | Overview
5 | ---
6 |
7 | ### Background
8 |
9 | ### ShowCase
10 |
11 | ### Architecture Design
12 |
13 | Steps
14 | ---
15 |
16 | ### Step 1: Django GIS
17 |
18 | ### Step 2: config haystack
19 |
20 | ### Step 3: Django Haystack Model
21 |
22 | ### Step 4: create search index
23 |
24 | ### Step 5: create page
25 |
26 | ### Step 6: Ionic ElasticSearch Service
27 |
28 | ### Step 7: Ionic OpenLayer Map
29 |
30 | Practise
31 | ---
32 |
--------------------------------------------------------------------------------
/chapters/about.md:
--------------------------------------------------------------------------------
1 | Phodal's Idea实战指南
2 | ===
3 |
4 | 关于作者
5 | ---
6 |
7 | 黄峰达(Phodal Huang)是一个创客、工程师、咨询师和作家。他毕业于西安文理学院电子信息工程专业,现作为一个咨询师就职于 ThoughtWorks 深圳。长期活跃于开源软件社区 GitHub,目前专注于物联网和前端领域。
8 |
9 | 作为一个开源软件作者,著有 Growth、Stepping、Lan、Echoesworks 等软件。其中开源学习应用 Growth,广受读者和用户好评,可在 APP Store 及各大 Android 应用商店下载。
10 |
11 | 作为一个技术作者,著有《自己动手设计物联网》(电子工业出版社)、《全栈应用开发:精益实践》(电子工业出版社,正在出版)。并在 GitHub 上开源有《Growth: 全栈增长工程师指南》、《GitHub 漫游指南》等七本电子书。
12 |
13 | 作为技术专家,他为英国 Packt 出版社审阅有物联网书籍《Learning IoT》、《Smart IoT》,前端书籍《Angular 2 Serices》、《Getting started with Angular》等技术书籍。
14 |
15 | 他热爱编程、写作、设计、旅行、hacking,你可以从他的个人网站:[https://www.phodal.com/](https://www.phodal.com/) 了解到更多的内容。
16 |
17 | 其它相关信息:
18 |
19 | - 微博:[http://weibo.com/phodal](http://weibo.com/phodal)
20 | - GitHub: [https://github.com/phodal](https://github.com/phodal)
21 | - 知乎:[https://www.zhihu.com/people/phodal](https://www.zhihu.com/people/phodal)
22 | - SegmentFault:[https://segmentfault.com/u/phodal](https://segmentfault.com/u/phodal)
23 |
24 | 当前为预览版,在使用的过程中遇到任何问题请及时与我联系。阅读过程中的问题,不妨在GitHub上提出来: [Issues](https://github.com/phodal/fe/issues)
25 |
26 | 阅读过程中遇到语法错误、拼写错误、技术错误等等,不妨来个Pull Request,这样可以帮助到其他阅读这本电子书的童鞋。
27 |
28 | 我的电子书:
29 |
30 | * 《[GitHub 漫游指南](https://github.com/phodal/github-roam)》
31 | * 《[我的职业是前端工程师](https://github.com/phodal/fe)》
32 | * 《[Serverless 架构应用开发指南](https://github.com/phodal/serverless)》
33 | * 《[Growth: 全栈增长工程师指南](https://github.com/phodal/growth-ebook)》
34 | * 《[Phodal's Idea实战指南](https://github.com/phodal/ideabook)》
35 | * 《[一步步搭建物联网系统](https://github.com/phodal/designiot)》
36 | * 《[RePractise](https://github.com/phodal/repractise)》
37 | * 《[Growth: 全栈增长工程师实战](https://github.com/phodal/growth-in-action)》
38 |
39 | 我的微信公众号:
40 |
41 | 
42 |
43 | 支持作者,可以加入作者的小密圈:
44 |
45 | 
46 |
47 | 或者转账:
48 |
49 |  
50 |
--------------------------------------------------------------------------------
/chapters/big-data.md:
--------------------------------------------------------------------------------
1 | 分析网站日志,打造访问地图
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 这个项目的背景是起源于,我有一个2G左右的网站访问日志。我想看看访问网站的人都来自哪里,于是我想开始想办法来分析这日志。当时正值大数据火热的时候,便想拿着Hadoop来做这样一件事。
10 |
11 | ### ShowCase
12 |
13 | 最后的效果如下图如示:
14 |
15 | 
16 |
17 | 这是一个Web生成的界面,通过Elastic.js向搜索引擎查询数据,将再这些数据渲染到地图上。
18 |
19 | ### Hadoop + Pig + Jython + AmMap + ElasticSearch
20 |
21 | 我们使用的技术栈有上面这些,他们的简介如下:
22 |
23 | - Hadoop是一个由Apache基金会所开发的分布式系统基础架构。用户可以在不了解分布式底层细节的情况下,开发分布式程序。充分利用集群的威力进行高速运算和存储。
24 | - Pig 是一个基于Hadoop的大规模数据分析平台,它提供的SQL-LIKE语言叫Pig Latin,该语言的编译器会把类SQL的数据分析请求转换为一系列经过优化处理的MapReduce运算。
25 | - Jython是一种完整的语言,而不是一个Java翻译器或仅仅是一个Python编译器,它是一个Python语言在Java中的完全实现。Jython也有很多从CPython中继承的模块库。
26 | - AmMap是用于创建交互式Flash地图的工具。您可以使用此工具来显示您的办公室地点,您的行程路线,创建您的经销商地图等。
27 | - ElasticSearch是一个基于Lucene 构建的开源,分布式,RESTful 搜索引擎。 设计用于云计算中,能够达到搜索实时、稳定、可靠和快速,并且安装使用方便。
28 |
29 | 步骤
30 | ---
31 |
32 | 总的步骤并不是很复杂,可以分为:
33 |
34 | - 搭建基础设施
35 | - 解析access.log
36 | - 转换IP为GEO信息
37 | - 展示数据到地图上
38 |
39 | ### Step 1: 搭建基础设施
40 |
41 | 在这一些系列的实战中,比较麻烦的就是安装这些工具,我们需要安装上面提到的一系列工具。对于不同的系统来说,都有相似的安装工具:
42 |
43 | - Windows上可以使用Chocolatey
44 | - Ubuntu / Mint上可以使用aptitude
45 | - CentOS / OpenSUSE上可以使用yum安装
46 | - Mac OS上可以使用brew安装
47 |
48 | 如下是Mac OS下安装Hadoop、Pig、Elasticsearch、Jython的方式
49 |
50 | ```bash
51 | brew install hadoop
52 | brew install pig
53 | brew install elasticsearch
54 | brew install jython
55 | ```
56 |
57 | 对于其他操作系统也可以使用相似的方法来安装。接着我们还需要安装一个Hadoop的插件,用于连接Hadoop和ElasticSearch。
58 |
59 | 下载地址:[https://github.com/elastic/elasticsearch-hadoop](https://github.com/elastic/elasticsearch-hadoop)
60 |
61 | 复制其中的``elasticsearch-hadoop-*.jar``、``elasticsearch-hadoop-pig-*.jar``到你的pig库的目录,如我的是:``/usr/local/Cellar/pig/0.14.0``。
62 |
63 | 由于我使用提早期的版本,所以这里我的文件名是:``elasticsearch-hadoop-2.1.0.Beta3.jar``、``elasticsearch-hadoop-pig-2.1.0.Beta3.jar``。
64 |
65 | 下面我们就可以尝试去解析我们的日志了。
66 |
67 | ### Step 2: 解析access.log
68 |
69 | 在开始解析之前,先让我们来看看几条Nginx的日志:
70 |
71 | ```
72 | 106.39.113.203 - - [28/Apr/2016:10:40:31 +0000] "GET / HTTP/2.0" 200 0 "https://www.phodal.com/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36" -
73 | 66.249.65.119 - - [28/Apr/2016:10:40:51 +0000] "GET /set_device/default/?next=/blog/use-falcon-peewee-build-high-performance-restful-services-wordpress/ HTTP/1.1" 302 5 "-" "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" -
74 | ```
75 |
76 | 而上面的日志实际上是有对应的格式的,这个格式写在我们的Nginx配置文件中。如下是上面的日志的格式:
77 |
78 | ```
79 | log_format access $remote_addr - $remote_user [$time_local] "$request" '
80 | '$status $body_bytes_sent "$http_referer" '
81 | '"$http_user_agent" $http_x_forwarded_for';
82 | ```
83 |
84 | 在最前面的是访问者的IP地址,然后是访问者的当地时间、请求的类型、状态码、访问的URL、用户的User Agent等等。随后,我们就可以针对上面的格式编写相应的程序,这些代码如下所示:
85 |
86 | ```
87 | register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/piggybank.jar;
88 | register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/elasticsearch-hadoop-pig-2.1.0.Beta3.jar;
89 |
90 | RAW_LOGS = LOAD 'data/access.log' USING TextLoader as (line:chararray);
91 |
92 | LOGS_BASE = FOREACH RAW_LOGS GENERATE
93 | FLATTEN(
94 | REGEX_EXTRACT_ALL(line, '(\\S+) - - \\[([^\\[]+)\\]\\s+"([^"]+)"\\s+(\\d+)\\s+(\\d+)\\s+"([^"]+)"\\s+"([^"]+)"\\s+-')
95 | )
96 | AS (
97 | ip: chararray,
98 | timestamp: chararray,
99 | url: chararray,
100 | status: chararray,
101 | bytes: chararray,
102 | referrer: chararray,
103 | useragent: chararray
104 | );
105 |
106 | A = FOREACH LOGS_BASE GENERATE ToDate(timestamp, 'dd/MMM/yyyy:HH:mm:ss Z') as date, ip, url,(int)status,(int)bytes,referrer,useragent;
107 | --B = GROUP A BY (timestamp);
108 | --C = FOREACH B GENERATE FLATTEN(group) as (timestamp), COUNT(A) as count;
109 | --D = ORDER C BY timestamp,count desc;
110 | STORE A INTO 'nginx/log' USING org.elasticsearch.hadoop.pig.EsStorage();
111 | ```
112 |
113 | 在第1~2行里,我们使用了自定义的jar文件。接着在第4行,载入了log文件,并其值赋予RAW_LOGS。随后的第6行里,我们取出RAW_LOGS中的每一个值 ,根据下面的正则表达式,取出其对应的值到对象里,如``- -``前面的(\\S+)对应的是ip,最后将这些值赋给LOGS_BASE。
114 |
115 | 接着,我们就可以对值进行一些特殊的处理,如A是转化时间戳后的结果。B是按时间戳排序后的结果。最后,我们再将这些值存储到ElasticSearch对应的索引``nginx/log``中。
116 |
117 | ### Step 3: 转换IP为GEO信息
118 |
119 | 在简单地完成了一个Demo之后,我们就可以将IP转换为GEO信息了,这里我们需要用到一个名为pygeoip的库。GeoIP是一个根据IP地址查询位置的API的集成。它支持对国家、地区、城市、纬度和经度的查询。实际上,就是在一个数据库中有对应的国家和地区的IP段,根据这个IP段,我们就可以获取对应的地理位置。
120 |
121 | 由于使用Java来实现这个功能比较麻烦,这里我们就使用Jython来实现。大部分的过程和上面都是一样的,除了注册了一个自定义的库,并在这个库里使用了解析GEO的方法,代码如下所示:
122 |
123 | ```
124 | register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/piggybank.jar;
125 | register file:/usr/local/Cellar/pig/0.14.0/libexec/lib/elasticsearch-hadoop-pig-2.1.0.Beta3.jar;
126 | register utils.py using jython as utils;
127 |
128 | RAW_LOGS = LOAD 'data/access.log' USING TextLoader as (line:chararray);
129 |
130 | LOGS_BASE = FOREACH RAW_LOGS GENERATE
131 | FLATTEN(
132 | REGEX_EXTRACT_ALL(line, '(\\S+) - - \\[([^\\[]+)\\]\\s+"([^"]+)"\\s+(\\d+)\\s+(\\d+)\\s+"([^"]+)"\\s+"([^"]+)"\\s+-')
133 | )
134 | AS (
135 | ip: chararray,
136 | timestamp: chararray,
137 | url: chararray,
138 | status: chararray,
139 | bytes: chararray,
140 | referrer: chararray,
141 | useragent: chararray
142 | );
143 |
144 | A = FOREACH LOGS_BASE GENERATE ToDate(timestamp, 'dd/MMM/yyyy:HH:mm:ss Z') as date, utils.get_country(ip) as country,
145 | utils.get_city(ip) as city, utils.get_geo(ip) as location,ip,
146 | url, (int)status,(int)bytes,referrer,useragent;
147 |
148 | STORE A INTO 'nginx/log' USING org.elasticsearch.hadoop.pig.EsStorage();
149 | ```
150 |
151 | 在第三行里,我们注册了``utils.py``并将其中的函数作为utils。接着在倒数第二行里,我们执行了四个utils函数。即:
152 |
153 | - get_country从IP中解析出国家
154 | - get_city从IP中解析出城市
155 | - get_geo从IP中解析出经纬度信息
156 |
157 | 其对应的Python代码如下所示:
158 |
159 | ```
160 | #!/usr/bin/python
161 |
162 | import sys
163 | sys.path.append('/Users/fdhuang/test/lib/python2.7/site-packages/')
164 | import pygeoip
165 | gi = pygeoip.GeoIP("data/GeoLiteCity.dat")
166 |
167 | @outputSchema('city:chararray')
168 | def get_city(ip):
169 | try:
170 | city = gi.record_by_name(ip)["city"]
171 | return city
172 | except:
173 | pass
174 |
175 |
176 | @outputSchema('country:chararray')
177 | def get_country(ip):
178 | try:
179 | city = gi.record_by_name(ip)["country_name"]
180 | return city
181 | except:
182 | pass
183 |
184 | @outputSchema('location:chararray')
185 | def get_geo(ip):
186 | try:
187 | geo = str(gi.record_by_name(ip)["longitude"]) + "," + str(gi.record_by_name(ip)["latitude"])
188 | return geo
189 | except:
190 | pass
191 | ```
192 |
193 | 代码相应的简单,和一般的Python代码也没有啥区别。这里一些用户自定义函数,在函数的最前面有一个``outputSchema``,用于返回输出的结果。
194 |
195 | ### Step 4: 展示数据到地图上
196 |
197 | 现在,我们终于可以将数据转化到可视化界面了。开始之前,我们需要几个库
198 |
199 | - jquery 地球人都知道
200 | - elasticsearch.jquery即用于搜索功能
201 | - ammap用于制作交互地图。
202 |
203 | 添加这些库到html文件里:
204 |
205 | ```html
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 | ```
215 |
216 | 生成过程大致如下所示:
217 |
218 | - 获取不同国家的全名,用于解析出全名,如US -> "United States"
219 | - 查找ElasticSearch搜索引擎中的数据,并计算访问量
220 | - 再将数据渲染到地图上
221 |
222 | 对应的main文件如下所示:
223 |
224 | ```javascript
225 | var client = new $.es.Client({
226 | hosts: 'localhost:9200'
227 | });
228 |
229 | // 创建ElasticSearch搜索条件
230 |
231 | var query = {
232 | index: 'nginx',
233 | type: 'log',
234 | size: 200,
235 | body: {
236 | query: {
237 | query_string: {
238 | query: "*"
239 | }
240 | },
241 | aggs: {
242 | 2: {
243 | terms: {
244 | field: "country",
245 | size: 200,
246 | order: {
247 | _count: "desc"
248 | }
249 | }
250 | }
251 | }
252 | }
253 | };
254 |
255 | // 获取到country.json后生成数据
256 |
257 | $(document).ready(function () {
258 | $.ajax({
259 | type: "GET",
260 | url: "country.json",
261 | success: function (data) {
262 | generate_info(data)
263 | }
264 | });
265 | });
266 |
267 | // 根据数据中的国家名,来计算不同国家的访问量大小。
268 |
269 | var generate_info = function(data){
270 | var mapDatas = [];
271 | client.search(query).then(function (results) {
272 | $.each(results.aggregations[2].buckets, function(index, bucket){
273 | var mapData;
274 | $.each(data, function(index, country){
275 | if(country.name.toLowerCase() === bucket.key) {
276 | mapData = {
277 | code: country.code,
278 | name: country.name,
279 | value: bucket.doc_count,
280 | color: "#eea638"
281 | };
282 | }
283 | });
284 | if(mapData !== undefined){
285 | mapDatas.push(mapData);
286 | }
287 | });
288 | create_map(mapDatas);
289 | });
290 | };
291 |
292 | var create_map = function(mapData){
293 | var map;
294 | var minBulletSize = 3;
295 | var maxBulletSize = 70;
296 | var min = Infinity;
297 | var max = -Infinity;
298 |
299 | AmCharts.theme = AmCharts.themes.black;
300 |
301 | for (var i = 0; i < mapData.length; i++) {
302 | var value = mapData[i].value;
303 | if (value < min) {
304 | min = value;
305 | }
306 | if (value > max) {
307 | max = value;
308 | }
309 | }
310 |
311 | map = new AmCharts.AmMap();
312 | map.pathToImages = "bower_components/ammap/dist/ammap/images/";
313 |
314 | map.areasSettings = {
315 | unlistedAreasColor: "#FFFFFF",
316 | unlistedAreasAlpha: 0.1
317 | };
318 |
319 | map.imagesSettings = {
320 | balloonText: "[[title]]: [[value]]",
321 | alpha: 0.6
322 | };
323 |
324 | var dataProvider = {
325 | mapVar: AmCharts.maps.worldLow,
326 | images: []
327 | };
328 |
329 | var maxSquare = maxBulletSize * maxBulletSize * 2 * Math.PI;
330 | var minSquare = minBulletSize * minBulletSize * 2 * Math.PI;
331 |
332 | for (var i = 0; i < mapData.length; i++) {
333 | var dataItem = mapData[i];
334 | var value = dataItem.value;
335 | // calculate size of a bubble
336 | var square = (value - min) / (max - min) * (maxSquare - minSquare) + minSquare;
337 | if (square < minSquare) {
338 | square = minSquare;
339 | }
340 | var size = Math.sqrt(square / (Math.PI * 2));
341 | var id = dataItem.code;
342 |
343 | dataProvider.images.push({
344 | type: "circle",
345 | width: size,
346 | height: size,
347 | color: dataItem.color,
348 | longitude: latlong[id].longitude,
349 | latitude: latlong[id].latitude,
350 | title: dataItem.name,
351 | value: value
352 | });
353 | }
354 |
355 | map.dataProvider = dataProvider;
356 |
357 | map.write("mapdiv");
358 | };
359 | ```
360 |
361 | 我们可以看到比较麻烦的地方就是生成地图上的数量点,也就是create_map函数。
362 |
363 | ### 练习建议
364 |
--------------------------------------------------------------------------------
/chapters/bookshelf.md:
--------------------------------------------------------------------------------
1 | 书籍录入移动应用:条形码扫描
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 这个项目的起源是我想录入我的书架上的书籍——当时,大概有近四百本左右。由于大部分的手机软件都是收费的,或封闭的,因此我便想着自己写一个app来完成书籍的录入。
10 |
11 | ### ShowCase
12 |
13 | 最后的效果如下图所示:
14 |
15 | 
16 |
17 | 代码见: [https://github.com/phodal/bookshelf/](https://github.com/phodal/bookshelf/)
18 |
19 | ### Ionic + Zxing
20 |
21 | 所需要的移动框架还是Ionic,用于扫描条形码的库是ZXing。
22 |
23 | 步骤
24 | ---
25 |
26 | 开始之前,我们需要先安装Ionic,并且使用它来创建一个APP。然后我们还需要添加对应的二维码扫描库,代码如下所示:
27 |
28 | ```bash
29 | phonegap plugin add phonegap-plugin-barcodescanner
30 | ```
31 | 7
32 | 接着我们就可以开始制作我们的APP了。
33 |
34 | ###Step 1: ZXing扫描与Douban API
35 |
36 | 我们需要在我们的模板里,添加一个ICON或者按钮来触发程序调用相应的函数:
37 |
38 | ```html
39 |
40 | ```
41 |
42 | 在我们的函数里,我们只需要调用cordovaBarcodeScanner的scan方法就可以获取到二维码的值。再用$http.get去获取豆瓣API的相应的结果,并且将这个结果存储到数据库中。代码如下所示:
43 |
44 | ```javascript
45 | $scope.scan = function () {
46 | $cordovaBarcodeScanner
47 | .scan()
48 | .then(function (barcodeData) {
49 | $scope.info = barcodeData.text;
50 | $http.get("https://api.douban.com/v2/book/isbn/" + barcodeData.text).success(function (data) {
51 | $scope.detail = data;
52 | saveToDatabase(data, barcodeData);
53 | });
54 | }, function (error) {
55 | alert(error);
56 | });
57 | }
58 | ```
59 |
60 | 随后,我们就可以创建我们的代码来保存数据到数据库中。
61 |
62 | ###Step 2: 存储数据库
63 |
64 | 开始之前,我们需要添加Cordova的SQLite插件:
65 |
66 | ```bash
67 | cordova plugin add https://github.com/litehelpers/Cordova-sqlite-storage.git
68 | ```
69 |
70 | 在系统初始化的时候,创建对应的数据库及其表。
71 |
72 | ```javascript
73 | if(window.cordova) {
74 | //$cordovaSQLite.deleteDB("my.db");
75 | db = $cordovaSQLite.openDB("my.db");
76 | } else {
77 | db = window.openDatabase("my.db", "1.0", "bookshelf", -1);
78 | }
79 | $cordovaSQLite.execute(db, "CREATE TABLE IF NOT EXISTS bookshelf (id integer primary key, title text, image text, publisher text, author text, isbn text, summary text)");
80 | ```
81 |
82 | 接着,我们就可以上一步获取的数据取出相应的字段,调用bookshelfDB服务将其存储到数据库中。
83 |
84 | ```javascript
85 | function saveToDatabase(data, barcodeData) {
86 | bookshelfDB.add({
87 | title: data.title,
88 | image: data.image,
89 | publisher: data.publisher,
90 | author: data.author,
91 | summary: data.summary,
92 | isbn: barcodeData.text
93 | });
94 | }
95 | ```
96 |
97 | 下面就是我们的bookshelfDB服务,我们实现了get、add、remove、update,即CRUD。
98 |
99 | ```javascript
100 | .factory('bookshelfDB', function($cordovaSQLite, DBA) {
101 | var self = this;
102 | self.all = function() {
103 | return DBA.query("SELECT id, title, image, publisher, author, isbn, summary FROM bookshelf")
104 | .then(function(result){
105 | return DBA.getAll(result);
106 | });
107 | };
108 | self.get = function(memberId) {
109 | var parameters = [memberId];
110 | return DBA.query("SELECT id, title, image, publisher, author, isbn, summary FROM bookshelf WHERE id = (?)", parameters)
111 | .then(function(result) {
112 | return DBA.getById(result);
113 | });
114 | };
115 | self.add = function(member) {
116 | var parameters = [member.id, member.title, member.image, member.publisher, member.author, member.isbn, member.summary];
117 | return DBA.query("INSERT INTO bookshelf (id, title, image, publisher, author, isbn, summary) VALUES (?,?,?,?,?,?,?)", parameters);
118 | };
119 | self.remove = function(member) {
120 | var parameters = [member.id];
121 | return DBA.query("DELETE FROM bookshelf WHERE id = (?)", parameters);
122 | };
123 | self.update = function(origMember, editMember) {
124 | var parameters = [editMember.id, editMember.title, origMember.id];
125 | return DBA.query("UPDATE bookshelf SET id = (?), title = (?) WHERE id = (?)", parameters);
126 | };
127 | return self;
128 | })
129 | ```
130 |
131 | ### 上传数据
132 |
133 | ### 练习建议
134 |
--------------------------------------------------------------------------------
/chapters/brand.md:
--------------------------------------------------------------------------------
1 | 制作专属Badge
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 前几天,再次看到一些CI的Badge的时候,就想着要做一个自己的Badge:
10 |
11 | 
12 |
13 | 接着,我就找了个图形工具简单地先设计了下面的一个Badge:
14 |
15 | 
16 |
17 | 生成的格式是SVG,接着我就打开SVG看看里面发现了什么。
18 |
19 | ```xml
20 |
21 |
37 | ```
38 |
39 | 看了看代码很简单,我就想这可以用代码生成——我就可以生成出不同的样子了。
40 |
41 | ### ShowCase
42 |
43 | 
44 |
45 | 代码: GitHub: [https://github.com/phodal/brand](https://github.com/phodal/brand)
46 |
47 | ### SVG与SVGWrite
48 |
49 | SVG就是一个XML
50 |
51 | > 可缩放矢量图形(Scalable Vector Graphics,SVG) ,是一种用来描述二维矢量图形的XML 标记语言。
52 |
53 | 要对这个XML进行修改也是一件很容易的事。只是,先找了PIL发现不支持,就找到了一个名为SVGWrite的工具。
54 |
55 | > A Python library to create SVG drawings.
56 |
57 | 步骤
58 | ---
59 |
60 | ### Step 1: 基本图形
61 |
62 | 示例代码如下:
63 |
64 | ```python
65 | import svgwrite
66 |
67 | dwg = svgwrite.Drawing('test.svg', profile='tiny')
68 | dwg.add(dwg.line((0, 0), (10, 0), stroke=svgwrite.rgb(10, 10, 16, '%')))
69 | dwg.add(dwg.text('Test', insert=(0, 0.2)))
70 | dwg.save()
71 | ```
72 |
73 | 然后我就照猫画虎地写了一个:
74 |
75 | ```python
76 | import svgwrite
77 |
78 | dwg = svgwrite.Drawing('idea.svg', profile='full', size=(u'1006', u'150'))
79 |
80 | shapes = dwg.add(dwg.g(id='shapes', fill='none'))
81 | shapes.add(dwg.rect((0, 0), (640, 150), fill='#5E6772'))
82 | shapes.add(dwg.rect((640, 0), (366, 150), fill='#2196F3'))
83 | shapes.add(dwg.text('PHODAL', insert=(83, 119), fill='#FFFFFF',font_size=120, font_family='Helvetica'))
84 | shapes.add(dwg.text('idea', insert=(704, 122), fill='#FFFFFF', font_size=120, font_family='Helvetica'))
85 |
86 | dwg.save()
87 | ```
88 |
89 | 发现和上面的样式几乎是一样的,就顺手做了剩下的几个。然后想了想,我这样做都一样,一点都不好看。
90 |
91 | ### Step 2: 高级Badge
92 |
93 | 第一眼看到
94 |
95 | 
96 |
97 | 我就想着要不和这个一样好了,不就是画几条线的事么。
98 |
99 | ```python
100 |
101 | def draw_for_bg_plus():
102 | for x in range(y_text_split + rect_length, width, rect_length):
103 | shapes.add(dwg.line((x, 0), (x, height), stroke='#EEEEEE', stroke_opacity=0.3))
104 |
105 | for y in range(rect_length, height, rect_length):
106 | shapes.add(dwg.line((y_text_split, y), (width, y), stroke='#EEEEEE', stroke_opacity=0.3))
107 |
108 | for x in range(y_text_split + max_rect_length, width, max_rect_length):
109 | for y in range(0, height, max_rect_length):
110 | shapes.add(dwg.line((x, y - 4), (x, y + 4), stroke='#EEEEEE', stroke_width='2', stroke_opacity=0.4))
111 |
112 | for y in range(0, height, max_rect_length):
113 | for x in range(y_text_split + max_rect_length, width, max_rect_length):
114 | shapes.add(dwg.line((x - 4, y), (x + 4, y), stroke='#EEEEEE', stroke_width='2', stroke_opacity=0.4))
115 |
116 | draw_for_bg_plus()
117 | ```
118 |
119 | 就有了下面的图,于是我又按照这种感觉来了好几下
120 |
121 | 
122 |
123 |
124 | ### Step 3: 完成
125 |
126 | 练习建议
127 | ---
128 |
129 |
--------------------------------------------------------------------------------
/chapters/congee.md:
--------------------------------------------------------------------------------
1 | 微信文章编辑器
2 | =====
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | ### ShowCase
10 |
11 | 
12 |
13 | GitHub: [https://github.com/phodal/congee](https://github.com/phodal/congee)
14 |
15 | ### CKEditor + Ractive
16 |
17 | 选用怎样的前端框架是一个有趣的话题,我需要一个数据绑定和模板。首先,我排除了React这个框架,我觉得他的模板会给我带来一堆麻烦事。Angluar是一个不错的选择,但是考虑Angluar 2.0就放弃了,Backbone也用了那么久。Knockout.js又进入了我的视野,但是后来我发现数据绑定到模板有点难。最后选了Ractive,后来发现果然上手很轻松。
18 |
19 | Ractive这个框架比React诞生早了一个月,还是以DOM为核心。Ractive自称是一个模板驱动UI的库,在Github上说是下一代的DOM操作。因为Virtual Dom的出现,这个框架并没有那么流行。
20 |
21 | 起先,这个框架是在卫报创建的用于产生新闻的应用程序 。有很多工具可以帮助我们构建Web应用程序 ,但是很少会考虑基本的问题:HTML,一个优秀的静态模板,但是并没有为交互设计。Ractive可以将一个模板插到DOM中,并且可以动态的改变它。
22 |
23 | 步骤
24 | ---
25 |
26 | 在创建这个项目的时候,我的迭代过程大致如下:
27 |
28 | - 创建hello,world —— 结合不同的几个框架
29 | - 创建基本的样式集
30 | - 引用ColorPicker来对颜色进行处理
31 | - 重构代码
32 |
33 |
34 | ### Step 1: hello,world
35 |
36 | 下面是一个简单的hello,world。
37 |
38 | ```html
39 |
42 |
43 |
49 | ```
50 |
51 | 这个hello,world和一般的MVC框架并没有太大区别,甚至和我们用的Backbone很像。然后,让我们来看一个事件的例子:
52 |
53 | ```javascript
54 | listView = new Ractive({
55 | el: 'sandboxTitle',
56 | template: listTemplate,
57 | data: {color: config.defaultColor, 'fontSize': config.defaultFontSize}
58 | });
59 |
60 | listView.on('changeColor', function (args) {
61 | listView.set('color', args.color);
62 | });
63 | ```
64 |
65 | 这是在监听,意味着你需要在某个地方Fire这个事件:
66 |
67 | ```javascript
68 | titleView.fire('changeColor', {color: color.toHexString()});
69 | ```
70 |
71 | 接着,问题来了,这和我们jQuery的on,或者React的handleClick似乎没有太大的区别。接着Component来了:
72 |
73 | ```javascript
74 | var Grid = Ractive.extend({
75 | isolated: false,
76 | template: parasTemplate,
77 | data: {
78 | }
79 | });
80 |
81 | var dataValue = 5;
82 | var category = 'category-3';
83 |
84 | var color = config.defaultColor;
85 |
86 | parasView = new Ractive({
87 | el: 'parasSanbox',
88 | template: '',
89 | components: {Grid: Grid},
90 | data: {
91 | styles: [
92 | {section_style: 'border: 2px dotted #4caf50; margin: 8px 14px; padding: 10px; border-radius: 14px;', p_style: 'font-size: 14px;', color: color, data_value: dataValue, category: category},
93 | ]
94 | }
95 | });
96 |
97 | parasView.on('changeColor', function(args) {
98 | parasView.findComponent('Grid').set('Style.*.color', args.color);
99 | });
100 | ```
101 |
102 | 上面是在[https://github.com/phodal/congee](https://github.com/phodal/congee)中用到的多个模板的View,他们用了同一个component。
103 |
104 | 对比和介绍就在这里结束了,我们就可以开始这个项目的实战了。
105 |
106 | ### Step 2: Require.js模块化
107 |
108 | 同样的在这里,我们也使用Require.js来作模块化和依赖管理。我们的项目的配置如下:
109 |
110 | ```javascript
111 | require(['scripts/app', 'ractive', 'scripts/views/titleView', 'scripts/views/hrView', 'scripts/views/parasView', 'scripts/views/followView', 'jquery', 'spectrum'],
112 | function (App, Ractive, TitleView, ParasView, HRView, FollowView, $) {
113 | 'use strict';
114 |
115 | App.init();
116 | Ractive.DEBUG = false;
117 | var config = App.config;
118 |
119 | var titleView = TitleView.init(config);
120 | var hrView = HRView.init(config);
121 | var parasView = ParasView.init(config);
122 | var followView = FollowView.init(config);
123 |
124 | App.colorPicker(function (color) {
125 | hrView.fire('changeColor', {color: color.toHexString()});
126 | titleView.fire('changeColor', {color: color.toHexString()});
127 | parasView.fire('changeColor', {color: color.toHexString()});
128 | followView.fire('changeColor', {color: color.toHexString()});
129 | });
130 |
131 | $('input#mpName').keyup(function () {
132 | followView.fire('changeName', {mpName: $(this).val()});
133 | });
134 | });
135 | ```
136 |
137 | 在那之前,你自然需要先clone代码。然后在这里我们不同的几个模块进行初始化,并且为colorPicker配置了相应的监听事件。现在,让我们先到App模块中,看看我们做了些什么事?
138 |
139 | ### Step 3: 初始化
140 |
141 | 初始化模块一共分为两部分,一部分是对CKEditor的初始化,一部分则是对colorPicker的初始化。
142 |
143 | #### CKEditor初始化
144 |
145 | CKEditor自身的编辑器配置比较长,我们就不在这里面列出这些代码了。
146 |
147 | ```javascript
148 | var init = function () {
149 | /**
150 | * @license Copyright (c) 2003-2015, CKSource - Frederico Knabben. All rights reserved.
151 | * For licensing, see LICENSE.md or http://ckeditor.com/license
152 | */
153 |
154 | CKEDITOR.editorConfig = function (config) {
155 | // ...
156 | };
157 | var congee = CKEDITOR.replace('congee', {
158 | uiColor: '#fafafa'
159 | });
160 |
161 | congee.on('change', function (evt) {
162 |
163 | });
164 |
165 | congee.on('instanceReady', function (ev) {
166 | $('.tabset8').pwstabs({
167 | effect: 'slideleft',
168 | defaultTab: 1,
169 | tabsPosition: 'vertical',
170 | verticalPosition: 'left'
171 | });
172 | $('#Container').mixItUp().on('click', '.mix', function (event) {
173 | var template = $(event.currentTarget).html();
174 | congee.insertHtml(template);
175 | });
176 | });
177 |
178 | $(document).ready(function () {
179 | $('#Container').niceScroll({
180 | mousescrollstep: 40
181 | });
182 | });
183 | ```
184 |
185 | ``instanceReady``事件主要就是在编程器初始化后进行的。因此我们在这里初始化了jQuery插件PWS Tabs,以及jQuery插件mixItUp,他们用于进行页面的排版。
186 |
187 | #### ColorPicker初始化
188 |
189 | 下面的代码便是对ColorPicker进行初始化,我们设置了几个常用的颜色放在调色板上。
190 |
191 | ```javascript
192 | var colorPicker = function (changeCB) {
193 | $('#colorpicker').spectrum({
194 | showPaletteOnly: true,
195 | togglePaletteOnly: true,
196 | togglePaletteMoreText: 'more',
197 | togglePaletteLessText: 'less',
198 | color: '#4caf50',
199 | palette: [
200 | ['#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#4caf50', '#8bc34a', '#cddc39'],
201 | ['#3498db', '#2980b9', '#34495e', '#2c3e50', '#2196f3', '#03a9f4', '#00bcd4', '#009688'],
202 | ['#e74c3c', '#c0392b', '#f44336'],
203 | ['#e67e22', '#d35400', '#f39c12', '#ff9800', '#ff5722', '#ffc107'],
204 | ['#f1c40f', '#ffeb3b'],
205 | ['#9b59b6', '#8e44ad', '#9c27b0', '#673ab7', '#e91e63', '#3f51b5'],
206 | ['#795548'],
207 | ['#9e9e9e', '#607d8b', '#7f8c8d', '#95a5a6', '#bdc3c7'],
208 | ['#ecf0f1', 'efefef']
209 | ],
210 | change: changeCB
211 | });
212 | };
213 | ```
214 |
215 | 而实际上在这里我们已经完成了大部分的工作。
216 |
217 | ### Step 4: 创建对应的View
218 |
219 | 在这个项目里,比较麻烦的地方就是使用同样的颜色来显示一个模板,如下的代码是用于显示水平线的模板:
220 |
221 | ```
222 | {{#hrStyle}}
223 |
228 | {{/hrStyle}}
229 | ```
230 |
231 | 下面的代码就是对应的View:
232 |
233 | ```
234 | parasView = new Ractive({
235 | el: 'sandboxHr',
236 | template: '',
237 | components: {Grid: Grid},
238 | data: {
239 | styles: [
240 | {section_style: '', p_style: 'background-color: #fff;border-top: 1px solid', color: color, data_value: dataValue, category: category},
241 | {section_style: '', p_style: 'background-color: #fff;border-top: 3px double', color: color, data_value: dataValue, category: category},
242 | {section_style: '', p_style: 'background-color: #fff;border-top: 1px dashed', color: color, data_value: dataValue, category: category},
243 | {section_style: '', p_style: 'background-color: #fff;border-top: 1px dotted', color: color, data_value: dataValue, category: category},
244 | {section_style: '', p_style: 'background-color: #fff;border-top: 2px dashed', color: color, data_value: dataValue, category: category},
245 | {section_style: '', p_style: 'background-color: #fff;border-top: 2px dotted', color: color, data_value: dataValue, category: category},
246 | {section_style: '', p_style: 'background-color: #fff;border-bottom: 1px solid #fff;border-top: 1px solid', color: color, data_value: dataValue, category: category},
247 | {section_style: 'border-top: 1px solid #8c8b8b; border-bottom: 1px solid #fff;', p_style: 'content: "";display: block;margin-top: 2px;border-top: 1px solid #8c8b8b;border-bottom: 1px solid #fff;', data_value: dataValue, category: category},
248 | {section_style: '', p_style: 'height: 6px;background: url(\'styles/images/hr/hr-11.png\') repeat-x 0 0;border: 0;', data_value: dataValue, category: category},
249 | {section_style: '', p_style: 'height: 6px;background: url(\'styles/images/hr/hr-12.png\') repeat-x 0 0;border: 0;', data_value: dataValue, category: category},
250 | {section_style: '', p_style: 'height: 10px;border: 0;box-shadow: 0 10px 10px -10px #8c8b8b inset;', data_value: dataValue, category: category},
251 | {section_style: '', p_style: 'border: 0;height: 1px;background-image: -webkit-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);background-image: -moz-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);background-image: -ms-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);background-image: -o-linear-gradient(left, #f0f0f0, #8c8b8b, #f0f0f0);', data_value: dataValue, category: category}
252 | ]
253 | }
254 | });
255 | ```
256 |
257 | 我们所做的只是拿出每个不同的布局,再将这些布局显示到页面上。最后在值被修改时,改变这其中的值:
258 |
259 | ```
260 | parasView.on('changeColor', function(args) {
261 | parasView.findComponent('Grid').set('hrStyle.*.color', args.color);
262 | });
263 | ```
264 |
265 | ### 练习建议
266 |
267 |
--------------------------------------------------------------------------------
/chapters/echoesworks.md:
--------------------------------------------------------------------------------
1 | JavaScript制作Slide框架
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 又开始造一个新的轮子了,不过这次的起因比较简单,是想重新发明一个更好的Slide框架 —— EchoesWorks。如名字所言,我所需要的是一个``回声``工坊,即将博客、Slide重新回放。
10 |
11 | ###Showcase
12 |
13 | 
14 |
15 | GitHub代码: [https://github.com/phodal/echoesworks](https://github.com/phodal/echoesworks)
16 |
17 | ### 需求
18 |
19 | 当前我们有不同的方式可以记录我们的想法、博客、过程,如视频、音频、博客、幻灯片等等。
20 |
21 | 然而这些并非那么完美,让我们说说这些方式的一些缺陷吧。
22 |
23 | 1. 视频。有很多技术视频从开始到结束,只有PPT,然后我们就为了这张PPT和声音下了几百M的视频。即使在今天网速很快,但是这并不代表我们可以在我们的手机上放下很多的视频。
24 |
25 | 2. 音频。音频所受到的限制我想大家都很清楚。什么也不知道~~,什么也看不到,只能听。
26 |
27 | 3. 博客。博客的主要缺点可能就是不够直接,有时会有点啰嗦。
28 |
29 | 4. 幻灯片。一个好的PPT,也就意味着上面的内容是很少的。即如果没有人说的时候,就缺少真正有用的东西。
30 |
31 | 5. 代码。我们真的需要在另外打开一个网址来看代码么?
32 |
33 | 于是,``EchoesWorks``出现了,它可以支持下面的一些功能:
34 |
35 | - 支持 Markdown
36 | - Github代码显示
37 | - 全屏背景图片
38 | - 左/右侧图片支持
39 | - 进度条
40 | - 自动播放
41 | - 字幕
42 | - 分屏控制
43 |
44 | 步骤
45 | ---
46 |
47 | ### Step 1: 基本的Slide功能
48 |
49 | 由于我是一个懒人,所以在实现基本的Slide功能时,我找到了一个名为``bespoke``的迷你框架。原理大致和大家分享一下,在这个库里一共有下面几个函数:
50 |
51 | - readURL() 读取URL来获取当前的页数,将跳转到相应的页数。
52 | - activate(index, customData) 主要的函数,实际上就是切换className而已——将新的页面标记为'active'。
53 | - writeURL(index) 切换slide的时候,更新URL的hash
54 | - step(offset, customData) 计算页面
55 | - on(eventName, callback) 事件监听函数
56 | - fire(eventName, eventData) 事件触发函数
57 | - createEventData (el, eventData) 创建事件的数据
58 |
59 | 大致的功能就如上所说的,相当简单。
60 |
61 | ### Step 2: 解析Markdown
62 |
63 | 接着,我们就可以创建解析Markdown的功能了,遗憾的是这里的代码我也是用别人的——``micromarkdown``,一个非常简单的Markdown解析器。
64 |
65 | ### Step 3: 事件处理
66 |
67 | 在我们完成了基本的Slide功能后,我们就可以处理一些特殊的事件,如移动设备和键盘事件。在EW初始化时,我们可以会trigger一个名为``ew:slide:init``的事件来告诉其他组件系统已经初始化了。这时在我们对应的事件处理函数中,我们就可以判断它是不是移动设备:
68 |
69 | ```javascript
70 | slides = document.getElementsByTagName('section');
71 | syncSliderEventHandler();
72 |
73 | if (slides && isTouchDevice && window.slide) {
74 | touchDeviceHandler();
75 | }
76 | ```
77 |
78 | 如果是移动设备,我们会额外的监听三个事件:
79 |
80 | - touchstart
81 | - touchend
82 | - touchmove
83 |
84 | 如果是键盘输入的话,那么依据不同的按键做不同的处理:
85 |
86 | ```javascript
87 | document.addEventListener("keyup", function (event) {
88 | var keyCode = event.keyCode;
89 | if (keyCode === TAB || ( keyCode >= SPACE && keyCode <= PAGE_DOWN ) || (keyCode >= LEFT && keyCode <= DOWN)) {
90 | switch (keyCode) {
91 | case PAGE_UP:
92 | case LEFT:
93 | case UP:
94 | window.slide.prev();
95 | break;
96 | case TAB:
97 | case SPACE:
98 | case PAGE_DOWN:
99 | case RIGHT:
100 | case DOWN:
101 | window.slide.next();
102 | break;
103 | }
104 |
105 | event.preventDefault();
106 | }
107 | });
108 | ```
109 |
110 | 如向上就展示下一张幻灯片,向下就展示下一张幻灯片。
111 |
112 | ### Step 4: 解析字幕
113 |
114 | 在EchoesWorks中提供了一个很有趣的功能——类似于听歌时的歌词显示,并且可以自动播放和切换。它并没有用到什么特殊的技能,只是简单的对比时间,并且替换文字。
115 |
116 | ```javascript
117 | if (that.time < nextTime && words.length > 1) {
118 | var length = words.length;
119 | var currentTime = that.parser.parseTime(that.data.times)[currentSlide];
120 | var time = nextTime - currentTime;
121 | var average = time / length * 1000;
122 | var i = 0;
123 | document.querySelector('words').innerHTML = words[i].word;
124 |
125 | timerWord = setInterval(function () {
126 | i++;
127 | if (i - 1 === length) {
128 | clearInterval(timerWord);
129 | } else {
130 | document.querySelector('words').innerHTML = words[i].word;
131 | }
132 | }, average);
133 | }
134 | return timerWord;
135 | }
136 | ```
137 |
138 | ### Step 5: 进度条
139 |
140 | 一如既往的,我们的进度条,还是用别人已经写好的组件。这里我们用到的是nanobar。在Nanobar里,我们定义了一个go函数,在这个函数里来转换进度条:
141 |
142 | ```
143 | Nanobar.prototype.go = function (p) {
144 | this.bars[0].go(p);
145 | if (p == 100) {
146 | init.call(this);
147 | }
148 | };
149 | ```
150 |
151 | 在我们的slide文件里,再对其进行处理:
152 |
153 | ```
154 | window.bar.go(100 * ( index + 1) / slides.length);
155 | ```
156 |
157 |
158 | ### Step 6: 同步
159 |
160 | 在这里并没有什么特别高级的用法,只是简单的事件监听
161 |
162 | ```javascript
163 | function handler() {
164 | window.slide.slide(parseInt(localStorage.getItem('echoesworks'), 10));
165 | }
166 |
167 | if (window.addEventListener) {
168 | window.addEventListener("storage", handler, false);
169 | } else {
170 | // IE
171 | window.attachEvent("onstorage", handler);
172 | }
173 | ```
174 |
175 | 即,当监听到调用``storage``的方法,就会跳转到相应的页面。
176 |
177 | 正常情况下,我们只用一个标签来展示我们的slide。当我们有另外一个标签的时候,我们就可以存储当前的slide。
178 |
179 | ```javascript
180 | localStorage.setItem('echoesworks', index);
181 | ```
182 |
183 | 这样就可以实现,在一个页面到下一页时,另外一个标签也会跳到下一页。
184 |
185 | ### 练习建议
186 |
--------------------------------------------------------------------------------
/chapters/eppsc.md:
--------------------------------------------------------------------------------
1 | 编辑-发布-分离的博客系统
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景: 编辑-发布-开发分离
8 |
9 | 在这种情形中,编辑能否完成工作就不依赖于网站——脱稿又少了 个借口。这时候网站出错的概率太小了——你不需要一个缓存服务器、HTTP服务器,由于没有动态生成的内容,你也不需要守护进程。这些内容都是静态文件,你可以将他们放在任何可以提供静态文件托管的地方——CloudFront、S3等等。或者你再相信自己的服务器,Nginx可是全球第二好(第一还没出现)的静态文件服务器。
10 |
11 | 开发人员只在需要的时候去修改网站的一些内容。
12 |
13 | So,你可能会担心如果这时候修改的东西有问题了怎么办。
14 |
15 | 1. 使用这种模式就意味着你需要有测试来覆盖这些构建工具、生成工具。
16 | 2. 相比于自己的代码,别人的CMS更可靠?
17 |
18 | 需要注意的是如果你上一次构建成功,你生成的文件都是正常的,那么你只需要回滚开发相关的代码即可。旧的代码仍然可以工作得很好。
19 |
20 | 其次,由于生成的是静态文件,查错的成本就比较低。
21 |
22 | 最后,重新放上之前的静态文件。
23 |
24 | > 动态网页是下一个要解决的难题。我们从数据库中读取数据,再用动态去渲染出一个静态页面,并且缓存服务器来缓存这个页面。既然我们都可以用Varnish、Squid这样的软件来缓存页面——表明它们可以是静态的,为什么不考虑直接使用静态网页呢?
25 |
26 | 为了实现之前说到的``编辑-发布-开发分离``的CMS,我还是花了两天的时间打造了一个面向普通用户的编辑器。效果截图如下所示:
27 |
28 | 
29 |
30 | 作为一个普通用户,这是一个很简单的软件。除了Electron + Node.js + React作了一个140M左右的软件,尽管打包完只有40M左右 ,但是还是会把用户吓跑的。不过作为一个快速构建的原型已经很不错了——构建速度很快、并且运行良好。
31 |
32 | 尽管这个界面看上去还是稍微复杂了一下,还在试着想办法将链接名和日期去掉——问题是为什么会有这两个东西?
33 |
34 | #### 从Schema到数据库
35 |
36 | 我们在我们数据库中定义好了Schema——对一个数据库的结构描述。在《[编辑-发布-开发分离](https://www.phodal.com/blog/editing-publishing-coding-seperate/)
37 | 》一文中我们说到了echeveria-content的一个数据文件如下所示:
38 |
39 | ```javascript
40 | {
41 | "title": "白米粥",
42 | "author": "白米粥",
43 | "url": "baimizhou",
44 | "date": "2015-10-21",
45 | "description": "# Blog post \n > This is an example blog post \n Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ",
46 | "blogpost": "# Blog post \n > This is an example blog post \n Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \n Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
47 | }
48 | ```
49 |
50 | 比起之前的直接生成静态页面这里的数据就是更有意思地一步了,我们从数据库读取数据就是为了生成一个JSON文件。何不直接以JSON的形式存储文件呢?
51 |
52 | 我们都定义了这每篇文章的基本元素:
53 |
54 | 1. title
55 | 2. author
56 | 3. date
57 | 4. description
58 | 5. content
59 | 6. url
60 |
61 | 即使我们使用NoSQL我们也很难逃离这种模式。我们定义这些数据,为了在使用的时候更方便。存储这些数据只是这个过程中的一部分,下部分就是取出这些数据并对他们进行过滤,取出我们需要的数据。
62 |
63 | Web的骨架就是这么简单,当然APP也是如此。难的地方在于存储怎样的数据,返回怎样的数据。不同的网站存储着不同的数据,如淘宝存储的是商品的信息,Google存储着各种网站的数据——人们需要不同的方式去存储这些数据,为了更好地存储衍生了更多的数据存储方案——于是有了GFS、Haystack等等。运营型网站想尽办法为最后一公里努力着,成长型的网站一直在想着怎样更好的返回数据,从更好的用户体验到机器学习。而数据则是这个过程中不变的东西。
64 |
65 | 尽管,我已经想了很多办法去尽可能减少元素——在最开始的版本里只有标题和内容。然而为了满足我们在数据库中定义的结构,不得不造出来这么多对于一般用户不友好的字段。如链接名是为了存储的文件名而存在的,即这个链接名在最后会变成文件名:
66 |
67 | ```javascript
68 | repo.write('master', 'contents/' + data.url + '.json', stringifyData, 'Robot: add article ' + data.title, options, function (err, data) {
69 | if(data.commit){
70 | that.setState({message: "上传成功" + JSON.stringify(data)});
71 | that.refs.snackbar.show();
72 | that.setState({
73 | sending: 0
74 | });
75 | }
76 | });
77 | ```
78 |
79 | 然后,上面的数据就会变成一个对象存储到“数据库”中。
80 |
81 | 今天 ,仍然有很多人用Word、Excel来存储数据。因为对于他们来说,这些软件更为直接,他们简单地操作一下就可以对数据进行排序、筛选。数据以怎样的形式存储并不重要,重要的是他们都以文件的形式存储着。
82 |
83 | #### git作为NoSQL数据库
84 |
85 | 在控制台中运行一下 ``man git``你会得到下面的结果:
86 |
87 | 
88 |
89 | 这个答案看起来很有意思——不过这看上去似乎无关主题。
90 |
91 | 不同的数据库会以不同的形式存储到文件中去。blob是git中最为基本的存储单位,我们的每个content都是一个blob。redis可以以rdb文件的形式存储到文件系统中。完成一个CMS,我们并不需要那么多的查询功能。
92 |
93 | > 这些上千年的组织机构,只想让人们知道他们想要说的东西。
94 |
95 | 我们使用NoSQL是因为:
96 |
97 | 1. 不使用关系模型
98 | 2. 在集群中运行良好
99 | 3. 开源
100 | 4. 无模式
101 | 5. 数据交换格式
102 |
103 | 我想其中只有两点对于我来说是比较重要的``集群``与``数据格式``。但是集群和数据格式都不是我们要考虑的问题。。。
104 |
105 | 我们也不存在数据格式的问题、开源的问题,什么问题都没有。。除了,我们之前说到的查询——但是这是可以解决的问题,我们甚至可以返回不同的历史版本的。在这一点上git做得很好,他不会像WordPress那样存储多个版本。
106 |
107 | #### git + JSON文件
108 |
109 | JSON文件 + Nginx就可以变成这样一个合理的API,甚至是运行方式。我们可以对其进行增、删、改、查,尽管就当前来说查需要一个额外的软件来执行,但是为了实现一个用得比较少的功能,而去花费大把的时间可能就是在浪费。
110 |
111 | git的“API”提供了丰富的增、删、改功能——你需要commit就可以了。我们所要做的就是:
112 |
113 | 1. git commit
114 | 2. git push
115 | Carrot使用了下面的方案来搭建他们的静态内容的CMS。
116 |
117 | 
118 |
119 | 在这个方案里内容是用Contentful来发布他们的内容。而在我司[ThoughtWorks](https://www.thoughtworks.com/)的官网里则采用了Github来管理这些内容。于是如果让我们写一个基于Github的CMS,那么架构变成了这样:
120 |
121 | 
122 |
123 | 或许你也用过Hexo / Jekyll / Octopress这样的静态博客,他们的原理都是类似的。我们有一个代码库用于生成静态页面,然后这些静态页面会被PUSH到Github Pages上。
124 |
125 | 从我们设计系统的角度来说,我们会在Github上有三个代码库:
126 |
127 | 1. Content。用于存放编辑器生成的JSON文件,这样我们就可以GET这些资源,并用Backbone / Angular / React 这些前端框架来搭建SPA。
128 | 2. Code。开发者在这里存放他们的代码,如主题、静态文件生成器、资源文件等等。
129 | 3. Builder。在这里它是运行于Travis CI上的一些脚本文件,用于Clone代码,并执行Code中的脚本。
130 |
131 | 以及一些额外的服务,当且仅当你有一些额外的功能需求的时候。
132 |
133 | 1. Extend Service。当我们需要搜索服务时,我们就需要这样的一些服务。如我正考虑使用Python的whoosh来完成这个功能,这时候我计划用Flask框架,但是只是计划中——因为没有合适的中间件。
134 | 2. Editor。相比于前面的那些知识这一步适合更重要,也就是为什么生成的格式是JSON而不是Markdown的原理。对于非程序员来说,要熟练掌握Markdown不是一件容易的事。于是,一个考虑中的方案就是使用 Electron + Node.js来生成API,最后通过GitHub API V3来实现上传。
135 |
136 | So,这一个过程是如何进行的。
137 |
138 | ### 用户场景
139 |
140 | 整个过程的Pipeline如下所示:
141 |
142 | 1. 编辑使用他们的编辑器来编辑的内容并点击发布,然后这个内容就可以通过GitHub API上传到Content这个Repo里。
143 | 2. 这时候需要有一个WebHooks监测到了Content代码库的变化,便运行Builder这个代码库的Travis CI。
144 | 3. 这个Builder脚本首先,会设置一些基本的git配置。然后clone Content和Code的代码,接着运行构建命令,生成新的内容。
145 | 4. 然后Builder Commit内容,并PUSH内容。
146 |
147 | 这里还依赖于WebHook这个东西——还没想到一个合适的解决方案。下面,我们对里面的内容进行一些拆解,Content里面由于是JSON就不多解释了。
148 |
149 | 步骤
150 | ---
151 |
152 | ### Step 1: 构建工具
153 |
154 | Github与Travis之间,可以做一个自动部署的工具。相信已经有很多人在Github上玩过这样的东西——先在Github上生成Token,然后用travis加密:
155 |
156 | ```bash
157 | travis encrypt-file ssh_key --add
158 | ```
159 |
160 | 加密后的Key就会保存到``.travis.yml``文件里,然后就可以在Travis CI上push你的代码到Github上了。
161 |
162 | 接着,你需要创建个deploy脚本,并且在``after_success``执行它:
163 |
164 | ```yml
165 | after_success:
166 | - test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh
167 | ```
168 |
169 | 在这个脚本里,你所需要做的就是clone content和code中的代码,并执行code中的生成脚本,生成新的内容后,提交代码。
170 |
171 | ```
172 | #!/bin/bash
173 |
174 | set -o errexit -o nounset
175 |
176 | rev=$(git rev-parse --short HEAD)
177 |
178 | cd stage/
179 |
180 | git init
181 | git config user.name "Robot"
182 | git config user.email "robot@phodal.com"
183 |
184 | git remote add upstream "https://$GH_TOKEN@github.com/phodal-archive/echeveria-deploy.git"
185 | git fetch upstream
186 | git reset upstream/gh-pages
187 |
188 | git clone https://github.com/phodal-archive/echeveria-deploy code
189 | git clone https://github.com/phodal-archive/echeveria-content content
190 | pwd
191 | cp -a content/contents code/content
192 |
193 | cd code
194 |
195 | npm install
196 | npm install grunt-cli -g
197 | grunt
198 | mv dest/* ../
199 | cd ../
200 | rm -rf code
201 | rm -rf content
202 |
203 | touch .
204 |
205 | if [ ! -f CNAME ]; then
206 | echo "deploy.baimizhou.net" > CNAME
207 | fi
208 |
209 | git add -A .
210 | git commit -m "rebuild pages at ${rev}"
211 | git push -q upstream HEAD:gh-pages
212 | ```
213 |
214 | 这就是这个builder做的事情——其中最主要的一个任务是``grunt``,它所做的就是:
215 |
216 | ```javascript
217 | grunt.registerTask('default', ['clean', 'assemble', 'copy']);
218 | ```
219 |
220 | ### Step 2: 静态页面生成
221 |
222 | Assemble是一个使用Node.js,Grunt.js,Gulp,Yeoman 等来实现的静态网页生成系统。这样的生成器有很多,Zurb Foundation, Zurb Ink, Less.js / lesscss.org, Topcoat, Web Experience Toolkit等组织都使用这个工具来生成。这个工具似乎上个Release在一年多以前,现在正在开始0.6。虽然,这并不重要,但是还是顺便一说。
223 |
224 | 我们所要做的就是在我们的``Gruntfile.js``中写相应的生成代码。
225 |
226 | ```javascript
227 | assemble: {
228 | options: {
229 | flatten: true,
230 | partials: ['templates/includes/*.hbs'],
231 | layoutdir: 'templates/layouts',
232 | data: 'content/blogs.json',
233 | layout: 'default.hbs'
234 | },
235 | site: {
236 | files: {'dest/': ['templates/*.hbs']}
237 | },
238 | blogs: {
239 | options: {
240 | flatten: true,
241 | layoutdir: 'templates/layouts',
242 | data: 'content/*.json',
243 | partials: ['templates/includes/*.hbs'],
244 | pages: pages
245 | },
246 | files: [
247 | { dest: './dest/blog/', src: '!*' }
248 | ]
249 | }
250 | }
251 | ```
252 |
253 | 配置中的site用于生成页面相关的内容,blogs则可以根据json文件的文件名生成对就的html文件存储到blog目录中。
254 |
255 | 生成后的目录结果如下图所示:
256 |
257 | ```
258 | .
259 | ├── about.html
260 | ├── blog
261 | │ ├── blog-posts.html
262 | │ └── blogs.html
263 | ├── blog.html
264 | ├── css
265 | │ ├── images
266 | │ │ └── banner.jpg
267 | │ └── style.css
268 | ├── index.html
269 | └── js
270 | ├── jquery.min.js
271 | └── script.js
272 |
273 | 7 directories, 30 files
274 | ```
275 |
276 | 这里的静态文件内容就是最后我们要发布的内容。
277 |
278 | 还需要做的一件事情就是:
279 |
280 | ```javascript
281 | grunt.registerTask('dev', ['default', 'connect:server', 'watch:site']);
282 | ```
283 |
284 | 用于开发阶段这样的代码就够了,这个和你使用WebPack + React 似乎相差不了多少。
285 |
--------------------------------------------------------------------------------
/chapters/google-map-solr.md:
--------------------------------------------------------------------------------
1 | Solr实现多边形地理搜索
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | ### Showcase
10 |
11 | 
12 |
13 | GitHub [https://github.com/phodal/gmap-solr](https://github.com/phodal/gmap-solr)
14 |
15 | ### Solr
16 |
17 | > Solr是一个高性能,采用Java5开发,基于Lucene的全文搜索服务器。同时对其进行了扩展,提供了比Lucene更为丰富的查询语言,同时实现了可配置、可扩展并对查询性能进行了优化,并且提供了一个完善的功能管理界面,是一款非常优秀的全文搜索引擎。
18 |
19 | 简单地说: 它是一个搜索引擎
20 |
21 | > 文档通过Http利用XML 加到一个搜索集合中。查询该集合也是通过http收到一个XML/JSON响应来实现。它的主要特性包括:高效、灵活的缓存功能,垂直搜索功能,高亮显示搜索结果,通过索引复制来提高可用性,提供一套强大Data Schema来定义字段,类型和设置文本分析,提供基于Web的管理界面等。
22 |
23 | 即schema.xml
24 |
25 | **Solr 安装**
26 |
27 | ```bash
28 | brew install solr
29 | ```
30 |
31 | 步骤
32 | ---
33 |
34 | 用Flask搭建一个简单的servrices,接着在前台用google的api,对后台发出请求。
35 |
36 | ### Step 1: Solr Flask
37 |
38 | 由于,直接调用的是Solr的接口,所以我们的代码显得比较简单:
39 |
40 | ```python
41 | class All(Resource):
42 | @staticmethod
43 | def get():
44 | base_url = ''
45 | url = (base_url + 'select?q=' + request.query_string + '+&wt=json&indent=true')
46 | result = requests.get(url)
47 | return (result.json()['response']['docs']), 201, {'Access-Control-Allow-Origin': '*'}
48 |
49 |
50 | api.add_resource(All, '/geo/')
51 | ```
52 |
53 | 我们在前台需要做的便是,组装geo query。
54 |
55 | ###Step 2: Google map Polygon
56 |
57 | 在Google Map的API是支持Polygon搜索的,有对应的一个
58 |
59 | ```javascript
60 | google.maps.event.addListener(drawingManager, 'polygoncomplete', renderMarker);
61 | ```
62 |
63 | 函数来监听,完成``polygoncomplete``时执行的函数,当我们完成搜索时,便执行``renderMarker``,在里面做的便是:
64 |
65 | ```javascript
66 | $.get('/geo/?' + query, function (results) {
67 | for (var i = 0; i < results.length; i++) {
68 | var location = results[i].geo[0].split(',');
69 | var myLatLng = new google.maps.LatLng(location[0], location[1]);
70 | var title = results[i].title;
71 | marker = new google.maps.Marker({
72 | position: myLatLng,
73 | map: map,
74 | title: title
75 | });
76 |
77 | contentString = 'City
address ' + i + '';
78 |
79 | google.maps.event.addListener(marker, 'click', (function (marker, contentString, infowindow) {
80 | return function () {
81 | infowindow.setContent(contentString);
82 | infowindow.open(map, marker);
83 | };
84 | })(marker, contentString, infowindow));
85 | }
86 | });
87 | ```
88 |
89 | 对应的去解析数据,并显示在地图上
90 |
--------------------------------------------------------------------------------
/chapters/growth.md:
--------------------------------------------------------------------------------
1 | 一份代码打造跨平台应用
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | Web本身就是跨平台的,这意味着这中间存在着无限的可能性。
10 |
11 | 我是一名Web Developer,对于我来能用Web开发的事情就用Web来完成就好了——不需要编译,不需要等它编译完。我想到哪我就可以写到哪,我改到哪我就可以发生哪发生了变化。
12 |
13 | 最近我在写Growth——一个帮助开发人员成长的应用,在近一个月的业余时间里,完成了这个应用的:
14 |
15 | - 移动应用版:Android、Windows Phone、iOS(等账号和上线)
16 | - Web版
17 | - 桌面版:Mac OS、Windows、GNU/Linux
18 |
19 | ### ShowCase
20 |
21 | 截图合并如下:
22 |
23 | 
24 |
25 | ### Ionic & Electron & Cordova
26 |
27 | 而更重要的是它们使用了同一份代码——除了对特定设备进行一些处理就没有其他修改。相信全栈的你已经看出来了:
28 |
29 | Web = Chrome + Angular.js + Ionic
30 |
31 | Desktop = Electron + Angular.js + Ionic
32 |
33 | Mobile = Cordova + Angular.js + Ionic
34 |
35 | 除了前面的WebView不一样,后面都是Angular.js + Ionic。
36 |
37 | 步骤
38 | ---
39 |
40 | ###Step 1: 从Web到混合应用,再到桌面应用
41 |
42 | 在最打开的时候它只是一个单纯的混合应用,我想总结一下我的学习经验,分享一下学习的心得,如:
43 |
44 | - 完整的Web开发,运维,部署,维护介绍
45 | - 如何写好代码——重构、测试、模式
46 | - 遗留代码、遗留系统的形成
47 | - 不同阶段所需的技能
48 | - 书籍推荐
49 | - 技术栈推荐
50 | - Web应用解决方案
51 |
52 | 接着我用Ionic创建了这个应用,这是一个再普通不过的过程。在这个过程里,我一直使用Chrome在调度我的代码。因为我是Android用户,我有Google Play的账号,便发布了Android版本。这时候遇到了一个问题,我并没有Apple Developer账号(现在在申请ing。。),而主要的用户对象程序员,这是一群**不土**的土豪。
53 |
54 | 
55 |
56 | 偶然间我才想到,我只要上传Web版本的代码就可以暂时性实现这个需求了。接着找了个AWS S3的插件,直接上传到了AWS S3上托管成静态文件服务。
57 |
58 | 几天前在Github上收到一个issue——关于创造桌面版, 我便想着这也是可能的,我只需要写一个启动脚本和编译脚本即可。
59 |
60 | 所以,最后我们的流程图就如下所示:
61 |
62 | 
63 |
64 | 除了显示到VR设备上,好像什么也不缺了。并且在我之前的文章《[Oculus + Node.js + Three.js 打造VR世界](https://github.com/phodal/oculus-nodejs-threejs-example)》,也展示了Web在VR世界的可能性。
65 |
66 | 在这实现期间有几个点可以分享一下:
67 |
68 | 1. 响应式设计
69 | 2. 平台/设备特定代码
70 |
71 | ### Step 2: 响应式设计
72 |
73 | 响应式设计可以主要依赖于Media Query,而响应式设计主要要追随的一点是不同的设备不同的显示,如:
74 |
75 | 
76 |
77 | 这也意味着,我们需要对不同的设备进行一些处理,如在大的屏幕下,我们需要展示菜单:
78 |
79 | 
80 |
81 | 而这可以依赖于Ionic的**expose-aside-when="large"**,而并非所有的情形都是这么简单的。如我最近遇到的问题就是图片缩放的问题,之前的图片针对的都是手机版——经过了一定的缩放。
82 |
83 | 这时在桌面应用上就会出现问题,就需要限定大小等等。
84 |
85 | 而这个问题相比于平台特定问题则更容易解决。
86 |
87 | ### Step 3: 平台特定代码
88 |
89 | 对于特定平台才有的问题就不是一件容易解决的事,分享一下:
90 |
91 | #### 存储
92 |
93 | 我遇到的第一个问题是**数据存储**的问题。最开始的时候,我只需要开始混合应用。因此我可以用**Preferences**、或者**SQLite**来存储数据。
94 |
95 | 后来,我扩展到了Web版,我只好用LocalStoarge。于是,我就开始抽象出一个**$storageServices**来做相应的事。接着遇到一系列的问题,我舍弃了原有的方案,直接使用LocalStoarge。
96 |
97 | #### 数据分析
98 |
99 | 为了开发方便,我使用Google Analytics来分析用户的行为——毕竟数据对我来说也不是特别重要,只要可以看到有人使用就可以了。
100 |
101 | 这时候遇到的一个问题是,我不需要记录Web用户的行为,但是我希望可以看到有这样的请求发出。于是对于Web用户来说,只需要:
102 |
103 | ```js
104 | trackView: function (view) {
105 | console.log(view);
106 | }
107 | ```
108 |
109 | 而对于手机用户则是:
110 |
111 | ```js
112 | trackView: function (view) {
113 | $window.analytics.startTrackerWithId('UA-71907748-1');
114 | $window.analytics.trackView(view)
115 | }
116 | ```
117 |
118 | 这样在我调试的时候我只需要打个Log,在产品环境时就会Track。
119 |
120 | #### 自动更新
121 |
122 | 同样的,对于Android用户来说,他们可以选择自行下载更新,所以我需要针对Android用户有一个自动更新:
123 |
124 | ```
125 | var isAndroid = ionic.Platform.isAndroid();
126 | if(isAndroid) {
127 | $updateServices.check('main');
128 | }
129 | ```
130 |
131 | #### 桌面应用
132 |
133 | 对于桌面应用来说也会有类似的问题,我遇到的第一个问题是Electron默认开启了AMD。于是,直接删之:
134 |
135 | ```html
136 |
142 | ```
143 |
144 | 类似的问题还有许多,不过由于应用内容的限制,这些问题就没有那么严重了。
145 |
146 | 如果有一天,我有钱开放这个应用的应用号,那么我就会再次献上这个图:
147 |
148 | 
149 |
150 | ### 未来
151 |
152 | 我就开始思索这个问题,未来的趋势是合并到一起,而这一个趋势在现在就已经是完成时了。
153 |
154 | 那么未来呢?你觉得会是怎样的?
155 |
--------------------------------------------------------------------------------
/chapters/ionic-es.md:
--------------------------------------------------------------------------------
1 | Ionic与ElasticSearch打造O2O应用
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 搜索引擎是个好东西,GIS也是个好东西。当前还有Django和Ionic。
10 |
11 | ### Showcase
12 |
13 | 最后效果图
14 |
15 | 
16 |
17 | 
18 |
19 | ### 构架设计
20 |
21 | 对我们的需求进行简要的思考后,设计出了下面的一些简单的架构。
22 |
23 | 
24 |
25 | #### 服务端
26 |
27 | 简单说明:
28 |
29 | - 用户在前台或者后台创建数据。
30 | - 在model保存数据的时候,会调用Google的API解析GPS
31 | - 在haystack的配置中设置实时更新,当数据创建的时候自动更新索引
32 | - 数据被ElasticSearch索引
33 |
34 | 下面是框架的一些简单的介绍
35 |
36 | **Django**
37 |
38 | > [Django](http://www.phodal.com/blog/tag/django/) 是一个开放源代码的Web应用框架,由Python写成。采用了MVC的软件设计模式,即模型M,视图V和控制器C。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的。并于2005年7月在BSD许可证下发布。这套框架是以比利时的吉普赛爵士吉他手Django Reinhardt来命名的。
39 |
40 | > [Django](http://www.phodal.com/blog/tag/django/) 的主要目标是使得开发复杂的、数据库驱动的网站变得简单。Django注重组件的重用性和“可插拔性”,敏捷开发和DRY法则(Don't Repeat Yourself)。在Django中Python被普遍使用,甚至包括配置文件和数据模型。
41 |
42 |
43 | 首先考虑Django,而不是其他Node或者Ruby框架的原因是:
44 |
45 | - 内置认证系统
46 | - 内置CSRF
47 |
48 | 当然这是其他框架也所拥有的,主要特性还有:
49 |
50 | - 一个表单序列化及验证系统,用于HTML表单和适于数据库存储的数据之间的转换。
51 | - 一套协助创建地理信息系统(GIS)的基础框架
52 |
53 | 最后一个才是亮点,内置GIS,虽然没怎么用到,但是至少在部署上还是比较方便的。
54 |
55 | **Haystack**
56 |
57 | > Haystack provides modular search for Django. It features a unified, familiar API that allows you to plug in different search backends (such as Solr, Elasticsearch, Whoosh, Xapian, etc.) without having to modify your code.
58 |
59 | Haystack是为Django提供一个搜索模块blabla..,他的主要特性是可以
60 |
61 | > write your search code once and choose the search engine you want it to run on
62 |
63 | 也就是说你只需要写你的代码选择你的搜索引擎就可以工作了。
64 |
65 | **ElasticSearch**
66 |
67 | 在上面的Haystack提供了这些一堆的搜索引擎,当然支持地点搜索的只有``Solr``和``ElasticSearch``,他们支持的空间搜索有:
68 |
69 | - within
70 | - dwithin
71 | - distance
72 | - order_by(‘distance’)
73 | - polygon
74 |
75 | 在文档上没有写Solr的polygon搜索,但是实际上也是支持的(详细见这篇文章: [google map solr polygon 搜索](http://www.phodal.com/blog/google-map-width-solr-use-polygon-search/,用的地图是谷歌,所以需要先学会访问谷歌)。
76 |
77 | 至于为什么用的是ElasticSearch,是因为之前用Solr做过。。。
78 |
79 | #### 客户端
80 |
81 | **简单说明 —— GET**
82 |
83 | 1. 当我们访问Map View的时候,会调用HTML5获取用户的位置
84 | 2. 根据用户的位置定位,设置缩放
85 | 3. 根据用户的位置发出ElasticSearch请求,返回结果中带上距离
86 | 4. 显示
87 |
88 | **简单说明 —— POST**
89 |
90 | 1. 用户填写数据会发给Django API,并验证
91 | 2. 成功时,存入数据库,更新索引。
92 |
93 | **Ionic**
94 |
95 | > Ionic提供了一个免费且开源的移动优化HTML,CSS和JS组件库,来构建高交互性应用。基于Sass构建和AngularJS 优化。
96 |
97 | 用到的主要是AngularJS,之前用他写过三个APP。
98 |
99 | **Django REST Framework**
100 |
101 | 与Django Tastypie相比,DRF的主要优势在于Web界面的调试。
102 |
103 | 步骤
104 | ---
105 |
106 | ### Step 1: Django GIS 设置
107 |
108 | 1.创建虚拟环境
109 |
110 | ```bash
111 | virtualenv -p /usr/bin/python2.67 django-elasticsearch
112 | ```
113 |
114 | 2.创建项目
115 |
116 | 为了方便,这里用的是Mezzanine CMS,相比Django的主要优势是,以后扩展方便。但是对于Django也是可以的。
117 |
118 | 3.安装依赖
119 |
120 | 这里我的所有依赖有
121 |
122 | - django-haystack
123 | - Mezzanine==3.1.10
124 | - djangorestframework
125 | - pygeocoder
126 | - elasticsearch
127 |
128 | 安装
129 |
130 | ```bash
131 | pip install requirements.txt
132 | ```
133 |
134 | 4.安装ElasticSearch
135 |
136 | CentOS
137 |
138 | ```bash
139 | wget https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-1.4.2.zip
140 | sudo unzip elasticsearch-1.4.2 -d /usr/local/elasticsearch
141 | rm elasticsearch-1.4.2.zip
142 | cd /usr/local/elasticsearch/elasticsearch-1.4.2/
143 | ./bin/plugin install elasticsearch/elasticsearch-cloud-aws/2.4.1
144 | curl -XGET http://localhost:9200
145 | ```
146 |
147 | Mac OS
148 |
149 | ```bash
150 | brew install elasticsearch
151 | ```
152 |
153 | 5.Django Geo环境搭建
154 |
155 | CentOS等GNU/Linux系统: 可以参照[CentOS Django Geo 环境搭建](http://www.phodal.com/blog/install-geo-django-in-centos/)
156 |
157 | MacOS: [Mac OS Django Geo 环境搭建](http://www.phodal.com/blog/django-elasticsearch-geo-solution/)
158 |
159 | ### Step 2: 配置Haystack
160 |
161 | **配置Haystack**
162 |
163 | ```python
164 | HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
165 |
166 | HAYSTACK_CONNECTIONS = {
167 | 'default': {
168 | 'ENGINE': 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine',
169 | 'URL': 'http://127.0.0.1:9200/',
170 | 'INDEX_NAME': 'haystack',
171 | },
172 | }
173 | ```
174 |
175 | ``HAYSTACK_SIGNAL_PROCESSOR``是为了可以实时处理。
176 | ``HAYSTACK_CONNECTIONS`` 则是配置搜索引擎用的。
177 |
178 | **配置Django**
179 |
180 | 在``settings.py``中的``INSTALLED_APPS``添加
181 |
182 | ```python
183 | "haystack",
184 | "rest_framework",
185 | ```
186 |
187 | 接着
188 |
189 | ```bash
190 | python manage.py createdb
191 | python manage.py migreate
192 | ```
193 |
194 | 运行
195 |
196 | ```bash
197 | python manage.py runserver
198 | ```
199 |
200 | 官方有一个简单的文档说明空间搜索—— [Spatial Search](http://django-haystack.readthedocs.org/en/latest/spatial.html)
201 |
202 | 里面只有``Solr``和``ElasticSearch``是支持的,当然我们也不需要这么复杂的特性。
203 |
204 | 创建Django app名为nx,目录结构如下
205 |
206 | ```
207 | .
208 | |______init__.py
209 | |____api.py
210 | |____models.py
211 | |____search_indexes.py
212 | |____templates
213 | | |____search
214 | | | |____indexes
215 | | | | |____nx
216 | | | | | |____note_text.txt
217 | ```
218 |
219 | api.py是后面要用的。
220 |
221 | ### Step 3: Django Haystack Model创建
222 |
223 | 而一般的model没有什么区别,除了修改了save方法
224 |
225 | ```python
226 | from django.contrib import admin
227 |
228 | from django.contrib.gis.geos import Point
229 | from django.core import validators
230 | from django.utils.translation import ugettext_lazy as _
231 | from django.db import models
232 | from pygeocoder import Geocoder
233 |
234 | class Note(models.Model):
235 | title = models.CharField("标题", max_length=30, unique=True)
236 | latitude = models.FloatField(blank=True)
237 | longitude = models.FloatField(blank=True)
238 |
239 | def __unicode__(self):
240 | return self.title
241 |
242 | def save(self, *args, **kwargs):
243 | results = Geocoder.geocode(self.province + self.city + self.address)
244 | self.latitude = results[0].coordinates[0]
245 | self.longitude = results[0].coordinates[1]
246 | super(Note, self).save(*args, **kwargs)
247 |
248 | def get_location(self):
249 | return Point(self.longitude, self.latitude)
250 |
251 | def get_location_info(self):
252 | return self.province + self.city + self.address
253 |
254 | admin.site.register(Note)
255 | ```
256 |
257 | 通过``Geocoder.geocode`` 解析用户输入的地址,为了方便直接后台管理了。
258 |
259 | ### Step 4: 创建search_index
260 |
261 | 在源码的目录下有一个``search_indexes.py``的文件就是用于索引用的。
262 |
263 | ```python
264 | from haystack import indexes
265 | from .models import Note
266 |
267 | class NoteIndex(indexes.SearchIndex, indexes.Indexable):
268 | text = indexes.CharField(document=True, use_template=True)
269 | title = indexes.CharField(model_attr='title')
270 | location = indexes.LocationField(model_attr='get_location')
271 | location_info = indexes.CharField(model_attr='get_location_info')
272 |
273 | def get_model(self):
274 | return Note
275 | ```
276 |
277 | 与些同时我们还需要在``templates/search/indexes/nx/``目录中有``note_text.txt``里面的内容是:
278 |
279 | ```python
280 | {{ object.title }}
281 | {{ object.get_location }}
282 | {{ object.get_location_info }}
283 | ```
284 |
285 | **创建数据**
286 |
287 | migrate数据库
288 |
289 | ```bash
290 | python manage.py migrate
291 | ```
292 |
293 | run
294 |
295 | ```bash
296 | python manage.py runserver
297 | ```
298 |
299 | 接着我们就可以后台创建数据了。 打开: http://127.0.0.1:8000/admin/nx/note/,把除了``Latitude``和``Longitude``以外的数据都一填——经纬度是自动生成的。就可以创建数据了。
300 |
301 | **测试**
302 |
303 | 访问 http://localhost:9200/haystack/_search
304 |
305 | 或者
306 |
307 | ```bash
308 | curl -XGET http://127.0.0.1:9200/haystack/_search
309 | ```
310 |
311 |
312 | 如果你没有Ionic的经验,可以参考一下之前的一些文章:[《HTML5打造原生应用——Ionic框架简介与Ionic Hello World》](http://www.phodal.com/blog/ionic-development-android-ios-windows-phone-application/)。
313 |
314 | 我们用到的库有:
315 |
316 | - elasticsearch
317 | - ionic
318 | - ngCordova
319 |
320 | 将他们添加到``bower.json``,然后执行
321 |
322 | ```bash
323 | bower install
324 | ```
325 |
326 | ### Step 5: Ionic ElasticSearch 创建页面
327 |
328 | 1.引入库
329 |
330 | 在``index.html``中添加
331 |
332 | ```html
333 |
334 |
335 | ```
336 |
337 | 接着开始写我们的搜索模板``tab-search.html``
338 |
339 | ```html
340 |
341 |
342 |
343 |
344 |
348 |
349 |
350 |
351 |
352 | ```
353 |
354 | 显示部分
355 |
356 | ```html
357 |
358 |
359 | {{result.title}}
360 | 简介: {{result.body}}
361 |
362 | {{result.location_info}}
363 |
364 |
367 |
368 |
369 | ```
370 |
371 | 而我们期待的``SearchCtrl``则是这样的
372 |
373 | ```javascript
374 | $scope.query = "";
375 | var doSearch = ionic.debounce(function(query) {
376 | ESService.search(query, 0).then(function(results){
377 | $scope.results = results;
378 | });
379 | }, 500);
380 |
381 | $scope.search = function(query) {
382 | doSearch(query);
383 | }
384 | ```
385 |
386 | 当我们点下搜索的时候,调用 ESService.
387 |
388 | ### Step 6: Ionic ElasticSearch Service
389 |
390 | 接着我们就来构建我们的ESService,下面的部分来自网上:
391 |
392 | ```javascript
393 | angular.module('starter.services', ['ngCordova', 'elasticsearch'])
394 |
395 | .factory('ESService',
396 | ['$q', 'esFactory', '$location', '$localstorage', function($q, elasticsearch, $location, $localstorage){
397 | var client = elasticsearch({
398 | host: $location.host() + ":9200"
399 | });
400 |
401 | var search = function(term, offset){
402 | var deferred = $q.defer(), query, sort;
403 | if(!term){
404 | query = {
405 | "match_all": {}
406 | };
407 | } else {
408 | query = {
409 | match: { title: term }
410 | }
411 | }
412 |
413 | var position = $localstorage.get('position');
414 |
415 | if(position){
416 | sort = [{
417 | "_geo_distance": {
418 | "location": position,
419 | "unit": "km"
420 | }
421 | }];
422 | } else {
423 | sort = [];
424 | }
425 |
426 | client.search({
427 | "index": 'haystack',
428 | "body": {
429 | "query": query,
430 | "sort": sort
431 | }
432 | }).then(function(result) {
433 | var ii = 0, hits_in, hits_out = [];
434 | hits_in = (result.hits || {}).hits || [];
435 | for(;ii < hits_in.length; ii++){
436 | var data = hits_in[ii]._source;
437 | var distance = {};
438 | if(hits_in[ii].sort){
439 | distance = {"distance": parseFloat(hits_in[ii].sort[0]).toFixed(1)}
440 | }
441 | angular.extend(data, distance);
442 | hits_out.push(data);
443 | }
444 | deferred.resolve(hits_out);
445 | }, deferred.reject);
446 |
447 | return deferred.promise;
448 | };
449 |
450 |
451 | return {
452 | "search": search
453 | };
454 | }]
455 | );
456 | ```
457 |
458 | 这个Service主要做的是创建ElasitcSearch Query,然后返回解析结果。
459 |
460 |
461 | **设计思路**
462 |
463 | 1. 判断是否有上次记录的位置信息,如果有则将地图的中心设置为上次的位置。
464 | 2. 将位置添加到ElasticSearch的Query中。
465 | 3. 从ElasticSearch中获取数据,并解析Render到地图上。
466 |
467 | **OpenLayer**
468 |
469 | > OpenLayers是一个用于开发WebGIS客户端的JavaScript包。OpenLayers 支持的地图来源包括Google Maps、Yahoo、 Map、微软Virtual Earth 等,用户还可以用简单的图片地图作为背景图,与其他的图层在OpenLayers 中进行叠加,在这一方面OpenLayers提供了非常多的选择。除此之外,OpenLayers实现访问地理空间数据的方法都符合行业标准。OpenLayers 支持Open GIS 协会制定的WMS(Web Mapping Service)和WFS(Web Feature Service)等网络服务规范,可以通过远程服务的方式,将以OGC 服务形式发布的地图数据加载到基于浏览器的OpenLayers 客户端中进行显示。OpenLayers采用面向对象方式开发,并使用来自Prototype.js和Rico中的一些组件。
470 |
471 | ### Step 7: Ionic OpenLayer 地图显示
472 |
473 | 1.下载OpenLayer
474 |
475 | 2.添加到``index.html``:
476 |
477 | ```html
478 |
479 | ```
480 |
481 | **创建NSService**
482 |
483 | 新建一个``MapCtrl``,需要用到``ESService``和 ``NSService``,NSService是官方示例中的一个函数,提供了一个``getRendererFromQueryString``方法。
484 |
485 | ```javascript
486 | .factory('NSService', function(){
487 | var exampleNS = {};
488 |
489 | exampleNS.getRendererFromQueryString = function() {
490 | var obj = {}, queryString = location.search.slice(1),
491 | re = /([^&=]+)=([^&]*)/g, m;
492 |
493 | while (m = re.exec(queryString)) {
494 | obj[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
495 | }
496 | if ('renderers' in obj) {
497 | return obj['renderers'].split(',');
498 | } else if ('renderer' in obj) {
499 | return [obj['renderer']];
500 | } else {
501 | return undefined;
502 | }
503 | };
504 |
505 | return {
506 | "exampleNS": exampleNS
507 | };
508 | })
509 | ```
510 |
511 | **创建基本地图显示**
512 |
513 | 这里我们使用的是Bing地图:
514 |
515 | ```javascirpt
516 | var view = new ol.View({
517 | center: map_center,
518 | zoom: 4
519 | });
520 |
521 | var controls = ol.control.defaults({rotate: false});
522 | var interactions = ol.interaction.defaults({altShiftDragRotate:false, pinchRotate:false});
523 |
524 | var map = new ol.Map({
525 | controls: controls,
526 | interactions: interactions,
527 | layers: [
528 | new ol.layer.Tile({
529 | source: new ol.source.BingMaps({
530 | key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3',
531 | culture: 'zh-CN',
532 | imagerySet: 'Road'
533 | })
534 | })
535 | ],
536 | renderer: NSService.exampleNS.getRendererFromQueryString(),
537 | target: 'map',
538 | view: view
539 | });
540 | ```
541 |
542 | 一个简单的地图如上如示。
543 |
544 | **获取当前位置**
545 |
546 | ngCordova有一个插件是``$cordovaGeolocation``,用于获取当前的位置。代码如下所示:
547 |
548 | ```javascript
549 | var posOptions = {timeout: 10000, enableHighAccuracy: true};
550 | $cordovaGeolocation
551 | .getCurrentPosition(posOptions)
552 | .then(function (position) {
553 | var pos = new ol.proj.transform([position.coords.longitude, position.coords.latitude], 'EPSG:4326', 'EPSG:3857');
554 |
555 | $localstorage.set('position', [position.coords.latitude, position.coords.longitude].toString());
556 | $localstorage.set('map_center', pos);
557 |
558 | view.setCenter(pos);
559 | }, function (err) {
560 | console.log(err)
561 | });
562 | ```
563 |
564 | 当获取到位置时,将位置存储到``localstorage``中。
565 |
566 | **获取结果并显示**
567 |
568 | 最后代码如下所示,获取解析后的结果,添加icon
569 |
570 | ```javascript
571 | ESService.search("", 0).then(function(results){
572 | var vectorSource = new ol.source.Vector({ });
573 | $.each(results, function(index, result){
574 | var position = result.location.split(",");
575 | var pos = ol.proj.transform([parseFloat(position[1]), parseFloat(position[0])], 'EPSG:4326', 'EPSG:3857');
576 |
577 | var iconFeature = new ol.Feature({
578 | geometry: new ol.geom.Point(pos),
579 | name: result.title,
580 | phone: result.phone_number,
581 | distance: result.distance
582 | });
583 | vectorSource.addFeature(iconFeature);
584 | });
585 |
586 | var iconStyle = new ol.style.Style({
587 | image: new ol.style.Icon(({
588 | anchor: [0.5, 46],
589 | anchorXUnits: 'fraction',
590 | anchorYUnits: 'pixels',
591 | opacity: 0.75,
592 | src: 'img/icon.png'
593 | }))
594 | });
595 |
596 | var vectorLayer = new ol.layer.Vector({
597 | source: vectorSource,
598 | style: iconStyle
599 | });
600 | map.addLayer(vectorLayer);
601 | });
602 | ```
603 |
604 | **添加点击事件**
605 |
606 | 在上面的代码中添加:
607 |
608 | ```javascript
609 | var element = document.getElementById('popup');
610 |
611 | var popup = new ol.Overlay({
612 | element: element,
613 | positioning: 'bottom-center',
614 | stopEvent: false
615 | });
616 | map.addOverlay(popup);
617 |
618 | map.on('click', function(evt) {
619 | var feature = map.forEachFeatureAtPixel(evt.pixel,
620 | function(feature, layer) {
621 | return feature;
622 | });
623 |
624 | if (feature) {
625 | var geometry = feature.getGeometry();
626 | var coord = geometry.getCoordinates();
627 | popup.setPosition(coord);
628 | $(element).popover({
629 | 'placement': 'top',
630 | 'html': true,
631 | 'content': "商品:" + feature.get('name') + "
" + '' +
632 | '' +
634 | " " + feature.get('distance') + "公里
"
635 | });
636 | $(element).popover('show');
637 | } else {
638 | $(element).popover('destroy');
639 | }
640 | });
641 | ```
642 |
643 | 当用户点击时,调用Bootstrap的Popover来显示信息。
644 |
645 | 
646 |
--------------------------------------------------------------------------------
/chapters/lettuce.md:
--------------------------------------------------------------------------------
1 | 一步步搭建JavaScript框架
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 从开始打算写一个MV*,到一个简单的demo,花了几天的时间,虽然很多代码都是复制/改造过来的,然而**It Works**(nginx的那句话会让人激动有木有)。现在他叫lettuce,代码 [https://github.com/phodal/lettuce](https://github.com/phodal/lettuce),如果有兴趣可以加入我们。
10 |
11 | 虽然js还不够expert,但是开始了。
12 |
13 | 步骤
14 | ---
15 |
16 | ###Step 1: 注册npm和bower包
17 |
18 | 一开始我做的3次commits是:
19 |
20 | * e4e6e04 - Add README.md (3 weeks ago)
21 | * 37411d7 - publish bower (3 weeks ago)
22 | * aabf278 - init project (3 weeks ago)
23 |
24 | 是的一开始什么也没做,除了从``bower``和``npm``上注册了一个叫做``lettuce``的库:
25 |
26 | ```javascript
27 | {
28 | "name": "lettuce",
29 | "version": "0.0.2",
30 | "authors": [
31 | "Fengda HUANG "
32 | ],
33 | "description": "A Mobile JavaScript Framework",
34 | "main": "index.js",
35 | "moduleType": [
36 | "amd",
37 | "node"
38 | ],
39 | "keywords": [
40 | "lettuce",
41 | "mobile"
42 | ],
43 | "license": "MIT",
44 | "homepage": "http://lettuce.phodal.com",
45 | "private": false,
46 | "ignore": [
47 | "**/.*",
48 | "node_modules",
49 | "bower_components",
50 | "test",
51 | "tests"
52 | ]
53 | }
54 | ```
55 |
56 | 然后在我们还没有开始写代码的时候版本就已经是``0.0.2``这个速度好快。。总结如下:
57 |
58 | - 取一个好的名字
59 | - 在npm和bower上挖一个坑给自己
60 | - 开始写README.md
61 |
62 | 所以我的``README.md``是这样子的
63 |
64 | ```
65 | #Lettuce
66 |
67 | > A is Mobile JavaScript Framework
68 |
69 | Coming soon
70 | ```
71 |
72 | 是的,我们的代码已经``Coming soon``了。
73 |
74 | ### Step 2: 生成Javascript项目框架
75 |
76 | 为了简化这一个痛苦的过程,我们还是用yeoman。
77 |
78 | #### 安装Yeoman lib生成器
79 |
80 | 1.安装yeoman
81 |
82 | ```bash
83 | npm install -g yo
84 | ```
85 |
86 | 2.安装generator-lib
87 |
88 | ```bash
89 | npm install -g generator-lib
90 | ```
91 |
92 | 3.创建项目
93 |
94 | ```bash
95 | mkdir ~/lettuce && cd $_
96 | yo lib
97 | ```
98 |
99 | 接着我们就迎来了
100 |
101 | ```
102 | _-----_
103 | | |
104 | |--(o)--| .--------------------------.
105 | `---------´ | Welcome to Yeoman, |
106 | ( _´U`_ ) | ladies and gentlemen! |
107 | /___A___\ '__________________________'
108 | | ~ |
109 | __'.___.'__
110 | ´ ` |° ´ Y `
111 |
112 | [?] What do you want to call your lib? Lettuce
113 | [?] Describe your lib: A Framework for Romantic
114 | [?] What is your GitHub username? phodal
115 | [?] What is your full name? Fengda Huang
116 | [?] What year for the copyright? 2015
117 | ```
118 |
119 | 省略上百字,你的目录里就会有
120 |
121 | ```
122 | .
123 | |____.editorconfig
124 | |____.gitattributes
125 | |____.gitignore
126 | |____.jshintrc
127 | |____bower.json
128 | |____demo
129 | | |____assets
130 | | | |____main.css
131 | | | |____normalize.css
132 | | |____index.html
133 | |____dist
134 | | |____Lettuce.js
135 | | |____Lettuce.min.js
136 | |____docs
137 | | |____MAIN.md
138 | |____Gruntfile.js
139 | |____index.html
140 | |____LICENSE.txt
141 | |____package.json
142 | |____README.md
143 | |____src
144 | | |_____intro.js
145 | | |_____outro.js
146 | | |____main.js
147 | |____test
148 | | |____all.html
149 | | |____all.js
150 | | |____lib
151 | | | |____qunit.css
152 | | | |____qunit.js
153 | ```
154 |
155 | 这么多的文件。
156 |
157 | #### Build JavaScript项目
158 |
159 | 于是我们执行了一下
160 |
161 | grunt
162 |
163 | 就有了这么多的log:
164 |
165 | ```
166 | Running "concat:dist" (concat) task
167 | File "dist/Lettuce.js" created.
168 |
169 | Running "jshint:files" (jshint) task
170 | >> 1 file lint free.
171 |
172 | Running "qunit:files" (qunit) task
173 | Testing test/all.html .OK
174 | >> 1 assertions passed (20ms)
175 |
176 | Running "uglify:dist" (uglify) task
177 | File "dist/Lettuce.min.js" created.
178 |
179 | Done, without errors.
180 | ```
181 |
182 | 看看我们的Lettuce.js里面有什么
183 |
184 | ```
185 | (function(root, undefined) {
186 | "use strict";
187 | /* Lettuce main */
188 | // Base function.
189 | var Lettuce = function() {
190 | // Add functionality here.
191 | return true;
192 | };
193 | // Version.
194 | Lettuce.VERSION = '0.0.1';
195 | // Export to the root, which is probably `window`.
196 | root.Lettuce = Lettuce;
197 | }(this));
198 | ```
199 |
200 | 我们的库写在[立即执行函数表达式](https://www.phodal.com/blog/javascript-immediately-invoked-function-expression)里面。这样便是和jQuery等库一样了。
201 |
202 | grunt里的任务包含了:
203 |
204 | - jshint 代码检查
205 | - contact 合并js到一个文件
206 | - minify js 压缩js
207 | - qunit 单元测试
208 |
209 | 这样我们就可以轻松上路了。
210 |
211 | ### Step 3: 寻找所需要的函数
212 |
213 | ### Step 4: 整合
214 |
215 | ### Step 5: 测试
216 |
217 | ### 练习建议
218 |
--------------------------------------------------------------------------------
/chapters/lock.md:
--------------------------------------------------------------------------------
1 | 制作简易Mac OS上的伪锁屏工具
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 某天中午在锁屏的时候,想到一件有意思的事。平时我会有两种“锁屏方式”:
10 |
11 | - 传统的锁屏方式:command + alt + 电源,适用于长时间离开。
12 | - 将屏幕亮度调暗,适用于上个厕所什么的。
13 |
14 | 然后在我回来的时候,我在想如果别人都知道我的屏幕是变暗的话,那么就有意思了。。。然后我就想做个简单的工具,来Mock当前的屏幕:
15 |
16 | 1. 一键截取当前屏幕
17 | 2. 打开截图将设为最大化
18 | 3. 降低屏幕亮度
19 |
20 | 接着,我就开始编写了。
21 |
22 | 步骤
23 | ---
24 |
25 | ### Step 1:屏幕截图
26 |
27 | 在Mac OS上有一个工具``screencapture``,可以用于截取当前屏幕。如:
28 |
29 | ```
30 | screencapture screen.png
31 | ```
32 |
33 | 将图片存储为screen.png,我们只需要调用Python调用系统的命令,即可:
34 |
35 | ```bash
36 | import os
37 |
38 | os.system("screencapture screen.png")
39 | ```
40 |
41 | ### Step 2:调节亮度
42 |
43 | 在Mac OS上有一个工具叫``brightness``,可以用百分比调节屏幕的亮度,如
44 |
45 | ```bash
46 | brightness 0
47 | ```
48 |
49 | 可以将屏幕的亮度调到0,即最低。所以,先安装这个工具:
50 |
51 | ```bash
52 | brew install brightness
53 | ```
54 |
55 | 然后用Python的os模块,即可调用 。
56 |
57 | ### Step 3:全屏图片
58 |
59 | 随后,用GTK简单的弄了个全屏图片的脚本,就完成了。
60 |
61 | ```python
62 |
63 | import os
64 | import pygtk
65 | import gtk
66 |
67 | os.system("screencapture screen.png")
68 | os.system("brightness 0")
69 |
70 | win = gtk.Window()
71 |
72 | im = gtk.Image()
73 | pixbuf = gtk.gdk.pixbuf_new_from_file("screen.png")
74 | scaled_buf = pixbuf.scale_simple(1440,900,gtk.gdk.INTERP_BILINEAR)
75 | im.set_from_pixbuf(scaled_buf)
76 | im.show()
77 |
78 | vbox = gtk.VBox()
79 | vbox.pack_start (im)
80 | win.add(vbox)
81 |
82 | win.fullscreen()
83 | win.show_all()
84 |
85 | win.set_keep_above(True);
86 | gtk.main()
87 | ```
88 |
89 | 最后,再写个简单的函数即可:
90 |
91 | ```bash
92 | function ss() {
93 | python "/Users/fdhuang/learing/mock-screen/main.py"
94 | }
95 | ```
96 |
--------------------------------------------------------------------------------
/chapters/luffa.md:
--------------------------------------------------------------------------------
1 | 基于Virtual DOM的测试代码生成
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 当我们在写一些UI测试的时候,我们总需要到浏览器去看一下一些DOM的变化。比如,我们点击了某个下拉菜单,会有另外一个联动的下拉菜单发生了变化。而如果这个事件更复杂的时候,有时我们可能就很难观察出来他们之间的变化。
10 |
11 | ### ShowCase
12 |
13 | 
14 |
15 | 源码见:[https://github.com/phodal/luffa](https://github.com/phodal/luffa)
16 |
17 | ### 基本原理
18 |
19 | 尽管这里的例子是以Jasmine作为例子,但是我想对于React也会有同样的方法。
20 |
21 | **一个Jasmine jQuery测试**
22 |
23 | 如下是一个简单的Jamine jQuery的测试示例:
24 |
25 | ```javascript
26 | describe("toHaveCss", function (){
27 | beforeEach(function (){
28 | setFixtures(sandbox())
29 | })
30 |
31 | it("should pass if the element has matching css", function (){
32 | $("#sandbox").css("display", "none")
33 | $("#sandbox").css("margin-left", "10px")
34 | expect($("#sandbox")).toHaveCss({display: "none", "margin-left": "10px"})
35 | })
36 | });
37 | ```
38 |
39 | 在beforeEach的时候,我们设定了固定的DOM进去,按照用户的行为做一些相应的操作。接着依据这个DOM中的元素变化 ,来作一些断言。
40 |
41 | 那么,即使我们已经有一个固定的DOM,想要监听这个DOM的变化就是一件容易的事。在我们断言之前,我们就会有一个新的DOM。我们只需要Diff一下这两个DOM的变化,就可以生成这部分测试代码。
42 |
43 | 步骤
44 | ---
45 |
46 | ###Step 1: Virtual-dom与HyperScript
47 |
48 | 在寻觅中发现了[virtual-dom](https://github.com/Matt-Esch/virtual-dom)这个库,一个可以支持创建元素、diff计算以及patch操作的库,并且它效率好像还不错。
49 |
50 | virtual-dom可以说由下面几部分组成的:
51 |
52 | 1. createElement,用于创建virtual Node。
53 | 2. diff,顾名思义,diff算法。
54 | 3. h,用于创建虚拟树的DSL——HyperScript。HyperScript是一个JavaScript的HyperText。
55 | 4. patch,用于patch修改的内容。
56 |
57 | 举例来说,我们有下面一个生成Virtual DOM的函数:
58 |
59 | ```javascript
60 | function render(count) {
61 | return h('div', {
62 | style: {
63 | textAlign: 'center',
64 | lineHeight: (100 + count) + 'px',
65 | border: '1px solid red',
66 | width: (100 + count) + 'px',
67 | height: (100 + count) + 'px'
68 | }
69 | }, [String(count)]);
70 | }
71 | ```
72 |
73 | render函数用于生成一个Virtual Node。在这里,我们可以将我们的变量传进去,如1。就会生成如下图所示的节点:
74 |
75 | ```javascript
76 | {
77 | "children": [
78 | {
79 | "text": "1"
80 | }
81 | ],
82 | "count": 1,
83 | "descendantHooks": false,
84 | "hasThunks": false,
85 | "hasWidgets": false,
86 | "namespace": null,
87 | "properties": {
88 | "style": {
89 | "border": "1px solid red",
90 | "height": "101px",
91 | "lineHeight": "101px",
92 | "textAlign": "center",
93 | "width": "101px"
94 | }
95 | },
96 | "tagName": "DIV"
97 | }
98 | ```
99 |
100 | 其中包含中相对应的属性等等。而我们只要调用createElement就可以创建出这个DOM。
101 |
102 | 如果我们修改了这个节点的一些元素,或者我们render了一个count=2的值时,我们就可以diff两个DOM。如:
103 |
104 | ```javascript
105 | virtualDom.diff(render(2), render(1))
106 | ```
107 |
108 | 根据两个值的变化就会生成如下的一个对象:
109 |
110 | ```javascript
111 | {
112 | "0": {
113 | "patch": {
114 | "style": {
115 | "height": "101px",
116 | "lineHeight": "101px",
117 | "width": "101px"
118 | }
119 | },
120 | "type": 4,
121 | "vNode": {
122 | ...
123 | }
124 | },
125 | "1": {
126 | "patch": {
127 | "text": "1"
128 | },
129 | "type": 1,
130 | "vNode": {
131 | "text": "2"
132 | }
133 | },
134 | ...
135 | }
136 | ```
137 |
138 | 第一个对象,即0中包含了一些属性的变化。而第二个则是文本的变化——从2变成了1。我们所要做的测试生成便是标记这些变化,并记录之。
139 |
140 | ### Step 2: 标记DOM变化
141 |
142 | 由于virtual-dom依赖于虚拟节点vNode,我们需要将fixtures转换为hyperscript。这里我们就需要一个名为html2hyperscript的插件,来解析html。接着,我们就可以diff转换完后的DOM:
143 |
144 | ```javascript
145 | var leftNode = "", rightNode = "";
146 | var fixtures = 'Hello World
';
147 | var change = 'Hello World
fs
';
148 | parser(fixtures, function (err, hscript) {
149 | leftNode = eval(hscript);
150 | });
151 |
152 | parser(change, function (err, hscript) {
153 | rightNode = eval(hscript);
154 | });
155 |
156 | var patches = diff(leftNode, rightNode);
157 | ```
158 |
159 | 接着,我们需要调用patch函数来做一些相应的改变。
160 |
161 | ```javascript
162 | luffa.patch(virtualDom.create(leftNode), patches)
163 | ```
164 |
165 | 并且,我们可以尝试在patch阶段做一些处理——输出修改:
166 |
167 | ```javascript
168 | function printChange(originRootNodeHTML, applyNode) {
169 | var patchType;
170 |
171 | for (var patchIndex = 0; patchIndex < applyNode.newNodes.length; patchIndex++) {
172 | patchType = applyNode.newNodes[patchIndex].method;
173 | switch (patchType) {
174 | case 'insert':
175 | printInsert(applyNode);
176 | break;
177 | case 'node':
178 | printNode(applyNode, originRootNodeHTML, patchIndex);
179 | break;
180 | case 'remove':
181 | printRemove(applyNode, originRootNodeHTML, patchIndex);
182 | break;
183 | case 'string':
184 | printString(applyNode, originRootNodeHTML, patchIndex);
185 | break;
186 | case 'prop':
187 | printProp(applyNode, originRootNodeHTML, patchIndex);
188 | break;
189 | default:
190 | printDefault(applyNode, originRootNodeHTML, patchIndex);
191 | }
192 | }
193 | }
194 | ```
195 |
196 | 根据不同的类型,作一些对应的输出处理,如pringNode:
197 |
198 | ```javascript
199 | function printNode(applyNode, originRootNodeHTML, patchIndex) {
200 | var originNode = $(applyNode.newNodes[patchIndex].vNode).prop('outerHTML') || $(applyNode.newNodes[patchIndex].vNode).text();
201 | var newNode = $(applyNode.newNodes[patchIndex].newNode).prop('outerHTML');
202 |
203 | console.log('%c' + originRootNodeHTML.replace(originNode, '%c' + originNode + '%c') + ', %c' + newNode, luffa.ORIGIN_STYLE, luffa.CHANGE_STYLE, luffa.ORIGIN_STYLE, luffa.NEW_STYLE);
204 | }
205 | ```
206 |
207 | 用Chrome的console来标记修改的部分,及添加的部分。
208 |
209 | 最后,我们似乎就可以生成相应的测试代码了。。。
210 |
--------------------------------------------------------------------------------
/chapters/moqimobi.md:
--------------------------------------------------------------------------------
1 | 基于Backbone的单页面移动应用
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 看到项目上的移动框架,网上寻找了一下,发现原来这些一开始都有。于是,找了个示例开始构建一个移动平台的CMS——[墨颀 CMS](http://cms.moqi.mobi),方便项目深入理解的同时,也可以自己维护一个CMS系统。
10 |
11 | ### Showcase
12 |
13 | GitHub: [http://github.com/phodal/moqi.mobi](http://github.com/phodal/moqi.mobi)
14 |
15 | Demo: [墨颀 CMS](http://cms.moqi.mobi)
16 |
17 | ### jQuery + Backbone + UnderScore + Require.JS
18 |
19 | 尝试过用AngularJS和EmberJS,发现对于使用AngularJS以及EmberJS来说,主要的问题是要使用自己熟悉的东西没那么容易引入。而且考虑到谷歌向来对自己的项目的支持不是很好~~,所以便放弃了AngluarJS的想法。
20 |
21 | 于是开始寻找一些方案,但是最后还是选择了一个比较通用的方案。
22 |
23 | - RequireJS
24 | - jQuery
25 | - Underscore
26 | - Backbone
27 |
28 | 相对于AngularJS来说,Backbone是一个轻量级的方案,从大小上来说。对于自己来说,灵活性算是其中好的一点,也就是自己可以随意的加入很多东西。
29 |
30 | **关于Backbone**
31 |
32 | > Backbone.js是一套JavaScript框架与RESTful JSON的应用程式接口。也是一套大致上符合MVC架构的编程范型。Backbone.js以轻量为特色,只需依赖一套Javascript 函式库即可运行。
33 |
34 | 具体功能上应该是
35 |
36 | - Backbone 轻量级,支持jquery,自带路由,对象化视图,强大的sync机制减少页面大小从而加快页面显示。
37 | - jQuery jQuery使用户能更方便地处理HTML(标准通用标记语言下的一个应用)、events、实现动画效果,并且方便地为网站提供AJAX交互。不过主要是jQuery能够使用户的html页面保持代码和html内容分离,只需定义id即可。
38 | - Underscore是Backbone的依赖库 Underscore 是一个JavaScript实用库,提供了类似Prototype.js的一些功能,但是没有继承任何JavaScript内置对象。
39 | - RequireJS 你可以顺序读取仅需要相关依赖模块。
40 |
41 | 前台UI,使用的是Pure CSS,一个轻量级的CSS框架,但是最后感觉,总体用到一起,大小还是相当的。只是可以有一个更好的移动体验。
42 |
43 | ****其他可替换的框架**
44 |
45 | **AngularJS**,考虑到某些因素,可能会替换掉Backbone,但是还不是当前可行的方案。为了学习是一方案,也为了更好的普及某些东西。
46 |
47 | **handlebars** Handlebars 是Mustache的改进,显示与逻辑分离,语法兼容Mustache,可以编译成代码,改进Mustache对路径的支持,但是若需要在服务端运行需要使用服务端Javascript引擎如Node.js。
48 |
49 | **项目**
50 |
51 | 前后端分离设计,后台对前台只提供JSON数据,所以在某种意义上来说可能会只适合浏览,和这个要配合后台的框架。总的来说,适合于阅读类的网站。
52 |
53 | **源码**
54 |
55 | 代码依然是放在Github上,基本功能已经可以Works了。
56 |
57 | [https://github.com/gmszone/moqi.mobi](https://github.com/gmszone/moqi.mobi)
58 |
59 | 步骤
60 | ---
61 |
62 | ### Step 1: 使用Require.js管理依赖
63 |
64 | **库及依赖**
65 |
66 | 这里用的是bower的JS来下载库,详细可以参考一下[bower install js使用bower管理js](http://www.phodal.com/blog/use-bower-to-install-js-plugins/) 这篇文章。
67 |
68 | 需要下载的库有
69 |
70 | - RequireJS
71 | - Backbone
72 | - Underscore
73 | - Mustache
74 | - jQuery
75 |
76 | 引用官网的示例
77 |
78 |
79 |
80 |
81 | My Sample Project
82 |
84 |
85 |
86 |
87 | My Sample Project
88 |
89 |
90 |
91 | 我们需要一个require.js和一个main.js放在同一个目录,在main.js中用使用require()来载入需要加载的脚本。
92 |
93 | require.config({
94 | baseUrl: 'lib/',
95 | paths: {
96 | jquery: 'jquery-2.1.1.min'
97 | },
98 | shim: {
99 | underscore: {
100 | exports: '_'
101 | }
102 | }
103 | });
104 |
105 | require(['../app'], function(App){
106 | App.initialize();
107 | });
108 |
109 | 在config中可以配置好其他的库,接着调用了app.js。
110 |
111 | define(['jquery', 'underscore'], function($, _){
112 | var initialize = function() {
113 | console.log("Hello World");
114 | }
115 |
116 | return {
117 | initialize: initialize
118 | };
119 | });
120 |
121 | 当打开index.html的时候便会在console中输出``Hello World``。这样我们就完成一个基本的框架,只是还没有HTML,
122 | 文件列表如下所示
123 |
124 | .
125 | |____app.js
126 | |____backbone.js
127 | |____HomeView.js
128 | |____index.html
129 | |____jquery.js
130 | |____main.js
131 | |____mustache.js
132 | |____require.js
133 | |____router.js
134 | |____text.js
135 | |____underscore.js
136 |
137 | 在这里有些混乱,但是为了少去其中的一些配置的麻烦,就先这样讲述。
138 |
139 | ### Step 2: 添加路由
140 |
141 | 用Backbone的一个目的就在于其的路由功能,于是便添加这样一个js——``router.js``,内容如下所示:
142 |
143 | define([
144 | 'jquery',
145 | 'underscore',
146 | 'backbone',
147 | 'HomeView.js'
148 | ], function($, _, Backbone, HomeView) {
149 |
150 | var AppRouter = Backbone.Router.extend({
151 | routes: {
152 | 'index': 'homePage',
153 | '*actions': 'homePage'
154 | }
155 | });
156 | var initialize = function() {
157 | var app_router = new AppRouter;
158 |
159 | app_router.on('route:homePage', function() {
160 | var homeView = new HomeView();
161 | homeView.render();
162 | });
163 |
164 | Backbone.history.start();
165 | };
166 | return {
167 | initialize: initialize
168 | };
169 | });
170 |
171 | 在这里我们先忽略掉HomeView.js,因为这是下面要讲的,在router.js中,我们定义了一个AppRouter,
172 |
173 | - ``index``指向的是在初始化时候定义的homePage,这样就可以将主页转向HomeView.js。
174 | - ``*actions``便是将其他未匹配的都转向homePage。
175 |
176 | 接着我们需要修改一下``app.js``,让他一运行地时候便可以进入路由选择
177 |
178 | define(['jquery', 'underscore', 'router'], function($, _, Router) {
179 | var initialize = function() {
180 | Router.initialize();
181 | };
182 |
183 | return {
184 | initialize: initialize
185 | };
186 | });
187 |
188 | 也就是初始化一下路由。
189 |
190 | ### Step 3: 创建主页View
191 |
192 | 使用Mustache的优点在于,后台仅仅只需要提供数据,并在前台提供一个位置。因此我们修改了下HTML
193 |
194 |
195 |
196 |
197 | My Sample Project
198 |
199 |
200 |
201 | {{project}}
202 |
203 | The best jQuery plugin for creating side menus and the easiest way for doing your menu responsive
258 |
259 | 这是一个创建响应式侧边栏的最好的也是最简单的工具,于是我们需要下载jQuery.sidr.min.js到目录中,接着修改一下main.js:
260 |
261 | require.config({
262 | baseUrl: 'lib/',
263 | paths: {
264 | 'text': 'text',
265 | jquery: 'jquery-2.1.1.min',
266 | async: 'require/async',
267 | json: 'require/json',
268 | mdown: 'require/mdown',
269 | router: '../router',
270 | templates: '../templates',
271 | jquerySidr: 'jquery.sidr.min',
272 | markdownConverter : 'require/Markdown.Converter'
273 | },
274 | shim: {
275 | jquerySidr:["jquery"],
276 | underscore: {
277 | exports: '_'
278 | }
279 | }
280 | });
281 |
282 | require(['../app'], function(App){
283 | App.initialize();
284 | });
285 |
286 | 添加jquery.sidr.min到里面。
287 |
288 | 引用官方的示例代码
289 |
290 | $(document).ready(function() {
291 | $('#simple-menu').sidr();
292 | });
293 |
294 | 我们需要将上面的初始化代码添加到app.js的初始化中,
295 |
296 | define([
297 | 'jquery',
298 | 'underscore',
299 | 'backbone',
300 | 'router',
301 | 'jquerySidr'
302 | ], function($, _, Backbone, Router){
303 |
304 | var initialize = function(){
305 | $(document).ready(function() {
306 | $('#menu').sidr();
307 | });
308 | Router.initialize();
309 | };
310 |
311 | return {
312 | initialize: initialize
313 | };
314 | });
315 |
316 | 这样打开[墨颀 CMS](http://cms.moqi.mobi)便可以看到最后的效果。
317 |
318 |
319 | 正在一步步完善[墨颀 CMS](http://cms.moqi.mobi/),在暂时不考虑其他新的功能的时候,先和[自己的博客](http://www.phodal.com)整合一下。
320 |
321 | ### Step 5: Django Tastypie示例
322 |
323 | 之前用AngluarJS做的全部文章的时候是Tastypie做的API,只是用来生成的是博客的内容。只是打开的速度好快,可以在1秒内打开,献上URL:
324 |
325 | [http://www.phodal.com/api/v1/url/?offset=0&limit=20&format=json](http://www.phodal.com/api/v1/url/?offset=0&limit=20&format=json)
326 |
327 | 之前只是拿Tastypie生成一些简单的JSON数据,如keywords_string,slug,title这些简单的数据。
328 |
329 | 因为这里的Blogpost是来自mezzanine,原来的``api.py``,如下所示:
330 |
331 | from tastypie.resources import ModelResource
332 | from mezzanine.blog.models import BlogPost, BlogCategory
333 |
334 |
335 | class AllBlogSlugResource(ModelResource):
336 | class Meta:
337 | queryset = BlogPost.objects.published()
338 | resource_name = "url"
339 | fields = ['keywords_string', 'slug', 'title']
340 | allowed_methods = ['get']
341 |
342 |
343 | class BlogResource(ModelResource):
344 | class Meta:
345 | queryset = BlogPost.objects.published()
346 | resource_name = "blog"
347 | fields = ['keywords_string', 'slug', 'title', 'content', 'description']
348 | allowed_methods = ['get']
349 |
350 | 而这时为了测试方便,还需要解决跨域请求的问题,生成的内容大致如下所示:
351 |
352 |
353 | {
354 | "meta": {
355 | "limit": 1,
356 | "next": "/api/v1/url/?offset=1&limit=1&format=json",
357 | "offset": 0,
358 | "previous": null,
359 | "total_count": 290
360 | },
361 | "objects": [
362 | {
363 | "keywords_string": "jquery backbone mustache underscore siderbar",
364 | "resource_uri": "/api/v1/url/369/",
365 | "slug": "use-jquery-backbone-mustache-build-mobile-app-cms-add-jquery-plugins",
366 | "title": "构建基于Javascript的移动web CMS——添加jQuery插件"
367 | }
368 | ]
369 | }
370 |
371 | #### 跨域支持
372 |
373 | 于是网上搜索了一下,有了下面的代码:
374 |
375 | from tastypie.resources import Resource, ModelResource
376 | from mezzanine.blog.models import BlogPost, BlogCategory
377 | from django.http.response import HttpResponse
378 | from tastypie.exceptions import ImmediateHttpResponse
379 | from tastypie import http
380 | from tastypie.serializers import Serializer
381 |
382 | class BaseCorsResource(Resource):
383 |
384 | def create_response(self, *args, **kwargs):
385 | response = super(BaseCorsResource, self).create_response(*args, **kwargs)
386 | response['Access-Control-Allow-Origin'] = '*'
387 | response['Access-Control-Allow-Headers'] = 'Content-Type'
388 | return response
389 |
390 | def post_list(self, request, **kwargs):
391 |
392 | response = super(BaseCorsResource, self).post_list(request, **kwargs)
393 | response['Access-Control-Allow-Origin'] = '*'
394 | response['Access-Control-Expose-Headers'] = 'Location'
395 | return response
396 |
397 | def method_check(self, request, allowed=None):
398 |
399 | if allowed is None:
400 | allowed = []
401 |
402 | request_method = request.method.lower()
403 | allows = ','.join(map(lambda s: s.upper(), allowed))
404 |
405 | if request_method == 'options':
406 | response = HttpResponse(allows)
407 | response['Access-Control-Allow-Origin'] = '*'
408 | response['Access-Control-Allow-Headers'] = 'Content-Type'
409 | response['Access-Control-Allow-Methods'] = "GET, PUT, POST, PATCH"
410 | response['Allow'] = allows
411 | raise ImmediateHttpResponse(response=response)
412 |
413 | if not request_method in allowed:
414 | response = http.HttpMethodNotAllowed(allows)
415 | response['Allow'] = allows
416 | raise ImmediateHttpResponse(response=response)
417 |
418 | return request_method
419 |
420 | class AllBlogSlugResource(BaseCorsResource, ModelResource):
421 | class Meta:
422 | queryset = BlogPost.objects.published()
423 | resource_name = "url"
424 | fields = ['keywords_string', 'slug', 'title']
425 | allowed_methods = ['get']
426 | serializer = Serializer()
427 |
428 | class BlogResource(BaseCorsResource, ModelResource):
429 | class Meta:
430 | queryset = BlogPost.objects.published()
431 | resource_name = "blog"
432 | fields = ['keywords_string', 'slug', 'title', 'content', 'description']
433 | allowed_methods = ['get']
434 | serializer = Serializer()
435 |
436 | 接着便可以很愉快地、危险地跨域。
437 |
438 | #### 整合
439 |
440 | 接着修改了一下代码中configure.json的blogListUrl,以及模块
441 |
442 |
443 |
动 态
444 | {{#objects}}
445 |
446 |
447 | {{title}}
448 |
449 | {{/objects}}
450 |
451 |
452 | 便可以请求到结果了。
453 |
454 | 一开始对于可配置的选择是正确的.
455 |
456 | ### Step 6: RequireJS Plugins
457 |
458 | 网上搜索到一个叫RequireJS Plugins的repo。
459 |
460 | 里面有这样的几个插件:
461 |
462 | - **async** : Useful for JSONP and asynchronous dependencies (e.g. Google Maps).
463 | - **font** : Load web fonts using the [WebFont Loader API](https://code.google.com/apis/webfonts/docs/webfont_loader.html)
464 | (requires `propertyParser`)
465 | - **goog** : Load [Google APIs](http://code.google.com/apis/loader/)
466 | asynchronously (requires `async!` plugin and `propertyParser`).
467 | - **image** : Load image files as dependencies. Option to "cache bust".
468 | - **json** : Load JSON files and parses the result. (Requires `text!` plugin).
469 | - **mdown** : Load Markdown files and parses into HTML. (Requires `text!`
470 | plugin and a markdown converter).
471 | - **noext** : Load scripts without appending ".js" extension, useful for
472 | dynamic scripts.
473 |
474 | 于是,我们可以用到这里的json用来加载JSON文件,虽然也可以用Requirejs的text插件,但是这里的json有对此稍稍的优化。
475 |
476 | 在后面的部分中我们也用到了mdown,用于显示一个md文件,用法上大致是一样的。
477 |
478 | 将json.js插件放到目录里,再配置好main.js。
479 |
480 | require.config({
481 | paths: {
482 | 'text': 'text',
483 | jquery: 'jquery',
484 | json: 'require/json'
485 | },
486 | shim: {
487 | underscore: {
488 | exports: '_'
489 | }
490 | }
491 | });
492 |
493 | require(['app'], function(App) {
494 | App.initialize();
495 | });
496 |
497 |
498 | 于是我们将HomeView.js中的data变为configure的数据,这样便可以直接使用这个json文件。
499 |
500 | define([
501 | 'jquery',
502 | 'underscore',
503 | 'mustache',
504 | 'text!/index.html',
505 | 'json!/configure.json'
506 | ], function($, _, Mustache, indexTemplate, configure) {
507 |
508 | var HomeView = Backbone.View.extend({
509 | el: $('#aboutArea'),
510 |
511 | render: function() {
512 | this.$el.html(Mustache.to_html(indexTemplate, configure));
513 | }
514 | });
515 |
516 | return HomeView;
517 | });
518 |
519 | configure.json的代码如下所示:
520 |
521 | {
522 | "project": "My Sample Project"
523 | }
524 |
525 | 最后实现的效果和模板结束是一样的,只会在页面上显示
526 |
527 | My Sample Project
528 |
529 | 在[墨颀 CMS](http://cms.moqi.mobi)中的动态的文章是从我博客的API加载过来的,因为当前没有其他好的CMS当接口。之前直接拿博客的DB文件+Nodejs+RESTify生成了一个博客的API,而且可以支持跨域请求。
530 |
531 |
532 | ### Step 6: 简单的博客
533 |
534 | 这次我们可以简单的做一个可以供移动平台阅读的博客,除了不能写作以外(ps:不能写作还能叫博客么)。对于写博客的人来说更多的只是写,而对于读者来说,他们只需要读,所以在某种意义上可以将博客的写和读分离开来。
535 |
536 | 对于用户来说,博客是由两个页面构建的:
537 |
538 | - 博文列表(blogposts list)
539 | - 博客内容(blogposts detail)
540 |
541 | 在这里我们先关注博文列表
542 |
543 | **博文列表**
544 |
545 | 博文列表的内容一般有:
546 |
547 | - 作者(Author)
548 | - 标题(title)
549 | - 创建时间/修改时间(Time)
550 | - 关键词(Keywords)
551 | - 摘要(Description)
552 | - 链接(Slug)
553 |
554 | 一个简单的示例如下,也就是我们接下来要用到的**1.json**中的一部分。
555 |
556 | [{
557 | "title": "构建基于Javascript的移动web CMS入门——简介",
558 | "slug": "use-jquery-backbone-mustache-build-mobile-app-cms",
559 | "description": "看到项目上的移动框架,网上寻找了一下,发现原来这些一开始都有。于是,找了个示例开始构建一个移动平台的CMS——墨颀 CMS,方便项目深入理解的同时,也可以自己维护一个CMS系统。",
560 | "keywords": [
561 | "backbone",
562 | "jquery",
563 | "underscore",
564 | "mustache"
565 | ],
566 | "created": "2014-07-17 14:16:18.035763"
567 | }]
568 |
569 | 这里基本上也就有了上面的要素,除了作者,当然因为作者只有一个,所以在里面写作者就是在浪费流量和钱啊。接着我们就是要把上面的内容读取出来放到CMS里。和之前不同的是,虽然我们可以用和[墨颀CMS文件 JSON文件](http://www.phodal.com/blog/use-jquery-backbone-mustache-build-mobile-app-cms-json-configure/)一样的方法,但是显然这种方法很快就会不适用。
570 |
571 |
572 | #### 获取在线数据
573 |
574 | 这里会用到Backbone的Model,我们先创建一个Model
575 |
576 | var BlogPostModel = Backbone.Model.extend({
577 | name: 'Blog Posts',
578 | url: function(){
579 | return this.instanceUrl;
580 | },
581 | initialize: function(props){
582 | this.instanceUrl = props;
583 | }
584 | });
585 |
586 | 我们需要在初始化的时候传入一个URL,以便在``getBlog``的时候用到,为了方便调试将url改为同路径的1.json文件
587 |
588 | getBlog: function() {
589 | var url = '/1.json';
590 | var that = this;
591 | collection = new BlogPostModel;
592 | collection.initialize(url);
593 | collection.fetch({
594 | success: function(collection, response){
595 | that.render(response);
596 | }
597 | });
598 | },
599 |
600 | 这样当成功获取数据的时候便render页面。最后的HomeView.js代码如下所示:
601 |
602 | define([
603 | 'jquery',
604 | 'underscore',
605 | 'mustache',
606 | 'text!/index.html',
607 | 'text!/blog.html'
608 | ], function($, _, Mustache, indexTemplate, blogTemplate) {
609 |
610 | var BlogPostModel = Backbone.Model.extend({
611 | name: 'Blog Posts',
612 | url: function(){
613 | return this.instanceUrl;
614 | },
615 | initialize: function(props){
616 | this.instanceUrl = props;
617 | }
618 | });
619 |
620 | var HomeView = Backbone.View.extend({
621 | el: $('#aboutArea'),
622 |
623 | initialize: function(){
624 | this.getBlog();
625 | },
626 |
627 | getBlog: function() {
628 | var url = '/1.json';
629 | var that = this;
630 | collection = new BlogPostModel;
631 | collection.initialize(url);
632 | collection.fetch({
633 | success: function(collection, response){
634 | that.render(response);
635 | }
636 | });
637 | },
638 |
639 | render: function(response) {
640 | this.$el.html(Mustache.to_html(blogTemplate, response));
641 | }
642 | });
643 |
644 | return HomeView;
645 | });
646 |
647 | 这样也就意味着我们需要在index.html中创建一个id为aboutArea的div。接着我们需要创建一个新的Template——blog.html,它的内容就比较简单了,只是简单的Mustache的使用。
648 |
649 | {{#.}}
650 | {{description}}
652 | {{/.}}
653 |
654 | ``{{#.}}``及``{{/.}}``可以用于JSON数组,即循环,也可以是判断是否存在。
655 |
656 | 最后的结果便是:
657 |
658 |
659 | 看到项目上的移动框架,网上寻找了一下,发现原来这些一开始都有。于是,找了个示例开始构建一个移动平台的CMS——墨颀 CMS,方便项目深入理解的同时,也可以自己维护一个CMS系统。
660 |
661 | 把description去掉,再修改一个CSS,便是我们在首页看到的结果。
662 |
663 | 下一次我们将打开这些URL。
664 |
665 | #### 如何查看是否支持JSON跨域请求
666 |
667 | 本次代码下载:[https://github.com/gmszone/moqi.mobi/archive/0.1.1.zip](https://github.com/gmszone/moqi.mobi/archive/0.1.1.zip)
668 |
669 | 一个简单的工具就是
670 |
671 | curl I -s http://example.com
672 |
673 | 在这里我们查看
674 |
675 | curl -I -s http://api.phodal.net/blog/page/1
676 |
677 | 应该要返回 Access-Control-Allow-Origin: *
678 |
679 |
680 | HTTP/1.1 200 OK
681 | Server: mokcy/0.17.0
682 | Date: Thu, 24 Jul 2014 00:38:19 GMT
683 | Content-Type: application/json; charset=utf-8
684 | Content-Length: 3943
685 | Connection: keep-alive
686 | Vary: Accept-Encoding
687 | Access-Control-Allow-Origin: *
688 | Access-Control-Allow-Headers: X-Requested-With
689 | Cache-Control: max-age=600
690 |
691 | 在有了上部分的基础之后,我们就可以生成一个博客的内容——BlogPosts Detail。这样就完成了我们这个[移动CMS](http://cms.moqi.mobi)的几乎主要的功能了,有了上节想必对于我们来说要获取一个文章已经不是一件难的事情了。
692 |
693 | #### 获取每篇博客
694 |
695 | 于是我们照猫画虎地写了一个``BlogDetail.js``
696 |
697 | define([
698 | 'jquery',
699 | 'underscore',
700 | 'mustache',
701 | 'text!/blog_details.html'
702 | ],function($, _, Mustache, blogDetailsTemplate){
703 |
704 | var BlogPostModel = Backbone.Model.extend({
705 | name: 'Blog Posts',
706 | url: function(){
707 | return this.instanceUrl;
708 | },
709 | initialize: function(props){
710 | this.instanceUrl = props;
711 | }
712 | });
713 |
714 | var BlogDetailView = Backbone.View.extend ({
715 | el: $("#content"),
716 |
717 | initialize: function () {
718 | },
719 |
720 | getBlog: function(slug) {
721 | url = "http://api.phodal.net/blog/" + slug;
722 | var that = this;
723 | collection = new BlogPostModel;
724 | collection.initialize(url);
725 | collection.fetch({
726 | success: function(collection, response){
727 | that.render(response);
728 | }
729 | });
730 | },
731 |
732 | render: function(response){
733 | this.$el.html(Mustache.to_html(blogDetailsTemplate, response));
734 | }
735 | });
736 |
737 | return BlogDetailView;
738 | });
739 |
740 | 又写了一个``blog_details.html``,然后,然后
741 |
742 |
758 |
759 | 我们显然需要稍微地修改一下之前``blog.html``的模板,为了让他可以在前台跳转
760 |
761 | {{#.}}
762 |
763 | {{description}}
764 | {{/.}}
765 |
766 | 问题出现了,我们怎样才能进入最后的页面?
767 |
768 | #### 添加博文的路由
769 |
770 | 在上一篇结束之后,每个博文都有对应的URL,即有对应的slug。而我们的博客的获取就是根据这个URL,获取的,换句话说,这些事情都是由API在做的。这里所要做的便是,获取博客的内容,再render。这其中又有一个问题是ajax执行的数据无法从外部取出,于是就有了上面的getBlog()调用render的方法。
771 |
772 | 我们需要传进一个参数,以便告诉BlogDetail需要获取哪一篇博文。
773 |
774 | routes: {
775 | 'index': 'homePage',
776 | 'blog/*slug': 'blog',
777 | '*actions': 'homePage'
778 | }
779 |
780 | ``*slug``便是这里的参数的内容,接着我们需要调用getBlog(slug)对其进行处理。
781 |
782 | app_router.on('route:blog', function(blogSlug){
783 | var blogDetailsView = new BlogDetail();
784 | blogDetailsView.getBlog(blogSlug);
785 | });
786 |
787 | 最后,我们的``router.js``的内容如下所示:
788 |
789 | define([
790 | 'jquery',
791 | 'underscore',
792 | 'backbone',
793 | 'HomeView',
794 | 'BlogDetail'
795 | ], function($, _, Backbone, HomeView, BlogDetail) {
796 |
797 | var AppRouter = Backbone.Router.extend({
798 | routes: {
799 | 'index': 'homePage',
800 | 'blog/*slug': 'blog',
801 | '*actions': 'homePage'
802 | }
803 | });
804 | var initialize = function() {
805 | var app_router = new AppRouter;
806 |
807 | app_router.on('route:homePage', function() {
808 | var homeView = new HomeView();
809 | homeView.render();
810 | });
811 |
812 | app_router.on('route:blog', function(blogSlug){
813 | var blogDetailsView = new BlogDetail();
814 | blogDetailsView.getBlog(blogSlug);
815 | });
816 |
817 | Backbone.history.start();
818 | };
819 | return {
820 | initialize: initialize
821 | };
822 | });
823 |
824 | 接着我们便可以很愉快地打开每一篇博客查看里面的内容了。
825 |
826 | 当前[墨颀CMS](http://cms.moqi.mobi/)的一些基础功能设计已经接近尾声了,在完成博客的前两部分之后,我们需要对此进行一个简单的重构。为的是提取出其中的获取Blog内容的逻辑,于是经过一番努力之后,终于有了点小成果。
827 |
828 | ### Step 7: 重构
829 |
830 | 我们想要的结果,便是可以直接初始化及渲染,即如下的结果:
831 |
832 | initialize: function(){
833 | this.getBlog();
834 | },
835 |
836 | render: function(response){
837 | var about = {
838 | about:aboutCMS,
839 | aboutcompany:urlConfig["aboutcompany"]
840 | };
841 | response.push(about);
842 | this.$el.html(Mustache.to_html(blogPostsTemplate, response));
843 | }
844 |
845 | 为的便是简化其中的逻辑,将与View无关的部分提取出来,最后的结果便是都放在初始化里,显然我们需要一个``render``,只是暂时放在``initialize``应该就够了。下面便是最后的结果:
846 |
847 | initialize: function(){
848 | var params='#content';
849 | var about = {
850 | about:aboutCMS,
851 | aboutcompany:configure["aboutcompany"]
852 | };
853 | var blogView = new RenderBlog(params, '/1.json', blogPostsTemplate);
854 | blogView.renderBlog(about);
855 | }
856 |
857 | 我们只需要将id、url、template传进去,便可以返回结果,再用getBlog部分传进参数。再渲染结果,这样我们就可以提取出两个不同View里面的相同的部分。
858 |
859 | #### 构建函数
860 |
861 | 于是,我们就需要构建一个函数RenderBlog,只需要将id,url,template等传进去就可以了。
862 |
863 | var RenderBlog = function (params, url, template) {
864 | this.params = params;
865 | this.url = url;
866 | this.template = template;
867 | };
868 |
869 | 用Javascript的原型继承就可以实现这样的功能,虽然还不是很熟练,但是还是勉强用了上来。
870 |
871 | RenderBlog.prototype.renderBlog = function(addInfo) {
872 | var template = this.template;
873 | var params = this.params;
874 | var url = this.url;
875 | var collection = new BlogPostModel;
876 |
877 | collection.initialize(url);
878 | collection.fetch({
879 | success: function(collection, response){
880 | if(addInfo !== undefined){
881 | response.push(addInfo);
882 | }
883 | RenderBlog.prototype.render(params, template, response);
884 | }
885 | });
886 | };
887 |
888 | RenderBlog.prototype.render = function(params, template, response) {
889 | $(params).html(Mustache.to_html(template, response));
890 | };
891 |
892 | 大致便是将原来的函数中的功能抽取出来,再调用自己的方法。于是就这样可以继续进行下一步了,只是暂时没有一个明确的方向。
893 |
894 | 在和几个有兴趣做**移动CMS**的小伙伴讨论了一番之后,我们觉得当前比较重要的便是统一一下RESTful API。然而最近持续断网中,又遭遇了一次停电,暂停了对API的思考。在周末无聊的时光了看了《人间失格》,又看了会《一个人流浪,不必去远方》。开始思考所谓的技术以外的事情,或许这将是下一篇讨论的话题。
895 |
896 | 正在我对这个[移动CMS](http://cms.moqi.mobi/)的功能一筹莫展的时候,帮小伙伴在做一个图片滑动的时候,便想着将这个功能加进去,很顺利地找到了一个库。
897 |
898 | ### Step 8: 移动CMS滑动
899 |
900 | 我们所需要的两个功能很简单
901 |
902 | - 当用户向右滑动的时候,菜单应该展开
903 | - 当用户向左滑动的时候,菜单应该关闭
904 |
905 | 在官网看到了一个简单的示例,然而并不是用于这个菜单,等到我完成之后我才知道:为什么不用于菜单?
906 |
907 | 找到了这样一个符合功能的库,虽然知道要写这个功能也不难。相比于自己写这个库,还不如用别人维护了一些时候的库来得简单、稳定。
908 |
909 | > jQuery Plugin to obtain touch gestures from iPhone, iPod Touch and iPad, should also work with Android mobile phones (not tested yet!)
910 |
911 | 然而,它并不会其他一些设备上工作。
912 |
913 | **添加jQuery Touchwipe**
914 |
915 | 添加到requirejs的配置中:
916 |
917 | require.config({
918 | baseUrl: 'lib/',
919 | paths: {
920 | jquery: 'jquery-2.1.1.min',
921 | router: '../router',
922 | touchwipe: 'jquery.touchwipe.min'
923 | },
924 | shim: {
925 | touchwipe: ["jquery"],
926 | underscore: {
927 | exports: '_'
928 | }
929 | }
930 | });
931 |
932 | require(['../app'], function(App){
933 | App.initialize();
934 | });
935 |
936 | (注:上面的代码中暂时去掉了一部分无关本文的,为了简单描述。)
937 |
938 | 接着,添加下面的代码添加到app.js的初始化方法中
939 |
940 | $(window).touchwipe({
941 | wipeLeft: function() {
942 | $.sidr('close');
943 | },
944 | wipeRight: function() {
945 | $.sidr('open');
946 | },
947 | preventDefaultEvents: false
948 | });
949 |
950 | 就变成了我们需要的代码。。
951 |
952 | define([
953 | 'jquery',
954 | 'underscore',
955 | 'backbone',
956 | 'router',
957 | 'jquerySidr',
958 | 'touchwipe'
959 | ], function($, _, Backbone, Router){
960 |
961 | var initialize = function(){
962 | $(window).touchwipe({
963 | wipeLeft: function() {
964 | $.sidr('close');
965 | },
966 | wipeRight: function() {
967 | $.sidr('open');
968 | },
969 | preventDefaultEvents: false
970 | });
971 | $(document).ready(function() {
972 | $('#sidr').show();
973 | $('#menu').sidr();
974 | $("#sidr li a" ).bind('touchstart click', function() {
975 | if(null != Backbone.history.fragment){
976 | _.each($("#sidr li"),function(li){
977 | $(li).removeClass()
978 | });
979 |
980 | $('a[href$="#/'+Backbone.history.fragment+'"]').parent().addClass("active");
981 | $.sidr('close');
982 | window.scrollTo(0,0);
983 | }
984 | });
985 | });
986 | Router.initialize();
987 | };
988 |
989 | return {
990 | initialize: initialize
991 | };
992 | });
993 |
994 | 便可以实现我们需要的
995 |
996 | - 当用户向右滑动的时候,菜单应该展开
997 | - 当用户向左滑动的时候,菜单应该关闭
998 |
--------------------------------------------------------------------------------
/chapters/oculus-three.md:
--------------------------------------------------------------------------------
1 | Oculus + Node.js + Three.js 打造VR世界
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 在尝试一个晚上的开发环境搭建后,我放弃了开发原生应用的想法。一是没有属于自己的电脑(如果Raspberry Pi II不算的话)——没有Windows、没有GNU/Linux,二是公司配的电脑是Mac OS。对于嵌入式开发和游戏开发来说,Mac OS简直是手机中的Windows Phone——坑爹的LLVM、GCC(Mac OS )、OpenGL、OGLPlus、C++11。并且官方对Mac OS和Linux的SDK的支持已经落后了好几个世纪。
10 |
11 | 说到底,还是Web的开发环境到底还是比较容易搭建的。
12 |
13 | ### Showcase
14 |
15 | 这个repo的最后效果图如下所示:
16 |
17 | 
18 |
19 | 
20 |
21 | 效果:
22 |
23 | 1. WASD控制前进、后退等等。
24 | 2. 旋转头部 = 真实的世界。
25 | 3. 附加效果: 看久了头晕。
26 |
27 | ### 框架: Oculus Rift & Node NMD
28 |
29 | > Oculus Rift 是一款为电子游戏设计的头戴式显示器。这是一款虚拟现实设备。这款设备很可能改变未来人们游戏的方式。
30 |
31 | 步骤
32 | ---
33 |
34 | 现在,让我们开始构建吧。
35 |
36 | ### Step 1: Node Oculus Services
37 |
38 | 这里,我们所要做的事情便是将传感器返回来的四元数(Quaternions)与欧拉角(Euler angles)以API的形式返回到前端。
39 |
40 | #### 安装Node NMD
41 |
42 | Node.js上有一个Oculus的插件名为node-hmd,hmd即面向头戴式显示器。它就是Oculus SDK的Node接口,虽说年代已经有些久远了,但是似乎是可以用的——官方针对 Mac OS和Linux的SDK也已经很久没有更新了。
43 |
44 | 在GNU/Linux系统下,你需要安装下面的这些东西的
45 |
46 | ```
47 | freeglut3-dev
48 | mesa-common-dev
49 | libudev-dev
50 | libxext-dev
51 | libxinerama-dev
52 | libxrandr-dev
53 | libxxf86vm-dev
54 | ```
55 |
56 | Mac OS如果安装失败,请使用Clang来,以及GCC的C标准库(PS: 就是 Clang + GCC的混合体,它们之间就是各种复杂的关系。。):
57 |
58 | ```
59 | export CXXFLAGS=-stdlib=libstdc++
60 |
61 | export CC=/usr/bin/clang
62 | export CXX=/usr/bin/clang++
63 | ```
64 |
65 | (PS: 我使用的是Mac OS El Captian + Xcode 7.0. 2)clang版本如下:
66 |
67 | ```
68 | Apple LLVM version 7.0.2 (clang-700.1.81)
69 | Target: x86_64-apple-darwin15.0.0
70 | Thread model: posix
71 | ```
72 |
73 | 反正都是会报错的:
74 |
75 | ```
76 | ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Service/Service_NetClient.o) was built for newer OSX version (10.7) than being linked (10.5)
77 | ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Tracking/Tracking_SensorStateReader.o) was built for newer OSX version (10.7) than being linked (10.5)
78 | ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_ImageWindow.o) was built for newer OSX version (10.7) than being linked (10.5)
79 | ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_Interface.o) was built for newer OSX version (10.7) than being linked (10.5)
80 | ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_LatencyTest2Reader.o) was built for newer OSX version (10.7) than being linked (10.5)
81 | ld: warning: object file (Release/obj.target/hmd/src/platform/mac/LibOVR/Src/Util/Util_Render_Stereo.o) was built for newer OSX version (10.7) than being linked (10.5)
82 | node-hmd@0.2.1 node_modules/node-hmd
83 | ```
84 |
85 | 不过,有最后一行就够了。
86 |
87 | ### Step 2: Node.js Oculus Hello,World
88 |
89 | 现在,我们就可以写一个Hello,World了,直接来官方的示例~~。
90 |
91 | ```javascript
92 | var hmd = require('node-hmd');
93 |
94 | var manager = hmd.createManager("oculusrift");
95 |
96 | manager.getDeviceInfo(function(err, deviceInfo) {
97 | if(!err) {
98 | console.log(deviceInfo);
99 | }
100 | else {
101 | console.error("Unable to retrieve device information.");
102 | }
103 | });
104 |
105 | manager.getDeviceOrientation(function(err, deviceOrientation) {
106 | if(!err) {
107 | console.log(deviceOrientation);
108 | }
109 | else {
110 | console.error("Unable to retrieve device orientation.");
111 | }
112 | });
113 | ```
114 |
115 | 运行之前,记得先连上你的Oculus。会有类似于下面的结果:
116 |
117 | ```javascript
118 | { CameraFrustumFarZInMeters: 2.5,
119 | CameraFrustumHFovInRadians: 1.29154372215271,
120 | CameraFrustumNearZInMeters: 0.4000000059604645,
121 | CameraFrustumVFovInRadians: 0.942477822303772,
122 | DefaultEyeFov:
123 | [ { RightTan: 1.0923680067062378,
124 | LeftTan: 1.0586576461791992,
125 | DownTan: 1.3292863368988037,
126 | UpTan: 1.3292863368988037 },
127 | { RightTan: 1.0586576461791992,
128 | LeftTan: 1.0923680067062378,
129 | DownTan: 1.3292863368988037,
130 | UpTan: 1.3292863368988037 } ],
131 | DisplayDeviceName: '',
132 | DisplayId: 880804035,
133 | DistortionCaps: 66027,
134 | EyeRenderOrder: [ 1, 0 ],
135 | ...
136 | ```
137 |
138 | 接着,我们就可以实时返回这些数据了。
139 |
140 | ### Step 3: Node Oculus WebSocket
141 |
142 | 在网上看到[http://laht.info/WebGL/DK2Demo.html](http://laht.info/WebGL/DK2Demo.html)这个虚拟现实的电影,并且发现了它有一个WebSocket,然而是Java写的,只能拿来当参考代码。
143 |
144 | 现在我们就可以写一个这样的Web Services,用的仍然是Express + Node.js + WS。
145 |
146 | ```javascript
147 | var hmd = require("node-hmd"),
148 | express = require("express"),
149 | http = require("http").createServer(),
150 | WebSocketServer = require('ws').Server,
151 | path = require('path');
152 |
153 | // Create HMD manager object
154 | console.info("Attempting to load node-hmd driver: oculusrift");
155 | var manager = hmd.createManager("oculusrift");
156 | if (typeof(manager) === "undefined") {
157 | console.error("Unable to load driver: oculusrift");
158 | process.exit(1);
159 | }
160 | // Instantiate express server
161 | var app = express();
162 | app.set('port', process.env.PORT || 3000);
163 |
164 | app.use(express.static(path.join(__dirname + '/', 'public')));
165 | app.set('views', path.join(__dirname + '/public/', 'views'));
166 | app.set('view engine', 'jade');
167 |
168 | app.get('/demo', function (req, res) {
169 | 'use strict';
170 | res.render('demo', {
171 | title: 'Home'
172 | });
173 | });
174 |
175 | // Attach socket.io listener to the server
176 | var wss = new WebSocketServer({server: http});
177 | var id = 1;
178 |
179 | wss.on('open', function open() {
180 | console.log('connected');
181 | });
182 |
183 | // On socket connection set up event emitters to automatically push the HMD orientation data
184 | wss.on("connection", function (ws) {
185 | function emitOrientation() {
186 | id = id + 1;
187 | var deviceQuat = manager.getDeviceQuatSync();
188 | var devicePosition = manager.getDevicePositionSync();
189 |
190 | var data = JSON.stringify({
191 | id: id,
192 | quat: deviceQuat,
193 | position: devicePosition
194 | });
195 |
196 | ws.send(data, function (error) {
197 | //it's a bug of websocket, see in https://github.com/websockets/ws/issues/337
198 | });
199 | }
200 |
201 | var orientation = setInterval(emitOrientation, 1000);
202 |
203 | ws.on("message", function (data) {
204 | clearInterval(orientation);
205 | orientation = setInterval(emitOrientation, data);
206 | });
207 |
208 | ws.on("close", function () {
209 | setTimeout(null, 500);
210 | clearInterval(orientation);
211 | console.log("disconnect");
212 | });
213 | });
214 |
215 | // Launch express server
216 | http.on('request', app);
217 | http.listen(3000, function () {
218 | console.log("Express server listening on port 3000");
219 | });
220 | ```
221 |
222 |
223 | 总之,就是连上的时候不断地发现设备的数据:
224 |
225 | ```javascript
226 | var data = JSON.stringify({
227 | id: id,
228 | quat: deviceQuat,
229 | position: devicePosition
230 | });
231 |
232 | ws.send(data, function (error) {
233 | //it's a bug of websocket, see in https://github.com/websockets/ws/issues/337
234 | });
235 | ```
236 |
237 | 上面有一行注释是我之前一直遇到的一个坑,总之需要callback就是了。
238 |
239 | ### Step 4: Oculus Effect + DK2 Control
240 |
241 | 在之前的版本中,Three.js都提供了Oculus的Demo,当然只能用来看。并且交互的接口是HTTP,感觉很难玩~~。
242 |
243 | **Three.js DK2Controls**
244 |
245 | 这时,我们就需要根据上面传过来的``四元数``(Quaternions)与欧拉角(Euler angles)来作相应的处理。
246 |
247 | ```javascript
248 | {
249 | "position": {
250 | "x": 0.020077044144272804,
251 | "y": -0.0040545957162976265,
252 | "z": 0.16216422617435455
253 | },
254 | "quat": {
255 | "w": 0.10187230259180069,
256 | "x": -0.02359195239841938,
257 | "y": -0.99427556991577148,
258 | "z": -0.021934293210506439
259 | }
260 | }
261 | ```
262 |
263 | **欧拉角与四元数**
264 |
265 | (ps: 如果没copy好,麻烦提出正确的说法,原谅我这个挂过高数的人。我只在高中的时候,看到这些资料。)
266 |
267 | > 欧拉角是一组用于描述刚体姿态的角度,欧拉提出,刚体在三维欧氏空间中的任意朝向可以由绕三个轴的转动复合生成。通常情况下,三个轴是相互正交的。
268 |
269 | 对应的三个角度又分别成为roll(横滚角),pitch(俯仰角)和yaw(偏航角),就是上面的postion里面的三个值。。
270 |
271 | ```
272 | roll = (rotation about Z);
273 |
274 | pitch = (rotation about (Roll • Y));
275 |
276 | yaw = (rotation about (Pitch • Raw • Z));”
277 | ```
278 |
279 | -- 引自《Oculus Rift In Action》
280 |
281 | 转换成代码。。
282 |
283 | ```
284 | this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10);
285 | ```
286 |
287 | > 四元数是由爱尔兰数学家威廉·卢云·哈密顿在1843年发现的数学概念。
288 |
289 | 从明确地角度而言,四元数是复数的不可交换延伸。如把四元数的集合考虑成多维实数空间的话,四元数就代表着一个四维空间,相对于复数为二维空间。
290 |
291 | 反正就是用于``描述三维空间的旋转变换``。
292 |
293 | 结合下代码:
294 |
295 | ```javascript
296 | this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10);
297 | this.headQuat.set(sensorData.quat.x, sensorData.quat.y, sensorData.quat.z, sensorData.quat.w);
298 |
299 | this.camera.setRotationFromQuaternion(this.headQuat);
300 | this.controller.setRotationFromMatrix(this.camera.matrix);
301 | ```
302 |
303 | 就是,我们需要设置camera和controller的旋转。
304 |
305 | 这使我有足够的理由相信Oculus就是一个手机 + 一个6轴运动处理组件的升级板——因为,我玩过MPU6050这样的传感器,如图。。。
306 |
307 | 
308 |
309 |
310 | **Three.js DK2Controls**
311 |
312 | 虽然下面的代码不是我写的,但是还是简单地说一下。
313 |
314 | ```javascript
315 | /*
316 | Copyright 2014 Lars Ivar Hatledal
317 | Licensed under the Apache License, Version 2.0 (the "License");
318 | you may not use this file except in compliance with the License.
319 | You may obtain a copy of the License at
320 | http://www.apache.org/licenses/LICENSE-2.0
321 | Unless required by applicable law or agreed to in writing, software
322 | distributed under the License is distributed on an "AS IS" BASIS,
323 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
324 | See the License for the specific language governing permissions and
325 | limitations under the License.
326 | */
327 |
328 | THREE.DK2Controls = function (camera) {
329 |
330 | this.camera = camera;
331 | this.ws;
332 | this.sensorData;
333 | this.lastId = -1;
334 |
335 | this.controller = new THREE.Object3D();
336 |
337 | this.headPos = new THREE.Vector3();
338 | this.headQuat = new THREE.Quaternion();
339 |
340 | var that = this;
341 | var ws = new WebSocket("ws://localhost:3000/");
342 | ws.onopen = function () {
343 | console.log("### Connected ####");
344 | };
345 |
346 | ws.onmessage = function (evt) {
347 | var message = evt.data;
348 | try {
349 | that.sensorData = JSON.parse(message);
350 | } catch (err) {
351 | console.log(message);
352 | }
353 | };
354 |
355 | ws.onclose = function () {
356 | console.log("### Closed ####");
357 | };
358 |
359 | this.update = function () {
360 |
361 | var sensorData = this.sensorData;
362 | if (sensorData) {
363 | var id = sensorData.id;
364 | if (id > this.lastId) {
365 | this.headPos.set(sensorData.position.x * 10 - 0.4, sensorData.position.y * 10 + 1.75, sensorData.position.z * 10 + 10);
366 | this.headQuat.set(sensorData.quat.x, sensorData.quat.y, sensorData.quat.z, sensorData.quat.w);
367 |
368 | this.camera.setRotationFromQuaternion(this.headQuat);
369 | this.controller.setRotationFromMatrix(this.camera.matrix);
370 | }
371 | this.lastId = id;
372 | }
373 |
374 |
375 | this.camera.position.addVectors(this.controller.position, this.headPos);
376 | if (this.camera.position.y < -10) {
377 | this.camera.position.y = -10;
378 | }
379 |
380 | if (ws) {
381 | if (ws.readyState === 1) {
382 | ws.send("get\n");
383 | }
384 | }
385 |
386 | };
387 | };
388 | ```
389 |
390 | 打开WebSocket的时候,不断地获取最新的传感器状态,然后update。谁在调用update方法?Three.js
391 |
392 | 我们需要在我们的初始化代码里初始化我们的control:
393 |
394 | ```javascript
395 | var oculusControl;
396 |
397 | function init() {
398 | ...
399 | oculusControl = new THREE.DK2Controls( camera );
400 | ...
401 | }
402 | ```
403 |
404 | 并且不断地调用update方法。
405 |
406 | ```javascript
407 | function animate() {
408 | requestAnimationFrame( animate );
409 | render();
410 | stats.update();
411 | }
412 | function render() {
413 | oculusControl.update( clock.getDelta() );
414 | THREE.AnimationHandler.update( clock.getDelta() * 100 );
415 |
416 | camera.useQuaternion = true;
417 | camera.matrixWorldNeedsUpdate = true;
418 |
419 | effect.render(scene, camera);
420 | }
421 | ```
422 |
423 | 最后,添加相应的KeyHandler就好了~~。
424 |
425 | ### Step 5: Three.js KeyHandler
426 |
427 | KeyHandler对于习惯了Web开发的人来说就比较简单了:
428 |
429 | ```javascript
430 | this.onKeyDown = function (event) {
431 | switch (event.keyCode) {
432 | case 87: //W
433 | this.wasd.up = true;
434 | break;
435 | case 83: //S
436 | this.wasd.down = true;
437 | break;
438 | case 68: //D
439 | this.wasd.right = true;
440 | break;
441 | case 65: //A
442 | this.wasd.left = true;
443 | break;
444 | }
445 | };
446 |
447 | this.onKeyUp = function (event) {
448 | switch (event.keyCode) {
449 | case 87: //W
450 | this.wasd.up = false;
451 | break;
452 | case 83: //S
453 | this.wasd.down = false;
454 | break;
455 | case 68: //D
456 | this.wasd.right = false;
457 | break;
458 | case 65: //A
459 | this.wasd.left = false;
460 | break;
461 | }
462 | };
463 | ```
464 |
465 | 然后就是万恶的if语句了:
466 |
467 | ```javascript
468 | if (this.wasd.up) {
469 | this.controller.translateZ(-this.translationSpeed * delta);
470 | }
471 |
472 | if (this.wasd.down) {
473 | this.controller.translateZ(this.translationSpeed * delta);
474 | }
475 |
476 | if (this.wasd.right) {
477 | this.controller.translateX(this.translationSpeed * delta);
478 | }
479 |
480 | if (this.wasd.left) {
481 | this.controller.translateX(-this.translationSpeed * delta);
482 | }
483 |
484 | this.camera.position.addVectors(this.controller.position, this.headPos);
485 |
486 | if (this.camera.position.y < -10) {
487 | this.camera.position.y = -10;
488 | }
489 | ```
490 |
491 | 快接上你的HMD试试吧~~
492 |
493 | ###练习建议
494 |
495 |
--------------------------------------------------------------------------------
/chapters/onmap.md:
--------------------------------------------------------------------------------
1 | 手动制作照片地图
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### Background:把照片放在地图上
8 |
9 | 我使用的Nokia Lumia 920没有一个好的照片应用,而我当时也没有钱换一个新手机
10 |
11 | ### Showcase
12 |
13 | 
14 |
15 | ###框架: EXIF & ExifRead & CartoDB
16 |
17 | **EXIF**
18 |
19 | > 可交换图像文件常被简称为EXIF(Exchangeable image file format),是专门为数码相机的照片设定的,可以记录数码照片的属性信息和拍摄数据。
20 |
21 | EXIF信息以0xFFE1作为开头标记,后两个字节表示EXIF信息的长度。所以EXIF信息最大为64 kB,而内部采用TIFF格式。
22 |
23 | **ExifRead**
24 |
25 | 来自官方的简述
26 |
27 | > **Python library to extract EXIF data from tiff and jpeg files.**
28 |
29 | **ExifRead安装**
30 |
31 | ```bash
32 | pip install exifread
33 | ```
34 |
35 | **ExifRead Exif.py**
36 |
37 | 官方写了一个exif.py的command可直接查看照片信息
38 |
39 | ```bash
40 | EXIF.py images.jpg
41 | ```
42 |
43 |
44 | **CartoDB**
45 |
46 | > Create dynamic maps, analyze and build location aware and geospatial applications with your data using the power using the power of PostGIS in the cloud.
47 |
48 | 步骤
49 | ---
50 |
51 | ### Step 1: 解析读取照片信息
52 |
53 | 简单的来说,就是我们可以创建包含位置信息的内容到上面去。
54 |
55 | 主要步骤如下:
56 |
57 | - 需要遍历自己的全部图片文件
58 | - 解析照片信息
59 | - 生成地理信息文件
60 | - 上传到cartodb
61 |
62 | **python 遍历文件**
63 |
64 | 代码如下,来自于《python cookbook》
65 |
66 | ```python
67 | import os, fnmatch
68 | def all_files(root, patterns='*', single_level=False, yield_folders=False):
69 | patterns = patterns.split(';')
70 | for path, subdirs, files in os.walk(root):
71 | if yield_folders:
72 | files.extend(subdirs)
73 | files.sort()
74 | for name in files:
75 | for pattern in patterns:
76 | if fnmatch.fnmatch(name, pattern):
77 | yield os.path.join(path, name)
78 | break
79 | if single_level:
80 | break
81 | ```
82 |
83 | **python 解析照片信息**
84 |
85 | 由于直接从照片中提取的信息是
86 |
87 | ```
88 | [34, 12, 51513/1000]
89 | ```
90 |
91 | 也就是
92 |
93 | ```
94 | N 34� 13' 12.718
95 | ```
96 |
97 | 几度几分几秒的形式,我们需要转换为
98 |
99 | ```
100 | 34.2143091667
101 | ```
102 |
103 | 具体的大致就是
104 |
105 | ```python
106 | def parse_gps(titude):
107 | first_number = titude.split(',')[0]
108 | second_number = titude.split(',')[1]
109 | third_number = titude.split(',')[2]
110 | third_number_parent = third_number.split('/')[0]
111 | third_number_child = third_number.split('/')[1]
112 | third_number_result = float(third_number_parent) / float(third_number_child)
113 | return float(first_number) + float(second_number)/60 + third_number_result/3600
114 | ```
115 |
116 | 也就是我们需要将second/60,还有minutes/3600。
117 |
118 | **python 提取照片信息生成文件**
119 |
120 | ```python
121 | import json
122 | import exifread
123 | import os, fnmatch
124 | from exifread.tags import DEFAULT_STOP_TAG, FIELD_TYPES
125 | from exifread import process_file, __version__
126 |
127 | def all_files(root, patterns='*', single_level=False, yield_folders=False):
128 | patterns = patterns.split(';')
129 | for path, subdirs, files in os.walk(root):
130 | if yield_folders:
131 | files.extend(subdirs)
132 | files.sort()
133 | for name in files:
134 | for pattern in patterns:
135 | if fnmatch.fnmatch(name, pattern):
136 | yield os.path.join(path, name)
137 | break
138 | if single_level:
139 | break
140 |
141 | def parse_gps(titude):
142 | first_number = titude.split(',')[0]
143 | second_number = titude.split(',')[1]
144 | third_number = titude.split(',')[2]
145 | third_number_parent = third_number.split('/')[0]
146 | third_number_child = third_number.split('/')[1]
147 | third_number_result = float(third_number_parent) / float(third_number_child)
148 | return float(first_number) + float(second_number)/60 + third_number_result/3600
149 |
150 | jsonFile = open("gps.geojson", "w")
151 | jsonFile.writelines('{\n"type": "FeatureCollection","features": [\n')
152 |
153 | def write_data(paths):
154 | index = 1
155 | for path in all_files('./' + paths, '*.jpg'):
156 | f = open(path[2:], 'rb')
157 | tags = exifread.process_file(f)
158 | # jsonFile.writelines('"type": "Feature","properties": {"cartodb_id":"'+str(index)+'"},"geometry": {"type": "Point","coordinates": [')
159 | latitude = tags['GPS GPSLatitude'].printable[1:-1]
160 | longitude = tags['GPS GPSLongitude'].printable[1:-1]
161 | print latitude
162 | print parse_gps(latitude)
163 | # print tags['GPS GPSLongitudeRef']
164 | # print tags['GPS GPSLatitudeRef']
165 | jsonFile.writelines('{"type": "Feature","properties": {"cartodb_id":"' + str(index) + '"')
166 | jsonFile.writelines(',"OS":"' + str(tags['Image Software']) + '","Model":"' + str(tags['Image Model']) + '","Picture":"'+str(path[7:])+'"')
167 | jsonFile.writelines('},"geometry": {"type": "Point","coordinates": [' + str(parse_gps(longitude)) + ',' + str(
168 | parse_gps(latitude)) + ']}},\n')
169 | index += 1
170 |
171 | write_data('imgs')
172 |
173 | jsonFile.writelines(']}\n')
174 | jsonFile.close()
175 | ```
176 |
177 | ### Step 2: 上传数据
178 |
179 | 注册CartoDB,然后上传数据。
180 |
181 | ### 练习建议
182 |
183 | 无
184 |
185 |
186 |
--------------------------------------------------------------------------------
/chapters/sherlock.md:
--------------------------------------------------------------------------------
1 | D3.js打造技能树
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 在开始设计新的技能树——[Sherlock](https://github.com/phodal/sherlock)的同时,结合一下原有的技能树,说说如何去设计,新的技能树还很丑。
10 |
11 | ### Showcase
12 |
13 | 代码见: [https://github.com/phodal/sherlock](https://github.com/phodal/sherlock)
14 |
15 | 
16 |
17 | ###Graphviz
18 |
19 | > Graphviz (英文:Graph Visualization Software的缩写)是一个由AT&T实验室启动的开源工具包,用于绘制DOT语言脚本描述的图形。它也提供了供其它软件使用的库。Graphviz是一个自由软件,其授权为Eclipse Public License。其Mac版本曾经获得2004年的苹果设计奖。
20 |
21 | 一个简单的示例代码如下:
22 |
23 | ```dot
24 | graph example1 {
25 | Server1 -- Server2
26 | Server2 -- Server3
27 | Server3 -- Server1
28 | }
29 | ```
30 |
31 | 执行编译后:
32 |
33 | ```bash
34 | dot -Tjpg lz.dot -o lz.jpg
35 | ```
36 |
37 | 就会生成下面的图片
38 |
39 | 
40 |
41 | 接着我们便可以建立一个简单的模型来构建我们的技能树。
42 |
43 | 步骤
44 | ---
45 |
46 | ### Step 1: 打造简单的技能树
47 |
48 | 先以JavaScript全栈作一个简单的示例,他们可能存在下面的依赖关系:
49 |
50 |
51 | - "JavaScript" -> "Web前端"
52 | - "HTML" -> "Web前端"
53 | - "CSS" -> "Web前端"
54 | - "Web前端" -> "Web开发"
55 | - "JavaScript" -> "Node.js" -> "Web服务端"
56 | - "SQL/NoSQL" -> "Web服务端"
57 | - "Web Server-Side" -> "Web开发"
58 |
59 |
60 | 即Web前端依赖于JavaScript、HTML、CSS,而Node.js依赖于JavaScript,当然我们也需要数据的支持,大部分的网站都是数据驱动型的开发。而构成完成的开发链的则是前端 + 服务端。
61 |
62 | 于是我们有了这张图:
63 |
64 | 
65 |
66 | 而我们的代码是这样的:
67 |
68 | ```dot
69 | digraph tree
70 | {
71 | nodesep=0.5;
72 | charset="UTF-8";
73 | rankdir=LR;
74 | fixedsize=true;
75 | node [style="rounded,filled", width=0, height=0, shape=box, fillcolor="#E5E5E5", concentrate=true]
76 | "JavaScript" ->"Web前端"
77 | "HTML" -> "Web前端"
78 | "CSS" -> "Web前端"
79 | "Web前端" -> "Web开发"
80 | "JavaScript" -> "Node.js" -> "Web服务端"
81 | "SQL/NoSQL" -> "Web服务端"
82 | "Web服务端" -> "Web开发"
83 | }
84 | ```
85 |
86 | 上面举出的是一个简单的例子,对应的我们可以做一些更有意思的东西,比如将dot放到Web上,详情见下一篇。
87 |
88 | ### Step 3: D3.js Tooltipster
89 |
90 | 使用D3.js与Darge-d3构建一个简单的技能树的时候,需要一个简单的类似于小贴士的插件。
91 |
92 | 
93 |
94 | #### Tooltipster
95 |
96 | Tooltipster是一个jQuery tooltip 插件,兼容Mozilla Firefox, Google Chrome, IE8+。
97 |
98 | 简单示例``html``:
99 |
100 | ```html
101 |
104 | ```
105 |
106 | 简单示例``js`:
107 |
108 | ```javascript
109 | $(document).ready(function() {
110 | $('.tooltip').tooltipster();
111 | });
112 | ```
113 |
114 | D3.js、Tooltipster与Requirejs的配置如下所示:
115 |
116 | ```javascript
117 | require.config({
118 | baseUrl: 'app',
119 | paths: {
120 | jquery: 'lib/jquery-2.1.3',
121 | d3: 'lib/d3.min',
122 | text: 'lib/text',
123 | 'jquery.tooltipster': 'lib/jquery.tooltipster.min'
124 | },
125 | 'shim': {
126 | 'jquery.tooltipster': {
127 | deps: ['jquery']
128 | }
129 | }
130 | });
131 | ```
132 |
133 | #### 整合代码
134 |
135 | 最后代码如下所示:
136 |
137 | ```javascript
138 | inner.selectAll('g.node')
139 | .each(function (v, id) {
140 | g.node(v).books = Utils.handleEmptyDocs(g.node(v).books);
141 | g.node(v).links = Utils.handleEmptyDocs(g.node(v).links);
142 |
143 | var data = {
144 | id: id,
145 | name: v,
146 | description: g.node(v).description,
147 | books: g.node(v).books,
148 | links: g.node(v).links
149 | };
150 | var results = lettuce.Template.tmpl(description_template, data);
151 |
152 | $(this).tooltipster({
153 | content: $(results),
154 | contentAsHTML: true,
155 | position: 'left',
156 | animation: 'grow',
157 | interactive: true});
158 | $(this).find('rect').css('fill', '#ecf0f1');
159 | });
160 | ```
161 |
162 |
--------------------------------------------------------------------------------
/chapters/tech-stack.md:
--------------------------------------------------------------------------------
1 | 技术雷达趋势
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ###背景
8 |
9 | 出于一些原因,我需要构建一个项目组相关的技术趋势图。首先也是想到了[ThoughtWorks 技术雷达](https://www.thoughtworks.com/cn/radar),然而我也发现了技术雷达只会发现一些新出现的技术,以及其对应的一些趋势。对于现有的技术栈的一些趋势不够明显,接着就只能去构建一个新的技术趋势图。
10 |
11 | 当然首选的框架也是D3.js,似乎会一些更好的工具,但是并不没有去尝试。
12 |
13 |
14 | ### Showcase
15 |
16 | 在线预览: [http://phodal.github.io/techstack](http://phodal.github.io/techstack)
17 |
18 | 最后的效果如下图:
19 |
20 | 
21 |
22 | ### D3.js
23 |
24 | 步骤
25 | ---
26 |
27 | ### Step 1: Schema与原始代码
28 |
29 | 最开始的代码是基于[https://github.com/simonellistonball/techradar](https://github.com/simonellistonball/techradar)这个库的,但是这其中的数据都是写好的。而在找到这个库之前,我也定义好了我的数据应该有的样子:
30 |
31 | ```javascript
32 | {
33 | "name": "Java",
34 | "important": 5,
35 | "usage": 5,
36 | "current": 4,
37 | "future": 3,
38 | "description": "--------"
39 | }
40 | ```
41 |
42 | 对就于每个技术栈都会有名字、重要程度、使用程度、当前级别、未来级别、描述的字段。毕竟技术是有其应该有的趋势的,如果仅仅只是在上面用一些图形来表示可能又不够。
43 |
44 | 接着,又按照不同的维度区分为language、others、tools、frameworks四个维度
45 |
46 | ```javascript
47 | {
48 | "language": [
49 | {
50 | "name": "Java",
51 | "important": 5,
52 | "usage": 5,
53 | "current": 4,
54 | "future": 3,
55 | "description": "--------"
56 | }
57 | ],
58 | "tools": [
59 | {
60 | "name": "Linux",
61 | "important": 3,
62 | "usage": 3,
63 | "current": 3,
64 | "future": 2,
65 | "description": "--------"
66 | }
67 | ],
68 | "others": [
69 | {
70 | "name": "Agile",
71 | "important": 3,
72 | "usage": 5,
73 | "current": 3,
74 | "future": 3,
75 | "description": "--------"
76 | }
77 | ],
78 | "frameworks": [
79 | {
80 | "name": "Node.js",
81 | "important": 3,
82 | "usage": 5,
83 | "current": 3,
84 | "future": 5,
85 | "description": "--------"
86 | }
87 | ]
88 | }
89 | ```
90 |
91 | 而在上述的版本中,则有了我想要的箭头,尽管数据不合适,但是还是可以改的。
92 |
93 | ### Step 2: 处理数据
94 |
95 | 然后,我们的主要精力就是在parse上面的数据中,取出每个数据,按照不同的维度去放置技术栈,并进行一些转换。
96 |
97 | ```javascript
98 | var results = [];
99 | for (var quadrant in data) {
100 | $.each(data[quadrant], function (index, skill) {
101 | results.push({
102 | name: skill.name,
103 | important: skill.important,
104 | usage: skill.usage,
105 | description: skill.description,
106 | trend: entry(quadrant, convertFractions(skill.current), convertFractions(skill.future))
107 | });
108 | })
109 | }
110 | ```
111 |
112 |
--------------------------------------------------------------------------------
/chapters/text2logo.md:
--------------------------------------------------------------------------------
1 | 文本转Logo
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### 背景
8 |
9 | 在设计技能树的时候需要做一些简单的Logo,方便我们来识别,这时候就想到了PIL。加上一些简单的圆角,以及特殊的字体,就可以构成一个简单的Logo。做成的图标看上去还不错:
10 |
11 | ### ShowCase
12 |
13 |    
14 |
15 | 代码见:[https://github.com/phodal/text2logo](https://github.com/phodal/text2logo)
16 |
17 | ### 需求说明
18 |
19 | 简单的说一些我们的附加需求
20 |
21 | - 圆角
22 | - 色彩(自动)
23 |
24 | 步骤
25 | ---
26 |
27 | ###Step 1: Python 文字转Logo实战
28 |
29 | 一个简单的PIL生成图片的代码:
30 |
31 | ```python
32 | # -*- coding: utf-8 -*-
33 | from PIL import Image, ImageDraw, ImageFont
34 |
35 | img = Image.new('L', (128, 128), 255)
36 | draw = ImageDraw.Draw(img)
37 | text_to_draw = unicode('xxs', 'utf-8')
38 | font = ImageFont.truetype('fonts/NotoSansCJKsc-Regular.otf', 12)
39 | draw.text((2, 2), text_to_draw, font=font)
40 | del draw
41 |
42 | img.save('build/image.png')
43 | ```
44 |
45 | #### 圆角代码
46 |
47 | 我们需要的是在上面的代码上加上一个圆角的功能,于是Google到了这个函数
48 |
49 | ```javascript
50 | def add_corners(im, rad):
51 | circle = Image.new('L', (rad * 2, rad * 2), 0)
52 | image = ImageDraw.Draw(circle)
53 | image.ellipse((0, 0, rad * 2, rad * 2), fill=255)
54 | alpha = Image.new('L', im.size, 255)
55 | w, h = im.size
56 | alpha.paste(circle.crop((0, 0, rad, rad)), (0, 0))
57 | alpha.paste(circle.crop((0, rad, rad, rad * 2)), (0, h - rad))
58 | alpha.paste(circle.crop((rad, 0, rad * 2, rad)), (w - rad, 0))
59 | alpha.paste(circle.crop((rad, rad, rad * 2, rad * 2)), (w - rad, h - rad))
60 | im.putalpha(alpha)
61 | return im
62 | ```
63 |
64 | #### 颜色配置
65 |
66 | 在Github上找到了一个配色还不错的CSS,将之改为color.Ini,在里面配置了不同色彩与文字、前景的有关系等等,如:
67 |
68 | [Color]
69 | turqoise: #1abc9c,#ecf0f1
70 | greenSea: #16a085,#ecf0f1
71 |
72 | [Text]
73 | 自动化测试: auto_test
74 | 前端开发: web_front_develop
75 |
76 | 读取配置则用的是``ConfigParser``:
77 |
78 | ```python
79 | import ConfigParser
80 |
81 | ConfigColor = ConfigParser.ConfigParser()
82 | ConfigColor.read("./color.ini")
83 |
84 | bg_colors = []
85 | font_colors = []
86 |
87 | for color_name, color in ConfigColor.items('Color'):
88 | bg_colors.append(color.replace('#', '').split(',')[0])
89 | font_colors.append(color.replace('#', '').split(',')[1])
90 |
91 | colors_length = ConfigColor.items('Color').__len__()
92 | ```
93 |
94 | 最后我们就可以得到我们想要的图片了~~
95 |
--------------------------------------------------------------------------------
/chapters/vmap.md:
--------------------------------------------------------------------------------
1 | GeoJSON与ElasticSearch实现高级图形搜索
2 | ===
3 |
4 | 概况
5 | ---
6 |
7 | ### Showcase
8 |
9 | 在线Demo见: [http://vmap.phodal.com/](http://vmap.phodal.com/)
10 |
11 | 或者你已经使用过了相应多的省市区与地图联动,但是这些联动往往是单向的、不可逆。并且这些数据往往都是在线使用的,不能离线使用。下图是一个结合百度地图的省市区与地图联动:
12 |
13 | 
14 |
15 | 我们可以在这个应用里选择,相应的省市区然后地图会跳转到相应的地图。当我们在地图上漫游的时候,如果没有显示当前的省市区是不是变得很难使用。于是,我们就来创建一个吧:
16 |
17 | 
18 |
19 | ### jQuery + Mustache + Leaflet
20 |
21 | 相关技术栈:
22 |
23 | - Bootstrap,UI显示~~,地球人都知道。
24 | - jQuery,Bootstrap依赖。
25 | - Require.js,模块化。
26 | - Mustache,模板生成。
27 | - Leaflet,交互地图库。
28 |
29 | 步骤
30 | ---
31 |
32 | ### Step 1: 离线地图与搜索
33 |
34 | 在GitHub上搜索数据的过程中,发现了一个名为[d3js-geojson](https://github.com/ufoe/d3js-geojson)的项目里面放着中国详细省、市、县数据,并且还有及GeoJSON文件。
35 |
36 | 这就意味着两件事:
37 |
38 | - 地图离线
39 | - 多边形搜索
40 |
41 | 首先,我们要知道GeoJSON是怎样的一个存在。
42 |
43 | > GeoJSON是一种对各种地理数据结构进行编码的格式,基于Javascript对象表示法的地理空间信息数据交换格式。GeoJSON对象可以表示几何、特征或者特征集合。GeoJSON支持下面几何类型:点、线、面、多点、多线、多面和几何集合。GeoJSON里的特征包含一个几何对象和其他属性,特征集合表示一系列特征。
44 |
45 | 换句话来说,根据这个文件里面的多边形,我们可以绘制出中国地图。由于上面的点是真实的地理位置信息,所以无论我们怎样的缩放这些点的位置都不会发生变化。如下图是GitHub对这个数据文件的解析:
46 |
47 | 
48 |
49 | (PS: 预览可以打开这个页面:[Vmap GeoJSON](https://github.com/phodal/vmap/blob/gh-pages/static/data/china.json)
50 |
51 | 当然这似乎不是一个专业人员维护的数据,所以存在一些偏差。但是这些数据意味着,我们不需要依靠于在线地图就可以完成大部分的功能了。在线地图一直都是一个缓慢的存在,并且Google Map在多数人那都是不可用的。
52 |
53 | 接着问题来了,我们并没有把每个用户的数据存入到数据库中,那么我们怎么才能实现搜索?
54 |
55 | #### 多边形搜索
56 |
57 | 所谓的多边形搜索就是画一个圈圈(任意多边形),然后你就可以去约这个圈圈里的人,如下图所示:
58 |
59 | 
60 |
61 | 而圈圈搜索依赖于圈圈上的连续的点构建的形状来进行搜索,上面的每个点都包含了相应的经纬度。因此,只要是在这个圈圈里的用户都是可以搜索得到的。
62 |
63 | 这样实现的前提是:
64 |
65 | - 要有一个支持多边形搜索的搜索引擎,如ElasticSearch、Solr、MongoDB等等。
66 | - 要将用户的数据成功地存成GEO信息。
67 |
68 | 详细信息可以见: [VMap Bot](https://github.com/phodal/vmap-bot)
69 |
70 |
71 | ### Step 2: 从地点到地图上显示
72 |
73 | 拿Bootstrap实现一个Dropdown是一件很容易的事,我们只要动用一下相应的模板就好了。难就难在,如果去与地图交互。
74 |
75 | 最初的时候要用Event的形式来实现,但是发现这样似乎会让其紧耦合。就改用了监听Hash Change的形式来实现,在总的地图上每一个省都有一个对应的ID,这个ID会对应相应的省的数据。如下图所示:
76 |
77 | 
78 |
79 | 接着,我们就需要从这个Hash中判断它的级别和ID,随后转由相应的函数来处理这些逻辑即可。随后,我们要做两件事:
80 |
81 | - 创建对应省的市的Dropdown
82 | - 从地图上跳转到省
83 |
84 | 创建对应省的市的Dropdown,我们只需要根据地点重新生成一个新的Menu再插入即可。
85 |
86 | 从地图上跳转到对应的省的时候:
87 |
88 | 1. 用Ajax请求获取这个省的GeoJSON文件
89 | 2. 获取这个市的中心位置,并对其进行缩放
90 | 3. 将上面的每个市绘制到地图上
91 |
92 | 在这个过程中遇到的最大的坑是:中国有北京、上海、天津、重庆等直辖市,还有港、澳等自治区(PS:台湾是一个省)。对于这些特殊的地点,那么的缩放级别肯定会更高。
93 |
94 | 同理,我们也可以对上面的市运行处理。但是因为这些市并不存在GEO信息,所以我只是从其多连形信息取了一个点,再将这个点放到data-geo中:
95 |
96 | 
97 |
98 | 对应于省市的,对于区的处理也是如此。这样,我们就完成了地点到地图的显示了。
99 |
100 | ###Step 3: 从地图到地点上显示
101 |
102 | 从地图上到地点就比较简单了,点击时修改对应的text即可。
103 |
104 | 
105 |
106 |
--------------------------------------------------------------------------------
/css/vendor.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | .acodecode {
4 | cursor: pointer
5 | }
6 | .acodecodeactive {
7 | cursor: default
8 | }
9 | .codecodecode {
10 | position: fixed;
11 | z-index: 100;
12 | left: 2%;
13 | bottom: -30px;
14 | width: 96%;
15 | height: 0;
16 | max-height: 270px;
17 | text-align: left
18 | }
19 | .codecodecode>div {
20 | box-shadow: 0 0 3px #444
21 | }
22 | .codecodecontrols {
23 | height: 30px;
24 | margin-top: -30px;
25 | background-color: #fff;
26 | background-color: rgba(255, 255, 255, .8);
27 | border-radius: 8px 8px 0 0
28 | }
29 | .codecodecontrols a {
30 | float: left;
31 | line-height: 30px;
32 | margin-left: 6px;
33 | font-family: Arial;
34 | font-size: 12px
35 | }
36 | .codecodecontrols .closeCodeCode {
37 | float: right;
38 | margin-right: 6px
39 | }
40 | .acodecode.codecode, .codecode {
41 | border-radius: 0 !important;
42 | position: relative !important;
43 | width: 100% !important;
44 | margin: 0 !important;
45 | overflow: auto !important;
46 | cursor: default !important
47 | }
48 | div.codecode [id^=highlighter] div.bar.show, div.codecode [id^=highlighter] div.toolbar {
49 | display: none !important
50 | }
51 | h1, h2, h3, h4, h5, h6, p {
52 | margin-top: 0;
53 | padding-right: 15px;
54 | padding-left: 15px;
55 | text-align: left;
56 | font-family: Georgia
57 | }
58 | h1 {
59 | text-align: left
60 | }
61 | h2.booktitle {
62 | font-size: 1.5em;
63 | color: #666
64 | }
65 | .subhead-link {
66 | font-size: .75em;
67 | margin-left: -15px;
68 | margin-top: 5px;
69 | float: left;
70 | visibility: hidden
71 | }
72 | .subhead-link:hover {
73 | text-decoration: none;
74 | visibility: visible
75 | }
76 | h1:hover .subhead-link, h2:hover .subhead-link, h3:hover .subhead-link, h4:hover .subhead-link {
77 | visibility: visible
78 | }
79 | .container {
80 | background: #FFF
81 | }
82 | header {
83 | background: #fff
84 | }
85 | .content {
86 | padding: 10px 0;
87 | text-align: left
88 | }
89 | .footer {
90 | background: #fff
91 | }
92 | .fltrt {
93 | float: right;
94 | margin-left: 8px
95 | }
96 | .fltlft {
97 | float: left;
98 | margin-right: 8px
99 | }
100 | .clearfloat {
101 | clear: both;
102 | height: 0;
103 | font-size: 1px;
104 | line-height: 0
105 | }
106 | .copyright {
107 | text-align: left
108 | }
109 | .booktitle {
110 | text-align: center;
111 | line-height: 41px;
112 | border-bottom: 1px solid #fff;
113 | padding: 0;
114 | font-size: 2.2em
115 | }
116 | .booktitle.author {
117 | font-size: 24px
118 | }
119 | #contents-list {
120 | background: none repeat scroll 0 0 #EEE;
121 | border: 3px solid #DDD;
122 | padding: 1em 1em 1em 3em
123 | }
124 | .subitem {
125 | margin-left: 25px
126 | }
127 | #references-list {
128 | word-wrap: break-word
129 | }
130 | pre {
131 | display: block;
132 | line-height: 18px;
133 | background-color: #f5f5f5;
134 | -webkit-border-radius: 4px;
135 | -moz-border-radius: 4px;
136 | border-radius: 4px;
137 | white-space: pre;
138 | white-space: pre-wrap;
139 | word-break: break-all;
140 | word-wrap: break-word
141 | }
142 | div.syntaxhighlighter {
143 | padding: 1em 0
144 | }
145 | .syntaxhighlighter a, .syntaxhighlighter code, .syntaxhighlighter div, .syntaxhighlighter table, .syntaxhighlighter table caption, .syntaxhighlighter table tbody, .syntaxhighlighter table td, .syntaxhighlighter table thead, .syntaxhighlighter table tr, .syntaxhighlighter textarea {
146 | -moz-border-radius: 0 !important;
147 | -webkit-border-radius: 0 !important;
148 | background: none !important;
149 | border: 0 !important;
150 | bottom: auto !important;
151 | float: none !important;
152 | height: auto !important;
153 | left: auto !important;
154 | line-height: 1.1em !important;
155 | margin: 0 !important;
156 | outline: 0 !important;
157 | overflow: visible !important;
158 | padding: 0 !important;
159 | position: static !important;
160 | right: auto !important;
161 | text-align: left !important;
162 | top: auto !important;
163 | vertical-align: baseline !important;
164 | width: auto !important;
165 | box-sizing: content-box !important;
166 | font-family: Consolas, "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important;
167 | font-weight: 400 !important;
168 | font-style: normal !important;
169 | font-size: 1em !important;
170 | min-height: inherit !important;
171 | min-height: auto !important
172 | }
173 | .syntaxhighlighter {
174 | width: 100% !important;
175 | margin: 1em 0 !important;
176 | position: relative !important;
177 | overflow: auto !important;
178 | font-size: 1em !important
179 | }
180 | .syntaxhighlighter.source {
181 | overflow: hidden !important
182 | }
183 | .syntaxhighlighter .bold {
184 | font-weight: 700 !important
185 | }
186 | .syntaxhighlighter .italic {
187 | font-style: italic !important
188 | }
189 | .syntaxhighlighter .line {
190 | white-space: pre !important
191 | }
192 | .syntaxhighlighter table {
193 | width: 100% !important
194 | }
195 | .syntaxhighlighter table caption {
196 | text-align: left !important;
197 | padding: .5em 0 .5em 1em !important
198 | }
199 | .syntaxhighlighter table td.code {
200 | width: 100% !important
201 | }
202 | .syntaxhighlighter table td.code .container {
203 | position: relative !important
204 | }
205 | .syntaxhighlighter table td.code .container textarea {
206 | box-sizing: border-box !important;
207 | position: absolute !important;
208 | left: 0 !important;
209 | top: 0 !important;
210 | width: 100% !important;
211 | height: 100% !important;
212 | border: 0 !important;
213 | background: #fff !important;
214 | padding-left: 1em !important;
215 | overflow: hidden !important;
216 | white-space: pre !important
217 | }
218 | .syntaxhighlighter table td.gutter .line {
219 | text-align: right !important;
220 | padding: 0 .5em 0 1em !important
221 | }
222 | .syntaxhighlighter table td.code .line {
223 | padding: 0 1em !important
224 | }
225 | .syntaxhighlighter.nogutter td.code .container textarea, .syntaxhighlighter.nogutter td.code .line {
226 | padding-left: 0 !important
227 | }
228 | .syntaxhighlighter.show {
229 | display: block !important
230 | }
231 | .syntaxhighlighter.collapsed table {
232 | display: none !important
233 | }
234 | .syntaxhighlighter.collapsed .toolbar {
235 | padding: .1em .8em 0 !important;
236 | font-size: 1em !important;
237 | position: static !important;
238 | width: auto !important;
239 | height: auto !important
240 | }
241 | .syntaxhighlighter.collapsed .toolbar span {
242 | display: inline !important;
243 | margin-right: 1em !important
244 | }
245 | .syntaxhighlighter.collapsed .toolbar span a {
246 | padding: 0 !important;
247 | display: none !important
248 | }
249 | .syntaxhighlighter.collapsed .toolbar span a.expandSource {
250 | display: inline !important
251 | }
252 | .syntaxhighlighter .toolbar {
253 | position: absolute !important;
254 | right: 1px !important;
255 | top: 1px !important;
256 | width: 11px !important;
257 | height: 11px !important;
258 | font-size: 10px !important;
259 | z-index: 10 !important
260 | }
261 | .syntaxhighlighter .toolbar span.title {
262 | display: inline !important
263 | }
264 | .syntaxhighlighter .toolbar a {
265 | display: block !important;
266 | text-align: center !important;
267 | text-decoration: none !important;
268 | padding-top: 1px !important
269 | }
270 | .syntaxhighlighter .toolbar a.expandSource {
271 | display: none !important
272 | }
273 | .syntaxhighlighter.ie {
274 | font-size: .9em !important;
275 | padding: 1px 0 !important
276 | }
277 | .syntaxhighlighter.ie .toolbar {
278 | line-height: 8px !important
279 | }
280 | .syntaxhighlighter.ie .toolbar a {
281 | padding-top: 0 !important
282 | }
283 | .syntaxhighlighter.printing .line.alt1 .content, .syntaxhighlighter.printing .line.alt2 .content, .syntaxhighlighter.printing .line.highlighted .number, .syntaxhighlighter.printing .line.highlighted.alt1 .content, .syntaxhighlighter.printing .line.highlighted.alt2 .content {
284 | background: none !important
285 | }
286 | .syntaxhighlighter.printing .line .number {
287 | color: #bbb !important
288 | }
289 | .syntaxhighlighter.printing .line .content {
290 | color: #000 !important
291 | }
292 | .syntaxhighlighter.printing .toolbar {
293 | display: none !important
294 | }
295 | .syntaxhighlighter.printing a {
296 | text-decoration: none !important
297 | }
298 | .syntaxhighlighter.printing .plain, .syntaxhighlighter.printing .plain a {
299 | color: #000 !important
300 | }
301 | .syntaxhighlighter.printing .comments, .syntaxhighlighter.printing .comments a {
302 | color: #008200 !important
303 | }
304 | .syntaxhighlighter.printing .string, .syntaxhighlighter.printing .string a {
305 | color: #00f !important
306 | }
307 | .syntaxhighlighter.printing .keyword {
308 | color: #069 !important;
309 | font-weight: 700 !important
310 | }
311 | .syntaxhighlighter.printing .preprocessor {
312 | color: gray !important
313 | }
314 | .syntaxhighlighter.printing .variable {
315 | color: #a70 !important
316 | }
317 | .syntaxhighlighter.printing .value {
318 | color: #090 !important
319 | }
320 | .syntaxhighlighter.printing .functions {
321 | color: #ff1493 !important
322 | }
323 | .syntaxhighlighter.printing .constants {
324 | color: #06c !important
325 | }
326 | .syntaxhighlighter.printing .script {
327 | font-weight: 700 !important
328 | }
329 | .syntaxhighlighter.printing .color1, .syntaxhighlighter.printing .color1 a {
330 | color: gray !important
331 | }
332 | .syntaxhighlighter.printing .color2, .syntaxhighlighter.printing .color2 a {
333 | color: #ff1493 !important
334 | }
335 | .syntaxhighlighter.printing .color3, .syntaxhighlighter.printing .color3 a {
336 | color: red !important
337 | }
338 | .syntaxhighlighter.printing .break, .syntaxhighlighter.printing .break a {
339 | color: #000 !important
340 | }
341 | .syntaxhighlighter, .syntaxhighlighter .line.alt1, .syntaxhighlighter .line.alt2 {
342 | background-color: #1b2426 !important
343 | }
344 | .syntaxhighlighter .line.highlighted.alt1, .syntaxhighlighter .line.highlighted.alt2 {
345 | background-color: #323e41 !important
346 | }
347 | .syntaxhighlighter .line.highlighted.number, .syntaxhighlighter table caption {
348 | color: #b9bdb6 !important
349 | }
350 | .syntaxhighlighter .gutter {
351 | color: #afafaf !important
352 | }
353 | .syntaxhighlighter .gutter .line {
354 | border-right: 3px solid #435a5f !important
355 | }
356 | .syntaxhighlighter .gutter .line.highlighted {
357 | background-color: #435a5f !important;
358 | color: #1b2426 !important
359 | }
360 | .syntaxhighlighter.printing .line .content {
361 | border: 0 !important
362 | }
363 | .syntaxhighlighter.collapsed {
364 | overflow: visible !important
365 | }
366 | .syntaxhighlighter.collapsed .toolbar {
367 | color: #5ba1cf !important;
368 | background: #000 !important;
369 | border: 1px solid #435a5f !important
370 | }
371 | .syntaxhighlighter.collapsed .toolbar a {
372 | color: #5ba1cf !important
373 | }
374 | .syntaxhighlighter.collapsed .toolbar a:hover {
375 | color: #5ce638 !important
376 | }
377 | .syntaxhighlighter .toolbar {
378 | color: #fff !important;
379 | background: #435a5f !important;
380 | border: 0 !important
381 | }
382 | .syntaxhighlighter .toolbar a {
383 | color: #fff !important
384 | }
385 | .syntaxhighlighter .toolbar a:hover {
386 | color: #e0e8ff !important
387 | }
388 | .syntaxhighlighter .plain, .syntaxhighlighter .plain a {
389 | color: #b9bdb6 !important
390 | }
391 | .syntaxhighlighter .comments, .syntaxhighlighter .comments a {
392 | color: #878a85 !important
393 | }
394 | .syntaxhighlighter .string, .syntaxhighlighter .string a {
395 | color: #5ce638 !important
396 | }
397 | .syntaxhighlighter .keyword {
398 | color: #5ba1cf !important
399 | }
400 | .syntaxhighlighter .preprocessor {
401 | color: #435a5f !important
402 | }
403 | .syntaxhighlighter .variable {
404 | color: #ffaa3e !important
405 | }
406 | .syntaxhighlighter .value {
407 | color: #090 !important
408 | }
409 | .syntaxhighlighter .functions {
410 | color: #ffaa3e !important
411 | }
412 | .syntaxhighlighter .constants {
413 | color: #e0e8ff !important
414 | }
415 | .syntaxhighlighter .script {
416 | font-weight: 700 !important;
417 | color: #5ba1cf !important;
418 | background-color: none !important
419 | }
420 | .syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
421 | color: #e0e8ff !important
422 | }
423 | .syntaxhighlighter .color2, .syntaxhighlighter .color2 a {
424 | color: #fff !important
425 | }
426 | .syntaxhighlighter .color3, .syntaxhighlighter .color3 a {
427 | color: #ffaa3e !important
428 | }
429 | .sausage-set {
430 | position: fixed;
431 | right: 0;
432 | top: 0;
433 | width: 15px;
434 | height: 100%;
435 | border-left: solid 2px #fff;
436 | border-right: solid 2px #fff;
437 | background-color: #fff;
438 | font-family: 'Helvetica Neue', Arial, sans-serif
439 | }
440 | .sausage {
441 | position: absolute;
442 | left: 0;
443 | width: 100%;
444 | height: 100%;
445 | background-color: #f1f1f1;
446 | text-decoration: none;
447 | -moz-border-radius: 8px;
448 | -webkit-border-bottom-left-radius: 8px;
449 | -webkit-border-top-left-radius: 8px;
450 | -webkit-border-bottom-right-radius: 8px;
451 | -webkit-border-top-right-radius: 8px;
452 | -moz-box-shadow: inset 0 1px 2px 4px rgba(0, 0, 0, .025);
453 | -webkit-box-shadow: inset 0 1px 2px 4px rgba(0, 0, 0, .025);
454 | cursor: pointer
455 | }
456 | .sausage-current, .sausage-hover {
457 | background-color: #f2e4ed;
458 | -moz-box-shadow: inset 0 1px 2px 4px rgba(51, 63, 70, .025)
459 | }
460 | .sausage-span {
461 | position: absolute;
462 | right: 24px;
463 | top: 5px;
464 | z-index: 2;
465 | display: none;
466 | width: 100px;
467 | padding: 2px 3px;
468 | color: #000;
469 | background-color: #fff;
470 | border: solid 2px #906;
471 | font-size: 10px;
472 | line-height: 12px;
473 | font-weight: 700;
474 | text-align: center;
475 | -moz-border-radius: 7px;
476 | -webkit-border-bottom-left-radius: 7px;
477 | -webkit-border-top-left-radius: 7px;
478 | -webkit-border-bottom-right-radius: 7px;
479 | -webkit-border-top-right-radius: 7px;
480 | -moz-box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, .05);
481 | -webkit-box-shadow: 1px 1px 1px 1px rgba(0, 0, 0, .05)
482 | }
483 | .sausage-current .sausage-span, .sausage-hover .sausage-span {
484 | display: block
485 | }
486 | a, abbr, acronym, address, article, aside, blockquote, body, caption, code, dd, del, dfn, dialog, div, dl, dt, em, fieldset, figure, footer, form, h1, h2, h3, h4, h5, h6, header, hgroup, html, iframe, img, label, legend, li, nav, object, ol, p, pre, q, section, span, table, tbody, td, tfoot, th, thead, tr, ul {
487 | margin: 0;
488 | padding: 0;
489 | border: 0;
490 | font-weight: inherit;
491 | font-style: inherit;
492 | font-size: 100%;
493 | font-family: inherit;
494 | vertical-align: baseline
495 | }
496 | article, aside, dialog, figure, footer, header, hgroup, nav, section {
497 | display: block
498 | }
499 | body {
500 | line-height: 1.5
501 | }
502 | table {
503 | border-collapse: separate;
504 | border-spacing: 0
505 | }
506 | caption, td, th {
507 | text-align: left;
508 | font-weight: 400
509 | }
510 | table, td, th {
511 | vertical-align: middle
512 | }
513 | blockquote:after, blockquote:before, q:after, q:before {
514 | content: ""
515 | }
516 | blockquote, q {
517 | quotes: "" ""
518 | }
519 | a img {
520 | border: 0
521 | }
522 | .search, body, input[type=submit], input[type=text] {
523 | font-family: Palatino, "Palatino Linotype", Georgia, Times, "Times New Roman", serif
524 | }
525 | html {
526 | font-size: 100.01%
527 | }
528 | h1, h2, h3, h4, h5, h6 {
529 | font-weight: 400;
530 | color: #000
531 | }
532 | h1 {
533 | font-size: 3em;
534 | line-height: 1;
535 | margin-bottom: .5em
536 | }
537 | h2 {
538 | font-size: 2em;
539 | margin-bottom: .75em
540 | }
541 | h3 {
542 | font-size: 1.5em;
543 | line-height: 1
544 | }
545 | h4 {
546 | font-size: 1.2em;
547 | line-height: 1.25;
548 | margin-bottom: 1.25em
549 | }
550 | h5 {
551 | font-size: 1em;
552 | font-weight: 700;
553 | margin-bottom: 1.5em
554 | }
555 | h6 {
556 | font-size: 1em;
557 | font-weight: 700
558 | }
559 | h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {
560 | margin: 0
561 | }
562 | p {
563 | margin: 0 0 1.5em
564 | }
565 | p img.left {
566 | float: left;
567 | margin: 1.5em 1.5em 1.5em 0;
568 | padding: 0
569 | }
570 | p img.right {
571 | float: right;
572 | margin: 1.5em 0 1.5em 1.5em
573 | }
574 | strong {
575 | font-weight: 700
576 | }
577 | dfn, em {
578 | font-style: italic
579 | }
580 | dfn {
581 | font-weight: 700
582 | }
583 | sub, sup {
584 | line-height: 0
585 | }
586 | address {
587 | margin: 0 0 1.5em;
588 | font-style: italic
589 | }
590 | del {
591 | color: #666
592 | }
593 | li ol, li ul {
594 | margin: 0
595 | }
596 | ol, ul {
597 | margin: 0 1.5em 1.5em 0;
598 | padding-left: 3.333em
599 | }
600 | ul {
601 | list-style-type: disc
602 | }
603 | ol {
604 | list-style-type: decimal
605 | }
606 | dl {
607 | margin: 0 0 1.5em
608 | }
609 | dl dt {
610 | font-weight: 700
611 | }
612 | dd {
613 | margin-left: 1.5em
614 | }
615 | table {
616 | margin-bottom: 1.4em
617 | }
618 | th {
619 | font-weight: 700
620 | }
621 | thead th {
622 | background: #c3d9ff
623 | }
624 | caption, td, th {
625 | padding: 4px 10px 4px 5px
626 | }
627 | tfoot {
628 | font-style: italic
629 | }
630 | body {
631 | font-size: 100%;
632 | color: #000;
633 | background: #F6f6F6 url(../images/base.png) repeat 0 0
634 | }
635 | h3 {
636 | border-bottom: 1px solid #CCC;
637 | margin-bottom: .5em;
638 | padding-bottom: .5em
639 | }
640 | .lead {
641 | font-size: 1.5em
642 | }
643 | .stage_links {
644 | color: #777
645 | }
646 | a, a:link, a:visited {
647 | color: #906;
648 | text-decoration: none
649 | }
650 | a:active, a:focus, a:hover {
651 | color: #E106B2;
652 | text-decoration: underline
653 | }
654 | hr.space {
655 | background: #fff;
656 | color: #fff;
657 | visibility: hidden
658 | }
659 | hr {
660 | background: #CCC;
661 | color: #CCC;
662 | clear: both;
663 | float: none;
664 | width: 100%;
665 | height: .1em;
666 | margin: 0 0 1.45em;
667 | border: 0
668 | }
669 | hr.bold {
670 | height: 1px;
671 | background-color: #906;
672 | color: #906
673 | }
674 | blockquote {
675 | overflow: hidden;
676 | margin: 0 0 1.5em;
677 | padding: 0 1.5em;
678 | color: #000;
679 | font-style: normal
680 | }
681 | blockquote p {
682 | margin-bottom: .5em
683 | }
684 | .attribution {
685 | font-style: italic;
686 | text-align: right;
687 | color: #777
688 | }
689 | table {
690 | width: 100%
691 | }
692 | code {
693 | padding: 2px 4px;
694 | color: #D14;
695 | background-color: #F7F7F9;
696 | border: 1px solid #E1E1E8;
697 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
698 | font-size: 12px;
699 | -webkit-border-radius: 3px;
700 | -moz-border-radius: 3px;
701 | border-radius: 3px
702 | }
703 | .caption, caption {
704 | caption-side: bottom;
705 | background: 0 0;
706 | font-style: italic
707 | }
708 | tr.even td {
709 | background: #F4F4F4
710 | }
711 | tfoot td {
712 | border-top: 1px solid #EAEAEA;
713 | font-weight: 700;
714 | font-style: normal
715 | }
716 | abbr, acronym {
717 | border: 0
718 | }
719 | abbr[title]:hover {
720 | border-bottom: 1px dotted #666;
721 | cursor: help
722 | }
723 | .red {
724 | background: red
725 | }
726 | .highlight {
727 | background: #EEC3C3
728 | }
729 | .blocks:after, .clearfix:after, .container:after, .edition_list:after, .editions:after, .inner:after {
730 | content: "\0020";
731 | display: block;
732 | height: 0;
733 | clear: both;
734 | visibility: hidden;
735 | overflow: hidden
736 | }
737 | .blocks, .clearfix, .container, .edition_list, .editions, .inner {
738 | display: block
739 | }
740 | .clear {
741 | clear: both
742 | }
743 | .container {
744 | max-width: 940px;
745 | margin: 0 auto;
746 | padding: 0 16px;
747 | text-align: left
748 | }
749 | .gttr {
750 | margin-right: 2.9%;
751 | float: left
752 | }
753 | .last {
754 | float: left
755 | }
756 | .sidebar {
757 | width: 32%;
758 | float: right;
759 | margin-right: 0
760 | }
761 | .content_footer {
762 | clear: both
763 | }
764 | .span10, .span2, .span3, .span4, .span6, .span8 {
765 | margin-right: 2.3%;
766 | float: left
767 | }
768 | .span2 {
769 | width: 14.6341%
770 | }
771 | .span3 {
772 | width: 23.1707%
773 | }
774 | .span4 {
775 | width: 31.7073%
776 | }
777 | .span6 {
778 | width: 48.7805%
779 | }
780 | .span8 {
781 | width: 65.853658%
782 | }
783 | .span10 {
784 | width: 73.17073%
785 | }
786 | .blocks, .editions {
787 | margin: 1.5em 0;
788 | clear: both
789 | }
790 | .editions {
791 | margin-bottom: 0
792 | }
793 | .last {
794 | margin-right: 0 !important
795 | }
796 | .lyt_img {
797 | max-width: 100%
798 | }
799 | .page_header {
800 | overflow: hidden;
801 | padding: 0 0 .2em;
802 | margin: 1.5em 0
803 | }
804 | .logo {
805 | width: 49.5%;
806 | float: left;
807 | margin-bottom: 0
808 | }
809 | .logo span {
810 | display: block;
811 | font-style: italic;
812 | font-size: .5em
813 | }
814 | .logo a, .logo a:link, .logo a:visited {
815 | color: #000
816 | }
817 | .logo a:active, .logo a:focus, .logo a:hover {
818 | color: #906;
819 | text-decoration: none
820 | }
821 | .search_box {
822 | width: 50%;
823 | float: right
824 | }
825 | .search {
826 | width: auto;
827 | float: right;
828 | vertical-align: middle
829 | }
830 | .edition_list, .info_bubble, .inner, .stage {
831 | padding: 1em 1.5em;
832 | background: #FFF;
833 | -moz-border-radius: 3px;
834 | -webkit-border-radius: 3px;
835 | border-radius: 3px;
836 | -moz-box-shadow: 0 0 8px #999;
837 | -webkit-box-shadow: 0 0 8px #999;
838 | box-shadow: 0 0 8px #999
839 | }
840 | .stage {
841 | margin-bottom: 1.5em;
842 | padding: 3em 50% 1.5em 3em
843 | }
844 | .edition_list {
845 | margin: 0 0 1em;
846 | clear: both;
847 | overflow: hidden
848 | }
849 | .edition_list p {
850 | margin-bottom: 0;
851 | font-style: italic
852 | }
853 | .edition_list h2 {
854 | margin-bottom: 0;
855 | font-size: 1.8em;
856 | line-height: 1.5;
857 | font-weight: 700
858 | }
859 | .edition_list ul {
860 | margin: 0;
861 | padding: 0;
862 | list-style: none;
863 | display: inline
864 | }
865 | .edition_list ul li {
866 | display: inline;
867 | margin-right: 1em;
868 | padding-right: 1em;
869 | border-right: 1px solid #F6f6F6
870 | }
871 | .edition_list ul li:last-child {
872 | margin-right: 0;
873 | padding-right: 0;
874 | border-right: 0
875 | }
876 | .info_bubble {
877 | padding-bottom: 1em;
878 | color: #FFF;
879 | text-shadow: 1px 1px 0 #000;
880 | background: #906;
881 | position: relative;
882 | overflow: visible
883 | }
884 | .info_bubble p {
885 | margin-bottom: 0
886 | }
887 | .info_bubble.startpage {
888 | padding-bottom: 1em;
889 | min-height: 9em;
890 | height: auto !important;
891 | height: 9em;
892 | z-index: 0
893 | }
894 | .info_bubble.startpage span {
895 | position: absolute;
896 | display: block;
897 | bottom: 1em;
898 | left: -2em;
899 | width: 40px;
900 | height: 40px;
901 | z-index: 100
902 | }
903 | .blocks>div {
904 | margin-bottom: 1.5em
905 | }
906 | .footer {
907 | margin: 1.5em 0 0;
908 | padding: 1.5em 0 0;
909 | overflow: hidden
910 | }
911 | #buy {
912 | position: relative
913 | }
914 | #offers {
915 | position: absolute;
916 | bottom: 0;
917 | font-size: .625em;
918 | margin: 0;
919 | padding: 0;
920 | list-style: none;
921 | display: none
922 | }
923 | .save_a_tree h3 {
924 | display: none;
925 | line-height: 1.5;
926 | border-bottom: 0
927 | }
928 | #offers li {
929 | display: inline;
930 | margin-right: 1em;
931 | padding-right: 1em;
932 | border-right: 1px solid #F6f6F6
933 | }
934 | #offers li:last-child {
935 | margin-right: 0;
936 | padding-right: 0;
937 | border-right: 0
938 | }
939 | .shopping_cart_button_line, .shopping_cart_button_line_bottom {
940 | border-top: 1px solid #CCC;
941 | margin-top: .5em;
942 | padding: .5em 1px 0;
943 | overflow: hidden;
944 | clear: both
945 | }
946 | .shopping_cart_button_line_bottom {
947 | margin-bottom: 7em
948 | }
949 | .shopping_cart_button_line:first-child {
950 | border-top: 0
951 | }
952 | div.header {
953 | font-size: 1.5em;
954 | line-height: 1;
955 | border-bottom: 1px solid #CCC;
956 | margin-bottom: .5em;
957 | padding-bottom: .5em;
958 | color: #000;
959 | text-shadow: 1px 1px 0 #FFF
960 | }
961 | .product_title {
962 | font-style: italic;
963 | margin-bottom: 1.5em
964 | }
965 | .product_price {
966 | float: left
967 | }
968 | .price {
969 | color: #666
970 | }
971 | .product_buy_link {
972 | float: right;
973 | line-height: 1
974 | }
975 | .ebook_formats, .safari_read_now, .whatisthis {
976 | font-size: .75em
977 | }
978 | .safari_read_now {
979 | float: right;
980 | line-height: 2
981 | }
982 | input[type=submit], input[type=text] {
983 | font-size: 1em;
984 | -moz-border-radius: .5em;
985 | -webkit-border-radius: .5em;
986 | border-radius: .5em;
987 | vertical-align: middle
988 | }
989 | input[type=text] {
990 | width: 140px;
991 | margin: .5em 0;
992 | padding: .5em .5em .3em;
993 | border: 1px solid #F1F1F0;
994 | color: #666;
995 | -moz-box-shadow: inset 3px 3px 2px #999;
996 | -webkit-box-shadow: inset 3px 3px 2px #999;
997 | box-shadow: inset 3px 3px 2px #999
998 | }
999 | input[type=text]:active, input[type=text]:focus, input[type=text]:hover {
1000 | background: #FFF;
1001 | color: #000;
1002 | outline: 0
1003 | }
1004 | input[type=submit] {
1005 | width: auto;
1006 | margin: 0;
1007 | padding: .28em .5em;
1008 | background: #906;
1009 | color: #FFF;
1010 | border: 0;
1011 | text-transform: uppercase;
1012 | cursor: pointer;
1013 | text-shadow: 1px 1px 0 #000;
1014 | -moz-box-shadow: 0 0 2px #999;
1015 | -webkit-box-shadow: 0 0 2px #999;
1016 | box-shadow: 0 0 2px #999
1017 | }
1018 | input[type=submit]:active, input[type=submit]:focus, input[type=submit]:hover {
1019 | color: #FFF;
1020 | background: #E106B2;
1021 | text-shadow: 1px 1px 0 #000;
1022 | -moz-box-shadow: 0 0 3px #333;
1023 | -webkit-box-shadow: 0 0 3px #333;
1024 | box-shadow: 0 0 3px #333
1025 | }
1026 | a.anchor {
1027 | color: #000
1028 | }
1029 | a.anchor:hover {
1030 | text-decoration: none
1031 | }
1032 | .figure {
1033 | padding-top: 1.5em
1034 | }
1035 | .caption, .figure {
1036 | display: block;
1037 | margin-bottom: 1.5em;
1038 | text-align: center
1039 | }
1040 | .figure * {
1041 | text-align: left
1042 | }
1043 | .caption {
1044 | font-size: .875em
1045 | }
1046 | .figure .caption {
1047 | text-align: center
1048 | }
1049 | .figure img {
1050 | margin-bottom: .5em;
1051 | max-width: 100%
1052 | }
1053 | .attribution::before {
1054 | content: ""
1055 | }
1056 | .sidebar ul {
1057 | font-size: .9em;
1058 | padding-left: 2em;
1059 | list-style-type: square
1060 | }
1061 | .disabled, .footer a, .footer a:active, .footer a:focus, .footer a:hover, .footer a:link, .footer a:visited, .info_bubble a, .info_bubble a:active, .info_bubble a:focus, .info_bubble a:hover, .info_bubble a:link, .info_bubble a:visited {
1062 | color: #BBB
1063 | }
1064 | .buybuttonswidget {
1065 | padding-bottom: 10px;
1066 | background: #FFF
1067 | }
1068 | @media (max-width:320px) {
1069 | .container {
1070 | max-width: 300px
1071 | }
1072 | #reviews {
1073 | display: none
1074 | }
1075 | }
1076 | @media (min-width:768px) and (max-width:1024px) {
1077 | .container {
1078 | max-width: 740px;
1079 | width: 740px
1080 | }
1081 | }
1082 | @media (min-width:1025px) {
1083 | .container {
1084 | width: 740px
1085 | }
1086 | }
--------------------------------------------------------------------------------
/epub.css:
--------------------------------------------------------------------------------
1 | html, body, div, span, h1, h2, h3, h4, h5, h6, p, a, em, strong, b, u, i, pre, code, del, strike, abbr, acronym, address, q, cite, blockquote, big, small, sub, sup, tt, var, center, img, dfn, ins, kbd, s, samp, dl, dt, dd, ol, ul, li, fieldset, legend, label, table, caption, tbody, tfoot, thead, tr, th, td {
2 | margin: 0;
3 | padding: 0;
4 | border: 0;
5 | outline: 0;
6 | font-size: 100%;
7 | vertical-align: baseline;
8 | background: transparent;
9 | }
10 | @page {
11 | margin: 5px;
12 | }
13 | p {
14 | margin-bottom: 9px;
15 | line-height: 1.4;
16 | }
17 | a {
18 | color: #0069d6;
19 | }
20 | a:hover {
21 | color: #0050a3;
22 | text-decoration: none;
23 | }
24 | a img {
25 | border: none;
26 | }
27 | h1, h2, h3, h4, h5, h6 {
28 | color: #404040;
29 | line-height: 1.5;
30 | margin: 1em 0 0.5em;
31 | -webkit-hyphens: none;
32 | hyphens: none;
33 | adobe-hyphenate: none;
34 | }
35 | h1 {
36 | font-size: 220%;
37 | margin-bottom: 1.5em;
38 | }
39 | h2 {
40 | font-size: 190%;
41 | }
42 | h3 {
43 | font-size: 160%;
44 | }
45 | h4 {
46 | font-size: 140%;
47 | }
48 | h5 {
49 | font-size: 130%;
50 | }
51 | h6 {
52 | font-size: 120%;
53 | }
54 | hr {
55 | margin: 0 0 19px;
56 | border: 0;
57 | border-bottom: 1px solid #ccc;
58 | }
59 | blockquote {
60 | padding: 13px 13px 21px 15px;
61 | margin-bottom: 18px;
62 | font-family: georgia, serif;
63 | font-style: italic;
64 | }
65 | blockquote:before {
66 | content: "\201C";
67 | font-size: 300%;
68 | margin-left: -10px;
69 | font-family: serif;
70 | color: #eee;
71 | }
72 | blockquote p {
73 | font-size: 120%;
74 | margin-bottom: 0;
75 | font-style: italic;
76 | }
77 | code, pre {
78 | font-family: monospace;
79 | }
80 | code {
81 | background-color: #fee9cc;
82 | color: rgba(0, 0, 0, 0.75);
83 | padding: 1px 3px;
84 | -webkit-border-radius: 5px;
85 | border-radius: 5px;
86 | font-size: 85%;
87 | }
88 | pre {
89 | display: block;
90 | padding: 14px;
91 | margin: 0 0 18px;
92 | font-size: 85%;
93 | line-height: 1.3;
94 | border: 1px solid #d9d9d9;
95 | white-space: pre-wrap;
96 | word-wrap: break-word;
97 | -webkit-hyphens: none;
98 | hyphens: none;
99 | adobe-hyphenate: none;
100 | }
101 | pre code {
102 | background-color: #fff;
103 | color: #737373;
104 | padding: 0;
105 | }
106 | code.sourceCode span.kw {
107 | color: #007020;
108 | font-weight: bold;
109 | }
110 | code.sourceCode span.dt {
111 | color: #902000;
112 | }
113 | code.sourceCode span.dv {
114 | color: #40a070;
115 | }
116 | code.sourceCode span.bn {
117 | color: #40a070;
118 | }
119 | code.sourceCode span.fl {
120 | color: #40a070;
121 | }
122 | code.sourceCode span.ch {
123 | color: #4070a0;
124 | }
125 | code.sourceCode span.st {
126 | color: #4070a0;
127 | }
128 | code.sourceCode span.co {
129 | color: #60a0b0;
130 | font-style: italic;
131 | }
132 | code.sourceCode span.ot {
133 | color: #007020;
134 | }
135 | code.sourceCode span.al {
136 | color: red;
137 | font-weight: bold;
138 | }
139 | code.sourceCode span.fu {
140 | color: #06287e;
141 | }
142 | code.sourceCode span.re {
143 | }
144 | code.sourceCode span.er {
145 | color: red;
146 | font-weight: bold;
147 | }
148 | body {
149 | font-family: serif;
150 | }
--------------------------------------------------------------------------------
/images/alipay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/alipay.png
--------------------------------------------------------------------------------
/images/anti-map-action.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/anti-map-action.jpg
--------------------------------------------------------------------------------
/images/badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/badge.png
--------------------------------------------------------------------------------
/images/bookshelf.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/bookshelf.jpg
--------------------------------------------------------------------------------
/images/brand-idea-prototype.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/brand-idea-prototype.jpg
--------------------------------------------------------------------------------
/images/carrot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/carrot.png
--------------------------------------------------------------------------------
/images/china-geojson.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/china-geojson.jpg
--------------------------------------------------------------------------------
/images/city-with-geo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/city-with-geo.jpg
--------------------------------------------------------------------------------
/images/clean_code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/clean_code.png
--------------------------------------------------------------------------------
/images/congee.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/congee.jpg
--------------------------------------------------------------------------------
/images/demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/demo.jpg
--------------------------------------------------------------------------------
/images/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/demo.png
--------------------------------------------------------------------------------
/images/eche-editor-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/eche-editor-screenshot.png
--------------------------------------------------------------------------------
/images/echoesworks.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/echoesworks.jpg
--------------------------------------------------------------------------------
/images/elasticsearch_ionic_info_page.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/elasticsearch_ionic_info_page.jpg
--------------------------------------------------------------------------------
/images/elasticsearch_ionic_map.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/elasticsearch_ionic_map.jpg
--------------------------------------------------------------------------------
/images/elasticsearch_ionit_map.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/elasticsearch_ionit_map.jpg
--------------------------------------------------------------------------------
/images/finally-brand.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/finally-brand.jpg
--------------------------------------------------------------------------------
/images/full-platforms.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/full-platforms.jpg
--------------------------------------------------------------------------------
/images/general-province-city-map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/general-province-city-map.png
--------------------------------------------------------------------------------
/images/geopoly2d-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/geopoly2d-small.png
--------------------------------------------------------------------------------
/images/gmap-solr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/gmap-solr.jpg
--------------------------------------------------------------------------------
/images/gnu-linux.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/gnu-linux.jpg
--------------------------------------------------------------------------------
/images/growth-arch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/growth-arch.png
--------------------------------------------------------------------------------
/images/growth-full-platforms.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/growth-full-platforms.jpg
--------------------------------------------------------------------------------
/images/hexoarch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/hexoarch.png
--------------------------------------------------------------------------------
/images/iot-3-layer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/iot-3-layer.jpg
--------------------------------------------------------------------------------
/images/iot-layer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/iot-layer.jpg
--------------------------------------------------------------------------------
/images/iot-server.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/iot-server.jpg
--------------------------------------------------------------------------------
/images/iphone.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/iphone.jpg
--------------------------------------------------------------------------------
/images/lan-struct.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/lan-struct.png
--------------------------------------------------------------------------------
/images/luffa.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/luffa.jpg
--------------------------------------------------------------------------------
/images/lz.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/lz.jpg
--------------------------------------------------------------------------------
/images/man-git.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/man-git.png
--------------------------------------------------------------------------------
/images/map_with_bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/map_with_bg.jpg
--------------------------------------------------------------------------------
/images/mpu6050.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/mpu6050.jpg
--------------------------------------------------------------------------------
/images/node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/node.png
--------------------------------------------------------------------------------
/images/oculus-vr.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/oculus-vr.jpg
--------------------------------------------------------------------------------
/images/onmap-demo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/onmap-demo.jpg
--------------------------------------------------------------------------------
/images/province-hash-with-map.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/province-hash-with-map.jpg
--------------------------------------------------------------------------------
/images/refactor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/refactor.png
--------------------------------------------------------------------------------
/images/sherlock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/sherlock.png
--------------------------------------------------------------------------------
/images/start.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/start.jpg
--------------------------------------------------------------------------------
/images/struct.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/struct.png
--------------------------------------------------------------------------------
/images/tdd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/tdd.png
--------------------------------------------------------------------------------
/images/tech-stack.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/tech-stack.jpg
--------------------------------------------------------------------------------
/images/tips.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/tips.jpg
--------------------------------------------------------------------------------
/images/travis-edit-publish-code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/travis-edit-publish-code.png
--------------------------------------------------------------------------------
/images/tree.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/tree.jpg
--------------------------------------------------------------------------------
/images/vmap-click-handler.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/vmap-click-handler.jpg
--------------------------------------------------------------------------------
/images/wechat-pay.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/wechat-pay.png
--------------------------------------------------------------------------------
/images/wechat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/wechat.jpg
--------------------------------------------------------------------------------
/images/xiaomiquan.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/images/xiaomiquan.jpg
--------------------------------------------------------------------------------
/img/cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/img/cover.jpg
--------------------------------------------------------------------------------
/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/ideabook/867c873b9c494838d68d0d8ac80404499459c41e/img/favicon.ico
--------------------------------------------------------------------------------
/listings-setup.tex:
--------------------------------------------------------------------------------
1 | % Contents of listings-setup.tex
2 | \usepackage{xcolor}
3 |
4 | \lstset{
5 | basicstyle=\ttfamily,
6 | numbers=left,
7 | keywordstyle=\color[rgb]{0.13,0.29,0.53}\bfseries,
8 | stringstyle=\color[rgb]{0.31,0.60,0.02},
9 | commentstyle=\color[rgb]{0.56,0.35,0.01}\itshape,
10 | numberstyle=\footnotesize,
11 | stepnumber=1,
12 | numbersep=5pt,
13 | backgroundcolor=\color[RGB]{248,248,248},
14 | showspaces=false,
15 | showstringspaces=false,
16 | showtabs=false,
17 | tabsize=2,
18 | captionpos=b,
19 | breaklines=true,
20 | breakatwhitespace=true,
21 | breakautoindent=true,
22 | escapeinside={\%*}{*)},
23 | linewidth=\textwidth,
24 | basewidth=0.5em,
25 | }
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | h1,
2 | h2,
3 | h3,
4 | h4,
5 | h5,
6 | h6,
7 | p,
8 | blockquote {
9 | margin: 0;
10 | padding: 0;
11 | }
12 |
13 | body {
14 | font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif;
15 | font-size: 18px;
16 | line-height: 1.4;
17 | color: #000000;
18 | margin: 10px 13px 10px 13px;
19 | }
20 |
21 | a {
22 | color: #0069d6;
23 | }
24 |
25 | a:hover {
26 | color: #0050a3;
27 | text-decoration: none;
28 | }
29 |
30 | a img {
31 | border: none;
32 | }
33 |
34 | p {
35 | text-indent: 2em;
36 | margin-bottom: 12px;
37 | }
38 |
39 | h1,
40 | h2,
41 | h3,
42 | h4,
43 | h5,
44 | h6 {
45 | color: #404040;
46 | line-height: 36px;
47 | }
48 |
49 | h1 {
50 | margin-bottom: 32px;
51 | margin-top: 32px;
52 | font-size: 30px;
53 | color: #0050a3;
54 | }
55 |
56 | h1 a {
57 | margin-top: 100px;
58 | display: block;
59 | }
60 |
61 | h2 {
62 | font-size: 28px;
63 | margin-bottom: 28px;
64 | }
65 |
66 | h3 {
67 | font-size: 24px;
68 | margin-bottom: 24px;
69 | }
70 |
71 | h4 {
72 | font-size: 20px;
73 | margin-bottom: 20px;
74 | }
75 |
76 | h5 {
77 | font-size: 16px;
78 | }
79 |
80 | h6 {
81 | font-size: 13px;
82 | }
83 |
84 | hr {
85 | margin: 0 0 19px;
86 | border: 0;
87 | border-bottom: 1px solid #ccc;
88 | }
89 |
90 | blockquote {
91 | padding: 2px 13px 21px 2px;
92 | margin-bottom: 12px;
93 | font-family: georgia,serif;
94 | font-style: italic;
95 | }
96 |
97 | blockquote:before {
98 | content: "\201C";
99 | font-size: 40px;
100 | margin-left: -10px;
101 | font-family: georgia,serif;
102 | color: #eee;
103 | }
104 |
105 | blockquote p {
106 | font-size: 14px;
107 | font-weight: 300;
108 | line-height: 18px;
109 | margin-bottom: 0;
110 | font-style: italic;
111 | }
112 |
113 | code, pre {
114 | font-family: Monaco, Andale Mono, Courier New, monospace;
115 | }
116 |
117 | code {
118 | background-color: #fee9cc;
119 | color: rgba(0, 0, 0, 0.75);
120 | padding: 1px 3px;
121 | font-size: 12px;
122 | -webkit-border-radius: 3px;
123 | -moz-border-radius: 3px;
124 | border-radius: 3px;
125 | }
126 |
127 | pre {
128 | display: block;
129 | padding: 14px;
130 | margin: 0 0 18px;
131 | line-height: 16px;
132 | font-size: 11px;
133 | border: 1px solid #d9d9d9;
134 | white-space: pre-line;
135 | word-wrap: break-word;
136 | }
137 |
138 | pre code {
139 | background-color: #fff;
140 | color: #737373;
141 | font-size: 11px;
142 | padding: 0;
143 | white-space: pre-wrap;
144 | }
145 |
146 | figure img {
147 | display: block;
148 | margin: 0 auto;
149 | max-width: 100%;
150 | }
151 |
152 | figcaption {
153 | text-align: center;
154 | }
155 |
156 | ul li a {
157 | font-weight: bold;
158 | }
159 |
160 | ul li ul a {
161 | font-weight: normal;
162 | }
163 |
164 | @media screen and (min-width: 768px) {
165 | body {
166 | width: 748px;
167 | margin: 40px auto;
168 | }
169 | }
170 |
171 |
172 | table {
173 | margin: 10px auto;
174 | }
175 |
176 | thead {
177 | font-size: 120%;
178 | font-weight: 1000;
179 | cursor: pointer;
180 | background: #c9dff0
181 | }
182 |
183 | thead tr th {
184 | text-align: center;
185 | font-weight: bold;
186 | padding: 12px 30px;
187 | padding-left: 42px
188 | }
189 |
190 | thead tr th span {
191 | padding-right: 20px;
192 | background-repeat: no-repeat;
193 | background-position: 100% 100%
194 | }
195 |
196 | tbody tr td {
197 | padding: 15px 10px
198 | }
199 |
200 | tbody tr td.lalign {
201 | text-align: left
202 | }
203 |
204 | tbody tr:nth-child(even) {
205 | background: #fff
206 | }
207 |
208 | tbody tr:nth-child(odd) {
209 | background: #eee
210 | }
211 |
212 | td,th {
213 | border-left: 1px solid #cbcbcb;
214 | border-width: 0 0 0 1px;
215 | font-size: inherit;
216 | margin: 0;
217 | overflow: visible;
218 | padding: .5em 1em
219 | }
--------------------------------------------------------------------------------
/template/template.tex:
--------------------------------------------------------------------------------
1 | \documentclass[a4paper, 11pt]{article}
2 | \usepackage{geometry} % 設定邊界
3 | \geometry{
4 | top=1in,
5 | inner=1in,
6 | outer=1in,
7 | bottom=1in,
8 | headheight=3ex,
9 | headsep=2ex
10 | }
11 | \usepackage{tabu}
12 | \usepackage[T1]{fontenc}
13 | \usepackage{lmodern}
14 | \usepackage{booktabs}
15 | \usepackage{amssymb,amsmath}
16 | \usepackage{ifxetex,ifluatex}
17 | \usepackage{fixltx2e} % provides \textsubscript
18 | % use upquote if available, for straight quotes in verbatim environments
19 | \IfFileExists{upquote.sty}{\usepackage{upquote}}{}
20 | \ifnum 0\ifxetex 1\fi\ifluatex 1\fi=0 % if pdftex
21 | \usepackage[utf8]{inputenc}
22 | $if(euro)$
23 | \usepackage{eurosym}
24 | $endif$
25 | \else % if luatex or xelatex
26 | \usepackage{fontspec} % 允許設定字體
27 | \usepackage{xeCJK} % 分開設置中英文字型
28 | \setCJKmainfont{STSong} % 設定中文字型
29 | \setmainfont[Mapping=tex-text]{Times New Roman}%\rmfamily 使用的字体,默认英文和数字的字体。 % 設定英文字型
30 | \setromanfont{Georgia} % 字型
31 | \setmonofont{Courier New}
32 | \linespread{1.2}\selectfont % 行距
33 | \XeTeXlinebreaklocale "zh" % 針對中文自動換行
34 | \XeTeXlinebreakskip = 0pt plus 1pt % 字與字之間加入0pt至1pt的間距,確保左右對整齊
35 | \parindent 0em % 段落縮進
36 | \setlength{\parskip}{20pt} % 段落之間的距離
37 | \ifxetex
38 | \usepackage{xltxtra,xunicode}
39 | \fi
40 | \defaultfontfeatures{Mapping=tex-text,Scale=MatchLowercase}
41 | \newcommand{\euro}{€}
42 | $if(mainfont)$
43 | \setmainfont{$mainfont$}
44 | $endif$
45 | $if(sansfont)$
46 | \setsansfont{$sansfont$}
47 | $endif$
48 | $if(monofont)$
49 | \setmonofont{$monofont$}
50 | $endif$
51 | $if(mathfont)$
52 | \setmathfont{$mathfont$}
53 | $endif$
54 | \fi
55 | % use microtype if available
56 | \IfFileExists{microtype.sty}{\usepackage{microtype}}{}
57 | $if(geometry)$
58 | \usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry}
59 | $endif$
60 | $if(natbib)$
61 | \usepackage{natbib}
62 | \bibliographystyle{plainnat}
63 | $endif$
64 | $if(biblatex)$
65 | \usepackage{biblatex}
66 | $if(biblio-files)$
67 | \bibliography{$biblio-files$}
68 | $endif$
69 | $endif$
70 | $if(listings)$
71 | \usepackage{listings}
72 | $endif$
73 | $if(lhs)$
74 | \lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
75 | $endif$
76 | $if(highlighting-macros)$
77 | $highlighting-macros$
78 | $endif$
79 | $if(verbatim-in-note)$
80 | \usepackage{fancyvrb}
81 | $endif$
82 | $if(tables)$
83 | \usepackage{longtable}
84 | $endif$
85 |
86 | \usepackage{graphicx}
87 | \usepackage{caption}
88 | % We will generate all images so they have a width \maxwidth. This means
89 | % that they will get their normal width if they fit onto the page, but
90 | % are scaled down if they would overflow the margins.
91 | \makeatletter
92 | \def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth
93 | \else\Gin@nat@width\fi}
94 | \makeatother
95 | \let\Oldincludegraphics\includegraphics
96 | \renewcommand{\includegraphics}[1]{\Oldincludegraphics[width=\maxwidth]{#1}}
97 | \ifxetex
98 | \usepackage[setpagesize=false, % page size defined by xetex
99 | unicode=false, % unicode breaks when used with xetex
100 | xetex]{hyperref}
101 | \else
102 | \usepackage[unicode=true]{hyperref}
103 | \fi
104 | \hypersetup{breaklinks=true,
105 | bookmarks=true,
106 | pdfauthor={$author-meta$},
107 | pdftitle={$title-meta$},
108 | colorlinks=true,
109 | urlcolor=$if(urlcolor)$$urlcolor$$else$blue$endif$,
110 | linkcolor=$if(linkcolor)$$linkcolor$$else$magenta$endif$,
111 | pdfborder={0 0 0}}
112 | \urlstyle{same} % don't use monospace font for urls
113 | $if(links-as-notes)$
114 | % Make links footnotes instead of hotlinks:
115 | \renewcommand{\href}[2]{#2\footnote{\url{#1}}}
116 | $endif$
117 | $if(strikeout)$
118 | \usepackage[normalem]{ulem}
119 | % avoid problems with \sout in headers with hyperref:
120 | \pdfstringdefDisableCommands{\renewcommand{\sout}{}}
121 | $endif$
122 | \setlength{\parindent}{0pt}
123 | %\setlength{\parskip}{6pt plus 2pt minus 1pt}
124 | \setlength{\emergencystretch}{3em} % prevent overfull lines
125 | \usepackage{titling}
126 | \setlength{\droptitle}{-8em} % 將標題移動至頁面的上面
127 |
128 | \usepackage{fancyhdr}
129 | \usepackage{lastpage}
130 | \pagestyle{fancyplain}
131 |
132 | $if(numbersections)$
133 | \setcounter{secnumdepth}{5}
134 | $else$
135 | \setcounter{secnumdepth}{0}
136 | $endif$
137 | $if(verbatim-in-note)$
138 | \VerbatimFootnotes % allows verbatim text in footnotes
139 | $endif$
140 | $if(lang)$
141 | \ifxetex
142 | \usepackage{polyglossia}
143 | \setmainlanguage{$mainlang$}
144 | \else
145 | \usepackage[$lang$]{babel}
146 | \fi
147 | $endif$
148 | $for(header-includes)$
149 | $header-includes$
150 | $endfor$
151 |
152 | $if(title)$
153 | \title{$title$}
154 | $endif$
155 | \author{$for(author)$$author$$sep$ \and $endfor$}
156 | \date{$date$}
157 |
158 | %%%% 段落首行缩进两个字 %%%%
159 | \makeatletter
160 | \let\@afterindentfalse\@afterindenttrue
161 | \@afterindenttrue
162 | \makeatother
163 | \setlength{\parindent}{2em} %中文缩进两个汉字位
164 |
165 |
166 | %%%% 下面的命令重定义页面边距,使其符合中文刊物习惯 %%%%
167 | \addtolength{\topmargin}{-2pt}
168 | \setlength{\oddsidemargin}{0.63cm} % 3.17cm - 1 inch
169 | \setlength{\evensidemargin}{\oddsidemargin}
170 | \setlength{\textwidth}{14.66cm}
171 | \setlength{\textheight}{24.00cm} % 24.62
172 |
173 | %%%% 下面的命令设置行间距与段落间距 %%%%
174 | \linespread{1.4}
175 | % \setlength{\parskip}{1ex}
176 | \setlength{\parskip}{0.5\baselineskip}
177 |
178 |
179 | \begin{document}
180 | %%%% 定理类环境的定义 %%%%
181 | \newtheorem{example}{例} % 整体编号
182 | \newtheorem{algorithm}{算法}
183 | \newtheorem{theorem}{定理}[section] % 按 section 编号
184 | \newtheorem{definition}{定义}
185 | \newtheorem{axiom}{公理}
186 | \newtheorem{property}{性质}
187 | \newtheorem{proposition}{命题}
188 | \newtheorem{lemma}{引理}
189 | \newtheorem{corollary}{推论}
190 | \newtheorem{remark}{注解}
191 | \newtheorem{condition}{条件}
192 | \newtheorem{conclusion}{结论}
193 | \newtheorem{assumption}{假设}
194 |
195 | \newcommand{\tightlist}{%
196 | \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}}
197 |
198 | %%%% 重定义 %%%%
199 | \renewcommand{\contentsname}{目录} % 将Contents改为目录
200 | \renewcommand{\abstractname}{摘要} % 将Abstract改为摘要
201 | \renewcommand{\refname}{参考文献} % 将References改为参考文献
202 | \renewcommand{\indexname}{索引}
203 | \renewcommand{\figurename}{图}
204 | \renewcommand{\tablename}{表}
205 | \renewcommand{\appendixname}{附录}
206 |
207 | $for(include-before)$
208 | $include-before$
209 |
210 | $endfor$
211 | $if(toc)$
212 | {
213 | \newpage
214 | \hypersetup{linkcolor=black}
215 | \setcounter{tocdepth}{$toc-depth$}
216 | \tableofcontents
217 | }
218 | \newpage
219 | $endif$
220 | $body$
221 |
222 | $if(natbib)$
223 | $if(biblio-files)$
224 | $if(biblio-title)$
225 | $if(book-class)$
226 | \renewcommand\bibname{$biblio-title$}
227 | $else$
228 | \renewcommand\refname{$biblio-title$}
229 | $endif$
230 | $endif$
231 | \bibliography{$biblio-files$}
232 |
233 | $endif$
234 | $endif$
235 | $if(biblatex)$
236 | \printbibliography$if(biblio-title)$[title=$biblio-title$]$endif$
237 |
238 | $endif$
239 | $for(include-after)$
240 | $include-after$
241 |
242 | $endfor$
243 | \end{document}
--------------------------------------------------------------------------------