├── tools ├── demo │ ├── package.json │ ├── app.js │ ├── pages │ │ ├── index │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── image │ │ │ └── file_transfer.jpg │ ├── app.wxss │ ├── app.json │ └── project.config.json ├── config.js ├── checkcomponents.js ├── test │ └── helper.js ├── utils.js └── build.js ├── test └── utils.js ├── src ├── index.json ├── index.wxss ├── index.wxml └── index.js ├── UPDATE.md ├── docs └── slide-view.gif ├── .gitignore ├── .npmignore ├── .babelrc ├── gulpfile.js ├── LICENSE ├── README.md ├── package.json └── .eslintrc.js /tools/demo/package.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tools/demo/app.js: -------------------------------------------------------------------------------- 1 | App({}); 2 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../tools/test/helper') 2 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: {} 3 | }) 4 | -------------------------------------------------------------------------------- /src/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /UPDATE.md: -------------------------------------------------------------------------------- 1 | ## 0.0.4 2 | 3 | * 提供 updateRight 接口,用于更新 slot="right" 这部分节点的宽度。 4 | -------------------------------------------------------------------------------- /docs/slide-view.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/slide-view/HEAD/docs/slide-view.gif -------------------------------------------------------------------------------- /tools/demo/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "slide-view": "/components/index" 4 | } 5 | } -------------------------------------------------------------------------------- /tools/demo/pages/image/file_transfer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/slide-view/HEAD/tools/demo/pages/image/file_transfer.jpg -------------------------------------------------------------------------------- /src/index.wxss: -------------------------------------------------------------------------------- 1 | /* slide-view/slide-view.wxss */ 2 | .movable-view{ 3 | display: flex; 4 | direction: row; 5 | overflow: hidden; 6 | } 7 | 8 | .container { 9 | overflow: hidden; 10 | } 11 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /tools/demo/app.wxss: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: space-between; 7 | padding: 200rpx 0; 8 | box-sizing: border-box; 9 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["module-resolver", { 4 | "root": ["./src"], 5 | "alias": {} 6 | }] 7 | ], 8 | "presets": [ 9 | ["env", {"loose": true, "modules": "commonjs"}] 10 | ] 11 | } -------------------------------------------------------------------------------- /tools/demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index" 4 | ], 5 | "window":{ 6 | "backgroundTextStyle":"light", 7 | "navigationBarBackgroundColor": "#000", 8 | "navigationBarTitleText": "WeChat", 9 | "navigationBarTextStyle":"white" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 文件传输助手 8 | 7:00 PM 9 | 10 | 11 | 12 | 标为已读 13 | 删除 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /tools/demo/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "postcss": true, 10 | "minified": true, 11 | "newFeature": true, 12 | "nodeModules": true 13 | }, 14 | "compileType": "miniprogram", 15 | "libVersion": "2.2.3", 16 | "appid": "", 17 | "projectname": "slide-view", 18 | "isGameTourist": false, 19 | "condition": { 20 | "search": { 21 | "current": -1, 22 | "list": [] 23 | }, 24 | "conversation": { 25 | "current": -1, 26 | "list": [] 27 | }, 28 | "game": { 29 | "currentL": -1, 30 | "list": [] 31 | }, 32 | "miniprogram": { 33 | "current": -1, 34 | "list": [] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /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 | new BuildTask(id, config.entry); 10 | 11 | // clean the generated folders and files 12 | gulp.task('clean', gulp.series(() => { 13 | return gulp.src(config.distPath, { read: false, allowEmpty: true }) 14 | .pipe(clean()) 15 | }, done => { 16 | if (config.isDev) { 17 | return gulp.src(config.demoDist, { read: false, allowEmpty: true }) 18 | .pipe(clean()); 19 | } 20 | 21 | done(); 22 | })); 23 | // watch files and build 24 | gulp.task('watch', gulp.series(`${id}-watch`)); 25 | // build for develop 26 | gulp.task('dev', gulp.series(`${id}-dev`)); 27 | // build for publish 28 | gulp.task('default', gulp.series(`${id}-default`)); 29 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | .slide{ 3 | /* border-top:1px solid #ccc; */ 4 | border-bottom:1px solid #DEDEDE; 5 | } 6 | .l { 7 | background-color: white; 8 | height: 110rpx; 9 | width: 750rpx; 10 | display: flex; 11 | flex-direction: row; 12 | } 13 | .r { 14 | height: 110rpx; 15 | display: flex; 16 | direction: row; 17 | text-align: center; 18 | vertical-align: middle; 19 | line-height: 110rpx; 20 | } 21 | .read { 22 | background-color: #ccc; 23 | color: #fff; 24 | width: 350rpx; 25 | } 26 | .delete { 27 | background-color: red; 28 | color: #fff; 29 | width: 150rpx; 30 | } 31 | .img { 32 | width: 90rpx; 33 | height: 90rpx; 34 | border-radius:10rpx; 35 | margin: 10rpx 15rpx; 36 | } 37 | .text { 38 | display: flex; 39 | flex-direction: row; 40 | } 41 | .title { 42 | margin-top: 15rpx; 43 | font-size: 33rpx; 44 | } 45 | .time { 46 | margin-top: 15rpx; 47 | color: #ccc; 48 | font-size: 25rpx; 49 | margin-left: 330rpx; 50 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /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 | copy: ['./wxml', './wxss', './wxs', './images'], 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 本仓库不再维护,有需求请使用[weui-miniprogram](https://github.com/wechat-miniprogram/weui-miniprogram)的slideview组件。 2 | 3 | # slide-view 4 | 5 | 小程序自定义组件 6 | 7 | > 使用此组件需要依赖小程序基础库 2.2.1 以上版本,同时依赖开发者工具的 npm 构建。具体详情可查阅[官方 npm 文档](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html)。 8 | 9 | ## 使用效果 10 | 11 | ![slide-view](./docs/slide-view.gif) 12 | 13 | > PS:此组件默认只携带基本样式,若想要获得上图中的效果,可参考 [tools/demo](./tools/demo/pages/index/index.wxss) 中的例子实现。 14 | 15 | ## 使用方法 16 | 17 | 1. 安装 slide-view 18 | 19 | ``` 20 | npm install --save miniprogram-slide-view 21 | ``` 22 | 23 | 2. 在需要使用 slide-view 的页面 page.json 中添加 slide-view 自定义组件配置 24 | 25 | ```json 26 | { 27 | "usingComponents": { 28 | "slide-view": "miniprogram-slide-view" 29 | } 30 | } 31 | ``` 32 | 33 | 3. WXML 文件中引用 slide-view 34 | 35 | 每一个 slide-view 提供两个``节点,用于承载组件引用时提供的子节点。left 节点用于承载静止时 slide-view 所展示的节点,此节点的宽高应与传入 slide-view 的宽高相同。right 节点用于承载滑动时所展示的节点,其宽度应于传入 slide-view 的 slideWidth 相同。 36 | 37 | ``` xml 38 | 39 | 这里是插入到组内容 40 | 41 | 标为已读 42 | 删除 43 | 44 | 45 | ``` 46 | 47 | **slide-view的属性介绍如下:** 48 | 49 | | 属性名 | 类型 | 单位 | 默认值 | 是否必须 | 说明 | 50 | |-------------------------|--------------|--------------|---------------------------|------------|---------------------------------------------| 51 | | width | Number | rpx | 显示屏幕的宽度 | 是 | slide-view组件的宽度 | 52 | | height | Number | rpx | 0 | 是 | slide-view组件的高度 | 53 | | slide-width | Number | rpx | 0 | 是 | 滑动展示区域的宽度(默认高度与slide-view相同)| 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram-slide-view", 3 | "version": "0.0.4", 4 | "description": "miniprogram custom component", 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 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/wechat-miniprogram/slide-view.git" 22 | }, 23 | "author": "wechat-miniprogram", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "babel-core": "^6.26.3", 27 | "babel-loader": "^7.1.5", 28 | "babel-plugin-module-resolver": "^3.1.1", 29 | "babel-preset-env": "^1.7.0", 30 | "colors": "^1.3.1", 31 | "eslint": "^5.3.0", 32 | "eslint-loader": "^2.1.0", 33 | "gulp": "^4.0.0", 34 | "gulp-clean": "^0.4.0", 35 | "gulp-if": "^2.0.2", 36 | "gulp-install": "^1.1.0", 37 | "gulp-less": "^3.5.0", 38 | "gulp-rename": "^1.4.0", 39 | "gulp-sourcemaps": "^2.6.4", 40 | "j-component": "git+https://github.com/JuneAndGreen/j-component.git", 41 | "jest": "^23.5.0", 42 | "through2": "^2.0.3", 43 | "webpack": "^4.16.5", 44 | "webpack-node-externals": "^1.7.2", 45 | "eslint-config-airbnb-base": "13.1.0", 46 | "eslint-plugin-import": "^2.14.0", 47 | "eslint-plugin-node": "^7.0.1", 48 | "eslint-plugin-promise": "^3.8.0" 49 | }, 50 | "dependencies": {}, 51 | "jest": { 52 | "testEnvironment": "jsdom", 53 | "testURL": "https://jest.test", 54 | "collectCoverageFrom": [ 55 | "src/**/*.js" 56 | ], 57 | "moduleDirectories": [ 58 | "node_modules", 59 | "src" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.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 | 'globals': { 87 | 'window': true, 88 | 'document': true, 89 | 'App': true, 90 | 'Page': true, 91 | 'Component': true, 92 | 'Behavior': true, 93 | 'wx': true, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // slide-view/slide-view.js 2 | const _windowWidth = wx.getSystemInfoSync().windowWidth // (px) 3 | Component({ 4 | /** 5 | * 组件的属性列表 6 | */ 7 | options: { 8 | multipleSlots: true, 9 | }, 10 | properties: { 11 | // 组件显示区域的宽度 (rpx) 12 | width: { 13 | type: Number, 14 | value: 750 // 750rpx 即整屏宽 15 | }, 16 | // 组件显示区域的高度 (rpx) 17 | height: { 18 | type: Number, 19 | value: 0, 20 | }, 21 | // 组件滑动显示区域的宽度 (rpx) 22 | slideWidth: { 23 | type: Number, 24 | value: 0 25 | } 26 | }, 27 | 28 | /** 29 | * 组件的初始数据 30 | */ 31 | data: { 32 | viewWidth: _windowWidth, // (rpx) 33 | // movable-view偏移量 34 | x: 0, 35 | // movable-view是否可以出界 36 | out: false, 37 | }, 38 | 39 | /** 40 | * 组件的方法列表 41 | */ 42 | ready() { 43 | this.updateRight() 44 | }, 45 | methods: { 46 | updateRight() { 47 | // 获取右侧滑动显示区域的宽度 48 | const that = this 49 | const query = wx.createSelectorQuery().in(this) 50 | query.select('.right').boundingClientRect(function (res) { 51 | that._slideWidth = res.width 52 | that._threshold = res.width / 2 53 | that._viewWidth = that.data.width + res.width * (750 / _windowWidth) 54 | that.setData({ 55 | viewWidth: that._viewWidth 56 | }) 57 | }).exec() 58 | }, 59 | onTouchStart(e) { 60 | this._startX = e.changedTouches[0].pageX 61 | }, 62 | // 当滑动范围超过阈值自动完成剩余滑动 63 | onTouchEnd(e) { 64 | this._endX = e.changedTouches[0].pageX 65 | const {_endX, _startX, _threshold} = this 66 | if (_endX > _startX && this.data.out === false) return 67 | if (_startX - _endX >= _threshold) { 68 | this.setData({ 69 | x: -this._slideWidth 70 | }) 71 | } else if (_startX - _endX < _threshold && _startX - _endX > 0) { 72 | this.setData({ 73 | x: 0 74 | }) 75 | } else if (_endX - _startX >= _threshold) { 76 | this.setData({ 77 | x: 0 78 | }) 79 | } else if (_endX - _startX < _threshold && _endX - _startX > 0) { 80 | this.setData({ 81 | x: -this._slideWidth 82 | }) 83 | } 84 | }, 85 | // 根据滑动的范围设定是否允许movable-view出界 86 | onChange(e) { 87 | if (!this.data.out && e.detail.x < -this._threshold) { 88 | this.setData({ 89 | out: true 90 | }) 91 | } else if (this.data.out && e.detail.x >= -this._threshold) { 92 | this.setData({ 93 | out: false 94 | }) 95 | } 96 | } 97 | } 98 | }) 99 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------