@zyqs-qiuyeyue/zz-wxams-ad 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/dist/components/pull-ad/index.js +354 -0
- package/dist/components/pull-ad/index.json +1 -0
- package/dist/components/pull-ad/index.wxml +22 -0
- package/dist/components/pull-ad/index.wxss +12 -0
- package/dist/components/zhizhu-ads/index.js +42 -0
- package/dist/components/zhizhu-ads/index.json +6 -0
- package/dist/components/zhizhu-ads/index.wxml +7 -0
- package/dist/components/zhizhu-ads/index.wxss +3 -0
- package/dist/index.js +12 -0
- package/dist/lib/client.js +108 -0
- package/dist/lib/constants.js +11 -0
- package/dist/lib/errors.js +27 -0
- package/dist/lib/navigator.js +90 -0
- package/dist/lib/renderer.js +18 -0
- package/dist/lib/transport.js +63 -0
- package/dist/lib/validator.js +131 -0
- package/dist/provider-config.js +13 -0
- package/package.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# 知蛛广告 npm 组件
|
|
2
|
+
|
|
3
|
+
此版本作为普通自定义组件运行在商家小程序内,不需要申请微信插件或配置 `app.json.plugins`。商家小程序本身仍按正常流程开发、审核和发布。
|
|
4
|
+
|
|
5
|
+
## 1. 安装与构建 npm
|
|
6
|
+
|
|
7
|
+
在小程序 npm 构建所使用的 `package.json` 所在目录执行:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @zyqs-qiuyeyue/zz-wxams-ad --save
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
如果没有 `package.json`,先运行 `npm init -y`。普通原生小程序建议将它放在 `miniprogramRoot` 指向的目录下;已有 npm 工程沿用原有位置及构建配置。
|
|
14
|
+
|
|
15
|
+
安装后,在微信开发者工具中执行“工具 → 构建 npm”,然后重新编译。每次更新安装包后都需要重新构建 npm。不使用 `plugin://` 或 `requirePlugin`。
|
|
16
|
+
|
|
17
|
+
## 2. 页面引用
|
|
18
|
+
|
|
19
|
+
页面 JSON:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"usingComponents": {
|
|
24
|
+
"zhizhu-ads": "@zyqs-qiuyeyue/zz-wxams-ad/components/zhizhu-ads/index"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
页面 WXML:
|
|
30
|
+
|
|
31
|
+
```xml
|
|
32
|
+
<zhizhu-ads
|
|
33
|
+
ad-code="服务商分配的广告位编码"
|
|
34
|
+
button-text="播放激励视频"
|
|
35
|
+
bind:load="onAdLoad"
|
|
36
|
+
bind:error="onAdError"
|
|
37
|
+
bind:close="onAdClose"
|
|
38
|
+
/>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
页面 JS(合并到现有 Page 中):
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
Page({
|
|
45
|
+
onAdLoad(event) { console.log("广告加载成功", event.detail); },
|
|
46
|
+
onAdError(event) { console.error("广告加载失败", event.detail); },
|
|
47
|
+
onAdClose(event) { console.log("广告关闭", event.detail); }
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
普通广告默认自动加载展示;激励视频需要点击播放按钮。无广告时组件隐藏。`provider`、`user-id` 为可选属性。
|
|
52
|
+
|
|
53
|
+
## 3. 可选初始化配置
|
|
54
|
+
|
|
55
|
+
默认请求正式环境 `https://api.zzspider.com`。在商家现有 `app.js` 的 `onLaunch` 中按需配置一次,所有组件共用:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const ads = require("@zyqs-qiuyeyue/zz-wxams-ad");
|
|
59
|
+
|
|
60
|
+
App({
|
|
61
|
+
onLaunch() {
|
|
62
|
+
ads.configure({
|
|
63
|
+
baseUrl: "https://api-test.zzspider.com",
|
|
64
|
+
webview_page: "pages/xx/webview?src="
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`webview_page` 可省略:默认半屏打开知蛛网络小程序 `wxf28dad03108653bc` 的 `pages/webview?src=`。传入时,打开商家已注册的 web-view 页面。广告 HTTPS URL 用 `encodeURIComponent` 编码后追加,承载页面读取 `options.src` 并解码一次。半屏不可用时尝试全屏,用户取消时不自动重试。
|
|
71
|
+
|
|
72
|
+
请求域名配置在商家小程序的 request 合法域名中;商家自己承载网页时还需配置业务域名。图片等资源按使用方式配置相应合法域名。目标小程序须已提供指定页面,并满足微信跳转条件。
|
|
73
|
+
|
|
74
|
+
微信广告单元必须与实际运行的商家小程序匹配。npm 版不会改变广告单元的归属、开通资格和微信校验规则。
|
|
75
|
+
|
|
76
|
+
## 4. 商家接口决定是否加载
|
|
77
|
+
|
|
78
|
+
```xml
|
|
79
|
+
<zhizhu-ads wx:if="{{showAd}}" ad-code="{{adCode}}" />
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`showAd` 初始为 `false`,商家接口成功后按业务结果设为 `true` 即可。无需额外调用加载方法,避免重复拉取。
|
|
83
|
+
|
|
84
|
+
## 服务商构建
|
|
85
|
+
|
|
86
|
+
在本目录执行:
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npm test
|
|
90
|
+
npm pack
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
源码来自仓库 `plugin/`,构建输出到 `dist/`;不要直接修改 `dist/`。修改源码后重新打包。交付包只包含运行文件与接入说明,不包含测试小程序或服务端代码。未来发布新包时更新本目录 `package.json` 的版本号。
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
var client = require("../../lib/client");
|
|
2
|
+
var validator = require("../../lib/validator");
|
|
3
|
+
var renderer = require("../../lib/renderer");
|
|
4
|
+
var navigator = require("../../lib/navigator");
|
|
5
|
+
var errors = require("../../lib/errors");
|
|
6
|
+
|
|
7
|
+
Component({
|
|
8
|
+
options: { styleIsolation: "isolated" },
|
|
9
|
+
properties: {
|
|
10
|
+
adCode: { type: String, value: "" },
|
|
11
|
+
provider: { type: String, value: "" },
|
|
12
|
+
userId: { type: String, value: "" },
|
|
13
|
+
ignoreAdUnitType: { type: Array, value: [] },
|
|
14
|
+
width: { type: String, value: "100%" },
|
|
15
|
+
autoLoad: { type: Boolean, value: true },
|
|
16
|
+
autoShow: { type: Boolean, value: true },
|
|
17
|
+
showClose: { type: Boolean, value: true },
|
|
18
|
+
showStatus: { type: Boolean, value: false },
|
|
19
|
+
buttonText: { type: String, value: "观看广告" }
|
|
20
|
+
},
|
|
21
|
+
data: {
|
|
22
|
+
pullData: null, kind: "hidden", mode: "", visible: false, loading: false,
|
|
23
|
+
nativeReady: false, errorText: "", navigationBusy: false, fullScreenRetry: false
|
|
24
|
+
},
|
|
25
|
+
observers: {
|
|
26
|
+
"adCode, provider, userId, ignoreAdUnitType": function () {
|
|
27
|
+
if (!this._attached) return;
|
|
28
|
+
this.reset();
|
|
29
|
+
if (this.properties.autoLoad) this.start().catch(function () {});
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
pageLifetimes: {
|
|
33
|
+
hide: function () { this._pageVisible = false; },
|
|
34
|
+
show: function () {
|
|
35
|
+
this._pageVisible = true;
|
|
36
|
+
if (this._attached && this.data.visible) this.watchVisibility();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
lifetimes: {
|
|
40
|
+
attached: function () {
|
|
41
|
+
this._attached = true;
|
|
42
|
+
this._pageVisible = true;
|
|
43
|
+
this._epoch = 0;
|
|
44
|
+
this.reset();
|
|
45
|
+
if (this.properties.autoLoad) this.start().catch(function () {});
|
|
46
|
+
},
|
|
47
|
+
detached: function () {
|
|
48
|
+
this._attached = false;
|
|
49
|
+
this._epoch += 1;
|
|
50
|
+
if (this._scope) this._scope.cancel();
|
|
51
|
+
this.teardown();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
methods: {
|
|
55
|
+
reset: function () {
|
|
56
|
+
this._epoch = (this._epoch || 0) + 1;
|
|
57
|
+
if (this._scope) this._scope.cancel();
|
|
58
|
+
this._scope = client.createRequestScope();
|
|
59
|
+
this.teardown();
|
|
60
|
+
this._pull = null;
|
|
61
|
+
this._view = null;
|
|
62
|
+
this._loading = null;
|
|
63
|
+
this._showing = null;
|
|
64
|
+
this._consumed = false;
|
|
65
|
+
this._failed = false;
|
|
66
|
+
this._fallbackTried = false;
|
|
67
|
+
this._ignoreWechatAds = false;
|
|
68
|
+
this._wantShow = false;
|
|
69
|
+
this._observed = {};
|
|
70
|
+
this._interaction = "";
|
|
71
|
+
this.setData({ pullData: null, kind: "hidden", mode: "", visible: false,
|
|
72
|
+
loading: false, nativeReady: false, errorText: "", navigationBusy: false, fullScreenRetry: false });
|
|
73
|
+
},
|
|
74
|
+
start: function () {
|
|
75
|
+
return this.prepare().then(function (pull) {
|
|
76
|
+
if (pull && pull === this._pull && pull.enabled && this.properties.autoShow
|
|
77
|
+
&& this._view.mode !== "REWARDED") return this.show();
|
|
78
|
+
return pull;
|
|
79
|
+
}.bind(this));
|
|
80
|
+
},
|
|
81
|
+
prepare: function () {
|
|
82
|
+
if (this._loading) return this._loading;
|
|
83
|
+
if (this._pull && !this._consumed) return Promise.resolve(this._pull);
|
|
84
|
+
if (this._pull) this.reset();
|
|
85
|
+
var epoch = this._epoch;
|
|
86
|
+
this.setData({ loading: true, errorText: "" });
|
|
87
|
+
var promise = client.pull({ spaceCode: this.properties.adCode, provider: this.properties.provider,
|
|
88
|
+
userId: this.properties.userId, ignoreAdUnitType: this.properties.ignoreAdUnitType,
|
|
89
|
+
ignoreWechatAds: this._ignoreWechatAds, requestScope: this._scope }).then(function (pull) {
|
|
90
|
+
if (!this._attached || epoch !== this._epoch) return null;
|
|
91
|
+
var view = renderer.describe(pull);
|
|
92
|
+
this._pull = pull;
|
|
93
|
+
this._view = view;
|
|
94
|
+
this._consumed = false;
|
|
95
|
+
this._failed = false;
|
|
96
|
+
this._observed = {};
|
|
97
|
+
this._imageLoaded = false;
|
|
98
|
+
this._inViewport = false;
|
|
99
|
+
this.setData({ pullData: pull, kind: view.kind, mode: view.mode,
|
|
100
|
+
visible: false, loading: false, nativeReady: false });
|
|
101
|
+
this.triggerEvent(pull.enabled ? "ready" : "disabled", { pull: pull });
|
|
102
|
+
if (pull.enabled && view.kind === "wechat_api") {
|
|
103
|
+
this.createNative(pull, epoch);
|
|
104
|
+
if (view.mode === "REWARDED") return Promise.resolve(this._native.load()).then(function () { return pull; });
|
|
105
|
+
}
|
|
106
|
+
return pull;
|
|
107
|
+
}.bind(this)).catch(function (error) {
|
|
108
|
+
if (this._attached && epoch === this._epoch) {
|
|
109
|
+
this.setData({ loading: false });
|
|
110
|
+
if (this._pull && this._pull.enabled) this.sourceFailure(error);
|
|
111
|
+
else this.emitError(error);
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}.bind(this));
|
|
115
|
+
this._loading = promise;
|
|
116
|
+
promise.then(function () { if (this._loading === promise) this._loading = null; }.bind(this),
|
|
117
|
+
function () { if (this._loading === promise) this._loading = null; }.bind(this));
|
|
118
|
+
return promise;
|
|
119
|
+
},
|
|
120
|
+
refresh: function () { this.reset(); return this.start(); },
|
|
121
|
+
show: function () {
|
|
122
|
+
if (this._showing) return this._showing;
|
|
123
|
+
if (this.data.visible || this._modalOpen || this._nativeVisible) return Promise.resolve(this._pull);
|
|
124
|
+
this._wantShow = true;
|
|
125
|
+
var pending = this._pull && !this._consumed ? this.display()
|
|
126
|
+
: this.prepare().then(function (pull) { return pull && pull === this._pull ? this.display() : null; }.bind(this));
|
|
127
|
+
this._showing = Promise.resolve(pending);
|
|
128
|
+
var promise = this._showing;
|
|
129
|
+
promise.then(function () { if (this._showing === promise) this._showing = null; }.bind(this),
|
|
130
|
+
function () { if (this._showing === promise) this._showing = null; }.bind(this));
|
|
131
|
+
return promise;
|
|
132
|
+
},
|
|
133
|
+
display: function () {
|
|
134
|
+
var pull = this._pull;
|
|
135
|
+
if (!this._attached || !pull || !pull.enabled) return Promise.resolve(pull);
|
|
136
|
+
if (this._view.kind === "modal") return this.showModal();
|
|
137
|
+
if (this._view.kind === "wechat_api") {
|
|
138
|
+
if (!this._native) return Promise.reject(new Error("微信广告尚未就绪"));
|
|
139
|
+
var epoch = this._epoch;
|
|
140
|
+
this._nativeVisible = true;
|
|
141
|
+
var shown;
|
|
142
|
+
try { shown = this._native.show(); } catch (error) { shown = Promise.reject(error); }
|
|
143
|
+
return Promise.resolve(shown).then(function () {
|
|
144
|
+
if (this._attached && epoch === this._epoch) this.observe("impression");
|
|
145
|
+
return pull;
|
|
146
|
+
}.bind(this)).catch(function (error) {
|
|
147
|
+
if (this._attached && epoch === this._epoch) { this._nativeVisible = false; this.sourceFailure(error); }
|
|
148
|
+
throw error;
|
|
149
|
+
}.bind(this));
|
|
150
|
+
}
|
|
151
|
+
this.setData({ visible: true }, this.watchVisibility.bind(this));
|
|
152
|
+
return Promise.resolve(pull);
|
|
153
|
+
},
|
|
154
|
+
createNative: function (pull, epoch) {
|
|
155
|
+
var rewarded = this._view.mode === "REWARDED";
|
|
156
|
+
var method = rewarded ? "createRewardedVideoAd" : "createInterstitialAd";
|
|
157
|
+
if (typeof wx === "undefined" || typeof wx[method] !== "function") throw new Error("当前基础库不支持该微信广告");
|
|
158
|
+
var ad = wx[method]({ adUnitId: pull.adUnitId });
|
|
159
|
+
this._native = ad;
|
|
160
|
+
var active = function () { return this._attached && epoch === this._epoch && this._native === ad; }.bind(this);
|
|
161
|
+
this._onLoad = function () {
|
|
162
|
+
if (!active()) return;
|
|
163
|
+
this.setData({ nativeReady: true });
|
|
164
|
+
this.observe("load");
|
|
165
|
+
this.triggerEvent("load", { pull: pull });
|
|
166
|
+
}.bind(this);
|
|
167
|
+
this._onError = function (error) { if (active()) this.sourceFailure(error); }.bind(this);
|
|
168
|
+
this._onClose = function (result) {
|
|
169
|
+
if (!active() || this._consumed || !this._nativeVisible) return;
|
|
170
|
+
this._nativeVisible = false;
|
|
171
|
+
this._consumed = true;
|
|
172
|
+
this.observe("close");
|
|
173
|
+
this.triggerEvent("close", { pull: pull });
|
|
174
|
+
if (rewarded) {
|
|
175
|
+
var completed = Boolean(result && result.isEnded === true);
|
|
176
|
+
if (completed) this.observe("reward");
|
|
177
|
+
this.triggerEvent("complete", { isFinished: completed, pull: pull });
|
|
178
|
+
}
|
|
179
|
+
}.bind(this);
|
|
180
|
+
ad.onLoad(this._onLoad);
|
|
181
|
+
ad.onError(this._onError);
|
|
182
|
+
ad.onClose(this._onClose);
|
|
183
|
+
},
|
|
184
|
+
teardown: function () {
|
|
185
|
+
if (this._observer) { this._observer.disconnect(); this._observer = null; }
|
|
186
|
+
var ad = this._native;
|
|
187
|
+
this._native = null;
|
|
188
|
+
this._nativeVisible = false;
|
|
189
|
+
this._modalOpen = false;
|
|
190
|
+
if (!ad) return;
|
|
191
|
+
if (ad.offLoad) ad.offLoad(this._onLoad);
|
|
192
|
+
if (ad.offError) ad.offError(this._onError);
|
|
193
|
+
if (ad.offClose) ad.offClose(this._onClose);
|
|
194
|
+
if (ad.destroy) ad.destroy();
|
|
195
|
+
},
|
|
196
|
+
sourceFailure: function (error) {
|
|
197
|
+
if (!this._attached || this._failed) return;
|
|
198
|
+
this._failed = true;
|
|
199
|
+
this.observe("creative_error", { message: String(error && (error.errMsg || error.message) || "广告加载失败") });
|
|
200
|
+
this.emitError(error);
|
|
201
|
+
var pull = this._pull;
|
|
202
|
+
var fallback = pull && pull.sourceType === "wechatmini"
|
|
203
|
+
&& pull.wxAdResultAction === "continue_pull_zzad" && !this._fallbackTried;
|
|
204
|
+
var wantShow = this._wantShow || (this.properties.autoShow && this._view.mode !== "REWARDED");
|
|
205
|
+
this._epoch += 1;
|
|
206
|
+
if (this._scope) this._scope.cancel();
|
|
207
|
+
this._scope = client.createRequestScope();
|
|
208
|
+
this.teardown();
|
|
209
|
+
this._loading = null;
|
|
210
|
+
this._showing = null;
|
|
211
|
+
this.setData({ visible: false, kind: "hidden", loading: false, nativeReady: false });
|
|
212
|
+
if (!fallback) {
|
|
213
|
+
this._pull = { spaceCode: pull.spaceCode, pullId: "", enabled: false };
|
|
214
|
+
this.triggerEvent("disabled", { pull: this._pull, reason: "creative_error", error: errors.toPublicError(error) });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
this._pull = null;
|
|
218
|
+
this._fallbackTried = true;
|
|
219
|
+
this._ignoreWechatAds = true;
|
|
220
|
+
this.prepare().then(function (next) {
|
|
221
|
+
if (!next || next !== this._pull) return;
|
|
222
|
+
this.triggerEvent("fallback", { pull: next });
|
|
223
|
+
if (wantShow && next.enabled) this.show().catch(function () {});
|
|
224
|
+
}.bind(this)).catch(function () {});
|
|
225
|
+
},
|
|
226
|
+
showModal: function () {
|
|
227
|
+
if (this._modalOpen) return Promise.resolve(this._pull);
|
|
228
|
+
if (typeof wx === "undefined" || typeof wx.showModal !== "function") {
|
|
229
|
+
var missing = new Error("当前基础库不支持系统弹窗");
|
|
230
|
+
this.sourceFailure(missing);
|
|
231
|
+
return Promise.reject(missing);
|
|
232
|
+
}
|
|
233
|
+
var epoch = this._epoch;
|
|
234
|
+
var pull = this._pull;
|
|
235
|
+
var action = pull.creative.action;
|
|
236
|
+
this._modalOpen = true;
|
|
237
|
+
this._interaction = this.newInteraction();
|
|
238
|
+
try {
|
|
239
|
+
wx.showModal({
|
|
240
|
+
title: action.modalTitle, content: action.modalContent,
|
|
241
|
+
confirmText: action.modalConfirmText, confirmColor: action.modalConfirmColor,
|
|
242
|
+
showCancel: action.modalShowCancel, cancelText: action.modalCancelText, cancelColor: action.modalCancelColor,
|
|
243
|
+
success: function (result) {
|
|
244
|
+
if (!this._attached || this._consumed || epoch !== this._epoch) return;
|
|
245
|
+
this._modalOpen = false;
|
|
246
|
+
if (result.confirm) this.navigate(false);
|
|
247
|
+
else { this.observe("navigate_cancel", {}, this._interaction); this.close(); }
|
|
248
|
+
this.observe("impression");
|
|
249
|
+
}.bind(this),
|
|
250
|
+
fail: function (error) {
|
|
251
|
+
if (!this._attached || epoch !== this._epoch) return;
|
|
252
|
+
this._modalOpen = false;
|
|
253
|
+
this.sourceFailure(error);
|
|
254
|
+
}.bind(this)
|
|
255
|
+
});
|
|
256
|
+
} catch (error) { this._modalOpen = false; this.sourceFailure(error); return Promise.reject(error); }
|
|
257
|
+
return Promise.resolve(pull);
|
|
258
|
+
},
|
|
259
|
+
newInteraction: function () {
|
|
260
|
+
return "act_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 12);
|
|
261
|
+
},
|
|
262
|
+
handleTap: function () {
|
|
263
|
+
if (this.data.navigationBusy || !this.data.visible || !this._pull.creative
|
|
264
|
+
|| this._pull.creative.action.type === "none") return;
|
|
265
|
+
this._interaction = this.newInteraction();
|
|
266
|
+
return this.navigate(false);
|
|
267
|
+
},
|
|
268
|
+
handleFullScreenTap: function () {
|
|
269
|
+
if (this.data.navigationBusy || !this.data.fullScreenRetry) return;
|
|
270
|
+
return this.navigate(true);
|
|
271
|
+
},
|
|
272
|
+
navigate: function (forceFullScreen) {
|
|
273
|
+
if (!this._attached || this.data.navigationBusy || !this._pull || !this._pull.creative) return;
|
|
274
|
+
var action = this._pull.creative.action;
|
|
275
|
+
var epoch = this._epoch;
|
|
276
|
+
var interaction = this._interaction || this.newInteraction();
|
|
277
|
+
this._interaction = interaction;
|
|
278
|
+
this.setData({ navigationBusy: true, fullScreenRetry: false });
|
|
279
|
+
var pending = navigator.perform(action, {
|
|
280
|
+
forceFullScreen: forceFullScreen,
|
|
281
|
+
isActive: function () { return this._attached && !this._consumed && epoch === this._epoch; }.bind(this),
|
|
282
|
+
onFallback: function (detail) { this.observe("half_screen_fallback", detail, interaction); }.bind(this)
|
|
283
|
+
});
|
|
284
|
+
if (!forceFullScreen && action.type !== "none") this.observe("click", {}, interaction);
|
|
285
|
+
return pending.then(function (result) {
|
|
286
|
+
if (!this._attached || this._consumed || epoch !== this._epoch || result.disposed) return;
|
|
287
|
+
this.setData({ navigationBusy: false });
|
|
288
|
+
if (!result.skipped) this.observe("navigate_success", { mode: result.mode }, interaction);
|
|
289
|
+
this.triggerEvent("action", { status: !result.skipped, action: action, result: result });
|
|
290
|
+
if (this.data.mode === "INTERSTITIAL") this.close();
|
|
291
|
+
}.bind(this)).catch(function (error) {
|
|
292
|
+
if (!this._attached || this._consumed || epoch !== this._epoch) return;
|
|
293
|
+
this.setData({ navigationBusy: false });
|
|
294
|
+
if (error.code === "NAVIGATION_CANCELLED") {
|
|
295
|
+
this.observe("navigate_cancel", {}, interaction);
|
|
296
|
+
this.triggerEvent("action", { status: false, cancelled: true, action: action });
|
|
297
|
+
if (this.data.mode === "INTERSTITIAL") this.close();
|
|
298
|
+
} else if (error.code === "NAVIGATION_GESTURE_REQUIRED" && !forceFullScreen) {
|
|
299
|
+
this.setData({ fullScreenRetry: true });
|
|
300
|
+
this.triggerEvent("action", { status: false, requiresGesture: true, action: action });
|
|
301
|
+
} else {
|
|
302
|
+
this.observe("navigate_fail", { message: error.message }, interaction);
|
|
303
|
+
this.emitError(error);
|
|
304
|
+
}
|
|
305
|
+
}.bind(this));
|
|
306
|
+
},
|
|
307
|
+
close: function () {
|
|
308
|
+
if (!this._attached || !this._pull || !this._pull.enabled || this._consumed || this._nativeVisible || this._modalOpen) return false;
|
|
309
|
+
if (this.data.fullScreenRetry) this.observe("navigate_cancel", {}, this._interaction);
|
|
310
|
+
this._consumed = true;
|
|
311
|
+
this.setData({ visible: false, fullScreenRetry: false });
|
|
312
|
+
if (this._observer) { this._observer.disconnect(); this._observer = null; }
|
|
313
|
+
this.observe("close");
|
|
314
|
+
this.triggerEvent("close", { pull: this._pull });
|
|
315
|
+
},
|
|
316
|
+
watchVisibility: function () {
|
|
317
|
+
if (!this._attached || !this.data.visible || typeof this.createIntersectionObserver !== "function") return;
|
|
318
|
+
if (this._observer) this._observer.disconnect();
|
|
319
|
+
var epoch = this._epoch;
|
|
320
|
+
this._observer = this.createIntersectionObserver({ thresholds: [0, 0.01] });
|
|
321
|
+
this._observer.relativeToViewport().observe("#adSurface", function (result) {
|
|
322
|
+
if (!this._attached || epoch !== this._epoch || !this.data.visible) return;
|
|
323
|
+
this._inViewport = result.intersectionRatio > 0;
|
|
324
|
+
if (this._pageVisible !== false && this._inViewport && this._imageLoaded) this.observe("impression");
|
|
325
|
+
}.bind(this));
|
|
326
|
+
},
|
|
327
|
+
currentEvent: function (event) {
|
|
328
|
+
return this._attached && !this._consumed && this._pull && event.currentTarget
|
|
329
|
+
&& event.currentTarget.dataset.pullId === this._pull.pullId;
|
|
330
|
+
},
|
|
331
|
+
handleLoad: function (event) {
|
|
332
|
+
if (!this.currentEvent(event)) return;
|
|
333
|
+
this._imageLoaded = true;
|
|
334
|
+
this.observe("load");
|
|
335
|
+
if (this._pageVisible !== false && this._inViewport && this.data.visible) this.observe("impression");
|
|
336
|
+
this.triggerEvent("load", { pull: this._pull });
|
|
337
|
+
},
|
|
338
|
+
handleError: function (event) { if (this.currentEvent(event)) this.sourceFailure(event.detail || {}); },
|
|
339
|
+
observe: function (type, context, interactionId) {
|
|
340
|
+
if (!this._attached || !this._pull || !this._pull.enabled) return;
|
|
341
|
+
var key = type + ":" + (interactionId || "");
|
|
342
|
+
if (this._observed[key]) return;
|
|
343
|
+
this._observed[key] = true;
|
|
344
|
+
this.triggerEvent("interaction", { eventType: type, pullId: this._pull.pullId,
|
|
345
|
+
spaceCode: this._pull.spaceCode, interactionId: interactionId || "", context: context || {} });
|
|
346
|
+
},
|
|
347
|
+
emitError: function (error) {
|
|
348
|
+
var result = errors.toPublicError(error);
|
|
349
|
+
this.setData({ errorText: result.message });
|
|
350
|
+
this.triggerEvent("error", result);
|
|
351
|
+
},
|
|
352
|
+
handleShowTap: function () { this.show().catch(function () {}); }
|
|
353
|
+
}
|
|
354
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "component": true }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<view wx:if="{{visible && (kind === 'image' || kind === 'wechat_inline' || kind === 'wechat_template')}}" class="{{mode === 'INTERSTITIAL' ? 'overlay' : 'inline'}}" style="{{mode === 'INTERSTITIAL' ? '' : 'width:' + width}}">
|
|
2
|
+
<view id="adSurface" class="surface {{mode === 'INTERSTITIAL' ? 'dialog' : ''}}">
|
|
3
|
+
<block wx:if="{{kind === 'image'}}">
|
|
4
|
+
<button class="image-button" bindtap="handleTap" disabled="{{navigationBusy || fullScreenRetry}}" aria-label="查看广告">
|
|
5
|
+
<image src="{{pullData.creative.imageUrl}}" mode="widthFix" class="image" data-pull-id="{{pullData.pullId}}" bindload="handleLoad" binderror="handleError" />
|
|
6
|
+
</button>
|
|
7
|
+
</block>
|
|
8
|
+
<ad-custom wx:elif="{{kind === 'wechat_template'}}" unit-id="{{pullData.adUnitId}}" data-pull-id="{{pullData.pullId}}" bindload="handleLoad" binderror="handleError" />
|
|
9
|
+
<ad wx:else unit-id="{{pullData.adUnitId}}" ad-type="{{mode === 'VIDEO' ? 'video' : 'banner'}}" data-pull-id="{{pullData.pullId}}" bindload="handleLoad" binderror="handleError" />
|
|
10
|
+
<button wx:if="{{mode === 'INTERSTITIAL' && showClose}}" class="close" bindtap="close" aria-label="关闭广告">×</button>
|
|
11
|
+
</view>
|
|
12
|
+
</view>
|
|
13
|
+
<view wx:if="{{fullScreenRetry}}" class="retry-overlay">
|
|
14
|
+
<view class="retry-dialog">
|
|
15
|
+
<text>请点击按钮全屏打开</text>
|
|
16
|
+
<button type="primary" bindtap="handleFullScreenTap" disabled="{{navigationBusy}}">全屏打开</button>
|
|
17
|
+
<button bindtap="close">取消</button>
|
|
18
|
+
</view>
|
|
19
|
+
</view>
|
|
20
|
+
<button wx:if="{{mode === 'REWARDED' && pullData.enabled && buttonText}}" bindtap="handleShowTap" disabled="{{loading || !nativeReady}}">{{buttonText}}</button>
|
|
21
|
+
<view wx:if="{{showStatus && loading}}" class="status">广告加载中</view>
|
|
22
|
+
<view wx:if="{{showStatus && errorText}}" class="status">{{errorText}}</view>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
:host { display: block; }
|
|
2
|
+
.inline, .surface { position: relative; }
|
|
3
|
+
.image-button { margin: 0; padding: 0; border: 0; background: transparent; line-height: 0; border-radius: 0; }
|
|
4
|
+
.image-button::after { border: 0; }
|
|
5
|
+
.image { display: block; width: 100%; }
|
|
6
|
+
.overlay, .retry-overlay { position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 48rpx; background: rgba(0,0,0,.58); }
|
|
7
|
+
.dialog { width: 100%; max-width: 650rpx; max-height: 85vh; overflow-y: auto; }
|
|
8
|
+
.close { position: absolute; top: 8rpx; right: 8rpx; width: 60rpx; height: 60rpx; padding: 0; border-radius: 50%; line-height: 56rpx; font-size: 36rpx; color: #fff; background: rgba(0,0,0,.6); }
|
|
9
|
+
.retry-overlay { z-index: 10001; }
|
|
10
|
+
.retry-dialog { width: 560rpx; padding: 36rpx; border-radius: 20rpx; background: #fff; text-align: center; }
|
|
11
|
+
.retry-dialog button { margin-top: 24rpx; }
|
|
12
|
+
.status { color: #888; font-size: 24rpx; padding: 16rpx; }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Component({
|
|
2
|
+
options: { styleIsolation: "isolated" },
|
|
3
|
+
properties: {
|
|
4
|
+
adCode: { type: String, value: "" },
|
|
5
|
+
provider: { type: String, value: "" },
|
|
6
|
+
userId: { type: String, value: "" },
|
|
7
|
+
ignoreAdUnitType: { type: Array, value: [] },
|
|
8
|
+
width: { type: String, value: "100%" },
|
|
9
|
+
autoShow: { type: Boolean, value: true },
|
|
10
|
+
autoLoad: { type: Boolean, value: true },
|
|
11
|
+
showClose: { type: Boolean, value: true },
|
|
12
|
+
showStatus: { type: Boolean, value: false },
|
|
13
|
+
buttonText: { type: String, value: "" }
|
|
14
|
+
},
|
|
15
|
+
methods: {
|
|
16
|
+
refresh: function () {
|
|
17
|
+
var component = this.selectComponent("#activeAd");
|
|
18
|
+
return component ? component.refresh() : Promise.reject(new Error("广告组件尚未就绪"));
|
|
19
|
+
},
|
|
20
|
+
prepare: function () {
|
|
21
|
+
var component = this.selectComponent("#activeAd");
|
|
22
|
+
return component ? component.prepare() : Promise.reject(new Error("广告组件尚未就绪"));
|
|
23
|
+
},
|
|
24
|
+
show: function () {
|
|
25
|
+
var component = this.selectComponent("#activeAd");
|
|
26
|
+
return component ? component.show() : Promise.reject(new Error("广告组件尚未就绪"));
|
|
27
|
+
},
|
|
28
|
+
close: function () {
|
|
29
|
+
var component = this.selectComponent("#activeAd");
|
|
30
|
+
if (component) component.close();
|
|
31
|
+
},
|
|
32
|
+
forwardReady: function (event) { this.triggerEvent("ready", event.detail); },
|
|
33
|
+
forwardLoad: function (event) { this.triggerEvent("load", event.detail); },
|
|
34
|
+
forwardError: function (event) { this.triggerEvent("error", event.detail); },
|
|
35
|
+
forwardClose: function (event) { this.triggerEvent("close", event.detail); },
|
|
36
|
+
forwardComplete: function (event) { this.triggerEvent("complete", event.detail); },
|
|
37
|
+
forwardAction: function (event) { this.triggerEvent("action", event.detail); },
|
|
38
|
+
forwardDisabled: function (event) { this.triggerEvent("disabled", event.detail); },
|
|
39
|
+
forwardFallback: function (event) { this.triggerEvent("fallback", event.detail); },
|
|
40
|
+
forwardInteraction: function (event) { this.triggerEvent("interaction", event.detail); }
|
|
41
|
+
}
|
|
42
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<pull-ad id="activeAd" ad-code="{{adCode}}" provider="{{provider}}" user-id="{{userId}}" ignore-ad-unit-type="{{ignoreAdUnitType}}" width="{{width}}"
|
|
2
|
+
auto-load="{{autoLoad}}" auto-show="{{autoShow}}" show-close="{{showClose}}"
|
|
3
|
+
show-status="{{showStatus}}" button-text="{{buttonText}}"
|
|
4
|
+
bindready="forwardReady" bindload="forwardLoad" binderror="forwardError"
|
|
5
|
+
bindclose="forwardClose" bindcomplete="forwardComplete" bindaction="forwardAction"
|
|
6
|
+
binddisabled="forwardDisabled" bindfallback="forwardFallback" bindinteraction="forwardInteraction"
|
|
7
|
+
/>
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
var client = require("./lib/client");
|
|
2
|
+
var constants = require("./lib/constants");
|
|
3
|
+
var validator = require("./lib/validator");
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
version: constants.VERSION,
|
|
7
|
+
AD_SLOTS: constants.AD_SLOTS,
|
|
8
|
+
configure: client.configure,
|
|
9
|
+
getConfiguration: client.getConfiguration,
|
|
10
|
+
pull: client.pull,
|
|
11
|
+
normalizePull: validator.normalizePull
|
|
12
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
var providerConfig = require("../provider-config");
|
|
2
|
+
var validator = require("./validator");
|
|
3
|
+
var transport = require("./transport");
|
|
4
|
+
var PluginError = require("./errors").PluginError;
|
|
5
|
+
|
|
6
|
+
var configuration = {
|
|
7
|
+
baseUrl: providerConfig.baseUrl,
|
|
8
|
+
endpoint: providerConfig.placementEndpoint,
|
|
9
|
+
webview_page: "",
|
|
10
|
+
timeout: providerConfig.timeout,
|
|
11
|
+
requestAdapter: null
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function configure(options) {
|
|
15
|
+
options = options || {};
|
|
16
|
+
if (options.baseUrl !== undefined) configuration.baseUrl = validator.baseUrl(options.baseUrl);
|
|
17
|
+
if (options.endpoint !== undefined) configuration.endpoint = validator.endpoint(options.endpoint);
|
|
18
|
+
if (options.webview_page !== undefined) {
|
|
19
|
+
var route = options.webview_page;
|
|
20
|
+
if (typeof route !== "string") throw new PluginError("INVALID_ARGUMENT", "webview_page 必须是页面路径", false);
|
|
21
|
+
route = route.trim();
|
|
22
|
+
if (route && !/^\/?[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)+\?(?:[^\s#?]*&)?[A-Za-z_][A-Za-z0-9_]*=$/.test(route)) {
|
|
23
|
+
throw new PluginError("INVALID_ARGUMENT", "webview_page 格式应为 pages/xx/webview?src=", false);
|
|
24
|
+
}
|
|
25
|
+
configuration.webview_page = route;
|
|
26
|
+
}
|
|
27
|
+
if (options.timeout !== undefined) {
|
|
28
|
+
var timeout = Number(options.timeout);
|
|
29
|
+
if (!Number.isInteger(timeout) || timeout < 1000 || timeout > 30000) {
|
|
30
|
+
throw new PluginError("INVALID_ARGUMENT", "timeout 超出允许范围", false);
|
|
31
|
+
}
|
|
32
|
+
configuration.timeout = timeout;
|
|
33
|
+
}
|
|
34
|
+
if (options.requestAdapter !== undefined) {
|
|
35
|
+
if (options.requestAdapter !== null && typeof options.requestAdapter !== "function") {
|
|
36
|
+
throw new PluginError("INVALID_ARGUMENT", "requestAdapter 必须是函数", false);
|
|
37
|
+
}
|
|
38
|
+
configuration.requestAdapter = options.requestAdapter;
|
|
39
|
+
}
|
|
40
|
+
return getConfiguration();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getConfiguration() {
|
|
44
|
+
return { baseUrl: configuration.baseUrl, endpoint: configuration.endpoint,
|
|
45
|
+
webview_page: configuration.webview_page, timeout: configuration.timeout };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function optionalId(value, name) {
|
|
49
|
+
if (value === undefined || value === null || value === "") return "";
|
|
50
|
+
if (typeof value !== "string" || value.trim().length > 256) {
|
|
51
|
+
throw new PluginError("INVALID_ARGUMENT", name + " 格式错误", false);
|
|
52
|
+
}
|
|
53
|
+
return value.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pull(params) {
|
|
57
|
+
params = params || {};
|
|
58
|
+
try {
|
|
59
|
+
var spaceCode = validator.spaceCode(params.spaceCode || params.adCode);
|
|
60
|
+
var url = params.baseUrl ? validator.baseUrl(params.baseUrl) : configuration.baseUrl;
|
|
61
|
+
var endpoint = params.endpoint ? validator.endpoint(params.endpoint) : configuration.endpoint;
|
|
62
|
+
var data = { space_code: spaceCode };
|
|
63
|
+
var provider = optionalId(params.provider, "provider");
|
|
64
|
+
var userId = optionalId(params.userId, "user_id");
|
|
65
|
+
if (provider) data.provider = provider;
|
|
66
|
+
if (userId) data.user_id = userId;
|
|
67
|
+
if (params.ignoreWechatAds === true) data.ignore_wechat_ads = true;
|
|
68
|
+
if (params.ignoreAdUnitType !== undefined) {
|
|
69
|
+
if (!Array.isArray(params.ignoreAdUnitType) || params.ignoreAdUnitType.length > 20
|
|
70
|
+
|| params.ignoreAdUnitType.some(function (item) { return typeof item !== "string" || !item || item.length > 64; })) {
|
|
71
|
+
throw new PluginError("INVALID_ARGUMENT", "ignore_ad_unit_type 格式错误", false);
|
|
72
|
+
}
|
|
73
|
+
if (params.ignoreAdUnitType.length) data.ignore_ad_unit_type = params.ignoreAdUnitType.slice();
|
|
74
|
+
}
|
|
75
|
+
return transport.request({
|
|
76
|
+
url: url + endpoint, timeout: configuration.timeout,
|
|
77
|
+
header: { "content-type": "application/json" }, data: data,
|
|
78
|
+
scope: params.requestScope, adapter: configuration.requestAdapter
|
|
79
|
+
}).then(function (response) {
|
|
80
|
+
var status = Number(response && response.statusCode);
|
|
81
|
+
if (!Number.isFinite(status) || status < 200 || status >= 300) {
|
|
82
|
+
throw new PluginError("HTTP_ERROR", "广告服务 HTTP " + status, status >= 500);
|
|
83
|
+
}
|
|
84
|
+
var body = response.data;
|
|
85
|
+
if (!body || typeof body !== "object" || Array.isArray(body)
|
|
86
|
+
|| (typeof body.code !== "number" && typeof body.code !== "string") || String(body.code).trim() === "") {
|
|
87
|
+
throw new PluginError("INVALID_RESPONSE", "广告拉取响应格式错误", false);
|
|
88
|
+
}
|
|
89
|
+
if (Number(body.code) === 0 && body.message === "当前暂无可投放广告" && body.data == null) {
|
|
90
|
+
return { spaceCode: spaceCode, pullId: "", enabled: false };
|
|
91
|
+
}
|
|
92
|
+
if ([1, 204].indexOf(Number(body.code)) < 0) {
|
|
93
|
+
throw new PluginError(String(body.code), body.message || "广告拉取失败", false);
|
|
94
|
+
}
|
|
95
|
+
var result = validator.normalizePull(body.data);
|
|
96
|
+
if (Number(body.code) === 204 && result.enabled) {
|
|
97
|
+
throw new PluginError("INVALID_RESPONSE", "无广告响应不能包含投放记录", false);
|
|
98
|
+
}
|
|
99
|
+
if (result.spaceCode !== spaceCode) {
|
|
100
|
+
throw new PluginError("INVALID_RESPONSE", "响应广告位编码不匹配", false);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
});
|
|
104
|
+
} catch (error) { return Promise.reject(error); }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { configure: configure, getConfiguration: getConfiguration,
|
|
108
|
+
pull: pull, createRequestScope: transport.createScope };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
VERSION: "0.4.0",
|
|
3
|
+
AD_SLOTS: {
|
|
4
|
+
BANNER: "SLOT_ID_WEAPP_BANNER",
|
|
5
|
+
REWARD_VIDEO: "SLOT_ID_WEAPP_REWARD_VIDEO",
|
|
6
|
+
INTERSTITIAL: "SLOT_ID_WEAPP_INTERSTITIAL",
|
|
7
|
+
VIDEO_FEEDS: "SLOT_ID_WEAPP_VIDEO_FEEDS",
|
|
8
|
+
VIDEO_BEGIN: "SLOT_ID_WEAPP_VIDEO_BEGIN",
|
|
9
|
+
TEMPLATE: "SLOT_ID_WEAPP_TEMPLATE"
|
|
10
|
+
}
|
|
11
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function PluginError(code, message, retryable, cause) {
|
|
2
|
+
this.name = "PluginError";
|
|
3
|
+
this.code = code || "UNKNOWN_ERROR";
|
|
4
|
+
this.message = message || "广告配置加载失败";
|
|
5
|
+
this.retryable = retryable === true;
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
if (Error.captureStackTrace) {
|
|
8
|
+
Error.captureStackTrace(this, PluginError);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
PluginError.prototype = Object.create(Error.prototype);
|
|
13
|
+
PluginError.prototype.constructor = PluginError;
|
|
14
|
+
|
|
15
|
+
function toPublicError(error) {
|
|
16
|
+
return {
|
|
17
|
+
code: error && error.code != null ? String(error.code)
|
|
18
|
+
: error && error.errCode != null ? String(error.errCode) : "UNKNOWN_ERROR",
|
|
19
|
+
message: error && (error.message || error.errMsg) || "广告配置加载失败",
|
|
20
|
+
retryable: Boolean(error && error.retryable)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
PluginError: PluginError,
|
|
26
|
+
toPublicError: toPublicError
|
|
27
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
var client = require("./client");
|
|
2
|
+
var providerConfig = require("../provider-config");
|
|
3
|
+
var PluginError = require("./errors").PluginError;
|
|
4
|
+
|
|
5
|
+
function cancelled(error) {
|
|
6
|
+
return /cancel|用户取消|取消操作/i.test(String(error && (error.errMsg || error.message) || ""));
|
|
7
|
+
}
|
|
8
|
+
function needsGesture(error) {
|
|
9
|
+
return /user.?gesture|user.?tap|user.?click|tap event|click event|must.*tap|must.*click|只能.*点击|用户.*点击|用户手势/i
|
|
10
|
+
.test(String(error && (error.errMsg || error.message) || ""));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// wx 导航调用始终在点击或 showModal 成功回调的同步栈中发起。
|
|
14
|
+
function perform(action, hooks) {
|
|
15
|
+
action = action || { type: "none" };
|
|
16
|
+
hooks = hooks || {};
|
|
17
|
+
if (action.type === "none") return Promise.resolve({ skipped: true });
|
|
18
|
+
var method;
|
|
19
|
+
var options;
|
|
20
|
+
var half = false;
|
|
21
|
+
if (action.type === "web_view") {
|
|
22
|
+
method = "navigateTo";
|
|
23
|
+
var route = client.getConfiguration().webview_page;
|
|
24
|
+
if (route != null && route !== "") {
|
|
25
|
+
if (typeof route !== "string") return Promise.reject(new PluginError("INVALID_ARGUMENT", "webview_page 必须是页面路径", false));
|
|
26
|
+
route = route.trim();
|
|
27
|
+
// 商家提供完整参数前缀,例 pages/webview/index?src=,不再追加固定的 url 参数。
|
|
28
|
+
if (!/^\/?[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)+\?(?:[^\s#?]*&)?[A-Za-z_][A-Za-z0-9_]*=$/.test(route)) {
|
|
29
|
+
return Promise.reject(new PluginError("INVALID_ARGUMENT", "webview_page 格式应为 pages/xx/webview?src=", false));
|
|
30
|
+
}
|
|
31
|
+
options = { url: (route[0] === "/" ? route : "/" + route) + encodeURIComponent(action.url) };
|
|
32
|
+
} else {
|
|
33
|
+
var target = providerConfig.webViewTarget;
|
|
34
|
+
method = "navigateToMiniProgram";
|
|
35
|
+
options = { appId: target.appId, path: target.pagePrefix + encodeURIComponent(action.url), envVersion: target.envVersion };
|
|
36
|
+
half = true;
|
|
37
|
+
}
|
|
38
|
+
} else if (action.type === "mini_program") {
|
|
39
|
+
method = "navigateToMiniProgram";
|
|
40
|
+
options = { appId: action.appId, path: action.path, envVersion: action.envVersion };
|
|
41
|
+
half = action.openMode === "half_screen";
|
|
42
|
+
} else {
|
|
43
|
+
return Promise.reject(new PluginError("INVALID_ARGUMENT", "不支持的广告跳转类型", false));
|
|
44
|
+
}
|
|
45
|
+
return new Promise(function (resolve, reject) {
|
|
46
|
+
var fallback = false;
|
|
47
|
+
function active() { return !hooks.isActive || hooks.isActive(); }
|
|
48
|
+
function fail(error, mode) {
|
|
49
|
+
if (!active()) { resolve({ disposed: true }); return; }
|
|
50
|
+
var code = cancelled(error) ? "NAVIGATION_CANCELLED" : needsGesture(error) ? "NAVIGATION_GESTURE_REQUIRED" : "NAVIGATION_ERROR";
|
|
51
|
+
var failure = new PluginError(code, code === "NAVIGATION_CANCELLED" ? "用户已取消" : "广告跳转失败", false, error);
|
|
52
|
+
failure.mode = mode;
|
|
53
|
+
reject(failure);
|
|
54
|
+
}
|
|
55
|
+
function invoke(api, mode, allowFallback) {
|
|
56
|
+
if (!active()) { resolve({ disposed: true }); return; }
|
|
57
|
+
if (typeof wx === "undefined" || typeof wx[api] !== "function") {
|
|
58
|
+
if (allowFallback) { openFull("unsupported"); return; }
|
|
59
|
+
fail({ errMsg: "当前基础库不支持 " + api }, mode);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
var done = false;
|
|
63
|
+
var params = Object.assign({}, options, {
|
|
64
|
+
success: function (result) {
|
|
65
|
+
if (done) return;
|
|
66
|
+
done = true;
|
|
67
|
+
resolve(Object.assign({}, result, { mode: mode, fallback: fallback, disposed: !active() }));
|
|
68
|
+
},
|
|
69
|
+
fail: function (error) {
|
|
70
|
+
if (done) return;
|
|
71
|
+
done = true;
|
|
72
|
+
if (allowFallback && !cancelled(error) && !needsGesture(error) && active()) openFull("half_screen_failed");
|
|
73
|
+
else fail(error, mode);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if (api === "openEmbeddedMiniProgram") params.verify = "binding";
|
|
77
|
+
try { wx[api](params); } catch (error) { params.fail(error); }
|
|
78
|
+
}
|
|
79
|
+
function openFull(reason) {
|
|
80
|
+
fallback = true;
|
|
81
|
+
invoke("navigateToMiniProgram", "full_screen", false);
|
|
82
|
+
if (hooks.onFallback) hooks.onFallback({ reason: reason });
|
|
83
|
+
}
|
|
84
|
+
if (half && hooks.forceFullScreen) openFull("user_full_screen");
|
|
85
|
+
else if (half) invoke("openEmbeddedMiniProgram", "half_screen", true);
|
|
86
|
+
else invoke(method, method === "navigateTo" ? "web_view" : "full_screen", false);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { perform: perform };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
var SLOTS = require("./constants").AD_SLOTS;
|
|
2
|
+
|
|
3
|
+
function describe(pull) {
|
|
4
|
+
if (!pull || !pull.enabled) return { kind: "hidden", mode: "" };
|
|
5
|
+
if (pull.sourceType === "custom") {
|
|
6
|
+
if (pull.creative.format === "wechat_modal") return { kind: "modal", mode: "INTERSTITIAL" };
|
|
7
|
+
return { kind: "image", mode: pull.scene === "popup" ? "INTERSTITIAL" : "BANNER" };
|
|
8
|
+
}
|
|
9
|
+
if (pull.adSlot === SLOTS.REWARD_VIDEO) return { kind: "wechat_api", mode: "REWARDED" };
|
|
10
|
+
if (pull.adSlot === SLOTS.INTERSTITIAL) return { kind: "wechat_api", mode: "INTERSTITIAL" };
|
|
11
|
+
if (pull.adSlot === SLOTS.TEMPLATE) return { kind: "wechat_template", mode: "BANNER" };
|
|
12
|
+
if (pull.adSlot === SLOTS.VIDEO_FEEDS || pull.adSlot === SLOTS.VIDEO_BEGIN) {
|
|
13
|
+
return { kind: "wechat_inline", mode: "VIDEO" };
|
|
14
|
+
}
|
|
15
|
+
return { kind: "wechat_inline", mode: "BANNER" };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { describe: describe };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
var PluginError = require("./errors").PluginError;
|
|
2
|
+
var sequence = 0;
|
|
3
|
+
|
|
4
|
+
function createScope() {
|
|
5
|
+
var listeners = [];
|
|
6
|
+
var scope = {
|
|
7
|
+
id: ++sequence, cancelled: false,
|
|
8
|
+
cancel: function () {
|
|
9
|
+
if (scope.cancelled) return;
|
|
10
|
+
scope.cancelled = true;
|
|
11
|
+
listeners.slice().forEach(function (callback) { callback(); });
|
|
12
|
+
listeners = [];
|
|
13
|
+
},
|
|
14
|
+
onCancel: function (callback) {
|
|
15
|
+
if (scope.cancelled) { callback(); return function () {}; }
|
|
16
|
+
listeners.push(callback);
|
|
17
|
+
return function () { listeners = listeners.filter(function (item) { return item !== callback; }); };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
return scope;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function request(options) {
|
|
24
|
+
return new Promise(function (resolve, reject) {
|
|
25
|
+
var settled = false;
|
|
26
|
+
var task;
|
|
27
|
+
var unsubscribe;
|
|
28
|
+
var timer;
|
|
29
|
+
function finish(error, result) {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
if (unsubscribe) unsubscribe();
|
|
34
|
+
if (error) reject(error); else resolve(result);
|
|
35
|
+
}
|
|
36
|
+
function abort(error) {
|
|
37
|
+
finish(error);
|
|
38
|
+
if (task && typeof task.abort === "function") { try { task.abort(); } catch (ignored) {} }
|
|
39
|
+
}
|
|
40
|
+
if (options.scope && options.scope.cancelled) {
|
|
41
|
+
finish(new PluginError("CANCELLED", "广告请求已取消", false));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
timer = setTimeout(function () { abort(new PluginError("TIMEOUT", "广告请求超时", true)); }, options.timeout);
|
|
45
|
+
if (options.scope) unsubscribe = options.scope.onCancel(function () { abort(new PluginError("CANCELLED", "广告请求已取消", false)); });
|
|
46
|
+
function fail(error) {
|
|
47
|
+
if (error instanceof PluginError) { finish(error); return; }
|
|
48
|
+
var message = String(error && (error.errMsg || error.message) || "");
|
|
49
|
+
var permanent = /domain.*list|url not in|域名.*合法|ssl|certificate/i.test(message);
|
|
50
|
+
finish(new PluginError(permanent ? "DOMAIN_ERROR" : "NETWORK_ERROR", permanent ? "请检查小程序合法域名与证书" : "广告服务请求失败", !permanent, error));
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
if (options.adapter) {
|
|
54
|
+
Promise.resolve(options.adapter(options)).then(function (response) { finish(null, response); }, fail);
|
|
55
|
+
} else if (typeof wx !== "undefined" && typeof wx.request === "function") {
|
|
56
|
+
task = wx.request({ url: options.url, method: "POST", data: options.data, header: options.header, timeout: options.timeout,
|
|
57
|
+
success: function (response) { finish(null, response); }, fail: fail });
|
|
58
|
+
} else finish(new PluginError("UNSUPPORTED_ENV", "当前环境不支持 wx.request", false));
|
|
59
|
+
} catch (error) { fail(error); }
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { request: request, createScope: createScope };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
var PluginError = require("./errors").PluginError;
|
|
2
|
+
var SLOTS = require("./constants").AD_SLOTS;
|
|
3
|
+
|
|
4
|
+
function fail(message) { throw new PluginError("INVALID_RESPONSE", message, false); }
|
|
5
|
+
function value(raw, name) { return raw[name] === undefined ? raw[name.replace(/_([a-z])/g, function (_, c) { return c.toUpperCase(); })] : raw[name]; }
|
|
6
|
+
function string(raw, name, max, required) {
|
|
7
|
+
var input = value(raw, name);
|
|
8
|
+
var result = typeof input === "string" ? input.trim() : "";
|
|
9
|
+
if (required && !result || result.length > max) fail(name + " 格式错误");
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
function spaceCode(input) {
|
|
13
|
+
var code = typeof input === "string" ? input.trim() : "";
|
|
14
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/.test(code)) {
|
|
15
|
+
throw new PluginError("INVALID_ARGUMENT", "space_code 格式错误", false);
|
|
16
|
+
}
|
|
17
|
+
return code;
|
|
18
|
+
}
|
|
19
|
+
function baseUrl(input) {
|
|
20
|
+
var url = typeof input === "string" ? input.trim().replace(/\/+$/, "") : "";
|
|
21
|
+
if (!/^https:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/.*)?$/.test(url)) {
|
|
22
|
+
throw new PluginError("INVALID_ARGUMENT", "baseUrl 必须是 HTTPS 地址", false);
|
|
23
|
+
}
|
|
24
|
+
return url;
|
|
25
|
+
}
|
|
26
|
+
function endpoint(input) {
|
|
27
|
+
if (typeof input !== "string" || !/^\/(?!\/)[^\s]*$/.test(input)) {
|
|
28
|
+
throw new PluginError("INVALID_ARGUMENT", "endpoint 必须是站内绝对路径", false);
|
|
29
|
+
}
|
|
30
|
+
return input;
|
|
31
|
+
}
|
|
32
|
+
function httpsUrl(input, name, required) {
|
|
33
|
+
var url = typeof input === "string" ? input.trim() : "";
|
|
34
|
+
if (required && !url || url.length > 2048 || url && !/^https:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:[/?#].*)?$/.test(url)) {
|
|
35
|
+
fail(name + " 必须是 HTTPS 地址");
|
|
36
|
+
}
|
|
37
|
+
return url;
|
|
38
|
+
}
|
|
39
|
+
function modalColor(input, fallback, name) {
|
|
40
|
+
var result = input == null || input === "" ? fallback : String(input);
|
|
41
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(result)) fail(name + " 格式错误");
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function action(raw, modal) {
|
|
45
|
+
raw = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
46
|
+
var type = string(raw, "type", 32, false).toLowerCase() || "none";
|
|
47
|
+
if (["none", "web_view", "mini_program"].indexOf(type) < 0) fail("不支持的广告跳转类型");
|
|
48
|
+
var result = {
|
|
49
|
+
type: type, url: "", appId: "", path: "", openMode: "navigate", envVersion: "release",
|
|
50
|
+
modalTitle: "温馨提示", modalContent: "", modalConfirmText: "确定",
|
|
51
|
+
modalConfirmColor: "#576B95", modalShowCancel: true,
|
|
52
|
+
modalCancelText: "取消", modalCancelColor: "#000000"
|
|
53
|
+
};
|
|
54
|
+
if (type === "web_view") result.url = httpsUrl(value(raw, "url"), "action.url", true);
|
|
55
|
+
if (type === "mini_program") {
|
|
56
|
+
result.appId = string(raw, "app_id", 64, true);
|
|
57
|
+
result.path = string(raw, "path", 512, true);
|
|
58
|
+
result.openMode = string(raw, "open_mode", 32, false).toLowerCase() || "navigate";
|
|
59
|
+
result.envVersion = string(raw, "env_version", 16, false).toLowerCase() || "release";
|
|
60
|
+
if (!/^wx[A-Za-z0-9]{16}$/.test(result.appId) || result.path.indexOf("..") >= 0
|
|
61
|
+
|| /^(?:https?:)?\/\//i.test(result.path)
|
|
62
|
+
|| ["navigate", "half_screen"].indexOf(result.openMode) < 0
|
|
63
|
+
|| ["release", "trial", "develop"].indexOf(result.envVersion) < 0) fail("目标小程序参数错误");
|
|
64
|
+
}
|
|
65
|
+
if (modal) {
|
|
66
|
+
result.modalTitle = string(raw, "modal_title", 64, false) || "温馨提示";
|
|
67
|
+
result.modalContent = string(raw, "modal_content", 255, true);
|
|
68
|
+
result.modalConfirmText = string(raw, "modal_confirm_text", 4, false) || "确定";
|
|
69
|
+
result.modalConfirmColor = modalColor(value(raw, "modal_confirm_color"), "#576B95", "modal_confirm_color");
|
|
70
|
+
var showCancel = value(raw, "modal_show_cancel");
|
|
71
|
+
if (showCancel != null && typeof showCancel !== "boolean") fail("modal_show_cancel 格式错误");
|
|
72
|
+
result.modalShowCancel = showCancel == null ? true : showCancel;
|
|
73
|
+
result.modalCancelText = string(raw, "modal_cancel_text", 4, false) || "取消";
|
|
74
|
+
result.modalCancelColor = modalColor(value(raw, "modal_cancel_color"), "#000000", "modal_cancel_color");
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function creative(raw) {
|
|
79
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) fail("自营广告缺少 creative");
|
|
80
|
+
var format = string(raw, "format", 32, true).toLowerCase();
|
|
81
|
+
if (format !== "image" && format !== "wechat_modal") fail("不支持的自营广告形式");
|
|
82
|
+
var modal = format === "wechat_modal";
|
|
83
|
+
return {
|
|
84
|
+
creativeId: string(raw, "creative_id", 128, false),
|
|
85
|
+
format: format,
|
|
86
|
+
imageUrl: modal ? "" : httpsUrl(value(raw, "image_url"), "creative.image_url", true),
|
|
87
|
+
imageMode: modal ? "" : string(raw, "image_mode", 32, false) || "widthFix",
|
|
88
|
+
action: action(raw.action, modal)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function normalizePull(raw) {
|
|
92
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) fail("拉取响应格式错误");
|
|
93
|
+
var code = spaceCode(value(raw, "space_code"));
|
|
94
|
+
var id = value(raw, "pull_id");
|
|
95
|
+
if (id == null || id === "") return { spaceCode: code, pullId: "", enabled: false };
|
|
96
|
+
var pullId = String(id);
|
|
97
|
+
if (!/^[1-9][0-9]{0,18}$/.test(pullId)) fail("pull_id 格式错误");
|
|
98
|
+
var sourceType = string(raw, "source_type", 32, true).toLowerCase();
|
|
99
|
+
var scene = string(raw, "scene", 32, true).toLowerCase();
|
|
100
|
+
if (["wechatmini", "custom"].indexOf(sourceType) < 0
|
|
101
|
+
|| ["banner", "popup", "reward"].indexOf(scene) < 0) fail("广告来源或场景不受支持");
|
|
102
|
+
var result = {
|
|
103
|
+
spaceCode: code, pullId: pullId, enabled: true,
|
|
104
|
+
sourceType: sourceType, scene: scene,
|
|
105
|
+
adUnitId: string(raw, "ad_unit_id", 128, false),
|
|
106
|
+
adUnitCode: string(raw, "ad_unit_code", 2048, false),
|
|
107
|
+
adSlot: string(raw, "ad_slot", 64, false),
|
|
108
|
+
renderMode: string(raw, "render_mode", 32, false),
|
|
109
|
+
wxAdResultAction: value(raw, "wx_ad_result_action") === "continue_pull_zzad" ? "continue_pull_zzad" : "none",
|
|
110
|
+
creative: null
|
|
111
|
+
};
|
|
112
|
+
if (sourceType === "custom") {
|
|
113
|
+
result.creative = creative(raw.creative);
|
|
114
|
+
if (scene === "reward" || result.creative.format === "wechat_modal" && scene !== "popup") {
|
|
115
|
+
fail("自营广告形式与广告位场景不一致");
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
if (!result.adUnitId || !result.adSlot) fail("微信广告缺少单元信息");
|
|
119
|
+
var inline = [SLOTS.TEMPLATE, SLOTS.BANNER, SLOTS.VIDEO_FEEDS, SLOTS.VIDEO_BEGIN];
|
|
120
|
+
if (inline.indexOf(result.adSlot) >= 0 && scene !== "banner"
|
|
121
|
+
|| result.adSlot === SLOTS.INTERSTITIAL && scene !== "popup"
|
|
122
|
+
|| result.adSlot === SLOTS.REWARD_VIDEO && scene !== "reward"
|
|
123
|
+
|| inline.indexOf(result.adSlot) < 0 && [SLOTS.INTERSTITIAL, SLOTS.REWARD_VIDEO].indexOf(result.adSlot) < 0) {
|
|
124
|
+
fail("微信广告单元与广告位场景不一致");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { spaceCode: spaceCode, baseUrl: baseUrl, endpoint: endpoint,
|
|
131
|
+
action: action, creative: creative, normalizePull: normalizePull };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 服务商发布插件前维护这里,商家接入时不需要配置这些地址。
|
|
3
|
+
*/
|
|
4
|
+
module.exports = {
|
|
5
|
+
baseUrl: "https://api-test.zzspider.com",
|
|
6
|
+
placementEndpoint: "/api/wxams-p/pull",
|
|
7
|
+
webViewTarget: {
|
|
8
|
+
appId: "wxf28dad03108653bc",
|
|
9
|
+
pagePrefix: "pages/webview?src=",
|
|
10
|
+
envVersion: "release"
|
|
11
|
+
},
|
|
12
|
+
timeout: 10000
|
|
13
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zyqs-qiuyeyue/zz-wxams-ad",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "知蛛小程序广告组件,支持微信广告与自营广告",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"miniprogram": "dist",
|
|
7
|
+
"files": ["dist", "README.md"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "node scripts/build.js",
|
|
10
|
+
"test": "npm run build && node scripts/test.js",
|
|
11
|
+
"prepack": "npm run build"
|
|
12
|
+
},
|
|
13
|
+
"license": "UNLICENSED",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"registry": "https://registry.npmjs.org"
|
|
17
|
+
}
|
|
18
|
+
}
|