WeChatアプレットアーキテクチャ分析(下)


作者:趙啓明リンク:https://zhuanlan.zhihu.com/p/22932309出所:著作権は著者の所有になります.商業転載は作者に連絡して授権を獲得してください.商業転載ではないので、出典を明記してください.
このページは、WeChatウィジェットを実行できるweb環境を実現するのが、私が想像していたより難しいため、コードに対してWeChatが圧縮混淆しているためです.一方、主な原因は開発者ツールの内部論理呼び出しが複雑で、まったく再利用できないからです.
リアルタイムで実行するためのツールweptの開発はほぼ完了しました.コードを通してウィジェットのウェブ環境をより全面的に認識することができます.次に、その実現過程とリアルタイム更新の原理を紹介します.
アプレットwebサービスの実現
私はweptの開発において、koaを使ってwebサービスを提供しています.そして、ET-inmproveはテンプレートレンダリングを提供しています.
第一歩:ページテンプレートの準備
私達は3つのページが必要です.コントロール層index.として、一つはservice層servicesとして、もう一つはview層としてのviewがあります.
index.
  var __wxConfig__ = {{= _.config}}   var __root__ = '{{= _.root}}'
service.

  
  
  
  var __wxAppData = {}
  var __wxRoute
  var __wxRouteBegin
  global = {}
  var __wxConfig = {{= _.config}}
  
  
  
  {{each _.utils as util}}  
  {{/}}  
  {{each _.routes as route}}   var __wxRoute = '{{= route | noext}}', __wxRouteBegin = true;
  
  {{/}}
  
    window._____sendMsgToNW({
      sdkName: 'APP_SERVICE_COMPLETE'
    })
  

view.html:



  
  
  
  
  
  
   var __path__ = '{{= _.path}}'
  
  
  
  {{= _.inject_js}}
  
  
    document.dispatchEvent(new CustomEvent("generateFuncReady", {
      detail: {
        generateFunc: $gwx('./{{= _.path}}.wxml')
      }
    }))
  
  

第二步: 实现 http 服务

用 koa 实现的代码逻辑非常简单:

server.js


//      app.use(logger())// gzipapp.use(compress({
  threshold: 2048,
  flush: require('zlib').Z_SYNC_FLUSH}))//        app.use(notifyError)//             404   app.use(staticFallback)//    route   app.use(router.routes())app.use(router.allowedMethods())//    public           app.use(require('koa-static')(path.resolve(__dirname, '../public')))//       let server = http.createServer(app.callback())server.listen(3000)
router.js
router.get('/', function *() {
  //    index.html      ,   index   })router.get('/appservice', function *() {
  //    service.html      ,   service   })//   `/app/**`            router.get('/app/(.*)', function* () {
  if (/\.(wxss|js)$/.test(file)) {
    //       css     js
  } else if (/\.wxml/.test(file)) {
    //       html
  } else {
    //         ,      
    let exists = util.exists(file)
    if (exists) {
      yield send(this, file)
    } else {
      this.status = 404
      throw new Error(`File: ${file} not found`)
    }
  }})
第三ステップ:制御層機能の実現
上記の二段階を実現すれば、viewページにアクセスできますが、これはレンダリングだけで、何の機能もないことが分かります.view層の機能は制御層の通信に依存していますので、制御層がメッセージを受信できないと、何のイベントにも応答しません.
制御層は全体の実現過程の中で最も複雑なブロックであり、公式ツールのコードはnwjs及びreactなどの第三者コンポーネントとの結合が高すぎるため、直接に使うことができない.wept項目のSrcディレクトリの下でコントロール層論理のすべてのコードを見つけることができます.全体的にコントロール層は以下のいくつかの機能を担当します.
  • は、service層、view層、および制御層間の通信ロジック
  • を実現する.
  • ルーティング命令に従って、viewを動的に作成する(weptはiframeを使用して実現する)
  • .
  • は、現在のページに従って、headerとtabbar
  • を動的にレンダリングする.
  • は、元のAPI呼び出しを実現し、結果をservice層
  • に返す.
    wept内のiframe間の通信はmessage.jsモジュールによって実現され、コントロールページ(index)のコードは以下の通りです.
    window.addEventListener('message', function (e) {
      let data = e.data
      let cmd = data.command
      let msg = data.msg
      //     contentscript     ,     
      if (data.to == 'contentscript') return
      //        ,      
      if (data.command == 'EXEC_JSSDK') {
        sdk(data)
      //      view      service,         
      } else if (cmd == 'TO_APP_SERVICE') {
        toAppService(data)
      //    publish       view              (      ),
      //        service   ,                service
      } else if (cmd == 'COMMAND_FROM_ASJS') {
        let sdkName = data.sdkName
        if (command.hasOwnProperty(sdkName)) {
          command[sdkName](data)
        } else {
          console.warn(`Method ${sdkName} not implemented for command!`)
        }
      } else {
        console.warn(`Command ${cmd} not recognized!`)
      }})
    具体的な実現ロジックはsrc/command.js src/service.j***c/sdk/*.jsを見ることができます.view/serviceページについては、元のbridge.jsのwindow.postMessageをwindow.top.postMessageに変更すればいいです.
    view層の制御ロジックはsrc/view.js及びsrc/view Manage.jsによって実現され、viemanageはnavigateTo、redirectTo及びnavigateBackを実現し、service層がpublishというcommandを通じて伝えられた対応ページルーティングイベントに応答します.
    header.jsとtabbar.jsはreactに基づいて実現されるheaderとtabbarモジュールを含んでいます.(もとの計画はvueを使っていますが、元のjsモジュールと通信するAPIが見つかりませんでした.)
    sdkカタログにはstorage、録音、羅針盤モジュールが含まれています.他の比較的簡単な原生コールは直接にcommand.jsに書きました.
    以上は小さいプログラムを実行するために必要なwebserverのすべてのロジックを実現しました.その実現は複雑ではなく、主にマイクロメッセージという一連の通信方式を理解するのが困難です.
    プログラムのリアルタイム更新を実現します.
    第一歩:ファイルの変化を監視し、フロントエンドを通知する
    weptはchkidarモジュールを使ってファイルの変化を監視し、変化したらWebSocketを使ってすべてのクライアントに更新操作を知らせる.具体的にはlib/watch.jsとlib/sockett.jsに位置し、送信内容はjson形式の文字列です.
    フロントエンドコントロール層は、WebSocketメッセージを受信した後、postMessageインターフェースを介してview/service層にメッセージを転送する.
    view.postMessage({
      msg: {
        data: {
          data: { path }
        },
        eventName: 'reload'
      },
      command: 'CUSTOM'})
    view/service階でreloadイベントを傍受する:
    WeixinJSBridge.subscribe('reload', function(data) {
      // data       msg.data})
    第二ステップ:先端応答が異なるファイルの変化
    フロントエンドは4種類(wxml wxss Json javascript)の異なるタイプのファイルに対して4種類の熱い更新処理を行う必要があります.ここで、wxssとjsonは比較的簡単です.
  • wxssファイルが変化した後、フロントエンド制御層通知(postMessageインターフェース)対応ページ(app.wxssであればすべてのviewページ)を更新し、view層はメッセージを受信した後、cssファイルに対応するタイムスタンプだけを変更すればいいです.コードは以下の通りです.
  • jsonファイルの変化はまず判断が必要です.もしapp.jsonなら、私達は熱い更新ができません.だから、現在のやり方はページを更新します.ページのjsonについては、コントロール層の上でheaderに該当状態を設定すればいいです.
  • wxmlは、Virtual Dom APIから提供されたdiffアプリを用いて処理される.まず一つのインターフェースが必要です.Virtual Domを生成するために、新しいgeneratFun関数を取得し、Coaのrouterを追加します.
    o.subscribe('reload', function(data) {
        if (/\.wxss$/.test(data.path)) {
        var p = '/app/' + data.path
        var els = document.getElementsByTagName('link')
        ;[].slice.call(els).forEach(function(el) {
          var href = el.getAttribute('href').replace(/\?(.*)$/, '')
          if (p == href) {
            console.info('Reload: ' + data.path)
            el.setAttribute('href', href + '?id=' + Date.now())
          }
        })
      }})
    はインターフェースがあれば、インターフェースを要求して、リターン関数を実行してdiff appyを行うことができます.
  • javascript更新ロジックは比較的複雑です.まず、新しいjavascriptコードを取得するためのインターフェースです.
    socket.onmessage = function (e) {
      let data = JSON.parse(e.data)
      let p = data.path
      if (data.type == 'reload'){
        if (p == 'app.json') {
          redirectToHome()
        } else if (/\.json$/.test(p)) {
          let win = window.__wxConfig__['window']
          win.pages[p.replace(/\.json$/, '')] = data.content
          // header      __wxConfig__    state     
          header.reset()
          console.info(`Reset header for ${p.replace(/\.json$/, '')}`)
        }
      }}
    は次に、WindowsオブジェクトにReload関数を追加して、具体的な交換ロジックを実行します.
    router.get('/generateFunc', function* () {
      this.body = yield loadFile(this.query.path + '.wxml')
      this.type = 'text'})function loadFile(p, throwErr = true) {
      return new Promise((resolve, reject) => {
        fs.stat(`./${p}`, (err, stats) => {
          if (err) {
            if (throwErr) return reject(new Error(`file ${p} not found`))
            //               ,       reject
            return resolve('')
          }
          if (stats && stats.isFile()) {
            // parer      exec      wcsc      wxml     javascript   
            return parser(`${p}`).then(resolve, reject)
          } else {
            return resolve('')
          }
        })
      })}
    以上のコードはt.pageHolder関数に追加してから実行できます.最後にview層初期化後にPage関数をReload関数に切り替える必要があります.(もちろん、javascriptに戻る前にPageをReloadと名前を変更してもいいです.
    // curr      VirtualDom  if (!curr) returnvar xhr = new XMLHttpRequest()xhr.onreadystatechange = function() {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          var text = xhr.responseText
          var func = new Function(text + '
     return $gwx("./' +__path__+ '.wxml")')       window.__generateFunc__ = func()       var oldTree = curr       //   data        var o = m(p.default.getData(), false),       //   diff apply       a = oldTree.diff(o);       a.apply(x);       document.dispatchEvent(new CustomEvent("pageReRender", {}));       console.info('Hot apply: ' + __path__ + '.wxml')     }   }}xhr.open('GET', '/generateFunc?path=' + encodeURIComponent(__path__))xhr.send()
  • ようやくこの穴を埋めました.この一連の分析を通じて、開発者にもっと多くの考えを与えたいです.