├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── commitlint.config.js ├── doc └── preview.gif ├── gulpfile.js ├── package.json ├── src ├── extra │ ├── customActive.js │ └── redDot.js ├── index.js ├── index.json ├── index.less ├── index.wxml └── index.wxss ├── test ├── index.test.js ├── utils.js └── wx.test.js ├── tools ├── build.js ├── checkcomponents.js ├── checkwxss.js ├── config.js ├── demo │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── image │ │ ├── index-pre.png │ │ ├── index.png │ │ ├── mine-pre.png │ │ ├── mine.png │ │ └── x.png │ ├── package.json │ ├── pages │ │ └── index │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ ├── index.wxss │ │ │ ├── mine.js │ │ │ ├── mine.json │ │ │ ├── mine.wxml │ │ │ └── mine.wxss │ └── project.config.json └── utils.js └── yarn.lock /.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 | '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 | 'getCurrentPages': true, 95 | '__wxConfig': true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "eslint.autoFixOnSave": true 4 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # miniprogram-custom-tab-bar 2 | 3 | 小程序自定义底部导航栏组件 4 | 5 | * 适配iPhone X 6 | * 无需额外配置,甚至不用传参,自动读取app.json中tabBar的配置 7 | * 使用wx.showTabBarRedDot方法可以设置红点 8 | * 可以自定义样式,摆脱微信限制(如borderStyle仅支持black/white的限制) 9 | * 可以定制个性化逻辑 10 | 11 | ## 效果图 12 | 13 | ![](./doc/preview.gif) 14 | 15 | ## 使用 16 | 17 | * npm i miniprogram-custom-tab-bar 18 | * 在tab页面注册并引入组件即可 19 | 20 | ## 如何实现预览图中 中间突出来的图标 21 | 22 | 因为组件原理是拿app.json实现的,我们可以直接在app.json的`"tabBar"`中加入如下代码,其中iconPath可以用绝对路径,也可以用网上路径 23 | 24 | // app.json 25 | "tabBar":{ 26 | ..., 27 | "customNode": { 28 | "iconPath": "/image/x.png", 29 | "activeStyle": "transform: rotate(45deg);", 30 | "style": "transform: rotate(0deg);" 31 | }, 32 | ... 33 | } 34 | 35 | **组件还在开发中,如果大家需要某些功能可以提一个issue,我会考虑把它加入到组件中去** 36 | 37 | ## 二次开发 38 | 39 | clone 本项目,之后修改src目录中的内容,目录结构基本与小程序插件模板一致,此处是[具体文档](https://github.com/wechat-miniprogram/miniprogram-custom-component) 40 | 41 | 1. 安装依赖: 42 | 43 | ``` 44 | npm i 或 yarn 45 | ``` 46 | 47 | 2. 执行命令: 48 | 49 | ``` 50 | npm run dev 51 | ``` 52 | 53 | 默认会在包根目录下生成 miniprogram\_dev 目录,src 中的源代码会被构建并生成到 miniprogram\_dev/components 目录下。如果需要监听文件变化动态构建,则可以执行命令: 54 | 55 | ``` 56 | npm run watch 57 | ``` 58 | 59 | > ps: 如果 minirpogram\_dev 目录下已存在小程序 demo,执行`npm run dev`则不会再将 tools 下的 demo 拷贝到此目录下。而执行`npm run watch`则会监听 tools 目录下的 demo 变动并进行拷贝。 60 | 61 | 3. 生成的 miniprogram\_dev 目录是一个小程序项目目录,以此目录作为小程序项目目录在开发者工具中打开即可查看自定义组件被使用的效果。 62 | 63 | 4. 进阶: 64 | 65 | * 如果有额外的构建需求,可自行修改 tools 目录中的构建脚本。 66 | * 内置支持 less、sourcemap 等功能,默认关闭。如若需要可以自行修改 tools/config.js 配置文件中相关配置。 67 | * 内置支持多入口构建,如若需要可自行调整 tools/config.js 配置文件的 entry 字段。 68 | * 默认开启 eslint,可自行调整规则或在 tools/config.js 中注释掉 eslint-loader 行来关闭此功能。 69 | 70 | ## 目录结构 71 | 72 | 以下为推荐使用的目录结构,如果有必要开发者也可以自行做一些调整: 73 | 74 | ``` 75 | |--miniprogram_dev // 开发环境构建目录 76 | |--miniprogram_dist // 生产环境构建目录 77 | |--src // 源码 78 | | |--components // 通用自定义组件 79 | | |--images // 图片资源 80 | | | 81 | | |--xxx.js/xxx.wxml/xxx.json/xxx.wxss // 暴露的 js 模块/自定义组件入口文件 82 | | 83 | |--test // 测试用例 84 | |--tools // 构建相关代码 85 | | |--demo // demo 小程序目录,开发环境下会被拷贝生成到 miniprogram_dev 目录中 86 | | |--config.js // 构建相关配置文件 87 | | 88 | |--gulpfile.js 89 | ``` 90 | 91 | > PS:对外暴露的 js 模块/自定义组件请放在 src 目录下,不宜放置在过深的目录。另外新增的暴露模块需要在 tools/config.js 的 entry 字段中补充,不然不会进行构建。 92 | 93 | ## 测试 94 | 95 | * 执行测试用例: 96 | 97 | ``` 98 | npm run test 99 | ``` 100 | 101 | * 检测覆盖率: 102 | 103 | ``` 104 | npm run coverage 105 | ``` 106 | 107 | 测试用例放在 test 目录下,使用 **miniprogram-simulate** 工具集进行测试,[点击此处查看](https://github.com/wechat-miniprogram/miniprogram-simulate/blob/master/README.md)使用方法。在测试中可能需要变更或调整工具集中的一些方法,可在 test/utils 下自行实现。 108 | 109 | ## 其他命令 110 | 111 | * 清空 miniprogram_dist 目录: 112 | 113 | ``` 114 | npm run clean 115 | ``` 116 | 117 | * 清空 miniprogam_dev 目录: 118 | 119 | ``` 120 | npm run clean-dev 121 | ``` 122 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /doc/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/doc/preview.gif -------------------------------------------------------------------------------- /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 | // 构建任务实例 9 | // eslint-disable-next-line no-new 10 | new BuildTask(id, config.entry) 11 | 12 | // 清空生成目录和文件 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 | // 监听文件变化并进行开发模式构建 22 | gulp.task('watch', gulp.series(`${id}-watch`)) 23 | // 开发模式构建 24 | gulp.task('dev', gulp.series(`${id}-dev`)) 25 | // 生产模式构建 26 | gulp.task('default', gulp.series(`${id}-default`)) 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram-custom-tab-bar", 3 | "version": "1.0.3", 4 | "description": "小程序自定义底部导航栏组件", 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/ljybill/miniprogram-custom-tab-bar" 33 | }, 34 | "author": "ljybill@aliyun.com", 35 | "license": "MIT", 36 | "devDependencies": { 37 | "babel-core": "^6.26.3", 38 | "babel-loader": "^7.1.5", 39 | "babel-plugin-module-resolver": "^3.2.0", 40 | "babel-preset-env": "^1.7.0", 41 | "colors": "^1.3.1", 42 | "eslint": "^5.14.1", 43 | "eslint-config-airbnb-base": "13.1.0", 44 | "eslint-loader": "^2.1.2", 45 | "eslint-plugin-import": "^2.16.0", 46 | "eslint-plugin-node": "^7.0.1", 47 | "eslint-plugin-promise": "^3.8.0", 48 | "gulp": "^4.0.0", 49 | "gulp-clean": "^0.4.0", 50 | "gulp-if": "^2.0.2", 51 | "gulp-install": "^1.1.0", 52 | "gulp-less": "^4.0.1", 53 | "gulp-rename": "^1.4.0", 54 | "gulp-sourcemaps": "^2.6.5", 55 | "jest": "^23.5.0", 56 | "miniprogram-simulate": "^1.0.0", 57 | "through2": "^2.0.3", 58 | "vinyl": "^2.2.0", 59 | "webpack": "^4.29.5", 60 | "webpack-node-externals": "^1.7.2" 61 | }, 62 | "dependencies": {} 63 | } 64 | -------------------------------------------------------------------------------- /src/extra/customActive.js: -------------------------------------------------------------------------------- 1 | let active = false 2 | const bus = [] 3 | 4 | export const getActive = function () { 5 | return active 6 | } 7 | 8 | export const setActive = function (status) { 9 | active = status 10 | bus.forEach(fn => { 11 | if (typeof fn === 'function') { 12 | fn(active) 13 | } 14 | }) 15 | } 16 | export const listenActiveChange = function (fn) { 17 | bus.push(fn) 18 | } 19 | 20 | export default { 21 | getActive, 22 | setActive, 23 | listenActiveChange 24 | } 25 | -------------------------------------------------------------------------------- /src/extra/redDot.js: -------------------------------------------------------------------------------- 1 | function index() { 2 | const showTabBarRedDotOld = wx.showTabBarRedDot 3 | const hideTabBarRedDotOld = wx.hideTabBarRedDot 4 | const redDotList = Array.from({length: __wxConfig.tabBar.list.length}, () => false) 5 | const redDotBus = [] 6 | const showTabBarRedDotNew = function (option) { 7 | const {index} = option 8 | if (typeof index !== 'number' || index < 0 || index >= redDotList.length) { 9 | return 10 | } 11 | redDotList[index] = true 12 | redDotBus.forEach(fun => { 13 | if (typeof fun === 'function') { 14 | fun() 15 | } 16 | }) 17 | showTabBarRedDotOld(option) 18 | } 19 | const hideTabBarRedDotNew = function (option) { 20 | const {index} = option 21 | if (typeof index !== 'number' || index < 0 || index >= redDotList.length) { 22 | return 23 | } 24 | redDotList[index] = false 25 | redDotBus.forEach(fun => { 26 | if (typeof fun === 'function') { 27 | fun() 28 | } 29 | }) 30 | hideTabBarRedDotOld(option) 31 | } 32 | Object.defineProperty(wx, 'showTabBarRedDot', { 33 | enumerable: true, 34 | configurable: true, 35 | writable: true, 36 | value: showTabBarRedDotNew 37 | }) 38 | Object.defineProperty(wx, 'hideTabBarRedDot', { 39 | enumerable: true, 40 | configurable: true, 41 | writable: true, 42 | value: hideTabBarRedDotNew 43 | }) 44 | return [redDotList, redDotBus] 45 | } 46 | 47 | const [redDotList, redDotBus] = index() 48 | 49 | export const getDotList = function () { 50 | return redDotList || [] 51 | } 52 | 53 | export const listenDotListChange = function (fn) { 54 | if (Array.isArray(redDotBus)) { 55 | redDotBus.push(fn) 56 | } 57 | } 58 | 59 | export default { 60 | getDotList, 61 | listenDotListChange 62 | } 63 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import {getDotList, listenDotListChange} from './extra/redDot' 2 | import {getActive, setActive, listenActiveChange} from './extra/customActive' 3 | // 通过app.json配置的 tabBar 解析成组件可用的list 4 | const fixListConfig = function (item, index) { 5 | const result = {} // 使用新对象,类似浅拷贝 6 | result.pagePath = '/' + item.pagePath.replace(/.html$/g, '') 7 | result.iconPath = item.iconData 8 | ? 'data:image/png;base64,' + item.iconData 9 | : '/' + item.iconPath.replace(/\\/g, '/') 10 | result.selectedIconPath = item.selectedIconData 11 | ? 'data:image/png;base64,' + item.selectedIconData 12 | : '/' + item.selectedIconPath.replace(/\\/g, '/') 13 | result.idx = index 14 | result.redDot = false 15 | result.text = item.text 16 | return result 17 | } 18 | 19 | const _tabBar = __wxConfig.tabBar 20 | if (!_tabBar) { 21 | throw new Error('app.json 未定义tabBar') 22 | } 23 | wx.hideTabBar() 24 | 25 | Component({ 26 | properties: { 27 | borderColor: { 28 | type: String, 29 | value: '' 30 | } 31 | }, 32 | data: { 33 | activeIdx: -1, 34 | config: _tabBar, 35 | list: _tabBar.list.map(fixListConfig), 36 | // 自定义节点 37 | customOrder: Math.floor(_tabBar.list.length / 2) - 1, 38 | customActive: false, 39 | customTransitionTime: '0.3s' 40 | }, 41 | methods: { 42 | switchTab(evt) { 43 | const {pagePath} = evt.currentTarget.dataset 44 | wx.switchTab({ 45 | url: pagePath 46 | }) 47 | }, 48 | updateRedDot() { 49 | if (Array.isArray(getDotList())) { 50 | this.setData({ 51 | list: this.data.list.map(item => { 52 | item.redDot = getDotList()[item.idx] 53 | return item 54 | }) 55 | }) 56 | } 57 | }, 58 | updateCustomNodeActive(status) { 59 | this.setData({ 60 | customActive: status 61 | }) 62 | }, 63 | handleCustomNodeTap() { 64 | setActive(!this.data.customActive) 65 | } 66 | }, 67 | ready() { 68 | this.updateRedDot() 69 | listenDotListChange(this.updateRedDot.bind(this)) 70 | if (this.data.config.customNode) { 71 | listenActiveChange(this.updateCustomNodeActive.bind(this)) 72 | } 73 | }, 74 | pageLifetimes: { 75 | show() { 76 | const pages = getCurrentPages() 77 | const page = pages[pages.length - 1] 78 | const route = page.__route__ 79 | const idx = this.data.list.find(item => item.pagePath === `/${route}`).idx 80 | if (this.data.activeIdx !== idx) { 81 | this.setData({ 82 | activeIdx: idx 83 | }) 84 | } 85 | if (this.data.config.customNode && this.data.customActive !== getActive()) { 86 | this.setData({ 87 | customActive: getActive() 88 | }) 89 | } 90 | } 91 | } 92 | }) 93 | -------------------------------------------------------------------------------- /src/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /src/index.less: -------------------------------------------------------------------------------- 1 | // out: ./index.wxss, compress: false, sourceMap: false 2 | .tab-bar { 3 | position: fixed; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | background-color: #fff; 8 | padding-bottom: env(safe-area-inset-bottom); 9 | 10 | &-border { 11 | position: absolute; 12 | top: 0; 13 | left: 0; 14 | right: 0; 15 | border-top: 0.5px solid rgba(0, 0, 0, 0.2); 16 | 17 | &.white { 18 | border-top-color: #fff; 19 | } 20 | } 21 | 22 | &-list { 23 | display: flex; 24 | } 25 | 26 | &-item { 27 | flex: 1; 28 | text-align: center; 29 | padding: 10rpx 0 0; 30 | 31 | &-icon { 32 | position: relative; 33 | margin: 0 auto; 34 | width: 27px; 35 | height: 27px; 36 | 37 | image { 38 | width: 27px; 39 | height: 27px; 40 | } 41 | } 42 | 43 | &-text { 44 | font-size: 20rpx; 45 | } 46 | 47 | &.custom { 48 | .tab-bar-item-icon { 49 | image { 50 | position: absolute; 51 | width: 100rpx; 52 | height: 100rpx; 53 | border-radius: 50%; 54 | top: 50%; 55 | left: 50%; 56 | margin-left: -50rpx; 57 | margin-top: -66rpx; 58 | transform-origin: center center; 59 | transition: transform 0.3s; 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | .red-dot { 67 | position: absolute; 68 | background-color: red; 69 | top: 0; 70 | right: 0; 71 | width: 20rpx; 72 | height: 20rpx; 73 | border-radius: 50%; 74 | overflow: hidden; 75 | transform: translate3d(10rpx, 0, 0); 76 | } 77 | -------------------------------------------------------------------------------- /src/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{item.text}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/index.wxss: -------------------------------------------------------------------------------- 1 | .tab-bar { 2 | position: fixed; 3 | bottom: 0; 4 | left: 0; 5 | right: 0; 6 | background-color: #fff; 7 | padding-bottom: env(safe-area-inset-bottom); 8 | } 9 | .tab-bar-border { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | border-top: 0.5px solid rgba(0, 0, 0, 0.2); 15 | } 16 | .tab-bar-border.white { 17 | border-top-color: #fff; 18 | } 19 | .tab-bar-list { 20 | display: flex; 21 | } 22 | .tab-bar-item { 23 | flex: 1; 24 | text-align: center; 25 | padding: 10rpx 0 0; 26 | } 27 | .tab-bar-item-icon { 28 | position: relative; 29 | margin: 0 auto; 30 | width: 27px; 31 | height: 27px; 32 | } 33 | .tab-bar-item-icon image { 34 | width: 27px; 35 | height: 27px; 36 | } 37 | .tab-bar-item-text { 38 | font-size: 20rpx; 39 | } 40 | .tab-bar-item.custom .tab-bar-item-icon image { 41 | position: absolute; 42 | width: 100rpx; 43 | height: 100rpx; 44 | border-radius: 50%; 45 | top: 50%; 46 | left: 50%; 47 | margin-left: -50rpx; 48 | margin-top: -66rpx; 49 | transform-origin: center center; 50 | transition: transform 0.3s; 51 | } 52 | .red-dot { 53 | position: absolute; 54 | background-color: red; 55 | top: 0; 56 | right: 0; 57 | width: 20rpx; 58 | height: 20rpx; 59 | border-radius: 50%; 60 | overflow: hidden; 61 | transform: translate3d(10rpx, 0, 0); 62 | } 63 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const _ = require('./utils') 2 | 3 | test('render', async () => { 4 | const componentId = _.load('index', 'comp') 5 | const component = _.render(componentId, {prop: 'index.test.properties'}) 6 | 7 | const parent = document.createElement('parent-wrapper') 8 | component.attach(parent) 9 | 10 | expect( 11 | _.match( 12 | component.dom, 13 | 'index.test.properties-false' 14 | ) 15 | ).toBe(true) 16 | 17 | await _.sleep(10) 18 | 19 | expect( 20 | _.match( 21 | component.dom, 22 | 'index.test.properties-true' 23 | ) 24 | ).toBe(true) 25 | }) 26 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const simulate = require('miniprogram-simulate') 3 | 4 | const config = require('../tools/config') 5 | 6 | const srcPath = config.srcPath 7 | const oldLoad = simulate.load 8 | simulate.load = function (componentPath, ...args) { 9 | componentPath = path.join(srcPath, componentPath) 10 | return oldLoad(componentPath, ...args) 11 | } 12 | 13 | module.exports = simulate 14 | 15 | // adjust the simulated wx api 16 | const oldGetSystemInfoSync = global.wx.getSystemInfoSync 17 | global.wx.getSystemInfoSync = function () { 18 | const res = oldGetSystemInfoSync() 19 | res.SDKVersion = '2.4.1' 20 | 21 | return res 22 | } 23 | -------------------------------------------------------------------------------- /test/wx.test.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const _ = require('./utils') 3 | 4 | test('wx.getSystemInfo', async () => { 5 | wx.getSystemInfo({ 6 | success(res) { 7 | expect(res.errMsg).toBe('getSystemInfo:ok') 8 | }, 9 | complete(res) { 10 | expect(res.errMsg).toBe('getSystemInfo:ok') 11 | } 12 | }) 13 | }) 14 | 15 | test('wx.getSystemInfoSync', async () => { 16 | const info = wx.getSystemInfoSync() 17 | expect(info.SDKVersion).toBe('2.4.1') 18 | expect(info.version).toBe('6.6.3') 19 | }) 20 | -------------------------------------------------------------------------------- /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 checkWxss = require('./checkwxss') 15 | const _ = require('./utils') 16 | 17 | const wxssConfig = config.wxss || {} 18 | const srcPath = config.srcPath 19 | const distPath = config.distPath 20 | 21 | /** 22 | * 获取 wxss 流 23 | */ 24 | function wxss(wxssFileList) { 25 | if (!wxssFileList.length) return false 26 | 27 | return gulp.src(wxssFileList, {cwd: srcPath, base: srcPath}) 28 | .pipe(checkWxss.start()) // 开始处理 import 29 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.init())) 30 | .pipe(gulpif(wxssConfig.less, less({paths: [srcPath]}))) 31 | .pipe(checkWxss.end()) // 结束处理 import 32 | .pipe(rename({extname: '.wxss'})) 33 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.write('./'))) 34 | .pipe(_.logger(wxssConfig.less ? 'generate' : undefined)) 35 | .pipe(gulp.dest(distPath)) 36 | } 37 | 38 | /** 39 | * 获取 js 流 40 | */ 41 | function js(jsFileMap, scope) { 42 | const webpackConfig = config.webpack 43 | const webpackCallback = (err, stats) => { 44 | if (!err) { 45 | // eslint-disable-next-line no-console 46 | console.log(stats.toString({ 47 | assets: true, 48 | cached: false, 49 | colors: true, 50 | children: false, 51 | errors: true, 52 | warnings: true, 53 | version: true, 54 | modules: false, 55 | publicPath: true, 56 | })) 57 | } else { 58 | // eslint-disable-next-line no-console 59 | console.log(err) 60 | } 61 | } 62 | 63 | webpackConfig.entry = jsFileMap 64 | webpackConfig.output.path = distPath 65 | 66 | if (scope.webpackWatcher) { 67 | scope.webpackWatcher.close() 68 | scope.webpackWatcher = null 69 | } 70 | 71 | if (config.isWatch) { 72 | scope.webpackWatcher = webpack(webpackConfig).watch({ 73 | ignored: /node_modules/, 74 | }, webpackCallback) 75 | } else { 76 | webpack(webpackConfig).run(webpackCallback) 77 | } 78 | } 79 | 80 | /** 81 | * 拷贝文件 82 | */ 83 | function copy(copyFileList) { 84 | if (!copyFileList.length) return false 85 | 86 | return gulp.src(copyFileList, {cwd: srcPath, base: srcPath}) 87 | .pipe(_.logger()) 88 | .pipe(gulp.dest(distPath)) 89 | } 90 | 91 | /** 92 | * 安装依赖包 93 | */ 94 | function install() { 95 | return gulp.series(async () => { 96 | const demoDist = config.demoDist 97 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 98 | const packageJson = _.readJson(path.resolve(__dirname, '../package.json')) 99 | const dependencies = packageJson.dependencies || {} 100 | 101 | await _.writeFile(demoPackageJsonPath, JSON.stringify({dependencies}, null, '\t')) // write dev demo's package.json 102 | }, () => { 103 | const demoDist = config.demoDist 104 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 105 | 106 | return gulp.src(demoPackageJsonPath) 107 | .pipe(gulpInstall({production: true})) 108 | }) 109 | } 110 | 111 | class BuildTask { 112 | constructor(id, entry) { 113 | if (!entry) return 114 | 115 | this.id = id 116 | this.entries = Array.isArray(config.entry) ? config.entry : [config.entry] 117 | this.copyList = Array.isArray(config.copy) ? config.copy : [] 118 | this.componentListMap = {} 119 | this.cachedComponentListMap = {} 120 | 121 | this.init() 122 | } 123 | 124 | init() { 125 | const id = this.id 126 | 127 | /** 128 | * 清空目标目录 129 | */ 130 | gulp.task(`${id}-clean-dist`, () => gulp.src(distPath, {read: false, allowEmpty: true}).pipe(clean())) 131 | 132 | /** 133 | * 拷贝 demo 到目标目录 134 | */ 135 | let isDemoExists = false 136 | gulp.task(`${id}-demo`, gulp.series(async () => { 137 | const demoDist = config.demoDist 138 | 139 | isDemoExists = await _.checkFileExists(path.join(demoDist, 'project.config.json')) 140 | }, done => { 141 | if (!isDemoExists) { 142 | const demoSrc = config.demoSrc 143 | const demoDist = config.demoDist 144 | 145 | return gulp.src('**/*', {cwd: demoSrc, base: demoSrc}) 146 | .pipe(gulp.dest(demoDist)) 147 | } 148 | 149 | return done() 150 | })) 151 | 152 | /** 153 | * 安装依赖包 154 | */ 155 | gulp.task(`${id}-install`, install()) 156 | 157 | /** 158 | * 检查自定义组件 159 | */ 160 | gulp.task(`${id}-component-check`, async () => { 161 | const entries = this.entries 162 | const mergeComponentListMap = {} 163 | for (let i = 0, len = entries.length; i < len; i++) { 164 | let entry = entries[i] 165 | entry = path.join(srcPath, `${entry}.json`) 166 | // eslint-disable-next-line no-await-in-loop 167 | const newComponentListMap = await checkComponents(entry) 168 | 169 | _.merge(mergeComponentListMap, newComponentListMap) 170 | } 171 | 172 | this.cachedComponentListMap = this.componentListMap 173 | this.componentListMap = mergeComponentListMap 174 | }) 175 | 176 | /** 177 | * 写 json 文件到目标目录 178 | */ 179 | gulp.task(`${id}-component-json`, done => { 180 | const jsonFileList = this.componentListMap.jsonFileList 181 | 182 | if (jsonFileList && jsonFileList.length) { 183 | return copy(this.componentListMap.jsonFileList) 184 | } 185 | 186 | return done() 187 | }) 188 | 189 | /** 190 | * 拷贝 wxml 文件到目标目录 191 | */ 192 | gulp.task(`${id}-component-wxml`, done => { 193 | const wxmlFileList = this.componentListMap.wxmlFileList 194 | 195 | if (wxmlFileList && 196 | wxmlFileList.length && 197 | !_.compareArray(this.cachedComponentListMap.wxmlFileList, wxmlFileList)) { 198 | return copy(wxmlFileList) 199 | } 200 | 201 | return done() 202 | }) 203 | 204 | /** 205 | * 生成 wxss 文件到目标目录 206 | */ 207 | gulp.task(`${id}-component-wxss`, done => { 208 | const wxssFileList = this.componentListMap.wxssFileList 209 | 210 | if (wxssFileList && 211 | wxssFileList.length && 212 | !_.compareArray(this.cachedComponentListMap.wxssFileList, wxssFileList)) { 213 | return wxss(wxssFileList, srcPath, distPath) 214 | } 215 | 216 | return done() 217 | }) 218 | 219 | /** 220 | * 生成 js 文件到目标目录 221 | */ 222 | gulp.task(`${id}-component-js`, done => { 223 | const jsFileList = this.componentListMap.jsFileList 224 | 225 | if (jsFileList && 226 | jsFileList.length && 227 | !_.compareArray(this.cachedComponentListMap.jsFileList, jsFileList)) { 228 | js(this.componentListMap.jsFileMap, this) 229 | } 230 | 231 | return done() 232 | }) 233 | 234 | /** 235 | * 拷贝相关资源到目标目录 236 | */ 237 | gulp.task(`${id}-copy`, gulp.parallel(done => { 238 | const copyList = this.copyList 239 | const copyFileList = copyList.map(dir => path.join(dir, '**/*.!(wxss)')) 240 | 241 | if (copyFileList.length) return copy(copyFileList) 242 | 243 | return done() 244 | }, done => { 245 | const copyList = this.copyList 246 | const copyFileList = copyList.map(dir => path.join(dir, '**/*.wxss')) 247 | 248 | if (copyFileList.length) return wxss(copyFileList, srcPath, distPath) 249 | 250 | return done() 251 | })) 252 | 253 | /** 254 | * 监听 json 变化 255 | */ 256 | 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`)))) 257 | 258 | /** 259 | * 监听 wxml 变化 260 | */ 261 | gulp.task(`${id}-watch-wxml`, () => { 262 | this.cachedComponentListMap.wxmlFileList = null 263 | return gulp.watch(this.componentListMap.wxmlFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxml`)) 264 | }) 265 | 266 | /** 267 | * 监听 wxss 变化 268 | */ 269 | gulp.task(`${id}-watch-wxss`, () => { 270 | this.cachedComponentListMap.wxssFileList = null 271 | return gulp.watch('**/*.wxss', {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxss`)) 272 | }) 273 | 274 | /** 275 | * 监听相关资源变化 276 | */ 277 | gulp.task(`${id}-watch-copy`, () => { 278 | const copyList = this.copyList 279 | const copyFileList = copyList.map(dir => path.join(dir, '**/*')) 280 | const watchCallback = filePath => copy([filePath]) 281 | 282 | return gulp.watch(copyFileList, {cwd: srcPath, base: srcPath}) 283 | .on('change', watchCallback) 284 | .on('add', watchCallback) 285 | .on('unlink', watchCallback) 286 | }) 287 | 288 | /** 289 | * 监听 demo 变化 290 | */ 291 | gulp.task(`${id}-watch-demo`, () => { 292 | const demoSrc = config.demoSrc 293 | const demoDist = config.demoDist 294 | const watchCallback = filePath => gulp.src(filePath, {cwd: demoSrc, base: demoSrc}) 295 | .pipe(gulp.dest(demoDist)) 296 | 297 | return gulp.watch('**/*', {cwd: demoSrc, base: demoSrc}) 298 | .on('change', watchCallback) 299 | .on('add', watchCallback) 300 | .on('unlink', watchCallback) 301 | }) 302 | 303 | /** 304 | * 监听安装包列表变化 305 | */ 306 | gulp.task(`${id}-watch-install`, () => gulp.watch(path.resolve(__dirname, '../package.json'), install())) 307 | 308 | /** 309 | * 构建相关任务 310 | */ 311 | 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`))) 312 | 313 | 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`))) 314 | 315 | gulp.task(`${id}-dev`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`)) 316 | 317 | gulp.task(`${id}-default`, gulp.series(`${id}-build`)) 318 | } 319 | } 320 | 321 | module.exports = BuildTask 322 | -------------------------------------------------------------------------------- /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 | * 获取 json 路径相关信息 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 | * 检测是否包含其他自定义组件 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 | // 检查相对路径 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 | // 进入存储 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: {}, // 为 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/checkwxss.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const through = require('through2') 3 | const Vinyl = require('vinyl') 4 | 5 | const _ = require('./utils') 6 | 7 | /** 8 | * 获取 import 列表 9 | */ 10 | function getImportList(wxss, filePath) { 11 | const reg = /@import\s+(?:(?:"([^"]+)")|(?:'([^"]+)'));/ig 12 | const importList = [] 13 | let execRes = reg.exec(wxss) 14 | 15 | while (execRes && (execRes[1] || execRes[2])) { 16 | importList.push({ 17 | code: execRes[0], 18 | path: path.join(path.dirname(filePath), execRes[1] || execRes[2]), 19 | }) 20 | execRes = reg.exec(wxss) 21 | } 22 | 23 | return importList 24 | } 25 | 26 | /** 27 | * 获取 wxss 内容 28 | */ 29 | async function getContent(wxss, filePath, cwd) { 30 | let importList = [] 31 | 32 | if (wxss) { 33 | const currentImportList = getImportList(wxss, filePath) 34 | 35 | for (const item of currentImportList) { 36 | // 替换掉 import 语句,不让 less 编译 37 | wxss = wxss.replace(item.code, `/* *updated for miniprogram-custom-component* ${item.code} */`) 38 | 39 | // 处理依赖的 wxss 40 | const importWxss = await _.readFile(item.path) 41 | const importInfo = await getContent(importWxss, item.path, cwd) 42 | 43 | // 获取依赖列表 44 | importList.push(new Vinyl({ 45 | cwd, 46 | path: item.path, 47 | contents: Buffer.from(importInfo.wxss, 'utf8'), 48 | })) 49 | importList = importList.concat(importInfo.importList) 50 | } 51 | } 52 | 53 | return { 54 | wxss, 55 | importList, 56 | } 57 | } 58 | 59 | module.exports = { 60 | start() { 61 | return through.obj(function (file, enc, cb) { 62 | if (file.isBuffer()) { 63 | getContent(file.contents.toString('utf8'), file.path, file.cwd).then(res => { 64 | const { wxss, importList } = res 65 | 66 | importList.forEach(importFile => this.push(importFile)) 67 | 68 | file.contents = Buffer.from(wxss, 'utf8') 69 | this.push(file) 70 | cb() 71 | }).catch(err => { 72 | // eslint-disable-next-line no-console 73 | 74 | console.warn(`deal with ${file.path} failed: ${err.stack}`) 75 | this.push(file) 76 | cb() 77 | }) 78 | } else { 79 | this.push(file) 80 | cb() 81 | } 82 | }) 83 | }, 84 | 85 | end() { 86 | return through.obj(function (file, enc, cb) { 87 | if (file.isBuffer) { 88 | const reg = /\/\*\s\*updated for miniprogram-custom-component\*\s(@import\s+(?:(?:"([^"]+)")|(?:'([^"]+)'));)\s\*\//ig 89 | const wxss = file.contents.toString('utf8').replace(reg, (all, $1) => $1) 90 | 91 | file.contents = Buffer.from(wxss, 'utf8') 92 | this.push(file) 93 | cb() 94 | } else { 95 | this.push(file) 96 | cb() 97 | } 98 | }) 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /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, // demo 源目录 23 | demoDist, // demo 目标目录 24 | 25 | wxss: { 26 | less: true, // 使用 less 来编写 wxss 27 | sourcemap: false, // 生成 less sourcemap 28 | }, 29 | 30 | webpack: { 31 | mode: 'production', 32 | output: { 33 | filename: '[name].js', 34 | libraryTarget: 'commonjs2', 35 | }, 36 | target: 'node', 37 | externals: [nodeExternals()], // 忽略 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', // 生成 js sourcemap 60 | performance: { 61 | hints: 'warning', 62 | assetFilter: assetFilename => assetFilename.endsWith('.js') 63 | } 64 | }, 65 | 66 | copy: ['./images'], // 将会复制到目标目录 67 | } 68 | -------------------------------------------------------------------------------- /tools/demo/app.js: -------------------------------------------------------------------------------- 1 | App({}) 2 | -------------------------------------------------------------------------------- /tools/demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": ["pages/index/index", "pages/index/mine"], 3 | "window": { 4 | "backgroundTextStyle": "light", 5 | "navigationBarBackgroundColor": "#fff", 6 | "navigationBarTitleText": "WeChat", 7 | "navigationBarTextStyle": "black" 8 | }, 9 | "tabBar": { 10 | "color": "#000000", 11 | "selectedColor": "#db0028", 12 | "backgroundColor": "#FFFFFF", 13 | "borderStyle": "black", 14 | "customNode": { 15 | "iconPath": "/image/x.png", 16 | "activeStyle": "transform: rotate(45deg);", 17 | "style": "transform: rotate(0deg);" 18 | }, 19 | "list": [ 20 | { 21 | "iconPath": "/image/index.png", 22 | "selectedIconPath": "/image/index-pre.png", 23 | "pagePath": "pages/index/index", 24 | "text": "首页" 25 | }, 26 | { 27 | "iconPath": "/image/mine.png", 28 | "selectedIconPath": "/image/mine-pre.png", 29 | "pagePath": "pages/index/mine", 30 | "text": "我的" 31 | } 32 | ] 33 | }, 34 | "usingComponents": { 35 | "custom-tab-bar": "/components/index" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tools/demo/app.wxss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/app.wxss -------------------------------------------------------------------------------- /tools/demo/image/index-pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/index-pre.png -------------------------------------------------------------------------------- /tools/demo/image/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/index.png -------------------------------------------------------------------------------- /tools/demo/image/mine-pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/mine-pre.png -------------------------------------------------------------------------------- /tools/demo/image/mine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/mine.png -------------------------------------------------------------------------------- /tools/demo/image/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ljybill/miniprogram-custom-tab-bar/5948e3e25b60e3f473084dc7971a821377d06ab1/tools/demo/image/x.png -------------------------------------------------------------------------------- /tools/demo/package.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tools/demo/pages/index/index.js: -------------------------------------------------------------------------------- 1 | const app = getApp(); 2 | 3 | Page({ 4 | data: {}, 5 | onLoad: function() {}, 6 | setRedDot() { 7 | wx.showTabBarRedDot({ 8 | index: 1 9 | }); 10 | }, 11 | hideRedDot() { 12 | wx.hideTabBarRedDot({ 13 | index: 1 14 | }); 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 直接复制custom-tab-bar文件夹到你的项目,并配置全局组件即可使用 2 | 3 | 设置红点方法测试 4 | 5 | 6 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | .hr { 2 | margin: 10rpx 0; 3 | border-top: 10rpx solid #eee; 4 | } -------------------------------------------------------------------------------- /tools/demo/pages/index/mine.js: -------------------------------------------------------------------------------- 1 | // index/mine.js 2 | Page({ 3 | 4 | /** 5 | * 页面的初始数据 6 | */ 7 | data: { 8 | 9 | }, 10 | 11 | /** 12 | * 生命周期函数--监听页面加载 13 | */ 14 | onLoad: function (options) { 15 | 16 | }, 17 | 18 | /** 19 | * 生命周期函数--监听页面初次渲染完成 20 | */ 21 | onReady: function () { 22 | 23 | }, 24 | 25 | /** 26 | * 生命周期函数--监听页面显示 27 | */ 28 | onShow: function () { 29 | 30 | }, 31 | 32 | /** 33 | * 生命周期函数--监听页面隐藏 34 | */ 35 | onHide: function () { 36 | 37 | }, 38 | 39 | /** 40 | * 生命周期函数--监听页面卸载 41 | */ 42 | onUnload: function () { 43 | 44 | }, 45 | 46 | /** 47 | * 页面相关事件处理函数--监听用户下拉动作 48 | */ 49 | onPullDownRefresh: function () { 50 | 51 | }, 52 | 53 | /** 54 | * 页面上拉触底事件的处理函数 55 | */ 56 | onReachBottom: function () { 57 | 58 | }, 59 | 60 | /** 61 | * 用户点击右上角分享 62 | */ 63 | onShareAppMessage: function () { 64 | 65 | } 66 | }) -------------------------------------------------------------------------------- /tools/demo/pages/index/mine.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /tools/demo/pages/index/mine.wxml: -------------------------------------------------------------------------------- 1 | index/mine.wxml 2 | -------------------------------------------------------------------------------- /tools/demo/pages/index/mine.wxss: -------------------------------------------------------------------------------- 1 | /* index/mine.wxss */ -------------------------------------------------------------------------------- /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": "miniprogram-demo", 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 | } -------------------------------------------------------------------------------- /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 | * 异步函数封装 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 | * 调整路径分隔符 40 | */ 41 | function transformPath(filePath, sep = '/') { 42 | return filePath.replace(/[\\/]/g, sep) 43 | } 44 | 45 | /** 46 | * 检查文件是否存在 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 | * 递归创建目录 59 | */ 60 | async function recursiveMkdir(dirPath) { 61 | const prevDirPath = path.dirname(dirPath) 62 | try { 63 | await accessSync(prevDirPath) 64 | } catch (err) { 65 | // 上一级目录不存在 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 | // 目标路径存在,但不是目录 75 | await renameSync(dirPath, `${dirPath}.bak`) // 将此文件重命名为 .bak 后缀 76 | await mkdirSync(dirPath) 77 | } 78 | } catch (err) { 79 | // 目标路径不存在 80 | await mkdirSync(dirPath) 81 | } 82 | } 83 | 84 | /** 85 | * 读取 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 | * 读取文件 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 | * 写文件 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 | * 时间格式化 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 | * 日志插件 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 | * 比较数组是否相等 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 | * 合并两个对象 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 | * 获取 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 | --------------------------------------------------------------------------------