业务
1、非对称加密
1) 对称加密(其中一种是AES算法)
1)、对称加密(AES算法)使用crypto-js依赖:在加密和解密时使用的是同一个秘钥;
import RSAUtil from '@/utils/RSAUtil.js'
//解密.
function decrypt(data) {
if(data == null || '' == data || "" == data){
return data;
}
var decryptData = null;
try{
var hexKey = CryptoJS.enc.Hex.parse(RSAUtil.decrypt(sessionStorage.getItem('ptk')))
var dec = CryptoJS.AES.decrypt(CryptoJS.format.Hex.parse(data), hexKey, {
//iv:iv,
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
decryptData = CryptoJS.enc.Utf8.stringify(dec);
}catch(e){
console.log('解密失败:',e);
}
return decryptData;
}
//加密
function encrypt(data) {
if(data == null || '' == data || "" == data){
return data;
}
var hexKey = CryptoJS.enc.Hex.parse(RSAUtil.decrypt(sessionStorage.getItem('ptk')))
var enc = CryptoJS.AES.encrypt(data, hexKey, {
//iv:iv,
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
})
var encryptData = enc.ciphertext.toString()
return encryptData;
}
export default {
encrypt,
decrypt
}
2)非对称加密(其中一种是RSA算法)
2)、非对称加密(其中一种是RSA算法)使用jsencrypt:与对称加密算法不同的是,非对称加密算法需要两个不同的密钥(公钥和私钥)。公钥和私钥是一对,用公钥加密的数据,只有用对应的私钥才能解密。做法就是 new 一个jsencrypt对象,然后加密方法setpublickey,解密方法setpriviceKey.
使用jsencrypt实现RSA非对称加解密
>1、npm install jsencrypt
>2、新建一个文件jsencrypt.js
import { JSEncrypt } from 'jsencrypt'
// 或者下面这种方式引入,最好不要直接引入,而是引入bin文件夹下的jsencrypt
// import JSEncrypt from 'jsencrypt/bin/jsencrypt'
实现—加密
/**
* RSA非对称加密
* @param data 需要加密的数据
*/
export function rsaEncrypt(data) {
// 公钥,,复制前面生成的公钥
const PUBLIC_KEY = `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLCf1tQoxpXg6FQPZnCmUpZLvN
IjXRjtFmsQ8LEarKnamjnwAhDnHOpz4pCWXHFcox40FhJQzUmyAzKgIluALvbjYl
HamB1IY+XBeEsR/h2BiRufwSWj1YV4r1rIbhd5p+ymqEZcQeWpcY4ueke+k98RZ0
+VBOuf4DGTdrb8L/PwIDAQAB`
// 使用公钥加密
const encrypt = new JSEncrypt()
encrypt.setPublicKey(PUBLIC_KEY)
let result = encrypt.encrypt(data)
return result
}
实现—解密
/**
* RSA 解密
* @param data 需要解密的数据
*/
export function rsaDecrypt(data) {
//私钥,复制对应的私钥用于解密
const PRIVATE_KEY = `MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMsJ/W1CjGleDoVA
9mcKZSlku80iNdGO0WaxDwsRqsqdqaOfACEOcc6nPikJZccVyjHjQWElDNSbIDMq
AiW4Au9uNiUdqYHUhj5cF4SxH+HYGJG5/BJaPVhXivWshuF3mn7KaoRlxB5alxji
56R76T3xFnT5UE65/gMZN2tvwv8/AgMBAAECgYBeFe4C0GDCftxZsWW9D9sa2FwS
TbUEu5qbbJbc+T3ckDzI3mgv6UKhkWxDleA85gMBJR7pxkJwzsWYD/JYyjFJMJRm
5T7muGxm9CnywUH4dng+IlJc+JtqNOGVF9QmCfA5dI+WQu7xqoBAh3GI5vEEHJMo
qYaDSGboYHSd+y5eYQJBAO6aITksboR7TflI6zwDtGmef5xq6DmcdEUtVGe+Bpx3
HOIABKIqnf3znTVe9MYkk95fXlDJlgPTmjDlo8TvxVECQQDZ2AWZKbcFqrpucv7B
O2uLwWfh5daBbmpt7ixhNRNrEFYopCJTUKFm/9Rk4NUQ89HBQdjSMUnlaYhqGjVB
IZePAj8UgSpZv3e/6tjIk3ujrK3UZcqRpp5OVSOozjxyreHjkFjrExVS2la5fDYG
YCKo5HvQoGF6j9hUe9rEWPe59OECQHwjL72CGfuuuKJsAWRX2gc/5VTDRqNnKlsO
mFekiTY/jvmF3tGfZvps2rnJrWEFsAfy3/2XfMawhr3/xU0iOV0CQQC74OOHYKhm
C6rvN+Kzk63OrUmC8F/a9xmLSSG8Jd0gmHGkmRMeYxRUxgs+LS/htHDAKYyGH2uD
msqCj47HstQs`
//使用私钥解密
const decrypt = new JSEncrypt()
decrypt.setPrivateKey(PRIVATE_KEY)
let result = decrypt.decrypt(data)
return result
}
在vue组件测试
import { rsaEncrypt, rsaDecrypt } from '@/utils'
// 在需要的地方使用
console.log('加密:', rsaEncrypt('123456'))
console.log('解密:', rsaDecrypt (rsaEncrypt('123456')))
2、双token 认证
就是有一个需求:token设置了30分钟的有效期,如果用户30分钟之内没有任何操作,那么下一次操作的时候被退出登录。如果30分钟之内有进行操作,那么就实现无感刷新token。
第一步:监听鼠标移动、鼠标点击、键盘按下这几个事件,获取当前的时间,与上一次操作的时间进行比较,判断过去有没有29分钟,如果没有就缓存时间或更新缓存的时间为当期时间、清除监听的事件、定时器,(这个步骤可以做一下节流),如果有超过29分钟(不能等到30分钟,因为等到30分钟的话已经失效了),就触发更新token的步骤,发送登录获取token的接口请求,这时只是加上了参数refresh_token,更新时账户和密码值由后端给。
有一个特殊的情况是在刷新token的时候并行地请求了其他接口,因为新的token还没有回来,而后面的请求带的时候旧token,这样的话,后面的接口可能会请求不成功。,所以有了第二步,
第二步:在请求拦截时拦截刷新token的接口,并且定义一个变量用来表示是不是正在更新token,此时,如果是正在刷新token的操作,如果正在刷新,就要要收集后面的接口,等到token更新结束之后重新请求一遍。
普通的鉴权机制是前端发送账号密码等参数给后端,后端检验数据库是否存在,如果存在就生成一个token返回给前端,之后的请求都要在头部带上token这个令牌,才能鉴权通过,然后返回相应的数据。
双token是返回两个token,一个accessToken,生成的时候设置了短暂的有效期,用于正常的登录鉴权,一个refreshToken,用于更新accessToken。
做法是:
const TokenKey = "currentUser_token";
const refreshTokenKey = "refresh_Token_Key";
const UserName = "currentUser_name";
const Secret = "currentUser_secret";
// refreshToken更新token
export async function refresh_Token() {
const passwordDeal = unescape(escape("secret"));
console.log("第一次");
let params = new URLSearchParams();
params.append("refresh_token", getRefreshToken());
params.append("grant_type", "refresh_token");
return request({
url: "/uaa/oauth/token",
method: "post",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
data: params,
auth: {
username: "zuul_server",
password: passwordDeal,
},
}).then(function (res) {
// 当刷新成功后, 重新发送缓存请求
setToken(res.data.access_token, true);
setSecret(res.headers["nonce-secret"]);
});
}
//
let Timer = null;
// 获取当前时间
let currentTime = new Date().getTime(),
lastTime = new Date().getTime();
// 设置自动失效时长
const diff = 1000 * 60 * 29;
// const diff = 1000 * 60 * 1; //测试
// 重置最后一次操作的时间
function resetlastTime() {
if (getLastTime() && new Date().getTime() - getLastTime() > diff) {
clear_all();
clearInterval(Timer);
}
lastTime = new Date().getTime();
setLastTime();
}
// 刷新token机制
const refresh_Token_update = function () {
// 监听用户长时间不操作后自动退出登录
console.log("timer start", new Date().toLocaleTimeString());
// 开启时间监听(鼠标移动、点击、键盘点击)
document.addEventListener("mouseover", resetlastTime, true, {
passive: true,
});
document.addEventListener("keydown", resetlastTime, true, { passive: true });
document.addEventListener("mousedown", resetlastTime, true, {
passive: true,
});
// 开启定时器
Timer = setInterval(async function () {
currentTime = new Date().getTime();
if (getToken() !== null) {
if (currentTime - lastTime > diff) {
// 清除登录状态操作
// message.error("检测到您30分钟内无操作,请重新登录");
clear_all();
clearInterval(Timer);
} else {
console.log("refreshToken:", new Date().toLocaleTimeString());
refresh_Token();
}
} else {
clearInterval(Timer);
}
}, diff);
};
// // 页面刷新重新开启token自动续期计时器
if (lastTime) {
// 如果当前时间减去上一次操作的时间<可操作时间
if (new Date().getTime() - lastTime < diff) {
// 当前操作时间在操作有效期内
// 就触发更新token的机制
refresh_Token_update();
}
}
export function clear_all() {
console.log("log out:", new Date().toLocaleTimeString());
document.removeEventListener("mouseover", resetlastTime, true);
document.removeEventListener("keydown", resetlastTime, true);
document.removeEventListener("mousedown", resetlastTime, true);
removeTokenTime();
}
// 清除token有效性
export async function removeTokenTime() {
const res = await axios({
url: "/uaa/removeToken",
method: "get",
headers: { Authorization: "Bearer " + getToken() },
});
removeAllCookies();
return res;
}
export function removeAllCookies() {
for (const key in Cookies.get()) {
Cookies.remove(key);
}
}
let lock = false;
// create an axios instance
const service = axios.create({
withCredentials: true, // 跨域请求时发送 cookies
// timeout: 30 * 1000 // request timeout second //超时暂时未用到. 2020-05-19 10:28:36
});
// request interceptor
service.interceptors.request.use(
(config) => {
config.method = config.fetchMethod || config.method;
let rt = Math.random().toString().replace(".", "") + new Date().getTime();
rt = AESUtils.encrypt(rt);
config.url = config.url + `?rt=${rt}`;
if (getToken()) {
config.headers.common["Authorization"] = "Bearer " + getToken();
}
if (getSecret()) {
config.headers.common["nonce-token"] = getNonceToken(getSecret());
}
if (config.method === "get" || config.method === "post") {
//如果是get请求,且params是数组类型如arr=[1,2],则转换成arr=1&arr=2
config.paramsSerializer = function (params) {
return qs.stringify(params, { arrayFormat: "repeat" });
};
}
return config;
},
(error) => {
// Do something with request error
Promise.reject(error);
}
);
// 是否正在刷新的标记 -- 防止重复发出刷新token接口
let isRefreshing = false;
// 失效后同时发送请求的容器 -- 缓存接口
let subscribers = [];
let can = false; //锁,拦截到第一次更新token之后才放开将普通接口缓存到寄存器
// 刷新 token 后, 将缓存的接口重新请求一次
export function onAccessTokenFetched(newToken) {
subscribers.forEach((callback) => {
callback(newToken);
});
// 清空缓存接口
can =false
subscribers = [];
}
// 添加缓存接口
function addSubscriber(callback) {
subscribers.push(callback);
}
service.interceptors.response.use(
(response) => {
if (
response.config.url.includes("/oauth/token") &&response.config.data.includes("grant_type=refresh_token")
) {
can = true;
}
if (can && getToken() && !response.config.url.includes("/oauth/token")) {
if (!isRefreshing) {
isRefreshing = true;
const passwordDeal = unescape(escape("secret"));
console.log('第二次');
let params = new URLSearchParams();
params.append("refresh_token", getRefreshToken());
params.append("grant_type", "refresh_token");
axios({
url: "/uaa/oauth/token",
method: "post",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
data: params,
auth: {
username: "zuul_server",
password: passwordDeal,
},
})
.then(async(res) => {
// 当刷新成功后, 重新发送缓存请求
setToken(res.data.access_token, true);
// setRefreshToken(res.data.refresh_token);
setSecret(res.headers["nonce-secret"]);
onAccessTokenFetched(res.data.access_token);
})
.catch(() => {
// 刷新token报错的话, 就需要跳转到登录页面
console.log('异常');
// window.location = "/#/login";
})
.finally(() => {
isRefreshing = false;
});
}
// 将其他接口缓存起来 -- 这个Promise函数很关键
const retryOriginalRequest = new Promise((resolve) => {
//
// 只有当token刷新成功后, 就会调用通过addSubscriber函数添加的缓存接口,
// 此时, Promise的状态就会变成resolve
addSubscriber((newToken) => {
// 表示用新的token去替换掉原来的token
response.config.headers.Authorization = `Bearer ${newToken}`;
// config.headers.common["Authorization"] = "Bearer " + newToken;
// 用重新封装的config去请求, 就会将重新请求后的返回
resolve(service(response.config));
});
});
return retryOriginalRequest;
}
// // 检验违法词状态码拦截
if (response.status == 202 && response.data.status == "ERROR") {
Message.error(response.data.message);
return;
} else {
return response;
}
},
(error) => {
// 因为用体验差,403不退出登录页
if (error.response.status && error.response.status === 403) {
Message({
message: "没有访问权限",
type: "error",
});
}
if (error.response.status && error.response.status === 401) {
if (!lock) {
lock = true;
axios({
url: "/uaa/checkOffLineToken",
method: "get",
params: {
token: getToken(),
},
})
.then((res) => {
if (res.data.status === "ERROR" && error.response.status === 401) {
MessageBox.alert("您的账号已在其他地方登录,请重新登录", "提示", {
confirmButtonText: "确定",
showClose: false,
type: "warning",
}).then((action) => {
if (action == "confirm") {
store.dispatch("user/resetToken");
location.href = "/#/login";
}
});
} else {
store.dispatch("user/resetToken");
location.href = "/#/login";
}
})
.catch((err) => {
store.dispatch("user/resetToken");
location.href = "/#/login";
})
.finally(() => {
lock = false;
});
}
}
if (
error.response.status === 400 &&
error.response.config.url === "/uaa/oauth/token"
) {
store.dispatch("user/resetToken");
location.href = "/#/login";
}
return Promise.reject(error);
}
);
export default service;
3、echart封装
在实际项目中封装一个 Vue 组件 在实际项目中封装一个Vue组件,你需要做以下几个步骤:
- 创建组件模板文件,定义组件的模板(template)、脚本(script)和样式(style)部分。
- 注册组件props:通过 props 属性定义组件的输入属性,用于父组件向子组件传递数据。
- 编写组件逻辑:在组件的 script 部分编写组件的逻辑,包括数据处理、事件处理、生命周期钩子等。
- 全局注册组件,或者使用的时候import引入,后面就是像普通子组件一样使用它
开发公共组件原则
1)和业务有关的数据不要再在组件中获取或处理,数据尽量从父组件传入
2)在父组件处理相关事件
3)公共组件一般需要留一个或者多个插槽
4)兼容性问题,公共组件可能设计到浏览器和屏幕的不同的原因,应该在适配方面做一些样式和js的兼容
5)可定制与可扩展,对不同的场景增加可定制性和可扩展性
6)样式方面可以让开发者自己选择定义
7)给给开发者尽可能多的配置空间,便于二次开发
第一步创建一个vue文件,初始化一个echart,同时利用组件通信开放高度、宽度、option、resize等配置项,
this.echart = echarts.init(this.$refs.echarts);
第二步,setOption,
this.echart.setOption(this.options);
第三步, 深度监听option的变化,当数据更新时,重新setOptionge重新渲染
第四步,添加窗口改变resize事件、图表点击事件,resize事件需要做监听和节流
第五步,在组建销毁前 释放该图例资源以及清除对resize事件的监听
export default {
data() {
return {
chartResizeTimer: null, // 定时器,用于resize事件函数节流
};
},
methods: {
ready() {
// 添加窗口resize事件
window.addEventListener('resize', vm.handleChartResize);
// 触发父组件的 @chartClick 事件
vm.myChart.on('click', function(param) {
vm.$emit('chartClick', param);
});
},
// 处理窗口resize事件
handleChartResize() {
let vm = this;
clearTimeout(vm.chartResizeTimer);
vm.chartResizeTimer = setTimeout(function() {
vm.myChart && vm.myChart.resize();
}, 200);
},
},
beforeDestroy() {
// 释放该图例资源,较少页面卡顿情况
if (this.myChart) this.myChart.clear();
// 移除窗口resize事件
window.removeEventListener('resize', this.handleChartResize);
}
};
/** 对echarts的配置进行封装 **/
<!--
图表
prop
@params:width 宽度
@params:height 高度
@params autoResize 是否自动调整大小
@params option 图表配置
event
@name series-click 点击在series的事件
-->
<template>
<div :style="{ height: height }" class="chart" ref="wrapper">
<div :style="{ height: height, width: width }" ref="chart"></div>
</div>
</template>
<script>
import echarts from "echarts";
import elementResizeDetectorMaker from "element-resize-detector";
export default {
name: "custom-chart-panel",
props: {
width: {
type: String,
default: "100%",
},
height: {
type: String,
default: "500px",
},
autoResize: {
type: Boolean,
default: true,
},
option: {
type: Object,
required: true,
},
isClick: {
type: Boolean,
default: true,
},
// /* hoverFlag:{//悬浮
// type:Boolean,
// default:false
// }, */
efWithScatter: {
type: Boolean,
default: false,
}, //带气泡的散点
},
data() {
return {
chart: null,
};
},
watch: {
option: {
deep: true,
handler(newVal) {
this.setOptions(newVal);
},
},
},
async mounted() {
await this.initChart();
if (this.autoResize) {
this.getRefPromise("chart").then((ref) => {
this.resizer = elementResizeDetectorMaker();
this.resizer.listenTo(
{ strategy: "scroll" },
ref,
this._debounce(this.handleResize, 200)
);
});
// this.handleResize = this._debounce(this.handleResize);
// window.addEventListener("resize", this.chart.resize());
}
},
beforeDestroy() {
if (!this.chart) {
return;
}
if (this.autoResize) {
window.removeEventListener("resize", this.handleResize);
}
this.chart.dispose();
this.chart = null;
},
methods: {
initChart() {
let resData;
const option = this.option;
this.chart = echarts.init(this.$refs.chart);
this.chart.setOption(option);
const emitHandlerData = ["scatter", "tree", "map", "graph"];
// this.chart.on('click', function () {
// console.log('testt');
// // series name 为 'uuu' 的系列中的图形元素被 'mouseover' 时,此方法被回调。
// });
if (this.isClick) {
this.chart.on("click", "series", (componentOptions) => {
const { seriesIndex, dataIndex, seriesType } = componentOptions;
if (emitHandlerData.includes(seriesType)) {
resData = componentOptions.data;
} else {
resData =
this.option.originData[seriesIndex][dataIndex] ||
this.option.originData[dataIndex];
}
this.$emit("series-click", resData, componentOptions, seriesIndex);
});
}
if (this.efWithScatter) {
this.chart.on("mouseout", (componentOptions) => {
this.chart.dispatchAction({
type: "downplay",
});
});
this.chart.on("mousemove", (componentOptions) => {
if (componentOptions.seriesType == "map") {
return;
}
this.chart.dispatchAction({
type: "downplay",
});
this.chart.dispatchAction({
type: "highlight",
name: componentOptions.data.name,
});
});
}
if (this.isClick) {
/* 解决饼图hover阴影消失问题 */
this.chart.on("mouseover", (e) => {
let op = this.chart.getOption();
this.chart.dispatchAction({
type: "downplay",
seriesIndex: 0,
dataIndex: e.dataIndex,
color: e.color,
});
this.chart.setOption(op, true);
});
}
},
// 防抖
_debounce(fn, dur = 200) {
const context = this;
let timer = null;
return function (...args) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(context, args);
}, dur);
};
},
handleResize() {
return this.chart.resize();
},
setOptions(options) {
if (this.chart) {
this.handleResize();
this.chart.setOption(options, true);
}
},
getRefPromise(name) {
let that = this;
return new Promise((resolve) => {
(function next() {
let ref = that.$refs[name];
if (ref) {
resolve(ref);
} else {
setTimeout(() => {
next();
}, 10);
}
})();
});
},
},
};
</script>
<style lang="less" scoped></style>
4、vuex使用
Vuex 的核心概念主要包括 state、getters、mutations、actions 、modules
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
count: 0 //保存基本数据
};
const mutations = {
increment(state) {
state.count++; //改变数据
},
decrement(state) {
state.count--;
}
};
const actions = { //异步操作
increment(context) {
context.commit('increment');
},
decrement(context) {
context.commit('decrement');
}
};
const getters = { //计算保存的基本属性数据
getCount: state => {
return state.count;
}
};
export default new Vuex.Store({
state,
mutations,
actions,
getters
});
还有一个是modules模块化,uex 的 modules 是一种让你可以将 store
分割成多个模块的方式,每个模块拥有自己的 state、mutations、
actions 和 getters。这样可以使得 Vuex store 更加可维护和扩展,
特别是在大型应用程序中
页面中调用vuex 数据和改变数据的方法:
可以通过this.$store.state.count
来访问 state中的 count 数据,
通过this.$store.commit('increment')
来调用 mutations 中名为 increment 的 mutation 方法,实现改变数据的操作。
使用this.$store. dispatch
方法来调用 actions中的异步操作方法。
5、请求封装
import axios from 'axios';
import axios from 'axios';
const instance = axios.create({
baseURL: 'https://your-api-url.com', // 设置默认请求的基础URL
timeout: 10000, // 设置请求超时时间
});
// 添加请求拦截器
instance.interceptors.request.use(
(config) => {
// 在发送请求之前做些处理,如添加token等
return config;
},
(error) => {
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(
(response) => {
// 对响应数据进行处理
return response.data;
},
(error) => {
// 对响应错误进行处理
return Promise.reject(error);
});
// 封装请求方法
export const get = async (url, params) => {
try {
const response = await instance.get(url, { params });
return response.data;
} catch (error) {
// 处理错误
throw error;
}
};
export const post = async (url, data) => {
try {
const response = await instance.post(url, data);
return response.data;
} catch (error) {
// 处理错误
throw error;
}
};
export default instance;
// 其他封装的请求方法,根据需要继续添加
6、长列表虚拟滚动
方法一、使用 Vue 和 vue-virtual-scroller 插件实现长列表虚拟滚动
方法二、动态加载数据
1)、使用了 @scroll 事件来监听滚动事件。当用户滚动列表时,会触发 handleScroll 方法。该方法根据滚动的距离计算出可见的列表项的起始索引,并更新 startIndex 的值。通过绑定 startIndex 到列表项的 v-for 循环,实现了虚拟滚动的效果。
2)、当用户滚动列表时,会根据可见区域的起始索引和结束索引调用 loadData 方法来加载数据,并更新 visibleItems 数组。同时,我们还通过 offsetY 控制列表的偏移量,实现滚动效果。
<template>
<div class="list-container" @scroll="handleScroll">
<div class="viewport" :style="{ height: viewportHeight + 'px' }">
<div :style="{ height: totalHeight + 'px', transform: 'translateY(' + offsetY + 'px)' }">
<div v-for="(item, index) in visibleItems" :key="startIndex + index" class="list-item">
Row {{ startIndex + index }} - {{ item }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
totalItems: 1000, // 列表总项数
visibleItems: [], // 可见的列表项
itemHeight: 50, // 单个列表项的高度
startIndex: 0, // 可见列表项的起始索引
viewportHeight: 500, // 可视区域的高度
offsetY: 0, // 列表偏移量
};
},
computed: {
totalHeight() {
return this.totalItems * this.itemHeight;
},
},
methods: {
handleScroll() {
const scrollTop = event.target.scrollTop;
this.startIndex = Math.floor(scrollTop / this.itemHeight);
// 模拟异步加载数据
setTimeout(() => {
this.visibleItems = this.loadData(this.startIndex, this.startIndex + Math.ceil(this.viewportHeight / this.itemHeight));
this.offsetY = this.startIndex * this.itemHeight;
}, 200);
},
loadData(start, end) {
// 模拟异步加载数据
return Array.from({ length: end - start }, (_, i) => `Item ${start + i}`);
},
},
};
</script>
<style>
.list-container {
width: 300px;
height: 400px;
overflow: auto;
}
.viewport {
position: relative;
overflow: hidden;
}
.list-item {
line-height: 50px;
height: 50px;
}
</style>
7、如何判断一个元素在不在可视区域内
一、用途
可视区域即我们浏览网页的设备肉眼可见的区域,如下图
在日常开发中,我们经常需要判断目标元素是否在视窗之内或者和视窗的距离小于一个值(例如 100 px),从而实现一些常用的功能,例如:
图片的懒加载
列表的无限滚动
计算广告元素的曝光情况
可点击链接的预加载
二、实现方式
判断一个元素是否在可视区域,我们常用的有三种办法:
1)、根据元素顶部到视口顶部的距离offsetTop,滚动的滚动条滚动距离scrollTop,视图高度innerHeigh三个比较判断,如果在可视区域,el.offsetTop - document.documentElement.scrollTop < viewPortHeight。
元素的offsetTop、根元素的 scrollTop、innerHeight(clientHeight)
2)、根据DOMRect对象的来判断,如果在可视区域内,对象的left大于0,top大于0,right小于视图宽度,bottom小于视图高度,根据元素的getBoundingClientRect
3)、利用浏览器的api,Intersection Observer,第一步是,new IntersectionObserve 创建观察者,一般为目标元素的父元素,观察重叠面积超过设置的比例时触发回调,第二部就是将目标元素注册为被观察者,具体是将元素作为观察者observer方法的参数
方法一、 offsetTop、scrollTop
offsetTop,元素的上外边框至包含元素的上内边框之间的像素距离,其他offset属性如下图所示:
下面再来了解下clientWidth、clientHeight:
clientWidth:元素内容区宽度加上左右内边距宽度,即clientWidth = content + padding
clientHeight:元素内容区高度加上上下内边距高度,即clientHeight = content + padding
这里可以看到client元素都不包括外边距
最后,关于scroll系列的属性如下:
scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小
scrollLeft 和 scrollTop 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置
垂直滚动 scrollTop > 0 水平滚动 scrollLeft > 0 将元素的 scrollLeft 和 scrollTop 设置为 0,可以重置元素的滚动位置
注意
上述属性都是只读的,每次访问都要重新开始 下面再看看如何实现判断:
公式如下:
el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
1
代码实现:
function isInViewPortOfOne (el) {
// viewPortHeight 兼容所有浏览器写法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
return top <= viewPortHeight
}
方法二 、getBoundingClientRect:返回值是一个 DOMRect对象,拥有left, top, right, bottom, x, y, width, 和 height属性
const target = document.querySelector('.target');
const clientRect = target.getBoundingClientRect();
console.log(clientRect);
// {
// bottom: 556.21875,
// height: 393.59375,
// left: 333,
// right: 1017,
// top: 162.625,
// width: 684
// }
属性对应的关系图如下所示:
当页面发生滚动的时候,top与left属性值都会随之改变
如果一个元素在视窗之内的话,那么它一定满足下面四个条件:
top 大于等于 0 left 大于等于 0 bottom 小于等于视窗高度 right 小于等于视窗宽度 实现代码如下:
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left,
} = element.getBoundingClientRect();
return (
top >= 0 &&
left >= 0 &&
right <= viewWidth &&
bottom <= viewHeight
);
}
方法三 、Intersection Observer
Intersection Observer 即重叠观察者,从这个命名就可以看出它用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比getBoundingClientRect会好很多
使用步骤主要分为两步:创建观察者和传入被观察者
创建观察者
const options = {
// 表示重叠面积占被观察者的比例,从 0 - 1 取值,
// 1 表示完全被包含
threshold: 1.0,
root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素
};
const callback = (entries, observer) => { ....}
const observer = new IntersectionObserver(callback, options);
通过new IntersectionObserver创建了观察者 observer,传入的参数 callback 在重叠比例超过 threshold 时会被执行
关于callback回调函数常用属性如下:
// 上段代码中被省略的 callback
const callback = function(entries, observer) {
entries.forEach(entry => {
entry.time; // 触发的时间
entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位置
entry.boundingClientRect; // 被观察者的位置举行
entry.intersectionRect; // 重叠区域的位置矩形
entry.intersectionRatio; // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
entry.target; // 被观察者
}); };
传入被观察者
通过 observer.observe(target) 这一行代码即可简单的注册被观察者
const target = document.querySelector('.target');
observer.observe(target);
8、图片懒加载
图片懒加载实现
是的,除了使用vue-lazyload插件外,还有其他几种常见的实现图片懒加载的方式。
我将介绍其中两种常用的方式:
Intersection Observer API:这是一个新的浏览器 API,可以观察目标元素
是否进入了视口(可见区域)。使用Intersection Observer API,可以在
目标元素进入视口时延迟加载图片。可以根据你的需求自定义阈值,
决定图片何时开始加载。
<template>
<div>
<img ref="lazyImage" src="占位符图片路径" alt="图片">
</div>
</template>
<script>
export default {
mounted() {
const options = {
threshold: 0.5 // 阈值,当目标元素50%进入视口时触发加载
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.$refs.lazyImage.src = '真实图片路径'
observer.unobserve(entry.target) // 停止观察已加载的图片
}
})
}, options)
observer.observe(this.$refs.lazyImage)
}
}
</script>
自定义指令:你也可以通过Vue的自定义指令来实现图片懒加载。自定义指令
允许你直接在DOM元素上添加特定的行为。
<template>
<div>
<img v-lazyload="imageSrc" src="占位符图片路径" alt="图片">
</div>
</template>
<script>
export default {
directives: {
lazyload: {
inserted(el, binding) {
const image = new Image()
image.src = binding.value
image.onload = () => {
el.src = binding.value
}
}
}
},
data() {
return {
imageSrc: '真实图片路径'
}
}
}
</script>
以上是两种常见的实现图片懒加载的方式,你可以根据自己的需求选择适合的
方法。需要注意的是,这些方法都需要在图片进入视口时加载真实图片,
以减少页面的加载时间和提高性能。
9、大屏可视化自适应
大屏自适应最优解决方案 ==> transform:scale
大屏使用rem 耗时 而且对浏览器最小字体不支持,
使用transform:scale可以节省百分之九十工作量
//左边打开控制台和底部打开控制台,使得内容的可视化内容位置不一致,不能让可视化内容显示在最左上角
步骤:
1、创建一个缩放组件
2、将缩放组件引入可视化页面,可视化页面最顶级节点设置宽100%,高100vh整个屏幕,
3、缩放组件在第二节点上
4、第三节点是包裹着真正可视化内容的节点
/**缩放组件*/
<template>
<div
class="ScaleBox"
ref="ScaleBox"
:style="{
width: width + 'px',
height: height + 'px'
}"
>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'ScaleBox',
props: {},
data () {
return {
scale: 0,
// alert(window.innerHeight)
//alert(window.innerHeight)
width: 1528;
height: 716;
}
},
mounted () {
this.setScale()
window.addEventListener('resize', this.debounce(this.setScale))
},
methods: {
getScale () {
const { width, height } = this
const wh = window.innerHeight / height
const ww = window.innerWidth / width
console.log(ww < wh ? ww : wh)
return ww < wh ? ww : wh
},
setScale () {
this.scale = this.getScale()
if (this.$refs.ScaleBox) {
this.$refs.ScaleBox.style.setProperty('--scale', this.scale)
}
},
debounce (fn, delay) {
const delays = delay || 500
let timer
return function () {
const th = this
const args = arguments
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(function () {
timer = null
fn.apply(th, args)
}, delays)
}
}
}
}
</script>
<style lang="less">
#ScaleBox {
--scale: 1;
}
.ScaleBox {
transform: scale(var(--scale));
transform-origin: 0 0;
background: rgba(255, 0, 0, 0.3);
}
</style>
/**可视化页面*/
<template>
<div id="screen">
<SacleBox>
<div class="content">内容</div>
</SacleBox>
</div>
</template>
<script>
import SacleBox from './Welcome.vue'
export default {
components: {
SacleBox
},
data () {
return {
}
}
}
</script>
<style>
#screen{
width: 100%;
height: 100vh;
overflow: hidden;
position: relative;
font-size: 16px;
background-color: pink;
}
.content{
width: 1528px;
height: 716px;
box-sizing: border-box;
margin: 0 auto;
}
</style>
10、地图常用的空间有哪些
地图常用的控件包括:
- 缩放控件
setZoom
:通常位于地图的角落,用于控制地图的缩放级别,包括放大和缩小按钮。- 比例尺控件
scaleControl
:用于显示地图上距离的比例尺,通常显示为地图上的一个标尺,方便用户估计地图上某一段距离的实际长度。- 缩略图控件
overviewMapControl
:在大比例尺地图上,通常显示一个缩略图,可以显示整个地图的范围和视窗位置,便于用户在整个地图中快速导航。- 信息窗控件
<bm-info-window>
: 用于显示地图上的特定信息,通常是用户点击地图上的点时,弹出的信息窗口,显示该点的具体信息或者自定义内容。- 绘制控件
<bm-drawingManager>
: 用于绘制点、线、面等地图要素,可以用来标记特定位置、规划路径等功能。- 定位控件
BmGeolocationControl
: 用于定位用户的位置,通常包括地图上的标记图标和定位按钮。
4)拖动控件(BmMarker )
以下是一个简单vue创建地图并添加常见控件的示例代码:
<template>
<div>
<!-- 使用 baidu-map 组件来展示百度地图 -->
<baidu-map
:ak="YOUR_BAIDU_MAP_API_KEY"
:center="{lng: 116.404, lat: 39.915}"
:zoom="11"
style="width: 100%; height: 400px"
ref="baiduMap"
>
<!-- 添加常见控件 -->
<!-- 导航控件,用于控制地图的缩放和平移 -->
<bm-navigationControl></bm-navigationControl>
<!-- 比例尺控件,显示当前地图的比例尺 -->
<bm-scaleControl></bm-scaleControl>
<!-- 缩略图控件,展示地图的全局视图 -->
<bm-overviewMapControl></bm-overviewMapControl>
<!-- 地图类型控件,用于切换地图的类型 -->
<bm-mapTypeControl></bm-mapTypeControl>
<!-- 地理编码功能 -->
<h2>Address:</h2>
<input type="text" v-model="address" placeholder="Enter an address"/>
<button @click="geocode">Geocode</button>
<p>{{ geocodeResult }}</p>
<h2>Coordinates:</h2>
<input type="text" v-model="latitude" placeholder="Latitude"/>
<input type="text" v-model="longitude" placeholder="Longitude"/>
<button @click="reverseGeocode">Reverse Geocode</button>
<p>{{ reverseGeocodeResult }}</p>
</baidu-map>
</div>
</template>
<script>
import {
BaiduMap,
BmNavigationControl,
BmScaleControl,
BmOverviewMapControl,
BmMapTypeControl
} from 'vue-baidu-map';
export default {
components: {
BaiduMap,
BmNavigationControl,
BmScaleControl,
BmOverviewMapControl,
BmMapTypeControl
},
data() {
return {
address: '',
latitude: '',
longitude: '',
geocodeResult: '',
reverseGeocodeResult: ''
};
},
methods: {
// 地址转坐标的方法
geocode() {
this.$refs['baiduMap'].geocoder.getPoint(this.address).then((point) => {
if (point) {
this.geocodeResult =
`Geocode result - Latitude: ${point.lat}, Longitude: ${point.lng}`;
} else {
this.geocodeResult = 'Geocode failed';
}
});
},
// 坐标转地址的方法
reverseGeocode() {
this.$refs['baiduMap']
.geocoder
.getLocation(new window.BMap.Point(this.longitude, this.latitude))
.then((result) => {
if (result) {
this.reverseGeocodeResult =
`Reverse Geocode result - Address: ${result.address}`;
} else {
this.reverseGeocodeResult = 'Reverse Geocode failed';
}
});
}
}
};
</script>
10、单点登录(SSO)
Single Sign ON, 是在企业内部多个应用系统(比如考勤系统、财务系统、人事系统)场景下,用户只需要登录一次们就可以访问多个应用系统,一次登录,全部登录,一次推出全部退出。(用这个理解就好了)
定义:允许用户使用一个单一的身份验证凭据(例如用户名和密码)登录多个相关但独立的软件系统或应用。通过SSO,用户只需要一次登录就能访问多个不同的应用程序,无需反复输入凭据,通常通过认证协议(如OAuth、OpenID Connect等)来实现用户的身份验证和授权。提高了用户体验的便利性和效率。
- 实现方式一:父域 Cookie
Cookie 的作用域由 domain 属性和 path 属性共同决定。domain 属性的有效值为当前域或其父域的域名/IP地址。如果将 Cookie 的 domain 属性设置为当前域的父域,那么就认为它是父域 Cookie。Cookie 有一个特点,即父域中的 Cookie 被子域所共享,换言之,子域会自动继承父域中的Cookie。
利用cookie的这个特点,将 Token保存到父域中,然后要将 Cookie 的 domain 属性设置为父域的域名(主域名),同时将 Cookie 的 path 属性设置为根路径,这样所有的子域应用就都可以访问到这个 Cookie 了。
缺点:不过这要求应用系统的域名需建立在一个共同的主域名之下,如 http://tieba.baidu.com 和 http://map.baidu.com,它们都建立在 http://baidu.com 这个主域名之下,那么它们就可以通过这种方式来实现单点登录。
总结
:此种实现方式比较简单,但不支持跨主域名。
- 实现方式二:LocalStorage 跨域
前端拿到 Token 后,除了将它写入自己的 LocalStorage 中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage 中。
// 获取 token
var token = result.data.token;
// 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
iframe.remove();
}, 6000);
// 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,
把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
localStorage.setItem('token', event.data)
}, false);
前端通过 iframe+postMessage() 方式,将同一份 Token 写入到了多个域下的 LocalStorage 中,前端每次在向后端发送请求之前,都会主动从 LocalStorage 中读取 Token 并在请求中携带,这样就实现了同一份 Token 被多个域所共享。
总结
:此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域。
3) 实现方式三:认证中心
流程图:
1)用户首次访问系统A系统时,需要进行登录
2)系统A带着用户登录信息重定向给认证系统
3) 认证系统验证用户登录信息
4) 验证通过后,返回一个token
5) 认证系统带着token重定向给系统A,得知用户是已登录状态
6)系统 A向用户返回请求的资源
7) 用户访问系统B时,重定向到认证系统
8) 系统B通过共享的token,得知用户是已经登录状态
9)、系统B向用户返回请求的资源
总结
:此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法
场景:顶级域名与子级域名间共享token实现单点登录。
补充:域名分级
从专业的角度来说(根据《计算机网络》中的定义),.com、.cn 为一级域名(也称顶级域名),.http://com.cn、http://baidu.com 为二级域名,http://sina.com.cn、http://tieba.baidu.com 为三级域名,以此类推,N 级域名就是 N-1 级域名的直接子域名
OAuth 第三方登录
OAuth 第三方登录方式通常使用以下三种方式:qq、微信、微博
OAuth 机制实现流程,这里以微信开放平台的接入流程为例:1)首先,a.com 的运营者需要在微信开放平台注册账号,并向微信申请使用微信登录功能。
2)申请成功后,得到申请的 appid、appsecret。
3)用户在 a.com 上选择使用微信登录。
4)这时会跳转微信的 OAuth 授权登录,并带上 a.com 的回调地址。
5)用户输入微信账号和密码,登录成功后,需要选择具体的授权范围,如:授权用户的头像、昵称等。
6)授权之后,微信会根据拉起 a.com?code=123 ,这时带上了一个临时票据 code。
7)获取 code 之后, a.com 会拿着 code 、appid、appsecret,向微信服务器申请 token,验证成功后,微信会下发一个 token。
8)有了 token 之后, a.com 就可以凭借 token 拿到对应的微信用户头像,用户昵称等信息了。
9)a.com 提示用户登录成功,并将登录状态写入 Cookie,以作为后续访问的凭证。其他平台的接入方式可以去对应得官方文档查看,流程基本类似。
要在Vue.js应用程序中实现OAuth
2.0第三方登录,你可以使用一些库来简化这个过程。常见的做法是使用vue-authenticate或vue-oauth2-implicit这样的库。
这里我将介绍如何使用vue-authenticate库来实现OAuth
2.0第三方登录。首先,确保你的应用程序安装了Vue.js和vue-authenticate库。你可以通过npm或yarn来安装它们:
npm install vue-authenticate --save
或者
yarn add vue-authenticate
接下来,在你的Vue.js应用程序中使用它:
import Vue from 'vue';
import VueAuthenticate from 'vue-authenticate';
Vue.use(VueAuthenticate, {
baseUrl: 'http://example.com', // OAuth 2.0 认证服务器的基础URL
providers: {
// 定义第三方登录提供商
github: {
clientId: 'your-github-client-id',
redirectUri: 'http://your-redirect-uri'
},
google: {
clientId: 'your-google-client-id',
redirectUri: 'http://your-redirect-uri'
},
// 可以添加其他第三方提供商
}
});
new Vue({
// your Vue app configurations
}).$mount('#app');
上面的代码中,你需要替换baseUrl为你的OAuth 2.0认证服务器的基础URL,
然后添加所需的第三方提供商(如GitHub、Google等)并提供相应的客户端ID
和重定向URI。
然后,在你的Vue组件中,你可以使用this.$auth.authenticate(provider)
来触发第三方登录:
<template>
<div>
<button @click="loginWithGitHub">Login with GitHub</button>
<button @click="loginWithGoogle">Login with Google</button>
</div>
</template>
<script>
export default {
methods: {
loginWithGitHub() {
this.$auth.authenticate('github')
.then(response => {
console.log(response);
// 登录成功后的操作
})
.catch(error => {
console.error(error);
// 处理登录失败
});
},
loginWithGoogle() {
this.$auth.authenticate('google')
.then(response => {
console.log(response);
// 登录成功后的操作
})
.catch(error => {
console.error(error);
// 处理登录失败
});
}
}
}
</script>
//这样,当用户点击相应的按钮时,将触发OAuth 2.0流程,
使用户能够登录到你的应用程序。记得在OAuth 2.0提供商的控制台中
注册你的应用程序,并获取客户端ID以及设置正确的重定向URI。
总结
:四种登录实现方案,基本囊括了现有的登录验证方案,原理以及实现流程基本都了解。Cookie + Session 历史悠久,适合于简单的后端架构,需开发人员自己处理好安全问题。
Token 方案对后端压力小,适合大型分布式的后端架构,但已分发出去的 token ,如果想收回权限,就不是很方便了。
SSO 单点登录,适用于中大型企业,想要统一内部所有产品的登录方式的情况。
OAuth 第三方登录,简单易用,对用户和开发者都友好,但第三方平台很多,需要选择合适自己的第三方登录平台。
在这里插入代码片
11、多个iframe会卡顿如何优化
1)、使用标签页替代 iframe
2)、使用懒加载
3)、定时刷新或自动卸载
1) 、使用标签页替代 iframe:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用标签页替代 iframe</title>
</head>
<body>
<button onclick="openNewTab('https://www.example1.com')">打开页面1</button>
<button onclick="openNewTab('https://www.example2.com')">打开页面2</button>
<button onclick="openNewTab('https://www.example3.com')">打开页面3</button>
<script>
function openNewTab(url) {
window.open(url, '_blank');
}
</script>
</body>
</html>
在这个示例中,点击按钮会打开一个新的标签页加载指定的 URL 地址,
代替使用 iframe 加载页面。
2)、使用懒加载:
<!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>
<div id="iframeContainer">
<!-- 初始只有一个占位符,当用户需要时再加载 iframe -->
</div>
<button onclick="loadIframe('https://www.lazyloadexample.com')">加载页面</button>
<script>
function loadIframe(url) {
const iframeContainer = document.getElementById('iframeContainer');
const iframe = document.createElement('iframe');
iframe.src = url; // 设置要加载的 iframe 地址
iframe.style.width = '100%';
iframe.style.height = '400px';
iframeContainer.appendChild(iframe);
}
</script>
</body>
</html>
在这个示例中,初始页面只有一个占位符,当用户点击按钮时才会加载 iframe,
实现了懒加载的效果。
>3)、定时刷新或自动卸载
<!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>
<div id="iframeContainer">
<iframe src="https://www.autorefreshexample.com/page1" id="iframe1" width="100%" height="400px"></iframe>
<iframe src="https://www.autorefreshexample.com/page2" id="iframe2" width="100%" height="400px"></iframe>
</div>
<button onclick="refreshIframe('iframe1')">刷新页面1</button>
<br>
<button onclick="refreshIframe('iframe2')">刷新页面2</button>
<script>
function refreshIframe(id) {
const iframe = document.getElementById(id);
iframe.src = iframe.src; // 刷新 iframe 页面
}
// 模拟自动卸载 iframe 的例子
setTimeout(() => {
const iframe2 = document.getElementById("iframe2");
iframe2.remove(); // 自动卸载第二个 iframe
}, 60000); // 60秒后自动卸载
</script>
</body>
</html>
在这个示例中,点击按钮可以手动刷新相应的 iframe 页面,另外在 60 秒后第二个 iframe 会被自动卸载,模拟定时刷新或自动卸载的情况。
12、文件上传与下载
我们说的文件是blob二进制流,二进制流是不能直接传给后端的,因为在 HTTP 请求中,文本数据(比如 JSON、XML 等)是以文本形式传输的,而无法直接将二进制数据放在请求体中传输。需要做处理特殊的处理,将读取的 file 对象append进FormData 对象,以formData的方式去传输。
还有一种是将二进制流数据转为base64编码字符串去传,利用FileReader实例的api,readAsDataURL,将二进流转为编码文本
大文件上传:
分片上传
答:
1、把需要上传的文件按照一定的规则进行分割,利用文件大的slice方法, Blob.prototype.slice 方法,(和数组的 slice 方法相似,文件的 slice 方法可以返回原文件的某个切片)
2、预先定义好单个切片大小,将文件切分为一个个切片,使用 Promise.all 方法来同时上传多个文件分片,这样从原本传一个大文件,变成了并发传多个小的文件切片,可以大大减少上传时间
3、另外由于是并发,传输到服务端的顺序可能会发生变化,因此我们还需要给每个切片记录顺序
4、发送完成后,服务端会判断数据上传的完整性,如果完整,那么就会把数据合并成原文件
下载:
如果后端返回的二进制流数据是由formData搭载的
要 const blob = new Blob([res.data])//读取并转为blob二进制流内容(也就是像电脑桌面的普通文件内容)
然后在生成一个临时的下载链接 window.URL.createObjectURL(blob)
如果是base64编码文本的话,<a>
标签的href属性设置为base64编码,并设置download属性,触发点击事件即可完成浏览器下载。
大文件下载
分片下载
1)对文件进行分片:在前端中,可以使用 Blob 对象或 File 对象的 slice() 方法将文件分割为多个小块。分割的大小可以根据需要进行调整,一般建议为 1MB 到 10MB 左右。
2)发起请求获取分块数据,需要注意的是,每个分块请求需要带上 Range 头部,指定要请求的数据范围。在请求分块数据时,可以通过 Promise.all() 或 async/await 等方式实现并发请求,以提高下载速度。
响应回来的的时候 将分块数据存入数组中, 等所有请求完成后,合并分块数据。后面就是生成临时下载链接了
断点下载
断点下载是一种下载文件的方法,允许用户在下载文件时中断,然后在之后恢复下载,而不需要重新从头开始下载整个文件。这种技术通常结合使用 HTTP 协议的 Range 头部字段和 Web Worker 来实现。
Range 头部字段:在 HTTP 请求中,Range 头部字段用于指示下载文件的特定范围。当客户端支持断点下载时,它可以向服务器发送带有 Range 头部字段的请求,请求服务器传送文件中的特定部分,而不是整个文件。
Web Worker:Web Worker 是在后台运行的 JavaScript 脚本,可以在独立的线程中执行耗时的任务,而不会阻塞主线程或页面的交互。Web Worker 可以用于处理下载的数据,执行文件写入等操作。
post请求的参数传参形式:?拼接,data(body)/params(序列化), // params传参+body传参
序列化:qs.stringify()
前端上传到后端有两种方案:
1)二进制blob appen进formData的形式传输 (formData)
2)base64形式传输(转为base64传输)
相关对象:
files:通过input标签都过来的文件对象,本质上也是blob,是blob的子类,和blob的方法一致
blob:不可变的二进制内容,不能直接将files文件对象传给后端,类似于不能将promise传给后端
formData:用于和后端blob传输的对象.所以就需要formData,搭载blob二进制内容
fileReader:多用于base64传输, 把文件读取为某种形式,如将file转为base64 , text文本
- 上传文件:
<input type='file' id="file">
<div @clcik="submit"><div>
submit(){
const formData = new FormData()
cosnt e = document.getElementById("file")
cosnt data = formData.append('fileName',e.target.files[0])
axios.post({url:'http://xxxx/xxx',data:data })
}
不出意外额话会自动地设置请求头header:
如果没有自动设置
headers: {
'Content-Type': 'multipart/form-data'
})
content-type:mutipart/formData,bourdary—feufheiofifjwfi
- 大文件上传: 分片(分割成很多小个,然后请求很多遍)
注意:form的action直接上传数据(不推荐)
1.会造成页面出现跳转,不太适用于现在开发场景,可以了解一下
- 下载,如果后端返回的二进制流数据是由formData搭载的
要 const blob = new Blob([res.data])//读取并转为blob二进制流内容(也就是像电脑桌面的普通文件内容)
然后在生成一个下载链接 window.URL.createObjectURL(blob)
axios.posttype(url, params, 'arraybuffer').then(res => {
const blob = new Blob([res.data])
const url = window.URL.createObjectURL(blob) // 表示一个指定的file对象或Blob对象
const a = document.createElement('a')
document.body.appendChild(a)
a.href = url
a.download = file.name // 命名下载名称
a.click() // 点击触发下载
window.URL.revokeObjectURL(url) // 下载完成进行释放
})
如果后端直接返回一个blob对象的附件的话,点击触发接口就行
- base64上传下载
上传
function updateFile() {
var fileInput = document.getElementById("selectFile");
if(fileInput.files[0] == undefined){
return;
}
var file = fileInput.files[0];
//FileReader可直接将上传文件二进制流转为base64编码
var reader = new FileReader();
reader.readAsDataURL(file);//二进制流转化编码,异步方法
reader.onload = function(){
var base64Str = this.result;
var startNum = base64Str.indexOf("base64,");
startNum = startNum*1 + 7;
//去除前部格式信息(如果有需求)
var baseStr = base64Str.slice(startNum);
}
下载
<a>标签的href属性设置为base64编码,并设置download属性,触发点击事件即可完成浏览器下载。
// 完整的base64编码格式:真正描述文件内容的base64编码前面会有类似于 'data:application/pdf;base64,' 的字符串来描述文件MIME类型(媒体类型,即文件类型相关)
const base64 = 'data:application/pdf;base64,sdfjalkscnDSFNKJFdasf...'
// 借助<a>标签实现下载:a标签的href属性可以设置为base64编码,然后同时a标签拥有download属性,a标签点击后的行为就是让浏览器下载资源(download的属性值为下载后的文件名),而不是页面转跳
let a = document.createElement('a');
a.href = base64;
a.download = 'fileName.png'; // 如果为空,默认文件名为:下载.xxx(后缀名与base64MIME部分指定)
a.click();
a = null; // a标签下载作用用完了,解除对它的引用即释放内存
}
- 移动端上传与下载
uni.chooseImage从本地相册选择图片或使用相机拍照。
input组件 没有type=file
APP端上传(这里用插件)
H5上传
因为产品的需求是在当前页面上传,那么久不用考虑web-view了。最后还是决定用的动态创建input file选择文件。
至此,文件选择已经完成了。接下来就是上传了。找找uni-app的api。1、uni.uploadFile
调用一下api,将选择好的文件传入formData,这样就不仅仅是只支持image、video、audio三种格式的文件(uni.的api是只支持那三种)
下载:
在uni-app的api中有uni.downloadFile和uni.saveFile可以将文件保存到本地,uni.openDocument可以打开文件。但是这三个api都不支持H5。那就先用两个方法先解决APP端的下载和预览。
APP端的下载完全就是调用api了,没什么问题了。下面开始H5的下载和预览,我使用的是a标签download。一开始我调用的下载接口,后端返回的是文件的二进制流,在PC端测试的时候是可以下载的,但是一放到app中访问就不行。
后来就去查了一下,原来在Android环境app的webview中是不支持a标签下载blob的。
ok,让后端同事提供一个get请求的接口,直接返回文件,再试一下。
这下文件直接打开了,去文件管理器中查找了一下,发现图片是没有保存下来的,而其他offce文件确实下载下来了。
13、移动端列表无限滚动,数据太多引起页面卡顿如何做性能优化(虚拟滚动、图片懒加载)
因为渲染的Dom节点太多
获取不到document、window对象,需要引入render.js
14、微信小程序echart包太大
分包subpack:true,或压缩
目前mp-weixin、mp-qq、mp-baidu的分包优化
1.配置manifest.json
"mp-weixin": {
"optimization":{"subPackages":true}
}
{
"subPackages": [
{
"root": "subpackage1",
"pages": [
"pages/subpackage1/index"
]
},
{
"root": "subpackage2",
"pages": [
"pages/subpackage2/index"
]
}
]
}
2.配置pages.json
```bash
page:[{ ///主包在page当中写
}]
"subPackages": [//分包在subPackages中写
{
"root": "subpackage1",
"pages": [
"pages/subpackage1/index"
]
},
{
"root": "subpackage2",
"pages": [
"pages/subpackage2/index"
]
}
]
在pages.json中新建数组"subPackages",数组中包含两个参数:
1.root:为子包的根目录,
2.pages:子包由哪些页面组成,参数同pages;
15、uniapp难点,触底下拉数据发生抖动
第一种方法:将 :scroll-top=“scrollTop” 这个属性删除,手指滑动内容区域就不会闪动。
第二种方法:data方法中定义timer:null,在scroll函数中使用延时器,
16、关于Timer定时器业务代码没执行完又开始下一次定时器调用–解决方案
方式一:定时器的使能开关
Timer是定时器,在规定的时间间隔里执行某个方法,但是你方法需要执行时间超过了你定的时间间隔,他并不会继续执行,而是开始执行下一次timer!
比如说现在是12点,timer的方法运行需要5分钟的时间,timer设置的时间间隔为2分钟,到了12:02的时候,timer的方法还没运行完成,他不会接着运行,而是直接开始下一次timer,也就是说你的方法调用了第二遍。
解决方法代码示例:
为了避免这个问题,很多猿在开始执行方法的时候禁用timer, 方法执行完了之后再启用它,这样就能解决这个问题。
private void timer1_Tick(object sender, EventArgs e)
{
timer.enable = false;
//你要执行的方法
timer.enable = true;
}
方式二:定时器执行加锁–1、用标志位 2、加锁;
代码示例如下:
int flag = 0;//全局变量
if (Interlocked.Exchange(ref flag, 1) == 0)
{
//do something
Interlocked.Exchange(ref flag, 0);
}
17、离开页面终止请求,请求拦截终止请求(路由守卫,axios取消请求方法)
18、角色权限与动态路由
meta ,role,addRoutes,window.location.onload
18、搭建框架有么有引入其他库
ruoyi
19、代码部署规范:git版本管理
上线版本与开发版本,分支合并,回滚动
git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit
git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容
在回滚这一操作上看,效果差不多。但是在日后继续 merge 以前的老版本时有区别
git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,之前提交合并的代码仍然存在,导致不能够重新合并
但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入
rebase和merge的区别
Rebase 和 Merge 是 Git 中整合不同分支提交的两种方式。Merge 会创建一个新的合并提交,保留各分支提交历史,看起来更杂乱;而 Rebase 则是将当前分支提交应用到目标分支上,使提交历史更线性清晰,但可能会产生冲突需要手动解决。选择 Merge 保留分支独立性,选择 Rebase 保持线性整洁的提交历史。根据需求和偏好选择合适的整合方式。
git reset --soft 会保留当前更改,而 git reset --hard 则会直接丢弃当前更改,
部署规范
首先是我们开发者开发完之后,提交测试
测试测完之后就会上一个预生产环境
比如说,有一个master分支、一个release的分支
在准备发预生产环境的时候,merge request只给相应的负责人
然后让它合到release分支。然后就有对应的发布人,将他发布到与生产环境
然后预生产环境测试,测试通过之后,再将release的代码合并到主分支然后再去进行测试。
在这个过程中,我们也可以进行代码审查(code review),code view通过再合到master
20、项目规范
当使用 Husky 时,它通过配置 Git 钩子(Git Hooks)来捕获我们的提交事件。具体步骤包括:
1)、在项目中
安装 Husky 和其他必要的工具
,例如lint-staged 和 ESLint:
npm install husky lint-staged eslint --save-dev
2)、在
package.json
文件中添加 Husky 的配置
,指定在哪个 Git 钩子事件中执行哪些任务
,比如在提交前执行lint-staged:
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"linters": {
"*.js": [
"eslint --fix",
"git add"
]
}
}
3)、在项目中配置 ESLint 并定义相应的规则(比如在 .eslintrc.json 文件中定义规则)。
4)、当我们执行 git commit 命令提交代码时,Git 会自动触发 pre-commit 钩子事件,然后 Husky 就可以捕获到该事件,并执行我们配置的 lint-staged 命令,从而进行 ESLint 检查和格式化代码。
21、前端错误类型与vue监控前端错误
语法错误(Syntax Errors):
语法错误是由于代码中的拼写错误、缺少符号或其他语法问题导致的,会在代码解析阶段就被捕获,通常会在控制台或编辑器中显示错误信息。
运行时错误(Runtime Errors):
运行时错误指的是代码在运行时才会发生的错误,可能由于变量未定义、除以零等原因引起,会在运行时阶段被捕获。比如:在 Vue 中,如果在模板中访问一个未定义的变量,比如 {{ undefinedVariable }},会导致运行时错误。
逻辑错误(Logic Errors):
也被称为 bug,这类错误通常不会导致代码崩溃,但会导致程序运行出现预期之外的行为。这类错误通常需要通过测试和调试来定位和修复
网络错误(Network Errors):
络错误通常发生在向服务器请求数据时,可能由网络中断、超时、服务器错误等引起,需要进行适当的网络请求处理和错误处理。
在处理这些错误时,前端开发者可以使用以下方法:
- 使用 try-catch 语句捕获和处理运行时错误。
- 在代码中加入断言以避免逻辑错误。
- 使用浏览器提供的开发者工具来检查和调试语法错误。
- 在网络请求中添加错误处理机制来处理网络错误,比如使用 Promise 的 catch 方法或者 try-catch 包裹 fetch 请求。
22、登录鉴权、角色权限(动态路由与菜单)、按钮权限
在 Vue 项目中实现按钮级别的权限控制通常需要以下步骤:
1)、获取用户权限信息: 在页面加载时,从后端获取当前用户的权限信息,通常是一个权限列表或者权限标识。
2)、定义权限指令(Directive): 在 Vue 中,你可以通过自定义指令来实现按钮级别的权限控制。可以创建一个名为 v-permission 的指令,用于根据用户权限控制按钮的显示和操作权限。
Vue.directive('permission', {
inserted: function (el, binding) {
const { value } = binding;
const hasPermission = /* 从用户权限信息中判断是否拥有该按钮的权限 */;
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
3)、在按钮上应用权限指令: 在页面中需要受权限控制的按钮上使用 v-permission 指令,并传入对应的权限标识。
<button v-permission="'editButton'">编辑按钮</button>
4)、检查按钮权限: 在权限指令中,根据用户的权限信息判断当前用户是否拥有该按钮的权限。如果用户有权限,则按钮正常显示;如果没有权限,可以隐藏或者禁用该按钮。
这样,当页面加载时,权限按钮将根据用户的实际权限情况进行动态控制显示或隐藏,实现了按钮级别的权限控制。
需要注意的是,前端的权限控制只是辅助作用,真正的安全控制还应当放在后端。前端只是根据用户权限信息来显示不同的交互界面,而后端需要对实际的操作进行权限校验。
23、二次封装localStorage
- 注意命名,防止污染
比如我现在一个域名下有两个子项目:
A项目
B项目
且这两个项目都需要存储 userInfo,那要怎么防止这两组数据互相污染呢?所以需要注意命名,在存储的时候加上对应的项目名前缀,或者其他标识符,保证这组数据是唯一的
const PROJECT_NAME = ‘test-project’
localStorage.setItem(
${PROJECT_NAME}_userInfo,
JSON.stringify({ name: ‘lsx’ })
)
- 注意版本,迭代防范
请看一个例子,假如我们存储一段信息,类型是 string
// 存数据
const set = () => {
const info = get()
if (!info) {
localStorage.setItem(
${PROJECT_NAME}_info,
‘info_string’
)
}
}
// 取数据
const get = () => {
const info = localStorage.getItem(
${PROJECT_NAME}_info
)
return info
}
然后项目上线了一段时间,但是这个时候,突然决定要换成 object 类型了,这时候对应的存取方法也变了
// 存数据
const set = () => {
const info = get()
if (!info) {
localStorage.setItem(
${PROJECT_NAME}_info,
JSON.stringify({ name: ‘lsx’ })
)
}
}
// 取数据
const get = () => {
const info = localStorage.getItem(
${PROJECT_NAME}_info
)
return JSON.parse(info)
}
但是这样其实是有隐患的,因为项目已经上线了一段时间,有些用户已经存过这个数据了,且存的是 string 类型,但是新版本上线之后,取数据却用了 object 的方式去取数据,这就导致了JSON.parse(字符串)会报错,影响正常的业务逻辑~
所以最好是加一个版本号,或者做一下错误兼容,这样就能避免了~
const PROJECT_NAME = ‘test-project’
// 每次升级时改变版本号,规则自己定
const VERSION = 1
// 存数据
localStorage.setItem(
${PROJECT_NAME}_userInfo_${VERSION},
JSON.stringify({ name: ‘lsx’ })
)
// 取数据
localStorage.getItem(
${PROJECT_NAME}_userInfo_${VERSION}
)
- 时效性,私密性
时效性,那就是给存进去的数据加一个时效,过了某个时间,这个数据就时效了,方法就是每次存数据进去的时候,加一个时间戳
// 原来
localStorage.setItem(
${PROJECT_NAME}_userInfo,
JSON.stringify({ name: ‘lsx’ })
)
const TIME_OUT = 3 * 60 * 60 * 1000
// 加时间戳
localStorage.setItem(
${PROJECT_NAME}_userInfo,
JSON.stringify({
data: { name: ‘lsx’ },
// 记录当前时间
time: new Date().getTime()
})
)
// 取数据时判断时间戳
const get = () => {
let info = localStorage.getItem(
${PROJECT_NAME}_userInfo_${VERSION}
)
info = JSON.parse(info)
const now = new Date().getTime()
if (now - info.time >= TIME_OUT) {
localStorage.removeItem(
${PROJECT_NAME}_userInfo_${VERSION}
)
return null
}
return info
}
有一些数据我们不得不存在 localStorage 中,但是又不想被用户看到,这时候就需要进行加密了(加密规则自己定)
```bash
// 加密函数
const encrypt = (v) => {}
// 解密函数
const decrypt = (v) => {}
// 存数据
localStorage.setItem(
${PROJECT_NAME}_userInfo_${VERSION},
// 加密
encrypt(JSON.stringify({ name: ‘lsx’ }))
)
// 取数据 解密
decrypt(localStorage.getItem(
${PROJECT_NAME}_userInfo_${VERSION}
))
- 兼容 SSR
SSR 就是服务端渲染,是在服务端运行代码,拼接成一个页面,发送到浏览器去展示出来,所以在服务端是使用不了 localStorage 的,因为不是浏览器环境,所以你像封装一个比较通用的 localStorage,得兼顾 SSR 的情况
// 在 SSR 中使用对象替代 localStorage
const SSRStorage = {
map: {},
setItem(v) {
this.map[key] = v
},
getItem(key) {
return this.map[key]
}
}
let storage = null
// 判断环境
if (!window) {
storage = SSRStorage
} else {
storage = window.localStorage
}