最近本站升级到了Umami统计,在关于我页面,也添加了简单的网站统计数据概览效果。

本站效果预览

洪哥提供了使用Docker搭建Umami统计方法,林木木提供了前端调用 Umami API 数据,我这个是基于Vercel自部署的功能实现。

个人认为我自己这套方案的主要优点如下:

基于Vercel部署,免费

Json 输出,便于使用

新增接口方式,尽量不改动Umami官方代码,后续升级容易

一次调用即可获得:概览数据、访客浏览器数据、访客操作系统数据、访客来源国家/地区数据(其他Umami统计数据也可返回,个人认为这些足够了就没折腾了)

访客浏览器数据:返回了浏览器 icon、格式化后的名称

访客操作系统数据:返回了操作系统 icon、格式化后的名称

访客来源国家/地区数据:返回了国家/地区旗帜、中文的名称,前端拿到手即用

感兴趣吗?开干!

Vercel 部署 Umami

首先Fork官方仓库Umami,名称随便。顺手也点个小星星吧,支持下开源项目。

接着在 Vercel 中,创建一个新项目,选择刚刚Fork的这个仓库,点击Import部署该项目。

在自己 Vercel 首页,点击“Storage” —> “Create Database” —> “Serverless Postgres”。

Storage

接着地区建议选择新加坡 “Singapore - sin1”,其他默认,创建完成。

创建 Serverless Postgres

访问刚创建的这个服务,点击“Show secret”查看DATABASE_URL的值,并保存好稍后使用。

小贴士:
通过此处的信息,后期还可以在桌面端如DBeaver软件中,直连数据库,访问Umami数据表,用SQL取数自己分析等等。

DBeaver为例,在DBeaver中创建一个链接,选择PostgreSQL后,简要配置如下:

  • 主机:即该页面的PGHOST
  • 端口:默认5432就行
  • 认证:选择Database Native
  • 用户名:即该页面的PGUSER
  • 密码:即该页面的PGPASSWORD

随后点击左下角“测试链接”,如果正常的话,勾选Save password locally保存密码到本地,然后确定即可。

回到 Vercel 中刚才创建的Umami项目,点击该项目,在 “Settings” —> “Environment Variables” 下,创建以下环境变量:

变量名取值说明
CORS_ALLOWED_ORIGINS配置你自己网站域名,解决跨域问题,如https://blog.guole.fun解决Umami跨域问题,同时一定程度保障网站数据安全性
DATABASE_URL刚才记录的DATABASE_URL官方配置项,数据库链接
TRACKER_SCRIPT_NAMEscript.js脚本名称,就是给Umami前端嵌入的跟踪器脚本一个新的名称,随便自定义

先不要急着去重新部署,因为我们还要调整代码。

在 Umami 中新增接口

在本地创建一个文件夹比如umami,然后执行以下命令关联和拉取远程仓库:

1
2
3
git init  
git remote add origin ${刚才 Fork 的 Umami 官方仓库 ssh 地址}
git pull

在本地编辑器中打开umami/package.json,在scriptsbuild下面插入一行:

1
"build-dev": "npm-run-all check-env build-db check-db build-tracker build-app",

这是为了解决本地调试时,除了第一次,后面无需执行build-geo步骤(安装完依赖,首次执行 npm run build,后续使用 npm run build-dev),避免国内网络情况每次构建都要等很久很久……

记得上面配置的环境变量DATABASE_URLCORS_ALLOWED_ORIGINS吗?在umami/.env中新增:

1
2
DATABASE_URL=postgres://neondb******ire  # 刚才记录的值
CORS_ALLOWED_ORIGINS=https://blog.guole.fun,http://127.0.0.1:4000,http://127.0.0.1:8000,https://hoppscotch.io # 换成你自己的

在本地编辑器中打开umami/tsconfig.json,在原文"styles/*": ["./styles/*"],下插入一行:

1
2
3
- "styles/*": ["./styles/*"]
+ "styles/*": ["./styles/*"],
+ "public/*": ["../public/*"]
修改后完整 tsconfig.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"compilerOptions": {
"target": "es2021",
"outDir": "./build",
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"lib": ["dom", "dom.iterable", "esnext"],
"skipLibCheck": true,
"esModuleInterop": true,
"noImplicitAny": false,
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"strict": true,
"strictNullChecks": false,
"noEmit": true,
"jsx": "preserve",
"incremental": false,
"baseUrl": "./src",
"types": ["jest"],
"typeRoots": ["node_modules/@types"],
"paths": {
"react": ["./node_modules/@types/react"],
"assets/*": ["./assets/*"],
"components/*": ["./components/*"],
"lib/*": ["./lib/*"],
"pages/*": ["./pages/*"],
"queries/*": ["./queries/*"],
"store/*": ["./store/*"],
"styles/*": ["./styles/*"],
"public/*": ["../public/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx", "next-env.d.ts", ".next/types/**/*.ts"],
"exclude": ["node_modules", "./cypress.config.ts", "cypress"]
}

在本地编辑器中打开umami/src/lib/middleware.ts,修改createMiddleware内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export const useCors = createMiddleware((req, res, next) => {
cors({
origin: (origin: string | undefined, callback) => {
const allowedOrigins = (process.env.CORS_ALLOWED_ORIGINS || '').split(',');
// 如果没有提供 origin,允许请求
if (!origin || allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin || '');
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
// Cache CORS preflight request 24 hours by default
maxAge: Number(process.env.CORS_MAX_AGE) || 86400,
})(req, res, next);
});

在本地编辑器中打开umami/src/pages/api/share/[shareId].ts,修改原文:

1
2
- import { useValidate } from 'lib/middleware';
+ import { useCors, useValidate } from 'lib/middleware';

另外,该文件中新增对useCors的调用:

1
2
+ await useCors(req, res);
await useValidate(schema, req, res);

接着在本地编辑器中打开umami/src/pages/api/websites/[websiteId]/目录,新建一个文件getWebsiteMetrics.ts,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import * as yup from 'yup';
import { NextApiResponse } from 'next';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { canViewWebsite } from 'lib/auth';
import { useAuth, useCors, useValidate } from 'lib/middleware';
import { NextApiRequestQueryBody } from 'lib/types';
import { getRequestFilters, getRequestDateRange } from 'lib/request';
import { getPageviewMetrics, getSessionMetrics, getWebsiteStats } from 'queries';
import {
SESSION_COLUMNS,
EVENT_COLUMNS,
FILTER_COLUMNS,
OPERATORS,
OS_NAMES,
BROWSERS,
} from 'lib/constants';
import { getCompareDate } from 'lib/date';
import countryNames from 'public/intl/country/zh-CN.json';

export interface CombinedMetricsRequestQuery {
websiteId: string;
startAt: number;
endAt: number;
limit?: number;
offset?: number;
search?: string;
url?: string;
referrer?: string;
title?: string;
query?: string;
event?: string;
host?: string;
os?: string;
browser?: string;
device?: string;
country?: string;
region?: string;
city?: string;
tag?: string;
compare?: string;
type: string;
language?: string;
}

const schema = {
GET: yup.object().shape({
websiteId: yup.string().uuid().required(),
startAt: yup.number().required(),
endAt: yup.number().required(),
limit: yup.number(),
offset: yup.number(),
search: yup.string(),
type: yup.string().required(),
url: yup.string(),
referrer: yup.string(),
title: yup.string(),
query: yup.string(),
host: yup.string(),
os: yup.string(),
browser: yup.string(),
device: yup.string(),
country: yup.string(),
region: yup.string(),
city: yup.string(),
language: yup.string(),
event: yup.string(),
tag: yup.string(),
compare: yup.string(),
}),
};

type dataType = ItemType[];

type SourceType = {
[key: string]: string;
};

interface ItemType {
x: string;
y: number;
}

export default async (
req: NextApiRequestQueryBody<CombinedMetricsRequestQuery>,
res: NextApiResponse,
) => {
await useCors(req, res);
await useAuth(req, res);
await useValidate(schema, req, res);

const { websiteId, type, limit, offset, search, compare } = req.query;

if (req.method === 'GET') {
if (!(await canViewWebsite(req.auth, websiteId))) {
return unauthorized(res);
}

const { startDate, endDate } = await getRequestDateRange(req);
const { startDate: compareStartDate, endDate: compareEndDate } = getCompareDate(
compare,
startDate,
endDate,
);
const column = FILTER_COLUMNS[type] || type;

const filters = getRequestFilters(req);
const filters_search = {
...getRequestFilters(req),
startDate,
endDate,
};

if (search) {
filters_search[type] = {
name: type,
column,
operator: OPERATORS.contains,
value: search,
};
}

// 获取访问统计数据
const metrics = await getWebsiteStats(websiteId, {
...filters,
startDate,
endDate,
});

const prevPeriod = await getWebsiteStats(websiteId, {
...filters,
startDate: compareStartDate,
endDate: compareEndDate,
});

const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {
value: Number(metrics[0][key]) || 0,
prev: Number(prevPeriod[0][key]) || 0,
};
return obj;
}, {});

// 获取操作系统/浏览器/国家地区的统计数据
const getMetricsData = async (websiteId, filters_search, limit, offset, types) => {
if (SESSION_COLUMNS.includes(types)) {
const data = await getSessionMetrics(websiteId, types, filters_search, limit, offset);
// console.log("data:", data);
if (types === 'language') {
const combined = {};
for (const { x, y } of data) {
const key = String(x).toLowerCase().split('-')[0];
if (combined[key] === undefined) {
combined[key] = { x: key, y };
} else {
combined[key].y += y;
}
}
return Object.values(combined);
}
return data;
}

if (EVENT_COLUMNS.includes(types)) {
const data = await getPageviewMetrics(websiteId, types, filters_search, limit, offset);
return data;
}

return [];
};

const osData = await getMetricsData(websiteId, filters_search, limit, offset, 'os');
const browserData = await getMetricsData(websiteId, filters_search, limit, offset, 'browser');
const countryData = await getMetricsData(websiteId, filters_search, limit, offset, 'country');

const formattedData = async (data: dataType, type: string) => {
let source: SourceType;
let icon: string;
switch (type) {
case 'os':
source = OS_NAMES;
break;
case 'browser':
source = BROWSERS;
break;
case 'country':
source = countryNames;
break;
}
if (source) {
return data.map((item: ItemType) => {
const itemName: string = item.x.toLowerCase().replace(/ /g, '-');
switch (type) {
case 'os':
icon = `//umami.guole.fun/images/os/${itemName}.png`;
break;
case 'browser':
icon = `//umami.guole.fun/images/browser/${itemName}.png`;
break;
case 'country':
icon = `//umami.guole.fun/images/country/${itemName}.png`;
break;
case 'device':
icon = `//umami.guole.fun/images/device/${itemName}.png`;
break;
default:
icon = '';
break;
}
return {
name: source[item.x] || item.x,
icon: icon,
x: item.x,
y: item.y,
};
});
}
return false;
};

// 返回合并的结果
return ok(res, {
os: (await formattedData(osData, 'os')) || [],
browser: (await formattedData(browserData, 'browser')) || [],
country: (await formattedData(countryData, 'country')) || [],
stats: stats || [],
});
}

return methodNotAllowed(res);
};

记得把代码中umami.guole.fun,换成你部署Umami后的自定义域名(在 Vercel 中设置)。

至此已完成修改,提交推送到远程仓库,Vercel 就会自动部署,并应用刚才设置的环境变量。

测试接口

确保你的网站已完成Umami接入(官方文档很详细,就不赘述了)。

Umami前端界面中,访问 “设置” —> “网站” —> 选择你的网站 —> “共享链接”,创建一个共享链接(也可以用官方指导里的方法,创建一个 API 秘钥,然后去调用……),这个共享链接只需其中的标识share//${你的域名}中间这部分字符串。

获取 token 地址:https://你的 umami 域名/api/share/ + 上文这个字符串

打开hoppscotch.io,创建一个获取 tokenget 请求,地址就是上面这个,发送看看。应该可以拿到结果。

如果hoppscotch无法发送请求,去安装下这个浏览器插件

再创建一个 get 请求,地址是:https://你的 umami 域名/api/websites/在umami设置中,你的网站ID/getWebsiteMetrics

添加一些请求参数:

1
2
3
4
5
6
7
startAt: 1731859200000
endAt: 1734451199999
unit: hour
timezone: Asia%2FShanghai
compare: false
limit: 10
type: os //这里随意 os / browser / country 都行

添加请求头:

1
2
X-Umami-Share-Token: //刚才调用获得的 token
Content-Type: application/json

发送请求试试看吧!应该可以获得下面这样的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
{
"os": [
{
"name": "Windows 10/11",
"icon": "//umami.guole.fun/images/os/windows-10.png",
"x": "Windows 10",
"y": 44
},
{
"name": "Android",
"icon": "//umami.guole.fun/images/os/android-os.png",
"x": "Android OS",
"y": 9
},
{
"name": "macOS",
"icon": "//umami.guole.fun/images/os/mac-os.png",
"x": "Mac OS",
"y": 4
},
{
"name": "iOS",
"icon": "//umami.guole.fun/images/os/ios.png",
"x": "iOS",
"y": 4
},
{
"name": "Linux",
"icon": "//umami.guole.fun/images/os/linux.png",
"x": "Linux",
"y": 1
},
{
"name": "Windows 7",
"icon": "//umami.guole.fun/images/os/windows-7.png",
"x": "Windows 7",
"y": 1
}
],
"browser": [
{
"name": "Chrome",
"icon": "//umami.guole.fun/images/browser/chrome.png",
"x": "chrome",
"y": 35
},
{
"name": "Edge (Chromium)",
"icon": "//umami.guole.fun/images/browser/edge-chromium.png",
"x": "edge-chromium",
"y": 18
},
{
"name": "iOS",
"icon": "//umami.guole.fun/images/browser/ios.png",
"x": "ios",
"y": 4
},
{
"name": "Chrome (webview)",
"icon": "//umami.guole.fun/images/browser/chromium-webview.png",
"x": "chromium-webview",
"y": 3
},
{
"name": "Firefox",
"icon": "//umami.guole.fun/images/browser/firefox.png",
"x": "firefox",
"y": 2
},
{
"name": "Safari",
"icon": "//umami.guole.fun/images/browser/safari.png",
"x": "safari",
"y": 1
}
],
"country": [
{
"name": "中国",
"icon": "//umami.guole.fun/images/country/cn.png",
"x": "CN",
"y": 39
},
{
"name": "中国香港特别行政区",
"icon": "//umami.guole.fun/images/country/hk.png",
"x": "HK",
"y": 10
},
{
"name": "新加坡",
"icon": "//umami.guole.fun/images/country/sg.png",
"x": "SG",
"y": 5
},
{
"name": "日本",
"icon": "//umami.guole.fun/images/country/jp.png",
"x": "JP",
"y": 3
},
{
"name": "美国",
"icon": "//umami.guole.fun/images/country/us.png",
"x": "US",
"y": 2
},
{
"name": "墨西哥",
"icon": "//umami.guole.fun/images/country/mx.png",
"x": "MX",
"y": 1
},
{
"name": "加拿大",
"icon": "//umami.guole.fun/images/country/ca.png",
"x": "CA",
"y": 1
},
{
"name": "台湾",
"icon": "//umami.guole.fun/images/country/tw.png",
"x": "TW",
"y": 1
},
{
"name": "澳大利亚",
"icon": "//umami.guole.fun/images/country/au.png",
"x": "AU",
"y": 1
}
],
"stats": {
"pageviews": {
"value": 194,
"prev": 0
},
"visitors": {
"value": 63,
"prev": 0
},
"visits": {
"value": 80,
"prev": 0
},
"bounces": {
"value": 51,
"prev": 0
},
"totaltime": {
"value": 9259,
"prev": 0
}
}
}

至此,接口完成。拿到数据随便怎么玩吧。其中name就是格式化后的名称,icon是图标,y就是对应数据值。

使用接口

直接把我自己的实现放出来吧。

之前参考洪哥页面,搞了个about页面,在其中插入这样的代码:

1
2
3
4
5
6
7
8
9
10
.author-content
.about-statistic.author-content-item
.card-content
.author-content-item-tips 网站数据
span.author-content-item-title 访问统计
#statistic
#stats
#metrics
.post-tips(style="opacity: 0.6;") * 数据来自:
a(href='https://umami.guole.fun/' target="_blank") Umami

在该文件末尾,插入一段脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
script.
//- 访问统计 umami
//- 这个 cachedData 方法,可以放到其他自定义脚本里,其他地方也可以用……为了演示方便,我写到这里了
function cachedData: (customCacheName) => {
const cacheName = customCacheName || 'default-cache'; // 提供默认缓存名称

return {
// 缓存数据
cache: function (key, value) {
if (!cacheName) {
throw new Error('Cache name is required'); // 抛出错误
} else {
localStorage.setItem(`${cacheName}-${key}`, JSON.stringify(value)); // 使用 localStorage 缓存数据
}
},

// 获取缓存数据
get: function (key) {
const value = localStorage.getItem(`${cacheName}-${key}`);
return value ? JSON.parse(value) : null; // 如果没有找到数据,返回 null
},

// 清除缓存数据
clear: function (key) {
localStorage.removeItem(`${cacheName}-${key}`);
},

// 检查缓存
has: function (key) {
return localStorage.getItem(`${cacheName}-${key}`) !== null; // 如果存在,返回 true
}
};
}

async function getToken() {
const url = '换成你获取 token 的地址'; // 似乎这玩意儿不会过期,反正我目前是假设它 4h 过期,然后去更新
try {
const res = await fetch(url);
if (!res.ok) {
throw new Error('Network response was not ok');
}
const data = await res.json();
return data.token;
} catch (error) {
console.error('Error fetching token:', error);
}
};

async function getData(token) {
const currentDate = new Date();
const endAt = new Date(currentDate);
endAt.setHours(23, 59, 59, 999);
const startAt = new Date(currentDate);
startAt.setDate(currentDate.getDate() - 30); //这里表示查询最近 30 天数据,可自行修改
startAt.setHours(0, 0, 0, 0);

const startAtTimestamp = startAt.getTime();
const endAtTimestamp = endAt.getTime();

// 如果没有有效缓存,进行网络请求
const url = `https://你的umami域名/api/websites/你的网站ID/getWebsiteMetrics?startAt=${startAtTimestamp}&endAt=${endAtTimestamp}&unit=hour&timezone=Asia%2FShanghai&compare=false&limit=5&type=os`;

try {
const res = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Umami-Share-Token': token
}
});

if (!res.ok) {
throw new Error('Network response was not ok');
}

const data = await res.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
}
};

(async () => {
let token, data;
const umamiCache = cachedData('umami');
const currentTime = Date.now();
const cacheDuration = 4 * 60 * 60 * 1000; // 缓存 4h

// 尝试获取已缓存数据
const cachedToken = await umamiCache.get('token');
const cachedData = await umamiCache.get('data');

if (!cachedToken || currentTime > parseInt(cachedToken.cacheTime, 10) + cacheDuration ) {
token = await getToken();
await umamiCache.cache('token', {
cacheTime: currentTime,
token: token
});
} else {
token = cachedToken.token;
}

if (!cachedData || currentTime > parseInt(cachedData.cacheTime, 10) + cacheDuration ) {
data = await getData(token);
await umamiCache.cache('data', {
cacheTime: currentTime,
data: data
});
} else {
data = cachedData.data;
}

const stats = document.getElementById("stats");
const metrics = document.getElementById("metrics");

const pageviews = data.stats.pageviews.value;
const visitors = data.stats.visitors.value;
const visits = data.stats.visits.value;

const countryHTML = data.country.map((item) => {
return `
<div class="list">
<div class="item">
<img class="loading nolazyload" alt="${item.x}" width="16" height="16" src="${item.icon}"></img>
<span class="name">${item.name}</span>
</div>
<div class="value">${item.y}</div>
</div>`
}).join('');
const browserHTML = data.browser.map((item) => {
return `
<div class="list">
<div class="item">
<img class="loading nolazyload" alt="${item.x}" width="16" height="16" src="${item.icon}"></img>
<span class="name">${item.name}</span>
</div>
<div class="value">${item.y}</div>
</div>`
}).join('');
const osHTML = data.os.map((item) => {
return `
<div class="list">
<div class="item">
<img class="loading nolazyload" alt="${item.x}" width="16" height="16" src="${item.icon}"></img>
<span class="name">${item.name}</span>
</div>
<div class="value">${item.y}</div>
</div>`
}).join('');

if (stats && metrics) {
stats.innerHTML += `<div><span>浏览量</span><span>${pageviews}</span></div>`;
stats.innerHTML += `<div><span>访问次数</span><span>${visitors}</span></div>`;
stats.innerHTML += `<div><span>访客</span><span>${visits}</span></div>`;
metrics.innerHTML += `
<div class="country"><span>访客国家/地区 (TOP5)</span>${countryHTML}</div>
<div class="os"><span>访客操作系统 (TOP5)</span>${osHTML}</div>
<div class="browser"><span>访客浏览器 (TOP5)</span>${browserHTML}</div>`;
if (window.lazyLoadInstance) window.lazyLoadInstance.update();
}
})();

收工~~