├── .npmrc ├── lib ├── copy.d.ts ├── detector.d.ts ├── utils.d.ts ├── copy.js ├── utils.js ├── index.d.ts ├── detector.js ├── wla.min.js └── index.js ├── examples ├── index.less ├── index.html └── index.ts ├── .gitignore ├── .npmignore ├── tsconfig.json ├── LICENSE ├── package.json ├── webpack.config.js ├── src ├── utils.ts ├── copy.ts ├── detector.ts └── index.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /lib/copy.d.ts: -------------------------------------------------------------------------------- 1 | export declare function copy(text: string, options?: any): boolean; 2 | -------------------------------------------------------------------------------- /examples/index.less: -------------------------------------------------------------------------------- 1 | .demo{ 2 | padding:30px; 3 | a{ 4 | display:block; 5 | margin:20px 0; 6 | } 7 | } 8 | .tip{ 9 | border:1px solid red; 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # node.js 5 | node_modules/ 6 | npm-debug.log 7 | yarn-error.log 8 | 9 | # vs 10 | .vs/ 11 | *.njsproj 12 | *.sln 13 | .vscode/* 14 | !.vscode/settings.json 15 | 16 | # Jetbrains 17 | .idea 18 | 19 | # project 20 | output 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # node.js 5 | node_modules/ 6 | npm-debug.log 7 | yarn-error.log 8 | 9 | # vs 10 | .vs/ 11 | *.njsproj 12 | *.sln 13 | .vscode/* 14 | !.vscode/settings.json 15 | 16 | examples/ 17 | output/ 18 | src/ 19 | tsconfig.json 20 | webpack.config.js -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "noImplicitAny": false, 7 | "experimentalDecorators": true, 8 | "preserveConstEnums": true, 9 | "outDir": "lib", 10 | "declaration": true 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "lib", 15 | "output" 16 | ], 17 | "include": [ 18 | "src/**/*.*" 19 | ] 20 | } -------------------------------------------------------------------------------- /lib/detector.d.ts: -------------------------------------------------------------------------------- 1 | export declare class Detector { 2 | _rules: { 3 | os: any[]; 4 | browser: any[]; 5 | }; 6 | constructor(rules: any); 7 | _detect(name: string, expression: any, ua: string): { 8 | name: string; 9 | version: string; 10 | codename: string; 11 | }; 12 | _parseItem(ua: string, patterns: any[], factory: any, detector: any): void; 13 | /** 14 | * parse ua 15 | * @param ua 16 | */ 17 | parse(ua: string): any; 18 | } 19 | declare const ua: string; 20 | declare const d: any; 21 | export { d as detector, ua }; 22 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /lib/utils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const isIos: boolean; 2 | export declare const isAndroid: boolean; 3 | export declare const inWeixin: boolean; 4 | export declare const inQQ: boolean; 5 | export declare const inWeibo: boolean; 6 | export declare const inBaidu: boolean; 7 | export declare const enableULink: boolean; 8 | export declare const enableApplink: boolean; 9 | export declare const isIOSWithLocationCallSupport: boolean; 10 | export declare const isAndroidWithLocationCallSupport: boolean; 11 | /** 12 | * detect support link 13 | */ 14 | export declare const supportLink: () => boolean; 15 | /** 16 | * location call 17 | * @param url 18 | */ 19 | export declare const locationCall: (url: string) => void; 20 | /** 21 | * iframe call 22 | * @param url 23 | */ 24 | export declare const iframeCall: (url: string) => void; 25 | /** 26 | * merge object 27 | */ 28 | export declare const deepMerge: (firstObj: any, secondObj: any) => any; 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-launch-app", 3 | "version": "2.2.8", 4 | "description": "launch app from web page", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "tsc": "tsc", 8 | "start": "http-server output", 9 | "build": "webpack -w" 10 | }, 11 | "keywords": [ 12 | "deeplink,scheme,universal link,applink,invoke,launch app" 13 | ], 14 | "author": "jawidx", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/jawidx/web-launch-app.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/jawidx/web-launch-app/issues" 21 | }, 22 | "homepage": "https://github.com/jawidx/web-launch-app#readme", 23 | "license": "MIT", 24 | "dependencies": {}, 25 | "devDependencies": { 26 | "@babel/core": "^7.10.4", 27 | "@babel/preset-env": "^7.10.4", 28 | "babel-loader": "^8.0.4", 29 | "clean-webpack-plugin": "^0.1.17", 30 | "css-loader": "^0.28.7", 31 | "html-loader": "^0.5.1", 32 | "html-webpack-plugin": "^3.2.0", 33 | "http-server": "^0.11.1", 34 | "less": "^2.7.3", 35 | "less-loader": "^4.0.5", 36 | "ts-loader": "^4.4.2", 37 | "webpack": "^4.43.0", 38 | "webpack-cli": "^3.3.12" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 4 | 5 | module.exports = { 6 | mode: 'production', 7 | target: 'web', 8 | entry: { 9 | demo: ['./examples/index.ts'], 10 | wla: ['./src/index.ts'] 11 | }, 12 | output: { 13 | filename: '[name].[chunkhash:6].js', 14 | path: path.resolve(__dirname, 'output'), 15 | library: 'WLA', 16 | libraryTarget: 'window', 17 | }, 18 | plugins: [ 19 | new CleanWebpackPlugin(['output']), 20 | new HtmlWebpackPlugin({ 21 | filename: 'demo.html', 22 | template: './examples/index.html', 23 | title: 'Demo Title', 24 | // chunksSortMode: none 25 | }), 26 | ], 27 | resolve: { 28 | extensions: ['*', '.js', '.jsx', '.ts', '.tsx'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.(js|jsx|ts|tsx)$/, 34 | include: path.resolve(__dirname, 'src'), 35 | use: [ 36 | { 37 | loader: 'babel-loader', 38 | options: { 39 | presets: ['@babel/env'], 40 | } 41 | }, 42 | { 43 | loader: 'ts-loader', 44 | options: { 45 | configFile: 'tsconfig.json', 46 | }, 47 | } 48 | ] 49 | }, 50 | { 51 | test: /\.(html)$/, 52 | use: { 53 | loader: 'html-loader', 54 | } 55 | }, 56 | { 57 | test: /\.(css|less)$/, 58 | use: [ 59 | 'css-loader', 60 | 'less-loader' 61 | ] 62 | }, 63 | ] 64 | } 65 | }; -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { detector } from './detector' 2 | 3 | export const isIos = detector.os.name === 'ios'; 4 | export const isAndroid = detector.os.name === 'android'; 5 | export const inWeixin = detector.browser.name === 'micromessenger'; 6 | export const inQQ = detector.browser.name === 'qq'; 7 | export const inWeibo = detector.browser.name === 'weibo'; 8 | export const inBaidu = detector.browser.name === 'baidu'; 9 | 10 | export const enableULink = isIos && detector.os.version >= 9; 11 | export const enableApplink = isAndroid && detector.os.version >= 6; 12 | 13 | export const isIOSWithLocationCallSupport = isIos && detector.browser.name == 'safari' && detector.os.version >= 9; 14 | const isChromeWithLocationCallSupport = detector.browser.name == 'chrome' && detector.browser.version > 55; 15 | const isSamsungWithLocationCallSupport = detector.browser.name == 'samsung'; 16 | export const isAndroidWithLocationCallSupport = isAndroid && (isChromeWithLocationCallSupport || isSamsungWithLocationCallSupport); 17 | 18 | /** 19 | * detect support link 20 | */ 21 | export const supportLink = () => { 22 | let supportLink = false; 23 | if (enableApplink) { 24 | switch (detector.browser.name) { 25 | case 'chrome': 26 | case 'samsung': 27 | case 'zhousi': 28 | supportLink = true; 29 | break; 30 | default: 31 | supportLink = false; 32 | break; 33 | } 34 | } 35 | if (enableULink) { 36 | switch (detector.browser.name) { 37 | case 'uc': 38 | case 'qq': 39 | supportLink = false; 40 | break; 41 | default: 42 | supportLink = true; 43 | break; 44 | } 45 | } 46 | return supportLink; 47 | } 48 | 49 | /** 50 | * location call 51 | * @param url 52 | */ 53 | export const locationCall = (url: string) => { 54 | (top.location || location).href = url; 55 | } 56 | 57 | /** 58 | * iframe call 59 | * @param url 60 | */ 61 | export const iframeCall = (url: string) => { 62 | const iframe = document.createElement('iframe'); 63 | iframe.setAttribute('src', url); 64 | iframe.setAttribute('style', 'display:none'); 65 | document.body.appendChild(iframe); 66 | setTimeout(function () { 67 | document.body.removeChild(iframe); 68 | }, 200); 69 | } 70 | 71 | /** 72 | * merge object 73 | */ 74 | export const deepMerge = (firstObj, secondObj) => { 75 | for (var key in secondObj) { 76 | firstObj[key] = firstObj[key] && firstObj[key].toString() === "[object Object]" ? 77 | deepMerge(firstObj[key], secondObj[key]) : firstObj[key] = secondObj[key]; 78 | } 79 | return firstObj; 80 | } 81 | -------------------------------------------------------------------------------- /src/copy.ts: -------------------------------------------------------------------------------- 1 | function select(element) { 2 | var selectedText; 3 | if (element.nodeName === 'SELECT') { 4 | element.focus(); 5 | selectedText = element.value; 6 | } 7 | else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { 8 | var isReadOnly = element.hasAttribute('readonly'); 9 | if (!isReadOnly) { 10 | element.setAttribute('readonly', ''); 11 | } 12 | 13 | element.select(); 14 | element.setSelectionRange(0, element.value.length); 15 | 16 | if (!isReadOnly) { 17 | element.removeAttribute('readonly'); 18 | } 19 | selectedText = element.value; 20 | } 21 | else { 22 | if (element.hasAttribute('contenteditable')) { 23 | element.focus(); 24 | } 25 | 26 | var selection = window.getSelection(); 27 | var range = document.createRange(); 28 | 29 | range.selectNodeContents(element); 30 | selection.removeAllRanges(); 31 | selection.addRange(range); 32 | 33 | selectedText = selection.toString(); 34 | } 35 | return selectedText; 36 | } 37 | 38 | export function copy(text: string, options?: any) { 39 | var debug, fakeElem, success = false; 40 | options = options || {}; 41 | debug = options.debug || false; 42 | try { 43 | const isRTL = document.documentElement.getAttribute('dir') == 'rtl'; 44 | fakeElem = document.createElement('textarea'); 45 | // Prevent zooming on iOS 46 | fakeElem.style.fontSize = '12pt'; 47 | // Reset box model 48 | fakeElem.style.border = '0'; 49 | fakeElem.style.padding = '0'; 50 | fakeElem.style.margin = '0'; 51 | // Move element out of screen horizontally 52 | fakeElem.style.position = 'absolute'; 53 | fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px'; 54 | // Move element to the same position vertically 55 | let yPosition = window.pageYOffset || document.documentElement.scrollTop; 56 | fakeElem.style.top = `${yPosition}px`; 57 | fakeElem.setAttribute('readonly', ''); 58 | fakeElem.value = text; 59 | document.body.appendChild(fakeElem); 60 | 61 | select(fakeElem); 62 | 63 | var successful = document.execCommand('copy'); 64 | if (!successful) { 65 | throw new Error('copy command was unsuccessful'); 66 | } 67 | success = true; 68 | } catch (err) { 69 | debug && console.error('unable to copy using execCommand: ', err); 70 | debug && console.warn('trying IE specific stuff'); 71 | try { 72 | (window as any).clipboardData.setData('text', text); 73 | success = true; 74 | } catch (err) { 75 | debug && console.error('unable to copy using clipboardData: ', err); 76 | } 77 | } finally { 78 | if (fakeElem) { 79 | document.body.removeChild(fakeElem); 80 | } 81 | } 82 | return success; 83 | } 84 | -------------------------------------------------------------------------------- /lib/copy.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.copy = void 0; 4 | function select(element) { 5 | var selectedText; 6 | if (element.nodeName === 'SELECT') { 7 | element.focus(); 8 | selectedText = element.value; 9 | } 10 | else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { 11 | var isReadOnly = element.hasAttribute('readonly'); 12 | if (!isReadOnly) { 13 | element.setAttribute('readonly', ''); 14 | } 15 | element.select(); 16 | element.setSelectionRange(0, element.value.length); 17 | if (!isReadOnly) { 18 | element.removeAttribute('readonly'); 19 | } 20 | selectedText = element.value; 21 | } 22 | else { 23 | if (element.hasAttribute('contenteditable')) { 24 | element.focus(); 25 | } 26 | var selection = window.getSelection(); 27 | var range = document.createRange(); 28 | range.selectNodeContents(element); 29 | selection.removeAllRanges(); 30 | selection.addRange(range); 31 | selectedText = selection.toString(); 32 | } 33 | return selectedText; 34 | } 35 | function copy(text, options) { 36 | var debug, fakeElem, success = false; 37 | options = options || {}; 38 | debug = options.debug || false; 39 | try { 40 | var isRTL = document.documentElement.getAttribute('dir') == 'rtl'; 41 | fakeElem = document.createElement('textarea'); 42 | // Prevent zooming on iOS 43 | fakeElem.style.fontSize = '12pt'; 44 | // Reset box model 45 | fakeElem.style.border = '0'; 46 | fakeElem.style.padding = '0'; 47 | fakeElem.style.margin = '0'; 48 | // Move element out of screen horizontally 49 | fakeElem.style.position = 'absolute'; 50 | fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px'; 51 | // Move element to the same position vertically 52 | var yPosition = window.pageYOffset || document.documentElement.scrollTop; 53 | fakeElem.style.top = yPosition + "px"; 54 | fakeElem.setAttribute('readonly', ''); 55 | fakeElem.value = text; 56 | document.body.appendChild(fakeElem); 57 | select(fakeElem); 58 | var successful = document.execCommand('copy'); 59 | if (!successful) { 60 | throw new Error('copy command was unsuccessful'); 61 | } 62 | success = true; 63 | } 64 | catch (err) { 65 | debug && console.error('unable to copy using execCommand: ', err); 66 | debug && console.warn('trying IE specific stuff'); 67 | try { 68 | window.clipboardData.setData('text', text); 69 | success = true; 70 | } 71 | catch (err) { 72 | debug && console.error('unable to copy using clipboardData: ', err); 73 | } 74 | } 75 | finally { 76 | if (fakeElem) { 77 | document.body.removeChild(fakeElem); 78 | } 79 | } 80 | return success; 81 | } 82 | exports.copy = copy; 83 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.deepMerge = exports.iframeCall = exports.locationCall = exports.supportLink = exports.isAndroidWithLocationCallSupport = exports.isIOSWithLocationCallSupport = exports.enableApplink = exports.enableULink = exports.inBaidu = exports.inWeibo = exports.inQQ = exports.inWeixin = exports.isAndroid = exports.isIos = void 0; 4 | var detector_1 = require("./detector"); 5 | exports.isIos = detector_1.detector.os.name === 'ios'; 6 | exports.isAndroid = detector_1.detector.os.name === 'android'; 7 | exports.inWeixin = detector_1.detector.browser.name === 'micromessenger'; 8 | exports.inQQ = detector_1.detector.browser.name === 'qq'; 9 | exports.inWeibo = detector_1.detector.browser.name === 'weibo'; 10 | exports.inBaidu = detector_1.detector.browser.name === 'baidu'; 11 | exports.enableULink = exports.isIos && detector_1.detector.os.version >= 9; 12 | exports.enableApplink = exports.isAndroid && detector_1.detector.os.version >= 6; 13 | exports.isIOSWithLocationCallSupport = exports.isIos && detector_1.detector.browser.name == 'safari' && detector_1.detector.os.version >= 9; 14 | var isChromeWithLocationCallSupport = detector_1.detector.browser.name == 'chrome' && detector_1.detector.browser.version > 55; 15 | var isSamsungWithLocationCallSupport = detector_1.detector.browser.name == 'samsung'; 16 | exports.isAndroidWithLocationCallSupport = exports.isAndroid && (isChromeWithLocationCallSupport || isSamsungWithLocationCallSupport); 17 | /** 18 | * detect support link 19 | */ 20 | exports.supportLink = function () { 21 | var supportLink = false; 22 | if (exports.enableApplink) { 23 | switch (detector_1.detector.browser.name) { 24 | case 'chrome': 25 | case 'samsung': 26 | case 'zhousi': 27 | supportLink = true; 28 | break; 29 | default: 30 | supportLink = false; 31 | break; 32 | } 33 | } 34 | if (exports.enableULink) { 35 | switch (detector_1.detector.browser.name) { 36 | case 'uc': 37 | case 'qq': 38 | supportLink = false; 39 | break; 40 | default: 41 | supportLink = true; 42 | break; 43 | } 44 | } 45 | return supportLink; 46 | }; 47 | /** 48 | * location call 49 | * @param url 50 | */ 51 | exports.locationCall = function (url) { 52 | (top.location || location).href = url; 53 | }; 54 | /** 55 | * iframe call 56 | * @param url 57 | */ 58 | exports.iframeCall = function (url) { 59 | var iframe = document.createElement('iframe'); 60 | iframe.setAttribute('src', url); 61 | iframe.setAttribute('style', 'display:none'); 62 | document.body.appendChild(iframe); 63 | setTimeout(function () { 64 | document.body.removeChild(iframe); 65 | }, 200); 66 | }; 67 | /** 68 | * merge object 69 | */ 70 | exports.deepMerge = function (firstObj, secondObj) { 71 | for (var key in secondObj) { 72 | firstObj[key] = firstObj[key] && firstObj[key].toString() === "[object Object]" ? 73 | exports.deepMerge(firstObj[key], secondObj[key]) : firstObj[key] = secondObj[key]; 74 | } 75 | return firstObj; 76 | }; 77 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import { copy } from './copy'; 2 | import { ua, detector } from './detector'; 3 | import { isAndroid, isIos, inWeibo, inWeixin, inQQ, inBaidu, enableApplink, enableULink, supportLink, locationCall, iframeCall } from './utils'; 4 | export { copy, ua, detector }; 5 | export { isAndroid, isIos, inWeibo, inWeixin, inQQ, inBaidu, enableApplink, enableULink, supportLink, locationCall, iframeCall, }; 6 | export declare class LaunchApp { 7 | static defaultConfig: any; 8 | static openChannel: { 9 | scheme: { 10 | preOpen(opt: any): any; 11 | open: (url: string) => void; 12 | }; 13 | link: { 14 | preOpen: (opt: any) => any; 15 | open: (url: string) => void; 16 | }; 17 | guide: { 18 | open: () => void; 19 | }; 20 | store: { 21 | open: (noTimeout: any) => void; 22 | }; 23 | unknown: { 24 | open: () => void; 25 | }; 26 | }; 27 | static openStatus: { 28 | FAILED: number; 29 | SUCCESS: number; 30 | UNKNOWN: number; 31 | }; 32 | static callbackResult: { 33 | DO_NOTING: number; 34 | OPEN_LAND_PAGE: number; 35 | OPEN_APP_STORE: number; 36 | }; 37 | private readonly configs; 38 | private readonly openMethod; 39 | private timer; 40 | private options; 41 | private timeoutDownload; 42 | private callback; 43 | private openUrl; 44 | private callbackId; 45 | constructor(opt?: any); 46 | /** 47 | * select open method according to the environment and config 48 | */ 49 | _getOpenMethod(): { 50 | preOpen(opt: any): any; 51 | open: (url: string) => void; 52 | } | { 53 | open: () => void; 54 | }; 55 | /** 56 | * launch app 57 | * @param {*} opt 58 | * page:'index', 59 | * param:{}, 60 | * paramMap:{} 61 | * scheme:'', for scheme 62 | * url:'', for link 63 | * launchType:{ 64 | * ios:link/scheme/store 65 | * android:link/scheme/store 66 | * } 67 | * autodemotion 68 | * guideMethod 69 | * useYingyongbao 70 | * updateTipMethod 71 | * clipboardTxt 72 | * pkgs:{android:'',ios:'',yyb:'',store:{...}} 73 | * timeout 是否走超时逻辑,<0表示不走 74 | * landPage 兜底页 75 | * callback 端回调方法 76 | * @param {*} callback: callbackResult 77 | */ 78 | open(opt?: any, callback?: (status: number, detector: any, scheme?: string) => number): void; 79 | /** 80 | * download package 81 | * opt: {android:'',ios:'',yyk:'',landPage} 82 | */ 83 | download(opt?: any): void; 84 | /** 85 | * 检验版本 86 | * @param pageConf {version:''} 87 | */ 88 | _checkVersion(pageConf: any): boolean; 89 | /** 90 | * map param (for different platform) 91 | * @param {*} param 92 | * @param {*} paramMap 93 | */ 94 | _paramMapProcess(param: any, paramMap: any): any; 95 | /** 96 | * generating URL parameters 97 | * @param {*} obj 98 | */ 99 | _stringtifyParams(obj: any): string; 100 | /** 101 | * generating URL 102 | * @param {*} conf 103 | * @param type 'scheme link yyb' 104 | */ 105 | _getUrlFromConf(conf: any, type: string): string; 106 | /** 107 | * callback 108 | * @param status 109 | */ 110 | _callend(status: number): void; 111 | /** 112 | * determine whether or not open successfully 113 | */ 114 | _setTimeEvent(): void; 115 | } 116 | -------------------------------------------------------------------------------- /examples/index.ts: -------------------------------------------------------------------------------- 1 | import { LaunchApp, detector } from '../src/index' 2 | import { isAndroid, inWeixin, inWeibo, supportLink } from '../src/utils'; 3 | import './index.less'; 4 | console.log('detector,', detector); 5 | 6 | function addHandler(element, type, handler) { 7 | if (!element) return; 8 | if (element.addEventListener) { 9 | element.addEventListener(type, handler, false); 10 | } else if (element.attachEvent) { 11 | element.attachEvent('on' + type, handler); 12 | } else { 13 | element['on' + type] = handler; 14 | } 15 | } 16 | 17 | const linkOpen = document.getElementsByClassName('j_open')[0]; 18 | const linkDown = document.getElementsByClassName('j_down')[0]; 19 | 20 | // config 21 | let schemeConfig = { 22 | protocol: 'baiduhaokan', 23 | index: { path: 'home/index' }, 24 | video: { path: 'video/details/' }, 25 | author: { path: 'author/details/', version: '4.7' }, 26 | }; 27 | const haokanConfig = { 28 | inApp: false, // TODO 29 | appVersion: '4.19.5.10', 30 | pkgName: 'com.baidu.haokan', 31 | deeplink: { 32 | scheme: { 33 | android: schemeConfig, 34 | ios: schemeConfig 35 | }, 36 | link: { 37 | index: { url: 'http://hku.baidu.com/h5/share/homeindex' }, 38 | video: { url: 'http://hku.baidu.com/h5/share/detail' }, 39 | author: { url: 'http://hku.baidu.com/h5/share/detailauthor' } 40 | } 41 | }, 42 | pkgs: { 43 | android: 'https://downpack.baidu.com/baidutieba_AndroidPhone_v8.8.8.6(8.8.8.6)_1020584c.apk', 44 | ios: 'https://itunes.apple.com/cn/app/id1322948417?mt=8', 45 | yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.baidu.haokan&ckey=CK1374101624513', 46 | store: { 47 | // other: { 48 | // reg: '', 49 | // scheme: '', 50 | // id: '' 51 | // }, 52 | } 53 | }, 54 | useUniversalLink: true, 55 | useAppLink: supportLink(), 56 | autodemotion: true, 57 | useYingyongbao: inWeixin && isAndroid, 58 | useGuideMethod: inWeibo && isAndroid, 59 | // guideMethod: () => { 60 | // alert('右上角->在浏览器中打开'); 61 | // }, 62 | timeout: 2000, 63 | landPage: 'https://haokan.baidu.com/download' 64 | }; 65 | const lanchHaokan = new LaunchApp(haokanConfig); 66 | 67 | addHandler(linkOpen, 'click', function () { 68 | lanchHaokan.open({ 69 | // useGuideMethod: true, 70 | useYingyongbao: true,//inWeixin && isAndroid, 71 | launchType: { 72 | ios: 'scheme', 73 | android: inWeixin ? 'store' : 'scheme' 74 | }, 75 | page: 'author', 76 | param: { 77 | url_key: '4215764431860909454', 78 | target: 'https%3A%2F%2Fbaijiahao.baidu.com%2Fu%3Fapp_id%3D1611116910625404%26fr%3Dbjhvideo', 79 | }, 80 | // scheme: 'baiduhaokan://my/history?a=b', 81 | // url: 'https://hku.baidu.com/h5/share/s/my/settings', 82 | // scheme:'baiduhaokan://my/setting', 83 | // url:'http://hku.baidu.com/h5/share/detailauthor?url_key=1611116910625404&target=https%3A%2F%2Fbaijiahao.baidu.com%2Fu%3Fapp_id%3D1611116910625404%26fr%3Dbjhvideo', 84 | // guideMethod: () => { 85 | // alert('出去玩opt'); 86 | // }, 87 | timeout: 2000, 88 | // clipboardTxt: '#baiduhaokan://webview/?url_key=https%3a%2f%2feopa.baidu.com%2fpage%2fauthorizeIndex-AcHzJLpa%3fproductid%3d1%26gtype%3d1%26idfrom%3dinside-baiduappbanner&pd=yq&tab=guide&tag=guide&source=yq-0-yq#', 89 | // pkgs: { 90 | // android: 'https://sv.bdstatic.com/static/haokanapk/apk/baiduhaokan1021176d.apk', 91 | // ios: 'https://itunes.apple.com/cn/app/id1322948417?mt=8', 92 | // // yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.baidu.tieba&ckey=CK1374101624513' 93 | // } 94 | }); 95 | // , (s, d, url) => { 96 | // console.log('callbackout', s, d, url); 97 | // s != 1 && copy(url); 98 | // return 2; 99 | // } 100 | 101 | }); 102 | 103 | addHandler(linkDown, 'click', function () { 104 | // lanchHaokan.download(); 105 | lanchHaokan.download({ 106 | android: 'https://downpack.baidu.com/baidutieba_AndroidPhone_v8.8.8.6(8.8.8.6)_1020584c.apk', 107 | ios: 'https://itunes.apple.com/cn/app/id1322948417?mt=8', 108 | yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.baidu.haokan&ckey=CK1374101624513&a=3', 109 | landPage: 'https://haokan.baidu.com/download' 110 | }); 111 | }) 112 | -------------------------------------------------------------------------------- /src/detector.ts: -------------------------------------------------------------------------------- 1 | function typeOf(type: string) { 2 | return function (object: any) { 3 | return Object.prototype.toString.call(object) === "[object " + type + "]"; 4 | }; 5 | } 6 | function each(object: any, factory: any) { 7 | for (let i = 0, l = object.length; i < l; i++) { 8 | if (factory.call(object, object[i], i) === false) { 9 | break; 10 | } 11 | } 12 | } 13 | 14 | export class Detector { 15 | _rules: { os: any[], browser: any[] } 16 | constructor(rules: any) { 17 | this._rules = rules; 18 | } 19 | 20 | _detect(name: string, expression: any, ua: string) { 21 | const expr = typeOf("Function")(expression) ? expression.call(null, ua) : expression; 22 | if (!expr) { return null; } 23 | const info = { 24 | name: name, 25 | version: "0", 26 | codename: "", 27 | }; 28 | if (expr === true) { 29 | return info; 30 | } else if (typeOf("String")(expr)) { 31 | if (ua.indexOf(expr) !== -1) { 32 | return info; 33 | } 34 | } else if (typeOf("Object")(expr)) { 35 | if (expr.hasOwnProperty("version")) { 36 | info.version = expr.version; 37 | } 38 | return info; 39 | } else if (typeOf("RegExp")(expr)) { 40 | const m = expr.exec(ua); 41 | if (m) { 42 | if (m.length >= 2 && m[1]) { 43 | info.version = m[1].replace(/_/g, "."); 44 | } 45 | return info; 46 | } 47 | } 48 | } 49 | 50 | _parseItem(ua: string, patterns: any[], factory: any, detector: any) { 51 | let self = this; 52 | let detected = { 53 | name: "na", 54 | version: "0", 55 | };; 56 | each(patterns, function (pattern: any) { 57 | const d = self._detect(pattern[0], pattern[1], ua); 58 | if (d) { 59 | detected = d; 60 | return false; 61 | } 62 | }); 63 | factory.call(detector, detected.name, detected.version); 64 | } 65 | 66 | /** 67 | * parse ua 68 | * @param ua 69 | */ 70 | parse(ua: string) { 71 | ua = (ua || "").toLowerCase(); 72 | const d: any = {}; 73 | 74 | this._parseItem(ua, this._rules.os, function (name: string, version: string) { 75 | const v = parseFloat(version); 76 | d.os = { 77 | name: name, 78 | version: v, 79 | fullVersion: version, 80 | }; 81 | d.os[name] = v; 82 | }, d); 83 | 84 | this._parseItem(ua, this._rules.browser, function (name: string, version: string) { 85 | let mode = version; 86 | const v = parseFloat(version); 87 | d.browser = { 88 | name: name, 89 | version: v, 90 | fullVersion: version, 91 | mode: parseFloat(mode), 92 | fullMode: mode, 93 | }; 94 | d.browser[name] = v; 95 | }, d); 96 | return d; 97 | } 98 | } 99 | 100 | const OS = [ 101 | ["ios", function (ua: string) { 102 | if (/\bcpu(?: iphone)? os /.test(ua)) { 103 | return /\bcpu(?: iphone)? os ([0-9._]+)/; 104 | } else if (ua.indexOf("iph os ") !== -1) { 105 | return /\biph os ([0-9_]+)/; 106 | } else { 107 | return /\bios\b/; 108 | } 109 | }], 110 | ["android", function (ua: string) { 111 | if (ua.indexOf("android") >= 0) { 112 | return /\bandroid[ \/-]?([0-9.x]+)?/; 113 | } else if (ua.indexOf("adr") >= 0) { 114 | if (ua.indexOf("mqqbrowser") >= 0) { 115 | return /\badr[ ]\(linux; u; ([0-9.]+)?/; 116 | } else { 117 | return /\badr(?:[ ]([0-9.]+))?/; 118 | } 119 | } 120 | return "android"; 121 | //return /\b(?:android|\badr)(?:[\/\- ](?:\(linux; u; )?)?([0-9.x]+)?/; 122 | }], 123 | ["wp", function (ua: string) { 124 | if (ua.indexOf("windows phone ") !== -1) { 125 | return /\bwindows phone (?:os )?([0-9.]+)/; 126 | } else if (ua.indexOf("xblwp") !== -1) { 127 | return /\bxblwp([0-9.]+)/; 128 | } else if (ua.indexOf("zunewp") !== -1) { 129 | return /\bzunewp([0-9.]+)/; 130 | } 131 | return "windows phone"; 132 | }], 133 | ["symbian", /\bsymbian(?:os)?\/([0-9.]+)/], 134 | ["chromeos", /\bcros i686 ([0-9.]+)/], 135 | ["linux", "linux"], 136 | ["windowsce", /\bwindows ce(?: ([0-9.]+))?/] 137 | ]; 138 | const BROWSER = [ 139 | // app 140 | ["micromessenger", /\bmicromessenger\/([\d.]+)/], 141 | ["qq", /\bqq/i], 142 | ["qzone", /qzone\/.*_qz_([\d.]+)/i], 143 | ["qqbrowser", /\bm?qqbrowser\/([0-9.]+)/], 144 | ["tt", /\btencenttraveler ([0-9.]+)/], 145 | ["weibo", /weibo__([0-9.]+)/], 146 | ["uc", function (ua: string) { 147 | if (ua.indexOf("ucbrowser/") >= 0) { 148 | return /\bucbrowser\/([0-9.]+)/; 149 | } else if (ua.indexOf("ubrowser/") >= 0) { 150 | return /\bubrowser\/([0-9.]+)/; 151 | } else if (/\buc\/[0-9]/.test(ua)) { 152 | return /\buc\/([0-9.]+)/; 153 | } else if (ua.indexOf("ucweb") >= 0) { 154 | // `ucweb/2.0` is compony info. 155 | // `UCWEB8.7.2.214/145/800` is browser info. 156 | return /\bucweb([0-9.]+)?/; 157 | } else { 158 | return /\b(?:ucbrowser|uc)\b/; 159 | } 160 | }], 161 | ["360", function (ua: string) { 162 | if (ua.indexOf("360 aphone browser") !== -1) { 163 | return /\b360 aphone browser \(([^\)]+)\)/; 164 | } 165 | return /\b360(?:se|ee|chrome|browser)\b/; 166 | }], 167 | ["baidu", 168 | function (ua) { 169 | let back = 0; 170 | let a; 171 | if (/ baiduboxapp\//i.test(ua)) { 172 | if (a = /([\d+.]+)_(?:diordna|enohpi)_/.exec(ua)) { 173 | a = a[1].split("."); 174 | back = a.reverse().join("."); 175 | } else if ((a = /baiduboxapp\/([\d+.]+)/.exec(ua))) { 176 | back = a[1]; 177 | } 178 | return { 179 | version: back, 180 | }; 181 | } 182 | return false; 183 | }, 184 | ], 185 | ["baidubrowser", /\b(?:ba?idubrowser|baiduhd)[ \/]([0-9.x]+)/], 186 | ["bdminivideo", /bdminivideo\/([0-9.]+)/], 187 | ["sogou", function (ua: string) { 188 | if (ua.indexOf("sogoumobilebrowser") >= 0) { 189 | return /sogoumobilebrowser\/([0-9.]+)/; 190 | } else if (ua.indexOf("sogoumse") >= 0) { 191 | return true; 192 | } 193 | return / se ([0-9.x]+)/; 194 | }], 195 | ["ali-ap", function (ua: string) { 196 | if (ua.indexOf("aliapp") > 0) { 197 | return /\baliapp\(ap\/([0-9.]+)\)/; 198 | } else { 199 | return /\balipayclient\/([0-9.]+)\b/; 200 | } 201 | }], 202 | ["ali-tb", /\baliapp\(tb\/([0-9.]+)\)/], 203 | ["ali-tm", /\baliapp\(tm\/([0-9.]+)\)/], 204 | ["tao", /\btaobrowser\/([0-9.]+)/], 205 | // 厂商 206 | ["mi", /\bmiuibrowser\/([0-9.]+)/], 207 | ["oppo", /\boppobrowser\/([0-9.]+)/], 208 | ["vivo", /\bvivobrowser\/([0-9.]+)/], 209 | ["meizu", /\bmzbrowser\/([0-9.]+)/], 210 | ["nokia", /\bnokiabrowser\/([0-9.]+)/], 211 | // ["huawei", /\bhuaweibrowser\/([0-9.]+)/], 212 | ["samsung", /\bsamsungbrowser\/([0-9.]+)/], 213 | // browser 214 | ["maxthon", /\b(?:maxthon|mxbrowser)(?:[ \/]([0-9.]+))?/], 215 | // Opera 15 之后开始使用 Chromniun 内核,需要放在 Chrome 的规则之前。 216 | ["opera", function (ua: string) { 217 | const re_opera_old = /\bopera.+version\/([0-9.ab]+)/; 218 | const re_opera_new = /\bopr\/([0-9.]+)/; 219 | return re_opera_old.test(ua) ? re_opera_old : re_opera_new; 220 | }], 221 | ["edge", /edge\/([0-9.]+)/], 222 | ["firefox", /\bfirefox\/([0-9.ab]+)/], 223 | ["chrome", / (?:chrome|crios|crmo)\/([0-9.]+)/], 224 | // Android 默认浏览器。该规则需要在 safari 之前。 225 | ["android", function (ua: string) { 226 | if (ua.indexOf("android") === -1) { return; } 227 | return /\bversion\/([0-9.]+(?: beta)?)/; 228 | }], 229 | ["safari", /\bversion\/([0-9.]+(?: beta)?)(?: mobile(?:\/[a-z0-9]+)?)? safari\//], 230 | // 如果不能识别为浏览器则为webview。 231 | ["webview", /\bcpu(?: iphone)? os (?:[0-9._]+).+\bapplewebkit\b/], 232 | ]; 233 | 234 | const detector = new Detector({ 235 | os: OS, 236 | browser: BROWSER 237 | }); 238 | const ua = navigator.userAgent + " " + navigator.appVersion + " " + navigator.vendor; 239 | const d = detector.parse(ua); 240 | export { d as detector, ua } 241 | -------------------------------------------------------------------------------- /lib/detector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ua = exports.detector = exports.Detector = void 0; 4 | function typeOf(type) { 5 | return function (object) { 6 | return Object.prototype.toString.call(object) === "[object " + type + "]"; 7 | }; 8 | } 9 | function each(object, factory) { 10 | for (var i = 0, l = object.length; i < l; i++) { 11 | if (factory.call(object, object[i], i) === false) { 12 | break; 13 | } 14 | } 15 | } 16 | var Detector = /** @class */ (function () { 17 | function Detector(rules) { 18 | this._rules = rules; 19 | } 20 | Detector.prototype._detect = function (name, expression, ua) { 21 | var expr = typeOf("Function")(expression) ? expression.call(null, ua) : expression; 22 | if (!expr) { 23 | return null; 24 | } 25 | var info = { 26 | name: name, 27 | version: "0", 28 | codename: "", 29 | }; 30 | if (expr === true) { 31 | return info; 32 | } 33 | else if (typeOf("String")(expr)) { 34 | if (ua.indexOf(expr) !== -1) { 35 | return info; 36 | } 37 | } 38 | else if (typeOf("Object")(expr)) { 39 | if (expr.hasOwnProperty("version")) { 40 | info.version = expr.version; 41 | } 42 | return info; 43 | } 44 | else if (typeOf("RegExp")(expr)) { 45 | var m = expr.exec(ua); 46 | if (m) { 47 | if (m.length >= 2 && m[1]) { 48 | info.version = m[1].replace(/_/g, "."); 49 | } 50 | return info; 51 | } 52 | } 53 | }; 54 | Detector.prototype._parseItem = function (ua, patterns, factory, detector) { 55 | var self = this; 56 | var detected = { 57 | name: "na", 58 | version: "0", 59 | }; 60 | ; 61 | each(patterns, function (pattern) { 62 | var d = self._detect(pattern[0], pattern[1], ua); 63 | if (d) { 64 | detected = d; 65 | return false; 66 | } 67 | }); 68 | factory.call(detector, detected.name, detected.version); 69 | }; 70 | /** 71 | * parse ua 72 | * @param ua 73 | */ 74 | Detector.prototype.parse = function (ua) { 75 | ua = (ua || "").toLowerCase(); 76 | var d = {}; 77 | this._parseItem(ua, this._rules.os, function (name, version) { 78 | var v = parseFloat(version); 79 | d.os = { 80 | name: name, 81 | version: v, 82 | fullVersion: version, 83 | }; 84 | d.os[name] = v; 85 | }, d); 86 | this._parseItem(ua, this._rules.browser, function (name, version) { 87 | var mode = version; 88 | var v = parseFloat(version); 89 | d.browser = { 90 | name: name, 91 | version: v, 92 | fullVersion: version, 93 | mode: parseFloat(mode), 94 | fullMode: mode, 95 | }; 96 | d.browser[name] = v; 97 | }, d); 98 | return d; 99 | }; 100 | return Detector; 101 | }()); 102 | exports.Detector = Detector; 103 | var OS = [ 104 | ["ios", function (ua) { 105 | if (/\bcpu(?: iphone)? os /.test(ua)) { 106 | return /\bcpu(?: iphone)? os ([0-9._]+)/; 107 | } 108 | else if (ua.indexOf("iph os ") !== -1) { 109 | return /\biph os ([0-9_]+)/; 110 | } 111 | else { 112 | return /\bios\b/; 113 | } 114 | }], 115 | ["android", function (ua) { 116 | if (ua.indexOf("android") >= 0) { 117 | return /\bandroid[ \/-]?([0-9.x]+)?/; 118 | } 119 | else if (ua.indexOf("adr") >= 0) { 120 | if (ua.indexOf("mqqbrowser") >= 0) { 121 | return /\badr[ ]\(linux; u; ([0-9.]+)?/; 122 | } 123 | else { 124 | return /\badr(?:[ ]([0-9.]+))?/; 125 | } 126 | } 127 | return "android"; 128 | //return /\b(?:android|\badr)(?:[\/\- ](?:\(linux; u; )?)?([0-9.x]+)?/; 129 | }], 130 | ["wp", function (ua) { 131 | if (ua.indexOf("windows phone ") !== -1) { 132 | return /\bwindows phone (?:os )?([0-9.]+)/; 133 | } 134 | else if (ua.indexOf("xblwp") !== -1) { 135 | return /\bxblwp([0-9.]+)/; 136 | } 137 | else if (ua.indexOf("zunewp") !== -1) { 138 | return /\bzunewp([0-9.]+)/; 139 | } 140 | return "windows phone"; 141 | }], 142 | ["symbian", /\bsymbian(?:os)?\/([0-9.]+)/], 143 | ["chromeos", /\bcros i686 ([0-9.]+)/], 144 | ["linux", "linux"], 145 | ["windowsce", /\bwindows ce(?: ([0-9.]+))?/] 146 | ]; 147 | var BROWSER = [ 148 | // app 149 | ["micromessenger", /\bmicromessenger\/([\d.]+)/], 150 | ["qq", /\bqq/i], 151 | ["qzone", /qzone\/.*_qz_([\d.]+)/i], 152 | ["qqbrowser", /\bm?qqbrowser\/([0-9.]+)/], 153 | ["tt", /\btencenttraveler ([0-9.]+)/], 154 | ["weibo", /weibo__([0-9.]+)/], 155 | ["uc", function (ua) { 156 | if (ua.indexOf("ucbrowser/") >= 0) { 157 | return /\bucbrowser\/([0-9.]+)/; 158 | } 159 | else if (ua.indexOf("ubrowser/") >= 0) { 160 | return /\bubrowser\/([0-9.]+)/; 161 | } 162 | else if (/\buc\/[0-9]/.test(ua)) { 163 | return /\buc\/([0-9.]+)/; 164 | } 165 | else if (ua.indexOf("ucweb") >= 0) { 166 | // `ucweb/2.0` is compony info. 167 | // `UCWEB8.7.2.214/145/800` is browser info. 168 | return /\bucweb([0-9.]+)?/; 169 | } 170 | else { 171 | return /\b(?:ucbrowser|uc)\b/; 172 | } 173 | }], 174 | ["360", function (ua) { 175 | if (ua.indexOf("360 aphone browser") !== -1) { 176 | return /\b360 aphone browser \(([^\)]+)\)/; 177 | } 178 | return /\b360(?:se|ee|chrome|browser)\b/; 179 | }], 180 | ["baidu", function (ua) { 181 | var back = 0; 182 | var a; 183 | if (/ baiduboxapp\//i.test(ua)) { 184 | if (a = /([\d+.]+)_(?:diordna|enohpi)_/.exec(ua)) { 185 | a = a[1].split("."); 186 | back = a.reverse().join("."); 187 | } 188 | else if ((a = /baiduboxapp\/([\d+.]+)/.exec(ua))) { 189 | back = a[1]; 190 | } 191 | return { 192 | version: back, 193 | }; 194 | } 195 | return false; 196 | }, 197 | ], 198 | ["baidubrowser", /\b(?:ba?idubrowser|baiduhd)[ \/]([0-9.x]+)/], 199 | ["bdminivideo", /bdminivideo\/([0-9.]+)/], 200 | ["sogou", function (ua) { 201 | if (ua.indexOf("sogoumobilebrowser") >= 0) { 202 | return /sogoumobilebrowser\/([0-9.]+)/; 203 | } 204 | else if (ua.indexOf("sogoumse") >= 0) { 205 | return true; 206 | } 207 | return / se ([0-9.x]+)/; 208 | }], 209 | ["ali-ap", function (ua) { 210 | if (ua.indexOf("aliapp") > 0) { 211 | return /\baliapp\(ap\/([0-9.]+)\)/; 212 | } 213 | else { 214 | return /\balipayclient\/([0-9.]+)\b/; 215 | } 216 | }], 217 | ["ali-tb", /\baliapp\(tb\/([0-9.]+)\)/], 218 | ["ali-tm", /\baliapp\(tm\/([0-9.]+)\)/], 219 | ["tao", /\btaobrowser\/([0-9.]+)/], 220 | // 厂商 221 | ["mi", /\bmiuibrowser\/([0-9.]+)/], 222 | ["oppo", /\boppobrowser\/([0-9.]+)/], 223 | ["vivo", /\bvivobrowser\/([0-9.]+)/], 224 | ["meizu", /\bmzbrowser\/([0-9.]+)/], 225 | ["nokia", /\bnokiabrowser\/([0-9.]+)/], 226 | // ["huawei", /\bhuaweibrowser\/([0-9.]+)/], 227 | ["samsung", /\bsamsungbrowser\/([0-9.]+)/], 228 | // browser 229 | ["maxthon", /\b(?:maxthon|mxbrowser)(?:[ \/]([0-9.]+))?/], 230 | // Opera 15 之后开始使用 Chromniun 内核,需要放在 Chrome 的规则之前。 231 | ["opera", function (ua) { 232 | var re_opera_old = /\bopera.+version\/([0-9.ab]+)/; 233 | var re_opera_new = /\bopr\/([0-9.]+)/; 234 | return re_opera_old.test(ua) ? re_opera_old : re_opera_new; 235 | }], 236 | ["edge", /edge\/([0-9.]+)/], 237 | ["firefox", /\bfirefox\/([0-9.ab]+)/], 238 | ["chrome", / (?:chrome|crios|crmo)\/([0-9.]+)/], 239 | // Android 默认浏览器。该规则需要在 safari 之前。 240 | ["android", function (ua) { 241 | if (ua.indexOf("android") === -1) { 242 | return; 243 | } 244 | return /\bversion\/([0-9.]+(?: beta)?)/; 245 | }], 246 | ["safari", /\bversion\/([0-9.]+(?: beta)?)(?: mobile(?:\/[a-z0-9]+)?)? safari\//], 247 | // 如果不能识别为浏览器则为webview。 248 | ["webview", /\bcpu(?: iphone)? os (?:[0-9._]+).+\bapplewebkit\b/], 249 | ]; 250 | var detector = new Detector({ 251 | os: OS, 252 | browser: BROWSER 253 | }); 254 | var ua = navigator.userAgent + " " + navigator.appVersion + " " + navigator.vendor; 255 | exports.ua = ua; 256 | var d = detector.parse(ua); 257 | exports.detector = d; 258 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # web-launch-app 2 | 3 | ## Intro 4 | - 唤起App到指定页、通过Scheme调用端能力、下载安装包、到应用商店,同时提供了相关的detector、copy、util等功能 5 | 6 | ## Install 7 | - npm install web-launch-app --save 8 | - https://unpkg.com/web-launch-app@version/lib/wla.min.js (window.WLA) 9 | 10 | ## Usage 11 | 12 | ```javascript 13 | import { 14 | LaunchApp, detector, ua, copy, supportLink, 15 | isAndroid, isIos, inWeixin, inQQ, inWeibo, inBaidu 16 | } from 'web-launch-app'; 17 | 18 | const lanchApp = new LaunchApp(config); 19 | // 简单唤起 20 | lanchApp.open({ 21 | page: 'pagename/action', 22 | param:{ 23 | k: 'v' 24 | } 25 | }); 26 | 27 | // 复杂唤起 28 | lanchApp.open({ 29 | useYingyongbao: inWeixin && isAndroid, 30 | launchType: { 31 | ios: inWeixin ? 'store' : 'link', 32 | android: inWeixin ? 'store' : 'scheme', 33 | }, 34 | autodemotion: false, 35 | scheme: 'app://path?k=v', 36 | url: 'https://link.domain.com/path?k=v', 37 | param:{ 38 | k2: 'v2' 39 | }, 40 | timeout: 2000, 41 | pkgs:{ 42 | android: 'https://cdn.app.com/package/app20190501.apk', 43 | ios: 'https://itunes.apple.com/cn/app/appid123?mt=8', 44 | yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.app.www&ckey=CK123' 45 | } 46 | }, (s, d, url) => { 47 | console.log('callbackout', s, d, url); 48 | s != 1 && copy(url); 49 | return 2; 50 | }); 51 | 52 | // 下载 53 | lanchApp.download(); 54 | lanchApp.download({ 55 | android: 'https://cdn.app.com/package/app20190501.apk', 56 | ios: 'https://itunes.apple.com/cn/app/appid123?mt=8', 57 | yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.app.www&ckey=CK123', 58 | landPage: 'https://haokan.baidu.com/download' 59 | }); 60 | ``` 61 | 62 | ## API 63 | #### export 64 | - LaunchApp:唤起类,核心逻辑所在,提供了open()及download()方法通过不同方案实现唤起App及下载 65 | - copy:复制方法(浏览器安全限制,必须由用户行为触发) 66 | - detector:宿主环境对象(含os及browser信息) 67 | - ua:= navigator.userAgent + " " + navigator.appVersion + " " + navigator.vendor 68 | - isAndroid、isIos、inWeixin、inQQ、inWeibo、inBaidu:字面含义,Boolea值 69 | - supportLink:当前环境是否支持universal link或applink 70 | 71 | #### open(options, callback) 72 | |Param | |Notes| 73 | |------|--------|-----| 74 | |options|useGuideMethod| 是否使用引导提示,优先级高于launchType指定的方案(适用于微信、微博等受限环境),默认false | 75 | | |guideMethod| 引导提示方法,默认蒙层文案提示 | 76 | | |updateTipMethod| 版本升级提示方法,scheme配置指定版本时使用,默认alert提示 | 77 | | |useYingyongbao| launchType为store方案时(应用宝归为应用商店),控制微信中是否走应用宝,默认false | 78 | | |launchType| 【1】link:iOS9+使用universal link,Android6+使用applink,可配置指定link无法使用时自动降级为scheme。【2】scheme:scheme协议,通过唤起超时逻辑进行未唤起处理,同时适用于app内打开页面调用native功能。【3】store:系统应用商店(去应用宝需要指定useYingyongbao为true) | 79 | | |autodemotion| 是否支持link方案不可用时自动降级为scheme方案,(注意参数配置:使用page时要有同page下的link和scheme配置,或同时指定url及scheme参数),默认false | 80 | | |scheme| 指定scheme | 81 | | |callback| scheme的回调方法 | 82 | | |url| 指定link url(iOS的universal link值或Android的applink值) | 83 | | |page| 在config中配置的页面名称或端能力名称(替代scheme和url参数方便维护)| 84 | | |param| scheme或link的参数 | 85 | | |paramMap| 参数映射(适用于iOS与Android同scheme功能但参数名不同的情况,真实世界就是有这么多坑orz)| 86 | | |clipboardTxt| 复制到剪贴板内容(针对未安装或环境受限等唤起中断情况使用,在打开app或下载app后可以通过剪贴板内容进行交互衔接或统计),浏览器安全限制需要用户动作触发才能生效| 87 | | |timeout| scheme/store方案中超时时间,默认2000毫秒,<0表示不走超时逻辑 | 88 | | |landPage| 落地页面(异常或未知情况均会进入此页面) | 89 | | |pkgs| {android:'',ios:'',yyb:'',store:{...}},指定子项会覆盖基础配置 | 90 | |callback|| (s, d, url) => { return 0;} ,launchType为scheme或store方案时默认有超时逻辑,可通过设置tmieout为负值取消或根据callback中的返回值进行超时处理。s表示唤起结果(0失败,1成功,2未知), d为detector,url为最终的scheme或link值。无返回值默认下载apk包或去appstore,1不处理,2落地页,3应用市场(百度春晚活动时引导去应用市场下载分流减压),详见`LaunchApp.callbackResult`。 91 | 92 | 93 | #### download(options) 94 | |Param | |Notes| 95 | |------|--------|-----| 96 | |options ||未指定项使用实例配置中的默认值| 97 | ||yyb| 应用宝地址,在微信中使用 | 98 | ||android| android apk包下载地址 | 99 | ||ios| appstore地址 | 100 | ||landPage| 落地页地址,非iOS/Android平台使用 | 101 | 102 | 103 | ## Config 104 | ```javascript 105 | // 针对各种环境及方案参数有点多,需要使用者了解scheme及link本身的区别 106 | // 虽然config中很多参数可以在使用api时指定,还是建议在实例时全局配置,减少使用api时传参 107 | { 108 | inApp: false, // 是否是app内(在app内使用了指定version的scheme会进行版本检测) 109 | appVersion: '', // 对具体scheme链接进行版本检测时使用 110 | pkgName:'', // 应用商店使用 111 | deeplink:{ 112 | // 配置scheme方案具体页面及参数,生成请求格式为"protocol://path?param¶m" 113 | scheme: { 114 | android: { 115 | // 指定android的scheme协议头 116 | protocol: 'appname', 117 | index: { // 页面名或端能力名(默认请设置为:index) 118 | protocol: 'appname', // 可选,如无会读取上一级protocol,一般不需要配置 119 | path: 'path', 120 | param: {}, // 生成scheme或linkurl时的固定参数 121 | paramMap: {},// 参数映射,解决不同平台参数名不一至情况 122 | version: '4.9.6' // 版本要求 123 | }, 124 | share:{...}, 125 | ... 126 | }, 127 | ios: { 128 | ... 129 | } 130 | }, 131 | // 配置univerlink或applink方案中具体页面url及参数 132 | link: { 133 | pagename: { 134 | url: 'https://link.app.com/p/{forumName}', // 支持占位符 135 | param: { 136 | }, 137 | paramMap: { 138 | }, 139 | version: 0 140 | }, 141 | ... 142 | } 143 | }, 144 | // 下载包配置 145 | pkgs: { 146 | yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.baidu.haokan&ckey=', 147 | android: 'https://cdn.app.com/package/app-default.apk', 148 | ios: 'https://itunes.apple.com/cn/app/id1092031003?mt=8', 149 | store: { // android手机商店匹配,一般不需要配置 150 | android: { 151 | reg: /\(.*Android.*\)/, 152 | scheme: 'market://details?id=packagename' 153 | }, 154 | samsung: { 155 | reg: /\(.*Android.*(SAMSUNG|SM-|GT-).*\)/, 156 | scheme: 'samsungapps://ProductDetail/{id}' 157 | }, 158 | ... 159 | } 160 | }, 161 | useUniversalLink: supportLink(), // 默认根据环境判断 162 | useAppLink: supportLink(), // 默认根据环境判断 163 | autodemotion: false, // 不支持link方案时自动降级为scheme方案,默认false 164 | useYingyongbao: false, // 在微信中store方案时是否走应用宝,默认false 165 | useGuideMethod: false, // 使用guide方案 166 | guideMethod: ()=>{}, // 引导方法,默认蒙层文案提示 167 | updateTipMethod: ()=>{}, // scheme版本检测时升级提示 168 | searchPrefix: '?', // scheme或univerlink生成请求中参数前缀,默认为"?" 169 | timeout: 2000, // scheme/store方案中超时时间,默认2000毫秒,<0表示不走超时逻辑 170 | landPage: '', // 兜底页 171 | } 172 | ``` 173 | 174 | ## Demo 175 | ```javascript 176 | // ------------------------------------------------------------------- 177 | // launch-app.ts(基础文件,通过默认配置减少业务代码开发量,多模块使用建议提npm包) 178 | // ------------------------------------------------------------------- 179 | import { 180 | LaunchApp, copy, detector, ua, supportLink, 181 | isAndroid, isIos, inWeixin, inWeibo 182 | } from 'web-launch-app'; 183 | const inApp = /appname(.*)/.test(ua); 184 | const appVersion = inApp ? /appname\/(\d+(\.\d+)*)/.exec(ua)[1] : ''; 185 | const lanchIns = new LaunchApp({ 186 | inApp: inApp, 187 | appVersion: appVersion, 188 | pkgName: 'com.app.www', 189 | deeplink: { 190 | scheme: { 191 | android: { 192 | protocol: 'app', 193 | index: { 194 | path: '/', 195 | }, 196 | frs: { 197 | protocol: 'app', 198 | path: 'forum/detail', 199 | param: {from:'h5'}, 200 | paramMap: { 201 | forumName: 'kw' 202 | } 203 | } 204 | }, 205 | ios: { 206 | protocol: 'app', 207 | index: { 208 | path: '/', 209 | }, 210 | frs: { 211 | path: 'forum/detail' 212 | } 213 | } 214 | }, 215 | link: { 216 | index: {url: 'https://link.app.com'}, 217 | frs: {url: 'https://link.app.com/p/{forumName}'} 218 | }, 219 | }, 220 | pkgs: { 221 | android: 'https://cdn.app.com/package/app-defult.apk', 222 | ios: 'https://itunes.apple.com/app/apple-store/appid123?pt=328057&ct=MobileQQ_LXY&mt=8', 223 | yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.app.www&ckey=123', 224 | }, 225 | useUniversalLink: supportLink(), 226 | useAppLink: supportLink(), 227 | autodemotion: true, 228 | useYingyongbao: inWeixin && isAndroid, // 2019.7.16发布iOS微信7.0.5支持ulink 229 | useGuideMethod: inWeibo && isAndroid, // 受限情况下使用引导方案 230 | timeout: 2500, 231 | landPage: 'http://www.app.com/download' 232 | }); 233 | 234 | /** 235 | * 唤起app到指定页面 236 | * @param options 237 | * @param callback 238 | */ 239 | export function launch(options?: any, callback?: (status, detector, scheme) => number) { 240 | // pkgs处理 241 | options.pkgs = options.pkgs || {}; 242 | if(options.param && options.param.pkg){ 243 | options.pkgs.android = options.pkgs.android || `https://cdn.app.com/download/app-${pkg}.apk`; 244 | } 245 | if(options.param && options.param.ckey){ 246 | options.pkgs.android = options.pkgs.android || `http://a.app.qq.com/o/simple.jsp?pkgname=com.app.www&ckey=${ckey}`; 247 | } 248 | 249 | // 针对scheme方案处理剪贴板口令 250 | if (options.clipboardTxt === undefined) { 251 | let paramStr = options.param ? stringtifyParams(options.param) : ''; 252 | if (options.scheme) { 253 | options.clipboardTxt = '#' + options.scheme + (paramStr ? ((options.scheme.indexOf('?') > 0 ? '&' : '?') + paramStr) : '') + '#'; 254 | } else if (options.page) { 255 | // schemeConfig为实例化时参数中scheme配置 256 | options.clipboardTxt = '#' + schemeConfig['protocol'] + '://' + schemeConfig[options.page].path + (paramStr ? '?' + paramStr : '') + '#'; 257 | } 258 | } 259 | // TODO 处理唤起&新增统计归因、qq浏览器写入剪贴板延迟等通用性功能... 260 | lanchIns.open(options, callback); 261 | } 262 | 263 | /** 264 | * 唤起app到指定页面(尝试唤起场景,使用link方案,适用于不阻断用户继续去h5页体验场景) 265 | * @param options 266 | * @param callback 267 | */ 268 | export function tryLaunch(options?: any={}, callback?: (status, detector, scheme) => number) { 269 | options.launchType = { 270 | ios: 'link', 271 | android: 'link' 272 | }; 273 | options.autodemotion = false; 274 | launch(options); 275 | } 276 | 277 | /** 278 | * 唤起app到指定页面(强制唤起场景,使用scheme方案) 279 | */ 280 | export function forceLaunch(options?: any={}, callback?: (status, detector, scheme) => number) { 281 | options.launchType = { 282 | ios: 'scheme', 283 | android: 'scheme' 284 | }; 285 | launch(options); 286 | } 287 | 288 | /** 289 | * 唤起app到指定页面(常见场景方案,ios走link,android优先走link不支持走scheme,android微信中走应用宝) 290 | * @param options 291 | * @param callback 292 | */ 293 | export function hotLaunch(options?: any, callback?: (status, detector, scheme) => number) { 294 | options.useGuideMethod = isAndroid && inWeibo; 295 | options.launchType = { 296 | ios: 'link', 297 | android: inWeixin ? 'store' : (supportLink ? 'link' : 'scheme') 298 | }; 299 | options.useYingyongbao = isAndroid && inWeixin; 300 | options.autodemotion = true; 301 | 302 | launch(options, callback); 303 | } 304 | 305 | /** 306 | * 端内H5页面调用端能力 307 | */ 308 | export function invoke(options: any) { 309 | options.launchType = { 310 | ios: 'scheme', 311 | android: 'scheme' 312 | }; 313 | options.timeout = -1; 314 | lanchIns.open(options); 315 | } 316 | 317 | /** 318 | * 下载安装包 319 | * @param opt 320 | */ 321 | export function download(opt) { 322 | // TODO 参数处理... 323 | lanchIns.download(opt); 324 | } 325 | 326 | // ------------------------------------------------------------------- 327 | // demopage.ts(业务代码部分) 328 | // ------------------------------------------------------------------- 329 | import {launch, tryLaunch, forceLaunch, hotLaunch, invoke, download} from 'launch-app' 330 | // 唤起(微博出引导提示,ios微信去appstore,android微信去应用宝,同时指定超时处理及下载包) 331 | launch({ 332 | useGuideMethod: inWeibo, 333 | useYingyongbao: inWeixin && isAndroid, 334 | launchType: { 335 | ios: inWeixin ? 'store' : 'link', 336 | android: inWeixin ? 'store' : 'scheme' 337 | }, 338 | page: 'frs', 339 | param: { 340 | k: 'v', 341 | ckey: '123', 342 | pkg: '20190502', 343 | target: 'https://www.app.com/download', 344 | }, 345 | // scheme:'', 346 | // url:'https://link.app.com/path', 347 | // guideMethod: () => { 348 | // alert('请点击右上角,选择浏览器打开~'); 349 | // }, 350 | timeout: 2000, 351 | // clipboardTxt: '#key#', // launch-app中自动生成 352 | // pkgs: { 353 | // yyb: 'http://a.app.qq.com/o/simple.jsp?pkgname=com.app.www&ckey=123', // 通过ckey参数处理,不传使用默认值 354 | // android: 'https://cdn.app.com/package/app-20190502.apk', // 通过pkg参数处理 355 | // ios: 'https://itunes.apple.com/cn/app/appid123?mt=8' // 不传使用默认值 356 | // } 357 | }, (status, detector, url) => { 358 | console.log('callback', status, detector, url); 359 | // s != 1 && copy(url); 360 | return 0; 361 | }); 362 | 363 | // 尽量使用封装的tryLaunch、forceLaunch、hotLaunch、invoke方法,减少直接使用launch方法,便于扩展 364 | tryLaunch({ 365 | url: 'https://link.domain.com/path?k=v', 366 | param:{ 367 | k2: 'v2' 368 | } 369 | }) 370 | invoke({ 371 | page:'share', 372 | // scheme:'app://share', 373 | param:{ 374 | k:'v' 375 | } 376 | }); 377 | 378 | // 下载 379 | download(); 380 | download({ 381 | pkgs:{ 382 | ios: '', 383 | android: '', 384 | yyb: '', 385 | landPage:'' 386 | } 387 | }); 388 | ``` 389 | 390 | ## Who use? 391 | 好看视频、百度贴吧、伙拍小视频... 392 | - [好看视频的免费增长技术体系建设](https://juejin.im/post/5e778f8b518825494f7e1eac) 393 | -------------------------------------------------------------------------------- /lib/wla.min.js: -------------------------------------------------------------------------------- 1 | window.WLA=function(e){var o={};function n(t){if(o[t])return o[t].exports;var i=o[t]={i:t,l:!1,exports:{}};return e[t].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=o,n.d=function(e,o,t){n.o(e,o)||Object.defineProperty(e,o,{enumerable:!0,get:t})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,o){if(1&o&&(e=n(e)),8&o)return e;if(4&o&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(n.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&o&&"string"!=typeof e)for(var i in e)n.d(t,i,function(o){return e[o]}.bind(null,i));return t},n.n=function(e){var o=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(o,"a",o),o},n.o=function(e,o){return Object.prototype.hasOwnProperty.call(e,o)},n.p="",n(n.s=8)}([function(e,o,n){"use strict";Object.defineProperty(o,"__esModule",{value:!0});var t=n(1);o.isIos="ios"===t.detector.os.name,o.isAndroid="android"===t.detector.os.name,o.inWeixin="micromessenger"===t.detector.browser.name,o.inQQ="qq"===t.detector.browser.name,o.inWeibo="weibo"===t.detector.browser.name,o.inBaidu="baidu"===t.detector.browser.name,o.enableULink=o.isIos&&t.detector.os.version>=9,o.enableApplink=o.isAndroid&&t.detector.os.version>=6,o.isIOSWithLocationCallSupport=o.isIos&&"safari"==t.detector.browser.name&&t.detector.os.version>=9;var i="chrome"==t.detector.browser.name&&t.detector.browser.version>55,r="samsung"==t.detector.browser.name;o.isAndroidWithLocationCallSupport=o.isAndroid&&(i||r),o.supportLink=function(){var e=!1;if(o.enableApplink)switch(t.detector.browser.name){case"chrome":case"samsung":case"zhousi":e=!0;break;default:e=!1}if(o.enableULink)switch(t.detector.browser.name){case"uc":case"qq":e=!1;break;default:e=!0}return e},o.locationCall=function(e){(top.location||location).href=e},o.iframeCall=function(e){var o=document.createElement("iframe");o.setAttribute("src",e),o.setAttribute("style","display:none"),document.body.appendChild(o),setTimeout((function(){document.body.removeChild(o)}),200)},o.deepMerge=function(e,n){for(var t in n)e[t]=e[t]&&"[object Object]"===e[t].toString()?o.deepMerge(e[t],n[t]):e[t]=n[t];return e}},function(e,o,n){"use strict";function t(e){return function(o){return Object.prototype.toString.call(o)==="[object "+e+"]"}}Object.defineProperty(o,"__esModule",{value:!0});var i=function(){function e(e){this._rules=e}return e.prototype._detect=function(e,o,n){var i=t("Function")(o)?o.call(null,n):o;if(!i)return null;var r={name:e,version:"0",codename:""};if(!0===i)return r;if(t("String")(i)){if(-1!==n.indexOf(i))return r}else{if(t("Object")(i))return i.hasOwnProperty("version")&&(r.version=i.version),r;if(t("RegExp")(i)){var a=i.exec(n);if(a)return a.length>=2&&a[1]&&(r.version=a[1].replace(/_/g,".")),r}}},e.prototype._parseItem=function(e,o,n,t){var i=this,r={name:"na",version:"0"};!function(e,o){for(var n=0,t=e.length;n