因为公司在这方面没有太多技术沉淀,所以在开发期间遇到了很多坑,经过一年多的技术攻克积累,最终形成了这套比较完善的解决方案,总结出来希望能够帮助到大家,尤其是对一些中小公司这方面经验不足的(PS: 大公司估计有他们自己的一套方案了)。 好了废话不多说,先亮下这个库的 GitHub 地址,后面还会不断完善,欢迎 star: mobile-web-best-practice[2] 移动端 web 最佳实践,基于 vue-cli3[3] 搭建的 typescript[4] 项目,可以用于 hybrid 应用或者纯 webapp 开发。以下大部分内容同样适用于 react[5] 等前端框架。 其中有三个点尚在完善中:领域驱动设计(DDD)应用、微前端、性能监控,后续完成后会以单独的文章发出来。其中性能监控还没有太好的选择,类似错误监控 sentry 那种开源免费而且功能强大的工具,如果有人知道的麻烦告知下。文中难免有些错误或者更好的方案,也欢迎不吝赐教。 目录
组件库vant[23] vux[24] mint-ui[25] cube-ui[26] vue 移动端组件库目前主要就是上面罗列的这几个库,本项目使用的是有赞前端团队开源的 vant。 vant 官方目前已经支持自定义样式主题,基本原理就是在 less-loader[27] 编译 less[28] 文件到 css 文件过程中,利用 less 提供的 modifyVars[29] 对 less 变量进行修改,本项目也采用了该方式,具体配置请查看相关文档: 定制主题[30] 推荐一篇介绍各个组件库特点的文章: Vue 常用组件库的比较分析(移动端)[31] JSBridgeDSBridge-IOS[32] DSBridge-Android[33] WebViewJavascriptBridge[34] 混合应用中一般都是通过 webview 加载网页,而当网页要获取设备能力(例如调用摄像头、本地日历等)或者 native 需要调用网页里的方法,就需要通过 JSBridge 进行通信。 开源社区中有很多功能强大的 JSBridge,例如上面列举的库。本项目基于保持 iOS android 平台接口统一原因,采用了 DSBridge,各位可以选择适合自己项目的工具。 本项目以 h5 调用 native 提供的同步日历接口为例,演示如何在 dsbridge 基础上进行两端通信的。下面是两端的关键代码摘要: 安卓端同步日历核心代码,具体代码请查看与本项目配套的安卓项目 mobile-web-best-practice-container[35]: public class JsApi { h5 端同步日历核心代码(通过装饰器来限制调用接口的平台)
另外推荐一个笔者之前写的一个基于安卓平台实现的教学版 JSBridge[36],里面详细阐述了如何基于底层接口一步步封装一个可用的 JSBridge: JSBridge 实现原理[37] 路由堆栈管理(模拟原生 APP 导航)vue-page-stack[38] vue-navigation[39] vue-stack-router[40] 在使用 h5 开发 app,会经常遇到下面的需求:从列表进入详情页,返回后能够记住当前位置,或者从表单点击某项进入到其他页面选择,然后回到表单页,需要记住之前表单填写的数据。可是目前 vue 或 react 框架的路由,均不支持同时存在两个页面实例,所以需要路由堆栈进行管理。 其中 vue-page-stack 和 vue-navigation 均受 vue 的 keepalive 启发,基于 vue-router[41],当进入某个页面时,会查看当前页面是否有缓存,有缓存的话就取出缓存,并且清除排在他后面的所有 vnode,没有缓存就是新的页面,需要存储或者是 replace 当前页面,向栈里面 push 对应的 vnode,从而实现记住页面状态的功能。 而逻辑思维前端团队的 vue-stack-router 则另辟蹊径,抛开了 vue-router,自己独立实现了路由管理,相较于 vue-router,主要是支持同时可以存活 A 和 B 两个页面的实例,或者 A 页面不同状态的两个实例,并支持原生左滑功能。但由于项目还在初期完善,功能还没有 vue-router 强大,建议持续关注后续动态再做决定是否引入。 本项目使用的是 vue-page-stack,各位可以选择适合自己项目的工具。同时推荐几篇相关文章: 【vue-page-stack】Vue 单页应用导航管理器 正式发布[42] Vue 社区的路由解决方案:vue-stack-router[43] 请求数据缓存mem[44] 在我们的应用中,会存在一些很少改动的数据,而这些数据有需要从后端获取,比如公司人员、公司职位分类等,此类数据在很长一段时间时不会改变的,而每次打开页面或切换页面时,就重新向后端请求。为了能够减少不必要请求,加快页面渲染速度,可以引用 mem 缓存库。 mem 基本原理是通过以接收的函数为 key 创建一个 WeakMap,然后再以函数参数为 key 创建一个 Map,value 就是函数的执行结果,同时将这个 Map 作为刚刚的 WeakMap 的 value 形成嵌套关系,从而实现对同一个函数不同参数进行缓存。而且支持传入 maxAge,即数据的有效期,当某个数据到达有效期后,会自动销毁,避免内存泄漏。 选择 WeakMap 是因为其相对 Map 保持对键名所引用的对象是弱引用,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。 mem 作为高阶函数,可以直接接受封装好的接口请求。但是为了更加直观简便,我们可以按照类的形式集成我们的接口函数,然后就可以用装饰器的方式使用 mem 了(装饰器只能修饰类和类的类的方法,因为普通函数会存在变量提升)。下面是相关代码: import http from '../http'; 构建时预渲染针对目前单页面首屏渲染时间长(需要下载解析 js 文件然后渲染元素并挂载到 id 为 app 的 div 上),SEO 不友好(index.html 的 body 上实际元素只有 id 为 app 的 div 元素,真正的页面元素都是动态挂载的,搜索引擎的爬虫无法捕捉到),目前主流解决方案就是服务端渲染(SSR),即从服务端生成组装好的完整静态 html 发送到浏览器进行展示,但配置较为复杂,一般都会借助框架,比如 vue 的 nuxt.js[45],react 的 next[46]。 其实有一种更简便的方式--构建时预渲染。顾名思义,就是项目打包构建完成后,启动一个 Web Server 来运行整个网站,再开启多个无头浏览器(例如 Puppeteer[47]、Phantomjs[48] 等无头浏览器技术)去请求项目中所有的路由,当请求的网页渲染到第一个需要预渲染的页面时(需提前配置需要预渲染页面的路由),会主动抛出一个事件,该事件由无头浏览器截获,然后将此时的页面内容生成一个 HTML(包含了 JS 生成的 DOM 结构和 CSS 样式),保存到打包文件夹中。 根据上面的描述,我们可以其实它本质上就只是快照页面,不适合过度依赖后端接口的动态页面,比较适合变化不频繁的静态页面。 实际项目相关工具方面比较推荐 prerender-spa-plugin[49] 这个 webpack 插件,下面是这个插件的原理图。不过有两点需要注意: 一个是这个插件需要依赖 Puppeteer,而因为国内网络原因以及本身体积较大,经常下载失败,不过可以通过 .npmrc 文件指定 Puppeteer 的下载路径为国内镜像; 另一个是需要设置路由模式为 history 模式(即基于 html5 提供的 history api 实现的,react 叫 BrowserRouter,vue 叫 history),因为 hash 路由无法对应到实际的物理路由。(即线上渲染时 history 下,如果 form 路由被设置成预渲染,那么访问 /form/ 路由时,会直接从服务端返回 form 文件夹下的 index.html,之前打包时就已经预先生成了完整的 HTML 文件 ) 本项目已经集成了 prerender-spa-plugin,但由于和 vue-stack-page/vue-navigation 这类路由堆栈管理器一起使用有问题(原因还在查找,如果知道的朋友也可以告知下),所以 prerender 功能是关闭的。 同时推荐几篇相关文章: vue 预渲染之 prerender-spa-plugin 解析(一)[50] 使用预渲提升 SPA 应用体验[51] Webpack 策略基础库抽离对于一些基础库,例如 vue、moment 等,属于不经常变化的静态依赖,一般需要抽离出来以提升每次构建的效率。目前主流方案有两种: 一种是使用 webpack-dll-plugin[52] 插件,在首次构建时就讲这些静态依赖单独打包,后续只需引入早已打包好的静态依赖包即可; 另一种就是外部扩展 Externals[53] 方式,即把不需要打包的静态资源从构建中剔除,使用 CDN 方式引入。下面是 webpack-dll-plugin 相对 Externals 的缺点:
不过选择 Externals 还是需要一个靠谱的 CDN 服务的。 本项目选择的是 Externals,各位可根据项目需求选择不同的方案。 更多内容请查看这篇文章(上面观点来自于这篇文章): Webpack 优化——将你的构建效率提速翻倍[54] 手势库hammer.js[55] AlloyFinger[56] 在移动端开发中,一般都需要支持一些手势,例如拖动(Pan),缩放(Pinch),旋转(Rotate),滑动(swipe)等。目前已经有很成熟的方案了,例如 hammer.js 和腾讯前端团队开发的 AlloyFinger 都很不错。本项目选择基于 hammer.js 进行二次封装成 vue 指令集,各位可根据项目需求选择不同的方案。 下面是二次封装的关键代码,其中用到了 webpack 的 require.context 函数来获取特定模块的上下文,主要用来实现自动化导入模块,比较适用于像 vue 指令这种模块较多的场景:
另外推荐一篇关于 hammer.js 和一篇关于 require.context 的文章: H5 案例分享:JS 手势框架 —— Hammer.js[57] 使用 require.context 实现前端工程自动化[58] 样式适配postcss-px-to-viewport[59] Viewport Units Buggyfill[60] flexible[61] postcss-pxtorem[62] Autoprefixer[63] browserslist[64] 在移动端网页开发时,样式适配始终是一个绕不开的问题。对此目前主流方案有 vw 和 rem(当然还有 vw rem 结合方案,请见下方 rem-vw-layout 仓库),其实基本原理都是相通的,就是随着屏幕宽度或字体大小成正比变化。因为原理方面的详细资料网络上已经有很多了,就不在这里赘述了。下面主要提供一些这工程方面的工具。 关于 rem,阿里无线前端团队在 15 年的时候基于 rem 推出了 flexible 方案,以及 postcss 提供的自动转换 px 到 rem 的插件 postcss-pxtorem。 关于 vw,可以使用 postcss-px-to-viewport 进行自动转换 px 到 vw。postcss-px-to-viewport 相关配置如下: 'postcss-px-to-viewport': { 下面是 vw 和 rem 的优缺点对比图: 关于 vw 兼容性问题,目前在移动端 iOS 8 以上以及 Android 4.4 以上获得支持。如果有兼容更低版本需求的话,可以选择 viewport 的 pollify 方案,其中比较主流的是 Viewport Units Buggyfill[65]。 本方案因不准备兼容低版本,所以直接选择了 vw 方案,各位可根据项目需求选择不同的方案。 另外关于设置 css 兼容不同浏览器,想必大家都知道 Autoprefixer(vue-cli3 已经默认集成了),那么如何设置要兼容的范围呢?推荐使用 browserslist,可以在 .browserslistrc 或者 pacakage.json 中 browserslist 部分设置兼容浏览器范围。因为不止 Autoprefixer,还有 Babel,postcss-preset-env 等工具都会读取 browserslist 的兼容配置,这样比较容易使 js css 兼容浏览器的范围保持一致。下面是本项目的 .browserslistrc 配置:
最后推荐一些移动端样式适配的资料: rem-vw-layout[66] 细说移动端 经典的 REM 布局 与 新秀 VW 布局[67] 如何在 Vue 项目中使用 vw 实现移动端适配[68] 表单校验async-validator[69] vee-validate[70] 由于大部分移动端组件库都不提供表单校验,因此需要自己封装。目前比较多的方式就是基于 async-validator 进行二次封装(elementUI 组件库提供的表单校验也是基于 async-validator ),或者使用 vee-validate(一种基于 vue 模板的轻量级校验框架)进行校验,各位可根据项目需求选择不同的方案。 本项目的表单校验方案是在 async-validator 基础上进行二次封装,代码如下,原理很简单,基本满足需求。如果还有更完善的方案,欢迎提出来。 其中 setRules 方法是将组件中设置的 rules(符合 async-validator 约定的校验规则)按照需要校验的数据的名字为 key 转化一个对象 validator,value 是 async-validator 生成的实例。validator 方法可以接收单个或多个需要校验的数据的 key,然后就会在 setRules 生成的对象 validator 中寻找 key 对应的 async-validator 实例,最后调用实例的校验方法。当然也可以不接受参数,那么就会校验所有传入的数据。 import schema from 'async-validator'; 阻止原生返回事件开发中可能会遇到下面这个需求:当页面弹出一个 popup 或 dialog 组件时,点击返回键时是隐藏弹出的组件而不是返回到上一个页面。 为了解决这个问题,我们可以从路由栈角度思考。一般弹出组件是不会在路由栈上添加任何记录,因此我们在弹出组件时,可以在路由栈中 push 一个记录,为了不让页面跳转,我们可以把跳转的目标路由设置为当前页面路由,并加上一个 query 来标记这个组件弹出的状态。 然后监听 query 的变化,当点击弹出组件时,query 中与该弹出组件有关的标记变为 true,则将弹出组件设为显示;当用户点击 native 返回键时,路由返回上一个记录,仍然是当前页面路由,不过 query 中与该弹出组件有关的标记不再是 true 了,这样我们就可以把弹出组件设置成隐藏,同时不会返回上一个页面。相关代码如下:
通过 UA 获取设备信息在开发 h5 开发时,可能会遇到下面几种情况:
所以需要一种方式来检测页面当前所处设备的平台类型、app 版本、系统版本等,目前比较靠谱的方式是通过 android / ios webview 修改 UserAgent,在原有的基础上加上特定后缀,然后在网页就可以通过 UA 获取设备相关信息了。当然这种方式的前提是 native 代码是可以为此做出改动的。以安卓为例关键代码如下: 安卓关键代码: // Activity -> onCreate h5 关键代码:
mock 数据Mock[71] 当前后端进度不一致,接口还尚未实现时,为了不影响彼此的进度,此时前后端约定好接口数据格式后,前端就可以使用 mock 数据进行独立开发了。本项目使用了 Mock 实现前端所需的接口。 调试控制台eruda[72] vconsole[73] 在调试方面,本项目使用 eruda 作为手机端调试面板,功能相当于打开 PC 控制台,可以很方便地查看 console, network, cookie, localStorage 等关键调试信息。与之类似地工具还有微信的前端研发团队开发的 vconsole,各位可以选择适合自己项目的工具。 关于 eruda 使用,推荐使用 cdn 方式加载,至于什么时候加载 eruda,可以根据不同项目制定不同策略。示例代码如下: <script> 抓包工具charles[74] fiddler[75] 虽然有了 eruda 调试工具,但某些情况下仍不能满足需求,比如现网完全关闭 eruda 等情况。 此时就需要抓包工具,相关工具主要就是上面罗列的这两个,各位可以选择适合自己项目的工具。 通过 charles 可以清晰的查看所有请求的信息(注:https 下抓包需要在手机上配置相关证书)。当然 charles 还有更多强大功能,比例模拟弱网情况,资源映射等。 推荐一篇不错的 charles 使用教程: 解锁 Charles 的姿势[76] 异常监控平台sentry[77] 移动端网页相对 PC 端,主要有设备众多,网络条件各异,调试困难等特点。导致如下问题:
这时就非常需要一个异常监控平台,将异常实时上传到平台,并及时通知相关人员。 相关工具有 sentry,fundebug 等,其中 sentry 因为功能强大,支持多平台监控(不仅可以监控前端项目),完全开源,可以私有化部署等特点,而被广泛采纳。 下面是 sentry 在本项目应用时使用的相关配套工具。 sentry 针对 javascript 的 sdk sentry-javascript[78] 自动上传 sourcemap 的 webpack 插件 sentry-webpack-plugin[79] 编译时自动在 try catch 中添加错误上报函数的 babel 插件 babel-plugin-try-catch-error-report[80] 补充: 前端的异常主要有以下几个部分:
其中静态资源加载失败,可以通过 window.addEventListener('error', ..., true) 在事件捕获阶段获取,然后筛选出资源加载失败的错误并手动上报错误。核心代码如下:
关于服务端接口异常,可以通过在封装的 http 模块中,全局集成上报错误函数(native 接口的错误上报类似,可在项目中查看)。核心代码如下: function errorReport( 关于全局 js 报错,sentry 针对的前端的 sdk 已经通过 window.onerror 和 window.addEventListener('unhandledrejection', ..., false) 进行全局监听并上报。 需要注意的是其中 window.onerror = (message, source, lineno, colno, error) =>{} 不同于 window.addEventListener('error', ...),window.onerror 捕获的信息更丰富,包括了错误字符串信息、发生错误的 js 文件,错误所在的行数、列数、和 Error 对象(其中还会有调用堆栈信息等)。所以 sentry 会选择 window.onerror 进行 js 全局监控。 但有一种错误是 window.onerror 监听不到的,那就是 unhandledrejection 错误,这个错误是当 promise reject 后没有 catch 住所引起的。当然 sentry 的 sdk 也已经做了监听。 针对 vue 项目,也可对 errorHandler 钩子进行全局监听,react 的话可以通过 componentDidCatch 钩子,vue 相关代码如下:
但是对于我们业务中,经常会对一些以报错代码使用 try catch,这些错误如果没有在 catch 中向上抛出,是无法通过 window.onerror 捕获的,针对这种情况,笔者开发了一个 babel 插件 babel-plugin-try-catch-error-report[81],该插件可以在 babel[82] 编译 js 的过程中,通过在 ast 中查找 catch 节点,然后再 catch 代码块中自动插入错误上报函数,可以自定义函数名,和上报的内容(源码所在文件,行数,列数,调用栈,以及当前 window 属性,比如当前路由信息 window.location.href)。相关配置代码如下: if (!IS_DEV) { 针对跨域 js 问题,当加载的不同域的 js 文件时,例如通过 cdn 加载打包后的 js。如果 js 报错,window.onerror 只能捕获到 script error,没有任何有效信息能帮助我们定位问题。此时就需要我们做一些事情:第一步、服务端需要在返回 js 的返回头设置 Access-Control-Allow-Origin: *第二部、设置 script 标签属性 crossorigin,代码如下:
如果是动态添加的,也可动态设置: const script = document.createElement('script'); 针对网页崩溃问题,推荐一个基于 service work 的监控方案,相关文章已列在下面的。如果是 webview 加载网页,也可以通过 webview 加载失败的钩子监控网页崩溃等。 如何监控网页崩溃?[83] 最后,因为部署到线上的代码一般都是经过压缩混淆的,如果没有上传 sourcemap 的话,是无法定位到具体源码的,可以现在 项目中添加 .sentryclirc 文件,其中内容可参考本项目的 .sentryclirc,然后通过 sentry-cli (需要全局全装 sentry-cli 即
当然官方也提供了 webpack 插件 sentry-webpack-plugin[84],当打包时触发 webpack 的 after-emit 事件钩子(即生成资源到 output 目录之后),插件会自动上传打包目录中的 sourcemap 和关联的 js,相关配置可参考本项目的 vue.config.js 文件。 通常为了安全,是不允许在线上部署 sourcemap 文件的,所以上传 sourcemap 到 sentry 后,可手动删除线上 sourcemap 文件。 常见问题
参考资料mattermost-mobile: https://github.com/mattermost/mattermost-mobile [2]mobile-web-best-practice: https://github.com/mcuking/mobile-web-best-practice [3]vue-cli3: https://cli./ [4]typescript: http://www./ [5]react: https:/// [6]组件库: #组件库 [7]JSBridge: #jsbridge [8]路由堆栈管理(模拟原生 APP 导航): #路由堆栈管理模拟原生-app-导航 [9]请求数据缓存: #请求数据缓存 [10]构建时预渲染: #构建时预渲染 [11]Webpack 策略: #webpack-策略 [12]基础库抽离: #基础库抽离 [13]手势库: #手势库 [14]样式适配: #样式适配 [15]表单校验: #表单校验 [16]阻止原生返回事件: #阻止原生返回事件 [17]通过 UA 获取设备信息: #通过-ua-获取设备信息 [18]mock 数据: #mock-数据 [19]调试控制台: #调试控制台 [20]抓包工具: #抓包工具 [21]异常监控平台: #异常监控平台 [22]常见问题: #常见问题 [23]vant: https://youzan./vant/#/zh-CN/intro [24]vux: https://github.com/airyland/vux [25]mint-ui: https://github.com/ElemeFE/mint-ui [26]cube-ui: https://github.com/didi/cube-ui [27]less-loader: https://github.com/webpack-contrib/less-loader [28]less: http:/// [29]modifyVars: http:///usage/#using-less-in-the-browser-modify-variables [30]定制主题: https://youzan./vant/#/zh-CN/theme [31]Vue 常用组件库的比较分析(移动端): https://blog.csdn.net/weixin_38633659/article/details/89736656 [32]DSBridge-IOS: https://github.com/wendux/DSBridge-IOS [33]DSBridge-Android: https://github.com/wendux/DSBridge-Android [34]WebViewJavascriptBridge: https://github.com/marcuswestin/WebViewJavascriptBridge [35]mobile-web-best-practice-container: https://github.com/mcuking/mobile-web-best-practice-container [36]JSBridge: https://github.com/mcuking/JSBridge [37]JSBridge 实现原理: https://github.com/mcuking/JSBridge [38]vue-page-stack: https://github.com/hezhongfeng/vue-page-stack [39]vue-navigation: https://github.com/zack24q/vue-navigation [40]vue-stack-router: https://github.com/luojilab/vue-stack-router [41]vue-router: https://router./ [42]【vue-page-stack】Vue 单页应用导航管理器 正式发布: https:///post/5d2ef417f265da1b971aa94f [43]Vue 社区的路由解决方案:vue-stack-router: https:///post/5d4ce4fd6fb9a06acd450e8c [44]mem: https://github.com/sindresorhus/mem [45]nuxt.js: https://github.com/nuxt/nuxt.js [46]next: https://github.com/zeit/next.js [47]Puppeteer: https://github.com/GoogleChrome/puppeteer [48]Phantomjs: https://github.com/ariya/phantomjs [49]prerender-spa-plugin: https://github.com/chrisvfritz/prerender-spa-plugin [50]vue 预渲染之 prerender-spa-plugin 解析(一): https://blog.csdn.net/vv_bug/article/details/84593052 [51]使用预渲提升 SPA 应用体验: https:///post/5d5fa22ee51d4561de20b5f5 [52]webpack-dll-plugin: https://webpack./plugins/dll-plugin/ [53]Externals: https://webpack./configuration/externals/ [54]Webpack 优化——将你的构建效率提速翻倍: https:///post/5d614dc96fb9a06ae3726b3e [55]hammer.js: https://github.com/hammerjs/hammer.js [56]AlloyFinger: https://github.com/AlloyTeam/AlloyFinger [57]H5 案例分享:JS 手势框架 —— Hammer.js: https://www./articles/201609/hammerjs.html [58]使用 require.context 实现前端工程自动化: https://www.jianshu.com/p/c894ea00dfec [59]postcss-px-to-viewport: https://github.com/evrone/postcss-px-to-viewport [60]Viewport Units Buggyfill: https://github.com/rodneyrehm/viewport-units-buggyfill [61]flexible: https://github.com/amfe/lib-flexible [62]postcss-pxtorem: https://github.com/cuth/postcss-pxtorem [63]Autoprefixer: https://github.com/postcss/autoprefixer [64]browserslist: https://github.com/browserslist/browserslist [65]Viewport Units Buggyfill: https://github.com/rodneyrehm/viewport-units-buggyfill [66]rem-vw-layout: https://github.com/imwtr/rem-vw-layout [67]细说移动端 经典的 REM 布局 与 新秀 VW 布局: https://www.cnblogs.com/imwtr/p/9648233.html [68]如何在 Vue 项目中使用 vw 实现移动端适配: https://www.jianshu.com/p/1f1b23f8348f [69]async-validator: https://github.com/yiminghe/async-validator [70]vee-validate: https://github.com/baianat/vee-validate [71]Mock: https://github.com/nuysoft/Mock [72]eruda: https://github.com/liriliri/eruda [73]vconsole: https://github.com/Tencent/vConsole [74]charles: https://www./ [75]fiddler: https://www./fiddler [76]解锁 Charles 的姿势: https:///post/5a1033d2f265da431f4aa81f [77]sentry: https://github.com/getsentry/sentry [78]sentry-javascript: https://github.com/getsentry/sentry-javascript [79]sentry-webpack-plugin: https://github.com/getsentry/sentry-webpack-plugin [80]babel-plugin-try-catch-error-report: https://github.com/mcuking/babel-plugin-try-catch-error-report [81]babel-plugin-try-catch-error-report: https://github.com/mcuking/babel-plugin-try-catch-error-report [82]babel: https:/// [83]如何监控网页崩溃?: https:///entry/5be158116fb9a049c6434f4a [84]sentry-webpack-plugin: https://github.com/getsentry/sentry-webpack-plugin [85]【Android】WebView 的 input 上传照片的兼容问题: https:///post/5a322cdef265da43176a2913 |
|
来自: men_darling > 《待分类》