实习总结

Posted by Qz on January 30, 2018

“Yeah It’s on. ”

正文

最近年底,临放假几天,有时间摸摸鱼,索性写一篇实习总结。我的实习是从2018年9月开始到2019年2月结束,在一家专门做小程序小游戏的公司(小黑屋科技),期间总体来说就是打造了前端的工作流(包括制定eslint规范,框架选型,编写基础组件,推动建立npm私服,打造前端团队cli脚手架,从头开始开发一套基于wepy的小程序sdk),当然还有一些别的业务,比如node爬虫,后台系统的一些页面,猜字小程序,等等。

基于wepy的小程序sdk

先写下最重要的这个sdk,一开始本来是打算在wepy框架基础上,进行再次封装,做成一个内部使用的框架,但由于框架太散,对业务代码的有一定的侵入性,而且容易被人修改,和老大讨论后做成了sdk的形式(如同一个黑盒),这样使用者无需关心内部实现,也不用到处去import方法。

另外,由于我这边是从零开始写需要频繁迭代更新,导致sdk使用者频繁地去pull代码,所以最终决定将sdk做成了一个git submodule去管理。同时,将sdk的配置项抽离出来,由外部的配置文件(baseConfig.js, reqConfig.js)决定。

一开始写sdk是采取面向对象的思想,但写到后面发现sdk的通用性存在一定的局限,经老大指导,最终选择采取IoC依赖倒置模式设计,但考虑到团队的编程习惯,更倾向使用配置文件,所以并没有完全采用IoC模式,没有使用use方法去加载模块,而采用去读配置文件的形式来选择使用什么模块,所以只能算是伪IoC模式。

enter description here

sdk挂载在wepy.$instance.xhwSdk这里

enter description here

当onLaunch初始化sdk后,就可以在任何地方使用sdk

  import { xhwSdk } from './sdk/src/index';
  import { baseConfig } from './config/baseConfig';
  import { reqConfig } from './config/reqConfig';
  
 onLaunch(option) {
     xhwSdk.xhwInit(option, baseConfig, reqConfig);
 }

下面总结一下一些比较难的点

封装request

封装request的难点在于

  • 需要降级处理 (请求错误或请求不到数据时 去请求json文件 避免前端拿不到数据渲染)
  • 需要监控上报处理 (请求错误或请求异常时 上报对应的情况 和监控组件Monitor配合)
  • 部分接口依赖sessionid openid
  • 部分请求使用protobuf协议
  • 一些特殊请求要求只能发起一次请求

设计封装了三种请求

  • xhwRequest(普通请求)
  • xhwRequestWithProto(protobuf协议的请求)
  • xhwGetJson(获取json文件的请求)

请求配置的一项

  getHomePageList: {
    url: '/template/home_page_list',
    method: 'POST',
    header: { 'content-type': 'application/x-www-form-urlencoded' },
    protoHeader: { 'content-type': 'application/octet-stream' },
    needSessionId: true,
    needOpenId: false,
    code: 10000,
    errorHandlingUrl: `/{appId}/homepage/{tagId}/{index}.json`,
    index: 0
  }

亮点就是将needSessionId和needOpenId做成配置项,请求前会根据这个自动去寻找sessionid和openid

/**
 * 封装wx小程序请求,返回一个promise
 * @string action 行为 (必填) (action的值只能为reqConfig中的key)
 * @object data 参数对象  (可省略)  (默认空对象{})
 */
async function xhwRequest(action, data = {}) {
  return new Promise(async (resolve, reject) => {
    let reqConfig = getGlobalData('reqConfig');
    let baseConfig = getGlobalData('baseConfig');
    // console.log(action, data, isProduction);
    if (Object.keys(reqConfig).indexOf(action) === -1) {
      //action不在reqConfig配置中
      throw new SyntaxError(`非法的xhwRequest:  ${action}`);
    }
    if (isJsonRequest() && reqConfig[action].errorHandlingUrl) {
      //降级处理
      let result = await _dealDegrade(reqConfig[action], data, false);
      return resolve(result);
    }
    if (reqConfig[action].needSessionId) {
      //需要sessionid
      data.sessionid = await getSessionId();
    }
    if (reqConfig[action].needOpenId) {
      //需要openid
      data.openid = await getOpenId();
    }
    data.client_version = baseConfig.clientVersion;
    data.item = baseConfig.identification;
    data.timestamp = new Date().getTime();
    data.sign = getSign(data);
    let url = `${baseConfig.baseUrl}${reqConfig[action].url}`;
    wepy.request({
      url: url,
      data: data,
      method: reqConfig[action].method,
      header: reqConfig[action].header
    }).then(
      res => {
        // console.log('res.data.code', res.data.code);
        if (res.statusCode >= 200 && res.statusCode < 300 && res.data.code == 1) {
          resolve(res);
        } else {
          _addMonitorReport(action);
          reject(res);
        }
      },
      rej => {
        _addMonitorReport(action);
        reject(rej);
      });
  });
}

一些特殊请求要求只能发起一次请求

举个例子:

sdk初始化的时候,也就onLaunch的时候,会调用三个接口getInitInfo(获取小程序的init信息 一些挂件和开关) getAbTest 和 getAdList(获取广告列表),但是页面的onLoad和app的onLaunch几乎处于同一时间,页面的onLoad也有可能去获取上面提到的三个接口,但这三个接口规定要设计成在程序运行期间只请求一次,请求后要将拿到的数据保存在globalData中,而不是每个页面独立请求一次,同时这三个接口都依赖sessionid openid,这也会导致多次调用makeSession。

当时整整思考了一天无果,后经老大指导,找到了最终的解决方案

将这些可能会在同一时刻发送多次的请求做成单例模式

/**
 * 将basePostRequest变成缓存队列模式 (只能用于拿seesionId 和 openId)
 * 核心思想:缓存singleton这个Promise的resolve 选择合适的时候回调
 * @param{String} key 标识
 * @param{String} url 请求
 * @param{Object} data 参数对象
 * @returns {Promise<*>}
 */
async function singletonForLogin(key, url) {
  return new Promise(async (resolve, reject) => {
    // const code = (await singletonWxLogin(WX_LOGIN_KEY)).code;
    global.singleton = global.singleton || {};
    global.singleton[key + 'lock'] = global.singleton[key + 'lock'] || false;
    if (global.singleton[key + 'lock']) {
      global.singleton[key].push(resolve);
    } else {
      global.singleton[key + 'lock'] = true;
      global.singleton[key] = [resolve];
      //这时候外面如果有请求过来就会进入缓存队列global.singleton[key]里
      const code = (await wepy.login()).code;
      basePostRequest(url, { code: code }).then((output) => {
        let sessionid = null;
        let openid = null;
        if (output.data.code == 1) {
          sessionid = output.data.data.mixData.sessionid;
          openid = base64Decode(output.data.data.mixData.baseid);
          saveSessionIdAndOpenId(sessionid, openid);
          console.log('sessionid', sessionid);
          console.log('openid', openid);
        } else {
          wepy.$instance.xhwSdk.addReport('makeSessionError');
        }
        let func = {};
        // eslint-disable-next-line
        while (func = global.singleton[key].shift()) {
          func({ openid: openid, sessionid: sessionid });
        }
        global.singleton[key + 'lock'] = false;
      }, reject => {
        wepy.$instance.xhwSdk.addReport('makeSessionError');
      });
    }
  });
}

核心思想:维护一个全局队列,每个发请求的方法都返回一个Promise对象,第一次调用这个方法,把锁设为true,发送请求,后面再调用这个方法,发现有锁,就不会真的发请求出去,会把后面的方法的resolve存到这个这个全局队列,这样第一个方法请求可以拿到response,后面的所有方法就直接 resolve这个response,最后再把锁设为false。

这样一个请求的response对应多个Promise的resolve,形成了单例,另外请求拿到数据后会立即将数据存储到globalData下次再调用方法就可以不用发请求直接拿到数据。


当时我将wx.login去拿code 和 拿code去makeSession 分别封装成了不同的单例,也就是两个全局队列

这样导致了一个bug 有一定概率出现首页白屏的情况

原因是wx.login去拿code 这里会调用多次 也就是有多个code 而我封装成单例 ,但只有第一个code有效,会存在一定的延时

最终解决方案,就是上面的singletonForLogin

将wx.login去拿code 和 拿code去makeSession一起封装成一个单例,也就是一个全局队列


遇到的一个坑

当时,makeSession之后要等200+到300+ms才会去调用getInitInfo getAbTest 和 getAdList,按理说这三个接口依赖sessionid openid,在makeSession之后本来无可厚非,但时间相隔应该不超过100ms。

enter description here

由于之前没遇到这种问题,经过几个小时排查还是束手无策,最后再老大的帮助下,找到了解决方案

排查方法:打印请求前后的时间点,判断出哪里出现了耗时操作

最终定位到有问题的代码

   while (func = global.singleton[key].shift()) {
          func({ openid: openid, sessionid: sessionid });
		  sessionid = output.data.data.mixData.sessionid;
          openid = base64Decode(output.data.data.mixData.baseid);
         saveSessionIdAndOpenId(sessionid, openid);
        }

原来是,我将base64解码 保存openid sessionid这些耗时操作放到了while循环里

最终解决方案,就是上面的singletonForLogin

将所有耗时操作放到while循环外面

数据存储

一般来说全局数据放到globalData或者storage之中,wepy是挂载在wepy.$instance.gloabalData下

当老大教了个黑科技 存储在global下

在Console控制台输入global就可以看到数据了

enter description here

这样就可以封装全局管理数据的方法,并暴露出去给sdk使用者使用

因为并没有很难的全局数据管理的需求,所以也没有必要用上wepy-redux这些库,减少了程序体积

/**
 * 保存数据到全局
 * @param key
 * @param value
 */
function saveGlobalData(key, value) {
  if (key && typeof key === 'string') {
    global.xhw = global.xhw || {};
    global.xhw[key] = value;
  }
}

/**
 * 获取全局数据
 * @param key
 * @return
 */
function getGlobalData(key) {
  if (key && typeof key === 'string') {
    global.xhw = global.xhw || {};
    return global.xhw[key];
  }
  return null;
}

sessionId 和 openId 的获取

  1. 去global取数据 若有数据返回 否则第二步
  2. 去localStorage取取数据 若有数据返回 否则第三步
  3. 进行wx.login拿到code 拿到sessionId 和 opede 进行后端请求nId 并把这两个存到global和localStorage中 方便下次请求

总结: global => localStorage => wx.login

其实不止sessionId 和 openId 一些其他字段也是采取这种设计,这样可以尽可能地减少请求

// openid.js

async function getOpenIdByRequest() {
  let baseConfig = getGlobalData('baseConfig');
  const { openid } = await singletonForLogin(WX_LOGIN_KEY, `${baseConfig.baseUrl}${MAKE_SESSION_URL}`);
  return openid;
}


async function getOpenId() {
  return getGlobalData('openId') || wx.getStorageSync('openId') || getOpenIdByRequest();
}

function getOpenIdSync() {
  return getGlobalData('openId') || wx.getStorageSync('openId') || 'none';
}


export {
  getOpenId,
  getOpenIdSync,
  getOpenIdByRequest
};

统计上报

统计上报这里也是做成了锁和队列模式,为了防止发送大量请求,占用过多请求通道,影响到业务请求

还是一样的套路,第一次调用sendkv就加上锁,后面再调用sendkv就将data存入一个队列中

function sendkv(data) {
  if (lock) {
    //存在锁,此时直接将数据丢入队列
    _pushDataToQueue(data);
  } else {
    //不存在锁,开始上锁
    lock = true;
    if (queue.length === 0) {
      //请求队列为空,直接发送请求
      _sendReport(data);
    } else {
      //请求队列不为空,请求队列添加一个元素
      _pushDataToQueue(data);
      // 发起请求
      _sendReport(queue);
    }
  }
}

_sendReport发送请求时会判断是单条数据还是多条数据

  • 单条数据就是发送到 ‘/api/v3/kv’ 接口
  • 多条数据就JSON.stringify一次,发送到 ‘/api/sdk/merge_kv’ 接口

另外,统计上报请求是不需要马上就发送的,为了避免影响页面,这里也用wx.nextTick做了处理

async function _sendReport(report) {
  let data = {};
  if (isObject(report)) {
    //单条请求
    data = report;
    await _addCommonParams(data);
    REPORT_URL = '/api/v3/kv';
  } else {
    //多条请求
    data.merge_data = JSON.stringify(report);
    //迅速置空队列
    queue = [];
    REPORT_URL = '/api/sdk/merge_kv';
  }
  let baseConfig = getGlobalData('baseConfig');
  const params = {
    url: `${baseConfig.prefixReportUrl}${REPORT_URL}`,
    data: data,
    method: 'POST',
    header: { 'content-type': 'application/x-www-form-urlencoded' }
  };
  if (wx.nextTick) {
    wx.nextTick(() => {
      wepy.request(params).then(_onCompleted, _onRejected);
    });
  } else {
    //不存在wx.nextTick
    wepy.request(params).then(_onCompleted, _onRejected);
  }
}

请求成功的话,如果队列空了就可以把锁关掉

请求失败的话,直接把锁关掉

function _onCompleted(res) {
  console.log('sendkv上报结果 ', res);
  if (queue.length === 0) {
    //_sendReport请求过程中,外部没有调用sendkv接口 所以queue为空数组
    lock = false;
  } else {
    //_sendReport请求过程中,外部有调用sendkv接口 queue不为空数组
    _sendReport(queue);
  }
}

function _onRejected() {
  lock = false;
}

protobuf协议

protobuf协议的话,用到是google-protobuf库,这里是后端和老大调好了的,我这里只是照着流程写写

根据协商好的数据格式生成xxx.proto文件,然后生成对应的js文件,在配置好protobuf的配置文件

关键点在于 配置好request的参数

    wepy.request({
      url: url,
      data: data,
      method: 'POST',
      header: { 'content-type': 'application/octet-stream' },
      dataType: 'application/octet-stream',
      responseType: 'arraybuffer'
    })

解析数据的时候,就按约定的规则,最终返回解析好的数据

一些util方法

工具方法中,也有一些蛮有趣的东西,比如:

由于之前后端规范有点乱所以会出现 下划线命名的数据 和 驼峰命名的数据

所以,这里就由sdk处理下不规范的数据格式

/**
 * 将对象中的key由下划线变成驼峰
 * @param{Object} obj
 * @return{Object}
 */
function transformToHump(obj) {
  let result = obj.constructor === Array ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      //判断ojb子元素是否为对象,如果是,递归复制
      if (obj[key] && typeof obj[key] === 'object') {
        result[underlineToHump(key)] = transformToHump(obj[key]);
      } else {
        //如果不是,简单复制
        result[underlineToHump(key)] = obj[key];
      }
    }
  }
  return result;
}

/**
 * 将单词由下滑线模式转化为驼峰  ab_cc => abCc
 * @param{String} str
 * @returns {String}
 */
function underlineToHump(str) {
  // return str;
  return str.replace(/_(\S)/g, (re, $1) => {
    return re.replace('_', '').replace($1, $1.toUpperCase());
  });
}

还有一些其他

/**
 * 比较版本号的大小
 * @param num1  例如 '1.2.4'
 * @param num2  例如 '1.2.5'
 * @return {boolean} true num1比num2大
 */
function compareVersion(num1, num2) {
  let arr1 = num1.split('.');
  let arr2 = num2.split('.');
  console.log(arr1, arr2);
  for (let i = 0; i < arr1.length; i++) {
    let num1 = Number(arr1[i]);
    let num2 = Number(arr2[i]);
    if (num1 > num2) {
      return true;
    } else if (num1 < num2) {
      return false;
    }
  }
  return false;
}
/**
 * 将普通的url转化成鉴权后的url
 * (对外暴露的方法)
 * https://cloud.tencent.com/document/product/228/13677
 * @param{String} url
 * @return {String}
 */
function transformAuthUrl(url) {
  if (!url || url.indexOf('sign') !== -1) {
    return url;
  }
  // console.log('url', url);
  let key = getGlobalData('resourceSignKey');
  if (!key) {
    //key为'' 或者 undefined
    key = 'alexander';
  } else if (key == 'none') {
    //不做任何处理 直接返回url
    return url;
  }
  //去掉url前缀
  let path = url.replace(/^https?:\/\/\S+?\//, '/');
  let t = (new Date().getTime() / 1000).toString(16).replace(/\.\S+/, '');
  // console.log('tttt', t);
  let sign = '';
  if (path.indexOf('?') !== -1) {
    //path中有?
    path = path.replace(/\?\S+$/, '');
    sign = md5(`${key}${path}${t}`).toString();
    // console.log('path', path);
    return `${url}&sign=${sign}&t=${t}`;
  } else {
    sign = md5(`${key}${path}${t}`).toString();
    // console.log('path', path);
    return `${url}?sign=${sign}&t=${t}`;
  }
}
/**
 *
 * 获取页面路径参数中的article_id
 * (对外暴露的方法)
 * @param{Number} num 索引 (不传或0代表当前页 1代表上一页 如此类推)
 * (如果访问到不存在的页面,返回默认值0)
 * @return
 */
function getArticleId(num) {
  /* eslint-disable */
  let pageLen = getCurrentPages().length;
  let curPage = null;
  if (!num) {
    curPage = getCurrentPages()[pageLen - 1];
  } else {
    if (typeof num !== 'number') {
      throw new SyntaxError(`非法的getArticleId:  ${num}`);
    }
    curPage = getCurrentPages()[pageLen - 1 - num];
    // console.log(curPage);
  }
  /* eslint-disable */
  if (curPage && curPage.options && curPage.options.article_id) {
    return curPage.options.article_id;
  }
  return 0;
}

/**
 * 解决路由跳转超过10层无法跳转的情况
 * (对外暴露的方法)
 * @param{Object} object
 * object包括url(路径),success(成功的回调函数),fail(失败的回调函数)
 */
function xhwNavigateTo({ url, success, fail }) {
  // console.log(url, success, fail);
  let pageLen = getCurrentPages().length;
  // console.log(pageLen);
  if (pageLen === 10) {
    wx.redirectTo({ url, success, fail });
  } else {
    wx.navigateTo({ url, success, fail });
  }
}

node爬虫

https://github.com/QinZhen001/crawler

node一些简单的脚本之前还是写过,但是要做一个要放到线上工程用的爬虫还是没有试过。也通过这个爬虫,加深了node的一些基础知识,之后才能打造出前端团队的脚手架工具

利用koa-generator快速搭建一个koa2项目

enter description here

这其中,也遇到很多问题:

  • ejs模板后端渲染,以瀑布流的形式展示抓回来的数据
  • 如何设计路由
  • 如何设计流程 (抓数据下来,存储服务器,生成json配置文件,上传到腾讯云cos桶,视频添加水印)
  • 如何设计异步流程,保证爬取下来的视频是完整的 (用到async库)

也学到不少东西

  • 学会了使用Fiddler抓包
  • 学会了使用Postman发送请求来验证请求
  • 学会了如何使用Xshell,跳板机
  • 学习了一些基础Linux指令
  • 学会了用pm2部署爬虫

xhw-cli前端脚手架工具

http://npm.heywoods.cn/#/detail/xhw-cli

为了解决wepy框架打包后dist包中的.wxss文件@import路径指向bug,一套代码根据不同的配置文件生成不同的项目等问题

需要对wepy小程序进行预编译和后编译,所以就做一个命令行工具,集成到项目中和npm scripts配合也就成了前端工作流

  "scripts": {
    "init": "xhw-cli compile -p",
    "dev": "wepy build --watch && xhw-cli compile -a",
    "build": "cross-env NODE_ENV=production wepy build --no-cache",
    "after-build": "xhw-cli compile -a",
    "xhw-build": "cross-env NODE_ENV=production wepy build --no-cache && xhw-cli compile -a",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

预编译

去读一个json配置文件,循环遍历相应的页面找到对应的字段,将配置项替换成找到的字段

这里逐行读取文件,用到linebyline库

  • 生成baseConfig.js (config/baseConfig.js)
  • 生成app的config (包括pages,window,navigateToMiniProgramAppIdList)
  • 复制通用页面到src/pages下 (可选功能)
  • 复制通用mixins到src/mixins下 (可选功能)
  • 生成所有页面的config

关键代码

        let answers = await askCopy()
        await _copyFiles(answers)
        let fileName = cmd.pre
        if (!(/json/.test(fileName))) {
            fileName += '.json'
        }
        // appConfig.json的相对路径
        const appConfigRelativePath = `/config/${fileName}`
        // console.log(appConfigRelativePath)
        //预编译
        let pagesPath = path.join(workingPath, `/src/pages`)
        let pageNames = await readDir(pagesPath)
        // console.log('pageNames', pageNames)
        let appConfigPath = path.join(workingPath, appConfigRelativePath)
        let appConfigData = JSON.parse(await readFile(appConfigPath))
        //将result分离成含有config和其他
        const {
            appConfig,
            pageConfig,
            ...data
        } = appConfigData
        let importPromiseArr = []
        pageNames.forEach(item => {
            let name = item.match(pathReg)[1]
            let pagePath = path.join(workingPath, `/src/pages/${name}.wpy`)
            importPromiseArr.push(dealPage(pageConfig[name], pagePath))
        })
        let baseConfigPath = path.join(workingPath, '/src/config/baseConfig.js')
        importPromiseArr.push(_genBaseConfig(data, baseConfigPath))
        let appWpyPath = path.join(workingPath, `/src/app.wpy`)
        importPromiseArr.push(_genAppConfig(appConfig, appWpyPath))
        await Promise.all(importPromiseArr)

后编译

 /**
 * 解决wepy框架 npm包嵌套导致的bug
 * 打包后dist中.wxss文件@import引入的路径会错误 (删去 /src )
 * @import "./../../src/npm/base-scene-image/BaseSceneImage.wxss";  =>  @import "./../../npm/base-scene-image/BaseSceneImage.wxss";
 */
async function afterCompile() {
    const workingPath = path.join(getWorkingPath(), '/dist')
    let fileNames = await readDir(workingPath)
    if (fileNames.indexOf('npm') === -1) {
        console.log(chalk.blue('当前目录不存在npm文件夹'))
        return
    }
    const searchPath = path.join(workingPath, '/npm')
    let searchFiles = await readDir(searchPath)
    // console.log(searchFiles)
    searchFiles.forEach(async item => {
        let finalPath = path.join(searchPath, `/${item}`)
        let finalFileNames = await readDir(finalPath)
        // console.log(finalFileNames)
        finalFileNames.forEach(async final => {
            if (final.indexOf('.wxss') !== -1) {
                await _fixBug(path.join(finalPath, `/${final}`))
            }
        })
    })
}



async function _fixBug(path) {
    // console.log(path)
    let result = await readFile(path)
    if (/\/src\/npm\//.test(result)) {
        // console.log(result)
        result = result.replace('/src/npm/', '/npm/')
        // console.log(result)
        writeFile(path, result)
        console.log(chalk.blue(`成功改变${path} @imoport指向`))
    }
}

其他

其余,还做一些零碎的东西,比如,后端系统的一些页面也就是vue+element没什么难点,猜字类小程序,其难点在于答案在不同的字段时要显示不同的UI,这也就出现了16种可能,还有当跳转到下一题时要有淡入淡出的效果。

这里可以提一下做的一些基础组件

监控组件Monitor

监控上报组件

本质上就是一个image标签,可以通过动态改变image的src来发送get请求从而达到上报错误

当出现错误情况时,把这个错误名加入到一个全局队列中(调用addReport),选择合适的时机上报(调用checkReport)

<template>
  <image src="" @load="imgSuccessLoad" @error="imgErrorLoad"></image>
</template>
  data = {
      monitorReportUrl: 'http://xhw-front.heywoods.cn/images/monitor-report.png'
    };

    methods = {
      imgSuccessLoad(src) {
        if (global.xhw && !global.xhw.reportReady && !global.xhw.reportClose) {
          global.xhw.reportReady = true;
          console.log('图片加载完成', src);
          if (wx.nextTick) {
            wx.nextTick(() => {
              this.monitorReport();
            });
          } else {
            this.monitorReport();
          }
        }
      },
      imgErrorLoad(err) {
        let monitorList = global.xhw.monitorList || [];
        // console.log('monitorList', monitorList)
        console.log('监控上报成功', err.detail);
        let tempName = monitorList.pop();
        this.monitorReportUrl = tempName ? this._nameToMonitorUrl(tempName) : '';
        this.$apply();
      }
    };
    /**
     * 监控上报
     * (对外暴露的方法)
     */
    monitorReport() {
      let monitorList = global.xhw.monitorList || [];
      // console.log('monitorList', monitorList)
      let last = monitorList.pop();
      if (last) {
        this.monitorReportUrl = this._nameToMonitorUrl(last);
        this.$apply();
      }
    }

这里有些细节设计

monitorReportUrl有初始值,是一张一像素的图片,为什么需要它呢?

因为由它来判断Monitor组件是否已经准备好了,准备好了的状态global.xhw.reportReady是一个全局状态,并不是每一个组件都维护一个这样的状态,而是所有Monitor组件共享一个全局状态

Monitor组件会在两种情况上报

  1. Monitor组件已经准备好了 (为了确保有些用户一进页面就退出这种情况下,还是可以监控到错误)
  2. 页面的onShow生命周期 (写在所有页面通用的mixins中)

为了不影响页面渲染,这里也做了nextTick处理