大家好,我是前端小张同学,最近在忙着公司的业务开发,都没有怎么更新自己的学习记录了,借着今天这个机会,就给大家分享一下,我是如何把上一次的弹幕设计,在业余时间写成了一个简单的
mini库,对比上次思路设计,这次是进阶版。
弹幕功能: 开启、关闭、重置、暂停、发布弹幕,icon图标、链接跳转、自定义用户自己发布的弹幕样式、修改颜色,批量删除等。
待添加功能 :点赞,自定义多类型弹幕,其他的还在等考虑,有好的建议可以在评论区留言。
1 : 前言
1.1 : 为什么会做这件事 ?
先跟大家说说为什么会做这件事,其实能开始动手写弹幕库这件事情是来自于我上班时的一个灵感,因为我们公司有一个需求,就是给基金走势图
上增加弹幕,但正好这个东西就是我负责开发的,所以我在公司完成了一版,但我发现不是很好,没有设计的那么完美,代码没有层次性等等,所以我决定再写一版,于是在业余时间开始设计弹幕。
1.2 想法
说说我的想法吧,对于弹幕写成一个库这件事情其实我是很避讳的,不敢挑战,为什么呢,因为弹幕这个功能太常见了,必定是会有集成好了的比我好上千倍的库,甚至别人的功能比我多得多 , 但是为什么我还要去写,去做这件事情呢? 我认为你去做一件事情没有对和错,对你自己来水只有成功和失败,过程中的收获全都是你自己学到的。
1.2 :学习收获
- 需求设计
- mini库的中文文档开发
- 如何实现中英文切换以及暗色主题切换
- 如何部署github静态网站(分支部署)
- vuePress 搭建文档网站学习
- 如何用vueCli打包一个组件package
- vueCli优化包体积。
- npm发包,更新包,版本查看等等。
2:我是怎么做的?
2.1思维导图
2.1.1 说说的弹幕设计吧,这里就不用文字描述了,我给大家提供一个思维导图,里面有我的设计层次。
2.2 开发弹幕
其实弹幕 主要是用面向对象的设计理念来进行开发的,其目的就是为了让后面开发更方便,每一个功能只需要调用一个方法,方法里去实现具体的功能。
js
import { BarrageType } from "../constant";
import { Barrage } from "./barrage";
// 弹幕管理 实体类
export class BarrageManager {
constructor(barrageVue) {
this.BarrageVueInstance = barrageVue;
this.pauseFlag = false;
this.barrageList = []; // 弹幕数据
this.cacheSourceBarrageList = []; // 缓存弹幕原始数据
this.bacthDeleteIds = []; // 所有需要批量删除的对象id
this.barrageTimer = null; // 定时器
this.maxDeleteCounter = 0; // 最大删除次数
this.lastDeleteCounter = 0; // 最后删除次数
this.currentRow = 0; //当前正在生成弹幕的行
this.createTotal = 0; // 总共取了多少条
this.total = 0; // 原始数据总数
this.jumpLinkFlag = barrageVue.jumpLinkFlag // 是否需要点击弹幕跳转链接
this.maxRows = barrageVue.rows / 2; // 最大的弹道数量
this.fullScreen = barrageVue.fullScreen; // 是否全屏弹幕
this.delay = barrageVue.delay; // 延迟时间 --> 你希望弹幕需要多少秒滚动一屏,弹幕文组滑过容器的时间
this.createTime = barrageVue.createTime;
this.isBatchDestory = barrageVue.isBatchDestory // 是否批量删除弹幕
this.batchDestoryNum = barrageVue.batchDestoryNum; //批量删除的数量
this.level = 1; // 弹幕等级
this.top = 0; // ,每一条弹幕相对于自己父盒子的距离
this.offsetWidth = 0; // 弹幕容器宽度
this.defaultColor = '#fff';
this.everyRowsLengths = new Array(this.maxRows).fill(0)
}
getBarrageList = () => this.barrageList
getBarrageTimer = () => this.barrageTimer
getPauseFlag = () => this.pauseFlag
saveOffsetWidth = (value) => { this.offsetWidth = value }
saveBarrageItemClientWidth = (clientWidth, currentRow) => {
this.everyRowsLengths[currentRow] = this.everyRowsLengths[currentRow] + clientWidth
}
setDefaultColor = (value) => { this.defaultColor !== value && (this.defaultColor = value) }
computedTopValue = (rowIndex = 0) => `${(rowIndex * this.BarrageVueInstance.rowHeight)}px`
addBarrage(value) {
this.barrageList.push(value) // 将创建完成的弹幕对象添加到队列中
}
addDeleteIdInQueue(id) {
this.bacthDeleteIds.push(id)
this.bacthDeleteHandler()
}
createBarrage(barrage, userCreateBarrage = false) {
if (userCreateBarrage) {
this.currentRow = this.everyRowsLengths.findIndex(cur => cur === Math.min(...this.everyRowsLengths))
this.top = this.computedTopValue(this.currentRow)
this.everyRowsLengths[this.currentRow] = this.everyRowsLengths[this.currentRow] + 150
this.currentRow = this.currentRow + 1
}
const options = {
url: barrage?.url || '',
color: barrage?.color || this.defaultColor,
content: barrage?.content || '',
id: barrage?.id || Date.now(),
top: barrage?.top || this.top,
level: barrage?.level || 1,
imgLink: barrage.imgLink || '',
delay: barrage?.delay || `${this.delay}s`,
type: barrage?.type || BarrageType.MYBARRAGE,
offsetWidth: barrage?.offsetWidth || this.offsetWidth,
animationPlayState: barrage?.animationPlayState || 'running',
}
return new Barrage(options)
}
bacthDeleteHandler() {
// 如果 最大删除数量 为 0 并且 你的 批量删除数组id长度 等于了 余数 那就证明只有余数这么多弹幕待删除
if (this.maxDeleteCounter === 0 && this.lastDeleteCounter === this.bacthDeleteIds.length) {
this.batchDelete(0, this.lastDeleteCounter)
this.lastDeleteCounter = 0
this.bacthDeleteIds = []
} else if (this.maxDeleteCounter > 0) {
if (this.bacthDeleteIds.length !== this.batchDestoryNum) return
this.batchDelete(0, this.batchDestoryNum)
this.bacthDeleteIds.splice(0, this.batchDestoryNum)
this.maxDeleteCounter = this.maxDeleteCounter - 1
}
}
batchDelete(start, count) {
if (this.barrageList.length === 0) return
this.barrageList.splice(start, count)
}
deleteBarrage(id) {
const index = this.barrageList.findIndex(item => item.id === id)
if (index === -1) return
this.barrageList.splice(index, 1)
}
deleteAllBarrage() {
this.barrageList = []
}
timedCreationBarrage() {
this.barrageTimer = setInterval(() => {
if (!this.cacheSourceBarrageList[this.createTotal]) return this.clearTimer(this.barrageTimer) // 如果创建的数量以及大于了总数则清除定时器s;
this.generateBarrage()
}, this.createTime * 1000);
}
generateBarrage() {
if (this.currentRow >= this.maxRows) {
this.currentRow = 0; // 回到初始行
}
const barrageItem = this.cacheSourceBarrageList[this.createTotal] // 取出每一个弹幕对象
// 是全屏弹幕吗 ? 是的话 随机两层 level 1 和 level 2
this.level = this.fullScreen ? Math.floor(Math.random() * 2) + 1 : 1;
this.top = this.computedTopValue(this.currentRow); // 计算准确的top
const assignBarrage = Object.assign(barrageItem, { level: this.level, top: this.top, delay: `${this.delay}s`, offsetWidth: this.offsetWidth })
const barrageConstructor = this.createBarrage(assignBarrage)
this.addBarrage(barrageConstructor)
this.createTotal = this.createTotal + 1
this.currentRow = this.currentRow + 1
}
close() {
this.clearTimer(this.barrageTimer)
this.resetData()
}
// 开启弹幕
play(barrages) {
// 如果没有暂停过 则 走初始化流程
if (!this.pauseFlag) return this._init(barrages || this.cacheSourceBarrageList)
// 否则 接着上一次的继续
this.timedCreationBarrage()
}
// 暂停弹幕
pause() {
this.clearTimer(this.barrageTimer)
// 是否暂停过
this.pauseFlag = true
}
reset(barrages) {
this.close()
this._init(barrages || this.cacheSourceBarrageList)
}
clearTimer(timerId) {
clearInterval(timerId)
this.barrageTimer = null
}
// 重置数据
resetData() {
this.total = 0
this.createTotal = 0
this.barrageList = []
this.maxDeleteCounter = 0
this.lastDeleteCounter = 0
this.pauseFlag = false
this.everyRowsLengths = new Array(this.maxRows).fill(0)
}
// 初始化弹幕弹幕数据
_init(barrageList) {
// 如果你不是一个数组 或者 你是 数组但长度为 0 则 return
if (!Array.isArray(barrageList) || (Array.isArray(barrageList) && barrageList.length === 0)) return;
this.cacheSourceBarrageList = barrageList; // 缓存传入的弹幕数据
this.total = barrageList.length;
if (this.isBatchDestory) { // 如果是批量删除的话才计算 次数
this.maxDeleteCounter = Math.floor(barrageList.length / this.batchDestoryNum); // 计算最大可删除的数量
this.lastDeleteCounter = barrageList.length % this.batchDestoryNum; // 计算最后一次需要删除的数量
}
this.timedCreationBarrage()
}
}
这里面就是对弹幕的一些操作,包括了弹幕的开启
,重置
,关闭
,暂停
,等等,如果要看完整版的代码,大家可以去我的 github拉取代码,逐行分析。
2.3 中文文档开发
相信一个好的库,是少不了一个好的文档的,虽然我的库很一般,但是我还是希望能做到最好,给大家代码更轻松地体验。
中文文档,我首页是自己开发的,想自己去定制一款第一份属于自己的文档,正如大家开头所见那个,其实它也是一个Vue项目,当然如果你想深入了解,你可以点击这里,支持主题,中英文切换。
这里可以跟大家插一嘴,像这种暗色主题是如何实现的?
2.4 :暗色主题切换实现方式。
技术选型 : scss + vue2 实现
思路 : 将 html身上加一个属性,然后根据属性选择器进行应用不同的样式(混入样式)。然后主题切换时 去更改 html的属性,这样就实现了暗色主题切换
, 如果你是less 原理也是一样的,只不过代码上略有差异
核心代码
scss
$themes : (
light : ( // 定义 百色主题 的一些颜色变量
bgColor : $theme-linear-gradient,
color : $black,
btnBgColor : #f1f1f1,
borderColor : #bcbcbc,
themeSvgBgColor : $white,
themeSvgColor : #767676,
githubLeftColor : $white,
docBgColor : $white,
customColor : $theme-linear-gradient
),
dark : ( // 定义暗色主题的一些变量
bgColor : $black,
color : $white,
btnBgColor : #2f2f2f,
borderColor : #474747,
themeSvgBgColor : $black,
themeSvgColor : $white,
githubLeftColor : $black,
docBgColor : $black,
customColor : $white
),
);
@function getVar($key){ // 函数获取刚刚定义中的变量属性的值
$themeMap : map-get($themes , $curTheme); // 遍历 themes对象 , $curTheme 值为 当前主题 默认为 暗色 dark
@return map-get($themeMap , $key); //返回 遍历的对象根据 传入的key来取出 变量的值
};
$curTheme :dark;
@mixin theme-color() { // 混入一个 改变主题色的工具
@each $key , $value in $themes{ // 遍历主题的键 和 值
$curTheme : $key !global; // 将curTheme作为全局变量 将key(dark or light ) 赋值给 curTheme 这样就可以根据主题定义的对象取出属性
html[data-theme=#{$key}] & { // 然后根据当前主题 根据属性选择器 选择
@content; // 将传入的 样式 作为 当前主题的 样式进行使用
}
}
}
// 使用 方式
@include theme-color { // 这里所有的内容将作为内容给到@content
background-color: getVar("btnBgColor");
border: 1px solid getVar("borderColor");
}
utils.js
js
export function getAttribute (el , attributeName ) {
return document.querySelector(el).getAttribute(attributeName)
}
export function setAttribute (el , attributeName , value) {
document.querySelector(el).setAttribute(attributeName , value)
}
app.vue
js
mounted() {
setAttribute("html", attributeType.DATA_THEME, themeType.LIGHT);
},
methods : {
const currentTheme = getAttribute("html", attributeType.DATA_THEME);
if (currentTheme === themeType.DARK) {
setAttribute("html", attributeType.DATA_THEME, themeType.LIGHT);
} else {
setAttribute("html", attributeType.DATA_THEME, themeType.DARK);
}
this.$emit("input", !this.value);
}
}
2.5 : github部署中文文档
这个我会在后面继续更新一篇文章,单独来讲,如何用分支部署github项目,如果你想学习,请随时关注我的动态。
2.6 :讲讲VuePress
说起这个,你不得不说,写代码也要靠缘分,其实在开发这个弹幕库之前,我是不知道 vuePress
这个框架的,有一天晚上我 刷抖音
看到有一个老师,讲vitePress
, 我就在想,既然有VitePress那应该有vuePress,然后第二天,我就开始学习了起来。照着官网搭建了一个项目。
它能够把你的Mackdown
转换成一个网页,平时你在Mack down 的笔记,你用vuepress 它也可以帮你实现文档自由,真的太好用了,家人们,强烈建议学一下!!!。
2.6.1 学习VuePress的疑难杂症
让我最头疼的一点就是 vuepress 项目的部署,如果你是在github上部署你的vuePress 项目你不能自定义项目名称,否则项目样式就会丢失,你必须设置根路径,并且项目还需要 用户名.github.io
这种规则,没有服务器的日子也太难过了,有没有大佬知道怎么解决,可以在评论区留言。
接下来就是打包我们自己写的组件了。
2.7 : 如何用vueCli打包一个组件
- 项目根目录下创建 package 文件夹
- 提供一个出口index.js和一个package.json
- 同级就是你的组件包
- 修改vue.config.js
index.js
这里我是参考Vant
组件库组件注册方式进行实现的
js
import miniVueBarrage from './barrage/index.vue'; // 导入你的组件入口
const version = '1.0.0'; // 设定版本
const components = [ // 将你的组件放入一个数组
miniVueBarrage
]
const install = (Vue) => { // 进行批量注册逐渐
components.forEach(component => {
Vue.component(component.name, component);
});
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) { // 判断window 身上是否有vue实例 有的话 调用install进行组件注册
install(window.Vue);
}
export {
miniVueBarrage// 导出组件
};
export default { // 导出一个 install 函数和版本
install, version
}
然后 你需要修改你的 vue.config.js
js
const { defineConfig } = require('@vue/cli-service')
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path')
const isProduction = process.env.NODE_ENV === 'production'
const resolve = (dir) => path.resolve(__dirname, dir)
module.exports = defineConfig({
publicPath: './', //设置根路径
outputDir: path.resolve(__dirname, './dist/lib'), // 设置打包出口
transpileDependencies: true, //指定需要被编译的依赖模块
productionSourceMap: false, // 生产环境是否需要 sourceMap
chainWebpack(config) {
isProduction && config.plugins.delete('html') // 打包完成后 我不需要 html文件生成 删除HTMLPlugin
},
css: {
extract: {
filename: isProduction ? 'mini-vue-barrage.css' : '[name].css', // 采用css 分离插件
chunkFilename: '[name].css', // 每一个csschunk css的文件名称
},
loaderOptions: {
postcss: {
postcssOptions: {
plugins: [
require('postcss-preset-env')({// css 编译转换 增加 --webkit等
browsers: [
"defaults",
"not ie < 11", // 版本不小于 ie 11的
"last 2 versions", //并且他的前两个版本
"> 1%",// 将市场份额大于1%的浏览器
"iOS 7",// ios大于 7
"last 3 iOS versions" // ios的前三版本
]
})
]
}
}
}
},
configureWebpack: {
entry: isProduction ? './package/index.js' : './src/main.js', // 设置入口, 如果是生产 则打包我的package 也就是我写的组件
output: {
filename: isProduction ? 'index.js' : '[name].js', // 设置打包出口
library: {
name: 'miniVueBarrage', // 指定库的名称
type: 'commonjs' // 指定输出类型
},
},
optimization: {
minimize: isProduction, // 是否采用 js 压缩
minimizer: [ // 采用js压缩的插件
isProduction ? new TerserPlugin({
terserOptions: {
nameCache: true,
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ["console.log"] // 移除console
},
output: {
beautify: true, // 压缩注释
comments: false,
}
}
}
) : ''
]
},
plugins: [ // 使用css压缩
new CssMinimizerPlugin(),
isProduction && new CopyWebpackPlugin({ // 使用copyPlugin 机型文件复制
patterns: [ //
{
from: './package/package.json',
to: resolve('./dist/package.json'),
},
{
from: './README.md',
to: resolve('./dist/README.md'),
}
]
})
]
}
})
有了以下配置你就可以对你的组件进行打包了,但我还是建议大家使用webpack进行搭建,我这里是因为以及用了cli所以方便,如果包比较多我建议你使用webpack,自己搭建一套,更有价值。
2.8 : 压缩体积
然后 关于压缩体积 , 我们这里做的就是 css压缩和js压缩以及移除一些注释,尽可能地减少包的体积,从代码层面因为只有一个组件也没有很大的优化空间,所以主要所做的还是压缩以及转换工作,将不必要的文件可以移除。
2.9 : npm发包,更新包,版本查看
好,到这里就是最后一步了,我们打包完成后 会生成下面的几个文件 ,分别是 lib
, package.json
, README.md
lib 就不说了 ,打包完的js和css都在lib下,给大家重点讲一下package.json
2.9.1 package.json
json
{
"name": "minivuebarrage", // 你的包名
"version": "0.2.9", // 包的版本 0 --> 你的主版本 2 --> 你的版本, 9 --> 你的每一次更新的小版本
"private": false, // 是否是私有的
"description": "minivuebarrage 是一个轻量级的弹幕组件", // 你的包描述
"main": "./lib/index.js", // 你的包的入口文件是哪个
"scripts": { // 你的脚本命令,相信大家都很熟悉
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": { // 包的作者
"name": "xiaozhang",
"url": "https://github.com/xiaozhangclassmater" // 作者的github
},
"repository": {
"url": "https://github.com/xiaozhangclassmater/miniVueBarrage", // 作者的仓库链接
"type": "git" //类型为 git
},
"engines": { // node版本限制
"node": ">= 10.0.0"
},
"keywords": [ // 你的包 关键字,别人在npm上可以通过哪些关键字来搜到你的包
"Barrage assembly",
"minivuebarrage"
],
"license": "MIT",//开源协议
"browserslist": [ // 浏览器的版本
"> 1%",
"last 2 versions",
"not dead"
]
}
在这里重点说一下 main
这个属性。
我们平时导入一个包都是直接引入包名,并没有向下再写路径,有没有想过这个问题,为什么我们能在antd这个包中导入一些东西。
js
import antd from 'antd'
其实很简单,就是antd这个包下面 有一个package.json
文件,main 指定了我们的入口,当我们打包的时候,它就会根据main提供的路径进行查找。所以这就是原因。
2.9.2 发布包
- 切换到你打包文件的目录,比如
dist
- 执行 npm login 进行登录
会生成
1: npm notice Log in on https://registry.npmjs.org/
代表你的 npm源,相信大家在国内的开发者很多都是 淘宝的镜像源,在这里我们需要切换成 npm原镜像。
2: Username
: 提示你输入你的npm 用户名,没有的可以去npm上注册 ,我的是 xiaozhangclassxxx
3: Password
: 提示你输入 npm密码
4: Email: (this IS public)
: 提示你输入你的邮箱,它会在邮箱里给你发一个验证码,然后你填到终端就可以了
npm notice Please check your email for a one-time password (OTP) Enter one-time password: (验证码)
登录成功后 它会提示 Logged in as xiaozhangclassmate on https://registry.npmjs.org/.
然后你可以通过 npm publish 就可以把当前目录下所有的文件发布到 npm仓库上。
更新包
查看当前包版本 输入你的更新类型 , 比如 update docs(更新文档)
js
npm version <update_type>
// 发布之前 你需要把你的package.json --> version 进行一次小版本升级 例如 你之前是 0.0.1 那你这一次应该是 0.0.2
npm publish -m '对此版本的描述'
弃用包
看官方文档,说得很详细 去官方文档
ok,到这里基本上以及结束了,这就是我的开发过程,下面给大家展示一波才艺。
3:幕后花絮
太多报错了,哈哈哈,这只是一点点,还有很多报错,没记录下来,所以 过程是艰辛的,结果是美好的不经历风雨,怎么能见彩虹
, 我们一起加油。
结束
最后,如果大家想去看源码怎么实现的可以去 我的github ,目前还在迭代开发中,有好的想法可以来提PR
我是前端小张同学,期待你的关注,跟我一起行动起来,前端没死。