共计 8526 个字符,预计需要花费 22 分钟才能阅读完成。
最近上头让我写个我的项目简略的官方网站,需要很简略,有前后端,前端负责获取要跳转的外链进行跳转和介绍视频的播放,后端负责传回外链和须要播放的视频。我拿到需要,想了想,这样子的需要就用不着数据库了,后端写个配置文件,传回固定的数据就能够了,视频嘛,就通过流的形式传给前端。
确定好了实现形式,那就撸起袖子开干。通过简略思考,应用 vue3+koa2 的形式来做。所有从简,装置 vue3-cli 和 koa2 来新建前后端我的项目。
一.vue3 前端我的项目搭建
通过 npm install -g @vue/cli
或者 yarn global add @vue/cli
装置好 vue/cli
, 再通过vue create 我的项目名
(本人用英文代替掉我的项目名)新建对应的我的项目。接下来,npm install
装置一遍全副依赖,通过 npm
给本人的我的项目加个配套的element-plus
, 即通过npm install element-plus --save
装置好 element-plus。再在 main.js 文件里援用。
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
createApp(App).use(ElementPlus).mount('#app')
二.koa2 后端我的项目搭建
具体的需要页面就不形容,次要就是两个 get 申请去申请后端。那么后端怎么做呢?一样的,先通过 npm install koa-generator -g
装置 koa2
,再通过 koa2 我的项目名
创立好我的项目,最初 npm install
装置一遍全副依赖。而后 npm start
跑一遍,能跑起来就是弄好了。
三. 前后端联调须要做的本地代理配置
1. 前端方面:
若有文件 vue.config.js,则在外面写上 proxy
代理规定,若没有文件,则新建一个在我的项目顶层再写上代理规定。规定大抵如下:
module.exports = {
publicPath: './',
outputDir: './dist',
productionSourceMap: false,
lintOnSave: false,
devServer: {
port: 8808,// 前端跑起来的端口
disableHostCheck: true,
hotOnly: false,
compress: true,
watchOptions: {ignored: /node_modules/,},
proxy: {// 代理规定,代理到本地 3000 端口,应用“/api”重写门路到“/”"/api": {
target: "http://127.0.0.1:3000/",
changeOrigin: true, // target 是域名的话,须要这个参数
secure: false, // 设置反对 https 协定的代理
ws: false,
pathRewrite: {"^/api": "/"},
},
}
},
chainWebpack: config => {config.plugins.delete('preload')
config.plugins.delete('prefetch')
},
css: {sourceMap: false,},
};
2. 后端方面:
在我的项目的 app.js 文件内补充对应的代理规定,大抵规定如下:
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const proxy = require('koa2-proxy-middleware')
const index = require('./routes/index')
const users = require('./routes/users')
const web = require('./routes/web')
const koaMedia = require('./routes/koaMedia')
// error handler
onerror(app)
// middlewares
app.use(bodyparser({enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {extension: 'ejs'}))
app.use(koaMedia({extMatch: /\.mp[3-4]$/i
}))
const options = {// 后端我的项目与前端我的项目代理对接,target 为前端端口,一样通过“/api”重写
targets: {
'/api': {
target: 'http://127.0.0.1:8808/',
ws: true,
changeOrigin: true,
pathRewrite: {'^/api': '' // 和前端代理一样, 抉择 api 替换}
},
}
}
app.use(proxy(options));
// logger
app.use(async (ctx, next) => {ctx.set("Access-Control-Allow-Origin", "*");// 在 app 内通过该字段,使全副端口过去的信息都能通过。const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
app.use(web.routes(), web.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {console.error('server error', err, ctx)
});
module.exports = app
按上述配置实现的话,前端发送申请时在门路首部减少 ”/api” 字段即可正确发送到后端,后端也可顺利发送信息返回前端。
四. 前后端实现视频流并分段传输
1. 前端方面:
<template>
<div class="videoPlay">
<video
ref="m3u8_video"
class="video-js vjs-default-skin vjs-big-play-centered"
controls
>
<source :src="videoSrc" type="video/mp4"/>
</video>
</div>
</template>
<script setup>
import {nextTick, onBeforeUnmount, onMounted, ref, watch} from "vue";
import videojs from "video.js";
import zh from "video.js/dist/lang/zh-CN.json";
import 'videojs-flash'
const props = defineProps({
videoSrc: {// 链接例子为:"/api/video?path=" + url;
type: String,
default: ""
}
})
const m3u8_video = ref();
let player;
const initPlay = async () => {videojs.addLanguage("zh-CN", zh);
await nextTick();
const options = {
muted: true,
controls: true,
autoplay: false,
loop: false,
language: "zh-CN",
techOrder: ["html5"],
};
player = videojs(m3u8_video.value, options, () => {if (props.autoPlay && props.videoSrc) {player.play();
}
player.on("error", () => {videojs.log("播放器解析出错!");
});
});
};
const resetPlayer = () => {player.load();
}
onMounted(() => {initPlay();
});
// 间接扭转门路测试
watch(() => props.videoSrc,
() => {player.pause();
player.src(props.videoSrc);
player.load();
if (props.videoSrc) {player.play();
}
}
);
onBeforeUnmount(() => {player?.dispose();
});
defineExpose({resetPlayer})
</script>
<style lang="scss" scoped>
.videoPlay {
width: 100%;
height: 100%;
.video-js {
height: 100%;
width: 100%;
object-fit: fill;
::v-deep .vjs-big-play-button {
font-size: 2.5em !important;
line-height: 2.3em !important;
height: 2.5em !important;
width: 2.5em !important;
-webkit-border-radius: 2.5em !important;
-moz-border-radius: 2.5em !important;
border-radius: 2.5em !important;
background-color: #73859f;
background-color: rgba(115, 133, 159, 0.5) !important;
border-width: 0.15em !important;
margin-top: -1.25em !important;
margin-left: -1.75em !important;
}
.vjs-big-play-button .vjs-icon-placeholder {font-size: 1.63em !important;}
}
.vjs-paused{
::v-deep .vjs-big-play-button {display: block !important;}
}
}
:deep(.vjs-tech) {object-fit: fill;}
</style>
2. 后端方面:
文件 koaMedia.js
const fs = require('fs')
const path = require('path')
const mine = {
'mp4': 'video/mp4',
'webm': 'video/webm',
'ogg': 'application/ogg',
'ogv': 'video/ogg',
'mpg': 'video/mepg',
'flv': 'flv-application/octet-stream',
'mp3': 'audio/mpeg',
'wav': 'audio/x-wav'
}
let getContentType = (type) => {if (mine[type]) {return mine[type]
} else {return null}
}
let readFile = async(ctx, options) => {
// 确认客户端申请的文件的长度范畴
let match = ctx.request.header['range']
// 获取文件的后缀名
let ext = path.extname(ctx.query.path).toLocaleLowerCase()
// 获取文件在磁盘上的门路
// let diskPath = decodeURI(path.resolve(options.root + ctx.query.path))
// 获取文件的开始地位和完结地位
let bytes = match.split('=')[1]
let stats = fs.statSync(ctx.query.path)
// 在返回文件之前,晓得获取文件的范畴(获取读取文件的开始地位和开始地位)let start = Number.parseInt(bytes.split('-')[0]) // 开始地位
let end = Number.parseInt(bytes.split('-')[1]) || start + 999999 // 完结地位
end = end > stats.size - 1 ? stats.size - 1 : end;
let chunksize = end - start + 1;
// 如果是文件类型
if (stats.isFile()) {return new Promise((resolve, reject) => {
// 读取所须要的文件
let stream = fs.createReadStream(ctx.query.path, {start: start, end: end})
// 监听‘close’当读取实现时,将 stream 销毁
ctx.res.on('close', function () {stream.destroy()
})
// 设置 Response Headers
ctx.set('Content-Range', `bytes ${start}-${end}/${stats.size}`)
ctx.set('Accept-Range', "bytes")
ctx.set("Content-Length", chunksize)
ctx.set("Connection", "keep-alive")
// 返回状态码
ctx.status = 206
// getContentType 上场了,设置返回的 Content-Type
ctx.type = getContentType(ext.replace('.',''))
stream.on('open', function(length) {
try {stream.pipe(ctx.res)
} catch (e) {stream.destroy()
}
})
stream.on('error', function(err) {
try {ctx.body = err} catch (e) {stream.destroy()
}
reject()})
// 传输实现
stream.on('end', function () {resolve()
})
})
}
}
module.exports = function (opts = {}) {
// 设置默认值
let options = Object.assign({}, {extMatch: ['.mp4', '.flv', '.webm', '.ogv', '.mpg', '.wav', '.ogg'],
root: process.cwd()}, opts)
return async (ctx, next) => {
// 获取文件的后缀名
if(ctx.url.indexOf("/video") > -1){// 如果文件申请门路有 video,则下一步
let ext = path.extname(ctx.query.path).toLocaleLowerCase()
// 判断用户传入的 extMath 是否为数组类型,且拜访的文件是否在此数组之中
let isMatchArr = options.extMatch instanceof Array && options.extMatch.indexOf(ext) > -1
// 判断用户传输的 extMath 是否为正则类型,且申请的文件门路蕴含相应的关键字
let isMatchReg = options.extMatch instanceof RegExp && options.extMatch.test(ctx.query.path)
if (isMatchArr || isMatchReg) {if (ctx.request.header && ctx.request.header['range']) {
// readFile 上场
return await readFile(ctx, options)
}
}}
await next()}
}
因为 node 限度,所以上述文件的执行胜利,须要先在 main.js 设置动态文件门路 ,即:app.use(require('koa-static')(__dirname + '/public'))
,能力顺利读取文件。
另外后端发送 base64 图片也须要设置动态文件门路。其操作代码相似于:
router.get('/getWebs', function (ctx, next) {let fileArr = fs.readdirSync(path.join(__dirname,'../public/images/icon'),{encoding:'utf8', withFileTypes:true})
for(let i = 0; i < fileArr.length; i++){let filePath = path.join(__dirname, `../public/images/icon/${fileArr[i].name}`);
let fileObj = fs.readFileSync(filePath);
urlData.data[i].background_image = `data:image/png;base64,${fileObj.toString('base64')}`;
}
ctx.body = {
success: true,
data: urlData.data
}
})
五. 我的项目上线服务器
以 winSCP 软件为例:
1. 前端我的项目:
前端我的项目上线很简略,这里临时不讲简单的 webpack 打包配置,毕竟只是简略的我的项目上线,走个残缺的流程。前端我的项目打包只须要执行 npm run build
打包取得我的项目里 dist 文件夹,把 dist 文件夹丢到对应的服务器上即可。
2. 后端我的项目:
后端我的项目不须要打包,然而也不须要上传 node_module 文件夹,把其余文件夹上传到服务器对应的后端文件夹中,winSCP 关上对应的文件门路,再运行我的项目。
然而 ,这里要留神的是,不能像本地一样间接运行 npm start
命令,因为在服务器端这样运行我的项目是无奈获取运行日志的。如果运行 npm start
命令,在服务器内是不能像开发时间接 ctrl + c
来完结过程的。须要通过 netstat -ano
命令查看所有端口的占用状况,在列表内查找端口对应的过程号来敞开过程。或者间接通过命令
netstat -nlp | grep 8080(举例的端口号)//-n --numeric 的缩写,即通过数值展现 ip 地址
//-l --listening 的缩写,只打印正在监听中的网络连接
//-p --program,打印相应端口号对应过程的过程号
来查找对应的过程号 PID
,再通过终止命令 kill -15 24971
或者强制终止命令 kill -9 24971
来终止对应过程。
实际上,服务端运行后端 node 我的项目,是通过 pm2
形式来治理过程的。在这之前,须要在我的项目文件顶层新建一个 app.json 文件,来包容本来的 ”npm start” 命令。
//app.json
{
"apps": [
{
"name": "afa-info",
"script": "npm",
"args": ["start"],
"out_file": "./logs/afa-info-app.log",
"error_file": "./logs/afa-info-err.log"
}
]
}
接着就能够应用 pm2
命令来启动我的项目了,通过这种形式启动,能够随时打印出我的项目的日志。通过 pm2 start npm --name 我的项目名 – start
来启动我的项目,其中我的项目名需替换成我的项目的名称,这个名称和原我的项目内 package.json 文件内的 name
字段无关,仅做服务器过程辨认作用。失常来说,这样子把我的项目在服务器上跑起来,在浏览器输出服务器的 IP 地址和端口拜访前端页面,就能够看到本来前端我的项目的页面,且前端申请失常获取了后端返回的信息。至此,功败垂成。
pm2 常用命令:
⑴pm2 start npm --name 我的项目名 – start
:将后端我的项目在服务器上跑起来。
⑵pm2 status
:查找 pm2 内我的项目过程的相干信息。例图:
⑶ pm2 stop 0
:进行过程 id 为 0 的过程。例图:
⑷ pm2 delete 0
:彻底删除 id 为 0 的过程。例图: