From d737f3db7b5daa01c31a52534883673bb7be32ba Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Mon, 20 Jan 2025 16:57:21 +0800 Subject: [PATCH 001/126] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96recy?= =?UTF-8?q?cle=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/web/mpx-recycle-item.vue | 13 + .../components/web/mpx-recycle-list.vue | 262 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-item.vue create mode 100644 packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-list.vue diff --git a/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-item.vue b/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-item.vue new file mode 100644 index 0000000000..29ac7c86fc --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-item.vue @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-list.vue b/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-list.vue new file mode 100644 index 0000000000..3a9cc0aadd --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-list.vue @@ -0,0 +1,262 @@ + + + + + From 7de08fbf0672f63fb6de0a49ac2463a0ee32eb68 Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Tue, 21 Jan 2025 17:03:15 +0800 Subject: [PATCH 002/126] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96recy?= =?UTF-8?q?cle=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/wx/mpx-recycle-view.mpx | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx new file mode 100644 index 0000000000..9c9185c5a6 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -0,0 +1,198 @@ + + + + From 78c6340a9a8f6754d9de470e5498148327ac336e Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Wed, 22 Jan 2025 19:57:47 +0800 Subject: [PATCH 003/126] =?UTF-8?q?chore:=20=E5=A2=9E=E5=BC=BArecycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/wx/mpx-recycle-view.mpx | 150 ++++++++++++------ 1 file changed, 98 insertions(+), 52 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx index 9c9185c5a6..f945adc7cb 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -2,11 +2,22 @@ - + - + @@ -25,11 +36,14 @@ import { createComponent } from '@mpxjs/core' createComponent({ properties: { - scrollX: Boolean, scrollY: Boolean, height: { - type: String, - value: '100%' + type: Number, + value: 0 + }, + width: { + type: Number, + value: 0 }, // 预估尺寸 itemSize: Object, @@ -37,10 +51,24 @@ createComponent({ type: Number, value: 2 }, - listData: Array + listData: Array, + scrollTop: { + type: Number, + value: 0 + }, + scrollWithAnimation: Boolean, + enableBackToTop: Boolean, + lowerThreshold: { + type: Number, + value: 50 + }, + upperThreshold: { + type: Number, + value: 50 + } }, data: { - screenHeight: 0, + containerHeight: 0, start: 0, end: 0, contentStyle: '', @@ -48,6 +76,12 @@ createComponent({ positions: [] }, computed: { + _width() { + return this.width ? `${this.width}px` : '100%' + }, + _height() { + return this.height ? `${this.height}px` : '100%' + }, _listData() { return this.listData.map((item, index) => { return { @@ -57,16 +91,13 @@ createComponent({ }) }, visibleCount() { - return Math.ceil(this.screenHeight / this.itemSize.height) + return Math.ceil(this.containerHeight / this.itemSize.height) }, aboveCount() { return Math.min(this.start, this.bufferScale * this.visibleCount) }, belowCount() { - return Math.min( - this.listData.length - this.end, - this.bufferScale * this.visibleCount - ) + return Math.min(this.listData.length - this.end, this.bufferScale * this.visibleCount) }, visibleData() { const start = this.start - this.aboveCount @@ -90,18 +121,27 @@ createComponent({ }) }, immediate: true + }, + scrollTop: { + handler(val) { + this.start = this.getStartIndex(val) + this.end = this.start + this.visibleCount + this.setStartOffset() + } } }, - attached() { + ready() { this.initPositions() const query = wx.createSelectorQuery().in(this) - setTimeout(() => { - query.select('.mpx-recycle-list').boundingClientRect(rect => { - this.screenHeight = rect.height - this.start = 0 + query + .select('.mpx-recycle-list') + .boundingClientRect((rect) => { + this.containerHeight = rect.height + this.start = this.getStartIndex(this.scrollTop) this.end = this.start + this.visibleCount - }).exec() - }, 300) + this.setStartOffset() + }) + .exec() }, methods: { initPositions() { @@ -141,10 +181,10 @@ createComponent({ let startOffset if (this.start >= 1) { const size = - this.positions[this.start].top - - (this.positions[this.start - this.aboveCount] - ? this.positions[this.start - this.aboveCount].top - : 0) + this.positions[this.start].top - + (this.positions[this.start - this.aboveCount] + ? this.positions[this.start - this.aboveCount].top + : 0) startOffset = this.positions[this.start - 1].bottom - size } else { startOffset = 0 @@ -156,43 +196,49 @@ createComponent({ this.start = this.getStartIndex(scrollTop) this.end = this.start + this.visibleCount this.setStartOffset() + this.triggerEvent('scroll', e) + }, + onScrollToUpper(e) { + this.triggerEvent('scrolltoupper', e) + }, + onScrollToLower(e) { + this.triggerEvent('scrolltolower', e) } } }) From e93b931623be2742c9a0d33c3371be9e0c201bea Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Wed, 22 Jan 2025 20:02:33 +0800 Subject: [PATCH 004/126] =?UTF-8?q?chore:=20=E5=A2=9E=E5=BC=BArecycle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wx/mpx-recycle-item-default.mpx | 21 +++++++++++++++++++ .../components/wx/mpx-recycle-view.mpx | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx new file mode 100644 index 0000000000..2501024b06 --- /dev/null +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx @@ -0,0 +1,21 @@ + + + diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx index f945adc7cb..d91432ec9b 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -212,7 +212,7 @@ createComponent({ "component": true, "componentGenerics": { "recycle-item": { - "default": "../components/test" + "default": "./mpx-recycle-item-default" } } } From db5ed91be324edeebe8e7e1a6454efcbdf61e88e Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Thu, 23 Jan 2025 17:02:44 +0800 Subject: [PATCH 005/126] =?UTF-8?q?fix:=20ali&web=E7=8E=AF=E5=A2=83scrollT?= =?UTF-8?q?op=E5=88=9D=E5=A7=8B=E5=8C=96=E4=B8=8D=E7=94=9F=E6=95=88?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/wx/mpx-recycle-view.mpx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx index d91432ec9b..aa6a8bac20 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -2,7 +2,7 @@ { + this.isReady = true + }) + } // 更新真实偏移量 this.setStartOffset() }) @@ -127,7 +137,8 @@ createComponent({ this.start = this.getStartIndex(val) this.end = this.start + this.visibleCount this.setStartOffset() - } + }, + immediate: true } }, ready() { @@ -212,7 +223,7 @@ createComponent({ "component": true, "componentGenerics": { "recycle-item": { - "default": "./mpx-recycle-item-default" + "default": "./mpx-recycle-item-default.mpx" } } } From b67f4a059876f11b4b910617408a05be3bbf15fc Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Wed, 5 Feb 2025 15:42:39 +0800 Subject: [PATCH 006/126] =?UTF-8?q?chore:=20=E9=80=82=E9=85=8Drn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/components/wx/mpx-recycle-item-default.mpx | 10 +++++----- .../lib/runtime/components/wx/mpx-recycle-view.mpx | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx index 2501024b06..5cf26e0eb4 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx @@ -12,10 +12,10 @@ border-radius 8px box-shadow 0 2px 4px rgba(0,0,0,0.1) - .item-content - display flex - flex-direction column - font-size 14px - color #666 +.item-content + display flex + flex-direction column + font-size 14px + color #666 diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx index aa6a8bac20..4a08981de4 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -1,6 +1,7 @@ - - \ No newline at end of file diff --git a/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-list.vue b/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-list.vue deleted file mode 100644 index 3a9cc0aadd..0000000000 --- a/packages/webpack-plugin/lib/runtime/components/web/mpx-recycle-list.vue +++ /dev/null @@ -1,262 +0,0 @@ - - - - - diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx index ac38b2d25d..066864edb3 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -2,12 +2,14 @@ Date: Mon, 10 Feb 2025 16:51:08 +0800 Subject: [PATCH 010/126] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=E8=8A=82?= =?UTF-8?q?=E6=B5=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/runtime/components/wx/mpx-recycle-view.mpx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx index 066864edb3..37b04cc613 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -71,6 +71,10 @@ createComponent({ scrollOptions: { type: Object, value: {} + }, + scrollThrottleDelay: { + type: Number, + value: 16 } }, data: { @@ -80,7 +84,8 @@ createComponent({ placeholderStyle: '', containerHeight: 0, positions: [], - isReady: false + isReady: false, + lastScrollTime: 0 }, computed: { _width() { @@ -209,6 +214,11 @@ createComponent({ this.contentStyle = `transform: translateY(${startOffset}px);` }, onScroll(e) { + const now = Date.now() + if (now - this.lastScrollTime < this.scrollThrottleDelay) { + return + } + this.lastScrollTime = now const { scrollTop } = e.detail this.start = this.getStartIndex(scrollTop) this.end = this.start + this.visibleCount From 3033c17c34201534735e6691e651c103c6612a17 Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Mon, 10 Feb 2025 20:40:23 +0800 Subject: [PATCH 011/126] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E4=BC=A0=E9=80=92=E5=A4=9A=E4=B8=AA=E6=8A=BD=E8=B1=A1?= =?UTF-8?q?=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs-vuepress/.vuepress/dist/404.html | 25 + .../.vuepress/dist/api/ApiIndex.html | 23 + .../.vuepress/dist/api/app-config.html | 116 ++ docs-vuepress/.vuepress/dist/api/builtIn.html | 56 + docs-vuepress/.vuepress/dist/api/compile.html | 1214 +++++++++++++++++ .../.vuepress/dist/api/composition-api.html | 117 ++ .../.vuepress/dist/api/directives.html | 434 ++++++ docs-vuepress/.vuepress/dist/api/extend.html | 365 +++++ .../.vuepress/dist/api/global-api.html | 253 ++++ docs-vuepress/.vuepress/dist/api/index.html | 43 + .../.vuepress/dist/api/instance-api.html | 265 ++++ .../.vuepress/dist/api/optional-api.html | 51 + .../.vuepress/dist/api/reactivity-api.html | 544 ++++++++ .../.vuepress/dist/api/store-api.html | 229 ++++ .../.vuepress/dist/articles/1.0.html | 54 + .../.vuepress/dist/articles/2.0.html | 58 + .../.vuepress/dist/articles/2.7-release.html | 63 + .../dist/articles/2.8-release-alter.html | 174 +++ .../.vuepress/dist/articles/2.8-release.html | 182 +++ .../dist/articles/2.9-release-alter.html | 277 ++++ .../.vuepress/dist/articles/2.9-release.html | 285 ++++ .../.vuepress/dist/articles/index.html | 43 + .../.vuepress/dist/articles/mpx-cli-next.html | 137 ++ .../.vuepress/dist/articles/mpx-cube-ui.html | 56 + .../.vuepress/dist/articles/mpx1.html | 55 + .../.vuepress/dist/articles/mpx2.html | 740 ++++++++++ .../.vuepress/dist/articles/performance.html | 60 + .../.vuepress/dist/articles/size-control.html | 51 + .../dist/articles/ts-derivation.html | 273 ++++ .../.vuepress/dist/articles/unit-test.html | 461 +++++++ .../dist/assets/css/0.styles.f22478ca.css | 3 + .../dist/assets/img/search.83621669.svg | 1 + .../dist/assets/img/start-tips1.3b76ac97.png | Bin 0 -> 39681 bytes .../dist/assets/img/start-tips2.7d8836f8.png | Bin 0 -> 51539 bytes .../.vuepress/dist/assets/js/1.4f7abe8f.js | 1 + .../.vuepress/dist/assets/js/10.0192acce.js | 1 + .../.vuepress/dist/assets/js/100.f31ffd06.js | 1 + .../.vuepress/dist/assets/js/101.283d0363.js | 1 + .../.vuepress/dist/assets/js/102.094a0a16.js | 1 + .../.vuepress/dist/assets/js/103.b34f6752.js | 1 + .../.vuepress/dist/assets/js/104.78f6358e.js | 1 + .../.vuepress/dist/assets/js/105.eb6680fc.js | 1 + .../.vuepress/dist/assets/js/106.471f03cf.js | 1 + .../.vuepress/dist/assets/js/107.63fd18fd.js | 1 + .../.vuepress/dist/assets/js/108.614260e9.js | 1 + .../.vuepress/dist/assets/js/109.3da50cc1.js | 1 + .../.vuepress/dist/assets/js/11.d7a8d045.js | 1 + .../.vuepress/dist/assets/js/110.f74ff3a3.js | 1 + .../.vuepress/dist/assets/js/111.51932e54.js | 1 + .../.vuepress/dist/assets/js/112.da7bd3e7.js | 1 + .../.vuepress/dist/assets/js/113.caaf11a3.js | 1 + .../.vuepress/dist/assets/js/114.16cda7ac.js | 1 + .../.vuepress/dist/assets/js/115.88124b12.js | 1 + .../.vuepress/dist/assets/js/116.4a116234.js | 1 + .../.vuepress/dist/assets/js/117.e45c1b95.js | 1 + .../.vuepress/dist/assets/js/118.9cbac09e.js | 1 + .../.vuepress/dist/assets/js/119.312e4c21.js | 1 + .../.vuepress/dist/assets/js/12.4d7217be.js | 1 + .../.vuepress/dist/assets/js/120.7a6b4035.js | 1 + .../.vuepress/dist/assets/js/14.7377c68f.js | 1 + .../.vuepress/dist/assets/js/15.6f885a83.js | 1 + .../.vuepress/dist/assets/js/16.974f551e.js | 1 + .../.vuepress/dist/assets/js/17.e94e6745.js | 1 + .../.vuepress/dist/assets/js/18.b1f7a4a2.js | 1 + .../.vuepress/dist/assets/js/19.62186e92.js | 1 + .../.vuepress/dist/assets/js/2.314c65b9.js | 1 + .../.vuepress/dist/assets/js/20.e9cd4de3.js | 1 + .../.vuepress/dist/assets/js/21.217f8fa9.js | 1 + .../.vuepress/dist/assets/js/22.4cd3344f.js | 1 + .../.vuepress/dist/assets/js/23.365f9a87.js | 1 + .../.vuepress/dist/assets/js/24.17777a13.js | 1 + .../.vuepress/dist/assets/js/25.889c07d7.js | 1 + .../.vuepress/dist/assets/js/26.3aa9f276.js | 1 + .../.vuepress/dist/assets/js/27.1a35170a.js | 1 + .../.vuepress/dist/assets/js/28.3670fa46.js | 1 + .../.vuepress/dist/assets/js/29.a911e510.js | 1 + .../.vuepress/dist/assets/js/3.145e69cd.js | 1 + .../.vuepress/dist/assets/js/30.04d1a7ea.js | 1 + .../.vuepress/dist/assets/js/31.ec06bf2f.js | 1 + .../.vuepress/dist/assets/js/32.f29d2cd1.js | 1 + .../.vuepress/dist/assets/js/33.936d018e.js | 1 + .../.vuepress/dist/assets/js/34.f494d685.js | 1 + .../.vuepress/dist/assets/js/35.c3ed1ff7.js | 1 + .../.vuepress/dist/assets/js/36.d2c9afa9.js | 1 + .../.vuepress/dist/assets/js/37.4e1eee2c.js | 1 + .../.vuepress/dist/assets/js/38.7826170e.js | 1 + .../.vuepress/dist/assets/js/39.094ffa40.js | 1 + .../.vuepress/dist/assets/js/4.9489102c.js | 1 + .../.vuepress/dist/assets/js/40.10d90636.js | 1 + .../.vuepress/dist/assets/js/41.8351af86.js | 1 + .../.vuepress/dist/assets/js/42.53972f6c.js | 1 + .../.vuepress/dist/assets/js/43.e2b432ea.js | 1 + .../.vuepress/dist/assets/js/44.e4242a1f.js | 1 + .../.vuepress/dist/assets/js/45.392413aa.js | 1 + .../.vuepress/dist/assets/js/46.22f9a9c6.js | 1 + .../.vuepress/dist/assets/js/47.2bd2cecc.js | 1 + .../.vuepress/dist/assets/js/48.55792a30.js | 1 + .../.vuepress/dist/assets/js/49.cde94a79.js | 1 + .../.vuepress/dist/assets/js/5.12d049b6.js | 1 + .../.vuepress/dist/assets/js/50.42e34e52.js | 1 + .../.vuepress/dist/assets/js/51.90dfa84e.js | 1 + .../.vuepress/dist/assets/js/52.10bb0251.js | 1 + .../.vuepress/dist/assets/js/53.7207ffc5.js | 1 + .../.vuepress/dist/assets/js/54.af3d3dcd.js | 1 + .../.vuepress/dist/assets/js/55.2d6ae2e1.js | 1 + .../.vuepress/dist/assets/js/56.9aec9a9e.js | 1 + .../.vuepress/dist/assets/js/57.9d9ddcbd.js | 1 + .../.vuepress/dist/assets/js/58.50eb3b91.js | 1 + .../.vuepress/dist/assets/js/59.27106fbb.js | 1 + .../.vuepress/dist/assets/js/6.6ea91b28.js | 1 + .../.vuepress/dist/assets/js/60.a47e5279.js | 1 + .../.vuepress/dist/assets/js/61.74ebd7d9.js | 1 + .../.vuepress/dist/assets/js/62.4bf212f9.js | 1 + .../.vuepress/dist/assets/js/63.b54aca3e.js | 1 + .../.vuepress/dist/assets/js/64.399e72b8.js | 1 + .../.vuepress/dist/assets/js/65.a8c627d3.js | 1 + .../.vuepress/dist/assets/js/66.7f19cc4b.js | 1 + .../.vuepress/dist/assets/js/67.e57db97e.js | 1 + .../.vuepress/dist/assets/js/68.ebe4213c.js | 1 + .../.vuepress/dist/assets/js/69.2d309866.js | 1 + .../.vuepress/dist/assets/js/7.01e4e942.js | 1 + .../.vuepress/dist/assets/js/70.60ae83e6.js | 1 + .../.vuepress/dist/assets/js/71.188d7bca.js | 1 + .../.vuepress/dist/assets/js/72.3f7bfd0d.js | 1 + .../.vuepress/dist/assets/js/73.171571e4.js | 1 + .../.vuepress/dist/assets/js/74.3d5cfaba.js | 1 + .../.vuepress/dist/assets/js/75.093d716c.js | 1 + .../.vuepress/dist/assets/js/76.50d84c61.js | 1 + .../.vuepress/dist/assets/js/77.7bc0f013.js | 1 + .../.vuepress/dist/assets/js/78.e2ded2d4.js | 1 + .../.vuepress/dist/assets/js/79.f7eeb76b.js | 1 + .../.vuepress/dist/assets/js/8.ba7f6ace.js | 1 + .../.vuepress/dist/assets/js/80.41418749.js | 1 + .../.vuepress/dist/assets/js/81.16e2cc83.js | 1 + .../.vuepress/dist/assets/js/82.56c0d7c1.js | 1 + .../.vuepress/dist/assets/js/83.ea65d6b1.js | 1 + .../.vuepress/dist/assets/js/84.d50d3d09.js | 1 + .../.vuepress/dist/assets/js/85.1eed1486.js | 1 + .../.vuepress/dist/assets/js/86.1ab04b8d.js | 1 + .../.vuepress/dist/assets/js/87.392384e7.js | 1 + .../.vuepress/dist/assets/js/88.a95133cd.js | 1 + .../.vuepress/dist/assets/js/89.213efaae.js | 1 + .../.vuepress/dist/assets/js/9.26319460.js | 1 + .../.vuepress/dist/assets/js/90.ef03688f.js | 1 + .../.vuepress/dist/assets/js/91.43930d52.js | 1 + .../.vuepress/dist/assets/js/92.ae5e9611.js | 1 + .../.vuepress/dist/assets/js/93.3cdab647.js | 1 + .../.vuepress/dist/assets/js/94.72df7b53.js | 1 + .../.vuepress/dist/assets/js/95.c8e36604.js | 1 + .../.vuepress/dist/assets/js/96.1ebdc00d.js | 1 + .../.vuepress/dist/assets/js/97.fa1ed9e7.js | 1 + .../.vuepress/dist/assets/js/98.939cb6ef.js | 1 + .../.vuepress/dist/assets/js/99.96c5a70e.js | 1 + .../.vuepress/dist/assets/js/app.84c88b57.js | 18 + .../dist/baidu_verify_codeva-GYcT5ujCTB.html | 1 + docs-vuepress/.vuepress/dist/desc.html | 49 + docs-vuepress/.vuepress/dist/favicon.ico | Bin 0 -> 9662 bytes .../guide/advance/ability-compatible.html | 87 ++ .../dist/guide/advance/async-subpackage.html | 110 ++ .../guide/advance/custom-output-path.html | 101 ++ .../dist/guide/advance/dll-plugin.html | 126 ++ .../.vuepress/dist/guide/advance/i18n.html | 276 ++++ .../dist/guide/advance/image-process.html | 175 +++ .../.vuepress/dist/guide/advance/mixin.html | 115 ++ .../.vuepress/dist/guide/advance/npm.html | 144 ++ .../.vuepress/dist/guide/advance/pinia.html | 191 +++ .../dist/guide/advance/platform.html | 300 ++++ .../.vuepress/dist/guide/advance/plugin.html | 77 ++ .../dist/guide/advance/progressive.html | 135 ++ .../dist/guide/advance/provide-inject.html | 200 +++ .../dist/guide/advance/resource-resolve.html | 88 ++ .../dist/guide/advance/size-report.html | 188 +++ .../.vuepress/dist/guide/advance/ssr.html | 190 +++ .../.vuepress/dist/guide/advance/store.html | 599 ++++++++ .../dist/guide/advance/subpackage.html | 264 ++++ .../dist/guide/advance/utility-first-css.html | 139 ++ .../dist/guide/basic/class-style-binding.html | 131 ++ .../.vuepress/dist/guide/basic/component.html | 117 ++ .../dist/guide/basic/conditional-render.html | 72 + .../.vuepress/dist/guide/basic/css.html | 173 +++ .../.vuepress/dist/guide/basic/event.html | 90 ++ .../.vuepress/dist/guide/basic/ide.html | 67 + .../.vuepress/dist/guide/basic/intro.html | 51 + .../dist/guide/basic/list-render.html | 144 ++ .../dist/guide/basic/option-chain.html | 76 ++ .../.vuepress/dist/guide/basic/reactive.html | 111 ++ .../.vuepress/dist/guide/basic/refs.html | 106 ++ .../dist/guide/basic/single-file.html | 77 ++ .../.vuepress/dist/guide/basic/start.html | 132 ++ .../.vuepress/dist/guide/basic/template.html | 137 ++ .../dist/guide/basic/two-way-binding.html | 81 ++ .../composition-api/composition-api.html | 527 +++++++ .../guide/composition-api/reactive-api.html | 386 ++++++ .../dist/guide/extend/api-proxy.html | 100 ++ .../.vuepress/dist/guide/extend/fetch.html | 139 ++ .../.vuepress/dist/guide/extend/index.html | 62 + .../.vuepress/dist/guide/extend/mock.html | 177 +++ .../.vuepress/dist/guide/migrate/2.7.html | 172 +++ .../.vuepress/dist/guide/migrate/2.8.html | 85 ++ .../.vuepress/dist/guide/migrate/2.9.html | 76 ++ .../dist/guide/migrate/mpx-cli-3.html | 97 ++ .../.vuepress/dist/guide/platform/index.html | 225 +++ .../dist/guide/platform/miniprogram.html | 43 + .../.vuepress/dist/guide/platform/rn.html | 198 +++ .../.vuepress/dist/guide/platform/web.html | 43 + .../.vuepress/dist/guide/tool/e2e-test.html | 297 ++++ .../.vuepress/dist/guide/tool/ts.html | 164 +++ .../.vuepress/dist/guide/tool/unit-test.html | 149 ++ .../dist/guide/understand/compile.html | 52 + .../dist/guide/understand/runtime.html | 51 + docs-vuepress/.vuepress/dist/index.html | 55 + docs-vuepress/.vuepress/dist/logo.png | Bin 0 -> 13635 bytes .../.vuepress/dist/manifest.webmanifest | 15 + docs-vuepress/.vuepress/dist/rn-api.html | 65 + .../.vuepress/dist/rn-component.html | 44 + docs-vuepress/.vuepress/dist/rn-style.html | 331 +++++ docs-vuepress/.vuepress/dist/rn-template.html | 104 ++ .../.vuepress/dist/service-worker.js | 901 ++++++++++++ .../platform/patch/getDefaultOptions.ios.js | 20 +- .../lib/template-compiler/compiler.js | 35 +- 220 files changed, 16558 insertions(+), 20 deletions(-) create mode 100644 docs-vuepress/.vuepress/dist/404.html create mode 100644 docs-vuepress/.vuepress/dist/api/ApiIndex.html create mode 100644 docs-vuepress/.vuepress/dist/api/app-config.html create mode 100644 docs-vuepress/.vuepress/dist/api/builtIn.html create mode 100644 docs-vuepress/.vuepress/dist/api/compile.html create mode 100644 docs-vuepress/.vuepress/dist/api/composition-api.html create mode 100644 docs-vuepress/.vuepress/dist/api/directives.html create mode 100644 docs-vuepress/.vuepress/dist/api/extend.html create mode 100644 docs-vuepress/.vuepress/dist/api/global-api.html create mode 100644 docs-vuepress/.vuepress/dist/api/index.html create mode 100644 docs-vuepress/.vuepress/dist/api/instance-api.html create mode 100644 docs-vuepress/.vuepress/dist/api/optional-api.html create mode 100644 docs-vuepress/.vuepress/dist/api/reactivity-api.html create mode 100644 docs-vuepress/.vuepress/dist/api/store-api.html create mode 100644 docs-vuepress/.vuepress/dist/articles/1.0.html create mode 100644 docs-vuepress/.vuepress/dist/articles/2.0.html create mode 100644 docs-vuepress/.vuepress/dist/articles/2.7-release.html create mode 100644 docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html create mode 100644 docs-vuepress/.vuepress/dist/articles/2.8-release.html create mode 100644 docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html create mode 100644 docs-vuepress/.vuepress/dist/articles/2.9-release.html create mode 100644 docs-vuepress/.vuepress/dist/articles/index.html create mode 100644 docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html create mode 100644 docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html create mode 100644 docs-vuepress/.vuepress/dist/articles/mpx1.html create mode 100644 docs-vuepress/.vuepress/dist/articles/mpx2.html create mode 100644 docs-vuepress/.vuepress/dist/articles/performance.html create mode 100644 docs-vuepress/.vuepress/dist/articles/size-control.html create mode 100644 docs-vuepress/.vuepress/dist/articles/ts-derivation.html create mode 100644 docs-vuepress/.vuepress/dist/articles/unit-test.html create mode 100644 docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css create mode 100644 docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg create mode 100644 docs-vuepress/.vuepress/dist/assets/img/start-tips1.3b76ac97.png create mode 100644 docs-vuepress/.vuepress/dist/assets/img/start-tips2.7d8836f8.png create mode 100644 docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/10.0192acce.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/104.78f6358e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/105.eb6680fc.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/106.471f03cf.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/107.63fd18fd.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/108.614260e9.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/109.3da50cc1.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/11.d7a8d045.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/110.f74ff3a3.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/111.51932e54.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/112.da7bd3e7.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/113.caaf11a3.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/114.16cda7ac.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/115.88124b12.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/116.4a116234.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/117.e45c1b95.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/118.9cbac09e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/119.312e4c21.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/12.4d7217be.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/120.7a6b4035.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/14.7377c68f.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/15.6f885a83.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/16.974f551e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/17.e94e6745.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/18.b1f7a4a2.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/19.62186e92.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/2.314c65b9.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/20.e9cd4de3.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/21.217f8fa9.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/22.4cd3344f.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/23.365f9a87.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/24.17777a13.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/25.889c07d7.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/26.3aa9f276.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/27.1a35170a.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/28.3670fa46.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/29.a911e510.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/3.145e69cd.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/30.04d1a7ea.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/31.ec06bf2f.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/32.f29d2cd1.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/33.936d018e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/34.f494d685.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/35.c3ed1ff7.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/36.d2c9afa9.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/37.4e1eee2c.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/38.7826170e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/39.094ffa40.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/4.9489102c.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/40.10d90636.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/41.8351af86.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/42.53972f6c.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/43.e2b432ea.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/44.e4242a1f.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/45.392413aa.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/46.22f9a9c6.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/47.2bd2cecc.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/48.55792a30.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/49.cde94a79.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/5.12d049b6.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/50.42e34e52.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/51.90dfa84e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/52.10bb0251.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/53.7207ffc5.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/54.af3d3dcd.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/55.2d6ae2e1.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/56.9aec9a9e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/57.9d9ddcbd.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/58.50eb3b91.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/59.27106fbb.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/6.6ea91b28.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/60.a47e5279.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/61.74ebd7d9.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/62.4bf212f9.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/63.b54aca3e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/64.399e72b8.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/65.a8c627d3.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/66.7f19cc4b.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/67.e57db97e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/68.ebe4213c.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/69.2d309866.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/7.01e4e942.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/70.60ae83e6.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/71.188d7bca.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/72.3f7bfd0d.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/73.171571e4.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/74.3d5cfaba.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/75.093d716c.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/76.50d84c61.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/77.7bc0f013.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/78.e2ded2d4.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/79.f7eeb76b.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/8.ba7f6ace.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/80.41418749.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/81.16e2cc83.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/82.56c0d7c1.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/83.ea65d6b1.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/84.d50d3d09.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/85.1eed1486.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/86.1ab04b8d.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/87.392384e7.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/88.a95133cd.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/89.213efaae.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/9.26319460.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/90.ef03688f.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/91.43930d52.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/92.ae5e9611.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/93.3cdab647.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/94.72df7b53.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/95.c8e36604.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/96.1ebdc00d.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/97.fa1ed9e7.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/98.939cb6ef.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/99.96c5a70e.js create mode 100644 docs-vuepress/.vuepress/dist/assets/js/app.84c88b57.js create mode 100644 docs-vuepress/.vuepress/dist/baidu_verify_codeva-GYcT5ujCTB.html create mode 100644 docs-vuepress/.vuepress/dist/desc.html create mode 100644 docs-vuepress/.vuepress/dist/favicon.ico create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/i18n.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/image-process.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/mixin.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/npm.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/pinia.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/platform.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/plugin.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/progressive.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/size-report.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/ssr.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/store.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/subpackage.html create mode 100644 docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/component.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/css.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/event.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/ide.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/intro.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/list-render.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/option-chain.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/reactive.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/refs.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/single-file.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/start.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/template.html create mode 100644 docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html create mode 100644 docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html create mode 100644 docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html create mode 100644 docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html create mode 100644 docs-vuepress/.vuepress/dist/guide/extend/fetch.html create mode 100644 docs-vuepress/.vuepress/dist/guide/extend/index.html create mode 100644 docs-vuepress/.vuepress/dist/guide/extend/mock.html create mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/2.7.html create mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/2.8.html create mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/2.9.html create mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html create mode 100644 docs-vuepress/.vuepress/dist/guide/platform/index.html create mode 100644 docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html create mode 100644 docs-vuepress/.vuepress/dist/guide/platform/rn.html create mode 100644 docs-vuepress/.vuepress/dist/guide/platform/web.html create mode 100644 docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html create mode 100644 docs-vuepress/.vuepress/dist/guide/tool/ts.html create mode 100644 docs-vuepress/.vuepress/dist/guide/tool/unit-test.html create mode 100644 docs-vuepress/.vuepress/dist/guide/understand/compile.html create mode 100644 docs-vuepress/.vuepress/dist/guide/understand/runtime.html create mode 100644 docs-vuepress/.vuepress/dist/index.html create mode 100644 docs-vuepress/.vuepress/dist/logo.png create mode 100644 docs-vuepress/.vuepress/dist/manifest.webmanifest create mode 100644 docs-vuepress/.vuepress/dist/rn-api.html create mode 100644 docs-vuepress/.vuepress/dist/rn-component.html create mode 100644 docs-vuepress/.vuepress/dist/rn-style.html create mode 100644 docs-vuepress/.vuepress/dist/rn-template.html create mode 100644 docs-vuepress/.vuepress/dist/service-worker.js diff --git a/docs-vuepress/.vuepress/dist/404.html b/docs-vuepress/.vuepress/dist/404.html new file mode 100644 index 0000000000..a33a589c95 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/404.html @@ -0,0 +1,25 @@ + + + + + + Mpx框架 + + + + + + + + + +

404

That's a Four-Oh-Four.
+ Take me home. +
+ + + diff --git a/docs-vuepress/.vuepress/dist/api/ApiIndex.html b/docs-vuepress/.vuepress/dist/api/ApiIndex.html new file mode 100644 index 0000000000..4f5bdb4a1f --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/ApiIndex.html @@ -0,0 +1,23 @@ + + + + + + Mpx框架 + + + + + + + + + +

API 参考

+ + + diff --git a/docs-vuepress/.vuepress/dist/api/app-config.html b/docs-vuepress/.vuepress/dist/api/app-config.html new file mode 100644 index 0000000000..15e02d6edd --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/app-config.html @@ -0,0 +1,116 @@ + + + + + + 全局配置 | Mpx框架 + + + + + + + + + +

# 全局配置

Mpx.config 是一个对象,包含 Mpx 的全局配置。可以在启动应用之前修改下列 property:

# useStrictDiff

boolean = false

每次有数据变更时,是否使用严格的 diff 算法。如果项目中有大数据集的渲染建议使用,可以提升效率。

import mpx from '@mpxjs/core'
+mpx.config.useStrictDiff = true
+

注意:由于微信小程序的bug,同时使用useStrictDiff和增强指令wx:style时,要注意更改数据的方式。如下所示:

// 入口文件
+import mpx, { createApp } from '@mpxjs/core'
+mpx.config.useStrictDiff = true
+
+// 页面page文件
+<template>
+  <view>
+    <view wx:style="{{style}}">test</view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+  createPage({
+    data: {
+      style: {
+        color: 'red',
+        fontSize: '18px'
+      }
+    },
+    onLoad () {
+      setTimeout(() => {
+        this.setData({ // 当useStrictDiff设置true时,需要用setData的方式设置整个style对象
+          style: {
+            color: 'blue',
+            fontSize: '18px'
+          }
+        })
+        // this.style.color = 'blue' // 当useStrictDiff设置true时,不能使用这种方式,style不会生效
+      }, 1000)
+    }
+  })
+</script>
+

# ignoreWarning

boolean = false

是否忽略运行时的 warning 信息,默认不忽略。

import mpx from '@mpxjs/core'
+mpx.config.ignoreWarning = true
+

# ignoreProxyWhiteList

Array<string> = ['id']

Mpx 实例上的 key(包括data、computed、methods)如果有重名冲突,在ignoreProxyWhiteList配置中的属性会被最新的覆盖;而不在ignoreProxyWhiteList配置中的属性,不会被覆盖。

只要有重名冲突均会有报错提示。

import mpx from '@mpxjs/core'
+mpx.config.ignoreConflictWhiteList = ['id', 'test']
+

# observeClassInstance

boolean = false

当需要对 class 对象的数据进行响应性转化,需要开启该选项。

# proxyEventHandler

Function

需要代理的事件的钩子方法,该钩子方法仅对内联传参事件或 forceProxyEventRules 规则匹配的事件生效。

import mpx from '@mpxjs/core'
+
+mpx.config.proxyEventHandler = function (event) {
+    // 入参为 event 事件对象
+}
+

# setDataHandler

function setDataHandler(data: object, target: ComponentIns<{}, {}, {}, {}, []>): any
+

页面/组件状态更新时,使用该方法可以对 setData 调用进行监听,可以用来统计 setData 调用次数和数据量的统计,方法的入参是 setData 传输的 data 和当前组件实例。

import mpx from '@mpxjs/core'
+
+mpx.config.setDataHandler = function(data, comp) {
+    console.log('setData trigger', data, comp)
+}
+

# forceFlushSync

boolean = false

Mpx 中更改响应性状态时,最终页面的更新并不是同步立即生效的,而是由 Mpx 将它们缓存在一个队列中, 异步等到下一个 tick 一起执行,如果想将所有队列的执行改为同步执行,我们可以通过该配置来实现。

import mpx from '@mpxjs/core'
+
+mpx.config.forceFlushSync = true
+

# errorHandler

Function

mpx.config.errorHandler = function (errmsg, location, error) {
+  // errmsg: 框架内部运行报错的报错归类信息,例如当执行一个watch方法报错时,会是 "Unhandled error occurs during execution of watch callback!"
+  // location: 具体报错的代码路径,可选项,不一定存在
+  // error: 具体的错误堆栈,可选项,不一定存在
+  // handle error
+}
+

Mpx 框架运行时报错捕获感知处理函数。

  • Mpx 框架生命周期执行错误;
  • Mpx 中的 computed、watch 等内置方法执行报错;
  • Mpx 框架的运行时的检测报错,例如存在目标平台不支持的属性,入参出参类型错误等;

同时被捕获的错误会通过 console.error 输出。

# webRouteConfig

Mpx 通过 config 暴露出 webRouteConfig 配置项,在 web 环境可以对路由进行配置。 +此配置后续将被废弃,请使用 webConfig 进行配置

  • 用法:
mpx.config.webRouteConfig = {
+  mode: 'history'
+}
+

# webConfig

web 环境下的一些配置,如路由模式,页面切换动画效果等

  • 用法:
// 修改路由模式
+mpx.config.webConfig.routeConfig = {
+  mode: 'history'
+}
+// 禁用页面切换动画
+mpx.config.webConfig.disablePageTransition = true
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/api/builtIn.html b/docs-vuepress/.vuepress/dist/api/builtIn.html new file mode 100644 index 0000000000..440c70c028 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/builtIn.html @@ -0,0 +1,56 @@ + + + + + + 内建组件 | Mpx框架 + + + + + + + + + +

# 内建组件

# component

  • is string : 动态渲染组件,通过改变is的值,来渲染不同的组件

  • range string : 使用 range 来指定可能渲染的组件,不传递时则为 全局注册 + usingComponents 中注册的所有组件,存在多个组件时使用逗号 , 分隔

  <!-- ComponentName 是在全局或者组件中完成注册的组件名称 -->
+  <component is="{{ComponentName}}"></component>
+
+  <!-- 只会展示 compA 或者 compB 组件 -->
+  <component is="{{ComponentName}}" range="compA,compB"></component>
+

参考动态组件

# slot

  • name string : 用于命名插槽

<slot> 元素作为组件模板中的内容分发插槽,<slot>元素自身将被替换。

详细用法,可参考下面的链接。

参考slot

+ + + diff --git a/docs-vuepress/.vuepress/dist/api/compile.html b/docs-vuepress/.vuepress/dist/api/compile.html new file mode 100644 index 0000000000..58c276a54b --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/compile.html @@ -0,0 +1,1214 @@ + + + + + + 编译构建 | Mpx框架 + + + + + + + + + +

# 编译构建

对于使用 @mpxjs/cli@3.x 脚手架初始化的项目而言,编译构建相关的配置统一收敛至项目根目录下的 vue.config.js 进行配置。一个新项目初始化的 vue.config.js 如下图,相较于 @mpxjs/cli@2.x 版本,在新的初始化项目当中原有的编译构建配置都收敛至 cli 插件当中进行管理和维护,同时还对外暴露相关的接口或者 api 使得开发者能自定义修改 cli 插件当中默认的配置。

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        // mpx webpack plugin options
+      },
+      unocss: {
+        // @mpxjs/unocss-plugin 相关的配置
+      }
+    }
+  }
+})
+

对于使用 @mpxjs/cli@2.x 脚手架初始化的项目,编译构建配置涉及到 mpx 插件相关的配置主要是在 config 目录下 mpxPlugin.conf.js,涉及到 webpack 本身的配置主要是在 build 目录下。

// config/mpxPlugin.conf.js
+module.exports = () => {
+  return {
+    // mpx webpack plugin options
+  }
+}
+

# 类型定义

为了便于对编译配置的数据类型进行准确地描述,我们在这里对一些常用的配置类型进行定义

# Rules

type Condition = string | ((resourcePath: string) => boolean) | RegExp
+
+interface Rules {
+  include?: Condition | Array<Condition>
+  exclude?: Condition | Array<Condition>
+}
+

# MpxWebpackPlugin

Mpx 编译构建跨平台小程序和 web 的 webpack 主插件,安装示例如下:

npm install -D @mpxjs/webpack-plugin
+pnpm install -D @mpxjs/webpack-plugin
+yarn add -D @mpxjs/webpack-plugin
+

使用示例如下:

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        // mpx webpack plugin options
+      }
+    }
+  }
+})
+

MpxWebpackPlugin支持传入以下配置:

# mode

'wx' | 'ali' | 'swan' | 'qq' | 'tt' | 'jd' | 'dd' | 'qa' | 'web' = 'wx'

mode 为 Mpx 编译的目标平台, 目前支持的有微信小程序(wx)\支付宝小程序(ali)\百度小程序(swan)\头条小程序(tt)\QQ 小程序(qq)\京东小程序(jd)\滴滴小程序(dd)\快应用(qa)\H5 页面(web)。

TIP

在 @mpxjs/cli@3.x 版本当中,通过在 npm script 当中定义 targets 来设置目标平台

// 项目 package.json
+{
+  "script": {
+    "build:cross": "mpx-cli-service build --targets=wx,ali"
+  }
+}
+

# srcMode

'wx' | 'ali' | 'swan' | 'qq' | 'tt' | 'jd' | 'dd' | 'qa' = 'wx'

默认和 mode 一致。,当 srcMode 和 mode 不一致时,会读取相应的配置对项目进行编译和运行时的转换。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        srcMode: 'wx' // 根据项目初始化所选平台来设定
+      }
+    }
+  }
+})
+

WARNING

暂时只支持微信为源 mode 做跨平台,为其他时,mode 必须和 srcMode 保持一致。

# modeRules

{ [key: string]: Rules }

批量指定文件mode,用于条件编译场景下使用某些单小程序平台的库时批量标记这些文件的mode为对应平台,而不再走转换规则。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        modeRules: {
+          ali: {
+            include: [resolve('node_modules/vant-aliapp')]
+          }
+        }
+      }
+    }
+  }
+})
+

# externalClasses

Array<string>

定义若干个外部样式类,这些将会覆盖元素原有的样式。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        externalClasses: ['custom-class', 'i-class']
+      }
+    }
+  }
+})
+

WARNING

抹平支付宝和微信之间的差异,当使用了微信 externalClasses 语法时,跨端输出需要在 @mpxjs/webpack-plugin 的配置中添加此配置来辅助框架进行转换。

# resolveMode

'webpack' | 'native' = 'webpack'

指定resolveMode,默认webpack,更便于引入npm包中的页面/组件等资源。若想编写时和原生保持一致或兼容已有原生项目,可设为native,此时需要提供projectRoot以指定项目根目录,且使用npm资源时需在前面加~

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        resolveMode: 'webpack'
+      }
+    }
+  }
+})
+

# projectRoot

string

当resolveMode为native时需通过该字段指定项目根目录。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+         resolveMode: 'native',
+         projectRoot: path.resolve(__dirname, '../src')
+      }
+    }
+  }
+})
+

# writeMode

'full' | 'change' = 'change'

webpack 的输出默认是全量输出,而小程序开发者工具不关心文件是否真正发生了变化。设置为 change 时,Mpx 在 watch 模式下将内部 diff 一次,只会对内容发生变化的文件进行写入,以提升小程序开发者工具编译性能。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+         writeMode: 'change'
+      }
+    }
+  }
+})
+

# autoScopeRules

Rules

是否需要对样式加 scope ,目前只有支付宝小程序平台没有样式隔离,因此该部分内容也只对支付宝小程序平台生效。提供 include 和 exclude 以精确控制对哪些文件进行样式隔离,哪些不隔离,和webpack的rules规则相同。也可以通过在 style 代码块上声明 scoped 进行。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+         autoScopeRules: {
+           include: [resolve('../src')],
+           exclude: [resolve('../node_modules/vant-aliapp')] // 比如一些组件库本来就是为支付宝小程序编写的,应该已经考虑过样式隔离,就不需要再添加
+         }
+      }
+    }
+  }
+})
+

# forceDisableProxyCtor

boolean = false

用于控制在跨平台输出时对实例构造函数(App | Page | Component | Behavior)进行代理替换以抹平平台差异。当配置 forceDisableProxyCtor 为 true 时,会强行取消平台差异抹平逻辑,开发时需针对输出到不同平台进行条件判断。

# transMpxRules

Rules

是否转换 wx / my 等全局对象为 Mpx 对象,

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        transMpxRules: {
+          include: () => true,
+          exclude: ['@mpxjs']
+        }
+      }
+    }
+  }
+})
+

# forceProxyEventRules

Rules

强制代理规则内配置的事件。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        forceProxyEventRules: {
+          include: ['bindtap']
+        }
+      }
+    }
+  }
+})
+

# defs

object

给模板、js、json、style中定义一些全局常量。一般用于区分平台/环境。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        defs: {
+          __env__: 'mini'
+        }
+      }
+    }
+  }
+})
+

在模板中使用:

<template>
+  <view>{{__env__}}</view>
+</template>
+

在js中使用:

const env = __env__;
+

在style中使用:

/* @mpx-if (__env__ === 'mini') */
+.color {
+  background: red;
+}
+/* @mpx-endif */
+

在json中使用:

<script name='json'>
+  module.exports = {
+    "component": true,
+    "usingComponents": {
+      "a": __env__
+    }
+  }
+</script>
+

注意:这里定义之后使用的时候是按照全局变量来使用,而非按照process.env.KEY这样的形式

# attributes

Array<string> = ['image:src', 'audio:src', 'video:src', 'cover-image:src', 'import:src', 'include:src']

Mpx 提供了可以给自定义标签设置资源的功能,配置该属性后,即可在目标标签中使用 :src 加载相应资源文件

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        attributes: ['customTag:src']
+      }
+    }
+  }
+})
+
<customTag :src="'https://www....../avator.png'"></customTag>
+

TIP

该属性可通过 MpxWebpackPlugin 配置,也可以通过配置 WxmlLoader,后者优先级高。

# externals

Array<string>

目前仅支持微信小程序 weui 组件库通过 useExtendedLib 扩展库的方式引入,这种方式引入的组件将不会计入代码包大小。配置 externals 选项,Mpx 将不会解析 weui 组件的路径并打包。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        externals: ['weui']
+      }
+    }
+  }
+})
+
<script name="json">
+  // app.mpx json部分
+  module.exports = {
+    "useExtendedLib": {
+      "weui": true
+    }
+  }
+</script>
+
<!-- 在 page 中使用 weui 组件 -->
+<template>
+  <view wx:if="{{__mpx_mode__ === 'wx'}}">
+    <mp-icon icon="play" color="black" size="{{25}}" bindtap="showDialog"></mp-icon>
+    <mp-dialog title="test" show="{{dialogShow}}" bindbuttontap="tapDialogButton" buttons="{{buttons}}">
+      <view>test content</view>
+    </mp-dialog>
+  </view>
+</template>
+
+<script>
+  import{ createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      dialogShow: false,
+      showOneButtonDialog: false,
+      buttons: [{text: '取消'}, {text: '确定'}],
+    },
+    methods: {
+      tapDialogButton () {
+        this.dialogShow = false
+        this.showOneButtonDialog = false
+      },
+      showDialog () {
+        this.dialogShow = true
+      }
+    }
+  })
+</script>
+
+<script name="json">
+  const wxComponents = {
+    "mp-icon": "weui-miniprogram/icon/icon",
+    "mp-dialog": "weui-miniprogram/dialog/dialog"
+  }
+  module.exports = {
+    "usingComponents": __mpx_mode__ === 'wx'
+      ? Object.assign({}, wxComponents)
+      : {}
+  }
+</script>
+

参考weui组件库

# miniNpmPackage

Array<string>

微信小程序官方提供了发布小程序 npm 包的约束 (opens new window)。 +部分小程序npm包,如vant组件库 (opens new window)官方文档使用说明,引用资源并不会包含miniprogram所指定的目录 +如 "@vant/weapp/button/index",导致 Mpx 解析路径失败。 +Mpx为解决这个问题,提供miniNpmPackage字段供用户配置需要解析的小程序npm包。miniNpmPackage对应的数组值为npm包对应的package.json中的name字段。 +Mpx解析规则如下:

  1. 如package.json中有miniprogram字段,则会默认拼接miniprogram对应的值到资源路径中
  2. 如package.json中无miniprogram字段,但配置了miniNpmPackage,则默认会拼接miniprogram_dist目录
// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        miniNpmPackage: ['@vant/weapp']
+      }
+    }
+  }
+})
+

# forceUsePageCtor

Boolean = false

为了获取更丰富的生命周期来进行更加完善的增强处理,在非支付宝小程序环境下,Mpx 默认会使用 Conponent 构造器来创建页面。将该值设置为 true 时,会强制使用 Page 构造器创建页面。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        forceUsePageCtor: true
+      }
+    }
+  }
+})
+

# transRpxRules

Array<object> | object

  • option.mode 可选值有 none/only/all,分别是不启用/只对注释内容启用/只对非注释内容启用
  • option.designWidth 设计稿宽度,默认值就是750,可根据需要修改
  • option.include 同webpack的include规则
  • option.exclude 同webpack的exclude规则
  • option.comment rpx注释,建议使用 'use px'/'use rpx',当 mode 为 all 时默认值为 use px,mode 为 only 时默认值为 'use rpx'

为了处理某些IDE中不支持rpx单位的问题,Mpx 提供了一个将 px 转换为 rpx 的功能。支持通过注释控制行级、块级的是否转换,支持局部使用,支持不同依赖分别使用不用的转换规则等灵活的能力。transRpxRules可以是一个对象,也可以是多个这种对象组成的数组。

// vue.config.js
+const path = require('path')
+
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        transRpxRules: [
+          {
+            mode: 'only', // 只对注释为'use rpx'的块儿启用转换rpx
+            comment: 'use rpx', // mode为'only'时,默认值为'use rpx'
+            include: path.resolve('src'),
+            exclude: path.resolve('lib'),
+            designWidth: 750
+          },
+          {
+            mode: 'all', // 所有样式都启用转换rpx,除了注释为'use px'的样式不转换
+            comment: 'use px', // mode为'all'时,默认值为'use px'
+            include: path.resolve('node_modules/@didi/mpx-sec-guard')
+          }
+        ]
+      }
+    }
+  }
+})
+

# 应用场景及相应配置

接下来我们来看下一些应用场景及如何配置。如果是用脚手架生成的项目,在mpx.plugin.conf.js里找到transRpxRules,应该已经有预设的transRpxRules选项,按例修改即可。

三种场景分别是 普通使用只对某些特殊样式转换不同路径分别配置规则

# 场景一

设计师给的稿是2倍图,分辨率750px。或者更高倍图。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        transRpxRules: [{
+          mode: 'all',
+          designWidth: 750 // 如果是其他倍,修改此值为设计稿的宽度即可
+        }]
+      }
+    }
+  }
+})
+

# 场景二

大部分样式都用px下,某些元素期望用rpx。或者反过来。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        transRpxRules: [{
+          mode: 'only',
+          comment: 'use rpx',
+          designWidth: 750 // 设计稿宽度
+        }]
+      }
+    }
+  }
+})
+

mpx的rpx注释能帮助你仅为部分类或者部分样式启用rpx转换,细节请看下方附录。

# 场景三

使用了第三方组件,它的设计宽度和主项目不一致,期望能设置不同的转换规则

// vue.config.js
+const path = require('path')
+
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        transRpxRules: [
+          {
+            mode: 'only',
+            designWidth: 750,
+            comment: 'use rpx',
+            include: resolve('src')
+          },
+          {
+            mode: 'all',
+            designWidth: 1280, // 对iview单独使用一个不同的designWidth
+            include: path.resolve('node_modules/iview-weapp')
+          }
+        ]
+      }
+    }
+  }
+})
+

注意事项:转换规则是不可以对一个文件做多次转换的,会出错,所以一旦被一个规则命中后就不会再次命中另一个规则,include 和 exclude 的编写需要注意先后顺序,就比如上面这个配置,如果第一个规则 include 的是 '/' 即整个项目,iview-weapp 里的样式就无法命中第二条规则了。

# transRpxRules附录

  • designWidth

设计稿宽度,单位为px。默认值为750px

mpx会基于小程序标准的屏幕宽度baseWidth 750rpx,与option.designWidth计算出一个转换比例transRatio

转换比例的计算方式为transRatio = (baseWidth / designWidth),精度为小数点后2位四舍五入。

所有生效的rpx注释样式中的px会乘上transRatio得出最终的 rpx 值。

例如:

/* 转换前:designWidth = 1280 */
+.btn {
+  width: 200px;
+  height: 100px;
+}
+
+/* 转换后: transRatio = 0.59 */
+.btn {
+  width: 118rpx;
+  height: 59rpx;
+}
+
  • comment: rpx 注释样式

根据rpx注释的位置,mpx会将一段css规则或者一条css声明视为rpx注释样式

开发者可以声明一段 rpx 注释样式,提示编译器是否转换这段 css 中的 px。

例如:

<style lang="css">
+  /* use rpx */
+  .not-translate-a {
+    font-size: 100px;
+    padding: 10px;
+  }
+  .not-translate-b {
+    /* use rpx */
+    font-size: 100px;
+    padding: 10px;
+  }
+  .translate-a {
+    font-size: 100px;
+    padding: 10px;
+  }
+  .translate-b {
+    font-size: 100px;
+    padding: 10px;
+  }
+</style>
+

第一个注释位于一个选择器前,是一个css规则注释,整个规则都会被视为rpx注释样式

第二个注释位于一个css声明前,是一个css声明注释,只有font-size: 100px会被视为rpx注释样式

transRpx = only模式下,只有两部分rpx注释样式会转rpx。

# postcssInlineConfig

{options? : PostcssOptions, plugins? : PostcssPlugin[], mpxPrePlugins? : PostcssPlugin[], ignoreConfigFile : Boolean}

使用类似于 postcss.config.js 的语法书写 postcss 的配置文件。用于定义 Mpx 对于组件/页面样式进行 postcss 处理时的配置, ignoreConfigFile 传递为 true 时会忽略项目中的 postcss 配置文件 。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        postcssInlineConfig: {
+          plugins: [
+            // require('postcss-import'),
+            // require('postcss-preset-env'),
+            // require('cssnano'),
+            // require('autoprefixer')
+          ]
+        }
+      }
+    }
+  }
+})
+

注意:默认添加的 postcss 插件均会在mpx的内置插件(例如如rpx插件等)之后处理。如需使配置的插件优先于内置插件,可以在 postcssInlineConfig 中添加 mpxPrePlugins 配置:

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        postcssInlineConfig: {
+          plugins: [
+            require('postcss-import'),
+            require('postcss-preset-env'),
+          ],
+          mpxPrePlugins: [
+            require('cssnano'),
+            require('autoprefixer')
+          ]
+          // 以下写法同理
+          // mpxPrePlugins: {
+          //   'cssnano': {},
+          //   'autoprefixer': {}
+          // }
+        }
+      }
+    }
+  }
+})
+

postcss.config.js 中配置同理:

// postcss.config.js
+module.exports = {
+  plugins: [
+    require('postcss-import'),
+    require('postcss-preset-env'),
+  ],
+  mpxPrePlugins: [
+    require('cssnano'),
+    require('autoprefixer')
+  ]
+}
+
+

在上面这个例子当中,postcss 插件处理的最终顺序为:cssnano -> autoprefixer -> mpx内置插件 -> postcss-import -> postcss-preset-env

WARNING

注意:在 mpxPrePlugins 中配置的 postcss 插件如果不通过 mpx 进行处理,那么将不会生效。

# decodeHTMLText

boolean = false

设置为 true 时在模板编译时对模板中的 text 内容进行 decode

# nativeConfig

{cssLangs: string[]}

为原生多文件写法添加css预处理语言支持,用于优先搜索预编译器后缀的文件,按 cssLangs 中的声明顺序查找。默认按照 css , less , stylus , scss , sass 的顺序

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        nativeConfig: {
+          cssLangs: ['css', 'less', 'stylus', 'scss', 'sass']
+        }
+      }
+    }
+  }
+})
+

# webConfig

{transRpxFn(match:string, $1:number): string}

transRpxFn 配置用于自定义输出 web 时对于 rpx 样式单位的转换逻辑,常见的方式有转换为 vw 或转换为 rem

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        webConfig: {
+          transRpxFn: function (match, $1) {
+            if ($1 === '0') return $1
+            return `${$1 * +(100 / 750).toFixed(8)}vw`
+          }
+        }
+      }
+    }
+  }
+})
+

# i18n

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        i18n: {
+          locale: 'en-US',
+          messages: {
+            'en-US': {
+              message: {
+                hello: '{msg} world'
+              }
+            },
+            'zh-CN': {
+              message: {
+                hello: '{msg} 世界'
+              }
+            }
+          },
+          // messagesPath: path.resolve(__dirname, '../src/i18n.js')
+        }
+      }
+    }
+  }
+})
+

Mpx 支持国际化,底层实现依赖类wxs能力,通过指定语言标识和语言包,可实现多语言之间的动态切换。可配置项包括locale、messages、messagesPath。

# i18n.locale

string

通过配置 locale 属性,可指定语言标识,默认值为 'zh-CN'

# i18n.messages

object

通过配置 messages 属性,可以指定项目语言包,Mpx 会依据语言包对象定义进行转换,示例如下:

messages: {
+  'en-US': {
+    message: {
+      'title': 'DiDi Chuxing',
+      'subTitle': 'Make travel better'
+    }
+  },
+  'zh-CN': {
+    message: {
+      'title': '滴滴出行',
+      'subTitle': '让出行更美好'
+    }
+  }
+}
+

# i18n.messagesPath

string

为便于开发,Mpx 还支持配置语言包资源路径 messagesPath 来代替 messages 属性,Mpx 会从该路径下的 js 文件导出语言包对象。如果同时配置 messages 和 messagesPath 属性,Mpx 会优先设定 messages 为 i18n 语言包资源。

详细介绍及使用见工具-国际化i18n一节。

# auditResource

'component' | boolean = false

检查资源输出情况,如果置为true,则会提示有哪些资源被同时输出到了多个分包,可以检查是否应该放进主包以消减体积,设置为 'component' 的话,则只检查组件资源是否被输出到多个分包。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        auditResource: true
+      }
+    }
+  }
+})
+

# subpackageModulesRules

object

是否将多分包共用的模块分别输出到各自分包中,匹配规则为include匹配到且未被exclude匹配到的资源

依据微信小程序的分包策略,多个分包使用到的 js 模块会打入主包当中,但在大型分包较多的项目中,该策略极易将大量的模块打入主包,从而使主包体积大小超出2M限制,该配置项提供给开发者自主抉择,可将部分模块冗余输出至多个分包,从而控制主包体积不超限

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        subpackageModulesRules: {
+          include: ['@someNpm/name/src/api/*.js'],
+          exclude: ['@someNpm/name/src/api/module.js']
+        }
+      }
+    }
+  }
+})
+

tips: 该功能是将模块分别放入多个分包,模块状态不可复用,使用前要依据模块功能做好评估,例如全局store就不适用该功能

# generateBuildMap

boolean = false

是否生成构建结果与源码之间的映射文件。用于单元测试等场景。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        generateBuildMap: true
+      }
+    }
+  }
+})
+

参考单元测试

# autoVirtualHostRules

Rules +批量配置是否虚拟化组件节点,对应微信中VirtualHost (opens new window) 。默认不开启,开启后也将抹平支付宝小程序中的表现差异。提供 include 和 exclude 以精确控制对哪些文件开启VirtualHost,哪些不开启。和webpack的rules规则相同。

默认情况下,自定义组件本身的那个节点是一个“普通”的节点,使用时可以在这个节点上设置 classstyle 、动画、 flex 布局等,就如同普通的 view 组件节点一样。但有些时候,自定义组件并不希望这个节点本身可以设置样式、响应 flex 布局等,而是希望自定义组件内部的第一层节点能够响应 flex 布局或者样式由自定义组件本身完全决定。这种情况下,可以将这个自定义组件设置为“虚拟的”。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        autoVirtualHostRules: {
+          include: [resolve('../src')],
+          exclude: [resolve('../components/other')]
+        }
+      }
+    }
+  }
+})
+

注意: 建议使用autoVirtualHostRules配置项,不要使用微信组件内部的 options virtualHost 配置,因为组件内部的 options virtualHost 在跨平台输出时无法进行兼容抹平处理。

# partialCompileRules

{ include: string | RegExp | Function | Array<string | RegExp | Function> }

在大型的小程序开发当中,全量打包页面耗时非常长,往往在开发过程中仅仅只需用到几个 pages 而已,该配置项支持打包指定的小程序页面。

注意: @mpxjs/webpack-plugin@2.9.41版本之前该配置为 partialCompile。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        // include 可以是正则、字符串、函数、数组
+        partialCompileRules: {
+          include: '/project/pages', // 文件路径包含 '/project/pages' 的页面都会被打包
+          include: /pages\/internal/, // 文件路径能与正则匹配上的页面都会被打包
+          include (pageResourcePath) {
+            // pageResourcePath 是小程序页面所在系统的文件路径
+            return pageResourcePath.includes('pages') // 文件路径包含 'pages' 的页面都会被打包
+          },
+          include: [
+            '/project/pages',
+            /pages\/internal/,
+            (pageResourcePath) => pageResourcePath.includes('pages')
+          ] // 满足任意条件的页面都会被打包
+        }
+      }
+    }
+  }
+})
+

WARNING

该特性只能用于开发环境,默认情况下会阻止所有页面(入口 app.mpx 除外)的打包。

# optimizeRenderRules

Rules

render 函数中可能会存在一些重复变量,该配置可消除 render 函数中的重复变量,进而减少包体积。不配置该参数,则不会消除重复变量。

同时框架 render 函数优化提供了两个等级,使用 level 字段来进行控制,默认为 level = 1

  • level = 1时,框架生成 render 函数中完成保留 template 中的计算逻辑,setData 传输量保持了最优。
  • level = 2时,框架生成 render 函数中仅保留所有 template 中使用到的响应性变量,无任何计算逻辑保留,render 函数体积达最小状态,但 setData 传输量相对于 level=1 会有所增加。
// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        optimizeRenderRules: {
+          include: [
+            resolve('src')
+          ],
+          level: 1
+        }
+      }
+    }
+  }
+})
+

# asyncSubpackageRules

type Condition = string | Function | RegExp
+
+interface AsyncSubpackageRules {
+  include: Condition | Array<Condition>
+  exclude?: Condition | Array<Condition>
+  root: string
+  placeholder: string | { name: string, resource?: string}
+}
+
  • include: 同 webpack include 规则
  • exclude: 同 webpack exclude 规则
  • root: 匹配规则的组件或js模块的输出分包名
  • placeholder: 匹配规则的组件所配置的componentPlaceholder,可支持配置原生组件和自定义组件,原生组件可直接以string类型配置,自定义组件需要配置对象,name 为该自定义组件名, resource 为自定义组件的路径,路径可为绝对路径和相对于项目目录的相对路径

异步分包场景下批量设置组件或 js 模块的异步分包,提升资源异步分包输出的灵活性。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        // include 可以是正则、字符串、函数、数组
+        asyncSubpackageRules: [
+          {
+            include: '/project/pages', // 文件路径包含 '/project/pages' 的组件或者 require.async 异步引用的js 模块都会被打包至sub1分包
+            root: 'sub1',
+            placeholder: 'view'
+          }
+        ]
+      }
+    }
+  }
+})
+

WARNING

  • 该配置匹配的组件,若使用方在引用路径已设置?root或componentPlaceholder,则以引用路径中的?root或componentPlaceholder为最高优先级
  • 若placeholder配置使用自定义组件,注意一定要配置 placeholder 中的 resource 字段
  • 本功能只会对使用require.async异步引用的js模块生效,若引用路径中已配置?root,则以路径中?root优先

# retryRequireAsync

boolean = false

开启时在处理require.async时会添加单次重试逻辑

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        retryRequireAsync: true
+      }
+    }
+  }
+})
+

# disableRequireAsync

boolean = false

Mpx 框架在输出 微信小程序、支付宝小程序、字节小程序、Web 平台时,默认支持分包异步化能力,但若在某些场景下需要关闭该能力,可配置该项。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        disableRequireAsync: true
+      }
+    }
+  }
+})
+

# optimizeSize

boolean = false

开启后可优化编译配置减少构建产物体积

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        optimizeSize: true
+      }
+    }
+  }
+})
+

# MpxWebpackPlugin static methods

MpxWebpackPlugin 通过静态方法暴露了以下五个内置 loader,详情如下:

# MpxWebpackPlugin.loader

MpxWebpackPlugin 所提供的最主要 loader,用于处理 .mpx 文件,根据不同的目标平台.mpx 文件输出为不同的结果。

在微信环境下 todo.mpx 被loader处理后的文件为:todo.wxmltodo.wxsstodo.jstodo.json

module.exports = {
+  module: {
+    rules: [
+      {
+        test: /\.mpx$/,
+        use: MpxWebpackPlugin.loader()
+      }
+    ]
+  }
+};
+

# MpxWebpackPlugin.pluginLoader

WARNING

该 loader 仅在开发小程序插件时使用,可在使用 Mpx 脚手架进行项目初始化时选择进行组件开发来生成对应的配置文件。

MpxWebpackPlugin.pluginLoader 用于根据开发者编写的plugin.json文件内容,将特定的小程序组件、页面以及 js 文件进行构建,最终以小程序插件的形式输出。

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  configureWebpack() {
+    return {
+      module: {
+        rules: [
+          {
+            resource: path.resolve('src/plugin/plugin.json'), // 小程序插件的plugin.json的绝对路径
+            use: MpxWebpackPlugin.pluginLoader()
+          }
+        ]
+      }
+    }
+  }
+})
+

更多细节请查阅 小程序插件开发 (opens new window)

# MpxWebpackPlugin.wxsPreLoader

加载并解析 wxs 脚本文件,并针对不同平台,做了差异化处理;同时可支持处理内联wxs

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  configureWebpack() {
+    return {
+      module: {
+        rules: [
+          {
+            test: /\.(wxs|qs|sjs|filter\.js)$/,
+            loader: MpxWebpackPlugin.wxsPreLoader(),
+            enforce: 'pre'
+          }
+        ]
+      }
+    }
+  }
+})
+

# MpxWebpackPlugin.fileLoader

提供图像资源的处理,生成对应图像文件,输出到输出目录并返回 public URL。如果是分包资源,则会输出到相应的分包资源文件目录中。

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  configureWebpack() {
+    return {
+      module: {
+        rules: [
+          {
+            test: /\.(png|jpe?g|gif|svg)$/,
+            loader: MpxWebpackPlugin.fileLoader({
+              name: 'img/[name][hash].[ext]'
+            })
+          }
+        ]
+      }
+    }
+  }
+})
+

选项

  • name : 自定义输出文件名模板

# MpxWebpackPlugin.urlLoader

微信小程序对于图像资源存在一些限制,MpxWebpackPlugin.urlLoader 针对这些差异做了相关处理,开发者可以使用web应用开发的方式进行图像资源的引入,MpxWebpackPlugin.urlLoader 可根据图像资源的不同引入方式,支持 CDN 或者 Base64 的方式进行处理。更多细节请查阅图像资源处理

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  configureWebpack() {
+    return {
+      module: {
+        rules: [
+          {
+            test: /\.(png|jpe?g|gif|svg)$/,
+            loader: MpxWebpackPlugin.urlLoader({
+              name: 'img/[name][hash].[ext]',
+              limit: 2048,
+            })
+          }
+        ]
+      }
+    }
+  }
+})
+

选项:

  • name : 自定义输出文件名模板
  • mimetype : 指定文件的 MIME 类型
  • limit : 对内联文件作为数据 URL 的字节数限制
  • publicPath : 自定义 public 目录
  • fallback : 文件字节数大于限制时,为文件指定加载程序

# MpxWebpackPlugin.getPageEntry

在 webpack config entry 入口文件配置中,你可以使用该方法获取独立构建页面路径,构建产物为该页面的独立原生代码, +你可以提供该页面给其他小程序使用。

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  chainWebpack(config) {
+    config.entry('index').add(MpxWebpackPlugin.getPageEntry('./index.mpx'))
+  }
+})
+

# MpxWebpackPlugin.getComponentEntry

在 webpack config entry 入口文件配置中,你可以使用该方法获取独立构建组件路径,构建产物为该组件的独立原生代码, +你可以提供该组件给其他小程序使用。

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  chainWebpack(config) {
+    config.entry('index').add(MpxWebpackPlugin.getComponentEntry('./components/list.mpx'))
+  }
+})
+

# MpxWebpackPlugin.getNativeEntry

在 webpack config entry 入口文件配置中,你可以使用该方法获取原生小程序入口文件路径。如果你不想将原生的小程序入口文件整合为 app.mpx 文件,则可以使用该方法直接使用原有的小程序入口文件进行编译。见#1330

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  chainWebpack(config) {
+    // 将 app 入口由默认的 app.mpx 更改为 app.js
+    config.entry('app').clear().add(MpxWebpackPlugin.getNativeEntry('./src/app.js'))
+  }
+})
+

# MpxUnocssPlugin

Mpx 编译 unocss 原子类的 webpack 主插件

如果在使用 @mpxjs/cli@3.x 创建项目时选择了 unocss,会自动安装 MpxUnocssPlugin ,直接在 mpx.unocss 配置项中传入相关配置即可

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        // @mpxjs/unocss-plugin 相关的配置
+      }
+    }
+  }
+})
+

如果创建项目时未选 unocss,需手动安装,安装示例如下:

npm install -D @mpxjs/unocss-plugin
+pnpm install -D @mpxjs/unocss-plugin
+yarn add -D @mpxjs/unocss-plugin
+

使用示例如下:

  // vue.config.js
+  const MpxUnocssPlugin = require('@mpxjs/unocss-plugin')
+  const { defineConfig } = require('@vue/cli-service')
+
+  module.exports = defineConfig({
+    configureWebpack: {
+      plugins: [
+        new MpxUnocssPlugin({
+          // @mpxjs/unocss-plugin 相关的配置
+        })
+      ]
+    },
+  })
+

插件支持配置如下:

# unoFile

string = 'styles/uno'

生成主包或分包通用样式存储的相对路径

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        unoFile: 'styles/uno'
+      }
+    }
+  }
+})
+

则会把通用样式存储到下面目录

  // 主包
+  dist/wx/styles/uno.wxss
+  // 分包
+  dist/wx/package/styles/uno.wxss
+

# minCount

number = 2

使用到某个原子类的最小分包个数,比如设置为2的话一个原子类只有超过2个分包使用才会输出到主包

主要是用来控制主包占用的,数值越大分包的原子类就有更大可能性不占用主包

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        minCount: 2
+      }
+    }
+  }
+})
+
  <!-- minCount=2 -->
+  <!-- a分包 -->
+  <view class="bg-black color-white"></view>
+  <!-- b分包 -->
+  <view class="bg-black"></view>
+

unocss将把生成的bg-black样式打包到主包

# styleIsolation

string = 'isolated'

需要和微信小程序的styleIsolation配合使用,比如小程序使用样式隔离的话,这里需要对应配置为isolated,这样的话每个组件会独立引用对应的原子类文件,配置为'apply-shared'的话只有父级页面和app会建立引用,然后通过配合微信的apply-shared的方式获取父级上定义的原子类

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        styleIsolation: 'isolated'
+      }
+    }
+  }
+})
+

# scan

  interface Scan {
+    include?: string[]
+    exclude?: string[]
+  }
+

配置需要扫描的文件目录

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        scan: {
+          include: ['src/**/*']
+        }
+      }
+    }
+  }
+})
+

# escapeMap

object

针对原子类中出现的[ ( ,等特殊字符,在web中会通过转义字符\进行转义,由于小程序环境下不支持css选择器中出现\转义字符,我们内置支持了一套不带\的转义规则对这些特殊字符进行转义,同时替换模版和css文件中的类名,内建的默认转义规则,可自定义转译规则

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        escapeMap: {
+          ':': '_d_',
+        }
+      }
+    }
+  }
+})
+
  <view class="dark:text-green-400"/>
+

将会转化为

  .dark .dark_d_text-green-400{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
+

# root

string = process.cwd()

文件根目录

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        root: process.cwd()
+      }
+    }
+  }
+})
+

# transformCSS

boolean = true

转化css指令为常规css

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        transformCSS: true
+      }
+    }
+  }
+})
+
  .custom-div {
+    @apply text-center my-0 font-medium;
+  }
+

将会转化为

  .custom-div {
+    margin-top: 0;
+    margin-bottom: 0;
+    text-align: center;
+    font-weight: 500;
+  }
+

# transformGroups

boolean = true

转化Variant group

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        transformGroups: true
+      }
+    }
+  }
+})
+
  <view class="lg:(p-2 m-2 text-red-600)"></view>
+

将会转化为

  <view class="lg:p-2 lg:m-2 lg:text-red-600"></view>
+

# config

UserConfig | string

config可以传配置对象也可以传一个配置文件路径

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        config: {
+          rules: [
+            ['m-1', { margin: '10rpx' }],
+          ]
+        }
+      }
+    }
+  }
+})
+

# configFiles

LoadConfigSource[]

  interface LoadConfigSource<T = any> {
+      files: Arrayable<string>;
+      /**
+       * @default ['mts', 'cts', 'ts', 'mjs', 'cjs', 'js', 'json', '']
+      */
+      extensions?: string[];
+      /**
+       * Loader for loading config,
+      *
+      * @default 'auto'
+      */
+      parser?: BuiltinParsers | CustomParser<T> | 'auto';
+      /**
+       * Rewrite the config object,
+      * return nullish value to bypassing loading the file
+      */
+      rewrite?: <F = any>(obj: F, filepath: string) => Promise<T | undefined> | T | undefined;
+      /**
+       * Transform the source code before loading,
+      * return nullish value to skip transformation
+      */
+      transform?: (code: string, filepath: string) => Promise<string | undefined> | string | undefined;
+      /**
+       * Skip this source if error occurred on loading
+      *
+      * @default false
+      */
+      skipOnError?: boolean;
+  }
+

configFiles的话是传递额外的配置文件数组,比如不想用uno.config作为配置文件的话可以在这里面配

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      unocss: {
+        configFiles: [
+          {
+            files: [
+              'uno2.config.js'
+            ]
+          }
+        ]
+      }
+    }
+  }
+})
+

# commentConfig

我们还支持了commentConfig进行组件局部配置,目前支持safelist和styleIsolation,safelist可以用空格分隔写多个

  <template>
+    <!-- mpx_config_styleIsolation: 'isolated' -->
+    <!-- mpx_config_safelist: 'text-red-500 bg-black' -->
+    <view>mpx-unocss</view>
+  </template>
+

# MpxUnocssBase

Mpx 内置的 unocss preset,继承自 @unocss/preset-uno,并额外提供小程序原子类的预设样式,安装示例如下:

npm install -D @mpxjs/unocss-base
+pnpm install -D @mpxjs/unocss-base
+yarn add -D @mpxjs/unocss-base
+

使用示例如下:

  // uno.config.js
+  const { defineConfig } = require('unocss')
+  const presetMpx = require('@mpxjs/unocss-base')
+
+  module.exports = defineConfig({
+    presets: [
+      presetMpx({
+        // ...
+      })
+    ],
+    // unocss的config,具体配置参考https://unocss.dev/config/
+  })
+

支持的配置项如下:

# baseFontSize

number = 37.5

同比换算1rem = 37.5rpx适配小程序

  // uno.config.js
+  const { defineConfig } = require('unocss')
+  const presetMpx = require('@mpxjs/unocss-base')
+
+  module.exports = defineConfig({
+    presets: [
+      presetMpx({
+        baseFontSize: 37.5
+      })
+    ],
+  })
+

# preflight

boolean = true

是否生成预设样式

  // uno.config.js
+  const { defineConfig } = require('unocss')
+  const presetMpx = require('@mpxjs/unocss-base')
+
+  module.exports = defineConfig({
+    presets: [
+      presetMpx({
+        preflight: true
+      })
+    ],
+  })
+

将添加预设样式在主包

page{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
+

# dark

class | media | DarkModeSelectors = 'class'

  interface DarkModeSelectors {
+    /**
+     * light variant的选择器.
+     *
+     * @default '.light'
+     */
+    light?: string
+
+    /**
+     * dark variant的选择器
+     *
+     * @default '.dark'
+     */
+    dark?: string
+  }
+

默认情况下,此预设使用dark:variant生成基于类的dark模式。

  <view class="dark:text-green-400" />
+

我们将生成

  .dark .dark_c_text-green-400{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
+

要选择基于媒体查询的dark模式,您可以使用@dark:variant

  <view class="@dark:text-green-400" />
+

我们将生成

  @media (prefers-color-scheme: dark){
+    ._u_dark_c_text-green-400{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
+  }
+

或者使用针对dark:variant的配置进行全局设置

  presetMpx({
+    dark: "media"
+  })
+

# attributifyPseudo

boolean = false

将伪选择器生成为[group=''],而不是.group。只支持group|peer|parent|previous +如果attributifyPseudo为true的话,

  <view class="group">
+    <view class="group-hover:opacity-100" />
+  </view>
+

上面template将生成

  [group=""]:hover .group-hover_c_opacity-100{opacity:1;}
+

为false则生成

  .group:hover .group-hover_c_opacity-100{opacity:1;}
+

# variablePrefix

string = 'un-'

CSS变量的前缀

  presetMpx({
+    variablePrefix: 'un-'
+  })
+
  .bg-red-500{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));}
+

# Request query

Mpx中允许用户在request中传递特定query执行特定逻辑,目前已支持的query如下:

# ?resolve

用于获取资源最终被输出的正确路径。

Mpx 在处理页面路径时会把对应的分包名作为 root 加在路径前。处理组件路径时会添加 hash,防止路径名冲突。直接写资源相对路径可能与最终输出的资源路径不符。

编写代码时使用 import 引入页面地址后加上 ?resolve,这个地址在编译时会被处理成正确的绝对路径,即资源的最终输出位置。

import subPackageIndexPage from '../subpackage/pages/index.mpx?resolve'
+
+mpx.navigateTo({
+  url: subPackageIndexPage
+})
+

# ?root

  1. 声明分包别名

指定分包别名,Mpx 项目在编译构建后会输出该别名的分包,外部小程序或 H5 页面跳转时,可直接配置该分包别名下的资源路径。

// 可在项目app.mpx中进行配置
+module.exports = {
+  packages: [
+    '@packagePath/src/app.mpx?root=test',
+  ]
+}
+
+// 使用
+wx.navigateTo({url : '/test/homepage/index'})
+
  1. 声明组件所属异步分包

微信小程序新增 分包异步化特性 (opens new window) ,使跨分包的组件可以等待对应分包下载后异步使用, 在mpx中使用需通过?root声明组件所属异步分包即可使用,示例如下:

<!--/packageA/pages/index.mpx-->
+// 这里在分包packageA中即可异步使用分包packageB中的hello组件
+<script type="application/json">
+  {
+    "usingComponents": {
+      "hello": "../../packageB/components/hello?root=packageB",
+      "simple-hello": "../components/hello"
+    },
+    "componentPlaceholder": {
+      "hello": "simple-hello"
+    }
+  }
+</script>
+

# ?fallback

boolean

对于使用MpxWebpackPlugin.urlLoader的文件,如果在引用资源的末尾加上?fallback=true,则使用配置的自定义loader。图片的引入和处理详见图像资源处理

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      urlLoader: {
+        name: 'img/[name][hash].[ext]',
+        publicPath: 'http://a.com/',
+        fallback: 'file-loader' // 自定义fallback为true时使用的loader
+      }
+    }
+  }
+})
+
/* png资源引入 */
+<style>
+  .logo2 {
+    background-image: url('./images/logo.png?fallback=true'); /* 设置fallback=true,则使用如上方所配置的file-loader */
+  }
+</style>
+

# ?useLocal

boolean

静态资源存放有两种方式:本地、远程(配置 publicPath )。useLocal 是用于在配置了 publicPath 时声明部分资源输出到本地。比如配置了通用的 CDN 策略,但如网络兜底资源需要强制走本地存储,可在引用资源的末尾加上?useLocal=true

/* 单个图片资源设置为存储到本地 */
+<style>
+  .logo2 {
+    background-image: url('./images/logo.png?useLocal=true');
+  }
+</style>
+

# ?isStyle

boolean

isStyle 是在非 style 模块中编写样式时,声明这部分引用的静态资源按照 style 环境来处理。如在 javascript 中 require 了一个图像资源,然后模版 template style 属性中进行引用, 则 require 资源时可选择配置?isStyle=true

<template>
+  <view class="list">
+    <!-- isStyle 案例一:引用 javascript 中的数据 -->
+    <view style="{{testStyle}}">测试</view>
+    <!-- isStyle 案例二:设置资源按照style处理规则处理。style处理规则为: 默认走base64,除非同时配置了 publicPath 和 fallback -->
+    <image src="../images/car.jpg?isStyle=true"></image>
+    <!-- 普通非style模式,默认走 fallback 或者 file-loader 解析,输出到 publicPath 或者 本地img目录下 -->
+    <image src="../images/car.jpg"></image>
+  </view>
+</template>
+
/* 将 script 中的图像资源标识为 style 资源 */
+<script>
+  import { createComponent } from '@mpxjs/core'
+  const backCar = require('../images/car.jpg?isStyle=true')
+
+  createComponent({
+    data: {},
+    computed: {
+      testStyle () {
+        return `background-image : url(${backCar}); width:100px; height: 100px`
+      }
+    }
+  })
+</script>
+

# ?isPage

boolean

在 webpack config entry 入口文件配置中,你可以在路径后追加 ?isPage 来声明独立页面构建,构建产物为该页面的独立原生代码, +你可以提供该页面给其他小程序使用。此外,独立页面构建也可以通过MpxWebpackPlugin.getPageEntry生成,推荐使用此方法。

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  chainWebpack(config) {
+    config.entry('index').add('../src/pages/index.mpx?isPage')
+    // 或者
+    // config.entry('index').add(MpxWebpackPlugin.getPageEntry('./index.mpx'))
+  }
+})
+

WARNING

对于使用 @mpxjs/cli@2.x 脚手架初始化的项目,配置 entry 的方式如下

// build/getWebpackConf.js
+module.exports = {
+  entry: {
+    index: '../src/pages/index.mpx?isPage'
+  }
+}
+

# ?isComponent

boolean

在 webpack config entry 入口文件配置中,你可以在路径后追加 ?isComponent 来声明独立组件构建,构建产物为该组件的独立原生代码, +你可以提供该组件给其他小程序使用。 +此外,独立组件构建也可以通过MpxWebpackPlugin.getComponentEntry生成,推荐使用此方法。

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
+module.exports = defineConfig({
+  chainWebpack(config) {
+    config.entry('index').add('../src/components/list.mpx?isComponent')
+    // 或者
+    // config.entry('index').add(MpxWebpackPlugin.getComponentEntry('./components/list.mpx'))
+  }
+})
+

# ?async

boolean | string

输出 H5 时 Vue Router 的路由懒加载功能,Mpx框架默认会对分包开启路由懒加载功能并将分包所有页面都打入同一个Chunk +,如果你希望对于部分主包页面或者分包页面配置路由懒加载并想自定义Chunk Name,则可以使用该功能。

// app.mpx 
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/index?async", // 主包页面配置路由懒加载,Chunk Name 为随机数字
+      "./pages/index2?async=app_pages2"// 主包页面配置路由懒加载,Chunk Name 自定义为 app_pages2
+    ],
+    "packages": [
+      "./packages/sub1/app.mpx?root=sub1"
+    ]
+  }
+</script>
+
+// packages/sub1/app.mpx
+
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/index?async=sub1_pages_index", // 分包中页面设置路由懒加载并设置自定义Chunk Name
+      "./pages/index2?async=sub2_pages_index2" // 分包中页面设置路由懒加载并设置自定义Chunk Name
+    ]
+  }
+</script>
+
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/api/composition-api.html b/docs-vuepress/.vuepress/dist/api/composition-api.html new file mode 100644 index 0000000000..3236d17230 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/composition-api.html @@ -0,0 +1,117 @@ + + + + + + 组合式 API | Mpx框架 + + + + + + + + + +

# 组合式 API

# setup

一个组件选项,在组件被创建之前,props 被解析之后执行。是组合式 API 的入口。

  • 参数: +
    • {Data} props
    • {SetupContext} context

props 对象仅包含显性声明的 properties。并且所有声明了的prop,不论父组件是否向其传递, +都将出现在 props 对象中。其中未被传入的可选的 prop 的值会是默认值或 undefined。

import { createComponent } from '@mpxjs/core'
+
+createComponent({
+    properties: {
+        min: {
+            type: Number,
+            value: 0
+        },
+        lastLeaf: {
+            // 这个属性可以是 Number 、 String 、 Boolean 三种类型中的一种
+            type: Number,
+            optionalTypes: [String, Object],
+            value: 0
+        }
+    },
+    setup(props) {
+        console.log(props.min)
+        console.log(props.lastLeaf)
+    }
+})
+
  • 类型声明
interface SetupContext {
+    triggerEvent(
+       name: string,
+       detail?: object, // detail对象,提供给事件监听函数
+       options?: {
+         bubbles?: boolean
+         composed?: boolean
+         capturePhase?: boolean
+       }
+    ): void
+    refs: ObjectOf<NodesRef & ComponentIns>
+    asyncRefs: ObjectOf<Promise<NodesRef & ComponentIns>> // 字节小程序特有
+    nextTick: (fn: () => void) => void
+    forceUpdate: (params?: object, callback?: () => void) => void
+    selectComponent(selector: string): ComponentIns
+    selectAllComponents(selector: string): ComponentIns[]
+    createSelectorQuery(): SelectorQuery
+    createIntersectionObserver(
+      options: {
+        thresholds?: Array<number>
+        initialRatio?: number
+        observeAll?: boolean
+      }
+    ): IntersectionObserver}
+
+function setup(props: Record<string, any>, context: SetupContext): Record<string, any>
+

# 生命周期钩子

可以通过直接导入 on* 函数来注册生命周期钩子:

import { onMounted, onUpdated, onUnmounted, createComponent } from '@mpxjs/core'
+
+createComponent({
+  setup() {
+    onMounted(() => {
+      console.log('mounted!')
+    })
+    onUpdated(() => {
+      console.log('updated!')
+    })
+    onUnmounted(() => {
+      console.log('unmounted!')
+    })
+  }
+})
+

这些生命周期钩子注册函数只能在 setup() 期间同步使用,因为它们依赖于内部的全局状态来定位当前活动的实例 (此时正在调用其 setup() 的组件实例)。 +在没有当前活动实例的情况下,调用它们将会出错。

组件实例的上下文也是在生命周期钩子的同步执行期间设置的,因此,在生命周期钩子内同步创建的侦听器和计算属性也会在组件卸载时自动删除。

新版本的生命周期钩子我们基本上和 Vue 中的生命周期钩子对齐,相较于之前还是有部分生命周期钩子的改动。

# onBeforeMount

Function

在组件布局完成后执行,refs 相关的前置工作在该钩子中执行。

# onMounted

Function

在组件布局完成后执行,refs 可以直接获取。

# onBeforeUpdate

Function

在数据发生改变后,组件/页面更新之前被调用。这里适合在现有组件/页面将要被更新之前访问它, +比如移除某个手动添加的监听器,或者获取某个元素更新前的高度。

# onUpdated

Function

在数据更改导致的页面/组件重新渲染和更新完毕之后被调用。

注意,onUpdated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等待整个视图都渲染完毕,可以在 onUpdated 内部使用 nextTick。

# onBeforeUnmount

Function

在卸载组件/页面实例之前调用。在这个阶段,实例仍然是完全正常的。

# onUnmount

Function

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除。

# onLoad

Function

小程序页面 onLoad 事件,监听页面加载。

# onShow

Function

小程序页面 onShow 事件,监听页面展示。

# onHide

Function +小程序页面 onHide 事件,监听页面隐藏。

# onResize

Function

小程序页面 onResize 事件,页面尺寸改变时触发。

# onPullDownRefresh

Function

小程序监听用户下拉刷新事件。详细介绍 (opens new window)

# onReachBottom

Function

小程序监听用户上拉触底事件。详细介绍 (opens new window)

# onShareAppMessage

Function

小程序监听用户点击页面内转发按钮(button 组件 open-type="share")或右上角菜单“转发”按钮的行为,并自定义转发内容。详细介绍 (opens new window)

# onShareTimeline

Function

小程序监听右上角菜单“分享到朋友圈”按钮的行为,并自定义分享内容。详细介绍 (opens new window)

注意: 仅微信小程序支持

# onAddToFavorites

Function

小程序监听用户点击右上角菜单“收藏”按钮的行为,并自定义收藏内容。详细介绍 (opens new window)

注意: 仅微信小程序支持

# onPageScroll

Function

小程序监听用户滑动页面事件。详细介绍 (opens new window)

# onTabItemTap

Function

点击 tab 时触发。详细介绍 (opens new window)

# onSaveExitState

Function

每当小程序可能被销毁之前,页面回调函数 onSaveExitState 会被调用,可以进行退出状态的保存。详细介绍 (opens new window)

注意: 仅微信小程序支持

# onServerPrefetch

  • 类型: Function
  • 详细:

SSR渲染定制钩子,在服务端渲染期间被调用,可以实现在服务端进行数据预取。

注意: 仅 web 环境支持

# getCurrentInstance

getCurrentInstance 支持访问内部组件实例。

  • 注意:

getCurrentInstance 只暴露给高阶使用场景,典型的比如在库中。强烈反对在应用的代码中使用 getCurrentInstance。请不要把它当作在组合式 API 中获取 this 的替代方案来使用。

getCurrentInstance 只能在 setup 或生命周期钩子中调用。

# useI18n

点击查看详情

# useFetch

点击查看详情

+ + + diff --git a/docs-vuepress/.vuepress/dist/api/directives.html b/docs-vuepress/.vuepress/dist/api/directives.html new file mode 100644 index 0000000000..655df54106 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/directives.html @@ -0,0 +1,434 @@ + + + + + + 模板指令 | Mpx框架 + + + + + + + + + +

# 模板指令

# wx:if

any

根据表达式的值的 truthiness (opens new window) 来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。 注意:如果元素是 <block/>, 注意它并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性

DANGER

当和 wx:if 一起使用时,wx:for 的优先级比 wx:if 更高。详见列表渲染教程

参考: 条件渲染 - wx:if

# wx:elif

any

前一兄弟元素必须有 wx:ifwx:elif。表示 wx:if 的“ wx:elif 块”,可以链式调用。

<view wx:if="{{type === 'A'}}">
+  A
+</view>
+<view wx:elif="{{type === 'B'}}">
+  B
+</view>
+<view wx:elif="{{type === 'C'}}">
+  C
+</view>
+<view wx:else>
+  Not A/B/C
+</view>
+

参考: 条件渲染 - wx:elif

# wx:else

不需要表达式,前一兄弟元素必须有 wx:ifwx:elif

wx:if 或者 wx:elif 添加 wx:else

<view wx:if="{{type === 'A'}}">
+  A
+</view>
+<view wx:else>
+  Not A
+</view>
+

参考: 条件渲染 - wx:else

# wx:for

Array | Object | number | string

在组件上使用 wx:for 控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item

<view wx:for="{{array}}">
+  {{ index }}: {{ item.message }}
+</view>
+
+// 0: foo
+// 1: bar
+
Page({
+  data: {
+    array: [{
+      message: 'foo'
+    }, {
+      message: 'bar'
+    }]
+  }
+})
+

wx:for 的默认行为会尝试原地修改元素而不是移动它们。要强制其重新排序元素,你需要用特殊 attribute key 来提供一个排序提示:

<view wx:for="{{array}}" wx:key="id">
+  {{ item.text }}
+</view>
+
+// foo
+// bar
+
Page({
+  data: {
+    array: [{
+      id: 1, text: 'foo'
+    }, {
+      id: 2, text: 'bar'
+    }]
+  }
+})
+

DANGER

当和 wx:if 一起使用时,wx:for 的优先级比 wx:if 更高。详见列表渲染教程

wx:for 的详细用法可以通过以下链接查看教程详细说明。

参考: 列表渲染 - wx:for

# wx:for-index

string

使用 wx:for-index 可以指定数组当前下标的变量名:

<view wx:for="{{array}}" wx:key="id" wx:for-index="idx">
+  {{ idx }}: {{ item.text }}
+</view>
+
+// 0: foo
+// 1: bar
+
Page({
+  data: {
+    array: [{
+      id: 1, text: 'foo'
+    }, {
+      id: 2, text: 'bar'
+    }]
+  }
+})
+

参考: 列表渲染 - wx:for-index

# wx:for-item

string

使用 wx:for-item 可以指定数组当前元素的变量名:

<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i">
+  <view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j">
+    <view wx:if="{{i <= j}}">
+      {{i}} * {{j}} = {{i * j}}
+    </view>
+  </view>
+</view>
+

参考: 列表渲染 - wx:for-item

# wx:key

number | string

如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态,需要使用 wx:key 来指定列表中项目的唯一的标识符。 +注意:如不提供 wx:key,会报一个 warning, 如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

常见的用例是结合 wx:for

<view wx:for="{{array}}" wx:key="id">
+  {{ item.text }}
+</view>
+

参考: 列表渲染 - wx:for

# wx:class

绑定HTML Class: 类似vue的class绑定

#对象用法

我们可以传给 wx:class 一个对象,以动态地切换 class:

<view wx:class="{{ {active: isActive} }}">
+  这是一段测试文字
+</view>
+

你可以在对象中传入更多字段来动态切换多个 class。此外,wx:class 指令也可以与普通的 class attribute 共存。

<view class="static" wx:class="{{ {active: isActive, text-danger: hasError} }}">
+  这是一段测试文字
+</view>
+
<script>
+  import {createComponent} from '@mpxjs/core'
+
+  createComponent({
+    data: {
+     isActive: true,
+     hasError: false
+    }
+  })
+</script>
+

渲染为:

<view class="static active">
+  这是一段测试文字
+</view>
+

注意:由于微信的限制,wx:class 中的 key 值不能使用引号(如: { 'my-class-name': xx })

绑定的数据对象不必内联定义在模板里:

<view wx:class="{{ classObject }}">
+  这是一段测试文字
+</view>
+
<script>
+  import {createComponent} from '@mpxjs/core'
+
+  createComponent({
+    data: {
+      classObject: {
+        active: true,
+        'text-danger': false
+      }
+    }
+  })
+</script>
+

我们也可以在这里绑定一个返回对象的计算属性。

如果你也想根据条件切换列表中的 class,可以用三元表达式:

<view wx:class="{{ isActive ? 'active' : '' }}">
+  这是一段测试文字
+</view>
+

#数组用法

我们可以把一个数组传给 wx:class,以应用一个 class 列表:

<view wx:class="{{[activeClass, errorClass]}}">
+  这是一段测试文字
+</view>
+
<script>
+  import {createComponent} from '@mpxjs/core'
+
+  createComponent({
+    data: {
+      activeClass: 'active',
+      errorClass: 'text-danger'
+    }
+  })
+</script>
+

渲染为:

<view wx:class="active text-danger">
+  这是一段测试文字
+</view>
+

参考: 类名样式绑定 - 类名绑定

# wx:style

wx:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case) 来命名:

<view wx:style="color: {{activeColor}}; font-size: {{fontSize}}px; fontWeight: bold">
+  这是一段测试文字
+</view>
+
<script>
+  import {createComponent} from '@mpxjs/core'
+
+  createComponent({
+    data: {
+      activeColor: 'red',
+      fontSize: 30
+    }
+  })
+</script>
+

直接绑定到一个样式对象通常更好,这会让模板更清晰:

<view wx:style="{{styleObject}}">
+  这是一段测试文字
+</view>
+
<script>
+  import {createComponent} from '@mpxjs/core'
+
+  createComponent({
+    data: {
+      styleObject: {
+        color: 'red',
+        fontWeight: 'bold'
+      }
+    },
+  })
+</script>
+

同样的,对象语法常常结合返回对象的计算属性使用。

wx:style 的数组语法可以将多个样式对象应用到同一个元素上

<view wx:style="{{[baseStyles, overridingStyles]}}">
+  这是一段测试文字
+</view>
+

参考: 类名样式绑定 - 样式绑定

# wx:model

除了小程序原生指令之外,mpx 基于input事件提供了一个指令 wx:model, 用于双向绑定。

<template>
+  <view>
+    <input wx:model="{{val}}"/>
+    <input wx:model="{{test.val}}"/>
+    <input wx:model="{{test['val']}}"/>
+  </view>
+</template>
+
+<script>
+  import {createComponent} from '@mpxjs/core'
+  createComponent({
+    data: {
+      val: 'test',
+      test: {
+        val: 'xxx'
+      }
+    }
+  })
+</script>
+

wx:model并不会影响相关的事件处理函数,比如像下面这样:

<input wx:model="{{inputValue}}" bindinput="handleInput"/>
+

参考: 双向绑定

# wx:model-prop

wx:model 默认使用 value 属性传值,使用 wx:model-prop 定义 wx:model 指令对应的属性;

# wx:model-event

wx:model 默认监听 input 事件,可以使用 wx:model-event 定义 wx:model 指令对应的事件;

父组件

<template>
+  <customCheck wx:model="{{checked}}" wx:model-prop="checkedProp" wx:model-event="checkedChange"></customCheck>
+</template>
+
+<script>
+  import {createPage} from '@mpxjs/core'
+  createPage({
+    data: {
+      checked: true
+    }
+  })
+</script>
+<script type="application/json">
+  {
+    "usingComponents": {
+      "customCheck": "./customCheck"
+    }
+  }
+</script>
+
+

子组件:(customCheck.mpx)

<template>
+  <view bindtap="handleTap" class="viewProps">{{checkedProp}}</view>
+</template>
+
+<style lang="stylus">
+  .viewProps {
+    width 100px
+    height 100px
+    color #000
+  }
+</style>
+
+<script>
+  import {createComponent} from '@mpxjs/core'
+  createComponent({
+    properties: {
+      checkedProp: Boolean
+    },
+    methods: {
+      handleTap () {
+        // 这里第二个参数为自定义事件的detail,需要以下面的形式传递值以保持与原生组件对齐
+        this.triggerEvent('checkedChange', {
+          value: !this.checkedProp
+        })
+      }
+    }
+  })
+</script>
+

如示例,当子组件被点击时,父组件的checked数据会发生变化

注意:由于微信限制,如果事件名使用横线分割('checked-change'),将不可以再使用该特性

# wx:model-value-path

指定 wx:model 双向绑定时的取值路径; +并非所有的组件都会按微信的标注格式 event.detail.value 来传值,例如 vant 的 input 组件,值是通过抛出 event.detail 本身传递的,这时我们可以使用 wx:model-value-path="[]" 重新指定取值路径。

<vant-field wx:model-value-path="[]" wx:model="{{a}}"></vant-field>
+

# wx:model-filter

在使用 wx:model 时我们可能需要像 Vue 的 .trim.lazy 这样的修饰符来对双向数据绑定的数据进行过滤和修饰;Mpx 通过增强指令 wx:model-filter 可以实现这一功能; +该指令可以绑定内建的 filter 或者自定义的 filter 方法,该方法接收过滤前的值,返回过滤操作后的值。

例如我们希望拿到的 input 元素中的数据是经过 trim 的。示例:

当然,Mpx 已经内置了 trim 过滤器;可以通过 wx:model-filter="trim" 直接使用;

<template>
+  <view class="cover-page">
+    <view>
+       <!-- wx:model-filter 过滤wx:model 的值-->
+      <input type="text" wx:model-filter="trimSpace" wx:model="{{filterData}}" />
+      <view >{{filterData.length}}</view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+  createPage({
+    data: {
+      filterData: 'model-filter'
+    },
+    trimSpace (val) {
+      // wx:model-filter 绑定的 filter 方法
+      return typeof val === 'string' && val.trim()
+    },
+    methods: {
+    }
+  })
+</script>
+

filter 方法除可以是和 methods 平级的方法,还可以是 methods 中的方法。

<template>...</template>  
+<script>
+  import { createPage } from '@mpxjs/core'
+  createPage({
+    data: {
+      filterData: 'model-filter'
+    },
+    methods: {
+      trimSpace (val) {
+        // wx:model-filter 支持将过滤器方法定义成 methods 中的方法
+        return typeof val === 'string' && val.trim()
+      }
+    }
+  })
+</script>
+

# wx:ref

string

Mpx提供了 wx:ref=xxx 来更方便获取 WXML 节点信息的对象。在JS里只需要通过this.$refs.xxx 即可获取节点。

  <view wx:ref="tref">
+    123
+  </view>
+
+  <script>
+    Page({
+      ready () {
+        this.$refs.tref.fields({size: true}, function (res) {
+          console.log(res)
+        }).exec()
+      }
+    })
+  </script>
+

参考: 获取组件实例 - wx:ref

# wx:show

boolean

wx:if 所不同的是不会移除节点,而是设置节点的 styledisplay: none

<view wx:show="{{show}}">
+  123
+</view>
+
Page({
+  data: {
+    show: false
+  }
+})
+

# bind

string

bind + (:?) + eventType 作为属性值

比如:bindtap

<view bindtap="tapTest"> Click me! </view>
+<view bind:tap="tapTest1(testVal, $event)"> Click me! </view>
+
Page({
+  methods: {
+    tapTest () {
+      console.log('Clicked!')
+    },
+    tapTest1 (val, event) {
+      console.log(val, event)
+    }
+  }
+})
+

Mpx做了增强的内联传参能力以及具体有哪些事件类型参考下方

参考: 事件处理 - bind

# catch

string

catch + (:?) + eventType 作为属性值

bind 外,也可以用 catch 来绑定事件。与 bind 不同,catch 会阻止事件向上冒泡。

<view id="outer" bindtap="handleTap1">
+  outer view
+  <view id="middle" catchtap="handleTap2">
+    middle view
+    <view id="inner" bindtap="handleTap3">
+      inner view
+    </view>
+  </view>
+</view>
+
Page({
+  methods: {
+    handleTap1 () {
+      console.log('outer')
+    },
+    handleTap2 () {
+      console.log('middle')
+    },
+    handleTap3 () {
+      console.log('inner')
+    }
+  }
+})
+// 通过几个操作看出被catchtap的middle view阻止了向上冒泡
+// click outer
+// outer
+
+// click middle
+// middle
+
+// click inner
+// inner
+// middle
+

参考: 事件处理 - catch

# capture-bind

string

capture-bind + (:?) + eventType 作为属性值

capture-bind要在bind之前执行,是因为事件是先捕获后冒泡,注意:仅触摸类事件支持捕获阶段

  <view id="outer" bind:touchstart="handleTap1" capture-bind:touchstart="handleTap2">
+    outer view
+    <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
+      inner view
+    </view>
+  </view>
+

点击inner view的调用顺序是(handleTap)2、4、3、1

参考: 事件处理 - capture-bind

# capture-catch

string

capture-catch + (:?) + eventType 作为属性值

capture-catch中断捕获阶段和取消冒泡阶段

<view id="outer" bind:touchstart="handleTap1" capture-catch:touchstart="handleTap2">
+  outer view
+  <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
+    inner view
+  </view>
+</view>
+

点击inner view仅执行handleTap2

参考: 事件处理 - capture-catch

# @mode

type mode = 'wx' | 'ali' | 'qq' | 'swan' | 'tt' | 'web' | 'qa'

跨平台输出场景下,Mpx 框架允许用户在组件上使用 @ 和 | 符号来指定某个节点或属性只在某些平台下有效。

<button
+  open-type@wx|swan="getUserInfo"
+  bindgetuserinfo@wx|swan="getUserInfo"
+  open-type@ali="getAuthorize"
+  scope@ali="userInfo"
+  onTap@ali="onTap">
+  获取用户信息
+</button>
+

在上方示例中,开发者可以便捷的设置在支付宝小程序与微信和百度小程序等平台分别生效的 open-type 属性, +以及事件绑定或其他属性。假设当前 srcMode 为 wx,目标平台为 ali,则输出产物为:

<button
+  open-type="getAuthorize"
+  scope="userInfo"
+  onTap="onTap">
+  获取用户信息
+</button>
+

同时,该指令也可以作用在单个节点上,但需要注意的是,该指令作用在单个节点时,节点仅在目标平台输出,同时节点自身属性不会进行跨平台语法转换,不过其子节点不受影响。

<!--当srcMode为wx,跨平台输出ali时-->
+<!--错误写法-->
+<view @ali bindtap="someClick">
+    <view wx:if="{{flag}}">text</view>
+</view>
+<!--正确写法-->
+<view @ali onTap="someClick">
+    <view wx:if="{{flag}}">text</view>
+</view>
+

# @_mode

type _mode = '_wx' | '_ali' | '_qq' | '_swan' | '_tt' | '_web' | '_qa'

有时开发者期望使用 @mode 这种方式仅控制节点的展示,保留节点属性的平台转换能力,为此 Mpx 实现了一个隐式属性条件编译能力。

<!--srcMode为 wx,输出 ali 时,bindtap 会被正常转换为 onTap-->
+<view @_ali bindtap="someClick">test</view>
+

在对应的平台前加一个_,例如@_ali、@_swan、@_tt等,使用该隐式规则仅有条件编译能力,节点属性语法转换能力依旧。

# @env

string

跨平台输出场景下,除了 mode 平台场景值,Mpx 框架还提供自定义 env 目标应用,来实现在不同应用下编译产出不同的代码。

关于 env 的详细介绍可以点击查看

跨平台输出使用 env 与 mode 一样支持文件纬度、区块纬度、节点纬度、属性纬度等条件编译,这里我们仅介绍下节点和属性纬度的指令模式使用,env 与 mode 可以组合使用。

env 属性维度条件编译与 mode 的用法大致相同,使用 : 符号与 mode 和其他 env 进行串联,与 mode 组合使用格式形如 attr@mode:env:env|mode:env,为了不与 mode 混淆,当条件编译中仅存在 env 条件时,也需要添加 : 前缀,形如 attr@:env。

<!--open-type 属性仅在百度平台且应用 env 为 didi 时保留-->
+<button open-type@swan:didi="getUserInfo">获取用户信息</button>
+<!--open-type 属性在应用 env 为 didi 的任意小程序平台保留-->
+<button open-type@:didi="getUserInfo">获取用户信息</button>
+<!--open-type 属性在微信平台且应用 env 为 didi 或 qingju 时保留-->
+<button open-type@wx:didi:qingju="getUserInfo">获取用户信息</button>
+

env 也可在单个节点上进行条件编译:

<!--整个节点在应用 env 为 didi时进行保留-->
+<view @:didi>this is a  view component</view>
+

需要注意的时,如果只声明了 env,没有声明 mode,跨平台输出时框架对于节点属性默认会进行转换:

<!--srcMode为wx,跨平台输出ali时,bindtap会被转为onTap-->
+<view @:didi bindtap="someClick">this is a  view component</view>
+<view bindtap@:didi ="someClick">this is a  view component</view>
+

# mpxTagName

string

跨平台输出时,有时开发者不仅需要对节点属性进行条件编译,可能还需对节点标签进行条件编译。

为此,Mpx 框架支持了一个特殊属性 mpxTagName,如果节点存在这个属性,在最终输出时将节点标签修改为该属性的值,配合属性维度条件编译,即可实现对节点标签进行条件编译,例如在百度环境下希望将某个 view 标签替换为 cover-view,我们可以这样写:

<view mpxTagName@swan="cover-view">will be cover-view in swan</view>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/api/extend.html b/docs-vuepress/.vuepress/dist/api/extend.html new file mode 100644 index 0000000000..e6fef447fb --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/extend.html @@ -0,0 +1,365 @@ + + + + + + 周边拓展 | Mpx框架 + + + + + + + + + +

# 周边拓展

# mpx-fetch

mpx-fetch提供了一个实例xfetch ,该实例包含以下api

# fetch(config)

正常的promisify风格的请求方法

  • {Object} config

    config 可指定以下属性:

    • url

      string

      设置请求url

    • method

      string

      设置请求方式,默认为GET

    • data

      object

      设置请求参数

    • params

      object

      设置请求参数,参数会以 Query String 的形式进行传递

    • header

      object

      设置请求的 header,header 中不能设置 Referer。 +content-type 默认为 application/json

    • timeout

      number

      单位为毫秒。若不传,默认读取app.json文件中__networkTimeout属性。 对于超时的处理可在 catch 方法中进行

    • emulateJSON

      boolean

      设置为 true 时,等价于 header = {'content-type': 'application/x-www-form-urlencoded'}

    • usePre

      boolean

      预请求开关,若设置为 true,则两次请求间隔在有效期内且请求参数和请求方式对比一致的情况下,会返回上一次的请求结果

    • cacheInvalidationTime

      number

      预请求缓存有效时长,单位 ms,默认为 5000ms。当两次请求时间间隔超过设置时长后再发起二次请求时,上一次的请求缓存会失效然后重新发起请求

    • ignorePreParamKeys

      array | string

      在判断缓存请求是否可用对比前后两次请求参数时,默认对比的是 options 传入的所有参数(包括 params 和 data )。但在具体业务场景下某些参数不一致时的缓存结果依旧可使用(比如参数中带有时间戳),所以提供 ignorePreParamKeys 来设置对比参数过程中可忽略的参数的 key,支持字符串数组和字符串(字符串传多个 key 时使用英文逗号分隔)类型。 +配置后在进行参数对比时,不会对比在 ignorePreParamKeys 设置的参数。

import mpx from '@mpxjs/core'
+import mpxFetch from '@mpxjs/fetch'
+mpx.use(mpxFetch)
+// 第一种访问形式
+mpx.xfetch.fetch({
+    url: 'http://xxx.com',
+    method: 'POST',
+    params: {
+        age: 10
+    },
+    data: {
+        name: 'test'
+    },
+    header: {
+      'content-type': 'application/x-www-form-urlencoded',
+    },
+    emulateJSON: true,
+    usePre: true,
+    cacheInvalidationTime: 3000,
+    ignorePreParamKeys: ['timestamp']
+}).then(res => {
+	console.log(res.data)
+})
+
+mpx.createApp({
+	onLaunch() {
+		// 第二种访问形式
+		this.$xfetch.fetch({url: 'http://test.com'})
+	}
+})
+

# CancelToken

命名导出,用于创建一个取消请求的凭证。

import { CancelToken } from '@mpxjs/fetch'
+const cancelToken = new CancelToken()
+mpx.xfetch.fetch({
+	url: 'http://xxx.com',
+	data: {
+		name: 'test'
+	},
+	cancelToken: cancelToken.token
+})
+cancelToken.exec('手动取消请求') // 执行后请求中断,返回abort fail
+

# XFetch

命名导出,用于创建一个新的mpx-fetch实例进行独立使用

interface FetchOptions{
+    useQueue: boolean // 是否开启队列功能
+    proxy: boolean // 是否开启代理功能
+}
+
import { XFetch } from '@mpxjs/fetch'
+const newFetch = new XFetch(options) // 生成新的mpx-fetch实例
+

# interceptors

实例属性,用于添加拦截器,包含两个属性,request & response

mpx.xfetch.interceptors.request.use(function(config) {
+    console.log(config)
+    // 也可以返回promise
+    return config
+})
+mpx.xfetch.interceptors.response.use(function(res) {
+    console.log(res)
+    // 也可以返回promise
+    return res
+})
+

# proxy 代理

# setProxy

配置代理项,可以传入一个数组或者一个对象,请求会按设置的规则进行代理

  • 参数:

    {Array | Object}

    • test

      object

      • url

        string

        全路径匹配,规则可以参考path-to-regexp (opens new window),也可参考下面的简单示例。

        WARNING

        如果设置了此项,则 protocol、host、port、path 规则不再生效。此项支持 path-to-regexp 匹配,protocol、host、port、path 为全等匹配。

      • protocol

        string

        待匹配的协议头

      • host

        string

        不包含端口的 host

      • port

        string

        待匹配的端口

      • path

        string

        待匹配的路径

      • params

        object

        同时匹配请求中的 paramsquery

      • data

        object

        匹配请求中的 data

      • header

        object

        匹配请求中的 header

      • method

        Method | Method[]

        匹配请求方法,不区分大小写,可以传一个方法,也可以传一个方法数组

      • custom

        function

        自定义匹配规则,参数会注入原始请求配置,结果需返回 truefalse

        WARNING

        如果设置了此项,匹配结果以此项为准,以上规则均不再生效。

    • proxy

      object

      • url

        string

        代理的 url

      • protocol

        string

        修改原请求的协议头

      • host

        string

        代理的 host,不包含端口号

      • port

        string

        修改端口号

      • path

        string

        修改原请求路径

      • params

        object

        合并原请求的 params

      • data

        object

        合并原请求的 data

      • header

        object

        合并原请求的 header

      • method

        Method

        替换原请求的方法

      • custom

        function

        自定义代理规则,会注入两个参数,第一个是上一个匹配规则处理后的请求配置,第二个是 match 的参数对象,结果需返回要修改的请求配置对象。

        WARNING

        如果设置了此项,最终代理配置将以此项为准,其他配置规则均不再生效。

    • waterfall

      boolean

      默认为 false,为 false 时,命中当前规则处理完就直接返回;为 true 时,命中当前匹配规则处理完成后将结果传递给下面命中匹配规则继续处理。

mpx.xfetch.setProxy([{
+    test: { // 此项匹配之后,会按下面 proxy 配置的修改请求配置
+		header: {
+            'content-type': 'application/x-www-form-urlencoded'
+        },
+        method: 'get',
+        params: {
+            a: 1
+        },
+        data: {
+            test1: 'abc'
+        }
+	},
+	proxy: {
+		header: {
+			env: 'env test'
+		},
+		params: {
+			b: 2
+        },
+        data: {
+            test2: 'cba'
+        }
+	},
+	waterfall: true // 为 true 时会将此次修改后的请求配置继续传递给下面的规则处理
+}, {
+    test: {// 可以分别单独匹配 protocol、host、port、path;代理规则同样
+        protocol: 'http:',
+		host: 'mock.didi.com',
+		port: '',
+		path: '/mock/test'
+    },
+    proxy: {
+        host: 'test.didi.com',
+        port: 8888
+    },
+    waterfall: true
+}, {
+    test: { // 自定义匹配规则
+        custom (config) { // config 为原始的请求配置
+            // 自定义匹配逻辑
+			if (xxx) {
+				return true
+			}
+			return false
+		}
+    },
+    proxy: { // 自定义代理配置
+        custom (config, params) {
+			// config 为上面匹配后修改过的请求配置
+            // params 为 match 后的参数对象
+            // 返回需要修改的请求配置对象
+			return {
+                params: {
+                    c: 3
+                },
+				data: {
+					test3: 'aaa'
+				}
+			}
+		}
+    },
+    waterfall: true
+}, {
+    test: {
+        // : 可以匹配目标项,并将 match 结果返回到代理 custom 函数中
+        // :和?都属于保留符号,若不想做匹配时需用 '\\' 转义
+        // (.*)为全匹配
+        url: ':protocol\\://mock.didi.com/mock/:id/oneapi/router/forum/api/v1/(.*)',
+        method: ['get', 'post']
+    },
+    proxy: {
+        url: 'https://127.0.0.1:8080/go/into/$id/api/v2/abcd' // '$'项在代理生效后会替换匹配规则中的':'项
+    },
+    waterfall: false // false 时不会继续匹配后面的规则
+}])
+

# prependProxy

向前追加代理规则

mpx.xfetch.prependProxy({
+	test: {},
+	proxy: {},
+	waterfall: true
+})
+

# appendProxy

向后追加代理规则

mpx.xfetch.appendProxy({
+	test: {},
+	proxy: {},
+	waterfall: true
+})
+

# getProxy

查看已有的代理配置

console.log(mpx.xfetch.getProxy())
+

# clearProxy

解除所有的代理配置

mpx.xfetch.clearProxy()
+

# useFetch

useFetch(options?: FetchOptions):xfetch
+

在组合式 API 中使用,用来获取 @mpxjs/fetch 插件的 xfetch 实例,等用于 mpx.xfetch。 关于 xfetch 实例的详细介绍,请点击查看

此外该方法可选择传入 options 参数,若传入参数,则会创建一个新的 XFetch 实例返回,若不传入参数,则默认将全局 xfetch 实例返回。

// app.mpx
+import mpx from '@mpxjs/core'
+import mpxFetch from '@mpxjs/fetch'
+mpx.use(mpxFetch)
+
+// script-setup.mpx
+import { useFetch } from '@mpxjs/fetch'
+useFetch().fetch({
+  url: 'http://xxx.com',
+  method: 'POST',
+  params: {
+    age: 10
+  },
+  data: {
+    name: 'test'
+  },
+  emulateJSON: true,
+  usePre: true,
+  cacheInvalidationTime: 3000,
+  ignorePreParamKeys: ['timestamp']
+}).then(res => {
+  console.log(res.data)
+})
+
  • 注意: options 参数同 XFetch 章节。

# api-proxy

Mpx目前已经支持的API转换列表,供参考

方法/平台 wx ali web RN
getSystemInfo
getSystemInfoSync
nextTick
showToast
hideToast
showModal
showLoading
hideLoading
showActionSheet
showNavigationBarLoading
hideNavigationBarLoading
setNavigationBarTitle
setNavigationBarColor
request
downloadFile
uploadFile
setStorage
setStorageSync
removeStorage
removeStorageSync
getStorage
getStorageSync
getStorageInfo
getStorageInfoSync
clearStorage
clearStorageSync
saveImageToPhotosAlbum
previewImage
compressImage
chooseImage
getLocation
saveFile
removeSavedFile
getSavedFileList
getSavedFileInfo
addPhoneContact
setClipboardData
getClipboardData
setScreenBrightness
getScreenBrightness
makePhoneCall
stopAccelerometer
startAccelerometer
stopCompass
startCompass
stopGyroscope
startGyroscope
scanCode
login
checkSession
getUserInfo
requestPayment
createCanvasContext
canvasToTempFilePath
canvasPutImageData
canvasGetImageData
createSelectorQuery
onWindowResize
offWindowResize
arrayBufferToBase64
base64ToArrayBuffer
connectSocket
getNetworkType
onNetworkStatusChange
offNetworkStatusChange

# webview-bridge

Mpx 支持小程序跨平台后,多个平台的小程序里都提供了 webview 组件,webview 打开的 H5 页面可以通过小程序提供的 API 来与小程序通信以及调用一些小程序的能力,但是各家小程序对于 webview 提供的API是不一样的。

比如微信的 webview 打开的 H5 页面里是通过调用 wx.miniProgram.navigateTo 来跳转到原生小程序页面的,而在支付宝是通过调用 my.navigateTo 来实现跳转的,那么我们开发 H5 时候为了让 H5 能适应各家小程序平台就需要写多份对应逻辑。

为解决这个问题,Mpx 提供了抹平平台差异的bridge库:@mpxjs/webview-bridge。

安装:

npm install @mpxjs/webview-bridge
+

使用:

import mpx from '@mpxjs/webview-bridge'
+mpx.navigateBack()
+mpx.env // 输出:wx/qq/ali/baidu/tt
+mpx.checkJSApi()
+

cdn地址引用:

<!-- 开发环境版本,方便调试 -->
+<script src="https://dpubstatic.udache.com/static/dpubimg/D2JeLyT0_Y/2.2.43.webviewbridge.js"></script>
+
+<!-- 生产环境版本,压缩了体积 -->
+<script src="https://dpubstatic.udache.com/static/dpubimg/PRg145LZ-i/2.2.43.webviewbridge.min.js"></script>
+
+
+<!-- 同时支持 ES Module 引入的 -->
+// index.html
+<script type="module" src="https://dpubstatic.udache.com/static/dpubimg/6MQOo-ocI4/2.2.43.webviewbridge.esm.browser.min.js"></script>
+// main.js
+import mpx from "https://dpubstatic.udache.com/static/dpubimg/6MQOo-ocI4/2.2.43.webviewbridge.esm.browser.min.js"
+
+//ES Module 开发版本地址: https://dpubstatic.udache.com/static/dpubimg/cdhpNhmWmJ/2.2.43.webviewbridge.esm.browser.js
+

基础方法提供:

方法/平台 wx qq ali baidu tt
navigateTo
navigateBack
switchTab
reLaunch
redirectTo
getEnv
postMessage
getLoadError
onMessage

扩展方法提供:

方法/平台 wx qq ali baidu tt
checkJSApi
chooseImage
previewImage
uploadImage
downloadImage
getLocalImgData
startRecord
stopRecord
onVoiceRecordEnd
playVoice
pauseVoice
stopVoice
onVoicePlayEnd
uploadVoice
downloadVoice
translateVoice
getNetworkType
openLocation
getLocation
stopSearchBeacons
onSearchBeacons
scanQRCode
chooseCard
addCard
openCard
alert
showLoading
hideLoading
setStorage
getStorage
removeStorage
clearStorage
getStorageInfo
startShare
tradePay
onMessage

WARNING

这个库仅提供给 H5 使用,请勿在小程序环境引入

# size-report

Mpx框架项目包体积可以进行分组、分包、页面、冗余Npm包等维度的分析和对比,详细请见

# 插件配置项

  • server

    object

    本地可视化服务相关配置

  • filename

    string

    构建生成的包体积详细输出文件地址

  • threshold

    object

    配置项目总体积和分包体积阈值,包含两个字段,size 为项目总体积阈值,packages 为分包体积阈值

    {
    +   size: '16MB', // 项目总包体积限额 16M
    +   packages: '2MB' // 项目每个分包体积限额 2M
    +}
    +
  • groups

    Array<object>

    配置体积计算分组,以输入分组为维度对体积进行分析,当没有该配置时结果中将不会包含分组体积信息

    • name

      string

      分组名称

    • threshold

      string | object

      分组相关体积阈值,若不配置则该分组不校验体积阈值,同时也支持对分组中占各分包体积阈值

      // 分组体积限额 500KB
      +threshold: '500KB'
      +// 或者如下方,分组体积限额500KB,分组占主包体积限额160KB
      +threshold: {
      +  size: '500KB',
      +  packages: {
      +    main: '160KB'
      +  }
      +}
      +
    • entryRules

      object

      配置分组 entry 匹配规则,小程序中所有的页面和组件都可被视为 entry

      • include: 包含符合条件的入口文件,默认为空数组,规则数组中支持函数、正则、字符串
      • exclude: 剔除符合条件的入口文件,默认为空数组,规则数组中支持函数、正则、字符串
      include: [/@someGroup\/some-npm-package/],
      +exclude: [/@someGroup\/some-two-pack/]
      +
    • noEntryRules

      object

      配置计算分组中纯 js 入口引入的体积(不包含组件和页面)

      • include: 包含符合条件的 js 文件,默认为空数组,规则数组中支持函数、正则、字符串
      • exclude: 剔除符合条件的 js 文件,默认为空数组,规则数组中支持函数、正则、字符串
      include: [/@someGroup\/some-npm-package/],
      +exclude: [/@someGroup\/some-two-pack/]
      +
  • reportPages

    boolean

    是否收集页面维度体积详情,默认 false

  • reportAssets

    boolean

    是否收集资源维度体积详情,默认 false

  • reportRedundance

    boolean

    是否收集冗余资源,默认 false

  • showEntrysPackages

    Array<string>

    展示某些分包资源的引用来源信息,例如 ['main'] 为查看主包资源的引用来源信息,默认为 []

配置使用示例:

{
+  // 本地可视化服务相关配置
+  server: {
+    enable: true, // 是否启动本地服务,非必填,默认 true
+    autoOpenBrowser: true, // 是否自动打开可视化平台页面,非必填,默认 true
+    port: 0, // 本地服务端口,非必填,默认 0(随机端口)
+    host: '127.0.0.1', // 本地服务host,非必填
+  },
+  // 体积报告生成后输出的文件地址名,路径相对为 dist/wx 或者 dist/ali
+  filename: '../report.json',
+  // 配置阈值,此处代表总包体积阈值为 16MB,分包体积阈值为 2MB,超出将会触发编译报错提醒,该报错不阻断构建
+  threshold: {
+    size: '16MB',
+    packages: '2MB'
+  },
+  // 配置体积计算分组,以输入分组为维度对体积进行分析,当没有该配置时结果中将不会包含分组体积信息
+  groups: [
+    {
+      // 分组名称
+      name: 'vant',
+      // 配置分组 entry 匹配规则,小程序中所有的页面和组件都可被视为 entry,如下所示的分组配置将计算项目中引入的 vant 组件带来的体积占用
+      entryRules: {
+        include: '@vant/weapp'
+      }
+    },
+    {
+      name: 'pageGroup',
+      // 每个分组中可分别配置阈值,如果不配置则表示
+      threshold: '500KB',
+      entryRules: {
+        include: ['src/pages/index', 'src/pages/user']
+      }
+    },
+    {
+      name: 'someSdk',
+      entryRules: {
+        include: ['@somegroup/someSdk/index', '@somegroup/someSdk2/index']
+      },
+      // 有的时候你可能希望计算纯 js 入口引入的体积(不包含组件和页面),这种情况下需要使用 noEntryRules
+      noEntryRules: {
+        include: 'src/lib/sdk.js'
+      }
+    }
+  ],
+  // 是否收集页面维度体积详情,默认 false
+  reportPages: true,
+  // 是否收集资源维度体积详情,默认 false
+  reportAssets: true,
+  // 是否收集冗余资源,默认 false
+  reportRedundance: true,
+  // 展示某些分包资源的引用来源信息,默认为 []
+  showEntrysPackages: ['main']
+}
+

# i18n

# useI18n

组合式 API 中使用,用来获取 i18n 实例。

参数选项


# locale

Locale

设置语言环境

注意: 只传 locale,不传 messages 属性时不起作用

# fallbackLocale

Locale

预设的语言环境,找不到语言环境时进行回退。

# messages

LocaleMessages

本地化的语言环境信息。

返回实例属性和方法


# locale

WritableComputedRef<Locale>

可响应性的 ref 对象,表示当前 i18n 实例所使用的 locale。

修改 ref 值会对局部或者全局语言集的 locale 进行更改,并触发翻译方法重新执行。

# fallbackRoot

boolean

本地化失败时是否回归到全局作用域。

# getLocaleMessage( locale )

function getLocaleMessage (locale: string): LocaleMessageObject
+

获取语言环境的 locale 信息。

# setLocaleMessage( locale, message )

function setLocaleMessage(locale: Locale, messages: LocaleMessageObject): void
+

设置语言环境的 locale 信息。

# mergeLocaleMessage( locale, message )

function mergeLocaleMessage(locale: Locale, messages: LocaleMessageObject): void
+

将语言环境信息 locale 合并到已注册的语言环境信息中。

# messages

readonly messages: ComputedRef<{
+   [K in keyof Messages]: Messages[K];
+}>;
+
  • 只读

局部或者全局的语言环境信息。

# isGlobal

boolean

是否是全局 i18n 实例。

# t

function t(key: string, choice?: number, values: Array | Object): TranslateResult
+

文案翻译函数

根据传入的 key 以及当前 locale 环境获取对应文案,文案来源是全局作用域还是本地作用域取决于 useI18n 执行时是否传入对应的 messages、locale 等值。

choice 参数可选 ,当传入 choice 时,t 函数的表现为使用复数进行翻译,和老版本中的 tc 函数表现一致。

<template>
+  <view>{{t('car', 1)}}</view>
+  <view>{{t('car', 2)}}</view>
+
+  <view>{{t('apple', 0)}}</view>
+  <view>{{t('apple', 1)}}</view>
+  <view>{{t('apple', 10, {count: 10})}}</view>
+</template>
+
+<script>
+  // 语言环境信息如下:
+  const messages = {
+    en: {
+      car: 'car | cars',
+      apple: 'no apples | one apple | {count} apples'
+    }
+  }
+</script>
+

输入如下:

<view>car</view>
+<view>cars</view>
+
+<view>no apples</view>
+<view>one apple</view>
+<view>10 apples</view>
+

关于复数的更多信息可以点击查看 (opens new window)

values 参数可选 ,如果需要对文案信息即逆行格式化处理,则需要传入 values。

<template>
+  // 模版输出 hello world
+  <view>{{t('message.hello', { msg: 'hello'})}}</view>
+</template>
+<script>
+  import {createComponent, useI18n} from "@mpxjs/core"
+
+  const messages = {
+    en: {
+      message: {
+        hello: '{msg} world'
+      }
+    }
+  }
+  
+  createComponent({
+    setup(){
+        const { t } = useI18n({
+          messages: {
+              'en-US': en
+          }
+        })
+      return {t}
+    }
+  })
+
+</script>
+

# te

function te(key: string): boolean
+

检查 key 是否存在。

+ + + diff --git a/docs-vuepress/.vuepress/dist/api/global-api.html b/docs-vuepress/.vuepress/dist/api/global-api.html new file mode 100644 index 0000000000..2fc002be47 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/global-api.html @@ -0,0 +1,253 @@ + + + + + + 全局 API | Mpx框架 + + + + + + + + + +

# 全局 API

# 全局对象 Mpx

@mpxjs/core 默认导出 mpx 全局实例对象,通过该实例对象我们可以访问部分应用实例 API

# mixin

全局注入mixin方法接收两个参数:mpx.mixin(mixins, options)

  • 第一个参数是要混入的mixins,接收类型 MixinObject|MixinObject[]
  • 第二个参数是为全局混入配置,形如{types:string|string[], stage:number},其中types用于控制mixin注入的范围,可选值有'app'|'page'|'component'stage用于控制注入mixin的插入顺序,当stage为负数时,注入的mixin会被插入到构造函数配置中的options.mixins之前,数值越小约靠前,反之当stage为正数时,注入的mixin会被插入到options.mixins之后,数值越大越靠后。

所有mixin中生命周期的执行均先于构造函数配置中直接声明的生命周期,mixin之间的执行顺序则遵从于其在options.mixins数组中的顺序

options的默认值为{types: ['app','page','component'], stage: -1},不传stage时,全局注入mixin的声明周期默认在options.mixins之前执行

import mpx from '@mpxjs/core'
+// 只在page中混入
+mpx.mixin({
+  methods: {
+    getData: function(){}
+  }
+}, {
+  types:'page'
+})
+
+// 默认混入,在app|page|component中都会混入
+mpx.mixin([
+  {
+    methods: {
+      getData: function(){}
+    }
+  },
+  {
+    methods: {
+      setData: function(){}
+    }
+  }
+])
+
+// 只在component中混入,且执行顺序在options.mixins之后
+mpx.mixin({
+  attached() {
+    console.log('com attached')
+  }
+}, {
+  types: 'component',
+  stage: 100
+})
+

# injectMixins

该方法是 mpx.mixin 方法的别名,mpx.injectMixins({}) 等同于 mpx.mixin({})

# observable

function observable(options: object): Mpx
+

用于创建响应式数据。

import mpx from '@mpxjs/core'
+// 直接通过 mpx 对象访问
+const b = mpx.observable(object)
+
  • 注意: +Mpx 2.8 版本后该 API 等同于 reactive,同时不再支持具名导出方式,建议直接使用 reactive 替代,请点击查看。

# set

用于对一个响应式对象新增属性,会触发订阅者更新操作查看详情

# delete

function delete(target: Object, key: string | number): void
+

用于对一个响应式对象删除属性,会触发订阅者更新操作

import mpx, { reactive } from '@mpxjs/core'
+const person = reactive({name: 1})
+mpx.delete(person, 'age')
+
  • 注意: mpx.delete 也可以使用具名导出的 del查看详情

# use

用于安装外部扩展, 支持多参数 +方法接收两个参数:mpx.use(plugin, options)

  • 第一个参数是要安装的外部扩展
  • 第二个参数是对象,如果第二个参数是一个包含(prefix or postfix)的option, 那么将会对插件扩展的属性添加前缀或后缀
import mpx from '@mpxjs/core'
+import test from './test'
+mpx.use(test)
+mpx.use(test, {prefix: 'mpx'}, 'otherparams')
+

# watch

watch 可以通过全局实例访问,也可以使用具名导出的方式,二者逻辑相同,我们推荐使用具名导出的方式。查看详情

# createApp

注册一个小程序,接受一个 Object 类型的参数

import {createApp} from '@mpxjs/core'
+
+createApp({
+  onLaunch () {
+    console.log('Launch')
+  },
+  onShow () {
+    console.log('Page show')
+  },
+  //全局变量 可通过getApp()访问
+  globalDataA: 'I am global dataA',
+  globalDataB: 'I am global dataB'
+})
+// 或者
+createApp(options)
+

# createPage

类微信小程序(微信、百度、头条等)内部使用Component的方式创建页面 (opens new window),所以除了支持页面的生命周期之外还同时支持组件的一切特性。当使用 Component 创建页面时,页面生命周期需要写在 methods 内部(微信小程序原生规则),mpx 进行了统一封装转换,页面生命周期都写在最外层即可

function createPage(options: object, config?: object): void
+
  • options:

    具体形式除了 computed、watch 这类 Mpx 扩展特性之外,其他的属性都参照原生小程序的官方文档即可。

  • config:

    如果希望标识一个组件是最纯粹的原生组件,不用数据响应等能力,可通过 config.isNative 传 true 声明。 +如果有需要复写/改写最终调用的创建页面的构造器,可以通过 config 对象的 customCtor 提供。

    • 注意: +mpx本身是用 component 来创建页面的,如果传page可能在初始化时候生命周期不正常导致取props有一点问题
import {createPage} from '@mpxjs/core'
+
+createPage({
+  data: {test: 1},
+  computed: {
+    test2 () {
+      return this.test + 1
+    }
+  },
+  watch: {
+    test (val, old) {
+      console.log(val, old)
+    }
+  },
+  onShow () {
+    this.test++
+  }
+})
+

# createComponent

创建自定义组件,接受两个Object类型的参数。

function createComponent(options: object, config?: object): void
+
import {createComponent} from '@mpxjs/core'
+
+createComponent({
+  properties: {
+    prop: {
+      type: Number,
+      value: 10
+    }
+  },
+  data: {test: 1},
+  computed: {
+    test2 () {
+      return this.test + this.prop
+    }
+  },
+  watch: {
+    test (val, old) {
+      console.log(val, old)
+    },
+    prop: {
+      handler (val, old) {
+        console.log(val, old)
+      },
+      immediate: true // 是否首次执行一次
+    }
+  }
+})
+

# nextTick

function nextTick(callback: Function): void
+

当我们在 Mpx 中更改响应性状态时,最终页面的更新并不是同步立即生效的,而是由 Mpx 将它们缓存在一个队列中, 等到下一个 tick 一起执行, +从而保证了组件/页面无论发生多少状态改变,都仅执行一次更新,从而减少 setData 调用次数。

nextTick() 可以在状态改变后立即调用,可以传递一个函数作为参数,在等待页面/组件更新完成后,函数参数会触发执行。

     import {createComponent, nextTick, ref} from '@mpxjs/core'
+     createComponent({
+       setup (props, context) {
+         const showChild = ref(false)
+         // DOM 还没有更新
+         setTimeOut(() => {
+            showChild.value = true
+         }, 2000)
+         nextTick(function() {
+           context.refs['child'].showToast()
+         })
+         return {
+           showChild
+         }
+       }
+     })
+

# toPureObject

function toPureObject(options: object): object
+

业务拿到的数据可能是响应式数据实例(包含了些其他属性),使用toPureObject方法可以将响应式的数据转化成纯 js 对象。

import {toPureObject} from '@mpxjs/core'
+const pureObject = toPureObject(object)
+

# getMixin

专为ts项目提供的反向推导辅助方法,该函数接收类型为 Object ,会将传入的嵌套mixins对象拉平成一个扁平的mixin对象

import { createComponent, getMixin} from '@mpxjs/core'
+// 使用mixins,需要对每一个mixin子项进行getMixin辅助函数包裹,支持嵌套mixin
+const mixin = getMixin({
+  mixins: [getMixin({
+    data: {
+      value1: 2
+    },
+    lifetimes: {
+      attached () {
+        console.log(this.value1, 'attached')
+      }
+    },
+    mixins: [getMixin({
+      data: {
+        value2: 6
+      },
+      created () {
+        console.log(this.value1 + this.value2 + this.outsideVal)
+      }
+    })]
+  })]
+})
+/*
+mixin值
+{
+  data: {value2: 6, value1: 2},
+  created: ƒ created(),
+  attached: ƒ attached()
+}
+*/
+createComponent({
+  data: {
+    outsideVal: 20
+  },
+  mixins: [mixin]
+})
+
+/*
+以上执行输出:
+28
+2 "attached"
+*/
+

# implement

function implement(name: string, options: object): object
+
  • {Object} options
    • {Array} modes:需要取消的平台
    • {Boolean} remove:是否将此能力直接移除
    • {Function} processor:设置成功的回调函数

以微信为 base 将代码转换输出到其他平台时(如支付宝、web 平台等),会存在一些无法进行模拟的跨平台差异,会在运行时进行检测并报错指出,例如微信转支付宝时使用 moved 生命周期等。使用implement方法可以取消这种报错。您可以使用 mixin 自行实现跨平台差异,然后使用 implement 取消报错。

import {implement} from '@mpxjs/core'
+
+if (__mpx_mode__ === 'web') {
+  const processor = () => {
+  }
+  implement('onShareAppMessage', {
+    modes: ['web'], // 需要取消的平台,可配置多个
+    remove: true, // 是否将此能力直接移除
+    processor // 设置成功的回调函数
+  })
+}
+

# 内建生命周期变量

Mpx 在运行时自身有着一套内建生命周期,当开发者想使用内建生命周期时,可以通过内建生命周期变量进行对应生命周期的注册, +需要注意的是,这部分内建生命周期变量只能用于选项式 API 中

# BEFORECREATE

string

在组件实例刚刚被创建时执行,在实例初始化之后、进行数据侦听和 data 初始化之前同步调用,注意此时不能调用 setData。

import {createComponent, BEFORECREATE} from "@mpxjs/core"
+
+createComponent({
+  [BEFORECREATE]() {
+      console.log('beforecreate trigger')
+  }
+})
+

# CREATED

string

在组件实例刚刚被创建时执行。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、事件/侦听器的回调函数。 +然而,挂载阶段还没开始,注意此时不能调用 setData。

import {createComponent, CREATED} from "@mpxjs/core"
+
+createComponent({
+  [CREATED]() {
+      console.log('beforecreate trigger')
+  }
+})
+

# BEFOREMOUNT

选项式 API 中使用,作用同onBeforeMount

# MOUNTED

选项式 API 中使用,作用同onMounted

# BEFOREUPDATE

选项式 API 中使用,作用同onBeforeUpdate

# UPDATED

选项式 API 中使用,作用同onUpdated

# SERVERPREFETCH

选项式 API 中使用,作用同onServerPrefetch

# BEFOREUNMOUNT

选项式 API 中使用,作用同onBeforeUnmount

# UNMOUNTED

选项式 API 中使用,作用同onUnmounted

# ONLOAD

选项式 API 中使用,作用同onLoad

# ONSHOW

选项式 API 中使用,作用同onShow

# ONHIDE

选项式 API 中使用,作用同onHide

# ONRESIZE

选项式 API 中使用,作用同onResize

# 响应式 API

详情请移步

# 组合式 API

详情请移步

# store API

详情请移步

+ + + diff --git a/docs-vuepress/.vuepress/dist/api/index.html b/docs-vuepress/.vuepress/dist/api/index.html new file mode 100644 index 0000000000..2e07eecb72 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/index.html @@ -0,0 +1,43 @@ + + + + + + Mpx框架 + + + + + + + + + +

API 参考

+ + + diff --git a/docs-vuepress/.vuepress/dist/api/instance-api.html b/docs-vuepress/.vuepress/dist/api/instance-api.html new file mode 100644 index 0000000000..a6c757cb2c --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/instance-api.html @@ -0,0 +1,265 @@ + + + + + + 实例 API | Mpx框架 + + + + + + + + + +

# 实例 API

# $set

function $set(target: Object | Array, property: string | number, value: any): void
+

这是全局 mpx.set 的别名。向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。 +它必须用于向响应式对象上添加新 property,因为 Mpx 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')

# $watch

function $watch(expOrFn: string | Function, callback: Function | Object, options?: Object): Function
+

观察 Mpx 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。

// 键路径
+this.$watch('a.b.c', function (newVal, oldVal) {
+  // 做点什么
+})
+
+// 函数
+this.$watch(
+  function () {
+    // 表达式 `this.a + this.b` 每次得出一个不同的结果时
+    // 处理函数都会被调用。
+    // 这就像监听一个未被定义的计算属性
+    return this.a + this.b
+  },
+  function (newVal, oldVal) {
+    // 做点什么
+  }
+)
+

this.$watch 返回一个取消观察函数,用来停止触发回调:

var unwatch = this.$watch('a', cb)
+// 之后取消观察
+unwatch()
+
  • options.deep

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

this.$watch('someObject', callback, {
+  deep: true
+})
+this.someObject.nestedValue = 123
+// callback is fired
+
  • options.deep

在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:

this.$watch('a', callback, {
+  immediate: true
+})
+// 立即以 `a` 的当前值触发回调
+

注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

// 这会导致报错
+var unwatch = this.$watch(
+  'value',
+  function () {
+    doSomething()
+    unwatch()
+  },
+  { immediate: true }
+)
+

如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

var unwatch = this.$watch(
+  'value',
+  function () {
+    doSomething()
+    if (unwatch) {
+      unwatch()
+    }
+  },
+  { immediate: true }
+)
+
  • options.pausable

可在选项参数中指定 pausable: true 来声明一个可被暂停的 Watcher 实例,配置好之后可以通过 this.$getPausableWatchers() 来获取当前组件或者页面下定义的所有可被暂停的 Watcher 实例, +然后根据具体是业务场景通过 watcher.pause() 或者 watcher.resume() 来暂停或者恢复 watch 的监听。 +比如说在小程序页面 hide 时不需要监听的 watch 可以配置为 pausable: true,在页面 hide 时调用 watcher.pause() 暂停监听,在页面 show 时调用 watcher.resume() 来恢复监听。

this.$watch('someObject', callback, {
+  pausable: true
+})
+
+const isHide = false
+const watchers = this.$getPausableWatchers()
+if (watchers && watchers.length) {
+  for (let i = 0; i < watchers.length; i++) {
+    const watcher = watchers[i]
+    isHide && watcher.pause()
+    !isHide && watcher.resume()
+  }
+}
+
  • options.name

为了方便获取用户定义的 Watcher 实例,可在选项参数增加配置 name 来设置当前 Watcher 实例的名称,配置 name 后可通过 this.$getWatcherByName(name) 在组件或页面中获取到命名为 name 的 Watcher 实例(注意当存在多个 name 相同 watch 时,this.$getWatcherByName 获取的最后一个使用该 name +创建的 Watcher 实例,所以为了避免混淆,请不要在多个 watch 中配置同一个 name。)

this.$watch('someObject', callback, {
+  name: 'someObject'
+})
+
+const someObjectWatch = this.$getWatcherByName('someObject')
+

参考mpx.watch

# $delete

function $delete(target: Object, key: string | number): void
+

删除对象属性,如果该对象是响应式的,那么该方法可以触发观察器更新(视图更新 | watch回调)

  import {createComponent} from '@mpxjs/core'
+  createComponent({
+  data: {
+    info: {
+      name: 'a'
+    }
+  },
+  watch: {
+    'info' (val) {
+      // 当删除属性之后会执行
+      console.log(val)
+    }
+  },
+  attached () {
+    // 删除name属性
+    this.$delete(this.info, 'name')
+  }
+  })
+

参考: Mpx.delete

# $refs

Object

一个对象,持有注册过 ref的所有 DOM 元素和组件实例,调用响应的组件方法或者获取视图节点信息。

以获取组件为例,模版中引用child子组件

<child wx:ref="childDom"></child>
+

javascript 中可以调用组件的方法

import { createComponent } from '@mpxjs/core'
+createComponent({
+ready (){
+  // 调用child中的方法
+  this.$refs.childDom.childMethods()
+  // 获取child中的data
+  this.$refs.childDom.data
+  },
+})
+

参考: 组件 ref

# $asyncRefs

仅字节小程序可用,因为字节小程序 selectComponentselectAllComponents 方法为异步方法,因此使用 $refs 同步获取组件实例并不保证能够拿到正确的组件实例,需使用异步 $asyncRefs

import mpx, {createComponent} from '@mpxjs/core'
+
+createComponent({
+  ready() {
+    if (__mpx_mode__ === 'tt') {
+      this.$asyncRefs.mlist.then(res => {
+        const data = res.data
+        //......
+      })
+    }
+  }
+})
+

# $forceUpdate

function $forceUpdate(target: Object, callback: Function): void
+

用于强制刷新视图,正常情况下只有发生了变化的数据才会被发送到视图层进行渲染。强制更新时,会将某些数据强制发送到视图层渲染,无论是否发生了变化

import {createComponent} from '@mpxjs/core'
+createComponent({
+  data: {
+    info: {
+      name: 'a'
+    },
+    age: 100
+  },
+  attached () {
+    // 虽然不会修改age的值,但仍会触发重新渲染,并且会将age发送到视图层
+    this.$forceUpdate({
+      age: 100
+    }, () => {
+      console.log('视图更新后执行')
+    })
+
+    // 也可用于正常的数据修改,key支持Path,数组可以使用'array[index]':value的形式
+    this.$forceUpdate({
+      'info.name': 'b'
+    }, () => {
+      console.log('视图更新后执行')
+    })
+  }
+})
+

# $nextTick

function $nextTick(callback: Function): void
+

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。注意:callbackthis并不是绑定当前实例,你可以使用箭头函数避免this指向问题

import {createComponent} from '@mpxjs/core'
+  createComponent({
+  data: {
+    info: {
+      name: 1
+    }
+  },
+  attached () {
+    // 修改数据
+    this.info.name = 2
+    // DOM 还没有更新
+
+    // this.$nextTick(function() {
+    //   // DOM 现在更新了
+    //   console.log('会在由name变化引起的视图更新之后执行')
+    //   this.doSomthing() // 报错
+    // })
+    this.$nextTick(() => {
+      // DOM 现在更新了
+      console.log('会在由name变化引起的视图更新之后执行')
+      this.doSomthing()
+    })
+  }
+})
+

# $i18n

组件中直接调用$i18n的方法,比如:$t,$tc,$te,$d,$n

首先在mpx.plugin.conf.js中配置i18n

module.exports = {
+
+  //...
+
+  i18n: {
+    locale: 'en-US',
+    // messages既可以通过对象字面量传入,也可以通过messagesPath指定一个js模块路径,在该模块中定义配置并导出,dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理
+    messages: {
+      'en-US': {
+        message: {
+          hello: '{msg} world'
+        }
+      },
+      'zh-CN': {
+        message: {
+          hello: '{msg} 世界'
+        }
+      }
+    }
+  }
+}
+

组件中直接使用

<template>
+  <view>{{$t('message.hello')}}</view>
+</template>
+
+import {createComponent} from '@mpxjs/core'
+createComponent({
+  ready () {
+    console.log(this.$t('message.hello', { msg: 'hello' }))
+    console.log(this.$te('message.hello')) 
+    //...
+  }
+})
+

# $rawOptions

Object

获取组件或页面构造器的构造参数。

import { createComponent } from "@mpxjs/core"
+
+createComponent({
+  ready() {
+    console.log(this.$rawOptions)
+    /**
+     * attached
+     * detached
+     * methods
+     * mpxConvertMode
+     * mpxCustomKeysForBlend
+     * mpxFileResource
+     * ready
+     * setup
+     * ...其他构造参数
+     */
+  }
+})
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/api/optional-api.html b/docs-vuepress/.vuepress/dist/api/optional-api.html new file mode 100644 index 0000000000..08662a27ca --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/optional-api.html @@ -0,0 +1,51 @@ + + + + + + 选项式 API | Mpx框架 + + + + + + + + + +

# 选项式 API

# onAppInit

  • 类型: Function
  • 详细:

通过 createApp 注册一个应用期间被调用,输出 web 时 SSR渲染模式下,需要在此钩子中生成 pinia 的实例并返回。

# onSSRAppCreated

  • 类型: Function
  • 详细:

SSR渲染定制钩子,在服务端渲染期间被调用,可以在这个钩子中可以去返回应用程序实例,以及完成服务器端路由匹配,store 的状态挂载等。类 Vue 的 server entry 中的功能。

注意: 仅 web 环境支持

+ + + diff --git a/docs-vuepress/.vuepress/dist/api/reactivity-api.html b/docs-vuepress/.vuepress/dist/api/reactivity-api.html new file mode 100644 index 0000000000..59d7bdaac4 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/reactivity-api.html @@ -0,0 +1,544 @@ + + + + + + 响应式 API | Mpx框架 + + + + + + + + + +

# 响应式 API

# 响应式基础 API

# reactive

将对象处理为响应性对象。

const obj = reactive({ count: 0 })
+obj.count++
+

响应式转换是“深层”的:它会影响到所有嵌套的 property。一个响应式对象也将深层地解包任何 ref property,同时保持响应性。

const count = ref(1)
+const obj = reactive({ count })
+
+// ref 会被解包
+console.log(obj.count === count.value) // true
+
+// 它会更新 `obj.count`
+count.value++
+console.log(count.value) // 2
+console.log(obj.count) // 2
+

当将 ref 分配给 reactive property 时,ref 将被自动解包。

const count = ref(1)
+const obj = reactive({})
+
+obj.count = count
+
+console.log(obj.count) // 1
+console.log(obj.count === count.value) // true
+

当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包

const refArr = ref([ref(2)])
+const state = reactive({
+    count: 1,
+    refArr
+})
+
+// 无需.vlaue
+console.log(state.refArr)
+// 这里需要.value
+console.log(state.refArr[0].value)
+

当需要给响应式对象新增属性时,需要使用set,并且新增属性的响应式对象需要为 ref 或者做为其他响应性对象的key

const state = reactive({
+    count: 1
+})
+const stateRef = ref({
+    count: 1
+})
+const stateRefWrap = reactive({
+    data: {
+        count: 1
+    }
+})
+
+onLoad(() => {
+    setTimeout(() => {
+        set(state, 'foo', 1) // 不生效
+        set(stateRef.value, 'foo', 1) // 生效
+        set(stateRefWrap.data, 'foo', 1) // 生效
+    }, 2000)
+})
+
+return {
+    state,
+    stateRef,
+    stateRefWrap
+}
+

# isReactive

检查对象是否是由 reactive 创建的响应式对象。

import { createComponent, reactive, isReactive } from '@mpxjs/core'
+
+createComponent({
+    setup(){
+        const state = reactive({
+            count: 1
+        })
+        console.log(isReactive(state)) // -> true
+        return {
+            state
+        }
+    }
+})
+

# markRaw

标记一个对象,使其永远不会被抓换为响应性对象,并返回对象本身

import { markRaw, reactive, isReactive } from '@mpxjs/core'
+
+const foo = markRaw({
+    count: 1
+})
+const state = reactive({
+    foo
+})
+console.log(isReactive(state.foo)) // -> false
+

注意:

如果将标记对象内部的未标记对象添加进响应性对象,然后再次访问该响应性对象,就会得到该原始对象的可响应性对象

import { markRaw, reactive, isReactive } from '@mpxjs/core'
+
+const foo = markRaw({
+    nested: {}
+})
+
+const bar = reactive({
+    nested: foo.nested
+})
+
+console.log(foo.nested === bar.nested) // -> true
+

# shallowReactive

reactive() 的浅层作用形式,只跟踪自身 property 的响应性,但不执行嵌套对象的深层响应式转换(返回原始值)

注意:

和 reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的 property 是响应式的。property 的值会被原样存储和暴露,这也意味着值为 ref 的 property 不会被自动解包了。

import { shallowReactive } from '@mpxjs/core'
+
+const count = ref(0)
+const state = shallowReactive({
+  foo: 1,
+  nested: {
+    bar: 2
+  },
+  head: count  
+})
+
+// 更改状态自身的属性是响应式的
+state.foo++
+
+// ...但下层嵌套对象不会被转为响应式
+isReactive(state.nested) // false
+
+// 不是响应式的
+state.nested.bar++
+
+// ref 属性不会解包
+state.head.value 
+

# set

用于对一个响应式对象新增属性,会触发订阅者更新操作

function set(target: Object | Array, property: string | number, value: any): void
+
import { set, reactive } from '@mpxjs/core'
+const person = reactive({name: 1})
+// 具名导出使用
+set(person, 'age', 17) // age 改变后会触发订阅者视图更新
+

# del

用于对一个响应式对象删除属性,会触发订阅者更新操作

function del(target: Object | Array, property: string | number): void
+
import {del, reactive } from '@mpxjs/core'
+const person = reactive({name: 1})
+del(person, 'age')
+

# Computed 与 Watch

# computed

// 只读形式
+function computed<T>(
+    getter: () => T
+): Readonly<Ref<Readonly<T>>>
+
+// 可写形式
+function computed<T>(
+    options: {
+        get: () => T
+        set: (value: T) => void
+    }
+): Ref<T>
+

接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。

const count = ref(1)
+const plusOne = computed(() => count.value + 1)
+
+console.log(plusOne.value) // 2
+
+plusOne.value++ // 错误
+

或者接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。

const count = ref(1)
+const plusOne = computed({
+  get: () => count.value + 1,
+  set: val => {
+    count.value = val - 1
+  }
+})
+
+plusOne.value = 1
+console.log(count.value) // 0
+

# watchEffect

function watchEffect(
+  effect: (onInvalidate: InvalidateCbRegistrator) => void,
+  options?: WatchEffectOptions
+): StopHandle
+
+interface WatchEffectOptions {
+    flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
+}
+
+type InvalidateCbRegistrator = (invalidate: () => void) => void
+
+type StopHandle = () => void
+

立即执行传入的函数,同时对其依赖进行响应式追踪,并在其依赖变更时重新运行该函数。

const count = ref(0)
+
+watchEffect(() => console.log(count.value))
+// -> logs 0
+
+setTimeout(() => {
+  count.value++
+  // -> logs 1
+}, 100)
+
  • 停止侦听
const stop = watchEffect(() => {
+  /* ... */
+})
+
+// later
+stop()
+
  • 清除副作用

侦听副作用传入的函数可以接收一个函数作入参,用来注册清理回调,清理回调会在该副作用下一次执行前被调用, +可以用来清理无效的副作用,例如等待中的异步请求

watchEffect(async (onCleanup) => {
+  const { response, cancel } = doAsyncWork(id.value)
+  // `cancel` 会在 `id` 更改时调用
+  // 以便取消之前未完成的请求
+  onCleanup(cancel)
+  data.value = await response
+})
+
  • 副作用刷新时机

响应式系统会进行副作用函数缓存,并异步地刷新执行他们

组件的 update 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update 前执行, +在watchEffect的第二个参数中,我们可以传入flush来进行副作用刷新时机调整

// 默认为 pre
+watchEffect(callback, {
+    flush: 'pre'
+})
+
+// 侦听器回调中能访问被更新之后的DOM
+// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
+watchEffect(callback, {
+  flush: 'post'
+})
+
+// 强制同步触发, 十分低效
+watchEffect(callback, {
+    flush: 'sync'
+})
+

# watchSyncEffect

watchEffect 的别名,带有 flush: 'sync' 选项。

# watchPostEffect

watchEffect 的别名,带有 flush: 'post' 选项。

# watch

// 侦听单一源
+function watch<T>(
+    source: WatcherSource<T>,
+    callback: (
+        value: T,
+        oldValue: T,
+        onInvalidate: InvalidateCbRegistrator
+    ) => void,
+    options?: WatchOptions
+): StopHandle
+
+// 侦听多个源
+
+type WatcherSource<T> = Ref<T> | (() => T)
+
+type InvalidateCbRegistrator = (invalidate: () => void) => void
+
+type StopHandle = () => void
+
+interface WatchOptions extends WatchEffectOptions {
+    immediate?: boolean // 默认:false
+    deep?: boolean // 默认:false
+    immediateAsync: boolean // 默认:false
+}
+

该 API 与选项式 API 中的 watch 基本等效,watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下 +是惰性的——即回调仅在侦听源发生变化时被调用。

与 watchEffect 比较,watch 允许我们:

  • 懒执行副作用;
  • 更具体地说明什么状态应该触发侦听器重新运行;
  • 访问侦听状态变化前后的值。

# 侦听单一源

watch 可以侦听一个具有返回值的 getter,也可以直接是一个 ref

// 侦听一个 getter
+const state = reactive({ count: 0 })
+watch(
+  () => state.count,
+  (count, prevCount) => {
+    /* ... */
+  }
+)
+
+// 直接侦听ref
+const count = ref(0)
+watch(count, (count, prevCount) => {
+  /* ... */
+})
+

# 侦听多个源

还可以使用数组的形式同时侦听多个数据源:

import { watch } from '@mpxjs/core'
+
+watch([aRef, bRef], ([a, b], [prevA, prevB]) => {
+    /* ... */
+})
+

# 与 watchEffect 相同的行为

watchwatchEffect 在手动停止侦听、清除副作用、副作用刷新时机方面有相同的行为。

import { watch } from '@mpxjs/core'
+
+let unwatch = watch(() => {
+  return a.value + b.value
+}, (newVal, oldVal) => {
+  // 做点什么
+})
+
+// 调用返回值unwatch可以取消观察
+unwatch()
+

# watch 选项

  • 选项:deep

    为了发现对象内部值的变化,可以在选项参数中指定 deep: true。

    import {watch} from '@mpxjs/core'
    +
    +watch(() => {
    +  return this.someObject
    +}, () => {
    +  // 回调函数
    +}), {
    +  deep: true
    +})
    +this.someObject.nestedValue = 123
    +// callback is fired
    +
  • 选项:once

    在选项参数中指定 once: true 该回调方法只会执行一次,后续的改变将不会触发回调;
    +该参数也可以是函数,若函数返回值为 true 时,则后续的改变将不会触发回调

    import {watch} from '@mpxjs/core'
    +
    +watch(() => {
    +  return this.a
    +}, () => {
    +  // 该回调函数只会执行一次
    +}, {
    +  once: true
    +})
    +
    +// 当 once 是函数时
    +watch(() => {
    +  return this.a
    + }, (val, newVal) => {
    +  // 当 val 等于2时,this.a 的后续改变将不会被监听
    + }, {
    +  once: (val, oldVal) => {
    +    if (val == 2) {
    +      return true
    +    }
    +  }
    +})
    +
  • 选项:immediate

    在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调。

    import {watch} from '@mpxjs/core'
    +
    +watch(() => {
    +  return this.a
    +}, () => {
    +  // 回调函数
    +}), {
    +  immediate: true
    +})
    +// 立即以 `this.a` 的当前值触发回调
    +

    注意在带有 immediate 选项时,你不能在第一次回调时取消侦听。

    import {watch} from '@mpxjs/core'
    +
    +var unwatch = watch(() => {
    +  return this.a
    +}, () => {
    +  unwatch() // 这会导致报错!
    +}), {
    +  immediate: true
    +})
    +
    +

    如果你仍然希望在回调内部调用取消侦听的函数,你应该先检查其可用性。

    import {watch} from '@mpxjs/core'
    +
    +var unwatch = watch(() => {
    +  return this.a
    +}, () => {
    +  if (unwatch) { // 请先检查其可用性!
    +    unwatch()
    +  }
    +}), {
    +  immediate: true
    +})
    +
    +

# Effect 作用域 API

# effectScope

function effectScope(detached?: boolean): EffectScope
+
+interface EffectScope {
+    run<T>(fn: () => T): T | undefined
+    stop(): void
+    pause(): void
+    resume(): void
+}
+

创建一个 effect 作用域对象,捕获在其内部创建的响应性副作用(例如计算属性或监听器),可以对这些副作用进行批量处理

import { effectScope } from '@mpxjs/core'
+
+const scope = effectScope()
+scope.run(() => {
+    const doubled = computed(() => counter.value * 2)
+    
+    watch(doubled, () => console.log(doubled.value))
+    
+    watchEffect(() => console.log('Count: ', doubled.value))
+})
+
+scope.stop()
+

需要注意的是,effectScope 接受一个 detached 参数,默认为false,该参数来表示当前作用域是否和父级作用域进行分离,若 detached 为 true, +当前 effectScope 则不会被父级作用域收集。

  • 暂停侦听

Mpx 提供了 pause 方法可以将整个作用域中的响应性副作用批量暂停侦听。

import { effectScope, onHide } from '@mpxjs/core'
+const scope = effectScope()
+scope.run(() => {
+    const doubled = computed(() => counter.value * 2)
+
+    watch(doubled, () => console.log(doubled.value))
+
+    watchEffect(() => console.log('Count: ', doubled.value))
+})
+
+onHide(() => {
+    scope.pause()
+})
+
  • 恢复侦听

被暂停的作用域可以使用 resume 方法来恢复侦听。

import {onShow} from '@mpxjs/core'
+// ......
+
+onShow(() => {
+    scope.resume()
+})
+

# getCurrentScope

function getCurrentScope(): EffectScope | undefined
+

返回当前活跃的 effect 作用域。

# onScopeDispose

function onScopeDispose(fn: () => void): void
+

在当前活跃的 effect 作用域上注册一个处理回调。该回调会在相关的 effect 作用域结束之后被调用。

# Refs

# ref

interface Ref<T> {
+    value: T
+}
+function ref<T>(value: T): Ref<T>
+

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的 property .value。

const count = ref(0)
+console.log(count.value) // 0
+
+count.value++
+console.log(count.value) // 1
+

注意事项:

1.所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

2.如果将一个对象赋值给 ref,那么这个对象将被 reactive 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

import { ref } from '@mpxjs/core'
+const foo = ref(0)
+const state = ref({
+    count: 1,
+    foo
+})
+// 获取count
+console.log(state.value.count)
+// 获取foo
+console.log(state.value.foo)
+

# unref

如果参数是一个 ref,则返回内部值,否则返回参数本身,是 val = isRef(ref) ? ref.value : ref 的语法糖函数。

import { ref, unref } from '@mpxjs/core'
+const count = ref(0)
+const foo = unref(count)
+
+console.log(foo === 0) // -> true
+

# toRef

用于为响应式对象上的property 创建 ref。创建的 ref 与其源 property 保持同步:改变源 property +将更新 ref,改变 ref 也将更新 property。

const state = reactive({
+    f: 1,
+    b: 2
+})
+
+const stateRef = ref({
+    black: 1,
+    white: 2
+})
+
+const fooRef = toRef(state, 'f')
+const blackRef = toRef(stateRef.value, 'black') 
+
+// 更改ref的值更新属性
+fooRef.value++
+blackRef.value++
+console.log(state.f) // 2
+console.log(stateRef.value.black) // 2
+
+// 更改property的值更新ref
+state.f++
+stateRef.value.black++
+console.log(fooRef.value) // 3
+console.log(blackRef.value) // 3
+

注意:

即使源 property 当前不存在,toRef() 也会返回一个可用的 ref,而 toRefs 则不会,这在处理可选properties的时候 +非常有用

# toRefs

将一个响应式对象转换为一个普通对象,这个普通对象的每个 property 都是指向源对象相应 property 的 ref。每个单独的 ref 都是 toRef 创建的

const state = reactive({
+    black: 1,
+    white: 2
+})
+
+const stateAsRefs = toRefs(state)
+
+state.black++
+console.log(stateAsRefs.black.value) // 2
+
+stateAsRefs.black.value++
+console.log(state.black) // 3
+

toRefs 在使用组合式函数中的响应式对象时有很大作用,使用它,对响应式对象进行解构将不会失去响应性

function useFeatX() {
+    const state = reactive({
+        black: 1,
+        white: 2
+    })
+    
+    // 在返回时都转为 ref
+    return toRefs(state)
+}
+
+// 解构而不会失去响应性
+const {black, white} = useFeatX()
+

注意:

  • toRefs 在调用时只会为源对象上可以列举出的 property 创建 ref。如果要为可能还不存在的 property +创建 ref,请改用 toRef

# isRef

检查某个值是否为 ref,返回true/false。

# customRef

function customRef<T>(factory: CustomRefFactory<T>): Ref<T>
+
+type CustomRefFactory<T> = (
+    track: () => void,
+    trigger: () => void
+) => {
+    get: () => T,
+    set: (value: T) => void
+}
+

创建一个自定义 ref,可对其进行依赖项跟踪和更新触发显示控制。需要一个工厂函数,该函数接收 tracktrigger +做为参数,并且应该返回一个带有 getset 的对象。

同时,track() 应该在 get() 方法中调用,trigger() 应该在 set() 中调用。不过这里具体何时调用、 +是否调用都将由用户自己来控制

示例:创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:

function useDebouncedRef(value, delay = 200) {
+  let timeout
+  return customRef((track, trigger) => {
+    return {
+      get() {
+        track()
+        return value
+      },
+      set(newValue) {
+        clearTimeout(timeout)
+        timeout = setTimeout(() => {
+          value = newValue
+          trigger()
+        }, delay)
+      }
+    }
+  })
+}
+
+// text 的每次改变都只在最近一次set后的200ms后调用
+const text = useDebouncedRef('hello')
+

# shallowRef

ref() 的浅层作用形式,创建一个仅跟踪自身 .value 变化的 ref,其他值不做任何处理都为非响应式

const state = shallowRef({
+    count: 1
+})
+
+// 不会触发更改
+state.value.count++
+
+// 会触发更改
+state.value = {
+    black: 1
+}
+

注意: +shallowRef 的内部值将会被原样存储和暴露,不会被深层递归转为响应性, 只有对 .value 的访问是响应式的

常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

# triggerRef

强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。

const shallow = shallowRef({
+  greet: 'Hello, world'
+})
+
+// 触发该副作用第一次应该会打印 "Hello, world"
+watchEffect(() => {
+  console.log(shallow.value.greet)
+})
+
+// 这次变更不应触发副作用,因为这个 ref 是浅层的
+shallow.value.greet = 'Hello, universe'
+
+// 手动执行该 shallowRef 关联的副作用,打印 "Hello, universe"
+triggerRef(shallow)
+
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/api/store-api.html b/docs-vuepress/.vuepress/dist/api/store-api.html new file mode 100644 index 0000000000..0a22caf8e0 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/api/store-api.html @@ -0,0 +1,229 @@ + + + + + + Store API | Mpx框架 + + + + + + + + + +

# Store API

注意: 以下 API 在 2.8 版本后无法通过全局应用实例 mpx 访问。若项目中有类似 mpx.createStore 的用法,在升级到 2.8 版本后请进行修改。

# createStore

function createStore(options: Object): Store
+

创建一个全局状态管理容器,实现复杂场景下的组件通信需求

  • options

    options 可指定以下属性:

    • state: Object

      store的根 state 对象。

      详细介绍

    • mutations: { [type: string]: Function }

      在 store 上注册 mutation,处理函数总是接受 state 作为第一个参数(如果定义在模块中,则为模块的局部状态),payload 作为第二个参数(可选)。

      详细介绍

    • actions: { [type: string]: Function }

      在 store 上注册 action。处理函数总是接受 context 作为第一个参数,payload 作为第二个参数(可选)。

      context 对象包含以下属性: +js { state, // 等同于 `store.state` commit, // 等同于 `store.commit` dispatch, // 等同于 `store.dispatch` getters // 等同于 `store.getters` } +同时如果有第二个参数 payload 的话也能够接收。

      详细介绍

    • getters{[key: string]: Function }

      在 store 上注册 getter,getter 方法接受以下参数:

      {
      +  state,     // 如果在模块中定义则为模块的局部状态
      +  getters   // 等同于 store.getters
      +}
      +

      注册的 getter 暴露为 store.getters。

      详细介绍

    • modulesObject

      包含了子模块的对象,会被合并到 store,大概长这样:

      {
      +  key: {
      +    state,
      +    mutations,
      +    actions?,
      +    getters?,
      +    modules?
      +    },
      +    // ...
      +}
      +

      与根模块的选项一样,每个模块也包含 state 和 mutations 选项。模块的状态使用 key 关联到 store 的根状态。模块的 mutation 和 getter 只会接收 module 的局部状态作为第一个参数,而不是根状态,并且模块 action 的 context.state 同样指向局部状态。

      详细介绍

    • depsObject

      包含了当前store依赖的第三方store:

      {
      +  store1: storeA,
      +  store2: storeB
      +}
      +

      详细介绍

import {createStore} from '@mpxjs/core'
+const store1 = createStore({
+  state: {
+    count: 0
+  },
+  mutations: {
+    increment (state) {
+      state.count++
+    }
+  },
+  actions: {
+    increment (context) {
+      context.commit('increment')
+    }
+  },
+  ...
+})
+const store2 = createStore({ ...options })
+

# Store 实例属性

  • stateObject +根状态。
  • gettersObject +暴露出注册的 getter。

# Store 实例方法

  • commit
commit(type: string, payload?: any, options?: Object) | commit(mutation: Object, options?: Object)
+

提交 mutation。详细介绍

  • dispatch
dispatch(type: string, payload?: any, options?: Object) | dispatch(action: Object, options?: Object)
+

分发 action。返回一个Promise。详细介绍

  • mapState
mapState(map: Array<string> | Object): Object
+

为组件创建计算属性以返回 store 中的状态。详细介绍

  • mapGetters
mapGetters(map: Array<string> | Object): Object
+

为组件创建计算属性以返回 getter 的返回值。详细介绍

  • mapActions
mapActions(map: Array<string> | Object): Object
+

创建组件方法分发 action。详细介绍

  • mapMutations
mapMutations(map: Array<string> | Object): Object
+

创建组件方法提交 mutation。详细介绍

  • mapStateToRefs
mapStateToRefs(maps: Array<string> | Object): {
+    [key: string]: ComputedRef<any>
+}
+

组合式 API 特有,在组合式 API 场景下解构访问 getter 并保持 getter 响应性,可以使用该方法。详细介绍

  • mapGettersToRefs
mapGettersToRefs(maps: Array<string> | Object): {
+    [key: string]: ComputedRef<any>
+}
+

组合式 API 特有,在组合式 API 场景下需解构访问 state 并保持 state 响应性,可以使用该方法。详细介绍

# createStoreWithThis

function createStoreWithThis(store: Store): Store
+

createStoreWithThis 为 createStore 的变种方法,主要为了在 Typescript 环境中,可以更好地支持 store 中的类型推导。
+其主要变化在于定义 getters, mutations 和 actions 时, +自身的 state,getters 等属性不再通过参数传入,而是会挂载到函数的执行上下文 this 当中,通过 this.state 或 this.getters 的方式进行访问。 +由于TS的能力限制,getters/mutations/actions 只有使用对象字面量的方式直接传入 createStoreWithThis 时 +才能正确推导出 this 的类型,当需要将 getters/mutations/actions 拆解为对象编写时,需要用户显式地声明 this 类型,无法直接推导得出。


+import {createComponent, getMixin, createStoreWithThis} from '@mpxjs/core'
+
+const store = createStoreWithThis({
+  state: {
+    aa: 1,
+    bb: 2
+  },
+  getters: {
+    cc() {
+      return this.state.aa + this.state.bb
+    }
+  },
+  actions: {
+    doSth3() {
+      console.log(this.getters.cc)
+      return false
+    }
+  }
+})
+
+createComponent({
+  data: {
+    a: 1,
+    b: '2'
+  },
+  computed: {
+    c() {
+      return this.b
+    },
+    d() {
+      // data, mixin, computed中定义的数据能够被推导到this中
+      return this.a + this.aaa + this.c
+    },
+    // 从store上map过来的计算属性或者方法同样能够被推导到this中
+    ...store.mapState(['aa'])
+  },
+  mixins: [
+    // 使用mixins,需要对每一个mixin子项进行getMixin辅助函数包裹,支持嵌套mixin
+    getMixin({
+      computed: {
+        aaa() {
+          return 123
+        }
+      },
+      methods: {
+        doSth() {
+          console.log(this.aaa)
+          return false
+        }
+      }
+    })
+  ],
+  methods: {
+    doSth2() {
+      this.a++
+      console.log(this.d)
+      console.log(this.aa)
+      this.doSth3()
+    },
+    ...store.mapActions(['doSth3'])
+  }
+})
+

# createStateWithThis

function createStateWithThis(state: Object): Object
+

createStateWithThis 为创建 state 提供了类型推导,对于基本类型可以由 TypeScript 自行推导,使用其他类型时,推荐使用 as 进行约束

import { createStateWithThis } from '@mpxjs/core'
+
+export type StatusType = 'start' | 'running' | 'stop'
+
+export default createStateWithThis({
+  status: 'running' as StatusType
+})
+

# createGettersWithThis

function createGettersWithThis(getters: Object, options: Object):Object
+
  • getters

    需要定义的 getters 对象。

  • options(可选参数)

    在 options 中可以传入 state,getters,deps。由于 getter 的类型推论需要基于 state,所以导出 getters 时,需要将 state 进行传入。deps 是作为一个扩展存在,getters 可以通过 deps 中传入的其他 store 来获取值,当 store 没有其他需要依赖的 deps 时可以不传。createMutationsWithThis 和 createActionsWithThis 同理。

import { createGettersWithThis, createStoreWithThis } from '@mpxjs/core'
+
+export default createGettersWithThis({
+  isStart () {
+    return this.state.status === 'start'
+  },
+  getNum () {
+    return this.state.base.test + this.getters.base.getTest
+  }
+}, {
+  state,
+  deps: {
+    base: createStoreWithThis({
+      state: {
+        testNum: 0
+      },
+      getters: {
+        getTest () {
+          return this.state.testNum * 2
+        }
+      }
+    })
+}})
+

# createMutationsWithThis

function createMutationsWithThis(mutations: Object, options: Object): Object
+
  • mutations

    需要定义的 mutations 对象。

  • options(可选参数)

    在 options 中可以传入 state,deps。

import { createMutationsWithThis } from '@mpxjs/core'
+
+export default createMutationsWithThis({
+  setCurrentStatus (payload: StatusType) {
+    this.state.status = payload
+  }
+}, { state })
+

# createActionsWithThis

function createActionsWithThis(actions: Object, options: Object): Object
+
  • actions

    需要定义的 actions 对象。

  • options(可选参数)

    由于action 可以同时调用 getters、mutations,所以需要将这些都传入,以便进行类型推导。因此 options 可以传入 state、getters、mutations、deps。

import { createActionsWithThis } from '@mpxjs/core'
+import state, { StatusType } from './state'
+import getters from './getters'
+import mutations from './mutations'
+
+export default createActionsWithThis({
+  testActions (payload: StatusType) {
+    return Promise.resolve(() => {
+      this.commit('setCurrentStatus', payload)
+    })
+  }
+}, {
+  state,
+  getters,
+  mutations
+})
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/1.0.html b/docs-vuepress/.vuepress/dist/articles/1.0.html new file mode 100644 index 0000000000..b344feb20d --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/1.0.html @@ -0,0 +1,54 @@ + + + + + + 滴滴开源小程序框架Mpx | Mpx框架 + + + + + + + + + +

# 滴滴开源小程序框架Mpx

作者:hiyuki (opens new window)

Mpx是一款致力于提高小程序开发体验的增强型小程序框架,通过Mpx,我们能够以最先进的web开发体验(Vue + Webpack)来开发生产性能深度优化的小程序,Mpx具有以下一些优秀特性:

  • 数据响应特性(watch/computed)
  • 增强的模板语法(动态组件/样式绑定/类名绑定/内联事件函数/双向绑定等)
  • 深度性能优化(原生自定义组件/基于依赖收集和数据变化的setData)
  • Webpack编译(npm/循环依赖/Babel/ESLint/css预编译/代码优化等)
  • 单文件组件开发
  • 状态管理(Vuex规范/多实例/可合并)
  • 跨团队合作(packages)
  • 逻辑复用能力(mixins)
  • 脚手架支持
  • 小程序自身规范的完全支持
  • 支付宝小程序的支持

# 设计思路

目前业界主流的小程序框架主要有WePY,mpvue和Taro,这三者都是将其他的语法规范转译为小程序语法规范,我们称其为转译型框架。不同于上述三者,Mpx是一款基于小程序语法规范的增强型框架,我们使用Vue中优秀的语法特性增强了小程序,而不是让用户直接使用vue语法来开发小程序,之所以采用这种设计主要是基于如下考虑:

  • 转译型框架无法支持源框架的所有语法特性(如Vue模板中的动态特性或React中动态生成的jsx),用户在使用源框架语法进行开发时可能会遇到不可预期的错误,具有不确定性
  • 小程序本身的技术规范在不断地更新进步,许多新的技术规范在转译型框架中无法支持或需要很高的支持成本,而对于增强型框架来说只要新的技术规范不与增强特性冲突,就能够直接支持

# 技术实现

小程序刚推出时不支持自定义组件,无法进行组件化开发,当时最早的小程序框架WePY做的核心工作就是支持了小程序的组件化开发,后来的mpvue也基于Vue做了类似的工作。由于当时的小程序本身不支持自定义组件,WePY和mpvue的组件化支持都是基于编译做的组件化封装,用户编写的组件模板会被编译为Page中模板的一部分,在组件中定义的数据会被编译为Page中的数据,对组件进行数据更新也会基于路径映射调用Page.setData。这个方案在当时的技术条件下确实是一个唯一解,但该方案在复杂的小程序应用中会存在明显性能问题,原因在于该方案中页面的数据量会很大,而且每个组件的局部更新实际上都会成为页面级别的全局更新,在组件较多的页面中容易引发性能问题。在小程序自定义组件推出后,我们通过性能测试确认了该方案解决了上述组件封装方案中的性能问题。由于自定义组件在当时是一个最新的技术标准,业内的小程序框架都未支持,而我们本身的业务又有复杂小程序的开发需求,于是我们就基于小程序自定义组件规范启动了Mpx框架的设计与开发

Page与Component setData性能对照

Component Page
局部更新 1975ms 7186ms
全局更新 6210ms 24612ms

性能衡量标准说明: +局部更新:文档中存在1000个节点,其中一个节点更新1000次的耗时; +全局更新:文档中存在5个节点,5个节点独立更新1000次的耗时 +以上数据在iphone7微信小程序环境下测试得出

# 数据响应与性能优化

数据响应作为Vue最核心的特性,在我们的日常开发中被大量使用,能够极大地提高前端开发体验和效率,我们在框架设计初期最早考虑的就是如何将数据响应特性加入到小程序开发中。在数据响应的实现上,我们引入了MobX,一个实现了纯粹数据响应能力的知名开源项目。借助MobX和mixins,我们在小程序组件创建初期建立了一个响应式数据管理系统,该系统观察着小程序组件中的所有数据(data/props/computed)并基于数据的变更驱动视图的渲染(setData)及用户注册的watch回调,实现了Vue中的数据响应编程体验。与此同时,我们基于MobX封装实现了一个Vuex规范的数据管理store,能够方便地注入组件进行全局数据管理。为了提高跨团队开发的体验,我们对store添加了多实例可合并的特性,不同团队维护自己的store,在需要时能够合并他人或者公共的store生成新的store实例,我们认为这是一种比Vuex中modules更加灵活便捷的跨团队数据管理模式

作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个:

  1. 尽可能减少setData调用的频次
  2. 尽可能减少单次setData传输的数据

为了实现以上两个优化方向,我们做了以下几项工作:

  • 将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次;
  • 将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。 +基于以上优化,我们大大减少了小程序setData的调用频次和传递数据量,与初版Mpx中track全量数据的方案相比提升显著。

Mpx setData优化效果

旧版mpx 新版mpx
setData次数 164 88
setData数据量 370kB 38kB

以上数据由较复杂小程序在固定交互流程后统计得出

Mpx数据响应机制流程示意图 Mpx数据响应机制流程示意图

# 编译构建

我们希望使用目前设计最强大、生态最完善的编译构建工具Webpack来实现小程序的编译构建,让用户得到web开发中先进强大的工程化开发体验。使用过Webpack的同学都知道,通常来说Webpack都是将项目中使用到的一系列碎片化模块打包为一个或几个bundle,而小程序所需要的文件结构是非常离散化的,如何调解这两者的矛盾成为了我们最大的难题。一种非常直观简单的思路在于遍历整个src目录,将其中的每一个.mpx文件都作为一个entry加入到Webpack中进行处理,这样做的问题主要有两个:

  1. src目录中用不到的.mpx文件也会被编译输出,最终也会被小程序打包进项目包中,无意义地增加了包体积;
  2. 对于node_modules下的.mpx文件,我们不认为遍历node_modules是一个好的选择。

最终我们采用了一种基于依赖分析和动态添加entry的方式来进行实现,用户在Webpack配置中只需要配置一个入口文件app.mpx,loader在解析到json时会解析json中pages域和usingComponents域中声明的路径,通过动态添加entry的方式将这些文件添加到Webpack的构建系统当中(注意这里是添加entry而不是添加依赖,因为只有entry能生成独立的文件,满足小程序的离散化文件结构),并递归执行这个过程,直到整个项目中所有用到的.mpx文件都加入进来,在输出前,我们借助了CommonsChunkPlugin/SplitChunksPlugin的能力将复用的模块抽取到一个外部的bundle中,确保最终生成的包中不包含重复模块。我们提供了一个Webpack插件和一个.mpx文件对应的loader来实现上述操作,用户只需要将其添加到Webpack配置中就可以以打包web项目的方式正常打包小程序,没有任何的前置和后置操作,支持Webpack本身的完整生态。

Mpx编译构建机制流程示意图 Mpx编译构建机制流程示意图

# 现状和未来

目前Mpx框架已经在滴滴内部大量使用,支撑了滴滴出行、青桔单车、街兔电单车、营销、车服等业务在小程序上的实现,线上运行稳定,收到了大量的好评反馈。未来我们在对框架进行持续迭代优化的同时会持续跟进微信和支付宝最新的技术标准,同时也会将在更多的小程序平台上进行适配。由于我们的设计初衷和专注点在于增强小程序开发体验,Mpx并没有进行跨H5甚至是跨Native的支持,但现实业务当中确实存在这样的诉求,未来我们会在Mpx的基础上对跨端进行一定的尝试,与此同时我们依然会持续维护升级Mpx,原因在于跨端意味着灵活性受限及能力的缺失,我们希望能给用户提供第二种选择。

Mpx与业内主流小程序框架异同对比

WePY mpvue Taro Mpx
代码规范 自定义 Vue React 小程序+
组件化实现 Page封装 Page封装 Component Component
数据响应 脏值检查 Vue Mobx
状态管理 Redux Vuex Redux 类Vuex

# 结语

如果你注重开发体验和产品性能,专注于小程序开发,那Mpx会是一个很好的选择。最后感谢开源社区源源不断涌现出的优秀项目,给我们提供了无限的启发和巨大的技术帮助。

Github: https://github.com/didi/mpx (opens new window) +使用文档: https://mpxjs.cn/ (opens new window) +用户交流群: +微信

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/2.0.html b/docs-vuepress/.vuepress/dist/articles/2.0.html new file mode 100644 index 0000000000..9482e8fab6 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/2.0.html @@ -0,0 +1,58 @@ + + + + + + 滴滴小程序框架Mpx发布2.0,支持小程序跨平台开发,可直接转换已有微信小程序 | Mpx框架 + + + + + + + + + +

# 滴滴小程序框架Mpx发布2.0,支持小程序跨平台开发,可直接转换已有微信小程序

作者:hiyuki (opens new window)

Mpx是一款致力于提高小程序开发体验和效率的增强型小程序框架,目前在滴滴公司内部支撑了包括滴滴出行小程序,滴滴出行广场小程序,青桔单车,黑马电单车,小桔养车,小桔加油在内的小程序生态;自去年11月开源以来,Mpx也吸纳了众多外部开发者的加入,基于Mpx开发了开走吧,好免街,花忆等小程序。

长期以来,Mpx优秀的开发体验和强大的稳定性得到了内外开发者的一致认可和好评,这非常符合Mpx的设计初衷。但是在各大厂商陆续推出自己的小程序平台,且各家的技术标准都不统一的今天,单纯地提高某一个平台的开发体验已经不能满足广大小程序开发者们的诉求,一套代码在多小程序平台运行已经成为一个现实上的刚需。为了解决这个小程序开发的痛点,Mpx发布了2.0版本,适配了目前业内已经发布的所有小程序平台(微信、支付宝、百度、头条、qq),并且提供了直接将现有微信小程序编译输出到其他平台运行的能力。

Mpx2.0版本新增的主要特性主要包含:

  • 完整支持了目前业内已发布的所有小程序平台(微信,支付宝,百度,qq,头条);
  • Mpx小程序跨平台开发,支持将已有的Mpx微信项目编译输出到其他已支持的小程序平台中运行,点击查看详情
  • 小程序原生组件跨平台编译,支持将已有的微信原生组件编译输出到其他已支持的小程序平台中运行;
  • 深度分包优化,编译过程中进行精准分包资源判断,所有分包only的资源(组件、js、外部样式、外部模板、wxs,图像媒体等)都会精确输出到分包目录中;
  • render函数中完整支持wxs模块,关于render函数点击查看详情
  • 支持了模板引入,内联wxs,自定义tabbar,独立分包,workers,云开发等原生能力,进一步完善原生兼容性。

同业内主流的小程序跨端框架相比,Mpx更专注于小程序开发本身,在小程序开发中具备以下优势:

  • 基于小程序自身的技术标准进行增强,没有进行过重的DSL转换,开发时遇到的坑会更少;
  • 完全兼容原生小程序技术规范,0成本迁移原生小程序项目;
  • 跨平台开发以跨小程序平台为目标,大部分差异抹平工作在编译阶段进行,大大减少运行时适配层增加的包体积;
  • 支持业内微信小程序组件库(如vant、iView等)直接转换到其他小程序平台运行;
  • 非常重视小程序性能,提供了深度的setData和包体积优化。

关于Mpx更详细的介绍可以查看官方文档 (opens new window)这篇文章

Github:https://github.com/didi/mpx (opens new window)

# 跨平台开发

作为2.0版本的核心能力,Mpx的跨平台开发能力允许用户直接将已有小程序项目编译输出到其他已支持的小程序平台中运行。微信小程序作为小程序概念的提出者,有着最广泛的生态覆盖,因此我们优先支持了将微信小程序编译为其他平台小程序的能力。基于这个能力,用户不仅能跨平台编译微信Mpx项目,甚至能够将微信的原生自定义组件也编译到其他小程序平台进行运行,这意味着我们的跨平台项目能够直接使用一些社区内已有的UI组件库生态(如vant、iView等),极大地提高了跨平台开发的适用范围。

# 设计理念

Mpx框架的核心设计理念在于增强,增强是指在小程序已有的原生能力基础上做加法,拓展小程序的开发能力,提高小程序的开发体验和效率。这个设计理念使Mpx给开发者带来了更强的确定性和可预期性,更低的学习上手和调试成本。基于这个理念,Mpx在不同的小程序平台中进行了差异性的增强适配,并参考各个平台的模板指令风格提供了不同的增强模板指令集,让用户在各小程序平台中都可以以增强的方式去最大限度地使用平台自有的原生能力。

我们在对Mpx提供跨平台能力的支持时也遵循了增强的核心设计理念。简单来讲,Mpx的跨平台能力是在多平台能力的基础上,在编译和运行时增加了一层转换层,将源平台的代码转换为目标平台的代码之后,再按照既有的目标平台的处理逻辑进行增强,同时我们也提供了一套完善的条件编译机制,让用户自行实现少数框架无法转换的部分。

Mpx跨平台开发流程示意图

Mpx跨平台开发流程示意图

Mpx跨平台能力设计思路明显区别于业内已有的其他小程序跨平台框架,主要差异在于:

  • Mpx以小程序本身的DSL作为基准,而没有使用web框架(React,Vue)的DSL;
  • Mpx主要通过编译和运行时转换的方式处理平台差异,没有提供额外的差异抹平层(基础组件库等)。

之所以采用这种设计,主要基于以下原因:

  • Mpx主要以跨小程序平台为目标,目前各大小程序平台的技术规范具有一定相似性,绝大部分平台差异能够通过编译和运行时手段抹平,同时省去的差异抹平层也能够进一步减少框架运行时体积;
  • 使用小程序本身的DSL作为基准允许用户直接在已有项目中使用跨平台能力,对于原生小程序项目或组件也能够使用该能力进行跨平台输出;
  • 结合完善的条件编译支持,该方案能够在满足用户跨平台需求的同时仍然允许用户最大限度地使用各个小程序平台提供的能力,完全延续了Mpx增强的核心设计理念。

# 使用方法

Mpx跨平台开发的使用方式非常简单,用户只需在MpxWebpackPlugin创建时传入mode和srcMode参数指定源平台和目标平台,当srcMode和mode不一致时,框架会读取相应的配置对项目进行编译和运行时转换。

// 微信转支付宝
+new MpxWebpackPlugin({
+  // mode指定目标平台,可选值有(wx|ali|swan|qq|tt)
+  mode: 'ali',
+  // srcMode指定源码平台,默认值同目标平台一致
+  srcMode: 'wx'
+})
+

# 差异抹平

目前各大厂商的小程序技术规范在宏观层面上大致保持一致,但是技术细节方面存在很多差异,大致划分为以下几个部分:

  • 模板语法/基础组件差异
  • json配置差异
  • wxs语法差异
  • 页面/组件对象差异
  • api调用差异
  • webview bridge差异

其中,对于模板语法/基础组件、json配置和wxs中的静态差异,我们主要通过编译手段进行转换处理,对于这部分差异中无法转换的部分会在编译阶段报错指出;而对于页面/组件对象、api调用和webview bridge中js运行时的差异,我们主要通过运行时手段进行处理,对应的无法转换部分也会在运行时中报错指出。

值得注意的是,我们在跨平台转换中做的工作不仅是对可转换的技术标准进行转换映射,对于一些目标平台中不存在的能力,我们也尽可能地通过编译和运行时手段提供了模拟和支持,最大限度地减少用户在跨平台开发中需要付出的额外工作量。以差异性最大但现实场景也最多的微信转支付宝为例,Mpx模拟提供了许多微信中支持但支付宝中未支持的能力:

  • 组件自定义事件
  • 组件间关系
  • 获取子组件实例
  • observers/property observer
  • 内联wxs
  • 外部样式类

对于原生自定义组件的跨平台转换,我们会对其进行简单的运行时注入,使其能够使用Mpx框架提供的运行时转换能力。

# 条件编译

对于框架无法抹平的差异部分,会在编译和运行时报错指出,对于这部分错误,我们提供了完善的条件编译机制让用户能够自行编写目标平台的patch进行修复,该能力也能用于实现具有平台差异性的业务逻辑。

上文中提到Mpx通过读取用户传入的mode和srcMode来决定是否以及如何对项目进行转换,mode和srcMode分别代表整个项目构建的目标平台和源平台,条件编译能够让用户在项目中创建声明了自身平台属性(localSrcMode)的文件和代码块。在项目构建中,框架会优先加载带有localSrcMode声明且localSrcMode与项目目标平台匹配(localSrcMode===mode)的文件和代码块,这部分文件和代码块需要完全依照自身声明的平台标准进行编写,Mpx不会对其进行任何编译和运行时的跨平台转换。

Mpx提供了三种维度的条件编译,分别是文件维度,区块维度和代码维度,用户可以根据平台差异的覆盖范围灵活选择使用。

# 性能优化

Mpx框架专注于小程序开发,在性能优化方面我们做过很多尝试和努力,主要集中在两个方面:

  • 运行时的setData优化
  • 编译构建时的包体积优化

# setData优化

数据响应是Mpx运行时增强的核心能力,该能力让用户在小程序开发中能够像Vue中一样使用watch和computed特性,并且用直接赋值的方式操作数据驱动视图更新,而不需要手动调用setData方法,换言之框架接管了小程序中的setData调用。

通过各大小程序平台的设计原理和性能优化建议可以得知,setData对于小程序的性能表现非常重要,而setData优化的两大方向在于:

  • 尽可能减少setData调用的频次
  • 尽可能减少单次setData传输的数据

为了实现setData的优化,我们在模板编译过程中对于每个组件的模板都生成了一个渲染函数(render function),该函数模拟模板的渲染逻辑,在每次执行时访问当次渲染所需的数据,并将当次访问过的数据路径记录下来作为函数返回值返回。

在运行时,框架会在每个组件创建时创建一个render watcher,该watcher追踪渲染函数,当渲染依赖数据发生变更时异步执行渲染函数,在render watcher回调中得到渲染函数返回的数据路径,基于这些路径与上一次的缓存数据进行diff比对,过滤掉未发生变化的数据后得到最小必要数据,最后调用setData将最小必要数据发送到真实的小程序渲染层更新视图。

基于这个机制,当数据发生变更时,只有当前渲染依赖的那部分数据发生变更才会异步地触发render watcher的执行,而每次执行后也只有实际发生变更的那部分数据会被setData发送到渲染层。这样用户就能自由地根据业务需求来操作数据,无需关注setData的调用优化,框架能够自动进行程序上最优的setData调用,在提升用户开发体验的同时也提升了程序性能。

在1.x版本中,渲染函数内无法执行wxs的逻辑,对于含有wxs的组件有可能降级到全量设置数据的模式,在2.0版本中,我们将wxs模块转译处理为js可执行的代码后注入到js bundle中,含有wxs的渲染函数也能够正常访问并执行wxs逻辑。

setData优化示意图

setData优化示意图

# 包体积优化

类似于运行时对于setData的接管,Mpx在编译阶段接管了项目的资源管理。得益于webpack强大的插件机制,Mpx开发了一个深度定制的webpack插件,基于webpack完成小程序的打包构建工作。用户在使用Mpx开发小程序时可以不受限制地使用npm依赖、最新的es特性和css预处理器等现代web开发能力。与此同时,Mpx在包体积优化上也做了很多工作,让用户专注于业务开发而无需花费过多精力进行包体积管理,我们所做的优化工作如下:

  • 打包构建工作完全基于依赖分析,任何没有被引用的资源都不会出现在dist当中;
  • 对于npm组件和页面的构建也完全基于依赖分析按需打包,不会copy整个miniprogram_dist目录,也不需要执行构建npm,使用体验和包体积均优于微信小程序自身的npm支持方案;
  • 基于webpack提供的能力进行公共模块抽取和代码压缩等优化工作;
  • 完善的分包支持,对所有资源进行从属分析,将所有分包only的资源都输出到分包目录中。

分包作为微信小程序中优化包体积的核心手段(类似于异步按需加载),Mpx对其进行了完善的支持。为了精确地标记出分包only的资源,我们在构建时将主包和分包的依赖收集步骤拆分开来串行处理,先处理主包,再处理分包。在主包的处理过程中,将主包页面中引用的所有非js资源(组件、外部样式、外部模板、wxs,图像媒体等)都记录下来,在处理分包时,对分包内引用的非js资源都进行检查,如果被主包引用过则输出到主包中,否则标记为分包only的资源输出到分包目录下。

对于js模块资源,我们在脚手架中生成的构建配置中提供了辅助函数,便于用户进行分包bundle的配置,经过该配置后,分包only的公用模块会被打入分包bundle输出到分包目录下,其余的公共模块会正常打入主bundle中。

在跨平台开发中,我们建议用户使用Mpx提供的packages来定义分包,这样在转换到不支持分包的小程序平台时会自动降级为同步包进行处理。

分包构建示意图

分包构建示意图

# 渐进迁移

Mpx提供了良好的渐进迁移支持,对于使用原生或其他小程序框架的开发者来说,采用渐进迁移的方式逐步引入Mpx进行开发成本并不大。

在2.0版本中我们进一步完善了Mpx的原生兼容性,跟进支持了各个小程序平台最新的技术能力,如自定义tabbar,独立分包,分包预加载,workers,云开发等能力,同时补齐了一些1.x版本遗漏的支持。得益于此,对于使用原生小程序开发的开发者来说,迁移Mpx的成本几乎为0,用户只需将对应页面组件的构造函数替换为Mpx提供的createPage/createCompnent,即可使用Mpx提供的各种增强能力。

对于使用其他框架的开发者,Mpx也提供了局部构建的机制,允许用户将特定的页面和组件单独构建输出为原生组件,用户只需手动或者编写脚本输出的原生组件整合进原有项目中即可。

# 未来规划

作为滴滴公司内部小程序生态的基础设施,我们会对Mpx框架进行长期的维护更新,确保能在第一时间支持各个小程序平台最新的技术特性。与此同时,我们也会进一步完善框架的基础能力,目前已排上日程待支持能力包括:

  • i18n
  • ts支持
  • 单元测试支持

在跨平台能力方面,我们也会根据社区的反馈和建议,以及小程序的标准化进程,对其进行持续的完善与更新。

最后,如果你专注小程序开发,关注开发体验和产品性能,那Mpx会是你最好的选择。

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/2.7-release.html b/docs-vuepress/.vuepress/dist/articles/2.7-release.html new file mode 100644 index 0000000000..f6ff6af0d1 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/2.7-release.html @@ -0,0 +1,63 @@ + + + + + + Mpx2.7 版本正式发布,大幅提升编译构建速度 | Mpx框架 + + + + + + + + + +

# Mpx2.7 版本正式发布,大幅提升编译构建速度

作者:hiyuki (opens new window)

Mpx (opens new window)是一款开源的增强型跨端小程序框架,它具有良好的开发体验,极致的应用性能和一份源码同构输出所有小程序平台及web环境的跨端能力。近期,我们发布了框架最新的2.7版本,基于webpack5彻底重构了框架的核心编译构建流程,利用持久化缓存大幅提升了编译构建速度,最高提升可达10倍。除此之外,Mpx2.7版本还带来了一系列重要的功能更新,包括分包输出能力增强,完善的单元测试支持和用户Rules应用等,下面会对这些新特性进行逐个展开介绍。

# 编译构建提速

随着小程序生态的日渐发展壮大,各类线上小程序业务体量和复杂度的不断升级,小程序的包体积从最开始的2M以内膨胀到20M甚至30M,已经远远不复当初的""程序之名。随着小程序项目大小的不断增加,采用框架进行小程序开发的开发者们往往都会面临一个问题,即框架的编译耗时过长,Mpx也不例外。

以团队负责的小程序项目为例,该项目目前的总包体积已经超过25M,包含近30000个JS模块,400多个页面和3000多个组件,在本地进行一次完整构建需要等待15分钟,CI环境下甚至需要近半个小时,远远超出能够忍受的范围,对小程序开发体验和开发效率造成了极大的影响。虽然旧版本中的watch模式能够在很大程度上缓解我们在开发调试过程中面临的编译耗时问题,但我们在日常开发中,仍然有很多场景无法使用watch模式(首次构建/CI环境/需真机预览等),基于内存缓存的watch模式也无法长期运行。对于这个问题,我们在过去也做了很多技术尝试,如支持watch:prod模式,局部编译,多线程编译,DLL预编译等等,但是整体尝试下来这些方案要么收效有限要么适用面不足,都没能探索到一个能从根本上解决问题的方案。

直到2020年底,webpack5正式发布,基于文件系统持久化缓存能力的出现,让我们看到了该问题的解决之道。由于webpack5相较于webpack4有着非常多的升级改动,而Mpx的编译构建在过去版本中也存在着各种历史遗留问题,我们花了较长的时间吃透webpack5的源码以及重新思考Mpx中存在的不合理设计。经过3个多月的开发,我们彻底重构了Mpx的核心编译构建流程,使其能够完整安全地利用webpack5持久化缓存能力进行构建提速,同时也彻底解决了旧版Mpx中存在的历史遗留问题,在去年10月完成了beta版的开发与发布,并在业务中进行了初步的落地尝试。之后又经过2个多月的业务回归测试和框架功能补全,我们在beta版的基础上又迭代修复了近30个patch版本,在业务上完成了充分的回归测试,让我们确信目前的新版本已经达到了能够release的阶段。

为什么不是Vite

聊起编译提速,可能大部分人首先想到的是Vite,诚然Vite是一个极富创造力的技术方案,但是在小程序场景下,却不一定是最合适的方案,这主要源于Vite最核心的设计利用了现代浏览器原生支持的ESM,而目前没有任何一家的小程序环境能够原生支持ESM,这就使得Vite最核心的按需编译能力无法得到发挥,而Vite使用esbuild带来的编译速度提升,在webpack环境中也可以选择esbuild-loader提供的能力来替换babel/terser,而且目前esbuild提供的编译能力成熟度还远不能和babel/terser相比,再加上Mpx的编译构建流程很大程度上依赖了webpack提供的能力,从成本和收益上考虑采用webpack5对于我们来说无疑是更好的方案。

以团队负责的小程序项目为例,我们来看一下Mpx2.7版本带来的编译提速效果:相较于旧版长达15分钟的构建耗时,在无缓存环境下,新版的构建耗时约为8分钟,速度提升了近1倍;而在有缓存环境下,新版构建耗时可以缩短至80s,速度提升了10倍以上!随着我们对CI流程的持久化缓存改造完成,可以确保在日常的大部分构建场景都会在有缓存的环境下进行。

# 分包输出能力增强

在Mpx2.7版本中,我们对小程序分包能力的支持进行了进一步的完善增强。

# 独立分包初始化模块

在过去的版本中,我们对独立分包进行过专门的构建支持,以满足独立分包资源独占的要求。不过在使用独立分包进行业务开发时,往往会面临的一个棘手问题在于初始化逻辑无处安放。这是由于独立分包没有app.js,而在小程序中,组件的js逻辑会早于页面js执行,具体的执行顺序又和组件的嵌套关系有关,因此我们无法找到一个确定的代码位置来存放独立分包的初始化逻辑。

在Mpx2.7版本中,我们针对独立分包新增了一个全新的增强特性,让用户能够声明独立分包的初始化模块,该模块将会在独立分包启动时全局最先执行,其实现思路大致如下:在构建时为独立分包中所有的组件和页面都添加模块引用,指向用户声明的初始化模块,这样在独立分包启动时,不管哪个组件/页面的js最先执行,都能保障这个初始化模块最先执行,同时由于模块缓存的存在,后续的组件/页面执行时,该模块也不会被重复执行。

该特性的详细使用方式可以参考我们的文档说明 (opens new window)

# 分包异步化

分包异步化 (opens new window)是微信小程序在去年下半年提出的全新技术特性,该特性打破了传统分包只能引用自身和主包资源的规则限制,通过相关配置和声明,允许分包异步地引用其他分包内的资源,对于复杂小程序的包体积和加载性能优化具有极其重要的意义。

在过去,受限于小程序分包资源引用规则,Mpx在编译构建时对于跨分包共用的资源有两种处理策略,其一是将其输出到主包当中,让多个分包都能够通过主包访问,这种策略下可以达到总包体积最优,但是往往会对主包体积造成过大的压力。当主包超出2m限制时,我们就需要采用第二种策略,将这部分跨分包共用的资源冗余地输出到各自的分包中,消除其对于主包体积的占用。在实际的Mpx编译构建当中,这两种策略是同时存在的,具体什么时候采用哪种策略是根据资源类型和用户配置来决定的。

由于分包异步化技术打破了传统分包资源引用规则的限制,理想情况下我们可以做到主包不超限的同时总包无冗余,不过该技术目前也存在一些不足:一是跨平台支持度不佳,只有微信支持,不过据我们了解支付宝目前也在跟进中;二是对交互和体验会带来一些影响,同时存在业务改造成本,但这依然不妨碍该技术成为大型小程序优化包体积和加载性能的最优路径。

分包异步化资源引用

我们在Mpx2.7版本中对分包异步化中最常用的跨分包自定义组件引用进行了完整支持,与原生小程序不同,Mpx中资源的分包归属不由源码位置决定,而是由资源引用关系决定,因此在跨分包资源引用的场景下,用户需要声明引用的资源属于哪个分包,简单使用示例如下:

{
+  "usingComponents": {
+    // 通过root query声明组件所属的分包,与packages语法下使用root query声明package所属分包的语义保持一致
+    "button": "../subPackageA/components/button?root=subPackageA",
+    "list": "../subPackageB/components/full-list?root=subPackageB",
+    "simple-list": "../components/simple-list"
+  },
+  "componentPlaceholder": {
+    "button": "view",
+    "list": "simple-list"
+  }
+}
+

详细使用方式可参考文档说明 (opens new window)

对于另一项跨分包JS代码引用能力的使用支持,我们目前也正在探索规划中,预计能在今年Q1内完成开发支持。

# 单元测试支持

Mpx自从20年开始就对单元测试有了初步的支持,但过去的单测方案在设计上存在一些缺陷,可用性不高,业务落地困难,在Mpx2.7版本中,我们设计了一套全新的技术方案,克服了原有方案中存在的所有问题,在可用性上得到了质的飞跃,新旧方案的对比如下:

旧方案:通过Mpx编译构建预先将完整的项目源码构建输出为原生小程序格式,再通过jest + miniprogram-simulate加载构建产出的原生小程序组件来执行测试case。该方案的优点在于编译流程统一,方案实现成本较低,缺点在于执行任何case都需要执行完整的构建流程,耗时较长;而且由于构建本身不基于jest进行,也无法使用jest提供的模块mock功能。

旧版单测方案

新方案:我们fork了miniprogram-simulate仓库对其扩展了load mpx组件的能力,在资源加载的transform过程中通过mpx-jest插件将mpx组件编译为原生小程序组件,再将内容传递给miniprogram-simulate执行渲染并运行测试case。该方案中模块加载完全基于jest并能够实现组件的按需编译,完美规避旧方案中存在的问题,缺点在于编译流程基于jest api重构,与mpx基于webpack的编译流程独立,存在额外的维护成本,后续我们会将通用的编译逻辑抽离维护,在webpack和jest两侧复用。

新版单测方案

关于单元测试更详细的使用指南可参考文档说明 (opens new window)

随着单测方案的完善,我们今年也会推动单元测试在小程序业务中全面落地,更好地保障代码质量与业务稳定。

# Module.rules复用

Mpx的单文件支持很大程度上参考了vue-loader的设计,在vue-loader@15版本前,对于单文件组件中各个区块(block)的loaders应用逻辑默认内置在loader当中,如需对某些区块进行自定义配置,需要向loader的options中传递额外的loader应用规则,无法复用webpack配置的module.rules中已经定义好的规则,这往往会导致我们需要在loader options和module.rules中维护重复的loader规则,同样的问题也存在于旧版的mpx-loader中。

在vue-loader@15版本发布之后,其通过克隆用户原始的rules的方式实现了module.rules的复用,用户不再需要往loader options中传递冗余的loaders规则,本次全新Mpx2.7版本也支持了该特性,我们使用了webpack提供的matchResource能力实现了module.rules的复用,该方案相较于clone rules的方式实现起来更加简洁优雅。

以上就是Mpx2.7版本的核心更新内容,目前使用脚手架工具创建的项目默认都会使用2.7版本,过往项目如需迁移升级可以查看这篇指南 (opens new window)

# 即将到来的新特性

以上就是我们本次Mpx2.7版本带来的全部特性,后续我们也有计划撰写一系列文章对其中的技术细节进行更加深入的分析与介绍,除了上面介绍到的特性外,我们还有一系列特性处于即将发布的状态:

# 未来规划

未来,我们的工作重心将重新回到运行时,重点致力于composition api编码规范支持和数据响应系统的升级,在保障性能和包体积方面优势的同时,带给开发者用户与时俱进的开发体验。

在跨端方面,我们与移动端跨端框架Hummer (opens new window)进行了合作,初步探索了Mpx基于Hummer输出为移动端原生应用的可行性,目前整体流程已经基本跑通,待后续能力逐步完善和业务充分落地后,就会在开源版本中发布。

与此同时,Mpx-cube-ui小程序跨端组件库也在持续地开发完善中,目前业务落地效果良好,预计会在今年下半年正式开源。

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html b/docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html new file mode 100644 index 0000000000..3d0f341bd0 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html @@ -0,0 +1,174 @@ + + + + + + 通过 Mpx 使用组合式 API 进行小程序开发 | Mpx框架 + + + + + + + + + +

# 通过 Mpx 使用组合式 API 进行小程序开发

滴滴开源小程序跨端框架 Mpx (opens new window) 自18年立项开源以来,如今已经进入第五个年头,期间 Mpx 在有效支持了公司内外小程序业务开发的同时,也因其高性能、优体验、跨平台的特性获得了开发者用户的一致好评。

为了不辜负开发者用户对我们的信赖,更好地支持小程序业务开发,一方面我们对 Mpx 的稳定版本进行着高频的维护迭代,快速响应处理开发者用户在框架使用过程中遇到的问题;另一方面我们持续跟进探索业内最新动态,力求带给用户更好的开发体验与更强大的开发能力。继我们在 2.7 版本 (opens new window)中对 Mpx 的编译系统进行 Webpack5 适配 ,完整支持持久化缓存并大幅提升编译速度后,在 2.8 版本 (opens new window)中,我们对 Mpx 的框架运行时也进行了重构改造,完整支持 Vue3 中的组合式 API 开发范式,让用户能够使用时下最热门的开发方式进行小程序业务开发。

我们先来简单感受一下组合式 API 的使用:

<template>
+  <view>{{ collectionName }}: {{ book.title }}({{ readersNumber }})</view>
+  <button bindtap="addReaders">addReaders</button>
+</template>
+
+<script>
+  import { createComponent, ref, reactive, onMounted } from '@mpxjs/core'
+
+  createComponent({
+    properties: {
+      collectionName: String
+    },
+    setup () {
+      const readersNumber = ref(0)
+      const book = reactive({ title: 'Mpx' })
+
+      onMounted(() => {
+        console.log('Component mounted.')
+      })
+
+      // 暴露给 template
+      return {
+        readersNumber,
+        book,
+        addReaders () {
+          readersNumber.value++
+        }
+      }
+    }
+  })
+</script>
+

可以看出和 Vue3 组合式 API 的使用是高度类似的,利用框架导出的一系列响应式 API 和 生命周期钩子函数在 setup 中编写业务逻辑,并将模板依赖的数据与方法作为返回值返回,与传统的选项式 API 相比,组合式 API 具备以下优势:

  • 更好的逻辑复用,通过函数包装复用逻辑,显式引入调用,方便简洁且符合直觉,规避消除了 mixins 复用中存在的缺陷;
  • 更灵活的代码组织,相比于选项式 API 提前规定了代码的组织方式,组合式 API 在这方面几乎没有做任何限制与规定,更加灵活自由,在功能复杂的庞大组件中,我们能够通过组合式 API 让我们的功能代码更加内聚且有条理,不过这也会对开发者自身的代码规范意识提出更高要求;
  • 更好的类型推导,虽然基于 this 的选项式 API 通过 ThisType 类型体操的方式也能在一定程度上实现 TS 类型推导,但推导和实现成本较高,同时仍然无法完美覆盖一些复杂场景(如嵌套 mixins 等);而组合式 API 以本地变量和函数为基础,本身就是类型友好的,我们在类型方面几乎不需要做什么额外的工作就能享受到完美的类型推导。

同时与 React Hooks 相比,组合式 API 中的 setup 函数只在初始化时单次执行,在数据响应能力的加持下大大降低了理解与使用成本,基于以上原因,我们决定为 Mpx 添加组合式 API 能力,让用户能够用组合式 API 方式进行小程序开发。

# 组合式 API 实现

从上面的简单示例中可以看出,抛开响应式 API 和生命周期注册模式的变化,组合式 API 的实现要点在于动态添加模板依赖的数据和方法,这也是我们在小程序中实现组合式 API 可能遇到的核心技术卡点。

对于动态添加模板依赖数据,我们在过去的实践中已经充分证明了其可行性,事实上,从 Mpx 最初的版本开始,我们就充分利用了这项能力来实现我们对计算属性和 dataFn (类似于 Vue 使用函数定义 data)的支持,这项能力的关键在于存在合适的生命周期用于动态添加初始化数据,这里对于初始化数据的定义是能够影响组件树的初始渲染,举个简单的例子:存在一对父子组件 parent/child,parent 使用 props 向 child 传递数据,当我们在 parent 初始创建时使用 setData 动态添加 props 数据,同时 child 在初始创建时能够通过 props 正确获取到这部分的数据时,我们就可以将这部分动态添加的数据视作初始化数据,这是我们在小程序中实现完备数据响应支持的基础。

幸运的是,目前业内所有主流小程序平台(微信/支付宝/百度/字节/QQ)都支持了上述能力,微信从一开始就支持在 attached 生命周期中调用 setData 函数动态添加初始化数据,在上述的父子 props 传递场景中,也能够在子组件的 attached 中正确获取这部分数据,支付宝和字节小程序一开始并不支持该能力,不过支付宝在 component2 组件系统重构后,字节在橙心合作项目中与我们沟通后,都成功支持了该能力。

而对于动态返回的方法,最简单能想到的方案就是直接挂载到组件实例上,经过我们的完整测试,上述业内主流小程序平台都支持使用这种方式动态添加方法,基于以上事实,我们非常确定组合式 API 能够在小程序环境中顺利实现,下图简要展示了 Mpx 支持组合式 API 的初始化流程:

composition-api-init

# 生命周期钩子函数

在组合式 API 中,setup 函数只有在组件创建时初始化单次执行,因此需要提供一系列生命周期钩子函数来代替选项式 API 中的生命周期钩子选项,由于小程序原生只支持选项式的生命周期注册方式,我们通过预注册 -> 驱动的方式来实现 setup 中函数式注册生命周期钩子的语法糖,简单来讲就是使用选项式 mixins 的方式提前注册所有需要的生命周期钩子,在选项式生命周期钩子执行时驱动对应在 setup 中使用生命周期钩子函数注册的代码逻辑执行,如下图所示:

composition-api-hook

作为跨端小程序框架,Mpx 需要兼容不同小程序平台不同的生命周期,在选项式 API 中,我们在框架中内置了一套统一的生命周期,将不同小程序平台的生命周期转换映射为内置生命周期后再进行统一的驱动,以抹平不同小程序平台生命周期钩子的差异,如微信小程序的 attached 钩子和支付宝小程序的 onInit 钩子,在组合式 API 中,我们沿用了同样的逻辑,设计了一套与框架内置生命周期对应的生命周期钩子函数,以相同的方式进行驱动,因此这些生命周期钩子函数天然具备了跨平台特性,下表显示了在组件 / 页面中框架生命周期与原生平台生命周期的对应关系:

框架内置生命周期 Hooks in setup 微信原生 支付宝原生
BEFORECREATE null attached(数据响应初始化前) onInit(数据响应初始化前)
CREATED null attached(数据响应初始化后) onInit(数据响应初始化后)
BEFOREMOUNT onBeforeMount ready(MOUNTED 执行前) didMount(MOUNTED 执行前)
MOUNTED onMounted ready(BEFOREMOUNT 执行后) didMount(BEFOREMOUNT 执行后)
BEFOREUPDATE onBeforeUpdate nullsetData 执行前) nullsetData 执行前)
UPDATED onUpdated nullsetData callback) nullsetData callback)
BEFOREUNMOUNT onBeforeUnmount detached(数据响应销毁前) didUnmount(数据响应销毁前)
UNMOUNTED onUnmounted detached(数据响应销毁后) didUnmount(数据响应销毁后)
ONLOAD onLoad onLoad onLoad
ONSHOW onShow onShow onShow
ONHIDE onHide onHide onHide
ONRESIZE onResize onResize events.onResize

同 Vue3 一样,Mpx 在组合式 API 中没有提供 BEFORECREATECREATED 对应的生命周期钩子函数,用户可以直接在 setup 中编写相关逻辑。

# 具有副作用的页面事件

在小程序中,一些页面事件的注册存在副作用,即该页面事件注册与否会产生实质性的影响,比如微信中的 onShareAppMessageonPageScroll,前者在不注册时会禁用当前页面的分享功能,而后者在注册时会带来视图与逻辑层之间的线程通信开销,对于这部分页面事件,我们无法通过预注册 -> 驱动方式提供组合式 API 的注册方式,用户可以通过选项式 API 的方式来注册使用,通过 this 访问组合式 API setup 函数的返回。

然而这种使用方式显然不够优雅,我们考虑是否可以通过一些非常规的方式提供这类副作用页面事件的组合式 API 注册支持,例如,借助编译手段。我们在运行时提供了副作用页面事件的注册函数,并在编译时通过 babel 插件的方式解析识别到当前页面中存在这些特殊注册函数的调用时,通过框架已有的编译 -> 运行时注入的方式将事件驱动逻辑添加到当前页面当中,以提供相对优雅的副作用页面事件在组合式 API 中的注册方式,同时不产生非预期的副作用影响,简单示例如下:

import { createPage, ref, onShareAppMessage } from '@mpxjs/core'
+
+createPage({
+  setup () {
+    const count = ref(0)
+
+    onShareAppMessage(() => {
+      return {
+        title: '页面分享'
+      }
+    })
+
+    return {
+      count
+    }
+  }
+})
+

目前我们通过这种方式支持的页面事件如下:

页面事件 Hooks in setup 平台支持
onPullDownRefresh onPullDownRefresh 全小程序平台 + web
onReachBottom onReachBottom 全小程序平台 + web
onPageScroll onPageScroll 全小程序平台 + web
onShareAppMessage onShareAppMessage 全小程序平台
onTabItemTap onTabItemTap 微信/支付宝/百度/QQ
onAddToFavorites onAddToFavorites 微信 / QQ
onShareTimeline onShareTimeline 微信
onSaveExitState onSaveExitState 微信

特别注意,由于静态编译分析实现方式的限制,这类页面事件的组合式 API 使用需要满足页面事件注册函数(如onShareAppMessage)的调用和 createPage 的调用位于同一个 js 文件当中。

关于生命周期钩子函数的更多信息可以查看这里 (opens new window)

# <script setup>

同 Vue3 一样,我们在 .mpx 单文件组件 / 页面中实现了 <script setup> 的组合式 API 编译语法糖,相较于常规的写法,<script setup> 具备以下优势:

  • 更少的样板内容,更简洁的代码
  • 能够使用纯 TypeScript 声明 props 类型
  • 更好的 IDE 类型推导性能

简单使用示例如下:

<script setup>
+  import { ref } from '@mpxjs/core'
+
+  const msg = ref('hello')
+
+  function log () {
+    console.log(msg.value)
+  }
+</script>
+<template>
+  <view>msg: {{msg}}</view>
+  <view ontap="log">click</view>
+</template>
+

可以看到使用方式与 Vue3 基本一致,不过由于 Mpx 的组合式 API 设计实现与 Vue3 存在差异,对应 <script setup> 也与 Vue3 中存在一些差异:

  • 不支持 import 快捷注册组件
  • 没有 defineEmits() 编译宏
  • 没有 useSlots()useAttrs() 运行时函数
  • 以编译宏的形式提供了 useContext(),获取 setup 函数的第二个参数 context
  • defineExpose() 编译宏的作用与 Vue3 中有所差别,能够限定模板中能访问的变量范围

特别注意,受小程序底层技术限制,我们在 Mpx 的实现中无法像 Vue3 那样将模板编译的渲染函数和 <script setup> 放置到同一作用域下进行变量访问,而是通过静态编译分析提取出 <script setup> 的顶层作用域变量,再以上文中提到的动态添加数据与方法的方式将其设置到模板当中,如果 <script setup> 中声明了较多顶层作用域变量,它们并不一定都会被模板访问,就会带来无效的性能开销,因此我们强烈建议使用 defineExpose() 限定模板中能访问的变量范围,你可以把它等同于 setup 函数中的 return

关于 <script setup> 的更多信息可以查看这里 (opens new window)

# 组合式 API 与 Vue3 中的差异

我们来总结一下 Mpx 中组合式 API 与 Vue3 中的区别:

关于组合式 API 的更多信息可以查看这里 (opens new window)

# 响应式 API 实现

组合式 API 的正常工作离不开响应式 API 的支持,下面我们来聊聊 Mpx 中响应式 API 的设计实现。我们知道 Vue3 中响应式 API 基于 proxy 进行了重构实现,但是目前 proxy 的浏览器兼容占比仍然无法达到我们对于线上可用性的要求,因此在 Mpx 中,我们仍然基于 Object.defineProperty 进行核心数据响应能力的实现,同时借鉴了 Vue3 中优秀的代码设计与实现,如 reactiveEffecteffectScope 等,尽可能实现与 Vue3 中响应式 API 能力对齐。

说到这里,很多同学可能会想到 @vue/composition-api 这个库,该库提供的关键能力正是基于 Vue2 的数据响应系统模拟实现 Vue3 中的响应式 API,我们在前期也对 @vue/composition-api 在 Mpx 中的复用进行了非常有价值的探索尝试。不过最终我们还是决定在 Mpx 的运行时框架中进行独立实现,原因主要在于:@vue/composition-api 是作为一个 Vue2 插件存在,无法直接侵入 Vue2 源码,导致部分能力无法实现或会带来额外的性能开销,例如 flush: 'post'ref 自动解包等。我们也看到在最新的 Vue2.7 版本中,也是在运行时框架里重新实现了这部分内容,以规避上述问题。

下图展示了 Mpx 中响应式 API 核心模块依赖关系:

composition-api-reactive

# 数据响应限制带来的差异

由于 Object.defineProperty 的能力限制,Mpx 存在和 Vue2 一致的数据响应限制,无法感知到对象 property 的添加和删除以及数组的索引赋值,与 Vue2 一致,我们暴露了 setdel API 来让用户显式地进行相关操作。除此之外,由于使用方式发生了变化,我们在使用 reactive API 创建响应式数据时,还会遇到新的限制,我们来看一下代码示例:

import { reactive, watchSyncEffect, set } from '@mpxjs/core'
+
+const state = reactive([0, 1, 2, 3])
+
+watchSyncEffect(() => {
+  console.log(JSON.stringify(state)) // [0,1,2,3]
+})
+
+set(state, 1, 3) // 不会触发 watchEffect
+
+state.push(4) // 不会触发 watchEffect
+

可以看出,即使我们使用了 set API 和数组原型方法对数组进行修改,我们仍然无法监听到数据变化。

相同的限制在使用 Object.defineProperty 的 Vue2.7 中也同样存在。

为什么会存在这个限制呢?原因在于:基于 Object.defineProperty 实现的数据响应系统中,我们会对对象的每个已有属性创建了一个 Dep 对象,在对该属性进行 get 访问时通过这个对象将其与依赖它的观察者 ReactiveEffect 关联起来,并在 set 操作时触发关联 ReactiveEffect 的更新,这是我们大家都知道的数据响应的基本原理。但是对于新增/删除对象属性和修改数组的场景,我们无法事先定义当前不存在属性的 get/set (当然这在 proxy 当中是可行的),因此我们会把对象或者数组本身作为一个数据依赖创建 Dep 对象,通过父级访问该数据时定义的 get/set 将其关联到对应的 ReactiveEffect,并在对数据进行新增/删除属性或数组操作时通过数据本身持有的 Dep 对象触发关联 ReactiveEffect 的更新,如下图所示:

数据响应原理

需要注意的是,通过父级访问是建立 DepReactiveEffect 关联关系的先决条件,在选项式 API 中,我们访问组件的响应式数据都需要通过 this 进行访问,相当于这些数据都存在 this 这个必要的父级,因此我们在使用 $set/$delete 进行对对象进行新增/删除属性或对数组进行修改时都能得到符合预期的结果,唯一的限制在于不能新增/删除根级数据属性,原因就在于 this 不存在访问它的父级。

但是在组合式 API 中,我们不需要通过 this 访问响应式数据,因此通过 reactive() 创建的响应式数据本身就是根级数据,我们自然无法通过上述方式感知到根级数据自身的变化(在 Vue3 中,基于 proxy 提供的强大能力响应式系统能够精确地感知到数据属性,甚至是当前不存在属性的访问与修改,不需要为数据自身建立 Dep 对象,自然也不存在相关问题)。

在这种情况下,我们就需要用 ref() 创建响应式数据,因为 ref 创建了一个包装对象,我们永远需要通过 .value 来访问其持有的数据(不管是显式访问还是隐式自动解包),这样就能保证 ref 数据自身的变化能够被响应式系统感知,因此也不会遇到上面描述的问题,如下所示:

import { ref, watchSyncEffect, set } from '@mpxjs/core'
+
+const state = ref([0, 1, 2, 3])
+
+watchSyncEffect(() => {
+  console.log(JSON.stringify(state.value)) // [0,1,2,3]
+})
+
+set(state.value, 1, 3) // [0,3,2,3]
+
+state.value.push(4) // [0,3,2,3,4]
+

# 响应式 API 与 Vue3 中的区别

我们来总结一下 Mpx 中响应式 API 与 Vue3 中的区别:

  • 不支持 raw 相关 API(markRaw 除外,我们提供了该 API 用于跳过部分数据的响应式转换)
  • 不支持 readonly 相关 API
  • 不支持 watchEffectwatchcomputed 的调试选项
  • 不支持对 mapset 等集合类型进行响应式转换
  • 受到 Object.defineProperty 实现带来的数据响应限制影响

关于响应式 API 的更多信息可以查看这里 (opens new window)

# 生态周边适配

除了 Mpx 运行时核心提供了组合式 API 支持外,我们对 Mpx 的周边生态能力也都进行了组合式 API 适配支持,包括 storei18nfetch 等。

# Pinia store 支持

Pinia 是基于组合式 API 设计的全新数据管理方案,目前已经取代 Vuex 成为 Vue3 官方推荐的 store,我们在研究了 pinia 的设计实现后,对其简练优雅的设计思想及其与组合式 API 的高度适配非常满意(特别是在使用 setup 函数创建 store 时,使用心智与编写组件完全一致,可以将其视作是没有视图的组件)。因此我们 fork 了 pinia 的源码仓库,基于 Mpx 提供的数据响应能力对其进行了适配改造,使其在 Mpx 环境下也能正常运行,目前相关代码维护在 @mpxjs/pinia 中,在组合式 API 中的简单使用示例如下:

import { createComponent, ref, computed, toRefs } from '@mpxjs/core'
+import { defineStore } from '@mpxjs/pinia'
+
+// 使用组合式 API 创建 pinia store 的使用心智与 setup 函数完全一致,强烈推荐
+const useSetupStore = defineStore('setup', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+
+  function increment () {
+    count.value++
+  }
+
+  return { count, doubleCount, increment }
+})
+
+createComponent({
+  setup () {
+    const store = useSetupStore()
+    // store 同 props 类似是一个 reactive 对象,解构数据需使用 toRefs 以保持数据响应性
+    const { count, doubleCount } = toRefs(store)
+    // 方法可以直接解构
+    const { increment } = store
+  
+    return { count, doubleCount, increment }
+    //
+  }
+})
+

Mpx 中通过 createStore 创建的类 Vuex store 在组合式 API 中仍然可以使用,我们可以在 setup 函数中引用 store 实例进行数据读取与方法调用 (opens new window),不过整体使用体验与 pinia store 存在较大差距,我们还是推荐在组合式 API 开发中优先使用 pinia store 作为数据管理方案。

# I18n 支持

传统选项式 API 中,我们使用 this.$t 方法在组件内调用翻译函数,但在组合式 API 中我们无法访问 this,为此我们参考了 Vue I18n 最新的 9.x 版本,该版本针对 Vue3 及组合式 API 进行了重构适配,提供了全新的 useI18n API,简单使用示例如下:

<template>
+  <view>{{t('message.hello')}}</view>
+  <button bindtap="changeLocale">change locale</button>
+</template>
+
+<script>
+  import { createComponent, useI18n } from '@mpxjs/core'
+
+  createComponent({
+    setup () {
+      // useI18n 不传参数时指向全局 i18n 对象,也可以传递 locale 和 messages 配置创建局部 i18n 对象
+      const { t, locale } = useI18n()
+
+      function changeLocale () {
+        locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
+      }
+      // 返回的翻译方法名必须为 t,不能进行重命名
+      return { t, changeLocale }
+    }
+  })
+</script>
+

上面示例代码看上去像是我们在模板上直接调用 setup() 返回的 t 翻译方法,但是熟悉小程序开发的同学都知道在小程序架构下这是不可能的,示例中的写法其实由框架通过编译 + 运行时手段实现的语法糖,我们会在模板编译时定向扫描 t/te/tm 等 i18n 方法,将其转换为计算属性注入到运行时当中,这就意味着如果我们对翻译方法进行重命名,模板编译时无法识别出 i18n 方法调用,自然也就无法正常运行。

Mpx 中 i18n 提供了两种实现模式,分别是 wxs 和 computed,可以使用编译选项中的 i18n.isComputed 进行切换,两种方式各有优劣,其中:

  • wxs 模式的优势在于逻辑层和视图层独立维护语言集,无额外运行时性能开销,且使用没有任何限制;劣势同样源于语言集同时存在于逻辑层(js)和视图层(wxs)当中,这部分的包体积占用翻倍;
  • computed 模式的优势在于语言集只存在于逻辑层中,无额外包体积占用,且可以通过动态添加语言集的方式进一步减少包体积占用;劣势则是会产生额外的运行时性能开销,且使用上存在限制,模板调用时无法直接访问 wx:for 中的 itemindex

在组合式 API 中模板上使用 useI18n() 返回的翻译函数 t/te/tm 时,为了完整实现 useI18n API的功能,会强制使用 computed 模式进行实现,这也意味着该用法会受到 computed 模式使用限制的影响。不过当你不需要使用 useI18n 接受 messages 参数创建局部语言集作用域功能时,你也完全可以在模板中继续使用原有的 $t/$tc/$te/$tm 方法,这些方法受编译选项 i18n.isComputed 的影响,同时指向全局语言集作用域。

更多关于生态周边的组合式 API 使用指南可以点击下方链接查看详情:

# 输出 web 适配

跨端输出 web 作为 Mpx 的一大核心特性,在业务中存在广泛使用,同时也是我们设计实现任何框架新特性需要优先考虑的事项。在本次组合式 API 支持中,我们从设计之初就考虑了跨端输出 web 的适配支持,保障使用 Mpx 组合式 API 开发的业务代码都能在 web 环境中正常运行。

我们输出 web 的整体技术方向在于尽可能复用 Vue 已有的生态能力,为了实现这个目标,我们需要提供尽可能与 Vue 保持一致的 API 设计,以降低抹平适配成本。在输出 web 时,核心组合式 API 基于 Vue2.7 版本中的已有能力进行适配提供,简单举个例子:对于 import { ref } from 'mpxjs/core' 这行语句,在小程序中会指向 Mpx 内部维护的 ref 实现,而在输出 web 时会指向 Vue 中维护的 ref 实现,两者的实现虽然不仅相同,但只要保障对外函数签名一致,对于开发者用户来说就无感知。

我们借助了 Mpx 强大的条件编译能力进行上述实现,对运行时导出根据输出平台进行重定向,这样还能保障跨端输出产物干净简洁,仅包含当前输出环境下必要的逻辑,如下图所示:

composition-api-web

同理,我们也采用了类似的方式实现了组合式 API 周边能力对于输出 web 的适配支持,如pinia store 使用 pinia 原始版本进行适配实现,而 i18n 能力则是使用 vue-i18n@8.x + vue-i18n-bridge 进行适配实现。

# 性能表现

性能是 Mpx 一直以来的核心关注点,我们对组合式 API 的最终实现版本进行了一系列性能评估测试,我们使用组合式 API 版本对业务中的评价组件进行了重构,评价组件属于我们业务中交互及功能相对比较复杂的组件,源码行数约 1000 行,组件数据 27 项,组件方法 18 个,我们在测试项目中对选项式 API 和组合式 API 两个版本实现的组件进行了一系列测试。

# 组件初始化耗时

由于组合式 API 改变了原有的组件初始化流程,我们对组件的初始化耗时进行了重点测试,测试口径如下:

  • 耗时计算以挂载组件为起点,以组件 ready 执行为终点
  • 测试结果为10次手工测试排除最大最小值后求均值
  • IOS 测试机型为 iPhone 13 pro max,安卓测试机型为 OPPO R9

结果显示两个版本的组件初始化耗时大抵持平,不分优劣。

IOS 安卓
选项式 API 42.5ms 366.6ms
组合式 API 42.4ms 370.1ms

# 组件 JS 体积

在构建产物体积方面,由于组合式 API 的写法对于 JS 代码压缩更加有利,同样的逻辑实现下,组合式 API 版本的组件构建压缩后 JS 体积略胜一筹。

组件 JS 体积
选项式 API 15.67KB
组合式 API 13.60KB

# 框架运行时体积

在 Mpx2.8 版本中,我们在框架运行时中新增了组合式 API 相关实现,不过通过优化运行时导出,使其对 tree shaking 更加友好,我们的框架运行时体积在实际构建产物中没有产生太大增长。

框架运行时体积
选项式 API 51.66KB
组合式 API 57.47KB

综上所述,组合式 API 版本的运行时性能与选项式 API 大抵持平,在包体积占用方面,新版框架运行时体积占用略有提升,不过由于组合式 API 开发模式对代码压缩更友好,加上组合式 API 更易进行逻辑复用的特点,我们预计在实际业务项目中,组合式 API 的包体积占用会更小。

# 破坏性改变

Mpx 组合式 API 版本完全兼容原有的选项式 API 开发方式,不过我们在运行时重构过程中依然带来了少量的破坏性改变,详情如下:

  • 框架过往提供的组件增强生命周期 pageShow/pageHide 与微信原生提供的 pageLifetimes.show/hide 完全对齐,不再提供组件初始挂载时必定执行 pageShow 的保障(因为组件可能在后台页面进行挂载),相关初始化逻辑一定不要放置在 pageShow 当中;
  • 取消了框架过去提供的基于内部生命周期实现的非标准增强生命周期,如 beforeCreate/onBeforeCreate 等,直接将内部生命周期变量导出提供给用户使用,详情查看这里 (opens new window);
  • 为了优化 tree shaking,作为框架运行时 default exportMpx 对象不再挂载 createComponent/createStore 等运行时方法,一律通过 named export 提供,Mpx 对象上仅保留 set/use 等全局 API,详情查看这里 (opens new window)
  • 使用 I18n 能力时,为了与新版 vue-i18n 保持对齐,this.$i18n 对象指向全局作用域,如需创建局部作用域需要使用组合式 API useI18n 的方式进行创建。
  • watch API 不再接受第二个参数为带有 handler 属性的对象形式(该参数形式只应存在于 watch option 中),第二个参数必须为回调函数,与 Vue (opens new window) 对齐。

更详细的迁移指南请点击查看这里 (opens new window)

# 未来规划

目前 Mpx2.8 版本已经在以滴滴出行小程序和花小猪小程序为代表的集团小程序业务中稳定全面落地,并在新的业务迭代中大范围使用了组合式 API,使用反馈良好,在社区内也产出了多个成功案例。

在完成持久化构建缓存和组合式 API 两个重大技术升级后,我们未来的技术规划如下:

  • 组合式 API 工具库 @mpxjs/use
  • 内置原子类支持
  • 输出 web 支持 SSR
  • 输出 web 支持使用 Vite 构建
  • 跨端输出 Hummer 合入主干正式 release
  • 优化运行时 render 函数,降低包体积占用
  • 跨端库 @mpxjs/cube-ui 开源

最后,再次感谢所有参与 Mpx 组合式 API 技术建设的同学们,也欢迎社区同学一同加入 Mpx 项目开源共建。

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/2.8-release.html b/docs-vuepress/.vuepress/dist/articles/2.8-release.html new file mode 100644 index 0000000000..6bc81b2c74 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/2.8-release.html @@ -0,0 +1,182 @@ + + + + + + Mpx2.8 版本正式发布,使用组合式 API 开发小程序 | Mpx框架 + + + + + + + + + +

# Mpx2.8 版本正式发布,使用组合式 API 开发小程序

作者:hiyuki (opens new window)

小程序跨端开发框架 Mpx (opens new window) 自18年立项开源以来,如今已经走过了第四个年头,其高性能、优体验、跨平台的特性收获了公司内外开发者用户的一致好评。

为了不辜负开发者用户对我们的信赖,更好地支持集团小程序业务开发,一方面我们对 Mpx 的稳定版本进行着高频的维护迭代,快速响应处理集团内外开发者用户在框架开发使用过程中遇到的问题;另一方面我们持续跟进探索业内最新动态,力争将更新更好的开发能力与体验带给小程序开发者用户。继年初我们在 2.7 版本 (opens new window)中对 Mpx 的编译系统进行重构适配 Webpack5,基于持久化缓存大幅提升编译速度后,在最新的 2.8 版本 (opens new window)中,我们对 Mpx 的运行时框架也进行了大量重构改造工作,完整支持了 Vue3 提出的组合式 API 开发范式,让用户能够使用当下最热门的开发方式进行小程序开发,我们先来简单感受一下组合式 API 的使用:

<template>
+  <view>{{ collectionName }}: {{ book.title }}({{ readersNumber }})</view>
+  <button bindtap="addReaders">addReaders</button>
+</template>
+
+<script>
+  import { createComponent, ref, reactive, onMounted } from '@mpxjs/core'
+
+  createComponent({
+    properties: {
+      collectionName: String
+    },
+    setup () {
+      const readersNumber = ref(0)
+      const book = reactive({ title: 'Mpx' })
+
+      onMounted(() => {
+        console.log('Component mounted.')
+      })
+
+      // 暴露给 template
+      return {
+        readersNumber,
+        book,
+        addReaders () {
+          readersNumber.value++
+        }
+      }
+    }
+  })
+</script>
+

可以看出和 Vue3 组合式 API 的使用是高度类似的,利用框架导出的一系列响应式 API 和 生命周期钩子函数在 setup 中编写业务逻辑,并将模板依赖的数据与方法作为返回值返回,与传统的选项式 API 相比,组合式 API 具备以下优势:

  • 更好的逻辑复用,通过函数包装复用逻辑,显式引入调用,方便简洁且符合直觉,规避消除了 mixins 复用中存在的缺陷;
  • 更灵活的代码组织,相比于选项式 API 提前规定了代码的组织方式,组合式 API 在这方面几乎没有做任何限制与规定,更加灵活自由,在功能复杂的庞大组件中,我们能够通过组合式 API 让我们的功能代码更加内聚且有条理,不过这也会对开发者自身的代码规范意识提出更高要求;
  • 更好的类型推导,虽然基于 this 的选项式 API 通过 ThisType 类型体操的方式也能在一定程度上实现 TS 类型推导,但推导和实现成本较高,同时仍然无法完美覆盖一些复杂场景(如嵌套 mixins 等);而组合式 API 以本地变量和函数为基础,本身就是类型友好的,我们在类型方面几乎不需要做什么额外的工作就能享受到完美的类型推导。

同时与 React Hooks 相比,组合式 API 中的 setup 函数只在初始化时单次执行,在数据响应能力的加持下大大降低了理解与使用成本,基于以上原因,我们决定为 Mpx 添加组合式 API 能力,让用户能够用组合式 API 方式进行小程序开发。

# 组合式 API 实现

从上面的简单示例中可以看出,抛开响应式 API 和生命周期注册模式的变化,组合式 API 的实现要点在于动态添加模板依赖的数据和方法,这也是我们在小程序中实现组合式 API 可能遇到的核心技术卡点。

对于动态添加模板依赖数据,我们在过去的实践中已经充分证明了其可行性,事实上,从 Mpx 最初的版本开始,我们就充分利用了这项能力来实现我们对计算属性和 dataFn (类似于 Vue 使用函数定义 data)的支持,这项能力的关键在于存在合适的生命周期用于动态添加初始化数据,这里对于初始化数据的定义是能够影响组件树的初始渲染,举个简单的例子:存在一对父子组件 parent/child,parent 使用 props 向 child 传递数据,当我们在 parent 初始创建时使用 setData 动态添加 props 数据,同时 child 在初始创建时能够通过 props 正确获取到这部分的数据时,我们就可以将这部分动态添加的数据视作初始化数据,这是我们在小程序中实现完备数据响应支持的基础。

幸运的是,目前业内所有主流小程序平台(微信/支付宝/百度/字节/QQ)都支持了上述能力,微信从一开始就支持在 attached 生命周期中调用 setData 函数动态添加初始化数据,在上述的父子 props 传递场景中,也能够在子组件的 attached 中正确获取这部分数据,支付宝和字节小程序一开始并不支持该能力,不过支付宝在 component2 组件系统重构后,字节在橙心合作项目中与我们沟通后,都成功支持了该能力。

而对于动态返回的方法,最简单能想到的方案就是直接挂载到组件实例上,经过我们的完整测试,上述业内主流小程序平台都支持使用这种方式动态添加方法,基于以上事实,我们非常确定组合式 API 能够在小程序环境中顺利实现,下图简要展示了 Mpx 支持组合式 API 的初始化流程:

composition-api-init

# 生命周期钩子函数

在组合式 API 中,setup 函数只有在组件创建时初始化单次执行,因此需要提供一系列生命周期钩子函数来代替选项式 API 中的生命周期钩子选项,由于小程序原生只支持选项式的生命周期注册方式,我们通过预注册 -> 驱动的方式来实现 setup 中函数式注册生命周期钩子的语法糖,简单来讲就是使用选项式 mixins 的方式提前注册所有需要的生命周期钩子,在选项式生命周期钩子执行时驱动对应在 setup 中使用生命周期钩子函数注册的代码逻辑执行,如下图所示:

composition-api-hook

作为跨端小程序框架,Mpx 需要兼容不同小程序平台不同的生命周期,在选项式 API 中,我们在框架中内置了一套统一的生命周期,将不同小程序平台的生命周期转换映射为内置生命周期后再进行统一的驱动,以抹平不同小程序平台生命周期钩子的差异,如微信小程序的 attached 钩子和支付宝小程序的 onInit 钩子,在组合式 API 中,我们沿用了同样的逻辑,设计了一套与框架内置生命周期对应的生命周期钩子函数,以相同的方式进行驱动,因此这些生命周期钩子函数天然具备了跨平台特性,下表显示了在组件 / 页面中框架生命周期与原生平台生命周期的对应关系:

框架内置生命周期 Hooks in setup 微信原生 支付宝原生
BEFORECREATE null attached(数据响应初始化前) onInit(数据响应初始化前)
CREATED null attached(数据响应初始化后) onInit(数据响应初始化后)
BEFOREMOUNT onBeforeMount ready(MOUNTED 执行前) didMount(MOUNTED 执行前)
MOUNTED onMounted ready(BEFOREMOUNT 执行后) didMount(BEFOREMOUNT 执行后)
BEFOREUPDATE onBeforeUpdate nullsetData 执行前) nullsetData 执行前)
UPDATED onUpdated nullsetData callback) nullsetData callback)
BEFOREUNMOUNT onBeforeUnmount detached(数据响应销毁前) didUnmount(数据响应销毁前)
UNMOUNTED onUnmounted detached(数据响应销毁后) didUnmount(数据响应销毁后)
ONLOAD onLoad onLoad onLoad
ONSHOW onShow onShow onShow
ONHIDE onHide onHide onHide
ONRESIZE onResize onResize events.onResize

同 Vue3 一样,Mpx 在组合式 API 中没有提供 BEFORECREATECREATED 对应的生命周期钩子函数,用户可以直接在 setup 中编写相关逻辑。

# 具有副作用的页面事件

在小程序中,一些页面事件的注册存在副作用,即该页面事件注册与否会产生实质性的影响,比如微信中的 onShareAppMessageonPageScroll,前者在不注册时会禁用当前页面的分享功能,而后者在注册时会带来视图与逻辑层之间的线程通信开销,对于这部分页面事件,我们无法通过预注册 -> 驱动方式提供组合式 API 的注册方式,用户可以通过选项式 API 的方式来注册使用,通过 this 访问组合式 API setup 函数的返回。

然而这种使用方式显然不够优雅,我们考虑是否可以通过一些非常规的方式提供这类副作用页面事件的组合式 API 注册支持,例如,借助编译手段。我们在运行时提供了副作用页面事件的注册函数,并在编译时通过 babel 插件的方式解析识别到当前页面中存在这些特殊注册函数的调用时,通过框架已有的编译 -> 运行时注入的方式将事件驱动逻辑添加到当前页面当中,以提供相对优雅的副作用页面事件在组合式 API 中的注册方式,同时不产生非预期的副作用影响,简单示例如下:

import { createPage, ref, onShareAppMessage } from '@mpxjs/core'
+
+createPage({
+  setup () {
+    const count = ref(0)
+
+    onShareAppMessage(() => {
+      return {
+        title: '页面分享'
+      }
+    })
+
+    return {
+      count
+    }
+  }
+})
+

目前我们通过这种方式支持的页面事件如下:

页面事件 Hooks in setup 平台支持
onPullDownRefresh onPullDownRefresh 全小程序平台 + web
onReachBottom onReachBottom 全小程序平台 + web
onPageScroll onPageScroll 全小程序平台 + web
onShareAppMessage onShareAppMessage 全小程序平台
onTabItemTap onTabItemTap 微信/支付宝/百度/QQ
onAddToFavorites onAddToFavorites 微信 / QQ
onShareTimeline onShareTimeline 微信
onSaveExitState onSaveExitState 微信

特别注意,由于静态编译分析实现方式的限制,这类页面事件的组合式 API 使用需要满足页面事件注册函数(如onShareAppMessage)的调用和 createPage 的调用位于同一个 js 文件当中。

关于生命周期钩子函数的更多信息可以查看这里 (opens new window)

# <script setup>

同 Vue3 一样,我们在 .mpx 单文件组件 / 页面中实现了 <script setup> 的组合式 API 编译语法糖,相较于常规的写法,<script setup> 具备以下优势:

  • 更少的样板内容,更简洁的代码
  • 能够使用纯 TypeScript 声明 props 类型
  • 更好的 IDE 类型推导性能

简单使用示例如下:

<script setup>
+  import { ref } from '@mpxjs/core'
+
+  const msg = ref('hello')
+
+  function log () {
+    console.log(msg.value)
+  }
+</script>
+<template>
+  <view>msg: {{msg}}</view>
+  <view ontap="log">click</view>
+</template>
+

可以看到使用方式与 Vue3 基本一致,不过由于 Mpx 的组合式 API 设计实现与 Vue3 存在差异,对应 <script setup> 也与 Vue3 中存在一些差异:

  • 不支持 import 快捷注册组件
  • 没有 defineEmits() 编译宏
  • 没有 useSlots()useAttrs() 运行时函数
  • 以编译宏的形式提供了 useContext(),获取 setup 函数的第二个参数 context
  • defineExpose() 编译宏的作用与 Vue3 中有所差别,能够限定模板中能访问的变量范围

特别注意,受小程序底层技术限制,我们在 Mpx 的实现中无法像 Vue3 那样将模板编译的渲染函数和 <script setup> 放置到同一作用域下进行变量访问,而是通过静态编译分析提取出 <script setup> 的顶层作用域变量,再以上文中提到的动态添加数据与方法的方式将其设置到模板当中,如果 <script setup> 中声明了较多顶层作用域变量,它们并不一定都会被模板访问,就会带来无效的性能开销,因此我们强烈建议使用 defineExpose() 限定模板中能访问的变量范围,你可以把它等同于 setup 函数中的 return

关于 <script setup> 的更多信息可以查看这里 (opens new window)

# 组合式 API 与 Vue3 中的差异

我们来总结一下 Mpx 中组合式 API 与 Vue3 中的区别:

关于组合式 API 的更多信息可以查看这里 (opens new window)

# 响应式 API 实现

组合式 API 的正常工作离不开响应式 API 的支持,下面我们来聊聊 Mpx 中响应式 API 的设计实现。我们知道 Vue3 中响应式 API 基于 proxy 进行了重构实现,但是目前 proxy 的浏览器兼容占比仍然无法达到我们对于线上可用性的要求,因此在 Mpx 中,我们仍然基于 Object.defineProperty 进行核心数据响应能力的实现,同时借鉴了 Vue3 中优秀的代码设计与实现,如 reactiveEffecteffectScope 等,尽可能实现与 Vue3 中响应式 API 能力对齐。

说到这里,很多同学可能会想到 @vue/composition-api 这个库,该库提供的关键能力正是基于 Vue2 的数据响应系统模拟实现 Vue3 中的响应式 API,我们在前期也对 @vue/composition-api 在 Mpx 中的复用进行了非常有价值的探索尝试。不过最终我们还是决定在 Mpx 的运行时框架中进行独立实现,原因主要在于:@vue/composition-api 是作为一个 Vue2 插件存在,无法直接侵入 Vue2 源码,导致部分能力无法实现或会带来额外的性能开销,例如 flush: 'post'ref 自动解包等。我们也看到在最新的 Vue2.7 版本中,也是在运行时框架里重新实现了这部分内容,以规避上述问题。

下图展示了 Mpx 中响应式 API 核心模块依赖关系:

composition-api-reactive

# 数据响应限制带来的差异

由于 Object.defineProperty 的能力限制,Mpx 存在和 Vue2 一致的数据响应限制,无法感知到对象 property 的添加和删除以及数组的索引赋值,与 Vue2 一致,我们暴露了 setdel API 来让用户显式地进行相关操作。除此之外,由于使用方式发生了变化,我们在使用 reactive API 创建响应式数据时,还会遇到新的限制,我们来看一下代码示例:

import { reactive, watchSyncEffect, set } from '@mpxjs/core'
+
+const state = reactive([0, 1, 2, 3])
+
+watchSyncEffect(() => {
+  console.log(JSON.stringify(state)) // [0,1,2,3]
+})
+
+set(state, 1, 3) // 不会触发 watchEffect
+
+state.push(4) // 不会触发 watchEffect
+

可以看出,即使我们使用了 set API 和数组原型方法对数组进行修改,我们仍然无法监听到数据变化。

相同的限制在使用 Object.defineProperty 的 Vue2.7 中也同样存在。

为什么会存在这个限制呢?原因在于:基于 Object.defineProperty 实现的数据响应系统中,我们会对对象的每个已有属性创建了一个 Dep 对象,在对该属性进行 get 访问时通过这个对象将其与依赖它的观察者 ReactiveEffect 关联起来,并在 set 操作时触发关联 ReactiveEffect 的更新,这是我们大家都知道的数据响应的基本原理。但是对于新增/删除对象属性和修改数组的场景,我们无法事先定义当前不存在属性的 get/set (当然这在 proxy 当中是可行的),因此我们会把对象或者数组本身作为一个数据依赖创建 Dep 对象,通过父级访问该数据时定义的 get/set 将其关联到对应的 ReactiveEffect,并在对数据进行新增/删除属性或数组操作时通过数据本身持有的 Dep 对象触发关联 ReactiveEffect 的更新,如下图所示:

数据响应原理

需要注意的是,通过父级访问是建立 DepReactiveEffect 关联关系的先决条件,在选项式 API 中,我们访问组件的响应式数据都需要通过 this 进行访问,相当于这些数据都存在 this 这个必要的父级,因此我们在使用 $set/$delete 进行对对象进行新增/删除属性或对数组进行修改时都能得到符合预期的结果,唯一的限制在于不能新增/删除根级数据属性,原因就在于 this 不存在访问它的父级。

但是在组合式 API 中,我们不需要通过 this 访问响应式数据,因此通过 reactive() 创建的响应式数据本身就是根级数据,我们自然无法通过上述方式感知到根级数据自身的变化(在 Vue3 中,基于 proxy 提供的强大能力响应式系统能够精确地感知到数据属性,甚至是当前不存在属性的访问与修改,不需要为数据自身建立 Dep 对象,自然也不存在相关问题)。

在这种情况下,我们就需要用 ref() 创建响应式数据,因为 ref 创建了一个包装对象,我们永远需要通过 .value 来访问其持有的数据(不管是显式访问还是隐式自动解包),这样就能保证 ref 数据自身的变化能够被响应式系统感知,因此也不会遇到上面描述的问题,如下所示:

import { ref, watchSyncEffect, set } from '@mpxjs/core'
+
+const state = ref([0, 1, 2, 3])
+
+watchSyncEffect(() => {
+  console.log(JSON.stringify(state.value)) // [0,1,2,3]
+})
+
+set(state.value, 1, 3) // [0,3,2,3]
+
+state.value.push(4) // [0,3,2,3,4]
+

# 响应式 API 与 Vue3 中的区别

我们来总结一下 Mpx 中响应式 API 与 Vue3 中的区别:

  • 不支持 raw 相关 API(markRaw 除外,我们提供了该 API 用于跳过部分数据的响应式转换)
  • 不支持 readonly 相关 API
  • 不支持 watchEffectwatchcomputed 的调试选项
  • 不支持对 mapset 等集合类型进行响应式转换
  • 受到 Object.defineProperty 实现带来的数据响应限制影响

关于响应式 API 的更多信息可以查看这里 (opens new window)

# 生态周边适配

除了 Mpx 运行时核心提供了组合式 API 支持外,我们对 Mpx 的周边生态能力也都进行了组合式 API 适配支持,包括 storei18nfetch 等。

# Pinia store 支持

Pinia 是基于组合式 API 设计的全新数据管理方案,目前已经取代 Vuex 成为 Vue3 官方推荐的 store,我们在研究了 pinia 的设计实现后,对其简练优雅的设计思想及其与组合式 API 的高度适配非常满意(特别是在使用 setup 函数创建 store 时,使用心智与编写组件完全一致,可以将其视作是没有视图的组件)。因此我们 fork 了 pinia 的源码仓库,基于 Mpx 提供的数据响应能力对其进行了适配改造,使其在 Mpx 环境下也能正常运行,目前相关代码维护在 @mpxjs/pinia 中,在组合式 API 中的简单使用示例如下:

import { createComponent, ref, computed, toRefs } from '@mpxjs/core'
+import { defineStore } from '@mpxjs/pinia'
+
+// 使用组合式 API 创建 pinia store 的使用心智与 setup 函数完全一致,强烈推荐
+const useSetupStore = defineStore('setup', () => {
+  const count = ref(0)
+  const doubleCount = computed(() => count.value * 2)
+
+  function increment () {
+    count.value++
+  }
+
+  return { count, doubleCount, increment }
+})
+
+createComponent({
+  setup () {
+    const store = useSetupStore()
+    // store 同 props 类似是一个 reactive 对象,解构数据需使用 toRefs 以保持数据响应性
+    const { count, doubleCount } = toRefs(store)
+    // 方法可以直接解构
+    const { increment } = store
+  
+    return { count, doubleCount, increment }
+    //
+  }
+})
+

Mpx 中通过 createStore 创建的类 Vuex store 在组合式 API 中仍然可以使用,我们可以在 setup 函数中引用 store 实例进行数据读取与方法调用 (opens new window),不过整体使用体验与 pinia store 存在较大差距,我们还是推荐在组合式 API 开发中优先使用 pinia store 作为数据管理方案。

# I18n 支持

传统选项式 API 中,我们使用 this.$t 方法在组件内调用翻译函数,但在组合式 API 中我们无法访问 this,为此我们参考了 Vue I18n 最新的 9.x 版本,该版本针对 Vue3 及组合式 API 进行了重构适配,提供了全新的 useI18n API,简单使用示例如下:

<template>
+  <view>{{t('message.hello')}}</view>
+  <button bindtap="changeLocale">change locale</button>
+</template>
+
+<script>
+  import { createComponent, useI18n } from '@mpxjs/core'
+
+  createComponent({
+    setup () {
+      // useI18n 不传参数时指向全局 i18n 对象,也可以传递 locale 和 messages 配置创建局部 i18n 对象
+      const { t, locale } = useI18n()
+
+      function changeLocale () {
+        locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
+      }
+      // 返回的翻译方法名必须为 t,不能进行重命名
+      return { t, changeLocale }
+    }
+  })
+</script>
+

上面示例代码看上去像是我们在模板上直接调用 setup() 返回的 t 翻译方法,但是熟悉小程序开发的同学都知道在小程序架构下这是不可能的,示例中的写法其实由框架通过编译 + 运行时手段实现的语法糖,我们会在模板编译时定向扫描 t/te/tm 等 i18n 方法,将其转换为计算属性注入到运行时当中,这就意味着如果我们对翻译方法进行重命名,模板编译时无法识别出 i18n 方法调用,自然也就无法正常运行。

Mpx 中 i18n 提供了两种实现模式,分别是 wxs 和 computed,可以使用编译选项中的 i18n.isComputed 进行切换,两种方式各有优劣,其中:

  • wxs 模式的优势在于逻辑层和视图层独立维护语言集,无额外运行时性能开销,且使用没有任何限制;劣势同样源于语言集同时存在于逻辑层(js)和视图层(wxs)当中,这部分的包体积占用翻倍;
  • computed 模式的优势在于语言集只存在于逻辑层中,无额外包体积占用,且可以通过动态添加语言集的方式进一步减少包体积占用;劣势则是会产生额外的运行时性能开销,且使用上存在限制,模板调用时无法直接访问 wx:for 中的 itemindex

在组合式 API 中模板上使用 useI18n() 返回的翻译函数 t/te/tm 时,为了完整实现 useI18n API的功能,会强制使用 computed 模式进行实现,这也意味着该用法会受到 computed 模式使用限制的影响。不过当你不需要使用 useI18n 接受 messages 参数创建局部语言集作用域功能时,你也完全可以在模板中继续使用原有的 $t/$tc/$te/$tm 方法,这些方法受编译选项 i18n.isComputed 的影响,同时指向全局语言集作用域。

更多关于生态周边的组合式 API 使用指南可以点击下方链接查看详情:

# 输出 web 适配

跨端输出 web 作为 Mpx 的一大核心特性,在业务中存在广泛使用,同时也是我们设计实现任何框架新特性需要优先考虑的事项。在本次组合式 API 支持中,我们从设计之初就考虑了跨端输出 web 的适配支持,保障使用 Mpx 组合式 API 开发的业务代码都能在 web 环境中正常运行。

我们输出 web 的整体技术方向在于尽可能复用 Vue 已有的生态能力,为了实现这个目标,我们需要提供尽可能与 Vue 保持一致的 API 设计,以降低抹平适配成本。在输出 web 时,核心组合式 API 基于 Vue2.7 版本中的已有能力进行适配提供,简单举个例子:对于 import { ref } from 'mpxjs/core' 这行语句,在小程序中会指向 Mpx 内部维护的 ref 实现,而在输出 web 时会指向 Vue 中维护的 ref 实现,两者的实现虽然不仅相同,但只要保障对外函数签名一致,对于开发者用户来说就无感知。

我们借助了 Mpx 强大的条件编译能力进行上述实现,对运行时导出根据输出平台进行重定向,这样还能保障跨端输出产物干净简洁,仅包含当前输出环境下必要的逻辑,如下图所示:

composition-api-web

同理,我们也采用了类似的方式实现了组合式 API 周边能力对于输出 web 的适配支持,如pinia store 使用 pinia 原始版本进行适配实现,而 i18n 能力则是使用 vue-i18n@8.x + vue-i18n-bridge 进行适配实现。

# 性能表现

性能是 Mpx 一直以来的核心关注点,我们对组合式 API 的最终实现版本进行了一系列性能评估测试,我们使用组合式 API 版本对业务中的评价组件进行了重构,评价组件属于我们业务中交互及功能相对比较复杂的组件,源码行数约 1000 行,组件数据 27 项,组件方法 18 个,我们在测试项目中对选项式 API 和组合式 API 两个版本实现的组件进行了一系列测试。

# 组件初始化耗时

由于组合式 API 改变了原有的组件初始化流程,我们对组件的初始化耗时进行了重点测试,测试口径如下:

  • 耗时计算以挂载组件为起点,以组件 ready 执行为终点
  • 测试结果为10次手工测试排除最大最小值后求均值
  • IOS 测试机型为 iPhone 13 pro max,安卓测试机型为 OPPO R9

结果显示两个版本的组件初始化耗时大抵持平,不分优劣。

IOS 安卓
选项式 API 42.5ms 366.6ms
组合式 API 42.4ms 370.1ms

# 组件 JS 体积

在构建产物体积方面,由于组合式 API 的写法对于 JS 代码压缩更加有利,同样的逻辑实现下,组合式 API 版本的组件构建压缩后 JS 体积略胜一筹。

组件 JS 体积
选项式 API 15.67KB
组合式 API 13.60KB

# 框架运行时体积

在 Mpx2.8 版本中,我们在框架运行时中新增了组合式 API 相关实现,不过通过优化运行时导出,使其对 tree shaking 更加友好,我们的框架运行时体积在实际构建产物中没有产生太大增长。

框架运行时体积
选项式 API 51.66KB
组合式 API 57.47KB

综上所述,组合式 API 版本的运行时性能与选项式 API 大抵持平,在包体积占用方面,新版框架运行时体积占用略有提升,不过由于组合式 API 开发模式对代码压缩更友好,加上组合式 API 更易进行逻辑复用的特点,我们预计在实际业务项目中,组合式 API 的包体积占用会更小。

# 破坏性改变

Mpx 组合式 API 版本完全兼容原有的选项式 API 开发方式,不过我们在运行时重构过程中依然带来了少量的破坏性改变,详情如下:

  • 框架过往提供的组件增强生命周期 pageShow/pageHide 与微信原生提供的 pageLifetimes.show/hide 完全对齐,不再提供组件初始挂载时必定执行 pageShow 的保障(因为组件可能在后台页面进行挂载),相关初始化逻辑一定不要放置在 pageShow 当中;
  • 取消了框架过去提供的基于内部生命周期实现的非标准增强生命周期,如 beforeCreate/onBeforeCreate 等,直接将内部生命周期变量导出提供给用户使用,详情查看这里 (opens new window);
  • 为了优化 tree shaking,作为框架运行时 default exportMpx 对象不再挂载 createComponent/createStore 等运行时方法,一律通过 named export 提供,Mpx 对象上仅保留 set/use 等全局 API,详情查看这里 (opens new window)
  • 使用 I18n 能力时,为了与新版 vue-i18n 保持对齐,this.$i18n 对象指向全局作用域,如需创建局部作用域需要使用组合式 API useI18n 的方式进行创建。
  • watch API 不再接受第二个参数为带有 handler 属性的对象形式(该参数形式只应存在于 watch option 中),第二个参数必须为回调函数,与 Vue (opens new window) 对齐。

更详细的迁移指南请点击查看这里 (opens new window)

# 未来规划

在完成编译持久化缓存和组合式 API 支持后,我们已经完成了去年规划中最大的两个技术升级,后续我们的技术规划如下:

  • 支持使用 Vite 进行 web 构建
  • 完善 Mpx 跨端输出 Hummer 并正式 release
  • 优化运行时 render 函数,降低包体积占用
  • 内置支持原子类使用
  • Mpx-cube-ui 正式开源

最后,再次感谢所有参与 Mpx 组合式 API 技术建设的同学们,也欢迎社区同学加入 Mpx 项目开源共建。

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html b/docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html new file mode 100644 index 0000000000..8cbd7936d3 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html @@ -0,0 +1,277 @@ + + + + + + Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化 | Mpx框架 + + + + + + + + + +

# Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化

作者:董宏平 (opens new window)

Mpx (opens new window) 是滴滴开源的一款增强型跨端小程序框架,自2018年立项开源以来如今已经进入第六个年头,在这六年间,Mpx 根植于业务,与业务共同成长,针对小程序业务开发中遇到的各类痛点问题提出了解决方案,并在集团内部建设了完善的小程序跨端开发生态。目前,Mpx 已经覆盖支持了集团内部全量小程序业务开发,成为了集团小程序开发的统一技术标准,并在今年年初被评选为集团内首个开源精品项目。

随着小程序业务的发展演进,性能和包体积的重要性愈发凸显,Mpx从设计之初就非常重视性能和包体积的优化,本次的 Mpx2.9 版本更新带来的三大核心特性——原子类、SSR 和包体积优化也都与性能和包体积息息相关,下面我们逐个展开介绍。

# 原子类支持

原子类(utility-first CSS)是近几年流行起来的一种全新的样式开发方式,在前端社区内取得了良好的口碑,越来越多的主流网站也基于原子类进行开发,比较知名的有 Github (opens new window)OpenAI (opens new window)Netflix (opens new window)NASA官网 (opens new window) 等。使用原子类离不开原子类框架的支持,常用的原子类框架有 Tailwindcss (opens new window)Windicss (opens new window)Unocss (opens new window) 等。

在 Mpx2.9 版本中,我们在框架中内置了基于 unocss 的原子类支持,让小程序开发也能使用原子类。对项目进行简单配置开启原子类支持后,用户就可以在 Mpx 页面/组件模板中直接使用一些预定义的基础样式类,诸如 flex,pt-4,text-center 和 rotate-90 等,对样式进行组合定义,并且在 Mpx 支持的所有小程序平台和 web 平台中正常运行,下面是一个简单示例:

<view class="container">
+  <view class="flex">
+    <view class="py-8 px-8 inline-flex mx-auto bg-white rounded-xl shadow-md">
+      <view class="text-center">
+        <view class="text-base text-black font-semibold mb-2">
+          Erin Lindford
+        </view>
+        <view class="text-gray-500 font-medium pb-3">
+          Product Engineer
+        </view>
+        <view
+          class="mt-2 px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-solid border-purple-200">
+          Message
+        </view>
+      </view>
+    </view>
+  </view>
+</view>
+

通过这种方式,我们在不编写任何自定义样式代码的情况下得到了一张简单的个人卡片,实际渲染效果如下:

utility-css-demo

相较于传统的自定义类编写样式的方式,使用原子类能给你带来以下这些好处:

  • 不用再烦恼于为自定义类取类名,传统样式开发中,我们仅仅是为某个元素定义样式就需要绞尽脑汁发明一些抽象的类名,还得提防类名冲突,使用原子类可以完全将你从这种琐碎无趣的工作中解放;
  • 停止 css 体积的无序增长,传统样式开发中,css 体积会随着你的迭代不断增长,而在原子类中,一切样式都可以复用,你几乎不需要编写新的 css;
  • 让调整样式变得更加安全,传统 css 是全局的,当你修改某个样式时无法保障其不会破坏其他地方的样式,而你在模板中使用的原子类是本地的,你完全不用担心修改它会影响其他地方。

而相较于使用内联样式,原子类也有一些重要的优势:

  • 约束下的设计,使用内联样式时,里面的每一个数值都是魔法数字(magic number) +,而通过原子工具类,你可以选择一些符合预定义设计规范的样式,便于构筑具有视觉一致性的UI;
  • 响应式设计,你无法在内联样式中使用媒体查询,然而通过原子类框架中提供的响应式工具,你可以轻而易举地构建出响应式界面;
  • Hover、focus 和其他状态,使用内联样式无法定义特定状态下的样式,如 hover 和 focus,通过原子类框架的状态变量能力,我们可以轻松为这些状态定义样式。

看到这里相信你已经迫不及待地想要在 Mpx 中体验原子类开发了吧,使用最新版本的 @mpxjs/cli 脚手架创建项目时,在 prompt 中选择使用原子类,就可以在新创建的项目模版中直接使用 unocss 的原子类,可使用的工具类可以参考 unocss 交互示例 (opens new window),在已有项目中开启原子类支持可以参考配置指南 (opens new window)

# 小程序原子类使用注意事项

小程序和 web 环境对于 css 的支持存在底层差异,小程序内也存在大量自身独有的技术特性,Mpx 在支持原子类时针对这些环境特异性进行了抹平和适配,在框架的支持下,我们实现了大部分(超过90%)原子类功能和工具类在小程序环境下正常使用,并额外支持了原子类产物的分包输出和样式隔离下的原子类使用,详情如下:

# 特殊字符转义

基于 unocss 的原子类支持 value auto-infer(值自动推导),可以在模版中根据相关规则书写灵活的自定义值原子类,如 p-5px bg-[hsl(211.7,81.9%,69.6%)] 等,针对原子类中出现的 [ ( , 等特殊字符,在 web 中会通过转义字符 \ 进行转义,由于小程序环境下不支持 css 选择器中出现 \ 转义字符,我们内置支持了一套不带 \ 的转义规则对这些特殊字符进行转义,同时替换模版和 css 文件中的类名,内建的默认转义规则如下:

const escapeMap = {
+    '(': '_pl_',
+    ')': '_pr_',
+    '[': '_bl_',
+    ']': '_br_',
+    '{': '_cl_',
+    '}': '_cr_',
+    '#': '_h_',
+    '!': '_i_',
+    '/': '_s_',
+    '.': '_d_',
+    ':': '_c_',
+    ',': '_2c_',
+    '%': '_p_',
+    '\'': '_q_',
+    '"': '_dq_',
+    '+': '_a_',
+    $: '_si_',
+    // unknown用于兜底不在上述范围中未知的转义字符
+    unknown: '_u_'
+  }
+

与此同时,用户也可以通过传递 @mpxjs/unocss-pluginescapeMap (opens new window) 配置项来覆盖内建的转义规则。

# 原子类分包输出

在 web 中,原子类会被全部打包输出单个样式文件,一般会放置在顶层样式表中以供全局访问,但在小程序中这种全量的输出策略并不是最优的,主要原因在于小程序中可供全局访问的主包体积存在 2M 大小限制,主包体积十分紧缺珍贵,Mpx 在构建输出时遵循着分包优先的原则,尽可能充分利用分包体积从而减少对主包体积的占用,再进行原子类产物输出时,我们也遵循了相同的原则。

在Mpx中,我们在收集原子类时同时记录了每个原子类的引用分包,在收集结束后根据每个原子类的分包引用数量决定该原子类应该输出到主包还是分包当中,我们在 @mpxjs/unocss-plugin 中提供了 minCount (opens new window) 配置项来决定分包的输出规则,该配置项的默认值为2,即当一个原子类被2个或以上分包引用时,会被作为公共原子类抽取到主包中,否则输出到所属分包中,这也是全局最优的策略。当我们想要让原子类输出产物更少地占用主包体积时,我们也可以将minCount值调大,让原子类抽取到主包的条件更加苛刻,不过这样也会伴随着原子类分包冗余的增加。

unocss.config.js 配置中定义的 safelist 原子类默认会输出到主包,为了组件局部使用的 safelist 有输出到分包的机会,我们在模版中提供了注释配置 (opens new window)(comments config),灵感来源于 webpack 中的魔法注释(magic comments),用户可以在组件模版中通过注释配置声明当前组件所需的 safelist,对应的原子类也会根据上述的规则输出到主包或分包中,使用示例如下:

<template>
+    <!-- mpx_config_safelist: 'text-red-500 bg-blue-500' -->
+    <!-- 动态样式中可以使用text-red-500和bg-blue-500原子类 -->
+    <view wx:class="{{classObj}}">test</view>
+    <!-- ... -->
+</template>
+

# 样式隔离与组件分包异步

在小程序中,自定义组件的样式默认是隔离的,web 中通过全局样式访问原子类的方式不再生效,不过由于小程序提供了样式隔离配置 (opens new window),我们可以将该组件样式隔离配置调整为 apply-shared 来获取页面或 app 中定义的原子类,但是当我们在使用传统类名和原子类混合开发或者迁移原子类的过程中,我们往往希望保留原本自定义组件的样式隔离。

针对这种情况,我们在 @mpxjs/unocss-plugin 中提供了 styleIsolation (opens new window) 配置项,可选设置为 isolated|apply-shared,当设置为 isolated 时每个组件都会通过 @import 独立引用主包或者分包的原子类样式文件,因此不会受到样式隔离的影响;当设置为 apply-shared 时,只有 app 和分包页面会引用对应的原子类样式文件,自定义组件需要通过配置样式隔离为 apply-shared 使原子类生效。

在组件分包异步的情况下对应组件即使将样式隔离配置为 apply-shared 的情况下,@mpxjs/unocss-plugin 也需要将 styleIsolation 设置为 isolated 才能正常工作,原因在于组件分包异步的情况下,组件被其他分包的页面所引用渲染,由于上述原子类样式分包输出的规则,其他分包的页面中可能并不包含当前组件所需的原子类,只有在 isolated 模式下由组件自身引用所需的原子类样式才能保证正常工作,类似于 safelist,我们也提供了注释配置 (opens new window)的方式对组件的 styleIsolation 模式进行局部配置,示例如下:

<template>
+    <!-- mpx_config_styleIsolation: 'isolated' -->
+    <!-- 当前组件会直接引用对应主包或分包的原子类样式 -->
+     <view class="@dark:(text-white bg-dark-500)">
+    <!-- ... -->
+</template>
+

# 原子类使用参考文档

Mpx 中使用原子类的详细参考文档如下:

# 输出 web 支持 SSR

近些年来,SSR/SSG 由于其良好的首屏展现速度和SEO友好性逐渐成为主流的技术规范,各类SSR框架层出不穷,未来进一步提升性能表现,在 SSR 的基础上还演进出 islands architecture (opens new window)0 hydration (opens new window) 等更加精细复杂的理念和架构。

近两年随着团队对于前端性能的重视,SSR/SSG 技术也在团队业务中逐步推广落地,并在首屏性能方面取得了显著的收效。但由于过去 Mpx 对 SSR 的支持不完善,使用 Mpx 开发的跨端页面一直无法享受到 SSR 带来的性能提升,在 Mpx2.9 版本中,我们对 web 输出流程进行了大量适配改造,解决了 SSR 中常见的内存泄漏、跨请求状态污染和数据预请求等问题,完整实现了基于 Vue 和 Pinia 的 SSR 技术方案。

# 配置使用 SSR

在 Vue SSR 项目中,我们一般需要提供 server entry 和 client entry 两个文件作为 webpack 的构建入口,然后通过 webpack 构建出 server bundle 和 client bundle。在用户访问页面时,在服务端使用 server bundle 渲染出 HTML 字符串,作为静态页面发送到客户端,然后在客户端使用 client bundle 通过水合(hydration)对静态页面进行激活,实现可交互效果,下图展示了 Vue SSR 的大致流程。

Vue SSR流程

Mpx SSR 核心基于 Vue SSR 实现,大致流程思路与 Vue 一致,不过为了保持与小程序代码的兼容性,在配置使用上有一些改动差异,下面我们详细展开介绍:

# 构建server/client bundle

SSR项目中,我们需要分别构建出 server bundle 和 client bundle,对于不同环境的产物构建,我们需要进行不同的配置。 +在 Vue 中,我们需要提供 entry-server.jsentry-client.js 两个文件分别作为 server 和 client 的构建入口,与 Vue 不同,在 Mpx 中我们通过编译处理与运行时增强生命周期实现了使用 app.mpx 作为统一构建入口,无需区分 server 和 client。

# 服务端构建配置

服务端配置中除了将 entry 制定为 app.mpx 及其它基础配置外,最重要的是安装 vue-server-renderer 包中提供的 server-plugin 插件,该插件能够构建产出 vue-ssr-server-bundle.json 文件供 renderer 后续消费。

// webpack.server.config.js
+const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
+
+module.exports = merge(baseConfig, {
+  // 将 entry 指向项目的 app.mpx 文件
+  entry: './app.mpx',
+  // ...
+  plugins: [
+   // 产出 `vue-ssr-server-bundle.json`
+    new VueSSRServerPlugin()
+  ]
+})
+

更加详细的配置说明可参考 Vue SSR的服务端配置 (opens new window)

# 客户端构建配置

类似服务端构建配置,在客户端构建中我们需要使用 vue-server-renderer 包中 client-plugin 插件来帮助我们生成客户端环境的资源清单 vue-ssr-client-manifest.json,并供 renderer 后续消费。

// webpack.client.config.js
+const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
+
+module.exports = merge(baseConfig, {
+  // 将 entry 指向项目的 app.mpx 文件
+  entry: './app.mpx',
+  // ...
+  plugins: [
+    // 产出 `vue-ssr-client-manifest.json`。
+    new VueSSRClientPlugin()
+  ]
+})
+

更加详细的配置说明可参考 Vue SSR的客户端配置 (opens new window)

# 准备页面模版

SSR 渲染中,我们创建 renderer 需要一个页面模板,简单的示例如下:

<!DOCTYPE html>
+<html lang="en">
+  <head><title>Hello</title></head>
+  <body>
+    <!--vue-ssr-outlet-->
+  </body>
+</html>
+

与 CSR 渲染模版不同,SSR 渲染模版中需要提供一个特殊的 <!--vue-ssr-outlet-->注释,标记 SSR 渲染产物的插入位置,如使用 @mpxjs/cli 脚手架创建 SSR 项目,该模版已经内置于脚手架中。

# 集成启动 SSR 服务

当我们准备好页面模版和双端构建产物后,我们就可以创建 renderer 并与 Node 服务进行集成,启动 SSR 服务,下面以 express 为例:

//server.js
+const app = require('express')()
+const { createBundleRenderer } = require('vue-server-renderer')
+// 通过 vue-server-renderer/server-plugin 生成的文件
+const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
+// 通过 vue-server-renderer/client-plugin 生成的文件
+const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
+ // 页面模版文件
+const template = require('fs').readFileSync('../src/index.template.html', 'utf-8')
+// 创建 renderer 渲染器
+const renderer = createBundleRenderer(serverBundle, {
+    runInNewContext: false,
+    template,
+    clientManifest,
+});
+// 注册启动 SSR 服务
+app.get('*', (req, res) => {
+  const context = { url: req.url }
+  renderer.renderToString(context, (err, html) => {
+  	if (err) {
+  	  res.status(500).end('Internal Server Error')
+      return
+    }
+  	res.end(html);
+  })
+})
+app.listen(8080)
+

# SSR 生命周期

在 Mpx 2.9 的版本中,我们提供了三个全新用于 SSR 的生命周期,分别是onAppInitserverPrefetchonSSRAppCreated,以统一服务端与客户端的构建入口,下面展开介绍:

# onAppInit

在 SSR 中用户每发出一个请求,我们都会为其生成一个新的应用实例,onAppInit 生命周期会在应用创建 new Vue(...) 前被调用,其执行的返回结果会被合并到创建应用的 options

很常见的使用场景在于返回新的全局状态管理实例,Mpx 中提供了 @mpxjs/pinia 作为全局状态管理工具,我们可以在 onAppInit 中返回全新的 pinia 示例避免产生跨请求状态污染,示例如下:

// app.mpx
+import mpx, { createApp } from '@mpxjs/core'
+import { createPinia } from '@mpxjs/pinia'
+
+createApp({
+  // ...
+  onAppInit () {
+    const pinia = createPinia()
+    return {
+      pinia
+    }
+  }
+})
+

# serverPrefetch

当我们需要在 SSR 使用数据预拉取时,可以使用这个生命周期进行,使用方法与 Vue 一致, 示例如下:

选项式 API:

import { createPage } from '@mpxjs/core'
+import useStore from '../store/index'
+
+createPage({
+  //...
+  serverPrefetch () {
+    const query = this.$route.query
+    const store = useStore(this.$pinia)
+    // return the promise from the action, fetch data and update state
+    return store.fetchData(query)
+  }
+})
+

组合式 API:

import { onServerPrefetch, getCurrentInstance, createPage } from '@mpxjs/core'
+import useStore from '../store/index'
+
+createPage({
+  setup () {
+    const store = userStore()
+    onServerPrefetch(() => {
+      const query = getCurrentInstance().proxy.$route.query
+      // return the promise from the action, fetch data and update state
+      return store.fetchData(query)
+    })
+  }
+})
+

关于数据预拉取更详细的说明可以查看这里 (opens new window)

# onSSRAppCreated

在 Vue SSR 项目中,我们会在 entry-server.js 中导出一个工厂函数,在该函数中实现创建应用实例、路由匹配和状态同步等逻辑,并返回应用实例 app

在 Mpx SSR 中,我们将这部分逻辑整合在 onSSRAppCreated 中执行,该生命周期执行时用户可以从参数中获取应用实例 app、路由实例 router、数据管理实例 pinia 和 SSR 上下文 context,在完成必要的操作后,该生命周期需要返回一个 resolve(app) 的 promise。

通常我们会在 onSSRAppCreated 中进行路由路径设置和数据预拉取后的状态同步工作,示例如下:

// app.mpx
+createApp({
+    // ...,
+    onSSRAppCreated ({ pinia, router, app, context }) {
+      return new Promise((resolve, reject) => {
+        // 设置服务端路由路径
+        router.push(context.url)
+        router.onReady(() => {
+          // 应用完成渲染时执行
+          context.rendered = () => {
+            // 将服务端渲染后得到的 pinia.state 同步到 context.state 中,
+            // context.state 会被自动序列化并通过 `window.__INITIAL_STATE__`
+            // 注入到 HTML 中,并在客户端运行时再读取同步
+            context.state = pinia.state.value
+          }
+          // 返回应用程序实例
+          resolve(app)
+        }, reject)
+      })
+    }
+})
+

如用户没有配置 onSSRAppCreated,框架内部会执行兜底逻辑,以保障 SSR 的正常运行。

# 其他注意事项

  1. Mpx SSR 渲染支持 i18n 的功能,但为了防止内存泄漏,当前 i18n 实例不会随着每次请求而重新创建,这是由于 Vue2.x 版本插件机制的设计缺陷所造成的,因此在使用 i18n 进行 SSR 时可能会产生跨请求状态污染的问题,这个问题会在未来 Mpx 输出 web 切换为 Vue3 后完全解决。

  2. 在服务端渲染阶段,对于 global 全局对象访问修改,如__mpx, __mpxRouter, __mpxPinia 都可能导致全局状态污染,所以在服务端渲染阶段请尽量避免进行相关操作;对于存在全局访问修改的方法,如 getApp(), getCurrentPages() 等在服务端渲染中被调用时,会产生相关报错提示。

  3. 由于服务器无法收到 URL 中的 hash 信息,使用 SSR 时需要通过修改 mpx.config.webRouteConfig 将路由模式设置成 history 模式。

# 包体积优化

近几年来随着集团超级 App 战略的推进,滴滴出行主小程序中集成了越来越多的业务线,主小程序的包体积也随之呈现爆炸式增长,到今天主小程序里已经集成了集团内大部分核心业务,但其总包体积也从 21 年的 12MB 增长至触达微信上限 30MB,成为一个页面数量400+,组件数量3600+,JS模块数40000+的“大程序”。在此期间,我们通过统一技术框架、分包异步改造和低效页面下线/改造等技术手段,对主小包体积进行一轮又一轮优化,并沉淀了分包异步构建、包体积分析管控和冗余包检测等一系列技术能力,累计优化总包体积超过 8MB

Mpx 在设计之初就考虑了包体积问题,完整继承了 webpack 已有的代码优化能力,如公共模块抽离、tree shaking、混淆压缩等。除此之外,我们还设计开发了业内最完善的分包构建流程,完整支持基于配置声明和依赖分析的普通分包、独立分包及分包异步化的构建输出,极大地缓解了复杂小程序中的主包和总包体积过大的问题。在 Mpx2.9 版本中,我们针对复杂小程序页面组件和 JS 模块众多的特点,对构建产物进行了针对性地优化,进一步降低了构建产物体积,当前版本在主小程序中实测能够在不做任何业务改造的情况下节省了约 2MB 总包体积,收效显著,主要优化项如下:

# 模版代码优化

Mpx 基于 webpack 进行打包构建,并基于小程序原生 commonjs 支持进行适配改造,以实现 webpack 构建产物在小程序环境下运行,为了将构建产物中的模块和 chunk 链接到一起,webpack 和 mpx 会在构建产物生成一些模版代码进行相关工作,下面是一个页面 js 中包含的模版代码示例:

var self = {}
+self.webpackChunkmpx_test_2_8 = require('../bundle.js')
+(self.webpackChunkmpx_test_2_8 = self.webpackChunkmpx_test_2_8 || []).push([[405], {
+  1307: function (t, e, o) {
+    // ...
+  },
+  4236: function (t) {
+    // ...
+  },
+  // ...
+}, function (t) {
+  var e
+  e = 1307, t(t.s = e)
+}])
+

可以看出模版代码主要由 chunk 链接代码,chunk id,模块 id 和模块包装函数组成,对于中小项目来说,这些模版代码一般不会占用太多体积,但是在滴滴出行主小程序这样的大体量项目下,这部分体积就变得不可忽视,@mpxjs/size-report 的分析结果显示,生产模式下主小程序中模版代码占用体积约为 1.2MB,相当于一个中等复杂度业务的占用体积。

我们对模版代码的生成逻辑和生成产物进行分析,发现 webpack 生产模式的默认配置中,很多配置项并不是体积最优的选项,一个典型的例子在于模块/chunk id:为了保障生成产物的内容稳定,来尽可能提升浏览器的缓存利用率,webpack 默认的模块/chunk id 采用 deterministic 模式进行生成,该模式下模块 id 为模块源路径的定长数字 hash,比项目模块总数长 1 位。由于在小程序中代码包按照版本的维度进行全量管理,保证文件局部的内容稳定在小程序环境下无正向意义,这就有了优化空间,我们可以简单将模块/chunk id 的生成逻辑改为数字自增,在主小程序中就能节省出上百 KB 总包体积。

类似的可优化点还存在于 chunk 链接代码和模块包装函数当中,我们在 @mpxjs/webpack-plugin@2.9 版本中提供了一个新的配置项 optimizeSize (opens new window),其中整合了一系列模版代码体积优化配置,开启后就能自动优化构建产物中的模版代码体积,在主小程序中,我们开启 optimizeSize 后可以减少总包体积约 540KB,效果非常显著,下面是上述示例在开启 optimizeSize 后的产物对比:

var g = {}
+g.c = require('../bundle.js'), (g.c = g.c || []).push([[8], {
+  448: function (t, e, o) {
+    // ...
+  },
+  463: function (t) {
+    // ...
+  }
+  // ...
+}, function (t) {
+  var e
+  e = 448, t(t.s = e)
+}])
+

# 空模块移除

Mpx 在处理 .mpx 单文件时会将其分拆为 template/js/style/json 四个新模块来进行分别处理,其中 template/style/json 部分的内容会在处理后通过内置的 extractor 抽取输出为静态文件,抽取之后原本分拆出来的模块就会以空模块的形式残留在构建产物中(template 模块在抽取后还需要保留 render 函数和 refs 等信息所以不会成为空模块),如下所示:

/***/ 533:
+/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
+// ...
+/* template */
+__webpack_require__(535)
+/* styles */
+__webpack_require__(536)
+/* json */
+__webpack_require__(537)
+/* script */
+__webpack_require__(534)
+/***/ }),
+/***/ 536:
+/***/ (function() {
+/***/ }),
+/***/ 537:
+/***/ (function() {
+/***/ })
+

从上面示例中可以看出 536/537 模块的定义和引用都是完全不必要的,但由于 webpack 本身对于空模块没有进行识别和优化的手段,在过去的版本中这些空模块代码会占用我们一部分总包体积。在 Mpx2.9 版本中,我们定义了全新的依赖类型 CommonjsExtractDependency 对于这类可能被抽取成为空模块的 request 进行识别处理,当模块内容在完成抽取后为空时,自动将其从模块图中移除,并在产物生成时不再生成其引用代码。该项优化措施内置开启,升级后自动生效,可减少主小程序总包体积约 232KB

# Render函数优化

Render 函数是 Mpx 运行时 setData 优化的一项核心设计,我们在模版编译时将用户模版转换为简易的 render 函数,该函数执行时能够完全模拟视图渲染的数据访问过程,正确地收集当前视图的数据依赖,避免将视图不需要的数据通过 setData 发送到视图中。

render函数

虽然我们生成的简化版 render 函数仅保留了数据访问逻辑,在代码体积上并不算大,但是仍然存在着一定的优化空间,我们来看下面这个例子:

function render(){
+  this.a
+  if(this.c){
+    this.a
+    this.b
+    this.c.a
+    this.c.b
+    this.d
+  }
+  this.b
+}
+

可以看出 if block 下存在着大量冗余不必要的数据访问,例如 this.athis.b 在父级 block 中已经进行过访问,this.c.athis.c.b 由于在 if condition 中进行过 this.c 的访问也不再必要(render函数执行时会对模版依赖数据进行深度diff,父路径访问后子路径就无需再进行访问),上述 render 函数可以优化为:

function render(){
+  this.a
+  if(this.c){
+    this.d
+  }
+  this.b
+}
+

在 Mpx2.9 版本中,我们通过两轮 ast 遍历(2 pass ast travese)的方式实现了这个优化,在第一轮遍历中,我们收集了数据访问信息并按照 block 结构存储在 blockVisitTree 中,第二轮遍历时依据 blockVisitTree 中的信息对不必要的数据访问进行剪枝优化。

除此之外,我们也大幅优化了 render 函数中的数据收集代码,有效地降低了 render 函数的体积占用。

该优化目前没有默认开启,可以通过 @mpxjs/webpack-plugin 中的 optimizeRenderRules (opens new window) 配置项配置生效范围进行开启, +全量开启后在主小程序中实测可节省总包体积约 1507KB

# 未来规划

在未来,我们对 Mpx 构建产物还有进一步的优化方案可以探索实施,主要分为两个方向:

  • 编译注入代码优化,在功能不变的情况下简化编译注入的代码,对于 render 函数也有一种运行时有损的方案(略微增大运行时开销)可以尝试
  • 模版和JSON压缩,对模版和JSON中的组件名及组件路径进行短哈希压缩,缺点是丢失dist产物的可读性,可能需要对其提供 sourceMap 支持

除此之外,我们近期的框架升级计划还包括:

  • 局部运行时渲染增强
  • 数据响应升级为 proxy 实现,输出 web 升级为 vue3
  • 支付宝 2.0 基础库适配优化
  • 微信 skyline 适配优化
  • 输出 Hummer 能力完善

在构建提速方面,我们也会在以下方向进行探索:

  • 输出 web 支持 vite 构建
  • 基于模块联邦的分布式构建
  • 基于 Rust 的高性能构建探索

最后,特别感谢@徐伟东、@闫宇和@朱志恒对于 2.9 版本功能开发的突出贡献,也欢迎大家使用 Mpx 进行小程序跨端开发并加入社区共建。

Github:https://github.com/didi/mpx

官网/文档:https://mpxjs.cn/

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/2.9-release.html b/docs-vuepress/.vuepress/dist/articles/2.9-release.html new file mode 100644 index 0000000000..f17db1933f --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/2.9-release.html @@ -0,0 +1,285 @@ + + + + + + Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化 | Mpx框架 + + + + + + + + + +

# Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化

作者:hiyuki (opens new window)

Mpx (opens new window) 是滴滴开源的一款增强型跨端小程序框架,自2018年立项开源以来如今已经进入第六个年头,在这六年间,Mpx 根植于业务,与业务共同成长,针对小程序业务开发中遇到的各类痛点问题提出了解决方案,并在集团内部建设了完善的小程序跨端开发生态。目前,Mpx 已经覆盖支持了集团内部全量小程序业务开发,成为了集团小程序开发的统一技术标准。

随着小程序业务的发展演进,性能和包体积的重要性愈发凸显,Mpx从设计之初就非常重视性能和包体积的优化,本次的 Mpx2.9 版本更新带来的三大核心特性——原子类、SSR 和包体积优化也都与性能和包体积息息相关,下面我们逐个展开介绍。

# 原子类支持

原子类(utility-first CSS)是近几年流行起来的一种全新的样式开发方式,在前端社区内取得了良好的口碑,越来越多的主流网站也基于原子类进行开发,比较知名的有 Github (opens new window)OpenAI (opens new window)Netflix (opens new window)NASA官网 (opens new window) 等。使用原子类离不开原子类框架的支持,常用的原子类框架有 Tailwindcss (opens new window)Windicss (opens new window)Unocss (opens new window) 等。

在 Mpx2.9 版本中,我们在框架中内置了原子类支持,让小程序开发也能使用原子类。对项目进行简单配置开启原子类支持后,用户就可以在 Mpx 页面/组件模板中直接使用一些预定义的基础样式类,诸如 flex,pt-4,text-center 和 rotate-90 等,对样式进行组合定义,并且在 Mpx 支持的所有小程序平台和 web 平台中正常运行,下面是一个简单示例:


+<view class="container">
+  <view class="flex">
+    <view class="py-8 px-8 inline-flex mx-auto bg-white rounded-xl shadow-md">
+      <view class="text-center">
+        <view class="text-base text-black font-semibold mb-2">
+          Erin Lindford
+        </view>
+        <view class="text-gray-500 font-medium pb-3">
+          Product Engineer
+        </view>
+        <view class="mt-2 px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-solid border-purple-200">
+          Message
+        </view>
+      </view>
+    </view>
+  </view>
+</view>
+

通过这种方式,我们在不编写任何自定义样式代码的情况下得到了一张简单的个人卡片,实际渲染效果如下:

utility-css-demo

相较于传统的自定义类编写样式的方式,使用原子类能给你带来以下这些好处:

  • 不用再烦恼于为自定义类取类名,传统样式开发中,我们仅仅是为某个元素定义样式就需要绞尽脑汁发明一些抽象的类名,还得提防类名冲突,使用原子类可以完全将你从这种琐碎无趣的工作中解放;
  • 停止 css 体积的无序增长,传统样式开发中,css 体积会随着你的迭代不断增长,而在原子类中,一切样式都可以复用,你几乎不需要编写新的 css;
  • 让调整样式变得更加安全,传统 css 是全局的,当你修改某个样式时无法保障其不会破坏其他地方的样式,而你在模板中使用的原子类是本地的,你完全不用担心修改它会影响其他地方。

而相较于使用内联样式,原子类也有一些重要的优势:

  • 约束下的设计,使用内联样式时,里面的每一个数值都是魔法数字(magic number) +,而通过原子工具类,你可以选择一些符合预定义设计规范的样式,便于构筑具有视觉一致性的UI;
  • 响应式设计,你无法在内联样式中使用媒体查询,然而通过原子类框架中提供的响应式工具,你可以轻而易举地构建出响应式界面;
  • Hover、focus 和其他状态,使用内联样式无法定义特定状态下的样式,如 hover 和 focus,通过原子类框架的状态变量能力,我们可以轻松为这些状态定义样式。

看到这里相信你已经迫不及待地想要在 Mpx 中体验原子类开发了吧,使用最新版本的 @mpxjs/cli 脚手架创建项目时,在 prompt 中选择使用原子类,就可以在新创建的项目模版中直接使用原子类,可使用的工具类可以参考 交互示例 (opens new window),在已有项目中开启原子类支持可以参考配置指南 (opens new window)

# 小程序原子类使用注意事项

小程序和 web 环境对于 css 的支持存在底层差异,小程序内也存在大量自身独有的技术特性,Mpx 在支持原子类时针对这些环境特异性进行了抹平和适配,在框架的支持下,我们实现了大部分(超过90%)原子类功能和工具类在小程序环境下正常使用,并额外支持了原子类产物的分包输出和样式隔离下的原子类使用,详情如下:

# 特殊字符转义

现代原子类框架支持 value auto-infer(值自动推导),可以在模版中根据相关规则书写灵活的自定义值原子类,如 p-5px bg-[hsl(211.7,81.9%,69.6%)] 等,针对原子类中出现的 [ ( , 等特殊字符,在 web 中会通过转义字符 \ 进行转义,由于小程序环境下不支持 css 选择器中出现 \ 转义字符,我们内置支持了一套不带 \ 的转义规则对这些特殊字符进行转义,同时替换模版和 css 文件中的类名,内建的默认转义规则如下:

const escapeMap = {
+  '(': '_pl_',
+  ')': '_pr_',
+  '[': '_bl_',
+  ']': '_br_',
+  '{': '_cl_',
+  '}': '_cr_',
+  '#': '_h_',
+  '!': '_i_',
+  '/': '_s_',
+  '.': '_d_',
+  ':': '_c_',
+  ',': '_2c_',
+  '%': '_p_',
+  '\'': '_q_',
+  '"': '_dq_',
+  '+': '_a_',
+  $: '_si_',
+  // unknown用于兜底不在上述范围中未知的转义字符
+  unknown: '_u_'
+}
+

与此同时,用户也可以通过传递 @mpxjs/unocss-pluginescapeMap (opens new window) 配置项来覆盖内建的转义规则。

# 原子类分包输出

在 web 中,原子类会被全部打包输出单个样式文件,一般会放置在顶层样式表中以供全局访问,但在小程序中这种全量的输出策略并不是最优的,主要原因在于小程序中可供全局访问的主包体积存在 2M 大小限制,主包体积十分紧缺珍贵,Mpx 在构建输出时遵循着分包优先的原则,尽可能充分利用分包体积从而减少对主包体积的占用,再进行原子类产物输出时,我们也遵循了相同的原则。

在Mpx中,我们在收集原子类时同时记录了每个原子类的引用分包,在收集结束后根据每个原子类的分包引用数量决定该原子类应该输出到主包还是分包当中,我们在 @mpxjs/unocss-plugin 中提供了 minCount (opens new window) 配置项来决定分包的输出规则,该配置项的默认值为2,即当一个原子类被2个或以上分包引用时,会被作为公共原子类抽取到主包中,否则输出到所属分包中,这也是全局最优的策略。当我们想要让原子类输出产物更少地占用主包体积时,我们也可以将minCount值调大,让原子类抽取到主包的条件更加苛刻,不过这样也会伴随着原子类分包冗余的增加。

unocss.config.js 配置中定义的 safelist 原子类默认会输出到主包,为了组件局部使用的 safelist 有输出到分包的机会,我们在模版中提供了注释配置 (opens new window)(comments config),灵感来源于 webpack 中的魔法注释(magic comments),用户可以在组件模版中通过注释配置声明当前组件所需的 safelist,对应的原子类也会根据上述的规则输出到主包或分包中,使用示例如下:

<template>
+  <!-- mpx_config_safelist: 'text-red-500 bg-blue-500' -->
+  <!-- 动态样式中可以使用text-red-500和bg-blue-500原子类 -->
+  <view wx:class="{{classObj}}">test</view>
+  <!-- ... -->
+</template>
+

# 样式隔离与组件分包异步

在小程序中,自定义组件的样式默认是隔离的,web 中通过全局样式访问原子类的方式不再生效,不过由于小程序提供了样式隔离配置 (opens new window),我们可以将该组件样式隔离配置调整为 apply-shared 来获取页面或 app 中定义的原子类,但是当我们在使用传统类名和原子类混合开发或者迁移原子类的过程中,我们往往希望保留原本自定义组件的样式隔离。

针对这种情况,我们在 @mpxjs/unocss-plugin 中提供了 styleIsolation (opens new window) 配置项,可选设置为 isolated|apply-shared,当设置为 isolated 时每个组件都会通过 @import 独立引用主包或者分包的原子类样式文件,因此不会受到样式隔离的影响;当设置为 apply-shared 时,只有 app 和分包页面会引用对应的原子类样式文件,自定义组件需要通过配置样式隔离为 apply-shared 使原子类生效。

在组件分包异步的情况下对应组件即使将样式隔离配置为 apply-shared 的情况下,@mpxjs/unocss-plugin 也需要将 styleIsolation 设置为 isolated 才能正常工作,原因在于组件分包异步的情况下,组件被其他分包的页面所引用渲染,由于上述原子类样式分包输出的规则,其他分包的页面中可能并不包含当前组件所需的原子类,只有在 isolated 模式下由组件自身引用所需的原子类样式才能保证正常工作,类似于 safelist,我们也提供了注释配置 (opens new window)的方式对组件的 styleIsolation 模式进行局部配置,示例如下:

<template>
+  <!-- mpx_config_styleIsolation: 'isolated' -->
+  <!-- 当前组件会直接引用对应主包或分包的原子类样式 -->
+  <view class="@dark:(text-white bg-dark-500)">
+    <!-- ... -->
+</template>
+

# 原子类使用参考文档

Mpx 中使用原子类的详细参考文档如下:

# 输出 web 支持 SSR

近些年来,SSR/SSG 由于其良好的首屏展现速度和SEO友好性逐渐成为主流的技术规范,各类SSR框架层出不穷,未来进一步提升性能表现,在 SSR 的基础上还演进出 islands architecture (opens new window)0 hydration (opens new window) 等更加精细复杂的理念和架构。

近两年随着团队对于前端性能的重视,SSR/SSG 技术也在团队业务中逐步推广落地,并在首屏性能方面取得了显著的收效。但由于过去 Mpx 对 SSR 的支持不完善,使用 Mpx 开发的跨端页面一直无法享受到 SSR 带来的性能提升,在 Mpx2.9 版本中,我们对 web 输出流程进行了大量适配改造,解决了 SSR 中常见的内存泄漏、跨请求状态污染和数据预请求等问题,完整实现了 SSR 技术方案。

# 配置使用 SSR

在 Vue SSR 项目中,我们一般需要提供 server entry 和 client entry 两个文件作为 webpack 的构建入口,然后通过 webpack 构建出 server bundle 和 client bundle。在用户访问页面时,在服务端使用 server bundle 渲染出 HTML 字符串,作为静态页面发送到客户端,然后在客户端使用 client bundle 通过水合(hydration)对静态页面进行激活,实现可交互效果,下图展示了 Vue SSR 的大致流程。

Vue SSR流程

Mpx SSR 实现的大致流程思路与 Vue 一致,不过为了保持与小程序代码的兼容性,在配置使用上有一些改动差异,下面我们详细展开介绍:

# 构建server/client bundle

SSR项目中,我们需要分别构建出 server bundle 和 client bundle,对于不同环境的产物构建,我们需要进行不同的配置。 +在 Vue 中,我们需要提供 entry-server.jsentry-client.js 两个文件分别作为 server 和 client 的构建入口,与 Vue 不同,在 Mpx 中我们通过编译处理与运行时增强生命周期实现了使用 app.mpx 作为统一构建入口,无需区分 server 和 client。

# 服务端构建配置

服务端配置中除了将 entry 制定为 app.mpx 及其它基础配置外,最重要的是安装 vue-server-renderer 包中提供的 server-plugin 插件,该插件能够构建产出 vue-ssr-server-bundle.json 文件供 renderer 后续消费。

// webpack.server.config.js
+const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
+
+module.exports = merge(baseConfig, {
+  // 将 entry 指向项目的 app.mpx 文件
+  entry: './app.mpx',
+  // ...
+  plugins: [
+   // 产出 `vue-ssr-server-bundle.json`
+    new VueSSRServerPlugin()
+  ]
+})
+

更加详细的配置说明可参考 Vue SSR的服务端配置 (opens new window)

# 客户端构建配置

类似服务端构建配置,在客户端构建中我们需要使用 vue-server-renderer 包中 client-plugin 插件来帮助我们生成客户端环境的资源清单 vue-ssr-client-manifest.json,并供 renderer 后续消费。

// webpack.client.config.js
+const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
+
+module.exports = merge(baseConfig, {
+  // 将 entry 指向项目的 app.mpx 文件
+  entry: './app.mpx',
+  // ...
+  plugins: [
+    // 产出 `vue-ssr-client-manifest.json`。
+    new VueSSRClientPlugin()
+  ]
+})
+

更加详细的配置说明可参考 Vue SSR的客户端配置 (opens new window)

# 准备页面模版

SSR 渲染中,我们创建 renderer 需要一个页面模板,简单的示例如下:

<!DOCTYPE html>
+<html lang="en">
+  <head><title>Hello</title></head>
+  <body>
+    <!--vue-ssr-outlet-->
+  </body>
+</html>
+

与 CSR 渲染模版不同,SSR 渲染模版中需要提供一个特殊的 <!--vue-ssr-outlet-->注释,标记 SSR 渲染产物的插入位置,如使用 @mpxjs/cli 脚手架创建 SSR 项目,该模版已经内置于脚手架中。

# 集成启动 SSR 服务

当我们准备好页面模版和双端构建产物后,我们就可以创建 renderer 并与 Node 服务进行集成,启动 SSR 服务,下面以 express 为例:

//server.js
+const app = require('express')()
+const { createBundleRenderer } = require('vue-server-renderer')
+// 通过 vue-server-renderer/server-plugin 生成的文件
+const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
+// 通过 vue-server-renderer/client-plugin 生成的文件
+const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
+ // 页面模版文件
+const template = require('fs').readFileSync('../src/index.template.html', 'utf-8')
+// 创建 renderer 渲染器
+const renderer = createBundleRenderer(serverBundle, {
+    runInNewContext: false,
+    template,
+    clientManifest,
+});
+// 注册启动 SSR 服务
+app.get('*', (req, res) => {
+  const context = { url: req.url }
+  renderer.renderToString(context, (err, html) => {
+    if (err) {
+      res.status(500).end('Internal Server Error')
+      return
+    }
+    res.end(html);
+  })
+})
+app.listen(8080)
+

# SSR 生命周期

在 Mpx 2.9 的版本中,我们提供了三个全新用于 SSR 的生命周期,分别是onAppInitserverPrefetchonSSRAppCreated,以统一服务端与客户端的构建入口,下面展开介绍:

# onAppInit

在 SSR 中用户每发出一个请求,我们都会为其生成一个新的应用实例,onAppInit 生命周期会在应用创建 new Vue(...) 前被调用,其执行的返回结果会被合并到创建应用的 options

很常见的使用场景在于返回新的全局状态管理实例,Mpx 中提供了 @mpxjs/pinia 作为全局状态管理工具,我们可以在 onAppInit 中返回全新的 pinia 示例避免产生跨请求状态污染,示例如下:

// app.mpx
+import mpx, { createApp } from '@mpxjs/core'
+import { createPinia } from '@mpxjs/pinia'
+
+createApp({
+  // ...
+  onAppInit () {
+    const pinia = createPinia()
+    return {
+      pinia
+    }
+  }
+})
+

# serverPrefetch

当我们需要在 SSR 使用数据预拉取时,可以使用这个生命周期进行,使用方法与 Vue 一致, 示例如下:

选项式 API:

import { createPage } from '@mpxjs/core'
+import useStore from '../store/index'
+
+createPage({
+  //...
+  serverPrefetch () {
+    const query = this.$route.query
+    const store = useStore(this.$pinia)
+    // return the promise from the action, fetch data and update state
+    return store.fetchData(query)
+  }
+})
+

组合式 API:

import { onServerPrefetch, getCurrentInstance, createPage } from '@mpxjs/core'
+import useStore from '../store/index'
+
+createPage({
+  setup () {
+    const store = userStore()
+    onServerPrefetch(() => {
+      const query = getCurrentInstance().proxy.$route.query
+      // return the promise from the action, fetch data and update state
+      return store.fetchData(query)
+    })
+  }
+})
+

关于数据预拉取更详细的说明可以查看这里 (opens new window)

# onSSRAppCreated

在 Vue SSR 项目中,我们会在 entry-server.js 中导出一个工厂函数,在该函数中实现创建应用实例、路由匹配和状态同步等逻辑,并返回应用实例 app

在 Mpx SSR 中,我们将这部分逻辑整合在 onSSRAppCreated 中执行,该生命周期执行时用户可以从参数中获取应用实例 app、路由实例 router、数据管理实例 pinia 和 SSR 上下文 context,在完成必要的操作后,该生命周期需要返回一个 resolve(app) 的 promise。

通常我们会在 onSSRAppCreated 中进行路由路径设置和数据预拉取后的状态同步工作,示例如下:

// app.mpx
+createApp({
+    // ...,
+    onSSRAppCreated ({ pinia, router, app, context }) {
+      return new Promise((resolve, reject) => {
+        // 设置服务端路由路径
+        router.push(context.url)
+        router.onReady(() => {
+          // 应用完成渲染时执行
+          context.rendered = () => {
+            // 将服务端渲染后得到的 pinia.state 同步到 context.state 中,
+            // context.state 会被自动序列化并通过 `window.__INITIAL_STATE__`
+            // 注入到 HTML 中,并在客户端运行时再读取同步
+            context.state = pinia.state.value
+          }
+          // 返回应用程序实例
+          resolve(app)
+        }, reject)
+      })
+    }
+})
+

如用户没有配置 onSSRAppCreated,框架内部会执行兜底逻辑,以保障 SSR 的正常运行。

# 其他注意事项

  1. Mpx SSR 渲染支持 i18n 的功能,但为了防止内存泄漏,当前 i18n 实例不会随着每次请求而重新创建,这是由于 Vue2.x 版本插件机制的设计缺陷所造成的,因此在使用 i18n 进行 SSR 时可能会产生跨请求状态污染的问题,这个问题会在未来 Mpx 输出 web 切换为 Vue3 后完全解决。

  2. 在服务端渲染阶段,对于 global 全局对象访问修改,如__mpx, __mpxRouter, __mpxPinia 都可能导致全局状态污染,所以在服务端渲染阶段请尽量避免进行相关操作;对于存在全局访问修改的方法,如 getApp(), getCurrentPages() 等在服务端渲染中被调用时,会产生相关报错提示。

  3. 由于服务器无法收到 URL 中的 hash 信息,使用 SSR 时需要通过修改 mpx.config.webRouteConfig 将路由模式设置成 history 模式。

# 包体积优化

近几年来随着小程序的商业成功和用户增长,各大互联网公司都在加大对小程序业务的投入,小程序的体量和复杂性都在快速增长,在这样的背景下,小程序的包体积大小成为了制约小程序业务迭代发展的一大限制,同时过大的包体积也会显著影响小程序的性能表现。

Mpx 在设计之初就考虑了包体积问题,完整继承了 webpack 已有的代码优化能力,如公共模块抽离、tree shaking、混淆压缩等。除此之外,我们还设计开发了业内最完善的分包构建流程,完整支持基于配置声明和依赖分析的普通分包、独立分包及分包异步化的构建输出,极大地缓解了复杂小程序中的主包和总包体积过大的问题。在 Mpx2.9 版本中,我们针对复杂小程序页面组件和 JS 模块众多的特点,对构建产物进行了针对性地优化,进一步降低了构建产物体积,主要优化项如下:

# 模版代码优化

Mpx 通过 webpack 进行打包构建,并对小程序原生 commonjs 支持进行适配改造,以实现 webpack 构建产物在小程序环境下运行,为了将构建产物中的模块和 chunk 链接到一起,webpack 和 mpx 会在构建产物生成一些模版代码进行相关工作,下面是一个页面 js 中包含的模版代码示例:

var self = {}
+self.webpackChunkmpx_test_2_8 = require('../bundle.js')
+(self.webpackChunkmpx_test_2_8 = self.webpackChunkmpx_test_2_8 || []).push([[405], {
+  1307: function (t, e, o) {
+    // ...
+  },
+  4236: function (t) {
+    // ...
+  },
+  // ...
+}, function (t) {
+  var e
+  e = 1307, t(t.s = e)
+}])
+

可以看出模版代码主要由 chunk 链接代码,chunk id,模块 id 和模块包装函数组成,对于中小项目来说,这些模版代码一般不会占用太多体积,但是在复杂大体量小程序中众多模块的叠加效应下,这部分体积就变得不可忽视。

我们对模版代码的生成逻辑和生成产物进行分析,发现 webpack 生产模式的默认配置中,很多配置项并不是体积最优的选项,一个典型的例子在于模块/chunk id:为了保障生成产物的内容稳定,来尽可能提升浏览器的缓存利用率,webpack 默认的模块/chunk id 采用 deterministic 模式进行生成,该模式下模块 id 为模块源路径的定长数字 hash,比项目模块总数长 1 位。由于在小程序中代码包按照版本的维度进行全量管理,保证文件局部的内容稳定在小程序环境下无正向意义,这就有了优化空间,我们可以简单将模块/chunk id 的生成逻辑改为数字自增,在主小程序中就能节省出上百 KB 总包体积。

类似的可优化点还存在于 chunk 链接代码和模块包装函数当中,我们在 @mpxjs/webpack-plugin@2.9 版本中提供了一个新的配置项 optimizeSize (opens new window),其中整合了一系列模版代码体积优化配置,开启后就能自动优化构建产物中的模版代码体积,在实际业务中,我们开启 optimizeSize 后可以减少总包体积约 1.6%,效果非常显著,下面是上述示例在开启 optimizeSize 后的产物对比:

var g = {}
+g.c = require('../bundle.js'), (g.c = g.c || []).push([[8], {
+  448: function (t, e, o) {
+    // ...
+  },
+  463: function (t) {
+    // ...
+  }
+  // ...
+}, function (t) {
+  var e
+  e = 448, t(t.s = e)
+}])
+

# 空模块移除

Mpx 在处理 .mpx 单文件时会将其分拆为 template/js/style/json 四个新模块来进行分别处理,其中 template/style/json 部分的内容会在处理后通过内置的 extractor 抽取输出为静态文件,抽取之后原本分拆出来的模块就会以空模块的形式残留在构建产物中(template 模块在抽取后还需要保留 render 函数和 refs 等信息所以不会成为空模块),如下所示:

/***/ 533:
+/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
+// ...
+/* template */
+__webpack_require__(535)
+/* styles */
+__webpack_require__(536)
+/* json */
+__webpack_require__(537)
+/* script */
+__webpack_require__(534)
+/***/ }),
+/***/ 536:
+/***/ (function() {
+/***/ }),
+/***/ 537:
+/***/ (function() {
+/***/ })
+

从上面示例中可以看出 536/537 模块的定义和引用都是完全不必要的,但由于 webpack 本身对于空模块没有进行识别和优化的手段,在过去的版本中这些空模块代码会占用我们一部分总包体积。在 Mpx2.9 版本中,我们定义了全新的依赖类型 CommonjsExtractDependency 对于这类可能被抽取成为空模块的 request 进行识别处理,当模块内容在完成抽取后为空时,自动将其从模块图中移除,并在产物生成时不再生成其引用代码。该项优化措施内置开启,升级后自动生效,在实际业务中可减少总包体积约 0.7%

# Render函数优化

Render 函数是 Mpx 运行时 setData 优化的一项核心设计,我们在模版编译时将用户模版转换为简易的 render 函数,该函数执行时能够完全模拟视图渲染的数据访问过程,正确地收集当前视图的数据依赖,避免将视图不需要的数据通过 setData 发送到视图中。

render函数

虽然我们生成的简化版 render 函数仅保留了数据访问逻辑,在代码体积上并不算大,但是仍然存在着一定的优化空间,我们来看下面这个例子:

function render(){
+  this.a
+  if(this.c){
+    this.a
+    this.b
+    this.c.a
+    this.c.b
+    this.d
+  }
+  this.b
+}
+

可以看出 if block 下存在着大量冗余不必要的数据访问,例如 this.athis.b 在父级 block 中已经进行过访问,this.c.athis.c.b 由于在 if condition 中进行过 this.c 的访问也不再必要(render函数执行时会对模版依赖数据进行深度diff,父路径访问后子路径就无需再进行访问),上述 render 函数可以优化为:

function render(){
+  this.a
+  if(this.c){
+    this.d
+  }
+  this.b
+}
+

在 Mpx2.9 版本中,我们通过两轮 ast 遍历(2 pass ast travese)的方式实现了这个优化,在第一轮遍历中,我们收集了数据访问信息并按照 block 结构存储在 blockVisitTree 中,第二轮遍历时依据 blockVisitTree 中的信息对不必要的数据访问进行剪枝优化。

除此之外,我们也大幅优化了 render 函数中的数据收集代码,有效地降低了 render 函数的体积占用。

该优化目前没有默认开启,可以通过 @mpxjs/webpack-plugin 中的 optimizeRenderRules (opens new window) 配置项配置生效范围进行开启,全量开启后在实际业务中可节省总包体积约 5%

# 未来规划

在未来,我们对 Mpx 构建产物还有进一步的优化方案可以探索实施,主要分为两个方向:

  • 编译注入代码优化,在功能不变的情况下简化编译注入的代码,对于 render 函数也有一种运行时有损的方案(略微增大运行时开销)可以尝试
  • 模版和JSON压缩,对模版和JSON中的组件名及组件路径进行短哈希压缩,缺点是丢失dist产物的可读性,可能需要对其提供 sourceMap 支持

除此之外,我们近期的框架升级计划还包括:

  • 局部运行时渲染增强
  • 数据响应升级为 proxy 实现,输出 web 升级为 vue3
  • 支付宝 2.0 基础库适配优化
  • 微信 skyline 适配优化
  • 输出 Hummer 能力完善

在构建提速方面,我们也会在以下方向进行探索:

  • 输出 web 支持 vite 构建
  • 基于模块联邦的分布式构建
  • 基于 Rust 的高性能构建探索

最后,特别感谢@pagnkelly (opens new window)、@yandadaFreedom (opens new window)和 +@zhuzhh (opens new window)对于 2.9 版本功能开发的突出贡献,也欢迎大家使用 Mpx 进行小程序跨端开发并加入社区共建。

Github:https://github.com/didi/mpx

官网/文档:https://mpxjs.cn/

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/index.html b/docs-vuepress/.vuepress/dist/articles/index.html new file mode 100644 index 0000000000..091d29837f --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/index.html @@ -0,0 +1,43 @@ + + + + + + Mpx框架相关文章 | Mpx框架 + + + + + + + + + + + + + diff --git a/docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html b/docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html new file mode 100644 index 0000000000..0d3e732cba --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html @@ -0,0 +1,137 @@ + + + + + + @mpxjs/cli 插件化改造 | Mpx框架 + + + + + + + + + +

# @mpxjs/cli 插件化改造

@mxpjs/cli 地址 (opens new window)

# 背景 & 现状

Mpx 脚手架 @mpxjs/cli 作为 Mpx 生态当中比较重要的一部分,是使用 Mpx 进行小程序开发的入口。

@mpxjs/cli@2.x 版本整体是基于模板配置的方式完成项目的初始化,整个的工作流是:

  1. 下载一份存放于远端的 mpx 项目原始模板(mpx-template)

  2. 根据用户的 prompts 选项完成用户选项的注入,并初始化最终的项目文件

完成项目的初始化后,除了一些基础配置文件外,整个项目的文件主要包含了如下的结构:

-- mpx-project
+ |-- src // 项目源码
+ |-- config // 项目配置文件
+   |-- index.js // 配置入口文件
+   |-- mpxLoader.conf.js // mpx-loader 配置
+   |-- mpxPlugin.conf.js // mpx webpack-plugin 配置
+   |-- user.conf.js // 用户的 prompts 选择信息
+ |-- build // 编译构建配置
+   |-- build.js // 构建编译脚本
+   |-- getPlugins.js // webpack plugins 
+   |-- getRules.js // webpack module rules
+   |-- getWebpackConf.js // webpack 配置生成辅助函数
+   |-- utils.js // 工具函数
+   |-- webpack.base.conf.js // webpack 基础配置
+

在初始化的项目当中,有关项目的所有配置文件,编译构建代码是全部暴露给开发者的,开发者可以对这些文件进行修改来满足自己实际的项目开发需要。同时还可以基于这一套原始的模板文件二次拓展为满足自己业务场景的模板。

基于远程模板初始化项目的方式最大的一个好处就是将项目所有的底层配置完全暴露给开发者,开发者可以任意去修改对应的配置。

# mpxjs/cli@2.x 自身的痛点

目前 @mpxjs/cli@2.x 采用这种基于模板的方式面临着两方面的痛点:

  1. 对于 @mpxjs/cli 的使用者而言:
  • 模板和业务项目割裂:远程模板没有严格的版本控制,用户无法感知到远程模板的更新变化;

  • 项目升级困难:对于用户来说没有一个很好的方式完成升级工作,基本只能通过 copy 代码的方式,将 mpx-template 更新后的内容复制一份到自己的项目当中;或者是通过脚手架重新创建一个新的项目,将老的代码迁移到新项目当中;

  • 项目结构臃肿:从项目结构角度来说没法做到按需,初始化代码臃肿。Mpx 支持了小程序跨平台、跨端、小程序插件等等相关的开发,不同的编译构建配置都需要全部生成,在运行时阶段才能决定是否启动对应的功能;

  • 跨 Web 构建能力弱:在基于 Mpx 的跨 Web 场景构建中有关 web 侧的编译构建的配置是比较初级的,像 MPA 多页应用 等比较常用的功能是需要用户重新去手动搭建一套的;

  • 可拓展性差,基于目前的模板拉取的方式无法满足多样化的业务需求场景迭代;

  1. 对于 @mpxjs/cli 的开发者而言:
  • 分支场景多,功能模块耦合度高:脚手架的所有功能全部集合到一个大的模板当中。各部分的能力都是耦合在一起,为了满足不同项目的实际开发需要,代码里面需要写比较多的 if...else... 判断逻辑来决定要开启哪些功能,生成哪部分的模板;
  • 长期可维护性差,开发心智负担重;

针对以上问题,通过调研业内一些优秀的脚手架工具,发现 @vue/cli 插件化的架构设计能很好的去解决我们以上所遇到的问题。核心思路是将 @vue/cli 作为 @mpxjs/cli 底层的引擎,收敛 Mpx 对于核心依赖管理、模板、构建配置的能力,充分利用 @vue/cli 提供的插件机制去构建、拓展上层的插件集。

# 有关 @vue/cli

简单介绍下 @vue/cli 的插件化架构:

@vue/cli@3.x 相较于 2.x 版本相比,整个 @vue/cli 的架构发生了非常大的变化,从基于模板的脚手架迭代为基于插件化的脚手架。简单的概述下整个插件化的构架就是:

mpx-cli-3

  • @vue/cli 提供 vue cli 命令,负责偏好设置,生成模板、vue-cli-plugin 插件依赖管理的工作,例如 vue create <projectName>vue add <pluginName>

  • @vue/cli-service 作为 @vue/cli 整个插件系统当中的内部核心插件,提供了 npm script 注册服务,内置了部分 webpack 配置的同时,又提供了 vue-cli-plugin 插件的导入、加载以及 webpack 配置更新服务等。

以上是 @vue/cli 生态当中最为核心的两部分内容,二者分工明确,各司其职。

此外在 @vue/cli 生态当中非常重要的一个点就是 vue-cli-plugin 插件,每个插件主要完成模板生成及生产编译构建配置。根据 @vue/cli 设计的规范,开发一个 vue-cli-plugin 需要遵照相关的约定来进行开发:

  • @vue/cli 约定插件如果要生成模板,那么需要提供 generator 入口文件;

  • @vue/cli-service 约定插件的 webpack 配置更新需要放到插件的入口文件当中来完成,同时插件的命名也需要包含 vue-cli-plugin 前缀,因为 @vue/cli-service 是依据命名来加载相关的插件的;

# 插件化改造方案

一张图了解下插件化改造之后的 @mpxjs/cli 的架构设计:

mpx-cli-2

# 底层能力

@vue/cli@vue/cli-service 分别作为 @mpx/cli@mpx/cli-service 的底层能力,即利用插件化的架构设计,同时还非常灵活的满足了 Mpx 做差异化场景迭代的定制化改造。上层的插件满足 vue-cli-plugin 插件开发的规范,最终底层的能力还是依托于 @vue/cli@vue/cli-service 进行工作。

# 上层插件拆分

将原本大而全的模板进行插件化拆分,从 Mpx 所要解决的问题以及设计思路来考虑,站在跨平台的角度:

  1. web 开发
  • 基于 wx 的跨 web 开发;
  1. 小程序开发
  • 基于 wx 的跨平台(aliswanttdd)的小程序开发;
  • 使用云函数的微信小程序开发;
  • 微信小程序的插件模式的开发;

一个项目可能只需要某一种开发模式,例如仅仅是微信小程序的插件模式开发,也有可能是小程序和web平台混合开发等等,不同的开发模式对应了:

  1. 不同的目录结构

  2. 不同的编译构建配置


基于这样一种现状以及 @mpxjs/cli 所要解决的问题,从跨平台的角度出发将功能进行了拆分,最终拆解为如下的9个插件:

这些拆解出来的插件都将和功能相关的项目模板以及编译构建配置进行了收敛。

项目模板的生成不用说,借助 @vue/cliGenerator API 按需去生成项目开发所需要的模板,例如项目需要使用 eslint 的功能,那么在生成项目的时候会生成对应 vue-cli-plugin-mpx-eslint 所提供的模板文件,如果不需要使用,项目当中最终也不会出现和 eslint 相关的文件配置。

重点说下编译构建的配置是如何进行拆解的:

@vue/cli@3.x 基于插件的架构设计当中,决定是否要使用某个插件的依据就是判断这个插件是否被你的项目所安装和基于模板的构架相比最大的区别就是:基于模板的架构在最终生成的模板配置里需要保存一些环境配置文件,以供编译构建的运行时来判断是否启用某些功能。但是基于插件的架构基本上是不再需要这些环境配置文件的,因为你如果要使用一个插件的功能,只需要安装它即可。

因此依照这样的设计规范,我们将:

  • eslint

  • unit-test

  • typescript

这些非常独立的功能都单独抽离成了可拔插的插件,安装即启用。

以上功能有个特点就是和平台是完全解耦的,所以在拆解的过程中可以拆的比较彻底。但是由于 mpx 项目的特殊性,即要支持基于 wx 小程序的跨端以及 web 开发,同时还要支持小程序的云函数、小程序插件模式的开发,且不同开发模式的编译构建配置都有些差异。可以用如下的集合图来表示他们之间的关系:

mpx-cli-4

不同插件进行组合使用来满足不同场景下的使用。

在不同平台开发模式下是有 mpx 编译构建的基础配置的,这个是和平台没有太多关系,因此将这部分的配置单独抽离为一个插件:vue-cli-plugin-mpx同时这个插件也被置为了 @mpxjs/clipreset 预设插件,不管任何项目开发模式下,这个插件都会被默认的安装

// vue-cli-plugin-mpx
+module.exports = function (api, options, webpackConfig) {
+  webpackConfig.module
+    .rule('json')
+    .test(/\.json$/)
+    .resourceQuery(/asScript/)
+    .type('javascript/auto')
+
+  webpackConfig.module
+    .rule('wxs-pre-loader')
+    .test(/\.(wxs|qs|sjs|filter\.js)$/)
+    .pre()
+    .use('mpx-wxs-pre-loader')
+    .loader(MpxWebpackPlugin.wxsPreLoader().loader)
+
+  const transpileDepRegex = genTranspileDepRegex(options.transpileDependencies || [])
+  webpackConfig.module
+    .rule('js')
+    .test(/\.js$/)
+    .include
+    .add(filepath => transpileDepRegex && transpileDepRegex.test(filepath))
+    .add(filepath => /\.mpx\.js/.test(filepath)) // 处理 mpx 转 web 的情况,vue-loader 会将 script block fake 出一个 .mpx.js 路径,用以 loader 的匹配
+    .add(api.resolve('src'))
+    .add(api.resolve('node_modules/@mpxjs'))
+    .add(api.resolve('test'))
+    .end()
+    .use('babel-loader')
+    .loader('babel-loader')
+
+  const transpileDepRegex = genTranspileDepRegex(options.transpileDependencies)
+  webpackConfig.module
+    .rule('js')
+    .test(/\.js$/)
+    .include
+      .add(filepath => transpileDepRegex && transpileDepRegex.test(filepath))
+      .add(api.resolve('src'))
+      .add(api.resolve('node_modules/@mpxjs'))
+      .add(api.resolve('test'))
+        .end()
+    .use('babel-loader')
+      .loader('babel-loader')
+
+  webpackConfig.resolve.extensions
+    .add('.mpx')
+    .add('.wxml')
+    .add('.ts')
+    .add('.js')
+
+  webpackConfig.resolve.modules.add('node_modules')
+}
+

在小程序的开发模式下,vue-cli-plugin-mpx-mp 会在 vue-cli-plugin-mpx 的基础上去拓展 webpack 配置以满足小程序的编译构建。

在跨 web 的开发模式下,vue-cli-plugin-mpx-web 会在 vue-cli-plugin-mpx@vue/cli 的基础上去拓展配置以满足 web 侧的开发编译构建。

# Web 平台编译构建能力增强

@mpxjs/cli@2.x 版本当中有关 web 侧的编译构建的配置是比较初级的,像 热更新MPA 多页应用 等比较常用的功能是需要用户重新去手动搭建一套的。而 @vue/cli@3.x 即为 vue 项目而生,提供了非常完备的 web 应用的编译构建打包配置。

所以 @mpxjs/cli@next 版本里面做了一项非常重要的工作就是复用 @vue/cli 的能力,弥补 mpx 项目在跨 web 项目编译构建的不足。

因此关于 mpxweb 编译构建的部分也单独抽离为一个插件:vue-cli-plugin-mpx-web,这个插件所做的工作就是在 @vue/cli 提供的 web 编译构建的能力上去适配 mpx 项目。这样也就完成了 mpxweb 项目编译构建能力的增强。

这也意味着 @vue/cli 所提供的能力基本上在 mpx 跨 web 项目当中都可使用。

# 项目配置拓展能力

@mpxjs/cli@2.x 版本的项目如果要进行配置拓展,基本需要进行以下2个步骤:

  1. config 文件夹下的相关的配置文件进行修改;

  2. build 文件夹下的编译构建配置文件进行修改;

这也是在文章一开始的时候就提到的基于模板的大而全的文件组织方式。

而在 @mpxjs/cli@next 版本当中,将项目的配置拓展全部收敛至 vue.config.js 文件当中去完成,同时减少了开发者需要了解项目结构的心智负担。

// vue.config.js
+
+module.exports = {
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        // mpx-plugin 相关的配置
+      },
+      loader: {
+        // mpx-loader 相关的配置
+      }
+    }
+  },
+  configureWebpack() {},
+  chainWebpack() {
+    // 自定义的 webpack 配置
+  }
+}
+

# 改造后的目录结构

在第一章节展示了目前 @mpxjs/cli@2.x 初始化项目的结构和现状。经过这次的插件化的改造后,项目的结构变得更为简洁,开发者只需要关注 src 源码目录以及 vue.config.js 配置文件即可:

-- mpx-project
+ |--src
+ |--vue.config.js 
+

# 没有银弹

虽然基于 @vue/cli 插件的架构模式完成了 @mpxjs/cli@3.x 的插件化改造升级。但是由于 mpx 项目开发的一些特殊性,不同插件之间的协同工作是有一些约定的。

例如 @vue/cli-service 内置了一些 webpack 的配置,因为 @vue/cli 是专门针对 web应用的,这些配置会在所有的编译构建流程当中生效,不过这些配置当中有些对于小程序的开发来说是不需要的。

那么针对这种情况,为了避免不同模式下的 webpack 配置相互污染。web 侧的编译构建还是基于 @vue/cli 提供的能力去适配 mpxweb 开发。而小程序侧的编译构建配置是通过 @vue/cli-service 内部暴露出来的一些方法去跳过一些对于小程序开发来说不需要的 webpack 配置来最终满足小程序的构建配置。

因此在各插件的开发当中,需要暴露该插件所应用的平台:

module.export.platform = 'mp'  // 可选值: 'web'
+

这样在实际的构建过程当中通过平台的标识来决定对应哪些插件会生效。

# 如何开发一个基于 mpx 的业务脚手架插件

具体参阅文档 (opens new window)

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html b/docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html new file mode 100644 index 0000000000..6c43a1c96d --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html @@ -0,0 +1,56 @@ + + + + + + 小程序跨端组件库 Mpx-cube-ui 开源啦 | Mpx框架 + + + + + + + + + +

# 小程序跨端组件库 Mpx-cube-ui 开源啦

作者:CommanderXL (opens new window)

Mpx-cube-ui 是一款基于 Mpx 小程序框架 (opens new window) 的移动端基础组件库,一份源码可以跨端输出所有小程序平台及 Web,同时具备良好的拓展能力和可定制化的能力来帮助你快速构建 Mpx 应用项目。

Mpx-cube-ui 提供了灵活配置的主题定制能力,在组件设计开发阶段对表现层的结构和样式进行抽离,利用预编译器和 CSS 变量的能力,提供细粒度(颜色、字体、圆角、阴影等)的样式定制能力,你的项目可以按需使用主题的编译方案还是运行时方案来满足不同样式风格的业务场景开发。

Mpx-cube-ui 提供了开箱即用的跨端输出能力,源码基于 Mpx 小程序框架进行开发,依托于 Mpx 提供的跨平台能力 (opens new window)即基于微信小程序跨平台编译输出为支付宝、百度、QQ、头条等目标平台的小程序代码,同时还可以输出到 Web。

接下来会通过这篇文章去分享一下 Mpx-cube-ui 是如何诞生的。

# 业务现状

# 集团产品的迭代

越来越多的业务产品开始借助小程序的渠道来拓展产品的推广和使用,不同的技术团队承接的不同业务产品方向的需求。在滴滴所有的小程序产品当中,滴滴出行小程序作为最大的C端流量入口,承接了不同业务产品流量分发。在具体到小程序产品的研发环节,不同业务的小程序都被集成到了滴滴出行小程序当中,一同打包上线并发布。 +当然在这种跨业务、技术部门的产品研发当中,不同的技术团队都会有自身配套的基础能力建设。就拿小程序的组件库来说,每个独立的业务产品都会依据对应的交互设计规范来搭建一套满足自身业务述求的基础组件用以提高日常的业务开发效率,例如按钮、半浮层弹窗、Toast、Dialog、表单等等。

# 团队内的业务迭代

除了跨业务产品研发的场景外,在同一个技术团队内部也可能会承接来自不同业务产品方向的需求,这些不同业务产品同样也会有独立发展迭代的述求。由于业务场景的约束,团队内部同样也会面临如何做基础能力的沉淀和复用的议题,在节省研发资源的同时去提升业务项目交付的质量和效率。 +


那么在不同的业务产品发展初期,为了快速交付产品的功能保障上线时间,研发侧会尽量利用之前已有的基础能力来快速搭建产品功能。就拿小程序组件库的复用来说,常见的方式有:

  • 直接侵入到原有的组件代码当中进行 Hard Code,不同的业务产品依赖同一份组件代码,利用一些 if/else 或者条件编译等手段来使得原有的组件在满足最新的业务述求的同时,还要确保不会影响到原有的业务使用;
  • Fork 一份原有组件库的代码,在此基础上单独迭代维护,做到和原有组件的彻底隔离而不会造成污染;

对于第一种方案来说:组件本身因为增量的差异化需求使得组件本身的维护成本变高,同时对于差异化的代码处理难免在编译打包环节出现代码冗余问题,也就是原本不属于当前业务产品的代码实现也被一同打包; +对于第二种方案来说:组件库维护的数量变多,使得后续的组件迭代和更新需要修改多处代码;

此外,在小程序的场景下还面临着包体积等平台规范的硬性约束,特别是对于像滴滴出行小程序这种体量大的小程序来说,众多小程序业务产品被集成到同一个小程序当中,包体积也是日常研发过程中重点关注的一个指标。模块或组件的可复用性好、可拓展性强也意味着不需要重复去开发同一份功能相同代码,随之代码包体积也不会因重复实现相同功能而增速过快。

# Mpx 技术生态

在 Mpx 整个技术生态当中,组件体系目前是不完备的。

在我们目前使用 Mpx 去搭建实际的业务产品的过程中,我们基于小程序平台的基础组件去搭建了特定业务场景的基础业务组件,因为业务场景、功能和代码实现等各方面的原因,这些业务场景的基础组件是没法开放给社区的 Mpx 开发者来使用的。因此对于社区来说,要么只能同样基于平台的基础组件去开发上层基础组件,或者是使用第三方的原生小程序组件库,不管哪种方式对于社区的开发者而言,都有不少的上手和开发成本。

因此在社区当中也有比较强的述求,期望 Mpx 能提供更为基础通用的组件来更好的支持上层的业务开发。

# 组件库自身的维护

  • 文档示例一体化

不管是我们内部维护的业务组件库还是之前我们开源的基于 Vue 的 Web 组件库 Cube-ui (opens new window),组件本身的文档示例也是在组件库迭代过程中也是比较占用时间精力的,在示例和文档方面往往需要重复写多份文案,还要确保它们之间是同步更新的。那么针对组件库自身的维护,如何尽量减少组件库本身的维护成本也是我们需要深入思考的一部分工作。

以上这些问题促使我们重新思考整个 Mpx 组件体系的构建和发展。

# Mpx 组件体系

经过业务的长期迭代和验证,Mpx-cube-ui 作为 Mpx 技术生态当中组件体系的基础设施:脱离于业务的基础组件库,同时又要有良好的拓展能力和可定制化的能力来更好的支持上层业务。

组件分层设计 +在整个组件体系的搭建过程中,我们依据组件的特性将组件拆分为如下几种类型:

  • 业务组件一般来源于某一个具体的产品功能,用以解决特定问题域,更加强调关注点分离,代码的维护成本;
  • 基础业务组件一般来源于某一类的业务产品,在特定的业务背景下抽象的比较通用的,较少糅合业务逻辑,介于基础组件和业务组件之间,强调解决效率,可复用性,可维护性;
  • 基础组件一般来源于我们设计交互元语言,用以定义业务产品和用户的交互、反馈,更加强调一致性,效率,可复用性;

底层的基础组件(可组合性、可拓展性、稳定性)是为了更好的服务于上层的(基础)业务组件。

从底层的基础组件到业务组件(由下至上),组件的业务属性越来越强,所要解决的问题域更加聚焦,更贴合具体的问题和场景,同时它们的可复用性也随之降低。

# Mpx-cube-ui 组件设计

这里通过乘客交易、司机运营两个不同业务方向的组件来举例说明 Mpx-cube-ui 在组件的设计和开发当中所做的一些思考。 +在这两个业务场景当中都有 Modal 半浮层组件,不过在司乘业务不同的设计规范约束下,组件间的差异还是比较大的。

mpx-cube-ui-modal

那么为了解决上文当中组件库在跨业务产品的的可复用性、可拓展性的问题,核心所遵循的原则是:

弱化不同业务场景的设计规范(去业务),回归到组件本身的行为(逻辑)、结构和样式(表现):

  • 行为,组件所特有的行为方法:展示(show),隐藏(hide),关闭(close);
  • 结构,方便组件间的重新组合:头部区,标题区,内容区,底部操作区;
  • 样式,方便业务场景的主题定制:颜色、字号、圆角、边距等;

组件设计原则

对于 Mpx-cube-ui 组件的主题定制有个简单的逻辑关系:

  • 全局基础样式变量:提供了基本色值、字号等,会影响到所有组件的展示;
  • 组件样式变量:对于基本的色值、字号等继承于全局基础变量,涉及到组件自身的结构、样式变量会单独定义;
  • 组件渲染样式:直接依赖组件样式变量;

mpx-cube-ui-modal

通过定制全局基础样式变量 (opens new window)组件样式变量 (opens new window)都能达到定制主题的目的,有关主题定制的功能具体参见文档 (opens new window)

在 Mpx-cube-ui 实现的定制主题的方案当中是利用预编译器和 Css Varaibles 的能力来提供细粒度的样式定制能力:

业务架构图

  • 利用预编译器可编程的能力,在编译阶段就可以完成主题能力的定制,当你的业务项目作为独立应用迭代,可以只利用预编译器的能力,从而使得你的 css 代码体积尽可能的小;
  • 利用 Css Varaibles 的能力,可以解决更为复杂的场景,例如同一个组件在巨型应用当中,在不同业务场景页面需要有不同的主题样式;

官网示例 (opens new window)当中可以直接体验主题切换功能。

# 未来的规划

  1. 开源更多我们内部所沉淀出的基础组件,去满足多样化的业务场景开发;
  2. 将文档示例一体化的能力独立打包开源,用以组件库的快速建站以及降低文档、示例代码的维护成本。
+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/mpx1.html b/docs-vuepress/.vuepress/dist/articles/mpx1.html new file mode 100644 index 0000000000..7aea0fe8b3 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/mpx1.html @@ -0,0 +1,55 @@ + + + + + + 小程序开发者,为什么你应该尝试下MPX | Mpx框架 + + + + + + + + + +

# 小程序开发者,为什么你应该尝试下MPX

作者:sky-admin (opens new window)

MPX框架是滴滴出行推出的一款专注小程序开发的增强型框架。本篇文章将从使用角度谈谈MPX的优势与好处。如果嫌内容太长,优势部分每个小节都有简单的一句话总结,可以快速阅读。如果想了解更多设计细节,可以阅读 前一篇文章 - MPX2.0发布

# 背景

在小程序逐渐火热的今天,越来越多的开发者需要进行小程序的开发。原生小程序的开发有诸多不便,开发者又需要在众多的小程序框架中做出抉择。

那么今天,我们要给大家安利一款小程序框架:MPX

# 优势

之所以建议开发者们考虑使用MPX框架来开发小程序,是因为MPX框架具有一些别的框架所没有的优点。

MPX立足原生小程序,在保证坑少的同时做了很多能力增强,提供了数据响应、模板增强、性能优化、跨平台开发等能力,以提升用户的开发体验及效率。

接下来会从 原生兼容 -> 第三方组件支持 -> 按需构建 -> 跨平台编译 -> 能力增强 -> 独特性能优势 六个点来逐一讲述。

# 原生兼容

MPX完全兼容原生,坑少。渐进接入简单。

从语法风格上,我们可以看到目前市面上流行的小程序框架基本是基于web框架(taro/nanachi - react,uniapp/megalo/mpvue - vue)或者是一套全新(chameleon)/ 半全新(wepy)的标准。

使用了这些框架,你所写的代码,并不是小程序代码。而是react/vue或者另一套代码。而这些代码源码到小程序代码,需要经过一次全面的转换,这个转换可能会引入一些未知的问题,产生一些坑。

同时随着时间,小程序自身会逐步迭代,做出更多的功能特性,提供更好的组件、方法。而一些框架可能会受限于精力或框架节奏,没有办法第一时间跟进,甚至框架慢慢疏于维护而无法使用。

而MPX选择的是,全面拥抱原生

口说无凭,我们来看个典型的MPX组件长什么样。

mpx组件示例

乍一看好像和vue没什么区别,也就多了个json块,template里写的是小程序的标签。

由于这一块全是符合微信小程序原生语法,我们是不会做任何转换的,所以你写什么就是什么。(如果使用了MPX的增强特性,还是会进行一些必要的转换的,后续我们也会出文章详细解释MPX的增强是如何实现的,相对来说,我们的转换比较轻量、透明、易理解)

当微信出了新的能力、新的标签、新的生命周期钩子,使用MPX框架来编写的小程序只需要直接用起来就行。

所以,使用MPX框架,你可以轻易地使用 自定义组件的relations (opens new window) 来搞定组件间关系,使用 wxs (opens new window) 来更好地构建页面。

MPX几乎支持原生的每一个特性,在 .mpx 文件里,模板部分写的是原生小程序的模板语法,脚本部分写的是原生小程序的脚本语法,json部分写的是原生小程序的配置信息。用MPX,你才是真的在开发小程序。

目前很多原生小程序开发者可能想尝试下框架,老项目接入框架,选MPX肯定是最简单的了。口说无凭,我们搞了个demo来给大家打个样:在我们GitHub项目中有examples文件夹,里面的 原生项目渐进接入MPX示例 (opens new window)

# 第三方组件库

MPX提供了完备的第三方组件库支持

上面说了MPX对原生的极致兼容,能让你想到什么?对,就是对第三方组件库的完美支持。

支持第三方组件库的重要性大家都知道,所以这个能力大部分框架都支持了。但是支持和完美支持还是有区别的。据简单观察,taro/mpvue/uniapp对于第三方组件库的支持都是以复制的形式进行的,也就是和微信小程序本来的行为很像。

那么MPX是怎么支持第三方组件库的呢,这里有个demo:也在我们的GitHub里的examples文件夹下,MPX使用第三方组件库示例 (opens new window) ,核心代码见下图:

MPX使用第三方组件库代码示例

乍一眼看不太出来有什么特别的?没用过别的框架引第三方组件,简单找了找其他框架好像也没提供相应的demo,用过的朋友可以自行对比下。

在MPX里使用第三方组件库,仅需要***像web项目一样npm安装即可,并不需要复制文件***。然后在json里直接写包名就会去node_modules下面查找了。再配合webpack alias可以做到更简单、更语义化。

然而这还没有结束~

细心的朋友会发现,这段示例代码中既有vant的组件,也有iview的组件,如果按照微信的规范,这些组件库会通过miniprogram字段指定自己的构建文件生成目录,开发者工具会把这个目录完全拷贝到最终发布的代码里去,我们就会有两个巨大的组件库占据宝贵的空间。

我们当然是希望用多少引多少,而不是一股脑全引进去,对,于是MPX提供了按需引用的能力,在下一章按需引用细讲。

以及,组件库目前很少有跨小程序平台的组件库啊,如果我用了vant,支付宝、QQ里没有vant怎么办?也许这是别的框架不怎么推荐使用第三方库的原因,而MPX里,我们帮你把别人的组件库也转了,细节看下下章跨小程序平台

# 按需引用

通过webpack依赖分析收集,使用第三方组件库或者拆分开发大型项目时MPX能保证构建的代码全是要用到的代码

原生小程序本身的编译是遍历项目文件夹里所有的JS,包装成一个AMD包,也就是说项目文件夹里所有的文件,不论是否被使用,都会占用包体积并上传。

同时,原生微信小程序的npm支持是基于文件夹复制的,第三方包通过声明miniprogram字段指定要拷贝的文件夹,不论使用还是未使用的资源(模板/js/样式/图片),全会被复制到项目文件夹中。

而我们提供了@mpxjs/webpack-plugin插件,借助webpack生态,解析.mpx文件的json部分或原生的json文件将依赖作为新的入口添加子编译。基于依赖收集,而不是文件遍历。

带来的好处就是:如果你喜欢vant的按钮,iview的输入框,wux的布局,欢迎尝试MPX,让你能同时使用多个UI框架的同时不用担心应用的体积爆炸。

同理,面对一个大型项目,我们可以拆成不同的部分,由不同的团队完成后发npm包,在一个主项目中引入即可,具体内容可以看文档packages一节。

收集依赖的细节可以查阅文档编译构建一节。

# 跨小程序平台

MPX的跨平台方法能带着第三方组件库一起跨小程序平台,同时提供了充足完善的条件编译能力。

在 MPX 1.0 时代,MPX框架是专注提升微信小程序的开发体验,虽然也提供了支付宝版,但代码完全要另写。

而随着越来越多的 super app 提供了小程序能力,目前至少有5种体系的小程序(微信、支付宝系列、百度系列、头条系列、QQ),如果每一个平台都需要维护一份代码,工程师人数明显不够用了,所以跨小程序平台的能力也是 MPX 2.0 的主打特性。

我们的跨平台的方法就是转换。都是小程序,语法基本一样,配置、钩子的差异在MPX运行时里提供了抹平。

而除此之外最大的区别也就是模板上的标签和指令。所以我们实现了一套转换的架子,再编写一份转换规则,即可完成微信小程序到支付宝、百度、头条小程序的转换。

采用这种转换的模式,非常方便用户理解我们是如何把微信小程序转换成支付宝、百度等小程序平台的。而且只要用户有需求,可以补齐任一套小程序转换其他平台的规则,就可以完成以某个小程序为标准为基础来编写小程序代码以及进一步转换成别的平台的能力。

再结合前面一直在说的我们对原生小程序的支持,就可以撞出一点不一样的东西,比如,前文提到的第三方组件库跨小程序平台。

对,我们能帮你把针对微信编写的ui组件库在支付宝、百度上运行起来,带着组件库一起跨小程序平台。

那么一定会有这样一个问题,就算MPX对原生的支持再怎么牛逼,有的基础能力只有微信平台有,别的平台没有,MPX的转换还能无中生有吗?

当然不能,其实这个问题对于所有的跨端框架都是一个问题,所以跨端最核心的问题是,如何搞定差异化部分。

MPX提供了丰富的条件编译能力,可以以文件为维度差别构建,可以以代码块为维度,也可以以代码维度进行差别构建。

而且MPX的差异化构建能力也是完全基于webpack实现的,所以上面提到的第三方组件库如果确实存在转换不了的地方,比如vant的picker组件使用内联wxs写了一个小方法叫isSimple在模板里调用了,但是这个方法的写法在百度小程序的filter脚本(filter可以理解为百度小程序的wxs)里不支持,因为百度的filter要求必须导出一个对象包裹方法。

最好的解决办法当然是给vant-weapp提pr帮他们解决一下这个问题,但时间可能会比较慢,所以在MPX里,可以利用webpack的alias能力:

通过alias解决第三方组件的跨平台问题

当尝试构建百度小程序时,会优先去查找pick/index.swan.wxml,再被alias到一个src下的文件,自己修改一下第三方包里有一些小问题的部分即可。

关于跨平台的条件编译,更多具体信息可以查阅我们的官方文档 - 跨平台编译

# 能力增强

通过数据响应、编译时预处理提供了computed/watch,完备的样式类型绑定,双向数据绑定,动态组件等一系列方便开发者更好开发小程序的能力增强

能力增强应该是一个框架提供的最核心最重要的能力了,而MPX也确实在这里下了很大的力气,提供了多且好用的能力增强,不过受限于此处的篇幅,就只简单介绍,细节大家还是查阅我们的文档的好。

别的框架由于往往基于react/vue的,会给个列表写明不支持哪些能力,用户写的时候习惯使然,往往用了后可能才反应过来哦这个不支持。MPX则是原生的小程序语法写着难受时候突然想起MPX有这个能力。

列一下MPX增强的能力:

  • 模板上的增强 +
    • 样式类名绑定
    • 内联事件传参
    • 动态组件
    • 双向绑定
    • 节点获取ref
  • JS里的增强 +
    • 数据响应
    • setData优化
    • ES6+
  • 样式上的增强 +
    • 预处理支持
    • rpx转换
  • JSON里的增强 +
    • packages
    • 分包资源优化

MPX最显著的能力是数据响应,它衍生出computed/watch,以及双向数据绑定等。这个能力和Vue比较像,不同的是在MPX里是由mobx提供的数据响应能力。

而同样是数据响应,我们做了一些不一样的优化。

# 性能优势

通过对模板的解析抽象出访问的数据以保证在提供了数据响应能力的同时不至于劣化性能。

mpvue/wepy/megalo等框架也提供了数据响应的能力,但是数据响应在小程序领域有个较大的问题,微信开发指南里明确提到要注意setData的调用频次和数据量的大小。

而数据响应最基本的做法就是数据变了就去set数据,这会极大劣化小程序的性能表现。

而MPX通过对模板进行解析,抽象出对应的render函数,在调用setData发送数据前执行render函数找到真正需要发送的数据。

效果如图:

小程序性能分析

小程序开发者工具的audits面板能辅助用户分析出可能需要优化的点。正如前文所说,MPX在红框部分,尤其是红框里的第三条,不将模板上未使用的数据发送到渲染层上做了极大的优化。

只要不出现渲染函数执行失败(会有warning在console里提示,同时兜底逻辑会进行全量setData以保证程序仍可正常运行),使用MPX开发的小程序就永远不用担心发送了模板未使用的数据。

虽然只是一个小小的TODO MVC示例,但是这个优化和应用的规模没关系,而且同时大家可以尝试别家的小demo对比看看。

这个优化的细节可以看前一篇文章,或者我们的文档MPX运行机制 - 数据响应与性能优化

# 总结

与目前市面上的诸多框架相比,MPX希望以原生小程序为基础,全面拥抱原生小程序,在原生小程序的基础上做增强,通过尽可能少的转换实现尽可能多的能力增强,在提升小程序开发体验的同时,保证不因转换或框架的问题产生过多的坑。

MPX框架的目标用户是对小程序质量有较高要求的开发者,如果你是原生小程序开发者,或者厌倦了解决某些以web框架DSL语法为基础的转换框架造成的坑,欢迎尝试MPX框架。

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/mpx2.html b/docs-vuepress/.vuepress/dist/articles/mpx2.html new file mode 100644 index 0000000000..be760c89ba --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/mpx2.html @@ -0,0 +1,740 @@ + + + + + + Mpx 小程序框架技术揭秘 | Mpx框架 + + + + + + + + + +

# Mpx 小程序框架技术揭秘

作者:CommanderXL (opens new window)

与目前业内的几个小程序框架相比较而言,mpx 开发设计的出发点就是基于原生的小程序去做功能增强。所以从开发框架的角度来说,是没有任何“包袱”,围绕着原生小程序这个 core 去做不同功能的 patch 工作,使得开发小程序的体验更好。

于是我挑了一些我非常感兴趣的点去学习了下 mpx 在相关功能上的设计与实现。

# 编译环节

# 动态入口编译

不同于 web 规范,我们都知道小程序每个 page/component 需要被最终在 webview 上渲染出来的内容是需要包含这几个独立的文件的:js/json/wxml/wxss。为了提升小程序的开发体验,mpx 参考 vue 的 SFC(single file component)的设计思路,采用单文件的代码组织方式进行开发。既然采用这种方式去组织代码的话,那么模板、逻辑代码、json配置文件、style样式等都放到了同一个文件当中。那么 mpx 需要做的一个工作就是如何将 SFC 在代码编译后拆分为 js/json/wxml/wxss 以满足小程序技术规范。熟悉 vue 生态的同学都知道,vue-loader 里面就做了这样一个编译转化工作。具体有关 vue-loader 的工作流程可以参见我写的文章 (opens new window)

这里会遇到这样一个问题,就是在 vue 当中,如果你要引入一个页面/组件的话,直接通过import语法去引入对应的 vue 文件即可。但是在小程序的标准规范里面,它有自己一套组件系统,即如果你在某个页面/组件里面想要使用另外一个组件,那么需要在你的 json 配置文件当中去声明usingComponents这个字段,对应的值为这个组件的路径。

在 vue 里面 import 一个 vue 文件,那么这个文件会被当做一个 dependency 去加入到 webpack 的编译流程当中。但是 mpx 是保持小程序原有的功能,去进行功能的增强。因此一个 mpx 文件当中如果需要引入其他页面/组件,那么就是遵照小程序的组件规范需要在usingComponents定义好组件名:路径即可,mpx 提供的 webpack 插件来完成确定依赖关系,同时将被引入的页面/组件加入到编译构建的环节当中

接下来就来看下具体的实现,mpx webpack-plugin 暴露出来的插件上也提供了静态方法去使用 loader。这个 loader 的作用和 vue-loader 的作用类似,首先就是拿到 mpx 原始的文件后转化一个 js 文本的文件。例如一个 list.mpx 文件里面有关 json 的配置会被编译为:

require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=json&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/json-compiler/index?root=!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=json&index=0!./list.mpx")
+

这样可以清楚的看到 list.mpx 这个文件首先 selector(抽离list.mpx当中有关 json 的配置,并传入到 json-compiler 当中) --->>> json-compiler(对 json 配置进行处理,添加动态入口等) --->>> extractor(利用 child compiler 单独生成 json 配置文件)

其中动态添加入口的处理流程是在 json-compiler 当中去完成的。例如在你的 page/home.mpx 文件当中的 json 配置中使用了 局部组件 components/list.mpx:

<script type="application/json">
+  {
+    "usingComponents": {
+      "list": "../components/list"
+    }
+  }
+</script>
+

在 json-compiler 当中:

...
+
+const addEntrySafely = (resource, name, callback) => {
+  // 如果loader已经回调,就不再添加entry
+  if (callbacked) return callback()
+  // 使用 webpack 提供的 SingleEntryPlugin 插件创建一个单文件的入口依赖(即这个 component)
+  const dep = SingleEntryPlugin.createDependency(resource, name)
+  entryDeps.add(dep)
+  // compilation.addEntry 方法开始将这个需要被编译的 component 作为依赖添加到 webpack 的构建流程当中
+  // 这里可以看到的是整个动态添加入口文件的过程是深度优先的
+  this._compilation.addEntry(this._compiler.context, dep, name, (err, module) => {
+    entryDeps.delete(dep)
+    checkEntryDeps()
+    callback(err, module)
+  })
+}
+
+const processComponent = (component, context, rewritePath, componentPath, callback) => {
+  ...
+  // 调用 loaderContext 上提供的 resolve 方法去解析这个 component path 完整的路径,以及这个 component 所属的 package 相关的信息(例如 package.json 等)
+  this.resolve(context, component, (err, rawResult, info) => {
+    ...
+    componentPath = componentPath || path.join(subPackageRoot, 'components', componentName + hash(result), componentName)
+    ...
+    // component path 解析完之后,调用 addEntrySafely 开始在 webpack 构建流程中动态添加入口
+    addEntrySafely(rawResult, componentPath, callback)
+  })
+}
+
+if (isApp) {
+  ...
+} else {
+  if (json.usingComponents) {
+    // async.forEachOf 流程控制依次调用 processComponent 方法
+    async.forEachOf(json.usingComponents, (component, name, callback) => {
+      processComponent(component, this.context, (path) => {
+        json.usingComponents[name] = path
+      }, undefined, callback)
+    }, callback)
+  }
+  ...
+}
+...
+

这里需要解释说明下有关 webpack 提供的 SingleEntryPlugin 插件。这个插件是 webpack 提供的一个内置插件,当这个插件被挂载到 webpack 的编译流程的过程中是,会绑定compiler.hooks.make.tapAsynchook,当这个 hook 触发后会调用这个插件上的 SingleEntryPlugin.createDependency 静态方法去创建一个入口依赖,然后调用compilation.addEntry将这个依赖加入到编译的流程当中,这个是单入口文件的编译流程的最开始的一个步骤(具体可以参见 Webpack SingleEntryPlugin 源码 (opens new window))。

Mpx 正是利用了 webpack 提供的这样一种能力,在遵照小程序的自定义组件的规范的前提下,解析 mpx json 配置文件的过程中,手动的调用 SingleEntryPlugin 相关的方法去完成动态入口的添加工作。这样也就串联起了所有的 mpx 文件的编译工作。

# Render Function

Render Function 这块的内容我觉得是 Mpx 设计上的一大亮点内容。Mpx 引入 Render Function 主要解决的问题是性能优化方向相关的,因为小程序的架构设计,逻辑层和渲染层是2个独立的。

这里直接引用 Mpx 有关 Render Function 对于性能优化相关开发工作的描述:

作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个:

  • 尽可能减少setData调用的频次
  • 尽可能减少单次setData传输的数据 +为了实现以上两个优化方向,我们做了以下几项工作:

将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次;

将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。

接下来我们看下 Mpx 是如何实现 Render Function 的。这里我们从一个简单的 demo 来说起:

<template>
+  <text>Computed reversed message: "{{ reversedMessage }}"</text>
+  <view>the c string {{ demoObj.a.b.c }}</view>
+  <view wx:class="{{ { active: isActive } }}"></view>
+</template>
+
+<script>
+import { createComponent } from "@mpxjs/core";
+
+createComponent({
+  data: {
+    isActive: true,
+    message: 'messages',
+    demoObj: {
+      a: {
+        b: {
+          c: 'c'
+        }
+      }
+    }
+  },
+  computed() {
+    reversedMessage() {
+      return this.message.split('').reverse().join('')
+    }
+  }
+})
+</script>
+

.mpx 文件经过 loader 编译转换的过程中。对于 template 模块的处理和 vue 类似,首先将 template 转化为 AST,然后再将 AST 转化为 code 的过程中做相关转化的工作,最终得到我们需要的 template 模板代码。

packages/webpack-plugin/lib/template-compiler.js模板处理 loader 当中:

let renderResult = bindThis(`global.currentInject = {
+    moduleId: ${JSON.stringify(options.moduleId)},
+    render: function () {
+      var __seen = [];
+      var renderData = {};
+      ${compiler.genNode(ast)}return renderData;
+    }
+};\n`, {
+    needCollect: true,
+    ignoreMap: meta.wxsModuleMap
+  })
+

在 render 方法内部,创建 renderData 局部变量,调用compiler.genNode(ast)方法完成 Render Function 核心代码的生成工作,最终将这个 renderData 返回。例如在上面给出来的 demo 实例当中,通过compiler.genNode(ast)方法最终生成的代码为:

((mpxShow)||(mpxShow)===undefined?'':'display:none;');
+if(( isActive )){
+}
+"Computed reversed message: \""+( reversedMessage )+"\"";
+"the c string "+( demoObj.a.b.c );
+(__injectHelper.transformClass("list", ( {active: isActive} )));
+

mpx 文件当中的 template 模块被初步处理成上面的代码后,可以看到这是一段可执行的 js 代码。那么这段 js 代码到底是用作何处呢?可以看到compiler.genNode方法是被包裹至bindThis方法当中的。即这段 js 代码还会被bindThis方法做进一步的处理。打开 bind-this.js 文件可以看到内部的实现其实就是一个 babel 的 transform plugin。在处理上面这段 js 代码的 AST 的过程中,通过这个插件对 js 代码做进一步的处理。最终这段 js 代码处理后的结果是:

/* mpx inject */ global.currentInject = {
+  moduleId: "2271575d",
+  render: function () {
+    var __seen = [];
+    var renderData = {};
+    (renderData["mpxShow"] = [this.mpxShow, "mpxShow"], this.mpxShow) || (renderData["mpxShow"] = [this.mpxShow, "mpxShow"], this.mpxShow) === undefined ? '' : 'display:none;';
+    "Computed reversed message: \"" + (renderData["reversedMessage"] = [this.reversedMessage, "reversedMessage"], this.reversedMessage) + "\"";
+    "the c string " + (renderData["demoObj.a.b.c"] = [this.demoObj.a.b.c, "demoObj"], this.__get(this.__get(this.__get(this.demoObj, "a"), "b"), "c"));
+    this.__get(__injectHelper, "transformClass")("list", { active: (renderData["isActive"] = [this.isActive, "isActive"], this.isActive) });
+    return renderData;
+  }
+};
+

bindThis 方法对于 js 代码的转化规则就是:

  1. 一个变量的访问形式,改造成 this.xxx 的形式;
  2. 对象属性的访问形式,改造成 this.__get(object, property) 的形式(this.__get方法为运行时 mpx runtime 提供的方法)

这里的 this 为 mpx 构造的一个代理对象,在你业务代码当中调用 createComponent/createPage 方法传入的配置项,例如 data,都会通过这个代理对象转化为响应式的数据。

需要注意的是不管哪种数据形式的改造,最终需要达到的效果就是确保在 Render Function 执行的过程当中,这些被模板使用到的数据能被正常的访问到,在访问的阶段中,这些被访问到的数据即被加入到 mpx 构建的整个响应式的系统当中。

只要在 template 当中使用到的 data 数据(包括衍生的 computed 数据),最终都会被 renderData 所记录,而记录的数据形式是例如:

renderData['xxx'] = [this.xxx, 'xxx'] // 数组的形式,第一项为这个数据实际的值,第二项为这个数据的 firstKey(主要用以数据 diff 的工作)
+

以上就是 mpx 生成 Render Function 的整个过程。总结下 Render Function 所做的工作:

  1. 执行 render 函数,将渲染模板使用到的数据加入到响应式的系统当中;
  2. 返回 renderData 用以接下来的数据 diff 以及调用小程序的 setData 方法来完成视图的更新

# Wxs Module

Wxs 是小程序自己推出的一套脚本语言。官方文档 (opens new window)给出的示例,wxs 模块必须要声明式的被 wxml 引用。和 js 在 jsCore 当中去运行不同的是 wxs 是在渲染线程当中去运行的。因此 wxs 的执行便少了一次从 jsCore 执行的线程和渲染线程的通讯,从这个角度来说是对代码执行效率和性能上的比较大的一个优化手段。

有关官方提到的有关 wxs 的运行效率的问题还有待论证:

“在 android 设备中,小程序里的 wxs 与 js 运行效率无差异,而在 ios 设备中,小程序里的 wxs 会比 js 快 2~20倍。”

因为 mpx 是对小程序做渐进增强,因此 wxs 的使用方式和原生的小程序保持一致。在你的.mpx文件当中的 template block 内通过路径直接去引入 wxs 模块即可使用:

<template>
+  <wxs src="../wxs/components/list.wxs" module="list">
+  <view>{{ list.FOO }}</view>
+</template>
+
// wxs/components/list.wxs
+const Foo = 'This is from list wxs module'
+module.exports = {
+  Foo
+}
+

在 template 模块经过 template-compiler 处理的过程中。模板编译器 compiler 在解析模板的 AST 过程中会针对 wxs 标签缓存一份 wxs 模块的映射表:

{
+  meta: {
+    wxsModuleMap: {
+      list: '../wxs/components/list.wxs'
+    }
+  }
+}
+

当 compiler 对 template 模板解析完后,template-compiler 接下来就开始处理 wxs 模块相关的内容:

// template-compiler/index.js
+
+module.exports = function (raw) {
+  ...
+
+  const addDependency = dep => {
+    const resourceIdent = dep.getResourceIdentifier()
+    if (resourceIdent) {
+      const factory = compilation.dependencyFactories.get(dep.constructor)
+      if (factory === undefined) {
+        throw new Error(`No module factory available for dependency type: ${dep.constructor.name}`)
+      }
+      let innerMap = dependencies.get(factory)
+      if (innerMap === undefined) {
+        dependencies.set(factory, (innerMap = new Map()))
+      }
+      let list = innerMap.get(resourceIdent)
+      if (list === undefined) innerMap.set(resourceIdent, (list = []))
+      list.push(dep)
+    }
+  }
+
+  // 如果有 wxsModuleMap 即为 wxs module 依赖的话,那么下面会调用 compilation.addModuleDependencies 方法
+  // 将 wxsModule 作为 issuer 的依赖再次进行编译,最终也会被打包进输出的模块代码当中
+  // 需要注意的就是 wxs module 不仅要被注入到 bundle 里的 render 函数当中,同时也会通过 wxs-loader 处理,单独输出一份可运行的 wxs js 文件供 wxml 引入使用
+  for (let module in meta.wxsModuleMap) {
+    isSync = false
+    let src = meta.wxsModuleMap[module]
+    const expression = `require(${JSON.stringify(src)})`
+    const deps = []
+    // parser 为 js 的编译器
+    parser.parse(expression, {
+      current: { // 需要注意的是这里需要部署 addDependency 接口,因为通过 parse.parse 对代码进行编译的时候,会调用这个接口来获取 require(${JSON.stringify(src)}) 编译产生的依赖模块
+        addDependency: dep => {
+          dep.userRequest = module
+          deps.push(dep)
+        }
+      },
+      module: issuer
+    })
+    issuer.addVariable(module, expression, deps) // 给 issuer module 添加 variable 依赖
+    iterationOfArrayCallback(deps, addDependency)
+  }
+
+  // 如果没有 wxs module 的处理,那么 template-compiler 即为同步任务,否则为异步任务
+  if (isSync) {
+    return result
+  } else {
+    const callback = this.async()
+
+    const sortedDependencies = []
+    for (const pair1 of dependencies) {
+      for (const pair2 of pair1[1]) {
+        sortedDependencies.push({
+          factory: pair1[0],
+          dependencies: pair2[1]
+        })
+      }
+    }
+
+    // 调用 compilation.addModuleDependencies 方法,将 wxs module 作为 issuer module 的依赖加入到编译流程中
+    compilation.addModuleDependencies(
+      issuer,
+      sortedDependencies,
+      compilation.bail,
+      null,
+      true,
+      () => {
+        callback(null, result)
+      }
+    )
+  }
+}
+

# template/script/style/json 模块单文件的生成

不同于 Vue 借助 webpack 是将 Vue 单文件最终打包成单独的 js chunk 文件。而小程序的规范是每个页面/组件需要对应的 wxml/js/wxss/json 4个文件。因为 mpx 使用单文件的方式去组织代码,所以在编译环节所需要做的工作之一就是将 mpx 单文件当中不同 block 的内容拆解到对应文件类型当中。在动态入口编译的小节里面我们了解到 mpx 会分析每个 mpx 文件的引用依赖,从而去给这个文件创建一个 entry 依赖(SingleEntryPlugin)并加入到 webpack 的编译流程当中。我们还是继续看下 mpx loader 对于 mpx 单文件初步编译转化后的内容:

/* script */
+export * from "!!babel-loader!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=script&index=0!./list.mpx"
+
+/* styles */
+require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=styles&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/wxss/loader?root=&importLoaders=1&extract=true!../../node_modules/@mpxjs/webpack-plugin/lib/style-compiler/index?{\"id\":\"2271575d\",\"scoped\":false,\"sourceMap\":false,\"transRpx\":{\"mode\":\"only\",\"comment\":\"use rpx\",\"include\":\"/Users/XRene/demo/mpx-demo-source/src\"}}!stylus-loader!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=styles&index=0!./list.mpx")
+
+/* json */
+require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=json&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/json-compiler/index?root=!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=json&index=0!./list.mpx")
+
+/* template */
+require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=template&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/wxml/wxml-loader?root=!../../node_modules/@mpxjs/webpack-plugin/lib/template-compiler/index?{\"usingComponents\":[],\"hasScoped\":false,\"isNative\":false,\"moduleId\":\"2271575d\"}!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=template&index=0!./list.mpx")
+

接下来可以看下 styles/json/template 这3个 block 的处理流程是什么样。

首先来看下 json block 的处理流程:list.mpx -> json-compiler -> extractor。第一个阶段 list.mpx 文件经由 json-compiler 的处理流程在前面的章节已经讲过,主要就是分析依赖增加动态入口的编译过程。当所有的依赖分析完后,调用 json-compiler loader 的异步回调函数:

// lib/json-compiler/index.js
+
+module.exports = function (content) {
+
+  ...
+  const nativeCallback = this.async()
+  ...
+
+  let callbacked = false
+  const callback = (err, processOutput) => {
+    checkEntryDeps(() => {
+      callbacked = true
+      if (err) return nativeCallback(err)
+      let output = `var json = ${JSON.stringify(json, null, 2)};\n`
+      if (processOutput) output = processOutput(output)
+      output += `module.exports = JSON.stringify(json, null, 2);\n`
+      nativeCallback(null, output)
+    })
+  }
+}
+

这里我们可以看到经由 json-compiler 处理后,通过nativeCallback方法传入下一个 loader 的文本内容形如:

var json = {
+  "usingComponents": {
+    "list": "/components/list397512ea/list"
+  }
+}
+
+module.exports = JSON.stringify(json, null, 2)
+

即这段文本内容会传递到下一个 loader 内部进行处理,即 extractor。接下来我们来看下 extractor 里面主要是实现了哪些功能:

// lib/extractor.js
+
+module.exports = function (content) {
+  ...
+  const contentLoader = normalize.lib('content-loader')
+  let request = `!!${contentLoader}?${JSON.stringify(options)}!${this.resource}` // 构建一个新的 resource,且这个 resource 只需要经过 content-loader
+  let resultSource = defaultResultSource
+  const childFilename = 'extractor-filename'
+  const outputOptions = {
+    filename: childFilename
+  }
+  // 创建一个 child compiler
+  const childCompiler = mainCompilation.createChildCompiler(request, outputOptions, [
+    new NodeTemplatePlugin(outputOptions),
+    new LibraryTemplatePlugin(null, 'commonjs2'), // 最终输出的 chunk 内容遵循 commonjs 规范的可执行的模块代码 module.exports = (function(modules) {})([modules])
+    new NodeTargetPlugin(),
+    new SingleEntryPlugin(this.context, request, resourcePath),
+    new LimitChunkCountPlugin({ maxChunks: 1 })
+  ])
+
+  ...
+  childCompiler.hooks.thisCompilation.tap('MpxWebpackPlugin ', (compilation) => {
+    // 创建 loaderContext 时触发的 hook,在这个 hook 触发的时候,将原本从 json-compiler 传递过来的 content 内容挂载至 loaderContext.__mpx__ 属性上面以供接下来的 content -loader 来进行使用
+    compilation.hooks.normalModuleLoader.tap('MpxWebpackPlugin', (loaderContext, module) => {
+      // 传递编译结果,子编译器进入content-loader后直接输出
+      loaderContext.__mpx__ = {
+        content,
+        fileDependencies: this.getDependencies(),
+        contextDependencies: this.getContextDependencies()
+      }
+    })
+  })
+
+  let source
+
+  childCompiler.hooks.afterCompile.tapAsync('MpxWebpackPlugin', (compilation, callback) => {
+    // 这里 afterCompile 产出的 assets 的代码当中是包含 webpack runtime bootstrap 的代码,不过需要注意的是这个 source 模块的产出形式
+    // 因为使用了 new LibraryTemplatePlugin(null, 'commonjs2') 等插件。所以产出的 source 是可以在 node 环境下执行的 module
+    // 因为在 loaderContext 上部署了 exec 方法,即可以直接执行 commonjs 规范的 module 代码,这样就最终完成了 mpx 单文件当中不同模块的抽离工作
+    source = compilation.assets[childFilename] && compilation.assets[childFilename].source()
+
+    // Remove all chunk assets
+    compilation.chunks.forEach((chunk) => {
+      chunk.files.forEach((file) => {
+        delete compilation.assets[file]
+      })
+    })
+
+    callback()
+  })
+
+  childCompiler.runAsChild((err, entries, compilation) => {
+    ...
+    try {
+      // exec 是 loaderContext 上提供的一个方法,在其内部会构建原生的 node.js module,并执行这个 module 的代码
+      // 执行这个 module 代码后获取的内容就是通过 module.exports 导出的内容
+      let text = this.exec(source, request)
+      if (Array.isArray(text)) {
+        text = text.map((item) => {
+          return item[1]
+        }).join('\n')
+      }
+
+      let extracted = extract(text, options.type, resourcePath, +options.index, selfResourcePath)
+      if (extracted) {
+        resultSource = `module.exports = __webpack_public_path__ + ${JSON.stringify(extracted)};`
+      }
+    } catch (err) {
+      return nativeCallback(err)
+    }
+    if (resultSource) {
+      nativeCallback(null, resultSource)
+    } else {
+      nativeCallback()
+    }
+  })
+}
+

稍微总结下上面的处理流程:

  1. 构建一个以当前模块路径及 content-loader 的 resource 路径
  2. 以这个 resource 路径作为入口模块,创建一个 childCompiler
  3. childCompiler 启动后,创建 loaderContext 的过程中,将 content 文本内容挂载至 loaderContext.mpx 上,这样在 content-loader 在处理入口模块的时候仅仅就是取出这个 content 文本内容并返回。实际上这个入口模块经过 loader 的过程不会做任何的处理工作,仅仅是将父 compilation 传入的 content 返回出去。
  4. loader 处理模块的环节结束后,进入到 module.build 阶段,这个阶段对 content 内容没有太多的处理
  5. createAssets 阶段,输出 chunk。
  6. 将输出的 chunk 构建为一个原生的 node.js 模块并执行,获取从这个 chunk 导出的内容。也就是模块通过module.exports导出的内容。

所以上面的示例 demo 最终会输出一个 json 文件,里面包含的内容即为:

{
+  "usingComponents": {
+    "list": "/components/list397512ea/list"
+  }
+}
+

# 运行时环节

以上几个章节主要是分析了几个 Mpx 在编译构建环节所做的工作。接下来我们来看下 Mpx 在运行时环节做了哪些工作。

# 响应式系统

小程序也是通过数据去驱动视图的渲染,需要手动的调用setData去完成这样一个动作。同时小程序的视图层也提供了用户交互的响应事件系统,在 js 代码中可以去注册相关的事件回调并在回调中去更改相关数据的值。Mpx 使用 Mobx 作为响应式数据工具并引入到小程序当中,使得小程序也有一套完成的响应式的系统,让小程序的开发有了更好的体验。

还是从组件的角度开始分析 mpx 的整个响应式的系统。每次通过createComponent方法去创建一个新的组件,这个方法将原生的小程序创造组件的方法Component做了一层代理,例如在 attched 的生命周期钩子函数内部会注入一个 mixin:

// attached 生命周期钩子 mixin
+
+attached() {
+  // 提供代理对象需要的api
+  transformApiForProxy(this, currentInject)
+  // 缓存options
+  this.$rawOptions = rawOptions // 原始的,没有剔除 customKeys 的 options 配置
+  // 创建proxy对象
+  const mpxProxy = new MPXProxy(rawOptions, this) // 将当前实例代理到 MPXProxy 这个代理对象上面去
+  this.$mpxProxy = mpxProxy // 在小程序实例上绑定 $mpxProxy 的实例
+  // 组件监听视图数据更新, attached之后才能拿到properties
+  this.$mpxProxy.created()
+}
+

在这个方法内部首先调用transformApiForProxy方法对组件实例上下文this做一层代理工作,在 context 上下文上去重置小程序的 setData 方法,同时拓展 context 相关的属性内容:

function transformApiForProxy (context, currentInject) {
+  const rawSetData = context.setData.bind(context) // setData 绑定对应的 context 上下文
+  Object.defineProperties(context, {
+    setData: { // 重置 context 的 setData 方法
+      get () {
+        return this.$mpxProxy.setData.bind(this.$mpxProxy)
+      },
+      configurable: true
+    },
+    __getInitialData: {
+      get () {
+        return () => context.data
+      },
+      configurable: false
+    },
+    __render: { // 小程序原生的 setData 方法
+      get () {
+        return rawSetData
+      },
+      configurable: false
+    }
+  })
+  // context 绑定注入的render函数
+  if (currentInject) {
+    if (currentInject.render) { // 编译过程中生成的 render 函数
+      Object.defineProperties(context, {
+        __injectedRender: {
+          get () {
+            return currentInject.render.bind(context)
+          },
+          configurable: false
+        }
+      })
+    }
+    if (currentInject.getRefsData) {
+      Object.defineProperties(context, {
+        __getRefsData: {
+          get () {
+            return currentInject.getRefsData
+          },
+          configurable: false
+        }
+      })
+    }
+  }
+}
+

接下来实例化一个 mpxProxy 实例并挂载至 context 上下文的 $mpxProxy 属性上,并调用 mpxProxy 的 created 方法完成这个代理对象的初始化的工作。在 created 方法内部主要是完成了以下的几个工作:

  1. initApi,在组件实例 this 上挂载$watch,$forceUpdate,$updated,$nextTick等方法,这样在你的业务代码当中即可直接访问实例上部署好的这些方法;
  2. initData
  3. initComputed,将 computed 计算属性字段全部代理至组件实例 this 上;
  4. 通过 Mobx observable 方法将 data 数据转化为响应式的数据;
  5. initWatch,初始化所有的 watcher 实例;
  6. initRender,初始化一个 renderWatcher 实例;

这里我们具体的来看下 initRender 方法内部是如何进行工作的:

export default class MPXProxy {
+  ...
+  initRender() {
+    let renderWatcher
+    let renderExcutedFailed = false
+    if (this.target.__injectedRender) { // webpack 注入的有关这个 page/component 的 renderFunction
+      renderWatcher = watch(this.target, () => {
+        if (renderExcutedFailed) {
+          this.render()
+        } else {
+          try {
+            return this.target.__injectedRender() // 执行 renderFunction,获取渲染所需的响应式数据
+          } catch(e) {
+            ...
+          }
+        }
+      }, {
+        handler: (ret) => {
+          if (!renderExcutedFailed) {
+            this.renderWithData(ret) // 渲染页面
+          }
+        },
+        immediate: true,
+        forceCallback: true
+      })
+    }
+  }
+  ...
+}
+

在 initRender 方法内部非常清楚的看到,首先判断这个 page/component 是否具有 renderFunction,如果有的话那么就直接实例化一个 renderWatcher:

export default class Watcher {
+  constructor (context, expr, callback, options) {
+    this.destroyed = false
+    this.get = () => {
+      return type(expr) === 'String' ? getByPath(context, expr) : expr()
+    }
+    const callbackType = type(callback)
+    if (callbackType === 'Object') {
+      options = callback
+      callback = null
+    } else if (callbackType === 'String') {
+      callback = context[callback]
+    }
+    this.callback = typeof callback === 'function' ? action(callback.bind(context)) : null
+    this.options = options || {}
+    this.id = ++uid
+    // 创建一个新的 reaction
+    this.reaction = new Reaction(`mpx-watcher-${this.id}`, () => {
+      this.update()
+    })
+    // 在调用 getValue 函数的时候,实际上是调用 reaction.track 方法,这个方法内部会自动执行 effect 函数,即执行 this.update() 方法,这样便会出发一次模板当中的 render 函数来完成依赖的收集
+    const value = this.getValue()
+    if (this.options.immediateAsync) { // 放置到一个队列里面去执行
+      queueWatcher(this)
+    } else { // 立即执行 callback
+      this.value = value
+      if (this.options.immediate) {
+        this.callback && this.callback(this.value)
+      }
+    }
+  }
+
+  getValue () {
+    let value
+    this.reaction.track(() => {
+      value = this.get() // 获取注入的 render 函数执行后返回的 renderData 的值,在执行 render 函数的过程中,就会访问响应式数据的值
+      if (this.options.deep) {
+        const valueType = type(value)
+        // 某些情况下,最外层是非isObservable 对象,比如同时观察多个属性时
+        if (!isObservable(value) && (valueType === 'Array' || valueType === 'Object')) {
+          if (valueType === 'Array') {
+            value = value.map(item => toJS(item, false))
+          } else {
+            const newValue = {}
+            Object.keys(value).forEach(key => {
+              newValue[key] = toJS(value[key], false)
+            })
+            value = newValue
+          }
+        } else {
+          value = toJS(value, false)
+        }
+      } else if (isObservableArray(value)) {
+        value.peek()
+      } else if (isObservableObject(value)) {
+        keys(value)
+      }
+    })
+    return value
+  }
+
+  update () {
+    if (this.options.sync) {
+      this.run()
+    } else {
+      queueWatcher(this)
+    }
+  }
+
+  run () {
+    const immediateAsync = !this.hasOwnProperty('value')
+    const oldValue = this.value
+    this.value = this.getValue() // 重新获取新的 renderData 的值
+    if (immediateAsync || this.value !== oldValue || isObject(this.value) || this.options.forceCallback) {
+      if (this.callback) {
+        immediateAsync ? this.callback(this.value) : this.callback(this.value, oldValue)
+      }
+    }
+  }
+
+  destroy () {
+    this.destroyed = true
+    this.reaction.getDisposer()()
+  }
+}
+

Watcher 观察者核心实现的工作流程就是:

  1. 构建一个 Reaction 实例;
  2. 调用 getValue 方法,即 reaction.track,在这个方法内部执行过程中会调用 renderFunction,这样在 renderFunction 方法的执行过程中便会访问到渲染所需要的响应式的数据并完成依赖收集;
  3. 根据 immediateAsync 配置来决定回调是放到下一帧还是立即执行;
  4. 当响应式数据发生变化的时候,执行 reaction 实例当中的回调函数,即this.update()方法来完成页面的重新渲染。

mpx 在构建这个响应式的系统当中,主要有2个大的环节,其一为在构建编译的过程中,将 template 模块转化为 renderFunction,提供了渲染模板时所需响应式数据的访问机制,并将 renderFunction 注入到运行时代码当中,其二就是在运行环节,mpx 通过构建一个小程序实例的代理对象,将小程序实例上的数据访问全部代理至 MPXProxy 实例上,而 MPXProxy 实例即 mpx 基于 Mobx 去构建的一套响应式数据对象,首先将 data 数据转化为响应式数据,其次提供了 computed 计算属性,watch 方法等一系列增强的拓展属性/方法,虽然在你的业务代码当中 page/component 实例 this 都是小程序提供的,但是最终经过代理机制,实际上访问的是 MPXProxy 所提供的增强功能,所以 mpx 也是通过这样一个代理对象去接管了小程序的实例。需要特别指出的是,mpx 将小程序官方提供的 setData 方法同样收敛至内部,这也是响应式系统提供的基础能力,即开发者只需要关注业务开发,而有关小程序渲染运行在 mpx 内部去帮你完成。

# 性能优化

由于小程序的双线程的架构设计,逻辑层和视图层之间需要桥接 native bridge。如果要完成视图层的更新,那么逻辑层需要调用 setData 方法,数据经由 native bridge,再到渲染层,这个工程流程为:

小程序逻辑层调用宿主环境的 setData 方法;

逻辑层执行 JSON.stringify 将待传输数据转换成字符串并拼接到特定的JS脚本,并通过evaluateJavascript 执行脚本将数据传输到渲染层;

渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染;

WebView 线程开始执行渲染时,待更新数据会合并到视图层保留的原始 data 数据,并将新数据套用在WXML片段中得到新的虚拟节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。同时,将新的节点树替换旧节点树,用于下一次重渲染。

文章来源 (opens new window)

而 setData 作为逻辑层和视图层之间通讯的核心接口,那么对于这个接口的使用遵照一些准则将有助于性能方面的提升。

# 尽可能的减少 setData 传输的数据

Mpx 在这个方面所做的工作之一就是基于数据路径的 diff。这也是官方所推荐的 setData 的方式。每次响应式数据发生了变化,调用 setData 方法的时候确保传递的数据都为 diff 过后的最小数据集,这样来减少 setData 传输的数据。

接下来我们就来看下这个优化手段的具体实现思路,首先还是从一个简单的 demo 来看:

<script>
+import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  data: {
+    obj: {
+      a: {
+        c: 1,
+        d: 2
+      }
+    }
+  }
+  onShow() {
+    setTimeout(() => {
+      this.obj.a = {
+        c: 1,
+        d: 'd'
+      }
+    }, 200)
+  }
+})
+</script>
+

在示例 demo 当中,声明了一个 obj 对象(这个对象里面的内容在模块当中被使用到了)。然后经过 200ms 后,手动修改 obj.a 的值,因为对于 c 字段来说它的值没有发生改变,而 d 字段发生了改变。因此在 setData 方法当中也应该只更新 obj.a.d 的值,即:

this.setData('obj.a.d', 'd')
+

因为 mpx 是整体接管了小程序当中有关调用 setData 方法并驱动视图更新的机制。所以当你在改变某些数据的时候,mpx 会帮你完成数据的 diff 工作,以保证每次调用 setData 方法时,传入的是最小的更新数据集。

这里也简单的分析下 mpx 是如何去实现这样的功能的。在上文的编译构建阶段有分析到 mpx 生成的 Render Function,这个 Render Function 每次执行的时候会返回一个 renderData,而这个 renderData 即用以接下来进行 setData 驱动视图渲染的原始数据。renderData 的数据组织形式是模板当中使用到的数据路径作为 key 键值,对应的值使用一个数组组织,数组第一项为数据的访问路径(可获取到对应渲染数据),第二项为数据路径的第一个键值,例如在 demo 示例当中的 renderData 数据如下:

renderData['obj.a.c'] = [this.obj.a.c, 'obj']
+renderData['obj.a.d'] = [this.obj.a.d, 'obj']
+

当页面第一次渲染,或者是响应式输出发生变化的时候,Render Function 都会被执行一次用以获取最新的 renderData 来进行接下来的页面渲染过程。

// src/core/proxy.js
+
+class MPXProxy {
+  ...
+  renderWithData(rawRenderData) { // rawRenderData 即为 Render Function 执行后获取的初始化 renderData
+    const renderData = preprocessRenderData(rawRenderData) // renderData 数据的预处理
+    if (!this.miniRenderData) { // 最小数据渲染集,页面/组件初次渲染的时候使用 miniRenderData 进行渲染,初次渲染的时候是没有数据需要进行 diff 的
+      this.miniRenderData = {}
+      for (let key in renderData) { // 遍历数据访问路径
+        if (renderData.hasOwnProperty(key)) {
+          let item = renderData[key]
+          let data = item[0]
+          let firstKey = item[1] // 某个字段 path 的第一个 key 值
+          if (this.localKeys.indexOf(firstKey) > -1) {
+            this.miniRenderData[key] = diffAndCloneA(data).clone
+          }
+        }
+      }
+      this.doRender(this.miniRenderData)
+    } else { // 非初次渲染使用 processRenderData 进行数据的处理,主要是需要进行数据的 diff 取值工作,并更新 miniRenderData 的值
+      this.doRender(this.processRenderData(renderData))
+    }
+  }
+
+  processRenderData(renderData) {
+    let result = {}
+    for (let key in renderData) {
+      if (renderData.hasOwnProperty(key)) {
+        let item = renderData[key]
+        let data = item[0]
+        let firstKey = item[1]
+        let { clone, diff } = diffAndCloneA(data, this.miniRenderData[key]) // 开始数据 diff
+        // firstKey 必须是为响应式数据的 key,且这个发生变化的 key 为 forceUpdateKey 或者是在 diff 阶段发现确实出现了 diff 的情况
+        if (this.localKeys.indexOf(firstKey) > -1 && (this.checkInForceUpdateKeys(key) || diff)) {
+          this.miniRenderData[key] = result[key] = clone
+        }
+      }
+    }
+    return result
+  }
+  ...
+}
+
+// src/helper/utils.js
+
+// 如果 renderData 里面即包含对某个 key 的访问,同时还有对这个 key 的子节点访问的话,那么需要剔除这个子节点
+/**
+ * process renderData, remove sub node if visit parent node already
+ * @param {Object} renderData
+ * @return {Object} processedRenderData
+ */
+export function preprocessRenderData (renderData) {
+  // method for get key path array
+  const processKeyPathMap = (keyPathMap) => {
+    let keyPath = Object.keys(keyPathMap)
+    return keyPath.filter((keyA) => {
+      return keyPath.every((keyB) => {
+        if (keyA.startsWith(keyB) && keyA !== keyB) {
+          let nextChar = keyA[keyB.length]
+          if (nextChar === '.' || nextChar === '[') {
+            return false
+          }
+        }
+        return true
+      })
+    })
+  }
+
+  const processedRenderData = {}
+  const renderDataFinalKey = processKeyPathMap(renderData) // 获取最终需要被渲染的数据的 key
+  Object.keys(renderData).forEach(item => {
+    if (renderDataFinalKey.indexOf(item) > -1) {
+      processedRenderData[item] = renderData[item]
+    }
+  })
+  return processedRenderData
+}
+

其中在 processRenderData 方法内部调用了 diffAndCloneA 方法去完成数据的 diff 工作。在这个方法内部判断新、旧值是否发生变化,返回的 diff 字段即表示是否发生了变化,clone 为 diffAndCloneA 接受到的第一个数据的深拷贝值。

这里大致的描述下相关流程:

  1. 响应式的数据发生了变化,触发 Render Function 重新执行,获取最新的 renderData;
  2. renderData 的预处理,主要是用以剔除通过路径访问时同时有父、子路径情况下的子路径的 key;
  3. 判断是否存在 miniRenderData 最小数据渲染集,如果没有那么 Mpx 完成 miniRenderData 最小渲染数据集的收集,如果有那么使用处理后的 renderData 和 miniRenderData 进行数据的 diff 工作(diffAndCloneA),并更新最新的 miniRenderData 的值;
  4. 调用 doRender 方法,进入到 setData 阶段

相关参阅文档:

# 尽可能的减少 setData 的调用频次

每次调用 setData 方法都会完成一次从逻辑层 -> native bridge -> 视图层的通讯,并完成页面的更新。因此频繁的调用 setData 方法势必也会造成视图的多次渲染,用户的交互受阻。所以对于 setData 方法另外一个优化角度就是尽可能的减少 setData 的调用频次,将多个同步的 setData 操作合并到一次调用当中。接下来就来看下 mpx 在这方面是如何做优化的。

还是先来看一个简单的 demo:

<script>
+import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  data: {
+    msg: 'hello',
+    obj: {
+      a: {
+        c: 1,
+        d: 2
+      }
+    }
+  }
+  watch: {
+    obj: {
+      handler() {
+        this.msg = 'world'
+      },
+      deep: true
+    }
+  },
+  onShow() {
+    setTimeout(() => {
+      this.obj.a = {
+        c: 1,
+        d: 'd'
+      }
+    }, 200)
+  }
+})
+</script>
+

在示例 demo 当中,msg 和 obj 都作为模板依赖的数据,这个组件开始展示后的 200ms,更新 obj.a 的值,同时 obj 被 watch,当 obj 发生改变后,更新 msg 的值。这里的逻辑处理顺序是:

obj.a 变化 -> 将 renderWatch 加入到执行队列 -> 触发 obj watch -> 将 obj watch 加入到执行队列 -> 将执行队列放到下一帧执行 -> 按照 watch id 从小到大依次执行 watch.run -> setData 方法调用一次(即 renderWatch 回调),统一更新 obj.a 及 msg -> 视图重新渲染
+

接下来就来具体看下这个流程:由于 obj 作为模板渲染的依赖数据,自然会被这个组件的 renderWatch 作为依赖而被收集。当 obj 的值发生变化后,首先触发 reaction 的回调,即 this.update() 方法,如果是个同步的 watch,那么立即调用 this.run() 方法,即 watcher 监听的回调方法,否则就通过 queueWatcher(this) 方法将这个 watcher 加入到执行队列:

// src/core/watcher.js
+export default Watcher {
+  constructor (context, expr, callback, options) {
+    ...
+    this.id = ++uid
+    this.reaction = new Reaction(`mpx-watcher-${this.id}`, () => {
+      this.update()
+    })
+    ...
+  }
+
+  update () {
+    if (this.options.sync) {
+      this.run()
+    } else {
+      queueWatcher(this)
+    }
+  }
+}
+

而在 queueWatcher 方法中,lockTask 维护了一个异步锁,即将 flushQueue 当成微任务统一放到下一帧去执行。所以在 flushQueue 开始执行之前,还会有同步的代码将 watcher 加入到执行队列当中,当 flushQueue 开始执行的时候,依照 watcher.id 升序依次执行,这样去确保 renderWatcher 在执行前,其他所有的 watcher 回调都执行完了,即执行 renderWatcher 的回调的时候获取到的 renderData 都是最新的,然后再去进行 setData 的操作,完成页面的更新。

// src/core/queueWatcher.js
+import { asyncLock } from '../helper/utils'
+const queue = []
+const idsMap = {}
+let flushing = false
+let curIndex = 0
+const lockTask = asyncLock()
+export default function queueWatcher (watcher) {
+  if (!watcher.id && typeof watcher === 'function') {
+    watcher = {
+      id: Infinity,
+      run: watcher
+    }
+  }
+  if (!idsMap[watcher.id] || watcher.id === Infinity) {
+    idsMap[watcher.id] = true
+    if (!flushing) {
+      queue.push(watcher)
+    } else {
+      let i = queue.length - 1
+      while (i > curIndex && watcher.id < queue[i].id) {
+        i--
+      }
+      queue.splice(i + 1, 0, watcher)
+    }
+    lockTask(flushQueue, resetQueue)
+  }
+}
+
+function flushQueue () {
+  flushing = true
+  queue.sort((a, b) => a.id - b.id)
+  for (curIndex = 0; curIndex < queue.length; curIndex++) {
+    const watcher = queue[curIndex]
+    idsMap[watcher.id] = null
+    watcher.destroyed || watcher.run()
+  }
+  resetQueue()
+}
+
+function resetQueue () {
+  flushing = false
+  curIndex = queue.length = 0
+}
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/performance.html b/docs-vuepress/.vuepress/dist/articles/performance.html new file mode 100644 index 0000000000..6dc9f5e7a6 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/performance.html @@ -0,0 +1,60 @@ + + + + + + 小程序框架运行时性能大测评 | Mpx框架 + + + + + + + + + +

# 小程序框架运行时性能大测评

作者:董宏平(hiyuki (opens new window)),滴滴出行小程序负责人,mpx框架负责人及核心作者

随着小程序在商业上的巨大成功,小程序开发在国内前端领域越来越受到重视,为了方便广大开发者更好地进行小程序开发,各类小程序框架也层出不穷,呈现出百花齐放的态势。但是到目前为止,业内一直没有出现一份全面、详细、客观、公正的小程序框架测评报告,为小程序开发者在技术选型时提供参考。于是我便筹划推出一系列文章,对业内流行的小程序框架进行一次全方位的、客观公正的测评,本文是系列文章的第一篇——运行时性能篇。

在本文中,我们会对下列框架进行运行时性能测试(排名不分先后):

  • wepy2(https://github.com/Tencent/wepy) @2.0.0-alpha.20
  • uniapp(https://github.com/dcloudio/uni-app) @2.0.0-26120200226001
  • mpx(https://github.com/didi/mpx) @2.5.3
  • chameleon(https://github.com/didi/chameleon) @1.0.5
  • mpvue(https://github.com/Meituan-Dianping/mpvue) @2.0.6
  • kbone(https://github.com/Tencent/kbone) @0.8.3
  • taro next(https://github.com/NervJS/taro) @3.0.0-alpha.5

其中对于kbone和taro next均以vue作为业务框架进行测试。

运行时性能的测试内容包括以下几个维度:

  • 框架运行时体积
  • 页面渲染耗时
  • 页面更新耗时
  • 局部更新耗时
  • setData调用次数
  • setData发送数据大小

框架性能测试demo全部存放于https://github.com/hiyuki/mp-framework-benchmark 中,欢迎广大开发者进行验证纠错及补全;

# 测试方案

为了使测试结果真实有效,我基于常见的业务场景构建了两种测试场景,分别是动态测试场景和静态测试场景。

# 动态测试场景

动态测试中,视图基于数据动态渲染,静态节点较少,视图更新耗时和setData调用情况是该测试场景中的主要测试点。

动态测试demo模拟了实际业务中常见的长列表+多tab场景,该demo中存在两份优惠券列表数据,一份为可用券数据,另一份为不可用券数据,其中同一时刻视图中只会渲染展示其中一份数据,可以在上方的操作区模拟对列表数据的各种操作及视图展示切换(切tab)。

动态测试demo

在动态测试中,我在外部通过函数代理的方式在初始化之前将App、Page和Component构造器进行代理,通过mixin的方式在Page的onLoad和Component的created钩子中注入setData拦截逻辑,对所有页面和组件的setData调用进行监听,并统计小程序的视图更新耗时及setData调用情况。该测试方式能够做到对框架代码的零侵入,能够跟踪到小程序全量的setData行为并进行独立的耗时计算,具有很强的普适性,代码具体实现可以查看https://github.com/hiyuki/mp-framework-benchmark/blob/master/utils/proxy.js

# 静态测试场景

静态测试模拟业务中静态页面的场景,如运营活动和文章等页面,页面内具备大量的静态节点,而没有数据动态渲染,初始ready耗时是该场景下测试的重心。

静态测试demo使用了我去年发表的一篇技术文章的html代码进行小程序适配构建,其中包含大量静态节点及文本内容。

静态测试demo

# 测试流程及数据

以下所有耗时类的测试数据均为微信小程序中真机进行5次测试计算平均值得出,单位均为ms。Ios测试环境为手机型号iPhone 11,系统版本13.3.1,微信版本7.0.12,安卓测试环境为手机型号小米9,系统版本Android10,微信版本7.0.12。

为了使数据展示不过于混乱复杂,文章中所列的数据以Ios的测试结果为主,安卓测试结论与Ios相符,整体耗时比Ios高3~4倍左右,所有的原始测试数据存放在https://github.com/hiyuki/mp-framework-benchmark/blob/master/rawData.csv

由于transform-runtime引入的core-js会对框架的运行时体积和运行耗时带来一定影响,且不是所有的框架都会在编译时开启transform-runtime,为了对齐测试环境,下述测试均在transform-runtime关闭时进行。

# 框架运行时体积

由于不是所有框架都能够使用webpack-bundle-analyzer得到精确的包体积占用,这里我通过将各框架生成的demo项目体积减去native编写的demo项目体积作为框架的运行时体积。

demo总体积(KB) 框架运行时体积(KB)
native 27 0
wepy2 66 39
uniapp 114 87
mpx 78 51
chameleon 136 109
mpvue 103 76
kbone 395 368
taro next 183 156

该项测试的结论为:
+native > wepy2 > mpx > mpvue > uniapp > chameleon > taro next > kbone

结论分析:

  • wepy2和mpx在框架运行时体积上控制得最好;
  • taro next和kbone由于动态渲染的特性,在dist中会生成递归渲染模板/组件,所以占用体积较大。

# 页面渲染耗时(动态测试)

我们使用刷新页面操作触发页面重新加载,对于大部分框架来说,页面渲染耗时是从触发刷新操作到页面执行onReady的耗时,但是对于像kbone和taro next这样的动态渲染框架,页面执行onReady并不代表视图真正渲染完成,为此,我们设定了一个特殊规则,在页面onReady触发的1000ms内,在没有任何操作的情况下出现setData回调时,以最后触发的setData回调作为页面渲染完成时机来计算真实的页面渲染耗时,测试结果如下:

页面渲染耗时
native 60.8
wepy2 64
uniapp 56.4
mpx 52.6
chameleon 56.4
mpvue 117.8
kbone 98.6
taro next 89.6

该项测试的耗时并不等同于真实的渲染耗时,由于小程序自身没有提供performance api,真实渲染耗时无法通过js准确测试得出,不过从得出的数据来看该项数据依然具备一定的参考意义。

该项测试的结论为:
+mpx ≈ chameleon ≈ uniapp ≈ native ≈ wepy2 > taro next ≈ kbone ≈ mpvue

结论分析:

  • 由于mpvue全量在页面进行渲染,kbone和taro next采用了动态渲染技术,页面渲染耗时较长,其余框架并无太大区别。

# 页面更新耗时(无后台数据)

这里后台数据的定义为data中存在但当前页面渲染中未使用到的数据,在这个demo场景下即为不可用券的数据,当前会在不可用券为0的情况下,对可用券列表进行各种操作,并统计更新耗时。

更新耗时的计算方式是从数据操作事件触发开始到对应的setData回调完成的耗时

mpvue中使用了当前时间戳(new Date)作为超时依据对setData进行了超时时间为50ms的节流操作,该方式存在严重问题,当vue内单次渲染同步流程执行耗时超过50ms时,后续组件patch触发的setData会突破这个节流限制,以50ms每次的频率对setData进行高频无效调用。在该性能测试demo中,当优惠券数量超过500时,界面就会完全卡死。为了顺利跑完整个测试流程,我对该问题进行了简单修复,使用setTimeout重写了节流部分,确保在vue单次渲染流程同步执行完毕后才会调用setData发送合并数据,之后mpvue的所有性能测试都是基于这个patch版本来进行的,该patch版本存放在https://github.com/hiyuki/mp-framework-benchmark/blob/master/frameworks/mpvue/runtime/patch/index.js

理论上来讲native的性能在进行优化的前提下一定是所有框架的天花板,但是在日常业务开发中我们可能无法对每一次setData都进行优化,以下性能测试中所有的native数据均采用修改数据后全量发送的形式来实现。

第一项测试我们使用新增可用券(100)操作将可用券数量由0逐级递增到1000:

100 200 300 400 500 600 700 800 900 1000
native 84.6 69.8 71.6 75 77.2 78.8 82.8 93.2 93.4 105.4
wepy2 118.4 168.6 204.6 246.4 288.6 347.8 389.2 434.2 496 539
uniapp 121.2 100 96 98.2 97.8 99.6 104 102.4 109.4 107.6
mpx 110.4 87.2 82.2 83 80.6 79.6 86.6 90.6 89.2 96.4
chameleon 116.8 115.4 117 119.6 122 125.2 133.8 133.2 144.8 145.6
mpvue 112.8 121.2 140 169 198.8 234.2 278.8 318.4 361.4 408.2
kbone 556.4 762.4 991.6 1220.6 1468.8 1689.6 1933.2 2150.4 2389 2620.6
taro next 470 604.6 759.6 902.4 1056.2 1228 1393.4 1536.2 1707.8 1867.2

然后我们按顺序逐项点击删除可用券(all) > 新增可用券(1000) > 更新可用券(1) > 更新可用券(all) > 删除可用券(1)

delete(all) add(1000) update(1) update(all) delete(1)
native 32.8 295.6 92.2 92.2 83
wepy2 56.8 726.4 49.2 535 530.8
uniapp 43.6 584.4 54.8 144.8 131.2
mpx 41.8 489.6 52.6 169.4 165.6
chameleon 39 765.6 95.6 237.8 144.8
mpvue 103.6 669.4 404.4 414.8 433.6
kbone 120.2 4978 2356.4 2419.4 2357
taro next 126.6 3930.6 1607.8 1788.6 2318.2

该项测试中初期我update(all)的逻辑是循环对每个列表项进行更新,形如listData.forEach((item)=>{item.count++}),发现在chameleon框架中执行界面会完全卡死,追踪发现chameleon框架中没有对setData进行异步合并处理,而是在数据变动时直接同步发送,这样在数据量为1000的场景下用该方式进行更新会高频触发1000次setData,导致界面卡死;对此,我在chameleon框架的测试demo中,将update(all)的逻辑调整为深clone产生一份更新后的listData,再将其整体赋值到this.listData当中,以确保该项测试能够正常进行。

该项测试的结论为:
+native > mpx ≈ uniapp > chameleon > mpvue > wepy2 > taro next > kbone

结论分析:

  • mpx和uniapp在框架内部进行了完善的diff优化,随着数据量的增加,两个框架的新增耗时没有显著上升;
  • wepy2会在数据变更时对props数据也进行setData,在该场景下造成了大量的无效性能损耗,导致性能表现不佳;
  • kbone和taro next采用了动态渲染方案,每次新增更新时会发送大量描述dom结构的数据,与此同时动态递归渲染的耗时也远大于常规的静态模板渲染,使得这两个框架在所有的更新场景下耗时都远大于其他框架。

# 页面更新耗时(有后台数据)

刷新页面后我们使用新增不可用券(1000)创建后台数据,观察该操作是否会触发setData并统计耗时

back add(1000)
native 45.2
wepy2 174.6
uniapp 89.4
mpx 0
chameleon 142.6
mpvue 134
kbone 0
taro next 0

mpx进行setData优化时inspired by vue,使用了编译时生成的渲染函数跟踪模板数据依赖,在后台数据变更时不会进行setData调用,而kbone和taro next采用了动态渲染技术模拟了web底层环境,在上层完整地运行了vue框架,也达到了同样的效果。

然后我们执行和上面无后台数据时相同的操作进行耗时统计,首先是递增100:

100 200 300 400 500 600 700 800 900 1000
native 88 69.8 71.2 80.8 79.4 84.4 89.8 93.2 99.6 108
wepy2 121 173.4 213.6 250 298 345.6 383 434.8 476.8 535.6
uniapp 135.4 112.4 110.6 106.4 109.6 107.2 114.4 116 118.8 117.4
mpx 112.6 86.2 84.6 86.8 90 87.2 91.2 88.8 92.4 93.4
chameleon 178.4 178.2 186.4 184.6 192.6 203.8 210 217.6 232.6 236.8
mpvue 139 151 173.4 194 231.4 258.8 303.4 340.4 384.6 429.4
kbone 559.8 746.6 980.6 1226.8 1450.6 1705.4 1927.2 2154.8 2367.8 2617
taro next 482.6 626.2 755 909.6 1085 1233.2 1384 1568.6 1740.6 1883.8

然后按下表操作顺序逐项点击统计

delete(all) add(1000) update(1) update(all) delete(1)
native 43.4 299.8 89.2 89 87.2
wepy2 43.2 762.4 50 533 522.4
uniapp 57.8 589.8 62.6 160.6 154.4
mpx 45.8 490.8 52.8 167 166
chameleon 93.8 837 184.6 318 220.8
mpvue 124.8 696.2 423.4 419 430.6
kbone 121.4 4978.2 2331.2 2448.4 2348
taro next 129.8 3947.2 1610.4 1813.8 2290.2

该项测试的结论为:
+native > mpx > uniapp > chameleon > mpvue > wepy2 > taro next > kbone

结论分析:

  • 具备模板数据跟踪能力的三个框架mpx,kbone和taro next在有后台数据场景下耗时并没有显著增加;
  • wepy2当中的diff精度不足,耗时也没有产生明显变化;
  • 其余框架由于每次更新都会对后台数据进行deep diff,耗时都产生了一定提升。

# 页面更新耗时(大数据量场景)

由于mpvue和taro next的渲染全部在页面中进行,而kbone的渲染方案会额外新增大量的自定义组件,这三个框架都会在优惠券数量达到2000时崩溃白屏,我们排除了这三个框架对其余框架进行大数据量场景下的页面更新耗时测试

首先还是在无后台数据场景下使用新增可用券(1000)将可用券数量递增至5000:

1000 2000 3000 4000 5000
native 332.6 350 412.6 498.2 569.4
wepy2 970.2 1531.4 2015.2 2890.6 3364.2
uniapp 655.2 593.4 655 675.6 718.8
mpx 532.2 496 548.6 564 601.8
chameleon 805.4 839.6 952.8 1086.6 1291.8

然后点击新增不可用券(5000)将后台数据量增加至5000,再测试可用券数量递增至5000的耗时:

back add(5000)
native 117.4
wepy2 511.6
uniapp 285
mpx 0
chameleon 824
1000 2000 3000 4000 5000
native 349.8 348.4 430.4 497 594.8
wepy2 1128 1872 2470.4 3263.4 4075.8
uniapp 715 666.8 709.2 755.6 810.2
mpx 538.8 501.8 562.6 573.6 595.2
chameleon 1509.2 1672.4 1951.8 2232.4 2586.2

该项测试的结论为:
+native > mpx > uniapp > chameleon > wepy2

结论分析:

  • 在大数据量场景下,框架之间基础性能的差异会变得更加明显,mpx和uniapp依然保持了接近原生的良好性能表现,而chameleon和wepy2则产生了比较显著的性能劣化。

# 局部更新耗时

我们在可用券数量为1000的情况下,点击任意一张可用券触发选中状态,以测试局部更新性能

toggleSelect(ms)
native 2
wepy2 2.6
uniapp 2.8
mpx 2.2
chameleon 2
mpvue 289.6
kbone 2440.8
taro next 1975

该项测试的结论为:
+native ≈ chameleon ≈ mpx ≈ wepy2 ≈ uniapp > mpvue > taro next > kbone

结论分析:

  • 可以看出所有使用了原生自定义组件进行组件化实现的框架局部更新耗时都极低,这足以证明小程序原生自定义组件的优秀性和重要性;
  • mpvue由于使用了页面更新,局部更新耗时显著增加;
  • kbone和taro next由于递归动态渲染的性能开销巨大,导致局部更新耗时同样巨大。

# setData调用

我们将proxySetData的count和size选项设置为true,开启setData的次数和体积统计,重新构建后按照以下流程执行系列操作,并统计setData的调用次数和发送数据的体积。

操作流程如下:

  1. 100逐级递增可用券(0->500)
  2. 切换至不可用券
  3. 新增不可用券(1000)
  4. 100逐级递增可用券(500->1000)
  5. 更新可用券(all)
  6. 切换至可用券

操作完成后我们使用getCountgetSize方法获取累积的setData调用次数和数据体积,其中数据体积计算方式为JSON.stringify后按照utf-8编码方式进行体积计算,统计结果为:

count size(KB)
native 14 803
wepy2 3514 1124
mpvue 16 2127
uniapp 14 274
mpx 8 261
chameleon 2515 319
kbone 22 10572
taro next 9 2321

该项测试的结论为:
+mpx > uniapp > native > chameleon > wepy2 > taro next > mpvue > kbone

结论分析:

  • mpx框架成功实现了理论上setData的最优;
  • uniapp由于缺失模板追踪能力紧随其后;
  • chameleon由于组件每次创建时都会进行一次不必要的setData,产生了大量无效setData调用,但是数据的发送本身经过diff,在数据发送量上表现不错;
  • wepy2的组件会在数据更新时调用setData发送已经更新过的props数据,因此也产生了大量无效调用,且diff精度不足,发送的数据量也较大;
  • taro next由于上层完全基于vue,在数据发送次数上控制到了9次,但由于需要发送大量的dom描述信息,数据发送量较大;
  • mpvue由于使用较长的数据路径描述数据对应的组件,也产生了较大的数据发送量;
  • kbone对于setData的调用控制得不是很好,在上层运行vue的情况依然进行了22次数据发送,且发送的数据量巨大,在此流程中达到了惊人的10MB。

# 页面渲染耗时(静态测试)

此处的页面渲染耗时与前面描述的动态测试场景中相同,测试结果如下:

页面渲染耗时
native 70.4
wepy2 86.6
mpvue 115.2
uniapp 69.6
mpx 66.6
chameleon 65
kbone 144.2
taro next 119.8

该项测试的结论为:
+chameleon ≈ mpx ≈ uniapp ≈ native > wepy2 > mpvue ≈ taro next > kbone

结论分析:

  • 除了kbone和taro next采用动态渲染耗时增加,mpvue使用页面模板渲染性能稍差,其余框架的静态页面渲染表现都和原生差不多。

# 结论

综合上述测试数据,我们得到最终的小程序框架运行时性能排名为:
+mpx > uniapp > chameleon > wepy2 > mpvue > taro next > kbone

# 一点私货

虽然kbone和taro next采用了动态渲染技术在性能表现上并不尽如人意,但是我依然认为这是很棒的技术方案。虽然本文从头到位都在进行性能测试和对比,但性能并不是框架的全部,开发效率和高可用性仍然是框架的重心,开发效率相信是所有框架设计的初衷,但是高可用性却在很大程度被忽视。从这个角度来说,kbone和taro next是非常成功的,不同于过去的转译思路,这种从抹平底层渲染环境的做法能够使上层web框架完整运行,在框架可用性上带来非常大的提升,非常适合于运营类简单小程序的迁移和开发。

我主导开发的mpx框架(https://github.com/didi/mpx) 选择了另一条道路解决可用性问题,那就是基于小程序原生语法能力进行增强,这样既能避免转译web框架时带来的不确定性和不稳定性,同时也能带来非常接近于原生的性能表现,对于复杂业务小程序的开发者来说,非常推荐使用。在跨端输出方面,mpx目前能够完善支持业内全部小程序平台和web平台的同构输出,滴滴内部最重要最复杂的小程序——滴滴出行小程序完全基于mpx进行开发,并利用框架提供的跨端能力对微信和支付宝入口进行同步业务迭代,大大提升了业务开发效率。

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/size-control.html b/docs-vuepress/.vuepress/dist/articles/size-control.html new file mode 100644 index 0000000000..f1c5d9cae9 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/size-control.html @@ -0,0 +1,51 @@ + + + + + + 滴滴出行小程序体积优化实践 | Mpx框架 + + + + + + + + + +

# 滴滴出行小程序体积优化实践

作者:sky-admin (opens new window)

# 概述

2019年下半年,为了将微信钱包/支付宝九宫格入口的滴滴出行迁移为小程序,团队对小程序进行了大量的功能升级与补全。在整个过程中也遇到并克服了一系列问题和挑战,其中包体积问题尤为突出。接下来全面介绍一下滴滴出行小程序在体积控制方面做的努力与沉淀。

# 背景

微信对小程序包体积的要求是总体积不得超过12M,主包及单个分包体积不得超过2M。支付宝对于小程序包体积的计算方式虽和微信略有区别,不过整体也大同小异。

18年至19年初时,滴滴出行小程序里承载的业务只有网约车,且业务需求较少,在主包内都能够搞定。而在下半年时,为了将微信钱包/支付宝九宫格入口迁移至小程序,小程序开始新增诸如公交/代驾/车服/单车/顺风车等众多业务线,同时网约车的业务需求也要做全面的补齐,业务量和代码量一起爆炸式增长。

滴滴出行包含了丰富多样的出行业务,包含了快车/专车/出租车/豪华车/拼车/单车/代驾/顺风车/公交/车生活等众多业务线。整个滴滴出行小程序的最重要,使用最高频的页面是首页与订单详情页,首页中承载了各个业务线的需求表达,各个业务线的订单详情页则承载了具体的出行订单展示逻辑。此外还有各种功能页面比如个人中心,营销页面,设置,历史行程。

按照滴滴出行的产品逻辑,所有业务线的需求表达逻辑都在首页承载,为了良好的切换体验,在首页采用了单页顶导的方案进行业务线展示。即每个业务线在首页中提供一个需求表达组件,当用户切换顶导业务线后,切换出对应的业务线组件。

在这种设计下,所有的业务线的需求表达逻辑都集中在首页这个单一页面中,导致在业务迭代过程中,承载首页的主包体积迅速增长,很快触碰了小程序平台的单包2M上限,对后续的业务迭代与发展带来巨大阻碍。因此,对于包体积的控制是我们在小程序开发过程中面临的一大难题。

# 体积控制

下面我们将介绍滴滴出行小程序开发迭代过程中,我们对于小程序包体积进行的一系列优化控制实践。

# 基础优化手段

对于小程序来说,基础的包体积优化手段包括:资源压缩/去除代码冗余/资源CDN化/异步加载

在web开发中,webpack提供了大量的代码优化能力,包括依赖分析、模块去重、代码压缩、tree shaking、side effects等,这些能力可以方便地完成资源压缩和去除代码冗余的工作。滴滴出行小程序基于滴滴开源的小程序框架Mpx( https://github.com/didi/mpx )进行开发,Mpx框架的编译构建完全基于webpack,兼容webpack内部生态,天然可以使用上述能力对包体积进行优化。

小程序中支持部分静态资源(如图像视频等)使用CDN地址加载,我们会尽可能将相关的资源压缩后放到CDN上,避免这部分资源对包体积的占用。

小程序场景下无法像web当中通过script标签便捷地进行异步加载,但是小程序平台后期纷纷支持了分包加载的方案来实现该能力,由于分包加载是小程序特有的技术规范,webpack无法直接支持,因此Mpx框架专门针对该技术规范进行了良好的适配支持,关于该能力的应用我们会在后文详细阐述。

除此之外,Mpx框架还针对小程序场景进行了许多包体积优化的适配工作,如尽可能减少框架运行时包体积占用(压缩后占用56Kb),对引用到的页面/组件按需进行打包构建,声明公共样式进行样式复用,分包内公共模块抽取等。

在Mpx框架的这些能力的支持下,基本不需要额外配置就能构建出一个经过初步优化的小程序包。

微信开发者工具选项里也有类似的"上传代码时自动压缩混淆"可勾选,但在开发者工具中上传代码时计算体积是直接计算的当前项目代码的体积,并不会依据压缩后的体积。因此,如果你使用原生小程序进行开发,你的source代码极有可能进行进一步的压缩以节省空间。

# 分析体积

虽然框架已经提供了很多在体积控制方面的优化,但是随着业务迭代我们发现主包体积依然偏大。

在遇到主包体积偏大后,我们需要弄明白,主包里有哪些东西?它们为什么这么大?

使用原生小程序或者其他非基于webpack的框架进行开发的同学遇到这个问题后,可能只能去看硬盘上的文件大小。这样一来,各个模块的大小占比可能并不直观。而我们则可以借助 webpack-bundle-analyzer 这样一个webpack插件去做辅助分析。

比如这是一个使用Mpx框架编写的demo,通过 npm run build --report 就可以看到这样一个界面:

体积分析图

可以看到这个demo工程由 moment / lodash / socket-weapp / core-js 等第三方库组成。各个库的大小,相互依赖关系也能清晰地看出。

对于滴滴出行小程序也是能看到类似的图,能看到整个项目到底是由哪些代码组成。

另外,滴滴出行前端开发一直是采用“源码编译”的,可以让整个项目里公共的依赖可以实现仅有一份,一起共用。简而言之,也有助于减少项目代码体积。相关资料:https://github.com/DDFE/DDFE-blog/issues/23 (opens new window)

要完美发挥源码编译的效果,需要上下游一起建立整套源码编译生态,比如主项目的依赖方在声明公用依赖时,就应该用peerDep或者devDep来声明一些公有依赖,这些共有依赖应该在主项目中统一声明,避免因版本不同装出两份公共依赖,那样反而会增大体积。由于滴滴出行小程序涉及业务线及团队众多,部分团队可能并不知道这个事情,因此代码里实际是可能出现上述劣化场景。而依照分析图,可以容易地发现这种问题,并推动相关团队清除这些重复依赖。

同时,我们依照体积分析图,对其中体积较大的文件重点分析,进行了一轮业务代码梳理和精简,删除了一些无用代码,精简了websocket的消息体描述文件等。

# 配置分包

分包是小程序给出的类似web异步引入的一个方案,把一些初始进入时不需要的页面可以放进分包里,跳转到对应页面时才去下载分包,将这些页面及其附属资源放到分包里可以有效减少主包体积。

Mpx框架早期对分包规范进行了初步支持,资源访问规则保持和微信一致,主要根据资源存放的目录判断应该输出到主包还是分包。有这个能力后,我们把行程页抽到了分包,大概抽出了200多K左右的空间。

有了行程页的成功拆分后,我们开始对所有的非首页代码进行分包操作,比如起终点选择和个人中心。以及部分业务线的接入是通过npm的方式接入,我们也尽可能将这些业务线的所有非首页的代码放到了分包。

这里还有个题外话,得益于mpx早期设计了packages形式的业务组合方案,可以很方便地让业务独立开发,又能及其方便地整合。而后发现微信的分包的json配置设计和packages很像,就在这个基础上支持了微信的分包,用户侧仅需在原来的packages基础上加一个query标记这个分包的名字即可。

拆除各个分包后,整个项目结构大概如图:

分包一期结构图

初阶的分包工作进行完毕后,总计从主包里拆了差不多400K的空间到分包里。

# 分包资源精细化管理

上面提到,Mpx框架初期的分包处理规则是完全按照微信的方式,把在分包路径下的资源收集到分包里。而npm管理的资源因为都在node_modules目录下,不属于任何分包路径,则会被全部收集进主包。

比如之前我们有行程页分包,行程页自有的状态管理store整个都在行程页分包的路径下,就会被收集到行程页分包中。而行程页还用到了封装好的didi-socket库,这个库是公共的npm包,即使它只在行程页分包里被使用,但由于它本身路径是在node_modules下的,那么就会将其收集进主包里。

因为早期的一些设计,行程页的资源和首页是分割开的,都比较独立地存在于各自的路径下,一期的分包处理的大头也主要是行程页,它刚好契合了Mpx初期对分包处理上的特点,因此能较好地收集进行程页分包里。

随着业务迭代,后续大量业务线的接入都是通过npm进行的,就会有大量npm包资源,他们都在node_modules目录下,因此全部会被收集进主包。

所以Mpx框架进行了一系列改造:

  1. 在构建的依赖收集过程中,我们会对收集到的依赖打上标记,记录它是被哪些分包引入的。一旦它只有一个分包引入,它就会被输出到这个分包中。
  2. 我们会根据用户定义的分包配置,自动在 SplitChunksPlugin 中生成各个分包的 cacheGroups ,把分包中的复用模块抽取到分包下的bundle中。
  3. 对于组件和静态资源,如果他们被多个分包所引用且未在主包中引用,为了确保主包体积最优,这些资源将产生多份副本分别输出到对应分包中,而不会占用主包体积。

这样一来,不管分包中引用的资源原本在什么位置,最终输出时都会尽可能将其输出到dist的分包目录下,避免占用主包空间

这个改动完成后项目结构看似和之前一样,但得益于Mpx处理分包资源能力的升级,我们得以将业务线分包中引用的npm资源成功输出到其所在的分包目录下。

# 封面方案

再后来滴滴出行小程序需要替换微信/支付宝里原有的WebApp入口,小程序接入的业务线迅速增加,包体积迅速增长。

这个部分体积增长的主要原因前面提到过,所有的业务线都要接入到主页来展示。这也是由于业务特点决定的,滴滴出行提供了丰富的出行产品线,包括快车/专车/出租车/豪华车/拼车/单车/代驾/顺风行车等产品,用户是可能需要反复切换挑选的。这个过程还要保留起终点车型之类的信息,必须是一个页面内切换组件加一整套非常复杂的大型状态管理才能比较流畅顺滑地实现。而不能像一些电商/信息平台,将不同的功能拆分到不同页面,让用户通过首页的菜单进入子页面再进行操作,首页只承载入口,只有较少的业务逻辑,分包处理起来就会容易很多。

因此各个业务线都要提供首页组件进行接入。这个组件会在首页被用到,所以无论如何也拆不到分包里。最终,整个首页主包部分的体积可以分成两个部分:基础库和业务代码。两者的体积占比大概是公共依赖基础库占1M左右,业务代码占1M左右。

这么庞大的基础库体积主要是由于滴滴出行的业务线及业务团队众多,各方均有一些自己的基础依赖。比如网约车依赖的长链接通信pb数据描述文件,地图会依的大数计算库,顺风车依赖的CML框架运行时、代驾依赖的通信网关库,以及公用的组件库和polyfill等。

所以滴滴出行小程序面对的问题在当时已经无法用纯技术方案在短期内快速解决问题了,于是我们做了一个工程架构调整,可以叫封面页方案,解决了主包问题。

封面方案简单讲,就是做一个带滴滴出行Logo的封面作为启动页面,而页面一旦加载,立刻跳转另一个页面,这个页面真正承载业务,且它被放在分包里。

这个操作的意义在于,主包里就只剩下了所有方都要依赖的基础框架/库等,而业务全被抽离到了分包里。

封面方案结构图

这是封面方案完成后项目的结构图,之前很大块的首页业务逻辑被抽出到首页分包中了。

这样一个挪移的操作的结果是我们可以有2M的主包空间来乘放基础的公共的库,有一个2M左右的分包来乘放前面提到的滴滴出行的集成了各种业务的“大主页”。而当时拆下来差不多有1.2M的主包,800K+的业务主分包。

这个改造最优秀的一点在于,后续的业务迭代产生的体积增长几乎全是在业务主分包里,剩下的1.1M+空间留给业务迭代还是比较充裕的。而主包的体积在理想条件下是可以长久保持不变的,就不会因为业务需求的不断开发反复导致主包体积临近超标,不再需要为主包体积感到焦虑。

当然,可以看到,这个方案本身是没有消减任何体积的,只是把位置变换了一下。除此之外,这个封面页方案其实也存在一些缺陷,比如首屏业务的展示会变慢,因为要加载的内容会变多,不过小程序本身有较好的缓存资源的能力,因此还算可以接受。

相比于因体积问题卡住需求迭代以及产品线的接入,目前这个方案至少能解决有无问题。我们开发团队后续也会持续跟进关注体积问题,看是否会有产品方案变更或者小程序本身给出一些解决方案来进一步优化这个部分。

# 总结

Mpx框架在包体积控制上做了大量工作,对于npm场景下的小程序分包进行了非常完善的支持。

滴滴出行小程序团队在框架支持的基础上,通过梳理业务依赖,充分利用分包,调整交互方案等一系列手段,在不阻碍业务发展的前提下,将庞大复杂的滴滴出行小程序包体积控制在平台限制范围内。

希望本文能给在包体积上遇到问题的小程序开发者们带来一些启发,欢迎留言交流。

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/ts-derivation.html b/docs-vuepress/.vuepress/dist/articles/ts-derivation.html new file mode 100644 index 0000000000..37ba492785 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/ts-derivation.html @@ -0,0 +1,273 @@ + + + + + + 使用Typescript新特性Template Literal Types完善链式key的类型推导 | Mpx框架 + + + + + + + + + +

# 使用Typescript新特性Template Literal Types完善链式key的类型推导

作者:anotherso1a (opens new window)

# 前言

Mpx框架中我们采用了类似 Vuex 的数据仓库设计,这使得小程序的数据也可以在框架中得到统一的管理。但由于设计上的原因,这一部分在TS推导的实现上变得异常艰难,其中有一个明显的问题就是链式key的推导:

// 一段 mpx-store 代码实例
+
+createStoreWithThis({
+  // other options ...
+  actions: {
+    someAction() {
+      // 下面这一个dispatch语句如何得到正确的推导?result的类型如何获取?
+      let result = this.dispatch('deeperActions.anotherAction', 1)
+      return result
+    }
+  },
+  deps: {
+    deeperActions: createStoreWithThis({
+      actions: {
+        anotherAction(payload: number) {
+          // do something
+          return 'result' + payload
+        }
+      }
+    }),
+    // other deps ...
+  }
+})
+

我们该如何获取 this.dispatch('deeperActions.anotherAction', 1) 的返回值?如何针对dispatch后续的参数类型做限制?这几乎是不可能解决的问题。

好消息是,在 2020.09.19,typescript团队release了一个beta版本 (opens new window)。这个版本新推出了一个特性:模板字符串类型(Template Literal Types)

这让链式key的推导出现了曙光,不久后,我们开始尝试完善 mpx-store 的推导,此篇文章也是基于我们实践中所做的一些工作总结而来。

# 特性

这里可以直接看官网 (opens new window)例子,可以十分直观的感受到其特性:

type Color = "red" | "blue";
+type Quantity = "one" | "two";
+
+type SeussFish = `${Quantity | Color} fish`;
+//   ^ = type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish"
+

根据例子得知:模板字符串的使用方式几乎和ES6的字符串模板一致,可以做字符串的拼接,当接收字面量联合类型时,会做matrix操作,返回所有可能的拼接后的字面量联合类型。

另一个例子:

type A = '1' | '2'
+type B = 'a' | 'b'
+type C = 'C' | 'D'
+
+type D = `${A}${B}${C}`
+// type D = "1aC" | "1aD" | "1bC" | "1bD" | "2aC" | "2aD" | "2bC" | "2bD"
+

# 可以用来做什么

这个新特性的出现使得在js中常用的链式key取值,调用等,都能够使用ts进行完整的推导。

第一个问题:

function getValueByPath(object, prop) {
+  prop = prop || '';
+  const paths = prop.split('.');
+  let current = object;
+  let result = null;
+  for (let i = 0, j = paths.length; i < j; i++) {
+    const path = paths[i];
+    if (!current) break;
+
+    if (i === j - 1) {
+      result = current[path];
+      break;
+    }
+    current = current[path];
+  }
+  return result;
+}
+
+// 调用例子
+getValueByPath({ a: false }, 'a') // false
+getValueByPath({ a: { b: { c: 1 } } }, 'a.b') // { c: 1 }
+

如何将以上 getValueByPath 函数使用ts改写,使该函数的调用具有完备的类型推断?

第二个问题:

type simpleActions = {
+  actionOne(payload: number): Promise<number>
+  actionTwo(payload: string): Promise<boolean>
+}
+
+declare function dispatch<K extends keyof simpleActions>(type: K): ReturnType<simpleActions[K]>
+
+dispatch('actionOne')
+// function dispatch<"actionOne">(type: "actionOne"): Promise<number>
+

我们很容易为上面的 simpleActions 编写出其对应的 dispatch 函数,并具备完整推导,但针对下面这种深层次的actions,如何为其编写dispatch函数?

const actions = {
+  someAction(payload: number) {
+    return payload
+  },
+  deeperActions: {
+    anotherAction() {
+      // do something
+      return 'result'
+    },
+    otherActions: {
+      finalAction(payload: string) {
+        return !payload
+      }
+    }
+  }
+}
+
+type Actions = typeof actions
+
+// 编写一个 dispatch 方法,使得:
+// let result = dispatch('deeperActions.anotherAction') // 返回类型为: Promise<string>
+// let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // 返回类型为:Promise<boolean>
+

在typescript发布4.1之前,这几乎是不可能完成的任务,但是4.1的发布使我们能够对这一部分的缺失做一些类型补全。

接下来我们围绕上面两个问题分别进行实现。

# getValueByPath使用TS实现

需要使 getValueByPath({ a: { b: { c: 1 } } }, 'a.b') 获得完整的推导,我们首先需要能拿到两个参数的完整类型,这在ts中是十分容易的。第二步则是对两个参数的类型做映射处理,最好使得第一个参数输入完成之后,第二个参数直接能具备完整推导。

# 单层推导

我们可以先尝试着实现对单层对象取值的类型推导:

首先能得到一个大致的函数结构,接受两个参数,返回一个值

declare function getValueByPath(object: Record<any, any>, prop: string): any
+

上面这种写法是拿不到参数类型的,为了能正确拿到参数的类型,我们使用范型来填充参数 object

declare function getValueByPath<T extends Record<any, any>>(object: T, prop: string): any
+

可以直接在编辑器上面尝试一下(我使用的是vscode),很明显可以看到,我们正确的拿到了第一个参数的类型:

xx

能取到参数的类型对象,就能使用关键字 keyof 获取对象的每一个key,接下来我们把候选key值限定给参数prop,然后把对应的返回值结果填充到函数的结果当中,这里我们引入第二个范型 K。尝试调用之后,我们发现结果都符合预期。

declare function getValueByPath<T extends Record<any, any>, K extends keyof T>(object: T, prop: K): T[K]
+
+let a = getValueByPath({a: 1, b: '2'}, 'a') // number
+let b = getValueByPath({a: 1, b: '2'}, 'b') // string
+let c = getValueByPath({a: 1, b: '2'}, 'c') // Argument of type '"c"' is not assignable to parameter of type '"a" | "b"'.ts(2345)
+

# 多层推导

仅仅是单层推导的结构依然是无法满足我们对于 getValueByPath({ a: { b: { c: 1 } } }, 'a.b') 推导的需求:

let a = getValueByPath({ a: { b: { c: 1 } } }, 'a.b') 
+// Argument of type '"a.b"' is not assignable to parameter of type '"a"'.ts(2345)
+

可以看到,第二个候选参数的类型仅有 "a",而我们按正常期望来讲,getValueByPath的第一个参数传入 { a: { b: { c: 1 } } } 之后,第二个候选参数类型应该为: "a" | "a.b" | "a.b.c"

接下来我们使用4.1的新特性,将对象: { a: { b: { c: 1 } } } 与其链式key: "a" | "a.b" | "a.b.c" 一一对应起来。

工欲善其事,必先利其器。我们先封装好两个工具函数:

// 取出对象的所有除了 symbol 以外的key
+type StringKeyof<T> = Exclude<keyof T, symbol>
+
+// 将字符串用 . 进行拼接
+type CombineStringKey<H extends string | number, L extends string | number> = H extends '' ? `${L}` : `${H}.${L}`
+

我们知道,symbol 类型是没办法放在模板字符串中的,所以对key进行拼接之前,需要过滤掉所有 symbol 类型的key。而 StringKeyof 函数,就是用来过滤对象中所有不符合模板字符串类型的key的方法。其实际用法与 keyof 相同,只是返回值能用于模板字符串

CombineStringKey 则是对两个key进行拼接,拼接的结果自然就是我们需要的链式key格式。

调用例子:

const symbol1 = Symbol()
+
+type A = {
+  1: string
+  a: string
+  [symbol1]: string
+}
+
+type K = StringKeyof<A> // 1 | 'a'
+
+type B = CombineStringKey<'', 'a'>         // "a"
+type C = CombineStringKey<B, 'b'>          // "a.b"
+type D = CombineStringKey<C, 'c1' | 'c2'>  // "a.b.c1" | "a.b.c2"
+type E = CombineStringKey<D, 'd'>          // "a.b.c1.d" | "a.b.c2.d"
+

我们现在以下面这个对象类型为例,取出该对象所有符合要求的链式key:

type deepObj = {
+  a: number;
+  b: {
+    c: string;
+    d: number;
+    e: {
+      f: number;
+      g: boolean;
+    };
+  };
+}
+

思路:遍历对象的所有key,同时对key对应的值进行判断,如果是一个更深层次的对象,则保留外层的key,对更深层对象递归处理,把后续递归出的key都和上一层中保留的key进行拼接。

type ChainKeys<T, P extends string | number = ''> = {
+  [K in StringKeyof<T>]: T[K] extends Record<any, any> ? ChainKeys<T[K], CombineStringKey<P, K>> : {
+    [_ in CombineStringKey<P, K>]:T[K]
+  }
+}[StringKeyof<T>]
+

我们将 ChainKeys 函数拆开来看,首先是它的参数,接受两个类型参数,第一个是类型T,第二个则是用于拼接的前缀字符串,其值可选。我们使用 in 语法遍历 T 的所有 key 值,也就是 [K in StringKeyof<T>],这里我们使用了前面封装好的方法 StringKeyof 来代替 keyof 关键字。对对应的 key 的值,也就是 T[K],我们使用 extends 进行前置判断,如果 T[K] 是一个复杂类型,我们则将当前的字符串前缀 P 和当前的 K 进行拼接,递归调用 ChainKeys 函数进行处理。如果 T[K]基本类型,我们直接使用 Record 方法组装拼接好的key(CombineStringKey<P, K>)和value(T[K]),为了方便在编辑器里面查看结果,这里将Record<CombineStringKey<P, K>, T[K]> 替换为 { [_ in CombineStringKey<P, K>]:T[K] }。最后将所有结果打平成联合类型,也就是最后的一个取值操作:{...}[StringKeyof<T>],这和 [1,2,3][number] // 3 | 1 | 2 的处理方式一致。

接下来我们在编辑器中查看一下调用结果:

img

如红框中所示,发现链式key已经和其类型一一对应起来了。

但是这还有一些问题:

一、"b" 以及 "b.e" 没有出现在枚举当中。

二、返回结果是一个联合类型,如何把这个结果运用到函数当中呢?

第一个问题其实很好解决,当我们在递归处理的同时,将当前的链式key与对应值也进行处理就可以,即:

type ChainKeys<T, P extends string | number = ''> = {
+  [K in StringKeyof<T>]: T[K] extends Record<any, any>
+    ? Record<CombineStringKey<P, K>, T[K]> & ChainKeys<T[K], CombineStringKey<P, K>> 
+    : {
+      [_ in CombineStringKey<P, K>]:T[K]
+    }
+}[StringKeyof<T>]
+

同样为了方便编辑器中查看结果我们把 Record<CombineStringKey<P, K>, T[K]> 替换成 { [_ in CombineStringKey<P, K>]:T[K] }

img

可以看到 b 以及 "b.e" 这样的key都被取出来并放到了结果当中。其实这里还是有一些问题,如果你实际去代码编辑器里面查看结果的话,会发现有很多重复的 b ,这是因为后续使用交叉类型进行拼接的 ChainKeys 的返回值是一个联合类型,如此一来,b 会被分配到联合类型的每一项上,在后面我们会解决这个问题。

第二个问题是结果为联合类型,我们如何才能将其运用至函数当中呢?

大家都知道,对联合类型使用 keyof 操作,返回的类型为 never,即无法取出联合类型的所有key值:

type T = { a: number } | { b: string }
+type K = keyof T // never
+// 如何使K的类型为 a | b 呢?
+

这里我们引入另一个工具函数:UnionToIntersection,顾名思义,该函数的作用就是将联合类型转换为交叉类型,至于为什么它能将联合类型转换为交叉类型,我们后面会讲:

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
+

我们将其运用到ChainKeys的推导当中去:

type ChainKeys<T, P extends string | number = ''> = UnionToIntersection<{
+  [K in StringKeyof<T>]: T[K] extends Record<any, any>
+    ? Record<CombineStringKey<P, K>, T[K]> & ChainKeys<T[K], CombineStringKey<P, K>> 
+    : {
+      [_ in CombineStringKey<P, K>]:T[K]
+    }
+}[StringKeyof<T>]>
+

再经过一些润色得到一个比较精简的函数:

type ChainKeys<T, P extends string | number = ''> = UnionToIntersection<{
+  [K in StringKeyof<T>]: Record<CombineStringKey<P, K>, T[K]> & (T[K] extends Record<any, any> ? ChainKeys<T[K], CombineStringKey<P, K>> : {})
+}[StringKeyof<T>]>
+

尝试调用该方法最终得到的结果为:

img

最终,我们以此为基础,编写 getValueByPath 函数的定义:

declare function getValueByPath<T extends Record<any, any>, K extends keyof ChainKeys<T>>(object: T, prop: K): ChainKeys<T>[K]
+
+let a = getValueByPath({ a: { b: { c: 1 } } }, 'a')     // { b: { c: number } }
+let b = getValueByPath({ a: { b: { c: 1 } } }, 'a.b')   // { c: number }
+let c = getValueByPath({ a: { b: { c: 1 } } }, 'a.b.c') // number
+

到此,我们算是完成了 getValueByPath 函数的类型定义以及推导。

# 深层次Actions的dispatch函数TS实现

其实在推导 getValueByPath 的过程中,我们已经基本上完成了所有实现该 dispatch 所需要的条件,与 getValueByPath 不同的是,我们只需要遍历出函数所对应的链式key即可,改写一下 ChainKeys 方法:

interface DeeperActions {
+  [k: string]: ((...args: any[]) => any) | DeeperActions
+}
+type ActionChainKeys<T, P extends string | number = ''> = UnionToIntersection<{
+  [K in StringKeyof<T>]: T[K] extends DeeperActions
+    ? ActionChainKeys<T[K], CombineStringKey<P, K>>
+    : Record<CombineStringKey<P, K>, T[K]>
+}[StringKeyof<T>]>
+

我们除去了对对象的混入,同时将判断类型改为了 DeeperActions ,以此来判断该 action 对象是否有更深层次的actions。

调用结果:

img

再看我们最开始提出的问题:

编写一个 dispatch 方法,使得:

let result = dispatch('deeperActions.anotherAction') // 返回类型为: Promise<string>

let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // 返回类型为:Promise<boolean>

我们开始定义 dispatch 方法:

declare function dispatch<K extends keyof ChainingActions>(key: K, ...args: Parameters<ChainingActions[K]>): ReturnType<ChainingActions[K]>
+

这里我们分别使用了 ParametersReturnType 函数来获取对应 action 的参数以及返回值,用来填充至 dispatch 方法中,实际编写调用代码:

考虑到我们的 actions 无论如何都会返回一个 Promise,上述定义其实还是稍微有一些问题,但是改动起来也很方便,只需要在 ReturnType 外层使用 Promise 包装一下就可以了:

type Promisify<T> = T extends Promise<any> ? T : Promise<T>
+
+declare function dispatch<K extends keyof ChainingActions>(key: K, ...args: Parameters<ChainingActions[K]>): Promisify<ReturnType<ChainingActions[K]>>
+
+let result = dispatch('deeperActions.anotherAction') // Promise<string>
+let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // Promise<boolean>
+

至此,我们 dispatch 的TS实现也已经完成。

最终结果:

  • 若图片加载失败请访问: http://cdn.qiniu.archerk.com.cn/QQ20210128-162139-HD.gif

# 结尾

其实 getValueByPath 这一部分的内容是有一些缺陷的,因为TS本身限制了递归的次数,当数据层级达到一定程度时,上文的推导会失效。在Mpx的建设中我们还对此做了一些优化,尽可能的减少了递归次数。

这里也给出另一个方案做链式key推导,这种方式性能上会好很多,因为不用解析出所有的链式key,而是根据链式key反向解析前面的对象,因此该方案可以解析出正确的类型,但是会丧失一部分代码提示:

type CutChainKeys<T extends string> = T extends `${infer A}.${infer B}` ? [A, B] : [T, never]
+
+type GetValueByPath<T extends Record<string, any>, K extends string> = CutChainKeys<K> extends [string, never]
+  ? T[CutChainKeys<K>[0]]
+  : GetValueByPath<T[CutChainKeys<K>[0]], CutChainKeys<K>[1]>
+
+declare function test<T extends Record<any, any>, K extends string>(o: T, k: K): GetValueByPath<T, K>
+
+let s = test({
+  a: {
+    b: 1,
+    c: {
+      d: {
+        e: {
+          f: 'f'
+        }
+      }
+    }
+  }
+}, 'a.c.d.e.f') // string
+

通过这一系列的处理,我们基本完成了 mpx-store 的类型推导,包括对应的辅助函数等。这也使得我们能够在项目中更好的使用TS进行开发。在最新的滴滴出行小程序更新中,我们已经开始使用TS全量进行开发,目前看来开发体验还不错,后期也会逐步将旧的代码重构为TS。我们后续也会持续维护和迭代框架,也欢迎有兴趣的朋友来一起共建 (opens new window)

有写的不好的地方请多包含,也欢迎大家批评指正。

# 题外话:UnionToIntersection

上面提到的,我们使用工具函数 UnionToIntersection 将联合类型转换为交叉类型。而 UnionToIntersection 又是如何将联合类型转换为交叉类型的呢?

Conditional Types (opens new window) 中有提到一些点:

# Distributive conditional types

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

即在执行 extends 语句时,如果 extends 左侧为联合类型,则会被分配成对应数量个子条件判断语句,最终联合每个子语句的结果。

type A = 1 | '2' | 3
+
+type IsNumber<T> = T extends number ? T : never
+
+type B = IsNumber<A> // 1 | 3
+

# Type inference in conditional types

Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.

在条件判断语句中,可以使用 infer 关键字做类型推断,同一个候选值能有多个推断位置。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
+

正位推断的结果会被推导为联合类型:

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
+type T10 = Foo<{ a: string; b: string }>; // string
+type T11 = Foo<{ a: string; b: number }>; // string | number
+

逆变位置推断的结果会被处理成交叉类型:

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
+  ? U
+  : never;
+type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
+type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
+

注意:这里是ts-2.8发布时的文档以及例子,现在来说是不存在 string & number 类型的,如果实际编写这段代码的话会发现 T21 的值为 never,因为不可能存在一个值既为 string 又为 number。逆变与协变的一些概念可以参考一下逆变与协变 (opens new window)

# UnionToIntersection 原理探究

有了以上两个信息之后,我们再来看看 UnionToIntersection 的实现,为了方便阅读我们做一些折行:

type UnionToIntersection<U> = (
+    U extends any 
+      ? (k: U) => void
+      : never
+  ) extends ((k: infer I) => void) 
+    ? I
+    : never;
+

发现实际上 UnionToIntersection 分为两步:

1、将联合类型的每一项填充至 函数类型 (k: U) => void 的参数 k 中。

2、使用 infer 关键字在逆变位(k: U) => void 做推导,将参数的类型推入结果 I 中,组成交叉类型。

拆分看实现:

// 联合类型推入函数参数
+type UnionToFunction<U> = U extends any ? (k: U) => void : never
+// 对函数逆变推导,将参数推导为交叉类型
+type FunctionsToIntersection<F> = [F] extends [(k: infer I) => void] ? I : never
+
+type Union = { a: number } | { b: string }
+
+type Fun = UnionToFunction<Union> // ((k: { a: number }) => void) | ((k: { b: string }) => void)
+
+type Intersection = FunctionsToIntersection<Fun> // { a: number } & { b: string }
+

可以注意到我们拆分开来了 UnionToIntersection 后并不是完全按照原有写法去写的,在 FunctionsToIntersection 函数中,我们使用了[]将类型给包裹起来,这是为了避免第一个特性(Distributive conditional types)的影响,如果不做包裹,则 extends 左侧为联合类型,被分配成若干个子句后,同一位置的 infer 就会被分别执行,无法合成为交叉类型。而使用[]进行包裹之后则避免了这个影响,extends 左侧不再是一个联合类型,而是一个数组类型。

# 参考资料汇总

+ + + diff --git a/docs-vuepress/.vuepress/dist/articles/unit-test.html b/docs-vuepress/.vuepress/dist/articles/unit-test.html new file mode 100644 index 0000000000..cc828c450b --- /dev/null +++ b/docs-vuepress/.vuepress/dist/articles/unit-test.html @@ -0,0 +1,461 @@ + + + + + + Mpx 小程序单元测试能力建设与实践 | Mpx框架 + + + + + + + + + +

# Mpx 小程序单元测试能力建设与实践

作者:Blackgan3 (opens new window)

# 什么是单元测试

In computer programming, unit testing is a software testing method by which individual units of source code—sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures—are tested to determine whether they are fit for use wikipedia

对一个函数,模块,类进行运行结果正确性检验的工作就是单元测试,此外每个单元测试的对象应该是一个最简单的组件/函数。

那写单测又能给我们哪些收益呢?

  • 大幅提高项目代码可维护性
  • 覆盖率到达一定指标后可大幅提高研发效率
  • 让你的代码零线上bug锐减,上线不再提心吊胆
  • 改进设计,促进重构

此外,单测高覆盖率的项目也会给公司节省大量支出: +unit-test-money.png +据微软统计单测效益结果,如上图展示,绝大多数bug都是在coding阶段产生,并且随着需求开发进度的推进,修复bug的成本会随指数级增长,当我们在unit test阶段发现并修复这个bug是能给公司带来巨大收益的。

下方介绍两种常见的项目单元测试规范:

  • TDD (Test driven development)

测试驱动开发,在书写业务代码之前,先根据需求进行单元功能测试用例书写

这里驱动开发的不是简单的测试用例,是能够持续验证、重构并且对需求功能极致细化的测试用例

  • BDD (Behavior driven development)

行为驱动开发,通过具有辨识力的测试用例驱动项目开发团队所有人,使用自然语言来描述功能,一般和 TDD 相结合,针对行为进行测试,让开发者在写单测时从专注代码实现转为业务行为,能使单测更加场景化和智能化

单元测试的书写初期必定伴随着大量精力与时间的消耗,但长期持续维护的业务在搭建并完善好整个单元测试体系后,可大大提高项目稳定性和研发效率

# 前端单元测试

# 前端单元测试工具

前端单元测试目前有很多框架和工具,我们下方列出三个较为流行的框架和工具库进行介绍

  • Mocha: 功能丰富的 javascript 测试框架(不包括断言和仿真环境,快照测试需额外配置),可以运行在node.js和浏览器中
  • Jasmine:Behavior-Drive development(BDD)风格的测试框架,在业内较为流行,功能很全面,自带asssert、mock功能
  • Jest:一个功能全面的 javascript 测试框架,基于Jasmine 做了大量新特性(例如并行执行、源代码改动感知等),开箱即用,适用于绝大多数 js 项目

# 测试断言库

在单测运行框架中,我们需要断言库来进行方法返回和实例状态的正确性验证

  • should: BDD风格断言 (true).should.be.ok
  • expect: expect()样式断言 expect(true).toBe(true)
  • assert: Node.js 内置断言模块 assert(true === true)
  • chai: expect(),assert()和should风格的断言都支持,全能型选手

在众多前端单元测试框架中,Jest 目前凭借零配置,高性能,且对于断言,快照,覆盖率等都有很好的集成,是目前较为流行的一个单测框架

# Jest 框架简介

简单来看下 Jest 框架的特点以及大致的运行原理

Jest 的整体框架特点大概归纳总结为以下几点:

  • 在操作系统上高效的进行文件搜索以及相互依赖关系匹配
  • 单测并行执行,运行效率高
  • 内置断言库、覆盖率、快照测试等功能,开箱可用
  • 使用 vm 来进行沙盒环境运行,单测之间相互隔离

image.png +打开 Jest pacakges,可以看到大概有50多个包,我们根据这些不同的包来将整个 jest 运行流程整体串起来

第一步 jest-cli 读取相关配置 +当我们执行 jest 命令时,先去执行 jest-cli 中的 run 方法,再调用 jest-core 中的 runCli 方法,其中通过 jest-config 提供的 readConfigs 来读取 Jest 相关配置,返回全局配置(globalConfig)和局部配置(configs)

unit-test-jest-step1.png

第二步 文件静态分析 +使用 jest-haste-map 来进行项目中所有文件的检索以及生成文件之间的相互依赖关系,在 jest-core 中的 _run10000 方法中执行 buildContextsAndHasteMaps,返回 contexts 和 hasteMapInstances,contexts 中的 hasteFs 存储的就是文件及依赖关系。

**jest-haste-map **检索的过程中借助 jest-worker 来根据当前cpu核数并行的进行文件检索,借助 fb-watch-man/crawler 对整体文件变动做实时监听,做到只执行有改动的单元测试文件,实现单测缓存效果。

unit-test-jest-haste.png +下方看一个简单的jest-haste-map使用示例

import JestHasteMap from 'jest-haste-map';
+import {cpus} from 'os';
+
+const hasteMap = new JestHasteMap.default({
+  extensions: ['js'],
+  maxWorkers: cpus().length,
+  name: 'test',
+  platforms: []
+});
+
+const {hasteFS} = await hasteMap.build();
+const testFiles = hasteFS.getAllFiles();
+
+console.log(testFiles);
+// ['/path/to/tests/list1.spec.js', '/path/to/tests/list2.spec.js', …]
+

第三步 单测检索和排序 +经过第一步和第二步,我们拿到了 配置对象 configs,以及文件Map HasteContext,接下来通过 SearchSource 对象检索出所有的单元测试到一个数组中,检索出单元测试文件后,在正式执行之前,我们需要先对当前拿到的所有单测进行权重优先级排序。

单测排序的工作是由 jest Sequencer 完成的,默认排序优先级为 failed (上次失败的先运行)> duration(耗时长的先运行) > size(文件体积大的先运行),当然这里我们也可以自定义customSequencer来覆盖 jest 默认的排序规则,jest 排序规则如下。

unit-test-jest-scquencer.png

		return tests.sort((testA, testB) => {
+      if (failedA !== failedB) {
+        return failedA ? -1 : 1;
+      } else if (hasTimeA != (testB.duration != null)) {
+        // If only one of two tests has timing information, run it last
+        return hasTimeA ? 1 : -1;
+      } else if (testA.duration != null && testB.duration != null) {
+        return testA.duration < testB.duration ? 1 : -1;
+      } else {
+        return fileSize(testA) < fileSize(testB) ? 1 : -1;
+      }
+    });
+

第四步 开始执行 +在经过第三步之后,我们拿到了经过排序后的单测文件,接下来开始进入到执行步骤,执行单测时的整个调度工作是 jest/core 中的 TestScheduler 来完成,例如 scheduler 会推算是串行执行还是并行执行,推算单测执行完的大概时间,覆盖率报告的生成等,scheduler 会调用 jest-runner 中的 runTests 方法去触发单测执行

如果需要并行执行,runTest 方法触发 jest-worker 创建多个child process 子进程来支持 parallel 执行

单元测试中全局方法和全局变量,比如 test() describe() it() 等是由 jest-cirucs(jest-jasmine) 提供并注入global中

unit-test-jest-run-test.png +最终单测的运行是由 jest-runtime 中创建的 vm 虚拟机隔离执行,vm 作用域中dom环境是由 jest-environment-jsdom 提供,此外 jest-runtime 中还包括了 transformer 能力以及mock功能的具体实现等,这部分功能在接下来的Mpx框架单元测试实现章节我们会去详细介绍它。

第五步 处理返回结果 +此外 jest-runner中提供了一套类似于 redux 的数据流机制和eventEmitter来管理维护单测状态以及单测执行结果,在jest-runner 中进行事件触发,在TestScheduler 中进行事件监听并对执行结果进行各种处理和序列化, +最后在 jest-core 中的runJest方法中进行执行结果的终端输出/文件输出等一系列处理。

# 小程序单元测试

# 与 web 应用的不同

上个章节讲完前端单测简介,以及jest单测框架的大概运行原理后,接下来我们看下单元测试在小程序场景下与web场景的不同

首先小程序本身是双线程分离的机制,但目前并没有这种独特的运行环境用来执行单元测试,这里需要借助小程序官方提供的 miniprogram-simulate 工具集,来将整体运行机制调整为单线程模拟运行,并利用 dom 环境来进行小程序组件的注册渲染以及整个自定义组件树的搭建

小程序的单元测试执行依赖 js 运行环境和 dom 环境,这里我们选择 jest 框架来提供对应的环境

下方是一个简单的微信小程序官方提供的单元测试demo,具体关于miniprogram-simulate 的更多API的使用可以去官方文档查看 https://github.com/wechat-miniprogram/miniprogram-simulate (opens new window)

import simulate from 'miniprogram-simulate'
+test('comp', () => {
+    const id = simulate.load(path.join(__dirname, './comp')) // 注册自定义组件,返回组件 id
+    const comp = simulate.render(id) // 使用 id 渲染自定义组件,返回组件封装实例
+
+    const parent = document.createElement('parent-wrapper') // 创建容器节点
+    comp.attach(parent) // 将组件插入到容器节点中,会触发 attached 生命周期
+
+    expect(comp.dom.innerHTML).toBe('<div>123</div>') // 判断组件渲染结果
+    // 执行其他的一些测试逻辑
+
+    comp.detach() // 将组件从容器节点中移除,会触发 detached 生命周期
+})
+

此外对于小程序工具集的整体运行流程,在下方章节进行了简要总结。

# 小程序单测框架整体流程

小程序单元测试中微信官方提供的相关库有 miniprogram-simulate、j-component 和 miniprogram-exparser等

  • miniprogram-simulate: 小程序自定义组件测试工具集,进行小程序内置组件的注册以及模拟微信原生api的注入
  • j-component: 仿小程序组件系统,可以让小程序自定义组件跑在 web 端。
  • miniprogram-exparser:微信小程序官方的组件系统模块,exparser 的组件模型和 WebComponents标准中的 Shadow DOM 高度相似,基于 Shadow DOM 原型,可在纯 JS 环境运行,维护整个组件的节点树相关的信息,包括属性、事件等。
  • miniprogram-compiler: wcc、wcsc 官方编译器的 node 封装版,用于编译 wxml、wxss 文件

开发者在使用的时候经常用的的两个方法就是 simulate.load 和 simulate.render 方法,那这里我们就从这两个方法为入口进行整个流程的总结 +1.miniprogramSimulate.load(path) 小程序-load.png

let nowLoad
+// miniprogram-simulate
+function load(path, definition){
+  // 省略部分判断
+  const id = register(componentPath, tagName, cache, hasRegisterCache)
+  cache.needRunJsList.forEach(item => {
+    // nowLoad 用于执行用户代码调用 Component 构造器时注入额外的参数给 j-component
+    nowLoad = item[1]
+    // require('xxx.js')
+    _.runJs(item[0])
+  })
+	return id
+}
+function register(componentPath, tagName, cache, hasRegisterCache) {
+  // 随机生成,不重复
+  const id = _.getId()
+  const component = {
+    id,
+    path: componentPath,
+    tagName,
+    json: _.readJson(`${componentPath}.json`),
+   	wxml: compile.getWxml(componentPath, cache.options),
+    wxss: wxss.getContent(`${componentPath}.wxss`)
+  }
+  // 存入需要执行的自定义组件 js
+  cache.needRunJsList.push([componentPath, component])
+  return component.id
+}
+global.Component = options => {
+	const component = nowLoad
+  jComponent.register(definition)
+}
+
+function register(definition = {}) {
+    const componentManager = new ComponentManager(definition)
+    return componentManager.id
+}
+
+// ComponentManager 方法
+class ComponentManager {
+    constructor(definition) {
+        // ......
+        this.exparserDef = this.registerToExparser()
+        _.cache(this.id, this)
+    },
+    registerToExparser() {
+    ...
+        const exparserDef = {
+            is: this.id,
+            using,
+            generics: [], // TODO
+            template: {
+                func: this.generateFunc,
+                data: this.data,
+            },
+            properties: definition.properties,
+            data: definition.data,
+            methods: definition.methods,
+            ...
+        }
+        // miniprogram-exparser
+        return exparser.registerElement(exparserDef)
+    }
+}
+
+

2.miniprogramSimulate.render(id)

微信小程序-render.png +miniprogram-simulate中render方法会调用j-component create,根据id从缓存对象中获取componentManager,进行组件实例创建

  /**
+   * 创建组件实例
+   */
+  create(id, properties) {
+    const componentManager = _.cache(id)
+
+    if (!componentManager) return
+
+    return new RootComponent(componentManager, properties)
+  },
+

RootComponent 构造函数中使用之前的 _exparserDef 对象进行真实dom节点创建,生成 _exparserNode

class RootComponent extends Component{
+	constructor(componentManager, properties) {
+  	...
+    this._exparserNode = exparser.createElement(tagName || id, exparserDef) // create exparser node and render
+		...
+    this._bindEvent() // touchstart,touchemove blur 等事件绑定
+  }
+}
+

新生成的 rootComponent 实例继承Component对象,定义了许多我们单测中需要用到的组件方法

class Component {
+	get dom() ...
+  get data() ....
+  get instance ...
+  dispatchEvent ...
+  addEventListener ...
+  removeEventListener ...
+  querySelector ...
+  setData ...
+  triggerLifeTime ...
+  triggerPageLifeTime ...
+  toJSON...
+}
+

当然中间还有很多细节实现,比如模版渲染 j-component/template/compile,组件更新 j-component/render 等,感兴趣的话可以详细去看下里边具体的实现,这里我们暂且按下不表。至此,我们拿到了 component 实例,并可以进行正常的组件状态获取以及更新,然后在Jest框架中去断言组件的各种属性以及方法执行后的预期。

# Mpx 框架单元测试

经过上方 Jest 框架讲解以及小程序单元测试流程分析后,接下来看下在Mpx框架中的单测能力支持实现

# 初期版本

Mpx框架的初期单测架构,是将Mpx框架开发的小程序项目,先构建编译为源码,再使用 miniprogram-simulate + j-component + jest 对构建后的小程序原生代码运行单元测试 +mpx-old-unit-test-architecture.png +该方案执行任何case都需要执行完整的构建流程,而且预构建已经完成了所有的模块收集,无法使用jest提供的模块mock功能,导致业务使用成本很高,落地困难。

# 优化版本

经过调研,Jest 本身支持代码转换功能

Jest在项目中以JavaScript的代码形式运行,但是如果使用一些Node.js不支持的,却可以开箱即用的语法(如JSX,TypeScript中的类型,Vue模板等)那你就需要将代码转换为纯JavaScript,转换的工作就是transformer

这里我们就可以通过Jest提供的转换能力编写mpx-jest转换器,实现在Jest模块加载过程中实时地将当前的Mpx组件编译转换为原生小程序组件,再交由miniprogram-simulate加载运行测试case。

该方案中模块加载完全基于Jest并能实现按需编译,完美规避旧方案中存在的缺陷,缺点在于编译构建流程基于Jest api重构,与Mpx自身基于Webpack的构建流程独立存在,带来额外维护成本,这一问题我们通过将通用的编译转换逻辑抽离出来统一维护,在Webpack和Jest两侧复用,从而解决了改问题。同时在实现这个方案的过程中也做了一部分对应库的改动,将会在下方介绍。

首先来看下 jest-runtime 中 transform 的整体流程。

  • runTest 方法调用 runtime.requireModule(path),传入对应的单测文件地址
  • 判断是否是mock module,如果是则直接走 requireMock方法,否则继续往下进行
  • 定义 localModule
  • 调用 this._loadModule
  • _createRequireImplementation(module, options) 赋值给module.require
  • transformFile 处理对应的文件
  • createScriptFromCode(transformdCode)
  • getVmContext 使用 vm 创建沙盒环境
  • 在沙盒环境执行对应的 jest 单测代码

jest-runtime1.png +其中关键节点的代码如下

			// 自定义 localModule
+			const localModule = {
+        children: [],
+        exports: {},
+        filename: modulePath,
+        id: modulePath,
+        loaded: false,
+        path: path().dirname(modulePath)
+      };
+      // 自定义 module.require
+			Object.defineProperty(module, 'require', {
+        // rquireModuleOrMock || rquireInternalModule
+      	value: this._createRequireImplementation(module, options)
+    	});
+
+			// transformCode
+			const transformedCode = this.transformFile(filename, options);
+			
+			// createScriptFromCode
+			const script = vm.script('({"' + EVAL_RESULT_VARIABLE + `":function(${args.join(',')}){` + transformedCode + '\n}});';)
+			const vmContext = this._environment.getVmContext();
+      runScript = script.runInContext(vmContext, {
+        filename
+      })
+			compiledFunction = runScript[EVAL_RESULT_VARIABLE]
+      compiledFunction.call(
+        module.exports,
+        module, // module object
+        module.exports, // module exports
+        module.require, // require implementation
+        module.path, // __dirname
+        module.filename, // __filename
+        // @ts-expect-error
+        ...lastArgs.filter(notEmpty),
+      );
+

上方是整个 jest-runtime 中对于require module 时transform的整体流程。在Jest的这一能力之上,我们基于 @mpxjs/webpack-plugin 开发了 mpx-jest transformer,实现将 Mpx 单文件组件转换输出为对应的小程序原生代码。 +改良方案01.png +在完成代码转换后,对应的 script 代码做为String存在于内存中,无法直接使用 Jest 环境的 require 加载执行,为此我们参考上述 jest-runtime 中的 loadModule方法实现了requireFromString方法,核心还是使用node vm 模块来进行 script 代码的执行,同时将jsdom-environment 和 Jest global 对象合并做为 vmContext

改造方案2.png +至此,我们的整体单测方案就大致完成,通过 mpx-jest 转换组件,再交由 miniprogram-simulate 来渲染组件实例,从而完成对组件状态的断言,在实际测试的过程中,还遇到了以下问题进行解决。

1.Mpx框架的文件纬度的条件编译支持 +Mpx框架的跨平台输出部分支持对文件进行平台和应用的条件编译引用

// 文件列表
+example.wx.mpx
+example.ali.mpx
+
+// 代码使用
+{
+	example: '../src/example'
+}
+

这里我们需要在Jest运行时环境中提供该功能,借助Jest本身提供的 resolver 自定义能力,让我们可以对请求的文件进行自定义resolve功能,这里我们使用Mpx中现有的resolve plugin 和 enhanced-resolve来自定义resolve方法进行文件条件编译的支持。

const { CachedInputFileSystem, ResolverFactory } = require('enhanced-resolve')
+const AddModePlugin = require('@mpxjs/webpack-plugin/lib/resolver/AddModePlugin')
+
+module.exports = (request, options) => {
+  const addModePlugin = new AddModePlugin('before-file', 'wx', {
+    include: () => true
+  }, 'file')
+  // create a resolver
+  const myResolver = ResolverFactory.createResolver({
+    ...
+    plugins: [addModePlugin]
+  })
+  let result = myResolver.resolveSync({}, options.basedir, request)
+	return result.split('?')[0]
+}
+

2.miniprogram-simulate 定制化方法 +在小程序单元测试的章节中,我们介绍了小程序相关库的运行机制,miniprogram-simulate提供的 load 方法默认只解析渲染原生组件,我们的Mpx组件,mpx-jest 转换器无法和miniprogram-simulate进行关联,所以这里我们选择fork miniprogram-simulate 仓库,自定义了loadMpx和registerMpx方法。

// 使用
+import simulate from '@mpxjs/miniprogram-simulate'
+const id = simulate.loadMpx('src/components/list.mpx')
+
+//@mpxjs/miniprogram-simulate 中 loadMpx 实现简单描述
+function loadMpx(path, tagName, options = {}) {
+	// ...省略一部分校验逻辑
+  // .mpx 结尾文件会经过 mpx-jest 进行转换,输出 wxml,wxss,json,script
+  const componentContent = require(componentPath)
+  id = registerMpx(componentPath, tagName, cache, hasRegisterCache, componentContent)
+  //....
+  return id
+}
+function registerMpx(...){
+  // 部分 require('xx/xx.json') 等修改为直接赋值
+}
+
+

对于组件/页面在存在大量组件引用的情况下,mock引用组件可大大提升单测的运行速度,原有miniprogram-simulate框架并没有提供mock功能,所以这里我们也自定义了mockComponent和clearMockComponent方法。

// 代码在 @mpxjs/miniprogram-simulate 中
+let mockComponentMap = {}
+
+function registerMpx() {
+	// 判断是否是mock的组件
+  if (mockComponentMap[tagName]) {
+    componentPath = mockComponentMap[tagName]
+  }
+}
+/**
+ * mock usingComponents中的组件
+ * @param compName
+ * @param compDefinition
+ */
+function mockComponent(compName, compDefinition) {
+  mockComponentMap[compName] = compDefinition
+}
+/**
+ * 清除 component mock 数据
+ */
+function clearMockComponent() {
+  mockComponentMap = {}
+}
+
+// 单测中使用时
+simulate.mockComponent('list', {
+  template: '<view>component list</view>'
+})
+

3.封装定制test-utils工具集 +书写单测的过程中我们会有很多重复工作,比如创建挂载组件、代理接口、模拟多个组件、断言多个数据等,这里我们将这些共性的方法抽离封装成了 test-utils

/**
+ * 创建渲染并挂载自定义组件
+ * @param {组件基于所在项目的相对路径,例如'src/subpackage/gulfstream/components/bottom/bottom.mpx'} compPath
+ * @returns 用于测试的自定义组件
+ */
+export function createCompAndAttach (compPath, renderProps = {}) {
+  const id = simulate.loadMpx(compPath)
+  let comp = simulate.render(id, renderProps)
+  const parent = document.createElement('div')
+  comp.attach(parent)
+  return comp
+}
+
+/**
+ * 借助xfetch构造mock请求
+ * @param {待模拟url} mockUrl
+ * @param {mock数据文件路径} mockFilePath
+ */
+export function proxyFetch (mockUrl, mockUrlData) {
+  let mockData
+  mpx.xfetch.fetch = jest.fn( (options) => {
+    return new Promise((resolve) => {
+      if (options.url.includes(mockUrl)) {
+        if (typeof mockUrlData === 'string') {
+          mockData = getMockContent(mockUrlData)
+        } else {
+          mockData = mockUrlData
+        }
+      }
+      resolve({
+        errno: 0,
+        data: mockData
+      })
+    })
+  })
+}
+......
+

至此,Mpx框架的单元测试方案整体上就完备了,整体上的方案架构如下图所示 +Mpx单测架构图.png

# Mpx组件单元测试实战

在上方介绍过整体的jest框架流程以及Mpx框架单元测试架构后,接下来我们着手进行 Mpx 框架开发的小程序组件的单元测试用例书写实战

使用 @mpxjs/cli 创建模版项目时选择使用单元测试,会自动生成有单测能力的模版项目,和普通 Jest + miniprogram-simulate 搭建的原生小程序单测项目不同的是,transform 中添加了 Mpx 文件的处理,这里jest.config.js其他配置就不过多列出,可通过新创建项目进行查看。

	transform: {
+    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
+    '^.+\\.mpx$': '<rootDir>/node_modules/@mpxjs/mpx-jest'
+  }
+

首先我们列出一个组件示例,具体项目可点击链接查看: +https://github.com/Blackgan3/mpx-unit-test-demo (opens new window)

首先对于组件单测的书写,我们希望有一套固定的规范,即所写的不同组件的单测能有一个相同的格式和顺序,这里我们建议的顺序为

  • 组件初始化断言
  • 组件初始化各种形态断言
  • 组件变化- data|computed|watch变化断言
  • 组件变化- 事件触发断言
  • 组件销毁断言

根据上方组件功能,首先我们建议对组件usingComponents 引入的组件进行模拟注册,这样可以节省组件渲染挂载的时间

	beforeEach(() => {
+    // 进行usingComponents 组件 mock
+    testUtils.mockComponents([
+      'list'
+    ])
+  })
+

1.我们首先需要对组件初始化成功后进行各种组件形态预期,即组件初始化断言

	it('test component init correctly', function () {
+    const comp = testUtils.createCompAndAttach(compPath)
+    const insData = comp.instance.data
+    testUtils.checkExpectedData(insData, {
+      someClassShowOne: false,
+      someClassShowTwo: false,
+      someClassShowTwoFlag: false,
+      listData2: ["手机", "电视", "电脑"],
+      key: 1,
+      compStatus: 1,
+      timeDeferFlag: false
+    })
+    const domHTML = comp.dom.innerHTML
+    // 进行组件初始化dom快照测试
+    expect(domHTML).toMatchSnapshot()
+  })
+

上方我们使用工具方法创建并挂载生成组件实例,然后对组件data中key值和value进行预期断言,最后对组件初始化生成的HTML进行快照测试

2.组件初始化各种形态断言 +当组件的初始化数据源有多种形态,比如你的组件初始化数据是由接口或者其他重要的props传递过来决定的,那这里我们可以对不同的数据源组件的不同表现进行断言

	it ('test comp different data', async function () {
+    // 进行唯一数据源请求的接口代理
+    proxyFetch('api/somePackage/getCompData', {
+      status: 1
+    })
+    const comp = testUtils.createCompAndAttach(compPath)
+    // 目前 status 为 1,再次改变数据源代理
+    proxyFetch('api/somePackage/getCompData', {
+      status: 2
+    })
+    await comp.instance.fetchCompData()
+    await comp.instance.$nextTick()
+    expect(comp.instance.data.compStatus).toBe(2)
+    expect(comp.instance.data.compData).toEqual({status: 2})
+    expect(comp.dom.innerHTML).toMatchSnapshot()
+		// 再次改变数据源代理,修改源数据
+    proxyFetch('api/somePackage/getCompData', {
+      status: 3
+    })
+    await comp.instance.fetchCompData()
+    await comp.instance.$nextTick()
+    expect(comp.instance.data.compStatus).toBe(3)
+    expect(comp.instance.data.compData).toEqual({status: 3})
+    expect(comp.dom.innerHTML).toMatchSnapshot()
+  })
+

3.组件变化- props|data|computed|watch变化断言 +接下来我们需要对组件自身的 props|data|computed|watch 等属性变化时所触发的组件相应变化做出各种预期。

	// 组件 props 改变后组件的各种形态预期
+	it('test props different values correspond to different performance', function () {
+    // 传入初始渲染 props
+    const successContent = 'some successContent'
+    const comp = testUtils.createCompAndAttach(compPath, {
+      successContent
+    })
+    const childComp = comp.querySelector('.successContent')
+    expect(childComp).toBeDefined()
+    expect(comp.instance.data.successContent).toBe(successContent)
+    // 对最终的HTML进行快照测试
+    expect(childComp.dom.innerHTML).toMatchSnapshot()
+  })
+	// 当组件data中的someClassShowOne改变之后需要做的哪些预期
+	// 当组件computed中的someCompShow改变之后需要做的预期
+	// ......
+	
+

4.组件必不可少的会有方法,这里我们对示例组件的方法调用进行预期

	it ('test someClassShowTwoFlag tap event trigger', async function () {
+    const comp = testUtils.createComp(compPath)
+    const fetchCompDataSpy = jest.spyOn(comp.instance, 'fetchCompData')
+    const changeSomeClassShowTwoFlagSpy = jest.spyOn(comp.instance, 'changeSomeClassShowTwoFlag')
+    proxyFetch('api/somePackage/getCompData', {
+      status: 1
+    })
+    comp.attach(document.body)
+    const compData = comp.instance.data
+    const changeFlagViewComp = comp.querySelector('.changeFlagClass')
+
+    // dispatchEvent 为异步
+    changeFlagViewComp.dispatchEvent('tap')
+    await testUtils.sleep(0)
+
+    expect(changeSomeClassShowTwoFlagSpy).toBeCalledWith(true)
+    expect(fetchCompDataSpy).toHaveBeenCalledTimes(2)
+    expect(compData.someClassShowTwo).toBeTruthy()
+    expect(comp.instance.someClassShowTwoFlag).toBeTruthy() // 此处注意 非template中使用过到的data,获取更新后的值,从instance中获取
+    expect(comp.dom.innerHTML).toMatchSnapshot()
+  })	
+
+	it('test someTimeDeferAction tap event trigger', async function () {
+    jest.useFakeTimers()
+    jest.spyOn(global, 'setTimeout')
+
+    const comp = testUtils.createCompAndAttach(compPath)
+    const compData = comp.instance.data
+    const someTimeDeferActionSpy = jest.spyOn(comp.instance, 'someTimeDeferAction')
+    const childComp = comp.querySelector('.someTimeDeferActionClass')
+    childComp.dispatchEvent('tap')
+
+    // comp.instance.someTimeDeferAction()
+    await Promise.resolve()
+
+    jest.runAllTimers()
+    await comp.instance.$nextTick()
+
+    expect(compData.timeDeferFlag).toBeTruthy()
+    expect(someTimeDeferActionSpy).toHaveBeenCalledTimes(1)
+    expect(comp.dom.innerHTML).toMatchSnapshot()
+    jest.useRealTimers()
+  })
+

以上,我们对当前的示例组件完成了整体内容的单元测试书写,完整版单测文件可点击链接查看 +https://github.com/Blackgan3/mpx-unit-test-demo/blob/master/test/components/example.spec.js (opens new window)

# 结语

通篇文章我们依次进行了前端常用单测框架简介,jest框架原理总结,小程序单元测试内部执行流程,最后介绍Mpx框架中单测能力的支持实现以及Mpx组件单测实战。

学习到了jest不仅仅是一个单元测试框架,你甚至可以使用它的各个工具库自己创建一个单元测试框架;以及感受到小程序场景下单元测试的差异化;Mpx框架层面也做了诸多改造来支撑单测功能的落地。

后续我们还会继续跟进推动业务中落地TDD规范,复杂逻辑组件中各种功能场景单测用例规范的补充等问题,持续在小程序单测方向深耕并有更好的规范总结产出。

参考文章:

+ + + diff --git a/docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css b/docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css new file mode 100644 index 0000000000..e0aefebd3c --- /dev/null +++ b/docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css @@ -0,0 +1,3 @@ +.search-box{display:inline-block;position:relative;margin-right:1rem}.search-box input{cursor:text;width:10rem;height:2rem;color:#4e6e8e;display:inline-block;border:1px solid #cfd4db;border-radius:2rem;font-size:.9rem;line-height:2rem;padding:0 .5rem 0 2rem;outline:none;transition:all .2s ease;background:#fff url(/assets/img/search.83621669.svg) .6rem .5rem no-repeat;background-size:1rem}.search-box input:focus{cursor:auto;border-color:#3eaf7c}.search-box .suggestions{background:#fff;width:20rem;position:absolute;top:2rem;border:1px solid #cfd4db;border-radius:6px;padding:.4rem;list-style-type:none}.search-box .suggestions.align-right{right:0}.search-box .suggestion{line-height:1.4;padding:.4rem .6rem;border-radius:4px;cursor:pointer}.search-box .suggestion a{white-space:normal;color:#5d82a6}.search-box .suggestion a .page-title{font-weight:600}.search-box .suggestion a .header{font-size:.9em;margin-left:.25em}.search-box .suggestion.focused{background-color:#f3f4f5}.search-box .suggestion.focused a{color:#3eaf7c}@media (max-width:959px){.search-box input{cursor:pointer;width:0;border-color:transparent;position:relative}.search-box input:focus{cursor:text;left:0;width:10rem}}@media (-ms-high-contrast:none){.search-box input{height:2rem}}@media (max-width:959px) and (min-width:719px){.search-box .suggestions{left:0}}@media (max-width:719px){.search-box{margin-right:0}.search-box input{left:1rem}.search-box .suggestions{right:0}}@media (max-width:419px){.search-box .suggestions{width:calc(100vw - 4rem)}.search-box input:focus{width:8rem}} + +/*! @docsearch/css 3.8.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,0.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,0.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,0.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,0.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,0.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,0.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,0.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 rgba(3,4,9,0.30196078431372547);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,0.5),0 -4px 8px 0 rgba(0,0,0,0.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::-moz-placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.logo[data-v-3ad594c3]{background-image:url(https://dpubstatic.udache.com/static/dpubimg/imdk1FF2QF/logo_color.png);background-size:135px 35px;background-repeat:no-repeat;font-size:0;width:135px;height:35px;margin-right:50px}.nav[data-v-3ad594c3]{margin-left:50px}.header-menu[data-v-3ad594c3],.row[data-v-3ad594c3]{display:flex;align-items:center}.header-menu[data-v-3ad594c3]{width:100%;justify-content:space-between;z-index:101;position:fixed;left:0;top:0;line-height:60px;padding:0 16px;box-sizing:border-box;background:#f6f6f6}.header-menu-icon[data-v-3ad594c3]{display:inline-block;padding-left:12px}.head-container[data-v-3ad594c3]{width:100%;height:3.5rem;display:flex;align-items:center;line-height:2.2rem;z-index:100;position:fixed;left:0;top:0;-webkit-backdrop-filter:saturate(180%) blur(1rem);backdrop-filter:saturate(180%) blur(1rem);background-color:hsla(0,0%,100%,.8);box-shadow:0 2px 8px #f0f1f2;padding:.5rem 3rem}.nav-link[data-v-3ad594c3]{color:#3a495d;display:flex;align-items:center;width:100%;justify-content:space-between}a.router-link-active[data-v-3ad594c3]{color:#3eaf7c;border-bottom:2px solid #46bd87;margin-bottom:-2px}.banner[data-v-3ad594c3]{position:absolute;right:0;top:0}.searchBox-wrapper[data-v-3ad594c3]{position:absolute;right:150px;z-index:2}.header__line[data-v-3ad594c3]{height:33px;opacity:.1;border:1px solid #3a495d}.header-container[data-v-3ad594c3]{position:absolute;width:100%}.header-nav[data-v-3ad594c3]{position:relative;z-index:5}.head-mask[data-v-3ad594c3]{width:100vw;height:100vh;position:fixed;left:0;top:0;z-index:9}@media (max-width:719px){.head-container[data-v-3ad594c3]{width:100vw;max-height:100vh;background:#fff;align-items:start;padding:16px 0;height:auto;top:60px;transform:translateY(-100%);transition:transform .3s}.row[data-v-3ad594c3]{flex-direction:column;align-items:start;width:100%}.nav[data-v-3ad594c3]{width:100%;height:60px;line-height:60px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;margin:0}}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}.theme-default-content code{color:#476582;padding:.25rem .5rem;margin:0;font-size:.85em;background-color:rgba(27,31,35,.05);border-radius:3px}.theme-default-content code .token.deleted{color:#ec5975}.theme-default-content code .token.inserted{color:#3eaf7c}.theme-default-content pre,.theme-default-content pre[class*=language-]{line-height:1.4;padding:1.25rem 1.5rem;margin:.85rem 0;background-color:#282c34;border-radius:6px;overflow:auto}.theme-default-content pre[class*=language-] code,.theme-default-content pre code{color:#fff;padding:0;background-color:transparent;border-radius:0}div[class*=language-]{position:relative;background-color:#282c34;border-radius:6px}div[class*=language-] .highlight-lines{-webkit-user-select:none;user-select:none;padding-top:1.3rem;position:absolute;top:0;left:0;width:100%;line-height:1.4}div[class*=language-] .highlight-lines .highlighted{background-color:rgba(0,0,0,.66)}div[class*=language-] pre,div[class*=language-] pre[class*=language-]{background:transparent;position:relative;z-index:1}div[class*=language-]:before{position:absolute;z-index:3;top:.8em;right:1em;font-size:.75rem;color:hsla(0,0%,100%,.4)}div[class*=language-]:not(.line-numbers-mode) .line-numbers-wrapper{display:none}div[class*=language-].line-numbers-mode .highlight-lines .highlighted{position:relative}div[class*=language-].line-numbers-mode .highlight-lines .highlighted:before{content:" ";position:absolute;z-index:3;left:0;top:0;display:block;width:3.5rem;height:100%;background-color:rgba(0,0,0,.66)}div[class*=language-].line-numbers-mode pre{padding-left:4.5rem;vertical-align:middle}div[class*=language-].line-numbers-mode .line-numbers-wrapper{position:absolute;top:0;width:3.5rem;text-align:center;color:hsla(0,0%,100%,.3);padding:1.25rem 0;line-height:1.4}div[class*=language-].line-numbers-mode .line-numbers-wrapper br{-webkit-user-select:none;user-select:none}div[class*=language-].line-numbers-mode .line-numbers-wrapper .line-number{position:relative;z-index:4;-webkit-user-select:none;user-select:none;font-size:.85em}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;z-index:2;top:0;left:0;width:3.5rem;height:100%;border-radius:6px 0 0 6px;border-right:1px solid rgba(0,0,0,.66);background-color:#282c34}div[class~=language-js]:before{content:"js"}div[class~=language-ts]:before{content:"ts"}div[class~=language-html]:before{content:"html"}div[class~=language-md]:before{content:"md"}div[class~=language-vue]:before{content:"vue"}div[class~=language-css]:before{content:"css"}div[class~=language-sass]:before{content:"sass"}div[class~=language-scss]:before{content:"scss"}div[class~=language-less]:before{content:"less"}div[class~=language-stylus]:before{content:"stylus"}div[class~=language-go]:before{content:"go"}div[class~=language-java]:before{content:"java"}div[class~=language-c]:before{content:"c"}div[class~=language-sh]:before{content:"sh"}div[class~=language-yaml]:before{content:"yaml"}div[class~=language-py]:before{content:"py"}div[class~=language-docker]:before{content:"docker"}div[class~=language-dockerfile]:before{content:"dockerfile"}div[class~=language-makefile]:before{content:"makefile"}div[class~=language-javascript]:before{content:"js"}div[class~=language-typescript]:before{content:"ts"}div[class~=language-markup]:before{content:"html"}div[class~=language-markdown]:before{content:"md"}div[class~=language-json]:before{content:"json"}div[class~=language-ruby]:before{content:"rb"}div[class~=language-python]:before{content:"py"}div[class~=language-bash]:before{content:"sh"}div[class~=language-php]:before{content:"php"}.custom-block .custom-block-title{font-weight:600;margin-bottom:-.4rem}.custom-block.danger,.custom-block.tip,.custom-block.warning{padding:.1rem 1.5rem;border-left-width:.5rem;border-left-style:solid;margin:1rem 0}.custom-block.tip{background-color:#f3f5f7;border-color:#42b983}.custom-block.warning{background-color:rgba(255,229,100,.3);border-color:#e7c000;color:#6b5900}.custom-block.warning .custom-block-title{color:#b29400}.custom-block.warning a{color:#2c3e50}.custom-block.danger{background-color:#ffe6e6;border-color:#c00;color:#4d0000}.custom-block.danger .custom-block-title{color:#900}.custom-block.danger a{color:#2c3e50}.custom-block.details{display:block;position:relative;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:#eee}.custom-block.details h4{margin-top:0}.custom-block.details figure:last-child,.custom-block.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-block.details summary{outline:none;cursor:pointer}.arrow{display:inline-block;width:0;height:0}.arrow.up{border-bottom:6px solid #ccc}.arrow.down,.arrow.up{border-left:4px solid transparent;border-right:4px solid transparent}.arrow.down{border-top:6px solid #ccc}.arrow.right{border-left:6px solid #ccc}.arrow.left,.arrow.right{border-top:4px solid transparent;border-bottom:4px solid transparent}.arrow.left{border-right:6px solid #ccc}.theme-default-content:not(.custom){max-width:740px;margin:0 auto;padding:2rem 2.5rem}@media (max-width:959px){.theme-default-content:not(.custom){padding:2rem}}@media (max-width:419px){.theme-default-content:not(.custom){padding:1.5rem}}.table-of-contents .badge{vertical-align:middle}body,html{padding:0;margin:0;background-color:#fff}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:16px;color:#2c3e50}.page{padding-left:20rem}.navbar{z-index:20;right:0;height:3.6rem;background-color:#fff;box-sizing:border-box;border-bottom:1px solid #eaecef}.navbar,.sidebar-mask{position:fixed;top:0;left:0}.sidebar-mask{z-index:9;width:100vw;height:100vh;display:none}.sidebar{font-size:16px;background-color:#fff;width:20rem;position:fixed;z-index:10;margin:0;top:3.6rem;left:0;bottom:0;box-sizing:border-box;border-right:1px solid #eaecef;overflow-y:auto}.theme-default-content:not(.custom)>:first-child{margin-top:3.6rem}.theme-default-content:not(.custom) a:hover{text-decoration:underline}.theme-default-content:not(.custom) p.demo{padding:1rem 1.5rem;border:1px solid #ddd;border-radius:4px}.theme-default-content:not(.custom) img{max-width:100%}.theme-default-content.custom{padding:0;margin:0}.theme-default-content.custom img{max-width:100%}a{font-weight:500;text-decoration:none}a,p a code{color:#3eaf7c}p a code{font-weight:400}kbd{background:#eee;border:.15rem solid #ddd;border-bottom:.25rem solid #ddd;border-radius:.15rem;padding:0 .15em}blockquote{font-size:1rem;color:#999;border-left:.2rem solid #dfe2e5;margin:1rem 0;padding:.25rem 0 .25rem 1rem}blockquote>p{margin:0}ol,ul{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25}.theme-default-content:not(.custom)>h1,.theme-default-content:not(.custom)>h2,.theme-default-content:not(.custom)>h3,.theme-default-content:not(.custom)>h4,.theme-default-content:not(.custom)>h5,.theme-default-content:not(.custom)>h6{margin-top:-3.1rem;padding-top:4.6rem;margin-bottom:0}.theme-default-content:not(.custom)>h1:first-child,.theme-default-content:not(.custom)>h2:first-child,.theme-default-content:not(.custom)>h3:first-child,.theme-default-content:not(.custom)>h4:first-child,.theme-default-content:not(.custom)>h5:first-child,.theme-default-content:not(.custom)>h6:first-child{margin-top:-1.5rem;margin-bottom:1rem}.theme-default-content:not(.custom)>h1:first-child+.custom-block,.theme-default-content:not(.custom)>h1:first-child+p,.theme-default-content:not(.custom)>h1:first-child+pre,.theme-default-content:not(.custom)>h2:first-child+.custom-block,.theme-default-content:not(.custom)>h2:first-child+p,.theme-default-content:not(.custom)>h2:first-child+pre,.theme-default-content:not(.custom)>h3:first-child+.custom-block,.theme-default-content:not(.custom)>h3:first-child+p,.theme-default-content:not(.custom)>h3:first-child+pre,.theme-default-content:not(.custom)>h4:first-child+.custom-block,.theme-default-content:not(.custom)>h4:first-child+p,.theme-default-content:not(.custom)>h4:first-child+pre,.theme-default-content:not(.custom)>h5:first-child+.custom-block,.theme-default-content:not(.custom)>h5:first-child+p,.theme-default-content:not(.custom)>h5:first-child+pre,.theme-default-content:not(.custom)>h6:first-child+.custom-block,.theme-default-content:not(.custom)>h6:first-child+p,.theme-default-content:not(.custom)>h6:first-child+pre{margin-top:2rem}h1:focus .header-anchor,h1:hover .header-anchor,h2:focus .header-anchor,h2:hover .header-anchor,h3:focus .header-anchor,h3:hover .header-anchor,h4:focus .header-anchor,h4:hover .header-anchor,h5:focus .header-anchor,h5:hover .header-anchor,h6:focus .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2.2rem}h2{font-size:1.65rem;padding-bottom:.3rem;border-bottom:1px solid #eaecef}h3{font-size:1.35rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;-webkit-user-select:none;user-select:none;opacity:0}a.header-anchor:focus,a.header-anchor:hover{text-decoration:none}.line-number,code,kbd{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}ol,p,ul{line-height:1.7}hr{border:0;border-top:1px solid #eaecef}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto}tr{border-top:1px solid #dfe2e5}tr:nth-child(2n){background-color:#f6f8fa}td,th{border:1px solid #dfe2e5;padding:.6em 1em}.theme-container.sidebar-open .sidebar-mask{display:block}.theme-container.no-navbar .theme-default-content:not(.custom)>h1,.theme-container.no-navbar h2,.theme-container.no-navbar h3,.theme-container.no-navbar h4,.theme-container.no-navbar h5,.theme-container.no-navbar h6{margin-top:1.5rem;padding-top:0}.theme-container.no-navbar .sidebar{top:0}@media (min-width:720px){.theme-container.no-sidebar .sidebar{display:none}.theme-container.no-sidebar .page{padding-left:0}}@media (max-width:959px){.sidebar{font-size:15px;width:16.4rem}.page{padding-left:16.4rem}}@media (max-width:719px){.sidebar{top:0;padding-top:3.6rem;transform:translateX(-100%);transition:transform .2s ease}.page{padding-left:0}.theme-container.sidebar-open .sidebar{transform:translateX(0)}.theme-container.no-navbar .sidebar{padding-top:0}}@media (max-width:419px){h1{font-size:1.9rem}.theme-default-content div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}.navbar .site-name{display:none!important}a{word-break:break-all}:root{--vt-c-white:#fff;--vt-c-white-soft:#f9f9f9;--vt-c-white-mute:#f1f1f1;--vt-c-black:#1a1a1a;--vt-c-black-pure:#000;--vt-c-black-soft:#242424;--vt-c-black-mute:#2f2f2f;--vt-c-indigo:#213547;--vt-c-indigo-soft:#476582;--vt-c-indigo-light:#aac8e4;--vt-c-gray:#8e8e8e;--vt-c-gray-light-1:#aeaeae;--vt-c-gray-light-2:#c7c7c7;--vt-c-gray-light-3:#d1d1d1;--vt-c-gray-light-4:#e5e5e5;--vt-c-gray-light-5:#f2f2f2;--vt-c-gray-dark-1:#636363;--vt-c-gray-dark-2:#484848;--vt-c-gray-dark-3:#3a3a3a;--vt-c-gray-dark-4:#282828;--vt-c-gray-dark-5:#202020;--vt-c-divider-light-1:rgba(60,60,60,0.29);--vt-c-divider-light-2:rgba(60,60,60,0.12);--vt-c-divider-dark-1:rgba(84,84,84,0.65);--vt-c-divider-dark-2:rgba(84,84,84,0.48);--vt-c-text-light-1:var(--vt-c-indigo);--vt-c-text-light-2:rgba(60,60,60,0.7);--vt-c-text-light-3:rgba(60,60,60,0.33);--vt-c-text-light-4:rgba(60,60,60,0.18);--vt-c-text-light-code:var(--vt-c-indigo-soft);--vt-c-text-dark-1:hsla(0,0%,100%,0.87);--vt-c-text-dark-2:hsla(0,0%,92.2%,0.6);--vt-c-text-dark-3:hsla(0,0%,92.2%,0.38);--vt-c-text-dark-4:hsla(0,0%,92.2%,0.18);--vt-c-text-dark-code:var(--vt-c-indigo-light);--vt-c-green:#42b883;--vt-c-green-light:#42d392;--vt-c-green-lighter:#35eb9a;--vt-c-green-dark:#33a06f;--vt-c-green-darker:#155f3e;--vt-c-blue:#3b8eed;--vt-c-blue-light:#549ced;--vt-c-blue-lighter:#50a2ff;--vt-c-blue-dark:#3468a3;--vt-c-blue-darker:#255489;--vt-c-yellow:#ffc517;--vt-c-yellow-light:#ffe417;--vt-c-yellow-lighter:#ffff17;--vt-c-yellow-dark:#e0ad15;--vt-c-yellow-darker:#bc9112;--vt-c-red:#ed3c50;--vt-c-red-light:#f43771;--vt-c-red-lighter:#fd1d7c;--vt-c-red-dark:#cd2d3f;--vt-c-red-darker:#ab2131;--vt-c-purple:#de41e0;--vt-c-purple-light:#e936eb;--vt-c-purple-lighter:#f616f8;--vt-c-purple-dark:#823c83;--vt-c-purple-darker:#602960;--c-brand:#3eaf7c;--c-bg:#fff;--c-bg-light:#f3f4f5;--c-bg-lighter:#eee;--c-text:#2c3e50;--c-text-light:#3a5169;--c-text-quote:#999;--c-border-dark:#dfe2e5;--vt-c-bg:var(--vt-c-white);--vt-c-bg-soft:var(--vt-c-white-soft);--vt-c-bg-mute:var(--vt-c-white-mute);--vt-c-divider:var(--vt-c-divider-light-1);--vt-c-divider-light:var(--vt-c-divider-light-2);--vt-c-divider-inverse:var(--vt-c-divider-dark-1);--vt-c-divider-inverse-light:var(--vt-c-divider-dark-2);--vt-c-text-1:var(--vt-c-text-light-1);--vt-c-text-2:var(--vt-c-text-light-2);--vt-c-text-3:var(--vt-c-text-light-3);--vt-c-text-4:var(--vt-c-text-light-4);--vt-c-text-code:var(--vt-c-text-light-code);--vt-c-text-inverse-1:var(--vt-c-text-dark-1);--vt-c-text-inverse-2:var(--vt-c-text-dark-2);--vt-c-text-inverse-3:var(--vt-c-text-dark-3);--vt-c-text-inverse-4:var(--vt-c-text-dark-4);--vt-c-brand:var(--vt-c-green);--vt-c-brand-light:var(--vt-c-green-light);--vt-c-brand-dark:var(--vt-c-green-dark);--vt-c-brand-highlight:var(--vt-c-brand-dark)}:root .DocSearch{--docsearch-primary-color:var(--c-brand);--docsearch-text-color:var(--c-text);--docsearch-highlight-color:var(--c-brand);--docsearch-muted-color:var(--c-text-quote);--docsearch-container-background:rgba(9,10,17,0.8);--docsearch-modal-background:var(--c-bg-light);--docsearch-searchbox-background:var(--c-bg-lighter);--docsearch-searchbox-focus-background:var(--c-bg);--docsearch-searchbox-shadow:inset 0 0 0 2px var(--c-brand);--docsearch-hit-color:var(--c-text-light);--docsearch-hit-active-color:var(--c-bg);--docsearch-hit-background:var(--c-bg);--docsearch-hit-shadow:0 1px 3px 0 var(--c-border-dark);--docsearch-footer-background:var(--c-bg)}@media (max-width:719px){:root{--c-bg-lighter:none}}#nprogress{pointer-events:none}#nprogress .bar{background:#3eaf7c;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px #3eaf7c,0 0 5px #3eaf7c;opacity:1;transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border-color:#3eaf7c transparent transparent #3eaf7c;border-style:solid;border-width:2px;border-radius:50%;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.go-to-top[data-v-5cf0f4c0]{cursor:pointer;position:fixed;bottom:2rem;right:2.5rem;width:2rem;color:#3eaf7c;z-index:1}.go-to-top[data-v-5cf0f4c0]:hover{color:#72cda4}@media (max-width:959px){.go-to-top[data-v-5cf0f4c0]{display:none}}.fade-enter-active[data-v-5cf0f4c0],.fade-leave-active[data-v-5cf0f4c0]{transition:opacity .3s}.fade-enter[data-v-5cf0f4c0],.fade-leave-to[data-v-5cf0f4c0]{opacity:0}.icon.outbound{color:#aaa;display:inline-block;vertical-align:middle;position:relative;top:-1px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}[data-v-ae3bb0ce]::-webkit-scrollbar{display:none}.swiper-container[data-v-ae3bb0ce]{position:relative;width:100%;height:152px;overflow:hidden}.swiper[data-v-ae3bb0ce]{height:100%;white-space:nowrap;transform:translateX(0);display:inline-block}.swiper-list[data-v-ae3bb0ce]{width:100%;display:inline-block}.swiper-dot[data-v-ae3bb0ce]{position:absolute;left:50%;bottom:12px;display:flex;padding:0;z-index:3;transform:translate3d(-50%,0,0)}.swiper-icon[data-v-ae3bb0ce]{list-style:none;width:6px;height:6px;border-radius:50%;background:#efefef;margin:0 3px}.active[data-v-ae3bb0ce]{background:#00bd81}.active[data-v-a2c0041c]{border:2px solid #00bd81!important}.m-banner[data-v-a2c0041c]{display:flex;flex-direction:column;align-items:center;height:468px;background-image:linear-gradient(0deg,#50be97 4%,#31bc7f 83%)}.m-banner .m-title[data-v-a2c0041c]{font-family:PingFangSC-Medium;font-size:26px;color:#fff;letter-spacing:0;text-align:justify;font-weight:500;margin-top:120px}.m-banner .m-subtitle[data-v-a2c0041c]{font-family:PingFangSC-Regular;font-size:13px;color:#fff;letter-spacing:0;text-align:center;line-height:22px;font-weight:400;margin:10px 30px 0}.m-banner .m-banner-btn-wrapper[data-v-a2c0041c]{display:flex;flex-direction:row;justify-content:center;align-content:center;padding-top:30px}.m-banner .m-banner-btn-wrapper .m-banner-btn[data-v-a2c0041c]{width:104px;height:36px;border-radius:4px;text-align:center;line-height:36px}.m-banner .m-banner-btn-wrapper .m-banner-btn-enter[data-v-a2c0041c]{background:#fff;border:0 solid #efefef;margin-right:15px}.m-banner .m-banner-btn-wrapper .m-banner-btn-jump[data-v-a2c0041c]{border:1px solid #fff;margin-left:15px;color:#fff;box-sizing:border-box}.m-banner .m-banner-btn-wrapper .white-link[data-v-a2c0041c]{color:#fff}.m-banner .m-banner-bg[data-v-a2c0041c]{width:375px;height:202px;background-image:url(https://dpubstatic.udache.com/static/dpubimg/A0HNx-AsoO/y_pic_banner.png);background-size:cover;margin-top:35px}.m-advantages[data-v-a2c0041c]{display:flex;justify-content:space-between;align-items:center;padding:20px 16px;background:#fff}.m-advantages .m-advan-section[data-v-a2c0041c]{width:100px;height:78px;background:#fff;box-shadow:0 11px 32px 0 rgba(49,188,127,.06),0 3px 10px 0 rgba(49,188,127,.04);border-radius:4px;text-align:center;list-style:none}.m-advantages .m-advan-section .m-advan-section-img[data-v-a2c0041c]{margin-top:10px}.m-advantages .m-advan-section .m-advan-section-title[data-v-a2c0041c]{margin-top:4px;font-family:PingFangSC-Regular;font-size:15px;color:#3a495d;letter-spacing:0;text-align:center;line-height:22px;font-weight:400}.m-example-swiper[data-v-a2c0041c]{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;border-radius:4px}.m-example-name[data-v-a2c0041c]{width:166px;height:60px;margin:6px;border:2px solid #ededed;box-shadow:0 11px 32px 0 rgba(49,188,127,.06),0 4px 10px 0 rgba(49,188,127,.04);border-radius:4px;display:flex;align-items:center;justify-content:center;background:#fff}.m-example-wrapper[data-v-a2c0041c],.m-feature-wrapper[data-v-a2c0041c],.m-util-wrapper[data-v-a2c0041c],.mdemo-wrapper[data-v-a2c0041c]{display:flex;flex-direction:column;align-items:center;background:#f7f7f7}.m-example-wrapper .m-example-title[data-v-a2c0041c],.m-example-wrapper .m-feature-title[data-v-a2c0041c],.m-example-wrapper .m-util-title[data-v-a2c0041c],.m-example-wrapper .mdemo-title[data-v-a2c0041c],.m-feature-wrapper .m-example-title[data-v-a2c0041c],.m-feature-wrapper .m-feature-title[data-v-a2c0041c],.m-feature-wrapper .m-util-title[data-v-a2c0041c],.m-feature-wrapper .mdemo-title[data-v-a2c0041c],.m-util-wrapper .m-example-title[data-v-a2c0041c],.m-util-wrapper .m-feature-title[data-v-a2c0041c],.m-util-wrapper .m-util-title[data-v-a2c0041c],.m-util-wrapper .mdemo-title[data-v-a2c0041c],.mdemo-wrapper .m-example-title[data-v-a2c0041c],.mdemo-wrapper .m-feature-title[data-v-a2c0041c],.mdemo-wrapper .m-util-title[data-v-a2c0041c],.mdemo-wrapper .mdemo-title[data-v-a2c0041c]{font-family:PingFangSC-Medium;font-size:24px;color:#3a495d;letter-spacing:0;text-align:justify;font-weight:500;margin-top:50px;margin-bottom:20px}.m-example-wrapper .m-feature-subtitle[data-v-a2c0041c],.m-example-wrapper .mdemo-subtitle[data-v-a2c0041c],.m-feature-wrapper .m-feature-subtitle[data-v-a2c0041c],.m-feature-wrapper .mdemo-subtitle[data-v-a2c0041c],.m-util-wrapper .m-feature-subtitle[data-v-a2c0041c],.m-util-wrapper .mdemo-subtitle[data-v-a2c0041c],.mdemo-wrapper .m-feature-subtitle[data-v-a2c0041c],.mdemo-wrapper .mdemo-subtitle[data-v-a2c0041c]{font-family:PingFangSC-Regular;font-size:13px;color:#394e5e;letter-spacing:0;text-align:center;line-height:22px;font-weight:400;padding:0 30px}.m-example-wrapper .m-feature-btn[data-v-a2c0041c],.m-example-wrapper .mdemo-btn[data-v-a2c0041c],.m-feature-wrapper .m-feature-btn[data-v-a2c0041c],.m-feature-wrapper .mdemo-btn[data-v-a2c0041c],.m-util-wrapper .m-feature-btn[data-v-a2c0041c],.m-util-wrapper .mdemo-btn[data-v-a2c0041c],.mdemo-wrapper .m-feature-btn[data-v-a2c0041c],.mdemo-wrapper .mdemo-btn[data-v-a2c0041c]{width:104px;height:36px;background:#00bd81;border:0 solid #efefef;border-radius:4px;margin-top:30px;font-family:PingFangSC-Medium;font-size:15px;color:#fff;letter-spacing:0;text-align:center;line-height:36px;font-weight:500}.m-example-wrapper .mdemo-icon-wrapper[data-v-a2c0041c],.m-feature-wrapper .mdemo-icon-wrapper[data-v-a2c0041c],.m-util-wrapper .mdemo-icon-wrapper[data-v-a2c0041c],.mdemo-wrapper .mdemo-icon-wrapper[data-v-a2c0041c]{margin:10px 0 40px;display:flex;flex-wrap:wrap;justify-content:space-around;align-items:center;align-content:space-around}.m-example-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c],.m-feature-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c],.m-util-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c],.mdemo-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c]{width:100px;display:flex;flex-direction:column;justify-content:center;align-items:center;margin-top:30px}.m-example-wrapper .m-feature-pic[data-v-a2c0041c],.m-feature-wrapper .m-feature-pic[data-v-a2c0041c],.m-util-wrapper .m-feature-pic[data-v-a2c0041c],.mdemo-wrapper .m-feature-pic[data-v-a2c0041c]{margin-top:40px}.m-example-wrapper .six-section__row[data-v-a2c0041c],.m-feature-wrapper .six-section__row[data-v-a2c0041c],.m-util-wrapper .six-section__row[data-v-a2c0041c],.mdemo-wrapper .six-section__row[data-v-a2c0041c]{margin:0 0 10px;flex-wrap:wrap;justify-content:center;padding:0 16px;width:100%;box-sizing:border-box}.m-example-wrapper .six-section__row .six-section__item[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__item[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__item[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__item[data-v-a2c0041c]{align-items:center;background:#fff;border:0 solid #efefef;border-radius:4px;height:72px;display:flex;padding:11px 0 11px 24px;box-sizing:border-box;box-shadow:0 11px 32px 0 rgba(49,188,127,.06),0 4px 10px 0 rgba(49,188,127,.04)}.m-example-wrapper .six-section__row .six-section__icon[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__icon[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__icon[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__icon[data-v-a2c0041c]{margin-right:9px}.m-example-wrapper .six-section__row .six-section__list[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__list[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__list[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__list[data-v-a2c0041c]{display:flex;flex-direction:column;justify-content:space-between;color:#3a495d;margin-left:10px;font-family:PingFangSC-Medium}.m-example-wrapper .six-section__row .six-section__bold[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__bold[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__bold[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__bold[data-v-a2c0041c]{font-size:15px;margin-right:9px;margin-bottom:4px}.m-example-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c]{font-size:13px}.m-example-wrapper .m-example-phone[data-v-a2c0041c],.m-feature-wrapper .m-example-phone[data-v-a2c0041c],.m-util-wrapper .m-example-phone[data-v-a2c0041c],.mdemo-wrapper .m-example-phone[data-v-a2c0041c]{width:100%;display:flex;justify-content:center;position:relative;height:450px;align-items:center}.m-example-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c],.m-feature-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c],.m-util-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c],.mdemo-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c]{position:absolute;top:0;left:50%;transform:translate3d(-50%,0,0);width:100%;width:190px;height:390px;background:url(https://dpubstatic.udache.com/static/dpubimg/Vx5n_3YCtP/anli_pic_phone.png) no-repeat 50%;background-size:contain;padding:12px}.m-feature-wrapper[data-v-a2c0041c]{background:#fff;padding-bottom:50px}.m-example-wrapper[data-v-a2c0041c]{background:#fff}.m-util-wrapper[data-v-a2c0041c]{padding-bottom:20px}[data-v-5724bcdd]::-webkit-scrollbar{display:none}.swiper-container[data-v-5724bcdd]{max-width:1190px;display:flex;margin:0 auto}.swiper-container .swiper-button[data-v-5724bcdd]{width:140px;margin-top:80px;transform:filter .3s}.swiper-container .swiper-img[data-v-5724bcdd]{cursor:pointer}.swiper-container .swiper-disable[data-v-5724bcdd]{cursor:not-allowed;filter:grayscale(100%)}.swiper[data-v-5724bcdd]{flex:1;position:relative;overflow-y:hidden;overflow-x:scroll;white-space:nowrap;height:300px}.swiper .swiper-list[data-v-5724bcdd]{display:inline-block;padding:24px;border-radius:10px;margin-top:20px;position:relative;transition:none 0s ease 0s;transform:scale(1)}.swiper .swiper-list.active[data-v-5724bcdd]{transition:transform .3s;transform:scale(1.5)}.swiper .swiper-item[data-v-5724bcdd]{width:136px;height:136px;background:#fff;border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer}.swiper-list[data-v-5724bcdd]:first-child{padding-left:50px}.swiper-list[data-v-5724bcdd]:last-child{padding-right:50px}.swiper-img[data-v-60490726]{width:188px;height:408px;height:100%;white-space:nowrap;overflow:hidden;border-radius:20px}.swiper-img .swiper-img__wrap[data-v-60490726]{display:inline-block}.swiper-img .swiper-img__list[data-v-60490726]{transform:translateZ(0);transition:transform .3s}.fade-enter-active[data-v-8bac5638],.fade-leave-active[data-v-8bac5638]{transition:all .3s}.fade-enter[data-v-8bac5638],.fade-leave-to[data-v-8bac5638]{opacity:0}.popover[data-v-8bac5638]{position:relative;display:inline-block}.popover .popover__top[data-v-8bac5638]{position:absolute;width:300px;height:300px;background:#fff;left:-150px;top:-350px;border-radius:4px}.popover .popover__content[data-v-8bac5638]{position:relative;width:100%;height:100%}.popover .popover__arrow[data-v-8bac5638]{position:absolute;left:50%;margin-left:-5px;bottom:-5px;width:6px;height:6px;background:#fff;transform:rotate(45deg);border-bottom:1px solid #ededed;border-right:1px solid #ededed;z-index:6}.popover .popover__inner[data-v-8bac5638]{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:14px;position:relative;z-index:4;background:#fff;border-radius:4px;border:1px solid #ededed}.popover .popover__content[data-v-8bac5638]{display:inline-block}.fade-enter-active[data-v-76bf3aa2],.fade-leave-active[data-v-76bf3aa2]{transition:all .3s}.fade-enter[data-v-76bf3aa2],.fade-leave-to[data-v-76bf3aa2]{opacity:0}.code-list[data-v-76bf3aa2]{margin-top:40px;display:flex}.code-list .code-list__item[data-v-76bf3aa2]{margin-right:62px;width:150px;height:170px;cursor:pointer}.code-list .code-list__inner[data-v-76bf3aa2]{position:relative}.code-list .code-list__big[data-v-76bf3aa2]{position:absolute;left:0;top:0;transform:scale(1.5)}ul li[data-v-2d48710e]{list-style:none}section[data-v-2d48710e]{position:relative;z-index:2}.row[data-v-2d48710e]{display:flex;align-items:center}.header[data-v-2d48710e]{padding:40px 0 0 30px;position:relative}.title[data-v-2d48710e]{font-size:30px;margin-bottom:20px;font-weight:500}.desc[data-v-2d48710e]{font-size:14px;line-height:22px;margin-bottom:40px}.btn[data-v-2d48710e]{width:116px;height:40px;background-image:linear-gradient(-58deg,#50be97 30%,#31bc7f 79%);box-shadow:-3px 12px 35px 0 rgba(49,188,127,.2);border-radius:4px;border:none;font-size:14px}.one-section__content[data-v-2d48710e]{display:flex;justify-content:flex-end}.one-section[data-v-2d48710e]{padding-top:181px;position:relative;display:flex}.one-section__img1[data-v-2d48710e]{position:absolute;top:0;left:45%;width:100%;height:100%;background:url(https://gift-static.hongyibo.com.cn/static/kfpub/3547/banner_bg_v3.png) no-repeat 0 0;background-size:auto 680px}.one-section__img2[data-v-2d48710e]{position:absolute;top:20;left:53%;width:100%;height:100%;background:url(https://gift-static.hongyibo.com.cn/static/kfpub/3547/banner_pic_v3.png) no-repeat 0 0;background-size:auto 550px}.one-section__inner[data-v-2d48710e]{flex:1}.one-section__title[data-v-2d48710e]{margin-bottom:20px;font-size:40px;font-weight:500;border-bottom:none}.one-section__desc[data-v-2d48710e]{width:450px;line-height:30px;margin-bottom:70px}.one-section__btn[data-v-2d48710e]{width:162px;height:52px;border-radius:4px;border:none;font-size:20px}.one-section__enter[data-v-2d48710e]{background-image:linear-gradient(-58deg,#50be97 30%,#31bc7f 79%);box-shadow:-3px 12px 35px 0 rgba(49,188,127,.2)}.one-section__github[data-v-2d48710e]{margin-left:30px;background:#fff;border:0 solid #efefef;box-shadow:-3px 12px 35px 0 rgba(49,188,127,.2)}.white-link[data-v-2d48710e]{color:#fff}.blue-link[data-v-2d48710e],.white-link[data-v-2d48710e]{font-weight:600;display:flex;align-items:center;justify-content:center}.blue-link[data-v-2d48710e]{color:#31bc7f;width:100%;height:100%}.target-link[data-v-2d48710e]{color:#fff;font-weight:600;text-decoration:underline}.two-section[data-v-2d48710e]{padding-top:240px;display:flex;justify-content:center}.two-section__item[data-v-2d48710e]{width:280px;padding:40px 18px;box-sizing:border-box;background:#fff;border:0 solid #efefef;box-shadow:0 51px 145px 0 rgba(49,188,127,.1),0 11px 32px 0 rgba(49,188,127,.06),0 3px 10px 0 rgba(49,188,127,.04);border-radius:4px;margin:0 10px;text-align:center;list-style:none;position:relative;overflow:hidden}.two-section__item[data-v-2d48710e]:nth-child(2){position:relative;top:54px}.two-section__line[data-v-2d48710e]{height:8px;width:100%;position:absolute;left:0;bottom:0;opacity:.6;background-image:linear-gradient(-58deg,#50be97 30%,#31bc7f 79%)}.two-section__title[data-v-2d48710e]{font-size:20px;margin:33px 0 21px}.two-section__desc[data-v-2d48710e]{font-size:14px;line-height:22px}.three-section[data-v-2d48710e]{margin-top:183px;display:flex;background-repeat:no-repeat;background-size:auto 100%;background-position:50%;height:713px;align-items:center;position:relative}.three-section__inner[data-v-2d48710e]{width:1190px;margin:0 auto;display:flex;align-items:center;box-sizing:border-box;padding:0 40px}.three-section__mvc[data-v-2d48710e]{margin-left:60px}.three-section__todo[data-v-2d48710e]{position:relative}.three-section__phone[data-v-2d48710e]{position:absolute;left:0;top:0}.three-section__iframe[data-v-2d48710e]{position:relative;left:20px;top:20px;border-radius:44px;background:#fff;overflow:hidden;box-shadow:0 80px 252px 0 rgba(49,188,127,.12),0 36px 76px 0 rgba(49,188,127,.08),0 15px 31px 0 rgba(49,188,127,.06),0 5px 11px 0 rgba(49,188,127,.04)}.iframe_wrapper[data-v-2d48710e],.three-section__iframe[data-v-2d48710e]{display:flex;align-items:center;justify-content:center;width:375px;height:672px}.white-text[data-v-2d48710e]{color:#fff}.three-section__btn[data-v-2d48710e]{background:#fff}.section[data-v-2d48710e]{display:flex;align-items:center;height:520px;box-sizing:border-box;margin-top:183px;position:relative;justify-content:center}.grow[data-v-2d48710e]{flex:1}.four-section__bg[data-v-2d48710e]{background-repeat:no-repeat;background-size:auto 600px;height:600px;width:100%;text-align:center;position:absolute;right:50%;background-position:100% 0}.four-section__inner[data-v-2d48710e]{display:flex;width:1190px;align-items:center;padding:0 40px;box-sizing:border-box}.five-section__bg[data-v-2d48710e]{background-repeat:no-repeat;background-size:auto 600px;height:600px;width:100%;text-align:center;position:absolute;left:50%;background-position:0 0}.five-section__inner[data-v-2d48710e]{display:flex;width:1190px;align-items:center;padding:0 40px;box-sizing:border-box}.six-section[data-v-2d48710e]{margin-top:140px;display:flex;align-items:center;justify-content:center;background-repeat:no-repeat;background-size:auto 100%;background-position:50%;height:694px}.six-section__inner[data-v-2d48710e]{margin-bottom:50px}.six-section__item[data-v-2d48710e]{background:#fff;border:0 solid #a4a4a4;border-radius:4px;width:290px;height:80px;display:flex;padding:17px 0 17px 17px;box-sizing:border-box}.six-section__step[data-v-2d48710e]{margin-right:20px}.six-section__row[data-v-2d48710e]{margin-bottom:20px;flex-wrap:wrap;justify-content:center}.six-section__list[data-v-2d48710e]{display:flex;flex-direction:column;justify-content:space-between;font-size:14px;color:#3a495d}.six-section__bold[data-v-2d48710e]{font-size:16px;font-weight:500;white-space:nowrap}.six-section__title[data-v-2d48710e]{margin-bottom:50px;text-align:center;color:#fff}.six-section__icon[data-v-2d48710e]{margin-right:9px}.seven-section[data-v-2d48710e]{margin-top:113px;text-align:center;background:#f5f5f5}.seven-section__center[data-v-2d48710e]{width:402px;height:100%;background-repeat:no-repeat;background-size:contain;background-position:50%;display:flex;justify-content:center;position:relative}.seven-section__inner[data-v-2d48710e]{padding:14px;box-shadow:0 43px 86px 0 rgba(49,188,127,.07),0 7px 21px 0 rgba(49,188,127,.04),0 2px 6px 0 rgba(49,188,127,.03);border-radius:30px}.seven-section__wrap[data-v-2d48710e]{height:433px;margin-top:40px;margin-bottom:40px}.seven-section_phone[data-v-2d48710e]{position:absolute;top:0;left:50%;transform:translate3d(-50%,0,0)}.seven-section__title[data-v-2d48710e]{text-align:right;font-size:20px;font-weight:600}.seven-section__desc[data-v-2d48710e]{margin-top:41px;text-align:left;font-size:14px;line-height:23px}.dot[data-v-2d48710e]{width:100%}.dot-inner[data-v-2d48710e]{background:#31bc7f;border-radius:4px;width:34px;height:10px;display:inline-block}ul li[data-v-f9ca297c]{list-style:none}.footer-container[data-v-f9ca297c]{display:flex;flex-direction:column;justify-content:flex-end;height:466px;background:url(https://dpubstatic.udache.com/static/dpubimg/cSRXkZjG5W/footer_bg.png) no-repeat 50%;background-size:auto 100%;margin-top:60px}.footer[data-v-f9ca297c]{display:flex;text-align:center;max-width:1280px;margin:0 auto;width:100%}.footer__list[data-v-f9ca297c]{margin-bottom:80px;display:flex;flex:1;text-align:left}.footer__wrap[data-v-f9ca297c]{margin-bottom:14px;color:#fff;text-align:left;height:28px;position:relative}.footer__text[data-v-f9ca297c]{font-size:14px;color:#fff;display:inline-block;text-align:left;height:28px}.footer__title[data-v-f9ca297c]{font-size:20px;font-weight:500}.copyright[data-v-f9ca297c]{font-size:12px;color:#fff;background:#3a495d;text-align:center;line-height:30px;padding:10px 0}.grow[data-v-f9ca297c]{flex:1}.footer__img[data-v-f9ca297c]{position:absolute;left:0;top:0;display:none}@media (max-width:750px){.footer__list[data-v-f9ca297c]{margin-bottom:16px}.footer__text[data-v-f9ca297c]{font-size:12px}.footer__title[data-v-f9ca297c]{font-size:12px;color:#979797}.footer__wrap[data-v-f9ca297c]{margin-bottom:0}.footer-container[data-v-f9ca297c]{background:#606d7c;height:auto;margin-top:0}.footer-inner[data-v-f9ca297c]{padding-left:6px}}.home{padding:3.6rem 2rem 0;max-width:960px;margin:0 auto;display:block}.home .hero{text-align:center}.home .hero img{max-width:100%;max-height:280px;display:block;margin:3rem auto 1.5rem}.home .hero h1{font-size:3rem}.home .hero .action,.home .hero .description,.home .hero h1{margin:1.8rem auto}.home .hero .description{max-width:35rem;font-size:1.6rem;line-height:1.3;color:#6a8bad}.home .hero .action-button{display:inline-block;font-size:1.2rem;color:#fff;background-color:#3eaf7c;padding:.8rem 1.6rem;border-radius:4px;transition:background-color .1s ease;box-sizing:border-box;border-bottom:1px solid #389d70}.home .hero .action-button:hover{background-color:#4abf8a}.home .features{border-top:1px solid #eaecef;padding:1.2rem 0;margin-top:2.5rem;display:flex;flex-wrap:wrap;align-items:flex-start;align-content:stretch;justify-content:space-between}.home .feature{flex-grow:1;flex-basis:30%;max-width:30%}.home .feature h2{font-size:1.4rem;font-weight:500;border-bottom:none;padding-bottom:0;color:#3a5169}.home .feature p{color:#4e6e8e}.home .footer{padding:2.5rem;border-top:1px solid #eaecef;text-align:center;color:#4e6e8e}@media (max-width:719px){.home .features{flex-direction:column}.home .feature{max-width:100%;padding:0 2.5rem}}@media (max-width:419px){.home{padding-left:1.5rem;padding-right:1.5rem}.home .hero img{max-height:210px;margin:2rem auto 1.2rem}.home .hero h1{font-size:2rem}.home .hero .action,.home .hero .description,.home .hero h1{margin:1.2rem auto}.home .hero .description{font-size:1.2rem}.home .hero .action-button{font-size:1rem;padding:.6rem 1.2rem}.home .feature h2{font-size:1.25rem}}.page-edit{max-width:740px;margin:0 auto;padding:2rem 2.5rem}@media (max-width:959px){.page-edit{padding:2rem}}@media (max-width:419px){.page-edit{padding:1.5rem}}.page-edit{padding-top:1rem;padding-bottom:1rem;overflow:auto}.page-edit .edit-link{display:inline-block}.page-edit .edit-link a{color:#4e6e8e;margin-right:.25rem}.page-edit .last-updated{float:right;font-size:.9em}.page-edit .last-updated .prefix{font-weight:500;color:#4e6e8e}.page-edit .last-updated .time{font-weight:400;color:#767676}@media (max-width:719px){.page-edit .edit-link{margin-bottom:.5rem}.page-edit .last-updated{font-size:.8em;float:none;text-align:left}}.page-nav{max-width:740px;margin:0 auto;padding:2rem 2.5rem}@media (max-width:959px){.page-nav{padding:2rem}}@media (max-width:419px){.page-nav{padding:1.5rem}}.page-nav{padding-top:1rem;padding-bottom:0}.page-nav .inner{min-height:2rem;margin-top:0;border-top:1px solid #eaecef;padding-top:1rem;overflow:auto}.page-nav .next{float:right}.page{padding-bottom:2rem;display:block}.dropdown-enter,.dropdown-leave-to{height:0!important}.sidebar-button{cursor:pointer;display:none;width:1.25rem;height:1.25rem;position:absolute;padding:.6rem;top:.6rem;left:1rem}.sidebar-button .icon{display:block;width:1.25rem;height:1.25rem}@media (max-width:719px){.sidebar-button{display:block}}.badge[data-v-59f00772]{display:inline-block;font-size:14px;height:18px;line-height:18px;border-radius:3px;padding:0 6px;color:#fff}.badge.green[data-v-59f00772],.badge.tip[data-v-59f00772],.badge[data-v-59f00772]{background-color:#42b983}.badge.error[data-v-59f00772]{background-color:#da5961}.badge.warn[data-v-59f00772],.badge.warning[data-v-59f00772],.badge.yellow[data-v-59f00772]{background-color:#e7c000}.badge+.badge[data-v-59f00772]{margin-left:5px}.theme-code-block[data-v-488b05bd]{display:none}.theme-code-block__active[data-v-488b05bd]{display:block}.theme-code-block>pre[data-v-488b05bd]{background-color:orange}.theme-code-group__nav[data-v-131b8180]{margin-bottom:-35px;background-color:#282c34;padding-bottom:22px;border-top-left-radius:6px;border-top-right-radius:6px;padding-left:10px;padding-top:10px}.theme-code-group__ul[data-v-131b8180]{margin:auto 0;padding-left:0;display:inline-flex;list-style:none}.theme-code-group__nav-tab[data-v-131b8180]{border:0;padding:5px;cursor:pointer;background-color:transparent;font-size:.85em;line-height:1.4;color:hsla(0,0%,100%,.9);font-weight:600}.theme-code-group__nav-tab-active[data-v-131b8180]{border-bottom:1px solid #42b983}.pre-blank[data-v-131b8180]{color:#42b983}.swiper-item[data-v-f19c6420]{width:100%}img[data-v-f19c6420]{display:block;width:100%;height:100%}#api-index[data-v-a2f64316]{max-width:1024px;margin:0 auto;padding:64px 32px}a[data-v-a2f64316]{text-decoration:none}h1[data-v-a2f64316],h2[data-v-a2f64316],h3[data-v-a2f64316]{font-weight:600;line-height:1}h1[data-v-a2f64316],h2[data-v-a2f64316]{letter-spacing:-.02em}h1[data-v-a2f64316]{font-size:38px}ul[data-v-a2f64316]{list-style:none;margin:0;padding:0}h2[data-v-a2f64316]{font-size:24px;color:var(--vt-c-text-1);margin:36px 0;transition:color .5s;padding-top:36px;border-top:1px solid var(--vt-c-divider-light);border-bottom:none}h3[data-v-a2f64316]{letter-spacing:-.01em;color:var(--vt-c-green);font-size:18px;margin-bottom:1em;transition:color .5s}.api-section[data-v-a2f64316]{margin-bottom:64px}.api-groups a[data-v-a2f64316]{font-size:15px;font-weight:500;line-height:2;color:var(--vt-c-text-code);transition:color .5s}.dark api-groups a[data-v-a2f64316]{font-weight:400}.api-groups a[data-v-a2f64316]:hover{color:var(--vt-c-green);transition:none}.api-group[data-v-a2f64316]{-moz-column-break-inside:avoid;break-inside:avoid;overflow:auto;margin-bottom:20px;background-color:var(--vt-c-bg-soft);border-radius:8px;padding:28px 26px;transition:background-color .5s}.header[data-v-a2f64316]{display:flex;align-items:center;justify-content:space-between}.api-filter[data-v-a2f64316]{display:flex;align-items:center;justify-content:flex-start;gap:1rem}.api-filter input[data-v-a2f64316]{border:1px solid var(--vt-c-divider);border-radius:8px;padding:6px 12px}.api-filter[data-v-a2f64316]:focus{border-color:var(--vt-c-green-light)}.no-match[data-v-a2f64316]{font-size:1.2em;color:var(--vt-c-text-3);text-align:center;margin-top:36px;padding-top:36px;border-top:1px solid var(--vt-c-divider-light)}@media (max-width:768px){#api-index[data-v-a2f64316]{padding:42px 24px}h1[data-v-a2f64316]{font-size:32px;margin-bottom:24px}h2[data-v-a2f64316]{font-size:22px;margin:42px 0 32px;padding-top:32px}.api-groups a[data-v-a2f64316]{font-size:14px}.header[data-v-a2f64316]{display:block}}@media (min-width:768px){.api-groups[data-v-a2f64316]{-moz-columns:2;column-count:2}}@media (min-width:1024px){.api-groups[data-v-a2f64316]{-moz-columns:3;column-count:3}}.sw-update-popup[data-v-613ca22e]{position:fixed;right:1em;bottom:1em;padding:1em;border:1px solid #3eaf7c;border-radius:3px;background:#fff;box-shadow:0 4px 16px rgba(0,0,0,.5);text-align:center;z-index:3}.sw-update-popup>button[data-v-613ca22e]{margin-top:.5em;padding:.25em 2em}.sw-update-popup-enter-active[data-v-613ca22e],.sw-update-popup-leave-active[data-v-613ca22e]{transition:opacity .3s,transform .3s}.sw-update-popup-enter[data-v-613ca22e],.sw-update-popup-leave-to[data-v-613ca22e]{opacity:0;transform:translateY(50%) scale(.5)}.sidebar-group .sidebar-group{padding-left:.5em}.sidebar-group:not(.collapsable) .sidebar-heading:not(.clickable){cursor:auto;color:inherit}.sidebar-group.is-sub-group{padding-left:0}.sidebar-group.is-sub-group>.sidebar-heading{font-size:.95em;line-height:1.4;font-weight:400;padding-left:2rem}.sidebar-group.is-sub-group>.sidebar-heading:not(.clickable){opacity:.5}.sidebar-group.is-sub-group>.sidebar-group-items{padding-left:1rem}.sidebar-group.is-sub-group>.sidebar-group-items>li>.sidebar-link{font-size:.95em;border-left:none}.sidebar-group.depth-2>.sidebar-heading{border-left:none}.sidebar-heading{color:#2c3e50;transition:color .15s ease;cursor:pointer;font-size:1.1em;font-weight:700;padding:.35rem 1.5rem .35rem 1.25rem;width:100%;box-sizing:border-box;margin:0;border-left:.25rem solid transparent}.sidebar-heading.open,.sidebar-heading:hover{color:inherit}.sidebar-heading .arrow{position:relative;top:-.12em;left:.5em}.sidebar-heading.clickable.active{font-weight:600;color:#3eaf7c;border-left-color:#3eaf7c}.sidebar-heading.clickable:hover{color:#3eaf7c}.sidebar-group-items{transition:height .1s ease-out;font-size:.95em;overflow:hidden}.sidebar .sidebar-sub-headers{padding-left:1rem;font-size:.95em}a.sidebar-link{font-size:1em;font-weight:400;display:inline-block;color:#2c3e50;border-left:.25rem solid transparent;padding:.35rem 1rem .35rem 1.25rem;line-height:1.4;width:100%;box-sizing:border-box}a.sidebar-link:hover{color:#3eaf7c}a.sidebar-link.active{font-weight:600;color:#3eaf7c;border-left-color:#3eaf7c}.sidebar-group a.sidebar-link{padding-left:2rem}.sidebar-sub-headers a.sidebar-link{padding-top:.25rem;padding-bottom:.25rem;border-left:none}.sidebar-sub-headers a.sidebar-link.active{font-weight:500}.dropdown-wrapper{cursor:pointer}.dropdown-wrapper .dropdown-title,.dropdown-wrapper .mobile-dropdown-title{display:block;font-size:.9rem;font-family:inherit;cursor:inherit;padding:inherit;line-height:1.4rem;background:transparent;border:none;font-weight:500;color:#2c3e50}.dropdown-wrapper .dropdown-title:hover,.dropdown-wrapper .mobile-dropdown-title:hover{border-color:transparent}.dropdown-wrapper .dropdown-title .arrow,.dropdown-wrapper .mobile-dropdown-title .arrow{vertical-align:middle;margin-top:-1px;margin-left:.4rem}.dropdown-wrapper .mobile-dropdown-title{display:none;font-weight:600}.dropdown-wrapper .mobile-dropdown-title font-size inherit:hover{color:#3eaf7c}.dropdown-wrapper .nav-dropdown .dropdown-item{color:inherit;line-height:1.7rem}.dropdown-wrapper .nav-dropdown .dropdown-item h4{margin:.45rem 0 0;border-top:1px solid #eee;padding:1rem 1.5rem .45rem 1.25rem}.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem-wrapper{padding:0;list-style:none}.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem-wrapper .dropdown-subitem{font-size:.9em}.dropdown-wrapper .nav-dropdown .dropdown-item a{display:block;line-height:1.7rem;position:relative;border-bottom:none;font-weight:400;margin-bottom:0;padding:0 1.5rem 0 1.25rem}.dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active,.dropdown-wrapper .nav-dropdown .dropdown-item a:hover{color:#3eaf7c}.dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active:after{content:"";width:0;height:0;border-left:5px solid #3eaf7c;border-top:3px solid transparent;border-bottom:3px solid transparent;position:absolute;top:calc(50% - 2px);left:9px}.dropdown-wrapper .nav-dropdown .dropdown-item:first-child h4{margin-top:0;padding-top:0;border-top:0}@media (max-width:719px){.dropdown-wrapper.open .dropdown-title{margin-bottom:.5rem}.dropdown-wrapper .dropdown-title{display:none}.dropdown-wrapper .mobile-dropdown-title{display:block}.dropdown-wrapper .nav-dropdown{transition:height .1s ease-out;overflow:hidden}.dropdown-wrapper .nav-dropdown .dropdown-item h4{border-top:0;margin-top:0;padding-top:0}.dropdown-wrapper .nav-dropdown .dropdown-item>a,.dropdown-wrapper .nav-dropdown .dropdown-item h4{font-size:15px;line-height:2rem}.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem{font-size:14px;padding-left:1rem}}@media (min-width:719px){.dropdown-wrapper{height:1.8rem}.dropdown-wrapper.open .nav-dropdown,.dropdown-wrapper:hover .nav-dropdown{display:block!important}.dropdown-wrapper .nav-dropdown{display:none;height:auto!important;box-sizing:border-box;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;background-color:#fff;padding:.6rem 0;border:1px solid;border-color:#ddd #ddd #ccc;text-align:left;border-radius:.25rem;white-space:nowrap;margin:0}}.nav-links{display:inline-block}.nav-links a{line-height:1.4rem;color:inherit}.nav-links a.router-link-active,.nav-links a:hover{color:#3eaf7c}.nav-links .nav-item{position:relative;display:inline-block;margin-left:1.5rem;line-height:2rem}.nav-links .nav-item:first-child{margin-left:0}.nav-links .repo-link{margin-left:1.5rem}@media (max-width:719px){.nav-links .nav-item,.nav-links .repo-link{margin-left:0}}@media (min-width:719px){.nav-links a.router-link-active,.nav-links a:hover{color:#2c3e50}.nav-item>a:not(.external).router-link-active,.nav-item>a:not(.external):hover{margin-bottom:-2px;border-bottom:2px solid #46bd87}}.sidebar ul{padding:0;margin:0;list-style-type:none}.sidebar a{display:inline-block}.sidebar .nav-links{display:none;border-bottom:1px solid #eaecef;padding:.5rem 0 .75rem}.sidebar .nav-links a{font-weight:600}.sidebar .nav-links .nav-item,.sidebar .nav-links .repo-link{display:block;line-height:1.25rem;font-size:1.1em;padding:.5rem 0 .5rem 1.5rem}.sidebar>.sidebar-links{padding:1.5rem 0}.sidebar>.sidebar-links>li>a.sidebar-link{font-size:1.1em;line-height:1.7;font-weight:700}.sidebar>.sidebar-links>li:not(:first-child){margin-top:.75rem}@media (max-width:719px){.sidebar .nav-links{display:block}.sidebar .nav-links .dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active:after{top:calc(1rem - 2px)}.sidebar>.sidebar-links{padding:1rem 0}}.test{width:200px;height:200px;background:red} \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg b/docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg new file mode 100644 index 0000000000..03d83913e8 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg @@ -0,0 +1 @@ + diff --git a/docs-vuepress/.vuepress/dist/assets/img/start-tips1.3b76ac97.png b/docs-vuepress/.vuepress/dist/assets/img/start-tips1.3b76ac97.png new file mode 100644 index 0000000000000000000000000000000000000000..6b037f53b8e4129bed96e17654b47caaca1a6979 GIT binary patch literal 39681 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 a0?p;miHTny0XBdAufF5I<2Crt$^QXRr|zHt literal 0 HcmV?d00001 diff --git a/docs-vuepress/.vuepress/dist/assets/img/start-tips2.7d8836f8.png b/docs-vuepress/.vuepress/dist/assets/img/start-tips2.7d8836f8.png new file mode 100644 index 0000000000000000000000000000000000000000..7297ca2ee43cc1c617d16ae5d49bc16e63bcfb44 GIT binary patch literal 51539 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 DPj#Nk literal 0 HcmV?d00001 diff --git a/docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js b/docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js new file mode 100644 index 0000000000..9af842699f --- /dev/null +++ b/docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[1,4,15,16,20,31,34],{290:function(t,e,n){"use strict";n.d(e,"d",(function(){return i})),n.d(e,"a",(function(){return s})),n.d(e,"i",(function(){return a})),n.d(e,"f",(function(){return l})),n.d(e,"g",(function(){return u})),n.d(e,"h",(function(){return c})),n.d(e,"b",(function(){return p})),n.d(e,"e",(function(){return h})),n.d(e,"k",(function(){return d})),n.d(e,"l",(function(){return f})),n.d(e,"c",(function(){return m})),n.d(e,"j",(function(){return g}));n(69),n(10),n(19),n(48),n(30);const i=/#.*$/,r=/\.(md|html)$/,s=/\/$/,a=/^[a-z]+:/i;function o(t){return decodeURI(t).replace(i,"").replace(r,"")}function l(t){return a.test(t)}function u(t){return/^mailto:/.test(t)}function c(t){return/^tel:/.test(t)}function p(t){if(l(t))return t;const e=t.match(i),n=e?e[0]:"",r=o(t);return s.test(r)?t:r+".html"+n}function h(t,e){const n=decodeURIComponent(t.hash),r=function(t){const e=t.match(i);if(e)return e[0]}(e);if(r&&n!==r)return!1;return o(t.path)===o(e)}function d(t,e,n){if(l(e))return{type:"external",path:e};n&&(e=function(t,e,n){const i=t.charAt(0);if("/"===i)return t;if("?"===i||"#"===i)return e+t;const r=e.split("/");n&&r[r.length-1]||r.pop();const s=t.replace(/^\//,"").split("/");for(let t=0;tfunction t(e,n,i,r=1){if("string"==typeof e)return d(n,e,i);if(Array.isArray(e))return Object.assign(d(n,e[0],i),{title:e[1]});{const s=e.children||[];return 0===s.length&&e.path?Object.assign(d(n,e.path,i),{title:e.title}):{type:"group",path:e.path,title:e.title,sidebarDepth:e.sidebarDepth,initialOpenGroupIndex:e.initialOpenGroupIndex,children:s.map(e=>t(e,n,i,r+1)),collapsable:!1!==e.collapsable}}}(t,r,n)):[]}return[]}function b(t){const e=m(t.headers||[]);return[{type:"group",collapsable:!1,title:t.title,path:null,children:e.map(e=>({type:"auto",title:e.title,basePath:t.path,path:t.path+"#"+e.slug,children:e.children||[]}))}]}function m(t){let e;return(t=t.map(t=>Object.assign({},t))).forEach(t=>{2===t.level?e=t:e&&(e.children||(e.children=[])).push(t)}),t.filter(t=>2===t.level)}function g(t){return Object.assign(t,{type:t.items&&t.items.length?"links":"link"})}},291:function(t,e,n){},293:function(t,e,n){"use strict";n.r(e);n(10),n(47);var i=n(290),r={name:"NavLink",props:{item:{required:!0}},computed:{link(){return Object(i.b)(this.item.link)},exact(){return this.$site.locales?Object.keys(this.$site.locales).some(t=>t===this.link):"/"===this.link},isNonHttpURI(){return Object(i.g)(this.link)||Object(i.h)(this.link)},isBlankTarget(){return"_blank"===this.target},isInternal(){return!Object(i.f)(this.link)&&!this.isBlankTarget},target(){return this.isNonHttpURI?null:this.item.target?this.item.target:Object(i.f)(this.link)?"_blank":""},rel(){return this.isNonHttpURI||!1===this.item.rel?null:this.item.rel?this.item.rel:this.isBlankTarget?"noopener noreferrer":null}},methods:{focusoutAction(){this.$emit("focusout")}}},s=n(4),a=Object(s.a)(r,(function(){var t=this,e=t._self._c;return t.isInternal?e("RouterLink",{staticClass:"nav-link",attrs:{to:t.link,exact:t.exact},nativeOn:{focusout:function(e){return t.focusoutAction.apply(null,arguments)}}},[t._v("\n "+t._s(t.item.text)+"\n")]):e("a",{staticClass:"nav-link external",attrs:{href:t.link,target:t.target,rel:t.rel},on:{focusout:t.focusoutAction}},[t._v("\n "+t._s(t.item.text)+"\n "),t.isBlankTarget?e("OutboundLink"):t._e()],1)}),[],!1,null,null,null);e.default=a.exports},294:function(t,e,n){"use strict";n(291)},296:function(t,e,n){"use strict";n.r(e);var i={name:"DropdownTransition",methods:{setHeight(t){t.style.height=t.scrollHeight+"px"},unsetHeight(t){t.style.height=""}}},r=(n(294),n(4)),s=Object(r.a)(i,(function(){return(0,this._self._c)("transition",{attrs:{name:"dropdown"},on:{enter:this.setHeight,"after-enter":this.unsetHeight,"before-leave":this.setHeight}},[this._t("default")],2)}),[],!1,null,null,null);e.default=s.exports},297:function(t,e,n){},300:function(t,e,n){},304:function(t,e,n){"use strict";n(297)},306:function(t,e,n){},311:function(t,e,n){},312:function(t,e,n){"use strict";n(313)},313:function(t,e,n){"use strict";var i=n(14),r=n(49),s=n(11),a=n(5),o=n(26);i({target:"Iterator",proto:!0,real:!0},{find:function(t){a(this),s(t);var e=o(this),n=0;return r(e,(function(e,i){if(t(e,n++))return i(e)}),{IS_RECORD:!0,INTERRUPTED:!0}).result}})},314:function(t,e,n){"use strict";n(300)},322:function(t,e,n){"use strict";n.r(e);var i=n(293),r=n(296),s=n(127),a=n.n(s),o={name:"DropdownLink",components:{NavLink:i.default,DropdownTransition:r.default},props:{item:{required:!0}},data:()=>({open:!1}),computed:{dropdownAriaLabel(){return this.item.ariaLabel||this.item.text}},watch:{$route(){this.open=!1}},methods:{setOpen(t){this.open=t},isLastItemOfArray:(t,e)=>a()(e)===t,handleDropdown(){0===event.detail&&this.setOpen(!this.open)}}},l=(n(304),n(4)),u=Object(l.a)(o,(function(){var t=this,e=t._self._c;return e("div",{staticClass:"dropdown-wrapper",class:{open:t.open}},[e("button",{staticClass:"dropdown-title",attrs:{type:"button","aria-label":t.dropdownAriaLabel},on:{click:t.handleDropdown}},[e("span",{staticClass:"title"},[t._v(t._s(t.item.text))]),t._v(" "),e("span",{staticClass:"arrow down"})]),t._v(" "),e("button",{staticClass:"mobile-dropdown-title",attrs:{type:"button","aria-label":t.dropdownAriaLabel},on:{click:function(e){return t.setOpen(!t.open)}}},[e("span",{staticClass:"title"},[t._v(t._s(t.item.text))]),t._v(" "),e("span",{staticClass:"arrow",class:t.open?"down":"right"})]),t._v(" "),e("DropdownTransition",[e("ul",{directives:[{name:"show",rawName:"v-show",value:t.open,expression:"open"}],staticClass:"nav-dropdown"},t._l(t.item.items,(function(n,i){return e("li",{key:n.link||i,staticClass:"dropdown-item"},["links"===n.type?e("h4",[t._v("\n "+t._s(n.text)+"\n ")]):t._e(),t._v(" "),"links"===n.type?e("ul",{staticClass:"dropdown-subitem-wrapper"},t._l(n.items,(function(i){return e("li",{key:i.link,staticClass:"dropdown-subitem"},[e("NavLink",{attrs:{item:i},on:{focusout:function(e){t.isLastItemOfArray(i,n.items)&&t.isLastItemOfArray(n,t.item.items)&&t.setOpen(!1)}}})],1)})),0):e("NavLink",{attrs:{item:n},on:{focusout:function(e){t.isLastItemOfArray(n,t.item.items)&&t.setOpen(!1)}}})],1)})),0)])],1)}),[],!1,null,null,null);e.default=u.exports},325:function(t,e,n){"use strict";n.r(e);n(10),n(47);var i=n(338),r=n(327),s=n(290);function a(t,e){if("group"===e.type){const n=e.path&&Object(s.e)(t,e.path),i=e.children.some(e=>"group"===e.type?a(t,e):"page"===e.type&&Object(s.e)(t,e.path));return n||i}return!1}var o={name:"SidebarLinks",components:{SidebarGroup:i.default,SidebarLink:r.default},props:["items","depth","sidebarDepth","initialOpenGroupIndex"],data(){return{openGroupIndex:this.initialOpenGroupIndex||0}},watch:{$route(){this.refreshIndex()}},created(){this.refreshIndex()},methods:{refreshIndex(){const t=function(t,e){for(let n=0;n-1&&(this.openGroupIndex=t)},toggleGroup(t){this.openGroupIndex=t===this.openGroupIndex?-1:t},isActive(t){return Object(s.e)(this.$route,t.regularPath)}}},l=n(4),u=Object(l.a)(o,(function(){var t=this,e=t._self._c;return t.items.length?e("ul",{staticClass:"sidebar-links"},t._l(t.items,(function(n,i){return e("li",{key:i},["group"===n.type?e("SidebarGroup",{attrs:{item:n,open:i===t.openGroupIndex,collapsable:n.collapsable||n.collapsible,depth:t.depth},on:{toggle:function(e){return t.toggleGroup(i)}}}):e("SidebarLink",{attrs:{"sidebar-depth":t.sidebarDepth,item:n}})],1)})),0):t._e()}),[],!1,null,null,null);e.default=u.exports},327:function(t,e,n){"use strict";n.r(e);n(10),n(312),n(30),n(47);var i=n(290);function r(t,e,n,i,r){const s={props:{to:e,activeClass:"",exactActiveClass:""},class:{active:i,"sidebar-link":!0}};return r>2&&(s.style={"padding-left":r+"rem"}),t("RouterLink",s,n)}function s(t,e,n,a,o,l=1){return!e||l>o?null:t("ul",{class:"sidebar-sub-headers"},e.map(e=>{const u=Object(i.e)(a,n+"#"+e.slug);return t("li",{class:"sidebar-sub-header"},[r(t,n+"#"+e.slug,e.title,u,e.level-1),s(t,e.children,n,a,o,l+1)])}))}var a={functional:!0,props:["item","sidebarDepth"],render(t,{parent:{$page:e,$site:n,$route:a,$themeConfig:o,$themeLocaleConfig:l},props:{item:u,sidebarDepth:c}}){const p=Object(i.e)(a,u.path),h="auto"===u.type?p||u.children.some(t=>Object(i.e)(a,u.basePath+"#"+t.slug)):p,d="external"===u.type?function(t,e,n){return t("a",{attrs:{href:e,target:"_blank",rel:"noopener noreferrer"},class:{"sidebar-link":!0}},[n,t("OutboundLink")])}(t,u.path,u.title||u.path):r(t,u.path,u.title||u.path,h),f=[e.frontmatter.sidebarDepth,c,l.sidebarDepth,o.sidebarDepth,1].find(t=>void 0!==t),b=l.displayAllHeaders||o.displayAllHeaders;if("auto"===u.type)return[d,s(t,u.children,u.basePath,a,f)];if((h||b)&&u.headers&&!i.d.test(u.path)){return[d,s(t,Object(i.c)(u.headers),u.path,a,f)]}return d}},o=(n(314),n(4)),l=Object(o.a)(a,void 0,void 0,!1,null,null,null);e.default=l.exports},329:function(t,e,n){"use strict";n(306)},331:function(t,e,n){"use strict";n(311)},338:function(t,e,n){"use strict";n.r(e);var i=n(290),r={name:"SidebarGroup",components:{DropdownTransition:n(296).default},props:["item","open","collapsable","depth"],beforeCreate(){this.$options.components.SidebarLinks=n(325).default},methods:{isActive:i.e}},s=(n(331),n(4)),a=Object(s.a)(r,(function(){var t=this,e=t._self._c;return e("section",{staticClass:"sidebar-group",class:[{collapsable:t.collapsable,"is-sub-group":0!==t.depth},"depth-"+t.depth]},[t.item.path?e("RouterLink",{staticClass:"sidebar-heading clickable",class:{open:t.open,active:t.isActive(t.$route,t.item.path)},attrs:{to:t.item.path},nativeOn:{click:function(e){return t.$emit("toggle")}}},[e("span",[t._v(t._s(t.item.title))]),t._v(" "),t.collapsable?e("span",{staticClass:"arrow",class:t.open?"down":"right"}):t._e()]):e("p",{staticClass:"sidebar-heading",class:{open:t.open},on:{click:function(e){return t.$emit("toggle")}}},[e("span",[t._v(t._s(t.item.title))]),t._v(" "),t.collapsable?e("span",{staticClass:"arrow",class:t.open?"down":"right"}):t._e()]),t._v(" "),e("DropdownTransition",[t.open||!t.collapsable?e("SidebarLinks",{staticClass:"sidebar-group-items",attrs:{items:t.item.children,"sidebar-depth":t.item.sidebarDepth,"initial-open-group-index":t.item.initialOpenGroupIndex,depth:t.depth+1}}):t._e()],1)],1)}),[],!1,null,null,null);e.default=a.exports},339:function(t,e,n){},349:function(t,e,n){"use strict";n.r(e);n(10),n(30),n(47);var i=n(322),r=n(290),s={name:"NavLinks",components:{NavLink:n(293).default,DropdownLink:i.default},computed:{userNav(){return this.$themeLocaleConfig.nav||this.$site.themeConfig.nav||[]},nav(){const{locales:t}=this.$site;if(t&&Object.keys(t).length>1){const e=this.$page.path,n=this.$router.options.routes,i=this.$site.themeConfig.locales||{},r={text:this.$themeLocaleConfig.selectText||"Languages",ariaLabel:this.$themeLocaleConfig.ariaLabel||"Select language",items:Object.keys(t).map(r=>{const s=t[r],a=i[r]&&i[r].label||s.lang;let o;return s.lang===this.$lang?o=e:(o=e.replace(this.$localeConfig.path,r),n.some(t=>t.path===o)||(o=r)),{text:a,link:o}})};return[...this.userNav,r]}return this.userNav},userLinks(){return(this.nav||[]).map(t=>Object.assign(Object(r.j)(t),{items:(t.items||[]).map(r.j)}))},repoLink(){const{repo:t}=this.$site.themeConfig;return t?/^https?:/.test(t)?t:"https://github.com/"+t:null},repoLabel(){if(!this.repoLink)return;if(this.$site.themeConfig.repoLabel)return this.$site.themeConfig.repoLabel;const t=this.repoLink.match(/^https?:\/\/[^/]+/)[0],e=["GitHub","GitLab","Bitbucket"];for(let n=0;n({codeTabs:[],activeCodeTabIndex:-1}),watch:{activeCodeTabIndex(e){this.activateCodeTab(e)}},mounted(){this.loadTabs()},methods:{changeCodeTab(e){this.activeCodeTabIndex=e},loadTabs(){this.codeTabs=(this.$slots.default||[]).filter(e=>Boolean(e.componentOptions)).map((e,t)=>(""===e.componentOptions.propsData.active&&(this.activeCodeTabIndex=t),{title:e.componentOptions.propsData.title,elm:e.elm})),-1===this.activeCodeTabIndex&&this.codeTabs.length>0&&(this.activeCodeTabIndex=0),this.activateCodeTab(0)},activateCodeTab(e){this.codeTabs.forEach(e=>{e.elm&&e.elm.classList.remove("theme-code-block__active")}),this.codeTabs[e].elm&&this.codeTabs[e].elm.classList.add("theme-code-block__active")}}},s=(a(362),a(4)),c=Object(s.a)(o,(function(){var e=this,t=e._self._c;return t("ClientOnly",[t("div",{staticClass:"theme-code-group"},[t("div",{staticClass:"theme-code-group__nav"},[t("ul",{staticClass:"theme-code-group__ul"},e._l(e.codeTabs,(function(a,o){return t("li",{key:a.title,staticClass:"theme-code-group__li"},[t("button",{staticClass:"theme-code-group__nav-tab",class:{"theme-code-group__nav-tab-active":o===e.activeCodeTabIndex},on:{click:function(t){return e.changeCodeTab(o)}}},[e._v("\n "+e._s(a.title)+"\n ")])])})),0)]),e._v(" "),e._t("default"),e._v(" "),e.codeTabs.length<1?t("pre",{staticClass:"pre-blank"},[e._v("// Make sure to add code blocks to your code group")]):e._e()],2)])}),[],!1,null,"131b8180",null);t.default=c.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js b/docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js new file mode 100644 index 0000000000..0b6c6e453d --- /dev/null +++ b/docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[100],{447:function(t,s,a){"use strict";a.r(s);var n=a(4),r=Object(n.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"网络请求"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#网络请求"}},[t._v("#")]),t._v(" 网络请求")]),t._v(" "),s("p",[t._v("Mpx 提供了网络请求库 fetch,抹平了微信,阿里等平台请求参数及响应数据的差异;同时支持请求拦截器,请求取消等")]),t._v(" "),s("h2",{attrs:{id:"使用说明"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#使用说明"}},[t._v("#")]),t._v(" 使用说明")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mpx "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/core'")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mpxFetch "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/fetch'")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("use")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("mpxFetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 第一种访问形式")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("then")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("res")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\tconsole"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("res"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("data"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("createApp")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("onLaunch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 第二种访问形式")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("this")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("$xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://test.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"导出说明"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#导出说明"}},[t._v("#")]),t._v(" 导出说明")]),t._v(" "),s("p",[t._v("mpx-fetch提供了一个实例 "),s("strong",[t._v("xfetch")]),t._v(" ,该实例包含以下api")]),t._v(" "),s("ul",[s("li",[t._v("fetch(config), 正常的promisify风格的请求方法")]),t._v(" "),s("li",[t._v("CancelToken,实例属性,用于创建一个取消请求的凭证。")]),t._v(" "),s("li",[t._v("interceptors,实例属性,用于添加拦截器,包含两个属性,request & response")])]),t._v(" "),s("h2",{attrs:{id:"请求拦截器"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#请求拦截器"}},[t._v("#")]),t._v(" 请求拦截器")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("interceptors"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("request"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("use")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("config")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\tconsole"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("config"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 也可以返回promise")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("return")]),t._v(" config\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("interceptors"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("response"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("use")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("res")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\tconsole"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("res"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 也可以返回promise")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("return")]),t._v(" res\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"请求中断"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#请求中断"}},[t._v("#")]),t._v(" 请求中断")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("const")]),t._v(" cancelToken "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("new")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token class-name"}},[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("CancelToken")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cancelToken")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" cancelToken"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("token\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\ncancelToken"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("exec")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'手动取消请求'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 执行后请求中断,返回abort fail")]),t._v("\n")])])]),s("h2",{attrs:{id:"设置请求参数"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#设置请求参数"}},[t._v("#")]),t._v(" 设置请求参数")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("params")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("method")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'POST'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// params 参数等价于 url query")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("params")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("age")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("10")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 设置参数序列化方式,等价于header = {'content-type': 'application/x-www-form-urlencoded'}")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("emulateJSON")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"设置请求-timeout"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#设置请求-timeout"}},[t._v("#")]),t._v(" 设置请求 timeout")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("method")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'POST'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("timeout")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("10000")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 超时时间")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"composition-api-usage"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#composition-api-usage"}},[t._v("#")]),t._v(" 在组合式 API 中使用 ")]),t._v(" "),s("p",[t._v("在组合式 API 中我们提供了 "),s("RouterLink",{attrs:{to:"/api/extend.html#usefetch"}},[t._v("useFetch")]),t._v(" 方法来访问 "),s("code",[t._v("xfetch")]),t._v(" 实例对象")],1),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// app.mpx")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" createComponent "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/core'")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" useFetch "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/fetch'")]),t._v("\n\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("createComponent")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("setup")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("useFetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("method")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'POST'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("params")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("age")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("10")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("emulateJSON")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("usePre")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cacheInvalidationTime")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("3000")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("ignorePreParamKeys")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'timestamp'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("then")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("res")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("res"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("data"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" \n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\n")])])])])}),[],!1,null,null,null);s.default=r.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js b/docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js new file mode 100644 index 0000000000..ffc2ca40be --- /dev/null +++ b/docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[101],{449:function(t,s,a){"use strict";a.r(s);var n=a(4),p=Object(n.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"扩展mpx"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#扩展mpx"}},[t._v("#")]),t._v(" 扩展mpx")]),t._v(" "),s("h2",{attrs:{id:"开发插件"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#开发插件"}},[t._v("#")]),t._v(" 开发插件")]),t._v(" "),s("p",[t._v("mpx支持使用mpx.use使用插件来进行扩展。插件本身需要提供一个install方法或本身是一个function,该函数接收一个proxyMPX。插件将采用直接在proxyMPX挂载新api属性或在prototype上挂属性。需要注意的是,一定要在app创建之前进行mpx.use。")]),t._v(" "),s("p",[t._v("简单示例如下:")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("install")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("proxyMPX")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n proxyMPX"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function-variable function"}},[t._v("newApi")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'is new api'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n proxyMPX\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mixin")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("onLaunch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'app onLaunch'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'app'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mixin")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("onShow")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'page onShow'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'page'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// proxyMPX.injectMixins === proxyMPX.mixin")]),t._v("\n\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 注意:proxyMPX.prototype上挂载的属性都将挂载到组件实例(page实例、app实例上,可以直接通过this访问), 可以看mixin中的case")]),t._v("\n proxyMPX"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("prototype"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function-variable function"}},[t._v("testHello")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'hello'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("h2",{attrs:{id:"目前已有插件"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#目前已有插件"}},[t._v("#")]),t._v(" 目前已有插件")]),t._v(" "),s("ul",[s("li",[s("p",[t._v("网络请求库fetch: @mpxjs/fetch "),s("a",{attrs:{href:"#fetch"}},[t._v("详细介绍")]),t._v(" "),s("a",{attrs:{href:"https://github.com/didi/mpx/tree/master/packages/fetch",target:"_blank",rel:"noopener noreferrer"}},[t._v("源码地址"),s("OutboundLink")],1)])]),t._v(" "),s("li",[s("p",[t._v("小程序API转换及promisify:@mpxjs/api-proxy "),s("a",{attrs:{href:"#api-proxy"}},[t._v("详细介绍")]),t._v(" "),s("a",{attrs:{href:"https://github.com/didi/mpx/tree/master/packages/api-proxy",target:"_blank",rel:"noopener noreferrer"}},[t._v("源码地址"),s("OutboundLink")],1)])]),t._v(" "),s("li",[s("p",[t._v("mock数据:@mpxjs/mock "),s("a",{attrs:{href:"#mock"}},[t._v("详细介绍")]),t._v(" "),s("a",{attrs:{href:"https://github.com/didi/mpx/tree/master/packages/mock",target:"_blank",rel:"noopener noreferrer"}},[t._v("源码地址"),s("OutboundLink")],1)])])])])}),[],!1,null,null,null);s.default=p.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js b/docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js new file mode 100644 index 0000000000..317d5c68b3 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[102],{448:function(t,s,a){"use strict";a.r(s);var n=a(4),r=Object(n.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"数据-mock"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#数据-mock"}},[t._v("#")]),t._v(" 数据 Mock")]),t._v(" "),s("h2",{attrs:{id:"安装"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#安装"}},[t._v("#")]),t._v(" 安装")]),t._v(" "),s("p",[t._v("Mpx 提供了对请求响应数据进行拦截的 mock 插件,可通过如下命令进行安装:")]),t._v(" "),s("div",{staticClass:"language-sh extra-class"},[s("pre",{pre:!0,attrs:{class:"language-sh"}},[s("code",[s("span",{pre:!0,attrs:{class:"token function"}},[t._v("npm")]),t._v(" i @mpxjs/mock\n")])])]),s("h2",{attrs:{id:"使用说明"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#使用说明"}},[t._v("#")]),t._v(" 使用说明")]),t._v(" "),s("p",[t._v("新建 mock 文件目录及文件(例如:"),s("code",[t._v("src/mock/index.js")]),t._v(" ):")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// src/mock/index.js")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"list|1-10"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"id|+1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),s("p",[t._v("在入口文件( "),s("code",[t._v("app.mpx")]),t._v(" )中引入:")]),t._v(" "),s("div",{staticClass:"language-html extra-class"},[s("pre",{pre:!0,attrs:{class:"language-html"}},[s("code",[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("script")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token attr-name"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token attr-value"}},[s("span",{pre:!0,attrs:{class:"token punctuation attr-equals"}},[t._v("=")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')]),t._v("text/javascript"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),s("span",{pre:!0,attrs:{class:"token script"}},[s("span",{pre:!0,attrs:{class:"token language-javascript"}},[t._v("\n "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"mock/index"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 引入mock即可")]),t._v("\n")])]),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("\x3c!-- 其他配置 --\x3e")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("script")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token attr-name"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token attr-value"}},[s("span",{pre:!0,attrs:{class:"token punctuation attr-equals"}},[t._v("=")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')]),t._v("application/json"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),s("span",{pre:!0,attrs:{class:"token script"}},[s("span",{pre:!0,attrs:{class:"token language-javascript"}},[t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"pages"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"./pages/index"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"window"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"backgroundTextStyle"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"light"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"navigationBarBackgroundColor"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"#fff"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"navigationBarTitleText"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"WeChat"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"navigationBarTextStyle"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"black"')]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])]),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])]),s("blockquote",[s("p",[t._v("由于 mock 为全局自动代理,执行"),s("code",[t._v("@mpxjs/mock")]),t._v("所暴露的方法之后会立即拦截小程序的原生请求,如果需要根据不同环境变量等去控制是否使用 mock 数据,可以参考如下方法:")])]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// src/mock/index.js")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"list|1-10"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"id|+1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),s("div",{staticClass:"language-html extra-class"},[s("pre",{pre:!0,attrs:{class:"language-html"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("\x3c!-- app.mpx --\x3e")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("script")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token attr-name"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token attr-value"}},[s("span",{pre:!0,attrs:{class:"token punctuation attr-equals"}},[t._v("=")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')]),t._v("text/javascript"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),s("span",{pre:!0,attrs:{class:"token script"}},[s("span",{pre:!0,attrs:{class:"token language-javascript"}},[t._v("\n "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mockSetup "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"mock/index"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 当为开发环境时才启用mock")]),t._v("\n process"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("env"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token constant"}},[t._v("NODE_ENV")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("===")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"development"')]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("&&")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mockSetup")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])]),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])]),s("h2",{attrs:{id:"mock-入参"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#mock-入参"}},[t._v("#")]),t._v(" Mock 入参")]),t._v(" "),s("p",[s("code",[t._v("@mpxjs/mock")]),t._v(" 所暴露的函数仅接收一个类型为 "),s("code",[t._v("mockRequstList")]),t._v(" 的参数,该类型定义如下:")]),t._v(" "),s("div",{staticClass:"language-ts extra-class"},[s("pre",{pre:!0,attrs:{class:"language-ts"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("type")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token class-name"}},[t._v("mockItem")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n url"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token builtin"}},[t._v("string")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n rule"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" object\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("type")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token class-name"}},[t._v("mockRequstList")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token builtin"}},[t._v("Array")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("<")]),t._v("mockItem"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(">")]),t._v("\n\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("//示例:")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("let")]),t._v(" mockList"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" mockRequstList "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n url"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求触发后匹配到该链接时其响应数据会被mock拦截")]),t._v("\n rule"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// mock生成返回数据的规则")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v("'number|1-10'")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n")])])]),s("h2",{attrs:{id:"mock-规则示例"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#mock-规则示例"}},[t._v("#")]),t._v(" Mock 规则示例")]),t._v(" "),s("ul",[s("li",[t._v("基本类型数据生成")])]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"number|1-10"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 随机生成1-10中的任意整数")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"string|6"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token regex"}},[s("span",{pre:!0,attrs:{class:"token regex-delimiter"}},[t._v("/")]),s("span",{pre:!0,attrs:{class:"token regex-source language-regex"}},[t._v("[0-9a-f]")]),s("span",{pre:!0,attrs:{class:"token regex-delimiter"}},[t._v("/")])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 值支持正则表达式,随机生成6位的16进制值")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"boolean|1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 随机生成一个布尔值,值为 true 的概率是 1/2")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求 http://api.example.com 后返回值为:")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// {")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// number: 2,")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// string: "e1e6dc",')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// boolean: false")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n")])])]),s("ul",[s("li",[t._v("生成随机长度id自增的列表")])]),t._v(" "),s("details",{staticClass:"custom-block details"},[s("summary",[t._v("查看示例")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"list|2-5"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 生成长度范围在2-5的数组")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"id|+1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("0")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// id每次自增1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求 http://api.example.com 后返回")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// {")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "list": [{')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "id": 0')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// },{")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "id": 1')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// },{")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "id": 2')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }]")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n")])])])]),t._v(" "),s("ul",[s("li",[t._v("pick对象中的随机个值")])]),t._v(" "),s("details",{staticClass:"custom-block details"},[s("summary",[t._v("查看示例")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"object|2"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 随机选取object中的两条数据作为返回")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"310000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"上海市"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"320000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"江苏省"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"330000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"浙江省"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"340000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"安徽省"')]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求 http://api.example.com 后返回")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// {")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "object": {')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "330000": "浙江省",')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "340000": "安徽省"')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n")])])])]),t._v(" "),s("p",[t._v("更多生成规则可查阅 "),s("a",{attrs:{href:"https://github.com/nuysoft/Mock/wiki/Syntax-Specification",target:"_blank",rel:"noopener noreferrer"}},[t._v("Mock官方文档-Syntax Specification"),s("OutboundLink")],1)]),t._v(" "),s("p",[t._v("更多示例可查看 "),s("a",{attrs:{href:"http://mockjs.com/examples.html",target:"_blank",rel:"noopener noreferrer"}},[t._v("Mock示例"),s("OutboundLink")],1)]),t._v(" "),s("div",{staticClass:"custom-block warning"},[s("p",{staticClass:"custom-block-title"},[t._v("WARNING")]),t._v(" "),s("p",[t._v("由于小程序环境的局限性,mockjs 依赖 eval 函数实现的相关能力(如:占位符)无法正确运行")])])])}),[],!1,null,null,null);s.default=r.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js b/docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js new file mode 100644 index 0000000000..927c61ed86 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js @@ -0,0 +1 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[103],{451:function(t,s,a){"use strict";a.r(s);var e=a(4),n=Object(e.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"从旧版本迁移至-2-7"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#从旧版本迁移至-2-7"}},[t._v("#")]),t._v(" 从旧版本迁移至 2.7")]),t._v(" "),s("p",[t._v("自 2.7 版本开始,Mpx 将编译构建流程基于 Webpack5 进行了"),s("strong",[t._v("全面的重构")]),t._v(",优化解决了老的编译流程中存在的大量历史遗留问题,安全完整地支持了基于文件系统的持久化缓存,大幅提升了 Mpx 想的编译构建速度,以滴滴出行小程序为例,无缓存场景下相较 2.6 版本提速约 180%,有缓存场景下提速约 "),s("strong",[t._v("10")]),t._v(" 倍。")]),t._v(" "),s("p",[t._v("与此同时,Mpx 2.7 版本还带来了 rules 复用,完善的单元测试支持,独立分包构建,分包异步化等众多新特性。直接通过 @mpxjs/cli 创建的新项目就能够直接使用这些新特性,如果你对于编译构建没有太多定制化诉求,我们也推荐使用 @mpxjs/cli 新建项目,并将老项目中的 src 目录 copy 覆盖至新项目的方式进行迁移,这是一种成本最低的迁移方式。")]),t._v(" "),s("p",[t._v("如果你的老项目中已经对编译构建进行了高度的定制,本文将提供详细的迁移工作指引。")]),t._v(" "),s("h2",{attrs:{id:"依赖升级"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#依赖升级"}},[t._v("#")]),t._v(" 依赖升级")]),t._v(" "),s("p",[t._v("将下面列出的相关依赖升级至对应版本,如果有其他自行引入的 loader 也确保将其升级至 webpack5 兼容的版本:")]),t._v(" "),s("div",{staticClass:"language-json5 extra-class"},[s("pre",{pre:!0,attrs:{class:"language-json5"}},[s("code",[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"dependencies"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/api-proxy"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/core"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/fetch"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"devDependencies"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"webpack"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.64.4"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"webpack-merge"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.8.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"terser-webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.2.5"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"copy-webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^9.0.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"webpack-bundle-analyzer"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^4.5.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"ts-loader"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^9.2.6"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// eslint相关配置,按需安装")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^7.32.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-config-standard"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^16.0.3"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-html"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^6.2.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-import"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.25.2"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-node"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^11.1.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-promise"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.1.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^3.1.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("h2",{attrs:{id:"开启持久化缓存"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#开启持久化缓存"}},[t._v("#")]),t._v(" 开启持久化缓存")]),t._v(" "),s("p",[t._v("在 webpack 配置中添加以下配置项:")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("module"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("exports "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cache")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'filesystem'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 声明构建配置,注意如果声明某个文件夹为构建配置,需要在文件夹下放置空的package.json文件,避免构建依赖收集时将主项目的依赖项视为构建依赖")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("buildDependencies")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("build")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'build/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("config")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'config/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cacheDirectory")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'.cache/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("snapshot")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 如果希望修改node_modules下的文件时对应的缓存可以失效,可以将此处的配置改为 managedPaths: []")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("managedPaths")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'node_modules/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("h2",{attrs:{id:"修改rules"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#修改rules"}},[t._v("#")]),t._v(" 修改rules")]),t._v(" "),s("p",[t._v("在 2.7 版本中,我们优化了 .mpx 文件中各个 block 应用 loader 的规则。")]),t._v(" "),s("p",[t._v('在之前的版本中,我们基本上使用内置的规则对 .mpx 中的 block 应用 loaders,如:对 + + + + + + +

# 文档指南

该文档使用vuepress生成

# 该项目指南

已安装依赖vuepress,并已经在package.json中新增写作和部署脚本

# 写作时
+npm run docs:dev
+
+# 部署时
+npm run docs:build
+# 将在docs-vuepress/.vuepress/dist下生成静态文件,部署到相应服务器即可,部署参考下方链接
+

# 编写格式指南

  • 对于引用的文档或资料使用链接指明出处
  • 中英文混排时为了阅读体验使用空格对于英文单词进行包裹,在标点符号边缘或句首时可省略该侧空格,例如:这是一个 word 而不是 world。
  • 句首的英文单词首字母大写
  • Mpx 正确的写法为 Mpx,首字母大写,任何地方都应该这样书写,其余的专业名称参考原始的写法进行书写
  • 除英文段落外,所有标点符号使用中文半角
  • 能使用示例代码描述的部分都尽量添加示例代码进行说明,show me the code
  • 对于文档内容拓展说明的部分使用>引用的方式进行编写,like this

我们来聊聊人生吧,这个地方你可以不看但没必要

# 参考

+ + + diff --git a/docs-vuepress/.vuepress/dist/favicon.ico b/docs-vuepress/.vuepress/dist/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..891c2ce0106d2e229eaa160a87a6f4a63e4bd519 GIT binary patch literal 9662 zcmZQzU<5(~0|p?ez_3DDhwLF^z z*=!hl)+wWuroBvmDRFz}L4n(cAGBi)qjM*YaOB1M?WEKXrKu59VQz-eOU}DnwCv~j zPKn!F_j7(;c`4WgW+paz`P``zz;tk%54vX(R^JE4GiUSF7ZeF>-P?Z%slae?9>CGrm z{ok;g{PL#zkl6DL*AlSiR{~yO*tBX9TYg-?QOf*a@jt&^imdXo{Q%GJ#pm732)PrV ze1B89SZ27#ON#v9ZT`P(Ev{)}Iit=-Eg6{wN9ZUrM(0f?&=+0Y(Y^1o47_~)7Ar$B5 z%wpyL4SR6s=gxzI&*z=70d*m%4rcYWuoWZ(ou?!n#duo$FYK1U>W9|-96y&|^mm}T z!;uV}(bFWC9_sd*B0m80bNK>3v~%CC7yR?&>vRa5Ac;Ai)f5p2>vuB#?>i!We(41ds^?cM&PEX`P7d8oNgROmmNaYs z-F!Vw3B_PKaHh1^>LmKxzo*3gv3_R%t5$IRpMFBOkq%CVTd{rp3X!xB*Q=Dc9aIjL zOce&F|K@$Hf9IdI3xHcdL&l3IkJt+012{1jUiQG+*T6o`<+D%ch|tOpjm2qRz&<`j?VyBk!~eCLSuorV^TWiW zifd`%eqer81NO-%s<&c&&Hh&|<;LlDm>;H}&?%#)A674%$&(r3d6BYoWAVRuq9{t5 zhq)J>2KIG-E{+<54zFZPP}r7*vS8vU=`$V#%?VSb!?Tyy1yYw;v| zp}90e1~?8xQGNxs@&4B?l)ejd3wbo-|7j<*b4c+(XLUYDc9hRqO4EF8USjjgOF>#4 z2lzje=XOXxr)3}e&v|F8?TGQiig{BRfcbSHrEV`w2;O+%^fB-_D6nl1)UuC_!f{W* z*Ne`%ND=Y_FwF8O?dN8Mdt6^KYoZXcUDHqKwNerXAicnL_Stn;VsMUQ0n?2Gu#Tdr zeoYB-{tO&{22FpWfGaOnu=F31-At(;fcbg;kp~?pW`j8WO%+nWI_fPY=?2)g_zR3b zobAYs*OG*Q{qviY_yIWn{vVhZQ1UZq{-roM4AiE4ObM>ej_bq-7&L*|r%W|k_jBC` z`r$FTxa|P<{aL4s>E7o-^U5f9Gz3ONU^E1VbO`(h0|o|C=?4t_9~hV!7#=XNe_&u| zU;uJ}LJSN)7?=+*$TKkfVPI?k(f>f9V-Kb4f%bs-K;nM`a9t9JJ^-a3FfcHK_&}N+ zM1Npl;0Mt^7#QS1^dBeq{j0}T8?DWDN#h9dx^v_XFW literal 0 HcmV?d00001 diff --git a/docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html b/docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html new file mode 100644 index 0000000000..8e7c1007d4 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html @@ -0,0 +1,87 @@ + + + + + + 原生能力兼容 | Mpx框架 + + + + + + + + + +

# 原生能力兼容

# custom-tab-bar

Mpx 支持微信小程序原生自定义 tabbar ,关于自定义 tabbar 的详情请查看这里 (opens new window)

在 Mpx 中使用自定义 tabbar ,需要在 app.mpx 的 json 部分的 tabBar 配置里的 custom 为 true 。

// app.mpx
+<script type="application/json">
+{
+  "tabBar": {
+    "custom": true,
+    "color": "#000000",
+    "selectedColor": "#000000",
+    "backgroundColor": "#000000",
+    "list": [{
+      "pagePath": "./page/component/index",
+      "text": "组件"
+    }, {
+      "pagePath": "./page/API/index",
+      "text": "接口"
+    }]
+  },
+  "usingComponents": {}
+}
+</script>
+

在 src 目录下创建 custom-tab-bar 目录,包含 index.mpx 文件,可以在该 index.mpx 文件中编写自定义 tabbar 的模板、js、样式和 json 部分,同时也支持原生写法。

# workers

Mpx 完全支持小程序原生的 worker ,需要在 app.mpx 文件中的 json 部分声明 worker 的目录,Mpx 会将其对应目录进行打包,输出到目标代码目录中。

<script type="application/json">
+{
+  // 指定 worker 的目录
+  "workers": "workers"
+}
+</script>
+

更多详情可查看这里 (opens new window)

# 云开发

Mpx 支持微信小程序提供的原生云开发能力。如果需要在项目中使用云开发的能力,可以通过 Mpx 脚手架工具在初始化项目时选择支持云开发。如果需要支持云开发能力,在项目初始化时需要选择是微信平台下,且不能支持跨平台开发。如下图所示:

云开发

更多关于云开发相关可查看这里 (opens new window)

# useExtendedLib

Mpx 对于引入扩展库做了相关处理,可以在 app.mpx 中配置使用的扩展库,目前支持 weui 。

<script type="application/json">
+  {
+    "useExtendedLib": {
+      "weui": true
+    }
+  }
+</script>
+

还需要在 Mpx 的配置文件配置 externals 属性,来指定外部依赖,这样 Mpx 在进行打包时,会将其当做外部依赖进行打包。

module.exports = {
+  // ...
+  externals: [ 'weui' ]
+}
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html b/docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html new file mode 100644 index 0000000000..36fb075f5e --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html @@ -0,0 +1,110 @@ + + + + + + 分包异步化 | Mpx框架 + + + + + + + + + +

# 分包异步化

在小程序中,不同的分包对应不同的下载单元;因此,除了非独立分包可以依赖主包外,分包之间不能互相使用自定义组件或进行 require。 +「分包异步化」特性将允许通过一些配置和新的接口,使部分跨分包的内容可以等待下载后异步使用,从而一定程度上解决这个限制。

具体功能介绍和功能目的可 点击查看 (opens new window), Mpx对于分包异步化功能进行了完整支持。

当前 Mpx 框架默认支持以下平台的分包异步化能力:

  • 微信小程序
  • 支付宝小程序
  • 字节小程序
  • Web

在非上述平台,异步分包代码会默认降级。

# 跨分包自定义组件引用

一个分包使用其他分包的自定义组件时,由于其他分包还未下载或注入,其他分包的组件处于不可用的状态。通过为其他分包的自定义组件设置 占位组件, +我们可以先渲染占位组件作为替代,在分包下载完成后再进行替换。

在 Mpx 中使用跨分包自定义组件引用通过?root声明组件所属异步分包即可使用,示例如下:

<!--/packageA/pages/index.mpx-->
+// 这里在分包packageA中即可异步使用分包packageB中的hello组件
+<script type="application/json">
+  {
+    "usingComponents": {
+      "hello": "../../packageB/components/hello?root=packageB",
+      "simple-hello": "../components/hello"
+    },
+    "componentPlaceholder": {
+      "hello": "simple-hello"
+    }
+  }
+</script>
+
  • 注意项:目前该能力已在微信、支付宝和Web环境下支持,其他环境下框架将进行自动降级。

# 跨分包 JS 代码引用

一个分包中的代码引用其它分包的代码时,为了不让下载阻塞代码运行,我们需要异步获取引用的结果

在 Mpx 中跨分包异步引用 JS 代码时,需要在引用的 JS 路径后拼接 JS 模块所在的分包名,此外由于 require 不能传入多个参数的限制,在Mpx中无法使用回调函数的 +风格跨分包 JS 代码引用,只能使用 Promise 风格。

示例如下:

// subPackageA/index.js
+// 或者使用 Promise 风格的调用
+require.async('../commonPackage/index.js?root=subPackageB').then(pkg => {
+  pkg.getPackageName() // 'common'
+})
+
  • 注意项:目前该能力仅微信平台下支持,其他平台下框架将会自动降级

# 跨分包 Store 引用

在 Mpx 中如果想要跨分包异步引用 Store 代码,分为三个步骤

  • 页面或父组件在 created 钩子加载异步 Store
  • 异步 Store 加载完成后再渲染使用异步 Store 的组件
  • 子组件在框架内部生命周期 BEFORECREATE 钩子中动态注入 computed 和 methods
<!--pages/index/index.mpx-->
+<template>
+  <store-list wx:if="{{showStoreList}}"></store-list>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+  createPage({
+    data: {
+      showStoreList: false
+    },
+    created () {
+      require.async('../subpackages/sub2/store?root=sub2').then(store => {
+        getApp().asyncStore.sub2 = store.default
+        // 当异步 Store 加载完成后再渲染使用异步 Store 的组件
+        this.showStoreList = true
+      })
+    }
+  })
+</script>
+
+<!-- 子组件:store-list -->
+<script>
+  import { createComponent, BEFORECREATE } from '@mpxjs/core'
+  createComponent({
+    // 在 BEFORECREATE 钩子中动态注入 options
+    [BEFORECREATE] () {
+      // 获取异步 Store实例
+      const subStore = getApp().asyncStore.sub2
+      // computed 中 mapState、mapGetters 替换为 mapStateToInstance、mapGettersToInstance,最后一个参数必须传当前 component 实例 this
+      subStore.mapStateToInstance(['pagename'], this)
+      subStore.mapGettersToInstance(['pageDataGetter'], this)
+      // methods 中 mapActions、mapMutations 替换为 mapMutationsToInstance、mapActionsToInstance,最后一个参数必须传当前 component 实例 this
+      subStore.mapMutationsToInstance(['updatePageData'], this)
+      subStore.mapActionsToInstance(['updatePageName'], this)
+    }
+  })
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html b/docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html new file mode 100644 index 0000000000..58a1f1c010 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html @@ -0,0 +1,101 @@ + + + + + + 自定义路径 | Mpx框架 + + + + + + + + + +

# 自定义路径

Mpx在构建时,如果引用的页面不存在于当前 app.mpx 所在的上下文中,例如存在于 npm 包中,为避免和本地声明的其他 page 路径冲突,Mpx 会对页面路径进行 hash 化处理, +同时处理组件路径时也会添加 hash 防止路径名冲突,hash 化处理后最终的文件名是 name+hash+ext 的格式

与此同时,部分开发者希望能够对最终输出的页面路径和组件路径能够自定义,Mpx 对此也提供了相应的配置和Api来支持用户自定义路径

# 自定义页面路径

针对 Mpx 对 json 中非当前上下文的页面路径 hash 化的特性,如果为提升可读性需要避免路径 hash 化,可以使用该特性,具体为在 +json 中配置 pages 时,数组中支持放入 Object,对象中传入两个字段,src 字段表示页面地址,path 字段表示自定义页面路径

  • 示例:

object风格的页面声明

{
+  // 主包中的声明
+  "pages": [
+    {
+      "src": "@someGroup/someNpmPackage/pages/view/index.mpx",
+      "path": "pages/somNpmPackage/index" // 注意保持 path 的唯一性
+    }
+  ],
+  // 分包中的声明
+  "subPackages": [
+    {
+      "root": "test",
+      "pages": [
+         {
+           "src": "@someGroup/someNpmPackage/pages/view/test.mpx",
+           "path": "pages/somNpmPackage/test" // 注意保持 path 的唯一性
+         }
+      ]
+    }
+  ]
+}
+

使用声明中配置的页面路径进行跳转

mpx.navigateTo({
+  url: '/pages/somNpmPackage/index'
+})
+
+mpx.navigateTo({
+  url: '/test/pages/somNpmPackage/test'
+})
+

# customOutputPath

Mpx 框架 webpack-plugin 也提供了 customOutputPath 方法可以让用户进行页面和组件路径的自定义, +可使用该方法对非原生组件和非当前文件context的页面输出路径进行自定义

需要注意的是,该方法需要具有稳定性和唯一性,即同样的输入不管什么时候执行都要有同样的返回以及不同的的输入一定会得到不同的输出。

  • 示例
// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+         customOutputPath: (type, name, hash, ext, resourcePath) => {
+          // type: 资源类型(page | component | static)
+          // name: 资源原有文件名
+          // hash: 8位长度的hash串
+          // ext: 文件后缀(.js| .wxml | .json 等)
+          // resourcePath: 资源路径
+
+          // 输出示例: pages/testax34dde3/index.js
+          return path.join(type + 's', name + hash, 'index' + ext)
+        }
+      }
+    }
+  }
+})
+

基于上方示例,你可以根据需要进行路径定制化,例如缩短hash、使用012代替文件名等各种自定义路径

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html b/docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html new file mode 100644 index 0000000000..ff5a026123 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html @@ -0,0 +1,126 @@ + + + + + + 使用 DllPlugin | Mpx框架 + + + + + + + + + +

# 使用 DllPlugin

# 相关插件简述

# DllPlugin

DllPlugin 和 DllReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度,这个插件是在一个额外的独立的 webpack 设置中创建一个只有 dll 的 bundle(dll-only-bundle)。并且会生成一个名为 manifest.json的文件,这个文件是用来让 DLLReferencePlugin 映射到相关的依赖上去的。

DllPlugin 用来将我们某些长时间不变更的资源,独立拆分出去,之后项目的每次构建这部分资源都不需要重新编译,而是直接通过 DLLReferencePlugin 引入 dll bundle, 大大的节省了构建时间。

构建生成对应的 dll bundle 和 manifest.json 文件,manifest 文件中是模块 id 与请求路径的关系映射。

# DllReferencePlugin

这个插件是在 webpack 主配置文件中设置的, 这个插件把只有 dll 的 bundle(们)(dll-only-bundle(s)) 引用到需要的预编译的依赖

通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 webpack_require 函数来 require 他们。

# Mpx中 DllPlugin 的使用

1.通过 @mpxjs/cli init 生成项目

初始化选项中选择使用dll功能。

2.在项目中 build/dll.config.js 进行 dll 文件配置

const path = require('path')
+
+// 这里是一个用法示例
+function resolve (file) {
+  return path.resolve(__dirname, '..', file || '')
+}
+module.exports = [
+  {
+    cacheGroups: [
+      {
+        entries: [resolve('node_modules/@someNpmGroup/someNpmPkgName/dist/wx/static/js/common.js')],
+        name: 'test',
+        root: 'testSomeDllFile'
+      },
+    ],
+    modes: ['wx'],
+    entryOnly: true,
+    format: true,
+    webpackCfg: {
+      mode: 'none', // 不使用任何默认优化选项
+    }
+  },
+  {
+    cacheGroups: [
+      {
+        entries: [resolve('node_modules/@someNpmGroup/someNpmPkgName/dist/ali/static/js/common.js')],
+        name: 'test',
+        root: 'testSomeDllFile'
+      },
+    ],
+    modes: ['ali'],
+    entryOnly: true,
+    format: true,
+    webpackCfg: {
+      mode: 'none', // 不使用任何默认优化选项
+    }
+  }
+
+]
+

DllPlugin 的配置项详见文档 (opens new window),Mpx 中相关配置项依据小程序环境做了相关调整。

  • cacheGroups

    • 类型: Array<object>
      • entries +
        • 类型: Arraydll

          构建入 dll 的文件入口

      • name +
        • 类型: String

          生成的 dll 文件名

      • root +
        • 类型: String

          生成的 dll 文件夹名

  • modes

    • 类型: Array

      构建 dll 产物对应的平台

      // 配置多平台输出,例如:
      +[
      +  'wx', // 微信平台
      +  'ali' // 支付宝平台
      +]
      +
  • entryOnly

    • 类型: Boolean

      如果为true,则仅暴露入口

      这里建立使用entryOnly: true 配置

      如果为 false 时,dllPlugin 中的 tree shakeing 功能就不再起作用

      另外如果为false,如果是将分包资源打入dll bundle,会存在将全局方法打入分包 dll 中的可能性,这样主包在使用该方法被映射到 dll bundle 中时,会因为分包未加载而报错

  • webpackCfg

    • 类型: Object

      构建 dll 时可添加其他的 webpack 配置

  • format

    • 类型: Boolean

      生成的 manifest json 文件 是否进行格式化

# Mpx 中对 DllPlugin 配置所做的相关处理

正常使用 DllPlugin 是单独创建一个 webpack 配置文件,配置文件中加入 DllPlugin,然后运行 webpack 编译构建生成 dll 文件。

考虑到 Mpx 在编译时需要跨平台输出,所以 Mpx 的配置项是 Array<object> 类型,同时增加了 modes 配置项,可以自主控制输出不同平台版本 dll 文件。

构建生成 dll bundle 的主要逻辑在 buildDll.js 中,通过对 dll.config 中数组的循环处理,生成 webpackCfgs 数组。

dllConfigs.forEach((dllConfig) => {
+  const entries = getDllEntries(dllConfig.cacheGroups, dllConfig.modes)
+  // 根据配置的 mode 以及 cacheGroups 生成 entry,结果例如:
+  /**
+  *{
+        'somePkgRoot/wx.somePkgName': [
+            '/Users/didi/didiProject/mp-apphome/node_modules/@someNpmGroup/somNpmName/dist/wx/static/js/common.js'
+        ]
+    }
+  **/
+  if (Object.keys(entries).length) {
+    webpackCfgs.push(merge({
+      entry: entries,
+      output: {
+        path: config.dllPath,
+        filename: path.join('lib', dllName),
+        libraryTarget: 'commonjs2'
+      },
+      mode: 'production',
+      plugins: [
+        new webpack.DllPlugin({
+          path: path.join(config.dllPath, manifestName),
+          format: dllConfig.format,
+          entryOnly: dllConfig.entryOnly,
+          name: dllName,
+          type: 'commonjs2',
+          context: config.context
+        })
+      ]
+    }, dllConfig.webpackCfg))
+  }
+})
+

生成的 webpackCfgs 数组配置项,传入 webpack 执行,最终在 dll 文件夹下生成 dll bundle 和 manifest.json 文件

之后在 build.js 中使用 DllReferencePlugin 将编译中的依赖项与 dll bundle 的模块 id 关联起来,这里我们通过多个 DllReferencePlugin 实例来将可能存在的多个 manifest 文件关联引入,具体在项目 build.js 文件中:

plugins.push(new webpack.DllReferencePlugin({
+    context: config.context, //(绝对路径) manifest (或者是内容属性)中请求的上下文
+    manifest: manifest.content // 请求到模块 id 的映射(默认值为 manifest.content)
+}))
+

DllReferencePlugin 的其他配置项详见文档 (opens new window)

# 总结

综上所述,在 Mpx 中使用 dllPlugin 时,只需要进行 build/dll.config.js 文件的配置,然后通过 build:dll 命令生成 dll bundle,之后就可以正常的进行代码的 build 了。不过每次 build 需要检查下项目中使用的 npm 包版本与 dll bundle 中的 npm 包版本是否一致,避免因为包版本的滞后更新导致线上 bug,这里我们后续也会提供相应的包版本比对插件。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/i18n.html b/docs-vuepress/.vuepress/dist/guide/advance/i18n.html new file mode 100644 index 0000000000..207d91a34b --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/i18n.html @@ -0,0 +1,276 @@ + + + + + + 国际化i18n | Mpx框架 + + + + + + + + + +

# 国际化i18n

Mpx 支持国际化 i18n,使用方式及支持能力与 vue-i18n 非常接近。

Mpx 自带 i18n 能力,无需额外安装插件。由于小程序模板中的 i18n 函数是通过 wxs 编译注入进行实现,我们需要将 i18n 配置传入到 MpxWebpackPlugin 中来使 i18n 生效,这是与 vue-i18n 最大的区别。

# 开启 i18n

I18n 配置传入到 MpxWebpackPlugin 选项中即可生效,额外支持 messagesPath 配置,通过模块路径传入语言集,其余配置参考 vue-i18n。

由于小程序的双线程特性,在选项式 API 场景下,默认情况下模板中调用的 i18n 函数由 wxs 实现,而 js 中调用的 i18n 函数由 js 实现,该设计能够将视图层和逻辑层之间的通信开销降至最低,得到最优的性能表现,但是由于 wxs 和 js 之间无法共享数据,在最终的编译产物中语言集会同时存在于 js 和 wxs 当中,对包体积产生负面影响。

为了平衡上述影响,自2.6.56版本之后我们新增了编译配置项 i18n.useComputed ,改配置项开启的情况下对于模板中的 i18n 调用将不再使用 wxs 实现,而是通过在 computed 进行实现,语言集将只存在于 js 逻辑层当中,对于节省了包体积的同时双线程通信成本也会增加,用时间换空间,具体是否开启可以根据实际项目的使用情况及资源瓶颈由开发者自行决定, +另外需要注意的是,在 Mpx 组合式 API 场景下 i18n 仅支持通过 computed 实现

开启 i18n.useComputed 配置时,由于 computed 技术架构的限制,i18n 函数无法对模板循环渲染中的 itemindex 生效。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        i18n: {
+          locale: 'en-US',
+          // messages既可以通过对象字面量传入,也可以通过messagesPath指定一个js模块路径,在该模块中定义配置并导出,dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理
+          messages: {
+            'en-US': {
+              message: {
+                hello: '{msg} world'
+              }
+            },
+            'zh-CN': {
+              message: {
+                hello: '{msg} 世界'
+              }
+            }
+          },
+          useComputed: false // 默认 false,  此开关将模板中的 i18n 函数注入 computed,在包体积空间紧张的情况下可以使用
+          // messagesPath: path.resolve(__dirname, '../src/i18n.js')
+        }
+      }
+    }
+  }
+})
+

# 选项式 API 中使用

同 vue-i18n,在组件中直接调用翻译函数使用,由于 wxs 执行环境的限制,目前 js 中支持了 vue-i18n 中 $t/$tc/$te 翻译函数,$d/$n 暂不支持,详细使用方法可参考 vue-i18n。

此外类似于 vue-i18n,在组件模板的 Mustache 插值中直接调用翻译函数。

<template>
+    <view>
+        <view wx:if="{{isMessageExist}}">{{ $t('message.hello', { msg: 'hello' }) }}</view>
+    </view>
+</template>
+<script>
+    createComponent({
+        ready () {
+            console.log(this.$t('message.hello', { msg: 'hello' }))
+        },
+        computed: {
+            isMessageExist () {
+                return this.$te('message.hello')
+            }
+        }
+    })
+</script>
+

# 在组合式 API 中使用

在组合式 API setup 中,this 不是该活跃实例的引用,为了继续使用 i18n 的相关能力,我们需要一个新的方法来替代 this +,这里 Mpx 提供 useI18n 方法支持对 i18n 相关功能的使用。

# 开始使用

同 vue-i18n, useI18n 返回一个 i18n 实例,该实例提供文案翻译 API 例如 t 方法,相较于选项式 API 中的翻译方法,在组合式 API 中有部分变化。

  1. 翻译方法 $t/t 和 $tc/tc,在组合式 API 中统一使用翻译方法 t
  2. 翻译方法 $te/te,在组合式 API 中统一使用 te
  3. 翻译方法 $tm/tm,在组合式 API 中统一使用 tm

useI18n 方法的执行必须在 setup 中的顶层。

i18n 同时也支持传入参数,例如 localefallbackLocale, +关于实例的更多信息可移步API章节 查看

在不给 useI18n 传入任何参数时,i18n 实例的上下文将是全局作用域,即翻译函数 t 引用的文案来源是我们在 MpxWebpackPlugin +中配置的文案。

通过在 setup 中返回翻译函数 t,我们可以在模版中直接使用它:

<template>
+    <view>{{t("message.hello")}}</view>
+</template>
+
import {createComponent, useI18n} from '@mpxjs/core'
+
+createComponent({
+  setup(props, context) {
+    const { t } = useI18n() // 从返回中解构出 t 方法
+    return {
+      t
+    } // 将 t 方法返回挂载到渲染实例
+  }
+})
+

注意事项:

组合式 API 中,Mpx 在编译时对模版进行 ast 遍历分析,检测到对应的翻译函数 t/tc/te 后,将其转换为 computed 方法注入,computed 中调用 setup 中返回的 t 方法进行文案获取,由此引申出来两个注意项:

1.不可对从 useI18n 中解构出的 t 方法进行重命名,否则模版中将无法正确获取文案。

下方即为错误示例:

<template>
+    <view>{{t1('message.hello')}}</view>
+</template>
+<script>
+    import {createComponent, useI18n} from '@mpxjs/core'
+    createComponent({
+        setup() {
+            const {t: t1} = useI18n()
+            return {
+                t1
+            }
+        }
+    })
+</script>
+

2.模版中的 t/tc/te 方法都是使用 computed 实现,由于 computed 技术架构的限制,i18n 函数无法对模板循环渲染中的 itemindex 生效。

例如下方示例将会报错 computed 中找不到 item

<template>
+    <!--tc方法使用 computed 实现,最终将无法正确获取item-->
+    <view wx:for="{{listData}}" wx:key="index">{{tc('message.hello', {msg: item})}}</view>
+</template>
+<script>
+    import {createComponent, ref, useI18n} from '@mpxjs/core'
+    createComponent({
+        setup() {
+            const listData = ref([1,2,3])
+            const {tc} = useI18n()
+            return {
+                tc,
+                listData
+            }
+        }
+    })
+</script>
+

# 作用域

在 Mpx 组合式 API 中,useI18n 返回的 i18n 实例默认是指向全局作用域,文案来源即为 MpxWebpackPlugin 中配置的 messages。

如果需要在组合式 API 中使用本地作用域,需要给 useI18n 传入响应的配置项,useI18n 将根据传入的 locale、messages 等配置项来生成一个全新的 +i18n 实例,该实例的文案源即为用户传入的 messages

<template>
+    <!--将展示本地作用域中的文案 哈喽-->
+    <view>{{t('message.hello')}}</view>
+</template>
+<script>
+    import {createPage, useI18n} from '@mpxjs/core'
+    createPage({
+        setup() {
+            const { t } = useI18n({
+                locale: 'en-US',
+                messages: {
+                    'en-US': {
+                        message: {
+                            hello: '哈喽'
+                        }
+                    }
+                }
+            })
+            return {
+                t
+            }
+        }
+    })
+</script>
+

注意事项:

在给 useI18n 传参时,若只传入 messages 属性,则 locale 会自动 fallback 到全局 locale,修改 useI18n 返回的 locale ref 值将改变本地作用域 locale。

若只传入 locale 属性,不传入 messages,则会自动 fallback 到全局 localemessages,且修改 useI18n 返回的 locale全局和本地都不会起作用, +因此建议不要单独传入 locale 属性,若想使用本地 i18n 实例,则必须传入 messages 属性。

在 Mpx 选项式 API 中,i18n 实例仅有全局 global i18n 实例,所有 i18n 翻译方法 $t/$tc/$te/$tm 的执行上下文都为全局作用域。

<template>
+    <view>{{$t('message.hello')}}</view>
+</template>
+<script>
+    import {createPage} from '@mpxjs/core'
+    createPage({
+        onLoad() {
+            console.log(this.$i18n)
+            /**
+             * locale 全局 locale
+             * fallbackLocale 全局兜底 locale
+             */
+        },
+        computed: {
+            name() {
+                // 作用域为 global scope
+                return this.$t('message.name')
+            }
+        }
+    })
+</script>
+

# 动态变更locale

在 Mpx 组合式 API 中,你可以通过修改 useI18n 返回的 locale 来更换局部或全局语言,或者更改 mpx.i18n 中的 locale 属性更换全局语言集,并自动更新视图。

  • 变更全局 locale
import mpx, { createComponent, useI18n } from '@mpxjs/core'
+
+createComponent({
+  setup () {
+    // 局部locale变更,生效范围为当前组件内
+    const {t, locale} = useI18n()
+    setTimeout(() => {
+      // 全局locale变更,生效范围为项目全局,locale 是一个 ref 变量
+      locale.value = 'zh-CN'
+      // 或者通过mpx.i18n来修改,也能起到全局locale修改作用
+      mpx.i18n.locale = 'zh-CN'
+    }, 1000)
+    return {
+        t
+    }  
+  }
+})
+

注意: 这里通过 mpx.i18n 修改 locale 时,为和 vue3 保持一直,需要通过 global 属性来获取 locale,同时 locale 也是一个 ref 变量,需要使用 .value 来修改。

  • 变更局部 locale
import mpx, { createComponent, useI18n } from '@mpxjs/core'
+
+createComponent({
+  setup () {
+    // 局部locale变更,生效范围为当前组件内
+    const {t, locale} = useI18n({
+        locale: 'en-US',
+        messages: {
+            'en-US': {
+                message: {
+                    hello: 'hello'
+                }
+            },
+            'zh-CN': {
+                message: {
+                    hello: '你好'
+                }
+            }
+        }
+    })
+    setTimeout(() => {
+      // 局部locale变更,生效范围为当前组件/页面,locale 是一个 ref 变量
+      locale.value = 'zh-CN'
+    }, 1000)
+    return {
+        t
+    }  
+  }
+})
+

在选项式 API 中,仅支持修改全局 locale, 不支持修改局部 locale。

在 Vue3 中,选项式 API 中直接修改 this.$i18n.locale 无任何作用,这里为和 Vue3 拉齐建议使用 mpx.i18n 来进行 locale 修改。

import mpx, { createComponent } from '@mpxjs/core'
+
+createComponent({
+  ready () {
+      // 修改全局 locale
+      mpx.i18n.locale = 'en-US'
+  }
+})
+

# 动态更新语言集

在组合式 API 中默认支持动态更新语言集。

在选项式 API 中,当模板中没有使用 i18n 函数或开启了 i18n.useComputed 配置时, 可以对语言集进行动态更新。

我们提供了 mergeLocaleMessage 对语言集进行动态扩展,setLocaleMessage 对语言集进行覆盖更新。

使用方式如下:

<template>
+    <!--2 秒之后展示 哈喽-->
+    <view>{{t('message.hello')}}</view>
+    <!--1秒之后展示 hello1, 2秒之后展示为 恭喜你-->
+    <view>{{t('message.hello1')}}</view>
+</template>
+<script>
+    import mpx, { createComponent, useI18n} from '@mpxjs/core'
+
+    createComponent({
+        setup () {
+            const { t, mergeLocaleMessage, setLocaleMessage } = useI18n()
+            setTimeout(() => {
+                // 新增一门语言或针对特定语言更新语言集
+                mergeLocaleMessage('en-US', {
+                    message: {
+                        hello1: 'hello1',
+                    }
+                })
+            }, 1000)
+            setTimeout(() => {
+                setLocaleMessage('en-US', {
+                    message: {
+                        hello: '哈喽',
+                        hello1: '恭喜你'
+                    }
+                })
+            }, 2000)
+            return { t }
+        }
+    })
+</script>
+

同理我们在选项式 API 中可以使用 mpx.i18n.global.mergeLocaleMessage, mpx.i18n.global.setLocaleMessage 方法来动态更新或扩展语言集。

# 平台支持

目前支持业内所有小程序平台(微信/支付宝/qq/百度/头条)。

在输出 web 时,构建会自动引入 vue-i18n 并进行安装配置,无需修改任何代码即可按照预期正常工作。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/image-process.html b/docs-vuepress/.vuepress/dist/guide/advance/image-process.html new file mode 100644 index 0000000000..37b1f3de55 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/image-process.html @@ -0,0 +1,175 @@ + + + + + + 图像资源处理 | Mpx框架 + + + + + + + + + +

# 图像资源处理

Mpx在小程序开发中提供了很好的图像资源处理能力,让开发者可以愉快地在项目中使用图片。

# 图像资源引入有三种方式

  1. Template 中通过 image src 指定图像资源 +
    • 直接指定图像的远程资源地址
    • 资源为本地路径,若配置 publicPath,则 publicPath 与 webpack loader 中配置的 name 进行拼接
  2. Style 中通过 src 指定图像资源
  3. Style 中通过 class 指定图像资源

Wxss文件中只能用 CDN 地址或 Base64, 针对第二、三种方式引入的资源,可以通过配置决定使用 CDN 还是 Base64,且 Mpx 中图像资源处理会优先检查 Base64,具体配置参数如下:

  • publicPath:资源存放 CDN 地址,可选
  • limit: 资源大小限制,可根据资源的大小判断走 Base64 还是 CDN, 可选
  • publicPathScope: 限制输出 CDN 图像资源的范围,可选 styleOnly、all,默认为 styleOnly。(图像引用方式分两大类 Template, Style)
  • outputPathCDN: 设置 CDN 图像对应的本地相对地址(相对于当前编译输出目录的地址,如 dist,或者 dist/wx),可写脚本将本地图像批量上传到 CDN

# Base64 图像资源

图像转 Base64的两种方式:

  • 未配置 publicPath
  • 配置了 publicPath,且用户未自定义图像处理 fallback query,且未配置 limit 或图像资源未超过 limit 的限制时
// webpack.config.js 配置,未配置 publicPath 必走 Base64
+const webpackConfig = {
+  module: {
+    rules: [{
+      test: /\.(png|jpe?g|gif|svg)$/,
+      loader: MpxWebpackPlugin.urlLoader({
+        name: 'img/[name][hash].[ext]'
+      })
+    }]
+  }
+}
+

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      urlLoader: {
+        name: 'img/[name][hash].[ext]'
+      }
+    }
+  }
+})
+
<style>
+  .logo {
+    background-image: url('~images/logo.png');
+  }
+</style>
+

# CDN 图像资源

// webpack.config.js 配置
+const webpackConfig = {
+  module: {
+    rules: [{
+      test: /\.(png|jpe?g|gif|svg)$/,
+      loader: MpxWebpackPlugin.urlLoader({
+        name: 'img/[name][hash].[ext]',
+        // CDN 地址
+        publicPath: 'http://a.com/',
+        limit: '1024' // Base64 的最大长度,超过则走 CDN 
+      })
+    }]
+  }
+}
+

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      urlLoader: {
+        name: 'img/[name][hash].[ext]',
+        // CDN 地址
+        publicPath: 'http://a.com/',
+        limit: '1024' // Base64 的最大长度,超过则走 CDN 
+      }
+    }
+  }
+})
+

# CDN 图像资源输出本地目录,用户自行批量上传到CDN服务器

// webpack.config.js 配置
+const webpackConfig = {
+  module: {
+    rules: [{
+      test: /\.(png|jpe?g|gif|svg)$/,
+      loader: MpxWebpackPlugin.urlLoader({
+          name: 'img/[name][hash].[ext]',
+          publicPath: 'http://a.com',
+          limit: 100,
+          publicPathScope: 'styleOnly',
+          outputPathCDN: './cdnImages'
+      })
+    }]
+  }
+}
+

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      urlLoader: {
+        name: 'img/[name][hash].[ext]',
+        publicPath: 'http://a.com',
+        limit: 100,
+        publicPathScope: 'styleOnly',
+        outputPathCDN: './cdnImages'
+      }
+    }
+  }
+})
+

备注:
+图像默认编译后会输出到 img 目录下, 当设置 outputPathCDN 后,输出的本地图像地址为 outputPathCDN + img/图像.png
+CND 文件地址为 publicPath + img/图像.png,所以当使用脚本上传到 CDN 时,路径要带上 img

# 用户自定义图像处理方式

// webpack.config.js 配置
+const webpackConfig = {
+  module: {
+    rules: [{
+      test: /\.(png|jpe?g|gif|svg)$/,
+      loader: MpxWebpackPlugin.urlLoader({
+        name: 'img/[name][hash].[ext]',
+        // CDN 地址
+        publicPath: 'http://a.com/',
+        limit: '1024' // Base64 的最大长度,超过则走 CDN,
+        fallback: 'file-loader' // 默认走 file-loader
+      })
+    }]
+  }
+}
+

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      urlLoader: {
+        name: 'img/[name][hash].[ext]',
+        // CDN 地址
+        publicPath: 'http://a.com/',
+        limit: '1024' // Base64 的最大长度,超过则走 CDN,
+        fallback: 'file-loader' // 默认走 file-loader
+      }
+    }
+  }
+})
+
/*不走 Base64 的情况下*/
+<style>
+  .logo2 {
+    background-image: url('~images/logo.png?fallback=true');
+  }
+</style>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/mixin.html b/docs-vuepress/.vuepress/dist/guide/advance/mixin.html new file mode 100644 index 0000000000..312f093e26 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/mixin.html @@ -0,0 +1,115 @@ + + + + + + 使用 mixin | Mpx框架 + + + + + + + + + +

# 使用 mixin

Mpx 提供了一套完善的 mixin 机制,有人可能要问,原生小程序中已经支持了 behaviors,为何我们还需要提供 mixin 呢?主要有以下两点原因:

  1. Behaviors 是平台限度的,只有在部分小程序平台中可以使用,而且内置 behaviors 承载了除了 mixin 外的其他功能,框架提供的 mixin 是一个与平台无关的基础能力;
  2. Behaviors 只有组件支持使用,页面不支持,而且只支持局部声明,框架提供的 mixin 与组件页面无关,且支持全局 mixin 声明。

# 局部 Mixin

AppPageComponent 接收 mixins 参数,参数格式为[Mixin1(Object),Mixin2(Object)]

Mixin 混合实例对象可以像正常的实例对象一样包含选项,相同选项将进行逻辑合并。举例:如果 mixin1 包含一个钩子 ready,而创建组件 Component 也有一个钩子 ready,两个函数将被调用。 Mixin 钩子按照传入顺序(数组顺序)依次调用,并在调用组件自身的钩子之前被调用。

// mixin.js
+export default {
+  data: {
+    list: {
+      'phone': '手机',
+      'tv': '电视',
+      'computer': '电脑'
+    }
+  },
+  ready () {
+    console.log('mixins ready:', this.list.phone)
+  }
+}
+
<template>
+  <view class="list">
+    <view wx:for="{{list}}" wx:key="index">{{item}}</view>
+  </view>
+</template>
+
+<script>
+  import { createComponent } from '@mpxjs/core'
+  import mixins from '../common/mixins'
+
+  createComponent({
+    mixins: [mixins],
+    data: {
+      list: ['手机', '电视', '电脑']
+    },
+    ready () {
+      console.log('component ready:', this.list.phone)
+    }
+  })
+</script>
+
// 输出结果为
+mixins ready: 手机
+component ready: 手机
+

# 全局 Mixin

Mpx 中可以使用 mpx.injectMixins 方法配置全局 mixin,能够按照 App / 组件 / 页面维度自由配置,简单示例如下:

import mpx from '@mpxjs/core'
+
+// 第一个参数为 mixins,可以混入任意配置,第二个参数为混入生效范围,可传递 'app' / 'page' / 'component' 字符串或由其组成的数组
+mpx.injectMixins([
+  {
+    data: {
+      customData: '123'
+    }
+  }
+], ['page'])
+
+// mpx.mixin 为 mpx.injectMixins 的别名,混入单个 mixin 时可以直接传递对象,生效范围可传递字符串
+mpx.mixin({
+  methods: {
+    useCustomData () {
+      console.log(this.customData)
+    }
+  }
+}, 'component')
+
+// 当未传递生效范围时默认为全局生效,对 app / page / component 都生效
+mpx.mixin({
+  computed: {
+    processedCustomData () {
+      return this.customData + '321'
+    }
+  }
+})
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/npm.html b/docs-vuepress/.vuepress/dist/guide/advance/npm.html new file mode 100644 index 0000000000..7dc074ae6d --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/npm.html @@ -0,0 +1,144 @@ + + + + + + 使用npm | Mpx框架 + + + + + + + + + +

# 使用npm

Mpx项目中用户能够方便自然地引用 npm 资源,像 web 开发中一样

在 Mpx 中使用 npm,通过 Webpack 插件在编译时将引用的 npm 包输出为小程序文件,所以 Mpx 的构建产物不需要在开发工具再次进行 npm 构建。

# 下载npm包

在项目 package.json 所在的目录中执行命令安装项目 npm 包

npm install
+

在项目中安装某个第三方 npm 包

npm install mpx-ui
+

如若之前未接触过 npm,请翻阅 官方 npm 文档 (opens new window) 进行学习。

# 引用npm模块

直接使用模块路径引用,可以直接通过 ES6 的 import 语法来引用 JS 文件,并且无需额外执行npm构建

import { createPage } from '@mpxjs/core'
+

# 引用npm组件/页面

在页面 script 标签中的 json 对象中使用 usingComponents 引入第三方组件,直接使用模块路径引用

<script type="application/json">
+  {
+    "usingComponents": {
+      "mpx-button": "mpx-ui/src/components/button"
+    }
+  }
+</script>
+

在 app.mpx 中的 script 标签中的 pages/packages/subPackages 中都可以声明引用 npm 包页面

示例:

<script type="application/json">
+  {
+    "pages": [
+      "@someGroup/someNpmPackage/pages/view/index.mpx"
+    ]
+  }
+</script>
+

以上这种写法为避免和本地页面路径冲突,Mpx 会将路径进行 hash 化处理,所以使用页面时要在路径后添加 ?resovle 标识符,编译时会被处理成正确的、完整的绝对路径。

import packageIndexPage from '@someGroup/someNpmPackage/pages/view/index.mpx?resolve'
+
+mpx.navigateTo({
+  url: packageIndexPage
+})
+

如果你觉得上述引用 npm 包页面的方式太繁琐,我们也提供了另一种 page 声明方式,可以让你自定义页面路径

// 声明
+{
+  "pages": [
+    {
+      "src": "@someGroup/someNpmPackage/pages/view/index.mpx",
+      "path": "pages/somNpmPackage/index" // 注意保持 path 的唯一性
+    }
+  ]
+}
+
+// 使用
+// 可以直接使用你自己声明的 path
+mpx.navigateTo({
+  url: '/pages/somNpmPackage/index'
+})
+

同理,我们在使用 subPackages 分包时也可以使用 pages 对象的方式

"subPackages": [
+  {
+    "root": "test",
+    "pages": [
+       {
+         "src": "@someGroup/someNpmPackage/pages/view/index.mpx",
+         "path": "pages/somNpmPackage/index" // 注意保持 path 的唯一性
+       }
+    ]
+  }
+]
+// 使用
+mpx.navigateTo({
+  url: '/test/pages/somNpmPackage/index'
+})
+

同时使用 Mpx 引用npm组件/页面时包体积比原生中的 npm 规范更优,好处有:

Mpx npm构建的优势主要有两点:1. 按需构建;2. 支持分包

  • 小程序的 npm 规范场景下,组件库需声明miniprogram_dist目录,执行构建npm命令,将整个miniprogram_dist中的代码copy到项目的miniprogram_npm目录下。而 Mpx 的 npm 包引用,借助 Webpack 强大的构建分析能力,loader 在解析 json 中的 pages 域和 usingComponents 域中的路径时,通过动态创建 entry 的方式把这些文件添加进来,同时按需加载被确切使用的文件,降低包体积,借助 CommonsChunkPlugin/SplitChunksPlugin 的能力将复用的 js 模块抽出到一个外部公用的 bundle 中。

  • 原生小程序的构建中,所有的 npm 模块都会输出到主包中,Mpx 在编译中,还会进行分包处理,对组件和静态资源,根据用户的分包配置,串行对主包和各个分包进行构建,标记出每个组件及静态资源的归属,根据小程序资源访问策略将其输出到主包或者分包中。

所以使用 Mpx 框架开发小程序,可以享受最舒适最自然最好用的 npm 机制,详细原理介绍请移步Mpx编译构建原理

# 兼容原生小程序路径规范

组件或者页面的引入有绝对路径和相对路径,或者引入 npm 第三方包,原生小程序中,我们通过相对路径引入一个组件时

{
+  "usingComponents": {
+    "component-tag-name": "path/to/the/custom/component"
+  }
+}
+

这种路径形式在 webpack 路径规范中会被当成 npm 包路径来处理,所以要对原生小程序的路径规范做下兼容。

Mpx 提供了两种路径规范的配置模式可供大家选择,具体的使用方式为:

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+         resolveMode: 'webpack'
+      }
+    }
+  }
+})
+

在 MpxWebpackPlugin 插件中设置 resolveMode 项,默认值为 webpack,可选值有 webpack/native,推荐使用 webpack 模式,更舒服一些,配置项为 webpack 时,json 中的 pages/usingComponents 等需要写相对路径,但是也可以直接写 npm 包路径。例如:

{
+  "usingComponents": {
+    "component-tag-name": "./path/to/the/custom/component", // 内部组件路径
+    "mpx-button": "mpx-ui/src/components/button" // npm 包路径
+  }
+}
+

如果希望使用类似小程序原始那种"绝对路径",可以将 resolveMode 设置为 native,但是 npm 路径需要在前面加一个~,类似 webpack 的样式引入规范,同时必须配合 projectRoot 参数提供项目根目录地址。

resolveMode 为 native 时的使用示例:

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      plugin: {
+        resolveMode: 'native',
+        // 当resolveMode为native时可通过该字段指定项目根目录
+        projectRoot: path.resolve(__dirname, '../src')
+      }
+    }
+  }
+})
+
+// 项目page.mpx
+{
+  "usingComponents": {
+    "mpx-button": "~mpx-ui/src/components/button", // npm 包路径
+    "component-tag-name": "path/to/the/custom/component" // 内部组件路径
+  }
+}
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/pinia.html b/docs-vuepress/.vuepress/dist/guide/advance/pinia.html new file mode 100644 index 0000000000..dff4cbe5af --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/pinia.html @@ -0,0 +1,191 @@ + + + + + + 状态管理(pinia) | Mpx框架 + + + + + + + + + +

# 状态管理(pinia)

Mpx 参考 Pinia (opens new window) 设计实现了一套外部状态逻辑管理系统(pinia),允许跨页面/组件共享状态,其中的概念与 api 与 Pinia 保持一致,同时支持在 Mpx 组合式 API(Composition API)和选项式 API(Options API)模式下使用。

# 介绍

pinia 是一个状态管理容器,可支持复杂场景下的组件通信机制,与Vuex状态管理不同之处在于:

  1. mutations 不再存在,使用 actions 来支持应用中状态的同步和异步变更。

  2. 动态创建 store,使用useStore实现运行时动态创建 store。

  3. 扁平架构,不再有 modules 嵌套结构,支持不同 store 之间的交叉组合方式使用。

# 创建 pinia

首先在应用中调用createPinia方法来创建全局 pinia 实例。

// app.mpx
+import mpx from '@mpxjs/core'
+import { createPinia } from '@mpxjs/pinia'
+
+const pinia = createPinia()
+

如果你的应用想使用 SSR 渲染模式,请将 pinia 的创建放在 onAppInit 钩子中执行

// app.mpx
+
+import mpx, { createApp } from '@mpxjs/core'
+import { createPinia } from '@mpxjs/pinia'
+
+createApp({
+  // ...
+  onAppInit () {
+    const pinia = createPinia()
+    return {
+      pinia
+    }
+  }
+})
+

# 创建 store

然后调用defineStore方法,传入 store 唯一标识(id),来创建一个 store,支持 Setup 和 Options 两种风格的 store。

# Setup store

与组合式 API 的 setup 函数类似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并返回一个带有我们想暴露出去的属性和方法的对象。

// setup.js
+import { defineStore } from '@mpxjs/pinia'
+import { ref, computed } from '@mpxjs/core'
+
+export const useSetupStore = defineStore('setup', () => {
+  const count = ref(0)
+  const name = ref('pinia')
+  const myName = computed(() => {
+    return name.value
+  })
+  function increment() {
+    count.value++
+  }
+  return { count, name, myName, increment }
+})
+

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

# Options store

// options.js
+import { defineStore } from '@mpxjs/pinia'
+import { ref } from '@mpxjs/core'
+
+export const useOptionsStore = defineStore('options', {
+  state : () => {
+    return {
+      count: 0,
+      name: 'pinia'
+    }
+  },
+  getters: {
+    myName(state) {
+      return state.name
+    }
+  },
+  actions: {
+    increment () {
+      this.$patch((state) => {
+        state.count++
+      })
+    }
+  }
+})
+

# 使用 store

在选项式 API 中使用,如果你不能使用组合式 API,但你可以使用 computed, methods, '...',那你可以使用 mapState(), mapActions() 辅助函数 +来将 state, getter, action 等映射到你的组件中。

无论是 optionsStore、还是 setupStore,我们都可以在选项式组件中使用,且使用方式并无差异,下方例子我们统一使用 useSetupStore 来作为示范例

import { createComponent } from '@mpxjs/core'
+import { storeToRefs, mapState, mapActions } from '@mpxjs/pinia'
+import { useOptionsStore } from 'xxx/options'
+import { useSetupStore } from 'xxx/setup'
+
+
+// 选项式API(Options API)风格下通过mapHelper函数使用
+createComponent({
+  computed: {
+    // 可以访问组件中的 this.name
+    // 此处与直接从 useSetupStore 中读取的数据相同
+    ...mapState(useSetupStore, ['name', 'count']),
+    // 也可以将state修改别名,例如将name 注册为 otherName
+    ...mapState(useSetupStore, {
+      otherName: 'name',
+      // 也可以写一个函数来获取对 useSetupStore 的访问权
+      double: store => store.count * 2,
+      // 也可以通过访问 `this` 拿到数据
+      magicValue() {
+        return this.count + this.double
+      }
+    }),
+    // 在pinia中,getter 也是通过 mapState 来进行映射
+    ...mapState(useSetupStore, ['myName'])
+  },
+  methods: {
+    ...mapActions(useSetupStore, ['increment']),
+    // 也可以将其注册问题 this.setupIncrement 方法
+    ...mapActions(useSetupStore, {
+      setupIncrement: 'increment'
+    })
+  }
+})
+

注意:pinia 中,getter 也是通过 mapState 来进行映射

在组合式API (Setup API) 风格下使用

import { useSetupStore } from 'xxx/setup'
+import { storeToRefs, mapState, mapActions } from '@mpxjs/pinia'
+
+createComponent({
+  setup (props, context) {
+    const setupStore = useSetupStore()
+    setupStore.count = 2
+    // 作为 store 的一个属性,我们可以直接访问任何 getter(与 state )
+    setupStore.myName // pinia
+
+    function onIncrementClick() {
+      // 调用 action 方法
+      setupStore.increment()
+      console.log('New Count:', setupStore.count)
+    }
+    return {
+      onIncrementClick,
+      ...storeToRefs(setupStore)
+    }
+  }
+})
+

注意:在组合式 API(Setup API)模式下,直接解构获取到的 store 数据会失去响应性,需要通过 storeToRefs 方法处理赋予数据响应性。另外storeToRefs方法只会返回 state 或 getter。

# 使用插件

Mpx pinia 支持使用插件扩展当前 store 实例的功能,用法如下:

// onStoreAction.js,订阅所有store实例的方法
+export const onStoreAction: context => {
+  context.store.$onAction((
+    {
+      name,
+      store,
+      args,
+      after,
+      onError
+    }) => {
+      after(name => {
+        console.error('after', name, store, args)
+      })
+
+      onError(error => {
+        console.log('onError', error)
+      })
+    }, false)
+}
+
+// app.mpx
+import mpx from '@mpxjs/core'
+import { createPinia } from '@mpxjs/pinia'
+import { onStoreAction } from 'xxx/onStoreAction'
+
+const pinia = createPinia()
+pinia.use(onStoreAction)
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/platform.html b/docs-vuepress/.vuepress/dist/guide/advance/platform.html new file mode 100644 index 0000000000..b95a932b6d --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/platform.html @@ -0,0 +1,300 @@ + + + + + + 跨平台 | Mpx框架 + + + + + + + + + +

# 跨平台

# 多平台支持

Mpx支持在多个小程序平台中进行增强,目前支持的小程序平台包括微信,支付宝,百度,qq和头条,不过自2.0版本后,Mpx支持了以微信增强语法为base的跨平台输出,实现了一套业务源码在多端输出运行的能力,大大提升了多小程序平台业务的开发效率,详情可以查看跨平台编译

# template增强特性

不同平台上的模板增强指令按照平台的指令风格进行设计,文档和代码示例为了方便统一采用微信小程序下的书写方式。

模板增强指令对应表:

增强指令 微信 支付宝 百度 qq 头条
双向绑定 wx:model a:model s-model qq:model tt:model
双向绑定辅助属性 wx:model-prop a:model-prop s-model-prop qq:model-prop tt:model-prop
双向绑定辅助属性 wx:model-event a:model-event s-model-event qq:model-event tt:model-event
双向绑定辅助属性 wx:model-value-path a:model-value-path s-model-value-path qq:model-value-path tt:model-value-path
双向绑定辅助属性 wx:model-filter a:model-filter s-model-filter qq:model-filter tt:model-filter
动态样式绑定 wx:class a:class s-class qq:class 暂不支持
动态样式绑定 wx:style a:style s-style qq:style 暂不支持
获取节点/组件实例 wx:ref a:ref s-ref qq:ref tt:ref
显示/隐藏 wx:show a:show s-show qq:show tt:show

# script增强特性

增强字段 微信 支付宝 百度 qq 头条
computed 支持 支持 支持 支持 部分支持,无法作为props传递(待头条修复生命周期执行顺序可完整支持)
watch 支持 支持 支持 支持 支持
mixins 支持 支持 支持 支持 支持

# style增强特性

无平台差异

# json增强特性

增强字段 微信 支付宝 百度 qq 头条
packages 支持 支持 支持 支持 部分支持,无法分包

# 跨平台输出小程序

自2.0版本开始,mpx开始支持跨小程序平台编译,不同于常规跨平台框架重新定义一套DSL的方式,mpx支持基于现有平台的源码编译为其他已支持平台的目标代码。跨平台编译能力依赖于mpx的多平台支持,目前mpx已经支持将微信小程序跨平台编译为支付宝、百度、qq和头条小程序。

# 使用方法

如果你是使用mpx init xxx新生成的项目,package.json里script部分有npm run build:cross,直接执行npm run build:cross(watch同理),如果仅需构建某几个平台的,可以修改该script,按已有的格式删除或增添某些某些平台

如果你是自行搭建的mpx项目,你只需要进行简单的配置修改,打开项目的webpack配置,找到@mpxjs/webpack-plugin的声明位置,传入mode和srcMode参数即可,示例如下

new MpxwebpackPlugin({
+  // mode为mpx编译的目标平台,可选值有(wx|ali|swan|qq|tt|jd|web|ios|android)
+  mode: 'ali',
+  // srcMode为mpx编译的源码平台,目前仅支持wx   
+  srcMode: 'wx'
+})
+

使用 @mpxjs/cli 创建的项目,可以通过在 npm script 当中定义 targets 来设置编译的目标平台

// 项目 package.json
+{
+  "script": {
+    "build:cross": "mpx-cli-service build --targets=wx,ali"
+  }
+}
+

# 跨平台差异抹平

为了实现小程序的跨平台编译,我们在编译和运行时做了很多工作以抹平小程序开发中各个方面的跨平台差异

# 模板语法差异抹平

对于通用指令/事件处理的差异,mpx提供了统一的编译转换抹平操作;而对于平台组件和组件属性的差异,我们也在力所能及的范围内进行了转换抹平,对于平台差异性过大无法转换的部分会在编译阶段报错指出。

# 组件/页面对象差异抹平

不同平台间组件/页面对象的差异主要体现在生命周期上,我们在支持多平台能力时已经将不同平台的生命周期映射到mpx框架的一套内部生命周期中,基于这个统一的映射,不同平台的生命周期差异也得到了抹平。

此外,我们还进行了一系列运行时增强来模拟微信平台中提供而其他平台中未提供的能力,例如:

  • 在支付宝组件实例中提供了this.triggerEvent方法模拟微信中的自定义组件事件;
  • 提供了this.selectComponent/this.selectAllComponents方法模拟微信中获取子组件实例的能力;
  • 重写了createSelectorQuery方法抹平了微信/支付宝平台间的使用差异;
  • 转换抹平了微信/支付宝中properties定义的差异;
  • 利用mpx本身的数据响应能力模拟了微信中的observers/property observer能力等;
  • 提供了this.getRelationNodes方法并支持了微信中组件间关系relations的能力

对于原生小程序组件的转换,还会进行一些额外的抹平,已兼容一些已有的原生组件库,例如:

  • 将支付宝组件中的props数据挂载到this.data中以模拟微信平台中的表现;
  • 利用mpx本身的mixins能力模拟微信中的behaviors能力。

对于一些无法进行模拟的跨平台差异,会在运行时进行检测并报错指出,例如微信转支付宝时使用moved生命周期等。

# json配置差异抹平

类似于模板语法,会在编译阶段进行转换抹平,无法转换的部分会在编译阶段报错指出。

# api调用差异抹平

对于api调用,mpx提供了一个api调用代理插件来抹平跨平台api调用的差异,使用时需要在项目中安装使用@mpxjs/api-proxy,可以通过两种方式使用

# 方式一:在调用小程序api时统一使用mpx对象进行调用,示例如下:

安装插件支持options传入,options说明如下:

参数名称 类型 含义 是否必填 默认值 备注
platform Object 各平台之间的转换 { from:'', to:'' } 已删除
usePromise Boolean 是否将 api 转化为 promise 格式使用 - - -
exclude Array(String) 跨平台时不需要转换的 api - - 已删除
whiteList Array(String) 强行转化为 promise 格式的 api [] 需要 usePromise 设为 true
blackList Array(String) 不转换 promise 格式的 api [] 需要 usePromise 设为 true
custom Object 提供用户在各渠道下自定义api开放能力 [] -

# 普通形式

import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+
+mpx.use(apiProxy)
+
+mpx.showModal({
+  title: '标题',
+  content: '这是一个弹窗',
+  success (res) {
+    if (res.cancel) {
+      console.log('用户点击取消')
+    }
+  }
+})
+

# 使用promise形式

import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+
+mpx.use(apiProxy, {
+  usePromise: true
+})
+
+mpx.showActionSheet({
+  itemList: ['A', 'B', 'C']
+})
+.then(res => {
+  console.log(res.tapIndex)
+})
+.catch(err => {
+  console.log(err)
+})
+

# 用户自定义

import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+import { scanCode } from '@test/scanCode'
+
+mpx.use(apiProxy, {
+  custom: {
+    web: {
+      scanCode
+    }
+  }
+})
+// 在web下调用的实际是用户自定义部分的scanCode
+mpx.scanCode({
+  onlyFromCamera: true,
+  success (res) {
+    console.log(res, 'scanCode, success')
+  },
+  fail (res) {
+    console.log(res, 'scanCode, fail')
+  }
+})
+

# 方式二:直接在@mpxjs/api-proxy导出想使用的方法

// 独立使用 支持treesharking能力
+import { showModal } from '@mpxjs/api-proxy'
+
+showModal({
+  title: '标题',
+  content: '这是一个弹窗',
+  success (res) {
+    console.log('弹框展示成功')
+  }
+})
+

对于无法转换抹平的api调用会在运行时阶段报错指出。

# webview bridge差异抹平

Mpx支持小程序跨平台后,多个平台的小程序里都有webview组件,webview打开的页面和小程序可以通过API来通信以及调用一些小程序能力,但是各方webview提供的API是不一样的。

比如微信是用 wx.miniProgram.navigateTo 来跳转到别的小程序页面,而支付宝里是 my.navigateTo ,那么我们开发H5时候为了让H5能适应各家小程序平台就需要写多份对应逻辑。

为解决这个问题,Mpx提供了用于运行在小程序的webview里的H5抹平平台差异的bridge库:@mpxjs/webview-bridge

使用方式很简单,不过注意这个库是给H5用的,不是给小程序用的。在H5项目中引入。

使用示例 (opens new window)

支持script标签引入和npm引入,标签引入的话,全局实例是mpx(npm模块使用下也鼓励import mpx from '@mpxjs/webview-birdge'),使用就例如 mpx.navigateTo ,能保持整个项目风格完全一致。

提供的API如下:navigateTo, navigateBack, switchTab, reLaunch, redirectTo, getEnv, postMessage, getLoadError

# 跨平台条件编译

Mpx跨平台编译的原则在于,能转则转,转不了则报错提示,对于无法抹平差异的部分,我们提供了完善的跨平台条件编译机制便于用户处理因平台差异而无法相互转换的部分,也能够用于实现具有平台差异性的业务逻辑。

mpx中我们支持了三种维度的条件编译,分别是文件维度,区块维度和代码维度,其中,文件维度和区块维度主要用于处理一些大块的平台差异性逻辑,而代码维度主要用于处理一些局部简单的平台差异。

# 文件维度条件编译

文件维度条件编译简单的来说就是文件为维度进行跨平台差异代码的编写,例如在微信->支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,这时你可以在相同的位置新建一个map.ali.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的mode来加载对应模块,当mode为ali时,会优先加载map.ali.mpx,反之则会加载map.mpx。

文件维度条件编译能够与webpack alias结合使用,对于npm包的文件我们并不方便在原本的文件位置创建.ali的条件编译文件,但我们可以通过webpack alias在相同位置创建一个虚拟的.ali文件,并将其指向项目中的其他文件位置。

  // 对于npm包中的文件依赖
+  import npmModule from 'somePackage/lib/index'
+
+  // 配置以下alias后,当mode为ali时,会优先加载项目目录中定义的projectRoot/somePackage/lib/index文件
+  // vue.config.js
+  module.exports = defineConfig({
+    configureWebpack() {
+      return {
+        resolve: {
+          alias: {
+            'somePackage/lib/index.ali': 'projectRoot/somePackage/lib/index'
+          }
+        }
+      }
+    }
+  })
+

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加mode属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali时使用如下区块-->
+<template mode="ali">
+<!--该区块中的所有代码需采用支付宝的技术标准进行编写-->
+  <view>支付宝环境</view>
+</template>
+
+<!--其他编译mode时使用如下区块-->
+<template>
+  <view>其他环境</view>
+</template>
+

# 代码维度条件编译

如果只有局部的代码存在跨平台差异,mpx同样支持在代码内使用if/else进行局部条件编译,用户可以在js代码和template插值中访问__mpx_mode__获取当前编译mode,进行平台差异逻辑编写,js代码中使用示例如下。

除了 __mpx_mode__ 这个默认插值以外,有别的环境变量需要的话可以在mpx.plugin.conf.js里通过defs进行配置。

if(__mpx_mode__ === 'ali') {
+  // 执行支付宝环境相关逻辑
+} else {
+  // 执行其他环境相关逻辑
+}
+

template代码中使用示例如下

<!--此处的__mpx_mode__不需要在组件中声明数据,编译时会基于当前编译mode进行替换-->
+<view wx:if="{{__mpx_mode__ === 'ali'}}">支付宝环境</view>
+<view wx:else>其他环境</view>
+

JSON中的条件编译(注意,这个依赖JSON的动态方案,得通过name="json"这种方式来编写,其实写的是js代码,最终module.exports导出一个可json化的对象即可):

<script name="json">
+const pages = __mpx_mode__ === 'wx' ? [
+  'main/xxx',
+  'sub/xxx'
+] : [
+  'test/xxx'
+] // 可以为不同环境动态书写配置
+module.exports = {
+  usingComponents: {
+    aComponents: '../xxxxx' // 可以打注释 xxx组件
+  }
+}
+</script>
+

样式的条件编译:

/*
+  @mpx-if (__mpx_env__ === 'someEvn')
+*/
+  /* @mpx-if (__mpx_mode__ === 'wx') */
+  .backColor {
+    background: green;
+  }
+  /*
+    @mpx-elif (__mpx_mode__ === 'qq')
+  */
+  .backColor {
+    background: black;
+  }
+  /* @mpx-endif */
+
+  /* @mpx-if (__mpx_mode__ === 'swan') */
+  .backColor {
+    background: cyan;
+  }
+  /* @mpx-endif */
+  .textSize {
+    font-size: 18px;
+  }
+/*
+  @mpx-else
+*/
+.backColor {
+  /* @mpx-if (__mpx_mode__ === 'swan') */
+  background: blue;
+  /* @mpx-else */
+  background: red;
+  /* @mpx-endif */
+}
+/*
+  @mpx-endif
+*/
+

# 属性维度条件编译

属性维度条件编译允许用户在组件上使用 @| 符号来指定某个节点或属性只在某些平台下有效。

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

比如业务中需要通过 button 按钮获取用户信息,虽然可以使用代码维度条件编译来解决,但是增加了很多代码量:

<button
+  wx:if="{{__mpx_mode__ === 'wx' || __mpx_mode__ === 'swan'}}"
+  open-type="getUserInfo"
+  bindgetuserinfo="getUserInfo">
+  获取用户信息
+</button>
+
+<button
+  wx:elif="{{__mpx_mode__ === 'ali'}}"
+  open-type="getAuthorize"
+  scope="userInfo"
+  onTap="onTap">
+  获取用户信息
+</button>
+

而用属性维度的编译则方便很多:

<button
+  open-type@wx|swan="getUserInfo"
+  bindgetuserinfo@wx|swan="getUserInfo"
+  open-type@ali="getAuthorize"
+  scope@ali="userInfo"
+  onTap@ali="onTap">
+  获取用户信息
+</button>
+

属性维度的编译也可以对整个节点进行条件编译,例如只想在支付宝小程序中输出某个节点:

<view @ali>this is view</view>
+

需要注意使用上述用法时,节点自身在构建时框架不会对节点属性进行平台语法转换,但对于其子节点,框架并不会继承父级节点 mode,会进行正常跨平台语法转换。

<!--错误示例-->
+<view @ali bindtap="otherClick">
+    <view bindtap="someClick">tap click</view>
+</view>
+// srcMode 为 wx 跨端输出 ali 结果为
+<view @ali bindtap="otherClick">
+    <view onTap="someClick">tap click</view>
+</view>
+

上述示例为错误写法,假如srcMode为微信小程序,用上述写法构建输出支付宝小程序时,父节点 bindtap 不会被转为 onTap,在支付宝平台执行时事件会无响应。

正确写法如下:

<!--正确示例-->
+<view @ali onTap="otherClick">
+    <view bindtap="someClick">tap click</view>
+</view>
+// 输出 ali 产物
+<view @ali onTap="otherClick">
+    <view onTap="someClick">tap click</view>
+</view>
+

有时开发者期望使用 @ali 这种方式仅控制节点的展示,保留节点属性的平台转换能力,为此 Mpx 实现了一个隐式属性条件编译能力

<!--srcMode为 wx,输出 ali 时,bindtap 会被正常转换为 onTap-->
+<view @_ali bindtap="someClick">test</view>
+

在对应的平台前加一个_,例如@_ali、@_swan、@_tt等,使用该隐式规则仅有条件编译能力,节点属性语法转换能力依旧。

有时候我们不仅需要对节点属性进行条件编译,可能还需要对节点标签进行条件编译。

为此,我们支持了一个特殊属性 mpxTagName,如果节点存在这个属性,我们会在最终输出时将节点标签修改为该属性的值,配合属性维度条件编译,即可实现对节点标签进行条件编译,例如在百度环境下希望将某个 view 标签替换为 cover-view,我们可以这样写:

<view mpxTagName@swan="cover-view">will be cover-view in swan</view>
+

# 通过 env 实现自定义目标环境的条件编译

Mpx 支持在以上四种条件编译的基础上,通过自定义 env 的形式实现在不同环境下编译产出不同的代码。

实例化 MpxWebpackPlugin 的时候,传入配置 env。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      srcMode: 'wx' // srcMode为mpx编译的源码平台,目前仅支持wx
+      plugin: {
+        env: "didi" // env为mpx编译的目标环境,需自定义
+      }
+    }
+  }
+})
+

# 文件维度条件编译

微信转支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,而且这个地图组件在不同的目标环境中也有很大的差异,这时你可以在相同的位置新建一个 map.ali.didi.mpx 或 map.ali.qingju.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的 mode 和 env 来加载对应模块,当 mode 为 ali,env 为 didi 时,会优先加载 map.ali.didi.mpx、map.ali.mpx,如果没有定义 env,则会优先加载 map.ali.mpx,反之则会加载 map.mpx。

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加modeenv属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali且env为didi时使用如下区块,优先级最高是4-->
+<template mode="ali" env="didi">
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+
+<!--编译mode为ali时使用如下区块,优先级是3-->
+<template mode="ali">
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+
+<!--编译env为didi时使用如下区块,优先级是2-->
+<template env="didi">
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+
+<!--其他环境,优先级是1-->
+<template>
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+

注意,如果多个相同的区块写相同的 mode 和 env,默认会用最后一个,如:

<template mode="ali">
+  <view>该区块会被忽略</view>
+</template>
+
+<template mode="ali">
+  <view>默认会用这个区块</view>
+</template>
+

# 代码维度条件编译

如果在 MpxWebpackPlugin 插件初始化时自定义了 env,你可以访问__mpx_env__获取当前编译env,进行环境差异逻辑编写。使用方法与__mpx_mode__相同。

# 属性维度条件编译

env 属性维度条件编译与 mode 的用法大致相同,使用 : 符号与 mode 和其他 env 进行串联,与 mode 组合使用格式形如 attr@mode:env:env|mode:env,为了不与 mode 混淆,当条件编译中仅存在 env 条件时,也需要添加 : 前缀,形如 attr@:env

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

如果当前编译的目标平台是 wx,以下写法 open-type 属性将被忽略

<button open-type@swan:didi="getUserInfo">获取用户信息</button>
+

如果当前 env 不是 didi,以下写法 open-type 属性也会被忽略

<button open-type@:didi="getUserInfo">获取用户信息</button>
+

如果只想在 mode 为 wx 且 env 为 didi 或 qingju 的环境下使用 open-type 属性,则可以这样写:

<button open-type@wx:didi:qingju="getUserInfo">获取用户信息</button>
+

env 属性维度的编译同样支持对整个节点或者节点标签名进行条件编译:

<view @:didi>this is a  view component</view>
+<view mpxTagName@:didi="cover-view">this is a  view component</view>
+

如果只声明了 env,没有声明 mode,跨平台输出时框架对于节点属性默认会进行转换:

<!--srcMode为wx,跨平台输出ali时,bindtap会被转为onTap-->
+<view @:didi bindtap="someClick">this is a  view component</view>
+<view bindtap@:didi ="someClick">this is a  view component</view>
+

# 其他注意事项

  • 当目标平台为支付宝时,需要启用支付宝最新的component2编译才能保障框架正常工作,关于component2点此查看详情 (opens new window)
  • 跨平台源码中自定义组件的标签名不能使用驼峰形式myComponent,请使用横杠形式my-component来书写;
  • 生成的目标代码中文件名和文件夹名不能带有@符号,目前媒体文件和原生自定义组件在编译时不会修改文件名,需要重点关注。

# 跨平台输出web

从2.3.0版本开始,Mpx开始支持将已有项目跨平台输出web平台中运行的能力,目前输出web能力完备,能够支持直接转换大型复杂项目,我们会持续对输出web的能力进行优化,不断的建全更全面的适用范围和开发体验。

# 技术实现

与小程序平台间的差异相比,web平台和小程序平台间的差异要大很多,小程序相当于是基于web技术的上层封装,所以不同于我们跨平台输出其他小程序平台时以编译转换为主的思路,在输出web时,我们更多地采用了封装的方式来抹平组件/Api和底层环境的差异,与当前大部分的跨平台框架相仿。但与当前大部分跨平台框架以web MVVM框架为base输出到小程序上运行的思路不同,我们是以Mpx小程序增强语法为基础输出到web中运行,前面说过小程序本身是基于web技术进行的实现,小程序->web的转换在可行性和兼容性上会好一些。

在具体实现上,Mpx项目输出到web中运行时在组件化和路由层面都是基于Vue生态实现,所以可以将Mpx的跨端输出产物整合到既有的Vue项目中,或者在条件编译中直接使用Vue语法进行web端的实现。

# 使用方法

使用@mpxjs/cli创建新项目时选择跨平台并选择输出web后,即可生成可输出web的示例项目,运行npm run build:web,就会在dist/web下输出构建后的web项目,并启动静态服务预览运行。

# 支持范围

目前对输出web的通用能力支持已经非常完备,下列表格中显示了当前版本中已支持的能力范围

# 模板指令

指令名称 是否支持
Mustache数据绑定
wx:for
wx:for-item
wx:for-index
wx:key
wx:if
wx:elif
wx:else
wx:model
wx:model-prop
wx:model-event
wx:model-value-path
wx:model-filter
wx:class
wx:style
wx:ref
wx:show

# 事件绑定方式

绑定方式 是否支持
bind
catch
capture-bind
capture-catch

# 事件名称

事件名称 是否支持
tap
longpress
longtap

web同名事件默认全部支持,已支持组件的特殊事件默认为支持,不支持的情况下会在编译时抛出异常

# 基础组件

组件名称 是否支持 说明
audio
block
button
canvas
checkbox
checkbox-group
cover-view
form
image
input
movable-area
movable-view
navigator
picker
picker-view
progress
radio
radio-group
rich-text
scroll-view scroll-view 输出 web 底层滚动依赖 BetterScroll (opens new window) 实现,支持额外传入以下属性:

scroll-options: object
可重写 BetterScroll 初始化基本配置
若出现无法滚动,可尝试手动传入 { observeDOM: true }

update-refresh: boolean
Vue updated 钩子函数触发时,可用于重新计算 BetterScroll

tips: 当使用下拉刷新相关属性时,由于 Vue 数据响应机制的限制,在 web 侧可能出现下拉组件状态无法复原的问题,可尝试在 refresherrefresh 事件中,手动将 refresher-triggered 属性值设置为 true
swiper swiper 输出 web 底层滚动依赖 BetterScroll (opens new window) 实现,支持额外传入以下属性:

scroll-options: object
可重写 BetterScroll 初始化基本配置
当滑动方向为横向滚动,希望在另一方向保留原生的滚动时,scroll-options 可尝试传入 { eventPassthrough: vertical },反之可将 eventPassthrough 设置为 horizontal
swiper-item
switch
slider
text
textarea
video
view
web-view

在项目的app.json 中配置 "style": "v2"启用新版的组件样式,涉及的组件有 button icon radio checkbox switch slider在输出web时也与小程序保持了一致

# 生命周期

生命周期名称 是否支持
onLaunch
onLoad
onReady
onShow
onHide
onUnload
onError

onServerPrefetch|是 +created|是 +attached|是 +ready|是 +detached|是 +updated|是 +serverPrefetch|是

# 应用级事件

应用级事件名称 是否支持
onPageNotFound
onPageScroll
onPullDownRefresh
onReachBottom
onResize
onTabItemTap

# 组件配置

配置项 支持度
properties 部分支持,observer不支持,请使用watch代替
data 支持
watch 支持
computed 支持
relations 支持
methods 支持
mixins 支持
pageLifetimes 支持
observers 不支持,请使用watch代替
behaviors 不支持,请使用mixins代替

# 组件API

api名称 支持度
triggerEvent 支持
$nextTick 支持
createSelectorQuery/selectComponent 支持

# 全局API

api名称 支持度
navigateTo 支持
navigateBack 支持
redirectTo 支持
request 支持
connectSocket 支持
SocketTask 支持
EventChannel 支持
createSelectorQuery 支持
base64ToArrayBuffer 支持
arrayBufferToBase64 支持
nextTick 支持
set 支持
setNavigationBarTitle 支持
setNavigationBarColor 支持
setStorage 支持
setStorageSync 支持
getStorage 支持
getStorageSync 支持
getStorageInfo 支持
getStorageInfoSync 支持
removeStorage 支持
removeStorageSync 支持
clearStorage 支持
clearStorageSync 支持
getSystemInfo 支持
getSystemInfoSync 支持
showModal 支持
showToast 支持
hideToast 支持
showLoading 支持
hideLoading 支持
onWindowResize 支持
offWindowResize 支持
createAnimation 支持

# JSON配置

配置项 是否支持
backgroundColor
backgroundTextStyle
disableScroll
enablePullDownRefresh
onReachBottomDistance
packages
pages
navigationBarBackgroundColor
navigationBarTextStyle
navigationBarTitleText
networkTimeout
subpackages
tabBar
usingComponents

# 拓展能力

能力 是否支持
fetch
i18n

# 小程序其他原生能力

能力 支持度
wxs 支持
animation 支持组件的animation属性,支持所有animation对象方法(export、step、width、height、rotate、scale、skew、translate等等)
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/plugin.html b/docs-vuepress/.vuepress/dist/guide/advance/plugin.html new file mode 100644 index 0000000000..19aa35df50 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/plugin.html @@ -0,0 +1,77 @@ + + + + + + 小程序插件 | Mpx框架 + + + + + + + + + +

# 小程序插件

插件,是可被添加到小程序内直接使用的功能组件,是对一组 js 接口、自定义组件或页面的封装,。开发者可以像开发小程序一样开发一个插件,供其他小程序使用。同时,小程序开发者可直接在小程序内使用插件,无需重复开发,但是在使用第三那个插件时,无法看到插件的代码。插件适合用来封装自己的功能或服务,提供给第三方小程序进行展示和使用。

开发小程序插件,大致要经过 开通插件功能,填写开发信息,提交审,发布,管理插件使用申请。同时在原生小程序使用插件,要先发出插件申请,等待使用申请通过,插件所有者还可以进行拒绝。 +原生小程序开发插件请移步:

新建插件类型的项目后,如果创建示例项目,则项目中将包含三个目录:

  • plugin 目录:插件代码目录。
  • miniprogram 目录:放置一个小程序,用于调试插件。
  • doc 目录:用于放置插件开发文档。

,插件会同时有多个线上版本,由使用插件的小程序决定具体使用的版本号。

# 如何编写一个插件

推荐使用 mpx 官方脚手架 @mpxjs/cli 创建一个小程序插件项目来快速的进入插件开发阶段,首先全局安装 @mpxjs/cli

npm i -g @mpxjs/cli
+

然后使用 cli 初始化项目

mpx create <project-name>
+? 请选择小程序项目所属平台(目前仅微信下支持跨平台输出) wx
+? 是否需要跨小程序平台 No
+? 是否需要使用小程序云开发能力 No
+? 是否是个插件项目?(不清楚请选 No !什么是插件项目请看微信官方文档!) Yes
+? 是否需要typescript No
+? 项目描述 A mpx project
+? 请输入小程序的Appid touristappid
+

文件目录

  src
+  |-- miniprogram // 目录:放置一个小程序,用于调试插件。
+  |   --pages
+  |   --app.mpx // 引入插件调试
+  |-- plugin // 目录:插件代码目录
+  |   --components // 插件组件
+  |     -- list.mpx // 插件提供的列表组件
+  |   --plugin.json // 插件配置文件
+

我们在 plugin/components/list.mpx 中开发插件中的列表组件,开发完成后,在plugin.json中我们向使用者小程序开放的所有自定义组件、页面和 js 接口,格式如下:

代码示例:

{
+  "publicComponents": {
+    "list": "./components/list" // 使用mpx 中的webpack 路径引入规范
+  },
+  "pages": {
+    "hello-page": "./pages/hello-page"
+  }
+}
+

运行 npm run build/serve 构建小程序产物,在 dist 文件夹下,生成最终的小程序插件产物,使用微信开发者工具,打开代码片段菜单栏,选择插件模式,打开 dist 文件夹。

我们可以像小程序一样预览和上传,但插件没有体验版,同时我们通常将 miniprogram 下的代码当做使用插件的小程序代码,来进行插件的调试和测试。

在开发完插件之后,我们可以上传插件代码,在小程序管理后台进行提交发布审核,审核通过后,就可以提供给第三方小程序使用我们的插件了。

使用 mpx 开发插件的优势相似于使用 mpx 开发小程序项目,可以使用 mpx 的各种增强特性以及跨平台输出的特性,提高开发效率和插件性能。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/progressive.html b/docs-vuepress/.vuepress/dist/guide/advance/progressive.html new file mode 100644 index 0000000000..7dc46cdf63 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/progressive.html @@ -0,0 +1,135 @@ + + + + + + 原生渐进迁移 | Mpx框架 + + + + + + + + + +

# 原生渐进迁移

已有项目 期望接入 Mpx,可根据项目或人力情况选择如何迁移,Mpx 不要求用户一次性用上框架的所有东西。

  1. 项目初始期:可以考虑一次性转为 Mpx,此时迁移成本比较低
  2. 项目成熟期:若人力有限,可选择逐步将原生小程序转为Mpx,而且 不需要对原有代码做全局重写。可参考此demo:Mpx渐进接入demo (opens new window)。 +
    • 可以保持原有代码不变,新的组件、页面期望使用 Mpx 某些特性时才引入 Mpx。(推荐新模块引 Mpx,老模块逐步迁移 Mpx)
    • 用 Mpx 编写新的页面、组件,再局部导出对应的页面、组件,反向应用到现有的原生小程序项目中。见导出原生一节。(建议优先考虑老项目渐进改为 Mpx,而不是反向 Mpx 输出原生小程序的模式)

# 原生接入

有些时候,我们需要在Mpx工程中使用原生小程序组件:

  • 通过npm引用安装第三包
  • 将第三方包源码拷贝到本地src目录下

注:Mpx并不限制第三方包的格式。开发者可以自己参考小程序官方的开发第三方自定义组件 (opens new window)

# 原理

根据unsingComponents中设定的路径,Mpx会去查找包入口的js文件。然后提取入口文件所在的目录中的js json wxss wxml进行编译

编译带来的好处是,常规的拷贝操作,会造成组件内部的依赖缺失,以及冗余代码被打包。而执行了编译,使得Mpx可以精确的收集依赖,这表现在:

  • js文件中的依赖也会被打包,没有被加载的依赖库不会打包,减小体积
  • json文件的usingComponents会被解析,因此原生组件内部可以再引用其他原生组件,甚至是mpx组件
  • wxss中引用外部样式
  • wxml中的图片资源会被打包

例如:使用第三方组件库时,很多组件可能并未使用,如果按照官方给出的组件库使用方式,会将整个组件库放进项目。
+而采用Mpx这种方式则只会引入使用了的组件,所以如果你喜欢vant的按钮,iview的输入框,ColorUI的布局,欢迎尝试mpx。
+(本段内容具有时效性,未来微信可能会有优化,毕竟一开始微信连npm都不支持)

# 例子

文件目录

node_modules
+|-- npm-a-wx-component // npm安装
+|   --package.json
+|   --src
+|     --index.js
+|     --index.json
+|     --index.wxss
+|     --index.wxml
+|-- npm-b-wx-component // npm安装
+|   --package.json
+|   --src
+|     --index.js
+|     --index.json
+|     --index.wxss
+|     --index.wxml
+component
+│-- container.mpx 
+│-- com-a.mpx 
+|-- src-wx-component // 手动拷贝
+|  --index.js
+|  --index.json
+|  --index.wxss
+|  --index.wxml
+
+

container.mpx

<template>
+  <view>
+    <!-- mpx组件 -->
+    <com-a></com-a>
+    <!-- npm安装的原生组件 -->
+    <npm-a-wx-component></npm-a-wx-component>
+    <!-- 手动拷贝到工程的原生组件 -->
+    <src-wx-component></src-wx-component>
+  </view>
+</template>
+
+<script type="application/json">
+  "usingComponents": {
+    "com-a": "./com-a",
+    "npm-a-wx-component": "npm-a-wx-component",
+    "src-wx-component": "./src-wx-component"
+  }
+</script>
+

node_modules/npm-a-wx-component/src/index.wxml

<template>
+  <view>
+    <view>this is a native component</view>
+    <!-- 原生组件内部使用原生组件 -->
+    <npm-b-wx-component></npm-b-wx-component>
+  </view>
+</template>
+

node_modules/npm-a-wx-component/src/index.json

{
+  "usingComponents": {
+    "npm-b-wx-component": "npm-b-wx-component"
+  }
+}
+

# 原生page支持

原生自定义组件的支持已经基本能保证第三方 UI 库和 Mpx 的完美结合,但如果是用户存在已经开发好的小程序,在后续的迭代中发现了 Mpx 想使用,就需要用户手工将4个文件变成 Mpx 文件,这不够友好。

Mpx 提供了对原生页面的支持,允许项目中存在原生小程序文件(wxml,js,json,wxss)和 Mpx 文件,两者可以混合使用,通过 webpack 的构建将两者完美混合在一起生成最终的 dist。

使用方式和组件相似。

# 原生导出

通过导出原生能力,你可以将一个 Mpx 项目融回到原生小程序项目中。有两种做法:

  • 一是局部导出部分页面、组件
  • 二是完整导出一个 Mpx 项目。

# 导出部分页面/组件

使用 Mpx 开发的页面/组件也可以局部导出为纯粹的普通的原生小程序页面/组件,整合到已有的原生小程序中。

  1. 修改 webpack config 中 entry 一项,将 app 改为对应的页面/组件即可。

在路径后追加 ?isPage 来声明独立页面构建,构建产物为该页面的独立原生代码,在路径后追加 ?isComponent 来声明独立组件构建,构建产物为该组件的独立原生代码。

请参考下面的例子,注意resolve时候最后的query不可以省略,一定要按正确的类型声明这是一个组件or页面。

例子:

// 
+module.exports = merge(baseWebpackConfig, {
+  name: 'main-compile',
+  // entry point of our application
+  entry: {
+    // 此处以mpx脚手架生成的项目为例
+    
+    // before
+    // app: resolveSrc('app.mpx')
+    
+    // after,这里"pages/dindex"代表将原页面导出到output目录下的pages目录,文件名改为dindex.*
+    'pages/dindex': resolveSrc('./pages/index.mpx?isPage'), // ?后标识导出类型
+    'components/dlist': resolveSrc('./components/list.mpx?isComponent')
+  }
+})
+
  1. 执行 webpack 打包命令
  2. 拷贝打包后 dist 里所有文件到原生微信小程序项目根目录即可正常工作。

# 完整导出

举例:假如我们使用 Mpx 开发了一个完整的项目,这个项目可能包含多个页面,这些页面组合完成一个完整的功能。一般可能是公共需求,比如登录/用户中心等公共模块

如果其它接入方想复用这一公共模块,考虑有以下两种情形

  • 接入方也是 Mpx 框架开发的项目,直接迁移
  • 接入方是原生开发,这时我们希望能将整个项目完整导出成原生,并让接入方顺利使用。

其实观察下 Mpx 项目的打包结构,结构是非常简单的,页面/组件都很规矩地放在对应文件夹里的,所以删掉app.json/app.js/app.wxss/project.config.json几个文件后直接整个拷贝即可。

完整导出整个项目的做法可以是这样:

  1. 确认页面路径不要冲突,一般这种公共模块项目,路径上就不要占据/pages/index/index,页面路径Mpx是不会修改的,所以定一个/pages/{模块名}/{页面名}就好。

  2. app.*的内容都要删掉的,所以全局样式都应该写在独立的文件中(wxss),全局配置有什么特殊的要告知接入方(json),因为App.js会被舍弃,所以入口js要抽出来(js)。

  3. 如果有要导出的入口文件,需要给output增加配置:

// webpack.conf
+module.exports = {
+  entry: {
+    app: resolveSrc('app.mpx'),
+    index: resolveSrc('index.js') // 导出的入口文件,若没有可不写
+  },
+  output: {
+    libraryTarget: 'commonjs2',
+    libraryExport: 'default' // 若export default导出需要写这个,module.exports可省略
+  },
+  // ... 略
+}
+
  1. 整个复制进接入方的项目里,注册对应的页面,然后就可以正常使用了!
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html b/docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html new file mode 100644 index 0000000000..b6d04c03a9 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html @@ -0,0 +1,200 @@ + + + + + + 依赖注入(Provide/Inject) | Mpx框架 + + + + + + + + + +

# 依赖注入(Provide/Inject)

# 适用场景

通常情况下,从父组件向子组件传递数据时,我们会使用 props 向下传递。如果在一颗组件层级嵌套很深的组件树中,某个深层的子组件依赖一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链路逐级传递下去,这就是 prop 逐级透传(prop-drilling) 问题。

使用 provide 和 inject 可以帮助我们解决这一麻烦问题:一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

组件间 provide-inject 示意图

# Provide 提供

# 组合式语法

在组合式语法中,需要使用到 provide() 函数,provide() 函数接收两个必填参数:

  1. 第一个参数表示注入名,是一个字符串或者 Symbol。后代组件会用注入名来查找期望注入的值,一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。使用 Symbol 作为注入名时,只能使用组合式语法而非选项式语法。
  2. 第二个参数表示提供值,值可以是任意类型,包括响应式的状态,比如一个 refreactive 或者 computed 计算值。
<script setup>
+import { computed, provide, ref } from '@mpxjs/core'
+
+// 静态数据
+provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
+
+// 使用 Symbol 作为注入名
+const key = Symbol()
+provide(key, 'hello!')
+
+// 响应式数据
+const count = ref(1)
+const double = computed(() => count.value * 2)
+provide('count', count)
+provide('double', double)
+
+</script>
+

# 选项式语法

选项式语法的 setup() 中,用法和组合式 API 一致。另外针对选项式语法,我们也提供了 provide 选项,它是一个对象或返回一个对象的函数

对于 provide 对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。

如果我们需要提供依赖当前组件实例的状态 (比如在 data 属性中定义的数据),那么需要使用函数形式的 provide 选项。需要注意的是,这 并不会 使注入保持响应性,如果需要保证注入方和供给方之间的响应性链接,我们需要使用 computed() 函数提供一个计算属性。

















 





<script>
+import { createComponent, computed } from '@mpxjs/core'
+
+createComponent({
+  data: {
+    count: 1,
+  },
+  // 1. 静态数据可以选择直接使用对象形式
+  provide: {
+    message: 'hello'
+  },
+  // 2. 选择使用函数的形式,可以访问到 `this`
+  provide() {
+    return {
+      count: this.count
+      // 显式提供一个计算属性
+      message: computed(() => this.count * 2)
+    }
+  }
+})
+</script>
+

# 应用级顶层 provide

前面介绍的是在一个组件中提供依赖,我们还可以在整个应用层面提供依赖,这样整个应用中所有组件都可以使用。

import { createApp } from '@mpxjs/core'
+
+createApp({
+  // 应用层 provide
+  provide() {
+    return {
+      'appMessage': 'provide from App scope!'
+    }
+  }
+})
+

# Inject 注入

# 组合式语法

inject() 函数最多接收三个参数:

  • 第一个参数必填,表示注入名,字符串类型。
  • 第二个参数可选,表示默认值。 +
    • 如果在注入一个值时不确定是否有提供者,那么应该声明一个默认值。如果既没有提供者也没有默认值,则会抛出一个运行时警告。
  • 第三个参数可选,表示是否将默认值视为工厂函数,布尔类型。 +
    • 某些场景下,默认值可能需要通过调用一个函数或初始化一个类来生成,或者某些非基础类型数据创建开销比较大,请使用工厂函数来创建默认值。
<script setup>
+import { inject } from '@mpxjs/core'
+
+// 注入 message 依赖
+const value = inject('message')
+// 创建默认值
+const value = inject('message', 'default value')
+// 使用工厂函数创建默认值
+const value = inject('key', () => new ExpensiveClass(), true)
+</script>
+

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方能够通过 ref 对象和提供方保持响应性链接。

# 选项式语法

选项式语法的 setup() 中,用法和组合式 API 一致。另外针对选项式语法,我们也提供了 inject 选项,它是一个数组或一个对象

  1. 数组形式使用。数组中的每一项对应一个注入名,注入的属性会以同名的 key 暴露到组件实例上。如下示例中,注入名 "message" 在注入后以 this.message 的形式暴露。另外,inject 会在组件自身的状态之前被解析,意味着你可以在 data() 中访问到注入的属性。





 










<script>
+import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  // 数组形式使用 `inject` 选项
+  inject: ['message', 'count'],
+  data() {
+    return {
+      // 可以在 `data()` 中访问到注入的属性
+      fullMessage: this.message,
+      fullCount: this.count
+    }
+  }
+})
+</script>
+
  1. 对象形式使用。相比数组形式,对象形式支持注入别名默认值。如下示例,通过 from 属性指定原注入名,通过 default 属性指定默认值。
<script>
+import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  inject: {
+    myCount: 'count', // 按注入名 'count' 注入
+    message: {
+      from: 'message', // 'message' 是原注入名,当与原注入名同名时,这个属性是可选的
+      default: 'default value' // 默认值
+    },
+    user: { // 与原注入名 'user' 同名
+      default: () => ({ name: 'Wang' }) // 使用工厂函数创建默认值
+    }
+  }
+})
+</script>
+

# 避免注入名潜在冲突

如果你正在构建大型的应用,包含非常多的依赖提供,那么随处定义的注入名容易存在潜在的同名冲突。在 Mpx 实现中,同名的注入名会覆盖之前已有的注入名对应的提供值。

针对大型应用的最佳实践,我们通常推荐在一个单独的文件中导出这些注入名,可以结合业务模块功能来统一注入名的命名和管理规范。以下示例仅供参考,具体实现请依据自身实际场景确定。

/** 推荐创建一个单独的文件来管理和导出注入名 */
+
+// 1. 使用枚举类型
+export const InjectionKeys = {
+  user: 'user',
+  auth: 'auth',
+  product: 'product',
+}
+
+// 2. 按业务模块功能划分命名,不容易命名冲突,可读性高
+export const InjectionKeys = {
+  user: {
+    service: 'user:service',
+    store: 'user:store',
+  },
+  auth: {
+    service: 'auth:service',
+    token: 'auth:token',
+  },
+  product: {
+    list: 'product:list',
+    details: 'product:details',
+  },
+}
+
+// 3. 使用 Symbol 创建唯一注入名,或再结合命名空间
+export const InjectionKeys = {
+  user: Symbol('user'),
+  auth: Symbol('auth'),
+  product: Symbol('product'),
+}
+
+// 4. 自行实现类似 Symbol polyfill 的命名生成函数
+export const createInjectionKey = (module, key) => `${module}:${key}`
+export const InjectionKeys = {
+  user: createInjectionKey('user', 'service'),
+  auth: createInjectionKey('auth', 'token'),
+  product: createInjectionKey('product', 'list'),
+}
+

# TS 类型支持

直接使用字符串注入 key 时,注入值的类型默认推导会是 unknown,需要通过泛型参数显式声明。因为无法保证运行时一定存在这个 provide,所以推导类型也可能是 undefined。当声明一个默认值后,这个 undefined 类型就可以成功被移除。

<script setup lang="ts">
+import { inject } from '@mpxjs/core'
+
+const foo = inject('foo') // 类型:unknown
+const foo = inject<string>('foo') // 类型:string | undefined
+const foo = inject<string>('foo', 'default value') // 类型:string ✅
+</script>
+

当然,如果你已经确定注入名肯定被提供了,也可以强制断言。

const foo = inject('foo') as string
+

如果使用 Symbol 作为注入名,可以使用我们提供的 InjectionKey 泛型接口,使用它对注入名进行注解或者断言后,可以用来在不同组件之间同步注入值的类型。建议将注入 key 放在单独文件,这样方便在多个组件中导入使用。




 








import { provide, inject } from '@mpxjs/core'
+import type { InjectionKey } from '@mpxjs/core'
+
+export const key: InjectionKey<string> = Symbol() // 类型注解
+// const key = Symbol() as InjectionKey<string> // 类型断言写法等效
+
+provide(key, 'foo') // 若默认值是非字符串则会 TS 类型报错
+
+const foo = inject(key) // ✅ foo 的类型:string | undefined
+const foo = inject(key, 'default value') // ✅ foo 的类型:string
+const foo = inject(key, 1) // ❌ 默认值是非字符串则会 TS 类型报错
+

# 跨端差异

  • Mpx 输出 Web 端后,使用规则与 Vue 一致,provide/inject 的生效范围严格遵行父子组件关系,只有父组件可以成功向子孙组件提供依赖。
  • Mpx 输出小程序端会略有不同,由于小程序原生框架限制,暂时无法在子组件获取真实渲染时的父组件引用关系,所以不能像 Vue 那样基于父组件原型继承来实现 provide。在 Mpx 底层实现中,我们将组件层的 provide 挂载在所属页面实例上,相当于将组件 scope 提升到页面 scope,可以理解成一种“降级模拟”。当然,这并不影响父组件向子孙组件 provide 的能力,只是会额外存在“副作用”:同一页面中的组件可以向页面中其他所有在其之后渲染子组件提供依赖。比如同一页面下的组件 A 可以向后渲染的兄弟组件 B 的子孙组件提供数据,这在 Web 端是不允许的。因此,针对小程序端可能出现的“副作用”需要开发者自行保证,可以结合上述注入名的管理优化来规避。
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html b/docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html new file mode 100644 index 0000000000..449adbb2ab --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html @@ -0,0 +1,88 @@ + + + + + + 资源路径获取 | Mpx框架 + + + + + + + + + +

# 资源路径获取

Mpx在构建时,如果引用的页面不存在于当前 app.mpx 所在的上下文中,例如存在于 npm 包中,为避免和本地声明的其他 page 路径冲突,Mpx 会对页面路径进行 hash 化处理; +处理组件路径时也会添加 hash 防止路径名冲突,hash 化处理后最终的文件名是 name+hash+ext 的格式;对于图片等资源路径输出也会进行 hash 化处理。

开发者经常会面临以下问题:

  • 希望获取 hash 化之后的页面/组件/图片路径
  • 在页面路径变化时(分包名更改或页面名修改),需要手动修改散落在代码中各处写死的资源路径

# ?resolve

为了解决以上开发者痛点,Mpx 框架提供了资源路径自动获取能力,只需要在资源引用路径后加上?resolve, +Mpx 在编译时会生成一个资源路径处理模块,该模块暴露出资源对应的真实输出路径,从而可以实现在页面/组件/图片路径变化时正确获取输出路径,并避免写死资源路径。

获取并使用页面路径

// app.json 中注册分包,此处仅列出 packages 语法示例
+{
+    packages: [
+        '@someNpm/app.mpx?root=someNpm'
+    ]
+}
+
+// import 语法获取并使用页面路径
+import subPackageIndexPage from '@someNpm/subpackage/pages/index.mpx?resolve'
+mpx.navigateTo({
+    url: subPackageIndexPage
+})
+
+// require 语法获取并使用页面路径
+mpx.navigateTo({
+    url: require('@someNpm/subpackage/pages/index.mpx?resolve')
+})
+

获取并使用组件资源路径

我们在使用小程序 relations 语法时,需要获取组件路径,这时就可使用 ?resolve 语法

import { createComponent } from '@mpxjs/core'
+import someComponentItem from './some-component-item?resolve'
+
+createComponent({
+    relations: {
+        [someComponentItem]: {
+            type: 'child'
+        }
+    }
+})
+

获取并使用图像资源路径

<template>
+    <view wx:style="{{someStyle}}">test</view>
+</template>
+
+<script>
+    import mpx, { createPage } from '@mpxjs/core'
+    import iconPath from '../assets/icon.png?resolve'
+    
+    createPage({
+        computed: {
+            someStyle() {
+                return `background-image url(${iconPath})`
+            }
+        }
+    })
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/size-report.html b/docs-vuepress/.vuepress/dist/guide/advance/size-report.html new file mode 100644 index 0000000000..9246a7aa13 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/size-report.html @@ -0,0 +1,188 @@ + + + + + + 包体积分析 | Mpx框架 + + + + + + + + + +

# 包体积分析

目前业内主流小程序平台都对小程序的代码包设置了严格的体积限制,微信是单包 2MB,总包 16MB,支付宝是单包 2MB,总包 8MB;包体积作为有限的资源,在小程序业务开发中异常重要,特别对于像滴滴出行这样的大型复杂业务。

Mpx 在包体积控制上做了很多工作,主要包括:

此外由于 Mpx 的编译构建完全基于 webpack,也能够直接复用webpack生态自带的代码压缩,模块复用,tree shaking,side effects 等能力对代码体积进行优化。

但是系统能做的工作终究有限,在一些人为不规范操作的影响下,最终输出的项目依然可能存在大量优化空间,比如在业务中不会被调用但无法被 tree shaking / side effects 移除的代码,因为 npm 依赖版本不统一造成的重复依赖,未经压缩的图像静态资源等,在大型项目中,这些问题想要被发现会非常困难,因此我们非常需要一个体积分析工具来管控项目体积和发现隐藏的体积问题。

# 与 webpack-bundle-analyzer 的区别

webpack-bundle-analyzer 提供了非常完善的体积分析和可视化展示能力,但是在 Mpx 构建输出小程序场景下,其所提供的能力还是有所缺失:

  • 只能对 js 资源进行模块体积分析,而小程序的输出中包含了大量的非 js 静态资源,如 wxss / wxml / json 等,这些资源的体积都不会被统计分析;
  • 没有针对某个分包进行体积分析的能力,由于小程序中存在对单一分包的体积限制,我们的体积往往会集中在主包和主要业务分包中,以分包维度进行体积分析的能力非常必要;
  • 无法以特定的输入范围为维度进行体积的统计分析,这个能力诉求更多地出现在跨团队合作的复杂小程序当中,如滴滴出行。在这种场景下,接入合作的各方会更加关注己方引入的体积,并进行针对性地优化。

由于上述原因,我们提供了@mpxjs/size-report插件提供包体积分析能力,弥补了 webpack-bundle-analyzer 的能力缺失,为业务提供了便捷准确的包体积管控优化抓手。

# 使用方法

# 安装插件

npm i @mpxjs/size-report --save-dev
+

# 配置插件

在项目 webpack.config 文件中配置使用 @mpxjs/size-report 插件即可使用,简单示例如下:

// vue.config.js
+const MpxSizeReportPlugin = require('@mpxjs/size-report')
+module.exports = defineConfig({
+  configureWebpack() {
+    return {
+      plugins: [
+        new MpxSizeReportPlugin(
+          {
+              // 本地可视化服务相关配置
+              server: {
+                  enable: true, // 是否启动本地服务,非必填,默认 true
+                  autoOpenBrowser: true, // 是否自动打开可视化平台页面,非必填,默认 true
+                  port: 0, // 本地服务端口,非必填,默认 0(随机端口)
+                  host: '127.0.0.1', // 本地服务host,非必填
+              },
+              // 体积报告生成后输出的文件地址名,路径相对为 dist/wx 或者 dist/ali
+              filename: '../report.json',
+              // 配置阈值,此处代表总包体积阈值为 16MB,分包体积阈值为 2MB,超出将会触发编译报错提醒,该报错不阻断构建
+              threshold: {
+                  size: '16MB',
+                  packages: '2MB'
+              },
+              // 配置体积计算分组,以输入分组为维度对体积进行分析,当没有该配置时结果中将不会包含分组体积信息
+              groups: [
+                  {
+                      // 分组名称
+                      name: 'vant',
+                      // 配置分组 entry 匹配规则,小程序中所有的页面和组件都可被视为 entry,如下所示的分组配置将计算项目中引入的 vant 组件带来的体积占用
+                      entryRules: {
+                          include: '@vant/weapp'
+                      }
+                  },
+                  {
+                      name: 'pageGroup',
+                      // 每个分组中可分别配置阈值,如果不配置则表示
+                      threshold: '500KB',
+                      entryRules: {
+                          include: ['src/pages/index', 'src/pages/user']
+                      }
+                  },
+                  {
+                      name: 'someSdk',
+                      entryRules: {
+                          include: ['@somegroup/someSdk/index', '@somegroup/someSdk2/index']
+                      },
+                      // 有的时候你可能希望计算纯 js 入口引入的体积(不包含组件和页面),这种情况下需要使用 noEntryRules
+                      noEntryRules: {
+                          include: 'src/lib/sdk.js'
+                      }
+                  }
+              ],
+              // 是否收集页面维度体积详情,默认 false
+              reportPages: true,
+              // 是否收集资源维度体积详情,默认 false
+              reportAssets: true,
+              // 是否收集冗余资源,默认 false
+              reportRedundance: true,
+              // 展示某些分包资源的引用来源信息,默认为 []
+              showEntrysPackages: ['main']
+          }
+      )
+      ]
+    }
+  }
+})
+

参考上述示例进行配置后,构建代码后,dist 目录下会产出 report.json 文件,里边是项目的具体体积信息,关于输入 json 的简单示例如下:

{
+    // 项目体积概要,大部分情况下,我们只需要看这部分就足够了
+    "sizeSummary": {
+        // 分组体积概要,与上述配置文件中的 groups 对应
+        "groups": [
+            {
+                "name": "vant",
+                // 只有该分组包含的模块体积
+                "selfSize": "164.75KiB",
+                "selfSizeInfo": {
+                  // 该分组所占 shansong
+                  "shansong": "164.75KiB"
+                },
+                "sharedSize": "885.68KiB",
+                "sharedSizeInfo": {
+                  "main": "885.68KiB"
+                }
+            },
+        ],
+        // 项目各个分包以及主包体积概要
+        "sizeInfo": {
+            "main": "1000KiB",
+            "fenbao1": "200kiB"
+        },
+        // 项目总体积
+        "totalSize": "13468.85KiB",
+        // 项目静态资源总体积
+        "staticSize": "4880.58KiB",
+        // 项目chunk 文件总体积
+        "chunkSize": "8587.70KiB",
+        // 非依赖项体积大小
+        "copySize": "1KiB"
+    },
+    // 分组资源详细体积
+    "groupsSizeInfo": [
+        {
+            "name": "groupOne",
+            // group 自身包含的 module 详情列表
+            "selfEntryModules": [],
+            // group 与其他group 共有的 module 详情列表
+            "sharedEntryModules": [],
+            // 自身包含 module 体积
+            "selfSize": "",
+            // 自身包含 module 体积详情
+            "selfSizeInfo": {
+                "main": {},
+                "homepage": {}
+            },
+            // 与其他 group 共有 module 体积
+            "sharedSize": "",
+            // 与其他 group 共有 module 体积详情
+            "sharedSizeInfo": {
+                "main": {},
+                "homepage": {}
+            },
+
+        }
+    ],
+    // 项目资源详细体积报告
+    "assetsSizeInfo": {
+        "assets": [{
+            // 资源类型
+            "type": "chunk",
+            "name": "test",
+            // 分包名
+            "packageName": "main",
+            "size": "",
+            "modules": []
+        }]
+    }
+}
+

与此同时,如果你开启了本地可视化平台服务,可以直接通过可视化平台查看项目体积构成。默认开启自动打开平台网页或者手动打开后,整体页面展示如下图:

size-report (opens new window)

可视化平台中包含三部分功能,第一个体积分析如上图所示,主要是展示整体项目的体积总览,以及 group 体积列表和分包体积列表。

第二个功能是体积详情模块,在该模块中,可以最小颗粒度的查看包体积构成,通过 table 表格的层层展开可看到每个 group 中包含的分包,点击分包可看到该分包包含的静态资源和 js 模块详细列表,同时聚合模式可将分散的模块整合为具体的npm包名,方便宏观查看。此外为了方便用户定向查看某个特定资源的分布情况,列表上方可进行资源路径/名称搜索,该搜索支持模糊匹配,搜索结果为该资源在项目中 group 和 分包的分布情况。

size-report (opens new window)

第三个功能为体积对比功能,这里主要进行项目不同版本之间的体积大小变化比较,体积对比依赖插件生成的json文件,通过该功能,可具体的分析出包体积具体增大的group,分包,以及模块,给包体积优化提供定向目标。

size-report (opens new window)

# 业务实践

目前 sizeReport 工具在滴滴出行小程序, 花小猪, 青桔骑行, 特惠出行小程序以及一部分外部小程序中使用。

在滴滴出行小程序中,配置使用 @mpxjs/size-report 插件后使包体积管控和优化更加工程化:

  • 在 sizeReport 检测结果文件中,通过对 groupsSizeInfo 和 assetsSizeInfo 中assets 与 module 体积分析,我们发现了部分体积较大图片文件和css资源,通过将图片存 CDN 和删除冗余文件;

  • 基于各小程序平台对小程序总包,主包,分包有大小限制的原则,给各接入方配置了主包和首页分包体积大小占比阈值,在构建时检测到包体积超过阈值时,抛出 error 阻断构建,开发者可通过 size-report.json 中详细的包体积分析准确的找到体积变动点。

# 总结

Mpx sizeReport 工具对小程序体积计算有更细微(模块级别)的体积展示。 更加适合小程序开发场景的包体积分析。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/ssr.html b/docs-vuepress/.vuepress/dist/guide/advance/ssr.html new file mode 100644 index 0000000000..809c77cb83 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/ssr.html @@ -0,0 +1,190 @@ + + + + + + SSR | Mpx框架 + + + + + + + + + +

# SSR

近些年来,SSR/SSG 由于其良好的首屏展现速度和SEO友好性逐渐成为主流的技术规范,但过去 Mpx 对 SSR 的支持不完善,使用 Mpx 开发的跨端页面一直无法享受到 SSR 带来的性能提升,在 2.9 版本中,我们对 Mpx 输出 web 流程进行了大量适配改造,解决了 SSR 中常见的内存泄漏、跨请求状态污染和数据预请求等问题,完整支持了基于 Vue 和 Pinia 的 SSR 技术方案。

# 配置使用 SSR

在 Vue SSR 项目中,我们一般需要提供 server entry 和 client entry 两个文件作为 webpack 的构建入口,然后通过 webpack 构建出 server bundle 和 client bundle。在用户访问页面时,在服务端使用 server bundle 渲染出 HTML 字符串,作为静态页面发送到客户端,然后在客户端使用 client bundle 通过水合(hydration)对静态页面进行激活,实现可交互效果,下图展示了 Vue SSR 的大致流程。

Vue SSR流程

Mpx SSR 核心基于 Vue SSR 实现,大致流程思路与 Vue 一致,不过为了保持与小程序代码的兼容性,在配置使用上有一些改动差异,下面我们详细展开介绍:

# 构建server/client bundle

SSR项目中,我们需要分别构建出 server bundle 和 client bundle,对于不同环境的产物构建,我们需要进行不同的配置。 +在 Vue 中,我们需要提供 entry-server.jsentry-client.js 两个文件分别作为 server 和 client 的构建入口,与 Vue 不同,在 Mpx 中我们通过编译处理与运行时增强生命周期实现了使用 app.mpx 作为统一构建入口,无需区分 server 和 client。

# 服务端构建配置

服务端配置中除了将 entry 制定为 app.mpx 及其它基础配置外,最重要的是安装 vue-server-renderer 包中提供的 server-plugin 插件,该插件能够构建产出 vue-ssr-server-bundle.json 文件供 renderer 后续消费。

// webpack.server.config.js
+const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
+
+module.exports = merge(baseConfig, {
+  // 将 entry 指向项目的 app.mpx 文件
+  entry: './app.mpx',
+  // ...
+  plugins: [
+   // 产出 `vue-ssr-server-bundle.json`
+    new VueSSRServerPlugin()
+  ]
+})
+

更加详细的配置说明可参考 Vue SSR的服务端配置 (opens new window)

# 客户端构建配置

类似服务端构建配置,在客户端构建中我们需要使用 vue-server-renderer 包中 client-plugin 插件来帮助我们生成客户端环境的资源清单 vue-ssr-client-manifest.json,并供 renderer 后续消费。

// webpack.client.config.js
+const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
+
+module.exports = merge(baseConfig, {
+  // 将 entry 指向项目的 app.mpx 文件
+  entry: './app.mpx',
+  // ...
+  plugins: [
+    // 产出 `vue-ssr-client-manifest.json`。
+    new VueSSRClientPlugin()
+  ]
+})
+

更加详细的配置说明可参考 Vue SSR的客户端配置 (opens new window)

# 准备页面模版

SSR 渲染中,我们创建 renderer 需要一个页面模板,简单的示例如下:

<!DOCTYPE html>
+<html lang="en">
+  <head><title>Hello</title></head>
+  <body>
+    <!--vue-ssr-outlet-->
+  </body>
+</html>
+

与 CSR 渲染模版不同,SSR 渲染模版中需要提供一个特殊的 <!--vue-ssr-outlet-->注释,标记 SSR 渲染产物的插入位置,如使用 @mpxjs/cli 脚手架创建 SSR 项目,该模版已经内置于脚手架中。

# 集成启动 SSR 服务

当我们准备好页面模版和双端构建产物后,我们就可以创建 renderer 并与 Node 服务进行集成,启动 SSR 服务,下面以 express 为例:

//server.js
+const app = require('express')()
+const { createBundleRenderer } = require('vue-server-renderer')
+// 通过 vue-server-renderer/server-plugin 生成的文件
+const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
+// 通过 vue-server-renderer/client-plugin 生成的文件
+const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
+ // 页面模版文件
+const template = require('fs').readFileSync('../src/index.template.html', 'utf-8')
+// 创建 renderer 渲染器
+const renderer = createBundleRenderer(serverBundle, {
+    runInNewContext: false,
+    template,
+    clientManifest,
+});
+// 注册启动 SSR 服务
+app.get('*', (req, res) => {
+  const context = { url: req.url }
+  renderer.renderToString(context, (err, html) => {
+  	if (err) {
+  	  res.status(500).end('Internal Server Error')
+      return
+    }
+  	res.end(html);
+  })
+})
+app.listen(8080)
+

# SSR 生命周期

在 Mpx 2.9 的版本中,我们提供了三个全新用于 SSR 的生命周期,分别是onAppInitserverPrefetchonSSRAppCreated,以统一服务端与客户端的构建入口,下面展开介绍:

# onAppInit

该生命周期仅可在 App 中使用

在 SSR 中用户每发出一个请求,我们都会为其生成一个新的应用实例,onAppInit 生命周期会在应用创建 new Vue(...) 前被调用,其执行的返回结果会被合并到创建应用的 options

很常见的使用场景在于返回新的全局状态管理实例,Mpx 中提供了 @mpxjs/pinia 作为全局状态管理工具,我们可以在 onAppInit 中返回全新的 pinia 示例避免产生跨请求状态污染,示例如下:

// app.mpx
+import mpx, { createApp } from '@mpxjs/core'
+import { createPinia } from '@mpxjs/pinia'
+
+createApp({
+  // ...
+  onAppInit () {
+    const pinia = createPinia()
+    return {
+      pinia
+    }
+  }
+})
+

SSR 中仅支持使用 @mpxjs/pinia 作为状态管理工具,@mpxjs/store 暂不支持

# serverPrefetch

该生命周期可在 App/Page/Component 中使用,只在服务端渲染时执行

当我们需要在 SSR 使用数据预拉取时,可以使用这个生命周期进行,使用方法与 Vue 一致, 示例如下:

选项式 API:

import { createPage } from '@mpxjs/core'
+import useStore from '../store/index'
+
+createPage({
+  //...
+  serverPrefetch () {
+    const query = this.$route.query
+    const store = useStore(this.$pinia)
+    // return the promise from the action, fetch data and update state
+    return store.fetchData(query)
+  }
+})
+

组合式 API:

import { onServerPrefetch, getCurrentInstance, createPage } from '@mpxjs/core'
+import useStore from '../store/index'
+
+createPage({
+  setup () {
+    const store = userStore()
+    onServerPrefetch(() => {
+      const query = getCurrentInstance().proxy.$route.query
+      // return the promise from the action, fetch data and update state
+      return store.fetchData(query)
+    })
+  }
+})
+

关于数据预拉取更详细的说明可以查看这里 (opens new window)

# onSSRAppCreated

该生命周期仅可在 App 中使用,只在服务端渲染时执行

在 Vue SSR 项目中,我们会在 entry-server.js 中导出一个工厂函数,在该函数中实现创建应用实例、路由匹配和状态同步等逻辑,并返回应用实例 app

在 Mpx SSR 中,我们将这部分逻辑整合在 onSSRAppCreated 中执行,该生命周期执行时用户可以从参数中获取应用实例 app、路由实例 router、数据管理实例 pinia 和 SSR 上下文 context,在完成必要的操作后,该生命周期需要返回一个 resolve(app) 的 promise。

通常我们会在 onSSRAppCreated 中进行路由路径设置和数据预拉取后的状态同步工作,示例如下:

// app.mpx
+createApp({
+    // ...,
+    onSSRAppCreated ({ pinia, router, app, context }) {
+      return new Promise((resolve, reject) => {
+        // 设置服务端路由路径
+        router.push(context.url)
+        router.onReady(() => {
+          // 应用完成渲染时执行
+          context.rendered = () => {
+            // 将服务端渲染后得到的 pinia.state 同步到 context.state 中,
+            // context.state 会被自动序列化并通过 `window.__INITIAL_STATE__`
+            // 注入到 HTML 中,并在客户端运行时再读取同步
+            context.state = pinia.state.value
+          }
+          // 返回应用程序实例
+          resolve(app)
+        }, reject)
+      })
+    }
+})
+

上述示例代码等价于 Vue 中的 entry-server.js

// entry-server.js
+import { createApp } from './app'
+
+export default context => {
+  return new Promise((resolve, reject) => {
+    const { app, router, store } = createApp()
+    router.push(context.url)
+    router.onReady(() => {
+      // This `rendered` hook is called when the app has finished rendering
+      context.rendered = () => {
+        // After the app is rendered, our store is now
+        // filled with the state from our components.
+        // When we attach the state to the context, and the `template` option
+        // is used for the renderer, the state will automatically be
+        // serialized and injected into the HTML as `window.__INITIAL_STATE__`.
+        context.state = store.state
+      }
+      resolve(app)
+    }, reject)
+  })
+}
+

如用户没有配置 onSSRAppCreated,框架内部会执行兜底逻辑,以保障 SSR 的正常运行。

# 其他注意事项

  1. Mpx SSR 渲染支持 i18n 的功能,但为了防止内存泄漏,当前 i18n 实例不会随着每次请求而重新创建,这是由于 Vue2.x 版本插件机制的设计缺陷所造成的,因此在使用 i18n 进行 SSR 时可能会产生跨请求状态污染的问题,这个问题会在未来 Mpx 输出 web 切换为 Vue3 后完全解决。

  2. 在服务端渲染阶段,对于 global 全局对象访问修改,如__mpx, __mpxRouter, __mpxPinia 都可能导致全局状态污染,所以在服务端渲染阶段请尽量避免进行相关操作;对于存在全局访问修改的方法,如 getApp(), getCurrentPages() 等在服务端渲染中被调用时,会产生相关报错提示。

  3. 由于服务器无法收到 URL 中的 hash 信息,使用 SSR 时需要通过修改 mpx.config.webRouteConfig 将路由模式设置成 history 模式。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/store.html b/docs-vuepress/.vuepress/dist/guide/advance/store.html new file mode 100644 index 0000000000..c8a3cea770 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/store.html @@ -0,0 +1,599 @@ + + + + + + 状态管理(store) | Mpx框架 + + + + + + + + + +

# 状态管理(store)

Mpx 参考 vuex 设计实现了外部状态管理系统(store),其中的概念与 api 与 vuex 保持一致,为了更好地支持状态模块管理和跨团队合作场景,我们提出多实例 store 作为 vuex 中 modules 的替代方案,该方案在模块拆分及合并上的灵活性远高于 modules。

# 介绍

Store 是一个全局状态管理容器,能够轻松实现复杂场景下的组件通信需求,store 与简单的全局状态对象主要有以下两点不同:

  1. Store 中存放的状态是响应式的。当用户将 store 中的状态注入到组件以后,若 store 中状态发生变化,那么对应的组件也会得到更新。

  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径是显式地提交变更(commit mutation),这种方式能使整个应用中的状态变更变得可回朔可追踪,同时也更加安全。

# 创建 store

让我们来创建一个简单 store。创建过程直截了当,仅需要提供一个初始 state 对象和一些 mutation 方法,并调用 Mpx 暴露的 createStore 方法进行创建:

import {createStore} from '@mpxjs/core'
+
+const store = createStore({
+  state: {
+    count: 0
+  },
+  mutations: {
+    increment (state) {
+      state.count++
+    }
+  }
+})
+
+export default store
+

现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

store.commit('increment')
+console.log(store.state.count) // 1
+

接下来,我们将会更深入地探讨一些 store 的核心概念。让我们先从 State 开始。

# State

State 存放了 store 中的原始状态数据,可以通过 store.state 进行访问。

# 在组件中获取 state

Mpx 在小程序组件中实现了数据响应,而刚才提到 store 中的状态也是响应式的,组件中获取 store 状态最简单的方法就是建立一个计算属性,在计算属性中访问 store 中需要的状态数据并返回:

// store.js
+import {createStore, createComponent} from '@mpxjs/core'
+
+const store = createStore({
+  state: {
+    count: 0
+  },
+  mutations: {
+    increment (state) {
+      state.count++
+    }
+  }
+})
+
+createComponent({
+  computed: {
+    count () {
+      return store.state.count
+    }
+  }
+})
+

当 store.state.count 发生变化时, 组件中访问了 count 计算属性的 watcher 将得到响应。

于 vuex 中的不同的地方在于,vuex 奉行单一状态树,一个应用当中只存在一个 store 示例,用户能够在组件中通过this.$store隐式地访问到当前应用的 store;而 Mpx 当中为了追求灵活便捷的状态模块化管理及跨团队合作的能力,支持了多实例 store,用户需要显式地引入 store 实例,并通过计算属性将其注入到组件当中。

# MapState 辅助函数

当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性

import store from '../store'
+import {createComponent} from '@mpxjs/core'
+
+createComponent({
+  computed: store.mapState({
+    // 箭头函数可使代码更简练
+    count: state => state.count,
+
+    // 传字符串参数 'count' 等同于 state => state.count
+    countAlias: 'count',
+
+    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
+    countPlusLocalState (state) {
+      return state.count + this.localCount
+    }
+  })
+})
+

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

import store from '../store'
+import {createComponent} from '@mpxjs/core'
+
+createComponent({
+  computed: store.mapState([
+    // 映射 this.count 为 store.state.count
+    'count'
+  ])
+})
+

# 对象展开运算符

MapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符 (opens new window),我们可以极大地简化写法:

import store from '../store'
+import {createComponent} from '@mpxjs/core'
+
+createComponent({
+  computed: {
+    localComputed () { /* ... */ },
+    // 使用对象展开运算符将 mapState 返回的对象合并到计算属性中
+    ...store.mapState([
+      'count',
+      // ...
+    ])
+  }
+})
+

使用 store 并不意味着你需要将所有的状态放入 store。如果有些状态严格属于单个组件,最好将其放到组件内部的 data 当中。

# Getter

有时候我们需要从 state 中派生出一些状态,例如经过过滤的列表:

createComponent({
+  computed: {
+    doneTodos () {
+      return store.state.todos.filter(todo => todo.done)
+    }
+  }
+})
+

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,无论哪种方式都不是很理想。

这个时候我们可以在 store 中定义 getter 来完成这个功能,getter 可以简单认为是 store 中的计算属性。同计算属性一样,getter 的返回值会根据它的依赖被缓存起来,只有当它的依赖发生变化时才会被重新计算。

Getter 接受 state 作为第一个参数:

import {createStore} from '@mpxjs/core'
+
+const store = createStore({
+  state: {
+    todos: [
+      { id: 1, text: 'sth1', done: true },
+      { id: 2, text: 'sth2', done: false }
+    ]
+  },
+  getters: {
+    doneTodos: state => {
+      return state.todos.filter(todo => todo.done)
+    }
+  }
+})
+
+export default store
+

定义好的 getter 可以通过 store.getters 访问:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
+

Getter 也可以接受其他 getters 作为第二个参数, rootState作为第三个参数,rootState是模块化中引入的概念,之后会详细介绍:

getters: {
+  // ...
+  doneTodosCount: (state, getters, rootState) => {
+    return getters.doneTodos.length
+  }
+}
+
store.getters.doneTodosCount // -> 1
+

我们采用与state类似的方法将其注入到组件中进行访问:

computed: {
+  doneTodosCount () {
+    return store.getters.doneTodosCount
+  }
+}
+

# MapGetters 辅助函数

MapGetters 的作用与使用方法同上面提到的 mapState 高度类似,唯一的区别在于 mapGetters 用于映射 store 中的 getter:

import store from '../store'
+import {createComponent} from '@mpxjs/core'
+
+createComponent({
+  // ...
+  computed: {
+    // 使用对象展开运算符将 getter 混入 computed 对象中
+    ...store.mapGetters([
+      'doneTodosCount',
+      'anotherGetter',
+      // ...
+    ])
+  }
+})
+

如果你想将一个 getter 在组件中映射为另一个名字,使用对象形式:

store.mapGetters({
+  // 映射 this.doneCount 为 store.getters.doneTodosCount
+  doneCount: 'doneTodosCount'
+})
+

# Mutation

更改 store 中的状态的唯一方法是提交 mutation。Mutation 非常类似于事件:每个 mutation 都有一个字符串的类型(type) 和 一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,它接受 state 作为第一个参数:

import {createStore} from '@mpxjs/core'
+
+const store = createStore({
+  state: {
+    count: 1
+  },
+  mutations: {
+    increment (state) {
+      // 变更状态
+      state.count++
+    }
+  }
+})
+
+export default store
+

当你需要触发某个 mutation 时,你需要使用对应的 type 调用 store.commit 方法,就像触发某个事件一样:

store.commit('increment')
+

# 提交载荷(Payload)

你可以向 store.commit 传入额外的参数,作为 mutation 的载荷(payload)

// ...
+mutations: {
+  increment (state, n) {
+    state.count += n
+  }
+}
+
store.commit('increment', 10)
+

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且增强 mutation 的可读性:

// ...
+mutations: {
+  increment (state, payload) {
+    state.count += payload.amount
+  }
+}
+
store.commit('increment', {
+  amount: 10
+})
+

# Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数

# 在组件中提交 Mutation

你可以在组件中使用 store.commit('increment') 提交 mutation,或者使用 store.mapMutations 辅助函数将组件中名为 incrementmethod 映射为 store.commit('increment') 调用。

import { createComponent } from '@mpxjs/core'
+import store from '../store'
+
+createComponent({
+  // ...
+  methods: {
+    ...store.mapMutations([
+      // 将 this.increment() 映射为 store.commit('increment')
+      'increment', 
+       // mapMutations 也支持载荷:将 this.incrementBy(amount) 映射为 store.commit('incrementBy', amount)
+      'incrementBy'
+    ]),
+    // mapMutations同样支持传入对象形式的参数进行别名映射
+    ...store.mapMutations({
+      // 将 this.add() 映射为 store.commit('increment')
+      add: 'increment'
+    })
+  }
+})
+

# Action

Action 类似于 mutation,不同在于:

  • Action 不能直接变更状态,但是可以提交 mutation 进行状态变更
  • Action 可以包含任意异步操作

让我们来创建一个简单的 action:

import {createStore} from '@mpxjs/core'
+
+const store = createStore({
+  state: {
+    count: 0
+  },
+  mutations: {
+    increment (state) {
+      state.count++
+    }
+  },
+  actions: {
+    increment (context) {
+      context.commit('increment')
+    }
+  }
+})
+
+export default store
+

Action 函数接受一个 context 对象,因此你可以调用 context.commit 提交一个 mutation,调用 context.dispatch 触发其他的 action 调用,或者通过 context.rootStatecontext.statecontext.getters 来获取全局state、局部state 和 全局 getters。

实践中,我们会经常用到 ES2015 的参数解构 (opens new window)来简化代码:

actions: {
+  increment ({ commit }) {
+    commit('increment')
+  }
+}
+

# 调用 action

Action 可以通过 store.dispatch 方法进行调用:

store.dispatch('increment')
+

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

actions: {
+  incrementAsync ({ commit }) {
+    setTimeout(() => {
+      commit('increment')
+    }, 1000)
+  }
+}
+

Action 同样支持载荷:

// 调用载荷
+store.dispatch('incrementAsync', {
+  amount: 10
+})
+

来看一个更加实际的购物车示例,涉及到调用异步 API提交多个 mutation

actions: {
+  checkout ({ commit, state }, products) {
+    // 把当前购物车的物品备份起来
+    const savedCartItems = [...state.cart.added]
+    // 发出结账请求,然后乐观地清空购物车
+    commit(types.CHECKOUT_REQUEST)
+    // 购物 API 接受一个成功回调和一个失败回调
+    shop.buyProducts(
+      products,
+      // 成功操作
+      () => commit(types.CHECKOUT_SUCCESS),
+      // 失败操作
+      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
+    )
+  }
+}
+

注意我们进行了一系列异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。

# 在组件中调用 Action

你可以在组件中使用 store.dispatch('increment') 进行 action 调用,或者使用 store.mapActions 辅助函数将组件中名为 incrementmethod 映射为 store.dispatch('increment')

import { createComponent } from '@mpxjs/core'
+import store from '../store'
+
+createComponent({
+  // ...
+  methods: {
+    ...store.mapActions([
+      // 将 this.increment() 映射为 store.dispatch('increment')
+      'increment', 
+      // mapActions 也支持载荷:将 this.incrementBy(amount) 映射为 store.dispatch('incrementBy', amount)
+      'incrementBy'
+    ]),
+    // mapActions 同样支持传入对象形式的参数进行别名映射
+    ...store.mapActions({
+      // 将 this.add() 映射为 store.dispatch('increment')
+      add: 'increment' 
+    })
+  }
+})
+

# 组合 Action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

在 Mpx 中,action 永远返回一个 Promise,你可以在定义 action 手动返回一个 Promise,即使你没有这样做,框架也会对你 action 的返回值进行 Promise.resolve(returned) 包装,确保你可以使用 Promise 的方式处理多个 action 之间的异步组合:

actions: {
+  actionA ({ commit }) {
+    return new Promise((resolve, reject) => {
+      setTimeout(() => {
+        commit('someMutation')
+        resolve()
+      }, 1000)
+    })
+  }
+}
+

现在你可以通过 then 方法获取 actionA 执行完毕的时机:

store.dispatch('actionA').then(() => {
+  // ...
+})
+

在另外一个 action 中也可以通过这种方式进行一系列异步调用:

actions: {
+  // ...
+  actionB ({ dispatch, commit }) {
+    return dispatch('actionA').then(() => {
+      commit('someOtherMutation')
+    })
+  }
+}
+

如果我们利用 async / await (opens new window),我们能够更方便地对一系列异步操作进行组合:

// 假设 getData() 和 getOtherData() 返回的是 Promise
+
+actions: {
+  async actionA ({ commit }) {
+    commit('gotData', await getData())
+  },
+  async actionB ({ dispatch, commit }) {
+    await dispatch('actionA') // 等待 actionA 完成
+    commit('gotOtherData', await getOtherData())
+  }
+}
+

# Modules

Mpx 虽然支持了 modules,但并不推荐使用。在 Mpx 中,我们更推荐使用[多实例模式](#多实例 store)进行对应用状态进行模块划分

在 Mpx 中,modules 的设计与 vuex 中基本保持一致,在 createStore 中将子模块配置传入 modules 配置项中即可使用:

import {createStore} from '@mpxjs/core'
+
+const moduleA = {
+  state: { ... },
+  mutations: { ... },
+  actions: { ... },
+  getters: { ... }
+}
+
+const moduleB = {
+  state: { ... },
+  mutations: { ... },
+  actions: { ... }
+}
+
+const store = createStore({
+  modules: {
+    moduleA,
+    moduleB
+  }
+})
+
+store.state.moduleA // -> moduleA 的状态
+store.state.moduleB // -> moduleB 的状态
+
+export default store
+

Mpx 并未实现 vuex 中的命名空间,除 state 外的所有属性(getters / mutations / actions)将被平铺展开到根 store 的对应空间下。在多实例 store 中,我们的实现方式则与命名空间高度相似。

# 模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。

const moduleA = {
+  state: { count: 0 },
+  mutations: {
+    increment (state) {
+      // 这里的 state 对象是模块的局部状态
+      state.count++
+    }
+  },
+  getters: {
+    doubleCount (state) {
+      return state.count * 2
+    }
+  }
+}
+

对于模块内部的 action,局部状态通过 context.state 访问,根状态则通过 context.rootState 访问:

const moduleA = {
+  // ...
+  actions: {
+    incrementIfOddOnRootSum ({ state, commit, rootState }) {
+      if ((state.count + rootState.count) % 2 === 1) {
+        commit('increment')
+      }
+    }
+  }
+}
+

对于模块内部的 getter,根状态能通过第三个参数访问:

const moduleA = {
+  // ...
+  getters: {
+    sumWithRootCount (state, getters, rootState) {
+      return state.count + rootState.count
+    }
+  }
+}
+

# 模块在组件中的引入方式

const store = createStore({
+  modules: {
+    a: {
+      state: {
+        name: 1
+      },
+      getters: {
+        getName: s => s.name
+      }
+    },
+    b: moduleB
+  }
+})
+
+createComponent({
+  // 我们支持了多种方式引入子模块中的 state
+  computed: {
+    // 通过函数引入
+    ...store.mapState({
+      nameA: state => state.a.name
+    }),
+    // 通过路径字符串引入
+    ...store.mapState({
+      nameA2: 'a.name'
+    }),
+    // 通过传入模块路径引入
+    ...store.mapState('a', ['name']),
+    // 对于 getters / mutations / actions,由于我们没有实现 namespace,子模块当中定义的 getters / mutations / actions 都能在根空间下直接访问,正常调用映射方法即可
+    ...store.mapGetters('getName')
+  }
+})
+

# 多实例 store

在 Mpx 中,我们允许在一个应用下创建多个 store 实例,进行模块化分布式的数据管理,同时提供了 deps 声明模块依赖的机制,让用户自由组合多个 store 实例并基于这些已有的 store 创建新的继承 store。在实际业务使用中,我们发现多实例模式的灵活性远高于 module,更加适合跨团队合作当中的数据管理。

使用多实例 store 的方式非常简单,你只需要多次调用 createStore api 创建出多个 store 示例,并分别将其注入到组件中即可使用,简单示例如下:

import { createComponent, createStore } from '@mpxjs/core'
+
+const storeA = createStore({
+  state: {
+    countA: 0
+  },
+  mutations: {
+    incrementA (state) {
+      state.countA++
+    }
+  }
+})
+
+const storeB = createStore({
+  state: {
+    countB: 0
+  },
+  mutations: {
+    incrementB (state) {
+      state.countB++
+    }
+  }
+})
+
+createComponent({
+  computed: {
+    // ...
+    ...storeA.mapState(['countA']),
+    ...storeB.mapState(['countB'])
+  },
+  methods: {
+    // ...
+    ...storeA.mapMutations(['incrementA']),
+    ...storeB.mapMutations(['incrementB'])
+  }
+})
+

可以看到 Mpx 中的 map 辅助方法都挂载在 store 实例上,正是为了支持多实例 store 的实现

# 合并继承多实例 store

在实际的跨团队业务当中,我们既希望不同团队间的数据管理尽量解耦,也希望一些共同的部分能够复用,这就要求我们的 store 实例可以以某种方式组合起来使用,我们提供了 deps 能来实现多实例 store 的合并与继承。

承接上面的示例,我们基于 storeA 和 storeB 创建一个新的 storeC,在 storeC 当中可以定义自身的独立状态,也能基于 storeA 和 storeB 进行状态衍生:

import { createComponent, createStore } from '@mpxjs/core'
+
+const storeA = createStore({
+  state: {
+    countA: 0
+  },
+  mutations: {
+    incrementA (state) {
+      state.countA++
+    }
+  }
+})
+
+const storeB = createStore({
+  state: {
+    countB: 0
+  },
+  mutations: {
+    incrementB (state) {
+      state.countB++
+    }
+  }
+})
+
+const storeC = createStore({
+  state: {
+    countC: 0
+  },
+  getters: {
+    abc (state) {
+      // 此处 state.storeA 指向了原始的 storeA.state
+      return state.storeA.countA + state.storeB.countB + state.countC
+    }
+  },
+  mutations: {
+    incrementC (state) {
+      state.countC++
+    }
+  },
+  actions: {
+    incrementB ({ commit }) {
+      // storeC内部也可以通过命名空间路径的方式提交 storeB 的 mutation
+      commit('storeB.incrementB')
+    }
+  },
+  // 此处 deps 声明了 storeC 的依赖,依赖中的 state / getters / mutations / actions 都会以 deps 中的 key 为命名空间存放在 storeC 对应的域下
+  deps: {
+    storeA,
+    storeB
+  }
+})
+
+// 通过继承合并得到的 storeC,我们可以完整访问其依赖 storeA / storeB
+createComponent({
+  computed: {
+    // ...
+    // 使用路径字符串或函数映射,可以看出和 modules 中 mapState 的方式非常类似
+    ...storeC.mapState({
+      countA: 'storeA.countA',
+      countA2: state => state.storeA.countA
+    }),
+    // 传入命名空间参数映射 storeB 中的 countB
+    ...storeC.mapState('storeB', ['countB']),
+    // 映射基于 storeA/B/C 衍生得到的 getters
+    ...storeC.mapGetters(['abc'])
+  },
+  methods: {
+    // ...
+    // mutation不支持函数映射
+    // 下面代码以三种方式分别映射了increment、incrementB和incrementC
+    ...storeC.mapMutations({
+      incrementA: 'storeA.incrementA'
+    }),
+    ...storeC.mapMutations('storeB', ['incrementB']),
+    ...storeC.mapMutations(['incrementC'])
+  }
+})
+

简单来讲,作为 deps 的 store 会以注册在 deps 中的 key 值作为命名空间,将其原始的 state / getters / mutations / actions 存放在新生成 store 对应的域下,便于新 store 对其进行访问并衍生出新的数据或操作,如上述示例中,storeA.state 会存放在 storeC.state.storeA 中,对于 getters / mutations / actions 亦然。

# 在 Typescript 中使用 store

Mpx 自 2.2 版本开始支持 Typescript,为了更好地支持 store 中的类型推导,我们针对 Typescript 环境提供了变种的 store api createStoreWithThis 进行 store 创建,该 api 最主要的变化在于定义 getters,mutations 和 actions 时,自身的 state,getters 等属性不再通过参数传入,而是会挂载到函数的执行上下文 this 当中,通过 this.state 或 this.getters 的方式进行访问,简单的使用示例如下:

const store = createStoreWithThis({
+  state: {
+    aa: 1,
+    bb: 2
+  },
+  getters: {
+    cc() {
+      // 使用 this.state 访问 state
+      return this.state.aa + this.state.bb
+    }
+  },
+  actions: {
+    doSth3() {
+      // 使用 this.getters 访问 getter
+      console.log(this.getters.cc)
+      return false
+    }
+  }
+})
+

详细的使用方式及推导规则请查看 Typescript 支持章节。

# 在组合式 API 中使用 store

Mpx 自 2.8 版本完整支持了组合式 API 的开发方式,虽然在组合式 API 中我们首推使用 pinia 作为外部状态管理方案,不过旧的 store 在组合式 API 中仍然可以使用,我们提供了新的 mapStateToRefsmapGettersToRefs 便于用户在组合式 API 中访问 stategetters,而对于 mutationsactions,用户可以直接调用 store 实例上的 commitdispatch 方法进行调用,或者使用原有的 map* API 进行映射访问,简单使用示例如下:

import { createPage, createStoreWithThis, watchEffect } from '@mpxjs/core'
+
+const store = createStoreWithThis({
+  state: {
+    count: 123
+  },
+  getters: {
+    doubleCount () {
+      return this.state.count * 2
+    }
+  },
+  mutations: {
+    addCount () {
+      this.state.count++
+    },
+    subCount () {
+      this.state.count--
+    }
+  }
+})
+
+createPage({
+  setup () {
+    const { count } = store.mapStateToRefs(['count'])
+    const { doubleCount } = store.mapGettersToRefs(['doubleCount'])
+    const { addCount } = store.mapMutations(['addCount'])
+
+    watchEffect(() => {
+      console.log(count.value, doubleCount.value)
+    })
+
+    return {
+      count,
+      doubleCount,
+      addCount,
+      subCount () {
+        // 两种方式均可以进行 mutations 调用,actions 同理
+        store.commit('subCount')
+      }
+    }
+  }
+})
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/subpackage.html b/docs-vuepress/.vuepress/dist/guide/advance/subpackage.html new file mode 100644 index 0000000000..79df9e595e --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/subpackage.html @@ -0,0 +1,264 @@ + + + + + + 使用分包 | Mpx框架 + + + + + + + + + +

# 使用分包

作为一个对 performance 极度重视的框架,分包作为提升小程序体验的重要能力,框架对各种类型的分包能力进行了完善支持。

分包是小程序平台提供的原生能力,mpx是对该能力做了部分加强,目前各大主流小程序平台都已支持分包,且框架在可能的情况下进行了抹平。

使用分包一定要记得阅读下面的分包注意事项

# 原生语法注册分包

Mpx 支持小程序原生语法注册分包,并且在框架层面对不同平台的的差异进行了抹平,我们以微信小程序原生语法注册分包为例。

{
+  "pages": [
+    "pages/index/index"
+  ],
+  "subPackages": [
+    {
+      "root": "test",
+      "pages": [
+        "pages/other/other",
+        "pages/other/other2"
+      ]
+    }
+  ]
+}
+

# packages 语法注册分包

Mpx 提供 packages 语法来对小程序的主包页面和分包划分能力进行增强,使用 packages 语法可以灵活的对业务进行拆分,允许以 npm 包的形式进行 +主包页面和分包注册,且分包名和页面路径可自定义,十分有利于大型多团队开发的项目维护。

# 使用方法

Mpx 拓展了 app.json 的语法,新增了 packages 域,用来声明依赖的 packages,packages 可嵌套依赖。

# 注册主包页面

首先我们介绍下 packages 注册主包页面的用法,在 packages 中直接配置资源路径,Mpx 会去读取该资源中 json 区块中的 pages 属性,合并到主包页面配置中。

// @file src/app.mpx
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/index/index"
+    ],
+    "packages": [
+      "{npmPackage || relativePathToPackage}/index"
+    ]
+  }
+</script>
+
+// @file src/packages/index.mpx
+// 注意确保页面路径的唯一性
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/other/other",
+      "./pages/other/other2"
+    ]
+  }
+</script>
+

打包结果:dist/app.json

{
+  "pages": [
+    "pages/index/index",
+    "pages/other/other",
+    "pages/other/other2"
+  ]
+}
+

# 注册分包

Mpx 会将 packages 域下的路径带 root 为 key 的 query 解析注册为分包,使用 packages 语法注册分包,只需要在 packages 中配置资源路径添加 root=xxx,root的值即为分包名。

// @file src/app.mpx
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/index/index"
+    ],
+    "packages": [
+      "{npmPackage || relativePathToPackage}/index?root=test"
+    ]
+  }
+</script>
+
+// @file src/packages/index.mpx (子包的入口文件)
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/other/other",
+      "./pages/other/other2"
+    ]
+  }
+</script>
+

打包结果:dist/app.json

{
+  "pages": [
+    "pages/index/index"
+  ],
+  "subPackages": [
+    {
+      "root": "test",
+      "pages": [
+        "pages/other/other",
+        "pages/other/other2"
+      ]
+    }
+  ]
+}
+

由上可见,经过我们的编译过程,packages 中注册的页面可按照原始的路径被合并入主包页面或注册为分包, +这样开发者可以不用考虑自己在被依赖时页面路径是怎么样的,也可以直接将调试用的app.mpx作为依赖入口直接暴露出去, +对于主app的开发者来说也不需要了解依赖内部的细节,只需要在packages中声明自己所需的依赖即可。

# 注意事项

  • 依赖的开发者在自己的入口 app.mpx 中注册页面时对于本地页面一定要使用相对路径进行注册,否则在主app中进行编译时会找不到对应的页面
  • 不管是用 json 还是 mpx 格式定义 package 入口,编译时永远只会解析 json 且只会关注 json 中的 pages 和 packages 域,其余所有东西在主app编译时都会被忽略
  • 由于我们是将 packages 中注册的页面按照原始的路径合并到主 app 当中,有可能会出现路径名冲突。
    +这种情况下编译会报出响应错误提示用户解决冲突,为了避免这种情况的发生,依赖的提供者最好将自己内部的页面放置在能够描述依赖特性的子文件夹下。

例如一个包叫login,建议包内页面文件目录为:

project
+│   app.mpx  
+└───pages
+    └───login
+        │   page1.mpx
+        │   page2.mpx
+        │   ...
+

# 独立分包

Mpx目前已支持独立分包构建,使用 packages 语法声明分包时只需要在后面添加 independent=true query 即可,同时也支持原生语法声明。 +如下方示例声明 packageA 分包为独立分包

示例:

// src/app.mpx 文件中 json 块
+
+// Mpx packages 方式
+{
+  "packages": [
+    "packageA/app.mpx?root=packageA&independent=true"
+  ]
+}
+
// 微信原生方式
+{
+  "subpackages": [
+    {
+      "root": "packageA",
+      "pages": [
+        "pages/index"
+      ],
+      "independent": true
+    },
+  ]
+}
+

需要注意的是,由于独立分包可以独立于主包和其他分包运行,从独立分包页面进入小程序时,主包中的相应初始化逻辑并不会执行,如果独立分包中多个页面需要某种通用初始化逻辑时就无法优雅的实现, +Mpx框架针对独立分包场景提供了独立分包初始化逻辑执行能力。

对于使用 packages 方式声明的独立分包,默认将 .mpx 文件自身的 script 块作为初始化逻辑执行。

<!--src/packagesA/app.mpx,packageA 独立分包入口文件-->
+<script>
+import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+
+mpx.use(apiProxy, { usePromise: true }) 
+if (isIndependent) {
+    // do some in independent package
+} else {
+    // do some not independent package
+}
+</script>
+
+<script type="application/json">
+{
+  "pages": [
+    "./pages/index"
+  ]
+}
+</script>
+

上方代码中 独立分包 packageA 的入口文件 app.mpx 中的 script block 代码会默认在独立分包初始化时执行,Mpx 同时提供了全局变量 isIndependent 标识当前代码执行环境是否为独立分包来进行特定逻辑区分

如果你不想走这个默认的初始化逻辑执行规则,想自定义一个 js 文件存储当前独立分包的初始化逻辑,我们支持 independent 配置项直接配置为初始化逻辑文件地址

// src/app.mpx 文件中 json 块
+// Mpx packages 方式
+{
+  "packages": [
+    "packageA/app.mpx?root=packageA&independent=./common" // 路径上下文为 packageA 文件夹
+  ]
+}
+
// 微信原生方式
+{
+  "subpackages": [
+    {
+      "root": "packageA",
+      "pages": [
+        "pages/index"
+      ],
+      "independent": "./common" // 路径上下文为 packageA 文件夹
+    },
+  ]
+}
+
// src/pacakgeA/common.js
+import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+
+mpx.use(apiProxy, { usePromise: true })
+if (isIndependent) {
+    // do some in independent package
+} else {
+    // do some not independent package
+}
+

注意上方配置 independent 为初始化逻辑文件地址时,路径相对地址上下文为 packageA

# 分包预下载

分包预下载是在 json中 新增一个 preloadRule 字段,mpx 打包时候会原封不动把这个部分放到 app.json 中,所以只需要按照 微信小程序官方文档 - 分包预下载 (opens new window) 或者 支付宝小程序官方文档 - 分包预下载 (opens new window) 配置即可。

示例:

// @file src/app.mpx
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/index/index"
+    ],
+    "packages": [
+      "{npmPackage || relativePathToPackage}/index?root=xxx"
+    ],
+    "preloadRule": {
+      "pages/index": {
+        "network": "all",
+        "packages": ["important"]
+      },
+      "sub1/index": {
+        "packages": ["hello", "sub3"]
+      }
+    }
+  }
+</script>
+
+// @file src/packages/index.mpx (子包的入口文件)
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/other/other",
+      "./pages/other/other2"
+    ]
+  }
+</script>
+

打包结果:dist/app.json

{
+  "pages": [
+    "pages/index/index"
+  ],
+  "subPackages": [
+    {
+      "root": "xxx",
+      "pages": [
+        "pages/other/other",
+        "pages/other/other2"
+      ]
+    }
+  ],
+  "preloadRule": {
+    "pages/index": {
+      "network": "all",
+      "packages": ["important"]
+    },
+    "sub1/index": {
+      "packages": ["hello", "sub3"]
+    }
+  }  
+}
+

# 分包注意事项

当我们使用分包加载时,依赖包内的跳转路径需注意,比如要跳转到other2页面

  • 不用分包时会是:/pages/other/other2
  • 使用分包后应为:/test/pages/other/other2

即前面会多?root={rootKey}的rootKey这一层

为了解决这个问题,有三种方案:

  • import的时候在最后加'?resolve', 例如: import testPagePath from '../pages/testPage.mpx?resolve' , 编译时就会把它处理成正确的完整的绝对路径。

  • 使用相对路径跳转。

  • 定死使用的分包路径名,直接写/{rootKey}/pages/xxx (极度不推荐,尤其在分包可能被多方引用的情况时)

这里我们建议使用第一种方式。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html b/docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html new file mode 100644 index 0000000000..7fc7a64234 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html @@ -0,0 +1,139 @@ + + + + + + 使用原子类 | Mpx框架 + + + + + + + + + +

# 使用原子类

原子类(utility-first CSS)是近几年流行起来的一种全新的样式开发方式,在前端社区内取得了良好的口碑,越来越多的主流网站也基于原子类进行开发,我们耳熟能详的有Github (opens new window)OpenAI (opens new window)Netflix (opens new window) +和NASA官网 (opens new window) +等。使用原子类离不开原子类框架的支持,常用的原子类框架有 Tailwindcss (opens new window)Windicss (opens new window) +和 Unocss (opens new window) 等,而在 Mpx2.9 以后,我们在框架中内置了基于 unocss +的原子类支持,让小程序开发也能使用原子类。对项目进行简单配置开启原子类支持后,用户就可以在 Mpx +页面/组件模板中直接使用一些预定义的基础样式类,诸如flex,pt-4,text-center 和 rotate-90 等,对样式进行组合定义,并且在 Mpx 支持的所有小程序平台和 web 平台中正常运行,下面是一个简单示例:


+<view class="container">
+  <view class="flex">
+    <view class="py-8 px-8 inline-flex mx-auto bg-white rounded-xl shadow-md">
+      <view class="text-center">
+        <view class="text-base text-black font-semibold mb-2">
+          Erin Lindford
+        </view>
+        <view class="text-gray-500 font-medium pb-3">
+          Product Engineer
+        </view>
+        <view
+          class="mt-2 px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-solid border-purple-200">
+          Message
+        </view>
+      </view>
+    </view>
+  </view>
+</view>
+

通过这种方式,我们在不编写任何自定义样式代码的情况下得到了一张简单的个人卡片,实际渲染效果如下:

utility-css-demo

相较于传统的自定义类编写样式的方式,使用原子类能给你带来以下这些好处:

  • 不用再烦恼于为自定义类取类名,传统样式开发中,我们仅仅是为某个元素定义样式就需要绞尽脑汁发明一些抽象的类名,还得提防类名冲突,使用原子类可以完全将你从这种琐碎无趣的工作中解放;
  • 停止css体积的无序增长,传统样式开发中,css体积会随着你的迭代不断增长,而在原子类中,一切样式都可以复用,你几乎不需要编写新的css;
  • 让调整样式变得更加安全,传统css是全局的,当你修改某个样式时无法保障其不会破坏其他地方的样式,而你在模板中使用的原子类是本地的,你完全不用担心修改它会影响其他地方。

而相较于使用内联样式,原子类也有一些重要的优势:

  • 约束下的设计,使用内联样式时,里面的每一个数值都是魔法数字(magic number) +,而通过原子工具类,你可以选择一些符合预定义设计规范的样式,便于构筑具有视觉一致性的UI;
  • 响应式设计,你无法在内联样式中使用媒体查询,然而通过原子类框架中提供的响应式工具,你可以轻而易举地构建出响应式界面;
  • Hover、focus和其他状态,使用内联样式无法定义特定状态下的样式,如hover和focus,通过原子类框架的状态变量能力,我们可以轻松为这些状态定义样式。

看到这里相信你已经迫不及待地想要在 Mpx 中体验原子类开发了吧,可以根据下面的指南开启你的原子类之旅。

# 原子类环境配置

如果你想在新项目中使用原子类,可以使用最新版本的 @mpxjs/cli 创建项目,在 prompt 中选择使用原子类,就可以在新创建的项目模版中直接使用 unocss 的原子类,关于可使用的工具类可参考 unocss 交互示例 (opens new window)及本指南下方的工具类支持范围

与 web 中使用 unocss 不同,在 Mpx 中使用 unocss 不需要显式引入虚拟模块 import 'uno.css' 来承载生成的样式内容,这是由于在 +Mpx 中,我们充分考虑到小程序分包架构的特殊性和主包体积的重要性,结合 Mpx +强大的分包构建能力,对生成的原子工具类的使用情况进行分析,将其自动注入到合适的主包或者分包中,来达到全局体积分配的最优(在没有内容冗余的情况下尽可能输出到分包)。

对于使用 @mpxjs/cli@3.0 新版脚手架创建的项目,可以在项目初始化时选择需要使用原子类 +选项,或在已有项目下执行mpx add @mpxjs/vue-cli-plugin-mpx-utility-first-css以激活原子类相关编译配置。

对于使用旧版脚手架创建的项目,可以通过修改项目编译配置以激活原子类支持:

  1. 安装相关依赖:
{
+  //...
+  "devDependencies": {
+    "@mpxjs/unocss-base": '^2.9.0',
+    "@mpxjs/unocss-plugin": '^2.9.0'
+  }
+}
+
  1. 新建uno.config.js,基础配置内容如下:
const { defineConfig } = require('unocss')
+const presetMpx = require('@mpxjs/unocss-base')
+
+module.exports = defineConfig({
+  presets: [
+    presetMpx()
+  ]
+})
+
  1. 注册MpxUnocssPlugin,在build/getPlugins中添加如下代码:
const MpxUnocssPlugin = require('@mpxjs/unocss-plugin')
+// ...
+plugins.push(new MpxUnocssPlugin())
+

即可在项目模版中使用基于unocss的原子类功能,unocss默认的preset兼容tailwindcss/windicss +,可以通过查阅tailwindcss文档 (opens new window)windicss文档 (opens new window)unocss可交互文档 (opens new window)进行探索使用。

关于uno.config.js可用配置项及@mpxjs/unocss-plugin@mpxjs/unocss-base的配置项请参考API文档

# vscode插件支持

推荐使用unocss官方插件https://unocss.dev/integrations/vscode +mpx文件则需要在unocss.config.js添加如下参数才能生效

const { defineConfig } = require('unocss')
+
+module.exports = defineConfig({
+  include: [/\.mpx($|\?)/]
+})
+

# 功能支持范围

我们支持了unocss大部分的配置项及功能,并针对小程序技术规范提供了一些额外的功能支持,如分包输出和样式隔离等功能,以下为详细功能支持范围。

功能 支持度 备注
Load Config 支持 支持windi默认的全部配置文件路径,并支持在plugin options中手动传入配置对象或配置文件路径
Rules 支持
Shortcuts 支持
Theme 支持
Variants 支持
Extractors 不支持
Transformers 部分支持 内建支持variant groups、directives和alias,不支持自定义transformers
Preflights 支持 支持内建和自定义preflight配置
Presets 支持
Safelist 支持 支持全局配置和模版注释语法局部配置声明safelist
Value Auto-infer 支持
Variant Groups 支持
Directives 支持
Alias 支持
Attributify Mode 不支持 小程序模版不支持不识别的自定义属性
Responsive Design 支持
Dark Mode 支持
Important Prefix 支持
Layers Ordering 支持
分包输出/公共样式抽离 支持 可通过设置@mpxjs/unocss-plugin配置项minCount决定公共样式范围
组件分包异步/组件样式隔离 支持 可通过全局配置和模版注释语法局部配置styleIsolation='isolated'支持相关场景的原子类使用
Rpx样式单位 支持
小程序类名特殊字符转义 支持 静态类名和动态类名均以支持
跨平台输出 支持 支持输出Mpx已支持的所有小程序平台及Web

# 工具类可用范围

经过我们的详细测试,大部分unocss +提供的工具类都能在小程序环境下正常工作,但是也有部分工具类由于小程序底层环境差异无法正常运行,以下是详细的测试结果,参考windicss文档 (opens new window) +进行分类组织。

# General

功能 支持度 备注
Colors 支持
Typography 部分支持 不支持子项:Font Variant Numeric Tab Size
SVG 不支持 小程序不支持svg标签
Variants 部分支持 不支持子项:Child Selectors,因为小程序不支持*选择器

# Accessibility

功能 支持度 备注
Screen Readers 支持

# Animations

功能 支持度 备注
Animation 支持
Transforms 支持
Transitions 支持

# Backgrounds

功能 支持度 备注
Background Attachment 支持
Background Clip 支持
Background Color 支持
Background Opacity 支持 需要和Background Color一起使用
Background Position 支持
Background Repeat 支持
Background Size 支持
Background Origin 支持
Gradient Direction 支持
Gradient From 支持
Gradient Via 支持
Gradient To 支持
Background Blend Mode 支持

# Behaviors

功能 支持度 备注
Box Decoration Break 支持
Image Rendering 部分支持 image-render-edge 真机不支持
Listings 支持
Overflow 支持
Overscroll Behavior 支持
Placeholder 不支持 微信小程序 input 需要通过 placeholder-style 设置 placeholder 样式

# Borders

功能 支持度 备注
Border Radius 支持
Border Width 支持
Border Color 支持
Border Opacity 支持
Border Style 支持
Divider Width 不支持 小程序不支持生成的css选择器
Divider Color 不支持 小程序不支持生成的css选择器
Divider Opacity 不支持 小程序不支持生成的css选择器
Divider Style 不支持 小程序不支持生成的css选择器
Outline 支持
Outline Solid 支持
Outline Dotted 支持
Ring Width 支持
Ring Color 支持
Ring Opacity 支持
Ring Offset Width 支持
Ring Offset Color 支持

# Effects

功能 支持度 备注
Box Shadow 支持
Opacity 支持
Mix Blend Mode 支持

# Filters

功能 支持度 备注
Filter 支持
Backdrop Filter 支持

# Interactivity

功能 支持度 备注
Accent Color 不支持 小程序不支持生成的样式
Appearance 不支持 小程序不支持生成的样式
Cursor 不支持 小程序不支持生成的样式
Caret 不支持 小程序不支持生成的样式
Pointer Events 支持
Resize 不支持 小程序不支持生成的样式
Scroll Behavior 不支持 小程序不支持生成的样式
Touch Action 支持
User Select 不支持 小程序不支持生成的样式
Will Change 不支持 小程序不支持生成的样式

# Layout

功能 支持度 备注
Columns 支持
Container 支持
Display 支持
Flex 支持
Grid 支持
Positioning 部分支持 小程序图片设置object-fit 无效,可使用mode代替
Sizing 支持
Spacing 部分支持 Space Between小程序不支持生成的css选择器
Tables 部分支持 小程序不支持部分table样式

# 小程序原子类使用注意点

小程序由于底层环境差异,我们在支持和使用原子类时有一些特殊的注意点。

# 特殊字符转义

基于unocss的原子类支持value auto-infer(值自动推导),可以在模版中根据相关规则书写灵活的自定义值原子类,如p-5px bg-[hsl(211.7,81.9%,69.6%)]等,针对原子类中出现的[ ( ,等特殊字符,在web中会通过转义字符\进行转义,由于小程序环境下不支持css选择器中出现\转义字符,我们内置支持了一套不带\的转义规则对这些特殊字符进行转义,同时替换模版和css文件中的类名,内建的默认转义规则如下:

const escapeMap = {
+    '(': '_pl_',
+    ')': '_pr_',
+    '[': '_bl_',
+    ']': '_br_',
+    '{': '_cl_',
+    '}': '_cr_',
+    '#': '_h_',
+    '!': '_i_',
+    '/': '_s_',
+    '.': '_d_',
+    ':': '_c_',
+    ',': '_2c_',
+    '%': '_p_',
+    '\'': '_q_',
+    '"': '_dq_',
+    '+': '_a_',
+    $: '_si_',
+    // unknown用于兜底不在上述范围中未知的转义字符
+    unknown: '_u_'
+  }
+

与此同时,用户也可以通过传递@mpxjs/unocss-pluginescapeMap配置项来覆盖内建的转义规则。

# 原子类分包输出

在web中,原子类会被全部打包输出单个样式文件,一般会放置在顶层样式表中以供全局访问,但在小程序中这种全量的输出策略并不是最优的,主要原因在于小程序中可供全局访问的主包体积存在2M大小限制,主包体积十分紧缺珍贵,Mpx在构建输出时遵循着分包优先的原则,尽可能充分利用分包体积从而减少对主包体积的占用,再进行原子类产物输出时,我们也遵循了相同的原则。

在Mpx中,我们在收集原子类时同时记录了每个原子类的引用分包,在收集结束后根据每个原子类的分包引用数量决定该原子类应该输出到主包还是分包当中,我们在@mpxjs/unocss-plugin中提供了minCount配置项来决定分包的输出规则,该配置项的默认值为2,即当一个原子类被2个或以上分包引用时,会被作为公共原子类抽取到主包中,否则输出到所属分包中,这也是全局最优的策略。当我们想要让原子类输出产物更少地占用主包体积时,我们也可以将minCount值调大,让原子类抽取到主包的条件更加苛刻,不过这样也会伴随着原子类分包冗余的增加。

unocss.config.js配置中定义的safelist原子类默认会输出到主包,为了组件局部使用的safelist有输出到分包的机会,我们在模版中提供了注释配置(comments config),灵感来源于webpack中的魔法注释(magic comments),用户可以在组件模版中通过注释配置声明当前组件所需的safelist,对应的原子类也会根据上述的规则输出到主包或分包中,使用示例如下:

<template>
+    <!-- mpx_config_safelist: 'text-red-500 bg-blue-500' -->
+    <!-- 动态样式中可以使用text-red-500和bg-blue-500原子类 -->
+    <view wx:class="{{classObj}}">test</view>
+    <!-- ... -->
+</template>
+

# 样式隔离与组件分包异步

在小程序中,自定义组件的样式默认是隔离的,web中通过全局样式访问原子类的方式不再生效,不过由于小程序提供了样式隔离配置 (opens new window),我们可以将该组件样式隔离配置调整为apply-shared来获取页面或app中定义的原子类,但是当我们在使用传统类名和原子类混合开发或者迁移原子类的过程中,我们往往希望保留原本自定义组件的样式隔离。

针对这种情况,我们在@mpxjs/unocss-plugin中提供了styleIsolation配置项,可选设置为isolated|apply-shared,当设置为isolated时每个组件都会通过@import独立引用主包或者分包的原子类样式文件,因此不会受到样式隔离的影响;当设置为apply-shared时,只有app和分包页面会引用对应的原子类样式文件,自定义组件需要通过配置样式隔离为apply-shared使原子类生效。

在组件分包异步的情况下对应组件即使将样式隔离配置为apply-shared的情况下,@mpxjs/unocss-plugin也需要将styleIsolation设置为isolated才能正常工作,原因在于组件分包异步的情况下,组件被其他分包的页面所引用渲染,由于上述原子类样式分包输出的规则,其他分包的页面中可能并不包含当前组件所需的原子类,只有在isolated模式下由组件自身引用所需的原子类样式才能保证正常work,类似于safelist,我们也提供了注释配置的方式对组件的styleIsolation模式进行局部配置,示例如下:

<template>
+    <!-- mpx_config_styleIsolation: 'isolated' -->
+    <!-- 当前组件会直接引用对应主包或分包的原子类样式 -->
+     <view class="@dark:(text-white bg-dark-500)">
+    <!-- ... -->
+</template>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html b/docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html new file mode 100644 index 0000000000..b8db15d7b5 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html @@ -0,0 +1,131 @@ + + + + + + 类名样式绑定 | Mpx框架 + + + + + + + + + +

# 类名样式绑定

Mpx利用wxs完整实现了Vue中的类名样式绑定,性能优良且没有任何使用限制(很多小程序框架基于字符串解析来实现该能力,只支持在模板上写简单的字面量,大大限制了使用场景)

# 类名绑定

类名绑定的增强指令是wx:class,可以与普通的class属性同时存在,在视图渲染中进行合成。

# 对象语法

wx:class中传入对象,key值为类名,value值控制该类名是否生效。

<template>
+  <!--支持传入对象字面量,此处视图的class="outer active"-->
+  <view class="outer" wx:class="{{ {active:active, disabled:disabled} }}">
+    <!--直接直接传入对象数据,此处视图的class="inner selected"-->
+    <view class="inner" wx:class="{{innerClass}}"></view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      active: true,
+      disable: false,
+      innerClass: {
+        selected: true
+      }
+    }
+  })
+</script>
+

# 数组语法

wx:class中传入字符串数组,字符串为类名。

<template>
+  <!--支持传入数组字面量,此处视图的class="outer active danger"-->
+  <view class="outer" wx:class="{{ ['active', 'danger'] }}">
+    <!--直接直接传入数组数据,此处视图的class="inner selected"-->
+    <view class="inner" wx:class="{{innerClass}}"></view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      innerClass: ['selected']
+    }
+  })
+</script>
+

# 样式绑定

样式的增强指令是wx:style,可以与普通的style属性同时存在,在视图渲染中进行合成。

# 对象语法

wx:style中传入用样式对象,带有横杠的样式名可以用驼峰写法来代替

<template>
+  <!--支持传入对象字面量,模板会显得杂乱,此处视图的style="color:red;font-size:16px;font-weight:bold;"-->
+  <view style="color:red;" wx:style="{{ {fontSize:'16px', fontWeight:'bold'} }}">
+    <!--更好的方式是直接传入对象数据,此处视图的style="color:blue;font-size:14px;"-->
+    <view wx:style="{{innerStyle}}"></view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      innerStyle: {
+        color: 'blue',
+        fontSize: '14px'
+      }
+    }
+  })
+</script>
+

# 数组语法

wx:style同样支持传入数组将多个样式合成应用到视图上

<template>
+  <!--此处视图的style="color:blue;font-size:14px;background-color:red;"-->
+  <view wx:style="{{ [baseStyle, activeStyle] }}">
+
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      baseStyle: {
+        color: 'blue',
+        fontSize: '14px'
+      },
+      activeStyle:{
+        backgroundColor: 'red'
+      }
+    }
+  })
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/component.html b/docs-vuepress/.vuepress/dist/guide/basic/component.html new file mode 100644 index 0000000000..6b1f590c6b --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/component.html @@ -0,0 +1,117 @@ + + + + + + 自定义组件 | Mpx框架 + + + + + + + + + +

# 自定义组件

Mpx中的自定义组件完全基于小程序原生的自定义组件支持,与此同时,Mpx提供的数据响应和模板增强等一系列增强能力都能在自定义组件中使用。

原生自定义组件的规范详情查看这里 (opens new window)

# 动态组件

Mpx中提供了使用方法类似于 Vue 的动态组件能力,这是一个基于 wx:if 实现的语法。通过对 is 属性进行动态绑定,可以实现在同一个挂载点切换多个组件,前提需要动态切换的组件已经在全局或者组件中完成注册。 +使用示例如下:

<view>
+  <!-- current为组件名称字符串,可选范围为局部注册的自定义组件和全局注册的自定义组件 -->
+  <!-- 当 `current`改变时,组件也会跟着切换  -->
+  <component is="{{current}}"></component>
+</view>
+
+<script>
+  import {createComponent} from '@mpxjs/core'
+  createComponent({
+    data: {
+      current: 'test'
+    },
+    ready () {
+      setTimeout(() => {
+        this.current = 'list'
+      }, 3000)
+    }
+  })
+</script>
+
+<script type="application/json">
+  {
+    "usingComponents": {
+      "list": "../components/list",
+      "test": "../components/test"
+    }
+  }
+</script>
+

# slot

在组件中使用slot(插槽)可以使我们封装的组件更具有可扩展性,Mpx完全支持原生插槽的使用。

简单示例如下:

<!-- 组件模板 -->
+<!-- components/mySlot.mpx -->
+
+<view>
+  <view>这是组件模板</view>
+  <slot name="slot1"></slot>
+  <slot name="slot2"></slot>
+</view>
+

下面是引入 mySlot 组件的页面

<!-- index.mpx -->
+
+<template>
+  <view>
+    <my-slot>
+      <view slot="slot1">我是slot1中的内容</view>
+      <view slot="slot2">我是slot2中的内容</view>
+    </my-slot>
+  </view>
+</template>
+
+<script>
+import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  options: {
+    multipleSlots: true // 启用多slot支持
+  },
+  // ...
+})
+</script>
+
+<script type="application/json">
+  {
+    "usingComponents": {
+      "my-slot": "components/mySlot"
+    }
+  }
+</script>
+

更多详情可查看这里 (opens new window)

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html b/docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html new file mode 100644 index 0000000000..0bf8e876a2 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html @@ -0,0 +1,72 @@ + + + + + + 条件渲染 | Mpx框架 + + + + + + + + + +

# 条件渲染

Mpx中的条件渲染与原生小程序中完全一致,详情可以查看这里 (opens new window)

简单示例如下:

<template>
+  <view class="container">
+    <!-- 通过 wx:if 的语法来控制需要渲染的元素 -->
+    <view wx:if="{{ score > 90 }}"> A </view>
+    <view wx:elif="{{ score > 60 }}"> B </view>
+    <view wx:else> C </view>
+
+    <!-- 通过 wx:show 来控制元素的显示隐藏-->
+    <view wx:show="{{ score > 90 }}"> very good! <view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      score: 80
+    }
+  })
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/css.html b/docs-vuepress/.vuepress/dist/guide/basic/css.html new file mode 100644 index 0000000000..d862ed1e58 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/css.html @@ -0,0 +1,173 @@ + + + + + + CSS 处理 | Mpx框架 + + + + + + + + + +

# CSS 处理

# CSS 预编译

Mpx 支持 CSS 预编译处理,你可以通过在 style 标签上设置 lang 属性,来指定使用的 CSS 预处理器,此外需要在对应的 webpack 配置文件中 +加入对应的 loader 配置

<!-- 使用 stylus -->
+<style lang="stylus">
+  .nav
+    width 100px
+    height 80px
+    color #f90
+    &:hover
+      background-color #f40
+      color #fff
+</style>
+
+// getRules 配置文件
+rules: [
+    {
+        test: /\.styl(us)?$/,
+        use: [
+            MpxWebpackPlugin.wxssLoader(),
+            'stylus-loader'
+        ]
+    }
+]
+
<!-- 使用 sass  -->
+<style lang="scss">
+  .nav {
+    width: 100px;
+    height: 80px;
+    color: #f90
+    &:hover {
+      background-color: #f40;
+      color: #fff
+    }
+  }
+</style>
+
+// getRules 配置文件
+rules: [
+    {
+        test: /\.scss$/,
+        use: [
+            'css-loader',
+            'sass-loader'
+        ]
+    }
+]
+
<!-- 使用 less -->
+<style lang="less">
+  .size {
+    width: 100px;
+    height: 80px
+  }
+  .nav {
+    .size();
+    color: #f90;
+    &:hover {
+      background-color: #f40;
+      color: #fff
+    }
+  }
+</style>
+
+// getRules 配置文件
+rules: [
+    {
+        test: /\.less$/,
+        use: [
+            'css-loader',
+            'less-loader'
+        ]
+    }
+]
+
+

# 公共样式复用

为了达到最大限度的样式复用,Mpx 提供了以下两种方式实现公共样式抽离,但是最终打包的效果有所区别。

// styles/mixin.styl
+.header-styl
+  width 100%
+  height 100px
+  background-color #f00
+

# style src 复用

通过给 style 标签添加 src 属性引入外部样式,最终公共样式代码只会打包一份。

<!-- index.mpx -->
+<style lang="stylus" src="../styles/common.styl"></style>
+
<!-- list.mpx -->
+<style lang="stylus" src="../styles/common.styl"></style>
+

Mpx 将 common.styl 中的代码经过 loader 编译后生成一份单独的 wxss 文件,这样既实现了样式抽离,又能节省打包后的代码体积。

# @import 复用

如果指定 style 标签的 lang 属性并且使用 @import 导入样式,那么这个文件经过对应的 loader 处理之后的内容会重复打包到引用它的文件目录下,并不会抽离成单独的文件,这样无形中增加了代码体积。

<!-- index.mpx -->
+<style lang="stylus">
+  @import "../styles/mixin.styl"
+</style>
+
<!-- list.mpx -->
+<style lang="less">
+  @import "../styles/mixin.less";
+</style>
+

如果导入的是一份 css 文件,则最终打包后的效果与 style src 一致。

// styles/mixin.css
+.header-css {
+  width: 100%;
+  height: 100px;
+  background-color: #f00;
+}
+
<!-- index.mpx -->
+<style>
+  @import "../styles/mixin.css";
+</style>
+
<!-- list.mpx -->
+<style>
+  @import "../styles/mixin.css";
+</style>
+

对于多个页面或组件公用的样式,建议使用 style src 形式引入,避免一份样式被内联打成多份,同时还能使用 less、scss 等提升编码效率。

# CSS 压缩

在 production 模式下,框架默认会使用 cssnano (opens new window) 对 css 内容进行压缩。

框架默认内置 cssnano 的 default 预设,默认配置为:

cssnanoConfig = {
+  preset: ['cssnano-preset-default', minimizeOptions.optimisation || {}]
+}
+

以上配置为框架内置,开发者无需手动配置。

如果你想要使用 cssnano advanced 预设,则需要在 wxssLoader 中传入配置开启

{
+  test: /\.(wxss|acss|css|qss|ttss|jxss|ddss)$/,
+  use: [
+    MpxWebpackPlugin.wxssLoader({
+      minimize: {
+        advanced: true, // 使用 cssnano advanced preset
+        optimisation: {
+          'autoprefixer': true,
+          'discardUnused': true,
+          'mergeIdents': true
+        }
+      }
+    })
+  ]
+},
+

同时也需要安装 advanced 依赖:

npm i -D cssnano-preset-advanced
+

optimisation 配置可以点击详情 (opens new window)查看更多配置项。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/event.html b/docs-vuepress/.vuepress/dist/guide/basic/event.html new file mode 100644 index 0000000000..2ad5251974 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/event.html @@ -0,0 +1,90 @@ + + + + + + 事件处理 | Mpx框架 + + + + + + + + + +

# 事件处理

Mpx在事件处理上基于原生小程序,支持原生小程序的全部事件处理技术规范,在此基础上新增了事件处理内联传参的增强机制。

原生小程序事件处理详情请参考这里 (opens new window)

增强的内联传参能力对于传递参数的个数和类型没有特殊限制,可以传递各种字面量,可以传递组件数据,甚至可以传递for中的item和index, +当内联事件处理器中需要访问原始事件对象时,可以传递$event特殊关键字作为参数,在事件处理器的对应参数位置即可获取。

示例如下:

<template>
+  <view>
+    <!--原生小程序语法,通过dataset进行传参-->
+    <button data-name="a" bindtap="handleTap">a</button>
+    <!--Mpx增强语法,模板内联传参,方便简洁-->
+    <button bindtap="handleTapInline('b')">b</button>
+    <!--参数支持传递字面量和组件数据-->
+    <button bindtap="handleTapInline(name)"></button>
+    <!--参数同样支持传递for作用域下的item/index-->
+    <button wx:for="{{names}}" bindtap="handleTapInline(item)">{{item}}</button>
+    <!--需要使用原始事件对象时可以传递$event特殊关键字-->
+    <button bindtap="handleTapInlineWithEvent('g', $event)">g</button>
+  </view>
+</template>
+
+<script>
+  import { createComponent } from '@mpxjs/core'
+
+  createComponent({
+    data: {
+      name: 'c',
+      names: ['d', 'e', 'f']
+    },
+    methods: {
+      handleTap (e) {
+        console.log('name:', e.target.dataset.name)
+      },
+      // 直接通过参数获取数据,直观方便
+      handleTapInline (name) {
+        console.log('name:', name)
+      },
+      handleTapInlineWithEvent (name, e) {
+        console.log('name:', name)
+        console.log('event:', e)
+      }
+    }
+  })
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/ide.html b/docs-vuepress/.vuepress/dist/guide/basic/ide.html new file mode 100644 index 0000000000..1a6dc86a12 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/ide.html @@ -0,0 +1,67 @@ + + + + + + IDE 高亮配置 | Mpx框架 + + + + + + + + + +

# IDE 高亮配置

# IntelliJ

如果使用 IntelliJ 系 IDE 开发,可将.mpx后缀文件关联到vue模板类型,按vue模板解析。

关联文件类型

但会报一个warning提示有重复的script标签,关闭该警告即可。

关闭警告

# vscode

git地址 (opens new window),有任何vscode插件的问题和需求可在仓库中提issue

下载

  1. 下载地址 (opens new window)

  2. 也可直接在vscode扩展处搜索mpx即可下载

使用

# 插件功能介绍

  • 高亮
  • emmet
  • 跳转定义
  • 自动补全
  • eslint
  • 格式化

视频介绍 (opens new window)

# 高亮

  与其他语言插件无异,提供相应代码的高亮,因为Mpx分为四个模块,所以每个模块都有相应的语法高亮,还包括注释快捷键,也区分了相应模块,比如<template>中使用的是html的高亮,且注释是<!-- -->,而<script>中就是js的高亮,注释是//

image

# emmet

  早在使用sublime时就在使用emmet插件,以提高写HTML的效率。

  比如键入多个<view>标签:view*n

  比如一些标签的快速键入,配合tab或者Enter键快速键入

  不仅仅是<template>模块,css,scss,less,stylus,sass模块也有相应的快捷指令

image image

提示组件标签

我们可以像编写 html 一样,只要输入对应的单词就会出现对应的标签,比如输入的是 view ,然后按下 tab 键,即可输入 <view></view> 标签。

图片名称

组件指令提示

指令的提示类似于 vue 文件一样,只要输入对应的指令前缀就会出现对应的完整指令,比如输入的是 wx ,然后按下 tab 键,就可以输入 wx:if="" 指令。

图片名称

组件属性提示

微信小程序的每个组件都有一些属性选项,在编写组件的时候输入前缀就会出现完整的属性,并且包含了属性的说明和属性的类型。

图片名称

组件事件提示

给组件绑定事件,也是只需要输入事件的前缀,就会出现完整的事件列表,然后按下 tab 键,即可输入 bindtap="" 类似的事件。

图片名称

# 跳转定义

  command + 鼠标左键 查看定义位置,也可以在当前文件查看内容,决定是否跳转

image

# 自动补全

  毕竟Mpx是个小程序的框架,对于微信和支付宝的api快速补全snippets没有怎么能行,可在<script>中通过键入部分文字插入相应的代码块

image

# eslint

  eslint这块要分两部分来讲,一部分是插件实现了按照模块区分的简单的eslint,另一部分是要配合eslint的vscode插件,配置.eslintrc高阶的eslint检测。

部分一可通过配置开关

<template>是通过我们自己实现的eslint插件eslint-plugin-mpx,通过调eslint提供的引擎api,返回eslint校验的结果,我们再进行展示。

<script>中是通过调用typescript提供的检测js代码的api来进行检测,返回 +的校验结果也是不太符合语法的,基础的检测,不会过于苛刻

<style>中会根据lang的设定进行相应的检测,此检测是vscode官方提供的库 +vscode-css-languageservice

<json>模块同tempalte,用到了一个eslint插件eslint-plugin-jsonc来检测json的部分

image

部分二可参照此链接 (opens new window)配置

# 代码格式化

支持代码格式化 JavaScript (ts)· JSON · CSS (less/scss/stylus) · WXML,通过鼠标右键选择代码格式化文档。

图片名称

默认每个区块都是调用 Prettier 这个库来完成格式化的,当然也可以在设置中切换成使用其他库。

图片名称

如果切换成 none 将会禁用格式化。

Prettier 支持从项目根目录读取 .prettierrc 配置文件。配置选项可以参考 官方 (opens new window) 文档。.prettierrc 文件可以使用 JSON 语法编写,比如下面这样:

{
+  "tabWidth": 4,
+  "semi": false,
+  "singleQuote": true
+}
+

注意:由于 Prettier 这个库不支持格式化 stylus 语法,所以 stylus 的格式化使用另外一个 stylus-supremacy 库,配置 stylus 格式化规则只能在编辑器的 settings 中配置。

"mpx.format.defaultFormatterOptions": {
+  "stylus-supremacy": {
+    "insertColons": false, // 不使用括号
+    "insertSemicolons": false, // 不使用冒号
+    "insertBraces": false, // 不使用分号
+    "insertNewLineAroundImports": true, // import之后插入空行
+    "insertNewLineAroundBlocks": false // 每个块不添加空行
+  }
+}
+

总结一下,配置格式化有两种方式,一种是使用 .prettierrc 文件的形式配置,另一种是在编辑器的 settings 中自行配置,通过 mpx.format.defaultFormatterOptions 选项。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/intro.html b/docs-vuepress/.vuepress/dist/guide/basic/intro.html new file mode 100644 index 0000000000..f96f40a674 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/intro.html @@ -0,0 +1,51 @@ + + + + + + 介绍 | Mpx框架 + + + + + + + + + +

# 介绍

# Mpx是什么?

Mpx是一款致力于提高小程序开发体验和开发效率的增强型小程序框架,通过Mpx,我们能够高效优雅地开发出具有极致性能的优质小程序应用,并将其输出到各大小程序平台和web平台中运行。

Mpx的核心设计理念在于增强,这意味着Mpx是对小程序原生开发标准的补强和扩充,同时也兼容了原生开发标准。Mpx的一个设计理念在于尽可能地依赖小程序原生的自有能力,如路由系统,自定义组件,事件系统和slot等能力,因此用户在使用Mpx开发小程序时,需要对原生小程序开发有一定程度的掌握。所幸小程序开发本身并不困难,我们也会在本文档中对必要的原生小程序开发知识进行一定程度地说明。

微信小程序作为小程序的开山鼻祖,具有最完善的生态和最全面的特性支持,后来的所有小程序平台在技术方案和代码语法上都与微信小程序高度相似,Mpx目前完善支持了以微信小程序增强语法为base的跨平台输出能力,本文档在介绍原生小程序开发相关的部分时也会以微信小程序为例。

最后,Mpx是一个开发框架,不是组件库,这一点经常会有开发者搞混。Mpx兼容业内已有的小程序组件库,如vant/iview等,我们之后也会开源内部基于Mpx开发的跨端组件库。

# Mpx提供了哪些能力?

# 单文件开发(SFC)

Mpx使用类似Vue的单文件开发模式,小程序原本的template/js/style/json都可以写在单个的.mpx文件中,清晰便捷。

# 数据响应

数据响应是Mpx提供的核心增强能力,该能力主要受Vue的启发,主要包含数据赋值响应,watch api和computed计算属性等能力,关于该能力更详细的介绍可以查看这里

# 增强的模板语法

同样受到Vue的启发,Mpx提供了很多增强模板语法便于开发者方便快捷地进行视图开发,主要包含以下:

# 极致性能

Mpx在性能上做到了极致,我们在框架中通过模板数据依赖收集进行了深度的setData优化,做到了程序上的最优,让用户能够专注于业务开发;

其次,Mpx的编译构建完全基于依赖收集,支持按需进行的npm构建,能够自动根据用户的分包配置抽离共用模块,确保用户最终产出项目的包体积最优;

最后,Mpx的运行时框架部分仅占用51KB;

Mpx和业内其他框架的运行时性能对比可以参考这篇文章 (opens new window)

# 状态管理

Mpx借鉴Vuex的设计实现一套与框架搭配使用的状态管理(store)工具,除了支持Vuex中已有的特性外,我们还创新地提出了一种多实例store的跨团队状态管理模式,我们在业务中实际使用后普遍认为该设计比原有的modules更加灵活方便,更多详情可以查看这里

# 编译构建

Mpx的编译构建以webpack为基础,针对小程序项目结构深度定制开发了一个webpack插件和一系列loaders,整个构建过程完全基于依赖收集按需打包,兼容大部分webpack自身能力及生态,此外Mpx的编译构建还支持以下能力:

# 跨平台能力

Mpx支持全部小程序平台(微信,支付宝,百度,头条,qq)的增强开发,同时支持将一份基于微信增强的业务源码输出到全部的小程序平台和web平台中运行,也即将支持输出快应用的能力,更多详情请查看这里

# 完善的周边能力

除了上述的核心能力外,Mpx还提供了丰富的周边能力支持,主要包括以下能力:

Mpx具有以下功能特性:

# 对比其他小程序框架

目前业内的小程序框架主要分为两类,一类是以uniapp,taro2为代表的静态编译型框架,这类框架以静态编译为主要手段,将React和Vue开发的业务源码转换到小程序环境中进行适配运行。这类框架的主要优点在于web项目迁移方便,跨端能力较强。但是由于React/Vue等web框架的DSL与小程序本身存在较大差距,无法完善支持原web框架的全部能力,开发的时候容易踩坑。

另一类是以kbone,taro3为代表的运行时框架,这类框架利用小程序本身提供的动态渲染能力,在小程序中模拟出web的运行时环境,让React/Vue等框架直接在上层运行。这类框架的优点在于web项目迁移方便,且在web框架语法能力的支持上比静态编译型的框架要强很多,开发时遇到的坑也会少很多。但是由于模拟的web运行时环境带来了巨大的性能开销,这类框架并不适合用于大型复杂的小程序开发。

不同于上面两类框架,Mpx以小程序本身的DSL为基础,通过编译和运行时手段结合对其进行了一系列拓展增强,没有复杂庞大的转译和环境抹平,在提升用户开发体验和效率的同时,既能保障开发的稳定和可预期性,又能保障接近原生的良好性能,非常适合开发大型复杂的小程序应用。

在跨端方面,Mpx重点保障跨小程序平台的跨端能力,由于各家小程序标准具有很强的相似性,Mpx在进行跨端输出时,以静态编译为主要手段,辅以灵活便捷的条件编译,保障了跨端输出的性能和可用性。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/list-render.html b/docs-vuepress/.vuepress/dist/guide/basic/list-render.html new file mode 100644 index 0000000000..def7b0a120 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/list-render.html @@ -0,0 +1,144 @@ + + + + + + 列表渲染 | Mpx框架 + + + + + + + + + +

# 列表渲染

Mpx中的列表渲染与原生小程序中完全一致,详情可以查看这里 (opens new window)

值得注意的是wx:key与Vue中的key属性的区别,不能使用数据绑定,只能传递普通字符串将数组item中的对应属性作为key,或者传入保留关键字*this将item本身作为key

下面是简单示例:

<template>
+  <!-- 使用数组中元素的 id属性/保留关键字*this 作为key值  -->
+  <view wx:for="{{titleList}}" wx:key="id">
+    <!-- item 默认代表数组的当前项 -->
+    <view>{{item.id}}: {{item.name}}</view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      titleList: [
+        { id: 1, name: 'A' },
+        { id: 2, name: 'B' },
+        { id: 3, name: 'C' }
+      ]
+    }
+  })
+</script>
+

# 特殊处理

当列表中一些需要特殊二次处理的数据,可参考下列两种方式

# computed方式

<template>
+  <!-- 使用数组中元素的 id属性/保留关键字*this 作为key值  -->
+  <view wx:for="{{refactorTitleList}}" wx:key="id">
+    <!-- item 默认代表数组的当前项 -->
+    <view>{{item.id}}: {{item.name}}</view>
+    <!-- bad方式 不可用computed或methods中方法处理 -->
+    <!-- <view>{{item.id}}: {{format(item.name)}}</view> -->
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      titleList: [
+        { id: 1, name: 'A' },
+        { id: 2, name: 'B' },
+        { id: 3, name: 'C' }
+      ],
+      nameMap: {
+        '1': 'mpx'
+      }
+    },
+    computed: {
+      refactorTitleList () {
+        return this.titleList.map(item => {
+          // 列表中需要特殊处理的数据可在computed中处理好再渲染
+          item.name = this.nameMap[item.id] ? this.nameMap[item.id] : item.name
+          return item
+        })
+      }
+    }
+  })
+</script>
+

# wxs方式

<template>
+  <wxs module="foo">
+    var formatName = function (item, nameMap) {
+      // 这里区别string和number
+      var id = ''+item.id
+      if(nameMap[id]){
+        return nameMap[id];
+      }
+      return item.name;
+    }
+    module.exports = {
+      formatName: formatName
+    };
+  </wxs>
+  <!-- 使用数组中元素的 id属性/保留关键字*this 作为key值  -->
+  <view wx:for="{{titleList}}" wx:key="id">
+    <!-- item 默认代表数组的当前项 -->
+    <view>{{item.id}}: {{foo.formatName(item, nameMap)}}</view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      titleList: [
+        { id: 1, name: 'A' },
+        { id: 2, name: 'B' },
+        { id: 3, name: 'C' }
+      ],
+      nameMap: {
+        '1': 'mpx'
+      }
+    }
+  })
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/option-chain.html b/docs-vuepress/.vuepress/dist/guide/basic/option-chain.html new file mode 100644 index 0000000000..f70ea3b9b0 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/option-chain.html @@ -0,0 +1,76 @@ + + + + + + 模版内可选链表达式 | Mpx框架 + + + + + + + + + +

# 模版内可选链表达式

Mpx提供了在模版中使用可选链?.访问变量属性的能力,用法/能力和JS的可选链基本一致,可以在任意被{{}}所包裹的模版语法内使用。

实现原理是在编译时将使用?.的部分转化为wxs函数调用,运行时通过wxs访问变量属性,模拟出可选链的效果。

使用示例:

  <template>
+    <view wx:if="{{ a?.b }}">{{ a?.c + a?.d }}</view>
+    <view wx:for="{{ a?.d || [] }}"></view>
+    <view>{{ a?.d ? 'a' : 'b' }}</view>
+    <view>{{ a?.g[e?.d] }}</view>
+  </template>
+
+  <script>
+    import { createComponent } from '@mpxjs/core'
+
+    createComponent({
+      data: {
+        a: {
+            b: true,
+            c: 123,
+            g: {
+                d: 321
+            }
+        },
+        e: {
+            d: 'd'
+        }
+      }
+    })
+  </script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/reactive.html b/docs-vuepress/.vuepress/dist/guide/basic/reactive.html new file mode 100644 index 0000000000..71fe87fcb3 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/reactive.html @@ -0,0 +1,111 @@ + + + + + + 数据响应 | Mpx框架 + + + + + + + + + +

# 数据响应

Vue中最为开发者们所津津乐道的特性就是其数据响应特性,Vue中的数据响应特性主要包含:响应数据赋值、watch观察数据和computed计算属性,该能力让我们能够以非常直观简洁的方式建立起数据与数据之间/数据与视图之间的绑定依赖关系,大幅提升复杂webapp的开发体验和开发效率。

受此启发,Mpx通过增强的方式在小程序开发中提供了完整的数据响应特性,在2.5.x版本前,Mpx通过mobx实现内部和核心数据响应,在2.5.x版本之后,从数据响应性能、完整度和运行时包体积等多方面角度考虑,我们借鉴了Vue2的设计,在内部自行维护了一套精简高效的数据响应系统,全面移除了对于mobx的依赖。

在新的响应系统中,所有的使用方法和使用限制都和Vue保持一致,关于数据响应的原理和使用限制可以参考这里 (opens new window)

下面是一个小程序数据响应的简单示例:

<template>
+  <view>
+    <view>Num: {{num}}</view>
+    <view>Minus num: {{mnum}}</view>
+    <view>Num plus: {{nump}}</view>
+    <view>{{info.name}}</view>
+    <view wx:if="{{info.age}}">{{info.age}}</view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    // data中定义的数据会在初始化时进行数据响应处理
+    data: {
+      num: 1,
+      info: {
+        name: 'test'
+      }
+    },
+    // 配置中直接定义watch
+    watch: {
+      num (val) {
+        console.log(val)
+      }
+    },
+    // 定义计算属性,模板中可以直接访问
+    computed: {
+      mnum () {
+        return -this.num
+      },
+      nump: {
+        get () {
+          return this.num + 1
+        },
+        // 支持计算属性的setter
+        set (val) {
+          this.num = val - 1
+        }
+      }
+    },
+    onReady () {
+      // 使用实例方法定义watch,可以传递追踪函数更加灵活
+      this.$watch(() => {
+        return this.nump - this.mnum
+      }, (val) => {
+        console.log(val)
+      })
+      // 每隔一秒进行一次更新,相关watcher会被触发,视图也会发生更新
+      setInterval(() => {
+        this.num++
+      }, 1000)
+      // 使用$set新增响应属性,视图同样能够得到更新
+      setTimeout(() => {
+        this.$set(this.info, 'age', 23)
+      }, 1000)
+    }
+  })
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/refs.html b/docs-vuepress/.vuepress/dist/guide/basic/refs.html new file mode 100644 index 0000000000..1ce9cdb5d7 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/refs.html @@ -0,0 +1,106 @@ + + + + + + 获取组件实例/节点信息 | Mpx框架 + + + + + + + + + +

# 获取组件实例/节点信息

微信小程序中原生提供了selectComponent/SelectorQuery.select方法获取自定义组件实例和wxml节点信息,但是该api使用起来不太方便,并且不具备平台无关性,我们提供了增强指令wx:ref用于获取组件实例及节点信息,该指令的使用方式同vue中的ref类似,在模板中声明了wx:ref后,在组件ready后用户可以通过this.$refs获取对应的组件实例或节点查询对象(NodeRefs),调用响应的组件方法或者获取视图节点信息。

wx:ref其实也是模板编译和运行时注入结合实现的语法糖,本质还是通过原生小程序平台提供的能力进行获取,其实现的主要意义在于抹平跨平台差异以及提升用户的使用体验。

简单的使用示例如下:

  <template>
+    <view class="container">
+      <!-- my-header 为一个组件,组件内部定义了一个 show 方法 -->
+      <my-header wx:ref="myHeader"></my-header>
+      <view wx:ref="content"></view>
+    </view>
+  </template>
+
+  <script>
+    import { createComponent } from '@mpxjs/core'
+
+    createComponent({
+      ready() {
+        // 通过 this.$refs 获取view的节点查询对象
+        this.$refs.content.fields({size: true},function (res){
+          // res 就是我们要拿到的节点大小
+        }).exec()
+        // 通过 this.$refs 可直接获取组件实例
+        this.$refs.myHeader.show()  // 拿到组件实例,调用组件内部的方法
+      }
+    })
+  </script>
+

# 在列表渲染中使用wx:ref

在列表渲染中定义的wx:ref存在多个实例/节点,Mpx会在模板编译中判断某个wx:ref是否存在于列表渲染wx:for中,是的情况下在注入this.$refs时会通过selectAllComponents/SelectQuery.selectAll方法获取组件实例数组或数组节点查询对象,确保开发者能拿到列表渲染中所有的组件实例/节点信息。

使用示例如下:

<template>
+  <view>
+    <!-- list 组件 -->
+    <list wx:ref="list" wx:for="{{listData}}" data="{{item.name}}" wx:key="id"></list>
+    <!-- view 节点 -->
+    <view wx:ref="test" wx:for="{{listData}}" wx:key="id">{{item.name}}</view>
+  </view>
+</template>
+
+<script>
+  import { createComponent } from '@mpxjs/core'
+
+  createComponent({
+    data: {
+      listData: [
+        {id: 1, name: 'A'},
+        {id: 2, name: 'B'},
+        {id: 3, name: 'C'},
+      ]
+    },
+    ready () {
+      // 通过 this.$refs.list 获取的是组件实例的数组
+      this.$refs.list.forEach(item => {
+        // 对每一个组件实例的操作...
+      })
+      // 通过 this.$refs.test 获取的是节点查询对象,通过相关的方法操作节点
+      this.$refs.test.fields({size: true}, function (res) {
+        // 此处拿到的 res 是一个数组,包含了列表渲染中的所有节点的大小信息
+      }).exec()
+    }
+  })
+</script>
+
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/single-file.html b/docs-vuepress/.vuepress/dist/guide/basic/single-file.html new file mode 100644 index 0000000000..4b872a020a --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/single-file.html @@ -0,0 +1,77 @@ + + + + + + 单文件开发 | Mpx框架 + + + + + + + + + +

# 单文件开发

小程序规范中每个页面和组件都是由四个文件描述组成的,wxml/js/wxss/json,分别描述了组件/页面的视图模板,执行逻辑,样式和配置,由于这四个部分彼此之间存在相关性,比如模板中的组件需要在json中注册,数据需要在js中定义,这种离散的文件结构在实际开发的时候体验并不理想;受Vue单文件开发的启发,Mpx也提供了类似的单文件开发模式,拓展名为.mpx。

从下面的简单例子可以看出,.mpx中的四个区块分别对应了原生规范中的四个文件,Mpx在执行编译构建时会通过内置的mpx-loader收集依赖,并将.mpx的文件转换输出为原生规范中的四个文件。

<!--对应wxml文件-->
+<template>
+  <list></list>
+</template>
+<!--对应js文件-->
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    onLoad () {
+    },
+    onReady () {
+    }
+  })
+</script>
+<!--对应wxss文件-->
+<style lang="stylus">
+</style>
+<!--对应json文件-->
+<script type="application/json">
+  {
+    "usingComponents": {
+      "list": "../components/list"
+    }
+  }
+</script>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/start.html b/docs-vuepress/.vuepress/dist/guide/basic/start.html new file mode 100644 index 0000000000..6aadde0f98 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/start.html @@ -0,0 +1,132 @@ + + + + + + 快速开始 | Mpx框架 + + + + + + + + + +

# 快速开始

# 安装脚手架

npm i -g @mpxjs/cli
+

@mpxjs/cli文档 https://github.com/mpx-ecology/mpx-cli

# 创建项目安装依赖

在当前目录下创建mpx项目。

mpx create mpx-project
+

也可以使用npx在不全局安装脚手架情况下创建项目。

npx @mpxjs/cli create mpx-project
+

执行命令后会弹出一系列问题进行项目初始配置,根据自身需求进行选择,完成后进入项目目录进行依赖安装。

npm install
+

创建插件项目由于微信限制必须填写插件的AppID,创建普通项目无强制要求。

# 编译构建

使用npm script执行mpx的编译构建,在开发模式下我们执行serve命令,将项目源码构建输出到dist/${平台目录}下,并且监听源码的改动进行重新编译。

npm run serve
+

# 预览调试

使用小程序开发者工具打开dist下对应平台的目录,对你的小程序进行预览、调试,详情可参考小程序开发指南 (opens new window)

开启小程序开发者工具的watch选项,配合mpx本身的watch,能够得到很好的开发调试体验。

# 开始code

在Mpx中,我们使用@mpxjs/core提供的createApp、createPage和createComponent函数(分别对应原生小程序中的App、Page和Component)来创建App、页面和组件,我们下面根据脚手架创建出的初始项目目录,进行简单的介绍。

进入src/app.mpx,我们可以看到里面的结构和.vue文件非常类似,三个区块分别对应了小程序中的js/wxss/json文件。

js区块中调用createApp用于注册小程序,传入的配置可以参考小程序App构造器 (opens new window),由于app.js是小程序全局最早执行的js模块,一般mpx插件安装等初始化操作也在这里进行。

style区块对应app.wxss定义了全局样式,可以自由使用sass/less/stylus等css预编译语言。

json区块完全支持小程序原生的app.json配置 (opens new window),还额外支持了packages多人合作等增强特性。

<script>
+  import mpx, { createApp } from '@mpxjs/core'
+  import apiProxy from '@mpxjs/api-proxy'
+
+  mpx.use(apiProxy, {
+    usePromise: true
+  })
+  
+  createApp({
+    onLaunch () {
+
+    }
+  })
+</script>
+
+<style lang="stylus">
+  .bold
+    font-weight bold
+</style>
+
+<script type="application/json">
+  {
+    "pages": [
+      "./pages/index"
+    ]
+  }
+</script>
+

进入src/pages/index.mpx,可以看到同样是.vue风格的单文件结构,比起上面的app.mpx多了一个template区块,用于定义页面模板,除了支持小程序本身的全部模块语法和指令外,mpx还参考vue提供了大量模板增强指令,便于用户更快速高效地进行界面开发。

在js中调用createPage创建页面时,除了支持原本小程序支持的Page配置 (opens new window)外,我们还支持以数据响应为核心的一系列增强能力。

在json中,我们同样支持原生的页面json配置 (opens new window),此外,我们能够直接在usingComponents中填写npm地址引用npm包中的组件,mpx组件和原生小程序组件均可引用,无需调用开发者工具npm编译,且能够通过依赖收集按需进行打包。

为了保障增强能力的完整性,在支持的平台中Mpx优先使用Component构造器创建页面,支持全部Component生命周期;在某些特殊情况下,你可以在@mpxjs/webpack-plugin中传入forceUsePageCtor:true配置来禁用掉这个行为。

<template>
+  <list></list>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    onLoad () {
+    },
+    onReady () {
+    }
+  })
+</script>
+
+<style lang="stylus">
+
+</style>
+
+<script type="application/json">
+  {
+    "usingComponents": {
+      "list": "../components/list"
+    }
+  }
+</script>
+

最后,我们进入src/components/list.mpx文件,可以看到其构成与页面文件十分相似,对于组件,Mpx提供了和页面完全一致的增强能力

<template>
+  <view class="list">
+    <view wx:for="{{listData}}" wx:key="index">{{item}}</view>
+  </view>
+</template>
+
+<script>
+  import { createComponent } from '@mpxjs/core'
+
+  createComponent({
+    data: {
+      listData: ['手机', '电视', '电脑']
+    }
+  })
+</script>
+
+<style lang="stylus">
+  .list
+    background-color red
+</style>
+
+<script type="application/json">
+  {
+    "component": true
+  }
+</script>
+

更多用法可以查看我们的官方实例:https://github.com/didi/mpx/tree/master/examples/ (opens new window)

# 跨平台输出

如果你选择的base平台为微信,mpx提供了强大的跨平台输出能力,能够将你的小程序源码输出到目前业内的全部小程序平台(微信/支付宝/百度/头条/QQ)中和web平台中运行。

执行以下命令进行跨平台输出

npm run build:cross
+

关于跨平台能力的更多详情请查看这里

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/template.html b/docs-vuepress/.vuepress/dist/guide/basic/template.html new file mode 100644 index 0000000000..975c328298 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/template.html @@ -0,0 +1,137 @@ + + + + + + 模板语法 | Mpx框架 + + + + + + + + + +

# 模板语法

Mpx中的模板语法以小程序模板语法为基础,支持小程序的全部模板语法,同时提供了一系列增强的模板指令及语法。

小程序原生模板语法请参考这里 (opens new window)

Mpx提供的增强指令语法如下:

下面是使用了模板增强语法的一个简单实例,许多在原生小程序上很繁琐的模板描述在增强语法的帮助下变得清晰简洁:

<template>
+  <!--动态样式-->
+  <view class="container" wx:style="{{dynamicStyle}}">
+    <!--数据绑定-->
+    <view class="title">{{title}}</view>
+    <!--计算属性数据绑定-->
+    <view class="title">{{reversedTitle}}</view>
+    <view class="list">
+      <!--循环渲染,动态类名,事件处理内联传参-->
+      <view wx:for="{{list}}" wx:key="id" class="list-item" wx:class="{{ {active:item.active} }}"
+            bindtap="handleTap(index)">
+        <view>{{item.content}}</view>
+        <!--循环内部双向数据绑定-->
+        <input type="text" wx:model="{{list[index].content}}"/>
+      </view>
+    </view>
+    <!--自定义组件获取实例,双向绑定,自定义双向绑定属性及事件-->
+    <custom-input wx:ref="ci" wx:model="{{customInfo}}" wx:model-prop="info" wx:model-event="change"/>
+    <!--动态组件,is传入组件名字符串,可使用的组件需要在json中注册,全局注册也生效-->
+    <component is="{{current}}"></component>
+    <!--显示/隐藏dom-->
+    <view class="bottom" wx:show="{{showBottom}}">
+      <!--模板条件编译,__mpx_mode__为框架注入的环境变量,条件判断为false的模板不会生成到dist-->
+      <view wx:if="{{__mpx_mode__ === 'wx'}}">wx env</view>
+      <view wx:if="{{__mpx_mode__ === 'ali'}}">ali env</view>
+    </view>
+  </view>
+</template>
+
+<script>
+  import { createPage } from '@mpxjs/core'
+
+  createPage({
+    data: {
+      // 动态样式和类名也可以使用computed返回
+      dynamicStyle: {
+        fontSize: '16px',
+        color: 'red'
+      },
+      title: 'hello world',
+      list: [
+        {
+          content: '全军出击',
+          id: 0,
+          active: false
+        },
+        {
+          content: '猥琐发育,别浪',
+          id: 1,
+          active: false
+        }
+      ],
+      customInfo: {
+        title: 'test',
+        content: 'test content'
+      },
+      current: 'com-a',
+      showBottom: false
+    },
+    computed: {
+      reversedTitle () {
+        return this.title.split('').reverse().join('')
+      }
+    },
+    handleTap (index) {
+      // 处理函数直接通过参数获取当前点击的index,清晰简洁
+      this.list[index].active = !this.list[index].active
+    }
+  })
+</script>
+
+<script type="application/json">
+  {
+    "usingComponents": {
+      "custom-input": "../components/custom-input",
+      "com-a": "../components/com-a",
+      "com-b": "../components/com-b"
+    }
+  }
+</script>
+

# 模板预编译

Mpx还支持开发者使用插值语法与小程序不冲突第三方的模板引擎语法来编写template,如pug:

<template lang="pug">
+  view(class="list")
+    view(class="list-item") red
+    view(class="list-item") blue
+    view(class="list-item") green
+</template>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html b/docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html new file mode 100644 index 0000000000..6f45b2eeeb --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html @@ -0,0 +1,81 @@ + + + + + + 双向绑定 | Mpx框架 + + + + + + + + + +

# 双向绑定

Mpx针对表单组件提供了wx:model双向绑定指令,类似于v-model,该指令是一个语法糖指令,监听了组件抛出的输入事件并对绑定的数据进行更新,默认情况下会监听表单组件的input事件,并将event.detail.value中的数据更新到组件的value属性上。

简单实用示例如下:

<view>
+  <input type="text" wx:model="{{message}}"/>
+  <!--view中的文案会随着用户对输入框进行输入而实时更新-->
+  <view>{{message}}</view>
+</view>
+

# 对自定义组件使用

对自定义组件使用双向绑定时用法与原生小程序组件完全一致

<view>
+  <custom-input type="text" wx:model="{{message}}"/>
+  <!--此处的文案会随着输入框输入实时更新-->
+  <view>{{message}}</view>
+</view>
+

# 更改双向绑定的监听事件及数据属性

如前文所述,wx:model指令默认监听组件抛出的input事件,并将声明的数据绑定到组件的value属性上,该行为在一些原生组件和自定义组件上并不成立,因为这些组件可能不存在input事件或value属性。对此,我们提供了wx:model-eventwx:model-prop指令来修改双向绑定的监听事件和数据属性,使用示例如下:

<view>
+  <!--原生组件picker中没有input事件,通过wx:model-event指令将双向绑定监听事件改为change事件-->
+  <picker mode="selector" range="{{countryRange}}" wx:model="{{country}}" wx:model-event="change">
+    <view class="picker">
+      当前选择: {{country}}
+    </view>
+  </picker>
+  <!--通过wx:model-event和wx:model-prop将该自定义组件的双向绑定监听事件和数据属性修改为customInput/customValue-->
+  <custom-input wx:model="{{message}}" wx:model-event="customInput" wx:model-prop="customValue"/>
+  <view>{{message}}</view>
+</view>
+

# 更改双向绑定事件数据路径

Mpx中双向绑定默认使用event对象中的event.detail.value作为用户输入来更新组件数据,该行为在一些原生组件和自定义组件中也不成立,例如vant中的field输入框组件,用户的输入直接存储在event.detail当中,当然用户也可以将其存放在detail中的其他数据路径下,对此,我们提供了wx:model-value-path指令让用户声明在事件当中应该访问的数据路径。

由于小程序triggerEvent的Api设计,事件的用户数据都只能存放在event.detail中,因此wx:model-value-path的值都是相对于event.detail的数据路径,我们支持两种形式进行声明:

  • 一种是点语法,如传入current.value时框架会从event.detail.current.value中取值作为用户输入,为空字符串时wx:model-value-path=""代表直接使用event.detail作为用户输入;
  • 第二种是数组字面量的JSON字符串,如["current", "value"]与上面的current.value等价,传入[]时与上面的空字符串等价。

使用示例如下:

<view>
+  <!--wx:model-value-path传入[]直接使用event.detail作为用户输入,使vant-field中双向绑定能够生效-->
+  <van-field wx:model="{{username}}" wx:model-value-path="[]" label="用户名" placeholder="请输入用户名"/>
+</view>
+

# 双向绑定过滤器

用户可以使用wx:model-filter指令定义双向绑定过滤器,在修改数据之前对用户输入进行过滤,来实现特定的效果,框架内置了trim过滤器对用户输入进行trim操作,传入其他字符串时会使用当前组件中的同名方法作为自定义过滤器,使用示例如下:

<view>
+  <!--以下示例中,用户输入的首尾空格将被过滤-->
+  <input wx:model="{{message}}" wx:model-filter="trim"/>
+  <view>{{message}}</view>
+</view>
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html b/docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html new file mode 100644 index 0000000000..7f655aade1 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html @@ -0,0 +1,527 @@ + + + + + + 组合式 API | Mpx框架 + + + + + + + + + +

# 组合式 API

# 什么是组合式 API

组合式 API 是 Vue3 中包含的最重要的特性之一,主要由 setup 函数及一系列响应式 API 及生命周期钩子函数组成,与传统的选项式 API 相比,组合式 API 具备以下优势:

  • 更好的逻辑复用,通过函数包装复用逻辑,显式引入调用,方便简洁且符合直觉,规避消除了 mixins 复用中存在的缺陷;
  • 更灵活的代码组织,相比于选项式 API 提前规定了代码的组织方式,组合式 API 在这方面几乎没有做任何限制与规定,更加灵活自由,在功能复杂的庞大组件中,我们能够通过组合式 API 让我们的功能代码更加内聚且有条理,不过这也会对开发者自身的代码规范意识提出更高要求;
  • 更方便的类型推导,虽然基于 this 的选项式 API 通过 ThisType 也能在一定程度上实现 TS 类型推导,但推导和实现成本较高,同时仍然无法完美覆盖一些复杂场景(如嵌套 mixins 等);而组合式 API 以本地变量和函数为基础,本身就是类型友好的,我们在类型方面几乎不需要做什么额外的工作就能享受到完美的类型推导。

同时与 React Hooks 相比,组合式 API 中的 setup 函数只在初始化时单次执行,在数据响应能力的加持下大大降低了理解与使用成本,基于以上原因,我们决定为 Mpx 添加组合式 API 能力,让用户能够用组合式 API 方式进行小程序开发。

更多关于组合式 API 的说明可以查看 Vue3 官方文档 (opens new window)

Mpx 是一个小程序优先的增强型跨端框架,因此我们在为 Mpx 设计实现组合式 API 的过程中,并不追求与 Vue3 中的组合式 API 完全一致,我们更多是借鉴 Vue3 中组合式 API 的设计思想,将其与目前 Mpx 及小程序中的开发模式结合起来,而非完全照搬其实现。因此在 Mpx 中一些具体的 API 设计实现会与 Vue3 存在差异,我们会在后续相关的文档中进行标注说明,如果你想查看 Mpx 与 Vue3 在组合式 API 中的差异,可以跳转到这里查看。

# 组合式 API 基础

# setup 函数

同 Vue3 一样,在 Mpx 当中 setup 函数是组合式 API 的基础,我们可以在 createPagecreateComponent 中声明 setup 函数。

setup 函数接收 propscontext 两个参数,其中 context 参数与 Vue3 中存在差别,详情可以查看这里

我们参考 Vue3 中的示例实现一个小程序版本,可以看到它和 Vue3 中的实现基本一致,包含以下功能:

  • 仓库列表
  • 更新仓库列表的函数
  • 返回列表和函数,以便其他组件选项可以对它们进行访问
import { createComponent } from '@mpxjs/core'
+import { fetchUserRepositories } from '@/api/repositories'
+
+createComponent({
+  properties: {
+    user: String
+  },
+  setup (props) {
+    let repositories = []
+    const getUserRepositories = async () => {
+      repositories = await fetchUserRepositories(props.user)
+    }
+
+    return {
+      repositories, // 返回的数据,可以在其他选项式 API 中通过 this 访问或在模板上直接访问
+      getUserRepositories // 返回的函数,它的行为与将其定义在 methods 选项中的行为相同
+    }
+  }
+})
+

目前 repositories 是个非响应式变量,用户层面将无法感知它的变化,仓库列表将始终为空。

#ref 的响应式变量

同 Vue3 一致,我们可以通过 ref 函数为任意一个变量创建响应式引用,ref 接收参数并将其包裹在一个带有 value property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值,简单示例如下:

import { ref } from '@mpxjs/core'
+
+const counter = ref(0)
+
+console.log(counter) // { value: 0 }
+console.log(counter.value) // 0
+
+counter.value++
+console.log(counter.value) // 1
+

回到我们的示例中,我们通过 ref 创建一个响应式的 repositories 变量:

import { createComponent, ref } from '@mpxjs/core'
+import { fetchUserRepositories } from '@/api/repositories'
+
+createComponent({
+  properties: {
+    user: String
+  },
+  setup (props) {
+    let repositories = ref([])
+    const getUserRepositories = async () => {
+      repositories.value = await fetchUserRepositories(props.user)
+    }
+
+    return {
+      repositories,
+      getUserRepositories
+    }
+  }
+})
+

通过 ref 包裹以后,我们每次调用 getUserRepositories 更新 repositories 的值都能被外部的 watch 观测到,并且触发视图的更新。

#setup 中注册生命周期钩子

为了完整实现选项式 API 中的能力,我们需要支持在 setup 中注册生命周期钩子,同 Vue3 类似,我们也提供了一系列生命周期钩子的注册函数,这些函数都以 on 开头,不过由于 Mpx 的跨平台特性,我们不可能针对不同的平台提供不同的生命周期钩子函数,因此我们提供了一份抹平跨平台差异后统一的生命周期钩子,与小程序原生生命周期的映射关系可查看这里

我们希望在组件挂载时调用 getUserRepositories,可以使用 onMounted 钩子来实现,注意这里不是 onReady,不过它确实对应于微信小程序中组件的 ready 钩子:

import { createComponent, ref, onMounted } from '@mpxjs/core'
+import { fetchUserRepositories } from '@/api/repositories'
+
+createComponent({
+  properties: {
+    user: String
+  },
+  setup (props) {
+    let repositories = ref([])
+    const getUserRepositories = async () => {
+      repositories.value = await fetchUserRepositories(props.user)
+    }
+    
+    // 在组件挂载时(微信小程序中相当于ready)调用 getUserRepositories
+    onMounted(getUserRepositories) 
+
+    return {
+      repositories,
+      getUserRepositories
+    }
+  }
+})
+

# watch 响应式更改

现在我们需要对 user prop 的变化做出响应,可以使用新的 watch API,该 API 接受 3 个参数:

  • 一个想要侦听的响应式引用或 getter 函数
  • 一个回调
  • 可选的配置选项

简单示例如下:

import { ref, watch } from '@mpxjs/core'
+
+const counter = ref(0)
+watch(counter, (newValue, oldValue) => {
+  console.log('The new counter value is: ' + counter.value)
+})
+

counter 被修改时,例如 counter.value = 5,侦听将触发并执行回调打印 The new counter value is:5

回到我们的示例中,当 user prop 发生变化时调用 getUserRepositories 更新仓库列表:

import { createComponent, ref, onMounted, watch, toRefs } from '@mpxjs/core'
+import { fetchUserRepositories } from '@/api/repositories'
+
+createComponent({
+  properties: {
+    user: String
+  },
+  setup (props) {
+    // 使用 `toRefs` 创建对 `props` 中的 `user` property 的响应式引用
+    const { user } = toRefs(props)
+    
+    let repositories = ref([])
+    const getUserRepositories = async () => {
+      // 更新 `prop.user` 到 `user.value` 访问引用值
+      repositories.value = await fetchUserRepositories(user.value)
+    }
+    
+    onMounted(getUserRepositories) 
+    
+    // 在 user prop 的响应式引用上设置一个侦听器
+    watch(user, getUserRepositories)
+
+    return {
+      repositories,
+      getUserRepositories
+    }
+  }
+})
+

你可能已经注意到在我们的 setup 的顶部使用了 toRefs。这是为了确保我们的侦听器能够根据 user prop 的变化做出反应。

# 独立的 computed 属性

refwatch 类似,也可以使用从 Mpx 导入的 computed 函数创建计算属性,简单示例如下:

import { ref, computed } from '@mpxjs/core'
+
+const counter = ref(0)
+const twiceTheCounter = computed(() => counter.value * 2)
+
+counter.value++
+console.log(counter.value) // 1
+console.log(twiceTheCounter.value) // 2
+

这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。

回到我们的示例,我们通过 computed 为仓库列表实现搜索功能:

import { createComponent, ref, onMounted, watch, toRefs, computed } from '@mpxjs/core'
+import { fetchUserRepositories } from '@/api/repositories'
+
+createComponent({
+  properties: {
+    user: String
+  },
+  setup (props) {
+    const { user } = toRefs(props)
+    
+    let repositories = ref([])
+    const getUserRepositories = async () => {
+      repositories.value = await fetchUserRepositories(user.value)
+    }
+    
+    onMounted(getUserRepositories) 
+    
+    watch(user, getUserRepositories)
+    
+    const searchQuery = ref('')
+    const repositoriesMatchingSearchQuery = computed(() => {
+      return repositories.value.filter(
+        repository => repository.name.includes(searchQuery.value)
+      )
+    })
+
+    return {
+      repositories,
+      getUserRepositories,
+      searchQuery,
+      repositoriesMatchingSearchQuery
+    }
+  }
+})
+

可以看到目前我们的 setup 函数已经相当庞大了,在不进行任何封装的情况下我们很可能在 setup 中写出流水账式的代码,可维护性很差,这也是为什么组合式 API 会对开发者的代码风格提出更高的要求,下面我们来拆解目前已经实现的功能,将它们提取到独立的组合函数中,先从创建 useUserRepositories 函数开始:

// src/composables/useUserRepositories.js
+
+import { fetchUserRepositories } from '@/api/repositories'
+import { ref, onMounted, watch } from '@mpxjs/core'
+
+export default function useUserRepositories(user) {
+  const repositories = ref([])
+  const getUserRepositories = async () => {
+    repositories.value = await fetchUserRepositories(user.value)
+  }
+
+  onMounted(getUserRepositories)
+  watch(user, getUserRepositories)
+
+  return {
+    repositories,
+    getUserRepositories
+  }
+}
+
+

然后是搜索功能:

// src/composables/useRepositoryNameSearch.js
+
+import { ref, computed } from '@mpxjs/core'
+
+export default function useRepositoryNameSearch(repositories) {
+  const searchQuery = ref('')
+  const repositoriesMatchingSearchQuery = computed(() => {
+    return repositories.value.filter(repository => {
+      return repository.name.includes(searchQuery.value)
+    })
+  })
+
+  return {
+    searchQuery,
+    repositoriesMatchingSearchQuery
+  }
+}
+

现在我们有了两个单独的功能模块,接下来就可以开始在组件中使用它们了:

import useUserRepositories from '@/composables/useUserRepositories'
+import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
+import { createComponent, toRefs } from '@mpxjs/core'
+
+
+createComponent({
+  properties: {
+    user: String
+  },
+  setup (props) {
+    const { user } = toRefs(props)
+    
+    const { repositories, getUserRepositories } = useUserRepositories(user)
+    
+    const {
+      searchQuery,
+      repositoriesMatchingSearchQuery
+    } = useRepositoryNameSearch(repositories)
+
+    return {
+      // 因为我们并不关心未经过滤的仓库
+      // 我们可以在 `repositories` 名称下暴露过滤后的结果
+      repositories: repositoriesMatchingSearchQuery,
+      getUserRepositories,
+      searchQuery,
+    }
+  }
+})
+

现在我们使用组合式 API 完成了仓库列表组件的开发,它看上去相当清晰且易于维护。想要了解更多组合式 API 的信息,请继续查看本页接下来的章节,想要了解新的响应式 API 的使用,请跳转查看响应式 API章节。

# Setup

setup 函数在组件创建时执行,返回组件所需的数据和方法,是组合式 API 的核心。

注意,setup 函数不可被混入,所有定义在 mixin 中的 setup 都将被丢弃。

# 参数

setup 函数接受两个参数,分别是 propscontext

# Props

setup 函数的第一个参数 props 和 Vue3 中完全一致,就是包含当前组件 props 的响应式数据,当父组件更新某个 prop 时,该数据也将被更新。

import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  props: {
+    title: String
+  },
+  setup(props) {
+    console.log(props.title)
+  }
+})
+

Warning: props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。

如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数将其转换一个包含 ref 数据的纯对象:

import { createComponent, toRefs } from '@mpxjs/core'
+
+createComponent({
+  props: {
+    title: String
+  },
+  setup(props) {
+    const { title } = toRefs(props)
+    // `title` 是一个 ref
+    console.log(title.value)
+  }
+})
+

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

import { createComponent, toRef } from '@mpxjs/core'
+
+createComponent({
+  props: {
+    title: String
+  },
+  setup(props) {
+    const title = toRef(props, 'title')
+    // `title` 是一个 ref
+    console.log(title.value)
+  }
+})
+

# Context

由于小程序与 web 基础技术规范的差异,Mpx 中 setup 的第二个参数 context 与 Vue3 中完全不同,context 包含的属性如下:

import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  setup(props, context) {
+    // 触发事件,等价于 this.triggerEvent
+    console.log(context.triggerEvent)
+    // 获取 NodesRef/组件实例,等价于 this.$refs
+    console.log(context.refs)
+    // 字节小程序中异步获取组件实例,等价于 this.$asyncRefs
+    console.log(context.asyncRefs)
+    // 获取组件实例,等价于 this.selectComponent,可用 this.$refs 替代
+    console.log(context.selectComponent)
+    // 批量获取组件实例,等价于 this.selectAllComponents,可用 $refs 替代
+    console.log(context.selectAllComponents)
+    // 获取 SelectorQuery 对象实例查询元素布局信息,等价于 this.createSelectorQuery,可用 $refs 替代
+    console.log(context.createSelectorQuery)
+    // 获取 IntersectionObserver 对象实例查询视图相交信息,等价于 this.createIntersectionObserver
+    console.log(context.createIntersectionObserver)
+  }
+})
+

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

import { createComponent } from '@mpxjs/core'
+
+createComponent({
+  setup(props, { triggerEvent, refs }) {
+    // ...
+  }
+})
+

refs 是有状态的对象,它会随组件本身的更新而更新。这意味着你应该避免对其进行解构,并始终以 refs.x 的方式访问 NodesRef 或组件实例。与 props 不同,refs 是非响应式的。如果你打算根据 refs 的更改应用副作用,那么应该在 onUpdated 生命周期钩子中执行此操作。

# 访问组件的 property

除了 propscontext 中包含的内容,执行 setup 时将无法访问部分组件选项,包括:

  • data
  • computed

# 结合模板使用

如果 setup 返回一个对象,那么该对象的 property 可以在模板中访问到:

<template>
+  <view>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</view>
+</template>
+
+<script>
+  import { createComponent, ref, reactive } from '@mpxjs/core'
+
+  createComponent({
+    properties: {
+      collectionName: String
+    },
+    setup(props) {
+      const readersNumber = ref(0)
+      const book = reactive({ title: 'Mpx' })
+
+      // 暴露给 template
+      return {
+        readersNumber,
+        book
+      }
+    }
+  })
+</script>
+

# 使用 this

setup() 内部,this 不是当前组件的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。

Mpx 的组合式 API 设计极力避免了用户需要在 setup() 中访问 this 的场景,不过在一些例外情况下,用户仍然可以通过 getCurrentInstance() 的方式获取到当前组件实例,注意该函数必须在 setup() 或生命周期钩子中同步调用。

# 生命周期钩子

组合式 API 中,我们通过 on${Hookname}(fn) 的方式注册访问生命周期钩子。

Mpx 作为一个跨端小程序框架,需要兼容不同小程序平台不同的生命周期,在选项式 API 中,我们在框架中内置了一套统一的生命周期,将不同小程序平台的生命周期转换映射为内置生命周期后再进行统一的驱动,以抹平不同小程序平台生命周期钩子的差异,如微信小程序的 attached 钩子和支付宝小程序的 onInit 钩子,在组合式 API 中,我们基于框架内置的生命周期暴露了一套统一的生命周期钩子函数,下表展示了框架内置生命周期/组合式 API 生命周期函数与不同小程序平台原生生命周期的对应关系:

# 组件生命周期

框架内置生命周期 Hook inside setup 微信原生 支付宝原生
BEFORECREATE null attached(数据响应初始化前) onInit(数据响应初始化前)
CREATED null attached(数据响应初始化后) onInit(数据响应初始化后)
BEFOREMOUNT onBeforeMount ready(MOUNTED 执行前) didMount(MOUNTED 执行前)
MOUNTED onMounted ready(BEFOREMOUNT 执行后) didMount(BEFOREMOUNT 执行后)
BEFOREUPDATE onBeforeUpdate nullsetData 执行前) nullsetData 执行前)
UPDATED onUpdated nullsetData callback) nullsetData callback)
BEFOREUNMOUNT onBeforeUnmount detached(数据响应销毁前) didUnmount(数据响应销毁前)
UNMOUNTED onUnmounted detached(数据响应销毁后) didUnmount(数据响应销毁后)

同 Vue3 一样,组合式 API 中没有提供 BEFORECREATECREATED 对应的生命周期钩子函数,用户可以直接在 setup 中编写相关逻辑。

除支付宝外的小程序平台支持使用Component构建页面,在页面中使用组件生命周期钩子与在组件中完全一致,并且框架在支付宝环境也进行了抹平实现。

# 页面生命周期

框架内置生命周期 Hook inside setup 微信原生 支付宝原生
ONLOAD onLoad onLoad onLoad
ONSHOW onShow onShow onShow
ONHIDE onHide onHide onHide
ONRESIZE onResize onResize events.onResize

# 组件中访问页面生命周期

框架内置生命周期 Hook inside setup 微信原生 支付宝原生
ONSHOW onShow pageLifetimes.show null(框架抹平实现)
ONHIDE onHide pageLifetimes.hide null(框架抹平实现)
ONRESIZE onResize pageLifetimes.resize null(框架抹平实现)

下面是简单的使用示例:

import { createComponent, onMounted, onUnmounted } from '@mpxjs/core'
+
+createComponent({
+  setup () {
+    // mounted
+    onMounted(()=>{
+      console.log('Component mounted.')
+    })
+    // unmounted
+    onUnmounted(()=>{
+      console.log('Component unmounted.')
+    })
+    return {}
+  }
+})
+

# 框架内置生命周期

从上面可以看到我们在框架内部内置了一套统一的生命周期来抹平不同平台生命周期的差异,由于存在数据响应机制,这套内置生命周期与小程序原生的生命周期不完全一一对应,反而与 Vue 的生命周期更加相似,在过去的版本中,我们没有显式地暴露出 BEFORECREATE 这这类框架内置的生命周期,更多都在框架内部使用,但是在组合式 API 版本中,为了使选项式 API 的生命周期能力与之对齐,我们将框架内置的生命周期显式导出,让用户在选项式 API 开发环境下也能正常使用这些能力,简单使用示例如下:

import { createComponent, BEFORECREATE } from '@mpxjs/core'
+
+createComponent({
+  [BEFORECREATE] () {
+    console.log('beforeCreate exec.')
+  },
+  created () {
+    // 原生的 created 会被映射为框架内部的 CREATED 执行,此处逻辑在 BEFORECREATE 后执行
+    console.log('created exec.')
+  }
+})
+

# 具有副作用的页面事件

在小程序中,一些页面事件的注册存在副作用,即该页面事件注册与否会产生实质性的影响,比如微信中的 onShareAppMessageonPageScroll,前者在不注册时会禁用当前页面的分享功能,而后者在注册时会带来视图与逻辑层之间的线程通信开销,对于这部分页面事件,我们无法通过预注册 -> 驱动方式提供组合式 API 的注册方式,用户可以通过选项式 API 的方式来注册使用,通过 this 访问组合式 API setup 函数的返回。

然而这种使用方式显然不够优雅,我们考虑是否可以通过一些非常规的方式提供这类副作用页面事件的组合式 API 注册支持,例如,借助编译手段。我们在运行时提供了副作用页面事件的注册函数,并在编译时通过 babel 插件的方式解析识别到当前页面中存在这些特殊注册函数的调用时,通过框架已有的编译 -> 运行时注入的方式将事件驱动逻辑添加到当前页面当中,以提供相对优雅的副作用页面事件在组合式 API 中的注册方式,同时不产生非预期的副作用影响。

我们需要先修改 babel 配置添加 @mpxjs/babel-plugin-inject-page-events 插件:

// babel.config.json
+{
+ "plugins": [
+    [
+      "@babel/transform-runtime",
+      {
+        "corejs": 3,
+        "version": "^7.10.4"
+      }
+    ],
+    "@mpxjs/babel-plugin-inject-page-events"
+  ]
+}
+

然后就能想普通生命周期一样使用组合式 API 进行页面事件注册,简单示例如下:

import { createComponent, ref, onShareAppMessage } from '@mpxjs/core'
+
+createComponent({
+  setup () {
+    const count = ref(0)
+
+    onShareAppMessage(() => {
+      return {
+        title: '页面分享'
+      }
+    })
+
+    return {
+      count
+    }
+  }
+})
+

目前我们通过这种方式支持的页面事件如下:

页面事件 Hook inside setup 平台支持
onPullDownRefresh onPullDownRefresh 全小程序平台 + web
onReachBottom onReachBottom 全小程序平台 + web
onPageScroll onPageScroll 全小程序平台 + web
onShareAppMessage onShareAppMessage 全小程序平台
onTabItemTap onTabItemTap 微信/支付宝/百度/QQ
onAddToFavorites onAddToFavorites 微信 / QQ
onShareTimeline onShareTimeline 微信
onSaveExitState onSaveExitState 微信

特别注意,由于静态编译分析实现方式的限制,这类页面事件的组合式 API 使用需要满足页面事件注册函数的调用和 createPage 的调用位于同一个 js 文件当中。

# 模板引用

在 Vue3 的组合式 API 中,我们可以在 setup 函数中使用 ref() 创建引用数据获取模板中绑定了 ref 属性的组件或 DOM 节点,优雅地将响应式引用模板引用进行了关联统一,但在 Mpx 中,受限于小程序的技术限制,我们无法在低性能损耗下实现相同的设计,因此我们在 setup 的 context 参数中提供了 refs 对象,结合模板中的wx:ref指令使用,与选项式 API 中的 $refs 保持一致。

下面是组合式 API 中进行模板引用的使用示例:

<template>
+  <view bindtap="handleHello" wx:ref="hello">hello</view>
+  <view wx:if="{{showWorld}}" wx:ref="world">world</view>
+  <view wx:for="{{list}}" wx:ref="list">{{item}}</view>
+</template>
+
+<script>
+  import { createComponent, ref, onMounted, nextTick } from '@mpxjs/core'
+
+  createComponent({
+    setup (props, { refs }) {
+      const showWorld = ref(false)
+      const list = ref(['手机', '电视', '电脑'])
+
+      onMounted(() => {
+        // 最早在 onMounted 中才能访问refs,对于节点返回 NodesRef 对象,对于组件返回组件实例
+        console.log('hello ref:', refs.hello)
+        // 在循环中定义 wx:ref,对应的 refs 返回数组
+        console.log('list ref:', refs.list)
+      })
+
+      const handleHello = () => {
+        showWorld.value = true
+        nextTick(() => {
+          // 数据变更后要在 nextTick 中访问更新后的视图数据
+          console.log('world ref:', refs.world)
+        })
+      }
+      
+      // 暴露给 template
+      return {
+        showWorld,
+        handleHello,
+        list
+      }
+    }
+  })
+</script>
+

# <script setup>

和 Vue 类似,<script setup> 是在 Mpx 单文件组件中使用组合式 API 时的编译时语法糖。不过受小程序底层技术限制,在 Mpx 中 <script setup> 无法完整提供其在 Vue 中所具备的相关优势,我们提供了这个语法能力,但不作为默认的推荐选项。

# 基本语法

启用该语法需要在 <script> 代码块上添加 setup attribute:

<script setup>
+    console.log('hello Mpx script setup')
+</script>
+

<script> 里边的代码会被编译成组件 setup() 函数的内容。

# 顶层的绑定会被暴露给模版

从 v2.8.19 开始,顶层绑定自动暴露给模板的能力被取消,构建时会强制用户通过 defineExpose 手动声明需要暴露给模板的数据或方法。

和 Vue 一样,当使用 <script setup> 时,任何在 <script setup> 声明的顶层的绑定(包括变量,函数声明,以及 import 导入的内容) 都能在模版中直接使用:

<script setup>
+    import { ref } from '@mpxjs/core'
+    const msg = ref('hello');
+    function log() {
+        console.log(msg.value)
+    }
+</script>
+<template>
+    <view>msg: {{msg}}</view>
+    <view ontap="log">click</view>
+</template>
+

import 导入的内容,除了从 @mpxjs/core 中导入的变量或方法,其他模块导入的属性和方法全部都会暴露给模版。这意味着我们可以直接在模版中使用引入的相关方法,而不需要通过 methods 选项来暴露:

<template>
+    <view ontap="clickTrigger">click</view>
+</template>
+<script setup>
+    import { clickTrigger } from './utils'
+</script>
+

注意项:如果你 script setup 中有较多对象或方法的声明和引入,比如全局 store 这种十分复杂的对象,走默认逻辑暴露给模版会造成性能问题,因此需要使用 defineExpose 来手动定义暴露给模版的数据和方法。

# 响应式

和 Vue 中一样,响应式状态需要明确使用响应性 API 来创建。和 setup() 函数的返回值一样,ref 在模版中使用的时候会自动解包:

<template>
+    <button ontap="addCount">{{count}}</button>
+</template>
+<script setup>
+    import { ref } from '@mpxjs/core'
+    const count = ref(0)
+    function addCount() {
+        count.value++
+    }
+</script>
+

# defineProps()

和 Vue 类似,为了在声明小程序组件 properties 选项时获得完整的类型推导支持,在 <script setup> 中,我们需要使用 defineProps API,它默认在 <script setup> 中可用:

<script setup>
+    const props = defineProps({
+        testA: String
+    })
+</script>
+
  • defineProps 是只能在 <script setup> 中使用的编译宏,不需要手动导入,会跟随 <script setup> 的处理过程一同被编译掉。
  • defineProps 接收与小程序 properties 选项相同的值。
  • 传入到 defineProps 的选项会从 setup 中提升到模块的作用域。因此传入的选项不能引用在 setup 作用域中声明的局部变量,否则会导致编译错误,不过可以引入导入的绑定。

# defineExpose()

<script setup> 中定义暴露给模版的变量和方法,在 Mpx <script setup> 中属于强制要求,若不使用该编译宏,则会构建报错。

Mpx 中的 defineExpose 和 Vue3 中的不尽相同,在 Vue3 中,使用 <scrip setup> 的组件默认是关闭的-即通过模版引用或者 $parent 链获取到的组件实例中不会暴露任何在<script setup> 中声明的绑定。

在 Mpx 中,<script setup> 中的声明绑定都会挂载到组件实例中,都可以通过组件实例来访问,Mpx defineExpose 更大的作用是假如你在 <scrip setup> 中引入了一些 store 实例,这些 store 实例默认会挂载到组件实例中,会导致后续的响应式处理以及组件更新速度变慢,这里我们通过强制使用 +defineExpose 来规避掉这个问题。

<script setup>
+    const count = ref(0)
+    const name = ref('black')
+    defineExpose({
+        name
+    })
+</script>
+<template>
+    <!--正确渲染 black-->
+    <view>{{name}}</view>
+    <!--找不到对应变量,无内容-->
+    <view>{{count}}</view>
+</template>
+

# defineOptions()

此编译宏相较于 Vue 是 Mpx 中独有的,主要是当开发者想在 <script setup> 中使用一些微信小程序中特有的选项式,例如 relations、moved 等,可以使用该编译宏进行定义。

<script setup>
+    defineOptions({
+        pageLifetimes: {
+            // 组件所在页面的生命周期函数
+            resize: function () { }
+        }
+    })
+</script>
+
  • defineOptions 是只能在 <script setup> 中使用的编译宏,不需要手动导入,会跟随 <script setup> 的处理过程一同被编译掉。
  • defineOptions 无返回值。
  • defineOptions 中的选项会无脑提升至组件或页面构造器的选项之中,因此不可引用 setup 中的局部变量。

# useContext()

<script setup> 中,当我们想要使用 context 时,可以使用 useContext() 来获 context 对象。

<script setup>
+    const context = useContext()
+    // 获取 NodesRef/组件实例,等价于 this.$refs
+    console.log(context.refs)
+</script>
+

# 针对 TypeScript 的功能

# 类型 props 的声明

props 可以通过给 defineProps 传递纯类型函数的方式来声明:

    const props = defineProps<{
+        foo: string,
+        bar: number
+    }>()
+
+    // 构建转换为
+    {
+        properties: {
+            foo: {
+                type: String
+            },
+            bar: {
+                type: Number
+            }
+        }
+    }
+
  • defineProps 要么使用运行时声明,要么使用类型声明。同时使用两种方式会导致编译报错。
  • 小程序中 defineProps 类型声明若有 optional 不会生效,因为小程序的 props 只要声明则一定会存在
  • 类型声明参数必须是一下内容之一,以确保正确的静态分析: +
    • 类型字面量
    • 在同一文件中的接口或者类型字面量的引用

# 使用类型声明时的默认 props 值

和 Vue3 一样,针对类型的 defineProps 声明的不足,它无法给 props 提供默认值。为了解决这个问题,我们也支持了 withDefaults 编译宏:

export interface Props {
+  msg: string
+  labels: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  msg: 'hello',
+  labels: 'world'
+})
+

上边代码会被编译为等价的运行时 props 的 value 选项。

  • 小程序 properties 定义中的 optionalTypes 和 observer 字段,无法使用 TypeScript 类型声明的方式定义,如果需要定义这两个字段,目前需要使用运行时的方式来定义。

# 限制

由于模块执行语义的差异,<script setup> 中的代码依赖单文件组件的上下文,如果将其移动到外部的 .js 或者 .ts 的时候,对于开发者可工具来说都十分混乱。因此 <script setup> 不能和 src attribute 一起使用。

# 组合式 API 与 Vue3 中的区别

下面我们来总结一下 Mpx 中组合式 API 与 Vue3 中的区别:

  • setupcontext 参数不同,详见这里
  • setup 不支持返回渲染函数
  • setup 不能是异步函数
  • <script setup> 提供的宏方法不同,详见这里
  • <script setup> 不支持 import 快捷引入组件
  • <script setup> 必须使用 defineExpose
  • 支持的生命周期钩子不同,详见这里
  • 模板引用的方式不同,详见这里

# 组合式 API 周边生态能力的使用

我们对 Mpx 提供的周边生态能力也都进行了组合式 API 适配升级,详情如下:

  • store 在组合式 API 中使用,详见这里
  • pinia 在组合式 API 中使用,详见这里
  • fetch 在组合式 API 中使用,详见这里
  • i18n 在组合式 API 中使用,详见这里
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html b/docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html new file mode 100644 index 0000000000..2cdf69c97a --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html @@ -0,0 +1,386 @@ + + + + + + 响应式 API | Mpx框架 + + + + + + + + + +

# 响应式 API

在 Mpx 中,为了支持组合式 API 的使用,我们参考 Vue3 提供了相关的响应式 API,但由于 proxy 目前仍然存在浏览器兼容性问题,我们在底层还是基于 Object.defineProperty 实现的数据响应,因此相较于 Vue3 提供的 API 存在一些删减,同时也存在与 Vue2 一样的数据响应使用限制 (opens new window)

# 创建响应式对象

在 Mpx 中,我们可以使用 reactive 方法将一个 JavaScript 对象深度转换为响应式对象,当对象内数据发生变化时能够被系统感知,相当于 Vue2 中的 observable,需要注意的是在 Mpx 中 reactive() 是将传入的对象进行响应性转化后返回原对象,而在 Vue3 中则会基于 proxy API 返回传入对象的响应式代理。

目前 reactive 仅支持传入基础对象类型,包括纯对象和数组,暂不支持 MapSet 这样的集合类型。

import { reactive } from '@mpxjs/core'
+
+// 响应式对象
+const state = reactive({
+  count: 0
+})
+

你可以在响应式基础 API 章节中了解更多关于 reactive 的信息。

# 使用ref()创建独立的响应式值

上面提到 reactive 只能传入对象类型数据,当我们想将一个原始数据类型的值(如数字、字符串、布尔值)变成响应式时,我们不得不先将其包装为一个对象,使用起来较为繁琐,新的 ref 方法能够让我们便捷地达成上述目标:

import { ref } from '@mpxjs/core'
+
+// 响应式值
+const count = ref(0)
+

ref() 会返回一个可变的响应式对象,该对象作为一个响应式引用通过 value 属性维护着传入的内部值:

import { ref } from '@mpxjs/core'
+
+const count = ref(0)
+console.log(count.value) // 0
+
+count.value++
+console.log(count.value) // 1
+

# Ref 解包

ref 作为渲染上下文 (从 setup() 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动浅层次解包内部值。只有访问嵌套的 ref 时需要在模板中添加 .value

<template>
+  <view>
+    <view>{{ count }}</view>
+    <view>{{ nested.count.value }}</view>
+  </view>
+</template>
+
+<script>
+  import { createComponent, ref } from '@mpxjs/core'
+   
+  createComponent({
+    setup() {
+      const count = ref(0)
+      return {
+        count,
+        nested: {
+          count
+        }
+      }
+    }
+  }
+</script>
+

# 访问响应式对象

ref 作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动解包内部值:

const count = ref(0)
+const state = reactive({
+  count
+})
+
+console.log(state.count) // 0
+
+state.count = 1
+console.log(count.value) // 1
+

如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:

const otherCount = ref(2)
+
+state.count = otherCount
+console.log(state.count) // 2
+console.log(count.value) // 1
+

Ref 解包仅发生在被响应式 Object 嵌套的时候。当从 Array 访问 ref 时,不会进行解包:

const arr = reactive([ref('Hello world')])
+// 这里需要 .value
+console.log(arr[0].value)
+

# 响应式对象解构

当我们想使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构来获取我们想要的 property:

import { reactive } from '@mpxjs/core'
+
+const people = reactive({
+  name: 'hiyuki',
+  age: 26,
+  gender: 'male',
+  city: 'Beijing'
+})
+
+let { name, age } = people
+

遗憾的是,使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:

import { reactive, toRefs } from '@mpxjs/core'
+
+const people = reactive({
+  name: 'hiyuki',
+  age: 26,
+  gender: 'male',
+  city: 'Beijing'
+})
+
+let { name, age } = toRefs(people)
+age.value = 30 // age 现在是个 ref,我们需要使用 .value 进行访问,对其进行修改也将直接作用在原响应式对象中
+console.log(people.age) // 30
+

你可以在Refs API 章节中了解更多关于 refs 的信息。

# 计算值

有时我们需要依赖于其他状态的状态——在 选项式 API 中,这是用组件计算属性处理的,在新的组合式 API 中,我们可以使用 computed 函数直接创建计算值:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象。

import { ref, computed } from '@mpxjs/core'
+
+const count = ref(1)
+const plusOne = computed(() => count.value + 1)
+
+console.log(plusOne.value) // 2
+
+plusOne.value++ // error
+

或者,可以使用一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

import { ref, computed } from '@mpxjs/core'
+
+const count = ref(1)
+const plusOne = computed({
+  get: () => count.value + 1,
+  set: val => {
+    count.value = val - 1
+  }
+})
+
+plusOne.value = 1
+console.log(count.value) // 0
+

# watchEffect

为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

const count = ref(0)
+
+watchEffect(() => console.log(count.value))
+// -> logs 0
+
+setTimeout(() => {
+  count.value++
+  // -> logs 1
+}, 100)
+

# 停止侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

const stop = watchEffect(() => {
+  /* ... */
+})
+
+// later
+stop()
+

# 清除副作用

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
+  const token = performAsyncOperation(id.value)
+  onInvalidate(() => {
+    // id has changed or watcher is stopped.
+    // invalidate previously pending async operation
+    token.cancel()
+  })
+})
+

之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。

在执行数据请求时,副作用函数往往是一个异步函数:

const data = ref(null)
+watchEffect(async onInvalidate => {
+  onInvalidate(() => {
+    /* ... */
+  }) // 在Promise解析之前注册清除函数
+  data.value = await fetchData(props.id)
+})
+

我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。

# 副作用刷新时机

默认情况下,数据发生变更时,关联的副作用会被推入异步队列中,进行异步刷新,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 render 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 render 前执行:

<template>
+  <view>{{ count }}</view>
+</template>
+
+<script>
+import { createComponent, ref, watchEffect } from '@mpxjs/core'
+
+createComponent({
+  setup() {
+    const count = ref(0)
+
+    watchEffect(() => {
+      console.log(count.value)
+    })
+
+    return {
+      count
+    }
+  }
+})
+</script>
+

在这个例子中:

  • count 会在初始运行时同步打印出来
  • 更改 count 时,将在组件更新前执行副作用。

如果需要在组件更新(例如:当与模板引用一起)后重新运行侦听器副作用,我们可以传递带有 flush 选项的附加 options 对象 (默认为 'pre'):

// 在组件更新后触发,这样你就可以访问更新的 DOM。
+// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
+watchEffect(
+  () => {
+    /* 访问视图 */
+  },
+  {
+    flush: 'post'
+  }
+)
+

flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。

与此同时,我们还提供了 watchPostEffectwatchSyncEffect 别名用来让代码意图更加明显。

# watch

watch API 相当于选项式 API 中的 watch propertywatch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。

  • watchEffect 比较,watch 允许我们: +
    • 懒执行副作用;
    • 更具体地说明什么状态应该触发侦听器重新运行;
    • 访问侦听状态变化前后的值。

# 侦听单个数据源

侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref

// 侦听一个 getter
+const state = reactive({ count: 0 })
+watch(
+  () => state.count,
+  (count, prevCount) => {
+    /* ... */
+  }
+)
+
+// 直接侦听ref
+const count = ref(0)
+watch(count, (count, prevCount) => {
+  /* ... */
+})
+

# 侦听多个数据源

侦听器还可以使用数组同时侦听多个源:

const firstName = ref('')
+const lastName = ref('')
+
+watch([firstName, lastName], (newValues, prevValues) => {
+  console.log(newValues, prevValues)
+})
+
+firstName.value = 'John' // logs: ["John", ""] ["", ""]
+lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
+

尽管如此,如果你在同一个函数里同时改变这些被侦听的来源,侦听器仍只会执行一次:

createComponent({
+  setup() {
+    const firstName = ref('')
+    const lastName = ref('')
+  
+    watch([firstName, lastName], (newValues, prevValues) => {
+      console.log(newValues, prevValues)
+    })
+  
+    const changeValues = () => {
+      firstName.value = 'John'
+      lastName.value = 'Smith'
+      // 打印 ["John", "Smith"] ["", ""]
+    }
+  
+    return { changeValues }
+  }
+})
+

注意多个同步更改只会触发一次侦听器。

通过更改设置 flush: 'sync',我们可以为每个更改都强制触发侦听器,尽管这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行。例如:

const changeValues = async () => {
+  firstName.value = 'John' // 打印 ["John", ""] ["", ""]
+  await nextTick()
+  lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
+}
+

# 侦听响应式对象

为了侦听深度嵌套的对象或数组中 property 变化,我们需要将 deep 选项设置为 true:

const state = reactive({ 
+  id: 1,
+  attributes: { 
+    name: '',
+  }
+})
+
+watch(
+  () => state,
+  (state, prevState) => {
+    console.log('not deep', state.attributes.name, prevState.attributes.name)
+  }
+)
+
+watch(
+  () => state,
+  (state, prevState) => {
+    console.log('deep', state.attributes.name, prevState.attributes.name)
+  },
+  { deep: true }
+)
+
+state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"
+

然而,侦听一个响应式对象或数组将始终返回该对象的引用。为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝。这可以通过诸如 lodash.cloneDeep 这样的实用工具来实现:

import _ from 'lodash'
+
+const state = reactive({
+  id: 1,
+  attributes: {
+    name: '',
+  }
+})
+
+watch(
+  () => _.cloneDeep(state),
+  (state, prevState) => {
+    console.log(state.attributes.name, prevState.attributes.name)
+  }
+)
+
+state.attributes.name = 'Alex' // 日志: "Alex" ""
+

我们也可以直接给 watch() 传入一个响应式对象,这种情况下会隐式地强制开启 deep 选项,确保嵌套的深层变更能够被监听到:

const state = reactive({ 
+  id: 1,
+  attributes: { 
+    name: '',
+  }
+})
+
+watch(
+  state,
+  (state, prevState) => {
+    console.log('implicit deep', state.attributes.name, prevState.attributes.name)
+  }
+)
+
+state.attributes.name = 'Alex' // 日志: "implicit deep" "Alex" "Alex"
+

需要注意的是,在侦听使用 reactive() 创建的响应式对象时,受数据响应限制影响,在改变数组或使用 set() 新增对象属性时,存在和 Vue3 中表现不一致的情况,详情查看这里

# 立即回调的侦听器

同选项式 watch 一致,我们也可以通过传递 immediate 选项让侦听回调立即执行:

const state = reactive({ 
+  count: 0
+})
+
+watch(
+  () => state.count,
+  (value, oldValue) => {
+    console.log('immediate', value, oldValue) // 日志: "immediate" 0 undefined
+  },
+  { immediate: true }
+)
+

# 与 watchEffect 共享的行为

watchwatchEffect 共享停止侦听清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入)、副作用刷新时机行为。

# 响应式 API 与 Vue3 中的区别

下面我们来总结一下 Mpx 中响应式 API 与 Vue3 中的区别:

  • 不支持 raw 相关 API(markRaw 除外,我们提供了该 API 用于跳过部分数据的响应式转换)
  • 不支持 readonly 相关 API
  • 不支持 watchEffectwatchcomputed 的调试选项
  • 不支持对 mapset 等集合类型进行响应式转换
  • 受到 Object.defineProperty 实现带来的数据响应限制影响

# 数据响应限制带来的差异

同 Vue2 一致,Mpx 无法感知到对象 property 的添加或移除,我们暴露了 setdel API 来让用户显式地进行相关操作:

import { ref, watchSyncEffect, set, del } from '@mpxjs/core'
+
+const state = ref({
+  count: 0
+})
+
+watchSyncEffect(() => {
+  console.log(JSON.stringify(state.value)) // {"count":0}
+})
+
+set(state.value, 'hello', 'world') // {"count":0,"hello":"world"}
+
+del(state.value, 'count') // {"hello":"world"}
+

同样,我们需要使用 set 或数组原型方法对数组进行修改:

import { ref, watchSyncEffect, set } from '@mpxjs/core'
+
+const state = ref([0, 1, 2, 3])
+
+watchSyncEffect(() => {
+  console.log(JSON.stringify(state.value)) // [0,1,2,3]
+})
+
+set(state.value, 1, 3) // [0,3,2,3]
+
+state.value.push(4) // [0,3,2,3,4]
+

可能你已经注意到,上面两个示例当中我们都使用了 ref() 进行响应式数据创建,这是有原因的,在新的响应式 API 模式下,我们使用 reactive() 创建的响应式数据在上述情况下仍然无法绕过 Vue2 设计中的数据响应限制,即使你使用了 set 或数组原型方法:

import { reactive, watchSyncEffect, set } from '@mpxjs/core'
+
+const state = reactive([0, 1, 2, 3])
+
+watchSyncEffect(() => {
+  console.log(JSON.stringify(state)) // [0,1,2,3]
+})
+
+set(state, 1, 3) // 不会触发 watchEffect
+
+state.push(4) // 不会触发 watchEffect
+

对于对象和在模板中使用也是同理:

<template>
+  <view bindtap="addCount2">
+    <view>{{state.count}}</view>
+    <view>{{state.count2}}</view>
+  </view>
+
+</template>
+
+<script>
+  import { createComponent, reactive, set } from '@mpxjs/core'
+  
+  createComponent({
+    setup () {
+      const state = reactive({
+        count: 0
+      })
+
+      const addCount2 = () => {
+        set(state, 'count2', 10) // 不会触发视图更新
+      }
+
+      return {
+        state,
+        addCount2
+      }
+    }
+  })
+</script>
+

为什么会产生这个现象呢?原因在于:基于 Object.defineProperty 实现的数据响应系统中,我们会对对象的每个已有属性创建了一个 Dep 对象,在对该属性进行 get 访问时通过这个对象将其与依赖它的观察者 ReactiveEffect 关联起来,并在 set 操作时触发关联 ReactiveEffect 的更新,这是我们大家都知道的数据响应的基本原理。但是对于新增/删除对象属性和修改数组的场景,我们无法事先定义当前不存在属性的 get/set (当然这在 proxy 当中是可行的),因此我们会把对象或者数组本身作为一个数据依赖创建 Dep 对象,通过父级访问该数据时定义的 get/set 将其关联到对应的 ReactiveEffect,并在对数据进行新增/删除属性或数组操作时通过数据本身持有的 Dep 对象触发关联 ReactiveEffect 的更新,如下图所示:

数据响应原理

需要注意的是,通过父级访问是建立 DepReactiveEffect 关联关系的先决条件,在选项式 API 中,我们访问组件的响应式数据都需要通过 this 进行访问,相当于这些数据都存在 this 这个必要的父级,因此我们在使用 $set/$delete 进行对对象进行新增/删除属性或对数组进行修改时都能得到符合预期的结果,唯一的限制在于不能新增/删除根级数据属性,原因就在于 this 不存在访问它的父级。

但是在组合式 API 中,我们不需要通过 this 访问响应式数据,因此通过 reactive() 创建的响应式数据本身就是根级数据,我们自然无法通过上述方式感知到根级数据自身的变化(在 Vue3 中,基于 proxy 提供的强大能力响应式系统能够精确地感知到数据属性,甚至是当前不存在属性的访问与修改,不需要为数据自身建立 Dep 对象,自然也不存在相关问题)。

在这种情况下,我们就需要用 ref() 创建响应式数据,因为 ref 创建了一个包装对象,我们永远需要通过 .value 来访问其持有的数据(不管是显式访问还是隐式自动解包),这样就能保证 ref 数据自身的变化能够被响应式系统感知,因此也不会遇到上面描述的问题。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html b/docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html new file mode 100644 index 0000000000..3dc598d21e --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html @@ -0,0 +1,100 @@ + + + + + + Api代理 | Mpx框架 + + + + + + + + + +

# Api代理

convert API at each end 各个平台之间 api 进行转换,目前已支持微信转支付宝、微信转web

# Usage

// 使用 mpx 生态
+
+import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+
+mpx.use(apiProxy, options)
+
// 脱离 Mpx 单独使用
+import { getProxy } from '@mpxjs/api-proxy'
+// proxy 即为target 实例
+const proxy = getProxy(options)
+
+proxy.navigateTo({
+  url: '/pages/test',
+  success (res) {
+    console.log(res)
+  }
+})
+

# Options

参数名称 类型 含义 是否必填 默认值 备注
platform Object 各平台之间的转换 { from:'', to:'' } 使用 mpx 脚手架配置会自动进行转换,无需配置
exclude Array(String) 跨平台时不需要转换的 api -
usePromise Boolean 是否将 api 转化为 promise 格式使用 false -
whiteList Array(String) 强行转化为 promise 格式的 api [] 需要 usePromise 设为 true
blackList Array(String) 强制不变成 promise 格式的 api [] -
fallbackMap Object 对于不支持的API,允许配置一个映射表,接管不存在的API {} -

# example

# 普通形式

import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+
+mpx.use(apiProxy, {
+  exclude: ['showToast'] // showToast 将不会被转换为目标平台
+})
+
+mpx.showModal({
+  title: '标题',
+  content: '这是一个弹窗',
+  success (res) {
+    if (res.cancel) {
+      console.log('用户点击取消')
+    }
+  }
+})
+

# 使用promise形式

开启usePromise时所有的异步api将返回promise,但是小程序中存在一些异步api本身的返回值是具有意义的,如uploadFile会返回一个uploadTask对象用于后续监听上传进度或者取消上传,在对api进行promise化之后我们会将原始的返回值挂载到promise.__returned属性上,便于开发者访问使用

import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+
+mpx.use(apiProxy, {
+  usePromise: true
+})
+
+mpx.showActionSheet({
+  itemList: ['A', 'B', 'C']
+})
+.then(res => {
+  console.log(res.tapIndex)
+})
+.catch(err => {
+  console.log(err)
+})
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/extend/fetch.html b/docs-vuepress/.vuepress/dist/guide/extend/fetch.html new file mode 100644 index 0000000000..7d412bec01 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/extend/fetch.html @@ -0,0 +1,139 @@ + + + + + + 网络请求 | Mpx框架 + + + + + + + + + +

# 网络请求

Mpx 提供了网络请求库 fetch,抹平了微信,阿里等平台请求参数及响应数据的差异;同时支持请求拦截器,请求取消等

# 使用说明

import mpx from '@mpxjs/core'
+import mpxFetch from '@mpxjs/fetch'
+mpx.use(mpxFetch)
+// 第一种访问形式
+mpx.xfetch.fetch({
+	url: 'http://xxx.com'
+}).then(res => {
+	console.log(res.data)
+})
+
+mpx.createApp({
+	onLaunch() {
+		// 第二种访问形式
+		this.$xfetch.fetch({url: 'http://test.com'})
+	}
+})
+

# 导出说明

mpx-fetch提供了一个实例 xfetch ,该实例包含以下api

  • fetch(config), 正常的promisify风格的请求方法
  • CancelToken,实例属性,用于创建一个取消请求的凭证。
  • interceptors,实例属性,用于添加拦截器,包含两个属性,request & response

# 请求拦截器

mpx.xfetch.interceptors.request.use(function(config) {
+	console.log(config)
+	// 也可以返回promise
+	return config
+})
+mpx.xfetch.interceptors.response.use(function(res) {
+	console.log(res)
+	// 也可以返回promise
+	return res
+})
+

# 请求中断

const cancelToken = new mpx.xfetch.CancelToken()
+mpx.xfetch.fetch({
+	url: 'http://xxx.com',
+	data: {
+		name: 'test'
+	},
+	cancelToken: cancelToken.token
+})
+cancelToken.exec('手动取消请求') // 执行后请求中断,返回abort fail
+

# 设置请求参数

mpx.xfetch.fetch({
+	url: 'http://xxx.com',
+	params: {
+		name: 'test'
+	}
+})
+
+mpx.xfetch.fetch({
+	url: 'http://xxx.com',
+	method: 'POST',
+	// params 参数等价于 url query
+	params: {
+		age: 10
+	},
+	data: {
+		name: 'test'
+	},
+	// 设置参数序列化方式,等价于header = {'content-type': 'application/x-www-form-urlencoded'}
+	emulateJSON: true
+})
+

# 设置请求 timeout

mpx.xfetch.fetch({
+	url: 'http://xxx.com',
+	method: 'POST',
+	data: {
+		name: 'test'
+	},
+	timeout: 10000 // 超时时间
+})
+

# 在组合式 API 中使用

在组合式 API 中我们提供了 useFetch 方法来访问 xfetch 实例对象

// app.mpx
+import mpx, { createComponent } from '@mpxjs/core'
+import { useFetch } from '@mpxjs/fetch'
+
+createComponent({
+  setup() {
+      useFetch().fetch({
+          url: 'http://xxx.com',
+          method: 'POST',
+          params: {
+              age: 10
+          },
+          data: {
+              name: 'test'
+          },
+          emulateJSON: true,
+          usePre: true,
+          cacheInvalidationTime: 3000,
+          ignorePreParamKeys: ['timestamp']
+      }).then(res => {
+          console.log(res.data)
+      })   
+  }
+})
+
+
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/extend/index.html b/docs-vuepress/.vuepress/dist/guide/extend/index.html new file mode 100644 index 0000000000..ce8aee8acf --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/extend/index.html @@ -0,0 +1,62 @@ + + + + + + 扩展mpx | Mpx框架 + + + + + + + + + +

# 扩展mpx

# 开发插件

mpx支持使用mpx.use使用插件来进行扩展。插件本身需要提供一个install方法或本身是一个function,该函数接收一个proxyMPX。插件将采用直接在proxyMPX挂载新api属性或在prototype上挂属性。需要注意的是,一定要在app创建之前进行mpx.use。

简单示例如下:

export default function install(proxyMPX) {
+  proxyMPX.newApi = () => console.log('is new api')
+  proxyMPX
+    .mixin({
+      onLaunch() {
+        console.log('app onLaunch')
+      }
+    }, 'app')
+    .mixin({
+      onShow() {
+        console.log('page onShow')
+      }
+    }, 'page') // proxyMPX.injectMixins === proxyMPX.mixin
+
+    //  注意:proxyMPX.prototype上挂载的属性都将挂载到组件实例(page实例、app实例上,可以直接通过this访问), 可以看mixin中的case
+    proxyMPX.prototype.testHello = function() {
+      console.log('hello')
+    }
+}
+

# 目前已有插件

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/extend/mock.html b/docs-vuepress/.vuepress/dist/guide/extend/mock.html new file mode 100644 index 0000000000..322e2bd0c4 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/extend/mock.html @@ -0,0 +1,177 @@ + + + + + + 数据 Mock | Mpx框架 + + + + + + + + + +

# 数据 Mock

# 安装

Mpx 提供了对请求响应数据进行拦截的 mock 插件,可通过如下命令进行安装:

npm i @mpxjs/mock
+

# 使用说明

新建 mock 文件目录及文件(例如:src/mock/index.js ):

// src/mock/index.js
+import mock from "@mpxjs/mock";
+mock([
+  {
+    url: "http://api.example.com",
+    rule: {
+      "list|1-10": [
+        {
+          "id|+1": 1
+        }
+      ]
+    }
+  }
+]);
+

在入口文件( app.mpx )中引入:

<script type="text/javascript">
+  import "mock/index"; // 引入mock即可
+</script>
+<!-- 其他配置 -->
+<script type="application/json">
+  {
+    "pages": ["./pages/index"],
+    "window": {
+      "backgroundTextStyle": "light",
+      "navigationBarBackgroundColor": "#fff",
+      "navigationBarTitleText": "WeChat",
+      "navigationBarTextStyle": "black"
+    }
+  }
+</script>
+

由于 mock 为全局自动代理,执行@mpxjs/mock所暴露的方法之后会立即拦截小程序的原生请求,如果需要根据不同环境变量等去控制是否使用 mock 数据,可以参考如下方法:

// src/mock/index.js
+import mock from "@mpxjs/mock";
+export default () => mock([
+  {
+    url: "http://api.example.com",
+    rule: {
+      "list|1-10": [
+        {
+          "id|+1": 1
+        }
+      ]
+    }
+  }
+]);
+
<!-- app.mpx -->
+<script type="text/javascript">
+  import mockSetup from "mock/index";
+  // 当为开发环境时才启用mock
+  process.env.NODE_ENV === "development" && mockSetup();
+</script>
+

# Mock 入参

@mpxjs/mock 所暴露的函数仅接收一个类型为 mockRequstList 的参数,该类型定义如下:

type mockItem = {
+  url: string,
+  rule: object
+}
+type mockRequstList = Array<mockItem>
+
+//示例:
+let mockList: mockRequstList = [
+  {
+    url: "http://api.example.com", // 请求触发后匹配到该链接时其响应数据会被mock拦截
+    rule: { // mock生成返回数据的规则
+      'number|1-10': 1
+    }
+  }
+]
+

# Mock 规则示例

  • 基本类型数据生成
import mock from "@mpxjs/mock";
+mock([
+  {
+    url: "http://api.example.com",
+    rule: {
+      "number|1-10": 1, // 随机生成1-10中的任意整数
+      "string|6": /[0-9a-f]/, // 值支持正则表达式,随机生成6位的16进制值
+      "boolean|1": true // 随机生成一个布尔值,值为 true 的概率是 1/2
+    }
+  }
+]);
+// 请求 http://api.example.com 后返回值为:
+// {
+//   number: 2,
+//   string: "e1e6dc",
+//   boolean: false
+// }
+
  • 生成随机长度id自增的列表
查看示例
import mock from "@mpxjs/mock";
+mock([
+  {
+    url: "http://api.example.com",
+    rule: {
+      "list|2-5": [ // 生成长度范围在2-5的数组
+        {
+          "id|+1": 0 // id每次自增1
+        }
+      ]
+    }
+  }
+]);
+// 请求 http://api.example.com 后返回
+// {
+//   "list": [{
+//     "id": 0
+//   },{
+//     "id": 1
+//   },{
+//     "id": 2
+//   }]
+// }
+
  • pick对象中的随机个值
查看示例
import mock from "@mpxjs/mock";
+mock([
+  {
+    url: "http://api.example.com",
+    rule: {
+      "object|2": { // 随机选取object中的两条数据作为返回
+        "310000": "上海市",
+        "320000": "江苏省",
+        "330000": "浙江省",
+        "340000": "安徽省"
+      }
+    }
+  }
+]);
+// 请求 http://api.example.com 后返回
+// {
+//   "object": {
+//     "330000": "浙江省",
+//     "340000": "安徽省"
+//   }
+// }
+

更多生成规则可查阅 Mock官方文档-Syntax Specification (opens new window)

更多示例可查看 Mock示例 (opens new window)

WARNING

由于小程序环境的局限性,mockjs 依赖 eval 函数实现的相关能力(如:占位符)无法正确运行

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/2.7.html b/docs-vuepress/.vuepress/dist/guide/migrate/2.7.html new file mode 100644 index 0000000000..c374438e70 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/migrate/2.7.html @@ -0,0 +1,172 @@ + + + + + + 从旧版本迁移至 2.7 | Mpx框架 + + + + + + + + + +

# 从旧版本迁移至 2.7

自 2.7 版本开始,Mpx 将编译构建流程基于 Webpack5 进行了全面的重构,优化解决了老的编译流程中存在的大量历史遗留问题,安全完整地支持了基于文件系统的持久化缓存,大幅提升了 Mpx 想的编译构建速度,以滴滴出行小程序为例,无缓存场景下相较 2.6 版本提速约 180%,有缓存场景下提速约 10 倍。

与此同时,Mpx 2.7 版本还带来了 rules 复用,完善的单元测试支持,独立分包构建,分包异步化等众多新特性。直接通过 @mpxjs/cli 创建的新项目就能够直接使用这些新特性,如果你对于编译构建没有太多定制化诉求,我们也推荐使用 @mpxjs/cli 新建项目,并将老项目中的 src 目录 copy 覆盖至新项目的方式进行迁移,这是一种成本最低的迁移方式。

如果你的老项目中已经对编译构建进行了高度的定制,本文将提供详细的迁移工作指引。

# 依赖升级

将下面列出的相关依赖升级至对应版本,如果有其他自行引入的 loader 也确保将其升级至 webpack5 兼容的版本:

{
+  "dependencies": {
+    "@mpxjs/api-proxy": "^2.7.1",
+    "@mpxjs/core": "^2.7.1",
+    "@mpxjs/fetch": "^2.7.1",
+  },
+  "devDependencies": {
+    "@mpxjs/webpack-plugin": "^2.7.1",
+    "webpack": "^5.64.4",
+    "webpack-merge": "^5.8.0",
+    "terser-webpack-plugin": "^5.2.5",
+    "copy-webpack-plugin": "^9.0.1",
+    "webpack-bundle-analyzer": "^4.5.0",
+    "ts-loader": "^9.2.6",
+    // eslint相关配置,按需安装
+    "eslint": "^7.32.0",
+    "eslint-config-standard": "^16.0.3",
+    "eslint-plugin-html": "^6.2.0",
+    "eslint-plugin-import": "^2.25.2",
+    "eslint-plugin-node": "^11.1.0",
+    "eslint-plugin-promise": "^5.1.1",
+    "eslint-webpack-plugin": "^3.1.0",
+  }
+}
+

# 开启持久化缓存

在 webpack 配置中添加以下配置项:

module.exports = {
+  cache: {
+    type: 'filesystem',
+    // 声明构建配置,注意如果声明某个文件夹为构建配置,需要在文件夹下放置空的package.json文件,避免构建依赖收集时将主项目的依赖项视为构建依赖
+    buildDependencies: {
+      build: [resolve('build/')],
+      config: [resolve('config/')]
+    },
+    cacheDirectory: resolve('.cache/')
+  },
+  snapshot: {
+    // 如果希望修改node_modules下的文件时对应的缓存可以失效,可以将此处的配置改为 managedPaths: []
+    managedPaths: [resolve('node_modules/')]
+  },
+}
+

# 修改rules

在 2.7 版本中,我们优化了 .mpx 文件中各个 block 应用 loader 的规则。

在之前的版本中,我们基本上使用内置的规则对 .mpx 中的 block 应用 loaders,如:对 <script> 应用 babel-loader,对 <style lang="stylus"> 应用 stylus-loader。这种方式的弊端主要在于用户无法便捷地自定义 block 的 loaders 应用规则,比如传入特定的 loaders 配置。

在 2.7 版本中,我们支持了构建时读取用户在 module.rules 中配置的规则来对 block 进行 loaders 应用, 例如:对于 <style lang="stylus"> 我们会对其应用用户在 rules 中对于 .stylus 文件 配置的 loaders,当用户没有在 block 中声明 lang 属性时,我们也会兜底使用不同区块默认的 lang 来进行 rules 匹配。

因此,相较于过去的版本,我们需要在 rules 中添加一些规则使得 .mpx 文件中的各类 block 都能得到正确的处理。

module.exports = {
+  module: {
+    rules: [
+      // 新版本中rules规则对.mpx文件中的区块同样生效,在旧版本中.mpx文件中script会走内置的babel转义,但是新版当中只有在include范围内的.mpx文件才会走babel,但由于新版本中.mpx文件中的script会采用.mpx.js的格式来匹配rules,因此我们可以用如下include条件让其保持与旧版本一致的表现。
+      {
+        test: /\.js$/,
+        loader: 'babel-loader',
+        include: [/\.mpx\.js/, resolve('src'), resolve('test'), resolve('node_modules/@mpxjs')]
+      },
+      // 如使用ts编码则需要添加本条rule
+      {
+        test: /\.ts$/,
+        use: [
+          'babel-loader',
+          {
+            loader: 'ts-loader',
+            options: {
+              appendTsSuffixTo: [/\.(mpx|vue)$/]
+            }
+          }
+        ]
+      },
+      // 处理json区块
+      {
+        test: /\.json$/,
+        resourceQuery: /asScript/,
+        type: 'javascript/auto'
+      },
+      // 处理wxs
+      {
+        test: /\.(wxs|qs|sjs|qjs|jds|dds|filter\.js)$/,
+        use: [
+          MpxWebpackPlugin.wxsPreLoader()
+        ],
+        enforce: 'pre'
+      },
+      // 处理图像资源
+      {
+        test: /\.(png|jpe?g|gif|svg)$/,
+        use: [
+          MpxWebpackPlugin.urlLoader({
+            name: 'img/[name][hash].[ext]'
+          })
+        ]
+      },
+      // 下面的rules输出小程序时需配置,如输出web需要修改为vue的相关rules,具体逻辑可以参考脚手架项目中build/getRules.js
+      // mpx主loader
+      {
+        test: /\.mpx$/,
+        use: MpxWebpackPlugin.loader(currentMpxLoaderConf)
+      },
+      // 如使用sass/less等其他预编译语言,自行修改test规则并用sass/less-loader替换stylus-loader
+      {
+        test: /\.styl(us)?$/,
+        use: [
+          MpxWebpackPlugin.wxssLoader(),
+          'stylus-loader'
+        ]
+      },
+      // 处理未使用预编译语言的style区块和独立样式文件
+      {
+        test: /\.(wxss|acss|css|qss|ttss|jxss|ddss)$/,
+        use: MpxWebpackPlugin.wxssLoader()
+      },
+      // 处理未使用预编译语言的template区块和独立模板文件
+      {
+        test: /\.(wxml|axml|swan|qml|ttml|qxml|jxml|ddml)$/,
+        use: MpxWebpackPlugin.wxmlLoader()
+      }
+    ]
+  }
+}
+

# entry配置修改

如果进行常规的小程序输出,无需关注。如进行插件输出或独立输出组件/页面等特殊场景,mpx 提供了辅助方法来生成对应的 entry request,示例如下:

module.exports = {
+  entry:{
+    // 输出plugin
+    plugin: MpxWebpackPlugin.getPluginEntry('./plugin.json'),
+    // 独立输出组件,相当于./list.mpx?isComponent,不过为了保障后续该配置不受框架内部query变动影响,建议使用辅助方法
+    list: MpxWebpackPlugin.getComponentEntry('./list.mpx'),
+    // 独立输出页面,相当于./index.mpx?isPage
+    index: MpxWebpackPlugin.getPageEntry('./index.mpx')
+  }
+}
+

# MpxWebpackPlugin配置变更

  • 移除了forceDisableInject配置
  • nativeOptions更名为nativeConfig

上述的内容中只列举了一些和 mpx 高度相关的配置项变更,如果你的项目使用了特定的插件或者loader,请确保将其升级至兼容 webpack5 的版本。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/2.8.html b/docs-vuepress/.vuepress/dist/guide/migrate/2.8.html new file mode 100644 index 0000000000..b88fd302bf --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/migrate/2.8.html @@ -0,0 +1,85 @@ + + + + + + 从 2.7 升级至 2.8 | Mpx框架 + + + + + + + + + +

# 从 2.7 升级至 2.8

不同于 2.7 版本对于编译构建进行的大幅度变动升级,Mpx@2.8 版本升级的核心在于运行时支持组合式 API 能力,并尽可能追求向前兼容,以降低用户的升级成本。我们默认脚手架生成的全新项目已经适配了 2.8 版本的全部变更,如果你需要将旧项目从 2.7 升级至 2.8 版本,下面是详细的升级指引。

# 依赖升级

请按照下方列表升级或新增相关依赖:

{
+  "dependencies": {
+    "@mpxjs/api-proxy": "^2.8.0",
+    "@mpxjs/core": "^2.8.0",
+    "@mpxjs/store": "^2.8.0",
+    "@mpxjs/pinia": "^2.8.0",
+    "@mpxjs/utils": "^2.8.0",
+    "@mpxjs/fetch": "^2.8.0",
+    // 这部分依赖为输出 web 专用,如项目无需输出 web 可以省略
+    "vue": "^2.7.10",
+    "vue-demi": "^0.13.11",
+    "vue-i18n": "^8.27.2",
+    "vue-i18n-bridge": "^9.2.2"
+  },
+  "devDependencies": {
+    "@mpxjs/webpack-plugin": "^2.8.0",
+    "@mpxjs/size-report": "^2.8.0",
+    "@mpxjs/babel-plugin-inject-page-events": "^2.8.0",
+    // ...
+  }
+}
+

# 编译配置变更

在编译配置方面,用户几乎不用进行任何改变,唯一的例外是当用户想要通过组合式 API 的方式注册具有副作用的页面事件时,需要在 babel 配置中添加 @mpxjs/babel-plugin-inject-page-events 插件,如下所示:

// babel.config.json
+{
+ "plugins": [
+    [
+      "@babel/transform-runtime",
+      {
+        "corejs": 3,
+        "version": "^7.10.4"
+      }
+    ],
+    "@mpxjs/babel-plugin-inject-page-events"
+  ]
+}
+

关于副作用页面事件的更多详情查看这里

# 运行时破坏性变化

在 2.8 开发过程中,我们修正了过去版本中存在的不合理的设计与实现,在运行时带来了少许破坏性改变,详情如下:

  • 框架过往提供的组件增强生命周期 pageShow/pageHide 与微信原生提供的 pageLifetimes.show/hide 完全对齐,不再提供组件初始挂载时必定执行 pageShow 的保障(因为组件可能在后台页面进行挂载),相关初始化逻辑一定不要放置在 pageShow 当中;
  • 取消了框架过去提供的基于内部生命周期实现的非标准增强生命周期,如 beforeCreate/onBeforeCreate 等,直接将内部生命周期变量导出提供给用户使用,详情查看这里
  • 为了优化 tree shaking,作为框架运行时 default exportMpx 对象不再挂载 createComponent/createStore 等运行时方法,一律通过 named export 提供,Mpx 对象上仅保留 set/use 等全局 API;
  • 使用 I18n 能力时,为了与新版 vue-i18n 保持对齐,this.$i18n 对象指向全局作用域,如需创建局部作用域需要使用组合式 API useI18n 的方式进行创建。
  • watch API 不再接受第二个参数为带有 handler 属性的对象形式(该参数形式只应存在于 watch option 中),第二个参数必须为回调函数,与 Vue (opens new window) 对齐。
+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/2.9.html b/docs-vuepress/.vuepress/dist/guide/migrate/2.9.html new file mode 100644 index 0000000000..e11caac560 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/migrate/2.9.html @@ -0,0 +1,76 @@ + + + + + + 从 2.8 升级至 2.9 | Mpx框架 + + + + + + + + + +

# 从 2.8 升级至 2.9

Mpx 2.9 版本不包含破坏性变更,主要新增了原子类支持、输出 web 支持 SSR 和构建产物体积优化三项核心特性,更详细的介绍查看这里

# 依赖升级

如果你从既有项目中进行升级,仅需将 @mpxjs 下的依赖升级至 2.9 即可

{
+  "dependencies": {
+    "@mpxjs/api-proxy": "^2.9.0",
+    "@mpxjs/core": "^2.9.0",
+    "@mpxjs/store": "^2.9.0",
+    "@mpxjs/pinia": "^2.9.0",
+    "@mpxjs/utils": "^2.9.0",
+    "@mpxjs/fetch": "^2.9.0",
+    // ...
+  },
+  "devDependencies": {
+    "@mpxjs/webpack-plugin": "^2.9.0",
+    "@mpxjs/size-report": "^2.9.0",
+    "@mpxjs/babel-plugin-inject-page-events": "^2.9.0",
+    // 如需使用原子类,加入以下依赖
+    "@mpxjs/unocss-plugin": "^2.9.0",
+    "@mpxjs/unocss-base": "^2.9.0",
+    // 如需使用SSR,加入以下依赖
+    "axios": "^1.6.0",
+    "express": "^4.18.2",
+    "serve-favicon": "^2.5.0",
+    "vue-server-renderer": "^2.7.14",
+    // ...
+  }
+}
+

# 使用原子类

详情查看这里

# 使用SSR

详情查看这里

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html b/docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html new file mode 100644 index 0000000000..efbd718296 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html @@ -0,0 +1,97 @@ + + + + + + mpx-cli v2 迁移到 v3 | Mpx框架 + + + + + + + + + +

# mpx-cli v2 迁移到 v3

# 升级@mpxjs/cli

npm install @mpxjs/cli@3.x -g
+

# 配置迁移

v3 兼容了 v2 的所有配置,如果没有特殊修改,则不需要进行配置迁移。

  • config/devServer.js迁移到vue.config.js下的devServer
  • config/mpxPlugin.conf.js迁移到vue.config.js下的pluginOptions.mpx.plugin
  • config/mpxLoader.conf.js迁移到vue.config.js下的pluginOptions.mpx.loader
// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      srcMode: 'wx',
+      plugin: {
+        // 这里等同于`@mpxjs/webpack-plugin`的参数
+      },
+      loader: {
+        // 这里等同于`mpx-loader`参数
+      }
+    }
+  },
+  devServer: {
+    // dev服务配置
+  }
+})
+

# 新增自定义配置/修改已有配置参数

// vue.config.js
+const { defineConfig } = require('@vue/cli-service')
+
+module.exports = defineConfig({
+  chainWebpack(config) {
+    config.plugin('newPlugin').use(newPlugin, [params])
+    // 使用mpx inspect 可以根据注释来查看插件命名
+    config.plugin('mpx-webpack-plugin').tap(args => newArgs)
+  },
+  // 或者也可以通过configureWebpack配置,这里返回的配置会通过webpack-merge合并到内部配置中
+  configureWebpack() {
+    return {
+      plugins: [new Plugin()]
+    }
+  }
+})
+

# 编译后钩子

由于 webpack 配置都内置到了插件里,所以编译后的钩子无法像 2.x 一样直接在webpack脚本里添加。

这里有两个方案来解决上述问题

  1. webpack插件
  2. 新建一个mpx-cli插件

例如我们新建一个vue-cli-plugin-mpx-build-upload插件,用来在构建完成后上传图片到cdn

// index.js
+module.exports = function (api, options) {
+  function runServiceCommand(api, command, ...args){
+    const { fn } = api.service.commands[command]
+    return fn && fn(...args)
+  }
+  // 注册一个新的命令
+  api.registerCommand('build:upload', function deploy(...args) {
+    // 运行原有的build命令,build会返回一个promise来表示构建完成
+    return runServiceCommand(api, 'build', ...args).then(() =>
+      // do something
+      uploadFile()
+    )
+  })
+}
+

然后在我们的项目里安装该插件并运行npx mpx-cli-service build:upload即可。

# 项目结构变化

项目结构变化

v3 版本相对于 v2 版本的目录结构更加清晰。

  • 移除了config/build的配置目录,将其统一到了插件配置当中,可以通过vue.config.js修改。
  • index.html移动到public目录下。
  • 增加jsconfig.json,让类型提示更加友好。

# More

v3 版本相对于 v2 版本的整体架构相差较大,v3 版本主要基于vue-cli架构,主要有以下优势。

# 1. 插件化

v3 版本的配置依靠插件化,将 v2 版本的文件配置整合到了各个自定义插件中。

  • vue-cli-plugin-mpx-eslint eslint 配置
  • vue-cli-plugin-mpx-mp 小程序构建配置以及命令
  • vue-cli-plugin-mpx-plugin-mode 插件配置
  • vue-cli-plugin-mpx-typescript ts 配置
  • vue-cli-plugin-mpx-web web 构建配置以及命令

除此之外,也可以使用统一的vue.config.js来自定义配置,或者将配置抽离到插件当中,来进行统一的管理。

# 2. 模板

v3 版本的模板也可以通过插件进行自定义生成,同时不依赖于 github,在国内网络下不会有生成模板时网络错误的问题。

# 3. 调试

v3 版本可以通过mpx inspect:mp/web来直接调试相关配置,可以更直观的发现配置错误。

# 4. 插件管理

使用mpx invoke/mpx add/mpx upgrade来管理插件,可以更细粒度的控制相关配置的更新。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/platform/index.html b/docs-vuepress/.vuepress/dist/guide/platform/index.html new file mode 100644 index 0000000000..43b6e08b46 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/platform/index.html @@ -0,0 +1,225 @@ + + + + + + 跨端输出基础 | Mpx框架 + + + + + + + + + +

# 跨端输出基础

Mpx以微信增强DSL为基础,支持跨端输出至多端小程序、web和客户端,包括支付宝、百度、抖音、京东、QQ等多端小程序平台,基于Vue的web平台,和基于react-native的ios、android及鸿蒙平台。

# 跨端输出配置

配置mpx进行跨端输出十分简单,找到项目构建的webpack配置,在@mpxjs/webpack-plugin的配置参数中设置mode和srcMode参数即可。

new MpxwebpackPlugin({
+  // mode为mpx编译的目标平台,可选值有(wx|ali|swan|qq|tt|jd|web|ios|android|harmony)
+  mode: 'ali',
+  // srcMode为mpx编译的源码平台,目前仅支持wx   
+  srcMode: 'wx'
+})
+

对于使用 @mpxjs/cli 创建的项目,可以通过在 npm script 当中定义 targets 来设置编译的目标平台,多个平台标识以,分隔。

// 项目 package.json
+{
+  "script": {
+    "build:cross": "mpx-cli-service build --targets=wx,ali,ios,android"
+  }
+}
+

# 跨端条件编译

Mpx跨端输出时在框架内针对不同平台的差异进行了大量的转换抹平工作,但框架能做的工作始终是有限的,对于框架无法抹平的部分我们会在编译和运行时进行报错提示,同时提供了完善的跨平台条件编译机制,便于用户自行进行差异化处理,该能力也能够用于实现区分平台进行业务逻辑实现。

Mpx中我们支持了三种维度的条件编译,分别是文件维度,区块维度和代码维度,其中,文件维度和区块维度主要用于处理一些大块的平台差异性逻辑,而代码维度主要用于处理一些局部简单的平台差异。

# 文件维度条件编译

文件维度条件编译简单的来说就是文件为维度进行跨平台差异代码的编写,例如在微信->支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,这时你可以在相同的位置新建一个map.ali.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的mode来加载对应模块,当mode为ali时,会优先加载map.ali.mpx,反之则会加载map.mpx。

文件维度条件编译能够与webpack alias结合使用,对于npm包的文件我们并不方便在原本的文件位置创建.ali的条件编译文件,但我们可以通过webpack alias在相同位置创建一个虚拟的.ali文件,并将其指向项目中的其他文件位置。

  // 对于npm包中的文件依赖
+  import npmModule from 'somePackage/lib/index'
+
+  // 配置以下alias后,当mode为ali时,会优先加载项目目录中定义的projectRoot/somePackage/lib/index文件
+  // vue.config.js
+  module.exports = defineConfig({
+    configureWebpack() {
+      return {
+        resolve: {
+          alias: {
+            'somePackage/lib/index.ali': 'projectRoot/somePackage/lib/index'
+          }
+        }
+      }
+    }
+  })
+

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加mode属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali时使用如下区块-->
+<template mode="ali">
+<!--该区块中的所有代码需采用支付宝的技术标准进行编写-->
+  <view>支付宝环境</view>
+</template>
+
+<!--其他编译mode时使用如下区块-->
+<template>
+  <view>其他环境</view>
+</template>
+

# 代码维度条件编译

如果只有局部的代码存在跨平台差异,mpx同样支持在代码内使用if/else进行局部条件编译,用户可以在js代码和template插值中访问__mpx_mode__获取当前编译mode,进行平台差异逻辑编写,js代码中使用示例如下。

除了 __mpx_mode__ 这个默认插值以外,有别的环境变量需要的话可以在mpx.plugin.conf.js里通过defs进行配置。

if(__mpx_mode__ === 'ali') {
+  // 执行支付宝环境相关逻辑
+} else {
+  // 执行其他环境相关逻辑
+}
+

template代码中使用示例如下

<!--此处的__mpx_mode__不需要在组件中声明数据,编译时会基于当前编译mode进行替换-->
+<view wx:if="{{__mpx_mode__ === 'ali'}}">支付宝环境</view>
+<view wx:else>其他环境</view>
+

JSON中的条件编译(注意,这个依赖JSON的动态方案,得通过name="json"这种方式来编写,其实写的是js代码,最终module.exports导出一个可json化的对象即可):

<script name="json">
+const pages = __mpx_mode__ === 'wx' ? [
+  'main/xxx',
+  'sub/xxx'
+] : [
+  'test/xxx'
+] // 可以为不同环境动态书写配置
+module.exports = {
+  usingComponents: {
+    aComponents: '../xxxxx' // 可以打注释 xxx组件
+  }
+}
+</script>
+

样式的条件编译:

/*
+  @mpx-if (__mpx_env__ === 'someEvn')
+*/
+  /* @mpx-if (__mpx_mode__ === 'wx') */
+  .backColor {
+    background: green;
+  }
+  /*
+    @mpx-elif (__mpx_mode__ === 'qq')
+  */
+  .backColor {
+    background: black;
+  }
+  /* @mpx-endif */
+
+  /* @mpx-if (__mpx_mode__ === 'swan') */
+  .backColor {
+    background: cyan;
+  }
+  /* @mpx-endif */
+  .textSize {
+    font-size: 18px;
+  }
+/*
+  @mpx-else
+*/
+.backColor {
+  /* @mpx-if (__mpx_mode__ === 'swan') */
+  background: blue;
+  /* @mpx-else */
+  background: red;
+  /* @mpx-endif */
+}
+/*
+  @mpx-endif
+*/
+

# 属性维度条件编译

属性维度条件编译允许用户在组件上使用 @| 符号来指定某个节点或属性只在某些平台下有效。

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

比如业务中需要通过 button 按钮获取用户信息,虽然可以使用代码维度条件编译来解决,但是增加了很多代码量:

<button
+  wx:if="{{__mpx_mode__ === 'wx' || __mpx_mode__ === 'swan'}}"
+  open-type="getUserInfo"
+  bindgetuserinfo="getUserInfo">
+  获取用户信息
+</button>
+
+<button
+  wx:elif="{{__mpx_mode__ === 'ali'}}"
+  open-type="getAuthorize"
+  scope="userInfo"
+  onTap="onTap">
+  获取用户信息
+</button>
+

而用属性维度的编译则方便很多:

<button
+  open-type@wx|swan="getUserInfo"
+  bindgetuserinfo@wx|swan="getUserInfo"
+  open-type@ali="getAuthorize"
+  scope@ali="userInfo"
+  onTap@ali="onTap">
+  获取用户信息
+</button>
+

属性维度的编译也可以对整个节点进行条件编译,例如只想在支付宝小程序中输出某个节点:

<view @ali>this is view</view>
+

需要注意使用上述用法时,节点自身在构建时框架不会对节点属性进行平台语法转换,但对于其子节点,框架并不会继承父级节点 mode,会进行正常跨平台语法转换。

<!--错误示例-->
+<view @ali bindtap="otherClick">
+    <view bindtap="someClick">tap click</view>
+</view>
+// srcMode 为 wx 跨端输出 ali 结果为
+<view @ali bindtap="otherClick">
+    <view onTap="someClick">tap click</view>
+</view>
+

上述示例为错误写法,假如srcMode为微信小程序,用上述写法构建输出支付宝小程序时,父节点 bindtap 不会被转为 onTap,在支付宝平台执行时事件会无响应。

正确写法如下:

<!--正确示例-->
+<view @ali onTap="otherClick">
+    <view bindtap="someClick">tap click</view>
+</view>
+// 输出 ali 产物
+<view @ali onTap="otherClick">
+    <view onTap="someClick">tap click</view>
+</view>
+

有时开发者期望使用 @ali 这种方式仅控制节点的展示,保留节点属性的平台转换能力,为此 Mpx 实现了一个隐式属性条件编译能力

<!--srcMode为 wx,输出 ali 时,bindtap 会被正常转换为 onTap-->
+<view @_ali bindtap="someClick">test</view>
+

在对应的平台前加一个_,例如@_ali、@_swan、@_tt等,使用该隐式规则仅有条件编译能力,节点属性语法转换能力依旧。

有时候我们不仅需要对节点属性进行条件编译,可能还需要对节点标签进行条件编译。

为此,我们支持了一个特殊属性 mpxTagName,如果节点存在这个属性,我们会在最终输出时将节点标签修改为该属性的值,配合属性维度条件编译,即可实现对节点标签进行条件编译,例如在百度环境下希望将某个 view 标签替换为 cover-view,我们可以这样写:

<view mpxTagName@swan="cover-view">will be cover-view in swan</view>
+

# 通过 env 实现自定义目标环境的条件编译

Mpx 支持在以上四种条件编译的基础上,通过自定义 env 的形式实现在不同环境下编译产出不同的代码。

实例化 MpxWebpackPlugin 的时候,传入配置 env。

// vue.config.js
+module.exports = defineConfig({
+  pluginOptions: {
+    mpx: {
+      srcMode: 'wx' // srcMode为mpx编译的源码平台,目前仅支持wx
+      plugin: {
+        env: "didi" // env为mpx编译的目标环境,需自定义
+      }
+    }
+  }
+})
+

# 文件维度条件编译

微信转支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,而且这个地图组件在不同的目标环境中也有很大的差异,这时你可以在相同的位置新建一个 map.ali.didi.mpx 或 map.ali.qingju.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的 mode 和 env 来加载对应模块,当 mode 为 ali,env 为 didi 时,会优先加载 map.ali.didi.mpx、map.ali.mpx,如果没有定义 env,则会优先加载 map.ali.mpx,反之则会加载 map.mpx。

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加modeenv属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali且env为didi时使用如下区块,优先级最高是4-->
+<template mode="ali" env="didi">
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+
+<!--编译mode为ali时使用如下区块,优先级是3-->
+<template mode="ali">
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+
+<!--编译env为didi时使用如下区块,优先级是2-->
+<template env="didi">
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+
+<!--其他环境,优先级是1-->
+<template>
+  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
+</template>
+

注意,如果多个相同的区块写相同的 mode 和 env,默认会用最后一个,如:

<template mode="ali">
+  <view>该区块会被忽略</view>
+</template>
+
+<template mode="ali">
+  <view>默认会用这个区块</view>
+</template>
+

# 代码维度条件编译

如果在 MpxWebpackPlugin 插件初始化时自定义了 env,你可以访问__mpx_env__获取当前编译env,进行环境差异逻辑编写。使用方法与__mpx_mode__相同。

# 属性维度条件编译

env 属性维度条件编译与 mode 的用法大致相同,使用 : 符号与 mode 和其他 env 进行串联,与 mode 组合使用格式形如 attr@mode:env:env|mode:env,为了不与 mode 混淆,当条件编译中仅存在 env 条件时,也需要添加 : 前缀,形如 attr@:env

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

如果当前编译的目标平台是 wx,以下写法 open-type 属性将被忽略

<button open-type@swan:didi="getUserInfo">获取用户信息</button>
+

如果当前 env 不是 didi,以下写法 open-type 属性也会被忽略

<button open-type@:didi="getUserInfo">获取用户信息</button>
+

如果只想在 mode 为 wx 且 env 为 didi 或 qingju 的环境下使用 open-type 属性,则可以这样写:

<button open-type@wx:didi:qingju="getUserInfo">获取用户信息</button>
+

env 属性维度的编译同样支持对整个节点或者节点标签名进行条件编译:

<view @:didi>this is a  view component</view>
+<view mpxTagName@:didi="cover-view">this is a  view component</view>
+

如果只声明了 env,没有声明 mode,跨平台输出时框架对于节点属性默认会进行转换:

<!--srcMode为wx,跨平台输出ali时,bindtap会被转为onTap-->
+<view @:didi bindtap="someClick">this is a  view component</view>
+<view bindtap@:didi ="someClick">this is a  view component</view>
+

# 环境API跨端抹平

# Webview环境跨端抹平

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html b/docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html new file mode 100644 index 0000000000..2e04c6e315 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html @@ -0,0 +1,43 @@ + + + + + + Mpx框架 + + + + + + + + + + + + + diff --git a/docs-vuepress/.vuepress/dist/guide/platform/rn.html b/docs-vuepress/.vuepress/dist/guide/platform/rn.html new file mode 100644 index 0000000000..d804f30d1b --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/platform/rn.html @@ -0,0 +1,198 @@ + + + + + + 跨端输出RN | Mpx框架 + + + + + + + + + +

# 跨端输出RN

大致介绍

# 跨端样式定义

# CSS选择器

# 样式单位

# 文本样式继承

# 简写样式属性

# CSS函数

# 使用原子类

# 混合编写RN代码

# 使用RN组件

# 使用React hooks

# 能力支持范围

# 模版语法

# 事件处理

目前 Mpx 输出 React Native 的事件编写遵循小程序的事件编写规范,支持事件的冒泡及捕获

普通事件绑定

<view bindtap="handleTap">
+    Click here!
+</view>
+

绑定并阻止事件冒泡

<view catchtap="handleTap">
+    Click here!
+</view>
+

事件捕获

<view capture-bind:touchstart="handleTap1">
+  outer view
+  <view capture-bind:touchstart="handleTap2">
+    inner view
+  </view>
+</view>
+

中断捕获阶段和取消冒泡阶段

<view capture-catch:touchstart="handleTap1">
+  outer view
+</view>
+
+

在此基础上也新增了事件处理内联传参的增强机制。

<template>
+ <!--Mpx增强语法,模板内联传参,方便简洁-->
+ <view bindtap="handleTapInline('b')">b</view>
+ </template>
+ <script setup>
+  // 直接通过参数获取数据,直观方便
+  const handleTapInline = (name) => {
+    console.log('name:', name)
+  }
+  // ...
+</script>
+

除此之外,Mpx 也支持了动态事件绑定

<template>
+ <!--动态事件绑定-->
+ <view wx:for="{{items}}" bindtap="handleTap_{{index}}">
+  {{item}}
+</view>
+ </template>
+ <script setup>
+  import { ref } from '@mpxjs/core'
+
+  const data = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4'])
+  const handleTap_0 = (event) => {
+    console.log('Tapped on item 1');
+  },
+
+  const handleTap_1 = (event) => {
+    console.log('Tapped on item 2');
+  },
+
+  const handleTap_2 = (event) => {
+    console.log('Tapped on item 3');
+  },
+
+  const handleTap_3 = (event) => {
+    console.log('Tapped on item 4');
+  }
+</script>
+

注意事项

  1. 当同一个元素上同时绑定了 catchtap 和 bindtap 事件时,两个事件都会被触发执行。但是是否阻止事件冒泡的行为,会以模板上第一个绑定的事件标识符为准。 +如果第一个绑定的是 catchtap,那么不管后面绑定的是什么,都会阻止事件冒泡。如果第一个绑定的是 bindtap,则不会阻止事件冒泡。
  2. 当同一个元素上绑定了 capture-bind:tap 和 bindtap 事件时,事件的执行时机会根据模板上第一个绑定事件的标识符来决定。如果第一个绑定的是 capture-bind:tap,则事件会在捕获阶段触发,如果第一个绑定的是 bindtap,则事件会在冒泡阶段触发。
  3. 当使用了事件委托想获取 e.target.dataset 时,只有点击到文本节点才能获取到,点击其他区域无效。建议直接将事件绑定到事件触发的元素上,使用 e.currentTarget 来获取 dataset 等数据。
  4. 如果元素上设置了 opacity: 0 的样式,会导致 ios 事件无法响应。

# 基础组件

目前 Mpx 输出 React Native 仅支持以下组件,文档中未提及的组件以及组件属性即为不支持,具体使用范围可参考如下文档

RN环境基础组件通用属性

属性名 类型 默认值 说明
enable-offset Boolean false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息
enable-var Boolean true 默认支持使用 css variable,若想关闭该功能可设置为 false
parent-font-size Number 父组件字体大小,主要用于百分比计算的场景,如 font-size: 100%
parent-width Number 父组件宽度,主要用于百分比计算的场景,如 width: calc(100% - 20px),需要在外部传递父组件的宽度
parent-height Number 父组件高度,主要用于百分比计算的场景,如 height: calc(100% - 20px),需要在外部传递父组件的高度

# view

视图容器。

属性

属性名 类型 默认值 说明
hover-class string 指定按下去的样式类。
hover-start-time number 50 按住后多久出现点击态,单位毫秒
hover-stay-time number 400 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

# scroll-view

可滚动视图区域。

属性

属性名 类型 默认值 说明
scroll-x Boolean false 允许横向滚动动
scroll-y Boolean false 允许纵向滚动
upper-threshold Number 50 距顶部/左边多远时(单位 px),触发 scrolltoupper 事件
lower-threshold Number 50 距底部/右边多远时(单位 px),触发 scrolltolower 事件
scroll-top Number 0 设置纵向滚动条位置
scroll-left Number 0 设置横向滚动条位置
scroll-with-animation Boolean false 在设置滚动条位置时使用动画过渡
enable-back-to-top Boolean false 点击状态栏的时候视图会滚动到顶部
enhanced Boolean false scroll-view 组件功能增强
refresher-enabled Boolean false 开启自定义下拉刷新
scroll-anchoring Boolean false 开启滚动区域滚动锚点
scroll-into-view Boolean false 值应为某子元素id(id不能以数字开头)
refresher-default-style String 'black' 设置下拉刷新默认样式,支持 blackwhitenone,仅安卓支持
refresher-background String '#fff' 设置自定义下拉刷新背景颜色,仅安卓支持
refresher-triggered Boolean false 设置当前下拉刷新状态,true 表示已触发
paging-enabled Number false 分页滑动效果 (同时开启 enhanced 属性后生效),当值为 true 时,滚动条会停在滚动视图的尺寸的整数倍位置
show-scrollbar Number true 滚动条显隐控制 (同时开启 enhanced 属性后生效)
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息
enable-trigger-intersection-observer Boolean [] 是否开启intersection-observer
simultaneous-handlers Array [] 主要用于组件嵌套场景,允许多个手势同时识别和处理并触发,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 scroll-view 组件
wait-for Array [] 主要用于组件嵌套场景,允许延迟激活处理某些手势,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 scroll-view 组件

事件

事件名 说明
binddragstart 滑动开始事件,同时开启 enhanced 属性后生效
binddragging 滑动事件,同时开启 enhanced 属性后生效
binddragend 滑动结束事件,同时开启 enhanced 属性后生效
bindscrolltoupper 滚动到顶部/左边触发
bindscrolltolower 滚动到底部/右边触发
bindscroll 滚动时触发
bindrefresherrefresh 自定义下拉刷新被触发

注意事项

  1. 目前不支持自定义下拉刷新节点,使用 slot="refresher" 声明无效,在 React Native 环境中还是会被当作普通节点渲染出来
  2. 若使用 scroll-into-view 属性,需要 id 对应的组件节点添加 wx:ref 标记,否则无法正常滚动
  3. simultaneous-handlers 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture]
  4. wait-for 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail]

# swiper

滑块视图容器。

属性

属性名 类型 默认值 说明
indicator-dots Boolean false 是否显示面板指示点
indicator-color color rgba(0, 0, 0, .3) 指示点颜色
indicator-active-color color #000000 当前选中的指示点颜色
autoplay Boolean false 是否自动切换
current Number 0 当前所在滑块的 index
interval Number 5000 自动切换时间间隔
duration Number 500 滑动动画时长
circular Boolean false 是否采用衔接滑动
vertical Boolean false 滑动方向是否为纵向
previous-margin String 0 前边距,可用于露出前一项的一小部分,接受px
next-margin String 0 后边距,可用于露出后一项的一小部分,接受px
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息
easing-function String linear 支持 linear、easeInCubic、easeOutCubic、easeInOutCubic
bindchange eventhandle current 改变时会触发 change 事件,event.detail = {current, source}

事件

事件名 说明
bindchange current 改变时会触发 change 事件,event.detail = {current, source}

# swiper-item

  1. 仅可放置在swiper组件中,宽高自动设置为100%。

属性

属性名 类型 默认值 说明
item-id string 该 swiper-item 的标识符

# movable-area

movable-view的可移动区域。

注意事项

  1. movable-area不支持设置 scale-area,缩放手势生效区域仅在 movable-view 内

# movable-view

可移动的视图容器,在页面中可以拖拽滑动。movable-view 必须在 movable-area 组件中,并且必须是直接子节点,否则不能移动。

属性

属性名 类型 默认值 说明
direction String none 目前支持 all、vertical、horizontal、none|
inertia boolean false movable-view是否带有惯性|
out-of-bounds boolean false 超过可移动区域后,movable-view是否还可以移动|
x Number 定义x轴方向的偏移
y Number 定义y轴方向的偏移
friction Number 7 摩擦系数
disabled boolean false 是否禁用
animation boolean true 是否使用动画
simultaneous-handlers Array [] 主要用于组件嵌套场景,允许多个手势同时识别和处理并触发,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件
wait-for Array [] 主要用于组件嵌套场景,允许延迟激活处理某些手势,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件

事件

事件名 说明
bindchange 拖动过程中触发的事件,event.detail = {x, y, source}
bindscale 缩放过程中触发的事件,event.detail = {x, y, scale}
htouchmove 初次手指触摸后移动为横向的移动时触发
vtouchmove 初次手指触摸后移动为纵向的移动时触发

注意事项

  1. simultaneous-handlers 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture]
  2. wait-for 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail]

# root-portal

使整个子树从页面中脱离出来,类似于在 CSS 中使用 fixed position 的效果。主要用于制作弹窗、弹出层等。 +属性

属性名 类型 默认值 说明

| enable | boolean | true | 是否从页面中脱离出来 |

注意事项

  1. style 样式不支持中使用百分比计算、css variable

# cover-view

视图容器。 +功能同 view 组件

# cover-image

视图容器。 +功能同 image 组件

# icon

图标组件

属性

属性名 类型 默认值 说明
type String icon 的类型,有效值:success、success_no_circle、info、warn、waiting、cancel、download、search、clear
size String | Number 23 icon 的大小
color String icon 的颜色,同 css 的 color

# text

文本。

属性

属性名 类型 默认值 说明
user-select boolean false 文本是否可选。
disable-default-style boolean false 会内置默认样式,比如fontSize为16。设置true可以禁止默认的内置样式。
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

注意事项

  1. 未包裹 text 标签的文本,会自动包裹 text 标签。
  2. text 组件开启 enable-offset 后,offsetLeft、offsetWidth 获取时机仅为组件首次渲染阶段

# button

按钮。

属性

属性名 类型 默认值 说明
size String default 按钮的大小
type String default 按钮的样式类型
plain Boolean false 按钮是否镂空,背景色透明
disabled Boolean false 是否禁用
loading Boolean false 名称前是否带 loading 图标
open-type String 微信开放能力,当前仅支持 share
hover-class String 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果
hover-start-time Number 20 按住后多久出现点击态,单位毫秒
hover-stay-time Number 70 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

# label

用来改进表单组件的可用性

注意事项

  1. 当前不支持使用 for 属性找到对应 id,仅支持将控件放在该标签内,目前可以绑定的空间有:checkbox、radio、switch。

# checkbox

多选项目

属性

属性名 类型 默认值 说明
value String checkbox 标识,选中时触发 checkbox-group 的 change 事件,并携带 checkbox 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox的颜色,同css的color

# checkbox-group

多项选择器,内部由多个checkbox组成。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# radio

单选项目

属性

属性名 类型 默认值 说明
value String radio 标识,当该 radio 选中时,radio-group 的 change 事件会携带 radio 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox 的颜色,同 css 的 color

# radio-group

单项选择器,内部由多个 radio 组成

事件

事件名 说明
bindchange radio-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 radio 的 value 的数组 ] }

# form

表单。将组件内的用户输入的switch input checkbox slider radio picker 提交。

当点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key。

事件

事件名 说明
bindsubmit 携带 form 中的数据触发 submit 事件,event.detail = {value : {'name': 'value'} }
bindreset 表单重置时会触发 reset 事件

# input

输入框。

属性

属性名 类型 默认值 说明
value String 输入框的初始内容
type String text input 的类型,不支持 safe-passwordnickname
password Boolean false 是否是密码类型
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
confirm-type String done 设置键盘右下角按钮的文字,仅在 type='text' 时生效
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bind:selectionchange 选区改变事件, event.detail = { selectionStart, selectionEnd }

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# textarea

多行输入框。

属性

属性名 类型 默认值 说明
value String 输入框内容
type String text input 的类型,不支持 safe-passwordnickname
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
auto-height Boolean false 是否自动增高,设置 auto-height 时,style.height不生效
confirm-type String done 设置键盘右下角按钮的文字,不支持 return
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bindlinechange 输入框行数变化时调用,event.detail = { height: 0, lineCount: 0 },不支持 heightRpx
bind:selectionchange 选区改变事件, {selectionStart, selectionEnd}

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# picker-view

嵌入页面的滚动选择器。其中只可放置 picker-view-column组件,其它节点不会显示

属性

属性名 类型 默认值 说明
value Array[number] false 数组中的数字依次表示 picker-view 内的 picker-view-column 选择的第几项(下标从 0 开始),数字大于 picker-view-column 可选项长度时,选择最后一项。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# picker-view-column

滚动选择器子项。仅可放置于picker-view中,其孩子节点的高度会自动设置成与picker-view的选中框的高度一致

# picker

从底部弹起的滚动选择器。

属性

属性名 类型 默认值 说明
mode string selector 选择器类型
disabled boolean false 是否禁用

公共事件

事件名 说明
bindcancel 取消选择时触发
bindchange 滚动选择时触发change事件,event.detail = {value};value为数组,表示 picker-view 内的 picker-view-column 当前选择的是第几项(下标从 0 开始)
# 普通选择器:mode = selector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value number 0 表示选择了 range 中的第几个(下标从 0 开始)
# 多列选择器:mode = multiSelector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value array [] 表示选择了 range 中的第几个(下标从 0 开始)
bindcolumnchange 列改变时触发
# 多列选择器:时间选择器:mode = time

属性

属性名 类型 默认值 说明
value string [] 表示选中的时间,格式为"hh:mm"
start string false 表示有效时间范围的开始,字符串格式为"hh:mm"
end string [] 表示有效时间范围的结束,字符串格式为"hh:mm"
# 多列选择器:时间选择器:mode = date

属性

属性名 类型 默认值 说明
value string 当天 表示选中的日期,格式为"YYYY-MM-DD"
start string false 表示有效日期范围的开始,字符串格式为"YYYY-MM-DD"
end string [] 表示有效日期范围的结束,字符串格式为"YYYY-MM-DD"
fields string day 有效值 year,month,day,表示选择器的粒度
# fields 有效值:
属性名 说明
year 选择器粒度为年
month 选择器粒度为月份
day 选择器粒度为天
# 省市区选择器:mode = region

属性

属性名 类型 默认值 说明
value array [] 表示选中的省市区,默认选中每一列的第一个值
custom-item string 可为每一列的顶部添加一个自定义的项
level string region 选择器层级
# level 有效值:
属性名 说明
province 选省级选择器
city 市级选择器
region 区级选择器

# image

图片。

属性

属性名 类型 默认值 说明
src String false 图片资源地址,支持本地图片资源及 base64 格式数据,暂不支持 svg 格式
mode String scaleToFill 图片裁剪、缩放的模式,适配微信 image 所有 mode 格式
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
binderror 当错误发生时触发,event.detail = { errMsg }
bindload 当图片载入完毕时触发,event.detail = { height, width }

注意事项

  1. image 组件默认宽度320px、高度240px
  2. image 组件进行缩放时,计算出来的宽高可能带有小数,在不同webview内核下渲染可能会被抹去小数部分

# canvas

画布。

事件

属性名 类型 说明
bindtouchstart eventhandle 手指触摸动作开始
bindtouchmove eventhandle 手指触摸后移动
bindtouchend eventhandle 手指触摸动作结束
bindtouchcancel eventhandle 手指触摸动作被打断
bindlongtap eventhandle 手指长按 300ms 之后触发
binderror eventhandle 当发生错误时触发 error 事件, detail = {errMsg}

API

方法名 说明
createImage 创建一个图片对象。 仅支持在 2D Canvas 中使用
createImageData 创建一个 ImageData 对象。仅支持在 2D Canvas 中使用
getContext 该方法返回 Canvas 的绘图上下文。仅支持在 2D Canvas 中使用
toDataURL 返回一个包含图片展示的 data URI

注意事项

  1. canvas 组件目前仅支持 2D 类型,不支持 webgl
  2. 通过 Canvas.getContext('2d') 接口可以获取 CanvasRenderingContext2D 对象,具体接口可以参考 (HTML Canvas 2D Context)[https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D] 定义的属性、方法
  3. canvas 的实现主要借助于 PostMessage 方式与 webview 容器通信进行绘制,所以对于严格依赖方法执行时机的场景,如调用 drawImage 绘图,再通过 getImageData 获取图片数据的场景,调用时需要使用 await 等方式来保证方法的执行时机
  4. 通过 Canvas.createImage 画图,图片的链接不能有特殊字符,安卓手机可能会 load 失败
# web-view

承载网页的容器。会自动铺满整个RN页面

属性

属性名 类型 默认值 说明
src String webview 指向网页的链接,如果需要对跳转的URL设定白名单可跳转,需要在业务跳转之前出来该逻辑
bindmessage EventHandler 网页向RN通过 postMessage 传递数据
bindload EventHandler 网页加载成功时候触发此事件
binderror EventHandler 网页加载失败的时候触发此事件

注意事项

  1. web-view网页中可使用@mpxjs/webview-bridge@2.9.68提供的接口返回RN页面或与RN页面通信,具体使用细节可以参见Webview API

# 自定义组件

# 样式规则

# 应用能力

# 环境API

在RN环境中也提供了一部分常用api能力,方法名与使用方式与小程序相同,可能对于某个api提供的能力会比微信小程序提供的能力少一些,以下是使用说明:

# 使用说明

如果全量引入api-proxy这种情况下,需要如下配置

// 全量引入api-proxy
+import mpx from '@mpxjs/core'
+import apiProxy from '@didi/mpxjs-api-proxy'
+mpx.use(apiProxy, { usePromise: true })
+

需要在mpx项目中需要配置externals

externals: {
+  ...
+  '@react-native-async-storage/async-storage': '@react-native-async-storage/async-storage',
+  '@react-native-clipboard/clipboard': '@react-native-clipboard/clipboard',
+  '@react-native-community/netinfo': '@react-native-community/netinfo',
+  'react-native-device-info': 'react-native-device-info',
+  'react-native-safe-area-context': 'react-native-safe-area-context',
+  'react-native-reanimated': 'react-native-reanimated',
+  'react-native-get-location': 'react-native-get-location',
+  'react-native-haptic-feedback': 'react-native-haptic-feedback'
+},
+

如果引用单独的api-proxy方法这种情况,需要根据下表说明是否用到一下方法,来确定是否需要配置externals,配置参考上面示例

api方法 依赖的react-native三方库
showActionSheet react-native-reanimated
getNetworkType、
offNetworkStatusChange、
onNetworkStatusChange
@react-native-community/netinfo
getLocation、
openLocation、
chooseLocation
react-native-get-location
setStorage、
setStorageSync、
getStorage、
getStorageSync、
getStorageInfo、
getStorageInfoSync、
removeStorage,
removeStorageSync,
clearStorage,
clearStorageSync
@react-native-async-storage/async-storage
getSystemInfo、
getSystemInfoSync、
getDeviceInfo、
getWindowInfo、
getLaunchOptionsSync、
getEnterOptionsSync
react-native-device-info
getWindowInfo、
getLaunchOptionsSync、
getEnterOptionsSync
react-native-safe-area-context
vibrateShort、
vibrateLong
react-native-haptic-feedback

在RN 项目中,如果是以全量引入api-proxy的方法需要在RN环境中执行以下所有的命令,如果只是使用单个api的能力,可以参考上表来判断安装对应的包

// 安装api-proxy下所用到的依赖 如果
+npm i @react-native-async-storage/async-storage
+npm i @react-native-clipboard/clipboard
+npm i @react-native-community/netinfo
+npm i react-native-safe-area-context
+npm i react-native-device-info
+npm i react-native-reanimated
+npm i react-native-haptic-feedback
+ios项目需要执行如下命令
+cd ios
+pod install
+
+npm i react-native-get-location
+

android下需要做如下配置: +安装react-native-get-location包后,需要在AndroidManifest.xml中定义位置权限,参考文档 (opens new window)

<!-- Define ACCESS_FINE_LOCATION if you will use enableHighAccuracy=true  -->
+<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+
+<!-- Define ACCESS_COARSE_LOCATION if you will use enableHighAccuracy=false  -->
+<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+
+

安装react-native-haptic-feedback包后需要在安卓中配置,配置参考文档 (opens new window) +打开 android/app/src/main/java/[...]/MainApplication.java. 在文件的顶部添加以下导入下面的代码片段

import com.mkuczera.RNReactNativeHapticFeedbackPackage;
+

修改设置,将下面的配置添加到android/settings.gradle文件中

include ':react-native-haptic-feedback'
+project(':react-native-haptic-feedback').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-haptic-feedback/android')
+

react-native-reanimated在mpx和RN项目都要安装,安装好包后需要在babel.config.json文件中做如下配置,并且RN环境中使用的react-native-reanimated与mpx项目中安装的react-native-reanimated版本要一致: +配置参考文档 (opens new window)

module.exports = {
+    presets: [
+      ... // don't add it here :)
+    ],
+    plugins: [
+      ...
+      'react-native-reanimated/plugin',
+    ],
+};
+

# Webview API

对于web-view组件打开的网页,想要跟RN通信,或者跳转到RN页面,提供了以下能力

方法名 说明
navigateTo 保留当前webview页面,跳转RN页面
navigateBack 关闭当前页面,返回上一页或多级RN页面
switchTab 跳转到RN的 tabBar 页面
reLaunch 关闭所有页面,打开到应用内的某个RN页面
redirectTo 关闭当前页面,跳转到应用内的某个RN页面
getEnv 获取当前环境
postMessage 向RN发送消息,实时触发组件的message事件
invoke 开放一个webview页面和web页面互通消息的能力
# webview-bridge示例代码
import webviewBridge from '@mpxjs/webview-bridge'
+webviewBridge.navigateTo({
+  url: 'RN地址',
+  success: () => {
+    console.log('跳转成功')
+  }
+})
+
# invoke示例代码

RN环境中挂载getTime的逻辑

import mpx from '@mpxjs/core'
+...
+// 普通方法
+mpx.config.webviewConfig = {
+  apiImplementations: {
+    getTime:  (options = {}) => {
+      const { params = {} } = options
+      return {
+        text: params.text,
+        time: '2024-12-24'
+      }
+    }
+  }
+}
+// 或者promise
+mpx.config.webviewConfig = {
+  apiImplementations: {
+    getTime:  (options = {}) => {
+      return new Promise((resolve, reject) => {
+        const { params = {} } = options
+        if (params.text) {
+          resolve({
+            text: params.text,
+            time: '2024-12-24'
+          })
+        } else {
+          reject(new Error('没有传text参数'))
+        }
+      })
+    }
+  }
+}
+...
+

web中通信的逻辑

import webviewBridge from '@mpxjs/webview-bridge'
+webviewBridge.invoke('getTime', {
+  params: {
+    text: '我是入参'
+  },
+  success: (res) => {
+    console.log('接收到的消息:', res.time)
+  }
+})
+

# 其他使用限制

如事件的target等

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/platform/web.html b/docs-vuepress/.vuepress/dist/guide/platform/web.html new file mode 100644 index 0000000000..d6ebdac7d9 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/platform/web.html @@ -0,0 +1,43 @@ + + + + + + Mpx框架 + + + + + + + + + + + + + diff --git a/docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html b/docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html new file mode 100644 index 0000000000..fce599e788 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html @@ -0,0 +1,297 @@ + + + + + + E2E自动化测试 | Mpx框架 + + + + + + + + + +

# E2E自动化测试

微信小程序的官方文档推荐 miniprogram-automator (opens new window),其与小程序IDE的关系,正如 Google 与 UiAutomator、selenium 与 webdriver 一样;它是最契合小程序的。

虽然微信小程序提供了 automator + ide 的 E2E 的解决方案,但该项目维护频率低且 case 编写效率低、API 不够友好等问题,因此基于微信提供的能力& Mpx 生态我们产出了相对完善的小程序E2E自动化测试增强方案,此外还提供了可扩展的开箱即用的E2E基础设施。

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。

如果你之前使用过 Selenium WebDriver 或者 Puppeteer,那你可以很容易快速上手。小程序自动化 SDK 与它们的工作原理是类似的,主要区别在于控制对象由浏览器换成了小程序。

# @mpx/cli 脚手架集成E2E

当使用 @mpxjs/cli 初始化 Mpx 项目的时候,交互式命令行里面新增了 E2E 选项,当选择了此选项,项目将会初始化 E2E 配置,完成相关内容的生成。

关于 E2E 的模板文件如下:

# 忽略部分文件夹
+vue-project
+├─ .e2erc.js # E2E配置
+├─ src
+│  ├─ app.mpx
+│  ├─ pages
+│  │  └─ index.png
+│  └─ components
+│     └─ list.mpx
+├─ test
+│  └─ e2e # case目录
+│     └─ list.spec.js # 示例文件
+├─ jest-e2e.config.js # Jest配置
+└─ package.json
+

这里罗列了 E2E 项目中约定(或推荐)的目录结构,在项目开发中,请遵照这个目录结构组织代码。

# 文件说明

package.json

使用自动化测试,我们要做的第一件事就是安装 @mpxjs/e2e 和 @mpxjs/e2e-scripts,然后我们需要在 package.json 中定义两个自动化测试的脚本 test 和 testServer。

{
+  "devDependencies": {
+    "@mpxjs/e2e": "0.0.13",
+    "@mpxjs/e2e-scripts": "0.0.12",
+  }
+  "scripts": {
+    "test": "e2e-runner",
+    "testServer": "e2e-runner --preview"
+  }
+}
+

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。

.e2erc.js

E2E 配置文件,包含 E2E 内置功能和插件的配置。可以在这里扩展运行时的能力,比如修改运行时是否自动保存页面快照。

const path = require('path');
+const PluginReport = require('@mpxjs/e2e/report-server/server.js');
+module.exports = {
+  recordsDir: 'dist/wx/minitest', // 录制 json 文件的存储目录
+  connectFirst: false, // 优先使用 automator.connect,默认 automator.launch 优先
+  defaultWaitFor: 5000, // 默认 waitFor 时长
+  devServer: { // 测试报告服务器配置
+    open: true,
+    port: 8886
+  },
+  jestTimeout: 990000, // jestTimeout
+  jsonCaseCpDir: 'test/e2e/e2e-json', // 从 minitest 目录复制 json 文件到该目录下
+  needRealMachine: false, // 是否需要真机
+  plugins: [new PluginReport()], // 自定义测试报告的插件
+  projectPath: path.resolve(__dirname, './dist/wx'),
+  sequence: [ // e2e 的 case 的执行顺序
+    // 'minitest-1'
+  ],
+  testSuitsDir: 'test/e2e/', // e2e 的 case 存放目录
+  useTS: false, // e2e 的 case 是否为 TS 语法
+  wsEndpoint: 'ws://localhost:9420', // automator.connect 的 wsEndpoint
+  timeoutSave: 3000, // 定时截图默认开启,设置为 false 即可关闭
+  cacheDirectory: path.resolve(__dirname, './node_modules/@mpxjs/e2e/report-server/cache'), // 配置截图数据的保存目录
+  tapSave: true, // 点击截图默认开启,设置为 false 即可关闭
+  routeUpdateSave: true, // 路由切换截图默认开启,设置为 false 即可关闭
+  routeTime: 300, // 路由切换 300ms 后再截图
+  watchResponse: [ { url: '/api/list', handler (newRes, oldRes) { return true }} ], // 配置接口请求截图
+}
+

我们提供了丰富的配置化选项,满足各种场景运行。

参数 类型 默认值 说明
projectPath String ./dist/wx 小程序代码路径,Mpx 框架的 wx 输出目录
testSuitsDir String test/e2e/suits/ e2e 的 case 存放目录
sequence string[ ] [ ] 用例运行的顺序
recordsDir String dist/wx/minitest 录制 json 文件的存储目录
connectFirst Boolean false 优先使用 automator 的 connect 方法
wsEndpoint String ws://localhost:9420 使用 connect 方式时的 wsEndpoint 选项
defaultWaitFor Number 15000 默认 waitFor 时长
useTS Boolean false 用例是否为 TS 语法
jestTimeout Number 990000 默认测试超时时间
jsonCaseCpDir String test/e2e-json 从 minitest 目录复制 json 文件到该目录下
needRealMachine Boolean false 是否需要真机回放
devServer Object null 测试报告服务器配置
plugins Array [ ] 自定义测试报告的插件
timeoutSave Number 3000 定时3000ms保存页面快照
cacheDirectory String report-server/cache 页面快照的保存目录
tapSave Boolean true 点击时候保存页面快照
routeUpdateSave Boolean true 路由切换时候保存页面快照
routeTime Number 300 路由切换300ms后保存页面快照
watchResponse Object null 监听接口请求保存页面快照

e2e

e2e 目录,所有的 case 文件存放在此目录下,默认会创建一个演示 demo 文件,也就是 list.spec.js 文件,约定 e2e 下所有的 .spec.js 结尾的作为自动化测试的文件,使用 Typescript 编写测试文件的时候, 需要将文件名改成 .spec.ts 格式,然后 tsconfig.json 加上 "esModuleInterop": true。

/**
+ * @file e2e test example
+ * 首先开启工具安全设置中的 CLI/HTTP 调用功能
+ * docs of miniprogram-automator: https://developers.weixin.qq.com/miniprogram/dev/devtools/auto/quick-start.html
+ */
+import automator from '@mpxjs/e2e'
+
+describe('index', () => {
+  let miniProgram
+  let page
+
+  beforeAll(async () => {
+    try {
+      miniProgram = await automator.connect({ wsEndpoint: 'ws://localhost:9420' })
+    } catch (e) {
+      miniProgram = await automator.launch({ projectPath: './dist/wx' })
+    }
+    page = await miniProgram.reLaunch('/pages/index')
+    await page.waitFor(500)
+  }, 30000)
+
+  it('desc', async () => {
+    const desc = await page.$('list', 'components/list2271575d/index')
+    // 断言页面标签
+    expect(desc.tagName).toBe('view')
+    // 断言文字内容
+    expect(await desc.text()).toContain('手机')
+    // 保存页面快照
+    await miniProgram.screenshot({
+      path: 'test/e2e/screenshot/homePage.png'
+    })
+  })
+
+  afterAll(async () => {
+    await miniProgram.close()
+  })
+})
+

如果你已经熟悉了 Jest,你应该很适应 Jest 的断言 API。

jest-e2e.config.js

Jest 配置文件,这些选项可让你控制 Jest 的行为,你可以了解 Jest 的默认选项,以便在必要时扩展它们:

module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'node',
+  testTimeout: 1000000000,
+  maxWorkers: 1,
+  reporters: [
+    'default',
+    ['<rootDir>/node_modules/@mpxjs/e2e/report-server/report.js', {}], // 自定义测试报告
+  ]
+}
+

除了 Jest 提供的默认测试报告器外,我们还可以自定义测试报告。框架 @mpxjs/e2e 内部提供了可视化测试报告平台,需要配置 reporters 字段。

目前对于@mpx/cli@3.*版本也会陆续完成E2E的相关支持。

# @mpxjs/e2e-scripts

默认情况下,Jest 将会递归的找到整个工程里所有 .spec.js 或 .test.js 扩展名的文件。 Jest 支持并行运行 test ,特别是在 ci 场景,将会极大减少 test 消耗时间。配置 --maxWorkers 参数表示的是 Jest 会开启多少个线程去完成所有的测试任务,默认值是 50% * os.cpus().length,相关的文档可见:链接 (opens new window)

合理的设置 maxWorkers 会使得运行变快,依赖的是并行跑用例,但是在自动化测试环境下,用例通过脚本操控模拟器或真机环境,多个用例不能同时操控一个环境,否则会相互干扰,就好比 JS 只能是单线程执行一样。

因为用例只能一个一个的执行,一个完整的项目自然会包含很多用例,但是一次只能执行一个用例的话,我们需要人工的操作很多次,才能全部执行完。为了跑一个遍就把所有的用例都执行完的话,我们会想到写一个脚本通过遍历的方式依次执行,这就是 @mpxjs/e2e-scripts 设计的初衷。

使用 @mpxjs/e2e-scripts 内部提供的命令脚本,执行 npx e2e-runner 将会按照 sequence 的定义的顺序依次执行用例文件。

module.exports = {
+  sequence: [ // 测试用例的执行顺序
+    'minitest-1',
+    'minitest-2'
+  ]
+}
+

上面代码表示会先执行 minitest-1.spec.js 文件,然后再执行 minitest-2.spec.js 文件。

# E2E可视化报告平台

E2E内置的 Jest 默认支持输出 HTML 的报告,因其只支持对测试结果数据的简单展示,故我们希望在其基础上,不仅针对报告查看的广度和颗粒度进行细化,还将对自动化测试过程中涉及到的痛点实现功能上的增强。

E2E可视化报告平台是一个运行在本地环境,统合了用例管理、测试报告、页面快照和错误日志的平台。支持对通过 WechatDevTools 录制回放功能录制出的 case 进行自定义增强的能力,同时提供执行 E2E 测试过程中产出的页面快照和错误日志等信息进行快捷、直观地查看的功能。

目前支持多种交互动作保存快照(点击、输入、滑动等),我们还在页面快照方面做了增强,提供了快照标记的功能,可以完善测试报告,增强排查手段,当点击元素后,页面快照上会自动标记出点击的区域或者元素。

# E2E录制+自动化生成case

微信推出了官方的录制回放能力。通过微信开发者工具可以录制用户的操作过程,期间可以进行简单断言,录制结束后支持回放。但是经过实际使用发现录制回放存在下列不足:

  1. 录制结果结果以 json 形式存在于 dist 目录,难以扩展、难维护;
  2. 仅支持 data快照、wxml快照、检查元素、检查路径四种简单断言,难以满足复杂业务场景的细粒度断言诉求;
  3. 因等待时长、接口响应时机、组件更新时机等难以和录制时对应,录制结果回放失败频率高、不稳定;

我们通过基于微信原生的录制进行增强的方案。使用录制的便捷性降低手写流程的成本,再通过 sdk 的能力对录制所得 Case 进行增强。

这样,我们通过分析录制 JSON 文件,把 JSON 中的每一条进行分析转换,最终得到 spec 的 JS 代码文件,通过这种方式,可以大幅度降低获取元素、触发事件等常规 Case 的编写。

JSON to Spec 本质只是录制结果的一种呈现,而这种转换的目的在于通过扩展强化录制 Case,弥补录制的能力有限。

为了方便我们进行扩展,首先需要对录制所得的 JSON 文件进行语义化的分析。这么做的意义在于把录制的操作步骤和 JSON 的数据关联起来,而关联步骤和数据又可以增强可读性为用户在某一个步骤之后进行扩展增加了极大的便利性。

上一个部分已经论证过把录制和SDK增强接合起来的可行性,但是上面一系列的操作都是通过脚本的形式呈现的,这对于我们前面的降低门槛来说仍然是繁琐的。最起码对于不会写代码的的人来说,还是不够理想。接下来就是探索如何更直观、更高效的方式把这种方案落地。

我们设计了 Mpx-E2E 的工作台,当然这些也都集成到了 Mpx-E2E 的可视化平台中,下面我们看看这些具体的可视化的工作。

分析 JSON 操作步骤后,我们把依据 JSON 生成的 Spec 同样做了可视化处理,起初的时候我们只是做了 Spec 代码的 highlight,并没有支持编辑。但是考虑到所见即所得的效率,我们又在此支持了 WEB-IDE。在生成 Spec 代码后,即可在线进行编辑,最终通过我们的@mpxjs/e2e SDK核心能力,完成了保存spec文件。

# 增强API

@mpxjs/e2e

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。

然而在我们使用过程中发现原始小程序官网提供的sdk存在能力的不足和缺失,或者说无法满足我们复杂的业务流程,针对这一系列的能力的缺失,我们开发了针对e2e的增强型api,来满足我们的整个自动化流程所需的功能

1、获取页面中的DOM元素 $ 方法 :

SDK 中重写 page 和 element 的 $ 方法。之所以这么处理是因为原生的 $ 方法存在自定义组件中的元素不能稳定获取的问题,而这个问题的根源在于微信小程序会以某种规则为元素拼接组件或者父组件的名字作为真实类名,而这种规则并不固定。

目前增强后的 $ 方法支持两个参数,第一个为选择器,第二个是自定义组件名。第二个参数不传时其行为和原生 $ 方法一致。

$(className: string, componentsName?: string): Promise<Element | any>
+ 
+const confirmbtn = await page.$('confirm-btn', 'homepage/components/confirmef91faba/confirm')
+ 
+const confirmbtn2 = await page.$('.confirm-btn')
+const view = await page.$('view')
+const id = await page.$('#id')
+

获取自定义组件中的元素需要以渲染结果为准,通常第二个参数是传入组件的 is 属性。注意弹窗类似的组件往往会存在动画所以需要异步获取。

// 页面的渲染结果
+<base-dialog is="homepage/components/dialogd2d0cea6/index">
+  #shadow-root
+    <view class="wrapper">
+      <view class="cancel-btn">确认</view>
+      <view class="confirm-btn">取消</view>
+    </view>
+</base-dialog>
+
+// 对应获取元素的方法
+const cancelbtn = await page.$('cancel-btn', 'homepage/components/dialogd2d0cea6/index')
+const confirmbtn = await page.$('confirm-btn', 'homepage/components/dialogd2d0cea6/index')
+

2、增强 wait/ waitAll

automator 中原生支持 wait 方法,表示等待时长或者等待终止的断言函数。但是经过实际测试发现,使用指定的等待时间并不可靠,其运行受限于硬件条件。

经过增强的 wait 方法可以支持: 路由到指定页面, 指定组件渲染,指定组件更新,指定接口发起, 指定接口响应后;

wait(path: string, type?: string): Promise<string | undefined> | void;
+ 
+const miniProgram = await Automator.launch({
+  projectPath: './dist/wx'
+})
+
+// 页面
+page = await miniProgram.reLaunch('/pages/index/index')
+await miniProgram.wait('pages/index/index')
+
+// 组件
+const suggest1 = await miniProgram.wait('suggest/components/suggestcaafe3e4/suggest', 'component')
+ 
+// 组件更新
+const suggest2 = await miniProgram.wait('suggest/components/suggestcaafe3e4/suggest', 'componentUpdate')
+ 
+// 请求
+const request = await miniProgram.wait('https://xxxx.xxx/xxx', 'request')
+ 
+// 返回结果
+const response = await miniProgram.wait('https://xxxx.xxx/xxx', 'response')
+expect(response.options.data.errno).toBe(0)
+const data = response.options.data.data
+expect(data.status).toBe(1)
+

3、新增 mock 能力

考虑到进行 e2e 测试时有些场景需要 mock 数据,所以 SDK 结合 mpx-fetch 的 setProxy 能力提供了静态资源文件 mock、手动设置接口响应结果的能力。

前置:如果需要使用 mock 需要使用支持 setProxy 的 mpx-fetch 版本;此外还需要在 createApp 的时候传入 setProxy 属性,其配置与 mpx-fetch 的 setProxy 方法的配置相同,示例:

createApp({
+	setProxy: [
+  	{
+    	test: {
+      	host: 'some-domain.com',
+      	protocol: 'https:'
+    	},
+    	proxy: {
+      	host: 'localhost',
+      	port: 8887,
+      	protocol: 'http:'
+    	}
+  	}
+  ],
+	// otherApp props
+})
+

3.1 初始化 mock:initMock

当传入 staticDir 时会优先匹配该目录下的静 json 文件作为指定接口的响应结果。

接口与文件名的映射规则:将域名后的 path 中的分隔符 / 替换为 -,示例:

  • 接口名称:api/pGetIndexInfo
  • json 文件名称:api-pGetIndexInfo.json
interface E2eMockConfig {
+  staticDir: string // 本地文件目录:
+}
+
+Automator.initMock(mockCfg: E2eMockConfig): Promise<MiniProgram>
+

3.2 setMock 方法,除了上面的静态资源文件,mock 内置了一个 Map 列表,因此可以按需的设置某一接口的响应结果。

Automator.setMock (path:string, response:any): () => void
+ 
+// 示例:
+let un = Automator.setMock('https://some-domain.com/api/pGetIndexInfo', {
+  errno: 0,
+  errmsg: 'mock-index-info',
+  data: {
+    a: 1,
+    b: 2,
+    c: 3
+  }
+});
+ 
+// 需要取消时可以调用 un,注意这一步骤非必须!!
+un();
+

3.3 removeMockFromMap 从 mock 内置的 Map 列表中移除指定 path 对应的的 mock 数据。

Automator.removeMockFromMap (path:string): void
+

# SOP

一、环境要求

1.1 微信环境 +安装 Node.js 并且版本大于 8.0 +基础库版本为 2.7.3 及以上 +开发者工具版本为 1.02.1907232 及以上 +  +1.2 Mpx 环境 +Mpx-E2E 很多能力基于 Mpx 增强的,最低版本要求如下: +@mpxjs/core >= 2.7.2 +@mpxjs/api-proxy >= 2.7.1 +@mpxjs/webpack-plugin >= 2.7.8

二、新建项目

2.1 安装 @mpxjs/cli 脚手架

$ npm install -g @mpxjs/cli
+

2.2 初始化项目选择 E2E 测试,执行以下命令执行初始化项目

$ mpx init some-proj
+

执行该命令后稍等片刻,等模板下载完成 Terminal 会输出以下提示,当输出到 ”是否需要 E2E 测试“时输入 “yes“ ,脚手架内置了 E2E 需要的依赖和简单的测试用例。待脚手架初始化完成后,你的新项目就已经拥有了开箱即用的E2E能力。

三、已有项目

3.1 安装 E2E sdk、runner

npm i @mpxjs/e2e  @mpxjs/e2e-scripts --save-dev
+

3.2 创建 .e2erc 配置文件

在项目的源文件目录下创建名为 .e2erc.js 的配置文件,以下为 e2erc 各选项及其含义,建议直接复制到项目中

const path = require('path');
+const PluginReport = require('@mpxjs/e2e/report-server/server.js'); // E2E 报告插件
+module.exports = {
+  recordsDir: 'dist/wx/minitest', // 录制 json 文件的存储目录
+  connectFirst: false, // 优先使用 automator.connect,默认 automator.launch 优先
+  wsEndpoint: 'ws://localhost:9420', // automator.connect 的 wsEndpoint 选项,用于 connectFirst 时链接到模拟器服务
+  defaultWaitFor: 15000, // 默认 waitFor 时长
+  useTS: false, // 是否启用 TS , 该特性暂不支持,
+  devServer: {
+    open: true
+  },
+  jestTimeout: 990000, // jestTimeout
+  jsonCaseCpDir: 'test/e2e-json', // 从 minitest 目录复制 json 文件到该目录下
+  needRealMachine: false, // 是否需要真机
+  plugins: [new PluginReport()], 
+  projectPath: path.resolve(__dirname, './dist/wx'), // 小程序代码路径,Mpx 框架的 wx 输出目录
+  testSuitsDir: 'test/e2e/suits/', // e2e 的 case 存放目录
+  sequence: [ // E2E case,runner 将会按照这个数组顺序执行其中的 case 文件,
+    'minitest-20', // 不需要写 .spec.js 这个扩展名
+  ],
+  reportsDir: 'test/reports' // 报告输出目录
+}
+

四、获取 Case

4.1 录制 JSON Case

录制 case 是依托于微信开发者工具已有的能力,操作流程如下:

  1. 启动录制 +启动微信开发者工具 -> 工具 -> 自动化测试
  2. 创建用例 +选择【开始录制】按钮 +【用例名】会成为录制 JSON case 的文件名,比如上图中录制结束后会得到一个名为 +minitest-3.json 的 JSON 文件;我们推荐您采用更语义化的测试用例名称,便于后续的 CASE 管理。 +【结束录制】后,开发者工具会在小程序的代目录(Mpx 的产物输出目录,如 dist/wx)下新增 minitest/ 目录,该目录是微信开发者工具自动创建的,用于存放录制 JSON case。例如上面的例子中,录制结束后该目录下就应该有 名为 minitest-3.json 的 文件。

4.2 生成 Spec Case

启动 Mpx-E2E 平台服务,首先在项目的 package.json 中加入以下 npm script:

{
+  "e2eServe": "npx e2e-runner --preview"
+},
+

在小程序项目录下通过命令行启动 Mpx-E2E 平台服务:

npm run e2eServe
+

平台服务已经成功启动!打开 Mpx-E2E 的可视化平台: +选择【用例管理】,加载录制的 JSON Case,点击可视化平台上的 【JSON 导入】按钮。界面中间是对当前 JSON 文件的语义化解析,最后侧是包含自动生成代码的 Web IDE。点击中间的语义化解析会联动右侧 WebIDE 代码跳转到对应的区域。

4.3 可视化扩展 Spec Case

  1. 新增 +鼠标悬浮在中间语义化区域中每一条的左侧的绿色按钮时,就会出现一个扩展菜单,在弹出框中填写相关信息,点击【保存】即可新增一条操作

  2. 编辑 +通过可视化的方式扩展的操作可以编辑,录制的操作不能编辑;

4.4 保存 Spec case

在完成上述操作后,点击 Web-IDE 下面的 【保存】即可.

点击保存后, Web-IDE 中的内容将会被写入到 .e2erc.js 的 testSuitsDir 字段指定的目录中。当文件写入成功后,被保存的 spec 文件名会被自动写入到剪贴板中。

另外,生成这份 Case 的 JSON 文件也会被复制到 .e2erc.js 的 jsonCaseCpDir 目录下,之所以保存 JSON 文件主要是做备份,你可以不关心这个 json 的文件内容,但是我们建议你随着 spec 代码已经提交到版本库; +  +经过保存后,我们就得到了 minitest-3.spec.js Case 文件。

五、修改 .e2erc.js sequence

5.1 sequence 数组 +经过前面的获取 Case 后步骤后,我们已经得到了 minitest-3.spec.js Case 文件。在正式运行前,我们需要把这个 minitest-3.spec.js Case 加入到 .e2erc.js 的 sequence 字段数组中。

sequence: [ 
+  'minitest-3', // 不用写 .spec.js 
+]
+

5.2 为什么手动维护? +之所以需要手动维护这个数组,是因为微信的 automator 不能并发运行,进而 E2E 的 Case 无法并发运行;又或者某几个 spec 间是有联系的, 这种情况下只能把顺序交给人工维护。

六、运行 e2e

6.1 新增 npm scripts +修改项目的 package.json 中 scripts 字段,向其中加入 "test:e2e":"npx e2e-runner" 命令

6.2 运行 E2E 测试命令

npm run test:e2e
+

6.3 在真机中运行 E2E

  1. 真机运行 E2E 相当于微信开发者工具的【真机调试】,首先需要确保你的 Mpx 构建输出产物是 prod 模式下的产物;
  2. 修改 .e2erc.js 中的 needRealMachine 字段为 true,再执行 npm run test:e2e 命令; +静等微信开发者工具完成真机调试二维码的构建完成,待完成后用真机扫描二维码即可开始;

注:目前因为微信 automator 的限制,包括 wait 接口请求等能力暂时在真机上不能支持,这些问题目前已经反馈到微信,有进展我们会及时更新!

七、测试报告

在 .e2erc.js 中配置 report-plugin(详情见.e2erc.js 配置文件部分) 即可在所有 spec 运行结束后自动打开浏览器并呈现测试结果,测试结果包含以下三部分:

1) Jest 数据 +2) 自动截图: +截图分析:在 Mpx-E2E 平台下,点击【截图】Tab 即可,截图以 spec 文件为维度展示,目前包含三种自动时机的截图:定时、点击时、路由发生时,默认全部展示。 +截图筛选:通过操作 timeout/tap/router 的复选框即可实现分类查看,另外在点击时的自动截图还会对点击位置进行标记; +3)JSError 分析 +如果运行过程中小程序抛出 JS 异常,此时会触发 Mpx-E2E 的收集动作,同时会抓取报错瞬间的截图

未来可视化平台会持续集成更多功能,可以涵盖业务稳定,数据稳定(埋点测试)和性能数据,持续更新...

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/tool/ts.html b/docs-vuepress/.vuepress/dist/guide/tool/ts.html new file mode 100644 index 0000000000..bcc732ee83 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/tool/ts.html @@ -0,0 +1,164 @@ + + + + + + 使用TypeScript开发小程序 | Mpx框架 + + + + + + + + + +

# 使用TypeScript开发小程序

# 什么是TypeScript

TypeScript 是一个开源的编程语言,通过在 JavaScript(世界上最常用的语言之一) 的基础上添加静态类型定义构建而成。

类型提供了一种描述对象形状的方法。可以帮助提供更好的文档,还可以让 TypeScript 验证你的代码可以正常工作。

在 TypeScript 中,不是每个地方都需要标注类型,因为类型推断允许您无需编写额外的代码即可获得大量功能。

# TypeScript优势

  1. 静态类型检查 +静态类型检查可以避免很多不必要类型的错误,在编译阶段提前发现问题;

  2. 强大的类型推断能力 +除了类型声明外,TypeScript 提供了强大的类型推断能力,该能力能够大大减少我们需要编写的类型声明,有效地减少我们使用 TypeScript 的额外压力;

  3. IDE 智能提示 +目前主流的 IDE 都对 TypeScript 提供了良好的支持,基于 TypeScript 的类型系统提供友好准确的编码提示与错误检查。

# 使用方式

# 编写ts前的准备工作

由于对 store 做类型推导使用了最新的 TypeScript 特性,因此需要将编辑器的 TypeScript 版本升级至 4.1.3 及以上版本。以下是 VSCode 配置示例:

  1. 更新项目中的TypeScript为 4.1.3 及以上版本。

  2. 在VSCode中打开一个 .js/.ts/.tsx 后缀的文件,使用 Shift+Command+P 唤出 VSCode 命令行,输入 TypeScript ,选择 Select TypeScript Version,选择使用工作区版本,将版本切换至 4.1.3 及以上版本。

  1. 在 VSCode 编辑器中安装 mpx 插件,来支持在.mpx单文件中编写 ts 时的代码提示和实时报错等优秀体验。

WARNING

使用 @typescript/eslint-plugin 对 ts 代码进行检查,当在.mpx文件中使用全局类型时,eslint 会抛出 no-undef 错误,可以关闭相关 eslint 规则校验

# .mpx中编写ts(推荐)

目前 Mpx 已经支持在.mpx文件的 script 标签中编写 ts 代码,需要在 script 标签上添加 lang="ts" 属性,在编译时会自动这部分 script 中的内容进行 ts 类型检查。

<script lang="ts">
+// 内联编写ts代码
+</script>
+

当然,由于大部分IDE对 ts 的语法支持都只对 .ts 和 .d.ts 文件生效,因此 Mpx 也支持创建一个 .ts 文件进行 ts 代码编写,在.mpx文件中,通过 src 的方式引入。

<script lang="ts" src="./index.ts"></script>
+

# 为.ts文件添加loader

在 Webpack 配置中添加如下 rules 以配置 ts-loader

{
+  test: /\.ts$/,
+  use: [
+    'babel-loader',
+    'ts-loader'
+  ]
+}
+

# 编写tsconfig.json文件

对相关配置不熟悉的同学可以直接采用下面配置,能够最大限度发挥 Mpx 中强大的 ts 类型推导能力

{
+  "compilerOptions": {
+    "target": "es6",
+    "allowJs": true,
+    "noImplicitThis": true,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "moduleResolution": "node",
+    "lib": [
+      "dom",
+      "es6",
+      "dom.iterable"
+    ]
+  }
+}
+

# 增强类型

如果需要增加 Mpx 的属性和选项,可以自定义声明 TypeScript 补充现有的类型。

例如,首先创建一个 types.d.ts 文件

// types.d.ts
+
+import { Mpx } from '@mpxjs/core'
+
+declare module '@mpxjs/core' {
+  // 声明为 Mpx 补充的属性
+  interface Mpx {
+    $myProperty: string
+  }
+}
+

之后在任意文件只需引用一次 types.d.ts 声明文件即可,例如在 app.mpx 中引用

// app.mpx
+
+/// <reference path="./types.d.ts" />
+import mpx from '@mpxjs/core'
+
+mpx.$myProperty = 'my-property'
+

# 类型推导及注意事项

Mpx 基于泛型函数提供了非常方便用户使用的反向类型推导能力,简单来说,就是用户可以用非常接近于 js 的方式调用 Mpx 提供的 api ,就能够获得大量基于用户输入参数反向推导得到的类型提示及检查。但是由于 ts 本身的能力限制,我们在 Mpx 的运行时中添加了少量辅助函数和变种api,便于用户最大程度地享受反向类型推导带来的便利性,简单的使用示例如下:

import {createComponent, getMixin, createStoreWithThis} from '@mpxjs/core'
+
+// createStoreWithThis作为createStore的变种方法,主要变化在于定义getters,mutations和actions时,
+// 获取自身的state,getters等属性不再通过参数传入,而是通过this.state或者this.getters等属性进行访问,
+// 由于TS的能力限制,getters/mutations/actions只有使用对象字面量的方式直接传入createStoreWithThis时
+// 才能正确推导出this的类型,当需要将getters/mutations/actions拆解为对象编写时,
+// 需要用户显式地声明this类型,无法直接推导得出。
+
+const store = createStoreWithThis({
+  state: {
+    aa: 1,
+    bb: 2
+  },
+  getters: {
+    cc() {
+      return this.state.aa + this.state.bb
+    }
+  },
+  actions: {
+    doSth3() {
+      console.log(this.getters.cc)
+      return false
+    }
+  }
+})
+
+createComponent({
+  data: {
+    a: 1,
+    b: '2'
+  },
+  computed: {
+    c() {
+      return this.b
+    },
+    d() {
+      // data, mixin, computed中定义的数据能够被推导到this中
+      return this.a + this.aaa + this.c
+    },
+    // 从store上map过来的计算属性或者方法同样能够被推导到this中
+    ...store.mapState(['aa'])
+  },
+  mixins: [
+    // 使用mixins,需要对每一个mixin子项进行getMixin辅助函数包裹,支持嵌套mixin
+    getMixin({
+      computed: {
+        aaa() {
+          return 123
+        }
+      },
+      methods: {
+        doSth() {
+          console.log(this.aaa)
+          return false
+        }
+      }
+    })
+  ],
+  methods: {
+    doSth2() {
+      this.a++
+      console.log(this.d)
+      console.log(this.aa)
+      this.doSth3()
+    },
+    ...store.mapActions(['doSth3'])
+  }
+})
+

# getMixin

为 ts 项目提供的反向推导 mixin 的辅助函数,getMixin 接收一个对象作为参数,使用 mixins 时,支持嵌套 mixin。具体用法见getMixin

# createStoreWithThis

为了在 ts 项目中更好的支持 store 的类型推导,提供了 createStoreWithThis 进行 store 的创建,createStoreWithThis 接收一个 options 对象作为参数。通过 createStoreWithThis 创建 store 时,使用this.statethis.gettersthis.commit, this.dispatch对自身的相关属性进行获取。具体用法见createStoreWithThis

# createStateWithThis

创建一个 state,支持 state 类型推导的辅助函数,具体用法见createStateWithThis

# createGettersWithThis

创建一个 getters,支持 getters 类型推导的辅助函数,具体用法见createGettersWithThis

# createMutationsWithThis

创建一个 mutations,支持 mutations 类型推导的辅助函数,具体用法见createMutationsWithThis

# createActionsWithThis

创建一个 actions,支持 actions 类型推导的辅助函数,具体用法见createActionsWithThis

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/tool/unit-test.html b/docs-vuepress/.vuepress/dist/guide/tool/unit-test.html new file mode 100644 index 0000000000..ab9f9a8676 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/tool/unit-test.html @@ -0,0 +1,149 @@ + + + + + + 单元测试 | Mpx框架 + + + + + + + + + +

# 单元测试

Mpx 框架提供了 jest 转换器 mpx-jest,结合微信小程序提供的 miniprogram-simulate (opens new window) 来进行单元测试的工作。

因为目前仅微信提供了仿真工具,暂时只支持微信小程序平台的单元测试。如果需要 E2E 测试,则和框架无关了,可参考微信的小程序自动化 (opens new window)

如果是初始化项目,单元测试相关的项目依赖和配置可以通过 @mpx/cli 创建项目时选择使用单元测试选项自动生成,如果时旧项目需要使用,可以按照下方步骤安装依赖和添加配置。

# 安装依赖

npm i -D @mpxjs/mpx-jest @mpxjs/miniprogram-simulate jest babel-jest
+
+// 如果项目使用了ts,则还需要安装
+npm i -D ts-jest
+

# jest 相关配置

首先在项目根目录创建 jest.config.js 配置文件,并加入以下关键配置

  testEnvironment: 'jsdom', // 使用 jsdom 环境
+  transform: {
+    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
+    '^.+\\.mpx$': '<rootDir>/node_modules/@mpxjs/mpx-jest',
+    '^.+\\.ts$': '<rootDir>/node_modules/ts-jest' // 如果没使用 ts 可不用添加
+  },
+  setupFiles: ['<rootDir>/test/setup'], // test 文件夹下声明 setup,路径可以随意定义,可以为每一个单测添加相应的配置
+  transformIgnorePatterns: ['node_modules/(?!(@mpxjs))'], // 定义node_modules 中需要进行 transform 的内容
+
+

# 简单的断言

暂时进行一个简单的组件单元测试书写,对于复杂组件以及通用测试逻辑的总结我们会在后续进行发布。

示例如下:

<template>
+  <view>{{ message }}</view>
+</template>
+
+<script>
+  import {createComponent} from '@mpxjs/core'
+  createComponent({
+    data: {
+      message: 'hello!'
+    },
+    attached () {
+      this.message = 'bye!'
+    }
+  })
+</script>
+

对应的 hello-world.spec.js

const simulate = require('@mpxjs/miniprogram-simulate')
+
+// 这里是一些 Jasmine 2.0 的测试,你也可以使用你喜欢的任何断言库或测试工具。
+describe('MyComponent', () => {
+  let id
+  beforeAll(() => {
+    id = simulate.loadMpx('<rootDir>/src/components/hello-world.mpx')
+  })
+
+  // 检查 mount 中的组件实例
+  it('correctly sets the message when component attached', () => {
+    const comp = simulate.render(id)
+    const instance = comp.instance
+    
+    // Mpx提供的数据响应是发生在组件挂载时的,未挂载前只能通过实例上的data访问数据
+    expect(instance.data.message).toBe('hello!')
+    
+    const parent = document.createElement('parent-wrapper') // 创建容器节点
+    comp.attach(parent) // 将组件插入到容器节点中,会触发 attached 生命周期
+    // 挂载后则可以直接通过实例访问
+    expect(instance.message).toBe('bye!')
+  })
+
+  // 创建一个实例并检查渲染输出
+  it('renders the correct message', () => {
+    const comp = simulate.render(id)
+    const parent = document.createElement('parent-wrapper') // 创建容器节点
+    comp.attach(parent) // 挂载组件到容器节点
+    expect(comp.dom.innerHTML).toBe('<wx-view>bye!</wx-view>')
+  })
+})
+

# 编写可被测试的组件

很多组件的渲染输出由它的 props 决定。事实上,如果一个组件的渲染输出完全取决于它的 props,那么它会让测试变得简单,就好像断言不同参数的纯函数的返回值。看下面这个例子:

<template>
+  <view>{{ msg }}</view>
+</template>
+
+<script>
+  import {createComponent} from '@mpxjs/core'
+  createComponent({
+    properties: { msg: String }
+  })
+</script>
+

你可以在不同的 properties 中,通过 simulate.render 的第二个参数控制组件的输出:

const simulate = require('@mpxjs/miniprogram-simulate')
+
+// 省略辅助方法
+describe('MyComponent', () => {
+  it('renders correctly with different props', () => {
+    const id = simulate.loadMpx('<rootDir>/src/components/hello-world.mpx')
+    const comp1 = simulate.render(id, { msg: 'hello' })
+    const parent1 = document.createElement('parent-wrapper')
+    comp1.attach(parent1)
+    expect(comp1.dom.innerHTML).toBe('<wx-view>hello</wx-view>')
+    
+    const comp2 = simulate.render(id, { msg: 'bye' })
+    const parent2 = document.createElement('parent-wrapper')
+    comp2.attach(parent2)
+    expect(comp2.dom.innerHTML).toBe('<wx-view>bye</wx-view>')
+  })
+})
+

# 断言异步更新

小程序视图层的更新是异步的,一些依赖视图更新结果的断言必须 await simulate.sleep() 或者 await comp.instance.$nextTick() 后进行:

const simulate = require('@mpxjs/miniprogram-simulate')
+
+// 省略辅助方法
+it('updates the rendered message when vm.message updates', async () => {
+  const id = simulate.loadMpx('<rootDir>/src/components/hello-world.mpx')
+  const comp = simulate.render(id)
+  const parent = document.createElement('parent-wrapper')
+  comp.attach(parent)
+  comp.instance.msg = 'foo'
+  await simulate.sleep(10)
+  expect(comp.dom.innerHTML).toBe('<wx-view>foo</wx-view>')
+})
+

更深入的 Mpx 单元测试的内容将在以后持续更新……

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/understand/compile.html b/docs-vuepress/.vuepress/dist/guide/understand/compile.html new file mode 100644 index 0000000000..a234b49880 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/understand/compile.html @@ -0,0 +1,52 @@ + + + + + + Mpx编译构建原理 | Mpx框架 + + + + + + + + + +

# Mpx编译构建原理

我们希望使用目前设计最强大、生态最完善的编译构建工具Webpack来实现小程序的编译构建,让用户得到web开发中先进强大的工程化开发体验。使用过Webpack的同学都知道,通常来说Webpack都是将项目中使用到的一系列碎片化模块打包为一个或几个bundle,而小程序所需要的文件结构是非常离散化的,如何调解这两者的矛盾成为了我们最大的难题。一种非常直观简单的思路在于遍历整个src目录,将其中的每一个.mpx文件都作为一个entry加入到Webpack中进行处理,这样做的问题主要有两个:

  1. src目录中用不到的.mpx文件也会被编译输出,最终也会被小程序打包进项目包中,无意义地增加了包体积;
  2. 对于node_modules下的.mpx文件,我们不认为遍历node_modules是一个好的选择。

最终我们采用了一种基于依赖分析和动态添加entry的方式来进行实现,用户在Webpack配置中只需要配置一个入口文件app.mpx,loader在解析到json时会解析json中pages域和usingComponents域中声明的路径,通过动态添加entry的方式将这些文件添加到Webpack的构建系统当中(注意这里是添加entry而不是添加依赖,因为只有entry能生成独立的文件,满足小程序的离散化文件结构),并递归执行这个过程,直到整个项目中所有用到的.mpx文件都加入进来,在输出前,我们借助了CommonsChunkPlugin/SplitChunksPlugin的能力将复用的模块抽取到一个外部的bundle中,确保最终生成的包中不包含重复模块。我们提供了一个Webpack插件和一个.mpx文件对应的loader来实现上述操作,用户只需要将其添加到Webpack配置中就可以以打包web项目的方式正常打包小程序,没有任何的前置和后置操作,支持Webpack本身的完整生态。

Mpx编译构建机制流程示意图

Mpx编译构建机制流程示意图

# 分包处理

构建时对于非JS资源,是所有的包串行处理,资源map会精确记录每个包中引用了哪些资源。

在主包的处理过程中,将主包页面中引用的所有非js资源(组件、外部样式、外部模板、wxs,图像媒体等)都记录下来,在处理分包时,对分包内引用的非js资源都进行检查,如果被主包引用过则输出到主包中,否则标记为分包only的资源输出到分包目录下。

由于主包和分包限制相同,一般情况下分包体积远小于主包体积 +,而微信小程序的体积限制策略主要卡主包体积不得超过2M,因此,Mpx设定的是主包体积优先策略,即尽可能减小主包体积,为此,会将分包资源尽可能打到分包去。

组件和静态资源的输出规则如下:

  1. 主包引用的资源输出至主包
  2. 分包引用且主包引用过的资源输出至主包,不在当前分包重复输出
  3. 分包引用且无其他包引用的资源输出至当前分包
  4. 分包引用且其他分包也引用过的资源,重复输出至当前分包
  5. 当用户通过packageName query显式指定了资源的所属包时,输出至指定的包

如此复杂的分包策略也完全是为了尽可能良好地支持处理分包引用npm资源,因此不管在主包还是分包中,使用Mpx框架开发小程序,可以享受最舒适最自然最好用的npm机制。

而小程序原生的分包是不具备这个能力,只能引用自己分包下的资源,同时大部分基于web技术而来的转译型框架应该也都是不会考虑这个部分的。

甚至如果出现这种场景:同一个组件在A分包和B分包中都有使用,但未在主包使用,会将这个组件分别放入A分包和B分包而不是主包。(是否需要这样可自行斟酌,如果不希望出现这种情况,可在主包中引入一次该组件就会被收到主包去,全局就这一份了)

总结一下:

对于组件,若同时被多个分包引用,主包未引用,会有多份分包存在于各分包中。一旦被主包使用,就仅有一份存在于主包中。

对于资源(例如纯JS模块),若被主包使用,会被收集到主包里。若被多个分包使用,会被收集进主包的bundle里。若仅被某分包使用,就会在分包的bundle.js里(视模块大小及引用次数也可能被内联到某个组件JS里)。

进阶用户有更花式的需求可通过packageName query来显式指定输出。

+ + + diff --git a/docs-vuepress/.vuepress/dist/guide/understand/runtime.html b/docs-vuepress/.vuepress/dist/guide/understand/runtime.html new file mode 100644 index 0000000000..ae5408c4ff --- /dev/null +++ b/docs-vuepress/.vuepress/dist/guide/understand/runtime.html @@ -0,0 +1,51 @@ + + + + + + Mpx运行时增强原理 | Mpx框架 + + + + + + + + + +

# Mpx运行时增强原理

数据响应作为Vue最核心的特性,在我们的日常开发中被大量使用,能够极大地提高前端开发体验和效率,我们在框架设计初期最早考虑的就是如何将数据响应特性加入到小程序开发中。在数据响应的实现上,我们引入了MobX,一个实现了纯粹数据响应能力的知名开源项目。借助MobX和mixins,我们在小程序组件创建初期建立了一个响应式数据管理系统,该系统观察着小程序组件中的所有数据(data/props/computed)并基于数据的变更驱动视图的渲染(setData)及用户注册的watch回调,实现了Vue中的数据响应编程体验。与此同时,我们基于MobX封装实现了一个Vuex规范的数据管理store,能够方便地注入组件进行全局数据管理。为了提高跨团队开发的体验,我们对store添加了多实例可合并的特性,不同团队维护自己的store,在需要时能够合并他人或者公共的store生成新的store实例,我们认为这是一种比Vuex中modules更加灵活便捷的跨团队数据管理模式

作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个:

  1. 尽可能减少setData调用的频次
  2. 尽可能减少单次setData传输的数据

为了实现以上两个优化方向,我们做了以下几项工作:

  • 将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次;
  • 将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。

Mpx数据响应机制流程示意图

Mpx数据响应机制流程示意图

+ + + diff --git a/docs-vuepress/.vuepress/dist/index.html b/docs-vuepress/.vuepress/dist/index.html new file mode 100644 index 0000000000..d44770cb87 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/index.html @@ -0,0 +1,55 @@ + + + + + + Mpx框架 + + + + + + + + + +

增强型跨端小程序框架

+ 良好的开发体验,极致的应用性能,完整的原生兼容,一份源码跨端输出所有小程序平台及Web。 +

  • svg

    高性能

    Mpx高度关注小程序性能与包体积,深度整合了运行时性能优化与包体积分析优化能力,让开发者在大部分场景下只需专注于业务开发,就能生产出媲美甚至超出原生的高性能小程序应用。

  • svg

    优体验

    以增强的方式将Vue中优良的开发特性引入到小程序开发中,如数据响应、组合式api等,配合强大的工程化能力,大大提升了小程序开发的体验与效率,同时保障了框架开发的可维护性与可预期性。

  • svg

    跨平台

    Mpx专注解决小程序跨端问题,通过静态转译与运行时适配结合,将一份源码跨端输出到所有开放的小程序平台和web环境下运行,同时最大限度减少跨端带来的性能与包体积损失。

phone

示例项目

+ 扫码体验Mpx版本的 + todoMVC + 在各个小程序平台和web中的一致表现 ,更多示例项目可点击 + 这里 + 进入查看。 +

code
code
svg

极致性能

+ 得益于增强的设计思路,Mpx在运行时没有复杂的封装抹平逻辑,而是专注于实现数据响应,setData优化和Composition api等关键增强能力,压缩后体积占用仅为60KB;配合编译构建中灵活强大的包体积分析优化能力,Mpx在性能与包体积方面做到了业内最优。 +

渐进迁移

+ 同样得益于增强的设计思路,Mpx能够完整兼容小程序原生技术规范,并以较低的成本进行持续跟进;借助框架提供的渐进迁移能力,小程序开发者可以方便地在Mpx项目中使用已有的原生开发生态,如组件库,统计工具等,同时也能将Mpx开发的组件输出到原生小程序项目中使用。 +

svg

成功案例

+ 滴滴出行 +

phone
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
left
二维码
滴滴出行
二维码
青桔单车
二维码
滴滴顺风车
二维码
出行广场
二维码
滴滴公交
二维码
滴滴金融
二维码
滴滴外卖
二维码
司机招募
二维码
小桔加油
二维码
番薯借阅
二维码
疫查查应用
二维码
小桔养车
二维码
学而思直播课
二维码
小猴启蒙课
二维码
科创书店
二维码
在武院
二维码
三股绳Lite
二维码
学而思优选课
二维码
食享会
二维码
青铜安全医生
二维码
青铜安全培训
二维码
视穹云机械
二维码
店有生意通
二维码
花小猪打车
二维码
橙心优选
二维码
小二押镖
二维码
顺鑫官方微商城
二维码
嘀嗒出行
二维码
汉行通Pro
二维码
滴滴出行(支付宝)
二维码
小桔充电(支付宝)
二维码
唯品会QQ
二维码
唯品会(百度)
二维码
唯品会(字节)
right
+ + + diff --git a/docs-vuepress/.vuepress/dist/logo.png b/docs-vuepress/.vuepress/dist/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c5e0693f74a5f2fbf80a8f7b7c73684a83f687b0 GIT binary patch literal 13635 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 literal 0 HcmV?d00001 diff --git a/docs-vuepress/.vuepress/dist/manifest.webmanifest b/docs-vuepress/.vuepress/dist/manifest.webmanifest new file mode 100644 index 0000000000..dcc89dfc8b --- /dev/null +++ b/docs-vuepress/.vuepress/dist/manifest.webmanifest @@ -0,0 +1,15 @@ +{ + "name": "Mpx", + "short_name": "Mpx", + "icons": [ + { + "src": "https://dpubstatic.udache.com/static/dpubimg/1ESVodfAED/logo.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "start_url": "/index.html", + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/rn-api.html b/docs-vuepress/.vuepress/dist/rn-api.html new file mode 100644 index 0000000000..ff2acf3a1a --- /dev/null +++ b/docs-vuepress/.vuepress/dist/rn-api.html @@ -0,0 +1,65 @@ + + + + + + API支持列表 | Mpx框架 + + + + + + + + + +

# API支持列表

mpx转RN的对标微信api目前支持及部分的api转换,目前支持的能力可以参考下表:

支持方法
getSystemInfo
getSystemInfoSync
getDeviceInfo
getWindowInfo
request
setStorage
removeStorage
removeStorageSync
getStorage
getStorageInfo
clearStorage
clearStorageSync
setClipboardData
getClipboardData
makePhoneCall
onWindowResize
offWindowResize
arrayBufferToBase64
base64ToArrayBuffer
connectSocket
getNetworkType
onNetworkStatusChange
offNetworkStatusChange

# 使用说明

对于一些api-proxy中没有提供的能力,用户可以搭配mpx对象方式传入custom使用即可示例如下:

import mpx from '@mpxjs/core'
+import apiProxy from '@mpxjs/api-proxy'
+import { showModal } from '@test/showModal'
+
+mpx.use(apiProxy, {
+  custom: {
+    ios: {
+      showModal
+    },
+    android: {
+      showModal
+    }
+  }
+})
+
+mpx.showModal({
+  title: '标题',
+  content: '这是一个弹窗',
+  success (res) {
+    console.log('弹框展示成功')
+  }
+})
+

# API抹平差异详情

对于一些微信独有的返回结果,或者RN目前不能支持的入参/返回结果,在下面会有详细说明:

# getSystemInfo/getSystemInfoSync

不支持返回值
language
version
SDKVersion
benchmarkLevel
albumAuthorized
cameraAuthorized
locationAuthorized
microphoneAuthorized
notificationAuthorized
phoneCalendarAuthorized
host
enableDebug
notificationAlertAuthorized
notificationBadgeAuthorized
notificationSoundAuthorized
bluetoothEnabled
locationEnabled
wifiEnabled
locationReducedAccuracy
theme

# getDeviceInfo

不支持返回值
benchmarkLevel
abi
cpuType

# getWindowInfo

不支持返回值
screenTop

# request

不支持入参
useHighPerformanceMode
enableHttp2
enableProfile
enableQuic
enableCache
enableHttpDNS
httpDNSServiceId
enableChunked
forceCellularNetwork
redirect
不支持返回值
cookies
profile
exception

# setStorage/getStorage

不支持入参
encrypt

# getStorageInfo

不支持返回值
currentSize
limitSize

# connectSocket

不支持入参
tcpNoDelay
perMessageDeflate
timeout
forceCellularNetwork

# getNetworkType

不支持返回值
signalStrength
hasSystemProxy
+ + + diff --git a/docs-vuepress/.vuepress/dist/rn-component.html b/docs-vuepress/.vuepress/dist/rn-component.html new file mode 100644 index 0000000000..71bec22783 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/rn-component.html @@ -0,0 +1,44 @@ + + + + + + RN 自定义组件支持 | Mpx框架 + + + + + + + + + +

# RN 自定义组件支持

创建自定义组件在 RN 环境下部分实例方法、属性存在兼容性问题不支持, +此文档以微信小程序为参照,会详细列出各方法、属性的支持度。

# 参数

# 组件定义属性说明

属性 类型 RN 是否支持 描述
properties Object Map 组件的对外属性,是属性名到属性设置的映射表
data Object 组件的内部数据,和 properties 一同用于组件的模板渲染
observers Object 组件数据字段监听器,用于监听 propertiesdata 的变化
methods Object 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用
behaviors String Array 输出 RN 不支持
created Function 组件生命周期函数-在组件实例刚刚被创建时执行,注意此时不能调用 setData
attached Function 组件生命周期函数-在组件实例进入页面节点树时执行
ready Function 组件生命周期函数-在组件布局完成后执行
moved Function RN 不支持,组件生命周期函数-在组件实例被移动到节点树另一个位置时执行
detached Function 组件生命周期函数-在组件实例被从页面节点树移除时执行
relations Object 输出 RN 不支持
externalClasses String Array 输出 RN 不支持
options Object Map 输出 RN 不支持,一些选项,诸如 multipleSlots、virtualHost、pureDataPattern,这些功能输出 RN 不支持
lifetimes Object 组件生命周期声明对象
pageLifetimes Object 组件所在页面的生命周期声明对象

# 组件实例属性与方法

生成的组件实例可以在组件的方法、生命周期函数中通过 this 访问。组件包含一些通用属性和方法。

属性名 类型 RN 是否支持 描述
is String 输出 RN 暂不支持,未来支持, 组件的文件路径
id String 节点id
dataset String 节点dataset
data Object 组件数据,通过 this 直接访问
properties Object 组件props,通过 this 直接访问
router Object 输出 RN 暂不支持
pageRouter Object 输出 RN 暂不支持
renderer string 输出 RN 暂不支持

微信小程序原生方法

方法名 RN是否支持 参数 描述
setData Object newData 设置data并执行视图层渲染
hasBehavior Object behavior 检查组件是否具有 behavior
triggerEvent String name, Object detail, Object options 触发事件
createSelectorQuery 输出 RN 暂不支持,未来支持,建议使用 ref
createIntersectionObserver 输出 RN 暂不支持
selectComponent String selector 输出 RN 暂不支持,未来支持,建议使用 ref
selectAllComponents String selector 输出 RN 暂不支持,未来支持,建议使用 ref
selectOwnerComponent 输出 RN 不支持
getRelationNodes String relationKey 输出 RN 不支持
groupSetData Function callback 输出 RN 不支持
getTabBar 输出 RN 不支持
getPageId 输出 RN 不支持
animate String selector, Array keyframes, ... 输出 RN 不支持
clearAnimation String selector, Object options, ... 输出 RN 不支持

Mpx 框架增强实例方法

方法名 RN是否支持 描述
$set 向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新
$watch 观察 Mpx 实例上的一个表达式或者一个函数计算结果的变化
$delete 删除对象属性,如果该对象是响应式的,那么该方法可以触发观察器更新(视图更新
$refs 一个对象,持有注册过 ref的所有 DOM 元素和组件实例,调用响应的组件方法或者获取视图节点信息。
$asyncRefs 输出 RN 不支持
$forceUpdate 用于强制刷新视图,不常用,通常建议使用响应式数据驱动视图更新
$nextTick 在下次 DOM 更新循环结束之后执行延迟回调函数,用于等待 Mpx 完成状态更新和 DOM 更新后再执行某些操作
$i18n 输出 RN 暂不支持,国际化功能访问器,用于获取多语言字符串资源
$rawOptions 访问组件原始选项对象
+ + + diff --git a/docs-vuepress/.vuepress/dist/rn-style.html b/docs-vuepress/.vuepress/dist/rn-style.html new file mode 100644 index 0000000000..10589cd7ff --- /dev/null +++ b/docs-vuepress/.vuepress/dist/rn-style.html @@ -0,0 +1,331 @@ + + + + + + Mpx转RN样式使用指南 | Mpx框架 + + + + + + + + + +

# Mpx转RN样式使用指南

RN 的样式的支持基本为 web 样式的一个子集,同时还有一些属性并未与 web 对齐,因此跨平台输出 RN 时,为了保障多端输出样式一致,可参考本文针对样式在RN上的支持情况来进行样式编写。

# 样式编写限制

# 选择器

RN 环境下仅支持以下类名选择器,不支持逗号之外的组合选择器。

/* 支持 */
+.classname {
+  color: red
+}
+.classA, .classB {
+    color: red
+}
+

# 布局

在 RN 中布局方式有限制,像 block inline inline-block 和 fixed 等都不支持,支持的布局方式如下:

# flex

在RN中,常规的组件元素可以用过 flex 布局来实现,参考文档 (opens new window)

:RN中view标签的主轴是column,和css不一致,使用mpx开发设置dispay:flex时,会默认的主轴方向是row。

# relative/absolute

在 RN 中 position 仅支持 relative(默认)和 absolute,可参考文档 (opens new window)

# 样式单位

# number 类型值

RN 环境中,number 数值型单位支持 px rpx % 三种,web 下的 vw em rem 等不支持。

# color 类型值

RN 环境支持大部分 css 中 color 定义方式,仅少量不支持,详情参考 RN 文档 https://reactnative.dev/docs/colors

# 组件样式规则

在web和RN下组件的渲染会存在一些差异的地方,比如默认样式不一致、继承的关系不一致等。框架针对这些不一致进行了抹平工作,但是仍旧存在一些使用限制,具体参考以下组件节点的介绍。

# text

RN中,文本节点需要通过Text组件来创建文本节点。文本节点需要给Text组件来设定属性 (opens new window)来调整文本的外观。

Web/小程序中,文本节点可以通过div/view节点进行直接包裹,在div/view标签上设定对应文本样式即可。不需单独包裹text节点。

框架抹平了此部分的差异,但仍因受限于RN内text的样式继承原则的限制 (opens new window),通过在祖先节点来设置文本节点的样式仍旧无法生效。具体例子说明如下

<view class="wrapper">
+    <view class="content">我是文本</view>
+</view>
+
+.wrapper {
+    font-size: 20px;
+}
+.content {
+    text-align: right;
+}
+/** 以上例子中
+web渲染效果: 字体的大小为20px,文字居右 
+转RN之后渲染效果: 字体大小为默认大小,文字居右
+*/
+
+

限制与使用说明

  1. 无法通过设置祖先节点的样式来修改文本节点的样式,只可通过修改直接包裹文本的节点来修改文本的样式。
  2. 框架处理将文本节点样式的默认值与web进行了对齐,如果需要按照RN的默认值来进行渲染,可设置disable-default-styletrue

# view

background、background-image、background-size、background-repeat仅在view标签下支持,background-color在其他标签上也支持。

各属性支持的类型

  • background - 仅支持 background-image | background-color | background-size | background-repeat,每个属性支持情况,可参考下面的介绍。
  • background-image - 仅支持 <url()>。
  • background-size - 支持px|rpx|%,也支持枚举值 contain|cover|auto; 不支持两个以上的值进行设置。
  • backgroundno-repeat - 仅支持值为no-repeat。
  • backfround-color - 参考Color (opens new window)

支持的语法


+/** 1. background **/
+/* 支持 */
+background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink contain no-repeat;
+background: #000;
+background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink;
+
+/* 不支持 */
+background: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
+
+
+/** 2. background-image **/
+/* 支持 */
+background-image: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg");
+/* 不支持 */
+background-image: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
+
+
+/** 3. background-size **/
+/* 支持 */
+background-size: 50%;
+background-size: 50% 25%;
+background-size: contain;
+background-size: cover;
+background-size: auto;
+background-size: 20px auto;
+
+/ * 不支持 * /
+background-size: 50%, 25%, 25%;
+
+
+/** 4. background-repeat **/
+/* 支持 */
+background-repeat: no-repeat;
+
+/* 不支持 */
+background-repeat: repeat;
+
+
+/** 5. background-color **/
+background-repeat: red;
+

# 样式参考

# Layout Style

# position

设置元素的定位样式 +值支持类型 +enum: absolute, relative, 默认relative。

# 语法
position: absolute;
+position: relative;
+
+

# top|right|left|bottom

设置元素的不同方向的偏移量

# 值支持类型

number: px,rpx,%

# 语法
top: 10px;
+top: 10rpx;
+top: 10%;
+

# z-index

控制着元素的堆叠覆盖顺序。

# 值支持类型

number

# 语法
z-index: 1;
+

# display

设置元素的布局方式。

# 值支持类型

flex/none

# 语法
/* 默认 */ 
+display:flex
+/* 隐藏 */
+display:none
+

# align-content

设置多根轴线的对齐方式。

# 值支持类型

enum: flex-start, flex-end, center, stretch, space-between, space-around, space-evenly

# 语法
/** 支持 **/
+align-content: flex-start;
+align-content: flex-end;
+align-content: center;
+align-content: stretch;
+align-content: space-between;
+align-content: space-around;
+align-content: space-evenly;
+
+/** 不支持 **/
+align-content: safe center;
+align-content: unsafe center;
+

# align-items

设置单根轴线上的子元素的对齐方式。默认会是交叉轴上的。

# 值支持类型

enum: flex-start, flex-end, center, stretch, baseline

# 语法
/** 支持 **/
+align-items: flex-start;
+align-items: flex-end;
+align-items: center;
+align-items: stretch;
+align-items: baseline;
+
+/** 不支持 **/
+align-items: first baseline;
+align-items: last baseline;
+

# align-self

设置单个子元素在单根轴线上的对齐方式

# 值支持类型

enum: auto, flex-start, flex-end, center, stretch, baseline

# 语法
/** 支持 **/
+align-self: auto;
+align-self: flex-start;
+align-self: flex-end;
+align-self: center;
+align-self: stretch;
+align-self: baseline;
+
+/** 不支持 **/
+align-self: first baseline;
+align-self: last baseline;
+

# flex

仅支持 flex-grow | flex-shrink | flex-basis 这种顺序,值以空格分隔按顺序赋值

# 值支持类型

flex: number number px/rpx

# 语法
/** 简写 **/
+flex: 1;
+/* flex-grow | flex-shrink | flex-basis */
+flex: 0 1 1;
+

# flex-grow

设置子盒子的放大比例

# 值支持类型

number

# 语法
flex-grow: 1;
+

# flex-shrink

设置子盒子的缩放比例。

# 值支持类型

number

# 语法
flex-shrink: 1;
+

# flex-basis

设置在分配多余空间之前,子盒子的初始主轴尺寸。

# 值支持类型

number px|rpx|%

# 语法
flex-shrink: 10px;
+flex-shrink: 10rpx;
+flex-shrink: 20%;
+

# flex-direction

设置主轴的方向。

# 值支持类型

enum: row, row-reverse, column, column-reverse

# 语法
flex-direction: row;
+flex-direction: row-reverse;
+flex-direction: column;
+flex-direction: column-reverse;
+
+

# flex-wrap

设置元素是否换行。当值为wrap时,alignItems:center不生效。

# 值支持类型

enum: wrap, nowrap, wrap-reverse

# 语法
flex-wrap: wrap;
+flex-wrap: nowrap;
+flex-wrap: wrap-reverse;
+

# flex-flow

是flex-direction flex-wrap 缩写,仅支持 flex-flow: flex-direction flex-wrap 这种顺序,值以空格分隔按顺序赋值

/* flex-direction */
+flex-flow: row;
+/* flex-direction|  flex-wrap*/
+flex-flow: row nowrap;
+

# View Style

# margin

margin 是 margin-top|margin-right|margin-left|margin-bottom 的缩写模式, 目前仅支持四种缩写模式。

# 值支持类型

string: 'auto' +number: rpx,px, %

# 语法
/* all */
+margin: 2px;
+
+/* top and bottom | left and right */
+margin: 5% auto;
+
+/* top | left and right | bottom */
+margin: 1rpx auto 2rpx;
+
+/* top | right | bottom | left */
+margin: 1rpx 2rpx 2rpx ;
+

# margin-top|margin-bottom|margin-right|margin-left

# 值支持类型

number: rpx,px, %

# 语法
margin-top: 2px;
+margin-top: 2rpx;
+margin-top: 10%;
+

# padding

padding是padding-left、padding-right、padding-left、padding-bottom的缩写模式, 目前仅支持四种缩写模式。

# 值支持类型

string: 'auto' +number: rpx,px, %

# 语法
/* all */
+padding: 2px;
+
+/* top and bottom | left and right */
+padding: 5% 10%;
+
+/* top | left and right | bottom */
+padding: 1rpx 0 2rpx;
+
+/* top | right | bottom | left */
+padding: 1rpx 2rpx 2rpx ;
+

# padding-top|padding-bottom|padding-left|padding-right

# 值支持类型

number: rpx,px, %

# 语法
/** padding-top **/
+padding-top: 2px;
+padding-top: 2rpx;
+padding-top: 10%;
+

# border

border 是 border-width|border-style|border-color 的缩写模式, 目前仅支持 width | style | color 这种排序,值以空格分隔按顺序赋值

/* width | style | color */
+border: 1px solid red;
+border: 1px;
+border: 1px solid;
+border: 1px double pink;
+

# border-color

设置边框的颜色, 目前只支持统一设置,不支持缩写。

# 值支持类型

color: 参考Color (opens new window)

# 语法
/* all border */
+/** 支持 **/
+border-color: red;
+

# border-style

设置边框的样式, 目前只支持统一设置,不支持缩写。

# 值支持类型

enum: solid|dotted|dashed

# 语法
/** 支持 **/
+/* all border */
+border-color: 'solid';
+border-color: 'dotted';
+border-color: 'dashed';
+
+/** 不支持 **/
+border-style: double;
+border-style: groove;
+border-style: ridge;
+border-style: dotted solid;
+border-style: hidden double dashed;
+border-style: none solid dotted dashed;
+

# border-width

设置边框的宽度,目前只支持统一设置,不支持缩写。

# 值支持类型

number: px rpx %

# 语法
/* all border */
+border-width: 2px;
+

# border-top-color|border-bottom-color|border-left-color|border-right-color

设置各边框的颜色

# 值支持类型

color: 参考Color (opens new window)

# 语法
border-top-color: red;
+

# border-top-width|border-bottom-width|border-left-width|border-right-width

设置各边框的宽度

# 值支持类型

number: px rpx

# 语法
border-top-width: 2px;
+

# border-radius

设置border的圆角格式,支持一种缩写方式

# 值支持类型

仅支持 border-radius 0px|border-radius 0px 0px 0px 0px(值以空格分隔按顺序赋值) +number: px rpx %

# 语法
/* all */
+border-radius: 2px;
+/* top-left | top-right | bottom-right | bottom-left */
+border-radius: 10px 10px 10px 0;
+

# border-bottom-left-radius|border-bottom-right-radius|border-top-left-radius|border-top-right-radius

# 值支持类型

number: px rpx %

# 语法
border-bottom-left-radius: 2px;
+

# Text Style

# color

# 值支持类型

color: 参考Color (opens new window)

# 语法
color: orange;
+color: #fff;
+color: #fafafa;
+color: rgb(255, 255, 255);
+color: rgba(255, 99, 71, 0.2)
+

# font-family

可设置系统字体,引入字体文件,暂时不支持。

# 值支持类型

string

# 语法
font-family: "PingFangSC-Regular"
+

# font-size

可设置字体的大小

# 值支持类型

number: px,rpx

# 语法
font-size: 12px;
+font-size: 12rpx;
+

# font-style

设置文本的字体样式。

# 值支持类型

enum: normal,italic

# 语法
font-style: italic;
+font-style: normal;
+

# font-weight

设置文字的权重。

# 值支持类型

enum: 100,200,300,400,500,600,800,900,normal,bold

# 语法
font-weight: 100;
+font-weight: 200;
+font-weight: 300;
+font-weight: 400;
+font-weight: 500;
+font-weight: 600;
+font-weight: 700;
+font-weight: 800;
+font-weight: 900;
+
+font-weight: normal;
+font-weight: bold;
+

# font-variant

设置文本的字体变体

# 值支持类型

enum: small-caps, oldstyle-nums, lining-nums, tabular-nums, proportional-nums

# 语法
font-variant: small-caps;
+font-variant: oldstyle-nums;
+font-variant: lining-nums;
+font-variant: tabular-nums;
+font-variant: lining-nums;
+font-variant: proportional-nums;
+

# letter-spacing

定义字符之间的间距

# 值支持类型

px,rpx

# 语法
letter-spacing: 2px;
+letter-spacing: 2rpx;
+

# line-height

设置行高。

# 值支持类型

px,rpx,%

# 语法
line-height: 16px
+line-height: 16rpx
+line-height: 100%
+line-height: 1
+

# text-align

设置文本的水平对齐方式。

# 值支持类型

enum: left, right, center, justify

# 语法
/** 支持 **/
+text-align: left;
+text-align: right;
+text-align: center;
+text-align: justify;
+
+/** 不支持 **/
+text-align: match-parent;
+text-align: auto;
+text-align: justify-all;
+

# text-decoration-line

设置文本的装饰线样式。

# 值支持类型

enum: none, underline, line-through, underline line-through

# 语法
/** 支持 **/
+text-decoration-line: none;
+text-decoration-line: underline;
+text-decoration-line: line-through;
+text-decoration-line: underline line-through;
+
+/** 不支持 **/
+text-decoration-line: overline;
+

# text-transform

设置文本的大小写转换。

# 值支持类型

enum: none, uppercase, lowercase, capitalize

# 语法
/** 支持 **/
+text-transform: none;
+text-transform: uppercase;
+text-transform: lowercase;
+text-transform: capitalize;
+
+/** 不支持 **/
+text-transform: none;
+text-transform: uppercase;
+text-transform: lowercase;
+text-transform: capitalize;
+

# text-shadow

设置文本阴影

# 值支持类型

仅支持 offset-x | offset-y | blur-radius | color 排序,值以空格分隔按顺序赋值

# 语法
text-shadow: 1rpx 3rpx 0 #2E0C02;
+

# Background Style

背景相关的属性

# background-color

表示背景色,可以在任何标签上使用。

# 值支持类型

color: 参考Color (opens new window)

# 语法
/* all border */
+background-color: red;
+

# background-image

表示背景图,只能在view上使用

# 值支持类型

仅支持 <url()>

# 语法
background-image: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg");
+
+/* 不支持 */
+background-image: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
+

# background-size

表示背景大小,只能在view上使用

# 值支持类型

number 支持 px|rpx|%,枚举值支持 contain|cover|auto; +支持一个值:这个值指定图片的宽度,图片的高度隐式的为 auto; +支持两个值:第一个值指定图片的宽度,第二个值指定图片的高度; +不支持逗号分隔的多个值:设置多重背景!!!

# 语法
/* 支持 */
+background-size: 50%;
+background-size: 50% 25%;
+background-size: contain;
+background-size: cover;
+background-size: auto;
+background-size: 20px auto;
+
+/ * 不支持 * /
+background-size: 50%, 25%, 25%;
+

# background-repeat

表示背景图是否重复,只能在view上使用

# 值支持类型

enum: no-repeat

# 语法
background-repeat: no-repeat;
+
+/* 不支持 */
+background-repeat: repeat; 
+

# background

表示背景的组合属性,只能在view上使用

# 值支持类型

仅支持 background-image | background-color | background-size | background-repeat,具体每个属性的支持情况参见上面具体属性支持的文档

# 语法
/* 支持 */
+background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink no-repeat;
+background: #000;
+background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink;
+
+/* 不支持 */
+background: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
+

# Shadow Style

# box-shadow

此属性是阴影颜色、阴影的偏移量

# 值支持类型

仅支持 offset-x | offset-y | blur-radius | color 排序,值以空格分隔按顺序赋值

# 语法
/* offset-x | offset-y | blur-radius | color */
+box-shadow: 0 1px 3px rgba(139,0,0,0.32);
+

# 附录

# RN 不支持的属性

若设置以下不支持的属性会被 mpx 编译处理时丢弃,有编译 error 提示

描述 不支持的属性
双端都不支持 box-sizing white-space text-overflow animation transition
ios 不支持 vertical-align
android 不支持 text-decoration-style text-decoration-color shadow-offset shadow-opacity shadow-radius

# RN 支持的枚举值

RN 支持的枚举值映射如下表,其他不支持的枚举值会被 mpx 编译处理时丢弃,设置无效

prop value 枚举
overflow visible hidden scroll
border-style solid dotted dashed
display flex none
pointer-events auto none
position relative absolute
vertical-align auto top bottom center
font-variant small-caps oldstyle-nums lining-nums tabular-nums proportional-nums
text-align left right center justify
font-style normal italic
font-weight normal bold 100-900
text-decoration-line none underline line-through 'underline line-through'
text-transform none uppercase lowercase capitalize
user-select auto text none contain all
align-content flex-start flex-end none center stretch space-between space-around
align-items flex-start flex-end center stretch baseline
align-self auto flex-start flex-end center stretch baseline
justify-content flex-start flex-end center space-between space-around space-evenly none
background-repeat no-repeat

# 缩写支持

RN 仅支持部分常用的缩写形式,具体参加下表:

缩写属性 支持的缩写格式 备注
text-decoration 仅支持 text-decoration-line text-decoration-style text-decoration-color 顺序固定,值以空格分隔后按按顺序赋值
margin margin: 0;margin: 0 auto;margin: 0 auto 10px;margin: 0 10px 10px 20px; -
padding padding: 0;padding: 0 auto;padding: 0 auto 10px;padding: 0 10px 10px 20px; -
text-shadow 仅支持 offset-x offset-y blur-radius color 排序 顺序固定,值以空格分隔后按按顺序赋值
border 仅支持 border-width border-style border-color 顺序固定,值以空格分隔后按按顺序赋值
box-shadow 仅支持 offset-x offset-y blur-radius color 顺序固定,值以空格分隔后按按顺序赋值
flex 仅支持 flex-grow flex-shrink flex-basis 顺序固定,值以空格分隔后按按顺序赋值
flex-flow 仅支持 flex-direction flex-wrap 顺序固定,值以空格分隔后按按顺序赋值
border-radius 支持 border-top-left-radius border-top-right-radius border-bottom-right-radius border-bottom-left-radius 顺序固定,值以空格分隔后按按顺序赋值;当设置 border-radius: 0 相当于同时设置了4个方向
background 仅支持 background-image background-color background-repeat 顺序不固定,具体每个属性的支持情况参见上面具体属性支持的文档;
+ + + diff --git a/docs-vuepress/.vuepress/dist/rn-template.html b/docs-vuepress/.vuepress/dist/rn-template.html new file mode 100644 index 0000000000..4881256db1 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/rn-template.html @@ -0,0 +1,104 @@ + + + + + + Mpx转RN模版使用指南 | Mpx框架 + + + + + + + + + +

# Mpx转RN模版使用指南

# 模版指令

目前 Mpx 输出 React Native 仅支持以下指令,具体使用范围可参考如下文档

指令名 说明
wx:if 根据表达式的值的来有条件地渲染元素
wx:elif 根据表达式的值的来有条件地渲染元素,前一兄弟元素必须有 wx:if 或 wx:elif
wx:else 不需要表达式,前一兄弟元素必须有 wx:if 或 wx:elif
wx:show 根据表达式的值的来有条件地渲染元素,与 wx:if 所不同的是不会移除节点,而是设置节点的 style 为 display: none
wx:style 动态绑定 style 样式
wx:class 动态绑定 class 样式
wx:for 在组件上使用 wx:for 绑定一个数组,即可使数组中各项的数据重复渲染该组件
wx:for-item 指定数组当前元素的变量名
wx:for-index 指定数组当前下标的变量名
wx:key 指定列表中项目的唯一的标识符
wx:ref 获取节点信息
mpxTagName 动态转换标签
component 使用 component is 动态切换组件
@mode 使用 @ 符号来指定某个节点或属性只在某些平台下有效
@_mode 隐式属性条件编译,仅控制节点的展示,保留节点属性的平台转换能力
@env 自定义 env 目标应用,来实现在不同应用下编译产出不同的代码

# 事件编写

目前 Mpx 输出 React Native 的事件编写遵循小程序的事件编写规范,支持事件的冒泡及捕获

普通事件绑定

<view bindtap="handleTap">
+    Click here!
+</view>
+

绑定并阻止事件冒泡

<view catchtap="handleTap">
+    Click here!
+</view>
+

事件捕获

<view capture-bind:touchstart="handleTap1">
+  outer view
+  <view capture-bind:touchstart="handleTap2">
+    inner view
+  </view>
+</view>
+

中断捕获阶段和取消冒泡阶段

<view capture-catch:touchstart="handleTap1">
+  outer view
+</view>
+
+

在此基础上也新增了事件处理内联传参的增强机制。

<template>
+ <!--Mpx增强语法,模板内联传参,方便简洁-->
+ <view bindtap="handleTapInline('b')">b</view>
+ </template>
+ <script setup>
+  // 直接通过参数获取数据,直观方便
+  const handleTapInline = (name) => {
+    console.log('name:', name)
+  }
+  // ...
+</script>
+

除此之外,Mpx 也支持了动态事件绑定

<template>
+ <!--动态事件绑定-->
+ <view wx:for="{{items}}" bindtap="handleTap_{{index}}">
+  {{item}}
+</view>
+ </template>
+ <script setup>
+  import { ref } from '@mpxjs/core'
+
+  const data = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4'])
+  const handleTap_0 = (event) => {
+    console.log('Tapped on item 1');
+  },
+
+  const handleTap_1 = (event) => {
+    console.log('Tapped on item 2');
+  },
+
+  const handleTap_2 = (event) => {
+    console.log('Tapped on item 3');
+  },
+
+  const handleTap_3 = (event) => {
+    console.log('Tapped on item 4');
+  }
+</script>
+

注意事项:

当同一个元素上同时绑定了 catchtap 和 bindtap 事件时:

两个事件都会被触发执行。 +但是是否阻止事件冒泡的行为,会以模板上第一个绑定的事件标识符为准。 +如果第一个绑定的是 catchtap,那么不管后面绑定的是什么,都会阻止事件冒泡。 +如果第一个绑定的是 bindtap,则不会阻止事件冒泡。

同理,如果同一个元素上绑定了 capture-bind:tap 和 bindtap:

事件的执行时机会根据模板上第一个绑定事件的标识符来决定: +如果第一个绑定的是 capture-bind:tap,则事件会在捕获阶段触发。 +如果第一个绑定的是 bindtap,则事件会在冒泡阶段触发。

# 基础组件

目前 Mpx 输出 React Native 仅支持以下组件,具体使用范围可参考如下文档

# view

视图容器。

属性

属性名 类型 默认值 说明
hover-class string 指定按下去的样式类。
hover-start-time number 50 按住后多久出现点击态,单位毫秒
hover-stay-time number 400 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

# text

文本。

属性

属性名 类型 默认值 说明
user-select boolean false 文本是否可选。
disable-default-style boolean false 会内置默认样式,比如fontSize为16。设置true可以禁止默认的内置样式。
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

注意事项

  1. 未包裹 text 标签的文本,会自动包裹 text 标签。
  2. text 组件开启 enable-offset 后,offsetLeft、offsetWidth 获取时机仅为组件首次渲染阶段

# image

图片。

属性

属性名 类型 默认值 说明
src String false 图片资源地址,支持本地图片资源及 base64 格式数据,暂不支持 svg 格式
mode String scaleToFill 图片裁剪、缩放的模式,适配微信 image 所有 mode 格式
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
binderror 当错误发生时触发,event.detail = { errMsg }
bindload 当图片载入完毕时触发,event.detail = { height, width }

注意事项

  1. image 组件默认宽度320px、高度240px
  2. image 组件进行缩放时,计算出来的宽高可能带有小数,在不同webview内核下渲染可能会被抹去小数部分

# input

输入框。

属性

属性名 类型 默认值 说明
value String 输入框的初始内容
type String text input 的类型,不支持 safe-passwordnickname
password Boolean false 是否是密码类型
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
confirm-type String done 设置键盘右下角按钮的文字,仅在 type='text' 时生效
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bind:selectionchange 选区改变事件, event.detail = { selectionStart, selectionEnd }

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# textarea

多行输入框。

属性

属性名 类型 默认值 说明
value String 输入框内容
type String text input 的类型,不支持 safe-passwordnickname
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
auto-height Boolean false 是否自动增高,设置 auto-height 时,style.height不生效
confirm-type String done 设置键盘右下角按钮的文字,不支持 return
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bindlinechange 输入框行数变化时调用,event.detail = { height: 0, lineCount: 0 },不支持 heightRpx
bind:selectionchange 选区改变事件, {selectionStart, selectionEnd}

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# button

按钮。

属性

属性名 类型 默认值 说明
size String default 按钮的大小
type String default 按钮的样式类型
plain Boolean false 按钮是否镂空,背景色透明
disabled Boolean false 是否禁用
loading Boolean false 名称前是否带 loading 图标
open-type String 微信开放能力,当前仅支持 share
hover-class String 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果
hover-start-time Number 20 按住后多久出现点击态,单位毫秒
hover-stay-time Number 70 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

# scroll-view

可滚动视图区域。

属性

属性名 类型 默认值 说明
scroll-x Boolean false 允许横向滚动动
scroll-y Boolean false 允许纵向滚动
upper-threshold Number 50 距顶部/左边多远时(单位 px),触发 scrolltoupper 事件
lower-threshold Number 50 距底部/右边多远时(单位 px),触发 scrolltolower 事件
scroll-top Number 0 设置纵向滚动条位置
scroll-left Number 0 设置横向滚动条位置
scroll-with-animation Boolean false 在设置滚动条位置时使用动画过渡
enable-back-to-top Boolean false 点击状态栏的时候视图会滚动到顶部
enhanced Boolean false scroll-view 组件功能增强
refresher-enabled Boolean false 开启自定义下拉刷新
scroll-anchoring Boolean false 开启滚动区域滚动锚点
refresher-default-style String 'black' 设置下拉刷新默认样式,支持 blackwhitenone,仅安卓支持
refresher-background String '#fff' 设置自定义下拉刷新背景颜色,仅安卓支持
refresher-triggered Boolean false 设置当前下拉刷新状态,true 表示已触发
paging-enabled Number false 分页滑动效果 (同时开启 enhanced 属性后生效),当值为 true 时,滚动条会停在滚动视图的尺寸的整数倍位置
show-scrollbar Number true 滚动条显隐控制 (同时开启 enhanced 属性后生效)
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
binddragstart 滑动开始事件,同时开启 enhanced 属性后生效
binddragging 滑动事件,同时开启 enhanced 属性后生效
binddragend 滑动结束事件,同时开启 enhanced 属性后生效
bindscrolltoupper 滚动到顶部/左边触发
bindscrolltolower 滚动到底部/右边触发
bindscroll 滚动时触发
bindrefresherrefresh 自定义下拉刷新被触发

注意事项

  1. 目前不支持自定义下拉刷新节点,使用 slot="refresher" 声明无效,在 React Native 环境中还是会被当作普通节点渲染出来

# swiper

滑块视图容器。

属性

属性名 类型 默认值 说明
indicator-dots Boolean false 是否显示面板指示点
indicator-color color rgba(0, 0, 0, .3) 指示点颜色
indicator-active-color color #000000 当前选中的指示点颜色
autoplay Boolean false 是否自动切换
current Number 0 当前所在滑块的 index
interval Number 5000 自动切换时间间隔
duration Number 500 滑动动画时长
circular Boolean false 是否采用衔接滑动
vertical Boolean false 滑动方向是否为纵向
previous-margin String 0 前边距,可用于露出前一项的一小部分,接受px
next-margin String 0 后边距,可用于露出后一项的一小部分,接受px
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindchange current 改变时会触发 change 事件,event.detail = {current, source}

# swiper-item

  1. 仅可放置在swiper组件中,宽高自动设置为100%。

属性

属性名 类型 默认值 说明
item-id string 该 swiper-item 的标识符

# checkbox

多选项目

属性

属性名 类型 默认值 说明
value String checkbox 标识,选中时触发 checkbox-group 的 change 事件,并携带 checkbox 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox的颜色,同css的color

# checkbox-group

多项选择器,内部由多个checkbox组成。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# radio

单选项目

属性

属性名 类型 默认值 说明
value String radio 标识,当该 radio 选中时,radio-group 的 change 事件会携带 radio 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox 的颜色,同 css 的 color

# radio-group

单项选择器,内部由多个 radio 组成

事件

事件名 说明
bindchange radio-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 radio 的 value 的数组 ] }

# label

用来改进表单组件的可用性

注意事项

  1. 当前不支持使用 for 属性找到对应 id,仅支持将控件放在该标签内,目前可以绑定的空间有:checkbox、radio、switch。

# icon

图标组件

属性

属性名 类型 默认值 说明
type String icon 的类型,有效值:success、success_no_circle、info、warn、waiting、cancel、download、search、clear
size String | Number 23 icon 的大小
color String icon 的颜色,同 css 的 color

# movable-area

movable-view的可移动区域。

注意事项

  1. movable-area不支持设置 scale-area,缩放手势生效区域仅在 movable-view 内

# movable-view

可移动的视图容器,在页面中可以拖拽滑动。movable-view 必须在 movable-area 组件中,并且必须是直接子节点,否则不能移动。

属性

属性名 类型 默认值 说明
direction String none 目前支持 all、vertical、horizontal、none|
x Number 定义x轴方向的偏移
y Number 定义y轴方向的偏移
friction Number 7 摩擦系数
disabled boolean false 是否禁用
scale boolean false 是否支持双指缩放
scale-min Number 0.1 定义缩放倍数最小值
scale-max Number 10 定义缩放倍数最大值
scale-value Number 1 定义缩放倍数,取值范围为 0.1 - 10

事件

事件名 说明
bindchange 拖动过程中触发的事件,event.detail = {x, y, source}
bindscale 缩放过程中触发的事件,event.detail = {x, y, scale}
htouchmove 初次手指触摸后移动为横向的移动时触发
vtouchmove 初次手指触摸后移动为纵向的移动时触发

# form

表单。将组件内的用户输入的switch input checkbox slider radio picker 提交。

当点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key。

事件

事件名 说明
bindsubmit 携带 form 中的数据触发 submit 事件,event.detail = {value : {'name': 'value'} }
bindreset 表单重置时会触发 reset 事件

# cover-view

视图容器。 +功能同 view 组件

# cover-image

视图容器。 +功能同 image 组件 +| bindchange | 滚动选择时触发change事件,event.detail = {value};value为数组,表示 picker-view 内的 picker-view-column 当前选择的是第几项(下标从 0 开始)|

# picker-view

嵌入页面的滚动选择器。其中只可放置 picker-view-column组件,其它节点不会显示

属性

属性名 类型 默认值 说明
value Array[number] false 数组中的数字依次表示 picker-view 内的 picker-view-column 选择的第几项(下标从 0 开始),数字大于 picker-view-column 可选项长度时,选择最后一项。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# picker-view-column

滚动选择器子项。仅可放置于picker-view中,其孩子节点的高度会自动设置成与picker-view的选中框的高度一致

# picker

从底部弹起的滚动选择器。

属性

属性名 类型 默认值 说明
mode string selector 选择器类型
disabled boolean false 是否禁用

公共事件

事件名 说明
bindcancel 取消选择时触发
bindchange 滚动选择时触发change事件,event.detail = {value};value为数组,表示 picker-view 内的 picker-view-column 当前选择的是第几项(下标从 0 开始)

# 普通选择器:mode = selector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value number 0 表示选择了 range 中的第几个(下标从 0 开始)

# 多列选择器:mode = multiSelector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value array [] 表示选择了 range 中的第几个(下标从 0 开始)
bindcolumnchange 列改变时触发

# 多列选择器:时间选择器:mode = time

属性

属性名 类型 默认值 说明
value string [] 表示选中的时间,格式为"hh:mm"
start string false 表示有效时间范围的开始,字符串格式为"hh:mm"
end string [] 表示有效时间范围的结束,字符串格式为"hh:mm"

# 多列选择器:时间选择器:mode = date

属性

属性名 类型 默认值 说明
value string 当天 表示选中的日期,格式为"YYYY-MM-DD"
start string false 表示有效日期范围的开始,字符串格式为"YYYY-MM-DD"
end string [] 表示有效日期范围的结束,字符串格式为"YYYY-MM-DD"
fields string day 有效值 year,month,day,表示选择器的粒度

# fields 有效值:

属性名 说明
year 选择器粒度为年
month 选择器粒度为月份
day 选择器粒度为天

# 省市区选择器:mode = region

属性

属性名 类型 默认值 说明
value array [] 表示选中的省市区,默认选中每一列的第一个值
custom-item string 可为每一列的顶部添加一个自定义的项
level string region 选择器层级

# level 有效值:

属性名 说明
province 选省级选择器
city 市级选择器
region 区级选择器
+ + + diff --git a/docs-vuepress/.vuepress/dist/service-worker.js b/docs-vuepress/.vuepress/dist/service-worker.js new file mode 100644 index 0000000000..3fcd8056f9 --- /dev/null +++ b/docs-vuepress/.vuepress/dist/service-worker.js @@ -0,0 +1,901 @@ +/** + * Welcome to your Workbox-powered service worker! + * + * You'll need to register this file in your web app and you should + * disable HTTP caching for this file too. + * See https://goo.gl/nhQhGp + * + * The rest of the code is auto-generated. Please don't update this file + * directly; instead, make changes to your Workbox build configuration + * and re-run your build process. + * See https://goo.gl/2aRDsh + */ + +importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); + +self.addEventListener('message', (event) => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } +}); + +/** + * The workboxSW.precacheAndRoute() method efficiently caches and responds to + * requests for URLs in the manifest. + * See https://goo.gl/S9QRab + */ +self.__precacheManifest = [ + { + "url": "404.html", + "revision": "5b00337f21832bd4573ba6c98bedacb4" + }, + { + "url": "api/ApiIndex.html", + "revision": "f5736e201b8355b58c4a3127475e6757" + }, + { + "url": "api/app-config.html", + "revision": "73b3f4c261fc6ad75418f21d0f696b62" + }, + { + "url": "api/builtIn.html", + "revision": "e854e0ad752129084b0de9d2a9b692fd" + }, + { + "url": "api/compile.html", + "revision": "5eb3d7fe5d6193c15061bcca578def13" + }, + { + "url": "api/composition-api.html", + "revision": "70dccb11a976b0321ae6c6df8cbccf83" + }, + { + "url": "api/directives.html", + "revision": "39ac7af3ed7a9131d4dad8bbda934d5e" + }, + { + "url": "api/extend.html", + "revision": "67026f94a9c9fb2b42094426f6157048" + }, + { + "url": "api/global-api.html", + "revision": "33d7c25d85ca74401645e05f2e5990f1" + }, + { + "url": "api/index.html", + "revision": "a0bb36716602184632c2781156f0256e" + }, + { + "url": "api/instance-api.html", + "revision": "2999969ec01f4b0893d81a7c810e05f7" + }, + { + "url": "api/optional-api.html", + "revision": "52cf38d179e9e86b426fe9c31b758659" + }, + { + "url": "api/reactivity-api.html", + "revision": "a580e6086de61b32a4442ff42d8e6caa" + }, + { + "url": "api/store-api.html", + "revision": "fc79ec11f5b437574ee9c0d3f0b51a36" + }, + { + "url": "articles/1.0.html", + "revision": "c7408bac5c637d07a68bd9f1e0579d47" + }, + { + "url": "articles/2.0.html", + "revision": "89411f8727853b0ad8db88d46d2f92d0" + }, + { + "url": "articles/2.7-release.html", + "revision": "36f589577d67385b01da5b910e5e4f43" + }, + { + "url": "articles/2.8-release-alter.html", + "revision": "c7c41c65d2fcc7577004ce04712d5e46" + }, + { + "url": "articles/2.8-release.html", + "revision": "c968dd0160e52f569d8a35f6978c22cb" + }, + { + "url": "articles/2.9-release-alter.html", + "revision": "842d2cf8a91c896bf46ff406a900951e" + }, + { + "url": "articles/2.9-release.html", + "revision": "7714799727658659001df91df0ab765d" + }, + { + "url": "articles/index.html", + "revision": "28e08a2249444d784e1b13e59cb8e0ba" + }, + { + "url": "articles/mpx-cli-next.html", + "revision": "9edaa671d20c5721ade80295c5487d9a" + }, + { + "url": "articles/mpx-cube-ui.html", + "revision": "339facb42ee86b3f21cbf22195f0ea7c" + }, + { + "url": "articles/mpx1.html", + "revision": "b09e8d522bf61190a9b2cf2c77b1c9e9" + }, + { + "url": "articles/mpx2.html", + "revision": "628254e250ec236cfc816e2f6dcd3098" + }, + { + "url": "articles/performance.html", + "revision": "62339e70fe8371692b301f390ece7791" + }, + { + "url": "articles/size-control.html", + "revision": "8256646d0a2da95674f4f7deab424781" + }, + { + "url": "articles/ts-derivation.html", + "revision": "15a7473ded3bb895564dce5ebb866f76" + }, + { + "url": "articles/unit-test.html", + "revision": "2b5854814775bb3f12b5f87cd3807433" + }, + { + "url": "assets/css/0.styles.f22478ca.css", + "revision": "20c20b6924c0d21c3946c0c2f73e3852" + }, + { + "url": "assets/img/search.83621669.svg", + "revision": "83621669651b9a3d4bf64d1a670ad856" + }, + { + "url": "assets/img/start-tips1.3b76ac97.png", + "revision": "3b76ac977e543ae853c79f5fb4bf648d" + }, + { + "url": "assets/img/start-tips2.7d8836f8.png", + "revision": "7d8836f8026aa7e1813ec450276d21ad" + }, + { + "url": "assets/js/1.4f7abe8f.js", + "revision": "12b0c0887ef86bb29d29e515a8254df4" + }, + { + "url": "assets/js/10.0192acce.js", + "revision": "2e489bb3c3e4db7eb021df8227a78d17" + }, + { + "url": "assets/js/100.f31ffd06.js", + "revision": "5204b3c7795a10953cdf3e4715daaeec" + }, + { + "url": "assets/js/101.283d0363.js", + "revision": "a02fdbbcd800637e2e923ae5ce3f526f" + }, + { + "url": "assets/js/102.094a0a16.js", + "revision": "c6084875d5966ca473c712a0c8eb8592" + }, + { + "url": "assets/js/103.b34f6752.js", + "revision": "385380e54f276b92c30b8c769eb3500f" + }, + { + "url": "assets/js/104.78f6358e.js", + "revision": "eeeb9573bb2e751a7efcf7b866d559a4" + }, + { + "url": "assets/js/105.eb6680fc.js", + "revision": "fbbf2d50f5f790400258587ca7d319ab" + }, + { + "url": "assets/js/106.471f03cf.js", + "revision": "3a2b07fdde55595e53ee58f20d93fb35" + }, + { + "url": "assets/js/107.63fd18fd.js", + "revision": "8e1580b610cbe287488f9dc6a17c21a0" + }, + { + "url": "assets/js/108.614260e9.js", + "revision": "960d17597536a94c9f465c1f183470ad" + }, + { + "url": "assets/js/109.3da50cc1.js", + "revision": "85bb938489ee7bdb469106adf4520d52" + }, + { + "url": "assets/js/11.d7a8d045.js", + "revision": "9bc45c1f6e30baa42b6f61119767b063" + }, + { + "url": "assets/js/110.f74ff3a3.js", + "revision": "8b75f04fbf04424cc6c55c9f2dbec4a7" + }, + { + "url": "assets/js/111.51932e54.js", + "revision": "08920579c78da19f6afbc656719be751" + }, + { + "url": "assets/js/112.da7bd3e7.js", + "revision": "fe8d576cf790411d588601fae4414b03" + }, + { + "url": "assets/js/113.caaf11a3.js", + "revision": "414f18602138c0fb69fb8e493d5659dd" + }, + { + "url": "assets/js/114.16cda7ac.js", + "revision": "a50fb0cd876585fa9a81503bfee109f9" + }, + { + "url": "assets/js/115.88124b12.js", + "revision": "3440d45dc5afd506ea94fd2159c1b733" + }, + { + "url": "assets/js/116.4a116234.js", + "revision": "77eb5727ecdf8abd544ab6180852ab0a" + }, + { + "url": "assets/js/117.e45c1b95.js", + "revision": "22697b9821bf9d90ef8542c9f525bc4f" + }, + { + "url": "assets/js/118.9cbac09e.js", + "revision": "2ee13a56a4d9163596caa0d4c599d268" + }, + { + "url": "assets/js/119.312e4c21.js", + "revision": "00e5ef4ca7025684be01fd3aefee9bdc" + }, + { + "url": "assets/js/12.4d7217be.js", + "revision": "cf2ee26c47e0f0d7ee3f2bda3d92cf40" + }, + { + "url": "assets/js/120.7a6b4035.js", + "revision": "7967e6835f248a74746ab0aa147c5177" + }, + { + "url": "assets/js/14.7377c68f.js", + "revision": "b7514f8cb2d06cdaf18d71594d346192" + }, + { + "url": "assets/js/15.6f885a83.js", + "revision": "40c60e1604dfac53cae641e598daf156" + }, + { + "url": "assets/js/16.974f551e.js", + "revision": "df88e248621a7536527b6a547143dc57" + }, + { + "url": "assets/js/17.e94e6745.js", + "revision": "afe65725c62091da4210e3c489f993f6" + }, + { + "url": "assets/js/18.b1f7a4a2.js", + "revision": "2746cf3eaba809a8792ab8e911b6e6b1" + }, + { + "url": "assets/js/19.62186e92.js", + "revision": "2e5228047eaba20f8fcc067431adb2a4" + }, + { + "url": "assets/js/2.314c65b9.js", + "revision": "b8cf0e72859eb5f0e3fb7f67d27ef340" + }, + { + "url": "assets/js/20.e9cd4de3.js", + "revision": "0045f2d19fe78531ce98e2db9aa6bdd5" + }, + { + "url": "assets/js/21.217f8fa9.js", + "revision": "bd130990daa769d2fb63d19c19c31932" + }, + { + "url": "assets/js/22.4cd3344f.js", + "revision": "db69d7654880d9f1c64373d171c787b6" + }, + { + "url": "assets/js/23.365f9a87.js", + "revision": "cd54b4361286190f19e5c3067303c425" + }, + { + "url": "assets/js/24.17777a13.js", + "revision": "eb0f29a4f257f591178f6201882b8298" + }, + { + "url": "assets/js/25.889c07d7.js", + "revision": "5a964529d1992e5ecbe2f545980ee947" + }, + { + "url": "assets/js/26.3aa9f276.js", + "revision": "03b8f1eb7e4dbebe65672c17914db693" + }, + { + "url": "assets/js/27.1a35170a.js", + "revision": "7d5ed813bbe766a1748a0f21ee5569d8" + }, + { + "url": "assets/js/28.3670fa46.js", + "revision": "6e3c125eaf27c095cad1d77637e4e80f" + }, + { + "url": "assets/js/29.a911e510.js", + "revision": "363f3beef26a3abf2161c7e1278cfcae" + }, + { + "url": "assets/js/3.145e69cd.js", + "revision": "1a153ade78a92885e52f897ec87c1c3e" + }, + { + "url": "assets/js/30.04d1a7ea.js", + "revision": "578f318e24529539c775d12d5436fca8" + }, + { + "url": "assets/js/31.ec06bf2f.js", + "revision": "00a3a86bdd7caad32fa0eec96cf4698a" + }, + { + "url": "assets/js/32.f29d2cd1.js", + "revision": "7d811f0e993fbee6e7c7d6800f830839" + }, + { + "url": "assets/js/33.936d018e.js", + "revision": "2714336439729f02460fbcd2af4a7c5c" + }, + { + "url": "assets/js/34.f494d685.js", + "revision": "3f1099fca4eff5ef8e62348425a3e7ff" + }, + { + "url": "assets/js/35.c3ed1ff7.js", + "revision": "8f836b9e9e2107c4c281203b190ef9a8" + }, + { + "url": "assets/js/36.d2c9afa9.js", + "revision": "fe308138cfddd6e5819569d6609a7672" + }, + { + "url": "assets/js/37.4e1eee2c.js", + "revision": "0efac40e5dd0e3fc8e94f0b8cda4f0b2" + }, + { + "url": "assets/js/38.7826170e.js", + "revision": "8e5e62243cab1c2f356d0f77ffac4b04" + }, + { + "url": "assets/js/39.094ffa40.js", + "revision": "4342737cae50042277b76721d79315d2" + }, + { + "url": "assets/js/4.9489102c.js", + "revision": "c5f4723242bd00da0724a4737e1cb142" + }, + { + "url": "assets/js/40.10d90636.js", + "revision": "76033b573e4da1b2cd491c8794b35af7" + }, + { + "url": "assets/js/41.8351af86.js", + "revision": "16e3ded52f241dfcedb215d3e784ffd2" + }, + { + "url": "assets/js/42.53972f6c.js", + "revision": "f59a9f585a6b06d80cebcdb81772acd0" + }, + { + "url": "assets/js/43.e2b432ea.js", + "revision": "f0fb7f9acf7bfd42c8777fa0532a4f7c" + }, + { + "url": "assets/js/44.e4242a1f.js", + "revision": "397c66f6b7866718983234d014f94e60" + }, + { + "url": "assets/js/45.392413aa.js", + "revision": "51fcb57964dd90d24c02593411a478cd" + }, + { + "url": "assets/js/46.22f9a9c6.js", + "revision": "d8f80568057ee4169e8f145ed42a28d8" + }, + { + "url": "assets/js/47.2bd2cecc.js", + "revision": "09684550aea02b1058ea119724a2fed1" + }, + { + "url": "assets/js/48.55792a30.js", + "revision": "6900523d21b183f5a8aae5e2799674cc" + }, + { + "url": "assets/js/49.cde94a79.js", + "revision": "3c992f7d6d21537ae32091b611215346" + }, + { + "url": "assets/js/5.12d049b6.js", + "revision": "d489c45818dd2315c2c97a3437acd6cf" + }, + { + "url": "assets/js/50.42e34e52.js", + "revision": "1831c8d5de17eeed779edb5f28385671" + }, + { + "url": "assets/js/51.90dfa84e.js", + "revision": "531ac61e50a863e5bb409b2f893c4240" + }, + { + "url": "assets/js/52.10bb0251.js", + "revision": "79eb24907253da0d7120ae9dcf34c73f" + }, + { + "url": "assets/js/53.7207ffc5.js", + "revision": "53164733a3c1e6f29a4ca6719591b70b" + }, + { + "url": "assets/js/54.af3d3dcd.js", + "revision": "f175b1f13236f10b690d5e86b9bb368d" + }, + { + "url": "assets/js/55.2d6ae2e1.js", + "revision": "eb8dd3e8ff4c57ed143c9778ec15bd4b" + }, + { + "url": "assets/js/56.9aec9a9e.js", + "revision": "5ebf6c52558f0709c076b202a55b9fa3" + }, + { + "url": "assets/js/57.9d9ddcbd.js", + "revision": "99117ec556700247d528c7acd29c203b" + }, + { + "url": "assets/js/58.50eb3b91.js", + "revision": "e7f565c15f3cab12bf1eedf0659fd523" + }, + { + "url": "assets/js/59.27106fbb.js", + "revision": "f485b2d1b0a56c26176ea8f44f9e2738" + }, + { + "url": "assets/js/6.6ea91b28.js", + "revision": "5fac1ead0dd12855a5d8976740456131" + }, + { + "url": "assets/js/60.a47e5279.js", + "revision": "f6f5d7dc973878332b5b1d0157f92135" + }, + { + "url": "assets/js/61.74ebd7d9.js", + "revision": "ae27832c8fa41953cc04725e0d5001d0" + }, + { + "url": "assets/js/62.4bf212f9.js", + "revision": "2ae416136507b54b6c19f00f94ea9dd4" + }, + { + "url": "assets/js/63.b54aca3e.js", + "revision": "80860c2eb8c0374ffd19206c3653a451" + }, + { + "url": "assets/js/64.399e72b8.js", + "revision": "d9f80db9175e32d06a3ea1077135f0e4" + }, + { + "url": "assets/js/65.a8c627d3.js", + "revision": "779b4c30258b0bd6aad23a1436bc1b92" + }, + { + "url": "assets/js/66.7f19cc4b.js", + "revision": "2e79a49876a6073bd459a1575199269d" + }, + { + "url": "assets/js/67.e57db97e.js", + "revision": "78a67eab42f946943b9651d089a4ef56" + }, + { + "url": "assets/js/68.ebe4213c.js", + "revision": "93e3246379979219b4af948dba1bc71a" + }, + { + "url": "assets/js/69.2d309866.js", + "revision": "f5dc40ff8ea229a2fa6ac697fcf42475" + }, + { + "url": "assets/js/7.01e4e942.js", + "revision": "e4797b632e362ecd096a64375e7003ae" + }, + { + "url": "assets/js/70.60ae83e6.js", + "revision": "acba0df2ef73d360ec4686677942d0a4" + }, + { + "url": "assets/js/71.188d7bca.js", + "revision": "cf49e628f26e54631f9a3fcb3f909fbc" + }, + { + "url": "assets/js/72.3f7bfd0d.js", + "revision": "8b3a18399033b6fbc07696b31226998a" + }, + { + "url": "assets/js/73.171571e4.js", + "revision": "4152daed0ad6a65b39b3f20c0ba648ca" + }, + { + "url": "assets/js/74.3d5cfaba.js", + "revision": "0fbea56200387d87caa90abc9f1c807d" + }, + { + "url": "assets/js/75.093d716c.js", + "revision": "0abd2acf22923e75799377e678b91077" + }, + { + "url": "assets/js/76.50d84c61.js", + "revision": "c299d111edbf4d3c46c44c9374a3e4f3" + }, + { + "url": "assets/js/77.7bc0f013.js", + "revision": "6d86d8510f2fb24917c56fcd00333e12" + }, + { + "url": "assets/js/78.e2ded2d4.js", + "revision": "4058b878e45344934acd21b46e02795f" + }, + { + "url": "assets/js/79.f7eeb76b.js", + "revision": "8e9491e599a437e2d5affbbfbd58559c" + }, + { + "url": "assets/js/8.ba7f6ace.js", + "revision": "3db41c27492ef5d04cfe98f3ba38c493" + }, + { + "url": "assets/js/80.41418749.js", + "revision": "76617f3118e34b77c5ce0cddcebd6ec4" + }, + { + "url": "assets/js/81.16e2cc83.js", + "revision": "2ad3cec1add1ede3456c3c89a27f2a07" + }, + { + "url": "assets/js/82.56c0d7c1.js", + "revision": "5ef8ed46677490c1264882fe0c1eade8" + }, + { + "url": "assets/js/83.ea65d6b1.js", + "revision": "2629982f8da068c64b96af5cb7cc0170" + }, + { + "url": "assets/js/84.d50d3d09.js", + "revision": "cdc0e25e4f147f635e4c04c409f99935" + }, + { + "url": "assets/js/85.1eed1486.js", + "revision": "99a6b518c198712eba4adb916001461c" + }, + { + "url": "assets/js/86.1ab04b8d.js", + "revision": "9218ab6fe0cc3e0d0cc9465ae1f04957" + }, + { + "url": "assets/js/87.392384e7.js", + "revision": "b247ed9b2da1e82b477313aa886db306" + }, + { + "url": "assets/js/88.a95133cd.js", + "revision": "4036de5ef3a7b6e9a318942ba6faf560" + }, + { + "url": "assets/js/89.213efaae.js", + "revision": "fa74c388f721c266e3ae5f6edbb4bd85" + }, + { + "url": "assets/js/9.26319460.js", + "revision": "b5a027c58f63f184432e7851068830a1" + }, + { + "url": "assets/js/90.ef03688f.js", + "revision": "58920a350be92626cfd2cf15e640fd3d" + }, + { + "url": "assets/js/91.43930d52.js", + "revision": "bff38c73ab05c14ddc5da88e4ba036e1" + }, + { + "url": "assets/js/92.ae5e9611.js", + "revision": "b3faf9fd83b537307278864b0e5464ec" + }, + { + "url": "assets/js/93.3cdab647.js", + "revision": "f66789e02a5ffd1afc485242e108c860" + }, + { + "url": "assets/js/94.72df7b53.js", + "revision": "bd73044a9f346ca23782d615da42d4a4" + }, + { + "url": "assets/js/95.c8e36604.js", + "revision": "e40ad2a5dfe1e25c058962cf86660287" + }, + { + "url": "assets/js/96.1ebdc00d.js", + "revision": "2c67b7841bd6c48f9af68c6d95c8ffe4" + }, + { + "url": "assets/js/97.fa1ed9e7.js", + "revision": "7e054747dd126fecc7e9d670794002b1" + }, + { + "url": "assets/js/98.939cb6ef.js", + "revision": "56b3178533a666894baddadd8ab9bc06" + }, + { + "url": "assets/js/99.96c5a70e.js", + "revision": "0cbc93b6dce3419f25775487b0d1407a" + }, + { + "url": "assets/js/app.84c88b57.js", + "revision": "1067055bba375603bb5781924201a8a5" + }, + { + "url": "baidu_verify_codeva-GYcT5ujCTB.html", + "revision": "05e79aba57b6dbeeb58f6aadc23f0074" + }, + { + "url": "desc.html", + "revision": "f46e5deaa0b32c74a5465f7e57401fcd" + }, + { + "url": "guide/advance/ability-compatible.html", + "revision": "3b8afd343db4d74cc203261e4141a4af" + }, + { + "url": "guide/advance/async-subpackage.html", + "revision": "bc5eb1b9ec36b9d874620ec322f4d323" + }, + { + "url": "guide/advance/custom-output-path.html", + "revision": "72a926d622c483691ffc39f9aa346f2d" + }, + { + "url": "guide/advance/dll-plugin.html", + "revision": "e8d95670c94231a0a5d0c9ac40d23795" + }, + { + "url": "guide/advance/i18n.html", + "revision": "f81fab30016db6683dc67d7f87ca65dd" + }, + { + "url": "guide/advance/image-process.html", + "revision": "4c139d9d8d72efe3136d29bb862e4fe4" + }, + { + "url": "guide/advance/mixin.html", + "revision": "5989556c2b6a6285617fcb2775e0b403" + }, + { + "url": "guide/advance/npm.html", + "revision": "acc08a0cd161d56e73c5ea141affc99c" + }, + { + "url": "guide/advance/pinia.html", + "revision": "2359fa01373e8bfb92e81ff3b275d5a8" + }, + { + "url": "guide/advance/platform.html", + "revision": "83cd97f6ec8954c086b2b0361fc608c8" + }, + { + "url": "guide/advance/plugin.html", + "revision": "7aecc3bfede3b13de6afc7a20e25e8af" + }, + { + "url": "guide/advance/progressive.html", + "revision": "13fd4ea947dd5e9c0b3c4cb2ffff6ef4" + }, + { + "url": "guide/advance/provide-inject.html", + "revision": "94f05629f5f4c9b7c9808d87e957eea8" + }, + { + "url": "guide/advance/resource-resolve.html", + "revision": "cf43ecea0f34b0087d6ea2310d68d903" + }, + { + "url": "guide/advance/size-report.html", + "revision": "e9b31045eeb57a567487eb0cec8c1a55" + }, + { + "url": "guide/advance/ssr.html", + "revision": "a9a2ab6192f90ecf01f14e5e23ca3afc" + }, + { + "url": "guide/advance/store.html", + "revision": "21fef840c6de0b5e61c831a76077e497" + }, + { + "url": "guide/advance/subpackage.html", + "revision": "b43e2e4c902b2e4d10b58d5b8406b880" + }, + { + "url": "guide/advance/utility-first-css.html", + "revision": "203f30c86d031caab57667d23d874a8c" + }, + { + "url": "guide/basic/class-style-binding.html", + "revision": "b8f5d12016d754c0504aab2e06a8dda2" + }, + { + "url": "guide/basic/component.html", + "revision": "c8e7cf6524386db839da0964dde4530a" + }, + { + "url": "guide/basic/conditional-render.html", + "revision": "458a0e675d9fc7e4f4ed3a3d7cb10d2f" + }, + { + "url": "guide/basic/css.html", + "revision": "a60840e3e0ab988469dd9701b8d9740f" + }, + { + "url": "guide/basic/event.html", + "revision": "34a29012d67135507b22853092744b7a" + }, + { + "url": "guide/basic/ide.html", + "revision": "84d9ce263667bac51aa966600d81f452" + }, + { + "url": "guide/basic/intro.html", + "revision": "3564335cfab7690662f7e6981e28384c" + }, + { + "url": "guide/basic/list-render.html", + "revision": "81564ae12132bfdeee22556dc29cf2aa" + }, + { + "url": "guide/basic/option-chain.html", + "revision": "b8af0aca729f041f5d32e1df83383f8c" + }, + { + "url": "guide/basic/reactive.html", + "revision": "fa1199d698f37cd18ab5e1cbd1fcde85" + }, + { + "url": "guide/basic/refs.html", + "revision": "1ad3afc97da4858d06e82df08d4715ef" + }, + { + "url": "guide/basic/single-file.html", + "revision": "82003549b10795b0c603a66854e2a89f" + }, + { + "url": "guide/basic/start.html", + "revision": "06f28accbba57fc3d2696c3303a17ca0" + }, + { + "url": "guide/basic/template.html", + "revision": "ac4adffc987166bd6b014e4874f9807b" + }, + { + "url": "guide/basic/two-way-binding.html", + "revision": "9f94591106431eae092285cc63bf73b3" + }, + { + "url": "guide/composition-api/composition-api.html", + "revision": "f372f2e4334ed4580d06f8fdfd741e9a" + }, + { + "url": "guide/composition-api/reactive-api.html", + "revision": "a8f594693bbd5ae4af7644ea13f697fb" + }, + { + "url": "guide/extend/api-proxy.html", + "revision": "13330ecd6678b2e40e034d8ad425029c" + }, + { + "url": "guide/extend/fetch.html", + "revision": "6a54e8095f85d420cd97707a5d3684c7" + }, + { + "url": "guide/extend/index.html", + "revision": "258dede7e9407b44d9998bf7fe1fd3b0" + }, + { + "url": "guide/extend/mock.html", + "revision": "2951d26e9b525fb4c94afabf4ac9924f" + }, + { + "url": "guide/migrate/2.7.html", + "revision": "fc84b08b7d29082e1dbb1999d900a704" + }, + { + "url": "guide/migrate/2.8.html", + "revision": "28587490b791a1b8ba35c7d92f12a6de" + }, + { + "url": "guide/migrate/2.9.html", + "revision": "658e207bc34b8a18cb8ed68442c62712" + }, + { + "url": "guide/migrate/mpx-cli-3.html", + "revision": "f91fdb6ac9c54c8c865d19637c7ee591" + }, + { + "url": "guide/platform/index.html", + "revision": "b0af12d7eaf7882a8aea01d8202c4d02" + }, + { + "url": "guide/platform/miniprogram.html", + "revision": "2b5ed743d1a4eeab70f2b4f8051eefba" + }, + { + "url": "guide/platform/rn.html", + "revision": "cb10026541b586deac1fc9c234328e78" + }, + { + "url": "guide/platform/web.html", + "revision": "e99c298705c937e4965c229cd7e4a2b9" + }, + { + "url": "guide/tool/e2e-test.html", + "revision": "858693cd05b55bc582f420643ef9e514" + }, + { + "url": "guide/tool/ts.html", + "revision": "a1b3d19233cad159d0bd946a2ea3eb41" + }, + { + "url": "guide/tool/unit-test.html", + "revision": "bf791feac87c5709dfb9e3fd326c99fd" + }, + { + "url": "guide/understand/compile.html", + "revision": "6e9a16d929784bcdbe34ca2564c47a18" + }, + { + "url": "guide/understand/runtime.html", + "revision": "c7008c7a29fefb544fb600bd71677b7a" + }, + { + "url": "index.html", + "revision": "51c2b45fb4b6fac444845411d55acbca" + }, + { + "url": "logo.png", + "revision": "b362e51deb26ea4ff1d0daa6da1e7c44" + }, + { + "url": "rn-api.html", + "revision": "41b9df161419f392a9a40e6b7b162ae3" + }, + { + "url": "rn-component.html", + "revision": "e0533defbccf789f8f8e2c15ae3ebd22" + }, + { + "url": "rn-style.html", + "revision": "163003bbcbe6b7665c2f420daee35296" + }, + { + "url": "rn-template.html", + "revision": "71cca96a9f67d8ddb84cb284342b8329" + } +].concat(self.__precacheManifest || []); +workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); +addEventListener('message', event => { + const replyPort = event.ports[0] + const message = event.data + if (replyPort && message && message.type === 'skip-waiting') { + event.waitUntil( + self.skipWaiting().then( + () => replyPort.postMessage({ error: null }), + error => replyPort.postMessage({ error }) + ) + ) + } +}) diff --git a/packages/core/src/platform/patch/getDefaultOptions.ios.js b/packages/core/src/platform/patch/getDefaultOptions.ios.js index 42ba425801..73c4f70d4b 100644 --- a/packages/core/src/platform/patch/getDefaultOptions.ios.js +++ b/packages/core/src/platform/patch/getDefaultOptions.ios.js @@ -46,15 +46,19 @@ function createEffect (proxy, components) { if (tagName === 'block') return Fragment // 从父组件传递的 genericComponents 中获取 moduleId if (proxy.target.__props.genericComponents && proxy.target.__props.generic) { - const genericKey = Object.keys(proxy.target.__props.generic)[0] - const actualComponentName = proxy.target.__props.generic[genericKey] - - if (tagName === actualComponentName) { - // 通过 moduleId 从全局获取组件定义 - const moduleId = proxy.target.__props.genericComponents[actualComponentName] - return global.__mpxOptionsMap[moduleId] + const genericKeys = Object.keys(proxy.target.__props.generic) + for (const genericKey of genericKeys) { + const actualComponentName = proxy.target.__props.generic[genericKey] + if (tagName === actualComponentName) { + // 通过 moduleId 从全局获取组件定义 + const moduleId = + proxy.target.__props.genericComponents[actualComponentName] + if (moduleId) { + return global.__mpxOptionsMap[moduleId] + } + } + } } - } return components[tagName] || getByPath(ReactNative, tagName) } const innerCreateElement = (type, ...rest) => { diff --git a/packages/webpack-plugin/lib/template-compiler/compiler.js b/packages/webpack-plugin/lib/template-compiler/compiler.js index 5d8145d5d4..a2f5c7165d 100644 --- a/packages/webpack-plugin/lib/template-compiler/compiler.js +++ b/packages/webpack-plugin/lib/template-compiler/compiler.js @@ -2474,30 +2474,41 @@ function processComponentGenericsReact (el, options) { } // 处理父组件中使用 generic 组件的情况 - const attrsToAdd = [] + const genericConfig = {} // 用一个对象收集所有的 generic 配置 + const genericComponentsConfig = {} // 收集所有组件的 moduleId el.attrsList.forEach(attr => { const match = attr.name.match(genericRE) if (match) { const key = match[1] const componentName = attr.value - // 传递 generic 配置 - attrsToAdd.push({ - name: 'generic', - value: { [key]: componentName } - }) + // 将每个 generic 配置添加到同一个对象中 + genericConfig[key] = componentName // 从 usingComponentsInfo 中获取组件的 moduleId if (usingComponentsInfo[componentName]) { const { mid } = usingComponentsInfo[componentName] - // 传递组件的 moduleId 给子组件 - attrsToAdd.push({ - name: 'genericComponents', - value: { [componentName]: mid } - }) + // 收集组件的 moduleId + genericComponentsConfig[componentName] = mid } } }) - el.attrsList = el.attrsList.concat(attrsToAdd) + // 只有在有 generic 配置时才添加属性 + if (Object.keys(genericConfig).length) { + const attrsToAdd = [ + { + name: 'generic', + value: genericConfig + } + ] + // 只有在有 moduleId 时才添加 genericComponents 属性 + if (Object.keys(genericComponentsConfig).length) { + attrsToAdd.push({ + name: 'genericComponents', + value: genericComponentsConfig + }) + } + el.attrsList = el.attrsList.concat(attrsToAdd) + } } function processShow (el, options, root) { From 1243a2ec9b3287e277320d67a25d830b1ec78e69 Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Mon, 10 Feb 2025 20:45:18 +0800 Subject: [PATCH 012/126] =?UTF-8?q?chore:=20=E5=88=A0=E9=99=A4vuepress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs-vuepress/.vuepress/dist/404.html | 25 - .../.vuepress/dist/api/ApiIndex.html | 23 - .../.vuepress/dist/api/app-config.html | 116 -- docs-vuepress/.vuepress/dist/api/builtIn.html | 56 - docs-vuepress/.vuepress/dist/api/compile.html | 1214 ----------------- .../.vuepress/dist/api/composition-api.html | 117 -- .../.vuepress/dist/api/directives.html | 434 ------ docs-vuepress/.vuepress/dist/api/extend.html | 365 ----- .../.vuepress/dist/api/global-api.html | 253 ---- docs-vuepress/.vuepress/dist/api/index.html | 43 - .../.vuepress/dist/api/instance-api.html | 265 ---- .../.vuepress/dist/api/optional-api.html | 51 - .../.vuepress/dist/api/reactivity-api.html | 544 -------- .../.vuepress/dist/api/store-api.html | 229 ---- .../.vuepress/dist/articles/1.0.html | 54 - .../.vuepress/dist/articles/2.0.html | 58 - .../.vuepress/dist/articles/2.7-release.html | 63 - .../dist/articles/2.8-release-alter.html | 174 --- .../.vuepress/dist/articles/2.8-release.html | 182 --- .../dist/articles/2.9-release-alter.html | 277 ---- .../.vuepress/dist/articles/2.9-release.html | 285 ---- .../.vuepress/dist/articles/index.html | 43 - .../.vuepress/dist/articles/mpx-cli-next.html | 137 -- .../.vuepress/dist/articles/mpx-cube-ui.html | 56 - .../.vuepress/dist/articles/mpx1.html | 55 - .../.vuepress/dist/articles/mpx2.html | 740 ---------- .../.vuepress/dist/articles/performance.html | 60 - .../.vuepress/dist/articles/size-control.html | 51 - .../dist/articles/ts-derivation.html | 273 ---- .../.vuepress/dist/articles/unit-test.html | 461 ------- .../dist/assets/css/0.styles.f22478ca.css | 3 - .../dist/assets/img/search.83621669.svg | 1 - .../dist/assets/img/start-tips1.3b76ac97.png | Bin 39681 -> 0 bytes .../dist/assets/img/start-tips2.7d8836f8.png | Bin 51539 -> 0 bytes .../.vuepress/dist/assets/js/1.4f7abe8f.js | 1 - .../.vuepress/dist/assets/js/10.0192acce.js | 1 - .../.vuepress/dist/assets/js/100.f31ffd06.js | 1 - .../.vuepress/dist/assets/js/101.283d0363.js | 1 - .../.vuepress/dist/assets/js/102.094a0a16.js | 1 - .../.vuepress/dist/assets/js/103.b34f6752.js | 1 - .../.vuepress/dist/assets/js/104.78f6358e.js | 1 - .../.vuepress/dist/assets/js/105.eb6680fc.js | 1 - .../.vuepress/dist/assets/js/106.471f03cf.js | 1 - .../.vuepress/dist/assets/js/107.63fd18fd.js | 1 - .../.vuepress/dist/assets/js/108.614260e9.js | 1 - .../.vuepress/dist/assets/js/109.3da50cc1.js | 1 - .../.vuepress/dist/assets/js/11.d7a8d045.js | 1 - .../.vuepress/dist/assets/js/110.f74ff3a3.js | 1 - .../.vuepress/dist/assets/js/111.51932e54.js | 1 - .../.vuepress/dist/assets/js/112.da7bd3e7.js | 1 - .../.vuepress/dist/assets/js/113.caaf11a3.js | 1 - .../.vuepress/dist/assets/js/114.16cda7ac.js | 1 - .../.vuepress/dist/assets/js/115.88124b12.js | 1 - .../.vuepress/dist/assets/js/116.4a116234.js | 1 - .../.vuepress/dist/assets/js/117.e45c1b95.js | 1 - .../.vuepress/dist/assets/js/118.9cbac09e.js | 1 - .../.vuepress/dist/assets/js/119.312e4c21.js | 1 - .../.vuepress/dist/assets/js/12.4d7217be.js | 1 - .../.vuepress/dist/assets/js/120.7a6b4035.js | 1 - .../.vuepress/dist/assets/js/14.7377c68f.js | 1 - .../.vuepress/dist/assets/js/15.6f885a83.js | 1 - .../.vuepress/dist/assets/js/16.974f551e.js | 1 - .../.vuepress/dist/assets/js/17.e94e6745.js | 1 - .../.vuepress/dist/assets/js/18.b1f7a4a2.js | 1 - .../.vuepress/dist/assets/js/19.62186e92.js | 1 - .../.vuepress/dist/assets/js/2.314c65b9.js | 1 - .../.vuepress/dist/assets/js/20.e9cd4de3.js | 1 - .../.vuepress/dist/assets/js/21.217f8fa9.js | 1 - .../.vuepress/dist/assets/js/22.4cd3344f.js | 1 - .../.vuepress/dist/assets/js/23.365f9a87.js | 1 - .../.vuepress/dist/assets/js/24.17777a13.js | 1 - .../.vuepress/dist/assets/js/25.889c07d7.js | 1 - .../.vuepress/dist/assets/js/26.3aa9f276.js | 1 - .../.vuepress/dist/assets/js/27.1a35170a.js | 1 - .../.vuepress/dist/assets/js/28.3670fa46.js | 1 - .../.vuepress/dist/assets/js/29.a911e510.js | 1 - .../.vuepress/dist/assets/js/3.145e69cd.js | 1 - .../.vuepress/dist/assets/js/30.04d1a7ea.js | 1 - .../.vuepress/dist/assets/js/31.ec06bf2f.js | 1 - .../.vuepress/dist/assets/js/32.f29d2cd1.js | 1 - .../.vuepress/dist/assets/js/33.936d018e.js | 1 - .../.vuepress/dist/assets/js/34.f494d685.js | 1 - .../.vuepress/dist/assets/js/35.c3ed1ff7.js | 1 - .../.vuepress/dist/assets/js/36.d2c9afa9.js | 1 - .../.vuepress/dist/assets/js/37.4e1eee2c.js | 1 - .../.vuepress/dist/assets/js/38.7826170e.js | 1 - .../.vuepress/dist/assets/js/39.094ffa40.js | 1 - .../.vuepress/dist/assets/js/4.9489102c.js | 1 - .../.vuepress/dist/assets/js/40.10d90636.js | 1 - .../.vuepress/dist/assets/js/41.8351af86.js | 1 - .../.vuepress/dist/assets/js/42.53972f6c.js | 1 - .../.vuepress/dist/assets/js/43.e2b432ea.js | 1 - .../.vuepress/dist/assets/js/44.e4242a1f.js | 1 - .../.vuepress/dist/assets/js/45.392413aa.js | 1 - .../.vuepress/dist/assets/js/46.22f9a9c6.js | 1 - .../.vuepress/dist/assets/js/47.2bd2cecc.js | 1 - .../.vuepress/dist/assets/js/48.55792a30.js | 1 - .../.vuepress/dist/assets/js/49.cde94a79.js | 1 - .../.vuepress/dist/assets/js/5.12d049b6.js | 1 - .../.vuepress/dist/assets/js/50.42e34e52.js | 1 - .../.vuepress/dist/assets/js/51.90dfa84e.js | 1 - .../.vuepress/dist/assets/js/52.10bb0251.js | 1 - .../.vuepress/dist/assets/js/53.7207ffc5.js | 1 - .../.vuepress/dist/assets/js/54.af3d3dcd.js | 1 - .../.vuepress/dist/assets/js/55.2d6ae2e1.js | 1 - .../.vuepress/dist/assets/js/56.9aec9a9e.js | 1 - .../.vuepress/dist/assets/js/57.9d9ddcbd.js | 1 - .../.vuepress/dist/assets/js/58.50eb3b91.js | 1 - .../.vuepress/dist/assets/js/59.27106fbb.js | 1 - .../.vuepress/dist/assets/js/6.6ea91b28.js | 1 - .../.vuepress/dist/assets/js/60.a47e5279.js | 1 - .../.vuepress/dist/assets/js/61.74ebd7d9.js | 1 - .../.vuepress/dist/assets/js/62.4bf212f9.js | 1 - .../.vuepress/dist/assets/js/63.b54aca3e.js | 1 - .../.vuepress/dist/assets/js/64.399e72b8.js | 1 - .../.vuepress/dist/assets/js/65.a8c627d3.js | 1 - .../.vuepress/dist/assets/js/66.7f19cc4b.js | 1 - .../.vuepress/dist/assets/js/67.e57db97e.js | 1 - .../.vuepress/dist/assets/js/68.ebe4213c.js | 1 - .../.vuepress/dist/assets/js/69.2d309866.js | 1 - .../.vuepress/dist/assets/js/7.01e4e942.js | 1 - .../.vuepress/dist/assets/js/70.60ae83e6.js | 1 - .../.vuepress/dist/assets/js/71.188d7bca.js | 1 - .../.vuepress/dist/assets/js/72.3f7bfd0d.js | 1 - .../.vuepress/dist/assets/js/73.171571e4.js | 1 - .../.vuepress/dist/assets/js/74.3d5cfaba.js | 1 - .../.vuepress/dist/assets/js/75.093d716c.js | 1 - .../.vuepress/dist/assets/js/76.50d84c61.js | 1 - .../.vuepress/dist/assets/js/77.7bc0f013.js | 1 - .../.vuepress/dist/assets/js/78.e2ded2d4.js | 1 - .../.vuepress/dist/assets/js/79.f7eeb76b.js | 1 - .../.vuepress/dist/assets/js/8.ba7f6ace.js | 1 - .../.vuepress/dist/assets/js/80.41418749.js | 1 - .../.vuepress/dist/assets/js/81.16e2cc83.js | 1 - .../.vuepress/dist/assets/js/82.56c0d7c1.js | 1 - .../.vuepress/dist/assets/js/83.ea65d6b1.js | 1 - .../.vuepress/dist/assets/js/84.d50d3d09.js | 1 - .../.vuepress/dist/assets/js/85.1eed1486.js | 1 - .../.vuepress/dist/assets/js/86.1ab04b8d.js | 1 - .../.vuepress/dist/assets/js/87.392384e7.js | 1 - .../.vuepress/dist/assets/js/88.a95133cd.js | 1 - .../.vuepress/dist/assets/js/89.213efaae.js | 1 - .../.vuepress/dist/assets/js/9.26319460.js | 1 - .../.vuepress/dist/assets/js/90.ef03688f.js | 1 - .../.vuepress/dist/assets/js/91.43930d52.js | 1 - .../.vuepress/dist/assets/js/92.ae5e9611.js | 1 - .../.vuepress/dist/assets/js/93.3cdab647.js | 1 - .../.vuepress/dist/assets/js/94.72df7b53.js | 1 - .../.vuepress/dist/assets/js/95.c8e36604.js | 1 - .../.vuepress/dist/assets/js/96.1ebdc00d.js | 1 - .../.vuepress/dist/assets/js/97.fa1ed9e7.js | 1 - .../.vuepress/dist/assets/js/98.939cb6ef.js | 1 - .../.vuepress/dist/assets/js/99.96c5a70e.js | 1 - .../.vuepress/dist/assets/js/app.84c88b57.js | 18 - .../dist/baidu_verify_codeva-GYcT5ujCTB.html | 1 - docs-vuepress/.vuepress/dist/desc.html | 49 - docs-vuepress/.vuepress/dist/favicon.ico | Bin 9662 -> 0 bytes .../guide/advance/ability-compatible.html | 87 -- .../dist/guide/advance/async-subpackage.html | 110 -- .../guide/advance/custom-output-path.html | 101 -- .../dist/guide/advance/dll-plugin.html | 126 -- .../.vuepress/dist/guide/advance/i18n.html | 276 ---- .../dist/guide/advance/image-process.html | 175 --- .../.vuepress/dist/guide/advance/mixin.html | 115 -- .../.vuepress/dist/guide/advance/npm.html | 144 -- .../.vuepress/dist/guide/advance/pinia.html | 191 --- .../dist/guide/advance/platform.html | 300 ---- .../.vuepress/dist/guide/advance/plugin.html | 77 -- .../dist/guide/advance/progressive.html | 135 -- .../dist/guide/advance/provide-inject.html | 200 --- .../dist/guide/advance/resource-resolve.html | 88 -- .../dist/guide/advance/size-report.html | 188 --- .../.vuepress/dist/guide/advance/ssr.html | 190 --- .../.vuepress/dist/guide/advance/store.html | 599 -------- .../dist/guide/advance/subpackage.html | 264 ---- .../dist/guide/advance/utility-first-css.html | 139 -- .../dist/guide/basic/class-style-binding.html | 131 -- .../.vuepress/dist/guide/basic/component.html | 117 -- .../dist/guide/basic/conditional-render.html | 72 - .../.vuepress/dist/guide/basic/css.html | 173 --- .../.vuepress/dist/guide/basic/event.html | 90 -- .../.vuepress/dist/guide/basic/ide.html | 67 - .../.vuepress/dist/guide/basic/intro.html | 51 - .../dist/guide/basic/list-render.html | 144 -- .../dist/guide/basic/option-chain.html | 76 -- .../.vuepress/dist/guide/basic/reactive.html | 111 -- .../.vuepress/dist/guide/basic/refs.html | 106 -- .../dist/guide/basic/single-file.html | 77 -- .../.vuepress/dist/guide/basic/start.html | 132 -- .../.vuepress/dist/guide/basic/template.html | 137 -- .../dist/guide/basic/two-way-binding.html | 81 -- .../composition-api/composition-api.html | 527 ------- .../guide/composition-api/reactive-api.html | 386 ------ .../dist/guide/extend/api-proxy.html | 100 -- .../.vuepress/dist/guide/extend/fetch.html | 139 -- .../.vuepress/dist/guide/extend/index.html | 62 - .../.vuepress/dist/guide/extend/mock.html | 177 --- .../.vuepress/dist/guide/migrate/2.7.html | 172 --- .../.vuepress/dist/guide/migrate/2.8.html | 85 -- .../.vuepress/dist/guide/migrate/2.9.html | 76 -- .../dist/guide/migrate/mpx-cli-3.html | 97 -- .../.vuepress/dist/guide/platform/index.html | 225 --- .../dist/guide/platform/miniprogram.html | 43 - .../.vuepress/dist/guide/platform/rn.html | 198 --- .../.vuepress/dist/guide/platform/web.html | 43 - .../.vuepress/dist/guide/tool/e2e-test.html | 297 ---- .../.vuepress/dist/guide/tool/ts.html | 164 --- .../.vuepress/dist/guide/tool/unit-test.html | 149 -- .../dist/guide/understand/compile.html | 52 - .../dist/guide/understand/runtime.html | 51 - docs-vuepress/.vuepress/dist/index.html | 55 - docs-vuepress/.vuepress/dist/logo.png | Bin 13635 -> 0 bytes .../.vuepress/dist/manifest.webmanifest | 15 - docs-vuepress/.vuepress/dist/rn-api.html | 65 - .../.vuepress/dist/rn-component.html | 44 - docs-vuepress/.vuepress/dist/rn-style.html | 331 ----- docs-vuepress/.vuepress/dist/rn-template.html | 104 -- .../.vuepress/dist/service-worker.js | 901 ------------ 218 files changed, 16523 deletions(-) delete mode 100644 docs-vuepress/.vuepress/dist/404.html delete mode 100644 docs-vuepress/.vuepress/dist/api/ApiIndex.html delete mode 100644 docs-vuepress/.vuepress/dist/api/app-config.html delete mode 100644 docs-vuepress/.vuepress/dist/api/builtIn.html delete mode 100644 docs-vuepress/.vuepress/dist/api/compile.html delete mode 100644 docs-vuepress/.vuepress/dist/api/composition-api.html delete mode 100644 docs-vuepress/.vuepress/dist/api/directives.html delete mode 100644 docs-vuepress/.vuepress/dist/api/extend.html delete mode 100644 docs-vuepress/.vuepress/dist/api/global-api.html delete mode 100644 docs-vuepress/.vuepress/dist/api/index.html delete mode 100644 docs-vuepress/.vuepress/dist/api/instance-api.html delete mode 100644 docs-vuepress/.vuepress/dist/api/optional-api.html delete mode 100644 docs-vuepress/.vuepress/dist/api/reactivity-api.html delete mode 100644 docs-vuepress/.vuepress/dist/api/store-api.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/1.0.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/2.0.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/2.7-release.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/2.8-release.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/2.9-release.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/index.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/mpx1.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/mpx2.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/performance.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/size-control.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/ts-derivation.html delete mode 100644 docs-vuepress/.vuepress/dist/articles/unit-test.html delete mode 100644 docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css delete mode 100644 docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg delete mode 100644 docs-vuepress/.vuepress/dist/assets/img/start-tips1.3b76ac97.png delete mode 100644 docs-vuepress/.vuepress/dist/assets/img/start-tips2.7d8836f8.png delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/10.0192acce.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/104.78f6358e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/105.eb6680fc.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/106.471f03cf.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/107.63fd18fd.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/108.614260e9.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/109.3da50cc1.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/11.d7a8d045.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/110.f74ff3a3.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/111.51932e54.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/112.da7bd3e7.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/113.caaf11a3.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/114.16cda7ac.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/115.88124b12.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/116.4a116234.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/117.e45c1b95.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/118.9cbac09e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/119.312e4c21.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/12.4d7217be.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/120.7a6b4035.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/14.7377c68f.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/15.6f885a83.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/16.974f551e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/17.e94e6745.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/18.b1f7a4a2.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/19.62186e92.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/2.314c65b9.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/20.e9cd4de3.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/21.217f8fa9.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/22.4cd3344f.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/23.365f9a87.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/24.17777a13.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/25.889c07d7.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/26.3aa9f276.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/27.1a35170a.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/28.3670fa46.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/29.a911e510.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/3.145e69cd.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/30.04d1a7ea.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/31.ec06bf2f.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/32.f29d2cd1.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/33.936d018e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/34.f494d685.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/35.c3ed1ff7.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/36.d2c9afa9.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/37.4e1eee2c.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/38.7826170e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/39.094ffa40.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/4.9489102c.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/40.10d90636.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/41.8351af86.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/42.53972f6c.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/43.e2b432ea.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/44.e4242a1f.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/45.392413aa.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/46.22f9a9c6.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/47.2bd2cecc.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/48.55792a30.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/49.cde94a79.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/5.12d049b6.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/50.42e34e52.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/51.90dfa84e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/52.10bb0251.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/53.7207ffc5.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/54.af3d3dcd.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/55.2d6ae2e1.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/56.9aec9a9e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/57.9d9ddcbd.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/58.50eb3b91.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/59.27106fbb.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/6.6ea91b28.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/60.a47e5279.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/61.74ebd7d9.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/62.4bf212f9.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/63.b54aca3e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/64.399e72b8.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/65.a8c627d3.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/66.7f19cc4b.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/67.e57db97e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/68.ebe4213c.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/69.2d309866.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/7.01e4e942.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/70.60ae83e6.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/71.188d7bca.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/72.3f7bfd0d.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/73.171571e4.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/74.3d5cfaba.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/75.093d716c.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/76.50d84c61.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/77.7bc0f013.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/78.e2ded2d4.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/79.f7eeb76b.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/8.ba7f6ace.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/80.41418749.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/81.16e2cc83.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/82.56c0d7c1.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/83.ea65d6b1.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/84.d50d3d09.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/85.1eed1486.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/86.1ab04b8d.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/87.392384e7.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/88.a95133cd.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/89.213efaae.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/9.26319460.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/90.ef03688f.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/91.43930d52.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/92.ae5e9611.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/93.3cdab647.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/94.72df7b53.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/95.c8e36604.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/96.1ebdc00d.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/97.fa1ed9e7.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/98.939cb6ef.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/99.96c5a70e.js delete mode 100644 docs-vuepress/.vuepress/dist/assets/js/app.84c88b57.js delete mode 100644 docs-vuepress/.vuepress/dist/baidu_verify_codeva-GYcT5ujCTB.html delete mode 100644 docs-vuepress/.vuepress/dist/desc.html delete mode 100644 docs-vuepress/.vuepress/dist/favicon.ico delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/i18n.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/image-process.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/mixin.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/npm.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/pinia.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/platform.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/plugin.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/progressive.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/size-report.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/ssr.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/store.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/subpackage.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/component.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/css.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/event.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/ide.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/intro.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/list-render.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/option-chain.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/reactive.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/refs.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/single-file.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/start.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/template.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/extend/fetch.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/extend/index.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/extend/mock.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/2.7.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/2.8.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/2.9.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/platform/index.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/platform/rn.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/platform/web.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/tool/ts.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/tool/unit-test.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/understand/compile.html delete mode 100644 docs-vuepress/.vuepress/dist/guide/understand/runtime.html delete mode 100644 docs-vuepress/.vuepress/dist/index.html delete mode 100644 docs-vuepress/.vuepress/dist/logo.png delete mode 100644 docs-vuepress/.vuepress/dist/manifest.webmanifest delete mode 100644 docs-vuepress/.vuepress/dist/rn-api.html delete mode 100644 docs-vuepress/.vuepress/dist/rn-component.html delete mode 100644 docs-vuepress/.vuepress/dist/rn-style.html delete mode 100644 docs-vuepress/.vuepress/dist/rn-template.html delete mode 100644 docs-vuepress/.vuepress/dist/service-worker.js diff --git a/docs-vuepress/.vuepress/dist/404.html b/docs-vuepress/.vuepress/dist/404.html deleted file mode 100644 index a33a589c95..0000000000 --- a/docs-vuepress/.vuepress/dist/404.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - Mpx框架 - - - - - - - - - -

404

That's a Four-Oh-Four.
- Take me home. -
- - - diff --git a/docs-vuepress/.vuepress/dist/api/ApiIndex.html b/docs-vuepress/.vuepress/dist/api/ApiIndex.html deleted file mode 100644 index 4f5bdb4a1f..0000000000 --- a/docs-vuepress/.vuepress/dist/api/ApiIndex.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - Mpx框架 - - - - - - - - - -

API 参考

- - - diff --git a/docs-vuepress/.vuepress/dist/api/app-config.html b/docs-vuepress/.vuepress/dist/api/app-config.html deleted file mode 100644 index 15e02d6edd..0000000000 --- a/docs-vuepress/.vuepress/dist/api/app-config.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - 全局配置 | Mpx框架 - - - - - - - - - -

# 全局配置

Mpx.config 是一个对象,包含 Mpx 的全局配置。可以在启动应用之前修改下列 property:

# useStrictDiff

boolean = false

每次有数据变更时,是否使用严格的 diff 算法。如果项目中有大数据集的渲染建议使用,可以提升效率。

import mpx from '@mpxjs/core'
-mpx.config.useStrictDiff = true
-

注意:由于微信小程序的bug,同时使用useStrictDiff和增强指令wx:style时,要注意更改数据的方式。如下所示:

// 入口文件
-import mpx, { createApp } from '@mpxjs/core'
-mpx.config.useStrictDiff = true
-
-// 页面page文件
-<template>
-  <view>
-    <view wx:style="{{style}}">test</view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-  createPage({
-    data: {
-      style: {
-        color: 'red',
-        fontSize: '18px'
-      }
-    },
-    onLoad () {
-      setTimeout(() => {
-        this.setData({ // 当useStrictDiff设置true时,需要用setData的方式设置整个style对象
-          style: {
-            color: 'blue',
-            fontSize: '18px'
-          }
-        })
-        // this.style.color = 'blue' // 当useStrictDiff设置true时,不能使用这种方式,style不会生效
-      }, 1000)
-    }
-  })
-</script>
-

# ignoreWarning

boolean = false

是否忽略运行时的 warning 信息,默认不忽略。

import mpx from '@mpxjs/core'
-mpx.config.ignoreWarning = true
-

# ignoreProxyWhiteList

Array<string> = ['id']

Mpx 实例上的 key(包括data、computed、methods)如果有重名冲突,在ignoreProxyWhiteList配置中的属性会被最新的覆盖;而不在ignoreProxyWhiteList配置中的属性,不会被覆盖。

只要有重名冲突均会有报错提示。

import mpx from '@mpxjs/core'
-mpx.config.ignoreConflictWhiteList = ['id', 'test']
-

# observeClassInstance

boolean = false

当需要对 class 对象的数据进行响应性转化,需要开启该选项。

# proxyEventHandler

Function

需要代理的事件的钩子方法,该钩子方法仅对内联传参事件或 forceProxyEventRules 规则匹配的事件生效。

import mpx from '@mpxjs/core'
-
-mpx.config.proxyEventHandler = function (event) {
-    // 入参为 event 事件对象
-}
-

# setDataHandler

function setDataHandler(data: object, target: ComponentIns<{}, {}, {}, {}, []>): any
-

页面/组件状态更新时,使用该方法可以对 setData 调用进行监听,可以用来统计 setData 调用次数和数据量的统计,方法的入参是 setData 传输的 data 和当前组件实例。

import mpx from '@mpxjs/core'
-
-mpx.config.setDataHandler = function(data, comp) {
-    console.log('setData trigger', data, comp)
-}
-

# forceFlushSync

boolean = false

Mpx 中更改响应性状态时,最终页面的更新并不是同步立即生效的,而是由 Mpx 将它们缓存在一个队列中, 异步等到下一个 tick 一起执行,如果想将所有队列的执行改为同步执行,我们可以通过该配置来实现。

import mpx from '@mpxjs/core'
-
-mpx.config.forceFlushSync = true
-

# errorHandler

Function

mpx.config.errorHandler = function (errmsg, location, error) {
-  // errmsg: 框架内部运行报错的报错归类信息,例如当执行一个watch方法报错时,会是 "Unhandled error occurs during execution of watch callback!"
-  // location: 具体报错的代码路径,可选项,不一定存在
-  // error: 具体的错误堆栈,可选项,不一定存在
-  // handle error
-}
-

Mpx 框架运行时报错捕获感知处理函数。

  • Mpx 框架生命周期执行错误;
  • Mpx 中的 computed、watch 等内置方法执行报错;
  • Mpx 框架的运行时的检测报错,例如存在目标平台不支持的属性,入参出参类型错误等;

同时被捕获的错误会通过 console.error 输出。

# webRouteConfig

Mpx 通过 config 暴露出 webRouteConfig 配置项,在 web 环境可以对路由进行配置。 -此配置后续将被废弃,请使用 webConfig 进行配置

  • 用法:
mpx.config.webRouteConfig = {
-  mode: 'history'
-}
-

# webConfig

web 环境下的一些配置,如路由模式,页面切换动画效果等

  • 用法:
// 修改路由模式
-mpx.config.webConfig.routeConfig = {
-  mode: 'history'
-}
-// 禁用页面切换动画
-mpx.config.webConfig.disablePageTransition = true
-
- - - diff --git a/docs-vuepress/.vuepress/dist/api/builtIn.html b/docs-vuepress/.vuepress/dist/api/builtIn.html deleted file mode 100644 index 440c70c028..0000000000 --- a/docs-vuepress/.vuepress/dist/api/builtIn.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - 内建组件 | Mpx框架 - - - - - - - - - -

# 内建组件

# component

  • is string : 动态渲染组件,通过改变is的值,来渲染不同的组件

  • range string : 使用 range 来指定可能渲染的组件,不传递时则为 全局注册 + usingComponents 中注册的所有组件,存在多个组件时使用逗号 , 分隔

  <!-- ComponentName 是在全局或者组件中完成注册的组件名称 -->
-  <component is="{{ComponentName}}"></component>
-
-  <!-- 只会展示 compA 或者 compB 组件 -->
-  <component is="{{ComponentName}}" range="compA,compB"></component>
-

参考动态组件

# slot

  • name string : 用于命名插槽

<slot> 元素作为组件模板中的内容分发插槽,<slot>元素自身将被替换。

详细用法,可参考下面的链接。

参考slot

- - - diff --git a/docs-vuepress/.vuepress/dist/api/compile.html b/docs-vuepress/.vuepress/dist/api/compile.html deleted file mode 100644 index 58c276a54b..0000000000 --- a/docs-vuepress/.vuepress/dist/api/compile.html +++ /dev/null @@ -1,1214 +0,0 @@ - - - - - - 编译构建 | Mpx框架 - - - - - - - - - -

# 编译构建

对于使用 @mpxjs/cli@3.x 脚手架初始化的项目而言,编译构建相关的配置统一收敛至项目根目录下的 vue.config.js 进行配置。一个新项目初始化的 vue.config.js 如下图,相较于 @mpxjs/cli@2.x 版本,在新的初始化项目当中原有的编译构建配置都收敛至 cli 插件当中进行管理和维护,同时还对外暴露相关的接口或者 api 使得开发者能自定义修改 cli 插件当中默认的配置。

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        // mpx webpack plugin options
-      },
-      unocss: {
-        // @mpxjs/unocss-plugin 相关的配置
-      }
-    }
-  }
-})
-

对于使用 @mpxjs/cli@2.x 脚手架初始化的项目,编译构建配置涉及到 mpx 插件相关的配置主要是在 config 目录下 mpxPlugin.conf.js,涉及到 webpack 本身的配置主要是在 build 目录下。

// config/mpxPlugin.conf.js
-module.exports = () => {
-  return {
-    // mpx webpack plugin options
-  }
-}
-

# 类型定义

为了便于对编译配置的数据类型进行准确地描述,我们在这里对一些常用的配置类型进行定义

# Rules

type Condition = string | ((resourcePath: string) => boolean) | RegExp
-
-interface Rules {
-  include?: Condition | Array<Condition>
-  exclude?: Condition | Array<Condition>
-}
-

# MpxWebpackPlugin

Mpx 编译构建跨平台小程序和 web 的 webpack 主插件,安装示例如下:

npm install -D @mpxjs/webpack-plugin
-pnpm install -D @mpxjs/webpack-plugin
-yarn add -D @mpxjs/webpack-plugin
-

使用示例如下:

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        // mpx webpack plugin options
-      }
-    }
-  }
-})
-

MpxWebpackPlugin支持传入以下配置:

# mode

'wx' | 'ali' | 'swan' | 'qq' | 'tt' | 'jd' | 'dd' | 'qa' | 'web' = 'wx'

mode 为 Mpx 编译的目标平台, 目前支持的有微信小程序(wx)\支付宝小程序(ali)\百度小程序(swan)\头条小程序(tt)\QQ 小程序(qq)\京东小程序(jd)\滴滴小程序(dd)\快应用(qa)\H5 页面(web)。

TIP

在 @mpxjs/cli@3.x 版本当中,通过在 npm script 当中定义 targets 来设置目标平台

// 项目 package.json
-{
-  "script": {
-    "build:cross": "mpx-cli-service build --targets=wx,ali"
-  }
-}
-

# srcMode

'wx' | 'ali' | 'swan' | 'qq' | 'tt' | 'jd' | 'dd' | 'qa' = 'wx'

默认和 mode 一致。,当 srcMode 和 mode 不一致时,会读取相应的配置对项目进行编译和运行时的转换。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        srcMode: 'wx' // 根据项目初始化所选平台来设定
-      }
-    }
-  }
-})
-

WARNING

暂时只支持微信为源 mode 做跨平台,为其他时,mode 必须和 srcMode 保持一致。

# modeRules

{ [key: string]: Rules }

批量指定文件mode,用于条件编译场景下使用某些单小程序平台的库时批量标记这些文件的mode为对应平台,而不再走转换规则。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        modeRules: {
-          ali: {
-            include: [resolve('node_modules/vant-aliapp')]
-          }
-        }
-      }
-    }
-  }
-})
-

# externalClasses

Array<string>

定义若干个外部样式类,这些将会覆盖元素原有的样式。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        externalClasses: ['custom-class', 'i-class']
-      }
-    }
-  }
-})
-

WARNING

抹平支付宝和微信之间的差异,当使用了微信 externalClasses 语法时,跨端输出需要在 @mpxjs/webpack-plugin 的配置中添加此配置来辅助框架进行转换。

# resolveMode

'webpack' | 'native' = 'webpack'

指定resolveMode,默认webpack,更便于引入npm包中的页面/组件等资源。若想编写时和原生保持一致或兼容已有原生项目,可设为native,此时需要提供projectRoot以指定项目根目录,且使用npm资源时需在前面加~

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        resolveMode: 'webpack'
-      }
-    }
-  }
-})
-

# projectRoot

string

当resolveMode为native时需通过该字段指定项目根目录。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-         resolveMode: 'native',
-         projectRoot: path.resolve(__dirname, '../src')
-      }
-    }
-  }
-})
-

# writeMode

'full' | 'change' = 'change'

webpack 的输出默认是全量输出,而小程序开发者工具不关心文件是否真正发生了变化。设置为 change 时,Mpx 在 watch 模式下将内部 diff 一次,只会对内容发生变化的文件进行写入,以提升小程序开发者工具编译性能。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-         writeMode: 'change'
-      }
-    }
-  }
-})
-

# autoScopeRules

Rules

是否需要对样式加 scope ,目前只有支付宝小程序平台没有样式隔离,因此该部分内容也只对支付宝小程序平台生效。提供 include 和 exclude 以精确控制对哪些文件进行样式隔离,哪些不隔离,和webpack的rules规则相同。也可以通过在 style 代码块上声明 scoped 进行。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-         autoScopeRules: {
-           include: [resolve('../src')],
-           exclude: [resolve('../node_modules/vant-aliapp')] // 比如一些组件库本来就是为支付宝小程序编写的,应该已经考虑过样式隔离,就不需要再添加
-         }
-      }
-    }
-  }
-})
-

# forceDisableProxyCtor

boolean = false

用于控制在跨平台输出时对实例构造函数(App | Page | Component | Behavior)进行代理替换以抹平平台差异。当配置 forceDisableProxyCtor 为 true 时,会强行取消平台差异抹平逻辑,开发时需针对输出到不同平台进行条件判断。

# transMpxRules

Rules

是否转换 wx / my 等全局对象为 Mpx 对象,

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        transMpxRules: {
-          include: () => true,
-          exclude: ['@mpxjs']
-        }
-      }
-    }
-  }
-})
-

# forceProxyEventRules

Rules

强制代理规则内配置的事件。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        forceProxyEventRules: {
-          include: ['bindtap']
-        }
-      }
-    }
-  }
-})
-

# defs

object

给模板、js、json、style中定义一些全局常量。一般用于区分平台/环境。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        defs: {
-          __env__: 'mini'
-        }
-      }
-    }
-  }
-})
-

在模板中使用:

<template>
-  <view>{{__env__}}</view>
-</template>
-

在js中使用:

const env = __env__;
-

在style中使用:

/* @mpx-if (__env__ === 'mini') */
-.color {
-  background: red;
-}
-/* @mpx-endif */
-

在json中使用:

<script name='json'>
-  module.exports = {
-    "component": true,
-    "usingComponents": {
-      "a": __env__
-    }
-  }
-</script>
-

注意:这里定义之后使用的时候是按照全局变量来使用,而非按照process.env.KEY这样的形式

# attributes

Array<string> = ['image:src', 'audio:src', 'video:src', 'cover-image:src', 'import:src', 'include:src']

Mpx 提供了可以给自定义标签设置资源的功能,配置该属性后,即可在目标标签中使用 :src 加载相应资源文件

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        attributes: ['customTag:src']
-      }
-    }
-  }
-})
-
<customTag :src="'https://www....../avator.png'"></customTag>
-

TIP

该属性可通过 MpxWebpackPlugin 配置,也可以通过配置 WxmlLoader,后者优先级高。

# externals

Array<string>

目前仅支持微信小程序 weui 组件库通过 useExtendedLib 扩展库的方式引入,这种方式引入的组件将不会计入代码包大小。配置 externals 选项,Mpx 将不会解析 weui 组件的路径并打包。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        externals: ['weui']
-      }
-    }
-  }
-})
-
<script name="json">
-  // app.mpx json部分
-  module.exports = {
-    "useExtendedLib": {
-      "weui": true
-    }
-  }
-</script>
-
<!-- 在 page 中使用 weui 组件 -->
-<template>
-  <view wx:if="{{__mpx_mode__ === 'wx'}}">
-    <mp-icon icon="play" color="black" size="{{25}}" bindtap="showDialog"></mp-icon>
-    <mp-dialog title="test" show="{{dialogShow}}" bindbuttontap="tapDialogButton" buttons="{{buttons}}">
-      <view>test content</view>
-    </mp-dialog>
-  </view>
-</template>
-
-<script>
-  import{ createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      dialogShow: false,
-      showOneButtonDialog: false,
-      buttons: [{text: '取消'}, {text: '确定'}],
-    },
-    methods: {
-      tapDialogButton () {
-        this.dialogShow = false
-        this.showOneButtonDialog = false
-      },
-      showDialog () {
-        this.dialogShow = true
-      }
-    }
-  })
-</script>
-
-<script name="json">
-  const wxComponents = {
-    "mp-icon": "weui-miniprogram/icon/icon",
-    "mp-dialog": "weui-miniprogram/dialog/dialog"
-  }
-  module.exports = {
-    "usingComponents": __mpx_mode__ === 'wx'
-      ? Object.assign({}, wxComponents)
-      : {}
-  }
-</script>
-

参考weui组件库

# miniNpmPackage

Array<string>

微信小程序官方提供了发布小程序 npm 包的约束 (opens new window)。 -部分小程序npm包,如vant组件库 (opens new window)官方文档使用说明,引用资源并不会包含miniprogram所指定的目录 -如 "@vant/weapp/button/index",导致 Mpx 解析路径失败。 -Mpx为解决这个问题,提供miniNpmPackage字段供用户配置需要解析的小程序npm包。miniNpmPackage对应的数组值为npm包对应的package.json中的name字段。 -Mpx解析规则如下:

  1. 如package.json中有miniprogram字段,则会默认拼接miniprogram对应的值到资源路径中
  2. 如package.json中无miniprogram字段,但配置了miniNpmPackage,则默认会拼接miniprogram_dist目录
// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        miniNpmPackage: ['@vant/weapp']
-      }
-    }
-  }
-})
-

# forceUsePageCtor

Boolean = false

为了获取更丰富的生命周期来进行更加完善的增强处理,在非支付宝小程序环境下,Mpx 默认会使用 Conponent 构造器来创建页面。将该值设置为 true 时,会强制使用 Page 构造器创建页面。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        forceUsePageCtor: true
-      }
-    }
-  }
-})
-

# transRpxRules

Array<object> | object

  • option.mode 可选值有 none/only/all,分别是不启用/只对注释内容启用/只对非注释内容启用
  • option.designWidth 设计稿宽度,默认值就是750,可根据需要修改
  • option.include 同webpack的include规则
  • option.exclude 同webpack的exclude规则
  • option.comment rpx注释,建议使用 'use px'/'use rpx',当 mode 为 all 时默认值为 use px,mode 为 only 时默认值为 'use rpx'

为了处理某些IDE中不支持rpx单位的问题,Mpx 提供了一个将 px 转换为 rpx 的功能。支持通过注释控制行级、块级的是否转换,支持局部使用,支持不同依赖分别使用不用的转换规则等灵活的能力。transRpxRules可以是一个对象,也可以是多个这种对象组成的数组。

// vue.config.js
-const path = require('path')
-
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        transRpxRules: [
-          {
-            mode: 'only', // 只对注释为'use rpx'的块儿启用转换rpx
-            comment: 'use rpx', // mode为'only'时,默认值为'use rpx'
-            include: path.resolve('src'),
-            exclude: path.resolve('lib'),
-            designWidth: 750
-          },
-          {
-            mode: 'all', // 所有样式都启用转换rpx,除了注释为'use px'的样式不转换
-            comment: 'use px', // mode为'all'时,默认值为'use px'
-            include: path.resolve('node_modules/@didi/mpx-sec-guard')
-          }
-        ]
-      }
-    }
-  }
-})
-

# 应用场景及相应配置

接下来我们来看下一些应用场景及如何配置。如果是用脚手架生成的项目,在mpx.plugin.conf.js里找到transRpxRules,应该已经有预设的transRpxRules选项,按例修改即可。

三种场景分别是 普通使用只对某些特殊样式转换不同路径分别配置规则

# 场景一

设计师给的稿是2倍图,分辨率750px。或者更高倍图。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        transRpxRules: [{
-          mode: 'all',
-          designWidth: 750 // 如果是其他倍,修改此值为设计稿的宽度即可
-        }]
-      }
-    }
-  }
-})
-

# 场景二

大部分样式都用px下,某些元素期望用rpx。或者反过来。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        transRpxRules: [{
-          mode: 'only',
-          comment: 'use rpx',
-          designWidth: 750 // 设计稿宽度
-        }]
-      }
-    }
-  }
-})
-

mpx的rpx注释能帮助你仅为部分类或者部分样式启用rpx转换,细节请看下方附录。

# 场景三

使用了第三方组件,它的设计宽度和主项目不一致,期望能设置不同的转换规则

// vue.config.js
-const path = require('path')
-
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        transRpxRules: [
-          {
-            mode: 'only',
-            designWidth: 750,
-            comment: 'use rpx',
-            include: resolve('src')
-          },
-          {
-            mode: 'all',
-            designWidth: 1280, // 对iview单独使用一个不同的designWidth
-            include: path.resolve('node_modules/iview-weapp')
-          }
-        ]
-      }
-    }
-  }
-})
-

注意事项:转换规则是不可以对一个文件做多次转换的,会出错,所以一旦被一个规则命中后就不会再次命中另一个规则,include 和 exclude 的编写需要注意先后顺序,就比如上面这个配置,如果第一个规则 include 的是 '/' 即整个项目,iview-weapp 里的样式就无法命中第二条规则了。

# transRpxRules附录

  • designWidth

设计稿宽度,单位为px。默认值为750px

mpx会基于小程序标准的屏幕宽度baseWidth 750rpx,与option.designWidth计算出一个转换比例transRatio

转换比例的计算方式为transRatio = (baseWidth / designWidth),精度为小数点后2位四舍五入。

所有生效的rpx注释样式中的px会乘上transRatio得出最终的 rpx 值。

例如:

/* 转换前:designWidth = 1280 */
-.btn {
-  width: 200px;
-  height: 100px;
-}
-
-/* 转换后: transRatio = 0.59 */
-.btn {
-  width: 118rpx;
-  height: 59rpx;
-}
-
  • comment: rpx 注释样式

根据rpx注释的位置,mpx会将一段css规则或者一条css声明视为rpx注释样式

开发者可以声明一段 rpx 注释样式,提示编译器是否转换这段 css 中的 px。

例如:

<style lang="css">
-  /* use rpx */
-  .not-translate-a {
-    font-size: 100px;
-    padding: 10px;
-  }
-  .not-translate-b {
-    /* use rpx */
-    font-size: 100px;
-    padding: 10px;
-  }
-  .translate-a {
-    font-size: 100px;
-    padding: 10px;
-  }
-  .translate-b {
-    font-size: 100px;
-    padding: 10px;
-  }
-</style>
-

第一个注释位于一个选择器前,是一个css规则注释,整个规则都会被视为rpx注释样式

第二个注释位于一个css声明前,是一个css声明注释,只有font-size: 100px会被视为rpx注释样式

transRpx = only模式下,只有两部分rpx注释样式会转rpx。

# postcssInlineConfig

{options? : PostcssOptions, plugins? : PostcssPlugin[], mpxPrePlugins? : PostcssPlugin[], ignoreConfigFile : Boolean}

使用类似于 postcss.config.js 的语法书写 postcss 的配置文件。用于定义 Mpx 对于组件/页面样式进行 postcss 处理时的配置, ignoreConfigFile 传递为 true 时会忽略项目中的 postcss 配置文件 。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        postcssInlineConfig: {
-          plugins: [
-            // require('postcss-import'),
-            // require('postcss-preset-env'),
-            // require('cssnano'),
-            // require('autoprefixer')
-          ]
-        }
-      }
-    }
-  }
-})
-

注意:默认添加的 postcss 插件均会在mpx的内置插件(例如如rpx插件等)之后处理。如需使配置的插件优先于内置插件,可以在 postcssInlineConfig 中添加 mpxPrePlugins 配置:

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        postcssInlineConfig: {
-          plugins: [
-            require('postcss-import'),
-            require('postcss-preset-env'),
-          ],
-          mpxPrePlugins: [
-            require('cssnano'),
-            require('autoprefixer')
-          ]
-          // 以下写法同理
-          // mpxPrePlugins: {
-          //   'cssnano': {},
-          //   'autoprefixer': {}
-          // }
-        }
-      }
-    }
-  }
-})
-

postcss.config.js 中配置同理:

// postcss.config.js
-module.exports = {
-  plugins: [
-    require('postcss-import'),
-    require('postcss-preset-env'),
-  ],
-  mpxPrePlugins: [
-    require('cssnano'),
-    require('autoprefixer')
-  ]
-}
-
-

在上面这个例子当中,postcss 插件处理的最终顺序为:cssnano -> autoprefixer -> mpx内置插件 -> postcss-import -> postcss-preset-env

WARNING

注意:在 mpxPrePlugins 中配置的 postcss 插件如果不通过 mpx 进行处理,那么将不会生效。

# decodeHTMLText

boolean = false

设置为 true 时在模板编译时对模板中的 text 内容进行 decode

# nativeConfig

{cssLangs: string[]}

为原生多文件写法添加css预处理语言支持,用于优先搜索预编译器后缀的文件,按 cssLangs 中的声明顺序查找。默认按照 css , less , stylus , scss , sass 的顺序

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        nativeConfig: {
-          cssLangs: ['css', 'less', 'stylus', 'scss', 'sass']
-        }
-      }
-    }
-  }
-})
-

# webConfig

{transRpxFn(match:string, $1:number): string}

transRpxFn 配置用于自定义输出 web 时对于 rpx 样式单位的转换逻辑,常见的方式有转换为 vw 或转换为 rem

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        webConfig: {
-          transRpxFn: function (match, $1) {
-            if ($1 === '0') return $1
-            return `${$1 * +(100 / 750).toFixed(8)}vw`
-          }
-        }
-      }
-    }
-  }
-})
-

# i18n

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        i18n: {
-          locale: 'en-US',
-          messages: {
-            'en-US': {
-              message: {
-                hello: '{msg} world'
-              }
-            },
-            'zh-CN': {
-              message: {
-                hello: '{msg} 世界'
-              }
-            }
-          },
-          // messagesPath: path.resolve(__dirname, '../src/i18n.js')
-        }
-      }
-    }
-  }
-})
-

Mpx 支持国际化,底层实现依赖类wxs能力,通过指定语言标识和语言包,可实现多语言之间的动态切换。可配置项包括locale、messages、messagesPath。

# i18n.locale

string

通过配置 locale 属性,可指定语言标识,默认值为 'zh-CN'

# i18n.messages

object

通过配置 messages 属性,可以指定项目语言包,Mpx 会依据语言包对象定义进行转换,示例如下:

messages: {
-  'en-US': {
-    message: {
-      'title': 'DiDi Chuxing',
-      'subTitle': 'Make travel better'
-    }
-  },
-  'zh-CN': {
-    message: {
-      'title': '滴滴出行',
-      'subTitle': '让出行更美好'
-    }
-  }
-}
-

# i18n.messagesPath

string

为便于开发,Mpx 还支持配置语言包资源路径 messagesPath 来代替 messages 属性,Mpx 会从该路径下的 js 文件导出语言包对象。如果同时配置 messages 和 messagesPath 属性,Mpx 会优先设定 messages 为 i18n 语言包资源。

详细介绍及使用见工具-国际化i18n一节。

# auditResource

'component' | boolean = false

检查资源输出情况,如果置为true,则会提示有哪些资源被同时输出到了多个分包,可以检查是否应该放进主包以消减体积,设置为 'component' 的话,则只检查组件资源是否被输出到多个分包。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        auditResource: true
-      }
-    }
-  }
-})
-

# subpackageModulesRules

object

是否将多分包共用的模块分别输出到各自分包中,匹配规则为include匹配到且未被exclude匹配到的资源

依据微信小程序的分包策略,多个分包使用到的 js 模块会打入主包当中,但在大型分包较多的项目中,该策略极易将大量的模块打入主包,从而使主包体积大小超出2M限制,该配置项提供给开发者自主抉择,可将部分模块冗余输出至多个分包,从而控制主包体积不超限

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        subpackageModulesRules: {
-          include: ['@someNpm/name/src/api/*.js'],
-          exclude: ['@someNpm/name/src/api/module.js']
-        }
-      }
-    }
-  }
-})
-

tips: 该功能是将模块分别放入多个分包,模块状态不可复用,使用前要依据模块功能做好评估,例如全局store就不适用该功能

# generateBuildMap

boolean = false

是否生成构建结果与源码之间的映射文件。用于单元测试等场景。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        generateBuildMap: true
-      }
-    }
-  }
-})
-

参考单元测试

# autoVirtualHostRules

Rules -批量配置是否虚拟化组件节点,对应微信中VirtualHost (opens new window) 。默认不开启,开启后也将抹平支付宝小程序中的表现差异。提供 include 和 exclude 以精确控制对哪些文件开启VirtualHost,哪些不开启。和webpack的rules规则相同。

默认情况下,自定义组件本身的那个节点是一个“普通”的节点,使用时可以在这个节点上设置 classstyle 、动画、 flex 布局等,就如同普通的 view 组件节点一样。但有些时候,自定义组件并不希望这个节点本身可以设置样式、响应 flex 布局等,而是希望自定义组件内部的第一层节点能够响应 flex 布局或者样式由自定义组件本身完全决定。这种情况下,可以将这个自定义组件设置为“虚拟的”。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        autoVirtualHostRules: {
-          include: [resolve('../src')],
-          exclude: [resolve('../components/other')]
-        }
-      }
-    }
-  }
-})
-

注意: 建议使用autoVirtualHostRules配置项,不要使用微信组件内部的 options virtualHost 配置,因为组件内部的 options virtualHost 在跨平台输出时无法进行兼容抹平处理。

# partialCompileRules

{ include: string | RegExp | Function | Array<string | RegExp | Function> }

在大型的小程序开发当中,全量打包页面耗时非常长,往往在开发过程中仅仅只需用到几个 pages 而已,该配置项支持打包指定的小程序页面。

注意: @mpxjs/webpack-plugin@2.9.41版本之前该配置为 partialCompile。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        // include 可以是正则、字符串、函数、数组
-        partialCompileRules: {
-          include: '/project/pages', // 文件路径包含 '/project/pages' 的页面都会被打包
-          include: /pages\/internal/, // 文件路径能与正则匹配上的页面都会被打包
-          include (pageResourcePath) {
-            // pageResourcePath 是小程序页面所在系统的文件路径
-            return pageResourcePath.includes('pages') // 文件路径包含 'pages' 的页面都会被打包
-          },
-          include: [
-            '/project/pages',
-            /pages\/internal/,
-            (pageResourcePath) => pageResourcePath.includes('pages')
-          ] // 满足任意条件的页面都会被打包
-        }
-      }
-    }
-  }
-})
-

WARNING

该特性只能用于开发环境,默认情况下会阻止所有页面(入口 app.mpx 除外)的打包。

# optimizeRenderRules

Rules

render 函数中可能会存在一些重复变量,该配置可消除 render 函数中的重复变量,进而减少包体积。不配置该参数,则不会消除重复变量。

同时框架 render 函数优化提供了两个等级,使用 level 字段来进行控制,默认为 level = 1

  • level = 1时,框架生成 render 函数中完成保留 template 中的计算逻辑,setData 传输量保持了最优。
  • level = 2时,框架生成 render 函数中仅保留所有 template 中使用到的响应性变量,无任何计算逻辑保留,render 函数体积达最小状态,但 setData 传输量相对于 level=1 会有所增加。
// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        optimizeRenderRules: {
-          include: [
-            resolve('src')
-          ],
-          level: 1
-        }
-      }
-    }
-  }
-})
-

# asyncSubpackageRules

type Condition = string | Function | RegExp
-
-interface AsyncSubpackageRules {
-  include: Condition | Array<Condition>
-  exclude?: Condition | Array<Condition>
-  root: string
-  placeholder: string | { name: string, resource?: string}
-}
-
  • include: 同 webpack include 规则
  • exclude: 同 webpack exclude 规则
  • root: 匹配规则的组件或js模块的输出分包名
  • placeholder: 匹配规则的组件所配置的componentPlaceholder,可支持配置原生组件和自定义组件,原生组件可直接以string类型配置,自定义组件需要配置对象,name 为该自定义组件名, resource 为自定义组件的路径,路径可为绝对路径和相对于项目目录的相对路径

异步分包场景下批量设置组件或 js 模块的异步分包,提升资源异步分包输出的灵活性。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        // include 可以是正则、字符串、函数、数组
-        asyncSubpackageRules: [
-          {
-            include: '/project/pages', // 文件路径包含 '/project/pages' 的组件或者 require.async 异步引用的js 模块都会被打包至sub1分包
-            root: 'sub1',
-            placeholder: 'view'
-          }
-        ]
-      }
-    }
-  }
-})
-

WARNING

  • 该配置匹配的组件,若使用方在引用路径已设置?root或componentPlaceholder,则以引用路径中的?root或componentPlaceholder为最高优先级
  • 若placeholder配置使用自定义组件,注意一定要配置 placeholder 中的 resource 字段
  • 本功能只会对使用require.async异步引用的js模块生效,若引用路径中已配置?root,则以路径中?root优先

# retryRequireAsync

boolean = false

开启时在处理require.async时会添加单次重试逻辑

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        retryRequireAsync: true
-      }
-    }
-  }
-})
-

# disableRequireAsync

boolean = false

Mpx 框架在输出 微信小程序、支付宝小程序、字节小程序、Web 平台时,默认支持分包异步化能力,但若在某些场景下需要关闭该能力,可配置该项。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        disableRequireAsync: true
-      }
-    }
-  }
-})
-

# optimizeSize

boolean = false

开启后可优化编译配置减少构建产物体积

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        optimizeSize: true
-      }
-    }
-  }
-})
-

# MpxWebpackPlugin static methods

MpxWebpackPlugin 通过静态方法暴露了以下五个内置 loader,详情如下:

# MpxWebpackPlugin.loader

MpxWebpackPlugin 所提供的最主要 loader,用于处理 .mpx 文件,根据不同的目标平台.mpx 文件输出为不同的结果。

在微信环境下 todo.mpx 被loader处理后的文件为:todo.wxmltodo.wxsstodo.jstodo.json

module.exports = {
-  module: {
-    rules: [
-      {
-        test: /\.mpx$/,
-        use: MpxWebpackPlugin.loader()
-      }
-    ]
-  }
-};
-

# MpxWebpackPlugin.pluginLoader

WARNING

该 loader 仅在开发小程序插件时使用,可在使用 Mpx 脚手架进行项目初始化时选择进行组件开发来生成对应的配置文件。

MpxWebpackPlugin.pluginLoader 用于根据开发者编写的plugin.json文件内容,将特定的小程序组件、页面以及 js 文件进行构建,最终以小程序插件的形式输出。

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  configureWebpack() {
-    return {
-      module: {
-        rules: [
-          {
-            resource: path.resolve('src/plugin/plugin.json'), // 小程序插件的plugin.json的绝对路径
-            use: MpxWebpackPlugin.pluginLoader()
-          }
-        ]
-      }
-    }
-  }
-})
-

更多细节请查阅 小程序插件开发 (opens new window)

# MpxWebpackPlugin.wxsPreLoader

加载并解析 wxs 脚本文件,并针对不同平台,做了差异化处理;同时可支持处理内联wxs

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  configureWebpack() {
-    return {
-      module: {
-        rules: [
-          {
-            test: /\.(wxs|qs|sjs|filter\.js)$/,
-            loader: MpxWebpackPlugin.wxsPreLoader(),
-            enforce: 'pre'
-          }
-        ]
-      }
-    }
-  }
-})
-

# MpxWebpackPlugin.fileLoader

提供图像资源的处理,生成对应图像文件,输出到输出目录并返回 public URL。如果是分包资源,则会输出到相应的分包资源文件目录中。

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  configureWebpack() {
-    return {
-      module: {
-        rules: [
-          {
-            test: /\.(png|jpe?g|gif|svg)$/,
-            loader: MpxWebpackPlugin.fileLoader({
-              name: 'img/[name][hash].[ext]'
-            })
-          }
-        ]
-      }
-    }
-  }
-})
-

选项

  • name : 自定义输出文件名模板

# MpxWebpackPlugin.urlLoader

微信小程序对于图像资源存在一些限制,MpxWebpackPlugin.urlLoader 针对这些差异做了相关处理,开发者可以使用web应用开发的方式进行图像资源的引入,MpxWebpackPlugin.urlLoader 可根据图像资源的不同引入方式,支持 CDN 或者 Base64 的方式进行处理。更多细节请查阅图像资源处理

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  configureWebpack() {
-    return {
-      module: {
-        rules: [
-          {
-            test: /\.(png|jpe?g|gif|svg)$/,
-            loader: MpxWebpackPlugin.urlLoader({
-              name: 'img/[name][hash].[ext]',
-              limit: 2048,
-            })
-          }
-        ]
-      }
-    }
-  }
-})
-

选项:

  • name : 自定义输出文件名模板
  • mimetype : 指定文件的 MIME 类型
  • limit : 对内联文件作为数据 URL 的字节数限制
  • publicPath : 自定义 public 目录
  • fallback : 文件字节数大于限制时,为文件指定加载程序

# MpxWebpackPlugin.getPageEntry

在 webpack config entry 入口文件配置中,你可以使用该方法获取独立构建页面路径,构建产物为该页面的独立原生代码, -你可以提供该页面给其他小程序使用。

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  chainWebpack(config) {
-    config.entry('index').add(MpxWebpackPlugin.getPageEntry('./index.mpx'))
-  }
-})
-

# MpxWebpackPlugin.getComponentEntry

在 webpack config entry 入口文件配置中,你可以使用该方法获取独立构建组件路径,构建产物为该组件的独立原生代码, -你可以提供该组件给其他小程序使用。

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  chainWebpack(config) {
-    config.entry('index').add(MpxWebpackPlugin.getComponentEntry('./components/list.mpx'))
-  }
-})
-

# MpxWebpackPlugin.getNativeEntry

在 webpack config entry 入口文件配置中,你可以使用该方法获取原生小程序入口文件路径。如果你不想将原生的小程序入口文件整合为 app.mpx 文件,则可以使用该方法直接使用原有的小程序入口文件进行编译。见#1330

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  chainWebpack(config) {
-    // 将 app 入口由默认的 app.mpx 更改为 app.js
-    config.entry('app').clear().add(MpxWebpackPlugin.getNativeEntry('./src/app.js'))
-  }
-})
-

# MpxUnocssPlugin

Mpx 编译 unocss 原子类的 webpack 主插件

如果在使用 @mpxjs/cli@3.x 创建项目时选择了 unocss,会自动安装 MpxUnocssPlugin ,直接在 mpx.unocss 配置项中传入相关配置即可

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        // @mpxjs/unocss-plugin 相关的配置
-      }
-    }
-  }
-})
-

如果创建项目时未选 unocss,需手动安装,安装示例如下:

npm install -D @mpxjs/unocss-plugin
-pnpm install -D @mpxjs/unocss-plugin
-yarn add -D @mpxjs/unocss-plugin
-

使用示例如下:

  // vue.config.js
-  const MpxUnocssPlugin = require('@mpxjs/unocss-plugin')
-  const { defineConfig } = require('@vue/cli-service')
-
-  module.exports = defineConfig({
-    configureWebpack: {
-      plugins: [
-        new MpxUnocssPlugin({
-          // @mpxjs/unocss-plugin 相关的配置
-        })
-      ]
-    },
-  })
-

插件支持配置如下:

# unoFile

string = 'styles/uno'

生成主包或分包通用样式存储的相对路径

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        unoFile: 'styles/uno'
-      }
-    }
-  }
-})
-

则会把通用样式存储到下面目录

  // 主包
-  dist/wx/styles/uno.wxss
-  // 分包
-  dist/wx/package/styles/uno.wxss
-

# minCount

number = 2

使用到某个原子类的最小分包个数,比如设置为2的话一个原子类只有超过2个分包使用才会输出到主包

主要是用来控制主包占用的,数值越大分包的原子类就有更大可能性不占用主包

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        minCount: 2
-      }
-    }
-  }
-})
-
  <!-- minCount=2 -->
-  <!-- a分包 -->
-  <view class="bg-black color-white"></view>
-  <!-- b分包 -->
-  <view class="bg-black"></view>
-

unocss将把生成的bg-black样式打包到主包

# styleIsolation

string = 'isolated'

需要和微信小程序的styleIsolation配合使用,比如小程序使用样式隔离的话,这里需要对应配置为isolated,这样的话每个组件会独立引用对应的原子类文件,配置为'apply-shared'的话只有父级页面和app会建立引用,然后通过配合微信的apply-shared的方式获取父级上定义的原子类

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        styleIsolation: 'isolated'
-      }
-    }
-  }
-})
-

# scan

  interface Scan {
-    include?: string[]
-    exclude?: string[]
-  }
-

配置需要扫描的文件目录

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        scan: {
-          include: ['src/**/*']
-        }
-      }
-    }
-  }
-})
-

# escapeMap

object

针对原子类中出现的[ ( ,等特殊字符,在web中会通过转义字符\进行转义,由于小程序环境下不支持css选择器中出现\转义字符,我们内置支持了一套不带\的转义规则对这些特殊字符进行转义,同时替换模版和css文件中的类名,内建的默认转义规则,可自定义转译规则

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        escapeMap: {
-          ':': '_d_',
-        }
-      }
-    }
-  }
-})
-
  <view class="dark:text-green-400"/>
-

将会转化为

  .dark .dark_d_text-green-400{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
-

# root

string = process.cwd()

文件根目录

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        root: process.cwd()
-      }
-    }
-  }
-})
-

# transformCSS

boolean = true

转化css指令为常规css

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        transformCSS: true
-      }
-    }
-  }
-})
-
  .custom-div {
-    @apply text-center my-0 font-medium;
-  }
-

将会转化为

  .custom-div {
-    margin-top: 0;
-    margin-bottom: 0;
-    text-align: center;
-    font-weight: 500;
-  }
-

# transformGroups

boolean = true

转化Variant group

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        transformGroups: true
-      }
-    }
-  }
-})
-
  <view class="lg:(p-2 m-2 text-red-600)"></view>
-

将会转化为

  <view class="lg:p-2 lg:m-2 lg:text-red-600"></view>
-

# config

UserConfig | string

config可以传配置对象也可以传一个配置文件路径

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        config: {
-          rules: [
-            ['m-1', { margin: '10rpx' }],
-          ]
-        }
-      }
-    }
-  }
-})
-

# configFiles

LoadConfigSource[]

  interface LoadConfigSource<T = any> {
-      files: Arrayable<string>;
-      /**
-       * @default ['mts', 'cts', 'ts', 'mjs', 'cjs', 'js', 'json', '']
-      */
-      extensions?: string[];
-      /**
-       * Loader for loading config,
-      *
-      * @default 'auto'
-      */
-      parser?: BuiltinParsers | CustomParser<T> | 'auto';
-      /**
-       * Rewrite the config object,
-      * return nullish value to bypassing loading the file
-      */
-      rewrite?: <F = any>(obj: F, filepath: string) => Promise<T | undefined> | T | undefined;
-      /**
-       * Transform the source code before loading,
-      * return nullish value to skip transformation
-      */
-      transform?: (code: string, filepath: string) => Promise<string | undefined> | string | undefined;
-      /**
-       * Skip this source if error occurred on loading
-      *
-      * @default false
-      */
-      skipOnError?: boolean;
-  }
-

configFiles的话是传递额外的配置文件数组,比如不想用uno.config作为配置文件的话可以在这里面配

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      unocss: {
-        configFiles: [
-          {
-            files: [
-              'uno2.config.js'
-            ]
-          }
-        ]
-      }
-    }
-  }
-})
-

# commentConfig

我们还支持了commentConfig进行组件局部配置,目前支持safelist和styleIsolation,safelist可以用空格分隔写多个

  <template>
-    <!-- mpx_config_styleIsolation: 'isolated' -->
-    <!-- mpx_config_safelist: 'text-red-500 bg-black' -->
-    <view>mpx-unocss</view>
-  </template>
-

# MpxUnocssBase

Mpx 内置的 unocss preset,继承自 @unocss/preset-uno,并额外提供小程序原子类的预设样式,安装示例如下:

npm install -D @mpxjs/unocss-base
-pnpm install -D @mpxjs/unocss-base
-yarn add -D @mpxjs/unocss-base
-

使用示例如下:

  // uno.config.js
-  const { defineConfig } = require('unocss')
-  const presetMpx = require('@mpxjs/unocss-base')
-
-  module.exports = defineConfig({
-    presets: [
-      presetMpx({
-        // ...
-      })
-    ],
-    // unocss的config,具体配置参考https://unocss.dev/config/
-  })
-

支持的配置项如下:

# baseFontSize

number = 37.5

同比换算1rem = 37.5rpx适配小程序

  // uno.config.js
-  const { defineConfig } = require('unocss')
-  const presetMpx = require('@mpxjs/unocss-base')
-
-  module.exports = defineConfig({
-    presets: [
-      presetMpx({
-        baseFontSize: 37.5
-      })
-    ],
-  })
-

# preflight

boolean = true

是否生成预设样式

  // uno.config.js
-  const { defineConfig } = require('unocss')
-  const presetMpx = require('@mpxjs/unocss-base')
-
-  module.exports = defineConfig({
-    presets: [
-      presetMpx({
-        preflight: true
-      })
-    ],
-  })
-

将添加预设样式在主包

page{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgba(0,0,0,0);--un-ring-shadow:0 0 rgba(0,0,0,0);--un-shadow-inset: ;--un-shadow:0 0 rgba(0,0,0,0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgba(147,197,253,0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}
-

# dark

class | media | DarkModeSelectors = 'class'

  interface DarkModeSelectors {
-    /**
-     * light variant的选择器.
-     *
-     * @default '.light'
-     */
-    light?: string
-
-    /**
-     * dark variant的选择器
-     *
-     * @default '.dark'
-     */
-    dark?: string
-  }
-

默认情况下,此预设使用dark:variant生成基于类的dark模式。

  <view class="dark:text-green-400" />
-

我们将生成

  .dark .dark_c_text-green-400{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
-

要选择基于媒体查询的dark模式,您可以使用@dark:variant

  <view class="@dark:text-green-400" />
-

我们将生成

  @media (prefers-color-scheme: dark){
-    ._u_dark_c_text-green-400{--un-text-opacity:1;color:rgba(74,222,128,var(--un-text-opacity));}
-  }
-

或者使用针对dark:variant的配置进行全局设置

  presetMpx({
-    dark: "media"
-  })
-

# attributifyPseudo

boolean = false

将伪选择器生成为[group=''],而不是.group。只支持group|peer|parent|previous -如果attributifyPseudo为true的话,

  <view class="group">
-    <view class="group-hover:opacity-100" />
-  </view>
-

上面template将生成

  [group=""]:hover .group-hover_c_opacity-100{opacity:1;}
-

为false则生成

  .group:hover .group-hover_c_opacity-100{opacity:1;}
-

# variablePrefix

string = 'un-'

CSS变量的前缀

  presetMpx({
-    variablePrefix: 'un-'
-  })
-
  .bg-red-500{--un-bg-opacity:1;background-color:rgba(239,68,68,var(--un-bg-opacity));}
-

# Request query

Mpx中允许用户在request中传递特定query执行特定逻辑,目前已支持的query如下:

# ?resolve

用于获取资源最终被输出的正确路径。

Mpx 在处理页面路径时会把对应的分包名作为 root 加在路径前。处理组件路径时会添加 hash,防止路径名冲突。直接写资源相对路径可能与最终输出的资源路径不符。

编写代码时使用 import 引入页面地址后加上 ?resolve,这个地址在编译时会被处理成正确的绝对路径,即资源的最终输出位置。

import subPackageIndexPage from '../subpackage/pages/index.mpx?resolve'
-
-mpx.navigateTo({
-  url: subPackageIndexPage
-})
-

# ?root

  1. 声明分包别名

指定分包别名,Mpx 项目在编译构建后会输出该别名的分包,外部小程序或 H5 页面跳转时,可直接配置该分包别名下的资源路径。

// 可在项目app.mpx中进行配置
-module.exports = {
-  packages: [
-    '@packagePath/src/app.mpx?root=test',
-  ]
-}
-
-// 使用
-wx.navigateTo({url : '/test/homepage/index'})
-
  1. 声明组件所属异步分包

微信小程序新增 分包异步化特性 (opens new window) ,使跨分包的组件可以等待对应分包下载后异步使用, 在mpx中使用需通过?root声明组件所属异步分包即可使用,示例如下:

<!--/packageA/pages/index.mpx-->
-// 这里在分包packageA中即可异步使用分包packageB中的hello组件
-<script type="application/json">
-  {
-    "usingComponents": {
-      "hello": "../../packageB/components/hello?root=packageB",
-      "simple-hello": "../components/hello"
-    },
-    "componentPlaceholder": {
-      "hello": "simple-hello"
-    }
-  }
-</script>
-

# ?fallback

boolean

对于使用MpxWebpackPlugin.urlLoader的文件,如果在引用资源的末尾加上?fallback=true,则使用配置的自定义loader。图片的引入和处理详见图像资源处理

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      urlLoader: {
-        name: 'img/[name][hash].[ext]',
-        publicPath: 'http://a.com/',
-        fallback: 'file-loader' // 自定义fallback为true时使用的loader
-      }
-    }
-  }
-})
-
/* png资源引入 */
-<style>
-  .logo2 {
-    background-image: url('./images/logo.png?fallback=true'); /* 设置fallback=true,则使用如上方所配置的file-loader */
-  }
-</style>
-

# ?useLocal

boolean

静态资源存放有两种方式:本地、远程(配置 publicPath )。useLocal 是用于在配置了 publicPath 时声明部分资源输出到本地。比如配置了通用的 CDN 策略,但如网络兜底资源需要强制走本地存储,可在引用资源的末尾加上?useLocal=true

/* 单个图片资源设置为存储到本地 */
-<style>
-  .logo2 {
-    background-image: url('./images/logo.png?useLocal=true');
-  }
-</style>
-

# ?isStyle

boolean

isStyle 是在非 style 模块中编写样式时,声明这部分引用的静态资源按照 style 环境来处理。如在 javascript 中 require 了一个图像资源,然后模版 template style 属性中进行引用, 则 require 资源时可选择配置?isStyle=true

<template>
-  <view class="list">
-    <!-- isStyle 案例一:引用 javascript 中的数据 -->
-    <view style="{{testStyle}}">测试</view>
-    <!-- isStyle 案例二:设置资源按照style处理规则处理。style处理规则为: 默认走base64,除非同时配置了 publicPath 和 fallback -->
-    <image src="../images/car.jpg?isStyle=true"></image>
-    <!-- 普通非style模式,默认走 fallback 或者 file-loader 解析,输出到 publicPath 或者 本地img目录下 -->
-    <image src="../images/car.jpg"></image>
-  </view>
-</template>
-
/* 将 script 中的图像资源标识为 style 资源 */
-<script>
-  import { createComponent } from '@mpxjs/core'
-  const backCar = require('../images/car.jpg?isStyle=true')
-
-  createComponent({
-    data: {},
-    computed: {
-      testStyle () {
-        return `background-image : url(${backCar}); width:100px; height: 100px`
-      }
-    }
-  })
-</script>
-

# ?isPage

boolean

在 webpack config entry 入口文件配置中,你可以在路径后追加 ?isPage 来声明独立页面构建,构建产物为该页面的独立原生代码, -你可以提供该页面给其他小程序使用。此外,独立页面构建也可以通过MpxWebpackPlugin.getPageEntry生成,推荐使用此方法。

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  chainWebpack(config) {
-    config.entry('index').add('../src/pages/index.mpx?isPage')
-    // 或者
-    // config.entry('index').add(MpxWebpackPlugin.getPageEntry('./index.mpx'))
-  }
-})
-

WARNING

对于使用 @mpxjs/cli@2.x 脚手架初始化的项目,配置 entry 的方式如下

// build/getWebpackConf.js
-module.exports = {
-  entry: {
-    index: '../src/pages/index.mpx?isPage'
-  }
-}
-

# ?isComponent

boolean

在 webpack config entry 入口文件配置中,你可以在路径后追加 ?isComponent 来声明独立组件构建,构建产物为该组件的独立原生代码, -你可以提供该组件给其他小程序使用。 -此外,独立组件构建也可以通过MpxWebpackPlugin.getComponentEntry生成,推荐使用此方法。

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-const MpxWebpackPlugin = require('@mpxjs/webpack-plugin')
-module.exports = defineConfig({
-  chainWebpack(config) {
-    config.entry('index').add('../src/components/list.mpx?isComponent')
-    // 或者
-    // config.entry('index').add(MpxWebpackPlugin.getComponentEntry('./components/list.mpx'))
-  }
-})
-

# ?async

boolean | string

输出 H5 时 Vue Router 的路由懒加载功能,Mpx框架默认会对分包开启路由懒加载功能并将分包所有页面都打入同一个Chunk -,如果你希望对于部分主包页面或者分包页面配置路由懒加载并想自定义Chunk Name,则可以使用该功能。

// app.mpx 
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/index?async", // 主包页面配置路由懒加载,Chunk Name 为随机数字
-      "./pages/index2?async=app_pages2"// 主包页面配置路由懒加载,Chunk Name 自定义为 app_pages2
-    ],
-    "packages": [
-      "./packages/sub1/app.mpx?root=sub1"
-    ]
-  }
-</script>
-
-// packages/sub1/app.mpx
-
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/index?async=sub1_pages_index", // 分包中页面设置路由懒加载并设置自定义Chunk Name
-      "./pages/index2?async=sub2_pages_index2" // 分包中页面设置路由懒加载并设置自定义Chunk Name
-    ]
-  }
-</script>
-
-
- - - diff --git a/docs-vuepress/.vuepress/dist/api/composition-api.html b/docs-vuepress/.vuepress/dist/api/composition-api.html deleted file mode 100644 index 3236d17230..0000000000 --- a/docs-vuepress/.vuepress/dist/api/composition-api.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - 组合式 API | Mpx框架 - - - - - - - - - -

# 组合式 API

# setup

一个组件选项,在组件被创建之前,props 被解析之后执行。是组合式 API 的入口。

  • 参数: -
    • {Data} props
    • {SetupContext} context

props 对象仅包含显性声明的 properties。并且所有声明了的prop,不论父组件是否向其传递, -都将出现在 props 对象中。其中未被传入的可选的 prop 的值会是默认值或 undefined。

import { createComponent } from '@mpxjs/core'
-
-createComponent({
-    properties: {
-        min: {
-            type: Number,
-            value: 0
-        },
-        lastLeaf: {
-            // 这个属性可以是 Number 、 String 、 Boolean 三种类型中的一种
-            type: Number,
-            optionalTypes: [String, Object],
-            value: 0
-        }
-    },
-    setup(props) {
-        console.log(props.min)
-        console.log(props.lastLeaf)
-    }
-})
-
  • 类型声明
interface SetupContext {
-    triggerEvent(
-       name: string,
-       detail?: object, // detail对象,提供给事件监听函数
-       options?: {
-         bubbles?: boolean
-         composed?: boolean
-         capturePhase?: boolean
-       }
-    ): void
-    refs: ObjectOf<NodesRef & ComponentIns>
-    asyncRefs: ObjectOf<Promise<NodesRef & ComponentIns>> // 字节小程序特有
-    nextTick: (fn: () => void) => void
-    forceUpdate: (params?: object, callback?: () => void) => void
-    selectComponent(selector: string): ComponentIns
-    selectAllComponents(selector: string): ComponentIns[]
-    createSelectorQuery(): SelectorQuery
-    createIntersectionObserver(
-      options: {
-        thresholds?: Array<number>
-        initialRatio?: number
-        observeAll?: boolean
-      }
-    ): IntersectionObserver}
-
-function setup(props: Record<string, any>, context: SetupContext): Record<string, any>
-

# 生命周期钩子

可以通过直接导入 on* 函数来注册生命周期钩子:

import { onMounted, onUpdated, onUnmounted, createComponent } from '@mpxjs/core'
-
-createComponent({
-  setup() {
-    onMounted(() => {
-      console.log('mounted!')
-    })
-    onUpdated(() => {
-      console.log('updated!')
-    })
-    onUnmounted(() => {
-      console.log('unmounted!')
-    })
-  }
-})
-

这些生命周期钩子注册函数只能在 setup() 期间同步使用,因为它们依赖于内部的全局状态来定位当前活动的实例 (此时正在调用其 setup() 的组件实例)。 -在没有当前活动实例的情况下,调用它们将会出错。

组件实例的上下文也是在生命周期钩子的同步执行期间设置的,因此,在生命周期钩子内同步创建的侦听器和计算属性也会在组件卸载时自动删除。

新版本的生命周期钩子我们基本上和 Vue 中的生命周期钩子对齐,相较于之前还是有部分生命周期钩子的改动。

# onBeforeMount

Function

在组件布局完成后执行,refs 相关的前置工作在该钩子中执行。

# onMounted

Function

在组件布局完成后执行,refs 可以直接获取。

# onBeforeUpdate

Function

在数据发生改变后,组件/页面更新之前被调用。这里适合在现有组件/页面将要被更新之前访问它, -比如移除某个手动添加的监听器,或者获取某个元素更新前的高度。

# onUpdated

Function

在数据更改导致的页面/组件重新渲染和更新完毕之后被调用。

注意,onUpdated 不会保证所有的子组件也都被重新渲染完毕。如果你希望等待整个视图都渲染完毕,可以在 onUpdated 内部使用 nextTick。

# onBeforeUnmount

Function

在卸载组件/页面实例之前调用。在这个阶段,实例仍然是完全正常的。

# onUnmount

Function

卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除。

# onLoad

Function

小程序页面 onLoad 事件,监听页面加载。

# onShow

Function

小程序页面 onShow 事件,监听页面展示。

# onHide

Function -小程序页面 onHide 事件,监听页面隐藏。

# onResize

Function

小程序页面 onResize 事件,页面尺寸改变时触发。

# onPullDownRefresh

Function

小程序监听用户下拉刷新事件。详细介绍 (opens new window)

# onReachBottom

Function

小程序监听用户上拉触底事件。详细介绍 (opens new window)

# onShareAppMessage

Function

小程序监听用户点击页面内转发按钮(button 组件 open-type="share")或右上角菜单“转发”按钮的行为,并自定义转发内容。详细介绍 (opens new window)

# onShareTimeline

Function

小程序监听右上角菜单“分享到朋友圈”按钮的行为,并自定义分享内容。详细介绍 (opens new window)

注意: 仅微信小程序支持

# onAddToFavorites

Function

小程序监听用户点击右上角菜单“收藏”按钮的行为,并自定义收藏内容。详细介绍 (opens new window)

注意: 仅微信小程序支持

# onPageScroll

Function

小程序监听用户滑动页面事件。详细介绍 (opens new window)

# onTabItemTap

Function

点击 tab 时触发。详细介绍 (opens new window)

# onSaveExitState

Function

每当小程序可能被销毁之前,页面回调函数 onSaveExitState 会被调用,可以进行退出状态的保存。详细介绍 (opens new window)

注意: 仅微信小程序支持

# onServerPrefetch

  • 类型: Function
  • 详细:

SSR渲染定制钩子,在服务端渲染期间被调用,可以实现在服务端进行数据预取。

注意: 仅 web 环境支持

# getCurrentInstance

getCurrentInstance 支持访问内部组件实例。

  • 注意:

getCurrentInstance 只暴露给高阶使用场景,典型的比如在库中。强烈反对在应用的代码中使用 getCurrentInstance。请不要把它当作在组合式 API 中获取 this 的替代方案来使用。

getCurrentInstance 只能在 setup 或生命周期钩子中调用。

# useI18n

点击查看详情

# useFetch

点击查看详情

- - - diff --git a/docs-vuepress/.vuepress/dist/api/directives.html b/docs-vuepress/.vuepress/dist/api/directives.html deleted file mode 100644 index 655df54106..0000000000 --- a/docs-vuepress/.vuepress/dist/api/directives.html +++ /dev/null @@ -1,434 +0,0 @@ - - - - - - 模板指令 | Mpx框架 - - - - - - - - - -

# 模板指令

# wx:if

any

根据表达式的值的 truthiness (opens new window) 来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。 注意:如果元素是 <block/>, 注意它并不是一个组件,它仅仅是一个包装元素,不会在页面中做任何渲染,只接受控制属性

DANGER

当和 wx:if 一起使用时,wx:for 的优先级比 wx:if 更高。详见列表渲染教程

参考: 条件渲染 - wx:if

# wx:elif

any

前一兄弟元素必须有 wx:ifwx:elif。表示 wx:if 的“ wx:elif 块”,可以链式调用。

<view wx:if="{{type === 'A'}}">
-  A
-</view>
-<view wx:elif="{{type === 'B'}}">
-  B
-</view>
-<view wx:elif="{{type === 'C'}}">
-  C
-</view>
-<view wx:else>
-  Not A/B/C
-</view>
-

参考: 条件渲染 - wx:elif

# wx:else

不需要表达式,前一兄弟元素必须有 wx:ifwx:elif

wx:if 或者 wx:elif 添加 wx:else

<view wx:if="{{type === 'A'}}">
-  A
-</view>
-<view wx:else>
-  Not A
-</view>
-

参考: 条件渲染 - wx:else

# wx:for

Array | Object | number | string

在组件上使用 wx:for 控制属性绑定一个数组,即可使用数组中各项的数据重复渲染该组件。默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item

<view wx:for="{{array}}">
-  {{ index }}: {{ item.message }}
-</view>
-
-// 0: foo
-// 1: bar
-
Page({
-  data: {
-    array: [{
-      message: 'foo'
-    }, {
-      message: 'bar'
-    }]
-  }
-})
-

wx:for 的默认行为会尝试原地修改元素而不是移动它们。要强制其重新排序元素,你需要用特殊 attribute key 来提供一个排序提示:

<view wx:for="{{array}}" wx:key="id">
-  {{ item.text }}
-</view>
-
-// foo
-// bar
-
Page({
-  data: {
-    array: [{
-      id: 1, text: 'foo'
-    }, {
-      id: 2, text: 'bar'
-    }]
-  }
-})
-

DANGER

当和 wx:if 一起使用时,wx:for 的优先级比 wx:if 更高。详见列表渲染教程

wx:for 的详细用法可以通过以下链接查看教程详细说明。

参考: 列表渲染 - wx:for

# wx:for-index

string

使用 wx:for-index 可以指定数组当前下标的变量名:

<view wx:for="{{array}}" wx:key="id" wx:for-index="idx">
-  {{ idx }}: {{ item.text }}
-</view>
-
-// 0: foo
-// 1: bar
-
Page({
-  data: {
-    array: [{
-      id: 1, text: 'foo'
-    }, {
-      id: 2, text: 'bar'
-    }]
-  }
-})
-

参考: 列表渲染 - wx:for-index

# wx:for-item

string

使用 wx:for-item 可以指定数组当前元素的变量名:

<view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="i">
-  <view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9]}}" wx:for-item="j">
-    <view wx:if="{{i <= j}}">
-      {{i}} * {{j}} = {{i * j}}
-    </view>
-  </view>
-</view>
-

参考: 列表渲染 - wx:for-item

# wx:key

number | string

如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态,需要使用 wx:key 来指定列表中项目的唯一的标识符。 -注意:如不提供 wx:key,会报一个 warning, 如果明确知道该列表是静态,或者不必关注其顺序,可以选择忽略

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

常见的用例是结合 wx:for

<view wx:for="{{array}}" wx:key="id">
-  {{ item.text }}
-</view>
-

参考: 列表渲染 - wx:for

# wx:class

绑定HTML Class: 类似vue的class绑定

#对象用法

我们可以传给 wx:class 一个对象,以动态地切换 class:

<view wx:class="{{ {active: isActive} }}">
-  这是一段测试文字
-</view>
-

你可以在对象中传入更多字段来动态切换多个 class。此外,wx:class 指令也可以与普通的 class attribute 共存。

<view class="static" wx:class="{{ {active: isActive, text-danger: hasError} }}">
-  这是一段测试文字
-</view>
-
<script>
-  import {createComponent} from '@mpxjs/core'
-
-  createComponent({
-    data: {
-     isActive: true,
-     hasError: false
-    }
-  })
-</script>
-

渲染为:

<view class="static active">
-  这是一段测试文字
-</view>
-

注意:由于微信的限制,wx:class 中的 key 值不能使用引号(如: { 'my-class-name': xx })

绑定的数据对象不必内联定义在模板里:

<view wx:class="{{ classObject }}">
-  这是一段测试文字
-</view>
-
<script>
-  import {createComponent} from '@mpxjs/core'
-
-  createComponent({
-    data: {
-      classObject: {
-        active: true,
-        'text-danger': false
-      }
-    }
-  })
-</script>
-

我们也可以在这里绑定一个返回对象的计算属性。

如果你也想根据条件切换列表中的 class,可以用三元表达式:

<view wx:class="{{ isActive ? 'active' : '' }}">
-  这是一段测试文字
-</view>
-

#数组用法

我们可以把一个数组传给 wx:class,以应用一个 class 列表:

<view wx:class="{{[activeClass, errorClass]}}">
-  这是一段测试文字
-</view>
-
<script>
-  import {createComponent} from '@mpxjs/core'
-
-  createComponent({
-    data: {
-      activeClass: 'active',
-      errorClass: 'text-danger'
-    }
-  })
-</script>
-

渲染为:

<view wx:class="active text-danger">
-  这是一段测试文字
-</view>
-

参考: 类名样式绑定 - 类名绑定

# wx:style

wx:style 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case) 来命名:

<view wx:style="color: {{activeColor}}; font-size: {{fontSize}}px; fontWeight: bold">
-  这是一段测试文字
-</view>
-
<script>
-  import {createComponent} from '@mpxjs/core'
-
-  createComponent({
-    data: {
-      activeColor: 'red',
-      fontSize: 30
-    }
-  })
-</script>
-

直接绑定到一个样式对象通常更好,这会让模板更清晰:

<view wx:style="{{styleObject}}">
-  这是一段测试文字
-</view>
-
<script>
-  import {createComponent} from '@mpxjs/core'
-
-  createComponent({
-    data: {
-      styleObject: {
-        color: 'red',
-        fontWeight: 'bold'
-      }
-    },
-  })
-</script>
-

同样的,对象语法常常结合返回对象的计算属性使用。

wx:style 的数组语法可以将多个样式对象应用到同一个元素上

<view wx:style="{{[baseStyles, overridingStyles]}}">
-  这是一段测试文字
-</view>
-

参考: 类名样式绑定 - 样式绑定

# wx:model

除了小程序原生指令之外,mpx 基于input事件提供了一个指令 wx:model, 用于双向绑定。

<template>
-  <view>
-    <input wx:model="{{val}}"/>
-    <input wx:model="{{test.val}}"/>
-    <input wx:model="{{test['val']}}"/>
-  </view>
-</template>
-
-<script>
-  import {createComponent} from '@mpxjs/core'
-  createComponent({
-    data: {
-      val: 'test',
-      test: {
-        val: 'xxx'
-      }
-    }
-  })
-</script>
-

wx:model并不会影响相关的事件处理函数,比如像下面这样:

<input wx:model="{{inputValue}}" bindinput="handleInput"/>
-

参考: 双向绑定

# wx:model-prop

wx:model 默认使用 value 属性传值,使用 wx:model-prop 定义 wx:model 指令对应的属性;

# wx:model-event

wx:model 默认监听 input 事件,可以使用 wx:model-event 定义 wx:model 指令对应的事件;

父组件

<template>
-  <customCheck wx:model="{{checked}}" wx:model-prop="checkedProp" wx:model-event="checkedChange"></customCheck>
-</template>
-
-<script>
-  import {createPage} from '@mpxjs/core'
-  createPage({
-    data: {
-      checked: true
-    }
-  })
-</script>
-<script type="application/json">
-  {
-    "usingComponents": {
-      "customCheck": "./customCheck"
-    }
-  }
-</script>
-
-

子组件:(customCheck.mpx)

<template>
-  <view bindtap="handleTap" class="viewProps">{{checkedProp}}</view>
-</template>
-
-<style lang="stylus">
-  .viewProps {
-    width 100px
-    height 100px
-    color #000
-  }
-</style>
-
-<script>
-  import {createComponent} from '@mpxjs/core'
-  createComponent({
-    properties: {
-      checkedProp: Boolean
-    },
-    methods: {
-      handleTap () {
-        // 这里第二个参数为自定义事件的detail,需要以下面的形式传递值以保持与原生组件对齐
-        this.triggerEvent('checkedChange', {
-          value: !this.checkedProp
-        })
-      }
-    }
-  })
-</script>
-

如示例,当子组件被点击时,父组件的checked数据会发生变化

注意:由于微信限制,如果事件名使用横线分割('checked-change'),将不可以再使用该特性

# wx:model-value-path

指定 wx:model 双向绑定时的取值路径; -并非所有的组件都会按微信的标注格式 event.detail.value 来传值,例如 vant 的 input 组件,值是通过抛出 event.detail 本身传递的,这时我们可以使用 wx:model-value-path="[]" 重新指定取值路径。

<vant-field wx:model-value-path="[]" wx:model="{{a}}"></vant-field>
-

# wx:model-filter

在使用 wx:model 时我们可能需要像 Vue 的 .trim.lazy 这样的修饰符来对双向数据绑定的数据进行过滤和修饰;Mpx 通过增强指令 wx:model-filter 可以实现这一功能; -该指令可以绑定内建的 filter 或者自定义的 filter 方法,该方法接收过滤前的值,返回过滤操作后的值。

例如我们希望拿到的 input 元素中的数据是经过 trim 的。示例:

当然,Mpx 已经内置了 trim 过滤器;可以通过 wx:model-filter="trim" 直接使用;

<template>
-  <view class="cover-page">
-    <view>
-       <!-- wx:model-filter 过滤wx:model 的值-->
-      <input type="text" wx:model-filter="trimSpace" wx:model="{{filterData}}" />
-      <view >{{filterData.length}}</view>
-    </view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-  createPage({
-    data: {
-      filterData: 'model-filter'
-    },
-    trimSpace (val) {
-      // wx:model-filter 绑定的 filter 方法
-      return typeof val === 'string' && val.trim()
-    },
-    methods: {
-    }
-  })
-</script>
-

filter 方法除可以是和 methods 平级的方法,还可以是 methods 中的方法。

<template>...</template>  
-<script>
-  import { createPage } from '@mpxjs/core'
-  createPage({
-    data: {
-      filterData: 'model-filter'
-    },
-    methods: {
-      trimSpace (val) {
-        // wx:model-filter 支持将过滤器方法定义成 methods 中的方法
-        return typeof val === 'string' && val.trim()
-      }
-    }
-  })
-</script>
-

# wx:ref

string

Mpx提供了 wx:ref=xxx 来更方便获取 WXML 节点信息的对象。在JS里只需要通过this.$refs.xxx 即可获取节点。

  <view wx:ref="tref">
-    123
-  </view>
-
-  <script>
-    Page({
-      ready () {
-        this.$refs.tref.fields({size: true}, function (res) {
-          console.log(res)
-        }).exec()
-      }
-    })
-  </script>
-

参考: 获取组件实例 - wx:ref

# wx:show

boolean

wx:if 所不同的是不会移除节点,而是设置节点的 styledisplay: none

<view wx:show="{{show}}">
-  123
-</view>
-
Page({
-  data: {
-    show: false
-  }
-})
-

# bind

string

bind + (:?) + eventType 作为属性值

比如:bindtap

<view bindtap="tapTest"> Click me! </view>
-<view bind:tap="tapTest1(testVal, $event)"> Click me! </view>
-
Page({
-  methods: {
-    tapTest () {
-      console.log('Clicked!')
-    },
-    tapTest1 (val, event) {
-      console.log(val, event)
-    }
-  }
-})
-

Mpx做了增强的内联传参能力以及具体有哪些事件类型参考下方

参考: 事件处理 - bind

# catch

string

catch + (:?) + eventType 作为属性值

bind 外,也可以用 catch 来绑定事件。与 bind 不同,catch 会阻止事件向上冒泡。

<view id="outer" bindtap="handleTap1">
-  outer view
-  <view id="middle" catchtap="handleTap2">
-    middle view
-    <view id="inner" bindtap="handleTap3">
-      inner view
-    </view>
-  </view>
-</view>
-
Page({
-  methods: {
-    handleTap1 () {
-      console.log('outer')
-    },
-    handleTap2 () {
-      console.log('middle')
-    },
-    handleTap3 () {
-      console.log('inner')
-    }
-  }
-})
-// 通过几个操作看出被catchtap的middle view阻止了向上冒泡
-// click outer
-// outer
-
-// click middle
-// middle
-
-// click inner
-// inner
-// middle
-

参考: 事件处理 - catch

# capture-bind

string

capture-bind + (:?) + eventType 作为属性值

capture-bind要在bind之前执行,是因为事件是先捕获后冒泡,注意:仅触摸类事件支持捕获阶段

  <view id="outer" bind:touchstart="handleTap1" capture-bind:touchstart="handleTap2">
-    outer view
-    <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
-      inner view
-    </view>
-  </view>
-

点击inner view的调用顺序是(handleTap)2、4、3、1

参考: 事件处理 - capture-bind

# capture-catch

string

capture-catch + (:?) + eventType 作为属性值

capture-catch中断捕获阶段和取消冒泡阶段

<view id="outer" bind:touchstart="handleTap1" capture-catch:touchstart="handleTap2">
-  outer view
-  <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
-    inner view
-  </view>
-</view>
-

点击inner view仅执行handleTap2

参考: 事件处理 - capture-catch

# @mode

type mode = 'wx' | 'ali' | 'qq' | 'swan' | 'tt' | 'web' | 'qa'

跨平台输出场景下,Mpx 框架允许用户在组件上使用 @ 和 | 符号来指定某个节点或属性只在某些平台下有效。

<button
-  open-type@wx|swan="getUserInfo"
-  bindgetuserinfo@wx|swan="getUserInfo"
-  open-type@ali="getAuthorize"
-  scope@ali="userInfo"
-  onTap@ali="onTap">
-  获取用户信息
-</button>
-

在上方示例中,开发者可以便捷的设置在支付宝小程序与微信和百度小程序等平台分别生效的 open-type 属性, -以及事件绑定或其他属性。假设当前 srcMode 为 wx,目标平台为 ali,则输出产物为:

<button
-  open-type="getAuthorize"
-  scope="userInfo"
-  onTap="onTap">
-  获取用户信息
-</button>
-

同时,该指令也可以作用在单个节点上,但需要注意的是,该指令作用在单个节点时,节点仅在目标平台输出,同时节点自身属性不会进行跨平台语法转换,不过其子节点不受影响。

<!--当srcMode为wx,跨平台输出ali时-->
-<!--错误写法-->
-<view @ali bindtap="someClick">
-    <view wx:if="{{flag}}">text</view>
-</view>
-<!--正确写法-->
-<view @ali onTap="someClick">
-    <view wx:if="{{flag}}">text</view>
-</view>
-

# @_mode

type _mode = '_wx' | '_ali' | '_qq' | '_swan' | '_tt' | '_web' | '_qa'

有时开发者期望使用 @mode 这种方式仅控制节点的展示,保留节点属性的平台转换能力,为此 Mpx 实现了一个隐式属性条件编译能力。

<!--srcMode为 wx,输出 ali 时,bindtap 会被正常转换为 onTap-->
-<view @_ali bindtap="someClick">test</view>
-

在对应的平台前加一个_,例如@_ali、@_swan、@_tt等,使用该隐式规则仅有条件编译能力,节点属性语法转换能力依旧。

# @env

string

跨平台输出场景下,除了 mode 平台场景值,Mpx 框架还提供自定义 env 目标应用,来实现在不同应用下编译产出不同的代码。

关于 env 的详细介绍可以点击查看

跨平台输出使用 env 与 mode 一样支持文件纬度、区块纬度、节点纬度、属性纬度等条件编译,这里我们仅介绍下节点和属性纬度的指令模式使用,env 与 mode 可以组合使用。

env 属性维度条件编译与 mode 的用法大致相同,使用 : 符号与 mode 和其他 env 进行串联,与 mode 组合使用格式形如 attr@mode:env:env|mode:env,为了不与 mode 混淆,当条件编译中仅存在 env 条件时,也需要添加 : 前缀,形如 attr@:env。

<!--open-type 属性仅在百度平台且应用 env 为 didi 时保留-->
-<button open-type@swan:didi="getUserInfo">获取用户信息</button>
-<!--open-type 属性在应用 env 为 didi 的任意小程序平台保留-->
-<button open-type@:didi="getUserInfo">获取用户信息</button>
-<!--open-type 属性在微信平台且应用 env 为 didi 或 qingju 时保留-->
-<button open-type@wx:didi:qingju="getUserInfo">获取用户信息</button>
-

env 也可在单个节点上进行条件编译:

<!--整个节点在应用 env 为 didi时进行保留-->
-<view @:didi>this is a  view component</view>
-

需要注意的时,如果只声明了 env,没有声明 mode,跨平台输出时框架对于节点属性默认会进行转换:

<!--srcMode为wx,跨平台输出ali时,bindtap会被转为onTap-->
-<view @:didi bindtap="someClick">this is a  view component</view>
-<view bindtap@:didi ="someClick">this is a  view component</view>
-

# mpxTagName

string

跨平台输出时,有时开发者不仅需要对节点属性进行条件编译,可能还需对节点标签进行条件编译。

为此,Mpx 框架支持了一个特殊属性 mpxTagName,如果节点存在这个属性,在最终输出时将节点标签修改为该属性的值,配合属性维度条件编译,即可实现对节点标签进行条件编译,例如在百度环境下希望将某个 view 标签替换为 cover-view,我们可以这样写:

<view mpxTagName@swan="cover-view">will be cover-view in swan</view>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/api/extend.html b/docs-vuepress/.vuepress/dist/api/extend.html deleted file mode 100644 index e6fef447fb..0000000000 --- a/docs-vuepress/.vuepress/dist/api/extend.html +++ /dev/null @@ -1,365 +0,0 @@ - - - - - - 周边拓展 | Mpx框架 - - - - - - - - - -

# 周边拓展

# mpx-fetch

mpx-fetch提供了一个实例xfetch ,该实例包含以下api

# fetch(config)

正常的promisify风格的请求方法

  • {Object} config

    config 可指定以下属性:

    • url

      string

      设置请求url

    • method

      string

      设置请求方式,默认为GET

    • data

      object

      设置请求参数

    • params

      object

      设置请求参数,参数会以 Query String 的形式进行传递

    • header

      object

      设置请求的 header,header 中不能设置 Referer。 -content-type 默认为 application/json

    • timeout

      number

      单位为毫秒。若不传,默认读取app.json文件中__networkTimeout属性。 对于超时的处理可在 catch 方法中进行

    • emulateJSON

      boolean

      设置为 true 时,等价于 header = {'content-type': 'application/x-www-form-urlencoded'}

    • usePre

      boolean

      预请求开关,若设置为 true,则两次请求间隔在有效期内且请求参数和请求方式对比一致的情况下,会返回上一次的请求结果

    • cacheInvalidationTime

      number

      预请求缓存有效时长,单位 ms,默认为 5000ms。当两次请求时间间隔超过设置时长后再发起二次请求时,上一次的请求缓存会失效然后重新发起请求

    • ignorePreParamKeys

      array | string

      在判断缓存请求是否可用对比前后两次请求参数时,默认对比的是 options 传入的所有参数(包括 params 和 data )。但在具体业务场景下某些参数不一致时的缓存结果依旧可使用(比如参数中带有时间戳),所以提供 ignorePreParamKeys 来设置对比参数过程中可忽略的参数的 key,支持字符串数组和字符串(字符串传多个 key 时使用英文逗号分隔)类型。 -配置后在进行参数对比时,不会对比在 ignorePreParamKeys 设置的参数。

import mpx from '@mpxjs/core'
-import mpxFetch from '@mpxjs/fetch'
-mpx.use(mpxFetch)
-// 第一种访问形式
-mpx.xfetch.fetch({
-    url: 'http://xxx.com',
-    method: 'POST',
-    params: {
-        age: 10
-    },
-    data: {
-        name: 'test'
-    },
-    header: {
-      'content-type': 'application/x-www-form-urlencoded',
-    },
-    emulateJSON: true,
-    usePre: true,
-    cacheInvalidationTime: 3000,
-    ignorePreParamKeys: ['timestamp']
-}).then(res => {
-	console.log(res.data)
-})
-
-mpx.createApp({
-	onLaunch() {
-		// 第二种访问形式
-		this.$xfetch.fetch({url: 'http://test.com'})
-	}
-})
-

# CancelToken

命名导出,用于创建一个取消请求的凭证。

import { CancelToken } from '@mpxjs/fetch'
-const cancelToken = new CancelToken()
-mpx.xfetch.fetch({
-	url: 'http://xxx.com',
-	data: {
-		name: 'test'
-	},
-	cancelToken: cancelToken.token
-})
-cancelToken.exec('手动取消请求') // 执行后请求中断,返回abort fail
-

# XFetch

命名导出,用于创建一个新的mpx-fetch实例进行独立使用

interface FetchOptions{
-    useQueue: boolean // 是否开启队列功能
-    proxy: boolean // 是否开启代理功能
-}
-
import { XFetch } from '@mpxjs/fetch'
-const newFetch = new XFetch(options) // 生成新的mpx-fetch实例
-

# interceptors

实例属性,用于添加拦截器,包含两个属性,request & response

mpx.xfetch.interceptors.request.use(function(config) {
-    console.log(config)
-    // 也可以返回promise
-    return config
-})
-mpx.xfetch.interceptors.response.use(function(res) {
-    console.log(res)
-    // 也可以返回promise
-    return res
-})
-

# proxy 代理

# setProxy

配置代理项,可以传入一个数组或者一个对象,请求会按设置的规则进行代理

  • 参数:

    {Array | Object}

    • test

      object

      • url

        string

        全路径匹配,规则可以参考path-to-regexp (opens new window),也可参考下面的简单示例。

        WARNING

        如果设置了此项,则 protocol、host、port、path 规则不再生效。此项支持 path-to-regexp 匹配,protocol、host、port、path 为全等匹配。

      • protocol

        string

        待匹配的协议头

      • host

        string

        不包含端口的 host

      • port

        string

        待匹配的端口

      • path

        string

        待匹配的路径

      • params

        object

        同时匹配请求中的 paramsquery

      • data

        object

        匹配请求中的 data

      • header

        object

        匹配请求中的 header

      • method

        Method | Method[]

        匹配请求方法,不区分大小写,可以传一个方法,也可以传一个方法数组

      • custom

        function

        自定义匹配规则,参数会注入原始请求配置,结果需返回 truefalse

        WARNING

        如果设置了此项,匹配结果以此项为准,以上规则均不再生效。

    • proxy

      object

      • url

        string

        代理的 url

      • protocol

        string

        修改原请求的协议头

      • host

        string

        代理的 host,不包含端口号

      • port

        string

        修改端口号

      • path

        string

        修改原请求路径

      • params

        object

        合并原请求的 params

      • data

        object

        合并原请求的 data

      • header

        object

        合并原请求的 header

      • method

        Method

        替换原请求的方法

      • custom

        function

        自定义代理规则,会注入两个参数,第一个是上一个匹配规则处理后的请求配置,第二个是 match 的参数对象,结果需返回要修改的请求配置对象。

        WARNING

        如果设置了此项,最终代理配置将以此项为准,其他配置规则均不再生效。

    • waterfall

      boolean

      默认为 false,为 false 时,命中当前规则处理完就直接返回;为 true 时,命中当前匹配规则处理完成后将结果传递给下面命中匹配规则继续处理。

mpx.xfetch.setProxy([{
-    test: { // 此项匹配之后,会按下面 proxy 配置的修改请求配置
-		header: {
-            'content-type': 'application/x-www-form-urlencoded'
-        },
-        method: 'get',
-        params: {
-            a: 1
-        },
-        data: {
-            test1: 'abc'
-        }
-	},
-	proxy: {
-		header: {
-			env: 'env test'
-		},
-		params: {
-			b: 2
-        },
-        data: {
-            test2: 'cba'
-        }
-	},
-	waterfall: true // 为 true 时会将此次修改后的请求配置继续传递给下面的规则处理
-}, {
-    test: {// 可以分别单独匹配 protocol、host、port、path;代理规则同样
-        protocol: 'http:',
-		host: 'mock.didi.com',
-		port: '',
-		path: '/mock/test'
-    },
-    proxy: {
-        host: 'test.didi.com',
-        port: 8888
-    },
-    waterfall: true
-}, {
-    test: { // 自定义匹配规则
-        custom (config) { // config 为原始的请求配置
-            // 自定义匹配逻辑
-			if (xxx) {
-				return true
-			}
-			return false
-		}
-    },
-    proxy: { // 自定义代理配置
-        custom (config, params) {
-			// config 为上面匹配后修改过的请求配置
-            // params 为 match 后的参数对象
-            // 返回需要修改的请求配置对象
-			return {
-                params: {
-                    c: 3
-                },
-				data: {
-					test3: 'aaa'
-				}
-			}
-		}
-    },
-    waterfall: true
-}, {
-    test: {
-        // : 可以匹配目标项,并将 match 结果返回到代理 custom 函数中
-        // :和?都属于保留符号,若不想做匹配时需用 '\\' 转义
-        // (.*)为全匹配
-        url: ':protocol\\://mock.didi.com/mock/:id/oneapi/router/forum/api/v1/(.*)',
-        method: ['get', 'post']
-    },
-    proxy: {
-        url: 'https://127.0.0.1:8080/go/into/$id/api/v2/abcd' // '$'项在代理生效后会替换匹配规则中的':'项
-    },
-    waterfall: false // false 时不会继续匹配后面的规则
-}])
-

# prependProxy

向前追加代理规则

mpx.xfetch.prependProxy({
-	test: {},
-	proxy: {},
-	waterfall: true
-})
-

# appendProxy

向后追加代理规则

mpx.xfetch.appendProxy({
-	test: {},
-	proxy: {},
-	waterfall: true
-})
-

# getProxy

查看已有的代理配置

console.log(mpx.xfetch.getProxy())
-

# clearProxy

解除所有的代理配置

mpx.xfetch.clearProxy()
-

# useFetch

useFetch(options?: FetchOptions):xfetch
-

在组合式 API 中使用,用来获取 @mpxjs/fetch 插件的 xfetch 实例,等用于 mpx.xfetch。 关于 xfetch 实例的详细介绍,请点击查看

此外该方法可选择传入 options 参数,若传入参数,则会创建一个新的 XFetch 实例返回,若不传入参数,则默认将全局 xfetch 实例返回。

// app.mpx
-import mpx from '@mpxjs/core'
-import mpxFetch from '@mpxjs/fetch'
-mpx.use(mpxFetch)
-
-// script-setup.mpx
-import { useFetch } from '@mpxjs/fetch'
-useFetch().fetch({
-  url: 'http://xxx.com',
-  method: 'POST',
-  params: {
-    age: 10
-  },
-  data: {
-    name: 'test'
-  },
-  emulateJSON: true,
-  usePre: true,
-  cacheInvalidationTime: 3000,
-  ignorePreParamKeys: ['timestamp']
-}).then(res => {
-  console.log(res.data)
-})
-
  • 注意: options 参数同 XFetch 章节。

# api-proxy

Mpx目前已经支持的API转换列表,供参考

方法/平台 wx ali web RN
getSystemInfo
getSystemInfoSync
nextTick
showToast
hideToast
showModal
showLoading
hideLoading
showActionSheet
showNavigationBarLoading
hideNavigationBarLoading
setNavigationBarTitle
setNavigationBarColor
request
downloadFile
uploadFile
setStorage
setStorageSync
removeStorage
removeStorageSync
getStorage
getStorageSync
getStorageInfo
getStorageInfoSync
clearStorage
clearStorageSync
saveImageToPhotosAlbum
previewImage
compressImage
chooseImage
getLocation
saveFile
removeSavedFile
getSavedFileList
getSavedFileInfo
addPhoneContact
setClipboardData
getClipboardData
setScreenBrightness
getScreenBrightness
makePhoneCall
stopAccelerometer
startAccelerometer
stopCompass
startCompass
stopGyroscope
startGyroscope
scanCode
login
checkSession
getUserInfo
requestPayment
createCanvasContext
canvasToTempFilePath
canvasPutImageData
canvasGetImageData
createSelectorQuery
onWindowResize
offWindowResize
arrayBufferToBase64
base64ToArrayBuffer
connectSocket
getNetworkType
onNetworkStatusChange
offNetworkStatusChange

# webview-bridge

Mpx 支持小程序跨平台后,多个平台的小程序里都提供了 webview 组件,webview 打开的 H5 页面可以通过小程序提供的 API 来与小程序通信以及调用一些小程序的能力,但是各家小程序对于 webview 提供的API是不一样的。

比如微信的 webview 打开的 H5 页面里是通过调用 wx.miniProgram.navigateTo 来跳转到原生小程序页面的,而在支付宝是通过调用 my.navigateTo 来实现跳转的,那么我们开发 H5 时候为了让 H5 能适应各家小程序平台就需要写多份对应逻辑。

为解决这个问题,Mpx 提供了抹平平台差异的bridge库:@mpxjs/webview-bridge。

安装:

npm install @mpxjs/webview-bridge
-

使用:

import mpx from '@mpxjs/webview-bridge'
-mpx.navigateBack()
-mpx.env // 输出:wx/qq/ali/baidu/tt
-mpx.checkJSApi()
-

cdn地址引用:

<!-- 开发环境版本,方便调试 -->
-<script src="https://dpubstatic.udache.com/static/dpubimg/D2JeLyT0_Y/2.2.43.webviewbridge.js"></script>
-
-<!-- 生产环境版本,压缩了体积 -->
-<script src="https://dpubstatic.udache.com/static/dpubimg/PRg145LZ-i/2.2.43.webviewbridge.min.js"></script>
-
-
-<!-- 同时支持 ES Module 引入的 -->
-// index.html
-<script type="module" src="https://dpubstatic.udache.com/static/dpubimg/6MQOo-ocI4/2.2.43.webviewbridge.esm.browser.min.js"></script>
-// main.js
-import mpx from "https://dpubstatic.udache.com/static/dpubimg/6MQOo-ocI4/2.2.43.webviewbridge.esm.browser.min.js"
-
-//ES Module 开发版本地址: https://dpubstatic.udache.com/static/dpubimg/cdhpNhmWmJ/2.2.43.webviewbridge.esm.browser.js
-

基础方法提供:

方法/平台 wx qq ali baidu tt
navigateTo
navigateBack
switchTab
reLaunch
redirectTo
getEnv
postMessage
getLoadError
onMessage

扩展方法提供:

方法/平台 wx qq ali baidu tt
checkJSApi
chooseImage
previewImage
uploadImage
downloadImage
getLocalImgData
startRecord
stopRecord
onVoiceRecordEnd
playVoice
pauseVoice
stopVoice
onVoicePlayEnd
uploadVoice
downloadVoice
translateVoice
getNetworkType
openLocation
getLocation
stopSearchBeacons
onSearchBeacons
scanQRCode
chooseCard
addCard
openCard
alert
showLoading
hideLoading
setStorage
getStorage
removeStorage
clearStorage
getStorageInfo
startShare
tradePay
onMessage

WARNING

这个库仅提供给 H5 使用,请勿在小程序环境引入

# size-report

Mpx框架项目包体积可以进行分组、分包、页面、冗余Npm包等维度的分析和对比,详细请见

# 插件配置项

  • server

    object

    本地可视化服务相关配置

  • filename

    string

    构建生成的包体积详细输出文件地址

  • threshold

    object

    配置项目总体积和分包体积阈值,包含两个字段,size 为项目总体积阈值,packages 为分包体积阈值

    {
    -   size: '16MB', // 项目总包体积限额 16M
    -   packages: '2MB' // 项目每个分包体积限额 2M
    -}
    -
  • groups

    Array<object>

    配置体积计算分组,以输入分组为维度对体积进行分析,当没有该配置时结果中将不会包含分组体积信息

    • name

      string

      分组名称

    • threshold

      string | object

      分组相关体积阈值,若不配置则该分组不校验体积阈值,同时也支持对分组中占各分包体积阈值

      // 分组体积限额 500KB
      -threshold: '500KB'
      -// 或者如下方,分组体积限额500KB,分组占主包体积限额160KB
      -threshold: {
      -  size: '500KB',
      -  packages: {
      -    main: '160KB'
      -  }
      -}
      -
    • entryRules

      object

      配置分组 entry 匹配规则,小程序中所有的页面和组件都可被视为 entry

      • include: 包含符合条件的入口文件,默认为空数组,规则数组中支持函数、正则、字符串
      • exclude: 剔除符合条件的入口文件,默认为空数组,规则数组中支持函数、正则、字符串
      include: [/@someGroup\/some-npm-package/],
      -exclude: [/@someGroup\/some-two-pack/]
      -
    • noEntryRules

      object

      配置计算分组中纯 js 入口引入的体积(不包含组件和页面)

      • include: 包含符合条件的 js 文件,默认为空数组,规则数组中支持函数、正则、字符串
      • exclude: 剔除符合条件的 js 文件,默认为空数组,规则数组中支持函数、正则、字符串
      include: [/@someGroup\/some-npm-package/],
      -exclude: [/@someGroup\/some-two-pack/]
      -
  • reportPages

    boolean

    是否收集页面维度体积详情,默认 false

  • reportAssets

    boolean

    是否收集资源维度体积详情,默认 false

  • reportRedundance

    boolean

    是否收集冗余资源,默认 false

  • showEntrysPackages

    Array<string>

    展示某些分包资源的引用来源信息,例如 ['main'] 为查看主包资源的引用来源信息,默认为 []

配置使用示例:

{
-  // 本地可视化服务相关配置
-  server: {
-    enable: true, // 是否启动本地服务,非必填,默认 true
-    autoOpenBrowser: true, // 是否自动打开可视化平台页面,非必填,默认 true
-    port: 0, // 本地服务端口,非必填,默认 0(随机端口)
-    host: '127.0.0.1', // 本地服务host,非必填
-  },
-  // 体积报告生成后输出的文件地址名,路径相对为 dist/wx 或者 dist/ali
-  filename: '../report.json',
-  // 配置阈值,此处代表总包体积阈值为 16MB,分包体积阈值为 2MB,超出将会触发编译报错提醒,该报错不阻断构建
-  threshold: {
-    size: '16MB',
-    packages: '2MB'
-  },
-  // 配置体积计算分组,以输入分组为维度对体积进行分析,当没有该配置时结果中将不会包含分组体积信息
-  groups: [
-    {
-      // 分组名称
-      name: 'vant',
-      // 配置分组 entry 匹配规则,小程序中所有的页面和组件都可被视为 entry,如下所示的分组配置将计算项目中引入的 vant 组件带来的体积占用
-      entryRules: {
-        include: '@vant/weapp'
-      }
-    },
-    {
-      name: 'pageGroup',
-      // 每个分组中可分别配置阈值,如果不配置则表示
-      threshold: '500KB',
-      entryRules: {
-        include: ['src/pages/index', 'src/pages/user']
-      }
-    },
-    {
-      name: 'someSdk',
-      entryRules: {
-        include: ['@somegroup/someSdk/index', '@somegroup/someSdk2/index']
-      },
-      // 有的时候你可能希望计算纯 js 入口引入的体积(不包含组件和页面),这种情况下需要使用 noEntryRules
-      noEntryRules: {
-        include: 'src/lib/sdk.js'
-      }
-    }
-  ],
-  // 是否收集页面维度体积详情,默认 false
-  reportPages: true,
-  // 是否收集资源维度体积详情,默认 false
-  reportAssets: true,
-  // 是否收集冗余资源,默认 false
-  reportRedundance: true,
-  // 展示某些分包资源的引用来源信息,默认为 []
-  showEntrysPackages: ['main']
-}
-

# i18n

# useI18n

组合式 API 中使用,用来获取 i18n 实例。

参数选项


# locale

Locale

设置语言环境

注意: 只传 locale,不传 messages 属性时不起作用

# fallbackLocale

Locale

预设的语言环境,找不到语言环境时进行回退。

# messages

LocaleMessages

本地化的语言环境信息。

返回实例属性和方法


# locale

WritableComputedRef<Locale>

可响应性的 ref 对象,表示当前 i18n 实例所使用的 locale。

修改 ref 值会对局部或者全局语言集的 locale 进行更改,并触发翻译方法重新执行。

# fallbackRoot

boolean

本地化失败时是否回归到全局作用域。

# getLocaleMessage( locale )

function getLocaleMessage (locale: string): LocaleMessageObject
-

获取语言环境的 locale 信息。

# setLocaleMessage( locale, message )

function setLocaleMessage(locale: Locale, messages: LocaleMessageObject): void
-

设置语言环境的 locale 信息。

# mergeLocaleMessage( locale, message )

function mergeLocaleMessage(locale: Locale, messages: LocaleMessageObject): void
-

将语言环境信息 locale 合并到已注册的语言环境信息中。

# messages

readonly messages: ComputedRef<{
-   [K in keyof Messages]: Messages[K];
-}>;
-
  • 只读

局部或者全局的语言环境信息。

# isGlobal

boolean

是否是全局 i18n 实例。

# t

function t(key: string, choice?: number, values: Array | Object): TranslateResult
-

文案翻译函数

根据传入的 key 以及当前 locale 环境获取对应文案,文案来源是全局作用域还是本地作用域取决于 useI18n 执行时是否传入对应的 messages、locale 等值。

choice 参数可选 ,当传入 choice 时,t 函数的表现为使用复数进行翻译,和老版本中的 tc 函数表现一致。

<template>
-  <view>{{t('car', 1)}}</view>
-  <view>{{t('car', 2)}}</view>
-
-  <view>{{t('apple', 0)}}</view>
-  <view>{{t('apple', 1)}}</view>
-  <view>{{t('apple', 10, {count: 10})}}</view>
-</template>
-
-<script>
-  // 语言环境信息如下:
-  const messages = {
-    en: {
-      car: 'car | cars',
-      apple: 'no apples | one apple | {count} apples'
-    }
-  }
-</script>
-

输入如下:

<view>car</view>
-<view>cars</view>
-
-<view>no apples</view>
-<view>one apple</view>
-<view>10 apples</view>
-

关于复数的更多信息可以点击查看 (opens new window)

values 参数可选 ,如果需要对文案信息即逆行格式化处理,则需要传入 values。

<template>
-  // 模版输出 hello world
-  <view>{{t('message.hello', { msg: 'hello'})}}</view>
-</template>
-<script>
-  import {createComponent, useI18n} from "@mpxjs/core"
-
-  const messages = {
-    en: {
-      message: {
-        hello: '{msg} world'
-      }
-    }
-  }
-  
-  createComponent({
-    setup(){
-        const { t } = useI18n({
-          messages: {
-              'en-US': en
-          }
-        })
-      return {t}
-    }
-  })
-
-</script>
-

# te

function te(key: string): boolean
-

检查 key 是否存在。

- - - diff --git a/docs-vuepress/.vuepress/dist/api/global-api.html b/docs-vuepress/.vuepress/dist/api/global-api.html deleted file mode 100644 index 2fc002be47..0000000000 --- a/docs-vuepress/.vuepress/dist/api/global-api.html +++ /dev/null @@ -1,253 +0,0 @@ - - - - - - 全局 API | Mpx框架 - - - - - - - - - -

# 全局 API

# 全局对象 Mpx

@mpxjs/core 默认导出 mpx 全局实例对象,通过该实例对象我们可以访问部分应用实例 API

# mixin

全局注入mixin方法接收两个参数:mpx.mixin(mixins, options)

  • 第一个参数是要混入的mixins,接收类型 MixinObject|MixinObject[]
  • 第二个参数是为全局混入配置,形如{types:string|string[], stage:number},其中types用于控制mixin注入的范围,可选值有'app'|'page'|'component'stage用于控制注入mixin的插入顺序,当stage为负数时,注入的mixin会被插入到构造函数配置中的options.mixins之前,数值越小约靠前,反之当stage为正数时,注入的mixin会被插入到options.mixins之后,数值越大越靠后。

所有mixin中生命周期的执行均先于构造函数配置中直接声明的生命周期,mixin之间的执行顺序则遵从于其在options.mixins数组中的顺序

options的默认值为{types: ['app','page','component'], stage: -1},不传stage时,全局注入mixin的声明周期默认在options.mixins之前执行

import mpx from '@mpxjs/core'
-// 只在page中混入
-mpx.mixin({
-  methods: {
-    getData: function(){}
-  }
-}, {
-  types:'page'
-})
-
-// 默认混入,在app|page|component中都会混入
-mpx.mixin([
-  {
-    methods: {
-      getData: function(){}
-    }
-  },
-  {
-    methods: {
-      setData: function(){}
-    }
-  }
-])
-
-// 只在component中混入,且执行顺序在options.mixins之后
-mpx.mixin({
-  attached() {
-    console.log('com attached')
-  }
-}, {
-  types: 'component',
-  stage: 100
-})
-

# injectMixins

该方法是 mpx.mixin 方法的别名,mpx.injectMixins({}) 等同于 mpx.mixin({})

# observable

function observable(options: object): Mpx
-

用于创建响应式数据。

import mpx from '@mpxjs/core'
-// 直接通过 mpx 对象访问
-const b = mpx.observable(object)
-
  • 注意: -Mpx 2.8 版本后该 API 等同于 reactive,同时不再支持具名导出方式,建议直接使用 reactive 替代,请点击查看。

# set

用于对一个响应式对象新增属性,会触发订阅者更新操作查看详情

# delete

function delete(target: Object, key: string | number): void
-

用于对一个响应式对象删除属性,会触发订阅者更新操作

import mpx, { reactive } from '@mpxjs/core'
-const person = reactive({name: 1})
-mpx.delete(person, 'age')
-
  • 注意: mpx.delete 也可以使用具名导出的 del查看详情

# use

用于安装外部扩展, 支持多参数 -方法接收两个参数:mpx.use(plugin, options)

  • 第一个参数是要安装的外部扩展
  • 第二个参数是对象,如果第二个参数是一个包含(prefix or postfix)的option, 那么将会对插件扩展的属性添加前缀或后缀
import mpx from '@mpxjs/core'
-import test from './test'
-mpx.use(test)
-mpx.use(test, {prefix: 'mpx'}, 'otherparams')
-

# watch

watch 可以通过全局实例访问,也可以使用具名导出的方式,二者逻辑相同,我们推荐使用具名导出的方式。查看详情

# createApp

注册一个小程序,接受一个 Object 类型的参数

import {createApp} from '@mpxjs/core'
-
-createApp({
-  onLaunch () {
-    console.log('Launch')
-  },
-  onShow () {
-    console.log('Page show')
-  },
-  //全局变量 可通过getApp()访问
-  globalDataA: 'I am global dataA',
-  globalDataB: 'I am global dataB'
-})
-// 或者
-createApp(options)
-

# createPage

类微信小程序(微信、百度、头条等)内部使用Component的方式创建页面 (opens new window),所以除了支持页面的生命周期之外还同时支持组件的一切特性。当使用 Component 创建页面时,页面生命周期需要写在 methods 内部(微信小程序原生规则),mpx 进行了统一封装转换,页面生命周期都写在最外层即可

function createPage(options: object, config?: object): void
-
  • options:

    具体形式除了 computed、watch 这类 Mpx 扩展特性之外,其他的属性都参照原生小程序的官方文档即可。

  • config:

    如果希望标识一个组件是最纯粹的原生组件,不用数据响应等能力,可通过 config.isNative 传 true 声明。 -如果有需要复写/改写最终调用的创建页面的构造器,可以通过 config 对象的 customCtor 提供。

    • 注意: -mpx本身是用 component 来创建页面的,如果传page可能在初始化时候生命周期不正常导致取props有一点问题
import {createPage} from '@mpxjs/core'
-
-createPage({
-  data: {test: 1},
-  computed: {
-    test2 () {
-      return this.test + 1
-    }
-  },
-  watch: {
-    test (val, old) {
-      console.log(val, old)
-    }
-  },
-  onShow () {
-    this.test++
-  }
-})
-

# createComponent

创建自定义组件,接受两个Object类型的参数。

function createComponent(options: object, config?: object): void
-
import {createComponent} from '@mpxjs/core'
-
-createComponent({
-  properties: {
-    prop: {
-      type: Number,
-      value: 10
-    }
-  },
-  data: {test: 1},
-  computed: {
-    test2 () {
-      return this.test + this.prop
-    }
-  },
-  watch: {
-    test (val, old) {
-      console.log(val, old)
-    },
-    prop: {
-      handler (val, old) {
-        console.log(val, old)
-      },
-      immediate: true // 是否首次执行一次
-    }
-  }
-})
-

# nextTick

function nextTick(callback: Function): void
-

当我们在 Mpx 中更改响应性状态时,最终页面的更新并不是同步立即生效的,而是由 Mpx 将它们缓存在一个队列中, 等到下一个 tick 一起执行, -从而保证了组件/页面无论发生多少状态改变,都仅执行一次更新,从而减少 setData 调用次数。

nextTick() 可以在状态改变后立即调用,可以传递一个函数作为参数,在等待页面/组件更新完成后,函数参数会触发执行。

     import {createComponent, nextTick, ref} from '@mpxjs/core'
-     createComponent({
-       setup (props, context) {
-         const showChild = ref(false)
-         // DOM 还没有更新
-         setTimeOut(() => {
-            showChild.value = true
-         }, 2000)
-         nextTick(function() {
-           context.refs['child'].showToast()
-         })
-         return {
-           showChild
-         }
-       }
-     })
-

# toPureObject

function toPureObject(options: object): object
-

业务拿到的数据可能是响应式数据实例(包含了些其他属性),使用toPureObject方法可以将响应式的数据转化成纯 js 对象。

import {toPureObject} from '@mpxjs/core'
-const pureObject = toPureObject(object)
-

# getMixin

专为ts项目提供的反向推导辅助方法,该函数接收类型为 Object ,会将传入的嵌套mixins对象拉平成一个扁平的mixin对象

import { createComponent, getMixin} from '@mpxjs/core'
-// 使用mixins,需要对每一个mixin子项进行getMixin辅助函数包裹,支持嵌套mixin
-const mixin = getMixin({
-  mixins: [getMixin({
-    data: {
-      value1: 2
-    },
-    lifetimes: {
-      attached () {
-        console.log(this.value1, 'attached')
-      }
-    },
-    mixins: [getMixin({
-      data: {
-        value2: 6
-      },
-      created () {
-        console.log(this.value1 + this.value2 + this.outsideVal)
-      }
-    })]
-  })]
-})
-/*
-mixin值
-{
-  data: {value2: 6, value1: 2},
-  created: ƒ created(),
-  attached: ƒ attached()
-}
-*/
-createComponent({
-  data: {
-    outsideVal: 20
-  },
-  mixins: [mixin]
-})
-
-/*
-以上执行输出:
-28
-2 "attached"
-*/
-

# implement

function implement(name: string, options: object): object
-
  • {Object} options
    • {Array} modes:需要取消的平台
    • {Boolean} remove:是否将此能力直接移除
    • {Function} processor:设置成功的回调函数

以微信为 base 将代码转换输出到其他平台时(如支付宝、web 平台等),会存在一些无法进行模拟的跨平台差异,会在运行时进行检测并报错指出,例如微信转支付宝时使用 moved 生命周期等。使用implement方法可以取消这种报错。您可以使用 mixin 自行实现跨平台差异,然后使用 implement 取消报错。

import {implement} from '@mpxjs/core'
-
-if (__mpx_mode__ === 'web') {
-  const processor = () => {
-  }
-  implement('onShareAppMessage', {
-    modes: ['web'], // 需要取消的平台,可配置多个
-    remove: true, // 是否将此能力直接移除
-    processor // 设置成功的回调函数
-  })
-}
-

# 内建生命周期变量

Mpx 在运行时自身有着一套内建生命周期,当开发者想使用内建生命周期时,可以通过内建生命周期变量进行对应生命周期的注册, -需要注意的是,这部分内建生命周期变量只能用于选项式 API 中

# BEFORECREATE

string

在组件实例刚刚被创建时执行,在实例初始化之后、进行数据侦听和 data 初始化之前同步调用,注意此时不能调用 setData。

import {createComponent, BEFORECREATE} from "@mpxjs/core"
-
-createComponent({
-  [BEFORECREATE]() {
-      console.log('beforecreate trigger')
-  }
-})
-

# CREATED

string

在组件实例刚刚被创建时执行。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、事件/侦听器的回调函数。 -然而,挂载阶段还没开始,注意此时不能调用 setData。

import {createComponent, CREATED} from "@mpxjs/core"
-
-createComponent({
-  [CREATED]() {
-      console.log('beforecreate trigger')
-  }
-})
-

# BEFOREMOUNT

选项式 API 中使用,作用同onBeforeMount

# MOUNTED

选项式 API 中使用,作用同onMounted

# BEFOREUPDATE

选项式 API 中使用,作用同onBeforeUpdate

# UPDATED

选项式 API 中使用,作用同onUpdated

# SERVERPREFETCH

选项式 API 中使用,作用同onServerPrefetch

# BEFOREUNMOUNT

选项式 API 中使用,作用同onBeforeUnmount

# UNMOUNTED

选项式 API 中使用,作用同onUnmounted

# ONLOAD

选项式 API 中使用,作用同onLoad

# ONSHOW

选项式 API 中使用,作用同onShow

# ONHIDE

选项式 API 中使用,作用同onHide

# ONRESIZE

选项式 API 中使用,作用同onResize

# 响应式 API

详情请移步

# 组合式 API

详情请移步

# store API

详情请移步

- - - diff --git a/docs-vuepress/.vuepress/dist/api/index.html b/docs-vuepress/.vuepress/dist/api/index.html deleted file mode 100644 index 2e07eecb72..0000000000 --- a/docs-vuepress/.vuepress/dist/api/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Mpx框架 - - - - - - - - - -

API 参考

- - - diff --git a/docs-vuepress/.vuepress/dist/api/instance-api.html b/docs-vuepress/.vuepress/dist/api/instance-api.html deleted file mode 100644 index a6c757cb2c..0000000000 --- a/docs-vuepress/.vuepress/dist/api/instance-api.html +++ /dev/null @@ -1,265 +0,0 @@ - - - - - - 实例 API | Mpx框架 - - - - - - - - - -

# 实例 API

# $set

function $set(target: Object | Array, property: string | number, value: any): void
-

这是全局 mpx.set 的别名。向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。 -它必须用于向响应式对象上添加新 property,因为 Mpx 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')

# $watch

function $watch(expOrFn: string | Function, callback: Function | Object, options?: Object): Function
-

观察 Mpx 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。

// 键路径
-this.$watch('a.b.c', function (newVal, oldVal) {
-  // 做点什么
-})
-
-// 函数
-this.$watch(
-  function () {
-    // 表达式 `this.a + this.b` 每次得出一个不同的结果时
-    // 处理函数都会被调用。
-    // 这就像监听一个未被定义的计算属性
-    return this.a + this.b
-  },
-  function (newVal, oldVal) {
-    // 做点什么
-  }
-)
-

this.$watch 返回一个取消观察函数,用来停止触发回调:

var unwatch = this.$watch('a', cb)
-// 之后取消观察
-unwatch()
-
  • options.deep

为了发现对象内部值的变化,可以在选项参数中指定 deep: true。注意监听数组的变更不需要这么做。

this.$watch('someObject', callback, {
-  deep: true
-})
-this.someObject.nestedValue = 123
-// callback is fired
-
  • options.deep

在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调:

this.$watch('a', callback, {
-  immediate: true
-})
-// 立即以 `a` 的当前值触发回调
-

注意在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

// 这会导致报错
-var unwatch = this.$watch(
-  'value',
-  function () {
-    doSomething()
-    unwatch()
-  },
-  { immediate: true }
-)
-

如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:

var unwatch = this.$watch(
-  'value',
-  function () {
-    doSomething()
-    if (unwatch) {
-      unwatch()
-    }
-  },
-  { immediate: true }
-)
-
  • options.pausable

可在选项参数中指定 pausable: true 来声明一个可被暂停的 Watcher 实例,配置好之后可以通过 this.$getPausableWatchers() 来获取当前组件或者页面下定义的所有可被暂停的 Watcher 实例, -然后根据具体是业务场景通过 watcher.pause() 或者 watcher.resume() 来暂停或者恢复 watch 的监听。 -比如说在小程序页面 hide 时不需要监听的 watch 可以配置为 pausable: true,在页面 hide 时调用 watcher.pause() 暂停监听,在页面 show 时调用 watcher.resume() 来恢复监听。

this.$watch('someObject', callback, {
-  pausable: true
-})
-
-const isHide = false
-const watchers = this.$getPausableWatchers()
-if (watchers && watchers.length) {
-  for (let i = 0; i < watchers.length; i++) {
-    const watcher = watchers[i]
-    isHide && watcher.pause()
-    !isHide && watcher.resume()
-  }
-}
-
  • options.name

为了方便获取用户定义的 Watcher 实例,可在选项参数增加配置 name 来设置当前 Watcher 实例的名称,配置 name 后可通过 this.$getWatcherByName(name) 在组件或页面中获取到命名为 name 的 Watcher 实例(注意当存在多个 name 相同 watch 时,this.$getWatcherByName 获取的最后一个使用该 name -创建的 Watcher 实例,所以为了避免混淆,请不要在多个 watch 中配置同一个 name。)

this.$watch('someObject', callback, {
-  name: 'someObject'
-})
-
-const someObjectWatch = this.$getWatcherByName('someObject')
-

参考mpx.watch

# $delete

function $delete(target: Object, key: string | number): void
-

删除对象属性,如果该对象是响应式的,那么该方法可以触发观察器更新(视图更新 | watch回调)

  import {createComponent} from '@mpxjs/core'
-  createComponent({
-  data: {
-    info: {
-      name: 'a'
-    }
-  },
-  watch: {
-    'info' (val) {
-      // 当删除属性之后会执行
-      console.log(val)
-    }
-  },
-  attached () {
-    // 删除name属性
-    this.$delete(this.info, 'name')
-  }
-  })
-

参考: Mpx.delete

# $refs

Object

一个对象,持有注册过 ref的所有 DOM 元素和组件实例,调用响应的组件方法或者获取视图节点信息。

以获取组件为例,模版中引用child子组件

<child wx:ref="childDom"></child>
-

javascript 中可以调用组件的方法

import { createComponent } from '@mpxjs/core'
-createComponent({
-ready (){
-  // 调用child中的方法
-  this.$refs.childDom.childMethods()
-  // 获取child中的data
-  this.$refs.childDom.data
-  },
-})
-

参考: 组件 ref

# $asyncRefs

仅字节小程序可用,因为字节小程序 selectComponentselectAllComponents 方法为异步方法,因此使用 $refs 同步获取组件实例并不保证能够拿到正确的组件实例,需使用异步 $asyncRefs

import mpx, {createComponent} from '@mpxjs/core'
-
-createComponent({
-  ready() {
-    if (__mpx_mode__ === 'tt') {
-      this.$asyncRefs.mlist.then(res => {
-        const data = res.data
-        //......
-      })
-    }
-  }
-})
-

# $forceUpdate

function $forceUpdate(target: Object, callback: Function): void
-

用于强制刷新视图,正常情况下只有发生了变化的数据才会被发送到视图层进行渲染。强制更新时,会将某些数据强制发送到视图层渲染,无论是否发生了变化

import {createComponent} from '@mpxjs/core'
-createComponent({
-  data: {
-    info: {
-      name: 'a'
-    },
-    age: 100
-  },
-  attached () {
-    // 虽然不会修改age的值,但仍会触发重新渲染,并且会将age发送到视图层
-    this.$forceUpdate({
-      age: 100
-    }, () => {
-      console.log('视图更新后执行')
-    })
-
-    // 也可用于正常的数据修改,key支持Path,数组可以使用'array[index]':value的形式
-    this.$forceUpdate({
-      'info.name': 'b'
-    }, () => {
-      console.log('视图更新后执行')
-    })
-  }
-})
-

# $nextTick

function $nextTick(callback: Function): void
-

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。注意:callbackthis并不是绑定当前实例,你可以使用箭头函数避免this指向问题

import {createComponent} from '@mpxjs/core'
-  createComponent({
-  data: {
-    info: {
-      name: 1
-    }
-  },
-  attached () {
-    // 修改数据
-    this.info.name = 2
-    // DOM 还没有更新
-
-    // this.$nextTick(function() {
-    //   // DOM 现在更新了
-    //   console.log('会在由name变化引起的视图更新之后执行')
-    //   this.doSomthing() // 报错
-    // })
-    this.$nextTick(() => {
-      // DOM 现在更新了
-      console.log('会在由name变化引起的视图更新之后执行')
-      this.doSomthing()
-    })
-  }
-})
-

# $i18n

组件中直接调用$i18n的方法,比如:$t,$tc,$te,$d,$n

首先在mpx.plugin.conf.js中配置i18n

module.exports = {
-
-  //...
-
-  i18n: {
-    locale: 'en-US',
-    // messages既可以通过对象字面量传入,也可以通过messagesPath指定一个js模块路径,在该模块中定义配置并导出,dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理
-    messages: {
-      'en-US': {
-        message: {
-          hello: '{msg} world'
-        }
-      },
-      'zh-CN': {
-        message: {
-          hello: '{msg} 世界'
-        }
-      }
-    }
-  }
-}
-

组件中直接使用

<template>
-  <view>{{$t('message.hello')}}</view>
-</template>
-
-import {createComponent} from '@mpxjs/core'
-createComponent({
-  ready () {
-    console.log(this.$t('message.hello', { msg: 'hello' }))
-    console.log(this.$te('message.hello')) 
-    //...
-  }
-})
-

# $rawOptions

Object

获取组件或页面构造器的构造参数。

import { createComponent } from "@mpxjs/core"
-
-createComponent({
-  ready() {
-    console.log(this.$rawOptions)
-    /**
-     * attached
-     * detached
-     * methods
-     * mpxConvertMode
-     * mpxCustomKeysForBlend
-     * mpxFileResource
-     * ready
-     * setup
-     * ...其他构造参数
-     */
-  }
-})
-
- - - diff --git a/docs-vuepress/.vuepress/dist/api/optional-api.html b/docs-vuepress/.vuepress/dist/api/optional-api.html deleted file mode 100644 index 08662a27ca..0000000000 --- a/docs-vuepress/.vuepress/dist/api/optional-api.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - 选项式 API | Mpx框架 - - - - - - - - - -

# 选项式 API

# onAppInit

  • 类型: Function
  • 详细:

通过 createApp 注册一个应用期间被调用,输出 web 时 SSR渲染模式下,需要在此钩子中生成 pinia 的实例并返回。

# onSSRAppCreated

  • 类型: Function
  • 详细:

SSR渲染定制钩子,在服务端渲染期间被调用,可以在这个钩子中可以去返回应用程序实例,以及完成服务器端路由匹配,store 的状态挂载等。类 Vue 的 server entry 中的功能。

注意: 仅 web 环境支持

- - - diff --git a/docs-vuepress/.vuepress/dist/api/reactivity-api.html b/docs-vuepress/.vuepress/dist/api/reactivity-api.html deleted file mode 100644 index 59d7bdaac4..0000000000 --- a/docs-vuepress/.vuepress/dist/api/reactivity-api.html +++ /dev/null @@ -1,544 +0,0 @@ - - - - - - 响应式 API | Mpx框架 - - - - - - - - - -

# 响应式 API

# 响应式基础 API

# reactive

将对象处理为响应性对象。

const obj = reactive({ count: 0 })
-obj.count++
-

响应式转换是“深层”的:它会影响到所有嵌套的 property。一个响应式对象也将深层地解包任何 ref property,同时保持响应性。

const count = ref(1)
-const obj = reactive({ count })
-
-// ref 会被解包
-console.log(obj.count === count.value) // true
-
-// 它会更新 `obj.count`
-count.value++
-console.log(count.value) // 2
-console.log(obj.count) // 2
-

当将 ref 分配给 reactive property 时,ref 将被自动解包。

const count = ref(1)
-const obj = reactive({})
-
-obj.count = count
-
-console.log(obj.count) // 1
-console.log(obj.count === count.value) // true
-

当访问到某个响应式数组或 Map 这样的原生集合类型中的 ref 元素时,不会执行 ref 的解包

const refArr = ref([ref(2)])
-const state = reactive({
-    count: 1,
-    refArr
-})
-
-// 无需.vlaue
-console.log(state.refArr)
-// 这里需要.value
-console.log(state.refArr[0].value)
-

当需要给响应式对象新增属性时,需要使用set,并且新增属性的响应式对象需要为 ref 或者做为其他响应性对象的key

const state = reactive({
-    count: 1
-})
-const stateRef = ref({
-    count: 1
-})
-const stateRefWrap = reactive({
-    data: {
-        count: 1
-    }
-})
-
-onLoad(() => {
-    setTimeout(() => {
-        set(state, 'foo', 1) // 不生效
-        set(stateRef.value, 'foo', 1) // 生效
-        set(stateRefWrap.data, 'foo', 1) // 生效
-    }, 2000)
-})
-
-return {
-    state,
-    stateRef,
-    stateRefWrap
-}
-

# isReactive

检查对象是否是由 reactive 创建的响应式对象。

import { createComponent, reactive, isReactive } from '@mpxjs/core'
-
-createComponent({
-    setup(){
-        const state = reactive({
-            count: 1
-        })
-        console.log(isReactive(state)) // -> true
-        return {
-            state
-        }
-    }
-})
-

# markRaw

标记一个对象,使其永远不会被抓换为响应性对象,并返回对象本身

import { markRaw, reactive, isReactive } from '@mpxjs/core'
-
-const foo = markRaw({
-    count: 1
-})
-const state = reactive({
-    foo
-})
-console.log(isReactive(state.foo)) // -> false
-

注意:

如果将标记对象内部的未标记对象添加进响应性对象,然后再次访问该响应性对象,就会得到该原始对象的可响应性对象

import { markRaw, reactive, isReactive } from '@mpxjs/core'
-
-const foo = markRaw({
-    nested: {}
-})
-
-const bar = reactive({
-    nested: foo.nested
-})
-
-console.log(foo.nested === bar.nested) // -> true
-

# shallowReactive

reactive() 的浅层作用形式,只跟踪自身 property 的响应性,但不执行嵌套对象的深层响应式转换(返回原始值)

注意:

和 reactive() 不同,这里没有深层级的转换:一个浅层响应式对象里只有根级别的 property 是响应式的。property 的值会被原样存储和暴露,这也意味着值为 ref 的 property 不会被自动解包了。

import { shallowReactive } from '@mpxjs/core'
-
-const count = ref(0)
-const state = shallowReactive({
-  foo: 1,
-  nested: {
-    bar: 2
-  },
-  head: count  
-})
-
-// 更改状态自身的属性是响应式的
-state.foo++
-
-// ...但下层嵌套对象不会被转为响应式
-isReactive(state.nested) // false
-
-// 不是响应式的
-state.nested.bar++
-
-// ref 属性不会解包
-state.head.value 
-

# set

用于对一个响应式对象新增属性,会触发订阅者更新操作

function set(target: Object | Array, property: string | number, value: any): void
-
import { set, reactive } from '@mpxjs/core'
-const person = reactive({name: 1})
-// 具名导出使用
-set(person, 'age', 17) // age 改变后会触发订阅者视图更新
-

# del

用于对一个响应式对象删除属性,会触发订阅者更新操作

function del(target: Object | Array, property: string | number): void
-
import {del, reactive } from '@mpxjs/core'
-const person = reactive({name: 1})
-del(person, 'age')
-

# Computed 与 Watch

# computed

// 只读形式
-function computed<T>(
-    getter: () => T
-): Readonly<Ref<Readonly<T>>>
-
-// 可写形式
-function computed<T>(
-    options: {
-        get: () => T
-        set: (value: T) => void
-    }
-): Ref<T>
-

接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。

const count = ref(1)
-const plusOne = computed(() => count.value + 1)
-
-console.log(plusOne.value) // 2
-
-plusOne.value++ // 错误
-

或者接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象。

const count = ref(1)
-const plusOne = computed({
-  get: () => count.value + 1,
-  set: val => {
-    count.value = val - 1
-  }
-})
-
-plusOne.value = 1
-console.log(count.value) // 0
-

# watchEffect

function watchEffect(
-  effect: (onInvalidate: InvalidateCbRegistrator) => void,
-  options?: WatchEffectOptions
-): StopHandle
-
-interface WatchEffectOptions {
-    flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
-}
-
-type InvalidateCbRegistrator = (invalidate: () => void) => void
-
-type StopHandle = () => void
-

立即执行传入的函数,同时对其依赖进行响应式追踪,并在其依赖变更时重新运行该函数。

const count = ref(0)
-
-watchEffect(() => console.log(count.value))
-// -> logs 0
-
-setTimeout(() => {
-  count.value++
-  // -> logs 1
-}, 100)
-
  • 停止侦听
const stop = watchEffect(() => {
-  /* ... */
-})
-
-// later
-stop()
-
  • 清除副作用

侦听副作用传入的函数可以接收一个函数作入参,用来注册清理回调,清理回调会在该副作用下一次执行前被调用, -可以用来清理无效的副作用,例如等待中的异步请求

watchEffect(async (onCleanup) => {
-  const { response, cancel } = doAsyncWork(id.value)
-  // `cancel` 会在 `id` 更改时调用
-  // 以便取消之前未完成的请求
-  onCleanup(cancel)
-  data.value = await response
-})
-
  • 副作用刷新时机

响应式系统会进行副作用函数缓存,并异步地刷新执行他们

组件的 update 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update 前执行, -在watchEffect的第二个参数中,我们可以传入flush来进行副作用刷新时机调整

// 默认为 pre
-watchEffect(callback, {
-    flush: 'pre'
-})
-
-// 侦听器回调中能访问被更新之后的DOM
-// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
-watchEffect(callback, {
-  flush: 'post'
-})
-
-// 强制同步触发, 十分低效
-watchEffect(callback, {
-    flush: 'sync'
-})
-

# watchSyncEffect

watchEffect 的别名,带有 flush: 'sync' 选项。

# watchPostEffect

watchEffect 的别名,带有 flush: 'post' 选项。

# watch

// 侦听单一源
-function watch<T>(
-    source: WatcherSource<T>,
-    callback: (
-        value: T,
-        oldValue: T,
-        onInvalidate: InvalidateCbRegistrator
-    ) => void,
-    options?: WatchOptions
-): StopHandle
-
-// 侦听多个源
-
-type WatcherSource<T> = Ref<T> | (() => T)
-
-type InvalidateCbRegistrator = (invalidate: () => void) => void
-
-type StopHandle = () => void
-
-interface WatchOptions extends WatchEffectOptions {
-    immediate?: boolean // 默认:false
-    deep?: boolean // 默认:false
-    immediateAsync: boolean // 默认:false
-}
-

该 API 与选项式 API 中的 watch 基本等效,watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下 -是惰性的——即回调仅在侦听源发生变化时被调用。

与 watchEffect 比较,watch 允许我们:

  • 懒执行副作用;
  • 更具体地说明什么状态应该触发侦听器重新运行;
  • 访问侦听状态变化前后的值。

# 侦听单一源

watch 可以侦听一个具有返回值的 getter,也可以直接是一个 ref

// 侦听一个 getter
-const state = reactive({ count: 0 })
-watch(
-  () => state.count,
-  (count, prevCount) => {
-    /* ... */
-  }
-)
-
-// 直接侦听ref
-const count = ref(0)
-watch(count, (count, prevCount) => {
-  /* ... */
-})
-

# 侦听多个源

还可以使用数组的形式同时侦听多个数据源:

import { watch } from '@mpxjs/core'
-
-watch([aRef, bRef], ([a, b], [prevA, prevB]) => {
-    /* ... */
-})
-

# 与 watchEffect 相同的行为

watchwatchEffect 在手动停止侦听、清除副作用、副作用刷新时机方面有相同的行为。

import { watch } from '@mpxjs/core'
-
-let unwatch = watch(() => {
-  return a.value + b.value
-}, (newVal, oldVal) => {
-  // 做点什么
-})
-
-// 调用返回值unwatch可以取消观察
-unwatch()
-

# watch 选项

  • 选项:deep

    为了发现对象内部值的变化,可以在选项参数中指定 deep: true。

    import {watch} from '@mpxjs/core'
    -
    -watch(() => {
    -  return this.someObject
    -}, () => {
    -  // 回调函数
    -}), {
    -  deep: true
    -})
    -this.someObject.nestedValue = 123
    -// callback is fired
    -
  • 选项:once

    在选项参数中指定 once: true 该回调方法只会执行一次,后续的改变将不会触发回调;
    -该参数也可以是函数,若函数返回值为 true 时,则后续的改变将不会触发回调

    import {watch} from '@mpxjs/core'
    -
    -watch(() => {
    -  return this.a
    -}, () => {
    -  // 该回调函数只会执行一次
    -}, {
    -  once: true
    -})
    -
    -// 当 once 是函数时
    -watch(() => {
    -  return this.a
    - }, (val, newVal) => {
    -  // 当 val 等于2时,this.a 的后续改变将不会被监听
    - }, {
    -  once: (val, oldVal) => {
    -    if (val == 2) {
    -      return true
    -    }
    -  }
    -})
    -
  • 选项:immediate

    在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调。

    import {watch} from '@mpxjs/core'
    -
    -watch(() => {
    -  return this.a
    -}, () => {
    -  // 回调函数
    -}), {
    -  immediate: true
    -})
    -// 立即以 `this.a` 的当前值触发回调
    -

    注意在带有 immediate 选项时,你不能在第一次回调时取消侦听。

    import {watch} from '@mpxjs/core'
    -
    -var unwatch = watch(() => {
    -  return this.a
    -}, () => {
    -  unwatch() // 这会导致报错!
    -}), {
    -  immediate: true
    -})
    -
    -

    如果你仍然希望在回调内部调用取消侦听的函数,你应该先检查其可用性。

    import {watch} from '@mpxjs/core'
    -
    -var unwatch = watch(() => {
    -  return this.a
    -}, () => {
    -  if (unwatch) { // 请先检查其可用性!
    -    unwatch()
    -  }
    -}), {
    -  immediate: true
    -})
    -
    -

# Effect 作用域 API

# effectScope

function effectScope(detached?: boolean): EffectScope
-
-interface EffectScope {
-    run<T>(fn: () => T): T | undefined
-    stop(): void
-    pause(): void
-    resume(): void
-}
-

创建一个 effect 作用域对象,捕获在其内部创建的响应性副作用(例如计算属性或监听器),可以对这些副作用进行批量处理

import { effectScope } from '@mpxjs/core'
-
-const scope = effectScope()
-scope.run(() => {
-    const doubled = computed(() => counter.value * 2)
-    
-    watch(doubled, () => console.log(doubled.value))
-    
-    watchEffect(() => console.log('Count: ', doubled.value))
-})
-
-scope.stop()
-

需要注意的是,effectScope 接受一个 detached 参数,默认为false,该参数来表示当前作用域是否和父级作用域进行分离,若 detached 为 true, -当前 effectScope 则不会被父级作用域收集。

  • 暂停侦听

Mpx 提供了 pause 方法可以将整个作用域中的响应性副作用批量暂停侦听。

import { effectScope, onHide } from '@mpxjs/core'
-const scope = effectScope()
-scope.run(() => {
-    const doubled = computed(() => counter.value * 2)
-
-    watch(doubled, () => console.log(doubled.value))
-
-    watchEffect(() => console.log('Count: ', doubled.value))
-})
-
-onHide(() => {
-    scope.pause()
-})
-
  • 恢复侦听

被暂停的作用域可以使用 resume 方法来恢复侦听。

import {onShow} from '@mpxjs/core'
-// ......
-
-onShow(() => {
-    scope.resume()
-})
-

# getCurrentScope

function getCurrentScope(): EffectScope | undefined
-

返回当前活跃的 effect 作用域。

# onScopeDispose

function onScopeDispose(fn: () => void): void
-

在当前活跃的 effect 作用域上注册一个处理回调。该回调会在相关的 effect 作用域结束之后被调用。

# Refs

# ref

interface Ref<T> {
-    value: T
-}
-function ref<T>(value: T): Ref<T>
-

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的 property .value。

const count = ref(0)
-console.log(count.value) // 0
-
-count.value++
-console.log(count.value) // 1
-

注意事项:

1.所有对 .value 的操作都将被追踪,并且写操作会触发与之相关的副作用。

2.如果将一个对象赋值给 ref,那么这个对象将被 reactive 转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。

import { ref } from '@mpxjs/core'
-const foo = ref(0)
-const state = ref({
-    count: 1,
-    foo
-})
-// 获取count
-console.log(state.value.count)
-// 获取foo
-console.log(state.value.foo)
-

# unref

如果参数是一个 ref,则返回内部值,否则返回参数本身,是 val = isRef(ref) ? ref.value : ref 的语法糖函数。

import { ref, unref } from '@mpxjs/core'
-const count = ref(0)
-const foo = unref(count)
-
-console.log(foo === 0) // -> true
-

# toRef

用于为响应式对象上的property 创建 ref。创建的 ref 与其源 property 保持同步:改变源 property -将更新 ref,改变 ref 也将更新 property。

const state = reactive({
-    f: 1,
-    b: 2
-})
-
-const stateRef = ref({
-    black: 1,
-    white: 2
-})
-
-const fooRef = toRef(state, 'f')
-const blackRef = toRef(stateRef.value, 'black') 
-
-// 更改ref的值更新属性
-fooRef.value++
-blackRef.value++
-console.log(state.f) // 2
-console.log(stateRef.value.black) // 2
-
-// 更改property的值更新ref
-state.f++
-stateRef.value.black++
-console.log(fooRef.value) // 3
-console.log(blackRef.value) // 3
-

注意:

即使源 property 当前不存在,toRef() 也会返回一个可用的 ref,而 toRefs 则不会,这在处理可选properties的时候 -非常有用

# toRefs

将一个响应式对象转换为一个普通对象,这个普通对象的每个 property 都是指向源对象相应 property 的 ref。每个单独的 ref 都是 toRef 创建的

const state = reactive({
-    black: 1,
-    white: 2
-})
-
-const stateAsRefs = toRefs(state)
-
-state.black++
-console.log(stateAsRefs.black.value) // 2
-
-stateAsRefs.black.value++
-console.log(state.black) // 3
-

toRefs 在使用组合式函数中的响应式对象时有很大作用,使用它,对响应式对象进行解构将不会失去响应性

function useFeatX() {
-    const state = reactive({
-        black: 1,
-        white: 2
-    })
-    
-    // 在返回时都转为 ref
-    return toRefs(state)
-}
-
-// 解构而不会失去响应性
-const {black, white} = useFeatX()
-

注意:

  • toRefs 在调用时只会为源对象上可以列举出的 property 创建 ref。如果要为可能还不存在的 property -创建 ref,请改用 toRef

# isRef

检查某个值是否为 ref,返回true/false。

# customRef

function customRef<T>(factory: CustomRefFactory<T>): Ref<T>
-
-type CustomRefFactory<T> = (
-    track: () => void,
-    trigger: () => void
-) => {
-    get: () => T,
-    set: (value: T) => void
-}
-

创建一个自定义 ref,可对其进行依赖项跟踪和更新触发显示控制。需要一个工厂函数,该函数接收 tracktrigger -做为参数,并且应该返回一个带有 getset 的对象。

同时,track() 应该在 get() 方法中调用,trigger() 应该在 set() 中调用。不过这里具体何时调用、 -是否调用都将由用户自己来控制

示例:创建一个防抖 ref,即只在最近一次 set 调用后的一段固定间隔后再调用:

function useDebouncedRef(value, delay = 200) {
-  let timeout
-  return customRef((track, trigger) => {
-    return {
-      get() {
-        track()
-        return value
-      },
-      set(newValue) {
-        clearTimeout(timeout)
-        timeout = setTimeout(() => {
-          value = newValue
-          trigger()
-        }, delay)
-      }
-    }
-  })
-}
-
-// text 的每次改变都只在最近一次set后的200ms后调用
-const text = useDebouncedRef('hello')
-

# shallowRef

ref() 的浅层作用形式,创建一个仅跟踪自身 .value 变化的 ref,其他值不做任何处理都为非响应式

const state = shallowRef({
-    count: 1
-})
-
-// 不会触发更改
-state.value.count++
-
-// 会触发更改
-state.value = {
-    black: 1
-}
-

注意: -shallowRef 的内部值将会被原样存储和暴露,不会被深层递归转为响应性, 只有对 .value 的访问是响应式的

常常用于对大型数据结构的性能优化或是与外部的状态管理系统集成。

# triggerRef

强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用。

const shallow = shallowRef({
-  greet: 'Hello, world'
-})
-
-// 触发该副作用第一次应该会打印 "Hello, world"
-watchEffect(() => {
-  console.log(shallow.value.greet)
-})
-
-// 这次变更不应触发副作用,因为这个 ref 是浅层的
-shallow.value.greet = 'Hello, universe'
-
-// 手动执行该 shallowRef 关联的副作用,打印 "Hello, universe"
-triggerRef(shallow)
-
-
- - - diff --git a/docs-vuepress/.vuepress/dist/api/store-api.html b/docs-vuepress/.vuepress/dist/api/store-api.html deleted file mode 100644 index 0a22caf8e0..0000000000 --- a/docs-vuepress/.vuepress/dist/api/store-api.html +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - Store API | Mpx框架 - - - - - - - - - -

# Store API

注意: 以下 API 在 2.8 版本后无法通过全局应用实例 mpx 访问。若项目中有类似 mpx.createStore 的用法,在升级到 2.8 版本后请进行修改。

# createStore

function createStore(options: Object): Store
-

创建一个全局状态管理容器,实现复杂场景下的组件通信需求

  • options

    options 可指定以下属性:

    • state: Object

      store的根 state 对象。

      详细介绍

    • mutations: { [type: string]: Function }

      在 store 上注册 mutation,处理函数总是接受 state 作为第一个参数(如果定义在模块中,则为模块的局部状态),payload 作为第二个参数(可选)。

      详细介绍

    • actions: { [type: string]: Function }

      在 store 上注册 action。处理函数总是接受 context 作为第一个参数,payload 作为第二个参数(可选)。

      context 对象包含以下属性: -js { state, // 等同于 `store.state` commit, // 等同于 `store.commit` dispatch, // 等同于 `store.dispatch` getters // 等同于 `store.getters` } -同时如果有第二个参数 payload 的话也能够接收。

      详细介绍

    • getters{[key: string]: Function }

      在 store 上注册 getter,getter 方法接受以下参数:

      {
      -  state,     // 如果在模块中定义则为模块的局部状态
      -  getters   // 等同于 store.getters
      -}
      -

      注册的 getter 暴露为 store.getters。

      详细介绍

    • modulesObject

      包含了子模块的对象,会被合并到 store,大概长这样:

      {
      -  key: {
      -    state,
      -    mutations,
      -    actions?,
      -    getters?,
      -    modules?
      -    },
      -    // ...
      -}
      -

      与根模块的选项一样,每个模块也包含 state 和 mutations 选项。模块的状态使用 key 关联到 store 的根状态。模块的 mutation 和 getter 只会接收 module 的局部状态作为第一个参数,而不是根状态,并且模块 action 的 context.state 同样指向局部状态。

      详细介绍

    • depsObject

      包含了当前store依赖的第三方store:

      {
      -  store1: storeA,
      -  store2: storeB
      -}
      -

      详细介绍

import {createStore} from '@mpxjs/core'
-const store1 = createStore({
-  state: {
-    count: 0
-  },
-  mutations: {
-    increment (state) {
-      state.count++
-    }
-  },
-  actions: {
-    increment (context) {
-      context.commit('increment')
-    }
-  },
-  ...
-})
-const store2 = createStore({ ...options })
-

# Store 实例属性

  • stateObject -根状态。
  • gettersObject -暴露出注册的 getter。

# Store 实例方法

  • commit
commit(type: string, payload?: any, options?: Object) | commit(mutation: Object, options?: Object)
-

提交 mutation。详细介绍

  • dispatch
dispatch(type: string, payload?: any, options?: Object) | dispatch(action: Object, options?: Object)
-

分发 action。返回一个Promise。详细介绍

  • mapState
mapState(map: Array<string> | Object): Object
-

为组件创建计算属性以返回 store 中的状态。详细介绍

  • mapGetters
mapGetters(map: Array<string> | Object): Object
-

为组件创建计算属性以返回 getter 的返回值。详细介绍

  • mapActions
mapActions(map: Array<string> | Object): Object
-

创建组件方法分发 action。详细介绍

  • mapMutations
mapMutations(map: Array<string> | Object): Object
-

创建组件方法提交 mutation。详细介绍

  • mapStateToRefs
mapStateToRefs(maps: Array<string> | Object): {
-    [key: string]: ComputedRef<any>
-}
-

组合式 API 特有,在组合式 API 场景下解构访问 getter 并保持 getter 响应性,可以使用该方法。详细介绍

  • mapGettersToRefs
mapGettersToRefs(maps: Array<string> | Object): {
-    [key: string]: ComputedRef<any>
-}
-

组合式 API 特有,在组合式 API 场景下需解构访问 state 并保持 state 响应性,可以使用该方法。详细介绍

# createStoreWithThis

function createStoreWithThis(store: Store): Store
-

createStoreWithThis 为 createStore 的变种方法,主要为了在 Typescript 环境中,可以更好地支持 store 中的类型推导。
-其主要变化在于定义 getters, mutations 和 actions 时, -自身的 state,getters 等属性不再通过参数传入,而是会挂载到函数的执行上下文 this 当中,通过 this.state 或 this.getters 的方式进行访问。 -由于TS的能力限制,getters/mutations/actions 只有使用对象字面量的方式直接传入 createStoreWithThis 时 -才能正确推导出 this 的类型,当需要将 getters/mutations/actions 拆解为对象编写时,需要用户显式地声明 this 类型,无法直接推导得出。


-import {createComponent, getMixin, createStoreWithThis} from '@mpxjs/core'
-
-const store = createStoreWithThis({
-  state: {
-    aa: 1,
-    bb: 2
-  },
-  getters: {
-    cc() {
-      return this.state.aa + this.state.bb
-    }
-  },
-  actions: {
-    doSth3() {
-      console.log(this.getters.cc)
-      return false
-    }
-  }
-})
-
-createComponent({
-  data: {
-    a: 1,
-    b: '2'
-  },
-  computed: {
-    c() {
-      return this.b
-    },
-    d() {
-      // data, mixin, computed中定义的数据能够被推导到this中
-      return this.a + this.aaa + this.c
-    },
-    // 从store上map过来的计算属性或者方法同样能够被推导到this中
-    ...store.mapState(['aa'])
-  },
-  mixins: [
-    // 使用mixins,需要对每一个mixin子项进行getMixin辅助函数包裹,支持嵌套mixin
-    getMixin({
-      computed: {
-        aaa() {
-          return 123
-        }
-      },
-      methods: {
-        doSth() {
-          console.log(this.aaa)
-          return false
-        }
-      }
-    })
-  ],
-  methods: {
-    doSth2() {
-      this.a++
-      console.log(this.d)
-      console.log(this.aa)
-      this.doSth3()
-    },
-    ...store.mapActions(['doSth3'])
-  }
-})
-

# createStateWithThis

function createStateWithThis(state: Object): Object
-

createStateWithThis 为创建 state 提供了类型推导,对于基本类型可以由 TypeScript 自行推导,使用其他类型时,推荐使用 as 进行约束

import { createStateWithThis } from '@mpxjs/core'
-
-export type StatusType = 'start' | 'running' | 'stop'
-
-export default createStateWithThis({
-  status: 'running' as StatusType
-})
-

# createGettersWithThis

function createGettersWithThis(getters: Object, options: Object):Object
-
  • getters

    需要定义的 getters 对象。

  • options(可选参数)

    在 options 中可以传入 state,getters,deps。由于 getter 的类型推论需要基于 state,所以导出 getters 时,需要将 state 进行传入。deps 是作为一个扩展存在,getters 可以通过 deps 中传入的其他 store 来获取值,当 store 没有其他需要依赖的 deps 时可以不传。createMutationsWithThis 和 createActionsWithThis 同理。

import { createGettersWithThis, createStoreWithThis } from '@mpxjs/core'
-
-export default createGettersWithThis({
-  isStart () {
-    return this.state.status === 'start'
-  },
-  getNum () {
-    return this.state.base.test + this.getters.base.getTest
-  }
-}, {
-  state,
-  deps: {
-    base: createStoreWithThis({
-      state: {
-        testNum: 0
-      },
-      getters: {
-        getTest () {
-          return this.state.testNum * 2
-        }
-      }
-    })
-}})
-

# createMutationsWithThis

function createMutationsWithThis(mutations: Object, options: Object): Object
-
  • mutations

    需要定义的 mutations 对象。

  • options(可选参数)

    在 options 中可以传入 state,deps。

import { createMutationsWithThis } from '@mpxjs/core'
-
-export default createMutationsWithThis({
-  setCurrentStatus (payload: StatusType) {
-    this.state.status = payload
-  }
-}, { state })
-

# createActionsWithThis

function createActionsWithThis(actions: Object, options: Object): Object
-
  • actions

    需要定义的 actions 对象。

  • options(可选参数)

    由于action 可以同时调用 getters、mutations,所以需要将这些都传入,以便进行类型推导。因此 options 可以传入 state、getters、mutations、deps。

import { createActionsWithThis } from '@mpxjs/core'
-import state, { StatusType } from './state'
-import getters from './getters'
-import mutations from './mutations'
-
-export default createActionsWithThis({
-  testActions (payload: StatusType) {
-    return Promise.resolve(() => {
-      this.commit('setCurrentStatus', payload)
-    })
-  }
-}, {
-  state,
-  getters,
-  mutations
-})
-
- - - diff --git a/docs-vuepress/.vuepress/dist/articles/1.0.html b/docs-vuepress/.vuepress/dist/articles/1.0.html deleted file mode 100644 index b344feb20d..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/1.0.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - 滴滴开源小程序框架Mpx | Mpx框架 - - - - - - - - - -

# 滴滴开源小程序框架Mpx

作者:hiyuki (opens new window)

Mpx是一款致力于提高小程序开发体验的增强型小程序框架,通过Mpx,我们能够以最先进的web开发体验(Vue + Webpack)来开发生产性能深度优化的小程序,Mpx具有以下一些优秀特性:

  • 数据响应特性(watch/computed)
  • 增强的模板语法(动态组件/样式绑定/类名绑定/内联事件函数/双向绑定等)
  • 深度性能优化(原生自定义组件/基于依赖收集和数据变化的setData)
  • Webpack编译(npm/循环依赖/Babel/ESLint/css预编译/代码优化等)
  • 单文件组件开发
  • 状态管理(Vuex规范/多实例/可合并)
  • 跨团队合作(packages)
  • 逻辑复用能力(mixins)
  • 脚手架支持
  • 小程序自身规范的完全支持
  • 支付宝小程序的支持

# 设计思路

目前业界主流的小程序框架主要有WePY,mpvue和Taro,这三者都是将其他的语法规范转译为小程序语法规范,我们称其为转译型框架。不同于上述三者,Mpx是一款基于小程序语法规范的增强型框架,我们使用Vue中优秀的语法特性增强了小程序,而不是让用户直接使用vue语法来开发小程序,之所以采用这种设计主要是基于如下考虑:

  • 转译型框架无法支持源框架的所有语法特性(如Vue模板中的动态特性或React中动态生成的jsx),用户在使用源框架语法进行开发时可能会遇到不可预期的错误,具有不确定性
  • 小程序本身的技术规范在不断地更新进步,许多新的技术规范在转译型框架中无法支持或需要很高的支持成本,而对于增强型框架来说只要新的技术规范不与增强特性冲突,就能够直接支持

# 技术实现

小程序刚推出时不支持自定义组件,无法进行组件化开发,当时最早的小程序框架WePY做的核心工作就是支持了小程序的组件化开发,后来的mpvue也基于Vue做了类似的工作。由于当时的小程序本身不支持自定义组件,WePY和mpvue的组件化支持都是基于编译做的组件化封装,用户编写的组件模板会被编译为Page中模板的一部分,在组件中定义的数据会被编译为Page中的数据,对组件进行数据更新也会基于路径映射调用Page.setData。这个方案在当时的技术条件下确实是一个唯一解,但该方案在复杂的小程序应用中会存在明显性能问题,原因在于该方案中页面的数据量会很大,而且每个组件的局部更新实际上都会成为页面级别的全局更新,在组件较多的页面中容易引发性能问题。在小程序自定义组件推出后,我们通过性能测试确认了该方案解决了上述组件封装方案中的性能问题。由于自定义组件在当时是一个最新的技术标准,业内的小程序框架都未支持,而我们本身的业务又有复杂小程序的开发需求,于是我们就基于小程序自定义组件规范启动了Mpx框架的设计与开发

Page与Component setData性能对照

Component Page
局部更新 1975ms 7186ms
全局更新 6210ms 24612ms

性能衡量标准说明: -局部更新:文档中存在1000个节点,其中一个节点更新1000次的耗时; -全局更新:文档中存在5个节点,5个节点独立更新1000次的耗时 -以上数据在iphone7微信小程序环境下测试得出

# 数据响应与性能优化

数据响应作为Vue最核心的特性,在我们的日常开发中被大量使用,能够极大地提高前端开发体验和效率,我们在框架设计初期最早考虑的就是如何将数据响应特性加入到小程序开发中。在数据响应的实现上,我们引入了MobX,一个实现了纯粹数据响应能力的知名开源项目。借助MobX和mixins,我们在小程序组件创建初期建立了一个响应式数据管理系统,该系统观察着小程序组件中的所有数据(data/props/computed)并基于数据的变更驱动视图的渲染(setData)及用户注册的watch回调,实现了Vue中的数据响应编程体验。与此同时,我们基于MobX封装实现了一个Vuex规范的数据管理store,能够方便地注入组件进行全局数据管理。为了提高跨团队开发的体验,我们对store添加了多实例可合并的特性,不同团队维护自己的store,在需要时能够合并他人或者公共的store生成新的store实例,我们认为这是一种比Vuex中modules更加灵活便捷的跨团队数据管理模式

作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个:

  1. 尽可能减少setData调用的频次
  2. 尽可能减少单次setData传输的数据

为了实现以上两个优化方向,我们做了以下几项工作:

  • 将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次;
  • 将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。 -基于以上优化,我们大大减少了小程序setData的调用频次和传递数据量,与初版Mpx中track全量数据的方案相比提升显著。

Mpx setData优化效果

旧版mpx 新版mpx
setData次数 164 88
setData数据量 370kB 38kB

以上数据由较复杂小程序在固定交互流程后统计得出

Mpx数据响应机制流程示意图 Mpx数据响应机制流程示意图

# 编译构建

我们希望使用目前设计最强大、生态最完善的编译构建工具Webpack来实现小程序的编译构建,让用户得到web开发中先进强大的工程化开发体验。使用过Webpack的同学都知道,通常来说Webpack都是将项目中使用到的一系列碎片化模块打包为一个或几个bundle,而小程序所需要的文件结构是非常离散化的,如何调解这两者的矛盾成为了我们最大的难题。一种非常直观简单的思路在于遍历整个src目录,将其中的每一个.mpx文件都作为一个entry加入到Webpack中进行处理,这样做的问题主要有两个:

  1. src目录中用不到的.mpx文件也会被编译输出,最终也会被小程序打包进项目包中,无意义地增加了包体积;
  2. 对于node_modules下的.mpx文件,我们不认为遍历node_modules是一个好的选择。

最终我们采用了一种基于依赖分析和动态添加entry的方式来进行实现,用户在Webpack配置中只需要配置一个入口文件app.mpx,loader在解析到json时会解析json中pages域和usingComponents域中声明的路径,通过动态添加entry的方式将这些文件添加到Webpack的构建系统当中(注意这里是添加entry而不是添加依赖,因为只有entry能生成独立的文件,满足小程序的离散化文件结构),并递归执行这个过程,直到整个项目中所有用到的.mpx文件都加入进来,在输出前,我们借助了CommonsChunkPlugin/SplitChunksPlugin的能力将复用的模块抽取到一个外部的bundle中,确保最终生成的包中不包含重复模块。我们提供了一个Webpack插件和一个.mpx文件对应的loader来实现上述操作,用户只需要将其添加到Webpack配置中就可以以打包web项目的方式正常打包小程序,没有任何的前置和后置操作,支持Webpack本身的完整生态。

Mpx编译构建机制流程示意图 Mpx编译构建机制流程示意图

# 现状和未来

目前Mpx框架已经在滴滴内部大量使用,支撑了滴滴出行、青桔单车、街兔电单车、营销、车服等业务在小程序上的实现,线上运行稳定,收到了大量的好评反馈。未来我们在对框架进行持续迭代优化的同时会持续跟进微信和支付宝最新的技术标准,同时也会将在更多的小程序平台上进行适配。由于我们的设计初衷和专注点在于增强小程序开发体验,Mpx并没有进行跨H5甚至是跨Native的支持,但现实业务当中确实存在这样的诉求,未来我们会在Mpx的基础上对跨端进行一定的尝试,与此同时我们依然会持续维护升级Mpx,原因在于跨端意味着灵活性受限及能力的缺失,我们希望能给用户提供第二种选择。

Mpx与业内主流小程序框架异同对比

WePY mpvue Taro Mpx
代码规范 自定义 Vue React 小程序+
组件化实现 Page封装 Page封装 Component Component
数据响应 脏值检查 Vue Mobx
状态管理 Redux Vuex Redux 类Vuex

# 结语

如果你注重开发体验和产品性能,专注于小程序开发,那Mpx会是一个很好的选择。最后感谢开源社区源源不断涌现出的优秀项目,给我们提供了无限的启发和巨大的技术帮助。

Github: https://github.com/didi/mpx (opens new window) -使用文档: https://mpxjs.cn/ (opens new window) -用户交流群: -微信

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/2.0.html b/docs-vuepress/.vuepress/dist/articles/2.0.html deleted file mode 100644 index 9482e8fab6..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/2.0.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - 滴滴小程序框架Mpx发布2.0,支持小程序跨平台开发,可直接转换已有微信小程序 | Mpx框架 - - - - - - - - - -

# 滴滴小程序框架Mpx发布2.0,支持小程序跨平台开发,可直接转换已有微信小程序

作者:hiyuki (opens new window)

Mpx是一款致力于提高小程序开发体验和效率的增强型小程序框架,目前在滴滴公司内部支撑了包括滴滴出行小程序,滴滴出行广场小程序,青桔单车,黑马电单车,小桔养车,小桔加油在内的小程序生态;自去年11月开源以来,Mpx也吸纳了众多外部开发者的加入,基于Mpx开发了开走吧,好免街,花忆等小程序。

长期以来,Mpx优秀的开发体验和强大的稳定性得到了内外开发者的一致认可和好评,这非常符合Mpx的设计初衷。但是在各大厂商陆续推出自己的小程序平台,且各家的技术标准都不统一的今天,单纯地提高某一个平台的开发体验已经不能满足广大小程序开发者们的诉求,一套代码在多小程序平台运行已经成为一个现实上的刚需。为了解决这个小程序开发的痛点,Mpx发布了2.0版本,适配了目前业内已经发布的所有小程序平台(微信、支付宝、百度、头条、qq),并且提供了直接将现有微信小程序编译输出到其他平台运行的能力。

Mpx2.0版本新增的主要特性主要包含:

  • 完整支持了目前业内已发布的所有小程序平台(微信,支付宝,百度,qq,头条);
  • Mpx小程序跨平台开发,支持将已有的Mpx微信项目编译输出到其他已支持的小程序平台中运行,点击查看详情
  • 小程序原生组件跨平台编译,支持将已有的微信原生组件编译输出到其他已支持的小程序平台中运行;
  • 深度分包优化,编译过程中进行精准分包资源判断,所有分包only的资源(组件、js、外部样式、外部模板、wxs,图像媒体等)都会精确输出到分包目录中;
  • render函数中完整支持wxs模块,关于render函数点击查看详情
  • 支持了模板引入,内联wxs,自定义tabbar,独立分包,workers,云开发等原生能力,进一步完善原生兼容性。

同业内主流的小程序跨端框架相比,Mpx更专注于小程序开发本身,在小程序开发中具备以下优势:

  • 基于小程序自身的技术标准进行增强,没有进行过重的DSL转换,开发时遇到的坑会更少;
  • 完全兼容原生小程序技术规范,0成本迁移原生小程序项目;
  • 跨平台开发以跨小程序平台为目标,大部分差异抹平工作在编译阶段进行,大大减少运行时适配层增加的包体积;
  • 支持业内微信小程序组件库(如vant、iView等)直接转换到其他小程序平台运行;
  • 非常重视小程序性能,提供了深度的setData和包体积优化。

关于Mpx更详细的介绍可以查看官方文档 (opens new window)这篇文章

Github:https://github.com/didi/mpx (opens new window)

# 跨平台开发

作为2.0版本的核心能力,Mpx的跨平台开发能力允许用户直接将已有小程序项目编译输出到其他已支持的小程序平台中运行。微信小程序作为小程序概念的提出者,有着最广泛的生态覆盖,因此我们优先支持了将微信小程序编译为其他平台小程序的能力。基于这个能力,用户不仅能跨平台编译微信Mpx项目,甚至能够将微信的原生自定义组件也编译到其他小程序平台进行运行,这意味着我们的跨平台项目能够直接使用一些社区内已有的UI组件库生态(如vant、iView等),极大地提高了跨平台开发的适用范围。

# 设计理念

Mpx框架的核心设计理念在于增强,增强是指在小程序已有的原生能力基础上做加法,拓展小程序的开发能力,提高小程序的开发体验和效率。这个设计理念使Mpx给开发者带来了更强的确定性和可预期性,更低的学习上手和调试成本。基于这个理念,Mpx在不同的小程序平台中进行了差异性的增强适配,并参考各个平台的模板指令风格提供了不同的增强模板指令集,让用户在各小程序平台中都可以以增强的方式去最大限度地使用平台自有的原生能力。

我们在对Mpx提供跨平台能力的支持时也遵循了增强的核心设计理念。简单来讲,Mpx的跨平台能力是在多平台能力的基础上,在编译和运行时增加了一层转换层,将源平台的代码转换为目标平台的代码之后,再按照既有的目标平台的处理逻辑进行增强,同时我们也提供了一套完善的条件编译机制,让用户自行实现少数框架无法转换的部分。

Mpx跨平台开发流程示意图

Mpx跨平台开发流程示意图

Mpx跨平台能力设计思路明显区别于业内已有的其他小程序跨平台框架,主要差异在于:

  • Mpx以小程序本身的DSL作为基准,而没有使用web框架(React,Vue)的DSL;
  • Mpx主要通过编译和运行时转换的方式处理平台差异,没有提供额外的差异抹平层(基础组件库等)。

之所以采用这种设计,主要基于以下原因:

  • Mpx主要以跨小程序平台为目标,目前各大小程序平台的技术规范具有一定相似性,绝大部分平台差异能够通过编译和运行时手段抹平,同时省去的差异抹平层也能够进一步减少框架运行时体积;
  • 使用小程序本身的DSL作为基准允许用户直接在已有项目中使用跨平台能力,对于原生小程序项目或组件也能够使用该能力进行跨平台输出;
  • 结合完善的条件编译支持,该方案能够在满足用户跨平台需求的同时仍然允许用户最大限度地使用各个小程序平台提供的能力,完全延续了Mpx增强的核心设计理念。

# 使用方法

Mpx跨平台开发的使用方式非常简单,用户只需在MpxWebpackPlugin创建时传入mode和srcMode参数指定源平台和目标平台,当srcMode和mode不一致时,框架会读取相应的配置对项目进行编译和运行时转换。

// 微信转支付宝
-new MpxWebpackPlugin({
-  // mode指定目标平台,可选值有(wx|ali|swan|qq|tt)
-  mode: 'ali',
-  // srcMode指定源码平台,默认值同目标平台一致
-  srcMode: 'wx'
-})
-

# 差异抹平

目前各大厂商的小程序技术规范在宏观层面上大致保持一致,但是技术细节方面存在很多差异,大致划分为以下几个部分:

  • 模板语法/基础组件差异
  • json配置差异
  • wxs语法差异
  • 页面/组件对象差异
  • api调用差异
  • webview bridge差异

其中,对于模板语法/基础组件、json配置和wxs中的静态差异,我们主要通过编译手段进行转换处理,对于这部分差异中无法转换的部分会在编译阶段报错指出;而对于页面/组件对象、api调用和webview bridge中js运行时的差异,我们主要通过运行时手段进行处理,对应的无法转换部分也会在运行时中报错指出。

值得注意的是,我们在跨平台转换中做的工作不仅是对可转换的技术标准进行转换映射,对于一些目标平台中不存在的能力,我们也尽可能地通过编译和运行时手段提供了模拟和支持,最大限度地减少用户在跨平台开发中需要付出的额外工作量。以差异性最大但现实场景也最多的微信转支付宝为例,Mpx模拟提供了许多微信中支持但支付宝中未支持的能力:

  • 组件自定义事件
  • 组件间关系
  • 获取子组件实例
  • observers/property observer
  • 内联wxs
  • 外部样式类

对于原生自定义组件的跨平台转换,我们会对其进行简单的运行时注入,使其能够使用Mpx框架提供的运行时转换能力。

# 条件编译

对于框架无法抹平的差异部分,会在编译和运行时报错指出,对于这部分错误,我们提供了完善的条件编译机制让用户能够自行编写目标平台的patch进行修复,该能力也能用于实现具有平台差异性的业务逻辑。

上文中提到Mpx通过读取用户传入的mode和srcMode来决定是否以及如何对项目进行转换,mode和srcMode分别代表整个项目构建的目标平台和源平台,条件编译能够让用户在项目中创建声明了自身平台属性(localSrcMode)的文件和代码块。在项目构建中,框架会优先加载带有localSrcMode声明且localSrcMode与项目目标平台匹配(localSrcMode===mode)的文件和代码块,这部分文件和代码块需要完全依照自身声明的平台标准进行编写,Mpx不会对其进行任何编译和运行时的跨平台转换。

Mpx提供了三种维度的条件编译,分别是文件维度,区块维度和代码维度,用户可以根据平台差异的覆盖范围灵活选择使用。

# 性能优化

Mpx框架专注于小程序开发,在性能优化方面我们做过很多尝试和努力,主要集中在两个方面:

  • 运行时的setData优化
  • 编译构建时的包体积优化

# setData优化

数据响应是Mpx运行时增强的核心能力,该能力让用户在小程序开发中能够像Vue中一样使用watch和computed特性,并且用直接赋值的方式操作数据驱动视图更新,而不需要手动调用setData方法,换言之框架接管了小程序中的setData调用。

通过各大小程序平台的设计原理和性能优化建议可以得知,setData对于小程序的性能表现非常重要,而setData优化的两大方向在于:

  • 尽可能减少setData调用的频次
  • 尽可能减少单次setData传输的数据

为了实现setData的优化,我们在模板编译过程中对于每个组件的模板都生成了一个渲染函数(render function),该函数模拟模板的渲染逻辑,在每次执行时访问当次渲染所需的数据,并将当次访问过的数据路径记录下来作为函数返回值返回。

在运行时,框架会在每个组件创建时创建一个render watcher,该watcher追踪渲染函数,当渲染依赖数据发生变更时异步执行渲染函数,在render watcher回调中得到渲染函数返回的数据路径,基于这些路径与上一次的缓存数据进行diff比对,过滤掉未发生变化的数据后得到最小必要数据,最后调用setData将最小必要数据发送到真实的小程序渲染层更新视图。

基于这个机制,当数据发生变更时,只有当前渲染依赖的那部分数据发生变更才会异步地触发render watcher的执行,而每次执行后也只有实际发生变更的那部分数据会被setData发送到渲染层。这样用户就能自由地根据业务需求来操作数据,无需关注setData的调用优化,框架能够自动进行程序上最优的setData调用,在提升用户开发体验的同时也提升了程序性能。

在1.x版本中,渲染函数内无法执行wxs的逻辑,对于含有wxs的组件有可能降级到全量设置数据的模式,在2.0版本中,我们将wxs模块转译处理为js可执行的代码后注入到js bundle中,含有wxs的渲染函数也能够正常访问并执行wxs逻辑。

setData优化示意图

setData优化示意图

# 包体积优化

类似于运行时对于setData的接管,Mpx在编译阶段接管了项目的资源管理。得益于webpack强大的插件机制,Mpx开发了一个深度定制的webpack插件,基于webpack完成小程序的打包构建工作。用户在使用Mpx开发小程序时可以不受限制地使用npm依赖、最新的es特性和css预处理器等现代web开发能力。与此同时,Mpx在包体积优化上也做了很多工作,让用户专注于业务开发而无需花费过多精力进行包体积管理,我们所做的优化工作如下:

  • 打包构建工作完全基于依赖分析,任何没有被引用的资源都不会出现在dist当中;
  • 对于npm组件和页面的构建也完全基于依赖分析按需打包,不会copy整个miniprogram_dist目录,也不需要执行构建npm,使用体验和包体积均优于微信小程序自身的npm支持方案;
  • 基于webpack提供的能力进行公共模块抽取和代码压缩等优化工作;
  • 完善的分包支持,对所有资源进行从属分析,将所有分包only的资源都输出到分包目录中。

分包作为微信小程序中优化包体积的核心手段(类似于异步按需加载),Mpx对其进行了完善的支持。为了精确地标记出分包only的资源,我们在构建时将主包和分包的依赖收集步骤拆分开来串行处理,先处理主包,再处理分包。在主包的处理过程中,将主包页面中引用的所有非js资源(组件、外部样式、外部模板、wxs,图像媒体等)都记录下来,在处理分包时,对分包内引用的非js资源都进行检查,如果被主包引用过则输出到主包中,否则标记为分包only的资源输出到分包目录下。

对于js模块资源,我们在脚手架中生成的构建配置中提供了辅助函数,便于用户进行分包bundle的配置,经过该配置后,分包only的公用模块会被打入分包bundle输出到分包目录下,其余的公共模块会正常打入主bundle中。

在跨平台开发中,我们建议用户使用Mpx提供的packages来定义分包,这样在转换到不支持分包的小程序平台时会自动降级为同步包进行处理。

分包构建示意图

分包构建示意图

# 渐进迁移

Mpx提供了良好的渐进迁移支持,对于使用原生或其他小程序框架的开发者来说,采用渐进迁移的方式逐步引入Mpx进行开发成本并不大。

在2.0版本中我们进一步完善了Mpx的原生兼容性,跟进支持了各个小程序平台最新的技术能力,如自定义tabbar,独立分包,分包预加载,workers,云开发等能力,同时补齐了一些1.x版本遗漏的支持。得益于此,对于使用原生小程序开发的开发者来说,迁移Mpx的成本几乎为0,用户只需将对应页面组件的构造函数替换为Mpx提供的createPage/createCompnent,即可使用Mpx提供的各种增强能力。

对于使用其他框架的开发者,Mpx也提供了局部构建的机制,允许用户将特定的页面和组件单独构建输出为原生组件,用户只需手动或者编写脚本输出的原生组件整合进原有项目中即可。

# 未来规划

作为滴滴公司内部小程序生态的基础设施,我们会对Mpx框架进行长期的维护更新,确保能在第一时间支持各个小程序平台最新的技术特性。与此同时,我们也会进一步完善框架的基础能力,目前已排上日程待支持能力包括:

  • i18n
  • ts支持
  • 单元测试支持

在跨平台能力方面,我们也会根据社区的反馈和建议,以及小程序的标准化进程,对其进行持续的完善与更新。

最后,如果你专注小程序开发,关注开发体验和产品性能,那Mpx会是你最好的选择。

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/2.7-release.html b/docs-vuepress/.vuepress/dist/articles/2.7-release.html deleted file mode 100644 index f6ff6af0d1..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/2.7-release.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - Mpx2.7 版本正式发布,大幅提升编译构建速度 | Mpx框架 - - - - - - - - - -

# Mpx2.7 版本正式发布,大幅提升编译构建速度

作者:hiyuki (opens new window)

Mpx (opens new window)是一款开源的增强型跨端小程序框架,它具有良好的开发体验,极致的应用性能和一份源码同构输出所有小程序平台及web环境的跨端能力。近期,我们发布了框架最新的2.7版本,基于webpack5彻底重构了框架的核心编译构建流程,利用持久化缓存大幅提升了编译构建速度,最高提升可达10倍。除此之外,Mpx2.7版本还带来了一系列重要的功能更新,包括分包输出能力增强,完善的单元测试支持和用户Rules应用等,下面会对这些新特性进行逐个展开介绍。

# 编译构建提速

随着小程序生态的日渐发展壮大,各类线上小程序业务体量和复杂度的不断升级,小程序的包体积从最开始的2M以内膨胀到20M甚至30M,已经远远不复当初的""程序之名。随着小程序项目大小的不断增加,采用框架进行小程序开发的开发者们往往都会面临一个问题,即框架的编译耗时过长,Mpx也不例外。

以团队负责的小程序项目为例,该项目目前的总包体积已经超过25M,包含近30000个JS模块,400多个页面和3000多个组件,在本地进行一次完整构建需要等待15分钟,CI环境下甚至需要近半个小时,远远超出能够忍受的范围,对小程序开发体验和开发效率造成了极大的影响。虽然旧版本中的watch模式能够在很大程度上缓解我们在开发调试过程中面临的编译耗时问题,但我们在日常开发中,仍然有很多场景无法使用watch模式(首次构建/CI环境/需真机预览等),基于内存缓存的watch模式也无法长期运行。对于这个问题,我们在过去也做了很多技术尝试,如支持watch:prod模式,局部编译,多线程编译,DLL预编译等等,但是整体尝试下来这些方案要么收效有限要么适用面不足,都没能探索到一个能从根本上解决问题的方案。

直到2020年底,webpack5正式发布,基于文件系统持久化缓存能力的出现,让我们看到了该问题的解决之道。由于webpack5相较于webpack4有着非常多的升级改动,而Mpx的编译构建在过去版本中也存在着各种历史遗留问题,我们花了较长的时间吃透webpack5的源码以及重新思考Mpx中存在的不合理设计。经过3个多月的开发,我们彻底重构了Mpx的核心编译构建流程,使其能够完整安全地利用webpack5持久化缓存能力进行构建提速,同时也彻底解决了旧版Mpx中存在的历史遗留问题,在去年10月完成了beta版的开发与发布,并在业务中进行了初步的落地尝试。之后又经过2个多月的业务回归测试和框架功能补全,我们在beta版的基础上又迭代修复了近30个patch版本,在业务上完成了充分的回归测试,让我们确信目前的新版本已经达到了能够release的阶段。

为什么不是Vite

聊起编译提速,可能大部分人首先想到的是Vite,诚然Vite是一个极富创造力的技术方案,但是在小程序场景下,却不一定是最合适的方案,这主要源于Vite最核心的设计利用了现代浏览器原生支持的ESM,而目前没有任何一家的小程序环境能够原生支持ESM,这就使得Vite最核心的按需编译能力无法得到发挥,而Vite使用esbuild带来的编译速度提升,在webpack环境中也可以选择esbuild-loader提供的能力来替换babel/terser,而且目前esbuild提供的编译能力成熟度还远不能和babel/terser相比,再加上Mpx的编译构建流程很大程度上依赖了webpack提供的能力,从成本和收益上考虑采用webpack5对于我们来说无疑是更好的方案。

以团队负责的小程序项目为例,我们来看一下Mpx2.7版本带来的编译提速效果:相较于旧版长达15分钟的构建耗时,在无缓存环境下,新版的构建耗时约为8分钟,速度提升了近1倍;而在有缓存环境下,新版构建耗时可以缩短至80s,速度提升了10倍以上!随着我们对CI流程的持久化缓存改造完成,可以确保在日常的大部分构建场景都会在有缓存的环境下进行。

# 分包输出能力增强

在Mpx2.7版本中,我们对小程序分包能力的支持进行了进一步的完善增强。

# 独立分包初始化模块

在过去的版本中,我们对独立分包进行过专门的构建支持,以满足独立分包资源独占的要求。不过在使用独立分包进行业务开发时,往往会面临的一个棘手问题在于初始化逻辑无处安放。这是由于独立分包没有app.js,而在小程序中,组件的js逻辑会早于页面js执行,具体的执行顺序又和组件的嵌套关系有关,因此我们无法找到一个确定的代码位置来存放独立分包的初始化逻辑。

在Mpx2.7版本中,我们针对独立分包新增了一个全新的增强特性,让用户能够声明独立分包的初始化模块,该模块将会在独立分包启动时全局最先执行,其实现思路大致如下:在构建时为独立分包中所有的组件和页面都添加模块引用,指向用户声明的初始化模块,这样在独立分包启动时,不管哪个组件/页面的js最先执行,都能保障这个初始化模块最先执行,同时由于模块缓存的存在,后续的组件/页面执行时,该模块也不会被重复执行。

该特性的详细使用方式可以参考我们的文档说明 (opens new window)

# 分包异步化

分包异步化 (opens new window)是微信小程序在去年下半年提出的全新技术特性,该特性打破了传统分包只能引用自身和主包资源的规则限制,通过相关配置和声明,允许分包异步地引用其他分包内的资源,对于复杂小程序的包体积和加载性能优化具有极其重要的意义。

在过去,受限于小程序分包资源引用规则,Mpx在编译构建时对于跨分包共用的资源有两种处理策略,其一是将其输出到主包当中,让多个分包都能够通过主包访问,这种策略下可以达到总包体积最优,但是往往会对主包体积造成过大的压力。当主包超出2m限制时,我们就需要采用第二种策略,将这部分跨分包共用的资源冗余地输出到各自的分包中,消除其对于主包体积的占用。在实际的Mpx编译构建当中,这两种策略是同时存在的,具体什么时候采用哪种策略是根据资源类型和用户配置来决定的。

由于分包异步化技术打破了传统分包资源引用规则的限制,理想情况下我们可以做到主包不超限的同时总包无冗余,不过该技术目前也存在一些不足:一是跨平台支持度不佳,只有微信支持,不过据我们了解支付宝目前也在跟进中;二是对交互和体验会带来一些影响,同时存在业务改造成本,但这依然不妨碍该技术成为大型小程序优化包体积和加载性能的最优路径。

分包异步化资源引用

我们在Mpx2.7版本中对分包异步化中最常用的跨分包自定义组件引用进行了完整支持,与原生小程序不同,Mpx中资源的分包归属不由源码位置决定,而是由资源引用关系决定,因此在跨分包资源引用的场景下,用户需要声明引用的资源属于哪个分包,简单使用示例如下:

{
-  "usingComponents": {
-    // 通过root query声明组件所属的分包,与packages语法下使用root query声明package所属分包的语义保持一致
-    "button": "../subPackageA/components/button?root=subPackageA",
-    "list": "../subPackageB/components/full-list?root=subPackageB",
-    "simple-list": "../components/simple-list"
-  },
-  "componentPlaceholder": {
-    "button": "view",
-    "list": "simple-list"
-  }
-}
-

详细使用方式可参考文档说明 (opens new window)

对于另一项跨分包JS代码引用能力的使用支持,我们目前也正在探索规划中,预计能在今年Q1内完成开发支持。

# 单元测试支持

Mpx自从20年开始就对单元测试有了初步的支持,但过去的单测方案在设计上存在一些缺陷,可用性不高,业务落地困难,在Mpx2.7版本中,我们设计了一套全新的技术方案,克服了原有方案中存在的所有问题,在可用性上得到了质的飞跃,新旧方案的对比如下:

旧方案:通过Mpx编译构建预先将完整的项目源码构建输出为原生小程序格式,再通过jest + miniprogram-simulate加载构建产出的原生小程序组件来执行测试case。该方案的优点在于编译流程统一,方案实现成本较低,缺点在于执行任何case都需要执行完整的构建流程,耗时较长;而且由于构建本身不基于jest进行,也无法使用jest提供的模块mock功能。

旧版单测方案

新方案:我们fork了miniprogram-simulate仓库对其扩展了load mpx组件的能力,在资源加载的transform过程中通过mpx-jest插件将mpx组件编译为原生小程序组件,再将内容传递给miniprogram-simulate执行渲染并运行测试case。该方案中模块加载完全基于jest并能够实现组件的按需编译,完美规避旧方案中存在的问题,缺点在于编译流程基于jest api重构,与mpx基于webpack的编译流程独立,存在额外的维护成本,后续我们会将通用的编译逻辑抽离维护,在webpack和jest两侧复用。

新版单测方案

关于单元测试更详细的使用指南可参考文档说明 (opens new window)

随着单测方案的完善,我们今年也会推动单元测试在小程序业务中全面落地,更好地保障代码质量与业务稳定。

# Module.rules复用

Mpx的单文件支持很大程度上参考了vue-loader的设计,在vue-loader@15版本前,对于单文件组件中各个区块(block)的loaders应用逻辑默认内置在loader当中,如需对某些区块进行自定义配置,需要向loader的options中传递额外的loader应用规则,无法复用webpack配置的module.rules中已经定义好的规则,这往往会导致我们需要在loader options和module.rules中维护重复的loader规则,同样的问题也存在于旧版的mpx-loader中。

在vue-loader@15版本发布之后,其通过克隆用户原始的rules的方式实现了module.rules的复用,用户不再需要往loader options中传递冗余的loaders规则,本次全新Mpx2.7版本也支持了该特性,我们使用了webpack提供的matchResource能力实现了module.rules的复用,该方案相较于clone rules的方式实现起来更加简洁优雅。

以上就是Mpx2.7版本的核心更新内容,目前使用脚手架工具创建的项目默认都会使用2.7版本,过往项目如需迁移升级可以查看这篇指南 (opens new window)

# 即将到来的新特性

以上就是我们本次Mpx2.7版本带来的全部特性,后续我们也有计划撰写一系列文章对其中的技术细节进行更加深入的分析与介绍,除了上面介绍到的特性外,我们还有一系列特性处于即将发布的状态:

# 未来规划

未来,我们的工作重心将重新回到运行时,重点致力于composition api编码规范支持和数据响应系统的升级,在保障性能和包体积方面优势的同时,带给开发者用户与时俱进的开发体验。

在跨端方面,我们与移动端跨端框架Hummer (opens new window)进行了合作,初步探索了Mpx基于Hummer输出为移动端原生应用的可行性,目前整体流程已经基本跑通,待后续能力逐步完善和业务充分落地后,就会在开源版本中发布。

与此同时,Mpx-cube-ui小程序跨端组件库也在持续地开发完善中,目前业务落地效果良好,预计会在今年下半年正式开源。

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html b/docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html deleted file mode 100644 index 3d0f341bd0..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/2.8-release-alter.html +++ /dev/null @@ -1,174 +0,0 @@ - - - - - - 通过 Mpx 使用组合式 API 进行小程序开发 | Mpx框架 - - - - - - - - - -

# 通过 Mpx 使用组合式 API 进行小程序开发

滴滴开源小程序跨端框架 Mpx (opens new window) 自18年立项开源以来,如今已经进入第五个年头,期间 Mpx 在有效支持了公司内外小程序业务开发的同时,也因其高性能、优体验、跨平台的特性获得了开发者用户的一致好评。

为了不辜负开发者用户对我们的信赖,更好地支持小程序业务开发,一方面我们对 Mpx 的稳定版本进行着高频的维护迭代,快速响应处理开发者用户在框架使用过程中遇到的问题;另一方面我们持续跟进探索业内最新动态,力求带给用户更好的开发体验与更强大的开发能力。继我们在 2.7 版本 (opens new window)中对 Mpx 的编译系统进行 Webpack5 适配 ,完整支持持久化缓存并大幅提升编译速度后,在 2.8 版本 (opens new window)中,我们对 Mpx 的框架运行时也进行了重构改造,完整支持 Vue3 中的组合式 API 开发范式,让用户能够使用时下最热门的开发方式进行小程序业务开发。

我们先来简单感受一下组合式 API 的使用:

<template>
-  <view>{{ collectionName }}: {{ book.title }}({{ readersNumber }})</view>
-  <button bindtap="addReaders">addReaders</button>
-</template>
-
-<script>
-  import { createComponent, ref, reactive, onMounted } from '@mpxjs/core'
-
-  createComponent({
-    properties: {
-      collectionName: String
-    },
-    setup () {
-      const readersNumber = ref(0)
-      const book = reactive({ title: 'Mpx' })
-
-      onMounted(() => {
-        console.log('Component mounted.')
-      })
-
-      // 暴露给 template
-      return {
-        readersNumber,
-        book,
-        addReaders () {
-          readersNumber.value++
-        }
-      }
-    }
-  })
-</script>
-

可以看出和 Vue3 组合式 API 的使用是高度类似的,利用框架导出的一系列响应式 API 和 生命周期钩子函数在 setup 中编写业务逻辑,并将模板依赖的数据与方法作为返回值返回,与传统的选项式 API 相比,组合式 API 具备以下优势:

  • 更好的逻辑复用,通过函数包装复用逻辑,显式引入调用,方便简洁且符合直觉,规避消除了 mixins 复用中存在的缺陷;
  • 更灵活的代码组织,相比于选项式 API 提前规定了代码的组织方式,组合式 API 在这方面几乎没有做任何限制与规定,更加灵活自由,在功能复杂的庞大组件中,我们能够通过组合式 API 让我们的功能代码更加内聚且有条理,不过这也会对开发者自身的代码规范意识提出更高要求;
  • 更好的类型推导,虽然基于 this 的选项式 API 通过 ThisType 类型体操的方式也能在一定程度上实现 TS 类型推导,但推导和实现成本较高,同时仍然无法完美覆盖一些复杂场景(如嵌套 mixins 等);而组合式 API 以本地变量和函数为基础,本身就是类型友好的,我们在类型方面几乎不需要做什么额外的工作就能享受到完美的类型推导。

同时与 React Hooks 相比,组合式 API 中的 setup 函数只在初始化时单次执行,在数据响应能力的加持下大大降低了理解与使用成本,基于以上原因,我们决定为 Mpx 添加组合式 API 能力,让用户能够用组合式 API 方式进行小程序开发。

# 组合式 API 实现

从上面的简单示例中可以看出,抛开响应式 API 和生命周期注册模式的变化,组合式 API 的实现要点在于动态添加模板依赖的数据和方法,这也是我们在小程序中实现组合式 API 可能遇到的核心技术卡点。

对于动态添加模板依赖数据,我们在过去的实践中已经充分证明了其可行性,事实上,从 Mpx 最初的版本开始,我们就充分利用了这项能力来实现我们对计算属性和 dataFn (类似于 Vue 使用函数定义 data)的支持,这项能力的关键在于存在合适的生命周期用于动态添加初始化数据,这里对于初始化数据的定义是能够影响组件树的初始渲染,举个简单的例子:存在一对父子组件 parent/child,parent 使用 props 向 child 传递数据,当我们在 parent 初始创建时使用 setData 动态添加 props 数据,同时 child 在初始创建时能够通过 props 正确获取到这部分的数据时,我们就可以将这部分动态添加的数据视作初始化数据,这是我们在小程序中实现完备数据响应支持的基础。

幸运的是,目前业内所有主流小程序平台(微信/支付宝/百度/字节/QQ)都支持了上述能力,微信从一开始就支持在 attached 生命周期中调用 setData 函数动态添加初始化数据,在上述的父子 props 传递场景中,也能够在子组件的 attached 中正确获取这部分数据,支付宝和字节小程序一开始并不支持该能力,不过支付宝在 component2 组件系统重构后,字节在橙心合作项目中与我们沟通后,都成功支持了该能力。

而对于动态返回的方法,最简单能想到的方案就是直接挂载到组件实例上,经过我们的完整测试,上述业内主流小程序平台都支持使用这种方式动态添加方法,基于以上事实,我们非常确定组合式 API 能够在小程序环境中顺利实现,下图简要展示了 Mpx 支持组合式 API 的初始化流程:

composition-api-init

# 生命周期钩子函数

在组合式 API 中,setup 函数只有在组件创建时初始化单次执行,因此需要提供一系列生命周期钩子函数来代替选项式 API 中的生命周期钩子选项,由于小程序原生只支持选项式的生命周期注册方式,我们通过预注册 -> 驱动的方式来实现 setup 中函数式注册生命周期钩子的语法糖,简单来讲就是使用选项式 mixins 的方式提前注册所有需要的生命周期钩子,在选项式生命周期钩子执行时驱动对应在 setup 中使用生命周期钩子函数注册的代码逻辑执行,如下图所示:

composition-api-hook

作为跨端小程序框架,Mpx 需要兼容不同小程序平台不同的生命周期,在选项式 API 中,我们在框架中内置了一套统一的生命周期,将不同小程序平台的生命周期转换映射为内置生命周期后再进行统一的驱动,以抹平不同小程序平台生命周期钩子的差异,如微信小程序的 attached 钩子和支付宝小程序的 onInit 钩子,在组合式 API 中,我们沿用了同样的逻辑,设计了一套与框架内置生命周期对应的生命周期钩子函数,以相同的方式进行驱动,因此这些生命周期钩子函数天然具备了跨平台特性,下表显示了在组件 / 页面中框架生命周期与原生平台生命周期的对应关系:

框架内置生命周期 Hooks in setup 微信原生 支付宝原生
BEFORECREATE null attached(数据响应初始化前) onInit(数据响应初始化前)
CREATED null attached(数据响应初始化后) onInit(数据响应初始化后)
BEFOREMOUNT onBeforeMount ready(MOUNTED 执行前) didMount(MOUNTED 执行前)
MOUNTED onMounted ready(BEFOREMOUNT 执行后) didMount(BEFOREMOUNT 执行后)
BEFOREUPDATE onBeforeUpdate nullsetData 执行前) nullsetData 执行前)
UPDATED onUpdated nullsetData callback) nullsetData callback)
BEFOREUNMOUNT onBeforeUnmount detached(数据响应销毁前) didUnmount(数据响应销毁前)
UNMOUNTED onUnmounted detached(数据响应销毁后) didUnmount(数据响应销毁后)
ONLOAD onLoad onLoad onLoad
ONSHOW onShow onShow onShow
ONHIDE onHide onHide onHide
ONRESIZE onResize onResize events.onResize

同 Vue3 一样,Mpx 在组合式 API 中没有提供 BEFORECREATECREATED 对应的生命周期钩子函数,用户可以直接在 setup 中编写相关逻辑。

# 具有副作用的页面事件

在小程序中,一些页面事件的注册存在副作用,即该页面事件注册与否会产生实质性的影响,比如微信中的 onShareAppMessageonPageScroll,前者在不注册时会禁用当前页面的分享功能,而后者在注册时会带来视图与逻辑层之间的线程通信开销,对于这部分页面事件,我们无法通过预注册 -> 驱动方式提供组合式 API 的注册方式,用户可以通过选项式 API 的方式来注册使用,通过 this 访问组合式 API setup 函数的返回。

然而这种使用方式显然不够优雅,我们考虑是否可以通过一些非常规的方式提供这类副作用页面事件的组合式 API 注册支持,例如,借助编译手段。我们在运行时提供了副作用页面事件的注册函数,并在编译时通过 babel 插件的方式解析识别到当前页面中存在这些特殊注册函数的调用时,通过框架已有的编译 -> 运行时注入的方式将事件驱动逻辑添加到当前页面当中,以提供相对优雅的副作用页面事件在组合式 API 中的注册方式,同时不产生非预期的副作用影响,简单示例如下:

import { createPage, ref, onShareAppMessage } from '@mpxjs/core'
-
-createPage({
-  setup () {
-    const count = ref(0)
-
-    onShareAppMessage(() => {
-      return {
-        title: '页面分享'
-      }
-    })
-
-    return {
-      count
-    }
-  }
-})
-

目前我们通过这种方式支持的页面事件如下:

页面事件 Hooks in setup 平台支持
onPullDownRefresh onPullDownRefresh 全小程序平台 + web
onReachBottom onReachBottom 全小程序平台 + web
onPageScroll onPageScroll 全小程序平台 + web
onShareAppMessage onShareAppMessage 全小程序平台
onTabItemTap onTabItemTap 微信/支付宝/百度/QQ
onAddToFavorites onAddToFavorites 微信 / QQ
onShareTimeline onShareTimeline 微信
onSaveExitState onSaveExitState 微信

特别注意,由于静态编译分析实现方式的限制,这类页面事件的组合式 API 使用需要满足页面事件注册函数(如onShareAppMessage)的调用和 createPage 的调用位于同一个 js 文件当中。

关于生命周期钩子函数的更多信息可以查看这里 (opens new window)

# <script setup>

同 Vue3 一样,我们在 .mpx 单文件组件 / 页面中实现了 <script setup> 的组合式 API 编译语法糖,相较于常规的写法,<script setup> 具备以下优势:

  • 更少的样板内容,更简洁的代码
  • 能够使用纯 TypeScript 声明 props 类型
  • 更好的 IDE 类型推导性能

简单使用示例如下:

<script setup>
-  import { ref } from '@mpxjs/core'
-
-  const msg = ref('hello')
-
-  function log () {
-    console.log(msg.value)
-  }
-</script>
-<template>
-  <view>msg: {{msg}}</view>
-  <view ontap="log">click</view>
-</template>
-

可以看到使用方式与 Vue3 基本一致,不过由于 Mpx 的组合式 API 设计实现与 Vue3 存在差异,对应 <script setup> 也与 Vue3 中存在一些差异:

  • 不支持 import 快捷注册组件
  • 没有 defineEmits() 编译宏
  • 没有 useSlots()useAttrs() 运行时函数
  • 以编译宏的形式提供了 useContext(),获取 setup 函数的第二个参数 context
  • defineExpose() 编译宏的作用与 Vue3 中有所差别,能够限定模板中能访问的变量范围

特别注意,受小程序底层技术限制,我们在 Mpx 的实现中无法像 Vue3 那样将模板编译的渲染函数和 <script setup> 放置到同一作用域下进行变量访问,而是通过静态编译分析提取出 <script setup> 的顶层作用域变量,再以上文中提到的动态添加数据与方法的方式将其设置到模板当中,如果 <script setup> 中声明了较多顶层作用域变量,它们并不一定都会被模板访问,就会带来无效的性能开销,因此我们强烈建议使用 defineExpose() 限定模板中能访问的变量范围,你可以把它等同于 setup 函数中的 return

关于 <script setup> 的更多信息可以查看这里 (opens new window)

# 组合式 API 与 Vue3 中的差异

我们来总结一下 Mpx 中组合式 API 与 Vue3 中的区别:

关于组合式 API 的更多信息可以查看这里 (opens new window)

# 响应式 API 实现

组合式 API 的正常工作离不开响应式 API 的支持,下面我们来聊聊 Mpx 中响应式 API 的设计实现。我们知道 Vue3 中响应式 API 基于 proxy 进行了重构实现,但是目前 proxy 的浏览器兼容占比仍然无法达到我们对于线上可用性的要求,因此在 Mpx 中,我们仍然基于 Object.defineProperty 进行核心数据响应能力的实现,同时借鉴了 Vue3 中优秀的代码设计与实现,如 reactiveEffecteffectScope 等,尽可能实现与 Vue3 中响应式 API 能力对齐。

说到这里,很多同学可能会想到 @vue/composition-api 这个库,该库提供的关键能力正是基于 Vue2 的数据响应系统模拟实现 Vue3 中的响应式 API,我们在前期也对 @vue/composition-api 在 Mpx 中的复用进行了非常有价值的探索尝试。不过最终我们还是决定在 Mpx 的运行时框架中进行独立实现,原因主要在于:@vue/composition-api 是作为一个 Vue2 插件存在,无法直接侵入 Vue2 源码,导致部分能力无法实现或会带来额外的性能开销,例如 flush: 'post'ref 自动解包等。我们也看到在最新的 Vue2.7 版本中,也是在运行时框架里重新实现了这部分内容,以规避上述问题。

下图展示了 Mpx 中响应式 API 核心模块依赖关系:

composition-api-reactive

# 数据响应限制带来的差异

由于 Object.defineProperty 的能力限制,Mpx 存在和 Vue2 一致的数据响应限制,无法感知到对象 property 的添加和删除以及数组的索引赋值,与 Vue2 一致,我们暴露了 setdel API 来让用户显式地进行相关操作。除此之外,由于使用方式发生了变化,我们在使用 reactive API 创建响应式数据时,还会遇到新的限制,我们来看一下代码示例:

import { reactive, watchSyncEffect, set } from '@mpxjs/core'
-
-const state = reactive([0, 1, 2, 3])
-
-watchSyncEffect(() => {
-  console.log(JSON.stringify(state)) // [0,1,2,3]
-})
-
-set(state, 1, 3) // 不会触发 watchEffect
-
-state.push(4) // 不会触发 watchEffect
-

可以看出,即使我们使用了 set API 和数组原型方法对数组进行修改,我们仍然无法监听到数据变化。

相同的限制在使用 Object.defineProperty 的 Vue2.7 中也同样存在。

为什么会存在这个限制呢?原因在于:基于 Object.defineProperty 实现的数据响应系统中,我们会对对象的每个已有属性创建了一个 Dep 对象,在对该属性进行 get 访问时通过这个对象将其与依赖它的观察者 ReactiveEffect 关联起来,并在 set 操作时触发关联 ReactiveEffect 的更新,这是我们大家都知道的数据响应的基本原理。但是对于新增/删除对象属性和修改数组的场景,我们无法事先定义当前不存在属性的 get/set (当然这在 proxy 当中是可行的),因此我们会把对象或者数组本身作为一个数据依赖创建 Dep 对象,通过父级访问该数据时定义的 get/set 将其关联到对应的 ReactiveEffect,并在对数据进行新增/删除属性或数组操作时通过数据本身持有的 Dep 对象触发关联 ReactiveEffect 的更新,如下图所示:

数据响应原理

需要注意的是,通过父级访问是建立 DepReactiveEffect 关联关系的先决条件,在选项式 API 中,我们访问组件的响应式数据都需要通过 this 进行访问,相当于这些数据都存在 this 这个必要的父级,因此我们在使用 $set/$delete 进行对对象进行新增/删除属性或对数组进行修改时都能得到符合预期的结果,唯一的限制在于不能新增/删除根级数据属性,原因就在于 this 不存在访问它的父级。

但是在组合式 API 中,我们不需要通过 this 访问响应式数据,因此通过 reactive() 创建的响应式数据本身就是根级数据,我们自然无法通过上述方式感知到根级数据自身的变化(在 Vue3 中,基于 proxy 提供的强大能力响应式系统能够精确地感知到数据属性,甚至是当前不存在属性的访问与修改,不需要为数据自身建立 Dep 对象,自然也不存在相关问题)。

在这种情况下,我们就需要用 ref() 创建响应式数据,因为 ref 创建了一个包装对象,我们永远需要通过 .value 来访问其持有的数据(不管是显式访问还是隐式自动解包),这样就能保证 ref 数据自身的变化能够被响应式系统感知,因此也不会遇到上面描述的问题,如下所示:

import { ref, watchSyncEffect, set } from '@mpxjs/core'
-
-const state = ref([0, 1, 2, 3])
-
-watchSyncEffect(() => {
-  console.log(JSON.stringify(state.value)) // [0,1,2,3]
-})
-
-set(state.value, 1, 3) // [0,3,2,3]
-
-state.value.push(4) // [0,3,2,3,4]
-

# 响应式 API 与 Vue3 中的区别

我们来总结一下 Mpx 中响应式 API 与 Vue3 中的区别:

  • 不支持 raw 相关 API(markRaw 除外,我们提供了该 API 用于跳过部分数据的响应式转换)
  • 不支持 readonly 相关 API
  • 不支持 watchEffectwatchcomputed 的调试选项
  • 不支持对 mapset 等集合类型进行响应式转换
  • 受到 Object.defineProperty 实现带来的数据响应限制影响

关于响应式 API 的更多信息可以查看这里 (opens new window)

# 生态周边适配

除了 Mpx 运行时核心提供了组合式 API 支持外,我们对 Mpx 的周边生态能力也都进行了组合式 API 适配支持,包括 storei18nfetch 等。

# Pinia store 支持

Pinia 是基于组合式 API 设计的全新数据管理方案,目前已经取代 Vuex 成为 Vue3 官方推荐的 store,我们在研究了 pinia 的设计实现后,对其简练优雅的设计思想及其与组合式 API 的高度适配非常满意(特别是在使用 setup 函数创建 store 时,使用心智与编写组件完全一致,可以将其视作是没有视图的组件)。因此我们 fork 了 pinia 的源码仓库,基于 Mpx 提供的数据响应能力对其进行了适配改造,使其在 Mpx 环境下也能正常运行,目前相关代码维护在 @mpxjs/pinia 中,在组合式 API 中的简单使用示例如下:

import { createComponent, ref, computed, toRefs } from '@mpxjs/core'
-import { defineStore } from '@mpxjs/pinia'
-
-// 使用组合式 API 创建 pinia store 的使用心智与 setup 函数完全一致,强烈推荐
-const useSetupStore = defineStore('setup', () => {
-  const count = ref(0)
-  const doubleCount = computed(() => count.value * 2)
-
-  function increment () {
-    count.value++
-  }
-
-  return { count, doubleCount, increment }
-})
-
-createComponent({
-  setup () {
-    const store = useSetupStore()
-    // store 同 props 类似是一个 reactive 对象,解构数据需使用 toRefs 以保持数据响应性
-    const { count, doubleCount } = toRefs(store)
-    // 方法可以直接解构
-    const { increment } = store
-  
-    return { count, doubleCount, increment }
-    //
-  }
-})
-

Mpx 中通过 createStore 创建的类 Vuex store 在组合式 API 中仍然可以使用,我们可以在 setup 函数中引用 store 实例进行数据读取与方法调用 (opens new window),不过整体使用体验与 pinia store 存在较大差距,我们还是推荐在组合式 API 开发中优先使用 pinia store 作为数据管理方案。

# I18n 支持

传统选项式 API 中,我们使用 this.$t 方法在组件内调用翻译函数,但在组合式 API 中我们无法访问 this,为此我们参考了 Vue I18n 最新的 9.x 版本,该版本针对 Vue3 及组合式 API 进行了重构适配,提供了全新的 useI18n API,简单使用示例如下:

<template>
-  <view>{{t('message.hello')}}</view>
-  <button bindtap="changeLocale">change locale</button>
-</template>
-
-<script>
-  import { createComponent, useI18n } from '@mpxjs/core'
-
-  createComponent({
-    setup () {
-      // useI18n 不传参数时指向全局 i18n 对象,也可以传递 locale 和 messages 配置创建局部 i18n 对象
-      const { t, locale } = useI18n()
-
-      function changeLocale () {
-        locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
-      }
-      // 返回的翻译方法名必须为 t,不能进行重命名
-      return { t, changeLocale }
-    }
-  })
-</script>
-

上面示例代码看上去像是我们在模板上直接调用 setup() 返回的 t 翻译方法,但是熟悉小程序开发的同学都知道在小程序架构下这是不可能的,示例中的写法其实由框架通过编译 + 运行时手段实现的语法糖,我们会在模板编译时定向扫描 t/te/tm 等 i18n 方法,将其转换为计算属性注入到运行时当中,这就意味着如果我们对翻译方法进行重命名,模板编译时无法识别出 i18n 方法调用,自然也就无法正常运行。

Mpx 中 i18n 提供了两种实现模式,分别是 wxs 和 computed,可以使用编译选项中的 i18n.isComputed 进行切换,两种方式各有优劣,其中:

  • wxs 模式的优势在于逻辑层和视图层独立维护语言集,无额外运行时性能开销,且使用没有任何限制;劣势同样源于语言集同时存在于逻辑层(js)和视图层(wxs)当中,这部分的包体积占用翻倍;
  • computed 模式的优势在于语言集只存在于逻辑层中,无额外包体积占用,且可以通过动态添加语言集的方式进一步减少包体积占用;劣势则是会产生额外的运行时性能开销,且使用上存在限制,模板调用时无法直接访问 wx:for 中的 itemindex

在组合式 API 中模板上使用 useI18n() 返回的翻译函数 t/te/tm 时,为了完整实现 useI18n API的功能,会强制使用 computed 模式进行实现,这也意味着该用法会受到 computed 模式使用限制的影响。不过当你不需要使用 useI18n 接受 messages 参数创建局部语言集作用域功能时,你也完全可以在模板中继续使用原有的 $t/$tc/$te/$tm 方法,这些方法受编译选项 i18n.isComputed 的影响,同时指向全局语言集作用域。

更多关于生态周边的组合式 API 使用指南可以点击下方链接查看详情:

# 输出 web 适配

跨端输出 web 作为 Mpx 的一大核心特性,在业务中存在广泛使用,同时也是我们设计实现任何框架新特性需要优先考虑的事项。在本次组合式 API 支持中,我们从设计之初就考虑了跨端输出 web 的适配支持,保障使用 Mpx 组合式 API 开发的业务代码都能在 web 环境中正常运行。

我们输出 web 的整体技术方向在于尽可能复用 Vue 已有的生态能力,为了实现这个目标,我们需要提供尽可能与 Vue 保持一致的 API 设计,以降低抹平适配成本。在输出 web 时,核心组合式 API 基于 Vue2.7 版本中的已有能力进行适配提供,简单举个例子:对于 import { ref } from 'mpxjs/core' 这行语句,在小程序中会指向 Mpx 内部维护的 ref 实现,而在输出 web 时会指向 Vue 中维护的 ref 实现,两者的实现虽然不仅相同,但只要保障对外函数签名一致,对于开发者用户来说就无感知。

我们借助了 Mpx 强大的条件编译能力进行上述实现,对运行时导出根据输出平台进行重定向,这样还能保障跨端输出产物干净简洁,仅包含当前输出环境下必要的逻辑,如下图所示:

composition-api-web

同理,我们也采用了类似的方式实现了组合式 API 周边能力对于输出 web 的适配支持,如pinia store 使用 pinia 原始版本进行适配实现,而 i18n 能力则是使用 vue-i18n@8.x + vue-i18n-bridge 进行适配实现。

# 性能表现

性能是 Mpx 一直以来的核心关注点,我们对组合式 API 的最终实现版本进行了一系列性能评估测试,我们使用组合式 API 版本对业务中的评价组件进行了重构,评价组件属于我们业务中交互及功能相对比较复杂的组件,源码行数约 1000 行,组件数据 27 项,组件方法 18 个,我们在测试项目中对选项式 API 和组合式 API 两个版本实现的组件进行了一系列测试。

# 组件初始化耗时

由于组合式 API 改变了原有的组件初始化流程,我们对组件的初始化耗时进行了重点测试,测试口径如下:

  • 耗时计算以挂载组件为起点,以组件 ready 执行为终点
  • 测试结果为10次手工测试排除最大最小值后求均值
  • IOS 测试机型为 iPhone 13 pro max,安卓测试机型为 OPPO R9

结果显示两个版本的组件初始化耗时大抵持平,不分优劣。

IOS 安卓
选项式 API 42.5ms 366.6ms
组合式 API 42.4ms 370.1ms

# 组件 JS 体积

在构建产物体积方面,由于组合式 API 的写法对于 JS 代码压缩更加有利,同样的逻辑实现下,组合式 API 版本的组件构建压缩后 JS 体积略胜一筹。

组件 JS 体积
选项式 API 15.67KB
组合式 API 13.60KB

# 框架运行时体积

在 Mpx2.8 版本中,我们在框架运行时中新增了组合式 API 相关实现,不过通过优化运行时导出,使其对 tree shaking 更加友好,我们的框架运行时体积在实际构建产物中没有产生太大增长。

框架运行时体积
选项式 API 51.66KB
组合式 API 57.47KB

综上所述,组合式 API 版本的运行时性能与选项式 API 大抵持平,在包体积占用方面,新版框架运行时体积占用略有提升,不过由于组合式 API 开发模式对代码压缩更友好,加上组合式 API 更易进行逻辑复用的特点,我们预计在实际业务项目中,组合式 API 的包体积占用会更小。

# 破坏性改变

Mpx 组合式 API 版本完全兼容原有的选项式 API 开发方式,不过我们在运行时重构过程中依然带来了少量的破坏性改变,详情如下:

  • 框架过往提供的组件增强生命周期 pageShow/pageHide 与微信原生提供的 pageLifetimes.show/hide 完全对齐,不再提供组件初始挂载时必定执行 pageShow 的保障(因为组件可能在后台页面进行挂载),相关初始化逻辑一定不要放置在 pageShow 当中;
  • 取消了框架过去提供的基于内部生命周期实现的非标准增强生命周期,如 beforeCreate/onBeforeCreate 等,直接将内部生命周期变量导出提供给用户使用,详情查看这里 (opens new window);
  • 为了优化 tree shaking,作为框架运行时 default exportMpx 对象不再挂载 createComponent/createStore 等运行时方法,一律通过 named export 提供,Mpx 对象上仅保留 set/use 等全局 API,详情查看这里 (opens new window)
  • 使用 I18n 能力时,为了与新版 vue-i18n 保持对齐,this.$i18n 对象指向全局作用域,如需创建局部作用域需要使用组合式 API useI18n 的方式进行创建。
  • watch API 不再接受第二个参数为带有 handler 属性的对象形式(该参数形式只应存在于 watch option 中),第二个参数必须为回调函数,与 Vue (opens new window) 对齐。

更详细的迁移指南请点击查看这里 (opens new window)

# 未来规划

目前 Mpx2.8 版本已经在以滴滴出行小程序和花小猪小程序为代表的集团小程序业务中稳定全面落地,并在新的业务迭代中大范围使用了组合式 API,使用反馈良好,在社区内也产出了多个成功案例。

在完成持久化构建缓存和组合式 API 两个重大技术升级后,我们未来的技术规划如下:

  • 组合式 API 工具库 @mpxjs/use
  • 内置原子类支持
  • 输出 web 支持 SSR
  • 输出 web 支持使用 Vite 构建
  • 跨端输出 Hummer 合入主干正式 release
  • 优化运行时 render 函数,降低包体积占用
  • 跨端库 @mpxjs/cube-ui 开源

最后,再次感谢所有参与 Mpx 组合式 API 技术建设的同学们,也欢迎社区同学一同加入 Mpx 项目开源共建。

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/2.8-release.html b/docs-vuepress/.vuepress/dist/articles/2.8-release.html deleted file mode 100644 index 6bc81b2c74..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/2.8-release.html +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - Mpx2.8 版本正式发布,使用组合式 API 开发小程序 | Mpx框架 - - - - - - - - - -

# Mpx2.8 版本正式发布,使用组合式 API 开发小程序

作者:hiyuki (opens new window)

小程序跨端开发框架 Mpx (opens new window) 自18年立项开源以来,如今已经走过了第四个年头,其高性能、优体验、跨平台的特性收获了公司内外开发者用户的一致好评。

为了不辜负开发者用户对我们的信赖,更好地支持集团小程序业务开发,一方面我们对 Mpx 的稳定版本进行着高频的维护迭代,快速响应处理集团内外开发者用户在框架开发使用过程中遇到的问题;另一方面我们持续跟进探索业内最新动态,力争将更新更好的开发能力与体验带给小程序开发者用户。继年初我们在 2.7 版本 (opens new window)中对 Mpx 的编译系统进行重构适配 Webpack5,基于持久化缓存大幅提升编译速度后,在最新的 2.8 版本 (opens new window)中,我们对 Mpx 的运行时框架也进行了大量重构改造工作,完整支持了 Vue3 提出的组合式 API 开发范式,让用户能够使用当下最热门的开发方式进行小程序开发,我们先来简单感受一下组合式 API 的使用:

<template>
-  <view>{{ collectionName }}: {{ book.title }}({{ readersNumber }})</view>
-  <button bindtap="addReaders">addReaders</button>
-</template>
-
-<script>
-  import { createComponent, ref, reactive, onMounted } from '@mpxjs/core'
-
-  createComponent({
-    properties: {
-      collectionName: String
-    },
-    setup () {
-      const readersNumber = ref(0)
-      const book = reactive({ title: 'Mpx' })
-
-      onMounted(() => {
-        console.log('Component mounted.')
-      })
-
-      // 暴露给 template
-      return {
-        readersNumber,
-        book,
-        addReaders () {
-          readersNumber.value++
-        }
-      }
-    }
-  })
-</script>
-

可以看出和 Vue3 组合式 API 的使用是高度类似的,利用框架导出的一系列响应式 API 和 生命周期钩子函数在 setup 中编写业务逻辑,并将模板依赖的数据与方法作为返回值返回,与传统的选项式 API 相比,组合式 API 具备以下优势:

  • 更好的逻辑复用,通过函数包装复用逻辑,显式引入调用,方便简洁且符合直觉,规避消除了 mixins 复用中存在的缺陷;
  • 更灵活的代码组织,相比于选项式 API 提前规定了代码的组织方式,组合式 API 在这方面几乎没有做任何限制与规定,更加灵活自由,在功能复杂的庞大组件中,我们能够通过组合式 API 让我们的功能代码更加内聚且有条理,不过这也会对开发者自身的代码规范意识提出更高要求;
  • 更好的类型推导,虽然基于 this 的选项式 API 通过 ThisType 类型体操的方式也能在一定程度上实现 TS 类型推导,但推导和实现成本较高,同时仍然无法完美覆盖一些复杂场景(如嵌套 mixins 等);而组合式 API 以本地变量和函数为基础,本身就是类型友好的,我们在类型方面几乎不需要做什么额外的工作就能享受到完美的类型推导。

同时与 React Hooks 相比,组合式 API 中的 setup 函数只在初始化时单次执行,在数据响应能力的加持下大大降低了理解与使用成本,基于以上原因,我们决定为 Mpx 添加组合式 API 能力,让用户能够用组合式 API 方式进行小程序开发。

# 组合式 API 实现

从上面的简单示例中可以看出,抛开响应式 API 和生命周期注册模式的变化,组合式 API 的实现要点在于动态添加模板依赖的数据和方法,这也是我们在小程序中实现组合式 API 可能遇到的核心技术卡点。

对于动态添加模板依赖数据,我们在过去的实践中已经充分证明了其可行性,事实上,从 Mpx 最初的版本开始,我们就充分利用了这项能力来实现我们对计算属性和 dataFn (类似于 Vue 使用函数定义 data)的支持,这项能力的关键在于存在合适的生命周期用于动态添加初始化数据,这里对于初始化数据的定义是能够影响组件树的初始渲染,举个简单的例子:存在一对父子组件 parent/child,parent 使用 props 向 child 传递数据,当我们在 parent 初始创建时使用 setData 动态添加 props 数据,同时 child 在初始创建时能够通过 props 正确获取到这部分的数据时,我们就可以将这部分动态添加的数据视作初始化数据,这是我们在小程序中实现完备数据响应支持的基础。

幸运的是,目前业内所有主流小程序平台(微信/支付宝/百度/字节/QQ)都支持了上述能力,微信从一开始就支持在 attached 生命周期中调用 setData 函数动态添加初始化数据,在上述的父子 props 传递场景中,也能够在子组件的 attached 中正确获取这部分数据,支付宝和字节小程序一开始并不支持该能力,不过支付宝在 component2 组件系统重构后,字节在橙心合作项目中与我们沟通后,都成功支持了该能力。

而对于动态返回的方法,最简单能想到的方案就是直接挂载到组件实例上,经过我们的完整测试,上述业内主流小程序平台都支持使用这种方式动态添加方法,基于以上事实,我们非常确定组合式 API 能够在小程序环境中顺利实现,下图简要展示了 Mpx 支持组合式 API 的初始化流程:

composition-api-init

# 生命周期钩子函数

在组合式 API 中,setup 函数只有在组件创建时初始化单次执行,因此需要提供一系列生命周期钩子函数来代替选项式 API 中的生命周期钩子选项,由于小程序原生只支持选项式的生命周期注册方式,我们通过预注册 -> 驱动的方式来实现 setup 中函数式注册生命周期钩子的语法糖,简单来讲就是使用选项式 mixins 的方式提前注册所有需要的生命周期钩子,在选项式生命周期钩子执行时驱动对应在 setup 中使用生命周期钩子函数注册的代码逻辑执行,如下图所示:

composition-api-hook

作为跨端小程序框架,Mpx 需要兼容不同小程序平台不同的生命周期,在选项式 API 中,我们在框架中内置了一套统一的生命周期,将不同小程序平台的生命周期转换映射为内置生命周期后再进行统一的驱动,以抹平不同小程序平台生命周期钩子的差异,如微信小程序的 attached 钩子和支付宝小程序的 onInit 钩子,在组合式 API 中,我们沿用了同样的逻辑,设计了一套与框架内置生命周期对应的生命周期钩子函数,以相同的方式进行驱动,因此这些生命周期钩子函数天然具备了跨平台特性,下表显示了在组件 / 页面中框架生命周期与原生平台生命周期的对应关系:

框架内置生命周期 Hooks in setup 微信原生 支付宝原生
BEFORECREATE null attached(数据响应初始化前) onInit(数据响应初始化前)
CREATED null attached(数据响应初始化后) onInit(数据响应初始化后)
BEFOREMOUNT onBeforeMount ready(MOUNTED 执行前) didMount(MOUNTED 执行前)
MOUNTED onMounted ready(BEFOREMOUNT 执行后) didMount(BEFOREMOUNT 执行后)
BEFOREUPDATE onBeforeUpdate nullsetData 执行前) nullsetData 执行前)
UPDATED onUpdated nullsetData callback) nullsetData callback)
BEFOREUNMOUNT onBeforeUnmount detached(数据响应销毁前) didUnmount(数据响应销毁前)
UNMOUNTED onUnmounted detached(数据响应销毁后) didUnmount(数据响应销毁后)
ONLOAD onLoad onLoad onLoad
ONSHOW onShow onShow onShow
ONHIDE onHide onHide onHide
ONRESIZE onResize onResize events.onResize

同 Vue3 一样,Mpx 在组合式 API 中没有提供 BEFORECREATECREATED 对应的生命周期钩子函数,用户可以直接在 setup 中编写相关逻辑。

# 具有副作用的页面事件

在小程序中,一些页面事件的注册存在副作用,即该页面事件注册与否会产生实质性的影响,比如微信中的 onShareAppMessageonPageScroll,前者在不注册时会禁用当前页面的分享功能,而后者在注册时会带来视图与逻辑层之间的线程通信开销,对于这部分页面事件,我们无法通过预注册 -> 驱动方式提供组合式 API 的注册方式,用户可以通过选项式 API 的方式来注册使用,通过 this 访问组合式 API setup 函数的返回。

然而这种使用方式显然不够优雅,我们考虑是否可以通过一些非常规的方式提供这类副作用页面事件的组合式 API 注册支持,例如,借助编译手段。我们在运行时提供了副作用页面事件的注册函数,并在编译时通过 babel 插件的方式解析识别到当前页面中存在这些特殊注册函数的调用时,通过框架已有的编译 -> 运行时注入的方式将事件驱动逻辑添加到当前页面当中,以提供相对优雅的副作用页面事件在组合式 API 中的注册方式,同时不产生非预期的副作用影响,简单示例如下:

import { createPage, ref, onShareAppMessage } from '@mpxjs/core'
-
-createPage({
-  setup () {
-    const count = ref(0)
-
-    onShareAppMessage(() => {
-      return {
-        title: '页面分享'
-      }
-    })
-
-    return {
-      count
-    }
-  }
-})
-

目前我们通过这种方式支持的页面事件如下:

页面事件 Hooks in setup 平台支持
onPullDownRefresh onPullDownRefresh 全小程序平台 + web
onReachBottom onReachBottom 全小程序平台 + web
onPageScroll onPageScroll 全小程序平台 + web
onShareAppMessage onShareAppMessage 全小程序平台
onTabItemTap onTabItemTap 微信/支付宝/百度/QQ
onAddToFavorites onAddToFavorites 微信 / QQ
onShareTimeline onShareTimeline 微信
onSaveExitState onSaveExitState 微信

特别注意,由于静态编译分析实现方式的限制,这类页面事件的组合式 API 使用需要满足页面事件注册函数(如onShareAppMessage)的调用和 createPage 的调用位于同一个 js 文件当中。

关于生命周期钩子函数的更多信息可以查看这里 (opens new window)

# <script setup>

同 Vue3 一样,我们在 .mpx 单文件组件 / 页面中实现了 <script setup> 的组合式 API 编译语法糖,相较于常规的写法,<script setup> 具备以下优势:

  • 更少的样板内容,更简洁的代码
  • 能够使用纯 TypeScript 声明 props 类型
  • 更好的 IDE 类型推导性能

简单使用示例如下:

<script setup>
-  import { ref } from '@mpxjs/core'
-
-  const msg = ref('hello')
-
-  function log () {
-    console.log(msg.value)
-  }
-</script>
-<template>
-  <view>msg: {{msg}}</view>
-  <view ontap="log">click</view>
-</template>
-

可以看到使用方式与 Vue3 基本一致,不过由于 Mpx 的组合式 API 设计实现与 Vue3 存在差异,对应 <script setup> 也与 Vue3 中存在一些差异:

  • 不支持 import 快捷注册组件
  • 没有 defineEmits() 编译宏
  • 没有 useSlots()useAttrs() 运行时函数
  • 以编译宏的形式提供了 useContext(),获取 setup 函数的第二个参数 context
  • defineExpose() 编译宏的作用与 Vue3 中有所差别,能够限定模板中能访问的变量范围

特别注意,受小程序底层技术限制,我们在 Mpx 的实现中无法像 Vue3 那样将模板编译的渲染函数和 <script setup> 放置到同一作用域下进行变量访问,而是通过静态编译分析提取出 <script setup> 的顶层作用域变量,再以上文中提到的动态添加数据与方法的方式将其设置到模板当中,如果 <script setup> 中声明了较多顶层作用域变量,它们并不一定都会被模板访问,就会带来无效的性能开销,因此我们强烈建议使用 defineExpose() 限定模板中能访问的变量范围,你可以把它等同于 setup 函数中的 return

关于 <script setup> 的更多信息可以查看这里 (opens new window)

# 组合式 API 与 Vue3 中的差异

我们来总结一下 Mpx 中组合式 API 与 Vue3 中的区别:

关于组合式 API 的更多信息可以查看这里 (opens new window)

# 响应式 API 实现

组合式 API 的正常工作离不开响应式 API 的支持,下面我们来聊聊 Mpx 中响应式 API 的设计实现。我们知道 Vue3 中响应式 API 基于 proxy 进行了重构实现,但是目前 proxy 的浏览器兼容占比仍然无法达到我们对于线上可用性的要求,因此在 Mpx 中,我们仍然基于 Object.defineProperty 进行核心数据响应能力的实现,同时借鉴了 Vue3 中优秀的代码设计与实现,如 reactiveEffecteffectScope 等,尽可能实现与 Vue3 中响应式 API 能力对齐。

说到这里,很多同学可能会想到 @vue/composition-api 这个库,该库提供的关键能力正是基于 Vue2 的数据响应系统模拟实现 Vue3 中的响应式 API,我们在前期也对 @vue/composition-api 在 Mpx 中的复用进行了非常有价值的探索尝试。不过最终我们还是决定在 Mpx 的运行时框架中进行独立实现,原因主要在于:@vue/composition-api 是作为一个 Vue2 插件存在,无法直接侵入 Vue2 源码,导致部分能力无法实现或会带来额外的性能开销,例如 flush: 'post'ref 自动解包等。我们也看到在最新的 Vue2.7 版本中,也是在运行时框架里重新实现了这部分内容,以规避上述问题。

下图展示了 Mpx 中响应式 API 核心模块依赖关系:

composition-api-reactive

# 数据响应限制带来的差异

由于 Object.defineProperty 的能力限制,Mpx 存在和 Vue2 一致的数据响应限制,无法感知到对象 property 的添加和删除以及数组的索引赋值,与 Vue2 一致,我们暴露了 setdel API 来让用户显式地进行相关操作。除此之外,由于使用方式发生了变化,我们在使用 reactive API 创建响应式数据时,还会遇到新的限制,我们来看一下代码示例:

import { reactive, watchSyncEffect, set } from '@mpxjs/core'
-
-const state = reactive([0, 1, 2, 3])
-
-watchSyncEffect(() => {
-  console.log(JSON.stringify(state)) // [0,1,2,3]
-})
-
-set(state, 1, 3) // 不会触发 watchEffect
-
-state.push(4) // 不会触发 watchEffect
-

可以看出,即使我们使用了 set API 和数组原型方法对数组进行修改,我们仍然无法监听到数据变化。

相同的限制在使用 Object.defineProperty 的 Vue2.7 中也同样存在。

为什么会存在这个限制呢?原因在于:基于 Object.defineProperty 实现的数据响应系统中,我们会对对象的每个已有属性创建了一个 Dep 对象,在对该属性进行 get 访问时通过这个对象将其与依赖它的观察者 ReactiveEffect 关联起来,并在 set 操作时触发关联 ReactiveEffect 的更新,这是我们大家都知道的数据响应的基本原理。但是对于新增/删除对象属性和修改数组的场景,我们无法事先定义当前不存在属性的 get/set (当然这在 proxy 当中是可行的),因此我们会把对象或者数组本身作为一个数据依赖创建 Dep 对象,通过父级访问该数据时定义的 get/set 将其关联到对应的 ReactiveEffect,并在对数据进行新增/删除属性或数组操作时通过数据本身持有的 Dep 对象触发关联 ReactiveEffect 的更新,如下图所示:

数据响应原理

需要注意的是,通过父级访问是建立 DepReactiveEffect 关联关系的先决条件,在选项式 API 中,我们访问组件的响应式数据都需要通过 this 进行访问,相当于这些数据都存在 this 这个必要的父级,因此我们在使用 $set/$delete 进行对对象进行新增/删除属性或对数组进行修改时都能得到符合预期的结果,唯一的限制在于不能新增/删除根级数据属性,原因就在于 this 不存在访问它的父级。

但是在组合式 API 中,我们不需要通过 this 访问响应式数据,因此通过 reactive() 创建的响应式数据本身就是根级数据,我们自然无法通过上述方式感知到根级数据自身的变化(在 Vue3 中,基于 proxy 提供的强大能力响应式系统能够精确地感知到数据属性,甚至是当前不存在属性的访问与修改,不需要为数据自身建立 Dep 对象,自然也不存在相关问题)。

在这种情况下,我们就需要用 ref() 创建响应式数据,因为 ref 创建了一个包装对象,我们永远需要通过 .value 来访问其持有的数据(不管是显式访问还是隐式自动解包),这样就能保证 ref 数据自身的变化能够被响应式系统感知,因此也不会遇到上面描述的问题,如下所示:

import { ref, watchSyncEffect, set } from '@mpxjs/core'
-
-const state = ref([0, 1, 2, 3])
-
-watchSyncEffect(() => {
-  console.log(JSON.stringify(state.value)) // [0,1,2,3]
-})
-
-set(state.value, 1, 3) // [0,3,2,3]
-
-state.value.push(4) // [0,3,2,3,4]
-

# 响应式 API 与 Vue3 中的区别

我们来总结一下 Mpx 中响应式 API 与 Vue3 中的区别:

  • 不支持 raw 相关 API(markRaw 除外,我们提供了该 API 用于跳过部分数据的响应式转换)
  • 不支持 readonly 相关 API
  • 不支持 watchEffectwatchcomputed 的调试选项
  • 不支持对 mapset 等集合类型进行响应式转换
  • 受到 Object.defineProperty 实现带来的数据响应限制影响

关于响应式 API 的更多信息可以查看这里 (opens new window)

# 生态周边适配

除了 Mpx 运行时核心提供了组合式 API 支持外,我们对 Mpx 的周边生态能力也都进行了组合式 API 适配支持,包括 storei18nfetch 等。

# Pinia store 支持

Pinia 是基于组合式 API 设计的全新数据管理方案,目前已经取代 Vuex 成为 Vue3 官方推荐的 store,我们在研究了 pinia 的设计实现后,对其简练优雅的设计思想及其与组合式 API 的高度适配非常满意(特别是在使用 setup 函数创建 store 时,使用心智与编写组件完全一致,可以将其视作是没有视图的组件)。因此我们 fork 了 pinia 的源码仓库,基于 Mpx 提供的数据响应能力对其进行了适配改造,使其在 Mpx 环境下也能正常运行,目前相关代码维护在 @mpxjs/pinia 中,在组合式 API 中的简单使用示例如下:

import { createComponent, ref, computed, toRefs } from '@mpxjs/core'
-import { defineStore } from '@mpxjs/pinia'
-
-// 使用组合式 API 创建 pinia store 的使用心智与 setup 函数完全一致,强烈推荐
-const useSetupStore = defineStore('setup', () => {
-  const count = ref(0)
-  const doubleCount = computed(() => count.value * 2)
-
-  function increment () {
-    count.value++
-  }
-
-  return { count, doubleCount, increment }
-})
-
-createComponent({
-  setup () {
-    const store = useSetupStore()
-    // store 同 props 类似是一个 reactive 对象,解构数据需使用 toRefs 以保持数据响应性
-    const { count, doubleCount } = toRefs(store)
-    // 方法可以直接解构
-    const { increment } = store
-  
-    return { count, doubleCount, increment }
-    //
-  }
-})
-

Mpx 中通过 createStore 创建的类 Vuex store 在组合式 API 中仍然可以使用,我们可以在 setup 函数中引用 store 实例进行数据读取与方法调用 (opens new window),不过整体使用体验与 pinia store 存在较大差距,我们还是推荐在组合式 API 开发中优先使用 pinia store 作为数据管理方案。

# I18n 支持

传统选项式 API 中,我们使用 this.$t 方法在组件内调用翻译函数,但在组合式 API 中我们无法访问 this,为此我们参考了 Vue I18n 最新的 9.x 版本,该版本针对 Vue3 及组合式 API 进行了重构适配,提供了全新的 useI18n API,简单使用示例如下:

<template>
-  <view>{{t('message.hello')}}</view>
-  <button bindtap="changeLocale">change locale</button>
-</template>
-
-<script>
-  import { createComponent, useI18n } from '@mpxjs/core'
-
-  createComponent({
-    setup () {
-      // useI18n 不传参数时指向全局 i18n 对象,也可以传递 locale 和 messages 配置创建局部 i18n 对象
-      const { t, locale } = useI18n()
-
-      function changeLocale () {
-        locale.value = locale.value === 'zh-CN' ? 'en-US' : 'zh-CN'
-      }
-      // 返回的翻译方法名必须为 t,不能进行重命名
-      return { t, changeLocale }
-    }
-  })
-</script>
-

上面示例代码看上去像是我们在模板上直接调用 setup() 返回的 t 翻译方法,但是熟悉小程序开发的同学都知道在小程序架构下这是不可能的,示例中的写法其实由框架通过编译 + 运行时手段实现的语法糖,我们会在模板编译时定向扫描 t/te/tm 等 i18n 方法,将其转换为计算属性注入到运行时当中,这就意味着如果我们对翻译方法进行重命名,模板编译时无法识别出 i18n 方法调用,自然也就无法正常运行。

Mpx 中 i18n 提供了两种实现模式,分别是 wxs 和 computed,可以使用编译选项中的 i18n.isComputed 进行切换,两种方式各有优劣,其中:

  • wxs 模式的优势在于逻辑层和视图层独立维护语言集,无额外运行时性能开销,且使用没有任何限制;劣势同样源于语言集同时存在于逻辑层(js)和视图层(wxs)当中,这部分的包体积占用翻倍;
  • computed 模式的优势在于语言集只存在于逻辑层中,无额外包体积占用,且可以通过动态添加语言集的方式进一步减少包体积占用;劣势则是会产生额外的运行时性能开销,且使用上存在限制,模板调用时无法直接访问 wx:for 中的 itemindex

在组合式 API 中模板上使用 useI18n() 返回的翻译函数 t/te/tm 时,为了完整实现 useI18n API的功能,会强制使用 computed 模式进行实现,这也意味着该用法会受到 computed 模式使用限制的影响。不过当你不需要使用 useI18n 接受 messages 参数创建局部语言集作用域功能时,你也完全可以在模板中继续使用原有的 $t/$tc/$te/$tm 方法,这些方法受编译选项 i18n.isComputed 的影响,同时指向全局语言集作用域。

更多关于生态周边的组合式 API 使用指南可以点击下方链接查看详情:

# 输出 web 适配

跨端输出 web 作为 Mpx 的一大核心特性,在业务中存在广泛使用,同时也是我们设计实现任何框架新特性需要优先考虑的事项。在本次组合式 API 支持中,我们从设计之初就考虑了跨端输出 web 的适配支持,保障使用 Mpx 组合式 API 开发的业务代码都能在 web 环境中正常运行。

我们输出 web 的整体技术方向在于尽可能复用 Vue 已有的生态能力,为了实现这个目标,我们需要提供尽可能与 Vue 保持一致的 API 设计,以降低抹平适配成本。在输出 web 时,核心组合式 API 基于 Vue2.7 版本中的已有能力进行适配提供,简单举个例子:对于 import { ref } from 'mpxjs/core' 这行语句,在小程序中会指向 Mpx 内部维护的 ref 实现,而在输出 web 时会指向 Vue 中维护的 ref 实现,两者的实现虽然不仅相同,但只要保障对外函数签名一致,对于开发者用户来说就无感知。

我们借助了 Mpx 强大的条件编译能力进行上述实现,对运行时导出根据输出平台进行重定向,这样还能保障跨端输出产物干净简洁,仅包含当前输出环境下必要的逻辑,如下图所示:

composition-api-web

同理,我们也采用了类似的方式实现了组合式 API 周边能力对于输出 web 的适配支持,如pinia store 使用 pinia 原始版本进行适配实现,而 i18n 能力则是使用 vue-i18n@8.x + vue-i18n-bridge 进行适配实现。

# 性能表现

性能是 Mpx 一直以来的核心关注点,我们对组合式 API 的最终实现版本进行了一系列性能评估测试,我们使用组合式 API 版本对业务中的评价组件进行了重构,评价组件属于我们业务中交互及功能相对比较复杂的组件,源码行数约 1000 行,组件数据 27 项,组件方法 18 个,我们在测试项目中对选项式 API 和组合式 API 两个版本实现的组件进行了一系列测试。

# 组件初始化耗时

由于组合式 API 改变了原有的组件初始化流程,我们对组件的初始化耗时进行了重点测试,测试口径如下:

  • 耗时计算以挂载组件为起点,以组件 ready 执行为终点
  • 测试结果为10次手工测试排除最大最小值后求均值
  • IOS 测试机型为 iPhone 13 pro max,安卓测试机型为 OPPO R9

结果显示两个版本的组件初始化耗时大抵持平,不分优劣。

IOS 安卓
选项式 API 42.5ms 366.6ms
组合式 API 42.4ms 370.1ms

# 组件 JS 体积

在构建产物体积方面,由于组合式 API 的写法对于 JS 代码压缩更加有利,同样的逻辑实现下,组合式 API 版本的组件构建压缩后 JS 体积略胜一筹。

组件 JS 体积
选项式 API 15.67KB
组合式 API 13.60KB

# 框架运行时体积

在 Mpx2.8 版本中,我们在框架运行时中新增了组合式 API 相关实现,不过通过优化运行时导出,使其对 tree shaking 更加友好,我们的框架运行时体积在实际构建产物中没有产生太大增长。

框架运行时体积
选项式 API 51.66KB
组合式 API 57.47KB

综上所述,组合式 API 版本的运行时性能与选项式 API 大抵持平,在包体积占用方面,新版框架运行时体积占用略有提升,不过由于组合式 API 开发模式对代码压缩更友好,加上组合式 API 更易进行逻辑复用的特点,我们预计在实际业务项目中,组合式 API 的包体积占用会更小。

# 破坏性改变

Mpx 组合式 API 版本完全兼容原有的选项式 API 开发方式,不过我们在运行时重构过程中依然带来了少量的破坏性改变,详情如下:

  • 框架过往提供的组件增强生命周期 pageShow/pageHide 与微信原生提供的 pageLifetimes.show/hide 完全对齐,不再提供组件初始挂载时必定执行 pageShow 的保障(因为组件可能在后台页面进行挂载),相关初始化逻辑一定不要放置在 pageShow 当中;
  • 取消了框架过去提供的基于内部生命周期实现的非标准增强生命周期,如 beforeCreate/onBeforeCreate 等,直接将内部生命周期变量导出提供给用户使用,详情查看这里 (opens new window);
  • 为了优化 tree shaking,作为框架运行时 default exportMpx 对象不再挂载 createComponent/createStore 等运行时方法,一律通过 named export 提供,Mpx 对象上仅保留 set/use 等全局 API,详情查看这里 (opens new window)
  • 使用 I18n 能力时,为了与新版 vue-i18n 保持对齐,this.$i18n 对象指向全局作用域,如需创建局部作用域需要使用组合式 API useI18n 的方式进行创建。
  • watch API 不再接受第二个参数为带有 handler 属性的对象形式(该参数形式只应存在于 watch option 中),第二个参数必须为回调函数,与 Vue (opens new window) 对齐。

更详细的迁移指南请点击查看这里 (opens new window)

# 未来规划

在完成编译持久化缓存和组合式 API 支持后,我们已经完成了去年规划中最大的两个技术升级,后续我们的技术规划如下:

  • 支持使用 Vite 进行 web 构建
  • 完善 Mpx 跨端输出 Hummer 并正式 release
  • 优化运行时 render 函数,降低包体积占用
  • 内置支持原子类使用
  • Mpx-cube-ui 正式开源

最后,再次感谢所有参与 Mpx 组合式 API 技术建设的同学们,也欢迎社区同学加入 Mpx 项目开源共建。

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html b/docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html deleted file mode 100644 index 8cbd7936d3..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/2.9-release-alter.html +++ /dev/null @@ -1,277 +0,0 @@ - - - - - - Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化 | Mpx框架 - - - - - - - - - -

# Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化

作者:董宏平 (opens new window)

Mpx (opens new window) 是滴滴开源的一款增强型跨端小程序框架,自2018年立项开源以来如今已经进入第六个年头,在这六年间,Mpx 根植于业务,与业务共同成长,针对小程序业务开发中遇到的各类痛点问题提出了解决方案,并在集团内部建设了完善的小程序跨端开发生态。目前,Mpx 已经覆盖支持了集团内部全量小程序业务开发,成为了集团小程序开发的统一技术标准,并在今年年初被评选为集团内首个开源精品项目。

随着小程序业务的发展演进,性能和包体积的重要性愈发凸显,Mpx从设计之初就非常重视性能和包体积的优化,本次的 Mpx2.9 版本更新带来的三大核心特性——原子类、SSR 和包体积优化也都与性能和包体积息息相关,下面我们逐个展开介绍。

# 原子类支持

原子类(utility-first CSS)是近几年流行起来的一种全新的样式开发方式,在前端社区内取得了良好的口碑,越来越多的主流网站也基于原子类进行开发,比较知名的有 Github (opens new window)OpenAI (opens new window)Netflix (opens new window)NASA官网 (opens new window) 等。使用原子类离不开原子类框架的支持,常用的原子类框架有 Tailwindcss (opens new window)Windicss (opens new window)Unocss (opens new window) 等。

在 Mpx2.9 版本中,我们在框架中内置了基于 unocss 的原子类支持,让小程序开发也能使用原子类。对项目进行简单配置开启原子类支持后,用户就可以在 Mpx 页面/组件模板中直接使用一些预定义的基础样式类,诸如 flex,pt-4,text-center 和 rotate-90 等,对样式进行组合定义,并且在 Mpx 支持的所有小程序平台和 web 平台中正常运行,下面是一个简单示例:

<view class="container">
-  <view class="flex">
-    <view class="py-8 px-8 inline-flex mx-auto bg-white rounded-xl shadow-md">
-      <view class="text-center">
-        <view class="text-base text-black font-semibold mb-2">
-          Erin Lindford
-        </view>
-        <view class="text-gray-500 font-medium pb-3">
-          Product Engineer
-        </view>
-        <view
-          class="mt-2 px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-solid border-purple-200">
-          Message
-        </view>
-      </view>
-    </view>
-  </view>
-</view>
-

通过这种方式,我们在不编写任何自定义样式代码的情况下得到了一张简单的个人卡片,实际渲染效果如下:

utility-css-demo

相较于传统的自定义类编写样式的方式,使用原子类能给你带来以下这些好处:

  • 不用再烦恼于为自定义类取类名,传统样式开发中,我们仅仅是为某个元素定义样式就需要绞尽脑汁发明一些抽象的类名,还得提防类名冲突,使用原子类可以完全将你从这种琐碎无趣的工作中解放;
  • 停止 css 体积的无序增长,传统样式开发中,css 体积会随着你的迭代不断增长,而在原子类中,一切样式都可以复用,你几乎不需要编写新的 css;
  • 让调整样式变得更加安全,传统 css 是全局的,当你修改某个样式时无法保障其不会破坏其他地方的样式,而你在模板中使用的原子类是本地的,你完全不用担心修改它会影响其他地方。

而相较于使用内联样式,原子类也有一些重要的优势:

  • 约束下的设计,使用内联样式时,里面的每一个数值都是魔法数字(magic number) -,而通过原子工具类,你可以选择一些符合预定义设计规范的样式,便于构筑具有视觉一致性的UI;
  • 响应式设计,你无法在内联样式中使用媒体查询,然而通过原子类框架中提供的响应式工具,你可以轻而易举地构建出响应式界面;
  • Hover、focus 和其他状态,使用内联样式无法定义特定状态下的样式,如 hover 和 focus,通过原子类框架的状态变量能力,我们可以轻松为这些状态定义样式。

看到这里相信你已经迫不及待地想要在 Mpx 中体验原子类开发了吧,使用最新版本的 @mpxjs/cli 脚手架创建项目时,在 prompt 中选择使用原子类,就可以在新创建的项目模版中直接使用 unocss 的原子类,可使用的工具类可以参考 unocss 交互示例 (opens new window),在已有项目中开启原子类支持可以参考配置指南 (opens new window)

# 小程序原子类使用注意事项

小程序和 web 环境对于 css 的支持存在底层差异,小程序内也存在大量自身独有的技术特性,Mpx 在支持原子类时针对这些环境特异性进行了抹平和适配,在框架的支持下,我们实现了大部分(超过90%)原子类功能和工具类在小程序环境下正常使用,并额外支持了原子类产物的分包输出和样式隔离下的原子类使用,详情如下:

# 特殊字符转义

基于 unocss 的原子类支持 value auto-infer(值自动推导),可以在模版中根据相关规则书写灵活的自定义值原子类,如 p-5px bg-[hsl(211.7,81.9%,69.6%)] 等,针对原子类中出现的 [ ( , 等特殊字符,在 web 中会通过转义字符 \ 进行转义,由于小程序环境下不支持 css 选择器中出现 \ 转义字符,我们内置支持了一套不带 \ 的转义规则对这些特殊字符进行转义,同时替换模版和 css 文件中的类名,内建的默认转义规则如下:

const escapeMap = {
-    '(': '_pl_',
-    ')': '_pr_',
-    '[': '_bl_',
-    ']': '_br_',
-    '{': '_cl_',
-    '}': '_cr_',
-    '#': '_h_',
-    '!': '_i_',
-    '/': '_s_',
-    '.': '_d_',
-    ':': '_c_',
-    ',': '_2c_',
-    '%': '_p_',
-    '\'': '_q_',
-    '"': '_dq_',
-    '+': '_a_',
-    $: '_si_',
-    // unknown用于兜底不在上述范围中未知的转义字符
-    unknown: '_u_'
-  }
-

与此同时,用户也可以通过传递 @mpxjs/unocss-pluginescapeMap (opens new window) 配置项来覆盖内建的转义规则。

# 原子类分包输出

在 web 中,原子类会被全部打包输出单个样式文件,一般会放置在顶层样式表中以供全局访问,但在小程序中这种全量的输出策略并不是最优的,主要原因在于小程序中可供全局访问的主包体积存在 2M 大小限制,主包体积十分紧缺珍贵,Mpx 在构建输出时遵循着分包优先的原则,尽可能充分利用分包体积从而减少对主包体积的占用,再进行原子类产物输出时,我们也遵循了相同的原则。

在Mpx中,我们在收集原子类时同时记录了每个原子类的引用分包,在收集结束后根据每个原子类的分包引用数量决定该原子类应该输出到主包还是分包当中,我们在 @mpxjs/unocss-plugin 中提供了 minCount (opens new window) 配置项来决定分包的输出规则,该配置项的默认值为2,即当一个原子类被2个或以上分包引用时,会被作为公共原子类抽取到主包中,否则输出到所属分包中,这也是全局最优的策略。当我们想要让原子类输出产物更少地占用主包体积时,我们也可以将minCount值调大,让原子类抽取到主包的条件更加苛刻,不过这样也会伴随着原子类分包冗余的增加。

unocss.config.js 配置中定义的 safelist 原子类默认会输出到主包,为了组件局部使用的 safelist 有输出到分包的机会,我们在模版中提供了注释配置 (opens new window)(comments config),灵感来源于 webpack 中的魔法注释(magic comments),用户可以在组件模版中通过注释配置声明当前组件所需的 safelist,对应的原子类也会根据上述的规则输出到主包或分包中,使用示例如下:

<template>
-    <!-- mpx_config_safelist: 'text-red-500 bg-blue-500' -->
-    <!-- 动态样式中可以使用text-red-500和bg-blue-500原子类 -->
-    <view wx:class="{{classObj}}">test</view>
-    <!-- ... -->
-</template>
-

# 样式隔离与组件分包异步

在小程序中,自定义组件的样式默认是隔离的,web 中通过全局样式访问原子类的方式不再生效,不过由于小程序提供了样式隔离配置 (opens new window),我们可以将该组件样式隔离配置调整为 apply-shared 来获取页面或 app 中定义的原子类,但是当我们在使用传统类名和原子类混合开发或者迁移原子类的过程中,我们往往希望保留原本自定义组件的样式隔离。

针对这种情况,我们在 @mpxjs/unocss-plugin 中提供了 styleIsolation (opens new window) 配置项,可选设置为 isolated|apply-shared,当设置为 isolated 时每个组件都会通过 @import 独立引用主包或者分包的原子类样式文件,因此不会受到样式隔离的影响;当设置为 apply-shared 时,只有 app 和分包页面会引用对应的原子类样式文件,自定义组件需要通过配置样式隔离为 apply-shared 使原子类生效。

在组件分包异步的情况下对应组件即使将样式隔离配置为 apply-shared 的情况下,@mpxjs/unocss-plugin 也需要将 styleIsolation 设置为 isolated 才能正常工作,原因在于组件分包异步的情况下,组件被其他分包的页面所引用渲染,由于上述原子类样式分包输出的规则,其他分包的页面中可能并不包含当前组件所需的原子类,只有在 isolated 模式下由组件自身引用所需的原子类样式才能保证正常工作,类似于 safelist,我们也提供了注释配置 (opens new window)的方式对组件的 styleIsolation 模式进行局部配置,示例如下:

<template>
-    <!-- mpx_config_styleIsolation: 'isolated' -->
-    <!-- 当前组件会直接引用对应主包或分包的原子类样式 -->
-     <view class="@dark:(text-white bg-dark-500)">
-    <!-- ... -->
-</template>
-

# 原子类使用参考文档

Mpx 中使用原子类的详细参考文档如下:

# 输出 web 支持 SSR

近些年来,SSR/SSG 由于其良好的首屏展现速度和SEO友好性逐渐成为主流的技术规范,各类SSR框架层出不穷,未来进一步提升性能表现,在 SSR 的基础上还演进出 islands architecture (opens new window)0 hydration (opens new window) 等更加精细复杂的理念和架构。

近两年随着团队对于前端性能的重视,SSR/SSG 技术也在团队业务中逐步推广落地,并在首屏性能方面取得了显著的收效。但由于过去 Mpx 对 SSR 的支持不完善,使用 Mpx 开发的跨端页面一直无法享受到 SSR 带来的性能提升,在 Mpx2.9 版本中,我们对 web 输出流程进行了大量适配改造,解决了 SSR 中常见的内存泄漏、跨请求状态污染和数据预请求等问题,完整实现了基于 Vue 和 Pinia 的 SSR 技术方案。

# 配置使用 SSR

在 Vue SSR 项目中,我们一般需要提供 server entry 和 client entry 两个文件作为 webpack 的构建入口,然后通过 webpack 构建出 server bundle 和 client bundle。在用户访问页面时,在服务端使用 server bundle 渲染出 HTML 字符串,作为静态页面发送到客户端,然后在客户端使用 client bundle 通过水合(hydration)对静态页面进行激活,实现可交互效果,下图展示了 Vue SSR 的大致流程。

Vue SSR流程

Mpx SSR 核心基于 Vue SSR 实现,大致流程思路与 Vue 一致,不过为了保持与小程序代码的兼容性,在配置使用上有一些改动差异,下面我们详细展开介绍:

# 构建server/client bundle

SSR项目中,我们需要分别构建出 server bundle 和 client bundle,对于不同环境的产物构建,我们需要进行不同的配置。 -在 Vue 中,我们需要提供 entry-server.jsentry-client.js 两个文件分别作为 server 和 client 的构建入口,与 Vue 不同,在 Mpx 中我们通过编译处理与运行时增强生命周期实现了使用 app.mpx 作为统一构建入口,无需区分 server 和 client。

# 服务端构建配置

服务端配置中除了将 entry 制定为 app.mpx 及其它基础配置外,最重要的是安装 vue-server-renderer 包中提供的 server-plugin 插件,该插件能够构建产出 vue-ssr-server-bundle.json 文件供 renderer 后续消费。

// webpack.server.config.js
-const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
-
-module.exports = merge(baseConfig, {
-  // 将 entry 指向项目的 app.mpx 文件
-  entry: './app.mpx',
-  // ...
-  plugins: [
-   // 产出 `vue-ssr-server-bundle.json`
-    new VueSSRServerPlugin()
-  ]
-})
-

更加详细的配置说明可参考 Vue SSR的服务端配置 (opens new window)

# 客户端构建配置

类似服务端构建配置,在客户端构建中我们需要使用 vue-server-renderer 包中 client-plugin 插件来帮助我们生成客户端环境的资源清单 vue-ssr-client-manifest.json,并供 renderer 后续消费。

// webpack.client.config.js
-const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
-
-module.exports = merge(baseConfig, {
-  // 将 entry 指向项目的 app.mpx 文件
-  entry: './app.mpx',
-  // ...
-  plugins: [
-    // 产出 `vue-ssr-client-manifest.json`。
-    new VueSSRClientPlugin()
-  ]
-})
-

更加详细的配置说明可参考 Vue SSR的客户端配置 (opens new window)

# 准备页面模版

SSR 渲染中,我们创建 renderer 需要一个页面模板,简单的示例如下:

<!DOCTYPE html>
-<html lang="en">
-  <head><title>Hello</title></head>
-  <body>
-    <!--vue-ssr-outlet-->
-  </body>
-</html>
-

与 CSR 渲染模版不同,SSR 渲染模版中需要提供一个特殊的 <!--vue-ssr-outlet-->注释,标记 SSR 渲染产物的插入位置,如使用 @mpxjs/cli 脚手架创建 SSR 项目,该模版已经内置于脚手架中。

# 集成启动 SSR 服务

当我们准备好页面模版和双端构建产物后,我们就可以创建 renderer 并与 Node 服务进行集成,启动 SSR 服务,下面以 express 为例:

//server.js
-const app = require('express')()
-const { createBundleRenderer } = require('vue-server-renderer')
-// 通过 vue-server-renderer/server-plugin 生成的文件
-const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
-// 通过 vue-server-renderer/client-plugin 生成的文件
-const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
- // 页面模版文件
-const template = require('fs').readFileSync('../src/index.template.html', 'utf-8')
-// 创建 renderer 渲染器
-const renderer = createBundleRenderer(serverBundle, {
-    runInNewContext: false,
-    template,
-    clientManifest,
-});
-// 注册启动 SSR 服务
-app.get('*', (req, res) => {
-  const context = { url: req.url }
-  renderer.renderToString(context, (err, html) => {
-  	if (err) {
-  	  res.status(500).end('Internal Server Error')
-      return
-    }
-  	res.end(html);
-  })
-})
-app.listen(8080)
-

# SSR 生命周期

在 Mpx 2.9 的版本中,我们提供了三个全新用于 SSR 的生命周期,分别是onAppInitserverPrefetchonSSRAppCreated,以统一服务端与客户端的构建入口,下面展开介绍:

# onAppInit

在 SSR 中用户每发出一个请求,我们都会为其生成一个新的应用实例,onAppInit 生命周期会在应用创建 new Vue(...) 前被调用,其执行的返回结果会被合并到创建应用的 options

很常见的使用场景在于返回新的全局状态管理实例,Mpx 中提供了 @mpxjs/pinia 作为全局状态管理工具,我们可以在 onAppInit 中返回全新的 pinia 示例避免产生跨请求状态污染,示例如下:

// app.mpx
-import mpx, { createApp } from '@mpxjs/core'
-import { createPinia } from '@mpxjs/pinia'
-
-createApp({
-  // ...
-  onAppInit () {
-    const pinia = createPinia()
-    return {
-      pinia
-    }
-  }
-})
-

# serverPrefetch

当我们需要在 SSR 使用数据预拉取时,可以使用这个生命周期进行,使用方法与 Vue 一致, 示例如下:

选项式 API:

import { createPage } from '@mpxjs/core'
-import useStore from '../store/index'
-
-createPage({
-  //...
-  serverPrefetch () {
-    const query = this.$route.query
-    const store = useStore(this.$pinia)
-    // return the promise from the action, fetch data and update state
-    return store.fetchData(query)
-  }
-})
-

组合式 API:

import { onServerPrefetch, getCurrentInstance, createPage } from '@mpxjs/core'
-import useStore from '../store/index'
-
-createPage({
-  setup () {
-    const store = userStore()
-    onServerPrefetch(() => {
-      const query = getCurrentInstance().proxy.$route.query
-      // return the promise from the action, fetch data and update state
-      return store.fetchData(query)
-    })
-  }
-})
-

关于数据预拉取更详细的说明可以查看这里 (opens new window)

# onSSRAppCreated

在 Vue SSR 项目中,我们会在 entry-server.js 中导出一个工厂函数,在该函数中实现创建应用实例、路由匹配和状态同步等逻辑,并返回应用实例 app

在 Mpx SSR 中,我们将这部分逻辑整合在 onSSRAppCreated 中执行,该生命周期执行时用户可以从参数中获取应用实例 app、路由实例 router、数据管理实例 pinia 和 SSR 上下文 context,在完成必要的操作后,该生命周期需要返回一个 resolve(app) 的 promise。

通常我们会在 onSSRAppCreated 中进行路由路径设置和数据预拉取后的状态同步工作,示例如下:

// app.mpx
-createApp({
-    // ...,
-    onSSRAppCreated ({ pinia, router, app, context }) {
-      return new Promise((resolve, reject) => {
-        // 设置服务端路由路径
-        router.push(context.url)
-        router.onReady(() => {
-          // 应用完成渲染时执行
-          context.rendered = () => {
-            // 将服务端渲染后得到的 pinia.state 同步到 context.state 中,
-            // context.state 会被自动序列化并通过 `window.__INITIAL_STATE__`
-            // 注入到 HTML 中,并在客户端运行时再读取同步
-            context.state = pinia.state.value
-          }
-          // 返回应用程序实例
-          resolve(app)
-        }, reject)
-      })
-    }
-})
-

如用户没有配置 onSSRAppCreated,框架内部会执行兜底逻辑,以保障 SSR 的正常运行。

# 其他注意事项

  1. Mpx SSR 渲染支持 i18n 的功能,但为了防止内存泄漏,当前 i18n 实例不会随着每次请求而重新创建,这是由于 Vue2.x 版本插件机制的设计缺陷所造成的,因此在使用 i18n 进行 SSR 时可能会产生跨请求状态污染的问题,这个问题会在未来 Mpx 输出 web 切换为 Vue3 后完全解决。

  2. 在服务端渲染阶段,对于 global 全局对象访问修改,如__mpx, __mpxRouter, __mpxPinia 都可能导致全局状态污染,所以在服务端渲染阶段请尽量避免进行相关操作;对于存在全局访问修改的方法,如 getApp(), getCurrentPages() 等在服务端渲染中被调用时,会产生相关报错提示。

  3. 由于服务器无法收到 URL 中的 hash 信息,使用 SSR 时需要通过修改 mpx.config.webRouteConfig 将路由模式设置成 history 模式。

# 包体积优化

近几年来随着集团超级 App 战略的推进,滴滴出行主小程序中集成了越来越多的业务线,主小程序的包体积也随之呈现爆炸式增长,到今天主小程序里已经集成了集团内大部分核心业务,但其总包体积也从 21 年的 12MB 增长至触达微信上限 30MB,成为一个页面数量400+,组件数量3600+,JS模块数40000+的“大程序”。在此期间,我们通过统一技术框架、分包异步改造和低效页面下线/改造等技术手段,对主小包体积进行一轮又一轮优化,并沉淀了分包异步构建、包体积分析管控和冗余包检测等一系列技术能力,累计优化总包体积超过 8MB

Mpx 在设计之初就考虑了包体积问题,完整继承了 webpack 已有的代码优化能力,如公共模块抽离、tree shaking、混淆压缩等。除此之外,我们还设计开发了业内最完善的分包构建流程,完整支持基于配置声明和依赖分析的普通分包、独立分包及分包异步化的构建输出,极大地缓解了复杂小程序中的主包和总包体积过大的问题。在 Mpx2.9 版本中,我们针对复杂小程序页面组件和 JS 模块众多的特点,对构建产物进行了针对性地优化,进一步降低了构建产物体积,当前版本在主小程序中实测能够在不做任何业务改造的情况下节省了约 2MB 总包体积,收效显著,主要优化项如下:

# 模版代码优化

Mpx 基于 webpack 进行打包构建,并基于小程序原生 commonjs 支持进行适配改造,以实现 webpack 构建产物在小程序环境下运行,为了将构建产物中的模块和 chunk 链接到一起,webpack 和 mpx 会在构建产物生成一些模版代码进行相关工作,下面是一个页面 js 中包含的模版代码示例:

var self = {}
-self.webpackChunkmpx_test_2_8 = require('../bundle.js')
-(self.webpackChunkmpx_test_2_8 = self.webpackChunkmpx_test_2_8 || []).push([[405], {
-  1307: function (t, e, o) {
-    // ...
-  },
-  4236: function (t) {
-    // ...
-  },
-  // ...
-}, function (t) {
-  var e
-  e = 1307, t(t.s = e)
-}])
-

可以看出模版代码主要由 chunk 链接代码,chunk id,模块 id 和模块包装函数组成,对于中小项目来说,这些模版代码一般不会占用太多体积,但是在滴滴出行主小程序这样的大体量项目下,这部分体积就变得不可忽视,@mpxjs/size-report 的分析结果显示,生产模式下主小程序中模版代码占用体积约为 1.2MB,相当于一个中等复杂度业务的占用体积。

我们对模版代码的生成逻辑和生成产物进行分析,发现 webpack 生产模式的默认配置中,很多配置项并不是体积最优的选项,一个典型的例子在于模块/chunk id:为了保障生成产物的内容稳定,来尽可能提升浏览器的缓存利用率,webpack 默认的模块/chunk id 采用 deterministic 模式进行生成,该模式下模块 id 为模块源路径的定长数字 hash,比项目模块总数长 1 位。由于在小程序中代码包按照版本的维度进行全量管理,保证文件局部的内容稳定在小程序环境下无正向意义,这就有了优化空间,我们可以简单将模块/chunk id 的生成逻辑改为数字自增,在主小程序中就能节省出上百 KB 总包体积。

类似的可优化点还存在于 chunk 链接代码和模块包装函数当中,我们在 @mpxjs/webpack-plugin@2.9 版本中提供了一个新的配置项 optimizeSize (opens new window),其中整合了一系列模版代码体积优化配置,开启后就能自动优化构建产物中的模版代码体积,在主小程序中,我们开启 optimizeSize 后可以减少总包体积约 540KB,效果非常显著,下面是上述示例在开启 optimizeSize 后的产物对比:

var g = {}
-g.c = require('../bundle.js'), (g.c = g.c || []).push([[8], {
-  448: function (t, e, o) {
-    // ...
-  },
-  463: function (t) {
-    // ...
-  }
-  // ...
-}, function (t) {
-  var e
-  e = 448, t(t.s = e)
-}])
-

# 空模块移除

Mpx 在处理 .mpx 单文件时会将其分拆为 template/js/style/json 四个新模块来进行分别处理,其中 template/style/json 部分的内容会在处理后通过内置的 extractor 抽取输出为静态文件,抽取之后原本分拆出来的模块就会以空模块的形式残留在构建产物中(template 模块在抽取后还需要保留 render 函数和 refs 等信息所以不会成为空模块),如下所示:

/***/ 533:
-/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
-// ...
-/* template */
-__webpack_require__(535)
-/* styles */
-__webpack_require__(536)
-/* json */
-__webpack_require__(537)
-/* script */
-__webpack_require__(534)
-/***/ }),
-/***/ 536:
-/***/ (function() {
-/***/ }),
-/***/ 537:
-/***/ (function() {
-/***/ })
-

从上面示例中可以看出 536/537 模块的定义和引用都是完全不必要的,但由于 webpack 本身对于空模块没有进行识别和优化的手段,在过去的版本中这些空模块代码会占用我们一部分总包体积。在 Mpx2.9 版本中,我们定义了全新的依赖类型 CommonjsExtractDependency 对于这类可能被抽取成为空模块的 request 进行识别处理,当模块内容在完成抽取后为空时,自动将其从模块图中移除,并在产物生成时不再生成其引用代码。该项优化措施内置开启,升级后自动生效,可减少主小程序总包体积约 232KB

# Render函数优化

Render 函数是 Mpx 运行时 setData 优化的一项核心设计,我们在模版编译时将用户模版转换为简易的 render 函数,该函数执行时能够完全模拟视图渲染的数据访问过程,正确地收集当前视图的数据依赖,避免将视图不需要的数据通过 setData 发送到视图中。

render函数

虽然我们生成的简化版 render 函数仅保留了数据访问逻辑,在代码体积上并不算大,但是仍然存在着一定的优化空间,我们来看下面这个例子:

function render(){
-  this.a
-  if(this.c){
-    this.a
-    this.b
-    this.c.a
-    this.c.b
-    this.d
-  }
-  this.b
-}
-

可以看出 if block 下存在着大量冗余不必要的数据访问,例如 this.athis.b 在父级 block 中已经进行过访问,this.c.athis.c.b 由于在 if condition 中进行过 this.c 的访问也不再必要(render函数执行时会对模版依赖数据进行深度diff,父路径访问后子路径就无需再进行访问),上述 render 函数可以优化为:

function render(){
-  this.a
-  if(this.c){
-    this.d
-  }
-  this.b
-}
-

在 Mpx2.9 版本中,我们通过两轮 ast 遍历(2 pass ast travese)的方式实现了这个优化,在第一轮遍历中,我们收集了数据访问信息并按照 block 结构存储在 blockVisitTree 中,第二轮遍历时依据 blockVisitTree 中的信息对不必要的数据访问进行剪枝优化。

除此之外,我们也大幅优化了 render 函数中的数据收集代码,有效地降低了 render 函数的体积占用。

该优化目前没有默认开启,可以通过 @mpxjs/webpack-plugin 中的 optimizeRenderRules (opens new window) 配置项配置生效范围进行开启, -全量开启后在主小程序中实测可节省总包体积约 1507KB

# 未来规划

在未来,我们对 Mpx 构建产物还有进一步的优化方案可以探索实施,主要分为两个方向:

  • 编译注入代码优化,在功能不变的情况下简化编译注入的代码,对于 render 函数也有一种运行时有损的方案(略微增大运行时开销)可以尝试
  • 模版和JSON压缩,对模版和JSON中的组件名及组件路径进行短哈希压缩,缺点是丢失dist产物的可读性,可能需要对其提供 sourceMap 支持

除此之外,我们近期的框架升级计划还包括:

  • 局部运行时渲染增强
  • 数据响应升级为 proxy 实现,输出 web 升级为 vue3
  • 支付宝 2.0 基础库适配优化
  • 微信 skyline 适配优化
  • 输出 Hummer 能力完善

在构建提速方面,我们也会在以下方向进行探索:

  • 输出 web 支持 vite 构建
  • 基于模块联邦的分布式构建
  • 基于 Rust 的高性能构建探索

最后,特别感谢@徐伟东、@闫宇和@朱志恒对于 2.9 版本功能开发的突出贡献,也欢迎大家使用 Mpx 进行小程序跨端开发并加入社区共建。

Github:https://github.com/didi/mpx

官网/文档:https://mpxjs.cn/

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/2.9-release.html b/docs-vuepress/.vuepress/dist/articles/2.9-release.html deleted file mode 100644 index f17db1933f..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/2.9-release.html +++ /dev/null @@ -1,285 +0,0 @@ - - - - - - Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化 | Mpx框架 - - - - - - - - - -

# Mpx2.9 版本正式发布,支持原子类、SSR 和包体积优化

作者:hiyuki (opens new window)

Mpx (opens new window) 是滴滴开源的一款增强型跨端小程序框架,自2018年立项开源以来如今已经进入第六个年头,在这六年间,Mpx 根植于业务,与业务共同成长,针对小程序业务开发中遇到的各类痛点问题提出了解决方案,并在集团内部建设了完善的小程序跨端开发生态。目前,Mpx 已经覆盖支持了集团内部全量小程序业务开发,成为了集团小程序开发的统一技术标准。

随着小程序业务的发展演进,性能和包体积的重要性愈发凸显,Mpx从设计之初就非常重视性能和包体积的优化,本次的 Mpx2.9 版本更新带来的三大核心特性——原子类、SSR 和包体积优化也都与性能和包体积息息相关,下面我们逐个展开介绍。

# 原子类支持

原子类(utility-first CSS)是近几年流行起来的一种全新的样式开发方式,在前端社区内取得了良好的口碑,越来越多的主流网站也基于原子类进行开发,比较知名的有 Github (opens new window)OpenAI (opens new window)Netflix (opens new window)NASA官网 (opens new window) 等。使用原子类离不开原子类框架的支持,常用的原子类框架有 Tailwindcss (opens new window)Windicss (opens new window)Unocss (opens new window) 等。

在 Mpx2.9 版本中,我们在框架中内置了原子类支持,让小程序开发也能使用原子类。对项目进行简单配置开启原子类支持后,用户就可以在 Mpx 页面/组件模板中直接使用一些预定义的基础样式类,诸如 flex,pt-4,text-center 和 rotate-90 等,对样式进行组合定义,并且在 Mpx 支持的所有小程序平台和 web 平台中正常运行,下面是一个简单示例:


-<view class="container">
-  <view class="flex">
-    <view class="py-8 px-8 inline-flex mx-auto bg-white rounded-xl shadow-md">
-      <view class="text-center">
-        <view class="text-base text-black font-semibold mb-2">
-          Erin Lindford
-        </view>
-        <view class="text-gray-500 font-medium pb-3">
-          Product Engineer
-        </view>
-        <view class="mt-2 px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-solid border-purple-200">
-          Message
-        </view>
-      </view>
-    </view>
-  </view>
-</view>
-

通过这种方式,我们在不编写任何自定义样式代码的情况下得到了一张简单的个人卡片,实际渲染效果如下:

utility-css-demo

相较于传统的自定义类编写样式的方式,使用原子类能给你带来以下这些好处:

  • 不用再烦恼于为自定义类取类名,传统样式开发中,我们仅仅是为某个元素定义样式就需要绞尽脑汁发明一些抽象的类名,还得提防类名冲突,使用原子类可以完全将你从这种琐碎无趣的工作中解放;
  • 停止 css 体积的无序增长,传统样式开发中,css 体积会随着你的迭代不断增长,而在原子类中,一切样式都可以复用,你几乎不需要编写新的 css;
  • 让调整样式变得更加安全,传统 css 是全局的,当你修改某个样式时无法保障其不会破坏其他地方的样式,而你在模板中使用的原子类是本地的,你完全不用担心修改它会影响其他地方。

而相较于使用内联样式,原子类也有一些重要的优势:

  • 约束下的设计,使用内联样式时,里面的每一个数值都是魔法数字(magic number) -,而通过原子工具类,你可以选择一些符合预定义设计规范的样式,便于构筑具有视觉一致性的UI;
  • 响应式设计,你无法在内联样式中使用媒体查询,然而通过原子类框架中提供的响应式工具,你可以轻而易举地构建出响应式界面;
  • Hover、focus 和其他状态,使用内联样式无法定义特定状态下的样式,如 hover 和 focus,通过原子类框架的状态变量能力,我们可以轻松为这些状态定义样式。

看到这里相信你已经迫不及待地想要在 Mpx 中体验原子类开发了吧,使用最新版本的 @mpxjs/cli 脚手架创建项目时,在 prompt 中选择使用原子类,就可以在新创建的项目模版中直接使用原子类,可使用的工具类可以参考 交互示例 (opens new window),在已有项目中开启原子类支持可以参考配置指南 (opens new window)

# 小程序原子类使用注意事项

小程序和 web 环境对于 css 的支持存在底层差异,小程序内也存在大量自身独有的技术特性,Mpx 在支持原子类时针对这些环境特异性进行了抹平和适配,在框架的支持下,我们实现了大部分(超过90%)原子类功能和工具类在小程序环境下正常使用,并额外支持了原子类产物的分包输出和样式隔离下的原子类使用,详情如下:

# 特殊字符转义

现代原子类框架支持 value auto-infer(值自动推导),可以在模版中根据相关规则书写灵活的自定义值原子类,如 p-5px bg-[hsl(211.7,81.9%,69.6%)] 等,针对原子类中出现的 [ ( , 等特殊字符,在 web 中会通过转义字符 \ 进行转义,由于小程序环境下不支持 css 选择器中出现 \ 转义字符,我们内置支持了一套不带 \ 的转义规则对这些特殊字符进行转义,同时替换模版和 css 文件中的类名,内建的默认转义规则如下:

const escapeMap = {
-  '(': '_pl_',
-  ')': '_pr_',
-  '[': '_bl_',
-  ']': '_br_',
-  '{': '_cl_',
-  '}': '_cr_',
-  '#': '_h_',
-  '!': '_i_',
-  '/': '_s_',
-  '.': '_d_',
-  ':': '_c_',
-  ',': '_2c_',
-  '%': '_p_',
-  '\'': '_q_',
-  '"': '_dq_',
-  '+': '_a_',
-  $: '_si_',
-  // unknown用于兜底不在上述范围中未知的转义字符
-  unknown: '_u_'
-}
-

与此同时,用户也可以通过传递 @mpxjs/unocss-pluginescapeMap (opens new window) 配置项来覆盖内建的转义规则。

# 原子类分包输出

在 web 中,原子类会被全部打包输出单个样式文件,一般会放置在顶层样式表中以供全局访问,但在小程序中这种全量的输出策略并不是最优的,主要原因在于小程序中可供全局访问的主包体积存在 2M 大小限制,主包体积十分紧缺珍贵,Mpx 在构建输出时遵循着分包优先的原则,尽可能充分利用分包体积从而减少对主包体积的占用,再进行原子类产物输出时,我们也遵循了相同的原则。

在Mpx中,我们在收集原子类时同时记录了每个原子类的引用分包,在收集结束后根据每个原子类的分包引用数量决定该原子类应该输出到主包还是分包当中,我们在 @mpxjs/unocss-plugin 中提供了 minCount (opens new window) 配置项来决定分包的输出规则,该配置项的默认值为2,即当一个原子类被2个或以上分包引用时,会被作为公共原子类抽取到主包中,否则输出到所属分包中,这也是全局最优的策略。当我们想要让原子类输出产物更少地占用主包体积时,我们也可以将minCount值调大,让原子类抽取到主包的条件更加苛刻,不过这样也会伴随着原子类分包冗余的增加。

unocss.config.js 配置中定义的 safelist 原子类默认会输出到主包,为了组件局部使用的 safelist 有输出到分包的机会,我们在模版中提供了注释配置 (opens new window)(comments config),灵感来源于 webpack 中的魔法注释(magic comments),用户可以在组件模版中通过注释配置声明当前组件所需的 safelist,对应的原子类也会根据上述的规则输出到主包或分包中,使用示例如下:

<template>
-  <!-- mpx_config_safelist: 'text-red-500 bg-blue-500' -->
-  <!-- 动态样式中可以使用text-red-500和bg-blue-500原子类 -->
-  <view wx:class="{{classObj}}">test</view>
-  <!-- ... -->
-</template>
-

# 样式隔离与组件分包异步

在小程序中,自定义组件的样式默认是隔离的,web 中通过全局样式访问原子类的方式不再生效,不过由于小程序提供了样式隔离配置 (opens new window),我们可以将该组件样式隔离配置调整为 apply-shared 来获取页面或 app 中定义的原子类,但是当我们在使用传统类名和原子类混合开发或者迁移原子类的过程中,我们往往希望保留原本自定义组件的样式隔离。

针对这种情况,我们在 @mpxjs/unocss-plugin 中提供了 styleIsolation (opens new window) 配置项,可选设置为 isolated|apply-shared,当设置为 isolated 时每个组件都会通过 @import 独立引用主包或者分包的原子类样式文件,因此不会受到样式隔离的影响;当设置为 apply-shared 时,只有 app 和分包页面会引用对应的原子类样式文件,自定义组件需要通过配置样式隔离为 apply-shared 使原子类生效。

在组件分包异步的情况下对应组件即使将样式隔离配置为 apply-shared 的情况下,@mpxjs/unocss-plugin 也需要将 styleIsolation 设置为 isolated 才能正常工作,原因在于组件分包异步的情况下,组件被其他分包的页面所引用渲染,由于上述原子类样式分包输出的规则,其他分包的页面中可能并不包含当前组件所需的原子类,只有在 isolated 模式下由组件自身引用所需的原子类样式才能保证正常工作,类似于 safelist,我们也提供了注释配置 (opens new window)的方式对组件的 styleIsolation 模式进行局部配置,示例如下:

<template>
-  <!-- mpx_config_styleIsolation: 'isolated' -->
-  <!-- 当前组件会直接引用对应主包或分包的原子类样式 -->
-  <view class="@dark:(text-white bg-dark-500)">
-    <!-- ... -->
-</template>
-

# 原子类使用参考文档

Mpx 中使用原子类的详细参考文档如下:

# 输出 web 支持 SSR

近些年来,SSR/SSG 由于其良好的首屏展现速度和SEO友好性逐渐成为主流的技术规范,各类SSR框架层出不穷,未来进一步提升性能表现,在 SSR 的基础上还演进出 islands architecture (opens new window)0 hydration (opens new window) 等更加精细复杂的理念和架构。

近两年随着团队对于前端性能的重视,SSR/SSG 技术也在团队业务中逐步推广落地,并在首屏性能方面取得了显著的收效。但由于过去 Mpx 对 SSR 的支持不完善,使用 Mpx 开发的跨端页面一直无法享受到 SSR 带来的性能提升,在 Mpx2.9 版本中,我们对 web 输出流程进行了大量适配改造,解决了 SSR 中常见的内存泄漏、跨请求状态污染和数据预请求等问题,完整实现了 SSR 技术方案。

# 配置使用 SSR

在 Vue SSR 项目中,我们一般需要提供 server entry 和 client entry 两个文件作为 webpack 的构建入口,然后通过 webpack 构建出 server bundle 和 client bundle。在用户访问页面时,在服务端使用 server bundle 渲染出 HTML 字符串,作为静态页面发送到客户端,然后在客户端使用 client bundle 通过水合(hydration)对静态页面进行激活,实现可交互效果,下图展示了 Vue SSR 的大致流程。

Vue SSR流程

Mpx SSR 实现的大致流程思路与 Vue 一致,不过为了保持与小程序代码的兼容性,在配置使用上有一些改动差异,下面我们详细展开介绍:

# 构建server/client bundle

SSR项目中,我们需要分别构建出 server bundle 和 client bundle,对于不同环境的产物构建,我们需要进行不同的配置。 -在 Vue 中,我们需要提供 entry-server.jsentry-client.js 两个文件分别作为 server 和 client 的构建入口,与 Vue 不同,在 Mpx 中我们通过编译处理与运行时增强生命周期实现了使用 app.mpx 作为统一构建入口,无需区分 server 和 client。

# 服务端构建配置

服务端配置中除了将 entry 制定为 app.mpx 及其它基础配置外,最重要的是安装 vue-server-renderer 包中提供的 server-plugin 插件,该插件能够构建产出 vue-ssr-server-bundle.json 文件供 renderer 后续消费。

// webpack.server.config.js
-const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
-
-module.exports = merge(baseConfig, {
-  // 将 entry 指向项目的 app.mpx 文件
-  entry: './app.mpx',
-  // ...
-  plugins: [
-   // 产出 `vue-ssr-server-bundle.json`
-    new VueSSRServerPlugin()
-  ]
-})
-

更加详细的配置说明可参考 Vue SSR的服务端配置 (opens new window)

# 客户端构建配置

类似服务端构建配置,在客户端构建中我们需要使用 vue-server-renderer 包中 client-plugin 插件来帮助我们生成客户端环境的资源清单 vue-ssr-client-manifest.json,并供 renderer 后续消费。

// webpack.client.config.js
-const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
-
-module.exports = merge(baseConfig, {
-  // 将 entry 指向项目的 app.mpx 文件
-  entry: './app.mpx',
-  // ...
-  plugins: [
-    // 产出 `vue-ssr-client-manifest.json`。
-    new VueSSRClientPlugin()
-  ]
-})
-

更加详细的配置说明可参考 Vue SSR的客户端配置 (opens new window)

# 准备页面模版

SSR 渲染中,我们创建 renderer 需要一个页面模板,简单的示例如下:

<!DOCTYPE html>
-<html lang="en">
-  <head><title>Hello</title></head>
-  <body>
-    <!--vue-ssr-outlet-->
-  </body>
-</html>
-

与 CSR 渲染模版不同,SSR 渲染模版中需要提供一个特殊的 <!--vue-ssr-outlet-->注释,标记 SSR 渲染产物的插入位置,如使用 @mpxjs/cli 脚手架创建 SSR 项目,该模版已经内置于脚手架中。

# 集成启动 SSR 服务

当我们准备好页面模版和双端构建产物后,我们就可以创建 renderer 并与 Node 服务进行集成,启动 SSR 服务,下面以 express 为例:

//server.js
-const app = require('express')()
-const { createBundleRenderer } = require('vue-server-renderer')
-// 通过 vue-server-renderer/server-plugin 生成的文件
-const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
-// 通过 vue-server-renderer/client-plugin 生成的文件
-const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
- // 页面模版文件
-const template = require('fs').readFileSync('../src/index.template.html', 'utf-8')
-// 创建 renderer 渲染器
-const renderer = createBundleRenderer(serverBundle, {
-    runInNewContext: false,
-    template,
-    clientManifest,
-});
-// 注册启动 SSR 服务
-app.get('*', (req, res) => {
-  const context = { url: req.url }
-  renderer.renderToString(context, (err, html) => {
-    if (err) {
-      res.status(500).end('Internal Server Error')
-      return
-    }
-    res.end(html);
-  })
-})
-app.listen(8080)
-

# SSR 生命周期

在 Mpx 2.9 的版本中,我们提供了三个全新用于 SSR 的生命周期,分别是onAppInitserverPrefetchonSSRAppCreated,以统一服务端与客户端的构建入口,下面展开介绍:

# onAppInit

在 SSR 中用户每发出一个请求,我们都会为其生成一个新的应用实例,onAppInit 生命周期会在应用创建 new Vue(...) 前被调用,其执行的返回结果会被合并到创建应用的 options

很常见的使用场景在于返回新的全局状态管理实例,Mpx 中提供了 @mpxjs/pinia 作为全局状态管理工具,我们可以在 onAppInit 中返回全新的 pinia 示例避免产生跨请求状态污染,示例如下:

// app.mpx
-import mpx, { createApp } from '@mpxjs/core'
-import { createPinia } from '@mpxjs/pinia'
-
-createApp({
-  // ...
-  onAppInit () {
-    const pinia = createPinia()
-    return {
-      pinia
-    }
-  }
-})
-

# serverPrefetch

当我们需要在 SSR 使用数据预拉取时,可以使用这个生命周期进行,使用方法与 Vue 一致, 示例如下:

选项式 API:

import { createPage } from '@mpxjs/core'
-import useStore from '../store/index'
-
-createPage({
-  //...
-  serverPrefetch () {
-    const query = this.$route.query
-    const store = useStore(this.$pinia)
-    // return the promise from the action, fetch data and update state
-    return store.fetchData(query)
-  }
-})
-

组合式 API:

import { onServerPrefetch, getCurrentInstance, createPage } from '@mpxjs/core'
-import useStore from '../store/index'
-
-createPage({
-  setup () {
-    const store = userStore()
-    onServerPrefetch(() => {
-      const query = getCurrentInstance().proxy.$route.query
-      // return the promise from the action, fetch data and update state
-      return store.fetchData(query)
-    })
-  }
-})
-

关于数据预拉取更详细的说明可以查看这里 (opens new window)

# onSSRAppCreated

在 Vue SSR 项目中,我们会在 entry-server.js 中导出一个工厂函数,在该函数中实现创建应用实例、路由匹配和状态同步等逻辑,并返回应用实例 app

在 Mpx SSR 中,我们将这部分逻辑整合在 onSSRAppCreated 中执行,该生命周期执行时用户可以从参数中获取应用实例 app、路由实例 router、数据管理实例 pinia 和 SSR 上下文 context,在完成必要的操作后,该生命周期需要返回一个 resolve(app) 的 promise。

通常我们会在 onSSRAppCreated 中进行路由路径设置和数据预拉取后的状态同步工作,示例如下:

// app.mpx
-createApp({
-    // ...,
-    onSSRAppCreated ({ pinia, router, app, context }) {
-      return new Promise((resolve, reject) => {
-        // 设置服务端路由路径
-        router.push(context.url)
-        router.onReady(() => {
-          // 应用完成渲染时执行
-          context.rendered = () => {
-            // 将服务端渲染后得到的 pinia.state 同步到 context.state 中,
-            // context.state 会被自动序列化并通过 `window.__INITIAL_STATE__`
-            // 注入到 HTML 中,并在客户端运行时再读取同步
-            context.state = pinia.state.value
-          }
-          // 返回应用程序实例
-          resolve(app)
-        }, reject)
-      })
-    }
-})
-

如用户没有配置 onSSRAppCreated,框架内部会执行兜底逻辑,以保障 SSR 的正常运行。

# 其他注意事项

  1. Mpx SSR 渲染支持 i18n 的功能,但为了防止内存泄漏,当前 i18n 实例不会随着每次请求而重新创建,这是由于 Vue2.x 版本插件机制的设计缺陷所造成的,因此在使用 i18n 进行 SSR 时可能会产生跨请求状态污染的问题,这个问题会在未来 Mpx 输出 web 切换为 Vue3 后完全解决。

  2. 在服务端渲染阶段,对于 global 全局对象访问修改,如__mpx, __mpxRouter, __mpxPinia 都可能导致全局状态污染,所以在服务端渲染阶段请尽量避免进行相关操作;对于存在全局访问修改的方法,如 getApp(), getCurrentPages() 等在服务端渲染中被调用时,会产生相关报错提示。

  3. 由于服务器无法收到 URL 中的 hash 信息,使用 SSR 时需要通过修改 mpx.config.webRouteConfig 将路由模式设置成 history 模式。

# 包体积优化

近几年来随着小程序的商业成功和用户增长,各大互联网公司都在加大对小程序业务的投入,小程序的体量和复杂性都在快速增长,在这样的背景下,小程序的包体积大小成为了制约小程序业务迭代发展的一大限制,同时过大的包体积也会显著影响小程序的性能表现。

Mpx 在设计之初就考虑了包体积问题,完整继承了 webpack 已有的代码优化能力,如公共模块抽离、tree shaking、混淆压缩等。除此之外,我们还设计开发了业内最完善的分包构建流程,完整支持基于配置声明和依赖分析的普通分包、独立分包及分包异步化的构建输出,极大地缓解了复杂小程序中的主包和总包体积过大的问题。在 Mpx2.9 版本中,我们针对复杂小程序页面组件和 JS 模块众多的特点,对构建产物进行了针对性地优化,进一步降低了构建产物体积,主要优化项如下:

# 模版代码优化

Mpx 通过 webpack 进行打包构建,并对小程序原生 commonjs 支持进行适配改造,以实现 webpack 构建产物在小程序环境下运行,为了将构建产物中的模块和 chunk 链接到一起,webpack 和 mpx 会在构建产物生成一些模版代码进行相关工作,下面是一个页面 js 中包含的模版代码示例:

var self = {}
-self.webpackChunkmpx_test_2_8 = require('../bundle.js')
-(self.webpackChunkmpx_test_2_8 = self.webpackChunkmpx_test_2_8 || []).push([[405], {
-  1307: function (t, e, o) {
-    // ...
-  },
-  4236: function (t) {
-    // ...
-  },
-  // ...
-}, function (t) {
-  var e
-  e = 1307, t(t.s = e)
-}])
-

可以看出模版代码主要由 chunk 链接代码,chunk id,模块 id 和模块包装函数组成,对于中小项目来说,这些模版代码一般不会占用太多体积,但是在复杂大体量小程序中众多模块的叠加效应下,这部分体积就变得不可忽视。

我们对模版代码的生成逻辑和生成产物进行分析,发现 webpack 生产模式的默认配置中,很多配置项并不是体积最优的选项,一个典型的例子在于模块/chunk id:为了保障生成产物的内容稳定,来尽可能提升浏览器的缓存利用率,webpack 默认的模块/chunk id 采用 deterministic 模式进行生成,该模式下模块 id 为模块源路径的定长数字 hash,比项目模块总数长 1 位。由于在小程序中代码包按照版本的维度进行全量管理,保证文件局部的内容稳定在小程序环境下无正向意义,这就有了优化空间,我们可以简单将模块/chunk id 的生成逻辑改为数字自增,在主小程序中就能节省出上百 KB 总包体积。

类似的可优化点还存在于 chunk 链接代码和模块包装函数当中,我们在 @mpxjs/webpack-plugin@2.9 版本中提供了一个新的配置项 optimizeSize (opens new window),其中整合了一系列模版代码体积优化配置,开启后就能自动优化构建产物中的模版代码体积,在实际业务中,我们开启 optimizeSize 后可以减少总包体积约 1.6%,效果非常显著,下面是上述示例在开启 optimizeSize 后的产物对比:

var g = {}
-g.c = require('../bundle.js'), (g.c = g.c || []).push([[8], {
-  448: function (t, e, o) {
-    // ...
-  },
-  463: function (t) {
-    // ...
-  }
-  // ...
-}, function (t) {
-  var e
-  e = 448, t(t.s = e)
-}])
-

# 空模块移除

Mpx 在处理 .mpx 单文件时会将其分拆为 template/js/style/json 四个新模块来进行分别处理,其中 template/style/json 部分的内容会在处理后通过内置的 extractor 抽取输出为静态文件,抽取之后原本分拆出来的模块就会以空模块的形式残留在构建产物中(template 模块在抽取后还需要保留 render 函数和 refs 等信息所以不会成为空模块),如下所示:

/***/ 533:
-/***/ (function(__unused_webpack_module, __unused_webpack_exports, __webpack_require__) {
-// ...
-/* template */
-__webpack_require__(535)
-/* styles */
-__webpack_require__(536)
-/* json */
-__webpack_require__(537)
-/* script */
-__webpack_require__(534)
-/***/ }),
-/***/ 536:
-/***/ (function() {
-/***/ }),
-/***/ 537:
-/***/ (function() {
-/***/ })
-

从上面示例中可以看出 536/537 模块的定义和引用都是完全不必要的,但由于 webpack 本身对于空模块没有进行识别和优化的手段,在过去的版本中这些空模块代码会占用我们一部分总包体积。在 Mpx2.9 版本中,我们定义了全新的依赖类型 CommonjsExtractDependency 对于这类可能被抽取成为空模块的 request 进行识别处理,当模块内容在完成抽取后为空时,自动将其从模块图中移除,并在产物生成时不再生成其引用代码。该项优化措施内置开启,升级后自动生效,在实际业务中可减少总包体积约 0.7%

# Render函数优化

Render 函数是 Mpx 运行时 setData 优化的一项核心设计,我们在模版编译时将用户模版转换为简易的 render 函数,该函数执行时能够完全模拟视图渲染的数据访问过程,正确地收集当前视图的数据依赖,避免将视图不需要的数据通过 setData 发送到视图中。

render函数

虽然我们生成的简化版 render 函数仅保留了数据访问逻辑,在代码体积上并不算大,但是仍然存在着一定的优化空间,我们来看下面这个例子:

function render(){
-  this.a
-  if(this.c){
-    this.a
-    this.b
-    this.c.a
-    this.c.b
-    this.d
-  }
-  this.b
-}
-

可以看出 if block 下存在着大量冗余不必要的数据访问,例如 this.athis.b 在父级 block 中已经进行过访问,this.c.athis.c.b 由于在 if condition 中进行过 this.c 的访问也不再必要(render函数执行时会对模版依赖数据进行深度diff,父路径访问后子路径就无需再进行访问),上述 render 函数可以优化为:

function render(){
-  this.a
-  if(this.c){
-    this.d
-  }
-  this.b
-}
-

在 Mpx2.9 版本中,我们通过两轮 ast 遍历(2 pass ast travese)的方式实现了这个优化,在第一轮遍历中,我们收集了数据访问信息并按照 block 结构存储在 blockVisitTree 中,第二轮遍历时依据 blockVisitTree 中的信息对不必要的数据访问进行剪枝优化。

除此之外,我们也大幅优化了 render 函数中的数据收集代码,有效地降低了 render 函数的体积占用。

该优化目前没有默认开启,可以通过 @mpxjs/webpack-plugin 中的 optimizeRenderRules (opens new window) 配置项配置生效范围进行开启,全量开启后在实际业务中可节省总包体积约 5%

# 未来规划

在未来,我们对 Mpx 构建产物还有进一步的优化方案可以探索实施,主要分为两个方向:

  • 编译注入代码优化,在功能不变的情况下简化编译注入的代码,对于 render 函数也有一种运行时有损的方案(略微增大运行时开销)可以尝试
  • 模版和JSON压缩,对模版和JSON中的组件名及组件路径进行短哈希压缩,缺点是丢失dist产物的可读性,可能需要对其提供 sourceMap 支持

除此之外,我们近期的框架升级计划还包括:

  • 局部运行时渲染增强
  • 数据响应升级为 proxy 实现,输出 web 升级为 vue3
  • 支付宝 2.0 基础库适配优化
  • 微信 skyline 适配优化
  • 输出 Hummer 能力完善

在构建提速方面,我们也会在以下方向进行探索:

  • 输出 web 支持 vite 构建
  • 基于模块联邦的分布式构建
  • 基于 Rust 的高性能构建探索

最后,特别感谢@pagnkelly (opens new window)、@yandadaFreedom (opens new window)和 -@zhuzhh (opens new window)对于 2.9 版本功能开发的突出贡献,也欢迎大家使用 Mpx 进行小程序跨端开发并加入社区共建。

Github:https://github.com/didi/mpx

官网/文档:https://mpxjs.cn/

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/index.html b/docs-vuepress/.vuepress/dist/articles/index.html deleted file mode 100644 index 091d29837f..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/index.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Mpx框架相关文章 | Mpx框架 - - - - - - - - - - - - - diff --git a/docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html b/docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html deleted file mode 100644 index 0d3e732cba..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/mpx-cli-next.html +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - @mpxjs/cli 插件化改造 | Mpx框架 - - - - - - - - - -

# @mpxjs/cli 插件化改造

@mxpjs/cli 地址 (opens new window)

# 背景 & 现状

Mpx 脚手架 @mpxjs/cli 作为 Mpx 生态当中比较重要的一部分,是使用 Mpx 进行小程序开发的入口。

@mpxjs/cli@2.x 版本整体是基于模板配置的方式完成项目的初始化,整个的工作流是:

  1. 下载一份存放于远端的 mpx 项目原始模板(mpx-template)

  2. 根据用户的 prompts 选项完成用户选项的注入,并初始化最终的项目文件

完成项目的初始化后,除了一些基础配置文件外,整个项目的文件主要包含了如下的结构:

-- mpx-project
- |-- src // 项目源码
- |-- config // 项目配置文件
-   |-- index.js // 配置入口文件
-   |-- mpxLoader.conf.js // mpx-loader 配置
-   |-- mpxPlugin.conf.js // mpx webpack-plugin 配置
-   |-- user.conf.js // 用户的 prompts 选择信息
- |-- build // 编译构建配置
-   |-- build.js // 构建编译脚本
-   |-- getPlugins.js // webpack plugins 
-   |-- getRules.js // webpack module rules
-   |-- getWebpackConf.js // webpack 配置生成辅助函数
-   |-- utils.js // 工具函数
-   |-- webpack.base.conf.js // webpack 基础配置
-

在初始化的项目当中,有关项目的所有配置文件,编译构建代码是全部暴露给开发者的,开发者可以对这些文件进行修改来满足自己实际的项目开发需要。同时还可以基于这一套原始的模板文件二次拓展为满足自己业务场景的模板。

基于远程模板初始化项目的方式最大的一个好处就是将项目所有的底层配置完全暴露给开发者,开发者可以任意去修改对应的配置。

# mpxjs/cli@2.x 自身的痛点

目前 @mpxjs/cli@2.x 采用这种基于模板的方式面临着两方面的痛点:

  1. 对于 @mpxjs/cli 的使用者而言:
  • 模板和业务项目割裂:远程模板没有严格的版本控制,用户无法感知到远程模板的更新变化;

  • 项目升级困难:对于用户来说没有一个很好的方式完成升级工作,基本只能通过 copy 代码的方式,将 mpx-template 更新后的内容复制一份到自己的项目当中;或者是通过脚手架重新创建一个新的项目,将老的代码迁移到新项目当中;

  • 项目结构臃肿:从项目结构角度来说没法做到按需,初始化代码臃肿。Mpx 支持了小程序跨平台、跨端、小程序插件等等相关的开发,不同的编译构建配置都需要全部生成,在运行时阶段才能决定是否启动对应的功能;

  • 跨 Web 构建能力弱:在基于 Mpx 的跨 Web 场景构建中有关 web 侧的编译构建的配置是比较初级的,像 MPA 多页应用 等比较常用的功能是需要用户重新去手动搭建一套的;

  • 可拓展性差,基于目前的模板拉取的方式无法满足多样化的业务需求场景迭代;

  1. 对于 @mpxjs/cli 的开发者而言:
  • 分支场景多,功能模块耦合度高:脚手架的所有功能全部集合到一个大的模板当中。各部分的能力都是耦合在一起,为了满足不同项目的实际开发需要,代码里面需要写比较多的 if...else... 判断逻辑来决定要开启哪些功能,生成哪部分的模板;
  • 长期可维护性差,开发心智负担重;

针对以上问题,通过调研业内一些优秀的脚手架工具,发现 @vue/cli 插件化的架构设计能很好的去解决我们以上所遇到的问题。核心思路是将 @vue/cli 作为 @mpxjs/cli 底层的引擎,收敛 Mpx 对于核心依赖管理、模板、构建配置的能力,充分利用 @vue/cli 提供的插件机制去构建、拓展上层的插件集。

# 有关 @vue/cli

简单介绍下 @vue/cli 的插件化架构:

@vue/cli@3.x 相较于 2.x 版本相比,整个 @vue/cli 的架构发生了非常大的变化,从基于模板的脚手架迭代为基于插件化的脚手架。简单的概述下整个插件化的构架就是:

mpx-cli-3

  • @vue/cli 提供 vue cli 命令,负责偏好设置,生成模板、vue-cli-plugin 插件依赖管理的工作,例如 vue create <projectName>vue add <pluginName>

  • @vue/cli-service 作为 @vue/cli 整个插件系统当中的内部核心插件,提供了 npm script 注册服务,内置了部分 webpack 配置的同时,又提供了 vue-cli-plugin 插件的导入、加载以及 webpack 配置更新服务等。

以上是 @vue/cli 生态当中最为核心的两部分内容,二者分工明确,各司其职。

此外在 @vue/cli 生态当中非常重要的一个点就是 vue-cli-plugin 插件,每个插件主要完成模板生成及生产编译构建配置。根据 @vue/cli 设计的规范,开发一个 vue-cli-plugin 需要遵照相关的约定来进行开发:

  • @vue/cli 约定插件如果要生成模板,那么需要提供 generator 入口文件;

  • @vue/cli-service 约定插件的 webpack 配置更新需要放到插件的入口文件当中来完成,同时插件的命名也需要包含 vue-cli-plugin 前缀,因为 @vue/cli-service 是依据命名来加载相关的插件的;

# 插件化改造方案

一张图了解下插件化改造之后的 @mpxjs/cli 的架构设计:

mpx-cli-2

# 底层能力

@vue/cli@vue/cli-service 分别作为 @mpx/cli@mpx/cli-service 的底层能力,即利用插件化的架构设计,同时还非常灵活的满足了 Mpx 做差异化场景迭代的定制化改造。上层的插件满足 vue-cli-plugin 插件开发的规范,最终底层的能力还是依托于 @vue/cli@vue/cli-service 进行工作。

# 上层插件拆分

将原本大而全的模板进行插件化拆分,从 Mpx 所要解决的问题以及设计思路来考虑,站在跨平台的角度:

  1. web 开发
  • 基于 wx 的跨 web 开发;
  1. 小程序开发
  • 基于 wx 的跨平台(aliswanttdd)的小程序开发;
  • 使用云函数的微信小程序开发;
  • 微信小程序的插件模式的开发;

一个项目可能只需要某一种开发模式,例如仅仅是微信小程序的插件模式开发,也有可能是小程序和web平台混合开发等等,不同的开发模式对应了:

  1. 不同的目录结构

  2. 不同的编译构建配置


基于这样一种现状以及 @mpxjs/cli 所要解决的问题,从跨平台的角度出发将功能进行了拆分,最终拆解为如下的9个插件:

这些拆解出来的插件都将和功能相关的项目模板以及编译构建配置进行了收敛。

项目模板的生成不用说,借助 @vue/cliGenerator API 按需去生成项目开发所需要的模板,例如项目需要使用 eslint 的功能,那么在生成项目的时候会生成对应 vue-cli-plugin-mpx-eslint 所提供的模板文件,如果不需要使用,项目当中最终也不会出现和 eslint 相关的文件配置。

重点说下编译构建的配置是如何进行拆解的:

@vue/cli@3.x 基于插件的架构设计当中,决定是否要使用某个插件的依据就是判断这个插件是否被你的项目所安装和基于模板的构架相比最大的区别就是:基于模板的架构在最终生成的模板配置里需要保存一些环境配置文件,以供编译构建的运行时来判断是否启用某些功能。但是基于插件的架构基本上是不再需要这些环境配置文件的,因为你如果要使用一个插件的功能,只需要安装它即可。

因此依照这样的设计规范,我们将:

  • eslint

  • unit-test

  • typescript

这些非常独立的功能都单独抽离成了可拔插的插件,安装即启用。

以上功能有个特点就是和平台是完全解耦的,所以在拆解的过程中可以拆的比较彻底。但是由于 mpx 项目的特殊性,即要支持基于 wx 小程序的跨端以及 web 开发,同时还要支持小程序的云函数、小程序插件模式的开发,且不同开发模式的编译构建配置都有些差异。可以用如下的集合图来表示他们之间的关系:

mpx-cli-4

不同插件进行组合使用来满足不同场景下的使用。

在不同平台开发模式下是有 mpx 编译构建的基础配置的,这个是和平台没有太多关系,因此将这部分的配置单独抽离为一个插件:vue-cli-plugin-mpx同时这个插件也被置为了 @mpxjs/clipreset 预设插件,不管任何项目开发模式下,这个插件都会被默认的安装

// vue-cli-plugin-mpx
-module.exports = function (api, options, webpackConfig) {
-  webpackConfig.module
-    .rule('json')
-    .test(/\.json$/)
-    .resourceQuery(/asScript/)
-    .type('javascript/auto')
-
-  webpackConfig.module
-    .rule('wxs-pre-loader')
-    .test(/\.(wxs|qs|sjs|filter\.js)$/)
-    .pre()
-    .use('mpx-wxs-pre-loader')
-    .loader(MpxWebpackPlugin.wxsPreLoader().loader)
-
-  const transpileDepRegex = genTranspileDepRegex(options.transpileDependencies || [])
-  webpackConfig.module
-    .rule('js')
-    .test(/\.js$/)
-    .include
-    .add(filepath => transpileDepRegex && transpileDepRegex.test(filepath))
-    .add(filepath => /\.mpx\.js/.test(filepath)) // 处理 mpx 转 web 的情况,vue-loader 会将 script block fake 出一个 .mpx.js 路径,用以 loader 的匹配
-    .add(api.resolve('src'))
-    .add(api.resolve('node_modules/@mpxjs'))
-    .add(api.resolve('test'))
-    .end()
-    .use('babel-loader')
-    .loader('babel-loader')
-
-  const transpileDepRegex = genTranspileDepRegex(options.transpileDependencies)
-  webpackConfig.module
-    .rule('js')
-    .test(/\.js$/)
-    .include
-      .add(filepath => transpileDepRegex && transpileDepRegex.test(filepath))
-      .add(api.resolve('src'))
-      .add(api.resolve('node_modules/@mpxjs'))
-      .add(api.resolve('test'))
-        .end()
-    .use('babel-loader')
-      .loader('babel-loader')
-
-  webpackConfig.resolve.extensions
-    .add('.mpx')
-    .add('.wxml')
-    .add('.ts')
-    .add('.js')
-
-  webpackConfig.resolve.modules.add('node_modules')
-}
-

在小程序的开发模式下,vue-cli-plugin-mpx-mp 会在 vue-cli-plugin-mpx 的基础上去拓展 webpack 配置以满足小程序的编译构建。

在跨 web 的开发模式下,vue-cli-plugin-mpx-web 会在 vue-cli-plugin-mpx@vue/cli 的基础上去拓展配置以满足 web 侧的开发编译构建。

# Web 平台编译构建能力增强

@mpxjs/cli@2.x 版本当中有关 web 侧的编译构建的配置是比较初级的,像 热更新MPA 多页应用 等比较常用的功能是需要用户重新去手动搭建一套的。而 @vue/cli@3.x 即为 vue 项目而生,提供了非常完备的 web 应用的编译构建打包配置。

所以 @mpxjs/cli@next 版本里面做了一项非常重要的工作就是复用 @vue/cli 的能力,弥补 mpx 项目在跨 web 项目编译构建的不足。

因此关于 mpxweb 编译构建的部分也单独抽离为一个插件:vue-cli-plugin-mpx-web,这个插件所做的工作就是在 @vue/cli 提供的 web 编译构建的能力上去适配 mpx 项目。这样也就完成了 mpxweb 项目编译构建能力的增强。

这也意味着 @vue/cli 所提供的能力基本上在 mpx 跨 web 项目当中都可使用。

# 项目配置拓展能力

@mpxjs/cli@2.x 版本的项目如果要进行配置拓展,基本需要进行以下2个步骤:

  1. config 文件夹下的相关的配置文件进行修改;

  2. build 文件夹下的编译构建配置文件进行修改;

这也是在文章一开始的时候就提到的基于模板的大而全的文件组织方式。

而在 @mpxjs/cli@next 版本当中,将项目的配置拓展全部收敛至 vue.config.js 文件当中去完成,同时减少了开发者需要了解项目结构的心智负担。

// vue.config.js
-
-module.exports = {
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        // mpx-plugin 相关的配置
-      },
-      loader: {
-        // mpx-loader 相关的配置
-      }
-    }
-  },
-  configureWebpack() {},
-  chainWebpack() {
-    // 自定义的 webpack 配置
-  }
-}
-

# 改造后的目录结构

在第一章节展示了目前 @mpxjs/cli@2.x 初始化项目的结构和现状。经过这次的插件化的改造后,项目的结构变得更为简洁,开发者只需要关注 src 源码目录以及 vue.config.js 配置文件即可:

-- mpx-project
- |--src
- |--vue.config.js 
-

# 没有银弹

虽然基于 @vue/cli 插件的架构模式完成了 @mpxjs/cli@3.x 的插件化改造升级。但是由于 mpx 项目开发的一些特殊性,不同插件之间的协同工作是有一些约定的。

例如 @vue/cli-service 内置了一些 webpack 的配置,因为 @vue/cli 是专门针对 web应用的,这些配置会在所有的编译构建流程当中生效,不过这些配置当中有些对于小程序的开发来说是不需要的。

那么针对这种情况,为了避免不同模式下的 webpack 配置相互污染。web 侧的编译构建还是基于 @vue/cli 提供的能力去适配 mpxweb 开发。而小程序侧的编译构建配置是通过 @vue/cli-service 内部暴露出来的一些方法去跳过一些对于小程序开发来说不需要的 webpack 配置来最终满足小程序的构建配置。

因此在各插件的开发当中,需要暴露该插件所应用的平台:

module.export.platform = 'mp'  // 可选值: 'web'
-

这样在实际的构建过程当中通过平台的标识来决定对应哪些插件会生效。

# 如何开发一个基于 mpx 的业务脚手架插件

具体参阅文档 (opens new window)

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html b/docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html deleted file mode 100644 index 6c43a1c96d..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/mpx-cube-ui.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - 小程序跨端组件库 Mpx-cube-ui 开源啦 | Mpx框架 - - - - - - - - - -

# 小程序跨端组件库 Mpx-cube-ui 开源啦

作者:CommanderXL (opens new window)

Mpx-cube-ui 是一款基于 Mpx 小程序框架 (opens new window) 的移动端基础组件库,一份源码可以跨端输出所有小程序平台及 Web,同时具备良好的拓展能力和可定制化的能力来帮助你快速构建 Mpx 应用项目。

Mpx-cube-ui 提供了灵活配置的主题定制能力,在组件设计开发阶段对表现层的结构和样式进行抽离,利用预编译器和 CSS 变量的能力,提供细粒度(颜色、字体、圆角、阴影等)的样式定制能力,你的项目可以按需使用主题的编译方案还是运行时方案来满足不同样式风格的业务场景开发。

Mpx-cube-ui 提供了开箱即用的跨端输出能力,源码基于 Mpx 小程序框架进行开发,依托于 Mpx 提供的跨平台能力 (opens new window)即基于微信小程序跨平台编译输出为支付宝、百度、QQ、头条等目标平台的小程序代码,同时还可以输出到 Web。

接下来会通过这篇文章去分享一下 Mpx-cube-ui 是如何诞生的。

# 业务现状

# 集团产品的迭代

越来越多的业务产品开始借助小程序的渠道来拓展产品的推广和使用,不同的技术团队承接的不同业务产品方向的需求。在滴滴所有的小程序产品当中,滴滴出行小程序作为最大的C端流量入口,承接了不同业务产品流量分发。在具体到小程序产品的研发环节,不同业务的小程序都被集成到了滴滴出行小程序当中,一同打包上线并发布。 -当然在这种跨业务、技术部门的产品研发当中,不同的技术团队都会有自身配套的基础能力建设。就拿小程序的组件库来说,每个独立的业务产品都会依据对应的交互设计规范来搭建一套满足自身业务述求的基础组件用以提高日常的业务开发效率,例如按钮、半浮层弹窗、Toast、Dialog、表单等等。

# 团队内的业务迭代

除了跨业务产品研发的场景外,在同一个技术团队内部也可能会承接来自不同业务产品方向的需求,这些不同业务产品同样也会有独立发展迭代的述求。由于业务场景的约束,团队内部同样也会面临如何做基础能力的沉淀和复用的议题,在节省研发资源的同时去提升业务项目交付的质量和效率。 -


那么在不同的业务产品发展初期,为了快速交付产品的功能保障上线时间,研发侧会尽量利用之前已有的基础能力来快速搭建产品功能。就拿小程序组件库的复用来说,常见的方式有:

  • 直接侵入到原有的组件代码当中进行 Hard Code,不同的业务产品依赖同一份组件代码,利用一些 if/else 或者条件编译等手段来使得原有的组件在满足最新的业务述求的同时,还要确保不会影响到原有的业务使用;
  • Fork 一份原有组件库的代码,在此基础上单独迭代维护,做到和原有组件的彻底隔离而不会造成污染;

对于第一种方案来说:组件本身因为增量的差异化需求使得组件本身的维护成本变高,同时对于差异化的代码处理难免在编译打包环节出现代码冗余问题,也就是原本不属于当前业务产品的代码实现也被一同打包; -对于第二种方案来说:组件库维护的数量变多,使得后续的组件迭代和更新需要修改多处代码;

此外,在小程序的场景下还面临着包体积等平台规范的硬性约束,特别是对于像滴滴出行小程序这种体量大的小程序来说,众多小程序业务产品被集成到同一个小程序当中,包体积也是日常研发过程中重点关注的一个指标。模块或组件的可复用性好、可拓展性强也意味着不需要重复去开发同一份功能相同代码,随之代码包体积也不会因重复实现相同功能而增速过快。

# Mpx 技术生态

在 Mpx 整个技术生态当中,组件体系目前是不完备的。

在我们目前使用 Mpx 去搭建实际的业务产品的过程中,我们基于小程序平台的基础组件去搭建了特定业务场景的基础业务组件,因为业务场景、功能和代码实现等各方面的原因,这些业务场景的基础组件是没法开放给社区的 Mpx 开发者来使用的。因此对于社区来说,要么只能同样基于平台的基础组件去开发上层基础组件,或者是使用第三方的原生小程序组件库,不管哪种方式对于社区的开发者而言,都有不少的上手和开发成本。

因此在社区当中也有比较强的述求,期望 Mpx 能提供更为基础通用的组件来更好的支持上层的业务开发。

# 组件库自身的维护

  • 文档示例一体化

不管是我们内部维护的业务组件库还是之前我们开源的基于 Vue 的 Web 组件库 Cube-ui (opens new window),组件本身的文档示例也是在组件库迭代过程中也是比较占用时间精力的,在示例和文档方面往往需要重复写多份文案,还要确保它们之间是同步更新的。那么针对组件库自身的维护,如何尽量减少组件库本身的维护成本也是我们需要深入思考的一部分工作。

以上这些问题促使我们重新思考整个 Mpx 组件体系的构建和发展。

# Mpx 组件体系

经过业务的长期迭代和验证,Mpx-cube-ui 作为 Mpx 技术生态当中组件体系的基础设施:脱离于业务的基础组件库,同时又要有良好的拓展能力和可定制化的能力来更好的支持上层业务。

组件分层设计 -在整个组件体系的搭建过程中,我们依据组件的特性将组件拆分为如下几种类型:

  • 业务组件一般来源于某一个具体的产品功能,用以解决特定问题域,更加强调关注点分离,代码的维护成本;
  • 基础业务组件一般来源于某一类的业务产品,在特定的业务背景下抽象的比较通用的,较少糅合业务逻辑,介于基础组件和业务组件之间,强调解决效率,可复用性,可维护性;
  • 基础组件一般来源于我们设计交互元语言,用以定义业务产品和用户的交互、反馈,更加强调一致性,效率,可复用性;

底层的基础组件(可组合性、可拓展性、稳定性)是为了更好的服务于上层的(基础)业务组件。

从底层的基础组件到业务组件(由下至上),组件的业务属性越来越强,所要解决的问题域更加聚焦,更贴合具体的问题和场景,同时它们的可复用性也随之降低。

# Mpx-cube-ui 组件设计

这里通过乘客交易、司机运营两个不同业务方向的组件来举例说明 Mpx-cube-ui 在组件的设计和开发当中所做的一些思考。 -在这两个业务场景当中都有 Modal 半浮层组件,不过在司乘业务不同的设计规范约束下,组件间的差异还是比较大的。

mpx-cube-ui-modal

那么为了解决上文当中组件库在跨业务产品的的可复用性、可拓展性的问题,核心所遵循的原则是:

弱化不同业务场景的设计规范(去业务),回归到组件本身的行为(逻辑)、结构和样式(表现):

  • 行为,组件所特有的行为方法:展示(show),隐藏(hide),关闭(close);
  • 结构,方便组件间的重新组合:头部区,标题区,内容区,底部操作区;
  • 样式,方便业务场景的主题定制:颜色、字号、圆角、边距等;

组件设计原则

对于 Mpx-cube-ui 组件的主题定制有个简单的逻辑关系:

  • 全局基础样式变量:提供了基本色值、字号等,会影响到所有组件的展示;
  • 组件样式变量:对于基本的色值、字号等继承于全局基础变量,涉及到组件自身的结构、样式变量会单独定义;
  • 组件渲染样式:直接依赖组件样式变量;

mpx-cube-ui-modal

通过定制全局基础样式变量 (opens new window)组件样式变量 (opens new window)都能达到定制主题的目的,有关主题定制的功能具体参见文档 (opens new window)

在 Mpx-cube-ui 实现的定制主题的方案当中是利用预编译器和 Css Varaibles 的能力来提供细粒度的样式定制能力:

业务架构图

  • 利用预编译器可编程的能力,在编译阶段就可以完成主题能力的定制,当你的业务项目作为独立应用迭代,可以只利用预编译器的能力,从而使得你的 css 代码体积尽可能的小;
  • 利用 Css Varaibles 的能力,可以解决更为复杂的场景,例如同一个组件在巨型应用当中,在不同业务场景页面需要有不同的主题样式;

官网示例 (opens new window)当中可以直接体验主题切换功能。

# 未来的规划

  1. 开源更多我们内部所沉淀出的基础组件,去满足多样化的业务场景开发;
  2. 将文档示例一体化的能力独立打包开源,用以组件库的快速建站以及降低文档、示例代码的维护成本。
- - - diff --git a/docs-vuepress/.vuepress/dist/articles/mpx1.html b/docs-vuepress/.vuepress/dist/articles/mpx1.html deleted file mode 100644 index 7aea0fe8b3..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/mpx1.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - 小程序开发者,为什么你应该尝试下MPX | Mpx框架 - - - - - - - - - -

# 小程序开发者,为什么你应该尝试下MPX

作者:sky-admin (opens new window)

MPX框架是滴滴出行推出的一款专注小程序开发的增强型框架。本篇文章将从使用角度谈谈MPX的优势与好处。如果嫌内容太长,优势部分每个小节都有简单的一句话总结,可以快速阅读。如果想了解更多设计细节,可以阅读 前一篇文章 - MPX2.0发布

# 背景

在小程序逐渐火热的今天,越来越多的开发者需要进行小程序的开发。原生小程序的开发有诸多不便,开发者又需要在众多的小程序框架中做出抉择。

那么今天,我们要给大家安利一款小程序框架:MPX

# 优势

之所以建议开发者们考虑使用MPX框架来开发小程序,是因为MPX框架具有一些别的框架所没有的优点。

MPX立足原生小程序,在保证坑少的同时做了很多能力增强,提供了数据响应、模板增强、性能优化、跨平台开发等能力,以提升用户的开发体验及效率。

接下来会从 原生兼容 -> 第三方组件支持 -> 按需构建 -> 跨平台编译 -> 能力增强 -> 独特性能优势 六个点来逐一讲述。

# 原生兼容

MPX完全兼容原生,坑少。渐进接入简单。

从语法风格上,我们可以看到目前市面上流行的小程序框架基本是基于web框架(taro/nanachi - react,uniapp/megalo/mpvue - vue)或者是一套全新(chameleon)/ 半全新(wepy)的标准。

使用了这些框架,你所写的代码,并不是小程序代码。而是react/vue或者另一套代码。而这些代码源码到小程序代码,需要经过一次全面的转换,这个转换可能会引入一些未知的问题,产生一些坑。

同时随着时间,小程序自身会逐步迭代,做出更多的功能特性,提供更好的组件、方法。而一些框架可能会受限于精力或框架节奏,没有办法第一时间跟进,甚至框架慢慢疏于维护而无法使用。

而MPX选择的是,全面拥抱原生

口说无凭,我们来看个典型的MPX组件长什么样。

mpx组件示例

乍一看好像和vue没什么区别,也就多了个json块,template里写的是小程序的标签。

由于这一块全是符合微信小程序原生语法,我们是不会做任何转换的,所以你写什么就是什么。(如果使用了MPX的增强特性,还是会进行一些必要的转换的,后续我们也会出文章详细解释MPX的增强是如何实现的,相对来说,我们的转换比较轻量、透明、易理解)

当微信出了新的能力、新的标签、新的生命周期钩子,使用MPX框架来编写的小程序只需要直接用起来就行。

所以,使用MPX框架,你可以轻易地使用 自定义组件的relations (opens new window) 来搞定组件间关系,使用 wxs (opens new window) 来更好地构建页面。

MPX几乎支持原生的每一个特性,在 .mpx 文件里,模板部分写的是原生小程序的模板语法,脚本部分写的是原生小程序的脚本语法,json部分写的是原生小程序的配置信息。用MPX,你才是真的在开发小程序。

目前很多原生小程序开发者可能想尝试下框架,老项目接入框架,选MPX肯定是最简单的了。口说无凭,我们搞了个demo来给大家打个样:在我们GitHub项目中有examples文件夹,里面的 原生项目渐进接入MPX示例 (opens new window)

# 第三方组件库

MPX提供了完备的第三方组件库支持

上面说了MPX对原生的极致兼容,能让你想到什么?对,就是对第三方组件库的完美支持。

支持第三方组件库的重要性大家都知道,所以这个能力大部分框架都支持了。但是支持和完美支持还是有区别的。据简单观察,taro/mpvue/uniapp对于第三方组件库的支持都是以复制的形式进行的,也就是和微信小程序本来的行为很像。

那么MPX是怎么支持第三方组件库的呢,这里有个demo:也在我们的GitHub里的examples文件夹下,MPX使用第三方组件库示例 (opens new window) ,核心代码见下图:

MPX使用第三方组件库代码示例

乍一眼看不太出来有什么特别的?没用过别的框架引第三方组件,简单找了找其他框架好像也没提供相应的demo,用过的朋友可以自行对比下。

在MPX里使用第三方组件库,仅需要***像web项目一样npm安装即可,并不需要复制文件***。然后在json里直接写包名就会去node_modules下面查找了。再配合webpack alias可以做到更简单、更语义化。

然而这还没有结束~

细心的朋友会发现,这段示例代码中既有vant的组件,也有iview的组件,如果按照微信的规范,这些组件库会通过miniprogram字段指定自己的构建文件生成目录,开发者工具会把这个目录完全拷贝到最终发布的代码里去,我们就会有两个巨大的组件库占据宝贵的空间。

我们当然是希望用多少引多少,而不是一股脑全引进去,对,于是MPX提供了按需引用的能力,在下一章按需引用细讲。

以及,组件库目前很少有跨小程序平台的组件库啊,如果我用了vant,支付宝、QQ里没有vant怎么办?也许这是别的框架不怎么推荐使用第三方库的原因,而MPX里,我们帮你把别人的组件库也转了,细节看下下章跨小程序平台

# 按需引用

通过webpack依赖分析收集,使用第三方组件库或者拆分开发大型项目时MPX能保证构建的代码全是要用到的代码

原生小程序本身的编译是遍历项目文件夹里所有的JS,包装成一个AMD包,也就是说项目文件夹里所有的文件,不论是否被使用,都会占用包体积并上传。

同时,原生微信小程序的npm支持是基于文件夹复制的,第三方包通过声明miniprogram字段指定要拷贝的文件夹,不论使用还是未使用的资源(模板/js/样式/图片),全会被复制到项目文件夹中。

而我们提供了@mpxjs/webpack-plugin插件,借助webpack生态,解析.mpx文件的json部分或原生的json文件将依赖作为新的入口添加子编译。基于依赖收集,而不是文件遍历。

带来的好处就是:如果你喜欢vant的按钮,iview的输入框,wux的布局,欢迎尝试MPX,让你能同时使用多个UI框架的同时不用担心应用的体积爆炸。

同理,面对一个大型项目,我们可以拆成不同的部分,由不同的团队完成后发npm包,在一个主项目中引入即可,具体内容可以看文档packages一节。

收集依赖的细节可以查阅文档编译构建一节。

# 跨小程序平台

MPX的跨平台方法能带着第三方组件库一起跨小程序平台,同时提供了充足完善的条件编译能力。

在 MPX 1.0 时代,MPX框架是专注提升微信小程序的开发体验,虽然也提供了支付宝版,但代码完全要另写。

而随着越来越多的 super app 提供了小程序能力,目前至少有5种体系的小程序(微信、支付宝系列、百度系列、头条系列、QQ),如果每一个平台都需要维护一份代码,工程师人数明显不够用了,所以跨小程序平台的能力也是 MPX 2.0 的主打特性。

我们的跨平台的方法就是转换。都是小程序,语法基本一样,配置、钩子的差异在MPX运行时里提供了抹平。

而除此之外最大的区别也就是模板上的标签和指令。所以我们实现了一套转换的架子,再编写一份转换规则,即可完成微信小程序到支付宝、百度、头条小程序的转换。

采用这种转换的模式,非常方便用户理解我们是如何把微信小程序转换成支付宝、百度等小程序平台的。而且只要用户有需求,可以补齐任一套小程序转换其他平台的规则,就可以完成以某个小程序为标准为基础来编写小程序代码以及进一步转换成别的平台的能力。

再结合前面一直在说的我们对原生小程序的支持,就可以撞出一点不一样的东西,比如,前文提到的第三方组件库跨小程序平台。

对,我们能帮你把针对微信编写的ui组件库在支付宝、百度上运行起来,带着组件库一起跨小程序平台。

那么一定会有这样一个问题,就算MPX对原生的支持再怎么牛逼,有的基础能力只有微信平台有,别的平台没有,MPX的转换还能无中生有吗?

当然不能,其实这个问题对于所有的跨端框架都是一个问题,所以跨端最核心的问题是,如何搞定差异化部分。

MPX提供了丰富的条件编译能力,可以以文件为维度差别构建,可以以代码块为维度,也可以以代码维度进行差别构建。

而且MPX的差异化构建能力也是完全基于webpack实现的,所以上面提到的第三方组件库如果确实存在转换不了的地方,比如vant的picker组件使用内联wxs写了一个小方法叫isSimple在模板里调用了,但是这个方法的写法在百度小程序的filter脚本(filter可以理解为百度小程序的wxs)里不支持,因为百度的filter要求必须导出一个对象包裹方法。

最好的解决办法当然是给vant-weapp提pr帮他们解决一下这个问题,但时间可能会比较慢,所以在MPX里,可以利用webpack的alias能力:

通过alias解决第三方组件的跨平台问题

当尝试构建百度小程序时,会优先去查找pick/index.swan.wxml,再被alias到一个src下的文件,自己修改一下第三方包里有一些小问题的部分即可。

关于跨平台的条件编译,更多具体信息可以查阅我们的官方文档 - 跨平台编译

# 能力增强

通过数据响应、编译时预处理提供了computed/watch,完备的样式类型绑定,双向数据绑定,动态组件等一系列方便开发者更好开发小程序的能力增强

能力增强应该是一个框架提供的最核心最重要的能力了,而MPX也确实在这里下了很大的力气,提供了多且好用的能力增强,不过受限于此处的篇幅,就只简单介绍,细节大家还是查阅我们的文档的好。

别的框架由于往往基于react/vue的,会给个列表写明不支持哪些能力,用户写的时候习惯使然,往往用了后可能才反应过来哦这个不支持。MPX则是原生的小程序语法写着难受时候突然想起MPX有这个能力。

列一下MPX增强的能力:

  • 模板上的增强 -
    • 样式类名绑定
    • 内联事件传参
    • 动态组件
    • 双向绑定
    • 节点获取ref
  • JS里的增强 -
    • 数据响应
    • setData优化
    • ES6+
  • 样式上的增强 -
    • 预处理支持
    • rpx转换
  • JSON里的增强 -
    • packages
    • 分包资源优化

MPX最显著的能力是数据响应,它衍生出computed/watch,以及双向数据绑定等。这个能力和Vue比较像,不同的是在MPX里是由mobx提供的数据响应能力。

而同样是数据响应,我们做了一些不一样的优化。

# 性能优势

通过对模板的解析抽象出访问的数据以保证在提供了数据响应能力的同时不至于劣化性能。

mpvue/wepy/megalo等框架也提供了数据响应的能力,但是数据响应在小程序领域有个较大的问题,微信开发指南里明确提到要注意setData的调用频次和数据量的大小。

而数据响应最基本的做法就是数据变了就去set数据,这会极大劣化小程序的性能表现。

而MPX通过对模板进行解析,抽象出对应的render函数,在调用setData发送数据前执行render函数找到真正需要发送的数据。

效果如图:

小程序性能分析

小程序开发者工具的audits面板能辅助用户分析出可能需要优化的点。正如前文所说,MPX在红框部分,尤其是红框里的第三条,不将模板上未使用的数据发送到渲染层上做了极大的优化。

只要不出现渲染函数执行失败(会有warning在console里提示,同时兜底逻辑会进行全量setData以保证程序仍可正常运行),使用MPX开发的小程序就永远不用担心发送了模板未使用的数据。

虽然只是一个小小的TODO MVC示例,但是这个优化和应用的规模没关系,而且同时大家可以尝试别家的小demo对比看看。

这个优化的细节可以看前一篇文章,或者我们的文档MPX运行机制 - 数据响应与性能优化

# 总结

与目前市面上的诸多框架相比,MPX希望以原生小程序为基础,全面拥抱原生小程序,在原生小程序的基础上做增强,通过尽可能少的转换实现尽可能多的能力增强,在提升小程序开发体验的同时,保证不因转换或框架的问题产生过多的坑。

MPX框架的目标用户是对小程序质量有较高要求的开发者,如果你是原生小程序开发者,或者厌倦了解决某些以web框架DSL语法为基础的转换框架造成的坑,欢迎尝试MPX框架。

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/mpx2.html b/docs-vuepress/.vuepress/dist/articles/mpx2.html deleted file mode 100644 index be760c89ba..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/mpx2.html +++ /dev/null @@ -1,740 +0,0 @@ - - - - - - Mpx 小程序框架技术揭秘 | Mpx框架 - - - - - - - - - -

# Mpx 小程序框架技术揭秘

作者:CommanderXL (opens new window)

与目前业内的几个小程序框架相比较而言,mpx 开发设计的出发点就是基于原生的小程序去做功能增强。所以从开发框架的角度来说,是没有任何“包袱”,围绕着原生小程序这个 core 去做不同功能的 patch 工作,使得开发小程序的体验更好。

于是我挑了一些我非常感兴趣的点去学习了下 mpx 在相关功能上的设计与实现。

# 编译环节

# 动态入口编译

不同于 web 规范,我们都知道小程序每个 page/component 需要被最终在 webview 上渲染出来的内容是需要包含这几个独立的文件的:js/json/wxml/wxss。为了提升小程序的开发体验,mpx 参考 vue 的 SFC(single file component)的设计思路,采用单文件的代码组织方式进行开发。既然采用这种方式去组织代码的话,那么模板、逻辑代码、json配置文件、style样式等都放到了同一个文件当中。那么 mpx 需要做的一个工作就是如何将 SFC 在代码编译后拆分为 js/json/wxml/wxss 以满足小程序技术规范。熟悉 vue 生态的同学都知道,vue-loader 里面就做了这样一个编译转化工作。具体有关 vue-loader 的工作流程可以参见我写的文章 (opens new window)

这里会遇到这样一个问题,就是在 vue 当中,如果你要引入一个页面/组件的话,直接通过import语法去引入对应的 vue 文件即可。但是在小程序的标准规范里面,它有自己一套组件系统,即如果你在某个页面/组件里面想要使用另外一个组件,那么需要在你的 json 配置文件当中去声明usingComponents这个字段,对应的值为这个组件的路径。

在 vue 里面 import 一个 vue 文件,那么这个文件会被当做一个 dependency 去加入到 webpack 的编译流程当中。但是 mpx 是保持小程序原有的功能,去进行功能的增强。因此一个 mpx 文件当中如果需要引入其他页面/组件,那么就是遵照小程序的组件规范需要在usingComponents定义好组件名:路径即可,mpx 提供的 webpack 插件来完成确定依赖关系,同时将被引入的页面/组件加入到编译构建的环节当中

接下来就来看下具体的实现,mpx webpack-plugin 暴露出来的插件上也提供了静态方法去使用 loader。这个 loader 的作用和 vue-loader 的作用类似,首先就是拿到 mpx 原始的文件后转化一个 js 文本的文件。例如一个 list.mpx 文件里面有关 json 的配置会被编译为:

require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=json&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/json-compiler/index?root=!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=json&index=0!./list.mpx")
-

这样可以清楚的看到 list.mpx 这个文件首先 selector(抽离list.mpx当中有关 json 的配置,并传入到 json-compiler 当中) --->>> json-compiler(对 json 配置进行处理,添加动态入口等) --->>> extractor(利用 child compiler 单独生成 json 配置文件)

其中动态添加入口的处理流程是在 json-compiler 当中去完成的。例如在你的 page/home.mpx 文件当中的 json 配置中使用了 局部组件 components/list.mpx:

<script type="application/json">
-  {
-    "usingComponents": {
-      "list": "../components/list"
-    }
-  }
-</script>
-

在 json-compiler 当中:

...
-
-const addEntrySafely = (resource, name, callback) => {
-  // 如果loader已经回调,就不再添加entry
-  if (callbacked) return callback()
-  // 使用 webpack 提供的 SingleEntryPlugin 插件创建一个单文件的入口依赖(即这个 component)
-  const dep = SingleEntryPlugin.createDependency(resource, name)
-  entryDeps.add(dep)
-  // compilation.addEntry 方法开始将这个需要被编译的 component 作为依赖添加到 webpack 的构建流程当中
-  // 这里可以看到的是整个动态添加入口文件的过程是深度优先的
-  this._compilation.addEntry(this._compiler.context, dep, name, (err, module) => {
-    entryDeps.delete(dep)
-    checkEntryDeps()
-    callback(err, module)
-  })
-}
-
-const processComponent = (component, context, rewritePath, componentPath, callback) => {
-  ...
-  // 调用 loaderContext 上提供的 resolve 方法去解析这个 component path 完整的路径,以及这个 component 所属的 package 相关的信息(例如 package.json 等)
-  this.resolve(context, component, (err, rawResult, info) => {
-    ...
-    componentPath = componentPath || path.join(subPackageRoot, 'components', componentName + hash(result), componentName)
-    ...
-    // component path 解析完之后,调用 addEntrySafely 开始在 webpack 构建流程中动态添加入口
-    addEntrySafely(rawResult, componentPath, callback)
-  })
-}
-
-if (isApp) {
-  ...
-} else {
-  if (json.usingComponents) {
-    // async.forEachOf 流程控制依次调用 processComponent 方法
-    async.forEachOf(json.usingComponents, (component, name, callback) => {
-      processComponent(component, this.context, (path) => {
-        json.usingComponents[name] = path
-      }, undefined, callback)
-    }, callback)
-  }
-  ...
-}
-...
-

这里需要解释说明下有关 webpack 提供的 SingleEntryPlugin 插件。这个插件是 webpack 提供的一个内置插件,当这个插件被挂载到 webpack 的编译流程的过程中是,会绑定compiler.hooks.make.tapAsynchook,当这个 hook 触发后会调用这个插件上的 SingleEntryPlugin.createDependency 静态方法去创建一个入口依赖,然后调用compilation.addEntry将这个依赖加入到编译的流程当中,这个是单入口文件的编译流程的最开始的一个步骤(具体可以参见 Webpack SingleEntryPlugin 源码 (opens new window))。

Mpx 正是利用了 webpack 提供的这样一种能力,在遵照小程序的自定义组件的规范的前提下,解析 mpx json 配置文件的过程中,手动的调用 SingleEntryPlugin 相关的方法去完成动态入口的添加工作。这样也就串联起了所有的 mpx 文件的编译工作。

# Render Function

Render Function 这块的内容我觉得是 Mpx 设计上的一大亮点内容。Mpx 引入 Render Function 主要解决的问题是性能优化方向相关的,因为小程序的架构设计,逻辑层和渲染层是2个独立的。

这里直接引用 Mpx 有关 Render Function 对于性能优化相关开发工作的描述:

作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个:

  • 尽可能减少setData调用的频次
  • 尽可能减少单次setData传输的数据 -为了实现以上两个优化方向,我们做了以下几项工作:

将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次;

将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。

接下来我们看下 Mpx 是如何实现 Render Function 的。这里我们从一个简单的 demo 来说起:

<template>
-  <text>Computed reversed message: "{{ reversedMessage }}"</text>
-  <view>the c string {{ demoObj.a.b.c }}</view>
-  <view wx:class="{{ { active: isActive } }}"></view>
-</template>
-
-<script>
-import { createComponent } from "@mpxjs/core";
-
-createComponent({
-  data: {
-    isActive: true,
-    message: 'messages',
-    demoObj: {
-      a: {
-        b: {
-          c: 'c'
-        }
-      }
-    }
-  },
-  computed() {
-    reversedMessage() {
-      return this.message.split('').reverse().join('')
-    }
-  }
-})
-</script>
-

.mpx 文件经过 loader 编译转换的过程中。对于 template 模块的处理和 vue 类似,首先将 template 转化为 AST,然后再将 AST 转化为 code 的过程中做相关转化的工作,最终得到我们需要的 template 模板代码。

packages/webpack-plugin/lib/template-compiler.js模板处理 loader 当中:

let renderResult = bindThis(`global.currentInject = {
-    moduleId: ${JSON.stringify(options.moduleId)},
-    render: function () {
-      var __seen = [];
-      var renderData = {};
-      ${compiler.genNode(ast)}return renderData;
-    }
-};\n`, {
-    needCollect: true,
-    ignoreMap: meta.wxsModuleMap
-  })
-

在 render 方法内部,创建 renderData 局部变量,调用compiler.genNode(ast)方法完成 Render Function 核心代码的生成工作,最终将这个 renderData 返回。例如在上面给出来的 demo 实例当中,通过compiler.genNode(ast)方法最终生成的代码为:

((mpxShow)||(mpxShow)===undefined?'':'display:none;');
-if(( isActive )){
-}
-"Computed reversed message: \""+( reversedMessage )+"\"";
-"the c string "+( demoObj.a.b.c );
-(__injectHelper.transformClass("list", ( {active: isActive} )));
-

mpx 文件当中的 template 模块被初步处理成上面的代码后,可以看到这是一段可执行的 js 代码。那么这段 js 代码到底是用作何处呢?可以看到compiler.genNode方法是被包裹至bindThis方法当中的。即这段 js 代码还会被bindThis方法做进一步的处理。打开 bind-this.js 文件可以看到内部的实现其实就是一个 babel 的 transform plugin。在处理上面这段 js 代码的 AST 的过程中,通过这个插件对 js 代码做进一步的处理。最终这段 js 代码处理后的结果是:

/* mpx inject */ global.currentInject = {
-  moduleId: "2271575d",
-  render: function () {
-    var __seen = [];
-    var renderData = {};
-    (renderData["mpxShow"] = [this.mpxShow, "mpxShow"], this.mpxShow) || (renderData["mpxShow"] = [this.mpxShow, "mpxShow"], this.mpxShow) === undefined ? '' : 'display:none;';
-    "Computed reversed message: \"" + (renderData["reversedMessage"] = [this.reversedMessage, "reversedMessage"], this.reversedMessage) + "\"";
-    "the c string " + (renderData["demoObj.a.b.c"] = [this.demoObj.a.b.c, "demoObj"], this.__get(this.__get(this.__get(this.demoObj, "a"), "b"), "c"));
-    this.__get(__injectHelper, "transformClass")("list", { active: (renderData["isActive"] = [this.isActive, "isActive"], this.isActive) });
-    return renderData;
-  }
-};
-

bindThis 方法对于 js 代码的转化规则就是:

  1. 一个变量的访问形式,改造成 this.xxx 的形式;
  2. 对象属性的访问形式,改造成 this.__get(object, property) 的形式(this.__get方法为运行时 mpx runtime 提供的方法)

这里的 this 为 mpx 构造的一个代理对象,在你业务代码当中调用 createComponent/createPage 方法传入的配置项,例如 data,都会通过这个代理对象转化为响应式的数据。

需要注意的是不管哪种数据形式的改造,最终需要达到的效果就是确保在 Render Function 执行的过程当中,这些被模板使用到的数据能被正常的访问到,在访问的阶段中,这些被访问到的数据即被加入到 mpx 构建的整个响应式的系统当中。

只要在 template 当中使用到的 data 数据(包括衍生的 computed 数据),最终都会被 renderData 所记录,而记录的数据形式是例如:

renderData['xxx'] = [this.xxx, 'xxx'] // 数组的形式,第一项为这个数据实际的值,第二项为这个数据的 firstKey(主要用以数据 diff 的工作)
-

以上就是 mpx 生成 Render Function 的整个过程。总结下 Render Function 所做的工作:

  1. 执行 render 函数,将渲染模板使用到的数据加入到响应式的系统当中;
  2. 返回 renderData 用以接下来的数据 diff 以及调用小程序的 setData 方法来完成视图的更新

# Wxs Module

Wxs 是小程序自己推出的一套脚本语言。官方文档 (opens new window)给出的示例,wxs 模块必须要声明式的被 wxml 引用。和 js 在 jsCore 当中去运行不同的是 wxs 是在渲染线程当中去运行的。因此 wxs 的执行便少了一次从 jsCore 执行的线程和渲染线程的通讯,从这个角度来说是对代码执行效率和性能上的比较大的一个优化手段。

有关官方提到的有关 wxs 的运行效率的问题还有待论证:

“在 android 设备中,小程序里的 wxs 与 js 运行效率无差异,而在 ios 设备中,小程序里的 wxs 会比 js 快 2~20倍。”

因为 mpx 是对小程序做渐进增强,因此 wxs 的使用方式和原生的小程序保持一致。在你的.mpx文件当中的 template block 内通过路径直接去引入 wxs 模块即可使用:

<template>
-  <wxs src="../wxs/components/list.wxs" module="list">
-  <view>{{ list.FOO }}</view>
-</template>
-
// wxs/components/list.wxs
-const Foo = 'This is from list wxs module'
-module.exports = {
-  Foo
-}
-

在 template 模块经过 template-compiler 处理的过程中。模板编译器 compiler 在解析模板的 AST 过程中会针对 wxs 标签缓存一份 wxs 模块的映射表:

{
-  meta: {
-    wxsModuleMap: {
-      list: '../wxs/components/list.wxs'
-    }
-  }
-}
-

当 compiler 对 template 模板解析完后,template-compiler 接下来就开始处理 wxs 模块相关的内容:

// template-compiler/index.js
-
-module.exports = function (raw) {
-  ...
-
-  const addDependency = dep => {
-    const resourceIdent = dep.getResourceIdentifier()
-    if (resourceIdent) {
-      const factory = compilation.dependencyFactories.get(dep.constructor)
-      if (factory === undefined) {
-        throw new Error(`No module factory available for dependency type: ${dep.constructor.name}`)
-      }
-      let innerMap = dependencies.get(factory)
-      if (innerMap === undefined) {
-        dependencies.set(factory, (innerMap = new Map()))
-      }
-      let list = innerMap.get(resourceIdent)
-      if (list === undefined) innerMap.set(resourceIdent, (list = []))
-      list.push(dep)
-    }
-  }
-
-  // 如果有 wxsModuleMap 即为 wxs module 依赖的话,那么下面会调用 compilation.addModuleDependencies 方法
-  // 将 wxsModule 作为 issuer 的依赖再次进行编译,最终也会被打包进输出的模块代码当中
-  // 需要注意的就是 wxs module 不仅要被注入到 bundle 里的 render 函数当中,同时也会通过 wxs-loader 处理,单独输出一份可运行的 wxs js 文件供 wxml 引入使用
-  for (let module in meta.wxsModuleMap) {
-    isSync = false
-    let src = meta.wxsModuleMap[module]
-    const expression = `require(${JSON.stringify(src)})`
-    const deps = []
-    // parser 为 js 的编译器
-    parser.parse(expression, {
-      current: { // 需要注意的是这里需要部署 addDependency 接口,因为通过 parse.parse 对代码进行编译的时候,会调用这个接口来获取 require(${JSON.stringify(src)}) 编译产生的依赖模块
-        addDependency: dep => {
-          dep.userRequest = module
-          deps.push(dep)
-        }
-      },
-      module: issuer
-    })
-    issuer.addVariable(module, expression, deps) // 给 issuer module 添加 variable 依赖
-    iterationOfArrayCallback(deps, addDependency)
-  }
-
-  // 如果没有 wxs module 的处理,那么 template-compiler 即为同步任务,否则为异步任务
-  if (isSync) {
-    return result
-  } else {
-    const callback = this.async()
-
-    const sortedDependencies = []
-    for (const pair1 of dependencies) {
-      for (const pair2 of pair1[1]) {
-        sortedDependencies.push({
-          factory: pair1[0],
-          dependencies: pair2[1]
-        })
-      }
-    }
-
-    // 调用 compilation.addModuleDependencies 方法,将 wxs module 作为 issuer module 的依赖加入到编译流程中
-    compilation.addModuleDependencies(
-      issuer,
-      sortedDependencies,
-      compilation.bail,
-      null,
-      true,
-      () => {
-        callback(null, result)
-      }
-    )
-  }
-}
-

# template/script/style/json 模块单文件的生成

不同于 Vue 借助 webpack 是将 Vue 单文件最终打包成单独的 js chunk 文件。而小程序的规范是每个页面/组件需要对应的 wxml/js/wxss/json 4个文件。因为 mpx 使用单文件的方式去组织代码,所以在编译环节所需要做的工作之一就是将 mpx 单文件当中不同 block 的内容拆解到对应文件类型当中。在动态入口编译的小节里面我们了解到 mpx 会分析每个 mpx 文件的引用依赖,从而去给这个文件创建一个 entry 依赖(SingleEntryPlugin)并加入到 webpack 的编译流程当中。我们还是继续看下 mpx loader 对于 mpx 单文件初步编译转化后的内容:

/* script */
-export * from "!!babel-loader!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=script&index=0!./list.mpx"
-
-/* styles */
-require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=styles&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/wxss/loader?root=&importLoaders=1&extract=true!../../node_modules/@mpxjs/webpack-plugin/lib/style-compiler/index?{\"id\":\"2271575d\",\"scoped\":false,\"sourceMap\":false,\"transRpx\":{\"mode\":\"only\",\"comment\":\"use rpx\",\"include\":\"/Users/XRene/demo/mpx-demo-source/src\"}}!stylus-loader!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=styles&index=0!./list.mpx")
-
-/* json */
-require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=json&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/json-compiler/index?root=!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=json&index=0!./list.mpx")
-
-/* template */
-require("!!../../node_modules/@mpxjs/webpack-plugin/lib/extractor?type=template&index=0!../../node_modules/@mpxjs/webpack-plugin/lib/wxml/wxml-loader?root=!../../node_modules/@mpxjs/webpack-plugin/lib/template-compiler/index?{\"usingComponents\":[],\"hasScoped\":false,\"isNative\":false,\"moduleId\":\"2271575d\"}!../../node_modules/@mpxjs/webpack-plugin/lib/selector?type=template&index=0!./list.mpx")
-

接下来可以看下 styles/json/template 这3个 block 的处理流程是什么样。

首先来看下 json block 的处理流程:list.mpx -> json-compiler -> extractor。第一个阶段 list.mpx 文件经由 json-compiler 的处理流程在前面的章节已经讲过,主要就是分析依赖增加动态入口的编译过程。当所有的依赖分析完后,调用 json-compiler loader 的异步回调函数:

// lib/json-compiler/index.js
-
-module.exports = function (content) {
-
-  ...
-  const nativeCallback = this.async()
-  ...
-
-  let callbacked = false
-  const callback = (err, processOutput) => {
-    checkEntryDeps(() => {
-      callbacked = true
-      if (err) return nativeCallback(err)
-      let output = `var json = ${JSON.stringify(json, null, 2)};\n`
-      if (processOutput) output = processOutput(output)
-      output += `module.exports = JSON.stringify(json, null, 2);\n`
-      nativeCallback(null, output)
-    })
-  }
-}
-

这里我们可以看到经由 json-compiler 处理后,通过nativeCallback方法传入下一个 loader 的文本内容形如:

var json = {
-  "usingComponents": {
-    "list": "/components/list397512ea/list"
-  }
-}
-
-module.exports = JSON.stringify(json, null, 2)
-

即这段文本内容会传递到下一个 loader 内部进行处理,即 extractor。接下来我们来看下 extractor 里面主要是实现了哪些功能:

// lib/extractor.js
-
-module.exports = function (content) {
-  ...
-  const contentLoader = normalize.lib('content-loader')
-  let request = `!!${contentLoader}?${JSON.stringify(options)}!${this.resource}` // 构建一个新的 resource,且这个 resource 只需要经过 content-loader
-  let resultSource = defaultResultSource
-  const childFilename = 'extractor-filename'
-  const outputOptions = {
-    filename: childFilename
-  }
-  // 创建一个 child compiler
-  const childCompiler = mainCompilation.createChildCompiler(request, outputOptions, [
-    new NodeTemplatePlugin(outputOptions),
-    new LibraryTemplatePlugin(null, 'commonjs2'), // 最终输出的 chunk 内容遵循 commonjs 规范的可执行的模块代码 module.exports = (function(modules) {})([modules])
-    new NodeTargetPlugin(),
-    new SingleEntryPlugin(this.context, request, resourcePath),
-    new LimitChunkCountPlugin({ maxChunks: 1 })
-  ])
-
-  ...
-  childCompiler.hooks.thisCompilation.tap('MpxWebpackPlugin ', (compilation) => {
-    // 创建 loaderContext 时触发的 hook,在这个 hook 触发的时候,将原本从 json-compiler 传递过来的 content 内容挂载至 loaderContext.__mpx__ 属性上面以供接下来的 content -loader 来进行使用
-    compilation.hooks.normalModuleLoader.tap('MpxWebpackPlugin', (loaderContext, module) => {
-      // 传递编译结果,子编译器进入content-loader后直接输出
-      loaderContext.__mpx__ = {
-        content,
-        fileDependencies: this.getDependencies(),
-        contextDependencies: this.getContextDependencies()
-      }
-    })
-  })
-
-  let source
-
-  childCompiler.hooks.afterCompile.tapAsync('MpxWebpackPlugin', (compilation, callback) => {
-    // 这里 afterCompile 产出的 assets 的代码当中是包含 webpack runtime bootstrap 的代码,不过需要注意的是这个 source 模块的产出形式
-    // 因为使用了 new LibraryTemplatePlugin(null, 'commonjs2') 等插件。所以产出的 source 是可以在 node 环境下执行的 module
-    // 因为在 loaderContext 上部署了 exec 方法,即可以直接执行 commonjs 规范的 module 代码,这样就最终完成了 mpx 单文件当中不同模块的抽离工作
-    source = compilation.assets[childFilename] && compilation.assets[childFilename].source()
-
-    // Remove all chunk assets
-    compilation.chunks.forEach((chunk) => {
-      chunk.files.forEach((file) => {
-        delete compilation.assets[file]
-      })
-    })
-
-    callback()
-  })
-
-  childCompiler.runAsChild((err, entries, compilation) => {
-    ...
-    try {
-      // exec 是 loaderContext 上提供的一个方法,在其内部会构建原生的 node.js module,并执行这个 module 的代码
-      // 执行这个 module 代码后获取的内容就是通过 module.exports 导出的内容
-      let text = this.exec(source, request)
-      if (Array.isArray(text)) {
-        text = text.map((item) => {
-          return item[1]
-        }).join('\n')
-      }
-
-      let extracted = extract(text, options.type, resourcePath, +options.index, selfResourcePath)
-      if (extracted) {
-        resultSource = `module.exports = __webpack_public_path__ + ${JSON.stringify(extracted)};`
-      }
-    } catch (err) {
-      return nativeCallback(err)
-    }
-    if (resultSource) {
-      nativeCallback(null, resultSource)
-    } else {
-      nativeCallback()
-    }
-  })
-}
-

稍微总结下上面的处理流程:

  1. 构建一个以当前模块路径及 content-loader 的 resource 路径
  2. 以这个 resource 路径作为入口模块,创建一个 childCompiler
  3. childCompiler 启动后,创建 loaderContext 的过程中,将 content 文本内容挂载至 loaderContext.mpx 上,这样在 content-loader 在处理入口模块的时候仅仅就是取出这个 content 文本内容并返回。实际上这个入口模块经过 loader 的过程不会做任何的处理工作,仅仅是将父 compilation 传入的 content 返回出去。
  4. loader 处理模块的环节结束后,进入到 module.build 阶段,这个阶段对 content 内容没有太多的处理
  5. createAssets 阶段,输出 chunk。
  6. 将输出的 chunk 构建为一个原生的 node.js 模块并执行,获取从这个 chunk 导出的内容。也就是模块通过module.exports导出的内容。

所以上面的示例 demo 最终会输出一个 json 文件,里面包含的内容即为:

{
-  "usingComponents": {
-    "list": "/components/list397512ea/list"
-  }
-}
-

# 运行时环节

以上几个章节主要是分析了几个 Mpx 在编译构建环节所做的工作。接下来我们来看下 Mpx 在运行时环节做了哪些工作。

# 响应式系统

小程序也是通过数据去驱动视图的渲染,需要手动的调用setData去完成这样一个动作。同时小程序的视图层也提供了用户交互的响应事件系统,在 js 代码中可以去注册相关的事件回调并在回调中去更改相关数据的值。Mpx 使用 Mobx 作为响应式数据工具并引入到小程序当中,使得小程序也有一套完成的响应式的系统,让小程序的开发有了更好的体验。

还是从组件的角度开始分析 mpx 的整个响应式的系统。每次通过createComponent方法去创建一个新的组件,这个方法将原生的小程序创造组件的方法Component做了一层代理,例如在 attched 的生命周期钩子函数内部会注入一个 mixin:

// attached 生命周期钩子 mixin
-
-attached() {
-  // 提供代理对象需要的api
-  transformApiForProxy(this, currentInject)
-  // 缓存options
-  this.$rawOptions = rawOptions // 原始的,没有剔除 customKeys 的 options 配置
-  // 创建proxy对象
-  const mpxProxy = new MPXProxy(rawOptions, this) // 将当前实例代理到 MPXProxy 这个代理对象上面去
-  this.$mpxProxy = mpxProxy // 在小程序实例上绑定 $mpxProxy 的实例
-  // 组件监听视图数据更新, attached之后才能拿到properties
-  this.$mpxProxy.created()
-}
-

在这个方法内部首先调用transformApiForProxy方法对组件实例上下文this做一层代理工作,在 context 上下文上去重置小程序的 setData 方法,同时拓展 context 相关的属性内容:

function transformApiForProxy (context, currentInject) {
-  const rawSetData = context.setData.bind(context) // setData 绑定对应的 context 上下文
-  Object.defineProperties(context, {
-    setData: { // 重置 context 的 setData 方法
-      get () {
-        return this.$mpxProxy.setData.bind(this.$mpxProxy)
-      },
-      configurable: true
-    },
-    __getInitialData: {
-      get () {
-        return () => context.data
-      },
-      configurable: false
-    },
-    __render: { // 小程序原生的 setData 方法
-      get () {
-        return rawSetData
-      },
-      configurable: false
-    }
-  })
-  // context 绑定注入的render函数
-  if (currentInject) {
-    if (currentInject.render) { // 编译过程中生成的 render 函数
-      Object.defineProperties(context, {
-        __injectedRender: {
-          get () {
-            return currentInject.render.bind(context)
-          },
-          configurable: false
-        }
-      })
-    }
-    if (currentInject.getRefsData) {
-      Object.defineProperties(context, {
-        __getRefsData: {
-          get () {
-            return currentInject.getRefsData
-          },
-          configurable: false
-        }
-      })
-    }
-  }
-}
-

接下来实例化一个 mpxProxy 实例并挂载至 context 上下文的 $mpxProxy 属性上,并调用 mpxProxy 的 created 方法完成这个代理对象的初始化的工作。在 created 方法内部主要是完成了以下的几个工作:

  1. initApi,在组件实例 this 上挂载$watch,$forceUpdate,$updated,$nextTick等方法,这样在你的业务代码当中即可直接访问实例上部署好的这些方法;
  2. initData
  3. initComputed,将 computed 计算属性字段全部代理至组件实例 this 上;
  4. 通过 Mobx observable 方法将 data 数据转化为响应式的数据;
  5. initWatch,初始化所有的 watcher 实例;
  6. initRender,初始化一个 renderWatcher 实例;

这里我们具体的来看下 initRender 方法内部是如何进行工作的:

export default class MPXProxy {
-  ...
-  initRender() {
-    let renderWatcher
-    let renderExcutedFailed = false
-    if (this.target.__injectedRender) { // webpack 注入的有关这个 page/component 的 renderFunction
-      renderWatcher = watch(this.target, () => {
-        if (renderExcutedFailed) {
-          this.render()
-        } else {
-          try {
-            return this.target.__injectedRender() // 执行 renderFunction,获取渲染所需的响应式数据
-          } catch(e) {
-            ...
-          }
-        }
-      }, {
-        handler: (ret) => {
-          if (!renderExcutedFailed) {
-            this.renderWithData(ret) // 渲染页面
-          }
-        },
-        immediate: true,
-        forceCallback: true
-      })
-    }
-  }
-  ...
-}
-

在 initRender 方法内部非常清楚的看到,首先判断这个 page/component 是否具有 renderFunction,如果有的话那么就直接实例化一个 renderWatcher:

export default class Watcher {
-  constructor (context, expr, callback, options) {
-    this.destroyed = false
-    this.get = () => {
-      return type(expr) === 'String' ? getByPath(context, expr) : expr()
-    }
-    const callbackType = type(callback)
-    if (callbackType === 'Object') {
-      options = callback
-      callback = null
-    } else if (callbackType === 'String') {
-      callback = context[callback]
-    }
-    this.callback = typeof callback === 'function' ? action(callback.bind(context)) : null
-    this.options = options || {}
-    this.id = ++uid
-    // 创建一个新的 reaction
-    this.reaction = new Reaction(`mpx-watcher-${this.id}`, () => {
-      this.update()
-    })
-    // 在调用 getValue 函数的时候,实际上是调用 reaction.track 方法,这个方法内部会自动执行 effect 函数,即执行 this.update() 方法,这样便会出发一次模板当中的 render 函数来完成依赖的收集
-    const value = this.getValue()
-    if (this.options.immediateAsync) { // 放置到一个队列里面去执行
-      queueWatcher(this)
-    } else { // 立即执行 callback
-      this.value = value
-      if (this.options.immediate) {
-        this.callback && this.callback(this.value)
-      }
-    }
-  }
-
-  getValue () {
-    let value
-    this.reaction.track(() => {
-      value = this.get() // 获取注入的 render 函数执行后返回的 renderData 的值,在执行 render 函数的过程中,就会访问响应式数据的值
-      if (this.options.deep) {
-        const valueType = type(value)
-        // 某些情况下,最外层是非isObservable 对象,比如同时观察多个属性时
-        if (!isObservable(value) && (valueType === 'Array' || valueType === 'Object')) {
-          if (valueType === 'Array') {
-            value = value.map(item => toJS(item, false))
-          } else {
-            const newValue = {}
-            Object.keys(value).forEach(key => {
-              newValue[key] = toJS(value[key], false)
-            })
-            value = newValue
-          }
-        } else {
-          value = toJS(value, false)
-        }
-      } else if (isObservableArray(value)) {
-        value.peek()
-      } else if (isObservableObject(value)) {
-        keys(value)
-      }
-    })
-    return value
-  }
-
-  update () {
-    if (this.options.sync) {
-      this.run()
-    } else {
-      queueWatcher(this)
-    }
-  }
-
-  run () {
-    const immediateAsync = !this.hasOwnProperty('value')
-    const oldValue = this.value
-    this.value = this.getValue() // 重新获取新的 renderData 的值
-    if (immediateAsync || this.value !== oldValue || isObject(this.value) || this.options.forceCallback) {
-      if (this.callback) {
-        immediateAsync ? this.callback(this.value) : this.callback(this.value, oldValue)
-      }
-    }
-  }
-
-  destroy () {
-    this.destroyed = true
-    this.reaction.getDisposer()()
-  }
-}
-

Watcher 观察者核心实现的工作流程就是:

  1. 构建一个 Reaction 实例;
  2. 调用 getValue 方法,即 reaction.track,在这个方法内部执行过程中会调用 renderFunction,这样在 renderFunction 方法的执行过程中便会访问到渲染所需要的响应式的数据并完成依赖收集;
  3. 根据 immediateAsync 配置来决定回调是放到下一帧还是立即执行;
  4. 当响应式数据发生变化的时候,执行 reaction 实例当中的回调函数,即this.update()方法来完成页面的重新渲染。

mpx 在构建这个响应式的系统当中,主要有2个大的环节,其一为在构建编译的过程中,将 template 模块转化为 renderFunction,提供了渲染模板时所需响应式数据的访问机制,并将 renderFunction 注入到运行时代码当中,其二就是在运行环节,mpx 通过构建一个小程序实例的代理对象,将小程序实例上的数据访问全部代理至 MPXProxy 实例上,而 MPXProxy 实例即 mpx 基于 Mobx 去构建的一套响应式数据对象,首先将 data 数据转化为响应式数据,其次提供了 computed 计算属性,watch 方法等一系列增强的拓展属性/方法,虽然在你的业务代码当中 page/component 实例 this 都是小程序提供的,但是最终经过代理机制,实际上访问的是 MPXProxy 所提供的增强功能,所以 mpx 也是通过这样一个代理对象去接管了小程序的实例。需要特别指出的是,mpx 将小程序官方提供的 setData 方法同样收敛至内部,这也是响应式系统提供的基础能力,即开发者只需要关注业务开发,而有关小程序渲染运行在 mpx 内部去帮你完成。

# 性能优化

由于小程序的双线程的架构设计,逻辑层和视图层之间需要桥接 native bridge。如果要完成视图层的更新,那么逻辑层需要调用 setData 方法,数据经由 native bridge,再到渲染层,这个工程流程为:

小程序逻辑层调用宿主环境的 setData 方法;

逻辑层执行 JSON.stringify 将待传输数据转换成字符串并拼接到特定的JS脚本,并通过evaluateJavascript 执行脚本将数据传输到渲染层;

渲染层接收到后, WebView JS 线程会对脚本进行编译,得到待更新数据后进入渲染队列等待 WebView 线程空闲时进行页面渲染;

WebView 线程开始执行渲染时,待更新数据会合并到视图层保留的原始 data 数据,并将新数据套用在WXML片段中得到新的虚拟节点树。经过新虚拟节点树与当前节点树的 diff 对比,将差异部分更新到UI视图。同时,将新的节点树替换旧节点树,用于下一次重渲染。

文章来源 (opens new window)

而 setData 作为逻辑层和视图层之间通讯的核心接口,那么对于这个接口的使用遵照一些准则将有助于性能方面的提升。

# 尽可能的减少 setData 传输的数据

Mpx 在这个方面所做的工作之一就是基于数据路径的 diff。这也是官方所推荐的 setData 的方式。每次响应式数据发生了变化,调用 setData 方法的时候确保传递的数据都为 diff 过后的最小数据集,这样来减少 setData 传输的数据。

接下来我们就来看下这个优化手段的具体实现思路,首先还是从一个简单的 demo 来看:

<script>
-import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  data: {
-    obj: {
-      a: {
-        c: 1,
-        d: 2
-      }
-    }
-  }
-  onShow() {
-    setTimeout(() => {
-      this.obj.a = {
-        c: 1,
-        d: 'd'
-      }
-    }, 200)
-  }
-})
-</script>
-

在示例 demo 当中,声明了一个 obj 对象(这个对象里面的内容在模块当中被使用到了)。然后经过 200ms 后,手动修改 obj.a 的值,因为对于 c 字段来说它的值没有发生改变,而 d 字段发生了改变。因此在 setData 方法当中也应该只更新 obj.a.d 的值,即:

this.setData('obj.a.d', 'd')
-

因为 mpx 是整体接管了小程序当中有关调用 setData 方法并驱动视图更新的机制。所以当你在改变某些数据的时候,mpx 会帮你完成数据的 diff 工作,以保证每次调用 setData 方法时,传入的是最小的更新数据集。

这里也简单的分析下 mpx 是如何去实现这样的功能的。在上文的编译构建阶段有分析到 mpx 生成的 Render Function,这个 Render Function 每次执行的时候会返回一个 renderData,而这个 renderData 即用以接下来进行 setData 驱动视图渲染的原始数据。renderData 的数据组织形式是模板当中使用到的数据路径作为 key 键值,对应的值使用一个数组组织,数组第一项为数据的访问路径(可获取到对应渲染数据),第二项为数据路径的第一个键值,例如在 demo 示例当中的 renderData 数据如下:

renderData['obj.a.c'] = [this.obj.a.c, 'obj']
-renderData['obj.a.d'] = [this.obj.a.d, 'obj']
-

当页面第一次渲染,或者是响应式输出发生变化的时候,Render Function 都会被执行一次用以获取最新的 renderData 来进行接下来的页面渲染过程。

// src/core/proxy.js
-
-class MPXProxy {
-  ...
-  renderWithData(rawRenderData) { // rawRenderData 即为 Render Function 执行后获取的初始化 renderData
-    const renderData = preprocessRenderData(rawRenderData) // renderData 数据的预处理
-    if (!this.miniRenderData) { // 最小数据渲染集,页面/组件初次渲染的时候使用 miniRenderData 进行渲染,初次渲染的时候是没有数据需要进行 diff 的
-      this.miniRenderData = {}
-      for (let key in renderData) { // 遍历数据访问路径
-        if (renderData.hasOwnProperty(key)) {
-          let item = renderData[key]
-          let data = item[0]
-          let firstKey = item[1] // 某个字段 path 的第一个 key 值
-          if (this.localKeys.indexOf(firstKey) > -1) {
-            this.miniRenderData[key] = diffAndCloneA(data).clone
-          }
-        }
-      }
-      this.doRender(this.miniRenderData)
-    } else { // 非初次渲染使用 processRenderData 进行数据的处理,主要是需要进行数据的 diff 取值工作,并更新 miniRenderData 的值
-      this.doRender(this.processRenderData(renderData))
-    }
-  }
-
-  processRenderData(renderData) {
-    let result = {}
-    for (let key in renderData) {
-      if (renderData.hasOwnProperty(key)) {
-        let item = renderData[key]
-        let data = item[0]
-        let firstKey = item[1]
-        let { clone, diff } = diffAndCloneA(data, this.miniRenderData[key]) // 开始数据 diff
-        // firstKey 必须是为响应式数据的 key,且这个发生变化的 key 为 forceUpdateKey 或者是在 diff 阶段发现确实出现了 diff 的情况
-        if (this.localKeys.indexOf(firstKey) > -1 && (this.checkInForceUpdateKeys(key) || diff)) {
-          this.miniRenderData[key] = result[key] = clone
-        }
-      }
-    }
-    return result
-  }
-  ...
-}
-
-// src/helper/utils.js
-
-// 如果 renderData 里面即包含对某个 key 的访问,同时还有对这个 key 的子节点访问的话,那么需要剔除这个子节点
-/**
- * process renderData, remove sub node if visit parent node already
- * @param {Object} renderData
- * @return {Object} processedRenderData
- */
-export function preprocessRenderData (renderData) {
-  // method for get key path array
-  const processKeyPathMap = (keyPathMap) => {
-    let keyPath = Object.keys(keyPathMap)
-    return keyPath.filter((keyA) => {
-      return keyPath.every((keyB) => {
-        if (keyA.startsWith(keyB) && keyA !== keyB) {
-          let nextChar = keyA[keyB.length]
-          if (nextChar === '.' || nextChar === '[') {
-            return false
-          }
-        }
-        return true
-      })
-    })
-  }
-
-  const processedRenderData = {}
-  const renderDataFinalKey = processKeyPathMap(renderData) // 获取最终需要被渲染的数据的 key
-  Object.keys(renderData).forEach(item => {
-    if (renderDataFinalKey.indexOf(item) > -1) {
-      processedRenderData[item] = renderData[item]
-    }
-  })
-  return processedRenderData
-}
-

其中在 processRenderData 方法内部调用了 diffAndCloneA 方法去完成数据的 diff 工作。在这个方法内部判断新、旧值是否发生变化,返回的 diff 字段即表示是否发生了变化,clone 为 diffAndCloneA 接受到的第一个数据的深拷贝值。

这里大致的描述下相关流程:

  1. 响应式的数据发生了变化,触发 Render Function 重新执行,获取最新的 renderData;
  2. renderData 的预处理,主要是用以剔除通过路径访问时同时有父、子路径情况下的子路径的 key;
  3. 判断是否存在 miniRenderData 最小数据渲染集,如果没有那么 Mpx 完成 miniRenderData 最小渲染数据集的收集,如果有那么使用处理后的 renderData 和 miniRenderData 进行数据的 diff 工作(diffAndCloneA),并更新最新的 miniRenderData 的值;
  4. 调用 doRender 方法,进入到 setData 阶段

相关参阅文档:

# 尽可能的减少 setData 的调用频次

每次调用 setData 方法都会完成一次从逻辑层 -> native bridge -> 视图层的通讯,并完成页面的更新。因此频繁的调用 setData 方法势必也会造成视图的多次渲染,用户的交互受阻。所以对于 setData 方法另外一个优化角度就是尽可能的减少 setData 的调用频次,将多个同步的 setData 操作合并到一次调用当中。接下来就来看下 mpx 在这方面是如何做优化的。

还是先来看一个简单的 demo:

<script>
-import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  data: {
-    msg: 'hello',
-    obj: {
-      a: {
-        c: 1,
-        d: 2
-      }
-    }
-  }
-  watch: {
-    obj: {
-      handler() {
-        this.msg = 'world'
-      },
-      deep: true
-    }
-  },
-  onShow() {
-    setTimeout(() => {
-      this.obj.a = {
-        c: 1,
-        d: 'd'
-      }
-    }, 200)
-  }
-})
-</script>
-

在示例 demo 当中,msg 和 obj 都作为模板依赖的数据,这个组件开始展示后的 200ms,更新 obj.a 的值,同时 obj 被 watch,当 obj 发生改变后,更新 msg 的值。这里的逻辑处理顺序是:

obj.a 变化 -> 将 renderWatch 加入到执行队列 -> 触发 obj watch -> 将 obj watch 加入到执行队列 -> 将执行队列放到下一帧执行 -> 按照 watch id 从小到大依次执行 watch.run -> setData 方法调用一次(即 renderWatch 回调),统一更新 obj.a 及 msg -> 视图重新渲染
-

接下来就来具体看下这个流程:由于 obj 作为模板渲染的依赖数据,自然会被这个组件的 renderWatch 作为依赖而被收集。当 obj 的值发生变化后,首先触发 reaction 的回调,即 this.update() 方法,如果是个同步的 watch,那么立即调用 this.run() 方法,即 watcher 监听的回调方法,否则就通过 queueWatcher(this) 方法将这个 watcher 加入到执行队列:

// src/core/watcher.js
-export default Watcher {
-  constructor (context, expr, callback, options) {
-    ...
-    this.id = ++uid
-    this.reaction = new Reaction(`mpx-watcher-${this.id}`, () => {
-      this.update()
-    })
-    ...
-  }
-
-  update () {
-    if (this.options.sync) {
-      this.run()
-    } else {
-      queueWatcher(this)
-    }
-  }
-}
-

而在 queueWatcher 方法中,lockTask 维护了一个异步锁,即将 flushQueue 当成微任务统一放到下一帧去执行。所以在 flushQueue 开始执行之前,还会有同步的代码将 watcher 加入到执行队列当中,当 flushQueue 开始执行的时候,依照 watcher.id 升序依次执行,这样去确保 renderWatcher 在执行前,其他所有的 watcher 回调都执行完了,即执行 renderWatcher 的回调的时候获取到的 renderData 都是最新的,然后再去进行 setData 的操作,完成页面的更新。

// src/core/queueWatcher.js
-import { asyncLock } from '../helper/utils'
-const queue = []
-const idsMap = {}
-let flushing = false
-let curIndex = 0
-const lockTask = asyncLock()
-export default function queueWatcher (watcher) {
-  if (!watcher.id && typeof watcher === 'function') {
-    watcher = {
-      id: Infinity,
-      run: watcher
-    }
-  }
-  if (!idsMap[watcher.id] || watcher.id === Infinity) {
-    idsMap[watcher.id] = true
-    if (!flushing) {
-      queue.push(watcher)
-    } else {
-      let i = queue.length - 1
-      while (i > curIndex && watcher.id < queue[i].id) {
-        i--
-      }
-      queue.splice(i + 1, 0, watcher)
-    }
-    lockTask(flushQueue, resetQueue)
-  }
-}
-
-function flushQueue () {
-  flushing = true
-  queue.sort((a, b) => a.id - b.id)
-  for (curIndex = 0; curIndex < queue.length; curIndex++) {
-    const watcher = queue[curIndex]
-    idsMap[watcher.id] = null
-    watcher.destroyed || watcher.run()
-  }
-  resetQueue()
-}
-
-function resetQueue () {
-  flushing = false
-  curIndex = queue.length = 0
-}
-
- - - diff --git a/docs-vuepress/.vuepress/dist/articles/performance.html b/docs-vuepress/.vuepress/dist/articles/performance.html deleted file mode 100644 index 6dc9f5e7a6..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/performance.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - 小程序框架运行时性能大测评 | Mpx框架 - - - - - - - - - -

# 小程序框架运行时性能大测评

作者:董宏平(hiyuki (opens new window)),滴滴出行小程序负责人,mpx框架负责人及核心作者

随着小程序在商业上的巨大成功,小程序开发在国内前端领域越来越受到重视,为了方便广大开发者更好地进行小程序开发,各类小程序框架也层出不穷,呈现出百花齐放的态势。但是到目前为止,业内一直没有出现一份全面、详细、客观、公正的小程序框架测评报告,为小程序开发者在技术选型时提供参考。于是我便筹划推出一系列文章,对业内流行的小程序框架进行一次全方位的、客观公正的测评,本文是系列文章的第一篇——运行时性能篇。

在本文中,我们会对下列框架进行运行时性能测试(排名不分先后):

  • wepy2(https://github.com/Tencent/wepy) @2.0.0-alpha.20
  • uniapp(https://github.com/dcloudio/uni-app) @2.0.0-26120200226001
  • mpx(https://github.com/didi/mpx) @2.5.3
  • chameleon(https://github.com/didi/chameleon) @1.0.5
  • mpvue(https://github.com/Meituan-Dianping/mpvue) @2.0.6
  • kbone(https://github.com/Tencent/kbone) @0.8.3
  • taro next(https://github.com/NervJS/taro) @3.0.0-alpha.5

其中对于kbone和taro next均以vue作为业务框架进行测试。

运行时性能的测试内容包括以下几个维度:

  • 框架运行时体积
  • 页面渲染耗时
  • 页面更新耗时
  • 局部更新耗时
  • setData调用次数
  • setData发送数据大小

框架性能测试demo全部存放于https://github.com/hiyuki/mp-framework-benchmark 中,欢迎广大开发者进行验证纠错及补全;

# 测试方案

为了使测试结果真实有效,我基于常见的业务场景构建了两种测试场景,分别是动态测试场景和静态测试场景。

# 动态测试场景

动态测试中,视图基于数据动态渲染,静态节点较少,视图更新耗时和setData调用情况是该测试场景中的主要测试点。

动态测试demo模拟了实际业务中常见的长列表+多tab场景,该demo中存在两份优惠券列表数据,一份为可用券数据,另一份为不可用券数据,其中同一时刻视图中只会渲染展示其中一份数据,可以在上方的操作区模拟对列表数据的各种操作及视图展示切换(切tab)。

动态测试demo

在动态测试中,我在外部通过函数代理的方式在初始化之前将App、Page和Component构造器进行代理,通过mixin的方式在Page的onLoad和Component的created钩子中注入setData拦截逻辑,对所有页面和组件的setData调用进行监听,并统计小程序的视图更新耗时及setData调用情况。该测试方式能够做到对框架代码的零侵入,能够跟踪到小程序全量的setData行为并进行独立的耗时计算,具有很强的普适性,代码具体实现可以查看https://github.com/hiyuki/mp-framework-benchmark/blob/master/utils/proxy.js

# 静态测试场景

静态测试模拟业务中静态页面的场景,如运营活动和文章等页面,页面内具备大量的静态节点,而没有数据动态渲染,初始ready耗时是该场景下测试的重心。

静态测试demo使用了我去年发表的一篇技术文章的html代码进行小程序适配构建,其中包含大量静态节点及文本内容。

静态测试demo

# 测试流程及数据

以下所有耗时类的测试数据均为微信小程序中真机进行5次测试计算平均值得出,单位均为ms。Ios测试环境为手机型号iPhone 11,系统版本13.3.1,微信版本7.0.12,安卓测试环境为手机型号小米9,系统版本Android10,微信版本7.0.12。

为了使数据展示不过于混乱复杂,文章中所列的数据以Ios的测试结果为主,安卓测试结论与Ios相符,整体耗时比Ios高3~4倍左右,所有的原始测试数据存放在https://github.com/hiyuki/mp-framework-benchmark/blob/master/rawData.csv

由于transform-runtime引入的core-js会对框架的运行时体积和运行耗时带来一定影响,且不是所有的框架都会在编译时开启transform-runtime,为了对齐测试环境,下述测试均在transform-runtime关闭时进行。

# 框架运行时体积

由于不是所有框架都能够使用webpack-bundle-analyzer得到精确的包体积占用,这里我通过将各框架生成的demo项目体积减去native编写的demo项目体积作为框架的运行时体积。

demo总体积(KB) 框架运行时体积(KB)
native 27 0
wepy2 66 39
uniapp 114 87
mpx 78 51
chameleon 136 109
mpvue 103 76
kbone 395 368
taro next 183 156

该项测试的结论为:
-native > wepy2 > mpx > mpvue > uniapp > chameleon > taro next > kbone

结论分析:

  • wepy2和mpx在框架运行时体积上控制得最好;
  • taro next和kbone由于动态渲染的特性,在dist中会生成递归渲染模板/组件,所以占用体积较大。

# 页面渲染耗时(动态测试)

我们使用刷新页面操作触发页面重新加载,对于大部分框架来说,页面渲染耗时是从触发刷新操作到页面执行onReady的耗时,但是对于像kbone和taro next这样的动态渲染框架,页面执行onReady并不代表视图真正渲染完成,为此,我们设定了一个特殊规则,在页面onReady触发的1000ms内,在没有任何操作的情况下出现setData回调时,以最后触发的setData回调作为页面渲染完成时机来计算真实的页面渲染耗时,测试结果如下:

页面渲染耗时
native 60.8
wepy2 64
uniapp 56.4
mpx 52.6
chameleon 56.4
mpvue 117.8
kbone 98.6
taro next 89.6

该项测试的耗时并不等同于真实的渲染耗时,由于小程序自身没有提供performance api,真实渲染耗时无法通过js准确测试得出,不过从得出的数据来看该项数据依然具备一定的参考意义。

该项测试的结论为:
-mpx ≈ chameleon ≈ uniapp ≈ native ≈ wepy2 > taro next ≈ kbone ≈ mpvue

结论分析:

  • 由于mpvue全量在页面进行渲染,kbone和taro next采用了动态渲染技术,页面渲染耗时较长,其余框架并无太大区别。

# 页面更新耗时(无后台数据)

这里后台数据的定义为data中存在但当前页面渲染中未使用到的数据,在这个demo场景下即为不可用券的数据,当前会在不可用券为0的情况下,对可用券列表进行各种操作,并统计更新耗时。

更新耗时的计算方式是从数据操作事件触发开始到对应的setData回调完成的耗时

mpvue中使用了当前时间戳(new Date)作为超时依据对setData进行了超时时间为50ms的节流操作,该方式存在严重问题,当vue内单次渲染同步流程执行耗时超过50ms时,后续组件patch触发的setData会突破这个节流限制,以50ms每次的频率对setData进行高频无效调用。在该性能测试demo中,当优惠券数量超过500时,界面就会完全卡死。为了顺利跑完整个测试流程,我对该问题进行了简单修复,使用setTimeout重写了节流部分,确保在vue单次渲染流程同步执行完毕后才会调用setData发送合并数据,之后mpvue的所有性能测试都是基于这个patch版本来进行的,该patch版本存放在https://github.com/hiyuki/mp-framework-benchmark/blob/master/frameworks/mpvue/runtime/patch/index.js

理论上来讲native的性能在进行优化的前提下一定是所有框架的天花板,但是在日常业务开发中我们可能无法对每一次setData都进行优化,以下性能测试中所有的native数据均采用修改数据后全量发送的形式来实现。

第一项测试我们使用新增可用券(100)操作将可用券数量由0逐级递增到1000:

100 200 300 400 500 600 700 800 900 1000
native 84.6 69.8 71.6 75 77.2 78.8 82.8 93.2 93.4 105.4
wepy2 118.4 168.6 204.6 246.4 288.6 347.8 389.2 434.2 496 539
uniapp 121.2 100 96 98.2 97.8 99.6 104 102.4 109.4 107.6
mpx 110.4 87.2 82.2 83 80.6 79.6 86.6 90.6 89.2 96.4
chameleon 116.8 115.4 117 119.6 122 125.2 133.8 133.2 144.8 145.6
mpvue 112.8 121.2 140 169 198.8 234.2 278.8 318.4 361.4 408.2
kbone 556.4 762.4 991.6 1220.6 1468.8 1689.6 1933.2 2150.4 2389 2620.6
taro next 470 604.6 759.6 902.4 1056.2 1228 1393.4 1536.2 1707.8 1867.2

然后我们按顺序逐项点击删除可用券(all) > 新增可用券(1000) > 更新可用券(1) > 更新可用券(all) > 删除可用券(1)

delete(all) add(1000) update(1) update(all) delete(1)
native 32.8 295.6 92.2 92.2 83
wepy2 56.8 726.4 49.2 535 530.8
uniapp 43.6 584.4 54.8 144.8 131.2
mpx 41.8 489.6 52.6 169.4 165.6
chameleon 39 765.6 95.6 237.8 144.8
mpvue 103.6 669.4 404.4 414.8 433.6
kbone 120.2 4978 2356.4 2419.4 2357
taro next 126.6 3930.6 1607.8 1788.6 2318.2

该项测试中初期我update(all)的逻辑是循环对每个列表项进行更新,形如listData.forEach((item)=>{item.count++}),发现在chameleon框架中执行界面会完全卡死,追踪发现chameleon框架中没有对setData进行异步合并处理,而是在数据变动时直接同步发送,这样在数据量为1000的场景下用该方式进行更新会高频触发1000次setData,导致界面卡死;对此,我在chameleon框架的测试demo中,将update(all)的逻辑调整为深clone产生一份更新后的listData,再将其整体赋值到this.listData当中,以确保该项测试能够正常进行。

该项测试的结论为:
-native > mpx ≈ uniapp > chameleon > mpvue > wepy2 > taro next > kbone

结论分析:

  • mpx和uniapp在框架内部进行了完善的diff优化,随着数据量的增加,两个框架的新增耗时没有显著上升;
  • wepy2会在数据变更时对props数据也进行setData,在该场景下造成了大量的无效性能损耗,导致性能表现不佳;
  • kbone和taro next采用了动态渲染方案,每次新增更新时会发送大量描述dom结构的数据,与此同时动态递归渲染的耗时也远大于常规的静态模板渲染,使得这两个框架在所有的更新场景下耗时都远大于其他框架。

# 页面更新耗时(有后台数据)

刷新页面后我们使用新增不可用券(1000)创建后台数据,观察该操作是否会触发setData并统计耗时

back add(1000)
native 45.2
wepy2 174.6
uniapp 89.4
mpx 0
chameleon 142.6
mpvue 134
kbone 0
taro next 0

mpx进行setData优化时inspired by vue,使用了编译时生成的渲染函数跟踪模板数据依赖,在后台数据变更时不会进行setData调用,而kbone和taro next采用了动态渲染技术模拟了web底层环境,在上层完整地运行了vue框架,也达到了同样的效果。

然后我们执行和上面无后台数据时相同的操作进行耗时统计,首先是递增100:

100 200 300 400 500 600 700 800 900 1000
native 88 69.8 71.2 80.8 79.4 84.4 89.8 93.2 99.6 108
wepy2 121 173.4 213.6 250 298 345.6 383 434.8 476.8 535.6
uniapp 135.4 112.4 110.6 106.4 109.6 107.2 114.4 116 118.8 117.4
mpx 112.6 86.2 84.6 86.8 90 87.2 91.2 88.8 92.4 93.4
chameleon 178.4 178.2 186.4 184.6 192.6 203.8 210 217.6 232.6 236.8
mpvue 139 151 173.4 194 231.4 258.8 303.4 340.4 384.6 429.4
kbone 559.8 746.6 980.6 1226.8 1450.6 1705.4 1927.2 2154.8 2367.8 2617
taro next 482.6 626.2 755 909.6 1085 1233.2 1384 1568.6 1740.6 1883.8

然后按下表操作顺序逐项点击统计

delete(all) add(1000) update(1) update(all) delete(1)
native 43.4 299.8 89.2 89 87.2
wepy2 43.2 762.4 50 533 522.4
uniapp 57.8 589.8 62.6 160.6 154.4
mpx 45.8 490.8 52.8 167 166
chameleon 93.8 837 184.6 318 220.8
mpvue 124.8 696.2 423.4 419 430.6
kbone 121.4 4978.2 2331.2 2448.4 2348
taro next 129.8 3947.2 1610.4 1813.8 2290.2

该项测试的结论为:
-native > mpx > uniapp > chameleon > mpvue > wepy2 > taro next > kbone

结论分析:

  • 具备模板数据跟踪能力的三个框架mpx,kbone和taro next在有后台数据场景下耗时并没有显著增加;
  • wepy2当中的diff精度不足,耗时也没有产生明显变化;
  • 其余框架由于每次更新都会对后台数据进行deep diff,耗时都产生了一定提升。

# 页面更新耗时(大数据量场景)

由于mpvue和taro next的渲染全部在页面中进行,而kbone的渲染方案会额外新增大量的自定义组件,这三个框架都会在优惠券数量达到2000时崩溃白屏,我们排除了这三个框架对其余框架进行大数据量场景下的页面更新耗时测试

首先还是在无后台数据场景下使用新增可用券(1000)将可用券数量递增至5000:

1000 2000 3000 4000 5000
native 332.6 350 412.6 498.2 569.4
wepy2 970.2 1531.4 2015.2 2890.6 3364.2
uniapp 655.2 593.4 655 675.6 718.8
mpx 532.2 496 548.6 564 601.8
chameleon 805.4 839.6 952.8 1086.6 1291.8

然后点击新增不可用券(5000)将后台数据量增加至5000,再测试可用券数量递增至5000的耗时:

back add(5000)
native 117.4
wepy2 511.6
uniapp 285
mpx 0
chameleon 824
1000 2000 3000 4000 5000
native 349.8 348.4 430.4 497 594.8
wepy2 1128 1872 2470.4 3263.4 4075.8
uniapp 715 666.8 709.2 755.6 810.2
mpx 538.8 501.8 562.6 573.6 595.2
chameleon 1509.2 1672.4 1951.8 2232.4 2586.2

该项测试的结论为:
-native > mpx > uniapp > chameleon > wepy2

结论分析:

  • 在大数据量场景下,框架之间基础性能的差异会变得更加明显,mpx和uniapp依然保持了接近原生的良好性能表现,而chameleon和wepy2则产生了比较显著的性能劣化。

# 局部更新耗时

我们在可用券数量为1000的情况下,点击任意一张可用券触发选中状态,以测试局部更新性能

toggleSelect(ms)
native 2
wepy2 2.6
uniapp 2.8
mpx 2.2
chameleon 2
mpvue 289.6
kbone 2440.8
taro next 1975

该项测试的结论为:
-native ≈ chameleon ≈ mpx ≈ wepy2 ≈ uniapp > mpvue > taro next > kbone

结论分析:

  • 可以看出所有使用了原生自定义组件进行组件化实现的框架局部更新耗时都极低,这足以证明小程序原生自定义组件的优秀性和重要性;
  • mpvue由于使用了页面更新,局部更新耗时显著增加;
  • kbone和taro next由于递归动态渲染的性能开销巨大,导致局部更新耗时同样巨大。

# setData调用

我们将proxySetData的count和size选项设置为true,开启setData的次数和体积统计,重新构建后按照以下流程执行系列操作,并统计setData的调用次数和发送数据的体积。

操作流程如下:

  1. 100逐级递增可用券(0->500)
  2. 切换至不可用券
  3. 新增不可用券(1000)
  4. 100逐级递增可用券(500->1000)
  5. 更新可用券(all)
  6. 切换至可用券

操作完成后我们使用getCountgetSize方法获取累积的setData调用次数和数据体积,其中数据体积计算方式为JSON.stringify后按照utf-8编码方式进行体积计算,统计结果为:

count size(KB)
native 14 803
wepy2 3514 1124
mpvue 16 2127
uniapp 14 274
mpx 8 261
chameleon 2515 319
kbone 22 10572
taro next 9 2321

该项测试的结论为:
-mpx > uniapp > native > chameleon > wepy2 > taro next > mpvue > kbone

结论分析:

  • mpx框架成功实现了理论上setData的最优;
  • uniapp由于缺失模板追踪能力紧随其后;
  • chameleon由于组件每次创建时都会进行一次不必要的setData,产生了大量无效setData调用,但是数据的发送本身经过diff,在数据发送量上表现不错;
  • wepy2的组件会在数据更新时调用setData发送已经更新过的props数据,因此也产生了大量无效调用,且diff精度不足,发送的数据量也较大;
  • taro next由于上层完全基于vue,在数据发送次数上控制到了9次,但由于需要发送大量的dom描述信息,数据发送量较大;
  • mpvue由于使用较长的数据路径描述数据对应的组件,也产生了较大的数据发送量;
  • kbone对于setData的调用控制得不是很好,在上层运行vue的情况依然进行了22次数据发送,且发送的数据量巨大,在此流程中达到了惊人的10MB。

# 页面渲染耗时(静态测试)

此处的页面渲染耗时与前面描述的动态测试场景中相同,测试结果如下:

页面渲染耗时
native 70.4
wepy2 86.6
mpvue 115.2
uniapp 69.6
mpx 66.6
chameleon 65
kbone 144.2
taro next 119.8

该项测试的结论为:
-chameleon ≈ mpx ≈ uniapp ≈ native > wepy2 > mpvue ≈ taro next > kbone

结论分析:

  • 除了kbone和taro next采用动态渲染耗时增加,mpvue使用页面模板渲染性能稍差,其余框架的静态页面渲染表现都和原生差不多。

# 结论

综合上述测试数据,我们得到最终的小程序框架运行时性能排名为:
-mpx > uniapp > chameleon > wepy2 > mpvue > taro next > kbone

# 一点私货

虽然kbone和taro next采用了动态渲染技术在性能表现上并不尽如人意,但是我依然认为这是很棒的技术方案。虽然本文从头到位都在进行性能测试和对比,但性能并不是框架的全部,开发效率和高可用性仍然是框架的重心,开发效率相信是所有框架设计的初衷,但是高可用性却在很大程度被忽视。从这个角度来说,kbone和taro next是非常成功的,不同于过去的转译思路,这种从抹平底层渲染环境的做法能够使上层web框架完整运行,在框架可用性上带来非常大的提升,非常适合于运营类简单小程序的迁移和开发。

我主导开发的mpx框架(https://github.com/didi/mpx) 选择了另一条道路解决可用性问题,那就是基于小程序原生语法能力进行增强,这样既能避免转译web框架时带来的不确定性和不稳定性,同时也能带来非常接近于原生的性能表现,对于复杂业务小程序的开发者来说,非常推荐使用。在跨端输出方面,mpx目前能够完善支持业内全部小程序平台和web平台的同构输出,滴滴内部最重要最复杂的小程序——滴滴出行小程序完全基于mpx进行开发,并利用框架提供的跨端能力对微信和支付宝入口进行同步业务迭代,大大提升了业务开发效率。

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/size-control.html b/docs-vuepress/.vuepress/dist/articles/size-control.html deleted file mode 100644 index f1c5d9cae9..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/size-control.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - 滴滴出行小程序体积优化实践 | Mpx框架 - - - - - - - - - -

# 滴滴出行小程序体积优化实践

作者:sky-admin (opens new window)

# 概述

2019年下半年,为了将微信钱包/支付宝九宫格入口的滴滴出行迁移为小程序,团队对小程序进行了大量的功能升级与补全。在整个过程中也遇到并克服了一系列问题和挑战,其中包体积问题尤为突出。接下来全面介绍一下滴滴出行小程序在体积控制方面做的努力与沉淀。

# 背景

微信对小程序包体积的要求是总体积不得超过12M,主包及单个分包体积不得超过2M。支付宝对于小程序包体积的计算方式虽和微信略有区别,不过整体也大同小异。

18年至19年初时,滴滴出行小程序里承载的业务只有网约车,且业务需求较少,在主包内都能够搞定。而在下半年时,为了将微信钱包/支付宝九宫格入口迁移至小程序,小程序开始新增诸如公交/代驾/车服/单车/顺风车等众多业务线,同时网约车的业务需求也要做全面的补齐,业务量和代码量一起爆炸式增长。

滴滴出行包含了丰富多样的出行业务,包含了快车/专车/出租车/豪华车/拼车/单车/代驾/顺风车/公交/车生活等众多业务线。整个滴滴出行小程序的最重要,使用最高频的页面是首页与订单详情页,首页中承载了各个业务线的需求表达,各个业务线的订单详情页则承载了具体的出行订单展示逻辑。此外还有各种功能页面比如个人中心,营销页面,设置,历史行程。

按照滴滴出行的产品逻辑,所有业务线的需求表达逻辑都在首页承载,为了良好的切换体验,在首页采用了单页顶导的方案进行业务线展示。即每个业务线在首页中提供一个需求表达组件,当用户切换顶导业务线后,切换出对应的业务线组件。

在这种设计下,所有的业务线的需求表达逻辑都集中在首页这个单一页面中,导致在业务迭代过程中,承载首页的主包体积迅速增长,很快触碰了小程序平台的单包2M上限,对后续的业务迭代与发展带来巨大阻碍。因此,对于包体积的控制是我们在小程序开发过程中面临的一大难题。

# 体积控制

下面我们将介绍滴滴出行小程序开发迭代过程中,我们对于小程序包体积进行的一系列优化控制实践。

# 基础优化手段

对于小程序来说,基础的包体积优化手段包括:资源压缩/去除代码冗余/资源CDN化/异步加载

在web开发中,webpack提供了大量的代码优化能力,包括依赖分析、模块去重、代码压缩、tree shaking、side effects等,这些能力可以方便地完成资源压缩和去除代码冗余的工作。滴滴出行小程序基于滴滴开源的小程序框架Mpx( https://github.com/didi/mpx )进行开发,Mpx框架的编译构建完全基于webpack,兼容webpack内部生态,天然可以使用上述能力对包体积进行优化。

小程序中支持部分静态资源(如图像视频等)使用CDN地址加载,我们会尽可能将相关的资源压缩后放到CDN上,避免这部分资源对包体积的占用。

小程序场景下无法像web当中通过script标签便捷地进行异步加载,但是小程序平台后期纷纷支持了分包加载的方案来实现该能力,由于分包加载是小程序特有的技术规范,webpack无法直接支持,因此Mpx框架专门针对该技术规范进行了良好的适配支持,关于该能力的应用我们会在后文详细阐述。

除此之外,Mpx框架还针对小程序场景进行了许多包体积优化的适配工作,如尽可能减少框架运行时包体积占用(压缩后占用56Kb),对引用到的页面/组件按需进行打包构建,声明公共样式进行样式复用,分包内公共模块抽取等。

在Mpx框架的这些能力的支持下,基本不需要额外配置就能构建出一个经过初步优化的小程序包。

微信开发者工具选项里也有类似的"上传代码时自动压缩混淆"可勾选,但在开发者工具中上传代码时计算体积是直接计算的当前项目代码的体积,并不会依据压缩后的体积。因此,如果你使用原生小程序进行开发,你的source代码极有可能进行进一步的压缩以节省空间。

# 分析体积

虽然框架已经提供了很多在体积控制方面的优化,但是随着业务迭代我们发现主包体积依然偏大。

在遇到主包体积偏大后,我们需要弄明白,主包里有哪些东西?它们为什么这么大?

使用原生小程序或者其他非基于webpack的框架进行开发的同学遇到这个问题后,可能只能去看硬盘上的文件大小。这样一来,各个模块的大小占比可能并不直观。而我们则可以借助 webpack-bundle-analyzer 这样一个webpack插件去做辅助分析。

比如这是一个使用Mpx框架编写的demo,通过 npm run build --report 就可以看到这样一个界面:

体积分析图

可以看到这个demo工程由 moment / lodash / socket-weapp / core-js 等第三方库组成。各个库的大小,相互依赖关系也能清晰地看出。

对于滴滴出行小程序也是能看到类似的图,能看到整个项目到底是由哪些代码组成。

另外,滴滴出行前端开发一直是采用“源码编译”的,可以让整个项目里公共的依赖可以实现仅有一份,一起共用。简而言之,也有助于减少项目代码体积。相关资料:https://github.com/DDFE/DDFE-blog/issues/23 (opens new window)

要完美发挥源码编译的效果,需要上下游一起建立整套源码编译生态,比如主项目的依赖方在声明公用依赖时,就应该用peerDep或者devDep来声明一些公有依赖,这些共有依赖应该在主项目中统一声明,避免因版本不同装出两份公共依赖,那样反而会增大体积。由于滴滴出行小程序涉及业务线及团队众多,部分团队可能并不知道这个事情,因此代码里实际是可能出现上述劣化场景。而依照分析图,可以容易地发现这种问题,并推动相关团队清除这些重复依赖。

同时,我们依照体积分析图,对其中体积较大的文件重点分析,进行了一轮业务代码梳理和精简,删除了一些无用代码,精简了websocket的消息体描述文件等。

# 配置分包

分包是小程序给出的类似web异步引入的一个方案,把一些初始进入时不需要的页面可以放进分包里,跳转到对应页面时才去下载分包,将这些页面及其附属资源放到分包里可以有效减少主包体积。

Mpx框架早期对分包规范进行了初步支持,资源访问规则保持和微信一致,主要根据资源存放的目录判断应该输出到主包还是分包。有这个能力后,我们把行程页抽到了分包,大概抽出了200多K左右的空间。

有了行程页的成功拆分后,我们开始对所有的非首页代码进行分包操作,比如起终点选择和个人中心。以及部分业务线的接入是通过npm的方式接入,我们也尽可能将这些业务线的所有非首页的代码放到了分包。

这里还有个题外话,得益于mpx早期设计了packages形式的业务组合方案,可以很方便地让业务独立开发,又能及其方便地整合。而后发现微信的分包的json配置设计和packages很像,就在这个基础上支持了微信的分包,用户侧仅需在原来的packages基础上加一个query标记这个分包的名字即可。

拆除各个分包后,整个项目结构大概如图:

分包一期结构图

初阶的分包工作进行完毕后,总计从主包里拆了差不多400K的空间到分包里。

# 分包资源精细化管理

上面提到,Mpx框架初期的分包处理规则是完全按照微信的方式,把在分包路径下的资源收集到分包里。而npm管理的资源因为都在node_modules目录下,不属于任何分包路径,则会被全部收集进主包。

比如之前我们有行程页分包,行程页自有的状态管理store整个都在行程页分包的路径下,就会被收集到行程页分包中。而行程页还用到了封装好的didi-socket库,这个库是公共的npm包,即使它只在行程页分包里被使用,但由于它本身路径是在node_modules下的,那么就会将其收集进主包里。

因为早期的一些设计,行程页的资源和首页是分割开的,都比较独立地存在于各自的路径下,一期的分包处理的大头也主要是行程页,它刚好契合了Mpx初期对分包处理上的特点,因此能较好地收集进行程页分包里。

随着业务迭代,后续大量业务线的接入都是通过npm进行的,就会有大量npm包资源,他们都在node_modules目录下,因此全部会被收集进主包。

所以Mpx框架进行了一系列改造:

  1. 在构建的依赖收集过程中,我们会对收集到的依赖打上标记,记录它是被哪些分包引入的。一旦它只有一个分包引入,它就会被输出到这个分包中。
  2. 我们会根据用户定义的分包配置,自动在 SplitChunksPlugin 中生成各个分包的 cacheGroups ,把分包中的复用模块抽取到分包下的bundle中。
  3. 对于组件和静态资源,如果他们被多个分包所引用且未在主包中引用,为了确保主包体积最优,这些资源将产生多份副本分别输出到对应分包中,而不会占用主包体积。

这样一来,不管分包中引用的资源原本在什么位置,最终输出时都会尽可能将其输出到dist的分包目录下,避免占用主包空间

这个改动完成后项目结构看似和之前一样,但得益于Mpx处理分包资源能力的升级,我们得以将业务线分包中引用的npm资源成功输出到其所在的分包目录下。

# 封面方案

再后来滴滴出行小程序需要替换微信/支付宝里原有的WebApp入口,小程序接入的业务线迅速增加,包体积迅速增长。

这个部分体积增长的主要原因前面提到过,所有的业务线都要接入到主页来展示。这也是由于业务特点决定的,滴滴出行提供了丰富的出行产品线,包括快车/专车/出租车/豪华车/拼车/单车/代驾/顺风行车等产品,用户是可能需要反复切换挑选的。这个过程还要保留起终点车型之类的信息,必须是一个页面内切换组件加一整套非常复杂的大型状态管理才能比较流畅顺滑地实现。而不能像一些电商/信息平台,将不同的功能拆分到不同页面,让用户通过首页的菜单进入子页面再进行操作,首页只承载入口,只有较少的业务逻辑,分包处理起来就会容易很多。

因此各个业务线都要提供首页组件进行接入。这个组件会在首页被用到,所以无论如何也拆不到分包里。最终,整个首页主包部分的体积可以分成两个部分:基础库和业务代码。两者的体积占比大概是公共依赖基础库占1M左右,业务代码占1M左右。

这么庞大的基础库体积主要是由于滴滴出行的业务线及业务团队众多,各方均有一些自己的基础依赖。比如网约车依赖的长链接通信pb数据描述文件,地图会依的大数计算库,顺风车依赖的CML框架运行时、代驾依赖的通信网关库,以及公用的组件库和polyfill等。

所以滴滴出行小程序面对的问题在当时已经无法用纯技术方案在短期内快速解决问题了,于是我们做了一个工程架构调整,可以叫封面页方案,解决了主包问题。

封面方案简单讲,就是做一个带滴滴出行Logo的封面作为启动页面,而页面一旦加载,立刻跳转另一个页面,这个页面真正承载业务,且它被放在分包里。

这个操作的意义在于,主包里就只剩下了所有方都要依赖的基础框架/库等,而业务全被抽离到了分包里。

封面方案结构图

这是封面方案完成后项目的结构图,之前很大块的首页业务逻辑被抽出到首页分包中了。

这样一个挪移的操作的结果是我们可以有2M的主包空间来乘放基础的公共的库,有一个2M左右的分包来乘放前面提到的滴滴出行的集成了各种业务的“大主页”。而当时拆下来差不多有1.2M的主包,800K+的业务主分包。

这个改造最优秀的一点在于,后续的业务迭代产生的体积增长几乎全是在业务主分包里,剩下的1.1M+空间留给业务迭代还是比较充裕的。而主包的体积在理想条件下是可以长久保持不变的,就不会因为业务需求的不断开发反复导致主包体积临近超标,不再需要为主包体积感到焦虑。

当然,可以看到,这个方案本身是没有消减任何体积的,只是把位置变换了一下。除此之外,这个封面页方案其实也存在一些缺陷,比如首屏业务的展示会变慢,因为要加载的内容会变多,不过小程序本身有较好的缓存资源的能力,因此还算可以接受。

相比于因体积问题卡住需求迭代以及产品线的接入,目前这个方案至少能解决有无问题。我们开发团队后续也会持续跟进关注体积问题,看是否会有产品方案变更或者小程序本身给出一些解决方案来进一步优化这个部分。

# 总结

Mpx框架在包体积控制上做了大量工作,对于npm场景下的小程序分包进行了非常完善的支持。

滴滴出行小程序团队在框架支持的基础上,通过梳理业务依赖,充分利用分包,调整交互方案等一系列手段,在不阻碍业务发展的前提下,将庞大复杂的滴滴出行小程序包体积控制在平台限制范围内。

希望本文能给在包体积上遇到问题的小程序开发者们带来一些启发,欢迎留言交流。

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/ts-derivation.html b/docs-vuepress/.vuepress/dist/articles/ts-derivation.html deleted file mode 100644 index 37ba492785..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/ts-derivation.html +++ /dev/null @@ -1,273 +0,0 @@ - - - - - - 使用Typescript新特性Template Literal Types完善链式key的类型推导 | Mpx框架 - - - - - - - - - -

# 使用Typescript新特性Template Literal Types完善链式key的类型推导

作者:anotherso1a (opens new window)

# 前言

Mpx框架中我们采用了类似 Vuex 的数据仓库设计,这使得小程序的数据也可以在框架中得到统一的管理。但由于设计上的原因,这一部分在TS推导的实现上变得异常艰难,其中有一个明显的问题就是链式key的推导:

// 一段 mpx-store 代码实例
-
-createStoreWithThis({
-  // other options ...
-  actions: {
-    someAction() {
-      // 下面这一个dispatch语句如何得到正确的推导?result的类型如何获取?
-      let result = this.dispatch('deeperActions.anotherAction', 1)
-      return result
-    }
-  },
-  deps: {
-    deeperActions: createStoreWithThis({
-      actions: {
-        anotherAction(payload: number) {
-          // do something
-          return 'result' + payload
-        }
-      }
-    }),
-    // other deps ...
-  }
-})
-

我们该如何获取 this.dispatch('deeperActions.anotherAction', 1) 的返回值?如何针对dispatch后续的参数类型做限制?这几乎是不可能解决的问题。

好消息是,在 2020.09.19,typescript团队release了一个beta版本 (opens new window)。这个版本新推出了一个特性:模板字符串类型(Template Literal Types)

这让链式key的推导出现了曙光,不久后,我们开始尝试完善 mpx-store 的推导,此篇文章也是基于我们实践中所做的一些工作总结而来。

# 特性

这里可以直接看官网 (opens new window)例子,可以十分直观的感受到其特性:

type Color = "red" | "blue";
-type Quantity = "one" | "two";
-
-type SeussFish = `${Quantity | Color} fish`;
-//   ^ = type SeussFish = "one fish" | "two fish" | "red fish" | "blue fish"
-

根据例子得知:模板字符串的使用方式几乎和ES6的字符串模板一致,可以做字符串的拼接,当接收字面量联合类型时,会做matrix操作,返回所有可能的拼接后的字面量联合类型。

另一个例子:

type A = '1' | '2'
-type B = 'a' | 'b'
-type C = 'C' | 'D'
-
-type D = `${A}${B}${C}`
-// type D = "1aC" | "1aD" | "1bC" | "1bD" | "2aC" | "2aD" | "2bC" | "2bD"
-

# 可以用来做什么

这个新特性的出现使得在js中常用的链式key取值,调用等,都能够使用ts进行完整的推导。

第一个问题:

function getValueByPath(object, prop) {
-  prop = prop || '';
-  const paths = prop.split('.');
-  let current = object;
-  let result = null;
-  for (let i = 0, j = paths.length; i < j; i++) {
-    const path = paths[i];
-    if (!current) break;
-
-    if (i === j - 1) {
-      result = current[path];
-      break;
-    }
-    current = current[path];
-  }
-  return result;
-}
-
-// 调用例子
-getValueByPath({ a: false }, 'a') // false
-getValueByPath({ a: { b: { c: 1 } } }, 'a.b') // { c: 1 }
-

如何将以上 getValueByPath 函数使用ts改写,使该函数的调用具有完备的类型推断?

第二个问题:

type simpleActions = {
-  actionOne(payload: number): Promise<number>
-  actionTwo(payload: string): Promise<boolean>
-}
-
-declare function dispatch<K extends keyof simpleActions>(type: K): ReturnType<simpleActions[K]>
-
-dispatch('actionOne')
-// function dispatch<"actionOne">(type: "actionOne"): Promise<number>
-

我们很容易为上面的 simpleActions 编写出其对应的 dispatch 函数,并具备完整推导,但针对下面这种深层次的actions,如何为其编写dispatch函数?

const actions = {
-  someAction(payload: number) {
-    return payload
-  },
-  deeperActions: {
-    anotherAction() {
-      // do something
-      return 'result'
-    },
-    otherActions: {
-      finalAction(payload: string) {
-        return !payload
-      }
-    }
-  }
-}
-
-type Actions = typeof actions
-
-// 编写一个 dispatch 方法,使得:
-// let result = dispatch('deeperActions.anotherAction') // 返回类型为: Promise<string>
-// let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // 返回类型为:Promise<boolean>
-

在typescript发布4.1之前,这几乎是不可能完成的任务,但是4.1的发布使我们能够对这一部分的缺失做一些类型补全。

接下来我们围绕上面两个问题分别进行实现。

# getValueByPath使用TS实现

需要使 getValueByPath({ a: { b: { c: 1 } } }, 'a.b') 获得完整的推导,我们首先需要能拿到两个参数的完整类型,这在ts中是十分容易的。第二步则是对两个参数的类型做映射处理,最好使得第一个参数输入完成之后,第二个参数直接能具备完整推导。

# 单层推导

我们可以先尝试着实现对单层对象取值的类型推导:

首先能得到一个大致的函数结构,接受两个参数,返回一个值

declare function getValueByPath(object: Record<any, any>, prop: string): any
-

上面这种写法是拿不到参数类型的,为了能正确拿到参数的类型,我们使用范型来填充参数 object

declare function getValueByPath<T extends Record<any, any>>(object: T, prop: string): any
-

可以直接在编辑器上面尝试一下(我使用的是vscode),很明显可以看到,我们正确的拿到了第一个参数的类型:

xx

能取到参数的类型对象,就能使用关键字 keyof 获取对象的每一个key,接下来我们把候选key值限定给参数prop,然后把对应的返回值结果填充到函数的结果当中,这里我们引入第二个范型 K。尝试调用之后,我们发现结果都符合预期。

declare function getValueByPath<T extends Record<any, any>, K extends keyof T>(object: T, prop: K): T[K]
-
-let a = getValueByPath({a: 1, b: '2'}, 'a') // number
-let b = getValueByPath({a: 1, b: '2'}, 'b') // string
-let c = getValueByPath({a: 1, b: '2'}, 'c') // Argument of type '"c"' is not assignable to parameter of type '"a" | "b"'.ts(2345)
-

# 多层推导

仅仅是单层推导的结构依然是无法满足我们对于 getValueByPath({ a: { b: { c: 1 } } }, 'a.b') 推导的需求:

let a = getValueByPath({ a: { b: { c: 1 } } }, 'a.b') 
-// Argument of type '"a.b"' is not assignable to parameter of type '"a"'.ts(2345)
-

可以看到,第二个候选参数的类型仅有 "a",而我们按正常期望来讲,getValueByPath的第一个参数传入 { a: { b: { c: 1 } } } 之后,第二个候选参数类型应该为: "a" | "a.b" | "a.b.c"

接下来我们使用4.1的新特性,将对象: { a: { b: { c: 1 } } } 与其链式key: "a" | "a.b" | "a.b.c" 一一对应起来。

工欲善其事,必先利其器。我们先封装好两个工具函数:

// 取出对象的所有除了 symbol 以外的key
-type StringKeyof<T> = Exclude<keyof T, symbol>
-
-// 将字符串用 . 进行拼接
-type CombineStringKey<H extends string | number, L extends string | number> = H extends '' ? `${L}` : `${H}.${L}`
-

我们知道,symbol 类型是没办法放在模板字符串中的,所以对key进行拼接之前,需要过滤掉所有 symbol 类型的key。而 StringKeyof 函数,就是用来过滤对象中所有不符合模板字符串类型的key的方法。其实际用法与 keyof 相同,只是返回值能用于模板字符串

CombineStringKey 则是对两个key进行拼接,拼接的结果自然就是我们需要的链式key格式。

调用例子:

const symbol1 = Symbol()
-
-type A = {
-  1: string
-  a: string
-  [symbol1]: string
-}
-
-type K = StringKeyof<A> // 1 | 'a'
-
-type B = CombineStringKey<'', 'a'>         // "a"
-type C = CombineStringKey<B, 'b'>          // "a.b"
-type D = CombineStringKey<C, 'c1' | 'c2'>  // "a.b.c1" | "a.b.c2"
-type E = CombineStringKey<D, 'd'>          // "a.b.c1.d" | "a.b.c2.d"
-

我们现在以下面这个对象类型为例,取出该对象所有符合要求的链式key:

type deepObj = {
-  a: number;
-  b: {
-    c: string;
-    d: number;
-    e: {
-      f: number;
-      g: boolean;
-    };
-  };
-}
-

思路:遍历对象的所有key,同时对key对应的值进行判断,如果是一个更深层次的对象,则保留外层的key,对更深层对象递归处理,把后续递归出的key都和上一层中保留的key进行拼接。

type ChainKeys<T, P extends string | number = ''> = {
-  [K in StringKeyof<T>]: T[K] extends Record<any, any> ? ChainKeys<T[K], CombineStringKey<P, K>> : {
-    [_ in CombineStringKey<P, K>]:T[K]
-  }
-}[StringKeyof<T>]
-

我们将 ChainKeys 函数拆开来看,首先是它的参数,接受两个类型参数,第一个是类型T,第二个则是用于拼接的前缀字符串,其值可选。我们使用 in 语法遍历 T 的所有 key 值,也就是 [K in StringKeyof<T>],这里我们使用了前面封装好的方法 StringKeyof 来代替 keyof 关键字。对对应的 key 的值,也就是 T[K],我们使用 extends 进行前置判断,如果 T[K] 是一个复杂类型,我们则将当前的字符串前缀 P 和当前的 K 进行拼接,递归调用 ChainKeys 函数进行处理。如果 T[K]基本类型,我们直接使用 Record 方法组装拼接好的key(CombineStringKey<P, K>)和value(T[K]),为了方便在编辑器里面查看结果,这里将Record<CombineStringKey<P, K>, T[K]> 替换为 { [_ in CombineStringKey<P, K>]:T[K] }。最后将所有结果打平成联合类型,也就是最后的一个取值操作:{...}[StringKeyof<T>],这和 [1,2,3][number] // 3 | 1 | 2 的处理方式一致。

接下来我们在编辑器中查看一下调用结果:

img

如红框中所示,发现链式key已经和其类型一一对应起来了。

但是这还有一些问题:

一、"b" 以及 "b.e" 没有出现在枚举当中。

二、返回结果是一个联合类型,如何把这个结果运用到函数当中呢?

第一个问题其实很好解决,当我们在递归处理的同时,将当前的链式key与对应值也进行处理就可以,即:

type ChainKeys<T, P extends string | number = ''> = {
-  [K in StringKeyof<T>]: T[K] extends Record<any, any>
-    ? Record<CombineStringKey<P, K>, T[K]> & ChainKeys<T[K], CombineStringKey<P, K>> 
-    : {
-      [_ in CombineStringKey<P, K>]:T[K]
-    }
-}[StringKeyof<T>]
-

同样为了方便编辑器中查看结果我们把 Record<CombineStringKey<P, K>, T[K]> 替换成 { [_ in CombineStringKey<P, K>]:T[K] }

img

可以看到 b 以及 "b.e" 这样的key都被取出来并放到了结果当中。其实这里还是有一些问题,如果你实际去代码编辑器里面查看结果的话,会发现有很多重复的 b ,这是因为后续使用交叉类型进行拼接的 ChainKeys 的返回值是一个联合类型,如此一来,b 会被分配到联合类型的每一项上,在后面我们会解决这个问题。

第二个问题是结果为联合类型,我们如何才能将其运用至函数当中呢?

大家都知道,对联合类型使用 keyof 操作,返回的类型为 never,即无法取出联合类型的所有key值:

type T = { a: number } | { b: string }
-type K = keyof T // never
-// 如何使K的类型为 a | b 呢?
-

这里我们引入另一个工具函数:UnionToIntersection,顾名思义,该函数的作用就是将联合类型转换为交叉类型,至于为什么它能将联合类型转换为交叉类型,我们后面会讲:

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
-

我们将其运用到ChainKeys的推导当中去:

type ChainKeys<T, P extends string | number = ''> = UnionToIntersection<{
-  [K in StringKeyof<T>]: T[K] extends Record<any, any>
-    ? Record<CombineStringKey<P, K>, T[K]> & ChainKeys<T[K], CombineStringKey<P, K>> 
-    : {
-      [_ in CombineStringKey<P, K>]:T[K]
-    }
-}[StringKeyof<T>]>
-

再经过一些润色得到一个比较精简的函数:

type ChainKeys<T, P extends string | number = ''> = UnionToIntersection<{
-  [K in StringKeyof<T>]: Record<CombineStringKey<P, K>, T[K]> & (T[K] extends Record<any, any> ? ChainKeys<T[K], CombineStringKey<P, K>> : {})
-}[StringKeyof<T>]>
-

尝试调用该方法最终得到的结果为:

img

最终,我们以此为基础,编写 getValueByPath 函数的定义:

declare function getValueByPath<T extends Record<any, any>, K extends keyof ChainKeys<T>>(object: T, prop: K): ChainKeys<T>[K]
-
-let a = getValueByPath({ a: { b: { c: 1 } } }, 'a')     // { b: { c: number } }
-let b = getValueByPath({ a: { b: { c: 1 } } }, 'a.b')   // { c: number }
-let c = getValueByPath({ a: { b: { c: 1 } } }, 'a.b.c') // number
-

到此,我们算是完成了 getValueByPath 函数的类型定义以及推导。

# 深层次Actions的dispatch函数TS实现

其实在推导 getValueByPath 的过程中,我们已经基本上完成了所有实现该 dispatch 所需要的条件,与 getValueByPath 不同的是,我们只需要遍历出函数所对应的链式key即可,改写一下 ChainKeys 方法:

interface DeeperActions {
-  [k: string]: ((...args: any[]) => any) | DeeperActions
-}
-type ActionChainKeys<T, P extends string | number = ''> = UnionToIntersection<{
-  [K in StringKeyof<T>]: T[K] extends DeeperActions
-    ? ActionChainKeys<T[K], CombineStringKey<P, K>>
-    : Record<CombineStringKey<P, K>, T[K]>
-}[StringKeyof<T>]>
-

我们除去了对对象的混入,同时将判断类型改为了 DeeperActions ,以此来判断该 action 对象是否有更深层次的actions。

调用结果:

img

再看我们最开始提出的问题:

编写一个 dispatch 方法,使得:

let result = dispatch('deeperActions.anotherAction') // 返回类型为: Promise<string>

let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // 返回类型为:Promise<boolean>

我们开始定义 dispatch 方法:

declare function dispatch<K extends keyof ChainingActions>(key: K, ...args: Parameters<ChainingActions[K]>): ReturnType<ChainingActions[K]>
-

这里我们分别使用了 ParametersReturnType 函数来获取对应 action 的参数以及返回值,用来填充至 dispatch 方法中,实际编写调用代码:

考虑到我们的 actions 无论如何都会返回一个 Promise,上述定义其实还是稍微有一些问题,但是改动起来也很方便,只需要在 ReturnType 外层使用 Promise 包装一下就可以了:

type Promisify<T> = T extends Promise<any> ? T : Promise<T>
-
-declare function dispatch<K extends keyof ChainingActions>(key: K, ...args: Parameters<ChainingActions[K]>): Promisify<ReturnType<ChainingActions[K]>>
-
-let result = dispatch('deeperActions.anotherAction') // Promise<string>
-let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // Promise<boolean>
-

至此,我们 dispatch 的TS实现也已经完成。

最终结果:

  • 若图片加载失败请访问: http://cdn.qiniu.archerk.com.cn/QQ20210128-162139-HD.gif

# 结尾

其实 getValueByPath 这一部分的内容是有一些缺陷的,因为TS本身限制了递归的次数,当数据层级达到一定程度时,上文的推导会失效。在Mpx的建设中我们还对此做了一些优化,尽可能的减少了递归次数。

这里也给出另一个方案做链式key推导,这种方式性能上会好很多,因为不用解析出所有的链式key,而是根据链式key反向解析前面的对象,因此该方案可以解析出正确的类型,但是会丧失一部分代码提示:

type CutChainKeys<T extends string> = T extends `${infer A}.${infer B}` ? [A, B] : [T, never]
-
-type GetValueByPath<T extends Record<string, any>, K extends string> = CutChainKeys<K> extends [string, never]
-  ? T[CutChainKeys<K>[0]]
-  : GetValueByPath<T[CutChainKeys<K>[0]], CutChainKeys<K>[1]>
-
-declare function test<T extends Record<any, any>, K extends string>(o: T, k: K): GetValueByPath<T, K>
-
-let s = test({
-  a: {
-    b: 1,
-    c: {
-      d: {
-        e: {
-          f: 'f'
-        }
-      }
-    }
-  }
-}, 'a.c.d.e.f') // string
-

通过这一系列的处理,我们基本完成了 mpx-store 的类型推导,包括对应的辅助函数等。这也使得我们能够在项目中更好的使用TS进行开发。在最新的滴滴出行小程序更新中,我们已经开始使用TS全量进行开发,目前看来开发体验还不错,后期也会逐步将旧的代码重构为TS。我们后续也会持续维护和迭代框架,也欢迎有兴趣的朋友来一起共建 (opens new window)

有写的不好的地方请多包含,也欢迎大家批评指正。

# 题外话:UnionToIntersection

上面提到的,我们使用工具函数 UnionToIntersection 将联合类型转换为交叉类型。而 UnionToIntersection 又是如何将联合类型转换为交叉类型的呢?

Conditional Types (opens new window) 中有提到一些点:

# Distributive conditional types

Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

即在执行 extends 语句时,如果 extends 左侧为联合类型,则会被分配成对应数量个子条件判断语句,最终联合每个子语句的结果。

type A = 1 | '2' | 3
-
-type IsNumber<T> = T extends number ? T : never
-
-type B = IsNumber<A> // 1 | 3
-

# Type inference in conditional types

Within the extends clause of a conditional type, it is now possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.

在条件判断语句中,可以使用 infer 关键字做类型推断,同一个候选值能有多个推断位置。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
-

正位推断的结果会被推导为联合类型:

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
-type T10 = Foo<{ a: string; b: string }>; // string
-type T11 = Foo<{ a: string; b: number }>; // string | number
-

逆变位置推断的结果会被处理成交叉类型:

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
-  ? U
-  : never;
-type T20 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; // string
-type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number
-

注意:这里是ts-2.8发布时的文档以及例子,现在来说是不存在 string & number 类型的,如果实际编写这段代码的话会发现 T21 的值为 never,因为不可能存在一个值既为 string 又为 number。逆变与协变的一些概念可以参考一下逆变与协变 (opens new window)

# UnionToIntersection 原理探究

有了以上两个信息之后,我们再来看看 UnionToIntersection 的实现,为了方便阅读我们做一些折行:

type UnionToIntersection<U> = (
-    U extends any 
-      ? (k: U) => void
-      : never
-  ) extends ((k: infer I) => void) 
-    ? I
-    : never;
-

发现实际上 UnionToIntersection 分为两步:

1、将联合类型的每一项填充至 函数类型 (k: U) => void 的参数 k 中。

2、使用 infer 关键字在逆变位(k: U) => void 做推导,将参数的类型推入结果 I 中,组成交叉类型。

拆分看实现:

// 联合类型推入函数参数
-type UnionToFunction<U> = U extends any ? (k: U) => void : never
-// 对函数逆变推导,将参数推导为交叉类型
-type FunctionsToIntersection<F> = [F] extends [(k: infer I) => void] ? I : never
-
-type Union = { a: number } | { b: string }
-
-type Fun = UnionToFunction<Union> // ((k: { a: number }) => void) | ((k: { b: string }) => void)
-
-type Intersection = FunctionsToIntersection<Fun> // { a: number } & { b: string }
-

可以注意到我们拆分开来了 UnionToIntersection 后并不是完全按照原有写法去写的,在 FunctionsToIntersection 函数中,我们使用了[]将类型给包裹起来,这是为了避免第一个特性(Distributive conditional types)的影响,如果不做包裹,则 extends 左侧为联合类型,被分配成若干个子句后,同一位置的 infer 就会被分别执行,无法合成为交叉类型。而使用[]进行包裹之后则避免了这个影响,extends 左侧不再是一个联合类型,而是一个数组类型。

# 参考资料汇总

- - - diff --git a/docs-vuepress/.vuepress/dist/articles/unit-test.html b/docs-vuepress/.vuepress/dist/articles/unit-test.html deleted file mode 100644 index cc828c450b..0000000000 --- a/docs-vuepress/.vuepress/dist/articles/unit-test.html +++ /dev/null @@ -1,461 +0,0 @@ - - - - - - Mpx 小程序单元测试能力建设与实践 | Mpx框架 - - - - - - - - - -

# Mpx 小程序单元测试能力建设与实践

作者:Blackgan3 (opens new window)

# 什么是单元测试

In computer programming, unit testing is a software testing method by which individual units of source code—sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures—are tested to determine whether they are fit for use wikipedia

对一个函数,模块,类进行运行结果正确性检验的工作就是单元测试,此外每个单元测试的对象应该是一个最简单的组件/函数。

那写单测又能给我们哪些收益呢?

  • 大幅提高项目代码可维护性
  • 覆盖率到达一定指标后可大幅提高研发效率
  • 让你的代码零线上bug锐减,上线不再提心吊胆
  • 改进设计,促进重构

此外,单测高覆盖率的项目也会给公司节省大量支出: -unit-test-money.png -据微软统计单测效益结果,如上图展示,绝大多数bug都是在coding阶段产生,并且随着需求开发进度的推进,修复bug的成本会随指数级增长,当我们在unit test阶段发现并修复这个bug是能给公司带来巨大收益的。

下方介绍两种常见的项目单元测试规范:

  • TDD (Test driven development)

测试驱动开发,在书写业务代码之前,先根据需求进行单元功能测试用例书写

这里驱动开发的不是简单的测试用例,是能够持续验证、重构并且对需求功能极致细化的测试用例

  • BDD (Behavior driven development)

行为驱动开发,通过具有辨识力的测试用例驱动项目开发团队所有人,使用自然语言来描述功能,一般和 TDD 相结合,针对行为进行测试,让开发者在写单测时从专注代码实现转为业务行为,能使单测更加场景化和智能化

单元测试的书写初期必定伴随着大量精力与时间的消耗,但长期持续维护的业务在搭建并完善好整个单元测试体系后,可大大提高项目稳定性和研发效率

# 前端单元测试

# 前端单元测试工具

前端单元测试目前有很多框架和工具,我们下方列出三个较为流行的框架和工具库进行介绍

  • Mocha: 功能丰富的 javascript 测试框架(不包括断言和仿真环境,快照测试需额外配置),可以运行在node.js和浏览器中
  • Jasmine:Behavior-Drive development(BDD)风格的测试框架,在业内较为流行,功能很全面,自带asssert、mock功能
  • Jest:一个功能全面的 javascript 测试框架,基于Jasmine 做了大量新特性(例如并行执行、源代码改动感知等),开箱即用,适用于绝大多数 js 项目

# 测试断言库

在单测运行框架中,我们需要断言库来进行方法返回和实例状态的正确性验证

  • should: BDD风格断言 (true).should.be.ok
  • expect: expect()样式断言 expect(true).toBe(true)
  • assert: Node.js 内置断言模块 assert(true === true)
  • chai: expect(),assert()和should风格的断言都支持,全能型选手

在众多前端单元测试框架中,Jest 目前凭借零配置,高性能,且对于断言,快照,覆盖率等都有很好的集成,是目前较为流行的一个单测框架

# Jest 框架简介

简单来看下 Jest 框架的特点以及大致的运行原理

Jest 的整体框架特点大概归纳总结为以下几点:

  • 在操作系统上高效的进行文件搜索以及相互依赖关系匹配
  • 单测并行执行,运行效率高
  • 内置断言库、覆盖率、快照测试等功能,开箱可用
  • 使用 vm 来进行沙盒环境运行,单测之间相互隔离

image.png -打开 Jest pacakges,可以看到大概有50多个包,我们根据这些不同的包来将整个 jest 运行流程整体串起来

第一步 jest-cli 读取相关配置 -当我们执行 jest 命令时,先去执行 jest-cli 中的 run 方法,再调用 jest-core 中的 runCli 方法,其中通过 jest-config 提供的 readConfigs 来读取 Jest 相关配置,返回全局配置(globalConfig)和局部配置(configs)

unit-test-jest-step1.png

第二步 文件静态分析 -使用 jest-haste-map 来进行项目中所有文件的检索以及生成文件之间的相互依赖关系,在 jest-core 中的 _run10000 方法中执行 buildContextsAndHasteMaps,返回 contexts 和 hasteMapInstances,contexts 中的 hasteFs 存储的就是文件及依赖关系。

**jest-haste-map **检索的过程中借助 jest-worker 来根据当前cpu核数并行的进行文件检索,借助 fb-watch-man/crawler 对整体文件变动做实时监听,做到只执行有改动的单元测试文件,实现单测缓存效果。

unit-test-jest-haste.png -下方看一个简单的jest-haste-map使用示例

import JestHasteMap from 'jest-haste-map';
-import {cpus} from 'os';
-
-const hasteMap = new JestHasteMap.default({
-  extensions: ['js'],
-  maxWorkers: cpus().length,
-  name: 'test',
-  platforms: []
-});
-
-const {hasteFS} = await hasteMap.build();
-const testFiles = hasteFS.getAllFiles();
-
-console.log(testFiles);
-// ['/path/to/tests/list1.spec.js', '/path/to/tests/list2.spec.js', …]
-

第三步 单测检索和排序 -经过第一步和第二步,我们拿到了 配置对象 configs,以及文件Map HasteContext,接下来通过 SearchSource 对象检索出所有的单元测试到一个数组中,检索出单元测试文件后,在正式执行之前,我们需要先对当前拿到的所有单测进行权重优先级排序。

单测排序的工作是由 jest Sequencer 完成的,默认排序优先级为 failed (上次失败的先运行)> duration(耗时长的先运行) > size(文件体积大的先运行),当然这里我们也可以自定义customSequencer来覆盖 jest 默认的排序规则,jest 排序规则如下。

unit-test-jest-scquencer.png

		return tests.sort((testA, testB) => {
-      if (failedA !== failedB) {
-        return failedA ? -1 : 1;
-      } else if (hasTimeA != (testB.duration != null)) {
-        // If only one of two tests has timing information, run it last
-        return hasTimeA ? 1 : -1;
-      } else if (testA.duration != null && testB.duration != null) {
-        return testA.duration < testB.duration ? 1 : -1;
-      } else {
-        return fileSize(testA) < fileSize(testB) ? 1 : -1;
-      }
-    });
-

第四步 开始执行 -在经过第三步之后,我们拿到了经过排序后的单测文件,接下来开始进入到执行步骤,执行单测时的整个调度工作是 jest/core 中的 TestScheduler 来完成,例如 scheduler 会推算是串行执行还是并行执行,推算单测执行完的大概时间,覆盖率报告的生成等,scheduler 会调用 jest-runner 中的 runTests 方法去触发单测执行

如果需要并行执行,runTest 方法触发 jest-worker 创建多个child process 子进程来支持 parallel 执行

单元测试中全局方法和全局变量,比如 test() describe() it() 等是由 jest-cirucs(jest-jasmine) 提供并注入global中

unit-test-jest-run-test.png -最终单测的运行是由 jest-runtime 中创建的 vm 虚拟机隔离执行,vm 作用域中dom环境是由 jest-environment-jsdom 提供,此外 jest-runtime 中还包括了 transformer 能力以及mock功能的具体实现等,这部分功能在接下来的Mpx框架单元测试实现章节我们会去详细介绍它。

第五步 处理返回结果 -此外 jest-runner中提供了一套类似于 redux 的数据流机制和eventEmitter来管理维护单测状态以及单测执行结果,在jest-runner 中进行事件触发,在TestScheduler 中进行事件监听并对执行结果进行各种处理和序列化, -最后在 jest-core 中的runJest方法中进行执行结果的终端输出/文件输出等一系列处理。

# 小程序单元测试

# 与 web 应用的不同

上个章节讲完前端单测简介,以及jest单测框架的大概运行原理后,接下来我们看下单元测试在小程序场景下与web场景的不同

首先小程序本身是双线程分离的机制,但目前并没有这种独特的运行环境用来执行单元测试,这里需要借助小程序官方提供的 miniprogram-simulate 工具集,来将整体运行机制调整为单线程模拟运行,并利用 dom 环境来进行小程序组件的注册渲染以及整个自定义组件树的搭建

小程序的单元测试执行依赖 js 运行环境和 dom 环境,这里我们选择 jest 框架来提供对应的环境

下方是一个简单的微信小程序官方提供的单元测试demo,具体关于miniprogram-simulate 的更多API的使用可以去官方文档查看 https://github.com/wechat-miniprogram/miniprogram-simulate (opens new window)

import simulate from 'miniprogram-simulate'
-test('comp', () => {
-    const id = simulate.load(path.join(__dirname, './comp')) // 注册自定义组件,返回组件 id
-    const comp = simulate.render(id) // 使用 id 渲染自定义组件,返回组件封装实例
-
-    const parent = document.createElement('parent-wrapper') // 创建容器节点
-    comp.attach(parent) // 将组件插入到容器节点中,会触发 attached 生命周期
-
-    expect(comp.dom.innerHTML).toBe('<div>123</div>') // 判断组件渲染结果
-    // 执行其他的一些测试逻辑
-
-    comp.detach() // 将组件从容器节点中移除,会触发 detached 生命周期
-})
-

此外对于小程序工具集的整体运行流程,在下方章节进行了简要总结。

# 小程序单测框架整体流程

小程序单元测试中微信官方提供的相关库有 miniprogram-simulate、j-component 和 miniprogram-exparser等

  • miniprogram-simulate: 小程序自定义组件测试工具集,进行小程序内置组件的注册以及模拟微信原生api的注入
  • j-component: 仿小程序组件系统,可以让小程序自定义组件跑在 web 端。
  • miniprogram-exparser:微信小程序官方的组件系统模块,exparser 的组件模型和 WebComponents标准中的 Shadow DOM 高度相似,基于 Shadow DOM 原型,可在纯 JS 环境运行,维护整个组件的节点树相关的信息,包括属性、事件等。
  • miniprogram-compiler: wcc、wcsc 官方编译器的 node 封装版,用于编译 wxml、wxss 文件

开发者在使用的时候经常用的的两个方法就是 simulate.load 和 simulate.render 方法,那这里我们就从这两个方法为入口进行整个流程的总结 -1.miniprogramSimulate.load(path) 小程序-load.png

let nowLoad
-// miniprogram-simulate
-function load(path, definition){
-  // 省略部分判断
-  const id = register(componentPath, tagName, cache, hasRegisterCache)
-  cache.needRunJsList.forEach(item => {
-    // nowLoad 用于执行用户代码调用 Component 构造器时注入额外的参数给 j-component
-    nowLoad = item[1]
-    // require('xxx.js')
-    _.runJs(item[0])
-  })
-	return id
-}
-function register(componentPath, tagName, cache, hasRegisterCache) {
-  // 随机生成,不重复
-  const id = _.getId()
-  const component = {
-    id,
-    path: componentPath,
-    tagName,
-    json: _.readJson(`${componentPath}.json`),
-   	wxml: compile.getWxml(componentPath, cache.options),
-    wxss: wxss.getContent(`${componentPath}.wxss`)
-  }
-  // 存入需要执行的自定义组件 js
-  cache.needRunJsList.push([componentPath, component])
-  return component.id
-}
-global.Component = options => {
-	const component = nowLoad
-  jComponent.register(definition)
-}
-
-function register(definition = {}) {
-    const componentManager = new ComponentManager(definition)
-    return componentManager.id
-}
-
-// ComponentManager 方法
-class ComponentManager {
-    constructor(definition) {
-        // ......
-        this.exparserDef = this.registerToExparser()
-        _.cache(this.id, this)
-    },
-    registerToExparser() {
-    ...
-        const exparserDef = {
-            is: this.id,
-            using,
-            generics: [], // TODO
-            template: {
-                func: this.generateFunc,
-                data: this.data,
-            },
-            properties: definition.properties,
-            data: definition.data,
-            methods: definition.methods,
-            ...
-        }
-        // miniprogram-exparser
-        return exparser.registerElement(exparserDef)
-    }
-}
-
-

2.miniprogramSimulate.render(id)

微信小程序-render.png -miniprogram-simulate中render方法会调用j-component create,根据id从缓存对象中获取componentManager,进行组件实例创建

  /**
-   * 创建组件实例
-   */
-  create(id, properties) {
-    const componentManager = _.cache(id)
-
-    if (!componentManager) return
-
-    return new RootComponent(componentManager, properties)
-  },
-

RootComponent 构造函数中使用之前的 _exparserDef 对象进行真实dom节点创建,生成 _exparserNode

class RootComponent extends Component{
-	constructor(componentManager, properties) {
-  	...
-    this._exparserNode = exparser.createElement(tagName || id, exparserDef) // create exparser node and render
-		...
-    this._bindEvent() // touchstart,touchemove blur 等事件绑定
-  }
-}
-

新生成的 rootComponent 实例继承Component对象,定义了许多我们单测中需要用到的组件方法

class Component {
-	get dom() ...
-  get data() ....
-  get instance ...
-  dispatchEvent ...
-  addEventListener ...
-  removeEventListener ...
-  querySelector ...
-  setData ...
-  triggerLifeTime ...
-  triggerPageLifeTime ...
-  toJSON...
-}
-

当然中间还有很多细节实现,比如模版渲染 j-component/template/compile,组件更新 j-component/render 等,感兴趣的话可以详细去看下里边具体的实现,这里我们暂且按下不表。至此,我们拿到了 component 实例,并可以进行正常的组件状态获取以及更新,然后在Jest框架中去断言组件的各种属性以及方法执行后的预期。

# Mpx 框架单元测试

经过上方 Jest 框架讲解以及小程序单元测试流程分析后,接下来看下在Mpx框架中的单测能力支持实现

# 初期版本

Mpx框架的初期单测架构,是将Mpx框架开发的小程序项目,先构建编译为源码,再使用 miniprogram-simulate + j-component + jest 对构建后的小程序原生代码运行单元测试 -mpx-old-unit-test-architecture.png -该方案执行任何case都需要执行完整的构建流程,而且预构建已经完成了所有的模块收集,无法使用jest提供的模块mock功能,导致业务使用成本很高,落地困难。

# 优化版本

经过调研,Jest 本身支持代码转换功能

Jest在项目中以JavaScript的代码形式运行,但是如果使用一些Node.js不支持的,却可以开箱即用的语法(如JSX,TypeScript中的类型,Vue模板等)那你就需要将代码转换为纯JavaScript,转换的工作就是transformer

这里我们就可以通过Jest提供的转换能力编写mpx-jest转换器,实现在Jest模块加载过程中实时地将当前的Mpx组件编译转换为原生小程序组件,再交由miniprogram-simulate加载运行测试case。

该方案中模块加载完全基于Jest并能实现按需编译,完美规避旧方案中存在的缺陷,缺点在于编译构建流程基于Jest api重构,与Mpx自身基于Webpack的构建流程独立存在,带来额外维护成本,这一问题我们通过将通用的编译转换逻辑抽离出来统一维护,在Webpack和Jest两侧复用,从而解决了改问题。同时在实现这个方案的过程中也做了一部分对应库的改动,将会在下方介绍。

首先来看下 jest-runtime 中 transform 的整体流程。

  • runTest 方法调用 runtime.requireModule(path),传入对应的单测文件地址
  • 判断是否是mock module,如果是则直接走 requireMock方法,否则继续往下进行
  • 定义 localModule
  • 调用 this._loadModule
  • _createRequireImplementation(module, options) 赋值给module.require
  • transformFile 处理对应的文件
  • createScriptFromCode(transformdCode)
  • getVmContext 使用 vm 创建沙盒环境
  • 在沙盒环境执行对应的 jest 单测代码

jest-runtime1.png -其中关键节点的代码如下

			// 自定义 localModule
-			const localModule = {
-        children: [],
-        exports: {},
-        filename: modulePath,
-        id: modulePath,
-        loaded: false,
-        path: path().dirname(modulePath)
-      };
-      // 自定义 module.require
-			Object.defineProperty(module, 'require', {
-        // rquireModuleOrMock || rquireInternalModule
-      	value: this._createRequireImplementation(module, options)
-    	});
-
-			// transformCode
-			const transformedCode = this.transformFile(filename, options);
-			
-			// createScriptFromCode
-			const script = vm.script('({"' + EVAL_RESULT_VARIABLE + `":function(${args.join(',')}){` + transformedCode + '\n}});';)
-			const vmContext = this._environment.getVmContext();
-      runScript = script.runInContext(vmContext, {
-        filename
-      })
-			compiledFunction = runScript[EVAL_RESULT_VARIABLE]
-      compiledFunction.call(
-        module.exports,
-        module, // module object
-        module.exports, // module exports
-        module.require, // require implementation
-        module.path, // __dirname
-        module.filename, // __filename
-        // @ts-expect-error
-        ...lastArgs.filter(notEmpty),
-      );
-

上方是整个 jest-runtime 中对于require module 时transform的整体流程。在Jest的这一能力之上,我们基于 @mpxjs/webpack-plugin 开发了 mpx-jest transformer,实现将 Mpx 单文件组件转换输出为对应的小程序原生代码。 -改良方案01.png -在完成代码转换后,对应的 script 代码做为String存在于内存中,无法直接使用 Jest 环境的 require 加载执行,为此我们参考上述 jest-runtime 中的 loadModule方法实现了requireFromString方法,核心还是使用node vm 模块来进行 script 代码的执行,同时将jsdom-environment 和 Jest global 对象合并做为 vmContext

改造方案2.png -至此,我们的整体单测方案就大致完成,通过 mpx-jest 转换组件,再交由 miniprogram-simulate 来渲染组件实例,从而完成对组件状态的断言,在实际测试的过程中,还遇到了以下问题进行解决。

1.Mpx框架的文件纬度的条件编译支持 -Mpx框架的跨平台输出部分支持对文件进行平台和应用的条件编译引用

// 文件列表
-example.wx.mpx
-example.ali.mpx
-
-// 代码使用
-{
-	example: '../src/example'
-}
-

这里我们需要在Jest运行时环境中提供该功能,借助Jest本身提供的 resolver 自定义能力,让我们可以对请求的文件进行自定义resolve功能,这里我们使用Mpx中现有的resolve plugin 和 enhanced-resolve来自定义resolve方法进行文件条件编译的支持。

const { CachedInputFileSystem, ResolverFactory } = require('enhanced-resolve')
-const AddModePlugin = require('@mpxjs/webpack-plugin/lib/resolver/AddModePlugin')
-
-module.exports = (request, options) => {
-  const addModePlugin = new AddModePlugin('before-file', 'wx', {
-    include: () => true
-  }, 'file')
-  // create a resolver
-  const myResolver = ResolverFactory.createResolver({
-    ...
-    plugins: [addModePlugin]
-  })
-  let result = myResolver.resolveSync({}, options.basedir, request)
-	return result.split('?')[0]
-}
-

2.miniprogram-simulate 定制化方法 -在小程序单元测试的章节中,我们介绍了小程序相关库的运行机制,miniprogram-simulate提供的 load 方法默认只解析渲染原生组件,我们的Mpx组件,mpx-jest 转换器无法和miniprogram-simulate进行关联,所以这里我们选择fork miniprogram-simulate 仓库,自定义了loadMpx和registerMpx方法。

// 使用
-import simulate from '@mpxjs/miniprogram-simulate'
-const id = simulate.loadMpx('src/components/list.mpx')
-
-//@mpxjs/miniprogram-simulate 中 loadMpx 实现简单描述
-function loadMpx(path, tagName, options = {}) {
-	// ...省略一部分校验逻辑
-  // .mpx 结尾文件会经过 mpx-jest 进行转换,输出 wxml,wxss,json,script
-  const componentContent = require(componentPath)
-  id = registerMpx(componentPath, tagName, cache, hasRegisterCache, componentContent)
-  //....
-  return id
-}
-function registerMpx(...){
-  // 部分 require('xx/xx.json') 等修改为直接赋值
-}
-
-

对于组件/页面在存在大量组件引用的情况下,mock引用组件可大大提升单测的运行速度,原有miniprogram-simulate框架并没有提供mock功能,所以这里我们也自定义了mockComponent和clearMockComponent方法。

// 代码在 @mpxjs/miniprogram-simulate 中
-let mockComponentMap = {}
-
-function registerMpx() {
-	// 判断是否是mock的组件
-  if (mockComponentMap[tagName]) {
-    componentPath = mockComponentMap[tagName]
-  }
-}
-/**
- * mock usingComponents中的组件
- * @param compName
- * @param compDefinition
- */
-function mockComponent(compName, compDefinition) {
-  mockComponentMap[compName] = compDefinition
-}
-/**
- * 清除 component mock 数据
- */
-function clearMockComponent() {
-  mockComponentMap = {}
-}
-
-// 单测中使用时
-simulate.mockComponent('list', {
-  template: '<view>component list</view>'
-})
-

3.封装定制test-utils工具集 -书写单测的过程中我们会有很多重复工作,比如创建挂载组件、代理接口、模拟多个组件、断言多个数据等,这里我们将这些共性的方法抽离封装成了 test-utils

/**
- * 创建渲染并挂载自定义组件
- * @param {组件基于所在项目的相对路径,例如'src/subpackage/gulfstream/components/bottom/bottom.mpx'} compPath
- * @returns 用于测试的自定义组件
- */
-export function createCompAndAttach (compPath, renderProps = {}) {
-  const id = simulate.loadMpx(compPath)
-  let comp = simulate.render(id, renderProps)
-  const parent = document.createElement('div')
-  comp.attach(parent)
-  return comp
-}
-
-/**
- * 借助xfetch构造mock请求
- * @param {待模拟url} mockUrl
- * @param {mock数据文件路径} mockFilePath
- */
-export function proxyFetch (mockUrl, mockUrlData) {
-  let mockData
-  mpx.xfetch.fetch = jest.fn( (options) => {
-    return new Promise((resolve) => {
-      if (options.url.includes(mockUrl)) {
-        if (typeof mockUrlData === 'string') {
-          mockData = getMockContent(mockUrlData)
-        } else {
-          mockData = mockUrlData
-        }
-      }
-      resolve({
-        errno: 0,
-        data: mockData
-      })
-    })
-  })
-}
-......
-

至此,Mpx框架的单元测试方案整体上就完备了,整体上的方案架构如下图所示 -Mpx单测架构图.png

# Mpx组件单元测试实战

在上方介绍过整体的jest框架流程以及Mpx框架单元测试架构后,接下来我们着手进行 Mpx 框架开发的小程序组件的单元测试用例书写实战

使用 @mpxjs/cli 创建模版项目时选择使用单元测试,会自动生成有单测能力的模版项目,和普通 Jest + miniprogram-simulate 搭建的原生小程序单测项目不同的是,transform 中添加了 Mpx 文件的处理,这里jest.config.js其他配置就不过多列出,可通过新创建项目进行查看。

	transform: {
-    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
-    '^.+\\.mpx$': '<rootDir>/node_modules/@mpxjs/mpx-jest'
-  }
-

首先我们列出一个组件示例,具体项目可点击链接查看: -https://github.com/Blackgan3/mpx-unit-test-demo (opens new window)

首先对于组件单测的书写,我们希望有一套固定的规范,即所写的不同组件的单测能有一个相同的格式和顺序,这里我们建议的顺序为

  • 组件初始化断言
  • 组件初始化各种形态断言
  • 组件变化- data|computed|watch变化断言
  • 组件变化- 事件触发断言
  • 组件销毁断言

根据上方组件功能,首先我们建议对组件usingComponents 引入的组件进行模拟注册,这样可以节省组件渲染挂载的时间

	beforeEach(() => {
-    // 进行usingComponents 组件 mock
-    testUtils.mockComponents([
-      'list'
-    ])
-  })
-

1.我们首先需要对组件初始化成功后进行各种组件形态预期,即组件初始化断言

	it('test component init correctly', function () {
-    const comp = testUtils.createCompAndAttach(compPath)
-    const insData = comp.instance.data
-    testUtils.checkExpectedData(insData, {
-      someClassShowOne: false,
-      someClassShowTwo: false,
-      someClassShowTwoFlag: false,
-      listData2: ["手机", "电视", "电脑"],
-      key: 1,
-      compStatus: 1,
-      timeDeferFlag: false
-    })
-    const domHTML = comp.dom.innerHTML
-    // 进行组件初始化dom快照测试
-    expect(domHTML).toMatchSnapshot()
-  })
-

上方我们使用工具方法创建并挂载生成组件实例,然后对组件data中key值和value进行预期断言,最后对组件初始化生成的HTML进行快照测试

2.组件初始化各种形态断言 -当组件的初始化数据源有多种形态,比如你的组件初始化数据是由接口或者其他重要的props传递过来决定的,那这里我们可以对不同的数据源组件的不同表现进行断言

	it ('test comp different data', async function () {
-    // 进行唯一数据源请求的接口代理
-    proxyFetch('api/somePackage/getCompData', {
-      status: 1
-    })
-    const comp = testUtils.createCompAndAttach(compPath)
-    // 目前 status 为 1,再次改变数据源代理
-    proxyFetch('api/somePackage/getCompData', {
-      status: 2
-    })
-    await comp.instance.fetchCompData()
-    await comp.instance.$nextTick()
-    expect(comp.instance.data.compStatus).toBe(2)
-    expect(comp.instance.data.compData).toEqual({status: 2})
-    expect(comp.dom.innerHTML).toMatchSnapshot()
-		// 再次改变数据源代理,修改源数据
-    proxyFetch('api/somePackage/getCompData', {
-      status: 3
-    })
-    await comp.instance.fetchCompData()
-    await comp.instance.$nextTick()
-    expect(comp.instance.data.compStatus).toBe(3)
-    expect(comp.instance.data.compData).toEqual({status: 3})
-    expect(comp.dom.innerHTML).toMatchSnapshot()
-  })
-

3.组件变化- props|data|computed|watch变化断言 -接下来我们需要对组件自身的 props|data|computed|watch 等属性变化时所触发的组件相应变化做出各种预期。

	// 组件 props 改变后组件的各种形态预期
-	it('test props different values correspond to different performance', function () {
-    // 传入初始渲染 props
-    const successContent = 'some successContent'
-    const comp = testUtils.createCompAndAttach(compPath, {
-      successContent
-    })
-    const childComp = comp.querySelector('.successContent')
-    expect(childComp).toBeDefined()
-    expect(comp.instance.data.successContent).toBe(successContent)
-    // 对最终的HTML进行快照测试
-    expect(childComp.dom.innerHTML).toMatchSnapshot()
-  })
-	// 当组件data中的someClassShowOne改变之后需要做的哪些预期
-	// 当组件computed中的someCompShow改变之后需要做的预期
-	// ......
-	
-

4.组件必不可少的会有方法,这里我们对示例组件的方法调用进行预期

	it ('test someClassShowTwoFlag tap event trigger', async function () {
-    const comp = testUtils.createComp(compPath)
-    const fetchCompDataSpy = jest.spyOn(comp.instance, 'fetchCompData')
-    const changeSomeClassShowTwoFlagSpy = jest.spyOn(comp.instance, 'changeSomeClassShowTwoFlag')
-    proxyFetch('api/somePackage/getCompData', {
-      status: 1
-    })
-    comp.attach(document.body)
-    const compData = comp.instance.data
-    const changeFlagViewComp = comp.querySelector('.changeFlagClass')
-
-    // dispatchEvent 为异步
-    changeFlagViewComp.dispatchEvent('tap')
-    await testUtils.sleep(0)
-
-    expect(changeSomeClassShowTwoFlagSpy).toBeCalledWith(true)
-    expect(fetchCompDataSpy).toHaveBeenCalledTimes(2)
-    expect(compData.someClassShowTwo).toBeTruthy()
-    expect(comp.instance.someClassShowTwoFlag).toBeTruthy() // 此处注意 非template中使用过到的data,获取更新后的值,从instance中获取
-    expect(comp.dom.innerHTML).toMatchSnapshot()
-  })	
-
-	it('test someTimeDeferAction tap event trigger', async function () {
-    jest.useFakeTimers()
-    jest.spyOn(global, 'setTimeout')
-
-    const comp = testUtils.createCompAndAttach(compPath)
-    const compData = comp.instance.data
-    const someTimeDeferActionSpy = jest.spyOn(comp.instance, 'someTimeDeferAction')
-    const childComp = comp.querySelector('.someTimeDeferActionClass')
-    childComp.dispatchEvent('tap')
-
-    // comp.instance.someTimeDeferAction()
-    await Promise.resolve()
-
-    jest.runAllTimers()
-    await comp.instance.$nextTick()
-
-    expect(compData.timeDeferFlag).toBeTruthy()
-    expect(someTimeDeferActionSpy).toHaveBeenCalledTimes(1)
-    expect(comp.dom.innerHTML).toMatchSnapshot()
-    jest.useRealTimers()
-  })
-

以上,我们对当前的示例组件完成了整体内容的单元测试书写,完整版单测文件可点击链接查看 -https://github.com/Blackgan3/mpx-unit-test-demo/blob/master/test/components/example.spec.js (opens new window)

# 结语

通篇文章我们依次进行了前端常用单测框架简介,jest框架原理总结,小程序单元测试内部执行流程,最后介绍Mpx框架中单测能力的支持实现以及Mpx组件单测实战。

学习到了jest不仅仅是一个单元测试框架,你甚至可以使用它的各个工具库自己创建一个单元测试框架;以及感受到小程序场景下单元测试的差异化;Mpx框架层面也做了诸多改造来支撑单测功能的落地。

后续我们还会继续跟进推动业务中落地TDD规范,复杂逻辑组件中各种功能场景单测用例规范的补充等问题,持续在小程序单测方向深耕并有更好的规范总结产出。

参考文章:

- - - diff --git a/docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css b/docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css deleted file mode 100644 index e0aefebd3c..0000000000 --- a/docs-vuepress/.vuepress/dist/assets/css/0.styles.f22478ca.css +++ /dev/null @@ -1,3 +0,0 @@ -.search-box{display:inline-block;position:relative;margin-right:1rem}.search-box input{cursor:text;width:10rem;height:2rem;color:#4e6e8e;display:inline-block;border:1px solid #cfd4db;border-radius:2rem;font-size:.9rem;line-height:2rem;padding:0 .5rem 0 2rem;outline:none;transition:all .2s ease;background:#fff url(/assets/img/search.83621669.svg) .6rem .5rem no-repeat;background-size:1rem}.search-box input:focus{cursor:auto;border-color:#3eaf7c}.search-box .suggestions{background:#fff;width:20rem;position:absolute;top:2rem;border:1px solid #cfd4db;border-radius:6px;padding:.4rem;list-style-type:none}.search-box .suggestions.align-right{right:0}.search-box .suggestion{line-height:1.4;padding:.4rem .6rem;border-radius:4px;cursor:pointer}.search-box .suggestion a{white-space:normal;color:#5d82a6}.search-box .suggestion a .page-title{font-weight:600}.search-box .suggestion a .header{font-size:.9em;margin-left:.25em}.search-box .suggestion.focused{background-color:#f3f4f5}.search-box .suggestion.focused a{color:#3eaf7c}@media (max-width:959px){.search-box input{cursor:pointer;width:0;border-color:transparent;position:relative}.search-box input:focus{cursor:text;left:0;width:10rem}}@media (-ms-high-contrast:none){.search-box input{height:2rem}}@media (max-width:959px) and (min-width:719px){.search-box .suggestions{left:0}}@media (max-width:719px){.search-box{margin-right:0}.search-box input{left:1rem}.search-box .suggestions{right:0}}@media (max-width:419px){.search-box .suggestions{width:calc(100vw - 4rem)}.search-box input:focus{width:8rem}} - -/*! @docsearch/css 3.8.2 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */:root{--docsearch-primary-color:#5468ff;--docsearch-text-color:#1c1e21;--docsearch-spacing:12px;--docsearch-icon-stroke-width:1.4;--docsearch-highlight-color:var(--docsearch-primary-color);--docsearch-muted-color:#969faf;--docsearch-container-background:rgba(101,108,133,0.8);--docsearch-logo-color:#5468ff;--docsearch-modal-width:560px;--docsearch-modal-height:600px;--docsearch-modal-background:#f5f6f7;--docsearch-modal-shadow:inset 1px 1px 0 0 hsla(0,0%,100%,0.5),0 3px 8px 0 #555a64;--docsearch-searchbox-height:56px;--docsearch-searchbox-background:#ebedf0;--docsearch-searchbox-focus-background:#fff;--docsearch-searchbox-shadow:inset 0 0 0 2px var(--docsearch-primary-color);--docsearch-hit-height:56px;--docsearch-hit-color:#444950;--docsearch-hit-active-color:#fff;--docsearch-hit-background:#fff;--docsearch-hit-shadow:0 1px 3px 0 #d4d9e1;--docsearch-key-gradient:linear-gradient(-225deg,#d5dbe4,#f8f8f8);--docsearch-key-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 2px 1px rgba(30,35,90,0.4);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #cdcde6,inset 0 0 1px 1px #fff,0 1px 1px 0 rgba(30,35,90,0.4);--docsearch-footer-height:44px;--docsearch-footer-background:#fff;--docsearch-footer-shadow:0 -1px 0 0 #e0e3e8,0 -3px 6px 0 rgba(69,98,155,0.12)}html[data-theme=dark]{--docsearch-text-color:#f5f6f7;--docsearch-container-background:rgba(9,10,17,0.8);--docsearch-modal-background:#15172a;--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:#090a11;--docsearch-searchbox-focus-background:#000;--docsearch-hit-color:#bec3c9;--docsearch-hit-shadow:none;--docsearch-hit-background:#090a11;--docsearch-key-gradient:linear-gradient(-26.5deg,#565872,#31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3,4,9,0.3);--docsearch-key-pressed-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 1px 1px 0 rgba(3,4,9,0.30196078431372547);--docsearch-footer-background:#1e2136;--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73,76,106,0.5),0 -4px 8px 0 rgba(0,0,0,0.2);--docsearch-logo-color:#fff;--docsearch-muted-color:#7f8497}.DocSearch-Button{align-items:center;background:var(--docsearch-searchbox-background);border:0;border-radius:40px;color:var(--docsearch-muted-color);cursor:pointer;display:flex;font-weight:500;height:36px;justify-content:space-between;margin:0 0 0 16px;padding:0 8px;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Button:active,.DocSearch-Button:focus,.DocSearch-Button:hover{background:var(--docsearch-searchbox-focus-background);box-shadow:var(--docsearch-searchbox-shadow);color:var(--docsearch-text-color);outline:none}.DocSearch-Button-Container{align-items:center;display:flex}.DocSearch-Search-Icon{stroke-width:1.6}.DocSearch-Button .DocSearch-Search-Icon{color:var(--docsearch-text-color)}.DocSearch-Button-Placeholder{font-size:1rem;padding:0 12px 0 6px}.DocSearch-Button-Keys{display:flex;min-width:calc(40px + .8em)}.DocSearch-Button-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:3px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 2px;position:relative;top:-1px;width:20px}.DocSearch-Button-Key--pressed{box-shadow:var(--docsearch-key-pressed-shadow);transform:translate3d(0,1px,0)}@media (max-width:768px){.DocSearch-Button-Keys,.DocSearch-Button-Placeholder{display:none}}.DocSearch--active{overflow:hidden!important}.DocSearch-Container,.DocSearch-Container *{box-sizing:border-box}.DocSearch-Container{background-color:var(--docsearch-container-background);height:100vh;left:0;position:fixed;top:0;width:100vw;z-index:200}.DocSearch-Container a{text-decoration:none}.DocSearch-Link{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;font:inherit;margin:0;padding:0}.DocSearch-Modal{background:var(--docsearch-modal-background);border-radius:6px;box-shadow:var(--docsearch-modal-shadow);flex-direction:column;margin:60px auto auto;max-width:var(--docsearch-modal-width);position:relative}.DocSearch-SearchBar{display:flex;padding:var(--docsearch-spacing) var(--docsearch-spacing) 0}.DocSearch-Form{align-items:center;background:var(--docsearch-searchbox-focus-background);border-radius:4px;box-shadow:var(--docsearch-searchbox-shadow);display:flex;height:var(--docsearch-searchbox-height);margin:0;padding:0 var(--docsearch-spacing);position:relative;width:100%}.DocSearch-Input{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:var(--docsearch-text-color);flex:1;font:inherit;font-size:1.2em;height:100%;outline:none;padding:0 0 0 8px;width:80%}.DocSearch-Input::-moz-placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::placeholder{color:var(--docsearch-muted-color);opacity:1}.DocSearch-Input::-webkit-search-cancel-button,.DocSearch-Input::-webkit-search-decoration,.DocSearch-Input::-webkit-search-results-button,.DocSearch-Input::-webkit-search-results-decoration{display:none}.DocSearch-LoadingIndicator,.DocSearch-MagnifierLabel,.DocSearch-Reset{margin:0;padding:0}.DocSearch-MagnifierLabel,.DocSearch-Reset{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,.DocSearch-LoadingIndicator{display:none}.DocSearch-Container--Stalled .DocSearch-LoadingIndicator{align-items:center;color:var(--docsearch-highlight-color);display:flex;justify-content:center}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Reset{animation:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;right:0;stroke-width:var(--docsearch-icon-stroke-width)}}.DocSearch-Reset{animation:fade-in .1s ease-in forwards;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:var(--docsearch-icon-color);cursor:pointer;padding:2px;right:0;stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Reset[hidden]{display:none}.DocSearch-Reset:hover{color:var(--docsearch-highlight-color)}.DocSearch-LoadingIndicator svg,.DocSearch-MagnifierLabel svg{height:24px;width:24px}.DocSearch-Cancel{display:none}.DocSearch-Dropdown{max-height:calc(var(--docsearch-modal-height) - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height));min-height:var(--docsearch-spacing);overflow-y:auto;overflow-y:overlay;padding:0 var(--docsearch-spacing);scrollbar-color:var(--docsearch-muted-color) var(--docsearch-modal-background);scrollbar-width:thin}.DocSearch-Dropdown::-webkit-scrollbar{width:12px}.DocSearch-Dropdown::-webkit-scrollbar-track{background:transparent}.DocSearch-Dropdown::-webkit-scrollbar-thumb{background-color:var(--docsearch-muted-color);border:3px solid var(--docsearch-modal-background);border-radius:20px}.DocSearch-Dropdown ul{list-style:none;margin:0;padding:0}.DocSearch-Label{font-size:.75em;line-height:1.6em}.DocSearch-Help,.DocSearch-Label{color:var(--docsearch-muted-color)}.DocSearch-Help{font-size:.9em;margin:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.DocSearch-Title{font-size:1.2em}.DocSearch-Logo a{display:flex}.DocSearch-Logo svg{color:var(--docsearch-logo-color);margin-left:8px}.DocSearch-Hits:last-of-type{margin-bottom:24px}.DocSearch-Hits mark{background:none;color:var(--docsearch-highlight-color)}.DocSearch-HitsFooter{color:var(--docsearch-muted-color);display:flex;font-size:.85em;justify-content:center;margin-bottom:var(--docsearch-spacing);padding:var(--docsearch-spacing)}.DocSearch-HitsFooter a{border-bottom:1px solid;color:inherit}.DocSearch-Hit{border-radius:4px;display:flex;padding-bottom:4px;position:relative}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--deleting{transition:none}}.DocSearch-Hit--deleting{opacity:0;transition:all .25s linear}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit--favoriting{transition:none}}.DocSearch-Hit--favoriting{transform:scale(0);transform-origin:top center;transition:all .25s linear;transition-delay:.25s}.DocSearch-Hit a{background:var(--docsearch-hit-background);border-radius:4px;box-shadow:var(--docsearch-hit-shadow);display:block;padding-left:var(--docsearch-spacing);width:100%}.DocSearch-Hit-source{background:var(--docsearch-modal-background);color:var(--docsearch-highlight-color);font-size:.85em;font-weight:600;line-height:32px;margin:0 -4px;padding:8px 4px 0;position:sticky;top:0;z-index:10}.DocSearch-Hit-Tree{color:var(--docsearch-muted-color);height:var(--docsearch-hit-height);opacity:.5;stroke-width:var(--docsearch-icon-stroke-width);width:24px}.DocSearch-Hit[aria-selected=true] a{background-color:var(--docsearch-highlight-color)}.DocSearch-Hit[aria-selected=true] mark{text-decoration:underline}.DocSearch-Hit-Container{align-items:center;color:var(--docsearch-hit-color);display:flex;flex-direction:row;height:var(--docsearch-hit-height);padding:0 var(--docsearch-spacing) 0 0}.DocSearch-Hit-icon{height:20px;width:20px}.DocSearch-Hit-action,.DocSearch-Hit-icon{color:var(--docsearch-muted-color);stroke-width:var(--docsearch-icon-stroke-width)}.DocSearch-Hit-action{align-items:center;display:flex;height:22px;width:22px}.DocSearch-Hit-action svg{display:block;height:18px;width:18px}.DocSearch-Hit-action+.DocSearch-Hit-action{margin-left:6px}.DocSearch-Hit-action-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:50%;color:inherit;cursor:pointer;padding:2px}svg.DocSearch-Hit-Select-Icon{display:none}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Select-Icon{display:block}.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:background-color .1s ease-in}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{transition:none}}.DocSearch-Hit-action-button:focus path,.DocSearch-Hit-action-button:hover path{fill:#fff}.DocSearch-Hit-content-wrapper{display:flex;flex:1 1 auto;flex-direction:column;font-weight:500;justify-content:center;line-height:1.2em;margin:0 8px;overflow-x:hidden;position:relative;text-overflow:ellipsis;white-space:nowrap;width:80%}.DocSearch-Hit-title{font-size:.9em}.DocSearch-Hit-path{color:var(--docsearch-muted-color);font-size:.75em}.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-action,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-icon,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-path,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-text,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-title,.DocSearch-Hit[aria-selected=true] .DocSearch-Hit-Tree,.DocSearch-Hit[aria-selected=true] mark{color:var(--docsearch-hit-active-color)!important}@media screen and (prefers-reduced-motion:reduce){.DocSearch-Hit-action-button:focus,.DocSearch-Hit-action-button:hover{background:rgba(0,0,0,.2);transition:none}}.DocSearch-ErrorScreen,.DocSearch-NoResults,.DocSearch-StartScreen{font-size:.9em;margin:0 auto;padding:36px 0;text-align:center;width:80%}.DocSearch-Screen-Icon{color:var(--docsearch-muted-color);padding-bottom:12px}.DocSearch-NoResults-Prefill-List{display:inline-block;padding-bottom:24px;text-align:left}.DocSearch-NoResults-Prefill-List ul{display:inline-block;padding:8px 0 0}.DocSearch-NoResults-Prefill-List li{list-style-position:inside;list-style-type:"» "}.DocSearch-Prefill{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;border-radius:1em;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;font-size:1em;font-weight:700;padding:0}.DocSearch-Prefill:focus,.DocSearch-Prefill:hover{outline:none;text-decoration:underline}.DocSearch-Footer{align-items:center;background:var(--docsearch-footer-background);border-radius:0 0 8px 8px;box-shadow:var(--docsearch-footer-shadow);display:flex;flex-direction:row-reverse;flex-shrink:0;height:var(--docsearch-footer-height);justify-content:space-between;padding:0 var(--docsearch-spacing);position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;width:100%;z-index:300}.DocSearch-Commands{color:var(--docsearch-muted-color);display:flex;list-style:none;margin:0;padding:0}.DocSearch-Commands li{align-items:center;display:flex}.DocSearch-Commands li:not(:last-of-type){margin-right:.8em}.DocSearch-Commands-Key{align-items:center;background:var(--docsearch-key-gradient);border:0;border-radius:2px;box-shadow:var(--docsearch-key-shadow);color:var(--docsearch-muted-color);display:flex;height:18px;justify-content:center;margin-right:.4em;padding:0 0 1px;width:20px}.DocSearch-VisuallyHiddenForAccessibility{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}@media (max-width:768px){:root{--docsearch-spacing:10px;--docsearch-footer-height:40px}.DocSearch-Dropdown{height:100%}.DocSearch-Container{height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);position:absolute}.DocSearch-Footer{border-radius:0;bottom:0;position:absolute}.DocSearch-Hit-content-wrapper{display:flex;position:relative;width:80%}.DocSearch-Modal{border-radius:0;box-shadow:none;height:100vh;height:-webkit-fill-available;height:calc(var(--docsearch-vh, 1vh)*100);margin:0;max-width:100%;width:100%}.DocSearch-Dropdown{max-height:calc(var(--docsearch-vh, 1vh)*100 - var(--docsearch-searchbox-height) - var(--docsearch-spacing) - var(--docsearch-footer-height))}.DocSearch-Cancel{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:none;border:0;color:var(--docsearch-highlight-color);cursor:pointer;display:inline-block;flex:none;font:inherit;font-size:1em;font-weight:500;margin-left:var(--docsearch-spacing);outline:none;overflow:hidden;padding:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;white-space:nowrap}.DocSearch-Commands,.DocSearch-Hit-Tree{display:none}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.logo[data-v-3ad594c3]{background-image:url(https://dpubstatic.udache.com/static/dpubimg/imdk1FF2QF/logo_color.png);background-size:135px 35px;background-repeat:no-repeat;font-size:0;width:135px;height:35px;margin-right:50px}.nav[data-v-3ad594c3]{margin-left:50px}.header-menu[data-v-3ad594c3],.row[data-v-3ad594c3]{display:flex;align-items:center}.header-menu[data-v-3ad594c3]{width:100%;justify-content:space-between;z-index:101;position:fixed;left:0;top:0;line-height:60px;padding:0 16px;box-sizing:border-box;background:#f6f6f6}.header-menu-icon[data-v-3ad594c3]{display:inline-block;padding-left:12px}.head-container[data-v-3ad594c3]{width:100%;height:3.5rem;display:flex;align-items:center;line-height:2.2rem;z-index:100;position:fixed;left:0;top:0;-webkit-backdrop-filter:saturate(180%) blur(1rem);backdrop-filter:saturate(180%) blur(1rem);background-color:hsla(0,0%,100%,.8);box-shadow:0 2px 8px #f0f1f2;padding:.5rem 3rem}.nav-link[data-v-3ad594c3]{color:#3a495d;display:flex;align-items:center;width:100%;justify-content:space-between}a.router-link-active[data-v-3ad594c3]{color:#3eaf7c;border-bottom:2px solid #46bd87;margin-bottom:-2px}.banner[data-v-3ad594c3]{position:absolute;right:0;top:0}.searchBox-wrapper[data-v-3ad594c3]{position:absolute;right:150px;z-index:2}.header__line[data-v-3ad594c3]{height:33px;opacity:.1;border:1px solid #3a495d}.header-container[data-v-3ad594c3]{position:absolute;width:100%}.header-nav[data-v-3ad594c3]{position:relative;z-index:5}.head-mask[data-v-3ad594c3]{width:100vw;height:100vh;position:fixed;left:0;top:0;z-index:9}@media (max-width:719px){.head-container[data-v-3ad594c3]{width:100vw;max-height:100vh;background:#fff;align-items:start;padding:16px 0;height:auto;top:60px;transform:translateY(-100%);transition:transform .3s}.row[data-v-3ad594c3]{flex-direction:column;align-items:start;width:100%}.nav[data-v-3ad594c3]{width:100%;height:60px;line-height:60px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;box-sizing:border-box;margin:0}}code[class*=language-],pre[class*=language-]{color:#ccc;background:none;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}.theme-default-content code{color:#476582;padding:.25rem .5rem;margin:0;font-size:.85em;background-color:rgba(27,31,35,.05);border-radius:3px}.theme-default-content code .token.deleted{color:#ec5975}.theme-default-content code .token.inserted{color:#3eaf7c}.theme-default-content pre,.theme-default-content pre[class*=language-]{line-height:1.4;padding:1.25rem 1.5rem;margin:.85rem 0;background-color:#282c34;border-radius:6px;overflow:auto}.theme-default-content pre[class*=language-] code,.theme-default-content pre code{color:#fff;padding:0;background-color:transparent;border-radius:0}div[class*=language-]{position:relative;background-color:#282c34;border-radius:6px}div[class*=language-] .highlight-lines{-webkit-user-select:none;user-select:none;padding-top:1.3rem;position:absolute;top:0;left:0;width:100%;line-height:1.4}div[class*=language-] .highlight-lines .highlighted{background-color:rgba(0,0,0,.66)}div[class*=language-] pre,div[class*=language-] pre[class*=language-]{background:transparent;position:relative;z-index:1}div[class*=language-]:before{position:absolute;z-index:3;top:.8em;right:1em;font-size:.75rem;color:hsla(0,0%,100%,.4)}div[class*=language-]:not(.line-numbers-mode) .line-numbers-wrapper{display:none}div[class*=language-].line-numbers-mode .highlight-lines .highlighted{position:relative}div[class*=language-].line-numbers-mode .highlight-lines .highlighted:before{content:" ";position:absolute;z-index:3;left:0;top:0;display:block;width:3.5rem;height:100%;background-color:rgba(0,0,0,.66)}div[class*=language-].line-numbers-mode pre{padding-left:4.5rem;vertical-align:middle}div[class*=language-].line-numbers-mode .line-numbers-wrapper{position:absolute;top:0;width:3.5rem;text-align:center;color:hsla(0,0%,100%,.3);padding:1.25rem 0;line-height:1.4}div[class*=language-].line-numbers-mode .line-numbers-wrapper br{-webkit-user-select:none;user-select:none}div[class*=language-].line-numbers-mode .line-numbers-wrapper .line-number{position:relative;z-index:4;-webkit-user-select:none;user-select:none;font-size:.85em}div[class*=language-].line-numbers-mode:after{content:"";position:absolute;z-index:2;top:0;left:0;width:3.5rem;height:100%;border-radius:6px 0 0 6px;border-right:1px solid rgba(0,0,0,.66);background-color:#282c34}div[class~=language-js]:before{content:"js"}div[class~=language-ts]:before{content:"ts"}div[class~=language-html]:before{content:"html"}div[class~=language-md]:before{content:"md"}div[class~=language-vue]:before{content:"vue"}div[class~=language-css]:before{content:"css"}div[class~=language-sass]:before{content:"sass"}div[class~=language-scss]:before{content:"scss"}div[class~=language-less]:before{content:"less"}div[class~=language-stylus]:before{content:"stylus"}div[class~=language-go]:before{content:"go"}div[class~=language-java]:before{content:"java"}div[class~=language-c]:before{content:"c"}div[class~=language-sh]:before{content:"sh"}div[class~=language-yaml]:before{content:"yaml"}div[class~=language-py]:before{content:"py"}div[class~=language-docker]:before{content:"docker"}div[class~=language-dockerfile]:before{content:"dockerfile"}div[class~=language-makefile]:before{content:"makefile"}div[class~=language-javascript]:before{content:"js"}div[class~=language-typescript]:before{content:"ts"}div[class~=language-markup]:before{content:"html"}div[class~=language-markdown]:before{content:"md"}div[class~=language-json]:before{content:"json"}div[class~=language-ruby]:before{content:"rb"}div[class~=language-python]:before{content:"py"}div[class~=language-bash]:before{content:"sh"}div[class~=language-php]:before{content:"php"}.custom-block .custom-block-title{font-weight:600;margin-bottom:-.4rem}.custom-block.danger,.custom-block.tip,.custom-block.warning{padding:.1rem 1.5rem;border-left-width:.5rem;border-left-style:solid;margin:1rem 0}.custom-block.tip{background-color:#f3f5f7;border-color:#42b983}.custom-block.warning{background-color:rgba(255,229,100,.3);border-color:#e7c000;color:#6b5900}.custom-block.warning .custom-block-title{color:#b29400}.custom-block.warning a{color:#2c3e50}.custom-block.danger{background-color:#ffe6e6;border-color:#c00;color:#4d0000}.custom-block.danger .custom-block-title{color:#900}.custom-block.danger a{color:#2c3e50}.custom-block.details{display:block;position:relative;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:#eee}.custom-block.details h4{margin-top:0}.custom-block.details figure:last-child,.custom-block.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-block.details summary{outline:none;cursor:pointer}.arrow{display:inline-block;width:0;height:0}.arrow.up{border-bottom:6px solid #ccc}.arrow.down,.arrow.up{border-left:4px solid transparent;border-right:4px solid transparent}.arrow.down{border-top:6px solid #ccc}.arrow.right{border-left:6px solid #ccc}.arrow.left,.arrow.right{border-top:4px solid transparent;border-bottom:4px solid transparent}.arrow.left{border-right:6px solid #ccc}.theme-default-content:not(.custom){max-width:740px;margin:0 auto;padding:2rem 2.5rem}@media (max-width:959px){.theme-default-content:not(.custom){padding:2rem}}@media (max-width:419px){.theme-default-content:not(.custom){padding:1.5rem}}.table-of-contents .badge{vertical-align:middle}body,html{padding:0;margin:0;background-color:#fff}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-size:16px;color:#2c3e50}.page{padding-left:20rem}.navbar{z-index:20;right:0;height:3.6rem;background-color:#fff;box-sizing:border-box;border-bottom:1px solid #eaecef}.navbar,.sidebar-mask{position:fixed;top:0;left:0}.sidebar-mask{z-index:9;width:100vw;height:100vh;display:none}.sidebar{font-size:16px;background-color:#fff;width:20rem;position:fixed;z-index:10;margin:0;top:3.6rem;left:0;bottom:0;box-sizing:border-box;border-right:1px solid #eaecef;overflow-y:auto}.theme-default-content:not(.custom)>:first-child{margin-top:3.6rem}.theme-default-content:not(.custom) a:hover{text-decoration:underline}.theme-default-content:not(.custom) p.demo{padding:1rem 1.5rem;border:1px solid #ddd;border-radius:4px}.theme-default-content:not(.custom) img{max-width:100%}.theme-default-content.custom{padding:0;margin:0}.theme-default-content.custom img{max-width:100%}a{font-weight:500;text-decoration:none}a,p a code{color:#3eaf7c}p a code{font-weight:400}kbd{background:#eee;border:.15rem solid #ddd;border-bottom:.25rem solid #ddd;border-radius:.15rem;padding:0 .15em}blockquote{font-size:1rem;color:#999;border-left:.2rem solid #dfe2e5;margin:1rem 0;padding:.25rem 0 .25rem 1rem}blockquote>p{margin:0}ol,ul{padding-left:1.2em}strong{font-weight:600}h1,h2,h3,h4,h5,h6{font-weight:600;line-height:1.25}.theme-default-content:not(.custom)>h1,.theme-default-content:not(.custom)>h2,.theme-default-content:not(.custom)>h3,.theme-default-content:not(.custom)>h4,.theme-default-content:not(.custom)>h5,.theme-default-content:not(.custom)>h6{margin-top:-3.1rem;padding-top:4.6rem;margin-bottom:0}.theme-default-content:not(.custom)>h1:first-child,.theme-default-content:not(.custom)>h2:first-child,.theme-default-content:not(.custom)>h3:first-child,.theme-default-content:not(.custom)>h4:first-child,.theme-default-content:not(.custom)>h5:first-child,.theme-default-content:not(.custom)>h6:first-child{margin-top:-1.5rem;margin-bottom:1rem}.theme-default-content:not(.custom)>h1:first-child+.custom-block,.theme-default-content:not(.custom)>h1:first-child+p,.theme-default-content:not(.custom)>h1:first-child+pre,.theme-default-content:not(.custom)>h2:first-child+.custom-block,.theme-default-content:not(.custom)>h2:first-child+p,.theme-default-content:not(.custom)>h2:first-child+pre,.theme-default-content:not(.custom)>h3:first-child+.custom-block,.theme-default-content:not(.custom)>h3:first-child+p,.theme-default-content:not(.custom)>h3:first-child+pre,.theme-default-content:not(.custom)>h4:first-child+.custom-block,.theme-default-content:not(.custom)>h4:first-child+p,.theme-default-content:not(.custom)>h4:first-child+pre,.theme-default-content:not(.custom)>h5:first-child+.custom-block,.theme-default-content:not(.custom)>h5:first-child+p,.theme-default-content:not(.custom)>h5:first-child+pre,.theme-default-content:not(.custom)>h6:first-child+.custom-block,.theme-default-content:not(.custom)>h6:first-child+p,.theme-default-content:not(.custom)>h6:first-child+pre{margin-top:2rem}h1:focus .header-anchor,h1:hover .header-anchor,h2:focus .header-anchor,h2:hover .header-anchor,h3:focus .header-anchor,h3:hover .header-anchor,h4:focus .header-anchor,h4:hover .header-anchor,h5:focus .header-anchor,h5:hover .header-anchor,h6:focus .header-anchor,h6:hover .header-anchor{opacity:1}h1{font-size:2.2rem}h2{font-size:1.65rem;padding-bottom:.3rem;border-bottom:1px solid #eaecef}h3{font-size:1.35rem}a.header-anchor{font-size:.85em;float:left;margin-left:-.87em;padding-right:.23em;margin-top:.125em;-webkit-user-select:none;user-select:none;opacity:0}a.header-anchor:focus,a.header-anchor:hover{text-decoration:none}.line-number,code,kbd{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}ol,p,ul{line-height:1.7}hr{border:0;border-top:1px solid #eaecef}table{border-collapse:collapse;margin:1rem 0;display:block;overflow-x:auto}tr{border-top:1px solid #dfe2e5}tr:nth-child(2n){background-color:#f6f8fa}td,th{border:1px solid #dfe2e5;padding:.6em 1em}.theme-container.sidebar-open .sidebar-mask{display:block}.theme-container.no-navbar .theme-default-content:not(.custom)>h1,.theme-container.no-navbar h2,.theme-container.no-navbar h3,.theme-container.no-navbar h4,.theme-container.no-navbar h5,.theme-container.no-navbar h6{margin-top:1.5rem;padding-top:0}.theme-container.no-navbar .sidebar{top:0}@media (min-width:720px){.theme-container.no-sidebar .sidebar{display:none}.theme-container.no-sidebar .page{padding-left:0}}@media (max-width:959px){.sidebar{font-size:15px;width:16.4rem}.page{padding-left:16.4rem}}@media (max-width:719px){.sidebar{top:0;padding-top:3.6rem;transform:translateX(-100%);transition:transform .2s ease}.page{padding-left:0}.theme-container.sidebar-open .sidebar{transform:translateX(0)}.theme-container.no-navbar .sidebar{padding-top:0}}@media (max-width:419px){h1{font-size:1.9rem}.theme-default-content div[class*=language-]{margin:.85rem -1.5rem;border-radius:0}}.navbar .site-name{display:none!important}a{word-break:break-all}:root{--vt-c-white:#fff;--vt-c-white-soft:#f9f9f9;--vt-c-white-mute:#f1f1f1;--vt-c-black:#1a1a1a;--vt-c-black-pure:#000;--vt-c-black-soft:#242424;--vt-c-black-mute:#2f2f2f;--vt-c-indigo:#213547;--vt-c-indigo-soft:#476582;--vt-c-indigo-light:#aac8e4;--vt-c-gray:#8e8e8e;--vt-c-gray-light-1:#aeaeae;--vt-c-gray-light-2:#c7c7c7;--vt-c-gray-light-3:#d1d1d1;--vt-c-gray-light-4:#e5e5e5;--vt-c-gray-light-5:#f2f2f2;--vt-c-gray-dark-1:#636363;--vt-c-gray-dark-2:#484848;--vt-c-gray-dark-3:#3a3a3a;--vt-c-gray-dark-4:#282828;--vt-c-gray-dark-5:#202020;--vt-c-divider-light-1:rgba(60,60,60,0.29);--vt-c-divider-light-2:rgba(60,60,60,0.12);--vt-c-divider-dark-1:rgba(84,84,84,0.65);--vt-c-divider-dark-2:rgba(84,84,84,0.48);--vt-c-text-light-1:var(--vt-c-indigo);--vt-c-text-light-2:rgba(60,60,60,0.7);--vt-c-text-light-3:rgba(60,60,60,0.33);--vt-c-text-light-4:rgba(60,60,60,0.18);--vt-c-text-light-code:var(--vt-c-indigo-soft);--vt-c-text-dark-1:hsla(0,0%,100%,0.87);--vt-c-text-dark-2:hsla(0,0%,92.2%,0.6);--vt-c-text-dark-3:hsla(0,0%,92.2%,0.38);--vt-c-text-dark-4:hsla(0,0%,92.2%,0.18);--vt-c-text-dark-code:var(--vt-c-indigo-light);--vt-c-green:#42b883;--vt-c-green-light:#42d392;--vt-c-green-lighter:#35eb9a;--vt-c-green-dark:#33a06f;--vt-c-green-darker:#155f3e;--vt-c-blue:#3b8eed;--vt-c-blue-light:#549ced;--vt-c-blue-lighter:#50a2ff;--vt-c-blue-dark:#3468a3;--vt-c-blue-darker:#255489;--vt-c-yellow:#ffc517;--vt-c-yellow-light:#ffe417;--vt-c-yellow-lighter:#ffff17;--vt-c-yellow-dark:#e0ad15;--vt-c-yellow-darker:#bc9112;--vt-c-red:#ed3c50;--vt-c-red-light:#f43771;--vt-c-red-lighter:#fd1d7c;--vt-c-red-dark:#cd2d3f;--vt-c-red-darker:#ab2131;--vt-c-purple:#de41e0;--vt-c-purple-light:#e936eb;--vt-c-purple-lighter:#f616f8;--vt-c-purple-dark:#823c83;--vt-c-purple-darker:#602960;--c-brand:#3eaf7c;--c-bg:#fff;--c-bg-light:#f3f4f5;--c-bg-lighter:#eee;--c-text:#2c3e50;--c-text-light:#3a5169;--c-text-quote:#999;--c-border-dark:#dfe2e5;--vt-c-bg:var(--vt-c-white);--vt-c-bg-soft:var(--vt-c-white-soft);--vt-c-bg-mute:var(--vt-c-white-mute);--vt-c-divider:var(--vt-c-divider-light-1);--vt-c-divider-light:var(--vt-c-divider-light-2);--vt-c-divider-inverse:var(--vt-c-divider-dark-1);--vt-c-divider-inverse-light:var(--vt-c-divider-dark-2);--vt-c-text-1:var(--vt-c-text-light-1);--vt-c-text-2:var(--vt-c-text-light-2);--vt-c-text-3:var(--vt-c-text-light-3);--vt-c-text-4:var(--vt-c-text-light-4);--vt-c-text-code:var(--vt-c-text-light-code);--vt-c-text-inverse-1:var(--vt-c-text-dark-1);--vt-c-text-inverse-2:var(--vt-c-text-dark-2);--vt-c-text-inverse-3:var(--vt-c-text-dark-3);--vt-c-text-inverse-4:var(--vt-c-text-dark-4);--vt-c-brand:var(--vt-c-green);--vt-c-brand-light:var(--vt-c-green-light);--vt-c-brand-dark:var(--vt-c-green-dark);--vt-c-brand-highlight:var(--vt-c-brand-dark)}:root .DocSearch{--docsearch-primary-color:var(--c-brand);--docsearch-text-color:var(--c-text);--docsearch-highlight-color:var(--c-brand);--docsearch-muted-color:var(--c-text-quote);--docsearch-container-background:rgba(9,10,17,0.8);--docsearch-modal-background:var(--c-bg-light);--docsearch-searchbox-background:var(--c-bg-lighter);--docsearch-searchbox-focus-background:var(--c-bg);--docsearch-searchbox-shadow:inset 0 0 0 2px var(--c-brand);--docsearch-hit-color:var(--c-text-light);--docsearch-hit-active-color:var(--c-bg);--docsearch-hit-background:var(--c-bg);--docsearch-hit-shadow:0 1px 3px 0 var(--c-border-dark);--docsearch-footer-background:var(--c-bg)}@media (max-width:719px){:root{--c-bg-lighter:none}}#nprogress{pointer-events:none}#nprogress .bar{background:#3eaf7c;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;box-shadow:0 0 10px #3eaf7c,0 0 5px #3eaf7c;opacity:1;transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border-color:#3eaf7c transparent transparent #3eaf7c;border-style:solid;border-width:2px;border-radius:50%;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@keyframes nprogress-spinner{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.go-to-top[data-v-5cf0f4c0]{cursor:pointer;position:fixed;bottom:2rem;right:2.5rem;width:2rem;color:#3eaf7c;z-index:1}.go-to-top[data-v-5cf0f4c0]:hover{color:#72cda4}@media (max-width:959px){.go-to-top[data-v-5cf0f4c0]{display:none}}.fade-enter-active[data-v-5cf0f4c0],.fade-leave-active[data-v-5cf0f4c0]{transition:opacity .3s}.fade-enter[data-v-5cf0f4c0],.fade-leave-to[data-v-5cf0f4c0]{opacity:0}.icon.outbound{color:#aaa;display:inline-block;vertical-align:middle;position:relative;top:-1px}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}[data-v-ae3bb0ce]::-webkit-scrollbar{display:none}.swiper-container[data-v-ae3bb0ce]{position:relative;width:100%;height:152px;overflow:hidden}.swiper[data-v-ae3bb0ce]{height:100%;white-space:nowrap;transform:translateX(0);display:inline-block}.swiper-list[data-v-ae3bb0ce]{width:100%;display:inline-block}.swiper-dot[data-v-ae3bb0ce]{position:absolute;left:50%;bottom:12px;display:flex;padding:0;z-index:3;transform:translate3d(-50%,0,0)}.swiper-icon[data-v-ae3bb0ce]{list-style:none;width:6px;height:6px;border-radius:50%;background:#efefef;margin:0 3px}.active[data-v-ae3bb0ce]{background:#00bd81}.active[data-v-a2c0041c]{border:2px solid #00bd81!important}.m-banner[data-v-a2c0041c]{display:flex;flex-direction:column;align-items:center;height:468px;background-image:linear-gradient(0deg,#50be97 4%,#31bc7f 83%)}.m-banner .m-title[data-v-a2c0041c]{font-family:PingFangSC-Medium;font-size:26px;color:#fff;letter-spacing:0;text-align:justify;font-weight:500;margin-top:120px}.m-banner .m-subtitle[data-v-a2c0041c]{font-family:PingFangSC-Regular;font-size:13px;color:#fff;letter-spacing:0;text-align:center;line-height:22px;font-weight:400;margin:10px 30px 0}.m-banner .m-banner-btn-wrapper[data-v-a2c0041c]{display:flex;flex-direction:row;justify-content:center;align-content:center;padding-top:30px}.m-banner .m-banner-btn-wrapper .m-banner-btn[data-v-a2c0041c]{width:104px;height:36px;border-radius:4px;text-align:center;line-height:36px}.m-banner .m-banner-btn-wrapper .m-banner-btn-enter[data-v-a2c0041c]{background:#fff;border:0 solid #efefef;margin-right:15px}.m-banner .m-banner-btn-wrapper .m-banner-btn-jump[data-v-a2c0041c]{border:1px solid #fff;margin-left:15px;color:#fff;box-sizing:border-box}.m-banner .m-banner-btn-wrapper .white-link[data-v-a2c0041c]{color:#fff}.m-banner .m-banner-bg[data-v-a2c0041c]{width:375px;height:202px;background-image:url(https://dpubstatic.udache.com/static/dpubimg/A0HNx-AsoO/y_pic_banner.png);background-size:cover;margin-top:35px}.m-advantages[data-v-a2c0041c]{display:flex;justify-content:space-between;align-items:center;padding:20px 16px;background:#fff}.m-advantages .m-advan-section[data-v-a2c0041c]{width:100px;height:78px;background:#fff;box-shadow:0 11px 32px 0 rgba(49,188,127,.06),0 3px 10px 0 rgba(49,188,127,.04);border-radius:4px;text-align:center;list-style:none}.m-advantages .m-advan-section .m-advan-section-img[data-v-a2c0041c]{margin-top:10px}.m-advantages .m-advan-section .m-advan-section-title[data-v-a2c0041c]{margin-top:4px;font-family:PingFangSC-Regular;font-size:15px;color:#3a495d;letter-spacing:0;text-align:center;line-height:22px;font-weight:400}.m-example-swiper[data-v-a2c0041c]{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;border-radius:4px}.m-example-name[data-v-a2c0041c]{width:166px;height:60px;margin:6px;border:2px solid #ededed;box-shadow:0 11px 32px 0 rgba(49,188,127,.06),0 4px 10px 0 rgba(49,188,127,.04);border-radius:4px;display:flex;align-items:center;justify-content:center;background:#fff}.m-example-wrapper[data-v-a2c0041c],.m-feature-wrapper[data-v-a2c0041c],.m-util-wrapper[data-v-a2c0041c],.mdemo-wrapper[data-v-a2c0041c]{display:flex;flex-direction:column;align-items:center;background:#f7f7f7}.m-example-wrapper .m-example-title[data-v-a2c0041c],.m-example-wrapper .m-feature-title[data-v-a2c0041c],.m-example-wrapper .m-util-title[data-v-a2c0041c],.m-example-wrapper .mdemo-title[data-v-a2c0041c],.m-feature-wrapper .m-example-title[data-v-a2c0041c],.m-feature-wrapper .m-feature-title[data-v-a2c0041c],.m-feature-wrapper .m-util-title[data-v-a2c0041c],.m-feature-wrapper .mdemo-title[data-v-a2c0041c],.m-util-wrapper .m-example-title[data-v-a2c0041c],.m-util-wrapper .m-feature-title[data-v-a2c0041c],.m-util-wrapper .m-util-title[data-v-a2c0041c],.m-util-wrapper .mdemo-title[data-v-a2c0041c],.mdemo-wrapper .m-example-title[data-v-a2c0041c],.mdemo-wrapper .m-feature-title[data-v-a2c0041c],.mdemo-wrapper .m-util-title[data-v-a2c0041c],.mdemo-wrapper .mdemo-title[data-v-a2c0041c]{font-family:PingFangSC-Medium;font-size:24px;color:#3a495d;letter-spacing:0;text-align:justify;font-weight:500;margin-top:50px;margin-bottom:20px}.m-example-wrapper .m-feature-subtitle[data-v-a2c0041c],.m-example-wrapper .mdemo-subtitle[data-v-a2c0041c],.m-feature-wrapper .m-feature-subtitle[data-v-a2c0041c],.m-feature-wrapper .mdemo-subtitle[data-v-a2c0041c],.m-util-wrapper .m-feature-subtitle[data-v-a2c0041c],.m-util-wrapper .mdemo-subtitle[data-v-a2c0041c],.mdemo-wrapper .m-feature-subtitle[data-v-a2c0041c],.mdemo-wrapper .mdemo-subtitle[data-v-a2c0041c]{font-family:PingFangSC-Regular;font-size:13px;color:#394e5e;letter-spacing:0;text-align:center;line-height:22px;font-weight:400;padding:0 30px}.m-example-wrapper .m-feature-btn[data-v-a2c0041c],.m-example-wrapper .mdemo-btn[data-v-a2c0041c],.m-feature-wrapper .m-feature-btn[data-v-a2c0041c],.m-feature-wrapper .mdemo-btn[data-v-a2c0041c],.m-util-wrapper .m-feature-btn[data-v-a2c0041c],.m-util-wrapper .mdemo-btn[data-v-a2c0041c],.mdemo-wrapper .m-feature-btn[data-v-a2c0041c],.mdemo-wrapper .mdemo-btn[data-v-a2c0041c]{width:104px;height:36px;background:#00bd81;border:0 solid #efefef;border-radius:4px;margin-top:30px;font-family:PingFangSC-Medium;font-size:15px;color:#fff;letter-spacing:0;text-align:center;line-height:36px;font-weight:500}.m-example-wrapper .mdemo-icon-wrapper[data-v-a2c0041c],.m-feature-wrapper .mdemo-icon-wrapper[data-v-a2c0041c],.m-util-wrapper .mdemo-icon-wrapper[data-v-a2c0041c],.mdemo-wrapper .mdemo-icon-wrapper[data-v-a2c0041c]{margin:10px 0 40px;display:flex;flex-wrap:wrap;justify-content:space-around;align-items:center;align-content:space-around}.m-example-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c],.m-feature-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c],.m-util-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c],.mdemo-wrapper .mdemo-icon-wrapper .mdemo-icon-card[data-v-a2c0041c]{width:100px;display:flex;flex-direction:column;justify-content:center;align-items:center;margin-top:30px}.m-example-wrapper .m-feature-pic[data-v-a2c0041c],.m-feature-wrapper .m-feature-pic[data-v-a2c0041c],.m-util-wrapper .m-feature-pic[data-v-a2c0041c],.mdemo-wrapper .m-feature-pic[data-v-a2c0041c]{margin-top:40px}.m-example-wrapper .six-section__row[data-v-a2c0041c],.m-feature-wrapper .six-section__row[data-v-a2c0041c],.m-util-wrapper .six-section__row[data-v-a2c0041c],.mdemo-wrapper .six-section__row[data-v-a2c0041c]{margin:0 0 10px;flex-wrap:wrap;justify-content:center;padding:0 16px;width:100%;box-sizing:border-box}.m-example-wrapper .six-section__row .six-section__item[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__item[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__item[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__item[data-v-a2c0041c]{align-items:center;background:#fff;border:0 solid #efefef;border-radius:4px;height:72px;display:flex;padding:11px 0 11px 24px;box-sizing:border-box;box-shadow:0 11px 32px 0 rgba(49,188,127,.06),0 4px 10px 0 rgba(49,188,127,.04)}.m-example-wrapper .six-section__row .six-section__icon[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__icon[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__icon[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__icon[data-v-a2c0041c]{margin-right:9px}.m-example-wrapper .six-section__row .six-section__list[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__list[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__list[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__list[data-v-a2c0041c]{display:flex;flex-direction:column;justify-content:space-between;color:#3a495d;margin-left:10px;font-family:PingFangSC-Medium}.m-example-wrapper .six-section__row .six-section__bold[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__bold[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__bold[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__bold[data-v-a2c0041c]{font-size:15px;margin-right:9px;margin-bottom:4px}.m-example-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c],.m-feature-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c],.m-util-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c],.mdemo-wrapper .six-section__row .six-section__subtitle[data-v-a2c0041c]{font-size:13px}.m-example-wrapper .m-example-phone[data-v-a2c0041c],.m-feature-wrapper .m-example-phone[data-v-a2c0041c],.m-util-wrapper .m-example-phone[data-v-a2c0041c],.mdemo-wrapper .m-example-phone[data-v-a2c0041c]{width:100%;display:flex;justify-content:center;position:relative;height:450px;align-items:center}.m-example-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c],.m-feature-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c],.m-util-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c],.mdemo-wrapper .m-example-phone .m-example-img-contain[data-v-a2c0041c]{position:absolute;top:0;left:50%;transform:translate3d(-50%,0,0);width:100%;width:190px;height:390px;background:url(https://dpubstatic.udache.com/static/dpubimg/Vx5n_3YCtP/anli_pic_phone.png) no-repeat 50%;background-size:contain;padding:12px}.m-feature-wrapper[data-v-a2c0041c]{background:#fff;padding-bottom:50px}.m-example-wrapper[data-v-a2c0041c]{background:#fff}.m-util-wrapper[data-v-a2c0041c]{padding-bottom:20px}[data-v-5724bcdd]::-webkit-scrollbar{display:none}.swiper-container[data-v-5724bcdd]{max-width:1190px;display:flex;margin:0 auto}.swiper-container .swiper-button[data-v-5724bcdd]{width:140px;margin-top:80px;transform:filter .3s}.swiper-container .swiper-img[data-v-5724bcdd]{cursor:pointer}.swiper-container .swiper-disable[data-v-5724bcdd]{cursor:not-allowed;filter:grayscale(100%)}.swiper[data-v-5724bcdd]{flex:1;position:relative;overflow-y:hidden;overflow-x:scroll;white-space:nowrap;height:300px}.swiper .swiper-list[data-v-5724bcdd]{display:inline-block;padding:24px;border-radius:10px;margin-top:20px;position:relative;transition:none 0s ease 0s;transform:scale(1)}.swiper .swiper-list.active[data-v-5724bcdd]{transition:transform .3s;transform:scale(1.5)}.swiper .swiper-item[data-v-5724bcdd]{width:136px;height:136px;background:#fff;border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer}.swiper-list[data-v-5724bcdd]:first-child{padding-left:50px}.swiper-list[data-v-5724bcdd]:last-child{padding-right:50px}.swiper-img[data-v-60490726]{width:188px;height:408px;height:100%;white-space:nowrap;overflow:hidden;border-radius:20px}.swiper-img .swiper-img__wrap[data-v-60490726]{display:inline-block}.swiper-img .swiper-img__list[data-v-60490726]{transform:translateZ(0);transition:transform .3s}.fade-enter-active[data-v-8bac5638],.fade-leave-active[data-v-8bac5638]{transition:all .3s}.fade-enter[data-v-8bac5638],.fade-leave-to[data-v-8bac5638]{opacity:0}.popover[data-v-8bac5638]{position:relative;display:inline-block}.popover .popover__top[data-v-8bac5638]{position:absolute;width:300px;height:300px;background:#fff;left:-150px;top:-350px;border-radius:4px}.popover .popover__content[data-v-8bac5638]{position:relative;width:100%;height:100%}.popover .popover__arrow[data-v-8bac5638]{position:absolute;left:50%;margin-left:-5px;bottom:-5px;width:6px;height:6px;background:#fff;transform:rotate(45deg);border-bottom:1px solid #ededed;border-right:1px solid #ededed;z-index:6}.popover .popover__inner[data-v-8bac5638]{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:14px;position:relative;z-index:4;background:#fff;border-radius:4px;border:1px solid #ededed}.popover .popover__content[data-v-8bac5638]{display:inline-block}.fade-enter-active[data-v-76bf3aa2],.fade-leave-active[data-v-76bf3aa2]{transition:all .3s}.fade-enter[data-v-76bf3aa2],.fade-leave-to[data-v-76bf3aa2]{opacity:0}.code-list[data-v-76bf3aa2]{margin-top:40px;display:flex}.code-list .code-list__item[data-v-76bf3aa2]{margin-right:62px;width:150px;height:170px;cursor:pointer}.code-list .code-list__inner[data-v-76bf3aa2]{position:relative}.code-list .code-list__big[data-v-76bf3aa2]{position:absolute;left:0;top:0;transform:scale(1.5)}ul li[data-v-2d48710e]{list-style:none}section[data-v-2d48710e]{position:relative;z-index:2}.row[data-v-2d48710e]{display:flex;align-items:center}.header[data-v-2d48710e]{padding:40px 0 0 30px;position:relative}.title[data-v-2d48710e]{font-size:30px;margin-bottom:20px;font-weight:500}.desc[data-v-2d48710e]{font-size:14px;line-height:22px;margin-bottom:40px}.btn[data-v-2d48710e]{width:116px;height:40px;background-image:linear-gradient(-58deg,#50be97 30%,#31bc7f 79%);box-shadow:-3px 12px 35px 0 rgba(49,188,127,.2);border-radius:4px;border:none;font-size:14px}.one-section__content[data-v-2d48710e]{display:flex;justify-content:flex-end}.one-section[data-v-2d48710e]{padding-top:181px;position:relative;display:flex}.one-section__img1[data-v-2d48710e]{position:absolute;top:0;left:45%;width:100%;height:100%;background:url(https://gift-static.hongyibo.com.cn/static/kfpub/3547/banner_bg_v3.png) no-repeat 0 0;background-size:auto 680px}.one-section__img2[data-v-2d48710e]{position:absolute;top:20;left:53%;width:100%;height:100%;background:url(https://gift-static.hongyibo.com.cn/static/kfpub/3547/banner_pic_v3.png) no-repeat 0 0;background-size:auto 550px}.one-section__inner[data-v-2d48710e]{flex:1}.one-section__title[data-v-2d48710e]{margin-bottom:20px;font-size:40px;font-weight:500;border-bottom:none}.one-section__desc[data-v-2d48710e]{width:450px;line-height:30px;margin-bottom:70px}.one-section__btn[data-v-2d48710e]{width:162px;height:52px;border-radius:4px;border:none;font-size:20px}.one-section__enter[data-v-2d48710e]{background-image:linear-gradient(-58deg,#50be97 30%,#31bc7f 79%);box-shadow:-3px 12px 35px 0 rgba(49,188,127,.2)}.one-section__github[data-v-2d48710e]{margin-left:30px;background:#fff;border:0 solid #efefef;box-shadow:-3px 12px 35px 0 rgba(49,188,127,.2)}.white-link[data-v-2d48710e]{color:#fff}.blue-link[data-v-2d48710e],.white-link[data-v-2d48710e]{font-weight:600;display:flex;align-items:center;justify-content:center}.blue-link[data-v-2d48710e]{color:#31bc7f;width:100%;height:100%}.target-link[data-v-2d48710e]{color:#fff;font-weight:600;text-decoration:underline}.two-section[data-v-2d48710e]{padding-top:240px;display:flex;justify-content:center}.two-section__item[data-v-2d48710e]{width:280px;padding:40px 18px;box-sizing:border-box;background:#fff;border:0 solid #efefef;box-shadow:0 51px 145px 0 rgba(49,188,127,.1),0 11px 32px 0 rgba(49,188,127,.06),0 3px 10px 0 rgba(49,188,127,.04);border-radius:4px;margin:0 10px;text-align:center;list-style:none;position:relative;overflow:hidden}.two-section__item[data-v-2d48710e]:nth-child(2){position:relative;top:54px}.two-section__line[data-v-2d48710e]{height:8px;width:100%;position:absolute;left:0;bottom:0;opacity:.6;background-image:linear-gradient(-58deg,#50be97 30%,#31bc7f 79%)}.two-section__title[data-v-2d48710e]{font-size:20px;margin:33px 0 21px}.two-section__desc[data-v-2d48710e]{font-size:14px;line-height:22px}.three-section[data-v-2d48710e]{margin-top:183px;display:flex;background-repeat:no-repeat;background-size:auto 100%;background-position:50%;height:713px;align-items:center;position:relative}.three-section__inner[data-v-2d48710e]{width:1190px;margin:0 auto;display:flex;align-items:center;box-sizing:border-box;padding:0 40px}.three-section__mvc[data-v-2d48710e]{margin-left:60px}.three-section__todo[data-v-2d48710e]{position:relative}.three-section__phone[data-v-2d48710e]{position:absolute;left:0;top:0}.three-section__iframe[data-v-2d48710e]{position:relative;left:20px;top:20px;border-radius:44px;background:#fff;overflow:hidden;box-shadow:0 80px 252px 0 rgba(49,188,127,.12),0 36px 76px 0 rgba(49,188,127,.08),0 15px 31px 0 rgba(49,188,127,.06),0 5px 11px 0 rgba(49,188,127,.04)}.iframe_wrapper[data-v-2d48710e],.three-section__iframe[data-v-2d48710e]{display:flex;align-items:center;justify-content:center;width:375px;height:672px}.white-text[data-v-2d48710e]{color:#fff}.three-section__btn[data-v-2d48710e]{background:#fff}.section[data-v-2d48710e]{display:flex;align-items:center;height:520px;box-sizing:border-box;margin-top:183px;position:relative;justify-content:center}.grow[data-v-2d48710e]{flex:1}.four-section__bg[data-v-2d48710e]{background-repeat:no-repeat;background-size:auto 600px;height:600px;width:100%;text-align:center;position:absolute;right:50%;background-position:100% 0}.four-section__inner[data-v-2d48710e]{display:flex;width:1190px;align-items:center;padding:0 40px;box-sizing:border-box}.five-section__bg[data-v-2d48710e]{background-repeat:no-repeat;background-size:auto 600px;height:600px;width:100%;text-align:center;position:absolute;left:50%;background-position:0 0}.five-section__inner[data-v-2d48710e]{display:flex;width:1190px;align-items:center;padding:0 40px;box-sizing:border-box}.six-section[data-v-2d48710e]{margin-top:140px;display:flex;align-items:center;justify-content:center;background-repeat:no-repeat;background-size:auto 100%;background-position:50%;height:694px}.six-section__inner[data-v-2d48710e]{margin-bottom:50px}.six-section__item[data-v-2d48710e]{background:#fff;border:0 solid #a4a4a4;border-radius:4px;width:290px;height:80px;display:flex;padding:17px 0 17px 17px;box-sizing:border-box}.six-section__step[data-v-2d48710e]{margin-right:20px}.six-section__row[data-v-2d48710e]{margin-bottom:20px;flex-wrap:wrap;justify-content:center}.six-section__list[data-v-2d48710e]{display:flex;flex-direction:column;justify-content:space-between;font-size:14px;color:#3a495d}.six-section__bold[data-v-2d48710e]{font-size:16px;font-weight:500;white-space:nowrap}.six-section__title[data-v-2d48710e]{margin-bottom:50px;text-align:center;color:#fff}.six-section__icon[data-v-2d48710e]{margin-right:9px}.seven-section[data-v-2d48710e]{margin-top:113px;text-align:center;background:#f5f5f5}.seven-section__center[data-v-2d48710e]{width:402px;height:100%;background-repeat:no-repeat;background-size:contain;background-position:50%;display:flex;justify-content:center;position:relative}.seven-section__inner[data-v-2d48710e]{padding:14px;box-shadow:0 43px 86px 0 rgba(49,188,127,.07),0 7px 21px 0 rgba(49,188,127,.04),0 2px 6px 0 rgba(49,188,127,.03);border-radius:30px}.seven-section__wrap[data-v-2d48710e]{height:433px;margin-top:40px;margin-bottom:40px}.seven-section_phone[data-v-2d48710e]{position:absolute;top:0;left:50%;transform:translate3d(-50%,0,0)}.seven-section__title[data-v-2d48710e]{text-align:right;font-size:20px;font-weight:600}.seven-section__desc[data-v-2d48710e]{margin-top:41px;text-align:left;font-size:14px;line-height:23px}.dot[data-v-2d48710e]{width:100%}.dot-inner[data-v-2d48710e]{background:#31bc7f;border-radius:4px;width:34px;height:10px;display:inline-block}ul li[data-v-f9ca297c]{list-style:none}.footer-container[data-v-f9ca297c]{display:flex;flex-direction:column;justify-content:flex-end;height:466px;background:url(https://dpubstatic.udache.com/static/dpubimg/cSRXkZjG5W/footer_bg.png) no-repeat 50%;background-size:auto 100%;margin-top:60px}.footer[data-v-f9ca297c]{display:flex;text-align:center;max-width:1280px;margin:0 auto;width:100%}.footer__list[data-v-f9ca297c]{margin-bottom:80px;display:flex;flex:1;text-align:left}.footer__wrap[data-v-f9ca297c]{margin-bottom:14px;color:#fff;text-align:left;height:28px;position:relative}.footer__text[data-v-f9ca297c]{font-size:14px;color:#fff;display:inline-block;text-align:left;height:28px}.footer__title[data-v-f9ca297c]{font-size:20px;font-weight:500}.copyright[data-v-f9ca297c]{font-size:12px;color:#fff;background:#3a495d;text-align:center;line-height:30px;padding:10px 0}.grow[data-v-f9ca297c]{flex:1}.footer__img[data-v-f9ca297c]{position:absolute;left:0;top:0;display:none}@media (max-width:750px){.footer__list[data-v-f9ca297c]{margin-bottom:16px}.footer__text[data-v-f9ca297c]{font-size:12px}.footer__title[data-v-f9ca297c]{font-size:12px;color:#979797}.footer__wrap[data-v-f9ca297c]{margin-bottom:0}.footer-container[data-v-f9ca297c]{background:#606d7c;height:auto;margin-top:0}.footer-inner[data-v-f9ca297c]{padding-left:6px}}.home{padding:3.6rem 2rem 0;max-width:960px;margin:0 auto;display:block}.home .hero{text-align:center}.home .hero img{max-width:100%;max-height:280px;display:block;margin:3rem auto 1.5rem}.home .hero h1{font-size:3rem}.home .hero .action,.home .hero .description,.home .hero h1{margin:1.8rem auto}.home .hero .description{max-width:35rem;font-size:1.6rem;line-height:1.3;color:#6a8bad}.home .hero .action-button{display:inline-block;font-size:1.2rem;color:#fff;background-color:#3eaf7c;padding:.8rem 1.6rem;border-radius:4px;transition:background-color .1s ease;box-sizing:border-box;border-bottom:1px solid #389d70}.home .hero .action-button:hover{background-color:#4abf8a}.home .features{border-top:1px solid #eaecef;padding:1.2rem 0;margin-top:2.5rem;display:flex;flex-wrap:wrap;align-items:flex-start;align-content:stretch;justify-content:space-between}.home .feature{flex-grow:1;flex-basis:30%;max-width:30%}.home .feature h2{font-size:1.4rem;font-weight:500;border-bottom:none;padding-bottom:0;color:#3a5169}.home .feature p{color:#4e6e8e}.home .footer{padding:2.5rem;border-top:1px solid #eaecef;text-align:center;color:#4e6e8e}@media (max-width:719px){.home .features{flex-direction:column}.home .feature{max-width:100%;padding:0 2.5rem}}@media (max-width:419px){.home{padding-left:1.5rem;padding-right:1.5rem}.home .hero img{max-height:210px;margin:2rem auto 1.2rem}.home .hero h1{font-size:2rem}.home .hero .action,.home .hero .description,.home .hero h1{margin:1.2rem auto}.home .hero .description{font-size:1.2rem}.home .hero .action-button{font-size:1rem;padding:.6rem 1.2rem}.home .feature h2{font-size:1.25rem}}.page-edit{max-width:740px;margin:0 auto;padding:2rem 2.5rem}@media (max-width:959px){.page-edit{padding:2rem}}@media (max-width:419px){.page-edit{padding:1.5rem}}.page-edit{padding-top:1rem;padding-bottom:1rem;overflow:auto}.page-edit .edit-link{display:inline-block}.page-edit .edit-link a{color:#4e6e8e;margin-right:.25rem}.page-edit .last-updated{float:right;font-size:.9em}.page-edit .last-updated .prefix{font-weight:500;color:#4e6e8e}.page-edit .last-updated .time{font-weight:400;color:#767676}@media (max-width:719px){.page-edit .edit-link{margin-bottom:.5rem}.page-edit .last-updated{font-size:.8em;float:none;text-align:left}}.page-nav{max-width:740px;margin:0 auto;padding:2rem 2.5rem}@media (max-width:959px){.page-nav{padding:2rem}}@media (max-width:419px){.page-nav{padding:1.5rem}}.page-nav{padding-top:1rem;padding-bottom:0}.page-nav .inner{min-height:2rem;margin-top:0;border-top:1px solid #eaecef;padding-top:1rem;overflow:auto}.page-nav .next{float:right}.page{padding-bottom:2rem;display:block}.dropdown-enter,.dropdown-leave-to{height:0!important}.sidebar-button{cursor:pointer;display:none;width:1.25rem;height:1.25rem;position:absolute;padding:.6rem;top:.6rem;left:1rem}.sidebar-button .icon{display:block;width:1.25rem;height:1.25rem}@media (max-width:719px){.sidebar-button{display:block}}.badge[data-v-59f00772]{display:inline-block;font-size:14px;height:18px;line-height:18px;border-radius:3px;padding:0 6px;color:#fff}.badge.green[data-v-59f00772],.badge.tip[data-v-59f00772],.badge[data-v-59f00772]{background-color:#42b983}.badge.error[data-v-59f00772]{background-color:#da5961}.badge.warn[data-v-59f00772],.badge.warning[data-v-59f00772],.badge.yellow[data-v-59f00772]{background-color:#e7c000}.badge+.badge[data-v-59f00772]{margin-left:5px}.theme-code-block[data-v-488b05bd]{display:none}.theme-code-block__active[data-v-488b05bd]{display:block}.theme-code-block>pre[data-v-488b05bd]{background-color:orange}.theme-code-group__nav[data-v-131b8180]{margin-bottom:-35px;background-color:#282c34;padding-bottom:22px;border-top-left-radius:6px;border-top-right-radius:6px;padding-left:10px;padding-top:10px}.theme-code-group__ul[data-v-131b8180]{margin:auto 0;padding-left:0;display:inline-flex;list-style:none}.theme-code-group__nav-tab[data-v-131b8180]{border:0;padding:5px;cursor:pointer;background-color:transparent;font-size:.85em;line-height:1.4;color:hsla(0,0%,100%,.9);font-weight:600}.theme-code-group__nav-tab-active[data-v-131b8180]{border-bottom:1px solid #42b983}.pre-blank[data-v-131b8180]{color:#42b983}.swiper-item[data-v-f19c6420]{width:100%}img[data-v-f19c6420]{display:block;width:100%;height:100%}#api-index[data-v-a2f64316]{max-width:1024px;margin:0 auto;padding:64px 32px}a[data-v-a2f64316]{text-decoration:none}h1[data-v-a2f64316],h2[data-v-a2f64316],h3[data-v-a2f64316]{font-weight:600;line-height:1}h1[data-v-a2f64316],h2[data-v-a2f64316]{letter-spacing:-.02em}h1[data-v-a2f64316]{font-size:38px}ul[data-v-a2f64316]{list-style:none;margin:0;padding:0}h2[data-v-a2f64316]{font-size:24px;color:var(--vt-c-text-1);margin:36px 0;transition:color .5s;padding-top:36px;border-top:1px solid var(--vt-c-divider-light);border-bottom:none}h3[data-v-a2f64316]{letter-spacing:-.01em;color:var(--vt-c-green);font-size:18px;margin-bottom:1em;transition:color .5s}.api-section[data-v-a2f64316]{margin-bottom:64px}.api-groups a[data-v-a2f64316]{font-size:15px;font-weight:500;line-height:2;color:var(--vt-c-text-code);transition:color .5s}.dark api-groups a[data-v-a2f64316]{font-weight:400}.api-groups a[data-v-a2f64316]:hover{color:var(--vt-c-green);transition:none}.api-group[data-v-a2f64316]{-moz-column-break-inside:avoid;break-inside:avoid;overflow:auto;margin-bottom:20px;background-color:var(--vt-c-bg-soft);border-radius:8px;padding:28px 26px;transition:background-color .5s}.header[data-v-a2f64316]{display:flex;align-items:center;justify-content:space-between}.api-filter[data-v-a2f64316]{display:flex;align-items:center;justify-content:flex-start;gap:1rem}.api-filter input[data-v-a2f64316]{border:1px solid var(--vt-c-divider);border-radius:8px;padding:6px 12px}.api-filter[data-v-a2f64316]:focus{border-color:var(--vt-c-green-light)}.no-match[data-v-a2f64316]{font-size:1.2em;color:var(--vt-c-text-3);text-align:center;margin-top:36px;padding-top:36px;border-top:1px solid var(--vt-c-divider-light)}@media (max-width:768px){#api-index[data-v-a2f64316]{padding:42px 24px}h1[data-v-a2f64316]{font-size:32px;margin-bottom:24px}h2[data-v-a2f64316]{font-size:22px;margin:42px 0 32px;padding-top:32px}.api-groups a[data-v-a2f64316]{font-size:14px}.header[data-v-a2f64316]{display:block}}@media (min-width:768px){.api-groups[data-v-a2f64316]{-moz-columns:2;column-count:2}}@media (min-width:1024px){.api-groups[data-v-a2f64316]{-moz-columns:3;column-count:3}}.sw-update-popup[data-v-613ca22e]{position:fixed;right:1em;bottom:1em;padding:1em;border:1px solid #3eaf7c;border-radius:3px;background:#fff;box-shadow:0 4px 16px rgba(0,0,0,.5);text-align:center;z-index:3}.sw-update-popup>button[data-v-613ca22e]{margin-top:.5em;padding:.25em 2em}.sw-update-popup-enter-active[data-v-613ca22e],.sw-update-popup-leave-active[data-v-613ca22e]{transition:opacity .3s,transform .3s}.sw-update-popup-enter[data-v-613ca22e],.sw-update-popup-leave-to[data-v-613ca22e]{opacity:0;transform:translateY(50%) scale(.5)}.sidebar-group .sidebar-group{padding-left:.5em}.sidebar-group:not(.collapsable) .sidebar-heading:not(.clickable){cursor:auto;color:inherit}.sidebar-group.is-sub-group{padding-left:0}.sidebar-group.is-sub-group>.sidebar-heading{font-size:.95em;line-height:1.4;font-weight:400;padding-left:2rem}.sidebar-group.is-sub-group>.sidebar-heading:not(.clickable){opacity:.5}.sidebar-group.is-sub-group>.sidebar-group-items{padding-left:1rem}.sidebar-group.is-sub-group>.sidebar-group-items>li>.sidebar-link{font-size:.95em;border-left:none}.sidebar-group.depth-2>.sidebar-heading{border-left:none}.sidebar-heading{color:#2c3e50;transition:color .15s ease;cursor:pointer;font-size:1.1em;font-weight:700;padding:.35rem 1.5rem .35rem 1.25rem;width:100%;box-sizing:border-box;margin:0;border-left:.25rem solid transparent}.sidebar-heading.open,.sidebar-heading:hover{color:inherit}.sidebar-heading .arrow{position:relative;top:-.12em;left:.5em}.sidebar-heading.clickable.active{font-weight:600;color:#3eaf7c;border-left-color:#3eaf7c}.sidebar-heading.clickable:hover{color:#3eaf7c}.sidebar-group-items{transition:height .1s ease-out;font-size:.95em;overflow:hidden}.sidebar .sidebar-sub-headers{padding-left:1rem;font-size:.95em}a.sidebar-link{font-size:1em;font-weight:400;display:inline-block;color:#2c3e50;border-left:.25rem solid transparent;padding:.35rem 1rem .35rem 1.25rem;line-height:1.4;width:100%;box-sizing:border-box}a.sidebar-link:hover{color:#3eaf7c}a.sidebar-link.active{font-weight:600;color:#3eaf7c;border-left-color:#3eaf7c}.sidebar-group a.sidebar-link{padding-left:2rem}.sidebar-sub-headers a.sidebar-link{padding-top:.25rem;padding-bottom:.25rem;border-left:none}.sidebar-sub-headers a.sidebar-link.active{font-weight:500}.dropdown-wrapper{cursor:pointer}.dropdown-wrapper .dropdown-title,.dropdown-wrapper .mobile-dropdown-title{display:block;font-size:.9rem;font-family:inherit;cursor:inherit;padding:inherit;line-height:1.4rem;background:transparent;border:none;font-weight:500;color:#2c3e50}.dropdown-wrapper .dropdown-title:hover,.dropdown-wrapper .mobile-dropdown-title:hover{border-color:transparent}.dropdown-wrapper .dropdown-title .arrow,.dropdown-wrapper .mobile-dropdown-title .arrow{vertical-align:middle;margin-top:-1px;margin-left:.4rem}.dropdown-wrapper .mobile-dropdown-title{display:none;font-weight:600}.dropdown-wrapper .mobile-dropdown-title font-size inherit:hover{color:#3eaf7c}.dropdown-wrapper .nav-dropdown .dropdown-item{color:inherit;line-height:1.7rem}.dropdown-wrapper .nav-dropdown .dropdown-item h4{margin:.45rem 0 0;border-top:1px solid #eee;padding:1rem 1.5rem .45rem 1.25rem}.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem-wrapper{padding:0;list-style:none}.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem-wrapper .dropdown-subitem{font-size:.9em}.dropdown-wrapper .nav-dropdown .dropdown-item a{display:block;line-height:1.7rem;position:relative;border-bottom:none;font-weight:400;margin-bottom:0;padding:0 1.5rem 0 1.25rem}.dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active,.dropdown-wrapper .nav-dropdown .dropdown-item a:hover{color:#3eaf7c}.dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active:after{content:"";width:0;height:0;border-left:5px solid #3eaf7c;border-top:3px solid transparent;border-bottom:3px solid transparent;position:absolute;top:calc(50% - 2px);left:9px}.dropdown-wrapper .nav-dropdown .dropdown-item:first-child h4{margin-top:0;padding-top:0;border-top:0}@media (max-width:719px){.dropdown-wrapper.open .dropdown-title{margin-bottom:.5rem}.dropdown-wrapper .dropdown-title{display:none}.dropdown-wrapper .mobile-dropdown-title{display:block}.dropdown-wrapper .nav-dropdown{transition:height .1s ease-out;overflow:hidden}.dropdown-wrapper .nav-dropdown .dropdown-item h4{border-top:0;margin-top:0;padding-top:0}.dropdown-wrapper .nav-dropdown .dropdown-item>a,.dropdown-wrapper .nav-dropdown .dropdown-item h4{font-size:15px;line-height:2rem}.dropdown-wrapper .nav-dropdown .dropdown-item .dropdown-subitem{font-size:14px;padding-left:1rem}}@media (min-width:719px){.dropdown-wrapper{height:1.8rem}.dropdown-wrapper.open .nav-dropdown,.dropdown-wrapper:hover .nav-dropdown{display:block!important}.dropdown-wrapper .nav-dropdown{display:none;height:auto!important;box-sizing:border-box;max-height:calc(100vh - 2.7rem);overflow-y:auto;position:absolute;top:100%;right:0;background-color:#fff;padding:.6rem 0;border:1px solid;border-color:#ddd #ddd #ccc;text-align:left;border-radius:.25rem;white-space:nowrap;margin:0}}.nav-links{display:inline-block}.nav-links a{line-height:1.4rem;color:inherit}.nav-links a.router-link-active,.nav-links a:hover{color:#3eaf7c}.nav-links .nav-item{position:relative;display:inline-block;margin-left:1.5rem;line-height:2rem}.nav-links .nav-item:first-child{margin-left:0}.nav-links .repo-link{margin-left:1.5rem}@media (max-width:719px){.nav-links .nav-item,.nav-links .repo-link{margin-left:0}}@media (min-width:719px){.nav-links a.router-link-active,.nav-links a:hover{color:#2c3e50}.nav-item>a:not(.external).router-link-active,.nav-item>a:not(.external):hover{margin-bottom:-2px;border-bottom:2px solid #46bd87}}.sidebar ul{padding:0;margin:0;list-style-type:none}.sidebar a{display:inline-block}.sidebar .nav-links{display:none;border-bottom:1px solid #eaecef;padding:.5rem 0 .75rem}.sidebar .nav-links a{font-weight:600}.sidebar .nav-links .nav-item,.sidebar .nav-links .repo-link{display:block;line-height:1.25rem;font-size:1.1em;padding:.5rem 0 .5rem 1.5rem}.sidebar>.sidebar-links{padding:1.5rem 0}.sidebar>.sidebar-links>li>a.sidebar-link{font-size:1.1em;line-height:1.7;font-weight:700}.sidebar>.sidebar-links>li:not(:first-child){margin-top:.75rem}@media (max-width:719px){.sidebar .nav-links{display:block}.sidebar .nav-links .dropdown-wrapper .nav-dropdown .dropdown-item a.router-link-active:after{top:calc(1rem - 2px)}.sidebar>.sidebar-links{padding:1rem 0}}.test{width:200px;height:200px;background:red} \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg b/docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg deleted file mode 100644 index 03d83913e8..0000000000 --- a/docs-vuepress/.vuepress/dist/assets/img/search.83621669.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs-vuepress/.vuepress/dist/assets/img/start-tips1.3b76ac97.png b/docs-vuepress/.vuepress/dist/assets/img/start-tips1.3b76ac97.png deleted file mode 100644 index 6b037f53b8e4129bed96e17654b47caaca1a6979..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39681 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 a0?p;miHTny0XBdAufF5I<2Crt$^QXRr|zHt diff --git a/docs-vuepress/.vuepress/dist/assets/img/start-tips2.7d8836f8.png b/docs-vuepress/.vuepress/dist/assets/img/start-tips2.7d8836f8.png deleted file mode 100644 index 7297ca2ee43cc1c617d16ae5d49bc16e63bcfb44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51539 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 DPj#Nk diff --git a/docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js b/docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js deleted file mode 100644 index 9af842699f..0000000000 --- a/docs-vuepress/.vuepress/dist/assets/js/1.4f7abe8f.js +++ /dev/null @@ -1 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[1,4,15,16,20,31,34],{290:function(t,e,n){"use strict";n.d(e,"d",(function(){return i})),n.d(e,"a",(function(){return s})),n.d(e,"i",(function(){return a})),n.d(e,"f",(function(){return l})),n.d(e,"g",(function(){return u})),n.d(e,"h",(function(){return c})),n.d(e,"b",(function(){return p})),n.d(e,"e",(function(){return h})),n.d(e,"k",(function(){return d})),n.d(e,"l",(function(){return f})),n.d(e,"c",(function(){return m})),n.d(e,"j",(function(){return g}));n(69),n(10),n(19),n(48),n(30);const i=/#.*$/,r=/\.(md|html)$/,s=/\/$/,a=/^[a-z]+:/i;function o(t){return decodeURI(t).replace(i,"").replace(r,"")}function l(t){return a.test(t)}function u(t){return/^mailto:/.test(t)}function c(t){return/^tel:/.test(t)}function p(t){if(l(t))return t;const e=t.match(i),n=e?e[0]:"",r=o(t);return s.test(r)?t:r+".html"+n}function h(t,e){const n=decodeURIComponent(t.hash),r=function(t){const e=t.match(i);if(e)return e[0]}(e);if(r&&n!==r)return!1;return o(t.path)===o(e)}function d(t,e,n){if(l(e))return{type:"external",path:e};n&&(e=function(t,e,n){const i=t.charAt(0);if("/"===i)return t;if("?"===i||"#"===i)return e+t;const r=e.split("/");n&&r[r.length-1]||r.pop();const s=t.replace(/^\//,"").split("/");for(let t=0;tfunction t(e,n,i,r=1){if("string"==typeof e)return d(n,e,i);if(Array.isArray(e))return Object.assign(d(n,e[0],i),{title:e[1]});{const s=e.children||[];return 0===s.length&&e.path?Object.assign(d(n,e.path,i),{title:e.title}):{type:"group",path:e.path,title:e.title,sidebarDepth:e.sidebarDepth,initialOpenGroupIndex:e.initialOpenGroupIndex,children:s.map(e=>t(e,n,i,r+1)),collapsable:!1!==e.collapsable}}}(t,r,n)):[]}return[]}function b(t){const e=m(t.headers||[]);return[{type:"group",collapsable:!1,title:t.title,path:null,children:e.map(e=>({type:"auto",title:e.title,basePath:t.path,path:t.path+"#"+e.slug,children:e.children||[]}))}]}function m(t){let e;return(t=t.map(t=>Object.assign({},t))).forEach(t=>{2===t.level?e=t:e&&(e.children||(e.children=[])).push(t)}),t.filter(t=>2===t.level)}function g(t){return Object.assign(t,{type:t.items&&t.items.length?"links":"link"})}},291:function(t,e,n){},293:function(t,e,n){"use strict";n.r(e);n(10),n(47);var i=n(290),r={name:"NavLink",props:{item:{required:!0}},computed:{link(){return Object(i.b)(this.item.link)},exact(){return this.$site.locales?Object.keys(this.$site.locales).some(t=>t===this.link):"/"===this.link},isNonHttpURI(){return Object(i.g)(this.link)||Object(i.h)(this.link)},isBlankTarget(){return"_blank"===this.target},isInternal(){return!Object(i.f)(this.link)&&!this.isBlankTarget},target(){return this.isNonHttpURI?null:this.item.target?this.item.target:Object(i.f)(this.link)?"_blank":""},rel(){return this.isNonHttpURI||!1===this.item.rel?null:this.item.rel?this.item.rel:this.isBlankTarget?"noopener noreferrer":null}},methods:{focusoutAction(){this.$emit("focusout")}}},s=n(4),a=Object(s.a)(r,(function(){var t=this,e=t._self._c;return t.isInternal?e("RouterLink",{staticClass:"nav-link",attrs:{to:t.link,exact:t.exact},nativeOn:{focusout:function(e){return t.focusoutAction.apply(null,arguments)}}},[t._v("\n "+t._s(t.item.text)+"\n")]):e("a",{staticClass:"nav-link external",attrs:{href:t.link,target:t.target,rel:t.rel},on:{focusout:t.focusoutAction}},[t._v("\n "+t._s(t.item.text)+"\n "),t.isBlankTarget?e("OutboundLink"):t._e()],1)}),[],!1,null,null,null);e.default=a.exports},294:function(t,e,n){"use strict";n(291)},296:function(t,e,n){"use strict";n.r(e);var i={name:"DropdownTransition",methods:{setHeight(t){t.style.height=t.scrollHeight+"px"},unsetHeight(t){t.style.height=""}}},r=(n(294),n(4)),s=Object(r.a)(i,(function(){return(0,this._self._c)("transition",{attrs:{name:"dropdown"},on:{enter:this.setHeight,"after-enter":this.unsetHeight,"before-leave":this.setHeight}},[this._t("default")],2)}),[],!1,null,null,null);e.default=s.exports},297:function(t,e,n){},300:function(t,e,n){},304:function(t,e,n){"use strict";n(297)},306:function(t,e,n){},311:function(t,e,n){},312:function(t,e,n){"use strict";n(313)},313:function(t,e,n){"use strict";var i=n(14),r=n(49),s=n(11),a=n(5),o=n(26);i({target:"Iterator",proto:!0,real:!0},{find:function(t){a(this),s(t);var e=o(this),n=0;return r(e,(function(e,i){if(t(e,n++))return i(e)}),{IS_RECORD:!0,INTERRUPTED:!0}).result}})},314:function(t,e,n){"use strict";n(300)},322:function(t,e,n){"use strict";n.r(e);var i=n(293),r=n(296),s=n(127),a=n.n(s),o={name:"DropdownLink",components:{NavLink:i.default,DropdownTransition:r.default},props:{item:{required:!0}},data:()=>({open:!1}),computed:{dropdownAriaLabel(){return this.item.ariaLabel||this.item.text}},watch:{$route(){this.open=!1}},methods:{setOpen(t){this.open=t},isLastItemOfArray:(t,e)=>a()(e)===t,handleDropdown(){0===event.detail&&this.setOpen(!this.open)}}},l=(n(304),n(4)),u=Object(l.a)(o,(function(){var t=this,e=t._self._c;return e("div",{staticClass:"dropdown-wrapper",class:{open:t.open}},[e("button",{staticClass:"dropdown-title",attrs:{type:"button","aria-label":t.dropdownAriaLabel},on:{click:t.handleDropdown}},[e("span",{staticClass:"title"},[t._v(t._s(t.item.text))]),t._v(" "),e("span",{staticClass:"arrow down"})]),t._v(" "),e("button",{staticClass:"mobile-dropdown-title",attrs:{type:"button","aria-label":t.dropdownAriaLabel},on:{click:function(e){return t.setOpen(!t.open)}}},[e("span",{staticClass:"title"},[t._v(t._s(t.item.text))]),t._v(" "),e("span",{staticClass:"arrow",class:t.open?"down":"right"})]),t._v(" "),e("DropdownTransition",[e("ul",{directives:[{name:"show",rawName:"v-show",value:t.open,expression:"open"}],staticClass:"nav-dropdown"},t._l(t.item.items,(function(n,i){return e("li",{key:n.link||i,staticClass:"dropdown-item"},["links"===n.type?e("h4",[t._v("\n "+t._s(n.text)+"\n ")]):t._e(),t._v(" "),"links"===n.type?e("ul",{staticClass:"dropdown-subitem-wrapper"},t._l(n.items,(function(i){return e("li",{key:i.link,staticClass:"dropdown-subitem"},[e("NavLink",{attrs:{item:i},on:{focusout:function(e){t.isLastItemOfArray(i,n.items)&&t.isLastItemOfArray(n,t.item.items)&&t.setOpen(!1)}}})],1)})),0):e("NavLink",{attrs:{item:n},on:{focusout:function(e){t.isLastItemOfArray(n,t.item.items)&&t.setOpen(!1)}}})],1)})),0)])],1)}),[],!1,null,null,null);e.default=u.exports},325:function(t,e,n){"use strict";n.r(e);n(10),n(47);var i=n(338),r=n(327),s=n(290);function a(t,e){if("group"===e.type){const n=e.path&&Object(s.e)(t,e.path),i=e.children.some(e=>"group"===e.type?a(t,e):"page"===e.type&&Object(s.e)(t,e.path));return n||i}return!1}var o={name:"SidebarLinks",components:{SidebarGroup:i.default,SidebarLink:r.default},props:["items","depth","sidebarDepth","initialOpenGroupIndex"],data(){return{openGroupIndex:this.initialOpenGroupIndex||0}},watch:{$route(){this.refreshIndex()}},created(){this.refreshIndex()},methods:{refreshIndex(){const t=function(t,e){for(let n=0;n-1&&(this.openGroupIndex=t)},toggleGroup(t){this.openGroupIndex=t===this.openGroupIndex?-1:t},isActive(t){return Object(s.e)(this.$route,t.regularPath)}}},l=n(4),u=Object(l.a)(o,(function(){var t=this,e=t._self._c;return t.items.length?e("ul",{staticClass:"sidebar-links"},t._l(t.items,(function(n,i){return e("li",{key:i},["group"===n.type?e("SidebarGroup",{attrs:{item:n,open:i===t.openGroupIndex,collapsable:n.collapsable||n.collapsible,depth:t.depth},on:{toggle:function(e){return t.toggleGroup(i)}}}):e("SidebarLink",{attrs:{"sidebar-depth":t.sidebarDepth,item:n}})],1)})),0):t._e()}),[],!1,null,null,null);e.default=u.exports},327:function(t,e,n){"use strict";n.r(e);n(10),n(312),n(30),n(47);var i=n(290);function r(t,e,n,i,r){const s={props:{to:e,activeClass:"",exactActiveClass:""},class:{active:i,"sidebar-link":!0}};return r>2&&(s.style={"padding-left":r+"rem"}),t("RouterLink",s,n)}function s(t,e,n,a,o,l=1){return!e||l>o?null:t("ul",{class:"sidebar-sub-headers"},e.map(e=>{const u=Object(i.e)(a,n+"#"+e.slug);return t("li",{class:"sidebar-sub-header"},[r(t,n+"#"+e.slug,e.title,u,e.level-1),s(t,e.children,n,a,o,l+1)])}))}var a={functional:!0,props:["item","sidebarDepth"],render(t,{parent:{$page:e,$site:n,$route:a,$themeConfig:o,$themeLocaleConfig:l},props:{item:u,sidebarDepth:c}}){const p=Object(i.e)(a,u.path),h="auto"===u.type?p||u.children.some(t=>Object(i.e)(a,u.basePath+"#"+t.slug)):p,d="external"===u.type?function(t,e,n){return t("a",{attrs:{href:e,target:"_blank",rel:"noopener noreferrer"},class:{"sidebar-link":!0}},[n,t("OutboundLink")])}(t,u.path,u.title||u.path):r(t,u.path,u.title||u.path,h),f=[e.frontmatter.sidebarDepth,c,l.sidebarDepth,o.sidebarDepth,1].find(t=>void 0!==t),b=l.displayAllHeaders||o.displayAllHeaders;if("auto"===u.type)return[d,s(t,u.children,u.basePath,a,f)];if((h||b)&&u.headers&&!i.d.test(u.path)){return[d,s(t,Object(i.c)(u.headers),u.path,a,f)]}return d}},o=(n(314),n(4)),l=Object(o.a)(a,void 0,void 0,!1,null,null,null);e.default=l.exports},329:function(t,e,n){"use strict";n(306)},331:function(t,e,n){"use strict";n(311)},338:function(t,e,n){"use strict";n.r(e);var i=n(290),r={name:"SidebarGroup",components:{DropdownTransition:n(296).default},props:["item","open","collapsable","depth"],beforeCreate(){this.$options.components.SidebarLinks=n(325).default},methods:{isActive:i.e}},s=(n(331),n(4)),a=Object(s.a)(r,(function(){var t=this,e=t._self._c;return e("section",{staticClass:"sidebar-group",class:[{collapsable:t.collapsable,"is-sub-group":0!==t.depth},"depth-"+t.depth]},[t.item.path?e("RouterLink",{staticClass:"sidebar-heading clickable",class:{open:t.open,active:t.isActive(t.$route,t.item.path)},attrs:{to:t.item.path},nativeOn:{click:function(e){return t.$emit("toggle")}}},[e("span",[t._v(t._s(t.item.title))]),t._v(" "),t.collapsable?e("span",{staticClass:"arrow",class:t.open?"down":"right"}):t._e()]):e("p",{staticClass:"sidebar-heading",class:{open:t.open},on:{click:function(e){return t.$emit("toggle")}}},[e("span",[t._v(t._s(t.item.title))]),t._v(" "),t.collapsable?e("span",{staticClass:"arrow",class:t.open?"down":"right"}):t._e()]),t._v(" "),e("DropdownTransition",[t.open||!t.collapsable?e("SidebarLinks",{staticClass:"sidebar-group-items",attrs:{items:t.item.children,"sidebar-depth":t.item.sidebarDepth,"initial-open-group-index":t.item.initialOpenGroupIndex,depth:t.depth+1}}):t._e()],1)],1)}),[],!1,null,null,null);e.default=a.exports},339:function(t,e,n){},349:function(t,e,n){"use strict";n.r(e);n(10),n(30),n(47);var i=n(322),r=n(290),s={name:"NavLinks",components:{NavLink:n(293).default,DropdownLink:i.default},computed:{userNav(){return this.$themeLocaleConfig.nav||this.$site.themeConfig.nav||[]},nav(){const{locales:t}=this.$site;if(t&&Object.keys(t).length>1){const e=this.$page.path,n=this.$router.options.routes,i=this.$site.themeConfig.locales||{},r={text:this.$themeLocaleConfig.selectText||"Languages",ariaLabel:this.$themeLocaleConfig.ariaLabel||"Select language",items:Object.keys(t).map(r=>{const s=t[r],a=i[r]&&i[r].label||s.lang;let o;return s.lang===this.$lang?o=e:(o=e.replace(this.$localeConfig.path,r),n.some(t=>t.path===o)||(o=r)),{text:a,link:o}})};return[...this.userNav,r]}return this.userNav},userLinks(){return(this.nav||[]).map(t=>Object.assign(Object(r.j)(t),{items:(t.items||[]).map(r.j)}))},repoLink(){const{repo:t}=this.$site.themeConfig;return t?/^https?:/.test(t)?t:"https://github.com/"+t:null},repoLabel(){if(!this.repoLink)return;if(this.$site.themeConfig.repoLabel)return this.$site.themeConfig.repoLabel;const t=this.repoLink.match(/^https?:\/\/[^/]+/)[0],e=["GitHub","GitLab","Bitbucket"];for(let n=0;n({codeTabs:[],activeCodeTabIndex:-1}),watch:{activeCodeTabIndex(e){this.activateCodeTab(e)}},mounted(){this.loadTabs()},methods:{changeCodeTab(e){this.activeCodeTabIndex=e},loadTabs(){this.codeTabs=(this.$slots.default||[]).filter(e=>Boolean(e.componentOptions)).map((e,t)=>(""===e.componentOptions.propsData.active&&(this.activeCodeTabIndex=t),{title:e.componentOptions.propsData.title,elm:e.elm})),-1===this.activeCodeTabIndex&&this.codeTabs.length>0&&(this.activeCodeTabIndex=0),this.activateCodeTab(0)},activateCodeTab(e){this.codeTabs.forEach(e=>{e.elm&&e.elm.classList.remove("theme-code-block__active")}),this.codeTabs[e].elm&&this.codeTabs[e].elm.classList.add("theme-code-block__active")}}},s=(a(362),a(4)),c=Object(s.a)(o,(function(){var e=this,t=e._self._c;return t("ClientOnly",[t("div",{staticClass:"theme-code-group"},[t("div",{staticClass:"theme-code-group__nav"},[t("ul",{staticClass:"theme-code-group__ul"},e._l(e.codeTabs,(function(a,o){return t("li",{key:a.title,staticClass:"theme-code-group__li"},[t("button",{staticClass:"theme-code-group__nav-tab",class:{"theme-code-group__nav-tab-active":o===e.activeCodeTabIndex},on:{click:function(t){return e.changeCodeTab(o)}}},[e._v("\n "+e._s(a.title)+"\n ")])])})),0)]),e._v(" "),e._t("default"),e._v(" "),e.codeTabs.length<1?t("pre",{staticClass:"pre-blank"},[e._v("// Make sure to add code blocks to your code group")]):e._e()],2)])}),[],!1,null,"131b8180",null);t.default=c.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js b/docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js deleted file mode 100644 index 0b6c6e453d..0000000000 --- a/docs-vuepress/.vuepress/dist/assets/js/100.f31ffd06.js +++ /dev/null @@ -1 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[100],{447:function(t,s,a){"use strict";a.r(s);var n=a(4),r=Object(n.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"网络请求"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#网络请求"}},[t._v("#")]),t._v(" 网络请求")]),t._v(" "),s("p",[t._v("Mpx 提供了网络请求库 fetch,抹平了微信,阿里等平台请求参数及响应数据的差异;同时支持请求拦截器,请求取消等")]),t._v(" "),s("h2",{attrs:{id:"使用说明"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#使用说明"}},[t._v("#")]),t._v(" 使用说明")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mpx "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/core'")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mpxFetch "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/fetch'")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("use")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("mpxFetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 第一种访问形式")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("then")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("res")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\tconsole"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("res"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("data"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("createApp")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("onLaunch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 第二种访问形式")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("this")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("$xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://test.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"导出说明"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#导出说明"}},[t._v("#")]),t._v(" 导出说明")]),t._v(" "),s("p",[t._v("mpx-fetch提供了一个实例 "),s("strong",[t._v("xfetch")]),t._v(" ,该实例包含以下api")]),t._v(" "),s("ul",[s("li",[t._v("fetch(config), 正常的promisify风格的请求方法")]),t._v(" "),s("li",[t._v("CancelToken,实例属性,用于创建一个取消请求的凭证。")]),t._v(" "),s("li",[t._v("interceptors,实例属性,用于添加拦截器,包含两个属性,request & response")])]),t._v(" "),s("h2",{attrs:{id:"请求拦截器"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#请求拦截器"}},[t._v("#")]),t._v(" 请求拦截器")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("interceptors"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("request"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("use")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("config")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\tconsole"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("config"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 也可以返回promise")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("return")]),t._v(" config\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("interceptors"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("response"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("use")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("res")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\tconsole"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("res"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 也可以返回promise")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("return")]),t._v(" res\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"请求中断"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#请求中断"}},[t._v("#")]),t._v(" 请求中断")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("const")]),t._v(" cancelToken "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("new")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token class-name"}},[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("CancelToken")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cancelToken")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" cancelToken"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("token\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\ncancelToken"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("exec")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'手动取消请求'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 执行后请求中断,返回abort fail")]),t._v("\n")])])]),s("h2",{attrs:{id:"设置请求参数"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#设置请求参数"}},[t._v("#")]),t._v(" 设置请求参数")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("params")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\nmpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("method")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'POST'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// params 参数等价于 url query")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("params")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("age")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("10")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 设置参数序列化方式,等价于header = {'content-type': 'application/x-www-form-urlencoded'}")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("emulateJSON")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"设置请求-timeout"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#设置请求-timeout"}},[t._v("#")]),t._v(" 设置请求 timeout")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("xfetch"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("method")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'POST'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n\t\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n\t"),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("timeout")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("10000")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 超时时间")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n")])])]),s("h2",{attrs:{id:"composition-api-usage"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#composition-api-usage"}},[t._v("#")]),t._v(" 在组合式 API 中使用 ")]),t._v(" "),s("p",[t._v("在组合式 API 中我们提供了 "),s("RouterLink",{attrs:{to:"/api/extend.html#usefetch"}},[t._v("useFetch")]),t._v(" 方法来访问 "),s("code",[t._v("xfetch")]),t._v(" 实例对象")],1),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// app.mpx")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mpx"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" createComponent "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/core'")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" useFetch "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'@mpxjs/fetch'")]),t._v("\n\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("createComponent")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("setup")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("useFetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("fetch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'http://xxx.com'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("method")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'POST'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("params")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("age")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("10")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("data")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("name")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'test'")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("emulateJSON")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("usePre")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cacheInvalidationTime")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("3000")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("ignorePreParamKeys")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'timestamp'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("then")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("res")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),t._v("res"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("data"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" \n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n\n")])])])])}),[],!1,null,null,null);s.default=r.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js b/docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js deleted file mode 100644 index ffc2ca40be..0000000000 --- a/docs-vuepress/.vuepress/dist/assets/js/101.283d0363.js +++ /dev/null @@ -1 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[101],{449:function(t,s,a){"use strict";a.r(s);var n=a(4),p=Object(n.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"扩展mpx"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#扩展mpx"}},[t._v("#")]),t._v(" 扩展mpx")]),t._v(" "),s("h2",{attrs:{id:"开发插件"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#开发插件"}},[t._v("#")]),t._v(" 开发插件")]),t._v(" "),s("p",[t._v("mpx支持使用mpx.use使用插件来进行扩展。插件本身需要提供一个install方法或本身是一个function,该函数接收一个proxyMPX。插件将采用直接在proxyMPX挂载新api属性或在prototype上挂属性。需要注意的是,一定要在app创建之前进行mpx.use。")]),t._v(" "),s("p",[t._v("简单示例如下:")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("install")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token parameter"}},[t._v("proxyMPX")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n proxyMPX"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function-variable function"}},[t._v("newApi")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'is new api'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n proxyMPX\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mixin")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("onLaunch")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'app onLaunch'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'app'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mixin")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("onShow")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'page onShow'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'page'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// proxyMPX.injectMixins === proxyMPX.mixin")]),t._v("\n\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 注意:proxyMPX.prototype上挂载的属性都将挂载到组件实例(page实例、app实例上,可以直接通过this访问), 可以看mixin中的case")]),t._v("\n proxyMPX"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("prototype"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function-variable function"}},[t._v("testHello")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("function")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n console"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("log")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'hello'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("h2",{attrs:{id:"目前已有插件"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#目前已有插件"}},[t._v("#")]),t._v(" 目前已有插件")]),t._v(" "),s("ul",[s("li",[s("p",[t._v("网络请求库fetch: @mpxjs/fetch "),s("a",{attrs:{href:"#fetch"}},[t._v("详细介绍")]),t._v(" "),s("a",{attrs:{href:"https://github.com/didi/mpx/tree/master/packages/fetch",target:"_blank",rel:"noopener noreferrer"}},[t._v("源码地址"),s("OutboundLink")],1)])]),t._v(" "),s("li",[s("p",[t._v("小程序API转换及promisify:@mpxjs/api-proxy "),s("a",{attrs:{href:"#api-proxy"}},[t._v("详细介绍")]),t._v(" "),s("a",{attrs:{href:"https://github.com/didi/mpx/tree/master/packages/api-proxy",target:"_blank",rel:"noopener noreferrer"}},[t._v("源码地址"),s("OutboundLink")],1)])]),t._v(" "),s("li",[s("p",[t._v("mock数据:@mpxjs/mock "),s("a",{attrs:{href:"#mock"}},[t._v("详细介绍")]),t._v(" "),s("a",{attrs:{href:"https://github.com/didi/mpx/tree/master/packages/mock",target:"_blank",rel:"noopener noreferrer"}},[t._v("源码地址"),s("OutboundLink")],1)])])])])}),[],!1,null,null,null);s.default=p.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js b/docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js deleted file mode 100644 index 317d5c68b3..0000000000 --- a/docs-vuepress/.vuepress/dist/assets/js/102.094a0a16.js +++ /dev/null @@ -1 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[102],{448:function(t,s,a){"use strict";a.r(s);var n=a(4),r=Object(n.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"数据-mock"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#数据-mock"}},[t._v("#")]),t._v(" 数据 Mock")]),t._v(" "),s("h2",{attrs:{id:"安装"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#安装"}},[t._v("#")]),t._v(" 安装")]),t._v(" "),s("p",[t._v("Mpx 提供了对请求响应数据进行拦截的 mock 插件,可通过如下命令进行安装:")]),t._v(" "),s("div",{staticClass:"language-sh extra-class"},[s("pre",{pre:!0,attrs:{class:"language-sh"}},[s("code",[s("span",{pre:!0,attrs:{class:"token function"}},[t._v("npm")]),t._v(" i @mpxjs/mock\n")])])]),s("h2",{attrs:{id:"使用说明"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#使用说明"}},[t._v("#")]),t._v(" 使用说明")]),t._v(" "),s("p",[t._v("新建 mock 文件目录及文件(例如:"),s("code",[t._v("src/mock/index.js")]),t._v(" ):")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// src/mock/index.js")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"list|1-10"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"id|+1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),s("p",[t._v("在入口文件( "),s("code",[t._v("app.mpx")]),t._v(" )中引入:")]),t._v(" "),s("div",{staticClass:"language-html extra-class"},[s("pre",{pre:!0,attrs:{class:"language-html"}},[s("code",[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("script")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token attr-name"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token attr-value"}},[s("span",{pre:!0,attrs:{class:"token punctuation attr-equals"}},[t._v("=")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')]),t._v("text/javascript"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),s("span",{pre:!0,attrs:{class:"token script"}},[s("span",{pre:!0,attrs:{class:"token language-javascript"}},[t._v("\n "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"mock/index"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 引入mock即可")]),t._v("\n")])]),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("\x3c!-- 其他配置 --\x3e")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("script")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token attr-name"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token attr-value"}},[s("span",{pre:!0,attrs:{class:"token punctuation attr-equals"}},[t._v("=")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')]),t._v("application/json"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),s("span",{pre:!0,attrs:{class:"token script"}},[s("span",{pre:!0,attrs:{class:"token language-javascript"}},[t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"pages"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"./pages/index"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"window"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"backgroundTextStyle"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"light"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"navigationBarBackgroundColor"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"#fff"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"navigationBarTitleText"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"WeChat"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"navigationBarTextStyle"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"black"')]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])]),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])]),s("blockquote",[s("p",[t._v("由于 mock 为全局自动代理,执行"),s("code",[t._v("@mpxjs/mock")]),t._v("所暴露的方法之后会立即拦截小程序的原生请求,如果需要根据不同环境变量等去控制是否使用 mock 数据,可以参考如下方法:")])]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// src/mock/index.js")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("export")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("default")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=>")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"list|1-10"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"id|+1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])])]),s("div",{staticClass:"language-html extra-class"},[s("pre",{pre:!0,attrs:{class:"language-html"}},[s("code",[s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("\x3c!-- app.mpx --\x3e")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("<")]),t._v("script")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token attr-name"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token attr-value"}},[s("span",{pre:!0,attrs:{class:"token punctuation attr-equals"}},[t._v("=")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')]),t._v("text/javascript"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v('"')])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(">")])]),s("span",{pre:!0,attrs:{class:"token script"}},[s("span",{pre:!0,attrs:{class:"token language-javascript"}},[t._v("\n "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mockSetup "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"mock/index"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 当为开发环境时才启用mock")]),t._v("\n process"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("env"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),s("span",{pre:!0,attrs:{class:"token constant"}},[t._v("NODE_ENV")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("===")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"development"')]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("&&")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mockSetup")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n")])]),s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token tag"}},[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("")])]),t._v("\n")])])]),s("h2",{attrs:{id:"mock-入参"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#mock-入参"}},[t._v("#")]),t._v(" Mock 入参")]),t._v(" "),s("p",[s("code",[t._v("@mpxjs/mock")]),t._v(" 所暴露的函数仅接收一个类型为 "),s("code",[t._v("mockRequstList")]),t._v(" 的参数,该类型定义如下:")]),t._v(" "),s("div",{staticClass:"language-ts extra-class"},[s("pre",{pre:!0,attrs:{class:"language-ts"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("type")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token class-name"}},[t._v("mockItem")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n url"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token builtin"}},[t._v("string")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n rule"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" object\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("type")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token class-name"}},[t._v("mockRequstList")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token builtin"}},[t._v("Array")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("<")]),t._v("mockItem"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(">")]),t._v("\n\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("//示例:")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("let")]),t._v(" mockList"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" mockRequstList "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n url"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求触发后匹配到该链接时其响应数据会被mock拦截")]),t._v("\n rule"),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// mock生成返回数据的规则")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v("'number|1-10'")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n")])])]),s("h2",{attrs:{id:"mock-规则示例"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#mock-规则示例"}},[t._v("#")]),t._v(" Mock 规则示例")]),t._v(" "),s("ul",[s("li",[t._v("基本类型数据生成")])]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"number|1-10"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("1")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 随机生成1-10中的任意整数")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"string|6"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token regex"}},[s("span",{pre:!0,attrs:{class:"token regex-delimiter"}},[t._v("/")]),s("span",{pre:!0,attrs:{class:"token regex-source language-regex"}},[t._v("[0-9a-f]")]),s("span",{pre:!0,attrs:{class:"token regex-delimiter"}},[t._v("/")])]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 值支持正则表达式,随机生成6位的16进制值")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"boolean|1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token boolean"}},[t._v("true")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 随机生成一个布尔值,值为 true 的概率是 1/2")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求 http://api.example.com 后返回值为:")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// {")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// number: 2,")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// string: "e1e6dc",')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// boolean: false")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n")])])]),s("ul",[s("li",[t._v("生成随机长度id自增的列表")])]),t._v(" "),s("details",{staticClass:"custom-block details"},[s("summary",[t._v("查看示例")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"list|2-5"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 生成长度范围在2-5的数组")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"id|+1"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token number"}},[t._v("0")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// id每次自增1")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求 http://api.example.com 后返回")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// {")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "list": [{')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "id": 0')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// },{")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "id": 1')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// },{")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "id": 2')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }]")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n")])])])]),t._v(" "),s("ul",[s("li",[t._v("pick对象中的随机个值")])]),t._v(" "),s("details",{staticClass:"custom-block details"},[s("summary",[t._v("查看示例")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("import")]),t._v(" mock "),s("span",{pre:!0,attrs:{class:"token keyword"}},[t._v("from")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"@mpxjs/mock"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("mock")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("url")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"http://api.example.com"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("rule")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"object|2"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 随机选取object中的两条数据作为返回")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"310000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"上海市"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"320000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"江苏省"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"330000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"浙江省"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token string-property property"}},[t._v('"340000"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"安徽省"')]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 请求 http://api.example.com 后返回")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// {")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "object": {')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "330000": "浙江省",')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v('// "340000": "安徽省"')]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// }")]),t._v("\n")])])])]),t._v(" "),s("p",[t._v("更多生成规则可查阅 "),s("a",{attrs:{href:"https://github.com/nuysoft/Mock/wiki/Syntax-Specification",target:"_blank",rel:"noopener noreferrer"}},[t._v("Mock官方文档-Syntax Specification"),s("OutboundLink")],1)]),t._v(" "),s("p",[t._v("更多示例可查看 "),s("a",{attrs:{href:"http://mockjs.com/examples.html",target:"_blank",rel:"noopener noreferrer"}},[t._v("Mock示例"),s("OutboundLink")],1)]),t._v(" "),s("div",{staticClass:"custom-block warning"},[s("p",{staticClass:"custom-block-title"},[t._v("WARNING")]),t._v(" "),s("p",[t._v("由于小程序环境的局限性,mockjs 依赖 eval 函数实现的相关能力(如:占位符)无法正确运行")])])])}),[],!1,null,null,null);s.default=r.exports}}]); \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js b/docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js deleted file mode 100644 index 927c61ed86..0000000000 --- a/docs-vuepress/.vuepress/dist/assets/js/103.b34f6752.js +++ /dev/null @@ -1 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[103],{451:function(t,s,a){"use strict";a.r(s);var e=a(4),n=Object(e.a)({},(function(){var t=this,s=t._self._c;return s("ContentSlotsDistributor",{attrs:{"slot-key":t.$parent.slotKey}},[s("h1",{attrs:{id:"从旧版本迁移至-2-7"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#从旧版本迁移至-2-7"}},[t._v("#")]),t._v(" 从旧版本迁移至 2.7")]),t._v(" "),s("p",[t._v("自 2.7 版本开始,Mpx 将编译构建流程基于 Webpack5 进行了"),s("strong",[t._v("全面的重构")]),t._v(",优化解决了老的编译流程中存在的大量历史遗留问题,安全完整地支持了基于文件系统的持久化缓存,大幅提升了 Mpx 想的编译构建速度,以滴滴出行小程序为例,无缓存场景下相较 2.6 版本提速约 180%,有缓存场景下提速约 "),s("strong",[t._v("10")]),t._v(" 倍。")]),t._v(" "),s("p",[t._v("与此同时,Mpx 2.7 版本还带来了 rules 复用,完善的单元测试支持,独立分包构建,分包异步化等众多新特性。直接通过 @mpxjs/cli 创建的新项目就能够直接使用这些新特性,如果你对于编译构建没有太多定制化诉求,我们也推荐使用 @mpxjs/cli 新建项目,并将老项目中的 src 目录 copy 覆盖至新项目的方式进行迁移,这是一种成本最低的迁移方式。")]),t._v(" "),s("p",[t._v("如果你的老项目中已经对编译构建进行了高度的定制,本文将提供详细的迁移工作指引。")]),t._v(" "),s("h2",{attrs:{id:"依赖升级"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#依赖升级"}},[t._v("#")]),t._v(" 依赖升级")]),t._v(" "),s("p",[t._v("将下面列出的相关依赖升级至对应版本,如果有其他自行引入的 loader 也确保将其升级至 webpack5 兼容的版本:")]),t._v(" "),s("div",{staticClass:"language-json5 extra-class"},[s("pre",{pre:!0,attrs:{class:"language-json5"}},[s("code",[s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"dependencies"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/api-proxy"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/core"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/fetch"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"devDependencies"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"@mpxjs/webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.7.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"webpack"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.64.4"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"webpack-merge"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.8.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"terser-webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.2.5"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"copy-webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^9.0.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"webpack-bundle-analyzer"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^4.5.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"ts-loader"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^9.2.6"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// eslint相关配置,按需安装")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^7.32.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-config-standard"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^16.0.3"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-html"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^6.2.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-import"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^2.25.2"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-node"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^11.1.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-plugin-promise"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^5.1.1"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token property"}},[t._v('"eslint-webpack-plugin"')]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v('"^3.1.0"')]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("h2",{attrs:{id:"开启持久化缓存"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#开启持久化缓存"}},[t._v("#")]),t._v(" 开启持久化缓存")]),t._v(" "),s("p",[t._v("在 webpack 配置中添加以下配置项:")]),t._v(" "),s("div",{staticClass:"language-js extra-class"},[s("pre",{pre:!0,attrs:{class:"language-js"}},[s("code",[t._v("module"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(".")]),t._v("exports "),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v("=")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cache")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("type")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'filesystem'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 声明构建配置,注意如果声明某个文件夹为构建配置,需要在文件夹下放置空的package.json文件,避免构建依赖收集时将主项目的依赖项视为构建依赖")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("buildDependencies")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("build")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'build/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("config")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'config/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("cacheDirectory")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'.cache/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("snapshot")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token comment"}},[t._v("// 如果希望修改node_modules下的文件时对应的缓存可以失效,可以将此处的配置改为 managedPaths: []")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token literal-property property"}},[t._v("managedPaths")]),s("span",{pre:!0,attrs:{class:"token operator"}},[t._v(":")]),t._v(" "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("[")]),s("span",{pre:!0,attrs:{class:"token function"}},[t._v("resolve")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("(")]),s("span",{pre:!0,attrs:{class:"token string"}},[t._v("'node_modules/'")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(")")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("]")]),t._v("\n "),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v(",")]),t._v("\n"),s("span",{pre:!0,attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])])]),s("h2",{attrs:{id:"修改rules"}},[s("a",{staticClass:"header-anchor",attrs:{href:"#修改rules"}},[t._v("#")]),t._v(" 修改rules")]),t._v(" "),s("p",[t._v("在 2.7 版本中,我们优化了 .mpx 文件中各个 block 应用 loader 的规则。")]),t._v(" "),s("p",[t._v('在之前的版本中,我们基本上使用内置的规则对 .mpx 中的 block 应用 loaders,如:对 - - - - - - -

# 文档指南

该文档使用vuepress生成

# 该项目指南

已安装依赖vuepress,并已经在package.json中新增写作和部署脚本

# 写作时
-npm run docs:dev
-
-# 部署时
-npm run docs:build
-# 将在docs-vuepress/.vuepress/dist下生成静态文件,部署到相应服务器即可,部署参考下方链接
-

# 编写格式指南

  • 对于引用的文档或资料使用链接指明出处
  • 中英文混排时为了阅读体验使用空格对于英文单词进行包裹,在标点符号边缘或句首时可省略该侧空格,例如:这是一个 word 而不是 world。
  • 句首的英文单词首字母大写
  • Mpx 正确的写法为 Mpx,首字母大写,任何地方都应该这样书写,其余的专业名称参考原始的写法进行书写
  • 除英文段落外,所有标点符号使用中文半角
  • 能使用示例代码描述的部分都尽量添加示例代码进行说明,show me the code
  • 对于文档内容拓展说明的部分使用>引用的方式进行编写,like this

我们来聊聊人生吧,这个地方你可以不看但没必要

# 参考

- - - diff --git a/docs-vuepress/.vuepress/dist/favicon.ico b/docs-vuepress/.vuepress/dist/favicon.ico deleted file mode 100644 index 891c2ce0106d2e229eaa160a87a6f4a63e4bd519..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9662 zcmZQzU<5(~0|p?ez_3DDhwLF^z z*=!hl)+wWuroBvmDRFz}L4n(cAGBi)qjM*YaOB1M?WEKXrKu59VQz-eOU}DnwCv~j zPKn!F_j7(;c`4WgW+paz`P``zz;tk%54vX(R^JE4GiUSF7ZeF>-P?Z%slae?9>CGrm z{ok;g{PL#zkl6DL*AlSiR{~yO*tBX9TYg-?QOf*a@jt&^imdXo{Q%GJ#pm732)PrV ze1B89SZ27#ON#v9ZT`P(Ev{)}Iit=-Eg6{wN9ZUrM(0f?&=+0Y(Y^1o47_~)7Ar$B5 z%wpyL4SR6s=gxzI&*z=70d*m%4rcYWuoWZ(ou?!n#duo$FYK1U>W9|-96y&|^mm}T z!;uV}(bFWC9_sd*B0m80bNK>3v~%CC7yR?&>vRa5Ac;Ai)f5p2>vuB#?>i!We(41ds^?cM&PEX`P7d8oNgROmmNaYs z-F!Vw3B_PKaHh1^>LmKxzo*3gv3_R%t5$IRpMFBOkq%CVTd{rp3X!xB*Q=Dc9aIjL zOce&F|K@$Hf9IdI3xHcdL&l3IkJt+012{1jUiQG+*T6o`<+D%ch|tOpjm2qRz&<`j?VyBk!~eCLSuorV^TWiW zifd`%eqer81NO-%s<&c&&Hh&|<;LlDm>;H}&?%#)A674%$&(r3d6BYoWAVRuq9{t5 zhq)J>2KIG-E{+<54zFZPP}r7*vS8vU=`$V#%?VSb!?Tyy1yYw;v| zp}90e1~?8xQGNxs@&4B?l)ejd3wbo-|7j<*b4c+(XLUYDc9hRqO4EF8USjjgOF>#4 z2lzje=XOXxr)3}e&v|F8?TGQiig{BRfcbSHrEV`w2;O+%^fB-_D6nl1)UuC_!f{W* z*Ne`%ND=Y_FwF8O?dN8Mdt6^KYoZXcUDHqKwNerXAicnL_Stn;VsMUQ0n?2Gu#Tdr zeoYB-{tO&{22FpWfGaOnu=F31-At(;fcbg;kp~?pW`j8WO%+nWI_fPY=?2)g_zR3b zobAYs*OG*Q{qviY_yIWn{vVhZQ1UZq{-roM4AiE4ObM>ej_bq-7&L*|r%W|k_jBC` z`r$FTxa|P<{aL4s>E7o-^U5f9Gz3ONU^E1VbO`(h0|o|C=?4t_9~hV!7#=XNe_&u| zU;uJ}LJSN)7?=+*$TKkfVPI?k(f>f9V-Kb4f%bs-K;nM`a9t9JJ^-a3FfcHK_&}N+ zM1Npl;0Mt^7#QS1^dBeq{j0}T8?DWDN#h9dx^v_XFW diff --git a/docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html b/docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html deleted file mode 100644 index 8e7c1007d4..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/ability-compatible.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - 原生能力兼容 | Mpx框架 - - - - - - - - - -

# 原生能力兼容

# custom-tab-bar

Mpx 支持微信小程序原生自定义 tabbar ,关于自定义 tabbar 的详情请查看这里 (opens new window)

在 Mpx 中使用自定义 tabbar ,需要在 app.mpx 的 json 部分的 tabBar 配置里的 custom 为 true 。

// app.mpx
-<script type="application/json">
-{
-  "tabBar": {
-    "custom": true,
-    "color": "#000000",
-    "selectedColor": "#000000",
-    "backgroundColor": "#000000",
-    "list": [{
-      "pagePath": "./page/component/index",
-      "text": "组件"
-    }, {
-      "pagePath": "./page/API/index",
-      "text": "接口"
-    }]
-  },
-  "usingComponents": {}
-}
-</script>
-

在 src 目录下创建 custom-tab-bar 目录,包含 index.mpx 文件,可以在该 index.mpx 文件中编写自定义 tabbar 的模板、js、样式和 json 部分,同时也支持原生写法。

# workers

Mpx 完全支持小程序原生的 worker ,需要在 app.mpx 文件中的 json 部分声明 worker 的目录,Mpx 会将其对应目录进行打包,输出到目标代码目录中。

<script type="application/json">
-{
-  // 指定 worker 的目录
-  "workers": "workers"
-}
-</script>
-

更多详情可查看这里 (opens new window)

# 云开发

Mpx 支持微信小程序提供的原生云开发能力。如果需要在项目中使用云开发的能力,可以通过 Mpx 脚手架工具在初始化项目时选择支持云开发。如果需要支持云开发能力,在项目初始化时需要选择是微信平台下,且不能支持跨平台开发。如下图所示:

云开发

更多关于云开发相关可查看这里 (opens new window)

# useExtendedLib

Mpx 对于引入扩展库做了相关处理,可以在 app.mpx 中配置使用的扩展库,目前支持 weui 。

<script type="application/json">
-  {
-    "useExtendedLib": {
-      "weui": true
-    }
-  }
-</script>
-

还需要在 Mpx 的配置文件配置 externals 属性,来指定外部依赖,这样 Mpx 在进行打包时,会将其当做外部依赖进行打包。

module.exports = {
-  // ...
-  externals: [ 'weui' ]
-}
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html b/docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html deleted file mode 100644 index 36fb075f5e..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/async-subpackage.html +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - 分包异步化 | Mpx框架 - - - - - - - - - -

# 分包异步化

在小程序中,不同的分包对应不同的下载单元;因此,除了非独立分包可以依赖主包外,分包之间不能互相使用自定义组件或进行 require。 -「分包异步化」特性将允许通过一些配置和新的接口,使部分跨分包的内容可以等待下载后异步使用,从而一定程度上解决这个限制。

具体功能介绍和功能目的可 点击查看 (opens new window), Mpx对于分包异步化功能进行了完整支持。

当前 Mpx 框架默认支持以下平台的分包异步化能力:

  • 微信小程序
  • 支付宝小程序
  • 字节小程序
  • Web

在非上述平台,异步分包代码会默认降级。

# 跨分包自定义组件引用

一个分包使用其他分包的自定义组件时,由于其他分包还未下载或注入,其他分包的组件处于不可用的状态。通过为其他分包的自定义组件设置 占位组件, -我们可以先渲染占位组件作为替代,在分包下载完成后再进行替换。

在 Mpx 中使用跨分包自定义组件引用通过?root声明组件所属异步分包即可使用,示例如下:

<!--/packageA/pages/index.mpx-->
-// 这里在分包packageA中即可异步使用分包packageB中的hello组件
-<script type="application/json">
-  {
-    "usingComponents": {
-      "hello": "../../packageB/components/hello?root=packageB",
-      "simple-hello": "../components/hello"
-    },
-    "componentPlaceholder": {
-      "hello": "simple-hello"
-    }
-  }
-</script>
-
  • 注意项:目前该能力已在微信、支付宝和Web环境下支持,其他环境下框架将进行自动降级。

# 跨分包 JS 代码引用

一个分包中的代码引用其它分包的代码时,为了不让下载阻塞代码运行,我们需要异步获取引用的结果

在 Mpx 中跨分包异步引用 JS 代码时,需要在引用的 JS 路径后拼接 JS 模块所在的分包名,此外由于 require 不能传入多个参数的限制,在Mpx中无法使用回调函数的 -风格跨分包 JS 代码引用,只能使用 Promise 风格。

示例如下:

// subPackageA/index.js
-// 或者使用 Promise 风格的调用
-require.async('../commonPackage/index.js?root=subPackageB').then(pkg => {
-  pkg.getPackageName() // 'common'
-})
-
  • 注意项:目前该能力仅微信平台下支持,其他平台下框架将会自动降级

# 跨分包 Store 引用

在 Mpx 中如果想要跨分包异步引用 Store 代码,分为三个步骤

  • 页面或父组件在 created 钩子加载异步 Store
  • 异步 Store 加载完成后再渲染使用异步 Store 的组件
  • 子组件在框架内部生命周期 BEFORECREATE 钩子中动态注入 computed 和 methods
<!--pages/index/index.mpx-->
-<template>
-  <store-list wx:if="{{showStoreList}}"></store-list>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-  createPage({
-    data: {
-      showStoreList: false
-    },
-    created () {
-      require.async('../subpackages/sub2/store?root=sub2').then(store => {
-        getApp().asyncStore.sub2 = store.default
-        // 当异步 Store 加载完成后再渲染使用异步 Store 的组件
-        this.showStoreList = true
-      })
-    }
-  })
-</script>
-
-<!-- 子组件:store-list -->
-<script>
-  import { createComponent, BEFORECREATE } from '@mpxjs/core'
-  createComponent({
-    // 在 BEFORECREATE 钩子中动态注入 options
-    [BEFORECREATE] () {
-      // 获取异步 Store实例
-      const subStore = getApp().asyncStore.sub2
-      // computed 中 mapState、mapGetters 替换为 mapStateToInstance、mapGettersToInstance,最后一个参数必须传当前 component 实例 this
-      subStore.mapStateToInstance(['pagename'], this)
-      subStore.mapGettersToInstance(['pageDataGetter'], this)
-      // methods 中 mapActions、mapMutations 替换为 mapMutationsToInstance、mapActionsToInstance,最后一个参数必须传当前 component 实例 this
-      subStore.mapMutationsToInstance(['updatePageData'], this)
-      subStore.mapActionsToInstance(['updatePageName'], this)
-    }
-  })
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html b/docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html deleted file mode 100644 index 58a1f1c010..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/custom-output-path.html +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - 自定义路径 | Mpx框架 - - - - - - - - - -

# 自定义路径

Mpx在构建时,如果引用的页面不存在于当前 app.mpx 所在的上下文中,例如存在于 npm 包中,为避免和本地声明的其他 page 路径冲突,Mpx 会对页面路径进行 hash 化处理, -同时处理组件路径时也会添加 hash 防止路径名冲突,hash 化处理后最终的文件名是 name+hash+ext 的格式

与此同时,部分开发者希望能够对最终输出的页面路径和组件路径能够自定义,Mpx 对此也提供了相应的配置和Api来支持用户自定义路径

# 自定义页面路径

针对 Mpx 对 json 中非当前上下文的页面路径 hash 化的特性,如果为提升可读性需要避免路径 hash 化,可以使用该特性,具体为在 -json 中配置 pages 时,数组中支持放入 Object,对象中传入两个字段,src 字段表示页面地址,path 字段表示自定义页面路径

  • 示例:

object风格的页面声明

{
-  // 主包中的声明
-  "pages": [
-    {
-      "src": "@someGroup/someNpmPackage/pages/view/index.mpx",
-      "path": "pages/somNpmPackage/index" // 注意保持 path 的唯一性
-    }
-  ],
-  // 分包中的声明
-  "subPackages": [
-    {
-      "root": "test",
-      "pages": [
-         {
-           "src": "@someGroup/someNpmPackage/pages/view/test.mpx",
-           "path": "pages/somNpmPackage/test" // 注意保持 path 的唯一性
-         }
-      ]
-    }
-  ]
-}
-

使用声明中配置的页面路径进行跳转

mpx.navigateTo({
-  url: '/pages/somNpmPackage/index'
-})
-
-mpx.navigateTo({
-  url: '/test/pages/somNpmPackage/test'
-})
-

# customOutputPath

Mpx 框架 webpack-plugin 也提供了 customOutputPath 方法可以让用户进行页面和组件路径的自定义, -可使用该方法对非原生组件和非当前文件context的页面输出路径进行自定义

需要注意的是,该方法需要具有稳定性和唯一性,即同样的输入不管什么时候执行都要有同样的返回以及不同的的输入一定会得到不同的输出。

  • 示例
// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-         customOutputPath: (type, name, hash, ext, resourcePath) => {
-          // type: 资源类型(page | component | static)
-          // name: 资源原有文件名
-          // hash: 8位长度的hash串
-          // ext: 文件后缀(.js| .wxml | .json 等)
-          // resourcePath: 资源路径
-
-          // 输出示例: pages/testax34dde3/index.js
-          return path.join(type + 's', name + hash, 'index' + ext)
-        }
-      }
-    }
-  }
-})
-

基于上方示例,你可以根据需要进行路径定制化,例如缩短hash、使用012代替文件名等各种自定义路径

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html b/docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html deleted file mode 100644 index ff5a026123..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/dll-plugin.html +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - 使用 DllPlugin | Mpx框架 - - - - - - - - - -

# 使用 DllPlugin

# 相关插件简述

# DllPlugin

DllPlugin 和 DllReferencePlugin 用某种方法实现了拆分 bundles,同时还大大提升了构建的速度,这个插件是在一个额外的独立的 webpack 设置中创建一个只有 dll 的 bundle(dll-only-bundle)。并且会生成一个名为 manifest.json的文件,这个文件是用来让 DLLReferencePlugin 映射到相关的依赖上去的。

DllPlugin 用来将我们某些长时间不变更的资源,独立拆分出去,之后项目的每次构建这部分资源都不需要重新编译,而是直接通过 DLLReferencePlugin 引入 dll bundle, 大大的节省了构建时间。

构建生成对应的 dll bundle 和 manifest.json 文件,manifest 文件中是模块 id 与请求路径的关系映射。

# DllReferencePlugin

这个插件是在 webpack 主配置文件中设置的, 这个插件把只有 dll 的 bundle(们)(dll-only-bundle(s)) 引用到需要的预编译的依赖

通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 webpack_require 函数来 require 他们。

# Mpx中 DllPlugin 的使用

1.通过 @mpxjs/cli init 生成项目

初始化选项中选择使用dll功能。

2.在项目中 build/dll.config.js 进行 dll 文件配置

const path = require('path')
-
-// 这里是一个用法示例
-function resolve (file) {
-  return path.resolve(__dirname, '..', file || '')
-}
-module.exports = [
-  {
-    cacheGroups: [
-      {
-        entries: [resolve('node_modules/@someNpmGroup/someNpmPkgName/dist/wx/static/js/common.js')],
-        name: 'test',
-        root: 'testSomeDllFile'
-      },
-    ],
-    modes: ['wx'],
-    entryOnly: true,
-    format: true,
-    webpackCfg: {
-      mode: 'none', // 不使用任何默认优化选项
-    }
-  },
-  {
-    cacheGroups: [
-      {
-        entries: [resolve('node_modules/@someNpmGroup/someNpmPkgName/dist/ali/static/js/common.js')],
-        name: 'test',
-        root: 'testSomeDllFile'
-      },
-    ],
-    modes: ['ali'],
-    entryOnly: true,
-    format: true,
-    webpackCfg: {
-      mode: 'none', // 不使用任何默认优化选项
-    }
-  }
-
-]
-

DllPlugin 的配置项详见文档 (opens new window),Mpx 中相关配置项依据小程序环境做了相关调整。

  • cacheGroups

    • 类型: Array<object>
      • entries -
        • 类型: Arraydll

          构建入 dll 的文件入口

      • name -
        • 类型: String

          生成的 dll 文件名

      • root -
        • 类型: String

          生成的 dll 文件夹名

  • modes

    • 类型: Array

      构建 dll 产物对应的平台

      // 配置多平台输出,例如:
      -[
      -  'wx', // 微信平台
      -  'ali' // 支付宝平台
      -]
      -
  • entryOnly

    • 类型: Boolean

      如果为true,则仅暴露入口

      这里建立使用entryOnly: true 配置

      如果为 false 时,dllPlugin 中的 tree shakeing 功能就不再起作用

      另外如果为false,如果是将分包资源打入dll bundle,会存在将全局方法打入分包 dll 中的可能性,这样主包在使用该方法被映射到 dll bundle 中时,会因为分包未加载而报错

  • webpackCfg

    • 类型: Object

      构建 dll 时可添加其他的 webpack 配置

  • format

    • 类型: Boolean

      生成的 manifest json 文件 是否进行格式化

# Mpx 中对 DllPlugin 配置所做的相关处理

正常使用 DllPlugin 是单独创建一个 webpack 配置文件,配置文件中加入 DllPlugin,然后运行 webpack 编译构建生成 dll 文件。

考虑到 Mpx 在编译时需要跨平台输出,所以 Mpx 的配置项是 Array<object> 类型,同时增加了 modes 配置项,可以自主控制输出不同平台版本 dll 文件。

构建生成 dll bundle 的主要逻辑在 buildDll.js 中,通过对 dll.config 中数组的循环处理,生成 webpackCfgs 数组。

dllConfigs.forEach((dllConfig) => {
-  const entries = getDllEntries(dllConfig.cacheGroups, dllConfig.modes)
-  // 根据配置的 mode 以及 cacheGroups 生成 entry,结果例如:
-  /**
-  *{
-        'somePkgRoot/wx.somePkgName': [
-            '/Users/didi/didiProject/mp-apphome/node_modules/@someNpmGroup/somNpmName/dist/wx/static/js/common.js'
-        ]
-    }
-  **/
-  if (Object.keys(entries).length) {
-    webpackCfgs.push(merge({
-      entry: entries,
-      output: {
-        path: config.dllPath,
-        filename: path.join('lib', dllName),
-        libraryTarget: 'commonjs2'
-      },
-      mode: 'production',
-      plugins: [
-        new webpack.DllPlugin({
-          path: path.join(config.dllPath, manifestName),
-          format: dllConfig.format,
-          entryOnly: dllConfig.entryOnly,
-          name: dllName,
-          type: 'commonjs2',
-          context: config.context
-        })
-      ]
-    }, dllConfig.webpackCfg))
-  }
-})
-

生成的 webpackCfgs 数组配置项,传入 webpack 执行,最终在 dll 文件夹下生成 dll bundle 和 manifest.json 文件

之后在 build.js 中使用 DllReferencePlugin 将编译中的依赖项与 dll bundle 的模块 id 关联起来,这里我们通过多个 DllReferencePlugin 实例来将可能存在的多个 manifest 文件关联引入,具体在项目 build.js 文件中:

plugins.push(new webpack.DllReferencePlugin({
-    context: config.context, //(绝对路径) manifest (或者是内容属性)中请求的上下文
-    manifest: manifest.content // 请求到模块 id 的映射(默认值为 manifest.content)
-}))
-

DllReferencePlugin 的其他配置项详见文档 (opens new window)

# 总结

综上所述,在 Mpx 中使用 dllPlugin 时,只需要进行 build/dll.config.js 文件的配置,然后通过 build:dll 命令生成 dll bundle,之后就可以正常的进行代码的 build 了。不过每次 build 需要检查下项目中使用的 npm 包版本与 dll bundle 中的 npm 包版本是否一致,避免因为包版本的滞后更新导致线上 bug,这里我们后续也会提供相应的包版本比对插件。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/i18n.html b/docs-vuepress/.vuepress/dist/guide/advance/i18n.html deleted file mode 100644 index 207d91a34b..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/i18n.html +++ /dev/null @@ -1,276 +0,0 @@ - - - - - - 国际化i18n | Mpx框架 - - - - - - - - - -

# 国际化i18n

Mpx 支持国际化 i18n,使用方式及支持能力与 vue-i18n 非常接近。

Mpx 自带 i18n 能力,无需额外安装插件。由于小程序模板中的 i18n 函数是通过 wxs 编译注入进行实现,我们需要将 i18n 配置传入到 MpxWebpackPlugin 中来使 i18n 生效,这是与 vue-i18n 最大的区别。

# 开启 i18n

I18n 配置传入到 MpxWebpackPlugin 选项中即可生效,额外支持 messagesPath 配置,通过模块路径传入语言集,其余配置参考 vue-i18n。

由于小程序的双线程特性,在选项式 API 场景下,默认情况下模板中调用的 i18n 函数由 wxs 实现,而 js 中调用的 i18n 函数由 js 实现,该设计能够将视图层和逻辑层之间的通信开销降至最低,得到最优的性能表现,但是由于 wxs 和 js 之间无法共享数据,在最终的编译产物中语言集会同时存在于 js 和 wxs 当中,对包体积产生负面影响。

为了平衡上述影响,自2.6.56版本之后我们新增了编译配置项 i18n.useComputed ,改配置项开启的情况下对于模板中的 i18n 调用将不再使用 wxs 实现,而是通过在 computed 进行实现,语言集将只存在于 js 逻辑层当中,对于节省了包体积的同时双线程通信成本也会增加,用时间换空间,具体是否开启可以根据实际项目的使用情况及资源瓶颈由开发者自行决定, -另外需要注意的是,在 Mpx 组合式 API 场景下 i18n 仅支持通过 computed 实现

开启 i18n.useComputed 配置时,由于 computed 技术架构的限制,i18n 函数无法对模板循环渲染中的 itemindex 生效。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        i18n: {
-          locale: 'en-US',
-          // messages既可以通过对象字面量传入,也可以通过messagesPath指定一个js模块路径,在该模块中定义配置并导出,dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理
-          messages: {
-            'en-US': {
-              message: {
-                hello: '{msg} world'
-              }
-            },
-            'zh-CN': {
-              message: {
-                hello: '{msg} 世界'
-              }
-            }
-          },
-          useComputed: false // 默认 false,  此开关将模板中的 i18n 函数注入 computed,在包体积空间紧张的情况下可以使用
-          // messagesPath: path.resolve(__dirname, '../src/i18n.js')
-        }
-      }
-    }
-  }
-})
-

# 选项式 API 中使用

同 vue-i18n,在组件中直接调用翻译函数使用,由于 wxs 执行环境的限制,目前 js 中支持了 vue-i18n 中 $t/$tc/$te 翻译函数,$d/$n 暂不支持,详细使用方法可参考 vue-i18n。

此外类似于 vue-i18n,在组件模板的 Mustache 插值中直接调用翻译函数。

<template>
-    <view>
-        <view wx:if="{{isMessageExist}}">{{ $t('message.hello', { msg: 'hello' }) }}</view>
-    </view>
-</template>
-<script>
-    createComponent({
-        ready () {
-            console.log(this.$t('message.hello', { msg: 'hello' }))
-        },
-        computed: {
-            isMessageExist () {
-                return this.$te('message.hello')
-            }
-        }
-    })
-</script>
-

# 在组合式 API 中使用

在组合式 API setup 中,this 不是该活跃实例的引用,为了继续使用 i18n 的相关能力,我们需要一个新的方法来替代 this -,这里 Mpx 提供 useI18n 方法支持对 i18n 相关功能的使用。

# 开始使用

同 vue-i18n, useI18n 返回一个 i18n 实例,该实例提供文案翻译 API 例如 t 方法,相较于选项式 API 中的翻译方法,在组合式 API 中有部分变化。

  1. 翻译方法 $t/t 和 $tc/tc,在组合式 API 中统一使用翻译方法 t
  2. 翻译方法 $te/te,在组合式 API 中统一使用 te
  3. 翻译方法 $tm/tm,在组合式 API 中统一使用 tm

useI18n 方法的执行必须在 setup 中的顶层。

i18n 同时也支持传入参数,例如 localefallbackLocale, -关于实例的更多信息可移步API章节 查看

在不给 useI18n 传入任何参数时,i18n 实例的上下文将是全局作用域,即翻译函数 t 引用的文案来源是我们在 MpxWebpackPlugin -中配置的文案。

通过在 setup 中返回翻译函数 t,我们可以在模版中直接使用它:

<template>
-    <view>{{t("message.hello")}}</view>
-</template>
-
import {createComponent, useI18n} from '@mpxjs/core'
-
-createComponent({
-  setup(props, context) {
-    const { t } = useI18n() // 从返回中解构出 t 方法
-    return {
-      t
-    } // 将 t 方法返回挂载到渲染实例
-  }
-})
-

注意事项:

组合式 API 中,Mpx 在编译时对模版进行 ast 遍历分析,检测到对应的翻译函数 t/tc/te 后,将其转换为 computed 方法注入,computed 中调用 setup 中返回的 t 方法进行文案获取,由此引申出来两个注意项:

1.不可对从 useI18n 中解构出的 t 方法进行重命名,否则模版中将无法正确获取文案。

下方即为错误示例:

<template>
-    <view>{{t1('message.hello')}}</view>
-</template>
-<script>
-    import {createComponent, useI18n} from '@mpxjs/core'
-    createComponent({
-        setup() {
-            const {t: t1} = useI18n()
-            return {
-                t1
-            }
-        }
-    })
-</script>
-

2.模版中的 t/tc/te 方法都是使用 computed 实现,由于 computed 技术架构的限制,i18n 函数无法对模板循环渲染中的 itemindex 生效。

例如下方示例将会报错 computed 中找不到 item

<template>
-    <!--tc方法使用 computed 实现,最终将无法正确获取item-->
-    <view wx:for="{{listData}}" wx:key="index">{{tc('message.hello', {msg: item})}}</view>
-</template>
-<script>
-    import {createComponent, ref, useI18n} from '@mpxjs/core'
-    createComponent({
-        setup() {
-            const listData = ref([1,2,3])
-            const {tc} = useI18n()
-            return {
-                tc,
-                listData
-            }
-        }
-    })
-</script>
-

# 作用域

在 Mpx 组合式 API 中,useI18n 返回的 i18n 实例默认是指向全局作用域,文案来源即为 MpxWebpackPlugin 中配置的 messages。

如果需要在组合式 API 中使用本地作用域,需要给 useI18n 传入响应的配置项,useI18n 将根据传入的 locale、messages 等配置项来生成一个全新的 -i18n 实例,该实例的文案源即为用户传入的 messages

<template>
-    <!--将展示本地作用域中的文案 哈喽-->
-    <view>{{t('message.hello')}}</view>
-</template>
-<script>
-    import {createPage, useI18n} from '@mpxjs/core'
-    createPage({
-        setup() {
-            const { t } = useI18n({
-                locale: 'en-US',
-                messages: {
-                    'en-US': {
-                        message: {
-                            hello: '哈喽'
-                        }
-                    }
-                }
-            })
-            return {
-                t
-            }
-        }
-    })
-</script>
-

注意事项:

在给 useI18n 传参时,若只传入 messages 属性,则 locale 会自动 fallback 到全局 locale,修改 useI18n 返回的 locale ref 值将改变本地作用域 locale。

若只传入 locale 属性,不传入 messages,则会自动 fallback 到全局 localemessages,且修改 useI18n 返回的 locale全局和本地都不会起作用, -因此建议不要单独传入 locale 属性,若想使用本地 i18n 实例,则必须传入 messages 属性。

在 Mpx 选项式 API 中,i18n 实例仅有全局 global i18n 实例,所有 i18n 翻译方法 $t/$tc/$te/$tm 的执行上下文都为全局作用域。

<template>
-    <view>{{$t('message.hello')}}</view>
-</template>
-<script>
-    import {createPage} from '@mpxjs/core'
-    createPage({
-        onLoad() {
-            console.log(this.$i18n)
-            /**
-             * locale 全局 locale
-             * fallbackLocale 全局兜底 locale
-             */
-        },
-        computed: {
-            name() {
-                // 作用域为 global scope
-                return this.$t('message.name')
-            }
-        }
-    })
-</script>
-

# 动态变更locale

在 Mpx 组合式 API 中,你可以通过修改 useI18n 返回的 locale 来更换局部或全局语言,或者更改 mpx.i18n 中的 locale 属性更换全局语言集,并自动更新视图。

  • 变更全局 locale
import mpx, { createComponent, useI18n } from '@mpxjs/core'
-
-createComponent({
-  setup () {
-    // 局部locale变更,生效范围为当前组件内
-    const {t, locale} = useI18n()
-    setTimeout(() => {
-      // 全局locale变更,生效范围为项目全局,locale 是一个 ref 变量
-      locale.value = 'zh-CN'
-      // 或者通过mpx.i18n来修改,也能起到全局locale修改作用
-      mpx.i18n.locale = 'zh-CN'
-    }, 1000)
-    return {
-        t
-    }  
-  }
-})
-

注意: 这里通过 mpx.i18n 修改 locale 时,为和 vue3 保持一直,需要通过 global 属性来获取 locale,同时 locale 也是一个 ref 变量,需要使用 .value 来修改。

  • 变更局部 locale
import mpx, { createComponent, useI18n } from '@mpxjs/core'
-
-createComponent({
-  setup () {
-    // 局部locale变更,生效范围为当前组件内
-    const {t, locale} = useI18n({
-        locale: 'en-US',
-        messages: {
-            'en-US': {
-                message: {
-                    hello: 'hello'
-                }
-            },
-            'zh-CN': {
-                message: {
-                    hello: '你好'
-                }
-            }
-        }
-    })
-    setTimeout(() => {
-      // 局部locale变更,生效范围为当前组件/页面,locale 是一个 ref 变量
-      locale.value = 'zh-CN'
-    }, 1000)
-    return {
-        t
-    }  
-  }
-})
-

在选项式 API 中,仅支持修改全局 locale, 不支持修改局部 locale。

在 Vue3 中,选项式 API 中直接修改 this.$i18n.locale 无任何作用,这里为和 Vue3 拉齐建议使用 mpx.i18n 来进行 locale 修改。

import mpx, { createComponent } from '@mpxjs/core'
-
-createComponent({
-  ready () {
-      // 修改全局 locale
-      mpx.i18n.locale = 'en-US'
-  }
-})
-

# 动态更新语言集

在组合式 API 中默认支持动态更新语言集。

在选项式 API 中,当模板中没有使用 i18n 函数或开启了 i18n.useComputed 配置时, 可以对语言集进行动态更新。

我们提供了 mergeLocaleMessage 对语言集进行动态扩展,setLocaleMessage 对语言集进行覆盖更新。

使用方式如下:

<template>
-    <!--2 秒之后展示 哈喽-->
-    <view>{{t('message.hello')}}</view>
-    <!--1秒之后展示 hello1, 2秒之后展示为 恭喜你-->
-    <view>{{t('message.hello1')}}</view>
-</template>
-<script>
-    import mpx, { createComponent, useI18n} from '@mpxjs/core'
-
-    createComponent({
-        setup () {
-            const { t, mergeLocaleMessage, setLocaleMessage } = useI18n()
-            setTimeout(() => {
-                // 新增一门语言或针对特定语言更新语言集
-                mergeLocaleMessage('en-US', {
-                    message: {
-                        hello1: 'hello1',
-                    }
-                })
-            }, 1000)
-            setTimeout(() => {
-                setLocaleMessage('en-US', {
-                    message: {
-                        hello: '哈喽',
-                        hello1: '恭喜你'
-                    }
-                })
-            }, 2000)
-            return { t }
-        }
-    })
-</script>
-

同理我们在选项式 API 中可以使用 mpx.i18n.global.mergeLocaleMessage, mpx.i18n.global.setLocaleMessage 方法来动态更新或扩展语言集。

# 平台支持

目前支持业内所有小程序平台(微信/支付宝/qq/百度/头条)。

在输出 web 时,构建会自动引入 vue-i18n 并进行安装配置,无需修改任何代码即可按照预期正常工作。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/image-process.html b/docs-vuepress/.vuepress/dist/guide/advance/image-process.html deleted file mode 100644 index 37b1f3de55..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/image-process.html +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - 图像资源处理 | Mpx框架 - - - - - - - - - -

# 图像资源处理

Mpx在小程序开发中提供了很好的图像资源处理能力,让开发者可以愉快地在项目中使用图片。

# 图像资源引入有三种方式

  1. Template 中通过 image src 指定图像资源 -
    • 直接指定图像的远程资源地址
    • 资源为本地路径,若配置 publicPath,则 publicPath 与 webpack loader 中配置的 name 进行拼接
  2. Style 中通过 src 指定图像资源
  3. Style 中通过 class 指定图像资源

Wxss文件中只能用 CDN 地址或 Base64, 针对第二、三种方式引入的资源,可以通过配置决定使用 CDN 还是 Base64,且 Mpx 中图像资源处理会优先检查 Base64,具体配置参数如下:

  • publicPath:资源存放 CDN 地址,可选
  • limit: 资源大小限制,可根据资源的大小判断走 Base64 还是 CDN, 可选
  • publicPathScope: 限制输出 CDN 图像资源的范围,可选 styleOnly、all,默认为 styleOnly。(图像引用方式分两大类 Template, Style)
  • outputPathCDN: 设置 CDN 图像对应的本地相对地址(相对于当前编译输出目录的地址,如 dist,或者 dist/wx),可写脚本将本地图像批量上传到 CDN

# Base64 图像资源

图像转 Base64的两种方式:

  • 未配置 publicPath
  • 配置了 publicPath,且用户未自定义图像处理 fallback query,且未配置 limit 或图像资源未超过 limit 的限制时
// webpack.config.js 配置,未配置 publicPath 必走 Base64
-const webpackConfig = {
-  module: {
-    rules: [{
-      test: /\.(png|jpe?g|gif|svg)$/,
-      loader: MpxWebpackPlugin.urlLoader({
-        name: 'img/[name][hash].[ext]'
-      })
-    }]
-  }
-}
-

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      urlLoader: {
-        name: 'img/[name][hash].[ext]'
-      }
-    }
-  }
-})
-
<style>
-  .logo {
-    background-image: url('~images/logo.png');
-  }
-</style>
-

# CDN 图像资源

// webpack.config.js 配置
-const webpackConfig = {
-  module: {
-    rules: [{
-      test: /\.(png|jpe?g|gif|svg)$/,
-      loader: MpxWebpackPlugin.urlLoader({
-        name: 'img/[name][hash].[ext]',
-        // CDN 地址
-        publicPath: 'http://a.com/',
-        limit: '1024' // Base64 的最大长度,超过则走 CDN 
-      })
-    }]
-  }
-}
-

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      urlLoader: {
-        name: 'img/[name][hash].[ext]',
-        // CDN 地址
-        publicPath: 'http://a.com/',
-        limit: '1024' // Base64 的最大长度,超过则走 CDN 
-      }
-    }
-  }
-})
-

# CDN 图像资源输出本地目录,用户自行批量上传到CDN服务器

// webpack.config.js 配置
-const webpackConfig = {
-  module: {
-    rules: [{
-      test: /\.(png|jpe?g|gif|svg)$/,
-      loader: MpxWebpackPlugin.urlLoader({
-          name: 'img/[name][hash].[ext]',
-          publicPath: 'http://a.com',
-          limit: 100,
-          publicPathScope: 'styleOnly',
-          outputPathCDN: './cdnImages'
-      })
-    }]
-  }
-}
-

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      urlLoader: {
-        name: 'img/[name][hash].[ext]',
-        publicPath: 'http://a.com',
-        limit: 100,
-        publicPathScope: 'styleOnly',
-        outputPathCDN: './cdnImages'
-      }
-    }
-  }
-})
-

备注:
-图像默认编译后会输出到 img 目录下, 当设置 outputPathCDN 后,输出的本地图像地址为 outputPathCDN + img/图像.png
-CND 文件地址为 publicPath + img/图像.png,所以当使用脚本上传到 CDN 时,路径要带上 img

# 用户自定义图像处理方式

// webpack.config.js 配置
-const webpackConfig = {
-  module: {
-    rules: [{
-      test: /\.(png|jpe?g|gif|svg)$/,
-      loader: MpxWebpackPlugin.urlLoader({
-        name: 'img/[name][hash].[ext]',
-        // CDN 地址
-        publicPath: 'http://a.com/',
-        limit: '1024' // Base64 的最大长度,超过则走 CDN,
-        fallback: 'file-loader' // 默认走 file-loader
-      })
-    }]
-  }
-}
-

@mpxjs/cli 3.x 版本配置如下

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      urlLoader: {
-        name: 'img/[name][hash].[ext]',
-        // CDN 地址
-        publicPath: 'http://a.com/',
-        limit: '1024' // Base64 的最大长度,超过则走 CDN,
-        fallback: 'file-loader' // 默认走 file-loader
-      }
-    }
-  }
-})
-
/*不走 Base64 的情况下*/
-<style>
-  .logo2 {
-    background-image: url('~images/logo.png?fallback=true');
-  }
-</style>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/mixin.html b/docs-vuepress/.vuepress/dist/guide/advance/mixin.html deleted file mode 100644 index 312f093e26..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/mixin.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - 使用 mixin | Mpx框架 - - - - - - - - - -

# 使用 mixin

Mpx 提供了一套完善的 mixin 机制,有人可能要问,原生小程序中已经支持了 behaviors,为何我们还需要提供 mixin 呢?主要有以下两点原因:

  1. Behaviors 是平台限度的,只有在部分小程序平台中可以使用,而且内置 behaviors 承载了除了 mixin 外的其他功能,框架提供的 mixin 是一个与平台无关的基础能力;
  2. Behaviors 只有组件支持使用,页面不支持,而且只支持局部声明,框架提供的 mixin 与组件页面无关,且支持全局 mixin 声明。

# 局部 Mixin

AppPageComponent 接收 mixins 参数,参数格式为[Mixin1(Object),Mixin2(Object)]

Mixin 混合实例对象可以像正常的实例对象一样包含选项,相同选项将进行逻辑合并。举例:如果 mixin1 包含一个钩子 ready,而创建组件 Component 也有一个钩子 ready,两个函数将被调用。 Mixin 钩子按照传入顺序(数组顺序)依次调用,并在调用组件自身的钩子之前被调用。

// mixin.js
-export default {
-  data: {
-    list: {
-      'phone': '手机',
-      'tv': '电视',
-      'computer': '电脑'
-    }
-  },
-  ready () {
-    console.log('mixins ready:', this.list.phone)
-  }
-}
-
<template>
-  <view class="list">
-    <view wx:for="{{list}}" wx:key="index">{{item}}</view>
-  </view>
-</template>
-
-<script>
-  import { createComponent } from '@mpxjs/core'
-  import mixins from '../common/mixins'
-
-  createComponent({
-    mixins: [mixins],
-    data: {
-      list: ['手机', '电视', '电脑']
-    },
-    ready () {
-      console.log('component ready:', this.list.phone)
-    }
-  })
-</script>
-
// 输出结果为
-mixins ready: 手机
-component ready: 手机
-

# 全局 Mixin

Mpx 中可以使用 mpx.injectMixins 方法配置全局 mixin,能够按照 App / 组件 / 页面维度自由配置,简单示例如下:

import mpx from '@mpxjs/core'
-
-// 第一个参数为 mixins,可以混入任意配置,第二个参数为混入生效范围,可传递 'app' / 'page' / 'component' 字符串或由其组成的数组
-mpx.injectMixins([
-  {
-    data: {
-      customData: '123'
-    }
-  }
-], ['page'])
-
-// mpx.mixin 为 mpx.injectMixins 的别名,混入单个 mixin 时可以直接传递对象,生效范围可传递字符串
-mpx.mixin({
-  methods: {
-    useCustomData () {
-      console.log(this.customData)
-    }
-  }
-}, 'component')
-
-// 当未传递生效范围时默认为全局生效,对 app / page / component 都生效
-mpx.mixin({
-  computed: {
-    processedCustomData () {
-      return this.customData + '321'
-    }
-  }
-})
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/npm.html b/docs-vuepress/.vuepress/dist/guide/advance/npm.html deleted file mode 100644 index 7dc074ae6d..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/npm.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - 使用npm | Mpx框架 - - - - - - - - - -

# 使用npm

Mpx项目中用户能够方便自然地引用 npm 资源,像 web 开发中一样

在 Mpx 中使用 npm,通过 Webpack 插件在编译时将引用的 npm 包输出为小程序文件,所以 Mpx 的构建产物不需要在开发工具再次进行 npm 构建。

# 下载npm包

在项目 package.json 所在的目录中执行命令安装项目 npm 包

npm install
-

在项目中安装某个第三方 npm 包

npm install mpx-ui
-

如若之前未接触过 npm,请翻阅 官方 npm 文档 (opens new window) 进行学习。

# 引用npm模块

直接使用模块路径引用,可以直接通过 ES6 的 import 语法来引用 JS 文件,并且无需额外执行npm构建

import { createPage } from '@mpxjs/core'
-

# 引用npm组件/页面

在页面 script 标签中的 json 对象中使用 usingComponents 引入第三方组件,直接使用模块路径引用

<script type="application/json">
-  {
-    "usingComponents": {
-      "mpx-button": "mpx-ui/src/components/button"
-    }
-  }
-</script>
-

在 app.mpx 中的 script 标签中的 pages/packages/subPackages 中都可以声明引用 npm 包页面

示例:

<script type="application/json">
-  {
-    "pages": [
-      "@someGroup/someNpmPackage/pages/view/index.mpx"
-    ]
-  }
-</script>
-

以上这种写法为避免和本地页面路径冲突,Mpx 会将路径进行 hash 化处理,所以使用页面时要在路径后添加 ?resovle 标识符,编译时会被处理成正确的、完整的绝对路径。

import packageIndexPage from '@someGroup/someNpmPackage/pages/view/index.mpx?resolve'
-
-mpx.navigateTo({
-  url: packageIndexPage
-})
-

如果你觉得上述引用 npm 包页面的方式太繁琐,我们也提供了另一种 page 声明方式,可以让你自定义页面路径

// 声明
-{
-  "pages": [
-    {
-      "src": "@someGroup/someNpmPackage/pages/view/index.mpx",
-      "path": "pages/somNpmPackage/index" // 注意保持 path 的唯一性
-    }
-  ]
-}
-
-// 使用
-// 可以直接使用你自己声明的 path
-mpx.navigateTo({
-  url: '/pages/somNpmPackage/index'
-})
-

同理,我们在使用 subPackages 分包时也可以使用 pages 对象的方式

"subPackages": [
-  {
-    "root": "test",
-    "pages": [
-       {
-         "src": "@someGroup/someNpmPackage/pages/view/index.mpx",
-         "path": "pages/somNpmPackage/index" // 注意保持 path 的唯一性
-       }
-    ]
-  }
-]
-// 使用
-mpx.navigateTo({
-  url: '/test/pages/somNpmPackage/index'
-})
-

同时使用 Mpx 引用npm组件/页面时包体积比原生中的 npm 规范更优,好处有:

Mpx npm构建的优势主要有两点:1. 按需构建;2. 支持分包

  • 小程序的 npm 规范场景下,组件库需声明miniprogram_dist目录,执行构建npm命令,将整个miniprogram_dist中的代码copy到项目的miniprogram_npm目录下。而 Mpx 的 npm 包引用,借助 Webpack 强大的构建分析能力,loader 在解析 json 中的 pages 域和 usingComponents 域中的路径时,通过动态创建 entry 的方式把这些文件添加进来,同时按需加载被确切使用的文件,降低包体积,借助 CommonsChunkPlugin/SplitChunksPlugin 的能力将复用的 js 模块抽出到一个外部公用的 bundle 中。

  • 原生小程序的构建中,所有的 npm 模块都会输出到主包中,Mpx 在编译中,还会进行分包处理,对组件和静态资源,根据用户的分包配置,串行对主包和各个分包进行构建,标记出每个组件及静态资源的归属,根据小程序资源访问策略将其输出到主包或者分包中。

所以使用 Mpx 框架开发小程序,可以享受最舒适最自然最好用的 npm 机制,详细原理介绍请移步Mpx编译构建原理

# 兼容原生小程序路径规范

组件或者页面的引入有绝对路径和相对路径,或者引入 npm 第三方包,原生小程序中,我们通过相对路径引入一个组件时

{
-  "usingComponents": {
-    "component-tag-name": "path/to/the/custom/component"
-  }
-}
-

这种路径形式在 webpack 路径规范中会被当成 npm 包路径来处理,所以要对原生小程序的路径规范做下兼容。

Mpx 提供了两种路径规范的配置模式可供大家选择,具体的使用方式为:

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-         resolveMode: 'webpack'
-      }
-    }
-  }
-})
-

在 MpxWebpackPlugin 插件中设置 resolveMode 项,默认值为 webpack,可选值有 webpack/native,推荐使用 webpack 模式,更舒服一些,配置项为 webpack 时,json 中的 pages/usingComponents 等需要写相对路径,但是也可以直接写 npm 包路径。例如:

{
-  "usingComponents": {
-    "component-tag-name": "./path/to/the/custom/component", // 内部组件路径
-    "mpx-button": "mpx-ui/src/components/button" // npm 包路径
-  }
-}
-

如果希望使用类似小程序原始那种"绝对路径",可以将 resolveMode 设置为 native,但是 npm 路径需要在前面加一个~,类似 webpack 的样式引入规范,同时必须配合 projectRoot 参数提供项目根目录地址。

resolveMode 为 native 时的使用示例:

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      plugin: {
-        resolveMode: 'native',
-        // 当resolveMode为native时可通过该字段指定项目根目录
-        projectRoot: path.resolve(__dirname, '../src')
-      }
-    }
-  }
-})
-
-// 项目page.mpx
-{
-  "usingComponents": {
-    "mpx-button": "~mpx-ui/src/components/button", // npm 包路径
-    "component-tag-name": "path/to/the/custom/component" // 内部组件路径
-  }
-}
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/pinia.html b/docs-vuepress/.vuepress/dist/guide/advance/pinia.html deleted file mode 100644 index dff4cbe5af..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/pinia.html +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - 状态管理(pinia) | Mpx框架 - - - - - - - - - -

# 状态管理(pinia)

Mpx 参考 Pinia (opens new window) 设计实现了一套外部状态逻辑管理系统(pinia),允许跨页面/组件共享状态,其中的概念与 api 与 Pinia 保持一致,同时支持在 Mpx 组合式 API(Composition API)和选项式 API(Options API)模式下使用。

# 介绍

pinia 是一个状态管理容器,可支持复杂场景下的组件通信机制,与Vuex状态管理不同之处在于:

  1. mutations 不再存在,使用 actions 来支持应用中状态的同步和异步变更。

  2. 动态创建 store,使用useStore实现运行时动态创建 store。

  3. 扁平架构,不再有 modules 嵌套结构,支持不同 store 之间的交叉组合方式使用。

# 创建 pinia

首先在应用中调用createPinia方法来创建全局 pinia 实例。

// app.mpx
-import mpx from '@mpxjs/core'
-import { createPinia } from '@mpxjs/pinia'
-
-const pinia = createPinia()
-

如果你的应用想使用 SSR 渲染模式,请将 pinia 的创建放在 onAppInit 钩子中执行

// app.mpx
-
-import mpx, { createApp } from '@mpxjs/core'
-import { createPinia } from '@mpxjs/pinia'
-
-createApp({
-  // ...
-  onAppInit () {
-    const pinia = createPinia()
-    return {
-      pinia
-    }
-  }
-})
-

# 创建 store

然后调用defineStore方法,传入 store 唯一标识(id),来创建一个 store,支持 Setup 和 Options 两种风格的 store。

# Setup store

与组合式 API 的 setup 函数类似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并返回一个带有我们想暴露出去的属性和方法的对象。

// setup.js
-import { defineStore } from '@mpxjs/pinia'
-import { ref, computed } from '@mpxjs/core'
-
-export const useSetupStore = defineStore('setup', () => {
-  const count = ref(0)
-  const name = ref('pinia')
-  const myName = computed(() => {
-    return name.value
-  })
-  function increment() {
-    count.value++
-  }
-  return { count, name, myName, increment }
-})
-

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

# Options store

// options.js
-import { defineStore } from '@mpxjs/pinia'
-import { ref } from '@mpxjs/core'
-
-export const useOptionsStore = defineStore('options', {
-  state : () => {
-    return {
-      count: 0,
-      name: 'pinia'
-    }
-  },
-  getters: {
-    myName(state) {
-      return state.name
-    }
-  },
-  actions: {
-    increment () {
-      this.$patch((state) => {
-        state.count++
-      })
-    }
-  }
-})
-

# 使用 store

在选项式 API 中使用,如果你不能使用组合式 API,但你可以使用 computed, methods, '...',那你可以使用 mapState(), mapActions() 辅助函数 -来将 state, getter, action 等映射到你的组件中。

无论是 optionsStore、还是 setupStore,我们都可以在选项式组件中使用,且使用方式并无差异,下方例子我们统一使用 useSetupStore 来作为示范例

import { createComponent } from '@mpxjs/core'
-import { storeToRefs, mapState, mapActions } from '@mpxjs/pinia'
-import { useOptionsStore } from 'xxx/options'
-import { useSetupStore } from 'xxx/setup'
-
-
-// 选项式API(Options API)风格下通过mapHelper函数使用
-createComponent({
-  computed: {
-    // 可以访问组件中的 this.name
-    // 此处与直接从 useSetupStore 中读取的数据相同
-    ...mapState(useSetupStore, ['name', 'count']),
-    // 也可以将state修改别名,例如将name 注册为 otherName
-    ...mapState(useSetupStore, {
-      otherName: 'name',
-      // 也可以写一个函数来获取对 useSetupStore 的访问权
-      double: store => store.count * 2,
-      // 也可以通过访问 `this` 拿到数据
-      magicValue() {
-        return this.count + this.double
-      }
-    }),
-    // 在pinia中,getter 也是通过 mapState 来进行映射
-    ...mapState(useSetupStore, ['myName'])
-  },
-  methods: {
-    ...mapActions(useSetupStore, ['increment']),
-    // 也可以将其注册问题 this.setupIncrement 方法
-    ...mapActions(useSetupStore, {
-      setupIncrement: 'increment'
-    })
-  }
-})
-

注意:pinia 中,getter 也是通过 mapState 来进行映射

在组合式API (Setup API) 风格下使用

import { useSetupStore } from 'xxx/setup'
-import { storeToRefs, mapState, mapActions } from '@mpxjs/pinia'
-
-createComponent({
-  setup (props, context) {
-    const setupStore = useSetupStore()
-    setupStore.count = 2
-    // 作为 store 的一个属性,我们可以直接访问任何 getter(与 state )
-    setupStore.myName // pinia
-
-    function onIncrementClick() {
-      // 调用 action 方法
-      setupStore.increment()
-      console.log('New Count:', setupStore.count)
-    }
-    return {
-      onIncrementClick,
-      ...storeToRefs(setupStore)
-    }
-  }
-})
-

注意:在组合式 API(Setup API)模式下,直接解构获取到的 store 数据会失去响应性,需要通过 storeToRefs 方法处理赋予数据响应性。另外storeToRefs方法只会返回 state 或 getter。

# 使用插件

Mpx pinia 支持使用插件扩展当前 store 实例的功能,用法如下:

// onStoreAction.js,订阅所有store实例的方法
-export const onStoreAction: context => {
-  context.store.$onAction((
-    {
-      name,
-      store,
-      args,
-      after,
-      onError
-    }) => {
-      after(name => {
-        console.error('after', name, store, args)
-      })
-
-      onError(error => {
-        console.log('onError', error)
-      })
-    }, false)
-}
-
-// app.mpx
-import mpx from '@mpxjs/core'
-import { createPinia } from '@mpxjs/pinia'
-import { onStoreAction } from 'xxx/onStoreAction'
-
-const pinia = createPinia()
-pinia.use(onStoreAction)
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/platform.html b/docs-vuepress/.vuepress/dist/guide/advance/platform.html deleted file mode 100644 index b95a932b6d..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/platform.html +++ /dev/null @@ -1,300 +0,0 @@ - - - - - - 跨平台 | Mpx框架 - - - - - - - - - -

# 跨平台

# 多平台支持

Mpx支持在多个小程序平台中进行增强,目前支持的小程序平台包括微信,支付宝,百度,qq和头条,不过自2.0版本后,Mpx支持了以微信增强语法为base的跨平台输出,实现了一套业务源码在多端输出运行的能力,大大提升了多小程序平台业务的开发效率,详情可以查看跨平台编译

# template增强特性

不同平台上的模板增强指令按照平台的指令风格进行设计,文档和代码示例为了方便统一采用微信小程序下的书写方式。

模板增强指令对应表:

增强指令 微信 支付宝 百度 qq 头条
双向绑定 wx:model a:model s-model qq:model tt:model
双向绑定辅助属性 wx:model-prop a:model-prop s-model-prop qq:model-prop tt:model-prop
双向绑定辅助属性 wx:model-event a:model-event s-model-event qq:model-event tt:model-event
双向绑定辅助属性 wx:model-value-path a:model-value-path s-model-value-path qq:model-value-path tt:model-value-path
双向绑定辅助属性 wx:model-filter a:model-filter s-model-filter qq:model-filter tt:model-filter
动态样式绑定 wx:class a:class s-class qq:class 暂不支持
动态样式绑定 wx:style a:style s-style qq:style 暂不支持
获取节点/组件实例 wx:ref a:ref s-ref qq:ref tt:ref
显示/隐藏 wx:show a:show s-show qq:show tt:show

# script增强特性

增强字段 微信 支付宝 百度 qq 头条
computed 支持 支持 支持 支持 部分支持,无法作为props传递(待头条修复生命周期执行顺序可完整支持)
watch 支持 支持 支持 支持 支持
mixins 支持 支持 支持 支持 支持

# style增强特性

无平台差异

# json增强特性

增强字段 微信 支付宝 百度 qq 头条
packages 支持 支持 支持 支持 部分支持,无法分包

# 跨平台输出小程序

自2.0版本开始,mpx开始支持跨小程序平台编译,不同于常规跨平台框架重新定义一套DSL的方式,mpx支持基于现有平台的源码编译为其他已支持平台的目标代码。跨平台编译能力依赖于mpx的多平台支持,目前mpx已经支持将微信小程序跨平台编译为支付宝、百度、qq和头条小程序。

# 使用方法

如果你是使用mpx init xxx新生成的项目,package.json里script部分有npm run build:cross,直接执行npm run build:cross(watch同理),如果仅需构建某几个平台的,可以修改该script,按已有的格式删除或增添某些某些平台

如果你是自行搭建的mpx项目,你只需要进行简单的配置修改,打开项目的webpack配置,找到@mpxjs/webpack-plugin的声明位置,传入mode和srcMode参数即可,示例如下

new MpxwebpackPlugin({
-  // mode为mpx编译的目标平台,可选值有(wx|ali|swan|qq|tt|jd|web|ios|android)
-  mode: 'ali',
-  // srcMode为mpx编译的源码平台,目前仅支持wx   
-  srcMode: 'wx'
-})
-

使用 @mpxjs/cli 创建的项目,可以通过在 npm script 当中定义 targets 来设置编译的目标平台

// 项目 package.json
-{
-  "script": {
-    "build:cross": "mpx-cli-service build --targets=wx,ali"
-  }
-}
-

# 跨平台差异抹平

为了实现小程序的跨平台编译,我们在编译和运行时做了很多工作以抹平小程序开发中各个方面的跨平台差异

# 模板语法差异抹平

对于通用指令/事件处理的差异,mpx提供了统一的编译转换抹平操作;而对于平台组件和组件属性的差异,我们也在力所能及的范围内进行了转换抹平,对于平台差异性过大无法转换的部分会在编译阶段报错指出。

# 组件/页面对象差异抹平

不同平台间组件/页面对象的差异主要体现在生命周期上,我们在支持多平台能力时已经将不同平台的生命周期映射到mpx框架的一套内部生命周期中,基于这个统一的映射,不同平台的生命周期差异也得到了抹平。

此外,我们还进行了一系列运行时增强来模拟微信平台中提供而其他平台中未提供的能力,例如:

  • 在支付宝组件实例中提供了this.triggerEvent方法模拟微信中的自定义组件事件;
  • 提供了this.selectComponent/this.selectAllComponents方法模拟微信中获取子组件实例的能力;
  • 重写了createSelectorQuery方法抹平了微信/支付宝平台间的使用差异;
  • 转换抹平了微信/支付宝中properties定义的差异;
  • 利用mpx本身的数据响应能力模拟了微信中的observers/property observer能力等;
  • 提供了this.getRelationNodes方法并支持了微信中组件间关系relations的能力

对于原生小程序组件的转换,还会进行一些额外的抹平,已兼容一些已有的原生组件库,例如:

  • 将支付宝组件中的props数据挂载到this.data中以模拟微信平台中的表现;
  • 利用mpx本身的mixins能力模拟微信中的behaviors能力。

对于一些无法进行模拟的跨平台差异,会在运行时进行检测并报错指出,例如微信转支付宝时使用moved生命周期等。

# json配置差异抹平

类似于模板语法,会在编译阶段进行转换抹平,无法转换的部分会在编译阶段报错指出。

# api调用差异抹平

对于api调用,mpx提供了一个api调用代理插件来抹平跨平台api调用的差异,使用时需要在项目中安装使用@mpxjs/api-proxy,可以通过两种方式使用

# 方式一:在调用小程序api时统一使用mpx对象进行调用,示例如下:

安装插件支持options传入,options说明如下:

参数名称 类型 含义 是否必填 默认值 备注
platform Object 各平台之间的转换 { from:'', to:'' } 已删除
usePromise Boolean 是否将 api 转化为 promise 格式使用 - - -
exclude Array(String) 跨平台时不需要转换的 api - - 已删除
whiteList Array(String) 强行转化为 promise 格式的 api [] 需要 usePromise 设为 true
blackList Array(String) 不转换 promise 格式的 api [] 需要 usePromise 设为 true
custom Object 提供用户在各渠道下自定义api开放能力 [] -

# 普通形式

import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-
-mpx.use(apiProxy)
-
-mpx.showModal({
-  title: '标题',
-  content: '这是一个弹窗',
-  success (res) {
-    if (res.cancel) {
-      console.log('用户点击取消')
-    }
-  }
-})
-

# 使用promise形式

import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-
-mpx.use(apiProxy, {
-  usePromise: true
-})
-
-mpx.showActionSheet({
-  itemList: ['A', 'B', 'C']
-})
-.then(res => {
-  console.log(res.tapIndex)
-})
-.catch(err => {
-  console.log(err)
-})
-

# 用户自定义

import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-import { scanCode } from '@test/scanCode'
-
-mpx.use(apiProxy, {
-  custom: {
-    web: {
-      scanCode
-    }
-  }
-})
-// 在web下调用的实际是用户自定义部分的scanCode
-mpx.scanCode({
-  onlyFromCamera: true,
-  success (res) {
-    console.log(res, 'scanCode, success')
-  },
-  fail (res) {
-    console.log(res, 'scanCode, fail')
-  }
-})
-

# 方式二:直接在@mpxjs/api-proxy导出想使用的方法

// 独立使用 支持treesharking能力
-import { showModal } from '@mpxjs/api-proxy'
-
-showModal({
-  title: '标题',
-  content: '这是一个弹窗',
-  success (res) {
-    console.log('弹框展示成功')
-  }
-})
-

对于无法转换抹平的api调用会在运行时阶段报错指出。

# webview bridge差异抹平

Mpx支持小程序跨平台后,多个平台的小程序里都有webview组件,webview打开的页面和小程序可以通过API来通信以及调用一些小程序能力,但是各方webview提供的API是不一样的。

比如微信是用 wx.miniProgram.navigateTo 来跳转到别的小程序页面,而支付宝里是 my.navigateTo ,那么我们开发H5时候为了让H5能适应各家小程序平台就需要写多份对应逻辑。

为解决这个问题,Mpx提供了用于运行在小程序的webview里的H5抹平平台差异的bridge库:@mpxjs/webview-bridge

使用方式很简单,不过注意这个库是给H5用的,不是给小程序用的。在H5项目中引入。

使用示例 (opens new window)

支持script标签引入和npm引入,标签引入的话,全局实例是mpx(npm模块使用下也鼓励import mpx from '@mpxjs/webview-birdge'),使用就例如 mpx.navigateTo ,能保持整个项目风格完全一致。

提供的API如下:navigateTo, navigateBack, switchTab, reLaunch, redirectTo, getEnv, postMessage, getLoadError

# 跨平台条件编译

Mpx跨平台编译的原则在于,能转则转,转不了则报错提示,对于无法抹平差异的部分,我们提供了完善的跨平台条件编译机制便于用户处理因平台差异而无法相互转换的部分,也能够用于实现具有平台差异性的业务逻辑。

mpx中我们支持了三种维度的条件编译,分别是文件维度,区块维度和代码维度,其中,文件维度和区块维度主要用于处理一些大块的平台差异性逻辑,而代码维度主要用于处理一些局部简单的平台差异。

# 文件维度条件编译

文件维度条件编译简单的来说就是文件为维度进行跨平台差异代码的编写,例如在微信->支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,这时你可以在相同的位置新建一个map.ali.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的mode来加载对应模块,当mode为ali时,会优先加载map.ali.mpx,反之则会加载map.mpx。

文件维度条件编译能够与webpack alias结合使用,对于npm包的文件我们并不方便在原本的文件位置创建.ali的条件编译文件,但我们可以通过webpack alias在相同位置创建一个虚拟的.ali文件,并将其指向项目中的其他文件位置。

  // 对于npm包中的文件依赖
-  import npmModule from 'somePackage/lib/index'
-
-  // 配置以下alias后,当mode为ali时,会优先加载项目目录中定义的projectRoot/somePackage/lib/index文件
-  // vue.config.js
-  module.exports = defineConfig({
-    configureWebpack() {
-      return {
-        resolve: {
-          alias: {
-            'somePackage/lib/index.ali': 'projectRoot/somePackage/lib/index'
-          }
-        }
-      }
-    }
-  })
-

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加mode属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali时使用如下区块-->
-<template mode="ali">
-<!--该区块中的所有代码需采用支付宝的技术标准进行编写-->
-  <view>支付宝环境</view>
-</template>
-
-<!--其他编译mode时使用如下区块-->
-<template>
-  <view>其他环境</view>
-</template>
-

# 代码维度条件编译

如果只有局部的代码存在跨平台差异,mpx同样支持在代码内使用if/else进行局部条件编译,用户可以在js代码和template插值中访问__mpx_mode__获取当前编译mode,进行平台差异逻辑编写,js代码中使用示例如下。

除了 __mpx_mode__ 这个默认插值以外,有别的环境变量需要的话可以在mpx.plugin.conf.js里通过defs进行配置。

if(__mpx_mode__ === 'ali') {
-  // 执行支付宝环境相关逻辑
-} else {
-  // 执行其他环境相关逻辑
-}
-

template代码中使用示例如下

<!--此处的__mpx_mode__不需要在组件中声明数据,编译时会基于当前编译mode进行替换-->
-<view wx:if="{{__mpx_mode__ === 'ali'}}">支付宝环境</view>
-<view wx:else>其他环境</view>
-

JSON中的条件编译(注意,这个依赖JSON的动态方案,得通过name="json"这种方式来编写,其实写的是js代码,最终module.exports导出一个可json化的对象即可):

<script name="json">
-const pages = __mpx_mode__ === 'wx' ? [
-  'main/xxx',
-  'sub/xxx'
-] : [
-  'test/xxx'
-] // 可以为不同环境动态书写配置
-module.exports = {
-  usingComponents: {
-    aComponents: '../xxxxx' // 可以打注释 xxx组件
-  }
-}
-</script>
-

样式的条件编译:

/*
-  @mpx-if (__mpx_env__ === 'someEvn')
-*/
-  /* @mpx-if (__mpx_mode__ === 'wx') */
-  .backColor {
-    background: green;
-  }
-  /*
-    @mpx-elif (__mpx_mode__ === 'qq')
-  */
-  .backColor {
-    background: black;
-  }
-  /* @mpx-endif */
-
-  /* @mpx-if (__mpx_mode__ === 'swan') */
-  .backColor {
-    background: cyan;
-  }
-  /* @mpx-endif */
-  .textSize {
-    font-size: 18px;
-  }
-/*
-  @mpx-else
-*/
-.backColor {
-  /* @mpx-if (__mpx_mode__ === 'swan') */
-  background: blue;
-  /* @mpx-else */
-  background: red;
-  /* @mpx-endif */
-}
-/*
-  @mpx-endif
-*/
-

# 属性维度条件编译

属性维度条件编译允许用户在组件上使用 @| 符号来指定某个节点或属性只在某些平台下有效。

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

比如业务中需要通过 button 按钮获取用户信息,虽然可以使用代码维度条件编译来解决,但是增加了很多代码量:

<button
-  wx:if="{{__mpx_mode__ === 'wx' || __mpx_mode__ === 'swan'}}"
-  open-type="getUserInfo"
-  bindgetuserinfo="getUserInfo">
-  获取用户信息
-</button>
-
-<button
-  wx:elif="{{__mpx_mode__ === 'ali'}}"
-  open-type="getAuthorize"
-  scope="userInfo"
-  onTap="onTap">
-  获取用户信息
-</button>
-

而用属性维度的编译则方便很多:

<button
-  open-type@wx|swan="getUserInfo"
-  bindgetuserinfo@wx|swan="getUserInfo"
-  open-type@ali="getAuthorize"
-  scope@ali="userInfo"
-  onTap@ali="onTap">
-  获取用户信息
-</button>
-

属性维度的编译也可以对整个节点进行条件编译,例如只想在支付宝小程序中输出某个节点:

<view @ali>this is view</view>
-

需要注意使用上述用法时,节点自身在构建时框架不会对节点属性进行平台语法转换,但对于其子节点,框架并不会继承父级节点 mode,会进行正常跨平台语法转换。

<!--错误示例-->
-<view @ali bindtap="otherClick">
-    <view bindtap="someClick">tap click</view>
-</view>
-// srcMode 为 wx 跨端输出 ali 结果为
-<view @ali bindtap="otherClick">
-    <view onTap="someClick">tap click</view>
-</view>
-

上述示例为错误写法,假如srcMode为微信小程序,用上述写法构建输出支付宝小程序时,父节点 bindtap 不会被转为 onTap,在支付宝平台执行时事件会无响应。

正确写法如下:

<!--正确示例-->
-<view @ali onTap="otherClick">
-    <view bindtap="someClick">tap click</view>
-</view>
-// 输出 ali 产物
-<view @ali onTap="otherClick">
-    <view onTap="someClick">tap click</view>
-</view>
-

有时开发者期望使用 @ali 这种方式仅控制节点的展示,保留节点属性的平台转换能力,为此 Mpx 实现了一个隐式属性条件编译能力

<!--srcMode为 wx,输出 ali 时,bindtap 会被正常转换为 onTap-->
-<view @_ali bindtap="someClick">test</view>
-

在对应的平台前加一个_,例如@_ali、@_swan、@_tt等,使用该隐式规则仅有条件编译能力,节点属性语法转换能力依旧。

有时候我们不仅需要对节点属性进行条件编译,可能还需要对节点标签进行条件编译。

为此,我们支持了一个特殊属性 mpxTagName,如果节点存在这个属性,我们会在最终输出时将节点标签修改为该属性的值,配合属性维度条件编译,即可实现对节点标签进行条件编译,例如在百度环境下希望将某个 view 标签替换为 cover-view,我们可以这样写:

<view mpxTagName@swan="cover-view">will be cover-view in swan</view>
-

# 通过 env 实现自定义目标环境的条件编译

Mpx 支持在以上四种条件编译的基础上,通过自定义 env 的形式实现在不同环境下编译产出不同的代码。

实例化 MpxWebpackPlugin 的时候,传入配置 env。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      srcMode: 'wx' // srcMode为mpx编译的源码平台,目前仅支持wx
-      plugin: {
-        env: "didi" // env为mpx编译的目标环境,需自定义
-      }
-    }
-  }
-})
-

# 文件维度条件编译

微信转支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,而且这个地图组件在不同的目标环境中也有很大的差异,这时你可以在相同的位置新建一个 map.ali.didi.mpx 或 map.ali.qingju.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的 mode 和 env 来加载对应模块,当 mode 为 ali,env 为 didi 时,会优先加载 map.ali.didi.mpx、map.ali.mpx,如果没有定义 env,则会优先加载 map.ali.mpx,反之则会加载 map.mpx。

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加modeenv属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali且env为didi时使用如下区块,优先级最高是4-->
-<template mode="ali" env="didi">
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-
-<!--编译mode为ali时使用如下区块,优先级是3-->
-<template mode="ali">
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-
-<!--编译env为didi时使用如下区块,优先级是2-->
-<template env="didi">
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-
-<!--其他环境,优先级是1-->
-<template>
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-

注意,如果多个相同的区块写相同的 mode 和 env,默认会用最后一个,如:

<template mode="ali">
-  <view>该区块会被忽略</view>
-</template>
-
-<template mode="ali">
-  <view>默认会用这个区块</view>
-</template>
-

# 代码维度条件编译

如果在 MpxWebpackPlugin 插件初始化时自定义了 env,你可以访问__mpx_env__获取当前编译env,进行环境差异逻辑编写。使用方法与__mpx_mode__相同。

# 属性维度条件编译

env 属性维度条件编译与 mode 的用法大致相同,使用 : 符号与 mode 和其他 env 进行串联,与 mode 组合使用格式形如 attr@mode:env:env|mode:env,为了不与 mode 混淆,当条件编译中仅存在 env 条件时,也需要添加 : 前缀,形如 attr@:env

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

如果当前编译的目标平台是 wx,以下写法 open-type 属性将被忽略

<button open-type@swan:didi="getUserInfo">获取用户信息</button>
-

如果当前 env 不是 didi,以下写法 open-type 属性也会被忽略

<button open-type@:didi="getUserInfo">获取用户信息</button>
-

如果只想在 mode 为 wx 且 env 为 didi 或 qingju 的环境下使用 open-type 属性,则可以这样写:

<button open-type@wx:didi:qingju="getUserInfo">获取用户信息</button>
-

env 属性维度的编译同样支持对整个节点或者节点标签名进行条件编译:

<view @:didi>this is a  view component</view>
-<view mpxTagName@:didi="cover-view">this is a  view component</view>
-

如果只声明了 env,没有声明 mode,跨平台输出时框架对于节点属性默认会进行转换:

<!--srcMode为wx,跨平台输出ali时,bindtap会被转为onTap-->
-<view @:didi bindtap="someClick">this is a  view component</view>
-<view bindtap@:didi ="someClick">this is a  view component</view>
-

# 其他注意事项

  • 当目标平台为支付宝时,需要启用支付宝最新的component2编译才能保障框架正常工作,关于component2点此查看详情 (opens new window)
  • 跨平台源码中自定义组件的标签名不能使用驼峰形式myComponent,请使用横杠形式my-component来书写;
  • 生成的目标代码中文件名和文件夹名不能带有@符号,目前媒体文件和原生自定义组件在编译时不会修改文件名,需要重点关注。

# 跨平台输出web

从2.3.0版本开始,Mpx开始支持将已有项目跨平台输出web平台中运行的能力,目前输出web能力完备,能够支持直接转换大型复杂项目,我们会持续对输出web的能力进行优化,不断的建全更全面的适用范围和开发体验。

# 技术实现

与小程序平台间的差异相比,web平台和小程序平台间的差异要大很多,小程序相当于是基于web技术的上层封装,所以不同于我们跨平台输出其他小程序平台时以编译转换为主的思路,在输出web时,我们更多地采用了封装的方式来抹平组件/Api和底层环境的差异,与当前大部分的跨平台框架相仿。但与当前大部分跨平台框架以web MVVM框架为base输出到小程序上运行的思路不同,我们是以Mpx小程序增强语法为基础输出到web中运行,前面说过小程序本身是基于web技术进行的实现,小程序->web的转换在可行性和兼容性上会好一些。

在具体实现上,Mpx项目输出到web中运行时在组件化和路由层面都是基于Vue生态实现,所以可以将Mpx的跨端输出产物整合到既有的Vue项目中,或者在条件编译中直接使用Vue语法进行web端的实现。

# 使用方法

使用@mpxjs/cli创建新项目时选择跨平台并选择输出web后,即可生成可输出web的示例项目,运行npm run build:web,就会在dist/web下输出构建后的web项目,并启动静态服务预览运行。

# 支持范围

目前对输出web的通用能力支持已经非常完备,下列表格中显示了当前版本中已支持的能力范围

# 模板指令

指令名称 是否支持
Mustache数据绑定
wx:for
wx:for-item
wx:for-index
wx:key
wx:if
wx:elif
wx:else
wx:model
wx:model-prop
wx:model-event
wx:model-value-path
wx:model-filter
wx:class
wx:style
wx:ref
wx:show

# 事件绑定方式

绑定方式 是否支持
bind
catch
capture-bind
capture-catch

# 事件名称

事件名称 是否支持
tap
longpress
longtap

web同名事件默认全部支持,已支持组件的特殊事件默认为支持,不支持的情况下会在编译时抛出异常

# 基础组件

组件名称 是否支持 说明
audio
block
button
canvas
checkbox
checkbox-group
cover-view
form
image
input
movable-area
movable-view
navigator
picker
picker-view
progress
radio
radio-group
rich-text
scroll-view scroll-view 输出 web 底层滚动依赖 BetterScroll (opens new window) 实现,支持额外传入以下属性:

scroll-options: object
可重写 BetterScroll 初始化基本配置
若出现无法滚动,可尝试手动传入 { observeDOM: true }

update-refresh: boolean
Vue updated 钩子函数触发时,可用于重新计算 BetterScroll

tips: 当使用下拉刷新相关属性时,由于 Vue 数据响应机制的限制,在 web 侧可能出现下拉组件状态无法复原的问题,可尝试在 refresherrefresh 事件中,手动将 refresher-triggered 属性值设置为 true
swiper swiper 输出 web 底层滚动依赖 BetterScroll (opens new window) 实现,支持额外传入以下属性:

scroll-options: object
可重写 BetterScroll 初始化基本配置
当滑动方向为横向滚动,希望在另一方向保留原生的滚动时,scroll-options 可尝试传入 { eventPassthrough: vertical },反之可将 eventPassthrough 设置为 horizontal
swiper-item
switch
slider
text
textarea
video
view
web-view

在项目的app.json 中配置 "style": "v2"启用新版的组件样式,涉及的组件有 button icon radio checkbox switch slider在输出web时也与小程序保持了一致

# 生命周期

生命周期名称 是否支持
onLaunch
onLoad
onReady
onShow
onHide
onUnload
onError

onServerPrefetch|是 -created|是 -attached|是 -ready|是 -detached|是 -updated|是 -serverPrefetch|是

# 应用级事件

应用级事件名称 是否支持
onPageNotFound
onPageScroll
onPullDownRefresh
onReachBottom
onResize
onTabItemTap

# 组件配置

配置项 支持度
properties 部分支持,observer不支持,请使用watch代替
data 支持
watch 支持
computed 支持
relations 支持
methods 支持
mixins 支持
pageLifetimes 支持
observers 不支持,请使用watch代替
behaviors 不支持,请使用mixins代替

# 组件API

api名称 支持度
triggerEvent 支持
$nextTick 支持
createSelectorQuery/selectComponent 支持

# 全局API

api名称 支持度
navigateTo 支持
navigateBack 支持
redirectTo 支持
request 支持
connectSocket 支持
SocketTask 支持
EventChannel 支持
createSelectorQuery 支持
base64ToArrayBuffer 支持
arrayBufferToBase64 支持
nextTick 支持
set 支持
setNavigationBarTitle 支持
setNavigationBarColor 支持
setStorage 支持
setStorageSync 支持
getStorage 支持
getStorageSync 支持
getStorageInfo 支持
getStorageInfoSync 支持
removeStorage 支持
removeStorageSync 支持
clearStorage 支持
clearStorageSync 支持
getSystemInfo 支持
getSystemInfoSync 支持
showModal 支持
showToast 支持
hideToast 支持
showLoading 支持
hideLoading 支持
onWindowResize 支持
offWindowResize 支持
createAnimation 支持

# JSON配置

配置项 是否支持
backgroundColor
backgroundTextStyle
disableScroll
enablePullDownRefresh
onReachBottomDistance
packages
pages
navigationBarBackgroundColor
navigationBarTextStyle
navigationBarTitleText
networkTimeout
subpackages
tabBar
usingComponents

# 拓展能力

能力 是否支持
fetch
i18n

# 小程序其他原生能力

能力 支持度
wxs 支持
animation 支持组件的animation属性,支持所有animation对象方法(export、step、width、height、rotate、scale、skew、translate等等)
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/plugin.html b/docs-vuepress/.vuepress/dist/guide/advance/plugin.html deleted file mode 100644 index 19aa35df50..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/plugin.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - 小程序插件 | Mpx框架 - - - - - - - - - -

# 小程序插件

插件,是可被添加到小程序内直接使用的功能组件,是对一组 js 接口、自定义组件或页面的封装,。开发者可以像开发小程序一样开发一个插件,供其他小程序使用。同时,小程序开发者可直接在小程序内使用插件,无需重复开发,但是在使用第三那个插件时,无法看到插件的代码。插件适合用来封装自己的功能或服务,提供给第三方小程序进行展示和使用。

开发小程序插件,大致要经过 开通插件功能,填写开发信息,提交审,发布,管理插件使用申请。同时在原生小程序使用插件,要先发出插件申请,等待使用申请通过,插件所有者还可以进行拒绝。 -原生小程序开发插件请移步:

新建插件类型的项目后,如果创建示例项目,则项目中将包含三个目录:

  • plugin 目录:插件代码目录。
  • miniprogram 目录:放置一个小程序,用于调试插件。
  • doc 目录:用于放置插件开发文档。

,插件会同时有多个线上版本,由使用插件的小程序决定具体使用的版本号。

# 如何编写一个插件

推荐使用 mpx 官方脚手架 @mpxjs/cli 创建一个小程序插件项目来快速的进入插件开发阶段,首先全局安装 @mpxjs/cli

npm i -g @mpxjs/cli
-

然后使用 cli 初始化项目

mpx create <project-name>
-? 请选择小程序项目所属平台(目前仅微信下支持跨平台输出) wx
-? 是否需要跨小程序平台 No
-? 是否需要使用小程序云开发能力 No
-? 是否是个插件项目?(不清楚请选 No !什么是插件项目请看微信官方文档!) Yes
-? 是否需要typescript No
-? 项目描述 A mpx project
-? 请输入小程序的Appid touristappid
-

文件目录

  src
-  |-- miniprogram // 目录:放置一个小程序,用于调试插件。
-  |   --pages
-  |   --app.mpx // 引入插件调试
-  |-- plugin // 目录:插件代码目录
-  |   --components // 插件组件
-  |     -- list.mpx // 插件提供的列表组件
-  |   --plugin.json // 插件配置文件
-

我们在 plugin/components/list.mpx 中开发插件中的列表组件,开发完成后,在plugin.json中我们向使用者小程序开放的所有自定义组件、页面和 js 接口,格式如下:

代码示例:

{
-  "publicComponents": {
-    "list": "./components/list" // 使用mpx 中的webpack 路径引入规范
-  },
-  "pages": {
-    "hello-page": "./pages/hello-page"
-  }
-}
-

运行 npm run build/serve 构建小程序产物,在 dist 文件夹下,生成最终的小程序插件产物,使用微信开发者工具,打开代码片段菜单栏,选择插件模式,打开 dist 文件夹。

我们可以像小程序一样预览和上传,但插件没有体验版,同时我们通常将 miniprogram 下的代码当做使用插件的小程序代码,来进行插件的调试和测试。

在开发完插件之后,我们可以上传插件代码,在小程序管理后台进行提交发布审核,审核通过后,就可以提供给第三方小程序使用我们的插件了。

使用 mpx 开发插件的优势相似于使用 mpx 开发小程序项目,可以使用 mpx 的各种增强特性以及跨平台输出的特性,提高开发效率和插件性能。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/progressive.html b/docs-vuepress/.vuepress/dist/guide/advance/progressive.html deleted file mode 100644 index 7dc46cdf63..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/progressive.html +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - 原生渐进迁移 | Mpx框架 - - - - - - - - - -

# 原生渐进迁移

已有项目 期望接入 Mpx,可根据项目或人力情况选择如何迁移,Mpx 不要求用户一次性用上框架的所有东西。

  1. 项目初始期:可以考虑一次性转为 Mpx,此时迁移成本比较低
  2. 项目成熟期:若人力有限,可选择逐步将原生小程序转为Mpx,而且 不需要对原有代码做全局重写。可参考此demo:Mpx渐进接入demo (opens new window)。 -
    • 可以保持原有代码不变,新的组件、页面期望使用 Mpx 某些特性时才引入 Mpx。(推荐新模块引 Mpx,老模块逐步迁移 Mpx)
    • 用 Mpx 编写新的页面、组件,再局部导出对应的页面、组件,反向应用到现有的原生小程序项目中。见导出原生一节。(建议优先考虑老项目渐进改为 Mpx,而不是反向 Mpx 输出原生小程序的模式)

# 原生接入

有些时候,我们需要在Mpx工程中使用原生小程序组件:

  • 通过npm引用安装第三包
  • 将第三方包源码拷贝到本地src目录下

注:Mpx并不限制第三方包的格式。开发者可以自己参考小程序官方的开发第三方自定义组件 (opens new window)

# 原理

根据unsingComponents中设定的路径,Mpx会去查找包入口的js文件。然后提取入口文件所在的目录中的js json wxss wxml进行编译

编译带来的好处是,常规的拷贝操作,会造成组件内部的依赖缺失,以及冗余代码被打包。而执行了编译,使得Mpx可以精确的收集依赖,这表现在:

  • js文件中的依赖也会被打包,没有被加载的依赖库不会打包,减小体积
  • json文件的usingComponents会被解析,因此原生组件内部可以再引用其他原生组件,甚至是mpx组件
  • wxss中引用外部样式
  • wxml中的图片资源会被打包

例如:使用第三方组件库时,很多组件可能并未使用,如果按照官方给出的组件库使用方式,会将整个组件库放进项目。
-而采用Mpx这种方式则只会引入使用了的组件,所以如果你喜欢vant的按钮,iview的输入框,ColorUI的布局,欢迎尝试mpx。
-(本段内容具有时效性,未来微信可能会有优化,毕竟一开始微信连npm都不支持)

# 例子

文件目录

node_modules
-|-- npm-a-wx-component // npm安装
-|   --package.json
-|   --src
-|     --index.js
-|     --index.json
-|     --index.wxss
-|     --index.wxml
-|-- npm-b-wx-component // npm安装
-|   --package.json
-|   --src
-|     --index.js
-|     --index.json
-|     --index.wxss
-|     --index.wxml
-component
-│-- container.mpx 
-│-- com-a.mpx 
-|-- src-wx-component // 手动拷贝
-|  --index.js
-|  --index.json
-|  --index.wxss
-|  --index.wxml
-
-

container.mpx

<template>
-  <view>
-    <!-- mpx组件 -->
-    <com-a></com-a>
-    <!-- npm安装的原生组件 -->
-    <npm-a-wx-component></npm-a-wx-component>
-    <!-- 手动拷贝到工程的原生组件 -->
-    <src-wx-component></src-wx-component>
-  </view>
-</template>
-
-<script type="application/json">
-  "usingComponents": {
-    "com-a": "./com-a",
-    "npm-a-wx-component": "npm-a-wx-component",
-    "src-wx-component": "./src-wx-component"
-  }
-</script>
-

node_modules/npm-a-wx-component/src/index.wxml

<template>
-  <view>
-    <view>this is a native component</view>
-    <!-- 原生组件内部使用原生组件 -->
-    <npm-b-wx-component></npm-b-wx-component>
-  </view>
-</template>
-

node_modules/npm-a-wx-component/src/index.json

{
-  "usingComponents": {
-    "npm-b-wx-component": "npm-b-wx-component"
-  }
-}
-

# 原生page支持

原生自定义组件的支持已经基本能保证第三方 UI 库和 Mpx 的完美结合,但如果是用户存在已经开发好的小程序,在后续的迭代中发现了 Mpx 想使用,就需要用户手工将4个文件变成 Mpx 文件,这不够友好。

Mpx 提供了对原生页面的支持,允许项目中存在原生小程序文件(wxml,js,json,wxss)和 Mpx 文件,两者可以混合使用,通过 webpack 的构建将两者完美混合在一起生成最终的 dist。

使用方式和组件相似。

# 原生导出

通过导出原生能力,你可以将一个 Mpx 项目融回到原生小程序项目中。有两种做法:

  • 一是局部导出部分页面、组件
  • 二是完整导出一个 Mpx 项目。

# 导出部分页面/组件

使用 Mpx 开发的页面/组件也可以局部导出为纯粹的普通的原生小程序页面/组件,整合到已有的原生小程序中。

  1. 修改 webpack config 中 entry 一项,将 app 改为对应的页面/组件即可。

在路径后追加 ?isPage 来声明独立页面构建,构建产物为该页面的独立原生代码,在路径后追加 ?isComponent 来声明独立组件构建,构建产物为该组件的独立原生代码。

请参考下面的例子,注意resolve时候最后的query不可以省略,一定要按正确的类型声明这是一个组件or页面。

例子:

// 
-module.exports = merge(baseWebpackConfig, {
-  name: 'main-compile',
-  // entry point of our application
-  entry: {
-    // 此处以mpx脚手架生成的项目为例
-    
-    // before
-    // app: resolveSrc('app.mpx')
-    
-    // after,这里"pages/dindex"代表将原页面导出到output目录下的pages目录,文件名改为dindex.*
-    'pages/dindex': resolveSrc('./pages/index.mpx?isPage'), // ?后标识导出类型
-    'components/dlist': resolveSrc('./components/list.mpx?isComponent')
-  }
-})
-
  1. 执行 webpack 打包命令
  2. 拷贝打包后 dist 里所有文件到原生微信小程序项目根目录即可正常工作。

# 完整导出

举例:假如我们使用 Mpx 开发了一个完整的项目,这个项目可能包含多个页面,这些页面组合完成一个完整的功能。一般可能是公共需求,比如登录/用户中心等公共模块

如果其它接入方想复用这一公共模块,考虑有以下两种情形

  • 接入方也是 Mpx 框架开发的项目,直接迁移
  • 接入方是原生开发,这时我们希望能将整个项目完整导出成原生,并让接入方顺利使用。

其实观察下 Mpx 项目的打包结构,结构是非常简单的,页面/组件都很规矩地放在对应文件夹里的,所以删掉app.json/app.js/app.wxss/project.config.json几个文件后直接整个拷贝即可。

完整导出整个项目的做法可以是这样:

  1. 确认页面路径不要冲突,一般这种公共模块项目,路径上就不要占据/pages/index/index,页面路径Mpx是不会修改的,所以定一个/pages/{模块名}/{页面名}就好。

  2. app.*的内容都要删掉的,所以全局样式都应该写在独立的文件中(wxss),全局配置有什么特殊的要告知接入方(json),因为App.js会被舍弃,所以入口js要抽出来(js)。

  3. 如果有要导出的入口文件,需要给output增加配置:

// webpack.conf
-module.exports = {
-  entry: {
-    app: resolveSrc('app.mpx'),
-    index: resolveSrc('index.js') // 导出的入口文件,若没有可不写
-  },
-  output: {
-    libraryTarget: 'commonjs2',
-    libraryExport: 'default' // 若export default导出需要写这个,module.exports可省略
-  },
-  // ... 略
-}
-
  1. 整个复制进接入方的项目里,注册对应的页面,然后就可以正常使用了!
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html b/docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html deleted file mode 100644 index b6d04c03a9..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/provide-inject.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - 依赖注入(Provide/Inject) | Mpx框架 - - - - - - - - - -

# 依赖注入(Provide/Inject)

# 适用场景

通常情况下,从父组件向子组件传递数据时,我们会使用 props 向下传递。如果在一颗组件层级嵌套很深的组件树中,某个深层的子组件依赖一个较远的祖先组件中的部分数据。在这种情况下,如果仅使用 props 则必须将其沿着组件链路逐级传递下去,这就是 prop 逐级透传(prop-drilling) 问题。

使用 provide 和 inject 可以帮助我们解决这一麻烦问题:一个父组件相对于其所有的后代组件,会作为依赖提供者。任何后代的组件树,无论层级有多深,都可以注入由父组件提供给整条链路的依赖。

组件间 provide-inject 示意图

# Provide 提供

# 组合式语法

在组合式语法中,需要使用到 provide() 函数,provide() 函数接收两个必填参数:

  1. 第一个参数表示注入名,是一个字符串或者 Symbol。后代组件会用注入名来查找期望注入的值,一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。使用 Symbol 作为注入名时,只能使用组合式语法而非选项式语法。
  2. 第二个参数表示提供值,值可以是任意类型,包括响应式的状态,比如一个 refreactive 或者 computed 计算值。
<script setup>
-import { computed, provide, ref } from '@mpxjs/core'
-
-// 静态数据
-provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
-
-// 使用 Symbol 作为注入名
-const key = Symbol()
-provide(key, 'hello!')
-
-// 响应式数据
-const count = ref(1)
-const double = computed(() => count.value * 2)
-provide('count', count)
-provide('double', double)
-
-</script>
-

# 选项式语法

选项式语法的 setup() 中,用法和组合式 API 一致。另外针对选项式语法,我们也提供了 provide 选项,它是一个对象或返回一个对象的函数

对于 provide 对象上的每一个属性,后代组件会用其 key 为注入名查找期望注入的值,属性的值就是要提供的数据。

如果我们需要提供依赖当前组件实例的状态 (比如在 data 属性中定义的数据),那么需要使用函数形式的 provide 选项。需要注意的是,这 并不会 使注入保持响应性,如果需要保证注入方和供给方之间的响应性链接,我们需要使用 computed() 函数提供一个计算属性。

















 





<script>
-import { createComponent, computed } from '@mpxjs/core'
-
-createComponent({
-  data: {
-    count: 1,
-  },
-  // 1. 静态数据可以选择直接使用对象形式
-  provide: {
-    message: 'hello'
-  },
-  // 2. 选择使用函数的形式,可以访问到 `this`
-  provide() {
-    return {
-      count: this.count
-      // 显式提供一个计算属性
-      message: computed(() => this.count * 2)
-    }
-  }
-})
-</script>
-

# 应用级顶层 provide

前面介绍的是在一个组件中提供依赖,我们还可以在整个应用层面提供依赖,这样整个应用中所有组件都可以使用。

import { createApp } from '@mpxjs/core'
-
-createApp({
-  // 应用层 provide
-  provide() {
-    return {
-      'appMessage': 'provide from App scope!'
-    }
-  }
-})
-

# Inject 注入

# 组合式语法

inject() 函数最多接收三个参数:

  • 第一个参数必填,表示注入名,字符串类型。
  • 第二个参数可选,表示默认值。 -
    • 如果在注入一个值时不确定是否有提供者,那么应该声明一个默认值。如果既没有提供者也没有默认值,则会抛出一个运行时警告。
  • 第三个参数可选,表示是否将默认值视为工厂函数,布尔类型。 -
    • 某些场景下,默认值可能需要通过调用一个函数或初始化一个类来生成,或者某些非基础类型数据创建开销比较大,请使用工厂函数来创建默认值。
<script setup>
-import { inject } from '@mpxjs/core'
-
-// 注入 message 依赖
-const value = inject('message')
-// 创建默认值
-const value = inject('message', 'default value')
-// 使用工厂函数创建默认值
-const value = inject('key', () => new ExpensiveClass(), true)
-</script>
-

如果提供的值是一个 ref,注入进来的会是该 ref 对象,而不会自动解包为其内部的值。这使得注入方能够通过 ref 对象和提供方保持响应性链接。

# 选项式语法

选项式语法的 setup() 中,用法和组合式 API 一致。另外针对选项式语法,我们也提供了 inject 选项,它是一个数组或一个对象

  1. 数组形式使用。数组中的每一项对应一个注入名,注入的属性会以同名的 key 暴露到组件实例上。如下示例中,注入名 "message" 在注入后以 this.message 的形式暴露。另外,inject 会在组件自身的状态之前被解析,意味着你可以在 data() 中访问到注入的属性。





 










<script>
-import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  // 数组形式使用 `inject` 选项
-  inject: ['message', 'count'],
-  data() {
-    return {
-      // 可以在 `data()` 中访问到注入的属性
-      fullMessage: this.message,
-      fullCount: this.count
-    }
-  }
-})
-</script>
-
  1. 对象形式使用。相比数组形式,对象形式支持注入别名默认值。如下示例,通过 from 属性指定原注入名,通过 default 属性指定默认值。
<script>
-import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  inject: {
-    myCount: 'count', // 按注入名 'count' 注入
-    message: {
-      from: 'message', // 'message' 是原注入名,当与原注入名同名时,这个属性是可选的
-      default: 'default value' // 默认值
-    },
-    user: { // 与原注入名 'user' 同名
-      default: () => ({ name: 'Wang' }) // 使用工厂函数创建默认值
-    }
-  }
-})
-</script>
-

# 避免注入名潜在冲突

如果你正在构建大型的应用,包含非常多的依赖提供,那么随处定义的注入名容易存在潜在的同名冲突。在 Mpx 实现中,同名的注入名会覆盖之前已有的注入名对应的提供值。

针对大型应用的最佳实践,我们通常推荐在一个单独的文件中导出这些注入名,可以结合业务模块功能来统一注入名的命名和管理规范。以下示例仅供参考,具体实现请依据自身实际场景确定。

/** 推荐创建一个单独的文件来管理和导出注入名 */
-
-// 1. 使用枚举类型
-export const InjectionKeys = {
-  user: 'user',
-  auth: 'auth',
-  product: 'product',
-}
-
-// 2. 按业务模块功能划分命名,不容易命名冲突,可读性高
-export const InjectionKeys = {
-  user: {
-    service: 'user:service',
-    store: 'user:store',
-  },
-  auth: {
-    service: 'auth:service',
-    token: 'auth:token',
-  },
-  product: {
-    list: 'product:list',
-    details: 'product:details',
-  },
-}
-
-// 3. 使用 Symbol 创建唯一注入名,或再结合命名空间
-export const InjectionKeys = {
-  user: Symbol('user'),
-  auth: Symbol('auth'),
-  product: Symbol('product'),
-}
-
-// 4. 自行实现类似 Symbol polyfill 的命名生成函数
-export const createInjectionKey = (module, key) => `${module}:${key}`
-export const InjectionKeys = {
-  user: createInjectionKey('user', 'service'),
-  auth: createInjectionKey('auth', 'token'),
-  product: createInjectionKey('product', 'list'),
-}
-

# TS 类型支持

直接使用字符串注入 key 时,注入值的类型默认推导会是 unknown,需要通过泛型参数显式声明。因为无法保证运行时一定存在这个 provide,所以推导类型也可能是 undefined。当声明一个默认值后,这个 undefined 类型就可以成功被移除。

<script setup lang="ts">
-import { inject } from '@mpxjs/core'
-
-const foo = inject('foo') // 类型:unknown
-const foo = inject<string>('foo') // 类型:string | undefined
-const foo = inject<string>('foo', 'default value') // 类型:string ✅
-</script>
-

当然,如果你已经确定注入名肯定被提供了,也可以强制断言。

const foo = inject('foo') as string
-

如果使用 Symbol 作为注入名,可以使用我们提供的 InjectionKey 泛型接口,使用它对注入名进行注解或者断言后,可以用来在不同组件之间同步注入值的类型。建议将注入 key 放在单独文件,这样方便在多个组件中导入使用。




 








import { provide, inject } from '@mpxjs/core'
-import type { InjectionKey } from '@mpxjs/core'
-
-export const key: InjectionKey<string> = Symbol() // 类型注解
-// const key = Symbol() as InjectionKey<string> // 类型断言写法等效
-
-provide(key, 'foo') // 若默认值是非字符串则会 TS 类型报错
-
-const foo = inject(key) // ✅ foo 的类型:string | undefined
-const foo = inject(key, 'default value') // ✅ foo 的类型:string
-const foo = inject(key, 1) // ❌ 默认值是非字符串则会 TS 类型报错
-

# 跨端差异

  • Mpx 输出 Web 端后,使用规则与 Vue 一致,provide/inject 的生效范围严格遵行父子组件关系,只有父组件可以成功向子孙组件提供依赖。
  • Mpx 输出小程序端会略有不同,由于小程序原生框架限制,暂时无法在子组件获取真实渲染时的父组件引用关系,所以不能像 Vue 那样基于父组件原型继承来实现 provide。在 Mpx 底层实现中,我们将组件层的 provide 挂载在所属页面实例上,相当于将组件 scope 提升到页面 scope,可以理解成一种“降级模拟”。当然,这并不影响父组件向子孙组件 provide 的能力,只是会额外存在“副作用”:同一页面中的组件可以向页面中其他所有在其之后渲染子组件提供依赖。比如同一页面下的组件 A 可以向后渲染的兄弟组件 B 的子孙组件提供数据,这在 Web 端是不允许的。因此,针对小程序端可能出现的“副作用”需要开发者自行保证,可以结合上述注入名的管理优化来规避。
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html b/docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html deleted file mode 100644 index 449adbb2ab..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/resource-resolve.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - 资源路径获取 | Mpx框架 - - - - - - - - - -

# 资源路径获取

Mpx在构建时,如果引用的页面不存在于当前 app.mpx 所在的上下文中,例如存在于 npm 包中,为避免和本地声明的其他 page 路径冲突,Mpx 会对页面路径进行 hash 化处理; -处理组件路径时也会添加 hash 防止路径名冲突,hash 化处理后最终的文件名是 name+hash+ext 的格式;对于图片等资源路径输出也会进行 hash 化处理。

开发者经常会面临以下问题:

  • 希望获取 hash 化之后的页面/组件/图片路径
  • 在页面路径变化时(分包名更改或页面名修改),需要手动修改散落在代码中各处写死的资源路径

# ?resolve

为了解决以上开发者痛点,Mpx 框架提供了资源路径自动获取能力,只需要在资源引用路径后加上?resolve, -Mpx 在编译时会生成一个资源路径处理模块,该模块暴露出资源对应的真实输出路径,从而可以实现在页面/组件/图片路径变化时正确获取输出路径,并避免写死资源路径。

获取并使用页面路径

// app.json 中注册分包,此处仅列出 packages 语法示例
-{
-    packages: [
-        '@someNpm/app.mpx?root=someNpm'
-    ]
-}
-
-// import 语法获取并使用页面路径
-import subPackageIndexPage from '@someNpm/subpackage/pages/index.mpx?resolve'
-mpx.navigateTo({
-    url: subPackageIndexPage
-})
-
-// require 语法获取并使用页面路径
-mpx.navigateTo({
-    url: require('@someNpm/subpackage/pages/index.mpx?resolve')
-})
-

获取并使用组件资源路径

我们在使用小程序 relations 语法时,需要获取组件路径,这时就可使用 ?resolve 语法

import { createComponent } from '@mpxjs/core'
-import someComponentItem from './some-component-item?resolve'
-
-createComponent({
-    relations: {
-        [someComponentItem]: {
-            type: 'child'
-        }
-    }
-})
-

获取并使用图像资源路径

<template>
-    <view wx:style="{{someStyle}}">test</view>
-</template>
-
-<script>
-    import mpx, { createPage } from '@mpxjs/core'
-    import iconPath from '../assets/icon.png?resolve'
-    
-    createPage({
-        computed: {
-            someStyle() {
-                return `background-image url(${iconPath})`
-            }
-        }
-    })
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/size-report.html b/docs-vuepress/.vuepress/dist/guide/advance/size-report.html deleted file mode 100644 index 9246a7aa13..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/size-report.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - 包体积分析 | Mpx框架 - - - - - - - - - -

# 包体积分析

目前业内主流小程序平台都对小程序的代码包设置了严格的体积限制,微信是单包 2MB,总包 16MB,支付宝是单包 2MB,总包 8MB;包体积作为有限的资源,在小程序业务开发中异常重要,特别对于像滴滴出行这样的大型复杂业务。

Mpx 在包体积控制上做了很多工作,主要包括:

此外由于 Mpx 的编译构建完全基于 webpack,也能够直接复用webpack生态自带的代码压缩,模块复用,tree shaking,side effects 等能力对代码体积进行优化。

但是系统能做的工作终究有限,在一些人为不规范操作的影响下,最终输出的项目依然可能存在大量优化空间,比如在业务中不会被调用但无法被 tree shaking / side effects 移除的代码,因为 npm 依赖版本不统一造成的重复依赖,未经压缩的图像静态资源等,在大型项目中,这些问题想要被发现会非常困难,因此我们非常需要一个体积分析工具来管控项目体积和发现隐藏的体积问题。

# 与 webpack-bundle-analyzer 的区别

webpack-bundle-analyzer 提供了非常完善的体积分析和可视化展示能力,但是在 Mpx 构建输出小程序场景下,其所提供的能力还是有所缺失:

  • 只能对 js 资源进行模块体积分析,而小程序的输出中包含了大量的非 js 静态资源,如 wxss / wxml / json 等,这些资源的体积都不会被统计分析;
  • 没有针对某个分包进行体积分析的能力,由于小程序中存在对单一分包的体积限制,我们的体积往往会集中在主包和主要业务分包中,以分包维度进行体积分析的能力非常必要;
  • 无法以特定的输入范围为维度进行体积的统计分析,这个能力诉求更多地出现在跨团队合作的复杂小程序当中,如滴滴出行。在这种场景下,接入合作的各方会更加关注己方引入的体积,并进行针对性地优化。

由于上述原因,我们提供了@mpxjs/size-report插件提供包体积分析能力,弥补了 webpack-bundle-analyzer 的能力缺失,为业务提供了便捷准确的包体积管控优化抓手。

# 使用方法

# 安装插件

npm i @mpxjs/size-report --save-dev
-

# 配置插件

在项目 webpack.config 文件中配置使用 @mpxjs/size-report 插件即可使用,简单示例如下:

// vue.config.js
-const MpxSizeReportPlugin = require('@mpxjs/size-report')
-module.exports = defineConfig({
-  configureWebpack() {
-    return {
-      plugins: [
-        new MpxSizeReportPlugin(
-          {
-              // 本地可视化服务相关配置
-              server: {
-                  enable: true, // 是否启动本地服务,非必填,默认 true
-                  autoOpenBrowser: true, // 是否自动打开可视化平台页面,非必填,默认 true
-                  port: 0, // 本地服务端口,非必填,默认 0(随机端口)
-                  host: '127.0.0.1', // 本地服务host,非必填
-              },
-              // 体积报告生成后输出的文件地址名,路径相对为 dist/wx 或者 dist/ali
-              filename: '../report.json',
-              // 配置阈值,此处代表总包体积阈值为 16MB,分包体积阈值为 2MB,超出将会触发编译报错提醒,该报错不阻断构建
-              threshold: {
-                  size: '16MB',
-                  packages: '2MB'
-              },
-              // 配置体积计算分组,以输入分组为维度对体积进行分析,当没有该配置时结果中将不会包含分组体积信息
-              groups: [
-                  {
-                      // 分组名称
-                      name: 'vant',
-                      // 配置分组 entry 匹配规则,小程序中所有的页面和组件都可被视为 entry,如下所示的分组配置将计算项目中引入的 vant 组件带来的体积占用
-                      entryRules: {
-                          include: '@vant/weapp'
-                      }
-                  },
-                  {
-                      name: 'pageGroup',
-                      // 每个分组中可分别配置阈值,如果不配置则表示
-                      threshold: '500KB',
-                      entryRules: {
-                          include: ['src/pages/index', 'src/pages/user']
-                      }
-                  },
-                  {
-                      name: 'someSdk',
-                      entryRules: {
-                          include: ['@somegroup/someSdk/index', '@somegroup/someSdk2/index']
-                      },
-                      // 有的时候你可能希望计算纯 js 入口引入的体积(不包含组件和页面),这种情况下需要使用 noEntryRules
-                      noEntryRules: {
-                          include: 'src/lib/sdk.js'
-                      }
-                  }
-              ],
-              // 是否收集页面维度体积详情,默认 false
-              reportPages: true,
-              // 是否收集资源维度体积详情,默认 false
-              reportAssets: true,
-              // 是否收集冗余资源,默认 false
-              reportRedundance: true,
-              // 展示某些分包资源的引用来源信息,默认为 []
-              showEntrysPackages: ['main']
-          }
-      )
-      ]
-    }
-  }
-})
-

参考上述示例进行配置后,构建代码后,dist 目录下会产出 report.json 文件,里边是项目的具体体积信息,关于输入 json 的简单示例如下:

{
-    // 项目体积概要,大部分情况下,我们只需要看这部分就足够了
-    "sizeSummary": {
-        // 分组体积概要,与上述配置文件中的 groups 对应
-        "groups": [
-            {
-                "name": "vant",
-                // 只有该分组包含的模块体积
-                "selfSize": "164.75KiB",
-                "selfSizeInfo": {
-                  // 该分组所占 shansong
-                  "shansong": "164.75KiB"
-                },
-                "sharedSize": "885.68KiB",
-                "sharedSizeInfo": {
-                  "main": "885.68KiB"
-                }
-            },
-        ],
-        // 项目各个分包以及主包体积概要
-        "sizeInfo": {
-            "main": "1000KiB",
-            "fenbao1": "200kiB"
-        },
-        // 项目总体积
-        "totalSize": "13468.85KiB",
-        // 项目静态资源总体积
-        "staticSize": "4880.58KiB",
-        // 项目chunk 文件总体积
-        "chunkSize": "8587.70KiB",
-        // 非依赖项体积大小
-        "copySize": "1KiB"
-    },
-    // 分组资源详细体积
-    "groupsSizeInfo": [
-        {
-            "name": "groupOne",
-            // group 自身包含的 module 详情列表
-            "selfEntryModules": [],
-            // group 与其他group 共有的 module 详情列表
-            "sharedEntryModules": [],
-            // 自身包含 module 体积
-            "selfSize": "",
-            // 自身包含 module 体积详情
-            "selfSizeInfo": {
-                "main": {},
-                "homepage": {}
-            },
-            // 与其他 group 共有 module 体积
-            "sharedSize": "",
-            // 与其他 group 共有 module 体积详情
-            "sharedSizeInfo": {
-                "main": {},
-                "homepage": {}
-            },
-
-        }
-    ],
-    // 项目资源详细体积报告
-    "assetsSizeInfo": {
-        "assets": [{
-            // 资源类型
-            "type": "chunk",
-            "name": "test",
-            // 分包名
-            "packageName": "main",
-            "size": "",
-            "modules": []
-        }]
-    }
-}
-

与此同时,如果你开启了本地可视化平台服务,可以直接通过可视化平台查看项目体积构成。默认开启自动打开平台网页或者手动打开后,整体页面展示如下图:

size-report (opens new window)

可视化平台中包含三部分功能,第一个体积分析如上图所示,主要是展示整体项目的体积总览,以及 group 体积列表和分包体积列表。

第二个功能是体积详情模块,在该模块中,可以最小颗粒度的查看包体积构成,通过 table 表格的层层展开可看到每个 group 中包含的分包,点击分包可看到该分包包含的静态资源和 js 模块详细列表,同时聚合模式可将分散的模块整合为具体的npm包名,方便宏观查看。此外为了方便用户定向查看某个特定资源的分布情况,列表上方可进行资源路径/名称搜索,该搜索支持模糊匹配,搜索结果为该资源在项目中 group 和 分包的分布情况。

size-report (opens new window)

第三个功能为体积对比功能,这里主要进行项目不同版本之间的体积大小变化比较,体积对比依赖插件生成的json文件,通过该功能,可具体的分析出包体积具体增大的group,分包,以及模块,给包体积优化提供定向目标。

size-report (opens new window)

# 业务实践

目前 sizeReport 工具在滴滴出行小程序, 花小猪, 青桔骑行, 特惠出行小程序以及一部分外部小程序中使用。

在滴滴出行小程序中,配置使用 @mpxjs/size-report 插件后使包体积管控和优化更加工程化:

  • 在 sizeReport 检测结果文件中,通过对 groupsSizeInfo 和 assetsSizeInfo 中assets 与 module 体积分析,我们发现了部分体积较大图片文件和css资源,通过将图片存 CDN 和删除冗余文件;

  • 基于各小程序平台对小程序总包,主包,分包有大小限制的原则,给各接入方配置了主包和首页分包体积大小占比阈值,在构建时检测到包体积超过阈值时,抛出 error 阻断构建,开发者可通过 size-report.json 中详细的包体积分析准确的找到体积变动点。

# 总结

Mpx sizeReport 工具对小程序体积计算有更细微(模块级别)的体积展示。 更加适合小程序开发场景的包体积分析。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/ssr.html b/docs-vuepress/.vuepress/dist/guide/advance/ssr.html deleted file mode 100644 index 809c77cb83..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/ssr.html +++ /dev/null @@ -1,190 +0,0 @@ - - - - - - SSR | Mpx框架 - - - - - - - - - -

# SSR

近些年来,SSR/SSG 由于其良好的首屏展现速度和SEO友好性逐渐成为主流的技术规范,但过去 Mpx 对 SSR 的支持不完善,使用 Mpx 开发的跨端页面一直无法享受到 SSR 带来的性能提升,在 2.9 版本中,我们对 Mpx 输出 web 流程进行了大量适配改造,解决了 SSR 中常见的内存泄漏、跨请求状态污染和数据预请求等问题,完整支持了基于 Vue 和 Pinia 的 SSR 技术方案。

# 配置使用 SSR

在 Vue SSR 项目中,我们一般需要提供 server entry 和 client entry 两个文件作为 webpack 的构建入口,然后通过 webpack 构建出 server bundle 和 client bundle。在用户访问页面时,在服务端使用 server bundle 渲染出 HTML 字符串,作为静态页面发送到客户端,然后在客户端使用 client bundle 通过水合(hydration)对静态页面进行激活,实现可交互效果,下图展示了 Vue SSR 的大致流程。

Vue SSR流程

Mpx SSR 核心基于 Vue SSR 实现,大致流程思路与 Vue 一致,不过为了保持与小程序代码的兼容性,在配置使用上有一些改动差异,下面我们详细展开介绍:

# 构建server/client bundle

SSR项目中,我们需要分别构建出 server bundle 和 client bundle,对于不同环境的产物构建,我们需要进行不同的配置。 -在 Vue 中,我们需要提供 entry-server.jsentry-client.js 两个文件分别作为 server 和 client 的构建入口,与 Vue 不同,在 Mpx 中我们通过编译处理与运行时增强生命周期实现了使用 app.mpx 作为统一构建入口,无需区分 server 和 client。

# 服务端构建配置

服务端配置中除了将 entry 制定为 app.mpx 及其它基础配置外,最重要的是安装 vue-server-renderer 包中提供的 server-plugin 插件,该插件能够构建产出 vue-ssr-server-bundle.json 文件供 renderer 后续消费。

// webpack.server.config.js
-const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
-
-module.exports = merge(baseConfig, {
-  // 将 entry 指向项目的 app.mpx 文件
-  entry: './app.mpx',
-  // ...
-  plugins: [
-   // 产出 `vue-ssr-server-bundle.json`
-    new VueSSRServerPlugin()
-  ]
-})
-

更加详细的配置说明可参考 Vue SSR的服务端配置 (opens new window)

# 客户端构建配置

类似服务端构建配置,在客户端构建中我们需要使用 vue-server-renderer 包中 client-plugin 插件来帮助我们生成客户端环境的资源清单 vue-ssr-client-manifest.json,并供 renderer 后续消费。

// webpack.client.config.js
-const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
-
-module.exports = merge(baseConfig, {
-  // 将 entry 指向项目的 app.mpx 文件
-  entry: './app.mpx',
-  // ...
-  plugins: [
-    // 产出 `vue-ssr-client-manifest.json`。
-    new VueSSRClientPlugin()
-  ]
-})
-

更加详细的配置说明可参考 Vue SSR的客户端配置 (opens new window)

# 准备页面模版

SSR 渲染中,我们创建 renderer 需要一个页面模板,简单的示例如下:

<!DOCTYPE html>
-<html lang="en">
-  <head><title>Hello</title></head>
-  <body>
-    <!--vue-ssr-outlet-->
-  </body>
-</html>
-

与 CSR 渲染模版不同,SSR 渲染模版中需要提供一个特殊的 <!--vue-ssr-outlet-->注释,标记 SSR 渲染产物的插入位置,如使用 @mpxjs/cli 脚手架创建 SSR 项目,该模版已经内置于脚手架中。

# 集成启动 SSR 服务

当我们准备好页面模版和双端构建产物后,我们就可以创建 renderer 并与 Node 服务进行集成,启动 SSR 服务,下面以 express 为例:

//server.js
-const app = require('express')()
-const { createBundleRenderer } = require('vue-server-renderer')
-// 通过 vue-server-renderer/server-plugin 生成的文件
-const serverBundle = require('../dist/server/vue-ssr-server-bundle.json')
-// 通过 vue-server-renderer/client-plugin 生成的文件
-const clientManifest = require('../dist/client/vue-ssr-client-manifest.json')
- // 页面模版文件
-const template = require('fs').readFileSync('../src/index.template.html', 'utf-8')
-// 创建 renderer 渲染器
-const renderer = createBundleRenderer(serverBundle, {
-    runInNewContext: false,
-    template,
-    clientManifest,
-});
-// 注册启动 SSR 服务
-app.get('*', (req, res) => {
-  const context = { url: req.url }
-  renderer.renderToString(context, (err, html) => {
-  	if (err) {
-  	  res.status(500).end('Internal Server Error')
-      return
-    }
-  	res.end(html);
-  })
-})
-app.listen(8080)
-

# SSR 生命周期

在 Mpx 2.9 的版本中,我们提供了三个全新用于 SSR 的生命周期,分别是onAppInitserverPrefetchonSSRAppCreated,以统一服务端与客户端的构建入口,下面展开介绍:

# onAppInit

该生命周期仅可在 App 中使用

在 SSR 中用户每发出一个请求,我们都会为其生成一个新的应用实例,onAppInit 生命周期会在应用创建 new Vue(...) 前被调用,其执行的返回结果会被合并到创建应用的 options

很常见的使用场景在于返回新的全局状态管理实例,Mpx 中提供了 @mpxjs/pinia 作为全局状态管理工具,我们可以在 onAppInit 中返回全新的 pinia 示例避免产生跨请求状态污染,示例如下:

// app.mpx
-import mpx, { createApp } from '@mpxjs/core'
-import { createPinia } from '@mpxjs/pinia'
-
-createApp({
-  // ...
-  onAppInit () {
-    const pinia = createPinia()
-    return {
-      pinia
-    }
-  }
-})
-

SSR 中仅支持使用 @mpxjs/pinia 作为状态管理工具,@mpxjs/store 暂不支持

# serverPrefetch

该生命周期可在 App/Page/Component 中使用,只在服务端渲染时执行

当我们需要在 SSR 使用数据预拉取时,可以使用这个生命周期进行,使用方法与 Vue 一致, 示例如下:

选项式 API:

import { createPage } from '@mpxjs/core'
-import useStore from '../store/index'
-
-createPage({
-  //...
-  serverPrefetch () {
-    const query = this.$route.query
-    const store = useStore(this.$pinia)
-    // return the promise from the action, fetch data and update state
-    return store.fetchData(query)
-  }
-})
-

组合式 API:

import { onServerPrefetch, getCurrentInstance, createPage } from '@mpxjs/core'
-import useStore from '../store/index'
-
-createPage({
-  setup () {
-    const store = userStore()
-    onServerPrefetch(() => {
-      const query = getCurrentInstance().proxy.$route.query
-      // return the promise from the action, fetch data and update state
-      return store.fetchData(query)
-    })
-  }
-})
-

关于数据预拉取更详细的说明可以查看这里 (opens new window)

# onSSRAppCreated

该生命周期仅可在 App 中使用,只在服务端渲染时执行

在 Vue SSR 项目中,我们会在 entry-server.js 中导出一个工厂函数,在该函数中实现创建应用实例、路由匹配和状态同步等逻辑,并返回应用实例 app

在 Mpx SSR 中,我们将这部分逻辑整合在 onSSRAppCreated 中执行,该生命周期执行时用户可以从参数中获取应用实例 app、路由实例 router、数据管理实例 pinia 和 SSR 上下文 context,在完成必要的操作后,该生命周期需要返回一个 resolve(app) 的 promise。

通常我们会在 onSSRAppCreated 中进行路由路径设置和数据预拉取后的状态同步工作,示例如下:

// app.mpx
-createApp({
-    // ...,
-    onSSRAppCreated ({ pinia, router, app, context }) {
-      return new Promise((resolve, reject) => {
-        // 设置服务端路由路径
-        router.push(context.url)
-        router.onReady(() => {
-          // 应用完成渲染时执行
-          context.rendered = () => {
-            // 将服务端渲染后得到的 pinia.state 同步到 context.state 中,
-            // context.state 会被自动序列化并通过 `window.__INITIAL_STATE__`
-            // 注入到 HTML 中,并在客户端运行时再读取同步
-            context.state = pinia.state.value
-          }
-          // 返回应用程序实例
-          resolve(app)
-        }, reject)
-      })
-    }
-})
-

上述示例代码等价于 Vue 中的 entry-server.js

// entry-server.js
-import { createApp } from './app'
-
-export default context => {
-  return new Promise((resolve, reject) => {
-    const { app, router, store } = createApp()
-    router.push(context.url)
-    router.onReady(() => {
-      // This `rendered` hook is called when the app has finished rendering
-      context.rendered = () => {
-        // After the app is rendered, our store is now
-        // filled with the state from our components.
-        // When we attach the state to the context, and the `template` option
-        // is used for the renderer, the state will automatically be
-        // serialized and injected into the HTML as `window.__INITIAL_STATE__`.
-        context.state = store.state
-      }
-      resolve(app)
-    }, reject)
-  })
-}
-

如用户没有配置 onSSRAppCreated,框架内部会执行兜底逻辑,以保障 SSR 的正常运行。

# 其他注意事项

  1. Mpx SSR 渲染支持 i18n 的功能,但为了防止内存泄漏,当前 i18n 实例不会随着每次请求而重新创建,这是由于 Vue2.x 版本插件机制的设计缺陷所造成的,因此在使用 i18n 进行 SSR 时可能会产生跨请求状态污染的问题,这个问题会在未来 Mpx 输出 web 切换为 Vue3 后完全解决。

  2. 在服务端渲染阶段,对于 global 全局对象访问修改,如__mpx, __mpxRouter, __mpxPinia 都可能导致全局状态污染,所以在服务端渲染阶段请尽量避免进行相关操作;对于存在全局访问修改的方法,如 getApp(), getCurrentPages() 等在服务端渲染中被调用时,会产生相关报错提示。

  3. 由于服务器无法收到 URL 中的 hash 信息,使用 SSR 时需要通过修改 mpx.config.webRouteConfig 将路由模式设置成 history 模式。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/store.html b/docs-vuepress/.vuepress/dist/guide/advance/store.html deleted file mode 100644 index c8a3cea770..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/store.html +++ /dev/null @@ -1,599 +0,0 @@ - - - - - - 状态管理(store) | Mpx框架 - - - - - - - - - -

# 状态管理(store)

Mpx 参考 vuex 设计实现了外部状态管理系统(store),其中的概念与 api 与 vuex 保持一致,为了更好地支持状态模块管理和跨团队合作场景,我们提出多实例 store 作为 vuex 中 modules 的替代方案,该方案在模块拆分及合并上的灵活性远高于 modules。

# 介绍

Store 是一个全局状态管理容器,能够轻松实现复杂场景下的组件通信需求,store 与简单的全局状态对象主要有以下两点不同:

  1. Store 中存放的状态是响应式的。当用户将 store 中的状态注入到组件以后,若 store 中状态发生变化,那么对应的组件也会得到更新。

  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径是显式地提交变更(commit mutation),这种方式能使整个应用中的状态变更变得可回朔可追踪,同时也更加安全。

# 创建 store

让我们来创建一个简单 store。创建过程直截了当,仅需要提供一个初始 state 对象和一些 mutation 方法,并调用 Mpx 暴露的 createStore 方法进行创建:

import {createStore} from '@mpxjs/core'
-
-const store = createStore({
-  state: {
-    count: 0
-  },
-  mutations: {
-    increment (state) {
-      state.count++
-    }
-  }
-})
-
-export default store
-

现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

store.commit('increment')
-console.log(store.state.count) // 1
-

接下来,我们将会更深入地探讨一些 store 的核心概念。让我们先从 State 开始。

# State

State 存放了 store 中的原始状态数据,可以通过 store.state 进行访问。

# 在组件中获取 state

Mpx 在小程序组件中实现了数据响应,而刚才提到 store 中的状态也是响应式的,组件中获取 store 状态最简单的方法就是建立一个计算属性,在计算属性中访问 store 中需要的状态数据并返回:

// store.js
-import {createStore, createComponent} from '@mpxjs/core'
-
-const store = createStore({
-  state: {
-    count: 0
-  },
-  mutations: {
-    increment (state) {
-      state.count++
-    }
-  }
-})
-
-createComponent({
-  computed: {
-    count () {
-      return store.state.count
-    }
-  }
-})
-

当 store.state.count 发生变化时, 组件中访问了 count 计算属性的 watcher 将得到响应。

于 vuex 中的不同的地方在于,vuex 奉行单一状态树,一个应用当中只存在一个 store 示例,用户能够在组件中通过this.$store隐式地访问到当前应用的 store;而 Mpx 当中为了追求灵活便捷的状态模块化管理及跨团队合作的能力,支持了多实例 store,用户需要显式地引入 store 实例,并通过计算属性将其注入到组件当中。

# MapState 辅助函数

当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性

import store from '../store'
-import {createComponent} from '@mpxjs/core'
-
-createComponent({
-  computed: store.mapState({
-    // 箭头函数可使代码更简练
-    count: state => state.count,
-
-    // 传字符串参数 'count' 等同于 state => state.count
-    countAlias: 'count',
-
-    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
-    countPlusLocalState (state) {
-      return state.count + this.localCount
-    }
-  })
-})
-

当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。

import store from '../store'
-import {createComponent} from '@mpxjs/core'
-
-createComponent({
-  computed: store.mapState([
-    // 映射 this.count 为 store.state.count
-    'count'
-  ])
-})
-

# 对象展开运算符

MapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符 (opens new window),我们可以极大地简化写法:

import store from '../store'
-import {createComponent} from '@mpxjs/core'
-
-createComponent({
-  computed: {
-    localComputed () { /* ... */ },
-    // 使用对象展开运算符将 mapState 返回的对象合并到计算属性中
-    ...store.mapState([
-      'count',
-      // ...
-    ])
-  }
-})
-

使用 store 并不意味着你需要将所有的状态放入 store。如果有些状态严格属于单个组件,最好将其放到组件内部的 data 当中。

# Getter

有时候我们需要从 state 中派生出一些状态,例如经过过滤的列表:

createComponent({
-  computed: {
-    doneTodos () {
-      return store.state.todos.filter(todo => todo.done)
-    }
-  }
-})
-

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,无论哪种方式都不是很理想。

这个时候我们可以在 store 中定义 getter 来完成这个功能,getter 可以简单认为是 store 中的计算属性。同计算属性一样,getter 的返回值会根据它的依赖被缓存起来,只有当它的依赖发生变化时才会被重新计算。

Getter 接受 state 作为第一个参数:

import {createStore} from '@mpxjs/core'
-
-const store = createStore({
-  state: {
-    todos: [
-      { id: 1, text: 'sth1', done: true },
-      { id: 2, text: 'sth2', done: false }
-    ]
-  },
-  getters: {
-    doneTodos: state => {
-      return state.todos.filter(todo => todo.done)
-    }
-  }
-})
-
-export default store
-

定义好的 getter 可以通过 store.getters 访问:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
-

Getter 也可以接受其他 getters 作为第二个参数, rootState作为第三个参数,rootState是模块化中引入的概念,之后会详细介绍:

getters: {
-  // ...
-  doneTodosCount: (state, getters, rootState) => {
-    return getters.doneTodos.length
-  }
-}
-
store.getters.doneTodosCount // -> 1
-

我们采用与state类似的方法将其注入到组件中进行访问:

computed: {
-  doneTodosCount () {
-    return store.getters.doneTodosCount
-  }
-}
-

# MapGetters 辅助函数

MapGetters 的作用与使用方法同上面提到的 mapState 高度类似,唯一的区别在于 mapGetters 用于映射 store 中的 getter:

import store from '../store'
-import {createComponent} from '@mpxjs/core'
-
-createComponent({
-  // ...
-  computed: {
-    // 使用对象展开运算符将 getter 混入 computed 对象中
-    ...store.mapGetters([
-      'doneTodosCount',
-      'anotherGetter',
-      // ...
-    ])
-  }
-})
-

如果你想将一个 getter 在组件中映射为另一个名字,使用对象形式:

store.mapGetters({
-  // 映射 this.doneCount 为 store.getters.doneTodosCount
-  doneCount: 'doneTodosCount'
-})
-

# Mutation

更改 store 中的状态的唯一方法是提交 mutation。Mutation 非常类似于事件:每个 mutation 都有一个字符串的类型(type) 和 一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,它接受 state 作为第一个参数:

import {createStore} from '@mpxjs/core'
-
-const store = createStore({
-  state: {
-    count: 1
-  },
-  mutations: {
-    increment (state) {
-      // 变更状态
-      state.count++
-    }
-  }
-})
-
-export default store
-

当你需要触发某个 mutation 时,你需要使用对应的 type 调用 store.commit 方法,就像触发某个事件一样:

store.commit('increment')
-

# 提交载荷(Payload)

你可以向 store.commit 传入额外的参数,作为 mutation 的载荷(payload)

// ...
-mutations: {
-  increment (state, n) {
-    state.count += n
-  }
-}
-
store.commit('increment', 10)
-

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且增强 mutation 的可读性:

// ...
-mutations: {
-  increment (state, payload) {
-    state.count += payload.amount
-  }
-}
-
store.commit('increment', {
-  amount: 10
-})
-

# Mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数

# 在组件中提交 Mutation

你可以在组件中使用 store.commit('increment') 提交 mutation,或者使用 store.mapMutations 辅助函数将组件中名为 incrementmethod 映射为 store.commit('increment') 调用。

import { createComponent } from '@mpxjs/core'
-import store from '../store'
-
-createComponent({
-  // ...
-  methods: {
-    ...store.mapMutations([
-      // 将 this.increment() 映射为 store.commit('increment')
-      'increment', 
-       // mapMutations 也支持载荷:将 this.incrementBy(amount) 映射为 store.commit('incrementBy', amount)
-      'incrementBy'
-    ]),
-    // mapMutations同样支持传入对象形式的参数进行别名映射
-    ...store.mapMutations({
-      // 将 this.add() 映射为 store.commit('increment')
-      add: 'increment'
-    })
-  }
-})
-

# Action

Action 类似于 mutation,不同在于:

  • Action 不能直接变更状态,但是可以提交 mutation 进行状态变更
  • Action 可以包含任意异步操作

让我们来创建一个简单的 action:

import {createStore} from '@mpxjs/core'
-
-const store = createStore({
-  state: {
-    count: 0
-  },
-  mutations: {
-    increment (state) {
-      state.count++
-    }
-  },
-  actions: {
-    increment (context) {
-      context.commit('increment')
-    }
-  }
-})
-
-export default store
-

Action 函数接受一个 context 对象,因此你可以调用 context.commit 提交一个 mutation,调用 context.dispatch 触发其他的 action 调用,或者通过 context.rootStatecontext.statecontext.getters 来获取全局state、局部state 和 全局 getters。

实践中,我们会经常用到 ES2015 的参数解构 (opens new window)来简化代码:

actions: {
-  increment ({ commit }) {
-    commit('increment')
-  }
-}
-

# 调用 action

Action 可以通过 store.dispatch 方法进行调用:

store.dispatch('increment')
-

乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:

actions: {
-  incrementAsync ({ commit }) {
-    setTimeout(() => {
-      commit('increment')
-    }, 1000)
-  }
-}
-

Action 同样支持载荷:

// 调用载荷
-store.dispatch('incrementAsync', {
-  amount: 10
-})
-

来看一个更加实际的购物车示例,涉及到调用异步 API提交多个 mutation

actions: {
-  checkout ({ commit, state }, products) {
-    // 把当前购物车的物品备份起来
-    const savedCartItems = [...state.cart.added]
-    // 发出结账请求,然后乐观地清空购物车
-    commit(types.CHECKOUT_REQUEST)
-    // 购物 API 接受一个成功回调和一个失败回调
-    shop.buyProducts(
-      products,
-      // 成功操作
-      () => commit(types.CHECKOUT_SUCCESS),
-      // 失败操作
-      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
-    )
-  }
-}
-

注意我们进行了一系列异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。

# 在组件中调用 Action

你可以在组件中使用 store.dispatch('increment') 进行 action 调用,或者使用 store.mapActions 辅助函数将组件中名为 incrementmethod 映射为 store.dispatch('increment')

import { createComponent } from '@mpxjs/core'
-import store from '../store'
-
-createComponent({
-  // ...
-  methods: {
-    ...store.mapActions([
-      // 将 this.increment() 映射为 store.dispatch('increment')
-      'increment', 
-      // mapActions 也支持载荷:将 this.incrementBy(amount) 映射为 store.dispatch('incrementBy', amount)
-      'incrementBy'
-    ]),
-    // mapActions 同样支持传入对象形式的参数进行别名映射
-    ...store.mapActions({
-      // 将 this.add() 映射为 store.dispatch('increment')
-      add: 'increment' 
-    })
-  }
-})
-

# 组合 Action

Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?

在 Mpx 中,action 永远返回一个 Promise,你可以在定义 action 手动返回一个 Promise,即使你没有这样做,框架也会对你 action 的返回值进行 Promise.resolve(returned) 包装,确保你可以使用 Promise 的方式处理多个 action 之间的异步组合:

actions: {
-  actionA ({ commit }) {
-    return new Promise((resolve, reject) => {
-      setTimeout(() => {
-        commit('someMutation')
-        resolve()
-      }, 1000)
-    })
-  }
-}
-

现在你可以通过 then 方法获取 actionA 执行完毕的时机:

store.dispatch('actionA').then(() => {
-  // ...
-})
-

在另外一个 action 中也可以通过这种方式进行一系列异步调用:

actions: {
-  // ...
-  actionB ({ dispatch, commit }) {
-    return dispatch('actionA').then(() => {
-      commit('someOtherMutation')
-    })
-  }
-}
-

如果我们利用 async / await (opens new window),我们能够更方便地对一系列异步操作进行组合:

// 假设 getData() 和 getOtherData() 返回的是 Promise
-
-actions: {
-  async actionA ({ commit }) {
-    commit('gotData', await getData())
-  },
-  async actionB ({ dispatch, commit }) {
-    await dispatch('actionA') // 等待 actionA 完成
-    commit('gotOtherData', await getOtherData())
-  }
-}
-

# Modules

Mpx 虽然支持了 modules,但并不推荐使用。在 Mpx 中,我们更推荐使用[多实例模式](#多实例 store)进行对应用状态进行模块划分

在 Mpx 中,modules 的设计与 vuex 中基本保持一致,在 createStore 中将子模块配置传入 modules 配置项中即可使用:

import {createStore} from '@mpxjs/core'
-
-const moduleA = {
-  state: { ... },
-  mutations: { ... },
-  actions: { ... },
-  getters: { ... }
-}
-
-const moduleB = {
-  state: { ... },
-  mutations: { ... },
-  actions: { ... }
-}
-
-const store = createStore({
-  modules: {
-    moduleA,
-    moduleB
-  }
-})
-
-store.state.moduleA // -> moduleA 的状态
-store.state.moduleB // -> moduleB 的状态
-
-export default store
-

Mpx 并未实现 vuex 中的命名空间,除 state 外的所有属性(getters / mutations / actions)将被平铺展开到根 store 的对应空间下。在多实例 store 中,我们的实现方式则与命名空间高度相似。

# 模块的局部状态

对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。

const moduleA = {
-  state: { count: 0 },
-  mutations: {
-    increment (state) {
-      // 这里的 state 对象是模块的局部状态
-      state.count++
-    }
-  },
-  getters: {
-    doubleCount (state) {
-      return state.count * 2
-    }
-  }
-}
-

对于模块内部的 action,局部状态通过 context.state 访问,根状态则通过 context.rootState 访问:

const moduleA = {
-  // ...
-  actions: {
-    incrementIfOddOnRootSum ({ state, commit, rootState }) {
-      if ((state.count + rootState.count) % 2 === 1) {
-        commit('increment')
-      }
-    }
-  }
-}
-

对于模块内部的 getter,根状态能通过第三个参数访问:

const moduleA = {
-  // ...
-  getters: {
-    sumWithRootCount (state, getters, rootState) {
-      return state.count + rootState.count
-    }
-  }
-}
-

# 模块在组件中的引入方式

const store = createStore({
-  modules: {
-    a: {
-      state: {
-        name: 1
-      },
-      getters: {
-        getName: s => s.name
-      }
-    },
-    b: moduleB
-  }
-})
-
-createComponent({
-  // 我们支持了多种方式引入子模块中的 state
-  computed: {
-    // 通过函数引入
-    ...store.mapState({
-      nameA: state => state.a.name
-    }),
-    // 通过路径字符串引入
-    ...store.mapState({
-      nameA2: 'a.name'
-    }),
-    // 通过传入模块路径引入
-    ...store.mapState('a', ['name']),
-    // 对于 getters / mutations / actions,由于我们没有实现 namespace,子模块当中定义的 getters / mutations / actions 都能在根空间下直接访问,正常调用映射方法即可
-    ...store.mapGetters('getName')
-  }
-})
-

# 多实例 store

在 Mpx 中,我们允许在一个应用下创建多个 store 实例,进行模块化分布式的数据管理,同时提供了 deps 声明模块依赖的机制,让用户自由组合多个 store 实例并基于这些已有的 store 创建新的继承 store。在实际业务使用中,我们发现多实例模式的灵活性远高于 module,更加适合跨团队合作当中的数据管理。

使用多实例 store 的方式非常简单,你只需要多次调用 createStore api 创建出多个 store 示例,并分别将其注入到组件中即可使用,简单示例如下:

import { createComponent, createStore } from '@mpxjs/core'
-
-const storeA = createStore({
-  state: {
-    countA: 0
-  },
-  mutations: {
-    incrementA (state) {
-      state.countA++
-    }
-  }
-})
-
-const storeB = createStore({
-  state: {
-    countB: 0
-  },
-  mutations: {
-    incrementB (state) {
-      state.countB++
-    }
-  }
-})
-
-createComponent({
-  computed: {
-    // ...
-    ...storeA.mapState(['countA']),
-    ...storeB.mapState(['countB'])
-  },
-  methods: {
-    // ...
-    ...storeA.mapMutations(['incrementA']),
-    ...storeB.mapMutations(['incrementB'])
-  }
-})
-

可以看到 Mpx 中的 map 辅助方法都挂载在 store 实例上,正是为了支持多实例 store 的实现

# 合并继承多实例 store

在实际的跨团队业务当中,我们既希望不同团队间的数据管理尽量解耦,也希望一些共同的部分能够复用,这就要求我们的 store 实例可以以某种方式组合起来使用,我们提供了 deps 能来实现多实例 store 的合并与继承。

承接上面的示例,我们基于 storeA 和 storeB 创建一个新的 storeC,在 storeC 当中可以定义自身的独立状态,也能基于 storeA 和 storeB 进行状态衍生:

import { createComponent, createStore } from '@mpxjs/core'
-
-const storeA = createStore({
-  state: {
-    countA: 0
-  },
-  mutations: {
-    incrementA (state) {
-      state.countA++
-    }
-  }
-})
-
-const storeB = createStore({
-  state: {
-    countB: 0
-  },
-  mutations: {
-    incrementB (state) {
-      state.countB++
-    }
-  }
-})
-
-const storeC = createStore({
-  state: {
-    countC: 0
-  },
-  getters: {
-    abc (state) {
-      // 此处 state.storeA 指向了原始的 storeA.state
-      return state.storeA.countA + state.storeB.countB + state.countC
-    }
-  },
-  mutations: {
-    incrementC (state) {
-      state.countC++
-    }
-  },
-  actions: {
-    incrementB ({ commit }) {
-      // storeC内部也可以通过命名空间路径的方式提交 storeB 的 mutation
-      commit('storeB.incrementB')
-    }
-  },
-  // 此处 deps 声明了 storeC 的依赖,依赖中的 state / getters / mutations / actions 都会以 deps 中的 key 为命名空间存放在 storeC 对应的域下
-  deps: {
-    storeA,
-    storeB
-  }
-})
-
-// 通过继承合并得到的 storeC,我们可以完整访问其依赖 storeA / storeB
-createComponent({
-  computed: {
-    // ...
-    // 使用路径字符串或函数映射,可以看出和 modules 中 mapState 的方式非常类似
-    ...storeC.mapState({
-      countA: 'storeA.countA',
-      countA2: state => state.storeA.countA
-    }),
-    // 传入命名空间参数映射 storeB 中的 countB
-    ...storeC.mapState('storeB', ['countB']),
-    // 映射基于 storeA/B/C 衍生得到的 getters
-    ...storeC.mapGetters(['abc'])
-  },
-  methods: {
-    // ...
-    // mutation不支持函数映射
-    // 下面代码以三种方式分别映射了increment、incrementB和incrementC
-    ...storeC.mapMutations({
-      incrementA: 'storeA.incrementA'
-    }),
-    ...storeC.mapMutations('storeB', ['incrementB']),
-    ...storeC.mapMutations(['incrementC'])
-  }
-})
-

简单来讲,作为 deps 的 store 会以注册在 deps 中的 key 值作为命名空间,将其原始的 state / getters / mutations / actions 存放在新生成 store 对应的域下,便于新 store 对其进行访问并衍生出新的数据或操作,如上述示例中,storeA.state 会存放在 storeC.state.storeA 中,对于 getters / mutations / actions 亦然。

# 在 Typescript 中使用 store

Mpx 自 2.2 版本开始支持 Typescript,为了更好地支持 store 中的类型推导,我们针对 Typescript 环境提供了变种的 store api createStoreWithThis 进行 store 创建,该 api 最主要的变化在于定义 getters,mutations 和 actions 时,自身的 state,getters 等属性不再通过参数传入,而是会挂载到函数的执行上下文 this 当中,通过 this.state 或 this.getters 的方式进行访问,简单的使用示例如下:

const store = createStoreWithThis({
-  state: {
-    aa: 1,
-    bb: 2
-  },
-  getters: {
-    cc() {
-      // 使用 this.state 访问 state
-      return this.state.aa + this.state.bb
-    }
-  },
-  actions: {
-    doSth3() {
-      // 使用 this.getters 访问 getter
-      console.log(this.getters.cc)
-      return false
-    }
-  }
-})
-

详细的使用方式及推导规则请查看 Typescript 支持章节。

# 在组合式 API 中使用 store

Mpx 自 2.8 版本完整支持了组合式 API 的开发方式,虽然在组合式 API 中我们首推使用 pinia 作为外部状态管理方案,不过旧的 store 在组合式 API 中仍然可以使用,我们提供了新的 mapStateToRefsmapGettersToRefs 便于用户在组合式 API 中访问 stategetters,而对于 mutationsactions,用户可以直接调用 store 实例上的 commitdispatch 方法进行调用,或者使用原有的 map* API 进行映射访问,简单使用示例如下:

import { createPage, createStoreWithThis, watchEffect } from '@mpxjs/core'
-
-const store = createStoreWithThis({
-  state: {
-    count: 123
-  },
-  getters: {
-    doubleCount () {
-      return this.state.count * 2
-    }
-  },
-  mutations: {
-    addCount () {
-      this.state.count++
-    },
-    subCount () {
-      this.state.count--
-    }
-  }
-})
-
-createPage({
-  setup () {
-    const { count } = store.mapStateToRefs(['count'])
-    const { doubleCount } = store.mapGettersToRefs(['doubleCount'])
-    const { addCount } = store.mapMutations(['addCount'])
-
-    watchEffect(() => {
-      console.log(count.value, doubleCount.value)
-    })
-
-    return {
-      count,
-      doubleCount,
-      addCount,
-      subCount () {
-        // 两种方式均可以进行 mutations 调用,actions 同理
-        store.commit('subCount')
-      }
-    }
-  }
-})
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/subpackage.html b/docs-vuepress/.vuepress/dist/guide/advance/subpackage.html deleted file mode 100644 index 79df9e595e..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/subpackage.html +++ /dev/null @@ -1,264 +0,0 @@ - - - - - - 使用分包 | Mpx框架 - - - - - - - - - -

# 使用分包

作为一个对 performance 极度重视的框架,分包作为提升小程序体验的重要能力,框架对各种类型的分包能力进行了完善支持。

分包是小程序平台提供的原生能力,mpx是对该能力做了部分加强,目前各大主流小程序平台都已支持分包,且框架在可能的情况下进行了抹平。

使用分包一定要记得阅读下面的分包注意事项

# 原生语法注册分包

Mpx 支持小程序原生语法注册分包,并且在框架层面对不同平台的的差异进行了抹平,我们以微信小程序原生语法注册分包为例。

{
-  "pages": [
-    "pages/index/index"
-  ],
-  "subPackages": [
-    {
-      "root": "test",
-      "pages": [
-        "pages/other/other",
-        "pages/other/other2"
-      ]
-    }
-  ]
-}
-

# packages 语法注册分包

Mpx 提供 packages 语法来对小程序的主包页面和分包划分能力进行增强,使用 packages 语法可以灵活的对业务进行拆分,允许以 npm 包的形式进行 -主包页面和分包注册,且分包名和页面路径可自定义,十分有利于大型多团队开发的项目维护。

# 使用方法

Mpx 拓展了 app.json 的语法,新增了 packages 域,用来声明依赖的 packages,packages 可嵌套依赖。

# 注册主包页面

首先我们介绍下 packages 注册主包页面的用法,在 packages 中直接配置资源路径,Mpx 会去读取该资源中 json 区块中的 pages 属性,合并到主包页面配置中。

// @file src/app.mpx
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/index/index"
-    ],
-    "packages": [
-      "{npmPackage || relativePathToPackage}/index"
-    ]
-  }
-</script>
-
-// @file src/packages/index.mpx
-// 注意确保页面路径的唯一性
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/other/other",
-      "./pages/other/other2"
-    ]
-  }
-</script>
-

打包结果:dist/app.json

{
-  "pages": [
-    "pages/index/index",
-    "pages/other/other",
-    "pages/other/other2"
-  ]
-}
-

# 注册分包

Mpx 会将 packages 域下的路径带 root 为 key 的 query 解析注册为分包,使用 packages 语法注册分包,只需要在 packages 中配置资源路径添加 root=xxx,root的值即为分包名。

// @file src/app.mpx
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/index/index"
-    ],
-    "packages": [
-      "{npmPackage || relativePathToPackage}/index?root=test"
-    ]
-  }
-</script>
-
-// @file src/packages/index.mpx (子包的入口文件)
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/other/other",
-      "./pages/other/other2"
-    ]
-  }
-</script>
-

打包结果:dist/app.json

{
-  "pages": [
-    "pages/index/index"
-  ],
-  "subPackages": [
-    {
-      "root": "test",
-      "pages": [
-        "pages/other/other",
-        "pages/other/other2"
-      ]
-    }
-  ]
-}
-

由上可见,经过我们的编译过程,packages 中注册的页面可按照原始的路径被合并入主包页面或注册为分包, -这样开发者可以不用考虑自己在被依赖时页面路径是怎么样的,也可以直接将调试用的app.mpx作为依赖入口直接暴露出去, -对于主app的开发者来说也不需要了解依赖内部的细节,只需要在packages中声明自己所需的依赖即可。

# 注意事项

  • 依赖的开发者在自己的入口 app.mpx 中注册页面时对于本地页面一定要使用相对路径进行注册,否则在主app中进行编译时会找不到对应的页面
  • 不管是用 json 还是 mpx 格式定义 package 入口,编译时永远只会解析 json 且只会关注 json 中的 pages 和 packages 域,其余所有东西在主app编译时都会被忽略
  • 由于我们是将 packages 中注册的页面按照原始的路径合并到主 app 当中,有可能会出现路径名冲突。
    -这种情况下编译会报出响应错误提示用户解决冲突,为了避免这种情况的发生,依赖的提供者最好将自己内部的页面放置在能够描述依赖特性的子文件夹下。

例如一个包叫login,建议包内页面文件目录为:

project
-│   app.mpx  
-└───pages
-    └───login
-        │   page1.mpx
-        │   page2.mpx
-        │   ...
-

# 独立分包

Mpx目前已支持独立分包构建,使用 packages 语法声明分包时只需要在后面添加 independent=true query 即可,同时也支持原生语法声明。 -如下方示例声明 packageA 分包为独立分包

示例:

// src/app.mpx 文件中 json 块
-
-// Mpx packages 方式
-{
-  "packages": [
-    "packageA/app.mpx?root=packageA&independent=true"
-  ]
-}
-
// 微信原生方式
-{
-  "subpackages": [
-    {
-      "root": "packageA",
-      "pages": [
-        "pages/index"
-      ],
-      "independent": true
-    },
-  ]
-}
-

需要注意的是,由于独立分包可以独立于主包和其他分包运行,从独立分包页面进入小程序时,主包中的相应初始化逻辑并不会执行,如果独立分包中多个页面需要某种通用初始化逻辑时就无法优雅的实现, -Mpx框架针对独立分包场景提供了独立分包初始化逻辑执行能力。

对于使用 packages 方式声明的独立分包,默认将 .mpx 文件自身的 script 块作为初始化逻辑执行。

<!--src/packagesA/app.mpx,packageA 独立分包入口文件-->
-<script>
-import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-
-mpx.use(apiProxy, { usePromise: true }) 
-if (isIndependent) {
-    // do some in independent package
-} else {
-    // do some not independent package
-}
-</script>
-
-<script type="application/json">
-{
-  "pages": [
-    "./pages/index"
-  ]
-}
-</script>
-

上方代码中 独立分包 packageA 的入口文件 app.mpx 中的 script block 代码会默认在独立分包初始化时执行,Mpx 同时提供了全局变量 isIndependent 标识当前代码执行环境是否为独立分包来进行特定逻辑区分

如果你不想走这个默认的初始化逻辑执行规则,想自定义一个 js 文件存储当前独立分包的初始化逻辑,我们支持 independent 配置项直接配置为初始化逻辑文件地址

// src/app.mpx 文件中 json 块
-// Mpx packages 方式
-{
-  "packages": [
-    "packageA/app.mpx?root=packageA&independent=./common" // 路径上下文为 packageA 文件夹
-  ]
-}
-
// 微信原生方式
-{
-  "subpackages": [
-    {
-      "root": "packageA",
-      "pages": [
-        "pages/index"
-      ],
-      "independent": "./common" // 路径上下文为 packageA 文件夹
-    },
-  ]
-}
-
// src/pacakgeA/common.js
-import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-
-mpx.use(apiProxy, { usePromise: true })
-if (isIndependent) {
-    // do some in independent package
-} else {
-    // do some not independent package
-}
-

注意上方配置 independent 为初始化逻辑文件地址时,路径相对地址上下文为 packageA

# 分包预下载

分包预下载是在 json中 新增一个 preloadRule 字段,mpx 打包时候会原封不动把这个部分放到 app.json 中,所以只需要按照 微信小程序官方文档 - 分包预下载 (opens new window) 或者 支付宝小程序官方文档 - 分包预下载 (opens new window) 配置即可。

示例:

// @file src/app.mpx
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/index/index"
-    ],
-    "packages": [
-      "{npmPackage || relativePathToPackage}/index?root=xxx"
-    ],
-    "preloadRule": {
-      "pages/index": {
-        "network": "all",
-        "packages": ["important"]
-      },
-      "sub1/index": {
-        "packages": ["hello", "sub3"]
-      }
-    }
-  }
-</script>
-
-// @file src/packages/index.mpx (子包的入口文件)
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/other/other",
-      "./pages/other/other2"
-    ]
-  }
-</script>
-

打包结果:dist/app.json

{
-  "pages": [
-    "pages/index/index"
-  ],
-  "subPackages": [
-    {
-      "root": "xxx",
-      "pages": [
-        "pages/other/other",
-        "pages/other/other2"
-      ]
-    }
-  ],
-  "preloadRule": {
-    "pages/index": {
-      "network": "all",
-      "packages": ["important"]
-    },
-    "sub1/index": {
-      "packages": ["hello", "sub3"]
-    }
-  }  
-}
-

# 分包注意事项

当我们使用分包加载时,依赖包内的跳转路径需注意,比如要跳转到other2页面

  • 不用分包时会是:/pages/other/other2
  • 使用分包后应为:/test/pages/other/other2

即前面会多?root={rootKey}的rootKey这一层

为了解决这个问题,有三种方案:

  • import的时候在最后加'?resolve', 例如: import testPagePath from '../pages/testPage.mpx?resolve' , 编译时就会把它处理成正确的完整的绝对路径。

  • 使用相对路径跳转。

  • 定死使用的分包路径名,直接写/{rootKey}/pages/xxx (极度不推荐,尤其在分包可能被多方引用的情况时)

这里我们建议使用第一种方式。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html b/docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html deleted file mode 100644 index 7fc7a64234..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/advance/utility-first-css.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - 使用原子类 | Mpx框架 - - - - - - - - - -

# 使用原子类

原子类(utility-first CSS)是近几年流行起来的一种全新的样式开发方式,在前端社区内取得了良好的口碑,越来越多的主流网站也基于原子类进行开发,我们耳熟能详的有Github (opens new window)OpenAI (opens new window)Netflix (opens new window) -和NASA官网 (opens new window) -等。使用原子类离不开原子类框架的支持,常用的原子类框架有 Tailwindcss (opens new window)Windicss (opens new window) -和 Unocss (opens new window) 等,而在 Mpx2.9 以后,我们在框架中内置了基于 unocss -的原子类支持,让小程序开发也能使用原子类。对项目进行简单配置开启原子类支持后,用户就可以在 Mpx -页面/组件模板中直接使用一些预定义的基础样式类,诸如flex,pt-4,text-center 和 rotate-90 等,对样式进行组合定义,并且在 Mpx 支持的所有小程序平台和 web 平台中正常运行,下面是一个简单示例:


-<view class="container">
-  <view class="flex">
-    <view class="py-8 px-8 inline-flex mx-auto bg-white rounded-xl shadow-md">
-      <view class="text-center">
-        <view class="text-base text-black font-semibold mb-2">
-          Erin Lindford
-        </view>
-        <view class="text-gray-500 font-medium pb-3">
-          Product Engineer
-        </view>
-        <view
-          class="mt-2 px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-solid border-purple-200">
-          Message
-        </view>
-      </view>
-    </view>
-  </view>
-</view>
-

通过这种方式,我们在不编写任何自定义样式代码的情况下得到了一张简单的个人卡片,实际渲染效果如下:

utility-css-demo

相较于传统的自定义类编写样式的方式,使用原子类能给你带来以下这些好处:

  • 不用再烦恼于为自定义类取类名,传统样式开发中,我们仅仅是为某个元素定义样式就需要绞尽脑汁发明一些抽象的类名,还得提防类名冲突,使用原子类可以完全将你从这种琐碎无趣的工作中解放;
  • 停止css体积的无序增长,传统样式开发中,css体积会随着你的迭代不断增长,而在原子类中,一切样式都可以复用,你几乎不需要编写新的css;
  • 让调整样式变得更加安全,传统css是全局的,当你修改某个样式时无法保障其不会破坏其他地方的样式,而你在模板中使用的原子类是本地的,你完全不用担心修改它会影响其他地方。

而相较于使用内联样式,原子类也有一些重要的优势:

  • 约束下的设计,使用内联样式时,里面的每一个数值都是魔法数字(magic number) -,而通过原子工具类,你可以选择一些符合预定义设计规范的样式,便于构筑具有视觉一致性的UI;
  • 响应式设计,你无法在内联样式中使用媒体查询,然而通过原子类框架中提供的响应式工具,你可以轻而易举地构建出响应式界面;
  • Hover、focus和其他状态,使用内联样式无法定义特定状态下的样式,如hover和focus,通过原子类框架的状态变量能力,我们可以轻松为这些状态定义样式。

看到这里相信你已经迫不及待地想要在 Mpx 中体验原子类开发了吧,可以根据下面的指南开启你的原子类之旅。

# 原子类环境配置

如果你想在新项目中使用原子类,可以使用最新版本的 @mpxjs/cli 创建项目,在 prompt 中选择使用原子类,就可以在新创建的项目模版中直接使用 unocss 的原子类,关于可使用的工具类可参考 unocss 交互示例 (opens new window)及本指南下方的工具类支持范围

与 web 中使用 unocss 不同,在 Mpx 中使用 unocss 不需要显式引入虚拟模块 import 'uno.css' 来承载生成的样式内容,这是由于在 -Mpx 中,我们充分考虑到小程序分包架构的特殊性和主包体积的重要性,结合 Mpx -强大的分包构建能力,对生成的原子工具类的使用情况进行分析,将其自动注入到合适的主包或者分包中,来达到全局体积分配的最优(在没有内容冗余的情况下尽可能输出到分包)。

对于使用 @mpxjs/cli@3.0 新版脚手架创建的项目,可以在项目初始化时选择需要使用原子类 -选项,或在已有项目下执行mpx add @mpxjs/vue-cli-plugin-mpx-utility-first-css以激活原子类相关编译配置。

对于使用旧版脚手架创建的项目,可以通过修改项目编译配置以激活原子类支持:

  1. 安装相关依赖:
{
-  //...
-  "devDependencies": {
-    "@mpxjs/unocss-base": '^2.9.0',
-    "@mpxjs/unocss-plugin": '^2.9.0'
-  }
-}
-
  1. 新建uno.config.js,基础配置内容如下:
const { defineConfig } = require('unocss')
-const presetMpx = require('@mpxjs/unocss-base')
-
-module.exports = defineConfig({
-  presets: [
-    presetMpx()
-  ]
-})
-
  1. 注册MpxUnocssPlugin,在build/getPlugins中添加如下代码:
const MpxUnocssPlugin = require('@mpxjs/unocss-plugin')
-// ...
-plugins.push(new MpxUnocssPlugin())
-

即可在项目模版中使用基于unocss的原子类功能,unocss默认的preset兼容tailwindcss/windicss -,可以通过查阅tailwindcss文档 (opens new window)windicss文档 (opens new window)unocss可交互文档 (opens new window)进行探索使用。

关于uno.config.js可用配置项及@mpxjs/unocss-plugin@mpxjs/unocss-base的配置项请参考API文档

# vscode插件支持

推荐使用unocss官方插件https://unocss.dev/integrations/vscode -mpx文件则需要在unocss.config.js添加如下参数才能生效

const { defineConfig } = require('unocss')
-
-module.exports = defineConfig({
-  include: [/\.mpx($|\?)/]
-})
-

# 功能支持范围

我们支持了unocss大部分的配置项及功能,并针对小程序技术规范提供了一些额外的功能支持,如分包输出和样式隔离等功能,以下为详细功能支持范围。

功能 支持度 备注
Load Config 支持 支持windi默认的全部配置文件路径,并支持在plugin options中手动传入配置对象或配置文件路径
Rules 支持
Shortcuts 支持
Theme 支持
Variants 支持
Extractors 不支持
Transformers 部分支持 内建支持variant groups、directives和alias,不支持自定义transformers
Preflights 支持 支持内建和自定义preflight配置
Presets 支持
Safelist 支持 支持全局配置和模版注释语法局部配置声明safelist
Value Auto-infer 支持
Variant Groups 支持
Directives 支持
Alias 支持
Attributify Mode 不支持 小程序模版不支持不识别的自定义属性
Responsive Design 支持
Dark Mode 支持
Important Prefix 支持
Layers Ordering 支持
分包输出/公共样式抽离 支持 可通过设置@mpxjs/unocss-plugin配置项minCount决定公共样式范围
组件分包异步/组件样式隔离 支持 可通过全局配置和模版注释语法局部配置styleIsolation='isolated'支持相关场景的原子类使用
Rpx样式单位 支持
小程序类名特殊字符转义 支持 静态类名和动态类名均以支持
跨平台输出 支持 支持输出Mpx已支持的所有小程序平台及Web

# 工具类可用范围

经过我们的详细测试,大部分unocss -提供的工具类都能在小程序环境下正常工作,但是也有部分工具类由于小程序底层环境差异无法正常运行,以下是详细的测试结果,参考windicss文档 (opens new window) -进行分类组织。

# General

功能 支持度 备注
Colors 支持
Typography 部分支持 不支持子项:Font Variant Numeric Tab Size
SVG 不支持 小程序不支持svg标签
Variants 部分支持 不支持子项:Child Selectors,因为小程序不支持*选择器

# Accessibility

功能 支持度 备注
Screen Readers 支持

# Animations

功能 支持度 备注
Animation 支持
Transforms 支持
Transitions 支持

# Backgrounds

功能 支持度 备注
Background Attachment 支持
Background Clip 支持
Background Color 支持
Background Opacity 支持 需要和Background Color一起使用
Background Position 支持
Background Repeat 支持
Background Size 支持
Background Origin 支持
Gradient Direction 支持
Gradient From 支持
Gradient Via 支持
Gradient To 支持
Background Blend Mode 支持

# Behaviors

功能 支持度 备注
Box Decoration Break 支持
Image Rendering 部分支持 image-render-edge 真机不支持
Listings 支持
Overflow 支持
Overscroll Behavior 支持
Placeholder 不支持 微信小程序 input 需要通过 placeholder-style 设置 placeholder 样式

# Borders

功能 支持度 备注
Border Radius 支持
Border Width 支持
Border Color 支持
Border Opacity 支持
Border Style 支持
Divider Width 不支持 小程序不支持生成的css选择器
Divider Color 不支持 小程序不支持生成的css选择器
Divider Opacity 不支持 小程序不支持生成的css选择器
Divider Style 不支持 小程序不支持生成的css选择器
Outline 支持
Outline Solid 支持
Outline Dotted 支持
Ring Width 支持
Ring Color 支持
Ring Opacity 支持
Ring Offset Width 支持
Ring Offset Color 支持

# Effects

功能 支持度 备注
Box Shadow 支持
Opacity 支持
Mix Blend Mode 支持

# Filters

功能 支持度 备注
Filter 支持
Backdrop Filter 支持

# Interactivity

功能 支持度 备注
Accent Color 不支持 小程序不支持生成的样式
Appearance 不支持 小程序不支持生成的样式
Cursor 不支持 小程序不支持生成的样式
Caret 不支持 小程序不支持生成的样式
Pointer Events 支持
Resize 不支持 小程序不支持生成的样式
Scroll Behavior 不支持 小程序不支持生成的样式
Touch Action 支持
User Select 不支持 小程序不支持生成的样式
Will Change 不支持 小程序不支持生成的样式

# Layout

功能 支持度 备注
Columns 支持
Container 支持
Display 支持
Flex 支持
Grid 支持
Positioning 部分支持 小程序图片设置object-fit 无效,可使用mode代替
Sizing 支持
Spacing 部分支持 Space Between小程序不支持生成的css选择器
Tables 部分支持 小程序不支持部分table样式

# 小程序原子类使用注意点

小程序由于底层环境差异,我们在支持和使用原子类时有一些特殊的注意点。

# 特殊字符转义

基于unocss的原子类支持value auto-infer(值自动推导),可以在模版中根据相关规则书写灵活的自定义值原子类,如p-5px bg-[hsl(211.7,81.9%,69.6%)]等,针对原子类中出现的[ ( ,等特殊字符,在web中会通过转义字符\进行转义,由于小程序环境下不支持css选择器中出现\转义字符,我们内置支持了一套不带\的转义规则对这些特殊字符进行转义,同时替换模版和css文件中的类名,内建的默认转义规则如下:

const escapeMap = {
-    '(': '_pl_',
-    ')': '_pr_',
-    '[': '_bl_',
-    ']': '_br_',
-    '{': '_cl_',
-    '}': '_cr_',
-    '#': '_h_',
-    '!': '_i_',
-    '/': '_s_',
-    '.': '_d_',
-    ':': '_c_',
-    ',': '_2c_',
-    '%': '_p_',
-    '\'': '_q_',
-    '"': '_dq_',
-    '+': '_a_',
-    $: '_si_',
-    // unknown用于兜底不在上述范围中未知的转义字符
-    unknown: '_u_'
-  }
-

与此同时,用户也可以通过传递@mpxjs/unocss-pluginescapeMap配置项来覆盖内建的转义规则。

# 原子类分包输出

在web中,原子类会被全部打包输出单个样式文件,一般会放置在顶层样式表中以供全局访问,但在小程序中这种全量的输出策略并不是最优的,主要原因在于小程序中可供全局访问的主包体积存在2M大小限制,主包体积十分紧缺珍贵,Mpx在构建输出时遵循着分包优先的原则,尽可能充分利用分包体积从而减少对主包体积的占用,再进行原子类产物输出时,我们也遵循了相同的原则。

在Mpx中,我们在收集原子类时同时记录了每个原子类的引用分包,在收集结束后根据每个原子类的分包引用数量决定该原子类应该输出到主包还是分包当中,我们在@mpxjs/unocss-plugin中提供了minCount配置项来决定分包的输出规则,该配置项的默认值为2,即当一个原子类被2个或以上分包引用时,会被作为公共原子类抽取到主包中,否则输出到所属分包中,这也是全局最优的策略。当我们想要让原子类输出产物更少地占用主包体积时,我们也可以将minCount值调大,让原子类抽取到主包的条件更加苛刻,不过这样也会伴随着原子类分包冗余的增加。

unocss.config.js配置中定义的safelist原子类默认会输出到主包,为了组件局部使用的safelist有输出到分包的机会,我们在模版中提供了注释配置(comments config),灵感来源于webpack中的魔法注释(magic comments),用户可以在组件模版中通过注释配置声明当前组件所需的safelist,对应的原子类也会根据上述的规则输出到主包或分包中,使用示例如下:

<template>
-    <!-- mpx_config_safelist: 'text-red-500 bg-blue-500' -->
-    <!-- 动态样式中可以使用text-red-500和bg-blue-500原子类 -->
-    <view wx:class="{{classObj}}">test</view>
-    <!-- ... -->
-</template>
-

# 样式隔离与组件分包异步

在小程序中,自定义组件的样式默认是隔离的,web中通过全局样式访问原子类的方式不再生效,不过由于小程序提供了样式隔离配置 (opens new window),我们可以将该组件样式隔离配置调整为apply-shared来获取页面或app中定义的原子类,但是当我们在使用传统类名和原子类混合开发或者迁移原子类的过程中,我们往往希望保留原本自定义组件的样式隔离。

针对这种情况,我们在@mpxjs/unocss-plugin中提供了styleIsolation配置项,可选设置为isolated|apply-shared,当设置为isolated时每个组件都会通过@import独立引用主包或者分包的原子类样式文件,因此不会受到样式隔离的影响;当设置为apply-shared时,只有app和分包页面会引用对应的原子类样式文件,自定义组件需要通过配置样式隔离为apply-shared使原子类生效。

在组件分包异步的情况下对应组件即使将样式隔离配置为apply-shared的情况下,@mpxjs/unocss-plugin也需要将styleIsolation设置为isolated才能正常工作,原因在于组件分包异步的情况下,组件被其他分包的页面所引用渲染,由于上述原子类样式分包输出的规则,其他分包的页面中可能并不包含当前组件所需的原子类,只有在isolated模式下由组件自身引用所需的原子类样式才能保证正常work,类似于safelist,我们也提供了注释配置的方式对组件的styleIsolation模式进行局部配置,示例如下:

<template>
-    <!-- mpx_config_styleIsolation: 'isolated' -->
-    <!-- 当前组件会直接引用对应主包或分包的原子类样式 -->
-     <view class="@dark:(text-white bg-dark-500)">
-    <!-- ... -->
-</template>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html b/docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html deleted file mode 100644 index b8db15d7b5..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/class-style-binding.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - 类名样式绑定 | Mpx框架 - - - - - - - - - -

# 类名样式绑定

Mpx利用wxs完整实现了Vue中的类名样式绑定,性能优良且没有任何使用限制(很多小程序框架基于字符串解析来实现该能力,只支持在模板上写简单的字面量,大大限制了使用场景)

# 类名绑定

类名绑定的增强指令是wx:class,可以与普通的class属性同时存在,在视图渲染中进行合成。

# 对象语法

wx:class中传入对象,key值为类名,value值控制该类名是否生效。

<template>
-  <!--支持传入对象字面量,此处视图的class="outer active"-->
-  <view class="outer" wx:class="{{ {active:active, disabled:disabled} }}">
-    <!--直接直接传入对象数据,此处视图的class="inner selected"-->
-    <view class="inner" wx:class="{{innerClass}}"></view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      active: true,
-      disable: false,
-      innerClass: {
-        selected: true
-      }
-    }
-  })
-</script>
-

# 数组语法

wx:class中传入字符串数组,字符串为类名。

<template>
-  <!--支持传入数组字面量,此处视图的class="outer active danger"-->
-  <view class="outer" wx:class="{{ ['active', 'danger'] }}">
-    <!--直接直接传入数组数据,此处视图的class="inner selected"-->
-    <view class="inner" wx:class="{{innerClass}}"></view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      innerClass: ['selected']
-    }
-  })
-</script>
-

# 样式绑定

样式的增强指令是wx:style,可以与普通的style属性同时存在,在视图渲染中进行合成。

# 对象语法

wx:style中传入用样式对象,带有横杠的样式名可以用驼峰写法来代替

<template>
-  <!--支持传入对象字面量,模板会显得杂乱,此处视图的style="color:red;font-size:16px;font-weight:bold;"-->
-  <view style="color:red;" wx:style="{{ {fontSize:'16px', fontWeight:'bold'} }}">
-    <!--更好的方式是直接传入对象数据,此处视图的style="color:blue;font-size:14px;"-->
-    <view wx:style="{{innerStyle}}"></view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      innerStyle: {
-        color: 'blue',
-        fontSize: '14px'
-      }
-    }
-  })
-</script>
-

# 数组语法

wx:style同样支持传入数组将多个样式合成应用到视图上

<template>
-  <!--此处视图的style="color:blue;font-size:14px;background-color:red;"-->
-  <view wx:style="{{ [baseStyle, activeStyle] }}">
-
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      baseStyle: {
-        color: 'blue',
-        fontSize: '14px'
-      },
-      activeStyle:{
-        backgroundColor: 'red'
-      }
-    }
-  })
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/component.html b/docs-vuepress/.vuepress/dist/guide/basic/component.html deleted file mode 100644 index 6b1f590c6b..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/component.html +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - 自定义组件 | Mpx框架 - - - - - - - - - -

# 自定义组件

Mpx中的自定义组件完全基于小程序原生的自定义组件支持,与此同时,Mpx提供的数据响应和模板增强等一系列增强能力都能在自定义组件中使用。

原生自定义组件的规范详情查看这里 (opens new window)

# 动态组件

Mpx中提供了使用方法类似于 Vue 的动态组件能力,这是一个基于 wx:if 实现的语法。通过对 is 属性进行动态绑定,可以实现在同一个挂载点切换多个组件,前提需要动态切换的组件已经在全局或者组件中完成注册。 -使用示例如下:

<view>
-  <!-- current为组件名称字符串,可选范围为局部注册的自定义组件和全局注册的自定义组件 -->
-  <!-- 当 `current`改变时,组件也会跟着切换  -->
-  <component is="{{current}}"></component>
-</view>
-
-<script>
-  import {createComponent} from '@mpxjs/core'
-  createComponent({
-    data: {
-      current: 'test'
-    },
-    ready () {
-      setTimeout(() => {
-        this.current = 'list'
-      }, 3000)
-    }
-  })
-</script>
-
-<script type="application/json">
-  {
-    "usingComponents": {
-      "list": "../components/list",
-      "test": "../components/test"
-    }
-  }
-</script>
-

# slot

在组件中使用slot(插槽)可以使我们封装的组件更具有可扩展性,Mpx完全支持原生插槽的使用。

简单示例如下:

<!-- 组件模板 -->
-<!-- components/mySlot.mpx -->
-
-<view>
-  <view>这是组件模板</view>
-  <slot name="slot1"></slot>
-  <slot name="slot2"></slot>
-</view>
-

下面是引入 mySlot 组件的页面

<!-- index.mpx -->
-
-<template>
-  <view>
-    <my-slot>
-      <view slot="slot1">我是slot1中的内容</view>
-      <view slot="slot2">我是slot2中的内容</view>
-    </my-slot>
-  </view>
-</template>
-
-<script>
-import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  options: {
-    multipleSlots: true // 启用多slot支持
-  },
-  // ...
-})
-</script>
-
-<script type="application/json">
-  {
-    "usingComponents": {
-      "my-slot": "components/mySlot"
-    }
-  }
-</script>
-

更多详情可查看这里 (opens new window)

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html b/docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html deleted file mode 100644 index 0bf8e876a2..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/conditional-render.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - 条件渲染 | Mpx框架 - - - - - - - - - -

# 条件渲染

Mpx中的条件渲染与原生小程序中完全一致,详情可以查看这里 (opens new window)

简单示例如下:

<template>
-  <view class="container">
-    <!-- 通过 wx:if 的语法来控制需要渲染的元素 -->
-    <view wx:if="{{ score > 90 }}"> A </view>
-    <view wx:elif="{{ score > 60 }}"> B </view>
-    <view wx:else> C </view>
-
-    <!-- 通过 wx:show 来控制元素的显示隐藏-->
-    <view wx:show="{{ score > 90 }}"> very good! <view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      score: 80
-    }
-  })
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/css.html b/docs-vuepress/.vuepress/dist/guide/basic/css.html deleted file mode 100644 index d862ed1e58..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/css.html +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - CSS 处理 | Mpx框架 - - - - - - - - - -

# CSS 处理

# CSS 预编译

Mpx 支持 CSS 预编译处理,你可以通过在 style 标签上设置 lang 属性,来指定使用的 CSS 预处理器,此外需要在对应的 webpack 配置文件中 -加入对应的 loader 配置

<!-- 使用 stylus -->
-<style lang="stylus">
-  .nav
-    width 100px
-    height 80px
-    color #f90
-    &:hover
-      background-color #f40
-      color #fff
-</style>
-
-// getRules 配置文件
-rules: [
-    {
-        test: /\.styl(us)?$/,
-        use: [
-            MpxWebpackPlugin.wxssLoader(),
-            'stylus-loader'
-        ]
-    }
-]
-
<!-- 使用 sass  -->
-<style lang="scss">
-  .nav {
-    width: 100px;
-    height: 80px;
-    color: #f90
-    &:hover {
-      background-color: #f40;
-      color: #fff
-    }
-  }
-</style>
-
-// getRules 配置文件
-rules: [
-    {
-        test: /\.scss$/,
-        use: [
-            'css-loader',
-            'sass-loader'
-        ]
-    }
-]
-
<!-- 使用 less -->
-<style lang="less">
-  .size {
-    width: 100px;
-    height: 80px
-  }
-  .nav {
-    .size();
-    color: #f90;
-    &:hover {
-      background-color: #f40;
-      color: #fff
-    }
-  }
-</style>
-
-// getRules 配置文件
-rules: [
-    {
-        test: /\.less$/,
-        use: [
-            'css-loader',
-            'less-loader'
-        ]
-    }
-]
-
-

# 公共样式复用

为了达到最大限度的样式复用,Mpx 提供了以下两种方式实现公共样式抽离,但是最终打包的效果有所区别。

// styles/mixin.styl
-.header-styl
-  width 100%
-  height 100px
-  background-color #f00
-

# style src 复用

通过给 style 标签添加 src 属性引入外部样式,最终公共样式代码只会打包一份。

<!-- index.mpx -->
-<style lang="stylus" src="../styles/common.styl"></style>
-
<!-- list.mpx -->
-<style lang="stylus" src="../styles/common.styl"></style>
-

Mpx 将 common.styl 中的代码经过 loader 编译后生成一份单独的 wxss 文件,这样既实现了样式抽离,又能节省打包后的代码体积。

# @import 复用

如果指定 style 标签的 lang 属性并且使用 @import 导入样式,那么这个文件经过对应的 loader 处理之后的内容会重复打包到引用它的文件目录下,并不会抽离成单独的文件,这样无形中增加了代码体积。

<!-- index.mpx -->
-<style lang="stylus">
-  @import "../styles/mixin.styl"
-</style>
-
<!-- list.mpx -->
-<style lang="less">
-  @import "../styles/mixin.less";
-</style>
-

如果导入的是一份 css 文件,则最终打包后的效果与 style src 一致。

// styles/mixin.css
-.header-css {
-  width: 100%;
-  height: 100px;
-  background-color: #f00;
-}
-
<!-- index.mpx -->
-<style>
-  @import "../styles/mixin.css";
-</style>
-
<!-- list.mpx -->
-<style>
-  @import "../styles/mixin.css";
-</style>
-

对于多个页面或组件公用的样式,建议使用 style src 形式引入,避免一份样式被内联打成多份,同时还能使用 less、scss 等提升编码效率。

# CSS 压缩

在 production 模式下,框架默认会使用 cssnano (opens new window) 对 css 内容进行压缩。

框架默认内置 cssnano 的 default 预设,默认配置为:

cssnanoConfig = {
-  preset: ['cssnano-preset-default', minimizeOptions.optimisation || {}]
-}
-

以上配置为框架内置,开发者无需手动配置。

如果你想要使用 cssnano advanced 预设,则需要在 wxssLoader 中传入配置开启

{
-  test: /\.(wxss|acss|css|qss|ttss|jxss|ddss)$/,
-  use: [
-    MpxWebpackPlugin.wxssLoader({
-      minimize: {
-        advanced: true, // 使用 cssnano advanced preset
-        optimisation: {
-          'autoprefixer': true,
-          'discardUnused': true,
-          'mergeIdents': true
-        }
-      }
-    })
-  ]
-},
-

同时也需要安装 advanced 依赖:

npm i -D cssnano-preset-advanced
-

optimisation 配置可以点击详情 (opens new window)查看更多配置项。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/event.html b/docs-vuepress/.vuepress/dist/guide/basic/event.html deleted file mode 100644 index 2ad5251974..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/event.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - 事件处理 | Mpx框架 - - - - - - - - - -

# 事件处理

Mpx在事件处理上基于原生小程序,支持原生小程序的全部事件处理技术规范,在此基础上新增了事件处理内联传参的增强机制。

原生小程序事件处理详情请参考这里 (opens new window)

增强的内联传参能力对于传递参数的个数和类型没有特殊限制,可以传递各种字面量,可以传递组件数据,甚至可以传递for中的item和index, -当内联事件处理器中需要访问原始事件对象时,可以传递$event特殊关键字作为参数,在事件处理器的对应参数位置即可获取。

示例如下:

<template>
-  <view>
-    <!--原生小程序语法,通过dataset进行传参-->
-    <button data-name="a" bindtap="handleTap">a</button>
-    <!--Mpx增强语法,模板内联传参,方便简洁-->
-    <button bindtap="handleTapInline('b')">b</button>
-    <!--参数支持传递字面量和组件数据-->
-    <button bindtap="handleTapInline(name)"></button>
-    <!--参数同样支持传递for作用域下的item/index-->
-    <button wx:for="{{names}}" bindtap="handleTapInline(item)">{{item}}</button>
-    <!--需要使用原始事件对象时可以传递$event特殊关键字-->
-    <button bindtap="handleTapInlineWithEvent('g', $event)">g</button>
-  </view>
-</template>
-
-<script>
-  import { createComponent } from '@mpxjs/core'
-
-  createComponent({
-    data: {
-      name: 'c',
-      names: ['d', 'e', 'f']
-    },
-    methods: {
-      handleTap (e) {
-        console.log('name:', e.target.dataset.name)
-      },
-      // 直接通过参数获取数据,直观方便
-      handleTapInline (name) {
-        console.log('name:', name)
-      },
-      handleTapInlineWithEvent (name, e) {
-        console.log('name:', name)
-        console.log('event:', e)
-      }
-    }
-  })
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/ide.html b/docs-vuepress/.vuepress/dist/guide/basic/ide.html deleted file mode 100644 index 1a6dc86a12..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/ide.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - IDE 高亮配置 | Mpx框架 - - - - - - - - - -

# IDE 高亮配置

# IntelliJ

如果使用 IntelliJ 系 IDE 开发,可将.mpx后缀文件关联到vue模板类型,按vue模板解析。

关联文件类型

但会报一个warning提示有重复的script标签,关闭该警告即可。

关闭警告

# vscode

git地址 (opens new window),有任何vscode插件的问题和需求可在仓库中提issue

下载

  1. 下载地址 (opens new window)

  2. 也可直接在vscode扩展处搜索mpx即可下载

使用

# 插件功能介绍

  • 高亮
  • emmet
  • 跳转定义
  • 自动补全
  • eslint
  • 格式化

视频介绍 (opens new window)

# 高亮

  与其他语言插件无异,提供相应代码的高亮,因为Mpx分为四个模块,所以每个模块都有相应的语法高亮,还包括注释快捷键,也区分了相应模块,比如<template>中使用的是html的高亮,且注释是<!-- -->,而<script>中就是js的高亮,注释是//

image

# emmet

  早在使用sublime时就在使用emmet插件,以提高写HTML的效率。

  比如键入多个<view>标签:view*n

  比如一些标签的快速键入,配合tab或者Enter键快速键入

  不仅仅是<template>模块,css,scss,less,stylus,sass模块也有相应的快捷指令

image image

提示组件标签

我们可以像编写 html 一样,只要输入对应的单词就会出现对应的标签,比如输入的是 view ,然后按下 tab 键,即可输入 <view></view> 标签。

图片名称

组件指令提示

指令的提示类似于 vue 文件一样,只要输入对应的指令前缀就会出现对应的完整指令,比如输入的是 wx ,然后按下 tab 键,就可以输入 wx:if="" 指令。

图片名称

组件属性提示

微信小程序的每个组件都有一些属性选项,在编写组件的时候输入前缀就会出现完整的属性,并且包含了属性的说明和属性的类型。

图片名称

组件事件提示

给组件绑定事件,也是只需要输入事件的前缀,就会出现完整的事件列表,然后按下 tab 键,即可输入 bindtap="" 类似的事件。

图片名称

# 跳转定义

  command + 鼠标左键 查看定义位置,也可以在当前文件查看内容,决定是否跳转

image

# 自动补全

  毕竟Mpx是个小程序的框架,对于微信和支付宝的api快速补全snippets没有怎么能行,可在<script>中通过键入部分文字插入相应的代码块

image

# eslint

  eslint这块要分两部分来讲,一部分是插件实现了按照模块区分的简单的eslint,另一部分是要配合eslint的vscode插件,配置.eslintrc高阶的eslint检测。

部分一可通过配置开关

<template>是通过我们自己实现的eslint插件eslint-plugin-mpx,通过调eslint提供的引擎api,返回eslint校验的结果,我们再进行展示。

<script>中是通过调用typescript提供的检测js代码的api来进行检测,返回 -的校验结果也是不太符合语法的,基础的检测,不会过于苛刻

<style>中会根据lang的设定进行相应的检测,此检测是vscode官方提供的库 -vscode-css-languageservice

<json>模块同tempalte,用到了一个eslint插件eslint-plugin-jsonc来检测json的部分

image

部分二可参照此链接 (opens new window)配置

# 代码格式化

支持代码格式化 JavaScript (ts)· JSON · CSS (less/scss/stylus) · WXML,通过鼠标右键选择代码格式化文档。

图片名称

默认每个区块都是调用 Prettier 这个库来完成格式化的,当然也可以在设置中切换成使用其他库。

图片名称

如果切换成 none 将会禁用格式化。

Prettier 支持从项目根目录读取 .prettierrc 配置文件。配置选项可以参考 官方 (opens new window) 文档。.prettierrc 文件可以使用 JSON 语法编写,比如下面这样:

{
-  "tabWidth": 4,
-  "semi": false,
-  "singleQuote": true
-}
-

注意:由于 Prettier 这个库不支持格式化 stylus 语法,所以 stylus 的格式化使用另外一个 stylus-supremacy 库,配置 stylus 格式化规则只能在编辑器的 settings 中配置。

"mpx.format.defaultFormatterOptions": {
-  "stylus-supremacy": {
-    "insertColons": false, // 不使用括号
-    "insertSemicolons": false, // 不使用冒号
-    "insertBraces": false, // 不使用分号
-    "insertNewLineAroundImports": true, // import之后插入空行
-    "insertNewLineAroundBlocks": false // 每个块不添加空行
-  }
-}
-

总结一下,配置格式化有两种方式,一种是使用 .prettierrc 文件的形式配置,另一种是在编辑器的 settings 中自行配置,通过 mpx.format.defaultFormatterOptions 选项。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/intro.html b/docs-vuepress/.vuepress/dist/guide/basic/intro.html deleted file mode 100644 index f96f40a674..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/intro.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - 介绍 | Mpx框架 - - - - - - - - - -

# 介绍

# Mpx是什么?

Mpx是一款致力于提高小程序开发体验和开发效率的增强型小程序框架,通过Mpx,我们能够高效优雅地开发出具有极致性能的优质小程序应用,并将其输出到各大小程序平台和web平台中运行。

Mpx的核心设计理念在于增强,这意味着Mpx是对小程序原生开发标准的补强和扩充,同时也兼容了原生开发标准。Mpx的一个设计理念在于尽可能地依赖小程序原生的自有能力,如路由系统,自定义组件,事件系统和slot等能力,因此用户在使用Mpx开发小程序时,需要对原生小程序开发有一定程度的掌握。所幸小程序开发本身并不困难,我们也会在本文档中对必要的原生小程序开发知识进行一定程度地说明。

微信小程序作为小程序的开山鼻祖,具有最完善的生态和最全面的特性支持,后来的所有小程序平台在技术方案和代码语法上都与微信小程序高度相似,Mpx目前完善支持了以微信小程序增强语法为base的跨平台输出能力,本文档在介绍原生小程序开发相关的部分时也会以微信小程序为例。

最后,Mpx是一个开发框架,不是组件库,这一点经常会有开发者搞混。Mpx兼容业内已有的小程序组件库,如vant/iview等,我们之后也会开源内部基于Mpx开发的跨端组件库。

# Mpx提供了哪些能力?

# 单文件开发(SFC)

Mpx使用类似Vue的单文件开发模式,小程序原本的template/js/style/json都可以写在单个的.mpx文件中,清晰便捷。

# 数据响应

数据响应是Mpx提供的核心增强能力,该能力主要受Vue的启发,主要包含数据赋值响应,watch api和computed计算属性等能力,关于该能力更详细的介绍可以查看这里

# 增强的模板语法

同样受到Vue的启发,Mpx提供了很多增强模板语法便于开发者方便快捷地进行视图开发,主要包含以下:

# 极致性能

Mpx在性能上做到了极致,我们在框架中通过模板数据依赖收集进行了深度的setData优化,做到了程序上的最优,让用户能够专注于业务开发;

其次,Mpx的编译构建完全基于依赖收集,支持按需进行的npm构建,能够自动根据用户的分包配置抽离共用模块,确保用户最终产出项目的包体积最优;

最后,Mpx的运行时框架部分仅占用51KB;

Mpx和业内其他框架的运行时性能对比可以参考这篇文章 (opens new window)

# 状态管理

Mpx借鉴Vuex的设计实现一套与框架搭配使用的状态管理(store)工具,除了支持Vuex中已有的特性外,我们还创新地提出了一种多实例store的跨团队状态管理模式,我们在业务中实际使用后普遍认为该设计比原有的modules更加灵活方便,更多详情可以查看这里

# 编译构建

Mpx的编译构建以webpack为基础,针对小程序项目结构深度定制开发了一个webpack插件和一系列loaders,整个构建过程完全基于依赖收集按需打包,兼容大部分webpack自身能力及生态,此外Mpx的编译构建还支持以下能力:

# 跨平台能力

Mpx支持全部小程序平台(微信,支付宝,百度,头条,qq)的增强开发,同时支持将一份基于微信增强的业务源码输出到全部的小程序平台和web平台中运行,也即将支持输出快应用的能力,更多详情请查看这里

# 完善的周边能力

除了上述的核心能力外,Mpx还提供了丰富的周边能力支持,主要包括以下能力:

Mpx具有以下功能特性:

# 对比其他小程序框架

目前业内的小程序框架主要分为两类,一类是以uniapp,taro2为代表的静态编译型框架,这类框架以静态编译为主要手段,将React和Vue开发的业务源码转换到小程序环境中进行适配运行。这类框架的主要优点在于web项目迁移方便,跨端能力较强。但是由于React/Vue等web框架的DSL与小程序本身存在较大差距,无法完善支持原web框架的全部能力,开发的时候容易踩坑。

另一类是以kbone,taro3为代表的运行时框架,这类框架利用小程序本身提供的动态渲染能力,在小程序中模拟出web的运行时环境,让React/Vue等框架直接在上层运行。这类框架的优点在于web项目迁移方便,且在web框架语法能力的支持上比静态编译型的框架要强很多,开发时遇到的坑也会少很多。但是由于模拟的web运行时环境带来了巨大的性能开销,这类框架并不适合用于大型复杂的小程序开发。

不同于上面两类框架,Mpx以小程序本身的DSL为基础,通过编译和运行时手段结合对其进行了一系列拓展增强,没有复杂庞大的转译和环境抹平,在提升用户开发体验和效率的同时,既能保障开发的稳定和可预期性,又能保障接近原生的良好性能,非常适合开发大型复杂的小程序应用。

在跨端方面,Mpx重点保障跨小程序平台的跨端能力,由于各家小程序标准具有很强的相似性,Mpx在进行跨端输出时,以静态编译为主要手段,辅以灵活便捷的条件编译,保障了跨端输出的性能和可用性。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/list-render.html b/docs-vuepress/.vuepress/dist/guide/basic/list-render.html deleted file mode 100644 index def7b0a120..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/list-render.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - 列表渲染 | Mpx框架 - - - - - - - - - -

# 列表渲染

Mpx中的列表渲染与原生小程序中完全一致,详情可以查看这里 (opens new window)

值得注意的是wx:key与Vue中的key属性的区别,不能使用数据绑定,只能传递普通字符串将数组item中的对应属性作为key,或者传入保留关键字*this将item本身作为key

下面是简单示例:

<template>
-  <!-- 使用数组中元素的 id属性/保留关键字*this 作为key值  -->
-  <view wx:for="{{titleList}}" wx:key="id">
-    <!-- item 默认代表数组的当前项 -->
-    <view>{{item.id}}: {{item.name}}</view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      titleList: [
-        { id: 1, name: 'A' },
-        { id: 2, name: 'B' },
-        { id: 3, name: 'C' }
-      ]
-    }
-  })
-</script>
-

# 特殊处理

当列表中一些需要特殊二次处理的数据,可参考下列两种方式

# computed方式

<template>
-  <!-- 使用数组中元素的 id属性/保留关键字*this 作为key值  -->
-  <view wx:for="{{refactorTitleList}}" wx:key="id">
-    <!-- item 默认代表数组的当前项 -->
-    <view>{{item.id}}: {{item.name}}</view>
-    <!-- bad方式 不可用computed或methods中方法处理 -->
-    <!-- <view>{{item.id}}: {{format(item.name)}}</view> -->
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      titleList: [
-        { id: 1, name: 'A' },
-        { id: 2, name: 'B' },
-        { id: 3, name: 'C' }
-      ],
-      nameMap: {
-        '1': 'mpx'
-      }
-    },
-    computed: {
-      refactorTitleList () {
-        return this.titleList.map(item => {
-          // 列表中需要特殊处理的数据可在computed中处理好再渲染
-          item.name = this.nameMap[item.id] ? this.nameMap[item.id] : item.name
-          return item
-        })
-      }
-    }
-  })
-</script>
-

# wxs方式

<template>
-  <wxs module="foo">
-    var formatName = function (item, nameMap) {
-      // 这里区别string和number
-      var id = ''+item.id
-      if(nameMap[id]){
-        return nameMap[id];
-      }
-      return item.name;
-    }
-    module.exports = {
-      formatName: formatName
-    };
-  </wxs>
-  <!-- 使用数组中元素的 id属性/保留关键字*this 作为key值  -->
-  <view wx:for="{{titleList}}" wx:key="id">
-    <!-- item 默认代表数组的当前项 -->
-    <view>{{item.id}}: {{foo.formatName(item, nameMap)}}</view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      titleList: [
-        { id: 1, name: 'A' },
-        { id: 2, name: 'B' },
-        { id: 3, name: 'C' }
-      ],
-      nameMap: {
-        '1': 'mpx'
-      }
-    }
-  })
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/option-chain.html b/docs-vuepress/.vuepress/dist/guide/basic/option-chain.html deleted file mode 100644 index f70ea3b9b0..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/option-chain.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - 模版内可选链表达式 | Mpx框架 - - - - - - - - - -

# 模版内可选链表达式

Mpx提供了在模版中使用可选链?.访问变量属性的能力,用法/能力和JS的可选链基本一致,可以在任意被{{}}所包裹的模版语法内使用。

实现原理是在编译时将使用?.的部分转化为wxs函数调用,运行时通过wxs访问变量属性,模拟出可选链的效果。

使用示例:

  <template>
-    <view wx:if="{{ a?.b }}">{{ a?.c + a?.d }}</view>
-    <view wx:for="{{ a?.d || [] }}"></view>
-    <view>{{ a?.d ? 'a' : 'b' }}</view>
-    <view>{{ a?.g[e?.d] }}</view>
-  </template>
-
-  <script>
-    import { createComponent } from '@mpxjs/core'
-
-    createComponent({
-      data: {
-        a: {
-            b: true,
-            c: 123,
-            g: {
-                d: 321
-            }
-        },
-        e: {
-            d: 'd'
-        }
-      }
-    })
-  </script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/reactive.html b/docs-vuepress/.vuepress/dist/guide/basic/reactive.html deleted file mode 100644 index 71fe87fcb3..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/reactive.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - 数据响应 | Mpx框架 - - - - - - - - - -

# 数据响应

Vue中最为开发者们所津津乐道的特性就是其数据响应特性,Vue中的数据响应特性主要包含:响应数据赋值、watch观察数据和computed计算属性,该能力让我们能够以非常直观简洁的方式建立起数据与数据之间/数据与视图之间的绑定依赖关系,大幅提升复杂webapp的开发体验和开发效率。

受此启发,Mpx通过增强的方式在小程序开发中提供了完整的数据响应特性,在2.5.x版本前,Mpx通过mobx实现内部和核心数据响应,在2.5.x版本之后,从数据响应性能、完整度和运行时包体积等多方面角度考虑,我们借鉴了Vue2的设计,在内部自行维护了一套精简高效的数据响应系统,全面移除了对于mobx的依赖。

在新的响应系统中,所有的使用方法和使用限制都和Vue保持一致,关于数据响应的原理和使用限制可以参考这里 (opens new window)

下面是一个小程序数据响应的简单示例:

<template>
-  <view>
-    <view>Num: {{num}}</view>
-    <view>Minus num: {{mnum}}</view>
-    <view>Num plus: {{nump}}</view>
-    <view>{{info.name}}</view>
-    <view wx:if="{{info.age}}">{{info.age}}</view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    // data中定义的数据会在初始化时进行数据响应处理
-    data: {
-      num: 1,
-      info: {
-        name: 'test'
-      }
-    },
-    // 配置中直接定义watch
-    watch: {
-      num (val) {
-        console.log(val)
-      }
-    },
-    // 定义计算属性,模板中可以直接访问
-    computed: {
-      mnum () {
-        return -this.num
-      },
-      nump: {
-        get () {
-          return this.num + 1
-        },
-        // 支持计算属性的setter
-        set (val) {
-          this.num = val - 1
-        }
-      }
-    },
-    onReady () {
-      // 使用实例方法定义watch,可以传递追踪函数更加灵活
-      this.$watch(() => {
-        return this.nump - this.mnum
-      }, (val) => {
-        console.log(val)
-      })
-      // 每隔一秒进行一次更新,相关watcher会被触发,视图也会发生更新
-      setInterval(() => {
-        this.num++
-      }, 1000)
-      // 使用$set新增响应属性,视图同样能够得到更新
-      setTimeout(() => {
-        this.$set(this.info, 'age', 23)
-      }, 1000)
-    }
-  })
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/refs.html b/docs-vuepress/.vuepress/dist/guide/basic/refs.html deleted file mode 100644 index 1ce9cdb5d7..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/refs.html +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - 获取组件实例/节点信息 | Mpx框架 - - - - - - - - - -

# 获取组件实例/节点信息

微信小程序中原生提供了selectComponent/SelectorQuery.select方法获取自定义组件实例和wxml节点信息,但是该api使用起来不太方便,并且不具备平台无关性,我们提供了增强指令wx:ref用于获取组件实例及节点信息,该指令的使用方式同vue中的ref类似,在模板中声明了wx:ref后,在组件ready后用户可以通过this.$refs获取对应的组件实例或节点查询对象(NodeRefs),调用响应的组件方法或者获取视图节点信息。

wx:ref其实也是模板编译和运行时注入结合实现的语法糖,本质还是通过原生小程序平台提供的能力进行获取,其实现的主要意义在于抹平跨平台差异以及提升用户的使用体验。

简单的使用示例如下:

  <template>
-    <view class="container">
-      <!-- my-header 为一个组件,组件内部定义了一个 show 方法 -->
-      <my-header wx:ref="myHeader"></my-header>
-      <view wx:ref="content"></view>
-    </view>
-  </template>
-
-  <script>
-    import { createComponent } from '@mpxjs/core'
-
-    createComponent({
-      ready() {
-        // 通过 this.$refs 获取view的节点查询对象
-        this.$refs.content.fields({size: true},function (res){
-          // res 就是我们要拿到的节点大小
-        }).exec()
-        // 通过 this.$refs 可直接获取组件实例
-        this.$refs.myHeader.show()  // 拿到组件实例,调用组件内部的方法
-      }
-    })
-  </script>
-

# 在列表渲染中使用wx:ref

在列表渲染中定义的wx:ref存在多个实例/节点,Mpx会在模板编译中判断某个wx:ref是否存在于列表渲染wx:for中,是的情况下在注入this.$refs时会通过selectAllComponents/SelectQuery.selectAll方法获取组件实例数组或数组节点查询对象,确保开发者能拿到列表渲染中所有的组件实例/节点信息。

使用示例如下:

<template>
-  <view>
-    <!-- list 组件 -->
-    <list wx:ref="list" wx:for="{{listData}}" data="{{item.name}}" wx:key="id"></list>
-    <!-- view 节点 -->
-    <view wx:ref="test" wx:for="{{listData}}" wx:key="id">{{item.name}}</view>
-  </view>
-</template>
-
-<script>
-  import { createComponent } from '@mpxjs/core'
-
-  createComponent({
-    data: {
-      listData: [
-        {id: 1, name: 'A'},
-        {id: 2, name: 'B'},
-        {id: 3, name: 'C'},
-      ]
-    },
-    ready () {
-      // 通过 this.$refs.list 获取的是组件实例的数组
-      this.$refs.list.forEach(item => {
-        // 对每一个组件实例的操作...
-      })
-      // 通过 this.$refs.test 获取的是节点查询对象,通过相关的方法操作节点
-      this.$refs.test.fields({size: true}, function (res) {
-        // 此处拿到的 res 是一个数组,包含了列表渲染中的所有节点的大小信息
-      }).exec()
-    }
-  })
-</script>
-
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/single-file.html b/docs-vuepress/.vuepress/dist/guide/basic/single-file.html deleted file mode 100644 index 4b872a020a..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/single-file.html +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - 单文件开发 | Mpx框架 - - - - - - - - - -

# 单文件开发

小程序规范中每个页面和组件都是由四个文件描述组成的,wxml/js/wxss/json,分别描述了组件/页面的视图模板,执行逻辑,样式和配置,由于这四个部分彼此之间存在相关性,比如模板中的组件需要在json中注册,数据需要在js中定义,这种离散的文件结构在实际开发的时候体验并不理想;受Vue单文件开发的启发,Mpx也提供了类似的单文件开发模式,拓展名为.mpx。

从下面的简单例子可以看出,.mpx中的四个区块分别对应了原生规范中的四个文件,Mpx在执行编译构建时会通过内置的mpx-loader收集依赖,并将.mpx的文件转换输出为原生规范中的四个文件。

<!--对应wxml文件-->
-<template>
-  <list></list>
-</template>
-<!--对应js文件-->
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    onLoad () {
-    },
-    onReady () {
-    }
-  })
-</script>
-<!--对应wxss文件-->
-<style lang="stylus">
-</style>
-<!--对应json文件-->
-<script type="application/json">
-  {
-    "usingComponents": {
-      "list": "../components/list"
-    }
-  }
-</script>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/start.html b/docs-vuepress/.vuepress/dist/guide/basic/start.html deleted file mode 100644 index 6aadde0f98..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/start.html +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - 快速开始 | Mpx框架 - - - - - - - - - -

# 快速开始

# 安装脚手架

npm i -g @mpxjs/cli
-

@mpxjs/cli文档 https://github.com/mpx-ecology/mpx-cli

# 创建项目安装依赖

在当前目录下创建mpx项目。

mpx create mpx-project
-

也可以使用npx在不全局安装脚手架情况下创建项目。

npx @mpxjs/cli create mpx-project
-

执行命令后会弹出一系列问题进行项目初始配置,根据自身需求进行选择,完成后进入项目目录进行依赖安装。

npm install
-

创建插件项目由于微信限制必须填写插件的AppID,创建普通项目无强制要求。

# 编译构建

使用npm script执行mpx的编译构建,在开发模式下我们执行serve命令,将项目源码构建输出到dist/${平台目录}下,并且监听源码的改动进行重新编译。

npm run serve
-

# 预览调试

使用小程序开发者工具打开dist下对应平台的目录,对你的小程序进行预览、调试,详情可参考小程序开发指南 (opens new window)

开启小程序开发者工具的watch选项,配合mpx本身的watch,能够得到很好的开发调试体验。

# 开始code

在Mpx中,我们使用@mpxjs/core提供的createApp、createPage和createComponent函数(分别对应原生小程序中的App、Page和Component)来创建App、页面和组件,我们下面根据脚手架创建出的初始项目目录,进行简单的介绍。

进入src/app.mpx,我们可以看到里面的结构和.vue文件非常类似,三个区块分别对应了小程序中的js/wxss/json文件。

js区块中调用createApp用于注册小程序,传入的配置可以参考小程序App构造器 (opens new window),由于app.js是小程序全局最早执行的js模块,一般mpx插件安装等初始化操作也在这里进行。

style区块对应app.wxss定义了全局样式,可以自由使用sass/less/stylus等css预编译语言。

json区块完全支持小程序原生的app.json配置 (opens new window),还额外支持了packages多人合作等增强特性。

<script>
-  import mpx, { createApp } from '@mpxjs/core'
-  import apiProxy from '@mpxjs/api-proxy'
-
-  mpx.use(apiProxy, {
-    usePromise: true
-  })
-  
-  createApp({
-    onLaunch () {
-
-    }
-  })
-</script>
-
-<style lang="stylus">
-  .bold
-    font-weight bold
-</style>
-
-<script type="application/json">
-  {
-    "pages": [
-      "./pages/index"
-    ]
-  }
-</script>
-

进入src/pages/index.mpx,可以看到同样是.vue风格的单文件结构,比起上面的app.mpx多了一个template区块,用于定义页面模板,除了支持小程序本身的全部模块语法和指令外,mpx还参考vue提供了大量模板增强指令,便于用户更快速高效地进行界面开发。

在js中调用createPage创建页面时,除了支持原本小程序支持的Page配置 (opens new window)外,我们还支持以数据响应为核心的一系列增强能力。

在json中,我们同样支持原生的页面json配置 (opens new window),此外,我们能够直接在usingComponents中填写npm地址引用npm包中的组件,mpx组件和原生小程序组件均可引用,无需调用开发者工具npm编译,且能够通过依赖收集按需进行打包。

为了保障增强能力的完整性,在支持的平台中Mpx优先使用Component构造器创建页面,支持全部Component生命周期;在某些特殊情况下,你可以在@mpxjs/webpack-plugin中传入forceUsePageCtor:true配置来禁用掉这个行为。

<template>
-  <list></list>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    onLoad () {
-    },
-    onReady () {
-    }
-  })
-</script>
-
-<style lang="stylus">
-
-</style>
-
-<script type="application/json">
-  {
-    "usingComponents": {
-      "list": "../components/list"
-    }
-  }
-</script>
-

最后,我们进入src/components/list.mpx文件,可以看到其构成与页面文件十分相似,对于组件,Mpx提供了和页面完全一致的增强能力

<template>
-  <view class="list">
-    <view wx:for="{{listData}}" wx:key="index">{{item}}</view>
-  </view>
-</template>
-
-<script>
-  import { createComponent } from '@mpxjs/core'
-
-  createComponent({
-    data: {
-      listData: ['手机', '电视', '电脑']
-    }
-  })
-</script>
-
-<style lang="stylus">
-  .list
-    background-color red
-</style>
-
-<script type="application/json">
-  {
-    "component": true
-  }
-</script>
-

更多用法可以查看我们的官方实例:https://github.com/didi/mpx/tree/master/examples/ (opens new window)

# 跨平台输出

如果你选择的base平台为微信,mpx提供了强大的跨平台输出能力,能够将你的小程序源码输出到目前业内的全部小程序平台(微信/支付宝/百度/头条/QQ)中和web平台中运行。

执行以下命令进行跨平台输出

npm run build:cross
-

关于跨平台能力的更多详情请查看这里

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/template.html b/docs-vuepress/.vuepress/dist/guide/basic/template.html deleted file mode 100644 index 975c328298..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/template.html +++ /dev/null @@ -1,137 +0,0 @@ - - - - - - 模板语法 | Mpx框架 - - - - - - - - - -

# 模板语法

Mpx中的模板语法以小程序模板语法为基础,支持小程序的全部模板语法,同时提供了一系列增强的模板指令及语法。

小程序原生模板语法请参考这里 (opens new window)

Mpx提供的增强指令语法如下:

下面是使用了模板增强语法的一个简单实例,许多在原生小程序上很繁琐的模板描述在增强语法的帮助下变得清晰简洁:

<template>
-  <!--动态样式-->
-  <view class="container" wx:style="{{dynamicStyle}}">
-    <!--数据绑定-->
-    <view class="title">{{title}}</view>
-    <!--计算属性数据绑定-->
-    <view class="title">{{reversedTitle}}</view>
-    <view class="list">
-      <!--循环渲染,动态类名,事件处理内联传参-->
-      <view wx:for="{{list}}" wx:key="id" class="list-item" wx:class="{{ {active:item.active} }}"
-            bindtap="handleTap(index)">
-        <view>{{item.content}}</view>
-        <!--循环内部双向数据绑定-->
-        <input type="text" wx:model="{{list[index].content}}"/>
-      </view>
-    </view>
-    <!--自定义组件获取实例,双向绑定,自定义双向绑定属性及事件-->
-    <custom-input wx:ref="ci" wx:model="{{customInfo}}" wx:model-prop="info" wx:model-event="change"/>
-    <!--动态组件,is传入组件名字符串,可使用的组件需要在json中注册,全局注册也生效-->
-    <component is="{{current}}"></component>
-    <!--显示/隐藏dom-->
-    <view class="bottom" wx:show="{{showBottom}}">
-      <!--模板条件编译,__mpx_mode__为框架注入的环境变量,条件判断为false的模板不会生成到dist-->
-      <view wx:if="{{__mpx_mode__ === 'wx'}}">wx env</view>
-      <view wx:if="{{__mpx_mode__ === 'ali'}}">ali env</view>
-    </view>
-  </view>
-</template>
-
-<script>
-  import { createPage } from '@mpxjs/core'
-
-  createPage({
-    data: {
-      // 动态样式和类名也可以使用computed返回
-      dynamicStyle: {
-        fontSize: '16px',
-        color: 'red'
-      },
-      title: 'hello world',
-      list: [
-        {
-          content: '全军出击',
-          id: 0,
-          active: false
-        },
-        {
-          content: '猥琐发育,别浪',
-          id: 1,
-          active: false
-        }
-      ],
-      customInfo: {
-        title: 'test',
-        content: 'test content'
-      },
-      current: 'com-a',
-      showBottom: false
-    },
-    computed: {
-      reversedTitle () {
-        return this.title.split('').reverse().join('')
-      }
-    },
-    handleTap (index) {
-      // 处理函数直接通过参数获取当前点击的index,清晰简洁
-      this.list[index].active = !this.list[index].active
-    }
-  })
-</script>
-
-<script type="application/json">
-  {
-    "usingComponents": {
-      "custom-input": "../components/custom-input",
-      "com-a": "../components/com-a",
-      "com-b": "../components/com-b"
-    }
-  }
-</script>
-

# 模板预编译

Mpx还支持开发者使用插值语法与小程序不冲突第三方的模板引擎语法来编写template,如pug:

<template lang="pug">
-  view(class="list")
-    view(class="list-item") red
-    view(class="list-item") blue
-    view(class="list-item") green
-</template>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html b/docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html deleted file mode 100644 index 6f45b2eeeb..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/basic/two-way-binding.html +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - 双向绑定 | Mpx框架 - - - - - - - - - -

# 双向绑定

Mpx针对表单组件提供了wx:model双向绑定指令,类似于v-model,该指令是一个语法糖指令,监听了组件抛出的输入事件并对绑定的数据进行更新,默认情况下会监听表单组件的input事件,并将event.detail.value中的数据更新到组件的value属性上。

简单实用示例如下:

<view>
-  <input type="text" wx:model="{{message}}"/>
-  <!--view中的文案会随着用户对输入框进行输入而实时更新-->
-  <view>{{message}}</view>
-</view>
-

# 对自定义组件使用

对自定义组件使用双向绑定时用法与原生小程序组件完全一致

<view>
-  <custom-input type="text" wx:model="{{message}}"/>
-  <!--此处的文案会随着输入框输入实时更新-->
-  <view>{{message}}</view>
-</view>
-

# 更改双向绑定的监听事件及数据属性

如前文所述,wx:model指令默认监听组件抛出的input事件,并将声明的数据绑定到组件的value属性上,该行为在一些原生组件和自定义组件上并不成立,因为这些组件可能不存在input事件或value属性。对此,我们提供了wx:model-eventwx:model-prop指令来修改双向绑定的监听事件和数据属性,使用示例如下:

<view>
-  <!--原生组件picker中没有input事件,通过wx:model-event指令将双向绑定监听事件改为change事件-->
-  <picker mode="selector" range="{{countryRange}}" wx:model="{{country}}" wx:model-event="change">
-    <view class="picker">
-      当前选择: {{country}}
-    </view>
-  </picker>
-  <!--通过wx:model-event和wx:model-prop将该自定义组件的双向绑定监听事件和数据属性修改为customInput/customValue-->
-  <custom-input wx:model="{{message}}" wx:model-event="customInput" wx:model-prop="customValue"/>
-  <view>{{message}}</view>
-</view>
-

# 更改双向绑定事件数据路径

Mpx中双向绑定默认使用event对象中的event.detail.value作为用户输入来更新组件数据,该行为在一些原生组件和自定义组件中也不成立,例如vant中的field输入框组件,用户的输入直接存储在event.detail当中,当然用户也可以将其存放在detail中的其他数据路径下,对此,我们提供了wx:model-value-path指令让用户声明在事件当中应该访问的数据路径。

由于小程序triggerEvent的Api设计,事件的用户数据都只能存放在event.detail中,因此wx:model-value-path的值都是相对于event.detail的数据路径,我们支持两种形式进行声明:

  • 一种是点语法,如传入current.value时框架会从event.detail.current.value中取值作为用户输入,为空字符串时wx:model-value-path=""代表直接使用event.detail作为用户输入;
  • 第二种是数组字面量的JSON字符串,如["current", "value"]与上面的current.value等价,传入[]时与上面的空字符串等价。

使用示例如下:

<view>
-  <!--wx:model-value-path传入[]直接使用event.detail作为用户输入,使vant-field中双向绑定能够生效-->
-  <van-field wx:model="{{username}}" wx:model-value-path="[]" label="用户名" placeholder="请输入用户名"/>
-</view>
-

# 双向绑定过滤器

用户可以使用wx:model-filter指令定义双向绑定过滤器,在修改数据之前对用户输入进行过滤,来实现特定的效果,框架内置了trim过滤器对用户输入进行trim操作,传入其他字符串时会使用当前组件中的同名方法作为自定义过滤器,使用示例如下:

<view>
-  <!--以下示例中,用户输入的首尾空格将被过滤-->
-  <input wx:model="{{message}}" wx:model-filter="trim"/>
-  <view>{{message}}</view>
-</view>
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html b/docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html deleted file mode 100644 index 7f655aade1..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/composition-api/composition-api.html +++ /dev/null @@ -1,527 +0,0 @@ - - - - - - 组合式 API | Mpx框架 - - - - - - - - - -

# 组合式 API

# 什么是组合式 API

组合式 API 是 Vue3 中包含的最重要的特性之一,主要由 setup 函数及一系列响应式 API 及生命周期钩子函数组成,与传统的选项式 API 相比,组合式 API 具备以下优势:

  • 更好的逻辑复用,通过函数包装复用逻辑,显式引入调用,方便简洁且符合直觉,规避消除了 mixins 复用中存在的缺陷;
  • 更灵活的代码组织,相比于选项式 API 提前规定了代码的组织方式,组合式 API 在这方面几乎没有做任何限制与规定,更加灵活自由,在功能复杂的庞大组件中,我们能够通过组合式 API 让我们的功能代码更加内聚且有条理,不过这也会对开发者自身的代码规范意识提出更高要求;
  • 更方便的类型推导,虽然基于 this 的选项式 API 通过 ThisType 也能在一定程度上实现 TS 类型推导,但推导和实现成本较高,同时仍然无法完美覆盖一些复杂场景(如嵌套 mixins 等);而组合式 API 以本地变量和函数为基础,本身就是类型友好的,我们在类型方面几乎不需要做什么额外的工作就能享受到完美的类型推导。

同时与 React Hooks 相比,组合式 API 中的 setup 函数只在初始化时单次执行,在数据响应能力的加持下大大降低了理解与使用成本,基于以上原因,我们决定为 Mpx 添加组合式 API 能力,让用户能够用组合式 API 方式进行小程序开发。

更多关于组合式 API 的说明可以查看 Vue3 官方文档 (opens new window)

Mpx 是一个小程序优先的增强型跨端框架,因此我们在为 Mpx 设计实现组合式 API 的过程中,并不追求与 Vue3 中的组合式 API 完全一致,我们更多是借鉴 Vue3 中组合式 API 的设计思想,将其与目前 Mpx 及小程序中的开发模式结合起来,而非完全照搬其实现。因此在 Mpx 中一些具体的 API 设计实现会与 Vue3 存在差异,我们会在后续相关的文档中进行标注说明,如果你想查看 Mpx 与 Vue3 在组合式 API 中的差异,可以跳转到这里查看。

# 组合式 API 基础

# setup 函数

同 Vue3 一样,在 Mpx 当中 setup 函数是组合式 API 的基础,我们可以在 createPagecreateComponent 中声明 setup 函数。

setup 函数接收 propscontext 两个参数,其中 context 参数与 Vue3 中存在差别,详情可以查看这里

我们参考 Vue3 中的示例实现一个小程序版本,可以看到它和 Vue3 中的实现基本一致,包含以下功能:

  • 仓库列表
  • 更新仓库列表的函数
  • 返回列表和函数,以便其他组件选项可以对它们进行访问
import { createComponent } from '@mpxjs/core'
-import { fetchUserRepositories } from '@/api/repositories'
-
-createComponent({
-  properties: {
-    user: String
-  },
-  setup (props) {
-    let repositories = []
-    const getUserRepositories = async () => {
-      repositories = await fetchUserRepositories(props.user)
-    }
-
-    return {
-      repositories, // 返回的数据,可以在其他选项式 API 中通过 this 访问或在模板上直接访问
-      getUserRepositories // 返回的函数,它的行为与将其定义在 methods 选项中的行为相同
-    }
-  }
-})
-

目前 repositories 是个非响应式变量,用户层面将无法感知它的变化,仓库列表将始终为空。

#ref 的响应式变量

同 Vue3 一致,我们可以通过 ref 函数为任意一个变量创建响应式引用,ref 接收参数并将其包裹在一个带有 value property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值,简单示例如下:

import { ref } from '@mpxjs/core'
-
-const counter = ref(0)
-
-console.log(counter) // { value: 0 }
-console.log(counter.value) // 0
-
-counter.value++
-console.log(counter.value) // 1
-

回到我们的示例中,我们通过 ref 创建一个响应式的 repositories 变量:

import { createComponent, ref } from '@mpxjs/core'
-import { fetchUserRepositories } from '@/api/repositories'
-
-createComponent({
-  properties: {
-    user: String
-  },
-  setup (props) {
-    let repositories = ref([])
-    const getUserRepositories = async () => {
-      repositories.value = await fetchUserRepositories(props.user)
-    }
-
-    return {
-      repositories,
-      getUserRepositories
-    }
-  }
-})
-

通过 ref 包裹以后,我们每次调用 getUserRepositories 更新 repositories 的值都能被外部的 watch 观测到,并且触发视图的更新。

#setup 中注册生命周期钩子

为了完整实现选项式 API 中的能力,我们需要支持在 setup 中注册生命周期钩子,同 Vue3 类似,我们也提供了一系列生命周期钩子的注册函数,这些函数都以 on 开头,不过由于 Mpx 的跨平台特性,我们不可能针对不同的平台提供不同的生命周期钩子函数,因此我们提供了一份抹平跨平台差异后统一的生命周期钩子,与小程序原生生命周期的映射关系可查看这里

我们希望在组件挂载时调用 getUserRepositories,可以使用 onMounted 钩子来实现,注意这里不是 onReady,不过它确实对应于微信小程序中组件的 ready 钩子:

import { createComponent, ref, onMounted } from '@mpxjs/core'
-import { fetchUserRepositories } from '@/api/repositories'
-
-createComponent({
-  properties: {
-    user: String
-  },
-  setup (props) {
-    let repositories = ref([])
-    const getUserRepositories = async () => {
-      repositories.value = await fetchUserRepositories(props.user)
-    }
-    
-    // 在组件挂载时(微信小程序中相当于ready)调用 getUserRepositories
-    onMounted(getUserRepositories) 
-
-    return {
-      repositories,
-      getUserRepositories
-    }
-  }
-})
-

# watch 响应式更改

现在我们需要对 user prop 的变化做出响应,可以使用新的 watch API,该 API 接受 3 个参数:

  • 一个想要侦听的响应式引用或 getter 函数
  • 一个回调
  • 可选的配置选项

简单示例如下:

import { ref, watch } from '@mpxjs/core'
-
-const counter = ref(0)
-watch(counter, (newValue, oldValue) => {
-  console.log('The new counter value is: ' + counter.value)
-})
-

counter 被修改时,例如 counter.value = 5,侦听将触发并执行回调打印 The new counter value is:5

回到我们的示例中,当 user prop 发生变化时调用 getUserRepositories 更新仓库列表:

import { createComponent, ref, onMounted, watch, toRefs } from '@mpxjs/core'
-import { fetchUserRepositories } from '@/api/repositories'
-
-createComponent({
-  properties: {
-    user: String
-  },
-  setup (props) {
-    // 使用 `toRefs` 创建对 `props` 中的 `user` property 的响应式引用
-    const { user } = toRefs(props)
-    
-    let repositories = ref([])
-    const getUserRepositories = async () => {
-      // 更新 `prop.user` 到 `user.value` 访问引用值
-      repositories.value = await fetchUserRepositories(user.value)
-    }
-    
-    onMounted(getUserRepositories) 
-    
-    // 在 user prop 的响应式引用上设置一个侦听器
-    watch(user, getUserRepositories)
-
-    return {
-      repositories,
-      getUserRepositories
-    }
-  }
-})
-

你可能已经注意到在我们的 setup 的顶部使用了 toRefs。这是为了确保我们的侦听器能够根据 user prop 的变化做出反应。

# 独立的 computed 属性

refwatch 类似,也可以使用从 Mpx 导入的 computed 函数创建计算属性,简单示例如下:

import { ref, computed } from '@mpxjs/core'
-
-const counter = ref(0)
-const twiceTheCounter = computed(() => counter.value * 2)
-
-counter.value++
-console.log(counter.value) // 1
-console.log(twiceTheCounter.value) // 2
-

这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。

回到我们的示例,我们通过 computed 为仓库列表实现搜索功能:

import { createComponent, ref, onMounted, watch, toRefs, computed } from '@mpxjs/core'
-import { fetchUserRepositories } from '@/api/repositories'
-
-createComponent({
-  properties: {
-    user: String
-  },
-  setup (props) {
-    const { user } = toRefs(props)
-    
-    let repositories = ref([])
-    const getUserRepositories = async () => {
-      repositories.value = await fetchUserRepositories(user.value)
-    }
-    
-    onMounted(getUserRepositories) 
-    
-    watch(user, getUserRepositories)
-    
-    const searchQuery = ref('')
-    const repositoriesMatchingSearchQuery = computed(() => {
-      return repositories.value.filter(
-        repository => repository.name.includes(searchQuery.value)
-      )
-    })
-
-    return {
-      repositories,
-      getUserRepositories,
-      searchQuery,
-      repositoriesMatchingSearchQuery
-    }
-  }
-})
-

可以看到目前我们的 setup 函数已经相当庞大了,在不进行任何封装的情况下我们很可能在 setup 中写出流水账式的代码,可维护性很差,这也是为什么组合式 API 会对开发者的代码风格提出更高的要求,下面我们来拆解目前已经实现的功能,将它们提取到独立的组合函数中,先从创建 useUserRepositories 函数开始:

// src/composables/useUserRepositories.js
-
-import { fetchUserRepositories } from '@/api/repositories'
-import { ref, onMounted, watch } from '@mpxjs/core'
-
-export default function useUserRepositories(user) {
-  const repositories = ref([])
-  const getUserRepositories = async () => {
-    repositories.value = await fetchUserRepositories(user.value)
-  }
-
-  onMounted(getUserRepositories)
-  watch(user, getUserRepositories)
-
-  return {
-    repositories,
-    getUserRepositories
-  }
-}
-
-

然后是搜索功能:

// src/composables/useRepositoryNameSearch.js
-
-import { ref, computed } from '@mpxjs/core'
-
-export default function useRepositoryNameSearch(repositories) {
-  const searchQuery = ref('')
-  const repositoriesMatchingSearchQuery = computed(() => {
-    return repositories.value.filter(repository => {
-      return repository.name.includes(searchQuery.value)
-    })
-  })
-
-  return {
-    searchQuery,
-    repositoriesMatchingSearchQuery
-  }
-}
-

现在我们有了两个单独的功能模块,接下来就可以开始在组件中使用它们了:

import useUserRepositories from '@/composables/useUserRepositories'
-import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
-import { createComponent, toRefs } from '@mpxjs/core'
-
-
-createComponent({
-  properties: {
-    user: String
-  },
-  setup (props) {
-    const { user } = toRefs(props)
-    
-    const { repositories, getUserRepositories } = useUserRepositories(user)
-    
-    const {
-      searchQuery,
-      repositoriesMatchingSearchQuery
-    } = useRepositoryNameSearch(repositories)
-
-    return {
-      // 因为我们并不关心未经过滤的仓库
-      // 我们可以在 `repositories` 名称下暴露过滤后的结果
-      repositories: repositoriesMatchingSearchQuery,
-      getUserRepositories,
-      searchQuery,
-    }
-  }
-})
-

现在我们使用组合式 API 完成了仓库列表组件的开发,它看上去相当清晰且易于维护。想要了解更多组合式 API 的信息,请继续查看本页接下来的章节,想要了解新的响应式 API 的使用,请跳转查看响应式 API章节。

# Setup

setup 函数在组件创建时执行,返回组件所需的数据和方法,是组合式 API 的核心。

注意,setup 函数不可被混入,所有定义在 mixin 中的 setup 都将被丢弃。

# 参数

setup 函数接受两个参数,分别是 propscontext

# Props

setup 函数的第一个参数 props 和 Vue3 中完全一致,就是包含当前组件 props 的响应式数据,当父组件更新某个 prop 时,该数据也将被更新。

import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  props: {
-    title: String
-  },
-  setup(props) {
-    console.log(props.title)
-  }
-})
-

Warning: props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。

如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数将其转换一个包含 ref 数据的纯对象:

import { createComponent, toRefs } from '@mpxjs/core'
-
-createComponent({
-  props: {
-    title: String
-  },
-  setup(props) {
-    const { title } = toRefs(props)
-    // `title` 是一个 ref
-    console.log(title.value)
-  }
-})
-

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

import { createComponent, toRef } from '@mpxjs/core'
-
-createComponent({
-  props: {
-    title: String
-  },
-  setup(props) {
-    const title = toRef(props, 'title')
-    // `title` 是一个 ref
-    console.log(title.value)
-  }
-})
-

# Context

由于小程序与 web 基础技术规范的差异,Mpx 中 setup 的第二个参数 context 与 Vue3 中完全不同,context 包含的属性如下:

import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  setup(props, context) {
-    // 触发事件,等价于 this.triggerEvent
-    console.log(context.triggerEvent)
-    // 获取 NodesRef/组件实例,等价于 this.$refs
-    console.log(context.refs)
-    // 字节小程序中异步获取组件实例,等价于 this.$asyncRefs
-    console.log(context.asyncRefs)
-    // 获取组件实例,等价于 this.selectComponent,可用 this.$refs 替代
-    console.log(context.selectComponent)
-    // 批量获取组件实例,等价于 this.selectAllComponents,可用 $refs 替代
-    console.log(context.selectAllComponents)
-    // 获取 SelectorQuery 对象实例查询元素布局信息,等价于 this.createSelectorQuery,可用 $refs 替代
-    console.log(context.createSelectorQuery)
-    // 获取 IntersectionObserver 对象实例查询视图相交信息,等价于 this.createIntersectionObserver
-    console.log(context.createIntersectionObserver)
-  }
-})
-

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

import { createComponent } from '@mpxjs/core'
-
-createComponent({
-  setup(props, { triggerEvent, refs }) {
-    // ...
-  }
-})
-

refs 是有状态的对象,它会随组件本身的更新而更新。这意味着你应该避免对其进行解构,并始终以 refs.x 的方式访问 NodesRef 或组件实例。与 props 不同,refs 是非响应式的。如果你打算根据 refs 的更改应用副作用,那么应该在 onUpdated 生命周期钩子中执行此操作。

# 访问组件的 property

除了 propscontext 中包含的内容,执行 setup 时将无法访问部分组件选项,包括:

  • data
  • computed

# 结合模板使用

如果 setup 返回一个对象,那么该对象的 property 可以在模板中访问到:

<template>
-  <view>{{ collectionName }}: {{ readersNumber }} {{ book.title }}</view>
-</template>
-
-<script>
-  import { createComponent, ref, reactive } from '@mpxjs/core'
-
-  createComponent({
-    properties: {
-      collectionName: String
-    },
-    setup(props) {
-      const readersNumber = ref(0)
-      const book = reactive({ title: 'Mpx' })
-
-      // 暴露给 template
-      return {
-        readersNumber,
-        book
-      }
-    }
-  })
-</script>
-

# 使用 this

setup() 内部,this 不是当前组件的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这使得 setup() 在和其它选项式 API 一起使用时可能会导致混淆。

Mpx 的组合式 API 设计极力避免了用户需要在 setup() 中访问 this 的场景,不过在一些例外情况下,用户仍然可以通过 getCurrentInstance() 的方式获取到当前组件实例,注意该函数必须在 setup() 或生命周期钩子中同步调用。

# 生命周期钩子

组合式 API 中,我们通过 on${Hookname}(fn) 的方式注册访问生命周期钩子。

Mpx 作为一个跨端小程序框架,需要兼容不同小程序平台不同的生命周期,在选项式 API 中,我们在框架中内置了一套统一的生命周期,将不同小程序平台的生命周期转换映射为内置生命周期后再进行统一的驱动,以抹平不同小程序平台生命周期钩子的差异,如微信小程序的 attached 钩子和支付宝小程序的 onInit 钩子,在组合式 API 中,我们基于框架内置的生命周期暴露了一套统一的生命周期钩子函数,下表展示了框架内置生命周期/组合式 API 生命周期函数与不同小程序平台原生生命周期的对应关系:

# 组件生命周期

框架内置生命周期 Hook inside setup 微信原生 支付宝原生
BEFORECREATE null attached(数据响应初始化前) onInit(数据响应初始化前)
CREATED null attached(数据响应初始化后) onInit(数据响应初始化后)
BEFOREMOUNT onBeforeMount ready(MOUNTED 执行前) didMount(MOUNTED 执行前)
MOUNTED onMounted ready(BEFOREMOUNT 执行后) didMount(BEFOREMOUNT 执行后)
BEFOREUPDATE onBeforeUpdate nullsetData 执行前) nullsetData 执行前)
UPDATED onUpdated nullsetData callback) nullsetData callback)
BEFOREUNMOUNT onBeforeUnmount detached(数据响应销毁前) didUnmount(数据响应销毁前)
UNMOUNTED onUnmounted detached(数据响应销毁后) didUnmount(数据响应销毁后)

同 Vue3 一样,组合式 API 中没有提供 BEFORECREATECREATED 对应的生命周期钩子函数,用户可以直接在 setup 中编写相关逻辑。

除支付宝外的小程序平台支持使用Component构建页面,在页面中使用组件生命周期钩子与在组件中完全一致,并且框架在支付宝环境也进行了抹平实现。

# 页面生命周期

框架内置生命周期 Hook inside setup 微信原生 支付宝原生
ONLOAD onLoad onLoad onLoad
ONSHOW onShow onShow onShow
ONHIDE onHide onHide onHide
ONRESIZE onResize onResize events.onResize

# 组件中访问页面生命周期

框架内置生命周期 Hook inside setup 微信原生 支付宝原生
ONSHOW onShow pageLifetimes.show null(框架抹平实现)
ONHIDE onHide pageLifetimes.hide null(框架抹平实现)
ONRESIZE onResize pageLifetimes.resize null(框架抹平实现)

下面是简单的使用示例:

import { createComponent, onMounted, onUnmounted } from '@mpxjs/core'
-
-createComponent({
-  setup () {
-    // mounted
-    onMounted(()=>{
-      console.log('Component mounted.')
-    })
-    // unmounted
-    onUnmounted(()=>{
-      console.log('Component unmounted.')
-    })
-    return {}
-  }
-})
-

# 框架内置生命周期

从上面可以看到我们在框架内部内置了一套统一的生命周期来抹平不同平台生命周期的差异,由于存在数据响应机制,这套内置生命周期与小程序原生的生命周期不完全一一对应,反而与 Vue 的生命周期更加相似,在过去的版本中,我们没有显式地暴露出 BEFORECREATE 这这类框架内置的生命周期,更多都在框架内部使用,但是在组合式 API 版本中,为了使选项式 API 的生命周期能力与之对齐,我们将框架内置的生命周期显式导出,让用户在选项式 API 开发环境下也能正常使用这些能力,简单使用示例如下:

import { createComponent, BEFORECREATE } from '@mpxjs/core'
-
-createComponent({
-  [BEFORECREATE] () {
-    console.log('beforeCreate exec.')
-  },
-  created () {
-    // 原生的 created 会被映射为框架内部的 CREATED 执行,此处逻辑在 BEFORECREATE 后执行
-    console.log('created exec.')
-  }
-})
-

# 具有副作用的页面事件

在小程序中,一些页面事件的注册存在副作用,即该页面事件注册与否会产生实质性的影响,比如微信中的 onShareAppMessageonPageScroll,前者在不注册时会禁用当前页面的分享功能,而后者在注册时会带来视图与逻辑层之间的线程通信开销,对于这部分页面事件,我们无法通过预注册 -> 驱动方式提供组合式 API 的注册方式,用户可以通过选项式 API 的方式来注册使用,通过 this 访问组合式 API setup 函数的返回。

然而这种使用方式显然不够优雅,我们考虑是否可以通过一些非常规的方式提供这类副作用页面事件的组合式 API 注册支持,例如,借助编译手段。我们在运行时提供了副作用页面事件的注册函数,并在编译时通过 babel 插件的方式解析识别到当前页面中存在这些特殊注册函数的调用时,通过框架已有的编译 -> 运行时注入的方式将事件驱动逻辑添加到当前页面当中,以提供相对优雅的副作用页面事件在组合式 API 中的注册方式,同时不产生非预期的副作用影响。

我们需要先修改 babel 配置添加 @mpxjs/babel-plugin-inject-page-events 插件:

// babel.config.json
-{
- "plugins": [
-    [
-      "@babel/transform-runtime",
-      {
-        "corejs": 3,
-        "version": "^7.10.4"
-      }
-    ],
-    "@mpxjs/babel-plugin-inject-page-events"
-  ]
-}
-

然后就能想普通生命周期一样使用组合式 API 进行页面事件注册,简单示例如下:

import { createComponent, ref, onShareAppMessage } from '@mpxjs/core'
-
-createComponent({
-  setup () {
-    const count = ref(0)
-
-    onShareAppMessage(() => {
-      return {
-        title: '页面分享'
-      }
-    })
-
-    return {
-      count
-    }
-  }
-})
-

目前我们通过这种方式支持的页面事件如下:

页面事件 Hook inside setup 平台支持
onPullDownRefresh onPullDownRefresh 全小程序平台 + web
onReachBottom onReachBottom 全小程序平台 + web
onPageScroll onPageScroll 全小程序平台 + web
onShareAppMessage onShareAppMessage 全小程序平台
onTabItemTap onTabItemTap 微信/支付宝/百度/QQ
onAddToFavorites onAddToFavorites 微信 / QQ
onShareTimeline onShareTimeline 微信
onSaveExitState onSaveExitState 微信

特别注意,由于静态编译分析实现方式的限制,这类页面事件的组合式 API 使用需要满足页面事件注册函数的调用和 createPage 的调用位于同一个 js 文件当中。

# 模板引用

在 Vue3 的组合式 API 中,我们可以在 setup 函数中使用 ref() 创建引用数据获取模板中绑定了 ref 属性的组件或 DOM 节点,优雅地将响应式引用模板引用进行了关联统一,但在 Mpx 中,受限于小程序的技术限制,我们无法在低性能损耗下实现相同的设计,因此我们在 setup 的 context 参数中提供了 refs 对象,结合模板中的wx:ref指令使用,与选项式 API 中的 $refs 保持一致。

下面是组合式 API 中进行模板引用的使用示例:

<template>
-  <view bindtap="handleHello" wx:ref="hello">hello</view>
-  <view wx:if="{{showWorld}}" wx:ref="world">world</view>
-  <view wx:for="{{list}}" wx:ref="list">{{item}}</view>
-</template>
-
-<script>
-  import { createComponent, ref, onMounted, nextTick } from '@mpxjs/core'
-
-  createComponent({
-    setup (props, { refs }) {
-      const showWorld = ref(false)
-      const list = ref(['手机', '电视', '电脑'])
-
-      onMounted(() => {
-        // 最早在 onMounted 中才能访问refs,对于节点返回 NodesRef 对象,对于组件返回组件实例
-        console.log('hello ref:', refs.hello)
-        // 在循环中定义 wx:ref,对应的 refs 返回数组
-        console.log('list ref:', refs.list)
-      })
-
-      const handleHello = () => {
-        showWorld.value = true
-        nextTick(() => {
-          // 数据变更后要在 nextTick 中访问更新后的视图数据
-          console.log('world ref:', refs.world)
-        })
-      }
-      
-      // 暴露给 template
-      return {
-        showWorld,
-        handleHello,
-        list
-      }
-    }
-  })
-</script>
-

# <script setup>

和 Vue 类似,<script setup> 是在 Mpx 单文件组件中使用组合式 API 时的编译时语法糖。不过受小程序底层技术限制,在 Mpx 中 <script setup> 无法完整提供其在 Vue 中所具备的相关优势,我们提供了这个语法能力,但不作为默认的推荐选项。

# 基本语法

启用该语法需要在 <script> 代码块上添加 setup attribute:

<script setup>
-    console.log('hello Mpx script setup')
-</script>
-

<script> 里边的代码会被编译成组件 setup() 函数的内容。

# 顶层的绑定会被暴露给模版

从 v2.8.19 开始,顶层绑定自动暴露给模板的能力被取消,构建时会强制用户通过 defineExpose 手动声明需要暴露给模板的数据或方法。

和 Vue 一样,当使用 <script setup> 时,任何在 <script setup> 声明的顶层的绑定(包括变量,函数声明,以及 import 导入的内容) 都能在模版中直接使用:

<script setup>
-    import { ref } from '@mpxjs/core'
-    const msg = ref('hello');
-    function log() {
-        console.log(msg.value)
-    }
-</script>
-<template>
-    <view>msg: {{msg}}</view>
-    <view ontap="log">click</view>
-</template>
-

import 导入的内容,除了从 @mpxjs/core 中导入的变量或方法,其他模块导入的属性和方法全部都会暴露给模版。这意味着我们可以直接在模版中使用引入的相关方法,而不需要通过 methods 选项来暴露:

<template>
-    <view ontap="clickTrigger">click</view>
-</template>
-<script setup>
-    import { clickTrigger } from './utils'
-</script>
-

注意项:如果你 script setup 中有较多对象或方法的声明和引入,比如全局 store 这种十分复杂的对象,走默认逻辑暴露给模版会造成性能问题,因此需要使用 defineExpose 来手动定义暴露给模版的数据和方法。

# 响应式

和 Vue 中一样,响应式状态需要明确使用响应性 API 来创建。和 setup() 函数的返回值一样,ref 在模版中使用的时候会自动解包:

<template>
-    <button ontap="addCount">{{count}}</button>
-</template>
-<script setup>
-    import { ref } from '@mpxjs/core'
-    const count = ref(0)
-    function addCount() {
-        count.value++
-    }
-</script>
-

# defineProps()

和 Vue 类似,为了在声明小程序组件 properties 选项时获得完整的类型推导支持,在 <script setup> 中,我们需要使用 defineProps API,它默认在 <script setup> 中可用:

<script setup>
-    const props = defineProps({
-        testA: String
-    })
-</script>
-
  • defineProps 是只能在 <script setup> 中使用的编译宏,不需要手动导入,会跟随 <script setup> 的处理过程一同被编译掉。
  • defineProps 接收与小程序 properties 选项相同的值。
  • 传入到 defineProps 的选项会从 setup 中提升到模块的作用域。因此传入的选项不能引用在 setup 作用域中声明的局部变量,否则会导致编译错误,不过可以引入导入的绑定。

# defineExpose()

<script setup> 中定义暴露给模版的变量和方法,在 Mpx <script setup> 中属于强制要求,若不使用该编译宏,则会构建报错。

Mpx 中的 defineExpose 和 Vue3 中的不尽相同,在 Vue3 中,使用 <scrip setup> 的组件默认是关闭的-即通过模版引用或者 $parent 链获取到的组件实例中不会暴露任何在<script setup> 中声明的绑定。

在 Mpx 中,<script setup> 中的声明绑定都会挂载到组件实例中,都可以通过组件实例来访问,Mpx defineExpose 更大的作用是假如你在 <scrip setup> 中引入了一些 store 实例,这些 store 实例默认会挂载到组件实例中,会导致后续的响应式处理以及组件更新速度变慢,这里我们通过强制使用 -defineExpose 来规避掉这个问题。

<script setup>
-    const count = ref(0)
-    const name = ref('black')
-    defineExpose({
-        name
-    })
-</script>
-<template>
-    <!--正确渲染 black-->
-    <view>{{name}}</view>
-    <!--找不到对应变量,无内容-->
-    <view>{{count}}</view>
-</template>
-

# defineOptions()

此编译宏相较于 Vue 是 Mpx 中独有的,主要是当开发者想在 <script setup> 中使用一些微信小程序中特有的选项式,例如 relations、moved 等,可以使用该编译宏进行定义。

<script setup>
-    defineOptions({
-        pageLifetimes: {
-            // 组件所在页面的生命周期函数
-            resize: function () { }
-        }
-    })
-</script>
-
  • defineOptions 是只能在 <script setup> 中使用的编译宏,不需要手动导入,会跟随 <script setup> 的处理过程一同被编译掉。
  • defineOptions 无返回值。
  • defineOptions 中的选项会无脑提升至组件或页面构造器的选项之中,因此不可引用 setup 中的局部变量。

# useContext()

<script setup> 中,当我们想要使用 context 时,可以使用 useContext() 来获 context 对象。

<script setup>
-    const context = useContext()
-    // 获取 NodesRef/组件实例,等价于 this.$refs
-    console.log(context.refs)
-</script>
-

# 针对 TypeScript 的功能

# 类型 props 的声明

props 可以通过给 defineProps 传递纯类型函数的方式来声明:

    const props = defineProps<{
-        foo: string,
-        bar: number
-    }>()
-
-    // 构建转换为
-    {
-        properties: {
-            foo: {
-                type: String
-            },
-            bar: {
-                type: Number
-            }
-        }
-    }
-
  • defineProps 要么使用运行时声明,要么使用类型声明。同时使用两种方式会导致编译报错。
  • 小程序中 defineProps 类型声明若有 optional 不会生效,因为小程序的 props 只要声明则一定会存在
  • 类型声明参数必须是一下内容之一,以确保正确的静态分析: -
    • 类型字面量
    • 在同一文件中的接口或者类型字面量的引用

# 使用类型声明时的默认 props 值

和 Vue3 一样,针对类型的 defineProps 声明的不足,它无法给 props 提供默认值。为了解决这个问题,我们也支持了 withDefaults 编译宏:

export interface Props {
-  msg: string
-  labels: string
-}
-
-const props = withDefaults(defineProps<Props>(), {
-  msg: 'hello',
-  labels: 'world'
-})
-

上边代码会被编译为等价的运行时 props 的 value 选项。

  • 小程序 properties 定义中的 optionalTypes 和 observer 字段,无法使用 TypeScript 类型声明的方式定义,如果需要定义这两个字段,目前需要使用运行时的方式来定义。

# 限制

由于模块执行语义的差异,<script setup> 中的代码依赖单文件组件的上下文,如果将其移动到外部的 .js 或者 .ts 的时候,对于开发者可工具来说都十分混乱。因此 <script setup> 不能和 src attribute 一起使用。

# 组合式 API 与 Vue3 中的区别

下面我们来总结一下 Mpx 中组合式 API 与 Vue3 中的区别:

  • setupcontext 参数不同,详见这里
  • setup 不支持返回渲染函数
  • setup 不能是异步函数
  • <script setup> 提供的宏方法不同,详见这里
  • <script setup> 不支持 import 快捷引入组件
  • <script setup> 必须使用 defineExpose
  • 支持的生命周期钩子不同,详见这里
  • 模板引用的方式不同,详见这里

# 组合式 API 周边生态能力的使用

我们对 Mpx 提供的周边生态能力也都进行了组合式 API 适配升级,详情如下:

  • store 在组合式 API 中使用,详见这里
  • pinia 在组合式 API 中使用,详见这里
  • fetch 在组合式 API 中使用,详见这里
  • i18n 在组合式 API 中使用,详见这里
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html b/docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html deleted file mode 100644 index 2cdf69c97a..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/composition-api/reactive-api.html +++ /dev/null @@ -1,386 +0,0 @@ - - - - - - 响应式 API | Mpx框架 - - - - - - - - - -

# 响应式 API

在 Mpx 中,为了支持组合式 API 的使用,我们参考 Vue3 提供了相关的响应式 API,但由于 proxy 目前仍然存在浏览器兼容性问题,我们在底层还是基于 Object.defineProperty 实现的数据响应,因此相较于 Vue3 提供的 API 存在一些删减,同时也存在与 Vue2 一样的数据响应使用限制 (opens new window)

# 创建响应式对象

在 Mpx 中,我们可以使用 reactive 方法将一个 JavaScript 对象深度转换为响应式对象,当对象内数据发生变化时能够被系统感知,相当于 Vue2 中的 observable,需要注意的是在 Mpx 中 reactive() 是将传入的对象进行响应性转化后返回原对象,而在 Vue3 中则会基于 proxy API 返回传入对象的响应式代理。

目前 reactive 仅支持传入基础对象类型,包括纯对象和数组,暂不支持 MapSet 这样的集合类型。

import { reactive } from '@mpxjs/core'
-
-// 响应式对象
-const state = reactive({
-  count: 0
-})
-

你可以在响应式基础 API 章节中了解更多关于 reactive 的信息。

# 使用ref()创建独立的响应式值

上面提到 reactive 只能传入对象类型数据,当我们想将一个原始数据类型的值(如数字、字符串、布尔值)变成响应式时,我们不得不先将其包装为一个对象,使用起来较为繁琐,新的 ref 方法能够让我们便捷地达成上述目标:

import { ref } from '@mpxjs/core'
-
-// 响应式值
-const count = ref(0)
-

ref() 会返回一个可变的响应式对象,该对象作为一个响应式引用通过 value 属性维护着传入的内部值:

import { ref } from '@mpxjs/core'
-
-const count = ref(0)
-console.log(count.value) // 0
-
-count.value++
-console.log(count.value) // 1
-

# Ref 解包

ref 作为渲染上下文 (从 setup() 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动浅层次解包内部值。只有访问嵌套的 ref 时需要在模板中添加 .value

<template>
-  <view>
-    <view>{{ count }}</view>
-    <view>{{ nested.count.value }}</view>
-  </view>
-</template>
-
-<script>
-  import { createComponent, ref } from '@mpxjs/core'
-   
-  createComponent({
-    setup() {
-      const count = ref(0)
-      return {
-        count,
-        nested: {
-          count
-        }
-      }
-    }
-  }
-</script>
-

# 访问响应式对象

ref 作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动解包内部值:

const count = ref(0)
-const state = reactive({
-  count
-})
-
-console.log(state.count) // 0
-
-state.count = 1
-console.log(count.value) // 1
-

如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:

const otherCount = ref(2)
-
-state.count = otherCount
-console.log(state.count) // 2
-console.log(count.value) // 1
-

Ref 解包仅发生在被响应式 Object 嵌套的时候。当从 Array 访问 ref 时,不会进行解包:

const arr = reactive([ref('Hello world')])
-// 这里需要 .value
-console.log(arr[0].value)
-

# 响应式对象解构

当我们想使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构来获取我们想要的 property:

import { reactive } from '@mpxjs/core'
-
-const people = reactive({
-  name: 'hiyuki',
-  age: 26,
-  gender: 'male',
-  city: 'Beijing'
-})
-
-let { name, age } = people
-

遗憾的是,使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:

import { reactive, toRefs } from '@mpxjs/core'
-
-const people = reactive({
-  name: 'hiyuki',
-  age: 26,
-  gender: 'male',
-  city: 'Beijing'
-})
-
-let { name, age } = toRefs(people)
-age.value = 30 // age 现在是个 ref,我们需要使用 .value 进行访问,对其进行修改也将直接作用在原响应式对象中
-console.log(people.age) // 30
-

你可以在Refs API 章节中了解更多关于 refs 的信息。

# 计算值

有时我们需要依赖于其他状态的状态——在 选项式 API 中,这是用组件计算属性处理的,在新的组合式 API 中,我们可以使用 computed 函数直接创建计算值:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象。

import { ref, computed } from '@mpxjs/core'
-
-const count = ref(1)
-const plusOne = computed(() => count.value + 1)
-
-console.log(plusOne.value) // 2
-
-plusOne.value++ // error
-

或者,可以使用一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

import { ref, computed } from '@mpxjs/core'
-
-const count = ref(1)
-const plusOne = computed({
-  get: () => count.value + 1,
-  set: val => {
-    count.value = val - 1
-  }
-})
-
-plusOne.value = 1
-console.log(count.value) // 0
-

# watchEffect

为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

const count = ref(0)
-
-watchEffect(() => console.log(count.value))
-// -> logs 0
-
-setTimeout(() => {
-  count.value++
-  // -> logs 1
-}, 100)
-

# 停止侦听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听:

const stop = watchEffect(() => {
-  /* ... */
-})
-
-// later
-stop()
-

# 清除副作用

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
-  const token = performAsyncOperation(id.value)
-  onInvalidate(() => {
-    // id has changed or watcher is stopped.
-    // invalidate previously pending async operation
-    token.cancel()
-  })
-})
-

之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。

在执行数据请求时,副作用函数往往是一个异步函数:

const data = ref(null)
-watchEffect(async onInvalidate => {
-  onInvalidate(() => {
-    /* ... */
-  }) // 在Promise解析之前注册清除函数
-  data.value = await fetchData(props.id)
-})
-

我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。

# 副作用刷新时机

默认情况下,数据发生变更时,关联的副作用会被推入异步队列中,进行异步刷新,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 render 函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 render 前执行:

<template>
-  <view>{{ count }}</view>
-</template>
-
-<script>
-import { createComponent, ref, watchEffect } from '@mpxjs/core'
-
-createComponent({
-  setup() {
-    const count = ref(0)
-
-    watchEffect(() => {
-      console.log(count.value)
-    })
-
-    return {
-      count
-    }
-  }
-})
-</script>
-

在这个例子中:

  • count 会在初始运行时同步打印出来
  • 更改 count 时,将在组件更新前执行副作用。

如果需要在组件更新(例如:当与模板引用一起)后重新运行侦听器副作用,我们可以传递带有 flush 选项的附加 options 对象 (默认为 'pre'):

// 在组件更新后触发,这样你就可以访问更新的 DOM。
-// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
-watchEffect(
-  () => {
-    /* 访问视图 */
-  },
-  {
-    flush: 'post'
-  }
-)
-

flush 选项还接受 sync,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。

与此同时,我们还提供了 watchPostEffectwatchSyncEffect 别名用来让代码意图更加明显。

# watch

watch API 相当于选项式 API 中的 watch propertywatch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。

  • watchEffect 比较,watch 允许我们: -
    • 懒执行副作用;
    • 更具体地说明什么状态应该触发侦听器重新运行;
    • 访问侦听状态变化前后的值。

# 侦听单个数据源

侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref

// 侦听一个 getter
-const state = reactive({ count: 0 })
-watch(
-  () => state.count,
-  (count, prevCount) => {
-    /* ... */
-  }
-)
-
-// 直接侦听ref
-const count = ref(0)
-watch(count, (count, prevCount) => {
-  /* ... */
-})
-

# 侦听多个数据源

侦听器还可以使用数组同时侦听多个源:

const firstName = ref('')
-const lastName = ref('')
-
-watch([firstName, lastName], (newValues, prevValues) => {
-  console.log(newValues, prevValues)
-})
-
-firstName.value = 'John' // logs: ["John", ""] ["", ""]
-lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
-

尽管如此,如果你在同一个函数里同时改变这些被侦听的来源,侦听器仍只会执行一次:

createComponent({
-  setup() {
-    const firstName = ref('')
-    const lastName = ref('')
-  
-    watch([firstName, lastName], (newValues, prevValues) => {
-      console.log(newValues, prevValues)
-    })
-  
-    const changeValues = () => {
-      firstName.value = 'John'
-      lastName.value = 'Smith'
-      // 打印 ["John", "Smith"] ["", ""]
-    }
-  
-    return { changeValues }
-  }
-})
-

注意多个同步更改只会触发一次侦听器。

通过更改设置 flush: 'sync',我们可以为每个更改都强制触发侦听器,尽管这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行。例如:

const changeValues = async () => {
-  firstName.value = 'John' // 打印 ["John", ""] ["", ""]
-  await nextTick()
-  lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
-}
-

# 侦听响应式对象

为了侦听深度嵌套的对象或数组中 property 变化,我们需要将 deep 选项设置为 true:

const state = reactive({ 
-  id: 1,
-  attributes: { 
-    name: '',
-  }
-})
-
-watch(
-  () => state,
-  (state, prevState) => {
-    console.log('not deep', state.attributes.name, prevState.attributes.name)
-  }
-)
-
-watch(
-  () => state,
-  (state, prevState) => {
-    console.log('deep', state.attributes.name, prevState.attributes.name)
-  },
-  { deep: true }
-)
-
-state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"
-

然而,侦听一个响应式对象或数组将始终返回该对象的引用。为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝。这可以通过诸如 lodash.cloneDeep 这样的实用工具来实现:

import _ from 'lodash'
-
-const state = reactive({
-  id: 1,
-  attributes: {
-    name: '',
-  }
-})
-
-watch(
-  () => _.cloneDeep(state),
-  (state, prevState) => {
-    console.log(state.attributes.name, prevState.attributes.name)
-  }
-)
-
-state.attributes.name = 'Alex' // 日志: "Alex" ""
-

我们也可以直接给 watch() 传入一个响应式对象,这种情况下会隐式地强制开启 deep 选项,确保嵌套的深层变更能够被监听到:

const state = reactive({ 
-  id: 1,
-  attributes: { 
-    name: '',
-  }
-})
-
-watch(
-  state,
-  (state, prevState) => {
-    console.log('implicit deep', state.attributes.name, prevState.attributes.name)
-  }
-)
-
-state.attributes.name = 'Alex' // 日志: "implicit deep" "Alex" "Alex"
-

需要注意的是,在侦听使用 reactive() 创建的响应式对象时,受数据响应限制影响,在改变数组或使用 set() 新增对象属性时,存在和 Vue3 中表现不一致的情况,详情查看这里

# 立即回调的侦听器

同选项式 watch 一致,我们也可以通过传递 immediate 选项让侦听回调立即执行:

const state = reactive({ 
-  count: 0
-})
-
-watch(
-  () => state.count,
-  (value, oldValue) => {
-    console.log('immediate', value, oldValue) // 日志: "immediate" 0 undefined
-  },
-  { immediate: true }
-)
-

# 与 watchEffect 共享的行为

watchwatchEffect 共享停止侦听清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入)、副作用刷新时机行为。

# 响应式 API 与 Vue3 中的区别

下面我们来总结一下 Mpx 中响应式 API 与 Vue3 中的区别:

  • 不支持 raw 相关 API(markRaw 除外,我们提供了该 API 用于跳过部分数据的响应式转换)
  • 不支持 readonly 相关 API
  • 不支持 watchEffectwatchcomputed 的调试选项
  • 不支持对 mapset 等集合类型进行响应式转换
  • 受到 Object.defineProperty 实现带来的数据响应限制影响

# 数据响应限制带来的差异

同 Vue2 一致,Mpx 无法感知到对象 property 的添加或移除,我们暴露了 setdel API 来让用户显式地进行相关操作:

import { ref, watchSyncEffect, set, del } from '@mpxjs/core'
-
-const state = ref({
-  count: 0
-})
-
-watchSyncEffect(() => {
-  console.log(JSON.stringify(state.value)) // {"count":0}
-})
-
-set(state.value, 'hello', 'world') // {"count":0,"hello":"world"}
-
-del(state.value, 'count') // {"hello":"world"}
-

同样,我们需要使用 set 或数组原型方法对数组进行修改:

import { ref, watchSyncEffect, set } from '@mpxjs/core'
-
-const state = ref([0, 1, 2, 3])
-
-watchSyncEffect(() => {
-  console.log(JSON.stringify(state.value)) // [0,1,2,3]
-})
-
-set(state.value, 1, 3) // [0,3,2,3]
-
-state.value.push(4) // [0,3,2,3,4]
-

可能你已经注意到,上面两个示例当中我们都使用了 ref() 进行响应式数据创建,这是有原因的,在新的响应式 API 模式下,我们使用 reactive() 创建的响应式数据在上述情况下仍然无法绕过 Vue2 设计中的数据响应限制,即使你使用了 set 或数组原型方法:

import { reactive, watchSyncEffect, set } from '@mpxjs/core'
-
-const state = reactive([0, 1, 2, 3])
-
-watchSyncEffect(() => {
-  console.log(JSON.stringify(state)) // [0,1,2,3]
-})
-
-set(state, 1, 3) // 不会触发 watchEffect
-
-state.push(4) // 不会触发 watchEffect
-

对于对象和在模板中使用也是同理:

<template>
-  <view bindtap="addCount2">
-    <view>{{state.count}}</view>
-    <view>{{state.count2}}</view>
-  </view>
-
-</template>
-
-<script>
-  import { createComponent, reactive, set } from '@mpxjs/core'
-  
-  createComponent({
-    setup () {
-      const state = reactive({
-        count: 0
-      })
-
-      const addCount2 = () => {
-        set(state, 'count2', 10) // 不会触发视图更新
-      }
-
-      return {
-        state,
-        addCount2
-      }
-    }
-  })
-</script>
-

为什么会产生这个现象呢?原因在于:基于 Object.defineProperty 实现的数据响应系统中,我们会对对象的每个已有属性创建了一个 Dep 对象,在对该属性进行 get 访问时通过这个对象将其与依赖它的观察者 ReactiveEffect 关联起来,并在 set 操作时触发关联 ReactiveEffect 的更新,这是我们大家都知道的数据响应的基本原理。但是对于新增/删除对象属性和修改数组的场景,我们无法事先定义当前不存在属性的 get/set (当然这在 proxy 当中是可行的),因此我们会把对象或者数组本身作为一个数据依赖创建 Dep 对象,通过父级访问该数据时定义的 get/set 将其关联到对应的 ReactiveEffect,并在对数据进行新增/删除属性或数组操作时通过数据本身持有的 Dep 对象触发关联 ReactiveEffect 的更新,如下图所示:

数据响应原理

需要注意的是,通过父级访问是建立 DepReactiveEffect 关联关系的先决条件,在选项式 API 中,我们访问组件的响应式数据都需要通过 this 进行访问,相当于这些数据都存在 this 这个必要的父级,因此我们在使用 $set/$delete 进行对对象进行新增/删除属性或对数组进行修改时都能得到符合预期的结果,唯一的限制在于不能新增/删除根级数据属性,原因就在于 this 不存在访问它的父级。

但是在组合式 API 中,我们不需要通过 this 访问响应式数据,因此通过 reactive() 创建的响应式数据本身就是根级数据,我们自然无法通过上述方式感知到根级数据自身的变化(在 Vue3 中,基于 proxy 提供的强大能力响应式系统能够精确地感知到数据属性,甚至是当前不存在属性的访问与修改,不需要为数据自身建立 Dep 对象,自然也不存在相关问题)。

在这种情况下,我们就需要用 ref() 创建响应式数据,因为 ref 创建了一个包装对象,我们永远需要通过 .value 来访问其持有的数据(不管是显式访问还是隐式自动解包),这样就能保证 ref 数据自身的变化能够被响应式系统感知,因此也不会遇到上面描述的问题。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html b/docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html deleted file mode 100644 index 3dc598d21e..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/extend/api-proxy.html +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - Api代理 | Mpx框架 - - - - - - - - - -

# Api代理

convert API at each end 各个平台之间 api 进行转换,目前已支持微信转支付宝、微信转web

# Usage

// 使用 mpx 生态
-
-import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-
-mpx.use(apiProxy, options)
-
// 脱离 Mpx 单独使用
-import { getProxy } from '@mpxjs/api-proxy'
-// proxy 即为target 实例
-const proxy = getProxy(options)
-
-proxy.navigateTo({
-  url: '/pages/test',
-  success (res) {
-    console.log(res)
-  }
-})
-

# Options

参数名称 类型 含义 是否必填 默认值 备注
platform Object 各平台之间的转换 { from:'', to:'' } 使用 mpx 脚手架配置会自动进行转换,无需配置
exclude Array(String) 跨平台时不需要转换的 api -
usePromise Boolean 是否将 api 转化为 promise 格式使用 false -
whiteList Array(String) 强行转化为 promise 格式的 api [] 需要 usePromise 设为 true
blackList Array(String) 强制不变成 promise 格式的 api [] -
fallbackMap Object 对于不支持的API,允许配置一个映射表,接管不存在的API {} -

# example

# 普通形式

import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-
-mpx.use(apiProxy, {
-  exclude: ['showToast'] // showToast 将不会被转换为目标平台
-})
-
-mpx.showModal({
-  title: '标题',
-  content: '这是一个弹窗',
-  success (res) {
-    if (res.cancel) {
-      console.log('用户点击取消')
-    }
-  }
-})
-

# 使用promise形式

开启usePromise时所有的异步api将返回promise,但是小程序中存在一些异步api本身的返回值是具有意义的,如uploadFile会返回一个uploadTask对象用于后续监听上传进度或者取消上传,在对api进行promise化之后我们会将原始的返回值挂载到promise.__returned属性上,便于开发者访问使用

import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-
-mpx.use(apiProxy, {
-  usePromise: true
-})
-
-mpx.showActionSheet({
-  itemList: ['A', 'B', 'C']
-})
-.then(res => {
-  console.log(res.tapIndex)
-})
-.catch(err => {
-  console.log(err)
-})
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/extend/fetch.html b/docs-vuepress/.vuepress/dist/guide/extend/fetch.html deleted file mode 100644 index 7d412bec01..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/extend/fetch.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - 网络请求 | Mpx框架 - - - - - - - - - -

# 网络请求

Mpx 提供了网络请求库 fetch,抹平了微信,阿里等平台请求参数及响应数据的差异;同时支持请求拦截器,请求取消等

# 使用说明

import mpx from '@mpxjs/core'
-import mpxFetch from '@mpxjs/fetch'
-mpx.use(mpxFetch)
-// 第一种访问形式
-mpx.xfetch.fetch({
-	url: 'http://xxx.com'
-}).then(res => {
-	console.log(res.data)
-})
-
-mpx.createApp({
-	onLaunch() {
-		// 第二种访问形式
-		this.$xfetch.fetch({url: 'http://test.com'})
-	}
-})
-

# 导出说明

mpx-fetch提供了一个实例 xfetch ,该实例包含以下api

  • fetch(config), 正常的promisify风格的请求方法
  • CancelToken,实例属性,用于创建一个取消请求的凭证。
  • interceptors,实例属性,用于添加拦截器,包含两个属性,request & response

# 请求拦截器

mpx.xfetch.interceptors.request.use(function(config) {
-	console.log(config)
-	// 也可以返回promise
-	return config
-})
-mpx.xfetch.interceptors.response.use(function(res) {
-	console.log(res)
-	// 也可以返回promise
-	return res
-})
-

# 请求中断

const cancelToken = new mpx.xfetch.CancelToken()
-mpx.xfetch.fetch({
-	url: 'http://xxx.com',
-	data: {
-		name: 'test'
-	},
-	cancelToken: cancelToken.token
-})
-cancelToken.exec('手动取消请求') // 执行后请求中断,返回abort fail
-

# 设置请求参数

mpx.xfetch.fetch({
-	url: 'http://xxx.com',
-	params: {
-		name: 'test'
-	}
-})
-
-mpx.xfetch.fetch({
-	url: 'http://xxx.com',
-	method: 'POST',
-	// params 参数等价于 url query
-	params: {
-		age: 10
-	},
-	data: {
-		name: 'test'
-	},
-	// 设置参数序列化方式,等价于header = {'content-type': 'application/x-www-form-urlencoded'}
-	emulateJSON: true
-})
-

# 设置请求 timeout

mpx.xfetch.fetch({
-	url: 'http://xxx.com',
-	method: 'POST',
-	data: {
-		name: 'test'
-	},
-	timeout: 10000 // 超时时间
-})
-

# 在组合式 API 中使用

在组合式 API 中我们提供了 useFetch 方法来访问 xfetch 实例对象

// app.mpx
-import mpx, { createComponent } from '@mpxjs/core'
-import { useFetch } from '@mpxjs/fetch'
-
-createComponent({
-  setup() {
-      useFetch().fetch({
-          url: 'http://xxx.com',
-          method: 'POST',
-          params: {
-              age: 10
-          },
-          data: {
-              name: 'test'
-          },
-          emulateJSON: true,
-          usePre: true,
-          cacheInvalidationTime: 3000,
-          ignorePreParamKeys: ['timestamp']
-      }).then(res => {
-          console.log(res.data)
-      })   
-  }
-})
-
-
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/extend/index.html b/docs-vuepress/.vuepress/dist/guide/extend/index.html deleted file mode 100644 index ce8aee8acf..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/extend/index.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - 扩展mpx | Mpx框架 - - - - - - - - - -

# 扩展mpx

# 开发插件

mpx支持使用mpx.use使用插件来进行扩展。插件本身需要提供一个install方法或本身是一个function,该函数接收一个proxyMPX。插件将采用直接在proxyMPX挂载新api属性或在prototype上挂属性。需要注意的是,一定要在app创建之前进行mpx.use。

简单示例如下:

export default function install(proxyMPX) {
-  proxyMPX.newApi = () => console.log('is new api')
-  proxyMPX
-    .mixin({
-      onLaunch() {
-        console.log('app onLaunch')
-      }
-    }, 'app')
-    .mixin({
-      onShow() {
-        console.log('page onShow')
-      }
-    }, 'page') // proxyMPX.injectMixins === proxyMPX.mixin
-
-    //  注意:proxyMPX.prototype上挂载的属性都将挂载到组件实例(page实例、app实例上,可以直接通过this访问), 可以看mixin中的case
-    proxyMPX.prototype.testHello = function() {
-      console.log('hello')
-    }
-}
-

# 目前已有插件

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/extend/mock.html b/docs-vuepress/.vuepress/dist/guide/extend/mock.html deleted file mode 100644 index 322e2bd0c4..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/extend/mock.html +++ /dev/null @@ -1,177 +0,0 @@ - - - - - - 数据 Mock | Mpx框架 - - - - - - - - - -

# 数据 Mock

# 安装

Mpx 提供了对请求响应数据进行拦截的 mock 插件,可通过如下命令进行安装:

npm i @mpxjs/mock
-

# 使用说明

新建 mock 文件目录及文件(例如:src/mock/index.js ):

// src/mock/index.js
-import mock from "@mpxjs/mock";
-mock([
-  {
-    url: "http://api.example.com",
-    rule: {
-      "list|1-10": [
-        {
-          "id|+1": 1
-        }
-      ]
-    }
-  }
-]);
-

在入口文件( app.mpx )中引入:

<script type="text/javascript">
-  import "mock/index"; // 引入mock即可
-</script>
-<!-- 其他配置 -->
-<script type="application/json">
-  {
-    "pages": ["./pages/index"],
-    "window": {
-      "backgroundTextStyle": "light",
-      "navigationBarBackgroundColor": "#fff",
-      "navigationBarTitleText": "WeChat",
-      "navigationBarTextStyle": "black"
-    }
-  }
-</script>
-

由于 mock 为全局自动代理,执行@mpxjs/mock所暴露的方法之后会立即拦截小程序的原生请求,如果需要根据不同环境变量等去控制是否使用 mock 数据,可以参考如下方法:

// src/mock/index.js
-import mock from "@mpxjs/mock";
-export default () => mock([
-  {
-    url: "http://api.example.com",
-    rule: {
-      "list|1-10": [
-        {
-          "id|+1": 1
-        }
-      ]
-    }
-  }
-]);
-
<!-- app.mpx -->
-<script type="text/javascript">
-  import mockSetup from "mock/index";
-  // 当为开发环境时才启用mock
-  process.env.NODE_ENV === "development" && mockSetup();
-</script>
-

# Mock 入参

@mpxjs/mock 所暴露的函数仅接收一个类型为 mockRequstList 的参数,该类型定义如下:

type mockItem = {
-  url: string,
-  rule: object
-}
-type mockRequstList = Array<mockItem>
-
-//示例:
-let mockList: mockRequstList = [
-  {
-    url: "http://api.example.com", // 请求触发后匹配到该链接时其响应数据会被mock拦截
-    rule: { // mock生成返回数据的规则
-      'number|1-10': 1
-    }
-  }
-]
-

# Mock 规则示例

  • 基本类型数据生成
import mock from "@mpxjs/mock";
-mock([
-  {
-    url: "http://api.example.com",
-    rule: {
-      "number|1-10": 1, // 随机生成1-10中的任意整数
-      "string|6": /[0-9a-f]/, // 值支持正则表达式,随机生成6位的16进制值
-      "boolean|1": true // 随机生成一个布尔值,值为 true 的概率是 1/2
-    }
-  }
-]);
-// 请求 http://api.example.com 后返回值为:
-// {
-//   number: 2,
-//   string: "e1e6dc",
-//   boolean: false
-// }
-
  • 生成随机长度id自增的列表
查看示例
import mock from "@mpxjs/mock";
-mock([
-  {
-    url: "http://api.example.com",
-    rule: {
-      "list|2-5": [ // 生成长度范围在2-5的数组
-        {
-          "id|+1": 0 // id每次自增1
-        }
-      ]
-    }
-  }
-]);
-// 请求 http://api.example.com 后返回
-// {
-//   "list": [{
-//     "id": 0
-//   },{
-//     "id": 1
-//   },{
-//     "id": 2
-//   }]
-// }
-
  • pick对象中的随机个值
查看示例
import mock from "@mpxjs/mock";
-mock([
-  {
-    url: "http://api.example.com",
-    rule: {
-      "object|2": { // 随机选取object中的两条数据作为返回
-        "310000": "上海市",
-        "320000": "江苏省",
-        "330000": "浙江省",
-        "340000": "安徽省"
-      }
-    }
-  }
-]);
-// 请求 http://api.example.com 后返回
-// {
-//   "object": {
-//     "330000": "浙江省",
-//     "340000": "安徽省"
-//   }
-// }
-

更多生成规则可查阅 Mock官方文档-Syntax Specification (opens new window)

更多示例可查看 Mock示例 (opens new window)

WARNING

由于小程序环境的局限性,mockjs 依赖 eval 函数实现的相关能力(如:占位符)无法正确运行

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/2.7.html b/docs-vuepress/.vuepress/dist/guide/migrate/2.7.html deleted file mode 100644 index c374438e70..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/migrate/2.7.html +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - 从旧版本迁移至 2.7 | Mpx框架 - - - - - - - - - -

# 从旧版本迁移至 2.7

自 2.7 版本开始,Mpx 将编译构建流程基于 Webpack5 进行了全面的重构,优化解决了老的编译流程中存在的大量历史遗留问题,安全完整地支持了基于文件系统的持久化缓存,大幅提升了 Mpx 想的编译构建速度,以滴滴出行小程序为例,无缓存场景下相较 2.6 版本提速约 180%,有缓存场景下提速约 10 倍。

与此同时,Mpx 2.7 版本还带来了 rules 复用,完善的单元测试支持,独立分包构建,分包异步化等众多新特性。直接通过 @mpxjs/cli 创建的新项目就能够直接使用这些新特性,如果你对于编译构建没有太多定制化诉求,我们也推荐使用 @mpxjs/cli 新建项目,并将老项目中的 src 目录 copy 覆盖至新项目的方式进行迁移,这是一种成本最低的迁移方式。

如果你的老项目中已经对编译构建进行了高度的定制,本文将提供详细的迁移工作指引。

# 依赖升级

将下面列出的相关依赖升级至对应版本,如果有其他自行引入的 loader 也确保将其升级至 webpack5 兼容的版本:

{
-  "dependencies": {
-    "@mpxjs/api-proxy": "^2.7.1",
-    "@mpxjs/core": "^2.7.1",
-    "@mpxjs/fetch": "^2.7.1",
-  },
-  "devDependencies": {
-    "@mpxjs/webpack-plugin": "^2.7.1",
-    "webpack": "^5.64.4",
-    "webpack-merge": "^5.8.0",
-    "terser-webpack-plugin": "^5.2.5",
-    "copy-webpack-plugin": "^9.0.1",
-    "webpack-bundle-analyzer": "^4.5.0",
-    "ts-loader": "^9.2.6",
-    // eslint相关配置,按需安装
-    "eslint": "^7.32.0",
-    "eslint-config-standard": "^16.0.3",
-    "eslint-plugin-html": "^6.2.0",
-    "eslint-plugin-import": "^2.25.2",
-    "eslint-plugin-node": "^11.1.0",
-    "eslint-plugin-promise": "^5.1.1",
-    "eslint-webpack-plugin": "^3.1.0",
-  }
-}
-

# 开启持久化缓存

在 webpack 配置中添加以下配置项:

module.exports = {
-  cache: {
-    type: 'filesystem',
-    // 声明构建配置,注意如果声明某个文件夹为构建配置,需要在文件夹下放置空的package.json文件,避免构建依赖收集时将主项目的依赖项视为构建依赖
-    buildDependencies: {
-      build: [resolve('build/')],
-      config: [resolve('config/')]
-    },
-    cacheDirectory: resolve('.cache/')
-  },
-  snapshot: {
-    // 如果希望修改node_modules下的文件时对应的缓存可以失效,可以将此处的配置改为 managedPaths: []
-    managedPaths: [resolve('node_modules/')]
-  },
-}
-

# 修改rules

在 2.7 版本中,我们优化了 .mpx 文件中各个 block 应用 loader 的规则。

在之前的版本中,我们基本上使用内置的规则对 .mpx 中的 block 应用 loaders,如:对 <script> 应用 babel-loader,对 <style lang="stylus"> 应用 stylus-loader。这种方式的弊端主要在于用户无法便捷地自定义 block 的 loaders 应用规则,比如传入特定的 loaders 配置。

在 2.7 版本中,我们支持了构建时读取用户在 module.rules 中配置的规则来对 block 进行 loaders 应用, 例如:对于 <style lang="stylus"> 我们会对其应用用户在 rules 中对于 .stylus 文件 配置的 loaders,当用户没有在 block 中声明 lang 属性时,我们也会兜底使用不同区块默认的 lang 来进行 rules 匹配。

因此,相较于过去的版本,我们需要在 rules 中添加一些规则使得 .mpx 文件中的各类 block 都能得到正确的处理。

module.exports = {
-  module: {
-    rules: [
-      // 新版本中rules规则对.mpx文件中的区块同样生效,在旧版本中.mpx文件中script会走内置的babel转义,但是新版当中只有在include范围内的.mpx文件才会走babel,但由于新版本中.mpx文件中的script会采用.mpx.js的格式来匹配rules,因此我们可以用如下include条件让其保持与旧版本一致的表现。
-      {
-        test: /\.js$/,
-        loader: 'babel-loader',
-        include: [/\.mpx\.js/, resolve('src'), resolve('test'), resolve('node_modules/@mpxjs')]
-      },
-      // 如使用ts编码则需要添加本条rule
-      {
-        test: /\.ts$/,
-        use: [
-          'babel-loader',
-          {
-            loader: 'ts-loader',
-            options: {
-              appendTsSuffixTo: [/\.(mpx|vue)$/]
-            }
-          }
-        ]
-      },
-      // 处理json区块
-      {
-        test: /\.json$/,
-        resourceQuery: /asScript/,
-        type: 'javascript/auto'
-      },
-      // 处理wxs
-      {
-        test: /\.(wxs|qs|sjs|qjs|jds|dds|filter\.js)$/,
-        use: [
-          MpxWebpackPlugin.wxsPreLoader()
-        ],
-        enforce: 'pre'
-      },
-      // 处理图像资源
-      {
-        test: /\.(png|jpe?g|gif|svg)$/,
-        use: [
-          MpxWebpackPlugin.urlLoader({
-            name: 'img/[name][hash].[ext]'
-          })
-        ]
-      },
-      // 下面的rules输出小程序时需配置,如输出web需要修改为vue的相关rules,具体逻辑可以参考脚手架项目中build/getRules.js
-      // mpx主loader
-      {
-        test: /\.mpx$/,
-        use: MpxWebpackPlugin.loader(currentMpxLoaderConf)
-      },
-      // 如使用sass/less等其他预编译语言,自行修改test规则并用sass/less-loader替换stylus-loader
-      {
-        test: /\.styl(us)?$/,
-        use: [
-          MpxWebpackPlugin.wxssLoader(),
-          'stylus-loader'
-        ]
-      },
-      // 处理未使用预编译语言的style区块和独立样式文件
-      {
-        test: /\.(wxss|acss|css|qss|ttss|jxss|ddss)$/,
-        use: MpxWebpackPlugin.wxssLoader()
-      },
-      // 处理未使用预编译语言的template区块和独立模板文件
-      {
-        test: /\.(wxml|axml|swan|qml|ttml|qxml|jxml|ddml)$/,
-        use: MpxWebpackPlugin.wxmlLoader()
-      }
-    ]
-  }
-}
-

# entry配置修改

如果进行常规的小程序输出,无需关注。如进行插件输出或独立输出组件/页面等特殊场景,mpx 提供了辅助方法来生成对应的 entry request,示例如下:

module.exports = {
-  entry:{
-    // 输出plugin
-    plugin: MpxWebpackPlugin.getPluginEntry('./plugin.json'),
-    // 独立输出组件,相当于./list.mpx?isComponent,不过为了保障后续该配置不受框架内部query变动影响,建议使用辅助方法
-    list: MpxWebpackPlugin.getComponentEntry('./list.mpx'),
-    // 独立输出页面,相当于./index.mpx?isPage
-    index: MpxWebpackPlugin.getPageEntry('./index.mpx')
-  }
-}
-

# MpxWebpackPlugin配置变更

  • 移除了forceDisableInject配置
  • nativeOptions更名为nativeConfig

上述的内容中只列举了一些和 mpx 高度相关的配置项变更,如果你的项目使用了特定的插件或者loader,请确保将其升级至兼容 webpack5 的版本。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/2.8.html b/docs-vuepress/.vuepress/dist/guide/migrate/2.8.html deleted file mode 100644 index b88fd302bf..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/migrate/2.8.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - 从 2.7 升级至 2.8 | Mpx框架 - - - - - - - - - -

# 从 2.7 升级至 2.8

不同于 2.7 版本对于编译构建进行的大幅度变动升级,Mpx@2.8 版本升级的核心在于运行时支持组合式 API 能力,并尽可能追求向前兼容,以降低用户的升级成本。我们默认脚手架生成的全新项目已经适配了 2.8 版本的全部变更,如果你需要将旧项目从 2.7 升级至 2.8 版本,下面是详细的升级指引。

# 依赖升级

请按照下方列表升级或新增相关依赖:

{
-  "dependencies": {
-    "@mpxjs/api-proxy": "^2.8.0",
-    "@mpxjs/core": "^2.8.0",
-    "@mpxjs/store": "^2.8.0",
-    "@mpxjs/pinia": "^2.8.0",
-    "@mpxjs/utils": "^2.8.0",
-    "@mpxjs/fetch": "^2.8.0",
-    // 这部分依赖为输出 web 专用,如项目无需输出 web 可以省略
-    "vue": "^2.7.10",
-    "vue-demi": "^0.13.11",
-    "vue-i18n": "^8.27.2",
-    "vue-i18n-bridge": "^9.2.2"
-  },
-  "devDependencies": {
-    "@mpxjs/webpack-plugin": "^2.8.0",
-    "@mpxjs/size-report": "^2.8.0",
-    "@mpxjs/babel-plugin-inject-page-events": "^2.8.0",
-    // ...
-  }
-}
-

# 编译配置变更

在编译配置方面,用户几乎不用进行任何改变,唯一的例外是当用户想要通过组合式 API 的方式注册具有副作用的页面事件时,需要在 babel 配置中添加 @mpxjs/babel-plugin-inject-page-events 插件,如下所示:

// babel.config.json
-{
- "plugins": [
-    [
-      "@babel/transform-runtime",
-      {
-        "corejs": 3,
-        "version": "^7.10.4"
-      }
-    ],
-    "@mpxjs/babel-plugin-inject-page-events"
-  ]
-}
-

关于副作用页面事件的更多详情查看这里

# 运行时破坏性变化

在 2.8 开发过程中,我们修正了过去版本中存在的不合理的设计与实现,在运行时带来了少许破坏性改变,详情如下:

  • 框架过往提供的组件增强生命周期 pageShow/pageHide 与微信原生提供的 pageLifetimes.show/hide 完全对齐,不再提供组件初始挂载时必定执行 pageShow 的保障(因为组件可能在后台页面进行挂载),相关初始化逻辑一定不要放置在 pageShow 当中;
  • 取消了框架过去提供的基于内部生命周期实现的非标准增强生命周期,如 beforeCreate/onBeforeCreate 等,直接将内部生命周期变量导出提供给用户使用,详情查看这里
  • 为了优化 tree shaking,作为框架运行时 default exportMpx 对象不再挂载 createComponent/createStore 等运行时方法,一律通过 named export 提供,Mpx 对象上仅保留 set/use 等全局 API;
  • 使用 I18n 能力时,为了与新版 vue-i18n 保持对齐,this.$i18n 对象指向全局作用域,如需创建局部作用域需要使用组合式 API useI18n 的方式进行创建。
  • watch API 不再接受第二个参数为带有 handler 属性的对象形式(该参数形式只应存在于 watch option 中),第二个参数必须为回调函数,与 Vue (opens new window) 对齐。
- - - diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/2.9.html b/docs-vuepress/.vuepress/dist/guide/migrate/2.9.html deleted file mode 100644 index e11caac560..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/migrate/2.9.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - 从 2.8 升级至 2.9 | Mpx框架 - - - - - - - - - -

# 从 2.8 升级至 2.9

Mpx 2.9 版本不包含破坏性变更,主要新增了原子类支持、输出 web 支持 SSR 和构建产物体积优化三项核心特性,更详细的介绍查看这里

# 依赖升级

如果你从既有项目中进行升级,仅需将 @mpxjs 下的依赖升级至 2.9 即可

{
-  "dependencies": {
-    "@mpxjs/api-proxy": "^2.9.0",
-    "@mpxjs/core": "^2.9.0",
-    "@mpxjs/store": "^2.9.0",
-    "@mpxjs/pinia": "^2.9.0",
-    "@mpxjs/utils": "^2.9.0",
-    "@mpxjs/fetch": "^2.9.0",
-    // ...
-  },
-  "devDependencies": {
-    "@mpxjs/webpack-plugin": "^2.9.0",
-    "@mpxjs/size-report": "^2.9.0",
-    "@mpxjs/babel-plugin-inject-page-events": "^2.9.0",
-    // 如需使用原子类,加入以下依赖
-    "@mpxjs/unocss-plugin": "^2.9.0",
-    "@mpxjs/unocss-base": "^2.9.0",
-    // 如需使用SSR,加入以下依赖
-    "axios": "^1.6.0",
-    "express": "^4.18.2",
-    "serve-favicon": "^2.5.0",
-    "vue-server-renderer": "^2.7.14",
-    // ...
-  }
-}
-

# 使用原子类

详情查看这里

# 使用SSR

详情查看这里

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html b/docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html deleted file mode 100644 index efbd718296..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/migrate/mpx-cli-3.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - mpx-cli v2 迁移到 v3 | Mpx框架 - - - - - - - - - -

# mpx-cli v2 迁移到 v3

# 升级@mpxjs/cli

npm install @mpxjs/cli@3.x -g
-

# 配置迁移

v3 兼容了 v2 的所有配置,如果没有特殊修改,则不需要进行配置迁移。

  • config/devServer.js迁移到vue.config.js下的devServer
  • config/mpxPlugin.conf.js迁移到vue.config.js下的pluginOptions.mpx.plugin
  • config/mpxLoader.conf.js迁移到vue.config.js下的pluginOptions.mpx.loader
// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      srcMode: 'wx',
-      plugin: {
-        // 这里等同于`@mpxjs/webpack-plugin`的参数
-      },
-      loader: {
-        // 这里等同于`mpx-loader`参数
-      }
-    }
-  },
-  devServer: {
-    // dev服务配置
-  }
-})
-

# 新增自定义配置/修改已有配置参数

// vue.config.js
-const { defineConfig } = require('@vue/cli-service')
-
-module.exports = defineConfig({
-  chainWebpack(config) {
-    config.plugin('newPlugin').use(newPlugin, [params])
-    // 使用mpx inspect 可以根据注释来查看插件命名
-    config.plugin('mpx-webpack-plugin').tap(args => newArgs)
-  },
-  // 或者也可以通过configureWebpack配置,这里返回的配置会通过webpack-merge合并到内部配置中
-  configureWebpack() {
-    return {
-      plugins: [new Plugin()]
-    }
-  }
-})
-

# 编译后钩子

由于 webpack 配置都内置到了插件里,所以编译后的钩子无法像 2.x 一样直接在webpack脚本里添加。

这里有两个方案来解决上述问题

  1. webpack插件
  2. 新建一个mpx-cli插件

例如我们新建一个vue-cli-plugin-mpx-build-upload插件,用来在构建完成后上传图片到cdn

// index.js
-module.exports = function (api, options) {
-  function runServiceCommand(api, command, ...args){
-    const { fn } = api.service.commands[command]
-    return fn && fn(...args)
-  }
-  // 注册一个新的命令
-  api.registerCommand('build:upload', function deploy(...args) {
-    // 运行原有的build命令,build会返回一个promise来表示构建完成
-    return runServiceCommand(api, 'build', ...args).then(() =>
-      // do something
-      uploadFile()
-    )
-  })
-}
-

然后在我们的项目里安装该插件并运行npx mpx-cli-service build:upload即可。

# 项目结构变化

项目结构变化

v3 版本相对于 v2 版本的目录结构更加清晰。

  • 移除了config/build的配置目录,将其统一到了插件配置当中,可以通过vue.config.js修改。
  • index.html移动到public目录下。
  • 增加jsconfig.json,让类型提示更加友好。

# More

v3 版本相对于 v2 版本的整体架构相差较大,v3 版本主要基于vue-cli架构,主要有以下优势。

# 1. 插件化

v3 版本的配置依靠插件化,将 v2 版本的文件配置整合到了各个自定义插件中。

  • vue-cli-plugin-mpx-eslint eslint 配置
  • vue-cli-plugin-mpx-mp 小程序构建配置以及命令
  • vue-cli-plugin-mpx-plugin-mode 插件配置
  • vue-cli-plugin-mpx-typescript ts 配置
  • vue-cli-plugin-mpx-web web 构建配置以及命令

除此之外,也可以使用统一的vue.config.js来自定义配置,或者将配置抽离到插件当中,来进行统一的管理。

# 2. 模板

v3 版本的模板也可以通过插件进行自定义生成,同时不依赖于 github,在国内网络下不会有生成模板时网络错误的问题。

# 3. 调试

v3 版本可以通过mpx inspect:mp/web来直接调试相关配置,可以更直观的发现配置错误。

# 4. 插件管理

使用mpx invoke/mpx add/mpx upgrade来管理插件,可以更细粒度的控制相关配置的更新。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/platform/index.html b/docs-vuepress/.vuepress/dist/guide/platform/index.html deleted file mode 100644 index 43b6e08b46..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/platform/index.html +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - 跨端输出基础 | Mpx框架 - - - - - - - - - -

# 跨端输出基础

Mpx以微信增强DSL为基础,支持跨端输出至多端小程序、web和客户端,包括支付宝、百度、抖音、京东、QQ等多端小程序平台,基于Vue的web平台,和基于react-native的ios、android及鸿蒙平台。

# 跨端输出配置

配置mpx进行跨端输出十分简单,找到项目构建的webpack配置,在@mpxjs/webpack-plugin的配置参数中设置mode和srcMode参数即可。

new MpxwebpackPlugin({
-  // mode为mpx编译的目标平台,可选值有(wx|ali|swan|qq|tt|jd|web|ios|android|harmony)
-  mode: 'ali',
-  // srcMode为mpx编译的源码平台,目前仅支持wx   
-  srcMode: 'wx'
-})
-

对于使用 @mpxjs/cli 创建的项目,可以通过在 npm script 当中定义 targets 来设置编译的目标平台,多个平台标识以,分隔。

// 项目 package.json
-{
-  "script": {
-    "build:cross": "mpx-cli-service build --targets=wx,ali,ios,android"
-  }
-}
-

# 跨端条件编译

Mpx跨端输出时在框架内针对不同平台的差异进行了大量的转换抹平工作,但框架能做的工作始终是有限的,对于框架无法抹平的部分我们会在编译和运行时进行报错提示,同时提供了完善的跨平台条件编译机制,便于用户自行进行差异化处理,该能力也能够用于实现区分平台进行业务逻辑实现。

Mpx中我们支持了三种维度的条件编译,分别是文件维度,区块维度和代码维度,其中,文件维度和区块维度主要用于处理一些大块的平台差异性逻辑,而代码维度主要用于处理一些局部简单的平台差异。

# 文件维度条件编译

文件维度条件编译简单的来说就是文件为维度进行跨平台差异代码的编写,例如在微信->支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,这时你可以在相同的位置新建一个map.ali.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的mode来加载对应模块,当mode为ali时,会优先加载map.ali.mpx,反之则会加载map.mpx。

文件维度条件编译能够与webpack alias结合使用,对于npm包的文件我们并不方便在原本的文件位置创建.ali的条件编译文件,但我们可以通过webpack alias在相同位置创建一个虚拟的.ali文件,并将其指向项目中的其他文件位置。

  // 对于npm包中的文件依赖
-  import npmModule from 'somePackage/lib/index'
-
-  // 配置以下alias后,当mode为ali时,会优先加载项目目录中定义的projectRoot/somePackage/lib/index文件
-  // vue.config.js
-  module.exports = defineConfig({
-    configureWebpack() {
-      return {
-        resolve: {
-          alias: {
-            'somePackage/lib/index.ali': 'projectRoot/somePackage/lib/index'
-          }
-        }
-      }
-    }
-  })
-

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加mode属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali时使用如下区块-->
-<template mode="ali">
-<!--该区块中的所有代码需采用支付宝的技术标准进行编写-->
-  <view>支付宝环境</view>
-</template>
-
-<!--其他编译mode时使用如下区块-->
-<template>
-  <view>其他环境</view>
-</template>
-

# 代码维度条件编译

如果只有局部的代码存在跨平台差异,mpx同样支持在代码内使用if/else进行局部条件编译,用户可以在js代码和template插值中访问__mpx_mode__获取当前编译mode,进行平台差异逻辑编写,js代码中使用示例如下。

除了 __mpx_mode__ 这个默认插值以外,有别的环境变量需要的话可以在mpx.plugin.conf.js里通过defs进行配置。

if(__mpx_mode__ === 'ali') {
-  // 执行支付宝环境相关逻辑
-} else {
-  // 执行其他环境相关逻辑
-}
-

template代码中使用示例如下

<!--此处的__mpx_mode__不需要在组件中声明数据,编译时会基于当前编译mode进行替换-->
-<view wx:if="{{__mpx_mode__ === 'ali'}}">支付宝环境</view>
-<view wx:else>其他环境</view>
-

JSON中的条件编译(注意,这个依赖JSON的动态方案,得通过name="json"这种方式来编写,其实写的是js代码,最终module.exports导出一个可json化的对象即可):

<script name="json">
-const pages = __mpx_mode__ === 'wx' ? [
-  'main/xxx',
-  'sub/xxx'
-] : [
-  'test/xxx'
-] // 可以为不同环境动态书写配置
-module.exports = {
-  usingComponents: {
-    aComponents: '../xxxxx' // 可以打注释 xxx组件
-  }
-}
-</script>
-

样式的条件编译:

/*
-  @mpx-if (__mpx_env__ === 'someEvn')
-*/
-  /* @mpx-if (__mpx_mode__ === 'wx') */
-  .backColor {
-    background: green;
-  }
-  /*
-    @mpx-elif (__mpx_mode__ === 'qq')
-  */
-  .backColor {
-    background: black;
-  }
-  /* @mpx-endif */
-
-  /* @mpx-if (__mpx_mode__ === 'swan') */
-  .backColor {
-    background: cyan;
-  }
-  /* @mpx-endif */
-  .textSize {
-    font-size: 18px;
-  }
-/*
-  @mpx-else
-*/
-.backColor {
-  /* @mpx-if (__mpx_mode__ === 'swan') */
-  background: blue;
-  /* @mpx-else */
-  background: red;
-  /* @mpx-endif */
-}
-/*
-  @mpx-endif
-*/
-

# 属性维度条件编译

属性维度条件编译允许用户在组件上使用 @| 符号来指定某个节点或属性只在某些平台下有效。

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

比如业务中需要通过 button 按钮获取用户信息,虽然可以使用代码维度条件编译来解决,但是增加了很多代码量:

<button
-  wx:if="{{__mpx_mode__ === 'wx' || __mpx_mode__ === 'swan'}}"
-  open-type="getUserInfo"
-  bindgetuserinfo="getUserInfo">
-  获取用户信息
-</button>
-
-<button
-  wx:elif="{{__mpx_mode__ === 'ali'}}"
-  open-type="getAuthorize"
-  scope="userInfo"
-  onTap="onTap">
-  获取用户信息
-</button>
-

而用属性维度的编译则方便很多:

<button
-  open-type@wx|swan="getUserInfo"
-  bindgetuserinfo@wx|swan="getUserInfo"
-  open-type@ali="getAuthorize"
-  scope@ali="userInfo"
-  onTap@ali="onTap">
-  获取用户信息
-</button>
-

属性维度的编译也可以对整个节点进行条件编译,例如只想在支付宝小程序中输出某个节点:

<view @ali>this is view</view>
-

需要注意使用上述用法时,节点自身在构建时框架不会对节点属性进行平台语法转换,但对于其子节点,框架并不会继承父级节点 mode,会进行正常跨平台语法转换。

<!--错误示例-->
-<view @ali bindtap="otherClick">
-    <view bindtap="someClick">tap click</view>
-</view>
-// srcMode 为 wx 跨端输出 ali 结果为
-<view @ali bindtap="otherClick">
-    <view onTap="someClick">tap click</view>
-</view>
-

上述示例为错误写法,假如srcMode为微信小程序,用上述写法构建输出支付宝小程序时,父节点 bindtap 不会被转为 onTap,在支付宝平台执行时事件会无响应。

正确写法如下:

<!--正确示例-->
-<view @ali onTap="otherClick">
-    <view bindtap="someClick">tap click</view>
-</view>
-// 输出 ali 产物
-<view @ali onTap="otherClick">
-    <view onTap="someClick">tap click</view>
-</view>
-

有时开发者期望使用 @ali 这种方式仅控制节点的展示,保留节点属性的平台转换能力,为此 Mpx 实现了一个隐式属性条件编译能力

<!--srcMode为 wx,输出 ali 时,bindtap 会被正常转换为 onTap-->
-<view @_ali bindtap="someClick">test</view>
-

在对应的平台前加一个_,例如@_ali、@_swan、@_tt等,使用该隐式规则仅有条件编译能力,节点属性语法转换能力依旧。

有时候我们不仅需要对节点属性进行条件编译,可能还需要对节点标签进行条件编译。

为此,我们支持了一个特殊属性 mpxTagName,如果节点存在这个属性,我们会在最终输出时将节点标签修改为该属性的值,配合属性维度条件编译,即可实现对节点标签进行条件编译,例如在百度环境下希望将某个 view 标签替换为 cover-view,我们可以这样写:

<view mpxTagName@swan="cover-view">will be cover-view in swan</view>
-

# 通过 env 实现自定义目标环境的条件编译

Mpx 支持在以上四种条件编译的基础上,通过自定义 env 的形式实现在不同环境下编译产出不同的代码。

实例化 MpxWebpackPlugin 的时候,传入配置 env。

// vue.config.js
-module.exports = defineConfig({
-  pluginOptions: {
-    mpx: {
-      srcMode: 'wx' // srcMode为mpx编译的源码平台,目前仅支持wx
-      plugin: {
-        env: "didi" // env为mpx编译的目标环境,需自定义
-      }
-    }
-  }
-})
-

# 文件维度条件编译

微信转支付宝的项目中存在一个业务地图组件map.mpx,由于微信和支付宝中的原生地图组件标准差异非常大,无法通过框架转译方式直接进行跨平台输出,而且这个地图组件在不同的目标环境中也有很大的差异,这时你可以在相同的位置新建一个 map.ali.didi.mpx 或 map.ali.qingju.mpx,在其中使用支付宝的技术标准进行开发,编译系统会根据当前编译的 mode 和 env 来加载对应模块,当 mode 为 ali,env 为 didi 时,会优先加载 map.ali.didi.mpx、map.ali.mpx,如果没有定义 env,则会优先加载 map.ali.mpx,反之则会加载 map.mpx。

# 区块维度条件编译

在.mpx单文件中一般存在template、js、stlye、json四个区块,mpx的编译系统支持以区块为维度进行条件编译,只需在区块标签中添加modeenv属性定义该区块的目标平台即可,示例如下:

<!--编译mode为ali且env为didi时使用如下区块,优先级最高是4-->
-<template mode="ali" env="didi">
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-
-<!--编译mode为ali时使用如下区块,优先级是3-->
-<template mode="ali">
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-
-<!--编译env为didi时使用如下区块,优先级是2-->
-<template env="didi">
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-
-<!--其他环境,优先级是1-->
-<template>
-  <view>该区块中的所有代码需采用支付宝的技术标准进行编写</view>
-</template>
-

注意,如果多个相同的区块写相同的 mode 和 env,默认会用最后一个,如:

<template mode="ali">
-  <view>该区块会被忽略</view>
-</template>
-
-<template mode="ali">
-  <view>默认会用这个区块</view>
-</template>
-

# 代码维度条件编译

如果在 MpxWebpackPlugin 插件初始化时自定义了 env,你可以访问__mpx_env__获取当前编译env,进行环境差异逻辑编写。使用方法与__mpx_mode__相同。

# 属性维度条件编译

env 属性维度条件编译与 mode 的用法大致相同,使用 : 符号与 mode 和其他 env 进行串联,与 mode 组合使用格式形如 attr@mode:env:env|mode:env,为了不与 mode 混淆,当条件编译中仅存在 env 条件时,也需要添加 : 前缀,形如 attr@:env

对于同一个 button 组件,微信小程序支持 open-type="getUserInfo",但是支付宝小程序支持 open-type="getAuthorize"。如果不使用任何维度的条件编译,则在编译的时候会有警告和报错信息。

如果当前编译的目标平台是 wx,以下写法 open-type 属性将被忽略

<button open-type@swan:didi="getUserInfo">获取用户信息</button>
-

如果当前 env 不是 didi,以下写法 open-type 属性也会被忽略

<button open-type@:didi="getUserInfo">获取用户信息</button>
-

如果只想在 mode 为 wx 且 env 为 didi 或 qingju 的环境下使用 open-type 属性,则可以这样写:

<button open-type@wx:didi:qingju="getUserInfo">获取用户信息</button>
-

env 属性维度的编译同样支持对整个节点或者节点标签名进行条件编译:

<view @:didi>this is a  view component</view>
-<view mpxTagName@:didi="cover-view">this is a  view component</view>
-

如果只声明了 env,没有声明 mode,跨平台输出时框架对于节点属性默认会进行转换:

<!--srcMode为wx,跨平台输出ali时,bindtap会被转为onTap-->
-<view @:didi bindtap="someClick">this is a  view component</view>
-<view bindtap@:didi ="someClick">this is a  view component</view>
-

# 环境API跨端抹平

# Webview环境跨端抹平

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html b/docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html deleted file mode 100644 index 2e04c6e315..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/platform/miniprogram.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Mpx框架 - - - - - - - - - - - - - diff --git a/docs-vuepress/.vuepress/dist/guide/platform/rn.html b/docs-vuepress/.vuepress/dist/guide/platform/rn.html deleted file mode 100644 index d804f30d1b..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/platform/rn.html +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - 跨端输出RN | Mpx框架 - - - - - - - - - -

# 跨端输出RN

大致介绍

# 跨端样式定义

# CSS选择器

# 样式单位

# 文本样式继承

# 简写样式属性

# CSS函数

# 使用原子类

# 混合编写RN代码

# 使用RN组件

# 使用React hooks

# 能力支持范围

# 模版语法

# 事件处理

目前 Mpx 输出 React Native 的事件编写遵循小程序的事件编写规范,支持事件的冒泡及捕获

普通事件绑定

<view bindtap="handleTap">
-    Click here!
-</view>
-

绑定并阻止事件冒泡

<view catchtap="handleTap">
-    Click here!
-</view>
-

事件捕获

<view capture-bind:touchstart="handleTap1">
-  outer view
-  <view capture-bind:touchstart="handleTap2">
-    inner view
-  </view>
-</view>
-

中断捕获阶段和取消冒泡阶段

<view capture-catch:touchstart="handleTap1">
-  outer view
-</view>
-
-

在此基础上也新增了事件处理内联传参的增强机制。

<template>
- <!--Mpx增强语法,模板内联传参,方便简洁-->
- <view bindtap="handleTapInline('b')">b</view>
- </template>
- <script setup>
-  // 直接通过参数获取数据,直观方便
-  const handleTapInline = (name) => {
-    console.log('name:', name)
-  }
-  // ...
-</script>
-

除此之外,Mpx 也支持了动态事件绑定

<template>
- <!--动态事件绑定-->
- <view wx:for="{{items}}" bindtap="handleTap_{{index}}">
-  {{item}}
-</view>
- </template>
- <script setup>
-  import { ref } from '@mpxjs/core'
-
-  const data = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4'])
-  const handleTap_0 = (event) => {
-    console.log('Tapped on item 1');
-  },
-
-  const handleTap_1 = (event) => {
-    console.log('Tapped on item 2');
-  },
-
-  const handleTap_2 = (event) => {
-    console.log('Tapped on item 3');
-  },
-
-  const handleTap_3 = (event) => {
-    console.log('Tapped on item 4');
-  }
-</script>
-

注意事项

  1. 当同一个元素上同时绑定了 catchtap 和 bindtap 事件时,两个事件都会被触发执行。但是是否阻止事件冒泡的行为,会以模板上第一个绑定的事件标识符为准。 -如果第一个绑定的是 catchtap,那么不管后面绑定的是什么,都会阻止事件冒泡。如果第一个绑定的是 bindtap,则不会阻止事件冒泡。
  2. 当同一个元素上绑定了 capture-bind:tap 和 bindtap 事件时,事件的执行时机会根据模板上第一个绑定事件的标识符来决定。如果第一个绑定的是 capture-bind:tap,则事件会在捕获阶段触发,如果第一个绑定的是 bindtap,则事件会在冒泡阶段触发。
  3. 当使用了事件委托想获取 e.target.dataset 时,只有点击到文本节点才能获取到,点击其他区域无效。建议直接将事件绑定到事件触发的元素上,使用 e.currentTarget 来获取 dataset 等数据。
  4. 如果元素上设置了 opacity: 0 的样式,会导致 ios 事件无法响应。

# 基础组件

目前 Mpx 输出 React Native 仅支持以下组件,文档中未提及的组件以及组件属性即为不支持,具体使用范围可参考如下文档

RN环境基础组件通用属性

属性名 类型 默认值 说明
enable-offset Boolean false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息
enable-var Boolean true 默认支持使用 css variable,若想关闭该功能可设置为 false
parent-font-size Number 父组件字体大小,主要用于百分比计算的场景,如 font-size: 100%
parent-width Number 父组件宽度,主要用于百分比计算的场景,如 width: calc(100% - 20px),需要在外部传递父组件的宽度
parent-height Number 父组件高度,主要用于百分比计算的场景,如 height: calc(100% - 20px),需要在外部传递父组件的高度

# view

视图容器。

属性

属性名 类型 默认值 说明
hover-class string 指定按下去的样式类。
hover-start-time number 50 按住后多久出现点击态,单位毫秒
hover-stay-time number 400 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

# scroll-view

可滚动视图区域。

属性

属性名 类型 默认值 说明
scroll-x Boolean false 允许横向滚动动
scroll-y Boolean false 允许纵向滚动
upper-threshold Number 50 距顶部/左边多远时(单位 px),触发 scrolltoupper 事件
lower-threshold Number 50 距底部/右边多远时(单位 px),触发 scrolltolower 事件
scroll-top Number 0 设置纵向滚动条位置
scroll-left Number 0 设置横向滚动条位置
scroll-with-animation Boolean false 在设置滚动条位置时使用动画过渡
enable-back-to-top Boolean false 点击状态栏的时候视图会滚动到顶部
enhanced Boolean false scroll-view 组件功能增强
refresher-enabled Boolean false 开启自定义下拉刷新
scroll-anchoring Boolean false 开启滚动区域滚动锚点
scroll-into-view Boolean false 值应为某子元素id(id不能以数字开头)
refresher-default-style String 'black' 设置下拉刷新默认样式,支持 blackwhitenone,仅安卓支持
refresher-background String '#fff' 设置自定义下拉刷新背景颜色,仅安卓支持
refresher-triggered Boolean false 设置当前下拉刷新状态,true 表示已触发
paging-enabled Number false 分页滑动效果 (同时开启 enhanced 属性后生效),当值为 true 时,滚动条会停在滚动视图的尺寸的整数倍位置
show-scrollbar Number true 滚动条显隐控制 (同时开启 enhanced 属性后生效)
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息
enable-trigger-intersection-observer Boolean [] 是否开启intersection-observer
simultaneous-handlers Array [] 主要用于组件嵌套场景,允许多个手势同时识别和处理并触发,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 scroll-view 组件
wait-for Array [] 主要用于组件嵌套场景,允许延迟激活处理某些手势,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 scroll-view 组件

事件

事件名 说明
binddragstart 滑动开始事件,同时开启 enhanced 属性后生效
binddragging 滑动事件,同时开启 enhanced 属性后生效
binddragend 滑动结束事件,同时开启 enhanced 属性后生效
bindscrolltoupper 滚动到顶部/左边触发
bindscrolltolower 滚动到底部/右边触发
bindscroll 滚动时触发
bindrefresherrefresh 自定义下拉刷新被触发

注意事项

  1. 目前不支持自定义下拉刷新节点,使用 slot="refresher" 声明无效,在 React Native 环境中还是会被当作普通节点渲染出来
  2. 若使用 scroll-into-view 属性,需要 id 对应的组件节点添加 wx:ref 标记,否则无法正常滚动
  3. simultaneous-handlers 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture]
  4. wait-for 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail]

# swiper

滑块视图容器。

属性

属性名 类型 默认值 说明
indicator-dots Boolean false 是否显示面板指示点
indicator-color color rgba(0, 0, 0, .3) 指示点颜色
indicator-active-color color #000000 当前选中的指示点颜色
autoplay Boolean false 是否自动切换
current Number 0 当前所在滑块的 index
interval Number 5000 自动切换时间间隔
duration Number 500 滑动动画时长
circular Boolean false 是否采用衔接滑动
vertical Boolean false 滑动方向是否为纵向
previous-margin String 0 前边距,可用于露出前一项的一小部分,接受px
next-margin String 0 后边距,可用于露出后一项的一小部分,接受px
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息
easing-function String linear 支持 linear、easeInCubic、easeOutCubic、easeInOutCubic
bindchange eventhandle current 改变时会触发 change 事件,event.detail = {current, source}

事件

事件名 说明
bindchange current 改变时会触发 change 事件,event.detail = {current, source}

# swiper-item

  1. 仅可放置在swiper组件中,宽高自动设置为100%。

属性

属性名 类型 默认值 说明
item-id string 该 swiper-item 的标识符

# movable-area

movable-view的可移动区域。

注意事项

  1. movable-area不支持设置 scale-area,缩放手势生效区域仅在 movable-view 内

# movable-view

可移动的视图容器,在页面中可以拖拽滑动。movable-view 必须在 movable-area 组件中,并且必须是直接子节点,否则不能移动。

属性

属性名 类型 默认值 说明
direction String none 目前支持 all、vertical、horizontal、none|
inertia boolean false movable-view是否带有惯性|
out-of-bounds boolean false 超过可移动区域后,movable-view是否还可以移动|
x Number 定义x轴方向的偏移
y Number 定义y轴方向的偏移
friction Number 7 摩擦系数
disabled boolean false 是否禁用
animation boolean true 是否使用动画
simultaneous-handlers Array [] 主要用于组件嵌套场景,允许多个手势同时识别和处理并触发,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件
wait-for Array [] 主要用于组件嵌套场景,允许延迟激活处理某些手势,这个属性可以指定一个或多个手势处理器,处理器支持使用 this.$refs.xxx 获取组件实例来作为数组参数传递给 movable-view 组件

事件

事件名 说明
bindchange 拖动过程中触发的事件,event.detail = {x, y, source}
bindscale 缩放过程中触发的事件,event.detail = {x, y, scale}
htouchmove 初次手指触摸后移动为横向的移动时触发
vtouchmove 初次手指触摸后移动为纵向的移动时触发

注意事项

  1. simultaneous-handlers 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture]
  2. wait-for 为 RN 环境特有属性,具体含义可参考(react-native-gesture-handler)[https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail]

# root-portal

使整个子树从页面中脱离出来,类似于在 CSS 中使用 fixed position 的效果。主要用于制作弹窗、弹出层等。 -属性

属性名 类型 默认值 说明

| enable | boolean | true | 是否从页面中脱离出来 |

注意事项

  1. style 样式不支持中使用百分比计算、css variable

# cover-view

视图容器。 -功能同 view 组件

# cover-image

视图容器。 -功能同 image 组件

# icon

图标组件

属性

属性名 类型 默认值 说明
type String icon 的类型,有效值:success、success_no_circle、info、warn、waiting、cancel、download、search、clear
size String | Number 23 icon 的大小
color String icon 的颜色,同 css 的 color

# text

文本。

属性

属性名 类型 默认值 说明
user-select boolean false 文本是否可选。
disable-default-style boolean false 会内置默认样式,比如fontSize为16。设置true可以禁止默认的内置样式。
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

注意事项

  1. 未包裹 text 标签的文本,会自动包裹 text 标签。
  2. text 组件开启 enable-offset 后,offsetLeft、offsetWidth 获取时机仅为组件首次渲染阶段

# button

按钮。

属性

属性名 类型 默认值 说明
size String default 按钮的大小
type String default 按钮的样式类型
plain Boolean false 按钮是否镂空,背景色透明
disabled Boolean false 是否禁用
loading Boolean false 名称前是否带 loading 图标
open-type String 微信开放能力,当前仅支持 share
hover-class String 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果
hover-start-time Number 20 按住后多久出现点击态,单位毫秒
hover-stay-time Number 70 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

# label

用来改进表单组件的可用性

注意事项

  1. 当前不支持使用 for 属性找到对应 id,仅支持将控件放在该标签内,目前可以绑定的空间有:checkbox、radio、switch。

# checkbox

多选项目

属性

属性名 类型 默认值 说明
value String checkbox 标识,选中时触发 checkbox-group 的 change 事件,并携带 checkbox 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox的颜色,同css的color

# checkbox-group

多项选择器,内部由多个checkbox组成。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# radio

单选项目

属性

属性名 类型 默认值 说明
value String radio 标识,当该 radio 选中时,radio-group 的 change 事件会携带 radio 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox 的颜色,同 css 的 color

# radio-group

单项选择器,内部由多个 radio 组成

事件

事件名 说明
bindchange radio-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 radio 的 value 的数组 ] }

# form

表单。将组件内的用户输入的switch input checkbox slider radio picker 提交。

当点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key。

事件

事件名 说明
bindsubmit 携带 form 中的数据触发 submit 事件,event.detail = {value : {'name': 'value'} }
bindreset 表单重置时会触发 reset 事件

# input

输入框。

属性

属性名 类型 默认值 说明
value String 输入框的初始内容
type String text input 的类型,不支持 safe-passwordnickname
password Boolean false 是否是密码类型
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
confirm-type String done 设置键盘右下角按钮的文字,仅在 type='text' 时生效
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bind:selectionchange 选区改变事件, event.detail = { selectionStart, selectionEnd }

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# textarea

多行输入框。

属性

属性名 类型 默认值 说明
value String 输入框内容
type String text input 的类型,不支持 safe-passwordnickname
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
auto-height Boolean false 是否自动增高,设置 auto-height 时,style.height不生效
confirm-type String done 设置键盘右下角按钮的文字,不支持 return
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bindlinechange 输入框行数变化时调用,event.detail = { height: 0, lineCount: 0 },不支持 heightRpx
bind:selectionchange 选区改变事件, {selectionStart, selectionEnd}

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# picker-view

嵌入页面的滚动选择器。其中只可放置 picker-view-column组件,其它节点不会显示

属性

属性名 类型 默认值 说明
value Array[number] false 数组中的数字依次表示 picker-view 内的 picker-view-column 选择的第几项(下标从 0 开始),数字大于 picker-view-column 可选项长度时,选择最后一项。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# picker-view-column

滚动选择器子项。仅可放置于picker-view中,其孩子节点的高度会自动设置成与picker-view的选中框的高度一致

# picker

从底部弹起的滚动选择器。

属性

属性名 类型 默认值 说明
mode string selector 选择器类型
disabled boolean false 是否禁用

公共事件

事件名 说明
bindcancel 取消选择时触发
bindchange 滚动选择时触发change事件,event.detail = {value};value为数组,表示 picker-view 内的 picker-view-column 当前选择的是第几项(下标从 0 开始)
# 普通选择器:mode = selector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value number 0 表示选择了 range 中的第几个(下标从 0 开始)
# 多列选择器:mode = multiSelector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value array [] 表示选择了 range 中的第几个(下标从 0 开始)
bindcolumnchange 列改变时触发
# 多列选择器:时间选择器:mode = time

属性

属性名 类型 默认值 说明
value string [] 表示选中的时间,格式为"hh:mm"
start string false 表示有效时间范围的开始,字符串格式为"hh:mm"
end string [] 表示有效时间范围的结束,字符串格式为"hh:mm"
# 多列选择器:时间选择器:mode = date

属性

属性名 类型 默认值 说明
value string 当天 表示选中的日期,格式为"YYYY-MM-DD"
start string false 表示有效日期范围的开始,字符串格式为"YYYY-MM-DD"
end string [] 表示有效日期范围的结束,字符串格式为"YYYY-MM-DD"
fields string day 有效值 year,month,day,表示选择器的粒度
# fields 有效值:
属性名 说明
year 选择器粒度为年
month 选择器粒度为月份
day 选择器粒度为天
# 省市区选择器:mode = region

属性

属性名 类型 默认值 说明
value array [] 表示选中的省市区,默认选中每一列的第一个值
custom-item string 可为每一列的顶部添加一个自定义的项
level string region 选择器层级
# level 有效值:
属性名 说明
province 选省级选择器
city 市级选择器
region 区级选择器

# image

图片。

属性

属性名 类型 默认值 说明
src String false 图片资源地址,支持本地图片资源及 base64 格式数据,暂不支持 svg 格式
mode String scaleToFill 图片裁剪、缩放的模式,适配微信 image 所有 mode 格式
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
binderror 当错误发生时触发,event.detail = { errMsg }
bindload 当图片载入完毕时触发,event.detail = { height, width }

注意事项

  1. image 组件默认宽度320px、高度240px
  2. image 组件进行缩放时,计算出来的宽高可能带有小数,在不同webview内核下渲染可能会被抹去小数部分

# canvas

画布。

事件

属性名 类型 说明
bindtouchstart eventhandle 手指触摸动作开始
bindtouchmove eventhandle 手指触摸后移动
bindtouchend eventhandle 手指触摸动作结束
bindtouchcancel eventhandle 手指触摸动作被打断
bindlongtap eventhandle 手指长按 300ms 之后触发
binderror eventhandle 当发生错误时触发 error 事件, detail = {errMsg}

API

方法名 说明
createImage 创建一个图片对象。 仅支持在 2D Canvas 中使用
createImageData 创建一个 ImageData 对象。仅支持在 2D Canvas 中使用
getContext 该方法返回 Canvas 的绘图上下文。仅支持在 2D Canvas 中使用
toDataURL 返回一个包含图片展示的 data URI

注意事项

  1. canvas 组件目前仅支持 2D 类型,不支持 webgl
  2. 通过 Canvas.getContext('2d') 接口可以获取 CanvasRenderingContext2D 对象,具体接口可以参考 (HTML Canvas 2D Context)[https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D] 定义的属性、方法
  3. canvas 的实现主要借助于 PostMessage 方式与 webview 容器通信进行绘制,所以对于严格依赖方法执行时机的场景,如调用 drawImage 绘图,再通过 getImageData 获取图片数据的场景,调用时需要使用 await 等方式来保证方法的执行时机
  4. 通过 Canvas.createImage 画图,图片的链接不能有特殊字符,安卓手机可能会 load 失败
# web-view

承载网页的容器。会自动铺满整个RN页面

属性

属性名 类型 默认值 说明
src String webview 指向网页的链接,如果需要对跳转的URL设定白名单可跳转,需要在业务跳转之前出来该逻辑
bindmessage EventHandler 网页向RN通过 postMessage 传递数据
bindload EventHandler 网页加载成功时候触发此事件
binderror EventHandler 网页加载失败的时候触发此事件

注意事项

  1. web-view网页中可使用@mpxjs/webview-bridge@2.9.68提供的接口返回RN页面或与RN页面通信,具体使用细节可以参见Webview API

# 自定义组件

# 样式规则

# 应用能力

# 环境API

在RN环境中也提供了一部分常用api能力,方法名与使用方式与小程序相同,可能对于某个api提供的能力会比微信小程序提供的能力少一些,以下是使用说明:

# 使用说明

如果全量引入api-proxy这种情况下,需要如下配置

// 全量引入api-proxy
-import mpx from '@mpxjs/core'
-import apiProxy from '@didi/mpxjs-api-proxy'
-mpx.use(apiProxy, { usePromise: true })
-

需要在mpx项目中需要配置externals

externals: {
-  ...
-  '@react-native-async-storage/async-storage': '@react-native-async-storage/async-storage',
-  '@react-native-clipboard/clipboard': '@react-native-clipboard/clipboard',
-  '@react-native-community/netinfo': '@react-native-community/netinfo',
-  'react-native-device-info': 'react-native-device-info',
-  'react-native-safe-area-context': 'react-native-safe-area-context',
-  'react-native-reanimated': 'react-native-reanimated',
-  'react-native-get-location': 'react-native-get-location',
-  'react-native-haptic-feedback': 'react-native-haptic-feedback'
-},
-

如果引用单独的api-proxy方法这种情况,需要根据下表说明是否用到一下方法,来确定是否需要配置externals,配置参考上面示例

api方法 依赖的react-native三方库
showActionSheet react-native-reanimated
getNetworkType、
offNetworkStatusChange、
onNetworkStatusChange
@react-native-community/netinfo
getLocation、
openLocation、
chooseLocation
react-native-get-location
setStorage、
setStorageSync、
getStorage、
getStorageSync、
getStorageInfo、
getStorageInfoSync、
removeStorage,
removeStorageSync,
clearStorage,
clearStorageSync
@react-native-async-storage/async-storage
getSystemInfo、
getSystemInfoSync、
getDeviceInfo、
getWindowInfo、
getLaunchOptionsSync、
getEnterOptionsSync
react-native-device-info
getWindowInfo、
getLaunchOptionsSync、
getEnterOptionsSync
react-native-safe-area-context
vibrateShort、
vibrateLong
react-native-haptic-feedback

在RN 项目中,如果是以全量引入api-proxy的方法需要在RN环境中执行以下所有的命令,如果只是使用单个api的能力,可以参考上表来判断安装对应的包

// 安装api-proxy下所用到的依赖 如果
-npm i @react-native-async-storage/async-storage
-npm i @react-native-clipboard/clipboard
-npm i @react-native-community/netinfo
-npm i react-native-safe-area-context
-npm i react-native-device-info
-npm i react-native-reanimated
-npm i react-native-haptic-feedback
-ios项目需要执行如下命令
-cd ios
-pod install
-
-npm i react-native-get-location
-

android下需要做如下配置: -安装react-native-get-location包后,需要在AndroidManifest.xml中定义位置权限,参考文档 (opens new window)

<!-- Define ACCESS_FINE_LOCATION if you will use enableHighAccuracy=true  -->
-<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
-
-<!-- Define ACCESS_COARSE_LOCATION if you will use enableHighAccuracy=false  -->
-<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-
-

安装react-native-haptic-feedback包后需要在安卓中配置,配置参考文档 (opens new window) -打开 android/app/src/main/java/[...]/MainApplication.java. 在文件的顶部添加以下导入下面的代码片段

import com.mkuczera.RNReactNativeHapticFeedbackPackage;
-

修改设置,将下面的配置添加到android/settings.gradle文件中

include ':react-native-haptic-feedback'
-project(':react-native-haptic-feedback').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-haptic-feedback/android')
-

react-native-reanimated在mpx和RN项目都要安装,安装好包后需要在babel.config.json文件中做如下配置,并且RN环境中使用的react-native-reanimated与mpx项目中安装的react-native-reanimated版本要一致: -配置参考文档 (opens new window)

module.exports = {
-    presets: [
-      ... // don't add it here :)
-    ],
-    plugins: [
-      ...
-      'react-native-reanimated/plugin',
-    ],
-};
-

# Webview API

对于web-view组件打开的网页,想要跟RN通信,或者跳转到RN页面,提供了以下能力

方法名 说明
navigateTo 保留当前webview页面,跳转RN页面
navigateBack 关闭当前页面,返回上一页或多级RN页面
switchTab 跳转到RN的 tabBar 页面
reLaunch 关闭所有页面,打开到应用内的某个RN页面
redirectTo 关闭当前页面,跳转到应用内的某个RN页面
getEnv 获取当前环境
postMessage 向RN发送消息,实时触发组件的message事件
invoke 开放一个webview页面和web页面互通消息的能力
# webview-bridge示例代码
import webviewBridge from '@mpxjs/webview-bridge'
-webviewBridge.navigateTo({
-  url: 'RN地址',
-  success: () => {
-    console.log('跳转成功')
-  }
-})
-
# invoke示例代码

RN环境中挂载getTime的逻辑

import mpx from '@mpxjs/core'
-...
-// 普通方法
-mpx.config.webviewConfig = {
-  apiImplementations: {
-    getTime:  (options = {}) => {
-      const { params = {} } = options
-      return {
-        text: params.text,
-        time: '2024-12-24'
-      }
-    }
-  }
-}
-// 或者promise
-mpx.config.webviewConfig = {
-  apiImplementations: {
-    getTime:  (options = {}) => {
-      return new Promise((resolve, reject) => {
-        const { params = {} } = options
-        if (params.text) {
-          resolve({
-            text: params.text,
-            time: '2024-12-24'
-          })
-        } else {
-          reject(new Error('没有传text参数'))
-        }
-      })
-    }
-  }
-}
-...
-

web中通信的逻辑

import webviewBridge from '@mpxjs/webview-bridge'
-webviewBridge.invoke('getTime', {
-  params: {
-    text: '我是入参'
-  },
-  success: (res) => {
-    console.log('接收到的消息:', res.time)
-  }
-})
-

# 其他使用限制

如事件的target等

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/platform/web.html b/docs-vuepress/.vuepress/dist/guide/platform/web.html deleted file mode 100644 index d6ebdac7d9..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/platform/web.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Mpx框架 - - - - - - - - - - - - - diff --git a/docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html b/docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html deleted file mode 100644 index fce599e788..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/tool/e2e-test.html +++ /dev/null @@ -1,297 +0,0 @@ - - - - - - E2E自动化测试 | Mpx框架 - - - - - - - - - -

# E2E自动化测试

微信小程序的官方文档推荐 miniprogram-automator (opens new window),其与小程序IDE的关系,正如 Google 与 UiAutomator、selenium 与 webdriver 一样;它是最契合小程序的。

虽然微信小程序提供了 automator + ide 的 E2E 的解决方案,但该项目维护频率低且 case 编写效率低、API 不够友好等问题,因此基于微信提供的能力& Mpx 生态我们产出了相对完善的小程序E2E自动化测试增强方案,此外还提供了可扩展的开箱即用的E2E基础设施。

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。

如果你之前使用过 Selenium WebDriver 或者 Puppeteer,那你可以很容易快速上手。小程序自动化 SDK 与它们的工作原理是类似的,主要区别在于控制对象由浏览器换成了小程序。

# @mpx/cli 脚手架集成E2E

当使用 @mpxjs/cli 初始化 Mpx 项目的时候,交互式命令行里面新增了 E2E 选项,当选择了此选项,项目将会初始化 E2E 配置,完成相关内容的生成。

关于 E2E 的模板文件如下:

# 忽略部分文件夹
-vue-project
-├─ .e2erc.js # E2E配置
-├─ src
-│  ├─ app.mpx
-│  ├─ pages
-│  │  └─ index.png
-│  └─ components
-│     └─ list.mpx
-├─ test
-│  └─ e2e # case目录
-│     └─ list.spec.js # 示例文件
-├─ jest-e2e.config.js # Jest配置
-└─ package.json
-

这里罗列了 E2E 项目中约定(或推荐)的目录结构,在项目开发中,请遵照这个目录结构组织代码。

# 文件说明

package.json

使用自动化测试,我们要做的第一件事就是安装 @mpxjs/e2e 和 @mpxjs/e2e-scripts,然后我们需要在 package.json 中定义两个自动化测试的脚本 test 和 testServer。

{
-  "devDependencies": {
-    "@mpxjs/e2e": "0.0.13",
-    "@mpxjs/e2e-scripts": "0.0.12",
-  }
-  "scripts": {
-    "test": "e2e-runner",
-    "testServer": "e2e-runner --preview"
-  }
-}
-

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。

.e2erc.js

E2E 配置文件,包含 E2E 内置功能和插件的配置。可以在这里扩展运行时的能力,比如修改运行时是否自动保存页面快照。

const path = require('path');
-const PluginReport = require('@mpxjs/e2e/report-server/server.js');
-module.exports = {
-  recordsDir: 'dist/wx/minitest', // 录制 json 文件的存储目录
-  connectFirst: false, // 优先使用 automator.connect,默认 automator.launch 优先
-  defaultWaitFor: 5000, // 默认 waitFor 时长
-  devServer: { // 测试报告服务器配置
-    open: true,
-    port: 8886
-  },
-  jestTimeout: 990000, // jestTimeout
-  jsonCaseCpDir: 'test/e2e/e2e-json', // 从 minitest 目录复制 json 文件到该目录下
-  needRealMachine: false, // 是否需要真机
-  plugins: [new PluginReport()], // 自定义测试报告的插件
-  projectPath: path.resolve(__dirname, './dist/wx'),
-  sequence: [ // e2e 的 case 的执行顺序
-    // 'minitest-1'
-  ],
-  testSuitsDir: 'test/e2e/', // e2e 的 case 存放目录
-  useTS: false, // e2e 的 case 是否为 TS 语法
-  wsEndpoint: 'ws://localhost:9420', // automator.connect 的 wsEndpoint
-  timeoutSave: 3000, // 定时截图默认开启,设置为 false 即可关闭
-  cacheDirectory: path.resolve(__dirname, './node_modules/@mpxjs/e2e/report-server/cache'), // 配置截图数据的保存目录
-  tapSave: true, // 点击截图默认开启,设置为 false 即可关闭
-  routeUpdateSave: true, // 路由切换截图默认开启,设置为 false 即可关闭
-  routeTime: 300, // 路由切换 300ms 后再截图
-  watchResponse: [ { url: '/api/list', handler (newRes, oldRes) { return true }} ], // 配置接口请求截图
-}
-

我们提供了丰富的配置化选项,满足各种场景运行。

参数 类型 默认值 说明
projectPath String ./dist/wx 小程序代码路径,Mpx 框架的 wx 输出目录
testSuitsDir String test/e2e/suits/ e2e 的 case 存放目录
sequence string[ ] [ ] 用例运行的顺序
recordsDir String dist/wx/minitest 录制 json 文件的存储目录
connectFirst Boolean false 优先使用 automator 的 connect 方法
wsEndpoint String ws://localhost:9420 使用 connect 方式时的 wsEndpoint 选项
defaultWaitFor Number 15000 默认 waitFor 时长
useTS Boolean false 用例是否为 TS 语法
jestTimeout Number 990000 默认测试超时时间
jsonCaseCpDir String test/e2e-json 从 minitest 目录复制 json 文件到该目录下
needRealMachine Boolean false 是否需要真机回放
devServer Object null 测试报告服务器配置
plugins Array [ ] 自定义测试报告的插件
timeoutSave Number 3000 定时3000ms保存页面快照
cacheDirectory String report-server/cache 页面快照的保存目录
tapSave Boolean true 点击时候保存页面快照
routeUpdateSave Boolean true 路由切换时候保存页面快照
routeTime Number 300 路由切换300ms后保存页面快照
watchResponse Object null 监听接口请求保存页面快照

e2e

e2e 目录,所有的 case 文件存放在此目录下,默认会创建一个演示 demo 文件,也就是 list.spec.js 文件,约定 e2e 下所有的 .spec.js 结尾的作为自动化测试的文件,使用 Typescript 编写测试文件的时候, 需要将文件名改成 .spec.ts 格式,然后 tsconfig.json 加上 "esModuleInterop": true。

/**
- * @file e2e test example
- * 首先开启工具安全设置中的 CLI/HTTP 调用功能
- * docs of miniprogram-automator: https://developers.weixin.qq.com/miniprogram/dev/devtools/auto/quick-start.html
- */
-import automator from '@mpxjs/e2e'
-
-describe('index', () => {
-  let miniProgram
-  let page
-
-  beforeAll(async () => {
-    try {
-      miniProgram = await automator.connect({ wsEndpoint: 'ws://localhost:9420' })
-    } catch (e) {
-      miniProgram = await automator.launch({ projectPath: './dist/wx' })
-    }
-    page = await miniProgram.reLaunch('/pages/index')
-    await page.waitFor(500)
-  }, 30000)
-
-  it('desc', async () => {
-    const desc = await page.$('list', 'components/list2271575d/index')
-    // 断言页面标签
-    expect(desc.tagName).toBe('view')
-    // 断言文字内容
-    expect(await desc.text()).toContain('手机')
-    // 保存页面快照
-    await miniProgram.screenshot({
-      path: 'test/e2e/screenshot/homePage.png'
-    })
-  })
-
-  afterAll(async () => {
-    await miniProgram.close()
-  })
-})
-

如果你已经熟悉了 Jest,你应该很适应 Jest 的断言 API。

jest-e2e.config.js

Jest 配置文件,这些选项可让你控制 Jest 的行为,你可以了解 Jest 的默认选项,以便在必要时扩展它们:

module.exports = {
-  preset: 'ts-jest',
-  testEnvironment: 'node',
-  testTimeout: 1000000000,
-  maxWorkers: 1,
-  reporters: [
-    'default',
-    ['<rootDir>/node_modules/@mpxjs/e2e/report-server/report.js', {}], // 自定义测试报告
-  ]
-}
-

除了 Jest 提供的默认测试报告器外,我们还可以自定义测试报告。框架 @mpxjs/e2e 内部提供了可视化测试报告平台,需要配置 reporters 字段。

目前对于@mpx/cli@3.*版本也会陆续完成E2E的相关支持。

# @mpxjs/e2e-scripts

默认情况下,Jest 将会递归的找到整个工程里所有 .spec.js 或 .test.js 扩展名的文件。 Jest 支持并行运行 test ,特别是在 ci 场景,将会极大减少 test 消耗时间。配置 --maxWorkers 参数表示的是 Jest 会开启多少个线程去完成所有的测试任务,默认值是 50% * os.cpus().length,相关的文档可见:链接 (opens new window)

合理的设置 maxWorkers 会使得运行变快,依赖的是并行跑用例,但是在自动化测试环境下,用例通过脚本操控模拟器或真机环境,多个用例不能同时操控一个环境,否则会相互干扰,就好比 JS 只能是单线程执行一样。

因为用例只能一个一个的执行,一个完整的项目自然会包含很多用例,但是一次只能执行一个用例的话,我们需要人工的操作很多次,才能全部执行完。为了跑一个遍就把所有的用例都执行完的话,我们会想到写一个脚本通过遍历的方式依次执行,这就是 @mpxjs/e2e-scripts 设计的初衷。

使用 @mpxjs/e2e-scripts 内部提供的命令脚本,执行 npx e2e-runner 将会按照 sequence 的定义的顺序依次执行用例文件。

module.exports = {
-  sequence: [ // 测试用例的执行顺序
-    'minitest-1',
-    'minitest-2'
-  ]
-}
-

上面代码表示会先执行 minitest-1.spec.js 文件,然后再执行 minitest-2.spec.js 文件。

# E2E可视化报告平台

E2E内置的 Jest 默认支持输出 HTML 的报告,因其只支持对测试结果数据的简单展示,故我们希望在其基础上,不仅针对报告查看的广度和颗粒度进行细化,还将对自动化测试过程中涉及到的痛点实现功能上的增强。

E2E可视化报告平台是一个运行在本地环境,统合了用例管理、测试报告、页面快照和错误日志的平台。支持对通过 WechatDevTools 录制回放功能录制出的 case 进行自定义增强的能力,同时提供执行 E2E 测试过程中产出的页面快照和错误日志等信息进行快捷、直观地查看的功能。

目前支持多种交互动作保存快照(点击、输入、滑动等),我们还在页面快照方面做了增强,提供了快照标记的功能,可以完善测试报告,增强排查手段,当点击元素后,页面快照上会自动标记出点击的区域或者元素。

# E2E录制+自动化生成case

微信推出了官方的录制回放能力。通过微信开发者工具可以录制用户的操作过程,期间可以进行简单断言,录制结束后支持回放。但是经过实际使用发现录制回放存在下列不足:

  1. 录制结果结果以 json 形式存在于 dist 目录,难以扩展、难维护;
  2. 仅支持 data快照、wxml快照、检查元素、检查路径四种简单断言,难以满足复杂业务场景的细粒度断言诉求;
  3. 因等待时长、接口响应时机、组件更新时机等难以和录制时对应,录制结果回放失败频率高、不稳定;

我们通过基于微信原生的录制进行增强的方案。使用录制的便捷性降低手写流程的成本,再通过 sdk 的能力对录制所得 Case 进行增强。

这样,我们通过分析录制 JSON 文件,把 JSON 中的每一条进行分析转换,最终得到 spec 的 JS 代码文件,通过这种方式,可以大幅度降低获取元素、触发事件等常规 Case 的编写。

JSON to Spec 本质只是录制结果的一种呈现,而这种转换的目的在于通过扩展强化录制 Case,弥补录制的能力有限。

为了方便我们进行扩展,首先需要对录制所得的 JSON 文件进行语义化的分析。这么做的意义在于把录制的操作步骤和 JSON 的数据关联起来,而关联步骤和数据又可以增强可读性为用户在某一个步骤之后进行扩展增加了极大的便利性。

上一个部分已经论证过把录制和SDK增强接合起来的可行性,但是上面一系列的操作都是通过脚本的形式呈现的,这对于我们前面的降低门槛来说仍然是繁琐的。最起码对于不会写代码的的人来说,还是不够理想。接下来就是探索如何更直观、更高效的方式把这种方案落地。

我们设计了 Mpx-E2E 的工作台,当然这些也都集成到了 Mpx-E2E 的可视化平台中,下面我们看看这些具体的可视化的工作。

分析 JSON 操作步骤后,我们把依据 JSON 生成的 Spec 同样做了可视化处理,起初的时候我们只是做了 Spec 代码的 highlight,并没有支持编辑。但是考虑到所见即所得的效率,我们又在此支持了 WEB-IDE。在生成 Spec 代码后,即可在线进行编辑,最终通过我们的@mpxjs/e2e SDK核心能力,完成了保存spec文件。

# 增强API

@mpxjs/e2e

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。

然而在我们使用过程中发现原始小程序官网提供的sdk存在能力的不足和缺失,或者说无法满足我们复杂的业务流程,针对这一系列的能力的缺失,我们开发了针对e2e的增强型api,来满足我们的整个自动化流程所需的功能

1、获取页面中的DOM元素 $ 方法 :

SDK 中重写 page 和 element 的 $ 方法。之所以这么处理是因为原生的 $ 方法存在自定义组件中的元素不能稳定获取的问题,而这个问题的根源在于微信小程序会以某种规则为元素拼接组件或者父组件的名字作为真实类名,而这种规则并不固定。

目前增强后的 $ 方法支持两个参数,第一个为选择器,第二个是自定义组件名。第二个参数不传时其行为和原生 $ 方法一致。

$(className: string, componentsName?: string): Promise<Element | any>
- 
-const confirmbtn = await page.$('confirm-btn', 'homepage/components/confirmef91faba/confirm')
- 
-const confirmbtn2 = await page.$('.confirm-btn')
-const view = await page.$('view')
-const id = await page.$('#id')
-

获取自定义组件中的元素需要以渲染结果为准,通常第二个参数是传入组件的 is 属性。注意弹窗类似的组件往往会存在动画所以需要异步获取。

// 页面的渲染结果
-<base-dialog is="homepage/components/dialogd2d0cea6/index">
-  #shadow-root
-    <view class="wrapper">
-      <view class="cancel-btn">确认</view>
-      <view class="confirm-btn">取消</view>
-    </view>
-</base-dialog>
-
-// 对应获取元素的方法
-const cancelbtn = await page.$('cancel-btn', 'homepage/components/dialogd2d0cea6/index')
-const confirmbtn = await page.$('confirm-btn', 'homepage/components/dialogd2d0cea6/index')
-

2、增强 wait/ waitAll

automator 中原生支持 wait 方法,表示等待时长或者等待终止的断言函数。但是经过实际测试发现,使用指定的等待时间并不可靠,其运行受限于硬件条件。

经过增强的 wait 方法可以支持: 路由到指定页面, 指定组件渲染,指定组件更新,指定接口发起, 指定接口响应后;

wait(path: string, type?: string): Promise<string | undefined> | void;
- 
-const miniProgram = await Automator.launch({
-  projectPath: './dist/wx'
-})
-
-// 页面
-page = await miniProgram.reLaunch('/pages/index/index')
-await miniProgram.wait('pages/index/index')
-
-// 组件
-const suggest1 = await miniProgram.wait('suggest/components/suggestcaafe3e4/suggest', 'component')
- 
-// 组件更新
-const suggest2 = await miniProgram.wait('suggest/components/suggestcaafe3e4/suggest', 'componentUpdate')
- 
-// 请求
-const request = await miniProgram.wait('https://xxxx.xxx/xxx', 'request')
- 
-// 返回结果
-const response = await miniProgram.wait('https://xxxx.xxx/xxx', 'response')
-expect(response.options.data.errno).toBe(0)
-const data = response.options.data.data
-expect(data.status).toBe(1)
-

3、新增 mock 能力

考虑到进行 e2e 测试时有些场景需要 mock 数据,所以 SDK 结合 mpx-fetch 的 setProxy 能力提供了静态资源文件 mock、手动设置接口响应结果的能力。

前置:如果需要使用 mock 需要使用支持 setProxy 的 mpx-fetch 版本;此外还需要在 createApp 的时候传入 setProxy 属性,其配置与 mpx-fetch 的 setProxy 方法的配置相同,示例:

createApp({
-	setProxy: [
-  	{
-    	test: {
-      	host: 'some-domain.com',
-      	protocol: 'https:'
-    	},
-    	proxy: {
-      	host: 'localhost',
-      	port: 8887,
-      	protocol: 'http:'
-    	}
-  	}
-  ],
-	// otherApp props
-})
-

3.1 初始化 mock:initMock

当传入 staticDir 时会优先匹配该目录下的静 json 文件作为指定接口的响应结果。

接口与文件名的映射规则:将域名后的 path 中的分隔符 / 替换为 -,示例:

  • 接口名称:api/pGetIndexInfo
  • json 文件名称:api-pGetIndexInfo.json
interface E2eMockConfig {
-  staticDir: string // 本地文件目录:
-}
-
-Automator.initMock(mockCfg: E2eMockConfig): Promise<MiniProgram>
-

3.2 setMock 方法,除了上面的静态资源文件,mock 内置了一个 Map 列表,因此可以按需的设置某一接口的响应结果。

Automator.setMock (path:string, response:any): () => void
- 
-// 示例:
-let un = Automator.setMock('https://some-domain.com/api/pGetIndexInfo', {
-  errno: 0,
-  errmsg: 'mock-index-info',
-  data: {
-    a: 1,
-    b: 2,
-    c: 3
-  }
-});
- 
-// 需要取消时可以调用 un,注意这一步骤非必须!!
-un();
-

3.3 removeMockFromMap 从 mock 内置的 Map 列表中移除指定 path 对应的的 mock 数据。

Automator.removeMockFromMap (path:string): void
-

# SOP

一、环境要求

1.1 微信环境 -安装 Node.js 并且版本大于 8.0 -基础库版本为 2.7.3 及以上 -开发者工具版本为 1.02.1907232 及以上 -  -1.2 Mpx 环境 -Mpx-E2E 很多能力基于 Mpx 增强的,最低版本要求如下: -@mpxjs/core >= 2.7.2 -@mpxjs/api-proxy >= 2.7.1 -@mpxjs/webpack-plugin >= 2.7.8

二、新建项目

2.1 安装 @mpxjs/cli 脚手架

$ npm install -g @mpxjs/cli
-

2.2 初始化项目选择 E2E 测试,执行以下命令执行初始化项目

$ mpx init some-proj
-

执行该命令后稍等片刻,等模板下载完成 Terminal 会输出以下提示,当输出到 ”是否需要 E2E 测试“时输入 “yes“ ,脚手架内置了 E2E 需要的依赖和简单的测试用例。待脚手架初始化完成后,你的新项目就已经拥有了开箱即用的E2E能力。

三、已有项目

3.1 安装 E2E sdk、runner

npm i @mpxjs/e2e  @mpxjs/e2e-scripts --save-dev
-

3.2 创建 .e2erc 配置文件

在项目的源文件目录下创建名为 .e2erc.js 的配置文件,以下为 e2erc 各选项及其含义,建议直接复制到项目中

const path = require('path');
-const PluginReport = require('@mpxjs/e2e/report-server/server.js'); // E2E 报告插件
-module.exports = {
-  recordsDir: 'dist/wx/minitest', // 录制 json 文件的存储目录
-  connectFirst: false, // 优先使用 automator.connect,默认 automator.launch 优先
-  wsEndpoint: 'ws://localhost:9420', // automator.connect 的 wsEndpoint 选项,用于 connectFirst 时链接到模拟器服务
-  defaultWaitFor: 15000, // 默认 waitFor 时长
-  useTS: false, // 是否启用 TS , 该特性暂不支持,
-  devServer: {
-    open: true
-  },
-  jestTimeout: 990000, // jestTimeout
-  jsonCaseCpDir: 'test/e2e-json', // 从 minitest 目录复制 json 文件到该目录下
-  needRealMachine: false, // 是否需要真机
-  plugins: [new PluginReport()], 
-  projectPath: path.resolve(__dirname, './dist/wx'), // 小程序代码路径,Mpx 框架的 wx 输出目录
-  testSuitsDir: 'test/e2e/suits/', // e2e 的 case 存放目录
-  sequence: [ // E2E case,runner 将会按照这个数组顺序执行其中的 case 文件,
-    'minitest-20', // 不需要写 .spec.js 这个扩展名
-  ],
-  reportsDir: 'test/reports' // 报告输出目录
-}
-

四、获取 Case

4.1 录制 JSON Case

录制 case 是依托于微信开发者工具已有的能力,操作流程如下:

  1. 启动录制 -启动微信开发者工具 -> 工具 -> 自动化测试
  2. 创建用例 -选择【开始录制】按钮 -【用例名】会成为录制 JSON case 的文件名,比如上图中录制结束后会得到一个名为 -minitest-3.json 的 JSON 文件;我们推荐您采用更语义化的测试用例名称,便于后续的 CASE 管理。 -【结束录制】后,开发者工具会在小程序的代目录(Mpx 的产物输出目录,如 dist/wx)下新增 minitest/ 目录,该目录是微信开发者工具自动创建的,用于存放录制 JSON case。例如上面的例子中,录制结束后该目录下就应该有 名为 minitest-3.json 的 文件。

4.2 生成 Spec Case

启动 Mpx-E2E 平台服务,首先在项目的 package.json 中加入以下 npm script:

{
-  "e2eServe": "npx e2e-runner --preview"
-},
-

在小程序项目录下通过命令行启动 Mpx-E2E 平台服务:

npm run e2eServe
-

平台服务已经成功启动!打开 Mpx-E2E 的可视化平台: -选择【用例管理】,加载录制的 JSON Case,点击可视化平台上的 【JSON 导入】按钮。界面中间是对当前 JSON 文件的语义化解析,最后侧是包含自动生成代码的 Web IDE。点击中间的语义化解析会联动右侧 WebIDE 代码跳转到对应的区域。

4.3 可视化扩展 Spec Case

  1. 新增 -鼠标悬浮在中间语义化区域中每一条的左侧的绿色按钮时,就会出现一个扩展菜单,在弹出框中填写相关信息,点击【保存】即可新增一条操作

  2. 编辑 -通过可视化的方式扩展的操作可以编辑,录制的操作不能编辑;

4.4 保存 Spec case

在完成上述操作后,点击 Web-IDE 下面的 【保存】即可.

点击保存后, Web-IDE 中的内容将会被写入到 .e2erc.js 的 testSuitsDir 字段指定的目录中。当文件写入成功后,被保存的 spec 文件名会被自动写入到剪贴板中。

另外,生成这份 Case 的 JSON 文件也会被复制到 .e2erc.js 的 jsonCaseCpDir 目录下,之所以保存 JSON 文件主要是做备份,你可以不关心这个 json 的文件内容,但是我们建议你随着 spec 代码已经提交到版本库; -  -经过保存后,我们就得到了 minitest-3.spec.js Case 文件。

五、修改 .e2erc.js sequence

5.1 sequence 数组 -经过前面的获取 Case 后步骤后,我们已经得到了 minitest-3.spec.js Case 文件。在正式运行前,我们需要把这个 minitest-3.spec.js Case 加入到 .e2erc.js 的 sequence 字段数组中。

sequence: [ 
-  'minitest-3', // 不用写 .spec.js 
-]
-

5.2 为什么手动维护? -之所以需要手动维护这个数组,是因为微信的 automator 不能并发运行,进而 E2E 的 Case 无法并发运行;又或者某几个 spec 间是有联系的, 这种情况下只能把顺序交给人工维护。

六、运行 e2e

6.1 新增 npm scripts -修改项目的 package.json 中 scripts 字段,向其中加入 "test:e2e":"npx e2e-runner" 命令

6.2 运行 E2E 测试命令

npm run test:e2e
-

6.3 在真机中运行 E2E

  1. 真机运行 E2E 相当于微信开发者工具的【真机调试】,首先需要确保你的 Mpx 构建输出产物是 prod 模式下的产物;
  2. 修改 .e2erc.js 中的 needRealMachine 字段为 true,再执行 npm run test:e2e 命令; -静等微信开发者工具完成真机调试二维码的构建完成,待完成后用真机扫描二维码即可开始;

注:目前因为微信 automator 的限制,包括 wait 接口请求等能力暂时在真机上不能支持,这些问题目前已经反馈到微信,有进展我们会及时更新!

七、测试报告

在 .e2erc.js 中配置 report-plugin(详情见.e2erc.js 配置文件部分) 即可在所有 spec 运行结束后自动打开浏览器并呈现测试结果,测试结果包含以下三部分:

1) Jest 数据 -2) 自动截图: -截图分析:在 Mpx-E2E 平台下,点击【截图】Tab 即可,截图以 spec 文件为维度展示,目前包含三种自动时机的截图:定时、点击时、路由发生时,默认全部展示。 -截图筛选:通过操作 timeout/tap/router 的复选框即可实现分类查看,另外在点击时的自动截图还会对点击位置进行标记; -3)JSError 分析 -如果运行过程中小程序抛出 JS 异常,此时会触发 Mpx-E2E 的收集动作,同时会抓取报错瞬间的截图

未来可视化平台会持续集成更多功能,可以涵盖业务稳定,数据稳定(埋点测试)和性能数据,持续更新...

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/tool/ts.html b/docs-vuepress/.vuepress/dist/guide/tool/ts.html deleted file mode 100644 index bcc732ee83..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/tool/ts.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - 使用TypeScript开发小程序 | Mpx框架 - - - - - - - - - -

# 使用TypeScript开发小程序

# 什么是TypeScript

TypeScript 是一个开源的编程语言,通过在 JavaScript(世界上最常用的语言之一) 的基础上添加静态类型定义构建而成。

类型提供了一种描述对象形状的方法。可以帮助提供更好的文档,还可以让 TypeScript 验证你的代码可以正常工作。

在 TypeScript 中,不是每个地方都需要标注类型,因为类型推断允许您无需编写额外的代码即可获得大量功能。

# TypeScript优势

  1. 静态类型检查 -静态类型检查可以避免很多不必要类型的错误,在编译阶段提前发现问题;

  2. 强大的类型推断能力 -除了类型声明外,TypeScript 提供了强大的类型推断能力,该能力能够大大减少我们需要编写的类型声明,有效地减少我们使用 TypeScript 的额外压力;

  3. IDE 智能提示 -目前主流的 IDE 都对 TypeScript 提供了良好的支持,基于 TypeScript 的类型系统提供友好准确的编码提示与错误检查。

# 使用方式

# 编写ts前的准备工作

由于对 store 做类型推导使用了最新的 TypeScript 特性,因此需要将编辑器的 TypeScript 版本升级至 4.1.3 及以上版本。以下是 VSCode 配置示例:

  1. 更新项目中的TypeScript为 4.1.3 及以上版本。

  2. 在VSCode中打开一个 .js/.ts/.tsx 后缀的文件,使用 Shift+Command+P 唤出 VSCode 命令行,输入 TypeScript ,选择 Select TypeScript Version,选择使用工作区版本,将版本切换至 4.1.3 及以上版本。

  1. 在 VSCode 编辑器中安装 mpx 插件,来支持在.mpx单文件中编写 ts 时的代码提示和实时报错等优秀体验。

WARNING

使用 @typescript/eslint-plugin 对 ts 代码进行检查,当在.mpx文件中使用全局类型时,eslint 会抛出 no-undef 错误,可以关闭相关 eslint 规则校验

# .mpx中编写ts(推荐)

目前 Mpx 已经支持在.mpx文件的 script 标签中编写 ts 代码,需要在 script 标签上添加 lang="ts" 属性,在编译时会自动这部分 script 中的内容进行 ts 类型检查。

<script lang="ts">
-// 内联编写ts代码
-</script>
-

当然,由于大部分IDE对 ts 的语法支持都只对 .ts 和 .d.ts 文件生效,因此 Mpx 也支持创建一个 .ts 文件进行 ts 代码编写,在.mpx文件中,通过 src 的方式引入。

<script lang="ts" src="./index.ts"></script>
-

# 为.ts文件添加loader

在 Webpack 配置中添加如下 rules 以配置 ts-loader

{
-  test: /\.ts$/,
-  use: [
-    'babel-loader',
-    'ts-loader'
-  ]
-}
-

# 编写tsconfig.json文件

对相关配置不熟悉的同学可以直接采用下面配置,能够最大限度发挥 Mpx 中强大的 ts 类型推导能力

{
-  "compilerOptions": {
-    "target": "es6",
-    "allowJs": true,
-    "noImplicitThis": true,
-    "noImplicitAny": true,
-    "strictNullChecks": true,
-    "moduleResolution": "node",
-    "lib": [
-      "dom",
-      "es6",
-      "dom.iterable"
-    ]
-  }
-}
-

# 增强类型

如果需要增加 Mpx 的属性和选项,可以自定义声明 TypeScript 补充现有的类型。

例如,首先创建一个 types.d.ts 文件

// types.d.ts
-
-import { Mpx } from '@mpxjs/core'
-
-declare module '@mpxjs/core' {
-  // 声明为 Mpx 补充的属性
-  interface Mpx {
-    $myProperty: string
-  }
-}
-

之后在任意文件只需引用一次 types.d.ts 声明文件即可,例如在 app.mpx 中引用

// app.mpx
-
-/// <reference path="./types.d.ts" />
-import mpx from '@mpxjs/core'
-
-mpx.$myProperty = 'my-property'
-

# 类型推导及注意事项

Mpx 基于泛型函数提供了非常方便用户使用的反向类型推导能力,简单来说,就是用户可以用非常接近于 js 的方式调用 Mpx 提供的 api ,就能够获得大量基于用户输入参数反向推导得到的类型提示及检查。但是由于 ts 本身的能力限制,我们在 Mpx 的运行时中添加了少量辅助函数和变种api,便于用户最大程度地享受反向类型推导带来的便利性,简单的使用示例如下:

import {createComponent, getMixin, createStoreWithThis} from '@mpxjs/core'
-
-// createStoreWithThis作为createStore的变种方法,主要变化在于定义getters,mutations和actions时,
-// 获取自身的state,getters等属性不再通过参数传入,而是通过this.state或者this.getters等属性进行访问,
-// 由于TS的能力限制,getters/mutations/actions只有使用对象字面量的方式直接传入createStoreWithThis时
-// 才能正确推导出this的类型,当需要将getters/mutations/actions拆解为对象编写时,
-// 需要用户显式地声明this类型,无法直接推导得出。
-
-const store = createStoreWithThis({
-  state: {
-    aa: 1,
-    bb: 2
-  },
-  getters: {
-    cc() {
-      return this.state.aa + this.state.bb
-    }
-  },
-  actions: {
-    doSth3() {
-      console.log(this.getters.cc)
-      return false
-    }
-  }
-})
-
-createComponent({
-  data: {
-    a: 1,
-    b: '2'
-  },
-  computed: {
-    c() {
-      return this.b
-    },
-    d() {
-      // data, mixin, computed中定义的数据能够被推导到this中
-      return this.a + this.aaa + this.c
-    },
-    // 从store上map过来的计算属性或者方法同样能够被推导到this中
-    ...store.mapState(['aa'])
-  },
-  mixins: [
-    // 使用mixins,需要对每一个mixin子项进行getMixin辅助函数包裹,支持嵌套mixin
-    getMixin({
-      computed: {
-        aaa() {
-          return 123
-        }
-      },
-      methods: {
-        doSth() {
-          console.log(this.aaa)
-          return false
-        }
-      }
-    })
-  ],
-  methods: {
-    doSth2() {
-      this.a++
-      console.log(this.d)
-      console.log(this.aa)
-      this.doSth3()
-    },
-    ...store.mapActions(['doSth3'])
-  }
-})
-

# getMixin

为 ts 项目提供的反向推导 mixin 的辅助函数,getMixin 接收一个对象作为参数,使用 mixins 时,支持嵌套 mixin。具体用法见getMixin

# createStoreWithThis

为了在 ts 项目中更好的支持 store 的类型推导,提供了 createStoreWithThis 进行 store 的创建,createStoreWithThis 接收一个 options 对象作为参数。通过 createStoreWithThis 创建 store 时,使用this.statethis.gettersthis.commit, this.dispatch对自身的相关属性进行获取。具体用法见createStoreWithThis

# createStateWithThis

创建一个 state,支持 state 类型推导的辅助函数,具体用法见createStateWithThis

# createGettersWithThis

创建一个 getters,支持 getters 类型推导的辅助函数,具体用法见createGettersWithThis

# createMutationsWithThis

创建一个 mutations,支持 mutations 类型推导的辅助函数,具体用法见createMutationsWithThis

# createActionsWithThis

创建一个 actions,支持 actions 类型推导的辅助函数,具体用法见createActionsWithThis

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/tool/unit-test.html b/docs-vuepress/.vuepress/dist/guide/tool/unit-test.html deleted file mode 100644 index ab9f9a8676..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/tool/unit-test.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - 单元测试 | Mpx框架 - - - - - - - - - -

# 单元测试

Mpx 框架提供了 jest 转换器 mpx-jest,结合微信小程序提供的 miniprogram-simulate (opens new window) 来进行单元测试的工作。

因为目前仅微信提供了仿真工具,暂时只支持微信小程序平台的单元测试。如果需要 E2E 测试,则和框架无关了,可参考微信的小程序自动化 (opens new window)

如果是初始化项目,单元测试相关的项目依赖和配置可以通过 @mpx/cli 创建项目时选择使用单元测试选项自动生成,如果时旧项目需要使用,可以按照下方步骤安装依赖和添加配置。

# 安装依赖

npm i -D @mpxjs/mpx-jest @mpxjs/miniprogram-simulate jest babel-jest
-
-// 如果项目使用了ts,则还需要安装
-npm i -D ts-jest
-

# jest 相关配置

首先在项目根目录创建 jest.config.js 配置文件,并加入以下关键配置

  testEnvironment: 'jsdom', // 使用 jsdom 环境
-  transform: {
-    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
-    '^.+\\.mpx$': '<rootDir>/node_modules/@mpxjs/mpx-jest',
-    '^.+\\.ts$': '<rootDir>/node_modules/ts-jest' // 如果没使用 ts 可不用添加
-  },
-  setupFiles: ['<rootDir>/test/setup'], // test 文件夹下声明 setup,路径可以随意定义,可以为每一个单测添加相应的配置
-  transformIgnorePatterns: ['node_modules/(?!(@mpxjs))'], // 定义node_modules 中需要进行 transform 的内容
-
-

# 简单的断言

暂时进行一个简单的组件单元测试书写,对于复杂组件以及通用测试逻辑的总结我们会在后续进行发布。

示例如下:

<template>
-  <view>{{ message }}</view>
-</template>
-
-<script>
-  import {createComponent} from '@mpxjs/core'
-  createComponent({
-    data: {
-      message: 'hello!'
-    },
-    attached () {
-      this.message = 'bye!'
-    }
-  })
-</script>
-

对应的 hello-world.spec.js

const simulate = require('@mpxjs/miniprogram-simulate')
-
-// 这里是一些 Jasmine 2.0 的测试,你也可以使用你喜欢的任何断言库或测试工具。
-describe('MyComponent', () => {
-  let id
-  beforeAll(() => {
-    id = simulate.loadMpx('<rootDir>/src/components/hello-world.mpx')
-  })
-
-  // 检查 mount 中的组件实例
-  it('correctly sets the message when component attached', () => {
-    const comp = simulate.render(id)
-    const instance = comp.instance
-    
-    // Mpx提供的数据响应是发生在组件挂载时的,未挂载前只能通过实例上的data访问数据
-    expect(instance.data.message).toBe('hello!')
-    
-    const parent = document.createElement('parent-wrapper') // 创建容器节点
-    comp.attach(parent) // 将组件插入到容器节点中,会触发 attached 生命周期
-    // 挂载后则可以直接通过实例访问
-    expect(instance.message).toBe('bye!')
-  })
-
-  // 创建一个实例并检查渲染输出
-  it('renders the correct message', () => {
-    const comp = simulate.render(id)
-    const parent = document.createElement('parent-wrapper') // 创建容器节点
-    comp.attach(parent) // 挂载组件到容器节点
-    expect(comp.dom.innerHTML).toBe('<wx-view>bye!</wx-view>')
-  })
-})
-

# 编写可被测试的组件

很多组件的渲染输出由它的 props 决定。事实上,如果一个组件的渲染输出完全取决于它的 props,那么它会让测试变得简单,就好像断言不同参数的纯函数的返回值。看下面这个例子:

<template>
-  <view>{{ msg }}</view>
-</template>
-
-<script>
-  import {createComponent} from '@mpxjs/core'
-  createComponent({
-    properties: { msg: String }
-  })
-</script>
-

你可以在不同的 properties 中,通过 simulate.render 的第二个参数控制组件的输出:

const simulate = require('@mpxjs/miniprogram-simulate')
-
-// 省略辅助方法
-describe('MyComponent', () => {
-  it('renders correctly with different props', () => {
-    const id = simulate.loadMpx('<rootDir>/src/components/hello-world.mpx')
-    const comp1 = simulate.render(id, { msg: 'hello' })
-    const parent1 = document.createElement('parent-wrapper')
-    comp1.attach(parent1)
-    expect(comp1.dom.innerHTML).toBe('<wx-view>hello</wx-view>')
-    
-    const comp2 = simulate.render(id, { msg: 'bye' })
-    const parent2 = document.createElement('parent-wrapper')
-    comp2.attach(parent2)
-    expect(comp2.dom.innerHTML).toBe('<wx-view>bye</wx-view>')
-  })
-})
-

# 断言异步更新

小程序视图层的更新是异步的,一些依赖视图更新结果的断言必须 await simulate.sleep() 或者 await comp.instance.$nextTick() 后进行:

const simulate = require('@mpxjs/miniprogram-simulate')
-
-// 省略辅助方法
-it('updates the rendered message when vm.message updates', async () => {
-  const id = simulate.loadMpx('<rootDir>/src/components/hello-world.mpx')
-  const comp = simulate.render(id)
-  const parent = document.createElement('parent-wrapper')
-  comp.attach(parent)
-  comp.instance.msg = 'foo'
-  await simulate.sleep(10)
-  expect(comp.dom.innerHTML).toBe('<wx-view>foo</wx-view>')
-})
-

更深入的 Mpx 单元测试的内容将在以后持续更新……

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/understand/compile.html b/docs-vuepress/.vuepress/dist/guide/understand/compile.html deleted file mode 100644 index a234b49880..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/understand/compile.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - Mpx编译构建原理 | Mpx框架 - - - - - - - - - -

# Mpx编译构建原理

我们希望使用目前设计最强大、生态最完善的编译构建工具Webpack来实现小程序的编译构建,让用户得到web开发中先进强大的工程化开发体验。使用过Webpack的同学都知道,通常来说Webpack都是将项目中使用到的一系列碎片化模块打包为一个或几个bundle,而小程序所需要的文件结构是非常离散化的,如何调解这两者的矛盾成为了我们最大的难题。一种非常直观简单的思路在于遍历整个src目录,将其中的每一个.mpx文件都作为一个entry加入到Webpack中进行处理,这样做的问题主要有两个:

  1. src目录中用不到的.mpx文件也会被编译输出,最终也会被小程序打包进项目包中,无意义地增加了包体积;
  2. 对于node_modules下的.mpx文件,我们不认为遍历node_modules是一个好的选择。

最终我们采用了一种基于依赖分析和动态添加entry的方式来进行实现,用户在Webpack配置中只需要配置一个入口文件app.mpx,loader在解析到json时会解析json中pages域和usingComponents域中声明的路径,通过动态添加entry的方式将这些文件添加到Webpack的构建系统当中(注意这里是添加entry而不是添加依赖,因为只有entry能生成独立的文件,满足小程序的离散化文件结构),并递归执行这个过程,直到整个项目中所有用到的.mpx文件都加入进来,在输出前,我们借助了CommonsChunkPlugin/SplitChunksPlugin的能力将复用的模块抽取到一个外部的bundle中,确保最终生成的包中不包含重复模块。我们提供了一个Webpack插件和一个.mpx文件对应的loader来实现上述操作,用户只需要将其添加到Webpack配置中就可以以打包web项目的方式正常打包小程序,没有任何的前置和后置操作,支持Webpack本身的完整生态。

Mpx编译构建机制流程示意图

Mpx编译构建机制流程示意图

# 分包处理

构建时对于非JS资源,是所有的包串行处理,资源map会精确记录每个包中引用了哪些资源。

在主包的处理过程中,将主包页面中引用的所有非js资源(组件、外部样式、外部模板、wxs,图像媒体等)都记录下来,在处理分包时,对分包内引用的非js资源都进行检查,如果被主包引用过则输出到主包中,否则标记为分包only的资源输出到分包目录下。

由于主包和分包限制相同,一般情况下分包体积远小于主包体积 -,而微信小程序的体积限制策略主要卡主包体积不得超过2M,因此,Mpx设定的是主包体积优先策略,即尽可能减小主包体积,为此,会将分包资源尽可能打到分包去。

组件和静态资源的输出规则如下:

  1. 主包引用的资源输出至主包
  2. 分包引用且主包引用过的资源输出至主包,不在当前分包重复输出
  3. 分包引用且无其他包引用的资源输出至当前分包
  4. 分包引用且其他分包也引用过的资源,重复输出至当前分包
  5. 当用户通过packageName query显式指定了资源的所属包时,输出至指定的包

如此复杂的分包策略也完全是为了尽可能良好地支持处理分包引用npm资源,因此不管在主包还是分包中,使用Mpx框架开发小程序,可以享受最舒适最自然最好用的npm机制。

而小程序原生的分包是不具备这个能力,只能引用自己分包下的资源,同时大部分基于web技术而来的转译型框架应该也都是不会考虑这个部分的。

甚至如果出现这种场景:同一个组件在A分包和B分包中都有使用,但未在主包使用,会将这个组件分别放入A分包和B分包而不是主包。(是否需要这样可自行斟酌,如果不希望出现这种情况,可在主包中引入一次该组件就会被收到主包去,全局就这一份了)

总结一下:

对于组件,若同时被多个分包引用,主包未引用,会有多份分包存在于各分包中。一旦被主包使用,就仅有一份存在于主包中。

对于资源(例如纯JS模块),若被主包使用,会被收集到主包里。若被多个分包使用,会被收集进主包的bundle里。若仅被某分包使用,就会在分包的bundle.js里(视模块大小及引用次数也可能被内联到某个组件JS里)。

进阶用户有更花式的需求可通过packageName query来显式指定输出。

- - - diff --git a/docs-vuepress/.vuepress/dist/guide/understand/runtime.html b/docs-vuepress/.vuepress/dist/guide/understand/runtime.html deleted file mode 100644 index ae5408c4ff..0000000000 --- a/docs-vuepress/.vuepress/dist/guide/understand/runtime.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - Mpx运行时增强原理 | Mpx框架 - - - - - - - - - -

# Mpx运行时增强原理

数据响应作为Vue最核心的特性,在我们的日常开发中被大量使用,能够极大地提高前端开发体验和效率,我们在框架设计初期最早考虑的就是如何将数据响应特性加入到小程序开发中。在数据响应的实现上,我们引入了MobX,一个实现了纯粹数据响应能力的知名开源项目。借助MobX和mixins,我们在小程序组件创建初期建立了一个响应式数据管理系统,该系统观察着小程序组件中的所有数据(data/props/computed)并基于数据的变更驱动视图的渲染(setData)及用户注册的watch回调,实现了Vue中的数据响应编程体验。与此同时,我们基于MobX封装实现了一个Vuex规范的数据管理store,能够方便地注入组件进行全局数据管理。为了提高跨团队开发的体验,我们对store添加了多实例可合并的特性,不同团队维护自己的store,在需要时能够合并他人或者公共的store生成新的store实例,我们认为这是一种比Vuex中modules更加灵活便捷的跨团队数据管理模式

作为一个接管了小程序setData的数据响应开发框架,我们高度重视Mpx的渲染性能,通过小程序官方文档中提到的性能优化建议可以得知,setData对于小程序性能来说是重中之重,setData优化的方向主要有两个:

  1. 尽可能减少setData调用的频次
  2. 尽可能减少单次setData传输的数据

为了实现以上两个优化方向,我们做了以下几项工作:

  • 将组件的静态模板编译为可执行的render函数,通过render函数收集模板数据依赖,只有当render函数中的依赖数据发生变化时才会触发小程序组件的setData,同时通过一个异步队列确保一个tick中最多只会进行一次setData,这个机制和Vue中的render机制非常类似,大大降低了setData的调用频次;
  • 将模板编译render函数的过程中,我们还记录输出了模板中使用的数据路径,在每次需要setData时会根据这些数据路径与上一次的数据进行diff,仅将发生变化的数据通过数据路径的方式进行setData,这样确保了每次setData传输的数据量最低,同时避免了不必要的setData操作,进一步降低了setData的频次。

Mpx数据响应机制流程示意图

Mpx数据响应机制流程示意图

- - - diff --git a/docs-vuepress/.vuepress/dist/index.html b/docs-vuepress/.vuepress/dist/index.html deleted file mode 100644 index d44770cb87..0000000000 --- a/docs-vuepress/.vuepress/dist/index.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - Mpx框架 - - - - - - - - - -

增强型跨端小程序框架

- 良好的开发体验,极致的应用性能,完整的原生兼容,一份源码跨端输出所有小程序平台及Web。 -

  • svg

    高性能

    Mpx高度关注小程序性能与包体积,深度整合了运行时性能优化与包体积分析优化能力,让开发者在大部分场景下只需专注于业务开发,就能生产出媲美甚至超出原生的高性能小程序应用。

  • svg

    优体验

    以增强的方式将Vue中优良的开发特性引入到小程序开发中,如数据响应、组合式api等,配合强大的工程化能力,大大提升了小程序开发的体验与效率,同时保障了框架开发的可维护性与可预期性。

  • svg

    跨平台

    Mpx专注解决小程序跨端问题,通过静态转译与运行时适配结合,将一份源码跨端输出到所有开放的小程序平台和web环境下运行,同时最大限度减少跨端带来的性能与包体积损失。

phone

示例项目

- 扫码体验Mpx版本的 - todoMVC - 在各个小程序平台和web中的一致表现 ,更多示例项目可点击 - 这里 - 进入查看。 -

code
code
svg

极致性能

- 得益于增强的设计思路,Mpx在运行时没有复杂的封装抹平逻辑,而是专注于实现数据响应,setData优化和Composition api等关键增强能力,压缩后体积占用仅为60KB;配合编译构建中灵活强大的包体积分析优化能力,Mpx在性能与包体积方面做到了业内最优。 -

渐进迁移

- 同样得益于增强的设计思路,Mpx能够完整兼容小程序原生技术规范,并以较低的成本进行持续跟进;借助框架提供的渐进迁移能力,小程序开发者可以方便地在Mpx项目中使用已有的原生开发生态,如组件库,统计工具等,同时也能将Mpx开发的组件输出到原生小程序项目中使用。 -

svg

成功案例

- 滴滴出行 -

phone
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
img
left
二维码
滴滴出行
二维码
青桔单车
二维码
滴滴顺风车
二维码
出行广场
二维码
滴滴公交
二维码
滴滴金融
二维码
滴滴外卖
二维码
司机招募
二维码
小桔加油
二维码
番薯借阅
二维码
疫查查应用
二维码
小桔养车
二维码
学而思直播课
二维码
小猴启蒙课
二维码
科创书店
二维码
在武院
二维码
三股绳Lite
二维码
学而思优选课
二维码
食享会
二维码
青铜安全医生
二维码
青铜安全培训
二维码
视穹云机械
二维码
店有生意通
二维码
花小猪打车
二维码
橙心优选
二维码
小二押镖
二维码
顺鑫官方微商城
二维码
嘀嗒出行
二维码
汉行通Pro
二维码
滴滴出行(支付宝)
二维码
小桔充电(支付宝)
二维码
唯品会QQ
二维码
唯品会(百度)
二维码
唯品会(字节)
right
- - - diff --git a/docs-vuepress/.vuepress/dist/logo.png b/docs-vuepress/.vuepress/dist/logo.png deleted file mode 100644 index c5e0693f74a5f2fbf80a8f7b7c73684a83f687b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13635 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 diff --git a/docs-vuepress/.vuepress/dist/manifest.webmanifest b/docs-vuepress/.vuepress/dist/manifest.webmanifest deleted file mode 100644 index dcc89dfc8b..0000000000 --- a/docs-vuepress/.vuepress/dist/manifest.webmanifest +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Mpx", - "short_name": "Mpx", - "icons": [ - { - "src": "https://dpubstatic.udache.com/static/dpubimg/1ESVodfAED/logo.png", - "sizes": "192x192", - "type": "image/png" - } - ], - "start_url": "/index.html", - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} \ No newline at end of file diff --git a/docs-vuepress/.vuepress/dist/rn-api.html b/docs-vuepress/.vuepress/dist/rn-api.html deleted file mode 100644 index ff2acf3a1a..0000000000 --- a/docs-vuepress/.vuepress/dist/rn-api.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - API支持列表 | Mpx框架 - - - - - - - - - -

# API支持列表

mpx转RN的对标微信api目前支持及部分的api转换,目前支持的能力可以参考下表:

支持方法
getSystemInfo
getSystemInfoSync
getDeviceInfo
getWindowInfo
request
setStorage
removeStorage
removeStorageSync
getStorage
getStorageInfo
clearStorage
clearStorageSync
setClipboardData
getClipboardData
makePhoneCall
onWindowResize
offWindowResize
arrayBufferToBase64
base64ToArrayBuffer
connectSocket
getNetworkType
onNetworkStatusChange
offNetworkStatusChange

# 使用说明

对于一些api-proxy中没有提供的能力,用户可以搭配mpx对象方式传入custom使用即可示例如下:

import mpx from '@mpxjs/core'
-import apiProxy from '@mpxjs/api-proxy'
-import { showModal } from '@test/showModal'
-
-mpx.use(apiProxy, {
-  custom: {
-    ios: {
-      showModal
-    },
-    android: {
-      showModal
-    }
-  }
-})
-
-mpx.showModal({
-  title: '标题',
-  content: '这是一个弹窗',
-  success (res) {
-    console.log('弹框展示成功')
-  }
-})
-

# API抹平差异详情

对于一些微信独有的返回结果,或者RN目前不能支持的入参/返回结果,在下面会有详细说明:

# getSystemInfo/getSystemInfoSync

不支持返回值
language
version
SDKVersion
benchmarkLevel
albumAuthorized
cameraAuthorized
locationAuthorized
microphoneAuthorized
notificationAuthorized
phoneCalendarAuthorized
host
enableDebug
notificationAlertAuthorized
notificationBadgeAuthorized
notificationSoundAuthorized
bluetoothEnabled
locationEnabled
wifiEnabled
locationReducedAccuracy
theme

# getDeviceInfo

不支持返回值
benchmarkLevel
abi
cpuType

# getWindowInfo

不支持返回值
screenTop

# request

不支持入参
useHighPerformanceMode
enableHttp2
enableProfile
enableQuic
enableCache
enableHttpDNS
httpDNSServiceId
enableChunked
forceCellularNetwork
redirect
不支持返回值
cookies
profile
exception

# setStorage/getStorage

不支持入参
encrypt

# getStorageInfo

不支持返回值
currentSize
limitSize

# connectSocket

不支持入参
tcpNoDelay
perMessageDeflate
timeout
forceCellularNetwork

# getNetworkType

不支持返回值
signalStrength
hasSystemProxy
- - - diff --git a/docs-vuepress/.vuepress/dist/rn-component.html b/docs-vuepress/.vuepress/dist/rn-component.html deleted file mode 100644 index 71bec22783..0000000000 --- a/docs-vuepress/.vuepress/dist/rn-component.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - RN 自定义组件支持 | Mpx框架 - - - - - - - - - -

# RN 自定义组件支持

创建自定义组件在 RN 环境下部分实例方法、属性存在兼容性问题不支持, -此文档以微信小程序为参照,会详细列出各方法、属性的支持度。

# 参数

# 组件定义属性说明

属性 类型 RN 是否支持 描述
properties Object Map 组件的对外属性,是属性名到属性设置的映射表
data Object 组件的内部数据,和 properties 一同用于组件的模板渲染
observers Object 组件数据字段监听器,用于监听 propertiesdata 的变化
methods Object 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用
behaviors String Array 输出 RN 不支持
created Function 组件生命周期函数-在组件实例刚刚被创建时执行,注意此时不能调用 setData
attached Function 组件生命周期函数-在组件实例进入页面节点树时执行
ready Function 组件生命周期函数-在组件布局完成后执行
moved Function RN 不支持,组件生命周期函数-在组件实例被移动到节点树另一个位置时执行
detached Function 组件生命周期函数-在组件实例被从页面节点树移除时执行
relations Object 输出 RN 不支持
externalClasses String Array 输出 RN 不支持
options Object Map 输出 RN 不支持,一些选项,诸如 multipleSlots、virtualHost、pureDataPattern,这些功能输出 RN 不支持
lifetimes Object 组件生命周期声明对象
pageLifetimes Object 组件所在页面的生命周期声明对象

# 组件实例属性与方法

生成的组件实例可以在组件的方法、生命周期函数中通过 this 访问。组件包含一些通用属性和方法。

属性名 类型 RN 是否支持 描述
is String 输出 RN 暂不支持,未来支持, 组件的文件路径
id String 节点id
dataset String 节点dataset
data Object 组件数据,通过 this 直接访问
properties Object 组件props,通过 this 直接访问
router Object 输出 RN 暂不支持
pageRouter Object 输出 RN 暂不支持
renderer string 输出 RN 暂不支持

微信小程序原生方法

方法名 RN是否支持 参数 描述
setData Object newData 设置data并执行视图层渲染
hasBehavior Object behavior 检查组件是否具有 behavior
triggerEvent String name, Object detail, Object options 触发事件
createSelectorQuery 输出 RN 暂不支持,未来支持,建议使用 ref
createIntersectionObserver 输出 RN 暂不支持
selectComponent String selector 输出 RN 暂不支持,未来支持,建议使用 ref
selectAllComponents String selector 输出 RN 暂不支持,未来支持,建议使用 ref
selectOwnerComponent 输出 RN 不支持
getRelationNodes String relationKey 输出 RN 不支持
groupSetData Function callback 输出 RN 不支持
getTabBar 输出 RN 不支持
getPageId 输出 RN 不支持
animate String selector, Array keyframes, ... 输出 RN 不支持
clearAnimation String selector, Object options, ... 输出 RN 不支持

Mpx 框架增强实例方法

方法名 RN是否支持 描述
$set 向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新
$watch 观察 Mpx 实例上的一个表达式或者一个函数计算结果的变化
$delete 删除对象属性,如果该对象是响应式的,那么该方法可以触发观察器更新(视图更新
$refs 一个对象,持有注册过 ref的所有 DOM 元素和组件实例,调用响应的组件方法或者获取视图节点信息。
$asyncRefs 输出 RN 不支持
$forceUpdate 用于强制刷新视图,不常用,通常建议使用响应式数据驱动视图更新
$nextTick 在下次 DOM 更新循环结束之后执行延迟回调函数,用于等待 Mpx 完成状态更新和 DOM 更新后再执行某些操作
$i18n 输出 RN 暂不支持,国际化功能访问器,用于获取多语言字符串资源
$rawOptions 访问组件原始选项对象
- - - diff --git a/docs-vuepress/.vuepress/dist/rn-style.html b/docs-vuepress/.vuepress/dist/rn-style.html deleted file mode 100644 index 10589cd7ff..0000000000 --- a/docs-vuepress/.vuepress/dist/rn-style.html +++ /dev/null @@ -1,331 +0,0 @@ - - - - - - Mpx转RN样式使用指南 | Mpx框架 - - - - - - - - - -

# Mpx转RN样式使用指南

RN 的样式的支持基本为 web 样式的一个子集,同时还有一些属性并未与 web 对齐,因此跨平台输出 RN 时,为了保障多端输出样式一致,可参考本文针对样式在RN上的支持情况来进行样式编写。

# 样式编写限制

# 选择器

RN 环境下仅支持以下类名选择器,不支持逗号之外的组合选择器。

/* 支持 */
-.classname {
-  color: red
-}
-.classA, .classB {
-    color: red
-}
-

# 布局

在 RN 中布局方式有限制,像 block inline inline-block 和 fixed 等都不支持,支持的布局方式如下:

# flex

在RN中,常规的组件元素可以用过 flex 布局来实现,参考文档 (opens new window)

:RN中view标签的主轴是column,和css不一致,使用mpx开发设置dispay:flex时,会默认的主轴方向是row。

# relative/absolute

在 RN 中 position 仅支持 relative(默认)和 absolute,可参考文档 (opens new window)

# 样式单位

# number 类型值

RN 环境中,number 数值型单位支持 px rpx % 三种,web 下的 vw em rem 等不支持。

# color 类型值

RN 环境支持大部分 css 中 color 定义方式,仅少量不支持,详情参考 RN 文档 https://reactnative.dev/docs/colors

# 组件样式规则

在web和RN下组件的渲染会存在一些差异的地方,比如默认样式不一致、继承的关系不一致等。框架针对这些不一致进行了抹平工作,但是仍旧存在一些使用限制,具体参考以下组件节点的介绍。

# text

RN中,文本节点需要通过Text组件来创建文本节点。文本节点需要给Text组件来设定属性 (opens new window)来调整文本的外观。

Web/小程序中,文本节点可以通过div/view节点进行直接包裹,在div/view标签上设定对应文本样式即可。不需单独包裹text节点。

框架抹平了此部分的差异,但仍因受限于RN内text的样式继承原则的限制 (opens new window),通过在祖先节点来设置文本节点的样式仍旧无法生效。具体例子说明如下

<view class="wrapper">
-    <view class="content">我是文本</view>
-</view>
-
-.wrapper {
-    font-size: 20px;
-}
-.content {
-    text-align: right;
-}
-/** 以上例子中
-web渲染效果: 字体的大小为20px,文字居右 
-转RN之后渲染效果: 字体大小为默认大小,文字居右
-*/
-
-

限制与使用说明

  1. 无法通过设置祖先节点的样式来修改文本节点的样式,只可通过修改直接包裹文本的节点来修改文本的样式。
  2. 框架处理将文本节点样式的默认值与web进行了对齐,如果需要按照RN的默认值来进行渲染,可设置disable-default-styletrue

# view

background、background-image、background-size、background-repeat仅在view标签下支持,background-color在其他标签上也支持。

各属性支持的类型

  • background - 仅支持 background-image | background-color | background-size | background-repeat,每个属性支持情况,可参考下面的介绍。
  • background-image - 仅支持 <url()>。
  • background-size - 支持px|rpx|%,也支持枚举值 contain|cover|auto; 不支持两个以上的值进行设置。
  • backgroundno-repeat - 仅支持值为no-repeat。
  • backfround-color - 参考Color (opens new window)

支持的语法


-/** 1. background **/
-/* 支持 */
-background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink contain no-repeat;
-background: #000;
-background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink;
-
-/* 不支持 */
-background: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
-
-
-/** 2. background-image **/
-/* 支持 */
-background-image: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg");
-/* 不支持 */
-background-image: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
-
-
-/** 3. background-size **/
-/* 支持 */
-background-size: 50%;
-background-size: 50% 25%;
-background-size: contain;
-background-size: cover;
-background-size: auto;
-background-size: 20px auto;
-
-/ * 不支持 * /
-background-size: 50%, 25%, 25%;
-
-
-/** 4. background-repeat **/
-/* 支持 */
-background-repeat: no-repeat;
-
-/* 不支持 */
-background-repeat: repeat;
-
-
-/** 5. background-color **/
-background-repeat: red;
-

# 样式参考

# Layout Style

# position

设置元素的定位样式 -值支持类型 -enum: absolute, relative, 默认relative。

# 语法
position: absolute;
-position: relative;
-
-

# top|right|left|bottom

设置元素的不同方向的偏移量

# 值支持类型

number: px,rpx,%

# 语法
top: 10px;
-top: 10rpx;
-top: 10%;
-

# z-index

控制着元素的堆叠覆盖顺序。

# 值支持类型

number

# 语法
z-index: 1;
-

# display

设置元素的布局方式。

# 值支持类型

flex/none

# 语法
/* 默认 */ 
-display:flex
-/* 隐藏 */
-display:none
-

# align-content

设置多根轴线的对齐方式。

# 值支持类型

enum: flex-start, flex-end, center, stretch, space-between, space-around, space-evenly

# 语法
/** 支持 **/
-align-content: flex-start;
-align-content: flex-end;
-align-content: center;
-align-content: stretch;
-align-content: space-between;
-align-content: space-around;
-align-content: space-evenly;
-
-/** 不支持 **/
-align-content: safe center;
-align-content: unsafe center;
-

# align-items

设置单根轴线上的子元素的对齐方式。默认会是交叉轴上的。

# 值支持类型

enum: flex-start, flex-end, center, stretch, baseline

# 语法
/** 支持 **/
-align-items: flex-start;
-align-items: flex-end;
-align-items: center;
-align-items: stretch;
-align-items: baseline;
-
-/** 不支持 **/
-align-items: first baseline;
-align-items: last baseline;
-

# align-self

设置单个子元素在单根轴线上的对齐方式

# 值支持类型

enum: auto, flex-start, flex-end, center, stretch, baseline

# 语法
/** 支持 **/
-align-self: auto;
-align-self: flex-start;
-align-self: flex-end;
-align-self: center;
-align-self: stretch;
-align-self: baseline;
-
-/** 不支持 **/
-align-self: first baseline;
-align-self: last baseline;
-

# flex

仅支持 flex-grow | flex-shrink | flex-basis 这种顺序,值以空格分隔按顺序赋值

# 值支持类型

flex: number number px/rpx

# 语法
/** 简写 **/
-flex: 1;
-/* flex-grow | flex-shrink | flex-basis */
-flex: 0 1 1;
-

# flex-grow

设置子盒子的放大比例

# 值支持类型

number

# 语法
flex-grow: 1;
-

# flex-shrink

设置子盒子的缩放比例。

# 值支持类型

number

# 语法
flex-shrink: 1;
-

# flex-basis

设置在分配多余空间之前,子盒子的初始主轴尺寸。

# 值支持类型

number px|rpx|%

# 语法
flex-shrink: 10px;
-flex-shrink: 10rpx;
-flex-shrink: 20%;
-

# flex-direction

设置主轴的方向。

# 值支持类型

enum: row, row-reverse, column, column-reverse

# 语法
flex-direction: row;
-flex-direction: row-reverse;
-flex-direction: column;
-flex-direction: column-reverse;
-
-

# flex-wrap

设置元素是否换行。当值为wrap时,alignItems:center不生效。

# 值支持类型

enum: wrap, nowrap, wrap-reverse

# 语法
flex-wrap: wrap;
-flex-wrap: nowrap;
-flex-wrap: wrap-reverse;
-

# flex-flow

是flex-direction flex-wrap 缩写,仅支持 flex-flow: flex-direction flex-wrap 这种顺序,值以空格分隔按顺序赋值

/* flex-direction */
-flex-flow: row;
-/* flex-direction|  flex-wrap*/
-flex-flow: row nowrap;
-

# View Style

# margin

margin 是 margin-top|margin-right|margin-left|margin-bottom 的缩写模式, 目前仅支持四种缩写模式。

# 值支持类型

string: 'auto' -number: rpx,px, %

# 语法
/* all */
-margin: 2px;
-
-/* top and bottom | left and right */
-margin: 5% auto;
-
-/* top | left and right | bottom */
-margin: 1rpx auto 2rpx;
-
-/* top | right | bottom | left */
-margin: 1rpx 2rpx 2rpx ;
-

# margin-top|margin-bottom|margin-right|margin-left

# 值支持类型

number: rpx,px, %

# 语法
margin-top: 2px;
-margin-top: 2rpx;
-margin-top: 10%;
-

# padding

padding是padding-left、padding-right、padding-left、padding-bottom的缩写模式, 目前仅支持四种缩写模式。

# 值支持类型

string: 'auto' -number: rpx,px, %

# 语法
/* all */
-padding: 2px;
-
-/* top and bottom | left and right */
-padding: 5% 10%;
-
-/* top | left and right | bottom */
-padding: 1rpx 0 2rpx;
-
-/* top | right | bottom | left */
-padding: 1rpx 2rpx 2rpx ;
-

# padding-top|padding-bottom|padding-left|padding-right

# 值支持类型

number: rpx,px, %

# 语法
/** padding-top **/
-padding-top: 2px;
-padding-top: 2rpx;
-padding-top: 10%;
-

# border

border 是 border-width|border-style|border-color 的缩写模式, 目前仅支持 width | style | color 这种排序,值以空格分隔按顺序赋值

/* width | style | color */
-border: 1px solid red;
-border: 1px;
-border: 1px solid;
-border: 1px double pink;
-

# border-color

设置边框的颜色, 目前只支持统一设置,不支持缩写。

# 值支持类型

color: 参考Color (opens new window)

# 语法
/* all border */
-/** 支持 **/
-border-color: red;
-

# border-style

设置边框的样式, 目前只支持统一设置,不支持缩写。

# 值支持类型

enum: solid|dotted|dashed

# 语法
/** 支持 **/
-/* all border */
-border-color: 'solid';
-border-color: 'dotted';
-border-color: 'dashed';
-
-/** 不支持 **/
-border-style: double;
-border-style: groove;
-border-style: ridge;
-border-style: dotted solid;
-border-style: hidden double dashed;
-border-style: none solid dotted dashed;
-

# border-width

设置边框的宽度,目前只支持统一设置,不支持缩写。

# 值支持类型

number: px rpx %

# 语法
/* all border */
-border-width: 2px;
-

# border-top-color|border-bottom-color|border-left-color|border-right-color

设置各边框的颜色

# 值支持类型

color: 参考Color (opens new window)

# 语法
border-top-color: red;
-

# border-top-width|border-bottom-width|border-left-width|border-right-width

设置各边框的宽度

# 值支持类型

number: px rpx

# 语法
border-top-width: 2px;
-

# border-radius

设置border的圆角格式,支持一种缩写方式

# 值支持类型

仅支持 border-radius 0px|border-radius 0px 0px 0px 0px(值以空格分隔按顺序赋值) -number: px rpx %

# 语法
/* all */
-border-radius: 2px;
-/* top-left | top-right | bottom-right | bottom-left */
-border-radius: 10px 10px 10px 0;
-

# border-bottom-left-radius|border-bottom-right-radius|border-top-left-radius|border-top-right-radius

# 值支持类型

number: px rpx %

# 语法
border-bottom-left-radius: 2px;
-

# Text Style

# color

# 值支持类型

color: 参考Color (opens new window)

# 语法
color: orange;
-color: #fff;
-color: #fafafa;
-color: rgb(255, 255, 255);
-color: rgba(255, 99, 71, 0.2)
-

# font-family

可设置系统字体,引入字体文件,暂时不支持。

# 值支持类型

string

# 语法
font-family: "PingFangSC-Regular"
-

# font-size

可设置字体的大小

# 值支持类型

number: px,rpx

# 语法
font-size: 12px;
-font-size: 12rpx;
-

# font-style

设置文本的字体样式。

# 值支持类型

enum: normal,italic

# 语法
font-style: italic;
-font-style: normal;
-

# font-weight

设置文字的权重。

# 值支持类型

enum: 100,200,300,400,500,600,800,900,normal,bold

# 语法
font-weight: 100;
-font-weight: 200;
-font-weight: 300;
-font-weight: 400;
-font-weight: 500;
-font-weight: 600;
-font-weight: 700;
-font-weight: 800;
-font-weight: 900;
-
-font-weight: normal;
-font-weight: bold;
-

# font-variant

设置文本的字体变体

# 值支持类型

enum: small-caps, oldstyle-nums, lining-nums, tabular-nums, proportional-nums

# 语法
font-variant: small-caps;
-font-variant: oldstyle-nums;
-font-variant: lining-nums;
-font-variant: tabular-nums;
-font-variant: lining-nums;
-font-variant: proportional-nums;
-

# letter-spacing

定义字符之间的间距

# 值支持类型

px,rpx

# 语法
letter-spacing: 2px;
-letter-spacing: 2rpx;
-

# line-height

设置行高。

# 值支持类型

px,rpx,%

# 语法
line-height: 16px
-line-height: 16rpx
-line-height: 100%
-line-height: 1
-

# text-align

设置文本的水平对齐方式。

# 值支持类型

enum: left, right, center, justify

# 语法
/** 支持 **/
-text-align: left;
-text-align: right;
-text-align: center;
-text-align: justify;
-
-/** 不支持 **/
-text-align: match-parent;
-text-align: auto;
-text-align: justify-all;
-

# text-decoration-line

设置文本的装饰线样式。

# 值支持类型

enum: none, underline, line-through, underline line-through

# 语法
/** 支持 **/
-text-decoration-line: none;
-text-decoration-line: underline;
-text-decoration-line: line-through;
-text-decoration-line: underline line-through;
-
-/** 不支持 **/
-text-decoration-line: overline;
-

# text-transform

设置文本的大小写转换。

# 值支持类型

enum: none, uppercase, lowercase, capitalize

# 语法
/** 支持 **/
-text-transform: none;
-text-transform: uppercase;
-text-transform: lowercase;
-text-transform: capitalize;
-
-/** 不支持 **/
-text-transform: none;
-text-transform: uppercase;
-text-transform: lowercase;
-text-transform: capitalize;
-

# text-shadow

设置文本阴影

# 值支持类型

仅支持 offset-x | offset-y | blur-radius | color 排序,值以空格分隔按顺序赋值

# 语法
text-shadow: 1rpx 3rpx 0 #2E0C02;
-

# Background Style

背景相关的属性

# background-color

表示背景色,可以在任何标签上使用。

# 值支持类型

color: 参考Color (opens new window)

# 语法
/* all border */
-background-color: red;
-

# background-image

表示背景图,只能在view上使用

# 值支持类型

仅支持 <url()>

# 语法
background-image: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg");
-
-/* 不支持 */
-background-image: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
-

# background-size

表示背景大小,只能在view上使用

# 值支持类型

number 支持 px|rpx|%,枚举值支持 contain|cover|auto; -支持一个值:这个值指定图片的宽度,图片的高度隐式的为 auto; -支持两个值:第一个值指定图片的宽度,第二个值指定图片的高度; -不支持逗号分隔的多个值:设置多重背景!!!

# 语法
/* 支持 */
-background-size: 50%;
-background-size: 50% 25%;
-background-size: contain;
-background-size: cover;
-background-size: auto;
-background-size: 20px auto;
-
-/ * 不支持 * /
-background-size: 50%, 25%, 25%;
-

# background-repeat

表示背景图是否重复,只能在view上使用

# 值支持类型

enum: no-repeat

# 语法
background-repeat: no-repeat;
-
-/* 不支持 */
-background-repeat: repeat; 
-

# background

表示背景的组合属性,只能在view上使用

# 值支持类型

仅支持 background-image | background-color | background-size | background-repeat,具体每个属性的支持情况参见上面具体属性支持的文档

# 语法
/* 支持 */
-background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink no-repeat;
-background: #000;
-background: url("https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg") pink;
-
-/* 不支持 */
-background: linear-gradient(rgba(0, 0, 255, 0.5), rgba(255, 255, 0, 0.5));
-

# Shadow Style

# box-shadow

此属性是阴影颜色、阴影的偏移量

# 值支持类型

仅支持 offset-x | offset-y | blur-radius | color 排序,值以空格分隔按顺序赋值

# 语法
/* offset-x | offset-y | blur-radius | color */
-box-shadow: 0 1px 3px rgba(139,0,0,0.32);
-

# 附录

# RN 不支持的属性

若设置以下不支持的属性会被 mpx 编译处理时丢弃,有编译 error 提示

描述 不支持的属性
双端都不支持 box-sizing white-space text-overflow animation transition
ios 不支持 vertical-align
android 不支持 text-decoration-style text-decoration-color shadow-offset shadow-opacity shadow-radius

# RN 支持的枚举值

RN 支持的枚举值映射如下表,其他不支持的枚举值会被 mpx 编译处理时丢弃,设置无效

prop value 枚举
overflow visible hidden scroll
border-style solid dotted dashed
display flex none
pointer-events auto none
position relative absolute
vertical-align auto top bottom center
font-variant small-caps oldstyle-nums lining-nums tabular-nums proportional-nums
text-align left right center justify
font-style normal italic
font-weight normal bold 100-900
text-decoration-line none underline line-through 'underline line-through'
text-transform none uppercase lowercase capitalize
user-select auto text none contain all
align-content flex-start flex-end none center stretch space-between space-around
align-items flex-start flex-end center stretch baseline
align-self auto flex-start flex-end center stretch baseline
justify-content flex-start flex-end center space-between space-around space-evenly none
background-repeat no-repeat

# 缩写支持

RN 仅支持部分常用的缩写形式,具体参加下表:

缩写属性 支持的缩写格式 备注
text-decoration 仅支持 text-decoration-line text-decoration-style text-decoration-color 顺序固定,值以空格分隔后按按顺序赋值
margin margin: 0;margin: 0 auto;margin: 0 auto 10px;margin: 0 10px 10px 20px; -
padding padding: 0;padding: 0 auto;padding: 0 auto 10px;padding: 0 10px 10px 20px; -
text-shadow 仅支持 offset-x offset-y blur-radius color 排序 顺序固定,值以空格分隔后按按顺序赋值
border 仅支持 border-width border-style border-color 顺序固定,值以空格分隔后按按顺序赋值
box-shadow 仅支持 offset-x offset-y blur-radius color 顺序固定,值以空格分隔后按按顺序赋值
flex 仅支持 flex-grow flex-shrink flex-basis 顺序固定,值以空格分隔后按按顺序赋值
flex-flow 仅支持 flex-direction flex-wrap 顺序固定,值以空格分隔后按按顺序赋值
border-radius 支持 border-top-left-radius border-top-right-radius border-bottom-right-radius border-bottom-left-radius 顺序固定,值以空格分隔后按按顺序赋值;当设置 border-radius: 0 相当于同时设置了4个方向
background 仅支持 background-image background-color background-repeat 顺序不固定,具体每个属性的支持情况参见上面具体属性支持的文档;
- - - diff --git a/docs-vuepress/.vuepress/dist/rn-template.html b/docs-vuepress/.vuepress/dist/rn-template.html deleted file mode 100644 index 4881256db1..0000000000 --- a/docs-vuepress/.vuepress/dist/rn-template.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - Mpx转RN模版使用指南 | Mpx框架 - - - - - - - - - -

# Mpx转RN模版使用指南

# 模版指令

目前 Mpx 输出 React Native 仅支持以下指令,具体使用范围可参考如下文档

指令名 说明
wx:if 根据表达式的值的来有条件地渲染元素
wx:elif 根据表达式的值的来有条件地渲染元素,前一兄弟元素必须有 wx:if 或 wx:elif
wx:else 不需要表达式,前一兄弟元素必须有 wx:if 或 wx:elif
wx:show 根据表达式的值的来有条件地渲染元素,与 wx:if 所不同的是不会移除节点,而是设置节点的 style 为 display: none
wx:style 动态绑定 style 样式
wx:class 动态绑定 class 样式
wx:for 在组件上使用 wx:for 绑定一个数组,即可使数组中各项的数据重复渲染该组件
wx:for-item 指定数组当前元素的变量名
wx:for-index 指定数组当前下标的变量名
wx:key 指定列表中项目的唯一的标识符
wx:ref 获取节点信息
mpxTagName 动态转换标签
component 使用 component is 动态切换组件
@mode 使用 @ 符号来指定某个节点或属性只在某些平台下有效
@_mode 隐式属性条件编译,仅控制节点的展示,保留节点属性的平台转换能力
@env 自定义 env 目标应用,来实现在不同应用下编译产出不同的代码

# 事件编写

目前 Mpx 输出 React Native 的事件编写遵循小程序的事件编写规范,支持事件的冒泡及捕获

普通事件绑定

<view bindtap="handleTap">
-    Click here!
-</view>
-

绑定并阻止事件冒泡

<view catchtap="handleTap">
-    Click here!
-</view>
-

事件捕获

<view capture-bind:touchstart="handleTap1">
-  outer view
-  <view capture-bind:touchstart="handleTap2">
-    inner view
-  </view>
-</view>
-

中断捕获阶段和取消冒泡阶段

<view capture-catch:touchstart="handleTap1">
-  outer view
-</view>
-
-

在此基础上也新增了事件处理内联传参的增强机制。

<template>
- <!--Mpx增强语法,模板内联传参,方便简洁-->
- <view bindtap="handleTapInline('b')">b</view>
- </template>
- <script setup>
-  // 直接通过参数获取数据,直观方便
-  const handleTapInline = (name) => {
-    console.log('name:', name)
-  }
-  // ...
-</script>
-

除此之外,Mpx 也支持了动态事件绑定

<template>
- <!--动态事件绑定-->
- <view wx:for="{{items}}" bindtap="handleTap_{{index}}">
-  {{item}}
-</view>
- </template>
- <script setup>
-  import { ref } from '@mpxjs/core'
-
-  const data = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4'])
-  const handleTap_0 = (event) => {
-    console.log('Tapped on item 1');
-  },
-
-  const handleTap_1 = (event) => {
-    console.log('Tapped on item 2');
-  },
-
-  const handleTap_2 = (event) => {
-    console.log('Tapped on item 3');
-  },
-
-  const handleTap_3 = (event) => {
-    console.log('Tapped on item 4');
-  }
-</script>
-

注意事项:

当同一个元素上同时绑定了 catchtap 和 bindtap 事件时:

两个事件都会被触发执行。 -但是是否阻止事件冒泡的行为,会以模板上第一个绑定的事件标识符为准。 -如果第一个绑定的是 catchtap,那么不管后面绑定的是什么,都会阻止事件冒泡。 -如果第一个绑定的是 bindtap,则不会阻止事件冒泡。

同理,如果同一个元素上绑定了 capture-bind:tap 和 bindtap:

事件的执行时机会根据模板上第一个绑定事件的标识符来决定: -如果第一个绑定的是 capture-bind:tap,则事件会在捕获阶段触发。 -如果第一个绑定的是 bindtap,则事件会在冒泡阶段触发。

# 基础组件

目前 Mpx 输出 React Native 仅支持以下组件,具体使用范围可参考如下文档

# view

视图容器。

属性

属性名 类型 默认值 说明
hover-class string 指定按下去的样式类。
hover-start-time number 50 按住后多久出现点击态,单位毫秒
hover-stay-time number 400 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

# text

文本。

属性

属性名 类型 默认值 说明
user-select boolean false 文本是否可选。
disable-default-style boolean false 会内置默认样式,比如fontSize为16。设置true可以禁止默认的内置样式。
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindtap 点击的时候触发

注意事项

  1. 未包裹 text 标签的文本,会自动包裹 text 标签。
  2. text 组件开启 enable-offset 后,offsetLeft、offsetWidth 获取时机仅为组件首次渲染阶段

# image

图片。

属性

属性名 类型 默认值 说明
src String false 图片资源地址,支持本地图片资源及 base64 格式数据,暂不支持 svg 格式
mode String scaleToFill 图片裁剪、缩放的模式,适配微信 image 所有 mode 格式
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
binderror 当错误发生时触发,event.detail = { errMsg }
bindload 当图片载入完毕时触发,event.detail = { height, width }

注意事项

  1. image 组件默认宽度320px、高度240px
  2. image 组件进行缩放时,计算出来的宽高可能带有小数,在不同webview内核下渲染可能会被抹去小数部分

# input

输入框。

属性

属性名 类型 默认值 说明
value String 输入框的初始内容
type String text input 的类型,不支持 safe-passwordnickname
password Boolean false 是否是密码类型
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
confirm-type String done 设置键盘右下角按钮的文字,仅在 type='text' 时生效
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bind:selectionchange 选区改变事件, event.detail = { selectionStart, selectionEnd }

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# textarea

多行输入框。

属性

属性名 类型 默认值 说明
value String 输入框内容
type String text input 的类型,不支持 safe-passwordnickname
placeholder String 输入框为空时占位符
placeholder-class String 指定 placeholder 的样式类,仅支持 color 属性
placeholder-style String 指定 placeholder 的样式,仅支持 color 属性
disabled Boolean false 是否禁用
maxlength Number 140 最大输入长度,设置为 -1 的时候不限制最大长度
auto-focus Boolean false (即将废弃,请直接使用 focus )自动聚焦,拉起键盘
focus Boolean false 获取焦点
auto-height Boolean false 是否自动增高,设置 auto-height 时,style.height不生效
confirm-type String done 设置键盘右下角按钮的文字,不支持 return
confirm-hold Boolean false 点击键盘右下角按钮时是否保持键盘不收起
cursor Number 指定 focus 时的光标位置
cursor-color String 光标颜色
selection-start Number -1 光标起始位置,自动聚集时有效,需与 selection-end 搭配使用
selection-end Number -1 光标结束位置,自动聚集时有效,需与 selection-start 搭配使用
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindinput 键盘输入时触发,event.detail = { value, cursor },不支持 keyCode
bindfocus 输入框聚焦时触发,event.detail = { value },不支持 height
bindblur 输入框失去焦点时触发,event.detail = { value },不支持 encryptedValueencryptError
bindconfirm 点击完成按钮时触发,event.detail = { value }
bindlinechange 输入框行数变化时调用,event.detail = { height: 0, lineCount: 0 },不支持 heightRpx
bind:selectionchange 选区改变事件, {selectionStart, selectionEnd}

方法

可通过 ref 方式调用以下组件实例方法

方法名 说明
focus 使输入框得到焦点
blur 使输入框失去焦点
clear 清空输入框的内容
isFocused 返回值表明当前输入框是否获得了焦点

# button

按钮。

属性

属性名 类型 默认值 说明
size String default 按钮的大小
type String default 按钮的样式类型
plain Boolean false 按钮是否镂空,背景色透明
disabled Boolean false 是否禁用
loading Boolean false 名称前是否带 loading 图标
open-type String 微信开放能力,当前仅支持 share
hover-class String 指定按钮按下去的样式类。当 hover-class="none" 时,没有点击态效果
hover-start-time Number 20 按住后多久出现点击态,单位毫秒
hover-stay-time Number 70 手指松开后点击态保留时间,单位毫秒
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

# scroll-view

可滚动视图区域。

属性

属性名 类型 默认值 说明
scroll-x Boolean false 允许横向滚动动
scroll-y Boolean false 允许纵向滚动
upper-threshold Number 50 距顶部/左边多远时(单位 px),触发 scrolltoupper 事件
lower-threshold Number 50 距底部/右边多远时(单位 px),触发 scrolltolower 事件
scroll-top Number 0 设置纵向滚动条位置
scroll-left Number 0 设置横向滚动条位置
scroll-with-animation Boolean false 在设置滚动条位置时使用动画过渡
enable-back-to-top Boolean false 点击状态栏的时候视图会滚动到顶部
enhanced Boolean false scroll-view 组件功能增强
refresher-enabled Boolean false 开启自定义下拉刷新
scroll-anchoring Boolean false 开启滚动区域滚动锚点
refresher-default-style String 'black' 设置下拉刷新默认样式,支持 blackwhitenone,仅安卓支持
refresher-background String '#fff' 设置自定义下拉刷新背景颜色,仅安卓支持
refresher-triggered Boolean false 设置当前下拉刷新状态,true 表示已触发
paging-enabled Number false 分页滑动效果 (同时开启 enhanced 属性后生效),当值为 true 时,滚动条会停在滚动视图的尺寸的整数倍位置
show-scrollbar Number true 滚动条显隐控制 (同时开启 enhanced 属性后生效)
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
binddragstart 滑动开始事件,同时开启 enhanced 属性后生效
binddragging 滑动事件,同时开启 enhanced 属性后生效
binddragend 滑动结束事件,同时开启 enhanced 属性后生效
bindscrolltoupper 滚动到顶部/左边触发
bindscrolltolower 滚动到底部/右边触发
bindscroll 滚动时触发
bindrefresherrefresh 自定义下拉刷新被触发

注意事项

  1. 目前不支持自定义下拉刷新节点,使用 slot="refresher" 声明无效,在 React Native 环境中还是会被当作普通节点渲染出来

# swiper

滑块视图容器。

属性

属性名 类型 默认值 说明
indicator-dots Boolean false 是否显示面板指示点
indicator-color color rgba(0, 0, 0, .3) 指示点颜色
indicator-active-color color #000000 当前选中的指示点颜色
autoplay Boolean false 是否自动切换
current Number 0 当前所在滑块的 index
interval Number 5000 自动切换时间间隔
duration Number 500 滑动动画时长
circular Boolean false 是否采用衔接滑动
vertical Boolean false 滑动方向是否为纵向
previous-margin String 0 前边距,可用于露出前一项的一小部分,接受px
next-margin String 0 后边距,可用于露出后一项的一小部分,接受px
enable-offset Number false 设置是否要获取组件的布局信息,若设置了该属性,会在 e.target 中返回组件的 offsetLeft、offsetWidth 信息

事件

事件名 说明
bindchange current 改变时会触发 change 事件,event.detail = {current, source}

# swiper-item

  1. 仅可放置在swiper组件中,宽高自动设置为100%。

属性

属性名 类型 默认值 说明
item-id string 该 swiper-item 的标识符

# checkbox

多选项目

属性

属性名 类型 默认值 说明
value String checkbox 标识,选中时触发 checkbox-group 的 change 事件,并携带 checkbox 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox的颜色,同css的color

# checkbox-group

多项选择器,内部由多个checkbox组成。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# radio

单选项目

属性

属性名 类型 默认值 说明
value String radio 标识,当该 radio 选中时,radio-group 的 change 事件会携带 radio 的 value
disabled Boolean false 是否禁用
checked Boolean false 当前是否选中,可用来设置默认选中
color String #09BB07 checkbox 的颜色,同 css 的 color

# radio-group

单项选择器,内部由多个 radio 组成

事件

事件名 说明
bindchange radio-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 radio 的 value 的数组 ] }

# label

用来改进表单组件的可用性

注意事项

  1. 当前不支持使用 for 属性找到对应 id,仅支持将控件放在该标签内,目前可以绑定的空间有:checkbox、radio、switch。

# icon

图标组件

属性

属性名 类型 默认值 说明
type String icon 的类型,有效值:success、success_no_circle、info、warn、waiting、cancel、download、search、clear
size String | Number 23 icon 的大小
color String icon 的颜色,同 css 的 color

# movable-area

movable-view的可移动区域。

注意事项

  1. movable-area不支持设置 scale-area,缩放手势生效区域仅在 movable-view 内

# movable-view

可移动的视图容器,在页面中可以拖拽滑动。movable-view 必须在 movable-area 组件中,并且必须是直接子节点,否则不能移动。

属性

属性名 类型 默认值 说明
direction String none 目前支持 all、vertical、horizontal、none|
x Number 定义x轴方向的偏移
y Number 定义y轴方向的偏移
friction Number 7 摩擦系数
disabled boolean false 是否禁用
scale boolean false 是否支持双指缩放
scale-min Number 0.1 定义缩放倍数最小值
scale-max Number 10 定义缩放倍数最大值
scale-value Number 1 定义缩放倍数,取值范围为 0.1 - 10

事件

事件名 说明
bindchange 拖动过程中触发的事件,event.detail = {x, y, source}
bindscale 缩放过程中触发的事件,event.detail = {x, y, scale}
htouchmove 初次手指触摸后移动为横向的移动时触发
vtouchmove 初次手指触摸后移动为纵向的移动时触发

# form

表单。将组件内的用户输入的switch input checkbox slider radio picker 提交。

当点击 form 表单中 form-type 为 submit 的 button 组件时,会将表单组件中的 value 值进行提交,需要在表单组件中加上 name 来作为 key。

事件

事件名 说明
bindsubmit 携带 form 中的数据触发 submit 事件,event.detail = {value : {'name': 'value'} }
bindreset 表单重置时会触发 reset 事件

# cover-view

视图容器。 -功能同 view 组件

# cover-image

视图容器。 -功能同 image 组件 -| bindchange | 滚动选择时触发change事件,event.detail = {value};value为数组,表示 picker-view 内的 picker-view-column 当前选择的是第几项(下标从 0 开始)|

# picker-view

嵌入页面的滚动选择器。其中只可放置 picker-view-column组件,其它节点不会显示

属性

属性名 类型 默认值 说明
value Array[number] false 数组中的数字依次表示 picker-view 内的 picker-view-column 选择的第几项(下标从 0 开始),数字大于 picker-view-column 可选项长度时,选择最后一项。

事件

事件名 说明
bindchange checkbox-group 中选中项发生改变时触发 change 事件,detail = { value: [ 选中的 checkbox 的 value 的数组 ] }

# picker-view-column

滚动选择器子项。仅可放置于picker-view中,其孩子节点的高度会自动设置成与picker-view的选中框的高度一致

# picker

从底部弹起的滚动选择器。

属性

属性名 类型 默认值 说明
mode string selector 选择器类型
disabled boolean false 是否禁用

公共事件

事件名 说明
bindcancel 取消选择时触发
bindchange 滚动选择时触发change事件,event.detail = {value};value为数组,表示 picker-view 内的 picker-view-column 当前选择的是第几项(下标从 0 开始)

# 普通选择器:mode = selector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value number 0 表示选择了 range 中的第几个(下标从 0 开始)

# 多列选择器:mode = multiSelector

属性

属性名 类型 默认值 说明
range array[object]/array [] mode 为 selector 或 multiSelector 时,range 有效
range-key string false 当 range 是一个 Object Array 时,通过 range-key 来指定 Object 中 key 的值作为选择器显示内容
value array [] 表示选择了 range 中的第几个(下标从 0 开始)
bindcolumnchange 列改变时触发

# 多列选择器:时间选择器:mode = time

属性

属性名 类型 默认值 说明
value string [] 表示选中的时间,格式为"hh:mm"
start string false 表示有效时间范围的开始,字符串格式为"hh:mm"
end string [] 表示有效时间范围的结束,字符串格式为"hh:mm"

# 多列选择器:时间选择器:mode = date

属性

属性名 类型 默认值 说明
value string 当天 表示选中的日期,格式为"YYYY-MM-DD"
start string false 表示有效日期范围的开始,字符串格式为"YYYY-MM-DD"
end string [] 表示有效日期范围的结束,字符串格式为"YYYY-MM-DD"
fields string day 有效值 year,month,day,表示选择器的粒度

# fields 有效值:

属性名 说明
year 选择器粒度为年
month 选择器粒度为月份
day 选择器粒度为天

# 省市区选择器:mode = region

属性

属性名 类型 默认值 说明
value array [] 表示选中的省市区,默认选中每一列的第一个值
custom-item string 可为每一列的顶部添加一个自定义的项
level string region 选择器层级

# level 有效值:

属性名 说明
province 选省级选择器
city 市级选择器
region 区级选择器
- - - diff --git a/docs-vuepress/.vuepress/dist/service-worker.js b/docs-vuepress/.vuepress/dist/service-worker.js deleted file mode 100644 index 3fcd8056f9..0000000000 --- a/docs-vuepress/.vuepress/dist/service-worker.js +++ /dev/null @@ -1,901 +0,0 @@ -/** - * Welcome to your Workbox-powered service worker! - * - * You'll need to register this file in your web app and you should - * disable HTTP caching for this file too. - * See https://goo.gl/nhQhGp - * - * The rest of the code is auto-generated. Please don't update this file - * directly; instead, make changes to your Workbox build configuration - * and re-run your build process. - * See https://goo.gl/2aRDsh - */ - -importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); - -self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting(); - } -}); - -/** - * The workboxSW.precacheAndRoute() method efficiently caches and responds to - * requests for URLs in the manifest. - * See https://goo.gl/S9QRab - */ -self.__precacheManifest = [ - { - "url": "404.html", - "revision": "5b00337f21832bd4573ba6c98bedacb4" - }, - { - "url": "api/ApiIndex.html", - "revision": "f5736e201b8355b58c4a3127475e6757" - }, - { - "url": "api/app-config.html", - "revision": "73b3f4c261fc6ad75418f21d0f696b62" - }, - { - "url": "api/builtIn.html", - "revision": "e854e0ad752129084b0de9d2a9b692fd" - }, - { - "url": "api/compile.html", - "revision": "5eb3d7fe5d6193c15061bcca578def13" - }, - { - "url": "api/composition-api.html", - "revision": "70dccb11a976b0321ae6c6df8cbccf83" - }, - { - "url": "api/directives.html", - "revision": "39ac7af3ed7a9131d4dad8bbda934d5e" - }, - { - "url": "api/extend.html", - "revision": "67026f94a9c9fb2b42094426f6157048" - }, - { - "url": "api/global-api.html", - "revision": "33d7c25d85ca74401645e05f2e5990f1" - }, - { - "url": "api/index.html", - "revision": "a0bb36716602184632c2781156f0256e" - }, - { - "url": "api/instance-api.html", - "revision": "2999969ec01f4b0893d81a7c810e05f7" - }, - { - "url": "api/optional-api.html", - "revision": "52cf38d179e9e86b426fe9c31b758659" - }, - { - "url": "api/reactivity-api.html", - "revision": "a580e6086de61b32a4442ff42d8e6caa" - }, - { - "url": "api/store-api.html", - "revision": "fc79ec11f5b437574ee9c0d3f0b51a36" - }, - { - "url": "articles/1.0.html", - "revision": "c7408bac5c637d07a68bd9f1e0579d47" - }, - { - "url": "articles/2.0.html", - "revision": "89411f8727853b0ad8db88d46d2f92d0" - }, - { - "url": "articles/2.7-release.html", - "revision": "36f589577d67385b01da5b910e5e4f43" - }, - { - "url": "articles/2.8-release-alter.html", - "revision": "c7c41c65d2fcc7577004ce04712d5e46" - }, - { - "url": "articles/2.8-release.html", - "revision": "c968dd0160e52f569d8a35f6978c22cb" - }, - { - "url": "articles/2.9-release-alter.html", - "revision": "842d2cf8a91c896bf46ff406a900951e" - }, - { - "url": "articles/2.9-release.html", - "revision": "7714799727658659001df91df0ab765d" - }, - { - "url": "articles/index.html", - "revision": "28e08a2249444d784e1b13e59cb8e0ba" - }, - { - "url": "articles/mpx-cli-next.html", - "revision": "9edaa671d20c5721ade80295c5487d9a" - }, - { - "url": "articles/mpx-cube-ui.html", - "revision": "339facb42ee86b3f21cbf22195f0ea7c" - }, - { - "url": "articles/mpx1.html", - "revision": "b09e8d522bf61190a9b2cf2c77b1c9e9" - }, - { - "url": "articles/mpx2.html", - "revision": "628254e250ec236cfc816e2f6dcd3098" - }, - { - "url": "articles/performance.html", - "revision": "62339e70fe8371692b301f390ece7791" - }, - { - "url": "articles/size-control.html", - "revision": "8256646d0a2da95674f4f7deab424781" - }, - { - "url": "articles/ts-derivation.html", - "revision": "15a7473ded3bb895564dce5ebb866f76" - }, - { - "url": "articles/unit-test.html", - "revision": "2b5854814775bb3f12b5f87cd3807433" - }, - { - "url": "assets/css/0.styles.f22478ca.css", - "revision": "20c20b6924c0d21c3946c0c2f73e3852" - }, - { - "url": "assets/img/search.83621669.svg", - "revision": "83621669651b9a3d4bf64d1a670ad856" - }, - { - "url": "assets/img/start-tips1.3b76ac97.png", - "revision": "3b76ac977e543ae853c79f5fb4bf648d" - }, - { - "url": "assets/img/start-tips2.7d8836f8.png", - "revision": "7d8836f8026aa7e1813ec450276d21ad" - }, - { - "url": "assets/js/1.4f7abe8f.js", - "revision": "12b0c0887ef86bb29d29e515a8254df4" - }, - { - "url": "assets/js/10.0192acce.js", - "revision": "2e489bb3c3e4db7eb021df8227a78d17" - }, - { - "url": "assets/js/100.f31ffd06.js", - "revision": "5204b3c7795a10953cdf3e4715daaeec" - }, - { - "url": "assets/js/101.283d0363.js", - "revision": "a02fdbbcd800637e2e923ae5ce3f526f" - }, - { - "url": "assets/js/102.094a0a16.js", - "revision": "c6084875d5966ca473c712a0c8eb8592" - }, - { - "url": "assets/js/103.b34f6752.js", - "revision": "385380e54f276b92c30b8c769eb3500f" - }, - { - "url": "assets/js/104.78f6358e.js", - "revision": "eeeb9573bb2e751a7efcf7b866d559a4" - }, - { - "url": "assets/js/105.eb6680fc.js", - "revision": "fbbf2d50f5f790400258587ca7d319ab" - }, - { - "url": "assets/js/106.471f03cf.js", - "revision": "3a2b07fdde55595e53ee58f20d93fb35" - }, - { - "url": "assets/js/107.63fd18fd.js", - "revision": "8e1580b610cbe287488f9dc6a17c21a0" - }, - { - "url": "assets/js/108.614260e9.js", - "revision": "960d17597536a94c9f465c1f183470ad" - }, - { - "url": "assets/js/109.3da50cc1.js", - "revision": "85bb938489ee7bdb469106adf4520d52" - }, - { - "url": "assets/js/11.d7a8d045.js", - "revision": "9bc45c1f6e30baa42b6f61119767b063" - }, - { - "url": "assets/js/110.f74ff3a3.js", - "revision": "8b75f04fbf04424cc6c55c9f2dbec4a7" - }, - { - "url": "assets/js/111.51932e54.js", - "revision": "08920579c78da19f6afbc656719be751" - }, - { - "url": "assets/js/112.da7bd3e7.js", - "revision": "fe8d576cf790411d588601fae4414b03" - }, - { - "url": "assets/js/113.caaf11a3.js", - "revision": "414f18602138c0fb69fb8e493d5659dd" - }, - { - "url": "assets/js/114.16cda7ac.js", - "revision": "a50fb0cd876585fa9a81503bfee109f9" - }, - { - "url": "assets/js/115.88124b12.js", - "revision": "3440d45dc5afd506ea94fd2159c1b733" - }, - { - "url": "assets/js/116.4a116234.js", - "revision": "77eb5727ecdf8abd544ab6180852ab0a" - }, - { - "url": "assets/js/117.e45c1b95.js", - "revision": "22697b9821bf9d90ef8542c9f525bc4f" - }, - { - "url": "assets/js/118.9cbac09e.js", - "revision": "2ee13a56a4d9163596caa0d4c599d268" - }, - { - "url": "assets/js/119.312e4c21.js", - "revision": "00e5ef4ca7025684be01fd3aefee9bdc" - }, - { - "url": "assets/js/12.4d7217be.js", - "revision": "cf2ee26c47e0f0d7ee3f2bda3d92cf40" - }, - { - "url": "assets/js/120.7a6b4035.js", - "revision": "7967e6835f248a74746ab0aa147c5177" - }, - { - "url": "assets/js/14.7377c68f.js", - "revision": "b7514f8cb2d06cdaf18d71594d346192" - }, - { - "url": "assets/js/15.6f885a83.js", - "revision": "40c60e1604dfac53cae641e598daf156" - }, - { - "url": "assets/js/16.974f551e.js", - "revision": "df88e248621a7536527b6a547143dc57" - }, - { - "url": "assets/js/17.e94e6745.js", - "revision": "afe65725c62091da4210e3c489f993f6" - }, - { - "url": "assets/js/18.b1f7a4a2.js", - "revision": "2746cf3eaba809a8792ab8e911b6e6b1" - }, - { - "url": "assets/js/19.62186e92.js", - "revision": "2e5228047eaba20f8fcc067431adb2a4" - }, - { - "url": "assets/js/2.314c65b9.js", - "revision": "b8cf0e72859eb5f0e3fb7f67d27ef340" - }, - { - "url": "assets/js/20.e9cd4de3.js", - "revision": "0045f2d19fe78531ce98e2db9aa6bdd5" - }, - { - "url": "assets/js/21.217f8fa9.js", - "revision": "bd130990daa769d2fb63d19c19c31932" - }, - { - "url": "assets/js/22.4cd3344f.js", - "revision": "db69d7654880d9f1c64373d171c787b6" - }, - { - "url": "assets/js/23.365f9a87.js", - "revision": "cd54b4361286190f19e5c3067303c425" - }, - { - "url": "assets/js/24.17777a13.js", - "revision": "eb0f29a4f257f591178f6201882b8298" - }, - { - "url": "assets/js/25.889c07d7.js", - "revision": "5a964529d1992e5ecbe2f545980ee947" - }, - { - "url": "assets/js/26.3aa9f276.js", - "revision": "03b8f1eb7e4dbebe65672c17914db693" - }, - { - "url": "assets/js/27.1a35170a.js", - "revision": "7d5ed813bbe766a1748a0f21ee5569d8" - }, - { - "url": "assets/js/28.3670fa46.js", - "revision": "6e3c125eaf27c095cad1d77637e4e80f" - }, - { - "url": "assets/js/29.a911e510.js", - "revision": "363f3beef26a3abf2161c7e1278cfcae" - }, - { - "url": "assets/js/3.145e69cd.js", - "revision": "1a153ade78a92885e52f897ec87c1c3e" - }, - { - "url": "assets/js/30.04d1a7ea.js", - "revision": "578f318e24529539c775d12d5436fca8" - }, - { - "url": "assets/js/31.ec06bf2f.js", - "revision": "00a3a86bdd7caad32fa0eec96cf4698a" - }, - { - "url": "assets/js/32.f29d2cd1.js", - "revision": "7d811f0e993fbee6e7c7d6800f830839" - }, - { - "url": "assets/js/33.936d018e.js", - "revision": "2714336439729f02460fbcd2af4a7c5c" - }, - { - "url": "assets/js/34.f494d685.js", - "revision": "3f1099fca4eff5ef8e62348425a3e7ff" - }, - { - "url": "assets/js/35.c3ed1ff7.js", - "revision": "8f836b9e9e2107c4c281203b190ef9a8" - }, - { - "url": "assets/js/36.d2c9afa9.js", - "revision": "fe308138cfddd6e5819569d6609a7672" - }, - { - "url": "assets/js/37.4e1eee2c.js", - "revision": "0efac40e5dd0e3fc8e94f0b8cda4f0b2" - }, - { - "url": "assets/js/38.7826170e.js", - "revision": "8e5e62243cab1c2f356d0f77ffac4b04" - }, - { - "url": "assets/js/39.094ffa40.js", - "revision": "4342737cae50042277b76721d79315d2" - }, - { - "url": "assets/js/4.9489102c.js", - "revision": "c5f4723242bd00da0724a4737e1cb142" - }, - { - "url": "assets/js/40.10d90636.js", - "revision": "76033b573e4da1b2cd491c8794b35af7" - }, - { - "url": "assets/js/41.8351af86.js", - "revision": "16e3ded52f241dfcedb215d3e784ffd2" - }, - { - "url": "assets/js/42.53972f6c.js", - "revision": "f59a9f585a6b06d80cebcdb81772acd0" - }, - { - "url": "assets/js/43.e2b432ea.js", - "revision": "f0fb7f9acf7bfd42c8777fa0532a4f7c" - }, - { - "url": "assets/js/44.e4242a1f.js", - "revision": "397c66f6b7866718983234d014f94e60" - }, - { - "url": "assets/js/45.392413aa.js", - "revision": "51fcb57964dd90d24c02593411a478cd" - }, - { - "url": "assets/js/46.22f9a9c6.js", - "revision": "d8f80568057ee4169e8f145ed42a28d8" - }, - { - "url": "assets/js/47.2bd2cecc.js", - "revision": "09684550aea02b1058ea119724a2fed1" - }, - { - "url": "assets/js/48.55792a30.js", - "revision": "6900523d21b183f5a8aae5e2799674cc" - }, - { - "url": "assets/js/49.cde94a79.js", - "revision": "3c992f7d6d21537ae32091b611215346" - }, - { - "url": "assets/js/5.12d049b6.js", - "revision": "d489c45818dd2315c2c97a3437acd6cf" - }, - { - "url": "assets/js/50.42e34e52.js", - "revision": "1831c8d5de17eeed779edb5f28385671" - }, - { - "url": "assets/js/51.90dfa84e.js", - "revision": "531ac61e50a863e5bb409b2f893c4240" - }, - { - "url": "assets/js/52.10bb0251.js", - "revision": "79eb24907253da0d7120ae9dcf34c73f" - }, - { - "url": "assets/js/53.7207ffc5.js", - "revision": "53164733a3c1e6f29a4ca6719591b70b" - }, - { - "url": "assets/js/54.af3d3dcd.js", - "revision": "f175b1f13236f10b690d5e86b9bb368d" - }, - { - "url": "assets/js/55.2d6ae2e1.js", - "revision": "eb8dd3e8ff4c57ed143c9778ec15bd4b" - }, - { - "url": "assets/js/56.9aec9a9e.js", - "revision": "5ebf6c52558f0709c076b202a55b9fa3" - }, - { - "url": "assets/js/57.9d9ddcbd.js", - "revision": "99117ec556700247d528c7acd29c203b" - }, - { - "url": "assets/js/58.50eb3b91.js", - "revision": "e7f565c15f3cab12bf1eedf0659fd523" - }, - { - "url": "assets/js/59.27106fbb.js", - "revision": "f485b2d1b0a56c26176ea8f44f9e2738" - }, - { - "url": "assets/js/6.6ea91b28.js", - "revision": "5fac1ead0dd12855a5d8976740456131" - }, - { - "url": "assets/js/60.a47e5279.js", - "revision": "f6f5d7dc973878332b5b1d0157f92135" - }, - { - "url": "assets/js/61.74ebd7d9.js", - "revision": "ae27832c8fa41953cc04725e0d5001d0" - }, - { - "url": "assets/js/62.4bf212f9.js", - "revision": "2ae416136507b54b6c19f00f94ea9dd4" - }, - { - "url": "assets/js/63.b54aca3e.js", - "revision": "80860c2eb8c0374ffd19206c3653a451" - }, - { - "url": "assets/js/64.399e72b8.js", - "revision": "d9f80db9175e32d06a3ea1077135f0e4" - }, - { - "url": "assets/js/65.a8c627d3.js", - "revision": "779b4c30258b0bd6aad23a1436bc1b92" - }, - { - "url": "assets/js/66.7f19cc4b.js", - "revision": "2e79a49876a6073bd459a1575199269d" - }, - { - "url": "assets/js/67.e57db97e.js", - "revision": "78a67eab42f946943b9651d089a4ef56" - }, - { - "url": "assets/js/68.ebe4213c.js", - "revision": "93e3246379979219b4af948dba1bc71a" - }, - { - "url": "assets/js/69.2d309866.js", - "revision": "f5dc40ff8ea229a2fa6ac697fcf42475" - }, - { - "url": "assets/js/7.01e4e942.js", - "revision": "e4797b632e362ecd096a64375e7003ae" - }, - { - "url": "assets/js/70.60ae83e6.js", - "revision": "acba0df2ef73d360ec4686677942d0a4" - }, - { - "url": "assets/js/71.188d7bca.js", - "revision": "cf49e628f26e54631f9a3fcb3f909fbc" - }, - { - "url": "assets/js/72.3f7bfd0d.js", - "revision": "8b3a18399033b6fbc07696b31226998a" - }, - { - "url": "assets/js/73.171571e4.js", - "revision": "4152daed0ad6a65b39b3f20c0ba648ca" - }, - { - "url": "assets/js/74.3d5cfaba.js", - "revision": "0fbea56200387d87caa90abc9f1c807d" - }, - { - "url": "assets/js/75.093d716c.js", - "revision": "0abd2acf22923e75799377e678b91077" - }, - { - "url": "assets/js/76.50d84c61.js", - "revision": "c299d111edbf4d3c46c44c9374a3e4f3" - }, - { - "url": "assets/js/77.7bc0f013.js", - "revision": "6d86d8510f2fb24917c56fcd00333e12" - }, - { - "url": "assets/js/78.e2ded2d4.js", - "revision": "4058b878e45344934acd21b46e02795f" - }, - { - "url": "assets/js/79.f7eeb76b.js", - "revision": "8e9491e599a437e2d5affbbfbd58559c" - }, - { - "url": "assets/js/8.ba7f6ace.js", - "revision": "3db41c27492ef5d04cfe98f3ba38c493" - }, - { - "url": "assets/js/80.41418749.js", - "revision": "76617f3118e34b77c5ce0cddcebd6ec4" - }, - { - "url": "assets/js/81.16e2cc83.js", - "revision": "2ad3cec1add1ede3456c3c89a27f2a07" - }, - { - "url": "assets/js/82.56c0d7c1.js", - "revision": "5ef8ed46677490c1264882fe0c1eade8" - }, - { - "url": "assets/js/83.ea65d6b1.js", - "revision": "2629982f8da068c64b96af5cb7cc0170" - }, - { - "url": "assets/js/84.d50d3d09.js", - "revision": "cdc0e25e4f147f635e4c04c409f99935" - }, - { - "url": "assets/js/85.1eed1486.js", - "revision": "99a6b518c198712eba4adb916001461c" - }, - { - "url": "assets/js/86.1ab04b8d.js", - "revision": "9218ab6fe0cc3e0d0cc9465ae1f04957" - }, - { - "url": "assets/js/87.392384e7.js", - "revision": "b247ed9b2da1e82b477313aa886db306" - }, - { - "url": "assets/js/88.a95133cd.js", - "revision": "4036de5ef3a7b6e9a318942ba6faf560" - }, - { - "url": "assets/js/89.213efaae.js", - "revision": "fa74c388f721c266e3ae5f6edbb4bd85" - }, - { - "url": "assets/js/9.26319460.js", - "revision": "b5a027c58f63f184432e7851068830a1" - }, - { - "url": "assets/js/90.ef03688f.js", - "revision": "58920a350be92626cfd2cf15e640fd3d" - }, - { - "url": "assets/js/91.43930d52.js", - "revision": "bff38c73ab05c14ddc5da88e4ba036e1" - }, - { - "url": "assets/js/92.ae5e9611.js", - "revision": "b3faf9fd83b537307278864b0e5464ec" - }, - { - "url": "assets/js/93.3cdab647.js", - "revision": "f66789e02a5ffd1afc485242e108c860" - }, - { - "url": "assets/js/94.72df7b53.js", - "revision": "bd73044a9f346ca23782d615da42d4a4" - }, - { - "url": "assets/js/95.c8e36604.js", - "revision": "e40ad2a5dfe1e25c058962cf86660287" - }, - { - "url": "assets/js/96.1ebdc00d.js", - "revision": "2c67b7841bd6c48f9af68c6d95c8ffe4" - }, - { - "url": "assets/js/97.fa1ed9e7.js", - "revision": "7e054747dd126fecc7e9d670794002b1" - }, - { - "url": "assets/js/98.939cb6ef.js", - "revision": "56b3178533a666894baddadd8ab9bc06" - }, - { - "url": "assets/js/99.96c5a70e.js", - "revision": "0cbc93b6dce3419f25775487b0d1407a" - }, - { - "url": "assets/js/app.84c88b57.js", - "revision": "1067055bba375603bb5781924201a8a5" - }, - { - "url": "baidu_verify_codeva-GYcT5ujCTB.html", - "revision": "05e79aba57b6dbeeb58f6aadc23f0074" - }, - { - "url": "desc.html", - "revision": "f46e5deaa0b32c74a5465f7e57401fcd" - }, - { - "url": "guide/advance/ability-compatible.html", - "revision": "3b8afd343db4d74cc203261e4141a4af" - }, - { - "url": "guide/advance/async-subpackage.html", - "revision": "bc5eb1b9ec36b9d874620ec322f4d323" - }, - { - "url": "guide/advance/custom-output-path.html", - "revision": "72a926d622c483691ffc39f9aa346f2d" - }, - { - "url": "guide/advance/dll-plugin.html", - "revision": "e8d95670c94231a0a5d0c9ac40d23795" - }, - { - "url": "guide/advance/i18n.html", - "revision": "f81fab30016db6683dc67d7f87ca65dd" - }, - { - "url": "guide/advance/image-process.html", - "revision": "4c139d9d8d72efe3136d29bb862e4fe4" - }, - { - "url": "guide/advance/mixin.html", - "revision": "5989556c2b6a6285617fcb2775e0b403" - }, - { - "url": "guide/advance/npm.html", - "revision": "acc08a0cd161d56e73c5ea141affc99c" - }, - { - "url": "guide/advance/pinia.html", - "revision": "2359fa01373e8bfb92e81ff3b275d5a8" - }, - { - "url": "guide/advance/platform.html", - "revision": "83cd97f6ec8954c086b2b0361fc608c8" - }, - { - "url": "guide/advance/plugin.html", - "revision": "7aecc3bfede3b13de6afc7a20e25e8af" - }, - { - "url": "guide/advance/progressive.html", - "revision": "13fd4ea947dd5e9c0b3c4cb2ffff6ef4" - }, - { - "url": "guide/advance/provide-inject.html", - "revision": "94f05629f5f4c9b7c9808d87e957eea8" - }, - { - "url": "guide/advance/resource-resolve.html", - "revision": "cf43ecea0f34b0087d6ea2310d68d903" - }, - { - "url": "guide/advance/size-report.html", - "revision": "e9b31045eeb57a567487eb0cec8c1a55" - }, - { - "url": "guide/advance/ssr.html", - "revision": "a9a2ab6192f90ecf01f14e5e23ca3afc" - }, - { - "url": "guide/advance/store.html", - "revision": "21fef840c6de0b5e61c831a76077e497" - }, - { - "url": "guide/advance/subpackage.html", - "revision": "b43e2e4c902b2e4d10b58d5b8406b880" - }, - { - "url": "guide/advance/utility-first-css.html", - "revision": "203f30c86d031caab57667d23d874a8c" - }, - { - "url": "guide/basic/class-style-binding.html", - "revision": "b8f5d12016d754c0504aab2e06a8dda2" - }, - { - "url": "guide/basic/component.html", - "revision": "c8e7cf6524386db839da0964dde4530a" - }, - { - "url": "guide/basic/conditional-render.html", - "revision": "458a0e675d9fc7e4f4ed3a3d7cb10d2f" - }, - { - "url": "guide/basic/css.html", - "revision": "a60840e3e0ab988469dd9701b8d9740f" - }, - { - "url": "guide/basic/event.html", - "revision": "34a29012d67135507b22853092744b7a" - }, - { - "url": "guide/basic/ide.html", - "revision": "84d9ce263667bac51aa966600d81f452" - }, - { - "url": "guide/basic/intro.html", - "revision": "3564335cfab7690662f7e6981e28384c" - }, - { - "url": "guide/basic/list-render.html", - "revision": "81564ae12132bfdeee22556dc29cf2aa" - }, - { - "url": "guide/basic/option-chain.html", - "revision": "b8af0aca729f041f5d32e1df83383f8c" - }, - { - "url": "guide/basic/reactive.html", - "revision": "fa1199d698f37cd18ab5e1cbd1fcde85" - }, - { - "url": "guide/basic/refs.html", - "revision": "1ad3afc97da4858d06e82df08d4715ef" - }, - { - "url": "guide/basic/single-file.html", - "revision": "82003549b10795b0c603a66854e2a89f" - }, - { - "url": "guide/basic/start.html", - "revision": "06f28accbba57fc3d2696c3303a17ca0" - }, - { - "url": "guide/basic/template.html", - "revision": "ac4adffc987166bd6b014e4874f9807b" - }, - { - "url": "guide/basic/two-way-binding.html", - "revision": "9f94591106431eae092285cc63bf73b3" - }, - { - "url": "guide/composition-api/composition-api.html", - "revision": "f372f2e4334ed4580d06f8fdfd741e9a" - }, - { - "url": "guide/composition-api/reactive-api.html", - "revision": "a8f594693bbd5ae4af7644ea13f697fb" - }, - { - "url": "guide/extend/api-proxy.html", - "revision": "13330ecd6678b2e40e034d8ad425029c" - }, - { - "url": "guide/extend/fetch.html", - "revision": "6a54e8095f85d420cd97707a5d3684c7" - }, - { - "url": "guide/extend/index.html", - "revision": "258dede7e9407b44d9998bf7fe1fd3b0" - }, - { - "url": "guide/extend/mock.html", - "revision": "2951d26e9b525fb4c94afabf4ac9924f" - }, - { - "url": "guide/migrate/2.7.html", - "revision": "fc84b08b7d29082e1dbb1999d900a704" - }, - { - "url": "guide/migrate/2.8.html", - "revision": "28587490b791a1b8ba35c7d92f12a6de" - }, - { - "url": "guide/migrate/2.9.html", - "revision": "658e207bc34b8a18cb8ed68442c62712" - }, - { - "url": "guide/migrate/mpx-cli-3.html", - "revision": "f91fdb6ac9c54c8c865d19637c7ee591" - }, - { - "url": "guide/platform/index.html", - "revision": "b0af12d7eaf7882a8aea01d8202c4d02" - }, - { - "url": "guide/platform/miniprogram.html", - "revision": "2b5ed743d1a4eeab70f2b4f8051eefba" - }, - { - "url": "guide/platform/rn.html", - "revision": "cb10026541b586deac1fc9c234328e78" - }, - { - "url": "guide/platform/web.html", - "revision": "e99c298705c937e4965c229cd7e4a2b9" - }, - { - "url": "guide/tool/e2e-test.html", - "revision": "858693cd05b55bc582f420643ef9e514" - }, - { - "url": "guide/tool/ts.html", - "revision": "a1b3d19233cad159d0bd946a2ea3eb41" - }, - { - "url": "guide/tool/unit-test.html", - "revision": "bf791feac87c5709dfb9e3fd326c99fd" - }, - { - "url": "guide/understand/compile.html", - "revision": "6e9a16d929784bcdbe34ca2564c47a18" - }, - { - "url": "guide/understand/runtime.html", - "revision": "c7008c7a29fefb544fb600bd71677b7a" - }, - { - "url": "index.html", - "revision": "51c2b45fb4b6fac444845411d55acbca" - }, - { - "url": "logo.png", - "revision": "b362e51deb26ea4ff1d0daa6da1e7c44" - }, - { - "url": "rn-api.html", - "revision": "41b9df161419f392a9a40e6b7b162ae3" - }, - { - "url": "rn-component.html", - "revision": "e0533defbccf789f8f8e2c15ae3ebd22" - }, - { - "url": "rn-style.html", - "revision": "163003bbcbe6b7665c2f420daee35296" - }, - { - "url": "rn-template.html", - "revision": "71cca96a9f67d8ddb84cb284342b8329" - } -].concat(self.__precacheManifest || []); -workbox.precaching.precacheAndRoute(self.__precacheManifest, {}); -addEventListener('message', event => { - const replyPort = event.ports[0] - const message = event.data - if (replyPort && message && message.type === 'skip-waiting') { - event.waitUntil( - self.skipWaiting().then( - () => replyPort.postMessage({ error: null }), - error => replyPort.postMessage({ error }) - ) - ) - } -}) From 042cccc9ead4f0e80669507efd59f9a58991d65e Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Tue, 11 Feb 2025 15:58:01 +0800 Subject: [PATCH 013/126] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96recycle-view?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lib/runtime/components/wx/mpx-recycle-view.mpx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx index 37b04cc613..2c26ec058d 100644 --- a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx @@ -23,11 +23,11 @@ - + @@ -96,10 +96,7 @@ createComponent({ }, _listData() { return this.listData.map((item, index) => { - return { - _index: `_${index}`, - item - } + return Object.assign({}, item, { _index: `_${index}` }) }) }, _scrollTop() { @@ -113,7 +110,7 @@ createComponent({ return Math.min(this.start, this.bufferScale * this.visibleCount) }, belowCount() { - return Math.min(this.listData.length - this.end, this.bufferScale * this.visibleCount) + return Math.min(this._listData.length - this.end, this.bufferScale * this.visibleCount) }, visibleData() { const start = this.start - this.aboveCount From b13b4acec0c58ff20f712facfa5f4e64ef2c302f Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Tue, 11 Feb 2025 15:58:24 +0800 Subject: [PATCH 014/126] =?UTF-8?q?chore:=20=E4=BC=98=E5=8C=96recycle-view?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/{wx => extend}/mpx-recycle-item-default.mpx | 0 .../lib/runtime/components/{wx => extend}/mpx-recycle-view.mpx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/webpack-plugin/lib/runtime/components/{wx => extend}/mpx-recycle-item-default.mpx (100%) rename packages/webpack-plugin/lib/runtime/components/{wx => extend}/mpx-recycle-view.mpx (100%) diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx b/packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-item-default.mpx similarity index 100% rename from packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-item-default.mpx rename to packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-item-default.mpx diff --git a/packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-view.mpx similarity index 100% rename from packages/webpack-plugin/lib/runtime/components/wx/mpx-recycle-view.mpx rename to packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-view.mpx From dd88f94035cb14b8aedf6e9a60d00d886e969313 Mon Sep 17 00:00:00 2001 From: lareinayanyu Date: Tue, 11 Feb 2025 17:03:34 +0800 Subject: [PATCH 015/126] =?UTF-8?q?feat:=20=E7=BC=96=E8=AF=91=E6=97=B6?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=86=85=E7=BD=AE=E6=89=A9=E5=B1=95=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/wx/component-config/index.js | 4 +++- .../wx/component-config/recycle-view.js | 22 +++++++++++++++++++ .../components/extend/mpx-recycle-view.mpx | 6 ++--- .../lib/template-compiler/compiler.js | 4 ++++ 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 packages/webpack-plugin/lib/platform/template/wx/component-config/recycle-view.js diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js index 40281a2953..ca8b4638a2 100644 --- a/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/index.js @@ -42,6 +42,7 @@ const wxs = require('./wxs') const component = require('./component') const fixComponentName = require('./fix-component-name') const rootPortal = require('./root-portal') +const recycleView = require('./recycle-view') module.exports = function getComponentConfigs ({ warn, error }) { /** @@ -125,6 +126,7 @@ module.exports = function getComponentConfigs ({ warn, error }) { hyphenTagName({ print }), label({ print }), component(), - rootPortal({ print }) + rootPortal({ print }), + recycleView({ print }) ] } diff --git a/packages/webpack-plugin/lib/platform/template/wx/component-config/recycle-view.js b/packages/webpack-plugin/lib/platform/template/wx/component-config/recycle-view.js new file mode 100644 index 0000000000..522fe7e658 --- /dev/null +++ b/packages/webpack-plugin/lib/platform/template/wx/component-config/recycle-view.js @@ -0,0 +1,22 @@ +const TAG_NAME = 'recycle-view' + +module.exports = function ({ print }) { + return { + test: TAG_NAME, + web (tag, { el }) { + el.isBuiltIn = true + el.isExtend = true + return 'mpx-recycle-view' + }, + ios (tag, { el }) { + el.isBuiltIn = true + el.isExtend = true + return 'mpx-recycle-view' + }, + android (tag, { el }) { + el.isBuiltIn = true + el.isExtend = true + return 'mpx-recycle-view' + } + } +} diff --git a/packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-view.mpx b/packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-view.mpx index 2c26ec058d..9bd40c6484 100644 --- a/packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-view.mpx +++ b/packages/webpack-plugin/lib/runtime/components/extend/mpx-recycle-view.mpx @@ -1,6 +1,6 @@