注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

关于浏览器缓存策略这件事儿

web
前言 我们打开百度这个网站并刷新多次时时,注意到百度的logo是没有每次都加载一遍的。我们知道图片是img标签中的src属性加载出来的,这也需要浏览器去请求图片资源的,那么为什么刷新多次浏览器只请求了一次图片资源呢?这就涉及到了浏览器的缓存策略了,这张图片被浏...
继续阅读 »

前言


我们打开百度这个网站并刷新多次时时,注意到百度的logo是没有每次都加载一遍的。我们知道图片是img标签中的src属性加载出来的,这也需要浏览器去请求图片资源的,那么为什么刷新多次浏览器只请求了一次图片资源呢?这就涉及到了浏览器的缓存策略了,这张图片被浏览器缓存下来了!


正文


一、为什么要有浏览器的缓存策略?



  • 提升用户体验,减少页面重复的http请求


二、为什么通过浏览器url地址栏访问的html页面不缓存?



  • 强制刷新页面浏览器url地址栏访问资源 时,浏览器默认会在请求头中设置Cache-control: no-cache,如设置该属性浏览器就会忽略响应头中的 Cache-control


如何优化网络资源请求的时间呢?有以下三种方式。


三、CDN网络分发



CDN:CDN会通过负载均衡技术,将用户的请求定向到最合适缓存服务器上去获取内容。



比如说,北京的用户,我们让他访问北京的节点,深圳的用户,我们让他访问深圳的节点。通过就近访问,加速用户对网站的访问,进而解决Internet网络拥堵状况,提高用户访问网络的响应速度。


四、强缓存



强缓存是浏览器的缓存策略,后端设置响应头中的属性值就能设置文件资源在浏览器的缓存时间过了缓存的有效期再次访问时,文件资源需再次加载



强缓存有两种方式来控制资源被浏览器缓存的时长:



  1. 后端设置响应头中的 Cache-control: max-age=3600 来控制缓存时长(为一个小时)

  2. 后端设置响应头中的 Expires:xxx 来控制缓存的截止日期(截止日期为xxx)


我们直接上代码让你更好理解,我们需要实现一个页面,页面上需展现一个标题一张图片


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Earth</h1>
<img src="assets/image/earth.jpeg" alt="">
</body>
</html>

const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg

const time = new Date(Date.now() + 3600000).toUTCString() //定义时间 作为缓存时间的有效期

let status = 200

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时
// 'expires': time //截止日期 缓存一小时后过期
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
}else{
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

第一次运行:
image.png


刷新页面后,可以看到图片资源没有重新加载:
image.png


三、协商缓存


我们想象这样的场景:当我们偷偷把图片偷偷换成另一张图片,图片名依然和之前那张一样,会是什么结果呢?

操作后,刷新页面发现图片还是之前那张图片,并没有换成新的!那这就出事儿了,后端图片换了,用户看到的还是老图片,有一种方案是改变图片资源的名字,直接请求最新图片资源,但这并不是最优方案,终极方案是需要协商缓存的帮忙。



协商缓存也是浏览器的缓存策略,它也有两种方式辅助强缓存,来判断文件资源是否被修改



1. 后端设置响应头中的 last-modified: xxxx



  • 辅助强缓存,让URL地址栏请求的资源也能被缓存

  • 辅助强缓存,借助请求头中的if-modified-since来判断资源文件是否被修改,如果被修改则返回新的资源,否则返回304状态码,让前端读取本地缓存


代码如下:


const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg

const time = new Date(Date.now() + 3600000).toUTCString() //定义时间 作为缓存时间的有效期

const timeStamp = req.headers['if-modified-since'] //请求头的if-modified-since字段
let status = 200

//判断文件是否修改过
if (timeStamp && Number(timeStamp) === stats.mtimeMs) { //timeStamp为字符串 转换为number类型判断
status = 304
}

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时 //max-age=0或no-cache不需要缓存
// 'expires': time //截止日期 缓存一小时后过期
'last-modified': stats.mtimeMs //文件最后一次修改时间
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
}else{
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})

我们只要看last-modified这个字段的值有无变化即可:
image.png


2. Etag:文件的标签



  • 请求头中会被携带If--Match

  • Etag保证了每一个资源是唯一的,资源变化都会导致Etag变化。服务器根据If--Match值来判断是否命中缓存。 当服务器返回304的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。


image.png

代码如下:


const http = require('http');
const path = require('path');
const fs = require('fs');
const mime = require('mime'); //接收一个文件后缀,返回一个文件类型
const md5 = require('crypto-js/md5');

const server = http.createServer((req, res) => {
const filePath = path.resolve(__dirname, `www/${req.url}`) //resolve合并地址
if (fs.existsSync(filePath)) { //判断路径是否有效
const stats = fs.statSync(filePath) //获取文件信息
const isDir = stats.isDirectory() //是否是文件夹
if (isDir) {
filePath = path.join(filePath, 'index.html')
}
//读取文件
if (!isDir || fs.existsSync(filePath)) {

//判断前端请求的路径的后缀名是图片还是文本
const { ext } = path.parse(filePath) //.html .jpeg
const content = fs.readFileSync(filePath);
let status = 200

//判断文件是否被修改过
if (req.headers['if-none-match'] == md5(content)) {
status=304
}

res.writeHead(status, {
'Content-Type': `${mime.getType(ext)};charset=utf-8`,
'Cache-control': 'max-age=3600', //缓存时长为一小时 //max-age=0或no-cache不需要缓存
'Etag': md5(content) //文件资源的md5值
})

if (status === 200) {
const fileStream = fs.createReadStream(filePath) //将文件读成流类型
fileStream.pipe(res) //将文件流导入响应体
} else {
res.end();
}

}
}
})

server.listen(3000, () => {
console.log('listening on port 3000');
})


最后附上一张图便于更好理解浏览器的缓存策略:


image.png

收起阅读 »

js十大手撕代码

web
前言 js中有很多API贼好用,省下了很多工夫,你知道它的原理吗?这篇文章对它们做一个总结。 正文 一、手撕instanceof instanceof的原理:通过判断对象的原型是否等于构造函数的原型来进行类型判断 代码实现: const myInstanc...
继续阅读 »

前言


js中有很多API贼好用,省下了很多工夫,你知道它的原理吗?这篇文章对它们做一个总结。


正文


一、手撕instanceof



  • instanceof的原理:通过判断对象的原型是否等于构造函数的原型来进行类型判断

  • 代码实现:


const myInstanceOf=(Left,Right)=>{
if(!Left){
return false
}
while(Left){
if(Left.__proto__===Right.prototype){
return true
}else{
Left=Left.__proto__
}
}
return false
}

//验证
console.log(myInstanceOf({},Array)); //false

二、手撕call,apply,bind


call,apply,bind是通过this的显示绑定修改函数的this指向


1. call


call的用法:a.call(b) -> 将a的this指向b

我们需要借助隐式绑定规则来实现call,具体实现步骤如下:

往要绑定的那个对象(b)上挂一个属性,值为需要被调用的那个函数名(a),在外层去调用函数。


function foo(x,y){
console.log(this.a,x+y);
}

const obj={
a:1
}

Function.prototype.myCall=function(context,...args){
if(typeof this !== 'function') return new TypeError('is not a function')
const fn=Symbol('fn') //使用Symbol尽可能降低myCall对其他的影响
context[fn]=this //this指向foo
const res=context[fn](...args) //解构,调用fn
delete context[fn] //不要忘了删除obj上的工具函数fn
return res //将结果返回
}

//验证
foo.myCall(obj,1,2) //1,3

2. apply


apply和call的本质区别就是接受的参数形式不同,call接收零散的参数,而apply以数组的方式接收参数,实现思路完全一样,代码如下:


function foo(x,y){
console.log(this.a,x+y);
}

const obj={
a:1
}

Function.prototype.myApply=function(context,args){
if(typeof this !== 'function') return new TypeError('is not a function')
const fn=Symbol('fn') //尽可能降低myCall对其他的影响
context[fn]=this
context[fn](...args)
delete context[fn]
}

//验证
foo.myApply(obj,[1,2]) //1,3

3. bind


bind和call,apply的区别是会返回一个新的函数,接收零散的参数

需要注意的是,官方bind的操作是这样的:



  • 当new了bind返回的函数时,相当于new了foo,且new的参数需作为实参传给foo

  • foo的this.a访问不到obj中的a


function foo(x,y,z){
this.name='zt'
console.log(this.a,x+y+z);
}

const obj={
a:1
}


Function.prototype.myBind=function(context,...args){

if(typeof this !== 'function') return new TypeError('is not a function')

context=context||window

let _this=this

return function F(...arg){
//判断返回出去的F有没有被new,有就要把foo给到new出来的对象
if(this instanceof F){
return new _this(...args,...arg) //new一个foo
}
_this.apply(context,args.concat(arg)) //this是F的,_this是foo的 把foo的this指向obj用apply
}
}

//验证
const bar=foo.myBind(obj,1,2)
console.log(new bar(3)); //undefined 6 foo { name: 'zt' }


三、手撕深拷贝


这篇文章中详细记录了实现过程
【js手写】浅拷贝与深拷贝


四、手撕Promise


思路:



  • 我们知道,promise是有三种状态的,分别是pending(异步操作正在进行), fulfilled(异步操作成功完成), rejected(异步操作失败)。我们可以定义一个变量保存promise的状态。

  • resolve和reject的实现:把状态变更,并把resolve或reject中的值保存起来留给.then使用

  • 要保证实例对象能访问.then,必须将.then挂在构造函数的原型上

  • .then接收两个函数作为参数,我们必须对所传参数进行判断是否为函数,当状态为fulfilled时,onFulfilled函数触发,并将前面resolve中的值传给onFulfilled函数;状态为rejected时同理。

  • 当在promise里放一个异步函数(例:setTimeout)包裹resolve或reject函数时,它会被挂起,那么当执行到.then时,promise的状态仍然是pending,故不能触发.then中的回调函数。我们可以定义两个数组分别存放.then中的两个回调函数,将其分别在resolve和reject函数中调用,这样保证了在resolve和reject函数触发时,.then中的回调函数即能触发。


代码如下:


const PENDING = 'pending'
const FULFILLED = 'fullfilled'
const REJECTED = 'rejected'

function myPromise(fn) {
this.state = PENDING
this.value = null
const that = this
that.resolvedCallbacks = []
that.rejectedCallbacks = []

function resolve(val) {
if (that.state == PENDING) {
that.state = FULFILLED
that.value = val
that.resolvedCallbacks.map((cb)=>{
cb(that.value)
})
}
}
function reject(val) {
if (that.state == PENDING) {
that.state = REJECTED
that.value = val
that.rejectedCallbacks.map((cb)=>{
cb(that.value)
})
}
}

try {
fn(resolve, reject)
} catch (error) {
reject(error)
}

}

myPromise.prototype.then = function (onFullfilled, onRejected) {
const that = this
onFullfilled = typeof onFullfilled === 'function' ? onFullfilled : v => v
onRejected= typeof onRejected === 'function' ? onRejected : r => { throw r }

if(that.state===PENDING){
that.resolvedCallbacks.push(onFullfilled)
that.resolvedCallbacks.push(onRejected)
}
if (that.state === FULFILLED) {
onFullfilled(that.value)
}
if (that.state === REJECTED) {
onRejected(that.value)
}
}

//验证 ok ok
let p = new myPromise((resolve, reject) => {
// reject('fail')
resolve('ok')
})

p.then((res) => {
console.log(res,'ok');
}, (err) => {
console.log(err,'fail');
})

五、手撕防抖,节流


这篇文章中详细记录了实现过程
面试官:什么是防抖和节流?如何实现?应用场景?


六、手撕数组API


1. forEach()


思路:



  • forEach()用于数组的遍历,参数接收一个回调函数,回调函数中接收三个参数,分别代表每一项的值、下标、数组本身。

  • 要保证数组能访问到我们自己手写的API,必须将其挂到数组的原型上


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

//代码实现
Array.prototype.my_forEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}

//验证
arr.my_forEach((item, index, arr) => { //111 111
if (item.age === 18) {
item.age = 17
return
}
console.log('111');
})


2. map()


思路:



  • map()也用于数组的遍历,与forEach不同的是,它会返回一个新数组,这个新数组是map接收的回调函数返回值

    代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_map=function(callback){
const res=[]
for(let i=0;i<this.length;i++){
res.push(callback(this[i],i,this))
}
return res
}

//验证
let newarr=arr.my_map((item,index,arr)=>{
if(item.age>18){
return item
}
})
console.log(newarr);
//[
// undefined,
// { name: 'aa', age: 19 },
// undefined,
// { name: 'cc', age: 21 }
//]

3. filter()


思路:



  • filter()用于筛选过滤满足条件的元素,并返回一个新数组


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_filter = function (callback) {
const res = []
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}

//验证
let newarr = arr.my_filter((item, index, arr) => {
return item.age > 18
})
console.log(newarr); [ { name: 'aa', age: 19 }, { name: 'cc', age: 21 } ]

4. reduce()


思路:



  • reduce()用于将数组中所有元素按指定的规则进行归并计算,返回一个最终值

  • reduce()接收两个参数:回调函数、初始值(可选)。

  • 回调函数中接收四个参数:初始值 或 存储上一次回调函数的返回值、每一项的值、下标、数组本身。

  • 若不提供初始值,则从第二项开始,并将第一个值作为第一次执行的返回值


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_reduce = function (callback,...arg) {
let pre,start=0
if(arg.length){
pre=arg[0]
}
else{
pre=this[0]
start=1
}
for (let i = start; i < this.length; i++) {
pre=callback(pre,this[i], i, this)
}
return pre
}

//验证
const sum = arr.my_reduce((pre, current, index, arr) => {
return pre+=current.age
},0)
console.log(sum); //76


5. fill()


思路:



  • fill()用于填充一个数组的所有元素,它会影响原数组 ,返回值为修改后原数组

  • fill()接收三个参数:填充的值、起始位置(默认为0)、结束位置(默认为this.length-1)。

  • 填充遵循左闭右开的原则

  • 不提供起始位置和结束位置时,默认填充整个数组


代码实现:


Array.prototype.my_fill = function (value,start,end) {
if(!start&&start!==0){
start=0
}
end=end||this.length
for(let i=start;i<end;i++){
this[i]=value
}
return this
}

//验证
const arr=new Array(7).my_fill('hh',null,3) //往数组的某个位置开始填充到哪个位置,左闭右开
console.log(arr); //[ 'hh', 'hh', 'hh', <4 empty items> ]


6. includes()


思路:



  • includes()用于判断数组中是否包含某个元素,返回值为 true 或 false

  • includes()提供第二个参数,支持从指定位置开始查找


代码实现:


const arr = ['a', 'b', 'c', 'd', 'e']

Array.prototype.my_includes = function (item,start) {
if(start<0){start+=this.length}
for (let i = start; i < this.length; i++) {
if(this[i]===item){
return true
}
}
return false
}

//验证
const flag = arr.my_includes('c',3) //查找的元素,从哪个下标开始查找
console.log(flag); //false


7. join()


思路:



  • join()用于将数组中的所有元素指定符号连接成一个字符串


代码实现:


const arr = ['a', 'b', 'c']

Array.prototype.my_join = function (s = ',') {
let str = ''
for (let i = 0; i < this.length; i++) {
str += `${this[i]}${s}`
}
return str.slice(0, str.length - 1)
}

//验证
const str = arr.my_join(' ')
console.log(str); //a b c

8. find()


思路:



  • find()用于返回数组中第一个满足条件元素,找不到返回undefined

  • find()的参数为一个回调函数


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_find = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return this[i]
}

}
return undefined
}

//验证
let j = arr.my_find((item, index, arr) => {
return item.age > 19
})
console.log(j); //{ name: 'cc', age: 21 }

9. findIndex()


思路:



  • findIndex()用于返回数组中第一个满足条件索引,找不到返回-1

  • findIndex()的参数为一个回调函数


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_findIndex = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return i
}
}
return -1
}


let j = arr.my_findIndex((item, index, arr) => {
return item.age > 19
})
console.log(j); //3

10. some()


思路:



  • some()用来检测数组中的元素是否满足指定条件。

  • 有一个元素符合条件,则返回true,且后面的元素会再检测。


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_some = function (callback) {
for (let i = 0; i < this.length; i++) {
if(callback(this[i], i, this)){
return true
}
}
return false
}

//验证
const flag = arr.some((item, index, arr) => {
return item.age > 20
})
console.log(flag); //true

11. every()


思路:



  • every() 用来检测所有元素是否都符合指定条件。

  • 有一个不满足条件,则返回false,后面的元素都会再执行。


代码实现:


const arr = [
{ name: 'zt', age: 18 },
{ name: 'aa', age: 19 },
{ name: 'bb', age: 18 },
{ name: 'cc', age: 21 },
]

Array.prototype.my_every = function (callback) {
for (let i = 0; i < this.length; i++) {
if(!callback(this[i], i, this)){
return false
}
}
return true
}

//验证
const flag = arr.my_every((item, index, arr) => {
return item.age > 16
})
console.log(flag); //true


七、数组去重


1. 双层for循环 + splice()


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]
function unique(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1)
j-- //删除后j向前走了一位,下标需要减一,避免少遍历一位
}
}
}
return arr
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

2. 排序后做前后比较


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]

function unique(arr) {
let res = []
let seen //记录上一次比较的值
let newarr=[...arr] //解构出来,开辟一个新数组
newarr.sort((a,b)=>a-b) //sort会影响原数组 n*logn
for (let i = 0; i < newarr.length; i++) {
if (newarr[i]!==seen) {
res.push(newarr[i])
}
seen=newarr[i]
}
return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

3. 借助include


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]

function unique(arr) {
let res = []
for (let i = 0; i < arr.length; i++) {
if(!res.includes(arr[i])){
res.push(arr[i])
}
}
return res
}

console.log(unique(arr)) //[ 1, '1', 2, 3 ]

4. 借助set


let arr = [1, 1, '1', '1', 2, 2, 2, 3, 2]
const res1 = Array.from(new Set(arr));
console.log(res1); //[ 1, '1', 2, 3 ]

八、数组扁平化


1. 递归


let arr1 = [1, 2, [3, 4, [5],6]]

function flatter(arr) {
let len = arr.length
let result = []
for (let i = 0; i < len; i++) { //遍历数组每一项
if (Array.isArray(arr[i])) { //判断子项是否为数组并拼接起来
result=result.concat(flatter(arr[i]))//是则使用递归继续扁平化
}
else {
result.push(arr[i]) //不是则存入result
}
}
return result
}

console.log(flatter(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

2. 借助reduce (本质也是递归)


let arr1 = [1, 2, [3, 4, [5],6]]

const flatter = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
console.log(flatter(arr1)) //[ 1, 2, 3, 4, 5, 6 ]

3. 借助正则


let arr1 = [1, 2, [3, 4, [5],6]]

const res = JSON.parse('[' + JSON.stringify(arr1).replace(/\[|\]/g, '') + ']');
console.log(res) //[ 1, 2, 3, 4, 5, 6 ]

九、函数柯里化


思路:



  • 函数柯里化是只传递给函数一部分参数调用它,让它返回一个函数去处理剩下的参数

  • 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数,小于则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数


代码实现:


const my_curry = (fn, ...args) => 
args.length >= fn.length
? fn(...args)
: (...args1) => curry(fn, ...args, ...args1);

function adder(x, y, z) {
return x + y + z;
}
const add = my_curry(adder);
console.log(add(1, 2, 3)); //6
console.log(add(1)(2)(3)); //6
console.log(add(1, 2)(3)); //6
console.log(add(1)(2, 3)); //6

十、new方法


思路:



  • new方法主要分为四步:

    (1) 创建一个新对象

    (2) 将构造函数中的this指向该对象

    (3) 执行构造函数中的代码(为这个新对象添加属性

    (4) 返回新对象


function _new(obj, ...rest){
// 基于obj的原型创建一个新的对象
const newObj = Object.create(obj.prototype);

// 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
const result = obj.apply(newObj, rest);

// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
return typeof result === 'object' ? result : newObj;
}



总结不易,

作者:zt_ever
来源:juejin.cn/post/7253260410664419389
动动手指给个赞吧!💗

收起阅读 »

在线代码编辑器介绍与选型

web
引言 作为数据生产和管理的平台,数据平台的一大核心功能是在线数据开发,工欲善其事必先利其器,所以平台具备一个功能较为丰富、用户体验友好的在线代码编辑器,就成为了前提条件。 经历最近一两年的代码编辑器方案调研、选型和开发,我们对内部平台使用的代码编辑器进行了统一...
继续阅读 »

引言


作为数据生产和管理的平台,数据平台的一大核心功能是在线数据开发,工欲善其事必先利其器,所以平台具备一个功能较为丰富、用户体验友好的在线代码编辑器,就成为了前提条件。


经历最近一两年的代码编辑器方案调研、选型和开发,我们对内部平台使用的代码编辑器进行了统一和升级,并根据用户需求和业务场景进行了插件化定制,其底层是使用了 Monaco Editor 来进行二次开发。


本文主要是结合自己的理解,对代码编辑器相关知识进行整理,跟大家分享。


1. 在线代码编辑器是什么?


1.1 介绍


在线代码编辑器是一种基于 Web 技术开发的代码文本编辑器,可以在 Web 浏览器中直接使用。它通常包括用户界面模块、文本处理模块、插件扩展模块等模块;用户可以通过 Web 编辑器创建、编辑各种类型的文本文件,例如 HTML、CSS、JavaScript、Markdown 等。


1.2 分类


我们先来看看编辑器的分类:


类型描述典型产品优势劣势
远古编辑器textarea 或contentEditable+execCommand早期轻型编辑器(《100行代码带你实现一个编辑器》系列)门槛低,短时间内快速研发无法定制
contentEditable+文档模型借助contentEditable,各种拦截用户操作draftjs (react)、quilljs (vue)、prosemirror(util)站在浏览器的肩膀上,可以实现绝大多数的业内需求无法突破浏览器本身的限制(排版)
独立开发脱离浏览器自带编辑能力,独立做光标和排版引擎Google Docs、WPS等所有内容都把握在自己手上,排版随意个性化技术难度较高,研发成本较大

第一类编辑器,其劣势明显:由于重度依赖浏览器 execCommand 接口,而该接口支持的能力非常有限,故大多数功能无法订制,比如 fontSize 只能设置 1 - 7。另外兼容性也是一大问题,例如 Safari 并没有支持 heading 的设置。参考 MDN。而且该类编辑器基本都会直接将 HTML 作为数据模型(Model)来使用,这样会引发另外一个问题:相同的UI,可能对应了不同的DOM结构。举个例子,对于“加粗字体”这个用户输入,在 chrome 上,是添加了<blod>标签,ie11上则是添加了<strong>标签。


第二类编辑器与上一类编辑器最大的不同是定义了自己的 Model 层,所有视图(View)都与 Model 一一对应,并且一切 View 的变化都将由 Model 层的变化引发。为了做到这一点,需要拦截一切用户操作,准确识别用户意图,再对 Model 层进行正确的修改。坑点主要来自于对用户操作的拦截以及浏览器实现层面上的一些疑难杂症。故该类编辑器实现中的 hack 代码会非常多,理解起来比较困难。


第三类编辑器,采用隐藏textarea方案,它只负责接收输入事件,其他视图输出全靠自己,相对来说,更容易解耦。因为基本脱离了浏览器原生的光标,这块可以实现出更强大的功能。排版引擎可以自己搞,只要码力够强,想搞一个从从上往下从右往左的富文本编辑器也没问题,也带来了各种各样的可能,比如可以通过将 View 层用 canvas 实现,以规避很多兼容性问题。


2. 一款优秀的在线代码编辑器需要有哪些功能?


下面我们来看一下一个可用于生产环境的在线代码编辑器需要有哪些能力和模块:



2.1 核心模块


模块名模块描述
文本编辑用于处理用户输入的文本内容,管理文本状态,还包括实现文本的插入、删除、替换、撤销、重做等操作
语言实现语言高亮、代码分析、代码补全、代码提示&校验等能力
主题主要用于实现主题的管理、注册、切换、等功能
渲染主要完成编辑器的整体设计与生命周期管理
命令 & 快捷键管理注册和编辑的各种命令,比如查找文件、撤销、复制&粘贴等,同时也支持将命令以快捷键的形式暴露给用户
通信 & 数据流管理编辑器各模块之前的通信,以及数据存储、流转过程

2.2 扩展模块


模块名模块描述
文本能力扩展在现有处理文本的基础上进行功能扩展,比如修改获取文本方式。
语言扩展包括自定义新语言,扩展现有语言的关键字,完善代码解析、提示&校验等能力。
主题扩展包括自定义新主题,扩展现有主题的能力
命令扩展增加新命令,或者改写&扩展现有命令

3. 开源市场上有哪些代码编辑器?


目前开源市场使用较多的代码编辑器主要有 3 个,分别是 Monaco Editor(第三类)、Ace(第三类)和 Code Mirror(第二类)。本文也将带大家去了解他们的整体架构,做一些对比分析。


3.1 Monaco Editor


基本介绍:


类别描述
介绍是一个功能相对比较完整的代码编辑器,实现使用了 MVP 架构,采用了模块化和组件化的思想,其中编辑器核心代码部分是与 vscode 共用的,从源码目录中能看到有很多 browser 与 common 的目录区分。
仓库地址github.com/microsoft/v…
入口文件/editor/editor.main.ts
开始使用editor.create()方法来自 /editor/standalone/browser/standaloneEditor.ts

目录结构:


├── base        			# 通用工具/协议和UI库
│ ├── browser # 基础UI组件,DOM操作,事件
│ ├── common # diff计算、处理,markdown解析器,worker协议,各种工具函数
├── editor # 代码编辑器核心
| ├── browser # 在浏览器环境下的实现,包括了用于处理 DOM 事件、测量文本尺寸和位置、渲染文本等功能的代码。
| ├── common # 浏览器和 Node.js 环境下共用的代码,其中包括了文本模型、文本编辑操作、语法分析等功能的实现
| ├── contrib # 扩展模块,包含很多额外功能 查找&替换,代码片段,多光标编辑等等
| └── standalone # 实现了一个完整的编辑器界面,也是我们通常使用的完整编辑器
├── language # 前端需要的几种语言类型,与basic-languages不同的是,这里的实现语言功能更完整,包含关键字提示与语法校验等
├── basic-languages # 基础语言声明,里面只包含了关键字的罗列,主要用于关键字的高亮,不包含提示和语法校验

特点:



  • 多线程处理,主要分为 主线程 和 语言服务线程(使用了 Web Worker 技术 来模拟多线程,主要通过 postMessage 来进行消息传递)

    • 主线程:主要负责处理用户与编辑器的交互操作,以及渲染编辑器的 UI 界面,还负责管理编辑器的生命周期和资源,例如创建和销毁编辑器实例、加载和卸载语言服务、加载和卸载扩展等。

    • 语言服务线程:负责提供代码分析、语法检查等功能,以及处理与特定语言相关的操作。




DOM 结构:


<div class="monaco-editor" role="presentation">
<div class="overflow-guard" role="presentation">
<div class="monaco-scrollable-element editor-scrollable" role="presentation">
<!--实现行高亮-->
<div class="monaco-editor-background" role="presentation"></div>
<!--实现关键字背景高亮-->
<div class="view-overlays" role="presentation">
<div>...</div>
</div>
<!--每一行内容-->
<div class="view-lines" role="presentation">
<div>...</div>
</div>
<!--光标-->
<div class="monaco-cursor-layer" role="presentation"></div>
<!--文本输入框-->
<textarea class="monaco-editor-textarea"></textarea>
<!--横向滚动条-->
<div class="scrollbar horizontal"></div>
<!--纵向滚动条-->
<div class="scrollbar vertical"></div>
</div>
</div>
</div>


3.2 Code Mirror


基本介绍:


类别描述
介绍CodeMirror 6 是一款浏览器端代码编辑器,基于 TypeScript,该版本进行了完全的重写,核心思想是模块化和函数式,支持超过 14 种语言的语法高亮,亮点是高性能、可扩展性高以及支持移动端。
仓库地址github.com/codemirror
入口文件由于高度模块化,没有一个集成的入口文件,这里放上核心库@codemirror/view的入口文件:src/index.ts

开始使用


import { EditorState } from '@codemirror/state'; import { EditorView, keymap } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
let startState = EditorState.create({
doc: 'console.log("hello, javascript!")',
extensions: [keymap.of(defaultKeymap)],
});
let view = new EditorView({
state: startState,
parent: document.body,
});

目录结构:


高度模块化(分为多个仓库),这里放上比较核心的库的分布和内部结构


核心模块:提供了编辑器视图(@codemirror/view)、编辑器状态(@codemirror/state)、基础命令(@codemirror/commands)等基础功能。


语言模块:提供了不同编程语言的语法高亮、自动补全、缩进等功能,例如@codemirror/lang-javascript@codemirror/lang-sql@codemirror/lang-python 等。


主题模块:提供了不同风格的编辑器主题,例如 @codemirror/theme-one-dark


扩展模块:提供了一些额外的编辑器功能,例如行号(@codemirror/gutter)、折叠(@codemirror/fold)、括号匹配(@codemirror/matchbrackets)等。


内部结构,以@codemirror/view为例:


├── src                         # 源文件夹
│ ├── editorview.ts # 编辑器视图层
│ ├── decoration.ts # 视图装饰
│ ├── cursor.ts # 光标的渲染
│ ├── domchange.ts # DOM 改变相关的逻辑
│ ├── domobserver.ts # 监听 DOM 的逻辑
│ ├── draw-selection.ts # 绘制选区
│ ├── placeholder.ts # placeholder的渲染
│ ├── ...
├── test # 测试用例
| ├── webtest-domchange.ts # 测试监听到 DOM 变化后的一系列处理。
| ├── ...

特点:


指导 CodeMirror 架构设计的核心观点是函数式代码(纯函数),它会创建一个没有副作用的新值,和命令式代码交互更方便。而浏览器 DOM 很明显也是命令式思维,和 CodeMirror 集成的大部分系统类似。


CodeMirror 6 的 state 表现层是严格函数式的 - 即 document 和 state 数据结构都是不可变的,而能操作它们的都是纯函数,view 包将它们封装在一个命令式接口中。


所以即使 editor 已经转到了新的 state,而旧的 state 依然原封不动的存在,保存旧状态和新状态在面对处理 state 改变的情况下极为有利,这也意味着直接改变一个 state 值,或者添加额外 state 属性的命令式扩展都是不建议的,后果也不太可控。


CodeMirror 处理状态更新的方式受 Redux 启发,除了极少数情况(如组合和拖拽处理),视图的状态完全是由 EditorState 里的 state 属性决定的。


通过创建一个描述改变document、selection 或其他 state 属性的 transaction,以这种函数调用方式来更新 state。这个 transaction 之后可以通过 dispatched 分发,告诉 view 更新 state,更新新 state 对应的 DOM 展示。


let transaction = view.state.update({ changes: { from: 0, insert: "0" }})
console.log(transaction.state.doc.toString()) // "0123"
// 此刻视图依然显示的旧状态
view.dispatch(transaction)
// 现在显示新状态了

典型的用户交互数据流如下图:



view 监听事件变化。当 DOM 事件发生时(或者快捷键触发的命令,或者由扩展注册的事件处理器),CodeMirror会把这些事件转换为新的状态 transcation,然后分发。此时生成一个新的 state,当接收到新 state 后就会去更新 DOM。


DOM 结构:


<div class="cm-editor [theme scope classes]">
<div class="cm-scroller">
<div class="cm-content" contenteditable="true">
<div class="cm-line">Content goes here</div>
<div class="cm-line">...</div>
</div>
</div>
</div>


cm-editor 为一个 editor view 实例(在 merge-view,也就是代码对比情况下,给做了一个合并,其实还是两个 editor view 合在一起)


cm-scroller 为编辑器主展示区,并且展示了滚动条


cm-tooltip-autocomplete 为展示一些独立的层,比如代码提示,代码补全等


cm-gutter 是行号


cm-content 是编辑器的内容区


cm-layer 是跟 content 平级的,主要负责自定义指针和选区的展示


view-port 为CodeMirror 的一个优化,只解析和渲染了这个可视区域内的 DOM


cm-line 是每一行的内容,里面就是真实的 DOM 了


line-decorator 是提供给插件使用,用来装饰每一行的


在这个架构下,每个 editor 比较独立,可以渲染多个



3.3 Ace


基本介绍:


类别描述
介绍基于 Web 技术的代码编辑器,可以在浏览器中运行,高性能,体积小,功能全是它的主要优点。支持了超过120种语言的语法高亮,超过20个不同风格的主题,与 Sublime,Vim 和 TextMate 等本地编辑器的功能和性能相匹配。
仓库地址github.com/ajaxorg/Ace
入口文件/src/Ace.js
开始使用Ace.edit()

目录结构:


Ace 的目录结构相对简单,按功能分成了一个个不同的 js 文件,我这里列举其中一部分,部分较为复杂的功能除了提供了入口 js 文件以外,还在对应同级建立了文件夹里面实现各种逻辑,这里列举了 layer (渲染层) 为例子。


src/
├── layer #渲染分层实现
├── cursor.js #鼠标滑入层
├── decorators.js #装饰层,例如波浪线
├── lines.js #行渲染层
├── text.js #文本内容层
├── ...
├── ... #其他功能,例如 keybord
├── Ace.js #入口文件
├── ...
├── autocomplete.js #定义了编辑器补全相关内容
├── clipboard.js #定义了pc移动端兼容的剪切板
├── config.js
├── document.js
├── edit_session.js #定义了 Session 对象
├── editor.js #定义了 editor 对象
├── editor_keybinding.js #键盘事件绑定
├── editor_mouse_handler.js
├── virtual_renderer.js #定义了渲染对象 Renderer,引用了 layer 中定义的个种类
├── ...
├── mode.js
├── search.js
├── selection.js
├── split.js
└── theme.js

特点:



  • 事件驱动

    • Ace 中提供了丰富的事件系统,以供使用者直接使用或者自定义,并且通过对事件的触发和响应来进行内部数据通信实现代码检查,数据更新等等



  • 多线程

    • Ace 编辑器将解析代码的任务交给 Web Worker 处理,以提高代码解析的速度并避免阻塞用户界面。在 Web Worke r中,Ace 使用 Acorn库来解析 JavaScript 代码,并将解析结果发送回主线程进行处理




DOM 结构:


<div class="ace-editor">

<textarea
class="ace_text-input"
wrap="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
>

</textarea>
<!-- 行号区域 -->
<div class="ace_gutter" aria-hidden="true">
<div
class="ace_layer ace_gutter-layer"
>

<div class="ace_gutter-cell" >1 <span></span></div>
</div>
</div>
<!-- 内容区域 -->
<div class="ace_scroller" >
<div class="ace_content">
<div class="ace_layer ace_print-margin-layer">
<div class="ace_print-margin" style="left: 580px; visibility: visible;"></div>
</div>
<div class="ace_layer ace_marker-layer">
<div class="ace_active-line"></div>
</div>
<div class="ace_layer ace_text-layer" >
<div class="ace_line" >
<span class="ace_keyword">select</span>
<span class="ace_keyword">from</span>
<span class="ace_string">'xxx'</span>
</div>
<div class="ace_line"></div>
</div>
<div class="ace_layer ace_marker-layer"></div>
<div class="ace_layer ace_cursor-layer ace_hidden-cursors">
<!-- 光标 -->
<div class="ace_cursor"></div>
</div>
</div>
</div>
<!-- 纵向滚动条 -->
<div class="ace_scrollbar ace_scrollbar-v">
<div class="ace_scrollbar-inner" >&nbsp;</div>
</div>
<!-- 横行滚动条 -->
<div class="ace_scrollbar ace_scrollbar-h">
<div class="ace_scrollbar-inner">&nbsp;</div>
</div>

</div>

4. 整体对比


4.1 功能完整度


类别Monaco EditorCode MirrorAce
代码主题内置 3 种,可扩展基于扩展来支持,现有官方 1 种内置 20+,可扩展
语言内置 70+, 可扩展基于扩展来支持,现有官方 16 种内置 110+,可扩展
代码提示/自动补全只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现基于扩展来支持,官方提供了自动补全的基础插件只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现
代码折叠
快捷键
多光标编辑
代码检查只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现基于扩展来支持,官方提供了代码检查的基础插件只支持 4 种语言,官方提供了自动补全的基础插件,可自行实现
代码对比❌,需自己扩展
MiniMap❌,需自己扩展❌,需自己扩展
多文本管理❌,需自己扩展
多视图❌,需自己扩展
协同编辑可引入额外插件支持 github.com/convergence…架构支持
移动端支持

4.2 性能体验


类别Monaco EditorCode MirrorAce
核心包大小800KB 左右核心包 115 KB 左右(未压缩)200KB 左右(不同版本有轻微出入)
编辑器渲染 (无代码)400ms 左右仅核心包情况下,120ms 左右185 ms 左右(实际使用包)

5. 结论与展望


一年前我们因为Monaco Editor丰富的生态、迅猛的迭代速度、开箱即用的特性和 VSCode 同款编辑器背书等原因选择了基于它来进行二次开发和插件化定制(后续文章会对这些定制开发做分享)。但由于编辑器的使用场景日渐多样化,个性化,以及移动端的占比日渐增加,我们对 Monaco Editor 的底层支持也越来越感觉到不足和乏力。对于这些点,我们的计划是先使用CodeMirror 6来支持移动端的代码编辑,然后逐步实

作者:pdai0001525
来源:juejin.cn/post/7252589598152851517
现代码编辑器的自研。

收起阅读 »

剑走偏锋,无头浏览器是什么神奇的家伙

web
浏览器是再熟悉不过的东西了,几乎每个人用过,比如 Chrome、FireFox、Safari,尤其是我们程序员,可谓开发最强辅助,摸鱼最好的伴侣。 浏览器能干的事儿,无头浏览器都能干,而且很多时候比标准浏览器还要更好用,而且能实现一些很好玩儿的功能,我们能借...
继续阅读 »

浏览器是再熟悉不过的东西了,几乎每个人用过,比如 Chrome、FireFox、Safari,尤其是我们程序员,可谓开发最强辅助,摸鱼最好的伴侣。



浏览器能干的事儿,无头浏览器都能干,而且很多时候比标准浏览器还要更好用,而且能实现一些很好玩儿的功能,我们能借助无头浏览器比肩标准浏览器强大的功能,而且又能灵活的用程序控制的特性,做出一些很有意思的产品功能来,稍后我们细说。


什么是浏览器


关于浏览器还有一个很好玩儿的梗,对于一些对计算机、对互联网不太了解的同学,你跟他说浏览器,他/她就默认是百度了,因为好多小白的浏览器都设置了百度为默认页面。所以很多小白将浏览器和搜索引擎(99%是百度)划等号了。



浏览器里我百分之99的时间都是用 Chrome,不过有一说一,这玩意是真耗内存,我基本上是十几、二十几个的 tab 开着,再加上几个 IDEA 进程,16G 的内存根本就不够耗的。


以 Chrome 浏览器为例,Chrome 由以下几部分组成:



  1. 渲染引擎(Rendering Engine):Chromium使用的渲染引擎主要有两个选项:WebKit和Blink。WebKit是最初由苹果开发的渲染引擎,后来被Google采用并继续开发。Blink则是Google从WebKit分支出来并进行独立开发的渲染引擎,目前Chromium主要使用Blink作为其默认的渲染引擎。

  2. JavaScript引擎(JavaScript Engine):Chromium使用V8引擎作为其JavaScript引擎。V8是由Google开发的高性能JavaScript引擎,它负责解析和执行网页中的JavaScript代码。

  3. 网络栈(Network Stack):Chromium的网络栈负责处理网络通信。它支持各种网络协议,包括HTTP、HTTPS、WebSocket等,并提供了网络请求、响应处理和数据传输等功能。

  4. 布局引擎(Layout Engine):Chromium使用布局引擎来计算网页中元素的位置和大小,并确定它们在屏幕上的布局。布局引擎将CSS样式应用于DOM元素,并计算它们的几何属性。

  5. 绘制引擎(Painting Engine):绘制引擎负责将网页内容绘制到屏幕上,生成最终的图像。它使用图形库和硬件加速技术来高效地进行绘制操作。

  6. 用户界面(User Interface):Chromium提供了用户界面的支持,包括地址栏、标签页、书签管理、设置等功能。它还提供了扩展和插件系统,允许用户根据自己的需求进行个性化定制。

  7. 其他组件:除了上述主要组件外,Chromium还包括其他一些辅助组件,如存储系统、安全模块、媒体处理、数据库支持等,以提供更全面的浏览器功能。


Chrome 浏览器光源码就有十几个G,2000多万行代码,可见,要实现一个功能完善的浏览器是一项浩大的工程。


什么是无头浏览器


无头浏览器(Headless Browser)是一种浏览器程序,没有图形用户界面(GUI),但能够执行与普通浏览器相似的功能。无头浏览器能够加载和解析网页,执行JavaScript代码,处理网页事件,并提供对DOM(文档对象模型)的访问和操作能力。


与传统浏览器相比,无头浏览器的主要区别在于其没有可见的窗口或用户界面。这使得它在后台运行时,不会显示实际的浏览器窗口,从而节省了系统资源,并且可以更高效地执行自动化任务。


常见的无头浏览器包括Headless Chrome(Chrome的无头模式)、PhantomJS、Puppeteer(基于Chrome的无头浏览器库)等。它们提供了编程接口,使开发者能够通过代码自动化控制和操作浏览器行为。


无头浏览器其实就是看不见的浏览器,所有的操作都要通过代码调用 API 来控制,所以浏览器能干的事儿,无头浏览器都能干,而且很多事儿做起来比标准的浏览器更简单。


我举几个常用的功能来说明一下无头浏览器的主要使用场景



  1. 自动化测试: 无头浏览器可以模拟用户行为,执行自动化测试任务,例如对网页进行加载、表单填写、点击按钮、检查页面元素等。

  2. 数据抓取: 无头浏览器可用于爬取网页数据,自动访问网站并提取所需的信息,用于数据分析、搜索引擎优化等。

  3. 屏幕截图: 无头浏览器可以加载网页并生成网页的截图,用于生成快照、生成预览图像等。

  4. 服务器端渲染: 无头浏览器可以用于服务器端渲染(Server-side Rendering),将动态生成的页面渲染为静态HTML,提供更好的性能和搜索引擎优化效果。

  5. 生成 PDF 文件:使用浏览器自带的生成 PDF 功能,将目标页面转换成 PDF 。


使用无头浏览器做一些好玩的功能


开篇就说了使用无头浏览器可以实现一些好玩儿的功能,这些功能别看不大,但是使用场景还是很多的,有些开发者就是抓住这些小功能,开发出好用的产品,运气好的话还能赚到钱,尤其是在国外市场。(在国内做收费的产品确实不容易赚到钱)


下面我们就来介绍两个好玩儿而且有用的功能。


前面的自动化测试、服务端渲染就不说了。


自动化测试太专业了,一般用户用不到,只有开发者或者测试工程师用。


服务端渲染使用无头浏览器确实没必要,因为有太多成熟的方案了,连 React 都有服务端渲染的能力(RSC)。


网页截图功能


我们可能见过一些网站提供下载文字卡片或者图文卡片的功能。比如读到一段想要分享的内容,选中之后将文本端所在的区域生成一张图片。



其实就是通过调用浏览器自身的 API page.screenshot,可以对整个页面或者选定的区域生成图片。


通过这个方法,我们可以做一个浏览器插件,用户选定某个区域后,直接生成对应的图片。这类功能在手机APP上很常见,在浏览器上一搬的网站都不提供。


说到这儿好像和无头浏览器都没什么关系吧,这都是标准浏览器中做的事儿,用户已经打开了页面,在浏览器上操作自己看到的内容,顺理成章。


但是如果这个操作是批量的呢,或者是在后台静默完成的情况呢?


那就需要无头浏览器来出手了,无头浏览器虽然没有操作界面,但是也具备绘制引擎的完整功能,仍然可以生成图像,利用这个功能,就可以批量的、静默生成图像了,并且可以截取完整的网页或者部分区域。


Puppeteer 是无头浏览器中的佼佼者,提供了简单好用的 API ,不过是 nodejs 版的。


如果是用 Java 开发的话,有一个替代品,叫做 Jvppeteer,提供了和 Puppeteer 几乎一模一样的 API。


下面这段代码就展示了如何用 Jvppeteer 来实现网页的截图。


下面这个方法是对整个网页进行截图,只需要给定网页 url 和 最终的图片路径就可以了。


public static boolean screenShotWx(String url, String path) throws IOException, ExecutionException, InterruptedException {
BrowserFetcher.downloadIfNotExist(null);
ArrayList arrayList = new ArrayList<>();
// MacOS 要这样写,指定Chrome的位置
String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
// Windows 和 Linux 这样就可以,不用指定 Chrome 的安装位置
//LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
arrayList.add("--no-sandbox");
arrayList.add("--disable-setuid-sandbox");
arrayList.add("--ignore-certificate-errors");
arrayList.add("--disable-gpu");
arrayList.add("--disable-web-security");
arrayList.add("--disable-infobars");
arrayList.add("--disable-extensions");
arrayList.add("--disable-bundled-ppapi-flash");
arrayList.add("--allow-running-insecure-content");
arrayList.add("--mute-audio");
Browser browser = Puppeteer.launch(options);
Page page = browser.newPage();
page.setJavaScriptEnabled(true);
page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37");
page.setCacheEnabled(true);
page.onConsole((msg) -> {
log.info("==> {}", msg.text());
});


PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
pageNavigateOptions.setTimeout(1000000);
//dom加载完毕就算导航完成
pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));
page.goTo(url, pageNavigateOptions, true);

autoScroll(page);
ElementHandle body = page.$("body");
double width = body.boundingBox().getWidth();
double height = body.boundingBox().getHeight();
Viewport viewport = new Viewport();

viewport.setWidth((int) width); // 设置视口宽度
viewport.setHeight((int) height + 100); // 设置视口高度
page.setViewport(viewport);
ScreenshotOptions screenshotOptions = new ScreenshotOptions();
screenshotOptions.setType("jpeg");
screenshotOptions.setFullPage(Boolean.FALSE);
//screenshotOptions.setClip(clip);
screenshotOptions.setPath(path);
screenshotOptions.setQuality(100);
// 或者转换为 base64
//String base64Str = page.screenshot(screenshotOptions);
//System.out.println(base64Str);

browser.close();
return true;
}

一个自动滚屏的方法。


虽然可以监听页面上的事件通知,比如 domcontentloaded,文档加载完成的通知,但是很多时候并不能监听到网页上的所有元素都加载完成了。对于那些滚动加载的页面,可以用这种方式模拟完全加载,加载完成之后再进行操作就可以了。


使用自动滚屏的操作,可以模拟我们人为的在界面上下拉滚动条的操作,随着滚动条的下拉,页面上的元素会自然的加载,不管是同步的还有延迟异步的,比如图片、图表等。


private static void autoScroll(Page page) {
if (page != null) {
try {
page.evaluate("() => {\n" +
" return new Promise((resolve, reject) => {\n" +
" //滚动的总高度\n" +
" let totalHeight = 0;\n" +
" //每次向下滚动的高度 500 px\n" +
" let distance = 500;\n" +
" let k = 0;\n" +
" let timeout = 1000;\n" +
" let url = window.location.href;\n" +
" let timer = setInterval(() => {\n" +
" //滚动条向下滚动 distance\n" +
" window.scrollBy(0, distance);\n" +
" totalHeight += distance;\n" +
" k++;\n" +
" console.log(`当前第${k}次滚动,页面高度: ${totalHeight}`);\n" +
" //页面的高度 包含滚动高度\n" +
" let scrollHeight = document.body.scrollHeight;\n" +
" //当滚动的总高度 大于 页面高度 说明滚到底了。也就是说到滚动条滚到底时,以上还会继续累加,直到超过页面高度\n" +
" if (totalHeight >= scrollHeight || k >= 200) {\n" +
" clearInterval(timer);\n" +
" resolve();\n" +
" window.scrollTo(0, 0);\n" +
" }\n" +
" }, timeout);\n" +
" })\n" +
" }");
} catch (Exception e) {

}
}
}

调用截图方法截图,这里是对一篇公众号文章进行整个网页的截图。


public static void main(String[] args) throws Exception {
screenShotWx("https://mp.weixin.qq.com/s/MzCyWqcH1TCytpnHI8dVjA", "/Users/fengzheng/Desktop/PICTURE/wx.jpeg");
}

或者也可以截取页面中的部分区域,比如某篇文章的正文部分,下面这个方法是截图一个博客文章的正文部分。


public static boolean screenShotJueJin(String url, String path) throws IOException, ExecutionException, InterruptedException {
BrowserFetcher.downloadIfNotExist(null);
ArrayList arrayList = new ArrayList<>();
String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();

//LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
arrayList.add("--no-sandbox");
arrayList.add("--disable-setuid-sandbox");
Browser browser = Puppeteer.launch(options);
Page page = browser.newPage();

PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
pageNavigateOptions.setTimeout(1000000);
//dom加载完毕就算导航完成
pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));
page.goTo(url, pageNavigateOptions, true);

WaitForSelectorOptions waitForSelectorOptions = new WaitForSelectorOptions();
waitForSelectorOptions.setTimeout(1000 * 15);
waitForSelectorOptions.setVisible(Boolean.TRUE);
// 指定截图的区域
ElementHandle elementHandle = page.waitForSelector("article.article", waitForSelectorOptions);
Clip clip = elementHandle.boundingBox();
Viewport viewport = new Viewport();
ElementHandle body = page.$("body");
double width = body.boundingBox().getWidth();
viewport.setWidth((int) width); // 设置视口宽度
viewport.setHeight((int) clip.getHeight() + 100); // 设置视口高度
page.setViewport(viewport);
ScreenshotOptions screenshotOptions = new ScreenshotOptions();
screenshotOptions.setType("jpeg");
screenshotOptions.setFullPage(Boolean.FALSE);
screenshotOptions.setClip(clip);
screenshotOptions.setPath(path);
screenshotOptions.setQuality(100);
// 或者生成图片的 base64编码
String base64Str = page.screenshot(screenshotOptions);
System.out.println(base64Str);
return true;
}


调用方式:


public static void main(String[] args) throws Exception {
screenShotJueJin("https://juejin.cn/post/7239715628172902437", "/Users/fengzheng/Desktop/PICTURE/juejin.jpeg");
}

最后的效果是这样的,可以达到很清晰的效果。



网页生成 PDF 功能


这个功能可太有用了,可以把一些网页转成离线版的文档。有人说直接保存网页不就行了,除了程序员,大部分人还是更能直接读 PDF ,而不会用离线存储的网页。


我们可以在浏览器上使用浏览器的「打印」功能,用来将网页转换成 PDF 格式。



但这是直接在页面上操作,如果是批量操作呢,比如想把一个专栏的所有文章都生成 PDF呢,就可以用无头浏览器来做了。


有的同学说,用其他的库也可以呀,Java 里面有很多生成 PDF 的开源库,可以把 HTML 转成 PDF,比如Apache PDFBox、IText 等,但是这些库应对一般的场景还行,对于那种页面上有延迟加载的图表啊、图片啊、脚本之类的就束手无策了。


而无头浏览器就可以,你可以监听页面加载完成的事件,可以模拟操作,主动触发页面加载,甚至还可以在页面中添加自定义的样式、脚本等,让生成的 PDF 更加完整、美观。


下面这个方法演示了如何将一个网页转成 PDF 。


public static boolean pdf(String url, String savePath) throws Exception {
Browser browser = null;
Page page = null;
try {
//自动下载,第一次下载后不会再下载
BrowserFetcher.downloadIfNotExist(null);
ArrayList arrayList = new ArrayList<>();
// MacOS
String executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
LaunchOptions options = new LaunchOptionsBuilder().withExecutablePath(executablePath).withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();
// windows 或 linux
//LaunchOptions options = new LaunchOptionsBuilder().withArgs(arrayList).withHeadless(true).withIgnoreHTTPSErrors(true).build();

arrayList.add("--no-sandbox");
arrayList.add("--disable-setuid-sandbox");
arrayList.add("--ignore-certificate-errors");
arrayList.add("--disable-gpu");
arrayList.add("--disable-web-security");
arrayList.add("--disable-infobars");
arrayList.add("--disable-extensions");
arrayList.add("--disable-bundled-ppapi-flash");
arrayList.add("--allow-running-insecure-content");
arrayList.add("--mute-audio");

browser = Puppeteer.launch(options);
page = browser.newPage();

page.onConsole((msg) -> {
log.info("==> {}", msg.text());
});

page.setViewport(viewport);
page.setJavaScriptEnabled(true);
page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36 Edg/83.0.478.37");
page.setCacheEnabled(true);

//设置参数防止检测
page.evaluateOnNewDocument("() =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => undefined } }) }");
page.evaluateOnNewDocument("() =>{ window.navigator.chrome = { runtime: {}, }; }");
page.evaluateOnNewDocument("() =>{ Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); }");
page.evaluateOnNewDocument("() =>{ Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5,6], }); }");

PageNavigateOptions pageNavigateOptions = new PageNavigateOptions();
pageNavigateOptions.setTimeout(1000000);
//dom加载完毕就算导航完成
pageNavigateOptions.setWaitUntil(Collections.singletonList("domcontentloaded"));

page.goTo(url, pageNavigateOptions, true);
// 添加自定义演示
StyleTagOptions styleTagOptions1 = new StyleTagOptions();
styleTagOptions1.setContent("html {-webkit-print-color-adjust: exact} .table > table > tr:nth-child(1),.table > table > tr:nth-child(2) {background: #4074b0;} #tableB td:nth-child(2) {width:60%;}");
page.addStyleTag(styleTagOptions1);

//滚屏
autoScroll(page);
Thread.sleep(1000);

PDFOptions pdfOptions = new PDFOptions();
// pdfOptions.setHeight("5200");
pdfOptions.setPath(savePath);
page.pdf(pdfOptions);

} catch (Exception e) {
log.error("生成pdf异常:{}", e.getMessage());
e.printStackTrace();
} finally {
if (page != null) {
page.close();
}
if (browser != null) {
browser.close();
}
}
return true;
}

调用生成 PDF 的方法,将一个微信公众号文章转成 PDF。


    public static void main(String[] args) throws Exception {
String pdfPath = "/Users/fengzheng/Desktop/PDF";
String filePath = pdfPath + "/hello.pdf";
JvppeteerUtils.pdf("https://mp.weixin.qq.com/s/MzCyWqcH1TCytpnHI8dVjA", filePath);
}

最终的效果,很清晰,样式都在,基本和页面一模一样。


作者:古时的风筝
来源:juejin.cn/post/7243780412547121208

收起阅读 »

面试官您好,这是我写的TodoList

web
前段时间看到掘金上有人二面被面试官要求写一个TodoList,今天趁着上班没啥事情,我也来写一个小Demo玩玩。 功能 一个TodoList大致就是长成这个样子,有一个输入框,可以通过输入任务名称进行新增,每个任务可以进行勾选,切换已完成和未完成状态,还可以...
继续阅读 »

前段时间看到掘金上有人二面被面试官要求写一个TodoList,今天趁着上班没啥事情,我也来写一个小Demo玩玩。


image.png


功能


一个TodoList大致就是长成这个样子,有一个输入框,可以通过输入任务名称进行新增,每个任务可以进行勾选,切换已完成和未完成状态,还可以删除。


组件设计


组件拆分


接下来,我们可以从功能层次上来拆分组件


image.png



  1. 最外层容器组件,只做一个统一的汇总(红色)

  2. 新增组件,管理任务的输入(绿色)

  3. 列表组件,管理任务的展示(紫色),同时我们也可以将每一个item拆分成为单独的组件(粉色)


数据流


组件拆分完毕之后,我们来管理一下数据流向,我们的数据应该存放在哪里?


我们的数据可以放在新增组件里面吗?不可以,我们的数据是要传递到列表组件进行展示的,他们两个是兄弟组件,管理起来非常不方便。同理,数据也不能放在列表组件里面。所以我们把数据放在我们的顶级组件里面去管理。


我们在最外层容器组件中把数据定义好,并写好删除,新增的逻辑,然后将数据交给列表组件进行展示,列表组件只管数据的展示,不管具体的实现逻辑,我只要把列表id抛出来,调用你传递的删除函数就可以了


现在,我们引出组件设计时的一些原则



  1. 从功能层次上拆分一些组件

  2. 尽量让组件原子化,一个组件只做一个功能就可以了,可以让组件吸收复杂度。每个组件都实现一部分功能,那么整个大复杂度的项目自然就被吸收了

  3. 区分容器组件和UI组件。容器组件来管理数据,具体的业务逻辑;UI组件就只管显示视图


image.png


数据结构的设计


一个合理的数据结构应该满足以下几点:



  1. 用数据描述所有的内容

  2. 数据要结构化,易于操作遍历和查找

  3. 数据要易于扩展,方便增加功能


[
{
id:"1",
title:'标题一',
completed:false
},
{
id:"2",
title:'标题二',
completed:false
}
]

coding


codesandbox.io/s/todolist-…


反思


看了下Antd表单组件的设计,它将一个Form拆分出了Form和Form.item


image.png


image.png


为什么要这么拆分呢?


上文说到,我们在设计一个组件的时候,需要从功能上拆分层次,尽量让组件原子化,只干一件事情。还可以让容器组件(只管理数据)和渲染组件(只管理视图)进行分离


通过Form表单的Api,我们可以发现,Form组件可以控制宏观上的布局,整个表单的样式和数据收集。Form.item控制每个字段的校验等。


个人拙见,如有

作者:晨出
来源:juejin.cn/post/7252678036692451388
不妥,还请指教!!!

收起阅读 »

给你十万条数据,给我顺滑的渲染出来!

web
前言 这是一道面试题,这个问题出来的一刹那,很容易想到的就是for循环100000次吧,但是这方案着实让浏览器崩溃啊!还有什么解决方案呢? 正文 1. for 循环100000次 虽说for循环有点low,但是,当面试官问,为什么会让浏览器崩溃的时候,你知道咋...
继续阅读 »

前言


这是一道面试题,这个问题出来的一刹那,很容易想到的就是for循环100000次吧,但是这方案着实让浏览器崩溃啊!还有什么解决方案呢?


正文


1. for 循环100000次


虽说for循环有点low,但是,当面试官问,为什么会让浏览器崩溃的时候,你知道咋解释吗?

来个例子吧,我们需要在一个容器(ul)中存放100000项数据(li):



我们的思路是打印js运行时间页面渲染时间,第一个console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间;第二个console.log是在 setTimeout 中的,它的触发时间是在渲染完成,在下一次Event Loop中执行的。



<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<ul id="ul"></ul>

<script>
let now = Date.now(); //Date.now()得到时间戳

const total = 100000
const ul = document.getElementById('ul')

for (let i = 0; i < total; i++) {
let li = document.createElement('li')
li.innerHTML = ~~(Math.random() * total)
ul.appendChild(li)
}
console.log('js运行时间',Date.now()-now);

setTimeout(()=>{
console.log('总时间',Date.now()-now);
},0)
console.log();
</script>
</body>

</html>

运行可以看到这个数据:


image.png

这渲染开销也太大了吧!而且它是十万条数据一起加载出来,没加载完成我们看到的会是一直白屏;在我们向下滑动过程中,页面也会有卡顿白屏现象,这就需要新的方案了。继续看!


2. 定时器


我们可以使用定时器实现分页渲染,我们继续拿上面那份代码进行优化:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<ul id="ul"></ul>

<script>
let now = Date.now(); //Date.now()得到时间戳

const total = 100000 //总共100000条数据
const once = 20 //每次插入20条
const page = total / once //总页数
let index = 1
const ul = document.getElementById('ul')

function loop(curTotal, curIndex) {
if (curTotal <= 0) { 判断总数居条数是否小于等于0
return false
}
let pageCount = Math.min(curTotal, once) //以便除不尽有余数
setTimeout(() => {
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i + ':' + ~~(Math.random() * total)
ul.appendChild(li)
}
loop(curTotal - pageCount, curIndex + pageCount)
}, 0)
}
loop(total, index)
</script>
</body>

</html>

运行后可以看到这十万条数据并不是一次性全部加载出来,浏览器右方的下拉条有顺滑的效果哦,如下图:


进度条.gif

但是当我们快速滚动时,页面还是会有白屏现象,如下图所示,这是为什么呢?


st.gif
可以说有两点原因:



  • 一是setTimeout的执行时间是不确定的,它属于宏任务,需要等同步代码以及微任务执行完后执行。

  • 二是屏幕刷新频率受分辨率和屏幕尺寸影响,而setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕刷新时间相同。


3. requestAnimationFrame


我们这次采用requestAnimationFrame的方法,它是一个用于在下一次浏览器重绘之前调用指定函数的方法,它是 HTML5 提供的 API。



我们插入一个小知识点, requestAnimationFrame 和 setTimeout 的区别:

· requestAnimationFrame的调用频率通常为每秒60次。这意味着我们可以在每次重绘之前更新动画的状态,并确保动画流畅运行,而不会对浏览器的性能造成影响。

· setIntervalsetTimeout它可以让我们在指定的时间间隔内重复执行一个操作,不考虑浏览器的重绘,而是按照指定的时间间隔执行回调函数,可能会被延迟执行,从而影响动画的流畅度。



还有一个问题,我们多次创建li挂到ul上,这样会导致回流,所以我们用虚拟文档片段的方式去优化它,因为它不会触发DOM树的重新渲染!


<!DOCTYPE html>
<html lang="en">

![rf.gif](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3eab42b37f53408b981411ee54088d5a~tplv-k3u1fbpfcp-watermark.image?)
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

![st.gif](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3e922cc57a044f5e9e48e58bda5f6756~tplv-k3u1fbpfcp-watermark.image?)
<body>
<ul id="ul"></ul>

<script>
let now = Date.now(); //Date.now()得到时间戳

const total = 10000
const once = 20
const page = total / once
let index = 1
const ul = document.getElementById('ul')

function loop(curTotal, curIndex) {
if (curTotal <= 0) {
return false
}
let pageCount = Math.min(curTotal, once) //以便除不尽有余数
requestAnimationFrame(()=>{
let fragment = document.createDocumentFragment() //虚拟文档
for (let i = 0; i < pageCount; i++) {
let li = document.createElement('li')
li.innerHTML = curIndex + i + ':' + ~~(Math.random() * total)
fragment.appendChild(li)
}
ul.appendChild(fragment)
loop(curTotal - pageCount, curIndex + pageCount)
})
}
loop(total, index)
</script>
</body>

</html>

可以看到它白屏时间没有那么长了:
rqf.gif

还有没有更好的方案呢?当然有!往下看!


4. 虚拟列表


我们可以通过这张图来表示虚拟列表红框代表你的手机黑条代表一条条数据


image.png

思路:我们只要知道手机屏幕最多能放下几条数据,当下拉滑动时,通过双指针的方式截取相应的数据就可以了。

🚩 PS:为了防止滑动过快导致的白屏现象,我们可以使用预加载的方式多加载一些数据出来。



代码如下:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<title>虚拟列表</title>
<style>
.v-scroll {
height: 600px;
width: 400px;
border: 3px solid #000;
overflow: auto;
position: relative;
-webkit-overflow-scrolling: touch;
}

.infinite-list {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}

.scroll-list {
left: 0;
right: 0;
top: 0;
position: absolute;
text-align: center;
}

.scroll-item {
padding: 10px;
color: #555;
box-sizing: border-box;
border-bottom: 1px solid #999;
}
</style>
</head>

<body>
<div id="app">
<div ref="list" class="v-scroll" @scroll="scrollEvent($event)">
<div class="infinite-list" :style="{ height: listHeight + 'px' }"></div>

<div class="scroll-list" :style="{ transform: getTransform }">
<div ref="items" class="scroll-item" v-for="item in visibleData" :key="item.id"
:style="{ height: itemHeight + 'px',lineHeight: itemHeight + 'px' }">
{{ item.msg }}</div>
</div>
</div>
</div>

<script>
var throttle = (func, delay) => { //节流
var prev = Date.now();
return function () {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
let listData = []
for (let i = 1; i <= 10000; i++) {
listData.push({
id: i,
msg: i + ':' + Math.floor(Math.random() * 10000)
})
}

const { createApp } = Vue
createApp({
data() {
return {
listData: listData,
itemHeight: 60,
//可视区域高度
screenHeight: 600,
//偏移量
startOffset: 0,
//起始索引
start: 0,
//结束索引
end: null,
};
},
computed: {
//列表总高度
listHeight() {
return this.listData.length * this.itemHeight;
},
//可显示的列表项数
visibleCount() {
return Math.ceil(this.screenHeight / this.itemHeight)
},
//偏移量对应的style
getTransform() {
return `translate3d(0,${this.startOffset}px,0)`;
},
//获取真实显示列表数据
visibleData() {
return this.listData.slice(this.start, Math.min(this.end, this.listData.length));
}
},
mounted() {
this.start = 0;
this.end = this.start + this.visibleCount;
},
methods: {
scrollEvent() {
//当前滚动位置
let scrollTop = this.$refs.list.scrollTop;
//此时的开始索引
this.start = Math.floor(scrollTop / this.itemHeight);
//此时的结束索引
this.end = this.start + this.visibleCount;
//此时的偏移量
this.startOffset = scrollTop - (scrollTop % this.itemHeight);
}
}
}).mount('#app')
</script>
</body>

</html>

可以看到白屏现象解决了!


zz.gif

结语


解决十万条数据渲染的方案基本都在这儿了,还有更好

作者:zt_ever
来源:juejin.cn/post/7252684645979111461
的方案等待大佬输出!

收起阅读 »

记一次修改一行代码导致的线上BUG

web
背景介绍 先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式,type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班! 《凉凉》送给自己 看标题就知道结果了,第二天下午...
继续阅读 »

1920_1200_20100319011154682575.jpg


背景介绍


先描述一下需求,要在一个老项目里根据type类型,给一个试题题干组件新增一个class样式type是在url地址栏上面携带的。简单,一行代码搞定,五分钟部署,十分钟留给测试,然后跟车上线,打卡下班!


《凉凉》送给自己


看标题就知道结果了,第二天下午现网问题来了,一线反馈某个页面题干不展示了,值班同事排查一圈,找到我说我昨天加的代码报错了!


006Cmetyly1ff16b3zxvxj308408caa8.jpg


惊了,就加了一行业务代码,其他都是样式,测试也通过了,这也能有问题?绩效C打底稳了(为方便写文章,实际判断用变量代替):


<div :class="{'addClass': $route.query.type === 'xx'}">
...
</div>

temp.png
问题其实很简单,$route为undefined了,导致query获取有问题,这让我点怀疑自己,难道这写错了?管不了太多,只能先兼容上线了。


$route && $route.query && $route.query.type

其实是可以用?.简写的,但是这个项目实在不“感动”了,保险写法,解决问题优先。提申请,拉评审,走流程,上线,问题解决,松口气,C是保住了。


问题分析


解决完问题,还要写线上问题分析报告,那只能来扒一扒代码来看看了。首先,这个项目使用的是多页应用,每个页面都是一个新的SPA,我改的页面先叫组件A吧,组件A在页面A里被使用,没问题;组件A同样被页面B使用,报错了。那接下来简单了,看代码:


// 2022-09-26 新增
import App from '@/components/pages/页面A'
import router from '@/config/router.js'
// initApp 为封装的 new Vue
import { initApp, Vue } from '../base-import'
initApp(App, router)

// 2020-10-18 新增
import App from '@/components/pages/页面b'
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})

两个页面的index.js文件,两种写法,一个引用了router,一个没有引用,被这个神仙代码整懵了。然后再看了一下其他页面,也都是两种写法掺着写的,心态崩了。这分析报告只能含着泪写了...


最后总结



  1. 问题不是关键,关键的是代码规范;

  2. 修改新项目之前,最好看一下代码逻辑,有熟悉的同事最好,可以沟通了解一下业务(可以避免部分问题);

  3. 当想优化之前代码的时候,要全面评估,统一优化,上面的写法我也找同事了解了,因为之前写法不满足当时的需求,他就封装了新方法,但是老的没有修改,所以就留了坑;


作者:追风筝的呆子
来源:juejin.cn/post/7252198762625089596
收起阅读 »

什么!一个项目给了8个字体包???

web
🙋 遇到的问题 在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。 首先,字体包的使用分为了以下几种情况: 无特殊要求的语言使用字体A,阿拉伯语言使用字体B; 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用...
继续阅读 »

🙋 遇到的问题


在一个新项目中,设计统一了项目中所有的字体,并提供了字体包。在项目中需要按需引入这些字体包。


首先,字体包的使用分为了以下几种情况:



  1. 无特殊要求的语言使用字体A,阿拉伯语言使用字体B;

  2. 加粗、中等、常规、偏细四种样式,AB两种字体分别对应使用 BoldMediumRegularThin 四种字体包;


所以,我现在桌面上摆着 8 个字体包:



  • A-Bold.tff

  • A-Medium.tff

  • A-Regular.tff

  • A-Thin.tff

  • B-Bold.tff

  • B-Medium.tff

  • B-Regular.tff

  • B-Thin.tff


image.png
不同语言要使用不同的字体包,不同粗细也要使用不同的字体包!


还有一个前提是,设计给的设计图都是以字体A为准,所以在 Figma 中复制出来的 CSS 代码中字体名称都是A。


刚接到这个需求时还是比较懵的,一时想不出来怎么样才能以最少的逻辑判断最少的文件下载最少的代码改动去实现在不同情况下自动的去选择对应的字体包。


因为要涉及到语言的判断,最先想到的还是通过 JS,然后去添加相应的类名。但这样也只能判断语言使用A或B,粗细还是解决不了。


image.png


看来还是要用 CSS 解决。


首先我将所有的8个字体先定义好:


@font-face {
font-family: A-Bold;
src: url('./fonts/A-Bold.ttf');
}

/* ... */

@font-face {
font-family: B-Thin;
src: url('./fonts/B-Thin.ttf');
}

image.png


🤲🏼 如何根据粗细程度自动选择对应字体包


有同学可能会问,为什么不直接使用 font-weight 来控制粗细而是用不同的字体包呢?


我们来看下面这个例子,我们使用同一个字体, font-weight 分别设置为900、500、100,结果我们看到的字体粗细是一样的。


对的,很多字体不支持 font-weight 所以我们需要用不同粗细的字体包。


image.png


所以,我们可以通过 @font-face 中的 font-weight 属性来设置字体的宽度:


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}
@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}
@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}
@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

注意,这里我们把字体名字都设为相同的,如下图所示,这样我们就成功的解决了第一个问题:不同粗细也要使用不同的字体包;


image.png


并且,如果我们只是定义而未真正使用时,不会去下载未使用的字体包,再加上字体包的缓存策略,就可以最大程度节省带宽:


image.png


🔤 如何根据不同语言自动选择字体包?


通过张鑫旭的博客找到了解决办法,使用 unicode-range 设置字符 unicode 范围,从而自定义字体包。


unicode-range 是一个 CSS 属性,用于指定字体文件所支持的 Unicode 字符范围,以便在显示文本时选择适合的字体。


它的语法如下:


@font-face {
font-family: "Font Name";
src: url("font.woff2") format("woff2");
unicode-range: U+0020-007E, U+4E00-9FFF;
}

在上述例子中,unicode-range 属性指定了字体文件支持的字符范围。使用逗号分隔不同的范围,并使用 U+XXXX-XXXX 的形式表示 Unicode 字符代码的范围。


通过设置 unicode-range 属性,可以优化字体加载和页面渲染性能,只加载所需的字符范围,减少不必要的网络请求和资源占用。


通过查表得知阿拉伯语的 unicode 的范围为:U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F 这么几个区间。所以我们设置字体如下,因为设计以 A 字体为准,所以在 Figma 中给出的样式代码字体名均为 A,所以我们把 B 字体的字体名也设置为 A:


image.png


当使用字体的字符中命中 unicode-rang 的范围时,自动下载相应的字体包。


@font-face {
font-family: A;
src: url('./fonts/A-Bold.ttf');
font-weight: 600;
}

@font-face {
font-family: A;
src: url('./fonts/A-Medium.ttf');
font-weight: 500;
}

@font-face {
font-family: A;
src: url('./fonts/A-Regular.ttf');
font-weight: 400;
}

@font-face {
font-family: A;
src: url('./fonts/A-Thin.ttf');
font-weight: 300;
}

:root {
--ARABIC_UNICODE_RANGE: U+06??, U+0750-077F, U+08A0-08FF, U+FB50-FDFF, U+FE70-FEFF, U+10A60-10A7F, U+10A80-10A9F;
}
@font-face {
font-family: A;
src: url('./fonts/B-Bold.ttf');
font-weight: 600;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Medium.ttf');
font-weight: 500;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Regular.ttf');
font-weight: 400;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
@font-face {
font-family: A;
src: url('./fonts/B-Thin.ttf');
font-weight: 300;
unicode-range: var(--ARABIC_UNICODE_RANGE);
}
p {
font-family: A;
}

总结


遇到的问题:



  1. 两种字体,B 字体为阿拉伯语使用,A 字体其他语言使用。根据语言自动选择。

  2. 根据字宽自动选择相应的字体包。

  3. 可以直接使用 Figma 中生成的样式而不必每次手动改动。

  4. 尽可能节省带宽。


我们通过 font-weight 解决了问题2,并通过 unicode-range 解决了问题1。


并且实现了按需下载相应字体包,不使用时不下载。


Figma 中的代码可以直接复制粘贴,无需任何修改即可根据语言和自宽自动使用相应字体包。




参考资料:http://www.zhangxinxu.com/wordpr

作者:Mengke
来源:juejin.cn/post/7251884086536781880
ess/2…

收起阅读 »

用 node 实战一下 CSRF

web
前言 之前面试经常被问到 CSRF, 跨站请求伪造 大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为...
继续阅读 »

前言


之前面试经常被问到 CSRF, 跨站请求伪造



大概流程比较简单, 大概就是用户登录了A页面,存下来登录凭证(cookie), 攻击者有诱导受害者打开了B页面, B页面中正好像A发送了一个跨域请求,并把cookie进行了携带, 欺骗浏览器以为是用户的行为,进而达到执行危险行为的目的,完成攻击



上面就是面试时,我们通常的回答, 但是到底是不是真是这样呢? 难道这么容易伪造吗?于是我就打算试一下能不能实现


接下来,我们就通过node起两个服务 A服务(端口3000)和B服务(端口4000), 然后通过两个页面 A页面、和B页面模拟一下CSRF。


我们先约定一下 B页面是正常的页面, 起一个 4000 的服务, 然后 A页面为伪造者的网站, 服务为3000


先看B页面的代码, B页面有一个登录,和一个获取数据的按钮, 模拟正常网站,需要登录后才可以获取数据


<body>
<div>
正常 页面 B
<button onclick="login()">登录</button>
<button onclick="getList()">拿数据</button>
<ul class="box"></ul>
<div class="tip"></div>
</div>
</body>
<script>
async function login() {
const response = await fetch("http://localhost:4000/login", {
method: "POST",
});
const res = await response.json();
console.log(res, "writeCookie");
if (res.data === "success") {
document.querySelector(".tip").innerHTML = "登录成功, 可以拿数据";
}
}

async function getList() {
const response = await fetch("http://localhost:4000/list", {
method: "GET",
});

if (response.status === 500) {
document.querySelector(".tip").innerHTML = "cookie失效,请先登录!";
document.querySelector(".box").innerHTML = "";
} else {
document.querySelector(".tip").innerHTML = "";
const data = await response.json();
let html = "";
data.map((el) => {
html += `<div>${el.id} - ${el.name}</div>`;
});
document.querySelector(".box").innerHTML = html;
}
}
</script>

在看B页面的服务端代码如下:


const express = require("express");
const app = express();

app.use(express.json()); // json
app.use(express.urlencoded({ extends: true })); // x-www-form-urlencoded

app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
// 允许客户端跨域传递的请求头
res.header("Access-Control-Allow-Headers", "Content-Type");
next();
});

app.use(express.static("public"));

app.get("/list", (req, res) => {
const cookie = req.headers.cookie;
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json([
{ id: 1, name: "zhangsan" },
{ id: 2, name: "lisi" },
]);
}
});

app.post("/login", (req, res) => {
res.cookie("user", "allow", {
expires: new Date(Date.now() + 86400 * 1000),
});
res.send({ data: "success" });
});

app.post("/delete", (req, res) => {
const cookie = req.headers.cookie;
if (req.headers.referer !== req.headers.host) {
console.log("should ban!");
}
if (cookie !== "user=allow") {
res.sendStatus("500");
} else {
res.json({
data: "delete success",
});
}
});

app.listen(4000, () => {
console.log("sever 4000");
});

B 服务有三个接口, 登录、获取列表、删除。 再触发登录接口的时候,会像浏览器写入cookie, 再删除或者获取列表的时候,都先检测有没有将指定的cookie传回,如果有就认为有权限


然后我们打开 http://localhost:4000/B.html 先看看B页面功能是否都正常


image.png


我们看到此时 B 页面功能和接口都是正常的, cookie 也正常进行了设置,每次获取数据的时候,都是会携带cookie到服务端校验的


那么接下来我们就通过A页面,起一个3000端口的服务,来模拟一下跨域情况下,能否完成获取 B服务器数据,调用 B 服务器删除接口的功能


A页面代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台执行,便于观察效果
// document.forms[0].submit();
</script>
</div>
<ul class="box"></ul>
<div class="tip"></div>
</body>

A页面服务端代码


  <body>
<div>
伪造者页面 A
<form action="http://localhost:4000/delete" method="POST">
<input type="hidden" name="account" value="xiaoming" />
</form>
<script>
// 这行可以放到控制台输入
// document.forms[0].submit();
</script>
<script src="http://localhost:4000/list"></script>
</div>

</body>

于是在我们 访问 http://localhost:3000/A.html 页面的时候发现, 发现list列表确实,请求到了, 控制台输入 document.forms[0].submit() 时发现,确实删除也发送成功了, 是不是说明csrf就成功了呢, 但是其实还不是, 关键的一点是, 我们在B页面设置cookie的时候, domain设置的是 localhost 那么其实在A页面, 发送请求的时候cookie是共享的状态, 真实情况下,肯定不会是这样, 那么为了模拟真实情况, 我们把 http://localhost:3000/A.html 改为 http://127.0.0.1:3000/A.html, 这时发现,以及无法访问了, 那么这是怎么回事呢, 说好的,cookie 会在获取过登录凭证下, 再次访问时可以携带呢。


image.png


于是,想了半天也没有想明白, 难道是浏览器限制严格进行了限制, 限制规避了这个问题? 难道我们背的面试题是错误的?


有知道的

作者:重阳微噪
来源:juejin.cn/post/7250374485567340603
小伙伴,欢迎下方讨论

收起阅读 »

前端流程图插件对比选型

web
前言 前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差...
继续阅读 »

Snipaste_2023-07-04_15-49-12.png


前言


前端领域有多种流程库可供选择,包括但不限于vue-flow、butterfly、JointJS、AntV G6、jsPlumb和Flowchart.js。这些库都提供了用于创建流程图、图形编辑和交互的功能。然而,它们在特性、易用性和生态系统方面存在一些差异。


流程图插件汇总


序号名称地址
1vue-flowgithub.com/bcakmakoglu…
2butterflygithub.com/alibaba/but…
3JointJShttp://www.jointjs.com/
4AntV G6antv-2018.alipay.com/zh-cn/g6/3.…
5jsPlumbgithub.com/jsplumb/jsp…
6Flowchart.jsgithub.com/adrai/flowc…

流程图插件分析


vue-flow


简介


vue-flowReactFlow 的 Vue 版本,目前只支持 在Vue3中使用,对Vue2不兼容,目前国内使用较少。包含四个功能组件 core、background、controls、minimap,可按需使用。


使用


Vue FlowVue下流程绘制库。安装:
npm i --save @vue-flow/core 安装核心组件
npm i --save @vue-flow/background 安装背景组件
npm i --save @vue-flow/controls 安装控件(放大,缩小等)组件
npm i --save @vue-flow/minimap 安装缩略图组件

引入组件:
import { Panel, PanelPosition, VueFlow, isNode, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'

引入样式:
@import '@vue-flow/core/dist/style.css';
@import '@vue-flow/core/dist/theme-default.css';

优缺点分析


优点:



  1. 轻松上手:内置缩放和平移功能、元素拖动、选择等等。

  2. 可定制:使用自定义节点、边缘和连接线并扩展Vue Flow的功能。

  3. 快速:链路被动更改,仅重新渲染适当的元素。

  4. 工具和组合:带有图形助手和状态可组合函数,用于高级用途。

  5. 附加组件:背景(内置模式、高度、宽度或颜色),小地图(右下角)、控件(左下角)。


缺点:



  1. 仓库迭代版本较少,2022年进入首次迭代。

  2. 国内使用人数少,没有相关技术博客介绍,通过官网学习。


butterfly


简介


Butterfly是由阿里云-数字产业产研部孵化出来的的图编辑器引擎,具有使用自由、定制性高的优势,已支持上百张画布。号称 “杭州余杭区最自由的图编辑器引擎”。


使用



  • 安装


//
npm install butterfly-dag --save


  • 在 Vue3 中使用


<script lang="ts" setup>
import {TreeCanvas, Canvas} from 'butterfly-dag';
const root = document.getElementById('chart')
const canvas = new Canvas({
root: root,
disLinkable: true, // 可删除连线
linkable: true, // 可连线
draggable: true, // 可拖动
zoomable: true, // 可放大
moveable: true, // 可平移
theme: {
edge: {
shapeType: "AdvancedBezier",
arrow: true,
arrowPosition: 0.5, //箭头位置(0 ~ 1)
arrowOffset: 0.0, //箭头偏移
},
},
});
canvas.draw(mockData, () => {
//mockData为从mock中获取的数据
canvas.setGridMode(true, {
isAdsorb: false, // 是否自动吸附,默认关闭
theme: {
shapeType: "circle", // 展示的类型,支持line & circle
gap: 20, // 网格间隙
background: "rgba(0, 0, 0, 0.65)", // 网格背景颜色
circleRadiu: 1.5, // 圆点半径
circleColor: "rgba(255, 255, 255, 0.8)", // 圆点颜色
},
});
});
</script>

<template>
<div class="litegraph-canvas" id="chart"></div>
</template>

优缺点分析


优点:



  1. 轻松上手:基于dom的设计模型大大方便了用户的入门门槛,提供自定义节点,锚点的模式大大降低了用户的定制性。

  2. 多技术栈支持:支持 jquery 基于 dom 的设计,也包含 butterfly-react、butterfly-vue 两种设计。

  3. 核心概念少而精:提供 画布(Canvas)、节点(Node)、线(Edge)等核心概念。

  4. 优秀的组件库支持:对于当前使用组件库来说,可以大量复用现有的组件。


缺点:



  1. butterfly 对 Vue的支持不是特别友好,这跟阿里的前端技术主栈为React有关,butterfly-vue库只支持 Vue2版本。在Vue3上使用需要对 butterfly-drag 进行封装。


JointJS


简介


创建静态图表或完全交互式图表工具,例如工作流编辑器、流程管理工具、IVR 系统、API 集成器、演示应用程序等等。


属于闭源收费项目,暂不考虑。


AntV G6


简介


AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便、专业可靠、无限可能的数据可视化最佳实践。G6 是一个图可视化引擎。它提供了图的绘制、布局、分析、交互、动画等图可视化的基础能力。G6可以实现很多d3才能实现的可视化图表。


使用



  • 安装


npm install --save @antv/g6	//安装


  • 在所需要的文件中引入


<template>
/* 图的画布容器 */
<div id="mountNode"></div>
</template>

<script lang="ts" setup>
import G6 from '@antv/g6';
// 定义数据源
const data = {
// 点集
nodes: [
{
id: 'node1',
x: 100,
y: 200,
},
{
id: 'node2',
x: 300,
y: 200,
},
],
// 边集
edges: [
// 表示一条从 node1 节点连接到 node2 节点的边
{
source: 'node1',
target: 'node2',
},
],
};

// 创建 G6 图实例
const graph = new G6.Graph({
container: 'mountNode', // 指定图画布的容器 id
// 画布宽高
width: 800,
height: 500,
});
// 读取数据
graph.data(data);
// 渲染图
graph.render();
</script>



优缺点分析


优点:



  1. 强大的可定制性:G6 提供丰富的图形表示和交互组件,可以通过自定义配置和样式来实现各种复杂的图表需求。

  2. 全面的图表类型支持:G6 支持多种常见图表类型,如关系图、流程图、树图等,可满足不同领域的数据可视化需求。

  3. 高性能:G6 在底层图渲染和交互方面做了优化,能够处理大规模数据的展示,并提供流畅的交互体验。


缺点:



  1. 上手难度较高:G6 的学习曲线相对较陡峭,需要对图形语法和相关概念有一定的理解和掌握。

  2. 文档相对不完善:相比其他成熟的图表库,G6 目前的文档相对较简单,部分功能和使用方法的描述可能不够详尽,需要进行更深入的了解与实践。


jsPlumb


简介


一个用于创建交互式、可拖拽的连接线和流程图的 JavaScript 库。它在 Web 应用开发中广泛应用于构建流程图编辑器、拓扑图、组织结构图等可视化操作界面。


使用


<template>
<div ref="container">
<div ref="sourceElement">Source</div>
<div ref="targetElement">Target</div>
</div>

</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { jsPlumb } from 'jsplumb';

const container = ref<HTMLElement | null>(null);
const sourceElement = ref<HTMLElement | null>(null);
const targetElement = ref<HTMLElement | null>(null);

onMounted(() => {
// 创建 jsPlumb 实例
const jsPlumbInstance = jsPlumb.getInstance();

// 初始化 jsPlumb 实例设置
if (container.value) {
jsPlumbInstance.setContainer(container.value);
}

// 创建连接线
if (sourceElement.value && targetElement.value) {
jsPlumbInstance.connect({
source: sourceElement.value,
target: targetElement.value,
});
}
});
</script>

优缺点分析


优点:



  1. 简单易用:jsPlumb 提供了直观的 API 和丰富的文档,比较容易上手和使用。

  2. 可拓展性:允许开发人员根据自己的需求进行定制和扩展,使其适应不同的应用场景。

  3. 强大的连接功能:jsPlumb 允许创建各种连接类型,包括直线、曲线和箭头等,满足了复杂交互需求的连接效果。
    缺点:

  4. 文档更新不及时:有时候,jsPlumb 的官方文档并没有及时更新其最新版本的特性和用法。

  5. 性能考虑:在处理大量节点、连接线或复杂布局时,jsPlumb 的性能可能受到影响,需要进行优化。


Flowchart.js


简介


Flowchart.js 是一款开源的JavaScript流程图库,可以使用最短的语法来实现在页面上展示一个流程图,目前大部分都是用在各大主流 markdown 编辑器中,如掘金、csdn、语雀等等。


使用


flowchat
start=>start: 开始
end=>end: 结束
input=>inputoutput: 我的输入
output=>inputoutput: 我的输出
operation=>operation: 我的操作
condition=>condition: 确认
start->input->operation->output->condition
condition(yes)->end
condition(no)->operation

优缺点


优点:



  1. 使用方便快捷,使用几行代码就可以生成一个简单的流程图。

  2. 可移植:在多平台上只需要写相同的代码就可以实现同样的效果。


缺点:



  1. 可定制化限制:对于拥有丰富需求的情况下,flowchartjs只能完成相对简单的需求,没有高级的定制化功能。

  2. 需要花费一定时间来学习他的语法和规则,但是flowchartjs的社区也相对不太活跃。


对比分析




  1. 功能和灵活性:



    • Butterfly、G6 和 JointJS 是功能较为丰富和灵活的库。它们提供了多种节点类型、连接线样式、布局算法等,并支持拖拽、缩放、动画等交互特性。

    • Vue-Flow 来源于 ReactFlow 基于 D3和vueuse等库,提供了 Vue 组件化的方式来创建流程图,并集成了一些常见功能。

    • jsPlumb 专注于提供强大的连接线功能,具有丰富的自定义选项和功能。

    • Flowchart.js 则相对基础,提供了构建简单流程图的基本功能。




  2. 技术栈和生态系统:



    • Vue-Flow 是基于 Vue.js 的流程图库,与 Vue.js 生态系统无缝集成。

    • Butterfly 是一个基于 TypeScript 的框架,适用于现代 Web 开发。

    • JointJS、AntV G6 和 jsPlumb 可以与多种前端框架(如Vue、React、Angular等)结合使用。

    • AntV G6 是 AntV 团队开发的库,其背后有强大的社区和文档支持。




  3. 文档和学习曲线:



    • Butterfly、G6 和 AntV G6 都有完善的文档和示例,提供了丰富的使用指南和教程。

    • JointJS 和 jsPlumb 也有较好的文档和示例资源,但相对于前三者较少。

    • Flowchart.js 的文档相对较少。




  4. 兼容性:



    • Butterfly、JointJS 和 G6 库在现代浏览器中表现良好,并提供了兼容低版本浏览器

    • 作者:WayneX
      来源:juejin.cn/post/7251835247595110457
      l>

收起阅读 »

为什么选择 Next.js 框架?

web
前言 Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。 文档:nextjs.org/docs 强大的服务端渲染和静态生成能力: Ne...
继续阅读 »

前言


Next.js 框架作为一种强大而受欢迎的工具,为开发人员提供了许多优势和便利。本文将探讨 Next.js 框架的优点,并解释为什么选择 Next.js 是一个明智的决策。



文档:nextjs.org/docs



强大的服务端渲染和静态生成能力:


Next.js 框架提供了先进的服务端渲染(SSR)和静态生成(SSG)能力,使得我们能够在服务器上生成动态内容并将其直接发送给客户端,从而大大减少首次加载的等待时间。这样可以提高网站的性能、搜索引擎优化(SEO)以及用户体验。


简化的数据获取:


Next.js 提供了简单易用的数据获取方法,例如 getServerSidePropsgetStaticProps,使得从后端获取数据并将其注入到组件中变得非常容易。这种无缝的数据获取流程,可以让开发人员专注于业务逻辑而不用过多关注数据获取的细节。


优化的路由系统:


Next.js 内置了灵活而强大的路由功能,使得页面之间的导航变得简单直观。通过自动化的路由管理,我们可以轻松地构建复杂的应用程序,并实现更好的用户导航体验。


支持现代前端技术栈:


Next.js 是建立在 React 生态系统之上的,因此可以充分利用 React 的强大功能和丰富的社区资源。同时,Next.js 也支持最新的 JavaScript(ES6+)特性,如箭头函数、模块化导入导出、解构赋值等,让开发人员可以使用最新的前端技术来构建现代化的应用。


简化的部署和扩展:


Next.js 提供了轻松部署和扩展应用程序的工具和解决方案。借助 Vercel、Netlify 等平台,我们可以快速将应用程序部署到生产环境,并享受高性能、弹性扩展的好处。Next.js 还支持构建静态站点,可以轻松地将应用部署到 CDN 上,提供更快的加载速度和更好的全球可访问性。


大型社区支持:


Next.js 拥有庞大的开发者社区,其中有许多优秀的开源项目和库。这意味着你可以从社区中获取到大量的学习资源、文档和支持。无论是在 Stack Overflow 上寻求帮助,还是参与讨论,你都能够从其他开发人员的经验中获益。


什么环境下需要选择nextjs框架?


需要服务端渲染或静态生成:


如果你的应用程序需要在服务器端生成动态内容,并将其直接发送给客户端,以提高性能和搜索引擎优化,那么 Next.js 是一个很好的选择。它提供了强大的服务端渲染和静态生成能力,使得构建高性能的应用变得更加简单。


需要快速开发和部署:


Next.js 提供了简化的开发流程和快速部署的解决方案。它具有自动化的路由管理、数据获取和构建工具,可以提高开发效率。借助 Vercel、Netlify 等平台,你可以轻松地将 Next.js 应用部署到生产环境,享受高性能和弹性扩展的好处。


基于 React 的应用程序:


如果你已经熟悉 React,并且正在构建一个基于 React 的应用程序,那么选择 Next.js 是自然而然的。Next.js 是建立在 React 生态系统之上的,提供了与 React 紧密集成的功能和工具。


需要良好的 SEO 和页面性能:


如果你的应用程序对搜索引擎优化和良好的页面性能有较高的要求,Next.js 可以帮助你实现这些目标。通过服务端渲染和静态生成,Next.js 可以在初始加载时提供完整的 HTML 内容,有利于搜索引擎索引和页面的快速呈现。


需要构建现代化的单页应用(SPA):


尽管 Next.js 可以支持传统的多页面应用(MPA),但它也非常适合构建现代化的单页应用(SPA)。你可以使用 Next.js 的路由系统、数据获取和状态管理功能,构建出功能丰富且响应快速的 SPA。


与nextjs相似的框架?


Nuxt.js:


Nuxt.js 是一个基于 Vue.js 的应用框架,提供了类似于 Next.js 的服务端渲染和静态生成功能。它通过使用 Vue.js 的生态系统,使得构建高性能、可扩展的 Vue.js 应用变得更加简单。


Gatsby:


Gatsby 是一个基于 React 的静态网站生成器,具有类似于 Next.js 的静态生成功能。它使用 GraphQL 来获取数据,并通过预先生成静态页面来提供快速的加载速度和良好的SEO。


Angular Universal:


Angular Universal 是 Angular 框架的一部分,提供了服务端渲染的能力。它可以生成动态的 HTML 内容,从而加快首次加载速度,并提供更好的 SEO 和用户体验。


Sapper:


Sapper 是一个基于 Svelte 的应用框架,支持服务端渲染和静态生成。它提供了简单易用的工具和流畅的开发体验,帮助开发者构建高性能的 Sv

作者:嚣张农民
来源:juejin.cn/post/7251875626906599485
elte 应用程序。

收起阅读 »

为什么你非常不适应 TypeScript

web
前言 在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了? 有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并...
继续阅读 »

前言


在群里看到一些问题和言论:为什么你们这么喜欢“类型体操”?为什么我根本学不下去 TypeScript?我最讨厌那些做类型体操的了;为什么我学了没过多久马上又忘了?


有感于这些问题,我想从最简单的一个角度来切入介绍一下 TypeScript,并向大家介绍并不是只要是个类型运算就是体操。并在文中介绍一种基本思想作为你使用类型系统的基本指引。


引子


我将从一个相对简单的 API 的设计过程中阐述关于类型的故事。在这里我们可以假设我们现在是一个工具的开发者,然后我们需要设计一个 API 用于从对象中拿取指定的一些 key 作为一个新的对象返回给外面使用。


垃圾 TypeScript


一个人说:我才不用什么破类型,我写代码就是要没有类型,我就是要随心所欲的写。然后写下了这段代码。


declare function pick(target: any, ...keys: any): any

他的用户默默的写下了这段代码:


pick(undefined, 'a', 1).b

写完运行,发现问题大条了,控制台一堆报错,接口数据也提交不上去了,怎么办呢?


刚学 TypeScript


一个人说:稍微检查一下传入类型就好了,别让人给我乱传参数就行。


declare function pick(target: Record<string, unknown>, ...keys: string[]): unknown

很好,上面的问题便不复存在了,API 也是基本可用的了。但是!当对象复杂的时候,以及字段并不是短单词长度的时候就会发现了一个没解决的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghikjl')

从肉眼角度上,我们很难发现这前后的不一致,所以我们为什么要让调用方的用户自己去 check 自己的字段有没有写对呢?


不就 TypeScript


一个人说:这还不简单,用个泛型加 keyof 不就行了。


declare function pick<
T extends Record<string, unknown>
>(target: T, ...keys: keyof T[]): unknown

我们又进一步解决的上面的问题,但是!还是有着相似的问题,虽然我们不用检查 keys 是不是传入的是一个正确的值了,但是我们实际上对返回的值也存在一个类似的问题。


pick({ abcdefghijkl: '123' }, 'abcdefghijkl').abcdefghikjl



  • 一点小小的拓展


    在这里我们看起来似乎是一个很简单的功能,但实际上蕴含着一个比较重要的信息。


    为什么我们之前的方式都拿不到用户传入进来的类型信息呢?是有原因的,当我们设计的 API 的时候,前面的角度是从,如何校验类型方向进行的思考。


    而这里是尝试去通过约定好的一种规则,通过 TypeScript 的隐式类型推断获得到传入的类型,再通过约定的规则转化出一种新的类型约束来对用户的输入进行限制。




算算 TypeScript


一个人说:好办,算出来一个新的类型就好了。


declare function pick<
T extends Record<string, unknown>,
Keys extends keyof T
>(target: T, ...keys: Keys[]): {
[K in Keys]: T[K]
}

到这里已经是对类型的作用有了基础的了解了,能写出来符合开发者所能接受的类型相对友好的代码了。我们可以再来思考一些更特殊的情况:


// 输入了重复的 key
pick({ a: '' }, 'a', 'a')

完美 TypeScript


到这里,我们便是初步开始了类型“体操”。但是在本篇里,我们不去分析它。


export type L2T<L, LAlias = L, LAlias2 = L> = [L] extends [never]
? []
: L extends infer LItem
? [LItem?, ...L2T<Exclude<LAlias2, LItem>, LAlias>]
: never

declare function pick<
T extends Record<string, unknown>,
Keys extends L2T<keyof T>
>(target: T, ...keys: Keys): Pick<T, Keys[number] & keyof T>

const x0 = pick({ a: '1', b: '2' }, 'a')
console.log(x0.a)
// @ts-expect-error
console.log(x0.b)

const x1 = pick({ a: '1', b: '2' }, 'a', 'a')
// ^^^^^^^^
// TS2345: Argument of type '["a", "a"]' is not assignable to parameter of type '["a"?, "b"?] | ["b"?, "a"?]'.
//   Type '["a", "a"]' is not assignable to type '["a"?, "b"?]'.
//     Type at position 1 in source is not compatible with type at position 1 in target.
//       Type '"a"' is not assignable to type '"b"'.

一个相对来说比较完美的 pick 函数便完成了。


总结


我们再来回到我们的标题吧,从我对大多数人的观察来说,很多的人开始来使用 TypeScript 有几种原因:



  • 看到大佬们都在玩,所以自己也想来“玩”,然后为了过类型校验而去写

  • 看到一些成熟的项目在使用 TypeScript ,想参与贡献,参与过程中为了让类型通过而想办法去解决类型报错

  • 公司整体技术栈采用的是 TypeScript ,要用 TypeScript 进行业务编写,从而为了过类型检查和 review 而去解决类型问题


诸如此类的问题还有很多,我将这种都划分为「为了解决类型检查的问题」而进行的类型编程,这也是大多数人为什么非常不适应 TypeScript,甚至不喜欢他的一个原因。这其实对学习 TypeScript 并不是一个很好的思路,在这里我觉得我们需要站在设计者的角度去对类型系统进行思考。我觉得有以下几个角度:



  • 类型检查到位

  • 类型提示友好

  • 类型检查严格

  • 扩展性十足


我们如果站在这几个角度对我们的 API 进行设计,我们可以发现,开发者能够很轻松的将他们需要的代码编写出来,而尽量不用去翻阅文档,查找 example。


希望通过我的这篇分享,大家能对 TypeScript 多一些理解,并参与到生态中来,守护我们的 JavaScript。




2023/06/27 更新



理性探讨,在评论区说什么屎不是屎的,嘴巴臭可以不说话的。


没谁逼着你一定要写最后一种层次的代码,能力不足可以学啊,不喜欢可以不学啊,能达到倒数第二个就已经很棒啊。


最后一种只是给大家看看 TypeScript 的一种可能,而不是说你应该这么做的。


作者:一介4188
来源:juejin.cn/post/7248599585751515173

收起阅读 »

次世代前端视图框架都在卷啥?

web
上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 Solid、Svelte、Qwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue。 目前 React/Augular/Vue 还...
继续阅读 »

state of JavaScript 2022 满意度排名


上图是 State of JavaScript 2022 前端框架满意度排名。前三名分别是 SolidSvelteQwik。我们可以称他们为次世代前端框架的三大代表,前辈是 React/Angular/Vue
目前 React/Augular/Vue 还占据的主流的市场地位, 现在我们还不知道下一个五年、十年谁会成为主流,有可能前辈会被后浪拍死在沙滩上, 也有可能你大爷还是你大爷。


就像编程语言一样,尽管每年都有新的语言诞生,但是撼动主流编程语言的地位谈何容易。在企业级项目中,我们的态度会趋于保守,选型会偏向稳定、可靠、生态完善的技术,因此留给新技术的生存空间并不多。除非是革命性的技术,或者有大厂支撑,否则这些技术或框架只会停留小众圈子内。



比如有一点革命性、又有大厂支撑的 Flutter。





那么从更高的角度看,这些次时代的前端视图框架在卷哪些方向呢?有哪些是革命性的呢?


先说一下本文的结论:



  • 整体上视图编程范式已经固化

  • 局部上体验上内卷






视图编程范式固化


从 JQuery 退出历史舞台,再到 React 等占据主流市场。视图的编程范式基本已经稳定下来,不管你在学习什么视图框架,我们接触的概念模型是趋同的,无非是实现的手段、开发体验上各有特色:



  • 数据驱动视图。数据是现代前端框架的核心,视图是数据的映射, View=f(State) 这个公式基本成立。

  • 声明式视图。相较于上一代的 jQuery,现代前端框架使用声明式描述视图的结构,即描述结果而不是描述过程。

  • 组件化视图。组件是现代前端框架的第一公民。组件涉及的概念无非是 props、slots、events、ref、Context…






局部体验内卷


回顾一下 4 年前写的 浅谈 React 性能优化的方向,现在看来依旧不过时,各大框架无非也是围绕着这些「方向」来改善。


当然,在「框架内卷」、「既要又要还要」时代,新的框架要脱颖而出并不容易,它既要服务好开发者(开发体验),又要服务好客户(用户体验) , 性能不再是我们选择框架的首要因素。




以下是笔者总结的,次世代视图框架的内卷方向:



  • 用户体验

    • 性能优化

      • 精细化渲染:这是次世代框架内卷的主要战场,它们的首要目的基本是实现低成本的精细化渲染

        • 预编译方案:代表有 Svelte、Solid

        • 响应式数据:代表有 Svelte、Solid、Vue、Signal(不是框架)

        • 动静分离





    • 并发(Concurrent):React 在这个方向独枳一树。

    • 去 JavaScript:为了获得更好的首屏体验,各大框架开始「抛弃」JavaScript,都在比拼谁能更快到达用户的眼前,并且是完整可交互的形态。



  • 开发体验

    • Typescript 友好:不支持 Typescript 基本就是 ca

    • 开发工具链/构建体验: Vite、Turbopack… 开发的工具链直接决定了开发体验

    • 开发者工具:框架少不了开发者工具,从 Vue Devtools 再到 Nuxt Devtools,酷炫的开发者工具未来可能都是标配

    • 元框架: 毛坯房不再流行,从前到后、大而全的元框架称为新欢,内卷时代我们只应该关注业务本身。代表有 Nextjs、Nuxtjs










精细化渲染






预编译方案


React、Vue 这些以 Virtual DOM 为主的渲染方式,通常只能做到组件级别的精细化渲染。而次世代的 Svelte、Solidjs 不约而同地抛弃了 Virtual DOM,采用静态编译的手段,将「声明式」的视图定义,转译为「命令式」的 DOM 操作


Svelte


<script>
let count = 0

function handleClick() {
count += 1
}
</script>

<button on:click="{handleClick}">Clicked {count} {count === 1 ? 'time' : 'times'}</button>

编译结果:


// ....
function create_fragment(ctx) {
let button
let t0
let t1
let t2
let t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + ''
let t3
let mounted
let dispose

return {
c() {
button = element('button')
t0 = text('Clicked ')
t1 = text(/*count*/ ctx[0])
t2 = space()
t3 = text(t3_value)
},
m(target, anchor) {
insert(target, button, anchor)
append(button, t0)
append(button, t1)
append(button, t2)
append(button, t3)

if (!mounted) {
dispose = listen(button, 'click', /*handleClick*/ ctx[1])
mounted = true
}
},
p(ctx, [dirty]) {
if (dirty & /*count*/ 1) set_data(t1, /*count*/ ctx[0])
if (
dirty & /*count*/ 1 &&
t3_value !== (t3_value = /*count*/ (ctx[0] === 1 ? 'time' : 'times') + '')
)
set_data(t3, t3_value)
},
i: noop,
o: noop,
d(detaching) {
if (detaching) {
detach(button)
}

mounted = false
dispose()
},
}
}

function instance($$self, $$props, $$invalidate) {
let count = 0

function handleClick() {
$$invalidate(0, (count += 1))
}

return [count, handleClick]
}

class App extends SvelteComponent {
constructor(options) {
super()
init(this, options, instance, create_fragment, safe_not_equal, {})
}
}

export default App

我们看到,简洁的模板最终被转移成了底层 DOM 操作的命令序列。


我写文章比较喜欢比喻,这种场景让我想到,编程语言对内存的操作,DOM 就是浏览器里面的「内存」:



  • Virtual DOM 就是那些那些带 GC 的语言,使用运行时的方案来屏蔽 DOM 的操作细节,这个抽象是有代价的

  • 预编译方案则更像 Rust,没有引入运行时 GC, 使用了一套严格的所有权和对象生命周期管理机制,让编译器帮你转换出安全的内存操作代码。

  • 手动操作 DOM, 就像 C、C++ 这类底层语言,需要开发者手动管理内存


使用 Svelte/SolidJS 这些方案,可以做到修改某个数据,精细定位并修改 DOM 节点,犹如我们当年手动操作 DOM 这么精细。而 Virtual DOM 方案,只能到组件这一层级,除非你的组件粒度非常细。








响应式数据


和精细化渲染脱不开身的还有响应式数据


React 一直被诟病的一点是当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树,如果要避免不必要的子组件的重渲染,需要开发者手动进行优化(比如 shouldComponentUpdatePureComponentmemouseMemo/useCallback)  。同时你可能会需要使用不可变的数据结构来使得你的组件更容易被优化。


在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染。


近期比较火热的 signal (信号,Angular、Preact、Qwik、Solid 等框架都引入了该概念),如果读者是 Vue 或者 MobX 之类的用户, Signal 并不是新的概念。


按 Vue 官方文档的话说:从根本上说,信号是与 Vue 中的 ref 相同的响应性基础类型。它是一个在访问时跟踪依赖、在变更时触发副作用的值容器。


不管怎样,响应式数据不过是观察者模式的一种实现。相比 React 主导的通过不可变数据的比对来标记重新渲染的范围,响应式数据可以实现更细粒度的绑定;而且响应式的另一项优势是它的可传递性(有些地方称为 Props 下钻(Props Drilling))。






动静分离


Vue 3 就是动静结合的典型代表。在我看来 Vue 深谙中庸之道,在它身上我们很难找出短板。


Vue 的模板是需要静态编译的,这使得它可以像 Svelte 等框架一样,有较大的优化空间;同时保留了 Virtual DOM 和运行时 Reactivity,让它兼顾了灵活和普适性。


基于静态的模板,Vue 3 做了很多优化,笔者将它总结为动静分离吧。比如静态提升、更新类型标记、树结构打平,无非都是将模板中的静态部分和动态部分作一些分离,避免一些无意义的更新操作。


更长远的看,受 SolidJS 的启发, Vue 未来可能也会退出 Vapor 模式,不依赖 Virtual DOM 来实现更加精细的渲染。








再谈编译时和运行时


编译时和运行时没有优劣之分, 也不能说纯编译的方案就必定是未来的趋势。


这几年除了新的编译时的方案冒出来,宣传自己是未来;也有从编译时的焦油坑里爬出来, 转到运行时方案的,这里面的典型代表就是 Taro。


Taro 2.0 之前采用的是静态编译的方案,即将 ’React‘ 组件转译为小程序原生的代码:


Untitled


但是这个转译工作量非常庞大,JSX 的写法千变万化,非常灵活。Taro 只能采用 穷举 的方式对 JSX 可能的写法进行了一 一适配,这一部分工作量很大,实际上 Taro 有大量的 Commit 都是为了更完善的支持 JSX 的各种写法。这也是 Taro 官方放弃这种架构的原因。


也就是说 Taro 也只能覆盖我们常见的 JSX 用法,而且我们必须严格遵循 Taro 规范才能正常通过。


有非常多的局限:



  • 静态的 JSX

  • 不支持高阶组件

  • 不支持动态组件

  • 不支持操作 JSX 的结果

  • 不支持 render function

  • 不能重新导出组件

  • 需要遵循 on*、render* 约束

  • 不支持 Context、Fragment、props 展开、forwardRef

  • ….


有太多太多的约束,这已经不是带着镣铐跳舞了,是被五花大绑了。




使用编译的方案不可避免的和实际运行的代码有较大的 Gap,源码和实际运行的代码存在较大的差别会导致什么?



  • 比较差的 Debug 体验。

  • 比较黑盒。


我们在歌颂编译式的方案,能给我们带来多大的性能提升、带来多么简洁的语法的同时。另一方面,一旦我们进行调试/优化,我们不得不跨越这层 Gap,去了解它转换的逻辑和底层实现。


这是一件挺矛盾的事情,当我们「精通」这些框架的时候,估计我们已经是一个人肉编译器了。


Taro 2.x 配合小程序, 这对卧龙凤雏, 可以将整个开发体验拉到地平线以下。




回到这些『次世代』框架。React/Vue/Angular 这些框架先入为主, 在它们的教育下,我们对前端视图开发的概念和编程范式的认知已经固化。


Untitled


比如在笔者看来 Svelte 是违法直觉的。因为 JavaScript 本身并不支持这种语义。Svelte 要支持这种语义需要一个编译器,而作为一个 JavaScript 开发者,我也需要进行心智上的转换。


而 SolidJS 则好很多,目之所及都是我们熟知的东西。尽管编译后可能是一个完全不一样的东西。



💡 Vue 曾经也过一个名为**响应性语法糖的实验性功能来探索这个方向,但最后由于这个原因**,废弃了。这是一次明智的决定



当然,年轻的次世代的前端开发者可能不这么认为,他们毕竟没有经过旧世代框架的先入为主和洗礼,他们更能接受新的开发范式,然后扛起这些旗帜,让它们成为未来主流。


总结。纯编译的方能可以带来更简洁的语法、更多性能优化的空间,甚至也可以隐藏一些跨平台/兼容性的细节。另一方面,源码和实际编译结果之间的 Gap,可能会逼迫开发者成为人肉编译器,尤其在复杂的场景,对开发者的心智负担可能是翻倍的。


对于框架开发者来说,纯编译的方案实现复杂度会更高,这也意味着,会有较高贡献门槛,间接也会影响生态。








去 JavaScript


除了精细化渲染,Web 应用的首屏体验也是框架内卷的重要方向,这个主要的发展脉络,笔者在 现代前端框架的渲染模式 一文已经详细介绍,推荐大家读一下:


Untitled


这个方向的强有力的代表主要有 Astro(Island Architecture 岛屿架构)、Next.js(React Server Component)、Qwik(Resumable 去 Hydration)。


这些框架基本都是秉承 SSR 优先,在首屏的场景,JavaScript 是「有害」的,为了尽量更少地向浏览器传递 JavaScript,他们绞尽脑汁 :



  • Astro:’静态 HTML‘优先,如果想要 SPA 一样实现复杂的交互,可以申请开启一个岛屿,这个岛屿支持在客户端进行水合和渲染。你可以把岛屿想象成一个 iframe 一样的玩意。

  • React Server Component: 划分服务端组件和客户端组件,服务端组件仅在服务端运行,客户端只会看到它的渲染结果,JavaScript 执行代码自然也仅存于服务端。

  • Qwik:我要直接革了水合(Hydration)的命,我不需要水合,需要交互的时候,我惰性从服务端拉取事件处理器不就可以了…


不得不说,「去 JavaScript」的各种脑洞要有意思多了。






总结


本文主要讲了次世代前端框架的内卷方向,目前来看还处于量变的阶段,并没有脱离现在主流框架的心智模型,因此我们上手起来基本不会有障碍。


作为普通开发者,我们可以站在更高的角度去审视这些框架的发展,避免随波逐流和无意义的内卷。






扩展阅读



作者:荒山
来源:juejin.cn/post/7251763342954512440
收起阅读 »

为了娃的暑期课,老父亲竟然用上了阿里云高大上的 Serverless FaaS!!!

web
起因 事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件...
继续阅读 »

起因


事件的起因是,最近家里的俩娃马上要放暑假了,家里的老母亲早早的就规划好了姐姐弟弟的暑期少年宫课程,奈何有些想上个课程一直没有”抢“到课程。平时带娃在少年宫上课的父母可能懂的,一般少年宫的课程都是提前预报名,然后会为了公平起见进行摇号,中者缴费。本来是一件比较合理的处理方式,但奈何不住各位鸡娃的父母们的上有政策下有对策的路子。



第一阶段:靠数量提高命中率 ,大家都各自报了很多不同课程,防止因为摇号没摇上,导致落空。我们家也是一样操作~~~。但是这里也会出现另一种状况,当摇号结束,大家缴费期间,有的摇中家长,发现课程多了或者有些课程和课外兴趣班冲突,或者种种其他原因,不想再上暑期课程了,就会取消这门课程。 即时你缴费了,后面也是可以取消的,只是会扣除一些费用。
第二阶段:捡漏,有报多的家长,就有没有抢到合适课程的家长。没错,说的正是我们家 哈哈。在我老婆规划中,我们还有几门课程没有摇中,那这个时候怎么办呢?只能蹲守,人工不定时的登录查课,寄期望于有些家长退课了,我们好第一时间补上去。


当当当,作为一个程序员老父亲,这个时候终于排上用场了~~~,花了一个晚上,写了个定时查询脚本+通知,当有课放出,咱们就通知一下领导(老婆大人)定夺,话说这个小查课定时任务深受领导的高度表扬。
好了起因就是这样,下面我们回到正题,给大家实操下如何使用阿里云的Serverless 函数,来构建这个小定时脚本。


架构


很简单的架构图,只用到了这么几个组件,



  • Serverless FC 无服务器函数,承载逻辑主体

  • OSS 存储中间结果数据

  • RAM是对计算函数赋予角色使其有对应权限。

  • 企业微信机器人,企业微信本身可以随便注册,拉个企业微信群,加入一个群机器人,就可以作为消息触达端。



实践


函数计算FC



本次实操中,我们需要先了解阿里云的函数计算FC几个概念,方便我们后面操作理解:



相关官方资料:基本概念

下面我只列了本次操作涉及到的概念,更详细资料,建议参考官方文档。




  • 服务:服务是函数计算资源管理的单位,是符合微服务理念的概念。从业务场景出发,一个应用可以拆分为多个服务。从资源使用维度出发,一个服务可以由多个函数组成。

  • FC函数:函数计算的资源调度与运行是以函数为单位。FC函数由函数代码和函数配置构成。FC函数 必须从属于服务,同一个服务下的所有函数共享一些相同的设置,例如服务授权、日志配置。函数的相关操作

  • 层:层可以为您提供自定义的公共依赖库、运行时环境及函数扩展等发布与部署能力。您可以将函数依赖的公共库提炼到层,以减少部署、更新时的代码包体积,也可以将自定义的运行时,以层部署在多个函数间共享。

  • 触发器:触发器是触发函数执行的方式。在事件驱动的计算模型中,事件源是事件的生产者,函数是事件的处理者,而触发器提供了一种集中、统一的方式来管理不同的事件源


创建函数



  1. 函数计算FC--> 任务--> 选择创建函数




  1. 配置函数


这里我截了个长屏,来给大家逐个解释



tips: 如果大家也有截长屏需求,推荐chrome 中的插件:Take Webpage Screenshots Entirely - FireShot




  • 函数方式:我的小脚本是python 代码,我直接使用自定义运行环境,如果你想了解这三种方式区别,建议详细阅读这篇文章:函数计算支持的多语言运行时信息

  • 服务名称:我们如果初次创建,选择创建一个服务,然后填入自己设定的服务名字即可

  • 函数代码:这里我选择运行时python 3.9 , 示例代码(代码等我们创建完成之后,再填充自己的代码逻辑)

  • 高级配置: 这里如果是初学者,个人建议尽量选最小配置,因为函数计算是按你使用的资源*次数 收费的, 这里我改成了资源粒度,0.05vCpu 128MB,并发度 1

  • 函数变量:我暂时不需要,就没有设置,如果你需要外部配置一些账号密码,可以使用这种方式来配置

  • 触发器:这里展示出了函数计算的协同作用,可以通过多种云服务产品来进行事件通知触发,我们这里的样例只需要一个定时轮询调度,所以这里我使用了定时触发器,5分钟调用一次。



配置依赖



函数整体创建成功之后,点击函数名称,进入函数详情页



函数代码模块填充本地已经调试好的代码, 测试函数,发现相关依赖并没有,这里我们需要编辑层,来将python相关依赖文件引入, 点击上图中编辑层



我选择的是在线构建依赖层,按照requirements.txt的格式书写,然后就可以在线安装了,很方便。创建成功之后,回到编辑层位置,选择刚开始创建的层,点击确定,既可,这样就不会再报相关依赖缺失了。


配置OSS映射


我的小脚本里,需要存储中间数据,因为函数计算FC本身是无状态的,所以需要借助外部存储,很自然的就会想到用OSS来存储。但是如何正确的使用OSS桶来存储中间数据呢?
官方关于python操作OSS的教程:python 操作 OSS



# -*- coding: utf-8 -*-
import oss2
# 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
auth = oss2.Auth('<yourAccessKeyId>', '<yourAccessKeySecret>')
# Endpoint以杭州为例,其它Region请按实际情况填写。
bucket = oss2.Bucket(auth, 'http://oss-cn-hangzhou.aliyuncs.com', '<yourBucketName>')

这里操作基本都会使用到 AK,SK。 但是基于云上的安全的实践操作以及要定期更换ak,sk来保证安全,尽量不要直接在代码中使用ak, sk来调用api 。那是否有其他更合理的操作方式?
我找到了这篇文章 配置OSS文件系统



函数计算支持与OSS无缝集成。您可以为函数计算的服务配置OSS挂载,配置成功后,该服务下的函数可以像使用本地文件系统一样使用OSS存储服务。



个人推荐这种解决方案



  • 只需要配置对应函数所授权的角色策略中,加上对相应的挂载OSS桶的读写权限

  • 这个操作符合最小粒度的赋予权限,同时也减少代码开发量,python可以像操作本地磁盘一样,操作oss,简直不要太方便~~~

  • 同时也不需要担心所谓的ak sk泄漏风险以及需要定期更换密钥的麻烦,因为就不存在使用ak sk


我最后也是用这种方式,配置了oss文件系统映射到函数运行时的环境磁盘上。


企业微信机器人


企业微信可以直接注册,不需要任何费用,之后两个人拉一个群,添加一个群机器人即可。
可以参考官方文档:如何使用群机器人 来用python 发送群消息,很简单的一段代码既可完成发送消息通知。


wx_url = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx-xxxxx-xxxxx"
def sendWechatBotMsg(msg,collagues):
"""艾特指定同事,并发送指定信息"""
data = json.dumps({"msgtype": "text", "text": {"content": msg, "mentioned_list":collagues}})
r = requests.post(wx_url, data, auth=('Content-Type', 'application/json'))

最后效果如图所示:



总结


通过日常生活中的一个小场景,实践了下阿里云的高大上的Serverless FC 服务。个人选择这种无服务器函数计算,也是结合了成本的因素。
给大家对比下两种方案价格:



  • 传统云主机方式:


阿里云官方ECS主机的定价:实例价格信息
最便宜的一档: 1vCPU 1GB 实例, 每个月也要34.2 RMB 还没有包括挂载的磁盘价格 ,以及公网带宽费用




  • Serverless FC


而使用无服务器函数计算服务, 按使用时长和资源计费,像我这种最小资源粒度就可以满足同时调度次数是周期性的,大大消减了费用, 我跑了大概一周的时间 大概花费了 0.16 RMB,哈哈 简直是不能再便宜了。大家感兴趣的也可以动手实践下自己的需求场景。




云计算已经是当下技术人员的必学的一门课程,如果有时间也鼓励大家可以多了解学习,提升自己的专业能力。感兴趣的朋友,如果有任何问题,需要沟通交流也可以添加我的个人微信 coder_wukong,备注:云计算,或者关注我的公众号 WuKongCoder日常也会不定期写一些文章和思考。




如果觉得文章不错,欢迎大家点赞,留言,转发,收藏 谢谢大家,我们下篇文章再会~~~



参考资料


中国唯一入选 Forrester 领导者象限,阿里云 Serverless 产品能力全球第一

函数计算支持的多语言运行时信息

阿里云OSS文档:python 操作 OSS

阿里云函数计算文档:配置OSS文件系统

企业微信文档:如何使用群机器人

让 Serverless 更普惠

Serverless 在阿里云函数计算中的实践


作者:WuKongCoder
来源:juejin.cn/post/7251786717652107301
收起阅读 »

你还在用传统轮播组件吗?来看看遮罩轮播组件

web
背景 最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。 这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。 传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,...
继续阅读 »

背景


最近有一个页面改版的需求,在UI走查阶段,设计师说原来的轮播组件和新版页面UI整体风格不搭,所以要换掉。


这里就涉及到两种轮播组件,一种是传统的轮播组件,一种是设计师要的那种。


传统的轮播组件,大家都见过,原理也清楚,就是把要轮播的图片横向排成一个队列,把他们当成一个整体,每次轮换,其实是把这个队列整体往左平移X像素,这里的X通常就是一个图片的宽度。
这种效果可以参见vant组件库里的swipe组件


而我们设计师要的轮播效果是另外一种,因为我利用端午假期已经做好了一个雏形,所以大家可以直接看Demo


当然你也可以直接打开 腾讯视频APP 首页,顶部的轮播,就是我们设计师要的效果。


需求分析


新式轮播,涉及要两个知识点:



  • 图片层叠

  • 揭开效果


与传统轮播效果一个最明显的不同是,新的轮播效果需要把N张待轮播的图片在Z轴上重叠放置,每次揭开其中的一张,下一张是自然漏出来的。这里的实现方式也有多种,但最先想到的还是用zindex的方案。


第二个问题是如何实现揭开的效果。这里就要使用到css3的新属性mask。
mask是一系列css的简化属性。包括mask-image, mask-position等。
因为mask的系列属性还有一定的兼容性,所以一部分浏览器需要带上-webkit-前缀才能生效。


还有少数浏览器不支持mask属性,退化的情况是轮播必须有效,但是没有轮换的动效。


实现


有了以上的分析,就可以把效果做出来了。核心代码如下:


<script setup lang="ts">
import { ref, onMounted, watch } from "vue";
// 定义属性
const props = defineProps([
'imgList',
'duration',
'transitionDuration',
'maskPositionFrom',
'maskPositionTo',
'maskImageUrl'
]);
// 定义响应式变量
const currentIndex = ref(0);
const oldCurrentIndex = ref(0);
const imgList = ref([...props.imgList, props.imgList[0]]);
const getInitZindex = () => {
const arr = [1];
for (let i = imgList.value.length - 1; i >= 1; i--) {
arr.unshift(arr[0] + 1);
}
return arr;
}
const zIndexArr = ref([...getInitZindex()]);
const maskPosition = ref(props.maskPositionFrom || 'left');
const transition = ref(`all ${props.transitionDuration || 1}s`);
// 设置动画参数
const transitionDuration = props.transitionDuration || 1000;
const duration = props.duration || 3000;

// 监听currentIndex变化
watch(currentIndex, () => {
if (currentIndex.value === 0) {
zIndexArr.value = [...getInitZindex()];
}
maskPosition.value = props.maskPositionFrom || 'left';
transition.value = 'none';
})
// 执行动画
const execAnimation = () => {
transition.value = `all ${props.transitionDuration || 1}s`;
maskPosition.value = props.maskPositionFrom || 'left';
maskPosition.value = props.maskPositionTo || 'right';
oldCurrentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
setTimeout(() => {
zIndexArr.value[currentIndex.value] = 1;
currentIndex.value = (currentIndex.value + 1) % (imgList.value.length - 1);
}, 1000)
}
// 挂载时执行动画
onMounted(() => {
const firstDelay = duration - transitionDuration;
function animate() {
execAnimation();
setTimeout(animate, duration);
}
setTimeout(animate, firstDelay);
})
</script>
<template>
<div class="fly-swipe-container">
<div class="swipe-item"
:class="{'swipe-item-mask': index === currentIndex}"
v-for="(url, index) in imgList"
:key="index"
:style="{ zIndex: zIndexArr[index],
'transition': index === currentIndex ? transition : 'none',
'mask-image': index === currentIndex ? `url(${maskImageUrl})` : '',
'-webkit-mask-image': index === currentIndex ? `url(${maskImageUrl})`: '',
'mask-position': index === currentIndex ? maskPosition: '',
'-webkit-mask-position': index === currentIndex ? maskPosition: '' }"
>

<img :src="url" alt="">
</div>
<div class="fly-indicator">
<div class="fly-indicator-item"
:class="{'fly-indicator-item-active': index === oldCurrentIndex}"
v-for="(_, index) in imgList.slice(0, imgList.length - 1)"
:key="index">
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.fly-swipe-container {
position: relative;
overflow: hidden;
width: 100%;
height: inherit;
.swipe-item:first-child {
position: relative;
}
.swipe-item {
position: absolute;
width: 100%;
top: 0;
left: 0;
img {
display: block;
width: 100%;
object-fit: cover;
}
}
.swipe-item-mask {
mask-repeat: no-repeat;
-webkit-mask-repeat: no-repeat;
mask-size: cover;
-webkit-mask-size: cover;
}
.fly-indicator {
display: flex;
justify-content: center;
align-items: center;
z-index: 666;
position: relative;
top: -20px;
.fly-indicator-item {
margin: 0 5px;
width: 10px;
height: 10px;
border-radius: 50%;
background: gray;
}
.fly-indicator-item-active {
background: #fff;
}
}
}
</style>

这是一个使用 Vue 3 构建的图片轮播组件。在这个组件中,我们可以通过传入一组图片列表、切换动画的持续时间、过渡动画的持续时间、遮罩层的起始位置、遮罩层的结束位置以及遮罩层的图片 URL 来自定义轮播效果。


组件首先通过 defineProps 定义了一系列的属性,并使用 ref 创建了一些响应式变量,如 currentIndexoldCurrentIndeximgListzIndexArr 等。


onMounted 钩子函数中,我们设置了一个定时器,用于每隔一段时间执行一次轮播动画。
在模板部分,我们使用了一个 v-for 指令来遍历图片列表,并根据当前图片的索引值为每个图片元素设置相应的样式。同时,我们还为每个图片元素添加了遮罩层,以实现轮播动画的效果。


在样式部分,我们定义了一些基本的样式,如轮播容器的大小、图片元素的位置等。此外,我们还为遮罩层设置了一些样式,包括遮罩图片的 URL、遮罩层的位置等。


总之,这是一个功能丰富的图片轮播组件,可以根据传入的参数自定义轮播效果。


后续


因为mask可以做的效果还有很多,后续该组件可以封装更多轮播效果,比如从多个方向的揭开效果,各种渐变方式揭开效果。欢迎使用和提建议。


仓库地址:github.com/cunzai

zhuyi…

收起阅读 »

你们公司的官网被搜索引擎收录了吗?

web
前言 前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求 网站要大气,炫酷,有科技感 图片文字要高大上 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列 为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于...
继续阅读 »

1.jpg


前言


前段时间,我司的官网要改版。老板们手一挥,提出了以下几点需求



  • 网站要大气,炫酷,有科技感

  • 图片文字要高大上

  • 注重SEA、SEO优化,用户查找关键字后,我们公司的网站排名要显示在前列


为此,我们还专门买了一个SEO优化的课程,大张旗鼓的学习了一通。至于效果如何,1个月见分晓


那么如何编写 JavaScript 代码以有利于 SEO 和 SEA 呢?


下面仅展示被被谷歌搜索引擎收录的


SEA、SEO优化


保持好的网页结构



  1. 使用语义化的 HTML结构


HTML语义化是指使用恰当的HTML标签来描述网页内容的结构和含义,以提高网页的可读性、可访问性和搜索引擎优化。



  • header: 网站的页眉部分

  • nav: 定义网站的主要导航链接

  • main: 定义页面的主要内容区域,每个页面应该只有一个<main>标签

  • section: 定义页面中的独立区块, 例如文章、产品列表等

  • article: 定义独立的文章内容,通常包含标题、作者、发布日期等信息

  • aside: 定义页面的侧边栏或附属信息区域

  • footer: 网站的页脚部分


<header>
<h1>官网</h1>
<nav>
<ul>
<li><a href="#">首页</a></li>
<li><a href="#">关于我们</a></li>
<li><a href="#">联系我们</a></li>
</ul>
</nav>
</header>

<main>
<div>欢迎来到我们的网站</div>
<p>这里是网站的主要内容。</p>
</main>


<section>
<h2>最新文章</h2>
<article>
<h3>文章标题</h3>
<p>文章内容...</p>
</article>
<article>
...
</article>
</section>


<aside>
<h3>最新消息</h3>
<ul>
<li><a href="#">链接1</a></li>
...
</ul>
</aside>

<article>
<h2>消息1</h2>
<p>文章内容...</p>
</article>


<footer>
<p>版权所有 &copy; 2023</p>
<p>联系我们:info@example.com</p>
</footer>



  1. 提供准确且吸引人的页面标题和描述


准确且简洁的标题和描述,有利于吸引访问者和搜索引擎的注意



  • 页面标题: Title

  • 页面描述: Meta Description


<head>
<title>精美手工艺品——手工制作的独特艺术品</title>
<meta name="description" content="我们提供精美手工艺品的设计与制作,包括陶瓷、木雕、织物等。每件艺术品都是独一无二的,以精湛的工艺和创造力打动您的心灵。欢迎浏览我们的作品集。">
</head>


标题要小于50个字符,描述要小于150字符





  1. 在关键位置使用关键字: 包括标题、段落文本、链接文本和图片的 alt 属性。



    • 段落文本: 自然的使用关键字,有助于搜索引擎收录

    • 链接文本: 使用描述性的链接文本,并在其中包含关键字,这有助于搜索引擎理解链接指向的内容

    • 图片的 alt 属性: 对于每个图像,使用描述性的 alt 属性来说明图像内容,并在其中包含关键字。这不仅有助于视力障碍用户理解图像,还可以提供关键字相关的图像描述给搜索引擎。




<h1>欢迎来到精美手工艺品网店</h1>
<p>我们提供各种精美手工艺品,包括陶瓷、木雕、织物等。每个艺术品都是由我们经验丰富的工匠手工制作而成,展现了精湛的工艺和创造力。</p>
<p>浏览我们的<a href="/products" title="手工艺品产品列表">产品列表</a>,您将发现独特的艺术品,适合作为礼物或收藏。</p>
<img src="product.jpg" alt="陶瓷花瓶 - 手工制作的精美艺术品" />


一个页面要保证有且只有h1标签



使用友好的 URL 结构


使用友好的URL结构是一个重要的优化策略,它可以提升网站的可读性、可维护性和用户体验



  • 使用关键字: 在URL中使用关键字,以便用户和搜索引擎可以更好地理解页面的主题和内容, URL中多个关键词使用连字符字符 "-"进行分隔。

  • 结构层次化: 层次化的URL结构来反映内容的结构和关系

  • 避免使用参数: 尽量避免在URL中使用过多的参数,特别是使用随机字符串或数字作为参数

  • 尽量使用永久链接: 尽可能使用永久链接,避免频繁更改URL

  • 尽量保持URL简洁: 避免过长的URL。短连接更易于分享和记忆


<!-- 不友好的URL -->
https://example.com/index.html?category=7&product=12345
https://example.com/qinghua/porcelain

<!-- 友好的URL -->
https://example.com/porcelain/qinghua
https://example.com/blog/friendly-urls


  1. 重要链接不要用JS


搜索引擎爬虫通常不会执行 JavaScript,并且依赖 JavaScript 的链接可能无法被爬虫正确解析和索引



使用标准的 <a> 标签进行跳转,避免使用 JavaScript 跳转




  1. 使用W3C规范


使用W3C规范是确保你的网页符合Web标准并具有良好可访问性的重要方式


不符合W3C的规范:



  • 未闭合的标签

  • 未正确嵌套的元素

  • 行内元素包裹块状元素


<!-- 未闭合的标签 -->
<p>This is a paragraph with no closing tag.
<!-- 未正确嵌套的元素 -->
<div><p>This paragraph is inside a div but not closed properly.</div></p>
<!-- 行内元素包裹块状元素 -->
<span><p>This paragraph is inside a div but not closed properly.</p></span>

响应式设计和移动优化


Google 现在使用了移动优先索引, 搜索引擎更倾向于优先索引和显示移动友好的网页


使用响应式设计,使你的网页在各种设备上都能正确显示。



  1. 响应式设计:确保网页具有响应式设计,能够适应不同设备的屏幕尺寸

  2. 关注移动友好性:确保网页在移动设备上加载和显示良好


JavaScript使用和加载优化


搜索引擎爬虫通常不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容




  1. 加载时间优化: 通过压缩和合并 JavaScript文件,减小文件大小,以及使用异步加载和延迟加载的方式,可以提高网页的加载速度




  2. 避免使用AJAX技术加载核心内容: 对于核心内容,避免使用 AJAX 或动态加载方式,而是在初始页面加载时就呈现。这样可以确保搜索引擎能够正确抓取和索引核心内容,提高网页的可见性和相关性。




  3. 减少懒加载、瀑布流、上拉刷新、下载加载、点击更多等互动加载: 这些常见的页面优化方式虽然有利于用户体验。但搜索引擎爬虫不会执行 JavaScript,并且在抓取和索引页面时可能会忽略其中的动态内容。




  4. js阻塞后保证页面正常运行: 确保网站在没有 JavaScript 的情况下仍然能够正常运行。这有助于搜索引擎爬虫能够正确索引你的网页内容。




性能和体验优化



  1. 提高网站加载速度: 搜索引擎和用户都更喜欢快速加载的网页,提高页面的转加载速度,会对搜索引擎排名产生积极影响。

  2. 优化移动体验: 在移动设备上,用户的粘性和耐心被放大,优化移动体验,减少用户的流失率,会对移动搜索排名产生积极影响。

  3. 无障碍: 在 Web 开发无障碍性意味着使尽可能多的人能够使用 Web 站点, 增加用户人群的受众,会提高搜索引擎排名


内容更新



  1. 内容持续更新: 搜索引擎比较喜欢新鲜的内容,如果网站内容长期不更新的话,搜索引擎就会厌烦我们的网站。反之,我们频繁的更新新闻、博客等内容,会大大的提高

  2. 网页数量尽可能的多: 尽可能的让网页超过15个,



频繁修改或调整网站结构的话就相当于修改了搜索引擎爬取网站的路径,导致网站即使更新再多的内容也难以得到收录



监测


索引


在浏览器中输入 site:你的地址(此方法仅适合谷歌,百度则直接搜索URL地址)


查看是否被索引



  1. 进入Google Search Console

  2. 进入URL检测工具。

  3. 将需要索引的URL粘贴到搜索框中。

  4. 等待谷歌检测URL。

  5. 点击“请求编入索引”按钮。


image.png


收录


点击网址检查: 如果页面被索引,那么会显示“URL is on Google(URL在谷歌中)”。


image.png


如何去收录


image.png


但是,请求编入收录索引不太可能解决旧页面的索引问题,并且这只是一个最原始的方式,提交链接不能确保你的URL一定被收录,尤其是百度。


参考11个让百度快速收录网站的奇思淫技


总结


持续的优化和监测是关键,以确保你的策略和实践符合不断变化的搜索引擎算法和用户需求。


期待一个月后见分晓啦!


参考文献



  1. 11个让百度快速收录网站的奇思淫技

  2. search

  3. JavaScript与SEO之间的藕断丝连关系<
    作者:高志小鹏鹏
    来源:juejin.cn/post/7251786985535275067
    /a>

收起阅读 »

一次微前端的改造记录

web
前言 由于公司的一些需求,需要去了解 iframe 和 qiankun 两种微前端方案,特此记录一下。 微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独...
继续阅读 »

前言


由于公司的一些需求,需要去了解 iframe 和 qiankun 两种微前端方案,特此记录一下。


微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。


iframe


HTML 内联框架元素,能够将另一个 HTML 页面嵌入到当前页面


<iframe src="文件路径"></iframe>

postMessage




  • window.postMessage() 方法可以安全地实现跨源通信:postMessage 讲解




  • 通常来说,对于两个不同页面的脚本,只有当他们页面位于具有相同的协议(通常为 https),端口号(443 为 https 的默认值),以及主机时,这两个脚本才能互相通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全


    otherWindow.postMessage(message, targetOrigin, [transfer]);



  • postMessage 的兼容性




实现思路


整体架构


以我们公司的真实项目为例:


1687167610369.png


一个父站点,很多子站点,不同的子站点即为完全独立的不同的项目
父站点包括:



  1. 公共部分:header 部分、左侧菜单部分都是公共的组件

  2. 主区域部分(子站点区域):需要展示不同子站点的业务部分


父子站点通信(如何展示不同的站点页面)


上面已经介绍过 iframe 和 postMessage:我们通过 iframe 去加载不同项目的线上地址



  1. 我们新建一个通用组件,无论菜单路由指向何处都指向这个组件,渲染这个组件

  2. 在这个组件中监听路由的变化,返回不同的线上地址,让 iframe 去加载对应的内容(公司项目比较老,还是使用的 vue2)


<template>
<div class="container">
<iframe :src="src"></iframe>
</div>
</template>

<script>
export default {
data() {
return {
src: '',
};
},
mounted() {
this.updateIframe(); // 生命周期加载一次,否则页面空白,第一次监听不到
},
watch: {
$route() {
this.updateIframe();
},
},
methods: {
updateIframe() {
// 更新 src
},
},
};
</script>


  1. 菜单以及子站点线上地址怎么来:目前我们的做法是单独的菜单配置,通过接口去拿,配置菜单的时候同事配置好 iframe 线上地址,这样就可以一起拿到了
    image.png

  2. 那我们究竟如何通信呢?
    父站点(补充上面的代码):


<template>
<div class="container">
<iframe :src="src" id="iframe"></iframe>
</div>
</template>
<script>
export default {
data() {
return {
src: '',
};
},
mounted() {
this.getMessage();
this.updateIframe(); // 生命周期加载一次,否则页面空白,第一次监听不到,同样的也可以使用 iframe 的 onload 方法
},
watch: {
$route() {
this.updateIframe();
},
},
methods: {
updateIframe() {
// 更新 src
},
messageCallBack(data) {
// 这边可以接收一些子站点的数据,去做一些逻辑判断,比如要在iframe加载完之后,父站点再去发消息给子站点,不然肯定存在问题
// 可以传递一些信息给子站点
this.postMessage({
data: {},
});
},
postMessage(data) {
document.getElementById('iframe').contentWindow.postMessage(JSON.stringify(data), '*');
},
getMessage() {
window.addEventListener('message', this.messageCallBack);
},
},
};
</script>


  1. 子站点也一样在需要的地方通过 postMessage 去发送或者接受数据(比如我们子站点每次都加载首页,然后接收到路由信息,再在子项目中跳转到对应页面)


需要干掉 iframe 滚动条吗


当然需要,不然多丑,加入以下代码即可去掉:


#app {
-ms-overflow-style: none; /* IE 和 Edge 浏览器隐藏滚动条 */
scrollbar-width: none; /* FireFox隐藏浏览器滚动条 */
}
/* Chrome浏览器隐藏滚动条 */
#app::-webkit-scrollbar {
display: none;
}

弹窗是否能覆盖整个屏幕


UI 不同步,DOM 结构不共享。 iframe 里来一个带遮罩层的弹框,只会在 iframe 区域内,为了好看,我们需要让它在整个屏幕的中间


解决:



  • 使得 iframe 区域的宽高本身就和屏幕宽高相等,子站点内部添加 padding ,使内容区缩小到原本子站点 content 区域;

  • 正常情况下,父站点 header、左侧菜单部分的层级需要高于 iframe 的层级(iframe 不会阻止这些区域的点击);

  • 当用户点了新建按钮,对话框出现的时候,给父项目发送一条消息,让父项目调高 iframe 的层级,遮罩便可以覆盖全屏。


这样的解决的缺点:每次打开弹窗,都得先发送 postMessage 数据,逻辑显得多余,对于新手不友好;可是为了好看,只能这样了。


iframe 方案总结


好用的地方:



  • 业务解耦

  • 技术隔离,vue、react 互不影响

  • 项目拆分,上线快速,对其他项目无影响

  • iframe 的硬隔离使得各个项目的 JS 和 CSS 完全独立,不会产生样式污染和变量冲突


存在的缺点:



  • 布局约束:不给定高度,会塌陷;iframe 内的 div 无法全屏(iframe 标签设置 allow="fullscreen" 属性即可)

  • 不利于 seo,会当成 2 个页面,破坏了语义化 ,对无障碍可访问性支持不好

  • url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用

  • 性能开销,慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程


还需要解决的问题:



  • 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果;至于怎么传,可以根据每个公司得实际情况去定


qiankun


接下来在 iframe 的基础下扩展下 qiankun,在使用方面还是简单的
qiankun 使用指南


父项目中


值得注意的是,我们需要增加一个类型,如果是 qiankun 的项目,需要全部指向新增的 qiankun 组件


<template>
<div>
<div id="microContainer" class="microContainer"></div>
</div>
</template>

<script>
import fetchData from './fetchData'; // 一些逻辑处理
import { loadMicroApp } from 'qiankun';
import { getToken } from '@/...';

export default {
data() {
return {
microRef: null,
};
},
methods: {
fetch(route) {
fetchData(route).then(({ data }) => {
const { name, entry } = data;

this.microRef = loadMicroApp({
name,
entry,
container: '#yourContainer',
props: {
router: this.$router,
data: {
// 一些参数
},
token: getToken(),
},
});
});
},
unregisterApplication() {
this.microAppRef.mountPromise.then(() => this.microAppRef.unmount());
},
},
mounted() {
this.fetch(this.$route);
},
beforeDestroy() {
this.unregisterApplication();
},
};
</script>

子项目中


在 src 目录新增文件 public-path.js


iif (window.__POWERED_BY_QIANKUN__) {
// 动态设置子应用的基础路径
// 使用 window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ 变量来获取主应用传递的基础路径
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

子项目中需要导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用


const render = (parent = {}) => {
if (window.__POWERED_BY_QIANKUN__) {
// 渲染 qiankun 的路由
} else {
// 渲染正常的路由
}
};

//全局变量来判断环境,独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/

export async function bootstrap() {
console.log('react app bootstraped');
}

/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/

export async function mount(props) {
render(props.data);
}

/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/

export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root')
);
}

/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/

export async function update(props) {
console.log('update props', props);
}

子站点 webpack 配置


const packageName = require('./package.json').name;

module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd', // 把子应用打包成 umd 库格式,让 qiankun 拿到其 export 的生命周期函数
jsonpFunction: `webpackJsonp_${packageName}`,
},
};

路由方面:qiankun 使用父站点的路由,在子站点获取到路由信息,然后加载不同的组件,如果单独运行子站点,则需要适配自己的路由组件,做一些差异化处理就好了


qiankun 总结



  • qiankun 自带 js/css 沙箱功能

    • js 隔离:Proxy 沙箱,它将 window 上的所有属性遍历拷贝生成一个新的 fakeWindow 对象,紧接着使用 proxy 代理这个 fakeWindow,用户对 window 操作全部被拦截下来,只作用于在这个 fakeWindow 之上

    • css 隔离:ShadowDOM 样式沙箱会被开启。在这种模式下 qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响;



  • qiankun 支持子项目预请求功能

  • 支持复用公共依赖

    • webpack 配置 externals:子项目独立运行时,这些依赖的来源有且仅有 index.html 中的外链 script 标签

    • 可以给子项目 index.html 中公共依赖的 script 和 link 标签加上 ignore 属性;有了这个属性,qiankun 便不会再去加载这个 js/css,而子项目独立运行,这些 js/css 仍能被加载




存在的问题:



  • css 污染问题

    • 全局 CSS:如果子站点中使用了全局 CSS 样式(如直接写在 HTML 中的 标签或通过 引入的外部样式表),这些全局样式可能会影响整个页面,包括父站点和其他子站点。
    • CSS 命名冲突:如果子站点和父站点使用相同的 CSS 类名或样式选择器,它们的样式规则可能会相互覆盖或产生不可预料的效果。为避免冲突,可以采用命名约定或 CSS 模块化来隔离样式。




总结


目前只是初步接入了 qiankun,后续还会基于 qiankun 做一些优化,当然不考虑一些因素的情况下,个人觉得 iframe 依旧是最完美的沙箱隔离,当然目前在我们的项目中,他们是共存的,各有优劣。微前端也是前端工

作者:Breeze
来源:juejin.cn/post/7251495270800752700
程化一个重要的方案。

收起阅读 »

再学http-为什么文件上传要转成Base64?

web
1 前言 最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本...
继续阅读 »

1 前言


最近在开发中遇到文件上传采用Base64的方式上传,记得以前刚开始学http上传文件的时候,都是通过content-type为multipart/form-data方式直接上传二进制文件,我们知道都通过网络传输最终只能传输二进制流,所以毫无疑问他们本质上都是一样的,那么为什么还要先转成Base64呢?这两种方式有什么区别?带着这样的疑问我们一起来分析下。


2 multipart/form-data上传


先来看看multipart/form-data的方式,我在本地通过一个简单的例子来查看http multipart/form-data方式的文件上传,html代码如下


<!DOCTYPE html>
<html>
<head>
<title>上传文件示例</title>
<meta charset="UTF-8">
<body>
<h1>上传文件示例</h1>
<form action="/upload" method="POST" enctype="multipart/form-data">
<label for="file">选择文件:</label>
<input type="file" id="file" name="file"><br>
<label for="tx">说明:</label>
<input type="text" id="tx" name="remark"><br><br>
<input type="submit" value="上传">
</form>
</body>
</html>

页面展示也比较简单


image.png


选择文件点击上传后,通过edge浏览器f12进入调试模式查看到的请求信息。

请求头如下
image.png
在请求头里Content-Type 为 multipart/form-data; boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo,刚开始看肯定有点懵,不过其实也不复杂,可以简单理解为在请求体里要传递的参数被分为多部份,每一部分通过分解符boundary分割,就比如在这个例子,表单里有file和remark两个字段,则在请求体里就被分为两部分,每一部分通过boundary=----WebKitFormBoundary4TaNXEII3UbH8VKo来分隔(实际上还要加上CRLF回车换行符,回车表示将光标移动到当前行的开头,换行表示一行文本的结束,也就是新文本行的开始)。需要注意下当最后一部分结尾时需要加多两个"-"结尾。

我们继续来看请求体


image.png
第一部分是file字段部分,它的Content-Type为image/png,第二部分为remark字段部分,它没有声明Content-Type,则默认为text/plain纯文本类型,也就是在例子中输入的“测试”,到这里大家肯定会有个疑问,上传的图片是放在哪里的,这里怎么没看到呢?别急,我猜测是浏览器做了特殊处理,请求体里不显示二进制流,我们通过Filder抓包工具来验证下。


image.png
可以看到在第一部分有一串乱码显示,这是因为图片是二进制文件,显示成文本格式自然就乱码了,这也证实了二进制文件也是放在请求体里。后端使用框架springboot通过MultipartFile接受文件也是解析请求体的每一部分最终拿到二进制流。


@RestController
public class FileController {
// @RequestParam可接收Content-Type 类型为:multipart/form-data 
// 或 application/x-www-form-urlencoded 请求体的内容
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
return "test";
}
}

到此multipart/form-data方式上传文件就分析完了,关于multipart/form-data官方说明可参考 RFC 7578 - Returning Values from Forms: multipart/form-data (ietf.org)


3 Base64上传


在http的请求方式中,文件上传只能通过multipart/form-data的方式上传,这样一来就会有比较大的限制,那有没其他方式可以突破这一限制,也就是说我可以通过其他的请求方式上传,比如application/json?当然有,把文件当成一个字符串,和其他普通参数没什么两样,我们可以通过其他任意请求方式上传。如果转成了字符串,那上传文件就比较简单了,但问题是我们怎么把二进制流转成字符串,因为这里面可能会有很多“坑”,业界一般的做法是通过Base64编码把二进制流转成字符串,那为什么不直接转成字符串而要先通过Base64来转呢?我们下面来分析下。


3.1 Base64编码原理


在分析原理之前,我们先来回答什么是Base64编码?首先我们要知道Base64只是一种编码方式,并不是加解密算法,因此Base64可以编码,那也可以解码,它只是按照某种编码规则把一些不可显示字符转成可显示字符。这种规则的原理是把要编码字符的二进制数每6位分为一组,每一组二进制数可对应Base64编码的可打印字符,因为一个字符要用一个字节显示,那么每一组6位Base64编码都要在前面补充两个0,因此总长度比编码前多了(2/6) = 1/3,因为6和8最小公倍数是24,所以要编码成Base64对字节数的要求是3的倍数(24/8=3字节),对于不足字节的需要在后面补充字节数,补充多少个字节就用多少个"="表示(一个或两个),这么说有点抽象,我们通过下面的例子来说明。

我们对ASCII码字符串"AB\nC"(\n和LF都代表换行)进行Base64编码,因为一共4字节,为了满足是3的倍数需要扩展到6个字节,后面补充了2个字节。


image.png


表3.1


转成二级制后每6位一组对应不同颜色,每6位前面补充两个0组成一个字节,最终Base64编码字符是QUIKQw==,Base64编码表大家可以自行网上搜索查看。


image.png
我们通过运行程序来验证下


image.png
最终得出的结果与我们上面推理的一样。


3.2 Base64编码的作用


在聊完原理之后,我们继续来探讨文件上传为什么要先通过Base64编码转成字符串而不直接转成字符串?一些系统对特殊的字符可能存在限制或者说会被当做特殊含义来处理,直接转成普通字符串可能会失真,因此上传文件要先转成Base64编码字符,不能把二进制流直接字符串。


另外,相比较multipart/form-data Base64编码文件上传比较灵活,它不受请求类型的限制,可以是任何请求类型,因为最终就是一串字符串,相当于请求的一个参数字段,它不像二进制流只能限定multipart/form-data的请求方式,日常开发中,我们用的比较多的是通过apllication/json的格式把文件字段放到请求体,这种方式提供了比较便利的可操作性。


4 总结


本文最后再来总结对比下这两种文件上传的方式优缺点。

(1)multipart/form-data可以传输二进制流,效率较高,Base64需要编码解码,会耗费一定的性能,效率较低。

(2)Base64不受请求方式的限制,灵活度高,http文件二进制流方式传输只能通过multipart/form-data的方式,灵活度低。

因为随着机器性能的提升,小文件通过二进制流传输和字符串传输,我们对这两种方式时间延迟的感知差异并不那么明显,因此大部分情况下我们更多考虑的是灵活性,所以采用Base64编码的情况也就比较多。


作者:初心不改_1
来源:juejin.cn/post/7251131990438264889
收起阅读 »

为啥你的tree的checkbox隐藏的这么艰难

web
场景: 近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用 element-ui 的 tree 还不支持特定节点的check...
继续阅读 »

场景:


近期在实现一个基于element-ui 的 Tree 组件的场景, 产品要求, 部门的数据,都不要checkbox, 只有节点值为 员工 才显示,而且还要部分员工的checkbox 禁用


element-ui 的 tree 还不支持特定节点的checkbox隐藏功能, 网上大多采用 class 的方式,将第一层的checkbox进行了隐藏, 但是不满足我们的要求


规则:



  • 第一层节点不显示checkbox

  • 后续任意子节点,如果数据为部门 则也不显示 checkbox

  • 后端返回的部分数据,如果人员符合特定规则(根据自己场景来即可),则表现为 禁用 checkbox


实现


数据
treeData.js


export default [
{
"id":1,
"label":"一级 1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":4,
"label":"二级 1-1-是部门",
"depType":1,
"disabled":false,
"children":[
{
"id":9,
"label":"三级 1-1-9",
"disabled":false
},
{
"id":25,
"label":"三级 1-1-25",
"disabled":false
},
{
"id":27,
"label":"三级 1-1-27",
"disabled":false
},
{
"id":30,
"label":"三级 1-30",
"disabled":false
},
{
"id":10,
"label":"三级 1-1-2 是部门",
"depType":5,
"disabled":false
}
]
}
]
},
{
"id":2,
"label":"一级 2 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":5,
"label":"二级 2-1 张三",
"disabled":false
},
{
"id":6,
"label":"二级 2-2 李四",
"disabled":false
}
]
},
{
"id":3,
"label":"一级 3 部门",
"depType":1,
"disabled":false,
"children":[
{
"id":7,
"depType":1,
"label":"二级 3-1 王武",
"disabled":false
},
{
"id":8,
"label":"二级 3-2 赵柳",
"disabled":false
}
]
}
]

上述数据,有的有 deptType字段 ,有的节点没有, 这其实是业务场景的特殊规则,有deptType的认为这个节点为部门节点,没有的则为 员工


<template>
<div>
<el-tree
node-key="id"
show-checkbox
:data="treeData"
:render-content="renderContent"
class="tree-box"
@node-expand='onNodeExpand'
>
</el-tree>
<div>

<ul>
<li>一开始的数据结构必须都有 disabled字段, 默认不禁用,设置为 false 否则会出现视图的响应式延迟问题</li>
<li>是否禁用某个节点,根据renderContent 里面的规则来的, 规则是, 只要是部门的维度,就禁用 设置 data.disabled= true</li>
<li>tree的第一层节点隐藏,是通过js控制的</li>
</ul>
</div>
</div>

</template>

<script>
import treeData from './treeData.js'

export default {
name: 'render-content-tree',
data() {
return {
treeData
}
},
mounted() {
let nodes = document.querySelector('.tree-box')
let children = nodes.querySelectorAll('.el-tree-node')

for(let i=0; i< children.length; i++) {
children[i].querySelector('.el-checkbox').style.display = 'none'
}

// 第一层不要checkbox
// 后续根据规则来
},

methods: {
renderContent(h, { node, data, store }) {
// console.log(node, data)

// 如果不是一级节点,并且符合数据的特定要求,比如这里是 id 大于27 的数据,禁用掉
if (node.level !== 1 && data.id > 27) {
data.disabled = true
}

return h('div',
{
// 如果是部门,就将所有的 checkbox 都隐藏
class: data.depType === undefined ? '' : 'dept-node'
},
data.label)
},

setDeptNodeHide() {
let deptNodes = document.querySelectorAll('.dept-node')

for(let i=0; i<deptNodes.length; i++) {
let checkbox = deptNodes[i].parentNode.querySelector('.el-checkbox')

checkbox.style.display = 'none'
}
},

onNodeExpand(data, node, com) {
// console.log(data);
// console.log(node);
// console.log(com);

this.$nextTick(() => {
this.setDeptNodeHide()
})
}
}
}
</script>

image.png


节点初次渲染的效果.png




展开后的效果


image.png


部门节点没有checkbox, 符合特定规则的c

作者:知了清语
来源:juejin.cn/post/7250040492162433081
heckbox 禁用

收起阅读 »

手撸一个私信功能

web
前言 几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了, 这篇文章就直接粘了之前的代码简单的改了改,说明一下问题; 主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等; 也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题...
继续阅读 »

前言


几年前的项目里写了一pc版的私信功能,使用的版本和代码比较老了,

这篇文章就直接粘了之前的代码简单的改了改,说明一下问题;

主要就是写一下这个功能如何下手,思想,以及界面如何整,消息怎么发等;

也只是截取了当时项目里私信的一部分功能,这个完全可以说明问题了;


效果


界面大概是这样的
image.png


整体动态效果是这样的


test 00_00_00-00_00_30~1.gif


test1 00_00_00-00_00_30.gif


说下大致思路吧


首先是把界面分成左边和右边,左边占少一部分,是朋友目录界面;

右边占多一点,右边是聊天的详情界面;

点击左边对应的那个人,右边就会出现本人跟点击的那个人的聊天详情;


左边人员目录的思路


左边的人员目录和显示的头像,最新的一条消息还有时间,这些都是后端返给前端的;

前端把数据展现出来就行,

时间那里可以根据公司需求以及后端返回的格式转成前天,刚刚等根据需求而定;

我这块时间项目中是有分开前天,昨天,刚刚的,

只不过这里就自己造的数据时间随便写的;

当然这里数据多的时候,可做成虚拟滚动效果;
每个人头像那个红色是消息数量,当读完消息时,就恢复成剩下的消息数量;


右边聊天详情的思路


右边是左边点击对应的聊天人员时,

拿这个人的id之类的数据去请求后端,拿对应的聊天详情数据;

最下面的显示的是最新的聊天信息,后端给的排序不对,可自己反转去排序;

这里也做成虚拟滚动;

最上面显示的那个名称是当前和谁聊天的那个人的昵称;


image.png


聊天界面里也显示的是时间,昵称,头像,聊天信息内容,

时间也需要分昨天,前天,刚刚等。。。


发送消息的思路


我这里也做了按键和点击按钮两种方式;

按键就是在代码里添加一个键盘的监听事件就可;


    var footerTarget = document.getElementById('footer');
footerTarget.addEventListener('keydown', this.footerKeydown);

Enter按键是13;



//底部keydown监听事件
footerKeydown = (e) => {
if (e?.keyCode === 13) {
this.handleSubmit();
}
};

发送消息界面其实就是个表单,做成那个样子就可以啦;

发送消息时,调用后端接口,把这条消息添加在消息数据后面就可;


结尾


只是简单写下思路就已经写这么多了;

代码后面有空给粘上;

由于我是临时把几年前的代码拿出来粘的,

为了显示效果,数据也是自己造的,

一些时间呀以及显示,已读信息的数量呀以及其他一些细节都没有管,

实际项目中直接对应接口嘛,

所以这里就只是随便

作者:浅唱_那一缕阳光
来源:juejin.cn/post/7250029035744149541
改改说明一下问题哈;

收起阅读 »

我工作中用到的性能优化全面指南

web
在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。 最小化和压缩代码 在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除...
继续阅读 »

在Web开发中,Web的性能优化是一个重要的话题。无论是页面加载速度,用户体验,或者是程序运行效率,都与Web的性能优化息息相关。



最小化和压缩代码


在构建过程中,为了减少文件的大小和加载时间,通常会对JavaScript代码进行最小化和压缩处理。这包括移除不必要的空格、换行、注释,以及缩短变量和函数名。工具如UglifyJS和Terser等可以帮助我们完成这个任务。


// 原始代码
function hello(name) {
let message = 'Hello, ' + name;
console.log(message);
}

// 压缩后的代码
function hello(n){var e='Hello, '+n;console.log(e)}

利用浏览器缓存


浏览器缓存是提升Web应用性能的一个重要手段。我们可以将一些经常用到的、变化不大的数据存储在本地,以减少对服务器的请求。例如,可以使用localStorage或sessionStorage来存储这些数据。


// 存储数据
localStorage.setItem('name', 'John');

// 获取数据
var name = localStorage.getItem('name');

// 移除数据
localStorage.removeItem('name');

// 清空所有数据
localStorage.clear();

避免过度使用全局变量


全局变量会占用更多的内存,并且容易导致命名冲突,从而降低程序的运行效率。我们应尽量减少全局变量的使用。


// 不好的写法
var name = 'John';

function greet() {
console.log('Hello, ' + name);
}

// 好的写法
function greet(name) {
console.log('Hello, ' + name);
}

greet('John');

使用事件委托减少事件处理器的数量


事件委托是将事件监听器添加到父元素,而不是每个子元素,以此来减少事件处理器的数量,并且提升性能。


document.getElementById('parent').addEventListener('click', function (event) {
if (event.target.classList.contains('child')) {
// 处理点击事件...
}
});

好的,下面我会详细解释一下这些概念以及相关的示例:


async 和 defer


asyncdefer 是用于控制 JavaScript 脚本加载和执行的 HTML 属性。



  • async 使浏览器在下载脚本的同时,继续解析 HTML。一旦脚本下载完毕,浏览器将中断 HTML 解析,执行脚本,然后继续解析 HTML。


<script async src="script.js"></script>


  • defer 也使浏览器在下载脚本的同时,继续解析 HTML。但是,脚本的执行会等到 HTML 解析完毕后再进行。


<script defer src="script.js"></script>

在需要控制脚本加载和执行的时机以优化性能的场景中,这两个属性是非常有用的。


防抖和节流


throttle(节流)和 debounce(防抖)。



  • throttle 保证函数在一定时间内只被执行一次。例如,一个常见的使用场景是滚动事件的监听函数:


function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
return func(...args);
};
}

window.addEventListener('scroll', throttle(() => console.log('Scrolling'), 100));


  • debounce 保证在一定时间内无新的触发后再执行函数。例如,实时搜索输入的监听函数:


function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}

searchInput.addEventListener('input', debounce(() => console.log('Input'), 300));

利用虚拟DOM和Diff算法进行高效的DOM更新


当我们频繁地更新DOM时,可能会导致浏览器不断地进行重绘和回流,从而降低程序的性能。因此,我们可以使用虚拟DOM和Diff算法来进行高效的DOM更新。例如,React和Vue等框架就使用了这种技术。


// React示例
class Hello extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}

ReactDOM.render(<Hello name="John" />, document.getElementById('root'));

避免长时间运行的任务


浏览器单线程的运行方式决定了JavaScript长时间运行的任务可能会阻塞UI渲染和用户交互,从而影响性能。对于这类任务,可以考虑将其分解为一系列较小的任务,并在空闲时执行,这就是“分片”或者“时间切片”的策略。


function chunk(taskList, iteration, context) {
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && taskList.length > 0) {
iteration.call(context, taskList.shift());
}

if (taskList.length > 0) {
chunk(taskList, iteration, context);
}
});
}

chunk(longTasks, (task) => {
task.execute();
}, this);

虚拟列表(Virtual List)


当我们在页面上渲染大量的元素时,这可能会导致明显的性能问题。虚拟列表是一种技术,可以通过只渲染当前可见的元素,来优化这种情况。



虚拟列表的等高方式实现:



// 列表项高度
const ITEM_HEIGHT = 20;

class VirtualList {
constructor(container, items, renderItem) {
this.container = container;
this.items = items;
this.renderItem = renderItem;

this.startIndex = 0;
this.endIndex = 0;
this.visibleItems = [];

this.update();

this.container.addEventListener('scroll', () => this.update());
}

update() {
const viewportHeight = this.container.clientHeight;
const scrollY = this.container.scrollTop;

this.startIndex = Math.floor(scrollY / ITEM_HEIGHT);
this.endIndex = Math.min(
this.startIndex + Math.ceil(viewportHeight / ITEM_HEIGHT),
this.items.length
);

this.render();
}

render() {
// 移除所有的可见元素
this.visibleItems.forEach((item) => this.container.removeChild(item));
this.visibleItems = [];

// 渲染新的可见元素
for (let i = this.startIndex; i < this.endIndex; i++) {
const item = this.renderItem(this.items[i]);
item.style.position = 'absolute';
item.style.top = `${i * ITEM_HEIGHT}px`;
this.visibleItems.push(item);
this.container.appendChild(item);
}
}
}

// 使用虚拟列表
new VirtualList(
document.getElementById('list'),
Array.from({ length: 10000 }, (_, i) => `Item ${i}`),
(item) => {
const div = document.createElement('div');
div.textContent = item;
return div;
}
);


优化循环


在处理大量数据时,循环的效率是非常重要的。我们可以通过一些方法来优化循环,例如:避免在循环中进行不必要的计算,使用倒序循环,使用forEach或map等函数。


// 不好的写法
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

// 好的写法
let length = arr.length;
for (let i = 0; i < length; i++) {
console.log(arr[i]);
}

// 更好的写法
arr.forEach(function (item) {
console.log(item);
});

避免阻塞UI


JavaScript的运行是阻塞UI的,当我们在进行一些耗时的操作时,应尽量使用setTimeout或Promise等异步方法,以避免阻塞UI。


setTimeout(function () {
// 执行耗时的操作...
}, 0);

使用合适的数据结构和算法


使用合适的数据结构和算法是优化程序性能的基础。例如,当我们需要查找数据时,可以使用对象或Map,而不是数组;当我们需要频繁地添加或移除数据时,可以使用链表,而不是数组。


// 使用对象进行查找
var obj = { 'John': 1, 'Emma': 2, 'Tom': 3 };
console.log(obj['John']);

// 使用Map进行查找
var map = new Map();
map.set('John', 1);
map.set('Emma', 2);
map.set('Tom', 3);
console.log(map.get('John'));

避免不必要的闭包


虽然闭包在某些情况下很有用,但是它们也会增加额外的内存消耗,因此我们应该避免不必要的闭包。


// 不必要的闭包
function createFunction() {
var name = 'John';
return function () {
return name;
}
}

// 更好的方式
function createFunction() {
var name = 'John';
return name;
}

避免使用with语句


with语句会改变代码的作用域,这可能会导致性能问题,因此我们应该避免使用它。


// 不好的写法
with (document.getElementById('myDiv').style) {
color = 'red';
backgroundColor = 'black';
}

// 好的写法
var style = document.getElementById('myDiv').style;
style.color = 'red';
style.backgroundColor = 'black';

避免在for-in循环中使用hasOwnProperty


hasOwnProperty方法会查询对象的整个原型链,这可能会影响性能。在for-in循环中,我们应该直接访问对象的属性。


// 不好的写法
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(key + ': ' + obj[key]);
}
}

// 好的写法
for (var key in obj) {
console.log(key + ': ' + obj[key]);
}

使用位操作进行整数运算


在进行整数运算时,我们可以使用位操作符,它比传统的算术运算符更快。


// 不好的写法
var half = n / 2;

// 好的写法
var half = n >> 1;

避免在循环中创建函数


在循环中创建函数会导致性能问题,因为每次迭代都会创建一个新的函数实例。我们应该在循环外部创建函数。


// 不好的写法
for (var i = 0; i < 10; i++) {
arr[i] = function () {
return i;
}
}

// 好的写法
function createFunction(i) {
return function () {
return i;
}
}

for (var i = 0; i < 10; i++) {
arr[i] = createFunction(i);
}

使用Web Worker进行多线程处理


JavaScript默认是单线程运行的,但我们可以使用Web Worker来进行多线程处理,以提升程序的运行效率。


// 主线程
var worker = new Worker('worker.js');

worker.onmessage = function (event) {
console.log('Received message ' + event.data);
}

worker.postMessage('Hello Worker');

// worker.js
self.onmessage = function(event) {
console.log('Received message ' + event.data);
self.postMessage('You said: ' + event.data);
};

使用WebAssembly进行性能关键部分的开发


WebAssembly是一种新的编程语言,它的代码运行速度接近原生代码,非常适合于进行性能关键部分的开发。例如,我们可以用WebAssembly来开发图形渲染、物理模拟等复杂任务。


// 加载WebAssembly模块
WebAssembly.instantiateStreaming(fetch('module.wasm'))
.then(result => {
// 调用WebAssembly函数
result.instance.exports.myFunction();
});

使用内存池来管理对象


当我们频繁地创建和销毁对象时,可以使用内存池来管理这些对象,以避免频繁地进行内存分配和垃圾回收,从而提升性能。


class MemoryPool {
constructor(createObject, resetObject) {
this.createObject = createObject;
this.resetObject = resetObject;
this.pool = [];
}

acquire() {
return this.pool.length > 0 ? this.resetObject(this.pool.pop()) : this.createObject();
}

release(obj) {
this.pool.push(obj);
}
}

var pool = new MemoryPool(
() => { return {}; },
obj => { for (var key in obj) { delete obj[key]; } return obj; }
);

使用双缓冲技术进行绘图


当我们需要进行频繁的绘图操作时,可以使用双缓冲技术,即先在离屏画布上进行绘图,然后一次性将离屏画布的内容复制到屏幕上,这样可以避免屏幕闪烁,并且提升绘图性能。


var offscreenCanvas = document.createElement('canvas');
var offscreenContext = offscreenCanvas.getContext('2d');

// 在离屏画布上进行绘图...
offscreenContext.fillRect(0, 0, 100, 100);

// 将离屏画布的内容复制到屏幕上
context.drawImage(offscreenCanvas, 0, 0);

使用WebGL进行3D渲染


WebGL是一种用于进行3D渲染的Web标准,它提供了底层的图形API,并且能够利用GPU进行加速,非常适合于进行复杂的3D渲染。


var canvas = document.getElementById('myCanvas');
var gl = canvas.getContext('webgl');

// 设置清空颜色缓冲区的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0);

// 清空颜色缓冲区
gl.clear(gl.COLOR_BUFFER_BIT);

使用Service Workers进行资源缓存


Service Workers可以让你控制网页的缓存策略,进一步减少HTTP请求,提升网页的加载速度。例如,你可以将一些不常变化的资源文件预先缓存起来。


// 注册一个service worker
navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
}).catch(function(error) {
console.log('ServiceWorker registration failed: ', error);
});

// service-worker.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('my-cache').then(function(cache) {
return cache.addAll([
'/style.css',
'/script.js',
// 更多资源...
]);
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

使用内容分发网络(CDN)


你可以将静态资源(如JavaScript、CSS、图片等)上传到CDN,这样用户可以从离他们最近的服务器下载资源,从而提高下载速度。


<!-- 从CDN加载jQuery库 -->
<script src="https://cdn.example.com/jquery.min.js"></script>

使用HTTP/2进行资源加载


HTTP/2支持头部压缩和多路复用,可以更高效地加载资源。如果你的服务器和用户的浏览器都支持HTTP/2,那么你可以使用它来提高性能。


// 假设我们有一个HTTP/2库
var client = new Http2Client('https://example.com');

client.get('/resource1');
client.get('/resource2');

使用Web Socket进行数据通信


如果你需要频繁地与服务器进行数据交换,可以使用Web Socket,它比HTTP有更低的开销。


var socket = new WebSocket('ws://example.com/socket');

socket.addEventListener('open', function() {
socket.send('Hello, server');
});

socket.addEventListener('message', function(event) {
console.log('Received message from server: ' + event.data);
});

使用Progressive Web Apps(PWA)技术


PWA可以让你的网站在离线时仍然可用,并且可以被添加到用户的主屏幕,提供类似于原生应用的体验。PWA需要使用Service Workers和Manifest等技术。


// 注册Service Worker
navigator.serviceWorker.register('/service-worker.js');

// 检测是否支持Manifest
if ('manifest' in document.createElement('link')) {
var link = document.createElement('link');
link.rel = 'manifest';
link.href = '/manifest.json';
document.head.appendChild(link);
}

使用WebRTC进行实时通信


WebRTC是一种提供实时通信(RTC)能力的技术,允许数据直接在浏览器之间传输,对于需要实时交互的应用,如视频聊天、实时游戏等,可以使用WebRTC来提高性能。


var pc = new RTCPeerConnection();

// 发送offer
pc.createOffer().then(function(offer) {
return pc.setLocalDescription(offer);
}).then(function() {
// 发送offer给其他浏览器...
});

// 收到answer
pc.setRemoteDescription(answer);

使用IndexedDB存储大量数据


如果你需要在客户端存储大量数据,可以使用IndexedDB。与localStorage相比,IndexedDB可以存储更大量的数据,并且支持事务和索引。


var db;
var request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = function(event) {
db = event.target.result;
var store = db.createObjectStore('myStore', { keyPath: 'id' });
store.createIndex('nameIndex', 'name');
};
request.onsuccess = function(event) {
db = event.target.result;
};
request.onerror = function(event) {
// 错误处理...
};

使用Web Push进行后台消息推送


Web Push允许服务器在后台向浏览器推送消息,即使网页已经关闭。这需要在Service Worker中使用Push API和Notification API。


// 请求推送通知的权限
Notification.requestPermission().then(function(permission) {
if (permission === 'granted') {
console.log('Push notification permission granted');
}
});

// 订阅推送服务
navigator.serviceWorker.ready.then(function(registration) {
registration.pushManager.subscribe({ userVisibleOnly: true }).then(function(subscription) {
console.log('Push subscription: ', subscription);
});
});

// 在Service Worker中接收和显示推送通知
self.addEventListener('push', function(event) {
var data = event.data.json();
self.registration.showNotification(data.title, data);
});

通过服务器端渲染(SSR)改善首次页面加载性能


服务器端渲染意味着在服务器上生成HTML,然后将其发送到客户端。这可以加快首次页面加载速度,因为用户可以直接看到渲染好的页面,而不必等待JavaScript下载并执行。这对于性能要求很高的应用来说,是一种有效的优化手段。


// 服务器端
app.get('/', function(req, res) {
const html = ReactDOMServer.renderToString(<MyApp />);
res.send(`<!DOCTYPE html><html><body>${html}</body></html>`);
});

利用HTTP3/QUIC协议进行资源传输


HTTP3/QUIC协议是HTTP/2的后续版本,采用了全新的底层传输协议(即QUIC),以解决HTTP/2中存在的队头阻塞(Head-of-line Blocking)问题,从而进一步提高传输性能。如果你的服务器和用户的浏览器都支持HTTP3/QUIC,那么可以考虑使用它进行资源传输。


使用Service Worker与Background Sync实现离线体验


通过Service Worker,我们可以将网络请求与页面渲染解耦,从而实现离线体验。并且,结合Background Sync,我们可以在用户离线时提交表单或同步数据,并在用户重新联网时自动重试。


// 注册Service Worker
navigator.serviceWorker.register('/sw.js');

// 提交表单
fetch('/api/submit', {
method: 'POST',
body: new FormData(form)
}).catch(() => {
// 如果请求失败,使用Background Sync重试
navigator.serviceWorker.ready.then(reg => {
return reg.sync.register('sync-submit');
});
});

// 在Service Worker中监听sync事件
self.addEventListener('sync', event => {
if (event.tag === 'sync-submit') {
event.waitUntil(submitForm());
}
});

使用PostMessage进行跨文档通信


如果你的应用涉及到多个窗口或者iframe,你可能需要在他们之间进行通信。使用postMessage方法可以进行跨文档通信,而不用担心同源策略的问题。


// 父窗口向子iframe发送消息
iframeElement.contentWindow.postMessage('Hello, child', 'https://child.example.com');

// 子iframe接收消息
window.addEventListener('message', function(event) {
if (event.origin !== 'https://parent.example.com') return;
console.log('Received message: ' + event.data);
});

使用Intersection Observer进行懒加载


Intersection Observer API可以让你知道一个元素何时进入或离开视口,这对于实现图片或者其他资源的懒加载来说非常有用。


var images = document.querySelectorAll('img.lazy');

var observer = new IntersectionObserver(function(entries, observer) {
entries.forEach(entry => {
if (entry.isIntersecting) {
var img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});

images.forEach(img => {
observer.observe(img);
});

利用OffscreenCanvas进行后台渲染


OffscreenCanvas API使得开发者可以在Web Worker线程中进行Canvas渲染,这可以提高渲染性能,尤其是在进行大量或者复杂的Canvas操作时。


var offscreen = new OffscreenCanvas(256, 256);
var ctx = offscreen.getContext('2d');

// 在后台线程中进行渲染...

利用Broadcast Channel进行跨标签页通信


Broadcast Channel API提供了一种在同源的不同浏览器上下文之间进行通信的方法,这对于需要在多个标签页之间同步数据的应用来说非常有用。


var channel = new BroadcastChannel('my_channel');

// 发送消息
channel.postMessage('Hello, other tabs');

// 接收消息
channel.onmessage = function(event) {
console.log('Received message: ' + event.data);
};

使用Web Cryptography API进行安全操作


Web Cryptography API 提供了一组底层的加密API,使得开发者可以在Web环境中进行安全的密码学操作,例如哈希、签名、加密、解密等。


window.crypto.subtle.digest('SHA-256', new TextEncoder().encode('Hello, world')).then(function(hash) {
console.log(new Uint8Array(hash));
});

使用Blob对象进行大型数据操作


Blob对象代表了一段二进制数据,可以用来处理大量的数据,比如文件。它们可以直接从服务端获取,或者由客户端生成,这对于处理大型数据或者二进制数据很有用。


var fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', function(event) {
var file = event.target.files[0];
var reader = new FileReader();
reader.onload = function(event) {
var contents = event.target.result;
processContents(contents);
};
reader.readAsArrayBuffer(file);
});

使用Page Visibility API进行页面可见性调整


Page Visibility API提供了一种方式来判断页面是否对用户可见。利用这个API,你可以在页面不可见时停止或减慢某些操作,例如动画或视频,从而节省CPU和电池使用。


document.addEventListener('visibilitychange', function() {
if (document.hidden) {
pauseAnimation();
} else {
resumeAnimation();
}
});

使用WeakMap和WeakSet进行高效的内存管理


在处理大量数据时,如果不小心可能会产生内存泄漏。WeakMap和WeakSet可以用来保存对对象的引用,而不会阻止这些对象被垃圾回收。这在一些特定的应用场景中,例如缓存、记录对象状态等,可能非常有用。


let cache = new WeakMap();

function process(obj) {
if (!cache.has(obj)) {
let result = /* 对obj进行一些复杂的处理... */
cache.set(obj, result);
}

return cache.get(obj);
}

使用requestAnimationFrame进行动画处理


requestAnimationFrame能够让浏览器在下一次重绘之前调用指定的函数进行更新动画,这样可以保证动画的流畅性,并且减少CPU的使用。


function animate() {


// 更新动画...
requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

使用CSS3动画替代JavaScript动画


CSS3动画不仅可以提供更好的性能,还可以在主线程之外运行,从而避免阻塞UI。因此,我们应该尽可能地使用CSS3动画替代JavaScript动画。


@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

.myDiv {
animation: fadeIn 2s ease

-in-out;
}

避免回流和重绘


回流和重绘是浏览器渲染过程中的两个步骤,它们对性能影响很大。优化的关键在于尽可能减少触发回流和重绘的操作,例如一次性修改样式,避免布局抖动等。


var el = document.getElementById('my-el');
el.style.borderLeft = '1px';
el.style.borderRight = '2px';
el.style.padding = '5px';
// 尽量避免上面的写法,以下为优化后的写法
el.style.cssText += 'border-left: 1px; border-right: 2px; padding: 5px;';

使用CSS3硬件加速提高渲染性能


使用 CSS3 的 transform 属性做动画效果,可以触发硬件加速,从而提高渲染性能。


element.style.transform = 'translate3d(0, 0, 0)';

避免使用同步布局


同步布局(或强制布局)是指浏览器强制在 DOM 修改和计算样式之后,立即进行布局。这会中断浏览器的优化过程,导致性能下降。一般出现在连续的样式修改和读取操作之间。


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 读样式,导致同步布局
let width = div.offsetWidth;
// 再写样式
div.style.height = width + 'px'; // 强制布局

为避免这个问题,可以将读操作移到所有写操作之后:


let div = document.querySelector('div');

// 写样式
div.style.width = '100px';
// 写样式
div.style.height = '100px';

// 读样式
let width = div.offsetWidth;

使用ArrayBuffer处理二进制数据


ArrayBuffer 提供了一种处理二进制数据的高效方式,例如图像,声音等。


var buffer = new ArrayBuffer(16);
var int32View = new Int32Array(buffer);
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}

利用ImageBitmap提高图像处理性能


ImageBitmap对象提供了一种在图像处理中避免内存拷贝的方法,可以提高图像处理的性能。


var img = new Image();
img.src = 'image.jpg';
img.onload = function() {
createImageBitmap(img).then(function(imageBitmap) {
// 在这里使用 imageBitmap
});
};
作者:linwu
来源:juejin.cn/post/7249991926307864613

收起阅读 »

我看UI小姐姐就是在为难我这个切图仔

web
前言 改成这个样子 咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢. 实现过程 刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的bord...
继续阅读 »

前言



image.png


改成这个样子


image.png


咱也不懂啊,这样更好看了吗,只能照着改了,谁让我只是个卑微的切图仔呢.


image.png


实现过程


刚开始我觉得很简单嘛,封装一个组件,用它包裹表单元素,比如Input、 Select、DatePicker等,然后修改css样式,把表单元素的border干掉,给外面的组件加上border不就搞定了,看起来也不是很复杂的样子.第一版长这样


image.png


发现问题了嘛,select下拉选项的宽度和表单元素不一样长,当然我觉得问题不大能用就行,但是在ui眼里那可不行,必须要一样长,不然不好看.
好吧,在我的据理力争下,我妥协啦,开始研究下一版.


image.png


在第一版的基础上我发现只有Select有这个问题,那就好办了,针对它单独处理就行了,解决方法思考了3种:



  • 第一种就是antd的Select可以设置dropdownStyle,通过获取父元素的宽度来设置下拉菜单的宽度,以此达到等长的目的

  • 第二种就是通过设置label元素为绝对定位,同时设置Select的paddingLeft

  • 还有一种就是通过在Select里添加css伪元素(注意这种方法需要把content里的中文转成unicode编码,不然可能会乱码)


最终我采用的是第二种方法,具体代码如下


import React, { CSSProperties, PropsWithChildren, useMemo } from 'react';
import { Form, FormItemProps, Col } from 'antd';
import styles from './index.module.less';

interface IProps extends FormItemProps {
label?: string;
style?: CSSProperties;
className?: string;
isSelect?: boolean;
noMargin?: boolean;
col?: number;
}
export const WrapFormComponent = ({ children, className, isSelect, style, col, noMargin = true, ...props }: PropsWithChildren<IProps>) => {
const labelWidth = useMemo(() => {
if (!isSelect || !props.label) return 11;
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context!.font = '12px PingFang SC';
const metrics = context!.measureText(props.label);
return metrics.width + (props.colon === undefined || props.colon ? 10 : 0) + 11;
}, [isSelect, props.label, props.colon]);
return (
<Col span={col}>
<Form.Item
style={{ '--label-length': labelWidth + 'px', marginBottom: noMargin ? 0 : '16px', ...style } as CSSProperties}
className={`${styles['wrap-form']} ${isSelect ? styles['wrap-form-select'] : ''} ${className || ''}`}
{...props}
>

{children}
</Form.Item>
</Col>

);
};


less代码


.wrap-form {
padding: 0 !important;
padding-left: 11px !important;
border: 1px solid #c1c7cd;
border-radius: 4px;

:global {
.ant-form-item-label {
display: inline-flex !important;
align-items: center !important;
}

.ant-form-item-label > label {
height: auto;
color: #919399;
font-weight: 400;
}

.ant-picker {
width: 100%;
}

.ant-input,
.ant-select-selector,
.ant-picker,
.ant-input-number {
border: none;
border-color: transparent !important;
}

.ant-input-affix-wrapper {
background: none;
border: none;
}
}
}

.wrap-form-select {
position: relative;
padding: 0 !important;

:global {
.ant-form-item-label {
position: absolute;
top: 50%;
left: 11px;
z-index: 1;
text-align: left;
transform: translateY(-50%);
}

.ant-select-selector {
padding-left: var(--label-length) !important;
}

.ant-select-selection-search {
left: var(--label-length) !important;
}

.ant-select-multiple .ant-select-selection-search {
top: -2px;
left: 0 !important;
margin-left: 0 !important;
}

.ant-select-multiple .ant-select-selection-placeholder {
left: var(--label-length) !important;
height: 28px;
line-height: 28px;
}
}
}

最后就变成这样了,完美解决,这下ui总不能挑刺儿了吧.


image.png

收起阅读 »

记两次优化导致的Bug

web
人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。 废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到...
继续阅读 »

人云,过早的优化不如不优化。个人的理解,还是要具体情况具体分析。一般这里认为的是,开发过程的变动会导致之前做出的优化失灵。 如果没有,那说明你赌对了,不,说明你眼光真好。


废话到此结束。 本文记录了两次巧合。优化本身一般不会导致Bug,但是可能会有其它没预料到的问题。Bug本天成,菜鸡偶遇之。


和requestFrameAnimation有关。


和requestFrameAnimation就是在下一帧的时候调用, 调用这个方法后会返回一个id,凭借此id可以取消下一帧的调用cancelAnimationFrame(id)


在图形渲染中,经常会使用连续渲染的方式来不断更新视图,使用此api可以保证每一帧只绘制一次,一帧绘制一次就够了,两次没有意义。 当然,这里说的一次,是指一整个绘制,包含了后处理(比如three的postprocess)的一系列操作。


大致情况是这样的, 现在已经封装好了一个3d渲染器,然后在更新场景数据的,会有比较多的循环,这个时候,3d渲染就可以停掉了,这里就是做了这个优化。


来看主要代码,渲染器部分。只要调用animate方法就会循环渲染, pause停止,resume恢复渲染。 看上去好像没啥问题。


// 渲染 
animate = () => {
if (this.paused ) return ;
this.animateId = requestAnimationFrame(this.animate);
};
pause() {
this.paused = true;
}
resume() {
if (this.paused) {
this.paused = false;
this.animate()
}
}

再看,更新数据的部分。 问题就出在这里,更新数据这个操作,是没有任何异步的,也就是说在当前帧的宏任务里,先暂停后恢复, 结果就是,下一帧执行animate的时候, paused仍为true, 这个优化毫无意义。


view.current.pause() ;

//更新数据完成
view.current.resume()


无意义倒也没啥,但是 resume方法执行了之后,会新开一个requestAnimationFrame, 上一个requestAnimationFrame的循环又没有取消,


所以现在, 一帧里会执行两次render方法。 这个卡顿啊,就十分明显了,也就是当时的项目模型还不是特别大,只有在某些场景模型较大的时候才会卡,所以没急着处理。


过了几个月,临近更新了,不得不解决。排查的时候,是靠git记录二分才找出来的。


其次,要说明的是,使用了requestAnimationFrame连续渲染,这种在一帧里先暂停再继续的操作肯定是无意义的,因为下一帧才执行,只看执行前的结果。


当然,前面的暂停和继续的逻辑,也是一个隐患。 于是,就改成了我惯用的那种方式。 那就是暂停的时候,只是不渲染,循环继续空跑,如此而已。


  // 渲染
animate = () => {
!this.paused && this.renderer.render(this.scene, this.camera);
this.animateId = requestAnimationFrame(this.animate);
};

pause() {
this.paused = true;
}

resume() {
this.paused = false;
}

和 URL.create 有关


上面的那个代码,还可以说是写的人不熟悉requestAnimationFrame的特性造成的。 这一次的这个,真的是因为,引用关系比较多。


这次是一个纯2d项目,这里有一个将(后端)处理后的图片在canvas2d编辑器上显示出来的操作。这个图,叫产品图, 产品图是用户上传的, 也可以不经过后端处理,就直接进入到2d编辑器里。 后端处理之后,返回的是一个url。 所以有了下面的函数。


  function afterImgMatter(img:File|string) {
setShowMatter(false);
if (img instanceof File) {
tempImg.src = URL.createObjectURL(img);
} else {
tempImg.src = img
}

console.log(tempImg.src);
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
if (refCanvas.current) {
tempImg.onload = () => {

// 产品图层不可手动删除
productImg = refCanvas.current!.addImg(tempImg, false, null, 'product');
if (img instanceof File) {
productImg.id = globalData.productId;
} else {
productImg.id = globalData.matteredId;
}
setCanvasEditState(true);
!curScene && changeSene(scenes[0]);

}
}
}


如果是没经过后端处理的,那个图片就是文件转URL,就用到了这个方法URL.createObjectURL, 这个方法为二进制文件生成一个临时路径,mdn强调了一定要手动释放它。 浏览器在 document 卸载的时候,会自动释放它们,也就是说在这之前GC不会自动释放他们,即便找不到引用了。


那这个优化,我必然不能不做。 所以,我就判断了一下,如果之前的src是这个方法生成的,我就去释放它。 于是,在重新上传图片,也就是二次触发这个方法的时候出问题了, 画布直接白屏了,原来是报错了。


Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.


说我提供的图像源是破碎的,所以这个drawImage方法不能执行,其实上面还有一个报错,我之前一直没在意, 说得是一个url路径失效了,图片加载失败了,因为我释放了路径,所以我觉得出现这个,应该是正常的。


但是,现在结合drawImage的执行失败,这里还是有问题的。 我发现,确实就是因为我释放了要用做图像源的那个路径。 因为这里的productImg和 tempImg其实是通一个引用,只不过语义不同。


解决办法也很简单,那就是把释放的这一段代码,放到onload的回调里执行即可,图片加载完成之后,释放这个url也能正常工作


    tempImg.onload = () => {
if (productImg) {
let url = (productImg.image as HTMLImageElement)?.src
url.startsWith('blob') && URL.revokeObjectURL(url)
}
}

这里之所以会有 productImg和 tempImg通一个引用,语义不同, 也是因为我想优化一下。之前是每次加载图片的时候,都new Image,实际上这个可以用同一个对象,所以就有了tempImg


结束


本文记录了两个bug,顺带说了一下requestAnimationFrame URL.createObjectURL的部分用法。


没有直接阐述他们的用法,有兴趣了解的可以直接看文档。


requestAnimationFrame


URL.createObj

ectURL

收起阅读 »

如何实现比 setTimeout 快 80 倍的定时器?

web
很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这...
继续阅读 »

很多人都知道,setTimeout 是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard 规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:


let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。



更详细的原因,可以参考 为什么 setTimeout 有最小时延 4ms ?


探索


假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,上面那篇 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,你可以参考这里所说的 window.postMessage()



这篇文章里的作者给出了这样一段代码,用 postMessage 来实现真正 0 延迟的定时器:


(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于 postMessage 的回调函数的执行时机和 setTimeout 类似,都属于宏任务,所以可以简单利用 postMessageaddEventListener('message') 的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:



全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于 postMessage 的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。但空口无凭,咱们用数据说话。


作者设计了一个实验方法,就是分别用 postMessage 版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。读者也可以在这里自己跑一下测试


实验代码:


function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过 setZeroTimeout 也就是 postMessage 版本来递归计数到 100,然后切换成 setTimeout 计数到 100。


直接放结论,这个差距不固定,在我的 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在我硬件更好的台式机上,甚至能到 200 倍以上。



Performance 面板


只是看冷冰冰的数字还不够过瘾,我们打开 Performance 面板,看看更直观的可视化界面中,postMessage 版的定时器和 setTimeout 版的定时器是如何分布的。



这张分布图非常直观的体现出了我们上面所说的所有现象,左边的 postMessage 版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的 setTimeout 版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


借用 React Scheduler 为什么使用 MessageChannel 实现 这篇文章中的一段伪代码:


const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage 的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?参考我的这篇对 EventLoop 规范的解读 深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


通过本文,你大概可以了解如下几个知识点:



  1. setTimeout 的 4ms 延迟历史原因,具体表现。

  2. 如何通过 postMessage 实现一个真正 0 延迟的定时器。

  3. postMessage 定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。

作者:ssh_晨曦时梦见兮
来源:juejin.cn/post/7249633061440749628

收起阅读 »

项目提交按钮没防抖,差点影响了验收

web
前言 一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误...
继续阅读 »

前言


一个运行了多年的ToB的项目,由于数据量越来越大,业务越来越复杂,也一直在迭代,今年的阶段性交付那几天,公司 最大的客户 现场那边人员提出,某某某单据页面速度太慢了,点击会出现没反应的情况,然后就多点了几次,结果后面发现有的数据重复提交了,由于数据错误个别单据流程给弄不正常了,一些报表的数据统计也不对了,客户相关人员很不满意,马上该交付了,出这问题可还了得,项目款不按时给了,这责任谁都担不起🤣


QQ图片20230627163527.jpg


领导紧急组织相关技术人员开会分析原因


初步分析原因


发生这个情况前端选手应该会很清楚这是怎么回事,明显是项目里的按钮没加防抖导致的,按钮点击触发接口,接口响应慢,用户多点了几次,可能查询接口还没什么问题,如果业务复杂的地方,部分按钮的操作涉及到一些数据计算和后端多次交互更新数据的情况,就会出现错误。


看下项目情况


用到的框架和技术


项目使用 angular8 ts devextreme 组合。对!这就是之前文章提到的那个屎山项目(试用期改祖传屎山是一种怎么样的体验


项目规模


业务单据页面大约几百个,项目里面的按钮几千个,项目里面的按钮由于场景复杂,分别用了如下几种写法:



  • dx-button

  • div

  • dx-icon

  • input type=button

  • svg


由于面临交付,领导希望越快越好,最好一两天之内解决问题


还好我们领导没有说这问题当天就要解决 😁


解决方案


1. 添加防抖函数


按钮点击添加防抖函数,设置合理的时间


function debounce(func, wait) {
let timeout;
return function () {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(func, wait)
}
}

优点


封装一个公共函数,往每个按钮的点击事件里加就行了


缺点


这种情况有个问题就是在业务复杂的场景下,时间设置会比较棘手,如果时间设置短了,接口请求慢,用户多次点击还会出现问题,如果时间设置长了,体验变差了


2. 设置按钮禁用


设置按钮的 disabled 相关属性,按钮点击后设置禁用效果,业务代码执行结束后取消禁用


this.disabled = true
this.disabled = false

优点


原生按钮和使用的UI库的按钮设置简单


缺点


div, icon, svg 这种自定义的按钮的需要单独处理效果,比较麻烦


3. 请求拦截器中添加loading


在请求拦截器中根据请求类型显示 loading,请求结束后隐藏


优点


直接在一个地方设置就行了,不用去业务代码里一个个加


缺点


由于我们的技术栈使用的 angular8 内置的请求,无法实现类似 axios 拦截器那种效果,还有就是项目中的接口涉及多个部门的接口,不同部门的规范命名不一样,没有统一的标准,在实际的业务场景中,一个按钮的行为可能触发了多个请求,因此这个方案不适合当前的项目


4. 添加 loading 组件(项目中使用此方案)


新增一个 loading 组件,绑定到全局变量中,按钮点击触发显示 loading,业务执行结束后隐藏。


loading 组件核心代码


import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private isLoading$ = new BehaviorSubject<boolean>(false);
  private message$ = new BehaviorSubject<string>('正在加载中...');
  constructor() {}
  show(): void {
    this.isLoading$.next(true);
  }
  hide(): void {
    this.isLoading$.next(false);
  }
}

主要是 show()hide() 函数,将 loading 组件绑定到 app.components.ts 中,绑定组件到window 对象上,


window['loading'] = this.loadingService

在按钮点击时触发 show() 函数,业务代码执行结束后触发 hide() 函数


window['loading'].show();
window['loading'].hide();

优点


这种方式很好的解决了问题,由于 loading 有遮罩层还避免了用户点击某提交按钮后,接口响应慢,这时候去点击了别的操作按钮的情况。


缺点


需要在业务单据的按钮提交的地方一个个加


问题来了,一两天解决所有问题了吗?


QQ图片20230627165837.png


这么大的项目一两天不管哪种方案,把所有按钮都处理好是不现实的,经过分析讨论,最终选择了折中处理,先把客户提出来的几个业务单据页面,以及相关的业务单据页面添加上提交 loading 处理,然后再一边改 bug 一边完善剩余的地方,优先保证客户正常使用



还有更好的解决思路吗?欢迎JYM讨论交流


作者:草帽lufei
来源:juejin.cn/post/7249288087820861499

收起阅读 »

面试官问:如何实现 H5 秒开?

web
我在简历上写了精通 H5,结果面试官上来就问: 同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开? 由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点: 网络优化:http2、dns 预解析、使用 CDN 图片优化:压缩、懒加...
继续阅读 »

我在简历上写了精通 H5,结果面试官上来就问:



同学,你说你精通 H5 ,那你能不能说一下怎么实现 H5 秒开?



image.png


由于没怎么做过性能优化,我只能凭着印象,断断续续地罗列了几点:




  • 网络优化:http2、dns 预解析、使用 CDN

  • 图片优化:压缩、懒加载、雪碧图

  • 体积优化:分包、tree shaking、压缩、模块外置

  • 加载优化:延迟加载、骨架屏

  • ...



看得出来面试官不太满意,最后面试也挂了。于是我请教了我的好友 Gahing ,问问他的观点。



Gahing:


你列的这些优化手段本身没啥问题,如果是一个工作一两年的我会觉得还可以。但你已经五年以上工作经验了,需要有一些系统性思考了。



好像有点 PUA 的味道,于是我追问道:什么是系统性的思考?



Gahing:


我们先说回答方式,你有没有发现,你回答时容易遗漏和重复。


比如说「图片懒加载」,你归到了「图片优化」,但其实也可以归到「加载优化」。同时你还漏了很多重要的优化手段,比如资源缓存、服务端渲染等等。


究其原因应该是缺少抽象分类方法。



那针对这个问题,应该如何分类回答?



Gahing:


分类并非唯一,可以有不同角度,但都需遵从 MECE 原则(相互独立、完全穷尽) ,即做到不重不漏




  • 按页面加载链路分类:容器启动、资源加载、代码执行、数据获取、绘制渲染。




  • 按资源性能分类:CPU、内存、本地 I/O、网络。该分类方法又被叫做 USE 方法(Utilization Saturation and Errors Method)




  • 按协作方分类:前端、客户端、数据后台、图片服务、浏览器引擎等。




  • 按流程优化分类前置、简化、拆分



    • 前置即调整流程,效果上可能是高优模块前置或并行,低优模块后置;

    • 简化即缩减或取消流程,体积优化是简化,执行加速也是简化;

    • 拆分即细粒度拆解流程,本身没有优化效果,是为了更好的进行前置和简化。

    • 这个角度抽象层次较高,通常能回答出来的都是高手。




  • 多级分类:使用多个层级的分类方法。比如先按页面加载链路分类,再将链路中的每一项用协作方或者流程优化等角度再次分类。突出的是一个系统性思维。




选择好分类角度,也便于梳理优化方案的目标。



现在,尝试使用「页面加载链路+流程优化+协作方」的多级分类思维,对常见的首屏性能优化手段进行分类。


image.png


PS: 可以打开飞书文档原文查看思维导图


好像有点东西,但是我并没有做过性能优化,面试官会觉得我在背八股么?



Gahing:


可以没有实操经验,但是得深入理解。随便追问一下,比如「页面预渲染效果如何?有什么弊端?什么情况下适用?」,如果纯背不加理解的话很容易露馅。


另外,就我个人认为,候选人拥有抽象思维比实操经验更重要,更何况有些人的实操仅仅是知道怎么做,而不知道为什么做。



那我按上面的方式回答了,能顺利通过面试么 🌝 ?



Gahing:


如果能按上面的抽象思维回答,并顶住追问,在以前应该是能顺利通过面试的(就这个问题)。


但如今行业寒冬,大厂降本增效,对候选人提出了更高的要求,即系统性思考业务理解能力


从这个问题出发,如果想高分通过,不仅需要了解优化方案,还要关注研发流程、数据指标、项目协作等等,有沉淀自己的方法论和指导性原则,能实施可执行的 SOP。。




最后,我还是忍不住问了 Gahing :如果是你来回答这个问题,你会怎么回答?



Gahing:


H5 秒开是一个系统性问题,可以从深度和广度两个方向来回答。


深度关注的是技术解决方案,可以从页面加载链路进行方案拆解,得到容器启动、资源加载、代码执行、数据获取、绘制渲染各个环节。其中每个环节还可以从协作方和流程优化的角度进一步拆解。


广度关注的是整个需求流程,可以用 5W2H 进行拆解,包括:



  • 优化目标(What):了解优化目标,即前端首屏加载速度

  • 需求价值(Why):关注需求收益,从技术指标(FMP、TTI)和业务指标(跳失率、DAU、LT)进行分析

  • 研发周期(When):从开发前到上线后,各个环节都需要介入

  • 项目协作(Who):确定优化专项的主导方和协作方

  • 优化范围(Where):关注核心业务链路,确定性能卡点

  • 技术方案(How):制定具体的优化策略和行动计划

  • 成本评估(How much):评估优化方案的成本和效益。考虑时间、资源和预期收益,确保优化方案的可行性和可持续性。


通过 5W2H 分析法,可以建立系统性思维,全面了解如何实现 H5 秒开,并制定相应的行动计划来改进用户体验和页面性能。





限于篇幅,后面会单独整理两篇文章来聊聊关于前端首屏优化的系统性思考以及可实施的解决方案。


👋🏻 Respect!欢迎一键三连 ~


作者:francecil
来源:juejin.cn/post/7249665163242307640
收起阅读 »

websocket 实时通信实现

web
轮询和websocket对比 开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式: 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果 这种方式比较古老,但是兼容性...
继续阅读 »

轮询和websocket对比


开发过程中,有些场景,如弹幕、聊天、统计实时在线人数、实时获取服务端最新数据等,就需要实现”实时通讯“,一般有如下两种方式:




  1. 轮询:定义一个定时器,不停请求数据并更新,近似地实现“实时通信”的效果


    这种方式比较古老,但是兼容性强。


    缺点就是不断请求,耗费了大量的带宽和 CPU 资源,而且存在一定的延迟性




  2. websocket 长连接:全双工通信,客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,更加方便




websocket 实现


创建 websocket 连接


建立ws连接,有如下两种形式:


ws 代表明文,默认端口号为 80,例如ws://http://www.example.com:80, 类似http


wss 代表密文,默认端口号为 443,例如wss://http://www.example.com:443, 使用SSL/TLS加密,类似https


const useWebSocket = (params: wsItem) => {
// 定义传参 url地址 phone手机号
let { url = "", phone = "" } = params;
const ws = (useRef < WebSocket) | (null > null);
// ws数据
const [wsData, setMessage] = (useState < wsDataItem) | (null > null);
// ws状态
const [readyState, setReadyState] =
useState < any > { key: 0, value: "正在连接中" };
// 是否在当前页
const [isLocalPage, setIsLocalPage] = useState(true);

// 创建Websocket
const createWebSocket = () => {
try {
window.slWs = ws.current = new WebSocket(
`wss://${url}/ws/message/${phone}`
);
// todo 全局定义发送函数
window.slWs.sendMessage = sendMessage;
// todo 准备初始化
initWebSocket();
} catch (error) {
// 创建失败需要进行异常捕获
slLog.error("ws创建失败", error);
// todo 准备重连
reconnect();
}
};

return { isLocalPage, wsData, closeWebSocket, sendMessage };
};

初始化 websocket


当前的连接状态定义如下,使用常量数组控制:


const stateArr = [
{ key: 0, value: "正在连接中" },
{ key: 1, value: "已经连接并且可以通讯" },
{ key: 2, value: "连接正在关闭" },
{ key: 3, value: "连接已关闭或者没有连接成功" },
];

主要有四个事件,连接成功的回调函数(onopen)、连接关闭的回调函数(onclose)、连接失败的回调函数(onerror)、收到消息的回调函数(onmessage)


const initWebSocket = () => {
ws.current.onopen = (evt) => {
slLog.log("ws建立链接", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 心跳检查重置
keepHeartbeat();
};
ws.current.onclose = (evt) => {
slLog.log("ws链接已关闭", evt);
};
ws.current.onerror = (evt) => {
slLog.log("ws链接错误", evt);
setReadyState(stateArr[ws.current?.readyState ?? 0]);
// todo 重连
reconnect();
};
ws.current.onmessage = (evt) => {
slLog.log("ws接受消息", evt.data);
if (evt && evt.data) {
setMessage({ ...JSON.parse(evt.data) });
}
};
};

ws_1.png


websocket 心跳机制


在使用 ws 过程中,可能因为网络异常或者网络比较差,导致 ws 断开链接了,此时 onclose 事件未执行,无法知道 ws 连接情况。就需要有一个心跳机制,监控 ws 连接情况,断开后,可以进行重连操作。


目前的实现方案就是:前端每隔 5s 发送一次心跳消息,服务端连续 1 分钟没收到心跳消息,就可以进行后续异常处理了


const timeout = 5000; // 心跳时间间隔
let timer = null; // 心跳定时器

// 保持心跳
const keepHeartbeat = () => {
timer && clearInterval(timer);
timer = setInterval(() => {
if (ws.current?.readyState == 1) {
// 发送心跳 消息接口可以自己定义
sendMessage({
cmd: "SL602",
content: { type: "heartbeat", desc: "发送心跳维持" },
});
}
}, timeout);
};

如下图所示,为浏览器控制台中的截图,可以查看ws连接请求及消息详情。


注意:正常情况下,是需要对消息进行加密的,最好不要明文传输。


ws_2.png


websocket 重连处理


let lockFlag = false; // 避免重复连接
// 重连
const reconnect = () => {
try {
if (lockFlag) {
// 是否已经执行重连
return;
}
lockFlag = true;
// 没连接上会一直重连
// 设置延迟避免请求过多
lockTimer && clearTimeout(lockTimer);
var lockTimer = setTimeout(() => {
closeWebSocket();
ws.current = null;
createWebSocket();
lockFlag = false;
}, timer);
} catch (err) {
slLog.error("ws重连失败", err);
}
};

websocket 关闭事件


关闭事件需要暴露出去,给外界控制


// 关闭 WebSocket
const closeWebSocket = () => {
ws.current?.close();
};

websocket 发送数据


发送数据时,数据格式定义为对象形式,如{ cmd: '', content: '' }


// 发送数据
const sendMessage = (message) => {
if (ws.current?.readyState === 1) {
// 需要转一下处理
ws.current?.send(JSON.stringify(message));
}
};

页面可见性


监听页面切换到前台,还是后台,可以通过visibilitychange事件处理。


当页面长时间处于后台时,可以进行关闭或者异常的逻辑处理。


// 判断用户是否切换到后台
function visibleChange() {
// 页面变为不可见时触发
if (document.visibilityState === "hidden") {
setIsLocalPage(false);
}
// 页面变为可见时触发
if (document.visibilityState === "visible") {
setIsLocalPage(true);
}
}

useEffect(() => {
// 监听事件
document.addEventListener("visibilitychange", visibleChange);
return () => {
// 监听销毁事件
document.removeEventListener("visibilitychange", visibleChange);
};
}, []);

页面关闭


页面刷新或者是页面窗口关闭时,需要做一些销毁、清除的操作,可以通过如下事件执行:


beforeunload:当浏览器窗口关闭或者刷新时会触发该事件。当前页面不会直接关闭,可以点击确定按钮关闭或刷新,也可以取消关闭或刷新。


onunload:当文档或一个子资源正在被卸载时,触发该事件。beforeunload在其前面执行,如果点的浏览器取消按钮,不会执行到该处。


function beforeunload(ev) {
const e = ev || window.event;
// 阻止默认事件
e.preventDefault();
if (e) {
e.returnValue = "关闭提示";
}
return "关闭提示";
}
function onunload() {
// 执行关闭事件
ws.current?.close();
}

useEffect(() => {
// 初始化
window.addEventListener("beforeunload", beforeunload);
window.addEventListener("unload", onunload);
return () => {
// 销毁
window.removeEventListener("beforeunload", beforeunload);
window.removeEventListener("unload", onunload);
};
}, []);

执行 beforeunload 事件时,会有如下取消、确认弹框


ws_3.png


参考文档:



作者:时光足迹
来源:juejin.cn/post/7249204284180086842
收起阅读 »

IM 聊天组件

web
IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示 传入参数 自定义内容:标题(title)、内容(children)、底部(footer) 弹框组件显隐控制: 一般通过一个变量控制显示或隐藏(visible); 并且暴露出一个事件,控制该变量(...
继续阅读 »

IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示


im_3.png


传入参数


自定义内容:标题(title)、内容(children)、底部(footer)


弹框组件显隐控制:


一般通过一个变量控制显示或隐藏(visible);


并且暴露出一个事件,控制该变量(setVisible)


interface iProps {
title?: string // 标题
maskClose?: boolean // 点击 x 或 mask 回调
visible?: boolean // 是否显示
setVisible: (args) => void // 设置是否显示
children?: React.ReactNode | Array<React.ReactNode> // 自定义内容
footer?: React.ReactNode | Array<React.ReactNode> // 自定义底部
}

基础结构


IM 聊天组件基础结构包含:头部、内容区、尾部


function wsDialog(prop: iProps) {
const wsContentRef = useRef(null); // 消息区
const { title = "消息", maskClose, visible, setVisible } = prop; // 传入参数
const [message, setMessage] = useState(""); // 当前消息
const imMessage = useSelector(
(state: rootState) => state.mediaReducer.imMessage
); // 消息列表 全局管理

return (
<Modal
className={styles.ws_modal}
visible={visible}
transparent
onClose={handleMaskClose}
popup
animationType="slide-up"
>

<div className={styles.ws_modal_widget}>
{/* 头部 */}
<div className={styles.ws_header}></div>
{/* 内容区 */}
<div ref={wsContentRef} className={styles.ws_content}></div>
{/* 尾部区域 */}
<div className={styles.ws_footer}></div>
</div>
</Modal>

);
}

头部区


头部区域主要展示标题和关闭图标


标题内容可以自定义


不仅可以点击“右上角关闭图标”进行关闭


也可以通过点击“遮罩”进行关闭


// 头部关闭事件
function handleClose() {
slLog.log("[wsDialog]点击了关闭按钮");
setVisible(false);
}

// 弹框遮罩关闭事件
function handleMaskClose() {
if (maskClose) {
slLog.log("[wsDialog]点击了遮罩关闭");
setVisible(false);
}
}

// 头部区域
<div className={styles.ws_header}>
<div>{title}</div>
<div className={styles.ws_header_close} onClick={handleClose}>
<Icon type="cross" color="#999" size="lg" />
</div>

</div>;

内容区


消息内容分类展示:



  1. 文本:直接展示内容

  2. 图片:通过 a 标签包裹展示,可以在新标签页中打开,通过target="_blank"控制

  3. 文件:不同类型文件展示不同的图标,包括 zip、rar、doc、docx、xls、xlsx、pdf、txt 等;文件还可以进行下载


<div ref={wsContentRef} className={styles.ws_content}>
{imMessage &&
imMessage.length &&
imMessage.map((o, index) => {
return (
<div
key={index}
className={`${styles.item} ${
o.category === "send" ? styles.self_item : ""
}`}
>

<div className={styles.title}>{o.showName + " " + o.showNum}</div>
{/* 消息为图片 */}
{o.desc === "img" ? (
<a
className={`${styles.desc} ${styles.desc_image}`}
href={o.fileUrl}
title={o.fileName}
target="_blank"
>

<img src={o.fileUrl} />
</a>
) : o.desc === "file" ? (
// 消息为文件
<div className={`${styles.desc} ${styles.desc_file}`}>
<img
className={styles.file_icon}
src={handleSuffix(o.fileSuffix)}
/>

<div className={styles.file_content}>
<a title={o.fileName}>{o.fileName}</a>
<div>{o.fileSize}</div>
</div>
<img
className={styles.down_icon}
src={downIcon}
onClick={() =>
handleDownload(o)}
/>
</div>
) : (
// 消息为文本
<div className={`${styles.desc} ${styles.desc_message}`}>
{o.message}
</div>
)}
</div>

);
})}
</div>

文件下载通过 a 标签模拟实现


// 下载文件
function handleDownload(o) {
slLog.log("[SLIM]下载消息文件", o.fileUrl);
const a = document.createElement("a");
a.href = o.fileUrl;
a.download = o.fileName;
document.body.appendChild(a);
a.target = "_blank";
a.click();
a.remove();
}

监听消息内容,自动滚动到最底部处理


useEffect(() => {
if (visible && imMessage && imMessage.length) {
// 滚动到底部
wsContentRef.current.scrollTop = wsContentRef.current.scrollHeight;
}
}, [visible, imMessage]);

尾部区


主要是操作区,用于展示和发送文本、图片、文件等消息。


图片和文件通过原生input实现,通过accept属性控制文件类型


<div className={styles.ws_footer}>
<div className={styles.tools_panel}>
{/* 上传图片 */}
<div className={styles.tool}>
<img src={imageIcon} />
<input type="file" accept="image/*" onChange={handleChange("img")} />
</div>
{/* 上传文件 */}
<div className={styles.tool}>
<img src={fileIcon} />
<input
type="file"
accept=".doc,.docx,.pdf,.txt,.xls,.xlsx,.zip,.rar"
onChange={handleChange("file")}
/>

</div>
</div>

<div className={styles.input_panel}>
{/* 输入框,上传文本 */}
<input
placeholder="输入文本"
value={message}
onChange={handleInputChange}
className={`${styles.message} ${styles.mMessage}`}
onKeyUp={handleKeyUp}
/>

{/* 消息发送按钮 */}
<div onClick={handleMessage} className={styles.btn}>
发送
</div>
</div>

</div>

获取图片、文件信息:


// 消息处理
function handleChange(type) {
return (ev) => {
switch (type) {
case "img":
case "file":
msgObj.type = type === "img" ? 4 : 7;
const e = window.event || ev;
const files = e.target.files || e.dataTransfer.files;
const file = files[0];
msgObj.content = file;
break;
}
};
}

实现回车键发送消息:


通过输入框,发送文本消息时,一般需要监听回车事件(onKeyUp 事件中的 event.keyCode 为 13),也能发送消息


// 回车事件
function handleKeyUp(event) {
const value = event.target.value;
if (event.keyCode === 13) {
slLog.log("[wsDialog]onKeyUp", value, event.keyCode);
handleInputChange(event);
handleMessage();
}
}

组件封装


组件级别:公司级、系统级、业务级


组件封装优势:



  1. 提升开发效率,组件化、统一化管理

  2. 考虑发布成 npm 形式,远程发布通用


组件封装考虑点:



  1. 组件的分层和分治

  2. 设置扩展性(合理预留插槽)

  3. 兼容性考虑(向下兼容)

  4. 使用对象考虑

  5. 适用范围考虑


组件封装步骤:



  1. 建立组件的模板:基础架子,UI 样式,基本逻辑

  2. 定义数据输入:分析逻辑,定义 props 里面的数据、类型

  3. 定义数据输出:根据组件逻辑,定义要暴露出来的方法,$emit 实现等

  4. 完成组件内部的逻辑,考虑扩展性和维护性

  5. 编写详细的说明文档


作者:时光足迹
来源:juejin.cn/post/7249286405025022009
收起阅读 »

关于正则表达式,小黄人有话要说!!!

web
引言(关于正则表达式,小黄人有话要说!!!) 掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率! 本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是...
继续阅读 »

38dbb6fd5266d016a9ef9caf912bd40734fa3546.jpeg


引言(关于正则表达式,小黄人有话要说!!!)


掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率!


本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都能帮助你更好地理解和应用正则表达式。


如果您认为这篇文章对您有帮助或有价值,请不吝点个赞支持一下。如果您有任何疑问、建议或意见,欢迎在评论区留言。


image.png


如果你想快速入门 JavaScript 正则表达式,不妨点击这里阅读文章 "点燃你的前端技能!五分钟掌握JavaScript正则表达式"


字面量和构造函数


在 JavaScript 中,我们可以使用正则表达式字面量构造函数来创建正则表达式对象。


// 使用字面量
let regexLiteral = /pattern/;
// 使用构造函数
let regexConstructor = new RegExp('pattern');

正则表达式的方法


在 JavaScript 中,你可以使用正则表达式的方法进行模式匹配和替换。以下是一些常用的方法:




  • test():测试一个字符串是否匹配正则表达式。


    const regex = /pattern/;
    regex.test('string'); // 返回 true 或 false



  • exec():在字符串中执行正则表达式匹配,返回匹配结果的数组。


    const regex = /pattern/;
    regex.exec('string'); // 返回匹配结果的数组或 null



  • match():在字符串中查找匹配正则表达式的结果,并返回匹配结果的数组。


    const regex = /pattern/;
    'string'.match(regex); // 返回匹配结果的数组或 null



  • search():在字符串中搜索匹配正则表达式的结果,并返回匹配的起始位置。


    const regex = /pattern/;
    'string'.search(regex); // 返回匹配的起始位置或 -1



  • replace():在字符串中替换匹配正则表达式的内容。


    const regex = /pattern/;
    'string'.replace(regex, 'replacement'); // 返回替换后的新字符串



  • split():将字符串根据匹配正则表达式的位置分割成数组。


    const regex = /pattern/;
    'string'.split(regex); // 返回分割后的数组



u=1690536536,1627515251&fm=253&fmt=auto&app=138&f=JPEG.webp


基本元字符


正则表达式由字母、数字和特殊字符组成。其中,特殊字符被称为元字符,具有特殊的意义和功能。以下是一些常见的基本元字符及其作用:


元字符及其作用




  • 字符类 []



    • [abc]:匹配任意一个字符 a、b 或 c。

    • [^abc]:匹配除了 a、b 或 c 之外的任意字符。

    • [0-9]:匹配任意一个数字。

    • [a-zA-Z]:匹配任意一个字母(大小写不限)。




  • 转义字符 \



    • \d:匹配任意一个数字字符。

    • \w:匹配任意一个字母、数字或下划线字符。

    • \s:匹配任意一个空白字符。




  • 量词 {}



    • {n}:匹配前一个元素恰好出现 n 次。

    • {n,}:匹配前一个元素至少出现 n 次。

    • {n,m}:匹配前一个元素出现 n 到 m 次。




  • 边界字符 ^



    • ^pattern:匹配以 pattern 开头的字符串。

    • pattern$:匹配以 pattern 结尾的字符串。

    • \b:匹配一个单词边界。




  • 其他元字符



    • .:匹配任意一个字符,除了换行符。

    • |:用于模式的分组和逻辑 OR。

    • ():捕获分组,用于提取匹配的子字符串。

    • ?::非捕获分组,用于匹配但不捕获子字符串。




实例演示


现在,让我们通过一些实例来演示正则表达式中元字符的实际作用:


u=3528014621,1838675307&fm=253&fmt=auto&app=138&f=JPEG.webp



  • 字符类 []


let regex = /[abc]/;
console.log(regex.test("apple")); // true
console.log(regex.test("banana")); // false


  • 转义字符 \


let regex = /\d{3}-\d{4}/;
console.log(regex.test("123-4567")); // true
console.log(regex.test("abc-1234")); // false


  • 量词 {}


let regex = /\d{2,4}/;
console.log(regex.test("123")); // true
console.log(regex.test("12345")); // false
console.log(regex.test("12")); // true


  • 边界字符 ^


// 以什么开头
let regex = /^hello/;
console.log(regex.test("hello world")); // true
console.log(regex.test("world hello")); // false

// 单词边界
const pattern = /\bcat\b/;
console.log(pattern.test("The cat is black.")); // 输出:true
console.log(pattern.test("A cat is running.")); // 输出:true
console.log(pattern.test("The caterpillar is cute.")); // 输出:false


  • 其他元字符


// 捕获分组与模式分组
let regex = /(red|blue) car/;
console.log(regex.test("I have a red car.")); // true
console.log(regex.test("I have a blue car.")); // true
console.log(regex.test("I have a green car.")); // false

// 点号元字符
const pattern = /a.b/;
console.log(pattern.test("acb")); // 输出:true
console.log(pattern.test("a1b")); // 输出:true
console.log(pattern.test("a@b")); // 输出:true
console.log(pattern.test("ab")); // 输出:false

修饰符的使用


修饰符用于改变正则表达式的匹配行为,常见的修饰符包括 g(全局)、i(不区分大小写)和 m(多行)。


// 使用 `g` 修饰符全局匹配
const regex = /a/g;
const str = "abracadabra";
console.log(str.match(regex)); // 输出:['a', 'a', 'a', 'a']

// 使用 `i` 修饰符进行不区分大小写匹配
const pattern = /abc/i;
console.log(pattern.test("AbcDef")); // 输出:true
console.log(pattern.test("XYZ")); // 输出:false

十个高度实用的正则表达式示例


u=4075901265,1581553886&fm=253&fmt=auto&app=120&f=JPEG.webp



  1. 验证电子邮件地址:


const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
console.log(emailPattern.test("example@example.com")); // 输出:true
console.log(emailPattern.test("invalid.email@com")); // 输出:false


  1. 验证手机号码:


const phonePattern = /^\d{11}$/;
console.log(phonePattern.test("12345678901")); // 输出:true
console.log(phonePattern.test("98765432")); // 输出:false


  1. 提取 URL 中的域名:


const url = "https://www.example.com";
const domainPattern = /^https?://([^/?#]+)(?:[/?#]|$)/i;
const domain = url.match(domainPattern)[1];
console.log(domain); // 输出:"www.example.com"


  1. 验证日期格式(YYYY-MM-DD):


const datePattern = /^\d{4}-\d{2}-\d{2}$/;
console.log(datePattern.test("2023-05-12")); // 输出:true
console.log(datePattern.test("12/05/2023")); // 输出:false


  1. 验证密码强度(至少包含一个大写字母、一个小写字母和一个数字):


const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
console.log(passwordPattern.test("Password123")); // 输出:true
console.log(passwordPattern.test("weakpassword")); // 输出:false


  1. 提取文本中的所有链接:


const text = "Visit my website at https://www.example.com. For more information, check out http://www.example.com/docs.";
const linkPattern = /https?://\S+/g;
const links = text.match(linkPattern);
console.log(links); // 输出:["[https://www.example.com](https://www.example.com/)", "<http://www.example.com/docs>"]


  1. 替换字符串中的所有数字为特定字符:


const text = "I have 3 apples and 5 oranges.";
const digitPattern = /\d/g;
const modifiedText = text.replace(digitPattern, "*");
console.log(modifiedText); // 输出:"I have * apples and * oranges."


  1. 匹配 HTML 标签中的内容:


const html = "<p>Hello, <strong>world</strong>!</p>";
const tagPattern = /<[^>]+>/g;
const content = html.replace(tagPattern, "");
console.log(content); // 输出:"Hello, world!"


  1. 检查字符串是否以特定后缀结尾:


const filename = "example.txt";
const suffixPattern = /.txt$/;
console.log(suffixPattern.test(filename)); // 输出:true


  1. 验证邮政编码(5 位或 5+4 位数字):


const zipCodePattern = /^\d{5}(?:-\d{4})?$/;
console.log(zipCodePattern.test("12345")); // 输出:true
console.log(zipCodePattern.test("98765-4321")); // 输出:true
console.log(zipCodePattern.test("1234")); // 输出:false

u=3763318279,485967013&fm=253&fmt=auto&app=138&f=JPEG.webp


通过正则表达式的核心概念和用法,结合实例和讲解。在实际开发中,不难发现正则表达式是一个强大的工具,可用于字符串处理、模式匹配和验证输入等方面。掌握正则表达式的技巧,可以大大提升 JavaScript 编程的效率和灵活性。


结语


感谢您的阅读!希望本文带给您有价值的信息。


如果对您有帮助,请「点赞」支持,并「关注」我的主页获取更多后续相关文章。同时,也欢迎「收藏」本文,方便以后查阅。


写作不易,我会继续努力,提供有意义的内容。感谢您的支持和关注!


290be963d171f8b42f347d7e97b62252.jpg.source.jpg


作者:Sailing
来源:juejin.cn/post/7249231231967232037
收起阅读 »

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的而且消息已读后需要多次刷新会话列表才会清空未读消息

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的
而且消息已读后需要多次刷新会话列表才会清空未读消息

你不常用的 FileReader 能干什么?

web
前言 欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群! 本文灵感源于上周小伙伴遇到一个问题: "一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!" 这会有什么问题呢? 按原逻辑就是调用该接口后,就会一股脑...
继续阅读 »

前言



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



本文灵感源于上周小伙伴遇到一个问题:


"一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!"


1C306E8E.jpg


这会有什么问题呢?


按原逻辑就是调用该接口后,就会一股脑把该接口接返回过来的内容,直接经过 Blob 对象 转换后再通过隐藏的 <a> 标签实现下载。


但是有一个问题,那就是接口也是需要进行各种逻辑处理、判断等等,然后再决定是给前端响应一个正常的 Blob 格式的文件流,还是返回相应 JSon 格式的异常信息 等等。


如果返回了 JSon 格式的异常信息,那前端应该给用户展示信息内容,而不是将其作为下载的内容!


1C3802FC.gif


FileReader 实现 Blob 从 String 到 JSON


复现问题


为了更直观看到对应的效果,我们这里来简单模拟一下前后端的交互过程吧!


前端


由于小伙伴发送请求时使用的是 Axios,并且设置了其对应的 responsetype:blob | arraybuffer,所以这里我们也使用 Axios 即可,具体如下:


    // 发起请求
const request = () => {
axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then((res) => {

// 转换为 bloc 对象
const blob = new Blob([res.data])

// 获取导出文件名,decodeURIComponent为中文解码方法
const fileName = decodeURIComponent(res.headers["content-disposition"].split("filename=")[1])

// 通过a标签进行下载
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = fileName;
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
});
}

后端


这里我们就简单通过 koa 来实现将一个表格文件响应给前端,具体如下:


    const xlsx = require("node-xlsx");

const Koa = require("koa");
const app = new Koa();

const cors = require("koa2-cors");

// 处理跨域
app.use(
cors({
origin: "*", // 允许来自指定域名请求
maxAge: 5, // 本次预检请求的有效期,单位为秒
methods: ["GET", "POST"], // 所允许的 HTTP 请求方法
credentials: true, // 是否允许发送 Cookie
})
);

// 响应
app.use(async (ctx) => {
// 文件名字
const filename = "人员信息";

// 数据
const data = [
{ name: "赵", age: 16 },
{ name: "钱", age: 20 },
{ name: "孙", age: 17 },
{ name: "李", age: 19 },
{ name: "吴", age: 18 },
];

// 表格样式
const oprions = {
"!cols": [{ wch: 24 }, { wch: 20 }, { wch: 100 }, { wch: 20 }, { wch: 10 }],
};

// JSON -> Buffer
const buffer = JSONToBuffer(data, oprions);

// 设置 content-type
ctx.set("Content-Type", "application/vnd.openxmlformats");

// 设置文件名,中文必须用 encodeURIComponent 包裹,否则会报异常
ctx.set(
"Content-Disposition",
"attachment; filename=" + encodeURIComponent(filename) + ".xlsx"
);

// 文件必须设置该请求头,否则前端拿不到 Content-Disposition 响应头信息
ctx.set("Access-Control-Expose-Headers", "Content-Disposition");

// 将 buffer 返回给前端
ctx.body = buffer;
});

// 将数据转成 Buffer
const JSONToBuffer = (data, options = {}) => {
let xlsxObj = [
{
name: "sheet",
data: [],
},
];

data.forEach((item, idx) => {
// 处理 excel 表头
if (idx === 0) {
xlsxObj[0].data.push(Object.keys(item));
}

// 处理其他 excel 数据
xlsxObj[0].data.push(Object.values(item));
});

// 返回 buffer 对象
return xlsx.build(xlsxObj, options);
};

// 启动服务
app.listen(3000);

正常效果展示


1.gif


异常效果展示


可以看到当返回的内容为 JSON 格式 的内容时,原本逻辑在获取 filename 处就发生异常了,即使这一块没有发生异常,被正常下载下来也是不对的,因为这种情况应该要进行提示。


1.gif


并且此时直接去访问 res.data 得到的也不是一个 JSON 格式 的内容,而是一个 ArrayBuffer


image.png


返回的明明是 JSON ,但是拿到的却是 ArrayBuffer?


responseType 惹的祸


还记得我们在通过 Axios 去发起请求时设置的 responseType:'arraybuffer' 吗?


没错,就是因为这个配置的问题,它会把得到的结果给转成设置的类型,所以看起是一个 JSON 数据,但实际上拿到的是 Arraybuffer



这个 responseType 实际上就是 XMLHttpRequest.responseType,可点击该链接自行查看。



不设置 responseType 行不行?


那么既然是这个配置的问题,那么我们不设置不就好了!


确实可行,如下是未设置 responseType 获取到的结果:


image.png


但也不行,如果不设置 responseType 或者设置的类型不对,那么在 正常情况 下(即 文件被下载)时 会导致文件格式被损坏,无法正常打开,如下:


image.png


FileReader 来救场


实际上还有个比较直接的解决方案,那就是把接收到的 Arraybuffer 转成 JSON 格式不就行了吗?


1CB04D6B.jpg


没错,我们只需要通过 FileReader 来完成这一步即可,请看如下示例:


// json -> blob
const obj = { hello: "world" };

const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: "application/json",
});

console.log(blob) // Blob {size: 22, type: 'application/json'}

// blob -> json
const reader = new FileReader()

reader.onload = () => {
console.log(JSON.parse(reader.result)) // { hello: "world" }
}

reader.readAsText(blob, 'utf-8')

是不是很简单啊!


值得注意的是,并不是任何时候都需要转成 JSON 数据,就像并不是任何时候都要下载一样,我们需要判断什么时候该走下载逻辑,什么时候该走转换成 JSON 数据。


怎么判断当前是该下载?还是该转成 JSON?


这个还是比较简单的,换个说法就是判断当前返回的是不是文件流,下面列举较常见的两种方式。


根据 filename 判断


正常情况来讲,再返回文件流的同时会在 Content-Disposition 响应头中添加和 filename 相关的信息,换句话说,如果当前没有返回 filename 相关的内容,那么就可以将其当做异常情况,此时就应该走转 JSON 的逻辑。


不过需要注意,有时候后端返回的某些文件流并不会设置 filename 的值,此时虽然符合异常情况,但是实际上返回的是一个正常的文件流,因此不太推荐这种方式


208EA3E8.gif


根据 Content-Type 判断


这种方式更合理,毕竟后端无论是返回 文件流 或是 JSON 格式的内容,其响应头中对应的 Content-Type,必然不同,这里的判断更简单,我们直接判断其是不是 JSON 类型即可。


更改后的代码,如下:


axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then(({headers, data}) => {
console.log("FileReader 处理前:", data)

const IsJson = headers['content-type'].indexOf('application/json') > -1;

if(IsJson){
const reader = new FileReader()

// readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob
// 若后端直接返回的就是 blob 类型,则直接使用即可
reader.readAsText(new Blob([data], {type: 'application/json'}), 'utf-8')

reader.onload = () => {
// 将字符内容转为 JSON 格式
console.log("FileReader 处理后:", JSON.parse(reader.result))
}
return
}

// 下载逻辑
download(data)
});

值得注意的是,readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob,若后端直接返回的就是 blob 类型,则直接使用即可。


image.png


FileReader 还能干什么?


以上是使用 FileReader 解决一个实际问题的例子,那么除此之外它还有什么应用场景呢?


不过我们还是先来了解一下 FileReader 的一些相关内容吧!!!


FileReader 是什么?


FileReader 对象允许 Web 应用程序 异步读取 存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。


不过还要注意如下两条规则:



  • FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中按路径名简单地读取文件

  • 要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行 服务器端文件读取


总结起来就是,FileReader 只能读取 FileBlob 类型的文件内容,并且不能直接按路径的方式读取文件,如果需要以路径方式读取,最好要通过 服务端 返回流的形式。


四种读取方式


FileReader 可以如下四种方式读取目标文件:




  • FileReader.readAsArrayBuffer()



    • 开始读取指定的 Blob中的内容,读取完成后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象




  • FileReader.readAsBinaryString() (非标准



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含所读取文件的 原始二进制数据




  • FileReader.readAsDataURL()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容




  • FileReader.readAsText()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 字符串 以表示所读取的文件内容




如上对应的方法命名十分符合顾名思义的特点,因此可以很容易看出来在不同场景下应该选择什么方法,并且如上方法一般都会配合 FileReader.onload 事件FileReader.result 属性 一起使用。


FileReader 的其他应用场景


预览本地文件


通常情况下,前端选择了相应的本地文件(图片、音/视频 等)后,需要通过接口发送到服务端,接着服务端在返回一个相应的预览地址,前端在实现支持预览的操作。


如果说现在有一个需要省略掉中间过程的需求,那么你就可以通过 FileReader.readAsDataURL() 方法来实现,但是要考虑文件大小带来转换时间快慢的问题。


这一部分比较简单,就不贴代码占篇幅了,效果如下:


1.gif


传输二进制格式数据


通常在上传文件时,前端直接将接收到的 File 对象以 FormData 发送给后端,但如果后端需要的是二进制的数据内容怎么办?


此时我们就可以使用 FileReader.readAsArrayBuffer() 来配合,为啥不用 FileReader.readAsBinaryString(),因为它是非标准的,而且 ArrayBuffer 也是原始的 二进制数据


具体代码如下:


// 文件变化
const fileChange = (e: any) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsArrayBuffer(file)

reader.onload = () => {
upload(reader.result, 'http://xxx')
}
}

// 上传
const upload = (binary, url) => {
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.overrideMimeType("application/octet-stream");

//直接发送二进制数据
xhr.send(binary);

// 监听变化
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 响应成功
}
}
}
}

最后



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



上面我们通过 FileReader 解决了一个实际问题,同时也简单介绍了其相应的使用场景,但这个场景具体是否是用于你的需求还要具体分析,不能盲目使用。


以上就是本文的全部内容了,希望本文对你有所帮助!!!


21E0754A.jpg

收起阅读 »

数组去重你想到几种办法呢?

web
前言 你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对...
继续阅读 »

前言


你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对于数组去重这道简单的面试题时,我们可以回答的方法有什么吧。


数组去重


1. 不使用数组API方法


首先我来介绍一种不是用数组身上的API的去重解法,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for(let i = 0; i < array.length; i++){
for( var j = 0; j < res.length; j++){
if(array[i] === res[j]){
break;
}
}
if(j === res.length){
res.push(array[i])
}
}
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]


既然不使用数组自带的API方法,那我们首先考虑的就是用双重for循环了,如上述代码:



  1. 我们准备了一个空的结果数组

  2. 我们对需要去重的数组进行循环

  3. 在第一层数据中再套一层循环,根据下标判断结果数组内是否有重复项。


我们调用该方法,打印结构如上述代码的注解处,成功的实现了对数组的去重。


2. 使用 indexOf


既然有不使用数组API的,那就肯定有使用数组API的,下面看我使用indexOf完成数组的去重,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for (let i = 0; i < array.length; i++) {
if (res.indexOf(array[i]) === -1) { // 返回找到的第一个值得下标
res.push(array[i])
}
}
return res
}
console.log(unique(array))// [ '1', 1, '2', 2 ]


如上述代码, 我们巧妙了使用了indexOf查找结果数组中是否已经存在,如果不存在才向结果数组中添加,实现了数组去重。


在上述代码的基础上,我们还可以转变一下,将for循环内的语句改为


if (array.indexOf((array[i])) == array.lastIndexOf(array[i])) {
i++
} else {
array.splice(array.lastIndexOf(array[i]), 1)
}

不新增其他变量,直接通过indexOf和lastIndexOf判断该值是否在原数组内为唯一值,从而直接修改原数组,实现数组的去重。


3. 使用 sort


对于数组去重,我们除了通过下标找出是否有重复项之外,我们还可以先排序,然后在判断前后项是否相同来实现去重,代码如下:


var  array = [1, 3, 5, 4, 2, 1, 2, 4, 4, 4]
function unique(array) {
let res = []
let sortedArray = array.concat().sort() //concat() 返回新的数组
let seen;
for (let i = 0; i < sortedArray.length; i++) {
if (!i || seen !== sortedArray[i]) {
res.push(sortedArray[i])
}
seen = sortedArray[i]
}
return res
}
console.log(unique(array)); // [ 1, 2, 3, 4, 5 ]

如上述代码, 我们先获取一个排好序的新数组,再对新数组进行循环,判断保存前一个值的seen与当前值是否相同来实现数组去重。


温馨小提示: 由于数组的排序方法不能区分数组和字符串,所以想要使用此方法必须要保证数组的值的类型相同,不然会出bug


4. 使用 filter


既然都用到了sort排序了,那我直接抬出ES6数组新增的filter过滤器API也不过分吧,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = array.filter((item, index, array) => {
return array.indexOf(item) === index
})
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

如上述代码,filter直接使用array.indexOf(item) === index作为过滤条件返回出一个新的数组,实现数组去重。


如上述代码,我们结合了 indexOf方法作为过滤条件,那我们也可以结合一下sort方法吧,直接使用一行代码就解决了数组的去重。代码如下:


function unique(array) {
return array.concat().sort().filter((item, index, array) => !index || item !== array[item - 1])
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

5. 使用Set、Map、或者对象


除了上述的通过数组API和不使用数组API的方法外,我们还能想到的就是借助对象来实现数组的去重。使用Set数据结构是我们最容易想到的办法,使用Map与对象方法的相似,都是以数组的值作为key,再将所有的可以取出来组成一个数组。 我就不给小伙伴们演示代码了,感兴趣的小伙伴可以自己动手试试。


(对于对象的key只能为字符串这个问题,我们可以换个思路,将下标存为key,值存为value,判断不同key的值相不相同来实现数组去重。我们还可以在存key时加上其类型,然后进行一次转换。)


自己封装一个去重API


在介绍上述数组去重的方法后,我们再来总结一下,将其融合成一个有复用性,而且还可以适用不同情况的API方法。


我来介绍一下如下我封装的一个数组去重的API方法,



  1. 该方法可接受三个参数,第一个参数为需要去重的数组,第二个参数为该数组是否为排好序的数组,第三个参数为一个回调函数

  2. 该回调函数也有三个参数,分别为值,下标,需要去重数组。该回调函数的作用是方便用户对数组进行一些额外的处理(例如将大写转为小写)

  3. 第二,三参数可不传递。


var array = [1, 2, '1', 'a', 'A', 2, 1]
var array2 = [1, 1, '1', 2, 2]
function uniq(array, isSorted, iteratee) {
let seen = []
let res = []
for(let i = 0; i < array.length; i++){
let computed = iteratee ? iteratee(array[i], i,array) : array[i]
if(isSorted) {
if(!i || seen !== array[i]){
res.push(array[i])
}
seen = array[i]
}else if(iteratee) {
if(seen.indexOf(computed) === -1){
seen.push(computed)
res.push(computed)
}
}
else {
if(res.indexOf(array[i]) === -1) {
res.push(array[i])
}
}
}
return res
}
let result = uniq(array, false, function(item, index, arr){
return typeof item == 'string' ? item.toLowerCase() : item
})
console.log(result); // [ 1, 2, '1', 'a' ]
console.log(uniq(array2, true)); // [ 1, 2 ]

总结


对于数组的去重,当我们能在面试中说到这个多方法的话,这道面试题也就过了,虽然这道面试不难,但如果我们想要想到这个多方法的话,还是

作者:潘小七
来源:juejin.cn/post/7248835844659970105
需要许多知识储备的。

收起阅读 »

在高德地图实现卷帘效果

web
介绍 今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。 实现思路 1.创建目标图...
继续阅读 »

介绍


今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。


实现思路


1.创建目标图层,这里除了有一个默认的底图,还增加了卫星影像图和路网图层,后两者是可以被掩模的。因此在创建图层时通过设置rejectMapMask(默认值false)让图层是否允许被掩模。


2.提供实时设置掩模的方法renderMask,核心代码只需要map.setMask(mask)。


3.实现拖拽交互逻辑,监听拖拽过程,实时触发 renderMask


实现代码


1.创建目标图层


// 基础底图
const baseLayer = new AMap.TileLayer({
zIndex: 1,
//拒绝被掩模
rejectMapMask: true,
})

map = new AMap.Map('container', {
center:[116.472804,39.995725],
viewMode:'3D',
labelzIndex:130,
zoom: 5,
cursor:'pointer',
layers:[
// 底图,不掩模
baseLayer,
// 路网图层
new AMap.TileLayer.RoadNet({
zIndex:7
}),
// 卫星影像图层
new AMap.TileLayer.Satellite()
]
});

2.提供实时设置掩模的方法


function renderMask(){
// 当前地图范围
const {northEast, southWest} = map.getBounds()
// 地理横向跨度
const width = northEast.lng - southWest.lng
// 拖拽条位置占比例
const dom = document.querySelector('#dragBar')
const ratio = Math.ceil(parseInt(dom.style.left) + 5) / map.getSize().width

let mask = [[
[northEast.lng, northEast.lat],
[southWest.lng+ width * ratio, northEast.lat],
[southWest.lng+ width * ratio, southWest.lat],
[northEast.lng, southWest.lat]
]]

map.setMask(mask)
}

3.实现拖拽交互逻辑


// 拖拽交互
function initDrag(){

const dom = document.querySelector('#dragBar')
dom.style.left = `${map.getSize().width/2}px`

// const position = {x:0, y:0}
interact('#dragBar').draggable({
listeners: {
start (event) {
// console.log(event.type, event.target)
},
move (event) {
// 改变拖拽条位置
const left = parseFloat(dom.style.left)
const targetLeft = Math.min(Math.max(left + event.dx, 0), map.getSize().width - 10)
dom.style.left = `${targetLeft}px`

if(event.dx !== 0){
renderMask()
//必须!强制地图重新渲染
map.render()
}
},
end(event){
// console.log(event.type, event.target)
}
}
})
}


  1. 启动相关方法,完善交互逻辑


initDrag()
renderMask()
map.on('mapmove', renderMask)
map.on('zoomchange', renderMask)
window.addEventListener('resize', renderMask)

相关链接


本文代码演示


jsfiddle.net/gyratesky/z…


maptalks 图层卷帘效果


maptalks.org/examples/cn…


卫星+区域掩模


lbs.amap.com/demo/j

avasc…

收起阅读 »

正则别光想着抄,看懂用法下次你也会写

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一些高级的正则表达式用法。


校验字符串是否包含大小写字母+数字+特殊字符,并且长度为8-12。


如果想要使用单个正则表达式就解决上述问题,就需要稍微学习一下正则的一些高级用法了。


^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$

先行断言(预搜索)


先行断言中不会获取任何内容,只是做一次筛查



  • 正向先行 指在某个位置向右看,该位置必须能匹配该表达式(?=表达式)。

  • 反向先行 指在某个位置往右看,该位置保证不能出现的表达式。

  • 正向后行 指在某个位置向左看,该位置必须能匹配该表达式,但不会获取表达式的内容(?<=表达式)

  • 反向后行 指在某个位置往左看,该位置保证不能出现的表达式(?<!表达式)


这个正则表达式使用了正向先行断言来同时检查字符串中是否包含大小写字母、数字和特殊符号。它的含义如下:



  • ^:匹配字符串的开头。

  • (?=.*[a-z]):正向先行断言,要求字符串中至少包含一个小写字母。

  • (?=.*[A-Z]):正向先行断言,要求字符串中至少包含一个大写字母。

  • (?=.*\d):正向先行断言,要求字符串中至少包含一个数字。

  • (?=.*[!@#$%^&*()_+]):正向先行断言,要求字符串中至少包含一个特殊符号(这里列出了一些常见的特殊符号,你可以根据需要添加或修改)。

  • [a-zA-Z\d!@#$%^&*()_+]:匹配允许的字符集合,包括大小写字母、数字和特殊符号。

  • {8,12}:限定字符串的长度在 8 到 12 位之间。

  • $:匹配字符串的结尾。


使用这个正则表达式可以对目标字符串进行检查,判断是否满足包含大小写、数字和特殊符号,并且长度为 8 到 12 位的要求。例如:


let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$/;
let str = "Password123!";
let isMatch = regex.test(str);
console.log(isMatch); // 输出: true

获取ip地址


当处理日志文件时,有时需要从日志文本中提取特定的信息。一个常见的场景是提取日志中的 IP 地址。


假设我们有一个日志文件,其中包含了多行日志记录,每行记录的格式如下:


[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home

在上述示例中,我们使用 match 方法来执行正则表达式匹配,并将匹配的结果存储在 match 变量中。如果有匹配结果,我们可以从数组中取得第一个元素 match[0],即提取到的 IP 地址。


let logText = "[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home";
let regex = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
let match = logText.match(regex);
if (match) {
let ipAddress = match[0];
console.log(ipAddress); // 输出: 192.168.0.1
} else {
console.log("No IP address found.");
}

非捕获型分组


非捕获型分组是正则表达式中的一种分组语法,用于对一组子表达式进行逻辑组合,但不会捕获匹配的结果。它以 (?: 开始,并以 ) 结束。


/\b(?:\d{1,3}\.){3}\d{1,3}\b/

解释一下这个正则表达式:



  • \b:单词边界,用于确保 IP 地址被完整匹配。

  • (?:\d{1,3}\.){3}:非捕获型分组,匹配由 1 到 3 个数字和一个点组成的序列,重复匹配 3 次,用于匹配 IP 地址的前三个数字和点的部分。

  • \d{1,3}:匹配由 1 到 3 个数字组成的序列,用于匹配 IP 地址的最后一个数字。

  • \b:单词边界,用于确保 IP 地址被完整
    作者:simple_lau
    来源:juejin.cn/post/7248832185808617509
    匹配。

收起阅读 »

从张鑫旭大佬文章中发现了我前端知识的匮乏

web
最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。 我们先看下页面是怎...
继续阅读 »

最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。


我们先看下页面是怎么样的:


chrome-capture-2023-5-26.gif


功能很简单,就是复制下面的二维码图片,然后粘贴到文本框中,最后点击识别按钮,把识别二维码的结果展示到下面。


源代码:


<!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>qrcode</title>
<style>
.area {
height: 200px;
border: 1px dashed skyblue;
background-color: #fff;
display: grid;
place-items: center;
margin-top: 20px;
}
.area:focus {
border-style: solid;
}
.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}
.button {
margin: 1rem auto;
width: 160px;
height: 40px;
font-size: 112.5%;
background-color: #eb4646;
color: #fff;
border: 0;
border-radius: 0.25rem;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<div class="container">
<input id="file" class="file" type="file" accept="image/png" />
<div id="area" class="area" tabindex="-1"></div>
</div>
<p align="center">
<button id="button" class="button">识别</button>
</p>

<p id="result" align="center"></p>

<p align="center">
方便大家复制的示意图:<br /><img
src="./qrcode.png"
style="margin-top: 10px"
/>

</p>

<script>
var reader = new FileReader()
reader.onload = function (event) {
area.innerHTML = '<img src="' + event.target.result + '">'
}
document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

file.addEventListener('change', function (event) {
const file = event.target.files && event.target.files[0]
if (file) {
reader.readAsDataURL(file)
}
})

button.addEventListener('click', function () {
if ('BarcodeDetector' in window) {
// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})

const eleImg = document.querySelector('#area img')
if (eleImg) {
barcodeDetector
.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})
} else {
result.innerHTML = `<span class="error">请先粘贴二维码图片</span>`
}
} else {
result.innerHTML = `<span class="error">当前浏览器不支持二维码识别</span>`
}
})
</script>
</body>
</html>

背景交代完成,现在就一点一点的来分析其中代码的精妙之处。


CSS部分


tabindex = -1


<div id="area" class="area" tabindex="-1"></div>

当我看到tabindex这个属性时,完全不知道它的用法,于是我继续在张鑫旭大佬的博客中搜索,找到一篇叫《HTML tabindex属性与web网页键盘无障碍访问》的文章,这里简要说下这个属性的用法和作用。


tabindex属性是一个全局属性,也就是所有 HTML 标签都可以用的属性,比方说idclass属性等。所以,可以在div上使用。同时,这个属性是一个非常老的属性,没有兼容性问题,放心使用。


tabindex属性是一个与键盘访问行为息息相关的属性。平常可能感觉不到它的价值,但是一旦我们的鼠标坏掉了或者没电了,我们就只能使用键盘。亦或者在电视机上,或者投影设备上访问我们的网页的时候,我们只能使用遥控器。就算设备都完全正常,对于资深用户而言,键盘访问可以大大提高我们的使用效率。


当一个元素设置tabindex属性值为-1的时候,元素会变得focusable,所谓focusable指的是元素可以被鼠标或者JS focus,在 Chrome 浏览器下表现为会有outline发光效果,IE浏览器下是虚框,同时能够响应focus事件。默认的focusable元素有<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>


但是,tabindex = -1不能被键盘的tab键进行focus。这种鼠标可以focus,但是键盘却不能focus的状态,只要tabindex属性值为负值就可以了。


因此,我们可以设置divfocus的样式,当鼠标点击div时,我们可以改变它的边框,如下:


.area:focus {
border-style: solid;
}

tabindex属性值是一个整数,它来决定被tabfocus的顺序,顺序越小越先被focus,但是 0除外,如下divfocus的顺序依次是:1,2,3。


<div id="area" class="area" tabindex="1"></div>
<div class="area" tabindex="3"></div>
<div class="area" tabindex="2"></div>

tabindex="0"又是怎么回事呢?


元素设置tabindex="-1",可以鼠标和JS可以focus,但键盘不能focus


tabindex="0"tabindex="-1"的唯一区别就是键盘也能focus,但是被focus的顺序是最后的。或者你可以这么理解,<div>设置了tabindex="0",从键盘访问的角度来讲,相对于<div>元素变成了<button>元素。


垂直居中


垂直居中是一个常用的需求了,我经常使用flex来完成:


display: flex;
align-items: center;
justify-content: center;

在大佬的文章中使用了一个新的用法:


display: grid;
place-items: center;

place-items 属性是以下属性的简写:align-itemsjustify-items


:empty::before


div元素没有内容时,.area:empty样式会生效,同时为了显示一段提示内容,使用了伪元素::before,在content写入提示内容。


.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}

JS部分


copy paste 事件


document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

这两个事件都属于ClipboardEvent事件(剪切板事件),还有一个cut剪切事件。


wrap.oncopy = function(event){}
wrap.oncut = function(event){}
wrap.onpaste = function(event) {}

任何软件上的内容,可以被复制粘贴,是因为软件对操作系统复制粘贴操作的实现,软件都会把复制剪切的内容存入操作系统的剪切板上。同样,浏览器也对操作系统的剪切板进行了实现,属于浏览器的自身的实现。


浏览器复制操作的默认行为是触发浏览器的 copy 事件,将 copy 的内容存入操作系统的剪切板中。


那如何干预浏览器的这种默认的复制粘贴操作呢?


可以通过event.preventDefault阻止事件的默认行为,即当触发这三个事件时,阻止对系统剪切板的数据操作。然后,我们对数据进行加工后,重新写入到剪贴板。


比如,当用户复制我们网站的内容时,可以在数据后面加一个版权的相关信息。


<div id="wrap">这是复制的复制内容</div>
<script>
var wrap = document.getElementById('wrap')
wrap.oncopy = function (event) {
// 通过copy事件监听,阻止将选中内容复制到系统剪切板上
event.preventDefault()
// 获取选中内容对象
const selection = document.getSelection()
// selection对象重构了toSring()方法,获取selection对象的选中内容
var selectContent = selection.toString()
var dealContent =
selectContent +
'转载请联系作者,内容地址:xxxxx'
// 把重写后的内容写入到剪贴板
event.clipboardData.setData('text/plain', dealContent)
}
</script>


ClipboardEvent 事件有个最重要的属性clipboardData,该属性值是DataTransfer对象,这个对象在拖拽场景中经常使用,后面会专门写一篇文章来说说这个对象。


new BarcodeDetector解析二维码


// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})
barcodeDetector.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})

浏览器提供了原生的API来解析二维码和条形码,即 Barcode Detection API


formats表示要解析那种码,如下图所示:


image.png


总结


通过学习上面的代码,可以发现自己在 css,js 方面上的不足,原因是缺乏探索性,老是用已有的知识来解决问题,或者直接去 github 上找第三方库,其实可以使

作者:小p
来源:juejin.cn/post/7248874230862233655
用最简单的方式实现。

收起阅读 »

从 0 到 1 实现一个 Terminal 终端

web
前言 之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。 每一步骤后都有对应的 com...
继续阅读 »

前言



之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。



每一步骤后都有对应的 commit 记录;


源码地址:github.com/ljq0226/my-… 欢迎 Star ⭐️⭐️⭐️


体验地址: my-terminal.netlify.app/



搭建环境


我们使用 vite 构建项目,安装所需要的依赖库:



  • @neodrag/react (拖拽)

  • tailwindcss

  • lucide-react (图标)
    步骤:

  • pnpm create vite

  • 选择 React+TS 模版

  • 安装依赖:pnpm install @neodrag/react lucide-react && pnpm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p
    配置 tailwind.config.js:


/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}


仓库代码:commit1


开发流程


搭建页面


image.png


以上是终端的静态页面,样式这里就不在详细展开了,此次代码仓库 commit2 。 接下来我们为该终端添加拖拽效果:


//App.tsx
···
import type { DragOptions } from '@neodrag/react'
import { useRef, useState } from 'react'

function APP(){
const [position, setPosition] = useState({ x: 0, y: 0 })
const draggableRef = useRef(null)
// 初始化 dragable 拖拽设置
const options: DragOptions = {
position,
onDrag: ({ offsetX, offsetY }) => setPosition({ x: offsetX, y: offsetY }),
bounds: { bottom: -500, top: 32, left: -600, right: -600 },
handle: '.window-header',
cancel: '.traffic-lights',
}
useDraggable(draggableRef, options)

}

return (
<div ref={draggableRef}> //将 draggableRef 挂在到节点上

</div>

)
···

这样我们的 Terminal 终端就有了拖拽效果,其它 API 方法在@neodrag/react 官网中,代码仓库 commit3


terminal2.gif


输入命令


一个终端最重要的当然是输入命令了,在这我们使用 input 框来收集收集输入命令的内容。
由于我们每次执行完一次命令之后,都会生成新的行,所以我们将新行封装成一个组件,Row 组件接收两个参数(id:当前 Row 的唯一标识;onkeydown:监听 input 框的操作):


// components.tsx
interface RowProps {
id: number
onkeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void
}
const Row: React.FC<RowProps> = ({ id, onkeydown }) => {

return (
<div className='flex flex-col w-full h-12'>
<div>
<span className="mr-2 text-yellow-400">funnycoder</span>
<span className="mr-2 text-green-400">@macbook-pro</span>
<span className="mr-2 text-blue-400">~{dir}</span>
<span id={`terminal-currentDirectory-${id}`} className="mr-2 text-blue-400"></span>
</div>
<div className='flex'>
<span className="mr-2 text-pink-400">$</span>
<input
type="text"
id={`terminal-input-${id}`}
autoComplete="off"
autoFocus={true}
className="flex-1 px-1 text-white bg-transparent outline-none"
onKeyDown={onkeydown}
/>

</div>

</div>

)
}

一开始的时候,我们通过初始化一个 Row 进行操作,我们所有生成的 Row 通过


//app.tsx
const [content, setContent] = useState<JSX.Element[]>(
[<Row
id={0}
key={key()} // React 渲染列表时需要key
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, 0)}
/>,
])

content 变量来存储,在后续我们经常要修改 content 的值,为了简化代码我们为 setContent 封装成 generateRow 方法:


// 生成内容
const generateRow = (row: JSX.Element) => {
setContent(s => [...s, row])
}

问题来了,当我们获取到了输入的命令时,怎么执行对应的方法呢?


每一个 Row 组件都有 onKeyDown事件监听,当按下按键时就调用 executeCommand 方法,通过 input 框的 id 获取该 input 框 dom 节点, const [cmd, args] = input.value.trim().split(' ') 获取执行命令 cmd 和 参数 args,此时根据 event.key 按键操作执行对应的方法:


 // 执行方法
function executeCommand(event: React.KeyboardEvent<HTMLInputElement>, id: number) {
const input = document.querySelector(`#terminal-input-${id}`) as HTMLInputElement
const [cmd, args] = input.value.trim().split(' ')
if (event.key === 'ArrowUp')
alert(`ArrowUp,Command is ${cmd} Args is ${args}`)

else if (event.key === 'ArrowDown')
alert(`ArrowDown,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Tab')
alert(`Tab,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Enter')
alert(`Enter,Command is ${cmd} Args is ${args}`)
}

接下来我们测试一下,输入cd desktop,按下 Enter 键:
terminal3.gif


代码仓库 commit3


构建文件夹系统


终端的最常用的功能就是操作文件,所以我们需要构建一个文件夹系统,起初,在我的项目中使用的是一个数组嵌套,类似下面这种


image.png


这种数据结构的话,每次寻找子项的都需要递归计算,非常麻烦。在这我们采用 map 进行存储,将数据扁平化:


image.png


代码仓库 commit4


执行命令


准备工作


我们先介绍一下几个变量:



  • currentFolderId :当前文件夹的 id,默认为 0 也就是最顶层的文件夹

  • currentDirectory : 当前路径

  • currentId : input 输入框的 id 标识


  const [currentId, setCurrentId] = useState<number>(0)
const [currentFolderId, setCurrentFolderId] = useState(0)
const [currentDirectory, setCurrentDirectory] = useState<string>('')

并把一些静态组件封装在 components.tsx 文件中:


image.png


核心介绍


我们用一个对象来存储需要执行对应的方法:


  const commandList: CommandList = {
cat,
cd,
clear,
ls,
help,
mkdir,
touch,
}

executeCommand 方法中,如果用户按下的是'Enter' 键,我们首先判断下输入的 cmd 是否在 commandlist 中,如果存在,就直接执行该方法,如果不存在,就生成一个 CommandNotFound
行:


//app.js 
function executeCommand(){
//...
else if (event.key === 'Enter') {
// 将新输入 command 加入 commandHistory 中
const newArr = commandHistory
newArr.push(input.value.trim())
setCommandHistory(newArr)
// 如果输入 command 符合就执行 ⭐️⭐️⭐️
if (cmd && Object.keys(commandList).includes(cmd))
commandList[cmd](args)
else if (cmd !== '')
generateRow(<CommandNotFound key={key()} command={input.value.trim()} />)
// 每次无论 command 符不符合,都需要生成一行新的 Row,并且 curentId++
setCurrentId(id => id + 1)
setTimeout(() => {
generateRow(
<Row
key={key()}
id={commandHistory.length}
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, commandHistory.length)}
/>,
)
}, 100)
}
//...
}

help


当输入的 cmd 识别为'help'时就会调用该方法,生成在 components.tsx 里 Help()中定义好的静态数据:


  // help 命令
const help = () => {
generateRow(<Help key={key()} />)
}

代码仓库:commit5


cd


首先,默认的currentFolderId为 0,也就是指向我们的根文件夹,我们可以通过 folderSysteam.get(currentFolderId) 来获取当前文件夹下的信息,包括该文件夹的 title,子文件的 id 数组 childIds
当我们获取到了参数 arg 时,首先要判断 是否为空或者'..',若是的话,即返回上一层目录,
如果是正常参数的话,通过 folderSysteam.get(currentFolderId) 获取子目录的 childIds 数组,遍历当前目录下的子目录,找到子目录中 title 和 arg 一样的目录并返回该子目录 id,将 currentFolderId 设置为该子目录 id 并且拼接文件路径:


  // cd 命令
const cd = (arg = '') => {
const dir: string = localStorage.getItem(CURRENTDIRECTORY) as string
//判断是否返回上一层目录
if (!arg || arg === '..') {
// 处理文件路径
const dirArr = dir.split('/')
dirArr.length = Math.max(0, dirArr.length - 2)
//区分是否是root层
if (!dirArr.length)
setCurrentDirectory(`${dirArr.join('')}`)
else
setCurrentDirectory(`${dirArr.join('')}/`)
// 将当前目录设置为上一层目录
setCurrentFolderId(folderSysteam.get(`${currentFolderId}`)?.parentId as number)
return
}
//若是正常的跳转子目录
//根据 arg 参数获取需跳转目录的 id
const id = searchFile(arg)
// 如果子目录存在,设置路径、更新当前目录id
if (id) {
const res = `${dir + folderSysteam.get(`${id}`)?.title}/`
setCurrentFolderId(id)
setCurrentDirectory(res)
}
// 否则返回 NoSuchFileOrDirectory
else { generateRow(<NoSuchFileOrDirectory key={key()} command={arg}/>) }
}
const searchFile = (arg: string) => {
// 对输入做一个优化,例如文件夹名为 Desktop,只要我们输入'Desktop'|'desktop'|'DESKTOP'都行
const args = [arg, arg.toUpperCase(), arg.toLowerCase(), arg.charAt(0).toUpperCase() + arg.slice(1)]
// 获取当前目录下子目录
const childIds = getStorage(CURRENTCHILDIDS)
// 遍历子目录,找到title 为 arg 的目录
for (const item of folderSysteam.entries()) {
if (childIds.includes(item[1].id) && args.includes(item[1].title))
return item[1].id
}
}


ls


  // ls 命令
const ls = () => {
let res = ''
// 获取当前目录下所有子目录 id
const ids = getStorage(CURRENTCHILDIDS)
// 遍历 id 进行拼接
for (const id of ids)
res = `${res + folderSysteam.get(`${id}`)?.title} `
if (!res) {
generateRow(<div key={key()} >There are no other folders or files in the current directory.</div>)
}
else {
res.split(' ').map((item: string) =>
generateRow(<div key={key()} className={item.includes('.') ? 'text-blue-500' : ''}>{item}</div>),
)
}
}

terminal6.gif


代码仓库:commit6| commit6.1


mkdir、touch


创建文件或文件夹,我们只需要创建该文件或文件夹对象,新对象的 parentId 指向当前目录,其新 id 加入到当前目录的 childIds 数组中,最后再更新一下 folderSysteam 变量:


  // mkdir 命令
const mkdir = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
childIds: [],
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}
// touch 命令
const touch = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
content: <div ><h1>
This is <span className='text-red-400 underline'>{arg}</span> file!
</h1>
<p>Imagine there's a lot of content here...</p>
</div>
,
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}


terminal7.gif


代码仓库:commit7


cat、clear


cat 命令只需要展示子文件的 content 属性值即可:


  // cat 命令
const cat = (arg = '') => {
//获取当前目录下 childIds 进行遍历
const ids = getStorage(CURRENTCHILDIDS)
ids.map((id: number) => {
const item = folderSysteam.get(`${id}`) as FolderSysteamType
//生成 title 为 arg 文件的 content Row 行
return item.title === arg ? generateRow(<div key={key()}>{item.content}</div> as JSX.Element) : ''
})
}

clear 命令只需要调用 setContent():


  // clear 命令
const clear = () => {
setContent([])
//清空 input 框内容
const input = document.querySelector('#terminal-input-0') as HTMLInputElement
input.value = ''
}

terminal8.gif
代码仓库:commit8


其它操作


准备工作


我们先介绍一下几个变量:



  • commandHistory : 用于存储输入过的 command数组

  • changeCount : 用来切换 command 计数


  const [changeCount, setChangeCount] = useState<number>(0)
const [commandHistory, setCommandHistory] = useState<string[]>([])

上下键切换 command


上面定义的 changeCount 变量默认为 0,当我们按上🔼键时,changeCount-1,当我们按下🔽键时,changeCount+1。
而当 changeCount 变量变化时,获取当前 input dom 节点,设置其值为commandHistory[commandHistory.length + changeCount],这样我们的上下键切换 command 就实现了:


    // 当按下上下键时 获取历史 command
useEffect(() => {
const input = document.querySelector(`#terminal-input-${commandHistory.length}`) as HTMLInputElement
if (commandHistory.length)
input.value = commandHistory[commandHistory.length + changeCount]
if (!changeCount) {
input.value = ''
setChangeCount(0)
}
}, [changeCount])

// 按向上🔼键
function handleArrowUp() {
setChangeCount(prev => Math.max(prev - 1, -commandHistory.length))
}
// 按向下🔽键
function handleArrowDown() {
setChangeCount(prev => Math.min(prev + 1, 0))
}
// 执行方法
function executeCommand(...) {
//...
if (event.key === 'ArrowUp') {
handleArrowUp()
}
else if (event.key === 'ArrowDown') {
handleArrowDown()
}
//...

Tab 键补全 command


根据历史记录补全 command ,利用 Array.filter() 和 String.startsWith() 就行:


  // 匹配历史 command 并补充
const matchCommand = (inputValue: string): string | null => {
// 遍历历史command 返回以当前输入 command 值开头(startsWith)的 command
const matchedCommands = commandHistory.filter(command => command.startsWith(inputValue))
return matchedCommands.length > 0 ? matchedCommands[matchedCommands.length - 1] : null
}


代码仓库:commit9


最后


大家有兴趣的话可以自己再去二次改造或添加一些新玩法,此组件已通过 Netlify 部署上线,地址为 my-terminal.netlify.app/
项目源代码:github.com/ljq0226/my-… 欢迎 S

作者:Aphelios_
来源:juejin.cn/post/7248599585735098405
tar ⭐️⭐️⭐️

收起阅读 »

前端面试题 - 96. hash 和 history 的区别?

web
hash和history是Web开发中常用的两个概念,它们都与浏览器URL相关。 Hash(哈希) URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个ha...
继续阅读 »

hashhistory是Web开发中常用的两个概念,它们都与浏览器URL相关。


Hash(哈希)


URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个hashchange事件。


// 监听 hashchange 事件
window.addEventListener('hashchange', function() {
var currentHash = window.location.hash;

// 根据不同的哈希值执行相应的操作
if (currentHash === '#section1') {
console.log('显示第一部分的内容')
} else if (currentHash === '#section2') {
console.log('显示第二部分的内容')
} else {
console.log('其他操作')
}
});

通过监听此事件,你可以根据哈希的变化来执行相应的操作,例如显示不同的内容或调用特定的函数。哈希可以直接通过JavaScript进行修改,例如window.location.hash = "section2",URL将变为(此时hashchange事件也会触发):


https://example.com/page.html#section2
// 输出 显示第二部分的内容

History(历史记录)


历史记录是浏览器跟踪用户访问过的URL的一种机制。通过history对象,你可以在JavaScript中操作浏览器的历史记录。一些常用的方法包括history.pushState()history.replaceState()history.back()。这些方法允许你添加、替换和移动浏览器的历史记录,并且不会导致页面的实际刷新。当历史记录发生变化时,浏览器不会重新加载页面,但可以通过popstate事件来捕获这些变化并做出响应。


示例:


// 添加新的历史记录
history.pushState({ page: "page2" }, "Page 2", "page2.html");

// 监听 popstate 事件
window.addEventListener('popstate', function(event) {
var state = event.state;
console.log(state)
// 根据历史记录的变化执行相应的操作
if (state.page === "page1") {
console.log('显示第一页的内容')
} else if (state.page === "page2") {
console.log('显示第二页的内容')
} else {
console.log('其他操作')
}
});

需要注意的是,使用pushState()方法修改历史记录并不会触发popstate事件。只有在用户点击浏览器的前进或后退按钮时,或者通过JavaScript代码调用history.back()history.forward()history.go()方法导致历史记录变化时,popstate

作者:总瓢把子
来源:juejin.cn/post/7248608019851755575
e>事件才会被触发。

收起阅读 »

面试官: 既然有了 cookie 为什么还要 localStorage?😕😕😕

web
Web Storage Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端...
继续阅读 »

Web Storage


Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。


Web Storage 规范最新的版本是第 2 版,这一版规范主要有两个目标:



  1. 提供在 cookie 之外的存储会话数据的途径;

  2. 提供跨会话持久化存储大量数据的机制;


Web Storage 定义了两个对象: localStoragesessionStorage。前者是永久存储机制,而后者是跨会话的存储机制。这两个浏览器存储 API 提供了在浏览器中不收页面刷新影响而存储数据的两种方式。


Storage 类型


Storage 类型用于保存 名/值 对数据,直至存储空间上限(由浏览器决定)。Storage 的实例与其他对象一样,但增加了以下方法:



  1. clear(): 删除所有值;

  2. getItem(name): 取得给定 name 值;

  3. key(index): 取得给定数值位置的名称;

  4. removeItem(name): 删除给定 name名/值 对;

  5. setItem(name,value): 设置给定 name 的值;


getItem()removeItem(name)setItem() 方法可以直接或间接通过 Storage 对象调用。因为每个数据项都作为属性存储在该对象上,所以可以使用点或括号操作符访问这些属性,统统同样的操作来设置值,也可以使用 delete 操作符来删除属性。即便如此,通常还是建议使用方法而非属性来执行这些操作,以免意外重写某个已存在的对象成员。


localStorage 对象


在修订的 HTML5 规范里,localStorage 对象取代了 globalStorage,作为在客户端持久存储数据的机制,要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在想用的端口上使用相同的协议。


因为 localStorageStorage 的实例,所以可以像使用 sessionStorage 一样使用 localStorage。具体实例请看下面几个例子:


// 使用方法存储数据
localStorage.setItem("moment", 777);

// 使用属性存储数据
localStorage.nickname = "moment";

// 使用方法获取数据
const name = localStorage.getItem("moment");

// 使用属性获得数据
const nickname = localStorage.nickname;

两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口,标签也或重新启动浏览器而丢失。


存储事件


每当 Storage 对象发生变化时,都会在文档上触发 storage 事件,使用属性或者 setItem() 设置值、使用 deleteremoveItem() 删除值,以及每次调用 clean() 时都会触发这个事件,这个事件的事件对象有如下四个属性:



  1. domain: 存储变化对应的域;

  2. key: 被设置或删除的键;

  3. newValue: 键被设置的新值,若键被删除则为 null;

  4. oldValue: 键变化之前的值。


我们可以使用如下代码监听 storage 事件:


window.addEventListener("storage", function (e) {
document.querySelector(".my-key").textContent = e.key;
});

对于 sessionStoragelocalStorage 上的任何更改都会触发 storage 事件,但 storage 事件不会区分这两者。


这是一道面试题


在不久前,被问到这样一个问题,我们通过后端返回来的 token 为什么是存储在 localStorage 而不是存储在 cookie 中?


考虑这个问题的首先我们应该知道,token 就是一个字符串,而使用 cookie 的话,大小是满足的,所以考察的点就不在这个内存上面了。


之所以使用 localStorage 存储 token,而不是使用 cookie,这可能基于以下几个方面考虑:



  1. 前后端分离架构: 在一些现代的 Web 应用程序中,前端和后端通常是通过 API 进行通信的,而不是使用传统的服务器端渲染。在这种情况下,前端可能是一个独立的应用程序,如基于 JavaScript 的单页应用或移动应用程序。由于前端和后端是分离的,Cookie 在这种架构中不太容易管理,因为跨域请求可能会遇到一些限制。localStorage 提供了一种更方便的解决方案,前端应用程序可以直接访问和管理存储在本地的令牌;

  2. 安全性需求: 在某些情况下,开发者可能认为将令牌存储在 Cookie 中存在一些安全风险,尤其是在面对跨站脚本攻击 XSS 时。使用 localStorage 可以减少某些安全风险,因为 LocalStorage 中的数据不会自动发送到服务器,且可以通过一些安全措施(如加密)来增强数据的安全性;

  3. 令牌过期处理: 使用 localStorage 存储令牌可以让令牌在浏览器关闭后仍然保持有效,这在某些应用场景下是有用的。例如,用户可能关闭了浏览器,然后再次打开时仍然保持登录状态,而不需要重新输入凭据;


值得注意的是,使用 localStorage 存储 token 也不是说百分百安全的,依然会存在一些问题和风险,如容易收到 XSS 攻击、不支持跨域贡献等。因此,在使用 localStorage 存储令牌时,开发者需要采取适当的安全措施,如加密存储数据、定期更新令牌等,以确保令牌的安全性和有效性。


localStorage 如何实现跨域


localStorage 是一直域限制的存储机制,通常只能在同一域名下的页面中访问。这意味着默认情况下,localStorage 的数据在不同域名或跨域的情况下是无法直接访问的。然而,有几种方法可以实现跨域访问 localStorage 中的数据:



  1. 域名映射(Domain Mapping): 将不同域名都指向同一个服务器 IP 地址。这样不同域名下的页面就可以共享同一个 localStorage 中的数据;

  2. postMessage API: postMessage 是一种浏览器提供的 API,用于在不同窗口或跨域的 iframe 之间进行安全的消息传递。你可以在不同域名的页面中使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中;


使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中,实例代码如下:


// 发送消息到目标窗口
window.postMessage(
{ key: "token", value: "1233211234567" },
"https://liangzai.com"
);

在接收消息的窗口中:


// 监听消息事件
window.addEventListener("message", function (event) {
if (event.origin === "https://sourcedomain.com") {
// 存储数据到 LocalStorage
localStorage.setItem(event.data.key, event.data.value);
}
});

这些方法提供了一些途径来实现跨域访问 localStorage 中的数据。具体选择哪种方法取决于你的需求和应用场景,以及你对目标域名的控制程度。需要注意的是,安全性是非常重要。


cookie 和 localStorage 的区别


CookieLocalStorage 是两种用于在浏览器中存储数据的机制,它们在以下方面有一些区别:



  1. 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合;

  2. 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据;

  3. 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除;

  4. 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些;


总结


Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据。选择使用哪种机制应根据具体的需

作者:Moment
来源:juejin.cn/post/7248623545219825723
求和使用场景来决定。

收起阅读 »

在高德地图中实现降雨图层

web
前言 有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。 需求说明 在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合; 可以结合当地天气预...
继续阅读 »

前言


有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。


需求说明


在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合;


可以结合当地天气预报情况,自动调节风速、风向、降雨量等参数。


需求分析


方案一:全局降雨


在用户视口面前加一层二维的降雨平面层。


优点: 只管二维图层就行了,不需要与地图同步坐标,实现起来比较简单,界面是全局的一劳永逸。


缺点:只适合从某些角度观看,没法再做更多定制了。


Honeycam_2023-06-16_11-10-37.gif


方案二:局部地区降雨


指定降雨范围,即一个三维空间,坐标与地图底图同步,仅在空间内实现降雨。


优点:降落的雨滴有远近关系,比较符合现实场景;可适用各种地图缩放程度。


缺点:需要考虑的参数比较多,比如降雨范围一项就必须考虑这个三维空间是什么形状,可能是立方体、圆柱体或者多边形挤压体;需要外部图层的配合,比如说下雨了,那么天空盒子的云层、建筑图层的明度是否跟着调整。


Honeycam_2023-06-16_11-20-08.gif


实现思路


根据上面利弊权衡,我选择了方案二进行开发,并尽量减少输入参数,降雨影响范围初步定为以地图中心为坐标中心的立方体,忽略风力影响,雨滴采用自由落体方式运动。


降雨采用自定义着色器的方式实现,充分利用GPU并行计算能力,刚好在网上搜到一位大佬写的three演示代码,改一下坐标轴(threejs空间坐标轴y轴朝上,高德GLCustomLayer空间坐标z轴朝上)就可以直接实现最基础的效果。这里为了演示方便增加坐标轴和影响范围的辅助线。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry,该几何体的构成就是在影响范围内随机位置的1000个平面,这些平面与地图底面垂直;


Honeycam_2023-06-24_15-40-31.gif


2.创建雨滴材质,雨滴不受光照影响,这里使用最基础的MeshBasicMaterial材质即可,半透明化且加上一张图片作为纹理;


Honeycam_2023-06-24_15-50-32.gif


3.为实现雨滴随着时间轴降落的动画效果,需要调整几何体的形状尺寸,并对MeshBasicMaterial材质进行改造,使其可以根据当前时间time改变顶点位置;


Honeycam_2023-06-24_16-01-39.gif



  1. 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;


Honeycam_2023-06-24_16-16-52.gif



  1. 将图层叠加到地图3D场景中


Honeycam_2023-06-24_16-28-46.gif


基础代码实现


为降低学习难度,本模块只讲解最基础版本的降雨效果,雨滴做自由落体,忽略风力影响。这里的示例以高德地图上的空间坐标轴为例,即z轴朝上,three.js默认空间坐标系是y轴朝上。我把three.js示例代码演示放到文末链接中。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry


createGeometry () {
// 影响范围:只需要设定好立方体的size [width/2, depth/2, height/2]
//
const { count, scale, ratio } = this._conf.particleStyle
// 立方体的size [width/2, depth/2, height/2]
const { size } = this._conf.bound
const box = new THREE.Box3(
new THREE.Vector3(-size[0], -size[1], 0),
new THREE.Vector3(size[0], size[1], size[2])
)

const geometry = new THREE.BufferGeometry()
// 设置几何体的顶点、法线、UV
const vertices = []
const normals = []
const uvs = []
const indices = []

// 在影响范围内随机位置创建粒子
for (let i = 0; i < count; i++) {
const pos = new THREE.Vector3()
pos.x = Math.random() * (box.max.x - box.min.x) + box.min.x
pos.y = Math.random() * (box.max.y - box.min.y) + box.min.y
pos.z = Math.random() * (box.max.z - box.min.z) + box.min.z

const height = (box.max.z - box.min.z) * scale / 15
const width = height * ratio

// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

vertices.push(...rect)

normals.push(
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z
)

uvs.push(1, 1, 0, 1, 0, 0, 1, 0)

indices.push(
i * 4 + 0,
i * 4 + 1,
i * 4 + 2,
i * 4 + 0,
i * 4 + 2,
i * 4 + 3
)
}

// 所有顶点的位置
geometry.setAttribute(
'position',
new THREE.BufferAttribute(new Float32Array(vertices), 3)
)
// 法线信息
geometry.setAttribute(
'normal',
new THREE.BufferAttribute(new Float32Array(normals), 3)
)
// 设置UV属性与顶点顺序一致
geometry.setAttribute(
'uv',
new THREE.BufferAttribute(new Float32Array(uvs), 2)
)
// 设置基本单元的顶点顺序
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1))

return geometry
}

2.创建材质


createMaterial () {
// 粒子透明度、贴图地址
const { opacity, textureUrl } = this._conf.particleStyle
// 实例化基础材质
const material = new THREE.MeshBasicMaterial({
transparent: true,
opacity,
alphaMap: new THREE.TextureLoader().load(textureUrl),
map: new THREE.TextureLoader().load(textureUrl),
depthWrite: false,
side: THREE.DoubleSide
})

// 降落起点高度
const top = this._conf.bound.size[2]

material.onBeforeCompile = function (shader, renderer) {
const getFoot = `
uniform float top; // 天花板高度
uniform float bottom; // 地面高度
uniform float time; // 时间轴进度[0,1]
#include <common>
float angle(float x, float y){
return atan(y, x);
}
// 让所有面始终朝向相机
vec2 getFoot(vec2 camera,vec2 normal,vec2 pos){
vec2 position;
// 计算法向量到点的距离
float distanceLen = distance(pos, normal);
// 计算相机位置与法向量之间的夹角
float a = angle(camera.x - normal.x, camera.y - normal.y);
// 根据点的位置和法向量的位置调整90度
pos.x > normal.x ? a -= 0.785 : a += 0.785;
// 计算投影值
position.x = cos(a) * distanceLen;
position.y = sin(a) * distanceLen;

return position + normal;
}
`

const begin_vertex = `
vec2 foot = getFoot(vec2(cameraPosition.x, cameraPosition.y), vec2(normal.x, normal.y), vec2(position.x, position.y));
float height = top - bottom;
// 计算目标当前高度
float z = normal.z - bottom - height * time;
// 落地后重新开始,保持运动循环
z = z + (z < 0.0 ? height : 0.0);
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 调整坐标参考值
z += bottom;
z += position.z - normal.z;
// 生成变换矩阵
vec3 transformed = vec3( foot.x, foot.y, z );
`

shader.vertexShader = shader.vertexShader.replace(
'#include <common>',
getFoot
)
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
begin_vertex
)
// 设置着色器参数的初始值
shader.uniforms.cameraPosition = { value: new THREE.Vector3(0, 0, 0) }
shader.uniforms.top = { value: top }
shader.uniforms.bottom = { value: 0 }
shader.uniforms.time = { value: 0 }
material.uniforms = shader.uniforms
}

this._material = material

return material
}

3.创建模型



createScope () {
const material = this.createMaterial()
const geometry = this.createGeometry()

const mesh = new THREE.Mesh(geometry, material)

this.scene.add(mesh)

// 便于调试,显示轮廓
// const box1 = new THREE.BoxHelper(mesh, 0xffff00)
// this.scene.add(box1)
}

4.更新参数


// 该对象用于跟踪时间
_clock = new THREE.Clock()

update () {
const { _conf, _time, _clock, _material, camera } = this

// 调整时间轴进度,_time都值在[0,1]内不断递增循环
// particleStyle.speed为降落速度倍率,默认值1
// _clock.getElapsedTime() 为获取自时钟启动后的秒数
this._time = _clock.getElapsedTime() * _conf.particleStyle.speed / 2 % 1

if (_material.uniforms) {
// 更新镜头位置
_material.uniforms.cameraPosition.value = camera.position
// 更新进度
_material.uniforms.time.value = _time
}
}

animate (time) {
if (this.update) {
this.update(time)
}
if (this.map) {
// 叠加地图时才需要
this.map.render()
}
requestAnimationFrame(() => {
this.animate()
})
}

优化调整


修改场景效果


通过对图层粒子、风力等参数进行封装,只需简单地调整配置就可以实现额外的天气效果,比如让场景下雪也是可以的,广州下雪这种场景,估计有生之年只能在虚拟世界里看到了。


Honeycam_2023-06-24_17-00-11.gif


以下是配置数据结构,可供参考


const layer = new ParticleLayer({
map: getMap(),
center: mapConf.center,
zooms: [4, 30],
bound: {
type: 'cube',
size: [500, 500, 500]
},
particleStyle: {
textureUrl: './static/texture/snowflake.png', //粒子贴图
ratio: 0.9, //粒子宽高比,雨滴是长条形,雪花接近方形
speed: 0.04, // 直线降落速度倍率,默认值1
scale: 0.2, // 粒子尺寸倍率,默认1
opacity: 0.5, // 粒子透明度,默认0.5
count: 1000 // 粒子数量,默认值10000
}
})

添加风力影响


要实现该效果需要添加2个参数:风向和风力,这两个参数决定了粒子在降落过程中水平面上移动的方向和速度。



  1. 首先调整一下代码实际那一节步骤2运动的相关代码


const begin_vertex = `
...
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 增加了下面这几行
float x = foot.x+ 200.0 * ratio; // 粒子最终在x轴的位移距离是200
float y = foot.y + 200.0 * ratio; // 粒子最终在y轴的位移距离是200
...
// 生成变换矩阵
vec3 transformed = vec3( foot.x, y, z );


  1. 如果粒子是长条形的雨滴,那么它在有风力影响的运动时,粒子就不是垂直地面的平面了,而是与地面有一定倾斜角度的平面,如图所示。


Untitled.png


我们调整调整一下代码实际那一节步骤1的代码,实现方式就是让每个粒子平面在创建之后,所有顶点绕着平面的法线中心轴旋转a角度。


本示例旋转轴(x, y, 1)与z轴(0,0,1)平行,这里有个技巧,我们在做平面绕轴旋转的时候先把平面从初始位置orgPos移到坐标原点,绕着z轴旋转后再移回orgPos,会让计算过程简单很多。


// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

// 定义旋转轴
const axis = new THREE.Vector3(0, 0, 1).normalize();
//定义旋转角度
const angle = Math.PI / 6;
// 创建旋转矩阵
const rotationMatrix = new THREE.Matrix4().makeRotationAxis(axis, angle);

for(let index =0; index< rect.length; index +=3 ){
const vec = new THREE.Vector3(rect[index], rect[index + 1], rect[index + 2]);
//移动到中心点
vec.sub(new THREE.Vector3(pos.x, pos.y,pos.z))
//绕轴旋转
vec.applyMatrix4(rotationMatrix);
//移动到原位
vec.add(new THREE.Vector3(pos.x, pos.y, pos.z))
rect[index] = vec.x;
rect[index + 1] = vec.y;
rect[index + 2] = vec.z;
}

待改进的地方


本示例中有个需要完善的地方,就是加入了风力影响之后,如果绕垂直轴旋转一定的角度,会看到如下图的异常,雨点的倾斜角度和运动倾斜角度是水平相反的。


Honeycam_2023-06-24_21-06-51.gif


问题的原因是材质着色器中的“让所有面始终朝向相机”方法会一直维持粒子的倾斜状态不变,解决这个问题应该是调整这个方法就可以了。然而作为学渣的我还没摸索出来,果然可视化工程的尽头全是数学Orz。


相关链接


1.THREE.JS下雨进阶版,面只旋转Y轴朝向相机


http://www.wjceo.com/blog/threej…


2.演示代码在线DEMO


jsfiddle.net/gyrate

sky/5…

收起阅读 »

我有个气人的同事......

web
前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。 曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 ...
继续阅读 »

前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。




曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 console-custom。沉寂在个人仓库很久,前段时间看到别人也有类似仓库,也就想着把自己的也发出来。




其实,我个人不是很推荐在代码里 写 console.log 之类的来调试代码,更推荐去浏览器控制台去打断点来调试,更好的理清数据的流转,事件的先后顺序等。



背景


官方背景:



  • 方便大家调试代码的时候,在浏览器控制台输出自定义个性化日志。

  • 防止控制台输出密密麻麻的 console.log,一眼看不到想看的。

  • 防止某个气人的小伙伴老是使用 console.error,强迫症不允许。

  • ......


真实背景:


其实,是我之前有个小伙伴同事——“小白菜”(也是为啥函数名叫 blog 的原因之一,下边会看到),他调试代码,打印输出总是喜欢 console.error(),用完了还不自己清理,大家协同开发的时候,git pull 他的代码后,总是让人就很难受!看着一堆报错,一时半会看不清是程序自己的报错,还是调试的输出!强迫症就犯了!想骂街......


不......不......要冷静!



  • 编码千万行

  • 调试要输出

  • log不规范

  • 同事两行泪


效果


浏览器页面 page


tu1.jpg


浏览器控制台 console


image.png



有个重点、痛点是这个, console.log(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333); 打印的数据多的时候不换行,需要找半天,我专门给处理成 分行, 一行一行展示了,这样好看清数据。



这个工具库有以下几个特点:



  1. 支持输入多个数据,并分行打印出来,并能看出是个整体

  2. 支持自己修改自己的默认样式部分,配置自己的默认部分

  3. 支持额外的自定义部分,拓展用户更多的玩法

  4. ......


其实 console 由于可以自定义,其实会有很多玩法,我个人在此主要的思路是



  1. 一定要简单,因为 console.log 本身就很简单,尽量不要造成使用者心智负担。

  2. 就简单的默认定制一个彩色个性化的部分,能区分出来,解决那个气人同事所谓的痛点就好。

  3. 代码要少,不要侵入,不要影响用户的业务代码


源码


此处源码有借鉴 github 开源代码:github.com/Redstone-1/…


大家如有更多、更丰富的需求场景可去参考使用。


// src/utils/console-custom.js
const GourdBabyColorMap = new Map([
["1", "#FF0000"],
["2", "#FFA500"],
["3", "#FFFF00"],
["4", "#008000"],
["5", "#00FFFF"],
["6", "#0000FF"],
["7", "#800080"],
]);

const createBLog = (config) => {
const logType = config.logType || "default";
const username = config.username || "";
const logName = config.logName || "";
const usernameColor = config.usernameColor || "#41b883";
const logNameColor = config.logNameColor || "#35495e";
const padding = config.padding || 6;
const borderRadius = config.borderRadius || 6;
const fontColor = config.fontColor || "#FFFFFF";
const usernameStyle = config.usernameStyle || "";
const logNameStyle = config.logNameStyle || "";

const logTemplate = (username = "myLog", logName = "") =>
`${username ? '%c' + username : ''} ${logName ? '%c' + logName : ''} `;

const customLog = (...data) => {
console.log(
logTemplate(username, logName),
usernameStyle ? usernameStyle : `background: ${usernameColor}; padding: 6px; border-radius: 6px 0 0 6px; color: #fff`,
logNameStyle ? logNameStyle : `background: ${logNameColor}; padding: 6px; border-radius: 0 6px 6px 0; color: #fff`,
...data
);
};

const defaultLog = (...data) => {
const len = data.length;
if (len > 1) {
data.map((item, index) => {
let firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
let secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
if (index === 0) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: ${borderRadius}px 0 0 0;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: 0 ${borderRadius}px 0 0;
color: ${fontColor}
`
;
} else if (index === len -1) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 0 ${borderRadius}px;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 ${borderRadius}px 0;
color: ${fontColor}
`
;
}
console.log(
logTemplate(username, `数据${index+1}`),
firstStyle,
secondStyle,
item
);
});
} else {
const firstStyle = `
background: ${usernameColor};
padding: ${padding}px;
border-radius: ${borderRadius}px 0 0 ${borderRadius}px;
color: ${fontColor}
`
;

const secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 ${borderRadius}px ${borderRadius}px 0;
color: ${fontColor}
`
;

console.log(
logTemplate(username, logName),
firstStyle,
secondStyle,
...data
);
}
};

const log = (...data) => {
switch(logType) {
case 'custom':
customLog(...data)
break;
default:
defaultLog(...data)
}
};

return {
log,
};
};

export default createBLog

API


唯一API createBLog(对!简单!易用!用起来没有负担!)


import createBLog from '@/utils/console-custom'

const myLog = createBLog(config)

配置 config: Object


一次配置,全局使用。(该部分是借鉴开源代码重构了配置内容)


配置项说明类型默认值
logTypelog 日志类型default、customdefault
usernamelog 的主人,也就是谁打的日志string-
logNamelog 的名字,也就是打的谁的日志string-
usernameColorusername 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#41b883
logNameColorlogName 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#35495e
paddingusername 和 logName 内边距,单位 pxnumber6
borderRadiususername 和 logName 圆角边框,单位 pxnumber6
fontColorusername 和 logName 字体颜色string#FFFFFF
usernameStyleusername 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-
logNameStylelogName 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-

基本用法 default



也是默认用法(default),同时也是最推荐大家用的一种方法。



vue2 版本


// main.js
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

// 不需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$blog = myLog.log;

// 需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$nlog = (logName, ...data) => {
myLog.logName = logName;
myLog.log(...data);
};

// vue2 组件里边使用
// 同时输入多个日志数据,可帮用户按照行的形式分开,好一一对应看清 log
this.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
this.$blog(111231231231231);

this.$nlog("logName", 2212121212122);

vue3 版本


// main.ts
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

app.config.globalProperties.$blog = myLog.log;

// vue3 组件里边使用
import { getCurrentInstance } from 'vue'

export default {
setup () {
const { proxy } = getCurrentInstance()

proxy.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
proxy.$blog(111231231231231);

proxy.$nlog("logName", 2212121212122);
}
}

自定义用法 custom



这部分我没有很多玩法,下边的例子也是借鉴别人的,主要全靠用户自己扩展 css 样式了。做一套自己喜欢的样式。



// main.js

// ....
Vue.prototype.$clog = (logName, ...data) => {
myLog.logType = "custom";
myLog.logName = logName;
myLog.usernameStyle = `text-align: center;
padding: 10px;
background-image: -webkit-linear-gradient(left, blue,
#66ffff 10%, #cc00ff 20%,
#CC00CC 30%, #CCCCFF 40%,
#00FFFF 50%, #CCCCFF 60%,
#CC00CC 70%, #CC00FF 80%,
#66FFFF 90%, blue 100%);`
;
myLog.logNameStyle = `background-color: #d2d500;
padding: 10px;
text-shadow: -1px -1px 0px #e6e600,-2px -2px 0px #e6e600,
-3px -3px 0px #e6e600,1px 1px 0px #bfbf00,2px 2px 0px #bfbf00,3px 3px 0px #bfbf00;`
;
myLog.log(...data);
};

// 提供的其他 css 样式
myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 0px 0px 15px #00FFFF,0px 0px 15px #00FFFF,0px 0px 15px #00FFFF;`
;
myLog.logNameStyle = `background-color: gray;
color: #eee;
padding: 10px;
text-shadow: 5px 5px 0 #666, 7px 7px 0 #eee;`
;

myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 1px 1px 0px #0000FF,2px 2px 0px #0000FF,-1px -1px 0px #E31B4E,-2px -2px 0px #E31B4E;`
;
myLog.logNameStyle = `font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
text-transform: uppercase;/* 全开大写 */
padding: 10px;
color: #f1ebe5;
text-shadow: 0 8px 9px #c4b59d, 0px -2px 1px #fff;
font-weight: bold;
letter-spacing: -4px;
background: linear-gradient(to bottom, #ece4d9 0%,#e9dfd1 100%);`
;
// ....

其中渐变色的玩法


myLog.usernameStyle = `background-image: linear-gradient(to right, #ff0000, #ff00ff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;
myLog.logNameStyle = `background-image: linear-gradient(to right, #66ff00 , #66ffff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;

其中输出 emoji 字符


this.$nlog("😭", 2212121212122);
this.$nlog("🤡", 2212121212122);
this.$nlog("💩", 2212121212122);
this.$nlog("🚀", 2212121212122);
this.$nlog("🎉", 2212121212122);
this.$nlog("🐷", 2212121212122);

小伙伴们你肯定还有什么好玩的玩法!尽情发挥吧!


最后


还是想极力劝阻那些用 console.error() 调试代码的人,同时也能尽量少用 console 来调试,可以选择控制台断点、编译器断点等。还是不是很推荐使用 console 来调试,不过本文也可以让大家知道,其实 console 还有这种玩法。如果写 JS 库的时候也可以使用,让自己

作者:Bigger
来源:juejin.cn/post/7248448028297855035
的库极具自己的特色。

收起阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf

web
常见 Node.js 版本管理器比较:nvm、Volta 和 asdf 随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本...
继续阅读 »

常见 Node.js 版本管理器比较:nvm、Volta 和 asdf


随着 Node.js 的发展,能够管理不同版本的运行时以确保兼容性和稳定性非常重要。这就是 Node.js 版本管理器的用武之地!在本文中,我们将比较和对比三种流行的 Node.js 版本管理器:nvm、voltaasdf 来帮助你为你的开发环境选择合适的版本管理器。


nvm


nvm(Node Version Manager)是最古老和最受欢迎的 Node.js 版本管理器之一,至今仍在积极维护。nvm 允许开发人员在一台机器上安装和管理多个版本的 Node.js。它还提供了一个方便的命令行界面,用于在可用版本之间切换。


nvm 的工作原理是将 Node.js 的每个版本安装到下的独立目录中。使用 nvm use 在版本之间切换时,它会更新环境变量以指向相应的目录。所以可以并行安装多个版本的 Node.js,并且每个版本都有自己的一组全局安装的软件包 ~/.nvm/versions/node/$PATH


nvm 的一个缺点是它只支持 Node.js。如果需要管理其他编程语言或工具,则需要使用单独的版本管理器。另外,nvm 需要手动安装和配置,这对于初学者来说可能有点难受。


要安装 nvm,可以使用以下命令:


curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

# or

wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash

这将从官方 NVM GitHub 仓库下载并运行 NVM 安装脚本,该仓库将在你的系统上安装 NVM。安装完成后,你可以通过运行以下命令来验证是否已安装 NVM:


nvm --version

1.1 如何使用特定版本的 Node.js


要将特定版本的 Node.js 与 nvm 配合使用,你需要执行以下步骤:



  1. 列出可用的 Node.js 版本:要查看可以使用 nvm 安装的所有可用 Node.js 版本的列表,请运行以下命令:


nvm ls-remote


  1. 安装所需版本:要安装特定版本的 Node.js,例如版本 18,请使用以下命令:


nvm install 18


  1. 使用已安装的版本:安装所需的 Node.js 版本后,你可以通过运行以下命令来使用它:


nvm use 16

设置默认版本:如果要使用特定版本的 Node.js 默认情况下,可以使用以下命令将其设置为默认版本:


nvm alias default 18

Volta


volta 是一个较新的 Node.js 版本管理器,旨在简化 Node.js 和其他工具的安装和管理,在 2019 年出世,仍在积极开发中。Volta 采用了与 nvm 不同的方法:它不是管理 Node.js 的多个版本,而是管理项目及其依赖项。当你创建新项目时,volta 会自动检测所需的 Node.js 版本并为你安装它。


volta 还支持其他工具,如 Yarn 和 Rust,开箱即用(不仅仅是 Node.js!对于使用多种编程语言并需要单个工具来管理它们的开发人员来说,这使其成为一个不错的选择。与 nvm 一样,volta 提供了一个命令行界面,用于在 Node.js 版本之间切换,但它通过使用拦截对 node 可执行文件的调用的全局填充程序来实现。


要安装 volta,你可以使用以下命令:


curl https://get.volta.sh | bash

此命令将下载并执行安装 volta 的脚本。


如何使用特定版本的 Node.js



  1. 安装所需版本:安装 volta 后,你可以使用它创建一个新项目并使用 volta install 命令设置所需的 Node.js 版本,以下命令创建一个新项目并将所需的 Node.js 版本设置为 16.0.0:


volta install node@16.0.0


  1. 在该项目的上下文中运行命令:此命令使用所需版本的 Node.js 在项目的上下文中运行 app.js 文件。


volta run node app.js


  1. 切换版本:你也可以使用 Volta 在不同版本的 Node.js 之间切换。例如,要切换到版本 10.0.0,可以使用以下命令:


volta pin node@10.0.0


  1. 设置默认版本:最后,以下命令将你的环境切换为 Node.js 版本 16.0.0 并设置为 Node.js 的默认版本:


nvm alias default 16.0.0

volta 的一个潜在缺点是它仍然是一个相对较新的工具,因此它可能不像 nvm 那样经过实战测试,而且它的社区支持有限,插件和集成也较少。


ASDF


ASDF 是一个版本管理器,旨在成为“通用语言版本管理器”。在 2015 年出世,支持广泛的编程语言和工具,包括 Node.js。ASDF 设计为可扩展,因此可以轻松地添加新语言和工具的支持。


asdf 支持几种流行的编程语言,包括 Node.js,Ruby,Python,Elixir,Java,Rust,PHP,Perl,Haskell,R,Lua 和 Earlang。这意味着你可以在一个地方管理不同的语言版本!如果要在不同语言的项目之间切换,使 asdf 成为一个不错的选择。


与 volta 一样,ASDF 管理项目及其依赖项,而不是同一工具的多个版本。创建新项目时,asdf 会自动检测所需的 Node.js 版本并为你安装。asdf 提供了一个命令行界面,用于在 Node.js 版本以及其他工具之间切换。


asdf 的一个潜在缺点是它的设置可能比 nvm 或 volta 复杂一些。你需要安装多个插件来添加对不同语言和工具的支持,并且可能需要修改 shell 配置以正确使用 asdf。


下面是如何使用 ASDF 安装和使用特定 Node.js 版本的示例:



  1. 安装 ASDF:可以使用以下命令安装 ASDF:


brew install asdf


  1. 将 Node.js 插件添加到 ASDF:你必须安装插件才能将 Node.js 添加到你的项目中


asdf plugin add nodejs


  1. 安装 Node.js 版本 18:使用以下命令使用特定版本的 Node.js:


asdf install nodejs 18


  1. 使用特定版本:


asdf global nodejs 18

nvm,volta 和 asdf 之间的差异



  1. 目的: NVM,Volta 和 ASDF 有不同的用途。NVM 专注于管理多个版本的 Node.js。而 Volta 将 Node.js 版本管理和包管理结合在一个工具中。ASDF 是一个版本管理器,支持多种编程语言,包括 Node.js。

  2. 安装: NVM、Volta 和 ASDF 的安装过程不同。NVM 可以使用 curl 命令安装,而 Volta 要求你手动下载并安装它。ASDF 可以使用 Homebrew 等包管理器安装,也可以直接从 GitHub 下载。

  3. 配置: NVM、Volta 和 ASDF 的配置过程是不同的。NVM 要求你手动更新 shell 配置文件。Volta 不需要任何手动配置。ASDF 要求你手动设置所需的插件。

  4. 自动版本检测: Volta 是唯一通过读取项目的 package.json 文件自动检测项目所需的 Node.js 版本的版本管理器。

  5. 包管理: Volta 是唯一将 Node.js 版本管理和包管理结合在一个工具中的版本管理器。NVM 和 ASDF 仅管理 Node.js 版本。


相似之处



  1. 多节点.js版本: NVM、Volta 和 ASDF 都允许你在同一台机器上管理多个版本的 Node.js。

  2. 全局和本地 node.js 版本: 所有三个版本管理器都允许你全局或本地安装 Node.js 版本。

  3. 简单命令: NVM、Volta 和 ASDF 有简单的命令来管理 Node.js 版本。

  4. 兼容性: 所有三个版本管理器都与 macOS,Linux 和 Windows 操作系统兼容。

  5. 开源: NVM、Volta 和 ASDF 都是开源项目,这意味着它们可以免费使用,并且可以由社区贡献。

  6. 版本锁定: 所有三个版本管理器都允许你锁定特定项目的 Node.js 版本,确保所有团队成员使用相同的版本。


小结


总之,nvmVoltaasdf 都是很棒的 Node.js 版本管理器,可以帮助你更改,管理和更新 Node.js 的多个版本,还可以与新版本保持同步,包括 LTS 版本。Nvm 是最古老和最受欢迎的版本管理器之一,Volta 有不同的方法,它不是管理多个版本的 Node.js,而是管理项目及其依赖项,最后 asdf 管理不同的语言版本,如果你在使用不同语言的项目之间切换,这使得 asdf

作者:夏安君
来源:juejin.cn/post/7247543825535270968
是一个不错的选择。

收起阅读 »

强制缓存这么暴力,为什么不使用协商缓存😡😡😡

web
前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存 和 协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。 强缓存和协商缓存 浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档...
继续阅读 »

前段时间在看面经的时候,发现很多份面经中都被问到了 强缓存协商缓存。因此我觉得有必要写一篇文章来好好聊聊这两者。


强缓存和协商缓存


浏览器缓存是浏览器在本地磁盘对用户最近请求过的文档进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载文档,其中浏览器缓存就分为 强缓存协商缓存:



  1. 强缓存: 当浏览器在请求资源时,根据响应头中的缓存策略信息,判断是否使用本地缓存副本而无需发送请求到服务器。如果资源被标记为强缓存,浏览器会直接从本地缓存中加载资源,而不发送请求到服务器,从而提高页面加载速度并减轻服务器负载;

  2. 协商缓存: 协商缓存是一种缓存策略,用于在资源未过期但可能已经发生变化时,通过与服务器进行协商确定是否使用本地缓存。协商缓存通过在请求中发送特定的条件信息,让服务器判断资源是否有更新,并返回相应的状态码和响应头信息,以指示浏览器是否可以使用本地缓存;


所以根据以上所聊到的特点,浏览器缓存有以下几个方面的优点:



  1. 减少冗余的数据传输;

  2. 减少服务器负担;

  3. 加快客户端加载网页的速度;


20230624082050


浏览器会首先获取该资源缓存的 header 信息,然后根据 Cache-Controlexpires 来判断是否过期。


如图,在浏览器第一次发送请求后,需要再次发送请求时,它会经过以下几个步骤:




  1. 首先,浏览器发送请求到服务器,请求的资源可能是一个网页、css 文件、JavaScript 文件或者其他类型的文件;




  2. 当服务器接收到请求后,首先检查请求中的缓存策略,例如请求头中的 Cache-Controlexpires 字段;




  3. 如果资源被标记为强缓存,服务器会进行以下判断:



    • 如果缓存有效,即资源的过期时间未到达或过期时间在当前时间之后,服务器返回状态码为 200 ok,并在响应头中设置适当的缓存策略,例如设置 Cache-ControlExpires 字段,告诉浏览器可以使用本地缓存;

    • 如果缓存无效,即资源的过期时间已过或过期时间在当前时间之前,服务器返回新的资源,状态码为 200 ok,并在响应头中设置适当的缓存策略;




  4. 如果资源未被标记为强缓存或缓存验证失败,服务器进行协商缓存的判断:



    • 如果请求头中包含 If-Modified-Since 字段,表示浏览器之前缓存了该组员并记录了最后修改时间,服务器会根据资源的最后修改时间进行判断;

      • 如果资源的最后修改时间与 If-Modified-Since 字段的值相同或更早,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应头;

      • 如果资源的最后修改时间晚于 If-Modified-Since 字段的值,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 ok,并在响应头中设置新的最后修改时间;



    • 如果请求头中包含 If--Match 字段,表示浏览器之前缓存了该资源并记录资源的 ETag 值,服务器会根据资源的 ETag 进行判断:

      • 如果资源的 ETagIf--Match 字段的值相同,服务器返回状态码 304 Not Modified,并在响应头中清除实际的响应体;

      • 如果资源的 ETagIf--Match 字段的值不同,表示资源已经发生了变化,服务器返回新的资源,状态码为 200 OK,并在响应头中设置新的 ETag;






  5. 浏览器接收到服务器的响应之后,根据状态码和响应头信息进行相应的处理:



    • 如果状态码为 200 OK,表示服务器返回了新的资源,浏览器使用新的资源并更新本地缓存;

    • 如果状态码为 304 Not Modified,表示资源未发生变化,浏览器使用本地缓存的副本;

    • 浏览器根据响应头中的缓存策略进行进一步处理:

      • 如果响应头中包含 Cache-Control 字段,浏览器根据其指令执行缓存策略。例如,如果响应头中的 Cache-Control 包含 no-cache,浏览器将不使用本地缓存,而是向服务器发送请求获得最新的资源;

      • 如果响应头中包含 Expires 字段,浏览器将与当前时间比较,判断资源的过期时间。如果过期时间已过,浏览器将不使用本地缓存,而是向服务器发送请求获取最新的资源;

      • 如果响应头中包含其他相关的缓存控制字段(如 ETag),浏览器可以根据这些字段进行更精确的缓存控制和验证;






其中,在上面的流程中,又有几个令人难懂的字段,主要有以下几个:



  1. ETag: 它是通过对比浏览器和服务器资源的特征值来决定是否要发送文件内容,如果一样就只发送 304 Not Modified;

  2. Expires: 设置过期时间,是绝对时间;

  3. Last-Modified: 以时刻作为标识,无法识别一秒内进行多次修改的情况,只要资源修改,无论内容是否发生实质性变化,都会将该资源返回客户端;

  4. If--Match: 当客户端发送 GET 请求时,如果之前已经键相用资源的请求时,并且服务器返回了 ETag,那么客户端可以将 ETag 的值添加到 If--Match 头中,当再次请求该资源时,客户端会将 If--Match 头发送给服务器,服务器收到请求之后,会检查 If--Match 投中的值是否与当前资源的 ETag 值匹配:

    • 如果匹配,则表示客户端所请求的资源没有发生变化,服务器会返回状态码 304 Not Modified,并且不返回实际的资源内容;

    • 如果 If--Match 头中的值与服务器上资源的 ETag 值不匹配,说明资源发生了变化,服务器会正常返回资源,并返回状态码 200 OK;




图解强缓存和协商缓存


在上面的内容中讲了这么多的理论, 你是否还是不太理解什么是 强缓存协商缓存 啊,那么接下来我们就用几张图片来弄清楚这两者的区别。


强缓存


强缓存就是文件直接从本地缓存中获取,不需要发送请求。


首次请求


20230624103449


当浏览器发送初次请求时,浏览器会向服务器发起请求,服务器接收到浏览器的请求后,返回资源并返回一个 Cache-Control 字段给客户端,在该字段中设置一些缓存相关的信息,例如最大过期时间。


再次请求


20230624103906


在前面的基础上,浏览器再次发送请求,浏览器一节接收到 Cache-Control 的值,那么这个时候浏览器它会首先检查它的 Cache-Control 是否过期,如果没有过期则直接从本地缓存中拉取资源,返回割到客户端,则无需再经过服务器。


缓存失效


20230624104233


强缓存有过期时间,那么就意味着总有一天缓存会失效,如果客户端的 Cache-Control 失效了,那么它就会像首次请求中一样,重新向服务器发起请求,之后服务器会再次返回资源和 Cache-Control 的值。


协商缓存


协商缓存也叫做对比缓存,服务端判断客户端的资源是否和服务端的一样,如果一样则返回 304,反之返回 200 和最新的资源。


初次请求


20230624112243


如果客户端是第一次向服务器发出请求,则服务器返回资源和对应的资源标识给浏览器,该资源标识就是对当前所返回资源的唯一标识,可以是 ETag 或者是 Last-Modified


之后如果浏览器再次发送请求是,浏览器就会带上这个资源表,此时服务端就会通过这个资源标识,可以判断出浏览器的资源跟服务器此时的资源是否一致,如果一致则返回 304 Not Modified,如果不一致,则返回 200,并返回资源以及新的资源标识。


不同刷新操作方式,对强制缓存和协商缓存的影响


不同的刷新操作方式对于强制缓存和写上缓存的影响如下:




  1. 普通刷新(F5刷新按钮):



    • 强制缓存的影响: 浏览器忽略强制缓存,直接向服务器发送请求,获取最新的资源,也就是强制缓存失效;

    • 协商缓存的影响: 浏览器放带有缓存验证的字段的请求,浏览器会根据验证结果返回新的资源或者 304 Not Modified;




  2. 强制刷新(Ctrl+F5Shift+刷新按钮):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器发送不带缓存验证字段的请求,服务器返回新的资源,不进行验证,也就是协商缓存失效;




  3. 禁用缓存刷新(DevTools 中的 Disable cacheNetwork 勾选 Disable cache):



    • 强制缓存的影响: 同上,强制缓存失效;

    • 协商缓存的影响: 浏览器会发送带有缓存验证字段的请求,服务器会根据验证结果返回新的资源或 304 Not Modified;




这玩意也就图一乐,一般出现了问题我都是直接重启......


总结


总的来说,强制缓存是通过在请求中添加缓存策略,判断缓存是否有效,避免发送请求到服务器。而协商缓存是通过条件请求与服务器进行通信,验证缓存是否仍然有效,并在服务器返回适当的响应状态码和缓存策略。


强制缓存可以减少对服务器的请求,加快资源加载速度,但可能无法获取到最新的资源。协商缓存能够验证资源的有效性,并在需要时获取最新的资源,但会增加对服务器的请求。选择使用哪种缓存策略取决于具体的应用场景和资源的特性。


参考资料


你知道 304 吗?图解强缓存和协商缓存


作者:Moment
来源:juejin.cn/post/7248235392284721209
收起阅读 »

你真的会用<a>标签下载文件吗?

web
最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。 <a> 标签 download 这应该是最常见,最受广大人民群众喜闻...
继续阅读 »

最近和后端联调下载时忽然发现屡试不爽的 <a> 标签下载失灵了?这才感觉自己对文件下载一直处在一知半解的模糊状态中,趁端午前夕有空赶紧总结了一下,和大家一起讨论讨论。


<a> 标签 download


这应该是最常见,最受广大人民群众喜闻乐见的一种下载方式了,搭配上 download 属性, 就能让浏览器将链接的 URL 视为下载资源,而不是导航到该资源。


如果 download 再指定个 filename ,那么就可以在下载文件时,将其作为预填充的文件名。不过名字中的 /\ 会被转化为下划线 _,而且文件系统可能会阻止文件名中的一些字符,因此浏览器会在必要时适当调整文件名。


封装下载方法


贴份儿我常用的下载方法:


const downloadByUrl = (url: string, filename: string) => {
if (!url) throw new Error('当前没有下载链接');

const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = filename;
// 使用target="_blank"时,添加rel="noopener noreferrer" 堵住钓鱼安全漏洞 防止新页面window指向之前的页面
a.rel = "noopener noreferrer";
document.body.append(a);
a.click();

setTimeout(() => {
a.remove();
}, 1000);
};

Firefox 不能一次点击多次下载


这里有个兼容性问题:在火狐浏览器中,当一个按钮同时下载多个文件(调用多次)时,只能下载第一个文件。所以,我们可以利用 <a> 标签的 target 属性,将其设置成 _blank 让火狐在一个新标签页中继续下载。


// 检查浏览器型号和版本
const useBrowser = () => {
const ua = navigator.userAgent.toLowerCase();
const re = /(msie|firefox|chrome|opera|version).*?([\d.]+)/;
const m = ua.match(re);
const Sys = {
browser: m[1].replace(/version/, "'safari"),
version: m[2]
};

return Sys;
};

添加一个浏览器判断:


const downloadByUrl = (url: string, filename: string) => {
// 略......

// 火狐兼容
if (useBrowser().browser === "firefox") {
a.target = "_blank";
}

document.body.append(a);
}

download 使用注意点


<a> 标签虽好,但还有一些值得注意的点:


1. 同源 URL 的限制



download 只在同源 URL 或 blob:data: 协议起作用



也就是说跨域是下载不了的......


首先,非同源 URL 会进行导航操作。其次,如果非要下载,那么正如上面的文档所说,可以先将其转换为 blob:data: 再进行下载,至于如何转换会在 Blob 章节中详细介绍。


2. 无法鉴权


使用 <a> 标签下载是带不了 Header 的,因此也不能携带登录态,所以无法进行鉴权。这里我们给出一个解决方案:



  1. 先发送请求获取 blob 文件流,这样就能在请求时进行鉴权;

  2. 鉴权通过后再执行下载操作。


这样是不是就能很好的同时解决问题1和问题2带来的两个痛点了呢😃


顺便提一下,location.hrefwindow.open 也存在同样的问题。


3. download 与 Content-Disposition 的优先级


这里需要关注一个响应标头 Content-Disposition,它会影响 <a>的 download 从而可能产生不同的下载行为,先看一个真实下载链接的 Response Headers


Snipaste_2023-06-20_18-19-21.png


如图所示,Content-Disposition 的 value 值为 attachment;filename=aaaa.bb。请记住,此时Content-Disposition 的 filename 优先级会大于 <a> download 的优先级。也就是说,当两者都指定了 filename 时,会优先使用 Content-Disposition 中的文件名。


接下来我们看看这个响应标头到底是什么。


Content-Disposition



在常规的 HTTP 应答中,Content-Disposition 响应标头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。



Content-Type 不同,后者用来指示资源的 MIME 类型,比如资源是图片(image/png)还是一段 JSON(application/json),而 Content-Disposition 则是用来指明该资源是直接展示在页面上的,还是应该当成附件下载保存到本地的。


当它作为 HTTP 消息主题的标头时,有以下三种写法:


Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

inline


默认值,即指明资源是直接展示在页面上的。
但是在同源 URL 情况下,<a> 元素的 download 属性优先级比 inline 大,浏览器优先使用 download 属性来处理下载(Firefox 早期版本除外)。


attachment


即指明资源应该被下载到本地,大多数浏览器会呈现一个 “保存为” 的对话框,如果此时有 filename,那么它将其优于 download 属性成为下载的预填充文件名。


<a>标签 VS Content-Disposition


介绍完 Content-Disposition,我们做一个横向比对的归纳一下:




  • download VS inline/attachment


    优先级:attachment > download > inline




  • download 的值 VS filename


    优先级:filename > download 的值




Blob 转换


前文介绍到,在非同源请情况下可以将资源当成二进制的 blob 先拿到手,再进行 <a> 的下载处理。接下来,我们介绍两种 blob 的操作:


方法1. 用作 URL(blob:)


URL.createObjectURL 可以给 FileBlob 生成一个URL,形式为 blob:<origin>/<uuid>,此时浏览器内部就会为每个这样的 URL 存储一个 URL → Blob 的映射。因此,此类 URL 很短,但可以访问 Blob。


那这就好办多了,写成代码就三行:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 生成访问 blob 的 URL
const url = URL.createObjectURL(blob);

// 调用刚刚封装的 a 标签下载方法
downloadByUrl(url, "表格文件.xlsx");

// 删除映射,释放内存
URL.revokeObjectURL(url);
};

不过它有个副作用。虽然这里有 Blob 的映射,但 Blob 本身只保存在内存中的。浏览器无法释放它。


在文档退出时(unload),该映射会被自动清除,因此 Blob 也相应被释放了。但是,如果应用程序寿命很长,那这个释放就不会很快发生。


因此,如果我们创建一个 URL,那么即使我们不再需要该 Blob 了,它也会被挂在内存中。


不过,URL.revokeObjectURL 可以从内部映射中移除引用,允许 Blob 被删除并释放内存。所以,在即时下载完资源后,不要忘记立即调用 URL.revokeObjectURL。


方法2. 转换为 base64(data:)


作为 URL.createObjectURL 的一个替代方法,我们也可以将 Blob 转换为 base64-编码的字符串。这种编码将二进制数据表示为一个由 0 到 64 的 ASCII 码组成的字符串,非常安全且“可读”。


更重要的是 —— 我们可以在 “data-url” 中使用此编码。“data-url” 的形式为 data:[<mediatype>][;base64],<data>。我们可以在任何地方使用这种 url,和使用“常规” url 一样。


FileReader 是一个对象,其唯一目的就是从 Blob 对象中读取数据,我们可以使用它的 readAsDataURL 方法将 Blob 读取为 base64。请看以下示例:


import downloadByUrl from "@/utils/download";

const download = async () => {
const blob = await fetchFile();

// 声明一个 fileReader
const fileReader = new FileReader();

// 将 blob 读取成 base64
fileReader.readAsDataURL(blob);

// 读取成功后 下载资源
fileReader.onload = function () {
downloadByUrl(fileReader.result);
};
};

在上述例子中,我们先实例化了一个 fileReader,用它来读取 blob。


一旦读取完成,就可以从 fileReader 的 result 属性中拿到一个data: URL 格式的 Base64 字符串。


最后,我们给 fileReader 注册了一个 onload 事件,在读取操作完成后开始下载。


两种方法总结与对比


URL.createObjectURL(blob) 可以直接访问,无需“编码/解码”,但需要记得撤销(revoke);


Data URL 无需撤销(revoke)任何操作,但对大的 Blob 进行编码时,性能和内存会有损耗。


总而言之,这两种从 Blob 创建 URL 的方法都可以用。但通常 URL.createObjectURL(blob) 更简单快捷。


responseType


最后,我们回头说一下请求的注意点:如果你的项目使用的是 XHR (比如 axios)而不是 fetch, 那么请记得在请求时添加上 responseType 为 'blob'。


export const fetchFile = async (params) => {
return axios.get(api, {
params,
responseType: "blob"
});
};

responseType 不是 axios 中的属性,而是 XMLHttpRequest 中的属性,它用于指定响应中包含的数据类型,当为 "blob" 时,表明 Response 是一个包含二进制数据的 Blob 对象。


除了 blob 之外,responseType 还有 arraybufferjsontext等其他枚举字符串值。


总结


一言以蔽之,同源就直接使用 <a> download 下载,跨域就先获取 blob,用 createObjectURLreadAsDataURL 读取链接,再用 <a> download 下载。


参考资料


收起阅读 »

那年毕业前,我花了一整个上午的时间走遍整个校园

web
又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情... 其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片。 那些照片 🔼 这张照片...
继续阅读 »

又逢毕业季,最近看了很多伤感的分别视频,在感叹年轻真好的同时,不禁想起来自己之前在离校前几天也做了一系列的... 所谓的告别的事情...


其中最令我印象深刻的就是拿着ipad,起很早,几乎走遍了整个校园,拍了一些有趣但更有意义的照片


那些照片


木头 2023-06-06 19.29.22.jpeg


🔼 这张照片左边黑白的是一天前拍的,右边是今天拍的,当时毕业典礼刚结束,我们要从体育馆走回计算机学院,人很多,大家边走边笑着,感觉就像... 对,就像是刚入学报到的那一天,那天也像这天一样热闹。今天的路上没有人,天气很好,只有静悄悄的阳光撒在地面上。


木头 2023-06-06 19.35.20.jpeg


🔼 这张照片是宿舍楼旁边的一条小路,路中间是校医院。七天前吃饭的时候路过这里,随手拍了一张照片,今天有风,落了一地小花,还挺好看。


木头 2023-06-06 19.40.15.jpeg


🔼 这是学校操场,28天前正好是在做毕业设计的阶段,在宿舍一坐就是一天,没思路的时候,我就喜欢一个人来看台上坐一会儿,戴着耳机灯歌,看下面跑步的人,等操场上的灯关了,我就回去。当时是一个傍晚,夕阳挺好看的。今天的天气很好,有很多人在跑步。


木头 2023-06-06 19.44.53.jpeg


🔼 老照片拍摄于596天前,快两年了吧... 那时候应该大三,中午刚下课,大家走在南广场上,一块去食堂,那天的云很好看。当时在拍新照片的时候,心里还是有点伤感,明天就要离校了,很多人很可能就再也见不到了...(现在来看,的确是这样)


木头 2023-06-06 19.49.36.jpeg


🔼 上学的时候,每天早晨我都起的很早,六点多就从宿舍出来,其实也不是为了学习或者什么,我就喜欢走在安静的校园里,阳光洒在草丛间,偶尔有鸟叫声,我觉得这种感觉很美好。拍左边图的时候应该是个深秋了吧,树叶落了一地,而右边又正好是一个盛夏,树木生长的正好!这张照片的对比感让我感到无比惊喜!


拍摄心得


其实当时拍的比这些成品照片要多得多,总共拍了20多个地方吧,最终只合成出了78张可以用的,废片率相当高


因为什么呢?


原因是当时的我没有一个准确的参照物,在拍新照片时我一般会历经以下步骤:


1、先拿出手机看看老照片的角度和位置


2、举起ipad,凭感觉走到自己认为准确的位置上


3、拍一张看一下效果


4、满意就再拍两张当备份,不满意就继续重复以上步骤,直到排除满意的


好像一个递归方法!用伪代码实现一下就是:


const takePhoto = () => {
// 1、拿出老照片看角度 + 位置
const { position } = showOldPhoto();

// 2、拿出ipad,走到对应的位置上
walkToPosition(position);

// 3、拍一张看看效果
const { isOK } = takeSomePhoto();

// 4、判断是否满意,满意就结束,不满意就继续递归
!isOk && takePhoto();
}

当然,我也没那么工匠精神,我可能还得再加一个结束条件


const takePhoto = () => {
// 如果拍5次还不满意,就
if(reTryTime > 5) {
return;
}

// others
}

这个过程是比较重复且枯燥的,当然可以适当优化一下比如我可以在ipad上看照片,这样就省掉手机这一步了,另外可以在拍摄时不断地切换照片和相机app,这样就可以稍微快点看到当前位置对不对了...


em... 当时的我真的希望有一个工具能来辅助我拍这些照片!


噢噢噢!


现在的我可以很开心的跟那时候的我说,有了!现在有了!


你可以去微信小程序里搜:历旧弥新


你就可以搜到一个看起来还蛮专业的一个小程序,UI做的也不错,不丑!


它好像提供了一个你非常需要的功能:和旧照片来一场对话


你可以非常轻松的用它来拍一张新旧照片合成的照片,


就像下图:


WechatIMG301.jpeg


你可以将你的旧照片半透明的状态覆盖到相机上(就像左边的图),可以缩放平移,把它放在准确的位置上之后,然后你就可以非常轻易的去拍摄相同角度的照片了!


嗯... 听到这里是不是感觉出来这是一个广告了哈哈哈,没错,那就是了!


打广告!


对,这就是我基于四年前的想法,最近花了几个周末开发的一个小程序,历旧弥新


名字取自 历久弥新 => ,代表一种新旧交替的含义


来看下小程序首页


木头 2023-06-06 21.31.46.jpeg


它一共包含四个功能:


1、与旧照片来一次对话


2、已有关联的照片拼接


3、快速找一个相同的拍照姿势


4、异地也可以来合照


我们也提供了比较好的一些用户拍摄过的照片,放在首页的下半部分:


木头 2023-06-06 21.54.15.jpeg


木头 2023-06-06 21.57.31.jpeg


你可以快速的进行 拍同款 !就可以拍摄类似的照片啦!


当然它或许也存在一些问题希望大家不要吝啬自己的建议,可以评论在下方哈!

作者:木头就是我呀
来源:juejin.cn/post/7242247549511663672

收起阅读 »

Vue KeepAlive 为什么不能缓存 iframe

web
最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下: <router-view v-slot="{ Component ...
继续阅读 »

最近做了个项目,其中有个页面是由 iframe 嵌套了一个另外的页面,在运行的过程中发现 KeepAlive 并不生效,每次切换路由都会触发 iframe 页面的重新渲染,代码如下:


  <router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>

看起来并没有什么问题,并且其他非 iframe 实现的页面都是可以被缓存的,因此可以推断问题出在 iframe 的实现上。


我们先了解下 KeepAlive


KeepAlive (熟悉的可跳过本节)


被 KeepAlive 包裹的组件不是真的卸载,而是从原来的容器搬运到另外一个隐藏容器中,实现“假卸载”, 当被搬运的容器需要再次挂载时,应该把组件从隐藏容器再搬运到原容器,这个过程对应到组件的生命周期就是 activated 和 deactivated


keepAlive 是需要渲染器支持的,在执行 mountComponent 时,如果发现是 __isKeepAlive 组件,那么会在上下文注入 move 方法。


function mountComponent(vnode, container, anchor) {
/**... */
const instance = {
/** ... */
state,
props: shallowReactive(props),
// KeepAlive 实例独有
keepAliveCtx: null
};

const isKeepAlive = vnode.__isKeepAlive;
if (isKeepAlive) {
instance.keepAliveCtx = {
move(vnode, container, anchor) {
insert(vnode.component.subTree.el, container, anchor);
},
createElement
};
}
}

实现一个最基本的 KeepAlive,需要注意几个点



  1. KeepAlive 组件会创建一个隐藏的容器 storageContainer

  2. KeepAlive 组件的实例增加两个方法 _deActive_active

  3. KeepAlive 组件存在一个缓存的 Map,并且缓存的值是 vnode


const KeepAlive = {
// KeepAlive 特有的属性,用来标识
__isKeepAlive: true,
setup() {
/**
* 创建一个缓存对象
* key: vnode.type
* value: vnode
*/

const cache = new Map();
// 当前 keepAlive 组件的实例
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx;
// 创建隐藏容器
const storageContainer = createElement('div');

// 为 KeepAlive 组件的实例增加两个方法
instance._deActive = vnode => {
move(vnode, storageContainer);
};
instance._active = (vnode, container, anchor) => {
move(vnode, container, anchor);
};

return () => {
// keepAlive 的默认插槽就是要被缓存的组件
let rawVNode = slot.default();
// 不是组件类型的直接返回,因为其无法被缓存
if (typeof rawVNode !== 'object') {
return rawVNode;
}

// 挂载时,优先去获取被缓存组件的 vnode
const catchVNode = cache.get(rawVNode.type);
if (catchVNode) {
rawVNode.component = catchVNode.component;
// 避免渲染器重新挂载它
rawVNode.keptAlive = true;
} else {
// 如果没有缓存,就将其加入到缓存,一般是组件第一次挂载
cache.set(rawVNode.type, rawVNode);
}
// 避免渲染器真的把组件卸载,方便特殊处理
rawVNode.shouldKeepAlive = true;
rawVNode.keepAliveInstance = instance;
return rawVNode;
};
}
};

从上可以看到,KeepAlive 组件不会渲染额外的内容,它的 render 函数最终只返回了要被缓存的组件(我们称要被缓存的组件为“内部组件”)。KeepAlive 会对“内部组件”操作,主要是在其 vnode 上添加一些特殊标记,从而使渲染器能够据此执行特殊的逻辑。


function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (type === 'string') {
/** 执行普通的标签 patch */
} else if (type === Text) {
/** 处理文本节点 */
} else if (type === Fragment) {
/** 处理Fragment节点 */
} else if (typeof type === 'object') {
if (!n1) {
if (n2.keptAlive) {
n2.keepAliveInstance._activate(n2, container, anchor);
} else {
mountComponent(n2, container, anchor);
}
} else {
patchComponent(n1, n2, anchor);
}
}
}

function unmount(vnode) {
const { type } = vnode;
if (type === Fragment) {
/**... */
} else if (typeof type === 'object') {
if (vnode.shouldKeepAlive) {
vnode.keepAliveInstance._deActivate(vnode);
} else {
unmount(vnode.component.subTree);
}
return;
}
}

从上面的代码我们可以看出,vue 在渲染 KeepAlive 包裹的组件时,如果有缓存过将执行 keepAliveInstance._activate,在卸载时将执行 keepAliveInstance._deActivate


原因


通过上面的了解,我们知道,KeepAlive 缓存的是 vnode 节点,vnode 上面会有对应的真实DOM。组件“销毁”时,会将真实 DOM 移动到“隐藏容器”中,组件重新“渲染”时会从 vnode 上取到真实 DOM,再重新插入到页面中。这样对普通元素是没有影响的,但是 iframe 很特别,当其插入到页面时会重新加载,这是浏览器特性,与 Vue 无关。


解决方案


思路:路由第一次加载时将 iframe 渲染到页面中,路由切换时通过 v-show 改变显/隐。



  1. 在路由注册时,将 component 赋值为一个空组件


  {
path: "/chathub",
name: "chathub",
component: { render() {} }, // 这里写 null 时控制台会出 warning,提示缺少 render 函数
},


  1. 在 router-view 处,渲染 iframe,通过 v-show 来控制显示隐藏


  <ChatHub v-if="chatHubVisited" v-show="isChatHubPage"></ChatHub>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveList">
<component :is="Component"></component>
</keep-alive>
</router-view>


  1. 监听路由的变化,改变 iframe 的显/隐


const isChatHubPage = ref(false)
// 这里是个优化,想的是只有页面访问过该路由才渲染,没访问过就不渲染该组件
const chatHubVisited = ref(false)

watch(
() => routes.path,
(value) => {
if (value === '/chathub') {
chatHubVisited.value = true
isChatHubPage.value = true
} else {
isChatHubPage.value = false
}
},
{
immediate: true
}
)
作者:莱米
来源:juejin.cn/post/7246310077233659941

收起阅读 »

uni-app实现微信小程序蓝牙打印

web
打印流程 小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备...
继续阅读 »

打印流程


小程序连接蓝牙打印大致可以分为九步:1.初始化蓝牙模块、2.开始搜索附近的蓝牙设备、3.获取搜索到的蓝牙列表、4.监听寻找到新设备的事件、5.连接蓝牙设备、6.关闭搜索蓝牙设备事件、7.获取蓝牙设备的所有服务、8.获取服务的所有特征值、9.向蓝牙设备写入数据


1.初始化蓝牙模块 uni.openBluetoothAdapter


注意:其他蓝牙相关 API 必须在 uni.openBluetoothAdapter 调用之后使用。


uni.openBluetoothAdapter({
success(res) {
console.log(res)
}
})

2.开始搜索附近的蓝牙设备 uni.startBluetoothDevicesDiscovery


此操作比较耗费系统资源,请在搜索并连接到设备后调用 uni.stopBluetoothDevicesDiscovery 方法停止搜索。


uni.startBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

3.获取搜索到的蓝牙列表 uni.getBluetoothDevices


获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备(不是很准确,有时会获取不到)。


uni.getBluetoothDevices({
success(res) {
console.log(res)
}
})

4.监听寻找到新设备的事件 uni.onBluetoothDeviceFound


监听寻找到新设备的事件,跟第三步一起使用,确保能获取附近所有蓝牙设备。


uni.onBluetoothDeviceFound(function (devices) {
console.log(devices)
})

5.连接蓝牙设备 uni.createBLEConnection


若APP在之前已有搜索过某个蓝牙设备,并成功建立连接,可直接传入之前搜索获取的 deviceId 直接尝试连接该设备,避免用户每次都要连接才能打印,省略二三四步减少资源浪费。


uni.createBLEConnection({
deviceId:获取到蓝牙的deviceId,
success(res) {
console.log(res)
}
})

6.关闭搜索蓝牙设备事件 uni.stopBluetoothDevicesDiscovery


停止搜寻附近的蓝牙外围设备。若已经找到需要的蓝牙设备并不需要继续搜索时,建议调用该接口停止蓝牙搜索。


uni.stopBluetoothDevicesDiscovery({
success(res) {
console.log(res)
}
})

7.获取蓝牙设备的所有服务 uni.getBLEDeviceServices


uni.getBLEDeviceServices({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
success(res) {
console.log('device services:', res.services)
}
})

8.获取服务的所有特征值 uni.getBLEDeviceCharacteristics


uni.getBLEDeviceCharacteristics({
// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
success(res) {
console.log('device getBLEDeviceCharacteristics:', res.characteristics)
}
})
三种不同特征值的id
for (var i = 0; i < res.characteristics.length; i++) {
if (!notify) {
notify = res.characteristics[i].properties.notify;
if (notify) readId = res.characteristics[i].uuid;
}
if (!indicate) {
indicate = res.characteristics[i].properties.indicate;
if (indicate) readId = res.characteristics[i].uuid;
}
if (!write) {
write = res.characteristics[i].properties.write;
writeId = res.characteristics[i].uuid;
}
if ((notify || indicate) && write) {
/* 获取蓝牙特征值uuid */
success &&
success({
serviceId,
writeId: writeId,
readId: readId,
});
finished = true;
break;
}

9.向蓝牙设备写入数据 uni.writeBLECharacteristicValue


向低功耗蓝牙设备特征值中写入二进制数据。注意:必须设备的特征值支持 write 才可以成功调用。


并行调用多次会存在写失败的可能性。


APP不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙4.0单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过20字节。


若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)。


安卓平台上,在调用 notifyBLECharacteristicValueChange 成功后立即调用 writeBLECharacteristicValue 接口,在部分机型上会发生 10008 系统错误


// 向蓝牙设备发送一个0x00的16进制数据
const buffer = new ArrayBuffer(1)
const dataView = new DataView(buffer)
dataView.setUint8(0, 0)
uni.writeBLECharacteristicValue({
// 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
deviceId,
// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
serviceId,
// 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
characteristicId,
// 这里的value是ArrayBuffer类型
value: buffer,
success(res) {
console.log('writeBLECharacteristicValue success', res.errMsg)
}
})

写在最后


DEMO地址:gitee.com/zhou_xuhui/… (plus可能会报错,demo中注释掉就好,不影响流程)


打印机CPCL编程参考手册(CPCL 语言):http://www.docin.com/p-2160

作者:我是真的菜呀
来源:juejin.cn/post/7246264754141773885
10502…

收起阅读 »

悟了两星期终于悟了,移动端适配核心思想——没讲懂揍我

web
移动端开发与pc端适配的不同 pc端布局常用方案 所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱) 先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的...
继续阅读 »

移动端开发与pc端适配的不同


pc端布局常用方案


所谓适配,就是指我们的项目开发出来用户在不同虚拟环境、不同硬件环境等等各种情况下有一个稳定、正常的展示(简单理解就是不会排版混乱)


先来回顾一下pc端的项目我们如何写页面做到适配的,大部分页面都是采用版心布局的形式。也就是说所有的内容都写在版心容器盒子里,这个容器盒子设置:margin: 0 auto; & min-width: <版心宽度> & width: <版心宽度>就可以保证:




  • 当用户的屏幕(浏览器)宽度很大,或者缩放浏览器到很小比例时,此时浏览器的宽度大于版心盒子的width,版心容器会自动生成margin-left & margin-right,总会保证版心容器处于页面的正中心。


    这里可以提一嘴pc端浏览器缩放的原理:页面所有元素的css宽高都不会改变,只是css像素的在屏幕上展示的大小缩水了,具体点来说,原本700px * 700px的盒子在浏览器上用10cm * 10cm面积(的物理像素)渲染,但现在用原本<浏览器缩放比率> * 10cm * 10cm面积(的物理像素)渲染。




  • 当用户的屏幕小于版心盒子的width,出现横向滚动条,版心盒子的左右margin为0,width内的内容可滑动滚动条完整查看。




可以参考大淘宝pc端官网就是版心布局的实践。


好了,那么问题来了,移动端为啥不能照搬pc端的这种适配方案呢?


我们有必要先梳理一下移动端对页面进行渲染展示的逻辑:


移动端页面渲染的逻辑


<meta name="viewport">的情况


在html文档里没有<meta name="viewport">标签配置的情况下(通过对比即可理解<meta>标签的意义):


plus:如下整个流程篇口语话主要是梳理核心思路,没有一字一板的细节考究



  1. 我们项目中布局写的所有dom元素的css大小都正常(完全按照css大小的预期)在一个非常大的空间进行渲染,这个空间可能不是无限大,但是为了帮助理解,因为这个空间的大小一般不影响我们项目的正常布局,所以我们可以理解为无限大,这是第一步,即项目页面就像在pc端一样完全按照css写的大小以及布局进行渲染。



  1. 因为我们的移动端设备没有电脑屏幕那么大,所以会把第一步在“很大空间”渲染的页面进行缩小,直至缩小到我们的大页面宽度正好与手机屏幕的宽度一样即可。所以第二步相当于为了让用户把页面看全,手机自动把页面缩小至屏幕内。


为了帮助大家理解,也验证我上面的说法,我写了如下的pc端的版心布局的页面:


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

我把上面的页面部署到osrc(一个国内的免费部署网站,类似于vercel)上了(不可用chrome浏览器的移动端去模拟移动端访问的场景,chrome浏览器只是模拟了屏幕大小,而并没有模拟移动端环境,也就是说根本不会处理<meta>标签,所以这里我们需要部署),大家可以自行用pc端和移动端访问体验(实践一下绝对秒懂我上面的文字),为了照顾没有双端设备的读者,我截一下图(直接喂饭到胃哈哈)


pc端访问:


pc端访问版心布局.png


移动端访问:


移动端访问版心布局.jpg


清晰了吧兄弟们,我们写死的1200px宽的container盒子因为手机本身没这么大,所以缩小之后塞进了手机屏幕中,仔细看手机中的文字,已经被缩小的看不清了。


配置<meta name="viewport">的情况


暂时只给我们的index.html<meta name="viewport">添加一个content="width=device-width, initial-scale=1.0"


<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Document</title>
</head>
<style>
.container {
   width: 1200px;
   min-width: 1200px;
   margin: 0 auto;
   border: 2px solid red;
   background: yellow;
}
.container .content {
   height: 3000px;
}
</style>
<body>
   <div class="container">
       <div class="content">内容</div>
   </div>
</body>
</html>

重新部署后访问查看效果,有meta的页面,部署地址<meta>标签是针对移动端的,所以pc端完全没影响,跟上面一样。现在我们访问移动端效果如下,我没有缩小图片,注意观察页面底部出现滚动条了(纵向滚动条有滚动所以文字没展示,不重要):


设置meta后移动端访问效果.jpg


解释一下content="width=device-width, initial-scale=1.0"的作用。


解读<meta> & dip & 布局视口(自认为最精华,全网少数不带偏新人的解释)


其实相当于我们在content字段中进行了两项配置,第一项是width=device-width,第一个width是指布局视口的宽度,引出概念,何为布局视口?还记得我们上面说的在没有<meta>时的那个非常大的布局空间嘛,就是它!我们让这个空间的宽度等于device-widthdevice-width就是指dip即设备独立像素,第二个概念,何为dip(device independent piexl设备独立像素)呢?听我的,完全不要被网上各种乱七八糟的解释弄迷糊了,什么dpr,什么物理像素,我只能说那些东西与我们开发者并没有直接关系,笔者读了几户所有能搜到的各种移动端入门文章,一言难尽... ,我来给出对于dip的理解,每一个型号的移动设备都具有的一个大小,这个大小是用设备独立像素dip来描述的,所以它仅仅是一个描述大小的单位,但是这个单位究竟是多大呢,换句话说dip有何特殊性呢?


在移动端不缩放的情况下,一个css像素等于一个设备独立像素dip

(chrome浏览器的移动端开发工具里显示的就是dip大小)也就是说,我们让布局视口的宽度等于设备的dip宽度,这里注意:布局视口由原来的”无限大“现在改为一个具体的数值,并不会影响页面的正常布局,页面还是会完整渲染, 只是最后不用缩小放进屏幕了,因为我们缩小的目的就是让布局视口完整的展现在屏幕中。因为屏幕不能展示完整整个页面的布局,所以底部出现滚动条。用户可以滚动访问页面全部内容。


其实这里initial-scale=1.0的作用就是让移动端浏览器不自行缩放,不然的话浏览器会把如上页面再缩小,然后放到手机屏幕里去。


关于<meta name="viewport">的最佳实践


简简单单如下:


<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />

为什么说这是最佳实践,论证它其实还缺少一个关键点,也就是移动端css单位的选取,概括来说,width=device-width配置与移动端css单位的选取两者相辅相成,共同构成了“最佳实践”。


先说一下css单位选取——以vw为例:vw是相对屏幕大小的一个长度单位,1vw等于移动设备屏幕宽度的1%


如何理解“最佳实践”?


首先width=device-width保证了无论何种机型的移动设备,我们开发时写页面的布局视口始终等于屏幕宽度,但看这一点,确实没啥用。如果再来一个条件:页面中所有长度单位用vw来表达。细品!


如何细品?别忘了初心,我们的目标是在不同的移动设备上都能有统一的展示效果,开品:我们用不同dip宽度的设备打开网页,首先布局视口的大小会随着设备dip的不同而不同,也就是始终等于dip宽度:


布局视口宽度 === 设备dip宽度,

并且我们的所有元素大小单位都是vw,也就是说页面中所有元素大小都以屏幕宽度为参照物。最终的效果就是,一个dip宽度超级小的设备打开网页,与一个dip宽度非常大的设备打开网页,看到的页面内容是完全相似的,也就是每个元素在页面中所占的比例不同设备都一样(不同点就在于屏幕本身的大小不一样)!


一般<meta>标签的content中还会设置initial-scale=1.0, maximum-scale=1.0, user-scalable=no,即不让页面进行缩放,感觉这个看需求吧,不让缩小应该是必须的,因为可以想一想,用户缩小完全没有意义呐!(需要大家自己去理解,属于只可意会),至于让不让放大,应该是看情况吧,反正移动端淘宝官网是允许放大的。


移动端适配方案理解


主流的有vw方案、flexible + rem方案,总而言之,把元素的大小用rem来表示或者vw表示,本质都是以手机屏幕宽度为参考,vw比较直接,表达的意思就是1vw等于手机屏幕宽度的百分之一;rem比较间接,通过flexible.js先把得知屏幕宽度是多少px,然后设置<html>font-size,进而所有元素的rem其实还是表达占屏幕宽度的百分之多少。


当然两种方案都有一些技术细节问题需要解决,比如1px问题、安全区域问题等等。这里就不多说了。


相信能一步一步走到这里的同志,对移动端适配绝对有了一个清晰的把握。


2023.6.19,3: 59。

作者:荣达
来源:juejin.cn/post/7246001188448731196
更文不易,点个赞吧!

收起阅读 »

值得学习的JavaScript调试技巧

web
引言 最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各...
继续阅读 »

引言


最近老大在cr我代码时,我就静静坐在他旁边看他装逼。他一顿断点+抓包+各种骚操作给我看楞了,我没忍住就让他手把手教我,他就和我在一个小屋子里xxxx了几个小时,他手法太快了俺看一遍就忘了,于是乎就靠回忆和资料查找整理了哈高频的js调试技巧,希望能帮助到各位。


一:console.dir


在打印dom节点时,普通的console.log是纯文本格式,而dir的打印是以对象的方式。因此在输出dom节点时,务必使用dir打印


<div id="main">
<div class="box1">
<p>p1</p>
</div>

</div>

let oD = document.querySelector('.box1')
console.log(oD)//普通的log输出
console.dir(oD)//dir输出方式

image.png


二:二次发起请求


在调试接口时,通常我们会刷新页面然后观察network的接口信息,如果项目加载时间过长,刷新页面查看接口的效率是十分低的。



  1. 对接口请求右键

  2. 选择Relpy xhr发送请求


image.png


三:接口请求参数修改


借助浏览器控制台可以不用修改代码就可以发送不同参数的新请求了。具体操作如下



  1. 对接口请求右键

  2. 选择copy。

  3. 再选择copy as fetch。

  4. 在console区域粘贴上面的请求信息,然后修改请求体参数。

  5. 然后切换到networkl查看最新请求的结果


效果展示


24.gif


四:css查看伪类hover,active样式


在控制台右侧选择:hov可以选择对应dom各种伪类状态下的css样式,十分的便捷


image.png


五:css样式跳转到对应文件查看


选择css样式,按住alt点击就可以跳到对应文件查看具体代码


25.gif


六:控制台输出选择的dom


首先在页面选择指定的位置dom,然后在在控制台使用$0就表示当前选中的dom了


26.gif


七:展开全部dom


有时候我们在页面查找一个dom时,它嵌套层级特别深。这巨他妈蛋疼一层层展开,这个时候我们就需要找到一键全部展开来帮助我们解决这个问题了。


27.gif


右键选择expand就可以展开选择的dom了。


八:断点调试


断点调试是本节最后一个内容了,它也是最核心的内容了,玩的6的是真的6,老大说我搞懂断点调试和对应的堆栈上下文就可以毕业了。(毕业=辞退?还是。。。)下面我列举的仅仅是入门级别的断点调试,只是说明如何上手操作,里面许多东西还望大家多多探索。


1. 打断点方式


代码中:debugger


在需要断点的地方写入debugger,此时程序运行后代码就会卡在这里,等待主人的安排


let a = 10
debugger
a++

浏览器中:



  1. 选择sources

  2. 在指定代码行左侧单击


image.png


2. 断点间调试


第一种断点调试是十分常用的方式,代码会从当前断点直接运行到下一个断点处执行,中间经过代码都默认被执行且跳过。如下图红色按钮就是断点间调试。


image.png


例子演示


28.gif


我们在上图中打了3个断点,逐个点击,首先从断点15行直接跳到断点17行,最后跳到19行。由于异步最后执行,所以最后又跳到断点15行结束。断点经过的地方鼠标移动到变量上可以查看其内部数据。


3. 逐步调试


逐步调试很明显就是字面意思,从当前断点位置开始一行一行的运行代码,稍微注意的是,遇到函数不进入函数的内部,而是直接执行完函数。


image.png


例子演示


29.gif


4. 进入与进出函数调试


逐步调试遇到函数是不进入函数内部的,因此需要借助进入和进出调试方式控制函数的访问


image.png


例子演示


30.gif


5. 逐步调试详细版


上面讲述了第一种逐步调试方式,其遇到函数是不进入函数内部的,而是直接执行函数。因此下面这种方式是逐步调试的详细版,它也是从断点位置逐步的调试运行,遇到函数也会进入函数的内部进行逐步执行。


image.png


九:React/Vue中尝试


有吊毛说react和vue咋调试?嗯,那个吊毛其实就是我,其实也很简单滴。



  1. 在需要调试的代码位置插入debugger

  2. 在浏览器控制台需要查看变量的地方插入断点

  3. 使用各种调试连招一顿操作就行。


代码例子


例如下面的例子,页面最后显示的num是多少?最后是101,不了解批量setState的开始肯定蒙,我们调试看看


import React,{useEffect, useState} from "react";
const Home = () => {
const [num,setNum] = useState(1)
useEffect(()=>{
debugger
setNum(100)
setTimeout(() => {
setNum(num+100)
}, 0);
},[])
return (
<div>num:{num}</div>
)
}
export default Home;

调试演示
根据调试发现,进入定时器的时候num还未更新,还是1。


31.gif


作者:前端兰博
来源:juejin.cn/post/7246376735838060603
收起阅读 »

前端时钟翻页效果,一看就会,一写就fei

web
最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。 一、元素拆解 从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一...
继续阅读 »

最近阅读了不少实现翻页效果的文章,受益匪浅,在此写个学习笔记。


22.gif


一、元素拆解


动画拆解.png


从侧面来观察这个翻页过程,能看到承载文字内容的主要有三个面板:一个静止在上半部分的面板,显示旧文字的上半部分;一个静止在下半部分的面板,显示新文字的下半部分;第三个是旋转面板,一面显示旧文字的下半部分,另一面显示新文字的上半部分。翻转的动画,我们考虑采用FLIP的思想:



  1. 先实现【动画结束帧】的样式;

  2. 再从【动画开始帧】播放。


二、实现结束帧样式


准备工作:用vue脚手架创建一个模板项目,并添加一个div容器:


image.png


<!-- App.vue -->
<template>
<div id="app">
<test-comp/>
</div>
</template>


<!-- Test.vue -->
<template>
<div class="card"></div>
</template>


<style lang="less" scoped>
.card {
position: relative;
border: solid 4px black;
width: 400px;
height: 400px;
perspective: 1000px;
}
</style>


2.1 实现静止的上半面板


image.png


<template>
<div class="card">
<div class="half-card top-half"></div>
<!-- <div class="half-card bottom-half">财</div> -->
</div>

</template>

<style lang="less" scoped>
/* ... */
.half-card {
position: absolute;
width: 100%;
height: 50%;
overflow: hidden;
background-color: #2c292c;
color: white;
font-size: 320px;
}
.top-half {
line-height: 400px;
}
</style>


我们知道line-height配合font-size可以控制文字在垂直方向的位置,大多数情况下,文字顶部与容器顶部的距离公式为(line-height - font-size) / 2。


记容器高度h,文字大小f,容器只显示文字上半部分的情况下,上述距离的值为h - f / 2,即(line-height - f) / 2 = h - f / 2,所以line-height为2h(400px)。


2.2 实现静止的下半面板


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<div class="half-card bottom-half"></div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.bottom-half {
top: 50%;
line-height: 0;
}
</style>


在容器只显示文字下半部分的情况下,完整的文字顶部距离容器顶部的距离是-f / 2,那么就有(line-height - f) / 2 = - f / 2,即line-height = 0;


2.3 实现旋转面板


2.3.1 旋转面板的正面————新文字的上半部分


image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<div class="half-card front-side"></div>
<!-- <div class="half-card back-side">发</div> -->
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.rotating-half {
position: absolute;
width: 100%;
height: 50%;
.half-card {
height: 100%;
}
}
.front-side {
line-height: 400px;
}
</style>


2.3.2 旋转面板的背面————旧文字的下半部分


怎么让一个div背对我们?只要让它绕着自己的腰部横线翻转180度即可(翻跟斗)。


image.png
image.png


<template>
<div class="card">
<!-- <div class="half-card top-half">发</div> -->
<!-- <div class="half-card bottom-half">财</div> -->
<div class="rotating-half">
<!-- <div class="half-card front-side">财</div> -->
<div class="half-card back-side"></div>
</div>
</div>

</template>

<style lang="less" scoped>
/* ... */
.back-side {
line-height: 0;
transform: rotateX(180deg); // !!!!!!!!!!!
}
</style>


现在,如果把正面也加上,会发现这样一个问题:两个面的位置是重叠的,在模板中后声明的背面元素(即使它是背对着我们)会覆盖正面元素。我们想让这两个面在背对我们的状态下都不显示,这就需要到如下的css属性:backface-visibility: hidden。


此外,现在一个旋转面板中带有两个“面”,我们想要这两个面随着父元素面板的3d旋转一起旋转,也就是保持相对静止,这就需要设置旋转面板【将子元素纳入自己的3d变换空间】:transform-style: preserve-3d。


加上css后,让旋转面板简单地旋转一下,看看效果(效果图有点慢):


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-style: preserve-3d;
.half-card {
/* ... */
backface-visibility: hidden;
}
/* to delete */
transition: transform 1s;
&:hover { transform: rotateX(-180deg); }
}
/* ... */
</style>

2.gif


至此,三个面板静态效果已经完成:


image.png


三、播放动画


在第二节已经得到了动画结束时的状态。接下来需要从动画开始的状态进行播放。


3.1 设置好旋转轴


在目标动画中,旋转面板应该是绕着底边进行旋转的。把【变换原点】设置为底边的中点,这样,经过这个点的X轴就和底边所在的直线重合,绕X轴旋转就等价于绕底边旋转:


<style lang="less" scoped>
/* ... */
.rotating-half {
/* ... */
transform-origin: center bottom;
}
/* ... */
</style>

3.2 找到动画开始帧,使用animate播放动画


动画开始时,旋转面板在主面板的下半区域。要从上半区域(无变换状态)到达下半区域,需要绕着底边逆时针旋转180度,因此开始帧所处于的变换状态就是rotateX(-180deg),从而得到动画的关键帧:


【transform: rotateX(-180deg)】->【transform: none】。


我们给旋转面板加上ref,然后在组件挂载完毕时播放即可:


<script>
export default{
mounted() {
this.$refs.rotate?.animate?.(
[
{ offset: 0, transform: 'rotateX(-180deg)' },
// { offset: 1, transform: 'none' },
],
{
duration: 1000,
easing: 'ease-in-out',
},
);
},
};
</script>

2.gif


四、应用


这样的UI组件可能会用于记录时间、比赛分数变化啥的,自然是不能把值写死。考虑如下的应用场景:


<!-- App.vue -->
<template>
<div id="app" class="flex-row">
<test-comp :value="scoreLGD"/>
<h1>VS</h1>
<test-comp :value="scoreLiquid"/>
</div>
</template>


<script>
import TestComp from './Test';
export default {
components: { TestComp },
data() { return {
scoreLGD : 15,
scoreLiquid: 13,
};
},
mounted() {
setInterval(() => {
this.scoreLGD = this.randomInt(99);
this.scoreLiquid = this.randomInt(99);
}, 5000);
},
/* ... */
};

在该场景下,翻页组件需要在更新时而不是挂载时执行动画(因为没有上一个值)。因此我们在组件内部维护一个记录上一个值的状态,然后把动画从挂载阶段移动到更新阶段:


<template>
<div class="card">
<!-- 旧文字上 -->
<div
v-if="staleValue !== undefined"
class="half-card top-half">

{{ staleValue }}
</div>
<!-- 新文字下 -->
<div class="half-card bottom-half">{{ value }}</div>
<!-- 旋转面板 -->
<div ref="rotate" class="rotating-half">
<!-- 新文字上 -->
<div class="half-card front-side">{{ value }}</div>
<!-- 旧文字下 -->
<div
v-if="staleValue !== undefined"
class="half-card back-side">

{{ staleValue }}
</div>
</div>
</div>

</template>

<script>
export default {
props: ['value'],
data() { return { staleValue: undefined }; },
watch: {
value(_, old) { this.staleValue = old; },
},
updated() {
this.$refs.rotate?.animate?.(
[{ offset: 0, transform: 'rotateX(-180deg)' }],
{ duration: 1000, easing: 'ease-in-out' },
);
},
};
</script>


基本完成:


22.gif


总结一下


实现翻页效果 = 实现两块静态面板 + 实现一块双面旋转面板 + 播放旋转动画。


这里用vue写了demo, react应该也差不多,将updated换成layoutEffect等等。


另外,动画也可以用类名加css实现,当元素不在视口可以不播放,一些样式可以改成props配置。总之应该有不少地方还可以迭代优化下。


参考文章如下,分析思路基本一致,代码实现上有差异:

【1】优雅的时钟翻页效果,让你的网页时钟与众不同!

【2】原生JS实现

一个翻页时钟

收起阅读 »

用js脚本下载某书的所有文章

web
前言 在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。 想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是...
继续阅读 »

前言


在某书上的写了好几年的文章,发现某书越来越烂了,全是广告,各种擦边标题党文章和小说等,已经不适合技术人员了。


想把某书上的文章全部下载下来整理一下,某书上是有一个下载所有文章功能的,用了以后发现下载功能现在有问题,无法下载个人账号里所有文章,不知道是不是下载功能根据日期什么判断了,还是bug了,试了好几次都这样,官方渠道只能放弃了。


手动一篇一篇粘贴的成本太高了,不仅有发布的文章,还有各种没有发布的笔记在里面,各种文章笔记加起来好几百篇呢,既然是工程师,就用工程师思维解决实际问题,能用脚本下载个人账号的下的所有文章吗?


思考.gif


思路梳理


由于是下载个人账号下的所有文章,包含发布的和未发布的,来看下个人账号的文章管理后台


文集模式.png


根据操作以及分析浏览器控制台 网络 请求得知,文章管理后台逻辑是这样的,默认查询所有文集(文章分类列表), 默认选中第一个文集,查询第一个文集下的所有文章,默认展示第一篇文章的内容,点击文集,获取当前文集下的所有文章,默认展示文集中的第一篇文章,点击文章获取当前文章数据,来分析一下相关的接口请求


获取所有文集


https://www.jianshu.com/author/notebooks 这个 Get 请求是获取所有 文集,用户信息是放在 cookie


分析请求模式.png


来看下返回结果


[
    {
        "id": 51802858,
        "name": "思考,工具,痛点",
        "seq": -4
    },
    {
        "id": 51783763,
        "name": "安全",
        "seq": -3
    },
    {
        "id": 51634011,
        "name": "数据结构",
        "seq": -2
    },
    ...
]

接口返回内容很简单,一个 json 数据,分别是:id、文集名称、排序字段。


获取文集中的所有文章


https://www.jianshu.com/author/notebooks/51802858/notes 这个 Get 请求是根据 文集id 获取所有文章,51802858"思考,工具,痛点" 文集的id, 返回数据如下


[
    {
        "id": 103888430, // 文章id
        "slug": "984db49de2c0",
        "shared": false,
        "notebook_id": 51802858, // 文集id
        "seq_in_nb": -4,
        "note_type": 2,
        "autosave_control": 0,
        "title": "2022-07-18", // 文章名称
        "content_updated_at": 1658111410,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    {
        "id": 98082442,
        "slug": "6595bc249952",
        "shared": false,
        "notebook_id": 51802858,
        "seq_in_nb": -3,
        "note_type": 2,
        "autosave_control": 3,
        "title": "架构图",
        "content_updated_at": 1644215292,
        "last_compiled_at": 0,
        "paid": false,
        "in_book": false,
        "is_top": false,
        "reprintable": true,
        "schedule_publish_at": null
    },
    ...
]

接口返回的 json 数据里包含 文章id文集名称,这是接下来需要的字段,其他字段暂时忽略。


获取文章内容


https://www.jianshu.com/author/notes/98082442/content 这个 Get 请求是根据 文章id 获取文章 Markdown 格式内容, 98082442《架构图》 文章的id, 接口返回为 Markdown 格式的字符串


{"content":"![微服务架构图 (3).jpg](https://upload-images.jianshu.io/upload_images/6264414-fa0a7893516725ff.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)\n"}

现在,我们了解清楚了文集,文章,以及文档内容的获取方式,接下来开始脚本实现。


代码实现


由于我是前端攻城狮,优先考虑使用 js 来实现下载,还有一个考虑因素是,接口请求里面的用户信息是通过读取 cookie 来实现的,js 脚本在浏览器的控制台执行发起请求时,会自动读取 cookie,很方便。


如果要下载个人账号下所有文章的话,根据梳理出来的思路编写代码就行


获取所有文集id


fetch("https://www.jianshu.com/author/notebooks")
  .then((res) => res.json())
  .then((data) => {
    // 输出所有文集
    console.log(data);
  })

使用fetch函数进行请求,得到返回结果,上面的代码直接在浏览器控制台执行即可,控制台输出效果如下


输出所有文集.png


根据文集数据获取所有文章


上一步得到了所有文集,使用 forEach 循环所有文集,再根据 文集id 获取对应文集下的所有文章,依然使用 fetch 进行请求


...
let wenjiArr = [];
wenjiArr = data; // 文集json数据
let articleLength = 0;
wenjiArr.forEach((item, index) => {
  // 根据文集获取文章
  fetch(`https://www.jianshu.com/author/notebooks/${item.id}/notes`)
    .then((res2) => res2.json())
    .then((data2) => {
      console.log("输出文集下的所有文章:", data2);
    });
});

根据文章id获取文章内容,并下载 Markdown 文件


有了文章 id, 根据 id 获取内容,得到的内容是一个对象,对象中的 content 属性是文章的 Markdown 字符串,使用 Blob 对象和 a 标签,通过 click() 事件实现下载。


在这里的代码中使用 articleLength 变量记录了一下文章数量,使用循环中的文集名称和文章名称拼成 Markdown 文件名 item.name - 《item2.title》.md


...
console.log(item.name + " 文集中的文章数量: " + data2.length);
articleLength = articleLength + data2.length;
console.log("articleLength: ", articleLength);
data2.forEach(async (item2, i) => {
// 根据文章id获取Markdown内容
fetch(`https://www.jianshu.com/author/notes/${item2.id}/content`)
.then((res3) => res3.json())
.then((data3) => {
console.log(data3);
const blob = new Blob([data.content], {
type: "text/markdown",
});
const link = document.createElement("a");
link.href = window.URL.createObjectURL(blob);
link.download = item.name + " - 《" + item2.title + `》.md`;
link.click();
});
});

代码基本完成,运行


在浏览器控制台中运行写好的代码,浏览器下方的下载提示嗖嗖的显示,由于没有做任何处理,当前脚本执行过程中报错了,文章下载了几十个以后就停止了,提示 429



HTTP 请求码 429 表示客户端发送了太多的请求,服务器无法处理。这种错误通常表示服务器被攻击或过载。



文章内容太多了,意料之中的情况,需要改进代码


思路改进分析


根据问题分析,脚本里的代码是循环套循环发请求的,这部分改造一下试试效果。


把每个循环里面发送 fetch 请求的外面是加个 setTimeout, 第一个循环里面的 setTimeout 延迟参数设置为 1000 * indexindex 为当前循环的索引,第一个请求0秒后执行,后面每一次都加1秒后执行,由于文集的数量不多,大约20个,这一步这样实现是没问题的。


重点是第二个循环,根据文集获取所有文章,每个文集里多的文章超过50篇,少的可能2,3篇,这里面的 setTimeout 延迟参数这样设置 2000 * (i + index)i 为第二个循环的索引,这样保证在后面的请求中避免了某个时间段发送大量请求,导致丢包的问题。


再次执行代码,对比控制台输出的文章数量和下载目录中的文章(项目)数量,如果一致,说明文章都下载好了


390.png


下载.png


改造后的完整代码地址


github.com/gywgithub/F…


思考


整体来看,文章下载脚本的逻辑并不复杂,接口参数也简单明确,两个 forEach 循环,三个 fetch 请求,把获取到的文章内容实用 a 标签下载下来就行了。关于大量请求发送导致 429 或者请求丢失的问题,脚本中使用了一种方案,当时还想到了另外两种方案:


请求同步执行


通过同步的方式先得到所有文集下的所有文章,再根据文章列表数组同步发请求下载,请求一个个发,文章一篇篇下载


Promise.all


使用 Promise.all() 分批发送请求,避免一次请求发送太多



也可能还有其他的解决方案,欢迎大家评论区讨论交流,一起学习共同进步 ^-^



作者:草帽lufei
来源:juejin.cn/post/7245184987531018300

收起阅读 »