vue每次发布线上传部分资源访问404!

问题分析

经过每次 run build 构建 -> 发布 -> 到线上环境阶段, 若未通知测试人员, 已发布最新内容, 或者 测试人员未及时刷新浏览器获取最新已构建的新静态资源时, 这个时候在去访问 已更新的模块内容,便会引起 静态资源 无法 加载,404 问题!

原因分析

该问题出现的原因,是因为操作着在发布新的内容前,已经处于系统界面状态,这时候的资源,已经被浏览器所缓存,每次访问加载资源时,便会通过缓存将页面呈现!
但是,当开发人员 每次 构建 到 发布新的内容时,浏览器不刷新,则认为,还是拿旧的资源去服务器里找,但这时,服务器里的资源已被更新成新的资源导致无法加载所引起的问题!

效果

问题处理

注意: 这里暂时针对前端模块进行问题处理,目前环境采用的 `vue2.x` `vue-cli3`

目前根据上述问题得知,只有在浏览器刷新后,才能重新获取新的静态资源!
所以,我们需要在每次打包发布后,通过在本地新建version.json来指定每次打包所更新的唯一版本! 最后通过 路由跳转 或者 定时器 的方式, 来获取version.json中 版本内容, 因此把 版本信息 保留在 localStorage 中 便 每次用来判断 是否有新的内容更新,若有,便通过提示信息告知用户有新的内容更新,随后通过location.reload() 重新加载页面获取新的浏览资源!

新建version.json

public/version.json

1
2
3
4
5
{
"name": "project-name",
"versionNo": "1.0.0",
"updateTime": "date-time"
}

新建updateVersion.js

此脚本作用,用来每次打包 将新的 版本 写入 public/version.json 文件当中!

src/script/version.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fs = require('fs');
const path = require('path');
const filePath = path.join( __dirname, '../../public/version.json' );

// 处理版本号
async function upgradedVersion() {
const d = new Date()
let result = await fs.readFileSync(filePath,{ encoding:"utf-8" })
if ( !result ) return
let version = JSON.parse(result)
version.updateTime = d.toLocaleString()
version.version = ['getFullYear', 'getMonth', 'getDate', 'getHours', 'getTime'].map(x => {
if (d[x]) return x === 'getMonth' ? d[x]() +1 : d[x]()
return ''
}).join('.')
version.buildCount++
console.log('version', version)
fs.writeFile(filePath, JSON.stringify(version,null, 2), () => {
console.log('新版本号生成成功');
});
}

upgradedVersion()

修改package.json run build 选项

1
2
3
4
5
6
7
"scripts": {
"serve": "vue-cli-service serve",
"dev": "vue-cli-service serve --mode development",
"test": "vue-cli-service serve --mode test",
"prod": "vue-cli-service serve --mode prod",
"build": "node ./src/script/updateVersion.js && vue-cli-service build --mode prod",
},

新建 utils/routerUtils.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import defaultSettings from '@/settings'
import request from '@/utils/request'
import { MessageBox } from 'element-ui';

const url = `${location.origin}/version.json`
function messageAlert(){
MessageBox.confirm('新版本已更新,请刷新页面后在进行相关业务操作,给您带来不便,尽情谅解!', '提示', {
confirmButtonText: '刷新',
showClose: false,
showCancelButton: false,
closeOnClickModal: false,
closeOnPressEscape: false,
closeOnHashChange: false,
type: 'warning'
}).then(() => {
location.reload()
})
}

export async function getNewVersion() {
if ( process.env.NODE_ENV === 'production') {
let localStorage = window.localStorage
await request.get(url).then((response) =>{
console.log(response)
let localVersion = localStorage.getItem('version')
let lastVersion = response
let { version: newVersion, buildCount } = lastVersion
if ( !localVersion ) {
localStorage.setItem('version', JSON.stringify(lastVersion))
if ( buildCount === 1 ) {
messageAlert()
}
} else {
let { version: oldVersion } = JSON.parse(localVersion)
console.log('version', localVersion, oldVersion, newVersion)
if ( oldVersion !== newVersion ) {
localStorage.removeItem("version")
messageAlert()
}
}
})
}
}

注意:

const url = ${location.origin}/version.json 这里的 requset 请求的url 存在问题!

最终 发布到线上的时候,通过 以上 路径 访问,会出现本地磁盘缓存问题,这样就会导致,每次请求都是旧的数据,无法拿到新的版本,进而就无法更新版本对比了!

磁盘缓存问题处理:

这里我们给url加上时间戳,保持每次请求都是新的,以便浏览器不在缓存此资源!

1
2
3
4
5

request.get(`${location.origin}/version.json?timestamp=${Date.now()}`)

// version.json?timestamp=1678259895529

permission.js 路由拦截中添加, 或者通过定时器加载!

1
2
3
4

let timer = null;
timer = setInterval(getNewVersion(), 2000);

注意: 使用定时器,一定要注意在合理条件内 清楚,避免内存浪费! 比如浏览器关闭的时候....

监听浏览器右上角关闭

需求 需要 关闭浏览器之前 做一次 版本清除 和 用户注销操作!

App.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
export default{
data(){
return {
beforeunloadTime: 0,
unloadTime: 0
}
},
methods: {
beforeunloadHandler(e) {
let userAgent = navigator.userAgent;
let is_fireFox = userAgent.indexOf("Firefox") > -1;
this.beforeunloadTime = new Date().getTime()
// 不知道怎么判断 是刷新 还是 关闭
if ( is_fireFox && e.srcDocument ) {
let { token, loginId } = this.userInfo
let API_URI = `${process.env.VUE_APP_BASE_API}/sso/redirect?type=logout&token=${token}&loginId=${loginId || null}`
// 避免使用异步请求 防止浏览器关闭后 导致请求无法发送成功!
navigator.sendBeacon(API_URI)
clearVersion()
console.log('unload close window!')
}
},
unload() {
let userAgent = navigator.userAgent;
let is_fireFox = userAgent.indexOf("Firefox") > -1;
this.unloadTime = new Date().getTime()
// window.localStorage.setItem('timer', String(this.unloadTime - this.beforeunloadTime))
// 本地通过localStorage中的数据看出,关闭事件间隔小于1,刷新则大于8
let _gap_time = this.unloadTime - this.beforeunloadTime
if ( _gap_time < 5 && !is_fireFox) {
let { token, loginId } = this.userInfo
let API_URI = `${process.env.VUE_APP_BASE_API}/sso/redirect?type=logout&token=${token}&loginId=${loginId || null}`
// 避免使用异步请求 防止浏览器关闭后 导致请求无法发送成功!
navigator.sendBeacon(API_URI)
clearVersion()
console.log('unload close window!')
}
}
},
mounted() {
// 刷新 或 关闭前 做一些操作
window.addEventListener('beforeunload', e => this.beforeunloadHandler(e));
// 完全卸载文档
window.addEventListener('unload', e => this.unload(e));
},
beforeDestroy() {
window.removeEventListener('beforeunload', e => this.beforeunloadHandler(e));
window.removeEventListener('unload', e => this.unload(e));
},
}
注意 火狐浏览器,不大友好!:

火狐浏览器 刷新关闭 会执行 beforeunload
火狐浏览器 刷新 会执行 unload 因此 暂且无法判断到底是 刷新 还是 关闭操作
还有一点 关闭之前打断点时,浏览器会顿以下,然后就会关掉,这个很头疼!
谷歌浏览器 会走 debugger 其次 手段关闭 debugger 模式后 才会关闭窗口!

注意 谷歌:

谷歌浏览器 刷新关闭 会执行 beforeunload
谷歌浏览器 关闭 会执行 unload
因此-> 在beforeunload 内 初始化事件戳,待执行完后, 在unload内部 在创建一个新的时间戳,进行对比,来判断是否关闭或刷新!

JavaScript相关问题

JSON字符串对长整型字段精度丢失的问题

处理方案

替换JSON串解析后 长整型 精度丢失

1
2
3
4
5
6
7
8
// 替换JSON串解析后 长整型 精度丢失
export const replaceJsonLongTranStr = ( jsonStr = '' ) => {
return jsonStr.replace(/"\w+":\s*\d{16,}/g, function(longVal){
let split = longVal.split(":");
// console.log('长整型数字:', longVal)
return split[0] + ':' + '"' + split[1].trim() + '"';
})
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
let jsonStr = '[
{
id: 9218932178937819273,
name: 'wdhuais'
},
{
id: 9218932178937812343,
name: 'jwiqjid'
}
]';
let newJsonStr = replaceJsonLongTranStr(jsonStr);
let parseJson = JSON.parse(newJsonStr)

ElementUi相关问题

el-row 和 el-col 导致布局错乱问题

1
2
3
4
.el-row{
display: flex;
flex-wrap: wrap; // 自动换行
}

el-table toggleRowSelection无效

需求描述

带有复选框并可支持多选的表格,关键:在选择表格中的数据并保存至后端时,下回看详情或者修改的时候要把上次保存的数据默认勾选至表格!
因此便考虑使用官方API toggleRowSelection来处理此需求!

有关toggleRowSelection此方法描述

用于多选表格,切换某一行的选中状态,如果使用了第二个参数,则是设置这一行选中与否(selected 为 true 则选中)

参数描述

row: 表格中选中的那一行数据 Object
selected: 是否被勾选 Boolean

实现思路

将保存至后端的数据通过详情接口反查得到对应数据后,对原表格列表进行唯一字段过滤[filter]得到对应的 row 当作参数传递至该toggleRowSelection方法中!

发现问题!

按照上述思路处理,并未达到预期勾选效果,经排查,原因时,最终过滤出来的row必须是 原 table 中 所绑定的 list 列表中的数据!

1
2
3
<el-table :data="sourceTableData">
<el-column></el-column>
</el-table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let saved = [
{id: '734234323', name: '测试2'},
{id: '431231231', name: '测试4'}
]
let sourceTableData = [
{id: '512421123', name: '测试1'},
{id: '734234323', name: '测试2'},
{id: '763421312', name: '测试3'},
{id: '431231231', name: '测试4'}
]
// 便利保存过的数据列表
saved.map(item => {
// 通过唯一字段 在 tableData 所绑定在原组件 el-table 中的数据 过滤所得到的 row 即可!
let row = sourceTableData.find(i => i.id === item.id)
this.$refs['table'].toggleRowSelection(row, true)
})

this.$msgBox内部使用自定义h函数无法双向绑定问题

问题描述

使用 elementui 中 this.$msgBox() 提示框函数,内部自定义 render h 函数,无法双向绑定问题!

错误案例

以下代码 无法实现 双向绑定,输入的内容 通过 input 事件 依然不能 将 新 的内容 绑定至 testMsg属性上!

1
<el-button @click="testMsg">testMsg</el-button>

1
2
3
4
5
6
7
data(){
return {
formData: {
testMsg: ''
},
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
testMsg(event){
const h = this.$createElement;
// h 函数 创建 一个 input textarea 文本框!
const tag = h('el-input', {
// 此处为 属性绑定
props: {
type: 'textarea',
placeholder: "请输入内容",
// 将绑定的值 关联到 input value
value: this.formData.testMsg
},
// 此处为 事件绑定
on: {
// 当输入内容时,在将新的内容 绑定至 testMsg 属性上
'input': (value) =>{
this.formData.testMsg = value
}
}
}, [])

this.$msgbox({
title: 'testMsg',
message: tag,
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消',
beforeClose: ( action, instance, done ) => {
console.log('action instance done', action, instance, done)
done();
}
})
},

处理方案

直接修改原组件所绑定的value值 会导致 vue 发出错误提示, 通过 该配置 Vue.config.silent 可以取消所有的日志与警告!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
testMsg(event){
const h = this.$createElement;
// tag tag.componentInstance 原组件实例
const tag = h('el-input', {
props: {
type: 'textarea',
placeholder: "请输入内容",
value: this.formData.testMsg
},
on: {
'input': (value) =>{
// Vue.config.silent 取消 Vue 所有的日志与警告
const bakSilent = Vue.config.silent = true
// 直接修改原组件实例所绑定的value值
tag.componentInstance.value = value

this.formData.testMsg = value

Vue.config.silent = bakSilent
}
}
}, [])
console.log('event', event)
this.$msgbox({
title: 'testMsg',
message: tag,
showCancelButton: true,
confirmButtonText: '确认',
cancelButtonText: '取消',
beforeClose: ( action, instance, done ) => {
console.log('action instance done', action, instance, done)
done();
}
})
},

el-checkbox-group 下使用文本时 不显示问题

如图所示,在使用ElementUi el-checkbox-group 多选组 组件时 所遇到的问题,内部使用文本即不显示的情况!

原因,是因为 el-checkbox-group组件 内置样式 设置了 front-size: 0,解决方法,将文本元素重新设置下大小即可!

image

ElementUI表格中图片懒加载无法显示的问题

ElementUI表格el-table-column中el-image图片懒加载无法显示的问题

是父元素同时设置了overflow: auto; 导致Element不能找到正确的container了

添加 表格的滚动scroll-container=".el-table__body-wrapper" 成功解决

注意: 注意不要在`el-image`中开`preview-src-list` 否则懒加载会失效(实际图片已全部请求)如要开启放大预览要用`el-image-viewer`组件!

项目优化记录

el-select 加载万条数据引起界面渲染卡顿

接口返回上万条数据,引起el-select渲染Dom条数导致页面加载缓慢卡顿情况!

处理方法

  1. Object.freeze冻结对象
    vue中, data对象里的属性都是响应式(getter, setter)的,vue会循环遍历data中的属性并通过defineProperty来为每个属性增加gettersetter!
    所以,当数量庞大的数组或者对象,我们可以对他们的属性进行冻结操作(Object.freeze),vue中会通过Object.isFrozen(obj);来判断此对象是否被冻结,被冻结的数据是不会被处理的!
  2. 对数据进行分片处理
    数据量庞大,对频繁大量的创建Dom操作会对页面渲染造成界面渲染卡顿,因此,我们,需要将渲染的数量进行分割处理,保证每次渲染的数量不会影响到界面加载速度!

    1. 虚拟滚动
      虚拟滚动意思就是在固定可视范围内通过position定位形式来下拉展示不同

      插件使用
      查看vue-virtual-scroll-list教程

      1
      npm install vue-virtual-scroll-list --save
    2. 分页加载
      考虑后端返回的数据没有带分页情况: 分页需要前端进行处理, 通过slice 并监听scroll事件,当scroll到底部的时候,给个截取范围`beginIndex endIndex返回截取后的数据并push到一个新的数组里 optionsel-select去加载!
      代码片段
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      methods: {
      // 监听 scroll 滚动到底部时触发
      loadMore(){
      // all options 大量数据集合
      let selectOptions = this.selectOptions.options
      // 筛选时下拉不触发分页
      if ( selectOptions.length > 150 ) {
      if ( !firstTag ) {
      // 滚动页数
      this.queryParams.page++
      }
      // 分页开始坐标 limit 每一页显示的数量
      const begin = this.queryParams.limit * (this.queryParams.page - 1)
      // 分页结束坐标
      const end = (this.queryParams.limit * (this.queryParams.page - 1)) + this.queryParams.limit
      selectOptions.slice(begin, end).map(item => this.options.push(item))
      // this.options 为处理后得到的新数组,给 el-select 去下拉加载
      this.options = unique(this.options, this.selectOptions.value)
      return
      }
      }
      }

待优化项

图片附件预览download问题

  1. 平时回显预览图片所做的

    平时在开发过程中在预览图片时,会先通过后端返回的fileId去下载相应的照片附件(ArrayBuffer),然后将ArrayBuffer类型的图片附件在转换为base64形式当作urlel-image or el-image-preview or el-upload 各个组件回显图片附件便可做预览展示!

  2. 然而附件如果是图片时,且无需download后在转换base64供页面回显!

    完全可以通过地址加http://download/file?id=fileId参数供 urlel-image or el-image-preview or el-upload 各个组件去直接展示! 这对于一个页面需要展示很多图片附件时很有效果,且完全没有必要download by fileId 后在转换base64形式去回显图片!

  3. 说下为什么这么做

    download 图片的时侯需要耗费资源,如果一个页面有7、8张图片附件甚至更多的时候,我们需要去循环遍历拿到 fileIddownload获取,难免我们需要 将异步请求改为同步请求,带数据正常返回后,在进行数据回显,如果图片附件资源较大这意味着页面,将会消耗更多时间来获取对应资源,并阻塞页面渲染!

    当然,附件资源比较大的情况下,对本地内存也有一定的影响,避免引起内存消耗,无法进行回收问题!

这是多张

这是一张大图

代码参考

template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<!--图片懒加载-->
<div class="lazy-image">
<collapse headerTitle="elUpload图片预览懒加载" style="height: 400px; overflow-y: scroll">
<div style="height: 850px; border: 1px solid #b3d8ff"></div>
<el-upload
class="upload-demo"
ref="upload"
list-type="picture-card"
action="https://jsonplaceholder.typicode.com/posts/"
v-lazy-image="fileList"
:auto-upload="true">
<!-- <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>-->
</el-upload>
</collapse>
<collapse headerTitle="elTable图片懒加载">
<pagination-table
ref="paginationTableRef"
:columns="columns"
:data="tableData"
height="280"
></pagination-table>
</collapse>
</div>
</template>

script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
data(){
return{
// 作为 el-uppload 使用
fileList: [{name: 'food.jpg', url: 'http://ip:port/oss/download?id=10db8d58-7c3e-4794-bfe4-5d95b79e920d'}],
columns: [
{ image: true, fileSrcField: 'imgSrc' }
],
// 作为 el-image 使用
tableData: [
{ imgSrc: 'http://ip:port/oss/download?id=10db8d58-7c3e-4794-bfe4-5d95b79e920d' },
{ imgSrc: 'http://ip:port/oss/download?id=7dfb2d39-7546-484d-999d-1f6fd9126f84' },
{ imgSrc: 'http://ip:port/oss/download?id=bb4b11ea-4c35-48b9-96d9-494e5a0ddcc4' },
{ imgSrc: 'http://ip:port/oss/download?id=6ae9aa77-87eb-4f55-b355-7d2b836f00f2' },
{ imgSrc: 'http://ip:port/oss/download?id=50b5a9c6-1954-41b2-9177-6f80cb1e2aff' },
]
}
}

处理后

结果看起来没什么不一样

  1. 处理前: 图片附件是在渲染整体页面前加载
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    getImg(fileId){
    return new Promise(async (resolve, reject) => {
    let data = await this.$axios.commonExportFile(fileId)
    if(data.data.byteLength > 0){
    let img = `data:image/jpeg;base64,${arrayBufferToBase64(data.data)}`
    resolve(img)
    }else{
    resolve('')
    }
    });
    },
  2. 处理后: 放在运行时处理的, 也就是说,这种情况不会影响到整体页面渲染加载,只会涉及到图片本身加载!

    图片在第一次请求加载完后,就不会在请求第二遍了!

    1
    <el-image src="http://ip:port/your/download?id=fb78fb33-969d-4310-954c-8d2fa882b4d1"></el-image>
    当然,后面会结合 图片懒加载 及各种场景下进行下一步优化!

FormPaginationTable 组件优化

  1. 组件描述

    FormPaginationTable 这个组件时平时开发中经常用到的组件之一,该组件可以帮我们完成一个二次封装后的可编辑表格式组件,支持下拉、输入、日期、tree常用编辑属性配置!

  2. 为何优化
    1. FormPaginationTable 组件,平时使用的时候,可能多列情况下,支持可编辑字段,多个列可支持下拉 or 输入框 or 日期等编辑组件,因此,当我们,一旦给这些字段配置可编辑后,el-table组件中,就会持久渲染更多可编辑表单组件!
    2. 其次,就是多列中可编辑下拉的组件,该组件,不仅在平时新增操作,或者修改以及详情下,都会请求大量的字典数据,去支持下拉,以及回显所保存过的字段值,过滤等,会比较消耗资源和内存空间!
  3. 优化方案

    这里优化需要结合数据字典项优化来进行处理,避免 多个下拉组件 在获取数据字典项时,根据`code`获取对应字典,引起资源耗费!

    1. 需要编辑的时候,才需要显示编辑组件,编辑完后,在把需要可编辑的组件替换成文本组件!
    2. el-select、tree-select特殊组件,需要单独处理,因为我们需要做,数据字典 以及 组织机构等可能需要异步处理来获取的数据,这里才是比较复杂的一块儿!
  4. 看下效果

    1. EditTable 可以按需使用,当需要选择编辑的时候,才会显示编辑组件,否则只会显示文本!
    2. el-select 下拉组件,支持异步加载,和 自定义下拉内容 options,当我点击的时候才会去加载options内容,不需要每次新建 修改 详情的时候做一次性的初始化操作!

el-tab 组件按需加载

注意: 该方案只适用于: tab 栏各个组件以及父组件之间没有可交互依赖关系的!

例如: 信息修改: 父组件有个save按钮,该save按钮,保存前或获取子组件中的param,以及会校验子组件中的表单项,这种情况,不适用!
在了解优化方案前的先了解下这个属性:is属性,该属性值需要传入组件的name属性,可以通过name属性来帮我们动态渲染组件!

图片懒加载

v-lazy-image 指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const lazyImage = {
// 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
intersectionObserver: null,
bind( el, binding, vNode ){
// console.log('initData', initData)
},
// 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
inserted(el, binding, vNode) {
// console.log('inserted', el, binding, vNode)
lazyImage.intersectionObserver = new IntersectionObserver((entries, observer) => {
// 如果 intersectionRatio 为 0,则目标在视野外,
// 视野外
let isInWindow = entries[0].intersectionRatio === 1
if ( isInWindow ) {
let { componentInstance } = vNode
let fileList = binding.value
// el-upload
if ( componentInstance?.uploadFiles && !componentInstance?.uploadFiles.length ) {
componentInstance.uploadFiles = fileList.map(item => {
return { ...item, uid: Date.now() }
})
return
}
// el-image
if ( componentInstance && !componentInstance.src){
componentInstance.src = fileList[0]
}
}
}, { threshold: 1.0 });
// threshold 规定可视元素到隐藏之间的一个范围 显示为 1 不显示 为小于1
lazyImage.intersectionObserver.observe(el)
},
unbind(el){
// console.log('unbind', lazyImage.intersectionObserver)
lazyImage.intersectionObserver.unobserve(el)
lazyImage.intersectionObserver.disconnect()
}
}
export default lazyImage

bq-image component

el-image 中的 src prop属性是不可以直接被修改的,为了避免此问题, 单独封装了el-image组件!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<el-image v-bind="$attrs" v-on="$listeners" :src="src"></el-image>
</template>
<script>
export default {
name: "BqImage",
data() {
return {
src: ''
}
},
}
</script>

<style scoped>

</style>

vue中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<template>
<!--图片懒加载-->
<div class="lazy-image">

<collapse headerTitle="elUpload图片预览懒加载" style="height: 400px; overflow-y: scroll">
<div style="height: 850px; border: 1px solid #b3d8ff"></div>
<el-upload
class="upload-demo"
ref="upload"
list-type="picture-card"
action="https://jsonplaceholder.typicode.com/posts/"
v-lazy-image="fileList"
:auto-upload="true">
<!-- <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
<el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">上传到服务器</el-button>
<div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>-->
</el-upload>
</collapse>

<collapse headerTitle="elTable图片懒加载">
<pagination-table
ref="paginationTableRef"
:columns="columns"
:data="tableData"
height="280"
></pagination-table>
</collapse>
</div>
</template>

<script>
export default {
name: "index",
data(){
return{
fileList: [{name: 'food.jpg', url: 'http://ip:port/oss/download?id=a46173b4-18a2-416c-b4c8-86552f57019b'}],
columns: [
{ image: true, fileSrcField: 'imgSrc' }
],
tableData: [
{ imgSrc: 'https://img2.baidu.com/it/u=3618236253,1028428296&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
{ imgSrc: 'https://img2.baidu.com/it/u=2421090168,324781765&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
{ imgSrc: 'https://img0.baidu.com/it/u=4060770951,4069855872&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
{ imgSrc: 'https://img2.baidu.com/it/u=1579991524,66947472&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
{ imgSrc: 'https://img1.baidu.com/it/u=2136061792,3055265331&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
{ imgSrc: 'https://img0.baidu.com/it/u=178849715,820264547&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
{ imgSrc: 'https://img0.baidu.com/it/u=2571430731,2285101823&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500' },
{ imgSrc: 'https://img1.baidu.com/it/u=22553051,3834724655&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
{ imgSrc: 'https://img1.baidu.com/it/u=1856720522,683752130&fm=253&fmt=auto&app=138&f=JPEG?w=529&h=500' },
{ imgSrc: 'https://img0.baidu.com/it/u=1577303935,3801284994&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' },
/*{ imgSrc: 'http://ip:port/oss/download?id=41d68691-22a2-4505-8c2a-8a15d3772ab3' },
{ imgSrc: 'http://ip:port/oss/download?id=86871049-696e-44ec-9323-b7a240c02146' },
{ imgSrc: 'http://ip:port/oss/download?id=70ac0163-4059-42bb-9fff-4bce241ad20b' },*/
]
}
}
}
</script>

<style scoped>

</style>

数据字典项优化

数据字典这个问题比较头疼,这个问题从开始,一直延申至今,只要有el-select的地方,就难免逃不掉字典的魔爪!

  1. 历史接口只有通过字典 code 来获取对应字典数据,所谓的就是一个 select 对应一个 字典 code,则多个 select 就需要多个 code!这避免不了一个页面会出现七八个 select,一打开一个菜单界面,就会init下七八个字典数据,甚至更多!
  2. 来瞅瞅这个可怕阵容
  3. 做点人事儿吧
    1. 后端来了一位救世主,给前端提供了一个查全量字典的接口,数据给我们了,剩下的难道不就想怎么玩就怎么玩了不是吗!
    2. 全量字典是通用的,那我们就需要把字典放到全局作用域下, ...enmmm vuex,需要的时候在拿来玩吧!
    3. 这时候filter出场了,我们把需要的code摘出来,通过filter来过滤,获取对应的options即刻!

      这样,一次收获,便可到处使用了,不用在每次初始化,页面来请求很多接口获取对应字典了!

注意:

可以完美结合 bq-form edit-table 两个组件,来快速配置字典数据,可生成表单 以及 可编辑表格!

bq-form: 内置,会根据JSON formConfigItems 配置项 列入字典 code,组件内部在渲染表单的时候,可同时根据配置项 code全局字典中获取对应options 一并设置到 select!
bq-table: 内置,会根据JSON columns 配置项列入字典 code 同上,一样会处理拿到对应options并设置给select组件!

前端获取ip

最近iot项目中有需求,需要通过在前端获取到用户的ip地址, 然后登录调用接口的时候顺便把ip地址信息传入后端!
当然,确实没做过这方面的需求,也没啥思路头绪,只能投靠百度来寻找答案emm….
因为我们期望获取到的ip 是用户的内网ip,所以因此百度给的答案大部分都是一致的!

通过三方 api获取ip

  1. http://api.ipify.org?format=json

    当然,这显然不是我们所需要的ip地址信息

  2. 使用搜狐api
    http://pv.sohu.com/cityjson?ie=utf-8

    本地调用接口返回结果你会发现
    var returnCitySN = {"cip": "127.0.0.1", "cid": "00", "cname": "未知"};

最后通过三方来获取api显示是不满足需求的!

使用js内置rtc方法获取ip

使用此方法需要做兼容处理,这个方法使用的时候还需要通过浏览器内置配置rtc后,便可获取内网ip!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getIPAddress() {
// 判断浏览器是否支持WebRTC协议
if (window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection) {
let pc = new RTCPeerConnection({
iceServers: []
});
// 创建数据通道
pc.createDataChannel('');
pc.createOffer(function (offerDesc) {
pc.setLocalDescription(offerDesc);
}, function (e) { });
pc.onicecandidate = function (evt) {
console.log("evt", evt)
if (evt.candidate)
ip = evt.candidate.address;
else
ip = "no IP found";
console.log(ip);
pc.close();
};
} else {
ip = "WebRTC is not supported by your browser!";
console.log(ip);
}
}

getIPAddress();

在浏览器环境下运行,你会发现

注意: 以下结果 在浏览器控制台输入返回的结果!


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function getIPAddress() {
// 判断浏览器是否支持WebRTC协议
if (window.RTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection) {
let pc = new RTCPeerConnection({
iceServers: []
});
pc.createDataChannel('');
pc.createOffer(function (offerDesc) {
pc.setLocalDescription(offerDesc);
}, function (e) { });
pc.onicecandidate = function (evt) {
console.log("evt", evt)
if (evt.candidate)
ip = evt.candidate.address;
else
ip = "no IP found";
console.log(ip);
pc.close();
};
} else {
ip = "WebRTC is not supported by your browser!";
console.log(ip);
}
}
getIPAddress()
undefined
VM561:12 evt RTCPeerConnectionIceEvent {isTrusted: true, candidate: RTCIceCandidate, type: 'icecandidate', target: RTCPeerConnection, currentTarget: RTCPeerConnection, …}
VM561:17 e0b7289e-9525-405f-8c06-9cc6523f870d.local

以上内容结果返回undefined,当然为什么会返回undefined呢,因为在浏览器中以被限制~
·某些网络和浏览器可能会出于隐私和安全原因限制WebRTC功能。

通过以下方法来配置`浏览器`的限制 !!

  1. chrome 浏览器地址栏中输入 chrome://flags/
  2. edge 浏览器地址栏中输入edge://flags/

搜索 #enable-webrtc-hide-local-ips-with-mdns 该配置 并将属性改为 disabled 然后点击Return重新加载浏览器就可以了!

此方法虽然可以获取到ip,出于浏览器的兼容性,以及浏览器隐私的考虑,我们不建议使用!

使用nodejs获取ip

最后尝试通过nodejs服务获取ip

nodejs 环境下获取ip地址,需要安装npmexpress or koa!

  1. 初始化node项目
    1
    npm init
  2. 安装express依赖
    1
    npm install express # or koa
  3. 创建server/index.js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const express = require('express');
    const app = express();
    const port = 3000;

    app.get('/', (req, res) => {
    res.json({ ip: req.ip })
    });

    app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
    }
  4. 启动服务
    1
    node server/index.js
  5. 访问服务

    放在 浏览器url直接访问即可!

    1
    http://localhost:3000

注意: 启动服务后,若获取用户ip需要直接访问,不可以使用代理或者其他方式转发此来访问该服务,否则获取到的ip这是代理转发的ip地址,而并不是用户的ip!

1
2
3
4
5
6
// 20231125133903
// http://192.168.31.111:3000/

{
"ip": "192.168.31.111"
}

注意: 为了演示,此ip为虚假ip!

注意: 在测试访问服务时不要使用 http://localhost:3000 去访问,否则会返回以下结果!
1
2
3
4
5
6
// 20231125134317
// http://localhost:3000/getUserIp

{
"ip": "1"
}

最后在服务器上安装node环境并将node服务部署到服务器上即可!

html导出word问题

在使用第三方库html-docx-jshtml里的报表导出到word文档中时发现问题,就是导出结果里的表格,出现了两次展示的问题,如下图所示:

本地环境vue2 + elementui,导出界面为el-table相关报表!

问题处理

后来排查问题发现el-tableel-table-column有一个fiexd属性,该属性是用来固定列的,因此查看f12元素后,发现会下方多一个.el-table_fiexd如下图所示:

1
<el-table-column prop="date" label="日期" width="180" fixed></el-table-column>

.el-table__fiexd 元素下面 会有一个原.el-table复制版! 因此我们通过第三方插件进行导出时,出现重复的问题!
这里只需要把fiexd属性去掉后,就可以解决问题了!

具体代码

安装插件

1
npm install html-docx-js file-saver -S

in-table组件

table
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<template>
<div class="in-table">
<el-table :data="data" :height="height" border>
<el-table-column
v-if="showIndex"
label="序号"
width="70"
align="center"
>
<template slot-scope="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>

<!-- 多列表头 -->
<!-- <template v-if="isMultiHeader">
<template v-for="(column, index) in columns">
<el-table-column
v-if="column.children"
:label="column.label"
:key="`${column.label}_${index}`"
>
<el-table-column
v-for="(child, index) in column.children"
:label="child.label"
:prop="child.prop"
:key="`${child.label}_${index}`"
>
<template v-if="child.children">
<el-table-column
v-for="(child2, index2) in child.children"
:label="child2.label"
:prop="child2.prop"
:key="`${child2.label}_${index2}`"
>
<template slot-scope="scope">
<span :style="child2.cellStyle || {}">
{{ scope.row[child2.prop] }}
</span>
</template>
</el-table-column>
</template>

<template slot-scope="scope">
<span :style="child.cellStyle || {}">
{{ scope.row[child.prop] }}
</span>
</template>
</el-table-column>
</el-table-column>
<el-table-column
v-else
:key="`${column.label}_${Date.now()}`"
:label="column.label"
:prop="column.prop"
/>
</template>
</template> -->
<template>
<el-table-column
v-for="(column, index) in columns"
:key="`${column.prop}-${index}`"
:prop="column.prop || ''"
:label="column.label"
:align="column.align || 'center'"
:width="column.width || 'auto'"
:min-width="column.minWidth || 150"
header-align="center"
:show-overflow-tooltip="!column.editable"
>
<template slot-scope="scope">
<!-- 自定义颜色 -->
<template v-if="column.cellStyle">
<span :style="column.cellStyle">
{{ scope.row[column.prop] }}
</span>
</template>

<template v-else>{{ scope.row[column.prop] }}</template>
</template>
</el-table-column>
</template>
</el-table>
</div>
</template>

<script>
export default {
name: "InTable",
props: {
// 是否显示序号列
showIndex: Boolean,
data: {
type: Array,
default: () => [],
},
height: {
type: [String, Number],
default: 600,
},
// 是否多级表头
isMultiHeader: {
type: Boolean,
default: false,
},
columns: {
type: Array,
default: () => [],
},
},
data() {
return {};
},
};
</script>

<style lang="scss" scoped>
</style>

导入插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { saveAs } from "file-saver";
import htmlDocx from "html-docx-js/dist/html-docx";

export default {
name: "index",
methods: {
exportWord() {
let contentHTML = document.querySelector("#report-table").innerHTML;
let htmlString = `<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Document</title>
<style>
body {
font-family: "微软雅黑";
font-size: 12px;
padding: 25px;
}
table{
border-radius: 5px;
}
table, table th, table tr, table td {
border: 1px solid #aecce8;
}
</style>
</head>
<body>
${contentHTML}
</body>
</html>`;
const converted = htmlDocx.asBlob(htmlString);
saveAs(converted, `销售报表.docx`);
}
}
}