用Electron-nightmare快速實現微信天氣助手Syaya

項目背景

龐大的用戶安裝量和恐怖的用戶使用時間,微信已成爲國內移動互聯網上基礎設施級的應用。前端

以微信爲平臺的客服服務有不少方式,好比訂閱號,服務號,小程序,但受到微信官方的限制,若是想作一個聊天羣的自助客服, 或者私人訂製的客服,那就沒有了官方的自動化方案。例如場景一:折扣商品導購羣裏,只要@客服+產品名,就能夠立刻反饋一條產品折扣信息。場景二:徒步羣裏參加一個活動報名,只須要@客服+活動名稱 就能夠自助報名參加活動。node

Electron-nightmare一週時間開發的微信天氣查詢助手 syaya,就是一次技術驗證性嘗試。python

依賴項
syaya
syaya
syayalinux

image-imagegit

技術選擇 python、phantomjs 對比 Electron-nightmare

python的優勢,python是很是棒的語言,有着廣闊的舞臺,在可預見的將來,python的熱度還將大幅提高。python有大量的三方庫支持,各類重量級的應用框架、前瞻性產品基本上都提供python接口,有大量的社區。就微信機器人來講,有很是優秀的itchat。但就針對微信來講,python不是最好的解決方案。由於python須要從底層web通信協議開始分析,須要用抓包、分解、組裝,完徹底全的實現一個web協議的機器人,這其間的開發工做量過大。並且越陷入web底層,未來隨着官方協議變動,維護版本的工做量也不容忽視。這背離了咱們的初衷:在鬆耦合基礎上儘量的專一業務自己。並且python還面臨2與3的不兼容的坑,也是須要考慮的問題。angularjs

phantomjs 一個headless的瀏覽器框架,基於webkit。第一次瞭解到phantomjs是用來作爬蟲,phantomjs很是適合動態頁面的內容爬取,並且phantomjs能夠很方便的運行在linux服務器上,相對於普通的瀏覽器,phantomjs消耗的內存要少太多。但phantomjs也有一些問題。最大的問題就是phantomjs的headless,沒法很簡單的測試代碼片斷。而這幾年瀏覽器前端發展迅速,各類js新的特性不斷涌現,但phantomjs兼容性問題也凸顯:例如phantomjs對set Object就不支持。並且就總體性能而言,沒有V8引擎的支持的js,在執行效率上也要打折扣。因此咱們仍是要儘量站在巨人的肩膀上拓展業務github

站在巨人的肩上,Electron。Electron是基於Google的Chromium框架,原本是專一快速開發跨平臺桌面應用。但基於高性能瀏覽器的Electron是天生的web自動化框架。配合Node.jsexpress,能夠很簡單的實現一個RESTFul服務。而瀏覽器的可視化調試工具也大大提升了測試、調試效率。在Electron上作微信網頁版自動化更像是作一個plugin,這將減小了大量的的開發工做。web

python, phantomjs, Electron在微信自動化上的對比

功能 python phantomjs Electron
三方庫 ★★★★★ ★★★★ ★★★★☆
學習容易 ★★★☆ ★★★☆ ★★★☆
開發效率 ★★ ★★★☆ ★★★★☆
方便測試 ★★☆ ★★☆ ★★★★☆
運行效率 ★★★★☆ ★★☆ ★★★☆
維護簡單 ★★☆ ★★★☆ ★★★★☆
硬件要求低 ★★★★☆ ★★★☆ ★★☆

syaya的系統說明

從模塊分離的原則,在設計上,將微信機器人做爲服務提供出來,這樣便可以在之後按照其約定方式(RESTFul)提供新的對接方式,也隔離的自己業務與機器人服務之間的耦合性,並且分離後,基礎服務能夠遠程部署。設計思路以下圖chrome

  • 系統分解圖
  • image

微信網頁版代碼分析(模塊分析)

當前(2017-11-30日)微信網頁版是基於AngularJS v1.2.28開發的,採用MVC分離模式。經過分析最主要的index.js源碼咱們能夠大概知道各個功能模塊的組合方式。express

  • 模塊分解圖
  • image

在瞭解網頁版模式後,接下來就是獲取,並調用對應的功能模塊,具體實現位於 wxinjector.js

var injector = angular.element(document.body).injector();
var F = {
  chatFactory:injector.get('chatFactory'),
  contactFactory:injector.get('contactFactory'),
  confFactory:injector.get('confFactory'),
  loginFactory:injector.get('loginFactory'),
  accountFactory:injector.get('accountFactory'),
  utilFactory:injector.get('utilFactory'),
}
var CTRLS = {
  appController:angular.element(document.body).scope(),
  loginController:angular.element(document.querySelector("body > div.login.ng-scope")).scope(),
  chatSenderController:angular.element(document.querySelector("#chatArea > div.box_ft.ng-scope")).scope(),
}

編寫測試片斷代碼

chrome的dev-tools是很是好用的可視化測試工具,咱們能夠將功能片斷一點一點的在console中測試後,再添加到項目的代碼中

image

  • 測試得到聯繫人列表

angular.element(document.body).injector().get('contactFactory').getAllContacts();

  • 測試發送文本消息
function SendMessage(ToUserName, msg){
  var a = angular.element(document.querySelector("#editArea")).scope();
  var confFactory = angular.element(document.body).injector().get('confFactory')
  var chatFactory = angular.element(document.body).injector().get('chatFactory');
  a.editAreaCtn = msg;
  var e = chatFactory.createMessage({
    ToUserName:ToUserName,
    MsgType: confFactory.MSGTYPE_TEXT,
    Content: a.editAreaCtn
  });
  chatFactory.appendMessage(e),
  chatFactory.sendMessage(e),
  // O[chatFactory.getCurrentUserName()] = "",
  a.editAreaCtn = "";
}

項目代碼組織

|____conf                       # 程序配置文件
| |____service.json
| |____wxconf.js
| |____cities.json              # 城市氣象編碼表
|____lib                        # 模塊代碼
| |____weather.js
| |____inject
| | |____wxinjector.js
| |____wxbot.js
|____test                       # 測試代碼目錄
|____.gitignore
|____package.json
|____README.md
|____sy-cli.js                  # 天氣問答業務程序
|____syaya.js                   # 微信基礎服務程序

運行項目

  1. cnpm install
  2. 啓動微信基礎服務 node syaya.js
  3. 啓動天氣問答業務 node sy-cli.js
  4. 掃碼登陸微信帳號(業務帳號)
  5. 用另外一個微信帳號給業務帳號發信息測試

image
image

開發過程當中的一些備忘經驗

  • ubuntu server上運行electron
$sudo apt-get install xvfb libgtk2.0-0 libnotify-bin libgconf-2-4 libnss3 libasound2 libcap2-bin libcups2 libxtst6 libxss1
$xvfb-run node --harmony syaya.js
  • window.reload後,從新加載注入腳本的時機: 在事件'dom-ready'中注入
page.engin.on('dom-ready', function () {
    console.log("DOM-READY for inject...");
    page.engin.inject("js", WX_HELPER_JS);
  });
  • express下,讓curl的post正常工做
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.json({
  limit: '100kb',
  type: 'application/x-www-form-urlencoded'
}));
  • js的sleep等待
async function sleep(ms) {
  return new Promise(resolve => {
    var tm = setTimeout(() => {
      console.log("clear", tm);
      clearTimeout(tm);
      resolve(0);
    }, ms);
  });
}
async function test_sleep(){
  var r =  await sleep(2000);
  console.log(r);
}
test_sleep();
  • js協程處理單條消息, 僞代碼
var co = require("co");
function process_one_message(msg){
  co(function *(message) {
    var action = yield extract_action(message);
    var r = yield action.do_step1();
    if(r.status == "success"){
      r = yield action.do_step2();
    }
    r = yield action.do_final();
    r = yield make_response(action, message);
  }, msg);
}

參考

問題和建議

若是有什麼問題或者建議均可以在這個Issue和我討論
或者也能夠在微博上聯繫我:rockee阿木
或者微信聯繫:
image

相關文章
相關標籤/搜索