使用 certbot 给网站设置 let’s encrypt 免费证书

certbot 现在已经智能到只在命令行运行就能配置成功了,不需要手工验证证书,不需要修改nginx配置。以 ubuntu 16.4 + Nginx 为例:

1. 首先安装 CERTBOT

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update
$ sudo apt-get install python-certbot-nginx 

2. 申请 LET’S ENCRYPT 免费证书

$ sudo certbot 
  • 会列出所有已配置的域名列表,域名前面是序号

  • 根据提示信息,输入用空格或者逗号分隔的序号

  • 如果一切正常,会提示证书已经安装,询问是否修改 nginx 配置文件强制 http 跳转到 https

  • 设置结束,提示你去 ssllabs.com 检测证书是否正常

以上就是设置全部的步骤。

3. 更新

更新命令是

$ sudo certbot renew --post-hook "service nginx reload"

更新完会使用post-hook重新加载 nginx 配置文件。

如果想要自动更新添加到定时任务就可以了:

$crontab -e

加上这一行

00 00 * * 1     certbot renew --post-hook "service nginx reload"

这样每周都会去检查一次

怎样用一天时间,开发上架一个天气小程序

早上醒来,我不愿意回想昨天温度多少度,只想要知道今天比昨天热还是冷,适当增减衣服就行了。穿衣指数什么的根本不适合我,污染指数也没啥用,难道我能不上班嘛?

那么我的需求就是有个天气应用,告诉我今天和昨天天气对比就行了。

历史天气接口不好找,我花了几个小时搜了国内外十几个天气API,很少有历史天气查询,有的也是付费服务。免费天气预报接口倒是很多。

为一个没有几个人用的小程序付费购买接口太奢侈了,这时想到一个绝妙的(笨)方法:查询到今天天气以后,缓存起来,明天再来看就有昨日天气了!

说干就干。

1. 注册小程序

小程序注册入口在这里 https://mp.weixin.qq.com/wxopen/waregister?action=step1 填写基本信息后,验证邮箱和微信,就能登录管理后台了。

在管理后台填写小程序名称、介绍和头像,会自动生成小程序码。
在开发设置页面可以看到AppID(小程序ID),记住这个 AppID。

2. 使用微信开发者工具

微信开发者工具在这里下载 https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html 下载后,用微信扫码打开,创建项目,这需要填入刚才的AppID。


假设你已经知道了微信开发的基础,代码应该有类似的结构。

3. 获取位置信息

(注:和风天气支持经纬度查天气,第3步是可选步骤)

要预报当地天气,需要知道位置,微信小程序有 wx.getLocation 可以获取经纬度。

wx.getLocation({
  type: 'wgs84',
  success: function(res) {
    var latitude = res.latitude
    var longitude = res.longitude
    var speed = res.speed
    var accuracy = res.accuracy
  }
})

但是天气查询的接口很多都不支持经纬度查询。我们需要用经纬度查城市名,这次使用腾讯地图的API。

我们需要的接口就是这个页面 http://lbs.qq.com/qqmap_wx_jssdk/method-reverseGeocoder.html 中的 reverseGeocoder,用经纬度可以获取详细的城市信息,但是我们只需要城市名就可以了。

要使用接口,先要注册腾讯lbs服务的开发者,页面右上角用QQ号登录,然后验证手机号,然后就可以调用接口了。

在小程序中使用前,要在小程序设置界面,开发设置中添加request合法域名: http://apis.map.qq.com 。

// 先在项目中引入微信小程序JavaScript SDK
var QQMapWX = require('../../lib/qqmap-wx-jssdk.min.js');

// onLoad中,实例化API核心类
  qqmapsdk = new QQMapWX({
    key: 'QQ lbs 开发者密钥'
  });


// 在 onShow 中获取城市名
qqmapsdk.reverseGeocoder({
  location: {
    latitude: latitude, // 使用 wx.getLocation 的返回值
    longitude: longitude
  },
  success: function (res) {
    console.log(res);
    if(res.status == 0){
      // 获取到城市名
      let city_name = res.result.ad_info.city;
    }
  },
  fail: function (res) {
    console.log(res);
  },
  complete: function (res) {
  //  console.log(res);
  }
});

4. 查询天气

得到城市名后,再用城市名查询天气的接口,得到未来三天天气预报。

天气接口使用和风天气 https://www.heweather.com/douments/api/s6/weather-forecast 。和风天气的接口比较简洁,返回值也有中文描述可以直接显示。免费版的天气信息足够多。历史天气接口需要付费,我们先用免费的接口。

同上,要使用接口,需要先注册开发者账户,验证手机。

在小程序中使用前,要在小程序设置界面,开发设置中添加request合法域名: http://free-api.heweather.com 。

wx.request({
    url:'https://free-api.heweather.com/s6/weather/forecast',
    data:{
      location:location,
      key: '和风天气开发者密钥',
      rnd:new Date().getTime() // 随机数,防止请求缓存
    },
    success:function(res){
      console.log(res);
    }
  })

拿到天气以后在本地做缓存,最多只存两天的记录。

5. 美化前端界面

对前端程序员来说,设计酷炫的界面有点难,但是基本的审美还是有的。

用关键词 “simple weather app” 在 Google 搜图片,出来的看起来舒服的界面,借用一下配色。

搜索结果中还发现一个可爱的logo,还是免费的!只有一条要求,需要在使用时展示这个网站的链接,因为是小程序,不能外链,我放了文本格式的网站地址,就是这个 https://www.freepik.com/free-vector/simple-weather-app_874144.htm 。


做好的界面。
晚上又优化了一下代码,还在12点前后做了测试,修改了几个问题,就提交审核了。

6. 测试小程序

就算是这么小的项目,测试也必不可少。经过测试发现和风天气的返回值,是未来三天的天气数组,12月7日晚上调用返回的结构与API一样,包含了[12-7,12-8,12-9]的天气。

和风天气接口测试

• 问题1:但是过了午夜12点以后,返回的仍然是[12-7,12-8,12-9],就不能随便的使用 arr[0] 当作今日天气了。

• 问题2:早上起床不到8点,看审核还没通过,再调试一次看看,这次调用返回的数组只有一个天气[12-8],倒是有今天了,明天后天是没有的,好在我现在还不需要。

7. 审核通过

八点又看了一下,上面的API问题不会影响程序。一个小时以后,审核通过了,

审核后,兴奋的发给朋友试用。现在才发现一个重要的问题,如果哪天没打开,第二天就没有昨日天气了,需要每天都打开一次!真希望有免费的历史天气接口啊,哪怕只有简化的昨日天气也行啊。


如果你想试用,可以在微信搜“昨日天气”小程序。如果这个需求很多,可能我会考虑买付费的历史天气接口。

后记

嗯,哪天想起来这个小程序了,还不能用…… 那你比别的天气应用,有什么区别呢。

说起来跟别的应用比较,微信可以直接搜天气,手机也都有天气app,包含昨日天气的也很多,小程序的优势在哪里呢? 看起来打开app的路径,并不比小程序复杂啊!

React-Native版来了 怎样用一天时间,开发发布一个天气App

相关链接

小程序注册
小程序开发工具
腾讯地图lbs服务
和风天气接口
Designed by Freepik

什么是关键渲染路径

这是去年看过 pagespeed insights 以后做的一个简单的分享,重新整理发布。

回顾浏览器渲染过程

  1. 加载HTML
  2. 加载资源
  3. 解析
  4. 渲染

页面HTML结构 (http://jslab.pro/demo/critical_rendering_path/index.html

页面加载过程


(注:紫色是 DomContentLoaded,红色是 Load)

DOM树解析过程,Document Object Model (DOM)

  • Conversion字节码根据编码转换成字符
  • Tokenizing将字符串转化为特殊的标签,如“<html>”、“<body>”
  • Lexing将标签转换成“对象”,定义他们的属性和规则
  • DOM construction根据HTML定义的不同标签的关系(嵌套等),将创建的对象链接到一个树形的数据结构中:HTML对象是Body对象的父级,Body是P的父级等。

最终生成的DOM树

CSSOM 构建过程 CSS Object Model (CSSOM)

页面CSS样式

CSSOM 构建过程

CSSOM

生成渲染树,Render tree

  1. 从DOM树根节点开始,遍历每个可见节点。
  2. 每个可见节点找到并应用对应的CSSOM规则。
  3. 返回计算好样式的内容

Reflow(Gecko) Layout(webkit) 

根据设备的 viewport 计算每个元素该怎样展示。

<meta name="viewport" content="width=device-width,initial-scale=1">

最后一步,绘制页面,Paint

总结解析渲染步骤:

  1. 处理HTML标签构建DOM树。
  2. 处理CSS标签构建CSSOM树。
  3. DOM和CSSOM树被组合形成 Render tree 渲染树(渲染树只包含需要显示在页面的节点)。
  4. Layout(reflow)计算每个对象的确切位置和大小。
  5. 最后是绘制(Paint),把最终的渲染树对象渲染成屏幕像素。

优化关键渲染路径,Critical Rendering Path

  • Critical必需的,关键的
  • Render渲染
  • Path浏览器显示网页(初始视图initial view )所必须的一系列过程。(初始视图也称为首屏 above the fold,用户不需要滚动就能看到的内容)

所以关键渲染路径的定义就是:浏览器显示网页所必须的一系列事件

路径(the Path)

  1. 浏览器下载HTML文件
  2. 浏览器读取HTML,看到有CSS文件、JavaScript文件、图像(解析HTML,遇到CSS资源,加载CSS、解析CSS,继续解析HTML;遇到JavaScript过程相同)
  3. 浏览器开始下载资源
  4. 浏览器决定没有获取CSS和JavaScript的时候不能显示网页
  5. 浏览器下载到CSS文件并读取,并且没有调用其他内容
  6. 浏览器得到JavaScript之前,仍不会展示网页
  7. 浏览器下载JavaScript并读取,并且没有调用其他内容
  8. 浏览器现在决定可以显示网页

这只是一个最简单的网页加载过程,如果页面有很多资源加载过程就会很复杂。

渲染(the Render)

只有很少的资源会阻塞网页渲染,最常见的就是 CSS 和 JavaScript 文件。

浏览器必须把这些资源全部加载并解析完毕才回开始渲染页面。

举个例子:

1,页面有10个CSS文件,浏览器渲染前,会等待这10个CSS文件全部下载、解析完毕。此时浏览器在等待“关键”的步骤时空白页面。

2,页面还有20个JS文件,CSS都下载解析后,也不会开始渲染,还需要等待这20个JS的加载和解析。

(其他还有什么会阻塞?HTML,字体文件等)
(浏览器会逐步解析、逐步渲染,这就是首屏内容可以单独优化的原因)

关键内容(the Critical)(above the fold)(初始视图initial view )

关键内容 = 初始内容 = 首屏

只要保证首屏尽可能快的显示出来,就算页面有1000张图片,100个JavaScript也没有问题。

怎样优化关键渲染路径?

首先找到渲染(首屏)必需的步骤。然后尽可能的:

  • 减少关键的资源;
  • 减少关键的字节数;
  • 减少关键的路径长度。

1,HTML:精简、压缩、缓存
2,CSS:Render blocking CSS,media query 只加载需要的样式,内联CSS

3,Javascript:Parser blocking Javascript,延时执行,async

渲染步骤 DOM->CSSOM->运行(内联)JS->重新构建DOM

外链JS,要先等待下载,执行再继续解析页面。

<script src=”…” async></script> 不阻塞DOM和CSSOM构建

  • Blocking: <script src=”anExteralScript.js”></script>
  • Inline: <script>document.write(“this is an inline script”)</script>
  • Async: <script async src=”anExternalScript.js”></script>

另外:图片不阻塞DOM构建

下面来看一个实际的例子

<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link href="style.css" rel="stylesheet">
</head>
<body>
<p>Hello <span>web performance</span> students!</p>
<div><img src="awesome-photo.jpg"></div>
<script src="app.js"></script>
</body>
</html>

 

第一次修改

<script src=”app.js” async></script>

第二次修改

<link href=”style.css” rel=”stylesheet” media=”print”>

不加载或者内联CSS

(PS:实际上,浏览器会提前下载资源,但是执行还是按照定义的顺序)

(PPS:浏览器会逐步解析、逐步渲染,这就是首屏内容可以单独优化的原因)

案例,分析一个网站的关键渲染路径

http://jslab.pro/demo/critical_rendering_path/index.html
http://jslab.pro/demo/critical_rendering_path/async.html

总结

关键渲染路径的定义

浏览器显示网页所必须的一系列事件。

优化方法

  • 减少关键的资源
  • 减少关键的字节数
  • 减少关键的路径长度

附录

下面是收集的一些其他优化方法还有最佳实践。

优化步骤

先测量,找到瓶颈,再优化。

not all requests are made equal
not all bytes are made equal

—— Paul Irish https://www.igvita.com/

优化技巧:

图片——就算页面有1000张图片,只加载关键的(首屏所需的)3张。其他的9997张都可以延迟加载或者滚屏加载(lazy Load)。

JavaScript文件——可以有100个JS文件,但是渲染头部只需要一个,就延迟加载剩下的99个。

CSS文件——可以有多种做法,合并请求、压缩文件、内联首屏样式

其他手段

异步加载HTML、HTML延迟渲染、内联CSS的缓存处理,减少首屏需要加载的字节数,控制首屏所需字节先加载(第一个14KB)

网页速度的度量

网页速度并不是页面展示下载的字节数,也不是整个页面的大小。要关注的是用户的体验,网页最快可用时间,或者首屏开始渲染时间,才是最重要的。

天猫首页源代码——2016年11月16日,首屏以外的DOM节点动态生成

淘宝首页源代码——2016年11月16日,inline关键CSS,其他CSS异步加载

参考链接:

Chrome Dev tools 查看页面加载

https://developers.google.com/web/tools/chrome-devtools/network-performance/resource-loading

学会看Chrome Dev tools 的 timeline

https://developers.google.com/web/tools/chrome-devtools/evaluate-performance/timeline-tool

一个资源的请求过程

https://developers.google.com/web/tools/chrome-devtools/network-performance/understanding-resource-timing

above the fold 定义

https://en.wikipedia.org/wiki/Above_the_fold

https://www.smashingmagazine.com/2009/09/10-useful-usability-findings-and-guidelines/

延迟加载图片

https://varvy.com/pagespeed/defer-images.html

都有谁在阻塞渲染

https://www.keycdn.com/blog/blocking-the-dom/

阻塞渲染的CSS

https://varvy.com/pagespeed/render-blocking-css.html

阻塞渲染的JavaScript

https://varvy.com/pagespeed/render-blocking.html

async的支持情况(IE10+)

http://caniuse.com/#search=async

网络字体阻塞渲染

http://ianfeather.co.uk/web-fonts-and-the-critical-path/

优化首屏显示

https://varvy.com/pagespeed/prioritize-visible-content.html

First RTT 14KB

https://docs.google.com/presentation/d/1MtDBNTH1g7CZzhwlJ1raEJagA8qM3uoV7ta6i66bO2M/present?slide=id.g3eb97ca8f_10

speed index 速度指标

https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index

https://gtmetrix.com/

谷歌的 pagespeed insights 对图片有损压缩

http://stackoverflow.com/questions/41313719/google-pagespeed-insights-optimize-images-running-new-image-compression

http://stackoverflow.com/questions/5451597/how-does-googles-page-speed-lossless-image-compression-work

Evernote 怎样导出到 Word

最近需要将稿件从Evernote导出到Word,文章里面有复杂的格式,还有插图等。很自然的查看文件->导出菜单,居然没有!一定是我哪里搞错了,去Evernote官方论坛看看,一搜不得了,这个问题5、6年前就有了,而且每年都有人重提,官方并没有任何动作!

但是事情还是要做的,就查找替代方案,首先想到的复制粘贴不可行,图片会丢掉。

导出HTML再复制粘贴也不好,HTML文件在word里打开还是会有样式问题。

最后经过搜寻,找到一个稍微麻烦的方法,但是格式总算是完整保留下来了。那么怎样把文档从Evernote导出到Word呢?答案就是用OneNote中转,以下是详细步骤:

用 OneNote 转存文档

保存文档到 OneNote

http://www.onenote.com/import-evernote-to-onenote页面下载 OneNote importer 工具,点开后直接导入Evernote的文档和笔记本。

注意如果文档比较多可能要花点时间,我是说,就算软件显示导入成功了,打开 OneNote 也可能看不到文章,等一会儿就有了。

这个导入工具还可以直接导入Evernote 导出的 .enex 文件,没有安装Evernote的时候也可以导入。

从 OneNote 导出笔记为 Word 格式

这一步没什么好说的,文件 -> 导出 -> 页面 -> Word 文档(.docx)。

现在用 Word 打开刚才导出的文档,是不是格式、图片都在?

用 Word 打开后可以看到 Evernote 换行和回车混用的情况很多啊

直接用 Evernote 写文字会经常出现同步错误的文档,我已经放弃在里面直接写内容了。明年也不准备续费了。

使用A-Frame从零开始做一个 Web 3D 互动游戏

不用学习新的语言,只有前端现有的知识就可以做AR了。而且是在我们熟悉的Web环境中,不用APP就可以做到。

1. 什么是A-Frame

这个框架的命名跟移动4G的“和”有得一拼,结果都是完美错过所有关键词。
A 会被浏览器忽略 – 是连词符,frame又有太多歧义。如果打出来 aframe 还会被浏览器“智能”拆分成 a frame 变成“一个框架”。
这就导致查资料不容易,沙里淘金的感觉,不过也可能是本身资料就少的缘故。

A-Frame 是一个可以在HTML中创建3d场景的框架,使用Three.js和WebGL来创建VR场景。
不需要了解低层渲染细节,但是A-Frame的说明文档也很长啊。

2. 最简单的demo

在codepen.io打开代码在codepen.io预览

<script src="https://aframe.io/releases/0.4.0/aframe.min.js"></script>

<a-scene>
  <a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
  <a-box position="-1 0.5 -3" rotation="0 45 0" width="1" height="1" depth="1" color="#4CC3D9"></a-box>
  <a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
  <a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
  
  <a-sky color="#ECECEC"></a-sky>
</a-scene>

代码中的 <a-sky> 就是纯色背景,其他几个看名字就知道是哪个了。
这个demo还可以用鼠标和键盘导航,如果用手机浏览器打开,就是VR效果了。

3. 从头做一个自己的场景

3.1 模板

A-Frame 的所有元素都放在 <a-scene> 中,初始代码如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Our First A-Frame Experience</title>
    <script src="https://aframe.io/releases/0.4.0/aframe.min.js"></script>
  </head>
  <body>
    <a-scene>
      
    </a-scene>
  </body>
</html>

3.2 天空

天空使用的元素是 <a-sky>,代码如下:

<a-sky color="#C500FF"></a-sky>

此时会产生一个紫红色的天空。天空也可以是一个全景图.
flickr 有很多全景图,我们选一个作为背景,比如这一张

现在把天空换成这张全景图。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>Our First A-Frame Experience</title>
    <script src="https://aframe.io/releases/0.4.0/aframe.min.js"></script>
  </head>
  <body>
    <a-scene>
      <a-sky src="https://c1.staticflickr.com/8/7376/16218590470_468084c950_h.jpg"></a-sky>
    </a-scene>
  </body>
</html>

现在代码(在codepen.io打开代码在codepen.io预览

现在用手机看效果是不是有身临其境的感觉了。

3.3 放一个球进去

<a-scene>
  <a-sky src="https://c1.staticflickr.com/8/7376/16218590470_468084c950_h.jpg"></a-sky>
  <a-sphere position="0 1.25 -5" radius="1.25" color="#66ffcc"></a-sphere>
</a-scene>

现在场景中多了一个蓝色的球,直接看效果

球也可以不是纯色的,这就需要给球表面贴图,我们先从 subtlepatterns 选一个材质,
这一张木纹吧:

<a-scene>
  <a-sky src="https://c1.staticflickr.com/8/7376/16218590470_468084c950_h.jpg"></a-sky>
  <a-sphere position="0 1.25 -5" radius="1.25" src="https://www.toptal.com/designers/subtlepatterns/patterns/retina_wood.png"></a-sphere>
</a-scene>

效果是这样的

3.4 光标交互

VR里也有对应的交互方案,我们现在增加动画和事件库。场景里增加一个camera和放在其中的curosr。

<script src="https://aframe.io/releases/0.4.0/aframe.min.js"></script>
<script src="https://npmcdn.com/aframe-animation-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-event-set-component@3.0.1"></script>

<a-scene>
  <a-sky src="https://c1.staticflickr.com/8/7376/16218590470_468084c950_h.jpg"></a-sky>
  <a-sphere position="0 1.25 -5" radius="1.25" src="https://www.toptal.com/designers/subtlepatterns/patterns/retina_wood.png"></a-sphere>

  <!-- Camera + cursor. -->
  <a-entity camera look-controls>
    <a-cursor id="cursor" 
        animation__click="property: scale; startEvents: click; from: 0.1 0.1 0.1; to: 1 1 1; dur: 150"
        animation__fusing="property: fusing; startEvents: fusing; from: 1 1 1; to: 0.1 0.1 0.1; dur: 1500"
        event-set__1="_event: mouseenter; color: #0092d8" 
        event-set__2="_event: mouseleave; color: black"></a-cursor>
  </a-entity>
</a-scene>

现在代码(在codepen.io查看代码在codepen.io预览)。

现在随着视口(摄像机)移动,在屏幕中央的光标(定位相对于摄像机固定)会跟着摄像机移动。

光标与鼠标一样遇到圆球,触发 mouseenter 事件,离开圆球触发 mouseleave 事件。现在我们已经增加了这两个事件,进入的时候光标变蓝色,离开变回默认的黑色。点击时伴随有光标缩放效果。

3.5 与场景中的球互动

就像鼠标点击按钮一样,这个光标也可以触发场景中的元素事件,不过这时就没有现成的代码库了,要自己写事件绑定。

<script src="https://aframe.io/releases/0.4.0/aframe.min.js"></script>
<script src="https://npmcdn.com/aframe-animation-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-event-set-component@3.0.1"></script>
<script>
    AFRAME.registerComponent('hide-on-click', {
        dependencies: ['raycaster'],
        schema: {
            target:{type: 'selector'}
        },
        init: function () {
            var data = this.data;
            var el = this.el;
            el.addEventListener('click', function () {
                el.setAttribute('visible', false);
                data.target.setAttribute('visible', true);
            });
        }
    });
</script>

<a-scene>
  <a-sky src="https://c1.staticflickr.com/8/7376/16218590470_468084c950_h.jpg"></a-sky>
  <a-sphere hide-on-click="target:#another_cube" position="0 1.25 -5" radius="1.25" src="https://www.toptal.com/designers/subtlepatterns/patterns/retina_wood.png"></a-sphere>
  <a-box id="another_cube" visible="false" position="-1 1.5 -4" rotation="0 45 0" width="1" height="1" depth="1" color="#4CC3D9"></a-box>

  <!-- Camera + cursor. -->
  <a-entity camera look-controls>
    <a-cursor id="cursor" 
        animation__click="property: scale; startEvents: click; from: 0.1 0.1 0.1; to: 1 1 1; dur: 150"
        animation__fusing="property: fusing; startEvents: fusing; from: 1 1 1; to: 0.1 0.1 0.1; dur: 1500"
        event-set__1="_event: mouseenter; color: #0092d8" 
        event-set__2="_event: mouseleave; color: black"></a-cursor>
  </a-entity>
</a-scene>

现在代码(在codepen.io打开代码在codepen.io预览)。

现在如果用PC浏览器看效果,鼠标左右拖动能移动摄像机(同时移动光标),鼠标单击任意位置,光标触发点击事件。点击圆球以后,圆球消失,正方体的盒子出现了。

4 做一个互动游戏

我的游戏过程是,进入页面在随机位置生成一个鸡蛋,用户用光标点击鸡蛋,鸡蛋隐藏,然后播放鸡蛋破壳动画,最后触发弹窗,提示用户你获得一只鸡。

4.1 基础代码

跟上面的差不多,只是去掉了绑定事件和圆球。因为球形太圆了,不像鸡蛋。只能用一张静态的鸡蛋图片代替。为什么不用椭圆形的3d元素呢,因为除了方形、圆柱、圆形、平面以外的物体,多边形太多了,占用内存太大。

要展示平面图形就需要另外一个库 aframe-html-shader。

<script src="https://aframe.io/releases/0.4.0/aframe.min.js"></script>
<script src="https://npmcdn.com/aframe-animation-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-event-set-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-layout-component@3.0.1"></script>
<script src="https://rawgit.com/mayognaise/aframe-html-shader/master/dist/aframe-html-shader.min.js"></script>

<script>
    AFRAME.registerComponent('hide-on-click', {
        dependencies: ['raycaster'],
        schema: {
            target:{type: 'selector'}
        },
        init: function () {
            var data = this.data;
            var el = this.el;
            el.addEventListener('click', function () {
                el.setAttribute('visible', false);
                data.target.setAttribute('visible', true);
                
            });
        }
    });
</script>

<div id="textToDisplay"><img src="./frame_1.png" alt=""></div>
<a-scene>
  <a-sky src="https://c1.staticflickr.com/8/7376/16218590470_468084c950_h.jpg"></a-sky>
  
    <a-entity hide-on-click="target:#chickenAnimate">
        <a-entity id="default_egg"
                  geometry="primitive:plane;width:2;height:2;"
                  position="-3 2 -10"
                  material="shader:html;target:#textToDisplay;transparent:true;fps:0;">
        </a-entity>
    </a-entity>
    <a-box id="chickenAnimate" visible="false" position="-1 1.5 -4" rotation="0 45 0" width="1" height="1" depth="1" color="#4CC3D9"></a-box>


  <!-- Camera + cursor. -->
  <a-entity camera look-controls>
    <a-cursor id="cursor" 
        animation__click="property: scale; startEvents: click; from: 0.1 0.1 0.1; to: 1 1 1; dur: 150"
        animation__fusing="property: fusing; startEvents: fusing; from: 1 1 1; to: 0.1 0.1 0.1; dur: 1500"
        event-set__1="_event: mouseenter; color: #0092d8" 
        event-set__2="_event: mouseleave; color: black"></a-cursor>
  </a-entity>
</a-scene>

上面的代码改动增加了一个div#textToDisplay 里面只有一张鸡蛋的图片,这张图会用来渲染到 a-entity#default_egg。语法是设置 matrial 的值。具体可参考
github aframe-html-shader 项目主页

这里有个问题,aframe-html-shader 原理是用canvas渲染DOM中的元素,如果有图片会需要由canvas处理,这就涉及到跨域问题。

跨域问题可以通过html2canvas-php-proxy解决,
但是在服务器部署这个代理有安全风险,所以我们的demo放到同一个域测试。

演示地址在: demo7

此时用光标点击鸡蛋,鸡蛋会消失,正方体出现。

4.2 播放动画

鸡蛋的动画也需要用canvas渲染出来,这里再引入一个库:

aframe-gif-shader github 地址

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://npmcdn.com/aframe-animation-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-event-set-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-layout-component@3.0.1"></script>
<script src="https://rawgit.com/mayognaise/aframe-html-shader/master/dist/aframe-html-shader.min.js"></script>
<script src="./aframe-gif-shader.js"></script>

然后把替代的正方体盒子换成孵蛋动画:

<a-entity id="chickenAnimate" visible="false">
    <a-entity id="animate_egg" geometry="primitive:plane;width:2;height:2;"
              position="-3 2 -10"
              material="shader:gif;src:url(./egg_2017_1.gif);transparent:true;"
              gif=""></a-entity>
</a-entity>

此处的gif渲染原理是用js加载并解析每一帧动画,并用canvas渲染出来。

这里有两个小坑:

  • aframe-gif-shader 组件中渲染每一帧之前,忘记清空画布,导致影响重叠。(难怪demo使用的都是不透明gif图片)我已经修复了这个问题,所以引用的是本地文件。
  • aframe-gif-shader 组件中使用了第三方的优化算法,但是这个优化算法存在bug,连续的两帧图片上部透明部分太多会被认为是多余帧优化掉。此处可以修改gif素材绕过。

脚本也增加一点内容,播放孵蛋的动画后,等3秒弹出alert,提醒用户游戏结果,然后重置游戏。

AFRAME.registerComponent('hide-on-click', {
    dependencies: ['raycaster'],
    schema: {
        target: {type: 'selector'}
    },
    init: function () {
        var data = this.data;
        var el = this.el;
        el.addEventListener('click', function () {
            el.setAttribute('visible', false);
            data.target.setAttribute('visible', true);
            setTimeout(function(){
                alert('恭喜您获得一只鸡!');
                window.location.reload();
            },5000);
        });
    }
});

完整的效果和源码请查看: demo8

4.3 增加趣味性,随机位置

现在游戏可以玩了,但是每次鸡蛋都处在统一位置,根本不用找。为了增加趣味性,我们随机改变鸡蛋的位置。
但是根据我的测试,完全随机的游戏一点都不好玩,太远了会变成几个像素大小,根本找不到,太近了有种糊在脸上的感觉。还有上下左右位置都不能太离谱。
多次测试后决定把位置放在用户站的位置周围固定距离的圆圈上,高度比身高多一点点的时候,比较合理。

我翻出来初中数学课本,找到了下面的公式:

var R = Math.random() * 360; // 弧度
var r = 8; // 半径
var X = Math.sin(R) * r; // x 坐标
var Z = Math.cos(R) * r; // 俯视图 z 坐标

$('#default_egg,#animate_egg').attr('position',{ x: X, y: 2, z: Z });

这下位置随机了,可是鸡蛋是平面图形,朝向必须面对摄像头才行,不然会有一种挨着窗户坐在教室第一排看黑板的感觉。

这时就需要另外一个库,从官方插件库中分出来的 kframe 插件库,其中有个 look-at 插件,看名字就知道作用就是让元素面对镜头。加上后的js引用有这么多:

<script src="https://aframe.io/releases/0.5.0/aframe.min.js"></script>
<script src="https://npmcdn.com/aframe-animation-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-event-set-component@3.0.1"></script>
<script src="https://npmcdn.com/aframe-layout-component@3.0.1"></script>
<script src="https://rawgit.com/mayognaise/aframe-html-shader/master/dist/aframe-html-shader.min.js"></script>
<script src="./aframe-gif-shader.js"></script>
<script src="./aframe-look-at-component.min.js"></script>

这里使用本地版本是没找到 cdn 地址的。

元素上也加上属性绑定:

<a-entity hide-on-click="target:#chickenAnimate">
    <a-entity id="default_egg"
              geometry="primitive:plane;width:2;height:2;"
              look-at="[camera]"
              position="-3 2 -10"
              material="shader:html;target:#textToDisplay;transparent:true;fps:0;">
    </a-entity>
</a-entity>
<a-entity id="chickenAnimate" visible="false">
    <a-entity id="animate_egg" geometry="primitive:plane;width:2;height:2;"
              look-at="[camera]"
              position="-3 2 -10"
              material="shader:gif;src:url(./egg_2017_1.gif);transparent:true;"
              gif=""></a-entity>
</a-entity>

此时已经可以完整的游戏了,源代码和演示都在: demo9

4.4 再增加点趣味性,AR?

现在浏览器的运算能力还做不到通过摄像头识别复杂的内容,最多只能识别有限的几个二维码。但是我们也可以退而求其次,先把场景放到用户周围再说。

我们调用摄像头,作为网页的背景就能模拟显示场景了。

iOS 系统没有开放摄像头的权限,只能通过android手机测试了。

我们再引用一个 threex.webar库中的一个threex.webcamgrabbing组件。

etUserMedia() no longer works on insecure origins.
To use this feature, you should consider switching your application to a secure origin,
such as HTTPS. See https://goo.gl/rStTGz for more details.

悲剧啊,过完年 MediaStreamTrack.getSources 接口被Chrome抛弃了,微信X5也停止支持了,但是新的接口navigator.mediaDevices.getUserMedia旧手机不支持。

所以我们使用 webrtc adapter 兼容新版本的获取资源方法。自己实现获取摄像头内容。

navigator.mediaDevices.getUserMedia 接口只能在 HTTPS 地址中被调用。

就是游戏的最终版本: demo10

微信里要点最下面的,访问原网页。

最终版做了判断,支持获取摄像头的设备就使用当前环境做背景,不支持的就在小树林里找蛋。

到这里这个游戏就做出来了,如果想要做成产品给用户玩,还需要做很多美化工作。至少要有加载游戏前的Loading页面,游戏界面的装饰效果,提示浮层美化,游戏后台接口记录成绩,给用户发奖品等工作。


参考资料