├── .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://www.npmjs.com/package/miniprogram-barrage)
4 | [](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 | 轨道{{tunnelId}}
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 |
--------------------------------------------------------------------------------