likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 20

大家好,我是前端小张同学,最近在忙着公司的业务开发,都没有怎么更新自己的学习记录了,借着今天这个机会,就给大家分享一下,我是如何把上一次的弹幕设计,在业余时间写成了一个简单的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 开发弹幕

其实弹幕 主要是用面向对象的设计理念来进行开发的,其目的就是为了让后面开发更方便,每一个功能只需要调用一个方法,方法里去实现具体的功能。

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 原理也是一样的,只不过代码上略有差异

核心代码


$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

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

  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组件库组件注册方式进行实现的

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

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

{
  "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这个包中导入一些东西。

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(更新文档)

 npm version <update_type>
 
 // 发布之前 你需要把你的package.json  --> version 进行一次小版本升级 例如 你之前是 0.0.1 那你这一次应该是 0.0.2 
 npm publish -m '对此版本的描述'

弃用包

看官方文档,说得很详细 去官方文档

ok,到这里基本上以及结束了,这就是我的开发过程,下面给大家展示一波才艺。

3:幕后花絮

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

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

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

太多报错了,哈哈哈,这只是一点点,还有很多报错,没记录下来,所以 过程是艰辛的,结果是美好的不经历风雨,怎么能见彩虹, 我们一起加油。

结束

最后,如果大家想去看源码怎么实现的可以去 我的github ,目前还在迭代开发中,有好的想法可以来提PR

minivueBarrage中文文档

minivueBarrage npm仓库

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