├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── package.json ├── src ├── componentmanager.js ├── index.js ├── render │ ├── component.js │ ├── diff.js │ └── render.js ├── template │ ├── compile.js │ ├── expr.js │ ├── parse.js │ ├── transform.js │ └── virtualnode.js └── tool │ ├── constant.js │ ├── intersectionobserver.js │ ├── selectorquery.js │ └── utils.js └── test ├── __snapshots__ └── component.test.js.snap ├── component.test.js ├── diff.test.js ├── expr.test.js ├── intersectionobserver.test.js ├── parse.test.js ├── render.test.js ├── run.js ├── selectorquery.test.js ├── this.test.js ├── transform.test.js ├── utils.js ├── utils.test.js └── wxml ├── comp.json ├── comp.wxml ├── comp.wxss ├── foot.wxml ├── head.wxml ├── index.js ├── index.json ├── index.wxml ├── index.wxss └── tmpl.wxml /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'airbnb-base', 4 | 'plugin:promise/recommended' 5 | ], 6 | 'parserOptions': { 7 | 'ecmaVersion': 9, 8 | 'ecmaFeatures': { 9 | 'jsx': false 10 | }, 11 | 'sourceType': 'module' 12 | }, 13 | 'env': { 14 | 'es6': true, 15 | 'node': true, 16 | 'jest': true 17 | }, 18 | 'plugins': [ 19 | 'import', 20 | 'node', 21 | 'promise' 22 | ], 23 | 'rules': { 24 | 'arrow-parens': 'off', 25 | 'comma-dangle': [ 26 | 'error', 27 | 'only-multiline' 28 | ], 29 | 'complexity': ['error', 10], 30 | 'func-names': 'off', 31 | 'global-require': 'off', 32 | 'handle-callback-err': [ 33 | 'error', 34 | '^(err|error)$' 35 | ], 36 | 'import/no-unresolved': [ 37 | 'error', 38 | { 39 | 'caseSensitive': true, 40 | 'commonjs': true, 41 | 'ignore': ['^[^.]'] 42 | } 43 | ], 44 | 'import/prefer-default-export': 'off', 45 | 'linebreak-style': 'off', 46 | 'no-catch-shadow': 'error', 47 | 'no-continue': 'off', 48 | 'no-div-regex': 'warn', 49 | 'no-else-return': 'off', 50 | 'no-param-reassign': 'off', 51 | 'no-plusplus': 'off', 52 | 'no-shadow': 'off', 53 | 'no-multi-assign': 'off', 54 | 'no-underscore-dangle': 'off', 55 | 'node/no-deprecated-api': 'error', 56 | 'node/process-exit-as-throw': 'error', 57 | 'object-curly-spacing': [ 58 | 'error', 59 | 'never' 60 | ], 61 | 'operator-linebreak': [ 62 | 'error', 63 | 'after', 64 | { 65 | 'overrides': { 66 | ':': 'before', 67 | '?': 'before' 68 | } 69 | } 70 | ], 71 | 'prefer-arrow-callback': 'off', 72 | 'prefer-destructuring': 'off', 73 | 'prefer-template': 'off', 74 | 'quote-props': [ 75 | 1, 76 | 'as-needed', 77 | { 78 | 'unnecessary': true 79 | } 80 | ], 81 | 'semi': [ 82 | 'error', 83 | 'never' 84 | ], 85 | // 补充 86 | 'no-return-assign': 'off', 87 | 'complexity': 'off', 88 | 'no-use-before-define': 'off', 89 | 'max-len': 'off', 90 | 'no-restricted-syntax': 'off', 91 | 'no-console': 'off', 92 | 'class-methods-use-this': 'off', 93 | 'no-nested-ternary': 'off', 94 | 'no-mixed-operators': 'off', 95 | 'consistent-return': 'off', 96 | 'no-restricted-globals': 'off', 97 | 'promise/always-return': 'off', 98 | 'camelcase': 'off', 99 | 'no-control-regex': 'off', 100 | 'no-await-in-loop': 'off', 101 | 'promise/no-callback-in-promise': 'off', 102 | }, 103 | 'globals': { 104 | 'window': true, 105 | 'document': true, 106 | 'TouchEvent': true, 107 | 'Touch': true, 108 | 'CustomEvent': true, 109 | 'Event': true, 110 | 'Component': true, 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | node_modules 11 | coverage 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | script: npm run test 5 | before_script: 6 | - npm install 7 | after_script: 8 | - npm run codecov -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## 1.4.9 4 | 5 | * 修复开启 virtual host 后 snapshot 中自定义组件根节点仍然存在的问题 6 | * 支持 selectOwnerComponent 7 | 8 | ## 1.4.6 9 | 10 | * 修复 selectQuery 找不到节点时会引发报错的问题 11 | 12 | ## 1.4.5 13 | 14 | * 内置模板编译器兼容 wx:for 传入字段不存在的场景 15 | 16 | ## 1.4.4 17 | 18 | * 支持内置 behavior:wx://form-field-group 19 | ## 1.4.3 20 | 21 | * 支持根组件的 virtual host 模式 22 | ## 1.4.2 23 | 24 | * 支持 addEventListener/removeEventListener 接口 25 | 26 | ## 1.4.1 27 | 28 | * triggerLifeTime 接口支持参数 29 | * 支持 triggerPageLifeTime 接口 30 | 31 | ## 1.4.0 32 | 33 | * 更新基础库 exparser 到 2.15.0 34 | * 修复 observers 在组件 init 之前就会被调一次的问题 35 | 36 | ## 1.3.3 37 | 38 | * 修复 behavior 创建没有调用 callDefinitionFilter 的问题 39 | 40 | ## 1.3.2 41 | 42 | * 修复 properties 的 type 值转换问题 43 | 44 | ## 1.3.1 45 | 46 | * 随机 id 生成使用 Math.random 47 | 48 | ## 1.3.0 49 | 50 | * 更新基础库 exparser 到 2.11.2 51 | * 支持 virtual host 特性 52 | 53 | ## 1.2.3 54 | 55 | * toJSON 方法将会在 root 组件上返回 `
` 56 | * 修复 wxml 属性简写导致 diff 后属性重复的问题 57 | * 修复部分场景 diff 失败问题 58 | 59 | ## 1.2.2 60 | 61 | * 支持 relations 62 | * 支持了 mutated 事件监听 63 | * 修复无法传递驼峰参数 64 | * 异步事件处理改为微任务 65 | 66 | ## 1.2.1 67 | 68 | * 支持 Component.prototype.toJSON 69 | * 添加 typescript 类型声明 70 | 71 | ## 1.2.0 72 | 73 | * 支持内置 behavior wx://component-export 74 | * 更新 exparser 版本为 2.10.4 75 | 76 | ## 1.1.11 77 | 78 | * 支持内置 behavior wx://form-field-button 79 | 80 | ## 1.1.10 81 | 82 | * 修复节点属性值为 falsely 值被强制转为空字符串的问题 83 | 84 | ## 1.1.9 85 | 86 | * dispatchEvent 接口支持触发自定义事件 87 | 88 | ## 1.1.8 89 | 90 | * 修复 diff 时没有处理父节点为根节点的情况 91 | 92 | ## 1.1.7 93 | 94 | * 废弃注册组件时对初始 data 进行深拷贝的逻辑。 95 | * 修复组件 dispatchEvent 方法中自定义事件传入 detail,组件函数接收不到 detail 的问题(#7)。 96 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wechat-miniprogram 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 | # j-component 2 | 3 | [![](https://img.shields.io/npm/v/j-component.svg?style=flat)](https://www.npmjs.org/package/wechat-miniprogram) 4 | [![](https://img.shields.io/travis/wechat-miniprogram/j-component.svg)](https://github.com/wechat-miniprogram/j-component) 5 | [![](https://img.shields.io/github/license/wechat-miniprogram/j-component.svg)](https://github.com/wechat-miniprogram/j-component) 6 | [![](https://img.shields.io/codecov/c/github/wechat-miniprogram/j-component.svg)](https://github.com/wechat-miniprogram/j-component) 7 | 8 | ## 简介 9 | 10 | 仿小程序组件系统,可以让小程序自定义组件跑在 web 端。 11 | 12 | ## 注意 13 | 14 | * 此框架的性能比不上小程序的实现。 15 | * 此框架的功能会比小程序略微弱一点。 16 | * 此框架不是小程序的子集,是完全独立的实现,请勿将此等价于小程序内的实现。 17 | 18 | ## 安装 19 | 20 | ``` 21 | npm install --save j-component 22 | ``` 23 | 24 | ## 使用 25 | 26 | ```js 27 | const jComponent = require('j-component') 28 | ``` 29 | 30 | ### behavior(definition) 31 | 32 | 注册 behavior。 33 | 34 | ```js 35 | let behavior = jComponent.behavior({ 36 | /* 小程序 behavior 支持的定义段 */ 37 | }) 38 | ``` 39 | 40 | ### register(definition) 41 | 42 | 注册自定义组件,返回自定组件 id。 43 | 44 | #### definition 45 | 46 | | 属性名 | 类型 | 描述 | 47 | |---|---|---| 48 | | id | String | 可选字段,如果传了此字段,则表明注册为全局组件,其他组件可直接在 template 中使用而无需在 usingComponents 里引入 | 49 | | tagName | String | 可选字段,指定组件对应的 dom 节点的 tagName,默认取 usingComponents 里的定义或组件自身的 id | 50 | | path | String | 可选字段,该组件在文件系统中的绝对路径,用于需要涉及到组件路径的方法中,如 getRelationNodes 方法 | 51 | | template | String | 组件模板,即组件对应的 wxml 内容 | 52 | | usingComponents | Object | 使用到的自定义组件映射表 | 53 | | behaviors | Array | behavior 的用法和小程序类似 | 54 | | options | Object | 配置对象,支持小程序自定义组件 options 定义段支持的所有字段 | 55 | | options.classPrefix | String | 组件样式的私有化前缀,默认是空串,即没有前缀 | 56 | 57 | ``` js 58 | jComponent.register({ 59 | id: 'view', 60 | tagName: 'wx-view', 61 | template: '
' 62 | }) 63 | 64 | let childId = jComponent.register({ 65 | tagName: 'xxx', 66 | template: '', // 直接使用全局组件 67 | }) 68 | 69 | let id = jComponent.register({ 70 | template: '123', 71 | usingComponents: { 72 | 'child': childId, // 声明要使用的组件,传入组件 id 73 | }, 74 | behaviors: [behavior], 75 | options: { 76 | classPrefix: 'xxx', 77 | 78 | /* 其他小程序自定义组件支持的 option,比如 addGlobalClass 等 */ 79 | }, 80 | 81 | /* 其他小程序自定义组件支持的定义段,比如 methods 定义段等 */ 82 | }) 83 | ``` 84 | 85 | ### create(componentId, properties) 86 | 87 | 创建自定义组件实例,返回 [RootComponent](#class-rootcomponent)。 88 | 89 | #### componentId 90 | 91 | 调用 [register](#registerdefinition) 接口返回的 id。 92 | 93 | #### properties 94 | 95 | 创建组件实例时,由组件接收的初始 properties 对象。 96 | 97 | ```js 98 | let rootComp = jComponent.create(id) 99 | ``` 100 | 101 | ### Class: Component 102 | 103 | 组件。 104 | 105 | #### 属性 106 | 107 | | 属性名 | 类型 | 描述 | 108 | |---|---|---| 109 | | dom | Object | 组件实例对应的 dom 节点 | 110 | | data | Object | 组件实例对应的 data 对象 | 111 | | instance | Object | 组件实例中的 this,通过此字段可以访问组件实例的 methods 等定义段 | 112 | 113 | #### 方法 114 | 115 | ##### querySelector(selector) 116 | 117 | 获取符合给定匹配串的第一个节点,返回 [Component](#class-component) 实例。 118 | 119 | > PS:支持 selector 同小程序自定义组件的 selectComponent 接口 120 | 121 | ```js 122 | let childComp = comp.querySelector('#a') 123 | ``` 124 | 125 | ##### querySelectorAll(selector) 126 | 127 | 获取符合给定匹配串的所有节点,返回 [Component](#class-component) 实例列表 128 | 129 | > PS:支持 selector 同小程序自定义组件的 selectAllComponents 接口 130 | 131 | ```js 132 | let childComps = comp.querySelectorAll('.a') 133 | ``` 134 | 135 | ##### setData(data, callback) 136 | 137 | 调用组件实例的 setData 方法. 138 | 139 | ```js 140 | comp.setData({ text: 'a' }, () => {}) 141 | ``` 142 | 143 | ##### dispatchEvent(eventName, options) 144 | 145 | 用于模拟触发该组件实例节点上的事件。 146 | 147 | ```js 148 | // 触发组件树中的节点事件 149 | comp.dispatchEvent('touchstart', { 150 | touches: [{ x: 0, y: 0 }], 151 | changedTouches: [{ x: 0, y: 0 }], 152 | }) 153 | 154 | // 触发组件树中的节点自定义事件 155 | comp.dispatchEvent('customevent', { 156 | touches: [{ x: 0, y: 0 }], 157 | changedTouches: [{ x: 0, y: 0 }], 158 | /* 其他 CustomEvent 构造器支持的 option */ 159 | }) 160 | ``` 161 | 162 | ##### addEventListener(eventName, handler, useCapture) 163 | 164 | 用于外部监听组件触发的事件。 165 | 166 | ```js 167 | comp.addEventListener('customevent', evt => { 168 | console.log(evt) 169 | }) 170 | ``` 171 | 172 | ##### removeEventListener(eventName, handler, useCapture) 173 | 174 | 用于外部取消监听组件触发的事件。 175 | 176 | ```js 177 | const handler = evt => { 178 | console.log(evt) 179 | comp.removeEventListener('customevent', handler) 180 | } 181 | comp.addEventListener('customevent', handler) 182 | ``` 183 | 184 | ##### triggerLifeTime(lifeTime, args) 185 | 186 | 触发组件实例的生命周期钩子。 187 | 188 | ```js 189 | comp.triggerLifeTime('moved', {test: 'xxx'}) 190 | ``` 191 | 192 | ##### triggerPageLifeTime(lifeTime, args) 193 | 194 | 触发组件实例中配置的页面的生命周期钩子。 195 | 196 | ```js 197 | comp.triggerPageLifeTime('show', {test: 'xxx'}) 198 | ``` 199 | 200 | ##### toJSON() 201 | 202 | 将节点组件下的节点树生成一个 JSON 树 203 | 204 | ```js 205 | comp.toJSON() 206 | ``` 207 | 208 | ### Class: RootComponent 209 | 210 | 根组件,继承自 [Component](#class-component)。亦即是说,所有 Component 支持的属性/接口,RootComponent 都支持。 211 | 212 | #### 方法 213 | 214 | ##### attach 215 | 216 | 将根组件实例挂载在传入的 dom 节点上。 217 | 218 | ```js 219 | const parent = document.createElement('div') 220 | rootComp.attach(parent) 221 | ``` 222 | 223 | ##### detach 224 | 225 | 将根组件实例从父亲 dom 节点上移除。 226 | 227 | ```js 228 | rootComp.detach() 229 | ``` 230 | 231 | ## TODO 232 | 233 | * 内置 wxml 解析器的 template 支持 234 | * 内置 wxml 解析器的 include 支持 235 | * 内置 wxml 解析器的 import 支持 236 | * 内置 wxml 解析器的 外部 wxs 237 | * generics 支持 238 | * moved 生命周期 239 | * ...... 240 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import "miniprogram-api-typings"; 2 | 3 | export const behavior: WechatMiniprogram.Behavior.Constructor; 4 | 5 | export interface ComponentId< 6 | TData extends WechatMiniprogram.Component.DataOption, 7 | TProperty extends WechatMiniprogram.Component.PropertyOption, 8 | TMethod extends WechatMiniprogram.Component.MethodOption 9 | > extends String {} 10 | 11 | export function register< 12 | TData extends WechatMiniprogram.Component.DataOption, 13 | TProperty extends WechatMiniprogram.Component.PropertyOption, 14 | TMethod extends WechatMiniprogram.Component.MethodOption 15 | >( 16 | options: WechatMiniprogram.Component.Options & { 17 | id?: string; 18 | tagName?: string; 19 | template?: string; 20 | usingComponents?: Object; 21 | } 22 | ): ComponentId; 23 | 24 | export function create< 25 | TData extends WechatMiniprogram.Component.DataOption, 26 | TProperty extends WechatMiniprogram.Component.PropertyOption, 27 | TMethod extends WechatMiniprogram.Component.MethodOption 28 | >( 29 | componentId: ComponentId, 30 | properties?: Partial< 31 | WechatMiniprogram.Component.PropertyOptionToData 32 | > 33 | ): RootComponent; 34 | export function create( 35 | componentId: string, 36 | properties?: any 37 | ): Component; 38 | 39 | export interface ComponentJSON { 40 | tagName: string; 41 | attrs: { name: string; value: any }[]; 42 | event: { 43 | [event: string]: { 44 | handler: string; 45 | isMutated: boolean; 46 | isCapture: boolean; 47 | isCatch: boolean; 48 | name: string; 49 | }; 50 | }; 51 | children: ComponentJSON[]; 52 | } 53 | 54 | export class Component< 55 | TData extends WechatMiniprogram.Component.DataOption, 56 | TProperty extends WechatMiniprogram.Component.PropertyOption, 57 | TMethod extends Partial 58 | > { 59 | readonly dom: HTMLElement | undefined; 60 | readonly data: Readonly; 61 | readonly instance: WechatMiniprogram.Component.Instance< 62 | TData, 63 | TProperty, 64 | TMethod 65 | >; 66 | 67 | dispatchEvent(eventName: string, options?: any): void; 68 | 69 | querySelector(selector: string): Component | undefined; 70 | 71 | querySelectorAll(selector: string): Component[]; 72 | 73 | setData( 74 | data: Partial & { [x: string]: any }, 75 | callback?: () => void 76 | ): void; 77 | 78 | triggerLifeTime( 79 | lifetime: 80 | | "created" 81 | | "ready" 82 | | "attached" 83 | | "moved" 84 | | "detached" 85 | | "saved" 86 | | "restored" 87 | | "error" 88 | | "listenerChanged" 89 | ): void; 90 | 91 | toJSON(): ComponentJSON; 92 | } 93 | 94 | export class RootComponent< 95 | TData extends WechatMiniprogram.Component.DataOption, 96 | TProperty extends WechatMiniprogram.Component.PropertyOption, 97 | TMethod extends Partial 98 | > extends Component { 99 | attach(parent: Node): void; 100 | 101 | detach(): void; 102 | } 103 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/index'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "j-component", 3 | "version": "1.4.9", 4 | "description": "miniprogram custom component framework", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "files": [ 8 | "src", 9 | "index.d.ts" 10 | ], 11 | "scripts": { 12 | "test": "jest --bail", 13 | "test-debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --bail", 14 | "coverage": "jest --coverage --bail", 15 | "codecov": "jest --coverage && codecov", 16 | "lint": "eslint \"src/**/*.js\" --fix && eslint \"test/**/*.js\" --fix" 17 | }, 18 | "jest": { 19 | "testEnvironment": "jsdom", 20 | "testURL": "https://jest.test", 21 | "testMatch": [ 22 | "**/test/**/*.test.js" 23 | ], 24 | "collectCoverageFrom": [ 25 | "src/**/*.js" 26 | ] 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/wechat-miniprogram/j-component.git" 31 | }, 32 | "author": "wechat-miniprogram", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@babel/core": "^7.9.6", 36 | "@babel/preset-env": "^7.9.6", 37 | "codecov": "^3.7.0", 38 | "eslint": "^5.3.0", 39 | "eslint-config-airbnb-base": "13.1.0", 40 | "eslint-plugin-import": "^2.14.0", 41 | "eslint-plugin-node": "^7.0.1", 42 | "eslint-plugin-promise": "^3.8.0", 43 | "jest": "^25.5.4", 44 | "jsdom": "^14.0.0", 45 | "miniprogram-compiler": "latest" 46 | }, 47 | "dependencies": { 48 | "expr-parser": "^1.0.0", 49 | "miniprogram-api-typings": "^3.2.2", 50 | "miniprogram-exparser": "latest" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/componentmanager.js: -------------------------------------------------------------------------------- 1 | const exparser = require('miniprogram-exparser') 2 | const compile = require('./template/compile') 3 | const transform = require('./template/transform') 4 | const diff = require('./render/diff') 5 | const render = require('./render/render') 6 | const _ = require('./tool/utils') 7 | const SelectorQuery = require('./tool/selectorquery') 8 | const IntersectionObserver = require('./tool/intersectionobserver') 9 | 10 | const PATH_TO_ID_MAP = [] 11 | 12 | class ComponentManager { 13 | constructor(definition) { 14 | this.id = definition.id || _.getId(true) 15 | this.path = _.normalizeAbsolute(definition.path) 16 | this.definition = definition 17 | 18 | if (definition.tagName) _.setTagName(this.id, definition.tagName) // 保存标签名 19 | if (this.path) PATH_TO_ID_MAP[this.path] = this.id // 保存 path 到 id 的映射 20 | 21 | const template = definition.template 22 | 23 | this.data = {} 24 | this.generateFunc = typeof template === 'function' ? transform(template, definition.usingComponents || {}) : compile(template, this.data, definition.usingComponents || {}) // 解析编译模板 25 | this.exparserDef = this.registerToExparser() 26 | 27 | _.cache(this.id, this) 28 | } 29 | 30 | /** 31 | * 注册 exparser 组件 32 | */ 33 | registerToExparser() { 34 | const definition = this.definition 35 | const options = definition.options || {} 36 | const usingComponents = definition.usingComponents || {} 37 | const using = Object.keys(usingComponents).map(key => usingComponents[key]) 38 | let methods = {} 39 | 40 | definition.behaviors = definition.behaviors || [] 41 | definition.behaviors = definition.behaviors.map((item) => { 42 | // 支持内置 behavior 43 | if (item === 'wx://component-export') { 44 | return global.wxComponentExport 45 | } else if (item === 'wx://form-field') { 46 | return global.wxFormField 47 | } else if (item === 'wx://form-field-group') { 48 | return global.wxFormFieldGroup 49 | } else if (item === 'wx://form-field-button') { 50 | return global.wxFormFieldButton 51 | } 52 | 53 | return item 54 | }) 55 | 56 | _.adjustExparserDefinition(definition) 57 | 58 | const path = this.path 59 | const definitionFilter = exparser.Behavior.callDefinitionFilter(definition) 60 | const exparserDef = { 61 | is: this.id, 62 | using, 63 | generics: [], // TODO 64 | template: { 65 | func: this.generateFunc, 66 | data: this.data, 67 | }, 68 | properties: definition.properties, 69 | data: definition.data, 70 | methods: definition.methods, 71 | behaviors: definition.behaviors, 72 | created: definition.created, 73 | attached: definition.attached, 74 | ready: definition.ready, 75 | moved: definition.moved, 76 | detached: definition.detached, 77 | saved: definition.saved, 78 | restored: definition.restored, 79 | relations: definition.relations, 80 | externalClasses: definition.externalClasses, 81 | options: { 82 | domain: `${options.writeOnly ? 'wo://' : ''}/`, 83 | writeOnly: options.writeOnly || false, 84 | allowInWriteOnly: false, 85 | lazyRegistration: true, 86 | classPrefix: options.classPrefix || '', 87 | addGlobalClass: false, 88 | templateEngine: TemplateEngine, 89 | renderingMode: 'full', 90 | multipleSlots: options.multipleSlots || false, 91 | publicProperties: true, 92 | reflectToAttributes: false, 93 | writeFieldsToNode: false, 94 | writeIdToDOM: false, 95 | virtualHost: options.virtualHost || undefined, 96 | }, 97 | lifetimes: definition.lifetimes, 98 | pageLifetimes: definition.pageLifetimes, 99 | observers: definition.observers, 100 | definitionFilter, 101 | initiator() { 102 | // 更新方法调用者,即自定义组件中的 this 103 | const caller = Object.create(this, { 104 | data: { 105 | get: () => this.data, 106 | set: newData => this.data = newData, 107 | configurable: true 108 | }, 109 | }) 110 | const originalSetData = caller.setData 111 | const getSelectComponentResult = selected => { 112 | const selectedFilter = exparser.Component.getMethod(selected, '__export__') 113 | const defaultResult = exparser.Element.getMethodCaller(selected) 114 | if (selectedFilter) { 115 | const res = selectedFilter.call(exparser.Element.getMethodCaller(selected), caller) 116 | return res === undefined ? defaultResult : res 117 | } 118 | return defaultResult 119 | } 120 | 121 | caller._exparserNode = this // 存入原本对应的 exparserNode 实例 122 | caller.properties = caller.data 123 | caller.selectComponent = selector => { 124 | const exparserNode = this.shadowRoot.querySelector(selector) 125 | return getSelectComponentResult(exparserNode) 126 | } 127 | caller.selectAllComponents = selector => { 128 | const exparserNodes = this.shadowRoot.querySelectorAll(selector) 129 | return exparserNodes.map(item => getSelectComponentResult(item)) 130 | } 131 | caller.selectOwnerComponent = () => { 132 | const ownerShadowRoot = this.ownerShadowRoot 133 | if (!(ownerShadowRoot instanceof exparser.ShadowRoot)) return null 134 | 135 | const exparserNode = ownerShadowRoot.getHostNode() 136 | if (!exparserNode) return null 137 | 138 | return getSelectComponentResult(exparserNode) 139 | } 140 | caller.createSelectorQuery = () => new SelectorQuery(caller) 141 | caller.createIntersectionObserver = options => new IntersectionObserver(caller, options) 142 | caller.setData = (data, callback) => { 143 | if (!originalSetData || typeof originalSetData !== 'function') return 144 | 145 | originalSetData.call(this, data) 146 | 147 | if (typeof callback === 'function') { 148 | // 模拟异步情况 149 | Promise.resolve().then(callback).catch(console.error) 150 | } 151 | } 152 | caller.getRelationNodes = relationKey => { 153 | if (!path || !relationKey) return null 154 | 155 | const id = PATH_TO_ID_MAP[_.relativeToAbsolute(path, relationKey)] 156 | if (!id) return null 157 | 158 | const res = this.getRelationNodes(id) 159 | if (!res) return null 160 | return res.map(exparserNode => exparser.Element.getMethodCaller(exparserNode)) 161 | } 162 | 163 | Object.keys(methods).forEach(name => caller[name] = methods[name]) 164 | exparser.Element.setMethodCaller(this, caller) 165 | }, 166 | } 167 | 168 | const exparserReg = exparser.registerElement(exparserDef) 169 | exparser.Behavior.prepare(exparserReg.behavior) 170 | methods = exparserReg.behavior.methods 171 | 172 | return exparserReg 173 | } 174 | } 175 | 176 | /** 177 | * exparser 的模板引擎封装 178 | */ 179 | class TemplateEngine { 180 | static create(behavior, initValues) { 181 | const templateEngine = new TemplateEngine() 182 | const data = Object.assign({}, initValues, behavior.template.data) 183 | 184 | templateEngine._data = data 185 | templateEngine._generateFunc = behavior.template.func 186 | 187 | return templateEngine 188 | } 189 | 190 | static collectIdMapAndSlots(exparserNode, idMap, slots) { 191 | const children = exparserNode.childNodes 192 | 193 | for (const child of children) { 194 | if (child instanceof exparser.TextNode) continue 195 | if (child.__id) idMap[child.__id] = child 196 | if (child.__slotName !== undefined) slots[child.__slotName] = child 197 | 198 | TemplateEngine.collectIdMapAndSlots(child, idMap, slots) 199 | } 200 | } 201 | 202 | createInstance(exparserNode, properties = {}) { 203 | this._data = Object.assign(this._data, properties) 204 | this._vt = this._generateFunc({data: this._data}) // 生成虚拟树 205 | 206 | const instance = new TemplateEngineInstance() 207 | instance._generateFunc = this._generateFunc 208 | instance._vt = this._vt 209 | 210 | instance.data = _.copy(this._data) 211 | instance.idMap = {} 212 | instance.slots = {} 213 | instance.shadowRoot = render.renderExparserNode(instance._vt, exparserNode, null) // 渲染成 exparser 树 214 | instance.shadowRoot._vt = instance._vt 215 | instance.listeners = [] 216 | 217 | TemplateEngine.collectIdMapAndSlots(instance.shadowRoot, instance.idMap, instance.slots) 218 | 219 | return instance 220 | } 221 | } 222 | 223 | /** 224 | * exparser 的模板引擎实例 225 | */ 226 | class TemplateEngineInstance { 227 | /** 228 | * 当遇到组件更新时,会触发此方法 229 | */ 230 | updateValues(exparserNode, data, changedPaths, changedValues, changes) { 231 | const newVt = this._generateFunc({data}) // 生成新虚拟树 232 | 233 | // 合并到方法调用者的 data 中 234 | const callerData = exparser.Element.getMethodCaller(exparserNode).data 235 | const hasOwnProperty = Object.prototype.hasOwnProperty 236 | for (const changeInfo of changes) { 237 | if (!changeInfo) continue 238 | 239 | const path = changeInfo[0] 240 | const newData = changeInfo[1] 241 | let currentData = callerData 242 | let currentPath = path[0] 243 | 244 | // 检查更新路径 245 | for (let i = 1, len = path.length; i < len; i++) { 246 | const nextPath = path[i] 247 | const currentValue = currentData[currentPath] 248 | 249 | if (!hasOwnProperty.call(currentData, currentPath)) { 250 | // 不存在,则进行初始化 251 | if (typeof nextPath === 'number' && isFinite(nextPath)) { 252 | // 数组 253 | if (!Array.isArray(currentValue)) currentData[currentPath] = [] 254 | } else if (currentValue === null || typeof currentValue !== 'object' || Array.isArray(currentValue)) { 255 | // 对象 256 | currentData[currentPath] = {} 257 | } 258 | } 259 | 260 | currentData = currentData[currentPath] 261 | currentPath = nextPath 262 | } 263 | 264 | const oldData = currentData[currentPath] 265 | currentData[currentPath] = _.copy(newData) 266 | changedValues = [currentData[currentPath], oldData] 267 | } 268 | 269 | // 应用更新 270 | diff.diffVt(this._vt, newVt) 271 | this._vt = newVt 272 | } 273 | } 274 | 275 | module.exports = ComponentManager 276 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const exparser = require('miniprogram-exparser') 2 | const ComponentManager = require('./componentmanager') 3 | const RootComponent = require('./render/component') 4 | const _ = require('./tool/utils') 5 | 6 | module.exports = { 7 | /** 8 | * 注册组件 9 | */ 10 | register(definition = {}) { 11 | const componentManager = new ComponentManager(definition) 12 | 13 | return componentManager.id 14 | }, 15 | 16 | /** 17 | * 注册 behavior 18 | */ 19 | behavior(definition) { 20 | definition.is = _.getId(true) 21 | definition.options = { 22 | lazyRegistration: true, 23 | publicProperties: true, 24 | } 25 | 26 | _.adjustExparserDefinition(definition) 27 | definition.definitionFilter = exparser.Behavior.callDefinitionFilter(definition) 28 | exparser.registerBehavior(definition) 29 | 30 | return definition.is 31 | }, 32 | 33 | /** 34 | * 创建组件实例 35 | */ 36 | create(id, properties) { 37 | const componentManager = _.cache(id) 38 | 39 | if (!componentManager) return 40 | 41 | return new RootComponent(componentManager, properties) 42 | }, 43 | } 44 | 45 | global.wxFormField = module.exports.behavior({ 46 | id: 'wx://form-field', 47 | properties: { 48 | name: { 49 | type: String 50 | }, 51 | value: { 52 | type: null 53 | } 54 | } 55 | }) 56 | 57 | global.wxFormFieldGroup = module.exports.behavior({ 58 | is: 'wx://form-field-group', 59 | }) 60 | 61 | global.wxFormFieldButton = module.exports.behavior({ 62 | is: 'wx://form-field-button', 63 | listeners: { 64 | formSubmit(data) { 65 | this.triggerEvent('formSubmit', data, {bubbles: true}) 66 | }, 67 | formReset(data) { 68 | this.triggerEvent('formReset', data, {bubbles: true}) 69 | }, 70 | } 71 | }) 72 | 73 | global.wxComponentExport = module.exports.behavior({ 74 | is: 'wx://component-export', 75 | definitionFilter(def) { 76 | if (typeof def.export === 'function') { 77 | if (typeof def.methods === 'object') { 78 | def.methods.__export__ = def.export 79 | } else { 80 | def.methods = { 81 | __export__: def.export, 82 | } 83 | } 84 | } 85 | } 86 | }) 87 | -------------------------------------------------------------------------------- /src/render/component.js: -------------------------------------------------------------------------------- 1 | const exparser = require('miniprogram-exparser') 2 | const _ = require('../tool/utils') 3 | const IntersectionObserver = require('../tool/intersectionobserver') 4 | const CONSTANT = require('../tool/constant') 5 | const render = require('./render') 6 | 7 | const MOVE_DELTA = 10 8 | const LONGPRESS_TIME = 350 9 | const SCROLL_PROTECTED = 150 10 | const NATIVE_TOUCH_EVENT = ['touchstart', 'touchmove', 'touchend', 'touchcancel'] 11 | 12 | /** 13 | * 遍历 exparser 树 14 | */ 15 | function dfsExparserTree(node, callback, fromTopToBottom) { 16 | if (node instanceof exparser.Component) { 17 | if (fromTopToBottom) callback(node) 18 | if (node.shadowRoot instanceof exparser.Element) dfsExparserTree(node.shadowRoot, callback, fromTopToBottom) 19 | if (!fromTopToBottom) callback(node) 20 | } 21 | node.childNodes.forEach(child => { 22 | if (child instanceof exparser.Element) dfsExparserTree(child, callback, fromTopToBottom) 23 | }) 24 | } 25 | 26 | /** 27 | * 用于 miniprogram-simulate/jest-snapshot-plugin 的识别 28 | */ 29 | const JSONSymbol = typeof Symbol === 'function' && Symbol.for ? Symbol.for('j-component.json') : 0xd846fe 30 | 31 | /** 32 | * 序列化 exparser 树节点上绑定的事件监听 33 | */ 34 | function exparserNodeEventToJSON(node) { 35 | return node._vt ? node._vt.event : {} 36 | } 37 | 38 | /** 39 | * 序列化 exparser 树节点的属性 40 | */ 41 | function exparserNodeAttrsToJSON(node) { 42 | const attrs = [] 43 | const vt = node._vt 44 | 45 | if (vt) { 46 | vt.attrs.forEach(attr => { 47 | if (!exparser.Component.hasPublicProperty(node, _.dashToCamelCase(attr.name))) { 48 | attrs.push(attr) 49 | } 50 | }) 51 | } 52 | return attrs 53 | } 54 | 55 | /** 56 | * 将 exparser 树转换为 JSON 对象 57 | */ 58 | function exparserTreeToJSON(node) { 59 | const _inner = (node, array) => { 60 | let children = array 61 | const vt = node._vt 62 | 63 | if (node instanceof exparser.TextNode) { 64 | array.push(node.textContent) 65 | } else if (node instanceof exparser.Element) { 66 | if (!node.__virtual) { 67 | children = [] 68 | const child = { 69 | tagName: _.getTagName(vt.componentId || vt.tagName) || vt.tagName, 70 | event: exparserNodeEventToJSON(node), 71 | attrs: exparserNodeAttrsToJSON(node), 72 | children, 73 | } 74 | Object.defineProperty(child, '$$typeof', { 75 | get() { 76 | return JSONSymbol 77 | } 78 | }) 79 | array.push(child) 80 | } 81 | ;(node.__wxSlotChildren || []).forEach(child => _inner(child, children)) 82 | } 83 | 84 | return array 85 | } 86 | 87 | return _inner(node, [])[0] 88 | } 89 | 90 | class Component { 91 | constructor(exparserNode) { 92 | this._exparserNode = exparserNode 93 | } 94 | 95 | get dom() { 96 | return this._exparserNode.$$ 97 | } 98 | 99 | get data() { 100 | const caller = exparser.Element.getMethodCaller(this._exparserNode) 101 | 102 | return caller && caller.data 103 | } 104 | 105 | get instance() { 106 | return exparser.Element.getMethodCaller(this._exparserNode) 107 | } 108 | 109 | /** 110 | * 触发事件 111 | */ 112 | dispatchEvent(eventName, options = {}) { 113 | const dom = this.dom 114 | 115 | if (NATIVE_TOUCH_EVENT.indexOf(eventName) >= 0) { 116 | // native touch event 117 | let touches = options.touches 118 | let changedTouches = options.changedTouches 119 | 120 | if (eventName === 'touchstart' || eventName === 'touchmove') { 121 | touches = touches || [{x: 0, y: 0}] 122 | changedTouches = changedTouches || [{x: 0, y: 0}] 123 | } else if (eventName === 'touchend' || eventName === 'touchcancel') { 124 | touches = touches || [] 125 | changedTouches = changedTouches || [{x: 0, y: 0}] 126 | } 127 | 128 | const touchEvent = new TouchEvent(eventName, { 129 | cancelable: true, 130 | bubbles: true, 131 | touches: touches.map(touch => new Touch({ 132 | identifier: _.getId(), 133 | target: dom, 134 | clientX: touch.x, 135 | clientY: touch.y, 136 | pageX: touch.x, 137 | pageY: touch.y, 138 | })), 139 | targetTouches: [], 140 | changedTouches: changedTouches.map(touch => new Touch({ 141 | identifier: _.getId(), 142 | target: dom, 143 | clientX: touch.x, 144 | clientY: touch.y, 145 | pageX: touch.x, 146 | pageY: touch.y, 147 | })), 148 | }) 149 | 150 | // 模拟异步情况 151 | Promise.resolve().then(() => { 152 | dom.dispatchEvent(touchEvent) 153 | }).catch(console.error) 154 | } else { 155 | // 自定义事件 156 | const customEvent = new CustomEvent(eventName, options) 157 | 158 | // 模拟异步情况 159 | Promise.resolve().then(() => { 160 | dom.dispatchEvent(customEvent) 161 | 162 | if (customEvent.target.__wxElement) { 163 | exparser.Event.dispatchEvent(customEvent.target.__wxElement, exparser.Event.create(eventName, options.detail || {}, { 164 | originalEvent: customEvent, 165 | bubbles: true, 166 | capturePhase: true, 167 | composed: true, 168 | extraFields: { 169 | touches: options.touches || {}, 170 | changedTouches: options.changedTouches || {}, 171 | }, 172 | })) 173 | } 174 | }).catch(console.error) 175 | } 176 | } 177 | 178 | /** 179 | * 监听组件事件 180 | */ 181 | addEventListener(eventName, handler, capture = false) { 182 | if (typeof capture === 'object') capture = !!capture.capture 183 | this._exparserNode.addListener(eventName, handler, {capture}) 184 | } 185 | 186 | /** 187 | * 取消监听组件事件 188 | */ 189 | removeEventListener(eventName, handler, capture = false) { 190 | if (typeof capture === 'object') capture = !!capture.capture 191 | this._exparserNode.removeListener(eventName, handler, {capture}) 192 | } 193 | 194 | /** 195 | * 选取第一个符合的子组件节点 196 | */ 197 | querySelector(selector) { 198 | const shadowRoot = this._exparserNode.shadowRoot 199 | const selExparserNode = shadowRoot && shadowRoot.querySelector(selector) 200 | 201 | if (selExparserNode) { 202 | return selExparserNode.__componentNode__ ? selExparserNode.__componentNode__ : new Component(selExparserNode) 203 | } 204 | } 205 | 206 | /** 207 | * 选取所有符合的子组件节点 208 | */ 209 | querySelectorAll(selector) { 210 | const shadowRoot = this._exparserNode.shadowRoot 211 | const selExparserNodes = shadowRoot.querySelectorAll(selector) || [] 212 | 213 | return selExparserNodes.map(selExparserNode => (selExparserNode.__componentNode__ ? selExparserNode.__componentNode__ : new Component(selExparserNode))) 214 | } 215 | 216 | /** 217 | * 小程序自定义组件的 setData 方法 218 | */ 219 | setData(data, callback) { 220 | const caller = exparser.Element.getMethodCaller(this._exparserNode) 221 | 222 | if (caller && typeof caller.setData === 'function') caller.setData(data) 223 | if (typeof callback === 'function') { 224 | // 模拟异步情况 225 | Promise.resolve().then(callback).catch(console.error) 226 | } 227 | } 228 | 229 | /** 230 | * 触发生命周期 231 | */ 232 | triggerLifeTime(lifeTime, ...args) { 233 | this._exparserNode.triggerLifeTime(lifeTime, args) 234 | } 235 | 236 | /** 237 | * 触发页面生命周期 238 | */ 239 | triggerPageLifeTime(lifeTime, ...args) { 240 | this._exparserNode.triggerPageLifeTime(lifeTime, args) 241 | } 242 | 243 | /** 244 | * 生成JSON 245 | */ 246 | toJSON() { 247 | return exparserTreeToJSON(this._exparserNode) 248 | } 249 | } 250 | 251 | class RootComponent extends Component { 252 | constructor(componentManager, properties) { 253 | super() 254 | 255 | const id = componentManager.id 256 | const tagName = _.getTagName(id) 257 | const exparserDef = componentManager.exparserDef 258 | this._exparserNode = exparser.createElement(tagName || id, exparserDef) // create exparser node and render 259 | this._isTapCancel = false 260 | this._lastScrollTime = 0 261 | 262 | const attrs = Object.keys(properties || {}).map(key => ({name: key, value: properties[key]})) 263 | if (attrs.length) { 264 | // 对齐 observer 逻辑,走 updateAttr 来更新 property 265 | render.updateAttrs(this._exparserNode, attrs) 266 | } 267 | 268 | this._exparserNode._vt = { 269 | type: CONSTANT.TYPE_COMPONENT, 270 | tagName: tagName || 'main', 271 | attrs, 272 | event: {}, 273 | children: [] 274 | } 275 | 276 | this.parentNode = null 277 | 278 | this._bindEvent() 279 | } 280 | 281 | get dom() { 282 | return _.getDom(this._exparserNode) 283 | } 284 | 285 | /** 286 | * 初始化事件 287 | */ 288 | _bindEvent() { 289 | const dom = this.dom 290 | 291 | // touch 事件 292 | dom.addEventListener('touchstart', evt => { 293 | this._triggerExparserEvent(evt, 'touchstart') 294 | 295 | if (this._touchstartEvt || evt.defaultPrevented) return 296 | if (evt.touches.length === 1) { 297 | if (this._longpressTimer) this._longpressTimer = clearTimeout(this._longpressTimer) 298 | 299 | this._touchstartX = evt.touches[0].pageX 300 | this._touchstartY = evt.touches[0].pageY 301 | this._touchstartEvt = evt 302 | 303 | if ((+new Date()) - this._lastScrollTime < SCROLL_PROTECTED) { 304 | // 滚动中 305 | this._isTapCancel = true 306 | this._lastScrollTime = 0 // 只检查一次 307 | } else { 308 | this._isTapCancel = false 309 | this._longpressTimer = setTimeout(() => { 310 | this._isTapCancel = true // 取消后续的 tap 311 | this._triggerExparserEvent(evt, 'longpress', {x: this._touchstartX, y: this._touchstartY}) 312 | }, LONGPRESS_TIME) 313 | } 314 | } 315 | }, {capture: true, passive: false}) 316 | 317 | dom.addEventListener('touchmove', evt => { 318 | this._triggerExparserEvent(evt, 'touchmove') 319 | 320 | if (!this._touchstartEvt) return 321 | if (evt.touches.length === 1) { 322 | if (!(Math.abs(evt.touches[0].pageX - this._touchstartX) < MOVE_DELTA && Math.abs(evt.touches[0].pageY - this._touchstartY) < MOVE_DELTA)) { 323 | // is moving 324 | if (this._longpressTimer) this._longpressTimer = clearTimeout(this._longpressTimer) 325 | this._isTapCancel = true 326 | } 327 | } 328 | }, {capture: true, passive: false}) 329 | 330 | dom.addEventListener('touchend', evt => { 331 | this._triggerExparserEvent(evt, 'touchend') 332 | 333 | if (!this._touchstartEvt) return 334 | if (evt.touches.length === 0) { 335 | if (this._longpressTimer) this._longpressTimer = clearTimeout(this._longpressTimer) 336 | if (!this._isTapCancel) this._triggerExparserEvent(this._touchstartEvt, 'tap', {x: evt.changedTouches[0].pageX, y: evt.changedTouches[0].pageY}) 337 | } 338 | 339 | this._touchstartEvt = null // 重置 touchStart 事件 340 | }, {capture: true, passive: false}) 341 | 342 | dom.addEventListener('touchcancel', evt => { 343 | this._triggerExparserEvent(evt, 'touchcancel') 344 | 345 | if (!this._touchstartEvt) return 346 | if (this._longpressTimer) this._longpressTimer = clearTimeout(this._longpressTimer) 347 | 348 | this._touchstartEvt = null // 重置 touchStart 事件 349 | }, {capture: true, passive: false}) 350 | 351 | // 其他事件 352 | dom.addEventListener('scroll', evt => { 353 | // 触发 intersectionObserver 354 | const listenInfoMap = this._exparserNode._listenInfoMap || {} 355 | Object.keys(listenInfoMap).forEach(key => { 356 | const listenInfo = listenInfoMap[key] 357 | IntersectionObserver.updateTargetIntersection(listenInfo) 358 | }) 359 | 360 | this._lastScrollTime = +new Date() 361 | this._triggerExparserEvent(evt, 'scroll') 362 | }, {capture: true, passive: false}) 363 | 364 | // eslint-disable-next-line no-unused-vars 365 | dom.addEventListener('blur', evt => { 366 | if (this._longpressTimer) this._longpressTimer = clearTimeout(this._longpressTimer) 367 | }, {capture: true, passive: false}) 368 | } 369 | 370 | /** 371 | * 触发 exparser 节点事件 372 | */ 373 | _triggerExparserEvent(evt, name, detail = {}) { 374 | Promise.resolve().then(() => { 375 | exparser.Event.dispatchEvent(evt.target, exparser.Event.create(name, detail, { 376 | originalEvent: evt, 377 | bubbles: true, 378 | capturePhase: true, 379 | composed: true, 380 | extraFields: { 381 | touches: evt.touches || {}, 382 | changedTouches: evt.changedTouches || {}, 383 | }, 384 | })) 385 | }).catch(console.error) 386 | } 387 | 388 | /** 389 | * 添加 390 | */ 391 | attach(parent) { 392 | parent.appendChild(this.dom) 393 | this.parentNode = parent 394 | 395 | exparser.Element.pretendAttached(this._exparserNode) 396 | dfsExparserTree(this._exparserNode, node => node.triggerLifeTime('ready')) 397 | } 398 | 399 | /** 400 | * 移除 401 | */ 402 | detach() { 403 | if (!this.parentNode) return 404 | 405 | this.parentNode.removeChild(this.dom) 406 | this.parentNode = null 407 | 408 | exparser.Element.pretendDetached(this._exparserNode) 409 | } 410 | } 411 | 412 | module.exports = RootComponent 413 | -------------------------------------------------------------------------------- /src/render/diff.js: -------------------------------------------------------------------------------- 1 | const exparser = require('miniprogram-exparser') 2 | const render = require('./render') 3 | const CONSTANT = require('../tool/constant') 4 | 5 | /** 6 | * diff 两棵虚拟树 7 | */ 8 | function diffVt(oldVt, newVt) { 9 | const node = oldVt.exparserNode 10 | const parent = node.parentNode 11 | 12 | newVt.exparserNode = node // 更新新虚拟树的 exparser 节点 13 | 14 | if (!newVt) { 15 | // 删除 16 | if (parent) parent.removeChild(node) 17 | } else if (oldVt.type === CONSTANT.TYPE_TEXT) { 18 | // 更新文本节点 19 | if (newVt.type !== CONSTANT.TYPE_TEXT || newVt.content !== oldVt.content) { 20 | if (parent) { 21 | const newNode = render.renderExparserNode(newVt, null, parent.ownerShadowRoot) 22 | newNode._vt = newVt 23 | parent.replaceChild(newNode, node) 24 | } 25 | } 26 | } else { 27 | // 更新其他节点 28 | // eslint-disable-next-line no-lonely-if 29 | if (newVt.type === CONSTANT.TYPE_TEXT) { 30 | // 新节点是文本节点 31 | if (parent) { 32 | const newNode = render.renderExparserNode(newVt, null, parent.ownerShadowRoot) 33 | newNode._vt = newVt 34 | parent.replaceChild(newNode, node) 35 | } 36 | } else if (newVt.type === oldVt.type && newVt.componentId === oldVt.componentId && newVt.key === oldVt.key) { 37 | // 检查属性 38 | const attrs = diffAttrs(oldVt.attrs, newVt.attrs) 39 | if (attrs) { 40 | // 更新属性 41 | newVt.attrs = attrs 42 | render.updateAttrs(node, attrs) 43 | } 44 | 45 | // 检查事件 46 | Object.keys(oldVt.event).forEach(key => { 47 | const {name, isCapture, id} = oldVt.event[key] 48 | 49 | exparser.removeListenerFromElement(node, name, id, {capture: isCapture}) 50 | }) 51 | render.updateEvent(node, newVt.event) 52 | 53 | // 检查子节点 54 | const oldChildren = oldVt.children 55 | const newChildren = newVt.children 56 | const diffs = diffList(oldChildren, newChildren) 57 | 58 | // diff 子节点树 59 | for (let i = 0, len = oldChildren.length; i < len; i++) { 60 | const oldChild = oldChildren[i] 61 | const newChild = diffs.children[i] 62 | 63 | if (newChild) diffVt(oldChild, newChild) 64 | } 65 | if (diffs.moves) { 66 | // 子节点的删除/插入/重排 67 | let {inserts} = diffs.moves 68 | const {removes} = diffs.moves 69 | const children = node.childNodes 70 | 71 | inserts = inserts.map(({oldIndex, index}) => { 72 | const newNode = children[oldIndex] || render.renderExparserNode(newChildren[index], null, node.ownerShadowRoot) 73 | newNode._vt = newChildren[index] 74 | 75 | return { 76 | newNode, 77 | index, 78 | } 79 | }) 80 | 81 | removes.forEach(index => node.removeChild(children[index])) 82 | inserts.forEach(({newNode, index}) => node.insertBefore(newNode, children[index])) 83 | } 84 | node._vt = newVt 85 | } else if (parent) { 86 | const newNode = render.renderExparserNode(newVt, null, parent.ownerShadowRoot) 87 | newNode._vt = newVt 88 | parent.replaceChild(newNode, node) 89 | } 90 | } 91 | } 92 | 93 | /** 94 | * diff 属性 95 | */ 96 | function diffAttrs(oldAttrs, newAttrs) { 97 | const oldAttrsMap = {} 98 | const newAttrsMap = {} 99 | const retAttrs = [] 100 | let isChange = false 101 | 102 | oldAttrs.forEach(attr => oldAttrsMap[attr.name] = attr.value) 103 | 104 | for (const attr of newAttrs) { 105 | // 添加/更新 106 | newAttrsMap[attr.name] = attr.value 107 | retAttrs.push(attr) 108 | 109 | if (oldAttrsMap[attr.name] === undefined || oldAttrsMap[attr.name] !== attr.value) isChange = true 110 | } 111 | 112 | for (const attr of oldAttrs) { 113 | if (newAttrsMap[attr.name] === undefined) { 114 | // 删除 115 | attr.value = undefined 116 | retAttrs.push(attr) 117 | 118 | isChange = true 119 | } 120 | } 121 | 122 | return isChange ? retAttrs : false 123 | } 124 | 125 | /** 126 | * diff 列表 127 | */ 128 | function diffList(oldList, newList) { 129 | const oldKeyMap = {} // 旧列表的 key-index 映射表 130 | const newKeyMap = {} // 新列表的 key-index 映射表 131 | const oldFreeList = [] // 旧列表中没有 key 的项的 index 列表 132 | const newFreeList = [] // 新列表中没有 key 的项的 index 列表 133 | 134 | oldList.forEach((item, index) => { 135 | if (item.key) { 136 | // 拥有 key 137 | if (Object.prototype.hasOwnProperty.call(oldKeyMap, item.key)) item.key = '' 138 | else oldKeyMap[item.key] = index 139 | } else { 140 | // 没有 key 141 | oldFreeList.push(index) 142 | } 143 | }) 144 | newList.forEach((item, index) => { 145 | if (item.key) { 146 | // 拥有 key 147 | if (Object.prototype.hasOwnProperty.call(newKeyMap, item.key)) newFreeList.push(index) 148 | else newKeyMap[item.key] = index 149 | } else { 150 | // 没有 key 151 | newFreeList.push(index) 152 | } 153 | }) 154 | 155 | const children = [] 156 | let removes = [] 157 | const inserts = [] 158 | 159 | // 检查旧列表 160 | for (let i = 0, j = 0; i < oldList.length; i++) { 161 | const item = oldList[i] 162 | const key = item.key 163 | 164 | if (key) { 165 | if (Object.prototype.hasOwnProperty.call(newKeyMap, key)) { 166 | // 在新列表中存在 167 | children.push(newList[newKeyMap[key]]) 168 | } else { 169 | // 需要从新列表中删除 170 | removes.push(i) 171 | children.push(null) 172 | } 173 | } else if (j < newFreeList.length) { 174 | // 在新列表中存在 175 | children.push(newList[newFreeList[j++]]) 176 | } else { 177 | // 需要从新列表中删除 178 | removes.push(i) 179 | children.push(null) 180 | } 181 | } 182 | removes = removes.reverse() // 从尾往头进行删除 183 | 184 | // 检查新列表 185 | const hasCheckIndexMap = {} 186 | for (let i = 0, j = 0, k = 0, len = newList.length; i < len; i++) { 187 | const item = newList[i] 188 | const key = item.key 189 | 190 | while (children[j] === null || hasCheckIndexMap[j]) j++ // 跳过已被删除/检查的项 191 | 192 | if (key) { 193 | if (Object.prototype.hasOwnProperty.call(oldKeyMap, key) && children[j]) { 194 | // 在旧列表中存在 195 | if (children[j].key === key) { 196 | // 拥有同样的 key 197 | j++ 198 | } else { 199 | // 拥有不同的 key 200 | const oldIndex = oldKeyMap[key] 201 | hasCheckIndexMap[oldIndex] = true 202 | if (oldIndex !== i) inserts.push({oldIndex, index: i}) 203 | } 204 | } else { 205 | // 插入新项 206 | inserts.push({oldIndex: -1, index: i}) 207 | } 208 | } else if (k < oldFreeList.length) { 209 | // 在旧列表中存在 210 | const oldIndex = oldFreeList[k++] 211 | hasCheckIndexMap[oldIndex] = true 212 | if (oldIndex !== i) inserts.push({oldIndex, index: i}) 213 | } else { 214 | // 插入新项 215 | inserts.push({oldIndex: -1, index: i}) 216 | } 217 | } 218 | 219 | return { 220 | children, 221 | moves: {removes, inserts}, 222 | } 223 | } 224 | 225 | module.exports = { 226 | diffVt, 227 | diffAttrs, 228 | diffList, 229 | } 230 | -------------------------------------------------------------------------------- /src/render/render.js: -------------------------------------------------------------------------------- 1 | const exparser = require('miniprogram-exparser') 2 | const CONSTANT = require('../tool/constant') 3 | const _ = require('../tool/utils') 4 | 5 | const transitionKeys = ['transition', 'transitionProperty', 'transform', 'transformOrigin', 'webkitTransition', 'webkitTransitionProperty', 'webkitTransform', 'webkitTransformOrigin'] 6 | 7 | /** 8 | * 更新 exparser 节点的属性 9 | */ 10 | function updateAttrs(exparserNode, attrs) { 11 | const isComponentNode = exparserNode instanceof exparser.Component 12 | const dataProxy = exparser.Component.getDataProxy(exparserNode) 13 | let needDoUpdate = false 14 | 15 | exparserNode.dataset = exparserNode.dataset || {} 16 | 17 | for (const {name, value} of attrs) { 18 | if (name === 'id' || name === 'slot' || (isComponentNode && name === 'class')) { 19 | // 普通属性 20 | exparserNode[name] = value || '' 21 | } else if (isComponentNode && name === 'style' && exparserNode.$$) { 22 | // style 23 | let animationStyle = exparserNode.__animationStyle || {} 24 | 25 | animationStyle = transitionKeys.map(key => { 26 | const styleValue = animationStyle[key.replace('webkitT', 't')] 27 | 28 | return styleValue !== undefined ? `${_.camelToDashCase(key)}:${styleValue}` : '' 29 | }).filter(item => !!item.trim()).join(';') 30 | 31 | exparserNode.setNodeStyle(_.transformRpx(value || '', true) + animationStyle) 32 | } else if (isComponentNode && exparser.Component.hasPublicProperty(exparserNode, _.dashToCamelCase(name))) { 33 | // public 属性,延迟处理 34 | dataProxy.scheduleReplace([_.dashToCamelCase(name)], value) 35 | needDoUpdate = true 36 | } else if (/^data-/.test(name)) { 37 | // dataset 38 | exparserNode.dataset[_.dashToCamelCase(name.slice(5).toLowerCase())] = value 39 | exparserNode.setAttribute(name, value) 40 | } else if (isComponentNode && name === 'animation') { 41 | // 动画 42 | if (exparserNode.$$ && value && value.actions && value.actions.length > 0) { 43 | let index = 0 44 | const actions = value.actions 45 | const length = actions.length 46 | const step = function () { 47 | if (index < length) { 48 | const styleObject = _.animationToStyle(actions[index]) 49 | const extraStyle = styleObject.style 50 | 51 | transitionKeys.forEach(key => { 52 | exparserNode.$$.style[key] = styleObject[key.replace('webkitT', 't')] 53 | }) 54 | 55 | Object.keys(extraStyle).forEach(key => { 56 | exparserNode.$$.style[key] = _.transformRpx(extraStyle[key]) 57 | }) 58 | 59 | exparserNode.__animationStyle = styleObject 60 | } 61 | } 62 | 63 | exparserNode.addListener('transitionend', () => { 64 | index += 1 65 | step() 66 | }) 67 | step() 68 | } 69 | } else if (isComponentNode && exparserNode.hasExternalClass(_.camelToDashCase(name))) { 70 | // 外部样式类 71 | exparserNode.setExternalClass(_.camelToDashCase(name), value) 72 | } 73 | } 74 | 75 | if (needDoUpdate) dataProxy.doUpdates(true) 76 | } 77 | 78 | /** 79 | * 更新 exparser 节点的事件监听 80 | */ 81 | function updateEvent(exparserNode, event) { 82 | const convertEventTarget = (target, currentTarget) => { 83 | if (currentTarget && (target instanceof exparser.VirtualNode) && !target.id && !Object.keys(target.dataset).length) { 84 | // 如果 target 是 slot 且 slot 未设置 id 和 dataset,则兼容以前的逻辑:target === currentTarget 85 | target = currentTarget 86 | } 87 | 88 | return { 89 | id: target.id, 90 | offsetLeft: target.$$ && target.$$.offsetLeft || 0, 91 | offsetTop: target.$$ && target.$$.offsetTop || 0, 92 | dataset: target.dataset, 93 | } 94 | } 95 | 96 | Object.keys(event).forEach(key => { 97 | const { 98 | name, isCapture, isMutated, isCatch, handler 99 | } = event[key] 100 | 101 | if (!handler) return 102 | 103 | event[key].id = exparser.addListenerToElement(exparserNode, name, function (evt) { 104 | const shadowRoot = exparserNode.ownerShadowRoot 105 | 106 | const mutatedMarked = evt.mutatedMarked() 107 | if (isMutated && evt.mutatedMarked()) return // 已经被标记为互斥的事件,不再触发 mut- 绑定的事件回调 108 | if (isMutated) evt.markMutated() 109 | 110 | if (shadowRoot) { 111 | const host = shadowRoot.getHostNode() 112 | const writeOnly = exparser.Component.getComponentOptions(host).writeOnly 113 | 114 | if (!writeOnly) { 115 | const caller = exparser.Element.getMethodCaller(host) 116 | 117 | if (typeof caller[handler] === 'function') { 118 | caller[handler]({ 119 | type: evt.type, 120 | timeStamp: evt.timeStamp, 121 | target: convertEventTarget(evt.target, this), 122 | currentTarget: convertEventTarget(this, null), 123 | detail: evt.detail, 124 | touches: evt.touches, 125 | changedTouches: evt.changedTouches, 126 | mut: mutatedMarked, 127 | }) 128 | } 129 | } 130 | } 131 | 132 | if (isCatch) return false 133 | }, {capture: isCapture}) 134 | }) 135 | } 136 | 137 | /** 138 | * 渲染成 exparser 节点 139 | */ 140 | function renderExparserNode(options, shadowRootHost, shadowRoot) { 141 | const type = options.type 142 | const tagName = options.tagName 143 | const componentId = options.componentId 144 | let exparserNode 145 | 146 | if (type === CONSTANT.TYPE_TEXT) { 147 | exparserNode = shadowRoot.createTextNode(options.content) // save exparser node 148 | } else { 149 | if (type === CONSTANT.TYPE_ROOT) { 150 | shadowRoot = exparser.ShadowRoot.create(shadowRootHost) 151 | exparserNode = shadowRoot 152 | } else if (type === CONSTANT.TYPE_SLOT) { 153 | exparserNode = shadowRoot.createVirtualNode(tagName) 154 | exparser.Element.setSlotName(exparserNode, options.slotName) 155 | } else if (type === CONSTANT.TYPE_TEMPLATE || type === CONSTANT.TYPE_IF || type === CONSTANT.TYPE_FOR || type === CONSTANT.TYPE_FORITEM) { 156 | exparserNode = shadowRoot.createVirtualNode(tagName) 157 | exparser.Element.setInheritSlots(exparserNode) 158 | } else { 159 | const componentTagName = _.getTagName(componentId || tagName) || tagName 160 | const componentName = componentId || tagName 161 | exparserNode = shadowRoot.createComponent(componentTagName, componentName, options.generics) 162 | } 163 | 164 | updateAttrs(exparserNode, options.attrs) 165 | updateEvent(exparserNode, options.event) 166 | 167 | // children 168 | options.children.forEach(vt => { 169 | const childExparserNode = renderExparserNode(vt, null, shadowRoot) 170 | exparserNode.appendChild(childExparserNode) 171 | }) 172 | } 173 | 174 | options.exparserNode = exparserNode // 保存 exparser node 175 | exparserNode._vt = options 176 | 177 | return exparserNode 178 | } 179 | 180 | module.exports = { 181 | updateAttrs, 182 | updateEvent, 183 | renderExparserNode, 184 | } 185 | -------------------------------------------------------------------------------- /src/template/compile.js: -------------------------------------------------------------------------------- 1 | const parse = require('./parse') 2 | const VirtualNode = require('./virtualnode') 3 | const expr = require('./expr') 4 | const _ = require('../tool/utils') 5 | const CONSTANT = require('../tool/constant') 6 | 7 | /** 8 | * 过滤属性 9 | */ 10 | function filterAttrs(attrs) { 11 | const statement = {} 12 | const event = {} 13 | const normalAttrs = [] 14 | 15 | for (const attr of attrs) { 16 | const name = attr.name 17 | const value = attr.value || '' 18 | 19 | if (name === 'wx:if') { 20 | statement.if = expr.getExpression(value) 21 | } else if (name === 'wx:elif') { 22 | statement.elif = expr.getExpression(value) 23 | } else if (name === 'wx:else') { 24 | statement.else = true 25 | } else if (name === 'wx:for') { 26 | statement.for = expr.getExpression(value) 27 | } else if (name === 'wx:for-item') { 28 | statement.forItem = value 29 | } else if (name === 'wx:for-index') { 30 | statement.forIndex = value 31 | } else if (name === 'wx:key') { 32 | statement.forKey = value 33 | } else { 34 | const eventObj = _.parseEvent(name, value) 35 | 36 | if (eventObj) { 37 | // 事件绑定 38 | event[eventObj.name] = eventObj 39 | } else { 40 | // 普通属性 41 | normalAttrs.push(attr) 42 | } 43 | } 44 | } 45 | 46 | return { 47 | statement, 48 | event, 49 | normalAttrs, 50 | } 51 | } 52 | 53 | module.exports = function (template, data, usingComponents) { 54 | if (!template || typeof template !== 'string' || !template.trim()) throw new Error('invalid template') 55 | template = template.trim() 56 | 57 | // 根节点 58 | const rootNode = new VirtualNode({ 59 | type: CONSTANT.TYPE_ROOT, 60 | componentManager: this, 61 | data, 62 | }) 63 | const stack = [rootNode] 64 | 65 | stack.last = function () { 66 | return this[this.length - 1] 67 | } 68 | 69 | parse(template, { 70 | start: (tagName, attrs, unary) => { 71 | let type 72 | let componentManager 73 | let id = '' 74 | 75 | if (tagName === 'slot') { 76 | type = CONSTANT.TYPE_SLOT 77 | } else if (tagName === 'template') { 78 | type = CONSTANT.TYPE_TEMPLATE 79 | tagName = 'virtual' 80 | } else if (tagName === 'block') { 81 | type = CONSTANT.TYPE_BLOCK 82 | } else if (tagName === 'import') { 83 | type = CONSTANT.TYPE_IMPORT 84 | } else if (tagName === 'include') { 85 | type = CONSTANT.TYPE_INCLUDE 86 | } else if (tagName === 'wxs') { 87 | type = CONSTANT.TYPE_WXS 88 | } else if (_.isHtmlTag(tagName)) { 89 | type = CONSTANT.TYPE_NATIVE 90 | id = tagName 91 | } else { 92 | type = CONSTANT.TYPE_COMPONENT 93 | id = usingComponents[tagName] 94 | componentManager = id ? _.cache(id) : _.cache(tagName) 95 | 96 | if (!componentManager) throw new Error(`component ${tagName} not found`) 97 | else id = componentManager.id 98 | } 99 | 100 | const {statement, event, normalAttrs} = filterAttrs(attrs) 101 | 102 | const parent = stack.last() 103 | const node = new VirtualNode({ 104 | type, 105 | tagName, 106 | componentId: id, 107 | attrs: normalAttrs, 108 | event, 109 | generics: {}, // TODO 110 | componentManager, 111 | root: rootNode, 112 | }) 113 | let appendNode = node 114 | 115 | // for 语句 116 | if (statement.for) { 117 | const itemNode = new VirtualNode({ 118 | type: CONSTANT.TYPE_FORITEM, 119 | tagName: 'virtual', 120 | statement: { 121 | forItem: statement.forItem || 'item', 122 | forIndex: statement.forIndex || 'index', 123 | forKey: statement.forKey, 124 | }, 125 | children: [node], 126 | root: rootNode, 127 | }) 128 | node.setParent(itemNode, 0) // 更新父节点 129 | 130 | const forNode = new VirtualNode({ 131 | type: CONSTANT.TYPE_FOR, 132 | tagName: 'wx:for', 133 | statement: { 134 | for: statement.for, 135 | }, 136 | children: [itemNode], 137 | root: rootNode, 138 | }) 139 | itemNode.setParent(forNode, 0) // 更新父节点 140 | 141 | appendNode = forNode 142 | } 143 | 144 | // 条件语句 145 | if (statement.if || statement.elif || statement.else) { 146 | const ifNode = new VirtualNode({ 147 | type: CONSTANT.TYPE_IF, 148 | tagName: 'wx:if', 149 | statement: { 150 | if: statement.if, 151 | elif: statement.elif, 152 | else: statement.else, 153 | }, 154 | children: [node], 155 | root: rootNode, 156 | }) 157 | node.setParent(ifNode, 0) // 更新父节点 158 | 159 | appendNode = ifNode 160 | } 161 | 162 | if (!unary) { 163 | stack.push(node) 164 | } 165 | 166 | appendNode.setParent(parent, parent.children.length) // 更新父节点 167 | parent.appendChild(appendNode) 168 | }, 169 | // eslint-disable-next-line no-unused-vars 170 | end: tagName => { 171 | stack.pop() 172 | }, 173 | text: content => { 174 | content = content.trim() 175 | if (!content) return 176 | 177 | const parent = stack.last() 178 | if (parent.type === CONSTANT.TYPE_WXS) { 179 | // wxs 节点 180 | parent.setWxsContent(content) 181 | } else { 182 | // 文本节点 183 | parent.appendChild(new VirtualNode({ 184 | type: CONSTANT.TYPE_TEXT, 185 | content: content.replace(/[\n\r\t\s]+/g, ' '), 186 | parent, 187 | index: parent.children.length, 188 | componentManager: this, 189 | root: rootNode, 190 | })) 191 | } 192 | }, 193 | }) 194 | 195 | if (stack.length !== 1) throw new Error(`build ast error: ${template}`) 196 | 197 | return rootNode.generate.bind(rootNode) 198 | } 199 | -------------------------------------------------------------------------------- /src/template/expr.js: -------------------------------------------------------------------------------- 1 | const Expression = require('expr-parser') 2 | 3 | module.exports = { 4 | /** 5 | * 获取表达式 6 | */ 7 | getExpression(content) { 8 | let end = 0 9 | let start = content.indexOf('{{', end) 10 | const res = [] 11 | 12 | while (start >= 0) { 13 | let expression 14 | 15 | res.push(content.substring(end, start)) // before 16 | start += 2 17 | end = content.indexOf('}}', start) 18 | 19 | if (end >= 0) { 20 | expression = new Expression(content.substring(start, end)) 21 | end += 2 22 | } else { 23 | // without end 24 | res.push(content.substring(start - 2)) 25 | end = content.length 26 | } 27 | 28 | if (expression) res.push(expression.parse()) 29 | start = content.indexOf('{{', end) 30 | } 31 | 32 | res.push(content.substring(end)) // after 33 | 34 | return res.filter(item => !!item) 35 | }, 36 | 37 | /** 38 | * 计算表达式 39 | */ 40 | calcExpression(arr, data = {}) { 41 | if (!arr || typeof arr === 'string' || typeof arr === 'number' || typeof arr === 'boolean') { 42 | return arr 43 | } if (arr.length === 1 && typeof arr[0] === 'function') { 44 | return arr[0](data) 45 | } 46 | 47 | return arr.map(item => { 48 | if (typeof item === 'string') return item 49 | if (typeof item === 'function') return item(data) 50 | 51 | return '' 52 | }).join('') 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /src/template/parse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 感谢 John Resig 3 | * 源码:https://johnresig.com/files/htmlparser.js 4 | */ 5 | 6 | const startTagReg = /^<([-A-Za-z0-9_]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/ 7 | const endTagReg = /^<\/([-A-Za-z0-9_]+)[^>]*>/ 8 | const attrReg = /([-A-Za-z0-9_:]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g 9 | 10 | module.exports = function (content, handler = {}) { 11 | const stack = [] 12 | let last = content 13 | 14 | stack.last = function () { 15 | return this[this.length - 1] 16 | } 17 | 18 | while (content) { 19 | let isText = true 20 | 21 | if (!stack.last() || stack.last() !== 'wxs') { 22 | if (content.indexOf('') 25 | 26 | if (index >= 0) { 27 | content = content.substring(index + 3) 28 | isText = false 29 | } 30 | } else if (content.indexOf(']*>`)).exec(content) 60 | 61 | if (execRes) { 62 | let text = content.substring(0, execRes.index) 63 | content = content.substring(execRes.index + execRes[0].length) 64 | 65 | text = text.replace(//g, '') 66 | if (text && handler.text) handler.text(text) 67 | } 68 | 69 | parseEndTag('', stack.last()) 70 | } 71 | 72 | 73 | if (content === last) throw new Error(`parse error: ${content}`) 74 | last = content 75 | } 76 | 77 | // 清空保留的标签 78 | parseEndTag() 79 | 80 | function parseStartTag(tag, tagName, rest, unary) { 81 | unary = !!unary 82 | 83 | if (!unary) stack.push(tagName) 84 | 85 | if (handler.start) { 86 | const attrs = [] 87 | 88 | rest.replace(attrReg, (all, $1, $2, $3, $4) => { 89 | attrs.push({ 90 | name: $1, 91 | value: $2 !== undefined ? $2 : $3 !== undefined ? $3 : $4 !== undefined ? $4 : true, 92 | }) 93 | }) 94 | 95 | if (handler.start) handler.start(tagName, attrs, unary) 96 | } 97 | } 98 | 99 | function parseEndTag(tag, tagName) { 100 | let pos 101 | 102 | if (!tagName) { 103 | pos = 0 104 | } else { 105 | // 找到最近的同类型开始标签 106 | for (pos = stack.length - 1; pos >= 0; pos--) { 107 | if (stack[pos] === tagName) break 108 | } 109 | } 110 | 111 | if (pos >= 0) { 112 | // 关闭所有的开始标签,并让其出栈 113 | for (let i = stack.length - 1; i >= pos; i--) { 114 | if (handler.end) handler.end(stack[i]) 115 | } 116 | 117 | stack.length = pos 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/template/transform.js: -------------------------------------------------------------------------------- 1 | const CONSTANT = require('../tool/constant') 2 | const _ = require('../tool/utils') 3 | 4 | /** 5 | * 过滤属性 6 | */ 7 | function filterAttrs(attrs = {}) { 8 | const event = {} 9 | const normalAttrs = [] 10 | let slotName = '' 11 | 12 | const attrsKeyList = Object.keys(attrs) 13 | 14 | for (const name of attrsKeyList) { 15 | const value = attrs[name] === undefined ? '' : attrs[name] 16 | const eventObj = _.parseEvent(name, value) 17 | 18 | if (eventObj) { 19 | // 事件绑定 20 | event[eventObj.name] = eventObj 21 | } else { 22 | // 普通属性 23 | if (name === 'name') slotName = value 24 | 25 | normalAttrs.push({name, value}) 26 | } 27 | } 28 | 29 | return { 30 | event, 31 | normalAttrs, 32 | slotName, 33 | } 34 | } 35 | 36 | /** 37 | * 将 wcc 输出转化成 j-component 需要的结构 38 | */ 39 | function transformCompileResTree(obj, parent, usingComponents) { 40 | let node = null 41 | 42 | // 特别注意:使用 wcc 编译,不会产生 import、block、include、wxs、native(小程序不支持 div 等标签);template 节点会当作 if 节点处理 43 | 44 | if (typeof obj === 'string' || (typeof obj === 'number' && obj % 1 === 0)) { 45 | // 文本节点 46 | node = { 47 | type: CONSTANT.TYPE_TEXT, 48 | tagName: '', 49 | componentId: '', 50 | content: '' + obj, // 文本节点的内容 51 | key: '', // 节点的 key,diff 用 52 | children: [], 53 | generics: [], 54 | attrs: [], 55 | event: {}, 56 | slotName: '', // slot 节点的 name 属性 57 | } 58 | } else { 59 | // 其他节点 60 | const children = [] 61 | const { 62 | tag, wxKey, wxXCkey, attr 63 | } = obj 64 | const tagName = tag.indexOf('wx-') === 0 && (tag === 'wx-slot' || !_.isOfficialTag(tag)) ? tag.substr(3) : tag 65 | const key = wxKey !== undefined && wxKey !== null ? '' + wxKey : undefined 66 | const {event, normalAttrs, slotName} = filterAttrs(attr) 67 | const isIf = wxXCkey === 1 || wxXCkey === 3 68 | const isFor = wxXCkey === 2 || wxXCkey === 4 69 | const isSlot = tagName === 'slot' 70 | const isRoot = tagName === 'shadow' 71 | let type = isRoot ? CONSTANT.TYPE_ROOT : isIf ? CONSTANT.TYPE_IF : isFor ? CONSTANT.TYPE_FOR : isSlot ? CONSTANT.TYPE_SLOT : CONSTANT.TYPE_COMPONENT 72 | 73 | if (parent && parent.type === CONSTANT.TYPE_FOR) { 74 | type = CONSTANT.TYPE_FORITEM 75 | } 76 | 77 | node = { 78 | type, 79 | tagName, 80 | componentId: usingComponents[tagName] || tagName, 81 | content: '', // 文本节点的内容 82 | key, // 节点的 key,diff 用 83 | children, 84 | generics: obj.generics, 85 | attrs: normalAttrs, 86 | event, 87 | slotName: isSlot ? slotName : '', // slot 节点的 name 属性 88 | } 89 | 90 | obj.children.forEach(child => children.push(transformCompileResTree(child, node, usingComponents))) 91 | } 92 | 93 | return node 94 | } 95 | 96 | module.exports = function (generateFunc, usingComponents) { 97 | return function (options = {}) { 98 | const data = options.data || {} 99 | const compileRes = generateFunc(data) 100 | 101 | if (compileRes.type !== CONSTANT.TYPE_ROOT && (compileRes.tag === 'wx-page' || compileRes.tag === 'shadow')) { 102 | // 进行 wcc 编译结果的转化 103 | compileRes.tag = 'shadow' 104 | return transformCompileResTree(compileRes, null, usingComponents) 105 | } else { 106 | return compileRes 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/template/virtualnode.js: -------------------------------------------------------------------------------- 1 | const expr = require('./expr') 2 | const CONSTANT = require('../tool/constant') 3 | 4 | class VirtualNode { 5 | constructor(options = {}) { 6 | this.type = options.type 7 | this.tagName = options.tagName || '' 8 | this.componentId = options.componentId 9 | this.root = options.root || this // 根节点的 root 是自己 10 | this.parent = options.parent 11 | this.index = options.index || 0 12 | this.content = options.content && expr.getExpression(options.content) 13 | this.attrs = options.attrs || [] 14 | this.event = options.event || {} 15 | this.statement = options.statement || {} // if/for 语句 16 | this.children = options.children || [] 17 | this.generics = options.generics 18 | this.componentManager = options.componentManager // 所属的 componentManager 实例 19 | 20 | // 根节点用 21 | this.data = options.data || {} 22 | 23 | // wxs 节点用 24 | this.wxsModuleName = '' 25 | 26 | // slot 节点用 27 | this.slotName = '' 28 | 29 | this.checkAttrs() 30 | } 31 | 32 | /** 33 | * 检查属性 34 | */ 35 | checkAttrs() { 36 | const type = this.type 37 | const attrs = this.attrs 38 | const filterAttrs = [] 39 | 40 | for (const attr of attrs) { 41 | const name = attr.name 42 | const value = attr.value 43 | 44 | if (type === CONSTANT.TYPE_WXS && name === 'module') { 45 | // wxs 模块 46 | this.wxsModuleName = value || '' 47 | } else if (type === CONSTANT.TYPE_SLOT && name === 'name') { 48 | // slot 名 49 | this.slotName = value || '' 50 | } else { 51 | if (value && typeof value === 'string') attr.value = expr.getExpression(value) 52 | filterAttrs.push(attr) 53 | } 54 | } 55 | 56 | this.attrs = filterAttrs 57 | } 58 | 59 | /** 60 | * 设置父节点 61 | */ 62 | setParent(parent, index = 0) { 63 | if (!parent) return 64 | 65 | this.parent = parent 66 | this.index = index 67 | } 68 | 69 | /** 70 | * 添加子节点 71 | */ 72 | appendChild(node) { 73 | this.children.push(node) 74 | } 75 | 76 | /** 77 | * 设置 wxs 内容并转换成函数 78 | */ 79 | setWxsContent(content) { 80 | if (!this.wxsModuleName) return 81 | 82 | // eslint-disable-next-line no-new-func 83 | const func = new Function('require', 'module', content) 84 | const req = () => {} // require function 85 | const mod = {exports: {}} // modules 86 | 87 | func.call(null, req, mod) 88 | 89 | this.root.data[this.wxsModuleName] = mod.exports // set in root's data 90 | } 91 | 92 | /** 93 | * 获取下一个兄弟节点 94 | */ 95 | nextSibling() { 96 | return this.parent && this.parent.children[this.index + 1] 97 | } 98 | 99 | /** 100 | * 获取前一个兄弟节点 101 | */ 102 | previousSibling() { 103 | return this.parent && this.parent.children[this.index - 1] 104 | } 105 | 106 | /** 107 | * 检查 if 语句 108 | */ 109 | checkIf(data) { 110 | const statement = this.statement 111 | 112 | if (!statement.if) return true 113 | 114 | return expr.calcExpression(statement.if, data) 115 | } 116 | 117 | /** 118 | * 检查 elif 语句 119 | */ 120 | checkElif(data) { 121 | const statement = this.statement 122 | 123 | if (!statement.elif) return true 124 | 125 | return this.checkPreviousCondition(data) ? false : expr.calcExpression(statement.elif, data) 126 | } 127 | 128 | /** 129 | * 检查 else 语句 130 | */ 131 | checkElse(data) { 132 | const statement = this.statement 133 | 134 | if (!statement.else) return true 135 | 136 | return !this.checkPreviousCondition(data) 137 | } 138 | 139 | /** 140 | * 检查前一个条件语句 141 | */ 142 | checkPreviousCondition(data) { 143 | let previousSibling = this.previousSibling() 144 | 145 | while (previousSibling) { 146 | const statement = previousSibling.statement 147 | 148 | if (previousSibling.type !== CONSTANT.TYPE_IF) return false // not if node 149 | if (!statement.if && !statement.elif) return false // not have condition statement 150 | if (statement.if) return previousSibling.checkIf(data) 151 | 152 | if (statement.elif) { 153 | if (!previousSibling.checkElif(data)) { 154 | previousSibling = previousSibling.previousSibling() 155 | } else { 156 | return true 157 | } 158 | } 159 | } 160 | 161 | return false 162 | } 163 | 164 | /** 165 | * 生成虚拟树 166 | */ 167 | generate(options = {}) { 168 | const data = options.data || {} 169 | const statement = this.statement 170 | let key = options.key || '' 171 | 172 | options.data = data 173 | 174 | delete options.key // 不能跨组件传递 175 | 176 | // 检查 include 节点 177 | if (this.type === CONSTANT.TYPE_INCLUDE) { 178 | return null 179 | } 180 | 181 | // 检查 import 节点 182 | if (this.type === CONSTANT.TYPE_IMPORT) { 183 | return null 184 | } 185 | 186 | // 检查 template 节点 187 | if (this.type === CONSTANT.TYPE_TEMPLATE) { 188 | return null 189 | } 190 | 191 | // 检查 wxs 节点 192 | if (this.type === CONSTANT.TYPE_WXS) { 193 | return null 194 | } 195 | 196 | // 检查 if / elif / else 语句 197 | if (this.type === CONSTANT.TYPE_IF && (!this.checkIf(data) || !this.checkElif(data) || !this.checkElse(data))) { 198 | return null 199 | } 200 | 201 | let children = [] 202 | 203 | // 检查子节点 204 | if (this.children && this.children.length) { 205 | if (this.type === CONSTANT.TYPE_FOR) { 206 | // 检查 for 语句 207 | const list = expr.calcExpression(statement.for, data) || [] 208 | options.extra = options.extra || {} 209 | 210 | for (let i = 0, len = list.length; i < len; i++) { 211 | const {forItem: bakItem, forIndex: bakIndex} = options.extra 212 | 213 | options.extra.forItem = list[i] 214 | options.extra.forIndex = i 215 | 216 | // eslint-disable-next-line no-loop-func 217 | this.children.forEach(node => { 218 | const vt = node.generate(options) 219 | children.push(vt) 220 | }) 221 | 222 | options.extra.forItem = bakItem 223 | options.extra.forIndex = bakIndex 224 | } 225 | } else if (this.type === CONSTANT.TYPE_FORITEM) { 226 | // 检查 for 子节点 227 | options.extra = options.extra || {} 228 | const {forItem, forIndex} = options.extra 229 | const {forItem: bakItem, forIndex: bakIndex} = data 230 | data[statement.forItem] = forItem // list item 231 | data[statement.forIndex] = forIndex // list index 232 | if (statement.forKey) key = statement.forKey === '*this' ? forItem : forItem[statement.forKey] // list key 233 | 234 | children = this.children.map(node => node.generate(options)) 235 | 236 | data[statement.forItem] = bakItem 237 | data[statement.forIndex] = bakIndex 238 | } else { 239 | // 其他节点 240 | children = this.children.map(node => node.generate(options)) 241 | } 242 | } 243 | 244 | // 过滤子节点 245 | const filterChildren = [] 246 | for (const child of children) { 247 | if (!child) continue 248 | 249 | if (child.type === CONSTANT.TYPE_BLOCK) { 250 | // block 节点 251 | const grandChildren = child.children 252 | for (const grandChild of grandChildren) { 253 | filterChildren.push(grandChild) 254 | } 255 | } else { 256 | filterChildren.push(child) 257 | } 258 | } 259 | 260 | // 检查属性 261 | const attrs = [] 262 | for (const {name, value} of this.attrs) { 263 | attrs.push({ 264 | name, 265 | value: value ? expr.calcExpression(value, data) : value, 266 | }) 267 | } 268 | 269 | // 计算内容 270 | let content = expr.calcExpression(this.content, data) 271 | content = content !== undefined ? String(content) : '' 272 | 273 | return { 274 | type: this.type, 275 | tagName: this.tagName, 276 | componentId: this.componentId, 277 | content, // 文本节点的内容 278 | key, // 节点的 key,diff 用 279 | children: filterChildren, 280 | generics: this.generics, 281 | attrs, 282 | event: this.event, 283 | slotName: this.slotName, // slot 节点的 name 属性 284 | } 285 | } 286 | } 287 | 288 | module.exports = VirtualNode 289 | -------------------------------------------------------------------------------- /src/tool/constant.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 节点类型 3 | TYPE_ROOT: 10, 4 | TYPE_SLOT: 11, 5 | TYPE_TEMPLATE: 12, 6 | TYPE_BLOCK: 13, 7 | TYPE_IMPORT: 14, 8 | TYPE_INCLUDE: 15, 9 | TYPE_WXS: 16, 10 | TYPE_COMPONENT: 17, 11 | TYPE_TEXT: 18, 12 | TYPE_IF: 19, 13 | TYPE_FOR: 20, 14 | TYPE_FORITEM: 21, 15 | TYPE_NATIVE: 22, 16 | } 17 | -------------------------------------------------------------------------------- /src/tool/intersectionobserver.js: -------------------------------------------------------------------------------- 1 | const exparser = require('miniprogram-exparser') 2 | const _ = require('./utils') 3 | 4 | /** 5 | * 测量相交区域 6 | */ 7 | function measureIntersect(baseRect, newRect) { 8 | const rect = { 9 | left: baseRect.left < newRect.left ? newRect.left : baseRect.left, 10 | top: baseRect.top < newRect.top ? newRect.top : baseRect.top, 11 | right: baseRect.right > newRect.right ? newRect.right : baseRect.right, 12 | bottom: baseRect.bottom > newRect.bottom ? newRect.bottom : baseRect.bottom, 13 | width: 0, 14 | height: 0, 15 | } 16 | if (rect.right > rect.left) rect.width = rect.right - rect.left 17 | else rect.right = rect.left = rect.bottom = rect.top = 0 18 | 19 | if (rect.bottom > rect.top) rect.height = rect.bottom - rect.top 20 | else rect.right = rect.left = rect.bottom = rect.top = 0 21 | 22 | return rect 23 | } 24 | 25 | /** 26 | * 测量参照区域 27 | */ 28 | function measureRelativeRect(relatives) { 29 | const clientWidth = document.documentElement.clientWidth 30 | const clientHeight = document.documentElement.clientHeight 31 | 32 | let retRect = null 33 | for (let i = 0; i < relatives.length; i++) { 34 | const {node, margins} = relatives[i] 35 | const boundingRect = node ? node.$$.getBoundingClientRect() : { 36 | left: 0, 37 | top: 0, 38 | right: clientWidth, 39 | bottom: clientHeight, 40 | width: clientWidth, 41 | height: clientHeight 42 | } 43 | const rect = { 44 | left: boundingRect.left - margins.left, 45 | top: boundingRect.top - margins.top, 46 | right: boundingRect.right + margins.right, 47 | bottom: boundingRect.bottom + margins.bottom, 48 | } 49 | 50 | if (retRect) retRect = measureIntersect(retRect, rect) 51 | else retRect = rect 52 | } 53 | 54 | return retRect 55 | } 56 | 57 | class IntersectionObserver { 58 | constructor(compInst, options = {}) { 59 | this._exparserNode = compInst._exparserNode 60 | this._relativeInfo = [] 61 | this._options = options 62 | this._disconnected = false 63 | this._observers = [] 64 | 65 | this._exparserNode._listenInfoMap = this._exparserNode._listenInfoMap || {} // 存入监听信息 66 | } 67 | 68 | /** 69 | * 检查并更新目标节点的相交情况 70 | */ 71 | static updateTargetIntersection(listenerInfo) { 72 | const { 73 | targetNode, relatives, thresholds, minWidthOrHeight, currentRatio, callback 74 | } = listenerInfo 75 | const targetRect = targetNode.$$.getBoundingClientRect() 76 | 77 | if (targetRect.right - targetRect.left < minWidthOrHeight) { 78 | targetRect.right = targetRect.left + minWidthOrHeight 79 | targetRect.width = minWidthOrHeight 80 | } 81 | if (targetRect.bottom - targetRect.top < minWidthOrHeight) { 82 | targetRect.bottom = targetRect.top + minWidthOrHeight 83 | targetRect.height = minWidthOrHeight 84 | } 85 | 86 | const relativeRect = measureRelativeRect(relatives) 87 | const intersectRect = measureIntersect(relativeRect, targetRect) 88 | const targetArea = targetRect.width * targetRect.height 89 | const intersectRatio = targetArea ? intersectRect.width * intersectRect.height / targetArea : 0 90 | 91 | listenerInfo.currentRatio = intersectRatio 92 | 93 | let isUpdate = currentRatio === undefined 94 | if (intersectRatio !== currentRatio) { 95 | thresholds.forEach(threshold => { 96 | if (isUpdate) return 97 | if (intersectRatio <= threshold && currentRatio >= threshold) isUpdate = true 98 | else if (intersectRatio >= threshold && currentRatio <= threshold) isUpdate = true 99 | }) 100 | } 101 | 102 | if (isUpdate) { 103 | callback.call(targetNode, { 104 | id: targetNode.id, 105 | dataset: targetNode.dataset, 106 | time: Date.now(), 107 | boundingClientRect: targetRect, 108 | intersectionRatio: intersectRatio, 109 | intersectionRect: intersectRect, 110 | relativeRect, 111 | }) 112 | } 113 | } 114 | 115 | disconnect() { 116 | this._disconnected = true 117 | this._observers.forEach(observer => observer.disconnect()) 118 | this._observers = [] 119 | } 120 | 121 | observe(selector, callback) { 122 | // 获取目标节点 123 | const shadowRoot = this._exparserNode.shadowRoot 124 | let targetNodes = this._options.observeAll ? shadowRoot.querySelectorAll(selector) : shadowRoot.querySelector(selector) 125 | if (!Array.isArray(targetNodes)) targetNodes = targetNodes ? [targetNodes] : [] 126 | 127 | // 获取参照区域 128 | const relatives = [] 129 | this._relativeInfo.forEach(item => { 130 | const {selector, margins} = item 131 | const node = selector === null ? null : shadowRoot.querySelector(selector) 132 | if (selector === null || node) { 133 | relatives.push({ 134 | node, 135 | margins: { 136 | left: margins.left || 0, 137 | top: margins.top || 0, 138 | right: margins.right || 0, 139 | bottom: margins.bottom || 0, 140 | }, 141 | }) 142 | } 143 | }) 144 | 145 | targetNodes.forEach(targetNode => { 146 | const id = _.getId() 147 | const listenerInfo = { 148 | targetNode, 149 | relatives, 150 | thresholds: this._options.thresholds || [0], 151 | currentRatio: this._options.initialRatio || 0, 152 | minWidthOrHeight: 0, 153 | callback, 154 | } 155 | const observer = exparser.Observer.create(evt => { 156 | if (evt.status === 'attached') { 157 | this._exparserNode._listenInfoMap[id] = listenerInfo 158 | window.requestAnimationFrame(() => { 159 | if (!this._disconnected) IntersectionObserver.updateTargetIntersection(listenerInfo) 160 | }) 161 | } else if (evt.status === 'detached') { 162 | delete this._exparserNode._listenInfoMap[id] 163 | observer.disconnect() 164 | } 165 | }) 166 | observer.observe(targetNode, {attachStatus: true}) 167 | if (exparser.Element.isAttached(targetNode)) { 168 | this._exparserNode._listenInfoMap[id] = listenerInfo 169 | window.requestAnimationFrame(() => { 170 | if (!this._disconnected) IntersectionObserver.updateTargetIntersection(listenerInfo) 171 | }) 172 | } 173 | 174 | this._observers.push(observer) 175 | }) 176 | } 177 | 178 | relativeTo(selector, margins = {}) { 179 | this._relativeInfo.push({ 180 | selector, 181 | margins, 182 | }) 183 | return this 184 | } 185 | 186 | relativeToViewport(margins = {}) { 187 | this._relativeInfo.push({ 188 | selector: null, 189 | margins, 190 | }) 191 | return this 192 | } 193 | } 194 | 195 | module.exports = IntersectionObserver 196 | -------------------------------------------------------------------------------- /src/tool/selectorquery.js: -------------------------------------------------------------------------------- 1 | const exparser = require('miniprogram-exparser') 2 | 3 | class NodesRef { 4 | constructor(selectorQuery, exparserNode, selector, isSelectSingle) { 5 | this._selectorQuery = selectorQuery 6 | this._exparserNode = exparserNode 7 | this._selector = selector 8 | this._isSelectSingle = isSelectSingle 9 | } 10 | 11 | boundingClientRect(callback) { 12 | return this._selectorQuery._push(this._selector, this._exparserNode, this._isSelectSingle, { 13 | id: true, 14 | dataset: true, 15 | rect: true, 16 | size: true, 17 | }, callback) 18 | } 19 | 20 | scrollOffset(callback) { 21 | return this._selectorQuery._push(this._selector, this._exparserNode, this._isSelectSingle, { 22 | id: true, 23 | dataset: true, 24 | scrollOffset: true, 25 | }, callback) 26 | } 27 | 28 | context(callback) { 29 | return this._selectorQuery._push(this._selector, this._exparserNode, this._isSelectSingle, { 30 | context: true, 31 | }, callback) 32 | } 33 | 34 | fields(fields, callback) { 35 | return this._selectorQuery._push(this._selector, this._exparserNode, this._isSelectSingle, fields, callback) 36 | } 37 | } 38 | 39 | class SelectorQuery { 40 | constructor(compInst) { 41 | this._exparserNode = compInst && compInst._exparserNode || null 42 | this._queue = [] 43 | this._queueCallback = [] 44 | } 45 | 46 | _push(selector, exparserNode, isSelectSingle, fields, callback) { 47 | this._queue.push({ 48 | selector, 49 | exparserNode, 50 | isSelectSingle, 51 | fields, 52 | }) 53 | this._queueCallback.push(callback || null) 54 | 55 | return this 56 | } 57 | 58 | in(compInst) { 59 | if (!compInst || typeof compInst !== 'object') { 60 | throw new Error('invalid params') 61 | } 62 | 63 | this._exparserNode = compInst._exparserNode 64 | 65 | return this 66 | } 67 | 68 | select(selector) { 69 | return new NodesRef(this, this._exparserNode, selector, true) 70 | } 71 | 72 | selectAll(selector) { 73 | return new NodesRef(this, this._exparserNode, selector, false) 74 | } 75 | 76 | selectViewport() { 77 | return new NodesRef(this, 0, '', false) 78 | } 79 | 80 | exec(callback) { 81 | Promise.resolve().then(() => { 82 | const res = [] 83 | 84 | this._queue.forEach((item, index) => { 85 | const { 86 | selector, exparserNode, isSelectSingle, fields 87 | } = item 88 | 89 | if (exparserNode === 0) { 90 | const itemRes = {} 91 | 92 | if (fields.id) { 93 | itemRes.id = '' 94 | } 95 | if (fields.dataset) { 96 | itemRes.dataset = {} 97 | } 98 | if (fields.rect) { 99 | itemRes.left = 0 100 | itemRes.right = 0 101 | itemRes.top = 0 102 | itemRes.bottom = 0 103 | } 104 | if (fields.size) { 105 | itemRes.width = document.documentElement.clientWidth 106 | itemRes.height = document.documentElement.clientHeight 107 | } 108 | if (fields.scrollOffset) { 109 | itemRes.scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft || 0 110 | itemRes.scrollTop = document.documentElement.scrollTop || document.body.scrollTop || 0 111 | } 112 | 113 | res.push(itemRes) 114 | } else { 115 | const shadowRoot = exparserNode.shadowRoot 116 | const nodes = isSelectSingle ? [shadowRoot.querySelector(selector)] : shadowRoot.querySelectorAll(selector) 117 | const itemResList = [] 118 | 119 | for (const node of nodes) { 120 | if (!node) continue 121 | const itemRes = {} 122 | 123 | if (fields.id) { 124 | itemRes.id = node.id || '' 125 | } 126 | if (fields.dataset) { 127 | itemRes.dataset = Object.assign({}, node.dataset || {}) 128 | } 129 | if (fields.rect || fields.size) { 130 | const rect = node.$$.getBoundingClientRect() 131 | 132 | if (fields.rect) { 133 | itemRes.left = rect.left 134 | itemRes.right = rect.right 135 | itemRes.top = rect.top 136 | itemRes.bottom = rect.bottom 137 | } 138 | if (fields.size) { 139 | itemRes.width = rect.width 140 | itemRes.height = rect.height 141 | } 142 | } 143 | if (fields.properties) { 144 | fields.properties.forEach(name => { 145 | name = name.replace(/-([a-z])/g, (all, $1) => $1.toUpperCase()) 146 | 147 | if (exparser.Component.hasPublicProperty(node, name)) { 148 | itemRes[name] = node.data[name] 149 | } 150 | }) 151 | } 152 | if (fields.scrollOffset) { 153 | itemRes.scrollLeft = node.$$.scrollLeft || 0 154 | itemRes.scrollTop = node.$$.scrollTop || 0 155 | } 156 | if (fields.computedStyle && fields.computedStyle.length) { 157 | const style = window.getComputedStyle(node.$$) 158 | 159 | fields.computedStyle.forEach(key => { 160 | if (key && style[key] !== undefined) itemRes[key] = style[key] 161 | }) 162 | } 163 | if (fields.context) { 164 | itemRes.context = {} // TODO 165 | } 166 | 167 | itemResList.push(itemRes) 168 | } 169 | 170 | res.push(isSelectSingle ? (itemResList[0] || null) : itemResList) 171 | } 172 | 173 | if (typeof this._queueCallback[index] === 'function') this._queueCallback[index].call(this, res[index]) 174 | }) 175 | 176 | if (typeof callback === 'function') callback.call(this, res) 177 | 178 | // reset 179 | this._queue = [] 180 | this._queueCallback = [] 181 | }).catch(console.error) 182 | } 183 | } 184 | 185 | module.exports = SelectorQuery 186 | -------------------------------------------------------------------------------- /src/tool/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取随机 id,生成 15 位 3 | */ 4 | let seed = 1e14 + Math.floor(Math.random() * 9e14) 5 | const charString = 'abcdefghij' 6 | function getId(notNumber) { 7 | const id = ++seed 8 | return notNumber ? id.toString().split('').map(item => charString[+item]).join('') : id 9 | } 10 | 11 | /** 12 | * 复制对象 13 | */ 14 | function copy(src) { 15 | if (typeof src === 'object' && src !== null) { 16 | let dest 17 | 18 | if (Array.isArray(src)) { 19 | dest = src.map(item => copy(item)) 20 | } else { 21 | dest = {} 22 | Object.keys(src).forEach(key => dest[key] = copy(src[key])) 23 | } 24 | 25 | return dest 26 | } 27 | 28 | if (typeof src === 'symbol') return undefined 29 | return src 30 | } 31 | 32 | /** 33 | * 判断是否是 html 标签 34 | */ 35 | const tags = ['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'dfn', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 'rb', 'rp', 'rt', 'rtc', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr'] 36 | function isHtmlTag(tagName) { 37 | return tags.indexOf(tagName) >= 0 38 | } 39 | 40 | /** 41 | * 判断是否是小程序内置组件 42 | */ 43 | const officialTags = [ 44 | 'view', 'scroll-view', 'swiper', 'movable-view', 'cover-view', 'cover-view', 45 | 'icon', 'text', 'rich-text', 'progress', 46 | 'button', 'checkbox', 'form', 'input', 'label', 'picker', 'picker', 'picker-view', 'radio', 'slider', 'switch', 'textarea', 47 | 'navigator', 'function-page-navigator', 48 | 'audio', 'image', 'video', 'camera', 'live-player', 'live-pusher', 49 | 'map', 50 | 'canvas', 51 | 'open-data', 'web-view', 'ad' 52 | ] 53 | function isOfficialTag(tagName) { 54 | return officialTags.indexOf(tagName) >= 0 || officialTags.indexOf(`wx-${tagName}`) >= 0 55 | } 56 | 57 | /** 58 | * 转换 rpx 单位为 px 单位 59 | */ 60 | function transformRpx(style) { 61 | return style.replace(/(\d+)rpx/ig, '$1px') 62 | } 63 | 64 | /** 65 | * 转换连字符为驼峰 66 | */ 67 | function dashToCamelCase(dash) { 68 | return dash.replace(/-[a-z]/g, s => s[1].toUpperCase()) 69 | } 70 | 71 | /** 72 | * 转换驼峰为连字符 73 | */ 74 | function camelToDashCase(camel) { 75 | return camel.replace(/([A-Z])/g, '-$1').toLowerCase() 76 | } 77 | 78 | /** 79 | * 转换动画对象为样式 80 | */ 81 | function animationToStyle({animates, option = {}}) { 82 | const {transformOrigin, transition} = option 83 | 84 | if (transition === undefined || animates === undefined) { 85 | return { 86 | transformOrigin: '', 87 | transform: '', 88 | transition: '', 89 | } 90 | } 91 | 92 | const addPx = value => (typeof value === 'number' ? value + 'px' : value) 93 | const transform = animates.filter(({type}) => type !== 'style').map(({type, args}) => { 94 | switch (type) { 95 | case 'matrix': 96 | return `matrix(${args.join(',')})` 97 | case 'matrix3d': 98 | return `matrix3d(${args.join(',')})` 99 | 100 | case 'rotate': 101 | return `rotate(${args[0]}deg)` 102 | case 'rotate3d': 103 | args[3] += 'deg' 104 | return `rotate3d(${args.join(',')})` 105 | case 'rotateX': 106 | return `rotateX(${args[0]}deg)` 107 | case 'rotateY': 108 | return `rotateY(${args[0]}deg)` 109 | case 'rotateZ': 110 | return `rotateZ(${args[0]}deg)` 111 | 112 | case 'scale': 113 | return `scale(${args.join(',')})` 114 | case 'scale3d': 115 | return `scale3d(${args.join(',')})` 116 | case 'scaleX': 117 | return `scaleX(${args[0]})` 118 | case 'scaleY': 119 | return `scaleY(${args[0]})` 120 | case 'scaleZ': 121 | return `scaleZ(${args[0]})` 122 | 123 | case 'translate': 124 | return `translate(${args.map(addPx).join(',')})` 125 | case 'translate3d': 126 | return `translate3d(${args.map(addPx).join(',')})` 127 | case 'translateX': 128 | return `translateX(${addPx(args[0])})` 129 | case 'translateY': 130 | return `translateY(${addPx(args[0])})` 131 | case 'translateZ': 132 | return `translateZ(${addPx(args[0])})` 133 | 134 | case 'skew': 135 | return `skew(${args.map(value => value + 'deg').join(',')})` 136 | case 'skewX': 137 | return `skewX(${args[0]}deg)` 138 | case 'skewY': 139 | return `skewY(${args[0]}deg)` 140 | default: 141 | return '' 142 | } 143 | }).join(' ') 144 | const style = animates.filter(({type}) => type === 'style').reduce((previous, current) => { 145 | previous[current.args[0]] = current.args[1] 146 | return previous 147 | }, {}) 148 | 149 | return { 150 | style, 151 | transformOrigin, 152 | transform, 153 | transitionProperty: ['transform', ...Object.keys(style)].join(','), 154 | transition: `${transition.duration}ms ${transition.timingFunction} ${transition.delay}ms`, 155 | } 156 | } 157 | 158 | /** 159 | * 调整 exparser 的定义对象 160 | */ 161 | function adjustExparserDefinition(definition) { 162 | // 调整 properties 163 | const properties = definition.properties || {} 164 | Object.keys(properties).forEach(key => { 165 | const value = properties[key] 166 | if (value === null) { 167 | properties[key] = {type: null} 168 | } else if (value === Number || value === String || value === Boolean || value === Object || value === Array) { 169 | properties[key] = {type: value} 170 | } else if (value.public === undefined || value.public) { 171 | properties[key] = { 172 | type: value.type === null ? null : value.type, 173 | value: value.value, 174 | observer: value.observer, 175 | } 176 | } 177 | }) 178 | 179 | return definition 180 | } 181 | 182 | /** 183 | * 存入标签名 184 | */ 185 | const idTagNameMap = {} 186 | function setTagName(id, tagName) { 187 | idTagNameMap[id] = tagName 188 | } 189 | 190 | /** 191 | * 根据 id 获取标签名 192 | */ 193 | function getTagName(id) { 194 | return idTagNameMap[id] 195 | } 196 | 197 | /** 198 | * 缓存 componentManager 实例 199 | */ 200 | const CACHE = {} 201 | function cache(id, instance) { 202 | if (instance) { 203 | // 存入缓存 204 | CACHE[id] = instance 205 | } else { 206 | // 取缓存 207 | return CACHE[id] 208 | } 209 | } 210 | 211 | /** 212 | * 解析事件语法 213 | */ 214 | function parseEvent(name, value) { 215 | const res = /^(capture-)?(mut-)?(bind|catch|)(?::)?(.*)$/ig.exec(name) 216 | 217 | if (res[3] && res[4]) { 218 | // 事件绑定 219 | const isCapture = !!res[1] 220 | const isMutated = !!res[2] 221 | const isCatch = res[3] === 'catch' 222 | const eventName = res[4] 223 | 224 | return { 225 | name: eventName, 226 | isMutated, 227 | isCapture, 228 | isCatch, 229 | handler: value, 230 | } 231 | } 232 | } 233 | 234 | /** 235 | * 标准化文件绝对路径 236 | */ 237 | function normalizeAbsolute(absolutePath) { 238 | if (!absolutePath) return null 239 | 240 | absolutePath = absolutePath.replace(/\\/g, '/') 241 | return absolutePath.split('/').filter(item => !!item).join('/') 242 | } 243 | 244 | /** 245 | * 文件相对路径转绝对路径,其中 basePath 路径必须是文件路径 246 | */ 247 | function relativeToAbsolute(basePath, relativePath) { 248 | let baseDirPath = normalizeAbsolute(basePath).split('/') 249 | baseDirPath.pop() 250 | baseDirPath = baseDirPath.join('/') 251 | 252 | const pathList = [] 253 | normalizeAbsolute(`${baseDirPath}/${relativePath}`).split('/').forEach(item => { 254 | if (item === '..') { 255 | pathList.pop() 256 | } else if (item !== '.') { 257 | pathList.push(item) 258 | } 259 | }) 260 | 261 | return pathList.join('/') 262 | } 263 | 264 | /** 265 | * 获取 exparser 节点对应的 dom 节点 266 | */ 267 | function getDom(exparserNode) { 268 | let dom = exparserNode.$$ 269 | if (!dom) { 270 | dom = document.createElement('virtual') 271 | const fragment = document.createDocumentFragment() 272 | const shadowRoot = exparserNode.shadowRoot 273 | const childNodes = shadowRoot && shadowRoot.childNodes 274 | if (childNodes && childNodes.length) { 275 | childNodes.forEach(child => fragment.appendChild(getDom(child))) 276 | } 277 | dom.appendChild(fragment) 278 | } 279 | 280 | return dom 281 | } 282 | 283 | module.exports = { 284 | getId, 285 | copy, 286 | isHtmlTag, 287 | isOfficialTag, 288 | transformRpx, 289 | dashToCamelCase, 290 | camelToDashCase, 291 | animationToStyle, 292 | adjustExparserDefinition, 293 | setTagName, 294 | getTagName, 295 | cache, 296 | parseEvent, 297 | normalizeAbsolute, 298 | relativeToAbsolute, 299 | getDom, 300 | } 301 | -------------------------------------------------------------------------------- /test/__snapshots__/component.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`toJSON 1`] = ` 4 | Object { 5 | "attrs": Array [], 6 | "children": Array [ 7 | Object { 8 | "attrs": Array [ 9 | Object { 10 | "name": "data-index", 11 | "value": false, 12 | }, 13 | ], 14 | "children": Array [], 15 | "event": Object {}, 16 | "tagName": "view", 17 | }, 18 | Object { 19 | "attrs": Array [], 20 | "children": Array [ 21 | Object { 22 | "attrs": Array [ 23 | Object { 24 | "name": "class", 25 | "value": "child", 26 | }, 27 | ], 28 | "children": Array [ 29 | "1", 30 | ], 31 | "event": Object {}, 32 | "tagName": "view", 33 | }, 34 | ], 35 | "event": Object {}, 36 | "tagName": "child", 37 | }, 38 | Object { 39 | "attrs": Array [], 40 | "children": Array [ 41 | Object { 42 | "attrs": Array [ 43 | Object { 44 | "name": "class", 45 | "value": "child", 46 | }, 47 | ], 48 | "children": Array [ 49 | "2", 50 | ], 51 | "event": Object {}, 52 | "tagName": "view", 53 | }, 54 | ], 55 | "event": Object {}, 56 | "tagName": "child", 57 | }, 58 | Object { 59 | "attrs": Array [], 60 | "children": Array [ 61 | Object { 62 | "attrs": Array [ 63 | Object { 64 | "name": "class", 65 | "value": "child", 66 | }, 67 | ], 68 | "children": Array [ 69 | "3", 70 | ], 71 | "event": Object {}, 72 | "tagName": "view", 73 | }, 74 | ], 75 | "event": Object {}, 76 | "tagName": "child", 77 | }, 78 | Object { 79 | "attrs": Array [], 80 | "children": Array [ 81 | Object { 82 | "attrs": Array [ 83 | Object { 84 | "name": "class", 85 | "value": "child", 86 | }, 87 | ], 88 | "children": Array [], 89 | "event": Object {}, 90 | "tagName": "view", 91 | }, 92 | ], 93 | "event": Object { 94 | "longtap": Object { 95 | "handler": "onCaptureMutatedLongTap", 96 | "id": 0, 97 | "isCapture": true, 98 | "isCatch": false, 99 | "isMutated": true, 100 | "name": "longtap", 101 | }, 102 | "tap": Object { 103 | "handler": "onTap", 104 | "id": 0, 105 | "isCapture": false, 106 | "isCatch": false, 107 | "isMutated": false, 108 | "name": "tap", 109 | }, 110 | "touchend": Object { 111 | "handler": "onMutatedtouchend", 112 | "id": 0, 113 | "isCapture": false, 114 | "isCatch": true, 115 | "isMutated": true, 116 | "name": "touchend", 117 | }, 118 | "touchmove": Object { 119 | "handler": "onCaptureTouchMove", 120 | "id": 0, 121 | "isCapture": true, 122 | "isCatch": false, 123 | "isMutated": false, 124 | "name": "touchmove", 125 | }, 126 | "touchstart": Object { 127 | "handler": "onCatchTouchStart", 128 | "id": 0, 129 | "isCapture": false, 130 | "isCatch": true, 131 | "isMutated": false, 132 | "name": "touchstart", 133 | }, 134 | }, 135 | "tagName": "child", 136 | }, 137 | ], 138 | "event": Object {}, 139 | "tagName": "main", 140 | } 141 | `; 142 | 143 | exports[`toJSON 2`] = ` 144 | Object { 145 | "attrs": Array [], 146 | "children": Array [ 147 | Object { 148 | "attrs": Array [ 149 | Object { 150 | "name": "data-index", 151 | "value": true, 152 | }, 153 | ], 154 | "children": Array [], 155 | "event": Object {}, 156 | "tagName": "view", 157 | }, 158 | ], 159 | "event": Object {}, 160 | "tagName": "main", 161 | } 162 | `; 163 | 164 | exports[`virtual host 1`] = ` 165 | Object { 166 | "attrs": Array [], 167 | "children": Array [ 168 | Object { 169 | "attrs": Array [], 170 | "children": Array [ 171 | Object { 172 | "attrs": Array [], 173 | "children": Array [ 174 | "else", 175 | ], 176 | "event": Object {}, 177 | "tagName": "div", 178 | }, 179 | ], 180 | "event": Object {}, 181 | "tagName": "wx-view", 182 | }, 183 | Object { 184 | "attrs": Array [ 185 | Object { 186 | "name": "class", 187 | "value": "item", 188 | }, 189 | ], 190 | "children": Array [ 191 | Object { 192 | "attrs": Array [], 193 | "children": Array [ 194 | "0-1-a", 195 | ], 196 | "event": Object {}, 197 | "tagName": "div", 198 | }, 199 | ], 200 | "event": Object {}, 201 | "tagName": "wx-view", 202 | }, 203 | Object { 204 | "attrs": Array [ 205 | Object { 206 | "name": "class", 207 | "value": "item", 208 | }, 209 | ], 210 | "children": Array [ 211 | Object { 212 | "attrs": Array [], 213 | "children": Array [ 214 | "1-2-a", 215 | ], 216 | "event": Object {}, 217 | "tagName": "div", 218 | }, 219 | ], 220 | "event": Object {}, 221 | "tagName": "wx-view", 222 | }, 223 | Object { 224 | "attrs": Array [], 225 | "children": Array [ 226 | "0", 227 | ], 228 | "event": Object {}, 229 | "tagName": "span", 230 | }, 231 | ], 232 | "event": Object {}, 233 | "tagName": "main", 234 | } 235 | `; 236 | 237 | exports[`virtual host 2`] = ` 238 | Object { 239 | "attrs": Array [], 240 | "children": Array [ 241 | Object { 242 | "attrs": Array [], 243 | "children": Array [ 244 | Object { 245 | "attrs": Array [], 246 | "children": Array [ 247 | "if", 248 | ], 249 | "event": Object {}, 250 | "tagName": "div", 251 | }, 252 | ], 253 | "event": Object {}, 254 | "tagName": "wx-view", 255 | }, 256 | Object { 257 | "attrs": Array [ 258 | Object { 259 | "name": "class", 260 | "value": "item", 261 | }, 262 | ], 263 | "children": Array [ 264 | Object { 265 | "attrs": Array [], 266 | "children": Array [ 267 | "0-1-a", 268 | ], 269 | "event": Object {}, 270 | "tagName": "div", 271 | }, 272 | ], 273 | "event": Object {}, 274 | "tagName": "wx-view", 275 | }, 276 | Object { 277 | "attrs": Array [ 278 | Object { 279 | "name": "class", 280 | "value": "item", 281 | }, 282 | ], 283 | "children": Array [ 284 | Object { 285 | "attrs": Array [], 286 | "children": Array [ 287 | "1-2-a", 288 | ], 289 | "event": Object {}, 290 | "tagName": "div", 291 | }, 292 | ], 293 | "event": Object {}, 294 | "tagName": "wx-view", 295 | }, 296 | Object { 297 | "attrs": Array [ 298 | Object { 299 | "name": "class", 300 | "value": "item", 301 | }, 302 | ], 303 | "children": Array [ 304 | Object { 305 | "attrs": Array [], 306 | "children": Array [ 307 | "2-3-a", 308 | ], 309 | "event": Object {}, 310 | "tagName": "div", 311 | }, 312 | ], 313 | "event": Object {}, 314 | "tagName": "wx-view", 315 | }, 316 | Object { 317 | "attrs": Array [], 318 | "children": Array [ 319 | "1", 320 | ], 321 | "event": Object {}, 322 | "tagName": "span", 323 | }, 324 | ], 325 | "event": Object {}, 326 | "tagName": "main", 327 | } 328 | `; 329 | 330 | exports[`virtual host 3`] = ` 331 | Object { 332 | "attrs": Array [], 333 | "children": Array [ 334 | Object { 335 | "attrs": Array [], 336 | "children": Array [ 337 | Object { 338 | "attrs": Array [], 339 | "children": Array [ 340 | "if", 341 | ], 342 | "event": Object {}, 343 | "tagName": "div", 344 | }, 345 | ], 346 | "event": Object {}, 347 | "tagName": "wx-view", 348 | }, 349 | Object { 350 | "attrs": Array [ 351 | Object { 352 | "name": "class", 353 | "value": "item", 354 | }, 355 | ], 356 | "children": Array [ 357 | Object { 358 | "attrs": Array [], 359 | "children": Array [ 360 | "0-1-a", 361 | ], 362 | "event": Object {}, 363 | "tagName": "div", 364 | }, 365 | ], 366 | "event": Object {}, 367 | "tagName": "wx-view", 368 | }, 369 | Object { 370 | "attrs": Array [ 371 | Object { 372 | "name": "class", 373 | "value": "item", 374 | }, 375 | ], 376 | "children": Array [ 377 | Object { 378 | "attrs": Array [], 379 | "children": Array [ 380 | "1-2-a", 381 | ], 382 | "event": Object {}, 383 | "tagName": "div", 384 | }, 385 | ], 386 | "event": Object {}, 387 | "tagName": "wx-view", 388 | }, 389 | Object { 390 | "attrs": Array [ 391 | Object { 392 | "name": "class", 393 | "value": "item", 394 | }, 395 | ], 396 | "children": Array [ 397 | Object { 398 | "attrs": Array [], 399 | "children": Array [ 400 | "2-3-a", 401 | ], 402 | "event": Object {}, 403 | "tagName": "div", 404 | }, 405 | ], 406 | "event": Object {}, 407 | "tagName": "wx-view", 408 | }, 409 | Object { 410 | "attrs": Array [], 411 | "children": Array [ 412 | "1", 413 | ], 414 | "event": Object {}, 415 | "tagName": "span", 416 | }, 417 | ], 418 | "event": Object {}, 419 | "tagName": "main", 420 | } 421 | `; 422 | -------------------------------------------------------------------------------- /test/component.test.js: -------------------------------------------------------------------------------- 1 | const jComponent = require('../src/index') 2 | const _ = require('./utils') 3 | const {getId} = require('../src/tool/utils') 4 | 5 | beforeAll(() => { 6 | _.env() 7 | }) 8 | 9 | test('register behavior', () => { 10 | const behavior = jComponent.behavior({}) 11 | 12 | expect(behavior.length).toBe(15) 13 | }) 14 | 15 | test('register and create global component', () => { 16 | const id = jComponent.register({ 17 | id: 'view', 18 | tagName: 'wx-view', 19 | template: '
' 20 | }) 21 | const view = jComponent.create(id) 22 | 23 | expect(id).toBe('view') 24 | expect(view.dom.tagName).toBe('WX-VIEW') 25 | expect(view.dom.innerHTML).toBe('
') 26 | }) 27 | 28 | test('register and create normal component', () => { 29 | const id = jComponent.register({ 30 | template: '{{index + \'-\' + item}}{{a}}', 31 | properties: { 32 | list: { 33 | type: Array, 34 | public: true, 35 | value: [], 36 | observer(newVal, oldVal) { 37 | this.setData({ 38 | observerArr1: [newVal, oldVal], 39 | }) 40 | }, 41 | }, 42 | a: { 43 | type: String, 44 | public: true, 45 | value: '', 46 | observer(newVal, oldVal) { 47 | this.setData({ 48 | observerArr2: [newVal, oldVal], 49 | }) 50 | }, 51 | }, 52 | }, 53 | }) 54 | const comp = jComponent.create(id, {list: ['a', 'b'], a: 'test'}) 55 | 56 | expect(id.length).toBe(15) 57 | expect(comp.dom.tagName.length).toBe(15) 58 | expect(comp.dom.innerHTML).toBe('
0-a
1-b
test') 59 | expect(comp.instance.data.observerArr1).toEqual([['a', 'b'], []]) 60 | expect(comp.instance.data.observerArr2).toEqual(['test', '']) 61 | }) 62 | 63 | test('register component with default form behavior', () => { 64 | const id = jComponent.register({ 65 | id: 'wx-input', 66 | tagName: 'wx-input', 67 | template: '', 68 | behaviors: ['wx://form-field'], 69 | properties: {} 70 | }) 71 | const comp = jComponent.create(id, {name: 'idcard', value: '123456'}) 72 | 73 | expect(id).toBe('wx-input') 74 | expect(comp.instance.data.name).toEqual('idcard') 75 | expect(comp.instance.data.value).toEqual('123456') 76 | 77 | jComponent.register({ 78 | id: 'wx-checkbox-group', 79 | tagName: 'wx-checkbox-group', 80 | template: '
', 81 | behaviors: ['wx://form-field-group'], 82 | properties: {} 83 | }) 84 | }) 85 | 86 | test('instance', () => { 87 | const data = { 88 | a: 1, 89 | c: 3, 90 | } 91 | const func = (a, b) => a + b 92 | let that = null 93 | const comp = jComponent.create(jComponent.register({ 94 | template: '123', 95 | data, 96 | attached() { 97 | that = this 98 | }, 99 | methods: { 100 | sum: func, 101 | } 102 | })) 103 | 104 | const parent = document.createElement('div') 105 | comp.attach(parent) 106 | expect(comp.instance).toBe(that) 107 | expect(comp.instance.data).toEqual(data) 108 | expect(comp.instance.sum(95, 27)).toBe(122) 109 | }) 110 | 111 | test('querySelector', () => { 112 | let observerArr = [] 113 | const compaId = jComponent.register({ 114 | template: '{{index + \'-\' + item}}', 115 | properties: { 116 | list: { 117 | type: Array, 118 | value: [], 119 | observer(newVal, oldVal) { 120 | observerArr = [newVal, oldVal] 121 | } 122 | } 123 | }, 124 | methods: { 125 | testMethod() { 126 | return 'compa' 127 | }, 128 | }, 129 | }) 130 | const comp = jComponent.create(jComponent.register({ 131 | tagName: 'compb', 132 | template: ` 133 | {{prop}} 134 | {{index}} 135 | if 136 | elif 137 | else 138 | {{index}} 139 | `, 140 | usingComponents: { 141 | compa: compaId, 142 | }, 143 | properties: { 144 | prop: { 145 | type: String, 146 | value: '', 147 | } 148 | }, 149 | data: { 150 | index: 0, 151 | styleObject: { 152 | style: 'color: green;', 153 | }, 154 | list: [1, 2], 155 | }, 156 | }), {prop: 'prop-value'}) 157 | 158 | expect(comp.dom.tagName).toBe('COMPB') 159 | expect(comp.dom.innerHTML).toBe('
prop-value
0
elif
0-1
1-2
0
') 160 | 161 | const node1List = comp.querySelectorAll('.a') 162 | expect(node1List.length).toBe(2) 163 | const node1 = node1List[0] 164 | expect(node1.dom.tagName).toBe('WX-VIEW') 165 | expect(node1.dom.innerHTML).toBe('
0
') 166 | 167 | const node2 = comp.querySelector('#compa') 168 | expect(node2.dom.tagName).toBe('COMPA') 169 | expect(node2.dom.innerHTML).toBe('
0-1
1-2
0') 170 | expect(node2.instance.testMethod).toBeInstanceOf(Function) 171 | expect(node2.instance.testMethod()).toBe('compa') 172 | 173 | const firstItem = node2.querySelector('.item') 174 | const items = node2.querySelectorAll('.item') 175 | expect(firstItem.dom.innerHTML).toBe(items[0].dom.innerHTML) 176 | expect(items[0].dom.innerHTML).toBe('
0-1
') 177 | expect(items[1].dom.innerHTML).toBe('
1-2
') 178 | 179 | comp.setData({ 180 | list: [1, 2, 3], 181 | }) 182 | expect(observerArr).toEqual([[1, 2, 3], [1, 2]]) 183 | }) 184 | 185 | test('dispatchEvent/addEventListener/removeEventListener', async () => { 186 | const eventList = [] 187 | let blurCount = 0 188 | let touchStartCount = 0 189 | let touchMoveCount = 0 190 | let touchEndCount = 0 191 | let touchCancelCount = 0 192 | let longPressCount = 0 193 | const behavior = jComponent.behavior({ 194 | methods: { 195 | onTap1() { 196 | eventList.push(this.data.index) 197 | }, 198 | } 199 | }) 200 | const compaId = jComponent.register({ 201 | template: '{{index + \'-\' + item}}', 202 | properties: { 203 | list: { 204 | type: Array, 205 | value: [], 206 | } 207 | }, 208 | methods: { 209 | triggerCustomA() { 210 | this.triggerEvent('customa', { 211 | index: 998, 212 | }) 213 | } 214 | }, 215 | }) 216 | const comp = jComponent.create(jComponent.register({ 217 | template: ` 218 | {{index}} 219 | if 220 | elif 221 | else 222 | {{index}} 223 | `, 224 | usingComponents: { 225 | compa: compaId, 226 | }, 227 | behaviors: [behavior], 228 | data: { 229 | index: 0, 230 | styleObject: { 231 | style: 'color: green;', 232 | }, 233 | list: [1, 2], 234 | }, 235 | methods: { 236 | onBlur() { 237 | blurCount++ 238 | }, 239 | onTap2(e) { 240 | expect(e.detail.userInfo.nickName).toBe('hello') 241 | this.setData({ 242 | index: ++this.data.index, 243 | 'styleObject.style': 'color: red;', 244 | list: [2, 3, 4], 245 | }) 246 | }, 247 | onLongPress() { 248 | longPressCount++ 249 | }, 250 | onTouchStart() { 251 | touchStartCount++ 252 | }, 253 | onTouchMove() { 254 | touchMoveCount++ 255 | }, 256 | onTouchEnd() { 257 | touchEndCount++ 258 | }, 259 | onTouchCancel() { 260 | touchCancelCount++ 261 | }, 262 | onCustomA(evt) { 263 | this.setData({ 264 | index: evt.detail.index, 265 | }) 266 | }, 267 | } 268 | })) 269 | 270 | const node1 = comp.querySelector('.a') 271 | node1.dispatchEvent('tap') 272 | await _.sleep(10) 273 | expect(eventList).toEqual([0]) 274 | 275 | // 触发 tap 276 | node1.dispatchEvent('touchstart') 277 | node1.dispatchEvent('touchend') 278 | await _.sleep(10) 279 | expect(eventList).toEqual([0, 0]) 280 | expect(touchStartCount).toBe(1) 281 | expect(touchEndCount).toBe(1) 282 | 283 | // touchmove 284 | node1.dispatchEvent('touchstart') 285 | node1.dispatchEvent('touchmove', {touches: [{x: 5, y: 5}]}) 286 | node1.dispatchEvent('touchmove', {touches: [{x: 10, y: 10}]}) 287 | node1.dispatchEvent('touchend') 288 | await _.sleep(10) 289 | expect(eventList).toEqual([0, 0]) 290 | expect(touchStartCount).toBe(2) 291 | expect(touchMoveCount).toBe(2) 292 | expect(touchEndCount).toBe(2) 293 | 294 | // 滚动后在保护时间内触发 touch 事件不触发 tap 295 | node1.dispatchEvent('scroll') 296 | node1.dispatchEvent('touchstart') 297 | node1.dispatchEvent('touchend') 298 | await _.sleep(10) 299 | expect(eventList).toEqual([0, 0]) 300 | expect(touchStartCount).toBe(3) 301 | expect(touchEndCount).toBe(3) 302 | 303 | // 滚动后超过保护时间,触发 tap 304 | node1.dispatchEvent('scroll') 305 | await _.sleep(200) 306 | node1.dispatchEvent('touchstart') 307 | node1.dispatchEvent('touchend') 308 | await _.sleep(10) 309 | expect(eventList).toEqual([0, 0, 0]) 310 | expect(touchStartCount).toBe(4) 311 | expect(touchEndCount).toBe(4) 312 | 313 | // longpress 314 | node1.dispatchEvent('touchstart') 315 | await _.sleep(400) 316 | node1.dispatchEvent('touchend') 317 | await _.sleep(10) 318 | expect(eventList).toEqual([0, 0, 0]) 319 | expect(touchStartCount).toBe(5) 320 | expect(touchEndCount).toBe(5) 321 | expect(longPressCount).toBe(1) 322 | 323 | // touchcancel 324 | node1.dispatchEvent('touchstart') 325 | node1.dispatchEvent('touchcancel') 326 | await _.sleep(10) 327 | expect(touchStartCount).toBe(6) 328 | expect(touchCancelCount).toBe(1) 329 | 330 | // blur 331 | node1.dispatchEvent('blur') 332 | await _.sleep(10) 333 | expect(blurCount).toBe(1) 334 | node1.dispatchEvent('touchstart') 335 | node1.dispatchEvent('blur') 336 | node1.dispatchEvent('touchcancel') 337 | await _.sleep(10) 338 | expect(touchStartCount).toBe(7) 339 | expect(touchCancelCount).toBe(2) 340 | expect(blurCount).toBe(2) 341 | 342 | // touchcancel 后触发其他触摸事件 343 | node1.dispatchEvent('touchstart') 344 | node1.dispatchEvent('touchcancel') 345 | node1.dispatchEvent('touchmove') 346 | node1.dispatchEvent('touchend') 347 | node1.dispatchEvent('touchcancel') 348 | await _.sleep(10) 349 | expect(touchStartCount).toBe(8) 350 | expect(touchMoveCount).toBe(3) 351 | expect(touchEndCount).toBe(6) 352 | expect(touchCancelCount).toBe(4) 353 | 354 | // 多指触摸 355 | node1.dispatchEvent('touchstart', {touches: [{x: 1, y: 1}, {x: 2, y: 2}]}) 356 | node1.dispatchEvent('touchstart', {touches: [{x: 3, y: 3}]}) 357 | node1.dispatchEvent('touchmove', {touches: [{x: 5, y: 5}, {x: 10, y: 10}]}) 358 | node1.dispatchEvent('touchend', {touches: [{x: 5, y: 5}]}) 359 | await _.sleep(10) 360 | expect(eventList).toEqual([0, 0, 0]) 361 | expect(touchStartCount).toBe(10) 362 | expect(touchMoveCount).toBe(4) 363 | expect(touchEndCount).toBe(7) 364 | expect(touchCancelCount).toBe(4) 365 | expect(longPressCount).toBe(1) 366 | 367 | const node2 = comp.querySelector('#compa') 368 | node2.dispatchEvent('tap', {detail: {userInfo: {nickName: 'hello'}}}) 369 | await _.sleep(10) 370 | expect(comp.dom.innerHTML).toBe('
1
if
0-2
1-3
2-4
1
') 371 | 372 | // 自组件事件 373 | node2.instance.triggerCustomA() 374 | await _.sleep(10) 375 | expect(comp.dom.innerHTML).toBe('
998
if
0-2
1-3
2-4
998
') 376 | 377 | node2.instance.triggerEvent('customa', {index: 999}) 378 | await _.sleep(10) 379 | expect(comp.dom.innerHTML).toBe('
999
if
0-2
1-3
2-4
999
') 380 | 381 | node2.dispatchEvent('customa', {detail: {index: 990}}) 382 | await _.sleep(10) 383 | expect(comp.dom.innerHTML).toBe('
990
if
0-2
1-3
2-4
990
') 384 | 385 | // 其他自定义事件 386 | let event = null 387 | comp.dom.addEventListener('test', evt => { 388 | event = evt 389 | }) 390 | comp.dispatchEvent('test') 391 | await _.sleep(10) 392 | expect(event.type).toBe('test') 393 | 394 | // 组件事件外部监听 395 | const outerEventList = [] 396 | const onOuterEventListener1 = evt => outerEventList.push(['bubble', evt.detail]) 397 | const onOuterEventListener2 = evt => outerEventList.push(['capture', evt.detail]) 398 | comp.addEventListener('outerListener', onOuterEventListener1) 399 | comp.addEventListener('outerListener', onOuterEventListener2, true) 400 | comp.dispatchEvent('outerListener', {detail: {a: 1}}) 401 | await _.sleep(10) 402 | expect(outerEventList).toEqual([['capture', {a: 1}], ['bubble', {a: 1}]]) 403 | outerEventList.length = 0 404 | comp.instance.triggerEvent('outerListener', {a: 2}, {capturePhase: true}) 405 | await _.sleep(10) 406 | expect(outerEventList).toEqual([['capture', {a: 2}], ['bubble', {a: 2}]]) 407 | outerEventList.length = 0 408 | comp.instance.triggerEvent('outerListener', {a: 3}) 409 | await _.sleep(10) 410 | expect(outerEventList).toEqual([['bubble', {a: 3}]]) 411 | outerEventList.length = 0 412 | comp.removeEventListener('outerListener', onOuterEventListener1) 413 | comp.dispatchEvent('outerListener', {detail: {a: 4}}) 414 | await _.sleep(10) 415 | expect(outerEventList).toEqual([['capture', {a: 4}]]) 416 | outerEventList.length = 0 417 | comp.removeEventListener('outerListener', onOuterEventListener2, true) 418 | comp.dispatchEvent('outerListener', {detail: {a: 5}}) 419 | await _.sleep(10) 420 | expect(outerEventList).toEqual([]) 421 | }) 422 | 423 | test('setData', async () => { 424 | const callbackCheck = [] 425 | const childId = jComponent.register({ 426 | tagName: 'child', 427 | template: '-{{show}}', 428 | data: { 429 | show: 1, 430 | }, 431 | }) 432 | const comp = jComponent.create(jComponent.register({ 433 | template: '{{num}}', 434 | data: { 435 | num: 0, 436 | }, 437 | usingComponents: { 438 | child: childId 439 | }, 440 | })) 441 | 442 | expect(comp.dom.innerHTML).toBe('
0-1
') 443 | comp.setData({num: 2}, () => { 444 | callbackCheck.push(0) 445 | }) 446 | await _.sleep(10) 447 | expect(callbackCheck.length).toBe(1) 448 | comp.querySelector('#a').setData({show: 14}, () => { 449 | callbackCheck.push(0) 450 | }) 451 | await _.sleep(10) 452 | expect(callbackCheck.length).toBe(2) 453 | expect(comp.dom.innerHTML).toBe('
2-14
') 454 | }) 455 | 456 | test('getData', () => { 457 | const childId = jComponent.register({ 458 | template: '', 459 | data: { 460 | show: 1, 461 | }, 462 | }) 463 | const comp = jComponent.create(jComponent.register({ 464 | template: '123', 465 | data: { 466 | num: 0, 467 | }, 468 | usingComponents: { 469 | child: childId 470 | }, 471 | })) 472 | 473 | const child = comp.querySelector('#a') 474 | expect(comp.data.num).toBe(0) 475 | expect(child.data.show).toBe(1) 476 | 477 | comp.setData({num: 2}) 478 | expect(comp.data.num).toBe(2) 479 | expect(child.data.show).toBe(1) 480 | 481 | comp.setData({num: 'I am a string'}) 482 | child.setData({show: 'do something'}) 483 | expect(comp.data.num).toBe('I am a string') 484 | expect(child.data.show).toBe('do something') 485 | }) 486 | 487 | test('update event', async () => { 488 | const comp = jComponent.create(jComponent.register({ 489 | template: ` 490 | if-{{num}} 491 | else-{{num}} 492 | `, 493 | data: { 494 | num: 0, 495 | flag: true, 496 | }, 497 | methods: { 498 | onTap() { 499 | this.setData({num: 1}) 500 | }, 501 | onTap2() { 502 | this.setData({num: 2}) 503 | }, 504 | }, 505 | })) 506 | 507 | expect(comp.dom.innerHTML).toBe('
if-0
') 508 | comp.querySelector('.a').dispatchEvent('tap') 509 | await _.sleep(10) 510 | expect(comp.dom.innerHTML).toBe('
if-1
') 511 | comp.setData({flag: false}) 512 | expect(comp.dom.innerHTML).toBe('
else-1
') 513 | comp.querySelector('.a').dispatchEvent('tap') 514 | await _.sleep(10) 515 | expect(comp.dom.innerHTML).toBe('
else-2
') 516 | }) 517 | 518 | test('attached and detached', () => { 519 | const comp = jComponent.create(jComponent.register({ 520 | tagName: 'test', 521 | template: '
123
', 522 | })) 523 | 524 | const parent = document.createElement('div') 525 | 526 | comp.detach() // 未 attach,作 detach 则不作任何处理 527 | expect(parent.innerHTML).toBe('') 528 | 529 | comp.attach(parent) 530 | expect(parent.innerHTML).toBe('
123
') 531 | 532 | comp.detach() 533 | expect(parent.innerHTML).toBe('') 534 | }) 535 | 536 | test('life time', () => { 537 | const callbackCheck = [] 538 | const grandChildId = jComponent.register({ 539 | template: '123', 540 | created() { 541 | callbackCheck.push('grand-child-created') 542 | }, 543 | attached() { 544 | callbackCheck.push('grand-child-attached') 545 | }, 546 | ready() { 547 | callbackCheck.push('grand-child-ready') 548 | }, 549 | moved() { 550 | callbackCheck.push('grand-child-moved') 551 | }, 552 | detached() { 553 | callbackCheck.push('grand-child-detached') 554 | }, 555 | }) 556 | const childId = jComponent.register({ 557 | template: '', 558 | usingComponents: { 559 | 'grand-child': grandChildId 560 | }, 561 | created() { 562 | callbackCheck.push('child-created') 563 | }, 564 | attached() { 565 | callbackCheck.push('child-attached') 566 | }, 567 | ready() { 568 | callbackCheck.push('child-ready') 569 | }, 570 | moved() { 571 | callbackCheck.push('child-moved') 572 | }, 573 | detached() { 574 | callbackCheck.push('child-detached') 575 | }, 576 | }) 577 | const comp = jComponent.create(jComponent.register({ 578 | tagName: 'lift-time-comp', 579 | template: '', 580 | usingComponents: { 581 | child: childId 582 | }, 583 | created() { 584 | callbackCheck.push('created') 585 | }, 586 | attached() { 587 | callbackCheck.push('attached') 588 | }, 589 | ready() { 590 | callbackCheck.push('ready') 591 | }, 592 | moved() { 593 | callbackCheck.push('moved') 594 | }, 595 | detached() { 596 | callbackCheck.push('detached') 597 | }, 598 | pageLifetimes: { 599 | show(args) { 600 | callbackCheck.push('pageShow') 601 | callbackCheck.push(args) 602 | }, 603 | }, 604 | })) 605 | const parent = document.createElement('div') 606 | 607 | expect(parent.innerHTML).toBe('') 608 | comp.attach(parent) 609 | comp.triggerLifeTime('moved') 610 | expect(parent.innerHTML).toBe('
123
') 611 | comp.triggerPageLifeTime('show', ['page show args']) 612 | comp.detach() 613 | expect(parent.innerHTML).toBe('') 614 | expect(callbackCheck).toEqual([ 615 | 'grand-child-created', 'child-created', 'created', 616 | 'attached', 'child-attached', 'grand-child-attached', 617 | 'grand-child-ready', 'child-ready', 'ready', 'moved', 618 | 'pageShow', ['page show args'], 619 | 'grand-child-detached', 'child-detached', 'detached' 620 | ]) 621 | }) 622 | 623 | test('wx://component-export', () => { 624 | const compaId = jComponent.register({ 625 | template: '123', 626 | behaviors: ['wx://component-export'], 627 | export() { 628 | return {myField: 'myValue'} 629 | }, 630 | }) 631 | const comp = jComponent.create(jComponent.register({ 632 | tagName: 'compb', 633 | template: ` 634 | header 635 | 636 | footer 637 | `, 638 | usingComponents: { 639 | compa: compaId, 640 | }, 641 | })) 642 | expect(comp.instance.selectComponent('#compa')).toEqual({myField: 'myValue'}) 643 | 644 | const compbId = jComponent.register({ 645 | template: '123', 646 | }) 647 | const comp2 = jComponent.create(jComponent.register({ 648 | tagName: 'compb', 649 | template: ` 650 | header 651 | 652 | footer 653 | `, 654 | behaviors: ['wx://component-export'], 655 | export() { 656 | return {myField: 'myValue'} 657 | }, 658 | usingComponents: { 659 | compb: compbId, 660 | }, 661 | })) 662 | expect(comp2.instance.selectComponent('#compb').selectOwnerComponent()).toEqual({myField: 'myValue'}) 663 | }) 664 | 665 | test('error', () => { 666 | let catchErr = null 667 | try { 668 | jComponent.register() 669 | } catch (err) { 670 | catchErr = err 671 | } 672 | expect(catchErr.message).toBe('invalid template') 673 | 674 | catchErr = null 675 | try { 676 | jComponent.register({ 677 | template: '{{num}}', 678 | usingComponents: { 679 | comp: 12345, 680 | }, 681 | data: { 682 | num: 0, 683 | }, 684 | }) 685 | } catch (err) { 686 | catchErr = err 687 | } 688 | expect(catchErr.message).toBe('component comp not found') 689 | 690 | expect(jComponent.create(123456)).toBe(undefined) 691 | }) 692 | 693 | test('toJSON', () => { 694 | const view = jComponent.register({ 695 | template: '', 696 | }) 697 | 698 | const child = jComponent.register({ 699 | template: '', 700 | usingComponents: {view} 701 | }) 702 | 703 | const comp = jComponent.create(jComponent.register({ 704 | usingComponents: {view, child}, 705 | template: ` 706 | 707 | module.exports.hasLength = function (arr) { return arr.length > 0; } 708 | 709 | 710 | 711 | {{item}} 712 | 720 | `, 721 | data: { 722 | condition: false, 723 | items: [1, 2, 3] 724 | }, 725 | methods: { 726 | onTap() {}, 727 | onCatchTouchStart() {}, 728 | onCaptureTouchMove() {}, 729 | onMutatedtouchend() {}, 730 | onCaptureMutatedLongTap() {}, 731 | }, 732 | })) 733 | expect(comp.toJSON()).toMatchSnapshot() 734 | comp.setData({condition: true, items: []}) 735 | expect(comp.toJSON()).toMatchSnapshot() 736 | }) 737 | 738 | test('relations', () => { 739 | const customUlId = getId() 740 | const customLiId = getId() 741 | let ulLink = 0 742 | const ulLinkTargetList = [] 743 | let ulUnlink = 0 744 | const ulUnlinkTargetList = [] 745 | let liLink = 0 746 | const liLinkTargetList = [] 747 | let liUnlink = 0 748 | const liUnlinkTargetList = [] 749 | 750 | jComponent.register({ 751 | id: customUlId, 752 | template: '', 753 | path: '/mp/component/ul', 754 | relations: { 755 | [customLiId]: { 756 | target: customLiId, 757 | type: 'child', 758 | linked(target) { 759 | ulLink++ 760 | ulLinkTargetList.push(target) 761 | }, 762 | unlinked(target) { 763 | ulUnlink++ 764 | ulUnlinkTargetList.push(target) 765 | }, 766 | }, 767 | }, 768 | }) 769 | jComponent.register({ 770 | id: customLiId, 771 | template: 'li-', 772 | path: '/mp/component/li', 773 | relations: { 774 | [customUlId]: { 775 | target: customUlId, 776 | type: 'parent', 777 | linked(target) { 778 | liLink++ 779 | liLinkTargetList.push(target) 780 | }, 781 | unlinked(target) { 782 | liUnlink++ 783 | liUnlinkTargetList.push(target) 784 | }, 785 | }, 786 | }, 787 | }) 788 | const comp = jComponent.create(jComponent.register({ 789 | tagName: 'comp', 790 | template: ` 791 | 792 | {{item}} 793 | 794 | `, 795 | usingComponents: { 796 | 'custom-ul': customUlId, 797 | 'custom-li': customLiId, 798 | }, 799 | data: { 800 | list: [1, 2], 801 | }, 802 | })) 803 | const parent = document.createElement('parent-wrapper') 804 | comp.attach(parent) 805 | 806 | const ul = comp.querySelectorAll('.ul')[0].instance 807 | 808 | // link 809 | expect(ulLink).toBe(2) 810 | expect(ulLinkTargetList.length).toBe(2) 811 | expect(ulLinkTargetList[0]).toBe(comp.querySelectorAll('.li')[0].instance) 812 | expect(ulLinkTargetList[1]).toBe(comp.querySelectorAll('.li')[1].instance) 813 | expect(liLink).toBe(2) 814 | expect(liLinkTargetList.length).toBe(2) 815 | expect(liLinkTargetList[0]).toBe(ul) 816 | expect(liLinkTargetList[1]).toBe(ul) 817 | 818 | let relationNodes = ul.getRelationNodes('./li') 819 | expect(relationNodes.length).toBe(2) 820 | expect(relationNodes[0]).toBe(ulLinkTargetList[0]) 821 | expect(relationNodes[1]).toBe(ulLinkTargetList[1]) 822 | 823 | let relationNodes2 = relationNodes[0].getRelationNodes('./ul') 824 | expect(relationNodes2.length).toBe(1) 825 | expect(relationNodes2[0]).toBe(ul) 826 | relationNodes2 = relationNodes[1].getRelationNodes('./ul') 827 | expect(relationNodes2.length).toBe(1) 828 | expect(relationNodes2[0]).toBe(ul) 829 | 830 | // unlink 831 | ulLinkTargetList.length = 0 832 | liLinkTargetList.length = 0 833 | comp.setData({list: [2, 3, 4]}) 834 | expect(ulLink).toBe(4) 835 | expect(ulLinkTargetList.length).toBe(2) 836 | expect(ulLinkTargetList[0]).toBe(comp.querySelectorAll('.li')[1].instance) 837 | expect(ulLinkTargetList[1]).toBe(comp.querySelectorAll('.li')[2].instance) 838 | expect(ulUnlink).toBe(1) 839 | expect(ulUnlinkTargetList.length).toBe(1) 840 | expect(ulUnlinkTargetList[0]).toBe(relationNodes[0]) 841 | expect(liLink).toBe(4) 842 | expect(liLinkTargetList.length).toBe(2) 843 | expect(liLinkTargetList[0]).toBe(ul) 844 | expect(liLinkTargetList[1]).toBe(ul) 845 | expect(liUnlink).toBe(1) 846 | expect(liUnlinkTargetList.length).toBe(1) 847 | expect(liUnlinkTargetList[0]).toBe(ul) 848 | 849 | relationNodes = ul.getRelationNodes('./li') 850 | expect(relationNodes.length).toBe(3) 851 | expect(relationNodes[0]).toBe(comp.querySelectorAll('.li')[0].instance) 852 | expect(relationNodes[1]).toBe(ulLinkTargetList[0]) 853 | expect(relationNodes[2]).toBe(ulLinkTargetList[1]) 854 | 855 | relationNodes2 = relationNodes[0].getRelationNodes('./ul') 856 | expect(relationNodes2.length).toBe(1) 857 | expect(relationNodes2[0]).toBe(ul) 858 | relationNodes2 = relationNodes[1].getRelationNodes('./ul') 859 | expect(relationNodes2.length).toBe(1) 860 | expect(relationNodes2[0]).toBe(ul) 861 | relationNodes2 = relationNodes[2].getRelationNodes('./ul') 862 | expect(relationNodes2.length).toBe(1) 863 | expect(relationNodes2[0]).toBe(ul) 864 | 865 | // detach 866 | ulLinkTargetList.length = 0 867 | ulUnlinkTargetList.length = 0 868 | liLinkTargetList.length = 0 869 | liUnlinkTargetList.length = 0 870 | comp.detach() 871 | expect(ulLink).toBe(4) 872 | expect(ulLinkTargetList.length).toBe(0) 873 | expect(ulUnlink).toBe(4) 874 | expect(ulUnlinkTargetList.length).toBe(3) 875 | expect(ulUnlinkTargetList[0]).toBe(relationNodes[0]) 876 | expect(ulUnlinkTargetList[1]).toBe(relationNodes[1]) 877 | expect(ulUnlinkTargetList[2]).toBe(relationNodes[2]) 878 | expect(liLink).toBe(4) 879 | expect(liLinkTargetList.length).toBe(0) 880 | expect(liUnlink).toBe(4) 881 | expect(liUnlinkTargetList.length).toBe(3) 882 | expect(liUnlinkTargetList[0]).toBe(ul) 883 | expect(liUnlinkTargetList[1]).toBe(ul) 884 | expect(liUnlinkTargetList[2]).toBe(ul) 885 | 886 | relationNodes = ul.getRelationNodes('./li') 887 | expect(relationNodes.length).toBe(0) 888 | }) 889 | 890 | test('virtual host', () => { 891 | const compaId = jComponent.register({ 892 | template: '{{index + \'-\' + item + \'-\' + style}}', 893 | properties: { 894 | list: { 895 | type: Array, 896 | value: [], 897 | }, 898 | style: { 899 | type: String, 900 | }, 901 | }, 902 | options: { 903 | virtualHost: true, 904 | }, 905 | }) 906 | const comp = jComponent.create(jComponent.register({ 907 | template: ` 908 | if 909 | else 910 | {{index}} 911 | `, 912 | usingComponents: { 913 | compa: compaId, 914 | }, 915 | data: { 916 | index: 0, 917 | list: [1, 2], 918 | }, 919 | })) 920 | 921 | expect(comp.dom.innerHTML).toBe('
else
0-1-a
1-2-a
0') 922 | expect(comp.toJSON()).toMatchSnapshot() 923 | 924 | comp.setData({ 925 | index: 1, 926 | list: [1, 2, 3], 927 | }) 928 | expect(comp.dom.innerHTML).toBe('
if
0-1-a
1-2-a
2-3-a
1') 929 | expect(comp.toJSON()).toMatchSnapshot() 930 | 931 | const comp2 = jComponent.create(jComponent.register({ 932 | template: ` 933 | 123 934 | 321 935 | `, 936 | options: { 937 | virtualHost: true, 938 | }, 939 | })) 940 | expect(comp2.dom.innerHTML).toBe('
123
321
') 941 | expect(comp.toJSON()).toMatchSnapshot() 942 | }) 943 | 944 | test('definition filter', () => { 945 | const defFieldsList = [] 946 | const definitionFilterArrList = [] 947 | const behavior1 = jComponent.behavior({ 948 | data: {is: 'behavior1'}, 949 | definitionFilter(defFields, definitionFilterArr) { 950 | defFieldsList.push(Object.assign({}, defFields.data)) 951 | definitionFilterArrList.push(definitionFilterArr.length) 952 | defFields.data.is = 'behavior1' 953 | } 954 | }) 955 | const behavior2 = jComponent.behavior({ 956 | behaviors: [behavior1], 957 | data: {is: 'behavior2'}, 958 | definitionFilter(defFields, definitionFilterArr) { 959 | defFieldsList.push(Object.assign({}, defFields.data)) 960 | definitionFilterArrList.push(definitionFilterArr.length) 961 | defFields.data.is = 'behavior2' 962 | definitionFilterArr[0](defFields) 963 | } 964 | }) 965 | defFieldsList.push({is: 'split'}) 966 | definitionFilterArrList.push(-1) 967 | const behavior3 = jComponent.behavior({ 968 | behaviors: [behavior2], 969 | data: {is: 'behavior3'}, 970 | definitionFilter(defFields, definitionFilterArr) { 971 | defFieldsList.push(Object.assign({}, defFields.data)) 972 | definitionFilterArrList.push(definitionFilterArr.length) 973 | defFields.data.is = 'behavior3' 974 | definitionFilterArr[0](defFields) 975 | } 976 | }) 977 | defFieldsList.push({is: 'split'}) 978 | definitionFilterArrList.push(-1) 979 | const comp = jComponent.create(jComponent.register({ 980 | template: '', 981 | behaviors: [behavior3], 982 | data: {is: 'comp'}, 983 | })) 984 | expect(comp.data.is).toBe('behavior1') 985 | expect(defFieldsList).toEqual([ 986 | {is: 'behavior2'}, 987 | {is: 'split'}, 988 | {is: 'behavior3'}, 989 | {is: 'behavior2'}, 990 | {is: 'split'}, 991 | {is: 'comp'}, 992 | {is: 'behavior3'}, 993 | {is: 'behavior2'}, 994 | ]) 995 | expect(definitionFilterArrList).toEqual([0, -1, 1, 0, -1, 1, 1, 0]) 996 | }) 997 | -------------------------------------------------------------------------------- /test/diff.test.js: -------------------------------------------------------------------------------- 1 | const jComponent = require('../src/index') 2 | const diff = require('../src/render/diff') 3 | 4 | test('diffAttrs', () => { 5 | expect(diff.diffAttrs([ 6 | {name: 'a', value: '123'}, 7 | {name: 'b', value: '123'}, 8 | {name: 'c', value: '123'} 9 | ], [ 10 | {name: 'b', value: '321'}, 11 | {name: 'c', value: '123'}, 12 | {name: 'd', value: '123'} 13 | ])).toEqual([ 14 | {name: 'b', value: '321'}, 15 | {name: 'c', value: '123'}, 16 | {name: 'd', value: '123'}, 17 | {name: 'a', value: undefined} 18 | ]) 19 | 20 | expect(diff.diffAttrs([ 21 | {name: 'a', value: '123'}, 22 | {name: 'b', value: '123'}, 23 | {name: 'c', value: '123'} 24 | ], [ 25 | {name: 'a', value: '123'}, 26 | {name: 'b', value: '123'}, 27 | {name: 'c', value: '123'} 28 | ])).toBe(false) 29 | }) 30 | 31 | test('diffList: test moves', () => { 32 | let oldList = [{key: 1}, {key: 2}, {}] 33 | let newList = [{key: 3}, {key: 1}] 34 | let diffs = diff.diffList(oldList, newList) 35 | 36 | expect(diffs.children).toEqual([{key: 1}, null, null]) 37 | expect(diffs.moves.removes).toEqual([2, 1]) 38 | expect(diffs.moves.inserts).toEqual([{oldIndex: -1, index: 0}]) 39 | 40 | oldList = [{key: 1}, {key: 2}, {key: 3}] 41 | newList = [{key: 3}, {key: 2}, {key: 1}] 42 | diffs = diff.diffList(oldList, newList) 43 | 44 | expect(diffs.children).toEqual([{key: 1}, {key: 2}, {key: 3}]) 45 | expect(diffs.moves.removes).toEqual([]) 46 | expect(diffs.moves.inserts).toEqual([{oldIndex: 2, index: 0}]) 47 | }) 48 | 49 | test('diffList: empty old list', () => { 50 | const oldList = [] 51 | const newList = [{key: 1}] 52 | const diffs = diff.diffList(oldList, newList) 53 | 54 | expect(diffs.children).toEqual([]) 55 | expect((diffs.moves.removes)).toEqual([]) 56 | expect((diffs.moves.inserts)).toEqual([{oldIndex: -1, index: 0}]) 57 | }) 58 | 59 | test('diffList: empty new list', () => { 60 | const oldList = [{key: 1}] 61 | const newList = [] 62 | const diffs = diff.diffList(oldList, newList) 63 | 64 | expect(diffs.children).toEqual([null]) 65 | expect((diffs.moves.removes)).toEqual([0]) 66 | expect((diffs.moves.inserts)).toEqual([]) 67 | }) 68 | 69 | test('diffList: removing items', () => { 70 | const oldList = [{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}, {key: 6}] 71 | const newList = [{key: 2}, {key: 3}, {key: 1}] 72 | const diffs = diff.diffList(oldList, newList) 73 | 74 | expect(diffs.children).toEqual([{key: 1}, {key: 2}, {key: 3}, null, null, null]) 75 | expect((diffs.moves.removes)).toEqual([5, 4, 3]) 76 | expect((diffs.moves.inserts)).toEqual([{oldIndex: 1, index: 0}, {oldIndex: 2, index: 1}]) 77 | }) 78 | 79 | test('diffList: key and free', () => { 80 | const oldList = [{key: 1}, {}] 81 | const newList = [{}, {key: 1}] 82 | const diffs = diff.diffList(oldList, newList) 83 | 84 | expect(diffs.children).toEqual([{key: 1}, {}]) 85 | expect((diffs.moves.removes)).toEqual([]) 86 | expect((diffs.moves.inserts)).toEqual([{oldIndex: 1, index: 0}]) 87 | }) 88 | 89 | test('diffList: inserting items', () => { 90 | const oldList = [{key: 1}, {key: 2}, {key: 3}, {key: 4}] 91 | const newList = [{key: 1}, {key: 2}, {key: 5}, {key: 6}, {key: 3}, {key: 4}] 92 | const diffs = diff.diffList(oldList, newList) 93 | 94 | expect(diffs.children).toEqual([{key: 1}, {key: 2}, {key: 3}, {key: 4}]) 95 | expect((diffs.moves.removes)).toEqual([]) 96 | expect((diffs.moves.inserts)).toEqual([{oldIndex: -1, index: 2}, {oldIndex: -1, index: 3}]) 97 | }) 98 | 99 | test('diffList: moving items from back to front', () => { 100 | const oldList = [{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}, {key: 6}] 101 | const newList = [{key: 1}, {key: 2}, {key: 5}, {key: 6}, {key: 3}, {key: 4}, {key: 7}, {key: 8}] 102 | const diffs = diff.diffList(oldList, newList) 103 | 104 | expect(diffs.children).toEqual([{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}, {key: 6}]) 105 | expect((diffs.moves.removes)).toEqual([]) 106 | expect((diffs.moves.inserts)).toEqual([ 107 | {oldIndex: 4, index: 2}, 108 | {oldIndex: 5, index: 3}, 109 | {oldIndex: -1, index: 6}, 110 | {oldIndex: -1, index: 7} 111 | ]) 112 | }) 113 | 114 | test('diffList: moving items from front to back', () => { 115 | const oldList = [{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}, {key: 6}] 116 | const newList = [{key: 1}, {key: 3}, {key: 5}, {key: 6}, {key: 2}, {key: 4}] 117 | const diffs = diff.diffList(oldList, newList) 118 | 119 | expect(diffs.children).toEqual([{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}, {key: 6}]) 120 | expect((diffs.moves.removes)).toEqual([]) 121 | expect((diffs.moves.inserts)).toEqual([ 122 | {oldIndex: 2, index: 1}, 123 | {oldIndex: 4, index: 2}, 124 | {oldIndex: 5, index: 3} 125 | ]) 126 | }) 127 | 128 | test('diffList: miscellaneous actions', () => { 129 | const oldList = [{key: 1}, {key: 2}, {key: 3}, {key: 4}, {key: 5}, {key: 6}] 130 | const newList = [{key: 3}, {key: 6}, {key: 7}, {key: 2}, {key: 8}, {key: 9}, {key: 4}, {key: 1}, {key: 11}] 131 | const diffs = diff.diffList(oldList, newList) 132 | 133 | expect(diffs.children).toEqual([{key: 1}, {key: 2}, {key: 3}, {key: 4}, null, {key: 6}]) 134 | expect((diffs.moves.removes)).toEqual([4]) 135 | expect((diffs.moves.inserts)).toEqual([ 136 | {oldIndex: 2, index: 0}, 137 | {oldIndex: 5, index: 1}, 138 | {oldIndex: -1, index: 2}, 139 | {oldIndex: 1, index: 3}, 140 | {oldIndex: -1, index: 4}, 141 | {oldIndex: -1, index: 5}, 142 | {oldIndex: 3, index: 6}, 143 | {oldIndex: -1, index: 8} 144 | ]) 145 | }) 146 | 147 | test('diffList: without key', () => { 148 | const oldList = [{a: 1}, {a: 2}] 149 | const newList = [{a: 2}, {a: 3}, {a: 4}] 150 | const diffs = diff.diffList(oldList, newList) 151 | 152 | expect(diffs.children).toEqual([{a: 2}, {a: 3}]) 153 | expect((diffs.moves.removes)).toEqual([]) 154 | expect((diffs.moves.inserts)).toEqual([{oldIndex: -1, index: 2}]) 155 | }) 156 | 157 | test('diffList: same key', () => { 158 | const oldList = [{key: 1}, {key: 1}, {}] 159 | const newList = [{key: 3}, {key: 3}, {key: 1}] 160 | const diffs = diff.diffList(oldList, newList) 161 | 162 | expect(diffs.children).toEqual([{key: 1}, {key: 3}, null]) 163 | expect(diffs.moves.removes).toEqual([2]) 164 | expect(diffs.moves.inserts).toEqual([{oldIndex: -1, index: 0}, {oldIndex: -1, index: 1}]) 165 | }) 166 | 167 | test('diffList: mixed', () => { 168 | const oldList = [{key: 1}, {key: 2}, {key: 3}, {key: 4}, {name: 'a'}, {key: 5}, {name: 'b'}, {key: 6}] 169 | const newList = [{name: 'c'}, {key: 6}, {key: 7}, {key: 2}, {key: 1}, {key: 9}, {key: 5}] 170 | const diffs = diff.diffList(oldList, newList) 171 | 172 | expect(diffs.children).toEqual([{key: 1}, {key: 2}, null, null, {name: 'c'}, {key: 5}, null, {key: 6}]) 173 | expect((diffs.moves.removes)).toEqual([6, 3, 2]) 174 | expect((diffs.moves.inserts)).toEqual([ 175 | {oldIndex: 4, index: 0}, 176 | {oldIndex: 7, index: 1}, 177 | {oldIndex: -1, index: 2}, 178 | {oldIndex: 1, index: 3}, 179 | {oldIndex: -1, index: 5} 180 | ]) 181 | }) 182 | 183 | test('diff children', async () => { 184 | const view = jComponent.register({ 185 | tagName: 'wx-view', 186 | template: '' 187 | }) 188 | const text = jComponent.register({ 189 | tagName: 'wx-text', 190 | template: '' 191 | }) 192 | const comp = jComponent.create(jComponent.register({ 193 | usingComponents: {view, text}, 194 | template: ` 195 | 196 | 197 | {{text}} 198 | 199 | 200 | `, 201 | data: { 202 | condition: true, 203 | text: '123' 204 | } 205 | })) 206 | comp.attach(document.createElement('parent-wrapper')) 207 | 208 | let parent = comp.querySelectorAll('.parent') 209 | let child = comp.querySelector('.child') 210 | expect(parent).toHaveLength(1) 211 | expect(parent[0].dom.tagName).toBe('WX-TEXT') 212 | expect(parent[0].dom.dataset.tag).toBe('text') 213 | expect(child).toBe(undefined) 214 | 215 | comp.setData({condition: false}) 216 | comp.setData({text: '233'}) 217 | 218 | parent = comp.querySelectorAll('.parent') 219 | child = comp.querySelector('.child') 220 | expect(parent).toHaveLength(1) 221 | expect(parent[0].dom.tagName).toBe('WX-VIEW') 222 | expect(parent[0].dom.dataset.tag).toBe('view') 223 | expect(child).not.toBe(undefined) 224 | expect(child.dom.innerHTML).toBe('233') 225 | }) 226 | -------------------------------------------------------------------------------- /test/expr.test.js: -------------------------------------------------------------------------------- 1 | const Expression = require('../src/template/expr') 2 | 3 | test('get and calc expression', () => { 4 | let arr = Expression.getExpression('123-{{a + b}}-456-{{a - b}}-{{') 5 | expect(arr.length).toBe(6) 6 | expect(arr[0]).toBe('123-') 7 | expect(arr[1]).toBeInstanceOf(Function) 8 | expect(arr[2]).toBe('-456-') 9 | expect(arr[3]).toBeInstanceOf(Function) 10 | expect(arr[4]).toBe('-') 11 | expect(arr[5]).toBe('{{') 12 | expect(Expression.calcExpression(arr, {a: 4, b: 1})).toBe('123-5-456-3-{{') 13 | 14 | arr = Expression.getExpression('123') 15 | expect(arr).toEqual(['123']) 16 | expect(Expression.calcExpression(arr, {})).toBe('123') 17 | 18 | arr = Expression.getExpression('{{a + b}}') 19 | expect(arr.length).toBe(1) 20 | expect(arr[0]).toBeInstanceOf(Function) 21 | expect(Expression.calcExpression(arr, {a: 1, b: 2})).toBe(3) 22 | 23 | expect(Expression.calcExpression(123)).toBe(123) 24 | 25 | expect(Expression.calcExpression(['123-', 56, '{{'])).toBe('123-{{') 26 | }) 27 | -------------------------------------------------------------------------------- /test/intersectionobserver.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | const jComponent = require('../src/index') 3 | const IntersectionObserver = require('../src/tool/intersectionobserver') 4 | 5 | function simulateScroll(comp, scrollTop, times = 50) { 6 | const dom = comp.dom 7 | const delta = scrollTop - dom.scrollTop 8 | const unit = delta / times 9 | 10 | for (let i = 0; i < times; i++) { 11 | if (i === times - 1) dom.scrollTop = scrollTop 12 | else dom.scrollTop += unit 13 | 14 | dom.dispatchEvent(new Event('scroll', {bubbles: true, cancelable: false})) 15 | } 16 | } 17 | 18 | beforeAll(() => { 19 | _.env() 20 | }) 21 | 22 | test('IntersectionObserver', async () => { 23 | const comp = jComponent.create(jComponent.register({ 24 | template: `
25 |
26 |
27 |
28 |
`, 29 | })) 30 | const outer = comp.querySelector('#outer') 31 | comp.attach(document.body) 32 | 33 | let intersectionObserver = new IntersectionObserver(comp) 34 | let resList = [] 35 | intersectionObserver.relativeTo('#outer').observe('#block', res => resList.push(res)) 36 | simulateScroll(outer, 320) 37 | await _.sleep(10) 38 | expect(resList).toEqual([]) // jsdom 没有实现布局引擎,getBoundingClientRect 返回各字段永远为 0,所以这个 intersesionObserver 无法很好的模拟 39 | intersectionObserver.disconnect() 40 | comp.detach() 41 | 42 | intersectionObserver = new IntersectionObserver(comp, { 43 | thresholds: [10, 30], 44 | initialRatio: 20, 45 | observeAll: true, 46 | }) 47 | resList = [] 48 | intersectionObserver.relativeTo('#outer').observe('#block', res => resList.push(res)) 49 | comp.attach(document.body) // 先创建 intersectionObserver 再 attached 50 | simulateScroll(outer, 320) 51 | await _.sleep(10) 52 | expect(resList).toMatchObject([{ 53 | boundingClientRect: { 54 | bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 55 | }, 56 | dataset: {}, 57 | id: 'block', 58 | intersectionRatio: 0, 59 | intersectionRect: { 60 | bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 61 | }, 62 | relativeRect: { 63 | bottom: 0, left: 0, right: 0, top: 0 64 | }, 65 | }]) // 同上 66 | comp.detach() 67 | intersectionObserver.disconnect() // detach 之后再 disconnect 68 | 69 | intersectionObserver = new IntersectionObserver(comp) 70 | resList = [] 71 | intersectionObserver.relativeToViewport().observe('#block', res => resList.push(res)) 72 | simulateScroll(outer, 320) 73 | await _.sleep(10) 74 | expect(resList).toEqual([]) 75 | intersectionObserver.disconnect() 76 | }) 77 | 78 | test('createIntersectionObserver', () => { 79 | const comp = jComponent.create(jComponent.register({ 80 | template: `
81 |
82 |
83 |
84 |
`, 85 | methods: { 86 | getIntersectionObserver() { 87 | return this.createIntersectionObserver() 88 | }, 89 | }, 90 | })) 91 | 92 | expect(comp.instance.getIntersectionObserver()._exparserNode).toBe(comp.instance._exparserNode) 93 | }) 94 | -------------------------------------------------------------------------------- /test/parse.test.js: -------------------------------------------------------------------------------- 1 | const parse = require('../src/template/parse') 2 | 3 | function getParseResult(content) { 4 | const startStack = [] 5 | const endStack = [] 6 | const textStack = [] 7 | 8 | parse(content, { 9 | start(tagName, attrs, unary) { 10 | startStack.push({tagName, attrs, unary}) 11 | }, 12 | end(tagName) { 13 | endStack.push(tagName) 14 | }, 15 | text(content) { 16 | content = content.trim() 17 | if (content) textStack.push(content) 18 | }, 19 | }) 20 | 21 | return {startStack, endStack, textStack} 22 | } 23 | 24 | test('parse template', () => { 25 | let res = getParseResult('
') 26 | expect(res.startStack).toEqual([{tagName: 'div', attrs: [], unary: false}, {tagName: 'slot', attrs: [], unary: true}]) 27 | expect(res.endStack).toEqual(['div']) 28 | expect(res.textStack).toEqual([]) 29 | 30 | res = getParseResult(` 31 |
32 |
123123
33 | 34 |
35 |
    36 |
  • 123
  • 37 |
  • 321
  • 38 |
  • 567
  • 39 |
40 |
41 | `) 42 | expect(res.startStack).toEqual([ 43 | {tagName: 'div', attrs: [], unary: false}, 44 | {tagName: 'slot', attrs: [], unary: true}, 45 | {tagName: 'div', attrs: [{name: 'id', value: 'a'}, {name: 'class', value: 'xx'}], unary: false}, 46 | {tagName: 'input', attrs: [{name: 'id', value: 'b'}, {name: 'type', value: 'checkbox'}, {name: 'checked', value: true}, {name: 'url', value: ''}], unary: true}, 47 | {tagName: 'div', attrs: [], unary: false}, 48 | {tagName: 'ul', attrs: [], unary: false}, 49 | {tagName: 'li', attrs: [], unary: false}, 50 | {tagName: 'span', attrs: [], unary: false}, 51 | {tagName: 'li', attrs: [], unary: false}, 52 | {tagName: 'span', attrs: [], unary: false}, 53 | {tagName: 'li', attrs: [], unary: false}, 54 | {tagName: 'span', attrs: [], unary: false} 55 | ]) 56 | expect(res.endStack).toEqual(['div', 'div', 'span', 'li', 'span', 'li', 'span', 'li', 'ul', 'div']) 57 | expect(res.textStack).toEqual(['123123', '123', '321', '567']) 58 | 59 | res = getParseResult('
123
') 60 | expect(res.startStack).toEqual([ 61 | {tagName: 'div', attrs: [], unary: false}, 62 | {tagName: 'span', attrs: [], unary: false} 63 | ]) 64 | expect(res.endStack).toEqual(['span', 'div']) 65 | expect(res.textStack).toEqual(['123']) 66 | 67 | res = getParseResult('
123') 68 | expect(res.startStack).toEqual([ 69 | {tagName: 'div', attrs: [], unary: false} 70 | ]) 71 | expect(res.endStack).toEqual(['div']) 72 | expect(res.textStack).toEqual(['123']) 73 | }) 74 | 75 | test('parse wxs', () => { 76 | let res = getParseResult(` 77 |
123
78 | 79 | var msg = "hello world"; 80 | module.exports.message = msg; 81 | 82 | {{m1.message}} 83 |
321
84 | `) 85 | expect(res.startStack).toEqual([ 86 | {tagName: 'div', attrs: [], unary: false}, 87 | {tagName: 'wxs', attrs: [{name: 'module', value: 'm1'}], unary: false}, 88 | {tagName: 'view', attrs: [], unary: false}, 89 | {tagName: 'div', attrs: [], unary: false} 90 | ]) 91 | expect(res.endStack).toEqual(['div', 'wxs', 'view', 'div']) 92 | expect(res.textStack).toEqual(['123', 'var msg = "hello world";\n module.exports.message = msg;', '{{m1.message}}', '321']) 93 | 94 | res = getParseResult('') 95 | expect(res.startStack).toEqual([ 96 | {tagName: 'wxs', attrs: [], unary: false} 97 | ]) 98 | expect(res.endStack).toEqual(['wxs']) 99 | expect(res.textStack).toEqual([]) 100 | }) 101 | 102 | test('parse comment', () => { 103 | const res = getParseResult('') 104 | expect(res.startStack).toEqual([]) 105 | expect(res.startStack).toEqual([]) 106 | expect(res.startStack).toEqual([]) 107 | }) 108 | 109 | test('parse without options', () => { 110 | let catchErr = null 111 | try { 112 | parse('
123
') 113 | } catch (err) { 114 | catchErr = err 115 | } 116 | 117 | expect(catchErr).toBe(null) 118 | }) 119 | 120 | test('parse error', () => { 121 | function getErr(str) { 122 | let catchErr = null 123 | try { 124 | getParseResult(str) 125 | } catch (err) { 126 | catchErr = err 127 | } 128 | 129 | return catchErr && catchErr.message || '' 130 | } 131 | 132 | expect(getErr('123')).toBe('parse error: 123') 134 | expect(getErr(' ', a, b) 8 | }, 9 | toEqual(b) { 10 | console.log('expect --> ', a, b) 11 | }, 12 | }) 13 | 14 | const {window} = new JSDOM(` 15 | 16 | 17 | 18 | test 19 | 20 | 21 | 22 |
23 | 24 | 25 | `, { 26 | runScripts: 'dangerously', 27 | userAgent: 'miniprogram test', 28 | }) 29 | global.window = window 30 | global.document = window.document 31 | global.TouchEvent = window.TouchEvent 32 | global.CustomEvent = window.CustomEvent 33 | 34 | require('./render.test.js') 35 | -------------------------------------------------------------------------------- /test/selectorquery.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | const jComponent = require('../src/index') 3 | const SelectorQuery = require('../src/tool/selectorquery') 4 | 5 | beforeAll(() => { 6 | _.env() 7 | 8 | jComponent.register({ 9 | id: 'view', 10 | tagName: 'wx-view', 11 | template: '
', 12 | properties: { 13 | hidden: { 14 | type: Boolean, 15 | value: false, 16 | }, 17 | } 18 | }) 19 | }) 20 | 21 | 22 | test('SelectorQuery', async () => { 23 | const comp = jComponent.create(jComponent.register({ 24 | template: '', 25 | })) 26 | 27 | // boundingClientRect 28 | let selectorQuery = new SelectorQuery() 29 | selectorQuery.in(comp) 30 | let res = await new Promise(resolve => { 31 | selectorQuery.select('#abc').boundingClientRect(res => { 32 | resolve(res) 33 | }).exec() 34 | }) 35 | expect(res).toEqual({ 36 | id: 'abc', 37 | dataset: { 38 | a: '1', 39 | b: '2', 40 | }, 41 | left: 0, 42 | right: 0, 43 | top: 0, 44 | bottom: 0, 45 | width: 0, 46 | height: 0, 47 | }) 48 | 49 | // scrollOffset 50 | selectorQuery = new SelectorQuery() 51 | selectorQuery.in(comp) 52 | res = await new Promise(resolve => { 53 | selectorQuery.selectAll('#abc').scrollOffset(res => { 54 | resolve(res) 55 | }).exec() 56 | }) 57 | expect(res).toEqual([{ 58 | id: 'abc', 59 | dataset: { 60 | a: '1', 61 | b: '2', 62 | }, 63 | scrollLeft: 0, 64 | scrollTop: 0, 65 | }]) 66 | 67 | // context 68 | selectorQuery = new SelectorQuery() 69 | selectorQuery.in(comp) 70 | res = await new Promise(resolve => { 71 | selectorQuery.select('#abc').context(res => { 72 | resolve(res) 73 | }).exec() 74 | }) 75 | expect(res).toEqual({ 76 | context: {}, 77 | }) 78 | 79 | // fields 80 | selectorQuery = new SelectorQuery() 81 | selectorQuery.in(comp) 82 | res = await new Promise(resolve => { 83 | selectorQuery.select('#abc').fields({ 84 | id: true, 85 | dataset: true, 86 | rect: true, 87 | size: true, 88 | scrollOffset: true, 89 | properties: ['hidden'], 90 | computedStyle: ['position', 'top', 'left', 'width', 'height'], 91 | context: true, 92 | }, res => { 93 | resolve(res) 94 | }).exec() 95 | }) 96 | expect(res).toEqual({ 97 | id: 'abc', 98 | dataset: { 99 | a: '1', 100 | b: '2', 101 | }, 102 | scrollLeft: 0, 103 | scrollTop: 0, 104 | left: '30px', 105 | right: 0, 106 | top: '20px', 107 | bottom: 0, 108 | width: '100px', 109 | height: '200px', 110 | hidden: false, 111 | position: 'absolute', 112 | context: {}, 113 | }) 114 | 115 | // exec 116 | selectorQuery = new SelectorQuery() 117 | selectorQuery.in(comp) 118 | res = await new Promise(resolve => { 119 | selectorQuery.select('#abc').boundingClientRect().selectAll('#abc').scrollOffset() 120 | .exec(res => { 121 | resolve(res) 122 | }) 123 | }) 124 | expect(res).toEqual([{ 125 | id: 'abc', 126 | dataset: { 127 | a: '1', 128 | b: '2', 129 | }, 130 | left: 0, 131 | right: 0, 132 | top: 0, 133 | bottom: 0, 134 | width: 0, 135 | height: 0, 136 | }, [{ 137 | id: 'abc', 138 | dataset: { 139 | a: '1', 140 | b: '2', 141 | }, 142 | scrollLeft: 0, 143 | scrollTop: 0, 144 | }]]) 145 | selectorQuery = new SelectorQuery() 146 | selectorQuery.in(comp) 147 | res = await new Promise(resolve => { 148 | selectorQuery.select('#cba').boundingClientRect() 149 | .exec(res => { 150 | resolve(res) 151 | }) 152 | }) 153 | expect(res).toEqual([null]) 154 | 155 | // selectViewport 156 | selectorQuery = new SelectorQuery() 157 | selectorQuery.in(comp) 158 | res = await new Promise(resolve => { 159 | selectorQuery.selectViewport().fields({ 160 | id: true, 161 | dataset: true, 162 | rect: true, 163 | size: true, 164 | scrollOffset: true, 165 | }).exec(res => { 166 | resolve(res) 167 | }) 168 | }) 169 | expect(res).toEqual([{ 170 | id: '', 171 | dataset: {}, 172 | left: 0, 173 | right: 0, 174 | top: 0, 175 | bottom: 0, 176 | width: 0, 177 | height: 0, 178 | scrollLeft: 0, 179 | scrollTop: 0, 180 | }]) 181 | 182 | selectorQuery = new SelectorQuery() 183 | selectorQuery.in(comp) 184 | res = await new Promise(resolve => { 185 | selectorQuery.selectViewport().fields({}).exec(res => { 186 | resolve(res) 187 | }) 188 | }) 189 | expect(res).toEqual([{}]) 190 | }) 191 | 192 | test('createSelectorQuery', () => { 193 | const comp = jComponent.create(jComponent.register({ 194 | template: '123', 195 | methods: { 196 | getSelectorQuery() { 197 | return this.createSelectorQuery() 198 | }, 199 | }, 200 | })) 201 | 202 | expect(comp.instance.getSelectorQuery()._exparserNode).toBe(comp.instance._exparserNode) 203 | }) 204 | 205 | test('error', () => { 206 | const selectorQuery = new SelectorQuery() 207 | let catchErr = null 208 | try { 209 | selectorQuery.in() 210 | } catch (err) { 211 | catchErr = err 212 | } 213 | expect(catchErr.message).toBe('invalid params') 214 | }) 215 | -------------------------------------------------------------------------------- /test/this.test.js: -------------------------------------------------------------------------------- 1 | const jComponent = require('../src/index') 2 | const _ = require('./utils') 3 | 4 | beforeAll(() => { 5 | _.env() 6 | 7 | jComponent.register({ 8 | id: 'view', 9 | tagName: 'wx-view', 10 | template: '
' 11 | }) 12 | }) 13 | 14 | test('this.data/this.properties', () => { 15 | const comp = jComponent.create(jComponent.register({ 16 | template: '
123
', 17 | properties: { 18 | aa: { 19 | type: String, 20 | value: '321', 21 | }, 22 | }, 23 | data: { 24 | bb: 123, 25 | }, 26 | })) 27 | 28 | expect(comp.instance.data.bb).toBe(123) 29 | expect(comp.instance.data.aa).toBe('321') 30 | expect(comp.instance.properties.aa).toBe('321') 31 | }) 32 | 33 | test('this.dataset', () => { 34 | const comp = jComponent.create(jComponent.register({ 35 | template: '
123
', 36 | })) 37 | const child = comp.querySelector('#abc') 38 | 39 | expect(child.instance.dataset.a).toBe('123') 40 | expect(child.instance.dataset.b).toBe('321') 41 | }) 42 | 43 | test('this.id/this.is', () => { 44 | const comp = jComponent.create(jComponent.register({ 45 | template: '123', 46 | })) 47 | const child = comp.querySelector('#abc') 48 | 49 | expect(child.instance.id).toBe('abc') 50 | expect(child.instance.is).toBe('view') 51 | }) 52 | 53 | test('this.setData/this.hasBehavior/this.triggerEvent', () => { 54 | const comp = jComponent.create(jComponent.register({ 55 | template: '
123
', 56 | })) 57 | 58 | expect(typeof comp.instance.setData).toBe('function') 59 | expect(typeof comp.instance.hasBehavior).toBe('function') 60 | expect(typeof comp.instance.triggerEvent).toBe('function') 61 | }) 62 | 63 | test('this.selectComponent/this.selectAllComponents', () => { 64 | const comp = jComponent.create(jComponent.register({ 65 | template: '
123
', 66 | })) 67 | 68 | expect(comp.instance.selectComponent('#abc').$$.innerHTML).toBe('123') 69 | expect(comp.instance.selectAllComponents('#abc')[0].$$.innerHTML).toBe('123') 70 | }) 71 | 72 | test('this.selectOwnerComponent', () => { 73 | const compaId = jComponent.register({ 74 | template: '
hello,
', 75 | }) 76 | const compb = jComponent.create(jComponent.register({ 77 | template: 'june', 78 | usingComponents: { 79 | compa: compaId, 80 | }, 81 | })) 82 | 83 | expect(compb.instance.selectComponent('#abc').selectOwnerComponent().$$.innerHTML).toBe('
hello,june
') 84 | }) 85 | -------------------------------------------------------------------------------- /test/transform.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const compiler = require('miniprogram-compiler') 3 | const jComponent = require('../src/index') 4 | const _ = require('./utils') 5 | 6 | beforeAll(() => { 7 | _.env() 8 | }) 9 | 10 | test('support wcc compiler', () => { 11 | window.__webview_engine_version__ = 0.02 12 | const compileString = compiler.wxmlToJs(path.join(__dirname, 'wxml')) 13 | // eslint-disable-next-line no-new-func 14 | const compileFunc = new Function(compileString) 15 | const gwx = compileFunc() 16 | 17 | const compId = jComponent.register({ 18 | properties: { 19 | aa: { 20 | type: String, 21 | value: '', 22 | }, 23 | }, 24 | template: gwx('comp.wxml'), 25 | }) 26 | const id = jComponent.register({ 27 | data: { 28 | tmplData: { 29 | index: 7, 30 | msg: 'I am msg', 31 | time: '12345' 32 | }, 33 | flag: true, 34 | elseData: 'else content', 35 | attrValue: 'I am attr value', 36 | content: 'node content', 37 | aa: 'haha', 38 | list: [ 39 | {id: 1, name: 1}, 40 | {id: 2, name: 2}, 41 | {id: 3, name: 3} 42 | ], 43 | }, 44 | template: gwx('index.wxml'), 45 | usingComponents: { 46 | comp: compId, 47 | }, 48 | }) 49 | const comp = jComponent.create(id) 50 | 51 | expect(comp.dom.innerHTML).toBe('headtmpl7: I am msgTime: 12345hello juneifnode content I am compI am slot1-item2-item3-itemin block1in block2foot') 52 | }) 53 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Touch polyfill 3 | */ 4 | class Touch { 5 | constructor(options = {}) { 6 | this.clientX = 0 7 | this.clientY = 0 8 | this.identifier = 0 9 | this.pageX = 0 10 | this.pageY = 0 11 | this.screenX = 0 12 | this.screenY = 0 13 | this.target = null 14 | 15 | Object.keys(options).forEach(key => { 16 | this[key] = options[key] 17 | }) 18 | } 19 | } 20 | window.requestAnimationFrame = func => setTimeout(func, 0) 21 | 22 | /** 23 | * 环境准备 24 | */ 25 | function env() { 26 | global.Touch = window.Touch = Touch 27 | } 28 | 29 | /** 30 | * 延迟执行后续代码 31 | */ 32 | async function sleep(timeout) { 33 | return new Promise(resolve => { 34 | setTimeout(() => { 35 | resolve() 36 | }, timeout) 37 | }) 38 | } 39 | 40 | /** 41 | * 动画实现 42 | */ 43 | class Animation { 44 | constructor(option = {}) { 45 | this.actions = [] 46 | this.currentTransform = [] 47 | this.currentStepAnimates = [] 48 | 49 | this.option = { 50 | transition: { 51 | duration: option.duration !== undefined ? option.duration : 400, 52 | timingFunction: option.timingFunction !== undefined ? option.timingFunction : 'linear', 53 | delay: option.delay !== undefined ? option.delay : 0, 54 | }, 55 | transformOrigin: option.transformOrigin || '50% 50% 0', 56 | } 57 | } 58 | 59 | export() { 60 | const actions = this.actions 61 | this.actions = [] 62 | return {actions} 63 | } 64 | 65 | step(option = {}) { 66 | this.currentStepAnimates.forEach((animate) => { 67 | if (animate.type !== 'style') { 68 | this.currentTransform[animate.type] = animate 69 | } else { 70 | this.currentTransform[`${animate.type}.${animate.args[0]}`] = animate 71 | } 72 | }) 73 | 74 | this.actions.push({ 75 | animates: Object.keys(this.currentTransform).reduce((prev, key) => [...prev, this.currentTransform[key]], []), 76 | option: { 77 | transformOrigin: option.transformOrigin !== undefined ? option.transformOrigin : this.option.transformOrigin, 78 | transition: { 79 | duration: option.duration !== undefined ? option.duration : this.option.transition.duration, 80 | timingFunction: option.timingFunction !== undefined ? option.timingFunction : this.option.transition.timingFunction, 81 | delay: option.delay !== undefined ? option.delay : this.option.transition.delay, 82 | }, 83 | }, 84 | }) 85 | 86 | this.currentStepAnimates = [] 87 | return this 88 | } 89 | 90 | matrix(a = 1, b = 0, c = 0, d = 1, tx = 1, ty = 1) { 91 | this.currentStepAnimates.push({type: 'matrix', args: [a, b, c, d, tx, ty]}) 92 | return this 93 | } 94 | 95 | matrix3d(a1 = 1, b1 = 0, c1 = 0, d1 = 0, a2 = 0, b2 = 1, c2 = 0, d2 = 0, a3 = 0, b3 = 0, c3 = 1, d3 = 0, a4 = 0, b4 = 0, c4 = 0, d4 = 1) { 96 | this.currentStepAnimates.push({type: 'matrix3d', args: [a1, b1, c1, d1, a2, b2, c2, d2, a3, b3, c3, d3, a4, b4, c4, d4]}) 97 | this.stepping = false 98 | return this 99 | } 100 | 101 | rotate(angle = 0) { 102 | this.currentStepAnimates.push({type: 'rotate', args: [angle]}) 103 | return this 104 | } 105 | 106 | rotate3d(x = 0, y = 0, z = 0, a = 0) { 107 | this.currentStepAnimates.push({type: 'rotate3d', args: [x, y, z, a]}) 108 | this.stepping = false 109 | return this 110 | } 111 | 112 | rotateX(a = 0) { 113 | this.currentStepAnimates.push({type: 'rotateX', args: [a]}) 114 | this.stepping = false 115 | return this 116 | } 117 | 118 | rotateY(a = 0) { 119 | this.currentStepAnimates.push({type: 'rotateY', args: [a]}) 120 | this.stepping = false 121 | return this 122 | } 123 | 124 | rotateZ(a = 0) { 125 | this.currentStepAnimates.push({type: 'rotateZ', args: [a]}) 126 | this.stepping = false 127 | return this 128 | } 129 | 130 | scale(sx = 1, sy) { 131 | this.currentStepAnimates.push({type: 'scale', args: [sx, sy !== undefined ? sy : sx]}) 132 | return this 133 | } 134 | 135 | scale3d(sx = 1, sy = 1, sz = 1) { 136 | this.currentStepAnimates.push({type: 'scale3d', args: [sx, sy, sz]}) 137 | return this 138 | } 139 | 140 | scaleX(s = 1) { 141 | this.currentStepAnimates.push({type: 'scaleX', args: [s]}) 142 | return this 143 | } 144 | 145 | scaleY(s = 1) { 146 | this.currentStepAnimates.push({type: 'scaleY', args: [s]}) 147 | return this 148 | } 149 | 150 | scaleZ(s = 1) { 151 | this.currentStepAnimates.push({type: 'scaleZ', args: [s]}) 152 | return this 153 | } 154 | 155 | skew(ax = 0, ay = 0) { 156 | this.currentStepAnimates.push({type: 'skew', args: [ax, ay]}) 157 | return this 158 | } 159 | 160 | skewX(a = 0) { 161 | this.currentStepAnimates.push({type: 'skewX', args: [a]}) 162 | return this 163 | } 164 | 165 | skewY(a = 0) { 166 | this.currentStepAnimates.push({type: 'skewY', args: [a]}) 167 | return this 168 | } 169 | 170 | translate(tx = 0, ty = 0) { 171 | this.currentStepAnimates.push({type: 'translate', args: [tx, ty]}) 172 | return this 173 | } 174 | 175 | translate3d(tx = 0, ty = 0, tz = 0) { 176 | this.currentStepAnimates.push({type: 'translate3d', args: [tx, ty, tz]}) 177 | return this 178 | } 179 | 180 | translateX(t = 0) { 181 | this.currentStepAnimates.push({type: 'translateX', args: [t]}) 182 | return this 183 | } 184 | 185 | translateY(t = 0) { 186 | this.currentStepAnimates.push({type: 'translateY', args: [t]}) 187 | return this 188 | } 189 | 190 | translateZ(t = 0) { 191 | this.currentStepAnimates.push({type: 'translateZ', args: [t]}) 192 | return this 193 | } 194 | 195 | opacity(value) { 196 | this.currentStepAnimates.push({type: 'style', args: ['opacity', value]}) 197 | return this 198 | } 199 | 200 | backgroundColor(value) { 201 | this.currentStepAnimates.push({type: 'style', args: ['background-color', value]}) 202 | return this 203 | } 204 | 205 | width(value) { 206 | this.currentStepAnimates.push({type: 'style', args: ['width', typeof value === 'number' ? value + 'px' : value]}) 207 | return this 208 | } 209 | 210 | height(value) { 211 | this.currentStepAnimates.push({type: 'style', args: ['height', typeof value === 'number' ? value + 'px' : value]}) 212 | return this 213 | } 214 | 215 | left(value) { 216 | this.currentStepAnimates.push({type: 'style', args: ['left', typeof value === 'number' ? value + 'px' : value]}) 217 | return this 218 | } 219 | 220 | right(value) { 221 | this.currentStepAnimates.push({type: 'style', args: ['right', typeof value === 'number' ? value + 'px' : value]}) 222 | return this 223 | } 224 | 225 | top(value) { 226 | this.currentStepAnimates.push({type: 'style', args: ['top', typeof value === 'number' ? value + 'px' : value]}) 227 | return this 228 | } 229 | 230 | bottom(value) { 231 | this.currentStepAnimates.push({type: 'style', args: ['bottom', typeof value === 'number' ? value + 'px' : value]}) 232 | return this 233 | } 234 | } 235 | function createAnimation(transition = {}) { 236 | return new Animation(transition) 237 | } 238 | 239 | module.exports = { 240 | env, 241 | sleep, 242 | createAnimation, 243 | } 244 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | const utils = require('../src/tool/utils') 3 | 4 | test('getId', () => { 5 | expect(typeof utils.getId()).toBe('number') 6 | expect(utils.getId() + '').toMatch(/\d{13}/) 7 | expect(utils.getId(true)).toMatch(/[a-j]{13}/) 8 | }) 9 | 10 | test('copy', () => { 11 | let src = {a: 123, b: [{c: 321}, 456]} 12 | let res = utils.copy(src) 13 | expect(res).not.toBe(src) 14 | expect(res).toEqual(src) 15 | 16 | src = Symbol('test') 17 | res = utils.copy(src) 18 | expect(res).toBe(undefined) 19 | }) 20 | 21 | test('isHtmlTag', () => { 22 | expect(utils.isHtmlTag('div')).toBe(true) 23 | expect(utils.isHtmlTag('span')).toBe(true) 24 | expect(utils.isHtmlTag('component')).toBe(false) 25 | expect(utils.isHtmlTag('wxs')).toBe(false) 26 | }) 27 | 28 | test('transformRpx', () => { 29 | expect(utils.transformRpx('width: 123rpx;')).toBe('width: 123px;') 30 | expect(utils.transformRpx('width: aaarpx;')).toBe('width: aaarpx;') 31 | expect(utils.transformRpx('width: 123px;')).toBe('width: 123px;') 32 | expect(utils.transformRpx('width: 12.3rpx;')).toBe('width: 12.3px;') 33 | expect(utils.transformRpx('width: 0.3rpx;')).toBe('width: 0.3px;') 34 | }) 35 | 36 | test('dashToCamelCase', () => { 37 | expect(utils.dashToCamelCase('abc-e')).toBe('abcE') 38 | expect(utils.dashToCamelCase('aBcDE-f')).toBe('aBcDEF') 39 | expect(utils.dashToCamelCase('a-bC-d-e-Fg')).toBe('aBCDE-Fg') 40 | }) 41 | 42 | test('camelToDashCase', () => { 43 | expect(utils.camelToDashCase('abcE')).toBe('abc-e') 44 | expect(utils.camelToDashCase('aBcDEF')).toBe('a-bc-d-e-f') 45 | expect(utils.camelToDashCase('aBCDE-Fg')).toBe('a-b-c-d-e--fg') 46 | }) 47 | 48 | test('animationToStyle', () => { 49 | let animation = _.createAnimation().rotate(45).scale(2, 2).translate(11) 50 | .skew(9) 51 | .step() 52 | .export() 53 | expect(utils.animationToStyle(animation.actions[0])).toEqual({ 54 | style: {}, 55 | transform: 'rotate(45deg) scale(2,2) translate(11px,0px) skew(9deg,0deg)', 56 | transformOrigin: '50% 50% 0', 57 | transition: '400ms linear 0ms', 58 | transitionProperty: 'transform', 59 | }) 60 | 61 | animation = _.createAnimation() 62 | .rotateX(180).rotateY(30).rotateZ(45) 63 | .scaleX(0.5) 64 | .scaleY(2) 65 | .scaleZ(2) 66 | .translateX(12) 67 | .translateY(34) 68 | .translateZ(56) 69 | .skewX(8) 70 | .skewY(9) 71 | .width(20) 72 | .left('5rpx') 73 | .step() 74 | .export() 75 | expect(utils.animationToStyle(animation.actions[0])).toEqual({ 76 | style: {left: '5rpx', width: '20px'}, 77 | transform: 'rotateX(180deg) rotateY(30deg) rotateZ(45deg) scaleX(0.5) scaleY(2) scaleZ(2) translateX(12px) translateY(34px) translateZ(56px) skewX(8deg) skewY(9deg)', 78 | transformOrigin: '50% 50% 0', 79 | transition: '400ms linear 0ms', 80 | transitionProperty: 'transform,width,left', 81 | }) 82 | 83 | animation = _.createAnimation().rotate3d(20, 30, 40).scale3d(1, 2, 0.5).translate3d(20, 44, 56) 84 | .step() 85 | .export() 86 | expect(utils.animationToStyle(animation.actions[0])).toEqual({ 87 | style: {}, 88 | transform: 'rotate3d(20,30,40,0deg) scale3d(1,2,0.5) translate3d(20px,44px,56px)', 89 | transformOrigin: '50% 50% 0', 90 | transition: '400ms linear 0ms', 91 | transitionProperty: 'transform', 92 | }) 93 | 94 | animation = _.createAnimation().matrix(1, 2, -1, 1, 80, 80).step().export() 95 | expect(utils.animationToStyle(animation.actions[0])).toEqual({ 96 | style: {}, 97 | transform: 'matrix(1,2,-1,1,80,80)', 98 | transformOrigin: '50% 50% 0', 99 | transition: '400ms linear 0ms', 100 | transitionProperty: 'transform', 101 | }) 102 | 103 | animation = _.createAnimation().matrix3d(0.85, 0.5, 0.15, 0, -0.5, 0.7, 0.5, 0, 0.15, -0.5, 0.85, 0, 22.63, -20.32, 101.37, 1).step().export() 104 | expect(utils.animationToStyle(animation.actions[0])).toEqual({ 105 | style: {}, 106 | transform: 'matrix3d(0.85,0.5,0.15,0,-0.5,0.7,0.5,0,0.15,-0.5,0.85,0,22.63,-20.32,101.37,1)', 107 | transformOrigin: '50% 50% 0', 108 | transition: '400ms linear 0ms', 109 | transitionProperty: 'transform', 110 | }) 111 | 112 | expect(utils.animationToStyle({})).toEqual({ 113 | transformOrigin: '', 114 | transform: '', 115 | transition: '', 116 | }) 117 | }) 118 | 119 | test('adjustExparserDefinition', () => { 120 | expect(utils.adjustExparserDefinition({})).toEqual({}) 121 | expect(utils.adjustExparserDefinition({ 122 | properties: { 123 | a: null, 124 | b: Number, 125 | c: String, 126 | d: Boolean, 127 | e: Array, 128 | f: Object, 129 | g: { 130 | type: Number, 131 | value: 123, 132 | }, 133 | h: { 134 | public: true, 135 | type: Number, 136 | value: 123, 137 | }, 138 | i: { 139 | public: false, 140 | type: Number, 141 | value: 123, 142 | }, 143 | j: { 144 | type: null, 145 | value: 123, 146 | }, 147 | }, 148 | })).toEqual({ 149 | properties: { 150 | a: {type: null}, 151 | b: {type: Number}, 152 | c: {type: String}, 153 | d: {type: Boolean}, 154 | e: {type: Array}, 155 | f: {type: Object}, 156 | g: { 157 | type: Number, 158 | value: 123, 159 | }, 160 | h: { 161 | type: Number, 162 | value: 123, 163 | }, 164 | i: { 165 | public: false, 166 | type: Number, 167 | value: 123, 168 | }, 169 | j: { 170 | type: null, 171 | value: 123, 172 | }, 173 | }, 174 | }) 175 | }) 176 | 177 | test('setTagName/getTagName', () => { 178 | utils.setTagName(1, 'abc') 179 | expect(utils.getTagName(1)).toBe('abc') 180 | }) 181 | 182 | test('normalizeAbsolute', () => { 183 | expect(utils.normalizeAbsolute('E:\\abc\\edf.xxx')).toBe('E:/abc/edf.xxx') 184 | expect(utils.normalizeAbsolute('E:\\\\abc\\edf.xxx')).toBe('E:/abc/edf.xxx') 185 | expect(utils.normalizeAbsolute('E:\\abc\\edf.xxx\\')).toBe('E:/abc/edf.xxx') 186 | expect(utils.normalizeAbsolute('E:/abc/edf.xxx')).toBe('E:/abc/edf.xxx') 187 | expect(utils.normalizeAbsolute('E:/abc/edf.xxx/')).toBe('E:/abc/edf.xxx') 188 | expect(utils.normalizeAbsolute('E://abc//edf.xxx')).toBe('E:/abc/edf.xxx') 189 | expect(utils.normalizeAbsolute('E:/\\/abc//edf.xxx/\\as/df\\/d\\')).toBe('E:/abc/edf.xxx/as/df/d') 190 | }) 191 | 192 | test('relativeToAbsolute', () => { 193 | expect(utils.relativeToAbsolute('E:/abc/edf.xxx/as/df/d', '/abc/dd.haha')).toBe('E:/abc/edf.xxx/as/df/abc/dd.haha') 194 | expect(utils.relativeToAbsolute('E:/abc/edf.xxx/as/df/d', 'abc/dd.haha')).toBe('E:/abc/edf.xxx/as/df/abc/dd.haha') 195 | expect(utils.relativeToAbsolute('E:/abc/edf.xxx/as/df/d', './abc/dd.haha')).toBe('E:/abc/edf.xxx/as/df/abc/dd.haha') 196 | expect(utils.relativeToAbsolute('E:/abc/edf.xxx/as/df/d', '../abc/dd.haha')).toBe('E:/abc/edf.xxx/as/abc/dd.haha') 197 | expect(utils.relativeToAbsolute('E:/abc/edf.xxx/as/df/d', '../abc/./dd.haha')).toBe('E:/abc/edf.xxx/as/abc/dd.haha') 198 | expect(utils.relativeToAbsolute('E:/abc/edf.xxx/as/df/d', '../abc/../dd.haha')).toBe('E:/abc/edf.xxx/as/dd.haha') 199 | }) 200 | -------------------------------------------------------------------------------- /test/wxml/comp.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /test/wxml/comp.wxml: -------------------------------------------------------------------------------- 1 | 2 | {{aa}} I am comp 3 | 4 | -------------------------------------------------------------------------------- /test/wxml/comp.wxss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/j-component/d8203de72242266063691f2e01e8114bd3433f10/test/wxml/comp.wxss -------------------------------------------------------------------------------- /test/wxml/foot.wxml: -------------------------------------------------------------------------------- 1 | foot -------------------------------------------------------------------------------- /test/wxml/head.wxml: -------------------------------------------------------------------------------- 1 | head -------------------------------------------------------------------------------- /test/wxml/index.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | data: { 3 | tmplData: { 4 | index: 7, 5 | msg: 'I am msg', 6 | time: '12345' 7 | }, 8 | flag: true, 9 | elseData: 'else content', 10 | attrValue: 'I am attr value', 11 | content: 'node content', 12 | aa: 'haha', 13 | list: [ 14 | {id: 1, name: 1}, 15 | {id: 2, name: 2}, 16 | {id: 3, name: 3} 17 | ], 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /test/wxml/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": { 4 | "comp": "./comp" 5 | } 6 | } -------------------------------------------------------------------------------- /test/wxml/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 |