├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── gulpfile.js ├── package.json ├── src ├── barrage-canvas.js ├── barrage-dom.js ├── index.js ├── index.json ├── index.wxml ├── index.wxss └── utils.js ├── 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 ├── assets │ ├── barrage.png │ ├── bookmark.png │ ├── car.png │ └── iconfont.wxss ├── package.json ├── pages │ └── index │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ ├── index.wxss │ │ └── util.js └── project.config.json └── utils.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["module-resolver", { 4 | "root": ["./src"], 5 | "alias": {} 6 | }] 7 | ], 8 | "presets": [ 9 | ["env", {"loose": true, "modules": "commonjs"}] 10 | ] 11 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'airbnb-base', 4 | 'plugin:promise/recommended' 5 | ], 6 | 'parserOptions': { 7 | 'ecmaVersion': 9, 8 | 'ecmaFeatures': { 9 | 'jsx': false 10 | }, 11 | 'sourceType': 'module' 12 | }, 13 | 'env': { 14 | 'es6': true, 15 | 'node': true, 16 | 'jest': true 17 | }, 18 | 'plugins': [ 19 | 'import', 20 | 'node', 21 | 'promise' 22 | ], 23 | 'rules': { 24 | '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 | 'no-await-in-loop': 'off', 86 | 'no-restricted-syntax': 'off', 87 | 'promise/always-return': 'off', 88 | }, 89 | 'globals': { 90 | 'window': true, 91 | 'document': true, 92 | 'App': true, 93 | 'Page': true, 94 | 'Component': true, 95 | 'Behavior': true, 96 | 'wx': true, 97 | 'getCurrentPages': true, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | miniprogram_dist 12 | miniprogram_dev 13 | node_modules 14 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | package-lock.json 4 | 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | 11 | test 12 | tools 13 | docs 14 | miniprogram_dev 15 | node_modules 16 | coverage -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 sanfordsun 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 | # Barrage for MiniProgram 2 | 3 | [![](https://img.shields.io/npm/v/miniprogram-barrage)](https://www.npmjs.com/package/miniprogram-barrage) 4 | [![](https://img.shields.io/npm/l/miniprogram-barrage)](https://github.com/wechat-miniprogram/miniprogram-barrage) 5 | 6 | 小程序弹幕组件,覆盖在 原生组件上时,请确保组件已经同层化。[参考用例](https://developers.weixin.qq.com/s/FvXaI3mt7EgY)。弹幕组件的实现采用了 canvas & dom 两种方式,通过 rendering-mode 属性进行指定。dom 的方式兼容性高,当对小程序基础库低版本( v2.9.2 以下)有要求时,可以采用这种渲染方式。canvas 的方式通常性能更好,动画更为流畅,但仅在基础库 v2.9.2 版本及以上可以使用。 7 | 8 | 注意事项:在开发者工具上,canvas 的渲染方式无法使用 `view` 等普通组件覆盖在弹幕上方,需采用 `cover-view`。真机上可以使用普通的 `view` 覆盖在弹幕上。 9 | 10 | ## 属性列表 11 | 12 | | 属性 | 类型 | 默认值 | 必填 | 说明 | 13 | | -------------- | ------ | ------ | ---- | ---------- | 14 | | z-index | number | 10 | 否 | 弹幕的层级 | 15 | | rendering-mode | string | canvas | 否 | 渲染模式 | 16 | 17 | 18 | ## 使用方法 19 | 1. npm 安装,参考 [小程序 npm 支持](https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html) 20 | 21 | ``` 22 | npm install --save miniprogram-barrage 23 | ``` 24 | 25 | 2. JSON 组件声明 26 | ```json 27 | { 28 | "usingComponents": { 29 | "barrage": "miniprogram-barrage", 30 | } 31 | } 32 | 33 | ``` 34 | 35 | 3. wxml 引入弹幕组件 36 | ```html 37 | 40 | ``` 41 | 42 | 4. js 获取实例 43 | ```js 44 | Page({ 45 | onReady() { 46 | this.addBarrage() 47 | }, 48 | addBarrage() { 49 | const barrageComp = this.selectComponent('.barrage') 50 | this.barrage = barrageComp.getBarrageInstance({ 51 | font: 'bold 16px sans-serif', 52 | duration: this.data.duration, 53 | lineHeight: 2, 54 | mode: 'separate', 55 | padding: [10, 0, 10, 0], 56 | range: [0, 1] 57 | }) 58 | this.barrage.open() 59 | this.barrage.addData(data) 60 | } 61 | }) 62 | 63 | ``` 64 | 65 | ## 配置 66 | ### Barrage 默认配置 67 | ```js 68 | { 69 | duration: 15, // 弹幕动画时长 (移动 2000px 所需时长) 70 | lineHeight: 1.2, // 弹幕行高 71 | padding: [0, 0, 0, 0], // 弹幕区四周留白 72 | alpha: 1, // 全局透明度 73 | font: '10px sans-serif', // 字体大小 74 | mode: 'separate', // 弹幕重叠 overlap 不重叠 separate 75 | range: [0, 1], // 弹幕显示的垂直范围,支持两个值。[0,1]表示弹幕整个随机分布, 76 | tunnelShow: false, // 显示轨道线 77 | tunnelMaxNum: 30, // 隧道最大缓冲长度 78 | maxLength: 30, // 弹幕最大字节长度,汉字算双字节 79 | safeGap: 4, // 发送时的安全间隔 80 | } 81 | ``` 82 | ### 弹幕数据配置 83 | ```js 84 | { 85 | color: '#000000', // 默认黑色 86 | content: '', // 弹幕内容 87 | image: { 88 | head: {src, width, height, gap = 4}, // 弹幕头部添加图片 89 | tail: {src, width, height, gap = 4}, // 弹幕尾部添加图片 90 | } 91 | 92 | } 93 | ``` 94 | 95 | ## 接口 96 | ```js 97 | barrage.open() // 开启弹幕功能 98 | barrage.close() // 关闭弹幕功能,清空弹幕 99 | barrage.addData(data: array) // 添加弹幕数据 100 | barrage.send(data: object) // 发送一条弹幕数据 101 | barrage.setRange(range: array) // 设置垂直方向显示范围,默认 [0, 1] 102 | barrage.setFont(font: string) // 设置全局字体,注 canvas 的渲染方式仅可设置大小,不支持字体设置 103 | barrage.setAlpha(alpha: number) // 设置全局透明度, alpha 0 ~ 1, 值越小,越透明 104 | barrage.showTunnel() // 显示弹幕轨道 105 | barrage.hideTunnel() // 隐藏弹幕轨道 106 | ``` 107 | -------------------------------------------------------------------------------- /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-barrage", 3 | "version": "1.1.0", 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 --bail", 14 | "test-debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --bail", 15 | "coverage": "jest ./test/* --coverage --bail", 16 | "lint": "eslint \"src/**/*.js\" --fix", 17 | "lint-tools": "eslint \"tools/**/*.js\" --rule \"import/no-extraneous-dependencies: false\" --fix" 18 | }, 19 | "miniprogram": "miniprogram_dist", 20 | "jest": { 21 | "testEnvironment": "jsdom", 22 | "testURL": "https://jest.test", 23 | "collectCoverageFrom": [ 24 | "src/**/*.js" 25 | ], 26 | "moduleDirectories": [ 27 | "node_modules", 28 | "src" 29 | ] 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "" 34 | }, 35 | "author": "sanfordsun", 36 | "license": "MIT", 37 | "devDependencies": { 38 | "babel-core": "^6.26.3", 39 | "babel-loader": "^7.1.5", 40 | "babel-plugin-module-resolver": "^3.2.0", 41 | "babel-preset-env": "^1.7.0", 42 | "colors": "^1.3.1", 43 | "eslint": "^5.14.1", 44 | "eslint-config-airbnb-base": "13.1.0", 45 | "eslint-loader": "^2.1.2", 46 | "eslint-plugin-import": "^2.16.0", 47 | "eslint-plugin-node": "^7.0.1", 48 | "eslint-plugin-promise": "^3.8.0", 49 | "gulp": "^4.0.0", 50 | "gulp-clean": "^0.4.0", 51 | "gulp-if": "^2.0.2", 52 | "gulp-install": "^1.1.0", 53 | "gulp-less": "^4.0.1", 54 | "gulp-rename": "^1.4.0", 55 | "gulp-sourcemaps": "^2.6.5", 56 | "jest": "^23.5.0", 57 | "miniprogram-simulate": "^1.0.0", 58 | "through2": "^2.0.3", 59 | "vinyl": "^2.2.0", 60 | "webpack": "^4.29.5", 61 | "webpack-node-externals": "^1.7.2" 62 | }, 63 | "dependencies": {} 64 | } 65 | -------------------------------------------------------------------------------- /src/barrage-canvas.js: -------------------------------------------------------------------------------- 1 | import {substring, getFontSize, getRandom} from './utils' 2 | 3 | class Bullet { 4 | constructor(barrage, opt = {}) { 5 | const defaultBulletOpt = { 6 | color: '#000000', // 默认黑色 7 | font: '10px sans-serif', 8 | fontSize: 10, // 全局字体大小 9 | content: '', 10 | textWidth: 0, 11 | speed: 0, // 根据屏幕停留时长计算 12 | x: 0, 13 | y: 0, 14 | tunnelId: 0, 15 | // image: { 16 | // head: {src, width, height}, // 弹幕头部添加图片 17 | // tail: {src, width, height}, // 弹幕尾部添加图片 18 | // gap: 4 // 图片与文本间隔 19 | // } 20 | image: {}, 21 | imageHead: null, // Image 对象 22 | imageTail: null, 23 | // status: 0 //0:待播放 1: 未完全进入屏幕 2: 完全进入屏幕 3: 完全退出屏幕 24 | } 25 | Object.assign(this, defaultBulletOpt, opt) 26 | 27 | this.barrage = barrage 28 | this.ctx = barrage.ctx 29 | this.canvas = barrage.canvas 30 | } 31 | 32 | move() { 33 | if (this.image.head && !this.imageHead) { 34 | const Image = this.canvas.createImage() 35 | Image.src = this.image.head.src 36 | Image.onload = () => { 37 | this.imageHead = Image 38 | } 39 | Image.onerror = () => { 40 | // eslint-disable-next-line no-console 41 | console.log(`Fail to load image: ${this.image.head.src}`) 42 | } 43 | } 44 | 45 | if (this.image.tail && !this.imageTail) { 46 | const Image = this.canvas.createImage() 47 | Image.src = this.image.tail.src 48 | Image.onload = () => { 49 | this.imageTail = Image 50 | } 51 | Image.onerror = () => { 52 | // eslint-disable-next-line no-console 53 | console.log(`Fail to load image: ${this.image.tail.src}`) 54 | } 55 | } 56 | 57 | if (this.imageHead) { 58 | const { 59 | width = this.fontSize, 60 | height = this.fontSize, 61 | gap = 4 62 | } = this.image.head 63 | const x = this.x - gap - width 64 | const y = this.y - 0.5 * height 65 | this.ctx.drawImage(this.imageHead, x, y, width, height) 66 | } 67 | 68 | if (this.imageTail) { 69 | const { 70 | width = this.fontSize, 71 | height = this.fontSize, 72 | gap = 4 73 | } = this.image.tail 74 | const x = this.x + this.textWidth + gap 75 | const y = this.y - 0.5 * height 76 | this.ctx.drawImage(this.imageTail, x, y, width, height) 77 | } 78 | 79 | this.x = this.x - this.speed 80 | this.ctx.fillStyle = this.color 81 | this.ctx.fillText(this.content, this.x, this.y) 82 | } 83 | } 84 | 85 | // tunnel(轨道) 86 | class Tunnel { 87 | constructor(barrage, opt = {}) { 88 | const defaultTunnelOpt = { 89 | activeQueue: [], // 正在屏幕中列表 90 | nextQueue: [], // 待播放列表 91 | maxNum: 30, 92 | freeNum: 30, // 剩余可添加量 93 | height: 0, 94 | width: 0, 95 | disabled: false, 96 | tunnelId: 0, 97 | safeArea: 4, 98 | sending: false, // 弹幕正在发送 99 | } 100 | Object.assign(this, defaultTunnelOpt, opt) 101 | 102 | this.freeNum = this.maxNum 103 | this.barrage = barrage // 控制中心 104 | this.ctx = barrage.ctx 105 | } 106 | 107 | disable() { 108 | this.disabled = true 109 | } 110 | 111 | enable() { 112 | this.disabled = false 113 | } 114 | 115 | clear() { 116 | this.activeQueue = [] 117 | this.nextQueue = [] 118 | this.sending = false 119 | this.freeNum = this.maxNum 120 | this.barrage.addIdleTunnel(this.tunnelId) 121 | } 122 | 123 | addBullet(bullet) { 124 | if (this.disabled) return 125 | if (this.freeNum === 0) return 126 | this.nextQueue.push(bullet) 127 | this.freeNum-- 128 | if (this.freeNum === 0) { 129 | this.barrage.removeIdleTunnel(this.tunnelId) 130 | } 131 | } 132 | 133 | animate() { 134 | if (this.disabled) return 135 | // 无正在发送弹幕,添加一条 136 | const nextQueue = this.nextQueue 137 | const activeQueue = this.activeQueue 138 | if (!this.sending && nextQueue.length > 0) { 139 | const bullet = nextQueue.shift() 140 | activeQueue.push(bullet) 141 | this.freeNum++ 142 | this.sending = true 143 | this.barrage.addIdleTunnel(this.tunnelId) 144 | } 145 | 146 | if (activeQueue.length > 0) { 147 | activeQueue.forEach(bullet => bullet.move()) 148 | const head = activeQueue[0] 149 | const tail = activeQueue[activeQueue.length - 1] 150 | // 队首移出屏幕 151 | if (head.x + head.textWidth < 0) { 152 | activeQueue.shift() 153 | } 154 | // 队尾离开超过安全区 155 | if (tail.x + tail.textWidth + this.safeArea < this.width) { 156 | this.sending = false 157 | } 158 | } 159 | } 160 | } 161 | 162 | class Barrage { 163 | constructor(opt = {}) { 164 | const defaultBarrageOpt = { 165 | font: '10px sans-serif', 166 | duration: 15, // 弹幕屏幕停留时长 167 | lineHeight: 1.2, 168 | padding: [0, 0, 0, 0], 169 | tunnelHeight: 0, 170 | tunnelNum: 0, 171 | tunnelMaxNum: 30, // 隧道最大缓冲长度 172 | maxLength: 30, // 最大字节长度,汉字算双字节 173 | safeArea: 4, // 发送时的安全间隔 174 | tunnels: [], 175 | idleTunnels: [], 176 | enableTunnels: [], 177 | alpha: 1, // 全局透明度 178 | mode: 'separate', // 弹幕重叠 overlap 不重叠 separate 179 | range: [0, 1], // 弹幕显示的垂直范围,支持两个值。[0,1]表示弹幕整个随机分布, 180 | fps: 60, // 刷新率 181 | tunnelShow: false, // 显示轨道线 182 | comp: null, // 组件实例 183 | } 184 | Object.assign(this, defaultBarrageOpt, opt) 185 | const systemInfo = wx.getSystemInfoSync() 186 | this.ratio = systemInfo.pixelRatio 187 | this.selector = '#weui-canvas' 188 | this._ready = false 189 | this._deferred = [] 190 | 191 | const query = this.comp.createSelectorQuery() 192 | query.select(this.selector).boundingClientRect() 193 | query.select(this.selector).node() 194 | query.exec((res) => { 195 | this.canvas = res[1].node 196 | this.init(res[0]) 197 | this.ready() 198 | }) 199 | } 200 | 201 | ready() { 202 | this._ready = true 203 | this._deferred.forEach(item => { 204 | // eslint-disable-next-line prefer-spread 205 | this[item.callback].apply(this, item.args) 206 | }) 207 | 208 | this._deferred = [] 209 | } 210 | 211 | _delay(method, args) { 212 | this._deferred.push({ 213 | callback: method, 214 | args 215 | }) 216 | } 217 | 218 | init(opt = {}) { 219 | this.width = opt.width 220 | this.height = opt.height 221 | this.fontSize = getFontSize(this.font) 222 | this.innerDuration = this.transfromDuration2Canvas(this.duration) 223 | 224 | const ratio = this.ratio// 设备像素比 225 | this.canvas.width = this.width * ratio 226 | this.canvas.height = this.height * ratio 227 | this.ctx = this.canvas.getContext('2d') 228 | this.ctx.scale(ratio, ratio) 229 | 230 | this.ctx.textBaseline = 'middle' 231 | this.ctx.globalAlpha = this.alpha 232 | this.ctx.font = this.font 233 | 234 | this.idleTunnels = [] 235 | this.enableTunnels = [] 236 | this.tunnels = [] 237 | 238 | this.availableHeight = (this.height - this.padding[0] - this.padding[2]) 239 | this.tunnelHeight = this.fontSize * this.lineHeight 240 | this.tunnelNum = Math.floor(this.availableHeight / this.tunnelHeight) 241 | for (let i = 0; i < this.tunnelNum; i++) { 242 | this.idleTunnels.push(i) // 空闲的隧道id集合 243 | this.enableTunnels.push(i) // 可用的隧道id集合 244 | this.tunnels.push(new Tunnel(this, { // 隧道集合 245 | width: this.width, 246 | height: this.tunnelHeight, 247 | safeArea: this.safeArea, 248 | maxNum: this.tunnelMaxNum, 249 | tunnelId: i, 250 | })) 251 | } 252 | // 筛选符合范围的隧道 253 | this.setRange() 254 | this._isActive = false 255 | } 256 | 257 | transfromDuration2Canvas(duration) { 258 | // 2000 是 dom 中移动的距离 259 | return duration * this.width / 2000 260 | } 261 | 262 | // 设置显示范围 range: [0,1] 263 | setRange(range) { 264 | if (!this._ready) { 265 | this._delay('setRange', range) 266 | return 267 | } 268 | 269 | range = range || this.range 270 | const top = range[0] * this.tunnelNum 271 | const bottom = range[1] * this.tunnelNum 272 | 273 | // 释放符合要求的隧道 274 | // 找到目前空闲的隧道 275 | const idleTunnels = [] 276 | const enableTunnels = [] 277 | this.tunnels.forEach((tunnel, tunnelId) => { 278 | if (tunnelId >= top && tunnelId < bottom) { 279 | tunnel.enable() 280 | enableTunnels.push(tunnelId) 281 | if (this.idleTunnels.indexOf(tunnelId) >= 0) { 282 | idleTunnels.push(tunnelId) 283 | } 284 | } else { 285 | tunnel.disable() 286 | } 287 | }) 288 | this.idleTunnels = idleTunnels 289 | this.enableTunnels = enableTunnels 290 | this.range = range 291 | } 292 | 293 | setFont(font) { 294 | if (!this._ready) { 295 | this._delay('setFont', font) 296 | return 297 | } 298 | 299 | this.font = font 300 | this.fontSize = getFontSize(this.font) 301 | this.ctx.font = font 302 | } 303 | 304 | setAlpha(alpha) { 305 | if (!this._ready) { 306 | this._delay('setAlpha', alpha) 307 | return 308 | } 309 | 310 | this.alpha = alpha 311 | this.ctx.globalAlpha = alpha 312 | } 313 | 314 | setDuration(duration) { 315 | if (!this._ready) { 316 | this._delay('setDuration', duration) 317 | return 318 | } 319 | 320 | this.clear() 321 | this.duration = duration 322 | this.innerDuration = this.transfromDuration2Canvas(duration) 323 | } 324 | 325 | // 开启弹幕 326 | open() { 327 | if (!this._ready) { 328 | this._delay('open') 329 | return 330 | } 331 | 332 | if (this._isActive) return 333 | this._isActive = true 334 | this.play() 335 | } 336 | 337 | // 关闭弹幕,清除所有数据 338 | close() { 339 | if (!this._ready) { 340 | this._delay('close') 341 | return 342 | } 343 | 344 | if (!this._isActive) return 345 | this._isActive = false 346 | this.pause() 347 | this.clear() 348 | } 349 | 350 | // 开启弹幕滚动 351 | play() { 352 | this._rAFId = this.canvas.requestAnimationFrame(() => { 353 | this.animate() 354 | this.play() 355 | }) 356 | } 357 | 358 | // 停止弹幕滚动 359 | pause() { 360 | if (typeof this._rAFId === 'number') { 361 | this.canvas.cancelAnimationFrame(this._rAFId) 362 | } 363 | } 364 | 365 | // 清空屏幕和缓冲的数据 366 | clear() { 367 | this.ctx.clearRect(0, 0, this.width, this.height) 368 | this.tunnels.forEach(tunnel => tunnel.clear()) 369 | } 370 | 371 | // 添加一批弹幕,轨道满时会被丢弃 372 | addData(data = []) { 373 | if (!this._ready) { 374 | this._delay('addData', data) 375 | return 376 | } 377 | 378 | if (!this._isActive) return 379 | data.forEach(item => this.addBullet2Tunnel(item)) 380 | } 381 | 382 | // 发送一条弹幕 383 | // 为保证发送成功,选取一条可用隧道,替换待发送队列队头元素 384 | send(opt = {}) { 385 | if (!this._ready) { 386 | this._delay('send', opt) 387 | return 388 | } 389 | 390 | const tunnel = this.getEnableTunnel() 391 | if (tunnel === null) return 392 | 393 | opt.tunnelId = tunnel.tunnelId 394 | const bullet = this.registerBullet(opt) 395 | tunnel.nextQueue[0] = bullet 396 | } 397 | 398 | // 添加至轨道 {content, color} 399 | addBullet2Tunnel(opt = {}) { 400 | const tunnel = this.getIdleTunnel() 401 | if (tunnel === null) return 402 | 403 | opt.tunnelId = tunnel.tunnelId 404 | const bullet = this.registerBullet(opt) 405 | tunnel.addBullet(bullet) 406 | } 407 | 408 | registerBullet(opt = {}) { 409 | opt.tunnelId = opt.tunnelId || 0 410 | opt.content = substring(opt.content, this.maxLength) 411 | const textWidth = this.getTextWidth(opt.content) 412 | const distance = this.mode === 'overlap' ? this.width + textWidth : this.width 413 | opt.textWidth = textWidth 414 | opt.speed = distance / (this.innerDuration * this.fps) 415 | opt.fontSize = this.fontSize 416 | opt.x = this.width 417 | opt.y = this.tunnelHeight * (opt.tunnelId + 0.5) + this.padding[0] 418 | return new Bullet(this, opt) 419 | } 420 | 421 | // 每帧执行的操作 422 | animate() { 423 | // 清空画面后重绘 424 | this.ctx.clearRect(0, 0, this.width, this.height) 425 | if (this.tunnelShow) { 426 | this.drawTunnel() 427 | } 428 | this.tunnels.forEach(tunnel => tunnel.animate()) 429 | } 430 | 431 | showTunnel() { 432 | this.tunnelShow = true 433 | } 434 | 435 | hideTunnel() { 436 | this.tunnelShow = false 437 | } 438 | 439 | removeIdleTunnel(tunnelId) { 440 | const idx = this.idleTunnels.indexOf(tunnelId) 441 | if (idx >= 0) this.idleTunnels.splice(idx, 1) 442 | } 443 | 444 | addIdleTunnel(tunnelId) { 445 | const idx = this.idleTunnels.indexOf(tunnelId) 446 | if (idx < 0) this.idleTunnels.push(tunnelId) 447 | } 448 | 449 | // 从可用的隧道中随机挑选一个 450 | getEnableTunnel() { 451 | if (this.enableTunnels.length === 0) return null 452 | const index = getRandom(this.enableTunnels.length) 453 | return this.tunnels[this.enableTunnels[index]] 454 | } 455 | 456 | // 从还有余量的隧道中随机挑选一个 457 | getIdleTunnel() { 458 | if (this.idleTunnels.length === 0) return null 459 | const index = getRandom(this.idleTunnels.length) 460 | return this.tunnels[this.idleTunnels[index]] 461 | } 462 | 463 | getTextWidth(content) { 464 | this.ctx.font = this.font 465 | return Math.ceil(this.ctx.measureText(content).width) 466 | } 467 | 468 | drawTunnel() { 469 | const ctx = this.ctx 470 | const tunnelColor = '#CCB24D' 471 | for (let i = 0; i <= this.tunnelNum; i++) { 472 | const y = this.padding[0] + i * this.tunnelHeight 473 | ctx.beginPath() 474 | ctx.strokeStyle = tunnelColor 475 | ctx.setLineDash([5, 10]) 476 | ctx.moveTo(0, y) 477 | ctx.lineTo(this.width, y) 478 | ctx.stroke() 479 | if (i < this.tunnelNum) { 480 | ctx.fillStyle = tunnelColor 481 | ctx.fillText(`弹道${i + 1}`, 10, this.tunnelHeight / 2 + y) 482 | } 483 | } 484 | } 485 | } 486 | 487 | export default Barrage 488 | -------------------------------------------------------------------------------- /src/barrage-dom.js: -------------------------------------------------------------------------------- 1 | const { 2 | substring, 3 | getRandom, 4 | getFontSize, 5 | isArray 6 | } = require('./utils') 7 | 8 | class Bullet { 9 | constructor(opt = {}) { 10 | this.bulletId = opt.bulletId 11 | this.addContent(opt) 12 | } 13 | 14 | /** 15 | * image 结构 16 | * { 17 | * head: {src, width, height}, 18 | * tail: {src, width, height}, 19 | * gap: 4 // 图片与文本间隔 20 | * } 21 | */ 22 | addContent(opt = {}) { 23 | const defaultBulletOpt = { 24 | duration: 0, // 动画时长 25 | passtime: 0, // 弹幕穿越右边界耗时 26 | content: '', // 文本 27 | color: '#000000', // 默认黑色 28 | width: 0, // 弹幕宽度 29 | height: 0, // 弹幕高度 30 | image: {}, // 图片 31 | paused: false // 是否暂停 32 | } 33 | Object.assign(this, defaultBulletOpt, opt) 34 | } 35 | 36 | removeContent() { 37 | this.addContent({}) 38 | } 39 | } 40 | 41 | // tunnel(轨道) 42 | class Tunnel { 43 | constructor(opt = {}) { 44 | const defaultTunnelOpt = { 45 | tunnelId: 0, 46 | height: 0, // 轨道高度 47 | width: 0, // 轨道宽度 48 | safeGap: 4, // 相邻弹幕安全间隔 49 | maxNum: 10, // 缓冲队列长度 50 | bullets: [], // 弹幕 51 | last: -1, // 上一条发送的弹幕序号 52 | bulletStatus: [], // 0 空闲,1 占用中 53 | disabled: false, // 禁用中 54 | sending: false, // 弹幕正在发送 55 | timer: null, // 定时器 56 | } 57 | Object.assign(this, defaultTunnelOpt, opt) 58 | this.bulletStatus = new Array(this.maxNum).fill(0) 59 | for (let i = 0; i < this.maxNum; i++) { 60 | this.bullets.push(new Bullet({ 61 | bulletId: i, 62 | })) 63 | } 64 | } 65 | 66 | disable() { 67 | this.disabled = true 68 | this.last = -1 69 | this.sending = false 70 | this.bulletStatus = new Array(this.maxNum).fill(1) 71 | this.bullets.forEach(bullet => bullet.removeContent()) 72 | } 73 | 74 | enable() { 75 | if (this.disabled) { 76 | this.bulletStatus = new Array(this.maxNum).fill(0) 77 | } 78 | this.disabled = false 79 | } 80 | 81 | clear() { 82 | this.last = -1 83 | this.sending = false 84 | this.bulletStatus = new Array(this.maxNum).fill(0) 85 | this.bullets.forEach(bullet => bullet.removeContent()) 86 | if (this.timer) { 87 | clearTimeout(this.timer) 88 | } 89 | } 90 | 91 | getIdleBulletIdx() { 92 | let idle = this.bulletStatus.indexOf(0, this.last + 1) 93 | if (idle === -1) { 94 | idle = this.bulletStatus.indexOf(0) 95 | } 96 | 97 | return idle 98 | } 99 | 100 | getIdleBulletNum() { 101 | let count = 0 102 | this.bulletStatus.forEach(status => { 103 | if (status === 0) count++ 104 | }) 105 | return count 106 | } 107 | 108 | addBullet(opt) { 109 | if (this.disabled) return 110 | const idx = this.getIdleBulletIdx() 111 | if (idx >= 0) { 112 | this.bulletStatus[idx] = 1 113 | this.bullets[idx].addContent(opt) 114 | } 115 | } 116 | 117 | removeBullet(bulletId) { 118 | if (this.disabled) return 119 | this.bulletStatus[bulletId] = 0 120 | const bullet = this.bullets[bulletId] 121 | bullet.removeContent() 122 | } 123 | } 124 | 125 | // Barrage(控制中心) 126 | class Barrage { 127 | constructor(opt = {}) { 128 | const defaultBarrageOpt = { 129 | duration: 10, // 弹幕动画时长 130 | lineHeight: 1.2, // 弹幕行高 131 | padding: [0, 0, 0, 0], // 弹幕区四周留白 132 | alpha: 1, // 全局透明度 133 | font: '10px sans-serif', // 全局字体 134 | mode: 'separate', // 弹幕重叠 overlap 不重叠 separate 135 | range: [0, 1], // 弹幕显示的垂直范围,支持两个值。[0,1]表示弹幕整个随机分布, 136 | tunnelShow: false, // 显示轨道线 137 | tunnelMaxNum: 30, // 隧道最大缓冲长度 138 | maxLength: 30, // 弹幕最大字节长度,汉字算双字节 139 | safeGap: 4, // 发送时的安全间隔 140 | enableTap: false, // 点击弹幕停止动画高亮显示 141 | tunnelHeight: 0, 142 | tunnelNum: 0, 143 | tunnels: [], 144 | idleTunnels: null, 145 | enableTunnels: null, 146 | distance: 2000, 147 | comp: null, // 组件实例 148 | } 149 | Object.assign(this, defaultBarrageOpt, opt) 150 | this._ready = false 151 | this._deferred = [] 152 | 153 | const query = this.comp.createSelectorQuery() 154 | query.select('.barrage-area').boundingClientRect((res) => { 155 | this.init(res) 156 | this.ready() 157 | }).exec() 158 | } 159 | 160 | ready() { 161 | this._ready = true 162 | this._deferred.forEach(item => { 163 | // eslint-disable-next-line prefer-spread 164 | this[item.callback].apply(this, item.args) 165 | }) 166 | 167 | this._deferred = [] 168 | } 169 | 170 | _delay(method, ...params) { 171 | this._deferred.push({ 172 | callback: method, 173 | args: params 174 | }) 175 | } 176 | 177 | init(opt) { 178 | this.width = opt.width 179 | this.height = opt.height 180 | this.fontSize = getFontSize(this.font) 181 | this.idleTunnels = new Set() 182 | this.enableTunnels = new Set() 183 | this.tunnels = [] 184 | this.availableHeight = (this.height - this.padding[0] - this.padding[2]) 185 | this.tunnelHeight = this.fontSize * this.lineHeight 186 | this.tunnelNum = Math.floor(this.availableHeight / this.tunnelHeight) 187 | for (let i = 0; i < this.tunnelNum; i++) { 188 | this.idleTunnels.add(i) // 空闲的隧道id集合 189 | this.enableTunnels.add(i) // 可用的隧道id集合 190 | 191 | this.tunnels.push(new Tunnel({ // 隧道集合 192 | width: this.width, 193 | height: this.tunnelHeight, 194 | safeGap: this.safeGap, 195 | maxNum: this.tunnelMaxNum, 196 | tunnelId: i, 197 | })) 198 | } 199 | this.comp.setData({ 200 | fontSize: this.fontSize, 201 | tunnelShow: this.tunnelShow, 202 | tunnels: this.tunnels, 203 | font: this.font, 204 | alpha: this.alpha, 205 | padding: this.padding.map(item => item + 'px').join(' ') 206 | }) 207 | // 筛选符合范围的隧道 208 | this.setRange() 209 | } 210 | 211 | // 设置显示范围 range: [0,1] 212 | setRange(range) { 213 | if (!this._ready) { 214 | this._delay('setRange', range) 215 | return 216 | } 217 | 218 | range = range || this.range 219 | const top = range[0] * this.tunnelNum 220 | const bottom = range[1] * this.tunnelNum 221 | // 释放符合要求的隧道 222 | // 找到目前空闲的隧道 223 | const idleTunnels = new Set() 224 | const enableTunnels = new Set() 225 | this.tunnels.forEach((tunnel, tunnelId) => { 226 | if (tunnelId >= top && tunnelId < bottom) { 227 | const disabled = tunnel.disabled 228 | tunnel.enable() 229 | enableTunnels.add(tunnelId) 230 | 231 | if (disabled || this.idleTunnels.has(tunnelId)) { 232 | idleTunnels.add(tunnelId) 233 | } 234 | } else { 235 | tunnel.disable() 236 | } 237 | }) 238 | this.idleTunnels = idleTunnels 239 | this.enableTunnels = enableTunnels 240 | this.range = range 241 | this.comp.setData({tunnels: this.tunnels}) 242 | } 243 | 244 | setFont(font) { 245 | if (!this._ready) { 246 | this._delay('setFont', font) 247 | return 248 | } 249 | 250 | if (typeof font !== 'string') return 251 | this.font = font 252 | this.comp.setData({font}) 253 | } 254 | 255 | setAlpha(alpha) { 256 | if (!this._ready) { 257 | this._delay('setAlpha', alpha) 258 | return 259 | } 260 | 261 | if (typeof alpha !== 'number') return 262 | this.alpha = alpha 263 | this.comp.setData({alpha}) 264 | } 265 | 266 | setDuration(duration) { 267 | if (!this._ready) { 268 | this._delay('setDuration', duration) 269 | return 270 | } 271 | 272 | if (typeof duration !== 'number') return 273 | this.duration = duration 274 | this.clear() 275 | } 276 | 277 | // 开启弹幕 278 | open() { 279 | if (!this._ready) { 280 | this._delay('open') 281 | return 282 | } 283 | 284 | this._isActive = true 285 | } 286 | 287 | // 关闭弹幕,清除所有数据 288 | close() { 289 | if (!this._ready) { 290 | this._delay('close') 291 | return 292 | } 293 | 294 | this._isActive = false 295 | this.clear() 296 | } 297 | 298 | clear() { 299 | this.tunnels.forEach(tunnel => tunnel.clear()) 300 | this.idleTunnels = new Set(this.enableTunnels) 301 | this.comp.setData({tunnels: this.tunnels}) 302 | } 303 | 304 | // 添加一批弹幕,轨道满时会被丢弃 305 | addData(data = []) { 306 | if (!isArray(data)) return 307 | 308 | if (!this._ready) { 309 | this._delay('addData', data) 310 | return 311 | } 312 | 313 | if (!this._isActive) return 314 | 315 | data.forEach(item => { 316 | item.content = substring(item.content, this.maxLength) 317 | this.addBullet2Tunnel(item) 318 | }) 319 | this.comp.setData({ 320 | tunnels: this.tunnels 321 | }, () => { 322 | this.updateBullets() 323 | }) 324 | } 325 | 326 | // 发送一条弹幕 327 | send(opt = {}) { 328 | if (!this._ready) { 329 | this._delay('send', opt) 330 | return 331 | } 332 | const tunnel = this.getEnableTunnel() 333 | if (tunnel === null) return 334 | 335 | const timer = setInterval(() => { 336 | const tunnel = this.getIdleTunnel() 337 | if (tunnel) { 338 | this.addData([opt]) 339 | clearInterval(timer) 340 | } 341 | }, 16) 342 | } 343 | 344 | // 添加至轨道 345 | addBullet2Tunnel(opt = {}) { 346 | const tunnel = this.getIdleTunnel() 347 | if (tunnel === null) return 348 | 349 | const tunnelId = tunnel.tunnelId 350 | tunnel.addBullet(opt) 351 | if (tunnel.getIdleBulletNum() === 0) this.removeIdleTunnel(tunnelId) 352 | } 353 | 354 | updateBullets() { 355 | const self = this 356 | const query = this.comp.createSelectorQuery() 357 | query.selectAll('.bullet-item').boundingClientRect((res) => { 358 | if (!this._isActive) return 359 | 360 | for (let i = 0; i < res.length; i++) { 361 | const {tunnelid, bulletid} = res[i].dataset 362 | const tunnel = self.tunnels[tunnelid] 363 | const bullet = tunnel.bullets[bulletid] 364 | bullet.width = res[i].width 365 | bullet.height = res[i].height 366 | } 367 | self.animate() 368 | }).exec() 369 | } 370 | 371 | animate() { 372 | this.tunnels.forEach(tunnel => { 373 | this.tunnelAnimate(tunnel) 374 | }) 375 | } 376 | 377 | tunnelAnimate(tunnel) { 378 | if (tunnel.disabled || tunnel.sending || !this._isActive) return 379 | 380 | const next = (tunnel.last + 1) % tunnel.maxNum 381 | const bullet = tunnel.bullets[next] 382 | 383 | if (!bullet) return 384 | 385 | if (bullet.content || bullet.image.head || bullet.image.tail) { 386 | tunnel.sending = true 387 | tunnel.last = next 388 | let duration = this.duration 389 | if (this.mode === 'overlap') { 390 | duration = this.distance * this.duration / (this.distance + bullet.width) 391 | } 392 | const passDistance = bullet.width + tunnel.safeGap 393 | bullet.duration = duration 394 | // 等上一条通过右边界 395 | bullet.passtime = Math.ceil(passDistance * bullet.duration * 1000 / this.distance) 396 | this.comp.setData({ 397 | [`tunnels[${tunnel.tunnelId}].bullets[${bullet.bulletId}]`]: bullet 398 | }, () => { 399 | tunnel.timer = setTimeout(() => { 400 | tunnel.sending = false 401 | this.tunnelAnimate(tunnel) 402 | }, bullet.passtime) 403 | }) 404 | } 405 | } 406 | 407 | showTunnel() { 408 | this.comp.setData({ 409 | tunnelShow: true 410 | }) 411 | } 412 | 413 | hideTunnel() { 414 | this.comp.setData({ 415 | tunnelShow: false 416 | }) 417 | } 418 | 419 | removeIdleTunnel(tunnelId) { 420 | this.idleTunnels.delete(tunnelId) 421 | } 422 | 423 | addIdleTunnel(tunnelId) { 424 | this.idleTunnels.add(tunnelId) 425 | } 426 | 427 | // 从可用的隧道中随机挑选一个 428 | getEnableTunnel() { 429 | if (this.enableTunnels.size === 0) return null 430 | const enableTunnels = Array.from(this.enableTunnels) 431 | const index = getRandom(enableTunnels.length) 432 | return this.tunnels[enableTunnels[index]] 433 | } 434 | 435 | // 从还有余量的隧道中随机挑选一个 436 | getIdleTunnel() { 437 | if (this.idleTunnels.size === 0) return null 438 | const idleTunnels = Array.from(this.idleTunnels) 439 | const index = getRandom(idleTunnels.length) 440 | return this.tunnels[idleTunnels[index]] 441 | } 442 | 443 | animationend(opt) { 444 | const {tunnelId, bulletId} = opt 445 | const tunnel = this.tunnels[tunnelId] 446 | if (!tunnel) return 447 | 448 | const bullet = tunnel.bullets[bulletId] 449 | if (!bullet) return 450 | 451 | tunnel.removeBullet(bulletId) 452 | this.addIdleTunnel(tunnelId) 453 | this.comp.setData({ 454 | [`tunnels[${tunnelId}].bullets[${bulletId}]`]: bullet 455 | }) 456 | } 457 | 458 | tapBullet(opt) { 459 | if (!this.enableTap) return 460 | 461 | const {tunnelId, bulletId} = opt 462 | const tunnel = this.tunnels[tunnelId] 463 | const bullet = tunnel.bullets[bulletId] 464 | bullet.paused = !bullet.paused 465 | this.comp.setData({ 466 | [`tunnels[${tunnelId}].bullets[${bulletId}]`]: bullet 467 | }) 468 | } 469 | } 470 | 471 | export default Barrage 472 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import DomBarrage from './barrage-dom' 2 | import CanvasBarrage from './barrage-canvas' 3 | 4 | Component({ 5 | options: { 6 | addGlobalClass: true, 7 | }, 8 | 9 | properties: { 10 | zIndex: { 11 | type: Number, 12 | value: 10 13 | }, 14 | 15 | renderingMode: { 16 | type: String, 17 | value: 'canvas' 18 | } 19 | }, 20 | 21 | methods: { 22 | getBarrageInstance(opt = {}) { 23 | opt.comp = this 24 | this.barrageInstance = this.data.renderingMode === 'dom' 25 | ? new DomBarrage(opt) 26 | : new CanvasBarrage(opt) 27 | return this.barrageInstance 28 | }, 29 | 30 | onAnimationend(e) { 31 | const {tunnelid, bulletid} = e.currentTarget.dataset 32 | this.barrageInstance.animationend({ 33 | tunnelId: tunnelid, 34 | bulletId: bulletid 35 | }) 36 | }, 37 | 38 | onTapBullet(e) { 39 | const {tunnelid, bulletid} = e.currentTarget.dataset 40 | this.barrageInstance.tapBullet({ 41 | tunnelId: tunnelid, 42 | bulletId: bulletid 43 | }) 44 | }, 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /src/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /src/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 25 | 26 | {{bullet.content}} 27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/index.wxss: -------------------------------------------------------------------------------- 1 | .barrage-area { 2 | position: relative; 3 | box-sizing: border-box; 4 | width: 100%; 5 | height: 100%; 6 | pointer-events: auto; 7 | } 8 | 9 | .barrage-tunnel { 10 | box-sizing: border-box; 11 | position: relative; 12 | display: flex; 13 | align-items: center; 14 | border-top: 1px dashed #CCB24D; 15 | width: 100%; 16 | } 17 | 18 | .tunnel-tips { 19 | display: inline-block; 20 | margin-left: 10px; 21 | color: #CCB24D; 22 | } 23 | 24 | .bullet-item { 25 | position: absolute; 26 | display: flex; 27 | align-items: center; 28 | top: 0; 29 | left: 100%; 30 | white-space: nowrap; 31 | } 32 | 33 | .bullet-item.paused { 34 | background: #000; 35 | opacity: 0.6; 36 | padding: 0 10px; 37 | z-index: 1001; 38 | } 39 | 40 | .bullet-item_img { 41 | max-height: 100%; 42 | display: inline-block; 43 | } 44 | 45 | .bullet-item_text { 46 | display: inline-block; 47 | margin: 0; 48 | } 49 | 50 | .bullet-move { 51 | animation: 0s linear slidein 52 | } 53 | 54 | @keyframes slidein { 55 | 0% { 56 | transform: translate3d(0, 0, 0) 57 | } 58 | 100% { 59 | transform: translate3d(-2000px, 0, 0) 60 | } 61 | } -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | // 获取字节长度,中文算2个字节 2 | function getStrLen(str) { 3 | // eslint-disable-next-line no-control-regex 4 | return str.replace(/[^\x00-\xff]/g, 'aa').length 5 | } 6 | 7 | // 截取指定字节长度的子串 8 | function substring(str, n) { 9 | if (!str) return '' 10 | 11 | const len = getStrLen(str) 12 | if (n >= len) return str 13 | 14 | let l = 0 15 | let result = '' 16 | for (let i = 0; i < str.length; i++) { 17 | const ch = str.charAt(i) 18 | // eslint-disable-next-line no-control-regex 19 | l = /[^\x00-\xff]/i.test(ch) ? l + 2 : l + 1 20 | result += ch 21 | if (l >= n) break 22 | } 23 | return result 24 | } 25 | 26 | function getRandom(max = 10, min = 0) { 27 | return Math.floor(Math.random() * (max - min) + min) 28 | } 29 | 30 | function getFontSize(font) { 31 | const reg = /(\d+)(px)/i 32 | const match = font.match(reg) 33 | return (match && match[1]) || 10 34 | } 35 | 36 | function compareVersion(v1, v2) { 37 | v1 = v1.split('.') 38 | v2 = v2.split('.') 39 | const len = Math.max(v1.length, v2.length) 40 | 41 | while (v1.length < len) { 42 | v1.push('0') 43 | } 44 | while (v2.length < len) { 45 | v2.push('0') 46 | } 47 | 48 | for (let i = 0; i < len; i++) { 49 | const num1 = parseInt(v1[i], 10) 50 | const num2 = parseInt(v2[i], 10) 51 | 52 | if (num1 > num2) { 53 | return 1 54 | } else if (num1 < num2) { 55 | return -1 56 | } 57 | } 58 | return 0 59 | } 60 | 61 | const getType = obj => Object.prototype.toString.call(obj).slice(8,-1) 62 | const isArray = obj => getType(obj) === 'Array' 63 | 64 | module.exports = { 65 | getStrLen, 66 | substring, 67 | getRandom, 68 | getFontSize, 69 | compareVersion, 70 | isArray 71 | } 72 | -------------------------------------------------------------------------------- /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(_.match(component.dom, 'index.test.properties-false')).toBe(true) 11 | 12 | await _.sleep(10) 13 | 14 | expect(_.match(component.dom, 'index.test.properties-true')).toBe(true) 15 | }) 16 | -------------------------------------------------------------------------------- /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 | if (typeof componentPath === 'string') 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 | const _ = require('./utils') 2 | 3 | test('wx.getSystemInfo', async () => { 4 | wx.getSystemInfo({ 5 | success(res) { 6 | expect(res.errMsg).toBe('getSystemInfo:ok') 7 | }, 8 | complete(res) { 9 | expect(res.errMsg).toBe('getSystemInfo:ok') 10 | }, 11 | }) 12 | }) 13 | 14 | test('wx.getSystemInfoSync', async () => { 15 | const info = wx.getSystemInfoSync() 16 | expect(info.SDKVersion).toBe('2.4.1') 17 | expect(info.version).toBe('6.6.3') 18 | }) 19 | -------------------------------------------------------------------------------- /tools/build.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | 4 | const gulp = require('gulp') 5 | const clean = require('gulp-clean') 6 | const less = require('gulp-less') 7 | const rename = require('gulp-rename') 8 | const gulpif = require('gulp-if') 9 | const sourcemaps = require('gulp-sourcemaps') 10 | const webpack = require('webpack') 11 | const gulpInstall = require('gulp-install') 12 | 13 | const config = require('./config') 14 | const checkComponents = require('./checkcomponents') 15 | const checkWxss = require('./checkwxss') 16 | const _ = require('./utils') 17 | 18 | const jsConfig = config.js || {} 19 | const wxssConfig = config.wxss || {} 20 | const srcPath = config.srcPath 21 | const distPath = config.distPath 22 | 23 | /** 24 | * 获取 wxss 流 25 | */ 26 | function wxss(wxssFileList) { 27 | if (!wxssFileList.length) return false 28 | 29 | return gulp.src(wxssFileList, {cwd: srcPath, base: srcPath}) 30 | .pipe(checkWxss.start()) // 开始处理 import 31 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.init())) 32 | .pipe(gulpif(wxssConfig.less, less({paths: [srcPath]}))) 33 | .pipe(checkWxss.end()) // 结束处理 import 34 | .pipe(rename({extname: '.wxss'})) 35 | .pipe(gulpif(wxssConfig.less && wxssConfig.sourcemap, sourcemaps.write('./'))) 36 | .pipe(_.logger(wxssConfig.less ? 'generate' : undefined)) 37 | .pipe(gulp.dest(distPath)) 38 | } 39 | 40 | /** 41 | * 获取 js 流 42 | */ 43 | function js(jsFileMap, scope) { 44 | const webpackConfig = config.webpack 45 | const webpackCallback = (err, stats) => { 46 | if (!err) { 47 | // eslint-disable-next-line no-console 48 | console.log(stats.toString({ 49 | assets: true, 50 | cached: false, 51 | colors: true, 52 | children: false, 53 | errors: true, 54 | warnings: true, 55 | version: true, 56 | modules: false, 57 | publicPath: true, 58 | })) 59 | } else { 60 | // eslint-disable-next-line no-console 61 | console.log(err) 62 | } 63 | } 64 | 65 | webpackConfig.entry = jsFileMap 66 | webpackConfig.output.path = distPath 67 | 68 | if (scope.webpackWatcher) { 69 | scope.webpackWatcher.close() 70 | scope.webpackWatcher = null 71 | } 72 | 73 | if (config.isWatch) { 74 | scope.webpackWatcher = webpack(webpackConfig).watch({ 75 | ignored: /node_modules/, 76 | }, webpackCallback) 77 | } else { 78 | webpack(webpackConfig).run(webpackCallback) 79 | } 80 | } 81 | 82 | /** 83 | * 拷贝文件 84 | */ 85 | function copy(copyFileList) { 86 | console.log('copyFileList', copyFileList) 87 | if (!copyFileList.length) return false 88 | 89 | return gulp.src(copyFileList, {cwd: srcPath, base: srcPath}) 90 | .pipe(_.logger()) 91 | .pipe(gulp.dest(distPath)) 92 | } 93 | 94 | /** 95 | * 安装依赖包 96 | */ 97 | function install() { 98 | return gulp.series(async () => { 99 | const demoDist = config.demoDist 100 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 101 | const packageJson = _.readJson(path.resolve(__dirname, '../package.json')) 102 | const dependencies = packageJson.dependencies || {} 103 | 104 | await _.writeFile(demoPackageJsonPath, JSON.stringify({dependencies}, null, '\t')) // write dev demo's package.json 105 | }, () => { 106 | const demoDist = config.demoDist 107 | const demoPackageJsonPath = path.join(demoDist, 'package.json') 108 | 109 | return gulp.src(demoPackageJsonPath) 110 | .pipe(gulpInstall({production: true})) 111 | }) 112 | } 113 | 114 | class BuildTask { 115 | constructor(id, entry) { 116 | if (!entry) return 117 | 118 | this.id = id 119 | this.entries = Array.isArray(config.entry) ? config.entry : [config.entry] 120 | this.copyList = Array.isArray(config.copy) ? config.copy : [] 121 | this.componentListMap = {} 122 | this.cachedComponentListMap = {} 123 | 124 | this.init() 125 | } 126 | 127 | init() { 128 | const id = this.id 129 | 130 | /** 131 | * 清空目标目录 132 | */ 133 | gulp.task(`${id}-clean-dist`, () => gulp.src(distPath, {read: false, allowEmpty: true}).pipe(clean())) 134 | 135 | /** 136 | * 拷贝 demo 到目标目录 137 | */ 138 | let isDemoExists = false 139 | gulp.task(`${id}-demo`, gulp.series(async () => { 140 | const demoDist = config.demoDist 141 | 142 | isDemoExists = await _.checkFileExists(path.join(demoDist, 'project.config.json')) 143 | }, done => { 144 | if (!isDemoExists) { 145 | const demoSrc = config.demoSrc 146 | const demoDist = config.demoDist 147 | 148 | return gulp.src('**/*', {cwd: demoSrc, base: demoSrc}) 149 | .pipe(gulp.dest(demoDist)) 150 | } 151 | 152 | return done() 153 | })) 154 | 155 | /** 156 | * 安装依赖包 157 | */ 158 | gulp.task(`${id}-install`, install()) 159 | 160 | /** 161 | * 检查自定义组件 162 | */ 163 | gulp.task(`${id}-component-check`, async () => { 164 | const entries = this.entries 165 | const mergeComponentListMap = {} 166 | for (let i = 0, len = entries.length; i < len; i++) { 167 | let entry = entries[i] 168 | entry = path.join(srcPath, `${entry}.json`) 169 | const newComponentListMap = await checkComponents(entry) 170 | 171 | _.merge(mergeComponentListMap, newComponentListMap) 172 | } 173 | 174 | this.cachedComponentListMap = this.componentListMap 175 | this.componentListMap = mergeComponentListMap 176 | }) 177 | 178 | /** 179 | * 写 json 文件到目标目录 180 | */ 181 | gulp.task(`${id}-component-json`, done => { 182 | const jsonFileList = this.componentListMap.jsonFileList 183 | 184 | if (jsonFileList && jsonFileList.length) { 185 | return copy(this.componentListMap.jsonFileList) 186 | } 187 | 188 | return done() 189 | }) 190 | 191 | /** 192 | * 拷贝 wxml 文件到目标目录 193 | */ 194 | gulp.task(`${id}-component-wxml`, done => { 195 | const wxmlFileList = this.componentListMap.wxmlFileList 196 | 197 | if (wxmlFileList && 198 | wxmlFileList.length && 199 | !_.compareArray(this.cachedComponentListMap.wxmlFileList, wxmlFileList)) { 200 | return copy(wxmlFileList) 201 | } 202 | 203 | return done() 204 | }) 205 | 206 | /** 207 | * 生成 wxss 文件到目标目录 208 | */ 209 | gulp.task(`${id}-component-wxss`, done => { 210 | const wxssFileList = this.componentListMap.wxssFileList 211 | 212 | if (wxssFileList && 213 | wxssFileList.length && 214 | !_.compareArray(this.cachedComponentListMap.wxssFileList, wxssFileList)) { 215 | return wxss(wxssFileList, srcPath, distPath) 216 | } 217 | 218 | return done() 219 | }) 220 | 221 | /** 222 | * 生成 js 文件到目标目录 223 | */ 224 | gulp.task(`${id}-component-js`, done => { 225 | const jsFileList = this.componentListMap.jsFileList 226 | 227 | if (jsFileList && 228 | jsFileList.length && 229 | !_.compareArray(this.cachedComponentListMap.jsFileList, jsFileList)) { 230 | if (jsConfig.webpack) { 231 | js(this.componentListMap.jsFileMap, this) 232 | } else { 233 | return copy(jsFileList) 234 | } 235 | } 236 | 237 | return done() 238 | }) 239 | 240 | /** 241 | * 拷贝相关资源到目标目录 242 | */ 243 | gulp.task(`${id}-copy`, gulp.parallel(done => { 244 | const copyList = this.copyList 245 | const copyFileList = copyList.map(copyFilePath => { 246 | try { 247 | if (fs.statSync(path.join(srcPath, copyFilePath)).isDirectory()) { 248 | return path.join(copyFilePath, '**/*.!(wxss)') 249 | } else { 250 | return copyFilePath 251 | } 252 | } catch (err) { 253 | // eslint-disable-next-line no-console 254 | console.error(err) 255 | return null 256 | } 257 | }).filter(copyFilePath => !!copyFilePath) 258 | 259 | if (copyFileList.length) return copy(copyFileList) 260 | 261 | return done() 262 | }, done => { 263 | const copyList = this.copyList 264 | const copyFileList = copyList.map(copyFilePath => { 265 | try { 266 | if (fs.statSync(path.join(srcPath, copyFilePath)).isDirectory()) { 267 | return path.join(copyFilePath, '**/*.wxss') 268 | } else if (copyFilePath.slice(-5) === '.wxss') { 269 | return copyFilePath 270 | } else { 271 | return null 272 | } 273 | } catch (err) { 274 | // eslint-disable-next-line no-console 275 | console.error(err) 276 | return null 277 | } 278 | }).filter(copyFilePath => !!copyFilePath) 279 | 280 | if (copyFileList.length) return wxss(copyFileList, srcPath, distPath) 281 | 282 | return done() 283 | })) 284 | 285 | /** 286 | * 监听 js 变化 287 | */ 288 | gulp.task(`${id}-watch-js`, done => { 289 | if (!jsConfig.webpack) { 290 | return gulp.watch(this.componentListMap.jsFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-js`)) 291 | } 292 | 293 | return done() 294 | }) 295 | 296 | /** 297 | * 监听 json 变化 298 | */ 299 | 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`)))) 300 | 301 | /** 302 | * 监听 wxml 变化 303 | */ 304 | gulp.task(`${id}-watch-wxml`, () => { 305 | this.cachedComponentListMap.wxmlFileList = null 306 | return gulp.watch(this.componentListMap.wxmlFileList, {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxml`)) 307 | }) 308 | 309 | /** 310 | * 监听 wxss 变化 311 | */ 312 | gulp.task(`${id}-watch-wxss`, () => { 313 | this.cachedComponentListMap.wxssFileList = null 314 | return gulp.watch('**/*.wxss', {cwd: srcPath, base: srcPath}, gulp.series(`${id}-component-wxss`)) 315 | }) 316 | 317 | /** 318 | * 监听相关资源变化 319 | */ 320 | gulp.task(`${id}-watch-copy`, () => { 321 | const copyList = this.copyList 322 | const copyFileList = copyList.map(copyFilePath => { 323 | try { 324 | if (fs.statSync(path.join(srcPath, copyFilePath)).isDirectory()) { 325 | return path.join(copyFilePath, '**/*') 326 | } else { 327 | return copyFilePath 328 | } 329 | } catch (err) { 330 | // eslint-disable-next-line no-console 331 | console.error(err) 332 | return null 333 | } 334 | }).filter(copyFilePath => !!copyFilePath) 335 | const watchCallback = filePath => copy([filePath]) 336 | 337 | return gulp.watch(copyFileList, {cwd: srcPath, base: srcPath}) 338 | .on('change', watchCallback) 339 | .on('add', watchCallback) 340 | .on('unlink', watchCallback) 341 | }) 342 | 343 | /** 344 | * 监听 demo 变化 345 | */ 346 | gulp.task(`${id}-watch-demo`, () => { 347 | const demoSrc = config.demoSrc 348 | const demoDist = config.demoDist 349 | const watchCallback = filePath => gulp.src(filePath, {cwd: demoSrc, base: demoSrc}) 350 | .pipe(gulp.dest(demoDist)) 351 | 352 | return gulp.watch('**/*', {cwd: demoSrc, base: demoSrc}) 353 | .on('change', watchCallback) 354 | .on('add', watchCallback) 355 | .on('unlink', watchCallback) 356 | }) 357 | 358 | /** 359 | * 监听安装包列表变化 360 | */ 361 | gulp.task(`${id}-watch-install`, () => gulp.watch(path.resolve(__dirname, '../package.json'), install())) 362 | 363 | /** 364 | * 构建相关任务 365 | */ 366 | 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`))) 367 | 368 | gulp.task(`${id}-watch`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`, gulp.parallel(`${id}-watch-wxml`, `${id}-watch-wxss`, `${id}-watch-js`, `${id}-watch-json`, `${id}-watch-copy`, `${id}-watch-install`, `${id}-watch-demo`))) 369 | 370 | gulp.task(`${id}-dev`, gulp.series(`${id}-build`, `${id}-demo`, `${id}-install`)) 371 | 372 | gulp.task(`${id}-default`, gulp.series(`${id}-build`)) 373 | } 374 | } 375 | 376 | module.exports = BuildTask 377 | -------------------------------------------------------------------------------- /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 | const hasCheckMap = {} 27 | async function checkIncludedComponents(jsonPath, componentListMap) { 28 | const json = _.readJson(jsonPath) 29 | if (!json) throw new Error(`json is not valid: "${jsonPath}"`) 30 | 31 | const {dirPath, fileName, fileBase} = getJsonPathInfo(jsonPath) 32 | if (hasCheckMap[fileBase]) return 33 | hasCheckMap[fileBase] = true 34 | 35 | for (let i = 0, len = checkProps.length; i < len; i++) { 36 | const checkProp = checkProps[i] 37 | const checkPropValue = json[checkProp] || {} 38 | const keys = Object.keys(checkPropValue) 39 | 40 | for (let j = 0, jlen = keys.length; j < jlen; j++) { 41 | const key = keys[j] 42 | let value = typeof checkPropValue[key] === 'object' ? checkPropValue[key].default : checkPropValue[key] 43 | if (!value) continue 44 | 45 | value = _.transformPath(value, path.sep) 46 | 47 | // 检查相对路径 48 | const componentPath = `${path.join(dirPath, value)}.json` 49 | const isExists = await _.checkFileExists(componentPath) 50 | if (isExists) { 51 | await checkIncludedComponents(componentPath, componentListMap) 52 | } 53 | } 54 | } 55 | 56 | // 进入存储 57 | componentListMap.wxmlFileList.push(`${fileBase}.wxml`) 58 | componentListMap.wxssFileList.push(`${fileBase}.wxss`) 59 | componentListMap.jsonFileList.push(`${fileBase}.json`) 60 | componentListMap.jsFileList.push(`${fileBase}.js`) 61 | 62 | componentListMap.jsFileMap[fileBase] = `${path.join(dirPath, fileName)}.js` 63 | } 64 | 65 | module.exports = async function (entry) { 66 | const componentListMap = { 67 | wxmlFileList: [], 68 | wxssFileList: [], 69 | jsonFileList: [], 70 | jsFileList: [], 71 | 72 | jsFileMap: {}, // 为 webpack entry 所用 73 | } 74 | 75 | const isExists = await _.checkFileExists(entry) 76 | if (!isExists) { 77 | const {dirPath, fileName, fileBase} = getJsonPathInfo(entry) 78 | 79 | componentListMap.jsFileList.push(`${fileBase}.js`) 80 | componentListMap.jsFileMap[fileBase] = `${path.join(dirPath, fileName)}.js` 81 | 82 | return componentListMap 83 | } 84 | 85 | await checkIncludedComponents(entry, componentListMap) 86 | 87 | return componentListMap 88 | } 89 | -------------------------------------------------------------------------------- /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 | // eslint-disable-next-line promise/no-callback-in-promise 71 | cb() 72 | }).catch(err => { 73 | // eslint-disable-next-line no-console 74 | console.warn(`deal with ${file.path} failed: ${err.stack}`) 75 | this.push(file) 76 | // eslint-disable-next-line promise/no-callback-in-promise 77 | cb() 78 | }) 79 | } else { 80 | this.push(file) 81 | cb() 82 | } 83 | }) 84 | }, 85 | 86 | end() { 87 | return through.obj(function (file, enc, cb) { 88 | if (file.isBuffer) { 89 | const reg = /\/\*\s\*updated for miniprogram-custom-component\*\s(@import\s+(?:(?:"([^"]+)")|(?:'([^"]+)'));)\s\*\//ig 90 | const wxss = file.contents.toString('utf8').replace(reg, (all, $1) => $1) 91 | 92 | file.contents = Buffer.from(wxss, 'utf8') 93 | this.push(file) 94 | cb() 95 | } else { 96 | this.push(file) 97 | cb() 98 | } 99 | }) 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /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: false, // 使用 less 来编写 wxss 27 | sourcemap: false, // 生成 less sourcemap 28 | }, 29 | 30 | js: { 31 | webpack: true, // 使用 webpack 来构建 js 32 | }, 33 | 34 | webpack: { 35 | mode: 'production', 36 | output: { 37 | filename: '[name].js', 38 | libraryTarget: 'commonjs2', 39 | }, 40 | target: 'node', 41 | externals: [nodeExternals()], // 忽略 node_modules 42 | module: { 43 | rules: [{ 44 | test: /\.js$/i, 45 | use: [ 46 | 'babel-loader', 47 | // 'eslint-loader' 48 | ], 49 | exclude: /node_modules/ 50 | }], 51 | }, 52 | resolve: { 53 | modules: [src, 'node_modules'], 54 | extensions: ['.js', '.json'], 55 | }, 56 | plugins: [ 57 | new webpack.DefinePlugin({}), 58 | new webpack.optimize.LimitChunkCountPlugin({maxChunks: 1}), 59 | ], 60 | optimization: { 61 | minimize: false, 62 | }, 63 | // devtool: 'nosources-source-map', // 生成 js sourcemap 64 | performance: { 65 | hints: 'warning', 66 | assetFilter: assetFilename => assetFilename.endsWith('.js') 67 | } 68 | }, 69 | 70 | copy: ['./utils.js'], // 将会复制到目标目录 71 | } 72 | -------------------------------------------------------------------------------- /tools/demo/app.js: -------------------------------------------------------------------------------- 1 | App({}) 2 | -------------------------------------------------------------------------------- /tools/demo/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages":[ 3 | "pages/index/index" 4 | ], 5 | "window":{ 6 | "backgroundTextStyle":"light", 7 | "navigationBarBackgroundColor": "#fff", 8 | "navigationBarTitleText": "WeChat", 9 | "navigationBarTextStyle":"black" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tools/demo/app.wxss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-barrage/11920108c89f0085a276b9c9a3e2807e7aca4edb/tools/demo/app.wxss -------------------------------------------------------------------------------- /tools/demo/assets/barrage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-barrage/11920108c89f0085a276b9c9a3e2807e7aca4edb/tools/demo/assets/barrage.png -------------------------------------------------------------------------------- /tools/demo/assets/bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-barrage/11920108c89f0085a276b9c9a3e2807e7aca4edb/tools/demo/assets/bookmark.png -------------------------------------------------------------------------------- /tools/demo/assets/car.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-barrage/11920108c89f0085a276b9c9a3e2807e7aca4edb/tools/demo/assets/car.png -------------------------------------------------------------------------------- /tools/demo/assets/iconfont.wxss: -------------------------------------------------------------------------------- 1 | @font-face {font-family: "iconfont"; 2 | src: url('//at.alicdn.com/t/font_945958_gjwpj5w64n.eot?t=1566030008649'); /* IE9 */ 3 | src: url('//at.alicdn.com/t/font_945958_gjwpj5w64n.eot?t=1566030008649#iefix') format('embedded-opentype'), /* IE6-IE8 */ 4 | url('data:application/x-font-woff2;charset=utf-8;base64,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') format('woff2'), 5 | url('//at.alicdn.com/t/font_945958_gjwpj5w64n.woff?t=1566030008649') format('woff'), 6 | url('//at.alicdn.com/t/font_945958_gjwpj5w64n.ttf?t=1566030008649') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */ 7 | url('//at.alicdn.com/t/font_945958_gjwpj5w64n.svg?t=1566030008649#iconfont') format('svg'); /* iOS 4.1- */ 8 | } 9 | 10 | .iconfont { 11 | font-family: "iconfont" !important; 12 | font-size: 16px; 13 | font-style: normal; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | 18 | .icon-redo:before { 19 | content: "\e627"; 20 | } 21 | 22 | .icon-undo:before { 23 | content: "\e633"; 24 | } 25 | 26 | .icon-indent:before { 27 | content: "\eb28"; 28 | } 29 | 30 | .icon-outdent:before { 31 | content: "\e6e8"; 32 | } 33 | 34 | .icon-fontsize:before { 35 | content: "\e6fd"; 36 | } 37 | 38 | .icon-format-header-1:before { 39 | content: "\e860"; 40 | } 41 | 42 | .icon-format-header-4:before { 43 | content: "\e863"; 44 | } 45 | 46 | .icon-format-header-5:before { 47 | content: "\e864"; 48 | } 49 | 50 | .icon-format-header-6:before { 51 | content: "\e865"; 52 | } 53 | 54 | .icon-clearup:before { 55 | content: "\e64d"; 56 | } 57 | 58 | .icon-preview:before { 59 | content: "\e631"; 60 | } 61 | 62 | .icon-date:before { 63 | content: "\e63e"; 64 | } 65 | 66 | .icon-fontbgcolor:before { 67 | content: "\e678"; 68 | } 69 | 70 | .icon-clearedformat:before { 71 | content: "\e67e"; 72 | } 73 | 74 | .icon-font:before { 75 | content: "\e684"; 76 | } 77 | 78 | .icon-723bianjiqi_duanhouju:before { 79 | content: "\e65f"; 80 | } 81 | 82 | .icon-722bianjiqi_duanqianju:before { 83 | content: "\e660"; 84 | } 85 | 86 | .icon-text_color:before { 87 | content: "\e72c"; 88 | } 89 | 90 | .icon-format-header-2:before { 91 | content: "\e75c"; 92 | } 93 | 94 | .icon-format-header-3:before { 95 | content: "\e75d"; 96 | } 97 | 98 | .icon-bofangqi-danmuguan:before { 99 | content: "\e696"; 100 | } 101 | 102 | .icon-bofangqi-danmukai:before { 103 | content: "\e697"; 104 | } 105 | 106 | .icon-bofangqi-danmudingbukai:before { 107 | content: "\e69b"; 108 | } 109 | 110 | .icon--checklist:before { 111 | content: "\e664"; 112 | } 113 | 114 | .icon-baocun:before { 115 | content: "\ec09"; 116 | } 117 | 118 | .icon-line-height:before { 119 | content: "\e7f8"; 120 | } 121 | 122 | .icon-quanping:before { 123 | content: "\ec13"; 124 | } 125 | 126 | .icon-direction-rtl:before { 127 | content: "\e66e"; 128 | } 129 | 130 | .icon-direction-ltr:before { 131 | content: "\e66d"; 132 | } 133 | 134 | .icon-selectall:before { 135 | content: "\e62b"; 136 | } 137 | 138 | .icon-fuzhi:before { 139 | content: "\ec7a"; 140 | } 141 | 142 | .icon-shanchu:before { 143 | content: "\ec7b"; 144 | } 145 | 146 | .icon-bianjisekuai:before { 147 | content: "\ec7c"; 148 | } 149 | 150 | .icon-fengexian:before { 151 | content: "\ec7f"; 152 | } 153 | 154 | .icon-dianzan:before { 155 | content: "\ec80"; 156 | } 157 | 158 | .icon-charulianjie:before { 159 | content: "\ec81"; 160 | } 161 | 162 | .icon-charutupian:before { 163 | content: "\ec82"; 164 | } 165 | 166 | .icon-wuxupailie:before { 167 | content: "\ec83"; 168 | } 169 | 170 | .icon-juzhongduiqi:before { 171 | content: "\ec84"; 172 | } 173 | 174 | .icon-yinyong:before { 175 | content: "\ec85"; 176 | } 177 | 178 | .icon-youxupailie:before { 179 | content: "\ec86"; 180 | } 181 | 182 | .icon-youduiqi:before { 183 | content: "\ec87"; 184 | } 185 | 186 | .icon-zitidaima:before { 187 | content: "\ec88"; 188 | } 189 | 190 | .icon-xiaolian:before { 191 | content: "\ec89"; 192 | } 193 | 194 | .icon-zitijiacu:before { 195 | content: "\ec8a"; 196 | } 197 | 198 | .icon-zitishanchuxian:before { 199 | content: "\ec8b"; 200 | } 201 | 202 | .icon-zitishangbiao:before { 203 | content: "\ec8c"; 204 | } 205 | 206 | .icon-zitibiaoti:before { 207 | content: "\ec8d"; 208 | } 209 | 210 | .icon-zitixiahuaxian:before { 211 | content: "\ec8e"; 212 | } 213 | 214 | .icon-zitixieti:before { 215 | content: "\ec8f"; 216 | } 217 | 218 | .icon-zitiyanse:before { 219 | content: "\ec90"; 220 | } 221 | 222 | .icon-zuoduiqi:before { 223 | content: "\ec91"; 224 | } 225 | 226 | .icon-zitiyulan:before { 227 | content: "\ec92"; 228 | } 229 | 230 | .icon-zitixiabiao:before { 231 | content: "\ec93"; 232 | } 233 | 234 | .icon-zuoyouduiqi:before { 235 | content: "\ec94"; 236 | } 237 | 238 | .icon-duigoux:before { 239 | content: "\ec9e"; 240 | } 241 | 242 | .icon-guanbi:before { 243 | content: "\eca0"; 244 | } 245 | 246 | .icon-shengyin_shiti:before { 247 | content: "\eca5"; 248 | } 249 | 250 | .icon-Character-Spacing:before { 251 | content: "\e964"; 252 | } 253 | -------------------------------------------------------------------------------- /tools/demo/package.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /tools/demo/pages/index/index.js: -------------------------------------------------------------------------------- 1 | const {mockData} = require('./util') 2 | 3 | Page({ 4 | data: { 5 | src: 'http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400', 6 | open: false, 7 | alpha: 1, 8 | fontSize: 16, 9 | duration: 15, 10 | showSetting: false, 11 | }, 12 | 13 | onUnload() { 14 | clearInterval(this.timer) 15 | }, 16 | 17 | onReady() { 18 | this.addBarrage() 19 | }, 20 | 21 | addBarrage() { 22 | const barrageComp = this.selectComponent('.barrage') 23 | this.barrage = barrageComp.getBarrageInstance({ 24 | font: 'bold 16px sans-serif', 25 | duration: this.data.duration, 26 | lineHeight: 2, 27 | mode: 'separate', 28 | padding: [10, 0, 10, 0], 29 | range: [0, 1] 30 | }) 31 | }, 32 | 33 | addData() { 34 | // 发送带图片的弹幕 35 | // const data = [ 36 | // { 37 | // content: '6666666666', 38 | // color: '#00ff00', 39 | // image: { 40 | // head: { 41 | // src: '/assets/bookmark.png', 42 | // }, 43 | // tail: { 44 | // src: '/assets/bookmark.png', 45 | // } 46 | // } 47 | // }, 48 | // ] 49 | // this.barrage.addData(data) 50 | 51 | const data = mockData(100) 52 | this.barrage.addData(data) 53 | this.timer = setInterval(() => { 54 | const data = mockData(100) 55 | this.barrage.addData(data) 56 | }, 2000) 57 | }, 58 | 59 | openDanmu() { 60 | this.barrage.open() 61 | this.addData() 62 | }, 63 | 64 | closeDanmu() { 65 | if (this.timer) { 66 | clearInterval(this.timer) 67 | } 68 | this.barrage.close() 69 | }, 70 | 71 | toggleDanmu() { 72 | const open = this.data.open 73 | if (open) { 74 | this.closeDanmu() 75 | } else { 76 | this.openDanmu() 77 | } 78 | this.setData({ 79 | open: !open 80 | }) 81 | }, 82 | 83 | // fullscreenchange() { 84 | // this.setData({ 85 | // toggle: false 86 | // }) 87 | // setTimeout(() => { 88 | // if (this.barrage) this.barrage.close() 89 | // this.setData({ 90 | // toggle: true 91 | // }) 92 | // this.addBarrage() 93 | // }, 1000) 94 | // }, 95 | 96 | disableDanmu() { 97 | this.barrage.setRange([0, 0]) 98 | }, 99 | 100 | showTopDanmu() { 101 | this.barrage.setRange([0, 0.3]) 102 | }, 103 | 104 | showAllDanmu() { 105 | this.barrage.setRange([0, 1]) 106 | }, 107 | 108 | toggleBarrageSetting() { 109 | this.setData({ 110 | showSetting: !this.data.showSetting 111 | }) 112 | }, 113 | 114 | fontChange(e) { 115 | const fontSize = e.detail.value 116 | this.setData({ 117 | fontSize 118 | }) 119 | this.barrage.setFont(`${fontSize}px sans-serif`) 120 | }, 121 | 122 | transparentChange(e) { 123 | const alpha = (e.detail.value / 100).toFixed(2) 124 | this.setData({ 125 | alpha 126 | }) 127 | this.barrage.setAlpha(Number(alpha)) 128 | }, 129 | 130 | durationChange(e) { 131 | const duration = e.detail.value 132 | this.setData({ 133 | duration 134 | }) 135 | this.barrage.setDuration(duration) 136 | }, 137 | 138 | send(e) { 139 | const value = e.detail.value 140 | this.barrage.send({ 141 | content: value, 142 | color: '#ff0000', 143 | image: { 144 | head: { 145 | src: '/assets/car.png', 146 | gap: 10, 147 | }, 148 | tail: { 149 | src: '/assets/car.png', 150 | gap: 10 151 | }, 152 | } 153 | }) 154 | }, 155 | }) 156 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "barrage": "../../components/index" 4 | } 5 | } -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /tools/demo/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | @import "../../assets/iconfont.wxss" 2 | 3 | .iconfont { 4 | display: inline-block; 5 | cursor: pointer; 6 | font-size: 28px; 7 | margin-right: 8px; 8 | } 9 | 10 | .video { 11 | width: 100%; 12 | height: 240px; 13 | } 14 | 15 | .barrage { 16 | width: 100%; 17 | height: 60%; 18 | } 19 | 20 | .switch-container { 21 | position: absolute; 22 | right: 20px; 23 | bottom: 40px; 24 | } 25 | 26 | .barrage-switch { 27 | width: 20px; 28 | height: 20px; 29 | } 30 | 31 | .container { 32 | position: absolute; 33 | width: 100%; 34 | height: 100%; 35 | left: 0; 36 | top: 0; 37 | box-sizing: border-box; 38 | pointer-events: none; 39 | z-index: 101; 40 | } 41 | 42 | .block { 43 | display: inline-block; 44 | width: 100%; 45 | margin: 10px 0; 46 | } 47 | 48 | .send-box { 49 | margin-top: 10px; 50 | padding: 0 5px; 51 | box-sizing: border-box; 52 | display: inline-block; 53 | width: 100%; 54 | height: 60px; 55 | border: 1px solid #ccc; 56 | } 57 | 58 | .setting { 59 | box-sizing: border-box; 60 | width: 200px; 61 | background: rgba(0, 0, 0, 0.4); 62 | color:#fff !important; 63 | font-size: 13px; 64 | height: 100%; 65 | position: absolute; 66 | right: 0; 67 | top: 0; 68 | display: flex; 69 | flex-direction: column; 70 | padding: 20px 4px; 71 | } 72 | 73 | .setting-item { 74 | display: flex; 75 | align-items: center; 76 | justify-content: start; 77 | margin: 10px 0; 78 | } 79 | 80 | .setting-item .item-left { 81 | width: 80px; 82 | } 83 | 84 | .slider { 85 | width: 100px; 86 | padding: 0; 87 | margin: 0; 88 | } -------------------------------------------------------------------------------- /tools/demo/pages/index/util.js: -------------------------------------------------------------------------------- 1 | const msgs = [ 2 | '666666', 3 | '我要上电视!!', 4 | '老板晚上好', 5 | '前方高能预警', 6 | '主播迟到了~~~', 7 | '干的漂亮', 8 | '早', 9 | '广东人民发来贺电', 10 | '不爱看的走开,别说话wen我' 11 | ] 12 | 13 | const color = ['red', 'rgb(0, 255, 0)', '#0000FF', '#fff'] 14 | 15 | const getRandom = (max = 10, min = 0) => Math.floor(Math.random() * (max - min) + min) 16 | const mockData = (num) => { 17 | const data = [] 18 | for (let i = 0; i < num; i++) { 19 | const msgId = getRandom(msgs.length) 20 | const colorId = getRandom(color.length) 21 | data.push({ 22 | content: msgs[msgId], 23 | color: color[colorId] 24 | }) 25 | } 26 | return data 27 | } 28 | 29 | module.exports = { 30 | mockData 31 | } 32 | -------------------------------------------------------------------------------- /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.10.3", 16 | "appid": "wxfabc8fbd43db56f6", 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 | --------------------------------------------------------------------------------