├── 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 | 
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 |
--------------------------------------------------------------------------------