Small program Webview status quo

H5 page interaction (jump) scenario in applets

  • H5 jumps to the native page of the applet (for example, call the address selection ability of the applet and return the corresponding address information to the H5 page)
  • H5 jumps to h5 page of its own business line (internal page interaction in various ways)
  • H5 jumps to the H5 page of other business lines (such as transaction process, related pages may be provided by other business lines)

The main pain points

  • After completing the relevant operations,The page status needs to be updatedCurrently, there are two common update modes:
    • The first: pass parameters through the URL (for example, add __isonShowRefresh =1 to the URL to tell the WebView to refresh when onshow again), concatenate the parameters to be passed into the URL, and open the URL again.
    • Second: need to jump to a new page for data update (e.g. : Order page – Address selection page – new order page)
  • In the first scenario, there is no problem in function, but the page will be refreshed. If the page operation is complex, it will need to be refreshed several times
  • The second option, which is better for forward operations than option 1, leads to another problem: the jump level is too deep, and the return is especially frustrating.

Small program, H5 page to open a new page

Let’s take a look at the common way h5 jumps h5 in applets:

  • Method 1: jump directly with location.href. When returning, different models show different performance. Some will swipe the page and re-execute JS, while others will directly display the previous cache
  • Method 2: Redirect the route hash to return the triggered hashchange without refreshing the page and re-rendering at the JS level
  • Method 3: Jump to the page To open a new WebView, which means that each page is an independent Webview

We adopted method 3 for the following reasons:

  1. When opening a new page, the effect is more similar to the jump between native pages (of course, the new page will also reload static resources, and this also has another problem, once you open 10 layers, then open a new WebView will not respond, this is the small application 10 layer limit).
  2. The returned experience is also more native, while ensuring the unity of the page state (no direct display or re-execution of JS).
  3. SRC link is the current page link, because if the page itself through the route and location. Href, the page link changes, the Webview does not know, this solution, the webview through this. SRC link is the current page link.

Because this scheme may reach the 10 layer limit of small programs. Therefore, it is recommended to add “back to the home page” operation in some important pages to shorten the history stack of small programs through this operation

Return to home page program brief

(You can skip this part if you’re not interested.)

wx.miniProgram.reLaunch({
  url: '/pages/webview/bridge? Url = project home page address '
})
Copy the code

Let’s declare that the path to our webView is /pages/ webView/webView

/pages/ webView /bridge

  • This page is not the webView page that finally opens the H5 page, but rather a transit page.
  • Mainly used for return processing
  • Page logic:
    • If it is the first display, jump to /pages/ webView/webView, at the same time pass the URL, normal open H5
    • If it is not shown for the first time, it is returned from the WebView and redirected to the home page of the applet

This transfer page: mainly to ensure that after reLaunch to an H5 page, users can still click back to the home page of the applet.

This scenario is typically used for h5 pages with multiple lines of business embedded in applets.

A content publishing scenario

We enter the release page from the home page and jump to the product details page after the release

So for a new user, the whole operation process looks like this:

  1. Home Page (Click Publish)
  2. Enter the publishing page (select the classification of the products to be published)
  3. Enter the product classification page (after selection)
  4. Type the category ID into the URL to enter the new publishing page (picker address)
  5. Enter the address list page (if the new user does not have an address, click Add Address)
  6. Enter the New Address page (after adding)
  7. Spell the address ID as URL to enter a new publish page (click Publish after editing the information)
  8. Enter the release success page (click to view product details)
  9. Enter the product details page

This scenario is the same page, different content items need to jump to different pages to operate, and then back to the original page to update the status of the problem.

If the product details page does not have a “back to home” entry, then the user wants to return to the home page… Need to press “back” = =!

After this experience, I don’t think the average user would have the courage to post again.

There is, of course, another kind of compromise

Add a flag bit to the connection, such as __isonShowRefresh =1 in the URL. When the webView opens the connection, it will read this parameter. If there is one, it will reload the URL every time in onShow and update the page status by refreshing the page.

The experience is also uncomfortable, with multiple refreshes on complex pages.

The statement

The solution I’m going to talk about isn’t just in the idea stage, it’s already online

Want to see the effect of friends, you can search in wechat small program:

“Turn around second-hand trading network” – “0 yuan free” -(bottom) “send idle to earn stars” – into the release page

  • Classification (skip h5, return after selecting the content, pass the parameter to the previous H5)

  • Select native address, return after selected, and pass the parameter to the previous H5.

OK, let’s move on to today’s topic

Small program h5 page onShow and cross page communication implementation

The first thing that comes to mind is the implementation of onShow method. Someone proposed to use visibilitychange to implement onShow method.

However, after research, this method works as expected on ios, but does not work as expected on Android phones. So I rejected it.

So here’s the plan

The principle is introduced

This program needs h5 and small program webView to do processing.

Core idea: Use the Hash feature of WebView

  • Applets hash arguments and the page does not update (this is the same as the browser)
  • H5 can use hashchange to capture the latest parameters for custom logic processing
  • Finally, window.history.go(-1)

Why window.history.go(-1)

This step is the essence of the whole scheme:

  • Since the hash change causes the webView history stack length to be +1, the user needs to return one more time. But this step is obviously redundant.
  • If window.history.go(-1) is used, the webView hash will be removed and the url will be the same.

Solution extension (cross-page data transfer)

Another common scenario in applets is to call a third business (or own business) that needs to bring the selected data back to the previous page after doing something.

As mentioned in the previous example: publish page, you need to select the publish type, and then return, publish page publish type local update

Of course, some of you might say, well, I can use setInterval to monitor localStorage. After the content is selected on the new page, set localStorage and then return to it.

I’m talking about generic solutions here. If the pages are maintained by your own line of business, of course, you can play around with them.

But when it comes to third-party business lines, especially business calls from different domain pages, this kind of communication becomes awkward.

So what do I do with my plan? I summarize a graph

Let’s interpret this graph:

  • Webview1 opens the publish page, h5 binds the Hashchange event (which is triggered when the WebView hashes the value)
  • Caches custom onShow methods. When hashchange fires, it looks for the specified argument and fires if it exists
  • The user clicks to jump to the type selection page
  • A new instance of the WebView2 page opens, opening the Type selection page
  • The user operation is completed, call wx. MiniProgram. PostMessage sends data webview, and return
  • Since the WebView is bound to the BindMessage event, it will receive the data sent by H5 when it returns
  • The received data is cached in a global store, webView2 is destroyed, and the applet returns
  • Returning from Webview2 to Webview1, webView1’s onShow hook fires
  • Webview1 reads the global store, fetches the parameters to be sent, concatenates the hash portion of the H5 link, and re-opens the link
  • Although the link is reopened, the page will not refresh because only the hash part has changed
  • However, the hashchange of the H5 page is triggered. At this time, the user-defined onShow method is called to read the hash parameter and update the page
  • After the onShow method is executed on h5 page, window.history.go(-1) is called to restore the history stack

That’s the whole process

Code diagram:

Small program

Small program WebView to do a few considerations:

  • For the consideration of smooth access, can not come up to do a one-size-fits-all, to ensure that the existing page does not do any modification under the circumstances of continued access.
  • New capabilities are distinguished by additional arguments, such as checking the query part of the URL with __isonShowPro =1 and passing hash arguments.
  • Change the original logic so that when __isonShowPro =1, the hash processing logic has the highest priority
  • Parameter definition, preceded by two underscores, in order to partition the normal parameters in the URL

Small program side webview.wpy

<web-view wx:if="{{url}}" src="{{url}}" binderror="onError" bindload="onLoaded" bindmessage="onPostMessage"></web-view> // link handling tool method import util from'@/lib/util'; // Global data store operation class import routeParams from'@/lib/routeParams'; const urlReg = /^(https? : \ \ / \ [^?#] +) (\? [^ #] *)? (# [? & ^ \] +)? (. +)? $/;
let messageData = {};

exportDefault class extends wepy.page {data = {pageShowCount: 0, // mQuery: {},... }onShow(){ ++this.pageShowCount; // Get the parameters that need to be passed to H5 after other pages are operated onletdata = routeParams.getBackFromData() || {}; // WebView page status updateif(this.pageShowCount > 1 && this.mQuery.__isonshowpro && this.mQuery.__isonshowpro === '1'| | data. Refresh) {/ / get the parameters of the need to pass to the h5 pageletrefreshParam = data.refreshParam; . // If the connection has parameters that need to handle the onShow logic (via the URL)hashInteract with H5 instead of scrolling)if (this.pageShowCount > 1 && this.mQuery.__isonshowpro === '1') {
        let [whole, mainUrl, queryStr, hashStr, hashQueryStr] = urlReg.exec(this.url); / / in the urlhashAdd new parameters tohashStr = (hashStr || The '#').substring(1);
        if(refreshParam) { delete refreshParam.refresh; } const messageData = this.getNavigateMessageData(); // Pass the parameters to be updated to the pagehash
        hashStr = util.addQuery(hashStr, object. assign({// onshow flag __isonshow: 1, // the hashchange flag is triggered by wa. // The hashchange flag is triggered by wa. // The hashchange flag is triggered by WA. __hashtimestamp: date.now ()}, messageData, refreshParam)); this.url = mainUrl + queryStr +The '#' + hashStr;
        console.log('【webview-hashchange-url】', this.url); // Add a delay, otherwise the hashchange will not be triggered when the WebView returns to the webViewsetTimeout(()=> {
          this.$apply(a); }, 50); // Refresh webView by modifying query parameters}else{... }... }} /** * Get the message data to send */getNavigateMessageData() {let rst = {};
    for(let i inMessageData){/* message struct: {key:'xx'// Message name content:'xx', // Message content trigger: {// trigger conditionstype: ' ', // Trigger type - immediately triggers the next onshow or open page, - URL triggers when the specified h5 link is found content:' '// Condition content -type=immediately is null -type}} */ const message = messageData[I]; const trigger = message.trigger || {}; // Send immediately, path triggeredif(trigger.type === 'immediately' || trigger.type === 'url'&& this.url.indexof (trigger.content) > -1){// Combine key and content into an object for conveniencehashRST [message.key] = message.content; Delete messageData[message.key]; delete messageData[message.key]; } } console.log('【webview-get-message】', rst);
    console.log('【webview-message-cache】', messageData);
    returnrst; } /** * Store message data */ storeNavigateMessageData(message){if(message && message.key){
      console.log('【webview-store-message】'MessageData [message.key] = message; messageData[message.key] = message; console.log('【webview-message-cache】', messageData); }} methods = {onPostMessage(e){if(! e.detail.data)return; const detailData = e.detail.data; // Get message datalet messageData = getValueFromMixedArray(detailData, 'messageData'.true);
      if(messageData) {/ / storage enclosing storeNavigateMessageData (messageData); }... }}... }Copy the code

It looks like a lot of things, but here are the points:

  • Bind the BindMessage event
  • After receiving the message from the page, it needs to be saved according to certain rules (I saved it according to key).
  • When the webView triggers the onShow hook, it retrieves the message data that needs to be sent according to the condition passed before it
  • Reloads the URL by concatenating the data to the hash part of the URL and adding the unique flag bits

The h5 end

The h5 side should also consider the following points when making changes:

  • It is best to encapsulate the interaction logic

  • Make it easier for the business side to call

  • I’ve defined two new methods here

    • onShow(callback)
      • Description: This is the same as the onShow hook, but is called to H5
      • Parameter: callback callback method

    Example: Publish page, need to select the category, return to need to update the category information

    import { isZZWA, onShow } from '@/lib/sdk'
    import URL from '@/lib/url'.created () {
    if(isZZWA()) {onShow(() => {// address const addressInfo = url.gethashParam ()'zzwaAddress')
          console.log('addressInfo:', decodeURIComponent(addressInfo)) ... Const selecteCateInfo = url.gethashParam (const selecteCateInfo = url.gethashParam ('selecteCateInfo')
          console.log('selecteCateInfo:', selecteCateInfo)
          ...
      } else{... }}...Copy the code
    • serviceDone(data, condition)
      • Description: The service ends and data needs to be transferred to the specified page
      • Parameters:
        • Data Object Data to be passed {key: ‘xx’, content: ‘xx’}
        • Condition String | trigger condition Number
          • String specifies the path to the URL. This message is sent when the webView opens the specified URL to trigger onShow
          • Number returns the specified test, like history.go(-1), for example: -1, -2

    Example: Type selection page

    import { isZZWA, serviceDone } from '@/lib/sdk'// Select the type and clicktypeChooseClick (param, type) {...if(isZZWA()) {// Need to return data const data = {key:'selecteCateInfo', content: JSON.stringify({... ServiceDone (data, -1)}else{... }}Copy the code

Ok, let’s see how the SDK of H5 is implemented

import util from './util';

class WASDK {
  /**
   * Create a instance.
   * @ignore
   */
  constructor(){// hashchang event handlingif('onhashchange' inwindow && window.addEventListener && ! Wasdk.hashinfo.isinit){// Update flag wasdk.hashinfo.isinit =true; // Bind hashchange window.addeventListener ('hashchange', ()=>{// if the small program webView modificationhashBefore processingif (util.getHash(window.location.href, '__wachangehash') = = ='1') {// there is a pit in this: // ios applet webView is modifying the URLhashAfter that, the page hashchange and update will work normally, but h5 will fail to invoke some of the applet capabilities (e.g., when ios is set up)hashWx.uploadimg will fail to call wx.uploadimg, wx.config will need to be reset. Wx. config: wx.config: appId: wx.config: appId: wx.config: appId: wx.config: appId: wx.config: appId: wx.config: appId: wx. Here take) directly from the cache const jsticket = window. The native && window. The native. The adapter && window. The native. Adapter. Jsticket | | null; const ua = navigator.userAgent; // For non-Android systems, reset wx.configif(jsticket && ! (ua.indexOf('Android') > -1 || ua.indexOf('Adr') > -1)) {
            window.wx.config({
              debug: false,
              appId: jsticket.appId,
              timestamp: jsticket.timestamp,
              nonceStr: jsticket.noncestr,
              signature: jsticket.signature,
              jsApiList: ['onMenuShareTimeline'.'onMenuShareAppMessage'.'onMenuShareQQ'.'onMenuShareQZone'.'onMenuShareWeibo'.'scanQRCode'.'chooseImage'.'uploadImage'.'previewImage'.'getLocation'.'openLocation']})} / / trigger cache array callback WASDK hashInfo. CallbackArr. ForEach (callback = > {callback (); }) // Perform the return operation (this step is important!! // Because the webView is set uphashThe webView history stack +1 // does not need this extra history, so you need to perform a return operation to remove it // even if the return operation, justhashLevel changes, so no page refresh is triggeredsetTimeout indicates that the next event loop will return. If there are subsequent dom operations can be completed in the current event loopsetTimeout(()=>{ window.history.go(-1); }, 0); }},false}} /** *hashRelated information */ statichashInfo = {// Whether isInit has been initialized:false, / /hashCallbackArr: []} @param {Function} callback - Mandatory, callback method, callback parameterhashThe argument after part of the question mark parses the object */ @execLog
  onShow(callback){
    if (typeof callback === 'function') {/ / to onshow logic packing callback methods, and pushed into the cache array WASDK. HashInfo. CallbackArr. Push (function(){// Check if the specified parameter has changedif(util.getHash(window.location.href, '__isonshow') = = ='1'){// Trigger the onShow callback(); }})}else{util.console.error(' argument error, call onShow please pass correct callback '); @param {Object} obj - Mandatory, message Object * @param {String} obj. Key - Mandatory, Message name * @ param {String} obj. The content - option, the message content, by default, an empty String if it is a content object, please convert String * @ param {String | Number} condition - options, By default, only postMessage * String - can be passed to the specified URL path, which is triggered when the applet webView opens the specified URL or onshow. * Can also be passed to the applet path, which is reserved for later * Number - returns to the specified test, Go (-1), for example: -1, -2 */ @execLog
  serviceDone(obj, condition){
    if(obj && obj. Key) {/ / the message body const message = {/ / message name key: obj. Key, / / the body of the message content: obj. Content | |' ', // trigger condition trigger: {// type'immediately'Immediately triggered in the next onShow,'url', when the specified H5 link is found,'path'Triggered when the specified applet path is openedtype: 'immediately'// Conditional content, immediately is empty, URL is h5 link address, path is the applet path content:' '}}; / / parse the trigger condition condition = condition | | 0; // If it is a pathif(typeof condition === 'string' && (condition.indexOf('http') > -1 || condition.indexOf('pages/'Message.trigger = {// set the message trigger condition message.trigger = {type: condition.indexOf('http') > 1?'url' : 'path'Content: condition}} / / messaging wx miniProgram. PostMessage ({data: {messageData: message}}); // If not the URL or path, then conditon needs to be returnedif(message.trigger.type === 'immediately'){// See if you need to return the specified hierarchy, compatible with incoming'1'Condition = parseInt(condition, 10); }catch(e){} // Ensure that the series returned is correctif(condition && typeof condition === 'number'&&! isNaN(condition)){ this.handler.navigateBack({delta: Math.abs(condition)}); }}}else{util.console.error(' argument error, call serviceDone method, passed object does not contain key '); }}... } window.native = new Native();export default native;

Copy the code

This seems to be quite a lot, summed up in two points:

  • Implementation of the onShow method
    • Bind a Hashchange event (this is done to prevent double binding events)
    • The incoming onShow custom event is cached in an array. When hashchange is triggered, it is determined by the unique flag bits __isonShow and __wachangehash
  • The implementation of serviceDone method
    • Process incoming data
    • Trigger conditions for handling this data: Immediately indicates the most recent onShow trigger, or you specify the URL yourself
    • Through the wx. MiniProgram. PostMessage sending data

Ok, that’s it for the whole scheme

conclusion

The original scheme wasn’t exactly like this, but the principle was the same. In the process of my implementation, I found many problems in the original scheme

So I did a lot of transformation and detail optimization, and then formed the final scheme above.

This solution is intrusive and requires each business party to modify its own code. Although there are some transformation costs, the user experience benefits are very clear.

Ps: Our QA testers said, “This is so much better.”

Note:

Several points need to be noted in adopting this scheme:

  • If you communicate in this way, you need to add __isonShowPro =1 to the query part of the current page URL, otherwise you won’t communicate over hash
  • Make sure the onShow method is actually called, otherwise the page will not refresh
  • If third-party services need to transmit values, use the serviceDone method for communication

Ok, that’s all for today. Let’s learn together