The author has been developing mobile terminal applications with Web technology in the company for more than a year. At the beginning, it was mainly based on VUE technology stack and Native, but now it has evolved into VUE + React Native technology architecture. Vue is mainly responsible for developing OA business. React Native focuses on instant messaging and builds on MatterMost, an open source instant messaging solution.

Because companies don’t have much precipitation technology in this aspect, so met a lot of pit during development, after more than a year of accumulation of technology to conquer and eventually formed the more perfect solutions, summed up hope can help to everyone, especially for some small and medium-sized companies that inexperienced (PS: big companies are estimated to have their own a plan).

The GitHub address of this library will continue to be improved. Welcome star:

mobile-web-best-practice

Mobile Web best practices, vuE-CLI3-based typescript projects for hybrid applications and pure WebApp development. Much of the following applies to front-end frameworks like React as well.

There are three areas that are still being worked on: Domain-driven Design (DDD) applications, microfronts, and performance monitoring, which will be published as separate articles when completed. Performance monitoring is not a good option, but there are free, open source and powerful tools like Sentry for error monitoring, so let me know if anyone knows. Some mistakes or better solutions are unavoidable in this paper, and your comments are welcome.

directory

  • Component library
  • JSBridge
  • Route Stack Management (emulating native APP navigation)
  • Request data caching
  • Pre-render at build time
  • Webpack strategy
    • Base libraries are pulled out
  • Gesture library
  • Style adapter
  • Form validation
  • Prevents native return events
  • Obtain device information through the UA
  • The mock data
  • Debug console
  • Caught tools
  • Anomaly Monitoring platform
  • Q&A

Component library

vant

vux

mint-ui

cube-ui

Vue mobile component library is currently mainly listed above several libraries, this project uses the open source Vant of the front-end team.

Vant officially supports custom style themes. The basic principle is to use modifyVars provided by less to modify less variables during the compilation of less files to CSS files. This project also adopts this method. For details, please refer to related documents:

Custom theme

Recommend an article that introduces the kutpoints of each component:

Comparative Analysis of Vue Common Component Library (Mobile terminal)

JSBridge

DSBridge-IOS

DSBridge-Android

WebViewJavascriptBridge

Hybrid applications generally load web pages through WebView, and when web pages need to acquire device capabilities (such as calling camera, local calendar, etc.) or native needs to call methods in web pages, they need to communicate through JSBridge.

There are many powerful JSBridges in the open source community, such as the libraries listed above. DSBridge is adopted in this project for the reason of keeping the interface of iOS and Android platform unified. You can choose the tool suitable for your own project.

This project takes h5 calling the synchronous calendar interface provided by Native as an example to demonstrate how to communicate on both ends based on DSBridge. Here are the key code summaries for both ends:

Android synchronous calendar core code, please refer to the Android project mobile-web-best-practice-Container matching this project for the specific code:

public class JsApi {
    /** * sync calendar interface * MSG format is as follows: *... * /
    @JavascriptInterface
    public void syncCalendar(Object msg, CompletionHandler<Integer> handler) {
        try {
            JSONObject obj = new JSONObject(msg.toString());
            String id = obj.getString("id");
            String title = obj.getString("title");
            String location = obj.getString("location");
            long startTime = obj.getLong("startTime");
            long endTime = obj.getLong("endTime");
            JSONArray earlyRemindTime = obj.getJSONArray("alarm");
            String res = CalendarReminderUtils.addCalendarEvent(id, title, location, startTime, endTime, earlyRemindTime);
            handler.complete(Integer.valueOf(res));
        } catch (Exception e) {
            e.printStackTrace();
            handler.complete(6005); }}}Copy the code

H5-side synchronous calendar core code (decorator to limit the platform to call interface)

Class NativeMethods {@p() public syncCalendar(params: SyncCalendarParams) {const cb = (errCode: number) => { const msg = NATIVE_ERROR_CODE_MAP[errCode]; Vue.prototype.$toast(msg); if (errCode ! == 6000) { this.errorReport(msg, 'syncCalendar', params); }}; dsbridge.call('syncCalendar', params, cb); } private errorReport(errorMsg: string, methodName: string, params: any) { if (window.$sentry) { const errorInfo: NativeApiErrorInfo = { error: new Error(errorMsg), type: 'callNative', methodName, params: JSON.stringify(params) }; window.$sentry.log(errorInfo); }}} / * * * @ param {platforms} - restricted platform * @ return {Function} - a decorator * / Function p (platforms = [' android ', 'ios']) { return (target: AnyObject, name: string, descriptor: PropertyDescriptor) => { if (! Ffices.includes (window.$platform)) {descriptor. Value = () => {return Vue. Prototype ${window.$platform}; }; } return descriptor; }; }Copy the code

In addition, I recommend a teaching version of JSBridge based on Android platform written by the author, which elaborates how to encapsulate a usable JSBridge step by step based on the underlying interface:

Implementation principle of JSBridge

Route Stack Management (emulating native APP navigation)

vue-page-stack

vue-navigation

vue-stack-router

When developing APP with H5, we often encounter the following requirements: enter the detail page from the list, and remember the current position after returning, or click an item from the form to enter another page for selection, and then return to the form page, and need to remember the data filled in the previous form. However, both vUE and React frameworks do not support two page instances at the same time. Therefore, the routing stack is required for management.

Vue-page-stack and Vue-Navigation are both inspired by keepalive of Vue and based on vue-Router. When entering a page, it will check whether the current page has cache. If there is cache, it will remove the cache. And remove all vnodes behind him, no cache is a new page, need to store or replace the current page, push the corresponding Vnode to the stack, so as to realize the function of remembering the page state.

The vue-stack-Router of the logical thinking front end team takes A different approach. It removes vue-Router and implements route management independently. Compared with vue-Router, it mainly supports instances of both A and B pages, or two instances of different states of A page. It also supports the native left swipe function. However, since the project is still in the early stage of improvement and its function is not as powerful as vue-Router, it is recommended to continue to pay attention to the follow-up dynamic and then decide whether to introduce it.

Vue-page-stack is used in this project. You can choose the tool suitable for your project. At the same time, several relevant articles are recommended:

Vue single-page Application navigation manager is released

Vue community routing solution: VUe-stack-Router

Request data caching

mem

In our application, there will be some data that is rarely changed, and these data need to be retrieved from the back end, such as company personnel, company job classification, etc., such data will not change for a long time, but every time the page is opened or changed, the back end will be requested again. To reduce unnecessary requests and speed up page rendering, you can reference the MEM cache library.

The basic principle of MEM is to create a WeakMap with the received function as key, and then create a Map with the function parameter as key. Value is the execution result of the function, and at the same time, the Map is used as the value of the WeakMap just to form nested relations. In this way, different parameters of the same function can be cached. MaxAge is the validity period of data. When a certain data reaches its validity period, it will be automatically destroyed to avoid memory leakage.

WeakMap is chosen because its relative Map keeps the object referenced by the key name a weak reference, that is, the garbage collection mechanism does not take that reference into account. As soon as all other references to the referenced object are cleared, the garbage collection mechanism frees the memory occupied by the object. In other words, once it is no longer needed, the key name object and the corresponding key value pair in WeakMap will disappear automatically, without manually deleting the reference.

As a higher-order function, MEM can directly accept encapsulated interface requests. But for simplicity, we can integrate our interface functions as classes, and then use MEM as decorators (decorators can only modify methods of classes and classes of classes, because ordinary functions have variable promotions). Here’s the code:

import http from '.. /http';
import mem from 'mem';

/** * @param {MemOption} -mem configuration item * @return {Function} -decorator */
export default function m(options: AnyObject) {
  return (target: AnyObject, name: string, descriptor: PropertyDescriptor) = > {
    const oldValue = descriptor.value;
    descriptor.value = mem(oldValue, options);
    return descriptor;
  };
}

class Home {
  @m({ maxAge: 60 * 1000 })
  public async getUnderlingDailyList(
    query: ListQuery
  ): Promise<{ total: number; list: DailyItem[] }> {
    const {
      data: { total, list }
    } = await http({
      method: 'post',
      url: '/daily/getList',
      data: query
    });

    return{ total, list }; }}export default new Home();
Copy the code

Pre-render at build time

Currently, it takes a long time to render the first screen of a single page (you need to download and parse THE JS file, then render the elements and mount them to the DIV with the ID of APP). SEO is not friendly (the actual elements on the body of index.html only have div elements with the ID of app. Real page elements are dynamically mounted, which cannot be captured by search engine crawlers). At present, the mainstream solution is server side rendering (SSR), which is to generate complete and assembled static HTML from the server side and send it to the browser for display. However, the configuration is relatively complex, and framework is generally used, such as NUxt.js of Vue. The react of the next.

There’s an easier way to do this — pre-render at build time. After the project is packaged and built, a Web Server is started to run the entire site, and multiple headless browsers (such as Puppeteer, Phantomjs and other headless browser technologies) are opened to request all routes in the project. When the request page rendering to the first page need pre-rendered (advanced configuration required to render a page of routing), will take the initiative to throw an event, the event by the headless browser intercept, then the content of the page to generate an HTML (including the DOM structure generated JS and CSS styles), save the package folder.

Based on the above description, we can see that it is essentially a snapshot page, not suitable for dynamic pages that rely too much on the back-end interface, but more suitable for static pages that do not change frequently.

In terms of actual project-related tools, the prerender-SPa-plugin is a webpack plug-in, which is illustrated below. But there are two caveats:

The first is that the plugin needs to rely on Puppeteer, which often fails to be downloaded due to domestic network problems and its large size. However, the.npmrc file can be used to specify the download path of the Puppeteer as the domestic image.

The other option isto set the route mode to History (based on the HISTORY API provided by HTML5, react is called BrowserRouter, vue is called history), because the hash route does not correspond to the actual physical route. If the /form/ route is pre-rendered, the server will return the index.html file in the form folder directly, which was pre-generated when the whole HTML file was packaged.

The prerender-SPa-plugin has been integrated in this project, but there are some problems using it with vue-stack-Page/vue-Navigation routing stack managers (we are still looking for the reason, if you know, please let us know). So prerender is turned off.

At the same time, several relevant articles are recommended:

Prerender-spa-plugin for vUE prerender-SPa-plugin

Use pre-shading to enhance the SPA application experience

Webpack strategy

Base libraries are pulled out

Some base libraries, such as Vue, Moment, etc., are static dependencies that don’t change very often and generally need to be pulled out to improve the efficiency of each build. At present, there are two mainstream schemes:

One is to use the Webpack-DLL-plugin, which packages these static dependencies separately during the first build and then simply introduces the already packaged static dependencies.

The other is to extend Externals, that is, to remove static resources that do not need to be packaged from the build and introduce them in CDN mode. The following are the disadvantages of webpack-DLL-plugin versus Externals:

  1. You need to configure static dependencies that are not compiled at each build and precompile a JS file for them at the first build (called lib files later). Dependencies need to be maintained manually each time they are updated, and errors or version errors occur when dependencies are added or deleted or resource versions are changed and forgotten to be updated.

  2. New browser features script type=”module” cannot be accessed, and the introduction of native ES Modules provided by some dependent libraries (such as the introduction of new versions of Vue) cannot be supported to better adapt to the good features provided by older browsers for better performance optimization.

  3. Precompiling all resources into a single file and explicitly injecting that file into the HTML template that the project builds was favored in the HTTP1 era because it reduced the number of requests for resources, but in the HTTP2 era if you split multiple CDN links, You can take full advantage of the multiplexing features of HTTP2.

However, the choice of Externals still requires a reliable CDN service.

This project is Externals. You can choose different schemes according to project requirements.

Check out this article for more (the views above are from this article) :

Webpack Optimization – Double your build efficiency

Gesture library

hammer.js

AlloyFinger

Gestures such as drag (Pan), Pinch (Pinch), Rotate (Rotate), swipe (swipe) and so on are generally supported in mobile development. There are already very mature solutions, such as Hammer. js and AlloyFinger developed by Tencent’s front-end team, which are very good. This project chooses to carry out secondary encapsulation into VUE instruction set based on hammer.js. You can choose different schemes according to project requirements.

The following is the key code of secondary encapsulation, which uses webpack require.context function to obtain the context of a specific module, mainly used to achieve automatic import modules, more suitable for the scenario like vue instruction module more:

// The context used to import the module
export const importAll = (
  context: __WebpackModuleApi.RequireContext,
  options: ImportAllOptions = {}
): AnyObject= > {
  const { useDefault = true, keyTransformFunc, filterFunc } = options;

  let keys = context.keys();

  if (isFunction(filterFunc)) {
    keys = keys.filter(filterFunc);
  }

  return keys.reduce((acc: AnyObject, curr: string) = > {
    const key = isFunction(keyTransformFunc) ? keyTransformFunc(curr) : curr;
    acc[key] = useDefault ? context(curr).default : context(curr);
    return acc;
  }, {});
};

// Index. ts in the cache folder
const directvieContext = require.context('/'.false./\.ts$/);
const directives = importAll(directvieContext, {
  filterFunc: (key: string) = >key ! = ='./index.ts',
  keyTransformFunc: (key: string) = >
    key.replace(/ ^ \ \ / /.' ').replace(/\.ts$/.' ')});export default {
  install(vue: typeof Vue): void {
    Object.keys(directives).forEach((key) = >vue.directive(key, directives[key]) ); }};// touch.ts
export default {
  bind(el: HTMLElement, binding: DirectiveBinding) {
    const hammer: HammerManager = new Hammer(el);
    const touch = binding.arg as Touch;
    const listener = binding.value as HammerListener;
    const modifiers = Object.keys(binding.modifiers);

    switch (touch) {
      case Touch.Pan:
        const panEvent = detectPanEvent(modifiers);
        hammer.on(`pan${panEvent}`, listener);
        break; . }}};Copy the code

Also recommended is an article on hammer.js and an article on require.context:

H5 Case Sharing: JS gesture framework — Hammer.js

Use require.context for front-end engineering automation

Style adapter

postcss-px-to-viewport

Viewport Units Buggyfill

flexible

postcss-pxtorem

Autoprefixer

browserslist

In mobile web development, style adaptation is always an unavoidable problem. For this, the current mainstream schemes include VW and REM (and of course vw + REM combination schemes, please see the reM-VW-Layout warehouse below). In fact, the basic principle is the same, that is, the screen width or font size is proportional to change. I won’t go into detail here because there are plenty of details on the principles available on the web. Here are some of the tools for this project.

As for REM, Alibaba Wireless front end team launched flexible solution based on REM in 2015, as well as postCSS-PxtoREM plug-in provided by PostCSS to automatically convert PX to REM.

For VW, you can automatically convert PX to VW using postcss-px-to-viewport. Postcss-px-to-viewport The configuration is as follows:

"postcss-px-to-viewport": {
  viewportWidth: 375.// The width of the window corresponds to the width of our design, which is usually 375
  viewportHeight: 667.// The window height can be specified according to the width of 750 devices
  unitPrecision: 3.// Specify the decimal number to convert 'px' to the window unit value (often not divisible)
  viewportUnit: 'vw'.// Specify the window unit to convert to. Vw is recommended
  selectorBlackList: ['.ignore'.'.hairlines'].// Specify a class that is not converted to Windows units. It can be customized and added indefinitely. It is recommended to define one or two common class names
  minPixelValue: 1.// less than or equal to '1px' does not convert to window units, you can also set to whatever value you want
  mediaQuery: false // Whether the unit in the media query needs to be converted
}
Copy the code

Here is a comparison of the advantages and disadvantages of VW and REM:

As for VW compatibility, it is currently supported on mobile terminal iOS 8 and Android 4.4. If you need to be compatible with a lower version, you can choose the Pollify scheme of viewport, among which the more mainstream is ViewPort Units Buggyfill.

This scheme is not compatible with the low version, so VW scheme is directly selected. You can choose different schemes according to the project requirements.

As for CSS compatibility with different browsers, we all know Autoprefixer (vue- CLI3 is integrated by default), so how to set the compatibility range? Browserslist is recommended. You can set compatible browser scopes in the browserslist section of.browserslistrc or pacakage.json. Since not only Autoprefixer but also Babel, postCSS-Preset -env and other tools read browserslist compatibility configurations, it’s easier to keep the range of JS CSS-compatible browsers consistent. Here is the.browserslistrc configuration for this project:

iOS >= 10  / / the iOS Safari
Android >= 6.0 / / the Android WebView
last 2 versions // The last two versions of each browser
Copy the code

Finally, recommend some information about mobile terminal style adaptation:

rem-vw-layout

Detail the classic REM and Rookie VW layouts on mobile

How to use VW for mobile adaptation in a Vue project

Form validation

async-validator

vee-validate

Since most mobile component libraries do not provide form validation, you need to wrap it yourself. The most common approach is secondary encapsulation based on Async-Validator (elementUI component library provides form validation based on Async-Validator). Or use Vee-validate (a lightweight validation framework based on VUE templates). You can choose different schemes according to your project requirements.

The form verification scheme of this project is a secondary encapsulation based on Async-Validator. The code is as follows. The principle is very simple and basically meets the requirements. If there is a more perfect plan, welcome to put forward.

The setRules method converts the rules set in the component into an object Validator based on the name of the data to be verified. Value is an instance generated by async-Validator. Validator methods can accept single or multiple keys for data to be validated, and then look for the async-Validator instance corresponding to the key in the setRules generated object Validator, and finally call the instance validation method. You can also take no arguments, and all incoming data will be validated.

import schema from 'async-validator'; .class ValidatorUtils {
  private data: AnyObject;
  private validators: AnyObject;

  constructor({ rules = {}, data = {}, cover = true }) {
    this.validators = {};
    this.data = data;
    this.setRules(rules, cover);
  }

  @param rules Validator rules for async-validator @param Cover Whether to replace old rules */
  public setRules(rules: ValidateRules, cover: boolean) {
    if (cover) {
      this.validators = {};
    }

    Object.keys(rules).forEach((key) = > {
      this.validators[key] = new schema({ [key]: rules[key] });
    });
  }

  publicvalidate( dataKey? :string | string[]
  ): Promise<ValidateError[] | string | string[] | undefined> {
    // Error array
    const err: ValidateError[] = [];

    Object.keys(this.validators)
      .filter((key) = > {
        // If no dataKey is passed, verify all. Otherwise verify the data corresponding to the dataKey (dataKey can correspond to one (string) or more (array))
        return (
          !dataKey ||
          (dataKey &&
            ((_.isString(dataKey) && dataKey === key) ||
              (_.isArray(dataKey) && dataKey.includes(key))))
        );
      })
      .forEach((key) = > {
        this.validators[key].validate(
          { [key]: this.data[key] },
          (error: ValidateError[]) = > {
            if (error) {
              err.push(error[0]); }}); });if (err.length > 0) {
      return Promise.reject(err);
    } else {
      return Promise.resolve(dataKey); }}}Copy the code

Prevents native return events

You may encounter the requirement that when a page pops up a popup or dialog component, hitting the back key hides the popup component instead of returning to the previous page.

To solve this problem, we can think in terms of routing stacks. Generally, the pop-up component will not add any records on the routing stack, so when we pop up the component, we can push a record in the routing stack. In order to prevent the page from jumping, we can set the target route of the jump as the current page route, and add a query to mark the pop-up status of the component.

Then listen for the change of query. When the pop-up component is clicked, the tag related to the pop-up component in Query becomes true, and the pop-up component is set to display. When the user clicks the Native return key, the route returns the previous record, which is still the current page route, but the flag associated with the pop-up component in query is no longer true, so we can set the pop-up component to hide and not return to the previous page. The relevant codes are as follows:

<template> <van-cell title=" when to pit "IS-link :value=" textData.pitdatestr" @click="goToSelect('calendar')" /> <van-popup v-model="showCalendar" position="right" :style="{ height: '100%', width: '100%'}"> <Calendar title=" select pitdate "@select="onSelectPitDate" /> </van-popup> <template/> <script lang="ts">... export default class Form extends Vue { private showCalendar = false; private goToSelect(popupName: string) { this.$router.push({ name: 'form', query: { [popupName]: 'true' } }); } private onSelectPitDate(... res: DateObject[]) { ... this.$router.go(-1); } @Watch('$route.query') private handlePopup(val: any) { switch (true) { case val.calendar && val.calendar === 'true': this.showCalendar = true; break; default: this.showCalendar = false; break; } } } </script>Copy the code

Obtain device information through the UA

When developing an H5 development, you may encounter the following situations:

  1. During development, all the development and debugging are conducted in the browser, so it is necessary to avoid calling native interfaces, because these interfaces do not exist in the browser environment.
  2. In some cases, you need to distinguish between android webView and ios WebView and do some platform-specific processing;
  3. When the H5 version has been updated but the client version has not been updated synchronously, an invocation error occurs if the interface calls between the two change.

Therefore, a way is needed to detect the platform type, APP version and system version of the device on which the page is currently located. At present, the more reliable way is to modify the UserAgent through Android/ios WebView and add a specific suffix on the original basis. Then you can obtain device related information through UA on the web page. Of course, this approach assumes that native code can be modified for this purpose. Taking Android as an example, the key codes are as follows:

Android key code:

// Activity -> onCreate.// Get the app version
PackageManager packageManager = getPackageManager();
PackageInfo packInfo = null;
try {
  // getPackageName() is the package name of your current class, 0 means to get version information
  packInfo = packageManager.getPackageInfo(getPackageName(),0);
} catch (PackageManager.NameNotFoundException e) {
  e.printStackTrace();
}
String appVersion = packInfo.versionName;

// Obtain the system version
String systemVersion = android.os.Build.VERSION.RELEASE;

mWebSettings.setUserAgentString(
  mWebSettings.getUserAgentString() + " DSBRIDGE_"  + appVersion + "_" + systemVersion + "_android"
);
Copy the code

H5 Key codes:

const initDeviceInfo = (a)= > {
  const UA = navigator.userAgent;
  const info = UA.match(/\s{1}DSBRIDGE[\w\.]+$/g);
  if (info && info.length > 0) {
    const infoArray = info[0].split('_');
    window.$appVersion = infoArray[1];
    window.$systemVersion = infoArray[2];
    window.$platform = infoArray[3] as Platform;
  } else {
    window.$appVersion = undefined;
    window.$systemVersion = undefined;
    window.$platform = 'browser'; }};Copy the code

The mock data

Mock

If the current back-end progress is inconsistent and the interface is not yet implemented, the front-end can use mock data for independent development after the interface data format is agreed on by the front and back ends in order not to affect each other’s progress. This project uses Mock to implement the interface required for the front end.

Debug console

eruda

vconsole

In terms of debugging, this project uses ERUDA as the mobile debugging panel, which functions as opening the PC console and can easily view key debugging information such as console, network, cookie and localStorage. A similar tool is vconsole developed by wechat’s front-end research and development team. You can choose the tool suitable for your project.

It is recommended to use CDN to load ERUDA. As for when to load ERUDA, different strategies can be formulated according to different projects. Example code is as follows:

<script> (function() { const NO_ERUDA = window.location.protocol === 'https:'; if (NO_ERUDA) return; Const SRC = 'https://cdn.jsdelivr.net/npm/[email protected]/eruda.min.js'; document.write('<scr' + 'ipt src="' + src + '"></scr' + 'ipt>'); document.write('<scr' + 'ipt>eruda.init(); </scr' + 'ipt>'); }) (); </script>Copy the code

Caught tools

charles

fiddler

The ERUDA debugging tool is available, but in some cases it is not sufficient, such as when erUDA is completely shut down on the live network.

At this point, you need to capture package tools, related tools are mainly listed above the two, you can choose the tools suitable for your project.

Charles allows you to clearly view all requested information (note: packet capture using HTTPS requires a certificate on the mobile phone). Of course, Charles has more powerful functions, such as proportional simulation of weak network conditions, resource mapping, etc.

A good Charles tutorial is recommended:

Unlock Charles’ pose

Anomaly Monitoring platform

sentry

Compared with PC, mobile web pages are mainly characterized by numerous devices, different network conditions and difficulty in debugging. Lead to the following problems:

  • The test cannot fully cover bugs that occur only in some cases due to device compatibility or network exceptions

  • The device of the user with the bug cannot be retrieved, and the bug feedback cannot be reproduced

  • Some bugs only appear for a few times, which cannot be repeated later and cannot restore the accident site

At this time, it is very necessary to have an anomaly monitoring platform, which can upload anomalies to the platform in real time and inform relevant personnel in time.

Related tools include Sentry and Fundebug, among which Sentry is widely adopted because of its powerful functions, support for multi-platform monitoring (not only monitoring front-end projects), full open source, and privatized deployment.

The following are the related supporting tools used by Sentry in this project.

Sentry SDK for javascript

sentry-javascript

Automatically upload the Sourcemap webpack plug-in

sentry-webpack-plugin

The Babel plug-in that automatically adds error-reporting functions to try catch at compile time

babel-plugin-try-catch-error-report

Supplement:

Front-end exceptions mainly include the following parts:

  • Static resource loading is abnormal

  • Interface exceptions (including back-end and native interfaces)

  • Js error

  • Web collapse

If static resource loading fails, the window. AddEventListener (‘error’,… , true) in the event capture phase, then filter out the resource load failure error and manually report the error. The core code is as follows:

// Global monitoring resource loading error
window.addEventListener(
  'error'.(event) = > {
    // Filter js error
    const target = event.target || event.srcElement;
    const isElementTarget =
      target instanceof HTMLScriptElement ||
      target instanceof HTMLLinkElement ||
      target instanceof HTMLImageElement;
    if(! isElementTarget) {return false;
    }
    // Report the resource address
    const url =
      (target as HTMLScriptElement | HTMLImageElement).src ||
      (target as HTMLLinkElement).href;

    this.log({
      error: new Error(`ResourceLoadError: ${url}`),
      type: 'resource load'
    });
  },
  true
);
Copy the code

For server interface exceptions, you can globally integrate error reporting functions in the encapsulated HTTP module (error reporting of native interfaces is similar, which can be viewed in the project). The core code is as follows:

function errorReport(
  url: string,
  error: string | Error, requestOptions: AxiosRequestConfig, response? : AnyObject) {
  if (window.$sentry) {
    const errorInfo: RequestErrorInfo = {
      error: typeof error === 'string' ? new Error(error) : error,
      type: 'request',
      requestUrl: url,
      requestOptions: JSON.stringify(requestOptions)
    };

    if (response) {
      errorInfo.response = JSON.stringify(response);
    }

    window.$sentry.log(errorInfo); }}Copy the code

Onerror and window.addeventListener (‘unhandledrejection’,… False) perform global listening and report.

Window. onerror = (message, source, lineno, colno, error) =>{} is different from window.addEventListener(‘error’,… Window. onError captures more information, including the Error string information, the JS file where the Error occurred, the number of lines and columns where the Error occurred, and the Error object (which also has call stack information). So sentry selects window. onError for js global monitoring.

But there is one type of error that window.onerror does not listen for, and that is the unhandledrejection error, which is caused when there is no catch after a Promise reject. Of course, the Sentry SDK already does this.

React () componentDidCatch (); react () componentDidCatch (); react ();

// Global monitor Vue errorHandler
Vue.config.errorHandler = (error, vm, info) = > {
  window.$sentry.log({
    error,
    type: 'vue errorHandler',
    vm,
    info
  });
};
Copy the code

However, in our business, we often use try catch for some error code. These errors cannot be caught by window.onError if they are not thrown up in the catch. The author developed a Babel plug-in, babel-plugin-try-catch-error-report, which can search for catch nodes in the AST during Babel compiling JS. Then the error reporting function is automatically inserted into the catch code block. You can customize the function name and the reported content (source file, line number, column number, call stack, and the current window attribute, such as the current routing information window.location.href). The configuration codes are as follows:

if(! IS_DEV) { plugins.push(['try-catch-error-report',
    {
      expression: 'window.$sentry.log'.needFilename: true.needLineNo: true.needColumnNo: false.needContext: true.exclude: ['node_modules']}]); }Copy the code

For cross-domain JS problems, when loading JS files of different domains, such as loading packaged JS through CDN. If js fails, window.onerror can only catch script error, and there is no valid information to help us locate the problem. Access-control-allow-origin: * In the return header that returns js, the server needs to set the access-Control-allow-origin: *

<script src="http://helloworld/main.js" crossorigin></script>
Copy the code

If it is dynamically added, it can also be dynamically set:

const script = document.createElement('script');
script.crossOrigin = 'anonymous';
script.src = url;
document.body.appendChild(script);
Copy the code

A service work-based monitoring scheme is recommended for web page crashes, and related articles are listed below. If the web page is loaded by a WebView, you can also monitor the crash of the web page through the hook of the webView loading failure.

How to monitor web crashes?

Finally, since the code deployed online is usually compressed and obtruded, it is impossible to locate the source code without uploading sourcemap. You can add.sentryclirc to the project now, which can refer to the.sentryclirc. Then run the sentry-cli command to upload the file as follows:

Sentry -cli releases -o agency name -p project name files version upload-sourcemaps sourcemap file relative position --url-prefix JS online relative to the root directory --rewrite // Example Sentry -cli releases -o McUkingdom -p Hello-world files 0.2.1 upload-sourcemaps dist/js --url-prefix'~/js/' --rewrite
Copy the code

The webpack plugin sentry-webpack-plugin triggers the after-emit event hook of Webpack when packaging. The plug-in will automatically upload sourcemap and associated JS from the package directory. For configuration, refer to the vue.config.js file of this project.

The Sourcemap file is usually not allowed to be deployed online for security reasons, so you can manually delete the sourcemap file online after uploading it to sentry.

Q&A

  • IOS WKWebView cookies are slow to write and easy to lose

    Phenomenon:

    1. IOS login immediately after entering the web page, the cookie can not be obtained or obtain the last login cache cookie
    2. After the App restarts, the cookie will be lost

    WKWebView writes cookies to NSHTTPCookieStorage, not live storage. From actual testing, it was found that the delay time was different for different IOS versions. Similarly, the request is not read in real time, so it cannot be synchronized with native, resulting in errors in page logic.

    There are two solutions:

    1. The client manually interferes with the cookie storage. Persisting the cookie of service response to the local, reading the local cookie value when the next WebView starts, and manually writing it to the WebView through Native. But occasionally there is the problem of losing cookies when spa page routing is switched.
    2. Persisting the session stored in cookie to localSorage, each request will take the session stored in localSorage, and add the CookieBack field in the request header. When authenticating the server, the cookieBack field will be verified first. This way, even if the cookie is lost or stored in the last session, it doesn’t matter. However, this approach bypasses the cookie transmission mechanism and does not enjoy the security features of this mechanism.

    You can choose the way suitable for their own projects, there is a better way to deal with welcome to leave a message.

  • The Input tag doesn’t work on some Android WebViews

    Due to Android version fragmentation, many versions of WebViews have different support for evocation functions. We need to rewrite openFileChooser() under WebChromeClient (system 5.0 and above callback onShowFileChooser()). We invoke the system camera and the associated app that supports the Intent in openFileChooser() with the Intent.

    [Android] WebView input upload photo compatibility issues

  • The input tag evokes the soft keyboard on iOS, and the page does not fall back after the keyboard is withdrawn.

    After the input is out of focus, the ios soft keyboard collapses, but the window resize is not triggered, resulting in the actual page DOM still being overlaid by the keyboard — dislocation. Workaround: Listen globally for input out-of-focus events and set the body scrollTop to 0 when the event is triggered.

    document.addEventListener('focusout'.(a)= > {
      document.body.scrollTop = 0;
    });
    Copy the code
  • Evoking the soft keyboard blocks the input box

    When the input or Textarea gets focus, the soft keyboard blocks the input field. Solution: Listen globally for the Window’s resize event. When the event is triggered, fetch the currently active element and check whether it is an input or textarea element. If so, call the element’s scrollIntoViewIfNeeded.

    window.addEventListener('resize'.(a)= > {
      // Determine whether the current active element is input or textarea
      if (
        document.activeElement! .tagName ==='INPUT' ||
        document.activeElement! .tagName ==='TEXTAREA'
      ) {
        setTimeout((a)= > {
          // Native method, scroll to the desired display position
          document.activeElement! .scrollIntoView(); },0); }});Copy the code
  • Wake up the keyboardposition: fixed; bottom: 0px;The element is jacked up by the keyboard

    < span style = “box-sizing: border-box; color: RGB (50, 50, 50); line-height: 20px; font-size: 14px! Important; word-break: inherit! Important;” When the keyboard is retracted, set it to display: block. .

    const clientHeight = document.documentElement.clientHeight;
    window.addEventListener('resize'.(a)= > {
      const bodyHeight = document.documentElement.clientHeight;
      const ele = document.getElementById('fixed-bottom');
      if(! ele)return;
      if (clientHeight > bodyHeight) {
        (ele as HTMLElement).style.display = 'none';
      } else {
        (ele as HTMLElement).style.display = 'block'; }});Copy the code
  • Click on the web input box and the page will be enlarged. Use viewport to set user-scalable=no. Minimum-scale =1 and maximum-scale=1 are not required when user-scalable=no, because the user has been disabled from scaling the page and the allowed scaling range is no longer available. The code is as follows:

    <meta
      name="viewport"
      content="Width = device - width, initial - scale = 1.0, user - scalable = 0, viewport - fit = cover"
    />
    Copy the code
  • A page loaded by webView via loadUrl is opened by a third-party browser at runtime, as shown below

    // Create a Webview
    Webview webview = (Webview) findViewById(R.id.webView);
    // Call Webview loadUrl
    webview.loadUrl("http://www.baidu.com/");
    Copy the code

    Solution: Before calling loadUrl, set up the WebviewClient class. Of course, you can implement the WebviewClient yourself if necessary (for example, by intercepting prompt to enable JS to communicate with native).

    webview.setWebViewClient(new WebViewClient());
    Copy the code