├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json ├── src ├── index.js ├── index.json ├── index.wxml └── index.wxss ├── test ├── index.test.js ├── utils.js └── wx.test.js └── tools ├── build.js ├── checkcomponents.js ├── config.js ├── test └── helper.js └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["module-resolver", { 4 | "root": ["./src"], 5 | "alias": {} 6 | }] 7 | ], 8 | "presets": [ 9 | ["env", {"loose": true, "modules": "commonjs"}] 10 | ] 11 | } -------------------------------------------------------------------------------- /.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 | 'no-console': 'off', 25 | "class-methods-use-this": "off", 26 | 'arrow-parens': 'off', 27 | 'comma-dangle': [ 28 | 'error', 29 | 'only-multiline' 30 | ], 31 | 'complexity': ['error', 10], 32 | 'func-names': 'off', 33 | 'global-require': 'off', 34 | 'handle-callback-err': [ 35 | 'error', 36 | '^(err|error)$' 37 | ], 38 | 'import/no-unresolved': [ 39 | 'error', 40 | { 41 | 'caseSensitive': true, 42 | 'commonjs': true, 43 | 'ignore': ['^[^.]'] 44 | } 45 | ], 46 | 'import/prefer-default-export': 'off', 47 | 'linebreak-style': 'off', 48 | 'no-catch-shadow': 'error', 49 | 'no-continue': 'off', 50 | 'no-div-regex': 'warn', 51 | 'no-else-return': 'off', 52 | 'no-param-reassign': 'off', 53 | 'no-plusplus': 'off', 54 | 'no-shadow': 'off', 55 | 'no-multi-assign': 'off', 56 | 'no-underscore-dangle': 'off', 57 | 'node/no-deprecated-api': 'error', 58 | 'node/process-exit-as-throw': 'error', 59 | 'object-curly-spacing': [ 60 | 'error', 61 | 'never' 62 | ], 63 | 'operator-linebreak': [ 64 | 'error', 65 | 'after', 66 | { 67 | 'overrides': { 68 | ':': 'before', 69 | '?': 'before' 70 | } 71 | } 72 | ], 73 | 'prefer-arrow-callback': 'off', 74 | 'prefer-destructuring': 'off', 75 | 'prefer-template': 'off', 76 | 'quote-props': [ 77 | 1, 78 | 'as-needed', 79 | { 80 | 'unnecessary': true 81 | } 82 | ], 83 | 'semi': [ 84 | 'error', 85 | 'never' 86 | ] 87 | }, 88 | 'globals': { 89 | 'window': true, 90 | 'document': true, 91 | 'App': true, 92 | 'Page': true, 93 | 'Component': true, 94 | 'Behavior': true, 95 | 'wx': true, 96 | 'getCurrentPages': true, 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | miniprogram_dist 12 | miniprogram_dev 13 | node_modules 14 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | test 12 | tools 13 | docs 14 | miniprogram_dev 15 | node_modules 16 | coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 sima.zhang1990@gmail.com 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 | # @antv/f2-canvas 2 | 3 | 微信小程序 F2 图表组件 4 | 5 | ## 安装 6 | 7 | ```bash 8 | npm i @antv/f2-canvas 9 | ``` 10 | 11 | ## 快速开始 12 | 13 | 下面我们就开始使用 `f2-canvas` 组件来绘制图表吧,这里假设用户已经初步了解微信小程序的基础框架,如不了解,请先阅读官网教程: [官方教程](https://developers.weixin.qq.com/miniprogram/dev/index.html)。 14 | 15 | 以绘制柱状图为例: 16 | 17 | 18 | 19 | * STEP 1:在 pages 目录下新建 column 目录,该目录需要包含以下几个文件: 20 | + index.js 21 | + index.json 22 | + index.wxml 23 | + index.wxss 24 | 25 | 各个文件的内容如下: 26 | 27 | + `index.json` 配置文件,引入 f2-canvas 组件,由于微信小程序组件名不允许包含数字,所以这里将其命名为 ff-canvas 28 | 29 | ```js 30 | // index.json 31 | { 32 | "usingComponents": { 33 | "ff-canvas": "@antv/f2-canvas" 34 | } 35 | } 36 | ``` 37 | 38 | + `index.wxml` 视图,使用 ff-canvas 组件,其中 `opts` 是一个我们在 index.js 中定义的对象,**必设属性**,它使得图表能够在页面加载后被初始化并设置,详见 index.js 中的使用。 39 | 40 | ```html 41 | 42 | 43 | 44 | 45 | ``` 46 | 47 | + `index.js` 逻辑处理,这里还需要引入 F2 用于绘制图表,结构如下,注意路径正确。 48 | 49 | ```js 50 | // index.js 51 | import F2 from '@antv/wx-f2'; // 注:也可以不引入, initChart 方法已经将 F2 传入,如果需要引入,注意需要安装 @antv/wx-f2 依赖 52 | 53 | let chart = null; 54 | 55 | function initChart(canvas, width, height, F2) { // 使用 F2 绘制图表 56 | const data = [ 57 | { year: '1951 年', sales: 38 }, 58 | { year: '1952 年', sales: 52 }, 59 | { year: '1956 年', sales: 61 }, 60 | { year: '1957 年', sales: 145 }, 61 | { year: '1958 年', sales: 48 }, 62 | { year: '1959 年', sales: 38 }, 63 | { year: '1960 年', sales: 38 }, 64 | { year: '1962 年', sales: 38 }, 65 | ]; 66 | chart = new F2.Chart({ 67 | el: canvas, 68 | width, 69 | height 70 | }); 71 | 72 | chart.source(data, { 73 | sales: { 74 | tickCount: 5 75 | } 76 | }); 77 | chart.tooltip({ 78 | showItemMarker: false, 79 | onShow(ev) { 80 | const { items } = ev; 81 | items[0].name = null; 82 | items[0].name = items[0].title; 83 | items[0].value = '¥ ' + items[0].value; 84 | } 85 | }); 86 | chart.interval().position('year*sales'); 87 | chart.render(); 88 | return chart; 89 | } 90 | 91 | Page({ 92 | data: { 93 | opts: { 94 | onInit: initChart 95 | } 96 | }, 97 | 98 | onReady() { 99 | } 100 | }); 101 | ``` 102 | 103 | 由于 f2-canvas 组件主要是对小程序的画布上下文和 html5 canvas 的上下文进行了适配以支持 F2 在小程序端的渲染,所以 **F2 能绘制什么图表,小程序端就能绘制什么图表**,使用时也只需按照 F2 的语法来写即可。 104 | 105 | 本项目只展示了部分 demos,更详细的请见 [AntV F2](https://antv.alipay.com/zh-cn/f2/3.x/demo/index.html)。 106 | 107 | 需要注意的是,在创建 chart 的时候,需要使用 'el' 属性来指定容器,对应 `this.data.opts.onInit` 方法形参中的 `canvas` 属性,另外该方法还会返回 `width`, `height` 属性分别对应画布的宽和高。 108 | 109 | ```js 110 | chart = new F2.Chart({ 111 | el: canvas, 112 | width, 113 | height 114 | }); 115 | ``` 116 | 117 | 118 | ## 不支持的功能 119 | 120 | 目前由于小程序不支持 document,所以 Guide.Html 辅助元素组件目前仍无法使用,其他 F2 的功能全部支持。 121 | 122 | 123 | ## 微信版本要求 124 | 125 | * 微信版本 **>= 6.6.3** 126 | * 基础库版本 **>= 2.2.1** 127 | * 开发者工具版本 **>= 1.02.1808300** 128 | 129 | ## 常见问题 130 | 131 | ### 问题 1 132 | 按照 demo 实例操作,但是报如下错误: 133 | 134 | ```js 135 | VM4466:1 TypeError: Cannot read property 'defaultView' of undefined 136 | at Object.getStyle (f2.js? [sm]:1) 137 | at Object.getWidth (f2.js? [sm]:1) 138 | at t._initCanvas (f2.js? [sm]:1) 139 | at new t (f2.js? [sm]:1) 140 | at e._initCanvas (f2.js? [sm]:1) 141 | at e._init (f2.js? [sm]:1) 142 | at new e (f2.js? [sm]:1) 143 | at Object.initChart [as onInit] (test.js? [sm]:18) 144 | at t. (f2-canvas.js? [sm]:94) 145 | at WAService.js:12 146 | ``` 147 | 148 | **解决** 149 | 检查是否有在 .wxss 文件中为 ff-canvas 组件定义 width 和 height 样式属性,如没有,加上即可,如此代码所示:https://github.com/antvis/wx-f2/blob/master/app.wxss#L16 150 | 151 | ## 如何贡献 152 | 153 | 如果您在使用的过程中碰到问题,可以先通过 [issues](https://github.com/antvis/f2-canvas/issues) 看看有没有类似的 bug 或者建议。 154 | 155 | ## License 156 | 157 | [MIT license](https://github.com/antvis/f2-canvas/blob/master/LICENSE) 158 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp') 2 | const clean = require('gulp-clean') 3 | 4 | const config = require('./tools/config') 5 | const BuildTask = require('./tools/build') 6 | const id = require('./package.json').name || 'miniprogram-custom-component' 7 | 8 | // build task instance 9 | // eslint-disable-next-line no-new 10 | new BuildTask(id, config.entry) 11 | 12 | // clean the generated folders and files 13 | gulp.task('clean', gulp.series(() => gulp.src(config.distPath, {read: false, allowEmpty: true}).pipe(clean()), done => { 14 | if (config.isDev) { 15 | return gulp.src(config.demoDist, {read: false, allowEmpty: true}) 16 | .pipe(clean()) 17 | } 18 | 19 | return done() 20 | })) 21 | // watch files and build 22 | gulp.task('watch', gulp.series(`${id}-watch`)) 23 | // build for develop 24 | gulp.task('dev', gulp.series(`${id}-dev`)) 25 | // build for publish 26 | gulp.task('default', gulp.series(`${id}-default`)) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/f2-canvas", 3 | "version": "1.0.3", 4 | "description": "微信小程序 F2 图表组件", 5 | "main": "miniprogram_dist/index.js", 6 | "scripts": { 7 | "dev": "gulp dev --develop", 8 | "watch": "gulp watch --develop --watch", 9 | "build": "gulp", 10 | "dist": "npm run build", 11 | "clean-dev": "gulp clean --develop", 12 | "clean": "gulp clean", 13 | "test": "jest ./test/* --silent --bail", 14 | "coverage": "jest ./test/* --coverage --bail", 15 | "lint": "eslint \"src/**/*.js\"", 16 | "lint-tools": "eslint \"tools/**/*.js\" --rule \"import/no-extraneous-dependencies: false\"" 17 | }, 18 | "miniprogram": "miniprogram_dist", 19 | "jest": { 20 | "testEnvironment": "jsdom", 21 | "testURL": "https://jest.test", 22 | "collectCoverageFrom": [ 23 | "src/**/*.js" 24 | ], 25 | "moduleDirectories": [ 26 | "node_modules", 27 | "src" 28 | ] 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/simaQ/f2-canvas.git" 33 | }, 34 | "author": "sima.zhang1990@gmail.com", 35 | "license": "MIT", 36 | "devDependencies": { 37 | "babel-core": "^6.26.3", 38 | "babel-loader": "^7.1.5", 39 | "babel-plugin-module-resolver": "^3.1.1", 40 | "babel-preset-env": "^1.7.0", 41 | "colors": "^1.3.1", 42 | "eslint": "^5.3.0", 43 | "eslint-loader": "^2.1.0", 44 | "gulp": "^4.0.0", 45 | "gulp-clean": "^0.4.0", 46 | "gulp-if": "^2.0.2", 47 | "gulp-install": "^1.1.0", 48 | "gulp-less": "^3.5.0", 49 | "gulp-rename": "^1.4.0", 50 | "gulp-sourcemaps": "^2.6.4", 51 | "j-component": "git+https://github.com/JuneAndGreen/j-component.git", 52 | "jest": "^23.5.0", 53 | "through2": "^2.0.3", 54 | "webpack": "^4.16.5", 55 | "webpack-node-externals": "^1.7.2", 56 | "eslint-config-airbnb-base": "13.1.0", 57 | "eslint-plugin-import": "^2.14.0", 58 | "eslint-plugin-node": "^7.0.1", 59 | "eslint-plugin-promise": "^3.8.0" 60 | }, 61 | "dependencies": { 62 | "@antv/wx-f2": "~1.0.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // f2-canvas.js 2 | import F2 from '@antv/wx-f2' 3 | 4 | Component({ 5 | /** 6 | * 组件的属性列表 7 | */ 8 | properties: { 9 | canvasId: { 10 | type: String, 11 | value: 'f2-canvas' 12 | }, 13 | 14 | opts: { 15 | type: Object 16 | } 17 | }, 18 | 19 | /** 20 | * 组件的初始数据 21 | */ 22 | data: { 23 | 24 | }, 25 | 26 | ready() { 27 | if (!this.data.opts) { 28 | console.warn('组件需绑定 opts 变量,例:') 29 | return 30 | } 31 | 32 | if (!this.data.opts.lazyLoad) { 33 | this.init() 34 | } 35 | }, 36 | 37 | /** 38 | * 组件的方法列表 39 | */ 40 | methods: { 41 | init(callback) { 42 | const version = wx.version.version.split('.').map(n => parseInt(n, 10)) 43 | const isValid = version[0] > 1 || (version[0] === 1 && version[1] > 9) || 44 | (version[0] === 1 && version[1] === 9 && version[2] >= 91) 45 | if (!isValid) { 46 | console.error('微信基础库版本过低,需大于等于 1.9.91。') 47 | return 48 | } 49 | 50 | const ctx = wx.createCanvasContext(this.data.canvasId, this) // 获取小程序上下文 51 | const canvas = new F2.Renderer(ctx) 52 | this.canvas = canvas 53 | 54 | const query = wx.createSelectorQuery().in(this) 55 | query.select('.f2-canvas').boundingClientRect(res => { 56 | if (typeof callback === 'function') { 57 | this.chart = callback(canvas, res.width, res.height, F2) 58 | } else if (this.data.opts && this.data.opts.onInit) { 59 | this.chart = this.data.opts.onInit(canvas, res.width, res.height, F2) 60 | } 61 | }).exec() 62 | }, 63 | touchStart(e) { 64 | if (this.canvas) { 65 | this.canvas.emitEvent('touchstart', [e]) 66 | } 67 | }, 68 | touchMove(e) { 69 | if (this.canvas) { 70 | this.canvas.emitEvent('touchmove', [e]) 71 | } 72 | }, 73 | touchEnd(e) { 74 | if (this.canvas) { 75 | this.canvas.emitEvent('touchend', [e]) 76 | } 77 | }, 78 | press(e) { 79 | if (this.canvas) { 80 | this.canvas.emitEvent('press', [e]) 81 | } 82 | } 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /src/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /src/index.wxml: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/index.wxss: -------------------------------------------------------------------------------- 1 | /* f2-canvas.wxss */ 2 | .f2-canvas { 3 | width: 100%; 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | 3 | let componentId 4 | let component 5 | 6 | beforeAll(async () => { 7 | componentId = await _.load('index', 'comp') 8 | }) 9 | 10 | test('render', async () => { 11 | component = _.render(componentId, {prop: 'index.test.properties'}) 12 | 13 | const parent = document.createElement('parent-wrapper') 14 | component.attach(parent) 15 | 16 | expect(_.match(component.dom, 'index.test.properties')).toBe(true) 17 | }) 18 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../tools/test/helper') 2 | 3 | const noop = () => {}; 4 | 5 | global.wx = { 6 | request: noop, 7 | 8 | uploadFile: noop, 9 | downloadFile: noop, 10 | 11 | connectSocket: noop, 12 | onSocketOpen: noop, 13 | onSocketError: noop, 14 | sendSocketMessage: noop, 15 | onSocketMessage: noop, 16 | closeSocket: noop, 17 | onSocketClose: noop, 18 | 19 | chooseImage: noop, 20 | previewImage: noop, 21 | getImageInfo: noop, 22 | saveImageToPhotosAlbum: noop, 23 | 24 | startRecord: noop, 25 | stopRecord: noop, 26 | 27 | getRecorderManager: noop, 28 | 29 | playVoice: noop, 30 | pauseVoice: noop, 31 | stopVoice: noop, 32 | 33 | getBackgroundAudioPlayerState: noop, 34 | playBackgroundAudio: noop, 35 | pauseBackgroundAudio: noop, 36 | seekBackgroundAudio: noop, 37 | stopBackgroundAudio: noop, 38 | onBackgroundAudioPlay: noop, 39 | onBackgroundAudioPause: noop, 40 | onBackgroundAudioStop: noop, 41 | 42 | getBackgroundAudioManager: noop, 43 | 44 | createAudioContext: noop, 45 | createInnerAudioContext: noop, 46 | getAvailableAudioSources: noop, 47 | 48 | chooseVideo: noop, 49 | saveVideoToPhotosAlbum: noop, 50 | 51 | createVideoContext: noop, 52 | 53 | createCameraContext: noop, 54 | 55 | createLivePlayerContext: noop, 56 | createLivePusherContext: noop, 57 | 58 | loadFontFace: noop, 59 | 60 | saveFile: noop, 61 | getFileInfo: noop, 62 | getSavedFileList: noop, 63 | getSavedFileInfo: noop, 64 | removeSavedFile: noop, 65 | openDocument: noop, 66 | 67 | setStorage: noop, 68 | setStorageSync: noop, 69 | getStorage: noop, 70 | getStorageSync: noop, 71 | getStorageInfo: noop, 72 | getStorageInfoSync: noop, 73 | removeStorage: noop, 74 | removeStorageSync: noop, 75 | clearStorage: noop, 76 | clearStorageSync: noop, 77 | 78 | getLocation: noop, 79 | chooseLocation: noop, 80 | 81 | openLocation: noop, 82 | 83 | createMapContext: noop, 84 | 85 | getSystemInfo(options = {}) { 86 | const res = { 87 | errMsg: 'getSystemInfo:ok', 88 | SDKVersion: '2.3.0', 89 | batteryLevel: 100, 90 | benchmarkLevel: 1, 91 | brand: 'devtools', 92 | fontSizeSetting: 16, 93 | language: 'zh_CN', 94 | model: 'iPhone 7 Plus', 95 | pixelRatio: 3, 96 | platform: 'devtools', 97 | screenHeight: 736, 98 | screenWidth: 414, 99 | statusBarHeight: 20, 100 | system: 'iOS 10.0.1', 101 | version: '6.6.3', 102 | windowHeight: 672, 103 | windowWidth: 414, 104 | } 105 | typeof options.success === 'function' && options.success(res) 106 | typeof options.complete === 'function' && options.complete(res) 107 | }, 108 | getSystemInfoSync() { 109 | let info = {} 110 | wx.getSystemInfo({ 111 | success(res) { 112 | info = res 113 | } 114 | }) 115 | 116 | delete info.errMsg 117 | return info 118 | }, 119 | canIUse: noop, 120 | 121 | onMemoryWarning: noop, 122 | 123 | getNetworkType: noop, 124 | onNetworkStatusChange: noop, 125 | 126 | onAccelerometerChange: noop, 127 | startAccelerometer: noop, 128 | stopAccelerometer: noop, 129 | 130 | onCompassChange: noop, 131 | startCompass: noop, 132 | stopCompass: noop, 133 | 134 | makePhoneCall: noop, 135 | 136 | scanCode: noop, 137 | 138 | setClipboardData: noop, 139 | getClipboardData: noop, 140 | 141 | openBluetoothAdapter: noop, 142 | closeBluetoothAdapter: noop, 143 | getBluetoothAdapterState: noop, 144 | onBluetoothAdapterStateChange: noop, 145 | startBluetoothDevicesDiscovery: noop, 146 | stopBluetoothDevicesDiscovery: noop, 147 | getBluetoothDevices: noop, 148 | getConnectedBluetoothDevices: noop, 149 | onBluetoothDeviceFound: noop, 150 | createBLEConnection: noop, 151 | closeBLEConnection: noop, 152 | getBLEDeviceServices: noop, 153 | getBLEDeviceCharacteristics: noop, 154 | readBLECharacteristicValue: noop, 155 | writeBLECharacteristicValue: noop, 156 | notifyBLECharacteristicValueChange: noop, 157 | onBLEConnectionStateChange: noop, 158 | onBLECharacteristicValueChange: noop, 159 | 160 | startBeaconDiscovery: noop, 161 | stopBeaconDiscovery: noop, 162 | getBeacons: noop, 163 | onBeaconUpdate: noop, 164 | onBeaconServiceChange: noop, 165 | 166 | setScreenBrightness: noop, 167 | getScreenBrightness: noop, 168 | setKeepScreenOn: noop, 169 | 170 | onUserCaptureScreen: noop, 171 | 172 | vibrateLong: noop, 173 | vibrateShort: noop, 174 | 175 | addPhoneContact: noop, 176 | 177 | getHCEState: noop, 178 | startHCE: noop, 179 | stopHCE: noop, 180 | onHCEMessage: noop, 181 | sendHCEMessage: noop, 182 | 183 | startWifi: noop, 184 | stopWifi: noop, 185 | connectWifi: noop, 186 | getWifiList: noop, 187 | onGetWifiList: noop, 188 | setWifiList: noop, 189 | onWifiConnected: noop, 190 | getConnectedWifi: noop, 191 | 192 | showToast: noop, 193 | showLoading: noop, 194 | hideToast: noop, 195 | hideLoading: noop, 196 | showModal: noop, 197 | showActionSheet: noop, 198 | 199 | setNavigationBarTitle: noop, 200 | showNavigationBarLoading: noop, 201 | hideNavigationBarLoading: noop, 202 | setNavigationBarColor: noop, 203 | 204 | setTabBarBadge: noop, 205 | removeTabBarBadge: noop, 206 | showTabBarRedDot: noop, 207 | hideTabBarRedDot: noop, 208 | setTabBarStyle: noop, 209 | setTabBarItem: noop, 210 | showTabBar: noop, 211 | hideTabBar: noop, 212 | 213 | setBackgroundColor: noop, 214 | setBackgroundTextStyle: noop, 215 | 216 | setTopBarText: noop, 217 | 218 | navigateTo: noop, 219 | redirectTo: noop, 220 | switchTab: noop, 221 | navigateBack: noop, 222 | reLaunch: noop, 223 | 224 | createAnimation: noop, 225 | 226 | pageScrollTo: noop, 227 | 228 | createCanvasContext: noop, 229 | createContext: noop, 230 | drawCanvas: noop, 231 | canvasToTempFilePath: noop, 232 | canvasGetImageData: noop, 233 | canvasPutImageData: noop, 234 | 235 | startPullDownRefresh: noop, 236 | stopPullDownRefresh: noop, 237 | 238 | createSelectorQuery: noop, 239 | 240 | createIntersectionObserver: noop, 241 | 242 | getExtConfig: noop, 243 | getExtConfigSync: noop, 244 | 245 | login: noop, 246 | checkSession: noop, 247 | 248 | authorize: noop, 249 | 250 | getUserInfo: noop, 251 | 252 | requestPayment: noop, 253 | 254 | showShareMenu: noop, 255 | hideShareMenu: noop, 256 | updateShareMenu: noop, 257 | getShareInfo: noop, 258 | 259 | chooseAddress: noop, 260 | 261 | addCard: noop, 262 | openCard: noop, 263 | 264 | openSetting: noop, 265 | getSetting: noop, 266 | 267 | getWeRunData: noop, 268 | 269 | getAccountInfoSync: noop, 270 | 271 | navigateToMiniProgram: noop, 272 | navigateBackMiniProgram: noop, 273 | 274 | chooseInvoiceTitle: noop, 275 | 276 | checkIsSupportSoterAuthentication: noop, 277 | startSoterAuthentication: noop, 278 | checkIsSoterEnrolledInDevice: noop, 279 | 280 | getUpdateManager: noop, 281 | 282 | createWorker: noop, 283 | 284 | getLogManager: noop, 285 | 286 | reportMonitor: noop, 287 | 288 | setEnableDebug: noop, 289 | } 290 | -------------------------------------------------------------------------------- /test/wx.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | 3 | test('wx.getSystemInfo', async () => { 4 | wx.getSystemInfo({ 5 | success(res) { 6 | expect(res.errMsg).toBe('getSystemInfo:ok') 7 | }, 8 | complete(res) { 9 | expect(res.errMsg).toBe('getSystemInfo:ok') 10 | }, 11 | }) 12 | }) 13 | 14 | test('wx.getSystemInfoSync', async () => { 15 | const info = wx.getSystemInfoSync() 16 | expect(info.SDKVersion).toBe('2.3.0') 17 | }) 18 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const gulp = require('gulp') 4 | const clean = require('gulp-clean') 5 | const less = require('gulp-less') 6 | const rename = require('gulp-rename') 7 | const gulpif = require('gulp-if') 8 | const sourcemaps = require('gulp-sourcemaps') 9 | const webpack = require('webpack') 10 | const gulpInstall = require('gulp-install') 11 | 12 | const config = require('./config') 13 | const checkComponents = require('./checkcomponents') 14 | const _ = require('./utils') 15 | 16 | const wxssConfig = config.wxss || {} 17 | const srcPath = config.srcPath 18 | const distPath = config.distPath 19 | 20 | /** 21 | * get wxss stream 22 | */ 23 | function wxss(wxssFileList) { 24 | if (!wxssFileList.length) return false 25 | 26 | return gulp.src(wxssFileList, {cwd: srcPath, base: srcPath}) 27 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.init())) 28 | .pipe(gulpif(wxssConfig.less, less({paths: [srcPath]}))) 29 | .pipe(rename({extname: '.wxss'})) 30 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.write('./'))) 31 | .pipe(_.logger(wxssConfig.less ? 'generate' : undefined)) 32 | .pipe(gulp.dest(distPath)) 33 | } 34 | 35 | /** 36 | * get js stream 37 | */ 38 | function js(jsFileMap, scope) { 39 | const webpackConfig = config.webpack 40 | const webpackCallback = (err, stats) => { 41 | if (!err) { 42 | // eslint-disable-next-line no-console 43 | console.log(stats.toString({ 44 | assets: true, 45 | cached: false, 46 | colors: true, 47 | children: false, 48 | errors: true, 49 | warnings: true, 50 | version: true, 51 | modules: false, 52 | publicPath: true, 53 | })) 54 | } else { 55 | // eslint-disable-next-line no-console 56 | console.log(err) 57 | } 58 | } 59 | 60 | webpackConfig.entry = jsFileMap 61 | webpackConfig.output.path = distPath 62 | 63 | if (scope.webpackWatcher) { 64 | scope.webpackWatcher.close() 65 | scope.webpackWatcher = null 66 | } 67 | 68 | if (config.isWatch) { 69 | scope.webpackWatcher = webpack(webpackConfig).watch({ 70 | ignored: /node_modules/, 71 | }, webpackCallback) 72 | } else { 73 | webpack(webpackConfig).run(webpackCallback) 74 | } 75 | } 76 | 77 | /** 78 | * copy file 79 | */ 80 | function copy(copyFileList) { 81 | if (!copyFileList.length) return false 82 | 83 | return gulp.src(copyFileList, {cwd: srcPath, base: srcPath}) 84 | .pipe(_.logger()) 85 | .pipe(gulp.dest(distPath)) 86 | } 87 | 88 | /** 89 | * install packages 90 | */ 91 | function install() { 92 | return gulp.series(async () => { 93 | const demoDist = config.demoDist 94 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 95 | const packageJson = _.readJson(path.resolve(__dirname, '../package.json')) 96 | const dependencies = packageJson.dependencies || {} 97 | 98 | await _.writeFile(demoPackageJsonPath, JSON.stringify({dependencies}, null, '\t')) // write dev demo's package.json 99 | }, () => { 100 | const demoDist = config.demoDist 101 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 102 | 103 | return gulp.src(demoPackageJsonPath) 104 | .pipe(gulpInstall({production: true})) 105 | }) 106 | } 107 | 108 | class BuildTask { 109 | constructor(id, entry) { 110 | if (!entry) return 111 | 112 | this.id = id 113 | this.entries = Array.isArray(config.entry) ? config.entry : [config.entry] 114 | this.copyList = Array.isArray(config.copy) ? config.copy : [] 115 | this.componentListMap = {} 116 | this.cachedComponentListMap = {} 117 | 118 | this.init() 119 | } 120 | 121 | init() { 122 | const id = this.id 123 | 124 | /** 125 | * clean the dist folder 126 | */ 127 | gulp.task(`${id}-clean-dist`, () => gulp.src(distPath, {read: false, allowEmpty: true}).pipe(clean())) 128 | 129 | /** 130 | * copy demo to the dev folder 131 | */ 132 | let isDemoExists = false 133 | gulp.task(`${id}-demo`, gulp.series(async () => { 134 | const demoDist = config.demoDist 135 | 136 | isDemoExists = await _.checkFileExists(path.join(demoDist, 'project.config.json')) 137 | }, done => { 138 | if (!isDemoExists) { 139 | const demoSrc = config.demoSrc 140 | const demoDist = config.demoDist 141 | 142 | return gulp.src('**/*', {cwd: demoSrc, base: demoSrc}) 143 | .pipe(gulp.dest(demoDist)) 144 | } 145 | 146 | return done() 147 | })) 148 | 149 | /** 150 | * install packages for dev 151 | */ 152 | gulp.task(`${id}-install`, install()) 153 | 154 | /** 155 | * check custom components 156 | */ 157 | gulp.task(`${id}-component-check`, async () => { 158 | const entries = this.entries 159 | const mergeComponentListMap = {} 160 | for (let i = 0, len = entries.length; i < len; i++) { 161 | let entry = entries[i] 162 | entry = path.join(srcPath, `${entry}.json`) 163 | // eslint-disable-next-line no-await-in-loop 164 | const newComponentListMap = await checkComponents(entry) 165 | 166 | _.merge(mergeComponentListMap, newComponentListMap) 167 | } 168 | 169 | this.cachedComponentListMap = this.componentListMap 170 | this.componentListMap = mergeComponentListMap 171 | }) 172 | 173 | /** 174 | * write json to the dist folder 175 | */ 176 | gulp.task(`${id}-component-json`, done => { 177 | const jsonFileList = this.componentListMap.jsonFileList 178 | 179 | if (jsonFileList && jsonFileList.length) { 180 | return copy(this.componentListMap.jsonFileList) 181 | } 182 | 183 | return done() 184 | }) 185 | 186 | /** 187 | * copy wxml to the dist folder 188 | */ 189 | gulp.task(`${id}-component-wxml`, done => { 190 | const wxmlFileList = this.componentListMap.wxmlFileList 191 | 192 | if (wxmlFileList && 193 | wxmlFileList.length && 194 | !_.compareArray(this.cachedComponentListMap.wxmlFileList, wxmlFileList)) { 195 | return copy(wxmlFileList) 196 | } 197 | 198 | return done() 199 | }) 200 | 201 | /** 202 | * generate wxss to the dist folder 203 | */ 204 | gulp.task(`${id}-component-wxss`, done => { 205 | const wxssFileList = this.componentListMap.wxssFileList 206 | 207 | if (wxssFileList && 208 | wxssFileList.length && 209 | !_.compareArray(this.cachedComponentListMap.wxssFileList, wxssFileList)) { 210 | return wxss(wxssFileList, srcPath, distPath) 211 | } 212 | 213 | return done() 214 | }) 215 | 216 | /** 217 | * generate js to the dist folder 218 | */ 219 | gulp.task(`${id}-component-js`, done => { 220 | const jsFileList = this.componentListMap.jsFileList 221 | 222 | if (jsFileList && 223 | jsFileList.length && 224 | !_.compareArray(this.cachedComponentListMap.jsFileList, jsFileList)) { 225 | js(this.componentListMap.jsFileMap, this) 226 | } 227 | 228 | return done() 229 | }) 230 | 231 | /** 232 | * copy resources to dist folder 233 | */ 234 | gulp.task(`${id}-copy`, gulp.parallel(done => { 235 | const copyList = this.copyList 236 | const copyFileList = copyList.map(dir => path.join(dir, '**/*.!(wxss)')) 237 | 238 | if (copyFileList.length) return copy(copyFileList) 239 | 240 | return done() 241 | }, done => { 242 | const copyList = this.copyList 243 | const copyFileList = copyList.map(dir => path.join(dir, '**/*.wxss')) 244 | 245 | if (copyFileList.length) return wxss(copyFileList, srcPath, distPath) 246 | 247 | return done() 248 | })) 249 | 250 | /** 251 | * watch json 252 | */ 253 | gulp.task(`${id}-watch-json`, () => gulp.watch(this.componentListMap.jsonFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-check`, gulp.parallel(`${id}-component-wxml`, `${id}-component-wxss`, `${id}-component-js`, `${id}-component-json`)))) 254 | 255 | /** 256 | * watch wxml 257 | */ 258 | gulp.task(`${id}-watch-wxml`, () => { 259 | this.cachedComponentListMap.wxmlFileList = null 260 | return gulp.watch(this.componentListMap.wxmlFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxml`)) 261 | }) 262 | 263 | /** 264 | * watch wxss 265 | */ 266 | gulp.task(`${id}-watch-wxss`, () => { 267 | this.cachedComponentListMap.wxssFileList = null 268 | return gulp.watch('**/*.wxss', {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxss`)) 269 | }) 270 | 271 | /** 272 | * watch resources 273 | */ 274 | gulp.task(`${id}-watch-copy`, () => { 275 | const copyList = this.copyList 276 | const copyFileList = copyList.map(dir => path.join(dir, '**/*')) 277 | const watchCallback = filePath => copy([filePath]) 278 | 279 | return gulp.watch(copyFileList, {cwd: srcPath, base: srcPath}) 280 | .on('change', watchCallback) 281 | .on('add', watchCallback) 282 | .on('unlink', watchCallback) 283 | }) 284 | 285 | /** 286 | * watch demo 287 | */ 288 | gulp.task(`${id}-watch-demo`, () => { 289 | const demoSrc = config.demoSrc 290 | const demoDist = config.demoDist 291 | const watchCallback = filePath => gulp.src(filePath, {cwd: demoSrc, base: demoSrc}) 292 | .pipe(gulp.dest(demoDist)) 293 | 294 | return gulp.watch('**/*', {cwd: demoSrc, base: demoSrc}) 295 | .on('change', watchCallback) 296 | .on('add', watchCallback) 297 | .on('unlink', watchCallback) 298 | }) 299 | 300 | /** 301 | * watch installed packages 302 | */ 303 | gulp.task(`${id}-watch-install`, () => gulp.watch(path.resolve(__dirname, '../package.json'), install())) 304 | 305 | /** 306 | * build custom component 307 | */ 308 | gulp.task(`${id}-build`, gulp.series(`${id}-clean-dist`, `${id}-component-check`, gulp.parallel(`${id}-component-wxml`, `${id}-component-wxss`, `${id}-component-js`, `${id}-component-json`, `${id}-copy`))) 309 | 310 | gulp.task(`${id}-watch`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`, gulp.parallel(`${id}-watch-wxml`, `${id}-watch-wxss`, `${id}-watch-json`, `${id}-watch-copy`, `${id}-watch-install`, `${id}-watch-demo`))) 311 | 312 | gulp.task(`${id}-dev`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`)) 313 | 314 | gulp.task(`${id}-default`, gulp.series(`${id}-build`)) 315 | } 316 | } 317 | 318 | module.exports = BuildTask 319 | -------------------------------------------------------------------------------- /tools/checkcomponents.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const _ = require('./utils') 4 | const config = require('./config') 5 | 6 | const srcPath = config.srcPath 7 | 8 | /** 9 | * get json path's info 10 | */ 11 | function getJsonPathInfo(jsonPath) { 12 | const dirPath = path.dirname(jsonPath) 13 | const fileName = path.basename(jsonPath, '.json') 14 | const relative = path.relative(srcPath, dirPath) 15 | const fileBase = path.join(relative, fileName) 16 | 17 | return { 18 | dirPath, fileName, relative, fileBase 19 | } 20 | } 21 | 22 | /** 23 | * check included components 24 | */ 25 | const checkProps = ['usingComponents', 'componentGenerics'] 26 | async function checkIncludedComponents(jsonPath, componentListMap) { 27 | const json = _.readJson(jsonPath) 28 | if (!json) throw new Error(`json is not valid: "${jsonPath}"`) 29 | 30 | const {dirPath, fileName, fileBase} = getJsonPathInfo(jsonPath) 31 | 32 | for (let i = 0, len = checkProps.length; i < len; i++) { 33 | const checkProp = checkProps[i] 34 | const checkPropValue = json[checkProp] || {} 35 | const keys = Object.keys(checkPropValue) 36 | 37 | for (let j = 0, jlen = keys.length; j < jlen; j++) { 38 | const key = keys[j] 39 | let value = typeof checkPropValue[key] === 'object' ? checkPropValue[key].default : checkPropValue[key] 40 | if (!value) continue 41 | 42 | value = _.transformPath(value, path.sep) 43 | 44 | // check relative path 45 | const componentPath = `${path.join(dirPath, value)}.json` 46 | // eslint-disable-next-line no-await-in-loop 47 | const isExists = await _.checkFileExists(componentPath) 48 | if (isExists) { 49 | // eslint-disable-next-line no-await-in-loop 50 | await checkIncludedComponents(componentPath, componentListMap) 51 | } 52 | } 53 | } 54 | 55 | // checked 56 | componentListMap.wxmlFileList.push(`${fileBase}.wxml`) 57 | componentListMap.wxssFileList.push(`${fileBase}.wxss`) 58 | componentListMap.jsonFileList.push(`${fileBase}.json`) 59 | componentListMap.jsFileList.push(`${fileBase}.js`) 60 | 61 | componentListMap.jsFileMap[fileBase] = `${path.join(dirPath, fileName)}.js` 62 | } 63 | 64 | module.exports = async function (entry) { 65 | const componentListMap = { 66 | wxmlFileList: [], 67 | wxssFileList: [], 68 | jsonFileList: [], 69 | jsFileList: [], 70 | 71 | jsFileMap: {}, // for webpack entry 72 | } 73 | 74 | const isExists = await _.checkFileExists(entry) 75 | if (!isExists) { 76 | const {dirPath, fileName, fileBase} = getJsonPathInfo(entry) 77 | 78 | componentListMap.jsFileList.push(`${fileBase}.js`) 79 | componentListMap.jsFileMap[fileBase] = `${path.join(dirPath, fileName)}.js` 80 | 81 | return componentListMap 82 | } 83 | 84 | await checkIncludedComponents(entry, componentListMap) 85 | 86 | return componentListMap 87 | } 88 | -------------------------------------------------------------------------------- /tools/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const webpack = require('webpack') 4 | const nodeExternals = require('webpack-node-externals') 5 | 6 | const isDev = process.argv.indexOf('--develop') >= 0 7 | const isWatch = process.argv.indexOf('--watch') >= 0 8 | const demoSrc = path.resolve(__dirname, './demo') 9 | const demoDist = path.resolve(__dirname, '../miniprogram_dev') 10 | const src = path.resolve(__dirname, '../src') 11 | const dev = path.join(demoDist, 'components') 12 | const dist = path.resolve(__dirname, '../miniprogram_dist') 13 | 14 | module.exports = { 15 | entry: ['index'], 16 | 17 | isDev, 18 | isWatch, 19 | srcPath: src, 20 | distPath: isDev ? dev : dist, 21 | 22 | demoSrc, 23 | demoDist, 24 | 25 | wxss: { 26 | less: false, // compile wxss with less 27 | sourcemap: false, // source map for less 28 | }, 29 | 30 | webpack: { 31 | mode: 'production', 32 | output: { 33 | filename: '[name].js', 34 | libraryTarget: 'commonjs2', 35 | }, 36 | target: 'node', 37 | externals: [nodeExternals()], // ignore node_modules 38 | module: { 39 | rules: [{ 40 | test: /\.js$/i, 41 | use: [ 42 | 'babel-loader', 43 | 'eslint-loader' 44 | ], 45 | exclude: /node_modules/ 46 | }], 47 | }, 48 | resolve: { 49 | modules: [src, 'node_modules'], 50 | extensions: ['.js', '.json'], 51 | }, 52 | plugins: [ 53 | new webpack.DefinePlugin({}), 54 | new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1}), 55 | ], 56 | optimization: { 57 | minimize: false, 58 | }, 59 | // devtool: 'nosources-source-map', // source map for js 60 | performance: { 61 | hints: 'warning', 62 | assetFilter: assetFilename => assetFilename.endsWith('.js') 63 | } 64 | }, 65 | 66 | copy: ['./wxml', './wxss', './wxs', './images'], // will copy to dist 67 | } 68 | -------------------------------------------------------------------------------- /tools/test/helper.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const jComponent = require('j-component') 4 | 5 | const config = require('../config') 6 | const _ = require('../utils') 7 | 8 | const srcPath = config.srcPath 9 | const componentMap = {} 10 | let nowLoad = null 11 | 12 | /** 13 | * register custom component 14 | */ 15 | global.Component = options => { 16 | const component = nowLoad 17 | const definition = Object.assign({ 18 | template: component.wxml, 19 | usingComponents: component.json.usingComponents, 20 | tagName: component.tagName, 21 | }, options) 22 | 23 | component.id = jComponent.register(definition) 24 | } 25 | 26 | /** 27 | * register behavior 28 | */ 29 | global.Behavior = options => jComponent.behavior(options) 30 | 31 | /** 32 | * register global components 33 | */ 34 | // eslint-disable-next-line semi-style 35 | ;[ 36 | 'view', 'scroll-view', 'swiper', 'movable-view', 'cover-view', 'cover-view', 37 | 'icon', 'text', 'rich-text', 'progress', 38 | 'button', 'checkbox', 'form', 'input', 'label', 'picker', 'picker', 'picker-view', 'radio', 'slider', 'switch', 'textarea', 39 | 'navigator', 'function-page-navigator', 40 | 'audio', 'image', 'video', 'camera', 'live-player', 'live-pusher', 41 | 'map', 42 | 'canvas', 43 | 'open-data', 'web-view', 'ad' 44 | ].forEach(name => { 45 | jComponent.register({ 46 | id: name, 47 | tagName: `wx-${name}`, 48 | template: '' 49 | }) 50 | }) 51 | 52 | /** 53 | * Touch polyfill 54 | */ 55 | class Touch { 56 | constructor(options = {}) { 57 | this.clientX = 0 58 | this.clientY = 0 59 | this.identifier = 0 60 | this.pageX = 0 61 | this.pageY = 0 62 | this.screenX = 0 63 | this.screenY = 0 64 | this.target = null 65 | 66 | Object.keys(options).forEach(key => { 67 | this[key] = options[key] 68 | }) 69 | } 70 | } 71 | global.Touch = window.Touch = Touch 72 | 73 | /** 74 | * load component 75 | */ 76 | async function load(componentPath, tagName) { 77 | if (typeof componentPath === 'object') { 78 | const definition = componentPath 79 | 80 | return jComponent.register(definition) 81 | } 82 | 83 | const wholePath = path.join(srcPath, componentPath) 84 | 85 | const oldLoad = nowLoad 86 | const component = nowLoad = {} 87 | 88 | component.tagName = tagName 89 | component.wxml = await _.readFile(`${wholePath}.wxml`) 90 | component.wxss = await _.readFile(`${wholePath}.wxss`) 91 | component.json = _.readJson(`${wholePath}.json`) 92 | 93 | if (!component.json) { 94 | throw new Error(`invalid component: ${wholePath}`) 95 | } 96 | 97 | // preload using components 98 | const usingComponents = component.json.usingComponents || {} 99 | const usingComponentKeys = Object.keys(usingComponents) 100 | for (let i = 0, len = usingComponentKeys.length; i < len; i++) { 101 | const key = usingComponentKeys[i] 102 | const usingPath = path.join(path.dirname(componentPath), usingComponents[key]) 103 | // eslint-disable-next-line no-await-in-loop 104 | const id = await load(usingPath) 105 | 106 | usingComponents[key] = id 107 | } 108 | 109 | // require js 110 | // eslint-disable-next-line import/no-dynamic-require 111 | require(wholePath) 112 | 113 | nowLoad = oldLoad 114 | componentMap[wholePath] = component 115 | 116 | return component.id 117 | } 118 | 119 | /** 120 | * render component 121 | */ 122 | function render(componentId, properties) { 123 | if (!componentId) throw new Error('you need to pass the componentId') 124 | 125 | return jComponent.create(componentId, properties) 126 | } 127 | 128 | /** 129 | * test a dom is similar to the html 130 | */ 131 | function match(dom, html) { 132 | if (!(dom instanceof window.Element) || !html || typeof html !== 'string') return false 133 | 134 | // remove some 135 | html = html.trim() 136 | .replace(/(>)[\n\r\s\t]+(<)/g, '$1$2') 137 | 138 | const a = dom.cloneNode() 139 | const b = dom.cloneNode() 140 | 141 | a.innerHTML = dom.innerHTML 142 | b.innerHTML = html 143 | 144 | return a.isEqualNode(b) 145 | } 146 | 147 | /** 148 | * wait for some time 149 | */ 150 | function sleep(time = 0) { 151 | return new Promise(resolve => { 152 | setTimeout(() => { 153 | resolve() 154 | }, time) 155 | }) 156 | } 157 | 158 | module.exports = { 159 | load, 160 | render, 161 | match, 162 | sleep, 163 | } 164 | -------------------------------------------------------------------------------- /tools/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | const colors = require('colors') 6 | const through = require('through2') 7 | 8 | /** 9 | * async function wrapper 10 | */ 11 | function wrap(func, scope) { 12 | return function (...args) { 13 | if (args.length) { 14 | const temp = args.pop() 15 | if (typeof temp !== 'function') { 16 | args.push(temp) 17 | } 18 | } 19 | 20 | return new Promise(function (resolve, reject) { 21 | args.push(function (err, data) { 22 | if (err) reject(err) 23 | else resolve(data) 24 | }) 25 | 26 | func.apply((scope || null), args) 27 | }) 28 | } 29 | } 30 | 31 | const accessSync = wrap(fs.access) 32 | const statSync = wrap(fs.stat) 33 | const renameSync = wrap(fs.rename) 34 | const mkdirSync = wrap(fs.mkdir) 35 | const readFileSync = wrap(fs.readFile) 36 | const writeFileSync = wrap(fs.writeFile) 37 | 38 | /** 39 | * transform path segment separator 40 | */ 41 | function transformPath(filePath, sep = '/') { 42 | return filePath.replace(/[\\/]/g, sep) 43 | } 44 | 45 | /** 46 | * check file exists 47 | */ 48 | async function checkFileExists(filePath) { 49 | try { 50 | await accessSync(filePath) 51 | return true 52 | } catch (err) { 53 | return false 54 | } 55 | } 56 | 57 | /** 58 | * create folder 59 | */ 60 | async function recursiveMkdir(dirPath) { 61 | const prevDirPath = path.dirname(dirPath) 62 | try { 63 | await accessSync(prevDirPath) 64 | } catch (err) { 65 | // prevDirPath is not exist 66 | await recursiveMkdir(prevDirPath) 67 | } 68 | 69 | try { 70 | await accessSync(dirPath) 71 | 72 | const stat = await statSync(dirPath) 73 | if (stat && !stat.isDirectory()) { 74 | // dirPath already exists but is not a directory 75 | await renameSync(dirPath, `${dirPath}.bak`) // rename to a file with the suffix ending in '.bak' 76 | await mkdirSync(dirPath) 77 | } 78 | } catch (err) { 79 | // dirPath is not exist 80 | await mkdirSync(dirPath) 81 | } 82 | } 83 | 84 | /** 85 | * read json 86 | */ 87 | function readJson(filePath) { 88 | try { 89 | // eslint-disable-next-line import/no-dynamic-require 90 | const content = require(filePath) 91 | delete require.cache[require.resolve(filePath)] 92 | return content 93 | } catch (err) { 94 | return null 95 | } 96 | } 97 | 98 | /** 99 | * read file 100 | */ 101 | async function readFile(filePath) { 102 | try { 103 | return await readFileSync(filePath, 'utf8') 104 | } catch (err) { 105 | // eslint-disable-next-line no-console 106 | return console.error(err) 107 | } 108 | } 109 | 110 | /** 111 | * write file 112 | */ 113 | async function writeFile(filePath, data) { 114 | try { 115 | await recursiveMkdir(path.dirname(filePath)) 116 | return await writeFileSync(filePath, data, 'utf8') 117 | } catch (err) { 118 | // eslint-disable-next-line no-console 119 | return console.error(err) 120 | } 121 | } 122 | 123 | /** 124 | * time format 125 | */ 126 | function format(time, reg) { 127 | const date = typeof time === 'string' ? new Date(time) : time 128 | const map = {} 129 | map.yyyy = date.getFullYear() 130 | map.yy = ('' + map.yyyy).substr(2) 131 | map.M = date.getMonth() + 1 132 | map.MM = (map.M < 10 ? '0' : '') + map.M 133 | map.d = date.getDate() 134 | map.dd = (map.d < 10 ? '0' : '') + map.d 135 | map.H = date.getHours() 136 | map.HH = (map.H < 10 ? '0' : '') + map.H 137 | map.m = date.getMinutes() 138 | map.mm = (map.m < 10 ? '0' : '') + map.m 139 | map.s = date.getSeconds() 140 | map.ss = (map.s < 10 ? '0' : '') + map.s 141 | 142 | return reg.replace(/\byyyy|yy|MM|M|dd|d|HH|H|mm|m|ss|s\b/g, $1 => map[$1]) 143 | } 144 | 145 | /** 146 | * logger plugin 147 | */ 148 | function logger(action = 'copy') { 149 | return through.obj(function (file, enc, cb) { 150 | const type = path.extname(file.path).slice(1).toLowerCase() 151 | 152 | // eslint-disable-next-line no-console 153 | console.log(`[${format(new Date(), 'yyyy-MM-dd HH:mm:ss').grey}] [${action.green} ${type.green}] ${'=>'.cyan} ${file.path}`) 154 | 155 | this.push(file) 156 | cb() 157 | }) 158 | } 159 | 160 | /** 161 | * compare arrays 162 | */ 163 | function compareArray(arr1, arr2) { 164 | if (!Array.isArray(arr1) || !Array.isArray(arr2)) return false 165 | if (arr1.length !== arr2.length) return false 166 | 167 | for (let i = 0, len = arr1.length; i < len; i++) { 168 | if (arr1[i] !== arr2[i]) return false 169 | } 170 | 171 | return true 172 | } 173 | 174 | /** 175 | * merge two object 176 | */ 177 | function merge(obj1, obj2) { 178 | Object.keys(obj2).forEach(key => { 179 | if (Array.isArray(obj1[key]) && Array.isArray(obj2[key])) { 180 | obj1[key] = obj1[key].concat(obj2[key]) 181 | } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { 182 | obj1[key] = Object.assign(obj1[key], obj2[key]) 183 | } else { 184 | obj1[key] = obj2[key] 185 | } 186 | }) 187 | 188 | return obj1 189 | } 190 | 191 | /** 192 | * get random id 193 | */ 194 | let seed = +new Date() 195 | function getId() { 196 | return ++seed 197 | } 198 | 199 | module.exports = { 200 | wrap, 201 | transformPath, 202 | 203 | checkFileExists, 204 | readJson, 205 | readFile, 206 | writeFile, 207 | 208 | logger, 209 | format, 210 | compareArray, 211 | merge, 212 | getId, 213 | } 214 | --------------------------------------------------------------------------------