懒懒的技术宅

懒懒的技术宅 查看完整档案

杭州编辑北京交通大学  |  热能与动力工程 编辑阿里巴巴  |  开发工程师 编辑 wen-jy.github.io 编辑
编辑

弱鸡全栈攻城狮

个人动态

懒懒的技术宅 赞了文章 · 2020-11-30

正则表达式高级进阶

概述

本文主要通过介绍正则表达式中的一些进阶内容,让读者了解正则表达式在日常使用中用到的比较少但是又比较重要的一部分内容,从而让大家对正则表达式有一个更加深刻的认识。

本文的主要内容为:

  • 正则表达式回溯法原理
  • 正则表达式操作符优先级

本文不介绍相关正则表达式的基本用法,如果对正则表达式的基本使用方法还不了解的同学,可以阅读我的上一篇博客——正则表达式语法入门

回溯法原理

回溯是影响正则表达式效率的一个非常重要的原因,我们在进行正则表达式匹配时,一定要尽可能的避免回溯。

很多人可能只是对听说过“回溯法”,并不了解其中具体内容和原理,接下来就先让我们看下什么是回溯法。

回溯法的定义

回溯法就是指正则表达式从头开始依次进行匹配,如果匹配到某个特定的情况下时,发现无法继续进行匹配,需要回退到之前匹配的结果,选择另一个分支继续进行匹配中的现象。这个描述可能有点抽象,我们举一个简单的例子,让大家能够更加明确的理解回溯法:

const reg = /ab{1,3}c/;

const str = 'abbc';

// 第1步:匹配/a/,得到'a'
// 第2步:匹配/ab{1}/,得到'ab'
// 第3步:匹配/ab{2}/,得到'abb'
// 第4步:匹配/ab{3}/,匹配失败,需要进行回溯
// 第5步:回溯到/ab{2}/,继续匹配/ab{2}c/,得到'abbc'
// 第6步:正则表达式匹配完成,得到'abbc'

如果我们把正则表达式的各个分支都整理成一棵树的话,正则表达式的匹配其实就是一个深度优先搜索算法。而回溯其实就是在进行深度优先匹配失败后的后退正常操作逻辑。

如果退回到了根节点仍然无法匹配的话,就会将index向后移动一位,重新构建匹配数。即/bc/'abc'时,由于第一个字符'a'无法匹配,则移动到'b'开始匹配。

回溯法产生场景

理解了回溯法和回溯操作,接下来我们来看下什么场景下会出现回溯。出现回溯的场景主要有以下几种:

  1. 贪婪量词(贪婪匹配)
  2. 惰性量词(非贪婪匹配)
  3. 分支结构(分支匹配)

接下来,让我们一个一个来看下这些场景是如何出现回溯的。

贪婪量词(贪婪匹配)

const reg = /ab{1,3}c/;

const str = 'abbc';

// 第1步:匹配/a/,得到'a'
// 第2步:匹配/ab{1}/,得到'ab'
// 第3步:匹配/ab{2}/,得到'abb'
// 第4步:匹配/ab{3}/,匹配失败,需要进行回溯
// 第5步:回溯到/ab{2}/,继续匹配/ab{2}c/,得到'abbc'
// 第6步:正则表达式匹配完成,得到'abbc'

最开始的例子其实就是一个贪婪匹配的示例,通过尽可能多的匹配b从而导致了回溯。

惰性量词(非贪婪匹配)

const reg = /ab{1,3}?c/;

const str = 'abbc';

// 第1步:匹配/a/,得到'a'
// 第2步:匹配/ab{1}/,得到'ab'
// 第3步:匹配/ab{1}c/,匹配失败,需要进行回溯
// 第4步:回溯到/ab{1}/,继续匹配/ab{2}/,得到'abb'
// 第5步:匹配/ab{2}c/,得到'abbc'
// 第6步:正则表达式匹配完成,得到'abbc'

与贪婪匹配类似,非贪婪匹配虽然每次都是去最小匹配数目,但是也会出现回溯的情况。

分支结构(分支匹配)

const reg = /(ab|abc)d/;

const str = 'abcd';

// 第1步:匹配/ab/,得到'ab'
// 第2步:匹配/abd/,匹配失败,需要进行回溯
// 第3步:回溯到//,继续匹配/abc/,得到'abc'
// 第4步:匹配/abcd/,得到'abcd'
// 第5步:正则表达式匹配完成,得到'abcd'

通过上面的示例我们可以看到,分支结构在出现两个分支情况类似的时候,也会出现回溯的情况,在这种情况下,如果一个分支无法匹配,则会回到这个分支的最初情况来重新进行匹配。

正则表达式操作符优先级

看完了回溯法,下面我们来了解下关于正则表达式操作符的优先级。

我们直接看结论,然后再根据结论来给大家提供示例进行理解。

操作符描述操作符优先级
转移符\1
小括号和中括号(…)、(?:…)、(?=…)、(?!…)、[…]2
量词限定符{m}、{m,n}、{m,}、?、*、+3
位置和序列^、$、元字符、一般字符4
管道符\5

通过操作符的优先级,我们能够知道如何来读一个正则表达式。以下面这个正则表达式为例,我们来介绍一下按照优先级进行分析的方法:

const reg = /ab?(c|de*)+|fg/;

// 第一步,根据优先级先考虑(c|de*)+,再根据优先级拆分得到c de*,即匹配c或者de*(注意,位置和序列的优先级高于管道符|,所以是c或de*而不是c或d和e*)
// 第二步,得到ab?,根据优先级拆分得到a和b?
// 第三步,得到fg,这个内容和第一步+第二步的结果为或的关系

最终,我们得到的效果如下:

clipboard.png

通过这个图,大家就能够理解我们的分析思路:先找括号,括号中的一定为一个整体(转移符只做转义,不分割正则,因此可以认为第一优先级其实是括号),没有括号后再从左到右按照优先级进行分析。量词限定符则看做是正则的一个整体。

注:如果大家需要话类似的正则表达式流程图,可以使用此网站

根据上面的优先级,我们就能够避免在正则表达式的理解中出现归类错误的情况。

总结

本文通过介绍在正则表达式中容易被忽略的两个内容:回溯法操作优先级,让大家能够在进行正则的阅读和书写过程中避免踩到相关的坑。

参考内容

  1. 《JavaScript正则表达式迷你书》——老姚 V1.1
  2. 《JavaScript权威指南》

我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev...

查看原文

赞 99 收藏 81 评论 0

懒懒的技术宅 回答了问题 · 2020-11-14

react项目修改tsconfig.json设置了paths别名报错

试试:

"compilerOptions": {
  +  "noFallthroughCasesInSwitch": true
}

参考:https://github.com/facebook/c...

关注 3 回答 2

懒懒的技术宅 回答了问题 · 2020-11-14

js怎样设置动态的table的tr的css样式?

就跟创建table一样的

const tr=document.createElement('tr');
tr.style.border = 0;
table.appendChild(tr)

关注 2 回答 1

懒懒的技术宅 回答了问题 · 2020-11-13

解决react + ts 路径别名配置不生效。

tsconfig也需要配置:

{
    ...,
    "paths":{
        "@":"./src"
    }
}

如果有使用babel编译,也配置下module resolver:
https://github.com/tleunen/ba...

{
    "presets": [
        "next/babel"
    ],
    "plugins": [
        ...,
        [
            "module-resolver",
            {
                "baseUrl": '.',
                "alias": {
                    "@/*": ["./src/*"],
                }
            }
        ]

    ]
}

关注 2 回答 2

懒懒的技术宅 关注了用户 · 2020-11-12

FanibzQo @fanibzqo

关注 1

懒懒的技术宅 收藏了文章 · 2020-07-24

复杂场景下的h5与小程序通信

复杂场景下的h5与小程序通信

一、背景

在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
  • 注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。
  • 小程序处理后的返回结果可能有多种,h5需要在具体使用时监听多个结果进行处理。
  • 一旦监听建立,就无法取消,在组件销毁时如果没有判断组件状态容易导致内存泄漏。

二、在业务内的实践

  • 因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:

    export function injectMiniAppScript() {
        if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
            const s = document.createElement('script'); 
    
            s.src = 'https://appx/web-view.min.js';
            s.onload = () => {
                // 加载完成时触发自定义事件
                const customEvent = new CustomEvent('myLoad', { detail:'' });
                document.dispatchEvent(customEvent);
            };
    
            s.onerror = (e) => {
                // 加载失败时上传日志
                uploadLog({
                    tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
                });
            };
    
            document.body.insertBefore(s, document.body.firstChild);
        }
    }
    

    加载脚本完成后,我们就可以调用my.postMessagemy.onMessage进行通信(统一约定h5发送消息给小程序时,必须带action,小程序根据action处理业务逻辑,同时小程序处理完成的结果必须带type,h5在不同的业务场景下通过my.onMessage处理不同type的响应),比如典型的,h5调用小程序签到:
    h5部分代码如下:

    // 处理扫脸签到逻辑
        const faceVerify = (): Promise<AlipaySignResult> => {
    
            return new Promise((resolve) => {
                const handle = () => {
                    window.my.onMessage = (result: AlipaySignResult) => {
                        if (result.type === 'FACE_VERIFY_TIMEOUT' ||
                            result.type === 'DO_SIGN' ||
                            result.type === 'FACE_VERIFY' ||
                            result.type === 'LOCATION' ||
                            result.type === 'LOCATION_UNBELIEVABLE' ||
                            result.type === 'NOT_IN_ALIPAY') {
                            resolve(result);
                        }
                    };
    
                    window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
                };
    
                if (window.my) {
                    handle();
                } else {
                    // 先记录错误日志
                    sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
                    // 监听load事件
                    document.addEventListener('myLoad', handle);
                }
            });
        };

    实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdkminiAppBus,先来看看怎么用,还是上面的场景

    // 处理扫脸签到逻辑
        const faceVerify = (): Promise<AlipaySignResult> => {
            miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
            return miniAppBus.subscribeAsync<AlipaySignResult>([
                'FACE_VERIFY_TIMEOUT',
                'DO_SIGN',
                'FACE_VERIFY',
                'LOCATION',
                'LOCATION_UNBELIEVABLE',
                'NOT_IN_ALIPAY',
            ])
        };

    可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。

三、实现及背后的思考

  • 为了满足不同场景和使用的方便,公开暴露的interface如下:

      
           interface MiniAppEventBus {
               /**
               * @description 回调函数订阅单个、或多个type
               * @template T
               * @param {(string | string[])} type
               * @param {MiniAppMessageSubscriber<T>} callback
               * @memberof MiniAppEventBus
               */
               subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
               /**
               * @description Promise 订阅单个、或多个type
               * @template T
               * @param {(string | string[])} type
               * @returns {Promise<MiniAppMessage<T>>}
               * @memberof MiniAppEventBus
               */
               subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
               /**
               * @description 取消订阅单个、或多个type
               * @param {(string | string[])} type
               * @returns {Promise<void>}
               * @memberof MiniAppEventBus
               */
               unSubscribe(type: string | string[]): Promise<void>;
               /**
               * @description postMessage替代,无需关注环境变量
               * @param {MessageToMiniApp} msg
               * @returns {Promise<unknown>}
               * @memberof MiniAppEventBus
               */
               postMessage(msg: MessageToMiniApp): Promise<unknown>;
           }

    subscribe:函数接收两个参数,
    type:需要订阅的type,可以是字符串,也可以是数组。
    callback:回调函数。
    subscribeAsync:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。
    unsubscribe:取消订阅。
    postMessage:postMessage替代,无需关注环境变量。

    完整代码:

       import { injectMiniAppScript } from './tools';
    
       /**
       * @description 小程序返回结果
       * @export
       * @interface MiniAppMessage
       */
    
       interface MiniAppMessageBase {
           type: string;
       }
    
       type MiniAppMessage<T extends unknown = {}> = MiniAppMessageBase & {
           [P in keyof T]: T[P]
       }
       /**
       * @description 小程序接收消息
       * @export
       * @interface MessageToMiniApp
       */
       export interface MessageToMiniApp {
           action: string;
           [x: string]: unknown
       }
    
       interface MiniAppMessageSubscriber<T extends unknown = {}> {
           (params: MiniAppMessage<T>): void
       }
       interface MiniAppEventBus {
           /**
           * @description 回调函数订阅单个、或多个type
           * @template T
           * @param {(string | string[])} type
           * @param {MiniAppMessageSubscriber<T>} callback
           * @memberof MiniAppEventBus
           */
           subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
           /**
           * @description Promise 订阅单个、或多个type
           * @template T
           * @param {(string | string[])} type
           * @returns {Promise<MiniAppMessage<T>>}
           * @memberof MiniAppEventBus
           */
           subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
           /**
           * @description 取消订阅单个、或多个type
           * @param {(string | string[])} type
           * @returns {Promise<void>}
           * @memberof MiniAppEventBus
           */
           unSubscribe(type: string | string[]): Promise<void>;
           /**
           * @description postMessage替代,无需关注环境变量
           * @param {MessageToMiniApp} msg
           * @returns {Promise<unknown>}
           * @memberof MiniAppEventBus
           */
           postMessage(msg: MessageToMiniApp): Promise<unknown>;
       }
       class MiniAppEventBus implements MiniAppEventBus{
    
           /**
           * @description: 监听函数
           * @type {Map<string, MiniAppMessageSubscriber[]>}
           * @memberof MiniAppEventBus
           */
           listeners: Map<string, MiniAppMessageSubscriber[]>;
           constructor() {
               this.listeners = new Map<string, Array<MiniAppMessageSubscriber<unknown>>>();
               this.init();
           }
    
           /**
           * @description 初始化
           * @private
           * @memberof MiniAppEventBus
           */
           private init() {
               if (!window.my) {
                   // 引入脚本
                   injectMiniAppScript();
               }
    
               this.startListen();
           }
    
           /**
           * @description 保证my变量存在的时候执行函数func
           * @private
           * @param {Function} func
           * @returns 
           * @memberof MiniAppEventBus
           */
           private async ensureEnv(func: Function) {
               return new Promise((resolve) => {
                   const promiseResolve = () => {
                       resolve(func.call(this));
                   };
    
                   // 全局变量
                   if (window.my) {
                       promiseResolve();
                   }
    
                   document.addEventListener('myLoad', promiseResolve);
               });
           }
    
           /**
           * @description 监听小程序消息
           * @private
           * @memberof MiniAppEventBus
           */
           private listen() {
               window.my.onMessage = (msg: MiniAppMessage<unknown>) => {
                   this.dispatch<unknown>(msg.type, msg);
               };
           }
    
           private async startListen() {
               return this.ensureEnv(this.listen);
           }
    
           /**
           * @description 发送消息,必须包含action
           * @param {MessageToMiniApp} msg
           * @returns 
           * @memberof MiniAppEventBus
           */
           public postMessage(msg: MessageToMiniApp) {
               return new Promise((resolve) => {
                   const realPost = () => {
                       resolve(window.my.postMessage(msg));
                   };
    
                   resolve(this.ensureEnv(realPost));
               });
           }
    
           /**
           * @description 订阅消息,支持单个或多个
           * @template T
           * @param {(string|string[])} type
           * @param {MiniAppMessageSubscriber<T>} callback
           * @returns 
           * @memberof MiniAppEventBus
           */
           public subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>) {
               const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber<T>) => {
                   let listeners = this.listeners.get(type) || [];
    
                   listeners.push(cb);
                   this.listeners.set(type, listeners);
               };
    
               this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
           }
    
           private forEach(type:string | string[],cb:(type:string)=>void){
               if (typeof type === 'string') {
                   return cb(type);
               }
    
               for (const key in type) {
                   if (Object.prototype.hasOwnProperty.call(type, key)) {
                       const element = type[key];
    
                       cb(element);
                   }
               }
           }
    
           /**
           * @description 异步订阅
           * @template T
           * @param {(string|string[])} type
           * @returns {Promise<MiniAppMessage<T>>}
           * @memberof MiniAppEventBus
           */
           public async subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>> {
               return new Promise((resolve, _reject) => {
                   this.subscribe<T>(type, resolve);
               });
           }
    
           /**
           * @description 触发事件
           * @param {string} type
           * @param {MiniAppMessage} msg
           * @memberof MiniAppEventBus
           */
           public async dispatch<T = {}>(type: string, msg: MiniAppMessage<T>) {
               let listeners = this.listeners.get(type) || [];
    
               listeners.map(i => {
                   if (typeof i === 'function') {
                       i(msg);
                   }
               });
           }
    
           public async unSubscribe(type:string | string[]){
               const unsubscribeSingle = (type: string) => {
                   this.listeners.set(type, []);
               };
    
               this.forEach(type,(type:string)=>unsubscribeSingle(type));
           }
       }
    
       export default new MiniAppEventBus();

    class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。

四、小程序内部的处理

  • 定义action handle,通过策略模式解耦:

    const actionHandles = {
        async FACE_VERIFY(){},
        async GET_STEP(){},
        async UPLOAD_HASH(){},
        async GET_AUTH_CODE(){},
        ...// 其他action
    }
    .... 
    // 在webview的消息监听函数中
    async startProcess(e) {
        const data = e.detail;
        // 根据不同的action调用不同的handle处理
        const handle = actionHandles[data.action];
        if (handle) {
            
            return actionHandles[data.action](this, data)
        }
        return uploadLogsExtend({
            tip: STRING_CONTANT.UNKNOWN_ACTIONS,
            data
        })
    }

    使用起来也是得心顺畅,舒服。

其他

类型完备,使用时智能提示,方便快捷。

查看原文

懒懒的技术宅 发布了文章 · 2020-07-24

复杂场景下的h5与小程序通信

复杂场景下的h5与小程序通信

一、背景

在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
  • 注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。
  • 小程序处理后的返回结果可能有多种,h5需要在具体使用时监听多个结果进行处理。
  • 一旦监听建立,就无法取消,在组件销毁时如果没有判断组件状态容易导致内存泄漏。

二、在业务内的实践

  • 因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:

    export function injectMiniAppScript() {
        if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
            const s = document.createElement('script'); 
    
            s.src = 'https://appx/web-view.min.js';
            s.onload = () => {
                // 加载完成时触发自定义事件
                const customEvent = new CustomEvent('myLoad', { detail:'' });
                document.dispatchEvent(customEvent);
            };
    
            s.onerror = (e) => {
                // 加载失败时上传日志
                uploadLog({
                    tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
                });
            };
    
            document.body.insertBefore(s, document.body.firstChild);
        }
    }
    

    加载脚本完成后,我们就可以调用my.postMessagemy.onMessage进行通信(统一约定h5发送消息给小程序时,必须带action,小程序根据action处理业务逻辑,同时小程序处理完成的结果必须带type,h5在不同的业务场景下通过my.onMessage处理不同type的响应),比如典型的,h5调用小程序签到:
    h5部分代码如下:

    // 处理扫脸签到逻辑
        const faceVerify = (): Promise<AlipaySignResult> => {
    
            return new Promise((resolve) => {
                const handle = () => {
                    window.my.onMessage = (result: AlipaySignResult) => {
                        if (result.type === 'FACE_VERIFY_TIMEOUT' ||
                            result.type === 'DO_SIGN' ||
                            result.type === 'FACE_VERIFY' ||
                            result.type === 'LOCATION' ||
                            result.type === 'LOCATION_UNBELIEVABLE' ||
                            result.type === 'NOT_IN_ALIPAY') {
                            resolve(result);
                        }
                    };
    
                    window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
                };
    
                if (window.my) {
                    handle();
                } else {
                    // 先记录错误日志
                    sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
                    // 监听load事件
                    document.addEventListener('myLoad', handle);
                }
            });
        };

    实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdkminiAppBus,先来看看怎么用,还是上面的场景

    // 处理扫脸签到逻辑
        const faceVerify = (): Promise<AlipaySignResult> => {
            miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
            return miniAppBus.subscribeAsync<AlipaySignResult>([
                'FACE_VERIFY_TIMEOUT',
                'DO_SIGN',
                'FACE_VERIFY',
                'LOCATION',
                'LOCATION_UNBELIEVABLE',
                'NOT_IN_ALIPAY',
            ])
        };

    可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。

三、实现及背后的思考

  • 为了满足不同场景和使用的方便,公开暴露的interface如下:

      
           interface MiniAppEventBus {
               /**
               * @description 回调函数订阅单个、或多个type
               * @template T
               * @param {(string | string[])} type
               * @param {MiniAppMessageSubscriber<T>} callback
               * @memberof MiniAppEventBus
               */
               subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
               /**
               * @description Promise 订阅单个、或多个type
               * @template T
               * @param {(string | string[])} type
               * @returns {Promise<MiniAppMessage<T>>}
               * @memberof MiniAppEventBus
               */
               subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
               /**
               * @description 取消订阅单个、或多个type
               * @param {(string | string[])} type
               * @returns {Promise<void>}
               * @memberof MiniAppEventBus
               */
               unSubscribe(type: string | string[]): Promise<void>;
               /**
               * @description postMessage替代,无需关注环境变量
               * @param {MessageToMiniApp} msg
               * @returns {Promise<unknown>}
               * @memberof MiniAppEventBus
               */
               postMessage(msg: MessageToMiniApp): Promise<unknown>;
           }

    subscribe:函数接收两个参数,
    type:需要订阅的type,可以是字符串,也可以是数组。
    callback:回调函数。
    subscribeAsync:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。
    unsubscribe:取消订阅。
    postMessage:postMessage替代,无需关注环境变量。

    完整代码:

       import { injectMiniAppScript } from './tools';
    
       /**
       * @description 小程序返回结果
       * @export
       * @interface MiniAppMessage
       */
    
       interface MiniAppMessageBase {
           type: string;
       }
    
       type MiniAppMessage<T extends unknown = {}> = MiniAppMessageBase & {
           [P in keyof T]: T[P]
       }
       /**
       * @description 小程序接收消息
       * @export
       * @interface MessageToMiniApp
       */
       export interface MessageToMiniApp {
           action: string;
           [x: string]: unknown
       }
    
       interface MiniAppMessageSubscriber<T extends unknown = {}> {
           (params: MiniAppMessage<T>): void
       }
       interface MiniAppEventBus {
           /**
           * @description 回调函数订阅单个、或多个type
           * @template T
           * @param {(string | string[])} type
           * @param {MiniAppMessageSubscriber<T>} callback
           * @memberof MiniAppEventBus
           */
           subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>): void;
           /**
           * @description Promise 订阅单个、或多个type
           * @template T
           * @param {(string | string[])} type
           * @returns {Promise<MiniAppMessage<T>>}
           * @memberof MiniAppEventBus
           */
           subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>>;
           /**
           * @description 取消订阅单个、或多个type
           * @param {(string | string[])} type
           * @returns {Promise<void>}
           * @memberof MiniAppEventBus
           */
           unSubscribe(type: string | string[]): Promise<void>;
           /**
           * @description postMessage替代,无需关注环境变量
           * @param {MessageToMiniApp} msg
           * @returns {Promise<unknown>}
           * @memberof MiniAppEventBus
           */
           postMessage(msg: MessageToMiniApp): Promise<unknown>;
       }
       class MiniAppEventBus implements MiniAppEventBus{
    
           /**
           * @description: 监听函数
           * @type {Map<string, MiniAppMessageSubscriber[]>}
           * @memberof MiniAppEventBus
           */
           listeners: Map<string, MiniAppMessageSubscriber[]>;
           constructor() {
               this.listeners = new Map<string, Array<MiniAppMessageSubscriber<unknown>>>();
               this.init();
           }
    
           /**
           * @description 初始化
           * @private
           * @memberof MiniAppEventBus
           */
           private init() {
               if (!window.my) {
                   // 引入脚本
                   injectMiniAppScript();
               }
    
               this.startListen();
           }
    
           /**
           * @description 保证my变量存在的时候执行函数func
           * @private
           * @param {Function} func
           * @returns 
           * @memberof MiniAppEventBus
           */
           private async ensureEnv(func: Function) {
               return new Promise((resolve) => {
                   const promiseResolve = () => {
                       resolve(func.call(this));
                   };
    
                   // 全局变量
                   if (window.my) {
                       promiseResolve();
                   }
    
                   document.addEventListener('myLoad', promiseResolve);
               });
           }
    
           /**
           * @description 监听小程序消息
           * @private
           * @memberof MiniAppEventBus
           */
           private listen() {
               window.my.onMessage = (msg: MiniAppMessage<unknown>) => {
                   this.dispatch<unknown>(msg.type, msg);
               };
           }
    
           private async startListen() {
               return this.ensureEnv(this.listen);
           }
    
           /**
           * @description 发送消息,必须包含action
           * @param {MessageToMiniApp} msg
           * @returns 
           * @memberof MiniAppEventBus
           */
           public postMessage(msg: MessageToMiniApp) {
               return new Promise((resolve) => {
                   const realPost = () => {
                       resolve(window.my.postMessage(msg));
                   };
    
                   resolve(this.ensureEnv(realPost));
               });
           }
    
           /**
           * @description 订阅消息,支持单个或多个
           * @template T
           * @param {(string|string[])} type
           * @param {MiniAppMessageSubscriber<T>} callback
           * @returns 
           * @memberof MiniAppEventBus
           */
           public subscribe<T extends unknown = {}>(type: string | string[], callback: MiniAppMessageSubscriber<T>) {
               const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber<T>) => {
                   let listeners = this.listeners.get(type) || [];
    
                   listeners.push(cb);
                   this.listeners.set(type, listeners);
               };
    
               this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
           }
    
           private forEach(type:string | string[],cb:(type:string)=>void){
               if (typeof type === 'string') {
                   return cb(type);
               }
    
               for (const key in type) {
                   if (Object.prototype.hasOwnProperty.call(type, key)) {
                       const element = type[key];
    
                       cb(element);
                   }
               }
           }
    
           /**
           * @description 异步订阅
           * @template T
           * @param {(string|string[])} type
           * @returns {Promise<MiniAppMessage<T>>}
           * @memberof MiniAppEventBus
           */
           public async subscribeAsync<T extends {} = MiniAppMessageBase>(type: string | string[]): Promise<MiniAppMessage<T>> {
               return new Promise((resolve, _reject) => {
                   this.subscribe<T>(type, resolve);
               });
           }
    
           /**
           * @description 触发事件
           * @param {string} type
           * @param {MiniAppMessage} msg
           * @memberof MiniAppEventBus
           */
           public async dispatch<T = {}>(type: string, msg: MiniAppMessage<T>) {
               let listeners = this.listeners.get(type) || [];
    
               listeners.map(i => {
                   if (typeof i === 'function') {
                       i(msg);
                   }
               });
           }
    
           public async unSubscribe(type:string | string[]){
               const unsubscribeSingle = (type: string) => {
                   this.listeners.set(type, []);
               };
    
               this.forEach(type,(type:string)=>unsubscribeSingle(type));
           }
       }
    
       export default new MiniAppEventBus();

    class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。

四、小程序内部的处理

  • 定义action handle,通过策略模式解耦:

    const actionHandles = {
        async FACE_VERIFY(){},
        async GET_STEP(){},
        async UPLOAD_HASH(){},
        async GET_AUTH_CODE(){},
        ...// 其他action
    }
    .... 
    // 在webview的消息监听函数中
    async startProcess(e) {
        const data = e.detail;
        // 根据不同的action调用不同的handle处理
        const handle = actionHandles[data.action];
        if (handle) {
            
            return actionHandles[data.action](this, data)
        }
        return uploadLogsExtend({
            tip: STRING_CONTANT.UNKNOWN_ACTIONS,
            data
        })
    }

    使用起来也是得心顺畅,舒服。

其他

类型完备,使用时智能提示,方便快捷。

查看原文

赞 16 收藏 9 评论 5

懒懒的技术宅 赞了文章 · 2020-07-24

你TM管这玩意儿叫H5编辑器?????

作为一名程序员,产品经理、设计师等身兼数职的我,推荐一款年度最佳的H5编辑器给大家,真心牛逼,业界良心,看的我热血沸腾,回家一脚踢飞正在熟睡的哈士奇!

H5DS编辑器,软件截图:

592956246-5f0edf4a2a0a5.gif

推荐理由1:时间轴

支持动画时间轴,调试动画和音频

image.png

给图层添加动画后可以通过时间轴设置动画,真的很直观和方便。效率提升N倍。其中紫色块表示进入动画,蓝色是离开动画,黄色块是强调动画,绿色块表示音频,另外时间轴开可以设置音乐播放的开始时间。

推荐理由2:批量操作

批量选中后,还可以一键布局,批量粘贴动画,批量设置动画间隔,真是强大的不要不要呀~

image.png

推荐理由3:逐帧动画

逐帧动画哇~ 真的优秀!

image.png

推荐理由4:路径动画

还可以自定义路径动画,贝塞尔曲线,钢笔工具????!!!这就叫专业

image.png

推荐理由5:黑暗模式

黑暗模式,传说中只有IOS才有的黑暗模式,居然在H5DS编辑器中也有,喜欢黑色的编辑器么?那就开启黑暗模式吧!

image.png

推荐理由6:下载代码

尼玛,还可以把做好的作品下载代码到本地。一次只要10个积分,积分不够可以关注官方的公众号,直接赠送500个积分,每天签到可以赠送10个积分。真的很良心~ 积分用完还可以找作者要积分,基本上可以免费使用,之所以为什么不直接免费?因为是怕有人恶意下载导致服务器负载过高挂了大家都没得玩了。

image.png

推荐理由7:强大音频

语音合成,在线录音,上传音频,你需要的都有!

image.png

image.png

推荐理由8:浮动层

唯一一款支持多浮动层的编辑器,给浮动层添加元素后每个页面都会全部加上元素,这个操作比较骚,真的~

image.png

图解浮动层:

微信截图_20200723190133.png

推荐理由9:吸附定位

吸附定位,这个概念真的比较新,通过名字可以了解到这个功能可以让元素吸附到窗口指定的位置,对这个研究了一下发现这个功能真的很酷,特别是解决各种机型兼容性的问题。

推荐理由10:自定义脚本

额,还可以自定义JS脚本,编辑器实现不了的功能,脚本帮你实现。老板:你TM管着玩意儿叫H5编辑器?????可以写代码的编辑器我还是第一次见。

image.png

推荐理由11:丰富的自定义交互功能

编辑器提供了各种交互功能,比如切换页面、拖动、超链接、打电话、隐藏显示图层、弹窗控制、音频控制等丰富的交互功能。真香~

image

推荐理由12:插件可扩展

编辑器自带了很多插件,这款叫H5DS的编辑器更牛逼了,插件还可以自己开发,为了让插件开发者更爽,居然自己开发了一套UI库,哦,你没看错,真的是一套UI库。【h5ds-ui】

woc~ 能不能好好的做编辑器!不得不说真的奥利给呀~

image.png

image.png

推荐理由13:H5DS-JSSDK

额,居然自己开发了一套JSSDK,让React开发的H5DS在vue, angular中大放异彩。传说中的混合开发来了????

image.png

推荐理由14:

推荐H5DS的理由很简单,我TM就是作者!【H5DS网址】

非要逼我跪下来求你才会给我点个赞吗?????老子堂堂七尺男儿!

image.png

大爷!赏个赞吧!

查看原文

赞 53 收藏 15 评论 13

懒懒的技术宅 赞了文章 · 2020-05-11

从 0 到 1 搭建前端异常监控系统

本篇文章读后,你将GET的技能:

●收集前端错误(原生、React、Vue)

●编写错误上报逻辑

●利用Egg.js编写一个错误日志采集服务

●编写webpack插件自动上传sourcemap

●利用sourcemap还原压缩代码源码位置

●利用Jest进行单元测试

有没有心动的感觉?赶紧学起来吧!

如何捕获异常

JS异常:

js异常的特点是,出现不会导致JS引擎崩溃,最多只会终止当前执行的任务。

比如一个页面有两个按钮,如果点击按钮导致页面发生异常,这个时候页面不会崩溃。

只是这个按钮的功能失效,其他按钮还会有效☟

上面的例子我们用setTimeout分别启动了两个任务。

虽然第一个任务执行了一个错误的方法。程序执行停止了。但是另外一个任务并没有收到影响。

其实如果你不打开控制台都看不到发生了错误。好像是错误是在静默中发生的。

下面我们来看看这样的错误该如何收集。

try-catch:

JS作为一门高级语言我们首先想到的使用try-catch来收集。

如果在函数中错误没有被捕获,错误会上抛。

image.png

控制台中打印出的分别是错误信息和错误堆栈。

读到这里大家可能会想那就在最底层做一个错误try-catch不就好了吗。

确实作为一个从java转过来的程序员也是这么想的。

但是理想很丰满,现实很骨感。我们看看下一个例子。

image.png

大家注意运行结果,异常并没有被捕获。

这是因为JS的try-catch功能非常有限一遇到异步就不好用了。

那总不能为了收集错误给所有的异步都加一个try-catch吧,太坑爹了。

其实你想想异步任务其实也不是由代码形式上的上层调用的就比如本例中的setTimeout。

大家想想eventloop就明白啦,其实这些异步函数都是就好比一群没娘的孩子出了错误找不到家大人。

当然我也想过一些黑魔法来处理这个问题比如代理执行或者用过的异步方法。

算了还是还是再看看吧。

异常任务捕获

window.onerror:

window.onerror 最大的好处就是同步任务、异步任务都可捕获。

image.png

onerror返回值

onerror还有一个问题大家要注意 如果返回true 就不会被上抛了。

不然控制台中还会看到错误日志。

监听error事件:

文件中的位置☟

window.addEventListener('error',() => {})

其实 onerror 固然好但是还是有一类异常无法捕获。这就是网络异常的错误。

比如下面的例子。

<img data-original="./xxxxx.png">

试想一下我们如果页面上要显示的图片突然不显示了,而我们浑然不知那就是麻烦了。

addEventListener就是☟

运行结果如下☟

Promise异常捕获:

Promise 的出现主要是为了让我们解决回调地域问题。基本是我们程序开发的标配了。

虽然我们提倡使用 es7 async/await 语法来写。

但是不排除很多祖传代码还是存在Promise写法。

new Promise((resolve, reject) => {
  abcxxx()
});

这种情况无论是onerror还是监听错误事件都是无法捕获的。

image.png

除非每个Promise都添加一个catch方法。

但显然,我们不能这样做。

window.addEventListener("unhandledrejection", e => {
 console.log('unhandledrejection',e)
});

我们可以考虑将unhandledrejection事件捕获的错误抛出交由错误事件统一处理就可以了。

async/await异常捕获:

实际上async/await语法本质还是Promise语法。

区别就是async方法可以被上层的try/catch捕获。

image.png

如果不去捕获的话就会和Promise一样,需要用unhandledrejection事件捕获。

这样的话我们只需要在全局增加unhandlerejection就好了。

image.png

小结:

实际上我们可以将unhandledrejection事件抛出的异常再次抛出就可以统一通过error事件进行处理了。

最终用代码表示如下:

前端工程化

Webpack工程化:

现在是前端工程化的时代,工程化导出的代码一般都是被压缩混淆后的。

比如:

setTimeout(() => {
    xxx(1223)
}, 1000)

image.png

出错的代码指向被压缩后的JS文件,而JS文件长下图这个样子。

image.png

如果想将错误和原有的代码关联起来,那就需要sourcemap文件的帮忙了。

sourceMap是什么?

简单说,sourceMap就是一个文件,里面储存着位置信息。

仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。

那么如何利用sourceMap还原异常代码发生的位置这个问题,我们到异常分析这个章节再讲。

VUE 工程

利用vue-cli工具直接创建一个项目。

image.png

为了测试的需要我们暂时关闭eslint 这里面还是建议大家全程打开eslint。

在vue.config.js进行配置

image.png

我们故意在(文件位置☟)

src/components/HelloWorld.vue

这个时候 错误会在控制台中被打印出来,但是错误事件并没有监听到。

errorHandle 句柄:

为了对Vue发生的异常进行统一的上报,需要利用vue提供的errorHandle句柄。

一旦Vue发生异常都会调用这个方法。

我们在src/main.js

image.png

React 工程:

npx create-react-app react-sample

cd react-sample

yarn start

我们用useEffect hooks 制造一个错误:

并且在src/index.js中增加错误事件监听逻辑:

window.addEventListener('error', args => {
    console.log('error', error)
})

但是从运行结果看虽然输出了错误日志但是还是服务捕获。

Error Boundary 组件

错误边界仅可以捕获其子组件的错误。

错误边界无法捕获其自身的错误。

如果一个错误边界无法渲染错误信息,则错误会向上冒泡至最接近的错误边界。

这也类似于 JavaScript 中 catch {} 的工作机制。

创建ErrorBoundary组件

在src/index.js中包裹App标签☟

最终运行的结果:

异常上报如何选择通讯方式

动态创建img标签:

其实上报就是要将捕获的异常信息发送到后端。最常用的方式首推动态创建标签方式。

因为这种方式无需加载任何通讯库,而且页面是无需刷新的。

基本上目前包括百度统计 Google统计都是基于这个原理做的埋点。

new Image().src = 'http://localhost:7001/monitor/error'+ '?info=xxxxxx'

通过动态创建一个img,浏览器就会向服务器发送get请求。

可以把你需要上报的错误数据放在querystring字符串中,利用这种方式就可以将错误上报到服务器了。

Ajax上报:

实际上我们也可以用ajax的方式上报错误,这和我们在业务程序中并没有什么区别。

上报哪些数据:

上报哪些数据:

我们先看一下error事件参数:

其中核心的应该是错误栈,其实我们定位错误最主要的就是错误栈。

错误堆栈中包含了绝大多数调试有关的信息。其中包括了异常位置(行号,列号),异常信息

上报数据序列化:

由于通讯的时候只能以字符串方式传输,我们需要将对象进行序列化处理。

大概分成以下三步:

1、将异常数据从属性中解构出来,存入一个JSON对象

2、将JSON对象转换为字符串

3、将字符串转换为Base64

当然在后端也要做对应的反向操作 这个我们后面再说。

image.png

异常上报的后端服务器

搭建egg.js工程:

异常上报的数据一定是要有一个后端服务接收才可以。

我们就以比较流行的开源框架eggjs为例来演示

# 全局安装egg-cli
npm i egg-init -g 

# 创建后端项目
egg-init backend --type=simple

cd backend
npm i

# 启动项目
npm run dev

编写error上传接口:

首先在app/router.js添加一个新的路由

image.png

创建一个新的:

controller (app/controller/monitor)

image.png

看一下接收后的结果 ☟

image.png

记入日志文件:

下一步就是将错误记入日志。实现的方法可以自己用fs写,也可以借助log4js这样成熟的日志库。

当然在eggjs中是支持我们定制日志那么就用这个功能定制一个前端错误日志好了。

在/config/config.default.js中增加一个定制日志配置

image.png

在/app/controller/monitor.js中添加日志记录:

image.png

最后实现的效果:

image.png

Webpack插件实现SourceMap上传

谈到异常分析最重要的工作其实是将webpack混淆压缩的代码还原。

创建Webpack插件:

/source-map/plugin(文件位置)

加载webpack插件:

webpack.config.js(文件位置)

添加sourceMap读取逻辑:

在apply函数中增加读取sourcemap文件的逻辑

/plugin/uploadSourceMapWebPlugin.js

实现http上传功能:

服务器端添加上传接口:

/backend/app/router.js(文件位置)

image.png

添加sourcemap上传接口:

/backend/app/controller/monitor.js

image.png

最终效果:

执行webpack打包时调用插件sourcemap被上传至服务器。

image.png

解析ErrorStack

考虑到这个功能需要较多逻辑,我们准备把他开发成一个独立的函数并且用Jest来做单元测试:

先看一下我们的需求☟

image.png

搭建Jest框架:

image.png

首先创建一个/utils/stackparser.js文件☟

image.png

在同级目录下创建测试文件stackparser.spec.js

以上需求我们用Jest表示就是

image.png

整理如下:

下面我们运行Jest

npx jest stackparser --watch

image.png

显示运行失败,原因很简单因为我们还没有实现对吧。

下面我们就实现一下这个方法

反序列Error对象:

首先创建一个新的Error对象 将错误栈设置到Error中。

然后利用error-stack-parser这个npm库来转化为stackFrame

image.png

运行效果如下☟

image.png

解析ErrorStack:

下一步我们将错误栈中的代码位置转换为源码位置

image.png

我们再用Jest测试一下☟

image.png

这时我们再看一下结果:

image.png

这样一来测试就通过啦~

将源码位置记入日志:

image.png

记录完成后,我们再来看一下运行效果:

image.png

结束了这一步,我们的ErrorStack工作就完成了。

需要运用的两种开源框架

Fundebug:

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 

自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有阳光保险、荔枝FM、掌门1对1、核桃编程、微脉等众多品牌企业。

Sentry:

Sentry 是一个开源的实时错误追踪系统,可以帮助开发者实时监控并修复异常问题。

它主要专注于持续集成、提高效率并且提升用户体验。

Sentry 分为服务端和客户端 SDK,前者可以直接使用它家提供的在线服务,也可以本地自行搭建;

后者提供了对多种主流语言和框架的支持,包括 React、Angular、Node、Django、RoR、PHP、Laravel、Android、.NET、JAVA 等。

同时它可提供了和其他流行服务集成的方案,例如 GitHub、GitLab、bitbuck、heroku、slack、Trello 等。

目前公司的项目也都在逐步应用上 Sentry 进行错误日志管理。

总结:

截止到目前为止,我们把前端异常监控的基本功能算是形成了一个MVP(最小化可行产品)。

后面需要升级的还有很多,对错误日志的分析和可视化方面可以使用ELK。

发布和部署可以采用Docker。对eggjs的上传和上报最好要增加权限控制功能。

查看原文

赞 39 收藏 27 评论 3

认证与成就

  • 获得 243 次点赞
  • 获得 24 枚徽章 获得 1 枚金徽章, 获得 7 枚银徽章, 获得 16 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-09
个人主页被 3.2k 人浏览