diff --git a/packages/webview-bridge/.babelrc b/packages/webview-bridge/.babelrc new file mode 100644 index 0000000000..b1ba638280 --- /dev/null +++ b/packages/webview-bridge/.babelrc @@ -0,0 +1,5 @@ +{ + "presets": [ + ["@babel/env", {"modules": false}] + ] +} diff --git a/packages/webview-bridge/.gitignore b/packages/webview-bridge/.gitignore new file mode 100644 index 0000000000..9b1c8b133c --- /dev/null +++ b/packages/webview-bridge/.gitignore @@ -0,0 +1 @@ +/dist diff --git a/packages/webview-bridge/build/build.js b/packages/webview-bridge/build/build.js new file mode 100644 index 0000000000..7f6e4e7ecc --- /dev/null +++ b/packages/webview-bridge/build/build.js @@ -0,0 +1,83 @@ +const fs = require('fs') +const path = require('path') +const zlib = require('zlib') +const terser = require('terser') +const rollup = require('rollup') +const configs = require('./configs') + +if (!fs.existsSync('dist')) { + fs.mkdirSync('dist') +} + +build(Object.keys(configs).map(key => configs[key])) + +function build (builds) { + let built = 0 + const total = builds.length + const next = () => { + buildEntry(builds[built]).then(() => { + built++ + if (built < total) { + next() + } + }).catch(logError) + } + + next() +} + +function buildEntry ({ input, output }) { + const { file, banner } = output + const isProd = /min\.js$/.test(file) + return rollup.rollup(input) + .then(bundle => bundle.generate(output)) + .then(({ output: [{ code }] }) => { + if (isProd) { + const minified = (banner ? banner + '\n' : '') + terser.minify(code, { + toplevel: true, + output: { + ascii_only: true + }, + compress: { + pure_funcs: ['makeMap'] + } + }).code + return write(file, minified, true) + } else { + return write(file, code) + } + }) +} + +function write (dest, code, zip) { + return new Promise((resolve, reject) => { + function report (extra) { + console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || '')) + resolve() + } + + fs.writeFile(dest, code, err => { + if (err) return reject(err) + if (zip) { + zlib.gzip(code, (err, zipped) => { + if (err) return reject(err) + report(' (gzipped: ' + getSize(zipped) + ')') + }) + } else { + report() + } + }) + }) +} + +function getSize (code) { + return (code.length / 1024).toFixed(2) + 'kb' +} + +function logError (e) { + console.log(e) +} + +function blue (str) { + return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m' +} diff --git a/packages/webview-bridge/build/configs.js b/packages/webview-bridge/build/configs.js new file mode 100644 index 0000000000..81d83385ec --- /dev/null +++ b/packages/webview-bridge/build/configs.js @@ -0,0 +1,87 @@ +const path = require('path') +const babel = require('rollup-plugin-babel') +const replace = require('rollup-plugin-replace') +const version = process.env.VERSION || require('../package.json').version +const banner = +`/** + * mpxjs webview bridge v${version} + * (c) ${new Date().getFullYear()} @mpxjs team + * @license Apache + */` + +const resolve = _path => path.resolve(__dirname, '../', _path) + +const configs = { + umdDev: { + input: resolve('src/index.js'), + file: resolve('dist/webviewbridge.js'), + format: 'umd', + env: 'development' + }, + umdProd: { + input: resolve('src/index.js'), + file: resolve('dist/webviewbridge.min.js'), + format: 'umd', + env: 'production' + }, + esm: { + input: resolve('src/index.esm.js'), + file: resolve('dist/webviewbridge.esm.js'), + format: 'es' + }, + 'esm-browser-dev': { + input: resolve('src/index.esm.js'), + file: resolve('dist/webviewbridge.esm.browser.js'), + format: 'es', + env: 'development', + transpile: false + }, + 'esm-browser-prod': { + input: resolve('src/index.esm.js'), + file: resolve('dist/webviewbridge.esm.browser.min.js'), + format: 'es', + env: 'production', + transpile: false + } +} + +function genConfig (opts) { + const config = { + input: { + input: opts.input, + plugins: [ + replace({ + __VERSION__: version + }) + ] + }, + output: { + banner, + file: opts.file, + format: opts.format, + name: 'mpx' + } + } + + if (opts.env) { + config.input.plugins.unshift(replace({ + 'process.env.NODE_ENV': JSON.stringify(opts.env) + })) + } + + if (opts.transpile !== false) { + config.input.plugins.push(babel()) + } + + return config +} + +function mapValues (obj, fn) { + const res = {} + Object.keys(obj).forEach(key => { + res[key] = fn(obj[key], key) + }) + return res +} + +module.exports = mapValues(configs, genConfig) diff --git a/packages/webview-bridge/package.json b/packages/webview-bridge/package.json index 2beac6c534..6941e13f15 100644 --- a/packages/webview-bridge/package.json +++ b/packages/webview-bridge/package.json @@ -25,7 +25,17 @@ "url": "https://github.com/didi/mpx/issues" }, "scripts": { - "test": "echo \"Error: run tests from root\" && exit 1" + "test": "echo \"Error: run tests from root\" && exit 1", + "build": "node build/build.js" }, - "sideEffects": true + "sideEffects": true, + "devDependencies": { + "@babel/core": "^7.4.5", + "@babel/preset-env": "^7.4.5", + "rollup": "^1.12.3", + "rollup-plugin-babel": "^4.3.2", + "rollup-plugin-node-resolve": "^5.0.0", + "rollup-plugin-replace": "^2.2.0", + "terser": "^4.0.0" + } } diff --git a/packages/webview-bridge/src/index.esm.js b/packages/webview-bridge/src/index.esm.js new file mode 100644 index 0000000000..868db4fe31 --- /dev/null +++ b/packages/webview-bridge/src/index.esm.js @@ -0,0 +1,257 @@ +import loadScript from './loadscript' + +const SDK_URL_MAP = { + wx: 'https://res.wx.qq.com/open/js/jweixin-1.3.2.js', + ali: 'https://appx/web-view.min.js', + baidu: 'https://b.bdstatic.com/searchbox/icms/searchbox/js/swan-2.0.4.js', + tt: 'https://s3.pstatp.com/toutiao/tmajssdk/jssdk.js' +} + +const ENV_PATH_MAP = { + wx: ['wx', 'miniProgram'], + ali: ['my'], + baidu: ['swan', 'webView'] +} + +let env = null +// 环境判断 +if (navigator.userAgent.indexOf('AlipayClient') > -1) { + env = 'ali' +} else if (navigator.userAgent.indexOf('miniProgram') > -1) { + env = 'wx' +} else if (navigator.userAgent.indexOf('swan') > -1) { + env = 'baidu' +} + +if (env === null) { + console.error('mpxjs/webview: 未识别的环境,当前仅支持 微信、支付宝、百度 小程序') +} +const sdkReady = !window[env] ? SDK_URL_MAP[env] ? loadScript(SDK_URL_MAP[env]) : Promise.reject(new Error('未找到对应的sdk')) : Promise.resolve() + +let wxConfig = null + +// 微信的非小程序相关api需要config配置 +const sdkConfigReady = () => (env !== 'wx') ? sdkReady : new Promise((resolve, reject) => { + sdkReady.then(() => { + if (!window.wx) { + reject(new Error('sdk未就绪')) + } + + if (wxConfig === null) { + reject(new Error('wxSDK 未配置')) + } + + window.wx.config(wxConfig) + window.wx.ready(() => { + resolve() + }) + + window.wx.error((res) => { + reject(res) + }) + }) +}) + +const wxsdkConfig = (config) => { + wxConfig = config +} + +function getEnvWebviewVariable () { + return ENV_PATH_MAP[env].reduce((acc, cur) => acc[cur], window) +} + +function getEnvVariable () { + return window[ENV_PATH_MAP[env][0]] +} + +// key为导出的标准名,对应平台不支持的话为undefined +const ApiList = { + 'checkJSApi': { + wx: 'checkJSApi' + }, + 'chooseImage': { + wx: 'chooseImage', + baidu: 'chooseImage', + ali: 'chooseImage' + }, + 'previewImage': { + wx: 'previewImage', + baidu: 'previewImage', + ali: 'previewImage' + }, + 'uploadImage': { + wx: 'uploadImage' + }, + 'downloadImage': { + wx: 'downloadImage' + }, + 'getLocalImgData': { + wx: 'getLocalImgData' + }, + 'startRecord': { + wx: 'startRecord' + }, + 'stopRecord': { + wx: 'stopRecord' + }, + 'onVoiceRecordEnd': { + wx: 'onVoiceRecordEnd' + }, + 'playVoice': { + wx: 'playVoice' + }, + 'pauseVoice': { + wx: 'pauseVoice' + }, + 'stopVoice': { + wx: 'stopVoice' + }, + 'onVoicePlayEnd': { + wx: 'onVoicePlayEnd' + }, + 'uploadVoice': { + wx: 'uploadVoice' + }, + 'downloadVoice': { + wx: 'downloadVoice' + }, + 'translateVoice': { + wx: 'translateVoice' + }, + 'getNetworkType': { + wx: 'getNetworkType', + baidu: 'getNetworkType', + ali: 'getNetworkType' + }, + 'openLocation': { + wx: 'openLocation', + baidu: 'openLocation', + ali: 'openLocation' + }, + 'getLocation': { + wx: 'getLocation', + baidu: 'getLocation', + ali: 'getLocation' + }, + 'startSearchBeacons': { + wx: 'startSearchBeacons' + }, + 'stopSearchBeacons': { + wx: 'stopSearchBeacons' + }, + 'onSearchBeacons': { + wx: 'onSearchBeacons' + }, + 'scanQRCode': { + wx: 'scanQRCode' + }, + 'chooseCard': { + wx: 'chooseCard' + }, + 'addCard': { + wx: 'addCard' + }, + 'openCard': { + wx: 'openCard' + }, + 'alert': { + ali: 'alert' + }, + 'showLoading': { + ali: 'showLoading' + }, + 'hideLoading': { + ali: 'hideLoading' + }, + 'setStorage': { + ali: 'setStorage' + }, + 'getStorage': { + ali: 'getStorage' + }, + 'removeStorage': { + ali: 'removeStorage' + }, + 'clearStorage': { + ali: 'clearStorage' + }, + 'getStorageInfo': { + ali: 'getStorageInfo' + }, + 'startShare': { + ali: 'startShare' + }, + 'tradePay': { + ali: 'tradePay' + }, + 'onMessage': { + ali: 'onMessage' + } +} + +const exportApiList = {} + +for (let item in ApiList) { + exportApiList[item] = (...args) => { + if (!ApiList[item][env]) { + console.error(`此环境不支持${item}方法`) + } else { + sdkConfigReady().then(() => { + getEnvVariable()[ApiList[item][env]](...args) + }, (res) => { + console.error(res) + }).catch(e => console.error(e)) + } + } +} + +const webviewApiNameList = { + navigateTo: 'navigateTo', + navigateBack: 'navigateBack', + switchTab: 'switchTab', + reLaunch: 'reLaunch', + redirectTo: 'redirectTo', + getEnv: 'getEnv', + postMessage: 'postMessage', + onMessage: { + ali: true + } +} + +const webviewApiList = {} + +for (let item in webviewApiNameList) { + const apiName = typeof webviewApiNameList[item] === 'string' ? webviewApiNameList[item] : !webviewApiNameList[item][env] ? false : typeof webviewApiNameList[item][env] === 'string' ? webviewApiNameList[item][env] : item + + webviewApiList[item] = (...args) => { + if (!apiName) { + console.log(`${env}小程序不支持 ${item} 方法`) + } else { + sdkReady.then(() => { + getEnvWebviewVariable()[apiName](...args) + }, (res) => { + console.error(res) + }).catch(e => console.log(e)) + } + } +} + +const bridgeFunction = { + ...webviewApiList, + ...exportApiList, + wxsdkConfig +} + +const { navigateTo, navigateBack, switchTab, reLaunch, redirectTo, getEnv, postMessage } = webviewApiList +const { getLocation, chooseImage, openLocation, getNetworkType, previewImage } = exportApiList + +// 此处导出的对象包含所有的api +export default bridgeFunction + +export { + // 此处导出的为3个平台均可使用的api + navigateTo, navigateBack, switchTab, reLaunch, redirectTo, getEnv, postMessage, + getLocation, chooseImage, openLocation, getNetworkType, previewImage, + // 微信特有的一个配置sdk的方法 + wxsdkConfig +} diff --git a/packages/webview-bridge/src/index.js b/packages/webview-bridge/src/index.js index 868db4fe31..53a59d3160 100644 --- a/packages/webview-bridge/src/index.js +++ b/packages/webview-bridge/src/index.js @@ -242,16 +242,5 @@ const bridgeFunction = { wxsdkConfig } -const { navigateTo, navigateBack, switchTab, reLaunch, redirectTo, getEnv, postMessage } = webviewApiList -const { getLocation, chooseImage, openLocation, getNetworkType, previewImage } = exportApiList - // 此处导出的对象包含所有的api export default bridgeFunction - -export { - // 此处导出的为3个平台均可使用的api - navigateTo, navigateBack, switchTab, reLaunch, redirectTo, getEnv, postMessage, - getLocation, chooseImage, openLocation, getNetworkType, previewImage, - // 微信特有的一个配置sdk的方法 - wxsdkConfig -}