一次专辑专题站开发的经验分享

饱含辛酸血泪

Posted by Kriz on 2017-11-22

写在前面的话

这篇总结主要还是自己的一个留档,对于我这种记性爆炸差的老年人还是烂笔头比较可靠。因为是第一次正儿八经地从头开始独立写后端,所以难免有很多过于基础或者有问题的部分,还请多包涵。

大大小小的坑踩了不少,其实有相当一部分都是低级错误。但自己写起这种网站来才会明白代码完善的重要性,果然测试真的是多少都不嫌少(。

前后端完全分离,前端主要靠Vue,混用了一点点jquery处理mounted期间的数据绑定。后端用了Express,第一次写Node果然还是有点虚的。

主站在等画师的图所以目前还没开放,投票页的页面地址可以戳这里参考。

开一个索引:

  • Part A:前端篇
    • 如何解决渐变色块的transition无效问题?
    • 如何有效地适配中、英文在线网络字体?
    • 有哪些好用又轻量的Font-Class/Unicode/SVG图标库?
    • 怎样控制页面根据滚动条位置刷新动画?
    • 怎样运用曲线,使动画效果更平滑?
    • 判断邮箱等信息是否有效,除了正则还有别的办法吗?
    • 还有什么想说的吗(?
  • Part B:后端篇
    • 用到的图形验证码是怎样处理逻辑的?
    • 有什么好用的短信平台?
    • 怎样解决多次回调导致代码不清楚的问题?
    • Node-mysql怎样防止sql注入?
    • 有什么简单的后端转发请求的方式?
    • 明明设定了响应头,为什么报跨域相关的错误?

前端篇

渐变色的动画表现

有一个需求是这样的:对于某个条形图,在选项选中的状况下对应条形的颜色会改变。为了美观,问题有两个:一是条形需要采用颜色渐变效果,二是颜色的变化需要半秒钟的平滑过渡。

这两个问题单独解决都非常简单。大家知道CSS3的渐变现在已经被广泛使用了,基本语法是

1
background: linear-gradient(white, black);

但只要把属性展开就可以发现,这样产生的渐变色是以图像的形式存在的:

1
background-image: linear-gradient(white, black);

因此,像background-color属性一样直接叠加transition动画是不可行的。试了一下也确实如此,设定好的transition属性会被直接忽略。

那么结果就是直接的变换非常丑,本颜狗完全无法容忍了(。

首先想到的办法是通过绝对定位来糊一个条形盖住原有的条形,然后使用透明度过渡效果来完成一个虚伪的颜色转换效果。CSS3提供opacity属性可以设置div元素的不透明级别,相当方便了。

这个办法是可行的,但因为我这部分用了bootstrap来布局,绝对定位的劣势就一览无遗了,所以还是打算用一些更好的办法。

后来静下心来想了想,这个渐变色条的过渡,真的需要进行两张图像——也就是两次linear-gradient的切换吗?

考虑了一下我自身的情况:网站整体都比较简约,所以渐变色条取的是类似色,对于颜色转换没有太高的要求。因此,这个过渡的过程可以当做单张图像的基础处理。类比一下关键帧动画,很容易想到这里可以通过色相和饱和度的调节来完成这个过渡动画。

查了一下发现CSS3的filter刚好支持hue,因此可以先打个关键帧:

1
2
3
4
5
6
7
8
@keyframes hue {
0% {
filter: hue-rotate(0deg);
}
100% {
filter: hue-rotate(51deg);
}
}

然后丢给animation调用:

1
animation: hue .5s linear;

这样的处理基本能实现期望的效果,不过它还是有色相处理的一贯弊端——颜色是按照色轮来转换的,因此想要达到较好的效果,要么选择相邻的颜色作为起始和终止,要么同时和饱和度一起进行细致的关键帧调整。

饱和度filter范例:

1
filter:saturate(1);

在线网络字体的适配

在线字体是为了解决用户没有对应的本地字体导致的页面丑和结构混乱。微软雅黑是真的丑啊别用了行不行

英文字体的网络适配其实已经非常完善了,以googlefont为首的一大批免费在线字体网站知名度还是挺高的。以它作为说明,在选择好需要的字体(以Barlow Condensed为例)之后,只需要在html头部加入

1
<link href="https://fonts.googleapis.com/css?family=Barlow+Condensed" rel="stylesheet">

或在样式部分引入

1
2
3
<style>
@import url('https://fonts.googleapis.com/css?family=Barlow+Condensed');
</style>

随后就可以应用字体了,非常方便。

1
font-family: 'Barlow Condensed', sans-serif;

如果要选择不同的font-weight,在引入时加入相关信息即可:

1
<link href="https://fonts.googleapis.com/css?family=Barlow+Condensed:400,500" rel="stylesheet">

其他类似的网站也大同小异,不过这个最好用,就不推荐别的了。

英文相对来说简单很多,而提供中文在线字体服务的网站就非常少见。就易用性、效果和价格方面分析,最终我确定下来使用的是有字库,小站基本可以免费用而且按需截取的质量很高。不过里面收录了很多商业字体,要注意版权问题。没有特殊情况的话还是win思源黑、mac苹方吧,好看又安心。

有字库是按渲染的字数收费的,每月有5W免费量,很适合做一些字少图多的网页。网站提供CSS和JS两种引入方式:CSS的引入需要提前把所有要用到的文字确定好,网站会自动截取成新的小字体,不会影响响应速度;JS则是每次都动态截取,有点慢但不用费心提前确定好文字了。不过根据我的使用经验强烈建议CSS,JS引入实在是非常容易崩,而且三天两头服务器维护,整个人都是不好的。

图标库推荐

用过不少大大小小的图标库,像ioniconslinea和经常和bootstrap搭配使用的font-awesome,质量都很高,而且内容丰富、风格统一。对于大型专题站或者主打视觉的页面,整齐划一的图标和风格还是挺重要的,因此这些都可以用得上。

不过有些只用得到几个图标的小站,把动辄几M的字体文件放在工程里,难免有点大动干戈的感觉。因此有不少提供图标切分的网站,在这方面就能够体现出优势了。

国内站强推阿里的图标库Iconfont,包含很多其他地方找不到的国内网站素材,缺陷在于图标太多,风格、大小都不统一,要自己人肉判断。通用站推Icomoon,图标质量很高,不过免费版的资源不多就是了。

位置驱动刷新动画

第一次提这个需求是在做宇宙级大失败企划说再见企划的时候。为了实现这个效果参考了很多站的源码,发现除了手撸的大神们,大家最通用的一个办法是使用第三方库WOW。配合居家必备的animate.css,可以轻易达成根据滚动条位置刷新的目标。

用起来也是非常简单易上手,先把带着wow选择器的元素隐藏掉:

1
2
3
.wow {
visibility: hidden;
}

然后挂在适配了animate的标签上就完成了:

1
<div class="demo wow slideInLeft"></div>

非常黑科技。可选的参数也都非常有用,比如延时什么的,具体参考官方文档。

平滑的动画效果

说到平滑首先想到的就是贝塞尔吧,一种根深蒂固的AE思想(。

而CSS3就引入了三阶贝塞尔曲线来辅助动画,感觉这玩意可以玩很久。定义的格式是cubic-bezier(P1x,P1y,P2x,P2y),首尾两点是固定的,可以调节的是两个控制点的位置。x轴表示时间,y轴表示变化率,调节控制点的二维坐标改变曲率来实现不同的移动效果。

我比较常用的可视化网站是这里,可以比较轻松地处理贝塞尔。

完成的曲线可以运用到transition里,像

1
transition: all 1.5s cubic-bezier(0.5, 0, 0, 0.5);

其实对于这个参数,平时用到的很多都是封装好的曲线,比如:

ease: cubic-bezier(0.25, 0.1, 0.25, 1.0)
linear: cubic-bezier(0.0, 0.0, 1.0, 1.0)
ease-in: cubic-bezier(0.42, 0, 1.0, 1.0)
ease-out: cubic-bezier(0, 0, 0.58, 1.0)
ease-in-out: cubic-bezier(0.42, 0, 0.58, 1.0)

y值也是可以为负的,会产生一些类似弹力的好玩的效果。

事实上很多loading动画也会大量应用贝塞尔,确实能很有效地提高页面的档次(。

信息判断

对于邮箱之类的这种格式判断首先想到的应该就是正则,不过我个人不太喜欢用正则处理非定制化的信息判断原因是代码看上去乱乱的,而且排带着一大堆正则的代码的错总觉得很头疼。

虽然嘴上说不要但其实这个投票页面还是很乖巧地全都套了正则

其实有个挺不错的轮子可以专门处理这些固定格式字符串的判断,叫Validator。Gxxhub上有好多validator,但个人觉得这个用起来最舒服,而且代码清楚功能丰富。比较适合稍微大一点的页面。

希望大家都重视测试(。

踩大坑的惨痛教训:如果,有可能的话,请务必把能测的浏览器都测一遍。

讲道理在校期间完全意识不到这个的重要性,在公司前端框架限制爆炸也没什么感受,但自己写起来看着好多参与者因为浏览器不支持而投票失败真是心在滴血了(。

并不完全是内核的锅,数据显示疑似有什么辣鸡浏览器因为检测不到favicon.ico就不显示页面?exom???

以及超棒浏览器Impossible Explorer不支持的方法真是太多了,分享几个:

  • Array.prototype.find() 三行顶十行的方法不能用

  • Array/String.prototype.includes() 判定数组中是否包含项目/字符串中是否包括子字符串最优雅的方法,不能用

  • String.prototype.startWith()

  • Object.values() 老生常谈了

  • 以及之前搞DOM的时候会用到的ChildNode.remove()。

真的是珍爱生命远离比较好(。

移动端的话,Chrome提供常用机型的尺寸,就在开发者工具的检查元素按钮旁边,快捷键Ctrl+Shift+M,可以用来测自适应。要做在线真机调试的话,腾讯爸爸提供了一个免费的远程移动端质量开放平台,是真·真机测试了,可以用来调试最终效果。

后端篇

极验验证码部署

现在网络上几乎唯一靠谱好用并且没被墙的第三方验证码服务就是极验了。在现在这种静态图形验证码形同虚设的年代,虽然网上也有攻破极验的经验分享,但它相比而言还是安全轻松一些。

而且感天动地免费版都非常好用,而且现在竟然已经能换滑动码的图像了(。

不过相对来讲官方给的sdk文档实在是有点差劲,尤其是前端很多地方都说得不清不楚,看上去也是挺长时间没有维护过,所以还是打算记录一下备份。

后端开始,首先npm安装gt3-sdk并引入,然后进行初始化:

1
2
3
4
5
var Geetest = require('gt3-sdk');
var captcha = new Geetest({
geetest_id: 'xxx',
geetest_key: 'xxx'
});

id和key都是注册了之后就可以拿到的。

然后开一个路由准备register:

1
2
3
4
5
6
7
8
9
10
11
12
13
captcha.register(null, function (err, data) {
if (err) {
console.error(err);
return;
}
if (!data.success) {
req.session.fallback = true;
res.send(data);
} else {
req.session.fallback = false;
res.send(data);
}
});

这里涉及到判定是否进入fallback模式(断网或者无法访问极验服务器等情况)的问题,详细信息可以参考官方文档的这一部分,当然也可以自己来确定方案。

再开一个路由做二次验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
captcha.validate(req.session.fallback, {
geetest_challenge: req.body.geetest_challenge,
geetest_validate: req.body.geetest_validate,
geetest_seccode: req.body.geetest_seccode
}, function (err, success) {
if (err) {
// 网络错误
} else if (!success) {
// 二次验证失败
} else {
// 二次验证成功
}
});

至此后端搞定,开始部署前端。先引入初始化函数:

1
<script src="gt.js"></script>

这个gt可以在这里下载,官方强烈推荐不要在线引用,那就不要用吧。

在生命周期的mounted阶段发请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$.ajax({
url: "to/backend/register",
type: "get",
dataType: "json",
success: function (data) {
initGeetest({
gt: data.gt,
challenge: data.challenge,
product: "embed",
offline: !data.success
}, handler);
},
error: function (err) {
// 网络错误
}
});

其中回调函数handler的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var handler = function (captchaObj) {
captchaObj.onSuccess(function () {
var validate = captchaObj.getValidate();
$.ajax({
url: "to/backend/verify",
type: "post",
dataType: "json",
data: {
geetest_challenge: validate.geetest_challenge,
geetest_validate: validate.geetest_validate,
geetest_seccode: validate.geetest_seccode
},
success: function (data) {
// 验证成功
},
error: function (err) {
// 验证失败
}
});
});
captchaObj.appendTo("#captcha");
};

对于initGeetest的各种定制需求可以参考官网的这一部分,大多数还是比较清楚的。

但重点说一下其中的product,分为popup(弹出式)、embed(嵌入式)、float(浮动式)、custom(自定义弹出区域)、bind(隐藏按钮类型)。每一种的部署方案都有所区别,可是这个药丸的说明文档的这个地方是真的乱七八糟讲不清楚,强烈建议去极验团队的github页面观摩。

搭好了框架,最后只需要把这个绑好的元素加入DOM,这里就会显示出验证码信息了。

1
<div id="captcha"></div>

极验官方提供了更多有用的方法,可以去官网查阅。

短信平台部署

虽然大家都吹大鱼但是大鱼好贵啊,于是就选了口碑差不多的云片。

短信两条一毛,注册自带十条测试量。前期要验证一大堆身份信息,然后填一下短信模板就可以开始使用有效的apikey处理请求了。云片要求部署的网站必须有图形验证码,而且是人工审核,所以没有的话这个也要解决一下。

云片的文档还是相当清楚的,基本没必要做什么说明。不过官网没有提供Node的部署方式,要自己动手。

解决回调地狱

因为投票页最多一次用到了六七个回调,这个问题不解决的话代码实在是太丑了,就咕果了一下。

一个非常优秀的解决方法是引入async/await模块来控制异步进程。虽然Promise也可以,但是仿佛已经有点时泪了于是作罢。虽然async也

写法真的是很简单,可以当做同步来写。最常用的几个方法:

  • series

    传入函数组成的数组或者对象就可以,函数之间通过callback连携,如果发现了error则直接进入error处理阶段。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    async.series({
    first: function(callback) {
    setTimeout(function() {
    callback(null, 1);
    },
    200);
    },
    second: function(callback) {
    setTimeout(function() {
    callback(null, 2);
    },
    100);
    }
    },
    function(err, results) {
    // result: {first: 1, second: 2}
    });
  • waterfall

    前一个函数的回调作为后一个函数的参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    async.waterfall([
    function(callback){
    callback(null, 'one', 'two');
    },
    function(arg1, arg2, callback){
    // arg1: 'one' arg2: 'two'
    callback(null, 'three');
    },
    function(arg1, callback){
    // arg1: 'three'
    callback(null, 'done');
    }
    ], function (err, result) {
    // result: 'done'
    });
  • parallel

    并行执行传入的全部函数。

防止sql注入

防止sql注入的主旨大意就是避免字符串的直接拼接。虽然可以使用connection.escape()来做安全拼接,但毕竟麻烦又不清楚。一个普遍使用的办法是使用查询参数占位符:

1
2
3
var query = connection.query('SELECT * FROM users WHERE id = ?, name = ?', [userId, name], function(err, results) {
// ...
});

这样的占位符内部会自动调用escape()方法编码传入参数,看上去就会清爽很多。

后端转发请求

我觉得这是一个很常见的需求啊,为啥网上那么难找,是我代码不足还是搜索力不足啊【

最后采取的方案是借助request模块来完成后端转发。npm安装并引入request模块,然后就可以直接在代码里像前端一样发请求:

1
2
3
4
5
6
7
8
9
10
11
request({
url: "https://sms.yunpian.com/v2/sms/single_send.json",
method: "POST",
json: true,
body: data,
headers: {
// ...
}
}, function (_err, _res, body) {
// ...
});

跨域相关

本渣新还是第一次处理跨域问题,不过因为后端是自己的所以处理起来会简单很多。以Express为例,只需要按需配好header就好了。

1
2
3
4
5
6
router.all('*', function (req, res, next) {
res.header("Access-Control-Allow-Origin", "http://xxx");
res.header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type");
res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
next();
});

还有个方便调试的chrome插件:Allow-Control-Allow-Origin: *

不过中间遇到了几次小麻烦,就是明明配置都没问题但前端还是报跨域的错。后来发现是res.send()出现了问题没有成功发回响应,整个人都不好了(。

虽然很蠢但万一遇到了类似问题可以参考一下((。