问题

今天公司运营要做拿之前的一个活动翻新重开一次,调整了分享文案,结果发出去之后,在自己这边是这样的:

但是在其他人看是这样的:

尝试解决

一开始以为是图片本身的问题,尝试过png 转 jpg, 减小图片质量来减少体积,减少图片尺寸等等方法,都还是没法让其他人可见。

然后尝试着把分享配置回滚为上周的,结果是正常的:

想着是不是上周的图片是好的。

就又把上周的图片搬过来用,可又不可见。

最终解决

用微信开发者工具来debug整个分享流程也都是没问题的。

这个时候有点懵了,Google了一下,发现有人说可能是分享文案导致的。

综合前面问题排查的情况,觉得很有可能,因为我们的分享文案里面是有现金红包这样的词汇的。

让运营同学调整了一下分享文案,再重试,可以了:

看来的确是分享文案的锅。后面又去找微信官方的说明,几番转折,找到了官方的一些说明:

微信外部链接内容管理规范

朋友圈管理常见问题

而在微信JS-SDK说明文档的分享接口部分,也做了一个说明:

请注意不要有诱导分享等违规行为,对于诱导分享行为将永久回收公众号接口权限,详细规则请查看:朋友圈管理常见问题

算是官方比较正式的说明了。

希望上述内容对大家能有帮助。

更新:2018/06/08

发现一个小的手段去规避这个问题,就是在敏感词之间填充一些字符,比如空格。这样微信就不会屏蔽图片了

参考链接:

微信 JS-SDK 分享图片不显示

动效对于页面状态的转移是很必要的,大部分时候的动效可以用css+js来实现,不过有些时候,因为产品上的需要,就需要使用按帧播放的动画来实现。其中比较常见的就是gif动图。

就比如我这边最近在项目里面用到的一个动图,原始图如下:

图片体积1.4MB,这么大的图片,肯定是不适合用来做动图。所以需要针对这张图做一些处理。

这里就要说到一个压缩gif图很专业的网站ezgif

经过裁剪,隔帧抽取,糢糊三步之后,最终的动图如下:

184kb

完成上述三步之后,出来的动图是184kb,基本上已经无法优化了。

不过如果网站的条件允许的话,还是有方法优化的。这就是gif to video

gif图本质上就是一个帧动画,里面存储了N张图片,然后按一定的顺序播放。它的存储是比较死板的,哪怕前后两帧的图片非常相似甚至是一模一样,它都会按两张图片来存储。在video里面就不一样了,视频格式会对所存储的帧画面做各种各样的优化。

还是以上面的动图为例,把最终效果的gif图转换为mp4格式:

转换后的视频体积是79kb,比gif图少了58.66%,压缩效果还是很明显的

问题

今天在重构之前写的一个小程序页面,在修改scroll-view组件之后,发现之前的scroll-into-view的跳转失效了

之前的页面是一直正常的,所以官方的关于scroll-view的一些tips是不会关联了

tip: 请勿在 scroll-view 中使用 textarea、map、canvas、video 组件
tip: scroll-into-view 的优先级高于 scroll-top
tip: 在滚动 scroll-view 时会阻止页面回弹,所以在 scroll-view 中滚动,是无法触发 onPullDownRefresh
tip: 若要使用下拉刷新,请使用页面的滚动,而不是 scroll-view ,这样也能通过点击顶部状态栏回到页面顶部

再仔细看官方的说明,发现有这么一句

使用竖向滚动时,需要给<scroll-view/>一个固定高度,通过 WXSS 设置 height。

当前我的页面的scroll-view是能滚动的,不过说不定问题可能出在这里,因为高度设定的100%

先尝试着手动把scroll-view改成一个比较小的高度,再试试调整scroll-into-view的值,发现可以了,那问题就出在height上了。

原因

在旧版本小程序里面navBar是系统自带的,设置height: 100%是能正确展示。

而新版本里面,在app.json里面配置了navigationStyle: 'custom',自己实现了一个自定义样式的导航栏。在调整样式的时候,会让内容区域margin-top: 128px;,这样就不会跟自定义的navBar冲突。

问题应该就出在这了。

scroll-view的父级只有margin-top,没有一个固定的高度,使得scroll-view的height: 100%失效,只要能重新计算出一个准确的高度,那问题就能完美解决。

解决

本来有想过调整scroll-view的父级,不过会是一个比较罗嗦的方法,所以想试试一些css里面的新技术:

scroll-view {
  height: calc(100vh - 128px);
}

基于calc()vh来动态计算scroll-view的高度

试了下效果,正确运行!

为了保证在各个设备和系统下能正确工作,查了一下caniuse:

vh

calc

可以看到这两个特性在iOS和Andriod下都是早早就支持了,可以安心使用的

总结

scroll-view需要一个明确的高度,这个高度的实现方式有两种:

  • 自身的css样式里面设置了明确的高度,vh, px, rem, em等等都可以
  • 自身的css样式里面height: 100%;同时直接父级有一个明确的高度

今天把最近一直在开发的小程序放安卓手机上测试一下,结果某个页面就一直报错:Uncaught TypeError: Converting circular structure to JSON

先说一下基本的环境:

系统:Android 6.0.1
手机:小米4
微信版本:6.6.6

小程序基于mpvue开发

在看到这个错误的时候,明白导致的原因应该是因为一个对象里面有循环引用,然后这个对象不幸的被JSON.stringify给调用了

比如下图这样的:

循环引用示例

可是这个有循环引用的对象在哪就不清楚了。

一开始想的是vue对象的data,因为小程序里面,jscore会把这个data stringify之后发送给webview来渲染页面。一直沿着这个思路在debug

而我出错的那个页面的data很简单

{
  list: [],
  currPage: 1,
  pageSize: 10,
  isEnd: false,
  isLoading: false
}

唯一可能出问题的地方也就list了,可是试了很多方法,都证明了list不是有循环引用的点。

无奈只好google看看大家的解决方案,然后在mpvue的github的issues里面发现有一个类似的错误的issue

虽然引发错误的原因不一样,不过最后最底下 @anchengjian的一个提示给了我另外一个方向:

除了JSON.stringify之外,console也可能导致类似的错误。

根据这个新的思路,从新读了一遍代码,发现了一句console.log(this),当初调试的时候,为了方便,把整个vue的实例给log了。注释掉这一行,重新编译,在安卓上预览,果然,一切正常了。

这个console.log在开发者工具和ios上都没问题,可到安卓上出了问题。可能是安卓上,微信的x5实现console.log的时候,先调用了JSON.stringify吧

这个问题不想深究了,不过被这种问题耽搁了半天又无益于技术进步,想想还是记录下来,方便后来者。

最近使用Nestjs来搭建梦支点的后台服务,头一回正式的尝试用NodeJs来做一次项目,很多后端的面向对象的概念,弄的有点窒息。看文档的几天,着实头大。

不过好在抗了过来,顺利进入项目开发阶段。

在执行npm run start:watch完成项目的开发之后,打算就按现有完成的功能先把后台上线了让大家使用,然后出了问题。

整个开发过程中,没有尝试过去编译和基于上线环境执行,真到这一步的时候就不行了。

执行npm run prestart:prod,基于tsc来编译项目,一切正常。

再执行命令npm run start:prod,基于编译完的代码来启动服务,失败了。错误大致如下:

npm run start:prod

> nest-typescript-starter@1.2.0 prestart:prod /path/to/project
> tsc


> nest-typescript-starter@1.2.0 start:prod /path/to/project
> node dist/main.js

[Nest] 41866   - 2018-1-28 14:17:30   [ExceptionHandler] Unexpected token import
/path/to/project/src/entites/exchange-orders.entity.ts:1
(function (exports, require, module, __filename, __dirname) { import {Entity, Column, PrimaryGeneratedColumn} from 'typeorm';
                                                              ^^^^^^

SyntaxError: Unexpected token import
    at createScript (vm.js:80:10)
    at Object.runInThisContext (vm.js:139:10)
    at Module._compile (module.js:607:28)
    at Object.Module._extensions..js (module.js:654:10)
    at Module.load (module.js:556:32)
    at tryModuleLoad (module.js:499:12)
    at Function.Module._load (module.js:491:3)
    at Module.require (module.js:587:17)

看错误,是nodejs识别不了import这个标识符,可是编译之后的代码没有import了。而且我这边的代码执行,是都执行的编译之后dist目录的,为啥会依赖了src目录下的文件,还报错了?

仔细看了一下dist目录里面的文件,都是相对路径引用相关模块,不存在会引用到src。

在网上用搜索一番之后,把问题定位到了typeorm上。在这个项目里面,按照官方在sql上的最佳实践,使用typeorm来管理数据。为了方便管理mysql连接的相关配置,就在项目根目录新建了一个ormconfig.json

{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "user",
  "password": "pwd",
  "database": "db",
  "entities": ["src/**/**.entity{.ts,.js}"],
  "synchronize": true
}

基于entities指定的通配符,就能实现entity的自动加载。

看来问题也就出现在这个配置上了,肯定是在production环境下,typeorm还是加载了src目录下的entity,这样自然就出了问题。

所以解决方法就是如何能让这个配置根据环境自动去调整entities的值,找到了typeorm上关于这个配置文件的文档, 里面说到可以以js文件的方式输出配置。

这样一来就可以动态了。

修改后的ormconfig.js如下:

const SOURCE_PATH = process.env.NODE_ENV === 'production' ? 'dist' : 'src';
module.exports = {
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "user",
  "password": "pwd",
  "database": "db",
  "entities": [`${SOURCE_PATH}/**/**.entity{.ts,.js}`],
  "synchronize": true
};

然后在package.json里面修改start:prod命令:

"start:prod": " NODE_ENV=production node dist/main.js"

重新执行代码,顺利通过!!

react-router下,基于Switch,可以实现针对未匹配的路由调用指定的component来展现

<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={NoMatch}/>
</Switch>

现在的需求是:在发现有未匹配的路由时,自动跳转到首页

如果把上面的代码改成:

<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={Home}/>
</Switch>

这个可以实现的时,未匹配的路由展现首页的内容,可是当前的url还是那个未被匹配的路由。这样的显示就会有点怪异。

针对这个问题,可以使用react-router里面的另外一个组件Redirect

基于Redirect新建如下组件:

const RouteFallback = (props) => {
    console.log('route fallback with location: ', props.location);
    return <Redirect to={{
        pathname: '/',
        from: props.location
    }} />
}

再重新调整Switch:

<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={RouteFallback}/>
</Switch>

这样就能实现针对非法路径的自动跳转首页的功能了

起因

今天上午把双十二活动相关的页面给上线了,这次的发版,涉及的内容点比较多,上线的时候,没仔细想,打了一个tag就直接在线上git pull,然后活动页挂了…

糟糕的一连串操作

我是用的pm2 list查看的项目的状态,发现状态变成errored才知道出了问题。

这时候一回想才意识到,有些模块没安装。

在本地执行tig -- package.json,快速的查找两个版本之间都安装过哪些模块,然后对应的在线上npm install。但是一直被webpakcuglifyjs-webpack-plugin干扰的没法正常安装。

一狠心,rm -rf webpack直接把webpack给移除了。再次执行安装,还是同样的错误,再找到node_modules/.bin/webpack,移除(后面才知道用npm rm/uninstall 才是更正确的做法)。

webpack的错误总算没了,然后安装又报403的错误,自有的npm服务verdaccio出错了。

这时候终于意识到,问题不是一会儿能解决的,时间来来回回也过了十分钟,赶紧回滚服务到稳定版不:

  1. git checkout v1.18.1,把代码回滚到之前稳定版本。
  2. 修改pm2的配置文件ecosystem.config.js,配置关闭watch模式,这样后面安装模块和分支切换不会干扰正在运行的服务
  3. pm2 restart来重启服务,顺利关闭watch模式,服务本身也没有报错。

回滚完成。

接下来先是登上内网服务器来修复npm服务的问题:

  1. ps aux | grep verdaccio找到verdaccio的进程号,kill掉
  2. verdaccio & 重启verdaccio
  3. 在线上尝试npm install一个模块,还是有问题
  4. 发现线上访问npm服务用的域名是之前废弃的
  5. 编辑~/.npmrc 调整相关配置
  6. 再次安装,成功通过

现在npm服务正常,开始来面对之前出现的webpack出错的问题。我这个是线上服务器,是用不上webpack的,在安装模块的时候,出现这样的问题,很不正常。

Google了一下,发现可以用npm install --only=production来让npm只安装package.json里面dependencies部分的模块。

编辑package.json,整理dependencies和devDependencies,使得对应的模块在对应的分类中。

执行npm install --only=production安装所有更新,之前查package.json的编辑历史那一步是多余的。

更新完毕之后,

  1. git checkout .去掉所有临时修改的文件。
  2. git checkout master回到master分支
  3. pm2 restart来重启服务
  4. pm2 listpm2 log来检测状态和日志

一切正常,完成上线。

后续操作

在本地,重新审查了一遍package.json,一个模块一个模块的查找,去掉代码里面已经没有使用的模块,划分清除相关模块归属。

编辑ecosystem.config.js,配置development环境里面开启watch模式,production环境禁用。

打tag。

整个操作算是完成。

总结

这次的发版算是一次当头棒喝,在尝试用NodeJs来做后端的前台服务这么久,一直顺风顺水,有点忘却应有的严谨。

后端代码的线上环境没法跟前端这样明确,很多时候一个git pull没法解决所有问题。

需要一个持续集成的好的工作流。

这次出问题的一个主要原因就是太长时间的一个大的任务分支,导致整个项目跨度太大,这样在合并主分支并上线的时候风险自然就大了。

下次上线,一个正确的更新应该类似这样:git pull;npm install --only=production;pm2 restart {server}

还是不打算把node_modules/目录的内容放到git仓库里,虽然这样可以保证模块的同步,可是会增加仓库的体积,也会对其他成员的开发环境造成干扰。

或许可以考虑用Docker来包裹服务,保证其代码和环境的一致性。

最近项目需要,决定入手React Native。

第一步要做的肯定就是能在本机顺利的把官网的Quick Start 跑出来.

先说一下本机的环境:

  • 系统: macOS Sierra 10.12.5
  • NodeJs: 8.1.0
  • Xcode: 8.3.3 (8E3004b)
  • react-native-cli: 2.0.1
  • react-native: 0.45.1

构建过程

官网是用的brew来安装node,但是我这边根据日常开发的需要,会在多个Node版本之间切换,所以我是用的nvm

接下来是watchmanreact-native-cli,都是按照教程来一步一步安装

brew install watchman
npm install -g react-native-cli

然后是初始化和启动项目

react-native init AwesomeProject
cd AwesomeProject
react-native run-ios

然后就出错了:“:CFBundleIdentifier”, Does Not Exist

debug过程

先是在github上面找到了一个同样错误的issue,发现有这个错误的人很多,给的解释也不少

端口占用

端口8081被别的程序占用了

lsof -n -i4TCP:8081
node    7601 CXI624   23u  IPv6 0xc6b249599e5f1169      0t0  TCP *:sunproxyadmin (LISTEN)
kill - 9 7601 

这里面的7601只是一种情况,具体看真实占用这个端口的是什么程序。

我这边可以确定不是,所以pass

重买一台mac解决了

这… 没钱,pass

react-native upgrade

更新react-native到最新版

我这刚刚下载的,肯定最新版没毛病

sudo

sudo react-native run-ios

这个我是不太想用的,因为用了之后,以后基本都得用sudo,比较麻烦,等确认没别的解决方式再考虑

尝试调整Xcode的配置:

  1. Go to File -> Project settings
  2. Click the Advanced button
  3. Select “Custom” and select “Relative to Workspace” in the pull down
  4. click done, done

按照上面的做了一下,然后直接基于Xcode来运行程序

结果又出了新的问题

double-conversion.build failed

然后是在这个issue下找到了问题的解决方式:

rm -r ~/.rncache
rm -r <your-project>/node_modules/react-native/third_party

不过最终的编译还是失败。

网络问题

最后在刚刚的那个issue里面找到了那么一段话:

The problem is when building react-native for iOS, the process tries to download some files (using curl) into /.rncache. Specifically, they are “boost1630.tar.gz, double-conversion-1.1.5.tar.gz folly-2016.09.26.00.tar.gz, glog-0.3.4.tar.gz”. The sources of them (at least some of them) are from AWS S3, which aren’t accessible in some regions (I’m from China). And that caused the problem.
By using a proxy server and setting the http_proxy and https_proxy environment variables, curl will go through the proxy and the build should succeed.
This is not a problem with React Native. This issue can be closed now.

是说的在build的时候,会从AWS S3里面下载一些资源,但是在咱们国内,很多时候是访问不到这些资源的,也自然就导致了一些文件的缺损。

作者在后面给的方式是给curl配置代理,具体看https://stackoverflow.com/questions/9445489/linux-curl-command-with-proxy

不过我这边用了另外一种方式,把我的蓝灯设置为代理全部流量,接着

rm -r ~/.rncache
rm -r <your-project>/node_modules/

把所有资源删掉,然后重新编译。

最后,成功编译通过!!

在PC时代,调用QQ, 微博之类的三方登录,对于前端来说是比较轻松的(如果不需要异步登录的话),而且体验也不错,QQ那边可以自动识别是否登录,对于用户只是一个确认和点击的流程。

但是到了现在,移动互联的时代,这样的三方登录完全都是APP那边前端的事情,Web前端无能为力。毕竟,让用户在一个小设备上的小网页里面输入他已经在登录状态的QQ的账号密码,是挺难为人也挺不现实的。

缘于产品庆庆同学的执着,偶然发现在QQ内置的浏览器里面进行QQ登录的话,是也可以跟PC那样自动识别到已登录状态的,这让事情有了一线曙光。不过这种便利也只能在QQ的内置浏览器里面

下面就来说说如何实现web网页在QQ内置浏览器里面的登录

识别QQ的内置浏览器

首先得识别当前所在的浏览器是不是QQ内置的。

QQ浏览器分两种情况,一个是独立的浏览器产品,一个是内置在QQ里面的webview。识别的方式,用userAgent。

先贴出对应平台对应分类的UA(使用v2ex提供的一个工具页面可以方便的打印UA )

安卓平台下的独立的QQ浏览器

Mozilla/5.0 (Linux; U; Android 4.4.4; zh-cn; HM 1S Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko)Version/4.0 Chrome/37.0.0.0 MQQBrowser/7.4 Mobile Safari/537.36

安卓平台下的内置的QQ浏览器

Mozilla/5.0 (Linux; Android 4.4.4; HM 1S Build/KTU84P; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/53.0.2785.49 Mobile MQQBrowser/6.2 TBS/043124 Safari/537.36 V1_AND_SQ_6.7.1_500_YYB_D QQ/6.7.1.3105 NetType/WIFI WebP/0.3.0 Pixel/720

IOS平台下的独立的QQ浏览器

Mozilla/5.0 (iPhone 5SGLOBAL; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 MQQBrowser/7.4 Mobile/14E304 Safari/8536.25 MttCustomUA/2 QBWebViewType/1

IOS平台下内置的QQ浏览器(TIM内)

Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Mobile/14E304 QQ/6.5.5.0 TIM/1.0.5.9 V1_IPH_SQ_6.5.5_1_TIM_D Pixel/640 Core/UIWebView NetType/4G QBWebViewType/1

这里因为测试设备有限,没法确切知道所有版本的QQ下的情况,只能等以后慢慢完善。

从上面的UA可以找到两个关键字QQMQQBrowser。本来两个关键字是分开的,独立浏览器里面是MQQBrowser,内置的是QQ,但是这在安卓的内置里面同时实现了。

所以判断方式需要绕一点:先判断MQQBrowser,如果存在,判断QQ是否存在,存在则是内置,不存在则是独立;如果不存在MQQBrowser,则判断QQ是否存在,存在是内置,不存在,就都不是。

不过我们这次的需求只需要判断内置,所以简单,判断QQ是否存在即可:navigator.userAgent.match(/QQ\/([\d.]+)/)

登录的实现

一开始是打算继续用PC时代用的那个登录SDK好实现一个优雅的异步免刷新的登录效果。在实际测试的时候,也的确是可以做到在登录页QQ自动识别状态只需要用户点击登录按钮确认,可是在登录回跳这块卡住了。

在PC,SDK是用window.open新开一个弹窗来实现登录和各种跳转,在新窗口的最终落地页和主窗口通信返回登录成功获取的AccessToken,然后关闭弹出窗口。

但是内置浏览器里面window.open的效果是替换当前网页展示,类似于location.href赋值跳转,这使得最终落地页一直联系不到主窗口,然后卡着了。

很自然的,就想到了用iframe,把window.open打开的地址放到iframe里面,不就可以实现两个window的通信了吗?说干就干,QQ登录界面顺利的展现了,点击登录,结果主界面跳了,跟之前一样,再次无法通信。看来QQ在登录成功之后是用window.top来跳转的。不明白QQ这样做是出于什么考虑的,安全性?可是登录成功的回调地址不是已经被限制了只能是对应域名下的了吗?唯一知道的就是,这个方法行不通……

所以,目前还是只能走以前的老路,通过页面跳转的方式来实现登录。主要的策略就是在QQ登录成功之后的回调页再跳回之前的发起登录的页面。

本来这个需求是由后端来实现,在回跳发起页之前,在cookie里面记录用户的登录信息,这样发起页就能知道用户登录成功了。

不过我这次的需求,有两个麻烦的地方:

  1. 后端目前还没有在移动站点配置登录功能
  2. 我需要知道QQ用户的OpenIDAccessToken

所以改为由后端配置好回调页路由,具体页面里面的跳转和信息记录由前端自行控制。

然后具体的实现步骤是:
1. 用户点击发起页上的QQ登录按钮
2. 前端在后端给的redirect_url里面加上一个search query字段redirect,方便后面回跳回来
3. 前端根据appIdredirect_url整合出跳转QQ登录的地址(https://graph.qq.com/oauth2.0/authorize? response_type=token&client_id={{appId}}&scope=all&display=mobile&redirect_uri={{回调地址}})并跳转
4. 在QQ登录页面,QQ识别到当前用户的登录状态,自动填充信息并添加一键登录按钮
5. 用户点击登录
6. QQ开始回跳到redirect_url并在redirect_url后附加hash query,这个query里面有 access_token expires_in
7. 这个回调页,js抓取token和token过期时间存储在localStorage,同时还在localStorage记录两个个临时的key: TempQQLoginSuccessTempQQLoginSuccessTime
8. 回调页识别search query里面的redirect字段,获取发起页地址并跳转过去。
9. 发起页初始化的时候,查看localStorage里面有没有token和expire_in对应的字段,如果没有则是未登录状态,执行一次退出操作(退出操作是尝试清空回调页记录的四个key,下文同理)。如果存在,则判断当前时间是否超过expire_in对应的时间,如果超过,则执行退出操作。没有超过,则此时用户是登录状态。
10. 确定用户登录之后,再判断回调页记录的两个临时字段是否存在。如果有,判断TempQQLoginSuccessTime存储的时间和当前时间的时间差是否有超过一定的阈值。如果没有超过则说明上一次的发起页有登录行为,可以提示登录成功,如果超过了,则直接显示登录后的状态即可。同时不管超过与否,清空这两个临时key。

以上,整个登录流程就通了,成功的获取了用户的accessToken。

后面再使用accssToken作为参数,以jsonp的方式访问QQ的接口https://graph.qq.com/oauth2.0/me?access_token={{accessToken}}就能顺利的获取到用户的openId。这里要注意的一点是,QQ的这个接口里面callback的函数名写死了是callback,所以在使用jsonp的时候,以Zepto为例,jsonpCallback需要设定为callback

关于这个项目,具体实现代码在这:https://github.com/shuizhongyueming/fe-qq-login,有兴趣的可以看看

这两天公司开始发力做APP的推广,很多活动页和移动端站点需要加上打开APP的功能,如果没安装则提示用户下载。

首先,基于隐私保护的目的,浏览器是不会提供接口给网页去获取系统上安装了的APP的列表

然后很多产品经理会在提需求的时候有一种洁癖,希望网页上那个可供用户点击的按钮,在已安装APP的时候是打开,在未安装的时候是下载

早先的做法

为了应对这样的需求,前端程序员们想出了一个很棒的方法,利用浏览器进驻后台之后会暂停js的执行这一特性,顺利实现了对是否安装了自己公司APP的探测。具体实现如下:

在用户点击或者进入页面时候,记录一个当前时间time1,然后设置一个300ms的timeout,在timeout的回调里面记录时间time2,接着就用location.href直接指向对应APP已经配置的url scheme。

如果time1time2的时间差超过400ms(考虑到setTimeout本身的误差)就认为安装了APP,不需要处理,否则则是没安装,提示下载或者自动下载。

因为,如果用户安装了对应的APP,那么会自动的跳转到APP的界面,浏览器这边的计时会被中断,等下次用户偶然再使用浏览器的时候,计时继续,会触发setTimeout的回调,不过这个时候time1time2的时间差就远大于300ms了。

如果用户没有安装对应APP就不会发生跳转,setTimeout也就自然在规定时间内运行回调了。

这种方法曾经是非常有效果的,不过现在会有些问题了。

首先是安卓下,有些浏览器,比如UC会在网页试图打开APP的时候阻断这个行为并弹出一个select弹窗,让用户选择是否打开,是否始终打开等等。

UC的select弹窗

但是这个弹窗并没有像alert那样阻塞js的执行,也就是说setTimeout会正常进行,在用户犹豫怎么选择的时候,300ms就过了,页面就自动跳转了下载页。

接着是IOS这边,到IOS10之后,如果已经安装了对应的APP,浏览器也会在网页尝试打开APP的时候弹出一个confirm窗,让用户选择是否打开对应的APP。

已安装:确认窗提示是否打开APP

如果没有安装对应APP,就会实现一个很丑陋的网页无法打开此链家的错误alert窗。

未安装:提示网址无效

而且这两个窗口也是没有阻断js脚本的执行的,setTimeout也是同样正常进行中。

尝试过把300ms的时间改成3000ms,去预想用户可以在3秒内在浏览器的选择窗内做好选择。可是仔细一想,这种预想又是非常不靠谱的,永远没法猜测用户的未知行为,而且3000ms的响应延迟也是非常不好的交互体验。

是时候去调整思路了

如果浏览器不希望我们知道设备上是否已经安装了对应的APP,那就不尝试去知晓。

目前我们的做法是:在用户通过点击或者别的行为触发了打开APP的策略的时候,首先是尝试直接访问url scheme,在这同时,展现一个弹窗,里面包含一个APP的介绍文案,然后底部是两个按钮,下载APP打开APP

下面是天猫的做法,很值得借鉴:

先尝试跳转
同时显示下载和打开

这样做的好处是:如果用户已经安装了APP,那么就直接跳转了,弹窗对用户不会有任何的影响;如果没有安装,或者用户安装了,但是因为不想跳转或者误操作没有跳转,则用户在关闭了浏览器的错误提示之后,看到弹窗,能大概理解之前的浏览器的行为是什么意思,同时可以再次选择打开或者下载APP。

同时,如果是进入页面立马就跳转的行为,最好能有一个次数限制,不然一个高频访问的页面,每进入一次都有这样一个弹窗,明摆着是想赶用户走了啊