├── .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
--------------------------------------------------------------------------------