├── .browserlistrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .prettierignore ├── CHANGELOG.md ├── README.md ├── babel.config.js ├── babel.plugin.js ├── demo ├── app.js └── index.html ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── core │ ├── download.ts │ ├── generate.ts │ ├── launch.ts │ ├── launchStrategy.ts │ ├── sdkLaunch.ts │ └── targetApp.ts ├── index.ts └── libs │ ├── config.ts │ ├── evoke.ts │ ├── hostname.ts │ ├── platform.ts │ ├── sdk │ ├── index.ts │ ├── wuba.ts │ ├── wx.ts │ └── zz.ts │ └── utils.ts ├── todo.md ├── tsconfig.json ├── types └── globals.d.ts ├── v4 ├── img │ ├── demo-diff.png │ ├── files-diff.png │ ├── loginfo.png │ ├── mid-page.png │ ├── new-arch.png │ ├── size-diff.png │ └── sms-evoke.png └── log.md └── yarn.lock /.browserlistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 3 versions 3 | iOS >= 8 4 | Android >= 4 5 | Chrome >= 40 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | dist/ 3 | es/ 4 | lib/ 5 | docs/ 6 | types/ 7 | *.js 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'airbnb-base', 4 | 'prettier', 5 | 'prettier/@typescript-eslint', 6 | 'plugin:@typescript-eslint/recommended', 7 | ], 8 | env: { 9 | browser: true, 10 | node: true, 11 | es6: true, 12 | }, 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | sourceType: 'module', 16 | }, 17 | plugins: ['@typescript-eslint'], 18 | rules: { 19 | // 末尾不加分号,只有在有可能语法错误时才会加分号 20 | semi: 0, 21 | '@typescript-eslint/semi': 0, 22 | // 箭头函数需要有括号 (a) => {} 23 | 'arrow-parens': 0, 24 | 'no-use-before-define': 0, 25 | // 关闭不允许回调未定义的变量 26 | 'standard/no-callback-literal': 0, 27 | // 关闭副作用的 new 28 | 'no-new': 'off', 29 | // 关闭每行最大长度小于 80 30 | 'max-len': 0, 31 | // 函数括号前面不加空格 32 | // 关闭要求 require() 出现在顶层模块作用域中 33 | 'global-require': 0, 34 | // 关闭关闭类方法中必须使用this 35 | 'class-methods-use-this': 0, 36 | // 关闭禁止对原生对象或只读的全局对象进行赋值 37 | 'no-global-assign': 0, 38 | // 关闭禁止对关系运算符的左操作数使用否定操作符 39 | 'no-unsafe-negation': 0, 40 | // 关闭禁止使用 console 41 | 'no-console': 0, 42 | // 关闭禁止末尾空行 43 | 'eol-last': 0, 44 | // 关闭强制在注释中 // 或 /* 使用一致的空格 45 | 'spaced-comment': 0, 46 | // 关闭禁止对 function 的参数进行重新赋值 47 | 'no-param-reassign': 0, 48 | // 强制使用一致的换行符风格 (linebreak-style) 49 | 'linebreak-style': ['error', 'unix'], 50 | // 关闭全等 === 校验 51 | eqeqeq: 0, 52 | // 禁止使用拖尾逗号(即末尾不加逗号) 53 | 'comma-dangle': 0, 54 | // 关闭强制使用骆驼拼写法命名约定 55 | camelcase: 1, 56 | 'import/extensions': 0, 57 | 'import/no-unresolved': 0, 58 | 'consistent-return': 0, 59 | 'no-plusplus': 0, 60 | 'no-restricted-globals': 0, 61 | 'prefer-promise-reject-errors': 0, 62 | 'prefer-destructuring': 0, 63 | 'prefer-const': 0, 64 | 'no-unused-expressions': 1, 65 | 'space-before-function-paren': 1, 66 | '@typescript-eslint/no-empty-function': 0, 67 | 'no-shadow': 1, 68 | 'no-underscore-dangle': 0, 69 | 'no-bitwise': 0, 70 | 'import/prefer-default-export': 1, 71 | '@typescript-eslint/no-extra-semi': 1, 72 | 'no-multi-assign': 1, 73 | }, 74 | } 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | **/.DS_Store 4 | npm-debug.log 5 | v2/ 6 | dist/* 7 | es/* 8 | lib/* 9 | .idea/* 10 | docs/* 11 | build/* 12 | types/dist/ 13 | .eslintcache 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .gitignore 3 | npm-debug.log 4 | test/ 5 | build/ 6 | .npmignore 7 | v2/ 8 | .babelrc.js 9 | config/ 10 | src/ 11 | src-old/ 12 | tools/ 13 | docs/ 14 | demo/ 15 | package-lock.json 16 | .eslintcache 17 | .browserlistrc 18 | .eslintrc.js 19 | .prettierignore 20 | babel.config.js 21 | V4/ 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | .npmignore 17 | .browserlistrc 18 | LICENSE 19 | .eslintcache 20 | *.lock 21 | yarn-error.log 22 | .history 23 | .prettierrc 24 | public 25 | lib 26 | es 27 | build 28 | docs 29 | doc 30 | README.md 31 | CHANGELOG.md 32 | src-old 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | | 版本 | 主要更新功能 | 时间 | 作者 | 4 | | :------: | :------------------------------------------------- | :--------: | :----------: | 5 | | ## 4.0.0 | 重构项目架构,详情见 [v4](./v4/log.md) | 2021.08.20 | zhironghao | 6 | | ## 4.0.1 | 支持找靓机 ulink | 2021.09.28 | zhironghao | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | `call-app` 是一个基于 `typescript` 开发的通用的唤起 app 的 sdk, 支持唤起多个app, 兼容主流浏览器、webview,并支持用户自定义唤起配置。 4 | 5 | ## 快速上手 6 | 7 | ### Step1:下载源码或者发布 8 | 9 | 把源代码 clone 下来,或者下载 并且 build 之后发布到自己私有的 npm 仓库下 10 | 11 | ### Step2:引入 12 | 13 | ```js 14 | import CallApp from './call-app/src/index.ts' 15 | // 或者 16 | import CallApp from 'call-app' 17 | ``` 18 | 19 | 如果是通过外链 js 引入,那么可以使用 `window.CallApp` 得到 `CallApp` 类 20 | 21 | ### Step3:使用 22 | 23 | 实例化 `CallApp` 后,即可使用 `start` 和 `download` 方法. 24 | 25 | ```javascript 26 | // 实例化 27 | const callApp = new CallApp({ 28 | path: '', // 要唤起目标 app 的 path ,默认目标app是转转 29 | }) 30 | // 执行 唤起方法 31 | callApp.start() 32 | // 执行 下载 33 | callApp.download() 34 | ``` 35 | 或者 36 | ```javascript 37 | // 实例化 38 | const callApp = new CallApp() 39 | // 执行 唤起方法 40 | callApp.start({ 41 | path: '', // 要唤起目标 app 的 path ,默认目标app是转转 42 | }) 43 | // 执行 下载 44 | callApp.download() 45 | ``` 46 | #### 参数配置项 47 | 48 | - **customConfig** `Object` 用户定义配置项, 高阶配置,用法可参考下面示例 49 | - **schemeUrl** `String` scheme uri 地址 50 | - **downloadConfig** `Object` 下载配置,可选,不传则采用 landingPage 51 | - **ios** `String` app-store 链接 52 | - **android** `String` apk下载链接 53 | - **android_yyb** `String` 应用宝 下载链接 54 | - **universalLink** `String` universal-link链接,可选,ios 会优先采用 universal-link 55 | - **landingPage** `String` 唤起失败落地页,一般是下载中间页,优先级高于 `downloadConfig` 56 | 57 | - **path** `String` 调起 app 时,默认打开的页面,类型为 app 的统跳地址. 58 | - **channelId** `String` 渠道号,可选,当用户没有安装 app 时,默认下载的渠道号,安卓支持,iOS 不支持(选填) 59 | - **targetApp** `String` 调起的目标 app,优先级低于 path 的 prefix,其中:`zz`(代表转转app), `zlj`(代表找靓机app), `zzHunter`(代表采货侠app), `zzSeller`(代表转转卖家版、已废弃), `wxMini`(代表微信小程序,目前只支持转转wx小程序),默认为`zz` (选填) 60 | - **universal** `Boolean` 是否开启通用链接调起模式,默认为`true` 61 | - **download** `Boolean` 是否会自动跳转下载页面,默认为 `true` 62 | - **middleWareUrl** `String` 中转 url,如为空则默认跳转下载安装包或 appstore 63 | - **delay** `Number` 调起app失败后触发下载延迟, 默认 2500(毫秒) 64 | - **callStart** `Function` 开始执行调起时的hook 65 | - **callSuccess** `Function` 执行调起成功时的hook 66 | - **callFailed** `Function` 执行调起失败时的hook 67 | - **callDownload** `Function` 执行下载时的hook 68 | - **callError** `Function` 内部异常时的hook 69 | 70 | - **urlSearch** `Object` [已废弃] 指定页面调起方式,不推荐,直接设置 path 来跳转即可 71 | - **openType** `String` 页面类型,可选值为 `home首页(默认),detail详情页,order订单,mysell我卖出的,person个人中心,village小区,web页面` 72 | - **id** `String` 存放 id 或者 url,配合`openType` 的值来用 73 | 74 | - **onWechatReady** `Function` 微信端sdk初始化成功后的回调 75 | 76 | 77 | #### api 方法 78 | 79 | - **start** `Function` 唤起功能 80 | 81 | ```js 82 | // 挂在CallApp实例上的方法 83 | // options 可选 配置同上 84 | const callApp = new CallApp() 85 | callApp.start(options) 86 | ``` 87 | 88 | - **download** `Function` 下载功能 89 | 90 | ```js 91 | // 挂在CallApp实例上的方法 92 | // options 可选 配置同上 93 | const callApp = new CallApp() 94 | callApp.download(options) 95 | ``` 96 | 97 | ## 示例用法 98 | 99 | ##### 1. 初始化实例时配置 options,唤起 转转/找靓机 100 | 101 | ```javascript 102 | // 引入 lego 埋点 (使用callApp基础库 务必引入埋点上报) 103 | import { lego } from 'lego' 104 | // 唤起 转转 105 | const callApp = new CallApp({ 106 | path: 'zhuanzhuan://jump/shortVideo/videoHome/jump', // 带 prefix 107 | channelId: '', // 渠道id ,下载渠道包 108 | deeplinkId: '', // 后台配置项 109 | targetApp: 'zz', // zz 代表转转,zlj 代表找靓机,zzHunter 代表采货侠;默认 zz 110 | callStart() { 111 | lego.send({ 112 | actiontype: 'DOWNLOADAPP-START', 113 | pagetype: 'ZZDOWNLOADH5', 114 | backup: { channelId }, 115 | }) 116 | console.log('ZZDOWNLOADH5 callStart') 117 | }, 118 | callSuccess() { 119 | lego.send({ 120 | actiontype: 'DOWNLOADAPP-SUCCESS', 121 | pagetype: 'ZZDOWNLOADH5', 122 | backup: { channelId }, 123 | }) 124 | console.log('ZZDOWNLOADH5 callSuccess') 125 | }, 126 | callFailed() { 127 | lego.send({ 128 | actiontype: 'DOWNLOADAPP-FAILED', 129 | pagetype: 'ZZDOWNLOADH5', 130 | backup: { channelId }, 131 | }) 132 | console.log('ZZDOWNLOADH5 callFailed') 133 | }, 134 | callDownload() { 135 | lego.send({ 136 | actiontype: 'DOWNLOADAPP-DOWNLOAD', 137 | pagetype: 'ZZDOWNLOADH5', 138 | backup: { channelId }, 139 | }) 140 | console.log('ZZDOWNLOADH5 callDownload') 141 | } 142 | callError: () => { 143 | console.log('内部异常') 144 | }, 145 | }) 146 | 147 | // 执行唤起 148 | callApp.start() 149 | // 执行下载 150 | callApp.download() 151 | ``` 152 | 153 | ```javascript 154 | // 唤起 找靓机 155 | const callApp = new CallApp({ 156 | path: 'native_api?type=132', 157 | // path: 'zljgo://native_api?type=132' 158 | targetApp: 'zlj', 159 | universal: false, // 找靓机目前还不支持 universalLink 160 | callStart: () => { 161 | console.log('触发 开始唤起钩子') 162 | }, 163 | callSuccess: () => { 164 | console.log('触发 唤起成功钩子') 165 | }, 166 | callFailed: () => { 167 | console.log('触发 唤起失败钩子') 168 | }, 169 | callDownload: () => { 170 | console.log('触发 下载钩子') 171 | }, 172 | callError: () => { 173 | console.log('内部异常') 174 | }, 175 | }) 176 | 177 | // 执行唤起 178 | callApp.start() 179 | // 执行下载 180 | callApp.download() 181 | ``` 182 | 183 | ##### 2. 调用 api 时配置 options, 唤起 转转/找靓机 184 | 185 | 该用法为 实例化CallApp类一次,通过 api 来配置 options,进行执行。 186 | 此一般用于较复杂业务场景下,避免多次实例化而造成内存浪费。 187 | 188 | ```javascript 189 | // 实例化一次 190 | const callApp = new CallApp() 191 | // 在方法内进行参数配置 192 | 193 | // 唤起转转 194 | callApp.start({ 195 | path: 'jump/shortVideo/videoHome/jump', 196 | channelId: '', 197 | deeplinkId: '', 198 | }) 199 | 200 | // 唤起找靓机 201 | callApp.start({ 202 | path: 'native_api?type=132', 203 | // path: 'zljgo://native_api?type=132', 204 | targetApp: 'zlj', 205 | universal: false, // 找靓机、采货侠 目前还不支持 universalLink 206 | }) 207 | 208 | // 下载转转 209 | callApp.download({ 210 | targetApp: 'zz', 211 | channelId: '', 212 | deeplinkId: '' 213 | }) 214 | 215 | // 下载找靓机 216 | callApp.download({ 217 | targetApp: 'zlj', 218 | }) 219 | ``` 220 | 221 | ##### 3. 第三方配置(高阶) 222 | 223 | ⚠️ 注意: 224 | 225 | 3-1. 如果配置了 `customConfig` 参数,则非 hooks 参数(如 path,targetApp 等)的配置不再生效。 226 | 227 | 3-2 `landingPage` 配置参数优先级大于 `downloadConfig` 228 | 229 | 3-3 如果没有配置 `universalLink` 则 ios 端降级为 `schemeUrl` 230 | 231 | ```javascript 232 | // 唤起支付宝 233 | const callApp = new CallApp({ 234 | customConfig: { 235 | schemeUrl: 'alipay://platformapi/startapp?appId=20000056', // 支付宝转账 236 | landingPage: 'https://render.alipay.com/p/s/i', // 支付宝落地页(下载页) 237 | }, 238 | callStart: () => { 239 | console.log('触发 开始唤起钩子') 240 | }, 241 | callSuccess: () => { 242 | console.log('触发 唤起成功钩子') 243 | }, 244 | callFailed: () => { 245 | console.log('触发 唤起失败钩子') 246 | }, 247 | }) 248 | 249 | callApp.start() 250 | ``` 251 | 252 | ##### 4. 插件配置(高阶) 253 | 提供 use 方法, 方便用户插入 js 或者 自定义 CallApp 实例内部方法。并支持链式调用。 254 | 255 | 使用示例: 256 | ```javascript 257 | const callApp = new CallApp(options) 258 | 259 | callApp.use(function PluginA(app, optsA) { 260 | const old = app.start 261 | 262 | app.start = function() { 263 | // 264 | old.call(app) // 或者 old.call(app, options) 265 | } 266 | }).use(function PluginB(app, optsB) { 267 | // 268 | 269 | }) 270 | ``` 271 | 272 | ## 兼容性 😈 273 | 274 | ### H5 275 | #### ios: [iphoneXR] 276 | 277 | 278 | | 环境 | 下载 | scheme/ulink 唤起(已装 app) | 失败回调(已装 app) | 成功回调(已装 app) | 失败回调(未装 app) | 279 | | ------------- | ------------- | --------------------------- | ------------------ | ------------------------ | ------------------ | 280 | | safari | 支持 location | ulink 支持 | 不支持 | 支持 | ulink不支持 | 281 | | qq 浏览器 | 支持 location | ulink 支持 | 支持 | 支持 | ulink不支持 | 282 | | uc 浏览器 | 支持 location | ulink 支持 | 支持 | ulink支持, scheme 不支持 | ulink不支持 | 283 | | 百度浏览器 | 支持 location | ulink 支持, scheme 不支持 | 支持 | ulink支持 scheme 不支持 | ulink不支持 | 284 | | 夸克浏览器 | 支持 iFrame | 不支持 ulink,支持 scheme | 支持 | 支持 | 支持 | 285 | | 谷歌浏览器 | 支持 location | ulink 支持 | 支持 | 支持 | ulink不支持 | 286 | | sougou 浏览器 | 不支持 | ulink 支持 | 支持 | 支持 | ulink不支持 | 287 | | wx | 支持,应用宝 | ulink 支持, scheme 不支持 | 支持 | 支持 | ulink不支持 | 288 | | weibo | 不支持 | ulink 支持, scheme 不支持 | 支持 | ulink支持,scheme 不支持 | ulink不支持 | 289 | | qq | 支持, 应用宝 | ulink 支持 | 支持 | 支持 | 支持 | 290 | 291 | #### android: [huawei-p30] 292 | 293 | 294 | | 环境 | 下载 | scheme 唤起(已装 app) | 失败回调(已装 app) | 成功回调(已装 app) | 失败回调(未装 app) | 295 | | ------------- | ------------- | --------------------- | ------------------ | ------------------ | ------------------ | 296 | | qq 浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 297 | | uc 浏览器 | 支持 tagA | 支持 | 支持 | 支持 | 支持 | 298 | | 百度浏览器 | 支持 location | 不支持 | 支持 | 不支持 | 支持 | 299 | | 夸克浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 300 | | sougou 浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 301 | | 360 浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 302 | | 华为浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 303 | | wx | 支持,应用宝 | 不支持 | 支持 | 不支持 | 支持 | 304 | | weibo | 不支持 | 不支持 | 支持 | 不支持 | 支持 | 305 | | qq | 支持, 应用宝 | 支持 | 支持 | 支持 | 支持 | 306 | 307 | #### android: [mi-9] 308 | 309 | 310 | | 环境 | 下载 | scheme 唤起(已装 app) | 失败回调(已装 app) | 成功回调(已装 app) | 失败回调(未装 app) | 311 | | ---------- | ------------- | --------------------- | ------------------ | ------------------ | ------------------ | 312 | | qq 浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 313 | | uc 浏览器 | 支持 tagA | 支持 | 支持 | 支持 | 支持 | 314 | | 百度浏览器 | 支持 location | 不支持 | 支持 | 不支持 | 支持 | 315 | | 夸克浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 316 | | 360 浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 317 | | 小米浏览器 | 支持 location | 支持 | 支持 | 支持 | 支持 | 318 | | wx | 支持,应用宝 | 不支持 | 支持 | 不支持 | 支持 | 319 | | weibo | 不支持 | 不支持 | 支持 | 不支持 | 支持 | 320 | | qq | 支持,应用宝 | 支持 | 支持 | 支持 | 支持 | 321 | 322 | ### native sdk 323 | 324 | #### ios / android 325 | 326 | 327 | | | 转转 | 采货侠 | 找靓机 | 卖家版 | 58app | 微信 | 328 | | ----------------------- | ---- | ------ | ------ | ------ | ----- | ---- | 329 | | 目标app: 转转 | x | ✅ | ✅ | ✅ | ✅ | ✅ | 330 | | 目标app: 采货侠 | ✅ | x | x | x | x | x | 331 | | 目标app: 找靓机 | ✅ | x | x | x | x | x | 332 | | 目标app: 卖家版(已下架) | ✅ | x | x | x | x | x | 333 | 334 | 335 | --- 336 | 337 | 338 | ### 公开文章 339 | [唤起 App 在转转的实践](https://mp.weixin.qq.com/s?__biz=MzU0OTExNzYwNg==&mid=2247486327&idx=1&sn=a4ed8b1b012638a60bd4065a6e5ee309) 340 | [复杂场景下唤起App实践](https://mp.weixin.qq.com/s?__biz=MzU0OTExNzYwNg==&mid=2247492140&idx=1&sn=9857ecdf80285020dd90fd3d26fb717d) 341 | 342 | 343 | ### Bug or PR 344 | 345 | ### Feature 346 | 350 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const { replaceVersionPlugin } = require('./babel.plugin.js') 2 | const masterVersion = require('./package.json').version 3 | console.log(['masterVersion'], masterVersion) 4 | 5 | module.exports = (api) => { 6 | const { BABEL_MODULE, RUN_ENV, NODE_ENV } = process.env 7 | const useESModules = 8 | BABEL_MODULE !== 'commonjs' && RUN_ENV !== 'PRODUCTION' && NODE_ENV !== 'test' 9 | 10 | api && api.cache(false) 11 | 12 | return { 13 | presets: [ 14 | [ 15 | require.resolve('@babel/preset-env'), 16 | Object.assign( 17 | {}, 18 | useESModules 19 | ? { 20 | modules: false, 21 | useBuiltIns: false, 22 | } 23 | : { 24 | modules: 'commonjs', 25 | useBuiltIns: 'usage', 26 | corejs: 3, 27 | } 28 | ), 29 | ], 30 | ['@babel/typescript'], 31 | ], 32 | plugins: [ 33 | [ 34 | require.resolve('@babel/plugin-transform-runtime'), 35 | { 36 | useESModules, 37 | }, 38 | ], 39 | [ 40 | replaceVersionPlugin, 41 | { 42 | __VERSION__: masterVersion, 43 | }, 44 | ], 45 | ], 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /babel.plugin.js: -------------------------------------------------------------------------------- 1 | const replaceVersionPlugin = (babel) => { 2 | const t = babel.types 3 | return { 4 | name: 'replace-version', 5 | visitor: { 6 | Identifier(path, state) { 7 | let replacementDescriptor = 8 | state.opts.hasOwnProperty(path.node.name) && state.opts[path.node.name] 9 | 10 | if (!replacementDescriptor) return 11 | 12 | console.log('replacementDescriptor', replacementDescriptor, state.opts[path.node.name]) 13 | const type = typeof replacementDescriptor 14 | 15 | if (type === 'string' || type === 'boolean') { 16 | replacementDescriptor = { 17 | type: type, 18 | replacement: replacementDescriptor, 19 | } 20 | } 21 | 22 | const replacement = replacementDescriptor.replacement 23 | 24 | switch (replacementDescriptor.type) { 25 | case 'boolean': 26 | path.replaceWith(t.booleanLiteral(replacement)) 27 | break 28 | default: 29 | // treat as string 30 | const str = String(replacement) 31 | path.replaceWith(t.stringLiteral(str)) 32 | break 33 | } 34 | }, 35 | }, 36 | } 37 | } 38 | 39 | module.exports = { 40 | replaceVersionPlugin, 41 | } 42 | -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | import CallApp from '../src' 2 | 3 | window.__callAppDev__ = true 4 | 5 | try { 6 | // 第三方配置, 唤起支付宝 7 | // initCustomPage() 8 | // 检测 m 端是否支持 vue/es6 // 后面考虑 升级脚手架支持vue/react 9 | // throw Error('') 10 | initVuePage() 11 | } catch (error) { 12 | console.error(error) 13 | initMiniPage({}) 14 | } 15 | 16 | function initMiniPage(opts) { 17 | var app = document.querySelector('#app') 18 | var btn_open = document.createElement('button') 19 | btn_open.innerText = '唤起' 20 | var btn_download = document.createElement('button') 21 | btn_download.innerText = '下载' 22 | // 23 | var hooks = { 24 | callStart: function () { 25 | console.log('--- trigger --- hook:callStart ') 26 | }, 27 | callFailed: function () { 28 | console.log('--- trigger --- hook:callFailed ') 29 | }, 30 | callSuccess: function () { 31 | console.log('--- trigger --- hook:callSuccess ') 32 | }, 33 | callDownload: function () { 34 | console.log('--- trigger --- hook:callDownload ') 35 | }, 36 | callError: function () { 37 | console.log('--- trigger --- hook:callError ') 38 | }, 39 | } 40 | 41 | var callApp = (window.callApp = new CallApp(opts)) 42 | // 43 | btn_open.onclick = function () { 44 | console.log('window.CallApp', window.CallApp) 45 | callApp.start({ 46 | path: opts.path || 'zhuanzhuan://jump/shortVideo/videoHome/jump', // 兼容app所有统跳地址 47 | // path: 'zzhunter%3A%2F%2Fjump%2Fcore%2FmainPage%2Fjump', 48 | universal: false, 49 | channelId: opts.channelId || 'BM_GJ618XC', 50 | // targetApp: opts.targetApp || 'zz', 51 | wechatStyle: 1, // 1表示浮层右上角,2表示浮层按钮 52 | customConfig: opts.customConfig, 53 | // deeplinkId: getQuery('channelId') 54 | callStart: hooks.callStart, 55 | callSuccess: hooks.callSuccess, 56 | callFailed: hooks.callFailed, 57 | callDownload: hooks.callDownload, 58 | callError: hooks.callError, 59 | }) 60 | } 61 | 62 | btn_download.onclick = function () { 63 | callApp.download({ 64 | targetApp: 'zzHunter', 65 | }) 66 | } 67 | 68 | app.style.cssText = 'display: flex;flex-direction: column;margin-top: 100px' 69 | btn_download.style.cssText = 'margin-top: 50px;' 70 | 71 | app.appendChild(btn_open) 72 | app.appendChild(btn_download) 73 | } 74 | 75 | function initVuePage() { 76 | const { createApp, onMounted, reactive, watch } = Vue || window.Vue 77 | 78 | createApp({ 79 | template: ` 80 |
81 |
82 |
83 |

参数配置项

84 |
85 | path: 86 | 87 |
88 |
89 | channelId: 90 | 91 |
92 |
93 | deeplinkId: 94 | 95 |
96 |
97 | 目标APP: 98 | 104 |
105 |
106 | 是否支持universal-link: 107 | 111 |
112 |
113 |
114 |
115 | 116 |
117 | urlScheme: 118 | 119 | universalLink: 120 | 121 |
122 |
123 |
124 | 125 |
126 | downloadLink: 127 | 128 |
129 |
130 |
131 | `, 132 | setup() { 133 | let callApp 134 | 135 | const hooks = { 136 | callStart: function () { 137 | console.log('--- trigger --- hook:callStart ') 138 | }, 139 | callFailed: function () { 140 | console.log('--- trigger --- hook:callFailed ') 141 | }, 142 | callSuccess: function () { 143 | console.log('--- trigger --- hook:callSuccess ') 144 | }, 145 | callDownload: function () { 146 | console.log('--- trigger --- hook:callDownload ') 147 | }, 148 | callError: function () { 149 | console.log('--- trigger --- hook:callError ') 150 | }, 151 | } 152 | const { callStart, callSuccess, callFailed, callDownload, callError } = hooks 153 | 154 | onMounted(() => { 155 | console.log('demo onMounted') 156 | }) 157 | // 158 | var state = reactive({ 159 | targetApp: 'zlj', // 160 | path: '', 161 | channelId: 'BM_GJ618XC', 162 | deeplinkId: 'BM_GJ618XC', 163 | // 164 | urlScheme: '', 165 | downloadLink: '', 166 | universalLink: '', 167 | universal: 0, 168 | }) 169 | 170 | watch( 171 | function () { 172 | return state 173 | }, 174 | function (opts) { 175 | // 176 | let p = 177 | opts.targetApp == 'zlj' 178 | ? 'native_api?type=132&content=%7B%22extra_tab_index%22%3A%220%22%7D' 179 | : opts.targetApp == 'zz' 180 | ? 'jump/shortVideo/videoHome/jump' 181 | : opts.targetApp == 'zzHunter' 182 | ? 'jump/core/web/jump' 183 | : '' 184 | 185 | callApp = window.callApp = new CallApp({ 186 | path: 'zhuanzhuan://jump/shortVideo/videoHome/jump' || opts.path || p, // 兼容app所有统跳地址 187 | channelId: opts.channelId, 188 | targetApp: opts.targetApp, 189 | wechatStyle: 1, // 1表示浮层右上角 190 | deeplinkId: opts.deeplinkId, 191 | universal: +opts.universal, 192 | callStart, 193 | callSuccess, 194 | callFailed, 195 | callDownload, 196 | callError, 197 | }) 198 | 199 | console.log('callApp', callApp) 200 | // 201 | state.downloadLink = callApp.downloadLink || '' 202 | state.urlScheme = callApp.urlScheme || '' 203 | state.universalLink = callApp.universalLink || '' 204 | }, 205 | { 206 | deep: true, 207 | immediate: true, 208 | } 209 | ) 210 | 211 | // 212 | const openApp = function () { 213 | console.log( 214 | window.navigator.userAgent, 215 | '\n', 216 | window.navigator.appVersion, 217 | '\n', 218 | window.navigator.appName 219 | ) 220 | 221 | console.log('trigger start') 222 | callApp.start() 223 | state.downloadLink = callApp.downloadLink || '' 224 | state.urlScheme = callApp.urlScheme || '' 225 | state.universalLink = callApp.universalLink || '' 226 | } 227 | // 228 | const handleDownload = function () { 229 | console.log('trigger download') 230 | 231 | callApp.download() 232 | 233 | state.downloadLink = callApp.downloadLink || '' 234 | state.urlScheme = callApp.urlScheme || '' 235 | state.universalLink = callApp.universalLink || '' 236 | } 237 | 238 | return { 239 | openApp, 240 | handleDownload, 241 | state, 242 | } 243 | }, 244 | }).mount('#app') 245 | } 246 | 247 | function initCustomPage() { 248 | initMiniPage({ 249 | customConfig: { 250 | schemeUrl: 'alipay://platformapi/startapp?appId=20000056', 251 | landingPage: 'https://render.alipay.com/p/s/i', 252 | }, 253 | }) 254 | } 255 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 唤起App测试demo 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 97 |

唤起App测试Demo

98 | 99 | 100 |
101 | 102 | 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "call-app", 3 | "version": "1.0.1", 4 | "description": "通用的唤起app的库", 5 | "scripts": { 6 | "ts-check": "tsc --noEmit", 7 | "declare-type": "tsc --emitDeclarationOnly", 8 | "prettier": "prettier -c --write \"**/*\"", 9 | "lint-staged:js": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src", 10 | "lint-check": "npm run ts-check && npm run lint-staged:js", 11 | "cz": "git cz -a", 12 | "test": "jest --coverage", 13 | "lint": "commander-tools run lint", 14 | "fix": "commander-tools run lint --fix", 15 | "staged": "commander-tools run lint --staged", 16 | "staged-fix": "commander-tools run lint --staged --fix", 17 | "dev": "commander-tools run dev", 18 | "compile": "commander-tools run compile", 19 | "dist": "commander-tools run dist", 20 | "analyz": "commander-tools run dist --analyz", 21 | "build": "commander-tools run build & npm run declare-type", 22 | "pub": "commander-tools run pub", 23 | "pub-beta": "commander-tools run pub-beta", 24 | "unpub": "commander-tools run unpub", 25 | "doc": "commander-tools run doc", 26 | "build-doc": "commander-tools run build-doc", 27 | "doc-upload": "commander-tools run doc-upload" 28 | }, 29 | "main": "lib/index.js", 30 | "module": "es/index.js", 31 | "typings": "types/dist/index.d.ts", 32 | "keywords": [ 33 | "vue", 34 | "react", 35 | "npm", 36 | "import" 37 | ], 38 | "author": "huangjiaxing ", 39 | "license": "ISC", 40 | "config": { 41 | "commitizen": { 42 | "path": "zz-commander-tools/lib/config/commitizen.config" 43 | } 44 | }, 45 | "husky": { 46 | "hooks": { 47 | "pre-commit": "lint-staged", 48 | "commit-msg": "commander-tools run commitlint" 49 | } 50 | }, 51 | "lint-staged": { 52 | "**/*.{js,jsx,tsx,ts,less,md,json}": [ 53 | "prettier --write", 54 | "git add" 55 | ], 56 | "**/*.{js,jsx,ts,tsx}": "npm run lint-check" 57 | }, 58 | "devDependencies": { 59 | "@types/node": "^14.14.34", 60 | "@typescript-eslint/eslint-plugin": "^4.29.0", 61 | "@typescript-eslint/parser": "^4.29.0", 62 | "zz-commander-tools": "^1.1.2", 63 | "babel-eslint": "^10.1.0", 64 | "eslint": "^7.32.0", 65 | "eslint-config-airbnb-base": "^14.2.1", 66 | "eslint-config-prettier": "^6.11.0", 67 | "eslint-plugin-html": "^6.0.2", 68 | "eslint-plugin-import": "^2.23.4", 69 | "eslint-plugin-prettier": "^3.1.3", 70 | "eslint-plugin-unicorn": "^20.1.0", 71 | "husky": "^4.2.5", 72 | "lint-staged": "^11.1.2", 73 | "prettier": "^2.3.2", 74 | "typescript": "^3.9.5" 75 | }, 76 | "peerDependencies": {}, 77 | "dependencies": { 78 | "core-js": "^3.6.5" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 在ES5中有效的结尾逗号(对象,数组等) 3 | trailingComma: 'es5', 4 | // 不使用缩进符,而使用空格 5 | useTabs: false, 6 | // tab 用两个空格代替 7 | tabWidth: 2, 8 | // 仅在语法可能出现错误的时候才会添加分号 9 | semi: false, 10 | // 使用单引号 11 | singleQuote: true, 12 | // 在Vue文件中缩进脚本和样式标签。 13 | vueIndentScriptAndStyle: true, 14 | // 一行最多 100 字符 15 | printWidth: 100, 16 | // 对象的 key 仅在必要时用引号 17 | quoteProps: 'as-needed', 18 | // jsx 不使用单引号,而使用双引号 19 | jsxSingleQuote: false, 20 | // 大括号内的首尾需要空格 21 | bracketSpacing: true, 22 | // jsx 标签的反尖括号需要换行 23 | jsxBracketSameLine: false, 24 | // 箭头函数,只有一个参数的时候,也需要括号 25 | arrowParens: 'always', 26 | // 每个文件格式化的范围是文件的全部内容 27 | rangeStart: 0, 28 | rangeEnd: Infinity, 29 | // 不需要写文件开头的 @prettier 30 | requirePragma: false, 31 | // 不需要自动在文件开头插入 @prettier 32 | insertPragma: false, 33 | // 使用默认的折行标准 34 | proseWrap: 'preserve', 35 | // 根据显示样式决定 html 要不要折行 36 | htmlWhitespaceSensitivity: 'css', 37 | // 换行符使用 lf 38 | endOfLine: 'lf', 39 | } 40 | -------------------------------------------------------------------------------- /src/core/download.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 下载处理中心 3 | * download-config-center && generate download-url 4 | */ 5 | import { is58App, isAndroid, isIos, isQQ, isWechat } from '../libs/platform' 6 | import { DownloadConfig, CallAppOptions, TargetInfo } from '../index' 7 | import { logError } from '../libs/utils' 8 | import { AppFlags } from './targetApp' 9 | 10 | export const AppNames = { 11 | [AppFlags.ZZ]: 'zz', 12 | [AppFlags.ZZSeeker]: 'zlj', 13 | [AppFlags.ZZHunter]: 'zzHunter', 14 | [AppFlags.ZZSeller]: 'zzSeller', 15 | [AppFlags.WXMini]: 'wxMini', 16 | } 17 | 18 | export interface CallAppInstance { 19 | options: CallAppOptions 20 | start: () => void 21 | download: () => void 22 | targetInfo?: TargetInfo 23 | downloadLink?: string 24 | urlScheme?: string 25 | universalLink?: string 26 | intentLink?: string 27 | } 28 | 29 | // 目标app 各平台下载地址 配置 30 | export const allDownloadUrls = { 31 | [AppFlags.ZZ]: { 32 | // ios 商店 下载 33 | // ios: 'https://apps.apple.com/app/apple-store/id1002355194?pt=118679317&ct=923&mt=8', // 这种格式链接qq内无法触发下载 34 | ios: 'https://itunes.apple.com/cn/app/id1002355194?pt=118679317&ct=923&mt=8', 35 | // 安卓 市场 下载 36 | android: 'market://details?id=com.wuba.zhuanzhuan', 37 | // 腾讯 应用宝 下载 38 | android_yyb: 'https://sj.qq.com/myapp/detail.htm?apkName=com.wuba.zhuanzhuan', 39 | // download-api 下载 / 转转特殊的处理方式 安卓ios 统一下载 会更改下载配置文件 40 | api: 'https://app.zhuanzhuan.com/zz/redirect/download', 41 | }, 42 | [AppFlags.ZZSeeker]: { 43 | ios: 'https://itunes.apple.com/cn/app/id1060362098', 44 | android: 'market://details?id=com.huodao.hdphone', 45 | android_api: 'https://dlapk.zhaoliangji.com/zlj_zhaoliangji.apk', 46 | android_yyb: 'https://sj.qq.com/myapp/detail.htm?apkName=com.huodao.hdphone', 47 | }, 48 | [AppFlags.ZZHunter]: { 49 | ios: 'https://itunes.apple.com/cn/app/id1491125379', 50 | android: 'market://details?id=com.zhuanzhuan.hunter', 51 | android_api: 52 | 'https://app.zhuanzhuan.com/zzopredirect/ypofflinemart/downloadIosOrAndroid?channelId=923   ', 53 | android_yyb: 'https://sj.qq.com/myapp/detail.htm?apkName=com.zhuanzhuan.hunter', 54 | }, 55 | [AppFlags.ZZSeller]: { 56 | ios: '', 57 | android: '', 58 | android_api: '', 59 | android_yyb: '', 60 | }, 61 | // 唤起微信小程序 目前走 native-sdk 62 | [AppFlags.WXMini]: { 63 | ios: 'https://itunes.apple.com/cn/app/id414478124', 64 | android: 'market://details?id=com.tencent.mm', 65 | android_api: 'https://dldir1.qq.com/weixin/android/weixin8011android1980.apk', 66 | android_yyb: 'https://sj.qq.com/myapp/detail.htm?apkName=com.tencent.mm', 67 | }, 68 | } 69 | 70 | // 构造 下载链接 (不同平台环境 需要兼容处理) 71 | export const generateDownloadUrl = (instance: CallAppInstance): string => { 72 | // 第三方配置 73 | const { 74 | options: { customConfig }, 75 | } = instance 76 | 77 | if (customConfig) { 78 | const { downloadConfig, landingPage } = customConfig 79 | if (landingPage) return landingPage || '' 80 | if (downloadConfig) { 81 | if (isIos) { 82 | return downloadConfig?.ios || '' 83 | } 84 | if (isWechat && isAndroid) { 85 | return downloadConfig?.android_yyb || '' 86 | } 87 | return downloadConfig?.android || '' 88 | } 89 | return '' 90 | } 91 | 92 | // 93 | const { options, targetInfo: { downloadConfig, flag } = {} } = instance 94 | const { channelId, middleWareUrl, download, deeplinkId } = options 95 | 96 | if (!download || flag === undefined) return '' 97 | 98 | let downloadUrl: string | undefined = '' 99 | // 下载配置 100 | // (目前 h5 环境 只考虑 zz、zlj、zzHunter) 101 | if (flag & AppFlags.ZZ) { 102 | if ((is58App && isIos) || ((isQQ || isWechat) && isIos)) { 103 | // plat 如果 58 + ios || wx + ios || qq + ios, 走 苹果商店 , downloadConfig[ios] 104 | downloadUrl = downloadConfig?.ios || '' 105 | } else if ((isQQ || isWechat) && isAndroid) { 106 | // plat 如果 wx + android || qq + android, 走应用宝, downloadConfig[android_yyb] 107 | downloadUrl = downloadConfig?.android_yyb 108 | } else { 109 | // 其他 走 download-api 下载 channelId deeplinkId, 110 | // channelId 下载来源/渠道, deeplinkId App 后台配置默认打开页 111 | // wx 特殊处理 deepLinkId 112 | const wechat = isWechat ? '#mp.weixin.qq.com' : '' 113 | const deeplink = deeplinkId ? `&deeplinkId=${deeplinkId}${wechat}` : '' 114 | 115 | downloadUrl = `${downloadConfig?.api}?channelId=${channelId}${deeplink}` 116 | } 117 | } else if (flag & AppFlags.NoZZ) { 118 | if (isIos) { 119 | downloadUrl = downloadConfig?.ios 120 | } else if (isWechat && isAndroid) { 121 | downloadUrl = downloadConfig?.android_yyb 122 | } else { 123 | downloadUrl = downloadConfig?.android_api || downloadConfig?.android 124 | } 125 | } else { 126 | // 不存在 name 127 | logError(`generate downloadUrl error`) 128 | } 129 | 130 | return middleWareUrl || downloadUrl || '' 131 | } 132 | 133 | // 根据目标app 获取下载链接 配置 134 | export const getDownloadConfig = (name: AppFlags): DownloadConfig => { 135 | // 根据需要唤起的 目标 app ,获取 downloadUrl 136 | return allDownloadUrls[name] 137 | } 138 | -------------------------------------------------------------------------------- /src/core/generate.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * uri 生成 处理中心 3 | * generate uri center (include generate url-scheme && generate universal-link && generate Intent uri) 4 | */ 5 | import { AppFlags, handlePath2appName } from './targetApp' 6 | import { CallAppInstance } from '../index' 7 | import { logError } from '../libs/utils' 8 | import { genWXminiJumpPath } from '../libs/sdk' 9 | 10 | export interface Intent { 11 | package: string 12 | scheme: string 13 | action?: string 14 | category?: string 15 | component?: string 16 | } 17 | 18 | // 生成 scheme 链接 19 | export const generateScheme = (instance: CallAppInstance): string => { 20 | // 生成 path || urlSearch || targetApp 21 | const { options, targetInfo } = instance 22 | let { path, urlSearch } = options 23 | 24 | path = path || urlSearch || '' 25 | // new Regexp(zzInnerSchemeReg).test(path) 26 | // 检验 path 中是否有 scheme-prefix // 旧版本逻辑迁移 27 | 28 | // todo: 兼容逻辑, path 中是否 [https?://] - prefix, 唤起对应目标app的path页面 29 | // 需要根据各app统跳协议规范 帮业务拼接好 scheme-uri 30 | const { appName } = handlePath2appName(path) 31 | 32 | let uri = appName ? path : `${targetInfo?.schemePrefix}//${path}` 33 | 34 | if (targetInfo && targetInfo.flag & AppFlags.WXMini) { 35 | uri = appName ? path : genWXminiJumpPath(path) 36 | } 37 | 38 | return uri 39 | } 40 | 41 | // universal-link-host 42 | const universalLinkHost = '' 43 | 44 | // 生成 universalLink 链接 45 | export const generateUniversalLink = (instance: CallAppInstance) => { 46 | const { 47 | targetInfo, 48 | options: { universal, channelId }, 49 | urlScheme = '', 50 | } = instance 51 | 52 | if (!universal) return '' 53 | 54 | const host = universalLinkHost 55 | const path = targetInfo?.universalPath 56 | const channel = channelId ? `&channelId=${channelId}` : '' 57 | 58 | let app = '&app=zz' 59 | if (targetInfo) { 60 | if (targetInfo.flag & AppFlags.ZZSeeker) { 61 | app = '&app=zlj' 62 | } else if (targetInfo.flag & AppFlags.ZZHunter) { 63 | app = '&app=hunter' 64 | } 65 | } 66 | 67 | const universalLink = `https://${host}/${path}/index.html?path=${encodeURIComponent( 68 | urlScheme 69 | )}${channel}${app}` 70 | 71 | return universalLink 72 | } 73 | 74 | // 生成 appLinks Intent 链接 // 目前客户端app 都还不支持该协议 75 | export const generateIntent = (instance: CallAppInstance): string => { 76 | const { options, downloadLink } = instance 77 | const { intent, intentParams } = options 78 | 79 | if (intent && !intentParams) { 80 | logError(`options.intentParams is not found, please check`) 81 | return '' 82 | } 83 | 84 | if (!intent || !intentParams) return '' 85 | 86 | const keys = Object.keys(intentParams) as Array 87 | const intentParam = keys.map((key) => `${key}=${intentParams[key]};`).join('') 88 | 89 | const intentTail = `#Intent;${intentParam}S.browser_fallback_url=${encodeURIComponent( 90 | downloadLink || '' 91 | )};end;` 92 | 93 | let urlPath = generateScheme(instance) 94 | 95 | urlPath = urlPath.slice(urlPath.indexOf('//') + 2) 96 | 97 | return `intent://${urlPath}${intentTail}` 98 | } 99 | -------------------------------------------------------------------------------- /src/core/launch.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * h5 唤起功能处理 (入口) 3 | * launch 处理中心, 根据不同运行时环境和目标app, 调用对应的 uri 和 evoke 方法 4 | */ 5 | 6 | import { isAndroid, isIos } from '../libs/platform' 7 | import { CallAppInstance } from '../index' 8 | import { logInfo } from '../libs/utils' 9 | import { getDefaultIosPlatRegList, getDefaultAndroidPlatRegList } from './launchStrategy' 10 | 11 | /** 12 | * 普通 url-scheme 唤起, 不同平台对应不同的 evoke 13 | * @param {Object} instance 14 | */ 15 | export const launch = (instance: CallAppInstance) => { 16 | if (isIos) { 17 | logInfo('isIos', isIos) 18 | const list = getDefaultIosPlatRegList(instance) 19 | const len = list.length 20 | for (let i = 0; i < len; i++) { 21 | const item = list[i] 22 | if (item.platReg()) { 23 | return item.handler(instance) 24 | } 25 | } 26 | } else if (isAndroid) { 27 | // 28 | logInfo('isAndroid', isAndroid) 29 | const list = getDefaultAndroidPlatRegList(instance) 30 | const len = list.length 31 | for (let i = 0; i < len; i++) { 32 | const item = list[i] 33 | if (item.platReg()) { 34 | return item.handler() 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/core/launchStrategy.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isQQ, 3 | isWeibo, 4 | isQzone, 5 | isAndroid, 6 | isIos, 7 | isQQBrowser, 8 | isBaidu, 9 | isOriginalChrome, 10 | isWechat, 11 | isQuark, 12 | isLow9Ios, 13 | isLow7WX, 14 | // isThan12Ios, 15 | } from '../libs/platform' 16 | import { evokeByTagA, evokeByIFrame, evokeByLocation, checkOpen } from '../libs/evoke' 17 | import { CallAppInstance } from '../index' 18 | import { logInfo, showMask } from '../libs/utils' 19 | 20 | let tempIosPlatRegList: any = null 21 | // 获取方法 22 | export const getIosPlatRegList = (ctx: CallAppInstance) => 23 | tempIosPlatRegList || (tempIosPlatRegList = getDefaultIosPlatRegList(ctx)) 24 | 25 | // 扩展方法 26 | export const addIosPlatReg = (ctx: CallAppInstance, item: Record) => { 27 | if (item) { 28 | const list = getDefaultIosPlatRegList(ctx) 29 | list.splice(-1, 0, item as any) 30 | tempIosPlatRegList = [...list] 31 | } 32 | return tempIosPlatRegList 33 | } 34 | 35 | export const getDefaultIosPlatRegList = (ctx: CallAppInstance) => { 36 | const { options, urlScheme: schemeURL, universalLink } = ctx 37 | const { 38 | universal = false, 39 | callFailed = () => {}, 40 | callSuccess = () => {}, 41 | callError = () => {}, 42 | delay = 2500, 43 | } = options 44 | 45 | const handleCheck = (delay = 2500) => 46 | checkOpen( 47 | () => { 48 | callFailed() 49 | ctx.download() 50 | }, 51 | callSuccess, 52 | callError, 53 | delay 54 | ) 55 | 56 | return [ 57 | { 58 | name: 'wxSub', 59 | platReg: () => isWechat && isLow7WX, 60 | handler: (instance: CallAppInstance) => { 61 | console.log(instance) 62 | 63 | logInfo('isIos - isWeibo || isWechat < 7.0.5', isIos && isWechat && isLow7WX) 64 | showMask() 65 | callFailed() 66 | }, 67 | }, 68 | { 69 | name: 'low9', 70 | platReg: () => isLow9Ios, 71 | handler: () => { 72 | handleCheck(3000) 73 | schemeURL && evokeByIFrame(schemeURL) 74 | }, 75 | }, 76 | { 77 | name: 'bd', 78 | platReg: () => !universal && isBaidu, 79 | handler: () => { 80 | handleCheck(3000) 81 | showMask() 82 | }, 83 | }, 84 | { 85 | name: 'weibo', 86 | platReg: () => !universal && (isWeibo || isWechat), 87 | handler: () => { 88 | showMask() 89 | callFailed() 90 | }, 91 | }, 92 | { 93 | name: 'qq', 94 | platReg: () => !universal || isQQ || isQQBrowser || isQzone, 95 | handler: () => { 96 | handleCheck(3000) 97 | schemeURL && evokeByTagA(schemeURL) 98 | }, 99 | }, 100 | { 101 | name: 'quark', 102 | platReg: () => isQuark, 103 | handler: () => { 104 | handleCheck(3000) 105 | schemeURL && evokeByTagA(schemeURL) 106 | }, 107 | }, 108 | { 109 | name: 'ul', 110 | platReg: () => isIos, 111 | handler: () => { 112 | handleCheck(delay) 113 | universalLink && evokeByLocation(universalLink) 114 | }, 115 | }, 116 | ] 117 | } 118 | 119 | export const getDefaultAndroidPlatRegList = (ctx: CallAppInstance) => { 120 | const { options, urlScheme: schemeURL, intentLink } = ctx 121 | const { 122 | intent = false, 123 | callFailed = () => {}, 124 | callSuccess = () => {}, 125 | callError = () => {}, 126 | delay = 2500, 127 | } = options 128 | 129 | const handleCheck = (delay = 2500) => 130 | checkOpen( 131 | () => { 132 | callFailed() 133 | ctx.download() 134 | }, 135 | callSuccess, 136 | callError, 137 | delay 138 | ) 139 | 140 | return [ 141 | { 142 | name: 'intent', 143 | platReg: () => isOriginalChrome && intent, 144 | handler: () => { 145 | handleCheck(delay) 146 | // app-links 无法处理 失败回调, 原因同 universal-link 147 | intentLink && evokeByLocation(intentLink) 148 | }, 149 | }, 150 | { 151 | name: 'chrome', 152 | platReg: () => isOriginalChrome, 153 | handler: () => { 154 | handleCheck(delay) 155 | // app-links 无法处理 失败回调, 原因同 universal-link 156 | schemeURL && evokeByLocation(schemeURL) 157 | }, 158 | }, 159 | { 160 | name: 'wx', 161 | platReg: () => isWechat || isBaidu || isWeibo || isQzone, 162 | handler: () => { 163 | // 不支持 scheme, 显示遮罩 请在浏览器打开 164 | showMask() 165 | callFailed() 166 | }, 167 | }, 168 | { 169 | name: 'android', 170 | platReg: () => isAndroid, 171 | handler: () => { 172 | handleCheck(delay) 173 | schemeURL && evokeByLocation(schemeURL) 174 | }, 175 | }, 176 | ] 177 | } 178 | // 179 | let tempAndroidPlatRegList: any = null 180 | // 获取方法 181 | export const getAndroidPlatRegList = (ctx: CallAppInstance) => 182 | tempAndroidPlatRegList || (tempAndroidPlatRegList = getDefaultAndroidPlatRegList(ctx)) 183 | -------------------------------------------------------------------------------- /src/core/sdkLaunch.ts: -------------------------------------------------------------------------------- 1 | import { isZZ, isZZHunter, isZZSeller, isZZSeeker, isWechat } from '../libs/platform' 2 | import { CallAppInstance } from '../index' 3 | import { openZZInWX, openZZInnerApp } from '../libs/sdk/index' 4 | import { logError, logInfo } from '../libs/utils' 5 | import { AppFlags } from './targetApp' 6 | 7 | /** 8 | * native-sdk 方式 唤起, 根据不同运行时环境和目标app, 加载对应的 sdk 9 | * @param {Object} instance 10 | */ 11 | export const sdkLaunch = async (instance: CallAppInstance) => { 12 | const { options, targetInfo } = instance 13 | const { callFailed = () => {}, callError = () => {} } = options 14 | if (!targetInfo) return logError(`please check options.targetApp is legal, ${targetInfo}`) 15 | try { 16 | if (isWechat) { 17 | // wx-js-sdk 18 | logInfo('isWXSDK', isWechat) 19 | openZZInWX(instance) 20 | } else if (isZZ) { 21 | if (targetInfo.flag) { 22 | openZZInnerApp(instance, AppFlags.ZZ, targetInfo.flag) 23 | } 24 | } else if (isZZSeeker) { 25 | if (targetInfo.flag & AppFlags.ZZ) { 26 | openZZInnerApp(instance, AppFlags.ZZSeeker, AppFlags.ZZ) 27 | } 28 | if (targetInfo.flag & AppFlags.ZZSeeker) { 29 | openZZInnerApp(instance, AppFlags.ZZSeeker, AppFlags.ZZSeeker) 30 | } 31 | } else if (isZZHunter) { 32 | if (targetInfo.flag & AppFlags.ZZ) { 33 | openZZInnerApp(instance, AppFlags.ZZHunter, AppFlags.ZZ) 34 | } 35 | } else if (isZZSeller) { 36 | if (targetInfo.flag & AppFlags.ZZ) { 37 | openZZInnerApp(instance, AppFlags.ZZSeller, AppFlags.ZZ) 38 | } 39 | } else { 40 | callError() 41 | logError('your platform do not support, please contact developer') 42 | } 43 | } catch (error) { 44 | callFailed() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/core/targetApp.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * 需要唤起的 目标app 信息处理中心 4 | * 5 | */ 6 | 7 | import { getDownloadConfig, AppNames } from './download' 8 | import { CallAppOptions, DownloadConfig, TargetApp } from '../index' 9 | import { logError, logInfo } from '../libs/utils' 10 | 11 | export const enum AppFlags { 12 | ZZ = 1, 13 | ZZSeller = 1 << 1, 14 | ZZHunter = 1 << 2, 15 | ZZSeeker = 1 << 3, 16 | WXMini = 1 << 4, 17 | NoZZ = (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4), 18 | } 19 | 20 | export const appSchemePrefix = { 21 | [AppFlags.ZZ]: 'zhuanzhuan:', 22 | [AppFlags.ZZSeller]: 'zhuanzhuanseller:', 23 | [AppFlags.ZZHunter]: 'zzhunter:', 24 | [AppFlags.ZZSeeker]: 'zljgo:', 25 | } 26 | 27 | export const appUniversalPath = { 28 | [AppFlags.ZZ]: 'zhuanzhuan', 29 | [AppFlags.ZZSeller]: 'seller', 30 | [AppFlags.ZZHunter]: 'zzhunter', 31 | [AppFlags.ZZSeeker]: 'zhaoliangji', 32 | } 33 | 34 | // 获取 目标 app 类型 35 | export const getTargetInfo = (options: CallAppOptions) => { 36 | let { path, targetApp } = options 37 | // 从 path 解析 target-app 38 | if (!path && !targetApp) { 39 | logError( 40 | `options.path '${options.path}' or options.targetApp '${options.targetApp}' is Invalid, please check! \n` 41 | ) 42 | return 43 | } 44 | 45 | const { appName } = handlePath2appName(path || '') 46 | // 优先取 options.targetApp 47 | targetApp = (targetApp || appName || AppNames[AppFlags.ZZ]) as TargetApp 48 | 49 | if (!targetApp) { 50 | logError(`(targetApp || appName) '${targetApp}' is Invalid, please check! \n`) 51 | return 52 | } 53 | 54 | let name = AppNames[AppFlags.ZZ] 55 | let flag = AppFlags.ZZ 56 | let schemePrefix: string 57 | let downloadConfig: DownloadConfig 58 | let universalPath: string 59 | 60 | if (isZZ(targetApp)) { 61 | name = AppNames[AppFlags.ZZ] 62 | flag = AppFlags.ZZ 63 | } else if (isZZSeeker(targetApp)) { 64 | name = AppNames[AppFlags.ZZSeeker] 65 | flag = AppFlags.ZZSeeker 66 | } else if (isZZHunter(targetApp)) { 67 | name = AppNames[AppFlags.ZZHunter] 68 | flag = AppFlags.ZZHunter 69 | } else if (isZZSeller(targetApp)) { 70 | name = AppNames[AppFlags.ZZSeller] 71 | flag = AppFlags.ZZSeller 72 | } else if (isWXMini(targetApp)) { 73 | name = AppNames[AppFlags.WXMini] 74 | flag = AppFlags.WXMini 75 | 76 | return { 77 | flag, 78 | name, 79 | downloadConfig: getDownloadConfig(flag), 80 | schemePrefix: appSchemePrefix[AppFlags.ZZ], 81 | universalPath: appUniversalPath[AppFlags.ZZ], 82 | } 83 | } else { 84 | logError(`options.targetApp '${options.targetApp}' is Invalid, please check! \n`) 85 | } 86 | 87 | ;[schemePrefix, universalPath, downloadConfig] = [ 88 | appSchemePrefix[flag], 89 | appUniversalPath[flag], 90 | getDownloadConfig(flag), 91 | ] 92 | 93 | return { flag, name, schemePrefix, universalPath, downloadConfig } 94 | } 95 | 96 | const isZZ = (targetApp: string): boolean => /^(zhuanzhuan|zz)$/i.test(targetApp) 97 | 98 | const isZZSeller = (targetApp: string): boolean => /^zzSeller$/i.test(targetApp) 99 | 100 | const isZZHunter = (targetApp: string): boolean => /^zzHunter$/i.test(targetApp) 101 | 102 | const isZZSeeker = (targetApp: string): boolean => /^zlj$/i.test(targetApp) 103 | // 104 | const isWXMini = (targetApp: string): boolean => /^wxMini$/i.test(targetApp) 105 | // 从 options.path 中获取 target-app 106 | const isZZPrefixPath = (path: string): boolean => /^zhuanzhuan:/.test(path) 107 | 108 | const isZZSeekerPrefixPath = (path: string): boolean => /^zljgo:/i.test(path) 109 | 110 | const isZZSellerPrefixPath = (path: string): boolean => /^zhuanzhuanseller:/i.test(path) 111 | 112 | const isZZHunterPrefixPath = (path: string): boolean => /^zzhunter:/i.test(path) 113 | 114 | export const handlePath2appName = (path: string) => { 115 | let appName 116 | if (isZZSeekerPrefixPath(path)) appName = AppNames[AppFlags.ZZSeeker] 117 | if (isZZPrefixPath(path)) appName = AppNames[AppFlags.ZZ] 118 | if (isZZSellerPrefixPath(path)) appName = AppNames[AppFlags.ZZSeller] 119 | if (isZZHunterPrefixPath(path)) appName = AppNames[AppFlags.ZZHunter] 120 | 121 | logInfo('handlePath2appName', appName, path) 122 | return { appName } 123 | } 124 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * CallApp 类 4 | * 5 | */ 6 | import { generateDownloadUrl, AppNames } from './core/download' 7 | import { launch } from './core/launch' 8 | import { sdkLaunch } from './core/sdkLaunch' 9 | import { 10 | is58App, 11 | isAndroid, 12 | isIos, 13 | isQQBrowser, 14 | isQuark, 15 | isSougou, 16 | isUC, 17 | isWechat, 18 | isWeibo, 19 | isZZ, 20 | isZZHunter, 21 | isZZSeeker, 22 | isZZSeller, 23 | } from './libs/platform' 24 | import { AppFlags, getTargetInfo } from './core/targetApp' 25 | import { evokeByIFrame, evokeByLocation, evokeByTagA } from './libs/evoke' 26 | import { 27 | generateScheme, 28 | generateUniversalLink, 29 | generateIntent, 30 | Intent, 31 | // UrlSearch, 32 | } from './core/generate' 33 | import { copy, logError, logInfo, showMask } from './libs/utils' 34 | 35 | const version = __VERSION__ 36 | 37 | const defaultOptions: CallAppOptions = { 38 | path: '', // 唤起的页面 path 39 | targetApp: undefined, // 唤起的目标app 40 | universal: true, // 是否开启 universal-link 41 | intent: false, // 是否开启 安卓 app-links / 目前不支持 42 | download: true, // 是否支持下载 43 | delay: 2500, // 触发下载 延迟检测时间 44 | channelId: '923', // 下载渠道 id 45 | onWechatReady: () => {}, // 微信端初始化检测安装后的回调函数 46 | wechatStyle: 1, // 蒙层样式, 默认 微信吊起失败后,提示右上角打开 47 | deeplinkId: '', // deeplink 接口支持的id配置 48 | middleWareUrl: '', // 下载中间页 url 49 | // urlSearch: undefined, 50 | callFailed: () => {}, // 失败 hook 51 | callSuccess: () => {}, // 成功 hook 52 | callStart: () => {}, // 开始唤起 hook 53 | callDownload: () => {}, // 触发下载 hook 54 | callError: () => {}, // 触发异常 hook 55 | } 56 | 57 | export default class CallApp { 58 | options: CallAppOptions = {} 59 | 60 | targetInfo?: TargetInfo 61 | 62 | downloadLink?: string 63 | 64 | urlScheme?: string 65 | 66 | universalLink?: string 67 | 68 | intentLink?: string 69 | 70 | version?: string 71 | 72 | installedPlugins?: any 73 | 74 | // Create an instance of CallApp 75 | constructor(options?: CallAppOptions) { 76 | options && this.init(options) 77 | } 78 | 79 | init(options: CallAppOptions) { 80 | // 第三方 配置 81 | const { customConfig } = options 82 | const callFailed = options.callFailed 83 | this.version = version 84 | 85 | if (customConfig) { 86 | this.options = options 87 | this.downloadLink = generateDownloadUrl(this) 88 | this.urlScheme = customConfig.schemeUrl 89 | if (customConfig.universalLink) { 90 | this.options.universal = true 91 | this.universalLink = customConfig.universalLink 92 | } 93 | return 94 | } 95 | // 96 | this.options = { ...defaultOptions, ...options } 97 | // 配置提前预处理 减少后续逻辑判断处理代价 98 | // 待唤起目标 app 信息 99 | this.targetInfo = getTargetInfo(this.options) 100 | logInfo('targetInfo', this.targetInfo) 101 | // 根据平台 初始化 下载链接 102 | this.downloadLink = generateDownloadUrl(this) 103 | logInfo('downloadLink', this.downloadLink) 104 | // 初始化 deep-link url-scheme 105 | this.urlScheme = generateScheme(this) 106 | logInfo('urlScheme', this.urlScheme) 107 | // 初始化 app-links universalLink 108 | this.universalLink = generateUniversalLink(this) 109 | logInfo('universalLink', this.universalLink) 110 | // 初始化 app-links intentLink // 目前zz不支持 兼容性较差 111 | this.intentLink = generateIntent(this) 112 | // 装饰器 113 | this.options.callFailed = () => { 114 | this.urlScheme && copy(`1.0$$${this.urlScheme}`) 115 | callFailed && callFailed() 116 | } 117 | } 118 | 119 | /** 120 | * 触发唤起 121 | */ 122 | start(options?: CallAppOptions) { 123 | // 124 | options && this.init(options) 125 | 126 | const { callStart, customConfig } = this.options 127 | 128 | callStart && callStart(this) 129 | // 第三方 配置 130 | if (customConfig?.schemeUrl) return launch(this) 131 | 132 | const { targetInfo: { name: targetApp } = {} } = this 133 | 134 | if ( 135 | is58App || 136 | isZZ || 137 | isZZHunter || 138 | isZZSeller || 139 | isZZSeeker || 140 | (isIos && isWechat && targetApp === AppNames[AppFlags.ZZ]) 141 | // 2021.11.24 安卓微信内 通过微信白名单js-sdk唤起转转出现异常 // 改为显示mask 142 | ) { 143 | // by native-app js-sdk launch 144 | sdkLaunch(this) 145 | } else { 146 | // by deepLinks/appLinks launch 147 | launch(this) 148 | } 149 | } 150 | 151 | /** 152 | * 触发下载 153 | */ 154 | download(options?: CallAppOptions) { 155 | // 156 | options && this.init(options) 157 | 158 | const { callDownload, customConfig } = this.options 159 | 160 | callDownload && callDownload() 161 | 162 | logInfo('downloadLink', this.downloadLink) 163 | 164 | if (!customConfig) copy(`1.0$$${this.urlScheme}`) 165 | 166 | if (this.downloadLink) { 167 | // 个别浏览器 evoke方式 需要单独处理, 防止页面跳转到下载链接 展示异常 168 | if (isAndroid && isUC && isQQBrowser) { 169 | return evokeByTagA(this.downloadLink) 170 | } 171 | 172 | if (isIos && isQuark) { 173 | return evokeByIFrame(this.downloadLink) 174 | } 175 | 176 | if (isWeibo || (isIos && isSougou)) { 177 | return showMask() 178 | } 179 | 180 | return evokeByLocation(this.downloadLink) 181 | } 182 | 183 | logError('please check options.download is true') 184 | } 185 | 186 | /** 187 | * plugins 188 | */ 189 | use(plugin: Plugin, ...options: any[]): this { 190 | if (!this.installedPlugins) this.installedPlugins = new Set() 191 | 192 | const { installedPlugins } = this 193 | 194 | if (installedPlugins.has(plugin)) { 195 | logError(`Plugin has already been applied`) 196 | } else if (typeof plugin === 'function') { 197 | installedPlugins.add(plugin) 198 | plugin(this, ...options) 199 | } 200 | 201 | return this 202 | } 203 | } 204 | 205 | export * as utils from './libs/platform' 206 | 207 | export type Plugin = (app: CallAppInstance, ...options: any[]) => any 208 | export interface DownloadConfig { 209 | // 苹果市场 210 | ios: string 211 | // 安卓市场 212 | android: string 213 | android_api?: string 214 | // 应用宝 215 | android_yyb: string 216 | // api 217 | api?: string 218 | } 219 | 220 | export interface TargetInfo { 221 | flag: AppFlags 222 | name: string 223 | schemePrefix: string 224 | universalPath: string 225 | downloadConfig: DownloadConfig 226 | } 227 | // 目前是硬编码 需要优化 228 | export type TargetApp = 'zz' | 'zlj' | 'zzHunter' | 'wxMini' | undefined 229 | export interface CallAppOptions { 230 | // 唤起的页面 path 231 | path?: string 232 | // 唤起的目标app 233 | targetApp?: TargetApp 234 | // 是否开启 universal-link, 默认 true 235 | universal?: boolean 236 | // 是否开启 app-links, 默认 false 237 | intent?: boolean 238 | // 是否支持下载, 默认 true 239 | download?: boolean 240 | // 触发下载 延迟检测时间, 默认 2500 241 | delay?: number 242 | // 下载渠道 id 243 | channelId?: string | number 244 | // 微信端初始化检测安装后的回调函数 245 | onWechatReady?: (...arg: any[]) => void 246 | // 蒙层样式, 默认 微信吊起失败后,提示右上角打开, // 1表示浮层右上角,2表示浮层按钮, 默认 1 247 | wechatStyle?: number | string 248 | // deeplink 接口支持的id配置 249 | deeplinkId?: number | string 250 | // 下载中间页 url 251 | middleWareUrl?: string 252 | // 兼容 旧版本 scheme 生成规则 253 | // urlSearch?: UrlSearch 254 | // 失败 hook 255 | callFailed?: () => void 256 | // 成功 hook 257 | callSuccess?: () => void 258 | // 开始唤起 hook 259 | callStart?: (ctx: CallAppInstance) => void 260 | // 开始下载 hook 261 | callDownload?: () => void 262 | intentParams?: Intent 263 | callError?: () => void 264 | // 用户定义 配置项 265 | customConfig?: { 266 | schemeUrl: string // url-scheme 地址,必选 267 | downloadConfig?: { 268 | // 下载配置, 可选,不传则采用 landingPage 269 | ios: string // app-store 链接 270 | android: string // apk 下载链接 271 | android_yyb: string // 应用宝 下载链接 272 | } 273 | universalLink?: string // universal-link 地址,可选,ios 优先采用 universal-link 274 | landingPage?: string // 唤起失败落地页,一般是下载页面,可选,与 downloadConfig 二选一 275 | } 276 | } 277 | 278 | export interface CallAppInstance { 279 | options: CallAppOptions 280 | start: () => void 281 | download: () => void 282 | targetInfo?: TargetInfo 283 | downloadLink?: string 284 | urlScheme?: string 285 | universalLink?: string 286 | intentLink?: string 287 | } 288 | -------------------------------------------------------------------------------- /src/libs/config.ts: -------------------------------------------------------------------------------- 1 | import { getUrlParams, getCookie } from './utils' 2 | /** 3 | * 授权的公众号id 4 | * */ 5 | 6 | const getWxPublicId = (): string | undefined => { 7 | if (typeof window === 'undefined') { 8 | return 9 | } 10 | const query = getUrlParams() 11 | const config = window.nativeAdapterConfig || {} 12 | return query.wxPublicId || config.wxPublicId || query.__t || '24' 13 | } 14 | 15 | /** 16 | * 第三方依赖, 外链js 17 | * */ 18 | export const enum SDKNames { 19 | Z_SDK = 'Z_SDK', 20 | W_SDK = 'W_SDK', 21 | WX_JSTICKET = 'WX_JSTICKET', 22 | WX_JWEIXIN= 'WX_JWEIXIN', 23 | } 24 | 25 | export const dependencies = { 26 | [SDKNames.Z_SDK]: { 27 | link: '', 28 | }, 29 | [SDKNames.W_SDK]: { 30 | link: '', 31 | }, 32 | [SDKNames.WX_JSTICKET]: { 33 | link: '', 34 | }, 35 | [SDKNames.WX_JWEIXIN]: { 36 | link: '' 37 | } 38 | } 39 | 40 | /** 41 | * App, native相关信息 42 | * */ 43 | export interface AppInfo { 44 | SCHEMA: string 45 | ANDROID_PACKAGE_NAME: string 46 | ANDROID_MAINCLS: string 47 | } 48 | 49 | export const zAppInfo: AppInfo = { 50 | SCHEMA: '', // App跳转协议(Android & IOS) 51 | ANDROID_PACKAGE_NAME: '', // Android客户端包名 52 | ANDROID_MAINCLS: '', // Android客户端启动页主类名 53 | } 54 | 55 | /** 56 | * 微信公众号相关信息 57 | * */ 58 | export interface WXInfo { 59 | appID: string 60 | miniID: string 61 | } 62 | 63 | export const wxInfo: WXInfo = { 64 | appID: '', // app在微信绑定的appid 65 | miniID: '', // 小程序id 66 | } 67 | -------------------------------------------------------------------------------- /src/libs/evoke.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * web evoke methods && check evoke-status 4 | */ 5 | 6 | import { logError, logInfo } from './utils' 7 | 8 | type Hidden = 'hidden' | 'msHidden' | 'webkitHidden' 9 | type VisibilityChange = 'visibilitychange' | 'msvisibilitychange' | 'webkitvisibilitychange' 10 | 11 | let hidden: Hidden 12 | let visibilityChange: VisibilityChange 13 | let iframe: HTMLIFrameElement 14 | 15 | declare const document: Document 16 | 17 | function getSupportedProperty() { 18 | if (typeof document === 'undefined') return 19 | 20 | if (typeof document.hidden !== 'undefined') { 21 | // Opera 12.10 and Firefox 18 and later support 22 | hidden = 'hidden' 23 | visibilityChange = 'visibilitychange' 24 | } else if (typeof document.msHidden !== 'undefined') { 25 | hidden = 'msHidden' 26 | visibilityChange = 'msvisibilitychange' 27 | } else if (typeof document.webkitHidden !== 'undefined') { 28 | hidden = 'webkitHidden' 29 | visibilityChange = 'webkitvisibilitychange' 30 | } 31 | } 32 | 33 | getSupportedProperty() 34 | 35 | /** 36 | * 判断页面是否隐藏(进入后台) 37 | */ 38 | function isPageHidden(): boolean { 39 | if (typeof hidden === undefined) return false 40 | return document[hidden] as boolean 41 | } 42 | 43 | /** 44 | * 通过 top.location.href 跳转 45 | * @param {string}} [uri] - 需要打开的地址 46 | */ 47 | export function evokeByLocation(uri: string) { 48 | window.top.location.href = uri 49 | } 50 | 51 | /** 52 | * 通过 A 标签唤起 53 | * @param {string} uri - 需要打开的地址 54 | */ 55 | export function evokeByTagA(uri: string) { 56 | const tagA = document.createElement('a') 57 | 58 | tagA.setAttribute('href', uri) 59 | tagA.style.display = 'none' 60 | document.body?.append(tagA) 61 | 62 | tagA.click() 63 | } 64 | 65 | /** 66 | * 通过 iframe 唤起 67 | * @param {string} [uri] - 需要打开的地址 68 | */ 69 | export function evokeByIFrame(uri: string) { 70 | if (!iframe) { 71 | iframe = document.createElement('iframe') 72 | iframe.style.cssText = 'display:none;border:0;width:0;height:0;' 73 | document.body.append(iframe) 74 | } 75 | 76 | iframe.src = uri 77 | } 78 | 79 | /** 80 | * hack 检测是否唤端成功 81 | * 原理见: https://developer.mozilla.org/zh-CN/docs/Web/API/Page_Visibility_API 82 | * @param failure - 唤端失败回调函数 83 | * @param success - 唤端成功回调函数 84 | * @param error - 唤端异常函数 85 | * @param timeout - hack 失败检测延时 - 一般会触发下载 86 | */ 87 | 88 | export function checkOpen( 89 | failure: () => void, 90 | success: () => void, 91 | error: () => void, 92 | timeout = 2500 93 | ) { 94 | let haveChanged = false 95 | logInfo('trigger -- checkOpen') 96 | 97 | const pageChange = function (e: any) { 98 | haveChanged = true 99 | 100 | if (document?.hidden || e?.hidden || document?.visibilityState == 'hidden') { 101 | logInfo('checkOpen pagehide -- success') 102 | success() 103 | } else { 104 | logError('unknown error when check pagehide') 105 | error() 106 | } 107 | 108 | // window.addEventListener('pagehide', pageChange, false); 109 | document.removeEventListener(visibilityChange, pageChange, false) 110 | document.removeEventListener('baiduboxappvisibilitychange', pageChange, false) 111 | } 112 | 113 | const timer = setTimeout(() => { 114 | clearTimeout(timer) 115 | 116 | if (haveChanged) { 117 | return 118 | } 119 | 120 | // window.addEventListener('pagehide', pageChange, false); 121 | document.removeEventListener(visibilityChange, pageChange, false) 122 | document.removeEventListener('baiduboxappvisibilitychange', pageChange, false) 123 | 124 | logInfo('checkOpen timeout', timeout) 125 | logInfo('checkOpen isPageHidden', isPageHidden()) 126 | // 判断页面是否隐藏(进入后台) 127 | const pageHidden = isPageHidden() 128 | if (!pageHidden) { 129 | failure() 130 | logInfo('checkOpen hasFailed-failure') 131 | } else { 132 | logError('pageHidden: unknown error') 133 | error() 134 | } 135 | 136 | haveChanged = true 137 | }, timeout) 138 | 139 | // window.addEventListener('pagehide', pageChange, false); 140 | document.addEventListener(visibilityChange, pageChange, false) 141 | document.addEventListener('baiduboxappvisibilitychange', pageChange, false) 142 | } 143 | -------------------------------------------------------------------------------- /src/libs/hostname.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * domain/host 判断 4 | * 5 | * */ 6 | 7 | export const is58Host: boolean = false 8 | 9 | export const isZZHost: boolean = false 10 | -------------------------------------------------------------------------------- /src/libs/platform.ts: -------------------------------------------------------------------------------- 1 | import { logInfo } from './utils' 2 | 3 | // 获取平台类型 4 | const ua: string = (window.navigator && window.navigator.userAgent) || '' 5 | 6 | /** 7 | * 8 | * native-webview 相关 9 | * 10 | */ 11 | 12 | export const is58App: boolean = /wuba/i.test(ua) 13 | 14 | export const isZZ: boolean = /58zhuanzhuan/i.test(ua) 15 | 16 | export const isZZHunter: boolean = /zzHunter/i.test(ua) 17 | 18 | export const isZZSeller: boolean = /zhuanzhuanSeller/i.test(ua) 19 | 20 | export const isZZSeeker: boolean = /zhaoliangji-v2/i.test(ua) //zhaoliangji-v2 21 | 22 | export const isWechat: boolean = /micromessenger\/([\d.]+)/i.test(ua) 23 | 24 | export const isWeibo: boolean = /(weibo).*weibo__([\d.]+)/i.test(ua) 25 | 26 | export const isQQ: boolean = /qq\/([\d.]+)/i.test(ua) 27 | 28 | export const isQzone: boolean = /qzone\/.*_qz_([\d.]+)/i.test(ua) 29 | 30 | /** 31 | * 32 | * 操作系统 相关 33 | * 34 | */ 35 | export const isAndroid: boolean = /android/i.test(ua) 36 | 37 | export const isIos: boolean = /iphone|ipad|ipod/i.test(ua) 38 | 39 | /** 40 | * 41 | * browser 相关 42 | * 43 | */ 44 | 45 | export const isBaidu: boolean = /(baiduboxapp)\/([\d.]+)/i.test(ua) 46 | 47 | export const isQQBrowser: boolean = /(qqbrowser)\/([\d.]+)/i.test(ua) 48 | 49 | export const isUC: boolean = /ucBrowser\//i.test(ua) 50 | 51 | export const isQuark: boolean = /quark\//i.test(ua) 52 | 53 | export const isSougou: boolean = /sogouMobileBrowser\//i.test(ua) 54 | 55 | export const isHuaWei: boolean = /huaweiBrowser\//i.test(ua) 56 | 57 | export const isMi: boolean = /XiaoMi\/MiuiBrowser\//i.test(ua) 58 | // 安卓 chrome 浏览器,包含 原生chrome浏览器、三星自带浏览器、360浏览器以及早期国内厂商自带浏览器 59 | export const isOriginalChrome: boolean = 60 | /chrome\/[\d.]+ mobile safari\/[\d.]+/i.test(ua) && isAndroid && ua.indexOf('Version') < 0 61 | 62 | /** 63 | * 64 | * 版本号 相关 65 | * 66 | */ 67 | 68 | // 版本号比较 69 | export const semverCompare = (versionA: string, versionB: string): number => { 70 | // eslint-disable-next-line no-restricted-properties 71 | const { isNaN } = window 72 | const splitA = versionA.split('.') 73 | const splitB = versionB.split('.') 74 | 75 | for (let i = 0; i < 3; i++) { 76 | const snippetA = Number(splitA[i]) 77 | const snippetB = Number(splitB[i]) 78 | 79 | if (snippetA > snippetB) return 1 80 | if (snippetB > snippetA) return -1 81 | 82 | // e.g. '1.0.0-rc' -- Number('0-rc') = NaN 83 | if (!isNaN(snippetA) && isNaN(snippetB)) return 1 84 | if (isNaN(snippetA) && !isNaN(snippetB)) return -1 85 | } 86 | 87 | return 0 88 | } 89 | 90 | // 获取 ios 大版本号 91 | export const getIOSVersion = (): number | null => { 92 | const version = window.navigator.appVersion.match(/OS (\d+)_(\d+)_?(\d+)?/) 93 | 94 | if (version) return Number.parseInt(version[1], 10) 95 | 96 | return null 97 | } 98 | 99 | // 获取 微信 版本号 100 | export const getWeChatVersion = (): string | null => { 101 | const version = window.navigator.appVersion.match(/micromessenger\/(\d+\.\d+\.\d+)/i) 102 | 103 | if (version) return version[1] 104 | 105 | return null 106 | } 107 | 108 | const getLow9Ios = (): boolean => { 109 | const v = getIOSVersion() 110 | if (v) { 111 | return v < 9 112 | } 113 | return false 114 | } 115 | 116 | export const isLow9Ios: boolean = getLow9Ios() 117 | 118 | const getLow7WX = (): boolean => { 119 | const vv = getWeChatVersion() 120 | if (vv) { 121 | return semverCompare(vv, '7.0.5') < 0 122 | } 123 | return false 124 | } 125 | 126 | export const isLow7WX: boolean = getLow7WX() 127 | 128 | // IOS 版本号 129 | export const IOSVersion = (): string => { 130 | const str = window.navigator.userAgent.toLowerCase() 131 | let ver = '0.0.0' 132 | try { 133 | const m = str.match(/cpu iphone os (.*?) like mac os/) 134 | if (m) ver = m[1].replace(/_/g, '.') 135 | } catch (error) { 136 | logInfo('IOSVersion', error) 137 | } 138 | return ver 139 | } 140 | 141 | const getThanNumIos = (version: string): boolean => semverCompare(IOSVersion(), version) > 0 142 | 143 | export const isThan12Ios: boolean = getThanNumIos('12.3.0') 144 | -------------------------------------------------------------------------------- /src/libs/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export { openZZInWX } from './wx' 2 | export { openZZIn5 } from './wuba' 3 | export * from './zz' 4 | -------------------------------------------------------------------------------- /src/libs/sdk/wuba.ts: -------------------------------------------------------------------------------- 1 | import { CallAppInstance } from '../../index' 2 | import { AppInfo, dependencies } from '../config' 3 | import { checkOpen } from '../evoke' 4 | import { loadJSArr, logError, logInfo } from '../utils' 5 | 6 | export type App5 = { 7 | action?: { 8 | openApp: (...res: any[]) => void 9 | isInstallApp: (...res: any[]) => void 10 | } 11 | common?: { 12 | appVersion?: string 13 | } 14 | } 15 | 16 | export const load5SDK = (): Promise => 17 | new Promise((resolve) => { 18 | loadJSArr([dependencies.WB_SDK.link], () => { 19 | resolve(window.WBAPP) 20 | }) 21 | }) 22 | 23 | export const sdk5 = { 24 | isInstallApp: (app: App5, options = {}) => { 25 | return new Promise((resolve, reject) => { 26 | app?.action?.isInstallApp(options, (_: any) => { 27 | _ ? resolve({}) : reject({}) 28 | }) 29 | }) 30 | }, 31 | openApp(app: App5, options = {}) { 32 | return app?.action?.openApp(options) 33 | }, 34 | getVersion(app: App5) { 35 | return app?.common?.appVersion 36 | }, 37 | } 38 | /** 39 | * app内打开 40 | * @param instance 41 | * @param appInfo 42 | */ 43 | export const openZZIn5 = (instance: CallAppInstance, appInfo: AppInfo) => { 44 | logInfo('openZZIn5') 45 | 46 | const { 47 | options: { delay = 2500, callError = () => {}, callSuccess = () => {}, callFailed = () => {} }, 48 | urlScheme, 49 | } = instance 50 | 51 | if (!urlScheme) { 52 | logError(`scheme-uri is invalid`) 53 | return 54 | } 55 | 56 | // load sdk 57 | load5SDK() 58 | .then((app) => { 59 | // hack 检测 open状态 60 | const handleCheck = () => 61 | checkOpen( 62 | () => { 63 | callFailed() 64 | }, 65 | callSuccess, 66 | callError, 67 | delay 68 | ) 69 | 70 | // sdk 71 | sdk5.openApp(app, { 72 | urlschema: urlScheme || appInfo.SCHEMA, 73 | package: appInfo.ANDROID_PACKAGE_NAME, 74 | maincls: appInfo.ANDROID_MAINCLS, 75 | }) 76 | 77 | handleCheck() 78 | }) 79 | .catch((error) => { 80 | callFailed() 81 | logError(error) 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /src/libs/sdk/wx.ts: -------------------------------------------------------------------------------- 1 | import { CallAppInstance } from '../../index' 2 | import { wxInfo, dependencies, zAppInfo } from '../config' 3 | import { evokeByLocation } from '../evoke' 4 | import { isAndroid } from '../platform' 5 | import { loadJSArr, logError, logInfo } from '../utils' 6 | 7 | export interface WXJSTICKET { 8 | appId?: string 9 | timestamp?: string 10 | noncestr?: string 11 | signature?: string 12 | [key: string]: any 13 | } 14 | 15 | export const loadWXSDK = () => { 16 | const _ = Object.create(null) 17 | return new Promise((resolve) => { 18 | window.__json_jsticket = (resp: { respCode: number; respData: WXJSTICKET }) => { 19 | if (resp) { 20 | _.WX_JSTICKET = (resp.respCode == 0 && resp.respData) || {} 21 | } else { 22 | logError('load wx-sdk error') 23 | } 24 | } 25 | 26 | loadJSArr([dependencies.WX_JWEIXIN.link, dependencies.WX_JSTICKET.link], () => { 27 | resolve(_.WX_JSTICKET) 28 | }) 29 | }) 30 | } 31 | 32 | // 调用微信 sdk api 回调 33 | export const invokeInWX = ( 34 | name: string, 35 | options: Record, 36 | app: Record 37 | ) => { 38 | return new Promise((resolve, reject) => { 39 | app.invoke(name, options, (data: any) => { 40 | logInfo('invokeInWX', data) 41 | const { err_msg } = data 42 | const Regex = /(:ok)|(:yes)/g 43 | if (Regex.test(err_msg)) { 44 | resolve({ 45 | code: 0, 46 | }) 47 | } else { 48 | reject({ code: -1 }) 49 | } 50 | }) 51 | }) 52 | } 53 | 54 | export const openAppInWX = ( 55 | schemeURL: string, 56 | instance: CallAppInstance, 57 | app: Record 58 | ) => { 59 | const { options, downloadLink = '', universalLink = '' } = instance 60 | const { callFailed = () => {}, callSuccess = () => {} } = options 61 | const { appID } = wxInfo 62 | const parameter = schemeURL 63 | const extInfo = schemeURL 64 | 65 | const handleByuLink = (cb: () => void, delay = 2000) => { 66 | universalLink && evokeByLocation(universalLink) 67 | setTimeout(() => { 68 | cb() 69 | }, delay) 70 | } 71 | 72 | invokeInWX('launchApplication', { appID, parameter, extInfo }, app) 73 | .then((res) => { 74 | logInfo('launchApplication', res) 75 | callSuccess() 76 | }) 77 | .catch((err) => { 78 | // sdk 失败则降级采用 uLink 尝试唤起 79 | handleByuLink(() => { 80 | logError('launchApplication', err) 81 | callFailed() 82 | downloadLink && evokeByLocation(downloadLink) 83 | }) 84 | }) 85 | } 86 | 87 | export const openZZInWX = async (instance: CallAppInstance) => { 88 | const { options, urlScheme = '' } = instance 89 | const { callFailed = () => {}, onWechatReady = () => {} } = options 90 | // if(isAndroid){ 91 | // return evokeByLocation(downloadLink) 92 | // } 93 | try { 94 | const conf: WXJSTICKET = await loadWXSDK() 95 | const wxconfig = { 96 | debug: false, 97 | appId: conf.appId, 98 | timestamp: conf.timestamp, 99 | nonceStr: conf.noncestr, 100 | signature: conf.signature, 101 | beta: true, 102 | jsApiList: ['launchApplication', 'getInstallState'], 103 | openTagList: ['wx-open-launch-app'], 104 | } 105 | window.wx && window.wx.config(wxconfig) 106 | window.wx.ready(() => { 107 | onWechatReady(window.WeixinJSBridge) 108 | // 实例化APP对象 109 | let app = window.WeixinJSBridge 110 | 111 | if (isAndroid) { 112 | const packageName = zAppInfo.ANDROID_PACKAGE_NAME 113 | const packageUrl = urlScheme 114 | invokeInWX('getInstallState', { packageName, packageUrl }, app) 115 | .then(() => { 116 | openAppInWX(urlScheme, instance, app) 117 | }) 118 | .catch(() => { 119 | callFailed() 120 | }) 121 | } else { 122 | // ios 123 | openAppInWX(urlScheme, instance, app) 124 | } 125 | }) 126 | } catch (e) { 127 | callFailed() 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/libs/sdk/zz.ts: -------------------------------------------------------------------------------- 1 | import { AppFlags } from '../../core/targetApp' 2 | import { CallAppInstance } from '../../index' 3 | import { dependencies, SDKNames } from '../config' 4 | // import { checkOpen } from '../evoke' 5 | import { loadJSArr, logError, logInfo } from '../utils' 6 | 7 | // const miniprogramType = 0 // 默认小程序 0 正式版 / 1 开发版 / 2 体验版 8 | // const zzWXMiniAppId = wxInfo.miniID 9 | 10 | export const genWXminiJumpPath = (path: string) => `` 11 | 12 | const openAPP = ( 13 | ctx: CallAppInstance, 14 | app: Record, 15 | curAppFlag: AppFlags, 16 | targetAppFlag: AppFlags 17 | ): void => { 18 | const { urlScheme, download } = ctx 19 | if (!urlScheme) 20 | return logError( 21 | 'openZZInnerAPP failed, please check options.path or options.targetApp is legal' 22 | ) 23 | 24 | const { 25 | callFailed = () => {}, 26 | callSuccess = () => {}, 27 | callError = () => {}, 28 | //delay = 2500, 29 | } = ctx.options 30 | 31 | const unifiedUrl = urlScheme 32 | 33 | logInfo('unifiedUrl ==', unifiedUrl) 34 | 35 | // 自己端内打开自己页面 走 enterUnifiedUrl 36 | if (targetAppFlag & curAppFlag) { 37 | app.enterUnifiedUrl( 38 | { 39 | unifiedUrl, 40 | needClose: '1', 41 | }, 42 | (res: any) => { 43 | // 需要确认 native端回调函数 支持情况 (目前 js-sdk回调无效) 44 | if (res && res.code == 0) { 45 | callSuccess() 46 | } else if (res && res.code != 0) { 47 | callFailed() 48 | download.call(ctx) 49 | } 50 | } 51 | ) 52 | return 53 | } 54 | 55 | logInfo('call APP ==', app.callApp) 56 | // 通过sdk唤起 , 失败成功 回调 57 | app 58 | .callApp({ url: unifiedUrl }, (res: any) => { 59 | // 需要确认 native端回调函数 支持情况 (目前 js-sdk回调无效) 60 | if (res && res.code == 0) { 61 | //必须要有code 返回 才处理回调逻辑 62 | callSuccess() 63 | } else if (Object.keys(res).length > 0 && res.code != 0) { 64 | callFailed() 65 | download.call(ctx) 66 | } 67 | logInfo('app.enterUnifiedUrl callback', res) 68 | }) 69 | .catch((_: any) => { 70 | callError() 71 | logError(_) 72 | }) 73 | } 74 | 75 | // 加载sdk 资源 76 | export const loadZZSkd = (sdkName: SDKNames): Promise> => { 77 | return new Promise((resolve, reject) => { 78 | try { 79 | loadJSArr([dependencies[sdkName].link], () => { 80 | if (sdkName === SDKNames.Z_SDK) { 81 | logInfo('window.ZZAPP ==', window['@zz-common/zz-jssdk'].default) 82 | resolve(window['@zz-common/zz-jssdk'].default) 83 | } 84 | }) 85 | } catch (error) { 86 | reject(error) 87 | } 88 | }) 89 | } 90 | 91 | export const openZZInnerApp = ( 92 | ctx: CallAppInstance, 93 | curAppFlag: AppFlags, 94 | targetAppFlag: AppFlags 95 | ) => { 96 | const { callError = () => {} } = ctx.options 97 | loadZZSkd(SDKNames.Z_SDK) 98 | .then((app) => { 99 | openAPP(ctx, app, curAppFlag, targetAppFlag) 100 | }) 101 | .catch((_) => { 102 | callError() 103 | logError('err', _) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /src/libs/utils.ts: -------------------------------------------------------------------------------- 1 | // 加载 js 资源 2 | export const loadJS = (url: string, cb: () => void) => { 3 | const head = window.document.getElementsByTagName('head')[0] 4 | const js = window.document.createElement('script') 5 | js.setAttribute('type', 'text/javascript') 6 | js.setAttribute('async', 'async') 7 | js.setAttribute('src', url) 8 | js.onload = cb 9 | head.appendChild(js) 10 | } 11 | 12 | export const loadJSArr = (urls: string[], cb: () => void) => { 13 | let done = 0 14 | if (typeof urls === 'string') urls = [urls] 15 | const { length } = urls 16 | urls.map((url) => 17 | loadJS(url, () => { 18 | ++done >= length && cb() 19 | }) 20 | ) 21 | } 22 | 23 | // 24 | export const getUrlParams = (url?: string): Record => { 25 | if (typeof window === 'undefined') { 26 | return {} 27 | } 28 | url = url || (location && location.href) 29 | if (url.indexOf('?') < 0) return {} 30 | 31 | return url 32 | .replace(/^.+?\?/, '') 33 | .replace(/#.+/, '') 34 | .split('&') 35 | .filter((param) => param) 36 | .map(decodeURIComponent) 37 | .reduce((obj: Record, param: string) => { 38 | const i = param.indexOf('=') 39 | const t = [param.slice(0, i), param.slice(i + 1)] 40 | const key: string = t[0] 41 | obj[key] = t[1] 42 | return obj 43 | }, {}) 44 | } 45 | // 46 | export const getCookie = (name: string): string => 47 | ( 48 | document.cookie 49 | .split('; ') 50 | .filter((cookie) => +cookie.indexOf(`${name}=`) === 0) 51 | .pop() || '' 52 | ).replace(/[^=]+=/, '') 53 | 54 | const select = (element: HTMLInputElement) => { 55 | if (typeof window === 'undefined') { 56 | return {} 57 | } 58 | let selectedText 59 | if (element.nodeName === 'SELECT') { 60 | element.focus() 61 | selectedText = element.value 62 | } else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { 63 | const isReadOnly = element.hasAttribute('readonly') 64 | if (!isReadOnly) { 65 | element.setAttribute('readonly', '') 66 | } 67 | 68 | element.select() 69 | element.setSelectionRange(0, element.value.length) 70 | 71 | if (!isReadOnly) { 72 | element.removeAttribute('readonly') 73 | } 74 | selectedText = element.value 75 | } else { 76 | if (element.hasAttribute('contenteditable')) { 77 | element.focus() 78 | } 79 | 80 | const selection = window.getSelection() 81 | const range = document.createRange() 82 | 83 | range.selectNodeContents(element) 84 | selection?.removeAllRanges() 85 | selection?.addRange(range) 86 | 87 | selectedText = selection?.toString() 88 | } 89 | return selectedText 90 | } 91 | // 复制内容到剪切板 92 | export function copy(text: string, options?: Record): boolean { 93 | if (typeof window === 'undefined') { 94 | return false 95 | } 96 | let debug 97 | let fakeElem: HTMLInputElement = document.createElement('input') 98 | let success = false 99 | options = options || {} 100 | debug = options.debug || false 101 | try { 102 | const isRTL = document.documentElement.getAttribute('dir') == 'rtl' 103 | fakeElem = document.createElement('input') 104 | fakeElem.type = 'textarea' 105 | // Prevent zooming on iOS 106 | fakeElem.style.fontSize = '12pt' 107 | // Reset box model 108 | fakeElem.style.border = '0' 109 | fakeElem.style.padding = '0' 110 | fakeElem.style.margin = '0' 111 | // Move element out of screen horizontally 112 | fakeElem.style.position = 'absolute' 113 | fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px' 114 | // Move element to the same position vertically 115 | const yPosition = window.pageYOffset || document.documentElement.scrollTop 116 | fakeElem.style.top = `${yPosition}px` 117 | fakeElem.setAttribute('readonly', '') 118 | fakeElem.value = text 119 | document.body.appendChild(fakeElem) 120 | 121 | select(fakeElem) 122 | 123 | const successful = document.execCommand('copy') 124 | 125 | logInfo('successful', successful) 126 | if (!successful) { 127 | throw new Error('copy command was unsuccessful') 128 | } 129 | success = true 130 | } catch (err) { 131 | debug && logError('unable to copy using execCommand: ', err) 132 | try { 133 | window.clipboardData.setData('text', text) 134 | success = true 135 | } catch (e) { 136 | debug && logError('unable to copy using clipboardData: ', e) 137 | } 138 | } finally { 139 | fakeElem && document.body.removeChild(fakeElem) 140 | } 141 | return success 142 | } 143 | 144 | // 展示遮罩层 145 | export const showMask = (): void => { 146 | const mask = document.createElement('div') 147 | mask.style.cssText = 148 | 'position: fixed;z-index: 100000;transition: all 0.5s;top: 0;left: 0;width: 100%;height: 100%;background-color: rgba(0,0,0,0.6);opacity:0' 149 | mask.innerHTML = 150 | '' 151 | document.body.appendChild(mask) 152 | setTimeout(() => { 153 | mask.style.opacity = '1' 154 | }, 300) 155 | 156 | mask.addEventListener('click', function () { 157 | document.body.removeChild(mask) 158 | }) 159 | } 160 | // 默认关闭 调试信息 161 | window.__callAppDev__ = false 162 | // 默认打开 异常信息 163 | window.__callAppError__ = true 164 | // 165 | export const logError = (...args: any[]): void => { 166 | if (window.__callAppError__) { 167 | // 168 | console.error 169 | ? console.error.call(undefined, ...args) 170 | : console.log.call(undefined, ...[`Error: \n `, ...args]) 171 | } 172 | } 173 | // 174 | export const logInfo = (...args: any[]): void => { 175 | if (window.__callAppDev__) { 176 | // 177 | console.log.call(undefined, ...args) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | - [x] 整理 download 方法 2 | - [x] 整理 platform 方法 3 | - [x] 整理 gen-url-scheme 方法 4 | - [x] 整理 launch 方法 5 | - [x] 支持 ts 6 | - [x] 支持第三方自定义配置 【支付宝】 7 | - [x] 支持 js-sdk 8 | - [x] 中间页迁移,兼容找靓机 9 | - [x] 支持 app 调起 zz 小程序 (appid) 10 | - [x] 支持短信 短链接 唤起机制【IOS 可以尝试,安卓侧转转/找靓机都未支持 app-links】 11 | - [x] 下载还原活动页面, zzzlj.cn/zz/xxxx | zzzlj.cn/zlj/xxxx 12 | - [x] 补充埋点文档 13 | - [x] 引入插件机制,方便可配置 UI 层,注入 JS 等 14 | - [ ] 支持 app-links (安卓端基于 http 协议唤起,减少唤起路径) 15 | - [ ] 提取核心逻辑部分,核心与业务逻辑分层 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": false, 4 | "outDir": "build/", 5 | "baseUrl": ".", 6 | "module": "esnext", 7 | "target": "es2016", 8 | "lib": ["esnext", "dom"], 9 | "sourceMap": false, 10 | "jsx": "preserve", 11 | "allowSyntheticDefaultImports": true, 12 | "moduleResolution": "node", 13 | "forceConsistentCasingInFileNames": true, 14 | "useDefineForClassFields": false, 15 | "experimentalDecorators": true, 16 | "esModuleInterop": true, 17 | "noImplicitReturns": true, 18 | "removeComments": false, 19 | "noUnusedLocals": true, 20 | "allowJs": false, 21 | "strict": true, 22 | "declaration": true, 23 | "declarationDir": "types/dist/", 24 | "paths": {} 25 | }, 26 | "include": ["src", "types/globals.d.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | // https://www.tslang.cn/docs/handbook/compiler-options.html 30 | -------------------------------------------------------------------------------- /types/globals.d.ts: -------------------------------------------------------------------------------- 1 | interface Document { 2 | webkitHidden?: boolean 3 | msHidden?: boolean 4 | } 5 | interface Window { 6 | ZZAPP: Record 7 | ZZSELLER: Record 8 | HUNTERAPP: Record 9 | WBAPP?: Record 10 | __json_jsticket: any 11 | WeixinJSBridge: any 12 | wx: any 13 | nativeAdapterConfig: any 14 | wxconfig: any 15 | clipboardData: Record 16 | [key: string]: any 17 | } 18 | 19 | declare var __DEV__: boolean 20 | declare var __VERSION__: string 21 | -------------------------------------------------------------------------------- /v4/img/demo-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/img/demo-diff.png -------------------------------------------------------------------------------- /v4/img/files-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/img/files-diff.png -------------------------------------------------------------------------------- /v4/img/loginfo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/img/loginfo.png -------------------------------------------------------------------------------- /v4/img/mid-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/img/mid-page.png -------------------------------------------------------------------------------- /v4/img/new-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/img/new-arch.png -------------------------------------------------------------------------------- /v4/img/size-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/img/size-diff.png -------------------------------------------------------------------------------- /v4/img/sms-evoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/img/sms-evoke.png -------------------------------------------------------------------------------- /v4/log.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhuanzhuanfe/call-app/37188d5ac64e1f19e8848fecf29b7da550df8c3c/v4/log.md --------------------------------------------------------------------------------