继需求开发之后我把它写成了一个库。

大家好,我是前端小张同学,最近在忙着公司的业务开发,都没有怎么更新自己的学习记录了,借着今天这个机会,就给大家分享一下,我是如何把上一次的弹幕设计,在业余时间写成了一个简单的mini库,对比上次思路设计,这次是进阶版。

弹幕功能: 开启、关闭、重置、暂停、发布弹幕,icon图标、链接跳转、自定义用户自己发布的弹幕样式、修改颜色,批量删除等。

待添加功能 :点赞,自定义多类型弹幕,其他的还在等考虑,有好的建议可以在评论区留言。

minivueBarrage中文文档

minivueBarrage npm仓库

1 : 前言

1.1 : 为什么会做这件事 ?

先跟大家说说为什么会做这件事,其实能开始动手写弹幕库这件事情是来自于我上班时的一个灵感,因为我们公司有一个需求,就是给基金走势图上增加弹幕,但正好这个东西就是我负责开发的,所以我在公司完成了一版,但我发现不是很好,没有设计的那么完美,代码没有层次性等等,所以我决定再写一版,于是在业余时间开始设计弹幕。

1.2 想法

说说我的想法吧,对于弹幕写成一个库这件事情其实我是很避讳的,不敢挑战,为什么呢,因为弹幕这个功能太常见了,必定是会有集成好了的比我好上千倍的库,甚至别人的功能比我多得多 , 但是为什么我还要去写,去做这件事情呢? 我认为你去做一件事情没有对和错,对你自己来水只有成功和失败,过程中的收获全都是你自己学到的。

1.2 :学习收获

  1. 需求设计
  2. mini库的中文文档开发
  3. 如何实现中英文切换以及暗色主题切换
  4. 如何部署github静态网站(分支部署)
  5. vuePress 搭建文档网站学习
  6. 如何用vueCli打包一个组件package
  7. vueCli优化包体积。
  8. 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打包一个组件

  1. 项目根目录下创建 package 文件夹
  2. 提供一个出口index.js和一个package.json
  3. 同级就是你的组件包
  4. 修改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 发布包

  1. 切换到你打包文件的目录,比如 dist
  2. 执行 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

minivueBarrage中文文档

minivueBarrage npm仓库

我是前端小张同学,期待你的关注,跟我一起行动起来,前端没死。

相关推荐
BigTopOne14 小时前
【ijkplayer】 硬解码流程
前端
kyriewen15 小时前
DeepSeek Harness开源第一天我就上手了——和Claude Code的差距比想象中大
前端·ai编程·deepseek
捡田螺的小男孩15 小时前
什么是 Skill?手把手带你写一个简单有用的 Skill!
前端·后端·程序员
IT_陈寒15 小时前
Redis集群这个坑,差点让我通宵
前端·人工智能·后端
90后的晨仔15 小时前
uni-app 路由跳转与页面导航 —— 终极详解(对标 iOS / Android / 鸿蒙三端原生)
前端
Heo16 小时前
大厂前端调试不能只会debugger
前端·javascript·面试
用户21816970493016 小时前
Flutter (十七) 网络请求
前端
cidy_9816 小时前
React 19 + Vite 企业级前端项目:从零搭建到规范交付
前端
用户9210802628616 小时前
如何把一张图片做成自定义复杂 UI 图标
前端
用户9385156350716 小时前
从 0 拆解一个 Next.js 笔记系统:npx、App Router、RSC 与组件规划全记录
前端·后端·全栈