这是我参与新手入门的第3篇文章
复制代码
背景、需求分析
背景
基于产品需求,需要开发一个播放器用于播放报警现场处置视频。目前内部组件库都没有基于浏览器直接可用的符合设计稿的播放器组件,因此尝试用最近较火的新语言TypeScript封装视频播放组件。
内部组件库介绍:公司的一套基于Vue.js和设计规范的UI组件库
TypeScript介绍:TypeScript是JavaScript的超集,它可以编译成纯JavaScript。TypeScript可以在任何浏览器任何计算机和任何操作系统上运行,并且是开源的。
(为了完整性,代码块较多可直接copy测试)
demo模块分解
1、列表的相关视频数据基础数据获取(截图视频数据来源于网络自测用)
2、弹层功能与播放器功能独立开发,适配其他功能需求。
3、弹层组件包括:popup.ts、popup.css
弹层组件类似于我们常用的Dialog,配置参数包括width(宽度)、height(高度)、title(标题)、content(内容)、pos(left、right、居中)、mask(遮罩)
4、播放器组件包括video.ts、video.css
配置参数width()、height()、elem、url、autoplay
目录结构
├─dist//打包后文件
├─node_modules
│ ├─//全部插件.........................
├─src
│ ├─components //组件
│ │ ├─popup //弹框组件
│ │ | ├─popup.css //组件样式
│ │ | ├─popup.ts //逻辑处理
│ │ ├─video //..
│ │ | ├─video.css //组件样式
│ │ | ├─video.ts //逻辑处理
│ ├─iconfont //字体图标
│ ├─index.html //首页面结构
│ ├─main.css //首页面样式
├─package.json //配置文件
├─package-lock.json //
├─webpack.config.dev.js //公共的函数库
├─webpack.config.pro.js //公共的函数库
复制代码
Webpack搭建项目环境
创建webpack与loader的使用
1、npm init -y 创建package.json配置文件
2、cnpm i -D webpack webpack-cli 局部安装webpack和webpack-cli
3、在根目录下创建webpack.config.js(与package.json同级)
4、在根目录下创建src文件夹下创建a.js 和mian.js
a.js
let a=123;
export default a;
复制代码
main.js
import a from './a.js'
console.log(a)
复制代码
webpack.config.js
//node语言
module.exports={
entry:"./src/main.js",//入口文件位置
output:{
path:path.resolve(__dirname,'dist')
filename: main.js
}, //出口
mode:"development"
}
复制代码
package.json 配置打包脚本
"scripts":{
"test":"echo \"Error: no test specified\" && exit 1",
"build":"webpack" //默认运行文件为webpack.config.js,若为其他webpack --config 文件名(如webpack.config.js)
}
复制代码
webpack对js的支持是友好的,但是对与css和html需要下载loader
cnpm i -D style-loader css-loader 用在css文件打包
webpack的插件使用
cnpm i -D html-webpack-plugin 用在html文件打包
cnpm i -D clean-webpack-plugin 对dist文件先清除再打包,否则会按照覆盖的方式,有些无用文件不会被删除
webpack-dev-server的使用
cnpm i -D webpack-dev-server(默认热更新,代码改变不需要刷新浏览器)
package.json
"start":"webpack-dev-server"
复制代码
webpack.config.js
devServer:{ //cnpm i -D webpack-dev-server
contentBase:"/dist" //指定文件根路径位置
open:true //自动打开
},
复制代码
支持字体图标的使用
cnpm i -D file-loader
webpack.config.js
module:{
rules:[{
test:/\.css$/,
use:['style-loader','css-loader'] //运行顺序为后往前
},{
test:/\.(eot|woff2|woff|svg|tff)$/,
use:['file-loader']
}]
},
复制代码
支持TypeScript的使用
安装ts-loader和typescript
cnpm i -D ts-loader typescript
创建tsconfig.json文件
{
"compilerOptions":{//编译配置选项
"module": "ES6",
"target": "ES5" //输出结果
}
}
复制代码
{
test:/\.ts$/,
use:['ts-loader'],
exclude:/node_modules/ //排除掉该文件夹下的ts文件
}
复制代码
import a from './a'; //ts模块化的要求不需要写后缀
复制代码
ts模块化的要求不需要写后缀,但是不写的情况下webpack会优先匹配js然后json,都没用则会报错,因此需要在webpack.config.js配置ts。
resolve:{
"extensions":['.ts','.js','.json'] //省略后缀名时的匹配先后顺序
}
复制代码
视频列表实现
视频列表页实现其结构
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="list">
<ul class="list-wrap clearfix">
<li data-url="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7cdabcaa763392c86b944eaf4e68d6a3.mp4" data-title="客厅视频001">
<div>
<img src="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/96563e75833ba4563bd469dd28203b09.jpg?thumb=1&w=296&h=180&f=webp&q=90" alt="">
<i class="iconfont iconbofang1"></i>
</div>
<h3>客厅视频001</h3>
</li>
<li data-url="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/e25d81c4922fca5ebe51877717ef9b76.mp4" data-title="客厅视频002">
<div>
<img src="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/2fd26bb99b723337a2f8eaba84f7d5bb.jpg?thumb=1&w=296&h=180&f=webp&q=90" alt="">
<i class="iconfont iconbofang1"></i>
</div>
<h3>客厅视频002</h3>
</li>
<li data-url="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/eadb8ddc86f1791154442a928b042e2f.mp4" data-title="主卧视频">
<div>
<img src="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/a8dd25cab48c60fc6387b9001eddc3f9.jpg?thumb=1&w=296&h=180&f=webp&q=90" alt="">
<i class="iconfont iconbofang1"></i>
</div>
<h3>主卧视频</h3>
</li>
<li data-url="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/7f49c1ccd75f76ec86b52c9ae4c4a082.mp4" data-title="次卧视频">
<div>
<img src="https://cdn.cnbj1.fds.api.mi-img.com/mi-mall/b4004fc968de331f702585d58b15aba2.jpg?thumb=1&w=296&h=180&f=webp&q=90" alt="">
<i class="iconfont iconbofang1"></i>
</div>
<h3>次卧视频</h3>
</li>
</ul>
</div>
</body>
</html>
复制代码
视频列表页实现其样式
@font-face {font-family: "iconfont";
src: url('./iconfont/iconfont.eot?t=1590117385937'); /* IE9 */
src: url('./iconfont/iconfont.eot?t=1590117385937#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAR8AAsAAAAACbgAAAQvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDSAqGHIUWATYCJAMYCw4ABCAFhG0HZxtbCBEVnJfJvkiwbS1sj8Y9msZx0vnYLzWSrgke+O+0PxfjwNMF7GnX6cJ8MtLHTzgJEHDG2AAmbf7K9y+u5akfkTBRc4fy7cijL6S9fCFdLx39cfjTP8/HtPn+mi5zzSZ1URd24QcKaIyJi6xA4sQS4JbhtS51QY9DAC7lqYvo2r3vUGw05DgBiLmzZkzCjnnRJTiB7TBzLtWIPRjYcrt8Ddjtfr/4mcbCBomhIKcOnN5tKh1zpUu2qCAfMNZGoOsvDLBXgQLqAhrE2tzocpRfrYvClX+DpjcQwkaic6VzVXNjc/tLtuTzxIwkKQjpujX9w1MYSBAm2BoCbF+LsYM6UwQBuaoiaMiNFUFBbr9aiVGyBbxIcJo9LgwD/yAaA7buXgKNwEBW9cld4hnXTcUikSUzDhdmMoeyqanTLb7UgSA9bSpf+mCxP2Oa2S/aZQLB1eLrQgw7aKti7uEelK65bjZutF8Jrnre0ANFfn9EBf+plZJrxGGblk0NFfUrcHKN5x5+qIU/PazdePGwNf7lwuqrD8VTmAevwt++Ipua6GbfKitPvu15ky4+eP22ME49MTN7KKMOH/Y8OSk44A5XI4oPMuzQhVSQfvr94L2OBzh40X/2w8IPhh67UBCk7wYND3D0Ypl7C9c8/34lfea9ri98UNk4/+ECeaz58kqVrlW4FjddlSrrhkBNgtfLgzbngeWv0+HjvyPxsvHI/a+8eqSgcsH2V1+5Px7/u1oZDffqK9tJR155dS6j0b0duW01jVXv6q1Y1aqJONKodL3STZ4+NciKD4s3jw6LhZidd/b66w5WicvhMl7lYHC7KiM/lMnhKv9te+dRe7j9qHMw6DAbpZyfZt4yTrFfcd/2r1+JWs2G1l63A8kPbWZFr1w/Vi/7mhnjF/5Gy0aLG0ktfls4oojYu5FFvXv7Cd9yLPI2LyeSMdZCDlRT5aeQp4qUcHHPqK7R1fV4PbVSRX6ynyFTrejbCdG2v9meBuCTX6c5OJOnaaf9U2zU1v1SupprtGIgPlNLq5tHIgCTsRjwJsyuvgF+pGSCL/qyp2v3g8AmHYLEohwobKoiNbYuGHg0BRObDuBShx5XeyRZjEJoB6jNZgSCGLdBEuFJUMR4Hamxn4NBhl/BJCYUuNg+u7vlmfvo9KGiErSg/4AMZ2cE5NQbfEXfoqpRg0v/xLpRAeZhSjZeMGPt44zt2y8iDlzlBM/oaRgjQ6l8QCPDKlL24+jyPmkwnLpTDyqkCGQB7Q8gBsucvbU4TX39K+Q1kVJLVpWVP6FqQzsHs8FUA/lizbVWncvem2/eQghHHeVULAHPEEZRrzJQ8g86QIYYrC3Kxd6I13N1rcP49nSV5wAXuatZaWOdz+6msqbrL2eXQtn/N7JqjZx/pNmp7OcL2zghCZ1Ruj/6VweYyb7rAAA=') format('woff2'),
url('./iconfont/iconfont.woff?t=1590117385937') format('woff'),
url('./iconfont/iconfont.ttf?t=1590117385937') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('./iconfont/iconfont.svg?t=1590117385937#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.iconguanbi:before {
content: "\e61a";
}
.iconquanpingzuidahua:before {
content: "\e786";
}
.iconbofang1:before {
content: "\e65c";
}
.iconduomeitiicon-:before {
content: "\e624";
}
.iconzanting:before {
content: "\e693";
}
*{ margin:0; padding:0;}
ul{ list-style: none;}
img{ display: block;}
body{ background:#f5f5f5; height:2000px;}
.clearfix::after{ content:"";display: block; clear:both;}
#list{ width:1226px; margin:20px auto;}
#list .list-wrap{ width:1240px;}
#list .list-wrap li{ float:left; width:296px; height:auto; margin-right:14px; background:white; cursor: pointer;}
#list .list-wrap li:hover i{ background:#ff6700;}
#list .list-wrap div{ position: relative;}
#list .list-wrap img{ width:100%; height:100%;}
#list .list-wrap i{ position: absolute; bottom:10px; left:19px; border:2px white solid; color:white; border-radius: 10px; width:32px; height:20px; text-align: center; line-height: 20px; font-size:12px;}
#list .list-wrap h3{ font-size: 12px; text-align: center; padding:30px 0;}
复制代码
Popup弹层组件开发
组件框架搭建
1、创建popup.css、popup.ts文件
2、引入popup.ts文件
3、为视频列表添加点击事件
main.ts文件
import './main.css';
import popup from './components/popup/popup';
import video from './components/video/video';
let listItem = document.querySelectorAll('#list li');
for(let i=0;i<listItem.length;i++){
listItem[i].addEventListener('click',function(){
let url = this.dataset.url;
let title = this.dataset.title;
//console.log(url , title);
popup({
width : '880px',
height : '556px',
title,
pos : 'center',
content(elem){
//console.log( elem );
video({
url,
elem,
autoplay : true
});
}
});
});
}
复制代码
模块化css方式
创建弹层布局结构及样式
1、结构块包括弹层标题、关闭按钮、视频组件
2、参数width、height、pos,分别表示高度、宽度、位置信息。位置信息默认居中,可选参数left、right。
//创建模板
template(){
this.tempContainer = document.createElement('div');
this.tempContainer.style.width = this.settings.width;
this.tempContainer.style.height = this.settings.height;
this.tempContainer.className = styles.popup;
this.tempContainer.innerHTML = `
<div class="${styles['popup-title']}">
<h3>${ this.settings.title }</h3>
<i class="iconfont iconguanbi"></i>
</div>
<div class="${styles['popup-content']}"></div>
`;
document.body.appendChild(this.tempContainer);
if(this.settings.pos === 'left'){
this.tempContainer.style.left = 0;
this.tempContainer.style.top = (window.innerHeight - this.tempContainer.offsetHeight) + 'px';
}
else if(this.settings.pos === 'right'){
this.tempContainer.style.right = 0;
this.tempContainer.style.top = (window.innerHeight - this.tempContainer.offsetHeight) + 'px';
}
else{
this.tempContainer.style.left = (window.innerWidth - this.tempContainer.offsetWidth)/2 + 'px';
this.tempContainer.style.top = (window.innerHeight - this.tempContainer.offsetHeight)/2 + 'px';
}
}
复制代码
创建遮罩层结构及其样式
createMask(){
this.mask = document.createElement('div');
this.mask.className = styles.mask;
this.mask.style.width = '100%';
this.mask.style.height = document.body.offsetHeight + 'px';
document.body.appendChild(this.mask);
}
复制代码
弹层交互及弹层容器回调实现
一、弹层交互
1、给关闭按钮添加点击事件监听
2、判断是否有遮罩层,如有需要移除
handle(){
let popupClose = this.tempContainer.querySelector(`.${styles['popup-title']} i`);
popupClose.addEventListener('click',()=>{
document.body.removeChild( this.tempContainer );
this.settings.mask && document.body.removeChild(this.mask);
});
}
复制代码
二、弹层容器视频块回调实现
contentCallback(){
let popupContent = this.tempContainer.querySelector(`.${styles['popup-content']}`);
this.settings.content(popupContent);
}
复制代码
弹出层中的Video播放器组件开发
视频组件框架搭建
1、创建video.css、video.ts文件
2、引入video.css样式文件
3、为视频列表添加点击事件
let styles = require('./video.css');
interface Ivideo {
url : string;
elem : string | HTMLElement;
width? : string;
height? : string;
autoplay? : boolean;
}
interface Icomponent {
tempContainer : HTMLElement;
init : () => void;
template : () => void;
handle : () => void;
}
function video(options : Ivideo){
return new Video(options);
}
复制代码
添加播放器布局结构
内容块如下:视频块、播放进度条(时间总进度条、已播放进度条、拖拽圆点)、播放按钮、播放时间、声音按钮、声音条、全屏按钮
template(){
this.tempContainer = document.createElement('div');
this.tempContainer.className = styles.video;
this.tempContainer.style.width = this.settings.width;
this.tempContainer.style.height = this.settings.height;
this.tempContainer.innerHTML = `
<video class="${styles['video-content']}" src="https://juejin.cn/post/${this.settings.url}"></video>
<div class="${styles['video-controls']}">
<div class="${styles['video-progress']}">
<div class="${styles['video-progress-now']}"></div>
<div class="${styles['video-progress-suc']}"></div>
<div class="${styles['video-progress-bar']}"></div>
</div>
<div class="${styles['video-play']}">
<i class="iconfont iconbofang1"></i>
</div>
<div class="${styles['video-time']}">
<span>00:00</span> / <span>00:00</span>
</div>
<div class="${styles['video-full']}">
<i class="iconfont iconquanpingzuidahua"></i>
</div>
<div class="${styles['video-volume']}">
<i class="iconfont iconduomeitiicon-"></i>
<div class="${styles['video-volprogress']}">
<div class="${styles['video-volprogress-now']}"></div>
<div class="${styles['video-volprogress-bar']}"></div>
</div>
</div>
</div>
`;
if(typeof this.settings.elem === 'object'){
this.settings.elem.appendChild(this.tempContainer);
}
else{
document.querySelector(`${this.settings.elem}`).appendChild(this.tempContainer);
}
}
复制代码
添加播放器样式
.video{ position: relative; overflow: hidden;}
.video-content{ width:100%; height: 100%; object-fit: cover;}
.video-controls{ width:100%; position: absolute; bottom: -50px; left:0; height:50px; background:rgba(0,0,0,.8); transition: .5s;}
.video-progress{ position: relative; width:100%; height:5px; background:#222223;}
.video-progress-now{ width:0; height:100%; background:#ff6a03; position:absolute; left:0; z-index: 1;}
.video-progress-suc{ width:0; height:100%; background:#666;position:absolute; left:0; }
.video-progress-bar{ height:14px; width:14px; background:white; border-radius: 50%; position: absolute; left:0; top:0; margin-left:-7px; margin-top:-4px; z-index: 2;}
.video-play{ float: left; height:45px; line-height: 45px; margin-left: 35px; }
.video-play i{ color:white; font-size:20px; cursor: pointer;}
.video-time{ float:left; height:45px; line-height: 45px; margin-left:30px; color:white;}
.video-full{ float: right; height:45px; line-height: 45px; margin-right: 20px;}
.video-full i{ color:white; font-size:20px; cursor: pointer;}
.video-volume{ float: right; height:45px; display: flex; align-items: center; margin-right:30px;}
.video-volume i{ font-size:20px; color:white; margin-right:20px;}
.video-volprogress{ width:100px; height:5px; background:#222223; position: relative; }
.video-volprogress-now{ width:50%; height:100%; background:#ff6a03;}
.video-volprogress-bar{ height:14px; width:14px; background:white; border-radius: 50%; position: absolute; left:50%; top:0; margin-left:-7px; margin-top:-4px; z-index: 2;}
复制代码
播放与暂停的实现
1、为视频播放按钮添加点击事件
2、判断设置或返回音频/视频是否暂停属性paused
3、调用play()、pause()方法。
let videoPlay = this.tempContainer.querySelector(`.${ styles['video-controls'] } i`);
let videoContent : HTMLVideoElement = this.tempContainer.querySelector(`.${ styles['video-content'] }`);
//播放暂停
videoPlay.addEventListener('click',()=>{
if(videoContent.paused){
videoContent.play();
}
else{
videoContent.pause();
}
});
复制代码
当前时间与总时间
1、为video对象添加播放暂停事件
2、当播放时定时获取播放的时间和缓冲的时间来改变进度条的宽度。
3、当暂停播放时,清除定时器。
let videoContent : HTMLVideoElement = this.tempContainer.querySelector(`.${ styles['video-content'] }`);
let videoProgress = this.tempContainer.querySelectorAll(`.${ styles['video-progress'] } div`);
let videoTimes = this.tempContainer.querySelectorAll(`.${ styles['video-time'] } span`);
let timer;
function playing(){ // 正在播放中
let scale = videoContent.currentTime / videoContent.duration;
let scaleSuc = videoContent.buffered.end(0) / videoContent.duration;
videoTimes[0].innerHTML = formatTime(videoContent.currentTime);
videoProgress[0].style.width = scale * 100 + '%';
videoProgress[1].style.width = scaleSuc * 100 + '%';
videoProgress[2].style.left = scale * 100 + '%';
}
function formatTime(number:number):string{
number = Math.round(number);
let min = Math.floor(number/60);
let sec = number%60;
return setZero(min) + ':' + setZero(sec);
}
function setZero(number:number):string{
if(number<10){
return '0'+number;
}
else{
return '' + number;
}
}
复制代码
播放器全屏功能
let videoFull = this.tempContainer.querySelector(`.${ styles['video-full'] } i`);
//全屏
videoFull.addEventListener('click',()=>{
videoContent.requestFullscreen();
});
复制代码
播放器进度条的实现
//视频播放事件
videoContent.addEventListener('play',()=>{
videoPlay.className = 'iconfont iconzanting';
timer = setInterval(playing , 1000);
});
//视频暂停事件
videoContent.addEventListener('pause',()=>{
videoPlay.className = 'iconfont iconbofang1';
clearInterval(timer);
});
function playing(){ // 正在播放中
let scale = videoContent.currentTime / videoContent.duration;
let scaleSuc = videoContent.buffered.end(0) / videoContent.duration;
videoTimes[0].innerHTML = formatTime(videoContent.currentTime);
videoProgress[0].style.width = scale * 100 + '%';
videoProgress[1].style.width = scaleSuc * 100 + '%';
videoProgress[2].style.left = scale * 100 + '%';
}
复制代码
拖拽播放进度条与音量进度条
videoProgress[2].addEventListener('mousedown',function(ev:MouseEvent){
let downX = ev.pageX;
let downL = this.offsetLeft;
document.onmousemove = (ev:MouseEvent)=>{
let scale = (ev.pageX - downX + downL + 8) / this.parentNode.offsetWidth;
if(scale<0){
scale = 0;
}
else if(scale > 1){
scale = 1;
}
videoProgress[0].style.width = scale * 100 + '%';
videoProgress[1].style.width = scale * 100 + '%';
this.style.left = scale * 100 + '%';
videoContent.currentTime = scale * videoContent.duration;
};
document.onmouseup = () => {
document.onmousemove = document.onmouseup = null;
};
ev.preventDefault();
});
复制代码
配置自动播放
if(this.settings.autoplay){ // 是否进行自动播放处理
timer = setInterval(playing,1000);
videoContent.play();
}
复制代码
打包项目案例
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry : "./src/main.ts",
output : {
path : path.resolve(__dirname , 'dist'),
filename : "main.js"
},
devServer : {
contentBase : "/dist",
open : true
},
resolve : {
"extensions" : ['.ts','.js','.json']
},
module : {
rules : [
{
test : /\.css$/,
use : [MiniCssExtractPlugin.loader,'css-loader'],
exclude : [
path.resolve(__dirname , 'src/components')
]
},
{
test : /\.css$/,
use : [MiniCssExtractPlugin.loader,{
loader : 'css-loader',
options : {
modules : {
localIdentName: '[path][name]__[local]--[hash:base64:5]'
}
}
}],
include : [
path.resolve(__dirname , 'src/components')
]
},
{
test : /\.(eot|woff2|woff|ttf|svg)$/,
use : [
{
loader : 'file-loader',
options : {
outputPath : 'iconfont'
}
}
]
},
{
test : /\.ts$/,
use : ['ts-loader'],
exclude : /node_modules/
}
]
},
plugins : [
new HtmlWebpackPlugin({
template : "./src/index.html"
}),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin()
],
mode : "production"
};
复制代码
总结
以上就是本次分享的全部内容,主要包括以下几个内容:
1、了解TypeScript是如何工作的
2、了解项目是如何构建的
3、了解代码是如何组织的,如何更加高效地进行开发
4、了解播放器api的基本使用
5、利用webpack工具搭建项目环境,让项目支持TS
6、需求分析,弹层组件与播放器组件需要哪些配置参数
7、如何设计组件的相关方法以及对css进行模块化
8、如何利用相关api完成项目开发