100行代碼實現從博客向公衆號引流

前幾日在朋友圈刷到一篇文章 我是怎麼把博客粉絲轉到公衆號的,以爲至關有創意。最近一年在作 toC 產品,一直在談拉新留存轉化。這恰好能夠做爲一個黑客增加的成功案例。javascript

鑑於我本身也有一個博客,而且日均UV在200左右,決定來試一試。整理了一下思路,差很少與短信驗證碼的邏輯類似,因而花了一天時間搞定。css

另外,在此以前我也花了一天時間調研了 serverless。因此你徹底能夠零成本實現從博客到公衆號引流的功能html

需求

先來談一談需求點:前端

  1. 博客的內容被二維碼彈框遮擋,彈框上有口令及公衆號二維碼
  2. 掃碼關注二維碼並回復口令,回覆口令後刷新博客彈框消失

需求很簡單,如圖下所示。你也能夠去個人網站 天天學習一點點 查看實現效果vue

用戶體驗

不得不說,彈窗實在是一件傷用戶體驗的事情了。但也有一些措施,可以讓用戶體驗變得稍微優化一點java

  1. 簡單的口令,如只有四位數字
  2. 非強制的彈窗,瀏覽內容時只有必定概率會出現彈窗,即便出現彈窗,刷新一下便可跳過

實現

簡單過一遍技術棧,博客採用了 vuepress,前端的技術棧就是 vue 了。node

因爲是一個小小的服務,後端則儘量輕量,因而我選擇了 koa。爲了方便後端遷移,如我之後將會遷移到 serverless,則使用無狀態服務,即不依賴數據存儲。若是沒有狀態,那怎麼認證用戶呢?使用 jwtgit

關於部署,則使用 dockerdocker-compose 以及 traefik。至於部署這塊,我有一個基礎設施很完善的服務器環境,能夠參考文章 當我有一臺服務器時我作了什麼github

這下子實現思路就很清晰了:面試

  • css 部分使用選擇器控制只顯示文章的前兩個標籤,其他隱藏
  • js 部分有兩個請求,一個根據口令請求 jwt,一個根據jwt判斷是否合法,口令是四位數字隨機生成
  • 後端,使用koa作一個簡單的服務,使用jwt免掉存儲狀態,且可使用四位簡短數字,避免衝突
  • 存儲,一個 lru,存在內存裏,只留三分鐘,用來交換 jwt
  • 部署,使用 docker composedocker 因爲無狀態,很容易遷移到 serverless。且在微信環境下很容易製做測試環境與生產環境
  • 網關 traefik,方便自動服務發現與證書管理

CSS

在前端控制內容被二維碼遮擋的主要實如今於 CSS,而CSS主要控制兩點

  1. 彈框
  2. 只顯示博客內容前N段

咱們博客大部分使用靜態生成器,從 markdown 生成,生成的 html 大體長這個樣子。

<div class="content">
  <h1></h1>
  <p></p>
  <p></p>
</div>
複製代碼

如今前端的發展趨勢是狀態即UI,咱們使用一個變量 isLock 來控制全部樣式。咱們在 vue template 中加入彈框。

<div :class="{ lock: isLock }">
  <!-- markdown內容 -->
  <Content />
  <!-- 彈框 -->
  <div class="content-lock">
  </div>
</div>
複製代碼

彈框的顯示隱藏容易控制,那 Content 即文章內容的呢,如何控制只顯示前N段?

這確定難不倒曾經三個月的工做只寫 CSS 的我,使用 nth-childstylus 代碼以下

.theme-default-content.lock
  .content__default
    :nth-child(3)
      opacity .5

    :nth-child(4)
      opacity .2

    :nth-child(n+5)
      display none

  .content-lock
    display block
複製代碼

另外,咱們也加了點漸變效果提高觀感:前兩段內容顯示,第三段第四段漸變,五段之後所有隱藏。具體見 css 代碼

解鎖狀態

vue 中主要控制狀態 isLock,除了真實解鎖邏輯還有一個隨機性的彈窗。this.lock 表明是否解鎖,this.isLock 表明是否顯示彈窗

{
  data () {
    return {
      lock: false,
      code: ''
    }
  },
  computed: {
    isLock () {
      return this.lock ? Math.random() > 0.5 : false
    }
  },
}
複製代碼

口令

口令須要是持久化的:保證每次刷新頁面口令都是一致的。所以口令存儲於 localStorage 中,隨機生成四位數字,代碼以下

function getCode () {
  if (localStorage.code) {
    return localStorage.code
  }
  const code = Math.random().toString().slice(2, 6)
  localStorage.code = code
  return code
}
複製代碼

微信開發

接下來的邏輯是

  1. 在微信中把口令傳給後端
  2. 前端刷新解鎖

先看在微信這邊的邏輯。我對微信開發封裝成了簡單的路由形式,核心邏輯以下:當接收到數字碼時存儲到 cache 中,這裏使用了一個簡單的內存 lru,只存儲口令三分鐘

cache 中存儲了 code: userOpenId 鍵值對

function handleCode (message) {
  const { FromUserName: from, Content: code } = message
  // 對於 code,存儲三分鐘
  cache.set(code, from, 3 * 60 * 1000)
  return '您好,在三分鐘內刷新網站便可無限制瀏覽全部文章'
}

const routes = [{
  default: true,
  handle: handleDefault
}, {
  text: /\d{4}/,
  handle: handleCode
}]
複製代碼

用戶認證

此時的用戶認證就很簡單了,傳統形式的基於 session 的用戶認證。後端採用 koa 開發,代碼以下,此時還使用了 JOI 作了簡單的輸入檢驗

exports.verifyCode = async function (ctx) {
  const { code } = Joi.attempt(ctx.request.body, Joi.object({
    code: Joi.string().pattern(/\d{4}/)
  }))
  if (!cache.get(code)) {
    ctx.body = ''
    return
  }
  const from = cache.get(code)
  ctx.body = jwt.sign({ from }, secret, { expiresIn: '3y' })
}
複製代碼

口令太短會不會形成衝突

不會,因爲在服務端用戶口令只在內存中存在三分鐘,因此衝突的可能性很小。那三分鐘以後,如何進行用戶狀態的持久化呢?

用戶狀態持久化

可是此時有一個問題,cache 只能存儲維護三分鐘數據狀態。這個問題如何解決?

此時使用 JWT 來作用戶認證。所以校驗口令時返回生成的 jwt,在瀏覽器端持久化,使用它來保持用戶狀態。關於 JWT,能夠看我之前的文章 JWT 深刻淺出

exports.verifyCode = async function (ctx) {
  // ...
  ctx.body = jwt.sign({ from }, secret, { expiresIn: '3y' })
}
複製代碼

持久化用戶認證邏輯前端部分

async function verifyToken (token) {
  const { data: { data: verify } } = await request.post('/api/verifyToken', {
    token
  })
  return verify
}

async mounted () {
  const code = getCode()
  this.code = code
  if (!localStorage.token) {
    this.lock = true
    const token = await verifyCode(code)
    if (token) {
      localStorage.token = token
      this.lock = false
    }
  } else {
    const token = localStorage.token
    const verify = await verifyToken(token)
    if (!verify) {
      this.lock = true
    }
  }
}
複製代碼

持久化用戶認證邏輯後端部分

exports.verifyToken = async function (ctx) {
  const { token } = Joi.attempt(ctx.request.body, Joi.object({
    token: Joi.string().required()
  }))
  ctx.body = jwt.verify(token, secret)
}
複製代碼

如何保證用戶取消訂閱後再次彈框

當後端使用 jwt 用戶認證時,服務器端收到 openId,根據 openId 獲取用戶信息,若是沒有獲取到,說明用戶取消了訂閱。

可是獲取用戶信息此項權限我的公衆號不曾擁有

部署

開發完成以後使用 dockerdocker-compose 部署,traefik 作服務發現,經過 https://we.shanyue.tech 暴露服務,這三者在本系列文章中有所介紹

Dockerfile 較爲簡單,配置文件以下

FROM node:10-alpine

WORKDIR /code 
ADD package.json /code RUN npm install --production 
ADD . /code 
CMD npm start 複製代碼

docker-compose.yaml 配置文件以下

version: '3'

services:
 wechat:
 build: .
 restart: always
 labels:
 - traefik.http.routers.wechat.rule=Host(`we.shanyue.tech`)
 - traefik.http.routers.wechat.tls=true
 - traefik.http.routers.wechat.tls.certresolver=le
 expose:
 - 3000

networks:
 default:
 external:
 name: traefik_default
複製代碼

測試環境與生產環境

當咱們須要測試微信公衆號時,直接使用本身的公衆號不太合適,特別是當已有上線內容時。微信官方提供了測試公衆號,咱們能夠從新填寫 域名 以及 token。在測試環境使用域名 https://we.dev.shanyue.tech

咱們在 docker-compose 中使用 service 中的 wechat 表明生產環境,wechat-dev 表明測試環境

wechat-dev 經過文件掛載提供服務,能夠更新重啓應用,即可以作到實時更新代碼,並實時在測試公衆號中看到效果。

docker-compose.yaml 配置文件以下

version: '3'

services:
 wechat:
 build: .
 restart: always
 labels:
 - traefik.http.routers.wechat.rule=Host(`we.shanyue.tech`)
 - traefik.http.routers.wechat.tls=true
 - traefik.http.routers.wechat.tls.certresolver=le
 expose:
 - 3000

 wechat-dev:
 image: 'node:10-alpine'
 restart: always
 volumes:
 - .:/code
 working_dir: /code
 command: npm run dev
 labels:
 - traefik.http.routers.wechat-dev.rule=Host(`we.dev.shanyue.tech`)
 - traefik.http.routers.wechat-dev.tls=true
 - traefik.http.routers.wechat-dev.tls.certresolver=le
 expose:
 - 3000

networks:
 default:
 external:
 name: traefik_default
複製代碼

代碼開源

關於前端代碼,皆在 shfshanyue/Daily-Question

關於後端代碼,皆在 shfshanyue/wechat

而本篇文章,屬於 我的服務器運維指南 的案例篇,關於 dockercomposetraefik 等基礎設施的搭建均在本系列中有所介紹

獲取一個永久 token

除了經過掃碼回覆公衆號口令得到文章解鎖外,這裏也有一個永久 token 能夠解鎖。複製如下代碼到控制檯刷新便可解鎖所有文章

localStorage.token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1Nzc2MjI1MjEsImV4cCI6MTY3MjI5NTMyMX0.tB-CgK6aIo3whD-mAu3X37XT8q9v2bVXxG6llodznws'
複製代碼

我是山月,能夠加我微信 shanyue94 與我交流,備註交流。另外能夠關注個人公衆號【全棧成長之路】

若是你對全棧面試,前端工程化,graphql,devops,我的服務器運維以及微服務感興趣的話,能夠關注我
相關文章
相關標籤/搜索