├── .babelrc ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── VERSION ├── blog └── 多平台小程序开发的差异性总结.md ├── build ├── build.js ├── clear.js ├── entrys.js └── util.js ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── ebus │ ├── README.md │ ├── event-emitter.ts │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── test │ │ └── index.js ├── func-helper │ ├── .gitignore │ ├── debug.js │ ├── hook.ts │ ├── index.ts │ ├── package.json │ ├── replace.ts │ └── test │ │ └── index.js ├── inject │ ├── README.md │ ├── config.ts │ ├── index.ts │ ├── name.d.ts │ ├── package-lock.json │ ├── package.json │ ├── plugins │ │ ├── ebus.ts │ │ ├── mixin.ts │ │ └── set-data.ts │ ├── types.ts │ └── util.ts ├── mixin │ ├── README.md │ ├── api.ts │ ├── debug.js │ ├── hook.ts │ ├── index.ts │ ├── mrege.ts │ ├── package-lock.json │ ├── package.json │ ├── store.ts │ ├── test │ │ └── index.js │ └── view.ts ├── mpspec │ ├── .gitignore │ ├── index.ts │ ├── package.json │ ├── validater.ts │ └── xml.ts ├── mpxml-parser │ ├── README.md │ ├── adapter │ │ ├── attr-base.ts │ │ ├── attr-for.ts │ │ ├── attr-where.ts │ │ ├── content.ts │ │ └── index.ts │ ├── content.ts │ ├── debug.js │ ├── index.ts │ ├── message.ts │ ├── package-lock.json │ ├── package.json │ ├── parse.ts │ ├── serialize.ts │ ├── spec.ts │ ├── test │ │ └── index.js │ ├── throw.ts │ ├── util.ts │ └── var.ts ├── mpxml-translator │ ├── .gitignore │ ├── index.ts │ ├── mp.ts │ ├── package-lock.json │ └── package.json ├── set-data │ ├── README.md │ ├── debug.js │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── test │ │ └── index.js ├── standardization │ ├── .gitignore │ ├── component.ts │ ├── global.ts │ ├── package.json │ └── tool.ts ├── types │ ├── README.md │ ├── api.ts │ ├── ebus.ts │ ├── func-helper.ts │ ├── global.d.ts │ ├── index.ts │ ├── inject.ts │ ├── mixin.ts │ ├── mock-web.ts │ ├── mpspec.ts │ ├── mpxml-parser.ts │ ├── mpxml-translator.ts │ ├── package-lock.json │ ├── package.json │ ├── platform.ts │ ├── set-data.ts │ ├── util.ts │ └── view.ts └── util │ ├── .travis.yml │ ├── README.md │ ├── index.ts │ ├── package-lock.json │ └── package.json └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env"], "@babel/preset-typescript"], 3 | "plugins": ["lodash"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | packages/**/spec 107 | mock-web 108 | # lerna publish --force-publish=* -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | cache: 5 | directories: 6 | - "node_modules" 7 | install: 8 | - npm install 9 | - lerna link 10 | before_script: 11 | - npm run build 12 | branches: 13 | only: 14 | - master 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "debug build", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/build/build.js", 13 | "outFiles": ["${workspaceFolder}/**/*.js"] 14 | }, 15 | { 16 | "type": "node", 17 | "request": "launch", 18 | "name": "debug view-parser", 19 | "skipFiles": ["/**"], 20 | "program": "${workspaceFolder}/packages/mpxml-parser/debug.js", 21 | "outFiles": ["${workspaceFolder}/**/*.js"] 22 | }, 23 | { 24 | "type": "node", 25 | "request": "launch", 26 | "name": "debug set-data", 27 | "skipFiles": ["/**"], 28 | "program": "${workspaceFolder}/packages/set-data/debug.js", 29 | "outFiles": ["${workspaceFolder}/**/*.js"] 30 | }, 31 | { 32 | "type": "node", 33 | "request": "launch", 34 | "name": "debug mixin", 35 | "skipFiles": ["/**"], 36 | "program": "${workspaceFolder}/packages/mixin/debug.js", 37 | "outFiles": ["${workspaceFolder}/**/*.js"] 38 | }, 39 | { 40 | "type": "node", 41 | "request": "launch", 42 | "name": "debug func-helper", 43 | "skipFiles": ["/**"], 44 | "program": "${workspaceFolder}/packages/func-helper/debug.js", 45 | "outFiles": ["${workspaceFolder}/**/*.js"] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "typescript.tsdk": "node_modules/typescript/lib" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 imingyu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MpKit 2 | 3 | [![Build Status](https://travis-ci.org/imingyu/mpkit.svg?branch=master)](https://travis-ci.org/imingyu/mpkit) 4 | ![image](https://img.shields.io/npm/l/@mpkit/inject.svg) 5 | [![image](https://img.shields.io/npm/v/@mpkit/inject.svg)](https://www.npmjs.com/package/@mpkit/inject) 6 | 7 | MpKit 是一个模块化的开发多平台小程序的 JavaScript 实用工具库 8 | 9 | 教程:https://imingyu.github.io/2020/mpkit/ 10 | 11 | ## 功能 12 | 13 | - ● 完全支持 14 | - ❍ 部分支持 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 85 | 86 | 87 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 108 | 109 | 110 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 131 | 132 | 133 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 155 | 156 | 157 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 175 | 176 | 177 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 198 | 199 | 200 |
状态适用语言适用平台简介
TypeScriptJavaScript小程序H5Node.js
微信支付宝百度字节跳动
42 | @mpkit/inject 45 | 已完成 56 | 提供小程序环境适用的多种实用函数或组件,如setData优化、Mixin、事件总线等。 57 | 查看文档 61 |
65 | @mpkit/ebus 68 | 已完成 79 | 提供事件触发、监听等功能。 80 | 查看文档 84 |
88 | @mpkit/mixin 91 | 已完成 102 | 为小程序提供混入功能。 103 | 查看文档 107 |
111 | @mpkit/set-data 114 | 已完成 125 | 小程序setData优化。 126 | 查看文档 130 |
134 | @mpkit/view-parser 138 | 已完成 149 | 将小程序模板编译为ast。 150 | 查看文档 154 |
158 | @mpkit/mpxml-translator 162 | 173 | 将一种小程序xml转译为另一种小程序的xml,如将wxml转为axml; 174 |
178 | @mpkit/util 181 | 已完成 192 | 工具函数。 193 | 查看文档 197 |
201 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.1 -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | const { nodeResolve } = require('@rollup/plugin-node-resolve'); 2 | const { babel } = require('@rollup/plugin-babel'); 3 | const { uglify } = require('rollup-plugin-uglify'); 4 | const rollupCommonjs = require('@rollup/plugin-commonjs'); 5 | const rollupReplace = require('@rollup/plugin-replace'); 6 | const rollupJSON = require('@rollup/plugin-json'); 7 | const path = require('path'); 8 | const fse = require('fse'); 9 | const rollup = require('rollup'); 10 | const entrys = require('./entrys'); 11 | const rollupTS = require('@rollup/plugin-typescript') 12 | const { replaceFileContent, oneByOne, copyFiles, rmdirSync } = require('./util'); 13 | const { readdirSync, existsSync, mkdirSync, readFileSync } = require('fs'); 14 | const getPackageName = (str) => { 15 | return (str || '').replace(path.resolve(__dirname, '../packages'), ''); 16 | } 17 | const version = readFileSync(path.resolve(__dirname, '../VERSION'), 'utf-8').trim(); 18 | 19 | console.log(`🌟开始编译...`); 20 | 21 | const targetPackNames = process.argv.slice(2); 22 | const specIsMoved = {}; 23 | 24 | const cloneEntry = (source) => { 25 | const res = { 26 | packageName: source.packageName, 27 | input: { 28 | input: source.input.input, 29 | }, 30 | output: { 31 | ...source.output 32 | }, 33 | done: source.done, 34 | }; 35 | if (source.output.globals) { 36 | res.output.globals = { 37 | ...source.output.globals 38 | } 39 | } 40 | if (source.input.external) { 41 | res.input.external = [ 42 | ...source.input.external 43 | ] 44 | } 45 | return res; 46 | } 47 | 48 | let index = -1; 49 | entrys.forEach((item) => { 50 | index++; 51 | if (item.output.format === 'umd' && !item.mini && !item.inclueFx) { 52 | const ni = cloneEntry(item); 53 | ni.mini = true; 54 | ni.output.file = ni.output.file.substr(0, ni.output.file.length - 2) + 'mini.js'; 55 | entrys.splice(index, 0, ni); 56 | index++; 57 | if (item.packageName === 'mpxml-parser' || item.packageName === 'mixin' || item.packageName === 'mpxml-translator') { 58 | const ni = cloneEntry(item); 59 | ni.inclueFx = true; 60 | ni.output.file = ni.output.file.substr(0, ni.output.file.length - 2) + 'full.js'; 61 | entrys.splice(index, 0, ni); 62 | index++; 63 | 64 | const ni2 = cloneEntry(item); 65 | ni2.inclueFx = true; 66 | ni2.mini = true; 67 | ni2.output.file = ni2.output.file.substr(0, ni2.output.file.length - 2) + 'full.mini.js'; 68 | entrys.splice(index, 0, ni2); 69 | } 70 | } 71 | }); 72 | entrys.forEach(item => { 73 | if (!item.output.globals) { 74 | item.output.globals = {} 75 | } 76 | item.output.globals['forgiving-xml-parser'] = 'ForgivingXmlParser'; 77 | item.output.globals['@mpkit/mpxml-parser'] = 'MpKitMpxmlParser'; 78 | item.output.globals['@mpkit/util'] = 'MpKitUtil'; 79 | item.output.globals['@mpkit/types'] = 'MpKitTypes'; 80 | item.output.globals['@mpkit/func-helper'] = 'MpKitFuncHelper'; 81 | }); 82 | 83 | oneByOne(entrys.map((rollupConfig, index) => { 84 | const fileName = getPackageName(rollupConfig.output.file); 85 | return () => { 86 | console.log(` 开始编译:${fileName}`) 87 | if (targetPackNames.length && targetPackNames.every(item => item !== rollupConfig.packageName)) { 88 | console.log(` 跳过编译:${fileName}`); 89 | return Promise.resolve(); 90 | } 91 | if (!rollupConfig.input.external) { 92 | rollupConfig.input.external = [/\@mpkit\//] 93 | rollupConfig.input.external.push(/lodash/); 94 | rollupConfig.input.external.push(/forgiving-xml-parser/); 95 | } 96 | if (!rollupConfig.input.plugins) { 97 | rollupConfig.input.plugins = []; 98 | } 99 | rollupConfig.input.plugins.push(nodeResolve()); 100 | rollupConfig.input.plugins.push(rollupReplace({ 101 | VERSION: version 102 | })); 103 | rollupConfig.input.plugins.push(rollupCommonjs()); 104 | rollupConfig.input.plugins.push(rollupJSON()); 105 | rollupConfig.input.plugins.push(rollupTS({ 106 | declaration: false 107 | })); 108 | rollupConfig.input.plugins.push(babel({ 109 | extensions: ['.ts', '.js'], 110 | babelHelpers: 'bundled', 111 | include: [ 112 | 'packages/**/*.ts' 113 | ], 114 | exclude: [ 115 | 'node_modules' 116 | ], 117 | extends: path.resolve(__dirname, '../.babelrc') 118 | })); 119 | if (rollupConfig.mini) { 120 | rollupConfig.input.plugins.push(uglify({ 121 | sourcemap: true 122 | })); 123 | } 124 | if (rollupConfig.inclueFx) { 125 | delete rollupConfig.input.external; 126 | } 127 | rollupConfig.output.sourcemap = true; 128 | rollupConfig.output.banner = `/*! 129 | * MpKit v${version} 130 | * (c) 2020-${new Date().getFullYear()} imingyu 131 | * Released under the MIT License. 132 | * Github: https://github.com/imingyu/mpkit/tree/master/packages/${rollupConfig.packageName} 133 | */`; 134 | return rollup.rollup(rollupConfig.input).then(res => { 135 | return res.write(rollupConfig.output); 136 | }).then(() => { 137 | return new Promise((resolve) => { 138 | setTimeout(() => { 139 | rollupConfig.options && rollupConfig.options.done && rollupConfig.options.done(); 140 | resolve(); 141 | }) 142 | }) 143 | }).then(() => { 144 | console.log(` 编译成功:${fileName}`); 145 | }) 146 | } 147 | }).concat(entrys.map((rollupConfig, index) => { 148 | return () => { 149 | console.log(`🌈开始转移${rollupConfig.output.file}的d.ts`); 150 | // 将所有的d.ts移到types目录下 151 | if (!specIsMoved[rollupConfig.packageName]) { 152 | specIsMoved[rollupConfig.packageName] = true; 153 | const arr = rollupConfig.input.input.split('/'); 154 | arr.splice(arr.length - 1, 1); 155 | const packageRoot = arr.join('/'); 156 | const typesOutDir = packageRoot + '/spec'; 157 | copyFiles(packageRoot, typesOutDir, srcFile => { 158 | return srcFile.endsWith('.d.ts') && !srcFile.endsWith('global.d.ts') && !srcFile.endsWith('name.d.ts'); 159 | }, true, targetFileName => { 160 | replaceFileContent(targetFileName, /\.\.\/types/, '@mpkit/types'); 161 | }) 162 | } 163 | return Promise.resolve(); 164 | } 165 | }))).then(() => { 166 | console.log(`🌈转移结束,开始复制到根目录的dist中`); 167 | const root = path.resolve(__dirname, "../packages"); 168 | const dist = path.resolve(__dirname, '../dist'); 169 | if (existsSync(dist)) { 170 | rmdirSync(dist); 171 | } 172 | mkdirSync(dist); 173 | readdirSync(root).forEach(dir => { 174 | if (dir.indexOf('.') === 0) { 175 | return; 176 | } 177 | mkdirSync(path.join(dist, dir)); 178 | fse.copyFile(path.join(root, dir, 'package.json'), path.join(dist, dir, 'package.json')); 179 | const dirDist = path.join(root, dir, 'dist'); 180 | if (existsSync(dirDist)) { 181 | mkdirSync(path.join(dist, dir, 'dist')); 182 | copyFiles(dirDist, path.join(dist, dir, 'dist'), '*', false); 183 | } 184 | const specDist = path.join(root, dir, 'spec'); 185 | if (existsSync(specDist)) { 186 | mkdirSync(path.join(dist, dir, 'spec')); 187 | copyFiles(specDist, path.join(dist, dir, 'spec'), '*', false); 188 | } 189 | }) 190 | console.log(`🌈编译全部成功.`); 191 | }).catch(err => { 192 | console.error(`🔥编译出错:${err.message}`); 193 | console.log(err); 194 | }); 195 | -------------------------------------------------------------------------------- /build/clear.js: -------------------------------------------------------------------------------- 1 | const entrys = require('./entrys'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const { clearDir } = require('./util'); 5 | const clear = dir => { 6 | if (fs.existsSync(dir)) { 7 | clearDir(dir); 8 | } else { 9 | fs.mkdirSync(dir); 10 | } 11 | } 12 | let mark = {}; 13 | entrys.forEach(rollupConfig => { 14 | if (!mark[rollupConfig.packageName]) { 15 | mark[rollupConfig.packageName] = true; 16 | clear(path.resolve(__dirname, `../packages/${rollupConfig.packageName}/dist`)); 17 | clear(path.resolve(__dirname, `../packages/${rollupConfig.packageName}/spec`)); 18 | } 19 | }); 20 | console.log(`dist与spec目录已重置。`); -------------------------------------------------------------------------------- /build/entrys.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { replaceFileContent } = require('./util'); 3 | const resolveFile = fileName => path.resolve(__dirname, `../packages${fileName}`); 4 | const formats = ['cjs', 'esm', 'umd']; 5 | const replaceInjectModules = function () { 6 | const fileName = typeof this.output === 'object' ? this.output.file : this.output; 7 | const isPlugin = fileName.indexOf('plugins') !== -1; 8 | replaceFileContent(fileName, [ 9 | [/\@mpkit\/util/g, isPlugin ? '../util' : './util'], 10 | [/\@mpkit\/types/g, isPlugin ? '../types' : './types'], 11 | ]) 12 | } 13 | const convertOptions = (options, format, p) => { 14 | let res; 15 | if (options.packageName) { 16 | res = { 17 | packageName: options.packageName, 18 | input: { 19 | input: options.input || resolveFile(`/${options.packageName}/index.ts`) 20 | }, 21 | output: { 22 | format, 23 | file: options.output || resolveFile(`/${options.packageName}/dist/index.${format}.js`), 24 | } 25 | } 26 | } else { 27 | res = { 28 | packageName: p.packageName, 29 | input: typeof options.input === 'object' ? options.input : { 30 | input: options.input 31 | }, 32 | output: Object.assign({ 33 | format, 34 | file: typeof options.output === 'object' ? options.output.file : options.output 35 | }, typeof options.output === 'object' ? options.output : {}), 36 | options 37 | } 38 | } 39 | if (res.output.format === 'umd' && !res.output.name) { 40 | const name = cssStyle2DomStyle(res.packageName); 41 | res.output.name = `MpKit${name[0].toUpperCase()}${name.substr(1)}`; 42 | } 43 | return res; 44 | } 45 | function cssStyle2DomStyle(sName) { 46 | return sName.replace(/^\-/, '').replace(/\-(\w)(\w+)/g, function (a, b, c) { 47 | return b.toUpperCase() + c.toLowerCase(); 48 | }); 49 | } 50 | module.exports = [ 51 | { 52 | packageName: 'inject', 53 | formats: ['esm'], 54 | entrys: [ 55 | { 56 | input: { 57 | input: resolveFile(`/inject/index.ts`), 58 | external: [ 59 | /\.\/config/, 60 | /\@mpkit\/util/, 61 | /\@mpkit\/types/ 62 | ] 63 | }, 64 | output: resolveFile(`/inject/dist/index.js`), 65 | done: replaceInjectModules 66 | }, 67 | { 68 | input: resolveFile(`/inject/config.ts`), 69 | output: resolveFile(`/inject/dist/config.js`) 70 | }, 71 | { 72 | input: { 73 | input: resolveFile(`/inject/plugins/ebus.ts`), 74 | external: [ 75 | /\@mpkit\/util/, 76 | /\@mpkit\/types/ 77 | ], 78 | }, 79 | output: resolveFile(`/inject/dist/plugins/ebus.js`), 80 | done: replaceInjectModules 81 | }, 82 | { 83 | input: { 84 | input: resolveFile(`/inject/plugins/mixin.ts`), 85 | external: [ 86 | /\@mpkit\/util/, 87 | /\@mpkit\/types/ 88 | ] 89 | }, 90 | output: resolveFile(`/inject/dist/plugins/mixin.js`), 91 | done: replaceInjectModules 92 | }, 93 | { 94 | input: { 95 | input: resolveFile(`/inject/plugins/set-data.ts`), 96 | external: [ 97 | /\@mpkit\/util/, 98 | /\@mpkit\/types/ 99 | ] 100 | }, 101 | output: resolveFile(`/inject/dist/plugins/set-data.js`), 102 | done: replaceInjectModules 103 | }, 104 | { 105 | input: { 106 | input: resolveFile(`/inject/types.ts`), 107 | external: [ 108 | ] 109 | }, 110 | output: resolveFile(`/inject/dist/types.js`), 111 | }, 112 | { 113 | input: { 114 | input: resolveFile(`/inject/util.ts`), 115 | external: [ 116 | /\@mpkit\/types/ 117 | ] 118 | }, 119 | output: resolveFile(`/inject/dist/util.js`), 120 | done: replaceInjectModules 121 | } 122 | ] 123 | }, 124 | 'mpxml-parser', 125 | 'mpxml-translator', 126 | 'func-helper', 127 | 'mixin', 128 | 'set-data', 129 | 'ebus', 130 | 'util', 131 | 'types', 132 | ].reduce((sum, package) => { 133 | package = typeof package === 'object' ? package : { 134 | packageName: package 135 | } 136 | if (!package.formats) { 137 | package.formats = [...formats]; 138 | } 139 | package.formats.forEach((format, index) => { 140 | if (package.entrys) { 141 | package.entrys.forEach(options => { 142 | sum.push(convertOptions(options, format, package)); 143 | }) 144 | } else { 145 | sum.push(convertOptions(package, format, package)); 146 | } 147 | }); 148 | return sum; 149 | }, []) -------------------------------------------------------------------------------- /build/util.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const fse = require('fse'); 3 | const path = require('path') 4 | exports.clearDir = dirName => { 5 | fs.readdirSync(dirName).forEach(fileName => { 6 | const fullName = path.join(dirName, fileName); 7 | const stat = fs.statSync(fullName) 8 | if (stat.isFile()) { 9 | fs.unlinkSync(fullName) 10 | } else if (stat.isDirectory()) { 11 | exports.clearDir(fullName); 12 | } 13 | }) 14 | } 15 | 16 | exports.getDirList = (dirName, deep, list) => { 17 | list = list || []; 18 | fs.readdirSync(dirName).forEach(fileName => { 19 | const fullName = path.join(dirName, fileName); 20 | const stat = fs.statSync(fullName) 21 | if (stat.isDirectory()) { 22 | list.push([fullName, fileName, dirName]) 23 | deep && exports.getDirList(fullName, deep, list); 24 | } 25 | }); 26 | return list; 27 | } 28 | 29 | exports.copyFiles = (sourceDir, targetDir, fileNameChar, deleteSourceFile, loopCallback) => { 30 | fs.readdirSync(sourceDir).forEach(item => { 31 | const sourceName = path.join(sourceDir, item); 32 | if (sourceName === targetDir || sourceName.indexOf('node_modules') !== -1) { 33 | return; 34 | } 35 | const targetName = path.join(targetDir, item); 36 | const stat = fs.statSync(sourceName) 37 | if (stat.isFile()) { 38 | if (fileNameChar === '*' || (typeof fileNameChar === 'function' && fileNameChar(sourceName)) || (typeof fileNameChar === 'string' && sourceName.indexOf(fileNameChar) !== -1)) { 39 | fse.copyFileSync(sourceName, targetName); 40 | if (deleteSourceFile) { 41 | fs.unlinkSync(sourceName) 42 | } 43 | loopCallback && loopCallback(targetName); 44 | } 45 | } else if (stat.isDirectory()) { 46 | exports.copyFiles(sourceName, targetName, fileNameChar, deleteSourceFile, loopCallback); 47 | } 48 | }) 49 | } 50 | 51 | exports.replaceFileContent = (fileName, source, target) => { 52 | let content = fs.readFileSync(fileName, 'utf8'); 53 | if (Array.isArray(source)) { 54 | source.forEach(item => { 55 | content = content.replace(item[0], item[1]); 56 | }) 57 | } else { 58 | content = content.replace(source, target); 59 | } 60 | fs.writeFileSync(fileName, content, 'utf8'); 61 | } 62 | 63 | exports.oneByOne = promiseHandlers => { 64 | return new Promise((resolve, reject) => { 65 | let index = 0; 66 | const exec = () => { 67 | promiseHandlers[index]().then(() => { 68 | index++; 69 | if (index < promiseHandlers.length) { 70 | exec(); 71 | } else { 72 | resolve(); 73 | } 74 | }).catch(reject); 75 | } 76 | exec(); 77 | }) 78 | } 79 | 80 | exports.rmdirSync = (path) => { 81 | var files = []; 82 | if (fs.existsSync(path)) { 83 | files = fs.readdirSync(path); 84 | files.forEach(function (file, index) { 85 | var curPath = path + "/" + file; 86 | if (fs.statSync(curPath).isDirectory()) { // recurse 87 | exports.rmdirSync(curPath); 88 | } else { // delete file 89 | fs.unlinkSync(curPath); 90 | } 91 | }); 92 | fs.rmdirSync(path); 93 | } 94 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/ebus", 4 | "packages/inject", 5 | "packages/mixin", 6 | "packages/func-helper", 7 | "packages/mpxml-parser", 8 | "packages/mpxml-translator", 9 | "packages/set-data", 10 | "packages/types", 11 | "packages/util" 12 | ], 13 | "version": "1.1.2" 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpkit", 3 | "private": true, 4 | "description": "mpkit 是一个模块化的开发多平台小程序的 JavaScript 实用工具库", 5 | "scripts": { 6 | "build": "node build/clear.js && tsc --emitDeclarationOnly && node --max-old-space-size=2048 build/build.js", 7 | "clear": "node build/clear.js", 8 | "dev": "tsc --emitDeclarationOnly", 9 | "test": "mocha packages/**/test/*.js" 10 | }, 11 | "author": "imingyu ", 12 | "publishConfig": { 13 | "access": "public" 14 | }, 15 | "keywords": [ 16 | "mpkit", 17 | "小程序", 18 | "miniprogram", 19 | "weapp", 20 | "wxapp", 21 | "微信小程序", 22 | "支付宝小程序", 23 | "百度小程序", 24 | "字节跳动小程序", 25 | "setData", 26 | "set-data", 27 | "setData优化", 28 | "diff", 29 | "小程序优化" 30 | ], 31 | "workspaces": [ 32 | "packages/*" 33 | ], 34 | "devDependencies": { 35 | "@babel/core": "^7.12.10", 36 | "@babel/preset-env": "^7.12.11", 37 | "@babel/preset-typescript": "^7.12.7", 38 | "@rollup/plugin-babel": "^5.2.2", 39 | "@rollup/plugin-commonjs": "^17.0.0", 40 | "@rollup/plugin-json": "^4.1.0", 41 | "@rollup/plugin-node-resolve": "^11.0.1", 42 | "@rollup/plugin-replace": "^2.3.4", 43 | "@rollup/plugin-typescript": "^8.1.0", 44 | "babel-plugin-lodash": "^3.3.4", 45 | "chai": "^4.2.0", 46 | "fse": "^4.0.1", 47 | "lerna": "^3.22.1", 48 | "mocha": "^8.2.1", 49 | "rollup": "^2.35.1", 50 | "rollup-plugin-uglify": "^6.0.4", 51 | "typescript": "^4.2.0-dev.20210126" 52 | }, 53 | "dependencies": { 54 | "tslib": "^2.1.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/ebus/README.md: -------------------------------------------------------------------------------- 1 | # @mpkit/ebus 2 | 3 | [![Build Status](https://travis-ci.org/imingyu/mpkit.svg?branch=master)](https://travis-ci.org/imingyu/mpkit) 4 | ![image](https://img.shields.io/npm/l/@mpkit/ebus.svg) 5 | [![image](https://img.shields.io/npm/v/@mpkit/ebus.svg)](https://www.npmjs.com/package/@mpkit/ebus) 6 | [![image](https://img.shields.io/npm/dt/@mpkit/ebus.svg)](https://www.npmjs.com/package/@mpkit/ebus) 7 | 8 | 提供事件触发、监听等功能。 9 | 10 | - `on(type:string, handler:Function)` 11 | - `off(type:string, handler:Function)` 12 | - `emit(type:string, data:any)` 13 | - `EventEmitter` 14 | -------------------------------------------------------------------------------- /packages/ebus/event-emitter.ts: -------------------------------------------------------------------------------- 1 | import { MkMap } from "@mpkit/types"; 2 | import { EventHandler, EBus } from "@mpkit/types"; 3 | export default class EventEmitter implements EBus { 4 | private events: MkMap = {}; 5 | constructor() { 6 | ["on", "off", "emit"].forEach((prop) => { 7 | this[prop] = this[prop].bind(this); 8 | }); 9 | } 10 | on(type: string, handler: EventHandler) { 11 | if (!this.events[type]) { 12 | this.events[type] = [] as EventHandler[]; 13 | } 14 | if (this.events[type].indexOf(handler) === -1) { 15 | this.events[type].push(handler); 16 | } 17 | } 18 | off(type: string, handler?: EventHandler) { 19 | if (this.events[type]) { 20 | if (handler) { 21 | const index = this.events[type].indexOf(handler); 22 | index !== -1 && this.events[type].splice(index, 1); 23 | } else { 24 | delete this.events[type]; 25 | } 26 | } 27 | } 28 | emit(type: string, data: any) { 29 | this.events[type] && 30 | this.events[type].forEach((handler) => { 31 | handler({ 32 | type, 33 | ts: Date.now(), 34 | data, 35 | }); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/ebus/index.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from "./event-emitter"; 2 | import { MpKitPlugin } from "@mpkit/types"; 3 | const ev = new EventEmitter(); 4 | export { EventEmitter }; 5 | export const on = ev.on; 6 | export const off = ev.off; 7 | export const emit = ev.emit; 8 | export const plugin: MpKitPlugin = { 9 | name: "ebus", 10 | apply(mpkit) { 11 | ["on", "off", "emit"].forEach((method) => { 12 | mpkit[method] = ev[method]; 13 | }); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ebus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mpkit/ebus", 3 | "version": "1.1.2", 4 | "description": "提供事件触发、监听等功能", 5 | "author": "imingyu ", 6 | "homepage": "https://github.com/imingyu/mpkit/tree/master/packages/ebus", 7 | "license": "MIT", 8 | "main": "dist/index.cjs.js", 9 | "module": "dist/index.esm.js", 10 | "types": "spec/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "spec" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/imingyu/mpkit.git" 18 | }, 19 | "scripts": {}, 20 | "bugs": { 21 | "url": "https://github.com/imingyu/mpkit/issues" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": [ 27 | "mpkit", 28 | "小程序", 29 | "miniprogram", 30 | "ebus" 31 | ], 32 | "dependencies": { 33 | "@mpkit/types": "^1.1.2", 34 | "@mpkit/util": "^1.1.2" 35 | }, 36 | "devDependencies": { 37 | "chai": "^4.2.0", 38 | "mocha": "^8.1.1" 39 | }, 40 | "gitHead": "0a4dc4e319b5134237165e9b09e812420b0d52a0" 41 | } 42 | -------------------------------------------------------------------------------- /packages/ebus/test/index.js: -------------------------------------------------------------------------------- 1 | const { assert } = require("chai"); 2 | const { EventEmitter, on, off, emit } = require('../dist/index.cjs'); 3 | describe("Ebus", () => { 4 | it('功能', done => { 5 | const state = {}; 6 | const ev = new EventEmitter(); 7 | ev.on('e1', e => { 8 | assert.equal(false, true); 9 | }); 10 | ev.on('e2', e => { 11 | assert.equal(data, e.data); 12 | assert.equal('e2', e.type); 13 | checkDone('e2'); 14 | }); 15 | const checkDone = type => { 16 | state[type] = true; 17 | if (state.on && state.off && state.e2) { 18 | done(); 19 | } 20 | } 21 | const data = {}; 22 | let count = 0; 23 | on("e1", e => { 24 | assert.equal(data, e.data); 25 | assert.equal('e1', e.type); 26 | count++; 27 | if (count > 1) { 28 | console.error('未正确卸载on') 29 | assert.equal(false, true); 30 | } 31 | checkDone('on'); 32 | }); 33 | emit('e1', data); 34 | off('e1'); 35 | emit('e1', data); 36 | checkDone('off'); 37 | ev.emit('e2', data); 38 | }) 39 | }) -------------------------------------------------------------------------------- /packages/func-helper/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts -------------------------------------------------------------------------------- /packages/func-helper/debug.js: -------------------------------------------------------------------------------- 1 | const { replaceFunc, hookFunc } = require('./dist/index.cjs.js'); const data = {}; 2 | const foo = () => { 3 | if (!data.a) { 4 | data.a = 0 5 | } 6 | data.a++; 7 | return Promise.resolve(data.a); 8 | } 9 | const hooks = [ 10 | { 11 | before() { 12 | data.before = true; 13 | }, 14 | after() { 15 | data.after = true; 16 | }, 17 | catch() { 18 | data.catch = true; 19 | }, 20 | complete(s) { 21 | data.complete = true; 22 | assert.equal(s.value, data.a); 23 | }, 24 | done(s) { 25 | data.s = s; 26 | data.done = true; 27 | } 28 | } 29 | ]; 30 | const foo2 = hookFunc(foo, false, hooks, { 31 | e: 1 32 | }).func; 33 | const res = foo2(); 34 | console.log(data); 35 | res.then(() => { 36 | console.log(data); 37 | }) -------------------------------------------------------------------------------- /packages/func-helper/hook.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MkFuncHook, 3 | MkFuncHookErrorType, 4 | MkFuncHookHandler, 5 | MkFuncHookName, 6 | MkFuncHookResult, 7 | MkFuncHookState, 8 | } from "@mpkit/types"; 9 | 10 | /**使用多种钩子钩住函数,并返回处理后的函数 */ 11 | export const hookFunc = (() => { 12 | const isFunc = (item: any) => 13 | typeof item === "function" || 14 | (Array.isArray(item) && 15 | (item as any[]).some((it) => typeof it === "function")); 16 | const hookNames = ["before", "after", "catch", "complete", "done"]; 17 | return ( 18 | func: T, 19 | hooksInvariant: boolean, 20 | hooks: MkFuncHook[], 21 | otherState?: any 22 | ): MkFuncHookResult => { 23 | let enabled: any = { 24 | before: 1, 25 | after: 1, 26 | catch: 1, 27 | complete: 1, 28 | done: 1, 29 | }; 30 | const has: any = {}; 31 | 32 | const hasHook = (name: MkFuncHookName): boolean => { 33 | if (!hooksInvariant) { 34 | // 如果hooks不是恒定的,则需要每次动态查询 35 | return hooks.some((item) => item && isFunc(item[name])); 36 | } 37 | if (!(name in has)) { 38 | hooks.forEach((item) => { 39 | if (!item) { 40 | return; 41 | } 42 | hookNames.forEach((n) => { 43 | if (isFunc(item[n])) { 44 | has[n] = 1; 45 | } 46 | }); 47 | }); 48 | hookNames.forEach((n) => { 49 | if (!(n in has)) { 50 | has[n] = 0; 51 | } 52 | }); 53 | } 54 | return has[name]; 55 | }; 56 | const targetFunc = (function MkFuncHelperOfHookTarget(...args) { 57 | const ctx = this; 58 | const state: MkFuncHookState = { 59 | ctx, 60 | func, 61 | args, 62 | state: {} as S, 63 | stepResultList: [], 64 | doneCallback(err: Error, res?: any): any { 65 | if (state.done) { 66 | return; 67 | } 68 | if (err) { 69 | fireCatch("RejectReason", err); 70 | state.fulfilled = false; 71 | } else { 72 | state.value = res; 73 | state.fulfilled = true; 74 | } 75 | fireHook("complete"); 76 | checkFireDone("callback"); 77 | }, 78 | }; 79 | if (otherState) { 80 | Object.assign(state.state, otherState); 81 | } 82 | 83 | const fireHook = (name: MkFuncHookName) => { 84 | if (name !== "done" && name !== "catch" && state.stop) { 85 | return; 86 | } 87 | let needExec = !enabled[name] 88 | ? false 89 | : hooksInvariant 90 | ? hasHook(name) 91 | : true; 92 | if (needExec) { 93 | try { 94 | for (let i = 0, len = hooks.length; i < len; i++) { 95 | const hook = hooks[i]; 96 | if (hook && hook[name]) { 97 | if (Array.isArray(hook[name])) { 98 | const list = hook[ 99 | name 100 | ] as MkFuncHookHandler[]; 101 | for ( 102 | let j = 0, lj = list.length; 103 | j < lj; 104 | j++ 105 | ) { 106 | state.stepResultList.push({ 107 | step: name, 108 | result: list[j] 109 | ? list[j](state) 110 | : undefined, 111 | }); 112 | if ( 113 | name !== "done" && 114 | name !== "catch" && 115 | state.stop 116 | ) { 117 | break; 118 | } 119 | } 120 | } else { 121 | state.stepResultList.push({ 122 | step: name, 123 | result: (hook[ 124 | name 125 | ] as MkFuncHookHandler)(state), 126 | }); 127 | } 128 | } 129 | if ( 130 | name !== "done" && 131 | name !== "catch" && 132 | state.stop 133 | ) { 134 | break; 135 | } 136 | } 137 | } catch (e) { 138 | const type = `${name[0].toUpperCase()}${name.substr( 139 | 1 140 | )}Exception` as MkFuncHookErrorType; 141 | if (name !== "catch") { 142 | fireCatch(type, e); 143 | } else { 144 | state.errors.push({ 145 | type, 146 | error: e, 147 | }); 148 | } 149 | } 150 | } 151 | }; 152 | const fireCatch = (type: MkFuncHookErrorType, error) => { 153 | if (!state.errors) { 154 | state.errors = []; 155 | } 156 | state.errors.push({ 157 | type, 158 | error, 159 | }); 160 | if (hasHook("catch")) { 161 | try { 162 | fireHook("catch"); 163 | } catch (e) { 164 | state.errors.push({ 165 | type: "CatchException", 166 | error: e, 167 | }); 168 | } 169 | } 170 | }; 171 | const doneStep: any = {}; 172 | const checkFireDone = (step: string) => { 173 | doneStep[step] = 1; 174 | const isPromise = 175 | typeof state.result === "object" && 176 | state.result && 177 | (state.result.then || state.result.catch); 178 | if (state.needDoneCallback && isPromise) { 179 | if (doneStep.promise && doneStep.callback) { 180 | state.done = true; 181 | fireHook("done"); 182 | } 183 | } else { 184 | state.done = true; 185 | fireHook("done"); 186 | } 187 | }; 188 | fireHook("before"); 189 | if (state.stop) { 190 | fireHook("done"); 191 | return state.result; 192 | } 193 | try { 194 | const res = func.apply(ctx, args); 195 | state.result = res; 196 | fireHook("after"); 197 | const isPromise = 198 | typeof state.result === "object" && 199 | state.result && 200 | (state.result.then || state.result.catch); 201 | if (state.needDoneCallback || isPromise) { 202 | if (isPromise && state.result.then) { 203 | state.result.then((value) => { 204 | state.value = value; 205 | state.fulfilled = true; 206 | fireHook("complete"); 207 | checkFireDone("promise"); 208 | }); 209 | } 210 | if (isPromise && state.result.catch) { 211 | state.result.catch((e) => { 212 | state.fulfilled = false; 213 | fireCatch("RejectReason", e); 214 | fireHook("complete"); 215 | checkFireDone("promise"); 216 | }); 217 | } 218 | } else { 219 | fireHook("done"); 220 | } 221 | return state.result; 222 | } catch (e) { 223 | fireCatch("MethodException", e); 224 | fireHook("done"); 225 | throw e; 226 | } 227 | } as unknown) as T; 228 | return { 229 | func: targetFunc, 230 | disable(name?: MkFuncHookName) { 231 | if (name) { 232 | enabled[name] = 0; 233 | } else { 234 | hookNames.forEach((n) => { 235 | enabled[n] = 0; 236 | }); 237 | } 238 | }, 239 | enable(name?: MkFuncHookName) { 240 | if (name) { 241 | enabled[name] = 1; 242 | } else { 243 | hookNames.forEach((n) => { 244 | enabled[n] = 1; 245 | }); 246 | } 247 | }, 248 | }; 249 | }; 250 | })(); 251 | -------------------------------------------------------------------------------- /packages/func-helper/index.ts: -------------------------------------------------------------------------------- 1 | export { replaceFunc } from "./replace"; 2 | export { hookFunc } from "./hook"; 3 | -------------------------------------------------------------------------------- /packages/func-helper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mpkit/func-helper", 3 | "version": "1.1.2", 4 | "description": "mpkit 函数替换", 5 | "author": "imingyu ", 6 | "homepage": "https://github.com/imingyu/mpkit/tree/master/packages/func-helper", 7 | "license": "MIT", 8 | "main": "dist/index.cjs.js", 9 | "module": "dist/index.esm.js", 10 | "types": "spec/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "spec" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/imingyu/mpkit.git" 18 | }, 19 | "scripts": {}, 20 | "bugs": { 21 | "url": "https://github.com/imingyu/mpkit/issues" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": [ 27 | "mpkit", 28 | "小程序", 29 | "miniprogram", 30 | "util" 31 | ], 32 | "devDependencies": { 33 | "chai": "^4.2.0", 34 | "mocha": "^8.1.1" 35 | }, 36 | "dependencies": { 37 | "@mpkit/types": "^1.1.2" 38 | }, 39 | "gitHead": "0a4dc4e319b5134237165e9b09e812420b0d52a0" 40 | } 41 | -------------------------------------------------------------------------------- /packages/func-helper/replace.ts: -------------------------------------------------------------------------------- 1 | import { MkReplaceFuncCallback } from "@mpkit/types"; 2 | 3 | export function replaceFunc( 4 | original: T, 5 | replacer: T, 6 | callback?: MkReplaceFuncCallback, 7 | data?: any 8 | ): T { 9 | let isReplace = true; 10 | callback && 11 | callback({ 12 | data, 13 | original, 14 | replace() { 15 | isReplace = true; 16 | }, 17 | restore() { 18 | isReplace = false; 19 | }, 20 | }); 21 | return (function (...args) { 22 | if (isReplace) { 23 | return replacer.apply(this, args); 24 | } else { 25 | return original.apply(this, args); 26 | } 27 | } as unknown) as T; 28 | } 29 | -------------------------------------------------------------------------------- /packages/func-helper/test/index.js: -------------------------------------------------------------------------------- 1 | const { replaceFunc, hookFunc } = require('../dist/index.cjs.js'); 2 | const { assert } = require('chai'); 3 | 4 | describe('FuncHelper', () => { 5 | it('ReplaceFunc', () => { 6 | const state = {} 7 | const foo = () => { 8 | if (!state.a) { 9 | state.a = 0 10 | } 11 | state.a++; 12 | } 13 | const foo2 = replaceFunc(foo, () => { 14 | if (!state.d) { 15 | state.d = 0 16 | } 17 | state.d++; 18 | }, store => { 19 | state.c = store; 20 | }, { 21 | b: 1 22 | }); 23 | assert.isTrue('c' in state); 24 | assert.equal(state.c.data.b, 1); 25 | assert.equal(state.c.original, foo); 26 | foo2(); 27 | assert.isTrue(!('a' in state)); 28 | assert.equal(state.d, 1); 29 | state.c.restore(); 30 | foo2(); 31 | assert.equal(state.a, 1); 32 | state.c.replace(); 33 | foo2(); 34 | assert.equal(state.a, 1); 35 | assert.equal(state.d, 2); 36 | }); 37 | describe('HookFunc', () => { 38 | it('hooksInvariant=true', () => { 39 | const hooksInvariant = true; 40 | const data = {}; 41 | const foo = () => { 42 | if (!data.a) { 43 | data.a = 0 44 | } 45 | data.a++; 46 | } 47 | const hooks = []; 48 | const foo2 = hookFunc(foo, hooksInvariant, hooks, { 49 | e: 1 50 | }).func; 51 | foo2(); 52 | assert.equal(data.a, 1); 53 | hooks.push({ 54 | before() { 55 | data.before = true; 56 | }, 57 | after() { 58 | data.after = true; 59 | }, 60 | catch() { 61 | data.catch = true; 62 | }, 63 | complete() { 64 | data.complete = true; 65 | } 66 | }); 67 | foo2(); 68 | assert.isTrue(!('before' in data)); 69 | assert.isTrue(!('after' in data)); 70 | assert.isTrue(!('catch' in data)); 71 | assert.isTrue(!('complete' in data)); 72 | assert.equal(data.a, 2); 73 | 74 | const foo3 = hookFunc(foo, hooksInvariant, [ 75 | { 76 | before(s) { 77 | data.before = true; 78 | assert.equal(s.state.e, 2); 79 | }, 80 | after: [ 81 | () => { 82 | data.after = true; 83 | }, 84 | () => { 85 | data.after2 = true; 86 | }, 87 | ], 88 | catch() { 89 | data.catch = true; 90 | }, 91 | complete() { 92 | data.complete = true; 93 | } 94 | } 95 | ], { 96 | e: 2 97 | }).func; 98 | foo3(); 99 | assert.equal(data.a, 3); 100 | assert.isTrue('before' in data); 101 | assert.isTrue('after' in data); 102 | assert.isTrue('after2' in data); 103 | assert.isTrue(!('catch' in data)); 104 | assert.isTrue(!('complete' in data)); 105 | }); 106 | it('hooksInvariant=false', () => { 107 | const hooksInvariant = false; 108 | const data = {}; 109 | const foo = () => { 110 | if (!data.a) { 111 | data.a = 0 112 | } 113 | data.a++; 114 | } 115 | const hooks = []; 116 | const foo2 = hookFunc(foo, hooksInvariant, hooks, { 117 | e: 1 118 | }).func; 119 | foo2(); 120 | assert.equal(data.a, 1); 121 | hooks.push({ 122 | before() { 123 | data.before = true; 124 | }, 125 | after() { 126 | data.after = true; 127 | }, 128 | catch() { 129 | data.catch = true; 130 | }, 131 | complete() { 132 | data.complete = true; 133 | } 134 | }); 135 | foo2(); 136 | assert.isTrue(('before' in data)); 137 | assert.isTrue(('after' in data)); 138 | assert.isTrue(!('catch' in data)); 139 | assert.isTrue(!('complete' in data)); 140 | assert.equal(data.a, 2); 141 | }); 142 | it('promise', (done) => { 143 | const data = {}; 144 | const foo = () => { 145 | if (!data.a) { 146 | data.a = 0 147 | } 148 | data.a++; 149 | return Promise.resolve(data.a); 150 | } 151 | const hooks = [ 152 | { 153 | before() { 154 | data.before = true; 155 | }, 156 | after() { 157 | data.after = true; 158 | }, 159 | catch() { 160 | data.catch = true; 161 | }, 162 | complete(s) { 163 | data.complete = true; 164 | assert.equal(s.value, data.a); 165 | }, 166 | done() { 167 | data.done = true; 168 | } 169 | } 170 | ]; 171 | const foo2 = hookFunc(foo, false, hooks, { 172 | e: 1 173 | }).func; 174 | const res = foo2(); 175 | assert.isTrue(('before' in data)); 176 | assert.isTrue(('after' in data)); 177 | assert.isTrue(!('catch' in data)); 178 | assert.isTrue(!('done' in data)); 179 | assert.isTrue(!('complete' in data)); 180 | assert.equal(data.a, 1); 181 | console.log('1.' + JSON.stringify(data)); 182 | res.then(() => { 183 | console.log('1.' + JSON.stringify(data)); 184 | assert.isTrue(('done' in data)); 185 | assert.isTrue(('complete' in data)); 186 | done(); 187 | }) 188 | }) 189 | }); 190 | }) -------------------------------------------------------------------------------- /packages/inject/README.md: -------------------------------------------------------------------------------- 1 | # @mpkit/inject 2 | 3 | [![Build Status](https://travis-ci.org/imingyu/mpkit.svg?branch=master)](https://travis-ci.org/imingyu/mpkit) 4 | ![image](https://img.shields.io/npm/l/@mpkit/inject.svg) 5 | [![image](https://img.shields.io/npm/v/@mpkit/inject.svg)](https://www.npmjs.com/package/@mpkit/inject) 6 | [![image](https://img.shields.io/npm/dt/@mpkit/inject.svg)](https://www.npmjs.com/package/@mpkit/inject) 7 | 8 | 提供小程序环境适用的多种实用函数或组件,如 setData 优化、Mixin、事件总线等。 9 | 10 | 教程:https://imingyu.github.io/2020/mpkit/ 11 | 12 | | 方法/变量 | 作用 | 依赖插件 | 13 | | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -------- | 14 | | `on(eventName:string, handler:Function)` | 为某全局事件添加监听函数 | ebus | 15 | | `off(eventName:string, handler:Function)` | 为某全局事件移除监听函数 | ebus | 16 | | `emit(eventName:string, data:any)` | 触发某全局事件并传递数据 | ebus | 17 | | `App(...mixins:MpAppSpec[]) : MpAppSpec` | 接收多个对象,对象结构与小程序`App`函数接收的对象结构一致,并将这些对象合并,返回一个新对象,包含所有对象的功能和数据,合并策略下文描述。 | mixin | 18 | | `Page(...mixins:MpPageSpec[]) : MpPageSpec` | 接收多个对象,对象结构与小程序`Page`函数接收的对象结构一致,并将这些对象合并,返回一个新对象,包含所有对象的功能和数据,合并策略下文描述。 | mixin | 19 | | `Component(...mixins:MpComponentSpec[]) : MpComponentSpec` | 接收多个对象,对象结构与小程序`Component`函数接收的对象结构一致,并将这些对象合并,返回一个新对象,包含所有对象的功能和数据,合并策略下文描述。 | mixin | 20 | | `Api` | 与小程序上的`wx` `my` `swan` `tt`等对象的属性和方法一致,只不过在其方法上添加了钩子函数,方便拦截处理 | mixin | 21 | | `MixinStore.addHook(type:string, hook:MpMethodHook)` | `MpKit`中内置了很多全局钩子函数,方可实现了全局拦截,setData 重写等功能,而如果你也想在全局添加自己的钩子函数,那么可以调用此函数 | mixin | 22 | | `setData(view:any, data:any, callback:Function) : Promise` | 以优化的方式向某个 Page/Component 设置数据,仅设置变化的数据,并以 Promise 的方式返回 diff 后的数据结果 | set-data | 23 | -------------------------------------------------------------------------------- /packages/inject/config.ts: -------------------------------------------------------------------------------- 1 | import { MpKitPlugin, MpKitConfig } from "@mpkit/types"; 2 | export default { 3 | rewrite: { 4 | App: false, 5 | Page: false, 6 | Component: false, 7 | Api: false, 8 | setData: false, 9 | }, 10 | plugins: [] as MpKitPlugin[], 11 | } as MpKitConfig; 12 | -------------------------------------------------------------------------------- /packages/inject/index.ts: -------------------------------------------------------------------------------- 1 | import { MpKitInject, MpKitPlugin, MkMap } from "@mpkit/types"; 2 | import { getApiVar } from "@mpkit/util"; 3 | import MpKitConfig from "./config"; 4 | class MpKit implements MpKitInject { 5 | version = "VERSION"; 6 | private plugins: MkMap = {}; 7 | static mopckApi(apiName: string, pluginNames: string | string[]) { 8 | return function (this: MpKit, ...args) { 9 | const pluginName = Array.isArray(pluginNames) 10 | ? pluginNames.join(",") 11 | : pluginNames; 12 | console.warn( 13 | `${apiName}的功能需要${pluginName}插件实现,请在引入MpKit时,调用plugin方法装载这些插件。` 14 | ); 15 | return args[0]; 16 | }; 17 | } 18 | Api = getApiVar(); 19 | on = MpKit.mopckApi("on", "ebus"); 20 | off = MpKit.mopckApi("off", "ebus"); 21 | emit = MpKit.mopckApi("emit", "ebus"); 22 | App = MpKit.mopckApi("App", "mixin"); 23 | Page = MpKit.mopckApi("App", "mixin"); 24 | Component = MpKit.mopckApi("App", "mixin"); 25 | setData = MpKit.mopckApi("setData", "setData"); 26 | getParentView = MpKit.mopckApi("getParentView", ["mixin", "view"]); 27 | getChildrenView = MpKit.mopckApi("getParentView", ["mixin", "view"]); 28 | plugin(plugin: MpKitPlugin) { 29 | this.plugins[plugin.name] = plugin; 30 | plugin.apply(this, MpKitConfig); 31 | } 32 | hasPlugin(name: string): boolean { 33 | return !!this.plugins[name]; 34 | } 35 | } 36 | const defaultMpKit = new MpKit() as MpKitInject; 37 | if (MpKitConfig && MpKitConfig.plugins) { 38 | MpKitConfig.plugins.forEach((plugin) => { 39 | defaultMpKit.plugin(plugin); 40 | }); 41 | } 42 | export default defaultMpKit; 43 | -------------------------------------------------------------------------------- /packages/inject/name.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.json"; 2 | -------------------------------------------------------------------------------- /packages/inject/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mpkit/inject", 3 | "version": "1.1.2", 4 | "description": "提供小程序环境适用的多种实用函数或组件,如setData优化、Mixin、事件总线等。", 5 | "author": "imingyu ", 6 | "homepage": "https://github.com/imingyu/mpkit/tree/master/packages/inject", 7 | "license": "MIT", 8 | "main": "dist/index.js", 9 | "module": "dist/index.js", 10 | "types": "spec/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "spec" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/imingyu/mpkit.git" 18 | }, 19 | "scripts": {}, 20 | "bugs": { 21 | "url": "https://github.com/imingyu/mpkit/issues" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": [ 27 | "mpkit", 28 | "小程序", 29 | "miniprogram", 30 | "weapp", 31 | "wxapp", 32 | "微信小程序", 33 | "支付宝小程序", 34 | "百度小程序", 35 | "字节跳动小程序", 36 | "mixin", 37 | "ebus", 38 | "setData", 39 | "set-data", 40 | "setData优化", 41 | "diff", 42 | "小程序优化" 43 | ], 44 | "dependencies": { 45 | "@mpkit/ebus": "^1.1.2", 46 | "@mpkit/mixin": "^1.1.2", 47 | "@mpkit/set-data": "^1.1.2", 48 | "@mpkit/types": "^1.1.2", 49 | "@mpkit/util": "^1.1.2" 50 | }, 51 | "devDependencies": { 52 | "chai": "^4.2.0", 53 | "mocha": "^8.1.1" 54 | }, 55 | "gitHead": "0a4dc4e319b5134237165e9b09e812420b0d52a0" 56 | } 57 | -------------------------------------------------------------------------------- /packages/inject/plugins/ebus.ts: -------------------------------------------------------------------------------- 1 | export * from "@mpkit/ebus"; 2 | -------------------------------------------------------------------------------- /packages/inject/plugins/mixin.ts: -------------------------------------------------------------------------------- 1 | export * from "@mpkit/mixin"; 2 | -------------------------------------------------------------------------------- /packages/inject/plugins/set-data.ts: -------------------------------------------------------------------------------- 1 | export * from "@mpkit/set-data"; 2 | -------------------------------------------------------------------------------- /packages/inject/types.ts: -------------------------------------------------------------------------------- 1 | export * from "@mpkit/types"; 2 | -------------------------------------------------------------------------------- /packages/inject/util.ts: -------------------------------------------------------------------------------- 1 | export * from "@mpkit/util"; 2 | -------------------------------------------------------------------------------- /packages/mixin/README.md: -------------------------------------------------------------------------------- 1 | # @mpkit/mixin 2 | 3 | [![Build Status](https://travis-ci.org/imingyu/mpkit.svg?branch=master)](https://travis-ci.org/imingyu/mpkit) 4 | ![image](https://img.shields.io/npm/l/@mpkit/mixin.svg) 5 | [![image](https://img.shields.io/npm/v/@mpkit/mixin.svg)](https://www.npmjs.com/package/@mpkit/mixin) 6 | [![image](https://img.shields.io/npm/dt/@mpkit/mixin.svg)](https://www.npmjs.com/package/@mpkit/mixin) 7 | 8 | 为小程序提供混入功能。 9 | 10 | ## 功能列表 11 | 12 | - `MkApp(...appSpecList:MpAppSpec[]):MpAppSpec` 13 | - `MkPage(...pageSpecList:MpPageSpec[]):MpPageSpec` 14 | - `MkComponent(...componentSpecList:MpComponentSpec[]):MpComponentSpec` 15 | 16 | 将多个对象进行混入操作,并在合并方法时添加钩子函数,方便统一拦截,可使用`MixinStore.addHook`添加全局钩子; 17 | 18 | ```javascript 19 | import { MkApp } from "@mpkit/mixin"; 20 | const appSpec = MkApp( 21 | { 22 | globalData: { 23 | user: { 24 | name: "Tom", 25 | }, 26 | }, 27 | onShow() { 28 | console.log("onShow1"); 29 | }, 30 | }, 31 | { 32 | globalData: { 33 | user: { 34 | name: "Alice", 35 | age: 20, 36 | }, 37 | }, 38 | onShow() { 39 | console.log("onShow2"); 40 | }, 41 | } 42 | ); 43 | console.log(appSpec); 44 | /* 45 | { 46 | globalData: { user: { name: 'Alice', age: 20 } }, 47 | onLaunch: { [Function] displayName: 'onLaunch' }, 48 | onShow: { [Function] displayName: 'onShow' } 49 | } 50 | */ 51 | App(appSpec); 52 | // 输出onShow1 53 | // 输出onShow2 54 | ``` 55 | 56 | - `MkApi` 57 | 58 | 与小程序原生的 Api 对象拥有的属性和方法一致,并在方法中添加了钩子函数,方便统一拦截,可使用`MixinStore.addHook`添加全局钩子; 59 | 60 | ```javascript 61 | import { MkApi } from "@mpkit/mixin"; 62 | // 等同于wx.request | my.request | tt.request ... 63 | const task = MkApi.request({ 64 | url: "...", 65 | success(res) { 66 | console.log(res); 67 | }, 68 | }); 69 | task.abort(); 70 | ``` 71 | 72 | - `MkApi.promiseify(apiName: string, ...apiArgs: any[])` 73 | 执行 api 方法,并以 Promise 的形式返回,本方法遵从以下规约: 74 | 75 | - 如果`apiName`对应的对象不是函数,则直接返回`Promise.resolve(MkApi[apiName])` 76 | - 如果`apiName`对应的对象是同步 api(如:getStorageSync),则以`Promise.resolve`的方式返回 api 的同步执行结果,如果执行中发生错误,则返回`Promise.reject(err)` 77 | 78 | ```javascript 79 | import { MkApi } from "@mpkit/mixin"; 80 | MkApi.promiseify("request", { 81 | url: "..", 82 | }).then((res) => { 83 | console.log(res); 84 | }); 85 | ``` 86 | 87 | - `MixinStore` 88 | 89 | - `addHook(type:MpViewType.App|MpViewType.Page|MpViewType.Component|'Api', hook:MpMethodHook)` 90 | 91 | 可调用`MixinStore.addHook`为 App/Page/Component/Api 添加全局钩子函数; 92 | 93 | ```typescript 94 | interface MpMethodHookLike { 95 | before?( 96 | methodName: string, 97 | methodArgs: any[], 98 | methodHandler: Function, 99 | funId?: string 100 | ); 101 | after?( 102 | methodName: string, 103 | methodArgs: any[], 104 | methodResult: any, 105 | funId?: string 106 | ); 107 | catch?( 108 | methodName: string, 109 | methodArgs: any[], 110 | error: Error, 111 | errType?: string, 112 | funId?: string 113 | ); 114 | complete?( 115 | methodName: string, 116 | methodArgs: any[], 117 | res: any, 118 | success?: boolean, 119 | funId?: string 120 | ); 121 | } 122 | interface MpMethodHook extends MpMethodHookLike { 123 | [prop: string]: Function | MpMethodHookLike; 124 | } 125 | ``` 126 | 127 | ```javascript 128 | import { MixinStore, MkApp, MkApi } from "@mpkit/mixin"; 129 | import { MpViewType } from "@mpkit/type"; 130 | MixinStore.addHook(MpViewType.App, { 131 | before(methodName, methodArgs) { 132 | console.log(`before methodName=${methodName}`); 133 | }, 134 | after(methodName, methodArgs, methodResult) { 135 | console.log(`after methodName=${methodName}, ${methodResult}`); 136 | }, 137 | catch(methodName, methodArgs, error) { 138 | console.log(`catch err=${error.message}`); 139 | }, 140 | }); 141 | App( 142 | MkApp({ 143 | onLaunch() { 144 | this.add(1, 2); 145 | }, 146 | onShow() { 147 | throw new Error("test"); 148 | }, 149 | add(a, b) { 150 | return a + b; 151 | }, 152 | }) 153 | ); 154 | // 输出:before methodName=onLaunch 155 | // 输出:before methodName=add 156 | // 输出:after methodName=add, 2 157 | // 输出:after methodName=onLaunch, 158 | // 输出:before methodName=onShow, 159 | // 输出:catch err=test 160 | 161 | MixinStore.addHook("Api", { 162 | before(methodName, methodArgs, methodHandler, funId) { 163 | console.log(`before api=${methodName}`); 164 | }, 165 | after(methodName, methodArgs, methodResult, funId) { 166 | console.log(`after api=${methodName}, ${methodResult}`); 167 | }, 168 | complete(methodName, methodArgs, res, isSuccess, funId) { 169 | console.log(`complete api=${methodName}, ${isSuccess}, ${res}`); 170 | }, 171 | }); 172 | MkApi.request({ 173 | url: "...", 174 | }); 175 | // 输出:before api=request 176 | // 输出:after api=request, [RequestTask Object] 177 | // 假设请求成功且返回字符串“1”,则输出:complete api=request, true, 1 178 | // 假设请求失败,则输出:complete api=request, false, { errMsg:'...' } 179 | ``` 180 | 181 | 在调用`MixinStore.addHook`传递的`MpMethodHook`参数可以是: 182 | 183 | - 包含`before/after/catch/complete`属性的对象 184 | - 也可以是包含任意属性名,属性值是包含`before/after/catch/complete`的对象 185 | 当属性名非`before/after/catch/complete`时,则会在调用钩子函数时,去`MpMethodHook`参数中寻找属性名是方法名的对象,如: 186 | 187 | ```javascript 188 | MixinStore.addHook(MpViewType.App, { 189 | onShow: { 190 | before(methodName, methodArgs) { 191 | console.log(`before methodName=${methodName}`); 192 | }, 193 | }, 194 | }); 195 | App( 196 | MkApp({ 197 | onLaunch() {}, 198 | onShow() {}, 199 | }) 200 | ); 201 | // 仅输出:before methodName=onShow 202 | ``` 203 | 204 | 另外: 205 | 206 | - 为 App/Page/Component 添加钩子时,如果`MpMethodHook`中的`before`函数钩子返回`false`时,将不会执行方法体 207 | - 为 Api 添加钩子时: 208 | - 如果`MpMethodHook`中的`before`函数钩子返回`false`时,将不会执行方法体 209 | - 如果`MpMethodHook`中的`before`函数钩子返回不为`true|undefined`时,将不会执行方法体,并将返回结果直接向外传递 210 | 211 | ```javascript 212 | MixinStore.addHook(MpViewType.App, { 213 | onShow: { 214 | before(methodName, methodArgs) { 215 | console.log("hook onShow"); 216 | return false; 217 | }, 218 | }, 219 | }); 220 | App( 221 | MkApp({ 222 | onLaunch() {}, 223 | onShow() { 224 | console.log("self onShow"); 225 | }, 226 | }) 227 | ); 228 | // 仅输出:hook onShow 229 | 230 | const store = {}; 231 | MixinStore.addHook("Api", { 232 | before(methodName, methodArgs, methodHandler, funId) { 233 | console.log(`before methodName=${methodName}`); 234 | if (methodName === "setStorageSync") { 235 | store[methodArgs[0]] = methodArgs[1]; 236 | } 237 | if (methodName === "getStorageSync") { 238 | // 并不会真正执行(wx|my|tt|..).getStorageSync 239 | return store[methodArgs[0]]; 240 | } 241 | }, 242 | after(methodName) { 243 | console.log(`after methodName=${methodName}`); 244 | }, 245 | }); 246 | MkApi.setStorageSync("name", "Tom"); 247 | const name = MkApi.getStorageSync("name"); 248 | console.log(name === store.name); 249 | // 输出:before methodName=setStorageSync 250 | // 输出:after methodName=setStorageSync 251 | // 输出:before methodName=getStorageSync 252 | // 输出:true 253 | ``` 254 | -------------------------------------------------------------------------------- /packages/mixin/api.ts: -------------------------------------------------------------------------------- 1 | import { hookFunc, replaceFunc } from "@mpkit/func-helper"; 2 | import { MkFuncHook, MkReplaceFuncCallback } from "@mpkit/types"; 3 | 4 | export const FormatApiMethodCallbackHook: MkFuncHook = { 5 | before(state) { 6 | hookApiMethodCallback( 7 | state.state.funcName, 8 | (res) => { 9 | state.doneCallback(null, res); 10 | }, 11 | (res) => { 12 | state.doneCallback(res); 13 | }, 14 | state.args 15 | ); 16 | if (state.args[0] && state.args[0].success) { 17 | state.needDoneCallback = true; 18 | } 19 | }, 20 | }; 21 | 22 | export const hookApiMethodCallback = ( 23 | apiName: string, 24 | onSuccess: Function, 25 | onFail: Function, 26 | args: any[] 27 | ) => { 28 | if (!apiName.endsWith("Sync") && (!args.length || args[0] === null)) { 29 | args[0] = {}; 30 | } 31 | if (typeof args[0] === "object" && args[0]) { 32 | const { success, fail } = args[0]; 33 | args[0].success = function HookApiSuccessCallback(...params) { 34 | onSuccess(...params); 35 | return success && success.apply(this, params); 36 | }; 37 | args[0].fail = function HookApiFailCallback(...params) { 38 | onFail(...params); 39 | return fail && fail.apply(this, params); 40 | }; 41 | } 42 | return args; 43 | }; 44 | 45 | export const hookApiMethod = ( 46 | apiName: string, 47 | apiMethod: Function, 48 | replaceCallback: MkReplaceFuncCallback, 49 | hooks: MkFuncHook[] 50 | ) => { 51 | return replaceFunc( 52 | apiMethod, 53 | hookFunc(apiMethod, false, hooks, { 54 | funcName: apiName, 55 | }).func, 56 | replaceCallback, 57 | { 58 | funcName: apiName, 59 | } 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/mixin/debug.js: -------------------------------------------------------------------------------- 1 | const { mergeApi, mergeView, MixinStore, MkApp, promiseifyApi } = require('./dist/index.cjs.js'); 2 | 3 | global.wx = {} 4 | with (global) { 5 | const state = { 6 | } 7 | MixinStore.addHook('App', { 8 | before() { 9 | state.gloabl = true; 10 | } 11 | }); 12 | const app = MkApp({ 13 | onShow() { 14 | state.onShow = true; 15 | } 16 | }); 17 | app.onShow(); 18 | } 19 | 20 | // const appSpec = MkApp({ 21 | // globalData: { 22 | // user: { 23 | // name: 'Tom' 24 | // } 25 | // }, 26 | // onShow() { 27 | // console.log('onShow1') 28 | // } 29 | // }, { 30 | // globalData: { 31 | // user: { 32 | // name: 'Alice', 33 | // age: 20 34 | // } 35 | // }, 36 | // onShow() { 37 | // console.log('onShow2') 38 | // } 39 | // }); 40 | // console.log(appSpec); 41 | 42 | 43 | const state = {} 44 | const mockApi = { 45 | name: 'Tom', 46 | method1Sync(a) { 47 | state.method1 = true; 48 | return a; 49 | }, 50 | method2(a) { 51 | setTimeout(() => { 52 | a && a.fail({ 53 | errMsg: 'test' 54 | }); 55 | }) 56 | }, 57 | method3(a) { 58 | state.method3 = true; 59 | setTimeout(() => { 60 | a && a.success(); 61 | }) 62 | } 63 | } 64 | // promiseifyApi(mockApi, 'method1Sync', 3).then(res => { 65 | // assert.equal(res, 3); 66 | // check('t1'); 67 | // }); 68 | // promiseifyApi(mockApi, 'method3', { 69 | // success() { 70 | // } 71 | // }).then(res => { 72 | // assert.equal(res, undefined); 73 | // assert.equal(state.method3, true); 74 | // check('t2'); 75 | // }); 76 | promiseifyApi(mockApi, 'method2', { 77 | success() { 78 | assert.equal(true, false); 79 | }, 80 | fail() { 81 | } 82 | }).then(res => { 83 | assert.equal(true, false); 84 | }).catch(err => { 85 | assert.equal(err.message, 'test'); 86 | check('t3'); 87 | }) -------------------------------------------------------------------------------- /packages/mixin/hook.ts: -------------------------------------------------------------------------------- 1 | import { MkFuncHook, MkFuncHookState, MpMethodHook } from "@mpkit/types"; 2 | import { uuid } from "@mpkit/util"; 3 | 4 | export const execHook = ( 5 | methodHook: MpMethodHook[], 6 | vm, 7 | step, 8 | ...hookArgs 9 | ): boolean => { 10 | if (!methodHook || !methodHook.length) { 11 | return; 12 | } 13 | const methodName = hookArgs[0]; 14 | let res; 15 | methodHook.forEach((item) => { 16 | if (res !== false) { 17 | const oldRes = res; 18 | res = 19 | item && 20 | item[step] && 21 | (item[step] as Function).apply(vm, hookArgs); 22 | res = typeof res === "undefined" ? oldRes : res; 23 | } 24 | if (res !== false) { 25 | const oldRes = res; 26 | res = 27 | item && 28 | item[methodName] && 29 | item[methodName][step] && 30 | item[methodName][step].apply(vm, hookArgs); 31 | res = typeof res === "undefined" ? oldRes : res; 32 | } 33 | }); 34 | return res; 35 | }; 36 | 37 | export const FuncIDHook: MkFuncHook = { 38 | before(state) { 39 | state.state.id = uuid(); 40 | }, 41 | }; 42 | 43 | const fireFuncHook = ( 44 | methodHook: MpMethodHook[], 45 | step: string, 46 | state: MkFuncHookState 47 | ) => { 48 | const args = [ 49 | methodHook, 50 | state.ctx, 51 | step, 52 | state.state.funcName, 53 | state.args, 54 | ]; 55 | if (step === "before") { 56 | args.push(state.func); 57 | } else if (step === "after") { 58 | args.push(state.result); 59 | } else if (step === "complete") { 60 | if (state.fulfilled) { 61 | args.push(state.value, true); 62 | } else { 63 | args.push( 64 | state.errors && state.errors[state.errors.length - 1] 65 | ? state.errors[state.errors.length - 1].error 66 | : null, 67 | false 68 | ); 69 | } 70 | } else if (step === "catch") { 71 | args.push( 72 | state.errors && state.errors[state.errors.length - 1] 73 | ? state.errors[state.errors.length - 1].error 74 | : null 75 | ); 76 | } 77 | args.push(state.state.id); 78 | const hookResult = execHook.apply(null, args); 79 | if (hookResult === false) { 80 | state.stop = true; 81 | } else if ( 82 | step === "before" && 83 | hookResult !== true && 84 | typeof hookResult !== "undefined" 85 | ) { 86 | state.stop = true; 87 | state.result = hookResult; 88 | } 89 | return hookResult; 90 | }; 91 | 92 | export const createFuncGeneralHook = ( 93 | methodHook: MpMethodHook[] 94 | ): MkFuncHook => { 95 | return { 96 | before(state) { 97 | return fireFuncHook(methodHook, "before", state); 98 | }, 99 | after(state) { 100 | return fireFuncHook(methodHook, "after", state); 101 | }, 102 | complete(state) { 103 | return fireFuncHook(methodHook, "complete", state); 104 | }, 105 | catch(state) { 106 | return fireFuncHook(methodHook, "catch", state); 107 | }, 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /packages/mixin/index.ts: -------------------------------------------------------------------------------- 1 | import { MpViewType, MpView, MpMethodHook } from "@mpkit/types"; 2 | import { 3 | getMpPlatform, 4 | getMpInitLifeName, 5 | getApiVar, 6 | getMpViewType, 7 | isFunc, 8 | isMpIvew, 9 | getMpViewPathName, 10 | } from "@mpkit/util"; 11 | import { mergeApi, mergeView, promiseifyApi } from "./mrege"; 12 | import MixinStore from "./store"; 13 | import { 14 | MpKitPlugin, 15 | MpKitInject, 16 | MpKitConfig, 17 | MpKitRewriteConfig, 18 | MpPlatform, 19 | } from "@mpkit/types"; 20 | import { hookViewMethod } from "./view"; 21 | import { createFuncGeneralHook, FuncIDHook } from "./hook"; 22 | export { mergeApi, mergeView, promiseifyApi }; 23 | export * from "./hook"; 24 | export * from "./api"; 25 | export * from "./view"; 26 | 27 | export const MkApi = (() => { 28 | const paltform = getMpPlatform(); 29 | if (paltform === MpPlatform.wechat) { 30 | return mergeApi(wx, MixinStore.getHook("Api")); 31 | } else if (paltform === MpPlatform.alipay) { 32 | return mergeApi(my, MixinStore.getHook("Api")); 33 | } else if (paltform === MpPlatform.smart) { 34 | return mergeApi(swan, MixinStore.getHook("Api")); 35 | } else if (paltform === MpPlatform.tiktok) { 36 | return mergeApi(tt, MixinStore.getHook("Api")); 37 | } 38 | })(); 39 | const mkView = (type: MpViewType) => { 40 | return (...specList) => { 41 | const setMkSpec = (view) => { 42 | if (view && isMpIvew(view)) { 43 | if (!view.$mkSpec) { 44 | Object.defineProperty(view, "$mkSpec", { 45 | get() { 46 | return fullSpec; 47 | }, 48 | }); 49 | } 50 | if (!fullSpec.$targetPath) { 51 | fullSpec.$targetPath = getMpViewPathName(view); 52 | } 53 | } 54 | }; 55 | let rewriteSetData = 56 | type === MpViewType.Component || type === MpViewType.Page; 57 | const hooks: MpMethodHook[] = []; 58 | const beforeHooks: MpMethodHook[] = [ 59 | { 60 | [getMpInitLifeName(type)]: { 61 | before() { 62 | setMkSpec(this); 63 | if (rewriteSetData && !this.$mkNativeSetData) { 64 | this.$mkNativeSetData = this.setData; 65 | this.setData = hookViewMethod( 66 | "setData", 67 | this.$mkNativeSetData, 68 | null, 69 | [ 70 | FuncIDHook, 71 | createFuncGeneralHook( 72 | MixinStore.getHook(type) 73 | ), 74 | ] 75 | ); 76 | } 77 | }, 78 | }, 79 | }, 80 | ]; 81 | if (type === MpViewType.Component) { 82 | beforeHooks[0].observer = { 83 | before() { 84 | setMkSpec(this); 85 | }, 86 | }; 87 | } 88 | hooks.push(...beforeHooks); 89 | hooks.push(...(MixinStore.getHook(type) || [])); 90 | 91 | specList.forEach((spec) => { 92 | if (spec && typeof spec === "object" && isFunc(spec.$mixinBegin)) { 93 | spec.$mixinBegin(spec, specList); 94 | } 95 | }); 96 | 97 | const fullSpec = mergeView(type, getMpPlatform(), hooks, ...specList); 98 | if (isFunc(fullSpec.$mixinEnd)) { 99 | fullSpec.$mixinEnd(fullSpec); 100 | } 101 | delete fullSpec.$mixinBegin; 102 | delete fullSpec.$mixinEnd; 103 | return fullSpec; 104 | }; 105 | }; 106 | export { MixinStore }; 107 | export const MkApp = mkView(MpViewType.App); 108 | export const MkPage = mkView(MpViewType.Page); 109 | export const MkComponent = mkView(MpViewType.Component); 110 | export const MkNative = { 111 | App: typeof App === "function" ? App : null, 112 | Page: typeof Page === "function" ? Page : null, 113 | Component: typeof Component === "function" ? Component : null, 114 | Api: getApiVar(), 115 | }; 116 | 117 | export const plugin: MpKitPlugin = { 118 | name: "mixin", 119 | apply(mpkit: MpKitInject, config?: MpKitConfig) { 120 | mpkit.MixinStore = MixinStore; 121 | mpkit.App = MkApp; 122 | mpkit.Page = MkPage; 123 | mpkit.Component = MkComponent; 124 | if (config && config.rewrite) { 125 | const rewriteSetData = function rewriteSetData(this: MpView) { 126 | const type = getMpViewType(this); 127 | if ( 128 | !this.$mkDiffSetDataBeforeValue && 129 | (config.rewrite === true || 130 | (config.rewrite[type] && 131 | (config.rewrite as MpKitRewriteConfig).setData)) && 132 | mpkit.hasPlugin("set-data") && 133 | mpkit.setData 134 | ) { 135 | this.$mkDiffSetDataBeforeValue = this.setData; 136 | this.setData = function DiffSetData(this: MpView, ...args) { 137 | return mpkit.setData.apply(mpkit, [this, ...args]); 138 | }; 139 | } 140 | }; 141 | const setDataMixin = (type: MpViewType): MpMethodHook => { 142 | return { 143 | [getMpInitLifeName(type)]: { 144 | before() { 145 | rewriteSetData.call(this); 146 | }, 147 | }, 148 | observer: { 149 | before() { 150 | rewriteSetData.call(this); 151 | }, 152 | }, 153 | }; 154 | }; 155 | MixinStore.addHook(MpViewType.Page, setDataMixin(MpViewType.Page)); 156 | MixinStore.addHook( 157 | MpViewType.Component, 158 | setDataMixin(MpViewType.Component) 159 | ); 160 | const rewriteApi = () => { 161 | const paltform = getMpPlatform(); 162 | if (paltform === MpPlatform.wechat) { 163 | MkNative.Api = wx; 164 | wx = mergeApi(wx, MixinStore.getHook("Api")); 165 | } else if (paltform === MpPlatform.alipay) { 166 | MkNative.Api = my; 167 | my = mergeApi(my, MixinStore.getHook("Api")); 168 | } else if (paltform === MpPlatform.smart) { 169 | MkNative.Api = swan; 170 | swan = mergeApi(swan, MixinStore.getHook("Api")); 171 | } else if (paltform === MpPlatform.tiktok) { 172 | MkNative.Api = tt; 173 | tt = mergeApi(tt, MixinStore.getHook("Api")); 174 | } 175 | }; 176 | if (typeof config.rewrite === "object") { 177 | const tsRewrite = config.rewrite as MpKitRewriteConfig; 178 | if (tsRewrite.Api) { 179 | rewriteApi(); 180 | } 181 | if (tsRewrite.App) { 182 | App = function (spec) { 183 | return MkNative.App(MkApp(spec)); 184 | }; 185 | } 186 | if (tsRewrite.Page) { 187 | Page = function (spec) { 188 | return MkNative.Page(MkPage(spec)); 189 | }; 190 | } 191 | if (tsRewrite.Component) { 192 | Component = function (spec) { 193 | return MkNative.Component(MkComponent(spec)); 194 | }; 195 | } 196 | } else { 197 | MixinStore.addHook( 198 | MpViewType.Page, 199 | setDataMixin(MpViewType.Page) 200 | ); 201 | MixinStore.addHook( 202 | MpViewType.Component, 203 | setDataMixin(MpViewType.Component) 204 | ); 205 | App = function (spec) { 206 | return MkNative.App(MkApp(spec)); 207 | }; 208 | Page = function (spec) { 209 | return MkNative.Page(MkPage(spec)); 210 | }; 211 | Component = function (spec) { 212 | return MkNative.Component(MkComponent(spec)); 213 | }; 214 | rewriteApi(); 215 | } 216 | } 217 | }, 218 | }; 219 | -------------------------------------------------------------------------------- /packages/mixin/mrege.ts: -------------------------------------------------------------------------------- 1 | import { isNativeFunc, isPromise, uuid, isFunc } from "@mpkit/util"; 2 | import { 3 | MpMethodHook, 4 | MpViewType, 5 | MpPlatform, 6 | MkReplaceFuncCallback, 7 | } from "@mpkit/types"; 8 | import { fireViewMethod, formatViewSpecList, hookViewMethod } from "./view"; 9 | import { 10 | FormatApiMethodCallbackHook, 11 | hookApiMethod, 12 | hookApiMethodCallback, 13 | } from "./api"; 14 | import { createFuncGeneralHook, FuncIDHook } from "./hook"; 15 | 16 | export const mergeMethod = ( 17 | methodHook, 18 | methodName, 19 | methodValues, 20 | allowStr = false, 21 | replaceCallback?: MkReplaceFuncCallback 22 | ) => { 23 | function MethodOriginal(...args) { 24 | return fireViewMethod.apply(this, [methodValues, allowStr, ...args]); 25 | } 26 | 27 | return hookViewMethod(methodName, MethodOriginal, replaceCallback, [ 28 | FuncIDHook, 29 | createFuncGeneralHook(methodHook), 30 | ]); 31 | }; 32 | const hookMethod = (methodHook, map, target, allowStr = false) => { 33 | for (let methodName in map) { 34 | const methodValues = map[methodName]; 35 | if (methodValues.length) { 36 | target[methodName] = mergeMethod( 37 | methodHook, 38 | methodName, 39 | methodValues, 40 | allowStr 41 | ); 42 | target[methodName].displayName = methodName; 43 | } 44 | } 45 | }; 46 | const mergeProperties = (methodHook, properties) => { 47 | const result = {}; 48 | const observer = {}; 49 | properties.forEach((item) => { 50 | for (let prop in item) { 51 | if (!result[prop]) { 52 | result[prop] = {}; 53 | } 54 | if (!observer[prop]) { 55 | observer[prop] = []; 56 | } 57 | const val = item[prop]; 58 | if (isNativeFunc(val)) { 59 | result[prop].type = val; 60 | } else if (typeof val === "object") { 61 | if (!val) { 62 | result[prop].type = val; 63 | result[prop].value = val; 64 | } else { 65 | if (val && "type" in val) { 66 | result[prop].type = val.type; 67 | } 68 | if (val && "value" in val) { 69 | result[prop].value = val.value; 70 | } 71 | val && val.observer && observer[prop].push(val.observer); 72 | } 73 | } else { 74 | result[prop].value = val; 75 | } 76 | } 77 | }); 78 | for (let prop in result) { 79 | if (observer[prop] && observer[prop].length) { 80 | result[prop].observer = mergeMethod( 81 | methodHook, 82 | "observer", 83 | observer[prop] 84 | ); 85 | } 86 | } 87 | return result; 88 | }; 89 | 90 | const mergeSpecialProps = ( 91 | prop, 92 | values, 93 | methodHook, 94 | paltform, 95 | target, 96 | removeMap = null 97 | ) => { 98 | if (values.length) { 99 | const subMethodMap = {}; 100 | values.forEach((item) => { 101 | Object.keys(item).forEach((key) => { 102 | if (prop === "lifetimes" && removeMap && removeMap[key]) { 103 | // lifetimes内声明的函数优先级比直接声明的created等要高,会被覆盖 104 | // https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/framework/custom-component/component-constructor 105 | // https://smartprogram.baidu.com/docs/develop/framework/custom-component_comp/ 106 | // https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/component.html 107 | delete removeMap[key]; 108 | } 109 | if (!subMethodMap[key]) { 110 | subMethodMap[key] = []; 111 | } 112 | subMethodMap[key].push(item[key]); 113 | }); 114 | }); 115 | target[prop] = {}; 116 | hookMethod( 117 | methodHook, 118 | subMethodMap, 119 | target[prop], 120 | prop === "lifetimes" && paltform === MpPlatform.wechat 121 | ); 122 | } 123 | }; 124 | 125 | export const mergeView = ( 126 | viewType: MpViewType, 127 | platform: MpPlatform, 128 | methodHook: MpMethodHook[], 129 | ...specList 130 | ): any => { 131 | const result = {}; 132 | const fullSpec = formatViewSpecList(viewType, platform, ...specList); 133 | if (fullSpec.specialProps) { 134 | if (fullSpec.specialProps.properties) { 135 | result["properties"] = mergeProperties( 136 | methodHook, 137 | fullSpec.specialProps.properties 138 | ); 139 | } 140 | if (fullSpec.specialProps.pageLifetimes) { 141 | mergeSpecialProps( 142 | "pageLifetimes", 143 | fullSpec.specialProps.pageLifetimes, 144 | methodHook, 145 | platform, 146 | result 147 | ); 148 | } 149 | if (fullSpec.specialProps.methods) { 150 | mergeSpecialProps( 151 | "methods", 152 | fullSpec.specialProps.methods, 153 | methodHook, 154 | platform, 155 | result 156 | ); 157 | } 158 | if (fullSpec.specialProps.lifetimes) { 159 | mergeSpecialProps( 160 | "lifetimes", 161 | fullSpec.specialProps.lifetimes, 162 | methodHook, 163 | platform, 164 | result, 165 | fullSpec.methodMap 166 | ); 167 | } 168 | delete fullSpec.specialProps; 169 | } 170 | if (fullSpec.methodMap) { 171 | hookMethod(methodHook, fullSpec.methodMap, result); 172 | delete fullSpec.methodMap; 173 | } 174 | Object.assign(result, fullSpec); 175 | return result; 176 | }; 177 | 178 | export const mergeApi = ( 179 | api: any, 180 | methodHook?: MpMethodHook[], 181 | methodReplaceCallback?: MkReplaceFuncCallback 182 | ) => { 183 | const result = {}; 184 | for (let prop in api) { 185 | if (isFunc(api[prop])) { 186 | const methodName = prop; 187 | const methodHandler = api[prop]; 188 | result[prop] = hookApiMethod( 189 | methodName, 190 | methodHandler, 191 | (store) => { 192 | methodReplaceCallback && methodReplaceCallback(store); 193 | }, 194 | [ 195 | FuncIDHook, 196 | FormatApiMethodCallbackHook, 197 | createFuncGeneralHook(methodHook), 198 | ] 199 | ); 200 | } else { 201 | result[prop] = api[prop]; 202 | } 203 | } 204 | result["promiseify"] = function (apiName: string, ...apiArgs: any[]) { 205 | return promiseifyApi.apply(this, [this, apiName, ...apiArgs]); 206 | }; 207 | return result; 208 | }; 209 | 210 | export const promiseifyApi = ( 211 | apiVar: any, 212 | apiName: string, 213 | ...apiArgs: any[] 214 | ): Promise => { 215 | return new Promise((resolve, reject) => { 216 | if (isFunc(apiVar[apiName])) { 217 | hookApiMethodCallback( 218 | apiName, 219 | (...args) => { 220 | if (args.length < 2) { 221 | resolve(args[0]); 222 | } else { 223 | resolve(args); 224 | } 225 | }, 226 | (...args) => { 227 | const err = new Error("未知错误"); 228 | if (args.length < 2 && args[0] && args[0].errMsg) { 229 | err.message = args[0].errMsg; 230 | } 231 | err["failResult"] = args; 232 | reject(err); 233 | }, 234 | apiArgs 235 | ); 236 | if (apiName.indexOf("Sync") === -1) { 237 | let apiOptions = apiArgs[0]; 238 | const res = apiVar[apiName].call(apiVar, apiOptions); 239 | if (res && apiOptions && isFunc(apiOptions.result)) { 240 | apiOptions.result(res); 241 | } 242 | } else { 243 | try { 244 | const res = apiVar[apiName].call(apiVar, apiArgs); 245 | resolve(res); 246 | } catch (error) { 247 | reject(error); 248 | } 249 | } 250 | } else { 251 | resolve(apiVar[apiName]); 252 | } 253 | }); 254 | }; 255 | -------------------------------------------------------------------------------- /packages/mixin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mpkit/mixin", 3 | "version": "1.1.2", 4 | "description": "为小程序提供混入功能。", 5 | "author": "imingyu ", 6 | "homepage": "https://github.com/imingyu/mpkit/tree/master/packages/mixin", 7 | "license": "MIT", 8 | "main": "dist/index.cjs.js", 9 | "module": "dist/index.esm.js", 10 | "types": "spec/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "spec" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/imingyu/mpkit.git" 18 | }, 19 | "scripts": {}, 20 | "bugs": { 21 | "url": "https://github.com/imingyu/mpkit/issues" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": [ 27 | "mpkit", 28 | "小程序", 29 | "miniprogram", 30 | "mixin" 31 | ], 32 | "dependencies": { 33 | "@mpkit/func-helper": "^1.1.2", 34 | "@mpkit/types": "^1.1.2", 35 | "@mpkit/util": "^1.1.2" 36 | }, 37 | "devDependencies": { 38 | "chai": "^4.2.0", 39 | "mocha": "^8.1.1" 40 | }, 41 | "gitHead": "0a4dc4e319b5134237165e9b09e812420b0d52a0" 42 | } 43 | -------------------------------------------------------------------------------- /packages/mixin/store.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MpMethodHook, 3 | MkMixinStore, 4 | MixinStoreHookProp, 5 | MixinStoreHooks, 6 | } from "@mpkit/types"; 7 | import { MpViewType } from "@mpkit/types"; 8 | import { getMpInitLifeName, initView, getMpMountLifeName } from "@mpkit/util"; 9 | export default (() => { 10 | let hooks: MixinStoreHooks = {} as MixinStoreHooks; 11 | const store: MkMixinStore = { 12 | addHook(type: MixinStoreHookProp, hook: MpMethodHook) { 13 | if (!hooks[type]) { 14 | store.getHook(type); 15 | } 16 | hooks[type].push(hook); 17 | }, 18 | getHook(type: MixinStoreHookProp): MpMethodHook[] { 19 | if (type !== "Api" && !hooks[type]) { 20 | hooks[type] = [ 21 | { 22 | [getMpInitLifeName(type)]: { 23 | before(methodName, methodArgs) { 24 | initView(this, type); 25 | }, 26 | }, 27 | [getMpMountLifeName(type)]: { 28 | before(methodName, methodArgs) { 29 | initView(this, type); 30 | }, 31 | }, 32 | }, 33 | ]; 34 | if (type === MpViewType.Component) { 35 | hooks[type].push({ 36 | observer: { 37 | before() { 38 | initView(this, type); 39 | }, 40 | }, 41 | }); 42 | } 43 | } 44 | if (!hooks[type]) { 45 | hooks[type] = []; 46 | } 47 | return hooks[type]; 48 | }, 49 | }; 50 | return store; 51 | })(); 52 | -------------------------------------------------------------------------------- /packages/mixin/test/index.js: -------------------------------------------------------------------------------- 1 | const { mergeApi, mergeView, MixinStore, MkApp, promiseifyApi } = require('../dist/index.cjs.js'); 2 | const { MpViewType, MpPlatform } = require('../../types/dist/index.cjs'); 3 | const { assert } = require('chai'); 4 | describe('Mixin', () => { 5 | const appSpec = MkApp({ 6 | globalData: { 7 | user: { 8 | name: 'Tom' 9 | } 10 | }, 11 | onShow() { 12 | } 13 | }, { 14 | globalData: { 15 | user: { 16 | name: 'Alice', 17 | age: 20 18 | } 19 | }, 20 | onShow() { 21 | } 22 | }); 23 | describe('mergeView', () => { 24 | it('App&Page', () => { 25 | const state = { 26 | }; 27 | const appSpec = mergeView(MpViewType.App, MpPlatform.wechat, [{ 28 | before() { 29 | state.count++; 30 | state.before = true; 31 | }, 32 | after() { 33 | assert.equal(state.before, true) 34 | state.after = true; 35 | }, 36 | catch(methodName, 37 | methodArgs, 38 | error, 39 | errType, 40 | funId) { 41 | state.catch = true; 42 | assert.equal(error.message, 'test'); 43 | assert.equal(methodName, 'onHide'); 44 | assert.equal(methodArgs[0], 3); 45 | } 46 | }], { 47 | data: { 48 | name: '1' 49 | }, 50 | onShow() { 51 | state.onShow1 = true; 52 | } 53 | }, { 54 | data: { 55 | name: '2' 56 | }, 57 | onShow() { 58 | state.onShow2 = true; 59 | }, 60 | onHide() { 61 | throw new Error('test') 62 | } 63 | }, { 64 | methods: { 65 | onShow() { 66 | state.onShow3 = true; 67 | } 68 | } 69 | }); 70 | appSpec.onShow(); 71 | assert.equal(state.onShow1, true); 72 | assert.equal(state.onShow3, undefined); 73 | assert.equal(state.onShow2, true); 74 | try { 75 | appSpec.onHide(3); 76 | } catch (error) { 77 | assert.equal(error.message, 'test'); 78 | } 79 | assert.equal(state.before, true); 80 | assert.equal(state.after, true); 81 | assert.equal(state.catch, true); 82 | assert.equal(appSpec.data.name, '2'); 83 | }); 84 | it('Component', () => { 85 | const state = { 86 | }; 87 | const spec = mergeView(MpViewType.Component, MpPlatform.wechat, [{ 88 | before() { 89 | state.count++; 90 | state.before = true; 91 | }, 92 | }], { 93 | methods: { 94 | show() { 95 | state.show1 = true; 96 | } 97 | } 98 | }, { 99 | methods: { 100 | show() { 101 | state.show2 = true; 102 | }, 103 | show3() { 104 | state.show3 = true; 105 | } 106 | } 107 | }); 108 | spec.methods.show(); 109 | assert.equal(state.before, true); 110 | assert.equal(state.show1, true); 111 | assert.equal(state.show2, true); 112 | assert.equal(!!state.show3, false); 113 | }) 114 | }) 115 | it('mergeApi', (done) => { 116 | const state = {} 117 | const mockApi = { 118 | name: 'Tom', 119 | method1(a) { 120 | state.method1 = true; 121 | state.a = a; 122 | }, 123 | method3(a) { 124 | state.method3 = true; 125 | setTimeout(() => { 126 | a && a.success(); 127 | }) 128 | }, 129 | method2Sync() { 130 | return 2; 131 | }, 132 | method4() { 133 | state.method4 = true; 134 | }, 135 | method5() { 136 | state.method5 = true; 137 | } 138 | } 139 | const api = mergeApi(mockApi, [ 140 | { 141 | before(name, args) { 142 | if (name === 'method1') { 143 | assert.equal(args[0], 2); 144 | } 145 | if (name === 'method4') { 146 | return false; 147 | } 148 | if (name === 'method5') { 149 | return 6; 150 | } 151 | }, 152 | complete(name, args, res, isSuccess) { 153 | state.success = true; 154 | assert.equal(isSuccess, true); 155 | } 156 | } 157 | ]); 158 | api.method1(2); 159 | assert.equal(state.method1, true); 160 | api.method3({ 161 | success() { 162 | assert.equal(true, true); 163 | setTimeout(() => { 164 | assert.equal(state.success, true); 165 | done(); 166 | }) 167 | }, 168 | fail() { 169 | assert.equal(false, true); 170 | } 171 | }); 172 | assert.equal(api.method2Sync(), 2); 173 | api.method4(); 174 | assert.equal(state.method4, undefined); 175 | const res = api.method5(); 176 | assert.equal(state.method5, undefined); 177 | assert.equal(res, 6); 178 | }); 179 | it('promiseifyApi', function (done) { 180 | this.timeout(5 * 1000); 181 | const check = (type) => { 182 | state[type] = true; 183 | if (state.t1 && state.t2 && state.t3) { 184 | done(); 185 | } 186 | } 187 | const state = {} 188 | const mockApi = { 189 | name: 'Tom', 190 | method1Sync(a) { 191 | state.method1 = true; 192 | return a; 193 | }, 194 | method2(a) { 195 | setTimeout(() => { 196 | a && a.fail({ 197 | errMsg: 'test' 198 | }); 199 | }) 200 | }, 201 | method3(a) { 202 | state.method3 = true; 203 | setTimeout(() => { 204 | a && a.success(); 205 | }) 206 | } 207 | } 208 | promiseifyApi(mockApi, 'method1Sync', 3).then(res => { 209 | assert.equal(res, 3); 210 | check('t1'); 211 | }); 212 | promiseifyApi(mockApi, 'method3', { 213 | success() { 214 | } 215 | }).then(res => { 216 | assert.equal(res, undefined); 217 | assert.equal(state.method3, true); 218 | check('t2'); 219 | }); 220 | promiseifyApi(mockApi, 'method2', { 221 | success() { 222 | assert.equal(true, false); 223 | }, 224 | fail() { 225 | } 226 | }).then(res => { 227 | assert.equal(true, false); 228 | }).catch(err => { 229 | assert.equal(err.message, 'test'); 230 | check('t3'); 231 | }) 232 | }); 233 | it('MixinStore', () => { 234 | global.wx = {} 235 | with (global) { 236 | const state = { 237 | } 238 | MixinStore.addHook(MpViewType.App, { 239 | before() { 240 | state.gloabl = true; 241 | } 242 | }); 243 | const app = MkApp({ 244 | onShow() { 245 | state.onShow = true; 246 | } 247 | }); 248 | app.onShow(); 249 | assert.equal(state.onShow, true); 250 | assert.equal(state.gloabl, true); 251 | } 252 | }); 253 | }); -------------------------------------------------------------------------------- /packages/mixin/view.ts: -------------------------------------------------------------------------------- 1 | import { hookFunc, replaceFunc } from "@mpkit/func-helper"; 2 | import { 3 | MkFuncHook, 4 | MkReplaceFuncCallback, 5 | MkViewFormatSpec, 6 | MpPlatform, 7 | MpViewType, 8 | } from "@mpkit/types"; 9 | import { 10 | isEmptyObject, 11 | isFunc, 12 | isNativeFunc, 13 | isPlainObject, 14 | merge, 15 | } from "@mpkit/util"; 16 | 17 | export const formatViewSpecList = ( 18 | viewType: MpViewType, 19 | platform: MpPlatform, 20 | ...specList 21 | ): MkViewFormatSpec => { 22 | const result: MkViewFormatSpec = {}; 23 | const specialProps: any = {}; 24 | const methodMap: any = {}; 25 | if (viewType === MpViewType.Component) { 26 | if ( 27 | platform === MpPlatform.wechat || 28 | platform === MpPlatform.smart || 29 | platform === MpPlatform.tiktok 30 | ) { 31 | Object.assign(specialProps, { 32 | properties: [], 33 | methods: [], 34 | lifetimes: [], 35 | pageLifetimes: [], 36 | }); 37 | } else if (platform === MpPlatform.alipay) { 38 | Object.assign(specialProps, { 39 | methods: [], 40 | }); 41 | } 42 | } 43 | specList.forEach((spec) => { 44 | Object.keys(spec).forEach((prop) => { 45 | const value = spec[prop]; 46 | const valType = typeof value; 47 | if (prop in specialProps && viewType === MpViewType.Component) { 48 | specialProps[prop].push(value); 49 | } else if (valType === "object" && value && isPlainObject(value)) { 50 | if (typeof result[prop] !== "object") { 51 | result[prop] = Array.isArray(value) ? [] : {}; 52 | } 53 | merge(result[prop], value); 54 | } else if (valType === "function") { 55 | if (isNativeFunc(value)) { 56 | result[prop] = value; 57 | } else { 58 | if (!methodMap[prop]) { 59 | methodMap[prop] = []; 60 | } 61 | methodMap[prop].push(value); 62 | } 63 | } else { 64 | result[prop] = value; 65 | } 66 | }); 67 | }); 68 | if (!specialProps.methods || !specialProps.methods.length) { 69 | delete specialProps.methods; 70 | } 71 | if (!specialProps.properties || !specialProps.properties.length) { 72 | delete specialProps.properties; 73 | } 74 | if (!specialProps.lifetimes || !specialProps.lifetimes.length) { 75 | delete specialProps.lifetimes; 76 | } 77 | if (!specialProps.pageLifetimes || !specialProps.pageLifetimes.length) { 78 | delete specialProps.pageLifetimes; 79 | } 80 | if (!isEmptyObject(specialProps)) { 81 | result.specialProps = specialProps; 82 | } 83 | if (!isEmptyObject(methodMap)) { 84 | result.methodMap = methodMap; 85 | } 86 | return result; 87 | }; 88 | 89 | export const hookViewMethod = ( 90 | name: string, 91 | method: Function, 92 | replaceCallback: MkReplaceFuncCallback, 93 | hooks: MkFuncHook[] 94 | ) => { 95 | return replaceFunc( 96 | method, 97 | hookFunc(method, false, hooks, { 98 | funcName: name, 99 | }).func, 100 | replaceCallback, 101 | { 102 | funcName: name, 103 | } 104 | ); 105 | }; 106 | 107 | export const fireViewMethod = function ( 108 | methodHandlers: Array, 109 | allowStr: boolean, 110 | ...args: any[] 111 | ) { 112 | const ctx = this; 113 | let methodResult; 114 | methodHandlers.forEach((item) => { 115 | if (typeof item === "function") { 116 | const res = item.apply(ctx, args); 117 | if (typeof res !== "undefined") { 118 | methodResult = res; 119 | } 120 | } else if ( 121 | typeof item === "string" && 122 | allowStr && 123 | ctx && 124 | ctx[item] && 125 | isFunc(ctx[item]) 126 | ) { 127 | const res = ctx[item].apply(this, args); 128 | if (typeof res !== "undefined") { 129 | methodResult = res; 130 | } 131 | } 132 | }); 133 | return methodResult; 134 | }; 135 | -------------------------------------------------------------------------------- /packages/mpspec/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts -------------------------------------------------------------------------------- /packages/mpspec/index.ts: -------------------------------------------------------------------------------- 1 | import { MkMpPlatformSpec, MkMpXmlSpec } from "@mpkit/types"; 2 | import * as _XmlSpec from "./xml"; 3 | 4 | export const XmlSpec: MkMpPlatformSpec = _XmlSpec; 5 | -------------------------------------------------------------------------------- /packages/mpspec/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mpkit/mpspec", 3 | "version": "1.0.12", 4 | "description": "各平台小程序技术特性说明书", 5 | "author": "imingyu ", 6 | "homepage": "https://github.com/imingyu/mpkit/tree/master/packages/mpspec", 7 | "license": "MIT", 8 | "main": "dist/index.cjs.js", 9 | "module": "dist/index.esm.js", 10 | "types": "spec/index.d.ts", 11 | "files": [ 12 | "dist", 13 | "spec" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/imingyu/mpkit.git" 18 | }, 19 | "scripts": {}, 20 | "bugs": { 21 | "url": "https://github.com/imingyu/mpkit/issues" 22 | }, 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": [ 27 | "mpkit", 28 | "小程序", 29 | "miniprogram", 30 | "mpspec", 31 | "wxml-to-json", 32 | "xml-to-json" 33 | ], 34 | "dependencies": { 35 | "@mpkit/types": "^1.0.12" 36 | }, 37 | "devDependencies": { 38 | "chai": "^4.2.0", 39 | "mocha": "^8.1.1", 40 | "typescript": "^3.9.7" 41 | }, 42 | "gitHead": "0a4dc4e319b5134237165e9b09e812420b0d52a0" 43 | } 44 | -------------------------------------------------------------------------------- /packages/mpspec/validater.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MkMpXmlAstAttr, 3 | MkMpXmlAstElement, 4 | MkMpXmlAstPreviousSiblingFinder, 5 | MkMpXmlNodeValidater, 6 | MkMpXmlNodeValidateResult, 7 | MkMpXmlNodeValidateResultLevel, 8 | } from "@mpkit/types"; 9 | 10 | const hasAttr = (node: MkMpXmlAstElement, ...attrNames: string[]): boolean => { 11 | return node.attrs.some((attr) => attrNames.indexOf(attr.name) !== -1); 12 | }; 13 | 14 | const code = (type: string, code: number) => `MK:${type || ""}:${code}`; 15 | 16 | export const attrElifValidater = (ifAttrName: string): MkMpXmlNodeValidater => { 17 | return ( 18 | attr: MkMpXmlAstAttr, 19 | parent: MkMpXmlAstElement, 20 | grandpa?: MkMpXmlAstElement, 21 | previousSiblingFinder?: MkMpXmlAstPreviousSiblingFinder 22 | ): MkMpXmlNodeValidateResult => { 23 | let warn = !attr.content; 24 | let message = warn ? "content is empty" : ""; 25 | let pass = true; 26 | let cd = warn ? 1 : 0; 27 | let level = warn 28 | ? MkMpXmlNodeValidateResultLevel.warn 29 | : MkMpXmlNodeValidateResultLevel.success; 30 | if (grandpa) { 31 | const parentPrevElement = 32 | grandpa.previousSibling || 33 | previousSiblingFinder(parent, grandpa.children); 34 | if (!parentPrevElement) { 35 | pass = false; 36 | level = MkMpXmlNodeValidateResultLevel.fail; 37 | message = "not find previousSibling"; 38 | cd = 2; 39 | } else { 40 | level = hasAttr(parentPrevElement, ifAttrName) 41 | ? level 42 | : MkMpXmlNodeValidateResultLevel.fail; 43 | if (level === MkMpXmlNodeValidateResultLevel.fail) { 44 | pass = false; 45 | message = `not find "${ifAttrName}" attr`; 46 | cd = 3; 47 | } 48 | } 49 | } 50 | return { 51 | pass, 52 | level, 53 | code: code("ATTR_ELIF", cd), 54 | message, 55 | } as MkMpXmlNodeValidateResult; 56 | }; 57 | }; 58 | 59 | export const attrElseValidater = ( 60 | ifAttrName: string, 61 | elifAttrName: string 62 | ): MkMpXmlNodeValidater => { 63 | return ( 64 | attr: MkMpXmlAstAttr, 65 | parent: MkMpXmlAstElement, 66 | grandpa?: MkMpXmlAstElement, 67 | previousSiblingFinder?: MkMpXmlAstPreviousSiblingFinder 68 | ): MkMpXmlNodeValidateResult => { 69 | let warn = "content" in attr; 70 | let message = warn ? '"else" attr has content' : ""; 71 | let pass = true; 72 | let cd = warn ? 1 : 0; 73 | let level = warn 74 | ? MkMpXmlNodeValidateResultLevel.warn 75 | : MkMpXmlNodeValidateResultLevel.success; 76 | if (grandpa) { 77 | const parentPrevElement = 78 | grandpa.previousSibling || 79 | previousSiblingFinder(parent, grandpa.children); 80 | if (!parentPrevElement) { 81 | pass = false; 82 | level = MkMpXmlNodeValidateResultLevel.fail; 83 | message = "not find previousSibling"; 84 | cd = 2; 85 | } else { 86 | level = hasAttr(parentPrevElement, ifAttrName, elifAttrName) 87 | ? level 88 | : MkMpXmlNodeValidateResultLevel.fail; 89 | if (level === MkMpXmlNodeValidateResultLevel.fail) { 90 | pass = false; 91 | message = `not find "${elifAttrName}" or "${ifAttrName}" attr`; 92 | cd = 3; 93 | } 94 | } 95 | } 96 | return { 97 | pass, 98 | level, 99 | code: code("ATTR_ELSE", cd), 100 | message, 101 | } as MkMpXmlNodeValidateResult; 102 | }; 103 | }; 104 | 105 | export const attrForValidater = (forAttrName: string): MkMpXmlNodeValidater => { 106 | return ( 107 | attr: MkMpXmlAstAttr, 108 | parent: MkMpXmlAstElement 109 | ): MkMpXmlNodeValidateResult => { 110 | let warn = !attr.content; 111 | let message = warn ? "content is empty" : ""; 112 | let pass = true; 113 | let cd = warn ? 1 : 0; 114 | let level = warn 115 | ? MkMpXmlNodeValidateResultLevel.warn 116 | : MkMpXmlNodeValidateResultLevel.success; 117 | level = hasAttr(parent, forAttrName) 118 | ? level 119 | : MkMpXmlNodeValidateResultLevel.fail; 120 | if (level === MkMpXmlNodeValidateResultLevel.fail) { 121 | pass = false; 122 | message = `not find "${forAttrName}" attr`; 123 | cd = 2; 124 | } 125 | return { 126 | pass, 127 | level, 128 | code: code("ATTR_FOR", cd), 129 | message, 130 | } as MkMpXmlNodeValidateResult; 131 | }; 132 | }; 133 | -------------------------------------------------------------------------------- /packages/mpspec/xml.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MkMpXmlAttrContentType, 3 | MkMpXmlAttrSpec, 4 | MkMpXmlDataBinding, 5 | MkMpXmlSpec, 6 | } from "@mpkit/types"; 7 | import { 8 | attrElifValidater, 9 | attrElseValidater, 10 | attrForValidater, 11 | } from "./validater"; 12 | 13 | export const commonDataBinding: MkMpXmlDataBinding = { 14 | leftBoundaryChar: "{{", 15 | rightBoundaryChar: "}}", 16 | leftBoundarySpace: -1, 17 | rightBoundarySpace: -1, 18 | }; 19 | export const commonAttrSpec: MkMpXmlAttrSpec = { 20 | dataBinding: commonDataBinding, 21 | dataBindingCount: -1, 22 | require: 0, 23 | isEvent: false, 24 | requireContent: 0, 25 | contentType: MkMpXmlAttrContentType.string, 26 | supportVersion: "0.0.0", 27 | }; 28 | const wechatAttrForValidater = attrForValidater("wx:for"); 29 | export const wechat: MkMpXmlSpec = { 30 | _common: { 31 | closeType: 0, 32 | dataBinding: commonDataBinding, 33 | dataBindingCount: -1, 34 | supportVersion: "0.0.0", 35 | attrsSpec: { 36 | _unclaimed: commonAttrSpec, 37 | "wx:if": { 38 | ...commonAttrSpec, 39 | requireContent: 1, 40 | }, 41 | "wx:elif": { 42 | ...commonAttrSpec, 43 | requireContent: 1, 44 | validater: attrElifValidater("wx:if"), 45 | }, 46 | "wx:else": { 47 | ...commonAttrSpec, 48 | validater: attrElseValidater("wx:if", "wx:elif"), 49 | }, 50 | "wx:for-item": { 51 | ...commonAttrSpec, 52 | validater: wechatAttrForValidater, 53 | }, 54 | "wx:for-index": { 55 | ...commonAttrSpec, 56 | validater: wechatAttrForValidater, 57 | }, 58 | }, 59 | }, 60 | }; 61 | let alipay: MkMpXmlSpec; 62 | let smart: MkMpXmlSpec; 63 | let tiktok: MkMpXmlSpec; 64 | 65 | export { alipay, smart, tiktok }; 66 | -------------------------------------------------------------------------------- /packages/mpxml-parser/README.md: -------------------------------------------------------------------------------- 1 | # @mpkit/view-parser 2 | 3 | [![Build Status](https://travis-ci.org/imingyu/mpkit.svg?branch=master)](https://travis-ci.org/imingyu/mpkit) 4 | ![image](https://img.shields.io/npm/l/@mpkit/view-parser.svg) 5 | [![image](https://img.shields.io/npm/v/@mpkit/view-parser.svg)](https://www.npmjs.com/package/@mpkit/view-parser) 6 | [![image](https://img.shields.io/npm/dt/@mpkit/view-parser.svg)](https://www.npmjs.com/package/@mpkit/view-parser) 7 | 8 | 将小程序模板编译为 ast。 9 | 10 | TODO:功能待开发 11 | -------------------------------------------------------------------------------- /packages/mpxml-parser/adapter/attr-base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MpPlatform, 3 | MpViewSyntaxSpec, 4 | MkXmlNode, 5 | IMkMpXmlAttrParseAdapter, 6 | LikeFxParseContext, 7 | } from "@mpkit/types"; 8 | import { mpViewSyntaxSpec } from "../spec"; 9 | import { FxNodeJSON } from "forgiving-xml-parser"; 10 | import { parseMpXmlContent } from "../content"; 11 | 12 | export class MkBaseAttrParseAdapter implements IMkMpXmlAttrParseAdapter { 13 | mpPlatform: MpPlatform; 14 | mpViewSyntax: MpViewSyntaxSpec; 15 | constructor(mpPlatform: MpPlatform) { 16 | this.mpPlatform = mpPlatform; 17 | const spec = mpViewSyntaxSpec[this.mpPlatform]; 18 | this.mpViewSyntax = spec; 19 | } 20 | parse( 21 | attr: FxNodeJSON, 22 | parent?: FxNodeJSON | LikeFxParseContext, 23 | grandpa?: FxNodeJSON | LikeFxParseContext 24 | ): MkXmlNode { 25 | if ("content" in attr && attr.content) { 26 | (attr as MkXmlNode).mpContents = parseMpXmlContent( 27 | attr.content, 28 | attr 29 | ); 30 | } 31 | return attr; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/mpxml-parser/adapter/attr-for.ts: -------------------------------------------------------------------------------- 1 | import { MpPlatform, MkXmlNode, IMkMpXmlAttrParseAdapter } from "@mpkit/types"; 2 | import { ATTR_FOR_INDEX_NOT_FOR, ATTR_FOR_ITEM_NOT_FOR } from "../message"; 3 | import { hasAttr } from "../util"; 4 | import throwError from "../throw"; 5 | import { MkBaseAttrParseAdapter } from "./attr-base"; 6 | import { FxCursorPosition, FxNode, FxNodeJSON } from "forgiving-xml-parser"; 7 | import { CursorInitValue } from "../var"; 8 | // 处理循环语句 9 | export default class MkForAttrParseAdapter 10 | extends MkBaseAttrParseAdapter 11 | implements IMkMpXmlAttrParseAdapter { 12 | forValue: string; 13 | forItemValue: string; 14 | forIndexValue: string; 15 | forKeyValue: string; 16 | constructor(mpPlatform: MpPlatform) { 17 | super(mpPlatform); 18 | this.mpPlatform = mpPlatform; 19 | const spec = this.mpViewSyntax; 20 | this.forValue = spec.namespace + spec.for; 21 | this.forItemValue = spec.namespace + spec.forItem; 22 | this.forIndexValue = spec.namespace + spec.forIndex; 23 | if (spec.key) { 24 | this.forKeyValue = spec.namespace + spec.key; 25 | } 26 | } 27 | // mergeForContent(content: MpForAttrContent, key: string, attr?: FxNode) { 28 | // if (attr && attr.content && attr.content.trim()) { 29 | // content[key] = attr.content.trim(); 30 | // } 31 | // } 32 | // getAttrValue(attr?: FxNode, defaultValue: string = ""): string { 33 | // return attr && attr.content && attr.content.trim() 34 | // ? attr.content.trim() 35 | // : defaultValue; 36 | // } 37 | // mergeEachVar( 38 | // eachVar: string, 39 | // data: MkParseAttrAdapterArg, 40 | // forAttrContent: MpForAttrContent, 41 | // forItemAttr: FxNode, 42 | // forIndexAttr: FxNode, 43 | // hasIn: boolean 44 | // ) { 45 | // let itemVal; 46 | // let indexVal; 47 | // eachVar = eachVar.trim(); 48 | // if (eachVar.indexOf(",") !== -1) { 49 | // const arr = this.splitString(eachVar, ",").filter((item) => item); 50 | // if (!arr.length && hasIn) { 51 | // return throwError({ 52 | // message: ATTR_FOR_EACH_VAR_WRONG, 53 | // position: MkValidateMessagePosition.attr, 54 | // target: data.currentAttr, 55 | // }); 56 | // } 57 | // itemVal = arr[0] || this.getAttrValue(forItemAttr); 58 | // indexVal = arr[1] || this.getAttrValue(forIndexAttr); 59 | // } else if (eachVar) { 60 | // itemVal = hasIn 61 | // ? eachVar || this.getAttrValue(forItemAttr) 62 | // : this.getAttrValue(forItemAttr); 63 | // indexVal = this.getAttrValue(forIndexAttr); 64 | // } else if (hasIn) { 65 | // return throwError({ 66 | // message: ATTR_FOR_EACH_VAR_WRONG, 67 | // position: MkValidateMessagePosition.attr, 68 | // target: data.currentAttr, 69 | // }); 70 | // } 71 | // if (hasIn && (!itemVal || !indexVal)) { 72 | // return throwError({ 73 | // message: ATTR_FOR_EACH_VAR_WRONG, 74 | // position: MkValidateMessagePosition.attr, 75 | // target: data.currentAttr, 76 | // }); 77 | // } 78 | // if (itemVal) { 79 | // forAttrContent.featureItem = itemVal; 80 | // } 81 | // if (indexVal) { 82 | // forAttrContent.featureIndex = indexVal; 83 | // } 84 | // } 85 | // splitString(str: string, char: string): string[] { 86 | // return str.split(char).map((item) => item.trim()); 87 | // } 88 | // mergeGeneralForAttrContent( 89 | // nativeContent: string, 90 | // data: MkParseAttrAdapterArg, 91 | // forAttrContent: MpForAttrContent, 92 | // forItemAttr: FxNode, 93 | // forIndexAttr: FxNode 94 | // ) { 95 | // const forKeyAttr = data.allAttrs.find( 96 | // (item) => item.name === this.forKeyValue 97 | // ); 98 | // forAttrContent.featureList = nativeContent; 99 | // this.mergeForContent(forAttrContent, "featureItem", forItemAttr); 100 | // this.mergeForContent(forAttrContent, "featureKey", forKeyAttr); 101 | // this.mergeForContent(forAttrContent, "featureIndex", forIndexAttr); 102 | // } 103 | // mergeSmartForAttrContent( 104 | // nativeContent: string, 105 | // data: MkParseAttrAdapterArg, 106 | // forAttrContent: MpForAttrContent, 107 | // forItemAttr: FxNode, 108 | // forIndexAttr: FxNode 109 | // ) { 110 | // // 百度小程序语法:s-for="item,index in list trackBy index" 111 | // const [valMain, valKey] = this.splitString(nativeContent, "trackBy"); 112 | // if (valKey) { 113 | // forAttrContent.featureKey = valKey; 114 | // } 115 | // if (valMain.indexOf(" in ") !== -1) { 116 | // const [eachVar, listVar] = this.splitString(valMain, " in "); 117 | // if (!listVar) { 118 | // return throwError({ 119 | // message: ATTR_FOR_NOT_LISTVAR, 120 | // position: MkValidateMessagePosition.attr, 121 | // target: data.currentAttr, 122 | // }); 123 | // } 124 | // forAttrContent.featureList = listVar; 125 | // this.mergeEachVar( 126 | // eachVar, 127 | // data, 128 | // forAttrContent, 129 | // forItemAttr, 130 | // forIndexAttr, 131 | // true 132 | // ); 133 | // } else { 134 | // this.mergeEachVar( 135 | // valMain, 136 | // data, 137 | // forAttrContent, 138 | // forItemAttr, 139 | // forIndexAttr, 140 | // false 141 | // ); 142 | // forAttrContent.featureList = valMain; 143 | // } 144 | // } 145 | // parseForAttrContent( 146 | // data: MkParseAttrAdapterArg, 147 | // forAttrContent: MkXmlContent 148 | // ): MpForAttrContent { 149 | // const result = forAttrContent as MpForAttrContent; 150 | // if (typeof forAttrContent.value !== "string") { 151 | // return throwError({ 152 | // message: ATTR_FOR_NOT_LISTVAR, 153 | // position: MkValidateMessagePosition.attr, 154 | // target: data.currentAttr, 155 | // }); 156 | // } 157 | // const attrVal = forAttrContent.value.trim(); 158 | // if (!attrVal) { 159 | // return throwError({ 160 | // message: ATTR_FOR_NOT_LISTVAR, 161 | // position: MkValidateMessagePosition.attr, 162 | // target: data.currentAttr, 163 | // }); 164 | // } 165 | // const forItemAttr = data.allAttrs.find( 166 | // (item) => item.name === this.forItemValue 167 | // ); 168 | // const forIndexAttr = data.allAttrs.find( 169 | // (item) => item.name === this.forIndexValue 170 | // ); 171 | 172 | // if ( 173 | // this.mpPlatform === MpPlatform.wechat || 174 | // this.mpPlatform === MpPlatform.alipay || 175 | // this.mpPlatform === MpPlatform.tiktok 176 | // ) { 177 | // this.mergeGeneralForAttrContent( 178 | // attrVal, 179 | // data, 180 | // result, 181 | // forItemAttr, 182 | // forIndexAttr 183 | // ); 184 | // } else if (this.mpPlatform === MpPlatform.smart) { 185 | // this.mergeSmartForAttrContent( 186 | // attrVal, 187 | // data, 188 | // result, 189 | // forItemAttr, 190 | // forIndexAttr 191 | // ); 192 | // } 193 | // return result; 194 | // } 195 | // validateForVarName(forContent: MpForAttrContent, attr: MkXmlNode) { 196 | // ["featureItem", "featureIndex", "featureList"].forEach( 197 | // (item, index, arr) => { 198 | // const val = forContent[item]; 199 | // if (val) { 200 | // const equalItem = arr 201 | // .filter((t) => t !== item) 202 | // .find((t) => forContent[t] === val); 203 | // if (equalItem) { 204 | // return throwError({ 205 | // message: ATTR_FOR_VAR_EQUAL, 206 | // position: MkValidateMessagePosition.attr, 207 | // target: attr, 208 | // }); 209 | // } 210 | // } 211 | // } 212 | // ); 213 | // } 214 | validate(attr: FxNodeJSON, parent?: FxNodeJSON) { 215 | const attrName = attr.name; 216 | const attrStartCursor: FxCursorPosition = attr.locationInfo 217 | ? { 218 | offset: attr.locationInfo.startOffset, 219 | column: attr.locationInfo.startColumn, 220 | lineNumber: attr.locationInfo.startLineNumber, 221 | } 222 | : CursorInitValue; 223 | if (parent) { 224 | if ( 225 | attrName === this.forItemValue && 226 | !hasAttr(parent, this.forValue) 227 | ) { 228 | // for-item一定要与for属性一同出现 229 | throwError({ 230 | ...ATTR_FOR_ITEM_NOT_FOR, 231 | target: attr, 232 | ...attrStartCursor, 233 | }); 234 | } 235 | if ( 236 | attrName === this.forIndexValue && 237 | !hasAttr(parent, this.forValue) 238 | ) { 239 | // for-index一定要与for属性一同出现 240 | return throwError({ 241 | ...ATTR_FOR_INDEX_NOT_FOR, 242 | target: attr, 243 | ...attrStartCursor, 244 | }); 245 | } 246 | } 247 | } 248 | parse( 249 | attr: FxNodeJSON, 250 | parent?: FxNodeJSON, 251 | grandpa?: FxNodeJSON 252 | ): MkXmlNode { 253 | this.validate(attr, parent); 254 | return super.parse.apply(this, arguments); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /packages/mpxml-parser/adapter/attr-where.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MpPlatform, 3 | MpViewSyntaxSpec, 4 | MkXmlNode, 5 | IMkMpXmlAttrParseAdapter, 6 | LikeFxParseContext, 7 | } from "@mpkit/types"; 8 | import { getPreviousSibling, hasAttr } from "../util"; 9 | import throwError from "../throw"; 10 | import { ATTR_ELSE_HAS_CONTENT, ATTR_WHERE_NOT_IF } from "../message"; 11 | import { MkBaseAttrParseAdapter } from "./attr-base"; 12 | import { 13 | FxCursorPosition, 14 | FxNodeJSON, 15 | FxParseContext, 16 | } from "forgiving-xml-parser"; 17 | import { CursorInitValue } from "../var"; 18 | 19 | // 处理条件语句 20 | export default class MpParseWehreAttrAdapter 21 | extends MkBaseAttrParseAdapter 22 | implements IMkMpXmlAttrParseAdapter { 23 | mpPlatform: MpPlatform; 24 | mpViewSyntax: MpViewSyntaxSpec; 25 | ifValue: string; 26 | elseifValue: string; 27 | elseValue: string; 28 | allowMoreContentVar: boolean = false; 29 | constructor(mpPlatform: MpPlatform) { 30 | super(mpPlatform); 31 | const spec = this.mpViewSyntax; 32 | this.ifValue = spec.namespace + spec.if; 33 | this.elseifValue = spec.namespace + spec.elseIf; 34 | this.elseValue = spec.namespace + spec.else; 35 | } 36 | parse( 37 | attr: FxNodeJSON, 38 | parent?: FxNodeJSON | LikeFxParseContext, 39 | grandpa?: FxNodeJSON | LikeFxParseContext 40 | ): MkXmlNode { 41 | const attrName = attr.name; 42 | if (parent && grandpa) { 43 | const parentPrevSibling = getPreviousSibling( 44 | parent as FxNodeJSON, 45 | grandpa 46 | ); 47 | if (attrName === this.elseifValue || attrName === this.elseValue) { 48 | const hasPrevWhere = 49 | parentPrevSibling && 50 | (attrName === this.elseifValue 51 | ? hasAttr(parentPrevSibling, this.ifValue) 52 | : hasAttr(parentPrevSibling, this.ifValue) || 53 | hasAttr(parentPrevSibling, this.elseifValue)); 54 | if (!parentPrevSibling || !hasPrevWhere) { 55 | const cursor: FxCursorPosition = attr.locationInfo 56 | ? { 57 | offset: attr.locationInfo.startOffset, 58 | column: attr.locationInfo.startColumn, 59 | lineNumber: attr.locationInfo.startLineNumber, 60 | } 61 | : CursorInitValue; 62 | throwError({ 63 | ...ATTR_WHERE_NOT_IF, 64 | ...cursor, 65 | target: attr, 66 | }); 67 | } 68 | } 69 | if (attrName === this.elseValue && "content" in attr) { 70 | const cursor: FxCursorPosition = attr.locationInfo 71 | ? { 72 | offset: attr.locationInfo.startOffset, 73 | column: attr.locationInfo.startColumn, 74 | lineNumber: attr.locationInfo.startLineNumber, 75 | } 76 | : CursorInitValue; 77 | throwError({ 78 | ...ATTR_ELSE_HAS_CONTENT, 79 | ...cursor, 80 | target: attr, 81 | }); 82 | } 83 | } 84 | return super.parse.apply(this, arguments); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/mpxml-parser/adapter/content.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IMkMpXmlContentParseAdapter, 3 | MkXmlContent, 4 | MkXmlNode, 5 | } from "@mpkit/types"; 6 | import { FxNodeJSON } from "forgiving-xml-parser"; 7 | import { parseMpXmlContent } from "../content"; 8 | 9 | export const contentAdapter: IMkMpXmlContentParseAdapter = { 10 | parse( 11 | content: string, 12 | node?: FxNodeJSON, 13 | parent?: FxNodeJSON, 14 | grandpa?: FxNodeJSON 15 | ): MkXmlContent[] { 16 | if (node && (node as MkXmlNode).mpContents) { 17 | return (node as MkXmlNode).mpContents; 18 | } 19 | return parseMpXmlContent(content, node); 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /packages/mpxml-parser/adapter/index.ts: -------------------------------------------------------------------------------- 1 | // 处理节点 2 | import { contentAdapter } from "./content"; 3 | import { IMkMpXmlParseAdapter, MpPlatform } from "@mpkit/types"; 4 | import MpParseWhereAttrAdapter from "./attr-where"; 5 | import MpParseForAttrAdapter from "./attr-for"; 6 | import { MkBaseAttrParseAdapter } from "./attr-base"; 7 | 8 | const initMpXmlParseAdapter = ( 9 | mpPlatform: MpPlatform 10 | ): IMkMpXmlParseAdapter => { 11 | const whereAttrAdapter = new MpParseWhereAttrAdapter(mpPlatform); 12 | const forAttrAdapter = new MpParseForAttrAdapter(mpPlatform); 13 | const res = { 14 | attrAdapters: { 15 | [whereAttrAdapter.ifValue]: whereAttrAdapter, 16 | [whereAttrAdapter.elseifValue]: whereAttrAdapter, 17 | [whereAttrAdapter.elseValue]: whereAttrAdapter, 18 | [forAttrAdapter.forValue]: forAttrAdapter, 19 | [forAttrAdapter.forItemValue]: forAttrAdapter, 20 | [forAttrAdapter.forIndexValue]: forAttrAdapter, 21 | __unclaimed: new MkBaseAttrParseAdapter(mpPlatform), 22 | }, 23 | contentAdapter, 24 | }; 25 | if (forAttrAdapter.forKeyValue) { 26 | res.attrAdapters[forAttrAdapter.forKeyValue] = forAttrAdapter; 27 | } 28 | return res; 29 | }; 30 | 31 | export const MpPlatformAdapters = { 32 | [MpPlatform.wechat]: initMpXmlParseAdapter(MpPlatform.wechat), 33 | [MpPlatform.alipay]: initMpXmlParseAdapter(MpPlatform.alipay), 34 | [MpPlatform.smart]: initMpXmlParseAdapter(MpPlatform.smart), 35 | [MpPlatform.tiktok]: initMpXmlParseAdapter(MpPlatform.tiktok), 36 | } as { 37 | [prop in 38 | | MpPlatform.wechat 39 | | MpPlatform.alipay 40 | | MpPlatform.smart 41 | | MpPlatform.tiktok]: IMkMpXmlParseAdapter; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/mpxml-parser/content.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MkXmlContent, 3 | MkXmlNode, 4 | MkXmlNodeJSON, 5 | MpXmlContentType, 6 | } from "@mpkit/types"; 7 | import { 8 | FxNode, 9 | FxNodeJSON, 10 | moveCursor, 11 | FxCursorPosition, 12 | repeatString, 13 | } from "forgiving-xml-parser"; 14 | import { cursorToLocation, plusCursor } from "./util"; 15 | import throwError from "./throw"; 16 | import { BRACKET_NOT_CLOSE, BRACKET_THAN_TWO } from "./message"; 17 | 18 | export const parseMpXmlContent = ( 19 | content: string, 20 | node?: FxNode | FxNodeJSON | MkXmlNode | MkXmlNodeJSON, 21 | leftBracketChar: string = "{", 22 | rightBracketChar: string = "}", 23 | bracketCount: number = 2 24 | ): MkXmlContent[] => { 25 | const cursor: FxCursorPosition = { 26 | lineNumber: 1, 27 | offset: 0, 28 | column: 1, 29 | }; 30 | if (!content) { 31 | return [ 32 | { 33 | type: MpXmlContentType.static, 34 | value: content, 35 | locationInfo: cursorToLocation(cursor), 36 | }, 37 | ]; 38 | } 39 | let contentStartCursor: FxCursorPosition = { 40 | offset: 0, 41 | column: 1, 42 | lineNumber: 1, 43 | }; 44 | 45 | if (node && node.locationInfo.content) { 46 | contentStartCursor = { 47 | offset: node.locationInfo.content.startOffset, 48 | column: node.locationInfo.content.startColumn, 49 | lineNumber: node.locationInfo.content.startLineNumber, 50 | }; 51 | } 52 | const result: MkXmlContent[] = []; 53 | let text = ""; 54 | let bkCount = 0; // 括号字符数量 55 | let bkPosition = 0; // 1=左括号,2=右括号 56 | let staticSartCursor: FxCursorPosition; 57 | let dyStartCursor: FxCursorPosition; 58 | const getCursor = () => { 59 | return plusCursor(cursor, contentStartCursor); 60 | }; 61 | const reset = () => { 62 | dyStartCursor = staticSartCursor = undefined; 63 | bkCount = 0; 64 | bkPosition = 0; 65 | text = ""; 66 | }; 67 | const pushStatic = (endCursor?: FxCursorPosition) => { 68 | result.push({ 69 | type: MpXmlContentType.static, 70 | value: text, 71 | locationInfo: cursorToLocation( 72 | plusCursor(staticSartCursor, contentStartCursor), 73 | plusCursor(endCursor, contentStartCursor) 74 | ), 75 | }); 76 | text = ""; 77 | staticSartCursor = undefined; 78 | }; 79 | for ( 80 | let len = content.length; 81 | cursor.offset < len; 82 | moveCursor(cursor, 0, 1, 1) 83 | ) { 84 | const char = content[cursor.offset]; 85 | if (!staticSartCursor) { 86 | staticSartCursor = { 87 | ...cursor, 88 | }; 89 | } 90 | if (char === leftBracketChar) { 91 | if (!bkCount) { 92 | // 之前未遇到括号表达式 93 | dyStartCursor = { 94 | ...cursor, 95 | }; 96 | bkCount++; 97 | bkPosition = 1; 98 | continue; 99 | } 100 | if (bkPosition === 1) { 101 | if (bkCount + 1 <= bracketCount) { 102 | bkCount++; 103 | continue; 104 | } 105 | return throwError({ 106 | ...BRACKET_THAN_TWO, 107 | target: node, 108 | ...getCursor(), 109 | }); 110 | } 111 | return throwError({ 112 | ...BRACKET_NOT_CLOSE, 113 | target: node, 114 | ...getCursor(), 115 | }); 116 | } 117 | if (char === rightBracketChar) { 118 | if (!bkPosition) { 119 | text += char; 120 | continue; 121 | } 122 | if (bkPosition === 1) { 123 | text += char; 124 | bkPosition = 0; 125 | dyStartCursor = undefined; 126 | bkCount = 0; 127 | continue; 128 | } 129 | if (bkCount + 1 < bracketCount) { 130 | bkCount++; 131 | continue; 132 | } 133 | result.push({ 134 | type: MpXmlContentType.dynamic, 135 | value: text, 136 | locationInfo: cursorToLocation( 137 | plusCursor(dyStartCursor, contentStartCursor), 138 | plusCursor(cursor, contentStartCursor) 139 | ), 140 | }); 141 | reset(); 142 | continue; 143 | } 144 | if (bracketCount === bkCount && bkPosition === 1) { 145 | if (text) { 146 | pushStatic(dyStartCursor); 147 | } 148 | text += char; 149 | bkPosition = 2; 150 | bkCount = 0; 151 | continue; 152 | } 153 | if (bkCount && bkCount < bracketCount) { 154 | if (bkPosition === 1) { 155 | text += `${repeatString(leftBracketChar, bkCount)}${char}`; 156 | bkPosition = 0; 157 | bkCount = 0; 158 | dyStartCursor = undefined; 159 | continue; 160 | } 161 | return throwError({ 162 | ...BRACKET_NOT_CLOSE, 163 | target: node, 164 | ...getCursor(), 165 | }); 166 | } 167 | text += char; 168 | } 169 | if (text) { 170 | if (bkPosition) { 171 | return throwError({ 172 | ...BRACKET_NOT_CLOSE, 173 | target: node, 174 | ...getCursor(), 175 | }); 176 | } else { 177 | pushStatic(cursor); 178 | reset(); 179 | } 180 | } 181 | return result; 182 | }; 183 | -------------------------------------------------------------------------------- /packages/mpxml-parser/debug.js: -------------------------------------------------------------------------------- 1 | const { MpPlatform } = require('@mpkit/types'); 2 | const { parseMpXml } = require('./dist/index.cjs.js'); 3 | // const json = parseMpXml(`123 4 | // 5 | // 6 | // {{a}} 7 | // 8 | // 9 | // 10 | // 哎呀,直播找不到啦~ 11 | // 更多直播 12 | // 13 | // 14 | // 15 | // `, MpPlatform.wechat); 16 | // console.log(JSON.stringify(json)); 17 | // const xml = `123456`; 18 | // const res = parseMpXml(xml, MpPlatform.wechat); 19 | const res1 = parseMpXml(` 20 | 21 | 22 | 23 | 24 | {{navTitle}} 25 | 26 | {{liveRoomTitle}} 27 | 28 | 29 | 30 | 34124观看 31 | ● 直播中 32 | 33 | 34 | 35 | 微信登录观看直播 36 | 37 |