最近上头让我写个我的项目简略的官方网站,需要很简略,有前后端,前端负责获取要跳转的外链进行跳转和介绍视频的播放,后端负责传回外链和须要播放的视频。我拿到需要,想了想,这样子的需要就用不着数据库了,后端写个配置文件,传回固定的数据就能够了,视频嘛,就通过流的形式传给前端。
  确定好了实现形式,那就撸起袖子开干。通过简略思考,应用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 handleronerror(app)// middlewaresapp.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));// loggerapp.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`)})// routesapp.use(index.routes(), index.allowedMethods())app.use(users.routes(), users.allowedMethods())app.use(web.routes(), web.allowedMethods())// error-handlingapp.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的过程。例图: