hqin数字人项目开发

背景

和hqin数字人项目,该项目采用 vite + vue3 技术开发的h5项目,放公众号打开。录音和保存视频跳转小程序,调用小程序的接口。对方要求代码风格使用 ESLint + Airbnb风格,然后,再设置一下gitHooks,提交git时,校验一下代码规范。项目引入了神策埋点和阿里arms监控。

技术点

1. ESLint + Airbnb 代码风格

1.1 安装与配置

参考文档, 综合分析普遍使用的 eslint 依赖包后,推荐下面这一整套依赖包:

推荐必配 9 个依赖包:

  • eslint: ESLint 是一种用于识别和报告在 ECMAScript/JavaScript 代码中发现的模式的工具。 必须首先安装这个依赖包,因为其他的依赖包建立在它之上。
  • eslint-define-config:为 .eslintrc.js 文件提供 defineConfig 功能。
  • eslint-plugin-vue:Vue.js 的官方 ESLint 插件,它允许我们使用 ESLint 检查文件中的 Vue 代码。
  • eslint-plugin-prettier: 将 Prettier 作为 ESLint 规则运行。 如果你想禁用与代码格式相关的所有其他 ESLint 规则,并且仅启用检测潜在错误的规则,则此插件效果最佳。 如果你安装了 eslint 那么你应该会遇到 eslint 规则和 prettier 规则冲突。可用 eslint-config-prettier 解决 eslint 规则和 prettier 规则的冲突。
  • eslint-config-prettier:关闭所有不必要或可能与 Prettier 冲突的规则。
  • vue-eslint-parser:文件的 ESLint 自定义解析器.vue。
  • @typescript-eslint/parser:一个 ESLint 解析器,它利用TypeScript ESTree允许 ESLint 对 TypeScript 源代码进行 lint。
  • @typescript-eslint/eslint-plugin:一个为 TypeScript 代码库提供 lint 规则的 ESLint 插件。
  • prettier: 代码格式化程序。 Prettier 2.5 发布:支持 TypeScript 4.5 新语法和 MDX v2 注释语法

推荐可选 2 个依赖包:

  • eslint-plugin-import:支持 ES2015+ (ES6+) import / export 语法的规则,并防止文件路径和导入名称拼写错误的问题。

  • eslint-config-airbnb-base: 这个包提供了 Airbnb 的基本 JS .eslintrc(没有 React 插件)作为可扩展的共享配置。 安装 eslint-config-airbnb-base 之前请先安装 eslint-plugin-import。

因为只有开发阶段需要 eslint,所以将 eslint 的这些依赖添加到开发阶段的依赖 devDependencies 中即可。

步骤一:安装

js 复制代码
//npm i -D eslint eslint-config-airbnb-base eslint-plugin-import eslint-plugin-vue
npm i -D eslint 
npx eslint --init //选择

步骤二: 添加 Vite 运行的时候自动检测 eslint 规范

npm install -D vite-plugin-eslint

js 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import eslintPlugin from 'vite-plugin-eslint'

export default defineConfig({
	 plugins: [
	    vue(),
	    // 增加下面的配置项,这样在运行时就能检查 eslint 规范
	    eslintPlugin({
	      include: ['src/**/*.js', 'src/**/*.vue', 'src/*.js', 'src/*.vue']
	    })
	  ]
})

步骤三:配置 eslintrc.js 文件

3.1:安装 babel 插件

3.2:配置 Eslint 基本配置

js 复制代码
module.exports = {
  env: {
    browser: true,
    node: true
  },
  extends: [
    'airbnb-base',
    'plugin:vue/vue3-recommended' // 使用插件支持vue3
  ],
  // 页面引用"@"报错的配置
  settings: {
    'import/extensions': [
      '.js', // 如果你只使用纯 JavaScript 文件
      '.ts', // 如果你在项目中使用 TypeScript
      '.vue', // 如果你在项目中使用 Vue 单文件组件
    ],
    'import/resolver': {
      alias: {
        map: [['@', './src']], // 设置别名解析
        extensions: ['.js', '.ts', '.vue'], // 设置支持的扩展名
      },
    },
  },
  parserOptions: {
    parser: '@babel/eslint-parser',
    sourceType: 'module',
    ecmaVersion: 12,
    allowImportExportEverywhere: true, // 不限制eslint对import使用位置
    ecmaFeatures: {
      modules: true,
      legacyDecorators: true
    },
    requireConfigFile: false // 解决报错:vue--Parsing error: No Babel config file detected for
  },
  plugins: [
    'vue'
  ],
  // 省略其他配置
  globals: {
    wx: true,
  },
  rules: {
  	...
  }
}

3.3:配置 Eslint rules 规则

js 复制代码
'semi': ['warn', 'never'],           // 禁止尾部使用分号
'no-console': 'warn',                // 禁止出现console
'no-debugger': 'warn',               // 禁止出现debugger
'no-duplicate-case': 'warn',         // 禁止出现重复case
'no-empty': 'warn',                  // 禁止出现空语句块
'no-extra-parens': 'warn',           // 禁止不必要的括号
'no-func-assign': 'warn',            // 禁止对Function声明重新赋值
'no-unreachable': 'warn',            // 禁止出现[return|throw]之后的代码块
'no-else-return': 'warn',            // 禁止if语句中return语句之后有else块
'no-empty-function': 'warn',         // 禁止出现空的函数块
'no-lone-blocks': 'warn',            // 禁用不必要的嵌套块
'no-multi-spaces': 'warn',           // 禁止使用多个空格
'no-redeclare': 'warn',              // 禁止多次声明同一变量
'no-return-assign': 'warn',          // 禁止在return语句中使用赋值语句
'no-return-await': 'warn',           // 禁用不必要的[return/await]
'no-self-compare': 'warn',           // 禁止自身比较表达式
'no-useless-catch': 'warn',          // 禁止不必要的catch子句
'no-useless-return': 'warn',         // 禁止不必要的return语句
'no-mixed-spaces-and-tabs': 'warn',  // 禁止空格和tab的混合缩进
'no-multiple-empty-lines': 'warn',   // 禁止出现多行空行
'no-trailing-spaces': 'warn',        // 禁止一行结束后面不要有空格
'no-useless-call': 'warn',           // 禁止不必要的.call()和.apply()
'no-var': 'warn',                    // 禁止出现var用let和const代替
'no-delete-var': 'off',              // 允许出现delete变量的使用
'no-shadow': 'off',                  // 允许变量声明与外层作用域的变量同名
...

步骤四:启动项目,测试效果

js 复制代码
"scripts": {
    // eslint --fix自动修复所有格式问题
    "lint": "eslint --fix --ext .js,.vue src", 
 },

Vue 使用 Eslint 和 Prettier 并采用 Airbnb 规范

1.2 如何设置一下gitHooks,提交git时,校验一下代码规范

方法一:

当我们创建了一个Git的本地仓库后,项目的根目录下看到一个.git文件夹,在文件夹下的hooks目录下有很多的.sample 为后缀的钩子文件,如下所示。这些文件就是我们要改造的脚本,这个以.sample后缀结束的脚本文件是不会执行的,如果需要执行,我们需要去掉.sample后缀。参考文档1参考文档2

方法二:

配置husky(官网): husky是Git hooks工具,可以防止使用Git hooks不好的commit或者push。使用方法如下:

  1. 安装husky:npm install --save-dev husky
  2. 初始化:npx husky init:可以看到在在我们项目的根目录出现了一个 .husky 文件夹,.husky/ 目录下新增了一个名为 pre-commit 的shell脚本。也就是说在在执行 git commit 命令时会先执行 pre-commit 这个脚本。
  3. 修改.husky/pre-commit文件的执行语句:

亦可如下配置:

如何在vscode中把换行的默认方式改为LF?

设置 -> 搜索files:eol进行设置​​​​​​​ -> 选择:\n

\n 对应的是 LF

\r\n对应的是CRLF

2. 配置不同环境

项目分三个环境:本地开发环境(developmen)、测试环境(uat)、生成环境(production)。域名和接口不统一且不同环境也不一样,所以通过加几个不同环境的.env文件,在不同的环境调用。可通过import.meta.env.MODE获取所处环境。参考vite官网配置。

注意:为了防止意外地将一些环境变量泄漏到客户端,只有以 VITE_ 为前缀的变量才会暴露给经过 vite 处理的代码。

js 复制代码
import.meta.env.MODE == 'development'; // 本地开发环境
import.meta.env.MODE == 'staging'; // uat测试环境
import.meta.env.MODE == 'production'; // 生产环境

.env 文件:本地运行环境

js 复制代码
VITE_BASE = "./"

# 【神策】上报地址 (uat环境)
VITE_SENSOES_SERVER_URL = "https://sensorsdata.xxxxx.com/sa?project=default"
# 多域名打通(uat环境)
VITE_IHOMEPRO_WEB_DOAMIN = "ihomepro-uat.xxxxx.com"

.env.staging 文件:测试环境

js 复制代码
NODE_ENV = test

# vite配置base
VITE_BASE = "/digital-person"

# h5跳转小程序的版本类型:所需跳转的小程序版本 env-version(正式版release、开发版develop、体验版trial)
VITE_MINI_ENV = "trial"

# 【申朴】接口域名 (uat环境)
VITE_API_DOAMIN  = "https://hq-test.xxxxxxx.com/api"
# 【hqin】接口域名 (uat环境)
VITE_HQAPI_DOAMIN = "https://hqins-api-uat.xxxxx.com/api"

# 【神策】上报地址 (uat环境)
VITE_SENSOES_SERVER_URL = "https://sensorsdata.xxxx.com/sa?project=default"
# 多域名打通(uat环境)
VITE_IHOMEPRO_WEB_DOAMIN = "ihomepro-uat.xxxx.com"

.env.production 文件:生成环境

js 复制代码
NODE_ENV = production
# vite配置base
VITE_BASE = "/digital-person"
# h5跳转小程序的版本类型:所需跳转的小程序版本 env-version(正式版release、开发版develop、体验版trial)
VITE_MINI_ENV = "release"

#【申朴】接口域名 (生产环境)
VITE_API_DOAMIN  = "https://hq.xxxxxx.com/api"
#【hqin】接口域名 (生产环境)
VITE_HQAPI_DOAMIN = "https://hqins-api.xxxxx.com/api"


# 【神策】上报地址 (生产环境)
VITE_SENSOES_SERVER_URL = "https://sensorsdata.xxxxx.com/sa?project=HQ_Data_Analytics"
# 多域名打通(生产环境)
VITE_IHOMEPRO_WEB_DOAMIN = "ihomepro.xxxxx.com"

调用方法:

js 复制代码
/*
**【hqin】接口域名
*/
export const requestHqinDomin = import.meta.env.VITE_HQAPI_DOAMIN || '';
/*
**【申扑】接口域名
*/
export const requestDomin = import.meta.env.VITE_API_DOAMIN || '';

/*
**【神策】上报地址
*/
export const sensorsServerUrl = import.meta.env.VITE_SENSOES_SERVER_URL || '';
// 多域名打通
export const iHomeProWebDomin = import.meta.env.VITE_IHOMEPRO_WEB_DOAMIN || '';

/*
** 【wx-open-launch-weapp】
** h5跳转小程序的版本类型:所需跳转的小程序版本 env-version:
** @param:合法值为:正式版release、开发版develop、体验版trial(支持的微信版本:iOS 8.0.18及以上、Android 8.0.19及以上)
*/
export const getMiniEnvVesion = () => import.meta.env.VITE_MINI_ENV;

运行和打包配置:

3. h5跳转小程序

3.1 使用方法

官方文档,步骤:

  1. 绑定域名

    登录微信公众平台进入"公众号设置"的"功能设置"里填写"JS接口安全域名"。

  2. 引入js文件

    在需要调用JS接口的页面引入如下JS文件:res.wx.qq.com/open/js/jwe... (支持https)

    如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:res2.wx.qq.com/open/js/jwe... (支持https))

  3. 通过config接口注入权限验证配置并申请所需开放标签

js 复制代码
wx.config({
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名
  jsApiList: [], // 必填,需要使用的JS接口列表
  openTagList: ['wx-open-launch-weapp'] // 可选,需要使用的开放标签列表,例如['wx-open-launch-app']
});
  1. 通过ready接口处理成功验证
js 复制代码
wx.ready(function () {
  // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中
});
  1. 使用开放标签 wx-open-launch-weapp

官网这种写法只是针对vue2,在vue3中会报错。

html 复制代码
<div class="uploader-audio">
    <img src="https://***.png" alt="" />
    <div class="audios-txt">录制配音</div>
    <wx-open-launch-weapp
      id="launch-btn"
      appid="wx6abc************"
      :path="miniUrl"
      :env-version="miniEnv"
      style="position: absolute;left: 0;top: 0;width: 100%;height: 100%;"
      >
      <component is="script" type="text/wxtag-template">
          <div class="audios-txt">录制配音</div>
          <component is="style">
              .audios-txt {
                text-align: center;
                margin-top: 2px;
                color: #018aff;
                font-weight: bold;
                font-size: 13px;
              }
          </component>
      </component>
    </wx-open-launch-weapp>
</div>

这里 使用 is="'script'" 来转义 script 标签, 在vue2中, component 是可以直接使用 script 标签的,在vue3中已经不支持直接使用 script 标签了(直接报错),所有这里用 is="'script'" 来转义 。

运行报警告,解决办法:在vite.config.js文件进行配置

样式设置:建议写成内联样式,wx-open-launch-weapp⽤来做这个的遮罩就好。

3.2 分享卡片标题描述未获取到

问题描述:从分享页进去再次分享页面,分享出来的卡片信息获取不成功。

导致原因:从首页进去详情页分享出来的页面正常是因为页面一进去就注入wxjssdk,等到了详情页肯定注入成功;但是直接点击分享出来的详情页存在异步请求(详情页接口和jssdk获取参数接口),可能不成功,

解决办法:给获取详情页信息接口添加一个延迟操作。等注入成功后再请求信息,再设置卡片信息。

js 复制代码
const pageStart = () => {
  isShare.value = route.query.isshare || false;
  if (isShare.value) {
    setTimeout(() => {
      getDetail();
    }, 500);
  } else {
    // 安卓跳转不成功出现一串代码问题:手动注册一次
    // wxShare.register();
    // getDetail();
    wxShare.register('', {
      success: () => {
        getDetail();
      },
      fail: () => {
        // 错误,是否触发重新注册
      },
    });
  }
};

3.3 安卓跳转小程序出现一串代码问题

问题描述:点击【上传录音】按钮跳转小程序,出现一串代码。

导致原因:注入时机问题(页面可能渲染完成了,微信jssdk还没注入)。

解决办法:等注入成功后再请求接口。代码如上

4. 视频详情页访问埋点

背景:视频制作者分享视频出去,访客打开页面后,进行埋点。因为也要统计访客的访问时长,所以需在访客离开页面的时候调用访问接口。

js 复制代码
// 处理页面可见性变化的回调函数(ios存在兼容问题,点击X关闭按钮不触发visibilitychange)
// const handleVisibilityChange = async() => {
//     // console.log("document.visibilityState----", document.visibilityState)
//     if (document.visibilityState === 'hidden') {
//         // 页面不可见,执行离开页面的操作,比如调用埋点接口
//         await postVideoLog();
//         document.removeEventListener('visibilitychange', handleVisibilityChange);
//     }
// }


onBeforeUnmount(async() => {
//     // if(!isShare.value) return
//     // await postVideoLog();

//     // 解绑事件监听
//     // document.removeEventListener('visibilitychange', handleVisibilityChange);
//     lifecycle.removeEventListener('statechange', handleVisibilityChange);
  ViewRecordUtil.clearStillTime();
});

4.1 关于Page lifecycle

W3C 最新的规范 Page LifecyclePage Lifecycle API 试图通过以下方式解决性能瓶颈:

  1. 引入标准化的页面生命周期状态和概念;
  2. 定义新的、由系统发起的变更状态,允许浏览器在 Tab 隐藏或者不活跃时限制其占用的系统资源;
  3. 创建新的 API 和 Event 来让开发者捕获和响应由系统引起的状态变化;

对于老式浏览器可以使用谷歌开发的兼容库 PageLifecycle.js进行管理。page-lifecycle.js

一开始直接在onBeforeUnMount里面调用该方法,发现数据丢失严重,定位发现关闭页面根本不会走到onBeforeUnmount/onUnmount生命周期里面。于是换成使用navigator.sendBeacon,安装 npm install page-lifecycle

js 复制代码
import lifecycle from 'page-lifecycle';

const handleVisibilityChange = async (event) => {
  const { oldState, newState } = event;
  if (oldState == 'passive' && newState == 'hidden') { // 关闭页面时候触发
    ViewRecordUtil.clearStillTime();
    await postVideoLog();
    lifecycle.removeEventListener('statechange', handleVisibilityChange);
  }
};
onMounted(() => {
  // 【进入分享页】且【非本人】打开添加【statechange】事件
  if (isShare.value && !isMakerSelf.value) {
    // logLocalStorageVal('sendBeacon_state', {name: '非本人'});
    lifecycle.addEventListener('statechange', handleVisibilityChange);
  }
});

const navigatorSendBeacon = (params) => {
  const logsApi = `${requestDomin}/visit_logs`;
  //= ===测试=====
  // logsApi = requestDomin + '/api/visit_logs';
  const formData = new FormData();
  Tool.convertDataToFormData(params, formData);
  navigator.sendBeacon(logsApi, formData);
  // const headers = {
  //   type: 'application/json',
  // };
  // let beParams= filterParams(params);
  // // navigator.sendBeacon(logsApi, new URLSearchParams(beParams));
  // const blob = new Blob([JSON.stringify(beParams)], headers);
  // // logLocalStorageVal('sendBeacon_state', {name: '发送sendBeacon请求'});
  // navigator.sendBeacon(logsApi, blob);
};

/*
 ** 视频访问记录
 ** @isOpenPolling 是否调用轮询记录
 */
const postVideoLog = async (isOpenPolling) => {
  // 定义两个日期
  const date2 = moment();
  // 计算两个日期之间的秒数差
  const secondsDiff = date2.diff(date1, 'seconds');
  date1 = moment();
  try {
    // 轮询最长时间(到点清定时器): 时长 + 10分钟
    const pollingMaxTime = tmplData.value.duration + 10 * 60;
    let params = {
      dataType: 1, // 类型1:视频
      duration: secondsDiff > 0 ? secondsDiff : 1,
      pollingMaxTime,
      // ====测试====
      // pollingMaxTime: 10,
    };
    params = Object.assign(visitParams, params);
    if (isOpenPolling) {
      // 访问埋点
      params.startLoop = true;
      await ViewRecordUtil.reportVisit(params);
    } else {
      const postParams = cloneDeep(params);
      postParams.sync_content = {
        url: postParams.url,
        nickName: postParams.nickName,
        headPhoto: postParams.headPhoto,
      };
      delete postParams.url;
      delete postParams.nickName;
      delete postParams.headPhoto;
      delete postParams.pollingMaxTime;
      if (postParams.visitorId == 0) return;
      navigatorSendBeacon(postParams);
    }
  } catch (err) {
    toastError(err);
  }
};

问题一: 安卓左滑 & X 关闭,以及ios左滑关闭页面都能监听到页面状态改变,但是ios点击左上角X关闭监听不到页面状态变化。(解决方案:采用3秒轮询调用接口上报)。

问题二:上报的时长是3的倍数,为了使数据更准确,关闭页面仍然调用navigatorSendBeacon()函数。一开始接口参数都是字符串,传的是URLSearchParams类型的参数;接口调整后参数有对象,于是改成了Blob类型的参数,存在的问题是关闭页面接口会调用2次(一次是跨域options请求)导致上报失败,后面查阅得知FormData类型的参数不存在options跨域请求,于是换成FormData传参。

4.2 post请求FormData二级参数格式封装函数:

js 复制代码
// 递归函数,将多层结构的数据转换为 FormData 对象
function convertDataToFormData(data, formData, parentKey) {
  forEach(data, (value, key) => {
    const newKey = parentKey ? `${parentKey}[${key}]` : key;
    if (isObject(value)) {
      convertDataToFormData(value, formData, newKey);
    } else if (value !== '') {
      formData.append(newKey, value);
    }
  });
}

5. 实现多图片链接下载到本地的脚本

旧项目图片链接域名要改,首先要把所有的图片下载到本地后重新上传,手动一张张下载慢,写一个脚本根据收集的链接数组程序自动下载。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>下载图片</title>
</head>
<body>
    <button id="downloadBtn">下载图片</button>

    <script>
        // 图片链接数组
        var imageUrls = [
          'https://static.jiabaometa.com/hengqin_test/3/2024-02-29/1709178303197487.png',
          'https://static.jiabaometa.com/hengqin_test/3/2024-02-29/1709178853268432.png',
        ];

        // 下载图片函数
        function downloadImage(url, index) {
          var xhr = new XMLHttpRequest();
          xhr.open('GET', url, true);
          xhr.responseType = 'blob';

          xhr.onload = function() {
            var reader = new FileReader();
            reader.onload = function() {
              // 创建临时链接并下载图片
              var downloadLink = document.createElement('a');
              downloadLink.href = reader.result;
              downloadLink.download = 'image' + index + '.jpg';
              downloadLink.click();
            };
            reader.readAsDataURL(xhr.response);
          };

          xhr.send();
        }
        // 点击事件处理
        document.addEventListener('DOMContentLoaded', function() {
          document.getElementById('downloadBtn').addEventListener('click', function() {
            for (var i = 0; i < imageUrls.length; i++) {
              downloadImage(imageUrls[i], i + 1); // 传入图片链接和索引
            }
          });
        });
    </script>
</body>
</html>
相关推荐
天天进步201518 分钟前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
疯狂的沙粒38 分钟前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员1 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
疯狂的沙粒1 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪1 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背1 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M1 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc1 小时前
学习electron
javascript·学习·electron
想自律的露西西★2 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5
白墨阳2 小时前
vue3:瀑布流
前端·javascript·vue.js