你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 再遇柯裡化:瀑布 IM 的消息封裝

再遇柯裡化:瀑布 IM 的消息封裝

編輯:IOS開發基礎

1.jpg

背景介紹

最近在做微信訂閱號爬蟲的時候,突然感覺可以搞這樣一個報警系統:如果解析的內容出現了錯誤,通過『瀑布 IM』發送消息給我。

有這樣聰明懂事的爬蟲,絕對省心不少。

初步實現

功能嘛很簡單,就是爬蟲解析網頁的時候,如果發現解析的內容和期待的內容格式不相符(比如正則沒匹配上),則調用報警接口,預計應該是 pubu.error('extract item failed') 這樣的調用方式。

我們先分析一下接口需要哪些數據,瀑布的文檔裡是這樣描述的:

{
  "text": "文本",
  "attachments": [{
    "title": "標題",
    "description": "描述",
    "url": "鏈接",
    "color": "warning|info|primary|error|muted|success"
  }],
  "displayUser": {
    "name": "機器人名稱",
    "avatarUrl": "頭像地址"
  }
}

大概是需要:消息的內容,附件的標題、描述、鏈接、類型,發送者的名稱、頭像。

於是我們很快可以寫出一個報警函數:

function sendPubuMessage(type, sender, title, description, url) {
  const attachment = {
    title: title,
    description: description,
    url: url,
    color: type,
  }
  request.post('https://hooks.pubu.im/services/xxxxxxx', {
    json: {
      text: moment().format('GGGG-MM-DD HH:mm'),
      attachments: [attachment],
      displayUser: {
        name: sender,
      },
    },
  }, (err, response) => {
    if (err || response.statusCode !== 200) {
      console.error('網絡異常!提交瀑布失敗:' + err) // eslint-disable-line
    }
  })
}

然後調用方法如下:

sendPubuMessage('error', '微信爬蟲', 'Extract key failed!', 'I do xxxx xxxx and failed', 'http://my.url/for/this/error')

測試一下,木問題:

調整函數

然而,現在這個調用方法用起來還是不方便:

  • 每次需要手動輸入消息的級別,比如 error 這種,容易手誤

  • 每次需要手動輸入發送者的機器人名字,不易管理

  • 消息發送的頻道接口寫死在了函數裡,不方便定制

於是乎,需要把 sendertype 分離出來。

先用 buildType 來組裝 type ,生成各種消息類型,主要是定義 color 屬性,用於在消息中顯示不同級別的顏色:

function buildType(color) {
  return {
    color: color,
  }
}
const info = buildType('info')
const warning = buildType('warning')
const error = buildType('error')
const success = buildType('success')

再用 buildSender 來組裝 sender ,生成各種發送者,主要是定義 nameurl 屬性,即發送者的名稱和需要發送的頻道地址:

function buildSender(name, url) {
  return {
    name: name,
    url: url,
  }
}
const wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111')
const sogou = buildSender('搜狗爬蟲', 'https://hooks.pubu.im/services/222222222')
const log = buildSender('系統日志', 'https://hooks.pubu.im/services/333333333')

最後函數稍作調整,變成了這樣:

function sendPubuMessage(type, sender, title, description, url) {
  const attachment = {
    title: title,
    description: description,
    url: url,
    color: type.color,
  }
  request.post(sender.url, {
    json: {
      text: moment().format('GGGG-MM-DD HH:mm'),
      attachments: [attachment],
      displayUser: {
        name: sender.name,
        avatarUrl: sender.avatar,
      },
    },
  }, (err, response) => {
    if (err || response.statusCode !== 200) {
      console.error('網絡異常!提交瀑布失敗:' + err) // eslint-disable-line
    }
  })
}

調用的地方成了這樣:

// 由 微信爬蟲 發送一條 error 消息
sendPubuMessage(error, wechat, 'failed!', 'I xx and failed', 'http://my.url/for/this/error')
// 由 搜狗爬蟲 發送給一條 warning 消息
sendPubuMessage(warn, sogou, 'failed!', 'I xx and failed', 'http://my.url/for/this/error')

封裝接口

函數基本是確定了,但是這樣的函數外部對象需要使用的時候,只能:

const pubu = require('./lib/pubu')
pubu.sendPubuMessage(pubu.error, pubu.wechat, 'failed!')

這真是太丑了。我希望能夠這樣調用:

const pubu = require('./lib/pubu')
pubu.wechat.error('failed!')

我們需要改造!我們希望能直接通過 sender 對象發送消息,所以需要改寫一下 senderbuilder 函數:

function buildSender(name, url) {
  return {
    name: name,
    url: url,
    info: function(title, description, url) {
      sendPubuMessage(info, this, title, description, url)
    },
    warn: function(title, description, url) {
      sendPubuMessage(warning, this, title, description, url)
    },
    error: function(title, description, url) {
      sendPubuMessage(error, this, title, description, url)
    },
    success: function(title, description, url) {
      sendPubuMessage(success, this, title, description, url)
    },
  }
}

const wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111111111')

修改過後我們就可以這樣調用啦:

wechat.info('info test')
wechat.warn('warn test')
wechat.error('error test')
wechat.success('success test')

測試結果看起來還不錯:

重構實現

然而,這部分代碼看得我總是慌得很:

function buildSender(name, url) {
  return {
    name: name,
    url: url,
    info: function(title, description, url) {
      sendPubuMessage(info, this, title, description, url)
    },
    warn: function(title, description, url) {
      sendPubuMessage(warning, this, title, description, url)
    },
    error: function(title, description, url) {
      sendPubuMessage(error, this, title, description, url)
    },
    success: function(title, description, url) {
      sendPubuMessage(success, this, title, description, url)
    },
  }
}

為什麼這個世界上充滿了重復。

為什麼?為什麼?為什麼?為什麼?

是的,重復了四遍。

是的,上面那句是個雙關。

仔細想想,其實我們要做的就是封裝 sendPubuMessage 以便外部調用。這個函數接受三類參數:

  • type,消息類型,不同類型的消息有不用的顏色區分

  • sender,發送者,包括發送者名稱和發送到的頻道地址

  • message,後面三個參數都是消息的內容,統一歸為一類,title 是必須的, descriptionurl 是可選的

每傳入一個參數,其實這個函數就完善了一點點。

比如我傳入了 error ,那後面不管傳入什麼,這都是個發送 error 消息的函數。
比如我再傳入了 wechat,那後面不管傳入什麼消息,這都是個發送微信爬蟲的 error 消息的函數。

感覺有點眼熟,這不是柯裡化的思路嗎?不妨用柯裡化函數試試。

柯裡化

找了一個 JS 的柯裡化的庫:curry,柯裡化後的調用是這樣的:

const curry = require('curry')
const curreidSend = curry(sendPubuMessage)

function buildSender(name, url) {
  const sender = {
    name: name,
    url: url,
  }
  sender.info = curreidSend(info)(sender)
  sender.warn = curreidSend(warning)(sender)
  sender.error = curreidSend(error)(sender)
  sender.success = curreidSend(success)(sender)
  return sender
}
const wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111111111')

由於不再是 function 了,所以 this 失效,只能通過這種『聲明外賦值』的方式來實現。(JS 學藝不精,應該有更好的方法,歡迎指點)

看起來似乎是簡潔了一些,然而,在測試的時候發現,wechat.info 這個函數如果接受了少於3個參數就不會執行了。

比如這樣的時候:

wechat.info('info test')

仔細一想,柯裡化之後的函數應該是期待五個參數輸入,而此時我才輸入了三個參數: typesendertitle。講道理的話,此時的執行結果,應該是一個期待輸入兩個參數的參數。我們打印一下,果然:

console.log(wechat.info('info test').length) // 2

這就有點辣手了啊,柯裡化之後把我本來的可選參數給搞沒了,而大部分情況下其實我只傳個 title 就結束了,剩下來兩個參數是不會傳的。

換句話說為了省幾個字母的內部實現,現在每次外部調用都需要傳入兩個額外的參數。

你知道什麼時候我會覺得我是個天才嗎?

當我發現我以前原來是一個傻逼的時候。

整理一下思緒,柯裡化顯然需要把所有的參數都假設成需要輸入的參數,然後再做局部應用,要不然一個 () 人家怎麼知道是該直接調用返回運算結果,還是該局部調用返回一個新的函數呢?

那我可以在柯裡化的結果外面包一層啊,根據傳入參數的數量來決定生成的柯裡化的結果是該有幾個入參,比如這樣:

const buildCurreidSend = (type) => {
  return () => {
    const args = [].slice.call(arguments)
    const curriedSend = curry.to(2 + args.length, sendPubuMessage)
    return curriedSend(type)(sender)
  }
}

然而這方法並沒有調用,雖然通過 arguments 知道了參數的數量,但是並沒有將參數傳入並調用函數。

如果要調用,我需要自己對這個生成的函數傳入參數,而不是像現在這樣直接返回一個函數。

『傳入參數』之後才能『生成新函數』,『生成新函數』之後需要傳入『傳入的參數』來調用函數,那我為什麼不直接把參數組裝一下給這個函數呢?

想到這裡的時候我的內心是崩潰的。

但是也是光明的:是啊,為什麼我一定要柯裡化呢?

去柯裡化

這種參數不確定的場景,其實並不適合柯裡化,個人感覺。

一開始的思路是:需要局部調用函數,生成一個新的函數供外部調用。

其實也就是:提供部分參數,然後將參數補全並調用。那我為何不用 apply 方法呢,將外部傳入的參數把持住,然後在前面插上 typesender ,然後作為參數傳給那個函數就可以了。

而且由於我可以自己組裝函數,this 指針也重新起了作用:

function buildSendMessage(type) {
  return () => {
    const args = [].slice.call(arguments)
    args.unshift(type, this)
    sendPubuMessage.apply(this, args)
  }
}

function buildSender(name, url) {
  const sender = {
    name: name,
    url: url,
    info: buildSendMessage(info),
    warning: buildSendMessage(warning),
    error: buildSendMessage(error),
    success: buildSendMessage(success),
  }
  return sender
}

最後的完整代碼是這樣的:

const request = require('request')
const moment = require('moment')

// ----------------------------------------------------------------------------
// 消息類型
// ----------------------------------------------------------------------------

function buildType(color) {
  return {
    color: color,
  }
}

const info = buildType('info')
const warning = buildType('warning')
const error = buildType('error')
const success = buildType('success')

// ----------------------------------------------------------------------------
// 發消息的函數定義
// ----------------------------------------------------------------------------

function sendPubuMessage(type, sender, title, description, url) {
  const attachment = {
    title: title,
    description: (typeof description === 'object') ? JSON.stringify(description) : description,
    url: url,
    color: type.color,
  }
  request.post(sender.url, {
    json: {
      text: moment().format('GGGG-MM-DD HH:mm'),
      attachments: [attachment],
      displayUser: {
        name: sender.name,
        avatarUrl: sender.avatar,
      },
    },
  }, (err, response) => {
    if (err || response.statusCode !== 200) {
      console.error('網絡異常!提交瀑布失敗' + err) // eslint-disable-line
    }
  })
}

// ----------------------------------------------------------------------------
// 消息的發送者
// ----------------------------------------------------------------------------

function buildSendMessage(type) {
  return () => {
    const args = [].slice.call(arguments)
    args.unshift(type, this)
    sendPubuMessage.apply(this, args)
  }
}

function buildSender(name, url) {
  const sender = {
    name: name,
    url: url,
    info: buildSendMessage(info),
    warning: buildSendMessage(warning),
    error: buildSendMessage(error),
    success: buildSendMessage(success),
  }
  return sender
}

module.exports.wechat = buildSender('微信爬蟲', 'https://hooks.pubu.im/services/111111111111111')
module.exports.sogou = buildSender('搜狗爬蟲', 'https://hooks.pubu.im/services/111111111111111')
module.exports.log = buildSender('系統日志', 'https://hooks.pubu.im/services/222222222222222')

終於可以這樣調用接口了:

pubu.log.warning('Test Warning')
pubu.log.error('Test Error')
pubu.log.success('Test Success')

小結

經過一通蝦折騰,花了半天的時間。

JS 還是有待深入學習,感覺一旦遇到一些稍微深入一點的話題,自己的知識儲備就顯得乏力了。比如 this 比如 apply 比如 call 比如 bind 各種。

回想起來,學習 Swift 的過程中了解過一段時間的 FRP 並且整理了一些文章。雖然粗淺地看了一些理論知識,但是並沒有什麼真槍實彈的經驗。今天終於在項目裡實驗了一次,雖然結果以失敗告終,但是內心是

崩潰的。


相關文章:

  • Functional Reactive Programming in Swift

  • Swift 中的柯裡化

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved