针对微信的前端文件版本管理原则

之前的文章里,我们讲到了微信的一个问题:缓存它认为是静态内容的文件,比如 javascript、css、html 还有图片。这给使用 Android 系统的开发人员带来了不便,因为 Android 中的微信没有刷新按钮,改完代码后想再测试很不方便。

但这是个契机,放大了一个被很多开发人员忽略的问题:前端文件是会被缓存的。不少开发人员甚至从未意识到这一点,因为在日常开发过程中,每次改完代码都会主动刷新一下再测试效果,如果不行就强制刷新一下,从来没有考虑普通用户会遇到什么问题。普通用户并不会在你的新代码上线后就刷新一下浏览器,知道强制刷新的就更少了,导致部分用户仍在用旧版的前端代码和你新版的后端交互,这经常会导致一些奇怪的 bug,而且是你永远无法查出来的 bug 。

因此,无论是不是针对微信,前端文件的版本管理都是十分重要的。

避免过时的缓存

浏览器缓存是一个非常有用的机制,可以明显减少浏览器和服务器间的数据传输,加快网页的加载速度,也能大大减少服务器的压力。但是缓存一旦过期,就会起到反作用。

有人会使用简单暴力的方法,通过在服务器配置文件头,告诉浏览器不要缓存某些文件,但这样做会牺牲缓存可以带来的好处,而且这一套在微信浏览器里完全行不通,它根据文件后缀来决定是不是要缓存,不管你的服务器是怎么跟它说的。

因此我们只能在迁就微信的情况下,合理的使用缓存,充分利用缓存的优点,同时避免缓存过期的问题。

两个原则
一、绝对不要缓存入口文件

除非你有信心,永远不需要修改这个页面以及所有被它使用的资源,否则绝对不要缓存入口文件。即使你对自己的代码有信心,你也很难保证需求不变化,呵呵。所以,绝对不要缓存入口文件

入口文件和其他文件是不同的,不能随意地变更地址,入口地址放在微信菜单里,会有最长 24 小时的生效时间,如果是分享类的活动,中途想换地址就更不可能。

实际操作中的规则,就是永远不要把入口文件命名成 .html 结尾。微信会无条件缓存 html 文件,导致你后续的修改无法传达到之前访问过的用户手中,他们会在很长一段时间里,只能看到旧版。

如果你的入口文件是用 php 之类的动态生成的,那么不要把地址重写成 .html 就好。

如果你的入口文件真的是静态的 html 文件,那么你可以考虑用你服务器端的编程语言加载并原样输出这个文件(以 php 为例):

1
2
<?php  
echo file_get_contents('index.html');

针对支持 php 的服务器,更简单的方法是直接将文件后缀改成 .php,asp 和 jsp 也类似。

如果你对性能有更高的要求,更好的方法是将文件后缀改掉,然后添加一个相应的 Mime 类型。例如将文件改成 index.nocache ,然后在 mime.conf (以 nginx 为例)里为 text/html 类别添加一个新后缀。

1
2
3
4
types {  
text/html html htm shtml nocache;
....
}

这样服务器就不必调用第三方的程序(如 php-fpm)尝试解析了,访问量大的时候也可以节约一些性能。

二、用版本管理其他资源

除了入口文件,所有其他资源都应该使用版本进行管理,主要包括 javascript、css、web 字体、还有图片。版本号可以包含在文件名里(如 app-0001.js,或者查询参数中(app.js?0001),两者各有好处,我个人更喜欢后者。

对版本号有两个要求:内容不同的文件,版本号一定是不同的;版本号不同的文件,内容一定是不同的

第一个要求用来避免用户使用过期的缓存。如果你改了内容,却不改版本号,用户就会继续使用缓存中的旧版本。

第二个要求用来避免缓存的浪费。因为你每次修改版本号,用户就需要重新加载一次那个文件,如果文件内容明明没有改变,缓存里的已经够用,你却更新了版本号,会让用户白白多下载一次。

因此,使用文件的哈希值来作为版本号是非常合适的,哈希值以文件内容作为计算依据,并且在有限的样本空间里,可以认为内容和哈希值是一一对应的,满足上面所说的两个条件。用哈希值还可以很方便的自动化,避免人工命名,我们都清楚,人迟早是会犯错的。

示例

结合上述两个原则,我的入口文件会被命名为 index.php ,简化后的内容如下:

1
2
3
4
5
6
7
8
9
10
<!doctype html>  
<head>
<title>缓存管理</title>
<script src="/app.js?4f05b2a7"></script>
...
</head>
<body>
...
</body>
</html>

下周,我们继续聊聊如何将今天提到的原则应用到实际的项目中,实现版本号的自动化管理,以及如何解决版本号带来的测试方面的问题。

用加载画面改善用户体验

毋庸置疑,单页面应用(Single Page Application,以下简称 SPA)正在变得越来越流行。SPA 具有前后端分离、接口重用度高、操作响应快速等优点,在用户体验上相比传统的网站也更接近原生应用。但与此同时,SPA 的体积却在不断增大,导致加载所需的时间越来越长,而这个问题在微信里面特别突出。

微信中的加载问题

首先,微信通常安装在移动设备上,网络的速度并不像固定宽带那么有保障,虽然 4G 已经普及,但总有些地方或某些时候网络没那么理想。在网络状况不佳时,加载较大的 SPA 耗时较长,屏幕表现为长时间白屏。

然后,微信对网页的缓存机制跟一般的浏览器不同。即使你没在服务器上进行特别的配置,它默认就会缓存 html、css、javascript 等它认为是“静态”的内容,而且在缓存有效期间“拒绝”去服务器上看看是否有更新,给不少新接触微信开发的程序员造成困扰。反过来,当你希望它将某个内容长期缓存,并通过响应头部的信息明确告知时,它也不能很好的遵守。说实话,你没法知道缓存什么时候会过期。不过也不能全怪它,微信毕竟不是浏览器。

结果,我们必须面对的事实就是经常会遇到这样的情况:用户打开你的公众号,点击某个按钮进入了你开发的 SPA,要加载的东西不少、网络又不太好、而缓存又都过期了,用户等待了 5 秒钟,页面还是空白的,用户关闭了页面并觉得你的网站很糟糕。

在对比 SPA 和原生应用时,常常会把“不需要下载和安装”作为一项优势,但我们都清楚,其实每次使用前都要“下载”一次。

加载画面

加载画面本身不能加快网站的加载速度,甚至会稍微拖慢一点,但它是一个信号,告诉用户这个网站并没有挂掉,他已经打开了这个网站,只是还要等待一会才能使用。这会让用户更有耐心继续等下去,尤其是在你能提供一个进度条的时候。

既然决定要在应用里加入加载画面了,那么一定要第一时间让它出现,并且随着网页内容的加载随时汇报加载进度。

显示加载画面

先来看一个没有加载画面的网页简化之后的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!doctype html>  
<html>
<head>
<meta charset="utf-8">
<title>loading demo</title>
<link href="/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<app></app>

<script src="/javascript/require.js"></script>
<script>
// 通过 require.js 来加载其他 javascript 文件
</script>
</body>
</html>

在上面这个例子中,样式文件放在 head 中,脚本文件放在 body 的尾部,是一种推荐写法,能让网页在第一时间进行显示,然后再加载 javascript 来对网页进行渐进式的增强。

但是这个做法对于 SPA 的帮助不是很大,现在大多数 SPA 都是基于 Angular、React、Vue 等前端框架,其内容都是通过 javascript 动态生成后再显示出来的。

这里我们假设网站已经使用 require.js 实现了按需加载和多线并行加载,但用户对加载速度仍不满意(这就是我遇到的问题和写这篇文章的原因)。

为了让加载画面能第一时间显示,我必须放弃利用其他框架,写成一个零依赖的库。它的位置应该同样位于 body 的尾部,但在所有其他 javascript 文件之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<!doctype html>  
<html>
<head>
<meta charset="utf-8">
<title>loading demo</title>
<link href="/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<app></app>

<script>
var splash = document.createElement('div');
splash.setAttribute('style', 'display: flex; justify-content: center; text-align: center; flex-direction: column; position: fixed; top: 0; left: 0; width: 100%; height: 100%; color: #fff; background: #288198');
var title = document.createElement('div');
title.textContent = 'WebCraft';
var progress = document.createElement('div');
progress.textContent = '开始加载';
splash.appendChild(title);
splash.appendChild(progress);
document.body.appendChild(splash);
</script>
<script src="/javascript/require.js"></script>
<script>
// 通过 require.js 来加载其他 javascript 文件
</script>
</body>
</html>

写完后,通过浏览器进行观察,方法是调出 Chrome 的开发者工具,打开 Network 标签,勾选 Disable Cache 禁用缓存,在 Throttling 处选择 GPRS,这样浏览器就会模拟 GRPS 网络的速度来加载文件。

可以观察到,加载画面在加载 require.js 之前就显示了,但是暴露出另一个问题,在样式文件加载完之前,网页一直是空白的,这是因为浏览器在加载完样式文件之前,是不会进行渲染的。

这让我想起了早期的 Opera 浏览器(换核之前),加载样式文件是不会阻止渲染的,它会直接显示出一个没有样式的网页,在加载完样式文件后再渲染成有样式的样子。

在新的标准中,link 标签将会有一个可选的 preload 属性,允许浏览器在不加载完这个样式文件的时候就进行渲染,但目前很多浏览器尚未支持,微信在 Android 中使用的 X5 内核,以及 iOS 的 Safari 也都不支持。

我目前开发的大多数的项目都需要加载 bootstrap、font-awesome 这两个体积稍大的样式库,还有一些根据项目需要另外引入的零碎的库。加在一起要加载的量也是不小的。

为了让加载画面出现的更快,样式文件必须下移到显示加载画面的 script 标签之后。但是很不幸,微信内嵌浏览器使用的内核,无论是自己的 X5 还是 iOS 的 Safari 内核,还有一个特性,就是只要遇到加载样式文件的 link 标签,就会停止渲染,直到样式文件加载完毕。

也就是说,即使把 link 放在 script 之后,也无法先显示加载画面。

现在就剩下一条路了,动态加载样式文件,听起来高大上,实现起来很简单,用 javascript 创建一个 link 标签,指定好要加载的样式文件,再挂到 DOM 树中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!doctype html>  
<html>
<head>
<meta charset="utf-8">
<title>loading demo</title>
<link href="/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<app></app>

<script>
var splash = document.createElement('div');
splash.setAttribute('style', 'display: flex; justify-content: center; text-align: center; flex-direction: column; position: fixed; top: 0; left: 0; width: 100%; height: 100%; color: #fff; background: #288198');
var title = document.createElement('div');
title.textContent = 'WebCraft';
var progress = document.createElement('div');
progress.textContent = '开始加载';
splash.appendChild(title);
splash.appendChild(progress);
document.body.appendChild(splash);

var link = c('link');
link.setAttribute('href', '/css/bootstrap.css');
link.setAttribute('rel', 'stylesheet');
document.body.appendChild(link);
</script>
<script src="/javascript/require.js"></script>
<script>
// 通过 require.js 来加载其他 javascript 文件
</script>
</body>
</html>

经过这一次的修改,加载画面就可以在只加载了 html 文件的情况下显示啦。接下来我们要实现显示加载进度的功能。

显示加载进度

首先,我们要明确在第一个主要内容页面显示之前,总共要加载多少东西,或者说有几个节点,这样我们就有了一个计数的目标;然后创建一个空的计数器,每完成一个节点,计数器加一,并更新进度;最终,当计数器到达计数目标的时候,加载完成,隐藏掉加载画面,显示出内容。

1
2
3
4
5
6
7
8
9
10
var total = 节点数;  
var count = 0;
var loaded = function () {
count++;
if (count < total) {
// 更新加载进度
} else {
// 隐藏加载画面
}
}

total 是计数目标,count 是计数器,每次完成一个节点,调用 loaded 函数就可以更新计数。

现在,我们暂不考虑应用本身的逻辑,只把文件的加载纳入进度显示中,那么在上面的例子中有两个需要加载的文件,一个是 bootstrap.css ,另一个是 require.js 。对于 JavaScript 文件,由于是顺序加载和执行的,实现起来非常简单:

1
2
3
4
<script src="/javascript/require.js"></script>  
<script>
loaded();
</script>

如上所示,require.js 加载和执行完毕后,就会执行 loaded() 使计数器加一。

对于样式文件,我们通过监听 load 事件来实现:

1
2
3
4
5
var link = c('link');  
link.setAttribute('href', '/css/bootstrap.css');
link.setAttribute('rel', 'stylesheet');
link.addEventListener('load', loaded);
document.body.appendChild(link);

回到现实的情况中,通过 require.js 加载的、运行 SPA 必备的库也可以作为节点,通过在 require 函数的回调中加入 loaded 来监控进度。

1
2
3
4
require(['jquery'], function () {  
loaded();
// 继续处理
});

如果需要,还可以进一步将进度监控到获取渲染第一个页面所需的数据的步骤,但是别忘了将计数目标做出相应改变。

1
2
3
4
$.get('/api/some-data', function (data) {
loaded();
// 渲染第一个主要内容页面
});

完整的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!doctype html>  
<html>
<head>
<meta charset="utf-8">
<title>loading demo</title>
<link href="/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<app></app>

<script>
var splash = document.createElement('div');
splash.setAttribute('style', 'display: flex; justify-content: center; text-align: center; flex-direction: column; position: fixed; top: 0; left: 0; width: 100%; height: 100%; color: #fff; background: #288198');
var title = document.createElement('div');
title.textContent = 'WebCraft';
var progress = document.createElement('div');
progress.textContent = '开始加载';
splash.appendChild(title);
splash.appendChild(progress);
document.body.appendChild(splash);

var total = 4;
var count = 0;
var loaded = function () {
count++;
if (count < total) {
progress.textContent = count + '/' + total;
} else {
splash.remove();
}
}
var link = c('link');
link.setAttribute('href', '/css/bootstrap.css');
link.setAttribute('rel', 'stylesheet');
link.addEventListener('load', loaded);
document.body.appendChild(link);
</script>
<script src="/javascript/require.js"></script>
<script>
loaded();
require(['/javascript/jquery.js'], function ($) {
loaded();
$.get('/api/some-data', function (data) {
loaded();
$('app').append(data);
});
});
</script>
</body>
</html>

至此,一个简单的,可显示进度的加载画面的原型已经完成。本文的主要内容也就到此为止了。

尾声

如果要应用到生产环境中,还有几个问题必须解决:动态添加样式的几个语句应该定义成一个函数以使加载多个样式文件更方便;暴露在全局中的变量太多,不利于维护;样式固定,无法适应不同风格的应用。

持续更新的演示地址:
https://bnlt.org/demo/loading/index.html

在 CentOS 6 和 Nginx 中部署 Let's Encrypt 的 SSL 证书

上周写了一篇文章讲了讲为什么应该使用 HTTPS 加密你的网站,并笼统的介绍了如何申请和使用 Let’s Encrypt 提供的 SSL 证书。这次我们说说如何在 CentOS 6 和 Nginx 中部署 Let’s Encrypt 的 SSL 证书,过程中可能遇到的问题,以及如何解决。

整体上分成申请证书和使用证书两个部分。

申请证书

准备工作

在申请证书之前,你必须有一个域名,并配置好 DNS 解析服务,使其指向你将要使用的服务器。说白了,就是你要保证所有人都可以通过在浏览器中输入域名打开你的网站。

此时 Nginx 的配置文件如下(为了便于说明只包含了最基本的信息):

1
2
3
4
5
6
7
8
9
[ngxin]
server {
listen 80;
server_name www.bnlt.org;

location / {
root /usr/share/nginx/html;
}
}

其中 80 是 http 协议默认端口,www.bnlt.org 是我的域名,/usr/share/nginx/html 是网站根目录。

安装 Certbot

Certbot 是 Let’s Encrypt 官方推出的,用来获取 SSL 证书的工具。Certbot 的官网为各大主流操作系统和 Web 服务器提供了安装和使用的指南。下面首先介绍官方提供的方法。

下列操作均在 root 用户权限下执行。

官方推荐

针对 CentOS 6 系统,官方推荐的是使用 certbot-auto 脚本。安装方法如下:

1
2
wget https://dl.eff.org/certbot-auto  
chmod a+x certbot-auto

第一个命令将 certbot-auto 下载到当前目录下,第二个命令给予执行权限。

这是一个 shell 脚本,可以通过在当前目录下执行 ./certbot-auto 来调用。

它首先检查你是否已经安装了最新版本的 certbot,如果尚未安装,则调用 yum 检查和安装相关的依赖,其中包括了 python、python-dev 和 python-pip,然后创建一个虚拟环境(相对独立的、运行 python 的虚拟环境,在里面安装的依赖和库独立于操作系统使用的 python 环境,可以避免影响主系统的运行),在这个虚拟环境里安装一个最新版本的 certbot。用户交给 certbot-auto 的参数都被委派给这个安装在虚拟环境中的 certbot 来执行。

由于这个 certbot 被安装在虚拟环境里,你不能直接在命令行里使用 certbot 命令,只能通过 certbot-auto 脚本间接使用它。

不过使用 certbot-auto 脚本有时会遇到问题,表现为虚拟环境创建完毕后,长时间卡在安装 Python 包的步骤:

1
2
Creating virtual environment...  
Installing Python packages...

这通常被认为是 pip 的源在国内访问受限导致。

直接使用 pip 安装 certbot

上面讲到了,certbot-auto 是先安装 python-pip,再通过 python-pip 来安装 certbot 的,所以我们也可以自己手动来完成这个过程。通过这个方法安装的 certbot 会成为一个全局的命令,具体安装在 /usr/bin/certbot 。

如果你想自己安装 certbot,我仍然建议你先执行一下 certbot-auto 命令,让它先帮你安装好相关的依赖。

默认情况下 certbot-auto 也会为你安装 python 和 python-pip,但是版本教旧的 python2.6 和 pip7.1,,虽然用来安装和使用 certbot 是完全没有问题的,但每次执行时都会提示你当前使用的版本较老,官方已不再支持,有安全隐患。

我使用的是 python2.7 以及 pip9.0,可以通过添加 IUS 的源来安装。

添加 IUS 源

1
yum install https://centos6.iuscommunity.org/ius-release.rpm

安装 python2.7 和 pip9.0+

1
yum install python27 python27-devel python-pip

其中 python27-devel 是运行 certbot 必需的。

全局安装 certbot

1
pip install certbot

如果 pip install 速度很慢或卡进度,可以尝试设置 pip 的源为阿里云的镜像,再尝试上面的命令。

在当前用户目录下建立 ~/.pip/pip.conf 文件。内容如下:

1
2
3
4
5
[global]
index-url = http://mirrors.aliyun.com/pypi/simple/

[install]
trusted-host=mirrors.aliyun.com

安装完毕后,你会得到一个新命令 certbot,用 which 命令可以查到其安装的位置。

1
2
which certbot  
/usr/bin/certbot

获取证书

现在可以使用 certbot 获取 SSL 证书了。

如果你使用官方脚本,将下列命令中的所有 certbot 替换为 ./certbot-auto 即可。

这里我们使用 certbot certonly --webroot 方式来获取证书,此命令借助已有的 Web 服务实现认证,并生成证书,命令执行过程中不会对网站的正常运行造成影响,以后给证书续期也更平滑。

完整的命令如下:

1
certbot certonly --webroot -w /usr/share/nginx/html -d www.bnlt.org

-w 参数指定了网站的根目录,-d 参数指定了网站的域名。

背后的原理大致如下:执行命令的计算机会和 Let’s Encrypt 的服务器进行通信,商定一个字符串,这个字符串以文件的形式保存在-w 参数指定的路径下的 .well-known/acme-challenge/ 目录中,服务器再访问 -d 指定的网站目录下的相应文件来进行核实。核对成功就可以基本证明这个域名确实归你所有,Let’s Encrypt 就会为你颁发相应的证书了。

实际上只能证明当前是你控制了这个域名的解析,如果域名解析被他人恶意控制或污染,他也能生成此域名的证书,因此 Let’s Encrypt 提供的证书仅能用于加密,保证信息在传输过程中不被篡改,但不能用来证明域名的所有权。从这个角度来看,只要攻击者能控制或污染域名解析,即使你访问的某个网站显示了绿锁,仍有可能是个钓鱼网站。

执行此命令后,会弹出一个界面问你是否接受相关的协议,选择 OK 并确认即可。你也可以在执行上述命令时加上 –agree-tos 自动接受协议以跳过此步骤。

如果服务器配置正确,命令行参数也无误,那么就能成功完成,提示如下:

1
2
3
4
5
6
- Congratulations! Your certificate and chain have been saved at
/etc/letsencrypt/live/www.bnlt.org/fullchain.pem. Your cert will
expire on 2017-01-30\. To obtain a new or tweaked version of this
certificate in the future, simply run certbot again. To
non-interactively renew *all* of your certificates, run
"certbot renew"

提示信息告诉你证书存放在 /etc/letsencrypt/live/www.bnlt.org 目录下,过期时间是 2017-01-30,最后还告诉你续期的方法是执行 certbot renew

而最常见的错误提示如下:

1
2
An unexpected error occurred:  
The request message was malformed :: No such challenge

遇到这样的情况是因为 Let’s Encrypt 的服务器无法在你的网站上找到验证用的文件。你首先要检查 -w-d 参数是否有误,是否把验证文件放在了别的目录下或者填错了域名,另外还要确保 Let’s Encrypt 的服务器能访问到你的网站,比如你是刚做的域名解析,可能对它所处的网络而言域名解析尚未生效。

使用证书

要在 Nginx 中用刚才得到的证书来启用 https ,需要将配置文件改为:

1
2
3
4
5
6
7
8
9
10
11
server {  
listen 443 ssl;
server_name www.bnlt.org;

ssl_certificate /etc/letsencrypt/live/www.bnlt.org/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.bnlt.org/privkey.pem;

location / {
root /usr/share/nginx/html;
}
}

和原来的配置文件相比主要有两处变化,一是将 listen 端口改为 https 协议的默认端口 443,并加上 ssl 说明是加密连接。

还有就是增加了 ssl_certificatessl_certificate_key,前者指定公钥,后者指定私钥。

保存配置文件,执行 /etc/init.d/nginx reload 使新配置生效

http 自动跳转 https

如果你想更进一步,对所有用户强制使用 https,可以在配置文件里增加如下内容:

1
2
3
4
5
server {  
listen 80;
server_name www.bnlt.org;
return 301 https://$server_name$request_uri;
}

此配置会将所有通过 80 端口的 http 协议访问的用户,安全的跳转到对应的 https 版本页面。

同样,执行 /etc/init.d/nginx reload 使新配置生效。

把你的网站升级到 HTTPS

把你的网站升级到 HTTPS

作为一个普通用户,在使用手机浏览网页,或是阅读微信中别人分享的内容时,经常会看到屏幕下方有个弹出广告,什么点击领红包啦,免费送流量啦,或是移动运营商的广告。通常这些广告跟你浏览的网站本身风格完全不搭、画质又非常糟糕、而且在几个毫不相干的网站中都出现了。遇到这样的情况,基本可以肯定是有中间人劫持了你的流量,并篡改了网页内容,在其中嵌入了广告。纯粹做做广告已经算是客气,更恶劣的可以替换你浏览的网页内容,把你引向钓鱼网站,甚至直接窃取你的密码。

作为一个开发者,你的境遇更糟糕,因为这一切时刻都发生在你自己的网站或 APP 上。也许是你花费不少心思才做出来的精美页面被一个很粗糙的广告拉低了好几个档次;也许是一个重要活动的报名按钮被广告遮挡,你的用户不得不先小心翼翼关了广告才能继续;也许是为一个大客户制作的严肃的内容下面出现了一段夸张又搞笑的广告语。

但最最糟糕的是,你的大多数客户和用户都不清楚这是运营商或广告商劫持了他们的流量,而是以为你这个家伙为了一点微薄的广告收入在里面嵌了品味低下的广告,毕竟这网站是你的,这应用是你开发的,不是你干的还能是谁呢。你是希望当这一切发生的时候向他们解释这不是你的错,还是从根本上杜绝这样的事情发生呢?更何况你不是一点错都没有,是你的安全工作没做到位,给了无良广告商机会,钱他们赚,黑锅你背。

换了过去,你还有借口,对网站进行 HTTPS(SSL/TLS) 安全加密的证书是要花钱买的,每个独立子域名就要几百元一年,高级的证书则需要几千上万。

不过现在有了 Let’s Encrypt。一个致力于推行互联网安全的非营利性组织,免费为你的网站进行认证并颁发证书,其颁发的证书已被各大浏览器广泛接受。

那么接下来就只剩一个问题了,到底怎么才能把自己的网站从 http 升级到 https 呢?

Let’s Encrypt 和 Certbot

Let’s Encrypt 颁发证书是完全自助的,如果你的网站已经上线运行,那么你只需要用它们提供的工具执行一个命令就能为其申请一个证书。

这个工具是 Certbot。

安装 Certbot
  • 打开 Certbot 的官方网站 https://certbot.eff.org/
  • 在 I’m Using 处选择自己使用的 Web 服务器和操作系统
  • 下方的内容会根据你的选择进行相应的变化
  • 根据网站给出的提示逐步操作

好吧,这个网站是英文的。

不过我简单总结了一下,主要是这两种方式:

首先尝试用操作系统对应的包管理器来安装,比如 ubuntu 用 apt-get install certbot, arch 用 pacman -S certbot,macOS 用 brew install certbot, centos 用 yum install certbot

如果提示包不存在,比如我用的 CentOS 6,那么可以尝试下载一个官方提供的叫做 certbot-auto 的脚本

1
2
wget https://dl.eff.org/certbot-auto  
chmod a+x certbot-auto

如果使用 certbot-auto ,后续的命令中用到 certbot 的地方都换成 ./certbot-auto 即可。

获取证书
1
certbot certonly --webroot -w 网站根目录 -d 你的域名

将网站根目录和域名换成你实际使用的目录和域名即可。

certbot 会自动完成一系列的验证工作,把证书放在你的服务器上,然后弹出一个恭喜你成功获取证书的提示,证书文件的名称也包含在该提示中。通常会是 /etc/letsecrypt/live/你的域名/fullchain.pem

为你的网站启用 HTTPS

接下来就要修改你 Web 服务器的配置文件了,比如 Nginx 默认是修改 /etc/nginx/conf.d/default.conf

将配文件中的

1
listen 80;

改成

1
listen 443 ssl;

然后添加两行配置说明使用哪个证书文件

1
2
ssl_certificate /etc/letsencrypt/live/你的域名/fullchain.pem;  
ssl_certificate_key /etc/letsencrypt/live/你的域名/privkey.pem;

重启 Web 服务器使证书生效。

证书续期

Let’s Encrypt 颁发的证书有效期为 90 天,你可以在证书快到期的时候进行续期操作,续期命令更加简单:

1
certbot renew

该命令会逐一检查你所有的证书文件,并对其中快到期的证书进行续期。

为了使续期后的新证书生效,重启一下 Web 服务器。

完整的例子

本来只是想写一篇如何在 CentOS 6 和 Nginx 下使用 Let’s Encrypt 为你的网站增加 HTTPS ,一不小心把题目写大了收不住。这个具体的例子,会涉及一些官方操作未提及的细节和问题,只好再另开一篇进行详细描述了。

ImageMagick 将 PDF 转换成多张图片

ImageMagick 将 PDF 转换成多张图片

今天遇到一个需求,客户发来几份PDF,想放到他们的微网站(专门给微信用的网站)上,由于是给手机看的,加上文件页数不多,就想到转换成图片再放上去。

第一时间想到用 ImageMagick 的 convert 命令,网上一搜,命令如下1

1
convert -density 600 foo.pdf foo-%02d.jpg

-density 设置了生成的图片的精度,数值越大,图片越清晰(分辨率高),转换也越慢。如果给手机用,普通 A4 大小的 PDF 设置在 200 左右比较合适。

foo-%02d.jpg 是希望生成的文件名,%02d 部分会替换成页码(从 0 开始),用过 printf 函数的应该对这个规则会比较熟悉。

然而在实际使用时遇到了一个意外情况:

1
convert: no images defined `foo-%02d.jpg' @ error/convert.c/ConvertImageCommand/3258\.

继续求助搜索引擎,找到解决方案,缺少 gs 2。gs 即 GhostScript,ImageMagick 用它来解析 PDF 文件。

1
brew install gs

安装完 gs ,再执行一次 convert 命令,问题解决。

Hash, PushState 和微信 JSSDK 授权

最近将 riot.js 升级到了 3.0,并用上了新版本的 riot-route,原先用了一年多的 2.2.4 版本内置的 riot.route 只支持 hash 形式的 SPA 单页面应用,riot-route 则支持 pushState。

Hash 方式有个缺点,就是服务器不知道地址栏中 # 之后的内容,放在微信里,就导致了未授权用户授权后想返回原界面需要借助 JS 来实现,导致历史记录多了一条,用户按返回键无法退出(返回上一页面后又被 JS “前进”了)。

PushState 方式就没有这个问题,可以直接用 HTTP 302 重定向过来,不影响历史记录和返回按钮功能。

于是在新项目中开始采用 pushState 方式。

不过等到应用到了微信环境中,又冒出来一个问题,通过 pushState 改变地址在 iOS 中会导致 JSSDK 的授权失败。

在 config 中开启 debug,可见 invalid signature 。原因是 location.href 中的地址变化了,但是微信客户端认为地址还是打开页面时的地址,微信“复制链接”得到的地址可以作为旁证。

解决方法

先按标准方法用当前地址计算 signature,如果失败,再用打开浏览器时的地址计算 signature。

由于之前已经把加载微信 JSSDK 和相关 Api 的功能独立成函数,因此这次解决起来也比较简单。

修改前的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function runWx(api, fn, loadError) {  
if (typeof api === 'string') {
api = [api];
}

// 用 require.js 加载 JSSDK 文件,如果你已经用 <script> 方式加载,可以省掉这一步
require(['jweixin'], function (wx) {
var url = location.href.split('#')[0];

// 将 url 发往服务器计算相应的 signature
$.get('/signature', { url: url }, function (ans) {
// wx.config 执行成功时调用
wx.ready(function () {
wx.checkJsApi({
jsApiList: _.clone(api), // 坑,微信会改变此参数的内容
success: function success(res) {
if (!res || !res.checkResult || _.any(api, function (v) {
return !res.checkResult[v];
})) {
alert('您的微信版本过低,无法使用此功能,请升级微信');
return;
}
fn(wx);
}
});
});

// wx.config 执行失败时调用
wx.error(function () {
alert('授权失败,您可能无法使用部分功能');
});

// 校验用的参数来自服务器
var config = {
// debug: true,
appId: ans.data.appId,
timestamp: ans.data.timestamp,
nonceStr: ans.data.nonceStr,
signature: ans.data.signature,
jsApiList: _.union(['checkJsApi'], api)
};

// 进行校验
wx.config(config);
});
}, loadError);
}

先简单展示一下这个 runWx 函数的用法:

1
2
3
4
5
6
7
8
// 当需要用到特定的微信接口时,运行 runWx
runWx(['uploadImage', 'chooseImage'], function (wx) {
// 可以在这里使用 wx.uploadImage 和 wx.chooseImage 功能
wx.uploadImage(...);
wx.chooseImage(...);
}, function () {
// 加载失败时要做的事
});

runWx 做了这么几件事:

  • 用 require.js 加载 weixin sdk 的 js 文件
  • 获取当前页面地址 location.href.split(‘#’)[0]
  • 将 url 作为参数发送到服务器计算出 signature
  • 调用 wx.config
  • 成功时调用 wx.ready 并最终调用回调函数 fn
  • 失败时调用 wx.error 提示错误
修改后的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 解决部分机型 pushState 不能正确改变地址导致授权失败

// 在第一次打开页面时加载此文件,记录当时的地址作为原始地址
var originUrl = location.href.split('#')[0];

// 增加 tryOrigin 参数
function runWx(api, fn, loadError, tryOrigin) {
if (typeof api === 'string') {
api = [api];
}
require(['jweixin'], function (wx) {
// tryOrigin 为真时使用原始地址
var url = tryOrigin ? originUrl : location.href.split('#')[0];

$.get('/signature', { url: url }, function (ans) {
wx.ready(function () {
wx.checkJsApi({
jsApiList: _.clone(api), // 坑,微信会改变此参数的内容
success: function success(res) {
if (!res || !res.checkResult || _.any(api, function (v) {
return !res.checkResult[v];
})) {
alert('您的微信版本过低,无法使用此功能,请升级微信');
return;
}
fn(wx);
}
});
});

wx.error(function () {
// 已经试了原始地址
if (originUrl === url) {
alert('授权失败,您可能无法使用部分功能');
return;
}
// 没有使用原始地址且 signature 不匹配,尝试用原始地址计算 signature
runWx(api, fn, loadError, true);
});

var config = {
debug: true,
appId: ans.data.appId,
timestamp: ans.data.timestamp,
nonceStr: ans.data.nonceStr,
signature: ans.data.signature,
jsApiList: _.union(['checkJsApi'], api)
};
wx.config(config);
});
}, loadError);
}

和原来相比,主要变化有:

  • 记录了打开浏览器时的原始地址
  • 先尝试用标准的 location.href.split(‘#’)[0] 计算 signature
  • 失败时用 originUrl 再试一次

这个改动的主要优点是原来用到 runWx 的地方,代码完全不需要进行变动,由 runWx 自己去尝试解决问题。

_.compact(_.map(_.toArray(n.toString(2)).reverse(), (v, i) => 2 ** i * v))

包含 underscore 和 es2016 语法

功能

将一个数转换成多个2的N次方的数的和

| 输入 | 二进制 | 输出 | | - | - | - | | 1 | 0001 | [1] | | 6 | 0110 | [2, 4] | | 7 | 0111 | [1, 2, 4] |

用途

数据库中以整数形式存储,界面上以多选的形式展现

1
2
3
4
5
6
7
8
9
<select multiple>
<option value="1">a</option>
<option value="2">b</option>
<option value="4">c</option>
</select>

<script>
$('select').val([2,4])
</script>

数据库存了6,取到前端,转换成 [2,4],同时选中 b 和 c

分解动作

假设 n = 6

1
2
3
4
5
6
n // 6
n.toString(2) // '110'
_.toArray(n.toString(2)) // ['1', '1', '0']
_.toArray(n.toString(2)).reverse() // ['0', '1', '1']
_.map(_.toArray(n.toString(2)).reverse(), (v, i) => 2 ** i * v) // [0, 2, 4]
_.compact(_.map(_.toArray(n.toString(2)).reverse(), (v, i) => 2 ** i * v)) // [2, 4]

其他写法

_.chain(n.toString(2)).toArray().map((b, i, a) => 2 ** (a.length - i - 1) * b).compact().value()
_.compact(_.range(32).map((v) => 2 ** v & n)) // 最大处理32位整型

CentOS 6 Nginx Mainline 1.9

准备

  • 版本:CentOS 6.7, Nginx 1.9
  • 下列所有操作都在命令行终端中进行
  • 权限:root

以下操作需要 root 权限,如果当前登录的用户不是 root,请输入 su 命令,并在提示中输入正确的密码切换为 root 用户

1
su

实现

1.添加源

在 /etc/yum.repo.d/ 下新建 nginx.repo 文件,文件内容如下:

1
2
3
4
name=nginx repo  
baseurl=http://nginx.org/packages/mainline/centos/6/$basearch/
gpgcheck=0
enabled=1
2.安装

通过 yum 安装 nginx

1
yum install nginx -y
3.启动

启动 Nginx 服务

1
/etc/init.d/nginx start
4.验证

用 curl 访问本机地址 127.0.0.1

1
curl 127.0.0.1

出现如下结果表明安装成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>  
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

你也可通过浏览器访问 127.0.0.1 来查看结果

说明

目录 /etc/yum.repos.d 下存放的是 yum 安装和更新软件时用到的源的配置文件。所谓源,就是软件仓库,里面存放了各种各样的软件,我们通过 yum 安装的软件,都是从这些源中下载过来的。

我们在这个目录下添加了一个新文件 nginx.repo ,这样当我们不再需要这个源的时候,只要将对应的文件删除即可。通过增加和删除配置文件的方式,我们可以实现向 yum 中添加新的源或去除不再使用的源的目的,这比修改原有的配置文件要更方便和安全。

配置文件中,关键的一行是,它指明了源的地址

1
baseurl=http://nginx.org/packages/mainline/centos/6/$basearch/

nginx 官方针对不同的 Linux 发行版提供了不同的源地址,这里的 centos6 对应了我们使用的 CentOS 6 操作系统。

其中的 mainline 表示这是 mainline 分支,这是相对 stable 分支而言的,mainline 会随着开发进度的推进加入一些新的特性,而 stable 的更新仅限于 bug 修复。mainline 是 nginx 官方推荐使用的分支。当然,如果 stable 分支的功能已经能够满足你的需求,并且你担心新特性可能带来 bug,那么可以考虑选择 stable 分支。

安装

yum install 是最常用的 yum 命令之一,其作用是安装指定的软件,这里我们指定了 nginx ,参数 -y 的作用是当遇到提示询问是否(y/N)时,自动确认并继续。输入这个命令,yum 就会在我们刚刚添加的源中找到 nginx,并自动完成下载和安装的过程。

启动

目录 /etc/init.d 下存放的是初始化脚本(init scripts),通过这些脚本,我们可以针对很多软件实现启动、停止、重启、重新加载配置文件、查看软件运行状态等操作。

当我们通过 yum 安装 nginx 的时候,也生成了一个对应的初始化脚本 /etc/init.d/nginx 。

执行 /etc/init.d/nginx start 即可启动 nginx 服务,相应的:

  • /etc/init.d/nginx start 启动
  • /etc/init.d/nginx stop 停止
  • /etc/init.d/nginx restart 重启
  • /etc/init.d/nginx status 显示状态
  • /etc/init.d/nginx reload 重新加载配置

还有另一种形式来调用这些命令:

  • service nginx start 启动
  • service nginx stop 停止
  • service nginx restart 重启
  • service nginx status 显示状态
  • service nginx reload 重新加载配置
验证

curl 是一个用途非常广泛的软件,这里我们使用它来快速的验证 nginx 服务是否已经开启,curl 后面跟上服务器地址,是一种便捷的检测网络服务是否可以正常访问的方法,它会向指定地址发送一个 GET 请求,并将收到的响应主体输出到终端的标准输出当中。

这里,我们收到的内容是 nginx 服务器的欢迎页面,这个页面的本体是 /usr/share/nginx/html/index.html 文件,刚才执行的 curl 命令的返回就是这个文件的内容。

其他

远程访问失败

如果本机访问成功,而其他机器(局域网中的其他机器或外网的机器)无法访问,一般是防火墙导致的,可以执行下面的命令关闭防火墙后再试:

1
/etc/init.d/iptables stop

iptables 是 CentOS 默认安装并启用的防火墙软件,其默认配置是阻止本机以外的其他机器访问本机端口的。在掌握足够多的知识来管理 iptables 配置前,最简单的解决方式就是关闭它。

开机启动

在完成上述操作后,nginx 和 iptables 都是会在开机时自动启动的,如果你想改变这些设置,可以安装 ntsysv 来管理服务器中的所有开机启动项

1
yum install -y ntsysv

安装完毕后,启动它

1
ntsysv

界面如图:
ntsysv

可以通过上下键移动光标,空格键切换启动或不启动,选完之后用 tab 切换至 OK,最后按回车保存。

参考

CentOS 6 Nginx Stable 1.8

准备

  • 版本:CentOS 6.7, Nginx 1.8
  • 下列所有操作都在命令行终端中进行
  • 权限:root

以下操作需要 root 权限,如果当前登录的用户不是 root,请输入 su 命令,并在提示中输入正确的密码切换为 root 用户

1
su

实现

1.添加源

在 /etc/yum.repo.d/ 下新建 nginx.repo 文件,文件内容如下:

1
2
3
4
name=nginx repo  
baseurl=http://nginx.org/packages/centos/6/$basearch/
gpgcheck=0
enabled=1
2.安装

通过 yum 安装 nginx

1
yum install nginx -y
3.启动

启动 Nginx 服务

1
/etc/init.d/nginx start
4.验证

用 curl 访问本机地址 127.0.0.1

1
curl 127.0.0.1

出现如下结果表明安装成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!DOCTYPE html>  
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

你也可通过浏览器访问 127.0.0.1 来查看结果

说明

目录 /etc/yum.repos.d 下存放的是 yum 安装和更新软件时用到的源的配置文件。所谓源,就是软件仓库,里面存放了各种各样的软件,我们通过 yum 安装的软件,都是从这些源中下载过来的。

我们在这个目录下添加了一个新文件 nginx.repo ,这样当我们不再需要这个源的时候,只要将对应的文件删除即可。通过增加和删除配置文件的方式,我们可以实现向 yum 中添加新的源或去除不再使用的源的目的,这比修改原有的配置文件要更方便和安全。

配置文件中,关键的一行是,它指明了源的地址

1
baseurl=http://nginx.org/packages/centos/6/$basearch/

Nginx 官方针对不同的 Linux 发行版提供了不同的源地址,这里的 centos6 对应了我们使用的 CentOS 6 操作系统。

安装

yum install 是最常用的 yum 命令之一,其作用是安装指定的软件,这里我们指定了 nginx ,参数 -y 的作用是当遇到提示询问是否(y/N)时,自动确认并继续。输入这个命令,yum 就会在我们刚刚添加的源中找到 nginx,并自动完成下载和安装的过程。

启动

目录 /etc/init.d 下存放的是初始化脚本(init scripts),通过这些脚本,我们可以针对很多软件实现启动、停止、重启、重新加载配置文件、查看软件运行状态等操作。

当我们通过 yum 安装 nginx 的时候,也生成了一个对应的初始化脚本 /etc/init.d/nginx 。

执行 /etc/init.d/nginx start 即可启动 nginx 服务,相应的:

  • /etc/init.d/nginx start 启动
  • /etc/init.d/nginx stop 停止
  • /etc/init.d/nginx restart 重启
  • /etc/init.d/nginx status 显示状态
  • /etc/init.d/nginx reload 重新加载配置

还有另一种形式来调用这些命令:

  • service nginx start 启动
  • service nginx stop 停止
  • service nginx restart 重启
  • service nginx status 显示状态
  • service nginx reload 重新加载配置
验证

curl 是一个用途非常广泛的软件,这里我们使用它来快速的验证 nginx 服务是否已经开启,curl 后面跟上服务器地址,是一种便捷的检测网络服务是否可以正常访问的方法,它会向指定地址发送一个 GET 请求,并将收到的响应主体输出到终端的标准输出当中。

这里,我们收到的内容是 nginx 服务器的欢迎页面,这个页面的本体是 /usr/share/nginx/html/index.html 文件,刚才执行的 curl 命令的返回就是这个文件的内容。

其他

远程访问失败

如果本机访问成功,而其他机器(局域网中的其他机器或外网的机器)无法访问,一般是防火墙导致的,可以执行下面的命令关闭防火墙后再试:

1
/etc/init.d/iptables stop

iptables 是 CentOS 默认安装并启用的防火墙软件,其默认配置是阻止本机以外的其他机器访问本机端口的。在掌握足够多的知识来管理 iptables 配置前,最简单的解决方法就是关闭它。

开机启动

在完成上述操作后,nginx 和 iptables 都是会在开机时自动启动的,如果你想改变这些设置,可以安装 ntsysv 来管理服务器中的所有开机启动项

1
yum install -y ntsysv

安装完毕后,启动它

1
ntsysv

界面如图:
ntsysv

可以通过上下键移动光标,空格键切换启动或不启动,选完之后用 tab 切换至 OK,最后按回车保存。

参考

CentOS 6 安装 gearman 和它的 php 扩展

0. PHP 中的 gearman 扩展

我的服务器使用的是 ius 的 php5.5 ,如果你使用其他源和版本,请自行替换部分包名

1. 安装 epel 和 ius 源

1
2
yum install http://dl.iuscommunity.org/pub/ius/stable/CentOS/6/x86_64/epel-release-6-5.noarch.rpm
yum install http://dl.iuscommunity.org/pub/ius/stable/CentOS/6/x86_64/ius-release-1.0-11.ius.centos6.noarch.rpm

2. 安装 gearman ,用于运行 gearman 服务

1
yum install gearmand

3. 安装 libgearman 和 libgearman-devel ,用于编译 php 扩展

1
yum install libgearman libgearman-devel

4. 安装 pecl 和其他依赖

1
yum install php-pear php55u-devel

5. 安装 php 扩展

1
pecl install gearman

6. 修改配置文件 /etc/php.ini 增加 extension=gearman.so

7.验证安装结果

1
2
$ php -m | grep gearman
gearman