mp-weixin-back 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "mp-weixin-back",
3
+ "version": "0.0.1",
4
+ "description": "监听微信小程序的手势返回和页面默认导航栏的返回",
5
+ "main": "./dist/index.ts",
6
+ "scripts": {
7
+ "build": "tsup",
8
+ "test": "vitest"
9
+ },
10
+ "keywords": [
11
+ "微信小程序",
12
+ "手势返回",
13
+ "vite",
14
+ "uniapp"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./dist/index.d.mts",
20
+ "default": "./dist/index.mjs"
21
+ }
22
+ },
23
+ "./client": {
24
+ "types": "./client.d.ts"
25
+ }
26
+ },
27
+ "author": "",
28
+ "license": "ISC",
29
+ "devDependencies": {
30
+ "@babel/generator": "^7.26.2",
31
+ "@babel/parser": "^7.26.2",
32
+ "@babel/traverse": "^7.25.9",
33
+ "@types/babel__generator": "^7.6.8",
34
+ "@types/node": "^22.9.3",
35
+ "@vitejs/plugin-vue": "^5.2.0",
36
+ "@vue/compiler-sfc": "^3.2.45",
37
+ "@vue/test-utils": "^2.4.6",
38
+ "ast-kit": "^1.3.1",
39
+ "happy-dom": "^15.11.6",
40
+ "tsup": "^8.3.5",
41
+ "typescript": "^5.7.2",
42
+ "vite": "^5.4.11",
43
+ "vitest": "^2.1.5",
44
+ "vue": "^3.5.13"
45
+ },
46
+ "lint-staged": {
47
+ "*": "prettier --write"
48
+ }
49
+ }
package/readme.md ADDED
@@ -0,0 +1,75 @@
1
+ ### 功能描述
2
+
3
+ 监听手势返回和页面默认导航栏的返回事件
4
+
5
+ ### 在项目中使用
6
+
7
+ #### 下载
8
+
9
+ ```ts
10
+ npm install mp-weixin-back
11
+ ```
12
+
13
+ #### 使用
14
+
15
+ `vite.config.ts` 中配置
16
+
17
+ ```ts
18
+ import mpBackPlugin from 'mp-weixin-back'
19
+
20
+ export default defineConfig({
21
+ plugins: [mpBackPlugin()],
22
+ })
23
+ ```
24
+
25
+ 在 vue3 中使用
26
+
27
+ ```ts
28
+ import onPageBack from 'mp-weixin-back-helper'
29
+
30
+ onPageBack(() => {
31
+ console.log('触发了手势返回')
32
+ })
33
+ ```
34
+
35
+ onPageBack 的类型定义为:
36
+
37
+ ```ts
38
+ type Config = {
39
+ /**
40
+ * 是否阻止默认的回退事件,默认为 false
41
+ */
42
+ preventDefault: boolean
43
+ /**
44
+ * 阻止次数,默认是 `1`
45
+ */
46
+ frequency: number
47
+ }
48
+
49
+ function onPageBack(callback: () => void, params: Partial<Config>)
50
+ ```
51
+
52
+ #### 引入类型
53
+
54
+ 在项目目录中的`src/env.d.ts` 或`src/shime-uni.d.ts` 文件中引入
55
+
56
+ ```
57
+ /// <reference types="mp-weixin-back/client" />
58
+ ```
59
+
60
+ 或在 `tsconfig.json` 的 `compilerOptions` 下配置
61
+
62
+ ```json
63
+ {
64
+ "compilerOptions": {
65
+ "types": ["mp-weixin-back/client"]
66
+ }
67
+ }
68
+ ```
69
+
70
+ ### todolist
71
+
72
+ - [ ] 兼容 uniapp 的 Vue2 项目
73
+ - [ ] debug 模式
74
+ - [ ] 热更新 pages.json 文件
75
+ - [ ] 单元测试
package/src/context.ts ADDED
@@ -0,0 +1,54 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import { ContextConfig, PagesJson } from '../types'
4
+ import { transformVueFile } from '../utils'
5
+
6
+ export class pageContext {
7
+ config: ContextConfig
8
+ pages: string[] = []
9
+
10
+ constructor(config: ContextConfig) {
11
+ this.config = config
12
+ }
13
+ getPagesJsonPath() {
14
+ const pagesJsonPath = path.join(this.config.root, 'src/pages.json')
15
+ return pagesJsonPath
16
+ }
17
+ // 获取页面配置详情
18
+ async getPagesJsonInfo() {
19
+ const hasPagesJson = fs.existsSync(this.getPagesJsonPath())
20
+ if (!hasPagesJson) return
21
+ try {
22
+ const content = await fs.promises.readFile(this.getPagesJsonPath(), 'utf-8')
23
+ const pagesContent = JSON.parse(content) as PagesJson
24
+ const { pages, subpackages } = pagesContent
25
+ if (pages) {
26
+ const mainPages = pages.reduce((acc: string[], current) => {
27
+ acc.push(current.path)
28
+ return acc
29
+ }, [])
30
+ this.pages.push(...mainPages)
31
+ }
32
+ if (subpackages) {
33
+ const root = subpackages.root
34
+ const subPages = subpackages.pages.reduce((acc: string[], current) => {
35
+ acc.push(`${root}/${current.path}`.replace('//', '/'))
36
+ return acc
37
+ }, [])
38
+ this.pages.push(...subPages)
39
+ }
40
+ } catch (error) {
41
+ throw new Error('请正确配置项目的pages.json文件')
42
+ }
43
+ }
44
+ // 获取指定id的page
45
+ getPageById(id: string) {
46
+ const path = (id.split('src/')[1] || '').replace('.vue', '')
47
+ // 页面级别
48
+ return this.pages.find((i) => i === path) || null
49
+ }
50
+ async transform(code: string, id: string) {
51
+ const result = await transformVueFile.call(this, code, id)
52
+ return result
53
+ }
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { pageContext } from './context'
2
+ import { virtualFileId } from '../utils/constant'
3
+ import type { Plugin } from 'vite'
4
+ import type { Config, UserOptions } from '../types'
5
+
6
+ function MpBackPlugin(userOptions: UserOptions = {}): Plugin {
7
+ let context: pageContext
8
+
9
+ const defaultOptions: Config = {
10
+ preventDefault: false,
11
+ frequency: 1,
12
+ }
13
+ const options = { ...defaultOptions, ...userOptions }
14
+
15
+ return {
16
+ name: 'vite-plugin-mp-weixin-back',
17
+ enforce: 'pre',
18
+ configResolved(config) {
19
+ context = new pageContext({ ...options, mode: config.mode, root: config.root })
20
+ },
21
+ buildStart() {
22
+ context.getPagesJsonInfo()
23
+ },
24
+ resolveId(id) {
25
+ if (id === virtualFileId) {
26
+ return virtualFileId
27
+ }
28
+ },
29
+ load(id) {
30
+ if (id.includes('node_modules')) {
31
+ return
32
+ }
33
+ // 导出一个对象
34
+ if (id === virtualFileId) {
35
+ return `export default function onPageBack() {}`
36
+ }
37
+ },
38
+ async transform(code, id) {
39
+ if (id.includes('node_modules') || !id.includes('.vue')) {
40
+ return
41
+ }
42
+ return context.transform(code, id)
43
+ },
44
+ } as Plugin
45
+ }
46
+
47
+ export default MpBackPlugin
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <div>我是默认的界面</div>
3
+ </template>
4
+
5
+ <script setup>
6
+ import onPageBack from 'mp-weixin-back-helper'
7
+
8
+ onPageBack(() => {
9
+ console.log('触发了手势返回')
10
+ })
11
+ </script>
12
+
13
+ <style></style>
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ // @ts-ignore
4
+ import Index from './data/index.vue'
5
+
6
+ describe('generate page-container components', () => {
7
+ it('page has template tag', async () => {
8
+ const wrapper = mount(Index)
9
+ await wrapper.vm.$nextTick()
10
+
11
+ const pageContainerRef = wrapper.find('page-container')
12
+ expect(pageContainerRef.exists()).toBe(true)
13
+ })
14
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2018",
4
+ "lib": ["esnext"],
5
+ "types": ["node"],
6
+ "module": "esnext",
7
+ "moduleResolution": "node",
8
+ "resolveJsonModule": true,
9
+ "strict": true,
10
+ "strictNullChecks": true,
11
+ "esModuleInterop": true,
12
+ "skipDefaultLibCheck": true,
13
+ "skipLibCheck": true
14
+ }
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['cjs', 'esm'],
6
+ sourcemap: false, // 生成 sourcemap
7
+ dts: {
8
+ resolve: true
9
+ },
10
+ clean: true, // 构建前清理 dist 目录
11
+ minify: false, // 是否压缩代码
12
+ target: 'es6',
13
+ external: ['vite'] // 标记 Vite 为外部依赖,避免打包到插件中
14
+ })
15
+
package/types/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ export type UserOptions = Partial<Config>
2
+
3
+ export type Config = {
4
+ /**
5
+ * 是否阻止默认的回退事件,默认为 false
6
+ */
7
+ preventDefault: boolean
8
+ /**
9
+ * 阻止次数,默认是 `1`
10
+ */
11
+ frequency: number
12
+ /**
13
+ * 页面回退时触发
14
+ */
15
+ onPageBack?: (params: BackParams) => void
16
+ }
17
+
18
+ export type BackParams = {
19
+ /**
20
+ * 当前页面路径
21
+ */
22
+ page: string
23
+ }
24
+
25
+ export type ContextConfig = Config & {
26
+ mode: string
27
+ root: string
28
+ }
29
+
30
+ type Pages = { path: string }[]
31
+
32
+ export type PagesJson = {
33
+ pages: Pages
34
+ subpackages: { root: string; pages: Pages }
35
+ }
@@ -0,0 +1,2 @@
1
+ export const virtualFileId = 'mp-weixin-back-helper'
2
+
package/utils/index.ts ADDED
@@ -0,0 +1,159 @@
1
+ import generate from '@babel/generator'
2
+ import { parse } from '@vue/compiler-sfc'
3
+ import { babelParse, walkAST } from 'ast-kit'
4
+ import { pageContext } from '../src/context'
5
+ import { virtualFileId } from './constant'
6
+ import type { SFCDescriptor } from '@vue/compiler-sfc'
7
+ import type { Node } from '@babel/types'
8
+
9
+ function isArrowFunction(func: Function) {
10
+ if (typeof func !== 'function') return false
11
+ return !func.hasOwnProperty('prototype') && func.toString().includes('=>')
12
+ }
13
+
14
+ async function parseSFC(code: string): Promise<SFCDescriptor> {
15
+ try {
16
+ return parse(code).descriptor
17
+ } catch (error) {
18
+ throw new Error(`解析vue文件失败,请检查文件是否正确`)
19
+ }
20
+ }
21
+
22
+ export async function transformVueFile(this: pageContext, code: string, id: string) {
23
+ // 检查代码中是否已经包含page-container组件
24
+ if (code.includes('<page-container')) {
25
+ return code
26
+ }
27
+
28
+ // 检查是否包含template标签
29
+ if (!code.includes('<template')) {
30
+ return code
31
+ }
32
+
33
+ const componentStr =
34
+ '<page-container :show="__MP_BACK_SHOW_PAGE_CONTAINER__" :overlay="false" @beforeleave="onBeforeLeave" :z-index="1" :duration="false"></page-container>'
35
+
36
+ const sfc = await parseSFC(code)
37
+ const setupCode = sfc.scriptSetup?.loc.source
38
+ const setupAst = babelParse(setupCode || '', sfc.scriptSetup?.lang)
39
+ let pageBackConfig = this.config
40
+ let hasPageBack = false,
41
+ hasImportRef = false,
42
+ pageBackFnName = 'onPageBack',
43
+ callbackCode = ``
44
+
45
+ if (setupAst) {
46
+ walkAST<Node>(setupAst, {
47
+ enter(node) {
48
+ if (node.type == 'ImportDeclaration' && node.source.value.includes(virtualFileId)) {
49
+ const importSpecifier = node.specifiers[0]
50
+ hasPageBack = true
51
+ pageBackFnName = importSpecifier.local.name
52
+ }
53
+
54
+ if (node.type == 'ImportDeclaration' && node.source.value === 'vue') {
55
+ const importSpecifiers = node.specifiers
56
+ for (let i = 0; i < importSpecifiers.length; i++) {
57
+ const element = importSpecifiers[i]
58
+ if (element.local.name == 'ref') {
59
+ hasImportRef = true
60
+ break
61
+ }
62
+ }
63
+ }
64
+
65
+ if (
66
+ node.type == 'ExpressionStatement' &&
67
+ node.expression.type == 'CallExpression' &&
68
+ node.expression.callee.loc?.identifierName == pageBackFnName
69
+ ) {
70
+ const callback = node.expression.arguments[0] // 获取第一个参数
71
+ const backArguments = node.expression.arguments[1]
72
+ // 第二个参数为object才有效,覆盖插件传入的配置
73
+ if (backArguments && backArguments.type == 'ObjectExpression') {
74
+ const config = new Function(`return (${generate(backArguments).code});`)()
75
+ pageBackConfig = { ...pageBackConfig, ...config }
76
+ }
77
+
78
+ if (
79
+ callback &&
80
+ (callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')
81
+ ) {
82
+ const body = callback.body
83
+ if (body.type === 'BlockStatement') {
84
+ // 遍历 BlockStatement 的内容
85
+ body.body.forEach((statement) => {
86
+ callbackCode += generate(statement).code // 将 AST 节点生成代码
87
+ })
88
+ }
89
+ }
90
+ }
91
+ },
92
+ })
93
+ }
94
+
95
+ if (!hasPageBack) return
96
+
97
+ // 不阻止默认行为就返回到上一层
98
+ if (!pageBackConfig.preventDefault) {
99
+ callbackCode += `uni.navigateBack({ delta: 1 });`
100
+ }
101
+
102
+ // 处理统一的返回方法
103
+ const configBack = (() => {
104
+ if (!pageBackConfig.onPageBack) return ''
105
+ if (typeof pageBackConfig.onPageBack !== 'function') {
106
+ throw new Error('`onPageBack` must be a function')
107
+ }
108
+
109
+ const params = JSON.stringify({
110
+ page: this.getPageById(id),
111
+ })
112
+
113
+ const hasFunction = pageBackConfig.onPageBack.toString().includes('function')
114
+
115
+ if (isArrowFunction(pageBackConfig.onPageBack) || hasFunction) {
116
+ return `(${pageBackConfig.onPageBack})(${params});`
117
+ }
118
+
119
+ return `(function ${pageBackConfig.onPageBack})()`
120
+ })()
121
+
122
+ const beforeLeaveStr = `
123
+ ${!hasImportRef ? "import { ref } from 'vue'" : ''}
124
+ let __MP_BACK_FREQUENCY__ = 1
125
+ const __MP_BACK_SHOW_PAGE_CONTAINER__ = ref(true);
126
+ const onBeforeLeave = () => {
127
+ console.log("__MP_BACK_FREQUENCY__", __MP_BACK_FREQUENCY__, ${pageBackConfig.frequency})
128
+ if (__MP_BACK_FREQUENCY__ < ${pageBackConfig.frequency}) {
129
+ __MP_BACK_SHOW_PAGE_CONTAINER__.value = false
130
+ setTimeout(() => {
131
+ __MP_BACK_SHOW_PAGE_CONTAINER__.value = true
132
+ }, 0);
133
+ __MP_BACK_FREQUENCY__++
134
+ }
135
+ // 运行配置的匿名函数
136
+ ${configBack}
137
+ ${callbackCode}
138
+ };
139
+ `
140
+
141
+ // 在template标签后插入page-container组件和script setup声明
142
+ const result = code.replace(
143
+ /(<template.*?>)([\s\S]*?)(<\/template>)([\s\S]*?)(<script\s+(?:lang="ts"\s+)?setup.*?>|$)/,
144
+ (match, templateStart, templateContent, templateEnd, middleContent, scriptSetup) => {
145
+ // 处理script setup标签
146
+ const hasScriptSetup = Boolean(scriptSetup)
147
+ const scriptStartTag = hasScriptSetup ? scriptSetup : '<script setup>'
148
+ const scriptEndTag = hasScriptSetup ? '' : '</script>'
149
+
150
+ // 构建注入的内容
151
+ const injectedTemplate = `${templateStart}${templateContent}\n ${componentStr}\n${templateEnd}`
152
+ const injectedScript = `\n${middleContent}${scriptStartTag}\n${beforeLeaveStr}\n${scriptEndTag}`
153
+
154
+ return injectedTemplate + injectedScript
155
+ }
156
+ )
157
+
158
+ return result
159
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import mpBack from './dist/index.mjs'
4
+
5
+ export default defineConfig({
6
+ plugins: [mpBack(), vue()],
7
+ test: {
8
+ environment: 'happy-dom',
9
+ },
10
+ })