全程直击:婚礼请柬 DIY 实录

21,383 阅读18分钟

本文首发于掘金,转载请注明出处,保留原文链接以及作者信息。

更新

2023.07.07

时隔四年多,我终于(敢)把代码放出来了!

对代码实现感兴趣的掘友们,请尽情 clone:github.com/JaxQian/wed…

此前一直有掘友问我要代码,很抱歉鸽了你们!对不起!鞠躬!

也祝福此后要结婚的掘友们,这份代码就当随份子了!

前言

这篇文章不算是技术干货,你大可以带着看热闹的轻松心态来瞧瞧,我,一个新郎,是怎么让结婚这事儿变得更加艰难的。

就在上个月,我举办了婚礼,如愿以偿地和心爱的姑娘缔结了一生的誓约。

在筹备婚礼的时候,我们就下定决心,要尽量不落俗套,尽量让参加婚礼的亲友感受到我们用心策划了每一个细节。作为重要一环的电子请柬,当然也不能将就。

如今,电子婚礼请柬已是一种常见之物。这种请柬一般都是婚庆 App 生成的 H5 页面,展示婚纱照和婚礼的时间、地点等,这两年还增加了送礼物、发祝福弹幕之类的功能。相信大家经常会在朋友圈见到朋友分享的请柬。

我是一个喜欢折腾的人,我喜欢用我所学的知识解决生活中的问题。那些通用的模板显然不能满足我。好巧不巧,我正好是一名前端开发者,出于敬业态度和极客精神,我决定自己做这份请柬。

想想也没啥难度嘛:弄一台云服务器,简单搭个网站,自己写页面,丢到服务器上,齐活。你看,一点都不难!

才怪。

前期准备

UI 模板

开头吹的气壮山河,但这第一脚就踢到了铁板上 —— 我不会做 UI 设计。我审视着镜子里的自己:Photoshop 水平二半吊子、个人网站的配色曾被面试官说辣眼睛、唯一一次被夸画画好是幼儿园时候…… 资质鲁钝,现在才开始提升审美、学习平面设计肯定是来不及了。我着急啊,我上火啊,我一缕一缕地揪头发啊……

等等,我是什么来着?程序员啊,我的看家本领是什么来着?面向搜索引擎编程啊!既然不能自己产出,只好践行拿来主义了。于是我 Google 到了许多电子请柬的模板(关键词:RSVP)。Why Google?防止和国内常用的模板撞脸。经过和新娘的反复讨论,我们敲定了一套风格最吸引我们的模板。

总算是有米可炊了。

服务器

云服务器我选的是阿里云的 ECS。因为只是用来放静态页面,所以不需要豪华配置,简单清爽(费用低)就好:

服务器配置

既然咱是前端工程师,那就正好借助近水楼台,用 Node.js 来搭建 Web 服务。参考阿里云的官方文档,我在 CentOS 里安装好了 Node 环境。框架方面我选用了 Express,因为它适合我(这种新手)。其实像电子请柬这种简单的应用,框架并非是必需品,但这是一个体验新鲜事物的机会不是吗?于是用 Express 搭建了简单的服务:

let http = require('http');
let express = require('express');

let app = express();

app.use(express.static('html'));
http.createServer(app).listen(80);

域名注册及备案

现在我可以访问到我的服务器中的网页,只不过其链接是这种原始人形态:http://127.127.127.127:80。对于一封婚礼请柬来说,这也太影响气质了吧。我把这样的链接发给亲友,人家还不以为我是盗号的?

不行不行。

玉皇大帝说:要有域名。于是便注册了域名。解析到服务器上后,就可以通过域名访问到自己的页面了。

另外,网站是要进行 ICP 备案的。你会需要填写一些表格,域名服务商(我的是阿里云)会寄给你一张大大的蓝色备案幕布,让你在幕布前拍照上传。这是一个不太复杂但必不可少、不算有趣但让我莫名感觉神圣的程序。最终你会得到一串备案号(如京ICP证030173号)。

https

在去年的一次小尝试中,我想把微信群二维码贴在墙上供邻居扫码加群。但群二维码是有时效的,定期打印新的二维码有点浪费纸。当时我恰好有自己的网站,就想了个办法:纸上打印一个网址的二维码,该网址指向我的网站中的一张图片 —— 微信群二维码,这样,我只需要定期上传新的群二维码即可。但测试时发现微信会拦截 http 协议的外链,需要用户确认访问方可进入页面(现在已经不再拦截)。彼时我没有余力继续研究 https,只好作罢。(现在想想,用第三方图床的链接应该就可以。)

所以 https 算是我未曾征服的疆土,这次我一定要把它拿下。

为了避免增加不必要的工(fèi)作(yòng),我选择了免费的 SSL 证书:

免费的 SSL

一手交钱一手交货,然后走个流程:

申请 SSL 流程

当流程走到最后一步 —— 下载证书时,却没有看到 Node.js 服务器专用的证书:

SSL 证书下载

幸好找到了一位前辈的文章,告诉我下载 Nginx 的就可以。有前辈撑腰,腰不酸腿不疼,一口气配完 https 不费劲:

let http = require('http');
let express = require('express');
let fs = require('fs');
let https = require('https');

//读取证书文件
let options = {
    key: fs.readFileSync('./0000000_域名.key'),
    cert: fs.readFileSync('./0000000_域名.pem'),
};

let app = express();

app.use(express.static('html'));
http.createServer(app).listen(80);
https.createServer(options,app).listen(443);

现在就可以用 https 协议的域名来访问请柬啦!

部署工具

  • 远程连接服务器:XShell
  • 文件上传:XFTP
  • 进程管理工具:PM2

PM2 可以帮你在更新代码后自动重启服务,老省事儿了。

忙活完这些,后端支撑基本就绪,可以开始着手开(魔)发(改)页面了。

页面开发

精简模板

先请大家检阅一下我们夫妇的审美水平:这是我们选中的模板。风格清新明快,传达出温馨甜蜜的情感信息。

页面中有些内容模块是不适用于我们的:

  • 『Featured People』:此处展示了伴郎伴娘的头像和信息,但彼时我们还未敲定人选。
  • 『Gifts Registry』:此处展示了新人预定礼品的商店 Logo —— 这个不太接地气。
  • 『RSVP』:这一部分是表单,用来确认受邀人的出席情况。由于表单的形式不灵活、不利于管理,我们打算用微信和电话来确认出席情况。
  • 『主题颜色设置』:即钉在界面左侧的转动的小齿轮。我们要做出的成品不需要提供换肤功能。

OK,砍掉这些无用部分,再将模板中的图片和文案替换为我们自己的信息,一封属于我们的电子请柬就初具雏形了。

但仅仅这样还是不行的,还需要稍稍整理一下代码。

字体压缩

首先面临的就是字体问题。

原模板使用的字体是 Oswald:

Oswald 字体

此款字体瘦高无衬线,效果优雅悦目,但不支持中文。恰当的字体能够提升用户体验,如果中文字体的风格与 Oswald 不符,整体感观将大打折扣。我需要一款风格相近的中文字体。

我在字由中反复检索比对,终于挖到了宝藏 —— 方正俊黑,该系列的方正俊黑_纤方正俊黑_粗颇有原模板字体的神韵:

方正俊黑_纤

方正俊黑_粗

这两款字体是非商用免费授权的,所以我可以放心大胆地使用。

字体免费授权

在方正官网上,想下载字体你得是设计师才行(见上图字体名称后面的 Tag)。但有趣的是,只要你注册账号时将身份勾选为设计师即可,官方还是挺厚道的。(从今天起我就是方正字库认证的设计师了)

在 CSS 中将字体设置为方正俊黑,页面效果恰如我愿。

但是!大家都知道,中文字体文件的体积,那简直,五大三粗。

请柬内容并没有那么多字,如果线上使用完整的字体包,必然拖慢加载速度。我把这样的请柬发给亲友,人家还不以为我是混子前端?

在百度上略微搜索就会发现,遇到这种问题的开发者不在少数,但网上的大多求助帖没有得到让我满意的解决方案。几经辗转,我发现了字蛛+,这个工具能帮我们自动分析并按需压缩字体文件。只需将本地代码路径或线上链接写进配置文件,然后运行 fsp run 即可完成。下图是我将字体进行分析压缩后的效果:

字体文件压缩后

这样的压缩效果,真让人神清气爽啊~~~(至于 FZJunHJW_Cu.TTF 为何没变,容我先卖个关子)

视差滚动(Parallax)

视差滚动,这个效果大家肯定不陌生吧?当你在模板页面中上下滑动页面时会发现,有些 section 的背景图片与内容并不是相对静止的,这种效果大大增加了页面的动(bī)感(gé),是我无论如何都想应用在请柬中的效果。

我们发送请柬的途径只有一个,那就是微信聊天。不论是热衷于在手机上抢红包的长辈,还是习惯在工作时将微信挂在电脑上的同龄朋友,微信绝对是最有效率的传达方式。

故此需要在桌面端(PC、Mac)和手机端的微信中测试我的请柬。

桌面端效果正常;但在手机端,视差滚动效果失灵了。

我们知道,实现视差滚动效果最简单的方式就是用 background-attachment: fixed;,而模板页面的做法,是在此基础上增加了动态修改 background-position,以实现滚动时背景图片以较慢的速度跟随(而不是 fix 在原地)。如今在移动端失效,想必是兼容性问题。让我们到 caniuse.com 中查证一下:

Can I use background-attachment?

果不其然,微信内置的 QQ 浏览器 X5 内核不支持这一 CSS 特性。有种说法是这一效果会给移动端浏览器带来性能上的压力,影响体验,所以干脆不支持该特性。

如果在手机微信中打开的请柬没有动感的效果,那将显得多么平庸啊。

在搜遍了古今中外所有技术贴而无果后,我决定用 JavaScript 来实现,正所谓以暴制暴、一力降十会……

思路是这的:

  • 先将背景图片以 <img /> 标签的方式加载到页面中,藏在视口上方;
  • 监听页面滚动,当需要特效的 section 滚动到视窗范围内时,将对应的图片拉到视口内登台表演;
  • 当元素滚动出视口后,对应的图片再退场,回到视口上方候场。

是不是感觉有点儿懵?瞧我这嘴,笨得跟棉裤腰似的,给你看代码你就懂了:

<!-- 背景图片 -->
<img src='img.png' />

<!-- 应用特效的 section -->
<section id='section'>All eyes on me.</section>
img {
    position: fixed;
    top: 0;
    left: 0;
    transform: translateY(-100%);
}
$(document).scroll(function () {
    var windowScrlTop = $(window).scrollTop();
    var windowH = $(window).height();
    if (
        ($('#section').offset().top - windowScrlTop) <= windowH 
        && ($('#section').offset().top - windowScrlTop) >-$('#section').height()
    ){
        // 当 section 位于视口中时:
        $('img').css('transform', 'translateY(0)');
    } else {
        // 当 section 位于视口以外时:
        $('img').css('transform', 'translateY(-100%)');
    }
});

通过这样的控制,就可以在微信内置浏览器中实现视差滚动的效果。无从验证这样做是否比 background-attachment: fixed; 性能要好,但从真机测试来看,上下滑动流畅,未出现卡顿。

我把页面优美、效果炫酷的请柬发给了新娘,已经准备好了接受汹涌而至的赞美和香吻 —— “咱们的请柬里怎么没有地图呀?我看人家的都有呢?”

😳哦,新需求是吧,好的我知道了。

地图与导航

在请柬中加入地图,目的就是明确婚礼地点的位置信息,方便亲友抵达。这就需要:

  1. 在地图上醒目标记地点位置;
  2. 点击地图能跳转到路线导航页面。

由于主要在微信中使用,我就无脑选择了腾讯地图。 在腾讯位置服务上注册账号、验证身份、申请开发密钥之后,即可开始开发。这里我用的是 JavaScript 地图 API

先在请柬页面中加载 API 服务:

<script charset="utf-8" src="https://map.qq.com/api/js?v=2.exp&key=YOUR_KEY"></script>
//“YOUR_KEY” 即为你自己的开发密钥

再设置容器元素并指定指定宽高:

<div id="map" style="width:500px; height:300px"></div>

接下来需要初始化:

var centerLatlng = new qq.maps.LatLng(39.908551, 116.397743);//地图的中心点的经纬度。
var myOptions = {
    zoom: 15,
    center: centerLatlng,
    mapTypeId: qq.maps.MapTypeId.ROADMAP
}
var map = new qq.maps.Map(document.getElementById("map"), myOptions);//地图实例

此时,展示地图的目的就达到了。但是为了提升交互体验,还需要做一些工作:

  1. 将婚礼地点标记出来。

    //在地图上添加标注:
    var anchor = new qq.maps.Point(10, 10),//相对于标记点的位置
        size = new qq.maps.Size(24, 24),//显示尺寸
        origin = new qq.maps.Point(0, 0),//相对于图片左上角的相对像素坐标
        icon = new qq.maps.MarkerImage('./img/center.gif', size, origin, anchor);//用于标记的图片的路径
    
    var marker = new qq.maps.Marker({
        icon,
        position: centerLatlng,
        map,
    });
    
  2. 用文字提示用户标记点是支持点击的:

    var label = new qq.maps.Label({
        position: centerLatlng,
        map,
        content: '点击红点查看导航',
        style: {
            margin: '-18px -10px 0px 10px'
        }
    });
    
  3. 实现点击后打开 App 导航(移动端)或详情地图页面(桌面端)的功能。如果是桌面设备,在新网页中打开以婚礼地点为中心的地图即可;可要是移动端,就要稍微繁琐一些:借助微信内置的地图服务调用第三方地图 App(百度地图、高德地图等)展示导航路线 ——— 想拔出这根萝卜,可得带出不少泥来。

    恭喜我自己,解锁了支线任务。

    那么,接下来的步骤就是:

    • 注册微信公众号。

    • 在公众号的【设置 - 公众号设置 - 功能设置】中配置 JS 接口安全域名

    • 获取 access_token

      //服务端 Node.js 代码:
      const request = require('request');
      request.get({
              uri: 'https://api.weixin.qq.com/cgi-bin/token',
              json: true,
              qs: {
                  grant_type: 'client_credential',
                  appid: YOUR_APPID, 
                  secret: YOUR_APPSECRET
              }
          }, (err, res, body) => {
              if (err) {
                  console.log(err)
                  return
              }
              
              if (body.errcode) {
                  return
              }
              console.log('返回值:',res);// {"access_token":"艾克赛斯_套肯","expires_in":7200}
          }
      );
      
    • 获取 jsapi_ticket

      //服务端 Node.js 代码:
      const request = require('request');
      request.get({
              uri: 'https://api.weixin.qq.com/cgi-bin/ticket/getticket',
              json: true,
              qs: {
                  access_token: access_token,
                  type: 'jsapi'
              }
          }, (err, res, body) => {
              if (err) {
                  console.log(err)
                  return
              }
              
              if (body.errcode) {
                  return
              }
              console.log('返回值:',res);
              /* 返回值示例:
              {
                "errcode":0,
                "errmsg":"ok",
                "ticket":"bxLdikRXVbTPdHSM05e5u5sUoXNKd8-41ZO3MhKoyN5OfkWITDGgnr2fwJ0m9E8NYzWKVZvdVtaUgWvsdshFKA",
                "expires_in":7200
              } */
          }
      );
      
    • 根据签名算法生成签名:

      // 服务端 Node.js 代码:
      const crypto = require('crypto');
      
      // sha1加密
      function sha1(str) {
          let shasum = crypto.createHash("sha1")
          shasum.update(str)
          str = shasum.digest("hex")
          return str
      }
      
      // 生成时间戳
      function createTimestamp() {
          return parseInt(new Date().getTime() / 1000) + ''
      }
      
      // 生成随机串
      function createNonceStr() {
          return Math.random().toString(36).substr(2, 15)
      }
      
      // 字典排序
      function raw(args) {
          var keys = Object.keys(args)
          keys = keys.sort()
          var newArgs = {}
          keys.forEach(function (key) {
              newArgs[key.toLowerCase()] = args[key]
          })
      
          var string = ''
          for (var k in newArgs) {
              string += '&' + k + '=' + newArgs[k]
          }
          string = string.substr(1)
          return string
      }
      
      var ret = {
              jsapi_ticket: ticket,//上个步骤中获取到的 jsapi_ticket。
              nonceStr: createNonceStr(),
              timestamp: createTimestamp(),
              url:'www.url.com',//当前网页的URL。
          };
          
      var string = raw(ret);
      const signature = sha1(string);//这就是我们需要的签名。
      
    • 在页面中引入 JS 文件:

      <script src="http://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
      
    • 权限验证(所需参数应向后端请求得到):

      //前端 JavaScript 代码:
      wx.config({
        debug: true, // 开启调试模式。
        appId: '', // 必填,APPID
        timestamp: , // 必填,生成签名的时间戳
        nonceStr: '', // 必填,生成签名的随机串
        signature: '',// 必填,上个步骤中生成的签名
        jsApiList: ['openLocation'] // 必填,需要使用的JS接口列表,此处为地理位置接口 openLocation。
      });
      
    • 监听点击事件:

          qq.maps.event.addListener(marker, 'click', function () {
              //判断运行设备是移动设备还是桌面设备
              if ((navigator.userAgent.match(
                      /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i
                      ))) {
                  //移动设备中调用 JSSDK 接口:
                  wx.openLocation({
                      latitude: 39.908551,
                      longitude: 116.397743,
                      name: '天安门',
                      address: '北京市东城区东长安街',
                      scale: 15, // 地图缩放级别,范围为从1~28。
                  });
              } else {
                  //桌面设备中打开新网页展示地图:
                  window.open('https://j.map.baidu.com/26/BK');
              }
          });
      

这一整套都操练完后,总算是实现了地图导航功能。

姓名可定制

可能各位看官读到这里已经和我一样累了 —— 费这么大劲,做出来一个稀松平常的静态页面,真的值得吗?

当然不止如此。

DIY 电子请柬的念头刚刚萌生之时,我就下定决心要赋予这份请柬一个亮点:嘉宾姓名可定制。

想想过年时你收到的群发短信,礼貌有余而诚意稍欠。你知道文字内容都是对方复制粘贴来的,所以那一行行气氛热烈的拜年话,你压根不会仔细看。

而我要做的,是让每一位嘉宾在请柬里看到自己的名字,让嘉宾明白:这封请柬是专属于你的,跟别人的都不一样。

这是我向受邀亲友表达敬爱的方式,也是我为了让自己的婚礼尽量不流于形式而做出的挣扎。

我将名字分为三部分:

  1. 前缀,可选项:亲爱的、尊敬的。
  2. 姓名或亲属称呼,手动输入,如:王大锤/老舅等。
  3. 后缀,可选项:先生、女士、小姐。(如果第二部分是亲属称呼,则省略第三部分)

实现思路是这样的:

  • 将嘉宾名称作为请求参数拼接到 URL 的末尾,例如:

    https://www.a.com?prefix=尊敬的&name=王大锤&suffix=先生
    
  • 在页面中用 JavaScript 获取到参数,动态插入到对应位置。

    const info = window.location.search;
    if (info.length) {
    	const arr = info.replace(/\?/, '').split('&');
    	const prefix = (arr[0].split('='))[1];
    	const name = (arr[1].split('='))[1];
    	const suffix = (arr[2].split('='))[1];
    	
    	$('#prefix').html(decodeURI(prefix))
    	$('#name').html(decodeURI(name))
    	$('#suffix').html(decodeURI(suffix))
    }
    

这样,只要把对应的称呼拼到链接后面发给嘉宾,对方打开看到的就是内容专属的请柬了。

在前文中字体压缩的部分里,我提到了没有压缩 FZJunHJW_Cu.TTF 这个字体文件。读到这里你应该知道原因了 —— 自定义称呼所用的汉字是不确定的,所以只能加载完整的字体包。

可是如果给每个嘉宾发请柬时都手动去拼链接,效率低不说,还容易出错。如果真这么整,我大概会被业界开除吧。

Don't repeat yourself.

解决这一需求的工具并不复杂:输入端是可变的文字,输出端是一个 URL。为了将懒惰发挥到极致,我还增加了一键复制功能和生成短网址。 页面实现非常简单 —— VUE + ElementUI 快速出货:

请柬网址生成器

在我当时试用的两三个免费的短网址服务中,只有百度短网址支持 生成 https 短网址,这样更保险一些。七月份的时候,百度短网址还是不收费的,现在变为收费模式了,不过生成短网址的速度提升了很多。

整个请柬到这里就全线贯通了,经过简单的测试和优化,就可以投入使用了。

微信封禁与申诉

在距离婚礼还有两个月的一个阳光明媚的上午,我心情舒畅地登录微信,哼着小曲儿打开嘉宾名单,行云流水般地输入姓名、生成网址、发送请柬,享受着亲友们的称赞和祝福,感觉自己牛逼坏了。

突然!

我懵了。

明明是人畜无害的静态页面,咋就被定性为洪水猛兽了呢?

冷静冷静。即使再离谱的表象,其背后必定都有合乎逻辑的缘由存在。不是说咱页面包含那个还有那个么,先从这个方向着手。我找到了一款 Chrome 插件,可以检测页面违规内容:

只要打开待检测页面,再点击 Chrome 右上角的 🚫 logo,该插件会在一个弹窗中列出页面中出现的嫌疑词,并在对应位置用明黄底色标记出来。以掘金首页为例,效果如下:

那么我的请柬页面中有哪些敏感词呢?

我可去你的吧。

我想微信所用的文本审核词库与插件的词库虽不完全相同但也必有重叠,我又用肉眼反复排查了文案和代码,也没有发现可疑的词汇。

我猜是因为我短时间内频繁地给多个用户发送链接(而且链接还带有参数),触发了微信的防御机制,将我的行为断定为骚扰行为之类的异常操作,所以将我的请柬封禁了。

好冤好委屈啊。别无他法,我点击微信拦截页面底部的 申请恢复访问,填写相关信息后提交,就开始了充满焦躁和挫败感的等待过程。

过了不到半小时……

我着急啊,我上火啊,我一缕一缕地揪头发啊……

我又从微信外部链接内容管理规范中找到了申诉邮箱,诚诚恳恳地写了一封血泪之书寄了过去。对方很快回复我说已经解封,我测试了,确实能正常访问了。总算雨过天晴了,我决定中午多吃几碗抒发一下愉悦的心情。

饭后发现,又被封了…… 这简直让我想起了被微信小程序审核不通过支配的恐惧。

总之,经过了反反复复的被封 — 解封、再被封 — 再解封后,我的请柬总算不再被认为是十八禁内容了。那两天我的人生就像过山车一般大起大落、上蹿下跳,安全落地后仍然心悸不已。

结语

好了,各种尺寸屏幕前的各位观众朋友们,这就是我 DIY 电子请柬的全过程。技术含量不高,但曲折琐碎,当然乐趣和成就感也与投入成正比。

讲了这么多,最想和大家分享的就是,会写代码是很了不起的事情

从笨拙地敲出 Hello, world 开始,我们学习语言语法,探索各种库与框架,钻研算法…… 我们会走得很远,我们将冲在最前。

可能我们都终日为工作所累,最初对编程那份最真挚的热爱和憧憬慢慢地淡了。

但请别忘了,编程是一种魔法,它可以解决工作中的问题,也同样能给你的生活添加不一样的色彩。也许只是会一点 HTML,就能做出自己想要的请柬。

我们都坚信,编程能给我们带来无穷的乐趣,这也就是为什么你在读这篇文章。


感谢大家的耐心阅读,若能针对文中的技术点指教一二,我将感激不尽;也欢迎遇到相同难题的掘友来和我讨论。

最后,祝大家都能和自己爱的人长相厮守、比翼连枝。