前言
在平时使用工具的时一直想着拥有自己的一套npm库,但是迟迟没有动静,这次项目中使用audio,想着封装一套自定义的audio小工具,下面我会为大家做详细介绍以及在途中遇到的坑
创建项目
可以使用两种方式创建vue3项目
1.第一种方式 vite
2.第二种方式创建 @vue/cli
相比之下我更喜欢使用vite创建(1.开发环境中,无需打包操作,可快速的冷启动2.轻量快速的热重载(HMR)3.真正的按需编译,不再等待整个应用编译完成),此处不过多介绍
npm init vite@latest
创建步骤
- 提示我们安装是否需要create-vite@5.2.3依赖,选择y,可以快速创建一个vite项目
- 起一个组件名称,然后选择vue
- 这里我们选择javaScript,项目中没有使用TS,然后按回车
- 安装成功
- 项目中我们需要使用到element-plus,需要手动下载
npm install element-plus --save
- –save 等同于 -S (常用,可保存在package.json文件中),
-S, --save 安装包信息将加入到dependencies(生产阶段的依赖,也就是项目运行时的依赖,就是程序上线后仍然需要依赖) - –save-dev 等同于 -D
-D, --save-dev 安装包信息将加入到devDependencies(开发阶段的依赖,就是我们在开发过程中需要的依赖,只在开发阶段起作用。)
创建组件
- 首先在项目中src目录下创建一个package文件夹
- 在package文件夹下创建一个.vue文件(自定义组件名)
- 这里就开始封装我们需要的组件
组件我不做介绍,不懂得爱豆请联系我
<template>
<div>
<audio @timeupdate="updateProgress"
controls
autoplay
ref="audioRef"
id="myaudio"
style="display: none">
<source :src="audioUrl"
type="audio/mpeg" />
您的浏览器不支持音频播放
</audio>
<div class="audio_right"
:style="{background:GradientBg}">
<img class="audio_icon"
src="../assets/images/Fastrewind.png"
@click="Fastrewind"
alt="快退" />
<span class="advanceRetreat">-{{backSecond}}s</span>
<img v-if="audioIsPlay"
@click="playAudio"
class="audio_icon"
src="../assets/images/play.png"
alt="播放" />
<img v-if="!audioIsPlay"
@click="playAudio"
class="audio_icon"
src="../assets/images/pause.png"
alt="暂停" />
<span class="advanceRetreat">+{{forwardSecond}}s</span>
<img class="audio_icon"
src="../assets/images/fastForward.png"
@click="fastForward"
alt="快进" />
<el-slider class="slider_box"
v-model="currentProgress"
:show-tooltip="false"
@input="handleProgressChange" />
<div class="audio_time">
<span class="audio_current">{{ audioStart }}</span>
/
<span class="audio_total">{{ durationTime }}</span>
</div>
<div class="volume">
<div class="volume_progress"
v-show="audioHuds">
<el-slider vertical
height="100px"
class="volume_bar"
v-model="audioVolume"
:show-tooltip="false"
@change="handleAudioVolume" />
<span class="Volume_Level"> {{ audioVolume}}%</span>
</div>
<img class="volume_icon"
v-if="audioVolume <= 0"
@click.stop="audioHuds = !audioHuds"
src="../assets/images/audio_mute.png"
alt="" />
<img class="volume_icon"
v-if="audioVolume > 0"
@click.stop="audioHuds = !audioHuds"
src="../assets/images/audio_high.png"
alt="" />
</div>
</div>
</div>
</template>
<script>
export default {
//一定要写组件名,切记切记★★★
name: 'l-vue3-audio'
}
</script>
<script setup name='l-vue3-audio'>
import { ref, onMounted, watch, onUnmounted, computed } from "vue";
const props = defineProps({
audioUrl: String, //试听的链接
isPauseTtsAudio: Boolean, //是否暂停播放试听
color1: { //背景颜色
type: String,
default: '#9cafe8'
},
color2: { //背景颜色
type: String,
default: '#9cebb8'
},
isinit: { //是否初始化
type: Boolean,
default: false
},
backSecond: {
type: Number,
default: 3
},
forwardSecond: {
type: Number,
default: 10
}
});
const audioIsPlay = ref(true); //音频是否在播放
const audioStart = ref("0:00");
const durationTime = ref("0:00"); //音频的总时长,显示的时间格式
const duration = ref(0); //音频的总时长
const audioVolume = ref(80); //音量的默认值是0.8
const audioHuds = ref(false); //是否显示音量slider
const audioRef = ref(null);
const currentProgress = ref(0);
watch(() => props.isPauseTtsAudio, (newVal, oldVal) => {
console.log(newVal)
if (newVal) {
// 如果 isPauseTtsAudio 为 true,试听暂停
handleCloseMusic();
} else {
setTimeout(() => {
audioIsPlay.value = false;
audioRef.value && audioRef.value.click() // 【主要代码 - 解决报错】先模拟与页面进行交互,防止报错
audioRef.value && audioRef.value.play() // 播放音频
}, 15)
// audioRef.value.play();
}
}, { deep: true, immediate: true });
watch(() => props.isinit, (newVal, oldVal) => {
console.log(newVal)
if (newVal) {
closeRendering()
}
}, { deep: true, immediate: true });
//动态背景颜色
const GradientBg = computed(() => {
return `linear-gradient(to left, ${props.color1}, ${props.color2})`
});
// 暂停
function handleCloseMusic () {
audioRef.value.pause();
audioIsPlay.value = true;
}
//关闭重新渲染
function closeRendering () {
nextTick(() => {
audioRef.value.pause();
audioIsPlay.value = true;
audioRef.value.currentTime = 0;
})
}
const handleGlobalClick = (event) => {
if (!event.target.closest('.volume_icon')) {
console.log('检测到全局点击');
audioHuds.value = false
}
};
onMounted(() => {
calculateDuration();
document.addEventListener('click', handleGlobalClick);
});
// 获取音频时长
function calculateDuration () {
var myVid = audioRef.value;
myVid.loop = false;
myVid.src = props.audioUrl;
// 监听音频播放完毕
myVid.addEventListener(
"ended",
function () {
audioIsPlay.value = true;
currentProgress.value = 0;
},
false
);
if (myVid != null) {
myVid.oncanplay = function () {
duration.value = myVid.duration; // 计算音频时长
durationTime.value = transTime(myVid.duration); //换算成时间格式
};
myVid.volume = 0.8; // 设置默认音量50%
// 进入页面默认开始播放
// audioRef.value.play();
// audioIsPlay.value = false;
}
}
// 音频播放时间换算
function transTime (duration) {
const minutes = Math.floor(duration / 60);
const seconds = Math.floor(duration % 60);
const formattedMinutes = String(minutes).padStart(2, "0"); //padStart(2,"0") 使用0填充使字符串长度达到2
const formattedSeconds = String(seconds).padStart(2, "0");
return `${formattedMinutes}:${formattedSeconds}`;
}
// 播放暂停控制
function playAudio () {
if (audioRef.value.paused) {
audioRef.value.play();
audioIsPlay.value = false;
} else {
audioRef.value.pause();
audioIsPlay.value = true;
}
}
// 根据当前播放时间,实时更新进度条
function updateProgress (e) {
var value = e.target.currentTime / e.target.duration;
if (audioRef.value.play) {
currentProgress.value = value * 100;
audioStart.value = transTime(audioRef.value.currentTime);
}
}
//调整播放进度
const handleProgressChange = (val) => {
console.log(val);
if (!val) {
return;
}
let currentTime = duration.value * (val / 100);
// 更新音频的当前播放时间
audioRef.value.currentTime = currentTime;
audioRef.value.pause();
audioIsPlay.value = true;
};
//调整音量
const handleAudioVolume = (val) => {
audioRef.value.volume = val / 100;
};
//鼠标移除音量控件
// const handleMouseLeave = () => {
// audioHuds.value = false
// }
//快退
const Fastrewind = () => {
//当前时间
if (audioRef.value.currentTime > props.backSecond) {
audioRef.value.currentTime = audioRef.value.currentTime - props.backSecond
audioRef.value.pause();
audioIsPlay.value = true;
}
}
//快进
const fastForward = () => {
if (audioRef.value.duration - audioRef.value.currentTime > props.forwardSecond) {
audioRef.value.currentTime = audioRef.value.currentTime + 10
audioRef.value.pause();
audioIsPlay.value = true;
}
}
onUnmounted(() => {
document.removeEventListener('click', handleGlobalClick);
});
</script>
<style lang="scss" scoped>
.audio_right {
width: 300px;
height: 40px;
display: flex;
align-items: center;
// background: linear-gradient(to left, #9cafe8 0%, #9cebb8 100%);
border-radius: 20px;
padding: 0 10px;
box-sizing: border-box;
position: relative;
.slider_box {
width: 160px;
height: 4px;
border-radius: 5px;
background-color: #f1f1f1;
flex: 1;
margin: 0 8px 4px;
}
.audio_icon {
width: 20px;
height: 20px;
margin-bottom: 4px;
cursor: pointer;
}
.audio_time {
color: #f1f1f1;
overflow: hidden;
font-size: 12px;
position: absolute;
bottom: -1px;
left: 110px;
.audio_total {
float: right;
}
.audio_current {
float: left;
}
}
}
.volume {
position: relative;
.volume_progress {
width: 32px;
height: 140px;
position: absolute;
top: -146px;
right: -4px;
}
.volume_bar {
background: #276bf982;
border-radius: 4px;
}
.volume_icon {
width: 24px;
height: 24px;
cursor: pointer;
}
}
</style>
<style lang="scss">
.el-slider__button-wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.slider_box,
.volume_bar {
.el-slider__button {
width: 8px;
height: 8px;
border: none;
}
.el-slider__bar {
background: #00db15;
}
}
.slider_box {
.el-slider__button-wrapper {
width: 8px;
}
}
.volume_bar {
.el-slider__runway {
margin: 0 14px !important;
}
}
.Volume_Level {
font-size: 12px;
text-align: center;
width: 100%;
color: #fff;
position: absolute;
bottom: 1px;
}
.advanceRetreat {
font-size: 12px;
color: #ffffff;
width: 20px;
height: 20px;
margin: 0 4px;
margin-bottom: 4px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
- 封装的组件可以在app.vue中引用下是否能正常使用
导出组件
- 在src下创建index.js
import lVue3Audio from './package/laudio.vue' // 引入封装好的组件
export { lVue3Audio } //实现按需引入*
const components = [lVue3Audio] // 将来如果有其它组件,都可以写到这个数组里
//批量注册组件
const install = function (App, options) {
components.forEach((item) => {
console.log(item)//这里可以输出下组件是否有name,组件中的name至关重要
App.component(item.name, item)
})
}
export default { install } // 批量的引入*
这里因为前期组件中我没有写 name: ‘l-vue3-audio’,导致后续下载一直引入不进去
2. 使用vite构建
编辑vite.config.js文件,新增build属性 vite中文文档
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import viteCompression from 'vite-plugin-compression'//打包压缩
export default (mode, command) => {
console.log('🚀 ~ command:', command)
console.log('🚀 ~ mode:', mode)
return defineConfig({
plugins: [
vue(),
dts(),
viteCompression({
verbose: true,
disable: false, // 不禁用压缩
deleteOriginFile: false, // 压缩后是否删除原文件
threshold: 10240, // 压缩前最小文件大小
algorithm: 'gzip', // 压缩算法
ext: '.gz', // 文件类型
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
minify: 'esbuild',//boolean | 'terser' | 'esbuild'//压缩方式
sourcemap: true,//boolean | 'inline' | 'hidden' 默认: false;构建后是否生成 source map 文件,下面会说一下
//库编译模式配置
lib: {
entry: resolve(__dirname, 'src/index.js'), //指定组件编译入口文件,是必须的因为库不能使用 HTML 作为入口
name: 'l-vue3-audio',// 则是暴露的全局变量,在 formats 包含 'umd' 或 'iife' 时是必须的
fileName: (format) => `l-vue3-audio.${format}.js`,//是输出的包文件名
},
// rollup打包配置
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ['vue'],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue',
},
},
},
},
})
}
上面所说的build.sourcemap
前端工程打包后代码会跟项目源码不一致,当代码运行出错时控制台上定位出错代码的位置跟项目源码上不对应。这时候我们很难定位错误代码的位置。SourceMap的用途是可以将转换后的代码映射回源码,如果设置了js文件对应的map资源,那么就可以在控制台进行调试时直接定位到源码位置。
boolean: true | false
默认为false,不生成map文件。当设置成true时,会生成单独的map文件,并且在对应的bundle文件中生成相应注释指明map文件。
build.minify 是指在构建(Build)过程中对代码进行压缩(Minify)的操作。在 Web 开发中,通常会将 JavaScript、CSS 和 HTML 文件进行压缩,以减小文件大小并提高加载速度。
代码压缩(Minification)是指去除代码中的多余空格、注释和换行符等,将代码压缩成一行或几行,以减小文件体积。这样做的好处包括:
减小文件大小:压缩代码可以显著减小文件大小,从而加快文件的下载速度。
提高加载速度:减小文件大小可以减少网络传输时间,加快网页加载速度。
减少带宽消耗:压缩代码可以减少服务器和客户端之间的数据传输量,节省带宽成本。
保护代码:压缩后的代码更难阅读和理解,可以一定程度上保护源代码不被轻易泄露。代码压缩(Minification)是指去除代码中的多余空格、注释和换行符等,将代码压缩成一行或几行,以减小文件体积。这样做的好处包括:
减小文件大小:压缩代码可以显著减小文件大小,从而加快文件的下载速度。
提高加载速度:减小文件大小可以减少网络传输时间,加快网页加载速度。
减少带宽消耗:压缩代码可以减少服务器和客户端之间的数据传输量,节省带宽成本。
保护代码:压缩后的代码更难阅读和理解,可以一定程度上保护源代码不被轻易泄露。
回到正滚,上面简单的了解一下vite的build属性
- 修改package.json文件
{
"name": "l-vue3-audio", //包名字
"private": true, //是否私有
"version": "0.0.1", //版本
"type": "module",//type 字段用于指定模块的类型,module表示该模块遵循ECMAScript 模块规范,使用 import 和 export 进行模块导入和导出。
"keyword": "vue3 audio 自定义组件",//在npm上可被搜索的关键字
"description": "基于vue3的audio组件", //描述
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
//当 npm 包发布时,files 指定的文件会被推送到 npm 服务器中,如果指定的是文件夹,那么该文件夹下面所有的文件都会被提交。
"files": [
"dist"
],
"main": "./dist/l-vue3-audio.umd.js", //入口文件
//module 字段可以定义 npm 包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用。如果 npm 包导出的是 ESM 规范的包,使用 module 来定义入口文件。
"module": "./dist/l-vue3-audio.es.js",
"exports": {
".": {
"import": "./dist/l-vue3-audio.es.js",
"require": "./dist/l-vue3-audio.umd.js"
}
},
"dependencies": {
"element-plus": "^2.6.2",
"vue": "^3.4.21"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"sass": "^1.72.0",
"vite": "^5.2.0",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-dts": "^3.8.1"
},
"keywords": [],
"author": "",
"license": "ISC"//开源协议
}
打包
当我们都配置好以后,我们就要打包了,这是我们要上传得文件
- 打包,生成dist文件
npm run build
2. 注册npm账号 官网地址
- 有些小伙伴可能本地的npm镜像源采用的是淘宝镜像源或者其它的,如果想要发布npm包,我们得吧我们得npm源切换为官方得源,命令如下:
npm config set registry=https://registry.npmjs.org
- 发布前准备
在dist文件生成package.json文件,自定义组件名(唯一,重名报错重新起一个就行),版本号不能相同每次
在dist根目录中运行
npm init -y //生成package.json
{
"name": "l-vue3-audio",//打包后npm下载的名字
"version": "0.0.8",//版本号,每次要保持不一致,删除指定版本后也不能在提交相同版本
"description": "基于vue3的audio组件",
"keyword": "vue3 audio 自定义组件",
"main": "l-vue3-audio.es.js",//入口文件
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
//npm 所搜的关键词
"keywords": [
"vue",
"vue3",
"audio",
"vue3audio"
],
"author": "",
"license": "ISC"//开源
}
- 添加npm用户
在dist目录下,运行命令
npm adduser
添加npm账号得用户名和密码还有邮箱地址
按回车键
填写一次性密码后登陆
5. 执行发布命令
npm publish
已经上传成功
这里讲解一下几个上传失败的原因
已经有此版本请重新修改版本号,重新上传
- 这里在讲个我途中遇见的问题,一个小插曲吧,挺闹心的,希望爱豆们不要遇见
登陆npm的时候一直登陆不上去,查找了半天原因是因为我开了代理,然后我就把代理关闭,还是不行,清除npm缓存npm cache clean --force,还是不可以,迫不得已重启了一下,好了,哎就是这么神奇吧,没有重启解决不了的,有了就多重启几遍,啊啊啊啊啊~~~~~~
终于成功啦✌✌✌
对了还有一个给爱豆避雷一下,在上传途中我打包上传了很多次,每次都叠加版本号,以至于越来越高,前面都还都不可用,无奈之下我就删除前面的版本号,npm unpublish l-vue3-audio@版本号 --force,后面想重新从第一个版本上传,发现一直报错,npm不允许上传已经删除的包以及版本号,因为每次上传都是会分配一个唯一值,所以删除就不可以在使用
使用组件
当我们要在项目中使用的时候就复制npm i l-vue3-audio
package.json文件中就有了我们安装的组件
这个时候只要像element-plus 那样引入就可以全局使用了,在main.js中引入
import lVue3Audio from 'l-vue3-audio'
import 'l-vue3-audio/style.css'
app.use(lVue3Audio)
在我们使用的页面使用即可
这里我们就全部结束啦,恭喜你又前进了一步,拥有了第一个自己npm包,愿我们在前进的道路上步步稳赢,加油爱豆们☛☀✈✈
稍微讲解一下这个插件的参数吧
ttsAudioUrl | audio 的 url |
---|---|
isPauseTtsAudio 是否暂停播放试听 | 默认 true |
forwardSecond 快进速度 | 默认 10 |
backSecond 快退速度 | 默认 3 |
color1背景渐变颜色从左往右1 | 默认 #9cafe8 |
color2背景渐变颜色从左往右2 | 默认 #9cebb8 |
isinit是否重置 | 默认fasle |