背景
和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。使用方法如下:
- 安装husky:
npm install --save-dev husky
; - 初始化:
npx husky init
:可以看到在在我们项目的根目录出现了一个.husky
文件夹,.husky/
目录下新增了一个名为pre-commit
的shell脚本。也就是说在在执行git commit
命令时会先执行 pre-commit 这个脚本。 - 修改.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 使用方法
官方文档,步骤:
-
绑定域名
登录微信公众平台进入"公众号设置"的"功能设置"里填写"JS接口安全域名"。
-
引入js文件
在需要调用JS接口的页面引入如下JS文件:res.wx.qq.com/open/js/jwe... (支持https)
如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:res2.wx.qq.com/open/js/jwe... (支持https))
-
通过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']
});
- 通过ready接口处理成功验证
js
wx.ready(function () {
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中
});
- 使用开放标签 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 Lifecycle。Page Lifecycle API
试图通过以下方式解决性能瓶颈:
- 引入标准化的页面生命周期状态和概念;
- 定义新的、由系统发起的变更状态,允许浏览器在 Tab 隐藏或者不活跃时限制其占用的系统资源;
- 创建新的 API 和 Event 来让开发者捕获和响应由系统引起的状态变化;
对于老式浏览器可以使用谷歌开发的兼容库 PageLifecycle.js进行管理。page-lifecycle.js:
4.2 navigator.sendBeacon埋点问题
一开始直接在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>