diff --git a/packages/shims-node.d.ts b/packages/shims-node.d.ts index b9d4a0d0ed0b106018e3e295b9fbd7de9f054dad..086f7127a45f3dddc582838d6436e89df9be9435 100644 --- a/packages/shims-node.d.ts +++ b/packages/shims-node.d.ts @@ -12,6 +12,7 @@ declare namespace NodeJS { UNI_CLI_CONTEXT: string UNI_SUBPACKAGE?: string UNI_MP_PLUGIN?: 'true' + UNI_MP_UNIAD?: Boolean UNI_COMPILER_VERSION: string UNI_COMPILER_VERSION_TYPE: 'a' | 'r' UNI_HBUILDERX_PLUGINS: string diff --git a/packages/uni-components/lib/ad-fullscreen-video/ad-fullscreen-video.vue b/packages/uni-components/lib/ad-fullscreen-video/ad-fullscreen-video.vue new file mode 100644 index 0000000000000000000000000000000000000000..a87d851390a6f1955858accf63e1b610392b4535 --- /dev/null +++ b/packages/uni-components/lib/ad-fullscreen-video/ad-fullscreen-video.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/uni-components/lib/ad-interstitial/ad-interstitial.vue b/packages/uni-components/lib/ad-interstitial/ad-interstitial.vue new file mode 100644 index 0000000000000000000000000000000000000000..e7d509d200be4e29ea2dc69f65cc5aaa4b0ac78d --- /dev/null +++ b/packages/uni-components/lib/ad-interstitial/ad-interstitial.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/uni-components/lib/ad-rewarded-video/ad-rewarded-video.vue b/packages/uni-components/lib/ad-rewarded-video/ad-rewarded-video.vue new file mode 100644 index 0000000000000000000000000000000000000000..670bf54def92b3aa15541420442336d90046dab6 --- /dev/null +++ b/packages/uni-components/lib/ad-rewarded-video/ad-rewarded-video.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/uni-components/lib/ad/ad.mixin.js b/packages/uni-components/lib/ad/ad.mixin.js new file mode 100644 index 0000000000000000000000000000000000000000..c8635f51443062a446460a1b6145b2a63443ae5f --- /dev/null +++ b/packages/uni-components/lib/ad/ad.mixin.js @@ -0,0 +1,556 @@ +const ADType = { + RewardedVideo: 'RewardedVideo', + FullScreenVideo: 'FullScreenVideo', + Interstitial: 'Interstitial' +} + +const EventType = { + Load: 'load', + Close: 'close', + Error: 'error' +} + +const EXPIRED_TIME = 1000 * 60 * 30 +const ProviderType = { + CSJ: 'csj', + GDT: 'gdt' +} + +const RETRY_COUNT = 1 + +class AdBase { + constructor (adInstance, options = {}) { + this._isLoad = false + this._isLoading = false + this._isPlaying = false + this._lastLoadTime = 0 + this._lastError = null + this._retryCount = 0 + if (options.retry !== undefined) { + this._retry = options.retry + } else { + this._retry = true + } + + this._loadCallback = null + this._closeCallback = null + this._errorCallback = null + + const ad = this._ad = adInstance + ad.onLoad((e) => { + this._isLoading = false + this._isLoad = true + this._lastLoadTime = Date.now() + + this.onLoad() + }) + ad.onClose((e) => { + this._isLoad = false + this._isPlaying = false + this.onClose(e) + }) + ad.onVerify && ad.onVerify((e) => { + // e.isValid + }) + ad.onError(({ + code, + message + }) => { + this._isLoading = false + const data = { + code: code, + errMsg: message + } + + if (this._retry && code === -5008) { + this._loadAd() + return + } + + if (this._retry && this._retryCount < RETRY_COUNT) { + this._retryCount += 1 + this._loadAd() + return + } + + this._lastError = data + this.onError(data) + }) + } + + get isExpired () { + return (this._lastLoadTime !== 0 && (Math.abs(Date.now() - this._lastLoadTime) > EXPIRED_TIME)) + } + + get isLoad () { + return this._isLoad + } + + get isLoading () { + return this._isLoading + } + + getProvider () { + return this._ad.getProvider() + } + + load (onload, onerror) { + this._loadCallback = onload + this._errorCallback = onerror + + if (this._isPlaying) { + onerror && onerror() + return + } + + if (this._isLoading) { + return + } + + if (this._isLoad) { + this.onLoad() + return + } + + this._retryCount = 0 + + this._loadAd() + } + + show (onclose, onshow) { + this._closeCallback = onclose + + if (this._isLoading || this._isPlaying || !this._isLoad) { + return + } + + if (this._lastError !== null) { + this.onError(this._lastError) + return + } + + const provider = this.getProvider() + if (provider === ProviderType.CSJ && this.isExpired) { + if (this._retry) { + this._loadAd() + } else { + this.onError(this._lastError) + } + return + } + + this._isPlaying = true + this._ad.show() + onshow && onshow() + } + + onLoad (e) { + if (this._loadCallback != null) { + this._loadCallback() + } + } + + onClose (e) { + if (this._closeCallback != null) { + this._closeCallback({ + isEnded: e.isEnded + }) + } + } + + onError (e) { + if (this._errorCallback != null) { + this._errorCallback(e) + } + } + + destroy () { + this._ad.destroy() + } + + _loadAd () { + this._isLoad = false + this._isLoading = true + this._lastError = null + this._ad.load() + } +} + +class RewardedVideo extends AdBase { + constructor (options = {}) { + super(plus.ad.createRewardedVideoAd(options), options) + } +} + +class FullScreenVideo extends AdBase { + constructor (options = {}) { + super(plus.ad.createFullScreenVideoAd(options), options) + } +} + +class Interstitial extends AdBase { + constructor (options = {}) { + super(plus.ad.createInterstitialAd(options), options) + } +} + +class AdHelper { + constructor (adType) { + this._ads = {} + this._adType = adType + this._lastWaterfallIndex = -1 + } + + load (options, onload, onerror) { + if (!options.adpid || this.isBusy(options.adpid)) { + return + } + + this.get(options).load(onload, onerror) + } + + show (options, onload, onerror, onclose, onshow) { + const ad = this.get(options) + + if (ad.isLoad) { + this._lastWaterfallIndex = -1 + ad.show((e) => { + onclose && onclose(e) + }, () => { + onshow && onshow() + }) + } else { + ad.load(() => { + this._lastWaterfallIndex = -1 + onload && onload() + ad.show((e) => { + onclose && onclose(e) + }, () => { + onshow && onshow() + }) + }, (err) => { + onerror && onerror(err) + }) + } + } + + // 底价预载逻辑 + loadWaterfall (options, onload, onfail, index = 0) { + const { + adpid, + urlCallback + } = options + if (!Array.isArray(adpid)) { + return + } + + const options2 = { + adpid: adpid[index], + urlCallback, + retry: false + } + + console.log('ad.loadWaterfall::index=' + index) + + this.load(options2, (res) => { + this._lastWaterfallIndex = index + onload(options2) + }, (err) => { + index++ + if (index >= adpid.length) { + onfail(err) + } else { + this.loadWaterfall(options, onload, onfail, index) + } + }) + } + + // 底价逻辑,失败后下一个,无重试机制 + showWaterfall (options, onload, onfail, onclose, onshow, index = 0) { + const { + adpid, + urlCallback + } = options + if (!Array.isArray(adpid)) { + return + } + + if (this._lastWaterfallIndex > -1) { + index = this._lastWaterfallIndex + } + + const options2 = { + adpid: adpid[index], + urlCallback, + retry: false + } + + console.log('ad.showWaterfall::index=' + index) + + this.show(options2, () => { + onload() + }, (err) => { + index++ + if (index >= adpid.length) { + onfail(err) + } else { + this.showWaterfall(options, onload, onfail, onclose, onshow, index) + } + }, (res) => { + onclose(res) + }, () => { + onshow() + }) + } + + // 预载底价瀑布流 + preloadWaterfall (options, index = 0, step = 1) { + if (step === 1) { + this.loadWaterfall(options, (res) => { + console.log('preloadWaterfall.success::', res) + }, (err) => { + console.log('loadWaterfall.fail', err) + }) + return + } + + const { + adpid, + urlCallback + } = options + for (let i = 0; i < step; i++) { + if (index < adpid.length) { + const options2 = { + adpid: adpid[index], + urlCallback + } + this.loadWaterfall(options2, (res) => { + console.log('preloadWaterfall.success::', res) + }, (err) => { + console.log('loadWaterfall.fail', err) + this.preloadWaterfall(options, index, step) + }) + index++ + } else { + break + } + } + } + + isBusy (adpid) { + return (this._ads[adpid] && this._ads[adpid].isLoading) + } + + get (options) { + const { + adpid + } = options + + if (!this._ads[adpid]) { + this._ads[adpid] = this._createInstance(options) + } + + return this._ads[adpid] + } + + getProvider (adpid) { + if (this._ads[adpid]) { + return this._ads[adpid].getProvider() + } + return null + } + + remove (adpid) { + if (this._ads[adpid]) { + this._ads[adpid].destroy() + delete this._ads[adpid] + } + } + + _createInstance (options) { + const adType = options.adType || this._adType + delete options.adType + + let ad = null + if (adType === ADType.RewardedVideo) { + ad = new RewardedVideo(options) + } else if (adType === ADType.FullScreenVideo) { + ad = new FullScreenVideo(options) + } else if (adType === ADType.Interstitial) { + ad = new Interstitial(options, true) + } + + return ad + } +} + +export default { + props: { + options: { + type: [Object, Array], + default () { + return {} + } + }, + disabled: { + type: [Boolean, String], + default: false + }, + adpid: { + type: [Number, String, Array], + default: '' + }, + preload: { + type: [Boolean, String], + default: true + }, + loadnext: { + type: [Boolean, String], + default: false + }, + urlCallback: { + type: Object, + default () { + return {} + } + } + }, + data () { + return { + loading: false, + errorMessage: null + } + }, + created() { + this.$watch('adpid', (newValue, oldValue) => { + this._removeInstance(oldValue) + if (this.preload) { + this._loadAd() + } + }) + + // 服务器回调透传参数,仅在创建广告实例时可传递参数,如果发生变化需要重新创建广告实例 + this.$watch('urlCallback', () => { + this._removeInstance() + }) + + this._adHelper = new AdHelper(this.adType) + + setTimeout(() => { + if (this.preload) { + this._loadAd() + } + }, 100) + }, + methods: { + load () { + if (this.isLoading) { + return + } + this._startLoading() + const invoke = this._isWaterfall() ? 'loadWaterfall' : 'load' + this._adHelper[invoke](this._getAdOptions(), () => { + this._onLoad() + }, (err) => { + this._onLoadFail(err) + }) + }, + + show () { + if (this.isLoading) { + return + } + this._startLoading() + const invoke = this._isWaterfall() ? 'showWaterfall' : 'show' + this._adHelper[invoke](this._getAdOptions(), () => { + this._onLoad() + }, (err) => { + this._onLoadFail(err) + }, (res) => { + this._dispatchEvent(EventType.Close, res) + + if (this.loadnext) { + this.load() + } + }, () => { + // show + this.loading = false + }) + }, + + getProvider () { + if (Array.isArray(this.adpid)) { + return null + } + return this._adHelper.getProvider(this.adpid) + }, + + _loadAd () { + if (this._canCreateAd()) { + this.load() + } + }, + + _onclick () { + if (!this.disabled) { + this.show() + } + }, + + _getAdOptions () { + return { + adpid: this.adpid, + urlCallback: this.urlCallback + } + }, + + _isWaterfall () { + return (Array.isArray(this.adpid) && this.adpid.length > 0) + }, + + _canCreateAd () { + let result = false + if (Array.isArray(this.adpid) && this.adpid.length > 0) { + result = true + } else if (typeof this.adpid === 'string' && this.adpid.length > 0) { + result = true + } else if (typeof this.adpid === 'number') { + result = true + } + return result + }, + + _removeInstance (adpid) { + const id = adpid || this.adpid + if (Array.isArray(id)) { + id.forEach((item) => { + this._adHelper.remove(item) + }) + } else if (id) { + this._adHelper.remove(id) + } + }, + + _startLoading () { + this.loading = true + this.errorMessage = null + }, + + _onLoad () { + this.loading = false + this._dispatchEvent(EventType.Load, {}) + }, + + _onLoadFail (err) { + this.loading = false + this.errorMessage = JSON.stringify(err) + this._dispatchEvent(EventType.Error, err) + }, + + _dispatchEvent (type, data) { + this.$emit(type, { + detail: data + }) + } + } +} diff --git a/packages/uni-components/lib/ad/ad.mixin.mp.js b/packages/uni-components/lib/ad/ad.mixin.mp.js new file mode 100644 index 0000000000000000000000000000000000000000..ea507591de985a27d15ad98b85b41112158b52c6 --- /dev/null +++ b/packages/uni-components/lib/ad/ad.mixin.mp.js @@ -0,0 +1,104 @@ +const EventType = { + Load: 'load', + Close: 'close', + Error: 'error' +} + +export default { + props: { + options: { + type: [Object, Array], + default () { + return {} + } + }, + adpid: { + type: [Number, String], + default: '' + }, + unitId: { + type: [Number, String], + default: '' + }, + preload: { + type: [Boolean, String], + default: true + }, + loadnext: { + type: [Boolean, String], + default: false + } + }, + data () { + return { + loading: false, + errorMessage: null + } + }, + created () { + this._ad = null + setTimeout(() => { + if (this.preload && this._canCreateAd()) { + this.load() + } + }, 100) + }, + methods: { + load () { + if (this.loading) { + return + } + this._startLoading() + + setTimeout(() => { + this.loading = false + }, 3000) + }, + + show () { + this.errorMessage = null + this._ad = this.selectComponent('.uniad-plugin') + this._ad.show() + }, + + _onclick () { + this.show() + }, + + _startLoading () { + this.loading = true + this.errorMessage = null + }, + + _canCreateAd () { + let result = false + if (typeof this.adpid === 'string' && this.adpid.length > 0) { + result = true + } else if (typeof this.adpid === 'number') { + result = true + } + return result + }, + + _onmpload (e) { + this.loading = false + this._dispatchEvent(EventType.Load, {}) + }, + + _onmpclose (e) { + this._dispatchEvent(EventType.Close, e.detail) + }, + + _onmperror (e) { + this.loading = false + this.errorMessage = JSON.stringify(e.detail) + this._dispatchEvent(EventType.Error, e.detail) + }, + + _dispatchEvent (type, data) { + this.$emit(type, { + detail: data + }) + } + } +} diff --git a/packages/uni-components/lib/uniad/uniad.vue b/packages/uni-components/lib/uniad/uniad.vue new file mode 100644 index 0000000000000000000000000000000000000000..d2eeaee11d78540cd32040094b002f40b851ce89 --- /dev/null +++ b/packages/uni-components/lib/uniad/uniad.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/uni-mp-weixin/src/compiler/options.ts b/packages/uni-mp-weixin/src/compiler/options.ts index 7082dbd640970155bb7560844483c31260fd19d0..e67014b0969361c1e2ece43d7a503c7bb51988c6 100644 --- a/packages/uni-mp-weixin/src/compiler/options.ts +++ b/packages/uni-mp-weixin/src/compiler/options.ts @@ -8,6 +8,9 @@ import { transformRef, } from '@dcloudio/uni-cli-shared' import { UniMiniProgramPluginOptions } from '@dcloudio/uni-mp-vite' +import { transformAd } from './transforms/transformAd' + +import uniadAppJson from './uniad.app.json' import source from './project.config.json' @@ -19,7 +22,7 @@ export const customElements = [ ] export const compilerOptions: CompilerOptions = { - nodeTransforms: [transformRef, transformComponentLink], + nodeTransforms: [transformRef, transformComponentLink, transformAd], } const COMPONENTS_DIR = 'wxcomponents' @@ -97,6 +100,12 @@ export const options: UniMiniProgramPluginOptions = { darkmode: true, subpackages: true, plugins: true, + normalize(appJson) { + if ((process.env.UNI_MP_UNIAD = true)) { + uniadAppJson(appJson) + } + return appJson + }, }, project: { filename: projectConfigFilename, diff --git a/packages/uni-mp-weixin/src/compiler/transforms/transformAd.ts b/packages/uni-mp-weixin/src/compiler/transforms/transformAd.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd6e1aaf30c581626eea98d3fb778775427b9eee --- /dev/null +++ b/packages/uni-mp-weixin/src/compiler/transforms/transformAd.ts @@ -0,0 +1,32 @@ +import { isElementNode } from '@dcloudio/uni-cli-shared' +import { + findProp, + RootNode, + ElementTypes, + TemplateChildNode, + TransformContext, +} from '@vue/compiler-core' + +const AD_COMPONENTS: Array = [ + 'uniad', + 'ad-rewarded-video', + 'ad-fullscreen-video', + 'ad-interstitial', +] + +export function transformAd( + node: RootNode | TemplateChildNode, + context: TransformContext +) { + if (!isElementNode(node)) { + return + } + const adpidProp = findProp(node, 'adpid') + if (node.tag === 'ad' && adpidProp) { + node.tag = 'uniad' + node.tagType = ElementTypes.COMPONENT + } + if (AD_COMPONENTS.indexOf(node.tag) > -1) { + process.env.UNI_MP_UNIAD = true + } +} diff --git a/packages/uni-mp-weixin/src/compiler/uniad.app.json.js b/packages/uni-mp-weixin/src/compiler/uniad.app.json.js new file mode 100644 index 0000000000000000000000000000000000000000..c05b24ae4b68455570e29656010ef366feed1900 --- /dev/null +++ b/packages/uni-mp-weixin/src/compiler/uniad.app.json.js @@ -0,0 +1,24 @@ +module.exports = function (appJson) { + if (!appJson.plugins) { + appJson.plugins = {} + } + if (!appJson.plugins['uni-ad']) { + appJson.plugins['uni-ad'] = { + version: '1.0.3', + provider: 'wx999bf02c8e05dfc9', + } + } + if (!appJson.plugins['coral-adv']) { + appJson.plugins['coral-adv'] = { + version: '1.0.7', + provider: 'wx0e203209e27b1e66', + } + } + + if (!appJson.usingComponents) { + appJson.usingComponents = {} + } + if (!appJson.usingComponents['uniad-plugin']) { + appJson.usingComponents['uniad-plugin'] = 'plugin://uni-ad/ad' + } +}