我开源了一个好玩且好用的前端脚手架😏
经过半年的幻想,一个多月的准备,十天的开发,我终于开源了自己的脚手架。
背景
在我最开始学习 React
的时候,使用的脚手架就是 create-react-app
,我想大部分刚开始学的时候都是使用这个脚手架吧。
使用这个脚手架挺适合新手的,零配置,执行该脚手架命令安装特定的模板,安装相关依赖包,通过执行 npm start
即可把项目运行起来。
但是这个脚手架在开发的过程中我要引入相对应的模块,例如要引入一个组件 import NiuBi from '../../../components/niubi.jsx'
,这个路径看起来就很丑,而且编写的时候极度困难,因此我们可以通过 Webpack
配置路径别名,可那时候我哪会配置 Webpack
啊,善于思考的我决定打开百度,发现可以使用 carco
配置 Webpack
,但是发现 carco
版本和 react-script
版本并不兼容,因为这个问题把我折磨了一天,因此这个时刻我想自己去开源一个脚手架的想法从此诞生,虽然那时候的我技术非常菜,不过我现在的技术也菜,但是我胆子大啊!!!😏😏😏
所以在这里我总结一下 create-react-app
脚手架的一些缺点,但是这仅仅是个人观点:
难定制: 如果你需要自定义配置
Webpack
,你需要额外使用第三方工具carco
或者eject
保留全部Webpack
配置文件;模板单一: 模板少而且简单,这意味着我们每次开发都要从零开始;
那么接下来就来看看我的这个脚手架是怎么使用的。
基本使用
全局安装
npm install @obstinate/react-cli -g
该脚手架提供的的全局指令为 crazy
,查看该脚手架帮助,你可以直接使用:
crazy
输入该命令后,输出的是整个脚手架的命令帮助,如下图所示:
创建项目
要想创建项目,你可以执行以下命令来根据你想要的项目:
crazy create <projectName> [options]
例如创建一个名为 moment
,如果当前终端所在的目录下存在同名文件时直接覆盖,你可以执行以下命令:
crazy create moment -f
如果你不想安装该脚手架,你也可以使用 npx
执行,使用 npx @obstinate/react-cli
代替 crazy
命令,例如,你要创建一个项目,你可以执行以下命令:
npx @obstinate/react-cli create moment -f
如果没有输入 -f
,则会在后面的交互信息询问是否覆盖当前已存在的文件夹。
之后便有以下交互信息,你可以根据这些交互选择你想要的模板:
最终生成的文件如下图所示:
当项目安装完成之后你就可以根据控制台的指示去启动你的项目了。
创建文件
通过该脚手架你可以快速创建不同类型的文件,你可以指定创建文件的指定路径,否则则使用默认路径。
要想成创建创建文件,请执行以下指令:
crazy mkdir <type> [router]
其中 type
为必选命令,为你要创建的文件类型,现在可供选择的有 axios、component、page、redux、axios
,router
为可选属性,为创建文件的路径。
具体操作请看下列动图:
输入不同的类型会有不同的默认路径,并且无需你输入文件的后缀名,会根据你的项目生成相对应的文件后缀名,其中最特别的是创建 redux 文件会自动全局导入 reduxer,无需你自己手动导入,方便了日常的开发效率。
灵活配置
与 create-react-app
不同的是,该脚手架提供了自定义 Webpack
和 babel
配置,并通过 webpack-merge
对其进行合并,美中不足的是暂时并还没有提供 env
环境变量,要区分环境你可以在你通过脚手架下来的项目的 webpack.config.js
文件中这样操作:
// 开发环境
const isDevelopment = process.argv.slice(2)[0] === "serve";
module.exports = {
// ...
};
最后一个小提示,如果全局安装失败,检查是否权限不够,可以通过管理员身份打开 cmd 即可解决。
这些就是目前仅有的功能,其他的功能正在逐渐开发中......
未来(画饼)
逐步优化用户体验效果,编写更完美的使用文档;
添加对
vue
的支持;提供更多代码规范化配置选择,例如
husky
;提供单元测试;
添加
env
环境变量配置;增加更多的完美配置,减少用户对项目的额外配置;
添加更多的模板,例如后台管理系统;
将来会考虑开发一些配套的生态,例如组件库;
等等......
如何贡献
项目从开发到现在都是我自己一人在开发,但仅凭一己之力会慢慢变得疲惫,其实现在这个版本早在几天前就已经写好了,就单纯不想写文档一直拖到现在。
所以希望能在这里找到一些志同道合的朋友一起把这个脚手架完善,我希望在不久的将来能创造出一个比 create-react-app
更好玩且好用的脚手架。
本人的联系方式请查看评论区图片。
最后
本人是一个掘金的活跃用户,一天里可能就两三次上 GitHub
,如果你联系不到我,如果你不想添加我微信好友你可以通过掘金里私信我,掘金私信有通知,如果我不忙,我可能很快就能回复到你。
如果该脚手架有什么问题或者有什么想法可以通过 Github 的 issue 给我留言。
如果觉得该项目对你有帮助,也欢迎你给个 star
,让更多的朋友能看到。
如果本篇文章的点赞或者评论较高,后期会考虑出一期文章来讲解如何基于 pnpm + monorepo + webpack
开发的脚手架,如果本篇文章对你有帮助,希望你能随时点个赞,让更多的人看到!!!😉😉😉
最后贴上一些地址:
作者:Moment
来源:juejin.cn/post/7202891949380173880
10 个值得掌握的 reduce 技巧
作为一个前端开发者,一定有接触过 reduce
函数,它是一个强大而实用的数组方法,熟练掌握 reduce
的使用可以在开发中提高开发效率和代码质量。本文介绍的 reduce
的 10 个技巧值得拥有,可以让你少写很多代码!
reduce
方法在数组的每个元素上执行提供的回调函数迭代器。它传入前一个元素计算的返回值,结果是单个值,它是在数组的所有元素上运行迭代器的结果。
迭代器函数逐个遍历数组的元素,在每一步中,迭代器函数将当前数组值添加到上一步的结果中,直到没有更多元素要添加。
语法
参数包含回调函数和可选的初始值,如下:
array.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
callback
(必须):执行数组中每个值(如果没有提供
initialValue
则第一个值除外)的
reducer
函数,包含四个参数
accumulator
(必须):累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,初始值可以通过initialValue
定义,默认为数组的第一个元素值,累加器将保留上一个操作的值,就像静态变量一样currentValue
(必须):数组中正在处理的元素index
(可选):数组中正在处理的当前元素的索引。 如果提供了initialValue
,则起始索引号为0
,否则从索引1
起始。注意:如果没有提供
initialValue
,reduce
会从索引1
的地方开始执行callback
方法,跳过第一个索引。如果提供initialValue
,从索引0
开始。array
(可选):调用reduce()
的数组
initialValue
(可选):作为第一次调用callback
函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用reduce
将报错。
1. 计算数组的最大值和最小值
有很多种方式可以获取数组的最大值或最小值?
使用 Math.max 和 Math.min
使用 Math 的 API 是最简单的方式。
const arrayNumbers = [-1, 10, 6, 5, -3];
const max = Math.max(...arrayNumbers); // 10
const min = Math.min(...arrayNumbers); // -3
console.log(`max=${max}`); // max=10
console.log(`min=${min}`); // min=-3
使用 reduce
一行代码,就可以实现与 Math
的 API 相同的效果。
const arrayNumbers = [-1, 10, 6, 5, -3];
const getMax = (array) => array.reduce((max, num) => (max > num ? max : num));
const getMin = (array) => array.reduce((max, num) => (max < num ? max : num));
const max = getMax(arrayNumbers); // 10
const min = getMin(arrayNumbers); // -3
console.log(`max=${max}`); // max=10
console.log(`min=${min}`); // min=-3
或者写成一个函数:
const arrayNumbers = [-1, 10, 6, 5, -3];
const getMaxOrMin = (array, type = "min") =>
type === "max"
? array.reduce((max, num) => (max > num ? max : num))
: array.reduce((max, num) => (max < num ? max : num));
const max = getMaxOrMin(arrayNumbers, "max"); // 10
const min = getMaxOrMin(arrayNumbers, "min"); // -3
console.log(`max=${max}`); // max=10
console.log(`min=${min}`); // min=-3
2. 数组求和和累加器
使用 reduce
,可以轻松实现多个数相加或累加的功能。
// 数组求和
const sum = (...nums) => {
return nums.reduce((sum, num) => sum + num);
};
// 累加器
const accumulator = (...nums) => {
return nums.reduce((acc, num) => acc * num);
};
const arrayNumbers = [1, 3, 5];
console.log(accumulator(1, 2, 3)); // 6
console.log(accumulator(...arrayNumbers)); // 15
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(...arrayNumbers)); // 9
3. 格式化搜索参数
获取 URL 种的搜索参数是经常要处理的功能。
// url https://www.devpoint.cn/index.shtml?name=devpoint&id=100
// 格式化 search parameters
{
name: "devpoint",
id: "100",
}
常规方式
这是大多数人使用它的方式。
const parseQuery = (search = window.location.search) => {
const query = {};
search
.slice(1)
.split("&")
.forEach((it) => {
const [key, value] = it.split("=");
query[key] = decodeURIComponent(value);
});
return query;
};
console.log(parseQuery("?name=devpoint&id=100")); // { name: 'devpoint', id: '100' }
使用 reduce
const parseQuery = (search = window.location.search) =>
search
.replace(/(^\?)|(&$)/g, "")
.split("&")
.reduce((query, it) => {
const [key, value] = it.split("=");
query[key] = decodeURIComponent(value);
return query;
}, {});
console.log(parseQuery("?name=devpoint&id=100")); // { name: 'devpoint', id: '100' }
4. 反序列化搜索参数
当要跳转到某个链接并为其添加一些搜索参数时,手动拼接的方式不是很方便。如果要串联的参数很多,那将是一场灾难。
const searchObj = {
name: "devpoint",
id: 100,
// ...
};
const strLink = `https://www.devpoint.cn/index.shtml?name=${searchObj.name}&age=${searchObj.id}`;
console.log(strLink); // https://www.devpoint.cn/index.shtml?name=devpoint&age=100
reduce
可以轻松解决这个问题。
const searchObj = {
name: "devpoint",
id: 100,
// ...
};
const stringifySearch = (search = {}) =>
Object.entries(search)
.reduce(
(t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`,
Object.keys(search).length ? "?" : ""
)
.replace(/&$/, "");
const strLink = `https://www.devpoint.cn/index.shtml${stringifySearch(
searchObj
)}`;
console.log(strLink); // https://www.devpoint.cn/index.shtml?name=devpoint&age=100
5. 展平多层嵌套数组
如何展平多层嵌套数组吗?
const array = [1, [2, [3, [4, [5]]]]];
const flatArray = array.flat(Infinity);
console.log(flatArray); // [ 1, 2, 3, 4, 5 ]
如果运行环境支持方法 flat ,则可以直接用,如果不支持,使用 reduce 也可以实现和flat一样的功能。
const array = [1, [2, [3, [4, [5]]]]];
const flat = (arrayNumbers) =>
arrayNumbers.reduce(
(acc, it) => acc.concat(Array.isArray(it) ? flat(it) : it),
[]
);
const flatArray = flat(array);
console.log(flatArray); // [ 1, 2, 3, 4, 5 ]
6. 计算数组成员的数量
如何计算数组中每个成员的个数?即计算重复元素的个数。
const count = (array) =>
array.reduce(
(acc, it) => (acc.set(it, (acc.get(it) || 0) + 1), acc),
new Map()
);
const array = [1, 2, 1, 2, -1, 0, "0", 10, "10"];
console.log(count(array));
这里使用了数据类型 Map
,关于 JavaScript 的这个数据类型,有兴趣可以阅读下文:
上面代码的输出结果如下:
Map(7) {
1 => 2,
2 => 2,
-1 => 1,
0 => 1,
'0' => 1,
10 => 1,
'10' => 1
}
7.获取一个对象的多个属性
这是一个项目开发中比较常遇见的场景。通过 API 获取后端数据,前端很多时候只需要取其中部分的数据。
// 一个有很多属性的对象
const obj = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
// ...
};
// 只是想得到它上面的一些属性来创建一个新的对象
const newObj = {
a: obj.a,
b: obj.b,
c: obj.c,
d: obj.d,
// ...
};
这个时候可以使用
reduce
来解决。
/**
*
* @param {*} obj 原始对象
* @param {*} keys 需要获取的属性值列表,数组形式
* @returns
*/
const getObjectKeys = (obj = {}, keys = []) =>
Object.keys(obj).reduce(
(acc, key) => (keys.includes(key) && (acc[key] = obj[key]), acc),
{}
);
const obj = {
a: 1,
b: 2,
c: 3,
d: 4,
e: 5,
// ...
};
const newObj = getObjectKeys(obj, ["a", "b", "c", "d"]);
console.log(newObj); // { a: 1, b: 2, c: 3, d: 4 }
8.反转字符串
反转字符串是面试中最常问到的 JavaScript 问题之一。
const reverseString = (string) => {
return string.split("").reduceRight((acc, s) => acc + s);
};
const string = "devpoint";
console.log(reverseString(string)); // tniopved
9.数组去重
reduce
也很容易实现数组去重。
const array = [1, 2, 1, 2, -1, 10, 11];
const uniqueArray1 = [...new Set(array)];
const uniqueArray2 = array.reduce(
(acc, it) => (acc.includes(it) ? acc : [...acc, it]),
[]
);
console.log(uniqueArray1); // [ 1, 2, -1, 10, 11 ]
console.log(uniqueArray2); // [ 1, 2, -1, 10, 11 ]
10. 模拟方法 flat
虽然现在的JavaScript有原生方法已经实现了对深度嵌套数组进行扁平化的功能,但是如何才能完整的实现扁平化的功能呢?下面就是使用 reduce
来实现其功能:
// 默认展开一层
Array.prototype.flat2 = function (n = 1) {
const len = this.length;
let count = 0;
let current = this;
if (!len || n === 0) {
return current;
}
// 确认当前是否有数组项
const hasArray = () => current.some((it) => Array.isArray(it));
// 每次循环后展开一层
while (count++ < n && hasArray()) {
current = current.reduce((result, it) => result.concat(it), []);
}
return current;
};
const array = [1, [2, [3, [4, [5]]]]];
// 展开一层
console.log(array.flat()); // [ 1, 2, [ 3, [ 4, [ 5 ] ] ] ]
console.log(array.flat2()); // [ 1, 2, [ 3, [ 4, [ 5 ] ] ] ]
// 展开所有
console.log(array.flat(Infinity)); // [ 1, 2, 3, 4, 5 ]
console.log(array.flat2(Infinity)); // [ 1, 2, 3, 4, 5 ]
作者:天行无忌
来源:https://juejin.cn/post/7202935318457860151
前端程序员是怎么做物联网开发的(下)
mqttController.js
// const mqtt = require('mqtt')
$(document).ready(() => {
// Welcome to request my open interface. When the device is not online, the latest 2000 pieces of data will be returned
$.post("https://larryblog.top/api", {
topic: "getWemosDhtData",
skip: 0
},
(data, textStatus, jqXHR) => {
setData(data.res)
// console.log("line:77 data==> ", data)
},
);
// for (let i = 0; i <= 10; i++) {
// toast.showToast(1, "test")
// }
const options = {
clean: true, // true: 清除会话, false: 保留会话
connectTimeout: 4000, // 超时时间
// Authentication information
clientId: 'userClient_' + generateRandomString(),
username: 'userClient',
password: 'aa995231030',
// You are welcome to use my open mqtt broker(My server is weak but come on you). When connecting, remember to give yourself a personalized clientId to prevent being squeezed out
// Topic rule:
// baseName/deviceId/events
}
// 连接字符串, 通过协议指定使用的连接方式
// ws 未加密 WebSocket 连接
// wss 加密 WebSocket 连接
// mqtt 未加密 TCP 连接
// mqtts 加密 TCP 连接
// wxs 微信小程序连接
// alis 支付宝小程序连接
let timer;
let isShowTip = 1
const connectUrl = 'wss://larryblog.top/mqtt'
const client = mqtt.connect(connectUrl, options)
client.on('connect', (error) => {
console.log('已连接:', error)
toast.showToast("Broker Connected")
timer = setTimeout(onTimeout, 3500);
// 订阅主题
client.subscribe('wemos/dht11', function (err) {
if (!err) {
// 发布消息
client.publish('testtopic', 'getDHTData')
}
})
client.subscribe('home/status/')
client.publish('testtopic', 'Hello mqtt')
})
client.on('reconnect', (error) => {
console.log('正在重连:', error)
toast.showToast(3, "reconnecting...")
})
client.on('error', (error) => {
console.log('连接失败:', error)
toast.showToast(2, "connection failed")
})
client.on('message', (topic, message) => {
// console.log('收到消息:', topic, message.toString())
switch (topic) {
case "wemos/dht11":
const str = message.toString()
const arr = str.split(", "); // 分割字符串
const obj = Object.fromEntries(arr.map(s => s.split(": "))); // 转化为对象
document.getElementById("Temperature").innerHTML = obj.Temperature + " ℃"
optionTemperature.xAxis.data.push(moment().format("MM-DD/HH:mm:ss"))
optionTemperature.xAxis.data.length >= 100 && optionTemperature.xAxis.data.shift()
optionTemperature.series[0].data.length >= 100 && optionTemperature.series[0].data.shift()
optionTemperature.series[0].data.push(parseFloat(obj.Temperature))
ChartTemperature.setOption(optionTemperature, true);
document.getElementById("Humidity").innerHTML = obj.Humidity + " %RH"
optionHumidity.xAxis.data.push(moment().format("MM-DD/HH:mm:ss"))
optionHumidity.xAxis.data.length >= 100 && optionHumidity.xAxis.data.shift()
optionHumidity.series[0].data.length >= 100 && optionHumidity.series[0].data.shift()
optionHumidity.series[0].data.push(parseFloat(obj.Humidity))
ChartHumidity.setOption(optionHumidity, true);
break
case "home/status/":
$("#statusText").text("device online")
deviceOnline()
$(".statusLight").removeClass("off")
$(".statusLight").addClass("on")
clearTimeout(timer);
timer = setTimeout(onTimeout, 3500);
break
}
})
function deviceOnline() {
if (isShowTip) {
toast.showToast(1, "device online")
}
isShowTip = 0
}
function setData(data) {
// console.log("line:136 data==> ", data)
for (let i = data.length - 1; i >= 0; i--) {
let item = data[i]
// console.log("line:138 item==> ", item)
optionTemperature.series[0].data.push(item.temperature)
optionHumidity.series[0].data.push(item.humidity)
optionHumidity.xAxis.data.push(moment(item.updateDatetime).format("MM-DD/HH:mm:ss"))
optionTemperature.xAxis.data.push(moment(item.updateDatetime).format("MM-DD/HH:mm:ss"))
}
ChartTemperature.setOption(optionTemperature);
ChartHumidity.setOption(optionHumidity);
}
function onTimeout() {
$("#statusText").text("device offline")
toast.showToast(3, "device offline")
isShowTip = 1
document.getElementById("Temperature").innerHTML = "No data"
document.getElementById("Humidity").innerHTML = "No data"
$(".statusLight").removeClass("on")
$(".statusLight").addClass("off")
}
function generateRandomString() {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for (let i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
});
showTip.js 是我发布在npm上的一个包,如果有需要可以自行npm下载
style.less
* {
padding: 0;
margin: 0;
color: #fff;
}
.app {
background: #1b2028;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
#deviceStatus {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
.statusLight {
display: block;
height: 10px;
width: 10px;
border-radius: 100px;
background: #b8b8b8;
&.on {
background: #00a890;
}
&.off {
background: #b8b8b8;
}
}
}
.container {
width: 100%;
height: 0;
flex: 1;
display: flex;
@media screen and (max-width: 768px) {
flex-direction: column;
}
>div {
flex: 1;
height: 100%;
text-align: center;
#echartsViewTemperature,
#echartsViewHumidity {
width: 80%;
height: 50%;
margin: 10px auto;
// background: #eee;
}
}
}
}
echarts.js 这个文件是我自己写的,别学我这种命名方式,这是反例
let optionTemperature = null
let ChartTemperature = null
$(document).ready(() => {
setTimeout(() => {
// waiting
ChartTemperature = echarts.init(document.getElementById('echartsViewTemperature'));
ChartHumidity = echarts.init(document.getElementById('echartsViewHumidity'));
// 指定图表的配置项和数据
optionTemperature = {
textStyle: {
color: '#fff'
},
tooltip: {
trigger: 'axis',
// transitionDuration: 0,
backgroundColor: '#fff',
textStyle: {
color: "#333",
align: "left"
},
},
xAxis: {
min: 0,
data: [],
boundaryGap: false,
splitLine: {
show: false
},
axisLine: {
lineStyle: {
color: '#fff'
}
}
},
yAxis: {
splitLine: {
show: false
},
axisTick: {
show: false // 隐藏 y 轴的刻度线
},
axisLine: {
show: false,
lineStyle: {
color: '#fff'
}
}
},
grid: {
// 为了让标尺和提示框在图表外面,需要将图表向外扩展一点
left: '10%',
right: '5%',
bottom: '5%',
top: '5%',
containLabel: true,
},
series: [{
// clipOverflow: false,
name: '温度',
type: 'line',
smooth: true,
symbol: 'none',
data: [],
itemStyle: {
color: '#00a890'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: '#00a89066' // 0% 处的颜色
}, {
offset: 1,
color: '#00a89000' // 100% 处的颜色
}],
global: false // 缺省为 false
}
},
hoverAnimation: true,
label: {
show: false,
},
markLine: {
symbol: ['none', 'none'],
data: [
{
type: 'average',
name: '平均值',
},
],
},
}]
};
optionHumidity = {
textStyle: {
color: '#fff'
},
tooltip: {
trigger: 'axis',
backgroundColor: '#fff',
textStyle: {
color: "#333",
align: "left"
},
},
xAxis: {
min: 0,
data: [],
boundaryGap: false,
splitLine: {
show: false
},
axisTick: {
//x轴刻度相关设置
alignWithLabel: true,
},
axisLine: {
lineStyle: {
color: '#fff'
}
}
},
yAxis: {
splitLine: {
show: false
},
axisTick: {
show: false // 隐藏 y 轴的刻度线
},
axisLine: {
show: false,
lineStyle: {
color: '#fff'
}
}
},
grid: {
// 为了让标尺和提示框在图表外面,需要将图表向外扩展一点
left: '5%',
right: '5%',
bottom: '5%',
top: '5%',
containLabel: true,
},
// toolbox: {
// feature: {
// dataZoom: {},
// brush: {
// type: ['lineX', 'clear'],
// },
// },
// },
series: [{
clipOverflow: false,
name: '湿度',
type: 'line',
smooth: true,
symbol: 'none',
data: [],
itemStyle: {
color: '#ffa74b'
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0,
color: '#ffa74b66' // 0% 处的颜色
}, {
offset: 1,
color: '#ffa74b00' // 100% 处的颜色
}],
global: false // 缺省为 false
}
},
hoverAnimation: true,
label: {
show: false,
},
markLine: {
symbol: ['none', 'none'],
data: [
{
type: 'average',
name: '平均值',
},
],
},
}]
};
// 使用刚指定的配置项和数据显示图表。
ChartTemperature.setOption(optionTemperature);
ChartHumidity.setOption(optionHumidity);
}, 100)
});
当你看到这里,你应该可以在你的前端页面上展示你的板子发来的每一条消息了,但是还远远做不到首图上那种密密麻麻的数据,我并不是把页面开了一天,而是使用了后端和数据库存储了一部分数据。
后端
后端我们分为了两个部分,一个是nodejs的后端程序,一个是nginx代理,这里先讲代理,因为上一步前端的连接需要走这个代理
nginx
如果你没有使用https连接,那么可以不看本节,直接使用未加密的mqtt协议,如果你有自己的域名,且申请了ssl证书,那么可以参考我的nginx配置,配置如下
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
server {
listen 80;
server_name jshub.cn;
#将请求转成https
rewrite ^(.*)$ https://$host$1 permanent;
}
server {
listen 443 ssl;
server_name jshub.cn;
location / {
root /larryzhu/web/release/toolbox;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /mqtt {
proxy_pass http://localhost:8083;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# SSL 协议版本
ssl_protocols TLSv1.2;
# 证书
ssl_certificate /larryzhu/web/keys/9263126_jshub.cn.pem;
# 私钥
ssl_certificate_key /larryzhu/web/keys/9263126_jshub.cn.key;
# ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# ssl_ciphers AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256;
# 与False Start没关系,默认此项开启,此处减少抓包的干扰而关闭
# ssl_session_tickets off;
# return 200 "https ok \n";
}
注意这只是部分配置,切不可全部覆盖你的配置。
如果你不会使用nginx,说明你无需配置 ssl ,直接使用 mqtt协议即可。
后端程序部分
这里以egg.js框架为例
首先需要下载egg.js的插件 egg-emqtt ,直接使用npm下载即可,详细配置和启用方法 参见 MQTT系列实践二 在EGG中使用mqtt
上面教程的方法并不全面,可以下载我的示例,仿照着写一下,因为内容相对复杂,地址:gitee.com/zhu_yongbo/…
其中还包含了 mysql 数据库的连接方法,内有我服务器的地址、mysql开放端口,用户名以及密码,我服务器还剩不到十天到期,有缘人看到我的文章可以对我的服务器为所欲为,没有什么重要数据。
mysql
mysql方面,只需要一个库,一个表即可完成全部工作
如图所示,不复杂,仿照我的建库即可
有一点,比较重要,因为mysql本身不适用于存储量级太大的数据,我们的数据重复的又比较多,可以考虑一下压缩算法,或者添加一个事件(每次插入时检查数据量是否超过一定值)。像我的板子大概正常累计运行了几天的时间(每两秒一条数据),到目前可以看到已经累计了七十万条数据了,如果不是因为我设置了插入事件,这个数据量已经可以明显影响查询速度了。
可以仿照我的事件,语句如下:
DELIMITER $$
CREATE TRIGGER delete_oldest_data
AFTER INSERT ON wemosd1_dht11
FOR EACH ROW
BEGIN
-- 如果数据量超过43200(每两秒插入一条,这是一天的量)条,调用存储过程删除最早的一条数据
IF (SELECT COUNT(*) FROM wemosd1_dht11) > 43200 THEN
CALL delete_oldest();
END IF;
END$$
DELIMITER ;
-- 创建存储过程
CREATE PROCEDURE delete_oldest()
BEGIN
-- 删除最早的一条数据
delete from wemosd1_dht11 order by id asc limit 1
END$$
DELIMITER ;
BTW:这是chatGPT教我的,我只进行了一点小小的修改。
这样做会删除id比较小的数据,然后就会导致,id会增长的越来越大,好处是可以看到一共累计了多少条数据。但是如果你不想让id累计,那么可以选择重建id,具体做法,建议你咨询一下chatGPT
结语
至此,我们已经完成了前端、后端、设备端三端连通。
我们整体梳理一下数据是怎么一步一步来到我们眼前的:
首先wemos d1开发板会在DHT11温湿度传感器上读取温湿度值,然后开发板把数据通过mqtt广播给某topic,我们的前后端都订阅了此topic,后端收到后,把处理过的数据存入mysql,前端直接使用echarts进行展示,当前端启动时,还可以向后端程序查询历史数据,比如前8000条数据,之后的变化由在线的开发板提供,我们就得到了一个实时的,并且能看到历史数据的温湿度在线大屏。
作者:加伊juejin
来源:juejin.cn/post/7203180003471081531
前端程序员是怎么做物联网开发的(上)
前端程序员是怎么做物联网开发的
上图是我历时一周做的在线的温湿度可视化项目,可以查看截至目前往前一天的温度、湿度变化趋势,并且实时更新当前温湿度
本文可能含有知识诅咒
概述和基础讲解
该项目用到的技术有:
前端:jq、less、echarts、mqtt.js
后端:eggjs、egg-emqtt
数据库:mysql
服务器:emqx(mqtt broker)
硬件:
板子:wemos D1 R2(设计基于 Arduino Uno R3 , 搭载esp8266 wifi模块)
调试工具:mqttx、Arduino IDE v2.0.3 使用Arduino C开发
必备知识:
nodejs(eggjs框架)能面向业务即可
mysql 能写基本插入查询语句即可
C语言的基本语法了解即可
知道mqtt协议的运作方式即可
arduino 开发板或任何其他电路板的初步了解即可
简单介绍一下上面几个的知识点:
从来没有后端学习经验的同学,推荐一个全栈项目供你参考:vue-xmw-admin-pro ,该项目用到了 前端VUE、后端eggjs、mysql、redis,对全栈学习很有帮助。
mysql 只需要知道最简单的插入和查询语句即可,在本项目中,其实使用mongodb是更合适的,但是我为了方便,直接用了现成的mysql
即使你不知道C语言的基本语法,也可以在一小时内快速了解一下,知道简单的定义变量、函数、返回值即可
MQTT(消息队列遥测传输)是一种网络协议(长连接,意思就是除了客户端可以主动向服务器通信外,服务器也可以主动向客户端发起),也是基于TCP/IP的,适用于算力低下的硬件设备使用,基于发布\订阅范式的消息协议,具体示例如下:
当某客户端想发布消息时,图大概长这样:
由上图可知,当客户端通过验证上线后,还需要订阅主题,当某客户端向某主题发布消息时,只有订阅了该主题的客户端会收到broker的转发。
举一个简单的例子:你和我,还有他,我们把自己的名字、学号报告给门卫大爷(broker),门卫大爷就同意我们在警卫室玩一会,警卫室有无数块黑板(topic),我们每个人都可以向门卫请求:如果某黑板上被人写了字,请转告给我。门卫会记住每个人的要求,比如当你向一块黑板写了字(你向某topic发送了消息),所有要求门卫告诉的人都会被门卫告知你写了什么(如果你也要求被告知,那么也包括你自己)。
开发板可以被写入程序,程序可以使用简单的代码控制某个针脚的高低电平,或者读取某针脚的数据。
开始
购买 wemos d1开发板、DHT11温湿度传感器,共计19.3元。
使用arduino ide(以下简称ide) 对wemos d1编程需要下载esp8266依赖 参见:Arduino IDE安装esp8266 SDK
在ide的菜单栏选择:文件>首选项>其他开发板管理器地址填入:arduino.esp8266.com/stable/pack…,可以顺便改个中文
安装ch340驱动参见: win10 安装 CH340驱动 实测win11同样可用
使用 micro-usb 线,连接电脑和开发板,在ide菜单中选择:工具>开发板>esp8266>LOLIN(WEMOS) D1 R2 & mini
选择端口,按win+x,打开设备管理器,查看你的ch340在哪个端口,在ide中选择对应的端口
当ide右下角显示LOLIN(WEMOS) D1 R2 & mini 在comXX上时,连接就成功了
打开ide菜单栏 :文件>示例>esp8266>blink,此时ide会打开新窗口,在新窗口点击左上角的上传按钮,等待上传完成,当板子上的灯一闪一闪,就表明:环境、设置、板子都没问题,可以开始编程了,如果报错,那么一定是哪一步出问题了,我相信你能够根据错误提示找出到底是什么问题,如果实在找不出问题,那么可能买到了坏的板子(故障率还是蛮高的)
wemos d1 针脚中有一个 3.3v电源输出,三个或更多的GND接地口,当安装DHT11传感器元件时,需要将正极插入3.3v口,负极插入GND口,中间的数据线插入随便的数字输入口,比如D5口(D5口的PIN值是14,后面会用到)。
使用DHT11传感器,需要安装库:DHT sensor library by Adafruit , 在ide的左侧栏中的库管理中直接搜索安装即可
下面是一个获取DHT11数据的简单示例,如果正常的话,在串口监视器中,会每秒输出温湿度数据
#include "DHT.h" //这是依赖或者叫库,或者叫驱动也行
#include "string.h"
#define DHTPIN 14 // DHT11数据引脚连接到D5引脚 D5引脚的PIN值是14
#define DHTTYPE DHT11 // 定义DHT11传感器
DHT dht(DHTPIN, DHTTYPE); //初始化传感器
void setup() {
Serial.begin(115200);
//wemos d1 的波特率是 115200
pinMode(BUILTIN_LED, OUTPUT); //设置一个输出的LED
dht.begin(); //启动传感器
}
char* getDHT11Data() {
float h = dht.readHumidity(); //获取湿度值
float t = dht.readTemperature(); //获取温度值
static char data[100];
if (isnan(h) || isnan(t)) {
Serial.println("Failed to read from DHT sensor!");
sprintf(data, "Temperature: %.1f, Humidity: %.1f", 0.0, 0.0); //如果任何一个值没有值,直接返回两个0.0,这样我们就知道传感器可能出问题了
return data;
}
sprintf(data, "Temperature: %.1f, Humidity: %.1f", t, h); //正常就取到值,我这里拼成了一句话
return data;
}
void loop() {
char* data = getDHT11Data(); //此处去取传感器值
Serial.println("got: " + String(data)); // 打印主题内容
delay(1000); //每次循环延迟一秒
}
继续
到了这一步,如果你用的是普通的arduino uno r3板子,就可以结束了。
取到数据之后,你就可以根据数据做一些其他的事情了,比如打开接在d6引脚上的继电器,而这个继电器控制着一个加湿器。
如果你跟我一样,使用了带wifi网络的板子,就可以继续跟我做。
我们继续分步操作:
设备端:
引入esp8266库(上面已经提到安装过程)
#include "ESP8266WiFi.h"
复制代码
安装mqtt客户端库 ,直接在库商店搜索 PubSubClient ,下载 PubSubClient by Nick O'Leary 那一项,下载完成后:
#include "PubSubClient.h"
复制代码
至此,库文件已全部安装引入完毕
设置 wifi ssid(即名字) 和 密码,如:
char* ssid = "2104";
char* passwd = "13912428897";
复制代码
尝试连接 wifi
WiFiClient espClient;
int isConnect = 0;
void connectWIFI() {
isConnect = 0;
WiFi.mode(WIFI_STA); //不知道什么意思,照着写就完了
WiFi.begin(ssid, passwd); //尝试连接
int timeCount = 0; //尝试次数
while (WiFi.status() != WL_CONNECTED) { //如果没有连上,继续循环
for (int i = 200; i <= 255; i++) {
analogWrite(BUILTIN_LED, i);
delay(2);
}
for (int i = 255; i >= 200; i--) {
analogWrite(BUILTIN_LED, i);
delay(2);
}
// 上两个循环共计200ms左右,在控制LED闪烁而已,你也可以不写
Serial.println("wifi connecting......" + String(timeCount));
timeCount++;
isConnect = 1; //每次都需要把连接状态码设置一下,只有连不上时设置为0
// digitalWrite(BUILTIN_LED, LOW);
if (timeCount >= 200) {
// 当40000毫秒时还没连上,就不连了
isConnect = 0; //设置状态码为 0
break;
}
}
if (isConnect == 1) {
Serial.println("Connect to wifi successfully!" + String("SSID is ") + WiFi.SSID());
Serial.println(String("mac address is ") + WiFi.macAddress());
// digitalWrite(BUILTIN_LED, LOW);
analogWrite(BUILTIN_LED, 250); //设置LED常亮,250的亮度对我来说已经很合适了
settMqttConfig(); //尝试连接mqtt服务器,在下一步有详细代码
} else {
analogWrite(BUILTIN_LED, 255); //设置LED常灭,不要问我为什么255是常灭,因为我的灯是高电平熄灭的
//连接wifi失败,等待一分钟重连
delay(60000);
}
}
复制代码
尝试连接 mqtt
const char* mqtt_server = "larryblog.top"; //这里是我的服务器,当你看到这篇文章的时候,很可能已经没了,因为我的服务器还剩11天到期
const char* TOPIC = "testtopic"; // 设置信息主题
const char* client_id = "mqttx_3b2687d2"; //client_id不可重复,可以随便取,相当于你的网名
PubSubClient client(espClient);
void settMqttConfig() {
client.setServer(mqtt_server, 1883); //设定MQTT服务器与使用的端口,1883是默认的MQTT端口
client.setCallback(onMessage); //设置收信函数,当订阅的主题有消息进来时,会进这个函数
Serial.println("try connect mqtt broker");
client.connect(client_id, "wemos", "aa995231030"); //后两个参数是用户名密码
client.subscribe(TOPIC); //订阅主题
Serial.println("mqtt connected"); //一切正常的话,就连上了
}
//收信函数
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic); // 打印主题信息
Serial.print("]:");
char* payloadStr = (char*)malloc(length + 1);
memcpy(payloadStr, payload, length);
payloadStr[length] = '\0';
Serial.println(payloadStr); // 打印主题内容
if (strcmp(payloadStr, (char*)"getDHTData") == 0) {
char* data = getDHT11Data();
Serial.println("got: " + String(data)); // 打印主题内容
client.publish("wemos/dht11", data);
}
free(payloadStr); // 释放内存
}
复制代码
发送消息
client.publish("home/status/", "{device:client_id,'status':'on'}");
//注意,这里向另外一个主题发送的消息,消息内容就是设备在线,当有其他的客户端(比如web端)订阅了此主题,便能收到此消息
复制代码
至此,板子上的代码基本上就写完了,完整代码如下:
#include "ESP8266WiFi.h"
#include "PubSubClient.h"
#include "DHT.h"
#include "string.h"
#define DHTPIN 14 // DHT11数据引脚连接到D5引脚
#define DHTTYPE DHT11 // DHT11传感器
DHT dht(DHTPIN, DHTTYPE);
char* ssid = "2104";
char* passwd = "13912428897";
const char* mqtt_server = "larryblog.top";
const char* TOPIC = "testtopic"; // 订阅信息主题
const char* client_id = "mqttx_3b2687d2";
int isConnect = 0;
WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
void setup() {
Serial.begin(115200);
// Set WiFi to station mode
connectWIFI();
pinMode(BUILTIN_LED, OUTPUT);
dht.begin();
}
char* getDHT11Data() {
float h = dht.readHumidity();
float t = dht.readTemperature();
static char data[100];
if (isnan(h) || isnan(t)) {
Serial.println("Failed to read from DHT sensor!");
sprintf(data, "Temperature: %.1f, Humidity: %.1f", 0.0, 0.0);
return data;
}
sprintf(data, "Temperature: %.1f, Humidity: %.1f", t, h);
return data;
}
void connectWIFI() {
isConnect = 0;
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, passwd);
int timeCount = 0;
while (WiFi.status() != WL_CONNECTED) {
for (int i = 200; i <= 255; i++) {
analogWrite(BUILTIN_LED, i);
delay(2);
}
for (int i = 255; i >= 200; i--) {
analogWrite(BUILTIN_LED, i);
delay(2);
}
// 上两个循环共计200ms左右
Serial.println("wifi connecting......" + String(timeCount));
timeCount++;
isConnect = 1;
// digitalWrite(BUILTIN_LED, LOW);
if (timeCount >= 200) {
// 当40000毫秒时还没连上,就不连了
isConnect = 0;
break;
}
}
if (isConnect == 1) {
Serial.println("Connect to wifi successfully!" + String("SSID is ") + WiFi.SSID());
Serial.println(String("mac address is ") + WiFi.macAddress());
// digitalWrite(BUILTIN_LED, LOW);
analogWrite(BUILTIN_LED, 250);
settMqttConfig();
} else {
analogWrite(BUILTIN_LED, 255);
//连接wifi失败,等待一分钟重连
delay(60000);
}
}
void settMqttConfig() {
client.setServer(mqtt_server, 1883); //设定MQTT服务器与使用的端口,1883是默认的MQTT端口
client.setCallback(onMessage);
Serial.println("try connect mqtt broker");
client.connect(client_id, "wemos", "aa995231030");
client.subscribe(TOPIC);
Serial.println("mqtt connected");
}
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic); // 打印主题信息
Serial.print("]:");
char* payloadStr = (char*)malloc(length + 1);
memcpy(payloadStr, payload, length);
payloadStr[length] = '\0';
Serial.println(payloadStr); // 打印主题内容
if (strcmp(payloadStr, (char*)"getDHTData") == 0) {
char* data = getDHT11Data();
Serial.println("got: " + String(data)); // 打印主题内容
client.publish("wemos/dht11", data);
}
free(payloadStr); // 释放内存
}
void publishDhtData() {
char* data = getDHT11Data();
Serial.println("got: " + String(data)); // 打印主题内容
client.publish("wemos/dht11", data);
delay(2000);
}
void reconnect() {
Serial.print("Attempting MQTT connection...");
// Attempt to connect
if (client.connect(client_id, "wemos", "aa995231030")) {
Serial.println("reconnected successfully");
// 连接成功时订阅主题
client.subscribe(TOPIC);
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
Serial.println(" try again in 5 seconds");
// Wait 5 seconds before retrying
delay(5000);
}
}
void loop() {
if (!client.connected() && isConnect == 1) {
reconnect();
}
if (WiFi.status() != WL_CONNECTED) {
connectWIFI();
}
client.loop();
publishDhtData();
long now = millis();
if (now - lastMsg > 2000) {
lastMsg = now;
client.publish("home/status/", "{device:client_id,'status':'on'}");
}
// Wait a bit before scanning again
delay(1000);
}
服务器
刚才的一同操作很可能让人一头雾水,相信大家对上面mqtt的操作还是一知半解的,不过没有关系,通过对服务端的设置,你会对mqtt的机制了解的更加透彻
我们需要在服务端部署 mqtt broker,也就是mqtt的消息中心服务器
在网络上搜索 emqx , 点击 EMQX: 大规模分布式物联网 MQTT 消息服务器 ,这是一个带有可视化界面的软件,而且画面特别精美,操作特别丝滑,功能相当强大,使用起来基本上没有心智负担。点击立即下载,并选择适合你的服务器系统的版本:
这里拿 ubuntu和windows说明举例,相信其他系统也都大差不差
在ubuntu上,推荐使用apt下载,按上图步骤操作即可,如中途遇到其他问题,请自行解决
sudo ufw status 查看开放端口,一般情况下,你只会看到几个你手动开放过的端口,或者只有80、443端口
udo ufw allow 18083 此端口是 emqx dashboard 使用的端口,开启此端口后,可以在外网访问 emqx看板控制台
当你看到如图所示的画面,说明已经开启成功了
windows下直接下载安装包,上传到服务器,双击安装即可
打开 “高级安全Windows Defender 防火墙”,点击入站规则>新建规则
点击端口 > 下一步
点击TCP、特定本地端口 、输入18083,点击下一步
一直下一步到最后一步,输入名称,推荐输入 emqx 即可
当你看到如图所示画面,说明你已经配置成功了。
完成服务端程序安装和防火墙端口配置后,我们需要配置服务器后台的安全策略,这里拿阿里云举例:
如果你是 ESC 云主机,点击实例>点击你的服务器名>安全组>配置规则>手动添加
添加这么一条即可:
如果你是轻量服务器,点击安全>防火墙>添加规则 即可,跟esc设置大差不差。
完成后,可以在本地浏览器尝试访问你的emqx控制台
直接输入域名:18083即可,初始用户名为admin,初始密码为public,登录完成后,你便会看到如下画面
接下来需要配置 客户端登录名和密码,比如刚刚在设备中写的用户名密码,就是在这个系统中设置的
点击 访问控制>认证 > 创建,然后无脑下一步即可,完成后你会看到如下画面
点击用户管理,添加用户即可,用户名和密码都是自定义的,这些用户名密码可以分配给设备端、客户端、服务端、测试端使用,可以参考我的配置
userClient是准备给前端页面用的 ,server是给后端用的,995231030是我个人自留的超级用户,wemos是设备用的,即上面设备连接时输入的用户名密码。
至此,emqx 控制台配置完成。
下载 mqttx,作为测试端尝试连接一下
点击连接,你会发现,根本连接不上......
因为,1883(mqtt默认端口)也是没有开启的,当然,和开启18083的方法一样。
同时,还建议你开启:
1803 websocket 默认端口
1804 websockets 默认端口
3306 mysql默认端口
后面这四个端口都会用到。
当你开启完成后,再次尝试使用mqttx连接broker,会发现可以连接了
这个页面的功能也是很易懂的,我们在左侧添加订阅,右侧的聊天框里会出现该topic的消息
你是否还记得,在上面的设备代码中,我们在loop中每一秒向 home/status/ 发送一条设备在线的提示,我们现在在这里就收到了。
当你看到这些消息的时候,就说明,你的设备、服务器、emqx控制台已经跑通了。
前后端以及数据库
前端
前端不必多说,我们使用echarts承载展示数据,由于体量较小,我们不使用任何框架,直接使用jq和echarts实现,这里主要讲前端怎么连接mqtt
首先引入mqtt库
然后设置连接参数
const options = {
clean: true, // true: 清除会话, false: 保留会话
connectTimeout: 4000, // 超时时间
clientId: 'userClient_' + generateRandomString(),
//前端客户端很可能比较多,所以这里我们生成一个随机的6位字母加数字作为clientId,以保证不会重复
username: 'userClient',
password: 'aa995231030',
}
function generateRandomString() {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for (let i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
连接
// const connectUrl = 'mqtt://larryblog.top/mqtt' 当然你可以使用mqtt协议,但是有可能会遇到 ssl 跨域的问题,如果你不使用 https 可以忽略这一项,直接使用mqtt即可
const connectUrl = 'wss://larryblog.top/mqtt' //注意,这里使用了nginx进行转发,后面会讲
const client = mqtt.connect(connectUrl, options)
因为前端代码不多,我这里直接贴了
html:
index.html
<span class="http"><span class="properties"><span class="hljs-attr">wemos</span> <span class="hljs-string">d1 test</span></span></span>
Loading device status
Current temperature:
loading...
Current humidity:
loading...
作者:加伊juejin
来源:juejin.cn/post/7203180003471081531
2023面试真题之CSS篇
恐惧就是这样,你直视它、向前一步的时候它就消失了,选择相信自己能克服一切困难,勇敢向前,直面恐惧,就会发现之前的害怕,变成了心里的能量。
大家好,我是柒八九。
今天,我们继续2023前端面试真题系列。我们来谈谈关于CSS的相关知识点。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
你能所学到的知识点
盒模型 推荐阅读指数⭐️⭐️⭐️⭐️
CSS的display属性有哪些值 推荐阅读指数⭐️⭐️⭐️⭐️
position 里面的属性有哪些 推荐阅读指数⭐️⭐️⭐️⭐️
flex里面的属性 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
flex布局的应用场景 推荐阅读指数⭐️⭐️⭐️⭐️
CSS的长度单位有哪些 推荐阅读指数⭐️⭐️⭐️⭐️
水平垂直居中 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
{块级格式化上下文|Block Formatting Context} 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
层叠规则 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
重绘和重排 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
CSS引入方式(4种) 推荐阅读指数⭐️⭐️⭐️⭐️
硬件加速 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
元素超出宽度...处理 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
元素隐藏 推荐阅读指数⭐️⭐️⭐️⭐️⭐️
Chrome支持小于12px 的文字 推荐阅读指数⭐️⭐️⭐️⭐️
好了,天不早了,干点正事哇。
盒模型
一个盒子由四个部分组成:content
、padding
、border
、margin
content
,即
实际内容
,显示文本和图像
content
属性大都是用在::before/::after
这两个伪元素中
padding
,即内边距,内容周围的区域
内边距是透明的
取值不能为负
受盒子的
background
属性影响padding
百分比值无论是水平还是垂直方向均是相对于宽度计算
boreder
,即边框,围绕元素内容的内边距的一条或多条线,由粗细、样式、颜色三部分组成margin
,即外边距,在元素外创建额外的空白,空白通常指不能放其他元素的区域
标准盒模型
盒子总宽度 =
width
+padding
+border
+margin
;盒子总高度 =
height
+padding
+border
+margin
也就是,width/height
只是内容宽高,不包含 padding
和 border
值
IE 怪异盒子模型
盒子总宽度 =
width
+margin
;盒子总高度 =
height
+margin
;
也就是,width/height
包含了 padding
和 border
值
更改盒模型
CSS 中的 box-sizing
属性定义了渲染引擎应该如何计算一个元素的总宽度和总高度
box-sizing: content-box|border-box
复制代码
content-box
(默认值),元素的width/height
不包含padding
,border
,与标准盒子模型表现一致border-box
元素的width/height
包含padding
,border
,与怪异盒子模型表现一致
CSS的display属性有哪些值
CSS
display
属性设置元素是否被视为块或者内联元素以及用于子元素的布局,例如流式布局
、网格布局
或弹性布局
。
形式上,display
属性设置元素的内部和外部的显示类型。
外部类型设置元素参与流式布局;
内部类型设置子元素的布局(子元素的
格式化上下文
)
常见属性值(8个)
inline
:默认block
inline-block
flex
grid
table
list-item
双值的:只有
Firefox70
支持了这一语法
position 里面的属性有哪些
定义和用法:
position
属性规定元素的定位类型。
说明:这个属性定义建立元素布局所用的 定位机制 。
任何元素都可以定位
绝对或固定元素会生成一个块级框,而不论该元素本身是什么类型。
相对定位元素会相对于它在正常流中的默认位置偏移。
position
有以下可选值:(6个)
CSS 有三种基本的定位机制:普通流、浮动和绝对定位。
flex里面的属性
容器的属性 (6个)
flex-direction
决定主轴的方向(即项目的排列方向)
row
(默认值):主轴为水平方向,起点在左端。row-reverse
:主轴为水平方向,起点在右端。column
:主轴为垂直方向,起点在上沿。column-reverse
:主轴为垂直方向,起点在下沿。
flex-wrap
flex-wrap
属性定义,如果一条轴线排不下,如何换行nowrap
:(默认):不换行。wrap
:换行,第一行在上方。wrap-reverse
:换行,第一行在下方
flex-flow
flex-flow
属性是flex-direction
属性和flex-wrap
属性的简写形式,默认值为row nowrap
。
justify-content
justify-content
属性定义了项目在主轴上的对齐方式。flex-start
(默认值):左对齐flex-end
:右对齐center
: 居中space-between
:两端对齐,项目之间的间隔都相等。space-around
:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
align-items
align-items
属性定义项目在交叉轴上如何对齐。stretch
(默认值):如果项目未设置高度或设为auto
,将占满整个容器的高度。flex-start
:交叉轴的起点对齐。flex-end
:交叉轴的终点对齐。center
:交叉轴的中点对齐。baseline
: 项目的第一行文字的基线对齐。
align-content
align-content
属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
项目的属性(6个)
order
order
属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
flex-grow
flex-grow
属性定义项目的放大比例默认为0,即如果存在剩余空间,也不放大
如果所有项目的
flex-grow
属性都为1,则它们将等分剩余空间(如果有的话)
flex-shrink
flex-shrink
属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
如果所有项目的
flex-shrink
属性都为1,当空间不足时,都将等比例缩小
flex-basis
flex-basis
属性定义了在分配多余空间之前,项目占据的{主轴空间|main size}。浏览器根据这个属性,计算主轴是否有多余空间。
它的默认值为auto,即项目的本来大小。
flex
flex
属性是flex-grow
,flex-shrink
和flex-basis
的简写,默认值为0 1 auto。后两个属性可选。flex: 1
=flex: 1 1 0%
flex: auto
=flex: 1 1 auto
align-self
flex:1
vs flex:auto
flex:1
和flex:auto
的区别,可以归结于flex-basis:0
和flex-basis:auto
的区别
当设置为0时(绝对弹性元素),此时相当于告诉flex-grow
和flex-shrink
在伸缩的时候不需要考虑我的尺寸
当设置为auto
时(相对弹性元素),此时则需要在伸缩时将元素尺寸纳入考虑
flex布局的应用场景
网格布局
Grid-
display:flex
Grid-Cell -
flex: 1
;flex:1
使得各个子元素可以等比伸缩,flex: 1 = flex: 1 1 0%
百分比布局
col2 -
flex: 0 0 50%
;col3 -
flex: 0 0 33.3%;
圣杯布局
页面从上到下,分成三个部分:头部(
header
),躯干(body
),尾部(footer
)。其中躯干又水平分成三栏,从左到右为:导航、主栏、副栏container
-display: flex
; -flex-direction: column;
-min-height: 100vh
;header/footer
-flex: 0 0 100px;
body
-display: flex;
-flex:1
content
-flex: 1;
ads/av
-flex: 0 0 100px;
nav
-order: -1;
侧边固定宽度
侧边固定宽度,右边自适应
aside1
-flex: 0 0 20%;
body1
-flex:1
流式布局
每行的项目数固定,会自动分行
container2
-display: flex;
-flex-flow: row wrap;
CSS的长度单位有哪些
相对长度
相对长度单位指的是这个单位没有一个固定的值,它的值受到其它元素属性(例如浏览器窗口的大小、父级元素的大小)的影响,在响应式布局方面相对长度单位非常适用
绝对长度
绝对长度单位表示一个真实的物理尺寸,它的大小是固定的,不会因为其它元素尺寸的变化而变化
水平垂直居中
宽&高固定
absolute
+ 负margin
absolute
+margin auto
absolute
+calc
宽&高不固定
absolute
+transform: translate(-50%, -50%);
flex布局
grid 布局
宽&高固定
absolute
+ 负 margin
.parent {
+ position: relative;
}
.child {
width: 300px;
height: 100px;
padding: 20px;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: -70px 0 0 -170px;
}
复制代码
初始位置为方块1的位置
当设置
left、top
为50%的时候,内部子元素为方块2的位置设置
margin
为负数时,使内部子元素到方块3的位置,即中间位置
absolute
+ margin auto
absolute
+ calc
宽&高不固定
absolute
+ transform: translate(-50%, -50%);
.parent {
position: relative;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
复制代码
flex布局
.parent {
display: flex;
justify-content: center;
align-items: center;
}
复制代码
grid布局
.parent {
display:grid;
}
.parent .child{
margin:auto;
}
复制代码
{块级格式化上下文|Block Formatting Context}
{块级格式化上下文|Block Formatting Context}(BFC),它是页面中的一块渲染区域,并且有一套属于自己的渲染规则:(6个)
内部的盒子会在垂直方向一个接一个的放置
对于同一个
BFC
的俩个相邻的盒子的margin会发生重叠,与方向无关。每个元素的左外边距与包含块的左边界相接触(页面布局方向从左到右),即使浮动元素也是如此
BFC
的区域不会与float
的元素区域重叠计算BFC的高度时,浮动子元素也参与计算
BFC
就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然
触发条件 (5个)RFODP
根元素,即
HTML
元素浮动元素:
float
值为left、right
overflow
值不为visible
,为auto
、scroll
、hidden
display
的值为inline-block、table、inline-table、flex、inline-flex、grid、inline-grid
position
的值为absolute
或fixed
应用场景
防止
margin
重叠
将位于同一个BFC的元素,分割到不同的BFC中
高度塌陷
计算BFC的高度时,浮动子元素也参与计算
子元素浮动
父元素
overflow: hidden;
构建BFC
多栏自适应
BFC的区域不会与
float
的元素区域重叠aside
-float:left
main
-margin-left:aside-width
-overflow: hidden
构建BFC
层叠规则
所谓层叠规则,指的是当网页中的元素发生层叠时的表现规则。
z-index
:z-index
属性只有和定位元素(position
不为static
的元素)在一起的时候才有作用。
CSS3
中,z-index
已经并非只对定位元素有效,flex
盒子的子元素也可以设置z-index
属性。
层叠上下文的特性
层叠上下文的层叠水平要比普通元素高
层叠上下文可以阻断元素的混合模式
层叠上下文可以嵌套,内部层叠上下文及其所有元素均受制于外部的层叠上下文
每个层叠上下文和兄弟元素独立
当进行层叠变化或渲染的时候,只需要考虑后代元素
每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的层叠顺序中
层叠上下文的创建(3类)
由一些CSS属性创建
天生派
页面根元素天生具有层叠上下文
根层叠上下文
正统派
z-index
值为数值的定位元素的传统层叠上下文
扩招派
其他CSS3属性
根层叠上下文
指的是页面根元素,页面中所有的元素一定处于至少一个层叠结界中
定位元素与传统层叠上下文
对于position
值为relative/absolute
的定位元素,当z-index
值不是auto
的时候,会创建层叠上下文。
CSS3属性(8个)FOTMFIWS
元素为
flex
布局元素(父元素display:flex|inline-flex
),同时z-index
值不是auto - flex布局元素的
opactity
值不是1 - {透明度|opactity}元素的
transform
值不是none
- {转换|transform}元素
mix-blend-mode
值不是normal
- {混合模式|mix-blend-mode}元素的
filter
值不是none
- {滤镜|filter}元素的
isolation
值是isolate
- {隔离|isolation}元素的
will-change
属性值为上面②~⑥的任意一个(如will-change:opacity
)元素的
-webkit-overflow-scrolling
设为touch
重绘和重排
页面渲染的流程, 简单来说,初次渲染时会经过以下6步:
构建DOM树;
样式计算;
布局定位;
图层分层;
图层绘制;
合成显示;
在CSS属性改变时,重渲染会分为回流、重绘和直接合成三种情况,分别对应从布局定位/图层绘制/合成显示开始,再走一遍上面的流程。
元素的CSS具体发生什么改变,则决定属于上面哪种情况:
回流(又叫重排):元素位置、大小发生变化导致其他节点联动,需要重新计算布局;
重绘:修改了一些不影响布局的属性,比如颜色;
直接合成:合成层的
transform、opacity
修改,只需要将多个图层再次合并,而后生成位图,最终展示到屏幕上;
触发时机
回流触发时机
回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流。
添加或删除可见的DOM元素
元素的位置发生变化
元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
页面一开始渲染的时候(这避免不了)
浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
获取一些特定属性的值
offsetTop、offsetLeft、 offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流。
重绘触发时机
触发回流一定会触发重绘
除此之外还有一些其他引起重绘行为:
颜色的修改
文本方向的修改
阴影的修改
浏览器优化机制
由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列存储重排操作并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。
当你获取布局信息的操作的时候,会强制队列刷新,例如offsetTop
等方法都会返回最新的数据。
因此浏览器不得不清空队列,触发回流重绘来返回正确的值
减少回流
对于那些复杂的动画,对其设置
position: fixed/absolute
,尽可能地使元素脱离文档流,从而减少对其他元素的影响使用css3硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起回流重绘在使用
JavaScript
动态插入多个节点时, 可以使用DocumentFragment
.创建后一次插入.通过设置元素属性
display: none
,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作
CSS引入方式(4种)
内联方式
<div style="background: red"></div>
嵌入方式
在
HTML
头部中的<style>
标签下书写CSS
代码
链接方式
使用
HTML
头部的<head>
标签引入外部的CSS
文件。<link rel="stylesheet" type="text/css" href="style.css">
导入方式
使用
CSS
规则引入外部CSS
文件
比较链接方式和导入方式
链接方式(用 link
)和导入方式(用 @import
)都是引入外部的 CSS
文件的方式
link
属于HTML
,通过<link>
标签中的href
属性来引入外部文件,而@import
属于CSS
,所以导入语句应写在CSS
中,要注意的是导入语句应写在样式表的开头,否则无法正确导入外部文件;@import
是CSS2.1
才出现的概念,所以如果浏览器版本较低,无法正确导入外部样式文件;
当
HTML
文件被加载时,link
引用的文件会同时被加载,而@import
引用的文件则会等页面全部下载完毕再被加载;
硬件加速
浏览器中的层分为两种:渲染层和合成层。
渲染层
渲染层的概念跟层叠上下文密切相关。简单来说,拥有z-index
属性的定位元素会生成一个层叠上下文,一个生成层叠上下文的元素就生成了一个渲染层。
层叠上下文的创建(3类)
由一些CSS属性创建
天生派
页面根元素天生具有层叠上下文
根层叠上下文
正统派
z-index
值为数值的定位元素的传统层叠上下文
扩招派 (CSS3属性)
元素为
flex
布局元素(父元素display:flex|inline-flex
),同时z-index
值不是auto - flex布局元素的
opactity
值不是1 - {透明度|opactity}元素的
transform
值不是none
- {转换|transform}元素
mix-blend-mode
值不是normal
- {混合模式|mix-blend-mode}元素的
filter
值不是none
- {滤镜|filter}元素的
isolation
值是isolate
- {隔离|isolation}元素的
will-change
属性值为上面②~⑥的任意一个(如will-change:opacity
)元素的
-webkit-overflow-scrolling
设为touch
合成层
只有一些特殊的渲染层才会被提升为合成层,通常来说有这些情况:
transform:3D
变换:translate3d
,translateZ
;will-change:opacity | transform | filter
对
opacity
|transform
|fliter
应用了过渡和动画(transition/animation
)video、canvas、iframe
硬件加速
浏览器为什么要分层呢?答案是硬件加速。就是给HTML元素加上某些CSS属性,比如3D变换,将其提升成一个合成层,独立渲染。
之所以叫硬件加速,就是因为合成层会交给GPU(显卡)去处理,在硬件层面上开外挂,比在主线程(CPU)上效率更高。
利用硬件加速,可以把需要重排/重绘的元素单独拎出来,减少绘制的面积。
避免重排/重绘,直接进行合成,合成层的transform
和 opacity
的修改都是直接进入合成阶段的;
可以使用
transform:translate
代替left/top
修改元素的位置;使用
transform:scale
代替宽度、高度的修改;
元素超出宽度...处理
单行 (AKA: TWO)
text-overflow:ellipsis
:当文本溢出时,显示省略符号来代表被修剪的文本white-space:nowrap
:设置文本不换行overflow:hidden
:当子元素内容超过容器宽度高度限制的时候,裁剪的边界是border box
的内边缘
p{
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width:400px;
}
复制代码
多行
基于高度截断(伪元素 + 定位)
基于行数截断()
基于高度截断
关键点 height + line-height + ::after + 子绝父相
核心的css代码结构如下:
.text {
position: relative;
line-height: 20px;
height: 40px;
overflow: hidden;
}
.text::after {
content: "...";
position: absolute;
bottom: 0;
right: 0;
padding: 0 20px 0 10px;
}
复制代码
基于行数截断
关键点:box + line-clamp + box-orient
+ overflow
display: -webkit-box
:将对象作为弹性伸缩盒子模型显示-webkit-line-clamp: n
:和①结合使用,用来限制在一个块元素显示的文本的行数(n
)-webkit-box-orient: vertical
:和①结合使用 ,设置或检索伸缩盒对象的子元素的排列方式overflow: hidden
p {
width: 300px;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
复制代码
元素隐藏
可按照隐藏元素是否占据空间分为两大类(6 + 3)
元素不可见,不占空间
(
3absolute
+
1relative
+
1script
+
1display
)
<script>
display:none
absolute
+visibility:hidden
absolute
+clip:rect(0,0,0,0)
absolute
+opacity:0
relative
+left
负值
元素不可见,占据空间
(3个)
visibility
:hidden
relative
+z-index
负值opacity:0
元素不可见,不占空间
<script>
<script type="text/html">
<img src="1.jpg">
</script>
复制代码
display:none
其他特点:辅助设备无法访问,资源加载,DOM可访问
对一个元素而言,如果display
计算值是none,则该元素以及所有后代元素都隐藏
.hidden {
display:none;
}
复制代码
absolute + visibility
.hidden{
position:absolute;
visibility:hidden;
}
复制代码
absolute + clip
.hidden{
position:absolute;
clip:rect(0,0,0,0);
}
复制代码
absolute + opacity
.hidden{
position:absolute;
opacity:0;
}
复制代码
relative
+负值
.hidden{
position:relative;
left:-999em;
}
复制代码
元素不可见,占据空间
visibility:hidden
visibility
的继承性
父元素设置
visibility:hidden
,子元素也看不见但是,如果子元素设置了
visibility:visible
,则子元素又会显示出来
.hidden{
visibility:hidden;
}
复制代码
relative + z-index
.hidden{
position:relative;
z-index:-1;
}
复制代码
opacity:0
.hidden{
opacity:0;
filter:Alpha(opacity=0)
}
复制代码
总结
最常用的还是display:none
和visibility:hidden
,其他的方式只能认为是奇招,它们的真正用途并不是用于隐藏元素,所以并不推荐使用它们。
关于display: none
、visibility: hidden
、opacity: 0
的区别,如下表所示:
Chrome支持小于12px 的文字
Chrome
中文版浏览器会默认设定页面的最小字号是12px,英文版没有限制
原由 Chrome
团队认为汉字小于12px就会增加识别难度
中文版浏览器 与网页语言无关,取决于用户在Chrome的设置里(
chrome://settings/languages
)把哪种语言设置为默认显示语言系统级最小字号 浏览器默认设定页面的最小字号,用户可以前往
chrome://settings/fonts
根据需求更改
解决方案(3种)
zoom
transform:scale()
-webkit-text-size-adjust:none
zoom
zoom
可以改变页面上元素的尺寸,属于真实尺寸。
其支持的值类型有:
zoom:50%
,表示缩小到原来的一半zoom:0.5
,表示缩小到原来的一半
.span10{
font-size: 12px;
display: inline-block;
zoom: 0.8;
}
复制代码
transform:scale()
用transform:scale()
这个属性进行放缩
使用scale
属性只对可以定义宽高的元素生效,所以,需要将指定元素转为行内块元素
.span10{
font-size: 12px;
display: inline-block;
transform:scale(0.8);
}
复制代码
text-size-adjust
该属性用来设定文字大小是否根据设备(浏览器)来自动调整显示大小
属性值:
auto
:默认,字体大小会根据设备/浏览器来自动调整;percentage
:字体显示的大小none
:字体大小不会自动调整
存在兼容性问题,chrome
受版本限制,safari
可以。
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
作者:前端小魔女
来源:https://juejin.cn/post/7203153899246780453
数据可视化大屏设计器开发-多选拖拽
开头
本文是数据可视化开始的开发细节第五章。关于画布中的元素的各种鼠标拖拽操作。
简单声明
本人只是一个菜鸡,以下方法仅个人思路,如有错误,轻喷🙏🏻 。
开头说明
下面所说的元素表示的是组或者组件的简称。
开始
大屏设计当中,不乏需要调整图表组件的位置和尺寸。
相较于网页低代码,图表大屏低代码可能需要更复杂的操作,比如嵌套成组、多选、单元素拖拽缩放、多元素拖拽缩放。
并且需要针对鼠标的动作做相应的区分,当中包含了相当的细节,这里就一一做相应的讲解。
涉及的依赖
react-rnd
react-rnd是一个包含了拖拽和缩放两个功能的react
组件,并且有非常丰富的配置项。
内部是依赖了拖拽(react-draggable)和缩放(re-resizable)两个模块。
奈何它并没有内置多元素的响应操作,本文就是针对它来实现对应的操作。react-selecto
react-selecto是一个简单的简单易用的多选元素组件。eventemitter3
eventemitter3是一个自定义事件模块,能够在任何地方触发和响应自定义的事件,非常的方便。
相关操作
多选
画布当中可以通过鼠标点击拖拽形成选区,选区内的元素即是被选中的状态。
这里即可以使用react-selecto
来实现此功能。
从图上操作可以看到,在选区内的元素即被选中(会出现黑色边框)。
import ReactSelecto from 'react-selecto';
const Selecto = () => {
return (
<ReactSelecto
// 会被选中元素的父容器 只有这个容器里的元素才会被选中
dragContainer={'#container'}
// 被选择的元素的query
selectableTargets={['.react-select-to']}
// 表示元素有被选中的百分比为多少时才能被选中
hitRate={10}
// 当已经存在选中项时,按住指定按键可进行继续选择
toggleContinueSelect={'shift'}
// 可以通过点击选择元素
selectByClick
// 是否从内部开始选择(?)
selectFromInside
// 拖拽的速率(不知道是不是这个意思)
ratio={0}
// 选择结束
onSelectEnd={handleSelectEnd}
></ReactSelecto>
);
};
复制代码
这里有几个需要注意的地方。
操作互斥
画布当中的多选和拖拽都是通过鼠标左键来完成的,所以当一个元素是被选中的时候,鼠标想从元素上开始拖拽选择组件是不被允许的,此时应该是拖拽元素,而不是多选元素。
而元素如果没有被选中时,上面的操作则变成了多选。
内部选中
画布当中有组的概念,它是一个组与组件无限嵌套的结构,并且可以单独选中组内的元素。
当选中的是组内的元素时,即说明最外层的组是被选中的状态,同样需要考虑上面所说的互斥问题。
单元素拖拽缩放
单元素操作相对简单,只需要简单使用react-rnd
提供的功能即可完成。
多元素拖拽缩放
这里就是本文的重点了,结合前面介绍的几个依赖,实现一个简单的多选拖拽缩放的功能。
具体思路
多个元素拖拽,说到底其实鼠标拖拽的还是一个元素,就是鼠标拖动的那一个元素。
而其余被选中的元素,仅仅需要根据被拖动的元素的尺寸位置变动来做相应的加减处理即可。
相关问题
信息计算
联动元素的位置尺寸信息该如何计算。组件间通信
因为每一个图表组件并非是单纯的同级关系,如果是通过层层props
传递,免不了会有多余的刷新,造成性能问题。
而通过全局的dva
状态同样在更新的时候会让组件刷新。数据刷新
图表数据是来自于dva
全局的数据,现在频繁自刷新相当于是一直更新全局的数据,同样会造成性能问题。其他
一些细节问题
解决方法
信息计算
关于位置的计算相对简单,只需要单纯的将操作的元素的位置和尺寸差值传递给联动组件即可。组件间通信
根据上面问题的解析,可以使用eventemitter3
来完成任意位置、层级的数据通信,并且它和react
渲染无任何关系。
import { useCallback, useEffect } from 'react'
import EventEmitter from 'eventemitter3'
const eventemitter = new EventEmitter()
const SonA = () => {
console.log('刷新')
useEffect(() => {
const listener = (value) => {
console.log(value)
}
eventemitter.addListener('change', listener)
return () => {
eventemitter.removeListener('change', listener)
}
}, [])
return (
<span>son A</span>
)
}
const SonB = () => {
const handleClick = useCallback(() => {
eventemitter.emit('change', 'son B')
}, [])
return (
<span>
<button onClick={handleClick}>son B</button>
</span>
)
}
const Parent = () => {
return (
<div>
<SonA />
<br />
<SonB />
</div>
)
}
运行上面的例子可以发现,点击SonB
组件的按钮,可以让SonA
接收到来自其的数据,并且并没有触发SonA
的刷新。
需要接收数据的组件只需要监听(addListener
)指定的事件即可,比如上面的change
事件。
而需要发送数据的组件则直接发布(emit
)事件即可。
这样就避免了一些不必要的刷新。
数据刷新
频繁刷新全局数据,会导致所有依赖其数据的组件都会刷新,所以考虑为需要刷新数据的组件在内部单独维护一份状态。
当开始操作时,记录下状态,标识开始使用内部状态表示图表的信息,结束操作时处理下内部数据状态,将数据更新到全局中去。
import { useMemo, useEffect, useState, useRef } from 'react'
import EventEmitter from 'eventemitter3'
const eventemitter = new EventEmitter()
const Component = (props: {
position: {left: number, top: number}
}) => {
const [ position, setPosition ] = useState({
left: 0,
top: 0
})
const isDrag = useRef(false)
const dragStart = () => {
isDrag.current = true
setPosition(props.position)
}
const drag = (position) => {
setPosition(position)
}
const dragEnd = () => {
isDrag.current = false
// TODO
// 更新数据到全局
}
useEffect(() => {
eventemitter.addListener('dragStart', dragStart)
eventemitter.addListener('drag', drag)
eventemitter.addListener('dragEnd', dragEnd)
return () => {
eventemitter.removeListener('dragStart', dragStart)
eventemitter.removeListener('drag', drag)
eventemitter.removeListener('dragEnd', dragEnd)
}
}, [])
return (
<span
style={{
left: (isDrag.current ? position : props.position).left,
top: (isDrag.current ? position : props.position).top
}}
>图表组件</span>
)
}
上面的数据更新还可以更加优化,对于短时间的多次更新操作,可以控制一下更新频率,将多次更新合并为一次。
其他
控制刷新
这里的控制刷新指的是上述的内部刷新,不需要每次都响应react-rnd
发出的相关事件,可以做对应的节流(throttle
)操作,减少事件触发频率。通信冲突问题
因为所有的组件都需要监听拖拽的事件,包括当前被拖拽的组件,所以在传递信息时,需要把自身的id
类似值传递,防止冲突。组件的缩放属性
这里是关于前文说到的成组的逻辑相关,因为组存在scaleX
和scaleY
两个属性,所以在调整大小的时候,也要兼顾此属性(本文先暂时不考虑这个问题)。单元素选中情况
自定义事件的监听是无差别的,当只选中了一个元素进行拖拽缩放操作时,无须触发相应的事件。
最后的DEMO
成品
其实在之前就已经发现其实react-selecto
的作者也有研发其他的可视化操作模块,包括本文所说的多选拖拽的操作,但是奈何无法满足本项目的需求,故自己实现了功能。
如果有兴趣可以去看一下这个成品moveable。
总结
通过上面的思路,即可完成一个简单的多元素拖拽缩放的功能,其核心其实就是eventemitter3
的自定义事件功能,它的用途在平常的业务中非常广泛。
比如我们完全可以在以上例子的基础上,加上元素拖拽时吸附的功能。
结束
结束🔚。
顺便在下面附上相关的链接。
作者:写代码请写注释
来源:juejin.cn/post/7202445722972815417
手把手教你实现一个自定义 eslint 规则
ESlint 概述
ESLint
是一个代码检查工具,通过静态的分析,寻找有问题的模式或者代码。默认使用 Espree 解析器将代码解析为 AST
抽象语法树,然后再对代码进行检查。
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
随着前端工程化体系的不断发展,Eslint
已经前端工程化不可缺失的开发工具。它解决了前端工程化中团队代码风格不统一的问题,避免了一些由于代码规范而产生的 Bug
, 同时它提高了了团队的整体效率。
运行机制
Eslint的内部运行机制不算特别复杂,主要分为以下几个部分:
preprocess
,把非 js 文本处理成 js确定
parser
(默认是espree
)调用
parser
,把源码parse
成SourceCode
(AST
)调用
rules
,对SourceCode
进行检查,返回linting problems
扫描出注释中的
directives
,对problems
进行过滤postprocess
,对problems
做一次处理基于字符串替换实现自动
fix
具体描述,这里就不补充了。详细的运行机制推荐大家去学习一下Eslint
的底层实现原理和源码。
常用规则
为了让使用者对规则有个更好的理解, Eslint
官方将常用的规则进行了分类并且定义了一个推荐的规则组 "extends": "eslint:recommended"
。具体规则详情请见官网
示例规则如下:
array-element-newline :
<string|object>
"always
"(默认) - 需要数组元素之间的换行符
"never
" - 不允许数组元素之间换行
"consistent
" - 数组元素之间保持一致的换行符
配置详解
Eslint
配置我们主要通过.eslintrc
配置来描述
extends
extends
的内容为
一个 ESLint
配置文件,一旦扩展了(即从外部引入了其他配置包),就能继承另一个配置文件的所有属性(包括rules
, plugins
, and language
option
在内),然后通过 merge
合并/覆盖所有原本的配置。最终得到的配置是前后继承和覆盖前后配置的并集。
extends
属性的值可以是:
定义一个配置的字符串(配置文件的路径、可共享配置的名称,如
eslint:recommended
或eslint:all
)定义规则组的字符串。
plugin
:插件名/规则名称 (插件名取eslint-plugin-
之后的名称)
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
parserOptions
指定你想要支持的 JavaScript
语言选项。默认支持 ECMAScript 5
语法。你可以覆盖该设置,以启用对 ECMAScript
其它版本和 JSX
的支持。
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
}
rules
ESLint
拥有大量的规则。你可以通过配置插件添加更多规则。使用注释或配置文件修改你项目中要使用的规则。要改变一个规则,你必须将规则 ID 设置为下列值之一:
"off"
或0
- 关闭规则"warn"
或1
- 开启规则,使用警告级别的错误:warn
(不会导致程序退出)"error"
或2
- 开启规则,使用错误级别的错误:error
(当被触发的时候,程序会退出)
"plugins": [
"plugin-demo",
],
"rules": {
"quotes": ["error", "double"], // 修改eslint recommended中quotes规则
"plugin-demo/rule1": "error", // 配置eslint-plugin-plugin-demo 下rule1规则
}
对于 Eslint
recommended
规则组中你不想使用的规则,也可以在这里进行关闭。
plugin
ESLint
支持使用第三方插件。要使用插件,必须先用 npm
进行安装
"plugins": [
"plugin-demo", // // 配置 eslint-plugin-plugin-demo 插件
],
这里做一下补充,extends
和 plugin
的区别在于,extends
是 plugin
的子集。就好比如 Eslint
中除了 recommended
规则组还有其他规则
自定义Eslint插件
团队开发中,我们经常会使用一些 eslint
规则插件来约束代码开发,但偶尔也会有一些个性定制化的团队规范,而这些规范就需要通过一些自定义的 ESlint
插件来实现。
我们先看一段简短的代码:
import { omit } from 'lodash';
上述代码是我们在使用lodash
的一个习惯性写法,但是这段代码会导致全量引入lodash
,造成工程包体积偏大。
正确的引用方式如下:
import omit from 'lodash/omit';
// 或
import { omit } from 'lodash-es';
我们希望可以通过插件去约束开发者的使用习惯。但是 Eslint
自带的规则对于这个定制化的场景就无法满足了。此时, 就需要去使用 Eslint
提供的开放能力去定制化一个 Eslint
规则。接下来我将从创建到使用去实现一个lodash
引用规范的Eslint
自定义插件
创建
工程搭建
Eslint
官方提供了脚手架来简化新规则的开发, 如不使用脚手架搭建,只需保证和脚手架一样的结构就可以啦。
创建工程前,先全局安装两个依赖包:
$ npm i -g yo
$ npm i -g generator-eslint
再执行如下命令生成 Eslint
插件工程。
$ yo eslint:plugin
这是一个交互式命令,需要你填写一些基本信息,如下
$ yo eslint:rule
? What is your name? // guming-eslint-plugin-custom-lodash
? What is the plugin ID? // 插件名 (eslint-plugin-xxx)
? Type a short description of this plugin: // 描述你的插件是干啥的
? Does this plugin contain custom ESLint rules? Yes // 是否为自定义Eslint 校验规则
? Does this plugin contain one or more processors? No // 是否需要处理器
接下来我们为插件创建一条规则,执行如下命令:
$ npx yo eslint:rule
这也是一个交互式命令,如下:
? What is your name? // guming-eslint-plugin-custom-lodash
? Where will this rule be published? ESLint Plugin
? What is the rule ID? // 规则名称 lodash-auto-import
? Type a short description of this rule: // 规则的描述
? Type a short example of the code that will fail: // 这里写你这条规则校验不通过的案例代码
填写完上述信息后, 我们可以得到如下的一个项目目录结构:
guming-eslint
├─ .eslintrc.js
├─ .git
├─ README.md
├─ docs
│ └─ rules
│ └─ lodash-auto-import.md
├─ lib // 规则
│ ├─ index.js
│ └─ rules
│ └─ lodash-auto-import.js
├─ node_modules
├─ package-lock.json
├─ package.json
└─ tests // 单测
└─ lib
└─ rules
└─ lodash-auto-import.js
eslint 规则配置
Eslint
官方制定了一套开发自定义规则的规范。我们只需要根据规范配置相应的内容就可以轻松的实现我们的自定义Eslint
规则。具体配置详情可见官网
相关配置的说明如下:
module.exports = {
meta: {
// 规则的类型 problem|suggestion|layout
// problem: 这条规则识别的代码可能会导致错误或让人迷惑。应该优先解决这个问题
// suggestion: 这条规则识别的代码不会导致错误,但是建议用更好的方式
// layout: 表示这条规则主要关心像空格、分号等这种问题
type: "suggestion",
// 对于自定义规则,docs字段是非必须的
docs: {
description: "描述你的规则是干啥的",
// 规则的分类,假如你把这条规则提交到eslint核心规则里,那eslint官网规则的首页会按照这个字段进行分类展示
category: "Possible Errors",
// 假如你把规则提交到eslint核心规则里
// 且像这样extends: ['eslint:recommended']继承规则的时候,这个属性是true,就会启用这条规则
recommended: true,
// 你的规则使用文档的url
url: "https://eslint.org/docs/rules/no-extra-semi",
},
// 定义提示信息文本 error-name为提示文本的名称 定义后我们可以在规则内部使用这个名称
messages: {
"error-name": "这是一个错误的命名"
},
// 标识这条规则是否可以修复,假如没有这属性,即使你在下面那个create方法里实现了fix功能,eslint也不会帮你修复
fixable: "code",
// 这里定义了这条规则需要的参数
// 比如我们是这样使用带参数的rule的时候,rules: { myRule: ['error', param1, param2....]}
// error后面的就是参数,而参数就是在这里定义的
schema: [],
},
create: function (context) {
// 这是最重要的方法,我们对代码的校验就是在这里做的
return {
// callback functions
};
},
};
本次Eslint 校验规则是推荐使用更好的lodash引用方式,所以常见规则类型 type
为 suggestion
AST 结构
Eslint 的本质是通过代码生成的 AST
树做代码的静态分析,我们可以使用 astexplorer 快速方便地查看解析成 AST
的结构。
我们将如下代码输入
import { omit } from 'lodash'
得到的 AST
结构如下:
{
"type": "Program",
"start": 0,
"end": 29,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 29,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 9,
"end": 13,
"imported": {
"type": "Identifier",
"start": 9,
"end": 13,
"name": "omit"
},
"local": {
"type": "Identifier",
"start": 9,
"end": 13,
"name": "omit"
}
}
],
"source": {
"type": "Literal",
"start": 21,
"end": 29,
"value": "lodash",
"raw": "'lodash'"
}
}
],
"sourceType": "module"
}
分析 AST
的结构,我们可以知道:
type
为 包的引入方式source
为 资源名(依赖包名)specifiers
为导出的模块
节点访问方法
Eslint
规则中的 create
函数create
(function) 返回一个对象,其中包含了 ESLint
在遍历 JavaScript
代码的抽象语法树 AST
(ESTree 定义的 AST) 时,用来访问节点的方法。其中, 访问节点的方法如下:
VariableDeclaration
,则返回声明中声明的所有变量。如果节点是一个
VariableDeclarator
,则返回 declarator 中声明的所有变量。如果节点是
FunctionDeclaration
或FunctionExpression
,除了函数参数的变量外,还返回函数名的变量。如果节点是一个
ArrowFunctionExpression
,则返回参数的变量。如果节点是
ClassDeclaration
或ClassExpression
,则返回类名的变量。如果节点是一个
CatchClause
子句,则返回异常的变量。如果节点是
ImportDeclaration
,则返回其所有说明符的变量。如果节点是
ImportSpecifier
,ImportDefaultSpecifier
或ImportNamespaceSpecifier
,则返回声明的变量。
本次我们是校验资源导入规范,所以我们使用ImportDeclaration
获取我们导入资源的节点结构
代码修复
report()
函数返回一个特定结构的对象,它用来发布警告或错误, 我们可以通过配置对象去配置错误AST
节点,错误提示的内容(可使用 meta
配置的 meaasge
名称)以及修复方式
实例配置代码如下
context.report({
node: node,
message: "Missing semicolon",
fix: function(fixer) {
return fixer.insertTextAfter(node, ";");
}
});
编写代码
了解完上述内容,我们就可以开始愉快的编写代码了。
自定义规则代码如下:
// lib/rules/lodash-auto-import.js
/**
* @fileoverview 这是一个lodash按需引入的eslint规则
* @author guming-eslint
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
const SOURCElIST = ["lodash", "lodash-es"];
module.exports = {
// eslint-disable-next-line eslint-plugin/prefer-message-ids
meta: {
type: "suggestion", // `problem`, `suggestion`, or `layout`
docs: {
description: "这是一个lodash按需引入的eslint规则",
recommended: true,
url: null, // URL to the documentation page for this rule
},
messages: {
autoImportLodash: "请使用lodash按需引用",
invalidImport: "lodash 导出依赖不为空",
},
fixable: "code",
schema: [],
},
create: function (context) {
// 获取lodash中导入的函数名称,并返回
function getImportSpecifierArray(specifiers) {
const incluedType = ["ImportSpecifier", "ImportDefaultSpecifier"];
return specifiers
.filter((item) => incluedType.includes(item.type))
.map((item) => {
return item.imported ? item.imported.name : item.local.name;
});
}
// 生成修复文本
function generateFixedImportText(importedList, dependencyName) {
let fixedText = "";
importedList.forEach((importName, index) => {
fixedText += `import ${importName} from "${dependencyName}/${importName}";`;
if (index != importedList.length - 1) fixedText += "\n";
});
return fixedText;
}
return {
ImportDeclaration(node) {
const source = node.source.value;
const hasUseLodash = SOURCElIST.inclues(source);
// 使用lodash
if (hasUseLodash) {
const importedList = getImportSpecifierArray(node.specifiers || []);
if (importedList.length <= 0) {
return context.report({
node,
messageId: "invalidImport",
});
}
const dependencyName = getImportDependencyName(node);
return context.report({
node,
messageId: "autoImportLodash",
fix(fixer) {
return fixer.replaceTextRange(
node.range,
generateFixedImportText(importedList, dependencyName)
);
},
});
}
},
};
},
};
配置规则组
// lib/rules/index.js
const requireIndex = require("requireindex");
// 在这里导入了我们上面写的自定义规则
const rules = requireIndex(__dirname + "/rules");
module.exports = {
// rules是必须的
rules,
// 增加configs配置
configs: {
// 配置了这个之后,就可以在其他项目中像下面这样使用了
// extends: ['plugin:guming-eslint/recommended']
recommended: {
plugins: ['guming-eslint'],
rules: {
'guming-eslint/lodash-auto-import': ['error'],
}
}
}
}
补充测试用例
// tests/lib/rules/lodash-auto-import.js
/**
* @fileoverview 这是一个lodash按需引入的eslint规则
* @author guming-eslint
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const rule = require("../../../lib/rules/lodash-auto-import"),
RuleTester = require("eslint").RuleTester;
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
const ruleTester = new RuleTester();
ruleTester.run("lodash-auto-import", rule, {
valid: ['import omit from "lodash/omit";', 'import { omit } from "lodash-es";'],
invalid: [
// eslint-disable-next-line eslint-plugin/consistent-output
{
code: 'import {} from "lodash";',
errors: [{ message: "invalidImport" }],
output: 'import xxx from lodash/xxx'
},
{
code: 'import {} from "lodash-es";',
errors: [{ message: "invalidImport" }],
output: 'import { xxx } from lodash-es'
},
{
code: 'import { omit } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output: 'import omit from "lodash/omit";',
},
{
code: 'import { omit as _omit } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output: 'import omit from "lodash/omit";',
},
{
code: 'import { omit, debounce } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output:
'import omit from "lodash/omit"; \n import debounce from "lodash/debounce";',
},
],
});
可输入如下指令,执行测试
$ yarn run test
注意事项
开发这个插件的一些注意事项如下
多个模块导出
lodash
具名导出和默认导出模块别名(
as
)
使用
插件安装
npm
包发布安装调试
$ yarn add eslint-plugin-guming-eslint
npm link
本地调试(推荐使用) 插件项目目录执行如下指令
$ npm link
项目目录执行如下指令
$ npm link eslint-plugin-guming-eslint
项目配置
添加你的 plugin
包名(eslint-plugin-
前缀可忽略) 到 .eslintrc
配置文件的 extends
字段。
.eslintrc
配置文件示例:
module.exports = {
// 你的插件
extends: ["plugin:guming-eslint/recommended"],
parserOptions: {
ecmaVersion: 7,
sourceType: "module",
},
};
效果
作者:古茗前端团队
来源:juejin.cn/post/7202413628807938108
用一周时间开发了一个微信小程序,我遇到了哪些问题?
功能截图
特别说明:由于本项目是用于教学案例,并没有上线的二维码供大家体验。 |
开发版本
微信开发者工具版本:1.06
调试基础库:2.30
代码仓库
gitee:gitee.com/guigu-fe/gu…
github:github.com/xiumubai/gu…
建议全文参考源代码观看效果更佳,代码可直接在微信开发者工具当中打开预览,appid需要替换成自己的。
获取用户信息变化
用户头像昵称获取规则已调整,现在微信小程序已经获取不到用户昵称和头像了,只能已通过用户回填(提供给用户一个修改昵称和头像的表单页面)的方式来实现。不过还是可以获取到code跟后端换取token的方式来进行登录。
具体参考 *用户信息接口调整说明*、小程序用户头像昵称获取规则调整公告
vant weapp组件库的使用
1.需要使用npm构建的能力,用 npm 构建前,请先阅读微信官方的 npm 支持。初始化package.json
npm init
2.安装@vant/weapp
# 通过 npm 安装
npm i @vant/weapp -S --production
# 通过 yarn 安装
yarn add @vant/weapp --production
# 安装 0.x 版本
npm i vant-weapp -S --production
2.修改 app.json 将 app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。 3.修改 project.config.json 开发者工具创建的项目,miniprogramRoot 默认为 miniprogram,package.json 在其外部,npm 构建无法正常工作。 需要手动在 project.config.json 内添加如下配置,使开发者工具可以正确索引到 npm 依赖的位置。
{
...
"setting": {
...
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./miniprogram/"
}
]
}
}
注意: 由于目前新版开发者工具创建的小程序目录文件结构问题,npm构建的文件目录为miniprogram_npm
,并且开发工具会默认在当前目录下创建miniprogram_npm
的文件名,所以新版本的miniprogramNpmDistDir
配置为'./'
即可。 4.构建 npm 包 打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。
使用组件
引入组件
// 通过 npm 安装
// app.json
"usingComponents": {
"van-button": "@vant/weapp/button/index"
}
使用组件
<van-button type="primary">按钮</van-button>
如果预览没有效果,从新构建一次npm,然后重新打开此项目
。
自定义tabbar
这里我的购物车使用了徽标,所以需要自定义一个tabbar,这里自定义以后,会引发后面的一系列连锁反应(比如内容区域高度塌陷,导致tabbar遮挡内容区域),后面会讲如何计算。效果如下图:
1. 配置信息
在 app.json 中的 tabBar 项指定 custom 字段,同时其余 tabBar 相关配置也补充完整。
所有 tab 页的 json 里需声明 usingComponents 项,也可以在 app.json 全局开启。
示例:
{
"tabBar": {
"custom": true,
"color": "#000000",
"selectedColor": "#000000",
"backgroundColor": "#000000",
"list": [{
"pagePath": "page/component/index",
"text": "组件"
}, {
"pagePath": "page/API/index",
"text": "接口"
}]
},
"usingComponents": {}
}
2. 添加 tabBar 代码文件
需要跟pages目录同级,创建一个custom-tab-bar
目录。 .wxml
代码如下:
<!--miniprogram/custom-tab-bar/index.wxml-->
<cover-view class="tab-bar">
<cover-view class="tab-bar-border"></cover-view>
<cover-view wx:for="{{list}}"
wx:key="index"
class="tab-bar-item"
data-path="{{item.pagePath}}"
data-index="{{index}}"
bindtap="switchTab">
<cover-view class="tab-img-wrap">
<cover-image src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></cover-image>
<cover-view wx-if="{{item.info && cartCount > 0}}"class="tab-badge">{{cartCount}}</cover-view>
</cover-view>
<cover-view style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</cover-view>
</cover-view>
</cover-view>
注意这里的徽标控制我是通过info
字段来控制的,然后数量cartCount
单独第一个了一个字段,这个字段是通过store
来管理的,后面会讲为什么通过stroe
来控制的。
3. 编写 tabBar 代码
用自定义组件的方式编写即可,该自定义组件完全接管 tabBar 的渲染。另外,自定义组件新增 getTabBar 接口,可获取当前页面下的自定义 tabBar 组件实例。
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { store } from '../store/index';
Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store,
fields: {
count: 'count',
},
actions: [],
},
observers: {
count: function (val) {
// 更新购物车的数量
this.setData({ cartCount: val });
},
},
data: {
selected: 0,
color: '#252933',
selectedColor: '#FF734C',
cartCount: 0,
list: [
{
pagePath: '/pages/index/index',
text: '首页',
iconPath: '/static/tabbar/home-icon1.png',
selectedIconPath: '/static/tabbar/home-icon1-1.png',
},
{
pagePath: '/pages/category/category',
text: '分类',
iconPath: '/static/tabbar/home-icon2.png',
selectedIconPath: '/static/tabbar/home-icon2-2.png',
},
{
pagePath: '/pages/cart/cart',
text: '购物车',
iconPath: '/static/tabbar/home-icon3.png',
selectedIconPath: '/static/tabbar/home-icon3-3.png',
info: true,
},
{
pagePath: '/pages/info/info',
text: '我的',
iconPath: '/static/tabbar/home-icon4.png',
selectedIconPath: '/static/tabbar/home-icon4-4.png',
},
],
},
lifetimes: {},
methods: {
// 改变tab的时候,记录index值
switchTab(e) {
const { path, index } = e.currentTarget.dataset;
wx.switchTab({ url: path });
this.setData({
selected: index,
});
},
},
});
这里的store大家不用理会,只需要记住是设置徽标的值就可以了。
4.设置样式
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: white;
display: flex;
padding-bottom: env(safe-area-inset-bottom);
}
这里的样式单独贴出来说明一下:
padding-bottom: env(safe-area-inset-bottom);
可以让出底部安全区域,不然的话tabbar会直接沉到底部 别忘了在index.json
中设置component=true
{
"component": true
}
5.tabbar页面设置index
上面的代码添加完毕以后,我们的tabbar就出来了,但是有个问题,就是在点击tab的时候,样式不会改变,必须再点击一次,这是因为当你切换页面或者刷新页面的时候,index的值会重置,为了解决这个问题,我们需要在每个tabbar的页面添加下面的代码:
/**
* 生命周期函数--监听页面显示
*/
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 0,
});
}
},
当页面每次show的时候,设置一下selected的值,也就是选中的index就可以了。其他的tabbar页面同样也是如此设置即可。
添加store状态管理
接下来我们来讲讲微信小程序如何用store来管理我们的数据。 上面我们说了我们需要实现一个tabbar的徽标,起初我想的是直接用个缓存来解决就完事了,后来发现我太天真了,忘记了这个字段是一个响应式的,它是需要渲染到页面上的,它变了,页面中的数据也得跟着一起变。后来我想通过globalData
来实现,也不行。后来我又又想到了把这个数据响应式监听一下不就行了?于是通过proxy,跟vue3的处理方式一样,监听一下这个字段的改变就可以了。在购物车这个页面触发的时候是挺好,可当我切换到其他tabbar页面的时候它就不见了。我忽略了一个问题,它是全局响应的啊。于是最后才想到了使用store的方式来实现。 我找到了一个针对微信小程序的解决方法,就是使用mobx-miniprogram-bindings和mobx-miniprogram
这两个库来解决。真是帮了我的大忙了。 下面我们直接来使用。 先安装两个插件:
npm install --save mobx-miniprogram mobx-miniprogram-bindings
方式跟安装vant weapp
一样,npm安装完成以后,在微信开发者工具当中构建npm
即可。 下面我们来通过如何实现一个tabbar徽标的场景来学习如何在微信小程序中使用store来管理全局数据。
tabbar徽标实现
1.定义store
import { observable, action, runInAction } from 'mobx-miniprogram';
import { getCartList } from './cart';
// 获取购物车数量
export const store = observable({
/** 数据字段 */
count: 0,
/** 异步方法 */
getCartListCount: async function () {
const num = await getCartList();
runInAction(() => {
this.count = num;
});
},
/** 更新购物车的数量 */
updateCount: action(function (num) {
this.count = num;
}),
});
看起来是不是非常简单。这里我们定义了一个count
,然后定义了两个方法,这两个方法有点区别:
updateCount
用来更新count
getCartListCount
用来异步更新count
,因为这里我们在进入小程序的时候就需要获取count
的初始值,这个值的计算又的依赖接口,所以需要使用异步的方式。
好了,现在我们字段有了,设置初始值的方法有了,更新字段的方法也有了。下面我们来看一下如何使用。
2.使用store
回到我们的tabbr组件,在custom-tab-bari/ndex.js
中,我们贴一下主要的代码:
import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { store } from '../store/index';
Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store,
fields: {
count: 'count',
},
actions: [],
},
observers: {
count: function (val) {
// 更新购物车的数量
this.setData({ cartCount: val });
},
},
data: {
cartCount: 0,
},
});
解释一下,这里我们只是获取了count
的值,然后通过observers的方式监听了一下count
,然后赋值给了cartCount
,这里你直接使用count
渲染到页面上也是没有问题的。我这里只是为了演示一下observers
的使用方式才这么写的。这样设置以后,tabbar上面的徽标数字已经可以正常展示了。 现在当我们的购物车数字改变以后,就要更新count
的值了。
3.使用action
找到我们的cart
页面,下面是具体的逻辑:
import {
findCartList,
deleteCart,
checkCart,
addToCart,
checkAllCart,
} from '../../utils/api';
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from '../../store/index';
import { getCartTotalCount } from '../../store/cart';
const app = getApp();
Page({
data: {
list: [],
totalCount: 0,
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.storeBindings = createStoreBindings(this, {
store,
fields: ['count'],
actions: ['updateCount'],
});
},
/**
* 声明周期函数--监听页面卸载
*/
onUnload() {
this.storeBindings.destroyStoreBindings();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 2,
});
}
this.getCartList();
},
/**
* 获取购物车列表
*/
async getCartList() {
const res = await findCartList();
this.setData({
list: res.data,
});
this.computedTotalCount(res.data);
},
/**
* 修改购物车数量
*/
async onChangeCount(event) {
const newCount = event.detail;
const goodsId = event.target.dataset.goodsid;
const originCount = event.target.dataset.count;
// 这里如果直接拿+以后的数量,接口的处理方式是直接在上次的基础累加的,
// 所以传给接口的购物车数量的计算方式如下:
// 购物车添加的数量=本次的数量-上次的数量
const count = newCount - originCount;
const res = await addToCart({
goodsId,
count,
});
if (res.code === 200) {
this.getCartList();
}
},
/**
* 计算购物车总数量
*/
computedTotalCount(list) {
// 获取购物车选中数量
const total = getCartTotalCount(list);
// 设置购物车徽标数量
this.updateCount(total);
},
});
上面的代码有所删减。在page和component中使用action方法有所区别,需要在onUnload
的时候销毁一下我们的storeBindings
。当修改购物车数量的时候,我这里会重新请求一次接口,然后计算一下totalCount的数量,通过updateCount
来修改count
的值。到了这里,我们的徽标就可以正常的使用了。不管是切换到哪一个tabbar
页面,徽标都会保持状态。
4.使用异步action
现在还剩最后一个问题,就是如何设置count的初始值,这个值还得从接口获取过来。下面是实现思路。 首先我们在store中定义了一个一步方法:
import { observable, action, runInAction } from 'mobx-miniprogram';
import { getCartList } from './cart';
// 获取购物车数量
export const store = observable({
/** 数据字段 */
count: 0,
/** 异步方法 */
getCartListCount: async function () {
const num = await getCartList();
runInAction(() => {
this.count = num;
});
},
/** 更新购物车的数量 */
updateCount: action(function (num) {
this.count = num;
}),
});
可以看到,异步action的实现跟同步的区别很大,使用了runInAction
这个方法,在它的回调函数中去修改count的值。很坑的是,这个方法在[mobx-miniprogram-bindings](https://www.npmjs.com/package/mobx-miniprogram-bindings)
中的官方文档中没有做任何说明,我百度了好久才找到。 现在,我们有了这个方法,在哪里触发好合适呢?答案是app.js
中的onShow
生命周期函数中。也就是每次我们进入小程序,就会设置一下count的初始值了。下面是代码:
// app.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from './store/index';
App({
onShow() {
this.storeBindings = createStoreBindings(this, {
store,
fields: [],
actions: ['getCartListCount'],
});
// 在页面初始化的时候,更新购物车徽标的数量
this.getCartListCount();
},
});
到此为止,整个完整的徽标响应式改变和store的使用完美的融合了。 参考文章:blog.csdn.net/ice_stone_k…
如何获取tabbar的高度
当我们自定义tabbar以后,由于tabbar是使用的fixed定位,我们的内容区域如果不做任何限制,底部的内容就会被tabbar遮挡,所以我们需要给内容区域整体设置一个padding-bottom
,那这个值是多少呢?有的人可能会说,直接把tabbar的高度固定,然后padding-bottom
设置成这个高度的值不就可以了吗?你别忘了,现在五花八门的手机下面还有一个叫做安全区域的东西,如下图:
如果你没有把这个高度加上,那内容区域还是会被tabbar遮挡。下面我们就来看看这个高度具体如何计算呢? 我们以通过wx.getSystemInfoSync()
获取机型的各种信息。
其中screenHeight
是屏幕高度,safeArea
的bottom
属性会自动计算安全区域也就是去除tabBar下面的空白区域后有用区域的纵坐标。如此我们就可以就算出来tabber的高度:
const res = wx.getSystemInfoSync()
const { screenHeight, safeArea: { bottom } } = res
if (screenHeight && bottom){
let safeBottom = screenHeight - bottom
const tabbarHeight = 48 + safeBottom
}
这里48是tabbar的高度,我们固定是48px
。拿到tabbarHeight
以后,把它设置成一个globalData
,我们就可以给其他页面设置padding-bottom
了。 我这里还使用了其他的一些属性,具体参考代码如下:
// app.js
App({
onLaunch() {
// 获取高度
this.getHeight();
},
onShow() {
},
globalData: {
// tabber+安全区域高度
tabbarHeight: 0,
// 安全区域的高度
safeAreaHeight: 0,
// 内容区域高度
contentHeight: 0,
},
getHeight() {
const res = wx.getSystemInfoSync();
// 胶囊按钮位置信息
const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
const {
screenHeight,
statusBarHeight,
safeArea: { bottom },
} = res;
// console.log('resHeight', res);
if (screenHeight && bottom) {
// 安全区域高度
const safeBottom = screenHeight - bottom;
// 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度
const navBarHeight =
(menuButtonInfo.top - statusBarHeight) * 2 +
menuButtonInfo.height +
statusBarHeight;
// tabbar高度+安全区域高度
this.globalData.tabbarHeight = 48 + safeBottom;
this.globalData.safeAreaHeight = safeBottom;
// 内容区域高度,用来设置内容区域最小高度
this.globalData.contentHeight = screenHeight - navBarHeight;
}
},
});
假如我们需要给首页设置一个首页设置一个padding-bottom
:
// components/layout/index.js
const app = getApp();
Component({
/**
* 组件的属性列表
*/
properties: {
bottom: {
type: Number,
value: 48,
},
},
/**
* 组件的方法列表
*/
methods: {},
});
<view style="padding-bottom: {{bottom}}px">
<slot></slot>
</view>
这里我简单粗暴的直接在外层套了一个组件,统一设置了padding-bottom。 除了自定义tabbar,还可以自定义navbar,这里我没这个功能,所以不展开讲了,这里放一个参考文章: 获取状态栏的高度。这个文章把如何自定义navbar,如何获取navbar的高度,讲的很通透,感兴趣的仔细拜读。
分页版上拉加载更多
为什么我称作是分页版本的上拉加载更多呢,因为就是上拉然后多加载一页,没有做那种虚拟加载,感兴趣的可以参考这篇文章(我觉得写的非常到位了)。下面我以商品列表为例,代码在pages/goods/list
下,讲讲简单版本的实现:
<!--pages/goods/list/index.wxml-->
<view style="min-height: {{contentHeight}}px; padding-bottom: {{safeAreaHeight}}px">
<view wx:if="{{list.length > 0}}">
<goods-card wx:for="{{list}}" wx:key="index" item="{{item}}"></goods-card>
<!-- 上拉加载更多 -->
<load-more
list-is-empty="{{!list.length}}"
status="{{loadStatus}}"
/>
</view>
<van-empty wx:else description="该分类下暂无商品,去看看其他的商品吧~">
<van-button
round
type="danger"
bindtap="gotoBack">
查看其他商品
</van-button>
</van-empty>
</view>
// pages/goods/list/index.js
import { findGoodsList } from '../../../utils/api';
const app = getApp();
Page({
/**
* 页面的初始数据
*/
data: {
page: 1,
limit: 10,
list: [],
options: {},
loadStatus: 0,
contentHeight: app.globalData.contentHeight,
safeAreaHeight: app.globalData.safeAreaHeight,
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.setData({ options });
this.loadGoodsList(true);
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
// 还有数据,继续请求接口
if (this.data.loadStatus === 0) {
this.loadGoodsList();
}
},
/**
* 商品列表
*/
async loadGoodsList(fresh = false) {
// wx.stopPullDownRefresh();
this.setData({ loadStatus: 1 });
let page = fresh ? 1 : this.data.page + 1;
// 组装查询参数
const params = {
page,
limit: this.data.limit,
...this.data.options,
};
try {
// loadstatus说明: 0-加载完毕,隐藏加载状态 1-正在加载 2-全部加载 3-加载失败
const res = await findGoodsList(params);
const data = res.data.records;
if (data.length > 0) {
this.setData({
list: fresh ? data : this.data.list.concat(data),
loadStatus: data.length === this.data.limit ? 0 : 2,
page,
});
} else {
// 数据全部加载完毕
this.setData({
loadStatus: 2,
});
}
} catch {
// 错误请求
this.setData({
loadStatus: 3,
});
}
},
});
代码已经很详细了,我再展开说明一下。
onLoad
的时候第一次请求商品列表数据loadGoodsList
,这里我加了一个fresh字段,用来区分是不是第一次加载,从而且控制page是不是等于1触发
onReachBottom
的时候,先判断loadStatus === 0
,表示接口数据还没加载完,继续请求loadGoodsList
在
loadGoodsList
里面,先设置loadStatus = 1
,表示状态为加载中。如果fresh为false,则表示要请求下一页的数据了,page+1。接口请求成功,给了list添加数据的时候要注意了,这里需要再上次list的基础上拼接数据,所以得用
concat
。同时修改loadStatus
状态,如果当前请求回来的数据条数小与limit(每页数据大小),则表示没有更对的数据了,loadStatus = 2
,反之为0。最后为了防止特殊情况出现,还有个
loadStatus = 3
,表示加载失败的情况。
这里我封装了一个load-more
组件,里面就是对loadStatus
各种不同状态的处理。具体详情看看源码。 思考:如果加上个下拉刷新,跟上拉加载放在一起,如何实现呢?
如何分包
为什么要做小程序分包?先来看看小程序对文件包的大小限制 在不使用分包的时候,代码总包的大小限制为2M,如果使用了分包,总包大小可以达到20M,也就是我们能分10个包。 那么如何分包?非常的简单。代码如下:
{
"pages": [
"pages/index/index",
"pages/category/category",
"pages/cart/cart",
"pages/info/info",
"pages/login/index"
],
"subpackages": [
{
"root": "pages/goods",
"pages": [
"list/index",
"detail/index"
]
},
{
"root": "pages/address",
"pages": [
"list/index",
"add/index"
]
},
{
"root": "pages/order",
"pages": [
"pay/index",
"list/index",
"result/index",
"detail/index"
]
}
],
}
目录结构如下: 解释一下: 我们subpackages下面的就是分包的内容,里面的每一个root就是一个包,pages里面的内容只能是这样的字符串路径,添加别的内容会报错。分包的逻辑:业务相关的页面放在一个包下,也就是一个目录下即可。 ⚠️注意:tabbar
的页面不能放在分包里面。 下面是分包以后的代码依赖分析截图:
后续更新计划
小程序如何自定义navbar
小程序如何添加typescript
在小程序中如何做表单校验的小技巧
微信支付流程
如何在小程序中mock数据
如何优化小程序
本文章可以随意转载。转给更多需要帮助的人。看了源码觉得有帮助的可以点个star。我会持续更新更多系列教程。
作者:白哥学前端
来源:https://juejin.cn/post/7202495679397511227
手把手教你实现MVVM架构
引言
现在的前端真可谓是百花齐放,百家争鸣,各种框架层出不穷,但是主要目前用的最多的还是要数Vue
、React
、以及Angular
,这三种,当然不乏近期新出的一些其他框架,但是她们都有一个显著的特点,那就是使用了MVVM的架构。
首先我们要搞清楚什么是
MVVM
?
MVVM
就是Model-View-ViewModel的缩写,MVVM
最早由微软提出来,它借鉴了桌面应用程序的MVC
思想,在前端页面中,把Model
用纯JavaScript
对象表示,View
负责显示,两者做到了最大限度的分离。把Model
和View
关联起来的就是ViewModel
。ViewModel
负责把Model
的数据同步到View
显示出来,还负责把View
的修改同步回Model
。改变
JavaScript
对象的状态,会导致DOM结构
作出对应的变化!这让我们的关注点从如何操作DOM
变成了如何更新JavaScript
对象的状态,而操作JavaScript
对象比DOM
简单多了。这就是
MVVM
的设计思想:关注Model
的变化,让MVVM
框架去自动更新DOM
的状态,从而把开发者从操作DOM
的繁琐步骤中解脱出来!
接下来我会带着你们如何去实现一个简易的MVVM架构。
一、构建MVVM构造函数
创建一个MVVM
构造函数,用于接收参数,如:data
、methods
、computed
等:
function MVVM(options) {
this.$options = options;
let data = this._data = this.$options.data;
observe(data);
for (let key in data) {
Object.defineProperty(this, key, {
enumerable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
});
};
initComputed.call(this);
new Compile(options.el, this);
options.mounted.call(this);
}
二、构建Observe构造函数
创建一个Observe
构造函数,用于监听数据变化:
function Observe(data) {
let dep = new Dep();
for (let key in data) {
let val = data[key];
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return val;
},
set(newVal) {
if (val === newVal) {
return;
}
val = newVal;
observe(newVal);
dep.notify();
}
});
};
};
三、构建Compile构造函数
创建一个Compile
构造函数,用于解析模板指令:
function Compile(el, vm) {
vm.$el = document.querySelector(el);
let fragment = document.createDocumentFragment();
while (child = vm.$el.firstChild) {
fragment.appendChild(child);
}
replace(fragment);
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g;
if (node.nodeType === 3 && reg.test(txt)) {
let arr = RegExp.$1.split('.');
let val = vm;
arr.forEach(key => { val = val[key]; });
node.textContent = txt.replace(reg, val).trim();
new Watcher(vm, RegExp.$1, newVal => {
node.textContent = txt.replace(reg, newVal).trim();
});
}
if (node.nodeType === 1) {
let nodeAttr = node.attributes;
Array.from(nodeAttr).forEach(attr => {
let name = attr.name;
let exp = attr.value;
if (name.includes('')) {
node.value = vm[exp];
}
new Watcher(vm, exp, newVal => {
node.value = newVal;
});
node.addEventListener('input', e => {
let newVal = e.target.value;
vm[exp] = newVal;
});
});
};
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
vm.$el.appendChild(fragment);
}
四、构建Watcher构造函数
创建一个Watcher
构造函数,用于更新视图:
function Watcher(vm, exp, fn) {
this.fn = fn;
this.vm = vm;
this.exp = exp;
Dep.target = this;
let arr = exp.split('.');
let val = vm;
arr.forEach(key => {
val = val[key];
});
Dep.target = null;
}
Watcher.prototype.update = function() {
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key];
});
this.fn(val);
}
五、构建Dep构造函数
创建一个Dep
构造函数,用于管理Watcher
:
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
}
Dep.prototype.notify = function() {
this.subs.forEach(sub => {
sub.update();
});
}
六、构建initComputed构造函数
创建一个initComputed
构造函数,用于初始化计算属性:
function initComputed() {
let vm = this;
let computed = this.$options.computed;
Object.keys(computed).forEach(key => {
Object.defineProperty(vm, key, {
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set() {}
});
});
}
总结:
至此我们就完成了一个简易的MVVM框架,虽然简易,但是基本的核心思想差不多都已经表达出来了,最后还是希望大家不要丢在收藏文件夹里吃灰,还是要多多动手练习一下,所谓眼过千遍,不如手过一遍。
作者:前端第一深情阿斌
来源:juejin.cn/post/7202431872968851517
Nginx基本介绍+跨域解决方案
Nginx简介
Nginx 是一款由俄罗斯的程序设计师 Igor Sysoev 所开发的高性能的 http 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,它的主要功能有:
- 反向代理
- 负载均衡
- HTTP 服务器
目前大部分运行的 Nginx 服务器都在使用其负载均衡的功能作为服务集群的系统架构。
功能说明
在上文中介绍了三种 Nginx 的主要功能,下面来讲讲具体每个功能的作用。
一、反向代理(Reverse Proxy)
介绍反向代理前,我们先理解下正向代理的概念。打个比方,你准备去看周杰伦的巡演,但是发现官方渠道的票已经卖完了,所以你只好托你神通广大的朋友A去内部购票,你如愿以偿地得到了这张门票。在这个过程中,朋友A就起到了一个正向代理的作用,即代理了客户端(你)去向服务端(售票方)发请求,但服务端(售票方)并不知道源头是谁发起的请求,只知道是代理服务(朋友A)向自己请求的。由这个例子,我们再去理解下反向代理,比如我们经常接到10086或者10000的电话,但是每次打过来的人都不一样,这是因为10086是中国移动的总机号,分机打给用户的时候,都是通过总机代理显示的号码,这个时候客户端(你)无法知道是谁发起的请求,只知道是代理服务(总机)向自己请求的。
而官方的解释说明就是,反向代理方式是指以代理服务器来接受 Internet 上 的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
下面贴一段简单实现反向代理的 Nginx 配置代码:
server {
listen 80;
server_name localhost;
client_max_body_size 1024M;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host:$server_port;
}
}
复制代码
其中的 http://localhost:8080 就是反代理的目标服务端,80是 Nginx 暴露给客户端访问的端口。
二、负载均衡(Load Balance**)**
负载均衡,顾名思义,就是将服务负载均衡地分摊到多个服务器单元上执行,来提高网站、应用等服务的性能和可靠性。
下面我们来对比一下两个系统拓扑,首先是未设计负载均衡的拓扑:
下面是设计了负载均衡的拓扑:
从图二可以看到,用户访问负载均衡器,再由负载均衡器将请求转发给后端服务器,在这种情况下,服务C故障后,用户访问负载会分配到服务A和服务B中,避免了系统崩溃,如果这种故障出现在图一中,该系统一定会会直接崩溃。
负载均衡算法
负载均衡算法决定了后端的哪些健康服务器会被选中。几个常用的算法:
- **Round Robin(轮询):**为第一个请求选择列表中的第一个服务器,然后按顺序向下移动列表直到结尾,然后循环。
- **Least Connections(最小连接):**优先选择连接数最少的服务器,在普遍会话较长的情况下推荐使用。
- **Source:**根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。
如果你的应用需要处理状态而要求用户能连接到和之前相同的服务器。可以通过 Source 算法基于客户端的 IP 信息创建关联,或者使用粘性会话(sticky sessions)。
负载均衡同时需要配合反向代理功能才能发挥其作用。
三、HTTP服务器
除了以上两大功能外,Nginx也可以作为静态资源服务器使用,例如没有使用 SSR(Server Side Render)的纯前端资源,就可以依托Nginx来实现资源托管。
下面看一段实现静态资源服务器的配置:
server {
listen 80;
server_name localhost;
client_max_body_size 1024M;
location / {
root e:\wwwroot;
index index.html;
}
}
复制代码
root
配置就是具体资源存放的根目录,index
配置的则是访问根目录时默认的文件。
动静分离
动静分离也是Nginx作为Http服务器使用的一个重要概念,要搞清楚动静分离,首先要弄明白什么是动态资源,什么是静态资源:
- **动态资源:**需要从服务器中实时获取的资源内容,如 JSP, SSR 渲染页面等,不同时间访问,资源内容会发生变化。
- **静态资源:**如 JS、CSS、Img 等,不同时间访问,资源内容不会发生变化。
由于Nginx可以作为静态资源服务器,但无法承载动态资源,因此出现需要动静分离的场景时,我们需要拆分静态、动态资源的访问策略:
upstream test{
server localhost:8080;
server localhost:8081;
}
server {
listen 80;
server_name localhost;
location / {
root e:\wwwroot;
index index.html;
}
# 所有静态请求都由nginx处理,存放目录为html
location ~ \.(gif|jpg|jpeg|png|bmp|swf|css|js)$ {
root e:\wwwroot;
}
# 所有动态请求都转发给tomcat处理
location ~ \.(jsp|do)$ {
proxy_pass http://test;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root e:\wwwroot;
}
}
复制代码
从这段配置可以大概理解到,当客户端访问不同类型的资源时,Nginx 会自动按照类型分配给自己的静态资源服务或者是远程的动态资源服务上,这样就能满足一个完整的资源服务器的功能了。
配置介绍
一、基本介绍
说完 Nginx 的功能,我们来简单进一步介绍下 Nginx 的配置文件。作为前端人员来讲,使用 Nginx 基本上就是修改配置 -> 启动/热重启 Nginx,就能搞定大部分日常和 Nginx 相关的工作了。
这里我们看下一份 Nginx 的默认配置,即安装 Nginx 后,默认的 nginx.conf 文件的内容:
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# location / {
# root html;
# index index.html index.htm;
# }
#}
# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;
# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;
# location / {
# root html;
# index index.html index.htm;
# }
#}
}
复制代码
对应的结构大致是:
... #全局块
events { #events块
...
}
http #http块
{
... #http全局块
server #server块
{
... #server全局块
location [PATTERN] #location块
{
...
}
location [PATTERN]
{
...
}
}
server
{
...
}
... #http全局块
}
复制代码
以上几个代码块对应功能是:
- 全局块:配置影响 Nginx 全局的指令。一般有运行 Nginx 服务器的用户组,Nginx 进程 pid 存放路径,日志存放路径,配置文件引入,允许生成 worker process 数等。
- events块:配置影响 Nginx 服务器或与用户的网络连接。有每个进程的最大连接数,选取哪种事件驱动模型处理连接请求,是否允许同时接受多个网路连接,开启多个网络连接序列化等。
- http块:可以嵌套多个 server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。如文件引入,mime-type 定义,日志自定义,是否使用 sendfile 传输文件,连接超时时间,单连接请求数等。
- server块:配置虚拟主机的相关参数,一个 http 中可以有多个 server。
- location块:配置请求的路由,以及各种页面的处理情况。
各代码块详细的配置方式可以参考 Nginx 文档
二、Nginx 解决跨域问题
下面展示一段常用于处理前端跨域问题的 location代码块,方面各位读者了解及使用 Nginx 去解决跨域问题。
location /cross-server/ {
set $corsHost $http_origin;
set $allowMethods "GET,POST,OPTIONS";
set $allowHeaders "broker_key,X-Original-URI,X-Request-Method,Authorization,access_token,login_account,auth_password,user_type,tenant_id,auth_code,Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With, usertoken";
if ($request_method = 'OPTIONS'){
add_header 'Access-Control-Allow-Origin' $corsHost always;
add_header 'Access-Control-Allow-Credentials' true always;
add_header 'Access-Control-Allow-Methods' $allowMethods always;
add_header 'Access-Control-Allow-Headers' $allowHeaders;
add_header 'Access-Control-Max-Age' 90000000;
return 200;
}
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
add_header Access-Control-Allow-Origin $corsHost always;
add_header Access-Control-Allow-Methods $allowMethods always;
add_header Access-Control-Allow-Headers $allowHeaders;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Expose-Headers *;
add_header Access-Control-Max-Age 90000000;
proxy_pass http://10.117.20.54:8000/;
proxy_set_header Host $host:443;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_redirect http:// $scheme://;
}
复制代码
可以看到,前段使用 set
设置了 location
中的局部变量,然后分别在下方的各处指令配置中使用了这些变量,以下是各指令的作用:
- add_header:用于给请求添加返回头字段,当且仅当状态码为以下列出的那些时有效:200, 201 (1.3.10), 204, 206, 301, 302, 303, 304, 307 (1.1.16, 1.0.13), or 308 (1.13.0)
- **proxy_hide_heade:**可以隐藏响应头中的信息。
- **proxy_redirect:**指定修改被代理服务器返回的响应头中的location头域跟refresh头域数值。
- **proxy_set_header:**重定义发往后端服务器的请求头。
- **proxy_pass:**被代理的转发服务路径。
以上这段配置可以直接复制到 nginx.conf 中,然后修改 /cross-server/
(Nginx 暴露给客户端访问的路径)和 http://10.117.20.54:8000/
(被转发的服务路径)即可实避免服务跨域问题。
跨域技巧补充
开发环境下,如果不想使用 Nginx 来处理跨域调试问题,也可以采用修改 Chrome 配置的方式来实现跨域调试,本质上跨域是一种浏览器的安全策略,所以从浏览器出发去解决这个问题反而更加方便。
Windows 系统:
1、复制chrome浏览器快捷方式,对快捷方式图标点右键打开“属性” 如图:
2、在“目标”后添加 --disable-web-security --user-data-dir
,例如图中修改完成后为:"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir
。
3、点击确定后重新打开浏览器,出现:
此时,屏蔽跨域设置修改完毕,点开此快捷方式访问的页面均会忽略跨域规则,避免了开发环境下,服务端配置跨域的麻烦。
Mac 系统:
以下内容转载自:Mac上解决Chrome浏览器跨域问题
首先创建一个文件夹,这个文件夹是用来保存关闭安全策略后的用户信息的,名字可以随意取,位置也可以随意放。
创建一个文件夹
然后打开控制台,输入下面这段代码
open -n /Applications/Google\ Chrome.app/ --args --disable-web-security --user-data-dir=/Users/LeoLee/Documents/MyChromeDevUserData
关闭安全策略代码
大家需要根据自己存放刚刚创建的文件夹的地址来更改上面的代码,也就是下面图中的红框区域,而网上大多数的教程中也正是缺少了这部分的代码导致很多用户在关闭安全策略时失败
用户需要根据自己的文件夹地址修改代码
输入代码,敲下回车,接下来Chrome应该会弹出一个窗口
Chrome弹窗
点击启动Google Chrome,会发现与之前的Chrome相比,此时的Chrome多了上方的一段提示,告诉你现在使用的模式并不安全
浏览器上方会多出一行提示
其原理和 Windows 版本差不多,都是通过修改配置来绕过安全策略。
来源:juejin.cn/post/7202252704978026551
从输入 URL 到页面显示,这中间发生了什么?
前言
从输入 URL 到页面显示的发生过程,这是一个在面试中经常会被问到的问题,此问题能比较全面地考察应聘者知识的掌握程度。其中涉及到了网络、操作系统、Web 等一系列的知识。
以 Chrome 浏览器为例,目前的 Chrome 浏览器有以下几个进程:
浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
渲染进程。主要职责是把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。
GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。
网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,后面才独立出来,成为一个单独的进程。
插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
1. 用户输入
如果输入的是内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。
如果输入的是 URL,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL。
2. URL 请求过程
浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。那具体流程是怎样的呢?
网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程。这请求前的第一步是要进行
DNS
解析,以获取请求域名的服务器IP
地址。如果请求协议是HTTPS
,那么还需要建立TLS
连接。接下来就是利用
IP
地址和服务器建立TCP
连接 (三次握手)。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的cookie
等数据附加到请求头中,然后向服务器发送构建的请求信息。服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。等网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。
Content-Type
是 HTTP 头中一个非常重要的字段, 它告诉浏览器服务器返回的响应体数据是什么类型,然后浏览器会根据 Content-Type
的值来决定如何显示响应体的内容。
如果 Content-Type
字段的值被浏览器判断为下载类型,那么该请求会被提交给浏览器的下载管理器,同时该 URL 请求的导航流程就此结束。但如果是 HTML,那么浏览器则会继续进行导航流程。
3. 准备渲染进程
如果协议和根域名相同,则属于同一站点。
但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点的话,那么新页面会复用父页面的渲染进程。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
4. 提交文档
所谓提交文档,就是指浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:
首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息。
渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
5. 渲染阶段
一旦文档被提交,渲染进程便开始页面解析和子资源加载。
渲染进程将
HTML
内容转换为能够读懂的DOM
树结构。渲染引擎将
CSS
样式表转化为浏览器可以理解的styleSheets
,计算出DOM
节点的样式。创建布局树,并计算元素的布局信息。
对布局树进行分层,并生成分层树。
为每个图层生成绘制列表,并将其提交到合成线程。
合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
合成线程发送绘制图块命令
DrawQuad
给浏览器进程。浏览器进程根据
DrawQuad
消息生成页面,并显示到显示器上。
最后
以上就是笔者对这一常考面试题的一些总结,对于其中的一些具体过程并没有详细地列举出来。如有不足欢迎大家在评论区指出......
作者:codinglin
来源:juejin.cn/post/7202602022355779644
vue中Axios添加拦截器刷新token的实现方法
vue中Axios添加拦截器刷新token的实现方法
Axios是一款网络前端请求框架,本文主要介绍了vue中Axios添加拦截器刷新token的实现方法,
1. Axios基本用法:
const response = await Axios.create({
baseURL: "https://test.api.com",
headers: {
'Content-Type': 'application/json',
},
}).post<RequestResponse>('/signin', {
user_id: "test_user",
password: "xxx",
});
其中,RequestResponse是返回的数据要解析为的数据类型,如下:
export interface RequestResponse {
data: any;
message: string;
resultCode: number;
}
这样,得到的response就是网络请求的结果,可以进行判断处理。
2. Axios基本封装用法:
对Axios进行简单的封装,使得多个网络请求可以使用统一的header等配置。
新建一个工具类,进行封装:
import Axios, { AxiosRequestConfig, AxiosError, AxiosInstance, AxiosResponse } from 'axios';
export const BASE_URL = "https://test.api.com";
export const axiosApi = (): AxiosInstance => {
const instance = Axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
Authorization: `${getAccessToken()}`,
},
});
return instance;
}
const getAccessToken = () => {
// 这里获取本地保存的token
return xxxxx
}
然后使用的地方是这样:
const response = await axiosApi().post<RequestResponse>('/signin', {
user_id: "test_user",
password: "xxx",
});
3. 添加拦截器的用法
现在我们想再增加个功能,就是调接口时,header里传了token,但是有时候token过期了接口就会返回失败,我们想在封装的地方添加统一处理,如果token过期就刷新token,然后再调接口。
其中token的数据格式及解析方法已知如下:
import * as crypto from 'crypto';
import * as jwt from "jsonwebtoken";
export interface TokenData {
userid: string;
exp: number;
iat: number;
}
export const decodeJWT = function (token: string): TokenData {
if (!token) {
return null;
}
const decoded = jwt.decode(token, { complete: true });
return decoded?.payload;
};
如何统一刷新token呢?可以添加拦截器进行处理。把对Axios的封装再改下,添加拦截器:
export const axiosApi = (): AxiosInstance => {
const instance = Axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json',
Authorization: `${getAccessToken()}`,
},
});
// 添加拦截器
instance.interceptors.request.use(
config => {
return refreshToken(config);
},
err => {
return Promise.reject(err)
}
)
return instance;
}
// 刷新token的方法
const refreshToken = async (config: AxiosRequestConfig) => {
const oldToken = getAccessToken();
if (!oldToken) { //如果本地没有token,也就是没登录,那就不用刷新token
return config;
}
const tokenData = decodeJWT(oldToken);//解析token,得到token里包含的过期时间信息
const currentTimeSeconds = new Date().getTime()/1000;
if (tokenData && tokenData.exp > currentTimeSeconds) {
return config; // token数据里的时间比当前时间大,也就是没到过期时间,那也不用刷新
}
// 下面是刷新token的逻辑,这里是调API获取新的token
const response = await signInRefreshToken(tokenData?.userid);
if (response && response.status == 200) {
const { token, refresh_token } = response.data?.data;
// 保存刷新后的token
storeAccessToken(token);
// 给API的header设置新的token
config.headers.Authorization = token;
}
return config;
}
经过这样添加了拦截器,如果token没过期,就直接进行网络请求;如果token过期了,那就会调接口刷新token,然后给header设置新的token再进行网络请求。
4. 注意事项:
要注意的一点是,实际应用时,要注意:
1.刷新token时如果调接口,所使用的网络请求工具不能也使用这个封装的工具,否则就会陷入无限循环,可以使用简单未封装的方式请求。
2.本例使用的方法,是进行请求前刷新token。也可以使用先调网络请求,如果接口返回错误码表示token过期,则刷新token,再重新请求的方式。
作者:程序员小徐同学
来源:juejin.cn/post/7159727466439770119
使用 husky 实现基础代码审查
在日常提交 PR 的过程中,我们提交的文件不应该有例如 console、debugger、test.only 等调试语句,这会影响到线上代码。那每次提交之前都检查似乎又像是一个繁琐的工作,如果有个工作能代替我们检查我们提交的代码,让不能提交到线上的代码在 commit 阶段停止下来,对 code reviewer 的工作会减少不少。 这里就来跟大家探讨一下我的一个实现方式。
前言
在提交代码的时候不知道大家有没有注意命令行中会打印一些日志:
像这里的
husky > pre-commit
🔍 Finding changed files since ...
🎯 Found 3 changed files.
✅ Everything is awesome!
husky > commit-msg
这个出处大家应该都知道,来自 pretty-quick ,然后通过 packge.json 中的:
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "pretty-quick --staged"
}
}
这段代码在我们 commit 的时候对代码进行相关处理。
这里是 husky 调用了相关的 Git hooks ,在合适的时机处理我们提交的代码文件,从而使我们的代码达到提交的要求(如上面的是格式化相关代码)。
看到这里肯定大家就会想到,那这个是不是可以做的还有更多?
没错,下面就直接上配置,来实现我们不想让某些代码提交到线上这样的需求。
第一版
知道这个原理,那就很简单了,我们在 pre-commit
这个事件触发的时候对我们要提交代码检查一下,看其中有没有那几个关键字就可以了。
那就直接动手:
我们在项目根目录找到 .git/hooks
:
可以看到,这里提供了各种触发时机,我们找到我们想要的 pre-commit
(如果后缀有 .sample,需要去除掉才能让此文件生效)。打开此文件,前端的话应该看到的是(不需要阅读):
#!/bin/sh
# husky
# Hook created by Husky
# Version: 2.7.0
# At: 2023/2/2 13:14:26
# See: https://github.com/typicode/husky#readme
# From
# Directory: /Users/frank/Documents/work/worktile/wt-cronus/projects/pc-flow-sky/node_modules/husky
# Homepage: undefined
scriptPath="node_modules/husky/run.js"
hookName=`basename "$0"`
gitParams="$*"
debug() {
if [ "${HUSKY_DEBUG}" = "true" ] || [ "${HUSKY_DEBUG}" = "1" ]; then
echo "husky:debug $1"
fi
}
debug "$hookName hook started"
if [ "${HUSKY_SKIP_HOOKS}" = "true" ] || [ "${HUSKY_SKIP_HOOKS}" = "1" ]; then
debug "HUSKY_SKIP_HOOKS is set to ${HUSKY_SKIP_HOOKS}, skipping hook"
exit 0
fi
if ! command -v node >/dev/null 2>&1; then
echo "Info: can't find node in PATH, trying to find a node binary on your system"
fi
if [ -f "$scriptPath" ]; then
# if [ -t 1 ]; then
# exec < /dev/tty
# fi
if [ -f ~/.huskyrc ]; then
debug "source ~/.huskyrc"
. ~/.huskyrc
fi
node_modules/run-node/run-node "$scriptPath" $hookName "$gitParams"
else
echo "Can't find Husky, skipping $hookName hook"
echo "You can reinstall it using 'npm install husky --save-dev' or delete this hook"
fi
看了下,感觉没啥用,就是一个检测 husky 有没有安装的脚本。我们这里直接使用下面的替换掉:
#!/bin/sh
errorForOnly() {
result=""
for FILE in `git diff --name-only --cached`; do
# 忽略检查的文件
if [[ $FILE == *".html"* ]] ; then
continue
fi
# 匹配不能上传的关键字
grep 'serial.only\|console.log(\|alert(' $FILE 2>&1 >/dev/null
if [ $? -eq 0 ]; then
# 将错误输出
echo '❌' $FILE '此文件中包含 [only]、[console]、[alert] 中的关键字, 删除后再次提交'
# exit 1
result=0
else
result=1
fi
done
if [[ ${result} == 0 ]];then
exit 1
fi
echo "✅ All files is OK!"
}
errorForOnly
然后我们在一些文件中添加我们不想要的关键字,然后 git commit
:
可以看到错误日志以及文件已经在命令行中打印出来了,同时文件也没有进入本地仓库(Repository),让然在我们的暂存区(Index)。
使用这个方式,在一定程度上我们避免了提交一些不想要的代码到线上这种情况了发生。同时也是在本地提交代码之前就做了这个事情,也避免了使用服务端 hooks 造成提交历史混乱的问题。
这时候你肯定会产生这样的疑问:
【欸,欸,欸,不对啊】
问题
这种方式有个显而易见的问题,那就是不同团队协同方面。由于 hooks 本身不跟随克隆的项目副本分发,所以必须通过其他途径把这些 hooks 分发到团队其他成员的 .git/hooks
目录并设为可执行文件。
另外一个问题是我们使用了 husky,在每次 npm i
之后都会重置 hooks 文件。也就是 .git/hooks/pre-commit
文件恢复到了最初(只有 husky 检测的代码)的样子,没有了我们写的逻辑。
这种情况是不能允许的。那就寻找解决途径。查了下相关文档,发现可以使用新版的 husky 来解决这个问题。(其他相关的工具应该也可以,这里使用 husky 来进行展示)。
最新实现
husky 在 v4 版本之后进行了大的重构,一些配置方式不一样了,至于为什么重构,大家可以去
安装
安装 npm install husky --save-dev
安装最新版本为 8.0.3
,安装完成后启用 Git hooks: npx husky install
在团队协作的情景下,得让本团队的其他人也能自动的启用相关 hooks ,所以添加下面这个命令,在每次 npm install 之后执行:
npm pkg set scripts.prepare="husky install"
我们就在 package.json 得到了这样的命令:
yarn2+ 不支持 prepare 生命周期脚本命令, 安装方式在此处
使用
先按照官方文档测试一下,执行 npx husky add .husky/pre-commit "npm test"
在相关目录我们就看到:
相应的文件以及内容已经准备就绪。这里就不运行了。
那如何将之前的流程使用新的版本配置好呢?
这里直接提供相关文件内容:
# .husky/commit-msg
# 用于 commit 信息的验证
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit $1
使用下面的语句生成;
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit `echo "\$1"`'
另外还有 pre-commit:
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# pretty-quick 相关
npx pretty-quick --staged
# 验证是否有调试关键字的脚本:
node bin/debugger-keywords.js
脚本内容:
// bin/debugger-keywords.js
const { execSync } = require('child_process');
const fs = require('fs');
const gitLog = execSync('git diff --name-only --cached').toString();
const fileList = gitLog.split('\n').slice(1);
const keyWordsRegex = /(console\.(log|error|info|warn))|(serial\.only)|(debugger)/g;
const result = [];
for (let i = 0; i < fileList.length; i++) {
const filePath = fileList[i].trim();
if (filePath.length === 0 || filePath.includes('.husky') || filePath.includes('bin/')) {
continue;
}
const fileContent = fs.readFileSync(`${filePath}`, 'utf8');
const containerKeyWords = Array.from(new Set(Array.from(fileContent.matchAll(keyWordsRegex), m => m[0])));
if (containerKeyWords.length > 0) {
const log = `❌ ${filePath} 中包含 \x1B[31m${containerKeyWords.join('、')}\x1B[0m 关键字`;
result.push(log);
console.log(log);
}
}
if (result.length >= 1) {
console.log(`💡 修改以上问题后再次提交 💡`);
process.exit(1);
} else {
console.log('✅ All files is OK! \n');
process.exit(0);
}
为几个文件添加 console
、测试 only
等,提交 commit 后效果展示:
会提示文件中不合规的关键字是哪些。
更多
有了以上的使用示例,我们可以在随意添加脚本,比如,为 19:00 之后或周末提交代码的你来上一杯奶茶🧋和一个甜甜圈🍩:
// bin/check-time.js
const now = new Date()
const week = now.getDay()
const hour = now.getHours()
const validWeek = week >= 1 && week <= 5
const validHour = hour >= 9 && hour < 19
if (validHour && validWeek) return
console.log(`🌃 来点 🧋 🍩`);
这次为了方便也在 pre-commit hook 中执行:
# .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# pretty-quick 相关
npx pretty-quick --staged
# 验证是否有调试关键字的脚本:
node bin/debugger-keywords.js
# 来杯奶茶
node bin/check-time.js
来看下结果:
结论
目前单纯使用 pre-commit hook 针对常见的 console、debugger 还有测试 only 这种。但是我们也可以看到只要我们写不同的脚本,可以实现不同的需求。
之后如果有更多的需求也能继续添加脚本,也可以产生我们 PingCode 自己的 lint 插件。
ps. 如果使用可视化工具提交可能会报错,大家可以自行查阅解决。
作者:阿朋
来源:juejin.cn/post/7202392792726011959
复盘:前端岗位的寒冬,用这3点进行自救
前言
先介绍一下个人情况,方便给正在求职的朋友一个参考。
本人是19年非计算机专业某末端二本毕业,去年8月31日从一线的一家国企离职,回去家里休息了一个多月,在国庆结束之后落地到了新的城市,开始了我的前端自救之旅。
10月17日开始投递简历,到10月27日决定停止面试。一共面试了6家公司,最终拿到了2家公司的offer。
我简历主要写到了vue、微信公众号、微信小程序、node.js(主要是egg.js和koa.js)、webpack和团队管理的经验。
我对react的使用比较少,所以react问的比较深的岗位我都没有投递。
经过了半个多月的面试经历,我主要从市场环境、作为面试官的体验、面试题目三部分聊一下我的心得。
为了对前东家和现东家表示尊重,我分别用A市代替上一个城市,C市代替现在的城市,并且把关键信息隐藏。
市场环境
还在A市的时候,只是听同事说了一下外面的情况很糟糕,如果考虑换工作的话建议三思后行,一定要想清楚了再决定是不是要行动。
我这人有点不信邪,因为这样的话我好像都听腻了。
小时候老师这样劝诫我,长大了父母这样叮嘱我,工作了老板这样劝说我。
周围的声音都劝你,要走最安全的路,不要冒险。
直到我打开了脉脉。
为了保护截图楼主的隐私,特意打了马赛克。
从脉脉上面反馈的话题都很让人绝望,仿佛每个人在脉脉上过的都很惨。
每天刷出来的话题都跟职场末日一样。
但是我自己的路还是要自己走的,别人的世界和经历只能作为参考,无法影响我的决定。
于是,我选择性的忽视了脉脉话题的讨论。
专注于自己当下的问题,直面最现实的问题,并解决这些问题。
这里有一点建议需要给到仍在空窗期的朋友:如果此时正处于职场求职的阶段,应该尽可能的忽略会给你产生自我怀疑或者负能量的东西,这些东西的一点点腐蚀你的心智,磨灭你的念想,进而影响你对当下正确的判断。
面试官的体验
我在上一家公司担任的是技术负责人的身份,所以在去年8月初跟上级领导提出离职的申请之后,就开始帮公司招人了。
所以我也能从面试官的视角给求职者提供一些有利的信息,进而促成面试的成功。
主要从下面3点展开来说一下吧。
1、投递有可能没有回应
给大家分享一个简历投递的小故事。
8月12日,公司人事上午刚把前端的岗位发布出去,仅仅经过了2个小时,我就收到了人事发给我的50份简历。
然后我从这50份简历里面选出了12份作为候选人,接着人事给候选人打电话,7位候选人接通了电话,并预约了面试时间。
下午人事又筛选了60份简历给我,我从这些简历里面筛选出了8位候选人,人事打电话,6位接通了电话。
这样下周就有了14位候选人进行面试,我也跟人事说暂时先不接收简历了,当前的面试结束之后,再开放招聘岗位吧。
同时说一下我的心理状态吧。
上午我刚拿到简历的时候看的很仔细,每一份简历都很认真的去看,甚至会看一下他的项目介绍。
但是当我上午看完了50份简历,我觉得很多简历很雷同,长得都差不多,千篇一律的感觉。
下午又收到了60份简历,这个时候我基本看麻了,如果技术栈方面不匹配(比如我们项目要求的是vue,但是以前的项目都是react,这样的简历我就直接跳过了),基本就不会预约面试了。
所以说,招聘的时机是有很大的偶然性的。
如果你的投递时间正好是8月12号的上午,并且是在我看简历的前1个小时,只要能满足干活的能力,大概率都有面试邀约。
但如果投递时间是在8月12号的下午,必须要技术栈强匹配,项目相对复杂,简历写精炼简洁,能一眼抓住我的眼球,才能获得邀约。
另一方面来看,
如果投递时间不是8月12号这一天的话,我其实是看不到你投递的简历的。
因为在我说下周之内都不需要给我推简历之后,人事很大概率会把这个职位挂着。
但是面试者是不了解我这边的情况,看到岗位还在招聘,就会继续投递简历。但真实的情况是:我已经触达不到他们了,简历投进来基本石沉大海。
这样有一些朋友在投递过程中遇到的很多公司人事没有回馈,很大可能是遇到了上述我说的情况:简历都在排着队,触达不到面试官。
所以正在求职的朋友也不要灰心,把时间线拉长来看这个事情。公司跟个人之间是双向选择的,好事多磨,最终都能获得自己想要的。
2、简历是门面
简历是作为公司能够在没有见到真人情况下,对你能力最直接的预判,然后决定是否需要把你列入候选人名单的重要条件。
所以这个门面特别重要。
首先是基本信息一定要清晰。
姓名、年龄、性别、工作年限、手机号、邮箱、学历跟求职地点一定要写清楚,这些算是必填项了。
然后技术栈尽量简短概括,陈述你大概能做哪些事情。
我在招聘求职者的时候首要看这一项,技术栈不精练的可能就直接跳过了。
像jquery、bootstrap这些比较老的库,或者说类似elementui等ui库尽量都不要写上去,看上去显得重复累赘,没有突出重点。
还有学历方面,上家公司的招聘学历是硬性要求的。
我也看到有技术能力完全能胜任的的两个面试者,就因为学历问题被卡了。问了人事学历能不能放宽一点,她说是硬性规定,不能放开。
我想人事大概也是不想徒增麻烦吧,直接按照流程走总不会出错,也能理解。
但是站在专科的求职者角度,技能到了但是因为学历原因,也就缺少了一个机会,还是有点遗憾的。
但是对于卡学历的求职者,我也显得有心无力,帮不上忙。
3、自我介绍很重要
面试的第一步首先是自我介绍,我认为这个是非常重要的一步。
其实面试官在拿到求职者简历的时候,可能也没有很认真的阅读。
所以面试官让你进行自我介绍,一方面是留给自己时间让面试官有的一个看你简历重点的缓冲时间,另一方面也希望你说点跟简历上不一样的东西,然后从自我介绍中找到话题,向求职者提问。
在我的面试体验下来,我觉得会让我比较舒服的自我介绍时间是2分钟左右。
一方面过于简短的自我介绍会让我感觉到面试者对项目或者对自己的认识不够深入,或者自我介绍就是简历中已经写过问题的重述,都让我感觉准备不充分,有青涩感。
还有过短的自我介绍会让面试官在认真看简历的过程有有一种突然被打断,或者没太想好根据简历中哪一个具体的点进行发问而错失了首次接触的好感度。
当然我其实在面试时已经会建立一个准备提问的题库。
即使面试者没有项目亮点,我也能就根据我在开发中遇到的问题或者一些基本面试题进行发问。
面试本身两个人就像聊天一样,本质上还是人之间情感的交互。
只要有人的地方,就会掺杂个人的情感偏好,相互之间有情绪感情的传递。
提供正向的情绪,尽可能的体现自己的加分项、专业度能让面试官觉得你很靠谱、值得推荐。
同时也能增加面试官对你的好感度。
举个栗子:我在面试中遇到一个女生在自我介绍的时候,她先简要介绍了一下自己常用的技术栈以及对应技术栈的熟悉程度,然后介绍了她上家公司做的项目,分模块的介绍一下自己在这些模块中做的事情以及两个业务线之间的关系。
她的介绍大概在3分钟左右,整个介绍说的比较有条理、有逻辑性,在她做完自我介绍的之后,我基本已经知道从她项目中挖掘哪些问题进行提问了。
全程没有一句废话,这让我对她的专业度的好感就增加了;即使她后面vue的一些原理说的很磕磕巴巴,但最后因为第一印象,我还是推荐了她。
在我面试的所有女生中,提炼中心思想,能有条理且有主次的把自己的擅长的技术和业务说明白的,她是最好的一个。因为我所在的业务生产线需要做一些跨部门沟通,所以能一句话说清楚自己的需要,情商在线的话,能提高整个工作的效率。
面试中整理的面试题
接下来就是在这6家公司面试中遇到的问题了。
一、公司一
1、机试
1、把一个url拆解成origin、文件名、hash拆解成示例的格式。
2、两个数组合并成一个数组,并进行算法优化。
3、设置值的时候是数字,输出的时候变成百分号的格式。
4、首屏优化的方案,分别从代码、网络和缓存说一下。
5、如果一次性增加100万个用户访问项目,前端角度你会怎么优化。
6、分别用es5和es6的方式解决一个继承问题
2、现场面试
第一轮的机试过了,约了下周进行面试。
双休在知乎上搜了一下这家公司,网上风评不太好,技术栈也不是我熟悉的,就拒绝了接下来的面试。
二、公司二
1、一面
1、简单的介绍一下你自己
2、你们这项项目中有微信公众号、后台管理端、医生app端和小程序端,有没有提炼出一些公共的工具js,怎么提炼的?
3、你们的git分支管理是怎么样的?
4、你在做完前端项目之后,一般都会写哪些文档,readme里面写一些什么?
5、你做完一个项目之后写一些什么内容,让接手你的人能够看懂你的项目架构,并且迅速上手?
6、你基于你的脚手架做了哪些优化?
7、你们的项目文档一般都要记录哪些基本的东西?
8、有在项目中遇到过xss攻击吗?
9、你这个错误数据上报了哪些数据,怎么实现的?
10、成功抵御过多次攻击能具体说一说吗?
11、说一下你在项目中遇到印象深刻的项目场景,并且怎么解决的?
12、能说一下session跟jwt的优缺点吗?
13、你说用户登录之后要在session中加入user_name,为什么要增加?
14、jwt的整个流程怎么实现的?
15、实现jwt的实现遇到什么困难了吗?
16、如果同一部手机用户A登录了,更换了B登录,此时使用jwt会出现什么问题?
- 我觉得这里他给挖坑了,B登录了之后就会返回B用户的token,然后请求头带着的就是B的token,验证到的用户也就是B,我认为没有问题啊。我不太清楚他说的可能会有什么问题值啥情况,可能是我考虑不周全,如果有网友知道可以提醒一下我他想问的答案,让我学习一下。
17、当线上出了紧急bug的时候,你们是怎么处理的?
18、你们团队成员是怎么配合完成任务的?
19、你近2年的职业规划?
20、还有什么想问我的吗?
问的一些问题偏向于后端,面试官大概是java工程师吧。然后从团队管理、怎么让新来的成员快速接手项目、文档方便的管理等等;前端技术问题整个面试过程中占比不大。
看得出来至少要在团队管理方面有比较深的总结,并且对于团队管理积累上,自己有独有的见解,一面才能面试通过。
一面就败了的原因,主要偏向于管理方面的经验,这部分我总结比较少。
另一方面,我觉得有可能招聘单位也没太想清楚要招聘怎样的人吧,时间过去了半年,我今天刷了一下boss直聘,发现这家公司的招聘信息还挂着,人事活跃时间也是几分钟前。
三、公司三
1、一面
1、事件循环的机制了解吗?宏任务和微任务的执行顺序是怎样的?
2、怎么理解闭包这个定义的,在平时工作中有用到闭包的使用吗,举个例子。
3、vue组件间的哪些通信方式?
4、一个父组件潜嵌套了子组件,他的生命周期函数顺序是怎么执行的?
5、vue的权限管理应该怎么做?路由级和按钮级分别怎么处理?
6、说一下你对虚拟DOM的理解
7、了解diff算法吗?vue的diff算法是怎样的一个过程
8、能说一下v-for中key的作用吗?
9、做过vue项目哪些性能方面的优化?
10、vue组件为什么只能有一个根元素?
11、如何实现路由懒加载呢?
12、客户端渲染和服务端渲染有什么区别呢?在之前的工作中有做过服务端渲染吗?
13、Vue长列表的优化方式怎么做?
14、Vue3相比Vue2有哪些优化?
15、为什么在模板中绑定事件的时候要加.native?
16、能说一下响应式原理的过程吗?
17、数组的响应式怎么实现的?
18、Vue是数据改变后页面也会重新改变嘛;this.a = 1; this.a = 2; 他是怎么实现异步更新优化整个渲染过程的?
19、render函数封装有什么特别的,或者用到比较巧妙的东西吗?
20、浏览器缓存的方式有哪些?
21、正向代理和反向代理的区别?
22、域名解析过程是怎样的?
23、TCP协议三次握手、四次挥手的过程,为什么挥手要4次?
2、二面
1、nextTick, setTimeout 以及 setImmediate 三者有什么区别?
2、说一下你在项目的安全性做了哪些工作?
3、当一张表数据量比较多的时候,为了提高查询速度,你们一般会使用哪些方式做优化?
4、webSocket与传统的http相比有什么优势?
5、用过koa吗?简要阐述一下koa的洋葱模型。
6、用过promise吗?它的使用是为了解决一个什么问题?promise底层是怎么设计的?
7、你们现在整个登录鉴权是怎么设计的?如果要考虑单点登录呢,会如何设计?
8、如何用同一套代码部署到服务器中,怎么区分当前本地开发环境还是线上环境?是测试环境还是生产环境呢,怎么去区分?
9、待支付的订单,到期后主动取消这个功能你会怎么设计去做?
10、如果要做音视频的安全性,你能想到哪些方案?
11、多台服务器部署定时任务怎么保证一个任务只会做一遍呢?
12、你觉得程序员除了提升技术能力之外,其他什么能力你比较看重?
3、人事面
人事面遇到的问题都比较类似,我在下面会专门拿一个部分汇总人事面的问题。
该公司最后也拿到了offer。
四、公司四
1、你在项目中用到mongodb吗?
2、在项目中用到mongodb存储哪些数据?
3、mongodb的管道有了解吗?聚合管道怎么用的?
4、mongodb和的mysql优缺点?
5、你对事务性的了解是怎样的?
6、node怎么开启子进程?
7、在一台机器上开启负载均衡的时候,如果这个项目有用到定时任务,你怎么去控制这个定时任务只会执行一次?
8、你在egg中怎么开启子进程,怎么编写一个定时任务?
9、react用的多吗?
10、react组件间通信的方式有哪些?
11、vuex跟redux的区别有哪些?
12、computed和watch的区别?
13、watch和computed哪一个可以实现异步?
14、vue的通信方式有哪些?
15、vue的history模式和hash模式的区别是什么?
16、history模式下会出现404,怎么处理?
17、你能说一下闭包的优缺点吗?
18、内存泄漏和内存溢出有什么区别?
19、还有什么想问我的吗?
该岗位是node全栈工程师的岗位,对后端的知识点问题的比较深,一层一层的往下问,我后端的知识点稍微薄弱一些,很多很细的问题答不上来;他那边技术栈用的是react,我复习的知识也比较少。
扑街了理所当然。
五、公司五
1.1、自我介绍?
1.2、常用的选择器有哪些,优先级怎么样?(除了这些还有其他的嘛)
1.3、垂直居中的实现方案有哪些?
2.1、你说的网格布局grid垂直居中有哪些属性值?
2.2、width:100%和width:auto有什么区别?
3、说一下cookie的作用是什么?
4、cookie有哪些属性?
5、设置cookie的domain用来实现什么功能?
6、懒加载的实现原理是怎样的?(除了你说的那一种还有其他的嘛)
7、vue中路由懒加载怎么实现?(除了你说的这一种还有其他的嘛)
8、说一下原型链的理解?
9、原型链__proto__这个隐式属性的实现原理是怎样的?
10、说一下vue中双向数据绑定?
11、vue中computed和watch的区别是什么?
12、说一下你们的前端登录流程是怎样的?
13、jwt是什么?
14、jwt由哪些部分组成?
15、你在项目中怎么实现打包优化的?
16、你说的这些优化方式是webpack哪个版本的?
17、你说一下项目中比较困难的事情有哪些(BFF处理模式)?
18、你们部署上线是怎么做的?
19、在项目中有使用jekenis和docker这些吗?
20、有什么想问我的吗?
我记得网格布局是有justify-content
和align-items
属性,并且面试之后专门写了一个文件测试,测试通过。面试官说我属性记错了。
width:100%
和width:auto有什么区别?
这个问题我没回答出来,最后我问面试官他们的区别是什么。面试结束之后我按照他说的那种方式,也没测试出来区别,很纳闷。
原型链__proto__这个隐式属性的实现原理是怎样的?
,我以为这个问题就是让我说一下实例.__proto__
指向构造函数的原型,抓住这个点然后扩展一下原型链的知识就好了。他说不是要你回答这个,而是让我说一下proto的底层实现,这个问题我不知道,有知道的朋友可以在评论区帮我回复一下,我学习一下这个知识点。
整个面试从面试官的表达上可以看得出来他有一些紧张,导致有一些问题我也听的不是特别清楚。结束之后我测试的几个知识点也没达到他说的效果,遗憾。
最后应该是挂了。
六、公司六
拿到offer,出于公司隐私考虑,不方便透露具体过程。
人事面问题汇总
1、对自己的评价?
2、你有哪些兴趣爱好?
3、描述一下你自己的优缺点?或者用三个词语描述你自己?
4、你在公司主要做一些什么工作?
5、离职原因是什么?
6、在工作之外有哪些学习技术的方式?
7、公司的整个开发流程是怎样的,你跟团队成员如何配合完成任务?
8、你有女(男)朋友了吗(稳定性)?
9、你有其他offer吗(稳定性)?
10、如何提高工作效率?
11、与领导意见不统一时应该怎么办?
12、你觉得目前自己的技术在什么位置,觉得自己哪一块能力需要加强?
13、您还有什么问题想问我的嘛?
15、你的职业规划是怎样的?
16、入职之后如何开展工作?
17、是否愿意接受加班?
18、你能为公司带来什么?你希望公司给你什么?
19、在项目中遇到了什么难点问题,最后怎么解决的?
20、谈一下你在上一家公司整个技术开发流程,你负责哪些工作?
21、你希望自己以后的发展方向是什么?
知己知彼,从容不迫。
接下来的计划
在这次的面试中,也发现了自己能力的不足。比如我之前通过node写了一些提升团队的工具,用于提升自己在团队中的kpi。但是在纯前端中可能只能作为一家加分项来看,因为如果问到很具体的node的内存问题、mongodb细节的问题,可能就会被问住了,整个面试体验确实会比较糟糕。
C市很多大公司都是用的react,我之前在项目中用react比较少,所以空余时间要把react用起来,不用起来就会忘。
然后vue3的源码和优化的方面也要继续看看了。
还有这一次没有投递大厂的原因是我没有刷算法,这一点在之前的求职过程中都没有重视起来。现在年限到这了,必须要刷起来,才能更进一步提升自己。
给大家的一个建议
我在面试的过程中有一个体会,就是:当面试官问你一个很大的问题的时候,你要怎么回答?
比如面试官问题,简单的跟我说一下继承是什么吧?
很多朋友遇到这样的一个问题,马上就大脑蒙圈了,脑袋里很紊乱,知道很多,但是无法把他们串起来,不知道从何说起,导致回答的结果不是很好。
所以像这些技能题,我们在记笔记的时候也应该用这样的方式去记忆和背诵。遵循下面的几个步骤就行了,套公式的方式,一点也不会慌乱。
1、解释是什么的问题。
2、解释这个技术的应用点、应用场景在哪里。
3、整理一下这个问题的优缺点是什么。
我举一个例子来回答一下。比如面试官问:你给我讲一下闭包吧?
我就可以按照上面的归纳,分3步走的原则。
1. 闭包是:能够访问其他函数内部变量的函数。
2. 闭包一般会在:封装模块的时候,通过函数自执行函数的方式进行实现;或者在模仿块级作用域的时候实现;如:我们常用的库jQuery本身就是一个大的闭包。
3. 闭包的优点是:
a、能够在离开函数之后继续访问该函数的变量,变量一直保存在内存中。
b、闭包中的变量是私有的,只有闭包函数才有权限访问它。不会被外面的变量和方法给污染。
闭包的缺点是:
a、会增加对内存的使用量,影响性能。
b、不正确的使用闭包会造成内存泄漏。
针对上面闭包的回答,可能面试官又会继续问你:内存泄漏是什么啊,你能给我讲一下吗?垃圾回收机制说一下吧?内存泄漏和内存溢出的区别是什么?
等等。
就是你回答完了这个问题之后,你可以先假设性的想一下面试官看到你的这个回答可能会问你什么。然后给自己提问深挖技术深度问题。
当然那不光是面试,自己平时学习深挖知识的时候,也可以用这种办法。
以上这3点,就是我在去年的面试,从自己的真实经历中的总结。
我觉得在求职的时候我遇到的问题,相信很多朋友有类似经历的朋友也会遇到。我就本着造轮子的这个心态,给大家输出一些流程化的经验,希望大家能在本次经济下行,行业萧条的情况下拿到好的结果。
另外,如果你想要更多关于前端方面的成长干货和学习方法与资料,欢迎你关注我这个账号「程序员摩根」,并且私信我。
里面有我最宝贵的私人学习经验,全都毫无保留分享给你,比如优质的前端编程类电子书,前端学习地图,前端入门到精通的学习路径、面试题等。
我的总结
去年10月份之后,整个面试下来,能明显感觉到整个市场的供需关系发生了变化。经过这一轮的洗刷之后,对整个前端求职者的深度、专业度一定会更上一个台阶。
所以在没找到更好的收入方式之前,刷题刷经验,技术上做精做深,这些事情还是需要重复做,认真做。
当然,即使在比较惨烈的市场环境下,我依然认为每个个体都有很大机会的。
我是个乐观主义者,积小胜为大胜。打能胜的仗,才能打胜仗。与各位共勉。
当然,这是我去年10月份的求职情况,今年的情况是否比去年年底还要更惨烈。我没有进入到实际的市场环境中体会。
希望还在求职的朋友们能在底部@我一下,跟大家分享一下目前你所在城市的前端求职现状,大家群策群力,一起商量下应对方法。
欢迎大家在评论区讨论一下。
如果这篇文档对你有帮助,欢迎点赞、关注或者在评论区留言,我会第一时间对你的认可进行回应。精彩内容在后面,防止跑丢,友友们可以先关注我,每一篇文章都能及时通知不会遗失。
作者:程序员摩根
来源:https://juejin.cn/post/7201491839815139389
最近很多人都在说 “前端已死”,讲讲我的看法
现状
我记得去年脉脉的论调还都是 客户端已死,前后端还都是一片祥和,有秀工资的,有咨询客户端转前端的,怎么最近打开脉脉一看,风向变了?
随便刷几下,出来的信息都是 前端已死,这种悲观信息,还有失业找不到工作的。
思考
我有时候会想,开源是个好东西,拉低了技术的门槛,好像再难的需求,只要善用搜索引擎,都能找到前人喂到嘴边的答案,如果没有开源文化,甚至自己能不能进入互联网开发行业都不好说。
记得那一年:
我第一次接触技术领域,开始在百度输入前端开发相关的问题,发现出来的结果都是 csdn
、博客园、简书等网站的内容,打开了新天地,我开始吸取前辈们的经验和心得,他们分享的各种书上学不到的知识让我解决了工作中遇到的各种问题,让我感叹 开源真好;
我发现程序员们都推崇谷歌搜索和谷歌浏览器,于是自己也开始使用,发现 真香;
我日夜沉迷在技术研究上,为自己找到了一份兴趣爱好和职业相结合的工作而庆幸和兴奋;
大佬们都说要追求 高复用、高内聚、低耦合、易拓展,于是我忙不停蹄的学习这些概念和应用实践;
后来社区开始讨论 低代码,自动化,人工智能,大家好像都蛮兴奋的;
后来听说 客户端开发 不行了,小程序、h5 分走了大量的市场需求,我很庆幸,当初选择了前端,但我隐隐有些不安,因为我发现自己达到了 瓶颈,从业务开发中已经难有太多的技术提升了;
我开始学习 Java
, springboot + mysql + mybatisPlus
做了一些简单的 crud
,貌似并不是很难,我又有了些自信,以后前端干不了了,还可以搞后端;
我相信 如果一种思想不能拿出来给公众思辨,那么它和不存在没什么两样,所以我开始写博客,我认为这是最好的学习方式,但是我还不太擅长,22年一年才写了20+篇;
然后 chatGPT
出来了,我注册体验了一番,它真的可以在很多场景下协助我提升效率,这点资本也看得到,所以直接后果就是市场可能不需要那么多 开发者了, 这不局限在前端,甚至 chatGPT
更擅长写后端代码;
最后,互联网市场已进入红海之争了,存量市场下,开源节流是正常的操作,去年就经常看到外国各大大厂裁员的消息,国内就更不可能独善其身了;
总结
若没有开源文化,会不会互联网开发,也是一个越老越吃香的职业呢 [微笑]?我不知道,我是开源的受益者,我也愿意为开源做贡献,但是我不会期待它能给我带来多大的商业收益了,开源和商业付费之间,是两种文化之争;
最近core-js 作者对开源社区的“控诉”更是印证了我的看法: core-js 作者快被缺钱“拖垮”了:全职做开源维护 9 年,月均收入从 2500 美元锐减到 400 美元
前端已死 更多的是一种焦虑情绪的表达,市场确实不太好,但这并不是针对前端,整个互联网行业衰败的表现而已,对此持不同意见的怕是只剩培训机构了吧;
前端老鸟,市场还是需要和欠缺的,只是对于初中级前端太卷了,我建议应届生不要继续入门前端了,搞搞嵌入式开发,或者芯片之类的,门槛高一些。
总之,不用过于悲观,互联网风口过去了,还会有下一个风口,比如 web3
, 人工智能,都可能带来新的市场机会,作为时代前沿的参与者,程序员因该更容易抓住这样的机会吧。
作者:Ethan_Zhou
来源:juejin.cn/post/7201047960826052667
实现一个微信录音功能过程
功能原型图
其实就是微信发送语音的功能。没有转文字的功能。
拆解需求
根据原型图可以很容易的得出我们需要做的内容包括下面三个部分:
接入微信的语音SDK
调用微信SDK的API逻辑
界面和交互的实现
其中第一点和第二点属于业务逻辑部分,第三点属于交互逻辑部分。对于业务逻辑和交互逻辑的关系在我的另外一篇文章描述过,我在vue中是这样拆分组件的 - 掘金 (juejin.cn)
从原型图可以分析出如下的流程图:
评估时间
第三事情是评估时间。在接到这个需求的时候,我们需要假设我们在此之前没有接入过微信相关的SDK,并以此为前提进行工期的评估。
可以将该用户故事拆分为如下任务:
微信语音SDK的技术调研(0.5天)
输出开发设计文档(0.5天)
接入微信语音SDK(0.5天)
编码(1天)
自测(0.5天)
随后将上面的时间都乘以2! 自此才可以将估算的工期上报给产品。多年的经验告诉自己,自己一开始估算的工期从来没够过。自行估算的时候,幻想的是在工作的时候能够一直保持专注。
就我自己而言,做不到,上班不可能不摸鱼!也是必须要摸鱼的。乘以2才是刚够而已。
代码实现
都说在实现代码之前要先设计,谋定而后动。我是这样做的,先想好文件夹创建,然后到文件的创建,再到具体文件中写出大体的框架。
需求并不复杂,只是一个界面中的一个模块。所以我只需要一个Record.vue
来承载界面,一个use-record-layout.js
来承载业务逻辑,以及一个use-record-interact.js
来承接交互逻辑。
|__im-record
|__Record.vue
|__use-record-layout.js
|__use-record-interact.js
为了便于说明,将这个聊天的界面简化如下:
<script setup>
import { useNamespace } from "@/use-namespace";
const ns = useNamespace('chat')
</script>
<template>
<header :class="ns.b('header')"></header>
<main :class="ns.b('main')">
<section :class="[ns.b('record'), ns.w('record', 'toast')]">
<div :class="ns.w('record', 'speak')"></div>
<div :class="ns.w('record', 'pause')"></div>
</section>
</main>
<footer :class="ns.w('button', 'wrap')">
<button :class="ns.b('button')">
<span>
按住 说话
</span>
</button>
</footer>
</template>
通过上面的代码片段可知,我们的主要的界面在section
标签的record
部分。
use-record-layout.js
的主题代码如下:
const recordStyle = {
default: { }, // 默认样式/确定发送录音
recording: { }, // 录音中
pause: { }, // 暂停录音
cancel: { } // 取消录音
}
const init = () => {
initEvent()
initStyle()
}
const initStyle = () => {
recordStyle.default.is = true
}
const initEvent = () => {
el.addEventListener('touchstart', handleTouchstart)
el.addEventListener('touchmove', handleTouchmove)
el.addEventListener('touchend', handleTouchend)
}
const axis = {
posStart: 0, // 初始化起点坐标
posMove: 0 // 初始化滑动坐标
}
const handleTouchstart = (event) => {
event.preventDefault()
axis.posStart = event.touches[0].pageY
recordStyle.recording.is = true
}
const handleTouchmove = (event) => {
event.preventDefault()
axis.posMove = event.targetTouches[0].pageY
const diffMove = axis.posMove - axis.posStart
if (diffMove > DEFAULT_AXIS) {
recordStyle.recording.is = true
}
}
const handleTouchend = (event) => {
event.preventDefault()
recordStyle.default.is = true
}
init()
其中recordStyle是交互的结果,在这个需求当中,我们的界面的四种变化都对应其中一个的样式。
use-record-interact.js
也很简单,注册微信录音功能 ➡️
const wx = 'wx'
const useRecordInteract = () => {
const isAuth = localStorage.getItem('allowWxRecord')
// 获取录音权限
const authRecord = () => {
if (!isAuth) {
wx.startRecord()
return
}
return isAuth
}
// 停止录音
const stopRecord = () => {}
// 上传录音
const uploadRecord = () => {}
}
交互逻辑和业务逻辑的联动通过recordStyle
对象的存取属性来实现,代码片段如下:
const interact = useRecordInteract()
const recordStyle = {
default: {
_is: false,
get is() {
return this._is
},
set is(value) {
this._is = value
if (value) {
this.recording.is = false
this.pause.is = false
this.cancel.is = false
interact.uploadRecord()
}
}
},
//...
}
实现了业务逻辑和交互逻辑的分离。
作者:砂糖橘加盐
来源:juejin.cn/post/7201491839815745597
真的有必要用微前端框架么?
前言
最近公司项目在用qiankun构建微前端的应用,深深体会到微前端的魅力,无框架限制,主应用统一管理,弹窗的统一位置等。如果是刚开始就植入微前端还好,不过基本上都是后期老项目植入微前端,各种拆分模块,也是一件很头疼的事情。
基石
我们为什么要用微前端
大的应用体量维护成本是很高的,拆分成单独的模块,由主应用处理登录等通用逻辑,子应用来只负责模块的业务实现,这样不管资源加载、按需加载、人员维护成本降低、增量升级、独立部署都有很好的体检提升。当然前提是体量非常大的web应用可以这么做,但是开始做的时候你会很头疼各种拆解带来的不确定性,但是长痛不如短痛。
Why Not Iframe
下面是我从qiankun文档摘抄的:
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。那么为什么不用iframe呢 ?
- url 不同步。浏览器刷新 iframe url 状态丢失等(本地缓存不就行了?)
- UI 不同步,DOM 结构不共享(主应用控制不就行了?子应用通过postMessage传递数据给父应用)
- 一次性加载,慢!(个人感觉就是项目体积小了!跟iframe有啥区别么?)
- 全局上下文完全隔离,内存变量不共享。(当然这里通过postMessage是可以实现通信的!)
所以其实用iframe就够了,微前端是不是有点kpi的味道呢?当然学习下源码还是对自己有提升的,万一iframe没有,是不是可以手撸一个呢?
源码入口
最核心的就是手动加载loadMicroApp、registerMicroApps注册微应用,start开始构建这3个api,但其实qiankun的核心是基于single-spa框架封装的, 我们看下single-spa做了些什么,以及single-spa内部核心api的registerApplication做了什么
single-spa
single-spa是一个框架,用于将多个JavaScript微前端组合在一个前端应用程序中。使用单一页面中心构建前端可以带来许多好处,例如:
- 在同一页面上使用多个框架而无需刷新页面(React,AngularJS,Angular,Ember或你正在使用的任何框架)
- 独立部署您的微前端
- 使用新框架编写代码,无需重写现有应用
- 延迟加载代码可缩短初始加载时间
registerApplication 注册应用
export function reroute (pendingPromises = [], eventArguments) {
//...
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges(); //返回不同生命周期的队列
//记录基础的应用信息
let appsThatChanged,
navigationIsCanceled = false,
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);
//这里根据是否已挂载做处理
if (isStarted()) {
//....
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
//加载apps
function loadApps () {
return Promise.resolve().then(() => {
const loadPromises = appsToLoad.map(toLoadPromise);
return (
Promise.all(loadPromises)
.then(callAllEventListeners)
// there are no mounted apps, before start() is called, so we always return []
.then(() => [])
.catch((err) => {
callAllEventListeners();
throw err;
})
);
});
}
//根据app状态改变发布对应的事件
function performAppChanges () {
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
//...做了大量的自定义事件以及卸载事件
}
//....
}
复制代码
说实话这里的源码很绕,这里只摘取最关键的,在registerApplication内部,将qiankun的registerMicroApps的参数传入做些兼容判断,然后调用了一个核心的reroute方法, 这里删除了不必要的干扰信息,说白了single-spa做了spa的生命周期的管理,每个应用有单独的html做页面的加载,但是环境的隔绝是需要qiankun做的
getAppChanges 状态管理
export function getAppChanges () {
const appsToUnload = [],
appsToUnmount = [],
appsToLoad = [],
appsToMount = [];
// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
const currentTime = new Date().getTime();
apps.forEach((app) => {
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);
switch (app.status) {
case LOAD_ERROR:
if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (appShouldBeActive) {
appsToLoad.push(app);
}
break;
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
appsToUnload.push(app);
} else if (appShouldBeActive) {
appsToMount.push(app);
}
break;
case MOUNTED:
if (!appShouldBeActive) {
appsToUnmount.push(app);
}
break;
// all other statuses are ignored
}
});
return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}
复制代码
getAppChange返回了应用一旦改变那么不同生命周期的队列会更新
registerMicroApps注册微应用
简单的看下用法,qiankun的核心api并不多
registerMicroApps注册微应用中,传入apps的信息,如name、路由匹配规则、container挂载dom等,和生命周期的钩子lifeCycles。核心的应用状态管理在single-spa中,而qiankun在外层又做了统一的上层的主应用封装。这里比较重要的是loadApp,里面统一处理解决了全局变量混入的问题也就是沙箱隔离,样式隔离等。下面是loadApp的一部分核心逻辑
环境隔离
1. 全局变量的隔离
createSandboxContainer 创建沙箱
这里核心就是createSandboxContainer的用法,这里简单讲下几个核心的参数,initialAppWarpperGetter用于处理检查是否有无包裹的dom元素,scpredCSS是代表样式是否已经被隔离状态, useLooseSandbox和speedySandbox处理不同状态的沙箱。
,在里面核心的方法是patchAtBootstrapping做了不同的沙箱隔离方式的隔绝处理
patchAtBootstrapping 启动器
patchAtBootstrapping中的策略者模式,我们可以看到有3种沙箱的处理方式,legacyProxy、Proxy、Snapshot,并分别对应了pathLooseSandbox、pathStrictSandbox、patchLooseSandbox,下面简单解析下原理,理解即可。
Snapshot沙箱隔离
先将主应用的window拷贝一份,一旦微应用切换到主应用做回退,如果微应用切换,那么会提前生成微应用的diff过程的对象,然后回退。而缺陷就是diff属性量一旦过大会性能不好
Legacy沙箱隔离
那么与Snapshot最大的不同是用了Proxy来处理,set做记录,一旦应用切换就回退,相对于我不断循环遍历diff,性能好了不少
Proxy沙箱隔离
前面两种应用场景在于都是一个路由对应一个微应用,那么如果是多个微应用同时出现在一个页面中,那么环境是不是不可控了呢。这种情况就不能在window直接操作,而是要每个应用都要有一个独立的fakeWindow,这样区分环境后,数据处理尽量在fakeWindow上处理,而不是原生window
Proxy模式的核心的我们看下pathStrictSandbox源码
pathStrictSandbox 严格模式
Proxy代理模式的沙箱,通过Object.defineProperty
来拦截对象属性,但是不可枚举可写入, 这样每次切换应用我都重新获取新的nativeGlobal
nativeGlobal 全局对象
export const nativeGlobal = new Function('return this')();
复制代码
通过new Function来更安全的返回全局对象
2. DOM的隔离
很明显这里是通过ShadowDOM来实现dom的隔离,我们常见的比如video、audio标签内部都是可以看到shadowDOM实现的,同时我们也可以看到做了兼容性的处理
3. 样式隔离
scopedCSS代表是否要隔离css,如果要隔离首先去判断将微应用的根元素挂载qiankun的属性标记,然后遍历所有style标签,css.process对每个内部的样式属性名做了模块化的处理,而appInstanceId就是做微应用样式隔离的id区分
通信
import-html-entry
qiankun用的是import-html-entry这个库的execSceipts方法来请求获得并解析脚本的,然后直接把html插入到容器里,所以应用间需要允许跨域才行,在importEntry你可以发现他使用了浏览器空闲的api,requestIdleCallback以及为基础实现预加载prefetch
initGlobalState 全局状态
我们主要看下initGlobalState,通过emitGlobal来触发更新全局状态,从上图可以看出核心通过deps发布订阅模式来管理每个微应用,然后更新状态。返回的onGlobalChange和setGlobalState来监听变化和触发通知。状态管理还是比较简单的。
总结
花了几天时间看了源码,收获还是挺大的,微前端其实主要有3个的核心点在于应用通信、应用的生命周期及状态管理、沙箱环境隔离。相对来说iframe足够满足我们业务需求了,微前端提供了一种思路还是不错的,但是真的有必要用qiankun么?
链接:https://juejin.cn/post/7201282972967944250
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
JavaScript 预编译
预编译发生在什么时候
预编译发生在函数执行的前一刻
一. 预编译的抽象理解
函数声明整体提升
变量,声明提升
举个例子
<script type="text/javascript">
test();
function test() {
console.log('我是test');
}
a = 10;
var a;
console.log(a);
</script>
这里控制台能正常输出就是因为在预编译时将函数的声明整体和变量的声明提升到了代码的最顶部,所以代码中先调用函数或者是先给变量赋值再声明都没有问题,当然这是抽象的概念,记住这个已经能解决8-90%的变成问题了,如果你想了解具体的流程,可以接着往下看。
二. 局部函数预编译过程
创建AO(Activation Object)对象
找形参和变量声明,将变量和形参名作为AO对象的属性名,默认值为undefind
将实参值和形参相关联
找函数声明,值为其函数体
例子一
<script type="text/javascript">
function test(a) {
var b = 10;
function c() {
}
}
test(1);
</script>
根据过程来我们可以得出右边的AO对象中的数值,a最终保存的是1,因为第三步需要把形参和实参相关联
例子二
<script type="text/javascript">
function test(a) {
var a = 10;
function a() {
}
}
test(1);
</script>
对象中同名的属性会只保留一个,所以经过第二步寻找变量声明和形参的时候,AO对象中会有一个a,且默认值为undefind,经过第三步实参值和形参相关联后,a的值变为1,经过第四步找函数声明的时候发现有个名字为a的函数,AO中本身就有a的属性名了,所以这个时候会将a的函数体赋值给AO中的a。
如果上面的例子能够搞懂的话,咱们就可以接着玩一个题
例子三
<script type="text/javascript">
function test(a) {
console.log(a);
var a = 10;
console.log(a);
function a() {
consoel.log('这是函数a');
}
}
test(1);
</script>
如果你心中的结果跟答案一致,说明已经清楚了局部函数预编译的四个步骤了
到test函数执行之前AO对象中a的值为a的函数体这个应该没有什么问题吧?所以第一个log打印出来的是a的函数体,第二个log之前由于有var a = 10;,这个经过变量的声明提升后可以看做是a = 10;,走到这里这里AO中的a被赋值为10,所以第二个log打印的就是10。
二. 全局函数预编译过程
创建GO(Global Object)对象
找变量声明,将变量名作为GO对象的属性名,默认值为undefind
找函数声明,值为其函数体
全局函数的过程跟局部函数差不多,由于全局函数没有形参和实参的传递,所以省略了一个步骤。
例子四
<script type="text/javascript">
console.log(a);
var a = 1111;
console.log(a);
function a() {
console.log('这里是a函数');
}
</script>
这里例子四的输出和转化结果跟例子三差不多,只不过是AO对象变成了GO对象。
例子五
<script type="text/javascript">
console.log(a);
var a = 10;
function a(a) {
console.log(a);
}
a(1);
</script>
这里可以看看第二个log打印的是什么,还是会报错?
第一个log打印a函数的函数体没有问题吧?第二个log为什么会报错呢?因为GO对象中保存的a属性在第一次log的时候保存的是a的函数体,但是下面有个a=10;*的赋值,这个时候GO中的a就被修改成10了,后面调用*a(1)*函数的时候,找到GO中的a,这个时候a是number数字而不是函数,所以会报错说*a is not a function。
例子六
<script type="text/javascript">
var a = 1;
var b = 2;
function test(a) {
var a;
console.log(a); // 输出10
a = 100;
console.log(a); // 输出100
console.log(b); // 输出2
}
test(10);
console.log(a); // 输出1
</script>
看看例子六的输出结果是不是符合自己的心里预期。我们都知道函数内部有变量会优先使用自身内部的变量,其实也可以转化成AO和GO来理解。 第一个log输出的时候找到自身的AO中有属性a,且这个值是实参传递过来的,所以是10。 第二个log由于前面有a=100所以a被赋值成了100。 第三个log会找自己的AO,发现自己的AO里没有这个b属性,就会去找到父函数的AO,由于这里父函数是全局函数了,所以就去找GO里有没有b属性,有的话就输出了GO里的b的值,所以是2。(如果这里GO中也没有b属性的话,就会报b is not defined的错误了) 第四个log输出的就是自身AO也就是GO中的a属性的值了。
看完这几个例子相信你应该对预编译有个比较清晰的认识了,这里的面试题很多,但是万变不离其中,我们只需要把AO和GO分析出来,那么就可以清晰的了解到函数运行过程中每一步的每个属性值分别是什么了。
作者:Charlin丶
来源:juejin.cn/post/7200681438315642941
心血来潮,这次我用代码“敲”木鱼
技术栈
面对这种寿命短,后期也基本不需要维护的项目(更没有复杂的网络请求一说),本篇文章直接使用原生JavaScript
进行开发。或者您也可以尝试一下低代码
关于低代码,您大可放心的阅读此篇干货文章《低代码都做了什么?(为什么?怎么实现Low-Code?)》
至于TypeScript,您可以通过《谈谈写TypeScript实践而来的心得体会》这篇文章快速上手或进阶TS
实现
页面布局
图中右侧标出了三个部分:
img
标签用于指定木鱼的图片url地址,在木鱼进行缩放时,对该标签增加/删除
css类名即可- 每次敲击时所产生的文字由
p
标签生成,且所有的p
标签都存在于div
标签之下 audio
标签会在敲击时播放声音
本篇文章不会涉及具体的Html、Css部分。如有疑问,请在此项目的GitHub中找到答案
逻辑部分
准备工作
通过JavaScript
获取要操作的真实dom
const dom = {
// 木鱼
woodenFish: document.querySelector("img"),
// 文字浮层
text: document.querySelector(".w-f-c-text"),
// 音频
audio: document.querySelector("audio")
}
复制代码
木鱼缩放
这里的思路是敲击时给img
追加一个带有css animation
的样式类,该animation
的作用是让木鱼进行一次缩放,例如
.w-f-c-i-size {
/** 这里的animation只会执行一次缩放,所以后面会通过增加/删除该类名来达到可以进行n次缩放的效果 */
animation: wooden-fish-size 0.3s;
}
@keyframes wooden-fish-size {
0% {
transform: scale(1);
}
50% {
transform: scale(0.9);
}
100% {
transform: scale(1);
}
}
复制代码
样式搞定之后,通过原生JavaScript
提供的dom classList
进行css
样式类名的增加与删除。dom classList
共有四个方法:
add
:在指定节点上增加一个样式类名remove
:在指定节点上删除一个样式类名toggle
:在指定节点A
上若已有样式类名a
,则将a
删除;若没有样式类名a
,则添加类名a
replace
:将指定节点上的样式类名替换为另一个样式类名。效果同String##replace
一致
const woodenFish = {
// 封装一个用于增加/删除类名的方法
className(type) {
dom.woodenFish.classList[type]("w-f-c-i-size")
},
size() {
this.className("add")
setTimeout(() => this.className("remove"), 300)
}
}
复制代码
size
方法用于进行一次木鱼的缩放。调用该方法时,首先为img
标签增加类w-f-c-i-size
,在300毫秒
后,再将该类名移除
为什么是300毫秒
?因为css animation
的持续时间为300毫秒
需要注意的是,size
方法中的this
为woodenFish
对象,所以this.className
就相当于woodenFish.className
关于this或其它JavaScript的问题,您可以在《JavaScript每日一题》专栏中找到对应的题目进行练习
文字浮层
const woodenFish = {
className() {},
size() {},
createText() {
const p = document.createElement("p")
p.innerText = "功德+1"
dom.text.appendChild(p)
}
}
复制代码
createText
方法用于创建一个p
标签,该标签的文字内容为“功德+1”,随后将该标签追加在div
下即可
小tip:JSX(或react)中书写HTML类型的注释
博主在此刻书写document.createElement
这个原生方法时,突然想到了最近用到的一个原生属性outerHTML
,该属性与innerHTML
的区别就不再赘述。在JSX
中书写html
类型的注释,使用大括号的形式({/** */}
)是不可以的,因为在编译时这些东西都会被扔掉,此时可以使用由React
提供的dangerouslySetInnerHTML
属性,但体验感不太好。所以可以使用ouertHTML
配合ref
来解决,vue
同理,例如:
const HtmlComment: FC<HtmlCommentType> = ({ children }) => {
const virtual = useRef<HTMLSpanElement>(null)
useEffect(() => {
virtual.current!.outerHTML = `<!-- ${children} -->`
}, [])
return <span ref={virtual} />
}
复制代码
H5控制手机震动
const vibrate = () => {
const navigator = window.navigator
if (!("vibrate" in navigator)) return
navigator.vibrate =
navigator.vibrate ||
navigator.webkitVibrate ||
navigator.mozVibrate ||
navigator.msVibrate
if (!navigator.vibrate) return
// 上面的代码全是进行兼容性判断,只有下面这一行是发起手机震动的API
navigator.vibrate(300)
}
复制代码
像发起手机震动这类Api
,首先就要进行兼容性判断,所以上面vibrate
方法的90%
部分都在进行兼容性判断。注意,window.navigator
提供了一个用于发起设备震动的方法,即window.navigator.vibrate
window.navigator.vibrate
方法的参数:
- 一个
number
类型的值
这种方式表示震动持续多长时间,例如window.navigator.vibrate(300)
,则表示震动持续300毫秒
- 一个
number
类型的数组
这种方式表示震动、暂停间隔的时间。例如window.navigator.vibrate([100, 30, 100])
,则表示先震动100毫秒
,随后暂停30毫秒
,然后再震动100毫秒
window.navigator.vibrate
方法在震动成功时返回true
,否则返回false
vibrate兼容性
浏览器全屏操作
const toggleFullScreen = () => {
if (!document.fullscreenElement)
return document.documentElement.requestFullscreen()
if (!document.exitFullscreen) return
document.exitFullscreen()
}
document.addEventListener("keydown", (e) =>
e.keyCode == 13 ? toggleFullScreen() : false
)
复制代码
toggleFullScreen
方法会在全屏或非全屏之间来回切换,用到了以下属性/方法:
document.fullscreenElement
返回当前正在以全屏模式显示的元素,如果没有,则返回null
document.documentElement.requestFullscreen
用于发起全屏请求。若全屏请求成功,则该函数返回成功的Promise
对象,否则返回失败的Promise
对象。
在全屏成功时,全屏显示的元素会触发fullscreenchange事件;类似于输入框在输入时会触发onchange事件
document.exitFullscreen
方法用于使当前元素退出全屏模式
随后为document
绑定keydown
事件,如果按下了回车键,则在全屏/非全屏之间切换,否则不做出任何操作
音频事件操作
之前博主在写播放器的时候就发现音频的属性、方法、事件很多很多,所以此处只列举两个本项目中用到的方法
play()
使播放开始pause()
使播放暂停
制作完成
通过以上几个步骤就已经完成了所有要用到的东西,最后只需为木鱼注册“敲击”事件即可
dom.woodenFish.addEventListener("click", () => {
// 木鱼缩放
woodenFish.size()
// 创建文字浮层
woodenFish.createText()
// 播放敲击木鱼的声音
dom.audio.play()
// 发起手机震动
vibrate()
})
复制代码
文末
从一次心血来潮,到自己从0至1完成这个简单而有趣的小项目,无论是技术角度,还是个人收获角度来讲,都是收获满满!现在您可以通过以下两个地址来 “功德+1” :
由于时间匆忙,文中错误之处在所难免,敬请读者斧正。如果您觉得本篇文章还不错,欢迎点赞收藏和关注,我们下篇文章见!
链接:https://juejin.cn/post/7199660596735164475
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
z-index不生效?让我们来掀开它的面具
前言
hi大家好,我是小鱼,今天复习的是z-index
。之前以为自己很了解它,可是在工作中总会遇到一些不思其解的问题,后来去深入学习了层叠上下文、层叠等级、层叠顺序,才发现z-index
只是其中的一叶小舟,今天就一起来看看它背后到底隐藏着什么。
z-index
.container {
z-index: auto | <integer> ;
}
复制代码
z-index
属性是允许给一个负值的。z-index
属性支持 CSS3animation
动画。- 在 CSS 2.1 的时候,需要配合
position
属性且值不为static
时使用。
这个属性大家应该都很熟悉了,指定了元素及其子元素的 在 Z 轴上面的顺序,而 Z 轴上面的顺序 可以决定当元素发生覆盖的时候,哪个元素在上面。 z-index
值大的元素会覆盖较低的。
不知道大家在工作中有没有遇到过这种情况,明明给其设置了z-index
并且也设置了position
不为static
,但是样式并不是你所想的那样。可能这里大家对z-index
不太了解,判断元素在Z轴
上的顺序,不仅仅是z-index
值的大小,接下来给大家解释层叠上下文、层叠等级和层叠顺序。
层叠上下文
层叠上下文(stacking context),是HTML中一个三维的概念。如果一个元素含有层叠上下文,我们可以理解为这个元素在Z轴上就“高人一等”。
大家应该都玩过王者荣耀,里面的段位就是一个层级概念。你可以把「层叠上下文」理解为上了最强王者的人,还有很多没上王者的人,我们可以看成是菜鸡。那王者选手和菜鸡之间就形成了一个差距,这个差距也就是在Z轴上的距离,王者选手离荣耀王者就更近了一步,这里的“荣耀王者”可以看成是我们的屏幕观察者。
这样抽象解释完大家应该明白了什么是层叠上下文。继续往下看↓
层叠等级
层叠等级(stacking level),决定了同一个层叠上下文中元素在Z轴上的显示顺序。这里又牵扯出一个level
,那么这个等级指的又是什么呢?
所有的元素都有层叠等级,包括层叠上下文元素,层叠上下文元素的层叠等级可以理解为是什么普通王者,无双,荣耀传奇之类。然后,对于普通元素的层叠等级,我们的探讨仅仅局限在当前层叠上下文元素中。为什么呢?因为否则没有意义。
还是回到王者荣耀,元素具有层叠上下文就相当于是王者段位,但是王者里面又分为普通王者,无双王者和荣耀王者还有传奇王者,那我们如果拿普通王者的韩信和传奇王者的韩信相比较实际上是没有意义的,那不吊打吗,那他牛不牛逼是由段位决定的(排除一些意外情况哈哈哈)。
层叠上下文的创建
说白了就是一个元素如何才能变成层叠上下文元素?
层叠上下文是由一些特点的CSS属性创建的,分为三点:
- 页面根元素天生具有层叠上下文,称之为“根层叠上下文”。
- 普通元素设置
position
属性为非static
值并设置z-index
属性为具体数值,产生层叠上下文。 - 其他CSS3中的新属性也可以
flex
容器的子元素,且z-index
值不为auto
- grid 容器的子元素,且 z-index 值不为
auto
opacity
属性值小于 1 的元素transform
属性值不为none
的元素filter
属性值不为none
的元素isolation
属性值为isolate
的元素-webkit-overflow-scrolling
属性值为touch
的元素;
简单写两个例子
栗子一
.box1,.box2 {
position: relative;
width: 100px;
height: 100px;
}
.a,.c {
width: 100px;
height: 100px;
position: absolute;
font-size: 20px;
padding: 5px;
color: white;
border: 1px solid rgb(119, 119, 119);
}
.a {
background-color: rgb(0, 163, 168);
z-index: 1;
}
.c {
background-color: rgb(0, 168, 84);
z-index: 2;
left: 50px;
top: -50px;
}
<div class="box1">
<div class="a">A</div>
</div>
<div class="box2">
<div class="c">C</div>
</div>
复制代码
因为box1,box2都没有设置
z-index
,所以没有创建层叠上下文,所以其子元素都处于‘根层叠上下文’中,在同一个层叠上下文领域,层叠水平值大的那一个覆盖小的那一个。
栗子2
只帖了修改部分
.box1 {
z-index: 2;
}
.box2 {
z-index: 1;
}
.a {
background-color: rgb(0, 163, 168);
z-index: 1;
}
.b {
background-color: rgb(21, 84, 180);
z-index: 2;
left: 50px;
top: 50px;
}
.c {
background-color: rgb(0, 168, 84);
z-index: 999;
left: 100px;
top: 50px;
}
复制代码
大家可以发现我们给C盒子设置的
z-index
为999远大于A、B两个盒子,效果却出现在他俩下面。那是因为给两个父盒子分别设置了z-index
,创建了两个不同的层叠上下文,而box1的z-index
值大,所以排在上面,这里验证了层叠等级。
栗子3
有一个父元素绝对定位,它有一个子元素也是绝对定位,父元素z-index大于子元素z-index,为何子元素还是在父元素的上面?如何让这个子元素放在父元素的下面。
.parent {
width: 100%;
height: 500px;
background-color: rgb(243, 151, 45);
position: absolute;
z-index: 1;
}
.child {
width: 20%;
height: 150px;
background-color: rgb(211, 56, 56);
position: absolute;
z-index: 0;
}
<div class="parent">
<div class="child">C</div>
</div>
复制代码
效果却是这样
解决方案
因为父元素和子元素之间,z-index是无法对比的,同级之间的z-index才能对比。可以考虑换一种方式,两个div做同级,外面包一层父元素,根据共同的父元素定位、做层级区分就可以。
父元素不指定 z-index, 而子元素 z-index 为 -1
结论
普通元素的层叠等级优先由层叠上下文决定,所以,层叠等级的比较只有在当前层叠上下文元素中才有意义。
层叠顺序
层叠顺序(stacking order),表示元素发生层叠时候有着特定的垂直显示顺序,注意,这里跟上面两个不一样,上面的层叠上下文和层叠等级是概念,而这里的层叠顺序是规则。
上图↓
在不考虑CSS3的情况下,当元素发生层叠时,层叠顺序遵循上面图中的规则。
这里稍微解释下为什么内联元素的层叠顺序要比浮动元素和块状元素都高?有些同学可能觉得浮动元素和块状元素要更屌一点,图中我标注了内联样式是内容,因为网页中最重要的是内容,文字和浮动图片的时候优先确保显示文字。
层叠准则
- 谁大谁上: 当具有明显层叠等级的时候,在同一个层叠上下文领域,
z-indx
大的那一个覆盖小的那一个。 - 后来居上: 当元素的层叠等级一致、层叠顺序相同的时候,在DOM流中处于后面的元素会覆盖前面的元素。
栗子4
.box1,.box2 {
position: relative;
width: 100px;
height: 100px;
}
.box1 {
z-index: 0;
}
.box2 {
z-index: 0;
}
.a,.c {
width: 100px;
height: 100px;
position: absolute;
font-size: 20px;
padding: 5px;
color: white;
border: 1px solid rgb(119, 119, 119);
}
.a {
background-color: rgb(0, 163, 168);
z-index: 999;
}
.c {
background-color: rgb(0, 168, 84);
z-index: 1;
left: 50px;
top: -50px;
}
<div class="box1">
<div class="a">A</div>
</div>
<div class="box2">
<div class="c">C</div>
</div>
复制代码
上面给两个父盒子都设置了
z-index
为0,这里要注意z-index
一旦变成数值,哪怕是0,都会创建一个层叠上下文。当然层叠规则就发生了变化,子元素的层叠顺序比较变成了优先比较其父级的层叠上下文的层叠顺序,尽管a盒子的z-index
为999。又由于两个父级都是z-index:0
,层叠顺序这一块一样大,这个时候就遵循后来居上原则,根据DOM流中的位置决定谁在上面。也可以说子元素上面的z-index
失效了!
end
回顾自己以前使用z-index都不太规范或者滥用,以后一定改正!
链接:https://juejin.cn/post/7158409848692932621
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
为什么有公司规定所有接口都用Post?
看到这个标题,你肯定觉得离谱。怎么会有公司规定所有接口都用Post,是架构菜还是开发菜。这可不是夸大其词,这样的公司不少。
在特定的情况下,规定使用Post可以减少不少的麻烦,一起看看。
Answer the question
我们都知道,get请求一半用来获取服务器信息,post一般用来更新信息。get请求能做的,post都能做,get请求不能做的,post也都能做。
如果你的团队都是大佬,或者有着良好的团队规范,所有人都在平均水平线之上,并且有良好的纠错机制,那基本不会制定这样的规则。
但如果团队成员水平参差不齐,尤其是小团队,创业团队,常常上来就开干,没什么规范,纯靠开发者个人素质决定代码质量,这样的团队就不得不制定这样的规范。
毕竟可以减少非常多的问题,Post不用担心URL长度限制,也不会误用缓存。通过一个规则减少了出错的可能,这个决策性价比极高。
造成的结果:公司有新人进来,什么lj公司,还有这种要求,回去就在群里讲段子。
实际上都是有原因的。
有些外包公司或者提供第三方接口的公司也会选择只用Post,就是图个方便。
最佳实践
可能各位大佬都懂了哈,我还是给大家科普下,GET、POST、PUT、DELETE,他们的区别和用法。
GET
GET 方法用于从服务器检索数据。这是一种只读方法,因此它没有改变或损坏数据的风险,使用 GET 的请求应该只被用于获取数据。
GET API 是幂等的。 每次发出多个相同的请求都必须产生相同的结果,直到另一个 API(POST 或 PUT)更改了服务器上资源的状态。
POST
POST 方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或创建新资源。POST既不安全也不幂等,调用两个相同的 POST 请求将导致两个不同的资源包含相同的信息(资源 ID 除外)。
PUT
主要使用 PUT API更新现有资源(如果资源不存在,则 API 可能决定是否创建新资源)。
DELETE
DELETE 方法删除指定的资源。DELETE 操作是幂等的。如果您删除一个资源,它会从资源集合中删除。
GET | POST | PUT | DELETE | |
---|---|---|---|---|
请求是否有主体 | 否 | 是 | 是 | 可以有 |
成功的响应是否有主体 | 是 | 是 | 否 | 可以有 |
安全 | 是 | 否 | 否 | 否 |
幂等 | 是 | 否 | 是 | 是 |
可缓存 | 是 | 否 | 否 | 否 |
HTML表单是否支持 | 是 | 是 | 否 | 否 |
来源:https://juejin.cn/post/7129685508589879327
异步阻塞IO是什么鬼?
这篇文章我们来聊一个很简单,但是很多人往往分不清的一个问题,同步异步、阻塞非阻塞到底怎么区分?
开篇先问大家一个问题:IO多路复用是同步IO还是异步IO?
先思考一下,再继续往下读。
巨著《Unix网络编程》将IO模型划分为5种,分别是
- 阻塞IO
- 非阻塞IO
- IO复用
- 信号驱动IO
- 异步IO
个人认为这么分类并不是很好,因为从字面上理解阻塞IO和非阻塞IO就已经是数学意义上的全集了,怎么又冒出了后边3种模型,会给初学者带来一些困扰。
接下来进入正文。
文章首发于公众号:「蝉沐风的码场」
1. 一个简单的IO流程
让我们先摒弃我们原本熟知的各种IO模型流程图,先看一个非常简单的IO流程,不涉及任何阻塞非阻塞、同步异步概念的图。
客户端发起系统调用之后,内核的操作可以被分成两步:
等待数据
此阶段网络数据进入网卡,然后网卡将数据放到指定的内存位置,此过程CPU无感知。然后经过网卡发起硬中断,再经过软中断,内核线程将数据发送到socket的内核缓冲区中。
数据拷贝
数据从socket的内核缓冲区拷贝到用户空间。
2. 阻塞与非阻塞
阻塞与非阻塞在API上区别在于socket是否设置了SOCK_NONBLOCK
这个参数,默认情况下是阻塞的,设置了该参数则为非阻塞。
2.1 阻塞
假设socket为阻塞模式,则IO调用如下图所示。
当处于运行状态的用户线程发起recv系统调用时,如果socket内核缓冲区内没有数据,则内核会将当前线程投入睡眠,让出CPU的占用。
直到网络数据到达网卡,网卡DMA数据到内存,再经过硬中断、软中断,由内核线程唤醒用户线程。
此时socket的数据已经准备就绪,用户线程由用户态进入到内核态,执行数据拷贝,将数据从内核空间拷贝到用户空间,系统调用结束。此阶段,开发者通常认为用户线程处于等待(称为阻塞也行)状态,因为在用户态的角度上,线程确实啥也没干(虽然在内核态干得累死累活)。
2.2 非阻塞
如果将socket设置为非阻塞模式,调用便换了一副光景。
用户线程发起系统调用,如果socket内核缓冲区中没有数据,则系统调用立即返回,不会挂起线程。而线程会继续轮询,直到socket内核缓冲区内有数据为止。
如果socket内核缓冲区内有数据,则用户线程进入内核态,将数据从内核空间拷贝到用户空间,这一步和2.1小节没有区别。
3. 同步与异步
同步和异步主要看请求发起方对消息结果的获取方式,是主动获取还是被动通知。区别主要体现在数据拷贝阶段。
3.1 同步
同步我们其实已经见识过了,2.1节和2.2节中的数据拷贝阶段其实都是同步!
注:把同步的流程画在阻塞和非阻塞的第二阶段,并不是说阻塞和非阻塞的第二阶段只能搭配同步手段!
同步指的是数据到达socket内核缓冲区之后,由用户线程参与到数据拷贝过程中,直到数据从内核空间拷贝到用户空间。
因此,IO多路复用,对于应用程序而言,仍然只能算是一种同步,因为应用程序仍然花费时间等待IO结果,等待期间CPU要么用于遍历文件描述符的状态,要么用于休眠等待事件发生。
以select
为例,用户线程发起select
调用,会切换到内核空间,如果没有数据准备就绪,则用户线程阻塞到有数据来为止,select
调用结束。结束之后用户线程获取到的只是「内核中有N个socket已经就绪」的这么一个信息,还需要用户线程对着1024长度的描述符数组进行遍历,才能获取到socket中的数据,这就是同步。
举个生活中的例子,我们给物流客服打电话询问我们的包裹是否已到达,如果未到达,我们就先睡一会儿,等到了之后客服给我们打电话把我们喊起来,然后我们屁颠屁颠地去快递驿站拿快递。这就是同步阻塞。
如果我们不想睡,就一直打电话问,直到包裹到了为止,然后再屁颠屁颠地去快递驿站拿快递。这就是同步非阻塞。
问题就是,能不能直接让物流的人把快递直接送到我家,别让我自己去拿啊!这就是异步。
3.2 理想的异步
我们理想中的完美异步应该是用户进程发起非阻塞调用,内核直接返回结果之后,用户线程可以立即处理下一个任务,只需要IO完成之后通过信号或回调函数的方式将数据传递给用户线程。如下图所示。
因此,在理想的异步环境下,数据准备阶段和数据拷贝阶段都是由内核完成的,不会对用户线程进行阻塞,这种内核级别的改进自然需要操作系统底层的功能支持。
3.3 现实的异步
现实比理想要骨感一些。
Linux内核并没有太惹眼的异步IO机制,这难不倒各路大神,比如Node的作者采用多线程模拟了这种异步效果。
比如让某个主线程执行主要的非IO逻辑操作,另外再起多个专门用于IO操作的线程,让IO线程进行阻塞IO或者非阻塞IO加轮询的方式来完成数据获取,通过IO线程和主线程之间通信进行数据传递,以此来实现异步。
还有一种方案是Windows上的IOCP
,它在某种程度上提供了理想的异步,其内部依然采用的是多线程的原理,不过是内核级别的多线程。
遗憾的是,用Windows做服务器的项目并不是特别多,期待Linux在异步的领域上取得更大的进步吧。
4. 异步阻塞?
说完了同步异步、阻塞非阻塞,一个很自然的操作就是对他们进行排列组合。
- 同步阻塞
- 同步非阻塞
- 异步非阻塞
- 异步阻塞
但是异步阻塞是什么鬼?按照上文的解释,该IO模型在第一阶段应该是用户线程阻塞,等待数据;第二阶段应该是内核线程(或专门的IO线程)处理IO操作,然后把数据通过事件或者回调的方式通知用户线程,既然如此,那么第一步的阻塞完全没有必要啊!非阻塞调用,然后继续处理其他任务岂不是更好。
因此,压根不存在异步阻塞这种模型哦~
5. 千万分清主语是谁
最后给各位提个醒,和别人讨论阻塞非阻塞的时候千万要带上主语。
如果我问你,epoll
是阻塞还是非阻塞?你怎么回答?
应该说,epoll_wait
这个函数本身是阻塞的,但是epoll
会将socket设置为非阻塞。因此单纯把epoll
认为阻塞是太委屈它,认为其是非阻塞又抬举它。
具体关于epoll
的说明可以参见IO多路复用中的epoll
部分。
链接:https://juejin.cn/post/7199809805362495546
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
你还在傻傻的npm run serve吗?快来尝尝这个!
背景
大家在日常开发中应该经常会有需要切换不同环境地址的情况。当一个项目代码切换环境地址时,vue-cli没有能够感知文件的变化,所以代理的还是旧的地址,所以通常我们需要执行npm run serve
进行项目重跑,而项目重跑往往意味着长时间的等待,非常痛苦!
方案调研
事实上,其实我们只是需要重启webpack为我们启动的proxy代理服务
,或许能够从webpack的代理服务插件中找到解决方法。
从webpack官网可以看到proxy服务其实是由
http-proxy-middleware提供的,或许我们能够从中找到解决方法。
初步方案
在http-proxy-middleware的配置选项中,除了我们常见的target,还有router。router返回一个字符串的服务地址,当两个选项都配置了的情况下,会优先使用router函数的返回值,只有当router的返回值不可用时,才会使用target的值。
我们可以利用这一点来重新配置我们的项目代码。参考文档在这里
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
const { proxy } = require('./environments/proxy.js')
module.exports = defineConfig({
devServer:{
proxy
},
})
复制代码
// proxy.js
const fs = require('fs')
const path = require('path')
const encoding = 'utf-8'
const getContent = filename => {
const dir = path.resolve(process.cwd(), 'environments')
return fs.readFileSync(path.resolve(dir, filename), { encoding })
}
const jsonParse = obj => { return Function('"use strict";return (' + obj + ')')() }
const getConfig = () => { try {
return jsonParse(getContent('proxy-config.json'))
} catch (e) { return {} } }
module.exports = {
proxy: {
// 接口匹配规则自行修改
'/api': {
// 这里必须要有字符串来进行占位
// 如果报错Invaild Url,将target改成有效的url字符串即可,如http://localhost:9001
target: 'that must have a empty placeholder',
changeOrigin: true,
router: () => (getConfig() || {}).target || ''
}
}
}
复制代码
// proxy-config.json
{ "target": "http://localhost:9001" }
复制代码
自此,当我们需要修改环境地址时,只需要修改proxy-config.json
文件便能够实时生效,不再需要npm run serve
!
重点代码分析
实现代码中其实最主要的就是getContent
这个方法,我们项目在每次发起http请求时都会调用router中的函数
,而getContent则会通过node的fs服务,对我们的环境地址文件进行实时读取
,从而指向我们最新修改的环境地址。
方案总结
在按照参考文档配置了项目代码之后,我们发现确实能够及时指向新的环境地址,再也不需要重启代码,不需要长时间的等待了。但是,我们多了两个需要维护的文件,每次我们修改环境地址时,不仅需要修改config中的api,还需要修改proxy-config.json中的target!
有没有可能在只需要修改config文件的情况下,实现代理地址动态修改
呢?
方案优化
从上面的重点代码分析中,可以看到只要我们可以在router函数执行时,拿到正确的config文件中导出的api属性的值,也可以实现同样的效果!
这是不是意味着只要我们在函数中对config文件进行require请求,读取api的值,再return出去就能及时修改代理指向了呢?
没错,你会发现无论你怎么修改,函数内require取到的api永远是不变的,还是服务刚启动时的环境地址。
参考源码可以知道,这是因为我们在使用require请求文件信息时,node会解析出我们传入的字符串的文件路径的绝对路径,并且以绝对路径为键值,对该文件进行缓存
。
因此,如果我们在执行require函数时打断点进行观察的话,会发现require上面有一个cache缓存了已经加载过的文件。
这也恰恰说明了只要我们能够删除掉文件保存在require中的缓存,我们就能够拿到最新的文件内容,那么我们也可以据此得出我们的最终优化方案。
// vue.config.js
const hotRequire = modulePath => {
// require.resolve可以通过相对路径获取绝对路径
// 以绝对路径为键值删除require中的对应文件的缓存
delete require.cache[require.resolve(modulePath)]
// 重新获取文件内容
const target = require(modulePath)
return target
}
...
proxy: {
'/api': {
// 如果router有效优先取router返回的值
target: 'that must have a empty placeholder',
changeOrigin: true,
// 每次发起http请求都会执行router函数
router: () => (hotRequire('./src/utils/config') || {}).api || '',
ws: true,
pathRewrite: {
'^/api': ''
}
}
}
复制代码
自此,我们项目修改环境地址将不在需要重启项目,也不需要维护额外的文件夹,再也不需要痛苦等待了!
来源:https://juejin.cn/post/7198696282336313400
从 微信 JS-SDK 认识 JSBridge
前言
前段时间由于要实现 H5 移动端拉取微信卡包并同步卡包数据的功能,于是在项目中引入了 微信 JS-SDK(jweixin)
相关包实现功能,但也由此让我对其产生了好奇心,于是打算好好了解下相关的内容,通过查阅相关资料发现这其实属于 JSBridge
的一种实现方式。
因此,只要了解 JSBridge
就能明白 微信 JS-SDK
是怎么一回事。
为什么需要 JSBridge?
相信大多数人都有相同的经历,第一次了解到关于 JSBridge
都是从 微信 JS-SDK(WeiXinJSBridge)
开始,当然如果你从事的是 Hybrid 应用
或 React-Native
开发的话相信你自然(应该、会)很了解。
其实 JSBridge
早就出现并被实际应用了,如早前桌面应用的消息推送等,而在移动端盛行的时代已经越来越需要 JSBridge
,因为我们期望移动端(Hybrid 应用
或 React-Native
)能做更多的事情,其中包括使用 客户端原生功能 提供更好的 交互 和 服务 等。
然而 JavaScript 并不能直接调用和它不同语言(如 Java、C/C++ 等)提供的功能特性,因此需要一个中间层去实现 JavaScript 与 其他语言 间的一个相互协作,这里通过一个 Node
架构来进行说明。
Node 架构
核心内容如下:
顶层 Node Api
- 提供 http 模块、流模块、fs文件模块等等,
可以通过 JavaScript 直接调用
- 提供 http 模块、流模块、fs文件模块等等,
中间层 Node Bindings
- 主要是使 JavaScript 和 C/C++ 进行通信,原因是 JavaScript 无法直接调用 C/C++ 的库(libuv),需要一个中间的桥梁,node 中提供了很多 binding,这些称为
Node bindings
- 主要是使 JavaScript 和 C/C++ 进行通信,原因是 JavaScript 无法直接调用 C/C++ 的库(libuv),需要一个中间的桥梁,node 中提供了很多 binding,这些称为
底层 V8 + libuv
- v8 负责解释、执行顶层的 JavaScript 代码
- libuv 负责提供 I/O 相关的操作,其主要语言是
C/C++
语言,其目的就是实现一个 跨平台(如 Windows、Linux 等)的异步 I/O 库,它直接与操作系统进行交互
这里不难发现 Node Bindings
就有点类似 JSBridge
的功能,所以 JSBridge 本身是一个很简单的东西,其更多的是 一种形式、一种思想。
为什么叫 JSBridge?
Stack Overflow 联合创始人 Jeff Atwood
在 2007 年的博客《The Principle of Least Power》中认为 “任何可以使用 JavaScript 来编写的应用,并最终也会由 JavaScript 编写”,后来 JavaScript 的发展确实非常惊人,现在我们可以基于 JavaScript 来做各种事情,比如 网页、APP、小程序、后端等,并且各种相关的生态越来越丰富。
作为 Web 技术逻辑核心的 JavaScript
自然而然就需要承担与 其他技术 进行『桥接』的职责,而且任何一个 移动操作系统 中都会包含 运行 JavaScript 的容器环境,例如 WebView
、JSCore
等,这就意味着 运行 JavaScript 不用像运行其他语言时需要额外添加相应的运行环境。
JSBridge
应用在国内真正流行起来则是因为 微信 的出现,当时微信的一个主要功能就是可以在网页中通过JSBridge
来实现 内容分享。
JSBridge 能做什么?
举个最常见的前端和后端的例子,后端只提供了一个查找接口,但是没有提供更新接口,那么对于前端来讲就是再想实现更新接口,也是没有任何法子的!
同样的,JSBridge 能做什么得看原生端给 JavaScript 提供调用 Native 什么功能的接口,比如通过 微信 JS-SDK
网页开发者可借助微信使用 拍照、选图、语音、位置 等手机系统的能力,同时可以直接使用 微信分享、扫一扫、卡券、支付 等微信特有的能力。
JSBridge
作为 JavaScript
与 Native
之间的一个 桥梁,表面上看是允许 JavaScript 调用 Native 的功能,但其核心是建立 Native 和 非 Native 间消息 双向通信 通道。
双向通信的通道:
JavaScript 向 Native 发送消息:
- 调用 Native 功能
- 通知 Native 当前 JavaScript 的相关状态等
Native 向 JavaScript 发送消息:
- 回溯调用结果
- 消息推送
- 通知 JavaScript 当前 Native 的状态等
JSBridge 是如何实现的?
JavaScript 的运行需要 JS 引擎的支持,包括 Chrome V8
、Firefox SpiderMonkey
、Safari JavaScriptCore
等,总之 JavaScript 运行环境 是和 原生运行环境 是天然隔离的,因此,在 JSBridge 的设计中我们可以把它 类比 成 JSONP 的流程:
- 客户端通过
JavaScript
定义一个回调函数,如:function callback(res) {...}
,并把这个回调函数的名称以参数的形式发送给服务端 - 服务端获取到
callback
并携带对应的返回数据,以JS
脚本形式返回给客户端 - 客户端接收并执行对应的
JS
脚本即可
JSBridge 实现 JavaScript 调用的方式有两种,如下:
JavaScript
调用Native
Native
调用JavaScript
在开始分析具体内容之前,还是有必要了解一下前置知识 WebView。
WebView 是什么?
WebView 是 原生系统 用于 移动端 APP
嵌入 Web
的技术,方式是内置了一款高性能 webkit 内核浏览器,一般会在 SDK 中封装为一个 WebView
组件。
WebView
具有一般 View
的属性和设置外,还对 url
进行请求、页面加载、渲染、页面交互进行增强处理,提供更强大的功能。
WebView 的优势 在于当需要 更新页面布局 或 业务逻辑发生变更 时,能够更便捷的提供 APP 更新:
- 对于
WebView
而言只需要修改前端部分的Html、Css、JavaScript
等,通知用户端进行刷新即可 - 对于
Native
而言需要修改前端内容后,再进行打包升级,重新发布,通知用户下载更新,安装后才可以使用最新的内容
微信小程序中的 WebView
小程序的主要开发语言是 JavaScript
,其中 逻辑层 和 渲染层 是分开的,分别运行在不同的线程中,而其中的渲染层就是运行在 WebView
上:
运行环境 | 逻辑层 | 渲染层 |
---|---|---|
iOS | JavaScriptCore | WKWebView |
安卓 | V8 | chromium 定制内核 |
小程序开发者工具 | NWJS | Chrome WebView |
在开发过程中遇到的一个 坑点
就是:
- 在真机中,需要实现同一域名下不同子路径的应用实现数据交互(纯前端操作,不涉及接口),由于同域名且是基于同一个页面进行跳转的(当然只是看起来是),而且这个数据是 临时数据,因此觉得使用
sessionStorage
实现数据交互是很合适的 - 实际上从 A 应用 跳转到 B 应用 中却无法获取对应的数据,而这是因为 sessionStorage 是基于当前窗口的会话级的数据存储,移动端浏览器 或 微信内置浏览器 中在跳转新页面时,可能打开的是一个新的 WebView,这就相当于我们在浏览器中的一个新窗口中进行存储,因此是没办法读取在之前的窗口中存储的数据
JavaScript 调用 Native — 实现方案一
通过 JavaScript 调用 Native 的方式,又会分为:
- 注入 API
- 劫持 URL Scheme
- 弹窗拦截
【 注入 API 】
核心原理:
- 通过
WebView
提供的接口,向JavaScript
的上下文(window
)中注入 对象 或者 方法 - 允许
JavaScript
进行调用时,直接执行相应的Native
代码逻辑,实现JavaScript
调用Native
这里不通过 iOS
的 UIWebView
和 WKWebView
注入方式来介绍了,感兴趣可以自行查找资料,咱们这里直接通过 微信 JS-SDK 来看看。
当通过 的方式引入
JS-SDK
之后,就可以在页面中使用和 微信相关的 API,例如:
// 微信授权
window.wx.config(wechatConfig)
// 授权回调
window.wx.ready(function () {...})
// 异常处理
window.wx.error(function (err) {...})
// 拉起微信卡包
window.wx.invoke('chooseInvoice', invokeConf, function (res) {...})
如果通过其内部编译打包后的代码(简化版)来看的话,其实不难发现:
- 其中的
this
(即参数e
)此时就是指向全局的window
对象 - 在代码中使用的
window.wx
实际上是e.jWeixin
也是其中定义的N
对象 - 而在
N
对象中定义的各种方法实际上又是通过e.WeixinJSBridge
上的方法来实际执行的 e.WeixinJSBridge
就是由 微信内置浏览器 向window
对象中注入WeiXinJsBridge
接口实现的!(function (e, n) {
'function' == typeof define && (define.amd || define.cmd)
? define(function () {
return n(e)
})
: n(e, !0)
})(this, function (e, n) {
...
function i(n, i, t) {
e.WeixinJSBridge
? WeixinJSBridge.invoke(n, o(i), function (e) {
c(n, e, t)
})
: u(n, t)
}
if (!e.jWeixin) {
var N = {
config(){
i(...)
},
ready(){},
error(){},
...
}
return (
S.addEventListener(
'error',callback1,
!0
),
S.addEventListener(
'load',callback2,
!0
),
n && (e.wx = e.jWeixin = N),
N
)
}
})
【 劫持 URL Scheme 】
URL Scheme 是什么?
URL Scheme
是一种特殊的 URL
,一般用于在 Web
端唤醒 App
(或是跳转到 App
的某个页面),它能方便的实现 App
间互相调用(例如 QQ 和 微信 相互分享讯息)。
URL Scheme
的形式和 普通 URL
(如:https://www.baidu.com
)相似,主要区别是 protocol
和 host
一般是对应 APP
自定义的。
通常当 App
被安装后会在系统上注册一个 自定义的 URL Scheme
,比如 weixin://
这种,所以我们在手机浏览器里面访问这个 scheme
地址,系统就会唤起对应的 App
。
例如,当在浏览器中访问 weixin://
时,浏览器就会询问你是否需要打开对应的 APP
:
劫持原理
Web
端通过某种方式(如 iframe.src
)发送 URL Scheme
请求,之后 Native
拦截到请求并根据 URL Scheme
和 携带的参数
进行对应操作。
例如,对于谷歌浏览器可以通过 chrome://version/、chrome://chrome-urls/、chrome://settings/
定位到不同的页面内容,假设 跳转到谷歌的设置页并期望当前搜索引擎改为百度,可以这样设计 chrome://settings/engine?changeTo=baidu&callbak=callback_id
:
- 谷歌客户端可以拦截这个请求,去解析对应参数
changeTo
来修改默认引擎 - 然后通过
WebView
上面的callbacks
对象来根据callback_id
进行回调
以上只是一个假设哈,并不是说真的可以这样去针对谷歌浏览器进行修改,当然它要是真的支持也不是不可以。
是不是感觉确实和 JSONP
的流程很相似呀 ~ ~
【 弹窗拦截 】
弹窗拦截核心:利用弹窗会触发 WebView
相应事件来实现的。
一般是在通过拦截 Prompt、Confirm、Alert
等方法,然后解析它们传递过来的消息,但这种方法存在的缺陷就是 iOS
中的 UIWebView
不支持,而且 iOS
中的 WKWebView
又有更好的 scriptMessageHandler
,因此很难统一。
Native 调用 JavaScript — 实现方案二
Native
调用 JavaScript
的方式本质就是 执行拼接 JavaScript
字符串,这就好比我们通过 eval()
函数来执行 JavaScript
字符串形式的代码一样,不同的系统也有相应的方法执行 JavaScript
脚本。
Android
在 Android
中需要根据版本来区分:
安卓 4.4 之前的版本使用
loadUrl()
loadUrl()
不能获取JavaScript
执行后的结果,这种方式更像在的
href
属性中编写的JavaScript
代码webView.loadUrl("javascript:foo()")
安卓 4.4 以上版本使用
evaluateJavascript()
webView.evaluateJavascript("javascript:foo()", null);
IOS
UIWebView
中通常使用stringByEvaluatingJavaScriptFromString
results = [self.webView stringByEvaluatingJavaScriptFromString:"foo()"];
WKWebView
中通常使用evaluateJavaScript
[self.webView evaluateJavaScript:@"document.body.offsetHeight;" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
// 获取返回值
}];最后
来源:segmentfault.com/a/1190000043417038
开始!使用node搭建一个小页面
介绍
这个小demo是Node.js, Express, MongoDB & More: The Complete Bootcamp
系列课程的第一个demo,本篇文章主要介绍实现过程以及可能带来的思考。
完成展示
首页
详情页面
前置知识
首先我们需要了解一些知识,以便完成这个demo
fs
首先是node对文件的操作,也就是fs模块。本文只介绍一些简单的操作,大部分是例子中需要用到的方法。想要了解更多可以去API文档去查找。
首先引入fs模块:const fs = require("fs");
readFileSync
const textIn = fs.readFileSync("./txt/append.txt", "utf-8");
复制代码
上面代码展示的是readFileSync的使用,两个参数中,第一个参数是要读取文件的位置,第二个参数是编码格式encoding
。如果指定encoding
返回一个字符串,否则返回一个Buffer
。
writeFileSync
fs.writeFileSync("./txt/output.txt", textOut);
复制代码
writeFileSync
毫无疑问是写文件,第一个参数为写文件的地址,第二个参数是写入的内容。
readFile、writeFile
上面的两个API都是同步的读写操作。但是nodeJs作为一个单线程的语言,在很多时候,使用同步的操作会造成不必要的拥堵。例如等待用户输入这类I/O操作,就会浪费很多时间。所以 js中有异步的方式解决这类问题,nodejs也一样。通过回调的方式来解决。
fs.readFile("./txt/append.txt", "utf-8", (err, data) => {
fs.writeFile("./txt/final.txt", `${data}`, (err) => {
console.log("ok");
});
});
复制代码
http
createServer
http.createServer(requestListener);
复制代码
http.createServer() 方法创建一个HTTP Server 对象,参数requestListener
为每次服务器收到请求时要执行的函数。
server.listen(8001, "127.0.0.1", () => {
console.log("Listening to requests on port 8001");
});
复制代码
上面表代码表示监听8001端口。
url
url.parse
这个模块可以很好的处理URL信息。比如当我们请求http://127.0.0.1:8001/product?id=0
的时候通过url.parse
可以获取到很多信息。如下图:
实现过程
对于已经给出的完成页面,我们可以看到在切换页面时URL的变化,所以我们需要得到用户请求时的 URL地址,并根据地址展示不同的页面。所以我们通过path
模块得到pathname
,进行处理。
对于不同的请求,我们返回不同的界面。首先对于Overview page
界面,由于它的类型是 html界面,所以我们通过writeHead
将它的Content-type
设置为text/html
。
res.writeHead(200, {
"Content-type": "text/html",
});
复制代码
其他的几个返回html的页面也是同样的处理。由于前端界面已经给出,我们只需要读取JSON里面的数据,并将模板字符串替换即可。最后我们通过res.end(output)
返回替换后的页面。
总结
通过这一个小页面的练习,可以学习到node对文件的操作以及HTTP模块的操作。并对后端有了初步的认识。
链接:https://juejin.cn/post/7171295946372972557
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
一篇文章告诉你 <按钮> 如何选择,div button 还是a?
前言
当你要创建一个可点击的元素时,是使用 a标签
、button按钮
还是 div
等其他不同的元素?
// 🚩
export function MyButton() {
return <div onClick={...}>点击我</div>
}
//❓
export function MyButton() {
return <button onClick={...}>点击我</button>
}
//❓
export function MyButton() {
return <a onClick={...}>点击我</a>
}
复制代码
出人意料的是大多数人都会选择div
,这似乎与我们所学的很不一样。
这篇文章将展开对比三者的区别,并做了一个总结,这对于你工作或者面试的时候是很有帮助的。
div
让我们首先弄清楚一件事
您不应该将 div
用于可点击的元素(至少在 99% 的情况下)。
为什么?
严格上来说, div != 按钮。 div 只是一个通用容器,缺少一些可正确点击的元素应具备的特性,例如:
- Div 不可聚焦,例如,
tab
键不会像设备上的任何其他按钮那样聚焦 div。 - 屏幕阅读器和其他辅助设备不会将 div 识别为可点击元素。
- Div 不会将某些键输入(如空格键或返回键)转换为获得焦点时的点击。
但是,您可以使用 tabindex="0"
和 role=”button”
等几个属性解决其中一些问题:
// 🚩 试着将 div 改造成像 button一样...
export function MyButton() {
function onClick() { ... }
return (
<div
className="my-button"
tabindex="0" // 让div 能聚焦
role="button" // 屏幕阅读器和其他辅助设备 识别可点击
onClick={onClick}
onKeydown={(event) => {
// 聚焦时监听 回车键和空格键
if (event.key === "Enter" || event.key === "Space") {
onClick()
}
}}
>
点击我
</div>
)
}
复制代码
是的,我们需要确保设置聚焦状态的样式,以便用户反馈该元素也被聚焦。我们必须确保这通过了所有问题可访问性,例如:
.my-button:focus-visible {
outline: 1px solid blue;
}
复制代码
如果要还原所有细微且关键的按钮行为,并手动实现,需要大量工作。
button
The beauty of the button
tag is it behaves just like any other button
on your device, and is exactly what users and accessibility tools expect.
button
标签的美妙之处在于它的行为与您设备上的任何其他 button
一样,并且正是用户和辅助工具所期望的。
它是可聚焦的、可访问的、可键盘输入的,具有兼容的焦点状态样式!
// ✅
export function MyButton() {
return (
<button onClick={...}>
点击我
</button>
)
}
复制代码
有几个我们需要注意的问题。
button 的问题
我一直对按钮最大的烦恼是它们的样式。
例如,给按钮一个浅紫色背景:
<button class="my-button">
Click me
</button>
<style>
/* 🤢 */
.my-button {
background-color: purple;
}
</style>
复制代码
这看起来就像 Windows 95 一样的样式。
这就是为什么我们都喜欢 div。它们没有额外的样式或默认行为。它们的工作和外观每次都完全符合预期。
你可以说, appearance: none
会重置外观!但是这并不能完全按照您的想法进行。
<button class="my-button">
Click me
</button>
<style>
.my-button {
appearance: none; /* 🤔 */
background-color: purple;
}
</style>
复制代码
它仍然是这样:
重置 button
的样式
没错,我们必须对每一个样式属性逐行重置:
/* ✅ */
button {
padding: 0;
border: none;
outline: none;
font: inherit;
color: inherit;
background: none
}
复制代码
这就是一个样式和行为都像 div 的按钮,它仍然使用浏览器的默认焦点样式。
您的另一种选择是使用 all: unset
恢复一个简单属性中的无特殊样式:
/* ✅ */
button { all: unset; }
button:focus-visible { outline: 1px solid var(--your-color); }
复制代码
但是不要忘记添加您自己的焦点状态;例如,您的品牌颜色的轮廓具有足够的对比度。
修复 button
行为属性
使用 button
标签时需要注意最后一个问题。
默认情况下, form
内的任何按钮都被视为提交按钮,单击时将提交表单。
function MyForm() {
return (
<form onSubmit={...}>
...
<button type="submit">Submit</button>
{/* 🚩 点击 "Cancel"仍然会提交表单! */}
<button onClick={...}>Cancel</button>
</form>
)
}
复制代码
没错,按钮的默认 type
属性是 submit
。很奇怪。而且很烦人。
要解决此问题,除非您的按钮实际上是为了提交表单,否则请始终向其添加 type="button"
,如下所示:
export function MyButton() {
return (
<button
type="button" // ✅
onClick={...}>
Click me
</button>
)
}
复制代码
现在我们的按钮将不再尝试找到它们最接近的 form
parent 并提交它。
哇,配置一个简单的按钮几乎变得奇怪了。
a标签 链接
这是大部分人也不注意的一点。我们使用按钮链接到其他页面:
// 🚩
function MyLink() {
return (
<button
type="button"
onClick={() => {
location.href = "/"
}}
>
Don't do this
</button>
)
}
复制代码
使用 点击事件 链接到页面的按钮的一些问题:
- 它们不可抓取,因此对 SEO 非常不利。
- 用户无法在新标签页或窗口中打开此链接;例如,右键单击在新选项卡中打开。
因此,我们不要使用按钮进行导航。这就是我们需要 a
标签。
// ✅
function MyLink() {
return (
<a href="/">
Do this for links
</button>
)
}
复制代码
a 标签具有按钮的所有上述优点——可访问、可聚焦、可键盘输入——而且它们没有一堆默认的样式!
那我们是否应该将它们用于任何可点击的东西为我们自己省去一些麻烦?
// 🚩
function MyButton() {
return (
<a onClick={...}>
Do this for links
</a>
)
}
复制代码
不行
这是因为没有 href
属性 的 a
标签不再像按钮一样工作。没错,当它 href
属性有值时,才有完整的按钮行为,例如可聚焦... 。
所以,我们一定要坚持使用按钮作为按钮,使用锚点作为链接。
把 button
和 a
结合起来
我非常喜欢的是将这些规则封装在一个组件中,这样你就可以只使用你的 MyButton
组件,
如果你 提供一个 URL,它就会变成一个链接,否则就是一个按钮就像这样:
// ✅
function MyButton(props) {
if (props.href) {
return <a className="my-button" {...props} />
}
return <button type="button" className="my-button" {...props} />
}
// 渲染出一个 <a href="/">
<MyButton href="/">Click me</MyButton>
// 渲染出 <button type="button">
<MyButton onClick={...}>Click me</MyButton>
复制代码
这样,无论按钮的用途是单击处理程序还是指向另一个页面的链接,我们都可以获得一致的开发人员体验和用户体验。
总结
对于链接,使用带有
href
属性的a
标签,
对于所有其他按钮,使用带有
type="button"
的button
标签。
需要一个点击容器,就用
div
标签
链接:https://juejin.cn/post/7197995910566740025
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
CSS简单实现一幅新春对联
前言
今年过年家里没有贴春联,这两天在网上看到一幅对联,觉得写得挺好的, 上联——只生欢喜不生愁;下联——此心安处是吾家; 横批——平安喜乐, 因此使用css简单实现这幅新春对联。
具体实现
页面先做一个简单描述,首先页面中间有一个大门,然后在大门两侧实现春联的上下联,大门的上面实现春联的横批,再做一个打开大门,出现兔年祝福图片的效果。
效果展示:(毛笔字体文件没有线上的资源,所以字体没有效果) code.juejin.cn/pen/7197022…
页面整体布局:
<div class="wrapper">
<div class="container">
<div class="title">平安喜乐</div>
<div class="content">
<h1>此心安处是吾家</h1>
<div class="door">
<div class="door-l"></div>
<div class="door-r"></div>
<!-- 送福图片 -->
<img src="/4034970a304e251fb44609698ce95a1c7e3e536c.webp" alt="" class="pic">
</div>
<h1>只生欢喜不生愁</h1>
</div>
</div>
</div>
1. 大门的实现
大门的总体宽高都设置成350px,设置视角(perspective:1000px
), 大门打开的时候呈现一种3D的视觉感受。
大门分成左右两部分门扇,使用绝对定位控制左右的位置,并使用transform-origin属性
设置大门旋转动画的基点,默认情况下,元素的动作参考点(基点)为元素盒子的中心(center),这里设置左边门扇的transform-origin: left,左门扇以左边基点旋转;右边门扇的transform-origin: right,右门扇以右边基点旋转。
大门门扇的圆形门环使用伪元素实现,使用hover属性实现当鼠标移到大门上时,大门的门扇分别旋转一定的角度,实现打开大门的效果
兔年祝福图片使用绝对定位控制在大门的居中位置,并设置层级最低,当打开大门图片慢慢变大
.door {
width: 350px;
height: 350px;
border: 2px solid #333;
margin: 0 auto;
position: relative;
perspective: 1000px;
}
.door .pic{
position: absolute;
top: 50%;
left: 50%;
width: 70%;
object-fit: cover;
transform: translate(-50%,-50%);
z-index: -1;
transition: all 0.3s ease-in;
}
.door-l,
.door-r {
width: 50%;
height: 100%;
background-color: #e1b12c;
position: absolute;
top: 0;
transition: all 0.5s;
}
.door-l {
left: 0;
border-right: 1px solid #000;
transform-origin: left;
}
.door-r {
right: 0;
border-left: 1px solid #000;
transform-origin: right;
}
.door-l::before,
.door-r::before {
content: "";
border: 1px solid #000;
width: 20px;
height: 20px;
position: absolute;
top: 50%;
border-radius: 50%;
transform: translateY(-50%);
}
.door-l::before {
right: 5px;
}
.door-r::before {
left: 5px;
}
.door:hover .door-l {
transform: rotateY(-120deg);
}
.door:hover .door-r {
transform: rotateY(120deg);
}
.door:hover .pic{
width: 100%;
}
2. 春联的实现
春联一般是用毛笔写的,因此在网上找了一款毛笔字体下载下来,并引入到样式中,并给春联设置红色的背景
网上下载下来的毛笔字体为trueType格式(.ttf,Windows和Mac上常见的字体格式,是一种原始格式,没有为网页进行优化处理),需要转换成Web Open Font格式(.woff,针对网页进行特殊优化,是Web字体中最佳格式)。可以在这个网站上传字体进行转换
@font-face 用于设置自定义字体,可以自定义字体名称。两个必要属性:
font-family:给引入的字体起一个名称,注意:名字不要和那些专属的名称起冲突了,比如:微软雅黑。
src:自定义字体的路径,一般采用相对路径去使用。
@font-face {
font-family: 'YFJLXS8';
src: url('./font.woff2') format('woff2'),
url('./font.woff') format('woff');
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box
}
.wrapper {
height: 100vh;
font-family: 'YFJLXS8', 'Courier New', Courier, monospace;
padding: 50px;
overflow: hidden;
background: #ccc;
}
.content {
display: flex;
align-items: center;
justify-content: center;
width: 44%;
margin: 20px auto;
}
h1 {
font-size: 40px;
font-weight: 700;
width: 5vw;
color: #000;
line-height: 1;
text-align: center;
background-color: #d63031;
padding: 20px 0;
}
.title{
width: 20%;
font-size: 40px;
font-weight: 700;
text-align: center;
margin: 0 auto;
background-color: #d63031;
}
作者:sherlockkid7
来源:juejin.cn/post/7196994373237866553
详解css中伪元素::before和::after和创意用法
伪类和伪元素
首先我们需要搞懂两个概念,伪类和伪元素,像我这种没有系统全面性的了解过css的人来说,突然一问我伪类和伪元素的区别我还真不知道,我之前一直以为这两个说法指的是一个东西,就是我题目中的提到的那两个::before
和::after
。偶然间才了解到,原来指的是两个东西
伪类
w3cSchool对于伪类的定义是”伪类用于定义元素的特殊状态“。向我们常用到的:link
、:hover
、:active
、:first-child
等都是伪类,全部伪类比较多,大家感兴趣的话可以去官方文档了解一下
伪元素
至于伪元素,w3cSchool的定义是”CSS 伪元素用于设置元素指定部分的样式“,光看定义我是搞不懂,其实我们只要记住有哪些东西就好了,伪元素共有5个,分别是::before
、::after
、::first-letter
、::first-line
和::selection
伪类和伪元素可以叠加使用,如
.sbu-btn:hover::before
,本文后面示例部分也会用到此种用法。
::first-letter
主要用于为文本的首字母添加特殊样式
注意:
::first-letter
伪元素只适用于块级元素。
::first-line
伪元素用于向文本的首行添加特殊样式。
注意:
::first-line
伪元素只能应用于块级元素。
::selection
伪元素匹配用户选择的元素部分。也就是给我们鼠标滑动选中的部分设置样式,它可以设置以下属性
color
background
cursor
outline
以上几种我们简单了解一下就可以了,也不在我们今天的讨论范围之内,今天我们来着重了解一下::before
和::after
,相信大家在工作中都或多或少的用过,但很少有人真的去深入的了解过他们,本文是我对我所知的关于他们用法的一个总结,如有缺漏,欢迎补充。
用法及示例
::before
用于在元素内容之前插入一些内容,::after
用于在元素内容之后插入一些内容,其他方面的都相同。写法就是只要在想要添加的元素选择器后面加上::before
或::after
即可,有些人会发现,写一个冒号和两个冒号都可以有相应的效果,那是因为在css3中,w3c为了区分伪类和伪元素,用双冒号取代了伪元素的单冒号表示法,所以我们以后在写伪元素的时候尽量使用双冒号。
不同于其他伪元素,::before
和::after
在使用的时候必须提供content
属性,可以为字符串和图片,也可以是空,但不能省略该属性,否则将不生效。
给指定元素前添加内容
这个用法是最基础也是最常用的,比如我们可以给一个或多个元素前面或者后面添加想要的文字
<div class="class1">
<p class="q">你的名字是?</p>
<p class="a">张三</p>
<p class="q">你的名字是?</p>
<p class="a">张三</p>
<p class="q">你的名字是?</p>
<p class="a">张三</p>
</div>
.class1::before {
content: '问卷';
font-size: 30px;
}
.class1 .q::before {
content: '问题:'
}
.class1 .a::before {
content: '回答:'
}
当然也可以添加形状,默认的是行内元素,如果有需要,我们可以把它变为块级元素
<div class="class2">
<div class="news-item">今天天气为多云</div>
<div class="news-item">今天天气为多云</div>
<div class="news-item">今天天气为多云</div>
<div class="news-item">今天天气为多云</div>
<div class="news-item">今天天气为多云</div>
</div>
.news-item::before {
content: '';
display: inline-block;
width: 16px;
height: 16px;
background: rgb(96, 228, 255);
margin-right: 8px;
border-radius: 50%;
}
我们也可以使用它来添加图片
<div class="class3">
<p class="text1">阅读和写作同样重要</p>
<p class="text1">阅读和写作同样重要</p>
<p class="text1">阅读和写作同样重要</p>
<p class="text1">阅读和写作同样重要</p>
</div>
.class3 .text1::before {
content: url(https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/e08da34488b114bd4c665ba2fa520a31.svg);
}
不过这一方法的缺点就是,不能调整图片大小,如果我们需要使用伪元素添加图片的话,建议通过给伪元素设置背景图片的方式设置
结合clear属性清除浮动
我们都知道清除浮动的一种方式就是给一个空元素设置clear:both
属性,但在页面里添加过多的空元素一方面代码不够简洁,另一方面也不便于维护,所以我们可以通过给伪元素设置clear:both
属性的方法更好的实现我们想要的效果
禁用网页ctrl+f搜索
有些时候,我们不想要用户使用ctrl+f搜索我们网页内的内容,必须在一些文字识别的网页小游戏里,我们又不想把文字做成图片,那么就可以使用这个属性,使用::before
和::after
渲染出来的文字,不可选中也不能搜索。当然这个低版本浏览器的兼容性我木有试,谷歌浏览器和safari是可以实现不能选中不可搜索的效果的。
拿上面的示例进行尝试,可以看到,我们使用伪元素添加的[问题]两个字,就无法使用浏览器的搜索工具搜到。
制作一款特殊的鼠标滑入滑出效果
这个效果还是之前一个朋友从某网站看到之后问我能不能实现,我去那个网站查看了代码学会的,觉得很有趣,特意分享给大家。
可以先看一下效果
这里附上源码和在线演示
.h-button {
z-index: 1;
position: relative;
overflow: hidden;
}
.h-button::before,
.h-button::after {
content: "";
width: 0;
height: 100%;
position: absolute;
filter: brightness(.9);
background-color: inherit;
z-index: -1;
}
.h-button::before {
left: 0;
}
.h-button:after {
right: 0;
transition: width .5s ease;
}
.h-button:hover::before {
width: 100%;
transition: width .5s ease;
}
.h-button:hover::after {
width: 100%;
background-color: transparent;
}
这里我做了一些改进,就是鼠标滑入之后的颜色是对按钮本身颜色进行一定的变换得来的,这样我们就无需对每一个按钮单独设置鼠标滑入时候的颜色了,全局时候的时候只需要对目标按钮添加一个类名h-button
就可以,更加的方便简单,当然,如果大家觉得这样的颜色不好看的话,还是可以自行设置,或者修改一我对颜色的处理方式
这个效果的实现思路其实很简单,就是使用::before
和::after
给目标按钮添加两个伪元素,然后使用定位让他们重合在一起,再通过改变两者的宽度实现的。
首先是创建两个伪元素,宽高都和目标元素一致,我这里的背景色由于是对按钮本身颜色进行处理得来的,所以给他们设置的背景色是沿用父级背景色,如果你想单独设置这里可以分别设置为自己想要的颜色。
.h-button {
z-index: 1;
position: relative;
overflow: hidden;
}
.h-button::before,
.h-button::after {
content: "";
width: 0;
height: 100%;
position: absolute;
filter: brightness(.9);
background-color: inherit;
z-index: -1;
}
我们的实现原理是通过改变伪元素的宽度实现,所以我们需要第一个伪元素的定位以左边为准,从而实现鼠标移入时色块从左往右出现的效果,而第二个伪元素的定位以右为准,从而实现鼠标移出时色块从左往右消失的效果。
这里可以看到,我们在没有给第一个伪元素的初始状态添加过渡效果,那是因为它只需要在从鼠标移出的时候展示动画即可,在鼠标移出的时候需要瞬间消失,所以在初始状态不需要添加过渡效果,而第二个伪元素恰恰相反,它在鼠标滑入的时候不需要展示动画效果,在鼠标滑入也就是回归初始状态的时候需要展示动画效果,所以我们需要在最开始的时候就添加上过渡效果。
.h-button::before {
left: 0;
}
.h-button::after {
right: 0;
transition: width .5s ease;
}
两个伪元素的初始宽度都为0,鼠标滑入的时候,让两个伪元素宽度都变为100%,由于鼠标滑入时我们并不需要第二个伪元素出现,所以这里我们给它的背景颜色设置为透明,这样就可以实现鼠标滑入时只展示第一个伪元素宽度从0到100%的动画,而鼠标移出时第一个伪元素宽度变为0,因为没有过渡效果,所以它的宽度会瞬间变为0,然后展示第二个色块宽度从100%到0的动画效果。
.h-button:hover::before {
width: 100%;
transition: width .5s ease;
}
.h-button:hover::after {
width: 100%;
background-color: transparent;
}
伪元素能实现的创意用法还有很多,如果大家有不同的用法,欢迎分享,希望本篇文章可以对大家有所帮助。
作者:十里青山
来源:juejin.cn/post/7163867155639828488
团队的技术分享又轮到我了,分享点啥才能显得牛逼又有趣?
引言
新年好,我是飞叶_程序员。
见过我这个ID的朋友们肯定都知道,作为前端,我主要通过 B站up主 的身份来来进行社区交流的。 虽然主要的交流渠道不是掘金、segmentfault这样的技术站点,但与在掘金活跃的大佬们遇到的问题其实是一样的。
那就是我们需要经常阅读技术文章、技术资讯,保持和丰富自己的知识储备,不然怎么给别人分享知识呢? 这是我作为一个创作者和分享者 和 广大其他创作者们遇到的共性问题。
那作为一线的开发者,其实也有技术分享的需要,我相信大家的技术团队都是需要技术分享的。 而技术分享一般都是通过轮流进行的,也不能逮着团队里的几个人一直薅羊毛对吧。
那轮到你技术分享的时候,你是否会苦恼于不知道该分享点啥呢?
你是否担心:万一我分享的东西其他人都已经知道了,显得自己不够牛逼呢?
我想这些问题,归根到底是不知道去哪里获取技术资讯的问题。
如果你手里有大量的技术站点,他们能给你提供大量的高质量技术文章,在里面找到一篇值得分享的内容应该就不难了。
回顾2022年,我在B站发布了100多个技术视频,平均约每周两个,现在看起来都不可思议。 哪有那么多可以分享的内容啊!
前端森林
实际上我能分享那么多,得益于我收录了一些英文站点。尤其是有一些技术周刊。
我的灵感来源都是他们。不是凭空产生的。
过年期间我一直在想着把我收藏的这些站点公开出来,让其他人和创作者们也不再有技术分享的苦恼。 所以创建了一个开源项目,叫awesome-fe-sites,GitHub, 并把它部署在了fesites.netlify.app。
他的作用是收录前端资讯类站点,周刊类网站,高质量个人博客和技术团队博客,在线服务类/工具类网站等。
slogan:前端网站,尽收眼底。
同时也希望它也可以解放你的浏览器书签栏。
参与贡献
不知道你有没有一些私藏的高质量的前端站点,如果你希望把它贡献出来,欢迎PR。
另外这个站点是通过qwik这个很新的前端框架搭建的,对qwik感兴趣的话,也可以看看这个项目的代码。
作者:飞叶_前端
来源:juejin.cn/post/7193136620948684860
不修改任何现有源代码,将项目从 webpack 迁移到 vite
背景
之前将公司项目开发环境从 webpack 迁移到 vite,实现了 dev 环境下使用 vite、打包使用 webpack 的共存方案。本文将讲述开发环境下 vue3 项目打包器从 webpack 迁移到 vite 过程中的所遇问题、解决方案、迁移感受,以及如何不修改任何源码完成迁移。
迁移的前提及目标
我们之前的项目大概有 10w+ 行代码,开发环境下冷启动所花费的时间大概 1 分钟多,所以迁移到 vite 就是看中了它的核心价值:快!但是迁移到 vite,也会伴随着风险:代码改动及回归成本。
作为一个大型的已上线项目,它的线上稳定性的一定比我们工程师开发时多减少一些项目启动时间的价值要高,所以如果迁移带来了很多线上问题,那便得不偿失了。
所以我们迁移过程中有前提也有目标:
- 前提:不因为迁移打包工具引发线上问题
- 目标:实现开发环境下的快速启动
方案
有了上述前提和目标,那我们的方案就可以从这两方面思考入手了。
- 如何能确保实现前提?我们已有了稳定版本,那只要保证源代码不改动,线上的打包工具 webpack 及配置也不改动,就可以确保实现前提。
- 如何实现目标?vite 的快主要是体现在开发环境,打包使用的 rollup 相比 webpack 速度上并无太明显的优势,所以我们只要开发环境下使用 vite 启动就可以实现目标。
由此得出最终方案:不改动任何现有源代码,开发环境使用 vite,线上打包使用 webpack。
迁移过程
安装 vite 及进行基础配置
- 在终端执行下述命令,安装 vite 相关基础依赖:
yarn add vite @vitejs/plugin-vue vite-plugin-html -D
复制代码
- 因为 vite 的 html 模板文件需要显示引入入口的
.js/.ts
文件,同时有一些模板变量上面的区别,为了完全不影响线上打包,在/public
目录下新建一个index.vite.html
文件。将/public/index.html
文件的内容拷贝进来并添加入口文件的引用(/src/main.ts
指向项目的入口文件):
<!DOCTYPE html>
<html lang="">
<!-- other code... -->
<body>
<!-- other code... -->
<div id="app"></div>
+ <script type="module" src="/src/main.ts"></script>
</body>
</html>
复制代码
- 新增
vite.config.js
,内容如下:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { createHtmlPlugin } from 'vite-plugin-html';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
createHtmlPlugin({
minify: true,
/**
* After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
* @default src/main.ts
*/
entry: 'src/main.ts',
/**
* If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
* @default index.html
*/
template: 'public/index.vite.html',
}),
]
});
复制代码
- 在
package.json
的scripts
里新增一条 vite 开发启动的指令:
{
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
+ "vite": "vite"
}
}
复制代码
到这里,我们基本的配置就已经完成了,现在可以通过 npm run vite
来启动 vite 开发环境了,只不过会有一大堆的报错,我们根据可能遇到的问题一个个去解决。
问题及解决方案
HtmlWebpackPlugin 变量处理
报错: htmlWebpackPlugin is not defined
是因为之前在 webpack 的 HtmlWebpackPlugin
插件中配置了变量,而 vite 中没有这个插件,所以缺少这个变量。
我们先前安装了 vite-plugin-html
插件,所以可以在这个插件中配置变量来代替:
- 将
index.vite.html
中所有的htmlWebpackPlugin.options.xxx
修改为xxx
,如:
<!DOCTYPE html>
<html lang="">
<head>
- <title><%= htmlWebpackPlugin.options.title %></title>
+ <title><%= title %></title>
</head>
</html>
复制代码
- 在
vite.config.js
中添加如下内容:
export default defineConfig({
plugins: [
createHtmlPlugin({
+ inject: {
+ data: {
+ title: '我的项目',
+ },
+ },
}),
]
});
复制代码
其他的 html 中未定义的变量亦可以通过此方案来解决。
alias 配置
报错:Internal server error: Failed to resolve import "@/ok.ts" from "src/main.ts". Does the file exist?
通常我们的项目都会在 alias 中将 src 目录配置为 @
来便于引用,所以遇到这个报错我们需要再 vite.config.js
中将之前 webpack 的 alias 配置补充进来(同时 vite 中 css 等样式文件的 alias 不需要加 ~
前缀,所以也需要配置下 ~@
):
import { defineConfig } from 'vite';
import path from 'path';
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'~@': path.resolve(__dirname, './src'),
// 其他的 alias 配置...
}
},
});
复制代码
css 全局变量
报错:Internal server error: [less] variable @primaryColor is undefined
是因为项目在 less 文件中定义了变量,并在 webpack 的配置中通过 style-resources-loader
将其设置为了全局变量。我们可以在 vite.config.js
中添加如下配置引入文件将其设置为全局变量:
// vite.coonfig.js
export default defineConfig({
css: {
preprocessorOptions: {
less: {
additionalData: `@import "src/styles/var.less";`
},
},
},
});
复制代码
环境变量
报错:ReferenceError: VUE_APP_HOST is not defined
这是因为项目中在 .env.local
文件中设置了以 VUE_APP_XXX
开头的环境变量,我们通过可以通过在 vite.config.js
的 define 中定义为全局变量:
// vite.config.js
export default defineConfig({
define: {
'process.env': {
NODE_ENV: import.meta.env,
APP_NAME: '我的项目名称',
},
+ VUE_APP_HOST: '"pinyin-pro.com"', // 这里需要注意定义为一个字符串
},
})
复制代码
process 未定义
报错: ReferenceError: process is not defined
这是因为 webpack 启动时会根据 node 环境将代码中的 process
变量会将值给替换,而 vite 未替换该变量,所以在浏览器环境下会报错。
我们可以通过在 vite.config.js
中将 process.env
定义成一个全局变量,将相应的属性给配置好:
// vite.config.js
export default defineConfig({
define: {
'process.env': {
NODE_ENV: import.meta.env,
APP_NAME: '我的项目名称',
},
},
})
复制代码
使用 JSX
报错:Uncaught ReferenceError: React is not defined
这是因为 react16 版本之后,babel 默认会将 .jsx/.tsx
语法转换为 react 函数,而我们需要以 vue 组件的方式来解析 .jsx/.tsx
文件,需要通过新的插件来解决:
- 安装
@vitejs/plugin-vue-jsx
插件:
yarn add @vitejs/plugin-vue-jsx -D
复制代码
- 在
vite.config.js
文件中引入插件:
// others
import vueJsx from '@vitejs/plugin-vue-jsx';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
// others...
],
});
复制代码
CommonJS 不识别
报错:ReferenceError: require is not defined
这是因为项目中通过 require()
引入了图片,webpack 支持 commonjs 语法,而 vite 开发环境是 esmodule 不支持 require。可以通过 @originjs/vite-plugin-commonjs
插件,它能解析 require
进行语法转换以支持同样效果:
- 安装
@originjs/vite-plugin-commonjs
插件:
yarn add @originjs/vite-plugin-commonjs -D
复制代码
- 在
vite.config.js
中引入插件:
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
export default defineConfig({
plugins: [
viteCommonjs()
]
})
复制代码
多模块导入
报错:Uncaught ReferenceError: require is not defined
这个报错注意比前面的 ReferenceError: require is not defined
多了一个 Uncaught,是因为 @originjs/vite-plugin-commonjs
并不是对所有的 require 进行了转换,我们项目中还通过 webpack 提供的 require.context
进行了多模块导入。要解决这个问题可以通过 @originjs/vite-plugin-require-context
插件实现:
- 安装
@originjs/vite-plugin-require-context
插件:
yarn add @originjs/vite-plugin-require-context -D
复制代码
- 在
vite.config.js
中引入插件:
import ViteRequireContext from '@originjs/vite-plugin-require-context'
export default defineConfig({
plugins: [
ViteRequireContext()
]
})
复制代码
其他 webpack 配置
其他的一些 webpack 配置例如 devServer
以及引用的一些 loader 和 plugin,只需要参考 vite 文档一一修改就行,由于各个团队的项目配置不同,我在这里就不展开了。需要注意的是,因为是开发环境下使用 vite,只需要适配开发环境的 webpack 配置就行,打包优化等不需要处理。
潜在隐患
上述方案中,我们通过不修改源代码 + 打包依然使用 webpack,保证了现有项目线上的稳定性:但还有一个潜在隐患:随着项目后期的迭代,因为开发环境是 vite,打包是 webpack,可能因为两种打包工具的不同导致开发和打包产物表现不同的缺陷。例如一旦你开发环境使用了 import.meta.xxx
,打包后立马就会报错。
写在最后
我们当时采用此方案是因为 vite 刚发布没太久,用于正式环境有不少坑,而现在 vite 已经成为一款比较成熟的打包工具了,如果要迁移的话还是建议开发和打包都采用 vite,这种方面可以作为 webpack 迁移 vite 的短期过渡方案使用。(我们的项目现在打包也迁移到了 vite 了)
另外我们要明确,作为公司项目稳定性是第一位的,技术方案的变更需要明确能给项目带来收益。例如 webpack 迁移的 vite,是明确能够大幅优化开发环境的等待时间成本,而非看到别人都在用随大流而用。如果已知项目后期发展规模不会太大,当前项目启动时间也不长,就没有迁移的必要了。
上述迁移过程中遇到的坑只是针对我们的项目,没能包含全部的迁移坑点,大家有其他的遇到问题欢迎分享一起讨论。
最后推荐一个工具,可以将项目一键 webpack 迁移到 vite: webpack-to-vite
链接:https://juejin.cn/post/7197222701220053047
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
10000+条数据的内容滚动功能如何实现?
遇到脑子有问题的产品经理该怎么办?如果有这么一个需求要你在一个可视区范围内不间断循环滚动几千上万条数据你会怎么去实现?
且不说提这个需求的人是不是脑子有问题,这个需求能不能实现?肯定是可以的,把数据请求回来渲染到页面写个滚动样式就好了。抛开这样一次性请求上万条数据合不合理不讲,一万条数据渲染到页面上估计都要卡死了吧。那有没有更好的方法呢? 当然有
分析一波思路
我们分批次请求数据,比如可视化区域展示的是20条数据,那我们就一次只请求30条,然后把请求回来的数据保存起来,定义一个滚动的数组,把第一次请求的30条数据赋值给它。后面每当有一条数据滚出可视区域我们就把它删掉,然后往尾部新增一条,让滚动数组始终保持30条的数据,这样渲染在页面上的数据始终只有30条而不是一万条。文字描述太生硬我们上代码
首先定义两个数组,一个滚动区域的数组scrollList,一个总数据的数组totalList,模拟一个异步请求的方法和获取数据的方法。
<script lang="ts" setup>
import { nextTick, ref } from "vue";
type cellType = {
id: number,
title: string,
}
interface faceRequest {
data: cellType,
total: number
}
// 总数据的数组
const totalList = ref<Array<cellType>>([]);
// 滚动的数组
const scrollList = ref<Array<cellType>>([]);
// 数据是否全部加载完毕
let loading: Boolean = false
// 模拟异步请求
const request = () => {
return new Promise<faceRequest>((resolve: any, reject: any) => {
let data: Array<cellType> = []
// 每次返回30条数据
for (let i = 0; i < 30; i++) {
data.push({
id: totalList.value.length + i,
title: 'cell---' + (totalList.value.length + i)
});
}
let total = 10000// 数据的总数
resolve({ data, total })
})
}
const getData = () => {
request().then(res => {
totalList.value = totalList.value.concat(res.data)
// 默认获取第一次请求回来的数据
if (totalList.value.length <= 30) {
scrollList.value = scrollList.value.concat(res.data)
}
// 当前请求的数量小于总数则继续请求
if (totalList.value.length < res.total) {
getData()
} else {
loading = true
}
})
}
getData()
</script>
复制代码
上面写好了数据的获取处理,接下来写一下页面
<template>
<div class="div">
<div :style="styleObj" @mouseover="onMouseover" @mouseout="onMouseout" ref="divv">
<div v-for="item in scrollList" :key="item.id" @click="onClick(item)">
<div class="cell">{{ item.title }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
// 滚动样式
const styleObj = ref({
transform: "translate(0px, 0px)",
});
</script>
<style scoped>
.div {
width: 500px;
height: 500px;
background-color: aquamarine;
overflow: hidden;
}
.cell {
height: 30px;
}
</style>
复制代码
现在页面跟数据的前提条件都写好,下面就是数据逻辑的处理了,也就是这篇文章的重点
- 获取页面上单条数据的总体高度
- 设置定时器使页面不停的滚动
- 当一条数据滚动出视图范围时调用处理数据的方法并且重置滚动高度为0
const divv = ref();
// 当前滚动高度
const ScrollHeight = ref<number>(0);
// 储存定时器
const setInt = ref();
// 内容滚动
const roll = () => {
nextTick(() => {
let offsetHeight = divv.value.childNodes[1].offsetHeight
setInt.value = setInterval(() => {
if (ScrollHeight.value == offsetHeight) {
onDel();
ScrollHeight.value = 0;
}
ScrollHeight.value++;
styleObj.value.transform = `translate(0px, -${ScrollHeight.value}px)`;
}, 10);
})
};
onMounted(() => {
roll()
})
复制代码
处理数据的方法
- 保存需要被删除的数据
- 删除超出视窗的数据
- 获取总数组的数据添加到滚动数组的最后一位
- 将被删除的数组数据添加到总数组最后面,
- 当滚动到最后一条数据时重置下标为0,使得数据首位相连不断循环
let index = 29;// 每次请求的数量-1,例如每次请求30条数据则为29
const onDel = () => {
index++;
if (loading) {
// 当滚动到最后一条数据时重置下标为0
if (index == totalList.value.length) {
index = 0;
}
scrollList.value.shift();
scrollList.value.push(totalList.value[index]);
} else {
if (index == totalList.value.length) {
index = 0;
}
// 保存需要被删除的数据
let value = scrollList.value[0]
// 删除超出视窗的数据
scrollList.value.shift();
// 获取总数组的数据添加到滚动数组的最后一位
scrollList.value.push(totalList.value[index]);
// 将被删除的数组数据添加到总数组最后面
totalList.value.push(value)
}
};
复制代码
到这里代码就写好了,接下来让我们看看效果怎么样
总结
在我们开发的过程中会遇到各种各样天马行空的需求,尤其会遇到很多不合理的需求,这时候我们就要三思而后行,
想清楚能不能不做?
能不能下次再做?
能不能让同事去做?
链接:https://juejin.cn/post/7169940462357184525
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
CSS动画篇之404动画
当前页面无法访问,可能没有权限或已删除。 作为一个从事互联网行业的你是不是见过各种各种的404页面,今天刚好发现一个比较有趣的404页面,如上图所示,是不是感觉挺炫酷呢,本文将和大家分享一下实现原理。
前言
看到上面的404你的第一感觉会是这么做呢?
来,UI同学给我上GIF。
当然这种方式对于前端同学来说肯定是最简单的实现方式,单纯的加载一张图片即可。
但是对于一个有追求的前端,绝对不会答应这么干,加载一张GIF图片的成本太高了,网络差的情况下会导致白屏时间过长,所以我们尽可能的用代码实现,减少这种不必要的网络请求。
实现
当你仔细看这个动画的时候可以发现其实主体只有一个标签,内容就是404,另外的几个动画都是基于这个主体实现,所以我们先写好这个最简单的html
代码。
<h1 data-t="404">404</h1>
复制代码
细心的同学应该看到了我们自定义了一个熟悉data-t
,这个我们后续在css中会用到,接下来实现主体的动画效果,主要的动画效果就是让主体抖动并增加模糊的效果,代码实现如下所示。
h1 {
text-align: center;
width: 100%;
font-size: 6rem;
animation: shake .6s ease-in-out infinite alternate;
}
@keyframes shake {
0% {
transform: translate(-1px)
}
10% {
transform: translate(2px, 1px)
}
30% {
transform: translate(-3px, 2px)
}
35% {
transform: translate(2px, -3px);
filter: blur(4px)
}
45% {
transform: translate(2px, 2px) skewY(-8deg) scaleX(.96);
filter: blur(0)
}
50% {
transform: translate(-3px, 1px)
}
}
复制代码
接下来增加主体动画后面子两个子动画内容,基于伪元素实现,伪元素的内容通过上面html
中自定义data-t
获取,主要还用了clip
中的rect
,具体css代码如下。
h1:before {
content: attr(data-t);
position: absolute;
left: 50%;
transform: translate(-50%,.34em);
height: .1em;
line-height: .5em;
width: 100%;
animation: scan .5s ease-in-out 275ms infinite alternate,glitch-anim .3s ease-in-out infinite alternate;
overflow: hidden;
opacity: .7;
}
@keyframes glitch-anim {
0% {
clip: rect(32px,9999px,28px,0)
}
10% {
clip: rect(13px,9999px,37px,0)
}
20% {
clip: rect(45px,9999px,33px,0)
}
30% {
clip: rect(31px,9999px,94px,0)
}
40% {
clip: rect(88px,9999px,98px,0)
}
50% {
clip: rect(9px,9999px,98px,0)
}
60% {
clip: rect(37px,9999px,17px,0)
}
70% {
clip: rect(77px,9999px,34px,0)
}
80% {
clip: rect(55px,9999px,49px,0)
}
90% {
clip: rect(10px,9999px,2px,0)
}
to {
clip: rect(35px,9999px,53px,0)
}
}
@keyframes scan {
0%,20%,to {
height: 0;
transform: translate(-50%,.44em)
}
10%,15% {
height: 1em;
line-height: .2em;
transform: translate(-55%,.09em)
}
}
复制代码
伪元素after
的动画与before
中的一致,只是部分参数改动,如下所示。
h1:after {
content: attr(data-t);
position: absolute;
top: -8px;
left: 50%;
transform: translate(-50%,.34em);
height: .5em;
line-height: .1em;
width: 100%;
animation: scan 665ms ease-in-out .59s infinite alternate,glitch-anim .3s ease-in-out infinite alternate;
overflow: hidden;
opacity: .8
}
复制代码
总结
到此为止我们的功能就实现完成啦,看完代码是不是感觉并没有很复杂,又为我们的页面性能提升了大大的一步。
完整的代码可以访问codepen查看 👉 codepen-404
链接:https://juejin.cn/post/7091848998830473230
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
老板说:把玉兔迎春图实现高亮
前言
兔年来临,老板意气风发的说:我们的系统登录页要换做玉兔迎春的背景页,而且用户
ctrl+f
搜索【玉兔迎春】关键字时,图片要高亮。
新的一年,祝大家身体健康、Bug--
一、明确需求
将系统的登录页面背景换做如上图【玉兔迎春】。
而且,用户可以通过搜索关键字【玉兔迎春】让背景图的文字进行高亮。
下面我们进行分析一下。
二、进行分析
接到该需求的时候,心里是这样子的。
于是,老板像是看穿我的疑惑时,语重心长的对我们说:我们要给用户一个焕然一新的感觉。
疯狂点点头,并想好如何让图片里面的文字进行高亮的对策。
静下来思考片刻,其实不是很难。
2.1 思路
我们只需要盖一层div在图片上,然后设置文字透明,浏览器ctrl+f
搜索的时候,会给文字他高亮黄的颜色,我们就可以看到文字了。
盖的这层div,里面包含着我们的文字。
那么,难点就是怎么从图片获取文字出来。
其实这个技术,有个专业词语来描述,叫ocr
识别技术。
2.2 ocr
ocr,其实也叫“光学字符识别技术”,是最为常见的、也是目前最高效的文字扫描技术,它可以从图片或者PDF中识别和提取其中的文字内容,输出文本文档,方便验证用户信息,或者直接进行内容编辑。
揭秘该技术:实现文字识别?从图片到文字的过程发生了什么?
分别是输入、图像与处理、文字检测、文本识别,及输出。每个过程都需要算法的深度配合,因此从技术底层来讲,从图片到文字输出,要经历以下的过程:
1、图像输入:读取不同图像格式文件;
2、图像预处理:主要包括图像二值化,噪声去除,倾斜校正等;
3、版面分析:将文档图片分段落,分行;
4、字符切割:处理因字符粘连、断笔造成字符难以简单切割的问题;
5、字符特征提取:对字符图像提取多维特征;
6、字符识别:将当前字符提取的特征向量与特征模板库进行模板粗分类和模板细匹配,识别出字符;
7、版面恢复:识别原文档的排版,按原排版格式将识别结果输出到文本文档;
8、后处理校正: 根据特定的语言上下文的关系,对识别结果进行校正。
2.3 应用
随着ocr技术的成熟,不少软件已经出了该功能。
比如:微信、qq、语雀等等。
还有一些试卷试题,都会用到ocr识别技术。
还有一些技术文档,实现自定义搜索功能,表格关键字高亮。
老板这次需求:把玉兔迎春图实现高亮。
和如上实现的技术思路类似。
我们也可以自定义颜色,加个span
标签给其想要的样式。
三、使用
当然,我们可能并不关心底层的实现,只关心怎么怎么去使用。
我们可以调用百度API:文字提取技术
还可以使用java的tesseract-ocr
库,其实就是文字的训练。
所以会有个弊端,就是文件可能会有点大,存放着大量文字。
后记
在一个需求的产生之后,我们如果没什么思路,可以借鉴一下,目前市场上有没有类似的技术的沉淀,从而实现需求。
最后,望大家的新的一年,工作顺利,身体健康。
玉兔迎春啦🐇🧨🐇🏮🐇~
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
作者:Dignity_呱
来源:juejin.cn/post/7186459084303335481
一个有趣的交互效果的实现
效果分析
最近在做项目,碰到了这样一个需求,就是页面有一个元素,这个元素可以在限定的区域内进行拖拽,拖拽完成吸附到左边或者右边,并且在滚动页面的时候,这个元素要半隐状态,停止滚动的时候恢复到原来的位置。如图所示:
根据视频所展示的效果,我们得出了我们需要实现的效果主要有2个部分:
拖拽并吸附
滚动半隐元素
那么如何实现这2个效果呢?我们一个效果一个效果的来分析。
ps: 由于这里采用的是react技术栈,所以这里以react作为讲解
首先对于第一个效果,我们要想实现拖拽,有2种方式,第一种就是html5提供的拖拽api,还有一种就是监听鼠标的mousedown,mousemove和mouseup事件,由于这里兼容的移动端,所以我采用的是第二种实现方法。
思路是有了,接下来我想的就是将这三个事件封装一下,写成一个hook函数,这样方便调用,也方便扩展。
对于拖拽的实现,我们只需要在鼠标按下的时候,记录一下横坐标x和纵坐标y,在鼠标拖动的时候用当前拖动的横坐标x和横坐标y去与鼠标按下的时候的横坐标x与y坐标相减就可以得到拖动的偏移坐标,而这个偏移坐标就是我们最终要使用到的坐标。
在鼠标按下的时候,我们还需要减去元素本身所在的left偏移和top偏移,这样计算出来的坐标才是正确的。
然后,由于元素需要通过设置偏移来改变位置,因此我们需要将元素脱离文档流,换句话说就是元素使用定位,这里我采用的是固定定位。
hooks函数的实现
基于以上思路,一个任意拖拽功能实现的hooks函数就结构就成型了。
当然由于我们需要限定范围,这时候我们可以思考会有2个方向上的限定,即水平方向和垂直方向上的限定。除此之外,我们还需要提供一个默认的坐标值,也就是说元素默认应该是在哪个位置上。现在我们用伪代码来表示一下这个函数的结构,代码如下:
const useLimitDrag = (el,options,container) => {
//核心代码
}
export default useLimitDrag;
参数类型
这个hooks函数有3个参数,第一个参数自然是需要拖拽的元素,第二个参数则是配置对象,而第三个参数则是限定的容器元素。拖拽的元素和容器元素都是属于dom元素,在react中,我们还可以传递ref来表示一个dom元素,所以这两个参数,我们可以约定一下类型定义。我们先来定义元素的类型如下:
export type ElementType = Element | HTMLElement | null;
dom元素的类型就是Element | HTMLElement这2个类型,现在我们知道react的ref可以传递dom元素,并且我们还可以传入一个函数当作参数,所以基于这个类型,我们又额外的扩展了参数的类型,也方便配置。让我们继续写下如下代码:
import type { RefObject } from 'react';
export type RefElementType = RefObject<ElementType>;
export type FunctionElementType = () => ElementType;
这样el和container元素的类型就一目了然,我们再定义一个类型简单合并一下这两个类型,代码如下:
export type ParamType = RefElementType | FunctionElementType;
接下来,让我们看配置对象,配置对象主要有2个地方,第一个就是默认值,第二个则是限定方向,因此我们约定了3个参数,islimitX,isLimitY,defaultPosition,并且配置对象都应该是可选的,我们可以使用Partial内置泛型将这个类型包裹一下,ok,来看看代码吧。
export type OptionType = Partial<{
isLimitX: boolean,
isLimitY: boolean,
defaultPosition: {
x: number,
y: number
}
}>;
嗯现在,我们可以修改一下以上的核心函数了,代码如下:
const useLimitDrag = (el: ParamType,options: OptionType,container?: ParamType) => {
//核心代码
}
export default useLimitDrag;
返回值类型
下一步,我们需要确定我们返回的值,首先肯定是当前被计算出来的x和y坐标,其次由于我们这个需求还有一个吸附效果,这个吸附效果是什么意思呢?就是说,以屏幕的中间作为划分界限为左右两部分,当拖动的x坐标大于中间,那么就吸附到最右边,否则就吸附到最左边。
根据这个需求,我们可以将坐标分为最大x坐标,最小x坐标以及中间的x坐标,当然由于需求只提到了水平方向上的吸附,垂直方向上并没有,但是为了考虑扩展,与之对应的我们同样要分成最大y坐标,最小y坐标以及中间的y坐标。
最后,我们还可以返回一个是否正在拖动中,方便我们做额外的操作。根据描述,以上的代码我们也就可以构造如下:
export type PositionType = Partial<{ x: number, y: number, isMove: boolean, maxX: number, maxY: number, minX: number, minY: number, centerX: number, centerY: number }>;
//
const useLimitDrag = (el: ParamType,options: OptionType,container?: ParamType): PositionType => {
//核心代码
}
export default useLimitDrag;
核心代码实现第一步---判断当前环境
最基本的结构搭建好了,接下来第一步,我们要做什么?首先当然是判断当前环境是否表示移动端啊。那么如何判断呢?浏览器提供了一个navigator对象,通过这个对象的userAgent属性我们就可以判断,这个属性是一个很长的字符串,但是我们可以从其中一些值看出一些端倪,在移动端的环境中,通常都会看到iPhone|iPod|Android|ios这些字符串值,比如在iphone手机中就会有iPhone字符串,同理android也是。所以我们就可以通过写一个正则表达式来匹配这些字符串,如果有这些字符串就代表是移动端环境,否则就是pc浏览器环境,代码如下:
const isMobile = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
我们为什么要判断是否是移动端环境?因为在移动端环境,我们通常监听的是触摸事件,即touchstart,touchmove与touchend,而非mousedown,mousemove和mouseup。所以下一行代码自然就是定义好事件呢。如下:
const eventType = isMobile ? ['touchstart', 'touchmove', 'touchend'] : ['mousedown', 'mousemove', 'mouseup'];
核心代码实现第二步---一些初始化工作
下一步,我们通过useRef方法来存储拖拽元素和限定拖拽容器元素。代码如下:
const element = useRef<ElementType>();
const containerElement = useRef<ElementType>();
接着我们获取配置对象的值,然后我们定义最大边界的值,代码如下:
const { isLimitX, isLimitY,defaultPosition } = option;
const globalWidthHeight = {
offsetWidth: window.innerWidth,
offsetHeight: window.innerHeight
}
随后,我们用一个变量代表鼠标是否按下的状态,这样做的目的是让拖拽变得更丝滑流畅一些,而不容易出问题,然后我们用useState定义返回的值,再定义一个对象存储鼠标按下时的x坐标和y坐标的值。代码如下:
let isStart = false;
const [position, setPosition] = useState<PositionType>({
x: defaultPosition?.x,
y: defaultPosition?.y,
maxX: 0,
maxY: 0,
centerX: 0,
centerY: 0,
minX: 0,
minY: 0
});
const [isMove, setIsMove] = useState(false);
const downPosition = {
x:0,
y:0
}
另外为了确保拖动在限定区域内,我们需要设置滚动截断的样式,让元素不能在出现滚动条后还能拖动,因为这样会出现问题。我们定义一个方法用来设置,代码如下:
const setOverflow = () => {
const limitEle = (containerElement.current || document.body) as HTMLElement;
if (isLimitX) {
limitEle.style.overflowX = 'hidden';
} else {
limitEle.style.overflowX = '';
}
if (isLimitY) {
limitEle.style.overflowY = 'hidden';
} else {
limitEle.style.overflowY = '';
}
}
这个方法也就比较好理解了,如果使用的时候传入isLimitX那么就设置overflowX为hidden,否则不设置,y方向同理。
核心代码的实现第三步---监听事件
接下来,我们在react的钩子函数中监听事件,此时有了一个选择就是钩子函数我们使用useEffect还是useLayoutEffect呢?要决定使用哪个,我们需要知道这两个钩子函数的区别,这个超出了本文范围,不提及,可以查阅相关资料了解,这里我选择的是useLayoutEffect。
在钩子函数的回调函数中,我们首先将拖拽元素和容器元素存储下来,然后如果拖拽元素不存在,我们就不执行后续事件,回调函数返回一个函数,在该函数中我们移除对应的事件。代码如下:
useLayoutEffect(() => {
element.current = typeof el === 'function' ? el() : el.current;
containerElement.current = typeof containerRef === 'function' ? containerRef() : containerRef?.current;
if (!element.current) {
return;
}
element.current.addEventListener(eventType[0], onStartHandler);
return () => {
element.current?.removeEventListener(eventType[0], onStartHandler);
}
}, []);
核心代码实现第四步---拖动开始事件回调
接下来,我们来看一下onStartHandler函数的实现,在这个函数中,我们主要其实就是存储按下时候的坐标值,并且设置状态以及拖拽元素的鼠标样式和滚动截断的样式,随后当然是监听拖动和拖动结束事件,代码如下:
const onStartHandler = useCallback((e:Event) => {
isStart = true;
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = 'move';
}
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
downPosition.x = clientX - target.offsetLeft;
downPosition.y = clientY - target.offsetTop;
setOverflow();
window.addEventListener(eventType[1], onMoveHandler);
window.addEventListener(eventType[2], onUpHandler);
}, []);
pc端是可以直接从事件对象中拿出来坐标,可是在移动端我们要通过一个changedTouches属性,这个属性是一个伪数组,第一项就是我们要获取到的坐标值。
接下来就是拖动事件的回调函数以及拖动结束的回调函数的实现了。
核心代码实现第五步---拖动事件回调
这是一个最核心实现的回调,我们在这个函数当中是要计算坐标的,首先当然是根据isStart状态来确定是否执行后续逻辑,其次,还要获取到当前拖拽元素,因为我们要根据这个拖拽元素的宽高做坐标的计算,另外还要获取到容器元素,如果没有提供容器元素,那么就是我们最开始定义的globalWidthHeight中取,然后获取鼠标按下时的x和y坐标值,将当前移动的x坐标和y坐标分别与按下时相减,就是我们的移动x坐标和y坐标,如果有设置isLimitX和isLimitY,我们还要额外设置滚动截断样式,并且我们通过将0和moveX以及最大值(也就是屏幕或者是容器元素的宽高减去拖拽元素的宽高)得到我们的最终的moveX和moveY值。
最后,我们将最终的moveX和moveY用react的状态存储起来即可。代码如下:
const onMoveHandler = useCallback((e: Event) => {
if (!isStart) {
return;
}
setOverflow();
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
if (!element.current) {
return;
}
const { offsetWidth, offsetHeight} = element.current as HTMLElement;
const { offsetWidth: containerWidth, offsetHeight: containerHeight } = (containerElement.current as HTMLElement ||globalWidthHeight);
const { x,y } = downPosition;
const moveX = clientX - x,
moveY = clientY - y;
const data = {
x: isLimitX ? Math.max(0, Math.min(containerWidth - offsetWidth, moveX)) : moveX,
y: isLimitY ? Math.max(0, Math.min(containerHeight - offsetHeight, moveY)) : moveY,
minX: 0,
minY: 0,
maxX: containerWidth - offsetWidth,
maxY: containerHeight - offsetHeight,
centerX: (containerWidth - offsetWidth) / 2,
centerY: (containerHeight - offsetHeight) / 2
}
setIsMove(true);
setPosition(data);
}, []);
核心代码实现第六步--拖动结束回调
最后在拖动结束后,我们需要重置我们做的一些操作,比如样式溢出截断,再比如移除事件的监听,以及恢复鼠标的样式等。代码如下:
const onUpHandler = useCallback(() => {
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = '';
}
isStart = false;
setIsMove(false);
const limitEle = (containerElement.current || document.body) as HTMLElement;
limitEle.style.overflowX = '';
limitEle.style.overflowY = '';
window.removeEventListener(eventType[1], onMoveHandler);
window.removeEventListener(eventType[2],onUpHandler);
}, []);
到此为止,我们的第一个效果核心实现就已经算是完成大半部分了,最后我们再把需要用到的状态值返回出去。代码如下:
return {
...position,
isMove
}
合并以上的代码,就成了我们最终的hooks函数,代码如下:
import { useState, useCallback, useLayoutEffect, useRef } from 'react';
import type { RefObject } from 'react';
export type ElementType = Element | HTMLElement | null;
export type RefElementType = RefObject<ElementType>;
export type FunctionElementType = () => ElementType;
export type ParamType = RefElementType | FunctionElementType;
export type PositionType = Partial<{ x: number, y: number, isMove: boolean, maxX: number, maxY: number, minX: number, minY: number, centerX: number, centerY: number }>;
export type OptionType = Partial<{
isLimitX: boolean,
isLimitY: boolean,
defaultPosition: {
x: number,
y: number
}
}>;
const useAnyDrag = (el: ParamType, option: OptionType = { isLimitX: true, isLimitY: true,defaultPosition:{ x:0,y:0 } }, containerRef?: ParamType): PositionType => {
const isMobile = navigator.userAgent.match(/(iPhone|iPod|Android|ios)/i);
const eventType = isMobile ? ['touchstart', 'touchmove', 'touchend'] : ['mousedown', 'mousemove', 'mouseup'];
const element = useRef<ElementType>();
const containerElement = useRef<ElementType>();
const { isLimitX, isLimitY,defaultPosition } = option;
const globalWidthHeight = {
offsetWidth: window.innerWidth,
offsetHeight: window.innerHeight
}
let isStart = false;
const [position, setPosition] = useState<PositionType>({
x: defaultPosition?.x,
y: defaultPosition?.y,
maxX: 0,
maxY: 0,
centerX: 0,
centerY: 0,
minX: 0,
minY: 0
});
const [isMove, setIsMove] = useState(false);
const downPosition = {
x:0,
y:0
}
const setOverflow = () => {
const limitEle = (containerElement.current || document.body) as HTMLElement;
if (isLimitX) {
limitEle.style.overflowX = 'hidden';
} else {
limitEle.style.overflowX = '';
}
if (isLimitY) {
limitEle.style.overflowY = 'hidden';
} else {
limitEle.style.overflowY = '';
}
}
const onStartHandler = useCallback((e:Event) => {
isStart = true;
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = 'move';
}
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
downPosition.x = clientX - target.offsetLeft;
downPosition.y = clientY - target.offsetTop;
setOverflow();
window.addEventListener(eventType[1], onMoveHandler);
window.addEventListener(eventType[2], onUpHandler);
}, []);
const onMoveHandler = useCallback((e: Event) => {
if (!isStart) {
return;
}
setOverflow();
const event: Touch | MouseEvent = e instanceof TouchEvent ? e.changedTouches[0] : e as MouseEvent;
const { clientX, clientY } = event;
if (!element.current) {
return;
}
const { offsetWidth, offsetHeight} = element.current as HTMLElement;
const { offsetWidth: containerWidth, offsetHeight: containerHeight } = (containerElement.current as HTMLElement || globalWidthHeight);
const { x,y } = downPosition;
const moveX = clientX - x,
moveY = clientY - y;
const data = {
x: isLimitX ? Math.max(0, Math.min(containerWidth - offsetWidth, moveX)) : moveX,
y: isLimitY ? Math.max(0, Math.min(containerHeight - offsetHeight, moveY)) : moveY,
minX: 0,
minY: 0,
maxX: containerWidth - offsetWidth,
maxY: containerHeight - offsetHeight,
centerX: (containerWidth - offsetWidth) / 2,
centerY: (containerHeight - offsetHeight) / 2
}
setIsMove(true);
setPosition(data);
}, []);
const onUpHandler = useCallback(() => {
const target = element.current as HTMLElement;
if (target) {
target.style.cursor = '';
}
isStart = false;
setIsMove(false);
const limitEle = (containerElement.current || document.body) as HTMLElement;
limitEle.style.overflowX = '';
limitEle.style.overflowY = '';
window.removeEventListener(eventType[1], onMoveHandler);
window.removeEventListener(eventType[2],onUpHandler);
}, []);
useLayoutEffect(() => {
element.current = typeof el === 'function' ? el() : el.current;
containerElement.current = typeof containerRef === 'function' ? containerRef() : containerRef?.current;
if (!element.current) {
return;
}
element.current.addEventListener(eventType[0], onStartHandler);
return () => {
element.current?.removeEventListener(eventType[0], onStartHandler);
}
}, []);
return {
...position,
isMove
}
}
export default useAnyDrag;
接下来我们来看第二个效果的实现。
半隐效果的实现分析
第二个效果实现的难点在哪里?我们都知道监听元素的滚动事件可以知道用户正在滚动页面,可是我们并不知道用户是否停止了滚动,而且也没有相关的事件或者是API能够让我们去监听用户停止了滚动,那么难点就在这里,如何知道用户是否停止了滚动。
要解决这个问题,我们还得从滚动事件中作文章,我们知道如果用户一直在滚动页面的话,滚动事件就会一直触发,假设我们在该事件中延迟个数百毫秒执行某个操作,是否就代表用户停止了滚动,然后我们可以执行相应的操作?
幸运的是,我从这里找到了答案,还真的是这么做。
如此一来,这个效果我们就实现了一大半了,我们实现一个useIsScroll函数,然后返回一个布尔值代表用户是否正在滚动和停止滚动两种状态,为了完成额外的操作,我们还可以返回一个用户滚动停止时的当前元素距离文档顶部的一个距离,也就是scrollTop。
如此一来,这个函数的实现就比较简单了,还是在react钩子函数中监听滚动事件,然后执行修改状态值的操作。但是现在还有一个问题,那就是我们如何去存储状态?
核心代码实现第一步--解决状态存储的响应式
如果使用useState来存储的话,似乎并没有达到响应式,好在react还提供了一个useRef函数,这个是一个响应式的,我们可以基于这个hooks函数结合useReducer函数来封装一个useGetSet函数,这个函数也在这里有总结到,感兴趣的可以去看看。
这个函数的实现其实也不难,主要就是利用useReducer的第二个参数强行去更新状态值,然后返回更新后的状态值。代码如下:
export const useGetSet = <T>(initialState:T):[() => T,(s:T) => void] => {
const state = useRef<T>(initialState);
const [,update] = useReducer(() => Object.create(null),{});
const updateState = (newState: T) => {
state.current = newState;
update();
}
return useMemo(() => [
() => state.current,
updateState
],[])
}
核心代码实现第二步--构建hooks函数
接下来我们来看这个hooks函数,很明显这个hooks函数有2个参数,第一个则是监听滚动事件的滚动元素,第二个则是一个延迟时间。滚动元素的类型与前面的拖拽函数保持一致,我们来详细看看。
const useIsScroll = <T extends HTMLElement>(el: Window | T | (() => T) = window,throlleTime:number = 300): {
isScroll: boolean,
scrollTop: number
} => {
//核心代码
}
需要注意这里设置了默认值,el默认值时window对象,throlleTime默认值时300
接下来我们就是使用useSetGet方法存储状态,定义一个timer用于延迟函数定时器,然后监听滚动事件,在事件的回调函数中执行相应的修改状态值的操作,最后就是返回这两个状态值即可。代码如下:
const useIsScroll = <T extends HTMLElement>(el: Window | T | (() => T) = window,throlleTime:number = 300): {
isScroll: boolean,
scrollTop: number
} => {
const [isScroll,setIsScroll] = useGetSet(false);
const [scrollTop,setScrollTop] = useGetSet(0);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
const onScrollHandler = useCallback(() => {
setIsScroll(true);
setScrollTop(window.scrollY);
if(timer.current){
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
setIsScroll(false);
},throlleTime)
},[])
useLayoutEffect(() => {
const ele = typeof el === 'function' ? (el as () => T)() : el;
if(!ele){
return;
}
ele.addEventListener('scroll',onScrollHandler,false);
return () => {
ele.removeEventListener('scroll',onScrollHandler,false);
}
},[]);
return {
isScroll: isScroll(),
scrollTop: scrollTop()
};
}
整个hooks函数代码实现起来简单明了,所以也没什么难点,只要理解到了思路,就很简单了。
两个hooks函数的使用
核心功能我们已经实现了,接下来使用起来也比较简单,样式代码如下:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,html {
height: 100%;
}
body {
overflow:auto;
}
.App {
position: relative;
}
.overHeight {
height: 3000px;
}
.drag {
position: fixed;
width: 150px;
height: 150px;
border: 3px solid #2396ef;
background: linear-gradient(135deg,#efac82 10%,#da5b0c 90%);
z-index: 2;
left: 0;
top: 0;
}
.transition {
transition: all 1.2s ease-in-out;
}
组件代码如下:
import useAnyDrag from "./hooks/useAnyDrag";
import './App.css';
import useIsScroll from "./hooks/useIsScroll";
import { createRef } from "react";
const App = () => {
// 这里是使用核心代码
const { x, y, isMove, centerX, minX, maxX } = useAnyDrag(() => document.querySelector('#drag'));
//这里是使用核心代码
const {isScroll} = useIsScroll();
const scrollElement = createRef<HTMLDivElement>();
const getLeftPosition = () => {
if (!x || !centerX || isMove) {
return x;
}
if (x <= centerX) {
return minX || 0;
} else {
return maxX;
}
}
const scrollPosition = () => {
if (typeof getLeftPosition() === 'number') {
if (getLeftPosition() === 0) {
return -((scrollElement.current?.offsetWidth || 0) / 2);
} else {
return getLeftPosition() as number + (scrollElement.current?.offsetWidth || 0) / 2;
}
}
return 0;
}
return (
<div className="App">
<div className="overHeight"></div>
<div className={`${ isScroll ? 'drag transition' : 'drag'}`}
style={{ left: (isScroll ? scrollPosition() : getLeftPosition()) + 'px', top: y + 'px' }}
id="drag"
ref={scrollElement}
></div>
</div>
)
}
export default App;
结语
经过以上的分析,我们就完成了这样一个需求,感觉实现完了之后,还是收获满满的,总结一下我学到了什么。
拖拽事件的监听以及拖拽坐标的计算
滚动事件的监听以及react响应式状态的实现
移动端环境与pc环境的判断
如何知道用户停止了滚动
本文就到此为止了,感谢大家观看,最后贴一下在线demo如下所示。
作者:夕水
来源:juejin.cn/post/7163153386911563813
动态适配 web 终端的尺寸
使Xterminal组件自适应容器
通过 xtermjs
所创建的终端大小是由cols、rows
这两个配置项的来决定,虽然你可以通过 CSS 样式来让其产生自适应效果,但是这种情况下字体会变得模糊变形等、会出现一系列的问题,要解决这个问题我们还是需要使用cols、rows
这两个值来动态设置。
红色部分则是通过cols
和rows
属性控制,我们可以很明显的看到该终端组件并没有继承父元素的宽度以及高度,而实际的宽度则是通过cols
、rows
两个属性控制的。
如何动态设置cols和rows这两个参数。
我们去看官方官方文档的时候,会注意到,官方有提供几个插件供我们使用。
而xterm-addon-fit
: 可以帮助我们来让 web 终端实现宽度自适应容器。目前的话行数还不行,暂时没有找到好的替代方案,需要动态的计算出来,关于如何计算可以参数 vscode 官方的实现方案。
引入xterm-addon-fit
,在我们的案例中,加入下面这两行:
动态计算行数
想要动态计算出行数的话,就需要获取到一个dom元素的高度:
动态计算尺寸的方法。
const terminalReact: null | HTMLDivElement = terminalRef.current // 100% * 100%
const xtermHelperReact: DOMRect | undefined = terminalReact?.querySelector(".xterm-helper-textarea")!.getBoundingClientRect()
const parentTerminalRect = terminalReact?.getBoundingClientRect()
const rows = Math.floor((parentTerminalRect ? parentTerminalRect.height : 20) / (xtermHelperReact ? xtermHelperReact.height : 1))
const cols = Math.floor((parentTerminalRect ? parentTerminalRect.width : 20) / (xtermHelperReact ? xtermHelperReact.width : 1))
// 调用resize方法,重新设置大小
termRef.current.resize(cols, rows)
复制代码
我们可以考虑封装成一个函数,只要父亲组件的大小发生变化,就动态适配一次。
链接:https://juejin.cn/post/7160332506015727629
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Backbone前端框架解读
一、 什么是Backbone
在前端的发展道路中,前端框架元老之一jQuery对繁琐的DOM操作进行了封装,提供了链式调用、各类选择器,屏蔽了不同浏览器写法的差异性,但是前端开发过程中依然存在作用域污染、代码复用度低、冗余度高、数据和事件绑定烦琐等痛点。
5年后,Backbone横空出世,通过与Underscore、Require、Handlebar的整合,提供了一个轻量和友好的前端开发解决方案,其诸多设计思想对于后续的现代化前端框架发展起到了举足轻重的作用,堪称现代前端框架的基石。
通过对Backbone前端框架的学习,让我们领略其独特的设计思想。
二、 核心架构
按照MVC框架的定义,MVC是用来将应用程序分为三个主要逻辑组件的架构模式:模型,视图和控制器。这些组件被用来处理一个面向应用的特定开发。 MVC是最常用的行业标准的Web开发框架,以创建可扩展的项目之一。 Backbone.js为复杂WEB应用程序提供模型(models)、集合(collections)、视图(views)的结构。
◦ 其中模型用于绑定键值数据,并通过RESRful JSON接口连接到应用程序;
◦ 视图用于UI界面渲染,可以声明自定义事件,通过监听模型和集合的变化执行相应的回调(如执行渲染)。
如图所示,当用户与视图层产生交互时,控制层监听变化,负责与数据层进行数据交互,触发数据Change事件,从而通知视图层重新渲染,以实现UI界面更新。更进一步,当数据层发生变化时,由Backbone提供了数据层和服务器数据共享同步的能力。
其设计思想主要包含以下几点:
◦数据绑定(依赖渲染模板引擎)、事件驱动(依赖Events)
◦视图组件化,并且组件有了生命周期的概念
◦前端路由配置化,实现页面局部刷新
这些创新的思想,在现代前端框架中进一步得到了继承和发扬。
三、 部分源码解析
Backbone极度轻量,编译后仅有几kb,贯穿其中的是大量的设计模式:工厂模式、观察者模式、迭代器模式、适配器模式……,代码流畅、实现过程比较优雅。按照功能拆分为了Events、Model、Collection、Router、History、View等若干模块,这里摘取了部分精彩源码进行了解析,相信对我们的日常代码开发也有一定指导作用:
(1)迭代器
EventsApi起到一个迭代器分流的作用,对多个事件进行解析拆分,设计的非常经典,执行时以下用法都是合法的:
◦用法一:传入一个名称和回调函数的对象
modal.on({
"change": change_callback,
"remove": remove_callback
})
◦用法二:使用空格分割的多个事件名称绑定到同一个回调函数上
model.on("change remove", common_callback)
实现如下:
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if(name && typeof name === 'object') {
// 处理第一种用法
if(callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
for(names = _.keys(names); i < names.length; i++) events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
} else if(name && eventSplitter.test(name)) {
// 处理第二种用法
for(names = name.split(eventSplitter); i < names.length; i++) events = iteratee(events, names[i], callback, opts);
} else {
events = iteratee(events, name, callback, opts);
}
return events;
}
(2)监听器
用于一个对象监听另外一个对象的事件,例如,在A对象上监听在B对象上发生的事件,并且执行A的回调函数:
A.listenTo(B, "b", callback)
实际上这个功能用B对象来监听也可以实现:
B.on("b", callback, A)
这么做的好处是,方便对A创建、销毁逻辑的代码聚合,并且对B的侵入程度较小。实现如下:
Events.listenTo = function(obj, name, callback) {
if(!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
// 当前对象的所有监听对象
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
if(!listening) {
// 创建自身监听id
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
}
// 执行对象绑定
internalOn(obj, name, callback, this, listening);
return this;
}
(3)Model值set
通过option-flags兼容赋值、更新、删除等操作,这么做的好处是融合公共逻辑,简化代码逻辑和对外暴露api。实现如下:
set: function(key, val, options) {
if(key == null) return this;
// 支持两种赋值方式: 对象或者 key\value
var attrs;
if(typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
……
var unset = options.unset;
var silent = options.silent;
var changes = [];
var changing = this._changing; // 处理嵌套set
this._changing = true;
if(!changing) {
// 存储变更前的状态快照
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
var current = this.attributes;
var changed = this.changed;
var prev = this._previousAttributes;
for(var attr in attrs) {
val = attrs[attr];
if(!_.isEqual(current[attr], val)) changes.push(attr);
// changed只存储本次变化的key
if(!_.isEqual(prev[attr], val)) {
changed[attr] = val;
} else {
delete changed[attr]
}
unset ? delete current[attr] : (current[attr] = val)
}
if(!silent) {
if(changes.length) this._pending = options;
for(var i=0; i<changes.length; i++) {
// 触发 change:attr 事件
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
if(changing) return this;
if(!silent) {
// 处理递归change场景
while(this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
}
四、 不足(对比react、vue)
对比现代前端框架,由于Backbone本身比较轻量,对一些内容细节处理不够细腻,主要体现在:
◦视图和数据的交互关系需要自己分类编写逻辑,需要编写较多的监听器
◦监听器数量较大,需要手动销毁,维护成本较高
◦视图树的二次渲染仅能实现组件整体替换,并非增量更新,存在性能损失
◦路由切换需要自己处理页面更新逻辑
五、为什么选择Backbone
看到这里,你可能有些疑问,既然Backbone存在这些缺陷,那么现在学习Backbone还有什么意义呢?
首先,对于服务端开发人员,Backbone底层依赖underscore/lodash、jQuery/Zepto,目前依然有很多基于Jquery和Velocity的项目需要维护,会jQuery就会Backbone,学习成本低;通过Backbone能够学习用数据去驱动View更新,优化jQuery的写法;Backbone面对对象编程,符合Java开发习惯。
其次,对于前端开发人员,能够学习其模块化封装库类函数,提升编程技艺。Backbone的组件化开发,和现代前端框架有很多共通之处,能够深入理解其演化历史。
作者:京东零售 陈震
来源:juejin.cn/post/7197075558941311035
一篇文章带你掌握Flex布局的所有用法
Flex 布局目前已经非常流行了,现在几乎已经兼容所有浏览器了。在文章开始之前我们需要思考一个问题:我们为什么要使用 Flex 布局?
其实答案很简单,那就是 Flex 布局好用。一个新事物的出现往往是因为旧事物不那么好用了,比如,如果想让你用传统的 css 布局来实现一个块元素垂直水平居中你会怎么做?实现水平居中很简单,margin: 0 auto
就行,而实现垂直水平居中则可以使用定位实现:
<div class="container">
<div class="item"></div>
</div>
.container {
position: relative;
width: 300px;
height: 300px;
background: red;
}
.item {
position: absolute;
background: black;
width: 50px;
height: 50px;
margin: auto;
left: 0;
top: 0;
bottom: 0;
right: 0;
}
或者
.item {
position: absolute;
background: black;
width: 50px;
height: 50px;
margin: auto;
left: calc(50% - 25px);
top: calc(50% - 25px);
}
但是这样都显得特别繁琐,明明可以一个属性就能解决的事情没必要写这么麻烦。而使用 Flex 则可以使用 place-content 属性简单的实现(place-content 为 justify-content 和 align-content 简写属性)
.container {
width: 300px;
height: 300px;
background: red;
display: flex;
place-content: center;
}
.item {
background: black;
width: 50px;
height: 50px;
}
接下来的本篇文章将会带领大家一起来探讨Flex
布局
基本概念
我们先写一段代码作为示例(部分属性省略)
html
<div class="container">
<div class="item">flex项目</div>
<div class="item">flex项目</div>
<div class="item">flex项目</div>
<div class="item">flex项目</div>
</div>
.container {
display: flex;
width: 800px;
gap: 10px;
}
.item {
color: #fff;
}
flex 容器
我们可以将一个元素的 display 属性设置为 flex,此时这个元素则成为flex 容器比如container
元素
flex 项目
flex 容器的子元素称为flex 项目,比如item
元素
轴
flex 布局有两个轴,主轴和交叉轴,至于哪个是主轴哪个是交叉轴则有flex 容器的flex-direction
属性决定,默认为:flex-direction:row
,既横向为主轴,纵向为交叉轴,
flex-direction
还可以设置其它三个属性,分别为row-reverse
,column
,column-reverse
。
row-reverse
column
column-reverse
从这里我们可以看出 Flex 轴的方向不是固定不变的,它受到flex-direction
的影响
不足空间和剩余空间
当 Flex 项目总宽度小于 Flex 容器宽度时就会出现剩余空间
当 Flex 项目总宽度大于 Flex 容器宽度时就会出现不足空间
Flex 项目之间的间距
Flex 项目之间的间距可以直接在 Flex 容器上设置 gap 属性即可,如
<div class="container">
<div class="item">A</div>
<div class="item">B</div>
<div class="item">C</div>
<div class="item">D</div>
</div>
.container {
display: flex;
width: 500px;
height: 400px;
gap: 10px;
}
.item {
width: 150px;
height: 40px;
}
Flex 属性
flex
属性是flex-grow
,flex-shrink
,flex-basis
三个属性的简写。下面我们来看下它们分别是什么。
flex-basis
可以设定 Flex 项目的大小,一般主轴为水平方向的话和 width 解析方式相同,但是它不一定是 Flex 项目最终大小,Flex 项目最终大小受到flex-grow
,flex-shrink
以及剩余空间等影响,后面文章会告诉大家最终大小的计算方式flex-grow
为 Flex 项目的扩展系数,当 Flex 项目总和小于 Flex 容器时就会出现剩余空间,而flex-grow
的值则可以决定这个 Flex 项目可以分到多少剩余空间flex-shrink
为 Flex 项目的收缩系数,同样的,当 Flex 项目总和大于 Flex 容器时就会出现不足空间,flex-shrink
的值则可以决定这个 Flex 项目需要减去多少不足空间
既然flex
属性是这三个属性的简写,那么flex
属性简写方式分别代表什么呢?
flex
属性可以为 1 个值,2 个值,3 个值,接下来我们就分别来看看它们代表什么意思
一个值
如果flex
属性只有一个值的话,我们可以看这个值是否带单位,带单位那就是flex-basis
,不带就是flex-grow
.item {
flex: 1;
/* 相当于 */
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
}
.item {
flex: 30px;
/* 相当于 */
flex-grow: 1;
flex-shrink: 1;
flex-basis: 30px;
}
两个值
当flex
属性有两个值的话,第一个无单位的值就是flex-grow
,第一个无单位的值则是flex-shrink
,有单位的就是flex-basis
.item {
flex: 1 2;
/* 相当于 */
flex-grow: 1;
flex-shrink: 2;
flex-basis: 0;
}
.item {
flex: 30px 2;
/* 相当于 */
flex-grow: 2;
flex-shrink: 1;
flex-basis: 30px;
}
三个值
当flex
属性有三个值的话,第一个无单位的值就是flex-grow
,第一个无单位的值则是flex-shrink
,有单位的就是flex-basis
.item {
flex: 1 2 10px;
/* 相当于 */
flex-grow: 1;
flex-shrink: 2;
flex-basis: 10px;
}
.item {
flex: 30px 2 1;
/* 相当于 */
flex-grow: 2;
flex-shrink: 1;
flex-basis: 30px;
}
.item {
flex: 2 30px 1;
/* 相当于 */
flex-grow: 2;
flex-shrink: 1;
flex-basis: 30px;
}
另外,flex 的值还可以为initial
,auto
,none
。
initial
initial 为默认值,和不设置 flex 属性的时候表现一样,既 Flex 项目不会扩展,但会收缩,Flex 项目大小有本身内容决定
.item {
flex: initial;
/* 相当于 */
flex-grow: 0;
flex-shrink: 1;
flex-basis: auto;
}
auto
当 flex 设置为 auto 时,Flex 项目会根据自身内容确定flex-basis
,既会拓展也会收缩
.item {
flex: auto;
/* 相当于 */
flex-grow: 1;
flex-shrink: 1;
flex-basis: auto;
}
none
none 表示 Flex 项目既不收缩,也不会扩展
.item {
flex: none;
/* 相当于 */
flex-grow: 0;
flex-shrink: 0;
flex-basis: auto;
}
Flex 项目大小的计算
首先看一下 flex-grow 的计算方式
flex-grow
面试中经常问到: 为什么 flex 设置为 1 的时候,Flex 项目就会均分 Flex 容器? 其实 Flex 项目设置为 1 不一定会均分容器(后面会解释),这里我们先看下均分的情况是如何发生的
同样的我们先举个例子
<div>
<div>Xiaoyue</div>
<div>June</div>
<div>Alice</div>
<div>Youhu</div>
<div>Liehuhu</div>
</div>
.container {
display: flex;
width: 800px;
}
.item {
flex: 1;
font-size: 30px;
}
flex 容器总宽度为 800px,flex 项目设置为flex:1
,此时页面上显示
我们可以看到每个项目的宽度为 800/5=160,下面来解释一下为什么会均分:
首先
.item {
flex: 1;
/* 相当于 */
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0;
}
因为flex-basis
为 0,所有 Flex 项目扩展系数都是 1,所以它们分到的剩余空间都是一样的。下面看一下是如何计算出最终项目大小的
这里先给出一个公式:
Flex项目弹性量 = (Flex容器剩余空间/所有flex-grow总和)\*当前Flex项目的flex-grow
其中Flex项目弹性量
指的是分配给 Flex 项目多少的剩余空间,所以 Flex 项目的最终宽度为
flex-basis+Flex项目弹性量
。
根据这个公式,上面的均分也就很好理解了,因为所有的flex-basis
为 0,所以剩余空间就是 800px,每个 Flex 项目的弹性量也就是(800/1+1+1+1+1)*1=160
,那么最终宽度也就是160+0=160
刚刚说过 flex 设置为 1 时 Flex 项目并不一定会被均分,下面就来介绍一下这种情况,我们修改一下示例中的 html,将第一个 item 中换成一个长单词
<div>
<div>Xiaoyueyueyue</div>
<div>June</div>
<div>Alice</div>
<div>Youhu</div>
<div>Liehu</div>
</div>
此时会发现 Flex 容器并没有被均分
因为计算出的灵活性 200px 小于第一个 Flex 项目的min-content
(217.16px),此时浏览器会采用 Flex 项目的min-content
作为最终宽度,而后面的 Flex 项目会在第一个 Flex 项目计算完毕后再进行同样的计算
我们修改一下 flex,给它设置一个 flex-basis,看下它计算之后的情况
.item {
text-align: center;
flex: 1 100px;
}
因为每个项目的flex-basis
都是 100px,Flex 容器剩余空间为800-500=300px
,所以弹性量就是(300/5)*1=60px
,最终宽度理论应该为100+60=160px
,同样的因为第一个 Flex 项目的min-content
为 217.16px,所以第一个 Flex 项目宽度被设置为 217.16px,最终表现和上面一样
我们再来看一下为什么第 2,3,4,5 个 Flex 项目宽度为什么是 145.71px
当浏览器计算完第一个 Flex 项目为 217.16px 后,此时的剩余空间为800-217.16-100*4=182.84
,第 2 个 Flex 项目弹性量为(182.84/1+1+1+1)*1=45.71
,所以最终宽度为100+45.71=145.71px
,同样的后面的 Flex 项目计算方式是一样的,但是如果后面再遇到长单词,假如第五个是长单词,那么不足空间将会发生变化,浏览器会将第五个 Flex 项目宽度计算完毕后再回头进行一轮计算,具体情况这里不再展开
所以说想要均分 Flex 容器 flex 设置为 1 并不能用在所有场景中,其实当 Flex 项目中有固定宽度元素也会出现这种情况,比如一张图片等,当然如果你想要解决这个问题其实也很简单,将 Flex 项目的min-width
设置为 0 即可
.item {
flex: 1 100px;
min-width: 0;
}
flex-grow 为小数
flex-grow 的值不仅可以为正整数,还可以为小数,当为小数时也分为两种情况:所有 Flex 项目的 flex-grow 之和小于等于 1 和大于 1,我们先看小于等于 1 的情况,将例子的改成
<div>
<div>Acc</div>
<div>Bc</div>
<div>C</div>
<div>DDD</div>
<div>E</div>
</div>
.item:nth-of-type(1) {
flex-grow: 0.1;
}
.item:nth-of-type(2) {
flex-grow: 0.2;
}
.item:nth-of-type(3) {
flex-grow: 0.2;
}
.item:nth-of-type(4) {
flex-grow: 0.1;
}
.item:nth-of-type(5) {
flex-grow: 0.1;
}
效果如图
我们可以发现项目并没有占满容器,它的每个项目的弹性量计算方式为
Flex项目弹性量=Flex容器剩余空间*当前Flex项目的flex-grow
相应的每个项目的实际宽度也就是flex-basis+弹性量
,首先先不设置 flex-grow,我们可以看到每个项目的 flex-basis 分别为: 51.2 , 33.88 , 20.08 , 68.56 , 16.5
所以我们可以计算出 Flex 容器的剩余空间为800-51.2 -33.88 - 20.08 - 68.56 - 16.5=609.78
,这样我们就可以算出每个项目的实际尺寸为
A: 实际宽度 = 51.2 + 609.78*0.1 = 112.178
B: 实际宽度 = 33.88 + 609.78*0.2 = 155.836
...
下面看下 flex-grow 之和大于 1 的情况,将例子中的 css 改为
.item:nth-of-type(1) {
flex-grow: 0.1;
}
.item:nth-of-type(2) {
flex-grow: 0.2;
}
.item:nth-of-type(3) {
flex-grow: 0.3;
}
.item:nth-of-type(4) {
flex-grow: 0.4;
}
.item:nth-of-type(5) {
flex-grow: 0.5;
}
此时的效果为
可以看出 Flex 项目是占满容器的,它的计算方式其实和 flex-grow 为正整数时一样
Flex项目弹性量 = (Flex容器剩余空间/所有flex-grow总和)\*当前Flex项目的flex-grow
所以我们可以得出一个结论: Flex 项目的 flex-grow 之和小于 1,Flex 项目不会占满 Flex 容器
flex-shrink
flex-shrink 其实和 flex-grow 基本一样,就是扩展变成了收缩,flex-grow 是项目比例增加容器剩余空间,而 flex-shrink 则是比例减去容器不足空间
修改一下我们的例子:
.item {
flex-basis: 200px;
/* 相当于 */
flex-shrink: 1;
flex-grow: 0;
flex-basis: 200px;
}
此时项目的总宽度200*5=1000px
已经大于容器总宽度800px
,此时计算第一个项目的不足空间就是800-200*5=-200px
,第二个项目的不足空间则是800-第一个项目实际宽度-200*4
,依次类推
最终计算公式其实和 flex-grow 计算差不多
Flex项目弹性量 = (Flex容器不足空间/所有flex-shrink总和)*当前Flex项目的flex-shrink
只不过,所以上面例子每个项目可以计算出实际宽度为
第一个 Flex 项目: 200+((800-200x5)/5)*1 = 160px
第二个 Flex 项目: 200+((800-160-200x4)/4)*1 = 160px
第三个 Flex 项目: 200+((800-160-160-200x3)/3)*1 = 160px
第四个 Flex 项目: 200+((800-160-160-160-200x2)/2)*1 = 160px
第五个 Flex 项目: 200+((800-160-160-160-160-200x1)/1)*1 = 160px
如果 Flex 项目的min-content
大于flex-basis
,那么最终的实际宽度将会取该项目的min-content
,比如改一下例子,将第一个 Flex 项目改成长单词
<div>
<div>XiaoyueXiaoyue</div>
<div>June</div>
<div>Alice</div>
<div>Youhu</div>
<div>Liehu</div>
</div>
可以看出浏览器最终采用的是第一个 Flex 项目的min-content
作为实际宽度,相应的后面 Flex 项目的宽度会等前一个 Flex 项目计算完毕后在进行计算
比如第二个 Flex 项目宽度= 200+((800-228.75-200x4)/4)*1 = 142.81px
flex-shrink 为小数
同样的 flex-shrink 也会出现小数的情况,也分为 Flex 项目的 flex-shrink 之和小于等于 1 和大于 1 两种情况,如果大于 1 和上面的计算方式一样,所以我们只看小于 1 的情况,将我们的例子改为
.item {
flex-basis: 200px;
flex-shrink: 0.1;
}
效果为
此时我们会发现 Flex 项目溢出了容器,所以我们便可以得出一个结论:Flex 项目的 flex-shrink 之和小于 1,Flex 项目会溢出 Flex 容器
下面看一下它的计算公式
Flex项目弹性量=Flex容器不足空间*当前Flex项目的flex-shrink
Flex项目实际宽度=flex-basis + Flex项目弹性量
比如上面例子的每个 Flex 项目计算结果为
第一个 Flex 项目宽度 = 200+(800-200x5)x0.1=180px
,但是由于它本身的min-content
为 228.75,所以最终宽度为 228.75
第二个 Flex 项目宽度 =200-(800-228.75-200x4)x0.1=117.125
第三个 Flex 项目宽度...
Flex 的对齐方式
Flex 中关于对齐方式的属性有很多,其主要分为两种,一是主轴对齐方式:justify-,二是交叉轴对齐方式:align-
首先改一下我们的例子,将容器设置为宽高为 500x400 的容器(部分属性省略)
<div>
<div>A</div>
<div>B</div>
<div>C</div>
</div>
.container {
display: flex;
width: 500px;
height: 400px;
}
.item {
width: 100px;
height: 40px;
}
主轴对齐属性
这里以横向为主轴,纵向为交叉轴
justify-content
justify-content的值可以为:
flex-start 默认值,主轴起点对齐
flex-end 主轴终点对齐
left 默认情况下和 flex-start 一致
right 默认情况下和 flex-end 一致
center 主轴居中对齐
space-between 主轴两端对齐,并且 Flex 项目间距相等
space-around 项目左右周围空间相等
space-evenly 任何两个项目之间的间距以及边缘的空间相等
交叉轴对齐方式
align-content
align-content 属性控制整个 Flex 项目在 Flex 容器中交叉轴的对齐方式
注意设置 align-content 属性时候必须将 flex-wrap 设置成 wrap 或者 wrap-reverse。它可以取得值为
stretch 默认值,当我们 Flex 元素不设置高度的时候,默认是拉伸的
比如将 Flex 元素宽度去掉
.item {
width: 100px;
}
flex-start 位于容器开头,这个和 flex-direction:属性有关,默认在顶部
flex-end 位于容器结尾
center 元素居中对齐
space-between 交叉轴上下对齐,并且 Flex 项目上下间距相等
此时我们改下例子中 Flex 项目的宽度使其换行,因为如果 Flex 项目只有一行,那么 space-between 与 flex-start 表现一致
.item {
width: 300px;
}
space-around 项目上下周围空间相等
space-evenly 任何两个项目之间的上下间距以及边缘的空间相等
align-items
align-items 属性定义 flex 子项在 flex 容器的当前行的交叉轴方向上的对齐方式。它与 align-content 有相似的地方,它的取值有
stretch 默认值,当我们 Flex 元素不设置高度的时候,默认是拉伸的
center 元素位于容器的中心,每个当前行在图中已经框起来
flex-start 位于容器开头
flex-end 位于容器结尾
baseline 位于容器的基线上
比如给 A 项目一个 padding-top
.item:nth-of-type(1) {
padding-top: 50px;
}
没设置 baseline 的表现
设置 baseline 之后
通过上面的例子我们可以发现,如果想要整个 Flex 项目垂直对齐,在只有一行的情况下,align-items 和 align-content 设置为 center 都可以做到,但是如果出现多行的情况下 align-items 就不再适用了
align-self
上面都是给 Flex 容器设置的属性,但是如果想要控制单个 Flex 项目的对齐方式该怎么办呢?
其实 Flex 布局中已经考虑到了这个问题,于是就有个 align-self 属性来控制单个 Flex 项目在 Flex 容器侧交叉轴的对齐方式。
align-self 和 align-items 属性值几乎是一致的,比如我们将整个 Flex 项目设置为 center,第二个 Flex 项目设置为 flex-start
.container {
display: flex;
width: 500px;
height: 400px;
align-items: center;
}
.item {
width: 100px;
height: 40px;
}
.item:nth-of-type(2) {
align-self: flex-start;
}
注意,除了以上提到的属性的属性值,还可以设置为 CSS 的关键词如 inherit 、initial 等
交叉轴与主轴简写
place-content
place-content` 为 `justify-content` 和 `align-content` 的简写形式,可以取一个值和两个值,如果设置一个值那么 `justify-content` 和 `align-content` 都为这个值,如果是两个值,第一个值为 `align-content`,第二个则是 `justify-content
到这里关于Flex布局基本已经介绍完了,肯定会有些细枝末节没有考虑到,这可能就需要我们在平时工作和学习中去发现了
作者:东方小月
来源:https://juejin.cn/post/7197229913156796472
我竟然完美地用js实现默认的文本框粘贴事件
前言:本文实际是用js移动控制光标的位置!解决了网上没有可靠教程的现状
废话连篇
默认情况对一个文本框粘贴,应该会有这样的功能:
粘贴文本后,光标不会回到所有文本的最后位置,而是在粘贴的文本之后
将选中的文字替换成粘贴的文本
但是由于需求,我们需要拦截粘贴的事件,对剪贴板的文字进行过滤,这时候粘贴的功能都得自己实现了,而一旦自己实现,上面2个功能就不见了,我们就需要还原它。
面对这样的需求,我们肯定要控制移动光标,可是现在的网上环境真的是惨,千篇一律的没用代码...于是我就发表了这篇文章。
先上代码
<textarea id="text" style="width: 996px; height: 423px;"></textarea>
<script>
// 监听输入框粘贴事件
document.getElementById('text').addEventListener('paste', function (e) {
e.preventDefault();
let clipboardData = e.clipboardData.getData('text');
// 这里写你对剪贴板的私货
let tc = document.querySelector("#text");
tc.focus();
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;
if(tc.selectionStart != tc.selectionEnd){
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionEnd)
}else{
tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);
}
// 重新设置光标位置
tc.selectionEnd =tc.selectionStart = start
});
</script>
怎么理解上述两个功能?
第一个解释:
比如说现在文本框有:
染念真的很生气
如果我们现在在真的后面粘贴不要
,变成
染念真的不要很生气|
拦截后的光标是在生气后面,但是我们经常使用发现,光标应该出现在不要的后面吧!
就像这样:
染念真的不要|很生气
第2个解释:
染念真的不要很生气
我们全选真的的同时粘贴求你
,拦截后会变成
染念真的求你不要很生气|
但默认应该是:
染念求你|不要很生气
代码分析
针对第2个问题,我们应该先要获取默认的光标位置在何处,tc.selectionStart
是获取光标开始位置,tc.selectionEnd
是获取光标结束位置。
为什么这里我写了一个判断呢?因为默认时候,我们没有选中一块区域,就是把光标人为移动到某个位置(读到这里,光标在位置后面,现在人为移动到就是前面,这个例子可以理解不?),这个时候两个值是相等的。
233|333
^--^
1--4
tc.selectionEnd=4,tc.selectionStart = 4
如果相等,说明就是简单的定位,tc.value = tc.value.substring(0,tc.selectionStart)+clipboardData+tc.value.substring(tc.selectionStart);
,tc.value.substring(0,tc.selectionStart)
获取光标前的内容,tc.value.substring(tc.selectionStart)
是光标后的内容。
如果不相等,说明我们选中了一个区域(光标选中一块区域说明我们选中了一个区域
),代码只需要在最后获取光标后的内容这的索引改成tc.selectionEnd
|233333|
^------^
1------7
tc.selectionEnd=7,tc.selectionStart = 1
在获取光标位置之前,我们应该先使用tc.focus();
聚焦,使得光标回到文本框的默认位置(最后),这样才能获得位置。
针对第1个问题,我们就要把光标移动到粘贴的文本之后,我们需要计算位置。
获得这个位置,一定要在tc.value重新赋值之前,因为这样的索引都没有改动。
const start = (tc.value.substring(0,tc.selectionStart)+clipboardData).length;
这个代码和上面解释重复,很简单,我就不解释了。
最后处理完了,重新设置光标位置,tc.selectionEnd =tc.selectionStart = start
,一定让selectionEnd和selectionStart相同,不然选中一个区域了。
如果我们在value重新赋值之后获取(tc.value.substr(0,tc.selectionStart)+clipboardData).length
,大家注意到没,我们操作的是tc.value,value已经变了,这里的重新定位光标开始已经没有任何意义了!
作者:染念
来源:dyedd.cn/943.html
闭包用多了会造成内存泄露 ?
闭包,是JS中的一大难点;网上有很多关于闭包会造成内存泄露的描述,说闭包会使其中的变量的值始终保持在内存中,一般都不太推荐使用闭包
而项目中确实有很多使用闭包的场景,比如函数的节流与防抖
那么闭包用多了,会造成内存泄露吗?
场景思考
以下案例: A 页面引入了一个 debounce
防抖函数,跳转到 B 页面后,该防抖函数中闭包所占的内存会被 gc 回收吗?
该案例中,通过变异版的防抖函数
来演示闭包的内存回收,此函数中引用了一个内存很大的对象 info
(42M的内存),便于明显地对比内存的前后变化
注:可以使用 Chrome 的 Memory 工具查看页面的内存大小:
场景步骤:
1) util.js
中定义了 debounce
防抖函数
// util.js`
let info = {
arr: new Array(10 * 1024 * 1024).fill(1),
timer: null
};
export const debounce = (fn, time) => {
return function (...args) {
info.timer && clearTimeout(info.timer);
info.timer = setTimeout(() => {
fn.apply(this, args);
}, time);
};
};
2) A 页面中引入并使用该防抖函数
import { debounce } from './util';
mounted() {
this.debounceFn = debounce(() => {
console.log('1');
}, 1000)
}
抓取 A 页面内存:
57.1M
3) 从 A 页面跳转到 B 页面,B 页面中没有引入该 debounce 函数
问题: 从 A 跳转到 B 后,该函数所占的内存会被释放掉吗?
此时,抓取 B 页面内存:
58.1M
刷新 B 页面,该页面的原始内存为:
16.1M
结论: 前后对比发现,从 A 跳转到 B 后,B 页面内存增大了 42M
,证明该防抖函数所占的内存没有被释放掉,即造成了内存泄露
为什么会这样呢? 按理说跳转 B 页面后,A 页面的组件都被销毁掉了,那么 A 页面所占的内存应该都被释放掉了啊😕
我们继续对比测试
4) 如果把 info 对象放到 debounce 函数内部,从 A 跳转到 B 后,该防抖函数所占的内存会被释放掉吗?
// util.js`
export const debounce = (fn, time) => {
let info = {
arr: new Array(10 * 1024 * 1024).fill(1),
timer: null
};
return function (...args) {
info.timer && clearTimeout(info.timer);
info.timer = setTimeout(() => {
fn.apply(this, args);
}, time);
};
};
按照步骤 4 的操作,重新从 A 跳转到 B 后,B 页面抓取内存为16.1M
,证明该函数所占的内存被释放掉了
为什么只是改变了 info 的位置,会引起内存的前后变化?
要搞懂这个问题,需要理解闭包的内存回收机制
闭包简介
闭包:一个函数内部有外部变量的引用,比如函数嵌套函数时,内层函数引用了外层函数作用域下的变量,就形成了闭包。最常见的场景为:函数作为一个函数的参数,或函数作为一个函数的返回值时
闭包示例:
function fn() {
let num = 1;
return function f1() {
console.log(num);
};
}
let a = fn();
a();
上面代码中,a 引用了 fn 函数返回的 f1 函数,f1 函数中引入了内部变量 num,导致变量 num 滞留在内存中
打断点调试一下
展开函数 f 的 Scope(作用域的意思)选项,会发现有 Local 局部作用域、Closure 闭包、Global 全局作用域等值,展开 Closure,会发现该闭包被访问的变量是 num,包含 num 的函数为 fn
总结来说,函数 f 的作用域中,访问到了fn 函数中的 num 这个局部变量,从而形成了闭包
所以,如果真正理解好闭包,需要先了解闭包的内存引用,并且要先搞明白这几个知识点:
函数作用域链
执行上下文
变量对象、活动对象
函数的内存表示
先从最简单的代码入手,看下变量是如何在内存中定义的
let a = '小马哥'
这样一段代码,在内存里表示如下
在全局环境 window 下,定义了一个变量 a,并给 a 赋值了一个字符串,箭头表示引用
再定义一个函数
let a = '小马哥'
function fn() {
let num = 1
}
内存结构如下:
特别注意的是,fn 函数中有一个 [[scopes]] 属性,表示该函数的作用域链,该函数作用域指向全局作用域(浏览器环境就是 window),函数的作用域是理解闭包的关键点之一
请谨记:函数的作用域链是在创建时就确定了,JS 引擎会创建函数时,在该对象上添加一个名叫作用域链的属性,该属性包含着当前函数的作用域以及父作用域,一直到全局作用域
函数在执行时,JS 引擎会创建执行上下文,该执行上下文会包含函数的作用域链(上图中红色的线),其次包含函数内部定义的变量、参数等。在执行时,会首先查找当前作用域下的变量,如果找不到,就会沿着作用域链中查找,一直到全局作用域
垃圾回收机制浅析
现在各大浏览器通常用采用的垃圾回收有两种方法:标记清除、引用计数
这里重点介绍 "引用计数"(reference counting),JS 引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0
,就表示这个值不再用到了,因此可以将这块内存释放
上图中,左下角的两个值,没有任何引用,所以可以释放
如果一个值不再需要了,引用数却不为0
,垃圾回收机制无法释放这块内存,从而导致内存泄漏
判断一个对象是否会被垃圾回收的标准: 从全局对象 window 开始,顺着引用表能找到的都不是内存垃圾,不会被回收掉。只有那些找不到的对象才是内存垃圾,才会在适当的时机被 gc 回收
分析内存泄露的原因
回到最开始的场景,当 info 在 debounce 函数外部时,为什么会造成内存泄露?
进行断点调试
展开 debounce 函数的 Scope选项,发现有两个 Closure 闭包对象,第一个 Closure 中包含了 info 对象,第二个 Closure 闭包对象,属于 util.js 这个模块
内存结构如下:
当从 A 页面切换到 B 页面时,A 页面被销毁,只是销毁了 debounce 函数当前的作用域,但是 util.js 这个模块的闭包却没有被销毁,从 window 对象上沿着引用表依然可以查找到 info 对象,最终造成了内存泄露
当 info 在 debounce 函数内部时,进行断点调试
其内存结构如下:
当从 A 页面切换到 B 页面时,A 页面被销毁,同时会销毁 debounce 函数当前的作用域,从 window 对象上沿着引用表查找不到 info 对象,info 对象会被 gc 回收
闭包内存的释放方式
1、手动释放(需要避免的情况)
如果将闭包引用的变量定义在模块中,这种会造成内存泄露,需要手动释放,如下所示,其他模块需要调用 clearInfo 方法,来释放 info 对象
可以说这种闭包的写法是错误的 (不推荐), 因为开发者需要非常小心,否则稍有不慎就会造成内存泄露,我们总是希望可以通过 gc 自动回收,避免人为干涉
let info = {
arr: new Array(10 * 1024 * 1024).fill(1),
timer: null
};
export const debounce = (fn, time) => {
return function (...args) {
info.timer && clearTimeout(info.timer);
info.timer = setTimeout(() => {
fn.apply(this, args);
}, time);
};
};
export const clearInfo = () => {
info = null;
};
2、自动释放(大多数的场景)
闭包引用的变量定义在函数中,这样随着外部引用的销毁,该闭包就会被 gc 自动回收 (推荐),无需人工干涉
export const debounce = (fn, time) => {
let info = {
arr: new Array(10 * 1024 * 1024).fill(1),
timer: null
};
return function (...args) {
info.timer && clearTimeout(info.timer);
info.timer = setTimeout(() => {
fn.apply(this, args);
}, time);
};
};
结论
综上所述,项目中大量使用闭包,并不会造成内存泄漏,除非是错误的写法
绝大多数情况,只要引用闭包的函数被正常销毁,闭包所占的内存都会被 gc 自动回收。特别是现在 SPA 项目的盛行,用户在切换页面时,老页面的组件会被框架自动清理,所以我们可以放心大胆的使用闭包,无需多虑
理解了闭包的内存回收机制,才算彻底搞懂了闭包。以上关于闭包的理解,如果有误,欢迎指正 🌹
参考链接:
浏览器是怎么看闭包的。
JavaScript 内存泄漏教程
JavaScript闭包(内存泄漏、溢出以及内存回收),超直白解析
作者:海阔_天空
来源:juejin.cn/post/7196636673694285882
字节前端监控实践
简述
Slardar 前端监控自18年底开始建设以来,从仅仅作为 Sentry 的替代,经历了一系列迭代和发展,目前做为一个监控解决方案,已经应用到抖音、西瓜、今日头条等众多业务线。
据21年下旬统计,Slardar 前端监控(Web + Hybrd) 工作日晚间峰值 qps 300w+,日均处理数据超过千亿条。
本文,我将针对在这一系列的发展过程中,字节内部监控设计和迭代遇到的落地细节设计问题,管中窥豹,向大家介绍我们团队所思考和使用的解决方案。
他们主要围绕着前端监控体系建设的一些关键问题展开。也许大家对他们的原理早已身经百战见得多了,不过当实际要落地实现的时候,还是有许多细节可以再进一步琢磨的。
如何做好 JS 异常监控
JS 异常监控本质并不复杂,浏览器早已提供了全局捕获异常的方案。
window.addEventListenr('error', (err) => {
report(normalize(err))
});
window.addEventListenr('unhandledrejection', (rejection) => {
report(normalize(rejection))
});
复制代码
但捕获到错误仅仅只是相关工作的第一步。在我看来,JS 异常监控的目标是:
开发者迅速感知到 JS 异常发生
通过监控平台迅速定位问题
开发者能够高效的处理问题,并统计,追踪问题的处理进度
在异常捕获之外,还包括堆栈的反解与聚合,处理人分配和报警这几个方面。
堆栈反解: Sourcemap
大家都知道 Script 脚本在浏览器中都是以明文传输并执行,现代前端开发方案为了节省体积,减少网络请求数,不暴露业务逻辑,或从另一种语言编译成 JS。都会选择将代码进行混淆和压缩。在优化性能,提升用户体验的同时,也为异常的处理带来了麻烦。
在本地开发时,我们通常可以清楚的看到报错的源代码堆栈,从而快速定位到原始报错位置。而线上的代码经过压缩,可读性已经变得非常糟糕,上报的堆栈很难对应到原始的代码中。 Sourcemap 正是用来解决这个问题的。
简单来说,Sourcemap 维护了混淆后的代码行列到原代码行列的映射关系,我们输入混淆后的行列号,就能够获得对应的原始代码的行列号,结合源代码文件便可定位到真实的报错位置。
Sourcemap 的解析和反解析过程涉及到 VLQ 编码,它是一种将代码映射关系进一步压缩为类base64编码的优化手段。
在实际应用中,我们可以把它直接当成黑盒,因为业界已经为我们提供了方便的解析工具。下面是一个利用 mozila 的 sourcemap 库进行反解的例子。
以上代码执行后通常会得到这样的结果,实际上我们的在线反解服务也就是这样实现的
当然,我们不可能在每次异常发生后,才去生成 sourcemap,在本地或上传到线上进行反解。这样的效率太低,定位问题也太慢。另一个方案是利用 sourcemappingURL 来制定sourcemap 存放的位置,但这样等于将页面逻辑直接暴露给了网站使用者。对于具有一定规模和保密性的项目,这肯定是不能接受的。
//# sourceMappingURL=http://example.com/path/hello.js.map
复制代码
为了解决这个问题,一个自然的方案便是利用各种打包插件或二进制工具,在构建过程中将生成的 sourcemap 直接上传到后端。Sentry 就提供了类似的工具,而字节内部也是使用相似的方案。
通过如上方案,我们能够让用户在发版构建时就可以完成 sourcemap 的上传工作,而异常发生后,错误可以自动完成解析。不需要用户再操心反解相关的工作了。
堆栈聚合策略
当代码被成功反解后,用户已经可以看到这一条错误的线上和原始代码了,但接下来遇到的问题则是,如果我们只是上报一条存一条,并且给用户展示一条错误,那么在平台侧,我们的异常错误列表会被大量的重复上报占满,
对于错误类型进行统计,后续的异常分配操作都无法正常进行。
在这种情况下,我们需要对堆栈进行分组和聚合。也就是,将具有相同特征的错误上报,归类为统一种异常,并且只对用户暴露这种聚合后的异常。
堆栈怎么聚合效果才好呢?我们首先可以观察我们的JS异常所携带的信息,一个异常通常包括以下部分
name: 异常的 Type,例如 TypeError, SyntaxError, DOMError
Message:异常的相关信息,通常是异常原因,例如
a.b is not defined.
Stack (非标准)异常的上下文堆栈信息,通常为字符串
那么聚合的方案自然就出来了,利用某种方式,将 error 相关的信息利用提取为 fingerprint,每一次上报如果能够获得相同的 fingerprint,它们就可以归为一类。那么问题进一步细化为:如何利用 Error 来保证 fingerprint 的区分尽量准确呢?
如果跟随标准,我们只能利用 name + message 作为聚合依据。但在实践过程中,我们发现这是远远不够的。如上所示,可以看到这两个文件发生的位置是完全不同的,来自于不同的代码段,但由于我们只按照 name + message 聚合。它们被错误聚合到了一起,这样可能造成我们修复了其中一个错误后。误以为相关的所有异常都被解决。
因此,很明显我们需要利用非标准的 error.stack 的信息来帮我们解决问题了。在这里我们参考了 Sentry 的堆栈聚合策略:
除了常规的 name, message, 我们将反解后的 stacktrace 进一步拆分为一系列的 Frame,每一个 Frame 内我们重点关注其调用函数名,调用文件名以及当前执行的代码行(图中的context_line)。
Sentry 将每一个拆分出的部分都称为一个 GroupingComponent,当堆栈反解完毕后,我们首先自上而下的递归检测,并自下而上的生成一个个嵌套的 GroupingComponent。最后,在顶层调用 GroupingComponent.getHash() 方法, 得到一个最终的哈希值,这就是我们最终求得的 fingerprint。
相较于message+name, 利用 stacktrace 能够更细致的提取堆栈特征,规避了不同文件下触发相同 message 的问题。因此获得的聚合效果也更优秀。这个策略目前在字节内部的工作效果良好,基本上能够做到精确的区分各类异常而不会造成混淆和错误聚合。
处理人自动分配策略
异常已经成功定位后,如果我们可以直接将异常分配给这行代码的书写者或提交者,可以进一步提升问题解决的效率,这就是处理人自动分配所关心的,通常来说,分配处理人依赖 git blame 来实现。
一般的团队或公司都会使用 Gitlab / Github 作为代码的远端仓库。而这些平台都提供了丰富的 open-api 协助用户进行blame,
我们很自然的会联想到,当通过 sourcemap 解出原始堆栈路径后,如果可以结合调用 open-api,获得这段代码所在文件的blame历史, 我们就有机会直接在线上确定某一行的可能的 author / commitor 究竟是谁。从而将这个异常直接分配给他。
思路出来了,那么实际怎么落地呢?
我们需要几个信息
线上报错的项目对应的源代码仓库名,如
toutiao-fe/slardar
线上报错的代码发生的版本,以及与此版本关联的 git commit 信息,为什么需要这些信息呢?
默认用来 blame 的文件都是最新版本,但线上跑的不一定是最新版本的代码。不同版本的代码可能发生行的变动,从而影响实际代码的行号。如果我们无法将线上版本和用来 blame 的文件划分在统一范围内,则很有可能自动定位失败。
因此,我们必须找到一种方法,确定当前 blame 的文件和线上报错的文件处于同一版本。并且可以直接通过版本定位到关联的源代码 commit 起止位置。这样的操作在 Sentry 的官方工具 Sentry-Cli 中亦有提供。字节内部同样使用了这种方案。
通过 相关的 二进制工具,在代码发布前的脚本中提供当前将要发布的项目的版本和关联的代码仓库信息。同时在数据采集侧也携带相同的版本,线上异常发生后,我们就可以通过线上报错的版本找到原始文件对应的版本,从而精确定位到需要哪个时期的文件了。
异常报警
当异常已经成功反解和聚合后,当用户访问监控平台,已经可以观察并处理相关的错误,不过到目前为止,异常的发生还无法触及开发者,问题的解决依然依靠“走查”行为。这样的方案对严重的线上问题依然是不够用,因此我们还需要主动通知用户的手段,这就是异常报警。
在字节内部,报警可以分为宏观报警,即针对错误指标的数量/比率的报警,以及微观报警,即针对新增异常的报警。
宏观报警
宏观报警是数量/比率报警, 它只是统计某一类指标是否超出了限定的阈值,而不关心它具体是什么。因此默认情况下它并不会告诉你报警的原因。只有通过归因维度或者下文会提到的 微观(新增异常)报警 才能够知晓引发报警的具体原因
关于宏观报警,我们有几个关键概念
第一是样本量,用户数阈值: 在配置比率指标时。如果上报量过低,可能会造成比率的严重波动,例如错误率 > 20%, 的报警下,如果 JS 错误数从 0 涨到 1, 那就是比率上涨到无限大从而造成没有意义的误报。如果不希望被少量波动干扰,我们设置了针对错误上报量和用户数的最低阈值,例如只有当错误影响用户数 > 5 时,才针对错误率变化报警。
第二是归因维度: 对于数量,比率报警,仅仅获得一个异常指标值是没什么意义的,因为我们无法快速的定位问题是由什么因素引发的,因此我们提供了归因维度配置。例如,通过对 JS 异常报警配置错误信息归因,我们可以在报警时获得引发当前报警的 top3 关键错误和增长最快的 top3 错误信息。
第三是时间窗口,报警运行频率: 如上文所说,报警是数量,比率报警,而数量,比率一定有一个统计范围,这个就是通过 时间窗口 来确定的。而报警并不是时时刻刻盯着我们的业务数据的,可以理解为利用一个定时器来定期检查 时间窗口 内的数据是否超出了我们定义的阈值。而这个定时器的间隔时间,就是 报警运行频率。通过这种方式,我们可以做到类实时的监测异常数据的变化,但又没有带来过大的资源开销。
微观报警(新增异常)
相较于在意宏观数量变化的报警,新增异常在意每一个具体问题,只要此问题是此前没有出现过的,就会主动通知用户。
同时,宏观报警是针对数据的定时查找,存在运行频率和时间窗口的限制,实时性有限。微观报警是主动推送的,具有更高的实时性。
微观报警适用于发版,灰度等对新问题极其关注,并且不方便在此时专门配置相关数量报警的阶段。
如何判断“新增”?
我们在 异常自动分配章节讲到了,我们的业务代码都是可以关联一个版本概念的。实际上版本不仅和源代码有关,也可以关联到某一类错误上。
在这里我们同样也可以基于版本视角判断“新增错误”。
对于新增异常的判断,针对两种不同场景做了区分
对于指定版本、最新版本的新增异常报警,我们会分析该报警的 fingerprint 是否为该版本代码中首次出现。
而对于全体版本,我们则将"首次”的范围增加了时间限制,因为对于某个错误,如果在长期没有出现后又突然出现,他本身还是具有通知的意义的,如果不进行时间限制,这个错误就不会通知到用户,可能会出现信息遗漏的情况。
如何做好性能监控?
如果说异常处理是前端监控体系60分的分界线,那么性能度量则是监控体系能否达到90分的关键。一个响应迟钝,点哪儿卡哪儿的页面,不会比点开到处都是报错的页面更加吸引人。页面的卡顿可能会直接带来用户访问量的下降,进而影响背后承载的服务收入。因此,监控页面性能并提升页面性能也是非常重要的。针对性能监控,我们主要关注指标选取,品质度量 、瓶颈定位三个关键问题。
指标选取
指标选取依然不是我们今天文章分享的重点。网上关于 RUM 指标,Navigation 指标的介绍和采集方式已经足够清晰。通常分为两个思路:
RUM (真实用户指标) -> 可以通过 Web Vitals (github.com/GoogleChrom…*
页面加载指标 -> NavigationTiming (ResourceTiming + DOM Processing + Load) 可以通过 MDN 相关介绍学习。这里都不多赘述。
瓶颈定位
收集到指标只是问题的第一步,接下来的关键问题便是,我们应该如何找出影响性能问题的根因,并且针对性的进行修复呢?
慢会话 + 性能时序分析
如果你对“数据洞察/可观测性”这个概念有所了解,那么你应该对 Kibana 或 Datadog 这类产品有所耳闻。在 kibana 或 Datadog 中都能够针对每一条上传的日志进行详细的追溯。和详细的上下文进行关联,让用户的体验可被观测,通过多种筛选找到需要用户的数据。
在字节前端的内部建设中,我们参考了这类数据洞察平台的消费思路。设计了数据探索能力。通过数据探索,我们可以针对用户上报的任意维度,对一类日志进行过滤,而不只是获得被聚合过的列表信息数据。这样的消费方式有什么好处呢?
我们可以直接定位到一条具体日志,找到一个现实的 data point 来分析问题
这种视图的状态是易于保存的,我们可以将找到的数据日志通过链接发送给其他人,其他用户可以直接还原现场。
对于性能瓶颈,在数据探索中,可以轻松通过针对某一类 PV 上报所关联的性能指标进行数值筛选。也可以按照某个固定时段进行筛选,从而直接获得响应的慢会话。这样的优势在于我们不用预先设定一个“慢会话阈值”,需要哪个范围的数据完全由我们自己说了算。例如,通过对 FCP > 3000ms 进行筛选,我们就能够获得一系列 FCP > 3s 的 PV 日志现场。
在每次 PV 上报后,我们会为数据采集的 SDK 设置一个全局状态,比如 view_id, 只要没有发生新的页面切换,当前的 view_id 就会保持不变。
而后续的一系列请求,异常,静态资源上报就可以通过 view_id 进行后端的时序串联。形成一张资源加载瀑布图。在瀑布图中我们可以观察到各类性能指标和静态资源加载,网络请求的关系。从而检测出是否是因为某些不必要的或者过大的资源,请求导致的页面性能瓶颈。这样的瀑布图都是一个个真实的用户上报形成的,相较于统计值产生的甘特图,更能帮助我们解决实际问题。
结合Longtask + 用户行为分析
通过指标过滤慢会话,并且结合性能时序瀑布图分析,我们能够判断出当前页面中是否存在由于网络或过大资源因素导致的页面加载迟缓问题
但页面的卡顿不一定全是由网络因素造成的。一个最简单的例子。当我在页面的 head 中插入一段非常耗时的同步脚本(例如 while N 次),则引发页面卡顿的原因就来自于代码执行而非资源加载。
针对这种情况,浏览器同样提供了 Longtask API 供我们收集这类占据主线程时间过长的任务。
同样的,我们将这类信息一并收集,并通过上文提到的 view_id 串联到一次页面访问中。用户就可以观察到某个性能指标是否受到了繁重的主线程加载的影响。若有,则可利用类似 lighthouse 的合成监控方案集中检查对应页面中是否存在相关逻辑了。
受限于浏览器所收集到的信息,目前的 longtask 我们仅仅只能获得它的执行时间相关信息。而无法像开发者面板中的 performance 工具一样准确获取这段逻辑是由那段代码引发的。如果我们能够在一定程度上收集到longtask触发的上下文,则可定位到具体的慢操作来源。
此外,页面的卡顿不一定仅仅发生在页面加载阶段,有时页面的卡顿会来自于页面的一次交互,如点击,滚动等等。这类行为造成的卡顿,仅仅依靠 RUM / navigation 指标是无法定位的。如果我们能够通过某种方式(在PPT中已经说明),对操作行为计时。并将操作计时范围内触发的请求,静态资源和longtask上报以同样的瀑布图方式收敛到一起。则可以进一步定位页面的“慢操作”,从而提升页面交互体验。
如下图所示,我们可以检查到,点击 slardar_web 这个按钮 / 标签带来了一系列的请求和 longtask,如果这次交互带来了一定的交互卡顿。我们便可以集中修复触发这个点击事件所涉及的逻辑来提升页面性能表现。
品质度量
当我们采集到一个性能指标后,针对这样一个数字,我们能做什么?
我们需要结论:好还是不好?
实际上我们通常是以单页面为维度来判定指标的,以整站视角来评判性能的优劣的置信度会受到诸多因素影响,比如一个站点中包含轻量的登陆页和功能丰富的中后台,两者的性能要求和用户的容忍度是不一致的,在实际状况下两者的绝对性能表现也是不一致的。而简单平均只会让我们观察不到重点,页面存在的问题数据也可能被其他的页面拉平。
其次,指标只是冷冰冰的数据,而数据想要发挥作用,一定需要参照系。比如,我仅仅提供 FMP = 4000ms,并不能说明这个页面的性能就一定需要重点关注,对于逻辑较重的PC页面,如数据平台,在线游戏等场景,它可能是符合业务要求的。而一个 FMP = 2000ms的页面则性能也不一定好,对于已经做了 SSR 等优化的回流页。这可能远远达不到我们的预期。
一个放之四海而皆准的指标定义是不现实的。不同的业务场景有不同的性能基准要求。我们可以把他们转化为具体的指标基准线。
通过对于现阶段线上指标的分布,我们可以可以自由定义当前站点场景下针对某个指标,怎样的数据是好的,怎样的数据是差的。
基准线应用后,我们便可以在具体的性能数据产出后,直观的观察到,在什么阶段,某些指标的表现是不佳的,并且可以集中针对这段时间的性能数据日志进行排查。
一个页面总是有多个性能指标的,现在我们已经知道了单个性能指标的优劣情况,如何整体的判断整个页面,乃至整个站点的性能状况,落实到消费侧则是,我们如何给一个页面的性能指标评分?
如果有关注过 lighthouse 的同学应该对这张图不陌生。
lighthouse 通过 google 采集到的大量线上页面的性能数据,针对每一个性能指标,通过对数正态分布将其指标值转化成 百分制分数。再通过给予每个指标一定的权重(随着 lighthouse 版本更迭), 计算出该页面性能模块的一个“整体分数”。在即将上线的“品质度量”能力中,我们针对 RUM 指标,异常指标,以及资源加载指标均采取了类似的方案。
我们通常可以给页面的整体性能分数再制定一个基准分数,当上文所述的性能得分超过分数线,才认为该页面的性能水平是“达标”的。而整站整体的达标水平,则可以利用整站达标的子页面数/全站页面数来计算,也就是达标率,通过达标率,我们可以非常直观的迅速找到需要优化的性能页面,让不熟悉相关技术的运营,产品同学也可以定期巡检相关页面的品质状况。
如何做好请求 / 静态资源监控?
除了 JS 异常和页面的性能表现以外,页面能否正常的响应用户的操作,信息能否正确的展示,也和 api 请求,静态资源息息相关。表现为 SLA,接口响应速度等指标。现在主流的监控方案通常是采用手动 hook相关 api 和利用 resource timing 来采集相关信息的。
手动打点通常用于请求耗时兜底以及记录请求状态和请求响应相关信息。
对于 XHR 请求: 通过 hook XHR 的 open 和 send 方法, 获取请求的参数,在 onreadystatechange 事件触发时打点记录请求耗时。
// 记录 method
hookObjectProperty(XMLHttpRequest.prototype, 'open', hookXHROpen);
// hook onreadystateChange,调用前后打点计算
hookObjectProperty(XMLHttpRequest.prototype, 'send', hookXHRSend);
复制代码
对于fetch请求,则通过 hook Fetch 实现
hookObjectProperty(global, 'fetch', hookFetch)
复制代码
第二种则是 resourceTiming 采集方案
静态资源上报:
pageLoad 前:通过 performance.getEntriesByType 获取 resource 信息
pageLoad后:通过 PerformanceObserver 监控 entryType 为 resource 的资源
const callback = (val, i, arr, ob) => // ... 略
const observer = new PerformanceObserver((list, ob) => {
if (list.getEntries) {
list.getEntries().forEach((val, i, arr) => callback(val, i, arr, ob))
} else {
onFail && onFail()
}
// ...
});
observer.observe({ type: 'resource', buffered: false })
复制代码
手动打点的优势在于无关兼容性,采集方便,而 Resource timing 则更精准,并且其记录中可以避开额外的事件队列处理耗时
如何理解和使用 resource timing 数据?
我们现在知道 ResourceTiming 是更能够反映实际资源加载状况的相关指标,而在工作中,我们经常遇到前端请求上报时间极长而后端对应接口日志却表现正常的情况。这通常就可能是由使用单纯的打点方案计算了太多非服务端因素导致的。影响一个请求在前端表现的因素除了服务端耗时以外,还包括网络,前端代码执行排队等因素。我们如何从 ResourceTiming 中分离出这些因素,从而更好的对齐后端口径呢?
第一种是 Chrome 方案(阿里的 ARMS 也采用的是这种方案):
它通过将线上采集的 ResoruceTiming 和 chrome timing 面板的指标进行类比还原出一个近似的各部分耗时值。他的简单计算方式如图所示。
不过 chrome 实际计算 timing 的方式不明,这种近似的方式不一定能够和 chrome 的面板数据对的上,可能会被用户质疑数据不一致。
第二种则是标准方案: 规范划分阶段,这种划分是符合 W3C 规范的格式,其优势便在于其通用性好,且数据一定是符合要求的而不是 chrome 方案那种“近似计算”。不过它的缺陷是阶段划分还是有点太粗了,比如用户无法判断出浏览器排队耗时,也无法完全区分网络下载和下载完成后的资源加载阶段。只是简单的划分成了 Request / Response 阶段,给用户理解和分析带来了一定成本
在字节内部,我们是以标准方案为主,chrome方案为辅的,用户可以针对自己喜好的那种统计方式来对齐指标。通常来说,和服务端对齐耗时阶段可以利用标准方案的request阶段减去severtiming中的cdn,网关部分耗时来确定。
接下来我们再谈谈采集 SDK 的设计。
SDK 如何降低侵入,减少用户性能损耗?体积控制和灵活使用可以兼得吗?
常需要尽早执行,其资源加载通常也会造成一定的性能影响。更大的资源加载可能会导致更慢的 Load,LCP,TTI 时间,影响用户体验。
为了进一步优化页面加载性能,我们采用了 JS Snippets 来实现异步加载 + 预收集。
异步加载主要逻辑
首先,如果通过 JS 代码创建 script 脚本并追加到页面中,新增的 script 脚本默认会携带 async 属性,这意味着这这部分代码将通过async方式延迟加载。下载阶段不会阻塞用户的页面加载逻辑。从而一定程度的提升用户的首屏性能表现。
预收集
试想一下我们通过 npm 或者 cdn 的方式直接引入监控代码,script必须置于业务逻辑最前端,这是因为若异常先于监控代码加载发生,当监控代码就位时,是没有办法捕获到历史上曾经发生过的异常的。但将script置于前端将不可避免的对用户页面造成一定阻塞,且用户的页面可能会因此受到我们监控 sdk 服务可用性的影响。
为了解决这个问题,我们可以同步的加载一段精简的代码,在其中启动 addEventListener 来采集先于监控主要逻辑发生的错误。并存储到一个全局队列中,这样,当监控代码就位,我们只需要读取全局队列中的缓存数据并上报,就不会出现漏报的情况了。
更进一步:事件驱动与插件化
方案1. 2在大部分情况下都已经比较够用了,但对于字节的某些特殊场景却还不够。由于字节存在大量的移动端页面,且这些页面对性能极为敏感。因而对于第三方库的首包体积要求非常苛刻,同时,也不希望第三方代码的执行占据主线程太长时间。
此外,公司内也有部分业务场景特殊,如 node 场景,小程序场景,electron,如果针对每一种场景,都完全重新开发一套新的监控 SDK,有很大的人力重复开发的损耗。
如果我们能够将 SDK 的框架逻辑做成平台无关的,而各个数据监控,收集方案都只是以插件形式存在,那么这个 SDK 完全是可插拔的,类似 Sentry 所使用的 integration 方案。用户甚至可以完全不使用任何官方插件,而是通过自己实现相关采集方案,来做到项目的定制化。
关于框架设计可以参见下图
我们把整个监控 SDK 看作一条流水线(Client),接受的是用户配置(config)(通过 ConfigManager),收集和产出的是具体事件(Event, 通过 Plugins)。流水线是平台无关的,它不关心处理的事件是什么,也不关心事件是从哪来的。它其实是将这一系列的组件交互都抽象为 client 上的事件,从而使得数据采集器能够介入数据流转的每个阶段
Client 通过 builder 包装事件后,转运给 Sender 负责批处理,Sender 最终调用 Transporter 上报。Transporter 是平台强相关的,例如 Web 使用 xhr 或 fetch,node 则使用 request 等。 同时,我们利用生命周期的概念设置了一系列的钩子,可以让用户可以在适当阶段处理流水线上的事件。例如利用 beforeSend 钩子去修改即将被发送的上报内容等。
当整体的框架结构设计完后,我们就可以把视角放到插件上了。由于我们将框架设置为平台无关的,它本身只是个数据流,有点像是一个精简版的 Rx.js。而应用在各个平台上,我们只需要根据各个平台的特性设计其对应的采集或数据处理插件。
插件方案某种意义上实现了 IOC,用户不需要关心事件怎么处理,传入的参数是哪里来的,只需要利用传入的参数去获取配置,启动自己的插件等。如下这段JS采集器代码,开发插件时,我们只需要关心插件自身相关的逻辑,并且利用传入 client 约定的相关属性和方法工作就可以了。不需要关心 client 是怎么来的,也不用关心 client 什么时候去执行它。
当我们写完了插件之后,它要怎么才能被应用在数据采集和处理中呢?为了达成降低首包大小的目标,我们将插件分为同步和异步两种加载方式。
可以预收集的监控代码都不需要出现在首包中,以异步插件方式接入
无法做到预收集的监控代码以同步形式和首包打在一起,在源码中将client传入,尽早启动,保证功能稳定。
3. 异步插件采用约定式加载,用户在使用层面是完全无感的。我们通过主包加载时向在全局初始化注册表和注册方法,在读取用户配置后,拉取远端插件加载并利用全局注册方法获取插件实例,最后传入我们的 client 实现代码执行。
经过插件化和一系列 SDK 的体积改造后,我们的sdk 首包体积降低到了从63kb 降低到了 34 kb。
总结
本文主要从 JS 异常监控,性能监控和请求,静态资源监控几个细节点讲述了 Slardar 在前端监控方向所面临关键问题的探索和实践,希望能够对大家在前端监控领域或者将来的工作中产生帮助。其实前端监控还有许多方面可以深挖,例如如何利用拨测,线下实验室数据采集来进一步追溯问题,如何捕获白屏等类崩溃异常,如何结合研发流程来实现用户无感知的接入等等。
作者:字节架构前端
来源:https://juejin.cn/post/7195496297150709821
一个炫酷的头像悬停效果
本文翻译自 A Fancy Hover Effect For Your Avatar,略有删改,有兴趣可以看看原文。
你知道当一个人的头像从一个圆圈或洞里伸出来时的那种效果吗?本文将使用一种很简洁的方式实现该悬停效果,可以用在你的头像交互上面。
看到了吗?我们将制作一个缩放动画,其中头像部分似乎从它所在的圆圈中钻出来了。是不是很酷呢?接下来让我们一起一步一步地构建这个动画交互效果。
HTML:只需要一个元素
是的,只需要一个img
图片标签即可,本次练习的挑战性部分是使用尽可能少的代码。如果你已经关注我一段时间了,你应该习惯了。我努力寻找能够用最小、最易维护的代码实现的CSS解决方案。
<img src="" alt="">
首先我们需要一个带有透明背景的正方形图像文件,以下是本次案例使用的图像。
在开始CSS之前,让我们先分析一下效果。悬停时图像会变大,所以我们肯定会在这里使用transform:scale
。头像后面有一个圆圈,径向渐变应该可以达到这个效果。最后我们需要一种在圆圈底部创建边框的方法,该边框将不受整体放大的影响且是在视觉顶层。
放大效果
放大的效果,增加transform:scale
,这个比较简单。
img:hover {
transform: scale(1.35);
}
上面说过背景是一个径向渐变。我们创建一个径向渐变,但是两个颜色之间不要有过渡效果,这样使得它看起来像我们画了一个有实线边框的圆。
img {
--b: 5px; /* border width */
background:
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
#C02942 calc(100% - var(--b)) 99%,
#0000
);
}
注意CSS变量,--b
,在这里它表示“边框”的宽度,实际上只是用于定义径向渐变红色部分的位置。
下一步是在悬停时调整渐变大小,随着图像的放大,圆需要保持大小不变。由于我们正在应用scale
变换,因此实际上需要减小圆圈的大小,否则它会随着化身的大小而增大。
让我们首先定义一个CSS变量--f
,它定义了“比例因子”,并使用它来设置圆的大小。我使用1作为默认值,因为这是图像和圆的初始比例,我们从圆转换。
现在我们必须将背景定位在圆的中心,并确保它占据整个高度。我喜欢把所有东西都直接简写在 background 属性,代码如下:
background: radial-gradient() 50% / calc(100% / var(--f)) 100% no-repeat;
背景放置在中心( 50%),宽度等于calc(100%/var(--f)),高度等于100%。
当 --f 等于 1 时是我们最初的比例。同时,渐变占据容器的整个宽度。当我们增加 --f,元素的大小会增长但是渐变的大小将减小。
越来越接近了!我们在顶部添加了溢出效果,但我们仍然需要隐藏图像的底部,这样它看起来就像是跳出了圆圈,而不是整体浮在圆圈前面。这是整个过程中比较复杂的部分,也是我们接下来要做的。
下边框
第一次尝试使用border-bottom
属性,但无法找到一种方法来匹配边框的大小与圆的大小。如图所示,相信你能看出来无法实现我们想要的效果:
实际的解决方案是使用outline
属性。不是border
。outline
可以让我们创造出很酷的悬停效果。结合 outline-offset
偏移量,我们就可以实现所需要的效果。
其核心是在图像上设置一个outline
轮廓并调整其偏移量以创建下边框。偏移量将取决于比例因子,与渐变大小相同。outline-offset
偏移量看起来相对比较复杂,这里对计算方式进行了精简,有兴趣的可以看看原文。
img {
--s: 280px; /* image size */
--b: 5px; /* border thickness */
--c: #C02942; /* border color */
--f: 1; /* initial scale */
border-radius: 0 0 999px 999px;
outline: var(--b) solid var(--c);
outline-offset: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
}
因为我们需要一个圆形的底部边框,所以在底部添加了一个边框圆角,使轮廓与渐变的弯曲程度相匹配。
现在我们需要找到如何从轮廓中删除顶部,也就是上图中挡住头像的那根线。换句话说,我们只需要图像的底部轮廓。首先,在顶部添加空白和填充,以帮助避免顶部头像的重叠,这通过增加padding
即可实现:
padding-top: calc(var(--s)/5)
这里还有一个注意点,需要添加 content-box
值添加到 background
:
background:
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
var(--c) calc(100% - var(--b)) 99%,
#0000
) 50%/calc(100%/var(--f)) 100% no-repeat content-box;
这样做是因为我们添加了padding填充,并且我们只希望将背景设置为内容框,因此我们必须显式地定义出来。
CSS mask
到了最后一部分!我们要做的就是藏起一些碎片。为此,我们将依赖于 CSS mask 属性,当然还有渐变。
下面的图说明了我们需要隐藏的内容或需要显示的内容,以便更加准确。左图是我们目前拥有的,右图是我们想要的。绿色部分说明了我们必须应用于原始图像以获得最终结果的遮罩内容。
我们可以识别mask的两个部分:
底部的圆形部分,与我们用来创建化身后面的圆的径向渐变具有相同的维度和曲率
顶部的矩形,覆盖轮廓内部的区域。请注意轮廓是如何位于顶部的绿色区域之外的-这是最重要的部分,因为它允许剪切轮廓,以便只有底部部分可见
最终的完整css如下,对有重复的代码进行抽离,如--g,--o:
img {
--s: 280px; /* image size */
--b: 5px; /* border thickness */
--c: #C02942; /* border color */
--f: 1; /* initial scale */
--_g: 50% / calc(100% / var(--f)) 100% no-repeat content-box;
--_o: calc((1 / var(--f) - 1) * var(--s) / 2 - var(--b));
width: var(--s);
aspect-ratio: 1;
padding-top: calc(var(--s)/5);
cursor: pointer;
border-radius: 0 0 999px 999px;
outline: var(--b) solid var(--c);
outline-offset: var(--_o);
background:
radial-gradient(
circle closest-side,
#ECD078 calc(99% - var(--b)),
var(--c) calc(100% - var(--b)) 99%,
#0000) var(--_g);
mask:
linear-gradient(#000 0 0) no-repeat
50% calc(-1 * var(--_o)) / calc(100% / var(--f) - 2 * var(--b)) 50%,
radial-gradient(
circle closest-side,
#000 99%,
#0000) var(--_g);
transform: scale(var(--f));
transition: .5s;
}
img:hover {
--f: 1.35; /* hover scale */
}
下面的一个演示,直观的说明mask的使用区域。中间的框说明了由两个渐变组成的遮罩层。把它想象成左边图像的可见部分,你就会得到右边的最终结果:
最后
搞定!我们不仅完成了一个流畅的悬停动画,而且只用了一个<img>
元素和不到20行的CSS技巧!如果我们允许自己使用更多的HTML,我们能简化CSS吗?当然可以。但我们是来学习CSS新技巧的!这是一个很好的练习,可以探索CSS渐变、遮罩、outline属性的行为、转换以及其他许多内容。
在线效果
实例里面是流行的CSS开发人员的照片。有兴趣的同学可以展示一下自己的头像效果。
看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~
作者:南城FE
来源:juejin.cn/post/7196747356796518460
React和Vue谁会淘汰谁?
在我的技术群里大家经常会聊一些宏观的技术问题,就比如:
Vue和React,最终谁会被淘汰?
这样的讨论,到最后往往会陷入技术的细枝末节的比较,比如:
对比两者响应式的实现原理
对比两者的运行时性能
很多程序员朋友,会觉得:
技术问题,就应该从技术的角度找到答案
但实际上,一些大家纠结的技术问题,往往跟技术本身无关。
谁才是框架的最终赢家?
讨论React和Vue谁会淘汰谁?这个问题,就像10年前,一个康师傅信徒和一个统一信徒争论:
哪家泡面企业最终会被淘汰呢?
他们把争论的重点放在口味的对比、面饼分量的对比等等,最终谁也无法说服谁。
实际我们最后知道了,外卖App的崛起,对泡面企业形成了降维打击。
回到框架这个问题上,在前端框架流行之前,前端最流行的开发库是jQuery
,他是命令式编程的编程范式。
取代jQuery
的并不是另一个更优秀的jQuery
,而是声明式编程的前端框架。
同样的,取代前端框架的,不会是另一个更优秀的前端框架,而是另一种更适合web
开发的编程范式。
那在前端框架这个领域内部,React
和Vue
最终谁会淘汰谁呢?
我的答案是:
谁也不会淘汰谁。
任何框架最核心的竞争力,不是性能,也不是生态是否繁荣,而是开发者用的是否顺手,也就是开发模式是否合理。
React
发明了JSX
这种开发模式,并持续教育了开发者3年,才让社区接受这种开发模式
这种发明开发模式,再教育开发者的行为,也只有meta
这种大公司才办得到。
而Vue
则直接使用了模版语法这种现成的开发模式。这种模式已经被广大后端工程师验证过是最好上手的web
开发模式。
所以像后端工程师或者编程新人会很容易上手Vue
。
经过多年迭代,他们各自的开发模式已经变成了事实上的前端框架DSL
标准。
这会为他们带来两个好处:
开发模式是个主观偏好,不存在优劣
所以他们谁也无法淘汰谁,只能说React
的开发模式受众范围更广而已。
后来者会永远居于他们的阴影之下
新的框架如果无法在编程范式上突破,那么为了抢占Vue
或React
的市场份额,只能遵循他们的开发模式,因为这样开发者才能无痛迁移。
比如最近两年比较优秀的新框架,svelte
是Vue
的开发模式,Solid.js
是React
的开发模式
在同样的开发模式下,占市场主导地位的框架可以迅速跟进那些竞争者的优秀特性。
比如Vue
就准备开发一个类似Svelte
的版本。
一句话总结就是:
你是无法在我的BGM中击败我的
总结
总体来说,在新的web
编程范式流行之前,React
、Vue
还会长期霸占开发者喜欢的前端框架前列。
在此过程中,会出现各种新框架,他们各有各的特点,但是,都很难撼动前者的地位。
作者:魔术师卡颂
来源:juejin.cn/post/7190550643386351653
记一次浏览器播放实时监控rtsp视频流的解决历程(利用Ffmpeg + node.js + websocket + flv.js实现)
背景
笔者目前在做一个智慧楼宇的产品(使用react开发的),在交付项目的时候,遇到了需要在浏览器端播放rtsp视频流的场景,而浏览器端是不能直接播放rtsp视频流的,原本打算让客户方提供flv格式的视频流 鉴于项目现场的环境以及种种原因,客户方只能提供rtsp视频流,所以就只能自己解决了。
于是乎,去网上随便一搜就搜到了Ffmpeg + node.js + websocket + flv.js的解决方案,但是真正自己实现下来,遇到了几个棘手的问题,例如:莫名其妙的报错,部分监控视频转换失败,不能转码h265格式的视频等等(本文会介绍自己遇到的问题以及解决方案)。
涉及到的技术点
ffmpeg:ffmpeg是一个转码工具,将rtsp视频里转换成flv格式的视频流
node.js
websocket
flv.js
node.js端
用到的关键库
@ffmpeg-installer/ffmpeg
自动为当前node服务所在的平台安装适合的ffmpeg,无需自己再去手动下载、安装配置了。通过该库安装的ffmpeg,其路径在node_modules/@ffmpeg-installer/darwin-x64/ffmpeg (我用的是mac,自动安装的是darwin-x64,不同平台不一样)
fluent-ffmpeg
该库是对ffmpeg 命令的封装,简化了命令的使用流程,原生ffmpeg的命令是比较复杂难懂的。
完整可复制直接运行的node代码
const ffmpegPath = require('@ffmpeg-installer/ffmpeg'); // 自动为当前node服务所在的系统安装ffmpeg
const ffmpeg = require('fluent-ffmpeg');
const express = require('express');
const webSocketStream = require('websocket-stream/stream');
const expressWebSocket = require('express-ws');
ffmpeg.setFfmpegPath(ffmpegPath.path);
/**
* 创建一个后端服务
*/
function createServer() {
const app = express();
app.use(express.static(__dirname));
expressWebSocket(app, null, {
perMessageDeflate: true
});
app.ws('/rtsp/', rtspToFlvHandle);
app.get('/', (req, response) => {
response.send('当你看到这个页面的时候说明rtsp流媒体服务正常启动中......');
});
app.listen(8100, () => {
console.log('转换rtsp流媒体服务启动了,服务端口号为8100');
});
}
/**
* rtsp 转换 flv 的处理函数
* @param ws
* @param req
*/
function rtspToFlvHandle(ws, req) {
const stream = webSocketStream(ws, {
binary: true,
browserBufferTimeout: 1000000
}, {
browserBufferTimeout: 1000000
});
// const url = req.query.url;
const url = new Buffer(req.query.url, 'base64').toString(); // 前端对rtsp url进行了base64编码,此处进行解码
console.log('rtsp url:', url);
try {
ffmpeg(url)
.addInputOption(
'-rtsp_transport', 'tcp',
'-buffer_size', '102400'
)
.on('start', (commandLine) => {
// commandLine 是完整的ffmpeg命令
console.log(commandLine, '转码 开始');
})
.on('codecData', function (data) {
console.log(data, '转码中......');
})
.on('progress', function (progress) {
// console.log(progress,'转码进度')
})
.on('error', function (err, a, b) {
console.log(url, '转码 错误: ', err.message);
console.log('输入错误', a);
console.log('输出错误', b);
})
.on('end', function () {
console.log(url, '转码 结束!');
})
.addOutputOption(
'-threads', '4', // 一些降低延迟的配置参数
'-tune', 'zerolatency',
'-preset', 'ultrafast'
)
.outputFormat('flv') // 转换为flv格式
.videoCodec('libx264') // ffmpeg无法直接将h265转换为flv的,故需要先将h265转换为h264,然后再转换为flv
.withSize('50%') // 转换之后的视频分辨率原来的50%, 如果转换出来的视频仍然延迟高,可按照文档上面的描述,自行降低分辨率
.noAudio() // 去除声音
.pipe(stream);
} catch (error) {
console.log('抛出异常', error);
}
}
createServer();
react 前端
用到的关键库
flv.js
用于前端播放flv格式视频库
完整可直接复制使用的react组件
import React, { useEffect, useRef } from 'react';
import './FlvVideoPlayer.scss';
import flvjs from 'flv.js';
import { Button } from '@alifd/next';
interface FlvVideoPlayerProps {
url?: string; // rtsp 的url
isNeedControl?: boolean;
fullScreenRef?: any; // 方便组件外部调用全屏方法的ref
}
const FlvVideoPlayer = React.forwardRef<any, FlvVideoPlayerProps>(({ isNeedControl, url, fullScreenRef }, ref) => {
const videoDomRef = useRef<any>();
const playerRef = useRef<any>(); // 储存player的实例
React.useImperativeHandle(ref, () => ({
requestFullscreen,
}));
useEffect(() => {
if (videoDomRef.current) {
if (fullScreenRef) {
fullScreenRef.current[url] = requestFullscreen;
}
// const url = `${videoUrl}/rtsp/video1/?url=${url}`;
playerRef.current = flvjs.createPlayer({
type: 'flv',
isLive: true,
url,
});
playerRef.current.attachMediaElement(videoDomRef.current);
try {
playerRef.current.load();
playerRef.current.play();
} catch (error) {
console.log(error);
}
}
return () => {
destroy();
};
}, [url]);
/**
* 全屏方法
*/
const requestFullscreen = () => {
if (videoDomRef.current) {
(videoDomRef.current.requestFullscreen && videoDomRef.current.requestFullscreen()) ||
(videoDomRef.current.webkitRequestFullScreen && videoDomRef.current.webkitRequestFullScreen()) ||
(videoDomRef.current.mozRequestFullScreen && videoDomRef.current.mozRequestFullScreen()) ||
(videoDomRef.current.msRequestFullscreen && videoDomRef.current.msRequestFullscreen());
}
};
/**
* 销毁flv的实例
*/
const destroy = () => {
if (playerRef.current) {
playerRef.current.pause();
playerRef.current.unload();
playerRef.current.detachMediaElement();
playerRef.current.destroy();
playerRef.current = null;
}
};
return (
<>
<Button type="primary" onClick={requestFullscreen}>
全屏按钮
</Button>
<video controls={isNeedControl} ref={videoDomRef} className="FlvVideoPlayer" loop />
</>
);
});
export default FlvVideoPlayer;
组件用到的url
本地开发时
本地开发时,node服务是启动在自己电脑上,所以node服务的地址就是 ws://127.0.0.1:8100
,为了防止在传rtsp地址的过程中出现参数丢失的情况,故采用window.btoa()
方法对rtsp进行base64编码一下,又由于node端代码中监听的是/rtsp/
,故完整的组件的url是
ws://127.0.0.1:8100/rtsp?url=${window.btoa('rtsp地址')}
部署线上
直接将服务器ip替换掉127.0.0.1
即可
提供一个测试的rtsp地址
rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mp4
遇到的问题
1. rtsp地址中存在?拼接的参数,传到后端丢失
错误详情
An error occured: ffmpeg exited with code 1: rtsp://... Server returned 404 Not Found
原因
完整的url是ws://127.0.0.1:8100/rtsp?url=${window.btoa('rtsp地址')}
,如果rtsp地址中再含有?
拼接参数的话,那么就会出现两个?
,传到node端之后,会被express去除掉rtsp地址中的?
解决方式
在前端对rtsp使用window.btoa
方法进行base64编码,在node端使用new Buffer
进行解码即可
2. 连接超时
报错截图
原因
部署到客户内网发现的,是两台服务网络不通造成的
解决方式
找运维解决
3. CPU飚到100%,卡顿
错误详情
监控视频采用分页显示,每页8个监控视频,切换到下一页的时候,上一页转换监控视频的ffmpeg进程,仍然存在,没有被kill掉。所以ffmpeg的进程不停地增加,导致CPU占用100%
原因
封装flvjs 的react组件中,在组件卸载的时候,没有把flvjs的实例销毁掉,导致进程不会被自动kill掉
解决方式
组件卸载的时候,将flvjs的实例销毁掉
4. 不能转码h265视频流
错误详情
Video codec hevc not compatible with flv 。Could not write header for output file #0 (incorrect codec parameters ?): Function not implemented
原因
有些监控摄像头的视频格式是 hevc h265, flv不支持,需要先将h265转化至h264格式
解决方式
node端代码中。ffmpeg添加 videoCodec('libx264')
配置即可
优化 ffmpeg 低延迟配置参数
'-threads', '4'
'-tune', 'zerolatency'
'-preset', 'ultrafast'
更新
当我把ffmpeg配置参数中的输出分辨率配置移除后,目前的延时在1~2s左右
作者:huisiyu
来源:juejin.cn/post/7124188097617051685
vue-video-player 播放m3u8视频流
该问题网上答案较少,翻阅github得到想要的答案,在此记录一下
首先,为了减少包体积,在组件中局部引入vue-video-player
(在main.j s中引入会增加包体积)
播放m3u8需要注意两点:
需要引入videojs并绑定到window上
安装依赖videojs-contrib-hls(
npm i videojs-contrib-hls
)并引入sources要指定type为
application/x-mpegURL
代码如下:
<template>
<section>
<video-player :options="options"></video-player>
</section>
</template>
<script>
import { videoPlayer } from 'vue-video-player'
import videojs from 'video.js'
//注意点1:需要引入videojs并绑定到window上
window.videojs = videojs
//注意点2:引入依赖
require('videojs-contrib-hls/dist/videojs-contrib-hls.js')
require('video.js/dist/video-js.css')
require('vue-video-player/src/custom-theme.css')
export default {
name: 'test-video-player',
components: {
videoPlayer
},
data() {
return {
options: {
autoplay: false,
height: '720',
playbackRates: [0.7, 1.0, 1.25, 1.5, 2.0],
sources: [
{
withCredentials: false,
type: 'application/x-mpegURL', //注意点3:这里的type需要指定为 'application/x-mpegURL'
src:
'https://tx-safety-video.acfun.cn/mediacloud/acfun/acfun_video/47252fc26243b079-e992c6c3928c6be2dcb2426c2743ceca-hls_720p_2.m3u8?pkey=ABDuFNTOUnsfYOEZC286rORZhpfh5uaNeFhzffUnwTFoS8-3NBSQEvWcqdKGtIRMgiywklkZvPdU-2avzKUT-I738UJX6urdwxy_ZHp617win7G6ga30Lfvfp2AyAVoUMjhVkiCnKeObrMEPVn4x749wFaigz-mPaWPGAf5uVvR0kbkVIw6x-HZTlgyY6tj-eE_rVnxHvB1XJ01_JhXMVWh70zlJ89EL2wsdPfhrgeLCWQ&safety_id=AAKir561j0mZgTqDfijAYjR6'
}
],
hls: true
}
}
},
computed: {},
methods: {},
created() {}
}
</script>
<style lang="" scoped></style>
参考
作者:我只是一个API调用工程师
来源:juejin.cn/post/7080748744592850951
项目没发版却出现了bug,原来是chrome春节前下毒
前言
农历: 腊月二十五
阳历: 2023-01-16
过年和年兽
已经临近过年,公司的迭代版本也已经封版,大家都在一片祥和又掺杂焦虑的气氛中等待春节的到来。 当然,等待的人群里面也有我,吼吼哈嘿。
突然企业微信的一声响,我习惯性的抬头瞅了一眼屏幕,嗯? 来至线上bug群?。
不过因为最近咱前端项目也没有发版,心里多少有点底气的。
于是怀着好奇的心情点开了群消息, 准备看看是什么情况。
结果进群看到是某前端页面元素拖拽功能的位置失效了。晴天霹雳啊,我们有一个类似给运营做自定义活动页面,说是无法拖拽了。然后需要做活动比较紧急,需要尽快修复。
这活脱脱就是跟着春节来的年兽啊。我还没放烟花打年兽,年兽就先朝我冲过来了,那说什么也得较量较量了。
项目背景
我们这个功能是属于一个基础功能,通过npm私有仓库维护版本
这个基础功能呢,很多项目中都在使用。
如果基础功能发了新版本,业务部门不进行升级安装,那么这个业务线的项目也是不会出问题的。所以只要线上出了问题,那么要满足两个条件
1、基础功能进行了发布了npm新版本,且这个版本有问题,
2、业务部门进行了升级,使用了这个新版本
排查问题
一般来说:造成问题的可能性有
有人发过新迭代版本
是不是存在莫名的缓存
有人在以前的版本里面下毒了,然后现在发作了(可能性不大)
经过粗略排查
猜测 | 结果 |
---|---|
1、发版导致? | 近期两周,该服务部分未更新,排除 |
2、缓存导致 | 已经清理,没用,排除 |
3、下毒了 | 看了相关代码,没什么问题,排除 |
问题初见端倪
接着发生了两件事情
1、然后我本地跑了一下项目的时候,在操作的时候,存在报错。
2、一个测试兄弟反馈说他那儿可以正常操作
这他么莫不是浏览器兼容问题了吧。
我去他那看了一下,都是chrome浏览器(这个项目我们只支持到chrome就可以)
这时的我感觉可能问题有点大了,莫不是chrome又调整了吧
点开测试兄弟的版本看了下,是108,而且处于重启就会升级的状态。 我赶紧回到我的工位,打开电脑发现是109。
在看了下那个报错, event.path为undefined, 这里先介绍下path是个什么玩意,他是一个数组,里面记录着从当前节点冒泡到顶层window的所有node节点。我们借助这个功能做了一写事情。。。
这直接被chrome釜底抽薪了。(path属于非标准api, 这些非标准api慎用,说不定什么时候就嘎了)
解决问题
1、问题一
既然是event.path没了,那么我们怎么办呢,首先得找到代替path的方法, 上面我们也说了,path里面记录的是从当前节点冒泡到顶层window的所有node节点(我们是拖拽事件)
那么我们可以自己遍历一下当前节点+他的父节点+父节点的父节点+...+window
let path = [];
let target = event.target;
while(target.parentNode !== null){
path.push(target);
target = target.parentNode;
}
path.push(document, window);
return path;
在项目里面试了一下,emm,很稳定。
1、问题二
但是我们又遇到了第二个问题,使用到event.path的项目还比较多,这就日了狗了 如果没有更好的方法,那么我只能挨个项目改,然后测试,然后逐个项目发版
这种原始的方法我们肯定是不会采用的,换个思路,既然event下的path被删除了,那么我们在event对象下追加个一个path属性就可以了
当然我们要记得判断下path属性是否存在,因为有部分用户的chrome是老版本的,我们只对升级后的版本做一些兼容就可以了
if (!Event.prototype.hasOwnProperty("path")){
Object.defineProperties(Event.prototype, {
path: {
get: function(){
var target = this.target;
console.log('target', target)
var path = [];
while(target.parentNode !== null){
path.push(target);
target = target.parentNode;
}
path.push(document, window);
return path;
}
},
composedPath: {
value: function(){
return this.path;
},
writable: true
}
});
}
这样,我们只需要在每个项目的根html,通过script引入这个js文件就可以了
反思
如题,这个事情怪chrome吗?其实不能怪的。 1、chrome在之前就已经给出了更新通知,只是我们没有去关注这个事情 2、本身event.path不是标准属性,我们却使用了(其实其他浏览器是没有这个属性的,只是chrome提供了path属性, 虽然现在他删除了) 3、总之还是自己不够警惕,同时使用了不标准的属性,以此为戒,共勉
作者:大鱼敢瞪猫
来源:juejin.cn/post/7193520080808837180
感受Vue3的魔法力量
近半年有幸参与了一个创新项目,由于没有任何历史包袱,所以选择了Vue3技术栈,总体来说感受如下:
• setup语法糖
• 可以通过Composition API(组合式API)封装可复用逻辑,将UI和逻辑分离,提高复用性,view层代码展示更清晰
• 和Vue3更搭配的状态管理库Pinia,少去了很多配置,使用起来更便捷
• 构建工具Vite,基于ESM和Rollup,省去本地开发时的编译步骤,但是build打包时还是会编译(考虑到兼容性)
• 必备VSCode插件Volar,支持Vue3内置API的TS类型推断,但是不兼容Vue2,如果需要在Vue2和Vue3项目中切换,比较麻烦
当然也遇到一些问题,最典型的就是响应式相关的问题
响应式篇
本篇主要借助watch函数,理解ref、reactive等响应式数据/状态,有兴趣的同学可以查看Vue3源代码部分加深理解,
watch数据源可以是ref (包括计算属性)、响应式对象、getter 函数、或多个数据源组成的数组
import { ref, reactive, watch, nextTick } from 'vue'
//定义4种响应式数据/状态
//1、ref值为基本类型
const simplePerson = ref('张三')
//2、ref值为引用类型,等价于:person.value = reactive({ name: '张三' })
const person = ref({
name: '张三'
})
//3、ref值包含嵌套的引用类型,等价于:complexPerson.value = reactive({ name: '张三', info: { age: 18 } })
const complexPerson = ref({ name: '张三', info: { age: 18 } })
//4、reactive
const reactivePerson = reactive({ name: '张三', info: { age: 18 } })
//改变属性,观察以下不同情景下的监听结果
nextTick(() => {
simplePerson.value = '李四'
person.value.name = '李四'
complexPerson.value.info.age = 20
reactivePerson.info.age = 22
})
//情景一:数据源为RefImpl
watch(simplePerson, (newVal) => {
console.log(newVal) //输出:李四
})
//情景二:数据源为'张三'
watch(simplePerson.value, (newVal) => {
console.log(newVal) //非法数据源,监听不到且控制台告警
})
//情景三:数据源为RefImpl,但是.value才是响应式对象,所以要加deep
watch(person, (newVal) => {
console.log(newVal) //输出:{name: '李四'}
},{
deep: true //必须设置,否则监听不到内部变化
})
//情景四:数据源为响应式对象
watch(person.value, (newVal) => {
console.log(newVal) //输出:{name: '李四'}
})
//情景五:数据源为'张三'
watch(person.value.name, (newVal) => {
console.log(newVal) //非法数据源,监听不到且控制台告警
})
//情景六:数据源为getter函数,返回基本类型
watch(
() => person.value.name,
(newVal) => {
console.log(newVal) //输出:李四
}
)
//情景七:数据源为响应式对象(在Vue3中状态都是默认深层响应式的)
watch(complexPerson.value.info, (newVal, oldVal) => {
console.log(newVal) //输出:Proxy {age: 20}
console.log(newVal === oldVal) //输出:true
})
//情景八:数据源为getter函数,返回响应式对象
watch(
() => complexPerson.value.info,
(newVal) => {
console.log(newVal) //除非设置deep: true或info属性被整体替换,否则监听不到
}
)
//情景九:数据源为响应式对象
watch(reactivePerson, (newVal) => {
console.log(newVal) //不设置deep: true也可以监听到
})
总结:
在Vue3中状态都是默认深层响应式的(情景七),嵌套的引用类型在取值(get)时一定是返回Proxy响应式对象
watch数据源为响应式对象时(情景四、七、九),会隐式的创建一个深层侦听器,不需要再显示设置deep: true
情景三和情景八两种情况下,必须显示设置deep: true,强制转换为深层侦听器
情景五和情景七对比下,虽然写法完全相同,但是如果属性值为基本类型时是监听不到的,尤其是ts类型声明为any时,ide也不会提示告警,导致排查问题比较费力
所以精确的ts类型声明很重要,否则经常会出现莫名其妙的watch不生效的问题
ref值为基本类型时通过get\set拦截实现响应式;ref值为引用类型时通过将.value属性转换为reactive响应式对象实现;
deep会影响性能,而reactive会隐式的设置deep: true,所以只有明确状态数据结构比较简单且数据量不大时使用reactive,其他一律使用ref
Props篇
设置默认值
type Props = {
placeholder?: string
modelValue: string
multiple?: boolean
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择',
multiple: false,
})
双向绑定(多个值)
• 自定义组件
//FieldSelector.vue
type Props = {
businessTableUuid: string
businessTableFieldUuid?: string
}
const props = defineProps<Props>()
const emits = defineEmits([
'update:businessTableUuid',
'update:businessTableFieldUuid',
])
const businessTableUuid = ref('')
const businessTableFieldUuid = ref('')
// props.businessTableUuid、props.businessTableFieldUuid转为本地状态,此处省略
//表切换
const tableChange = (businessTableUuid: string) => {
emits('update:businessTableUuid', businessTableUuid)
emits('update:businessTableFieldUuid', '')
businessTableFieldUuid.value = ''
}
//字段切换
const fieldChange = (businessTableFieldUuid: string) => {
emits('update:businessTableFieldUuid', businessTableFieldUuid)
}
• 使用组件
<template>
<FieldSelector
v-model:business-table-uuid="stringFilter.businessTableUuid"
v-model:business-table-field-uuid="stringFilter.businessTableFieldUuid"
/>
</template>
<script setup lang="ts">
import { reactive } from 'vue'
const stringFilter = reactive({
businessTableUuid: '',
businessTableFieldUuid: ''
})
</script>
单向数据流
大部分情况下应该遵循【单向数据流】原则,禁止子组件直接修改props,否则复杂应用下的数据流将变得混乱,极易出现bug且难排查
直接修改props会有告警,但是如果props是引用类型,修改props内部值将不会有告警提示,因此应该有团队约定(第5条除外)
如果props为引用类型,赋值到子组件状态时,需要解除引用(第5条除外)
复杂的逻辑,可以将状态以及修改状态的方法,封装成自定义hooks或者提升到store内部,避免props的层层传递与修改
一些父子组件本就紧密耦合的场景下,可以允许修改props内部的值,可以减少很多复杂度和工作量(需要团队约定固定场景)
逻辑/UI解耦篇
利用Vue3的Composition/组合式API,将某种逻辑涉及到的状态,以及修改状态的方法封装成一个自定义hook,将组件中的逻辑解耦,这样即使UI有不同的形态或者调整,只要逻辑不变,就可以复用逻辑。下面是本项目中涉及的一个真实案例-逻辑树组件,UI有2种形态且可以相互转化。
• hooks部分的代码:useDynamicTree.ts
import { ref } from 'vue'
import { nanoid } from 'nanoid'
export type TreeNode = {
id?: string
pid: string
nodeUuid?: string
partentUuid?: string
nodeType: string
nodeValue?: any
logicValue?: any
children: TreeNode[]
level?: number
}
export const useDynamicTree = (root?: TreeNode) => {
const tree = ref<TreeNode[]>(root ? [root] : [])
const level = ref(0)
//添加节点
const add = (node: TreeNode, pid: string = 'root'): boolean => {
//添加根节点
if (pid === '') {
tree.value = [node]
return true
}
level.value = 0
const pNode = find(tree.value, pid)
if (!pNode) return false
//嵌套关系不能超过3层
if (pNode.level && pNode.level > 2) return false
if (!node.id) {
node.id = nanoid()
}
if (pNode.nodeType === 'operator') {
pNode.children.push(node)
} else {
//如果父节点不是关系节点,则构建新的关系节点
const current = JSON.parse(JSON.stringify(pNode))
current.pid = pid
current.id = nanoid()
Object.assign(pNode, {
nodeType: 'operator',
nodeValue: 'and',
// 重置回显信息
logicValue: undefined,
nodeUuid: undefined,
parentUuid: undefined,
children: [current, node],
})
}
return true
}
//删除节点
const remove = (id: string) => {
const node = find(tree.value, id)
if (!node) return
//根节点处理
if (node.pid === '') {
tree.value = []
return
}
const pNode = find(tree.value, node.pid)
if (!pNode) return
const index = pNode.children.findIndex((item) => item.id === id)
if (index === -1) return
pNode.children.splice(index, 1)
if (pNode.children.length === 1) {
//如果只剩下一个节点,则替换父节点(关系节点)
const [one] = pNode.children
Object.assign(
pNode,
{
...one,
},
{
pid: pNode.pid,
},
)
if (pNode.pid === '') {
pNode.id = 'root'
}
}
}
//切换逻辑关系:且/或
const toggleOperator = (id: string) => {
const node = find(tree.value, id)
if (!node) return
if (node.nodeType !== 'operator') return
node.nodeValue = node.nodeValue === 'and' ? 'or' : 'and'
}
//查找节点
const find = (node: TreeNode[], id: string): TreeNode | undefined => {
// console.log(node, id)
for (let i = 0; i < node.length; i++) {
if (node[i].id === id) {
Object.assign(node[i], {
level: level.value,
})
return node[i]
}
if (node[i].children?.length > 0) {
level.value += 1
const result = find(node[i].children, id)
if (result) {
return result
}
level.value -= 1
}
}
return undefined
}
//提供遍历节点方法,支持回调
const dfs = (node: TreeNode[], callback: (node: TreeNode) => void) => {
for (let i = 0; i < node.length; i++) {
callback(node[i])
if (node[i].children?.length > 0) {
dfs(node[i].children, callback)
}
}
}
return {
tree,
add,
remove,
toggleOperator,
dfs,
}
}
• 在不同组件中使用(UI1/UI2组件为递归组件,内部实现不再展开)
//组件1
<template>
<UI1
:logic="logic"
:on-add="handleAdd"
:on-remove="handleRemove"
:toggle-operator="toggleOperator"
</UI1>
</template>
<script setup lang="ts">
import { useDynamicTree } from '@/hooks/useDynamicTree'
const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree()
const handleAdd = () => {
//添加条件
}
const handleRemove = () => {
//删除条件
}
const toggleOperator = () => {
//切换逻辑关系:且、或
}
</script>
//组件2
<template>
<UI2 :logic="logic"
:on-add="handleAdd"
:on-remove="handleRemove"
:toggle-operator="toggleOperator"
</UI2>
</template>
<script setup lang="ts">
import { useDynamicTree } from '@/hooks/useDynamicTree'
const { add, remove, toggleOperator, tree: logic, dfs } = useDynamicTree()
const handleAdd = () => { //添加条件 }
const handleRemove = () => { //删除条件 }
const toggleOperator = () => { //切换逻辑关系:且、或 }
</script>
Pinia状态管理篇
将复杂逻辑的状态以及修改状态的方法提升到store内部管理,可以避免props的层层传递,减少props复杂度,状态管理更清晰
• 定义一个store(非声明式):User.ts
import { computed, reactive } from 'vue'
import { defineStore } from 'pinia'
type UserInfo = {
userName: string
realName: string
headImg: string
organizationFullName: string
}
export const useUserStore = defineStore('user', () => {
const userInfo = reactive<UserInfo>({
userName: '',
realName: '',
headImg: '',
organizationFullName: ''
})
const fullName = computed(() => {
return `${userInfo.userName}[${userInfo.realName}]`
})
const setUserInfo = (info: UserInfo) => {
Object.assgin(userInfo, {...info})
}
return {
userInfo,
fullName,
setUserInfo
}
})
• 在组件中使用
<template>
<div class="welcome" font-JDLangZheng>
<el-space>
<el-avatar :size="60" :src="userInfo.headImg ? userInfo.headImg : avatar"> </el-avatar>
<div>
<p>你好,{{ userInfo.realName }},欢迎回来</p>
<p style="font-size: 14px">{{ userInfo.organizationFullName }}</p>
</div>
</el-space>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import avatar from '@/assets/avatar.png'
const { userInfo } = useUserStore()
</script>
作者:京东云开发者
来源:juejin.cn/post/7193538517480243258
阿里iconfont审核很慢?自己搭建一个,直接从figma上传
iconfont我们前端都认为挺好用的,但设计师经常说:“这玩意真不好,上传个图标审核半天,审核通过了还不会自动上传😡”
不应该呀,我一看,原来是阿里iconfont管理平台的问题;那简单,我不用它不就行了😎
原来的工作流程
“宁花机器10分钟,不花人工1分钟”,在旧流程中,我们可以看到,人工操作的环节足足有6个(听说阿里icon审核也是人工的)。很显然,这是相当低效的流程。看来,除了阿里第三方的审核问题,我们内部原有的图标交付流程也出现了问题😀
怎么解决
先看看有没有可以直接用的方案
转转的方案 和我们想到一块去了,不使用阿里的iconfont管理平台,而是魔改了YIcon;可惜转转暂时没有开源他们方案的想法,但也给予我们一些思路,我们能不能也学着魔改。
除了YIcon,还有一个开源的iconfont管理平台Nicon。他们的优点都是具有完善的管理&鉴权机制,但缺点是代码过于老旧,长时间没人维护。这也意味着要花费较大的人力要魔改,这对于我们是不能接受的。
很幸运的是,figma社区有较为成熟的Figma图标交付方案figma-icon-automation,看到了Shoppee与得物等公司都参考figma-icon-automation来实现了自己一套的图标交付流程,看起来figma插件是目前最优的选择。可是,我们还是希望保留iconfont的使用方式,不然的话改用SVG组件,这个改变成本也是无法接受的。
因此,我们决定修改figma-icon-automation的流程,实现适合我们的iconfont交付方案🚀
新的iconfont交付方案
魔改1: github 改为 gitlab
出于保护设计师的资产
我们存储icons到内网部署的gitlab上,保护了设计资产的同时,也自然不会有第三方来审核图标。
出于iconont权限管理
gitlab可对不同项目分配不同权限,我们不再需要一个iconfont平台来管理权限;同时解决了,可能会没有及时回收离职员工的阿里iconfont平台权限所带来的风险。
出于iconfont项目管理
这个与普通的gitlab项目没什么区别,你可以创建多个iconfont项目对应不同前端项目,每个项目都是独立的。
出于iconfont版本管理
得益于git的强大,我们可以还拥有了版本管理,这是阿里iconfont平台没有给我们带来的;我们可以清楚地追溯到是谁修改/删除了图标,或者及时地回滚iconfont版本。
魔改2: 更加好用的figma插件
基于gitlab官方Figma插件,我们对其进行改造(主要因为可以节省查阅Figma API文档的时间),实现了一款更加适合我们的设计师使用的Figma插件————UpCon,主要功能如下:
支持配置自己公司gitlab域名
使用自定义gitlab域名作为请求的BaseUrl来调用gitlab开放的api,默认为v4版api,支持最新版gitlab。
支持配置project-id,支持存储多个id
通过project-id来管理不同项目,并且通过本地storage存储多个project-id,方便用户快速切换项目
支持配置gitlab access token
通过access token来登录gitlab,同时识别该用户是否具有该project对应的开发权限(无权限用户无法跳转到上传页)
支持自定义选择多个Icons,并实时预览
通过在figma中选择要上传的icon(支持frame与component类型),填写本次提交的信息,即可触发上传。
支持去除颜色上传
我们保留了阿里iconfont平台的去除颜色上传功能,其原理是通过正则修改SVG代码中的color属性为
currentcolor
选择去除颜色后,当前选择的所有icon都会去除颜色,并可实时预览去除颜色后的效果。
校验icon命名
我们会对已上传的icons名称与当前选择icons名称进行对比,重名的icon,会给予橙色边框与tooltip提示。如果你执意要上传,则会覆盖原先的图标。
与之同时,我们对icon命名进行了强制规范,名称中如含有/\:*?"<>|.
字符,会给予红色边框提示,并不允许上传
魔改3: 触发Gitlab CI脚本
在figma插件触发上传后,会生成一次commit记录并同时触发Gitlab CI操作。可以通过clone 我们开源的iconfont-build-tools来实现自定义Gitlab CI操作,iconfont-build-tools的主要功能如下:
处理转换SVG代码为iconfont
我们会读取当前项目下的
icons/
路径下的所有svg文件(此路径暂不支持修改),将svg代码转换为iconfont.js代码,详细实现代码可查看iconfont-build-tools。
转换svg名称为拼音
我们保留了阿里iconfont平台中,把中文名称自动转为拼音的功能,这一功能大大降低了设计师们的icon命名带来的困扰。
自动生成tag信息并发布新版本
我们还自动把本次git commit的Hash值作为版本tag,并自动发布新版本,这是实现版本管理关键的一步。
自动上传iconfont到CDN
生成的iconfont.js文件可以通过自定义配置来自动上传到自己的CDN,返回的url会自动携带在release信息中,具体的数据格式可查看iconfont-build-tools。
方案开源&计划
目前,我们的方案已经开源了,欢迎大家积极尝试并提出宝贵的建议👍
未来,我们计划给gitlab ci流程中接入微信机器人通知,大家可以持续关注或者star我们的项目😊
Figma UpCon iconfont-build-tools
总结
相比于旧的图标交付流程,新的流程直接把步骤缩减到两步,这大大地提高了我们的效率。而且我们保留许多旧流程的习惯,如依旧使用iconfont方案,upcon中去除颜色功能,build-tools的中文转拼音功能,这些功能的迁移让我们几乎不用花费额外的成本去使用新的流程。
同时我们也希望有更多的用户给予我们正向的反馈,完善此流程,让图标交付变得更简单。
参考链接
figma plugin juejin.cn/post/706816…
得物 IconBot juejin.cn/post/704398…
Shopee IconBot juejin.cn/post/690372…
svgicons2svgfont juejin.cn/post/713711…
iconfont预览 segmentfault.com/a/119000002…
gitlab figma gitlab.com/gitlab-org/…
作者:BlackGoldRoad
来源:juejin.cn/post/7184324458063069245
舍弃传统文档阅读吧!~新一代代码文档神器code-hike
最终效果如图。
起因
相信不少小伙伴阅读过一篇文章:build-your-own-react
这是一篇通俗易懂的从头开始讲述如何创建一个react过程的文章,其中最吸引我的就是这个文章的代码排版方式。将所有代码放置在左侧,随着文档的滚动,左侧代码不断发生变化,不断提示我哪些代码发生了变动。这样的文档方式,是我之前没体验过的船新版本。去作者的gayhub看到正好有开源工具,于是自己搭建了demo,马上惊为天人。所以在这里我要做一个违背祖宗的决定
,将其分享给大家。
code-hike简介
codehike.org/ code-hike是一个 mdx的插件,专注于文档写作的组件库。专注于代码的展示,具有以下几个功能:
代码块的展示
支持134种不同的编程语言,基本涵盖了目前市面上的编程语言。
批注和对代码的交互体验
可以看到在code-hike中可以对部分代码进行高亮显示,这部分主要通过force和mark来操作。同时它还允许你在代码块中进行链接,可以点击跳转到页面的其他位置。也可以自定义自己的样式显示。
一系列的code组件
一系列帮你优化code展示的组件,在本文中,将主要使用CH-scrollycoding
作为展示。
安装
我们这里以Docusaurus
为例作为展示,当然你也可以使用React,vite或其他任意模版或者docs框架作为开始。
我们首先安装docusaurus
npx create-docusaurus@latest my-website classic
然后安装hike的相关依赖
cd my-website
npm i @mdx-js/react@2 docusaurus-theme-mdx-v2 @code-hike/mdx
配置
首先配置docusaurus.config.js
,插入mdx-v2
主题
// docusaurus.config.js
const config = {
...
themes: ["mdx-v2"],
...
}
然后插入code-hike插件
// docusaurus.config.js
const theme = require("shiki/themes/nord.json")
const {
remarkCodeHike,
} = require("@code-hike/mdx")
const config = {
presets: [
[
"classic",
{
docs: {
beforeDefaultRemarkPlugins: [
[remarkCodeHike, { theme }],
],
sidebarPath: require.resolve("./sidebars.js"),
},
},
],
],
...
}
再设置下style
// docusaurus.config.js
...
const config={
theme: {
customCss: [
require.resolve("@code-hike/mdx/styles.css"),
],
},
}
}
至此所有配置完成,我的完整配置如下:
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require("prism-react-renderer/themes/github");
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
const theme = require("shiki/themes/nord.json");
const { remarkCodeHike } = require("@code-hike/mdx");
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "css and js",
tagline: "read everyday",
url: "https://your-docusaurus-test-site.com",
baseUrl: "/",
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
favicon: "img/favicon.ico",
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "facebook", // Usually your GitHub org/user name.
projectName: "docusaurus", // Usually your repo name.
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "en",
locales: ["en"],
},
presets: [
[
"classic",
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
beforeDefaultRemarkPlugins: [[remarkCodeHike, { theme }]],
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/",
},
blog: {
showReadingTime: true,
// Please change this to your repo.
// Remove this to remove the "edit this page" links.
editUrl:
"https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/",
},
theme: {
customCss: [
require.resolve("@code-hike/mdx/styles.css"),
require.resolve("./src/css/custom.css"),
],
},
}),
],
],
themes: ["mdx-v2"],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: "My Site",
logo: {
alt: "My Site Logo",
src: "img/logo.svg",
},
items: [
{
type: "doc",
docId: "intro",
position: "left",
label: "阅读",
},
{ to: "/blog", label: "Blog", position: "left" },
{
href: "https://github.com/facebook/docusaurus",
label: "GitHub",
position: "right",
},
],
},
footer: {
style: "dark",
links: [
{
title: "Docs",
items: [
{
label: "Tutorial",
to: "/docs/intro",
},
],
},
{
title: "Community",
items: [
{
label: "Stack Overflow",
href: "https://stackoverflow.com/questions/tagged/docusaurus",
},
{
label: "Discord",
href: "https://discordapp.com/invite/docusaurus",
},
{
label: "Twitter",
href: "https://twitter.com/docusaurus",
},
],
},
{
title: "More",
items: [
{
label: "Blog",
to: "/blog",
},
{
label: "GitHub",
href: "https://github.com/facebook/docusaurus",
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} My Project, Inc. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;
特别注意
因为code hike使用的是mdx2,在此版本有一个破坏性更新,如果遇到以下问题
请找到<!--truncate-->
,将其删除
开始自己的demo
将docs下的一个md文件,改为如下的数据
为啥不在掘金贴代码块呢,因为跟掘金的markdown冲突了。。。
你得到了如下的效果
这就是一个简单的如我开头展示的效果。
代码标记
这里主要有两个点需要注意 : 第一个是
---
三条横线,作为每段展示文档的分割,所以你可以看到我们的每一段都有这个标记
第二个是 focus
这个标记表明了你有哪些代码需要高亮,在demo中使用的是行高亮。一共有两种用法:
你可以写在文件开头,例如```js statement.js focus=6:8,这表示将从文件的开头进行计算,第6-8行 你也可以写在文件内,例如
// focus(1,1)
const result=[]
这表示,从标记位置开始的后面的1-1行,也就是第一行
除了这种按照行进行标记,你也可以标记列,例如
// focus[7:12]
result = 40000;
它表示从下一行的第7-12个字符。 效果为
作者:im天元
来源:juejin.cn/post/7175000675523887159
vue阻止重复请求(下)
(c)代码
步骤1-通过axios请求拦截器取消重复请求
通过axios
请求拦截器,在每次请求前把请求信息和请求的取消方法放到一个map对象当中,并且判断map对象当中是否已经存在该请求信息的请求,如果存在取消上传请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.min.js" type="application/javascript"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js" type="application/javascript"></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/qs/6.10.3/qs.js" type="application/javascript"></script>
</head>
<body>
<div id="app">
<button @click="onClick1" ref="btn1">请求1</button>
<button @click="onClick2" ref="btn2">请求2</button>
</div>
</body>
<script>
//存储请求信息和取消方法的的map对象
const pendingRequest = new Map();
//根据请求的信息(请求方式,url,请求get/post数据),产生map的key
function getRequestKey(config){
const { method, url, params, data } = config;
return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&");
}
//请求拦截器
axios.interceptors.request.use(
function (config) {
//根据请求的信息(请求方式,url,请求get/post数据),产生map的key
let requestKey = getRequestKey(config)
//判断请求是否重复
if(pendingRequest.has(requestKey)){
//取消上次请求
let cancel = pendingRequest.get(requestKey)
cancel()
//删除请求信息
pendingRequest.delete(requestKey)
}
//把请求信息,添加请求到map当中
// 生成取消方法
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
// 把取消方法添加到map
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel)
}
})
return config;
},
(error) => {
return Promise.reject(error);
}
);
let sendPost = function(data){
return axios({
url: 'http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test',
method: 'post',
data
})
}
new Vue({
el: '#app',
mounted() {
this.$refs.btn1.click()
this.$refs.btn2.click()
},
methods: {
// 使用lodash对请求方法做防抖
//这里有问题,只是对每个按钮的点击事件单独做了防抖,但是两个按钮之间做不到防抖的效果
onClick1: async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求1的结果', res.data)
},
onClick2: async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求2的结果', res.data)
},
},
})
</script>
</html>
步骤2-通过axios响应拦截器处理请求成功
通过axios
的响应拦截器,在请求成功后在map对象当中,删除该请求信息的数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.min.js" type="application/javascript"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js" type="application/javascript"></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/qs/6.10.3/qs.js" type="application/javascript"></script>
</head>
<body>
<div id="app">
<button @click="onClick1" ref="btn1">请求1</button>
<button @click="onClick2" ref="btn2">请求2</button>
</div>
</body>
<script>
//存储请求信息和取消方法的的map对象
const pendingRequest = new Map();
//根据请求的信息(请求方式,url,请求get/post数据),产生map的key
function getRequestKey(config){
const { method, url, params, data } = config;
return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&");
}
//请求拦截器
axios.interceptors.request.use(
function (config) {
//根据请求的信息(请求方式,url,请求get/post数据),产生map的key
let requestKey = getRequestKey(config)
//判断请求是否重复
if(pendingRequest.has(requestKey)){
//取消上次请求
let cancel = pendingRequest.get(requestKey)
cancel()
//删除请求信息
pendingRequest.delete(requestKey)
}
//把请求信息,添加请求到map当中
// 生成取消方法
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
// 把取消方法添加到map
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel)
}
})
return config;
},
(error) => {
return Promise.reject(error);
}
);
//响应拦截器
axios.interceptors.response.use(
(response) => {
//请求成功
//删除请求的信息
let requestKey = getRequestKey(response.config)
if(pendingRequest.has(requestKey)){
pendingRequest.delete(requestKey)
}
return response;
},
(error) => {
return Promise.reject(error);
}
);
let sendPost = function(data){
return axios({
url: 'http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test',
method: 'post',
data
})
}
new Vue({
el: '#app',
mounted() {
this.$refs.btn1.click()
this.$refs.btn2.click()
},
methods: {
// 使用lodash对请求方法做防抖
//这里有问题,只是对每个按钮的点击事件单独做了防抖,但是两个按钮之间做不到防抖的效果
onClick1: async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求1的结果', res.data)
},
onClick2: async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求2的结果', res.data)
},
},
})
</script>
</html>
步骤3-通过axios响应拦截器处理请求失败
通过axios
的响应拦截器,在请求失败后在map对象当中,删除该请求信息的数据
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.min.js" type="application/javascript"></script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js" type="application/javascript"></script>
<script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/qs/6.10.3/qs.js" type="application/javascript"></script>
</head>
<body>
<div id="app">
<button @click="onClick1" ref="btn1">请求1</button>
<button @click="onClick2" ref="btn2">请求2</button>
</div>
</body>
<script>
//存储请求信息和取消方法的的map对象
const pendingRequest = new Map();
//根据请求的信息(请求方式,url,请求get/post数据),产生map的key
function getRequestKey(config){
const { method, url, params, data } = config;
return [method, url, Qs.stringify(params), Qs.stringify(data)].join("&");
}
//请求拦截器
axios.interceptors.request.use(
function (config) {
//根据请求的信息(请求方式,url,请求get/post数据),产生map的key
let requestKey = getRequestKey(config)
//判断请求是否重复
if(pendingRequest.has(requestKey)){
//取消上次请求
let cancel = pendingRequest.get(requestKey)
cancel()
//删除请求信息
pendingRequest.delete(requestKey)
}
//把请求信息,添加请求到map当中
// 生成取消方法
config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
// 把取消方法添加到map
if (!pendingRequest.has(requestKey)) {
pendingRequest.set(requestKey, cancel)
}
})
return config;
},
(error) => {
return Promise.reject(error);
}
);
//删除请求信息
function delPendingRequest(config){
let requestKey = getRequestKey(config)
if(pendingRequest.has(requestKey)){
pendingRequest.delete(requestKey)
}
}
//响应拦截器
axios.interceptors.response.use(
(response) => {
//请求成功
//删除请求的信息
delPendingRequest(response.config)
return response;
},
(error) => {
//请求失败
//不是取消请求的错误
if (!axios.isCancel(error)){
//服务器报400,500报错,删除请求信息
delPendingRequest(error.config || {})
}
return Promise.reject(error);
}
);
let sendPost = function(data){
return axios({
url: 'http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test',
method: 'post',
data
})
}
new Vue({
el: '#app',
mounted() {
this.$refs.btn1.click()
this.$refs.btn2.click()
},
methods: {
// 使用lodash对请求方法做防抖
//这里有问题,只是对每个按钮的点击事件单独做了防抖,但是两个按钮之间做不到防抖的效果
onClick1: async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求1的结果', res.data)
},
onClick2: async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求2的结果', res.data)
},
},
})
</script>
</html>
作者:黄金林
来源:juejin.cn/post/7189231050806001719
vue阻止重复333请求(上)
背景
项目当中前端代码会遇到同一个请求向服务器发了多次的情况,我们要避免服务器资源浪费,同一个请求一定时间只允许发一次请求
思路
(1)如果业务简单,例如同一个按钮防止多次点击,我们可以用定时器做防抖处理
(2)如果业务复杂,例如多个组件通过代码,同一个请求发多次,这个时候防抖已经不好处理了,最好是对重复的ajax请求统一做取消操作
实现
方式1-通过定时器做防抖处理
(a)概述
效果:当用户连续点击多次同一个按钮,最后一次点击之后,过小段时间后才发起一次请求
原理:每次调用方法后都产生一个定时器,定时器结束以后再发请求,如果重复调用方法,就取消当前的定时器,创建新的定时器,等结束后再发请求,工作当中可以用第三方封装的工具函数例如lodash
的debounce
方法来简化防抖的代码
(b)代码
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/lodash.js/4.17.21/lodash.min.js" type="application/javascript">script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.min.js" type="application/javascript">script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js" type="application/javascript">script>
head>
<body>
<div id="app">
<button @click="onClick">请求button>
div>
body>
<script>
// 定义请求接口
function sendPost(data){
return axios({
url: 'https://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test',
method: 'post',
data
})
}
new Vue({
el: '#app',
methods: {
// 调用lodash的防抖方法debounce,实现连续点击按钮多次,0.3秒后调用1次接口
onClick: _.debounce(async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求的结果', res.data)
}, 300),
},
})
script>
html>
(c)预览
(d)存在的问题
无法解决多个按钮件的重复请求的发送问题,例如下面两种情况
情况-在点击事件上做防抖
按钮事件间是相互独立的,调用的是不同方法,做不到按钮间防抖效果
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/lodash.js/4.17.21/lodash.min.js" type="application/javascript">script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.min.js" type="application/javascript">script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js" type="application/javascript">script>
head>
<body>
<div id="app">
<button @click="onClick1" ref="btn1">请求1button>
<button @click="onClick2" ref="btn2">请求2button>
div>
body>
<script>
let sendPost = function(data){
return axios({
url: 'http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test',
method: 'post',
data
})
}
new Vue({
el: '#app',
mounted() {
this.$refs.btn1.click()
this.$refs.btn2.click()
},
methods: {
// 使用lodash对请求方法做防抖
//这里有问题,只是对每个按钮的点击事件单独做了防抖,但是两个按钮之间做不到防抖的效果
onClick1: _.debounce(async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求1的结果', res.data)
}, 300),
onClick2: _.debounce(async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求2的结果', res.data)
}, 300),
},
})
script>
html>
情况2-在接口方法做防抖
按钮间调用的方法是相同的,是可以对方法做防抖处理,但是处理本身对方法做了一次封装,会影响到之前方法的返回值接收,需要对之前的方法做更多处理,变得更加复杂,不推荐
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/lodash.js/4.17.21/lodash.min.js" type="application/javascript">script>
<script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.min.js" type="application/javascript">script>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js" type="application/javascript">script>
head>
<body>
<div id="app">
<button @click="onClick1" ref="btn1">请求1button>
<button @click="onClick2" ref="btn2">请求2button>
div>
body>
<script>
// 使用lodash对请求方法做防抖,
let sendPost = _.debounce(function(data){
//这里有问题,这里的返回值不能作为sendPost方法执行的返回值,因为debounce内部包裹了一层
return axios({
url: 'http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test',
method: 'post',
data
})
}, 300)
new Vue({
el: '#app',
mounted() {
this.$refs.btn1.click()
this.$refs.btn2.click()
},
methods: {
onClick1: async function(){
//这里有问题,sendPost返回值不是promise,而是undefined
let res = await sendPost({username:'zs', age: 20})
console.log('请求1的结果', res)
},
onClick2: async function(){
let res = await sendPost({username:'zs', age: 20})
console.log('请求2的结果', res)
},
},
})
script>
html>
方式2-通过取消ajax请求
(a) 概述
直接对请求方法做处理,通过ajax库的api方法把重复的请求给取消掉
(b)原理
原生ajax取消请求
通过调用XMLHttpRequest
对象实例的abort
方法把请求给取消掉
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
body>
<script>
//原生ajax的语法
let xhr = new XMLHttpRequest();
xhr.open("GET", "http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test?username=zs&age=20", true);
xhr.onload = function(){
console.log(xhr.responseText)
}
xhr.send();
//在谷歌浏览器的低速3g下面测试
//通过XMLHttpRequest实例的abort方法取消请求
setTimeout(() => xhr.abort(), 100);
script>
html>
axios取消请求
通过axios
的CancelToken
对象实例cancel
方法把请求给取消掉
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/axios/0.26.0/axios.min.js" type="application/javascript">script>
head>
<body>
body>
<script>
/*axios的取消的语法*/
// 方式1-通过axios.CancelToken.source产生cancelToken和cancel方法
/*
const source = axios.CancelToken.source();
axios.get('http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test', {
params: {username: 'zs', age: 20},
cancelToken: source.token
}).then(res=>{
console.log('res', res.data)
}).catch(err=>{
console.log('err', err)
})
//在谷歌浏览器的低速3g下面测试
//通过调用source的cancel方法取消
setTimeout(() => source.cancel(), 100);
*/
/**/
// 方式2-通过new axios.CancelToken产生cancelToken和cancel方法
let cancelFn
const cancelToken = new axios.CancelToken(cancel=>{
cancelFn = cancel
});
axios.get('http://nodejs-cloud-studio-demo-bkzxs.nodejs-cloud-studio-demo.50185620.cn-hangzhou.fc.devsapp.net/test', {
params: {username: 'zs', age: 20},
cancelToken: cancelToken
}).then(res=>{
console.log('res', res.data)
}).catch(err=>{
console.log('err', err)
})
//在谷歌浏览器的低速3g下面测试
//通过调用cancelFn方法取消
setTimeout(() => cancelFn(), 100);
script>
html>
作者:黄金林
来源:juejin.cn/post/7189231050806001719