Web 前端开发模块检测究竟要如何写?

  • 栏目:知识 时间:2021-01-06 14:30 分享新闻到:
<返回列表

原题目:Web 前端开发模块检测究竟要如何写?

创作者:deepfunc

segmentfault/a/1190000015935519

伴随着 Web 运用的繁杂水平越来越越高,许多企业越来越越高度重视前端开发模块检测。大家见到的大多数数实例教程都是讲模块检测的关键性、一些有意味着性的检测架构 api 如何应用,但在具体新项目中模块检测要如何着手?检测测试用例应当包括什么实际內容呢?

文中从一个真正的运用情景考虑,从设计方案方式、编码构造来剖析模块检测应当包括什么內容,实际检测测试用例如何写,期待见到的朋友都能有一定的获得。

新项目采用的技术性架构

此项目选用 react技术性栈,采用的关键架构包含: react、 redux、 react-redux、 redux-actions、 reselect、 redux-saga、 seamless-immutable、 antd。
外贸网站建设报价

运用情景详细介绍

这一运用情景从 UI 层来说关键由2个一部分构成:

专用工具栏,包括更新按键、重要字检索框 报表展现,选用分页查询的方式访问

见到这儿有的朋友将会要说:切!那么简易的页面和业务流程逻辑性,還是真正情景吗,还必须写神马模块检测吗?

别着急,以便确保文章内容的阅读文章感受和长短适度,能讲明楚难题的简约情景便是好情景并不是吗?渐渐地向下看。

设计方案方式与构造剖析

在这里个情景设计方案开发设计中,大家严苛遵循 redux单边数据信息流 与 react-redux的最好实践活动,并选用 redux-saga来解决业务流程流, reselect来解决情况缓存文件,根据 fetch来启用后台管理插口,与真正的新项目沒有差别。

层次设计方案与编码机构以下所显示:

正中间 store中的內容全是 redux有关的,看名字应当都能了解含意了。

实际的编码可以看这儿:https://github/deepfunc/react-test-demo。

模块检测一部分详细介绍

先讲一下要来到什么检测架构和专用工具,关键內容包含:

jest ,检测架构 enzyme ,专测 react ui 层 sinon ,具备单独的 fakes、spies、stubs、mocks 作用库 nock ,仿真模拟 HTTP Server

假如有朋友对上边这种应用和配备不太熟得话,立即看官方网文本文档吧,比一切实例教程都写的好。

接下去,大家就刚开始撰写实际的检测测试用例编码了,下边会对于每一个方面得出编码片断调解析。那麼大家先从 actions刚开始吧。

为使文章内容尽可能简洁明了、清楚,下边的编码片断并不是每一个文档的详细內容,详细內容在这里里:https://github/deepfunc/react-test-demo。

actions

业务流程里边我应用了 redux-actions来造成 action,这儿劳动力具栏做实例,首先看一段业务流程编码:

import { createAction } from 'redux-actions'; import * as type from '../types/bizToolbar'; export const updateKeywords = createAction(type.BIZ_TOOLBAR_KEYWORDS_UPDATE); // ...

针对 actions检测,大家关键是认证造成的 action目标是不是恰当:

import * as type from '@/store/types/bizToolbar'; import * as actions from '@/store/actions/bizToolbar'; /* 检测 bizToolbar 有关 actions */ describe('bizToolbar actions', () = { /* 检测升级检索重要字 */ test('should create an action for update keywords', () = { // 搭建总体目标 action const keywords = 'some keywords'; const expectedAction = { type: type.BIZ_TOOLBAR_KEYWORDS_UPDATE, payload: keywords }; // 肯定 redux-actions 造成的 action 是不是恰当 expect(actions.updateKeywords(keywords)).toEqual(expectedAction); }); // ... });

这一检测测试用例的逻辑性非常简单,最先搭建一个大家期待的結果,随后启用业务流程编码,最终认证业务流程编码的运作結果与期待是不是一致。这便是写检测测试用例的基本招数。

大家在写检测测试用例时尽可能维持测试用例的单一岗位职责,不必遮盖过多不一样的业务流程范畴。检测测试用例总数能够有许多个,但每一个也不应当很繁杂。

reducers

然后是 reducers,仍然选用 redux-actions的 handleActions来撰写 reducer,这儿用报表的来做实例:

import { handleActions } from 'redux-actions'; import Immutable from 'seamless-immutable'; import * as type from '../types/bizTable'; /* 默认设置情况 */ export const defaultState = Immutable({ loading: false, pagination: { current: 1, pageSize: 15, total: 0 }, data: [] }); export default handleActions( { // ... /* 解决得到数据信息取得成功 */ [type.BIZ_TABLE_GET_RES_SUCCESS]: (state, {payload}) = { return state.merge( { loading: false, pagination: {total: payload.total}, data: payload.items }, {deep: true} ); }, // ... }, defaultState );

这儿的情况目标应用了 seamless-immutable。

针对 reducer,大家关键检测2个层面:

针对不明的 action.type ,是不是能回到当今情况。 针对每一个业务流程 type ,是不是都回到了历经恰当解决的情况。

下边是对于之上二点的检测编码:

import * as type from '@/store/types/bizTable'; import reducer, { defaultState } from '@/store/reducers/bizTable'; /* 检测 bizTable reducer */ describe('bizTable reducer', () = { /* 检测未特定 state 主要参数状况下回到当今默认设置 state */ test('should return the default state', () = { expect(reducer(undefined, {type: 'UNKNOWN'})).toEqual(defaultState); }); // ... /* 检测解决一切正常数据信息結果 */ test('should handle successful data response', () = { /* 仿真模拟回到数据信息結果 */ const payload = { items: [ {id: 1, code: '1'}, {id: 2, code: '2'} ], total: 2 }; /* 期待回到的情况 */ const expectedState = defaultState .setIn(['pagination', 'total'], payload.total) .set('data', payload.items) .set('loading', false); expect( reducer(defaultState, { type: type.BIZ_TABLE_GET_RES_SUCCESS, payload }) ).toEqual(expectedState); }); // ... });

这儿的检测测试用例逻辑性也非常简单,仍然是上边肯定期待結果的招数。下边是 selectors 的一部分。

selectors

selector的功效是获得相匹配业务流程的情况,这儿应用了 reselect来做缓存文件,避免 state未更改的状况下再次测算,首先看一下报表的 selector 编码:

import { createSelector } from 'reselect'; import * as defaultSettings from '@/utils/defaultSettingsUtil'; // ... const getBizTableState = (state) = state.bizTable; export const getBizTable = createSelector(getBizTableState, (bizTable) = { return bizTable.merge({ pagination: defaultSettings.pagination }, {deep: true}); });

这儿的分页查询器一部分主要参数在新项目中是统一设定,因此 reselect 非常好的进行了这一工作中:假如业务流程情况不会改变,立即回到之前的缓存文件。分页查询器默认设置设定以下:

export const pagination = { size: 'small', showTotal: (total, range) = `${range[0]}-${range[1]} / ${total}`, pageSizeOptions: ['15', '25', '40', '60'], showSizeChanger: true, showQuickJumper: true };

那麼大家的检测也关键是2个层面:

针对业务流程 selector ,是不是回到了恰当的內容。 缓存文件作用是不是一切正常。

检测编码以下:

import Immutable from 'seamless-immutable'; import { getBizTable } from '@/store/selectors'; import * as defaultSettingsUtil from '@/utils/defaultSettingsUtil'; /* 检测 bizTable selector */ describe('bizTable selector', () = { let state; beforeEach(() = { state = createState(); /* 每一个测试用例实行前重设缓存文件测算频次 */ getBizTable.resetRecomputations(); }); function createState() { return Immutable({ bizTable: { loading: false, pagination: { current: 1, pageSize: 15, total: 0 }, data: [] } }); } /* 检测回到恰当的 bizTable state */ test('should return bizTable state', () = { /* 业务流程情况 ok 的 */ expect(getBizTable(state)).toMatchObject(state.bizTable); /* 分页查询默认设置主要参数设定 ok 的 */ expect(getBizTable(state)).toMatchObject({ pagination: defaultSettingsUtil.pagination }); }); /* 检测 selector 缓存文件是不是合理 */ test('check memoization', () = { getBizTable(state); /* 第一次测算,缓存文件测算频次为 1 */ expect(getBizTable.recomputations()).toBe(1); getBizTable(state); /* 业务流程情况不会改变的状况下,缓存文件测算频次应当還是 1 */ expect(getBizTable.recomputations()).toBe(1); const newState = state.setIn(['bizTable', 'loading'], true); getBizTable(newState); /* 业务流程情况更改了,缓存文件测算频次应当是 2 了 */ expect(getBizTable.recomputations()).toBe(2); }); });

检测测试用例仍然非常简单有没有?维持这一节奏感就正确了。下边来说下略微有点儿繁杂的地区,sagas 一部分。

sagas

这儿我用了 redux-saga解决业务流程流,这儿实际也便是多线程启用 api 恳求数据信息,解决取得成功結果和不正确結果等。

将会有的朋友感觉搞那么繁杂干什么,多线程恳求用个 redux-thunk不就完了了没有?别着急,细心看了你也就搞清楚了。

这儿必须大约详细介绍下 redux-saga的工作中方法。saga 是一种 es6的转化成器涵数 - Generator ,大家运用他来造成各种各样申明式的 effects,由 redux-saga模块来消化吸收解决,促进业务流程开展。

这儿大家看来看获得报表数据信息的业务流程编码:

import { all, takeLatest, put, select, call } from 'redux-saga/effects'; import * as type from '../types/bizTable'; import * as actions from '../actions/bizTable'; import { getBizToolbar, getBizTable } from '../selectors'; import * as api from '@/services/bizApi'; // ... export function* onGetBizTableData() { /* 先获得 api 启用必须的主要参数:重要字、分页查询信息内容等 */ const {keywords} = yield select(getBizToolbar); const {pagination} = yield select(getBizTable); const payload = { keywords, paging: { skip: (pagination.current - 1) * pagination.pageSize, max: pagination.pageSize } }; try { /* 启用 api */ const result = yield call(api.getBizTableData, payload); /* 一切正常回到 */ yield put(actions.putBizTableDataSuccessResult(result)); } catch (err) { /* 不正确回到 */ yield put(actions.putBizTableDataFailResult()); } }

不太熟悉 redux-saga的朋友都不要太在乎编码的实际书写,看注解应当能掌握这一业务流程的实际流程:

从相匹配的 state 里取到启用 api 时要要的主要参数一部分(检索重要字、分页查询),这儿启用了刚刚的 selector。 组成好主要参数并启用相匹配的 api 层。 假如一切正常回到結果,则推送取得成功 action 通告 reducer 升级情况。 假如不正确回到,则推送不正确 action 通告 reducer。

那麼实际的检测测试用例应当如何写呢?大家都了解这类业务流程编码涉及到来到 api 或别的层的启用,假如要写模块检测务必做一些 mock 这类来避免真实启用 api 层,下边大家看来一下 如何对于这一 saga 来写检测测试用例:

import { put, select } from 'redux-saga/effects'; // ... /* 检测获得数据信息 */ test('request data, check success and fail', () = { /* 当今的业务流程情况 */ const state = { bizToolbar: { keywords: 'some keywords' }, bizTable: { pagination: { current: 1, pageSize: 15 } } }; const gen = cloneableGenerator(saga.onGetBizTableData)(); /* 1. 是不是启用了恰当的 selector 来得到恳求时要推送的主要参数 */ expect(gen.next().value).toEqual(select(getBizToolbar)); expect(gen.next(state.bizToolbar).value).toEqual(select(getBizTable)); /* 2. 是不是启用了 api 层 */ const callEffect = gen.next(state.bizTable).value; expect(callEffect['CALL'].fn).toBe(api.getBizTableData); /* 启用 api 层主要参数是不是传送恰当 */ expect(callEffect['CALL'].args[0]).toEqual({ keywords: 'some keywords', paging: {skip: 0, max: 15} }); /* 3. 仿真模拟恰当回到支系 */ const successBranch = gen.clone(); const successRes = { items: [ {id: 1, code: '1'}, {id: 2, code: '2'} ], total: 2 }; expect(successBranch.next(successRes).value).toEqual( put(actions.putBizTableDataSuccessResult(successRes))); expect(successBranch.next().done).toBe(true); /* 4. 仿真模拟不正确回到支系 */ const failBranch = gen.clone(); expect(failBranch.throw(new Error('仿真模拟造成出现异常')).value).toEqual( put(actions.putBizTableDataFailResult())); expect(failBranch.next().done).toBe(true); });

这一检测测试用例对比前边的繁杂了一些,大家先来讲下检测 saga 的基本原理。前边说过 saga 具体上是回到各种各样申明式的 effects,随后由模块来真实实行。因此大家检测的目地便是需看 effects的造成是不是合乎预估。那麼 effect究竟是个神马物品呢?实际上便是字面上量目标!

大家能够用在业务流程编码一样的方法来造成这种字面上量目标,针对字面上量目标的肯定就十分简易了,而且沒有立即启用 api 层,就用不到做 mock 咯!这一检测测试用例的流程便是运用转化成器涵数一步歩的造成下一个 effect,随后肯定较为。

从上边的注解 3、4 能看到, redux-saga还出示了一些輔助涵数来便捷的解决支系断点。

这也就是我挑选 redux-saga的缘故:强劲而且有利于检测。

api 和 fetch 专用工具库

接下去便是api 层有关的了。前边讲过启用后台管理恳求是用的 fetch,我封裝了2个方式来简单化启用和結果解决: getJSON()、 postJSON(),各自相匹配 GET 、POST 恳求。先看来看 api 层编码:

import { fetcher } from '@/utils/fetcher'; export function getBizTableData(payload) { return fetcher.postJSON('/api/biz/get-table', payload); }

业务流程编码非常简单,那麼检测测试用例也非常简单:

import sinon from 'sinon'; import { fetcher } from '@/utils/fetcher'; import * as api from '@/services/bizApi'; /* 检测 bizApi */ describe('bizApi', () = { let fetcherStub; beforeAll(() = { fetcherStub = sinon.stub(fetcher); }); // ... /* getBizTableData api 应当启用恰当的 method 和传送恰当的主要参数 */ test('getBizTableData api should call postJSON with right params of fetcher', () = { /* 仿真模拟主要参数 */ const payload = {a: 1, b: 2}; api.getBizTableData(payload); /* 查验是不是启用了专用工具库 */ expect(fetcherStub.postJSON.callCount).toBe(1); /* 查验启用主要参数是不是恰当 */ expect(fetcherStub.postJSON.lastCall.calledWith('/api/biz/get-table', payload)).toBe(true); }); });

因为 api 层立即启用了专用工具库,因此这儿用 sinon.stub()来更换专用工具库做到检测目地。

然后便是检测自身封裝的 fetch 专用工具库了,这儿 fetch 我是用的 isomorphic-fetch,因此挑选了 nock来仿真模拟 Server 开展检测,关键是检测一切正常浏览回到結果和仿真模拟网络服务器出现异常等,实例片断以下:

import nock from 'nock'; import { fetcher, FetchError } from '@/utils/fetcher'; /* 检测 fetcher */ describe('fetcher', () = { afterEach(() = { nock.cleanAll(); }); afterAll(() = { nock.restore(); }); /* 检测 getJSON 得到一切正常数据信息 */ test('should get success result', () = { nock('http://some') .get('/test') .reply(200, {success: true, result: 'hello, world'}); return expect(fetcher.getJSON('http://some/test')).resolves.toMatch(/^hello.+$/); }); // ... /* 检测 getJSON 捕捉 server 超过 400 的出现异常情况 */ test('should catch server status: 400+', (done) = { const status = 500; nock('http://some') .get('/test') .reply(status); fetcher.getJSON('http://some/test').catch((error) = { expect(error).toEqual(expect.any(FetchError)); expect(error).toHaveProperty('detail'); expect(error.detail.status).toBe(status); done(); }); }); /* 检测 getJSON 传送恰当的 headers 和 query strings */ test('check headers and query string of getJSON()', () = { nock('http://some', { reqheaders: { 'Accept': 'application/json', 'authorization': 'Basic Auth' } }) .get('/test') .query({a: '123', b: 456}) .reply(200, {success: true, result: true}); const headers = new Headers(); headers.append('authorization', 'Basic Auth'); return expect(fetcher.getJSON( 'http://some/test', {a: '123', b: 456}, headers)).resolves.toBe(true); }); // ... });

基本也没有什么繁杂的,关键留意 fetch 是 promise 回到, jest的各种各样多线程检测计划方案都能非常好考虑。

剩余的一部分便是跟 UI 有关的了。

器皿部件

器皿部件的关键目地是传送 state 和 actions,看看专用工具栏的器皿部件编码:

import { connect } from 'react-redux'; import { getBizToolbar } from '@/store/selectors'; import * as actions from '@/store/actions/bizToolbar'; import BizToolbar from '@/components/BizToolbar'; const mapStateToProps = (state) = ({ ...getBizToolbar(state) }); const mapDispatchToProps = { reload: actions.reload, updateKeywords: actions.updateKeywords }; export default connect(mapStateToProps, mapDispatchToProps)(BizToolbar);

那麼检测测试用例的目地也是查验这种,这儿应用了 redux-mock-store来仿真模拟 redux 的 store :

import React from 'react'; import { shallow } from 'enzyme'; import configureStore from 'redux-mock-store'; import BizToolbar from '@/containers/BizToolbar'; /* 检测器皿部件 BizToolbar */ describe('BizToolbar container', () = { const initialState = { bizToolbar: { keywords: 'some keywords' } }; const mockStore = configureStore(); let store; let container; beforeEach(() = { store = mockStore(initialState); container = shallow( BizToolbar store={store}/ }); /* 检测 state 到 props 的投射是不是恰当 */ test('should pass state to props', () = { const props = container.props(); expect(props).toHaveProperty('keywords', initialState.bizToolbar.keywords); }); /* 检测 actions 到 props 的投射是不是恰当 */ test('should pass actions to props', () = { const props = container.props(); expect(props).toHaveProperty('reload', expect.any(Function)); expect(props).toHaveProperty('updateKeywords', expect.any(Function)); }); });

非常简单有没有,因此也没啥可说的了。

UI 部件

这儿以报表部件做为实例,大家将立即看来检测测试用例是如何写。一般来讲 UI 部件大家关键检测下列好多个层面:

是不是3D渲染了恰当的 DOM 构造 款式是不是恰当 业务流程逻辑性开启是不是恰当

下边是检测测试用例编码:

import React from 'react'; import { mount } from 'enzyme'; import sinon from 'sinon'; import { Table } from 'antd'; import * as defaultSettingsUtil from '@/utils/defaultSettingsUtil'; import BizTable from '@/components/BizTable'; /* 检测 UI 部件 BizTable */ describe('BizTable component', () = { const defaultProps = { loading: false, pagination: Object.assign({}, { current: 1, pageSize: 15, total: 2 }, defaultSettingsUtil.pagination), data: [{id: 1}, {id: 2}], getData: sinon.fake(), updateParams: sinon.fake() }; let defaultWrapper; beforeEach(() = { defaultWrapper = mount( BizTable {...defaultProps}/ }); // ... /* 检测是不是3D渲染了恰当的作用子部件 */ test('should render table and pagination', () = { /* 是不是3D渲染了 Table 部件 */ expect(defaultWrapper.find(Table).exists()).toBe(true); /* 是不是3D渲染了 分页查询器 部件,款式是不是恰当(mini) */ expect(defaultWrapper.find('.ant-table-pagination.mini').exists()).toBe(true); }); /* 检测初次载入时数据信息目录为空是不是进行载入数据信息恳求 */ test('when componentDidMount and data is empty, should getData', () = { sinon.spy(BizTable.prototype, 'componentDidMount'); const props = Object.assign({}, defaultProps, { pagination: Object.assign({}, { current: 1, pageSize: 15, total: 0 }, defaultSettingsUtil.pagination), data: [] }); const wrapper = mount( BizTable {...props}/ expect(BizTable.prototypeponentDidMount.calledOnce).toBe(true); expect(props.getData.calledOnce).toBe(true); BizTable.prototypeponentDidMount.restore(); }); /* 检测 table 换页后是不是恰当开启 updateParams */ test('when change pagination of table, should updateParams', () = { const table = defaultWrapper.find(Table); table.props().onChange({current: 2, pageSize: 25}); expect(defaultProps.updateParams.lastCall.args[0]) .toEqual({paging: {current: 2, pageSize: 25}}); }); });

归功于设计方案层次的有效性,大家非常容易运用结构 props来做到检测目地,融合 enzyme和 sinon,检测测试用例仍然维持简易的节奏感。

小结

之上便是这一情景详细的检测测试用例撰写构思和实例编码,原文中谈及的构思方式也彻底能够用在 Vue、 Angular新项目上。详细的编码內容在 这儿 (关键的事儿多讲几遍,诸位朋友感觉好帮助去给个 :star: 哈)。

最终大家能够运用遮盖率看来下要例的遮盖水平是不是充足(一般来讲无需有意追求完美 100%,依据具体状况而定):

模块检测是 TDD 检测驱动器开发设计的基本。从之上全部全过程能看出,好的设计方案层次是非常容易撰写检测测试用例的,模块检测不光单仅仅以便确保编码品质:他会逼着你思索编码设计方案的有效性,回绝面条编码 :muscle:

使用 Clean Code 的完毕语:

2005 年,在报名参加于丹佛举办的灵巧交流会时,Elisabeth Hedrickson 拿给我一条相近 Lance Armstrong 热卖的那类翠绿色腕带。这条腕携带面写着“迷恋检测”(Test Obsessed)的字眼。我开心地戴上,并引以为豪地一直系着。自打 1999 年从 Kent Beck 那里学得 TDD 至今,我确实迷到了检测驱动器开发设计。

但是跟随就产生了些奇事。发了现自身没法取下腕带。不但是由于腕带太紧,并且那也是条精神实质上的紧箍咒。那腕带便是我岗位社会道德的宣布,也就是我服务承诺尽己能够写成最好编码的提醒。取下它,好像便是违反了这种宣布和服务承诺一样。

因此它仍在我的手段上。在敲代码时,我用视线瞟见它。它一直提示我,我干了写成干净整洁编码的服务承诺。

H5网站模版全集

无需敲代码自身就可以动手能力嵌套循环一个网站

领到

戳“阅读文章全文!回到凡科,查询大量

义务编写:

分享新闻到:

更多阅读

Web 前端开发模块检测究竟要如何写?

知识 2021-01-06
原题目:Web 前端开发模块检测究竟要如何写? 创作者:deepfunc segmentfault/a/1190000015935519 伴随着...
查看全文

六分钟600W次散播,微信朋友圈广告宣传有

知识 2021-01-06
原题目:六分钟600W次散播,微信朋友圈广告宣传有啥招数? 需看什么叫有吸引住力的微信朋友...
查看全文

手机微信微信朋友圈广告宣传营销推广方

知识 2021-01-06
原题目:手机微信微信朋友圈广告宣传营销推广方法小结 和新浪微博一样,手机微信在营销推...
查看全文
返回全部新闻


区域站点: 南丰县h5和小程序有什么区别   南宫市h5微信   囊谦县h5免费   南和县h5抽奖大转盘制作   南华县h5和小程序有什么区别   南江县h5微信   南京市h5免费   南靖县h5抽奖大转盘制作   南康市h5和小程序有什么区别   南乐县h5微信   南陵县h5免费   南宁市h5抽奖大转盘制作   南平市h5和小程序有什么区别   南皮县h5微信   南市区h5免费   南通市h5抽奖大转盘制作   南投县h5和小程序有什么区别   南雄市h5微信   南溪县h5免费   南阳市h5抽奖大转盘制作   南漳县h5和小程序有什么区别   南召县h5微信   南郑县h5免费   那坡县h5抽奖大转盘制作   那曲县h5和小程序有什么区别   纳雍县h5微信   讷河市h5免费   内黄县h5抽奖大转盘制作   内江市h5和小程序有什么区别   内丘县h5微信   内乡县h5免费   嫩江市h5抽奖大转盘制作   聂荣县h5和小程序有什么区别   尼玛县h5微信   尼木县h5免费   宁安市h5抽奖大转盘制作   宁波市h5和小程序有什么区别   宁城县h5微信   宁德市h5免费   宁都县h5抽奖大转盘制作   宁国市h5和小程序有什么区别   宁海县h5微信   宁化县h5免费   宁晋县h5抽奖大转盘制作   宁陵县h5和小程序有什么区别   宁明县h5微信   宁南县h5免费   宁强县h5抽奖大转盘制作   宁陕县h5和小程序有什么区别   宁武县h5微信   宁乡市h5免费   宁阳县h5抽奖大转盘制作   宁远县h5和小程序有什么区别   农安县h5微信   磐安县h5免费   盘锦市h5抽奖大转盘制作   盘山县h5和小程序有什么区别   磐石市h5微信   盘州市h5免费   蓬安县h5抽奖大转盘制作   澎湖县h5和小程序有什么区别   蓬莱市h5微信   彭山县h5免费   蓬溪县h5抽奖大转盘制作   彭阳县h5和小程序有什么区别   彭泽县h5微信   彭州市h5免费   偏关县h5抽奖大转盘制作   平安县h5和小程序有什么区别   平昌县h5微信   平定县h5免费   屏东县h5抽奖大转盘制作   平度市h5和小程序有什么区别   平果县h5微信   平和县h5免费   平湖市h5抽奖大转盘制作   平江县h5和小程序有什么区别   平乐县h5微信   平凉市h5免费   平利县h5抽奖大转盘制作   平罗县h5和小程序有什么区别   平陆县h5微信   屏南县h5免费   平泉市h5抽奖大转盘制作   屏山县h5和小程序有什么区别   平顺县h5微信   平塘县h5免费   平潭县h5抽奖大转盘制作   平武县h5和小程序有什么区别   萍乡市h5微信   平乡县h5免费   平阳县h5抽奖大转盘制作   平遥县h5和小程序有什么区别   平阴县h5微信   平邑县h5免费   平远县h5抽奖大转盘制作   平舆县h5和小程序有什么区别   皮山县h5微信   普安县h5免费   浦北县h5抽奖大转盘制作   浦城县h5和小程序有什么区别   普洱市h5微信   普格县h5免费   浦江县h5抽奖大转盘制作   普兰县h5和小程序有什么区别   普宁市h5微信   莆田市h5免费   迁安市h5抽奖大转盘制作   乾安县h5和小程序有什么区别   潜江市h5微信   潜山市h5免费  

友情链接: 月福步宜 微信小游戏h5 超恒鑫电子 h5免费制作 h5商城 h5 大转盘

Copyright © 2002-2020 h5免费_h5抽奖大转盘制作_h5和小程序有什么区别_h5微信_手机抽奖小程序 版权所有 (网站地图) 备案号:粤ICP备10235580号