Talking about iOS Hot Repair Architecture Downgradable

Keywords: Mongoose Database MongoDB Vue

Router is really a very good design pattern, which can achieve ultra-low coupling. As the last section of the series, we will coordinate the three ends to achieve a hot repair architecture. Next, the definition of this architecture is that when the business logic of the Native page appears Bug, it can not be packed online immediately to pass the audit, after passing. Desktop configuration demotes the page to H5, waiting for the repair to be completed before returning to Native's design idea.

But as I write here, I can't help feeling that the architecture I wrote is not a traditional hot update, but a disguised replacement. Like AT&T's cellular data from LTE to 3G, it is the difference in loading speed, but it must be better than Bug.

Let's finish the front page first. As an M station, the navigation bar needs to be displayed under the browser's access, but not when the mobile terminal is downgraded. So we need to finish this work first. First, we need to judge the jump situation by get parameters.

src
├── javascripts
│ ├── http.js
│ └── regex.js

regex.js

export function URLQuery(key) {

  let reg = new RegExp("(^|&)" + key + "=([^&]*)(&|$)");
  let r = window.location.search.substr(1).match(reg);
  if (r != null) {
    return unescape(r[2]);
  }
  return null;
}

Next we create the navigation component:

src
├── components
│ ├── J1.vue
│ └── navigation.vue

navigation.vue

<template lang="html">
  <div v-show="show">
    <div class="nav">
        <span class="title">{{documentTitle}}</span>
    </div>
    <div class="nav-offset"></div>
  </div>
</template>

<script>

import {
  URLQuery
} from '../javascripts/regex'

export default {
  data () {
    return {
      show: true
    }
  },
  props: {
    documentTitle: String
  },
  mounted:function () {
    if (URLQuery('client') === 'app') {
      this.show = false;
    }
  }
}
</script>

<style scoped>

.nav {
    width: 100%;
    height: 44px;
    position: relative;
    background: rgba(248,248,248,1.0);
    border-bottom: 1px solid lightgray;
    position: fixed;
    top: 0px;
    left: 0px;
    z-index: 1;
}

.nav-offset {
    height: 44px;
}

.title {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

</style>

Mount subcomponents in J1.vue

<template lang="html">
  <div>
    <navigation :documentTitle="title"></navigation> //update
    <div class="cell" v-for="model in models">
        ![](model.imageUrl)
        <span>{{model.text}}</span>
        <span>{{model.detailText}}</span>
    </div>
  </div>
</template>

<script>

import navigation from './navigation' //update
import { GET,URL } from '../javascripts/http'
import { URLQuery } from '../javascripts/regex' //update

export default {
  data () {
    return {
      title: '',
      models: []
    }
  },
  components: { //update
    navigation
  },
  methods: {
    request: function() {
      GET(URL.getJ1List).then((data) => {
          this.models = data.models;
      }).catch((error) => {

      })
    }
  },
  mounted:function () {
    this.title = document.title = "J1";
    this.request();
    // alert(window.location.href);
  }
}
</script>

<style scoped>

.cell {
    height: 100px;
    position: relative;
    border-bottom: 1px solid lightgray;
}

.cell img {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    padding-left: 10px;
}

.cell span:first-of-type {
    position: absolute;
    top: 35%;
    transform: translateY(-35%);
    padding-left: 84px;
    font-size: 15px;
}

.cell span:last-of-type {
    position: absolute;
    top: 65%;
    transform: translateY(-65%);
    padding-left: 84px;
    font-size: 12px;
}

</style>

On the mobile side, the webView page is passed on:

extension Router {

    func addParam(key: String, value: Any) {
        params?[key] = value
    }

    func clearParams() {
        params?.removeAll()
    }

    func push(_ path: String) {

        guardRouters {
            guard let state = self.routers?[path] as? String else { return }

            if state == "app" {
                guard let nativeController = NSClassFromString("RouterPatterm.\(self.map[path]!)") as? UIViewController.Type else { return }
                currentController?.navigationController?.pushViewController(nativeController.init(), animated: true)
            }

            if state == "web" { //update

                let host = "http://localhost:3000/"
                var query = ""
                let ref = "client=app"

                guard let params = self.params else { return }
                for (key, value) in params {
                    query += "\(key)=\(value)&"
                }

                self.clearParams()

                let webViewController = WebViewController("\(host)\(path)?\(query)\(ref)")
                currentController?.navigationController?.pushViewController(webViewController, animated: true)
            }
        }
    }
}

The display and hiding of navigation bar is judged by whether the client is app. Next, we communicate with JS through WKWebView:

class WebViewController: ViewController {

    fileprivate lazy var configuretion: WKWebViewConfiguration = { [weak self] in
        let configuretion = WKWebViewConfiguration()
        configuretion.userContentController.add(self!, name: "push") //update
        configuretion.userContentController.add(self!, name: "params") //update
        return configuretion
    }()

    ...    
}

extension WebViewController: WKScriptMessageHandler {

    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { //update

        let methods = "\(message.name):"
        let selector = NSSelectorFromString(methods)
        if self.responds(to: selector) {
            self.perform(selector, with: message.body)
        }
    }
}

extension WebViewController {

    @objc fileprivate func push(_ path: String) { //update
        Router.shareRouter.push(path)
    }

    @objc fileprivate func params(_ params: [String : Any]) { //update
        Router.shareRouter.params = params
    }
}

The method of adding jump and parameter in JS and matching SEL native method by runtime method. Next, we add interactive code in the front end:

├── javascripts
│ ├── http.js
│ ├── native.js
│ └── regex.js

native.js

export function NativePush(path) {
  window.webkit.messageHandlers.push.postMessage(path);
}

export function NativeParams(params) {
  window.webkit.messageHandlers.params.postMessage(params);
}

And add interaction logic in J1.vue

<template lang="html">
  <div>
    <navigation :documentTitle="title"></navigation>
    <div class="cell" v-for="model in models" @click="push('J1')"> //update
        ![](model.imageUrl)
        <span>{{model.text}}</span>
        <span>{{model.detailText}}</span>
    </div>
  </div>
</template>

<script>

import navigation from './navigation'
import { GET,URL } from '../javascripts/http'
import { URLQuery } from '../javascripts/regex'
import { NativePush, NativeParams } from '../javascripts/native' //update

export default {
  data () {
    return {
      title: '',
      models: []
    }
  },
  components: {
    navigation
  },
  methods: {
    request: function() {
      GET(URL.getJ1List).then((data) => {
          this.models = data.models;
      }).catch((error) => {

      })
    },
    push : function(path) { //update
      if (URLQuery('client') === 'app') {
          let params = {
            text : "web End-to-end incoming data",
            code : 1002
          }
          NativeParams(params);
          NativePush(path);
      } else {
          this.$router.push({
                path: '/' + path
          });
      }
    }
  },
  mounted:function () {
    this.title = document.title = "J1";
    this.request();
    // alert(window.location.href);
  }
}
</script>

In this way, our architecture design is basically completed.

Eggs:
Connect the database to our back end, change the value of the database through the interface, and you can control the page without restarting the service.
The database we use here mongodb Of course, you can also use the popular lover mysql, casually.

After the installation is completed, we will execute it.

  1. Mongod-config/usr/local/etc/mongod.conf Opens the service.
  2. use J1// Create a database
  3. switched to db J1// Switched to database
  4. db.createCollection('routers') // Create routers collection
  5. Db. routers. insert ({'J1':'app'}) / / / insert data into a collection

So the preparation of our database is finished. For Node, we import mongoose into package.json and add db path to the directory structure at the back end:

db
├── J1
│ └── routers.js
├── base.js
└── config.js

config.js

const mongoose = require('mongoose');

var db = mongoose.connect('mongodb://localhost/J1');

db.connection.on("error", function(error) {
    console.log("database connect fail: " + error);
});

db.connection.on("open", function() {
    console.log("database connect success");
})

db.connection.on('disconnected', function() {
    console.log('database disconnected');
})

exports.mongoose = mongoose;

base.js

let mongodb = require('./config');
let mongoose = mongodb.mongoose;
let Schema = mongoose.Schema;
let ObjectId = Schema.Types.ObjectId;

exports.mongodb = mongodb;
exports.mongoose = mongoose;
exports.Schema = Schema;
exports.ObjectId = ObjectId;
exports.Mixed = Schema.Types.Mixed;

routers.js

let base = require('../base');
let ObjectId = base.ObjectId;
let Scheme = new base.Schema({
    J1: String
});

module.exports = base.mongoose.model('routers', Scheme);

Then the database is invoked in the interface:

J1.js

exports.getRouters = async(ctx, next) => {

    var routers = await Routers.find({});
    ctx.body = {
        routers: routers[0]
    }
}

exports.updateRouters = async(ctx, next) => {

    const controller = ctx.params.controller;
    const client = ctx.params.client;

    if (controller === 'J1') {
        Routers.update({
            J1: client
        }, (err, doc) => {
            console.log(doc);
        })
    } else {
        console.log('controller is not exist');
    }
}

Add routing:

J1_router.js

var router = require('koa-router')();
var J1 = require('../../app/controllers/J1');

router.get('/getRouters', J1.getRouters);
router.get('/updateRouters/:controller/:client', J1.updateRouters); //update
router.get('/getJ1List', J1.getJ1List);

module.exports = router;

So we call the updateRouters interface when all services are open.

Browser input:
http://localhost:3001/api/J1/updateRouters/J1/web
http://localhost:3001/api/J1/updateRouters/J1/app

We can directly change the database dynamic downgrade or upgrade pages.

Finally, to sum up, we implemented a degradable mobile architecture through Swift3 + Vue2 + Koa2:

  1. Cd/RouterPattern/server/RouterPattern$NPM start opens the interface server
  2. Cd/RouterPattern/server/RouterPattern/public/javascripts$node image.js Open Picture Server
  3. Cd/RouterPattern/web/RouterPattern$NPM run dev opens the front-end page
  4. Cd/RouterPattern/app/RouterPattern $open RouterPatterm.xcworkspace opens Xcode

These are the design ideas that I want to share with you. I hope you can put forward valuable suggestions.

Demonstration effect:


RouterPattern.gif
github Download Address!!!


Click on the link below to jump!!
Specific source code, please download it on github! Like the friend sent the little star yo!!

Posted by macmonkey on Tue, 09 Jul 2019 11:35:11 -0700