注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

为什么别人的 hooks 里面有那么多的 ref

前言 最近因为一些原因对useCallback有了一些新的认知,然后遇到一个朋友说他面试的时候被问到如何实现一个倒计时的hooks的时候,想到了这个问题,突然的茅塞顿开,对react 中的hooks有了新的了解,所以有了这篇文章,记录一下我个人的想法。 在...
继续阅读 »

前言



最近因为一些原因对useCallback有了一些新的认知,然后遇到一个朋友说他面试的时候被问到如何实现一个倒计时的hooks的时候,想到了这个问题,突然的茅塞顿开,对react 中的hooks有了新的了解,所以有了这篇文章,记录一下我个人的想法。



在学习一些开源的库的时候,很容易发现开源库中 hooks 里面会写很多的 ref 来存储hooks的参数。


使用了 ref 之后,使用变量的地方就需要 .current 才能拿到变量的值,这比我直接使用变量肯定是变得麻烦了。对于有代码洁癖的人来说,这肯定是很别扭的。


但是在开源库的 hooks 中频繁的使用了 ref,这肯定不是一个毫无原因的点,那么究竟是什么原因,让开源库也不得不使用 .current 去获取变量呢?


useCallback


先跑个题,什么时候我们需要使用 useCallback 呢?


每个人肯定有每个人心中的答案,我来讲讲我的心路历程吧。


第一阶段-这是个啥


刚开始学react的时候,写函数式组件,我们定义函数的时候,肯定是不会有意识的把这个函数使用 useCallback 包裹。就这样写了一段时间的代码。


突然有一天我们遇到了一个问题,useEffect无限调用,找了半天原因,原来是因为我们还不是很清楚useEffect依赖的概念,把使用到的所有的变量一股脑的塞到了依赖数组里面,碰巧,我们这次的依赖数组里面有一个函数,在react每一次渲染的时候,函数都被重新创建了,导致我们的依赖每一次都是新的,然后就触发了无限调用。


百度了一圈,原来使用 useCallback 缓存一下这个函数就可以了,这样useEffect中的依赖就不会每一次都是一个新值了。


小总结: 在这个阶段,我们第一次使用 useCallback ,了解到了它可以缓存一个函数。


第二阶段-可以缓存


可以缓存就遇到了两个点:

  1. 缓存是吧,不会每一次都重新创建是吧,这样是不是性能就能提高了!那我把我所有用到的函数都使用 useCallback缓存一下。
  2. react 每一次render的时候会导致子组件重新渲染,使用memo可以缓存这个子组件,在父组件更新的时候,会浅层的比较子组件的props,所以传给子组件的函数就需要使用缓存useCallback起来,那么父组件中定义函数的时候图方便,一股脑的都使用 useCallback缓存。

小总结: 在这里我们错误的认为了缓存就能够帮助我们做一些性能优化的事情,但是因为还不清楚根本的原因,导致我们很容易就滥用 useCallback


第三阶段-缓存不一定是好事


在这个阶段,写react也有一段时间了,我们了解到处处缓存其实还不如不缓存,因为缓存的开销不一定就比每一次重新创建函数的开销要小。


在这里肯定也是看了很多介绍 useCallback的文章了,推荐一下下面的文章


how-to-use-memo-use-callback,这个是全英文的,掘金有翻译这篇文章的,「好文翻译」


小总结: 到这里我们就大概的意识到了,处处使用useCallback可能并不是我们想象的那样,对正确的使用useCallback有了一定的了解


总结


那么究竟在何时应该使用useCallback呢?

  1. 我们知道 react 在父组件更新的时候,会对子组件进行全量的更新,我们可以使用 memo对子组件进行缓存,在更新的时候浅层的比较一下props,如果props没有变化,就不会更新子组件,那如果props中有函数,我们就需要使用 useCallback缓存一下这个父组件传给子组件的函数。
  2. 我们的useEffect中可能会有依赖函数的场景,这个时候就需要使用useCallback缓存一下函数,避免useEffect的无限调用

是不是就这两点呢?那肯定不是呀,不然就和我这篇文章的标题联系不起来了吗。


针对useEffect这个hooks补充一点react官方文档里面有提到,建议我们使用自定义的 hooks 封装 useEffect

  • 那使用useCallback的第三个场景就出现了,就是我们在自定义hooks需要返回函数的时候,建议使用 useCallback缓存一下,因为我们不知道用户拿我们返回的函数去干什么,万一他给加到他的useEffect的依赖里面不就出问题了嘛。

一个自定义hook的案例



实现一个倒计时 hooks



需求介绍


我们先简单的实现一个倒计时的功能,就模仿我们常见的发短息验证码的功能。页面效果




app.jsx




MessageBtn.jsx



 功能比较简单,按钮点击的时候创建了一个定时器,然后时间到了就清除这个定时器。


现在把 MessageBtn 中倒计时的逻辑写到一个自定义的hooks里面。


useCountdown


把上面的一些逻辑抽取一下,useCountdown主要接受一个倒计时的时长,返回当前时间的状态,以及一个开始倒计时的函数




这里的start函数用了useCallback,因为我们不能保证用户的使用场景会不会出问题,所以我们包一下


升级 useCountdown


现在我们期望useCountdown支持两个函数,一个是在倒计时的时候调用,一个是在倒计时结束的时候调用


预期的使用是这样的,通过一个配置对象传入 countdownCallBack函数和onEnd




改造 useCountdown

  • 然后我们这里count定义 0 有点歧义,0 不能准确的知道是一开始的 0 还是倒计时结束的 0,所以还需要加一个标志位来表示当前是结束的 0
    1. 使用 useEffect监听count的变化,变化的时候触发对应的方法

    实现如下, 新增了红框的内容




    提出问题


    那么,现在就有一个很严重的问题,onEndcountdownCallBack这两个函数是外部传入的,我们要不要把他放到我们自定义hookuseEffect依赖项里面呢


    我们不能保证外部传入的变量一定是一个被useCallback包裹的函数,那么肯定就不能放到useEffect依赖项里面。


    如何解决这个问题呢?


    答案就是使用useRef。(兜兜转转这么久才点题 (╥╯^╰╥))


    用之前我们可以看一下成熟的方案是什么


    比如ahooks里面的useLatestuseMemoizedFn的实现

    • useLatest 源码


    • useMemoizedFn 源码,主要看圈起来的地方就好了,本质也是用useRef记录传入的内容



    ok,我们使用一下 useLatest 改造一下我们的useCountdown,变动点被红框圈起来了




    总结


    其实这篇文章的核心点有两个

    1. 带着大家重新的学习了一下useCallback的使用场景。(useMemo类似)
    2. 编写自定义hooks时候,我们需要注意一下外部传入的参数,以及我们返回给用户的返回值,核心点是决不相信外部传入的内容,以及绝对要给用户一个可靠的返回值。

    作者:既见君子
    链接:https://juejin.cn/post/7271643757640007680
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    移动端的双击事件好不好用?

    web
    前言 2023年了,我不允许还有人不会自己实现移动端的双击事件。 过来,看这里,不足 50 行的代码实现的双击事件。 听笔者娓娓道来。 dblclick js原生有个dblclick双击事件,但是几乎不支持移动端。 而且,该dblclick事件在pc端鼠标双...
    继续阅读 »

    前言


    2023年了,我不允许还有人不会自己实现移动端的双击事件。


    过来,看这里,不足 50 行的代码实现的双击事件。


    听笔者娓娓道来。


    dblclick


    js原生有个dblclick双击事件,但是几乎不支持移动端。


    developer.mozilla.org_zh-CN_docs_Web_API_Element_dblclick_event.png


    而且,该dblclick事件在pc端鼠标双击时,会触发两次click与一次dblclick


    window.addEventListener('click', () => {
    console.log('click')
    });
    window.addEventListener('dblclick', () => {
    console.log('dblclick')
    });

    // 双击页面,打印:click✖️2 dblclick

    我们期望可以在移动端也能有双击事件,并且隔离单击与双击事件,双击时只触发双击事件,只执行双击回调函数,让注册双击事件像注册原生事件一样简单。


    点击穿透


    简单聊聊移动端的点击穿透。



    在移动端单击会依次触发touchstart->touchmove->touchend->click事件。



    有这样一段逻辑,在touchstart时出现全屏弹框,在click弹框时关闭弹框。实际上,在点击页面时,弹框会一闪而过,并没有出现正确的交互。在移动端单击时touchstart早于click,当弹框出现了,后来的click事件就落在了弹框上,导致弹框被关闭。这就是点击穿透的一种表现。


    笔者的业务需求是双击元素,出现全屏弹框,单击弹框时关闭弹框。因此基于这样的业务需求与现实的点击穿透问题,笔者选择采用click事件来模拟双击事件,并且适配pc端使用。大家也可以选择解决点击穿透问题,并采用touchstart模拟双击事件,可以更快地响应用户操作。



    采用touchstart模拟时,可以再考虑排除双指点击的情况。


    在实现上与下文代码除了事件对象获取位置属性有所不同外,其它代码基本一致,实现思路无差别。



    模拟双击事件


    采用click事件来模拟实现双击。


    双击事件定义:2次点击事件间隔小于200ms,并且点击范围小于10px的视为双击。这里的双击事件是自定义事件,为了区分原生的 dblclick,又优先满足移动端使用,则事件名定义为 dbltouch,后续可以使用window.addEventListener('dbltouch', ()=>{})来监听双击事件。



    这个间隔与位移限制大家可以根据自己的业务需求调整。通常采用的是300ms的间隔与10px的位移,笔者业务中发现200ms间隔也可使用。


    自定义事件名大家可以随意设置,满足语义化即可。





    1. 监听click事件,并在捕获阶段监听,目的是为了后续能够阻止click事件传播。


      window.addEventListener('click', handler, true);



    2. 监听函数中,第1次点击时,记录点击位置,并设置200ms倒计时。如果第2次点击在200ms后,则重新派发当前事件,让事件继续传播,使其它的监听函数可以继续处理对应事件。


      // 标识是否在等待第2次点击
      let isWaiting = false;

      // 记录点击位置
      let prevPosition = {};

      function handler(evt) {
      const { pageX, pageY } = evt;
      prevPostion = { pageX, pageY };
      // 阻止冒泡,不让事件继续传播
      evt.stopPropagation();
      // 开始等待第2次点击
      isWaiting = true;
      // 设置200ms倒计时,200ms后重新派发当前事件
      timer = setTimeout(() => {
      isWaiting = false;
      evt.target.dispatchEvent(evt);
      }, 200)
      }

      注意: 倒计时结束时evt.target.dispatchEvent(evt)派发的事件仍是原来的事件对象,即仍是click事件,会触发继续handler函数,进入了循环。


      这里需要破局,已知Event事件对象下有一个 isTrusted 属性,是一个只读属性,是一个布尔值。当事件是由用户行为生成的时候,这个属性的值为 true ,而当事件是由脚本创建、修改、通过 EventTarget.dispatchEvent()派发的时候,这个属性的值为 false 。


      因此,此处脚本派发的事件是希望继续传递的事件,不用handler内处理。


      function handler(evt) {
      // 如果事件是脚本派发的则不处理,将该事件继续传播
      if(!evt.isTrusted){
      return;
      }
      }



    3. 处理完第1次点击后,接着处理在200ms内的第2次点击事件。如果满足位移小于10px的条件,则视为双击。


      // 标识是否在等待第2次点击
      let isWaiting = false;

      // 记录点击位置
      const prevPosition = {};

      function handler(evt) {
      // 如果事件是脚本派发的则不处理,将该事件继续传播
      if(!evt.isTrusted){
      return;
      }
      const { pageX, pageY } = evt;
      if(isWaiting) {
      isWaiting = false;
      const diffX = Math.abs(pageX - prevPosition.pageX);
      const diffY = Math.abs(pageY - prevPosition.pageY);
      // 如果满足位移小于10,则是双击
      if(diffX <= 10 && diffY <= 10) {
      // 取消当前事件传递,并派发1个自定义双击事件
      evt.stopPropagation();
      evt.target.dispatchEvent(
      new PointerEvent('dbltouch', {
      cancelable: false,
      bubbles: true,
      })
      )
      }
      } else {
      prevPostion = { pageX, pageY };
      // 阻止冒泡,不让事件继续传播
      evt.stopPropagation();
      // 开始等待第2次点击
      isWaiting = true;
      // 设置200ms倒计时,200ms后重新派发当前事件
      timer = setTimeout(() => {
      isWaiting = false;
      evt.target.dispatchEvent(evt);
      }, 200)
      }
      }



    4. 以上便实现了双击事件,全局任意地方监听双击。


      window.addEventListener('dbltouch', () => {
      console.log('dbltouch');
      })
      window.addEventListener('click', () => {
      console.log('click');
      })
      // 使用鼠标、手指双击
      // 打印出 dbltouch
      // 而且不会打印有click



    笔者要在这里说句 但是: 由于200ms的延时,虽不多,但是对于操作迅速的用户来讲,还是会有不好的体验。


    优化双击事件


    由于是在window上注册的click函数,虽说注册双击事件像单击事件一样简单了,但却也导致整个产品页面的click事件都会推迟200ms执行。


    因此,我们应该只对需要处理双击的地方添加双击事件,至少只在局部发生延迟情况。稍微调整下代码,将需要注册双击事件的元素由开发决定,通过参数传递。而且事件处理函数也可以通过参数传递,即可以通过监听双击事件,也可以通过回调函数执行。


    以下是完整的代码。


    class RegisterDbltouchEvent {
    constructor(el, fn) {
    this.el = el || window;
    this.callback = fn;
    this.timer = null;
    this.prevPosition = {};
    this.isWaiting = false;

    // 注册click事件,注意this指向
    this.el.addEventListener('click', this.handleClick.bind(this), true);
    }
    handleClick(evt){
    if(this.timer) {
    clearTimeout(this.timer);
    this.timer = null;
    }
    if(!evt.isTrusted) {
    return;
    };
    if(this.isWaiting){
    this.isWaiting = false;
    const diffX = Math.abs(pageX - this.prevPosition.pageX);
    const diffY = Math.abs(pageY - this.prevPosition.pageY);
    // 如果满足位移小于10,则是双击
    if(diffX <= 10 && diffY <= 10) {
    // 取消当前事件传递,并派发1个自定义双击事件
    evt.stopPropagation();
    evt.target.dispatchEvent(
    new PointerEvent('dbltouch', {
    cancelable: false,
    bubbles: true,
    })
    );
    // 也可以采用回调函数的方式
    this.callback && this.callback(evt);
    }
    } else {
    this.prevPostion = { pageX, pageY };
    // 阻止冒泡,不让事件继续传播
    evt.stopPropagation();
    // 开始等待第2次点击
    this.isWaiting = true;
    // 设置200ms倒计时,200ms后重新派发当前事件
    this.timer = setTimeout(() => {
    this.isWaiting = false;
    evt.target.dispatchEvent(evt);
    }, 200)
    }
    }
    }

    只为需要实现双击逻辑的元素注册双击事件。可以通过传递回调函数的方式执行业务逻辑,也可以通过监听dbltouch事件的方式,也可以同时使用,it's up to you.


    const el = document.querySelector('#dbltouch');
    new RegisterDbltouchEvent(el, (evt) => {
    // 实现双击逻辑
    })

    最后


    采用的click事件模拟双击事件,因此在移动端和pc端都可以使用该构造函数。


    作者:Yue栎廷
    来源:juejin.cn/post/7274043371731796003
    收起阅读 »

    为什么我的页面鼠标一滑过,布局就错乱了?

    web
    前言 这天刚到公司,测试同事又在群里@我: 为什么页面鼠标一滑过,布局就错乱了? 以前是正常的啊? 刷新后也是一样 快看看怎么回事 同时还给发了一段bug复现视频,我本地跑个例子模拟下 可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。...
    继续阅读 »

    前言


    这天刚到公司,测试同事又在群里@我:

    为什么页面鼠标一滑过,布局就错乱了?

    以前是正常的啊?

    刷新后也是一样

    快看看怎么回事


    同时还给发了一段bug复现视频,我本地跑个例子模拟下


    GIF 2023-8-28 11-23-25.gif


    可以看到,鼠标没滑过是正常的,鼠标一滑过图片就换行了,鼠标移出又正常了。


    正文


    首先说下我们的产品逻辑,我们产品的需求是:要滚动的页面,滚动条不应该占据空间,而是悬浮在滚动页面上面,而且滚动条只在滚动的时候展示。


    我们的代码是这样写:


      <style>
    .box {
    width: 630px;
    display: flex;
    flex-wrap: wrap;
    overflow: hidden; /* 注意⚠️ */
    height: 50vh;
    box-shadow: 0 0 5px rgba(93, 96, 93, 0.5);
    }
    .box:hover {
    overflow: overlay; /* 注意⚠️ */
    }
    .box .item {
    width: 200px;
    height: 200px;
    margin-right: 10px;
    margin-bottom: 10px;
    }
    img {
    width: 100%;
    height: 100%;
    }
    </style>
    <div class="box">
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    <div class="item">
    <img src="https://p6.itc.cn/q_70/images03/20210422/229df67605934f719b5e2efe6f171d5f.jpeg" alt="">
    </div>
    </div>

    我们使用了overflow属性的overlay属性,滚动条不会占据空间,而是悬浮在页面之上,刚好吻合了产品的需求。


    image.png


    然后我们在滚动的区域默认是overflow:hidden,正常时候不会产生滚动条,hover的时候把overflow改成overlay,滚动区域可以滚动,并且不占据空间。


    简写代码如下:


      .box {
    overflow: hidden;
    }
    .box:hover {
    overflow: overlay;
    }

    然后我们是把它封装成sass的mixins,滚动区域使用这个mixins即可。


    上线后没什么问题,符合预期,获得产品们的一致好评。


    直接这次bug的出现。


    排查


    我先看看我本地是不是正常的,我打开一看,咦,我的布局不会错乱。


    然后我看了我的chrome的版本,是113版本


    然后我问了测试的chrome版本,她是114版本


    然后我把我的chrome升级到最新114版本,咦,滑过页面之后我的布局也乱了。


    初步判断,那就有可能是chrome版本的问题。


    去网上看看chrome的升级日志,看看有没有什么信息。


    image.png


    具体说明:


    image.png


    可以看到chrome114有了一个升级,就是把overflow:overlay当作overflow:auto的别名,也就是说,overlay的表现形式和auto是一致的。


    实锤了,auto是占据空间的,所以导致鼠标一滑过,布局就乱了。


    其实我们从mdn上也能看到,它旁边有个删除按钮,然后滑过有个tips:


    image.png


    解决方案


    第一种方式


    既然overflow:overlay表现形式和auto一致,那么我们得事先预留出滚动条的位置,然后设置滚动条的颜色是透明的,滑过才显示颜色,基本上也能符合产品的需求。


    代码如下:


      // 滚动条
    ::-webkit-scrollbar {
    background: transparent;
    width: 6px;
    height: 6px;
    }
    // 滚动条上的块
    ::-webkit-scrollbar-thumb {
    background-clip: padding-box;
    background-color: #d6d6d6;
    border: 1px solid transparent;
    border-radius: 10px;
    }
    .box {
    overflow: auto;
    }
    .box::-webkit-scrollbar-thumb {
    background-color: transparent;
    }
    .box:hover::-webkit-scrollbar-thumb {
    background-color: #d6d6d6;
    }

    第二种方式


    如果有用element-ui,它内部封装了el-scrollbar组件,模拟原生的滚动条,这个也不占据空间,而是悬浮,并且默认不显示,滑过才显示。



    element-ui没有el-scrollbar组件的文档说明,element-plus才有,不过都可以用。



    总结


    这次算是踩了一个坑,当mdn上面提示有些属性已经要废弃⚠️了,我们就要小心了,尽量不要使用这些属性,就算当前浏览器还是支持的,但是不能保证未来某个时候浏览器会不会废弃这个属性。(比如这次问题,改这些问题挺费时间的,因为用的地方挺多的。)


    因为一旦这些属性被废弃了,它预留的bug就等着你去解决,谨记!


    作者:答案cp3
    来源:juejin.cn/post/7273875079658209319
    收起阅读 »

    JS 获取页面尺寸

    web
    通过 JS 获取页面相关的尺寸是比较常见的操作,尤其是在动态计算页面布局时,今天我们就来学习一下几个获取页面尺寸的基本方法。 获取页面高度 function getPageHeight() { var g = document, a = g.bod...
    继续阅读 »

    通过 JS 获取页面相关的尺寸是比较常见的操作,尤其是在动态计算页面布局时,今天我们就来学习一下几个获取页面尺寸的基本方法。


    获取页面高度


    function getPageHeight() {
    var g = document,
    a = g.body,
    f = g.documentElement,
    d = g.compatMode == "BackCompat" ? a : g.documentElement;
    return Math.max(f.scrollHeight, a.scrollHeight, d.clientHeight);
    }

    获取页面scrollLeft


    function getPageScrollLeft() {
    var a = document;
    return a.documentElement.scrollLeft || a.body.scrollLeft;
    }

    获取页面scrollTop


    function getPageScrollTop() {
    var a = document;
    return a.documentElement.scrollTop || a.body.scrollTop;
    }

    获取页面可视宽度


    function getPageViewWidth() {
    var d = document,
    a = d.compatMode == "BackCompat" ? d.body : d.documentElement;
    return a.clientWidth;
    }

    获取页面可视高度


    function getPageViewHeight() {
    var d = document,
    a = d.compatMode == "BackCompat" ? d.body : d.documentElement;
    return a.clientHeight;
    }

    获取页面宽度


    function getPageWidth() {
    var g = document,
    a = g.body,
    f = g.documentElement,
    d = g.compatMode == "BackCompat" ? a : g.documentElement;
    return Math.max(f.scrollWidth, a.scrollWidth, d.clientWidth);
    }

    ~


    ~ 全文完


    ~


    作者:编程三昧
    来源:juejin.cn/post/7274856158175363126
    收起阅读 »

    一个有意思的点子

    web
    前言 前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔 背景 部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。 首先要确定...
    继续阅读 »

    前言


    前端基于运行时检测问题、定位原因、预警提示的可行性思考🤔



    背景


    部门要治理线上故障过多的问题,故障主要分后端异常以及前端的性能问题和业务问题。我们讨论的范围是前端业务故障,经过分析可以把它们大致分为UI、业务、工程技术、日志、三方依赖等。



    首先要确定降低故障的指标,MTTI和MTTD是关键,因为只有及时发现和定位问题才能快速消灭它们。我们暂时没有统计线上MTTI、MTTD的数据,因为缺乏相关的预警所以问题的发现和定位耗时通常很久,下面的一些复盘统计显示发现问题的时间可以持续甚至好几个月。



    这些问题中UI和业务异常占比超过80%,这些问题发现的不及时,一方面是问题本身不会阻碍核心链路,另一方面说明团队对业务的稳定缺少监控手段。
    现有开发流程已经包含了Design QA验收交付的UI是否符合预期;开发工程师会和QA工程师一起执行Test Case验证业务的稳定并且在CI环节还有UT的保障。既然如此那为什么线上还是会不可避免的出现故障呢?


    问题归因


    在Dev和Stage阶段的验收能发现和处理绝显而易见的异常,但是这些验收的场景是有限的



    1. 开发环境数据集的局限

    2. 考虑到AB因素的影响,很难做到全场景全业务覆盖。

    3. 开发过程可能会无意间影响到其他业务,但是我们的注意力集中在现有的业务,也就导致问题难以被发现


    所以归根到底,验收环节中数据和场景的局限以及人治导致一些Edge case被遗漏。


    解决方案


    我们该如何解决数据和场景的局限呢?这个其实通过Monkey和数据流量回放就能解决。
    运行时阶段包含了所有业务和代码的上下文,所有在这个阶段发现问题、分析原因并预警效率是最高的,人治的问题可以得到改善。下面是这种机制执行的思路



    1. 自动化测试时,通过流量回放的形式模拟线上的数据和环境尽可能多的覆盖场景。

    2. 运行时阶段,前端通过UT代码验收业务(🤔UT只能在Unit Test阶段执行,运行时执行的原理后面会讲)

    3. 运行时阶段,分析UI元素间的关系并探测异常问题


    方案实现


    方案实现仅讨论前端的部分。
    UI和业务检测比较通用,因为它们的判别条件是客观的。比如我们完全可以复用UT代码检测业务。


    自动检测、定位原因、预警


    这个机制实现没有困难。考虑到自动检测的范围在不同项目中不尽相同,所以实现的思路可以是插件的形式,模块间通过协议解耦。
    主要功能模块有:



    1. 告警模块

    2. 日志生成模块

    3. 业务注册模块(接收业务自定义的检查日志)

    4. 内嵌的UI检测模块


    UI检测


    业务不同,遇到的UI问题会有差异,这部分需要具体问题具体分析,所以不做过多讨论。针对我们业务的现状Overlap、Truncate、Clip在UI中占比较高。我的做法是对显示的视图按多叉树遍历到叶子节点并分析子节点和兄弟节点间的关系,找到Overlap、Truncate、Clip问题。具体的实现可以参考代码LensWindowGuard.swift:31


    业务检测


    UT代码从逻辑上可以被分为三个部分:



    1. Give

    2. When

    3. Then


    Given表示输入的数据,可以是真实接口也可以是Mock数据。


    When表示调用业务函数,同时这里会产生一个输出结果。


    Then表示检验输入的数据是否和输出的结果匹配,如果不匹配会在UT环节报错。


    业务代码从逻辑上可以被分为两个部分



    1. Give

    2. When


    Given可以是上下文的变量也可以是API调用


    When表示执行业务的代码块


    Blank diagram (23).png
    如果把UT的Then引入到业务的函数里,就可以实现运行时执行UT的效果了😁。


    将UT代码引入到业务函数中,思路是首先把UT按照Given、When、Then三部分拆分,把Given和Then部分独立出去,独立的部分通过代码插桩的方式在UT和业务代码中复用。


    Blank diagram (24).png


    不过到这里遗留了几个问题,暂时还没有太好的思路🧐



    1. 异步回调 - 业务代码或者UT检测逻辑只能执行一个

    2. 业务方法名的修改 - 使用Swift Macro插桩方式等同新增加一个全新的方法,所以运行时执行UT需要更改业务逻辑

    3. UT代码被执行多次 - 上面的图可以看出来,新的UT代码被同时插到旧的UT和业务代码中,所以执行UT时会UT逻辑被执行了两次


    代码插桩


    我们项目基于Swift,且插桩需要有判别逻辑,基于此选用AST的方式在编译期修改FunctionDecliation,把代码通过字符串的形式插到function body的合适位置。正巧2023 WWDC苹果发布了Swift Macro,其中的@Attached(Peer)正好可以满足对FunctionDecliation可编程修改,通过实现自动以@Attached(Peer)以及增加DEBUG宏(Swift Macro不能读取工程配置信息)可以改变UT的流程。真想说Swift太香了。


    最终


    完整的项目的整体架构大致如下,主要分了三部分。



    1. Hubble - 主要职责是提供基础协议、日志、预警和一些协助分析问题的工具集

    2. HubbleCoordinator - 主要职责是作为业务和组件交互的中间层,业务相关的逻辑都放在这里,隔离业务和Hubble

    3. 业务 - 仅处理Hubble初始化工作,对业务代码没有任何侵入
      AR


    作者:tom猪
    来源:juejin.cn/post/7274140856034099252
    收起阅读 »

    刚咬了一口馒头,服务器突然炸了!

    首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。 其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket...
    继续阅读 »

    首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。
    其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket管理器。


    看着群里一群“小可爱”疯狂乱叫,我被吵的头都炸了,赶紧尝试定位问题。

    1. 查看是否存在Jenkins发版 -> 无
    2. 查看最新提交记录 -> 最后一次提交是下午,到晚上这段时间系统一直是稳定的
    3. 查看服务器资源,htop后,发现三台相关的云服务器资源都出现闲置状态
    4. 查看PolarDB后,既MySQL,连接池正常、吞吐量和锁正常
    5. 查看Redis,资源正常,无异常key
    6. 查看前端控制台,出现一些报错,但是这些报错经常会变化
    7. 查看前端测试环境、后端测试环境,程序全部正常
    8. 重启前端服务、后端服务、NGINX服务,好像没用,过了5分钟后,咦,好像可以访问了

    就在我们组里的“小可爱”通知系统恢复正常后,20分钟不到,再一次处于无法打开的状态,沃焯!!!
    完蛋了,找不出问题在哪里了,实在想不通问题究竟出在哪里。


    我不服啊,我不理解啊!


    咦,Nginx?对呀,我瞅瞅访问日志,于是我浏览了一下access.log,看起来貌似没什么问题,不过存在大量不同浏览器的访问记录,刷的非常快。


    再瞅瞅error.log,好像哪里不太对

    2023/04/20 23:15:35 [alert] 3348512#3348512: 768 worker_connections are not enough
    2023/04/20 23:33:33 [alert] 3349854#3349854: *3492 open socket #735 left in connection 1013

    这是什么?貌似是连接池问题,赶紧打开nginx.conf看一下配置

    events {
    worker_connections 666;
    # multi_accept on;
    }

    ???


    运维小可爱,你特么在跟我开玩笑?虽然我们这个系统是给B端用的,还是我们自己组的不到100人,但是这连接池给的也太少了吧!


    另外,前端为什么会开到 1000 个 WS 连接呢?后端为什么没有释放掉FD呢?


    询问后,才知道,前端的Socket管理器,会在连接超时或者其它异常情况下,重新开启一个WS连接。


    后端的心跳配置给了300秒

    Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true, // websocket 协议
    Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 150,
    Constant::OPTION_HEARTBEAT_IDLE_TIME => 300,

    此时修改nginx.conf的配置,直接拉满!!!

    worker_connections 655350;

    重启Nginx,哇绰,好像可以访问了,但是每当解决一个问题,总会产生新的问题。


    此时error.log中出现了新的报错:

    2023/04/20 23:23:41 [crit] 3349767#3349767: accept4() failed (24: Too many open files)

    这种就不怕了,貌似和Linux的文件句柄限制有关系,印象中是1024个。
    至少有方向去排查,不是吗?而且这已经算是常规问题了,我只需小小的百度一下,哼哼~


    拉满拉满!!

    worker_rlimit_nofile 65535;

    此时再次重启Nginx服务,系统恢复稳定,查看当前连接数:

    netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
    # 打印结果
    TIME_WAIT 1175

    FIN_WAIT1 52

    SYN_RECV 1

    FIN_WAIT2 9

    ESTABLISHED 2033

    经过多次查看,发现TIME_WAITESTABLISHED都在不断减少,最后完全降下来。


    本次问题排查结束,问题得到了解决,但是关于socket的连接池管理器,仍然需要优化,及时释放socket无用的连接。


    作者:兰陵笑笑生666
    链接:https://juejin.cn/post/7224314619865923621
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    弃用qiankun!看古茗中后台架构如何破局

    引言 我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向...
    继续阅读 »

    引言


    我们团队之前发布过一篇文章,介绍了古茗前端到底在做什么,当然这只包含了我们团队内做的一部分事情。以端侧来划分,主要包括:中后台 web 端、H5 端、4 端小程序、Electron PC 端、Android/Flutter 客户端,我们需要为这些端侧方向做一些通用能力的解决方案,同时也需要深入每个端侧细分领域做它们特有的技术沉淀。本文主要介绍古茗在中后台技术方向的一些思考和技术沉淀。


    业务现状


    古茗目前有大量的中后台业务诉求,包括:权限系统、会员系统、商品中心、拓展系统、营运系统、财务系统、门店系统、供应链系统等多个子系统,服务于内部产品、技术、运营,及外部加盟商等用户群体,这些子系统分别由不同业务线的同学负责开发和维护,而这些子系统还没有系统性的技术沉淀。随着业务体量和业务复杂度的不断增大,在“降本增效”的大背景下,如何保证业务快速且高质量交付是我们面临的技术挑战。


    技术演进




    如上述技术演进路线图所示,我们的中后台技术架构大致经历了 4 个阶段:当业务起步时,我们很自然的使用了单应用开发模式;当业务发展到一定的体量,多人协同开发变得困难时,我们拆分了多个子系统维护,并使用了 systemjs 来加载子系统资源(微前端模式的雏形);当遇到有三方库的多版本隔离诉求时,我们又引入了 qiankun 微前端框架来隔离多个子系统,保证各子系统间互不影响;那是什么原因让我们放弃 qiankun,转向完全自研的一套中后台解决方案?


    弃用 qiankun?


    其实准确来说,qiankun 并不能算是一个完整的中后台解决方案,而是一个微前端框架,它在整个技术体系里面只充当子应用管理的角色。我们在技术演进过程中使用了 qiankun 尝试解决子应用的隔离问题,但同时也带来了一些新的问题:某些场景跳转路由后视图无反应;某些三方库集成进来后导致奇怪的 bug...。同时,还存在一些问题无法使用 qiankun 解决,如:子应用入口配置、样式隔离、运维部署、路由冲突、规范混乱、要求资源必须跨域等。


    探索方向


    我们重新思考了古茗中后台技术探索方向是什么,也就是中后台技术架构到底要解决什么问题,并确定了当下的 2 大方向:研发效率用户体验。由此我们推导出了中后台技术探索要做的第一件事情是“统一”,这也为我们整个架构的基础设计确立了方向。




    架构设计


    我们的整体架构是围绕着“统一”和“规范” 2 大原则来设计,目标是提升整个团队的研发效能。我们认为好的架构应该是边界清晰的,而不是一味的往上面堆功能,所以我们思考更多的是,如果没有这个功能,能否把这件事情做好。


    取个“好”名字


    我老板曾说过一个好的(技术)产品要做的第一件事就是取个“好”名字。我们给这套中后台架构方案取名叫「Mars」,将相关的 NPM 包、组件库、SDK、甚至部署路径都使用 mars 关键字来命名,将这个产品名字深入人心,形成团队内共同的产品认知。这样的好处是可以加强团队对这套技术方案的认同感,以及减少沟通负担,大家一提到 mars,就都知道在说哪一件事。


    框架设计




    正如上述大图所示,我们是基于微前端的思路做的应用框架设计,所以与市面上大多数的微前端框架的设计思路、分层结构大同小异。这里还是稍微介绍一下整个流程:当用户访问网站时,首先经过网关层,请求到基座应用的资源并渲染基础布局和菜单,当监听路由变化时,加载并渲染路由关联的子应用及页面。


    但是,市面上大部分的微前端框架往往需要在基座应用上配置子应用的 nameentryavtiveRule 等信息,因为框架需要根据这些信息来决定什么时机去加载哪一个子应用,以及如何加载子应用的资源。这就意味着每新增一个子应用,都需要去基座维护这份配置。更要命的是,不同环境的 entry 可能还要根据不同的环境做区别判断。如果遇到本地开发时需要跳转至其他子应用的场景,那也是非常不好的开发体验。所以,这是我们万万不能接受的。


    针对这一痛点,我们想到了 2 种解决思路:

    1. 把基座应用的配置放到云端,用 node 作为中间层来维护各子应用的信息,每次新增应用、发布资源后同步更新,问题就是这套方案实现成本比较高,且新增子应用还是需要维护子应用的信息,只是转移到在云端维护了。
    2. 使用约定式路由及部署路径,当我们识别到一个约定的路由路径时,可以反推它的应用 ID 及部署资源路径,完全 0 配置。很明显,我们选择了这种方案。

    约定式路由及部署路径


    路由约定


    我们制定了如下的标准 Mars 路由规范

      /mars/appId/path/some?name=ferret
    \_/ \_/ \_____/ \_______/
    | | | |
    标识 appId path query


    1. 路由必须以 /mars 开头(为了兼容历史路由包袱)

    2. 其后就是 appId ,这是子应用的唯一标识

    3. 最后的 pathquery 部分就是业务自身的路由和参数


    部署路径约定


    我们制定了如下的标准 Mars 子应用部署路径规范

      https://cdn.example.com/mars/[appId]/[env]/manifest.json
    \__________________/ \_/ \___/ \_/ \________/
    | | | | |
    cdn 域名 标识 appId 环境 入口资源清单

    从上述部署路径规范可以看出,整个路径就 appIdenv 2 个变量是不确定的,而 env 可以在发布时确定,因此可由 appId 推导出完整的部署路径。而根据路由约定,我们可以很容易的从路由中解析出 appId,由此就可以拿到完整的 manifest.json 部署路径 ,并以此获取到整个子应用的入口资源信息。


    编译应用


    虽然制定了上述 2 大规范,但是如何保障规范落地,防止规范腐化也是非常重要的一个问题。我们是通过编译手段来强制约束执行的(毕竟“人”往往是靠不住的😄)。


    依赖工程化体系



    提示:Kone 是古茗内部前端工程化的工具产品。



    首先,子应用需要配置一个工程配置文件,并注册 @guming/kone-plugin-mars 插件来完成子应用的本地开发、构建、发布等工程化相关的任务。其中:配置项 appId 就代表约定路由中的 appId 和 部署路径中的 appId,也是子应用的唯一标识。


    工程配置文件:kone.config.json

    {
    "plugins": ["@guming/kone-plugin-mars"],
    "mars": {
    "appId": "demo"
    }
    }

    编译流程


    然后,子应用通过静态化配置式(json 配置)注册路由,由编译器去解析配置文件,注册路由,以及生成子应用 mountunmount 生命周期方法。这样实现有以下 3 个好处:

    • 完整的路由 path 由编译生成,可以非常好的保障约定式路由落地
    • 生命周期方法由编译生成,减少项目中的模板代码,同样可以约束子应用的渲染和卸载按照预定的方式执行
    • 可以约束不规范的路由 path 定义,例如我们会禁用掉 :param 形式的动态路由

    应用配置文件:src/app.json

    {
    "routes": [
    {
    "path": "/some/list",
    "component": "./pages/list",
    "description": "列表页"
    },
    {
    "path": "/some/detail",
    "component": "./pages/detail",
    "description": "详情页"
    }
    ]
    }


    上述示例最终会生成路由:/mars/demo/some/list/mars/demo/some/detail



    webpack-loader 实现


    解析 src/app.json 需要通过一个自定义的 webpack-loader 来实现,部分示例代码如下:

    import path from 'path';
    import qs from 'qs';

    export default function marsAppLoader(source) {
    const { appId } = qs.parse(this.resourceQuery.slice(1));
    let config;
    try {
    config = JSON.parse(source);
    } catch (err) {
    this.emitError(err);
    return;
    }

    const { routes = [] } = config;

    const routePathSet = new Set();
    const routeRuntimes = [];
    const basename = `/mars/${appId}`;

    for (let i = 0; i < routes.length; i++) {
    const item = routes[i];
    if (routePathSet.has(item.path.toLowerCase())) {
    this.emitError(new Error(`重复定义的路由 path: ${item.path}`));
    return;
    }

    routeRuntimes.push(
    `routes[${i}] = { ` +
    `path: ${JSON.stringify(basename + item.path)}, ` +
    `component: _default(require(${JSON.stringify(item.component)})) ` +
    `}`
    );
    routePathSet.add(item.path.toLowerCase());
    }

    return `
    const React = require('react');
    const ReactDOM = require('react-dom');

    // 从 mars sdk 中引入 runtime 代码
    const { __internals__ } = require('@guming/mars');
    const { defineApp, _default } = __internals__;

    const routes = new Array(${routeRuntimes.length});
    ${routeRuntimes.join('\n')}

    // define mars app: ${appId}
    defineApp({
    appId: '${appId}',
    routes,
    });

    `.trim();
    }

    src/app.json 作为编译入口并经过此 webpack-loader 编译之后,将自动编译关联的路由组件,创建子应用路由渲染模板,注册生命周期方法等,并最终输出 manifest.json 文件作为子应用的入口(类似 index.html),根据入口文件的内容就可以去加载入口的 js、css 资源并触发 mount 生命周期方法执行渲染逻辑。生成的 manifest.json 内容格式如下:

    {
    "js": [
    "https://cdn.example.com/mars/demo/prod/app.a0dd6a27.js"
    ],
    "css": [
    "https://cdn.example.com/mars/demo/prod/app.230ff1ef.css"
    ]
    }

    聊聊沙箱隔离


    一个好的沙箱隔离方案往往是市面上微前端框架最大的卖点,我们团队内也曾引入 qiankun 来解决子应用间隔离的痛点问题。而我想让大家回归到自己团队和业务里思考一下:“我们团队需要隔离?不做隔离有什么问题”。而我们团队给出的答案是:不隔离 JS,要隔离 CSS,理由如下:

    1. 不隔离 JS 可能会有什么问题:window 全局变量污染?能污染到哪儿去,最多也就内存泄露,对于现代 B 端应用来说,个别内容泄露几乎可以忽略不计;三方库不能混用版本?如文章开头所提及的,我们要做的第一件事就是统一,其中就包括统一常用三方库版本,在统一的前提下这种问题也就不存在了。当然也有例外情况,比如高德地图 sdk 在不同子系统需要隔离(使用了不同的 key),针对这种问题我们的策略就是专项解决;当然,最后的理由是一套非常好的 JS 隔离方案实现成本太高了,需要考虑太多的问题和场景,这些问题让我们意识到隔离 JS 带来的实际价值可能不太高。
    2. 由于 CSS 的作用域是全局的,所以非常容易造成子应用间的样式污染,其次,CSS 隔离是容易实现的,我们本身就基于编译做了很多约束的事情,同样也可以用于 CSS 隔离方案中。实现方案也非常简单,就是通过实现一个 postcss 插件,将子应用中引入的所有 css 样式都加上特有的作用域前缀,例如:
    .red {
    color: red;
    }

    将会编译成:

    .mars__demo .red {
    color: red;
    }

    当然,某些场景可能就是需要全局样式,如 antd 弹层内容默认就会在子应用内容区外,造成隔离后的样式失效。针对这种场景,我们的解法是用隔离白名单机制,使用也非常简单,在最前面加上 :global 选择器,编译就会直接跳过,示例:

    :global {
    .some-modal-cls {
    font-size: 14px;
    }
    }

    将会编译成:

    .some-modal-cls {
    font-size: 14px;
    }

    除此之外,在子应用卸载的时候,还会禁用掉子应用的 CSS 样式,这是如何做到的?首先,当加载资源的时候,会找到该资源的 CSSStyleSheet 对象:

    const link = document.createElement('link');
    link.setAttribute('href', this.url);
    link.setAttribute('rel', 'stylesheet');
    link.addEventListener('load', () => {
    // 找到当前资源对应的 CSSStyleSheet 对象
    const styleSheets = document.styleSheets;
    for (let i = styleSheets.length - 1; i >= 0; i--) {
    const sheet = styleSheets[i];
    if (sheet.ownerNode === this.node) {
    this.sheet = sheet;
    break;
    }
    }
    });

    当卸载资源的时候,将该资源关联的 CSSStyleSheet 对象的 disabled 属性设置为 true 即可禁用样式:

    if (this.sheet) {
    this.sheet.disabled = true;
    }

    框架 SDK 设计




    框架 SDK 按照使用场景可以归为 3 类,分别是:子应用、基座应用、编译器。同样的遵循我们的一贯原则,如果一个 API 可以满足诉求,就不会提供 2 个 API,尽可能保证团队内的代码风格都是统一的。例如:路由跳转 SDK 只提供唯一 API,并通过编译手段禁用掉其他路由跳转方式(如引入 react-router-dom 的 API 会报错):

    import { mars } from '@guming/mars';

    // 跳转路由:/mars/demo/some/detail?a=123
    mars.navigate('/mars/demo/some/detail', {
    params: { a: '123' }
    });

    // 获取路由参数
    const { pathname, params } = mars.getLocation();
    // pathname: /mars/demo/some/detail
    // params: { a: '123' }

    当然,我们也会根据实际情况提供一些便利的 API,例如:跳转路由要写完整的 /mars/[appId] 路由前缀太繁琐,所以我们提供了一个语法糖来减少样板代码,在路由最前面使用 : 来代替 /mars/[appId] 前缀(仅在当前子应用内跳转有效):

    import { mars } from '@guming/mars';

    // 跳转路由:/mars/demo/some/detail?a=123
    mars.navigate(':/some/detail', {
    params: { a: '123' }
    });

    另外,值得一提的是在基座应用上使用的一个 API bootstrap(),得益于这个 API 的设计,我们可以快速创建多个基座应用(不同域名),还能使用这个 API 在本地开发的时候启动一个基座来模拟本地开发环境,提升开发体验。


    本地开发体验


    开发模拟器


    为更好的支持本地开发环境,我们提供了一套本地开发模拟器,在子应用启动本地服务的时候,会自动启动一个模拟的基座应用,拥有和真实基座应用几乎一样的布局,运行环境,集成的登录逻辑等。除此之外,开发模拟器还提供了辅助开发的「debug 小组件」,比如通过 debug 工具可以动态修改本地开发代理规则,保存之后立即生效。




    IDE 支持


    为了提升开发体验,我们分别开发了 Webstorm 插件 和 VSCode 插件服务于 Mars 应用,目前支持路由组件配置的路径补全、点击跳转、配置校验等功能。此外,我们会为配置文件提供 json schema,配置后将会获得 IDE 的自动补全能力和配置校验能力。




    历史项目迁移


    技术架构演进对于业务项目来说最大的问题在于,如何完成历史项目向新架构的迁移改造?我们团队投入 2 个人花了 2 个月时间将 12 个历史项目全部迁移至最新的架构上,这里分享一些我们在迁移过程中的经验。


    定目标


    首先要确定历史项目迁移这件事情一定是要做的,这是毋庸置疑的,而且要越快越好。所以我们要做的第一件事就是制定迁移计划,最后确定投入 2 个人力,大约花 2 个月时间将历史项目全部迁移至新的架构。第二件事情就是确定改造的范围(确定边界,不做非目标范围内的改造),对于我们的业务现状来说,主要包括:

    • 统一 reactreact-dom 版本为 17.0.2
    • 统一 antd 版本为 4.24.8
    • 统一路由
    • 统一接入 request 请求库
    • 统一接入工程化体系
    • 统一环境变量

    梳理 SOP


    因为迁移的流程不算简单,迁移要做的事情还挺多的,所以接下来要做的一件事就是梳理迁移流程 SOP,SOP 文档要细化到每种可能的场景,以及遇到问题对应的解法,让后续项目的迁移可以傻瓜式的按照标准流程去操作即可。我们的做法是,先以一个项目作为试点,一边迁移一边梳理 SOP,如果在迁移其他项目中发现有遗漏的场景,再持续补充这份 SOP 文档。


    例如:之前项目中使用了 dva 框架,但是它的 routermodel 是耦合的,这样就无法使用我们制定的统一路由方案,对此我们的解法是,通过 hack dva 的源代码,将 model 前置注入到应用中,完成与路由的解耦。


    上线方案


    由于业务迭代频繁,所以我们代码改造持续的时间不能太长,否则要多次合并代码冲突,而我们的经验就是,项目改造从拉分支到发布上线,要在 1 周内完成。当然,整个上线过程还遇到许多需要解决的问题,比如在测试参与较少的情况下如何保障代码质量,包括:业务回归的策略,回滚策略,信息同步等等。


    总结


    之前看到 Umi 4 设计思路文字稿 里面有句话我觉得特别有道理:“社区要开放,团队要约束”,我们团队也在努力践行“团队约束”这一原则,因为它为团队带来的收益是非常高的。


    没有最完美的方案,只有最适合自己的方案,以上这套架构方案只是基于当下古茗前端团队现状做的选择后的结果,可能并不适合每个团队,希望本文的这些思考和技术沉淀能对您有所帮助和启发。


    最后


    关注公众号「Goodme前端团队」,获取更多干货实践,欢迎交流分享~


    作者:古茗前端团队
    链接:https://juejin.cn/post/7269352663258284069
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    树形列表翻页,后端: 搞不了搞不了~~

    web
    背景 记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用...
    继续阅读 »

    背景


    记得几年前做了一个报告,报告里面加载的是用户的历年作品还有会员信息,然后按照年月倒序展示出来,其中历年作品都要将作品的封面展示出来。一开始这个报告到没啥问题,而且一个时间轴下来感觉挺好,有用户的作品、会员记录、关注以及粉丝记录很全面。直到最近忽然有一批用户说一进到这个报告页面就卡住不动了,上去一查发现不得了,都是铁杆用户,每年作品都几百个,导致几年下来,这个报告返回了几千个作品,包含上千的图片。


    问题分析


    上千的图片,肯定会卡,首先想到的是做图片懒加载。这个很简单,使用一个vue的全局指令就可以了。但是上线发现,没啥用,dom节点多的时候,懒加载也卡。


    然后就问服务端能不能支持分页,服务端说数据太散,连表太多,树形结构很难做分页。光查询出来就已经很费劲了。


    没办法于是想了一下如何前端来处理掉。


    思路




    1. 由于是app中的嵌入页面,首先考虑通过滚动进行分页加载。




    2. 一次性拿了全部的数据,肯定不能直接全部渲染,我们可以只渲染一部分,比如第一个节点,或者前几个节点。




    3. 随着滚动一个节点一个节点或者一批一批的渲染到dom中。




    实现


    本文仅展示一种基于vue的实现


    1. 容器

    设计一个可以进行滚动翻页的容器 然后绑定滚动方法OnPageScrolling



    <style lang="less" scoped>

    .study-backup {

    overflow-x: hidden;

    overflow-y: auto;

    -webkit-overflow-scrolling: touch;

    width: 100%;

    height: 100%;

    position: relative;

    min-height: 100vh;

    background: #f5f8fb;

    box-sizing: border-box;

    }

    </style>

    <template>

    <section class="report" @scroll="OnPageScrolling($event)">

    </section>

    </template>



    2.初始化数据

    这里定义一下树形列表的数据结构,实现初始化渲染,可以渲染一个树节点,或者一个树节点的部分子节点



    GetTreeData() {

    treeapi

    .GetTreeData({ ... })

    .then((result) => {

    // 处理结果

    const data = Handle(result)

    // 这里备份一份数据 不参与展示

    this.backTreeList = data.map((item) => {

    return {

    id: item.id,

    children: item.children

    }

    })

    // 这里可以初始化为第一个树节点

    const nextTree = this.backTreeList[0]

    const nextTansformTree = nextTree.children.splice(0)

    this.treeList = [{

    id: nextTree.id,

    children: nextTansformTree

    }]

    // 这里可以初始化为第一树节点 但是只渲染第一个子节点

    const nextTree = this.backTreeList[0]

    const nextTansformTree = nextTree.children.splice(0, 1)

    this.treeList = [{

    id: nextTree.id,

    children: nextTansformTree

    }]

    })

    },


    3.滚动加载

    这里通过不断的把 backTreeList 的子节点转存入 treeList来实现分页加载。



    OnPageScrolling(event) {

    const container = event.target

    const scrollTop = container.scrollTop

    const scrollHeight = container.scrollHeight

    const clientHeight = container.clientHeight

    // console.log(scrollTop, clientHeight, scrollHeight)

    // 判断是否接近底部

    if (scrollTop + clientHeight >= scrollHeight - 10) {

    // 执行滚动到底部的操作

    const currentReport = this.backTreeList[this.treeList.length - 1]

    // 检测匹配的当前树节点 treeList的长度作为游标定位

    if (currentReport) {

    // 判断当前节点的子节点是否还存在 如果存在则转移到渲染树中

    if (currentReport.children.length > 0) {

    const transformMonth = currentReport.children.splice(0, 1)

    this.treeList[this.treeList.length - 1].children.push(

    transformMonth[0]

    )

    // 如果不存在 则寻找下一树节点进行复制 同时复制下一节点的第一个子节点 当然如果寻找不到下一树节点则终止翻页

    } else if (this.treeList.length < this.backTreeList.length) {

    const nextTree = this.backTreeList[this.treeList.length]

    const nextTansformTree = nextTree.children.splice(0, 1)

    this.treeList.push({

    id: nextTree.id,

    children: nextTansformTree

    })

    }

    }

    }

    }


    4. 逻辑细节

    从上面代码可以看到,翻页的操作是树copy的操作,将备份树的子节点转移到渲染树中




    1. copy备份树的第一个节点到渲染树,同时将备份树的第一个节点的子节点的第一个节点转移到渲染树的第一个节点的子节点中




    2. 所谓转移操作,就是数组splice操作,从一颗树中删除,然后把删除的内容插入到另一颗树中




    3. 由于渲染树是从长度1开始的,所以我们可以根据渲染树的长度作为游标和备份树进行匹配,设渲染树的长度为当前游标




    4. 根据当前游标查询备份树,如果备份树的当前游标节点的子节点不为空,则进行转移




    5. 如果备份树的当前游标节点的子节点为空,则查找备份树的当前游标节点的下一节点,设为下一树节点




    6. 如果找到了备份树的当前游标节点的下一节点,扩展渲染树,将下一树节点复制到渲染树,同时将下一树节点的子节点的第一节点复制到渲染树




    7. 循环4-6,将备份树完全转移到渲染树,完成所有翻页




    扩展思路


    这个方法可以进行封装,将每次复制的节点数目和每次复制的子节点数目作为传参,一次可复制多个节点,这里就不做展开


    作者:CodePlayer
    来源:juejin.cn/post/7270503053358612520
    收起阅读 »

    创建一个可以循环滚动的文本,可能没这么简单。

    web
    如何创建一个可以向左循环滚动的文本? 创建如上图效果的滚动文本,你能想到几种方式? -------- 暂停阅读,不如你自己先试一下 -------- 方式一: 根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。 如果偏移的元素不可见后...
    继续阅读 »

    如何创建一个可以向左循环滚动的文本?


    loop.gif


    创建如上图效果的滚动文本,你能想到几种方式?


    -------- 暂停阅读,不如你自己先试一下 --------


    方式一:


    根据页面宽度,生成多个元素。每个元素通过js控制,每帧向左偏移一点像素。

    如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


    此方式容易理解,实现起来也不困难,但是有性能上的风险,因为每一帧都在修改元素的位置。


    方式二:


    根据页面宽度,生成多个元素。每个元素通过js控制,通过setInterval每一秒向左偏移一些像素。

    然后结合css的transition: all 1s linear;使得偏移更加顺滑。

    如果偏移的元素不可见后,将此元素再移动到屏幕最右边。


    使用此方法可以避免高频率计算元素位置,但是此方式控制起来更复杂,主要是因为,将元素移动到最右边的时候,也会触发transition ,需要额外逻辑控制在元素移到最右边的时候不触发transition

    并且在实际开发中发现。当窗口不可见时候动画实际会暂停,还需要控制当窗口隐藏时候,暂停setInterval


    方式三:


    换一种思路。按顺序排列元素,多个子元素首位相接。将每个子元素通过animation: xxx 10s linear infinite;

    从左到右移动。在一定范围内移动子元素,通过视觉错觉,像是整个大元素(盒子)都在移动。

    此方式简单,并且无需JS,性能较好。


    下面是完整代码(可以控制浏览器宽度,查看不同尺寸屏幕的效果)


    <!doctype html>  
    <html>
    <head>
    <title>LOOP</title>
    <style>
    @keyframes loop {
    0% {
    transform: translateX(0);
    }
    100% {
    transform: translateX(-100%);
    }
    }

    .box {
    white-space: nowrap;
    }

    .scrollContent {
    width: 600px;
    display: inline-block;
    text-align: center;
    animation: loop 3s linear infinite;
    }
    </style>
    </head>
    <body>
    <div class="box">
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    </div>
    </body>

    </html>


    方式四:


    方式三会创建多份一样的文本内容,你可能会说,屏幕上同时出现这么多文本元素,当然要创建这么多一样的内容。

    其实还有一种性能更佳的方式:text-shadow: 600px 0 currentColor,通过此方式创建多份文本副本,达到类似效果。

    此方法性能最佳。但是对非文本无能为力。


    <!doctype html>  
    <html>
    <head>
    <title>LOOP</title>
    <style>
    @keyframes loop {
    0% {
    transform: translateX(0);
    }
    100% {
    transform: translateX(-100%);
    }
    }

    .box {
    white-space: nowrap;
    }

    .scrollContent {
    color: rebeccapurple;
    width: 600px;
    display: inline-block;
    text-align: center;
    animation: loop 3s linear infinite;
    text-shadow: 600px 0 currentColor, 1200px 0 currentColor, 1800px 0 currentColor, 2400px 0 currentColor;
    }
    </style>
    </head>
    <body>
    <div class="box">
    <div class="scrollContent">
    这是一段可以滚动的文本
    </div>
    </div>
    </body>

    </html>

    总结


    方式1:应该是最直接想到的方式。但是出于对性能的担忧。

    方式2:由于方式1性能优化得到,但是方式2过于复杂。
    方式3: 看上去非常易于实现,实际很难想到。
    方式4:如果对text-shadow和css颜色掌握不熟,根本难以实现。


    希望对你有所启发


    作者:wuwei123
    来源:juejin.cn/post/7273026570930257932
    收起阅读 »

    位运算,能不能一次记住!

    web
    写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧! 我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,...
    继续阅读 »

    写给新手朋友的位运算,你是不是也是总是不记得,记了又忘记,记住了又不知道怎么运用到编程中?那记一次理论+题目的方式理解记忆,搞清楚它吧!


    我们接触最多的是十进制数,加减乘除这种四则远算其实就是二进制数据中的一种运算法则,当谈到位运算,它不是用来操作十进制数的,我们实际上是在操作二进制数的不同位。位运算在前端开发中可能不常用,但了解它们对你理解计算机底层运作和一些特定情况下的优化是有帮助的。


    接下来我们从几种常见的位运算开始,以及它们的使用场景,好好理解一番。


    1. 二进制转换


    既然是写给新手朋友也能看得明白的,那就顺带提一下二进制数吧(熟悉二进制的可以跳过这段)



    当计算机处理数据时,它实际上是在执行一系列的二进制操作,因为计算机内部使用的是电子开关,这些开关只能表示两个状态:开(表示1)和关(表示0)。因此,计算机中的所有数据最终都被转换为二进制表示。


    二进制(binary)是一种使用两个不同符号(通常是 0 和 1)来表示数字、字符、图像等信息的数字系统。这种二元系统是现代计算机科学的基础。





    • 十进制到二进制的转换:




    将十进制数转换为二进制数的过程涉及到不断地除以2,然后记录余数。最后,将这些余数按相反的顺序排列,就得到了对应的二进制数。


    例如,将十进制数 13 转换为二进制数:



    1. 13 除以 2 得商 6,余数 1

    2. 6 除以 2 得商 3,余数 0

    3. 3 除以 2 得商 1,余数 1

    4. 1 除以 2 得商 0,余数 1


    将这些余数按相反的顺序排列,得到二进制数 1101。


    或者你也可以这么想


    1. (1 || 0) * 2^n + (1 || 0) * 2^(n-1) + ... + (1 || 0) * 2^0 = 13

    2. 只需要满足以上公式,加出来你想要的值

    3. 2 的 4次方大于13,2的3次方小于13,那么就从2的3次方开始依次递减到0次方

    4. 1 * 2^3 + 1 * 2^2 + 1 * 2^1 + 1 * 2^0 显然 8 + 4 + 2 + 1 = 15已经超出了13,所以你得在这个式子中减少2

    5. 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 取该等式中的1,0;所以 13 的二进制是 1101


    以上两种方式都能得出一个数的二进制,看你喜欢




    • 二进制到十进制的转换:




    将二进制数转换为十进制数的过程涉及到将每个位上的数字与2的幂相乘,然后将这些结果相加。


    例如,将二进制数 1101 转换为十进制数:



    1. 第0位(最右边)上的数字是 1,表示 2^0 = 1

    2. 第1位上的数字是 0,表示 2^1 = 0

    3. 第2位上的数字是 1,表示 2^2 = 4

    4. 第3位上的数字是 1,表示 2^3 = 8


    将这些结果相加:1 + 0 + 4 + 8 = 13,得到十进制数 13。


    在编程中,通常会使用不同的函数或方法来实现十进制到二进制以及二进制到十进制的转换,这些转换可以帮助我们在计算机中处理和表示不同的数据。


    2. 按位与(&)


    按位与运算会将两个数字的二进制表示的每一位进行 操作,如果两个相应位都是 1,则结果为 1,否则为 0。


    使用场景: 常用于权限控制和掩码操作。


    image.png


    一道题让你更好的理解它的用法


    题目:判断一个整数是否是2的幂次方。


    问题描述:给定一个整数 n,判断它是否是2的幂次方,即是否满足 n = 2^k,其中 k 是非负整数。


    使用位运算中的按位与操作可以很巧妙地解决这个问题。


    思路:如果一个数 n 是2的幂次方,那么它的二进制表示一定只有一位是1,其他位都是0(例如:8的二进制是 1000)。而 n - 1 的二进制表示则是除了最高位的1之外,其他位都是1(例如:7的二进制是 0111)。如果我们对 nn - 1 进行按位与操作,结果应该是0。


    那我们可以这么写:


    image.png


    在这个示例中,我们巧妙的使用了 (n & (n - 1)) 来检查是否满足条件,如果结果为0,说明 n 是2的幂次方。


    希望这个示例能够帮助你更好地理解按位与运算的应用方式!


    2. 按位或(|)


    按位或运算会将两个数字的二进制表示的每一位进行或操作,如果两个相应位至少有一个是 1,则结果为 1,否则为 0。


    使用场景: 常用于设置选项和权限。


    image.png


    一道题让你更好的理解它的用法


    题目:如何将一个整数的特定位设置为1,而不影响其余位。


    问题描述:给定一个整数 num,以及一个表示要设置为1的位的位置 bitPosition(从右向左,最低位的位置为0),编写一个函数将 num 的第 bitPosition 位设置为1。


    我们可以使用按位或运算来实现这个效果


    image.png


    在这个示例中,我们首先创建了一个掩码 mask(这里用到了另一个位运算,左移,下面会讲到),它只有第 bitPosition 位是1,其他位都是0。然后,我们使用按位或运算 num | masknum 的第 bitPosition 位设置为1,得到了结果。


    这个问题演示了如何使用按位或运算来修改一个整数的特定位,而不影响其他位。希望这个示例能帮助你更好地理解按位或运算的应用方式!


    3. 按位异或(^)


    按位异或运算会将两个数字的二进制表示的每一位进行异或操作,如果两个相应位不相同则结果为 1,相同则为 0。


    使用场景: 常用于数据加密和校验。


    image.png


    一道题让你更好的理解它的用法


    题目:如何交换两个整数的值,而不使用额外的变量


    问题描述:给定两个整数 ab,编写一个函数来交换它们的值,而不使用额外的变量。


    我们可以使用按位异或运算来实现这个效果:


    image.png


    上述代码中,我们首先将 a 更新为 a ^ b,这使得 a 包含了 ab 的异或值。然后,我们使用同样的方法将 b 更新为 a 的原始值,最后,我们再次使用异或运算将 a 更新为 b 的原始值,完成了交换操作。



    此处应该沉思,思考清楚这个问题:(a ^ b) ^ b 得到的是 a 的原始值



    不使用额外的变量来做两个变量值的交换,这还是个面试题哦!


    4. 按位非(~)


    按位非运算会将一个数字的二进制表示的每一位取反,即 0 变成 1,1 变成 0。它将操作数转化为 32 位的有符号整型。


    image.png


    一道题让你更好的理解它的用法


    题目:反转二进制数的位,然后返回其对应的十进制数


    问题描述:给定一个二进制字符串,编写一个函数来反转该字符串的位,并返回其对应的十进制数。


    image.png


    这里你可能会有疑问,为什么13的二进制取反会的到-14,这里就不得不介绍一下 补码 的概念了


    5. 补码小插曲


    假设我们要求 -6 的二进制,那就相当于是求 -6 的补码


    因为负数的二进制表示通常使用二进制补码来表示。要计算-6的二进制补码表示,可以按照以下步骤操作:



    1. 首先,找到6的二进制表示。6的二进制表示是 00000110

    2. 然后,对6的二进制表示进行按位取反操作,即将0变成1,将1变成0。这将得到 11111001

    3. 最后,将取反后的结果加1。11111001 + 1 = 11111010


    所以,-6的二进制补码表示是 11111010。在补码中,最高位表示符号位,0表示正数,1表示负数,其余位表示数值的绝对值。因此,11111010 表示的是-6。


    注意:

    -6的二进制补码表示的位数不一定是8位。位数取决于数据类型和计算机系统的规定。在许多计算机系统中,整数的表示采用固定的位数,通常是32位或64位,但也可以是其他位数,例如16位。


    在常见的32位表示中,-6的二进制补码表示可能是 11111111111111111111111111111010。这是32位二进制,其中最高位是符号位(1表示负数),其余31位表示数值的绝对值。


    在64位表示中,-6的二进制补码表示可能是 1111111111111111111111111111111111111111111111111111111111110。这是64位二进制,同样,最高位是符号位,其余63位表示数值的绝对值。


    因此,-6的二进制补码表示的位数取决于计算机系统和数据类型的规定。不同的系统和数据类型可能采用不同的位数。


    6. 左移(<<)和右移(>>)


    左移运算将一个数字的二进制表示向左移动指定的位数,右移运算将二进制表示向右移动指定的位数。


    image.png



    注意:因为我们的计算可以是32位或者是64位的,所以理论上 5 的二进制应该是 00... 00000101, 整体长度为32或者64。 左移我们只是把有效值 101 向左拖动,右边补0,右移左边补 0, 但是要保证整体32或64位长度不能变,所以,右移会砍掉超出去的值



    一道题让你更好的理解它的用法


    题目: 如何实现整数的乘法和除法,使用左移和右移操作来提高效率。


    问题描述:编写一个函数,实现整数的乘法和除法运算,但是只能使用左移和右移操作,不能使用乘法运算符 * 和除法运算符 /


    这也是一道面试题,实现起来很简单


    image.png



    想清楚,一个数的二进制,每次左移一位的结果会怎么样?


    比如 6 的二进制是 00000110, 左移一次后变成 00001100,


    也就是说 从 2^2 + 2^1 变成了 2^3+ 2^2 。 4 + 2 变成了 8 + 4。


    所以每左移一位,都相当于是原数值本身放大了一倍



    这样你是否更清楚了用左移来实现乘法的效果了呢?


    最后


    以上列举的是常见的位运算方法,还有一些不常见的,比如:



    1. 位清零(Bit Clearing):将特定位设置为0,通常使用按位与运算和适当的掩码来实现。

    2. 位设置(Bit Setting):将特定位设置为1,通常使用按位或运算和适当的掩码来实现。

    3. 位翻转(Bit Flipping):将特定位取反,通常使用按位异或运算和适当的掩码来实现。

    4. 检查特定位:通过使用按位与运算和适当的掩码来检查特定位是否为1或0。

    5. 位计数:计算一个整数二进制表示中1的个数,这通常使用一种称为Brian Kernighan算法的技巧来实现。

    6. 位交换:交换两个整数的特定位,通常使用按位异或运算来实现。


    等等...有兴趣的可以自行摸索了


    作者:一个大蜗牛
    来源:juejin.cn/post/7274188187675902004
    收起阅读 »

    如何告诉后端出身的领导:这个前端需求很难实现

    本文源于一条评论。 有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。” 这位朋友让我写一写,那我就写一写。 反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评...
    继续阅读 »

    本文源于一条评论。




    有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。


    这位朋友让我写一写,那我就写一写。


    反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评论回复吧。愿意看扯淡的同学可以留一下。


    现象分析


    首先,前半句让我很有同感。因为,我就是前端出身,并且在研发管理岗上又待过。这个身份,让我既了解前端的工作流程,又经常和后端平级一起交流经验。这有点卧底的意思。


    有一次,我们几个朋友聚餐闲聊。他们也都是各个公司的研发负责人。大家就各自吐槽自己的项目。


    有一个人就说:“我们公司,有一个干前端的带项目了,让他搞得一团糟”


    另一个人听到后,就叹了一口气:“唉!这不是外行领导内行吗?!


    我当时听了感觉有点别扭。我扫视了一圈儿,好像就我一个前端。因此,我也点了点头(默念:我是卧底)。


    是啊,一般的项目、研发管理岗,多是后端开发。因为后端是业务的设计者,他掌握着基本的数据结构以及业务流转。而前端在他们看来,就是调调API,然后把结果展示到页面上。


    另外,一般项目遇到大事小情,也都是找后端处理。比如手动修改数据、批量生成数据等等。从这里可以看出,项目的“根基”在后端手里。


    互联网编程界流传着一条鄙视链。它是这样说的,搞汇编的鄙视写C语言的,写C的鄙视做Java的,写Java的鄙视敲Python的,搞Python的鄙视写js的,搞js的鄙视作图的。然后,作图的设计师周末带着妹子看电影,前面的那些大哥继续加班写代码。


    当然,我们提倡一个友好的环境,不提倡三六九等。这里只是调侃一下,采用夸张的手法来阐述一种现象。


    这里所谓的“鄙视”,其本质是源于谁更接近原理。


    比如写汇编的人,他认为自己的代码能直接调用“CPU”、“内存”,可以直接操纵硬件。有些前端会认为美工设计的东西,得依靠自己来实现,并且他们不懂技术就乱设计,还有脸要求100%还原。


    所以啊,有些只做过后端的人,可能会认为前端开发没啥东西。


    好了,上面是现象分析。关于谁更有优越感,这不能细究啊,因为这是没有结论的。如果搞一个辩论赛,就比方说”Python一句话,汇编写三年“之类论据,各有千秋。各种语言既然能出现,必定有它的妙用。


    我就是胡扯啊。不管您是哪个工种,请不要对号入座。如果您觉得被冒犯了,可以评论“啥都不懂”,然后离开。千万不要砸自己的电脑。


    下面再聊聊第二部分,面对这种情况,有什么好的应对措施?


    应对方法


    我感觉,后端出身的负责人,就算是出于人际关系,也是会尊重前端开发的


    “小张啊,对于前端我不是很懂。所以请帮我评估下前端的工期。最好列得细致一些,这样有利于我了解人员的工作量。弄完了,发给我,我们再讨论一下!”


    一般都是这么做。


    这种情况,我们就老老实实给他上报就行了,顶多加上10%~30%处理未知风险的时间。


    但是,他如果这样说:“什么?这个功能你要做5天?给你1天都多!


    这时,他是你的领导,对你又有考核,你怎么办?


    你心里一酸:“我离职吧!小爷我受不了这委屈!”


    这……当然也可以。


    如果你有更好的去处,可以这样。就算是没有这回事,有好去处也赶紧去。


    但是,如果这个公司整体还行呢?只是这个直接领导有问题。那么你可以考虑,这个领导是流水的,你俩处不了多长时间。哪个公司每年不搞个组织架构调整啊?你找个看上的领导,吃一顿饭,求个投靠呗。


    或者,这个领导只是因为不懂才这样。谁又能样样精通呢?给他说明白,他可能就想通了。人是需要说服的。


    如果你奔着和平友好的心态去,那么可以试试以下几点:


    第一,列出复杂原因


    既然你认为难以实现,那肯定是难以实现。具体哪里不好实现,你得说说


    记得刚工作时,我去找后端PK,我问他:“你这个接口怎么就难改了?你不改这个字段,我们得多调好几个接口,而且还对应不起来!”


    后端回复我:“首先,ES……;其次,mango……;最后,redis……”


    我就像是被反复地往水缸中按,听得”呜噜呜噜“的,一片茫然。


    虽然,我当时不懂后端,但我觉得他从气势上,就显得有道理


    到后来,还是同样的场景。我变成了项目经理,有一个年轻的后端也是这么回复我的。


    我说:“首先,你不用……;其次,数据就没在……;最后,只需要操作……”。他听完,挠了挠头说,好像确实可以这么做。


    所以,你要列出难以实现的地方。比如,没有成熟的UI组件,需要自己重新写一个。又或者,某种特效,业内都没有,很难做到。再或者,某个接口定得太复杂,循环调组数据,会产生什么样的问题。


    如果他说“我看到某某软件就是这样”。


    你可以说,自己只是一个初级前端,完成那些,得好多高级前端一起研究才行。


    如果你懂后端,可以做一下类比,比如哪些功能等同于多库多表查询、多线程并发问题等等,这可以辅助他理解。不过,如果能到这一步,他的位子好像得换你来坐。


    第二,给出替代方案


    这个方案,适用于”我虽然做不了,但我能解决你的问题“。


    就比如那个经典的打洞问题。需求是用电钻在墙上钻一个孔,我搞不定电钻,但是我用锤子和钉子,一样能搞出一个孔来。


    如果,你遇到难以实现的需求。比如,让你画一个很有特色的扇形玫瑰图。你觉得不好实现,不用去抱怨UI,这只会激化问题。咱可以给他提供几个业界成熟的组件。然后,告诉他,哪里能改,都能改什么(颜色、间距、字体……)。


    我们有些年轻的程序员很直,遇到不顺心的事情就怼。像我们大龄程序员就不这样。因为有房贷,上有老下有小。年龄大的程序员就想,到底是哪里不合适,我得怎样通过我的经验来促成这件事——这并不光彩,年轻人就得有年轻人的样子。


    第二招是给出替代方案。那样难以实现,你看这样行不行


    第三,车轮战,搞铺垫


    你可能遇到了一个硬茬。他就是不变通,他不想听,也不想懂,坚持“怎么实现我不管,明天之前要做完”。


    那他可能有自己的压力,比如老板就是这么要求他的。他转手就来要求你。


    你俩的前提可能是不一样。记住我一句话,没有共同前提,是不能做对比的。你是员工,他是领导,他不能要求你和他有一样的压力,这就像你不能要求,你和他有一样的待遇。多数PUA,就是拿不同的前提,去要求同样的结果。


    那你就得开始为以后扯皮找铺垫了。


    如果你们组有多个前端,可以发动大家去进谏。


    ”张工啊,这个需求,确实不简单,不光是小刘,我看了,也觉得不好实现“


    你一个人说了他不信,人多了可能就信了。


    如果还是不信。那没关系,已经将风险提前抛出了


    “这个需求,确实不好实现。非要这么实现,可能有风险。工期、测试、上线后的稳定性、用户体验等,都可能会出现一些问题”


    你要表现出为了项目担忧,而不是不想做的样子。如果,以后真发生了问题,你可以拿出“之前早就说过,多次反馈,无人理睬”这类的说辞。


    ”你居然不想着如何解决问题,反倒先想如何逃避责任?!“


    因此说,这是下下策。不建议程序员玩带有心机的东西。


    以上都是浅层次的解读。因为具体还需要结合每个公司,每个领导,每种局面。


    总之,想要解决问题,就得想办法


    作者:TF男孩
    链接:https://juejin.cn/post/7235966417564532794
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    京东一面:post为什么会发送两次请求?🤪🤪🤪

    web
    在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。 那么接下来这篇文章我们就一点一点开始引出这个问题。 同源策略 在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、...
    继续阅读 »

    在前段时间的一次面试中,被问到了一个如标题这样的问题。要想好好地去回答这个问题,这里牵扯到的知识点也是比较多的。


    那么接下来这篇文章我们就一点一点开始引出这个问题。


    同源策略


    在浏览器中,内容是很开放的,任何资源都可以接入其中,如 JavaScript 文件、图片、音频、视频等资源,甚至可以下载其他站点的可执行文件。


    但也不是说浏览器就是完全自由的,如果不加以控制,就会出现一些不可控的局面,例如会出现一些安全问题,如:



    • 跨站脚本攻击(XSS)

    • SQL 注入攻击

    • OS 命令注入攻击

    • HTTP 首部注入攻击

    • 跨站点请求伪造(CSRF)

    • 等等......


    如果这些都没有限制的话,对于我们用户而言,是相对危险的,因此需要一些安全策略来保障我们的隐私和数据安全。


    这就引出了最基础、最核心的安全策略:同源策略。


    什么是同源策略


    同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。


    如果两个 URL 的协议、主机和端口都相同,我们就称这两个 URL 同源。



    • 协议:协议是定义了数据如何在计算机内和之间进行交换的规则的系统,例如 HTTP、HTTPS。

    • 主机:是已连接到一个计算机网络的一台电子计算机或其他设备。网络主机可以向网络上的用户或其他节点提供信息资源、服务和应用。使用 TCP/IP 协议族参与网络的计算机也可称为 IP 主机。

    • 端口:主机是计算机到计算机之间的通信,那么端口就是进程到进程之间的通信。


    如下表给出了与 URL http://store.company.com:80/dir/page.html 的源进行对比的示例:


    URL结果原因
    http://store.company.com:80/dir/page.html同源只有路径不同
    http://store.company.com:80/dir/inner/another.html同源只有路径不同
    https://store.company.com:80/secure.html不同源协议不同,HTTP 和 HTTPS
    http://store.company.com:81/dir/etc.html不同源端口不同
    http://news.company.com:80/dir/other.html不同源主机不同

    同源策略主要表现在以下三个方面:DOM、Web 数据和网络。



    • DOM 访问限制:同源策略限制了网页脚本(如 JavaScript)访问其他源的 DOM。这意味着通过脚本无法直接访问跨源页面的 DOM 元素、属性或方法。这是为了防止恶意网站从其他网站窃取敏感信息。

    • Web 数据限制:同源策略也限制了从其他源加载的 Web 数据(例如 XMLHttpRequest 或 Fetch API)。在同源策略下,XMLHttpRequest 或 Fetch 请求只能发送到与当前网页具有相同源的目标。这有助于防止跨站点请求伪造(CSRF)等攻击。

    • 网络通信限制:同源策略还限制了跨源的网络通信。浏览器会阻止从一个源发出的请求获取来自其他源的响应。这样做是为了确保只有受信任的源能够与服务器进行通信,以避免恶意行为。


    出于安全原因,浏览器限制从脚本内发起的跨源 HTTP 请求,XMLHttpRequest 和 Fetch API,只能从加载应用程序的同一个域请求 HTTP 资源,除非使用 CORS 头文件


    CORS


    对于浏览器限制这个词,要着重解释一下:不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但是返回结果被浏览器拦截了。


    浏览器将不同域的内容隔离在不同的进程中,网络进程负责下载资源并将其送到渲染进程中,但由于跨域限制,某些资源可能被阻止加载到渲染进程。如果浏览器发现一个跨域响应包含了敏感数据,它可能会阻止脚本访问这些数据,即使网络进程已经获得了这些数据。CORB 的目标是在渲染之前尽早阻止恶意代码获取跨域数据。



    CORB 是一种安全机制,用于防止跨域请求恶意访问跨域响应的数据。渲染进程会在 CORB 机制的约束下,选择性地将哪些资源送入渲染进程供页面使用。



    例如,一个网页可能通过 AJAX 请求从另一个域的服务器获取数据。虽然某些情况下这样的请求可能会成功,但如果浏览器检测到请求返回的数据可能包含恶意代码或与同源策略冲突,浏览器可能会阻止网页访问返回的数据,以确保用户的安全。


    跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,允许在受控的条件下,不同源的网页能够请求和共享资源。由于浏览器的同源策略限制了跨域请求,CORS 提供了一种方式来解决在 Web 应用中进行跨域数据交换的问题。


    CORS 的基本思想是,服务器在响应中提供一个标头(HTTP 头),指示哪些源被允许访问资源。浏览器在发起跨域请求时会先发送一个预检请求(OPTIONS 请求)到服务器,服务器通过设置适当的 CORS 标头来指定是否允许跨域请求,并指定允许的请求源、方法、标头等信息。


    简单请求


    不会触发 CORS 预检请求。这样的请求为 简单请求,。若请求满足所有下述条件,则该请求可视为 简单请求



    1. HTTP 方法限制:只能使用 GET、HEAD、POST 这三种 HTTP 方法之一。如果请求使用了其他 HTTP 方法,就不再被视为简单请求。

    2. 自定义标头限制:请求的 HTTP 标头只能是以下几种常见的标头:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。HTML 头部 header field 字段:DPR、Download、Save-Data、Viewport-Width、WIdth。如果请求使用了其他标头,同样不再被视为简单请求。

    3. 请求中没有使用 ReadableStream 对象。

    4. 不使用自定义请求标头:请求不能包含用户自定义的标头。

    5. 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器;XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问


    预检请求


    非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为 预检请求


    需预检的请求要求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。


    例如我们在掘金上删除一条沸点:


    20230822094049


    它首先会发起一个预检请求,预检请求的头信息包括两个特殊字段:



    • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 POST。

    • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 content-type,x-secsdk-csrf-token

    • access-control-allow-origin:在上述例子中,表示 https://juejin.cn 可以请求数据,也可以设置为* 符号,表示统一任意跨源请求。

    • access-control-max-age:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是 1 天(86408 秒),即允许缓存该条回应 1 天(86408 秒),在此期间,不用发出另一条预检请求。


    一旦服务器通过了 预检请求,以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。


    20230822122441


    上面头信息中,Access-Control-Allow-Origin 字段是每次回应都必定包含的。


    附带身份凭证的请求与通配符


    在响应附带身份凭证的请求时:



    • 为了避免恶意网站滥用 Access-Control-Allow-Origin 头部字段来获取用户敏感信息,服务器在设置时不能将其值设为通配符 *。相反,应该将其设置为特定的域,例如:Access-Control-Allow-Origin: https://juejin.cn。通过将 Access-Control-Allow-Origin 设置为特定的域,服务器只允许来自指定域的请求进行跨域访问。这样可以限制跨域请求的范围,避免不可信的域获取到用户敏感信息。

    • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Headers 的值设为通配符 *。这是因为不受限制的请求头可能被滥用。相反,应该将其设置为一个包含标头名称的列表,例如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type。通过将 Access-Control-Allow-Headers 设置为明确的标头名称列表,服务器可以限制哪些自定义请求头是允许的。只有在允许的标头列表中的头部字段才能在跨域请求中被接受。

    • 为了避免潜在的安全风险,服务器不能将 Access-Control-Allow-Methods 的值设为通配符 *。这样做将允许来自任意域的请求使用任意的 HTTP 方法,可能导致滥用行为的发生。相反,应该将其设置为一个特定的请求方法名称列表,例如:Access-Control-Allow-Methods: POST, GET。通过将 Access-Control-Allow-Methods 设置为明确的请求方法列表,服务器可以限制哪些方法是允许的。只有在允许的方法列表中的方法才能在跨域请求中被接受和处理。

    • 对于附带身份凭证的请求(通常是 Cookie),


    这是因为请求的标头中携带了 Cookie 信息,如果 Access-Control-Allow-Origin 的值为 *,请求将会失败。而将 Access-Control-Allow-Origin 的值设置为 https://juejin。cn,则请求将成功执行。


    另外,响应标头中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。


    参考文章



    总结


    预检请求是在进行跨域资源共享 CORS 时,由浏览器自动发起的一种 OPTIONS 请求。它的存在是为了保障安全,并允许服务器决定是否允许跨域请求。


    跨域请求是指在浏览器中向不同域名、不同端口或不同协议的资源发送请求。出于安全原因,浏览器默认禁止跨域请求,只允许同源策略。而当网页需要进行跨域请求时,浏览器会自动发送一个预检请求,以确定是否服务器允许实际的跨域请求。


    预检请求中包含了一些额外的头部信息,如 Origin 和 Access-Control-Request-Method 等,用于告知服务器实际请求的方法和来源。服务器收到预检请求后,可以根据这些头部信息,进行验证和授权判断。如果服务器认可该跨域请求,将返回一个包含 Access-Control-Allow-Origin 等头部信息的响应,浏览器才会继续发送实际的跨域请求。


    使用预检请求机制可以有效地防范跨域请求带来的安全风险,保护用户数据和隐私。


    整个完整的请求流程有如下图所示:


    20230822122544


    最后分享两个我的两个开源项目,它们分别是:



    这两个项目都会一直维护的,如果

    作者:Moment
    来源:juejin.cn/post/7269952188927017015
    你也喜欢,欢迎 star 🥰🥰🥰

    收起阅读 »

    网易云音乐 Tango 低代码引擎正式开源!

    web
    📝 Tango 简介 Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供...
    继续阅读 »

    📝 Tango 简介


    Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对源代码的修改。借助于 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成。


    Tango 低代码引擎开发效果


    如上图所示,Tango 低代码引擎支持可视化视图与源码双向同步,双向互转,为开发者提供 LowCode+ ProCode 无缝衔接的开发体验。


    ✨ 核心特性



    • 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等

    • 基于源码 AST 驱动,无私有 DSL 和协议

    • 提供实时出码能力,支持源码进,源码出

    • 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件

    • 使用 TypeScript 开发,提供完整的类型定义文件


    🏗️ 基于源码的低代码搭建方案


    Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的 基于 Schema 驱动的低代码方案 相比,不受私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。



    📄 源码进,源码出


    由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。


    code in, code out


    🏆 产品优势


    与基于私有 Schema 的低代码搭建方案相比,Tango 低代码引擎具有如下优势:


    对比项基于 Schema 的低代码搭建方案Tango(基于源码 AST 转换)
    适用场景面向特定的垂直搭建场景,例如表单,营销页面等🔥 面面向以源码为中心的应用搭建场景
    语言能力依赖私有协议扩展,不灵活,且难以与编程语言能力对齐🔥 直接基于 JavaScript 语言,可以使用所有的语言特性,不存在扩展性问题
    开发能力LowCode🔥 LowCode + ProCode
    源码导出以 Schema 为中心,单向出码,不可逆🔥 以源码为中心,双向转码
    自定义依赖需要根据私有协议扩展封装,定制成本高🔥 原有组件可以无缝低成本接入
    集成研发设施定制成本高,需要额外定制🔥 低成本接入,可以直接复用原有的部署发布能力

    📐 技术架构


    Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。此外,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。


    具体的技术架构如下图所示:


    low-code engine


    ⏰ 开源里程碑


    Tango 低代码引擎是网易云音乐内部低代码平台的核心构件,开源涉及到大量的核心逻辑解藕的工作,这将给我们正常的工作带来大量的额外工作,因此我们计划分阶段推进 Tango 低代码引擎的开源事项。



    1. 今天我们正式发布 Tango 低代码引擎的第一个社区版本,该版本将会包括 Tango 低代码引擎的核心代码库,TangoBoot 应用框架,以及基于 antd v4 适配的低代码组件库。

    2. 我们计划在今年的 9 月 30 日 发布低代码引擎的 1.0 Beta 版本,该版本将会对核心的实现面向社区场景重构,移除掉我们在云音乐内部的一些兼容代码,并将核心的实现进行重构和优化。

    3. 我们计划在今年的 10 月 30 日 发布低代码引擎的 1.0 RC 版本,该版本将会保证核心 API 基本稳定,不再发生 BREAKING CHANGE,同时我们将会提供完善翔实的开发指南、部署文档、和演示应用。

    4. 正式版本我们将在 2023 年 Q4 结束前 发布,届时我们会进一步完善我们的开源社区运营机制。


    milestones


    🤝 社区建设


    我们的开源工作正在积极推进中,可以通过如下的信息了解到我们的最新进展:



    欢迎大家加入到我们的社区中来,一起参与到 Tango 低代码引擎的开源建设中来。有任何问题都可以通过 Github Issues 反馈给我们,我们会及时跟进处理。


    💗 致谢


    感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。


    感谢 CodeSandbox 提供的 Sandpack 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。

    作者:网易云音乐技术团队
    来源:juejin.cn/post/7273051203562749971

    收起阅读 »

    前端比localStorage存储还大的本地存储方案

    产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。 方案选择既然要存储的数量大,得排除cookielocalStorage,虽然比cookie多,但是同样有上限(5M)左右,备选websql 使用简单,存储量大,兼容性差,备选index...
    继续阅读 »

    产品的原话就是“要又大又全”。既然存储量大,也要覆盖全多种设备多种浏览器。


    方案选择

    • 既然要存储的数量大,得排除cookie
    • localStorage,虽然比cookie多,但是同样有上限(5M)左右,备选
    • websql 使用简单,存储量大,兼容性差,备选
    • indexDB api多且繁琐,存储量大、高版本浏览器兼容性较好,备选

    既然罗列了一些选择,都没有十全十美的,那么有没有一种能够集合这多种方式的插件呢?渐进增强 or 优雅降级 的存在
    冲着这个想法,就去github和谷歌找了一下,还真的有这么一个插件。


    那就是 localforage


    localforage


    localForage 是一个 JavaScript 库,只需要通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。 




    关于兼容性


    localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。下面是 indexDB、web sql、localStorage 的一个浏览器支持情况,可以发现,兼容性方面loaclForage基本上满足99%需求


    使用


    解决了兼容性和存储量的点,我们就来看看localforage的基础用法


    安装

    # 通过 npm 安装:
    npm install localforage
    // 直接引用
    <script src="localforage.js"></script>
    <script>console.log('localforage is: ', localforage);</script>

    获取存储


    getItem(key, successCallback)


    从仓库中获取 key 对应的值并将结果提供给回调函数。如果 key 不存在,getItem() 将返回 null。

    localforage.getItem('somekey').then(function(value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 回调版本:
    localforage.getItem('somekey', function(err, value) {
    // 当离线仓库中的值被载入时,此处代码运行
    console.log(value);
    });

    设置存储


    setItem(key, value, successCallback)


    将数据保存到离线仓库。你可以存储如下类型的 JavaScript 对象:

    • Array
    • ArrayBuffer
    • Blob
    • Float32Array
    • Float64Array
    • Int8Array
    • Int16Array
    • Int32Array
    • Number
    • Object
    • Uint8Array
    • Uint8ClampedArray
    • Uint16Array
    • Uint32Array
    • String
    localforage
    .setItem("somekey", "some value")
    .then(function (value) {
    // 当值被存储后,可执行其他操作
    console.log(value);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 不同于 localStorage,你可以存储非字符串类型
    localforage
    .setItem("my array", [1, 2, "three"])
    .then(function (value) {
    // 如下输出 `1`
    console.log(value[0]);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    // 你甚至可以存储 AJAX 响应返回的二进制数据
    req = new XMLHttpRequest();
    req.open("GET", "/photo.jpg", true);
    req.responseType = "arraybuffer";

    req.addEventListener("readystatechange", function () {
    if (req.readyState === 4) {
    // readyState 完成
    localforage
    .setItem("photo", req.response)
    .then(function (image) {
    // 如下为一个合法的 <img> 标签的 blob URI
    var blob = new Blob([image]);
    var imageURI = window.URL.createObjectURL(blob);
    })
    .catch(function (err) {
    // 当出错时,此处代码运行
    console.log(err);
    });
    }
    });

    删除存储


    removeItem(key, successCallback)


    从离线仓库中删除 key 对应的值。

    localforage.removeItem('somekey').then(function() {
    // 当值被移除后,此处代码运行
    console.log('Key is cleared!');
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    清空存储


    clear(successCallback)


    从数据库中删除所有的 key,重置数据库。


    localforage.clear() 将会删除离线仓库中的所有值。谨慎使用此方法。

    localforage.clear().then(function() {
    // 当数据库被全部删除后,此处代码运行
    console.log('Database is now empty.');
    }).catch(function(err) {
    // 当出错时,此处代码运行
    console.log(err);
    });

    localforage是否万事大吉?


    用上了localforage一开始我也以为可以完全满足万恶的产品了,然而。。。翻车了.。


    内存不足的前提下,localforage继续缓存会怎么样?


    在这种状态下,尝试使用localforage,不出意外,抛错了 QuotaExceededError 的 DOMErro


    解决
    存储数据的时候加上存储的时间戳和模块标识,加时间戳一起存储

    setItem({
    value: '1',
    label: 'a',
    module: 'a',
    timestamp: '11111111111'
    })

    • 如果是遇到存储使用报错的情况,try/catch捕获之后,通过判断报错提示,去执行相应的操作,遇到内存不足的情况,则根据时间戳和模块标识清理一部分旧数据(内存不足的情况还是比较少的)
    • 在用户手机上产生脏数据的情况,想要清理的这种情况的 处理方式是:
    • 让后端在用户信息接口里面加上缓存有效期时间戳,当该时间戳存在,则前端会进行一次对本地存储扫描
    • 在有效期时间戳之前的数据,结合模块标识,进行清理,清理完毕后调用后端接口上报清理日志
    • 模块标识的意义是清理数据的时候,可以按照模块去清理(选填)

    作者:前端成长指南
    链接:https://juejin.cn/post/7273028474973012007
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    如何消除异步的传染性

    web
    本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。 前言 各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想...
    继续阅读 »

    本文的知识点是笔者在抖音看到 渡一前端 发布的视频学习到的。笔者觉得处理问题的思路非常值得学习,因此来掘金分享一下。



    前言


    各位掘友好!本文跟大家分享一个关于 消除异步传染性 的知识点,你可能不熟悉甚至没听过异步传染性这个词,其实笔者也是最近才看到的,因此想着来分享一下!好了,接下来笔者会从两个方面来说这个知识点,一方面是概念,另一方面就是如何消除。


    什么是 异步传染性


    笔者通过一个例子来介绍异步传染性的概念。


    CleanShot 2023-08-30 at <a href=10.10.55@2x.png" loading="lazy" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/448eaad1c5f34f319bc3361fcad882ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1234&h=528&s=154964&e=png&b=282a35"/>


    上图中由于m2中的fetch是异步的,导致了使用m2m1变成了async functionmain 又使用了m1,从而main也变成了async function。类似这种现象就叫做异步的传染性。(可能你会觉得,为什么main不直接调m2,我们此处是为了理解这个概念,不要钻牛角尖😁)


    m2就好像病毒🦠,m1明知道到m2有毒,还要来挨着,结果就被传染了,main也是一样。


    那什么是消除传染性呢?就是希望不要 async/await,让mian、m1变成纯函数调用。也就是mian、m1不依赖fetch的状态。期望像下面这样调用:
    CleanShot 2023-08-30 at <a href=10.52.24@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ba99c7db117e46319d778002889c51ee~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=924&h=498&s=64284&e=png&b=282a35"/>



    纯函数:



    1. 输入决定输出: 纯函数的输出完全由输入决定,即相同的输入始终产生相同的输出。这意味着函数不依赖于外部状态,也不会对外部状态进行修改。

    2. 没有副作用: 纯函数没有副作用,即在函数的执行过程中不会对除函数作用域外的其他部分产生影响。它不会修改全局变量、改变输入参数或进行文件IO等操作。




    纯函数在函数式编程中具有重要作用,因为它们易于理解、测试和维护。由于不依赖于外部状态,纯函数可以很好地并行执行,也有助于避免常见的错误,例如竞态条件和不确定性行为。



    接下来咱们就分析一下要如何实现消除。


    如何消除


    当我们把async/await去掉之后,就变成了同步调用,那么m2返回的肯定是pending状态的promisemain得到的也是,肯定达不到我们想要的效果。


    那我们能不能等promise变成fulfilled/rejected状态再接着执行main


    可以,第一次调用main,我们直接throw,第一次调用就会终止,然后等promise变成fulfilled/rejected状态,我们将返回结果或错误信息缓存一下,再调用一次main,再次调用时存在缓存,直接返回缓存即可,此时也就变成了同步。流程图如下:


    CleanShot 2023-08-30 at <a href=11.30.26@2x.png" loading="lazy" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc591e6d9bf24b4eb0d3e78fecf5dcc1~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1590&h=1048&s=165391&e=png&b=fdfdfd"/>


    具体实现如下:
    CleanShot 2023-08-30 at <a href=11.34.06@2x.png" loading="lazy" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9b4193e4c6c4a35850c487f1ad0bcbc~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp#?w=1494&h=1336&s=353606&e=png&b=282a35"/>


    效果如下:
    CleanShot 2023-08-30 at 11.44.35.gif


    到此本次分享的内容就完了,感谢阅读!


    总结


    本文通过简单的例子,描述了什么是异步的传染性,以及如何利用缓存throw重写fetch实现了消除异步的传染性


    如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^<

    作者:Lvzl
    来源:juejin.cn/post/7272751454497996815
    /code>。

    收起阅读 »

    基于 Axios 封装一个完美的双 token 无感刷新

    web
    用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。 标识登录状态的方案有两种: session 和 jwt。 session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出...
    继续阅读 »

    用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。


    标识登录状态的方案有两种: session 和 jwt。


    session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。



    jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。



    这个 token 一般是放在一个叫 authorization 的 header 里。


    这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。


    session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有。



    jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。



    所以 jwt 的方案用的还是很多的。


    服务端把用户信息放入 token 里,设置一个过期时间,客户端请求的时候通过 authorization 的 header 携带 token,服务端验证通过,就可以从中取到用户信息。


    但是这样有个问题:


    token 是有过期时间的,比如 3 天,那过期后再访问就需要重新登录了。


    这样体验并不好。


    想想你在用某个 app 的时候,用着用着突然跳到登录页了,告诉你需要重新登录了。


    是不是体验很差?


    所以要加上续签机制,也就是延长 token 过期时间。


    主流的方案是通过双 token,一个 access_token、一个 refresh_token。


    登录成功之后,返回这两个 token:



    访问接口时带上 access_token 访问:



    当 access_token 过期时,通过 refresh_token 来刷新,拿到新的 access_token 和 refresh_token



    这里的 access_token 就是我们之前的 token。


    为什么多了个 refresh_token 就能简化呢?


    因为如果你重新登录,是不是需要再填一遍用户名密码?而有了 refresh_token 之后,只要带上这个 token 就能标识用户,不需要传用户名密码就能拿到新 token。


    而 access_token 一般过期时间设置的比较短,比如 30 分钟,refresh_token 设置的过期时间比较长,比如 7 天。


    这样,只要你 7 天内访问一次,就能刷新 token,再续 7 天,一直不需要登录。


    但如果你超过 7 天没访问,那 refresh_token 也过期了,就需要重新登录了。


    想想你常用的 APP,是不是没再重新登录过?


    而不常用的 APP,再次打开是不是就又要重新登录了?


    这种一般都是双 token 做的。


    知道了什么是双 token,以及它解决的问题,我们来实现一下。


    新建个 nest 项目:


     npx nest new token-test


    进入项目,把它跑起来:


    npm run start:dev

    访问 http://localhost:3000 可以看到 hello world,代表服务跑成功了:



    在 AppController 添加一个 login 的 post 接口:



    @Post('login')
    login(@Body() userDto: UserDto) {
    console.log(userDto);
    return 'success';
    }

    这里通过 @Body 取出请求体的内容,设置到 dto 中。


    dto 是 data transfer object,数据传输对象,用来保存参数的。


    我们创建 src/user.dto.ts


    export class UserDto {
    username: string;
    password: string;
    }

    在 postman 里访问下这个接口:



    返回了 success,服务端也打印了收到的参数:



    然后我们实现下登录逻辑:



    这里我们就不连接数据库了,就是内置几个用户,匹配下信息。


    const users = [
    { username: 'guang', password: '111111', email: 'xxx@xxx.com'},
    { username: 'dong', password: '222222', email: 'yyy@yyy.com'},
    ]

    @Post('login')
    login(@Body() userDto: UserDto) {
    const user = users.find(item => item.username === userDto.username);

    if(!user) {
    throw new BadRequestException('用户不存在');
    }

    if(user.password !== userDto.password) {
    throw new BadRequestException("密码错误");
    }

    return {
    userInfo: {
    username: user.username,
    email: user.email
    },
    accessToken: 'xxx',
    refreshToken: 'yyy'
    };
    }

    如果没找到,就返回用户不存在。


    找到了但是密码不对,就返回密码错误。


    否则返回用户信息和 token。


    测试下:


    当 username 不存在时:



    当 password 不对时:



    登录成功时:



    然后我们引入 jwt 模块来生成 token:


    npm install @nestjs/jwt

    在 AppModule 里注册下这个模块:



    JwtModule.register({
    secret: 'guang'
    })

    然后在 AppController 里就可以注入 JwtService 来用了:



    @Inject(JwtService)
    private jwtService: JwtService

    这个是 nest 的依赖注入功能。


    然后用这个 jwtService 生成 access_token 和 refresh_token:



    const accessToken = this.jwtService.sign({
    username: user.username,
    email: user.email
    }, {
    expiresIn: '0.5h'
    });

    const refreshToken = this.jwtService.sign({
    username: user.username
    }, {
    expiresIn: '7d'
    })

    access_token 过期时间半小时,refresh_token 过期时间 7 天。


    测试下:



    登录之后,访问别的接口只要带上这个 access_token 就好了。


    前面讲过,jwt 是通过 authorization 的 header 携带 token,格式是 Bearer xxxx


    也就是这样:



    我们再定义个需要登录访问的接口:


    @Get('aaa')
    aaa(@Req() req: Request) {
    const authorization = req.headers['authorization'];

    if(!authorization) {
    throw new UnauthorizedException('用户未登录');
    }
    try{
    const token = authorization.split(' ')[1];
    const data = this.jwtService.verify(token);

    console.log(data);
    } catch(e) {
    throw new UnauthorizedException('token 失效,请重新登录');
    }
    }

    接口里取出 authorization 的 header,如果没有,说明没登录。


    然后从中取出 token,用 jwtService.verify 校验下。


    如果校验失败,返回 token 失效的错误,否则打印其中的信息。


    试一下:


    带上 token 访问这个接口:



    服务端打印了 token 中的信息,这就是我们登录时放到里面的:



    试一下错误的 token:



    然后我们实现刷新 token 的接口:


    @Get('refresh')
    refresh(@Query('token') token: string) {
    try{
    const data = this.jwtService.verify(token);

    const user = users.find(item => item.username === data.username);

    const accessToken = this.jwtService.sign({
    username: user.username,
    email: user.email
    }, {
    expiresIn: '0.5h'
    });

    const refreshToken = this.jwtService.sign({
    username: user.username
    }, {
    expiresIn: '7d'
    })

    return {
    accessToken,
    refreshToken
    };

    } catch(e) {
    throw new UnauthorizedException('token 失效,请重新登录');
    }
    }

    定义了个 get 接口,参数是 refresh_token。


    从 token 中取出 username,然后查询对应的 user 信息,再重新生成双 token 返回。


    测试下:


    登录之后拿到 refreshToken:



    然后带上这个 token 访问刷新接口:



    返回了新的 token,这种方式也叫做无感刷新。


    那在前端项目里怎么用呢?


    我们新建个 react 项目试试:


    npx create-react-app --template=typescript token-test-frontend


    把它跑起来:


    npm run start


    因为 3000 端口被占用了,这里跑在了 3001 端口。



    成功跑起来了。


    我们改下 App.tsx


    import { useCallback, useState } from "react";

    interface User {
    username: string;
    email?: string;
    }

    function App() {
    const [user, setUser] = useState<User>();

    const login = useCallback(() => {
    setUser({username: 'guang', email: 'xx@xx.com'});
    }, []);

    return (
    <div className="App">
    {
    user?.username
    ? `当前登录用户: ${ user?.username }`
    : <button onClick={login}>登录button>

    }
    div>
    );
    }

    export default App;

    如果已经登录,就显示用户信息,否则显示登录按钮。


    点击登录按钮,会设置用户信息。


    这里的 login 方法因为作为参数了,所以用 useCallback 包裹下,避免不必要的渲染。



    然后我们在 login 方法里访问登录接口。


    首先要在 nest 服务里开启跨域支持:



    在 main.ts 里调用 enbalbeCors 开启跨域。


    然后在前端代码里访问下这个接口:


    先安装 axios


    npm install --save axios

    然后创建个 interface.ts 来管理所有接口:


    import axios from "axios";

    const axiosInstance = axios.create({
    baseURL: 'http://localhost:3000/',
    timeout: 3000
    });

    export async function userLogin(username: string, password: string) {
    return await axiosInstance.post('/login', {
    username,
    password
    });
    }

    async function refreshToken() {

    }
    async function aaa() {

    }

    在 App 组件里调用下:


    const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    console.log(res.data);
    }, []);

    接口调用成功了,我们拿到了 userInfo、access_token、refresh_token



    然后我们把 token 存到 localStorage 里,因为后面还要用。


    const login = useCallback(async () => {
    const res = await userLogin('guang', '111111');

    const { userInfo, accessToken, refreshToken } = res.data;

    setUser(userInfo);

    localStorage.setItem('access_token', accessToken);
    localStorage.setItem('refresh_token', refreshToken);
    }, []);


    在 interface.ts 里添加 aaa 接口:


    export async function aaa() {
    return await axiosInstance.get('/aaa');
    }

    组件里访问下:



    const xxx = useCallback(async () => {
    const res = await aaa();

    console.log(res);
    }, []);


    点击 aaa 按钮,报错了,因为接口返回了 401。


    因为访问接口时没带上 token,我们可以在 interceptor 里做这个。


    interceptor 是 axios 提供的机制,可以在请求前、响应后加上一些通用处理逻辑:



    添加 token 的逻辑就很适合放在 interceptor 里:



    axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
    config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
    })

    现在再点击 aaa 按钮,接口就正常响应了:



    因为 axios 的拦截器里给它带上了 token:



    那当 token 失效的时候,刷新 token 的逻辑在哪里做呢?


    很明显,也可以放在 interceptor 里。


    比如我们改下 localStorage 里的 access_token,手动让它失效。



    这时候再点击 aaa 按钮,提示的就是 token 失效的错误了:



    我们在 interceptor 里判断下,如果失效了就刷新 token:


    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    async (error) => {
    let { data, config } = error.response;

    if (data.statusCode === 401 && !config.url.includes('/refresh')) {

    const res = await refreshToken();

    if(res.status === 200) {
    return axiosInstance(config);
    } else {
    alert(data || '登录过期,请重新登录');
    }
    } else {
    return error.response;
    }
    }
    )

    async function refreshToken() {
    const res = await axiosInstance.get('/refresh', {
    params: {
    token: localStorage.getItem('refresh_token')
    }
    });
    localStorage.setItem('access_token', res.data.accessToken);
    localStorage.setItem('refresh_token', res.data.refreshToken);
    return res;
    }

    响应的 interceptor 有两个参数,当返回 200 时,走第一个处理函数,直接返回 response。


    当返回的不是 200 时,走第二个处理函数 ,判断下如果返回的是 401,就调用刷新 token 的接口。


    这里还要排除下 /refresh 接口,也就是刷新失败不继续刷新。


    刷新 token 成功,就重发之前的请求,否则,提示重新登录。


    其他错误直接返回。


    刷新 token 的接口里,我们拿到新的 access_token 和 refresh_token 后,更新本地的 token。


    测试下:


    我手动改了 access_token 让它失效后,点击 aaa 按钮,发现发了三个请求:



    第一次访问 aaa 接口返回 401,自动调了 refresh 接口来刷新,之后又重新访问了 aaa 接口。


    这样,基于 axios interceptor 的无感刷新 token 就完成了。


    但现在还不完美,比如点击按钮的时候,我同时调用了 3 次 aaa 接口:



    这时候三个接口用的 token 都失效了,会刷新几次呢?



    是 3 次。


    多刷新几次也没啥,不影响功能。


    但做的再完美一点可以处理下:



    加一个 refreshing 的标记,如果在刷新,那就返回一个 promise,并且把它的 resolve 方法还有 config 加到队列里。


    当 refresh 成功之后,重新发送队列中的请求,并且把结果通过 resolve 返回。


    interface PendingTask {
    config: AxiosRequestConfig
    resolve: Function
    }
    let refreshing = false;
    const queue: PendingTask[] = [];

    axiosInstance.interceptors.response.use(
    (response) => {
    return response;
    },
    async (error) => {
    let { data, config } = error.response;

    if(refreshing) {
    return new Promise((resolve) => {
    queue.push({
    config,
    resolve
    });
    });
    }

    if (data.statusCode === 401 && !config.url.includes('/refresh')) {
    refreshing = true;

    const res = await refreshToken();

    refreshing = false;

    if(res.status === 200) {

    queue.forEach(({config, resolve}) => {
    resolve(axiosInstance(config))
    })

    return axiosInstance(config);
    } else {
    alert(data || '登录过期,请重新登录');
    }
    } else {
    return error.response;
    }
    }
    )

    axiosInstance.interceptors.request.use(function (config) {
    const accessToken = localStorage.getItem('access_token');

    if(accessToken) {
    config.headers.authorization = 'Bearer ' + accessToken;
    }
    return config;
    })

    测试下:



    现在就是并发请求只 refresh 一次了。


    这样,我们就基于 axios 的 interceptor 实现了完美的双 token 无感刷新机制。


    总结


    登录状态的标识有 session 和 jwt 两种方案。


    session 是通过 cookie 携带 sid,关联服务端的 session,用户信息保存在服务端。


    jwt 是 token 保存用户信息,在 authorization 的 header 里通过 Bearer xxx 的方式携带,用户信息保存在客户端。


    jwt 的方式因为天然支持分布式,用的比较多。


    但是只有一个 token 会有过期后需要重新登录的问题,为了更好的体验,一般都是通过双 token 来做无感刷新。


    也就是通过 access_token 标识用户身份,过期时通过 refresh_token 刷新,拿到新 token。


    我们通过 nest 实现了这种双 token 机制,在 postman 里测试了一下。


    在 react 项目里访问这些接口,也需要双 token 机制。我们通过 axios 的 interceptor 对它做了封装。


    axios.request.interceptor 里,读取 localStorage 里的 access_token 放到 header 里。


    axios.response.interceptor 里,判断返回的如果是 401 就调用刷新接口刷新 token,之后重发请求。


    我们还支持了并发请求时,如果 token 过期,会把请求放到队列里,只刷新一次,刷新完批量重发请求。


    这样,就是一个基于 Axios 的完美的双 token 无感刷新了。

    作者:zxg_神说要有光
    来源:juejin.cn/post/7271139265442021391

    收起阅读 »

    求求别再叫我切图仔了,我是前端开发!

    web
    ☀️ 前言 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。 群友2: 没有耕坏的田,只有累死...
    继续阅读 »

    ☀️ 前言





    • 大家好我是小卢,前几天在群里见到有群友抱怨一周内要完成这么一个大概20~30页的小程序。



      • 群友: 这20多个页面一个星期让我开发完,我是不相信😮‍💨。

      • 群友1: 跑吧,这公司留着没用了,不然就只有自己加班。

      • 群友2: 没有耕坏的田,只有累死的牛啊,老哥!🐮。

      • 群友3: 用CodeFun啊,分分钟解决你这种外包需求。

      • 群友2: 对喔!可以试一下CodeFun,省下来的时间开黑去。




    • 在我印象中智能生成页面代码的工具一般都不这么智能,我抱着怀疑的心态去调研了一下CodeFun看看是不是群友们说的这么神奇,试用了过后发现确实挺强大的,所以这次借此机会分享给大家。




    🤔 什么是 CodeFun



    • 大部分公司中我们前端现在的开发工作流大概是下面这几步。

      • 一般会有UI先根据产品提供的原型图产出设计稿。

      • 前端根据设计稿上的标注(大小,边距等)进行编写代码来开发。

      • 开发完后需要给UI走查来确认是不是他/她想要的效果。

      • 如果发现有问题之后又继续重复上面的工作->修改样式->走查。






    • 我们做前端的都知道,重复的东西都可以封装成组件来复用,而上面这种重复的劳作是我们最不想去做的。

    • 但是因为设计图的精细可能有时候会有1px的差异就会让产品UI打回重新编写代码的情况,久而久之就严重影响了开发效率。

    • 我时常会有这么一种疑惑,明明设计稿上都有样式了,为什么还要我重新手写一遍呢?那么有没有一种可能我们可以直接通过设计稿就自动生成代码呢?

    • 有的!通过我的调研过后发现,发现确实CodeFun在同类产品中更好的解决了我遇到的问题。




    • CodeFun是一款 UI 设计稿智能生成源代码的工具,可以将 SketchPhotoshopFigma 的设计稿智能转换为前端源代码。

    • 8 小时工作量,10 分钟完成是它的slogan,它可以精准还原设计稿,不再需要反复 UI 走查,我觉得在使用CodeFun后可以极大地程度减少工作流的复杂度,让我们的工作流变成以下这样:

      • UI设计稿产出。

      • CodeFun产出代码,前端开发略微修改交付。





    🖥 CodeFun 如何使用



    • 接下来我就演示一下如何快速的根据设计稿来产出前端代码,首先我们拿到一个设计稿,这里我就在网上搜了一套Figma的设计稿来演示。

    • 我们在Figma中安装了一个CodeFun的插件,选择对应CodeFun的项目后点击上传可以看到很轻松的就传到我们的CodeFun项目中,当然除了FigamaCodeFun还支持Sketch,PSD,即时设计等设计稿。

    • 我们随便进入一个页面,引入眼帘的是中间设计稿,而在左侧的列表相当于这个页面的节点,而我们点击一下右上角的生成代码可以看到它通过自己的算法很智能的生成了代码。

    • 我上面选择生成的是React的代码,当然啦,他还有很多种选择微信小程序Vueuni-app等等等等,简直就是多端项目的福音!不止是框架,连Css预处理器都可以选择适合自己的。

    • 将生成的代码复制到编辑器中运行,可以看到对于简单的页面完全不用动脑子,直接就渲染出来我们想要的效果了,如果是很复杂的页面进行一些微调即可,是不是很方便嘿嘿。

    • CodeFun不管是根据你选择的模块进行生成代码还是整页生成代码用户进行复制使用之外,它还提供了代码包下载功能,在下载界面可以选择不同页面,不同框架,不同Css预处理器,不同像素单位

    • 如果是React相关甚至还会帮你把脚手架搭建好,直接下载安装依赖使用即可,有点牛呀。



    🔥 CodeFun 好在哪



    • 笔者在这之前觉得想象中的AI生成前端代码的功能一直都挺简陋,用起来不会到达我的预期,到底能不能解决我的痛点,其实我是有以下固有思想的:

      • 生成代码就是很简单的帮你把HtmlCss写完嘛但是我们不同框架又不能生成。

      • 生成代码的变量名肯定不好看。

      • 生成的代码肯定固定了宽高,不同的手机端看的效果会差很多。

      • 平时习惯了v-for,wx:for,map遍历列表,这种生成代码肯定全部给你平铺出来吧。



    • 但是当我使用过CodeFun之后发现确实他可以解决我们很多的重复编写前端页面代码的场景,而且也打消了我对这类AI生成前端页面代码功能的一些固有思想,就如它的slogan所说:8 小时工作量,10 分钟完成


    多平台、多框架支持



    • 支持 Vue 等主流 Web 开发框架代码输出。

    • 支持微信小程序代码输出,当你选择小程序代码输出时,像素单位会新增一个rpx的选项供大家选择。

    • 使用最简单的复制代码功能,我们可以快速的将我们想要的样式复制到我们的项目中进行使用 。

    • 笔者在使用的过程中一直很好奇下载代码的功能,如果我选择了React难不成还会给我自动生成脚手架?结果一试,还真给我生成了脚手架,只需要安装依赖即可,可以说是很贴心了~。



    循环列表自动输出



    • 我们平时在写一个列表组件的时候都喜欢使用v-for,wx:for,map等遍历输出列表,而CodeFun也做到了这种代码的生成。

    • CodeFun在导入设计稿的时候会自动识别哪些是list组件,当然你也可以手动标记组件为List

    • 然后再开启“将 List 标签输出为循环列表”选项即可自动根据当前选择的框架生成对应的循环遍历语法,确实是很智能了~



    批量数据绑定




    • 在我们平时Coding的过程中都不会把数据写死,而是用变量来代替进行动态渲染,而CodeFun支持批量数据绑定功能,我们可以把任何在页面中看到的元素进行数据绑定和命名修改




    • 就拿上面的循环列表举例吧,在我们一开始识别的Html中,遍历循环了一个typeCards数组,每一个都展示对应的信息,我们可以看到这里一开始是写死的,而我们平时写的时候会将它用变量替代。




    • 我们只需要点击右上角的数据绑定进行可视化修改即可,我们可以看到它的全部写法都改成了变量动态渲染,这就很符合我们平时编码的套路了。





    一键预览功能



    • 有很多同学反馈在之前做小程序的情况下需要将代码编写完整并跑起来的情况下,使用微信的预览功能才可以看到效果,会比较繁琐

    • CodeFun支持直接预览,当我们导入设计稿后,选择右上角的预览功能可以直接生成小程序二维码扫码即可进行预览,好赞!。



    更加舒适的“生成代码”



    • CodeFun生成的代码中是会让人看起来比较舒适的。

      • 变量名可读性会比较强。

      • 布局一般不会固定死宽高,而是使用padding等属性来自适应屏幕百分比

      • 自动处理设计稿中的无用图层、不可见元素、错误的编组乃至不合理的文字排列。

      • 全智能切图,自动分离背景图层、图标元素。




    ✍🏻 一些思考与建议



    • 前端开发不仅仅是一个切图的工具人,如果你一直局限于视图的表现的时候,你的前端水平也就是curd工程师的水平了,我们前端更多的要深入一些性能优化前端插件封装等等有意思的事情🙋🏻。

    • 总之如果你想你的前端水平要更加精进的情况下,可以减少一些在页面上的投入时间,现在的工具越来越成熟,而这些切图工作完全可以交给现有的工具去帮助你完成

    • 在使用体验上来说,CodeFun确实可以解决大部分切图功能,减少大家进行切图的工作时间,大家可以去试一下~但是肯定会有一些小细节不符合自己的想法,表示理解吧,毕竟AI智能生成代码能做成CodeFun这种水平已经很厉害了👍🏻。

    • 在使用建议上来说,我建议大家可以把CodeFun当成一个助手,而不要完全依赖,过度依赖,去找到更合适自己使用CodeFun的使用方法可以大量减少开发时间从而去做👉🏻更有意义的事情。

    • 很多人会很排斥,觉得没自己写的好,但是时代已经变啦~我还是那句话,所有东西都是一个辅助,一个工具,它提供了这些优质的功能而使用的好不好是看使用者本身的,欢迎大家去使用一下CodeFun~支持国产!!




    • 记住我们是前端开发,不是切图仔!做前端,不搬砖!



    作者:快跑啊小卢_
    来源:juejin.cn/post/7145977342861508638

    收起阅读 »

    为了弄清楚几个现象,重新学习了 flex

    web
    flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点: 相对于常规布局(float, position),它具备更高的灵活性; 相对于 grid 布局,它具有更强的兼容性; 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈) ...
    继续阅读 »

    flex 布局作为开发中的常规手段,使用起来简直不要太爽。其特点:



    • 相对于常规布局(float, position),它具备更高的灵活性;

    • 相对于 grid 布局,它具有更强的兼容性;

    • 使用简单,几个属性就能解决常规布局需求(当然 grid 布局也可以哈)


    但是在开发使用 flex 布局的过程中,也会遇到一些自己难以解释的现象;通俗表述:为什么会是这样效果,跟自己想象的不一样啊?


    那么针对自己提出的为什么,自己有去研究过?为什么是这样的效果?如何解决呢?


    自己也存在同样的问题。所以最近有时间,重新学习了一遍 flex,发现自己对 flex 的某些属性了解少之又少,也就导致针对一些现象确实说不清楚。


    下面我就针对自己遇到的几种疑惑现象进行学习,来对 flex 部分属性深入理解。



    每天多问几个为什么,总有想象不到的意外收获 ---我的学习座右铭



    回顾 flex 模型


    flex 的基本知识和基本熟悉就不介绍了,只需要简单的回顾一下 flex 模型。


    在使用 flex 布局的时候,脑海中就要呈现出清晰的 flex 模型,利于正确的使用 flex 属性,进行开发。


    flex1.png

    理解如下的几个概念:



    • 主轴(main axis)

    • 交叉轴(cross axis)

    • flex 容器(flex container)

    • flex 项(flex item)



    main size 也可以简单理解下,后面内容 flex-basis 会涉及到。



    还顺便理解一下 flex-item 的基本特点



    1. flex item 的布局将由 flex container 属性的设置来进行控制的。

    2. flex item 不在严格区分块级元素和行内级元素。

    3. flex item 默认情况下是包裹内容的,但是可以设置的高度和宽度。


    现象一:flex-wrap 换行引起的间距


    关键代码:


    <!-- css -->
    <style>
     .father {
       width: 400px;
       height: 400px;
       background-color: #ddd;
       display: flex;
       flex-wrap: wrap;
    }
     .son {
       width: 120px;
       height: 120px;
    }
    </style>

    <!-- html -->
    <body>
     <div class="father">
       <div class="son" style="background-color: aqua">1</div>
       <div class="son" style="background-color: blueviolet">2</div>
       <div class="son" style="background-color: burlywood">3</div>
       <div class="son" style="background-color: chartreuse">4</div>
     </div>
    </body>

    具体现象:


    flex2.png

    疑惑:为什么使用 flex-wrap 换行后,不是依次排列,而是排列之间存在间距?



    一般来说,父元素的高度不会固定的,而是由内容撑开的。但是我们也不能排除父元素的高度固定这种情况。



    排查问题:针对多行,并且在交叉轴上,不能想到是 align-content 属性的影响。但是又由于代码中根本都没有设置该属性,那么问题肯定出现在该属性的默认值身上。


    那么通过 MDN 查询:


    flex3.png

    align-content 的默认值为 normal,其解释是按照默认位置填充。这里默认位置填充到底代表什么呢,MDN 上没有明确说明。


    但是在 MDN 上查看 align-items 时,却发现了有用的信息(align-items 是单行,align-content 是多行),normal 在不同布局中有不同的表现形式。


    flex4.png

    可以发现,针对弹性盒子,normal 与 stretch 的表现形式一样。


    自己又去测试 align-content,果然发现 normal 和 stretch 的表现形式一样。那么看看 stretch 属性的解释:


    flex6.png

    那么只需简单的需改,去掉 height 属性,那么 height 属性默认值就为 auto。


    <!-- css -->
    <style>
     .son {
       width: 120px;
       /* 注释掉 height */
       /* height: 120px */
    }
    </style>

    看效果:


    flex5.png

    可以发现,子元素被拉伸了,这是子元素在默认情况下应该占据的空间大小。



    这里就需要理解 flex item 的特点之一:flex item 的布局将由 flex container 属性的设置来进行控制的



    那么当子元素设置高度时,是子元素自己把自己的高度限制了,但是并没有改变 flex container 对 flex item 布局占据的空间大小,所以就会多出一点空间,也就是所谓的间距。


    所以针对上面这个案例,换行存在间隔的现象也就理解了,因为第四个元素本身就排布在弹性盒子的正确位置,只是我们把子元素高度固定了,造成的现象是有存在间隔。



    可以想一下,如果子元素的高度加起来大于父元素的高度,又是什么效果呢?可以自己尝试一下,看自己能够解释不?



    现象二:flex item 拉伸?压缩?


    在使用 flex 时,最常见的现象是这样的:


    flex7.png

    当子元素为 3 个时,不会被拉伸,为什么呢?


    当子元素为 6 个事,会被压缩,又是为什么呢?


    其实上面这两个疑问❓,只需了解两个属性:flex-growflex-shrink。因为这两个属性不常用,所以容易忽略,从而不去了解,那么就会造成疑惑。


    flex-grow 属性指定了 flex 元素的拉伸规则。flex 元素当存在剩余空间时,根据 flex-grow 的系数去分配剩余空间。 flex-grow 的默认值为 0,元素不拉伸


    flex-shrink 属性指定了 flex 元素的收缩规则。flex 元素仅在默认宽度之和大于容器的时候才会发生收缩,其收缩的大小是依据 flex-shrink 的值。flex-shrink 的默认值为 1,元素压缩



    该两个属性都是针对 主轴方向的剩余空间



    所以



    • 当子元素数量较少时,存在剩余空间,但是又由于 flex-grow 的值为 0,所以子元素宽度不会进行拉伸。

    • 当子元素数量较多时,空间不足,但是又由于 flex-shrink 的值为 1,那么子元素就会根据相应的计算,来进行压缩。


    特殊场景: 当空间不足时,子元素一定会压缩?试试单词很长(字符串很长)的时候呢?


    flex8.png

    现象三:文本溢出,flex-basis?width?


    在布局中,如果指定了宽度,当内容很长的时候,就会换行。但是会存在一种特殊情况,就是如果一个单词很长为内容时,则不会进行换行;跟汉字是一样的道理,不可能从把汉字分成两半。


    那么在 flex 布局中,会存在两种情况:


    flex9.png

    可以发现:



    • 设置了固定的 width 属性,字符串超出宽度之后,就会截取。

    • 而设置了固定的 flex-basis 属性,字符串超出宽度之后,会自动扩充宽度。


    其实在这里可能有人会有疑惑:为什么把 width 和 flex-basis 进行对比?或者说 flex-basis 这个属性到底是干什么?



    其实我也是刚刚才熟悉到这个属性,哈哈哈,不知道吧!!!



    因为 flex-basis 是使用在 flex item 上,而 flex-basis(主轴上的基础尺寸)属性在大多数情况下跟 width 属性是等价的,都是设置 flex-item 的宽度。


    上面的案例就是属于特殊情况,针对单词超长不换行时,flex-basis 就会表现出不一样的形式,自动扩充宽度


    简单学习一下 flex-basis 的基本语法吧。


    flex-basis 属性值:



    • auto: 默认值,参照自身 width 或者 height 属性。

    • content: 自动尺寸,根据内容撑开。

    • <'width'>: 指定宽度。


    当一个属性同时设置 flex-basis(属性值不为 auto) 和 width 时,flex-basis 具有更高的优先级


    现象四:flex 平分


    当相对父容器里面的子元素进行平分时,我们会毫不犹豫的写出:


    .father {
     width: 400px;
     height: 400px;
     background-color: #ddd;
     display: flex;
    }
    .son {
     flex: 1; /* 平分 */
     height: 90px;
    }

    flex10.png

    那么我们是否会想过为什么会平分空间? 其中 flex:1 起了什么作用?


    我们也许都知道 flex 属性是一个简写,是 flex-growflex-shrinkflex-basis 的简写。所以,flex 的属性值应该是三个组合值。


    但是呢,flex 又类似于 font 属性一样,是一个很多属性的简写,其中一些属性值是可以不用写的,采用其默认值。


    所以 flex 的属性值就会分析三种情况:一个值,两个值,三个值。


    MDN 对其做了总结:


    flex11.png

    看图,规则挺多的,如果要死记的话,还是挺麻烦的。


    针对上面的规则,其实只需要理解 flex 的语法形式,还是能够完全掌握(有公式,谁想背呢)。


    flex = none | auto | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ]  

    希望你能看懂这个语法,很多 api 都有类似的组合。



    • | 表示要么是 none, 要么是 auto, 要么是后面这一坨,三选一。

    • || 逻辑或

    • ? 可选


    理解上面这种语法之后,总结起来就是如下:


    一个值



    1. none(0 0 auto)auto(1 1 auto) 是需要单独记一下的,这个无法避免。

    2. 无单位,就是 flex-grow,因为存在单位,就是 flex-grow 属性值规定为一个 number 类型的

    3. 有单位,就是 flex-basis,因为类似 width 属性是需要单位的,不然没有效果。


    两个值



    1. 无单位,就是 flex-grow 和 flow-shrink,理由如上

    2. 其中一个有单位,就是 flex-grow 和 flex-basis,因为 flex-shrink 是可选的(这种情况是没有任何实际意义的,flex-basis设置了根本无效)。


    三个值


    三个值不用多说,一一对应。


    理解了上面的语法形式,再来看 flex: 1 的含义就轻而易举了。一个值,没有单位,就是 flex-grow,剩余空间平均分配


    现象五:多行,两边对齐布局


    无论是 app 开发,还是网页开发,遇到最多的场景就是这样的:


    flex12.png

    两边对齐,一行元素之间的间距相同;如果一行显示不下,就换行依次对齐排布。


    那么不难想到的就是 flex 布局,会写下如此代码:


    .father {
     display: flex;
     justify-content: space-between;
     flex-wrap: wrap;
    }
    .son {
     width: 90px;
     height: 90px;
    }

    那么你就会遇到如下情况:


    flex13.png

    其中的第二、三种情况布局是不可以接受,数据数量不齐的问题。但是数据是动态的,所以不能避免出现类似情况。


    你们遇到过这种类似的布局吗?会存在这种情况吗?是怎么解决的呢?


    第一种解决方案:硬算


    不使用 flex 的 justify-content 属性,直接算出元素的 margin 间隔。


    .father {
     width: 400px;
     background-color: #ddd;
     display: flex;
     flex-wrap: wrap;
    }
    .son {
     margin-right: calc(40px / 3); /* 40px 为 父元素的宽度 - 子元素的宽度总和,   然后平分剩余空间*/
     width: 90px;
     height: 90px;
     background-color: #5adacd;
     margin-bottom: 10px;
    }
    /* 针对一行最后一个,清空边距 */
    .son:nth-child(4n) {
     margin-right: 0;
    }

    缺点:只要其中的一个宽度发生变化,又要重新计算。


    第二种解决方案:添加空节点


    为什么要添加空节点呢?因为在 flex 布局中,没有严格的区分块级元素和行内元素。那么就可以使用空节点,来占据空间,引导正确的布局。


    <style>
     .father {
       width: 400px;
       background-color: #ddd;
       display: flex;
       justify-content: space-between;
       flex-wrap: wrap;
    }
     .son {
       width: 90px;
       height: 90px;
       background-color: #5adacd;
       margin-bottom: 10px;
    }
     
     /* height 设置为 0 */
     .father span {
       width: 90px; /*空节点也是 flex-item, width 是必须一致的,只是设置高度为0,不占据空间*/
    }
    </style>
    </head>
    <body>
     <div></div>
     <div class="father">
       <div class="son">1</div>
       <div class="son">2</div>
       <div class="son">3</div>
       <div class="son">4</div>
       <div class="son">5</div>
       <div class="son">6</div>
       <div class="son">7</div>
       <!-- 添加空节点,个数为 n-2 -->
       <i></i>
       <i></i>
     </div>
    </body>

    这样也能解决上面的问题。


    添加空节点的个数:n(一行的个数) - 2(行头和行尾,就是类似第一种情况和第四种情况本身就是正常的,就不需要空间点占据)


    缺点:添加了dom节点


    上面两种方案都解决问题,但是都有着各自的缺点,具体采用哪种方式,就看自己的选择了。


    那么你们还有其他的解决方案吗?


    总结


    其实本篇所解释的现象是自己对 flex 知识掌握不牢而造成的,从而记录此篇,提升熟悉度。也希望能够帮助对这些现象有困惑的码友。


    作者:copyer_xyf
    来源:juejin.cn/post/7273025171111444540
    >如果存在错误解释,评论区留言。

    收起阅读 »

    第一份只干了五天的前端工作

    可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。我当时问了五险一金说是按照7000多交,结果公积金按最低2千多我去的第三天,后端管理A,负责给我分配任...
    继续阅读 »

    可能是前面运气太好,工作五年了反而遇到奇怪的操作,首先是一些事情没有提前给我讲,入职当天才知道。

    • 当月工资要在下个月月末发,还会有绩效工资的变动,有减少的可能。
    • 我当时问了五险一金说是按照7000多交,结果公积金按最低2千多
    • 我去的第三天,后端管理A,负责给我分配任务的,他早上和我讲,类似初创公司领导不愿意看到早下班,晚上也会找我过一下进度啥的,要定制一个学习计划,自己找时间去熟悉代码...

    面试过程倒还顺利,前端A问了些技术,比较偏实际应用的,对我github上做的东西也感兴趣。前端管理B问的有些偏人事的,然后问我下午有没有其它安排,没有的话不走流程叫领导A面一下。领导A就直接说了平薪,试用期3个月80%能不能接受,当时那种场景我就顺势而为说可以,我之前的工作都是少一千。


    我往回走的时候hr给我打电话,前面的没接到,我打过去得知是想问我在哪,想问一下总裁有没有时间,不然就要到周六。


    等到周六面完,下周一打电话说通过,让我周三去报到,我说太快要下周一。然后这家公司每个月月末的周六是组织学习培训的,我也参加了。


    第一天搭建环境,账号权限这些,电脑连不上WiFi,我问前台有没有网线,她说没有我也没去下面找,又换了一台,接口就不同了,又弄双屏的转接线。我找她一次,她都是从前台去存放物品的地方,发现不太对再找她已经回前台了,前后不到一分钟的时间吧。确实我对这个也不太熟,有些不好意思麻烦她,最后少一个转接器我就自己买了,否则屏幕显示不清晰,顺便买了两个增高架。


    前端管理B给我说了几个项目,我看了会代码,管理B找我谈了一会。


    九点上班六点下班,上班第一天我看快到六点半了就准备走,这时候管理B拉着我去展厅看了下公司的产品。


    第二天后端管理A请假了,前端管理B给我说了会代码,后面主要负责的项目,跑了下本地联调。


    基本上一个菜单一个项目,每个项目用iframe嵌套,然后有一些组件库这些,代码之间组件嵌套的比较深,多以component。数据走的是配置,流向很乱。接口的传递和返回都很庞大,有些还是json字符串,20-60多个kb,看结构话要单独复制出来。一些项目调试没有sourcemap,给我的感觉就是把简单的事做复杂了。


    第三天后端管理A给我安排了一些事情,就是口头说了一下,意思这种改动不需要ui、不需要产品自己就可以定,基本上就是我做完让他看一下,他觉得不好在改。这里面就有一个问题,就是到底改哪里没有全部列出来,我对项目又不熟,一般都有一个上下文的概念,可对不上的时候,才发现是另一个地方,他又是没有规划的那种。


    往往走配置基本会出现一种情况,就是一些东西需要单独处理,或者配置选项越加越多,或者当初实现的时候偷懒就给写死了,东改一下西改一下,而且这种封装太笨重了,不好优化,只能说熟悉了更快些,但是维护成本始终会处在一个固定的量级,而且随着功能迭代,补丁会越来越多。


    下午的时候管理B把我叫过去,让我协助后台A排查两个问题,说别的环境没有只有这个环境有,据说问题已经存在很久了,我找到问题A的代码所在地、以及问题的原因就花了很长时间,单从前端看,是因为一个代码报错导致的。因为这些东西要按照业务流程来,我不知道什么是对的,只能关注接口的返回然后找对应的前端组件渲染逻辑,对比差异反馈,本质上就是反推接口返回有哪些不对,找到问题已经晚上10点了,两个问题都是后台在处理用户操作后,前后id不一样导致拿到的数据不对所致。


    这种问题让一个刚入职的人排查显然浪费时间,中间链路太多了,我问了下后端A他之前都是和谁对接,得知前面的人离职了,我就想着这种情况是最难受的,总不可能我一上来就能接手他的工作,巧妇难为无米之炊,哪有那么丝滑的过度,总要有个渐进性的过程吧。


    每天要建tapd,再把tapd的内容复制出来写成日报,然后也要写周报,还要在领导有时间的时候找他汇报进度,或者等他来找你。


    第四天,后端管理A给我说了下今天的任务,有一些历史遗留问题,我处理的还是很快的,直到前端写完对接口的时候,他只是钉钉发了一些字段给我,发现还有另一个项目要改,找到代码熟悉,对好逻辑写好前端代码,我又在本地连了下测试环境,跑了下流程,接口报错了,我看六点半了就走了。


    第四天上午还过了个需求,虽然我也听不太懂,但是管理B直接说这个事情15个自然日还是工作日搞完。


    后端管理A评价我的日报,意思任务完不成要及时上报,晚上要和他汇报进度,我想着我都不知道一天到底有多少任务,也不知道完成任务花费多长时间,更不知道啥样算完成,我咋完成?


    第五天,前端管理B找我聊了一会,说是来了一周,我就把我的感触说了,他问我打算怎么处理,我就说这种强度我就不干了,感觉不值,他说他来处理。


    我对比了下我能够得到的和我将要面对的,平薪80%,三个月,我觉得有些不值得,待遇还不如我两年前,也没到山穷水尽的地步,受这罪干嘛?以前入职也不是没有压力大的时候,但待遇有所增长,看了代码啥的我也觉得对我是一种历练,即便不说我也会主动学习,因为我知道当我很熟悉的时候后面效率更高,算提早付出了,还是在时间不那么紧凑的时候,但这家公司给我一种压榨的感觉。


    后面我把项目分支发给后端管理A,部署发版耽搁了一会,后面是找的前端管理B解决的,我后面了解到走的是自己的搭建的运维系统,两个项目有不同的分支名,我把自己的分支手动合并,再找后端管理A就好了。


    接下来就是他发现一些问题让我改,持续到下午五点半左右,我再次提交代码时,发现gtilab账号已经被注销了,他让我把代码改动发给前端管理B,这个时候我的电脑已经重置,被前台收走了。还是有些遗憾,再次改动时发现轻车熟路了许多,前面还是花了不少精力的。


    公司提供了午休床,我贴了个标签,前端管理B贴心的给我个东西增加区分性,但我用过一次后没再找见,离职也是要交给前台的,找的时候还在想不会所有放午休床的地方都要找一遍吧,还好发现了破碎的标签,应该被别人用的时候弄碎了。


    其实周五不聊的话我可能想着再适应下,也没想到当天就能走完离职,清理tapd的时候发现有五十多个bug挂在我这,看到一个六月份的。


    幸亏我带了包过去,不然东西都不好拿,看着8月份4天32小时...。


    可能是太久没工作了吧,我便抱着试一试的态度,坦白的讲我也想过边干边找,入职的这几天有了新的方向,我github上写的工具依旧发挥稳定,替我节省了很多时间。


    作者:这次假期很长
    链接:https://juejin.cn/post/7263653558385868857
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    某法宝网站 js 逆向思路

    web
    本文章只做技术探讨, 请勿用于非法用途。 目标网站 近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。 本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。 网站分析 文章内容 详情页图片 可以看到下载的方式还蛮多的,...
    继续阅读 »

    本文章只做技术探讨, 请勿用于非法用途。



    目标网站


    近期为了深刻学习法律知识, 需要来下载一些法律条文相关的内容。


    本文通过 某法宝网站来下载数据 http://www.pkulaw.com/。


    网站分析


    文章内容


    image.png
    详情页图片


    可以看到下载的方式还蛮多的, 尝试复制全文, 得到内容保留原格式, 所以我选择使用复制全文的方式来得到文章内容。


    同时详情页没有看到明显的反扒措施, 不需要特殊处理。


    列表页


    image.png


    最终决定通过专题分类来获取所有的数据, 然后调试分析接口参数, 这里没有什么加密的参数, 确定需要关注的参数如下:


    {
    "Aggs.SpecialType": "", // 专题类型(编号)
    "VerifyCodeResult": "", // 验证码值(后边讲解)
    "Pager.PageIndex": "2", // 页码
    "RecordShowType": "List", // 显示方式(List 方式显示所有数据, 保持该值即可)
    "Pager.PageSize": 100, // 每页的数量(最大 100)
    }


    这里仅列出了需要关注的参数, 其他参数保持原值即可(需要的话可以自己调试对比参数值确定意义), 当 pageSize 设置为 100 时, 第 3 页之后的数据需要验证码才能查看。


    验证码


    为方便分析验证码的校验方式, 推荐使用无痕模式来调试, 获取验证码之前清空一次 cookie。


    image.png


    image.png


    image.png


    可以看到验证码的流程为:



    1. 请求验证码, 并返回 set-cookie 。

    2. 携带该 cookie 信息并校验验证码, 通过后得到 code。

    3. 携带 code 请求数据, 得到数据。


    开整


    确定了问题后, 就可以开始了, 一个一个解决。


    文章内容


    确定了要用复制的方式来得到数据, 那就分析下他的复制干了点啥。


    image.png


    查看他的页面元素, 发现了这两个东西, 全局搜索后很容易定位到处理函数(找不到的话刷新页面)。


    image.png


    然后接着定位这个 toCopyFulltext() 函数。


    image.png


    找到这个就对了, 然后可以看代码他是用的 jQuery 的选择器来做的, 我们只能来仿造一个页面结构来用它, 这里推荐使用 cheerio(nodejs) 库来做(jsdom 应该也可以?)。


    image.png


    伪造后直接调用就大功告成。


    列表页


    这个问题不大, 问题主要在于验证码。


    image.png


    通过查看页面元素可以得到专题编号。


    验证码


    请求


    // 验证码图片请求返回结果
    {
    "errcode": 0,
    "y": 131,
    "array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10",
    "imgx": 300,
    "imgy": 200,
    "small": "data:image/jpg;base64,...", // 滑块图片
    "normal": "data:image/jpg;base64,..." // 背景图片
    }

    image.png
    得到的 base64 可以用 python 的 PIL 库来解析出来。


    image.png
    然后我解析出来的图片就是这个样子, 基本上就确定了这验证码就是老朋友了, 我们先来把他还原。



    还是简单解释一下这个, 图片被切割为了上下两部分, 每个部分又被切割为了 10 等份, 原图为 300x200, 也就是说这里被切割为 20 张 30x100 的图片, 然后打乱顺序拼接后返回, 我们需要先完成切割然后再按正确的顺序来拼接还原。



    "errcode": 0,
    "y": 131,
    "array": "14,12,11,7,6,2,13,15,9,8,16,0,1,3,4,18,17,5,19,10", // 图片的正确顺序
    "imgx": 300, // 图片宽
    "imgy": 200, // 图片高
    "small": "data:image/jpg;base64,...", // 滑块图片
    "normal": "data:image/jpg;base64,..." // 背景图片

    image.png


    image.png


    还原后的图片就是这个样子了。


    校验


    // 校验接口请求参数
    {
    "act": "check",
    "point": "197", // 缺口位置
    "timespan": "1067", // 滑动耗时
    "datelist": "1,1692848585282|10,1692848585288|20,1692848585295|34,1692848585305|50,1692848585312|58,1692848585320|74,1692848585328|90,1692848585338|95,1692848585344|107,1692848585355|117,1692848585360|124,1692848585371|126,1692848585376|130,1692848585388|133,1692848585393|136,1692848585404|137,1692848585409|138,1692848585416|139,1692848585425|139,1692848585433|140,1692848585441|140,1692848585449|140,1692848585457|141,1692848585465|141,1692848585473|141,1692848585481|142,1692848585489|142,1692848585498|142,1692848585506|143,1692848585514|143,1692848585522|143,1692848585530|144,1692848585539|145,1692848585546|146,1692848585554|146,1692848585562|149,1692848585572|151,1692848585579|154,1692848585589|157,1692848585596|160,1692848585604|161,1692848585611|164,1692848585621|167,1692848585627|170,1692848585639|172,1692848585643|174,1692848585655|177,1692848585659|179,1692848585667|181,1692848585676|182,1692848585683|184,1692848585692|185,1692848585699|186,1692848585710|187,1692848585716|188,1692848585724|189,1692848585732|189,1692848585740|190,1692848585748|191,1692848585758|192,1692848585764|192,1692848585773|193,1692848585781|194,1692848585789|195,1692848585797|195,1692848585806|196,1692848585813|196,1692848585821|197,1692848585829|197,1692848585839|197,1692848585845|197,1692848585855|197,1692848586219"
    } // 滑动轨迹(位置,时间戳)

    需要解决的参数为 point(缺口位置) 及 datelist(滑动轨迹)。


    point

    image.png


    推荐使用 ddddocr 库来识别, 准确率还可以吧, 挺方便的。


    datelist

    image.png


    轨迹方面自己设计算法来吧, 这里可以作为一个参考, 他的 datelist 的长度不固定, 一般也就是一百多些轨迹点吧, 可以通过调整参数来达到效果, 反正就是多测试吧, 这个方法大概有 百分之九十 左右的通过率吧, 暂时够用。


    VerifyCodeResult


    // 校验请求成功后返回数据
    {
    "state": 0,
    "info": "正确",
    "data": 197
    }
    // VerifyCodeResult: YmRmYl8xOTc=

    解决了验证码, 惊喜的发现还是不咋对, 这个返回的 data 明显长得和要用的 VerifyCodeResult 不太像, 就接着来找。


    image.png


    全局搜索, 找到两个 js 文件, 都打上断点来调试。


    image.png


    可以看到我们成功断到, 并得到是由 (new Base64).encode("bdfb_" + y) 这种方式来生成的 code 值, 这个 y 值就是上一步返回的 data, 接下来只需要把 Base64 的代码那里扣下来, 或者自己实现就行了, 方便些, 这里直接抠下来了, 然后拿到 code 带上验证码请求返回的 cookie, 就能正常拿到数据了。


    结语


    整体来说不算困难, 没有什么加密啊混淆啊之类的东西, 确定方向之后很快就能搞定了, 用来练手还是很不错的, 有什么问题欢迎交流, 不知道这个得几年啊。。。



    请洒潘江,各倾陆海云

    作者:Glommer
    来源:juejin.cn/post/7270702261293039635
    尔。


    收起阅读 »

    JS长任务(耗时操作)导致页面卡顿,优化方案大比拼!

    web
    抛出问题 前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因: 什么是长任务? 长任务是指JS代码执行耗时超过50ms,能让...
    继续阅读 »

    抛出问题


    前端是直接和客户交互的界面,前端的流畅性对于客户的体验和留存率至关重要,可以说,前端界面的流畅性是一款产品能不能成功的关键因素之一。要解决界面卡顿,首先我们先要知道造成页面卡顿的原因:




    • 什么是长任务?


      长任务是指JS代码执行耗时超过50ms,能让用户感知到页面卡顿的代码执行流。




    • 长任务为什么会造成页面卡顿?


      UI界面的渲染由UI线程控制,UI线程和JS线程是互斥的,所以在执行JS代码时,UI线程无法工作,就表现出页面卡死状态。




    我们现在来模拟一个长任务,看看它是怎么影响页面流畅性的:



    • 先看效果(GIF),这里我们给div加了个滚动的动画,当我们开始执行长任务后,页面卡住了,等待执行完后才恢复,总耗时3秒左右,记住总耗时,后面会用到。


    动画.gif



    • 再看代码(有点长,主要看JS部分,后面的优化方案代码只展示优化过的JS函数)


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
    .myDiv {
    width: 100px;
    height: 100px;
    margin: 50px;
    background-color: blue;
    position: relative;
    animation: my-animation 5s linear infinite;
    }
    @keyframes my-animation {
    from {
    left: 0%;
    rotate: 0deg;
    }
    to {
    left: 100%;
    rotate: 360deg;
    }
    }
    </style>
    </head>
    <body>
    <div class="myDiv"></div>
    <button onclick="longTask()">执行长任务</button>

    <script>
    // 模拟耗时操作,大概10ms
    function myFunc() {
    const startTime = Date.now();
    while (Date.now() - startTime < 10) {}
    }

    // 长任务,循环执行myFunc300次,耗时3秒左右
    function longTask() {
    console.log("开始长任务");
    const startTime = Date.now();
    for (let i = 0; i < 300; i++) {
    myFunc();
    }
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    </script>
    </body>
    </html>


    本段代码有一个模拟耗时的函数,有一个模拟长任务的函数(调用300次耗时函数),后面的优化方案都会基于这段代码来进行。


    优化方案


    setTimeout 宏任务方案


    第一个优化方案,我们将长任务拆成多个宏任务来执行,这里我们用setTimeout函数。为什么拆成多个宏任务可以优化卡顿问题呢?


    正如我们上文所说,页面卡顿的原因是因为JS执行线程占用了控制权,导致UI线程无法工作。在浏览器的事件轮询(EventLoop)机制中,每一个宏任务执行完之后会将控制权重新交给UI线程,待UI线程执行完渲染任务后,才会继续执行下一个宏任务。浏览器轮询机制流程图如下所示,想要深入了解浏览器轮询机制,可以参考我的另一篇文章:从进程和线程入手,我彻底明白了EventLoop的原理! image.png



    • 先看效果(GIF),执行长任务的同时,页面也很流畅,没有了先前卡顿的感觉,总耗时4.4秒。


    动画.gif



    • 再看代码


    // setTimeout方案 递归,循环300次
    function timeOutTask(i, startTime) {
    setTimeout(() => {
    if (!startTime) {
    console.log("开始长任务");
    i = 0;
    startTime = Date.now();
    }
    if (i === 300) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    return;
    }
    myFunc();
    timeOutTask(++i, startTime);
    });
    }

    把代码改为多个宏任务之后,解决了页面卡顿的问题,但是总耗时比之前多了1.4秒,主要原因是因为递归调用需要不断地向下开栈,会增加开销。当我们每个任务都不依赖于上一个任务的执行结果时,就可以不使用递归,直接使用循环创建宏任务。



    • 先看效果(GIF),耗时缩短到了3.1秒,但是可以看到明显掉帧。


    动画.gif



    • 再看代码


    // setTimeout不递归方案,循环300次
    function timeOutTask2() {
    console.log("开始长任务");
    const startTime = Date.now();

    for (let i = 0; i < 300; i++) {
    setTimeout(() => {
    myFunc();
    if (i === 300 - 1) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    });
    }
    }

    使用300个循环同时创建宏任务后,虽然耗时降低了,但是div滚动会出现明显掉帧,这也是我们不愿意看到的,那执行代码速度和页面流畅度就没办法兼得了吗?很幸运,requestIdleCallback函数可以帮你解决这个难题。


    requestIdleCallback 函数方案


    requestIdleCallback提供了由浏览器决定,在空闲的时候执行队列任务的能力,从而不会影响到UI线程的正常运行,保证了页面的流畅性。


    它的用法也很简单,第一个参数是一个函数,浏览器空闲的时候就会把函数放到队列执行,第二个参数为options,包含一个timeout,则超时时间,即使浏览器非空闲,超时时间到了,也会将任务放到事件队列。
    下面我们把setTimeout替换为requestIdleCallback



    • 先看效果(GIF),耗时3.1秒,也没有出现掉帧的情况。


    动画.gif



    • 再看代码


    // requestIdleCallback不递归方案,循环300次
    function callbackTask() {
    console.log("开始长任务");
    const startTime = Date.now();

    for (let i = 0; i < 300; i++) {
    requestIdleCallback(() => {
    myFunc();
    if (i === 300 - 1) {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    }
    });
    }
    }

    requestIdleCallback解决了setTimeout方案掉帧的问题,这两种方案都需要拆分任务,有没有一种不需要拆分任务,还能不影响页面流畅度的方法呢?Web Worker满足你。


    Web Worker 多线程方案


    WebWorker是运行在后台的javascript,独立于其他脚本,不会影响页面的性能。



    • 先看效果,耗时不到3.1秒,页面也没有受到影响。


    动画.gif



    • 再看代码,需要额外创建一个js文件。(注意,浏览器本地直接运行HTML会被当成跨域,需要开一个服务运行,我使用的http-server)


    task.js 文件代码


    // 模拟耗时
    function myFunc() {
    const startTime = Date.now();
    while (Date.now() - startTime < 10) {}
    }

    // 循环执行300次
    for (let i = 0; i < 300; i++) {
    myFunc();
    }

    // 通知主线程已执行完
    self.postMessage("我执行完啦");

    主文件代码


    // Web Worker 方案
    function workerTask() {
    console.log("开始长任务");
    const startTime = Date.now();
    const worker = new Worker("./task.js");

    worker.addEventListener("message", (e) => {
    console.log(`长任务结束,总耗时:${Date.now() - startTime}ms`);
    });
    }

    WebWorker方案额外增加的耗时很少,也不需要拆分代码,也不会影响页面性能,算是很完美的一种方案了。
    但它也有一些缺点:



    • 浏览器兼容性差

    • 不能访问DOM,即不能更新UI

    • 不能跨域加载JS


    总结


    三种方案中,如果不需要访问DOM的话,我认为最好的方案为WebWorker方案,其次requestIdleCallback方案,最后是setTimeout方案。
    WebWorker和requestIdleCallback属于比较新的特性,并非所有浏览器都支持,所以我们需要先进行判断,代码如下:


    if (typeof Worker !== 'undefined') {
    //使用 WebWorker
    }else if(typeof requestIdleCallback !== 'undefined'){
    //使用 requestIdleCallback
    }else{
    //使用 setTimeout
    }

    希望本文对您有帮助,其他所有代码可在下方直接执行。(WebWorker不支持)

    作者:TuYuHao
    来源:juejin.cn/post/7272632260180377634
    n>

    收起阅读 »

    你看这个圆脸😁,又大又可爱~ (Compose低配版)

    web
    前言 阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新) 在网上看到有人用css写出了下面这种效果;原文链接 我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。 一、Canvas画图 这...
    继续阅读 »


    前言


    阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新)


    在网上看到有人用css写出了下面这种效果;原文链接


    请添加图片描述


    我看到了觉得很好玩,于是用Compose撸了一个随手势移动眼珠的版本。




    一、Canvas画图


    这种笑脸常用的控件肯定实现不了,我们只能用Canvas自己画了


    笑脸


    我们先画脸



    下例当中的size和center都是onDraw 的DrawScope提供的属性,drawCircle则是DrawScope提供的画圆的方法



    Canvas(modifier = modifier
    .size(300.dp),
    onDraw = {

    // 脸
    drawCircle(
    color = Color(0xfffecd00),
    radius = size.width / 2,
    center = center
    )

    })

    属性解释



    • color:填充颜色

    • radius: 半径

    • center: 圆心坐标


    这里我们半径取屏幕宽度一半,圆心取屏幕中心,画出来的脸效果如下


    在这里插入图片描述


    微笑


    微笑是一个弧形,我们使用drawArc来画微笑


    // 微笑
    val smilePadding = size.width / 4

    drawArc(
    color = Color(0xffb57700),
    startAngle = 0f,
    sweepAngle = 180f,
    useCenter = true,
    topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    size = Size(size.width / 2, size.height / 4)
    )

    属性解释



    • color:填充颜色

    • startAngle: 弧形开始的角度,默认以3点钟方向为0度

    • sweepAngle:弧形结束的角度,默认以3点钟方向为0度

    • useCenter :指示圆弧是否要闭合边界中心的标志(上例加不加都无所谓)

    • topLeft :相对于当前平移从0的本地原点偏移,0开始

    • size:要绘制的圆弧的尺寸


    效果如下
    在这里插入图片描述


    眼睛和眼珠子


    眼睛也是drawCircle方法,只是位置不同,这边就不再多做解释


                // 左眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )


    //左眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )



    // 右眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )


    //右眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )


    整个笑脸就画出来了,效果如下


    在这里插入图片描述


    二、跟随手势移动


    在实现功能之前我们需要介绍transformableanimateFloatAsStatetranslate


    transformable


    transformablemodifier用于平移、缩放和旋转的多点触控手势的修饰符,此修饰符本身不会转换元素,只会检测手势。


    animateFloatAsState


    animateFloatAsState 是通过Float状态变化来控制动画 的状态


    知道了这两个玩意过后我们就可以先通过transformable监听手势滑动然后通过translate方法和animateFloatAsState方法组成一个平移动画来实现眼珠跟随手势移动


    完整的代码:


    @Composable
    fun SmileyFaceCanvas(
    modifier: Modifier
    )
    {

    var x by remember {
    mutableStateOf(0f)
    }

    var y by remember {
    mutableStateOf(0f)
    }

    val state = rememberTransformableState { _, offsetChange, _ ->

    x = offsetChange.x
    if (offsetChange.x >50f){
    x = 50f
    }

    if (offsetChange.x < -50f){
    x=-50f
    }

    y = offsetChange.y
    if (offsetChange.y >50f){
    y= 50f
    }

    if (offsetChange.y < -50f){
    y=-50f
    }
    }

    val animTranslateX by animateFloatAsState(
    targetValue = x,
    animationSpec = TweenSpec(1000)
    )

    val animTranslateY by animateFloatAsState(
    targetValue = y,
    animationSpec = TweenSpec(1000)
    )



    Canvas(
    modifier = modifier
    .size(300.dp)
    .transformable(state = state)
    ) {



    // 脸
    drawCircle(
    Color(0xfffecd00),
    radius = size.width / 2,
    center = center
    )

    // 微笑
    val smilePadding = size.width / 4

    drawArc(
    color = Color(0xffb57700),
    startAngle = 0f,
    sweepAngle = 180f,
    useCenter = true,
    topLeft = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    size = Size(size.width / 2, size.height / 4)
    )

    // 左眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )

    translate(left = animTranslateX, top = animTranslateY) {
    //左眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )
    }


    // 右眼
    drawCircle(
    color = Color.White,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 2
    )

    translate(left = animTranslateX, top = animTranslateY) {
    //右眼珠子
    drawCircle(
    color = Color.Black,
    center = Offset(smilePadding * 3, size.height / 2 - smilePadding / 2),
    radius = smilePadding / 4
    )
    }


    }
    }

    为了不让眼珠子从眼眶里蹦出来,我们将位移的范围限制在了50以内,运行效果如下


    在这里插入图片描述




    总结


    通过Canvas中的一些方法配合简单的动画API实

    作者:我怀里的猫
    来源:juejin.cn/post/7272550100139098170
    现了这个眼珠跟随手势移动的笑脸😁

    收起阅读 »

    pdf为什么不能被修改

    web
    PDF简介 PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。 PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进...
    继续阅读 »

    PDF简介



    • PDF是Portable Document Format 的缩写,可翻译为“便携文件格式”,由Adobe System Incorporated 公司在1992年发明。

    • PDF文件是一种编程形式的文档格式,它所有显示的内容,都是通过相应的操作符进行绘制的。

    • PDF基本显示单元包括:文字,图片,矢量图,图片

    • PDF扩展单元包括:水印,电子署名,注释,表单,多媒体,3D

    • PDF动作单元:书签,超链接(拥有动作的单元有很多个,包括电子署名,多媒体等等)


    PDF的优点



    • 一致性:在所有可以打开PDF的机器上,展示的效果是完全一致,不会出现段落错乱、文字乱码这些排版问题。尤其是文档中,本身可以嵌入字体,避免了客户端没有对应字体,而导致文字显示不一致的问题。所以,在印刷行业,绝大多数用的都是PDF格式。

    • 不易修改:用过PDF文件的人,都会知道,对已经保存之后的PDF文件,想要进行重新排版,基本上就不可能的,这就保证了从资料源发往外界的资料,不容易被篡改。

    • 安全性:PDF文档可以进行加密,包括以下几种加密形式:文档打开密码,文档权限密码,文档证书密码,加密的方法包括:RC4,AES,通过加密这种形式,可以达到资料防扩散等目的。

    • 不失真:PDF文件中,使用了矢量图,在文件浏览时,无论放大多少倍,都不会导致使用矢量图绘制的文字,图案的失真。

    • 支持多种压缩方式:为了减少PDF文件的size,PDF格式支持各种压缩方式:asciihex,ascii85,lzw,runlength,ccitt,jbig2,jpeg(DCT),jpeg2000(jpx)

    • 支持多种印刷标准:支持PDF-A,PDF-X


    PDF格式


    根据PDF官方指南,理解PDF格式可以从四个方面下手——Objects(对象)、File structure(物理文件结构)、Document structure(逻辑文件结构)、Content streams(内容流)。


    对象


    物理文件结构




    • 整体上分为文件头(Header)、对象集合(Body)、交叉引用表(Xref table)、文件尾(Trailer)四个部分,结构如图。修改过的PDF结构会有部分变化。




    • 未经修改






    编辑


    img




    • 经修改






    编辑


    img


    文件头



    • 文件头是PDF文件的第一行,格式如下:


    %PDF-1.7

    复制



    • 这是个固定格式,表示这个PDF文件遵循的PDF规范版本,解析PDF的时候尽量支持高版本的规范,以保证支持大多数工具生成的PDF文件。1.7版本支持1.0-1.7之间的所有版本。


    对象集合



    • 这是一个PDF文件最重要的部分,文件中用到的所有对象,包括文本、图象、音乐、视频、字体、超连接、加密信息、文档结构信息等等,都在这里定义。格式如下:


    2 0 obj
    ...
    end obj

    复制



    • 一个对象的定义包含4个部分:前面的2是对象序号,其用来唯一标记一个对象;0是生成号,按照PDF规范,如果一个PDF文件被修改,那这个数字是累加的,它和对象序号一起标记是原始对象还是修改后的对象,但是实际开发中,很少有用这种方式修改PDF的,都是重新编排对象号;obj和endobj是对象的定义范围,可以抽象的理解为这就是一个左括号和右括号;省略号部分是PDF规定的任意合法对象。

    • 可以通过R关键字来引用任何一个对象,比如要引用上面的对象,可以使用2 0 R,需要主意的是,R关键字不仅可以引用一个已经定义的对象,还可以引用一个并不存在的对象,而且效果就和引用了一个空对象一样。

    • 对象主要有下面几种

    • booleam 用关键字true或false表示,可以是array对象的一个元素,或dictionary对象的一个条目。也可以用在PostScript计算函数里面,做为if或if esle的一个条件。

    • numeric


    包括整形和实型,不支持非十进制数字,不支持指数形式的数字。例: 1)整数 123 4567 +111 -2 范围:正2的31次方-1到负的2的31次方 2)实数 12.3 0.8 +6.3 -4.01 -3. +.03 范围:±3.403 ×10的38次方 ±1.175 × 10的-38次方



    • 注意:如果整数超过表示范围将转化成实数,如果实数超过范围就会出错

    • string


    由一系列0-255之间的字节组成,一个string总长度不能超过65535.string有以下两种方式:



    • 十六进制字串 由<>包含起来的一个16进制串,两位表示一个字符,不足两位用0补齐。例: \ 表示AA和BB两个字符 \ 表示AA和B0两个字符

    • 直接字串 由()包含起来的一个字串,中间可以使用转义符"/"。例:(abc) 表示abc (a//) 表示a/ 转义符的定义如下:


    转义字符含义
    /n换行
    /r回车
    /t水平制表符
    /b退格
    /f换页(Form feed (FF))
    /(左括号
    /)右括号
    //反斜杠
    /ddd八进制形式的字符



    • 对象类别(续)




    • name 由一个前导/和后面一系列字符组成,最大长度为127。和string不同的是,name是不可分割的并且是唯一的,不可分割就是说一个name对象就是一个原子,比如/name,不能说n就是这个name的一个元素;唯一就是指两个相同的name一定代表同一个对象。从pdf1.2开始,除了ascii的0,别的都可以用一个#加两个十六进制的数字表示。例: /name 表示name /name#20is 表示name is /name#200 表示name 0




    • array 用[]包含的一组对象,可以是任何pdf对象(包括array)。虽然pdf只支持一维array,但可以通过array的嵌套实现任意维数的array(但是一个array的元素不能超过8191)。例:[549 3.14 false (Ralph) /SomeName]




    • Dictionary 用"<<"和">>"包含的若干组条目,每组条目都由key和value组成,其中key必须是name对象,并且一个dictionary内的key是唯一的;value可以是任何pdf的合法对象(包括dictionary对象)。例: << /IntegerItem 12 /StringItem (a string) /Subdictionary << /Item1 0.4 /Item2 true /LastItem (not!) /VeryLastItem (OK) >> >>




    • stream 由一个字典和紧跟其后面的一组关键字stream和endstream以及这组关键字中间包含一系列字节组成。内容和string很相似,但有区别:stream可以分几次读取,分开使用不同的部分,string必须作为一个整体一次全部读取使用;string有长度限制,但stream却没有这个限制。一般较大的数据都用stream表示。需要注意的是,stream必须是间接对象,并且stream的字典必须是直接对象。从1.2规范以后,stream可以以外部文件形式存在,这种情况下,解析PDF的时候stream和endstream之间的内容就被忽略掉。例: dictionary stream…data…endstreamstream字典中常用的字段如下: 字段名类型值Length整形(必须)关键字stream和endstream之间的数据长度,endstream之前可能会有一个多余的EOL标记,这个不计算在数据的长度中。Filter名字 或 数组(可选)Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。DecodeParms字典 或 数组(可选)一个参数字典或由参数字典组成的一个数组,供Filter使用。如果仅有一个Filter并且这个Filter需要参数,除非这个Filter的所有参数都已经给了默认值,否则的话 DecodeParms必须设置给Filter。如果有多个Filter,并且任意一个Filter使用了非默认的参数, DecodeParms 必须是个数组,每个元素对应一个Filter的参数列表(如果某个Filter无需参数或所有参数都有了默认值,就用空对象代替)。如果没有Filter需要参数,或者所有Filter的参数都有默认值,DecodeParms 就被忽略了。F文件标识(可选)保存stream数据的文件。如果有这个字段, stream和endstream就被忽略,FFilter将会代替Filter, FDecodeParms将代替DecodeParms。Length字段还是表示stream和endstream之间数据的长度,但是通常此刻已经没有数据了,长度是0.FFilter名字 或 字典(可选)和filter类似,针对外部文件。FDecodeParms字典 或 数组(可选)和DecodeParams类似,针对外部文件。




    • Stream的编码算法名称(列表)。如果有多个,则数组中的编码算法列表顺序就是数据被编码的顺序。且需要被编码。编码算法主要如下:






    编辑切换为居中


    img


    编码可视化主要显示为乱码,所以提供了隐藏信息的机会,如下图的steam内容为乱码。




    编辑切换为居中


    img



    • NULL 用null表示,代表空。如果一个key的值为null,则这个key可以被忽略;如果引用一个不存在的object则等价于引用一个空对象。


    交叉引用表



    • 交叉引用表是PDf文件内部一种特殊的文件组织方式,可以很方便的根据对象号随机访问一个对象。其格式如下:


    xref
    0 1
    0000000000 65535 f
    4 1
    0000000009 00000 n
    8 3
    0000000074 00000 n
    0000000120 00000 n
    0000000179 00000 n

    复制



    • 其中,xref是开始标志,表示以下为一个交叉引用表的内容;每个交叉引用表又可以分为若干个子段,每个子段的第一行是两个数字,第一个是对象起始号,后面是连续的对象个数,接着每行是这个子段的每个对象的具体信息——每行的前10个数字代表这个这个对象相对文件头的偏移地址,后面的5位数字是生成号(用于标记PDF的更新信息,和对象的生成号作用类似),最后一位f或n表示对象是否被使用(n表示使用,f表示被删除或没有用)。上面这个交叉引用表一共有3个子段,分别有1个,1个,3个对象,第一个子段的对象不可用,其余子段对象可用。


    文件尾



    • 通过trailer可以快速的找到交叉引用表的位置,进而可以精确定位每一个对象;还可以通过它本身的字典还可以获取文件的一些全局信息(作者,关键字,标题等),加密信息,等等。具体形式如下:


    trailer
    <<
    key1 value1
    key2 value2
    key3 value3

    >>
    startxref
    553
    %%EOF

    复制



    • trailer后面紧跟一个字典,包含若干键-值对。具体含义如下:


    值类型值说明
    Size整形数字所有间接对象的个数。一个PDF文件,如果被更新过,则会有多个对象集合、交叉引用表、trailer,最后一个trailer的这个字段记录了之前所有对象的个数。这个值必须是直接对象。
    Prev整形数字当文件有多个对象集合、交叉引用表和trailer时,才会有这个键,它表示前一个相对于文件头的偏移位置。这个值必须是直接对象。
    Root字典Catalog字典(文件的逻辑入口点)的对象号。必须是间接对象。
    Encrypt字典文档被保护时,会有这个字段,加密字典的对象号。
    Info字典存放文档信息的字典,必须是间接对象。
    ID数组文件的ID


    • 上面代码中的startxref:后面的数字表示最后一个交叉引用表相对于文件起始位置的偏移量

    • %%EOF:文件结束符


    逻辑文件结构




    编辑切换为居中


    img


    catalog根节点



    • catalog是整个PDF逻辑结构的根节点,这个可以通过trailer的Root字段定位,虽然简单,但是相当重要,因为这里是PDF文件物理结构和逻辑结构的连接点。Catalog字典包含的信息非常多,这里仅就最主要的几个字段做个说明。 字段类型值Typename(必须)只能为Pages 。Parentdictionary(如果不是catalog里面指定的跟节点,则必须有,并且必须是间接对象) 当前节点的直接父节点。Kidsarray(必须)一个间接对象组成的数组,节点可能是page或page tree。Countinteger(必须) page tree里面所包含叶子节点(page 对象)的个数。从以上字段可以看出,Pages最主要的功能就是组织所有的page对象。Page对象描述了一个PDF页面的属性、资源等信息。Page对象是一个字典,它主要包含一下几个重要的属性:

    • Pages字段 这是个必须字段,是PDF里面所有页面的描述集合。Pages字段本身是个字典,它里面又包含了一下几个主要字段:


    字段类型
    Typename(必须)必须是Page。
    Parentdictionary(必须;并且只能是间接对象)当前page节点的直接父节点page tree 。
    LastModifieddate(如果存在PieceInfo字段,就必须有,否则可选)记录当前页面被最后一次修改的日期和时间。
    Resourcesdictionary(必须; 可继承)记录了当前page用到的所有资源。如果当前页不用任何资源,则这是个空字典。忽略所有字段则表示继承父节点的资源。
    MediaBoxrectangle(必须; 可继承)定义了要显示或打印页面的物理媒介的区域(default user space units)
    CropBoxrectangle(可选; 可继承)定义了一个可视区域,当前页被显示或打印的时候,它的内容会被这个区域裁剪。默认值就是 MediaBox。
    BleedBoxrectangle(可选) 定义了一个区域,当输出设备是个生产环境( production environment)的时候,页面显示的内容会被裁剪。默认值是 CropBox.
    Contentsstream or array(可选) 描述页面内容的流。如果这个字段缺省,则页面上什么也不会显示。这个值可以是一个流,也可以是由几个流组成的一个数组。如果是数组,实际效果相当于所有的流是按顺序连在一起的一个流,这就允许PDF生成的时候可以随时插入图片或其他资源。流之间的分割只是词汇上的一个分割,并不是逻辑上或者组织形式的切割。
    Rotateinteger(可选; 可继承) 顺时钟旋转的角度数,这个必须是90的整数倍,默认是0。
    Thumbstream(可选)定义当前页的缩略图。
    Annotsarray(可选) 和当前页面关联的注释。
    Metadatastream(可选) 当前页包含的元数据。


    • 一个简单例子:


    3 0 obj
    << /Type /Page
    /Parent 4 0 R
    /MediaBox [ 0 0 612 792 ]
    /Resources <</Font<<
    /F3 7 0 R /F5 9 0 R /F7 11 0 R
    >>
    /ProcSet [ /PDF ]
    >>
    /
    Contents 12 0 R
    /Thumb 14 0 R
    /Annots [ 23 0 R 24 0 R]
    >>
    endobj

    复制



    • Outlines字段 Outline是PDF里面为了方便用户从PDF的一部分跳转到另外一部分而设计的,有时候也叫书签(Bookmark),它是一个树状结构,可以直观的把PDF文件结构展现给用户。用户可以通过鼠标点击来打开或者关闭某个outline项来实现交互,当打开一个outline时,用户可以看到它的所有子节点,关闭一个outline的时候,这个outline的所有子节点会自动隐藏。并且,在点击的时候,阅读器会自动跳转到outline对应的页面位置。Outlines包含以下几个字段: 字段类型值Typename(可选)如果这个字段有值,则必须是Outlines。Firstdictionary(必须;必须是间接对象) 第一个顶层Outline item。Lastdictionary(必须;必须是间接对象)最后一个顶层outline item。Countinteger(必须)outline的所有层次的item的总数。

    • Outline是一个管理outline item的顶层对象,我们看到的,其实是outline item,这个里面才包含了文字、行为、目标区域等等。一个outline item主要有一下几个字段: 字段类型值Titletext string(必须)当前item要显示的标题。Parentdictionary(必须;必须是间接对象) outline层级中,当前item的父对象。如果item本身是顶级item,则父对象就是它本身。Prevdictionary(除了每层的第一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的前一个item。Nextdictionary(除了每层的最后一个item外,其他item必须有这个字段;必须是间接对象)当前层级中,此item的后一个item。Firstdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的第一个直接子节点。Lastdictionary(如果当前item有任何子节点,则这个字段是必须的;必须是间接对象) 当前item的最后一个直接子节点。Destname,byte string, or array(可选; 如果A字段存在,则这个不能被会略)当前的outline item被激活的时候,要显示的区域。Adictionary(可选; 如果Dest 字段存在,则这个不能被忽略)当前的outline item被激活的时候,要执行的动作。

    • URI字段 URI(uniform resource identifier),定义了文档级别的统一资源标识符和相关链接信息。目录和文档中的链接就是通过这个字段来处理的.

    • Metadata字段 文档的一些附带信息,用xml表示,符合adobe的xmp规范。这个可以方便程序不用解析整个文件就能获得文件的大致信息。

    • 其他 Catalog字典中,常用的字段一般有以下一些:


    字段类型
    Typename(必须)必须为Catalog。
    Versionname(可选)PDF文件所遵循的版本号(如果比文件头指定的版本号高的话)。如果这个字段缺省或者文件头指定的版本比这里的高,那就以文件头为准。一个PDF生成程序可以通过更新这个字段的值来修改PDF文件版本号。
    Pagesdictionary(必须并且必须为间接对象)当前文档的页面集合入口。
    PageLabelsnumber tree(可选) number tree,定义了页面和页面label对应关系。
    Namesdictionary(可选)文档的name字典。
    Destsdictionary(可选;必须是间接对象)name和相应目标对应关系字典。
    ViewerPreferencesdictionary(可选)阅读参数配置字典,定义了文档被打开时候的行为。如果缺省,则使用阅读器自己的配置。
    PageLayoutname(可选) 指定文档被打开的时候页面的布局方式。SinglePageDisplay 单页OneColumnDisplay 单列TwoColumnLeftDisplay 双列,奇数页在左TwoColumnRightDisplay 双列,奇数页在右TwoPageLeft 双页,奇数页在左TwoPageRight 双页,奇数页在右缺省值: SinglePage.
    PageModename(可选) 当文档被打开时,指定文档怎么显示Use 目录和缩略图都不显示UseOutlines 显示目录UseThumbs 显示缩略图FullScreen 全屏模式,没有菜单,任何其他窗口UseOC 显示Optional content group 面板UseAttachments显示附件面板缺省值: Use.
    Outlinesdictionary(可选;必须为间接对象)文档的目录字典
    Threadsarray(可选;必须为间接对象)文章线索字典组成的数组。
    OpenActionarray or dictionary(可选) 指定一个区域或一个action,在文档打开的时候显示(区域)或者执行(action)。如果缺省,则会用默认缩放率显示第一页的顶部。
    AAdictionary(可选)一个附加的动作字典,在全局范围内定义了响应各种事件的action。
    URIdictionary(可选)一个URI字典包含了文档级别的URI action信息。
    AcroFormdictionary(可选)文档的交互式form (AcroForm)字典。
    Metadatastream(可选;必须是间接对象)文档包含的元数据流。

    具体组成


    1 Header部分


    PDF文件的第一行应是由5个字符“%PDF-”后跟“1.N”的版本号组成的标题,其中N是0到7之间的数字。例如下面的:


    %PDF–1.0   %PDF–1.1   %PDF–1.2   %PDF–1.3   %PDF–1.4   %PDF–1.5   %PDF–1.6   %PDF–1.7


    从PDF 1.4开始,应使用文档目录字典中的Version 条目(通过文件Trailer部分的Root条目指定版本),而不是标题中指定的版本。


    2 Body部分


    PDF文件的正文应由表示文件内容的一系列间接对象组成,例如字体、页面和采样图像。从PDF 1.5开始,Body还可以包含对象流,每个对象流包含一系列间接对象。例如下面这样:


    1 0 obj
    << /Type /Catalog
      /Outlines 2 0 R
      /Pages 3 0 R
    >>
    endobj
    2 0 obj
    << /Type Outlines
      /Count 0
    >>
    endobj
    3 0 obj
    << /Type /Pages
    /Kids [4 0 R]
    /Count 1
    >>
    endobj
    4 0 obj
    << /Type /Page
      /Parent 3 0 R
      /MediaBox [0 0 612 792]
      /Contents 5 0 R
      /Resources << /ProcSet 6 0 R >>
    >>
    endobj
    5 0 obj
    << /Length 35 >>
    stream
      …Page-marking operators…
    endstream
    endobj
    6 0 obj
    [/PDF]
    endobj

    3 Cross-Reference Table 交叉引用表部分


    交叉引用表包含文件中间接对象的信息,以便允许对这些对象进行随机访问,因此无需读取整个文件即可定位任何特定对象。


    交叉引用表以xref开始,紧接着是一个空格隔开的两个数字,然后每一行就是一个对象信息:


    xref
    0 7
    0000000000 65535 f
    0000000009 00000 n
    0000000074 00000 n
    0000000120 00000 n
    0000000179 00000 n
    0000000300 00000 n
    0000000384 00000 n

    上面第二行中的两个数字“0 7”,0表示下面的对象从0号对象开始,7表示对象的数量,也就是说表示从0到6共7个对象。


    每行一个对象信息的格式如下:


    nnnnnnnnnn ggggg n eol


    • nnnnnnnnnn 长度10个字节,表示对象在文件的偏移地址;

    • ggggg 长度5个字节,表示对象的生成号;

    • n (in-use)表示对象被引用,如果此值是f (free),表示对象未被引用;

    • eol 就是回车换行


    交叉引用表中的第一个编号为0的对象始终是f(free)的,并且生成号为65535;除了编号0的对象外,交叉引用表中的所有对象最初的生成号应为0。删除间接对象时,应将其交叉引用条目标记为“free”,并将其添加到free条目的链表中。下次创建具有该对象编号的对象时,条目的生成号应增加1,最大生成号为65535;当交叉引用条目达到此值时,它将永远不会被重用。


    交叉引用表也可以是这样的:


    xref
    0 1
    0000000000 65535 f
    3 1
    0000025325 00000 n
    23 2
    0000025518 00002 n
    0000025635 00000 n
    30 1
    0000025777 00000 n

    [


    4 Trailer部分


    PDF阅读器是从PDF的尾部开始解析文件的,通过Trailer部分能够快速找到交叉引用表和某些特殊对象。如下所示:


    trailer
    << /Size 7
    /Root 1 0 R
    >>
    startxref
    408
    %%EOF

    文件的最后一行应仅包含文件结束标记%%EOF。关键字startxref下面的数字表示最后一个交叉引用表的xref关键字开头的字节偏移量。trailer和startxref之间是尾部字典,由包含在双尖括号(<<…>>)中的键值对组成。


    为什么不容易被修改



    1. 文件结构和编码:PDF文件采用了一种复杂的文件结构和编码方式,这使得在未经授权的情况下修改PDF文件变得非常困难。PDF文件采用二进制格式存储,而不是像文本文件那样以可读的形式存储。这导致无法直接编辑和修改PDF文件,需要使用特定的软件或工具。

    2. 加密和安全特性:PDF文件可以使用密码进行加密和保护,以确保只有授权的用户才能进行修改。加密可以防止未经授权的访问和修改,使得修改PDF文件变得更加困难和复杂。

    3. 文件签名和验证:PDF文件可以使用数字签名进行验证,以确保文件的完整性和可信性。数字签名可以证明文件的来源和真实性,一旦数字签名验证失败,即表明文件已被篡改。

    4. 版本兼容性和规范:PDF格式被国际标准化组织(ISO)制定为ISO 32000标准。这个标准确保了不同版本和软件之间的PDF文件的兼容性,并定义了丰富的功能和规范,包括页面布局、字体嵌入、图形和图像处理等。这些严格的规范使得对PDF文件进行修改变得复杂和具有挑战性。


    如何修改pdf


    因为pdf的局限性是无法进行修改的,所以我们只能通过将他转换为其他类型的文件进行查看修改,当然,转换的过程不可能是百分百完美的进行转换的。


    下面推荐这俩个可以进行转换的网站


    PDF转换成Word在线转换器 - 免费 - CleverPDF


    Convert PDF to Word for free |

    Smallpdf.com

    收起阅读 »

    你会用nginx部署前端项目吗

    web
    前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。 对于前端项目来说,nginx主要有两个功能: 对静态资源做托管,即作为一个静态资源服务器; 对...
    继续阅读 »

    前端项目的部署以前一直是把静态资源放到后端工程中,随后端部署一起部署。随着前后端分离开发模式的流行,前端项目可以单独部署了,目前最流行的方式使用nginx来部署。


    对于前端项目来说,nginx主要有两个功能:



    • 对静态资源做托管,即作为一个静态资源服务器

    • 对动态资源做反向代理,即代理后台接口服务,防止跨域


    路由配置


    nginx配置最多就是路由配置,路由配置又有几种写法。


    1. =


    location = /111/ {
    default_type text/plain;
    return 200 "111 success";
    }

    location 和路径之间加了个 =,代表精准匹配,也就是只有完全相同的 url 才会匹配这个路由。


    image.png


    在路径后面添加了aa,那么就不是精确匹配了,所以是404


    image.png


    2. 不带 =


    代表根据前缀匹配,后面可以是任意路径


    location /222 {
    default_type text/plain;
    // 这里的 $uri 是取当前路径。
    return 200 $uri;
    }

    image.png


    3. 支持正则匹配~


    // 匹配以/333/bbb开头,以.html结尾的路径
    location ~ ^/333/bbb.*\.html$ {
    default_type text/plain;
    return 200 $uri;
    }

    image.png


    上面的~是区分大小写的,如果不区分大小写是~*


    // 匹配以/333/bbb开头,以.html结尾的路径
    location ~* ^/333/bbb.*\.html$ {
    default_type text/plain;
    return 200 $uri;
    }

    4. ^~代表优先级


    下面的配置有两个路径是以/444开头的:


    location ~* ^/444/AAA.*\.html$ {
    default_type text/plain;
    return 200 $uri;
    }
    location ~ /444/ {
    default_type text/plain;
    return 200 $uri;
    }

    如果访问/444/AAA45.html,就会直接命中第一个路由,如果我想命中/444/呢? 加上^就好了。


    location ^~ /444/ {
    default_type text/plain;
    return 200 $uri;
    }

    也就是说 ^~ 能够提高前缀匹配的优先级。


    总结一下,location 语法一共有四种:




    1. location = /aaa 是精确匹配 /aaa 的路由;




    2. location /bbb 是前缀匹配 /bbb 的路由。




    3. location ~ /ccc.*.html 是正则匹配,可以再加个 * 表示不区分大小写 location ~* /ccc.*.html;




    4. location ^~ /ddd 是前缀匹配,但是优先级更高。




    这 4 种语法的优先级是这样的:


    精确匹配(=) > 高优先级前缀匹配(^~) > 正则匹配(~ / ~*) > 普通前缀匹配


    root 与 alias


    nginx指定文件路径有两种方式rootaliasrootalias主要区别在于nginx如何解释location后面的uri,这会使两者以不同的方式将请求映射到服务器文件上。



    1. root的处理结果是:root路径 + location路径;

    2. alias的处理结果是:使用alias路径替换location路径;


    alias是一个目录别名的定义,root则是最上层目录的定义。


    需要注意的是alias后面必须要用/结束,否则会找不到文件的,而root则可有可无。另外,alias只能位于location块中。


    root示例:


    location ^~ /test/ {
    root /www/root/html/;
    }

    如果一个请求的 uri 是 /test/a.html时,web服务器将会返回服务器上的/www/root/html/test/a.html的文件。


    alias示例:


    location ^~ /test/ {
    alias /www/root/html/new_test/;
    }

    如果一个请求的 uri 是 /test/a.html 时,web服务器将会返回服务器上的/www/root/html/new_test/a.html的文件。


    注意, 这里是new_test,因为alias会把location后面配置的路径丢弃掉,把当前匹配到的目录指向到指定的目录。


    二级目录


    有时候需要在一个端口下,部署多个项目,那么这时可以采用二级目录的形式来部署。


    采用二级目录来部署会有一些坑,比如,当我请求 http://xxxxxxxx.com/views/basedata 的时候,浏览器自动跳转到了http://xxxxxxxxm:8100/store/views/basedata/


    这是什么原因呢?


    最根本的问题是因为http://xxxxxxxx.com/views/basedata后面没有/,所以就触发了nginx301重定向,重定向到了http://xxxxxxxxm:8100/store/views/basedata/,因此,只要避免触发重定向即可。


    如果你能忍受直接使用如 http://xxxxxxxxm:8100/store/views/basedata/ 这样最后带/的地址,那也就没有什么问题了。


    那为什么会触发重定向呢?


    当用户请求 http.xxxxxx.cn/osp 时,这里的 $uri 就是 /ospnginx 会尝试到alias或 root 指定的目录下找这个文件。


    如果存在名为 {alias指定目录}/osp 的文件,注意这里是文件,不是目录,就直接把这个文件的内容发送给用户。


    很显然,目录中没有叫 osp 的文件,于是就看 osp/,增加了一个 /,也就是看有没有名为 {alias指定目录}/osp/ 的目录。


    即,当我们访问 uri 时,如果访问资源为一个目录,并且 uri 没有以正斜杠 / 结尾,那么nginx 服务就会返回一个301跳转,目标地址就是要加一个正斜杠/


    一种最简单的方式就是直接访问一个具体的文件,如 http.xxxxxx.cn/osp/index.html,这样就不会发生重定向了。但是,这样方式不够优雅,每次都要输入完整的文件路径。


    另一种方式是调整 nginx 中关于重定向的配置,nginx 重定向中的三个配置:absolute_redirectserver_name_in_redirectport_in_redirect


    absolute_redirect通过该指令控制 nginx 发出的重定向地址是相对地址还是绝对地址:


    1、如果设置为 off,则 nginx 发出的重定向将是相对的,没有域名和端口, 也就没有server_name_in_redirectport_in_redirect什么事儿了。


    image.png


    加了这个配置后,尽管也会发生重定向,但是不会在路径上加上域名和端口了。


    2、如果设置为 on,则 nginx 发出的重定向将是绝对的;只有 absolute_redirect 设置为 onserver_name_in_redirectport_in_redirect 的设置才有作用。


    image.png


    nginx 开启 gzip 静态压缩提升效率


    gzip 是一种格式,也是一种 linux 下的解压缩工具,我们使用 gzipapp.js 文件压缩后,原始文件就变为了以.gz结尾的文件,同时文件大小从 42571 减小到 11862。


    image.png


    目前,对静态资源压缩有两种形式:



    • 动态压缩: 服务器在返回任何的静态文件之前,由服务器对每个请求压缩在进行输出。

    • 静态压缩:服务器直接使用现成的扩展名为 .gz 的预压缩文件,直接进行输出。


    我们知道 gzipCPU 密集型的,实时动态压缩比较消耗 CPU 资源。为进一步提高 nginx 的性能,我们可以使用静态 gzip 压缩,提前将需要压缩的文件压缩好,当请求到达时,直接发送压缩好的.gz文件,如此就减轻了服务器 CPU 的压力,提高了性能。


    因此,我们一般采用静态压缩的方式,实现静态压缩有以下两个步骤:


    1. 生成gzip压缩文件


    在利用webpack打包的时候,我们就把文件进行压缩,配置如下:


    const isProduction = process.env.NODE_ENV === 'production'

    if (isProduction) {
    config.plugins.push(
    new CompressionWebpackPlugin({
    // 采用gzip进行压缩
    algorithm: 'gzip',
    test: /\.js$|\.html$|\.json$|\.css/,
    threshold: 10240
    })
    )
    }

    可以看到,多生成了一个以.gz结尾的文件,然后把.gz后缀的文件上传到服务器中即可。


    image.png


    2. 在 nginx 开启支持静态压缩的模块


    nginx配置中加上如下配置:


    gzip_static on;

    如果不加的话,访问的时候就会找不到,报404错误,因为服务器只有.gz的文件,没有原始文件。


    总结


    前端项目nginx部署主要的配置基本上就是上面提到的这些。


    首先是location路由的四种写法;


    接着就是分清楚rootalias的区别;


    当项目较多时需要使用二级路由时,需要注意重定向的配置;


    如果你的项目文件较大,可以开启gzip

    作者:小p
    来源:juejin.cn/post/7270902621065560120
    压缩提升传输效率。

    收起阅读 »

    坏了!要长脑子了...这么多前端框架没听过

    web
    市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。 React 官网链接 React 是一个用于构建用户界面的 JavaScript 库。它由 Faceboo...
    继续阅读 »

    市面上有很多不同的 JavaScript 框架,很难对它们一一进行跟踪。在本文中,我们将重点介绍最受欢迎的几种,并探讨开发人员喜欢或不喜欢它们的原因。


    React



    官网链接


    React 是一个用于构建用户界面的 JavaScript 库。它由 Facebook 和一个由个人开发者和公司组成的社区维护。React 可以作为开发单页或移动应用程序的基础。然而,React 只关心将数据呈现给 DOM,因此创建 React 应用程序通常需要使用额外的库来进行状态管理、路由和与 API 的交互。React 还用于构建可重用的 UI 组件。从这个意义上讲,它的工作方式很像 Angular 或 Vue 等 JavaScript 框架。然而,React 组件通常以声明式方式编写,而不是使用命令式代码,这使得它们更容易阅读和调试。正因为如此,许多开发人员更喜欢使用 React 来构建 UI 组件,即使他们不使用它作为整个前端框架。


    优点:



    • React 快速而高效,因为它使用虚拟 DOM 而不是操纵真实的 DOM。

    • React 很容易学习,因为它的声明性语法和清晰的文档。

    • React 组件是可重用的,使代码维护更容易。


    缺点:



    • React 有一个很大的学习曲线,因为它是一个复杂的 JavaScript 库。

    • React 不是一个成熟的框架,因此它需要使用额外的库来完成许多任务。


    Next.js



    官网链接


    Next.js 是一个 javascript 库,支持 React 应用程序的服务器端渲染。这意味着 next.js 可以在将 React 应用程序发送到客户端之前在服务器上渲染它。这有几个好处。首先,它允许您预呈现组件,以便当用户请求它们时,它们已经在客户机上可用。其次,它允许爬虫更容易地索引你的内容,从而为你的 React 应用程序提供更好的 SEO。最后,它可以通过减少客户机为呈现页面而必须执行的工作量来提高性能。


    以下是开发者喜欢 Next.js 的原因:




    • js 使得无需做任何配置就可以轻松地开始服务器端渲染。




    • js 会自动对应用程序进行代码拆分,以便每个页面只在被请求时加载,这可以提高性能。
      缺点:




    • 如果不小心,next.js 会使应用程序代码库变得更复杂,更难以维护。




    • 一些开发人员发现 next.js 的内置特性固执己见且不灵活。




    Vue.js



    官网链接


    Vue.js 是一个用于构建用户界面和单页应用程序的开源 JavaScript 框架。与 React 和 Angular 等其他框架不同,Vue.js 被设计为轻量级且易于使用。Vue.js 库可以与其他库和框架结合使用,也可以作为创建前端 web 应用程序的独立工具使用。Vue.js 的一个关键特性是它的双向数据绑定,当模型发生变化时,它会自动更新视图,反之亦然。这使得它成为构建动态用户界面的理想选择。此外,Vue.js 还提供了许多内置功能,如模板系统、响应系统和事件总线。这些特性使创建复杂的应用程序成为可能,而不必依赖第三方库。因此,Vue.js 已经成为近年来最流行的 JavaScript 框架之一。


    优点:



    • Vue.js 很容易学习,因为它的小尺寸和清晰的文档。

    • Vue.js 组件是可重用的,这使得代码维护更容易。

    • 由于虚拟 DOM 和异步组件加载,Vue.js 应用程序非常快。


    缺点:



    • 虽然 Vue.js 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。

    • Vue.js 没有像其他框架那样多的库和工具。


    Angular



    官网链接


    Angular 是一个 JavaScript 框架,用于用 JavaScript、html 和 Typescript 构建 web 应用和应用。Angular 是由 Google 创建和维护的。Angular 提供了双向数据绑定,这样对模型的更改就会自动传播到视图。它还提供了一种声明性语法,使构建动态 ui 变得容易。最后,Angular 提供了许多有用的内置服务,比如 HTTP 请求处理,以及对路由和模板的支持。


    优点:



    • Angular 有一个庞大的社区和许多可用的库和工具。

    • Angular 很容易学习,因为它有组织良好的文档和清晰的语法。


    缺点:



    • 虽然 Angular 很容易学习,但如果你想掌握它的所有特性,它有一个很大的学习曲线。

    • Angular 不像其他一些框架那样轻量级。


    Svelte



    官网链接


    简而言之,Svelte 是一个类似于 React、Vue 或 Angular 的 JavaScript 框架。然而,这些框架使用虚拟 DOM(文档对象模型)来区分视图之间的变化,而 Svelte 使用了一种称为 DOM 区分的技术。这意味着它只更新 DOM 中已更改的部分,从而实现更高效的呈现过程。此外,Svelte 还包括一些其他框架没有的内置优化,例如自动批处理 DOM 更新和代码分割。这些特性使 Svelte 成为高性能应用程序的一个很好的选择。


    优点:




    • Svelte 有其他框架没有的内置优化,比如代码分割。




    • 由于其清晰的语法和组织良好的文档,Svelte 很容易学习。
      缺点:




    • 虽然 Svelte 很容易学习,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




    • Svelte 没有像其他框架那样多的库和工具。




    Gatsby



    官网链接


    Gatsby 是一个基于 React 的免费开源框架,可以帮助开发人员构建快速的网站和应用程序。它使用尖端技术,使建立网站和应用程序的过程更加高效。它的一个关键特性是能够预取资源,以便在需要时立即可用。这使得盖茨比网站非常快速和响应。使用 Gatsby 的另一个好处是,它允许开发人员使用 GraphQL 查询来自任何来源的数据,从而使构建复杂的数据驱动应用程序变得容易。此外,Gatsby 附带了许多插件,使其更易于使用,包括用于 SEO,分析和图像优化的插件。所有这些因素使 Gatsby 成为构建现代网站和应用程序的一个非常受欢迎的选择。


    优点:




    • 由于使用了预取,Gatsby 网站的速度和响应速度非常快。




    • 由于对 GraphQL 的支持,Gatsby 使构建复杂的数据驱动应用程序变得容易。




    • Gatsby 附带了许多插件,使其更易于使用。
      缺点:




    • 虽然 Gatsby 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




    • Gatsby 没有像其他框架那样多的库和工具。




    Nuxt.js



    官网链接


    js 是一个用于构建 JavaScript 应用程序的渐进式框架。它基于 Vue.js,并附带了一组工具和库,可以轻松创建可以在服务器端和客户端呈现的通用应用程序。js 还提供了一种处理异步数据和路由的方法,这使得它非常适合构建高度交互的应用程序。此外,nuxt.js 附带了一个 CLI 工具,可以很容易地构建新项目并构建、运行和测试它们。使用 nuxt.js,您可以创建快速、可靠和可扩展的令人印象深刻的 JavaScript 应用程序。


    优点:




    • js 易于使用和扩展。




    • 由于服务器端渲染,nuxt.js 应用程序快速响应。
      缺点:




    • 虽然 nuxt.js 很容易使用,但如果你想掌握它的所有功能,它有一个很大的学习曲线。




    • nuxt.js 没有像其他框架那样多的库和工具。




    Ember.js



    官网链接


    Ember.js 以其优于配置的约定方法而闻名,这使得开发人员更容易开始使用该框架。它还为数据持久化和路由等常见任务提供了内置库,从而加快了开发速度。尽管 Ember.js 有一个陡峭的学习曲线,但它为开发人员提供了创建富 web 应用程序的灵活性和强大功能。如果你正在寻找一个前端 JavaScript 框架来构建 spa, Ember.js 绝对值得考虑。


    优点:




    • Ember.js 使用约定而不是配置,这使得它更容易开始使用框架。




    • Ember.js 为数据持久化和路由等常见任务提供了内置库。




    • Ember.js 为开发人员创建富 web 应用程序提供了很大的灵活性和能力。
      缺点:




    • Ember.js 有一个陡峭的学习曲线。




    • Ember.js 没有像其他框架那样多的库和工具。




    Backbone.js



    官网链接


    Backbone.js 是一个轻量级 JavaScript 库,允许开发人员创建单页面应用程序。它基于模型-视图-控制器(MVC)体系结构,这意味着它将数据和逻辑从用户界面中分离出来。这使得代码更易于维护和扩展,也使创建复杂的应用程序变得更容易。Backbone.js 还包含了许多使其成为开发移动应用程序的理想选择的特性,例如将数据绑定到 HTML 元素的能力以及对触摸事件的支持。因此,对于想要创建快速响应的应用程序的开发人员来说,Backbone.js 是一个受欢迎的选择。


    优点:




    • js 是轻量级的,只是一个库,而不是一个完整的框架。




    • js 很容易学习和使用。




    • Backbone.js 具有很强的可扩展性,可以使用许多第三方库。
      缺点:




    • Backbone.js 不像其他框架那样提供那么多的内置功能。




    • 与其他一些框架相比,Backbone.js 只有一个小社区。




    结论


    总之,虽然有许多不同的 JavaScript 框架可供选择,但最流行的框架仍然相对稳定。每一种都有自己的优点和缺点,开发人员在决定在他们的项目中使用哪一种时必须权衡。虽然没有一个框架是完美的,但每个框架都有一些可以使开发更容易或更快的东西。


    在选择框架时,每个人都应该考虑他们项目的具体需求,以及他们团队的技能和他们必须投入学习新框架的时间。通过考虑所有这些因素,您可以为您的项目选择最好的 JavaScript 框架!


    参考链接:
    blog.risingstack.com/

    best-javasc…

    收起阅读 »

    虚拟dom

    vue中的虚拟dom 简介 首先vue会把模板编译成render函数,运行render函数生成虚拟dom 虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图 为什么会需要虚拟dom 在主流框架 Angular , Vue.js (1.0)...
    继续阅读 »

    vue中的虚拟dom


    简介



    首先vue会把模板编译成render函数,运行render函数生成虚拟dom


    虚拟dom通过 diff算法 对比差异 渲染不同的部分 然后更新视图



    为什么会需要虚拟dom


    在主流框架 Angular , Vue.js (1.0)React 中都有一个共同点,那就是它们都不知道哪些状态(state)变了。因此就需要进行比对,在React中使用的虚拟dom比对, Angular 中使用的是脏检查的流程



    而在 Vue.js中使用的是变化侦测的方式,它在一定程度上知道具体哪些状态发生了变化,这样就可以通过更细粒度的绑定来更新视图。也就是说,在Vue.js中,当状态发生变化时,它在一定程度上知道哪些节点使用了这个状态,从而对这些节点进行更新操作,根本不需要比对



    但是这样做的代价就是,粒度太细,每一个都有对应的 watcher 来观察状态变化,这样就会浪费一些内存开销,绑定的越多开销越大,如果这运用在一个大型项目中,那么他的开销无疑是非常大的


    因此从 Vue.js 2.0 开始借鉴 React 中的虚拟DOM ,组件级别是一个watcher实例,就是说即便一个组件内有10个节点使用了某个状态,但其实也只有一个watcher在观察这个状态的变化。


    什么是虚拟dom


    虚拟DOM是通过状态生成一个虚拟节点树(vnode) ,然后使用虚拟节点树进行渲染。 在渲染之前,会使用新生成的虚拟节点树和上一次生成的虚拟节点树进行对比 (diff算法) ,只渲染不同的部分



    虚拟节点树其实是由组件树建立起来的整个虚拟节点(Virtual Node,也经常简写为vnode)树



    在Vue.js中,我们使用模板来描述状态DOM之间的映射关系。Vue.js通过编译将模板转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树,使用这个虚拟节点树就可以渲染页面


    模板编译成render函数


    将模板编译成渲染函数可以分两个步骤,先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使用AST生成渲染函数。


    但是由于静态节点不需要总是重新渲染,所以在生成AST之后、生成渲染函数之前这个阶段,需要做一个操作,那就是遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。所以,在大体逻辑上,模板编译分三部分内容:

    • 将模板解析为AST
    • 遍历AST标记静态节点
    • 使用AST生成渲染函数

    虚拟dom做了什么


    虚拟DOM在Vue.js中所做的事情其实并没有想象中那么复杂,它主要做了两件事。

    • 提供与真实DOM节点所对应的虚拟节点vnode。
    • 将虚拟节点vnode和旧虚拟节点oldVnode进行比对,然后更新视图。

    对两个虚拟节点对比是虚拟dom 中最核心的算法 (diff),它可以判断出哪些节点发生了变化,从而只对发生了变化的节点进行更新操作


    小结


    虚拟DOM是将状态映射成视图的众多解决方案中的一种,它的运作原理是使用状态生成虚拟节点,然后使用虚拟节点渲染视图。


    为什么会需要虚拟dom

    框架设计

    Vue 和 React 框架设计理念都是基于数据驱动的,当数据发生变化时 就要去更新视图,要想知道在页面众多元素中改动数据的元素 并根据改动后的数据去更新视图 是非常困难的



    所以 Vue 和 React 中都会有一个 Render函数 或者类似于Render函数的功能,当数据变化时 全量生成Dom 元素
    如果是直接操作 真实Dom 的话 是很昂贵的,就会严重拖累效率,所以就不生成真实的Dom,而是生成虚拟的Dom
    当数据变化时就是 对象 和 对象 进行一个对比 ,这样就能知道哪些数据发生了改变 从而去操作改变的数据后的Dom元素



    这也是一个 “妥协的结果”


    跨平台

    现阶段的框架他不仅仅能在浏览器里面使用,在小程序,移动端,或者桌面端也可以使用,但是真实Dom仅仅指的是在浏览器的环境下使用,因此他不能直接生成真实Dom ,所以选择生成一个在任何环境下都能被认识的虚拟Dom
    最后根据不同的环境,使用虚拟Dom 去生成界面,从而实现跨平台的作用 --- 一套代码在多端运行


    作者:明天休息吗
    链接:https://juejin.cn/post/7258171660145868860
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    不会封装hook? 看下ahooks这6个hook是怎么做的

    1 useUpdate 在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:import { useCallback...
    继续阅读 »

    1 useUpdate


    在react函数组件中如何强制组件进行刷新? 虽然react没有提供原生的方法,但是我们知道当state值变化的时候,react函数组件就会刷新,所以useUpdate就是利用了这一点,源码如下:

    import { useCallback, useState } from 'react';

    const useUpdate = () => {
    const [, setState] = useState({});

    return useCallback(() => setState({}), []);
    };

    export default useUpdate;

    可以看到useUpdate的返回值函数,就是每次都用一个新的对象调用setState,触发组件的更新。


    2 useMount


    react函数组件虽然没有了mount的生命周期,但是我们还会有这种需求,就是在组件第一次渲染之后执行一次的需求,就可以封装useEffect实现这个需求, 只需要把依赖项设置成空数组,那么就只在渲染结束后,执行一次回调:

    import { useEffect } from 'react';

    const useMount = (fn: () => void) => {

    useEffect(() => {
    fn?.();
    }, []);
    };

    export default useMount;


    3 useLatest


    react函数组件是一个可中断,可重复执行的函数,所以在每次有state或者props变化的时候,函数都会重新执行,我们知道函数的作用域是创建函数的时候就固定下来的,如果其中的内部函数是不更新的,那么这些函数获取到的外部变量就是不会变的。例如:

    import React, { useState, useEffect } from 'react';
    import { useLatest } from 'ahooks';


    export default () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
    const interval = setInterval(() => {
    setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
    }, []);

    return (
    <>
    <p>count: {count}</p>
    </>
    );
    };

    这是一个定时更新count值的例子,但是上边的代码只会让count一直是1,因为setInterval中的函数在创建的时候它的作用域就定下来了,它拿到的count永远是0, 当执行了setCount后,会触发函数的重新执行,重新执行的时候,虽然count值变成了1,但是这个count已经不是它作用域上的count变量了,函数的每次执行都会创建新的环境,而useState, useRef 等这些hooks 是提供了函数重新执行后保持状态的能力, 但是对于那些没有重新创建的函数,他们的作用域就永远的停留在了创建的时刻。 如何让count正确更新, 简单直接的方法如下,在setCount的同时,也直接更新count变量,也就是直接改变这个闭包变量的值,这在JS中也是允许的。

    import React, { useState, useEffect } from 'react';
    import { useLatest } from 'ahooks';


    export default () => {
    let [count, setCount] = useState(0);

    useEffect(() => {
    const interval = setInterval(() => {
    count = count + 1
    setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
    }, []);

    return (
    <>
    <p>count: {count}</p>
    </>
    );
    };

    setCount是为了让函数刷新,并且更新函数的count值,而直接给count赋值,是为了更新定时任务函数中维护的闭包变量。 这显然不是一个好的解决办法,更好的办法应该是让定时任务函数能够拿到函数最新的count值。
    useState返回的count每次都是新的变量,也就是变量地址是不同的,应该让定时任务函数中引用一个变量地址不变的对象,这个对象中再记录最新的count值,而实现这个功能就需要用到了useRef,它就能帮助我们在每次函数刷新都返回相同变量地址的对象, 实现方式如下:

    import React, { useState, useEffect, useRef } from 'react'

    export default () => {
    const [count, setCount] = useState(0)

    const latestCount = useRef(count)
    latestCount.current = count

    useEffect(() => {
    const interval = setInterval(() => {
    setCount(latestCount.current + 1)
    }, 1000)
    return () => clearInterval(interval)
    }, [])

    return (
    <>
    <p>count: {count}</p>
    </>
    )
    }


    可以看到定时函数获取的latestCount永远是定义时的变量,但因为useRef,每次函数执行它的变量地址都不变,并且还把count的最新值,赋值给了latestCount.current, 定时函数就可以获取到了最新的count值。
    所以这个功能可以封装成了useLatest,获取最新值的功能。

    import { useRef } from 'react';

    function useLatest<T>(value: T) {
    const ref = useRef(value);
    ref.current = value;

    return ref;
    }

    export default useLatest;


    上边的例子是为了说明useLatest的作用,但针对这个例子,只是为了给count+1,还可以通过setCount方法本身获取,虽然定时任务函数中的setCount页一直是最开始的函数,但是它的功能是可以通过传递函数的方式获取到最新的count值,代码如下:

      const [count, setCount] = useState(0)
    useEffect(() => {
    const interval = setInterval(() => {
    setCount(count=>count+1)
    }, 1000)
    return () => clearInterval(interval)
    }, [])

    4 useUnMount


    有了useMount就会有useUnmount,利用的就是useEffect的函数会返回一个cleanup函数,这个函数在组件卸载和useEffect的依赖项变化的时候触发。 正常情况 我们应该是useEffect的时候做了什么操作,返回的cleanup函数进行相应的清除,例如useEffect创建定时器,那么返回的cleanup函数就应该清除定时器:

     const [count, setCount] = useState(0);
    useEffect(() => {
    const interval = setInterval(() => {
    count = count + 1
    setCount(count + 1);
    }, 1000);
    return () => clearInterval(interval);
    }, []);

    所以useUnMount就是利用了这个cleanup函数实现useUnmount的能力,代码如下:

    import { useEffect } from 'react';
    import useLatest from '../useLatest';

    const useUnmount = (fn: () => void) => {
    const fnRef = useLatest(fn);

    useEffect(
    () => () => {
    fnRef.current();
    },
    [],
    );
    };

    export default useUnmount;


    使用了useLatest存放fn的最新值,写了一个空的useEffect,依赖是空数组,只在函数卸载的时候执行。


    5 useToggle和useBoolean


    useToggle 封装了可以state在2个值之间的变化,useBoolean则是利用了useToggle,固定2个值只能是true和false。 看下他们的源码:

    function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
    const [state, setState] = useState<D | R>(defaultValue);

    const actions = useMemo(() => {
    const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;

    const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
    const set = (value: D | R) => setState(value);
    const setLeft = () => setState(defaultValue);
    const setRight = () => setState(reverseValueOrigin);

    return {
    toggle,
    set,
    setLeft,
    setRight,
    };
    // useToggle ignore value change
    // }, [defaultValue, reverseValue]);
    }, []);

    return [state, actions];
    }

    可以看到,调用useToggle的时候可以设置初始值和相反值,默认初始值是false,actions用useMemo封装是为了提高性能,没有必要每次渲染都重新创建这些函数。setLeft是设置初始值,setRight是设置相反值,set是用户随意设置,toggle是切换2个值。
    useBoolean则是在useToggle的基础上进行了封装,让我们用起来对更加的简洁方便。

    export default function useBoolean(defaultValue = false): [boolean, Actions] {
    const [state, { toggle, set }] = useToggle(defaultValue);

    const actions: Actions = useMemo(() => {
    const setTrue = () => set(true);
    const setFalse = () => set(false);
    return {
    toggle,
    set: (v) => set(!!v),
    setTrue,
    setFalse,
    };
    }, []);

    return [state, actions];
    }

    总结


    本文介绍了ahooks中封装的6个简单的hook,虽然简单,但是可以通过他们的做法,学习到自定义hook的思路和作用,就是把一些能够重用的逻辑封装起来,在实际项目中我们有这个意识就可以封装出适合项目的hook。


    作者:前端良文
    链接:https://juejin.cn/post/7248073216512901178
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    你的代码不堪一击!太烂了!

    前言 小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返...
    继续阅读 »

    前言


    小王,你的页面白屏了,赶快修复一下。小王排查后发现是服务端传回来的数据格式不对导致,无数据时传回来不是 [] 而是 null, 从而导致 forEach 方法报错导致白屏,于是告诉测试,这是服务端的错误导致,要让服务端来修改,结果测试来了一句:“服务端返回数据格式错误也不能白屏!!” “好吧,千错万错都是前端的错。” 小王抱怨着把白屏修复了。


    刚过不久,老李喊道:“小王,你的组件又渲染不出来了。” 小王不耐烦地过来去看了一下,“你这个属性data 格式不对,要数组,你传个对象干嘛呢。”老李反驳: “ 就算 data 格式传错,也不应该整个组件渲染不出来,至少展示暂无数据吧!” “行,你说什么就是什么吧。” 小王又抱怨着把问题修复了。


    类似场景,小王时不时都要经历一次,久而久之,大家都觉得小王的技术太菜了。小王听到后,倍感委屈:“这都是别人的错误,反倒成为我的错了!”


    等到小王离职后,我去看了一下他的代码,的确够烂的,不堪一击!太烂了!下面来吐槽一下。


    一、变量解构一解就报错


    优化前

    const App = (props) => {
    const { data } = props;
    const { name, age } = data
    }

    如果你觉得以上代码没问题,我只能说你对你变量的解构赋值掌握的不扎实。



    解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于 undefinednull 无法转为对象,所以对它们进行解构赋值时都会报错。



    所以当 dataundefinednull 时候,上述代码就会报错。


    优化后

    const App = (props) => {
    const { data } = props || {};
    const { name, age } = data || {};
    }

    二、不靠谱的默认值


    估计有些同学,看到上小节的代码,感觉还可以再优化一下。


    再优化一下

    const App = (props = {}) => {
    const { data = {} } = props;
    const { name, age } = data ;
    }

    我看了摇摇头,只能说你对ES6默认值的掌握不扎实。



    ES6 内部使用严格相等运算符(===)判断一个变量是否有值。所以,如果一个对象的属性值不严格等于 undefined ,默认值是不会生效的。



    所以当 props.datanull,那么 const { name, age } = null 就会报错!


    三、数组的方法只能用真数组调用


    优化前:

    const App = (props) => {
    const { data } = props || {};
    const nameList = (data || []).map(item => item.name);
    }

    那么问题来了,当 data123 , data || [] 的结果是 123123 作为一个 number 是没有 map 方法的,就会报错。


    数组的方法只能用真数组调用,哪怕是类数组也不行。如何判断 data 是真数组,Array.isArray 是最靠谱的。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let nameList = [];
    if (Array.isArray(data)) {
    nameList = data.map(item => item.name);
    }
    }

    四、数组中每项不一定都是对象


    优化前:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => `我的名字是${item.name},今年${item.age}岁了`);
    }
    }

    一旦 data 数组中某项值是 undefinednull,那么 item.name 必定报错,可能又白屏了。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => `我的名字是${item?.name},今年${item?.age}岁了`);
    }
    }

    ? 可选链操作符,虽然好用,但也不能滥用。item?.name 会被编译成 item === null || item === void 0 ? void 0 : item.name,滥用会导致编辑后的代码大小增大。


    二次优化后:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => {
    const { name, age } = item || {};
    return `我的名字是${name},今年${age}岁了`;
    });
    }
    }

    五、对象的方法谁能调用


    优化前:

    const App = (props) => {
    const { data } = props || {};
    const nameList = Object.keys(data || {});
    }

    只要变量能被转成对象,就可以使用对象的方法,但是 undefinednull 无法转换成对象。对其使用对象方法时就会报错。


    优化后:

    const _toString = Object.prototype.toString;
    const isPlainObject = (obj) => {
    return _toString.call(obj) === '[object Object]';
    }
    const App = (props) => {
    const { data } = props || {};
    const nameList = [];
    if (isPlainObject(data)) {
    nameList = Object.keys(data);
    }
    }

    六、async/await 错误捕获


    优化前:

    import React, { useState } from 'react';

    const App = () => {
    const [loading, setLoading] = useState(false);
    const getData = async () => {
    setLoading(true);
    const res = await queryData();
    setLoading(false);
    }
    }

    如果 queryData() 执行报错,那是不是页面一直在转圈圈。


    优化后:

    import React, { useState } from 'react';

    const App = () => {
    const [loading, setLoading] = useState(false);
    const getData = async () => {
    setLoading(true);
    try {
    const res = await queryData();
    setLoading(false);
    } catch (error) {
    setLoading(false);
    }
    }
    }

    如果使用 trycatch 来捕获 await 的错误感觉不太优雅,可以使用 await-to-js 来优雅地捕获。


    二次优化后:

    import React, { useState } from 'react';
    import to from 'await-to-js';

    const App = () => {
    const [loading, setLoading] = useState(false);
    const getData = async () => {
    setLoading(true);
    const [err, res] = await to(queryData());
    setLoading(false);
    }
    }

    七、不是什么都能用来JSON.parse


    优化前:

    const App = (props) => {
    const { data } = props || {};
    const dataObj = JSON.parse(data);
    }

    JSON.parse() 方法将一个有效的 JSON 字符串转换为 JavaScript 对象。这里没必要去判断一个字符串是否为有效的 JSON 字符串。只要利用 trycatch 来捕获错误即可。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let dataObj = {};
    try {
    dataObj = JSON.parse(data);
    } catch (error) {
    console.error('data不是一个有效的JSON字符串')
    }
    }

    八、被修改的引用类型数据


    优化前:

    const App = (props) => {
    const { data } = props || {};
    if (Array.isArray(data)) {
    data.forEach(item => {
    if (item) item.age = 12;
    })
    }
    }

    如果谁用 App 这个函数后,他会搞不懂为啥 dataage 的值为啥一直为 12,在他的代码中找不到任何修改 dataage 值的地方。只因为 data 是引用类型数据。在公共函数中为了防止处理引用类型数据时不小心修改了数据,建议先使用 lodash.clonedeep 克隆一下。


    优化后:

    import cloneDeep from 'lodash.clonedeep';

    const App = (props) => {
    const { data } = props || {};
    const dataCopy = cloneDeep(data);
    if (Array.isArray(dataCopy)) {
    dataCopy.forEach(item => {
    if (item) item.age = 12;
    })
    }
    }

    九、并发异步执行赋值操作


    优化前:

    const App = (props) => {
    const { data } = props || {};
    let urlList = [];
    if (Array.isArray(data)) {
    data.forEach(item => {
    const { id = '' } = item || {};
    getUrl(id).then(res => {
    if (res) urlList.push(res);
    });
    });
    console.log(urlList);
    }
    }

    上述代码中 console.log(urlList) 是无法打印出 urlList 的最终结果。因为 getUrl 是异步函数,执行完才给 urlList 添加一个值,而 data.forEach 循环是同步执行的,当 data.forEach 执行完成后,getUrl 可能还没执行完成,从而会导致 console.log(urlList) 打印出来的 urlList 不是最终结果。


    所以我们要使用队列形式让异步函数并发执行,再用 Promise.all 监听所有异步函数执行完毕后,再打印 urlList 的值。


    优化后:

    const App = async (props) => {
    const { data } = props || {};
    let urlList = [];
    if (Array.isArray(data)) {
    const jobs = data.map(async item => {
    const { id = '' } = item || {};
    const res = await getUrl(id);
    if (res) urlList.push(res);
    return res;
    });
    await Promise.all(jobs);
    console.log(urlList);
    }
    }

    十、过度防御


    优化前:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => {
    const { name, age } = item || {};
    return `我的名字是${name},今年${age}岁了`;
    });
    }
    const info = infoList?.join(',');
    }

    infoList 后面为什么要跟 ?,数组的 map 方法返回的一定是个数组。


    优化后:

    const App = (props) => {
    const { data } = props || {};
    let infoList = [];
    if (Array.isArray(data)) {
    infoList = data.map(item => {
    const { name, age } = item || {};
    return `我的名字是${name},今年${age}岁了`;
    });
    }
    const info = infoList.join(',');
    }

    后续


    以上对小王代码的吐槽,最后我只想说一下,以上的错误都是一些 JS 基础知识,跟任何框架没有任何关系。如果你工作了几年还是犯这些错误,真的可以考虑转行。


    作者:红尘炼心
    链接:https://juejin.cn/post/7259007674520158268
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    🤭新的广告方式,很新很新!

    哈哈 会爬树的金鱼,树上的金鱼呦😀 前言 老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭ 产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道! 产品B:对! 大数据分析公司产品的广...
    继续阅读 »

    哈哈 会爬树的金鱼,树上的金鱼呦😀


    前言


    老板:针对上半年业绩发表一下大家的看法,大家自由发言,已经定好晚餐和夜宵了有需要的自取୧(๑•̀⌄•́๑)૭


    产品A:根据大数据分析公司产品的广告收入较低拖后腿,建议扩宽曝光渠道!


    产品B:对! 大数据分析公司产品的广告大部分来自移动端,pc端曝光率较少,可以增加一下曝光率,据统计移动端大部分广告收入来自首屏广告,我觉得pc也可以加上


    程序员: PC哪有首屏广告啊,行业都没有先例


    老板:这个好,没有先例! 我们又可以申请专利了Ψ( ̄∀ ̄)Ψ 搞起!!今年公司专利指标有着落了


    程序员: 这也太影响体验了


    产品A:就说能不能做吧, 今天面试的那个应届生不错能倒背chromium源码,还能手写react源码并且指出优化方案


    程序员: 我做!!! ╭( ̄m ̄*)╮


    先来个全屏遮罩🤔


    这还不简单,直接一个定位搞定

    <!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>XX百货</title>
    <style>
    #ADBox {
    background: #fff;
    position: fixed;
    width: 100%;
    height: 100%;
    display: none;
    }
    </style>
    </head>
    <body>
    <div id="ADBox">广告</div>
    </body>
    </html>


    搞定提测!


    重来没接过这么简单的需求,送业绩这是╮(╯﹏╰)╭


    第一次提测🤐


    测试A: 送业绩这是? 产品说要和移动端一模一样,你这哪一样了?? 直系看需求文档!!


    程序员: 需求文档就一句话, 和移动端一样的开屏广告, 这那不一样了?


    测试A: 这哪一样了?? 你家移动端广告露个顶部出来?看看哪个app广告不是全屏的???


    程序员: 啥? 还要全屏?? 行...


    // 必须用点击事件触发才能全屏
    document.addEventListener("click", async (elem) => {
    const box = document.getElementById("ADBox");
    if (box.requestFullscreen) {
    box.requestFullscreen();
    }
    setTimeout(() => {
    const state = !!document.fullscreenElement;
    // 是否全屏状态
    if (state) {
    // 取消全屏
    if (document.exitFullscreen) {
    document.exitFullscreen();
    }
    }
    }, 5 * 1000);
    });

    搞定提测


    第二次提测🙄


    产品A: 嗯...有点感觉了,这鼠标去掉都遮住广告了,万一广告商不满意投诉怎么办?


    程序员: 鼠标这么小这么能遮住广告??


    产品B: 看我鼠标? (大米老鼠标PC主题)


    程序员: ...

    <style>
    #ADBox {
    background: #fff;
    position: fixed;
    width: 100%;
    height: 100%;
    // 隐藏广告Box让用户点任意地方激活
    opacity: 0;

    }
    </style>

    提测...


    第三次提测🤕


    测试A: 为啥还有鼠标???


    程序员: 怎么那可能还有?


    测试A: 过来看,鼠标不动的话还是会显示鼠标哦,动一下才消失


    程序员: ##..行, 那我直接锁指针

        <script>
    let pointerLockElement = null;
    // 指针锁定或解锁
    document.addEventListener(
    "pointerlockchange",
    () => {
    // 锁定的元素是否为当前的元素 -- 没啥也意义可以去掉
    if (document.pointerLockElement === pointerLockElement) {
    console.log("指针锁定成功了。");
    } else {
    pointerLockElement = null;
    console.log("已经退出锁定。");
    }
    },
    false
    );
    // 锁定失败事件
    document.addEventListener(
    "pointerlockerror",
    () => {
    console.log("锁定指针时出错。");
    },
    false
    );

    // 锁定指针,锁定指针的元素必须让用户点一下才能锁定
    function lockPointer(elem) {
    // 如果已经存锁定的元素则不操作
    if (document.pointerLockElement) {
    return;
    }
    if (elem) {
    pointerLockElement = elem;
    elem.requestPointerLock();
    }
    }

    // 解除锁定
    function unlockPointer() {
    document.exitPointerLock();
    }

    // 必须用点击事件触发才能全屏
    document.addEventListener("click", async () => {
    const box = document.getElementById("ADBox");
    if (box.requestFullscreen) {
    box.requestFullscreen();
    box.style.opacity = 1;
    box.style.display = "block";
    lockPointer(box);
    }
    // 5秒后解除锁定
    setTimeout(() => {
    const state = !!document.fullscreenElement;
    // 是否全屏状态
    if (state) {
    // 取消全屏
    if (document.exitFullscreen) {
    document.exitFullscreen();
    unlockPointer();
    box.style.display = "none";
    }
    }
    }, 5 * 1000);
    });
    </script>

    提测...


    第四次提测😤


    测试A: Safari上失效哦


    程序员: 额....

    <script>

    // requestFullscreen 方法兼容处理
    function useRequestFullscreen(elem) {
    const key = ['requestFullscreen', 'mozRequestFullScreen', 'webkitRequestFullscreen', 'msRequestFullscreen']
    for (const value of key) {
    if (elem[value]) {
    elem[value]()
    return true
    }
    }
    return false
    }

    // document.exitFullscreen 方法兼容处理
    document.exitFullscreenUniversal = document.exitFullscreen || document.webkitExitFullscreen || document.mozCancelFullScreen

    // fullscreenElement 对象兼容处理
    function getFullscreenElement() {
    const key = ['fullscreenElement', 'webkitFullscreenElement']
    for (const value of key) {
    if (document[value]) {
    return document[value]
    }
    }
    return null
    }

    // fullscreenchange 事件兼容处理
    addEventListener("fullscreenchange", endCallback);
    addEventListener("webkitfullscreenchange", endCallback);

    // requestPointerLock 方法在Safari下不可与 requestFullscreen 方法共用一个事件周期 暂无解决方法,必须让用户点两次鼠标,第一次全屏,第二次锁鼠标
    // 同一事件周期内会出现的问题: 1.有小机率会正常执行, 2.顶部出现白条(实际上是个浏览器锁鼠标的提示语,但显示异常了) 3.锁定鼠标失败

    </script>


    结尾😩


    产品A: 效果不错,但还有点小小的瑕疵,为啥要鼠标点一下才能弹广告,改成进入就弹窗吧


    程序员: 要不还是找上次那个应届生来吧,改chromium源码应该能实现╭∩╮(︶︿︶)╭∩╮


    效果预览: http://www.npmstart.top/BSOD.html


    作者:会爬树的金鱼
    链接:https://juejin.cn/post/7270082131962183739
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    项目部署之后页面没有刷新怎么办?

    web
    最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。 浏览器输入url之后,就会进行下面一系列判断,来实现...
    继续阅读 »

    最近项目部署成功之后,突然产品找我,上线之后,页面没有生效,这是怎么回事?我这是第一次部署这个项目,也不太清楚历史问题,接下来就慢慢寻找答案吧, 如果心急的可以直接看后面的总结,下面我们好好聊聊缓存的问题。


    浏览器输入url之后,就会进行下面一系列判断,来实现页面渲染。



    首先讲一下常见的http缓存~


    HTTP缓存常见的有两类:



    • 强缓存:可以由这两个字段其中一个决定





      • expires

      • cache-control(优先级更高)





    • 协商缓存:可以由这两对字段中的一对决定





      • Last-Modified,If-Modified-Since

      • Etag,If--Match(优先级更高)




    强缓存


    使用的是express框架


    expires


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    const time = new Date(Date.now() + 300000).toUTCString()
    res.header('Expires', time)
    res.render('login');
    });

    然后我们在前端页面刷新,我们可以看到请求的资源的响应头里多了一个expires的字段, 取消Disable cache



    刷新



    勾选Disable cache



    但是,Expires已经被废弃了。对于强缓存来说,Expires已经不是实现强缓存的首选。


    因为Expires判断强缓存是否过期的机制是:获取本地时间戳,并对先前拿到的资源文件中的Expires字段的时间做比较。来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”


    是的,Expires过度依赖本地时间,如果本地与服务器时间不同步,就会出现资源无法被缓存或者资源永远被缓存的情况。所以,Expires字段几乎不被使用了。现在的项目中,我们并不推荐使用Expires,强缓存功能通常使用cache-control字段来代替Expires字段。


    cache-control


    其实cache-control跟expires效果差不多,只不过这两个字段设置的值不一样而已,前者设置的是秒数,后者设置的是毫秒数


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'max-age=300')
    res.render('login');
    });

    前端页面响应头多了cache-control这个字段,且300s内都走本地缓存,不会去请求服务端



    Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。


    Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。


    Cache-control的多种属性:developer.mozilla.org/zh-CN/docs/…


    但是使用最多的就是no-cache和no-store,接下来就重点学习这两种


    no-cache和no-store


    no_cache是Cache-control的一个属性。它并不像字面意思一样禁止缓存,实际上,no-cache的意思是强制进行协商缓存。如果某一资源的Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存。而no-store就是禁止所有的缓存策略了。


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-cache')
    res.render('login');
    });

    no-cache(进行协商缓存,下次再次请求,没有勾选控制台Disable cache,状态码是304)



    app.get('/login', function(req, res){
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-store')
    res.render('login');
    });

    no-store(每次都请求服务器的最新资源,没有缓存策略)



    强制缓存就是以上这两种方法了。现在我们回过头来聊聊,Expires难道就一点用都没有了吗?也不是,虽然Cache-control是Expires的完全替代品,但是如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。


    协商缓存


    与强缓存不同的是,强缓存是在时效时间内,不走服务端,只走本地缓存;而协商缓存是要走服务端的,如果请求某个资源,去请求服务端时,发现命中缓存则返回304,否则则返回所请求的资源,那怎么才算命中缓存呢?接下来讲讲


    Last-Modified,If-Modified-Since


    简单来说就是:



    • 第一次请求资源时,服务端会把所请求的资源的最后一次修改时间当成响应头中Last-Modified的值发到浏览器并在浏览器存起来

    • 第二次请求资源时,浏览器会把刚刚存储的时间当成请求头中If-Modified-Since的值,传到服务端,服务端拿到这个时间跟所请求的资源的最后修改时间进行比对

    • 比对结果如果两个时间相同,则说明此资源没修改过,那就是命中缓存,那就返回304,如果不相同,则说明此资源修改过了,则没命中缓存,则返回修改过后的新资源


    基于last-modified的协商缓存实现方式是:



    1. 首先需要在服务器端读出文件修改时间,

    2. 将读出来的修改时间赋给响应头的last-modified字段。

    3. 最后设置Cache-control:no-cache


    三步缺一不可。


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)

    const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
    console.log(mtime.toUTCString(), '--------')
    // 响应头的last-modified字段
    res.header('last-modified', mtime.toUTCString())
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-cache')
    res.render('login');
    });


    当index.css发生改变再次请求时



    终端输出的时间变化



    服务端的时间跟last-modified的值是一致的



    使用以上方式的协商缓存已经存在两个非常明显的漏洞。这两个漏洞都是基于文件是通过比较修改时间来判断是否更改而产生的。


    1.因为是更具文件修改时间来判断的,所以,在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。


    2.当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会 返回新的文件。


    为了解决上述的这两个问题。从http1.1开始新增了一个头信息,ETag(Entity 实体标签)


    Etag,If--Match


    ETag就是将原先协商缓存的比较时间戳的形式修改成了比较文件指纹。


    其实Etag,If--Match跟Last-Modified,If-Modified-Since大体一样,区别在于:



    • 后者是对比资源最后一次修改时间,来确定资源是否修改了

    • 前者是对比资源内容,来确定资源是否修改


    那我们要怎么比对资源内容呢?我们只需要读取资源内容,转成hash值,前后进行比对就行了!


    app.get('/login', function(req, res){
    // 设置 Expires 响应头
    // const time = new Date(Date.now() + 300000).toUTCString()
    // res.header('Expires', time)

    // const { mtime } = fs.statSync(path.join(__dirname, 'public/index.css')) // 读取最后修改时间
    // console.log(mtime.toUTCString(), '--------')
    // 响应头的last-modified字段
    // res.header('last-modified', mtime.toUTCString())


    // 设置ETag
    const ifMatch = req.header['if-none-match']
    const hash = crypto.createHash('md5')
    const fileBuf = fs.readFileSync(path.join(__dirname, 'public/index.css'))
    hash.update(fileBuf, 'utf8')
    const etag = `"${hash.digest('hex')}"`
    console.log(etag, '---etag----')
    // 对比hash值
    if (ifMatch === etag) {
    res.status = 304
    } else {
    res.header('etag', etag)
    // ctx.body = fileBuffer
    }
    // 设置 Cache-Control 响应头
    res.header('Cache-Control', 'no-cache')
    res.render('login');
    });


    当资源发生改变时,状态码变成200,更新缓存


    比如更改css样式



    ETag也有缺点



    • ETag需要计算文件指纹这样意味着,服务端需要更多的计算开销。如果文件尺寸大,数量多,并且计算频繁,那么ETag的计算就会影响服务器的性能。显然,ETag在这样的场景下就不是很适合。

    • ETag有强验证和弱验证,所谓将强验证,ETag生成的哈希码深入到每个字节。哪怕文件中只有一个字节改变了,也会生成不同的哈希值,它可以保证文件内容绝对的不变。但是,强验证非常消耗计算量。ETag还有一个弱验证,弱验证是提取文件的部分属性来生成哈希值。因为不必精确到每个字节,所以他的整体速度会比强验证快,但是准确率不高。会降低协商缓存的有效性。


    值得注意的一点是,不同于cache-control是expires的完全替代方案(说人话:能用cache-control就不要用expiress)。ETag并不是last-modified的完全替代方案。而是last-modified的补充方案(说人话:项目中到底是用ETag还是last-modified完全取决于业务场景,这两个没有谁更好谁更坏)。


    disk cache & memory cache


    磁盘缓存+内存缓存,这两种缓存不属于http缓存,而是本地缓存了~


    我们直接打开掘金官网,点击network,类型选择all



    可以看的很多请求,这里请求包括了静态资源+接口请求


    这里我们能够看的很多请求的size中有很多是disk cache(磁盘缓存)


    也有一些图片是memory cache(内存缓存)



    这两者有什么区别呢?


    disk cache: 磁盘缓存,很明显将内容存储在计算机硬盘中,很明显,这种缓存可以占用比较大的空间,但是由于是读取硬盘,所以速度低于内存


    memory cache: 内存缓存,速度快,优先级高,但是大小受限于计算机的内存大小,很大的资源还是缓存到硬盘中


    上面的浏览器缓存已经有三个大点了,那它们的优先级是什么样的呢?


    缓存的获取顺序如下:


    1.内存缓存


    2.磁盘缓存


    3.强缓存


    4.协商缓存


    如果勾选了Disable cache,那磁盘缓存都不存在了,之还有内存缓存



    我还发现,勾选了Disable cache,就base64图片一定会在内存缓存中,其他图片则会发起请求;而不勾选了Disable cache,则大多数图片都在内存缓存中




    CDN缓存


    CDN缓存是一种服务端缓存,CDN服务商可以将源站上的资源缓到其各地的边缘服务器节点上。当用户访问该资源时,CDN再通过负载均衡将用户的请求调度到最近的缓存节点上,有效减少了链路回源,提高了资源访问效率及可用性,降低带宽消耗。


    如果客户端没有命中缓存,那接下来就要发起一次网络请求,根据网络环境,一般大型站点都会配置CDN,CDN会找一个最合适的服务节点接管网络请求。CDN节点都会在本地缓存静态文件数据,一旦命中直接返回,不会穿透去请求应用服务器。并且CDN会通过在不同的网络,策略性地通过部署边缘服务器和应用大量的技术和算法,把用户的请求指定到最佳响应节点上。所以会减少非常多的网络开销和响应延迟。


    如果没有部署CDN或者CDN没有命中,请求最终才会落入应用服务器,现在的http服务器都会添加一层反向代理,例如nginx,在这一层同样会添加缓存层,代表技术是squid,varnish,当然nginx作为http服务器而言也支持静态文件访问和本地缓存技术,当然也可以使用远程缓存,如redis,memcache,这里缓存的内容一般为静态文件或者由服务器已经生成好的动态页面,在返回用户之前缓存。


    如果前面的缓存机制全部失效,请求才会落入真正的服务器节点。


    总结


    1.如果页面是协商缓存,如何获取页面最新内容?


    协商缓存比较好办,那就刷新页面,不过需要勾选Disable cache,但是用户不知道打开控制台怎么办?


    那就右击页面的刷新按钮,然后选择硬性重新加载,或者清空缓存并硬性重新加载,页面就获取到最新资源了



    2.如果页面没有设置cache-control,那默认的缓存机制是什么样的?



    默认是协商缓存,这也符合浏览器设计,缓存可以减少宽度流量,加快响应速度


    3.如果项目重新部署还是没有更新,怎么办?


    在确定项目已经部署成功


    这样子,可以去问一下公司的运维同事,这个项目是否有CDN缓存


    如果项目的域名做了CDN缓存,就需要刷新CDN目录,来解决缓存问题了,不然就只能等,等CDN策略失效,来请求最新的内容


    向如下配置的缓存策略,只有过30天才会去真正服务器去请求最新内容



    当然你可以测试一下是否为CDN缓存,在url后面拼接一个参数,就能够获取到最新资源了,比如有缓存的链接是baidu.com/abc


    你可以在浏览器中输入baidu.com/abc&t=1234来…


    当然特定场景,我们不能随意给链接后面添加参数,所以这也只适用于测试一下是否有CDN缓存


    所以最好的解决办法还是需要让运维同事去刷新目录,这样就能快速解决CDN缓存问题。


    参考链接


    juejin.cn/post/712719…


    juejin.cn/post/717756…


    xiaolincoding.com/

    network/2_h…

    收起阅读 »

    Android 时钟翻页效果

    web
    背景 今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。 原文链接:juejin.cn/post/724435… 具体实现分析请看上文原文链接,那我们开始吧! 容器 val space = 10f //上下半间隔 val...
    继续阅读 »

    背景


    今天逛掘金,发现了一个有意思的web view效果,想着Android能不能实现一下捏。


    image.png
    原文链接:juejin.cn/post/724435…


    具体实现分析请看上文原文链接,那我们开始吧!


    容器


    val space = 10f //上下半间隔
    val bgBorderR = 10f //背景圆角
    //上半部分
    val upperHalfBottom = height.toFloat() / 2 - space / 2
    canvas.drawRoundRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom,
    bgBorderR,
    bgBorderR,
    bgPaint
    )
    //下半部分
    val lowerHalfTop = height.toFloat() / 2 + space / 2
    canvas.drawRoundRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat(),
    bgBorderR,
    bgBorderR,
    bgPaint
    )

    image.png


    绘制数字


    我们首先居中绘制数字4


    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    //居中显示
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom
    canvas.drawText(number4, x, y, textPaint)

    image.png


    接下来我们将数字切分为上下两部分,分别绘制。


    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom
    // 上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    // 下半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    canvas.drawText(number4, x, y, textPaint)

    image.png


    翻转卡片


    如何实现让其旋转呢?
    而且还得是3d的效果了。我们选择Camera来实现。
    我们先让数字'4'旋转起来。


    准备工作,通过属性动画来改变旋转的角度。


    private var degree = 0f //翻转角度
    private val camera = Camera()
    private var flipping = false //是否处于翻转状态
    ...
    //动画
    val animator = ValueAnimator.ofFloat(0f, 360f)
    animator.addUpdateListener { animation ->
    val animatedValue = animation.animatedValue as Float
    setDegree(animatedValue)
    }
    animator.doOnStart {
    flipping = true
    }
    animator.doOnEnd {
    flipping = false
    }
    animator.duration = 1000
    animator.interpolator = LinearInterpolator()
    animator.start()
    ...

    private fun setDegree(degree: Float) {
    this.degree = degree
    invalidate()
    }

    让数字'4'旋转起来:


      override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    // 居中绘制数字4
    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom

    if (!flipping) {
    canvas.drawText(number4, x, y, textPaint)
    } else {
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    camera.rotateX(-degree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    }
    }

    file.gif

    我们再来看一边效果图:
    我们希望将卡片旋转180度,并且0度-90度由上半部分完成,90度-180度由下半部分完成。


    我们调整一下代码,先处理一下上半部分:


    ...
    val animator = ValueAnimator.ofFloat(0f, 180f)
    ...
    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val space = 10f //上下半间隔
    //上半部分
    val upperHalfBottom = height.toFloat() / 2 - space / 2
    ...
    // 居中绘制数字4
    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom

    if (!flipping) {
    //上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    } else {
    if (degree < 90) {
    //上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    camera.rotateX(-degree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    }
    }
    }

    效果如下:


    upper.gif

    接下来我们再来看一下下半部分:


    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    val space = 10f //上下半间隔
    //下半部分
    val lowerHalfTop = height.toFloat() / 2 + space / 2

    // 居中绘制数字4
    val number4 = "4"
    textPaint.getTextBounds(number4, 0, number4.length, textBounds)
    val x = (width - textBounds.width()) / 2f - textBounds.left
    val y = (height + textBounds.height()) / 2f - textBounds.bottom

    if (!flipping) {
    // 下半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    } else {
    if (degree > 90) {
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    val bottomDegree = 180 - degree
    camera.rotateX(bottomDegree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    }
    }
    }

    lower.gif

    那我们将上下部分结合起来,效果如下:


    all.gif

    数字变化


    好!我们完成了翻转部分,现在需要在翻转的过程中将数字改变:

    我们还是举例说明:数字由'4'变为'5'的情况。我们思考个问题,什么时候需要改变数字?

    上半部分在翻转开始的时候,上半部分底部显示的数字就应该由'4'变为'5',但是旋转的部分还是应该为'4',
    下半部分开始旋转的时候底部显示的数字还是应该为'4',而旋转的部分该为'5'。


    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    canvas.drawText(number5, x, y, textPaint)
    canvas.restore()
    // 下半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    //=====⬆️=====上述的代码显示的上下底部显示的内容,即上半部分地步显示5,下半部分显示4
    if (degree < 90) {
    //上半部分裁剪
    canvas.save()
    canvas.clipRect(
    0f,
    0f,
    width.toFloat(),
    upperHalfBottom
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    camera.rotateX(-degree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number4, x, y, textPaint)
    canvas.restore()
    //=====⬆️=====上述的代码表示上半部分旋转显示的内容,即数字4
    } else {
    canvas.save()
    canvas.clipRect(
    0f,
    lowerHalfTop,
    width.toFloat(),
    height.toFloat()
    )
    camera.save()
    canvas.translate(width / 2f, height / 2f)
    val bottomDegree = 180 - degree
    camera.rotateX(bottomDegree)
    camera.applyToCanvas(canvas)
    canvas.translate(-width / 2f, -height / 2f)
    camera.restore()
    canvas.drawText(number5, x, y, textPaint)
    canvas.restore()
    //=====⬆️=====上述的代码表示下半部分旋转显示的内容,即数字5
    }

    效果图如下:大伙可以在去理一下上面数字的变化的逻辑。


    a.gif

    最后我们加上背景再看一下效果:


    a.gif

    小结


    上述代码仅仅提供个思路,仅为测试code,正式代码可不

    作者:蹦蹦蹦
    来源:juejin.cn/post/7271518821809438781
    能这么写哦 >..<

    收起阅读 »

    虚拟列表 or 时间分片

    前言 最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。 为啥要用虚拟列表呢! 在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布...
    继续阅读 »

    前言


    最近在做一个官网,原本接口做的都是分页的,但是客户提出不要分页,之前看过虚拟列表这个东西,所以进行一下了解。


    为啥要用虚拟列表呢!


    在日常工作中,所要渲染的也不单单只是一个li那么简单,会有很多嵌套在里面。但数据量过多,同时渲染式,会在 渲染样式 跟 布局计算上花费太多时间,体验感不好,那你说要不要优化嘛,不是你被优化就是你优化它。


    进入正题,啥是虚拟列表?


    可以这么理解,根据你视图能显示多少就先渲染多少,对看不到的地方采取不渲染或者部分渲染。




    这时候你完成首次加载,那么其他就是在你滑动时渲染,就可以通过计算,得知此时屏幕应该显示的列表项。


    怎么弄?


    备注:很多方案对于动态不固定高度、网络图片以及用户异常操作等形式处理的也并不好,了解下原理即可。


    虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。


    1、计算当前可视区域起始数据索引(startIndex)

    2、计算当前可视区域结束数据索引(endIndex)

    3、计算当前可视区域的数据,并渲染到页面中

    4、计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上


    由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

    <div class="infinite-list-container">
    <div class="infinite-list-phantom"></div>
    <div class="infinite-list">
    <!-- item-1 -->
    <!-- item-2 -->
    <!-- ...... -->
    <!-- item-n -->
    </div>
    </div>


    • infinite-list-container 为可视区域的容器

    • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条

    • infinite-list 为列表项的渲染区域

      接着,监听infinite-list-containerscroll事件,获取滚动位置scrollTop

    • 假定可视区域高度固定,称之为screenHeight

    • 假定列表每项高度固定,称之为itemSize

    • 假定列表数据称之为listData

    • 假定当前滚动位置称之为scrollTop

    •   则可推算出:

      • 列表总高度listHeight = listData.length * itemSize
      • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
      • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
      • 数据的结束索引endIndex = startIndex + visibleCount
      • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

        当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

      • 偏移量startOffset = scrollTop - (scrollTop % itemSize);

    时间分片


    那么虚拟列表是一方面可以优化的方式,另一个就是时间分片。


    先看看我们平时的情况


    1.直接开整,直接渲染。




    诶???我们可以发现,js运行时间为113ms,但最终 完成时间是 1070ms,一共是 js 运行时间加上渲染总时间。

    PS:

    • 在 JS 的 EventLoop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
    • 第一个 console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间
    • 第二个 console.log是放到 setTimeout 中的,它的触发时间是在渲染完成,在下一次 EventLoop中执行的

    那我们改用定时器


    上面看是因为我们同时渲染,那我们可以分批看看。

    let once = 30
    let ul = document.getElementById('testTime')
    function loopRender (curTotal, curIndex) {
    if (curTotal <= 0) return
    let pageCount = Math.min(curTotal, once) // 每页最多20条
    setTimeout(_ => {
    for (let i=0; i<pageCount;i++) {
    let li = document.createElement('li')
    li.innerHTML = curIndex + i
    ul.appendChild(li)
    }
    loopRender(curTotal - pageCount, curIndex + pageCount)
    }, 0)
    }
    loopRender(100000, 0)

    这时候可以感觉出来渲染很快,但是如果渲染复杂点的dom会闪屏,为什么会闪屏这就需要清楚电脑刷新的概念了,这里就不详细写了,有兴趣的小朋友可以自己去了解一下。

    可以改用 requestAnimationFrame 去分批渲染,因为这个关于电脑自身刷新效率的,不管你代码的事,可以解决丢帧问题。

    let once = 30
    let ul = document.getElementById('container')
    // 循环加载渲染数据
    function loopRender (curTotal, curIndex) {
    if (curTotal <= 0) return
    let pageCount = Math.min(curTotal, once) // 每页最多20条
    window.requestAnimationFrame(_ => {
    for (let i=0; i<pageCount;i++) {
    let li = document.createElement('li')
    li.innerHTML = curIndex + i
    ul.appendChild(li)
    }
    loopRender(curTotal - pageCount, curIndex + pageCount)
    })
    }
    loopRender(100000, 0)

    还可以改用 DocumentFragment


    什么是 DocumentFragment



    DocumentFragment,文档片段接口,表示一个没有父级文件的最小文档对象。它被作为一个轻量版的 Document使用,用于存储已排好版的或尚未打理好格式的XML片段。最大的区别是因为 DocumentFragment不是真实DOM树的一部分,它的变化不会触发DOM树的(重新渲染) ,且不会导致性能等问题。

    可以使用 document.createDocumentFragment方法或者构造函数来创建一个空的 DocumentFragment

    ocumentFragments是DOM节点,但并不是DOM树的一部分,可以认为是存在内存中的,所以将子元素插入到文档片段时不会引起页面回流。



    当 append元素到 document中时,被 append进去的元素的样式表的计算是同步发生的,此时调用 getComputedStyle 可以得到样式的计算值。而 append元素到 documentFragment 中时,是不会计算元素的样式表,所以 documentFragment 性能更优。当然现在浏览器的优化已经做的很好了, 当 append元素到 document中后,没有访问 getComputedStyle 之类的方法时,现代浏览器也可以把样式表的计算推迟到脚本执行之后。

    let once = 30 
    let ul = document.getElementById('container')
    // 循环加载渲染数据
    function loopRender (curTotal, curIndex) {
    if (curTotal <= 0) return
    let pageCount = Math.min(curTotal, once) // 每页最多20条
    window.requestAnimationFrame(_ => {
    let fragment = document.createDocumentFragment()
    for (let i=0; i<pageCount;i++) {
    let li = document.createElement('li')
    li.innerHTML = curIndex + i
    fragment.appendChild(li)
    }
    ul.appendChild(fragment)
    loopRender(curTotal - pageCount, curIndex + pageCount)
    })
    }
    loopRender(100000, 0)

    其实同时渲染十万条数据这个情况还是比较少见的,就当做个了解吧。


    作者:NIIBLE
    链接:https://juejin.cn/post/7263009476058742840
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    CSS命名太头疼?这个Vite插件自动生成,让你解放双手!

    web
    CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生...
    继续阅读 »

    CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生成原子CSS类呢?今天我要介绍的Vite插件atom-css-generator就可完美实现这一功能。


    原子CSS简介


    原子CSS(Atomic CSS)将传统的CSS类拆分成一个个独立的、原子级的类,每个类仅包含一个CSS属性,例如:



    .p-10 {
    padding: 10px;
    }

    .bg-red {
    background: red;
    }

    相比于传统的CSS类,原子类具有以下特点:



    • 原子:每个类只包含一个CSS属性,拆分到最小粒度

    • 独立:类名语义明确,可以任意组合使用而不会产生冲突

    • 可复用:一个原子类可以重复使用在不同的组件中


    使用原子CSS的优势在于:



    • 更模块化:样式属性高内聚、解耦

    • 更可维护:不同类名称、不同文件,避免影响

    • 更灵活:组件样式由原子类组合,更容易扩展和维护


    但是编写大量原子类也比较麻烦,多达几千个类定义都可能出现。有没有自动生成的方式呢?


    atom-css-generator插件介绍


    atom-css-generator是一个Vite插件,它可以通过解析Vue组件中的class,自动生成对应的原子CSS定义


    安装和配置


    使用npm或yarn安装:


    Copy code

    npm install atom-css-generator

    在vite.config.js中引入插件:


    js

    Copy code

    import atomCssGenerator from 'atom-css-generator';

    export default {
    plugins: [
    atomCssGenerator({
    outputPath: 'assets/styles'
    })
    ]
    }

    主要的配置项有:



    • outputPath:指定生成的CSS文件输出目录,默认为public


    使用方式



    1. 在Vue组件的template中,使用特定格式的class,例如:


    html

    Copy code

    <template>
    <div class="bg-red fc-white p-20">
    <!-- ... -->
    </div>
    </template>


    1. 构建项目时,插件会自动生成对应的原子CSS类定义:


    css

    Copy code

    .bg-red {
    background-color: red;
    }

    .fc-white {
    color: white;
    }

    .p-20 {
    padding: 20px;
    }


    1. style.css会被自动生成到指定的outputPath中,并注入到HTML文件头部。


    支持的类名格式


    插件支持多种格式的类名规则生成,包括:



    • 颜色类名:bg-red、fc-333

    • 间距类名:p-20、ml-10

    • 尺寸类名:w-100、h-200

    • Flexbox类名:jc-center、ai-stretch

    • 边框类名:bc-333、br-1-f00-solid

    • 布局类名:p-relative、p-fixed

    • 文字类名:fs-14、fw-bold


    等等,非常全面。


    而且也内置了一些预设的实用样式类,比如文字截断类te-ellipsis。


    原理简析


    插件主要通过以下处理流程实现自动生成原子CSS:



    1. 使用@vue/compiler-sfc解析Vue文件,获取模板内容

    2. 通过正则表达式提取模板中的class名称

    3. 根据特定类名规则,生成对应的CSS定义

    4. 将CSS写入style.css文件中,并注入到HTML中


    同时,插件还会在热更新时自动检查新添加的类名,从而动态更新style.css。


    总结


    通过atom-css-generator这个插件,我们可以非常轻松地在Vue项目中使用原子CSS样式,而不需要手动编写。它省去了我们大量重复的工作,使得样式的维护和扩展更加简单。


    如果你也想尝试在自己的项目中引入原子CSS,不妨试试这个插件。相信它能给你带来意想不到的便利!
    GitHub地址

    收起阅读 »

    自建”IT兵器库”,你值得一看!

    web
    现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!! 常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小...
    继续阅读 »

    现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!


    常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。


    接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步


    需求:


    动态列表格,就是一个表格,存在默认列,但是支持我们操控,实现动态效果


    实现效果


    默认表格配置


    image.png


    默认列配置


    image.png


    动态列组件支持查询


    image.png


    动态列组件支持勾选


    image.png


    动态列组件支持清空


    image.png


    动态列组件支持一键全选


    image.png


    动态列组件支持一键清空


    image.png


    功能点划分



    • 表格默认列和动态列组件默认选中项 实现双向绑定

    • 动态列组件 增删改与表格 实现双向绑定

    • 动态列组件 实现搜索

    • 动态列组件 实现单点控制 添加与删除

    • 动态列组件 实现一键控制功能 全选与清空

    • 动态列组件 实现恢复初始态


    使用到组件(Antd 组件库哈)



    • Table

    • Pagination

    • Modal

    • Input

    • Button

    • Checkbox


    动态列组件区域划分



    • 头部标题

    • 头部提示语

    • 核心内容区



      • 核心区域头部功能按钮





      • 搜索区域





      • 左边所有内容项





      • 待选内容项




    动态列组件最终可支持配置项


      open?: boolean // Modal状态
    setOpen?: React.Dispatch> // 控制Modal状态
    modalTitle?: string | React.ReactNode
    modalWidth?: number
    modalHeadContent?: React.ReactNode
    leftHeadContent?: React.ReactNode | string
    rightHeadContent?: React.ReactNode | string
    modalBodyStyle?: any
    searchPlaceholder?: string
    modalOk?: (val, isUseDefaultData?: boolean) => void // 第二个参数 内部数据处理支持
    enableSelectAll?: boolean // 是否开启全选功能
    selectData: SelectItem[] // 下拉框数据
    isOutEmitData?: boolean
    defaultSelectKeyList?: string[] // 默认选中的key(当前表格列)自定义数据(外部做逻辑处理)
    initSelectKey?: string[] // 初始表格选中的key 自定义数据(外部做逻辑处理)
    curColumns?: any[] // 当前表格列 内部做逻辑处理
    originColumns?: any[] // 原始表格列 内部做逻辑处理
    isDelHeadCol?: boolean // 删除头部columnKey 例如序号 只有内部处理数据时生效
    isDelTailCol?: boolean // 删除尾部columnKey 例如操作 只有内部处理数据时生效
    isDelHeadAndTail?: boolean // 删除头尾部columnKey 只有内部处理数据时生效

    动态列组件布局



        

    // 头部内容区
    {modalHeadContent}

    // 以下维核心区


    // 核心区-左边

    // 核心区-功能按钮 - 一键全选
    {enableSelectAll && (

    onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
    全选


    )}
    {leftHeadContent || ''}



    // 核心区-左搜索
    {childSearchRender({ curData: leftSelectData })}

    // 核心区-左列表区域
    {selectItemRender(leftSelectData)}



    // 核心区-右边


    {rightHeadContent || ''}

    // 核心区-功能按钮 - 一键清空
    handleRightClearSelectData()}>
    清空



    // 核心区-右搜索
    {childSearchRender({ curData: rightSelectData }, true)}

    // 核心区-右列表区域
    {selectItemRender(rightSelectData, true)}






    动态列组件-列表渲染


    const selectItemRender = (listArr = [], isRight = false) => {
    return (


    // 数据遍历形式
    {listArr?.map(({ label, value, disabled = false }) => (

    {!isRight && (

    {label}

    )}
    // 判断是否是 右边列表区域 添加删除按钮
    {isRight && {label}}
    {isRight && (



    )}

    ))}


    )
    }

    动态列组件-搜索渲染


    const childSearchRender = (childSearchProps: any, isRight = false) => {
    // eslint-disable-next-line react/prop-types
    const { curData } = childSearchProps
    return (
    {
    onSearch(e, curData, isRight)
    }}
    allowClear
    />
    )
    }

    动态列组件样式


    .content-box {
    width: 100%;
    height: 550px;
    border: 1px solid #d9d9d9;
    }
    .content-left-box {
    border-right: 1px solid #d9d9d9;
    }
    .content-left-head {
    padding: 16px 20px;
    background: #f5f8fb;
    height: 48px;
    box-sizing: border-box;
    }
    .content-right-head {
    padding: 16px 20px;
    background: #f5f8fb;
    height: 48px;
    box-sizing: border-box;

    &-clear {
    color: #f38d29;
    cursor: pointer;
    }
    }
    .content-right-box {
    }
    .content-left-main {
    padding: 10px 20px 0 20px;
    height: calc(100% - 50px);
    box-sizing: border-box;
    }
    .content-right-main {
    padding: 10px 20px 0 20px;
    height: calc(100% - 50px);
    box-sizing: border-box;
    }
    .right-head-content {
    font-weight: 700;
    color: #151e29;
    font-size: 14px;
    }
    .modal-head-box {
    color: #151e29;
    font-size: 14px;
    height: 30px;
    }
    .icon-box {
    color: #f4513b;
    }
    .ant-checkbox-group {
    flex-wrap: nowrap;
    }
    .left-select-box {
    height: 440px;
    padding-bottom: 10px;
    }
    .right-select-box {
    height: 440px;
    padding-bottom: 10px;
    }
    .ant-checkbox-wrapper {
    align-items: center;
    }
    .display-box {
    height: 22px;
    }

    功能点逐一拆解实现


    点1:表格默认列和动态列组件默认选中项 实现双向绑定



    • 首先,先写一个表格啦,确定列,这个就不用代码展示了吧,CV大法

    • 其次,把表格原始列注入动态列组件当中,再者注入当前表格列当前能选择的所有项

    • 当前能选择所有项内容参数示例


    [
    { label: '项目编码', value: 'projectCode' },
    { label: '项目名称', value: 'projectName' },
    { label: '项目公司', value: 'company' },
    { label: '标段', value: 'lot' },
    ]




    • 动态组件内部默认选中当前表格列

    • 这里需要把表格列数据 进行过滤 映射成 string[]


       内部是通过checkbox.Grop 实现选中 我们只需要 通过一个状态去控制即可 `selectKey`
    <Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
    ...
    Checkbox.Group>

    动态列组件 单点控制 增删改



    • 增,删,改就是实现 左右边列表的双向绑定

    • 监听 左边勾选事件 + 右边删除事件 + 一键清空事件

    • 通过左右两边的状态 控制数据即可

    • 状态


      const [originSelectData, setOriginSelectData] = useState([]) // 下拉原始数据 搜索需要
    const [originRightSelectData, setOriginRightSelectData] = useState([])
    const [rightSelectData, setRightSelectData] = useState([])
    const [selectKey, setSelectKey] = useState([])
    const [transferObj, setTransferObj] = useState({})
    const [indeterminate, setIndeterminate] = useState(false)
    const [checkAll, setCheckAll] = useState(false)
    const [leftSelectData, setLeftSelectData] = useState([])
    const [defaultSelectKey, setDefaultSelectKey] = useState([])
    const [originSelectKey, setOriginSelectKey] = useState([])

    const onCheckChange = checkedValues => {
    // 往右边追加数据
    const selectResArr = checkedValues?.map(val => transferObj[val])
    setSelectKey(checkedValues) // 我们选中的key (选项)
    setRightSelectData(selectResArr) // 右边列表数据
    setOriginRightSelectData(selectResArr) // 右边原数据(搜索时候需要)
    }

    const deleteRightData = key => {
    const preRightData = rightSelectData
    const filterResKeyArr = preRightData?.filter(it => it.value !== key).map(it => it.value) // 数据处理 只要你不等于删除的key 保留
    const filterResItemArr = preRightData?.filter(it => it.value !== key)
    setRightSelectData(filterResItemArr) // 更新右边数据 即可刷新右边列表
    setOriginRightSelectData(filterResItemArr) // 更新右边数据
    setSelectKey(filterResKeyArr) // 更新选中的key 即可刷新左边选中项
    }

      const handleRightClearSelectData = () => {
    // 这就暴力了塞
    setSelectKey([])
    setRightSelectData([])
    setOriginRightSelectData([])
    }

    动态列组件 实现搜索



    • 搜索,就是改变一下数据 视觉上看起来有过滤的效果塞

    • 刚才我们不是多存了一份数据源嘛

    • 出来见见啦~


    const onSearch = (val, curData, isRight = false) => {
    const searchKey = val
    // 这个是同时支持 左右两边
    // 做个判断
    if (!isRight) {
    // 在判断一下是否有搜索内容 因为也需要清空的啦
    if (searchKey) {
    // 有,我就过滤呗
    const searchResArr = curData?.filter(item => item.label.includes(searchKey))
    setLeftSelectData(searchResArr)
    }
    if (!searchKey) {
    // 没有 我就把原本数据还给你呗
    setLeftSelectData(originSelectData)
    }
    }
    // 右边 一样
    if (isRight) {
    if (searchKey) {
    const searchResArr = curData?.filter(item => item.label.includes(searchKey))
    setRightSelectData(searchResArr)
    }
    if (!searchKey) {
    setRightSelectData(originRightSelectData)
    }
    }
    }

    动态列组件 增删改与表格 实现数据绑定



    • 里面的数据 处理好了 直接再关闭的时候 丢给外面嘛

    • 把右边的内容(也就是选中的key)返回给表格

    • 表格再自己构造


      const handleOk = (colVal, isUseDefaultCol) => {
    `colVal` : 选中的列key
    `isUseDefaultCol`:是否使用默认列
    // table column字段组装
    const normalColConstructor = (
    title,
    dataIndex,
    isSort = true,
    colWidth = 150,
    isEllipsis = false,
    render = null
    ) => {
    const renderObj = render ? { render } : {}
    return {
    title,
    dataIndex,
    sorter: isSort,
    width: colWidth,
    ellipsis: isEllipsis,
    key: dataIndex,
    ...renderObj,
    }
    }
    const statusRender = text => approvalStatusRender(text)
    const dateRender = (text, record) => {dayjs(text).format('YYYY-MM-DD')}
    const newColArr = []
    // 定制化处理 (其实还有2.0)
    colVal?.forEach(({ label, value }, index) => {
    let isSort = false
    let renderFn = null
    const isSubmissionAmount = value === 'submissionAmount'
    const isApprovalAmount = value === 'approvalAmount'
    const isReductionRate = value === 'reductionRate'
    const isInitiationTime = value === 'initiationTime'

    // 特定的业务场景 特殊状态渲染
    const isStatus = value === 'status'
    // 特定的业务场景 时间类型 加上排序
    if (isApprovalAmount || isInitiationTime || isReductionRate || isSubmissionAmount) {
    isSort = true
    }
    if (isStatus) {
    renderFn = statusRender
    }
    // 普通列 已就绪
    // 普通列 标题 拿label就ok
    newColArr.push(normalColConstructor(label, value, isSort, 100, true, renderFn))
    })

    // 最后在头部追加一个序号
    newColArr.unshift({
    title: '序号',
    dataIndex: 'orderCode',
    width: 45,
    render: (_: any, record: any, index) => tablePageSize * (tablePage - 1) + index + 1,
    })
    // 最后在尾部部追加一个操作
    newColArr.push({
    title: '操作',
    dataIndex: 'action',
    fixed: 'right',
    width: 50,
    render: (text, row: DataType) => (



    ),
    })

    if (colVal?.length) {
    if (isUseDefaultCol) {
    setColumns([...originColumns])
    } else {
    setColumns([...newColArr])
    }
    } else {
    setColumns([...originColumns])
    }
    }

    // 解决表格存在子列 -- 先搞定数据结构 -- 在解决表格列内容填充 -- 无需捆绑
    // 遍历拿到新增列数组 匹配上代表 需要子数组 进行转换
    // eslint-disable-next-line consistent-return
    colVal?.forEach(({ label, value }, index) => {
    // DesignHomeDynamicCol[value] 返回的值能匹配到 说明存在 嵌套列
    const validVal = DesignHomeDynamicClassify[DesignHomeDynamicCol[value]]
    const isHasChild = newColChildObj[validVal]
    const titleText = DesignHomeDynamicLabel[value]
    if (validVal) {
    // 如果已经有孩子 追加子列
    if (isHasChild) {
    newColChildObj[validVal] = [...isHasChild, normalColConstructor(titleText, value)]
    } else {
    // 则 新增
    newColChildObj[validVal] = [normalColConstructor(titleText, value)]
    }
    } else {
    // 普通列 已就绪
    // 普通列 标题 拿label就ok
    newColArr.push(normalColConstructor(label, value, false, 100, true))
    }
    })

    动态列组件 实现恢复初始态 实现双向绑定



    • 这个就更简单啦 再点击确定的时候 传一个 isUseDefaultData:true

    • 只是这个isUseDefaultData 的逻辑判断问题

    • 当动态列组件 点击恢复默认列 我们只需把 当初传进来的 原始列数据 更新到 selectKey 状态即可


    const handleDefaultCol = () => {
    // 这里是考虑到组件灵活性 数据可由自己处理好在传入
    if (isOutEmitData) {
    setSelectKey(initSelectKey)
    } else {
    // 这里是使用 内部数据处理逻辑
    setSelectKey(originSelectKey)
    }
    }

    const handleOk = () => { 
    // 数据比对 是否使用默认校验
    // originColumnMapSelectKey 源数据与传出去的数据 进行比对
    const originRightMapKey = originRightSelectData?.map(it => it.value)
    // 采用 lodash isEqual 方法
    const isSame = isEqual(originSelectKey, originRightMapKey)
    // 判断外部是否有传 确定事件 handleOk
    if (modalOk) {
    modalOk(originRightSelectData, isSame)
    }
    setOpen(false)
    }

    const handleOk = (colVal, isUseDefaultCol) => {
    ... 一堆代码
    // 当用户清空以后 还是恢复表格默认状态
    if (colVal?.length) {
    // 恢复默认列
    if (isUseDefaultCol) {
    setColumns([...originColumns])
    } else {
    // 否则就拿新数据更新
    setColumns([...newColArr])
    }
    } else {
    setColumns([...originColumns])
    }
    }

    动态列组件 实现一键控制功能 全选与清空



    • 这就是Vip版本的噻

    • 但是也简单 无非就是操作多选框 无非多选框就三种态

    • 未选 半选 全选

    • 既然我们下面的逻辑已处理好 这个其实也很快的锅

    • 首先,就是下面数据变化的时候 我们上面需要去感应

    • 其次就是 上面操作的时候 下面也需要感应

    • 最后 双向数据绑定 就能搞定 没有那么神秘

    • 一步一步来 先分别把 上下事件处理好


    const onCheckBoxChange = (dataArr = [], e = null) => {
    // 判断所有数据长度
    const allLen = originSelectData?.length
    // 根据当前选中数据长度 判断是 多选框三种状态当中的哪一种
    const checkLen = e ? selectKey?.length : dataArr?.length // 全选
    const isAllSelect = allLen === checkLen // 半选
    const isHalfSelect = allLen > checkLen
    // 然后再判断一下是点击一键全选事件 触发还是 点击下面选项的时候触发
    // 点击一键全选 能拿到事件的 e.target 从而来判断
    // 这里是操作下面按钮的时候 触发
    if (!e) {
    // 如果没有选中
    if (checkLen === 0) {
    // 恢复未选状态
    setCheckAll(false)
    setIndeterminate(false)
    return ''
    }
    if (isAllSelect) {
    // 如果是全选 改为全选态
    setCheckAll(true)
    setIndeterminate(false)
    }
    if (isHalfSelect) {
    // 半选态
    setIndeterminate(true) // 这个控制 多选框的半选态
    setCheckAll(false)
    }
    }
    // 这个就是用户操作 一键全选按钮触发
    if (e) {
    // 如果当前长度为0 那么应该更新为全选
    if (checkLen === 0) {
    setCheckAll(true)
    setIndeterminate(false)
    setSelectKey(originSelectData?.map(it => it.value))
    }
    // 如果已经全选 就取消全选
    if (isAllSelect) {
    setCheckAll(false)
    setIndeterminate(false)
    setSelectKey([])
    }
    // 如果是半选态 就全选
    if (isHalfSelect) {
    setCheckAll(true)
    setIndeterminate(false)
    setSelectKey(originSelectData?.map(it => it.value))
    }
    }
    }

    const onCheckChange = checkedValues => {
    // 往右边追加数据
    const selectResArr = checkedValues?.map(val => transferObj[val]) setSelectKey(checkedValues)
    setRightSelectData(selectResArr)
    setOriginRightSelectData(selectResArr)
    }


    • 我们两个事件都处理好 那么开始进行联动

    • 意思就是 我们拿什么去控制这两个机关 核心是不是就是 选中的选项啊

    • 有两种解法,第二种可能有点绕



      • 一、就是在下面每个选项改变的时候 都去调一下 上面一键全选事件;然后上面变化的时候 也去调一下 下面选项的事件 (常规)缺点:容易漏 一变多改





      • 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效




    // 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效 
    useEffect(() => {
    onCheckBoxChange(selectKey)
    onCheckChange(selectKey)
    // eslint-disable-next-line react-hooks/exhaustive-deps },
    [selectKey]
    )

    结束


    都看到这里了,不留点痕迹,是怕我发现么?

    作者:造更多的轮子
    来源:juejin.cn/post/7266463919139684367

    收起阅读 »

    卸下if-else 侠的皮衣!- 适配器模式

    web
    🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
    继续阅读 »

    🤭当我是if-else侠的时候


    😶怕出错


    给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


    😑难调试


    我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


    🤨交接容易挨打


    当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


    🤔脱下if-else侠的皮衣


    先学习下开发的设计原则


    单一职责原则(SRP)



    就一个类而言,应该仅有一个引起他变化的原因



    开放封闭原则(ASD)



    类、模块、函数等等应该是可以扩展的,但是不可以修改的



    里氏替换原则(LSP)



    所有引用基类的地方必须透明地使用其子类的对象



    依赖倒置原则(DIP)



    高层模块不应该依赖底层模块



    迪米特原则(LOD)



    一个软件实体应当尽可能的少与其他实体发生相互作用



    接口隔离原则(ISP)



    一个类对另一个类的依赖应该建立在最小的接口上



    在学习下设计模式


    大致可以分三大类:创建型结构型行为型

    创建型:工厂模式 ,单例模式,原型模式

    结构型:装饰器模式,适配器模式,代理模式

    行为型:策略模式,状态模式,观察者模式


    上篇文章学习了 策略模式,有兴趣可以过去看看,下面我们来学习适配器模式


    场景:将一个接口返回的数据,转化成列表格式,单选框数据格式,多选框数据格式


    用if-else来写,如下


    let list = []
    let selectArr = []
    let checkedArr = []

    http().then(res =>{
    //处理成列表格式
    this.list = this.handel(res,0)
    //处理成下拉框模式
    this.selectArr = this.handel(res,1)
    //处理成多选框模式
    this.checkedArr = this.handel(res,2)
    })
    handel(data,num){
    if(num == 0){
    ....
    }else if(num ==1){
    ....
    }else if(num ==2){
    ....
    }
    }

    分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


    //定义一个Adapter
    class Adpater {
    data = [];
    constructor(){
    this.data = data
    }
    toList(col){
    //col代表要的字段,毕竟整个data的字段不是我们都想要的
    //转列表
    return this.data.map(item =>{
    const obj = {}
    for(let e of col){
    const f = e.f
    obj[f] = item[f];
    }
    return obj
    })
    }
    //用了map的省略写法,来处理转化单选的格式
    toSelect(opt){
    const {label,value} = opt
    return this.data.map(item => ({
    label,
    value
    }))
    }
    //同上处理转化多选的格式
    toChecked(opt){
    const {field} = opt
    return this.data.map(item => ({
    checked:false,
    value:item[field]
    }))
    }
    }

    //下面是调用这个适配类
    let list = []
    let selectArr = []
    let checkedArr = []
    http.then(data =>{
    const adapter = new Adatpter(data)
    //处理列表
    list = adapter.toList(['id','name','age'])
    //处理下拉
    selectArr = adapter.toSelect({
    label:'name'
    value:'id'
    })
    //处理多选
    checkedArr = adapter.toChecked({
    field:'id'
    })
    })

    这个扩展性就能大大提高,看着也会好看很多,可以通过继承等等方式来扩展,继承方式下次有机会再来写代码,文章先到这里!


    结尾


    遵守设计规则,脱掉if-else的皮衣,善用设计模式,加

    作者:向乾看
    来源:juejin.cn/post/7265694012962537513
    油,骚年们!给我点点赞,关注下!

    收起阅读 »

    当文字成为雨滴:HTML、CSS、JS创作炫酷的"文字雨"动画!

    web
    简介 大家好,今天要给大家带来一个Super Cool的玩意儿😎! 在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落...
    继续阅读 »

    简介


    大家好,今天要给大家带来一个Super Cool的玩意儿😎!


    rain-preview.gif


    在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落像是将文字变成雨滴从天而降,营造出与众不同的视觉效果;


    HTML


    创建一个基本的HTML结构,这段HTML代码定义了一个容器,其中包含了"云朵"和"雨滴"(即文字元素)。基本结构如下:



    • 首先是类名为container的容器,表示整个动画的容器;

    • 其次是类名为cloud的容器,表示云朵的容器;

    • 接着是cloud容器中的文字元素,表示雨滴(即文字元素);
      然后引入外部创建的css和js文件,可以先定义几个text容器,用于调整样式;


    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Text Rain Animation</title>

    <link rel="stylesheet" href="./css/style.css">
    </head>
    <body>
    <div class="container">
    <div class="cloud">
    <!-- <div class="text">a</div> -->
    <!-- <div class="text">b</div> -->
    <!-- <div class="text">c</div> -->
    <!-- 雨滴将会在这里出现 -->
    </div>
    </div>

    <script src="./js/main.js"></script>
    </body>
    </html>

    CSS


    CSS是为文字雨效果增色添彩的关键,使动画效果更加丰富,关于一些 CSS 样式:



    • 使用了自定义的颜色变量来为背景色和文本颜色提供值,有助于使代码易于维护和修改;

    • 利用CSS的阴影效果和动画功能,创造逼真的"云朵"和流畅的"雨滴"动画;


    * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    }

    :root {
    --body-color: #181c1f;
    --primary-color: #ffffff;
    }

    body {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    background-color: var(--body-color);
    }

    .container {
    width: 100%;
    height: 400px;
    display: flex;
    justify-content: center;
    border-bottom: 1px solid rgba(255, 255, 255, .1);
    /* 添加一个从下往上线性渐变的镜像效果,增加视觉层次感 */
    -webkit-box-reflect: below 1px linear-gradient(transparent, transparent, transparent, transparent, #0005);
    }

    .cloud {
    position: relative;
    top: 50px;
    z-index: 100;

    /* 横向云朵 */
    width: 320px;
    height: 100px;
    background-color: var(--primary-color);
    border-radius: 100px;

    /* drop-shadow函数将阴影效果应用于投影图像 */
    filter: drop-shadow(0 0 30px var(--primary-color));
    }
    .cloud::before {
    content: "";
    /* 左侧小云朵 */
    width: 110px;
    height: 110px;
    background-color: var(--primary-color);
    border-radius: 50%;
    position: absolute;
    top: -50px;
    left: 40px;

    /* 右侧大云朵 */
    box-shadow: 90px 0 0 30px var(--primary-color);
    }

    .cloud .text {
    position: absolute;
    top: 40px;
    height: 20px;
    line-height: 20px;

    text-transform: uppercase;
    color: var(--primary-color);
    /* 为文字添加阴影,看上去发光,增加视觉效果 */
    text-shadow: 0 0 5px var(--primary-color), 0 0 15px var(--primary-color), 0 0 30px var(--primary-color);
    transform-origin: bottom;
    animation: animate 2s linear forwards;
    }

    @keyframes animate {
    0% {
    transform: translateX(0);
    }

    70% {
    transform: translateY(290px);
    }

    100% {
    transform: translateY(290px);
    }
    }

    通过关键帧动画 @keyframes animate 定义文字运动的过程,在这里是垂直移动290px,也就是向下移动,模拟下雨的状态。当然为了让文字雨效果更加好看,还可以引入一下字体库;



    Warning


    -webkit-box-reflect:可将元素内容在特定方向上进行轴对称反射;


    但是该特性是非标准的,请尽量不要在生产环境中使用它!


    目前只有webkit内核的浏览器支持,如:谷歌浏览器、Safari浏览器。在火狐浏览器中是不支持的;



    JavaScript


    最后,使用JavaScript来实现文字雨的效果。通过动态生成并随机选择字符,可以实现让这些字符(雨滴)从.cloud(云朵)中降落的效果。JavaScript 脚本逻辑:



    • 首先,定义函数 generateText() 并创建字符集,定义函数 randomText() 通过从给定的字符集中随机选择一个字符返回;

    • 接下来,编写 rain() 函数,在函数内部,首先选取 .cloud 元素同时创建一个新的 <div>元素作为字符节点,设置元素文本内容为函数返回的字符,并添加类名;

    • 然后,利用 Math.random() 方法生成一些随机值,将这些随机值应用到创建的 <div> 元素上,包括:

      • 字符距离左侧位置,在 .cloud 容器的宽度区间;

      • 字体大小,最大不超过32px;

      • 动画周期所需的时间(动画持续时间),2s内;



    • 最后,将其<div>添加到 .cloud 元素中,使用 setTimeout() 函数在2秒后将文字节点从 .cloud 元素中移除,模拟雨滴落地消失效果;


    定时器: 为了让字符(雨滴)持续下落,使用 setInterval 函数和一个时间间隔值来调用 rain() 函数。这样就是每20毫秒就会生成一个新的字符(雨滴)节点并添加到云朵中。


    // 生成字母和数字数组
    function generateText() {
    const letters = [];
    const numbers = [];

    const a = "a".charCodeAt(0);

    for (let i = 0; i < 26; i++) {
    letters.push(String.fromCharCode(a + i));

    if (i < 9) {
    numbers.push(i + 1);
    }
    };

    return [...letters, ...numbers];
    };

    // 从生成的数组中随机取出一个字符
    function randomText() {
    const texts = generateText();
    const text = texts[Math.floor(Math.random() * texts.length)];

    return text;
    };

    function rainEffect() {
    const cloudEle = document.querySelector(".cloud");
    const textEle = document.createElement("div");

    textEle.innerText = randomText();
    textEle.classList.add("text");

    const left = Math.floor(Math.random() * 310);
    const size = Math.random() * 1.5;
    const duration = Math.random();
    const styleSheets = {
    left: `${left}px`,
    fontSize: `${0.5 + size}em`,
    animationDuration: `${1 + duration}s`,
    };
    Object.assign(textEle.style, styleSheets);

    cloudEle.appendChild(textEle);
    setTimeout(() => {
    cloudEle.removeChild(textEle);
    }, 2000);
    };

    // 每隔20ms创建一个雨滴元素
    setInterval(() => rainEffect(), 20);

    结论


    通过HTML、CSS和JS的紧密合作,成功创建了一个炫酷的"文字雨"动画效果,这个动画可以增加网页的吸引力! 不要犹豫🖐️,动手尝试一下,或者甚至你也可以根据自己的需求对文字、样式和动画参数进行调整,进一步改善和扩展这个效果;


    希望这篇文章对你在开发类似动画效果时有所帮助!另外如果你对这个案例还有任何问题,欢迎在评论区留

    作者:掘一
    来源:juejin.cn/post/7270648629378367528
    言或联系(私信)我。谢谢阅读!🎉

    收起阅读 »

    别再用unload了:拥抱浏览器生命周期API

    web
    以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下...
    继续阅读 »

    以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下现有问题:


    unload事件的问题


    上面的这些事件(特别是unload)会有以下一些问题:



    • 用户主动触发的事件,无系统自动触发的一些状态监控

    • 无法稳定触发,特别是在手机端

    • 前进/后退时无法进入浏览器缓存(back/forward cache),导致用户来回切换时加载慢

    • 用户离开时埋点数据可能无法稳定上报

    • 无法追踪用户在页面上的完整生命周期


    虽然现在计算机的算力硬件越来越强,但是现在我们同时需要处理的事情越来越多,开的Tab也就越来越多,一直被人诟病的比如chrome的疯狂吃内存的问题也很是头疼,接近顶配的电脑可能很快就被chrome给耗尽😂,所以现在的chrome版本会根据系统资源消耗程度来对不活动的Tab冻结或直接销毁来释放内存及减少电量的消耗。


    针对这些问题现代浏览器提供了多个生命周期相关的事件来让开发者可以监听,并在触发时做相应处理。这里主要介绍几个最常用的而且对埋点相对比较重要的节点的生命周期事件。


    页面是否可见: visibilitychange


    visibilitychange事件涉及的场景会比较多,主要有用户切换tab导航到其他页面关闭Tab最小化窗口手机端切换app等,可在document上添加该事件,回调里通过document.visibilityState可以知道当前Tab是否隐藏了:


    document.addEventListener(
    'visibilitychange',
    (e) => {
    const isTabVisible = document.visibilityState === 'visible'
    console.log('tab is visible: ', isTabVisible)
    },
    {
    capture: true,
    }
    )

    以上事件需要在捕获阶段进行监听(包括下面讲到的其他相关事件),避免被业务代码阻止冒泡,而且有些是window层面的,没有冒泡的阶段,所以需要在capture阶段执行。


    页面的加载与离开: pageshow/pagehide


    首先,这事件名是真的很容易让人理解错😭(pageshow/pagehide感觉才是上面visibilitychange所表示的意义)。


    pageshow主要在页面新加载或被浏览器冻结后重新解冻时触发, 可通过e.persisted来确定是如何加载的:


    window.addEventListener(
    'pageshow',
    (e) => {
    // true 为之前冻结现在解冻,false相当于重新加载或新加载
    console.log('e.persisted: ', e.persisted)
    },
    {
    capture: true,
    }
    )

    pagehide本质上是对unload事件的真正替换且具有更稳定的触发时机,我们可以在这里对一些埋点事件或者其他一些小批量的数据进行上报(sendBeacon或fetch, 下面讲到),这里还有个需要注意的是visibilitychange触发范围更广,也就是页面进来和离开时也会触发,和pageshow/pagehide同时触发(都触发时在这2个之前),所以如果业务需要区分页面离开还是用户仅仅是切换tab时,需要在不同的事件回调里做不同的处理。


    页面离开时的数据上报


    当需要在用户离开页面时(pagehide触发时)稳定的上报一些数据,我们一般会使用navigator.sendBeacon()方法:


    // queued = true 说明浏览器接收请求,马上会上报出去,false的话可能业务要做其他处理
    const queued = navigator.sendBeacon('/api', data)

    当然sendBeacon有大小限制,一般在64KB以下,超出很可能会失败,所以我们在这里上报时要控制大小,如果数据量较大,建议提前上报一部分,比如在visibilitychange时(用户切换tab时)先上报一部分,确保只留64KB以下的数据放到最后。
    除了sendBeacon之外我们还可以用fetch上报,通过设置keepalive: true来达到sendBeacon一样的效果,当然也有一样的大小限制,这里可以做兼容处理:


    function send(body) {
    if (navigator.sendBeacon) {
    navigator.sendBeacon('/api', body)
    return
    }
    fetch('/api', {
    body,
    method: 'POST',
    keepalive: true
    })
    }

    其他生命周期事件


    除了上面常用的生命周期之外,浏览器的生命周期API还提供了页面的其他状态如:freeze, resume,freeze一般在用户导航到其他页面并且满足进入back/forward cache的条件时,或者用户在其他tab,浏览器根据系统资源自动使其进入冻结状态,当用户通过浏览器的返回按钮或重新切回到该Tab时,会触发resume事件说明页面继续,紧接着会触发pageshow事件,说明页面又进来了。关于更多的说明可以参考Page Lifecycle API


    电脑息屏、休眠时


    还有一些场景是电脑休眠或者只是关闭屏幕时,页面的生命周期以及页面里的一些定时器会有哪些变化?


    休眠比较简单,定时器、网络连接这些都会被暂停,如果不想丢失数据,需要在pagehide做处理;息屏时可在document.visibilityState !== 'visible'时做处理,相当于页面不可见了,而且页面里的定时器不会被停掉但是可能会被浏览器延时处理,比如正常代码里是5s执行一次,此时可能会变成30s或者1min执行一次来节省资源。


    总结


    以上主要介绍了如何使用现代浏览器提供的生命周期API来在页面的不同阶段做相应的处理,pagehide事件主要用来替换unload, 关于beforeunload事件在有些应用中还是需要的,但是我们应该有选择性的添加该事件及在合适的时间移除监听该事件,比如未保存的数据已经保存完毕时可以移除,当又有新改动时再监听。


    还有很多其他的生命周期事件可以让开发者能对用户在页面里/外的整个生命周期有更好的理解,以此来分析并提升网站的整体体验。笔者后面会写一篇如何跟踪用户在浏览器里的同一tab/不同tab之间的来回操作、记录并上报,并能够在后台进行session回放的文章,通过这些扩展能力相较于纯粹的埋点能更好地理

    作者:jasonboy7
    来源:juejin.cn/post/7269983642155663419
    解用户行为以此来优化产品体验🍻。

    收起阅读 »

    新一代前端工具链Rome:革新前端开发

    web
    在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使...
    继续阅读 »

    在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使用这个令人激动的工具。


    image.png
    image.png


    Rome 是什么?


    Rome 是一个全新的前端工具链,旨在重新定义前端开发体验。它是一个一站式解决方案,涵盖了许多前端开发中常见的问题和任务。Rome 的主要目标是提供一致性和高性能,以加速前端开发流程。它的核心特点包括:


    1. 依赖管理


    Rome 提供了强大的依赖管理系统,可用于管理你的项目中的依赖关系。它支持 JavaScript、TypeScript 和 Flow,并能够准确地分析和处理依赖项,以确保你的项目始终保持一致。


    // 安装依赖
    rome deps add react

    // 查看依赖树
    rome deps list

    这个特性非常有用,因为它将所有的依赖关系都纳入统一管理,无需依赖其他工具。


    2. 代码格式化


    Rome 自带了一个先进的代码格式化工具,可帮助你统一项目中的代码风格。无需争论缩进或分号,Rome 将自动处理这些问题。


    # 格式化整个项目
    rome format

    代码格式化对于团队协作和维护项目非常重要,它可以消除代码风格的争议,使代码更易读。


    3. 静态类型检查


    Rome 集成了强大的静态类型检查器,可以在编码过程中捕获潜在的类型错误,提高代码质量和可维护性。它支持多种类型系统,包括 TypeScript、Flow 等。


    // 检查类型
    rome check

    这个特性有助于减少运行时错误,提前发现潜在的问题。


    4. 构建工具


    Rome 提供了一套强大的构建工具,可用于将你的代码编译成浏览器可执行的代码。这有助于优化性能并减小最终部署包的大小。


    # 构建项目
    rome build

    Rome 的构建工具支持多种目标,你可以轻松地将项目构建成不同的输出格式。


    5. 包管理


    Rome 不仅支持 JavaScript 包管理,还可以处理 CSS、图片、字体等多种资源。这意味着你可以在一个地方管理所有资源,而无需额外的工具。


    // 导入 CSS 文件
    import './styles.css';

    这个特性使得资源管理变得更加一致和方便。


    Rome 的安装和配置


    现在,让我们来看看如何安装 Rome 并进行基本配置。


    步骤 1:安装 Rome


    你可以使用 npm 或 yarn 安装 Rome。这里以 npm 为例:


    npm install -g rome

    安装完成后,你可以在终端中运行 rome -v 来确认 Rome 是否成功安装。


    步骤 2:初始化项目


    在你的项目目录中,运行以下命令来初始化一个 Rome 项目:


    rome init

    这将创建一个 .romerc.js 文件,其中包含了


    项目的配置信息。


    步骤 3:配置选项


    你可以编辑 .romerc.js 文件来配置 Rome 的选项,以满足你的项目需求。例如,你可以指定项目的目标环境、依赖管理方式、构建选项等。


    // .romerc.js
    module.exports = {
    target: 'browser',
    module: {
    type: 'commonjs',
    },
    build: {
    minify: true,
    },
    };

    使用 Rome


    一旦你的项目配置好了,就可以开始使用 Rome 提供的工具来进行开发。以下是一些常用的命令和示例:


    运行 linter 来检查代码风格和潜在问题。


    rome check

    运行格式化程序来自动格式化你的代码。


    rome format

    运行类型检查以确保类型安全。


    rome typecheck

    构建你的项目。


    rome build

    运行你的应用程序。


    rome run

    自定义和插件


    Rome 还支持自定义插件和配置。你可以编写自己的插件,或者将现有的插件集成到你的项目中,以满足特定需求。


    // .romerc.js
    module.exports = {
    custom: {
    myPlugin: {
    enabled: true,
    options: {
    // 自定义选项
    },
    },
    },
    };

    在这个示例中,我们启用了一个名为 "myPlugin" 的自定义插件,并提供了一些自定义选项。


    Rome发展前景




    1. 性能优化: Rome 的核心目标之一是提供高性能。未来,它将不断进行性能优化,以确保更快的构建和更快的开发体验。随着前端项目变得越来越复杂,性能优化将成为前端开发的重要问题,Rome 在这方面有望发挥重要作用。




    2. 更好的类型检查: Rome 集成了静态类型检查器,帮助开发者在编码阶段捕获潜在的类型错误。未来,这个类型系统可能会进一步增强,提供更多的类型推断和错误检查功能。




    3. 更多的插件和扩展: Rome 的自定义插件和配置功能使得开发者可以根据自己的需求扩展 Rome。随着社区的不断壮大,预计会有更多的插件和扩展出现,为开发者提供更多的选择和解决方案。




    4. 更广泛的应用领域: Rome 目前主要用于前端开发,但未来可能会扩展到其他领域,如后端开发或跨平台开发。这将使 Rome 成为一个更加通用的工具链,适用于各种不同类型的项目。




    5. 更丰富的文档和教程: 随着 Rome 的发展,预计会有更多的文档、教程和学习资源出现,帮助开发者更好地掌握和使用 Rome。这将有助于扩大 Rome 的用户群体。




    6. 更强大的生态系统: Rome 的生态系统将继续扩大,包括各种开发工具、编辑器插件和整合解决方案。这将使 Rome 成为一个完整的开发生态系统,为开发者提供一站式的解决方案。




    总之,Rome作为一个新一代前端工具链,充满了潜力,未来有望在前端开发领域取得更多的成功和影响力。开发者可以继续关注 Rome 的发展,利用它来提高前端开发效率和质量。


    结语


    Rome 是一个前端工具链的新星,它为前端开发者提供了许多强大的功能,以提高开发效率和代码质量。虽然 Rome 还在不断发展和改进中,但它已经展现出了巨大的潜力。无论你是新手还是经验丰富的前端开发者,都值得一试 Rome,看看它如何改变你的前端开发流程。在使用 Rome 时,记得查阅官方文档以获取更多详细信息和示例代码。希望这篇文章能帮助你入门 Ro

    作者:侠名风
    来源:juejin.cn/post/7269745800178925603
    me 并开始在你的项目中使用它。

    收起阅读 »

    JS 不写分号踩了坑,但也可以不踩坑

    web
    踩的坑 写一个方法将秒数转为“xx天xx时xx分xx秒”的形式 const ONEDAYSECOND = 24 * 60 * 60 const ONEHOURSECOND = 60 * 60 const ONEMINUTESECOND = 60 functi...
    继续阅读 »

    踩的坑


    写一个方法将秒数转为“xx天xx时xx分xx秒”的形式


    const ONEDAYSECOND = 24 * 60 * 60
    const ONEHOURSECOND = 60 * 60
    const ONEMINUTESECOND = 60

    function getQuotientandRemainder(dividend,divisor){
    const remainder = dividend % divisor
    const quotient = (dividend - remainder) / divisor
    return [quotient,remainder]
    }

    function formatSeconds(time){
    let restTime,day,hour,minute
    restTime = time
    [day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)
    [hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)
    [minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)
    return day + '天' + hour + '时' + minute + '分' + restTime + '秒'
    }
    console.log(formatSeconds(time)) // undefined天undefined时undefined分NaN,NaN秒

    按照这段代码执行完后,day、hour、minute这些变量得到的都是 undefined,而 restTime 则好像得到一个数组。

    问题就在于 13、14、15、16 行之间没有添加分号,导致解析时,没有将这三行解析成三条语句,而是解析成一条语句。最终的表达式就是这样的:


    restTime = time[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)

    那执行的过程相当于给 restTime 进行赋值,表达式从左往右执行,最终表达式的值为右值。最右边的值就是 getQuotientandRemainder(restTime,ONEMINUTESECOND),由于在计算过程中 restTime 还没有被赋值,一直是 undefined,所以经过 getQuotientandRemainder 计算后得到的数组对象每个成员都是 NaN,最终赋值给 restTime 就是这样一个数组。


    分号什么时候会“自动”出现


    有时候好像不写分号也不会出问题,比如这种情况:


    let a,b,c
    a = 1
    b = 2
    c = 3
    console.log(a,b,c) // 1 2 3

    这是因为,JS 进行代码解析的时候,能够识别出语句的结束位置并“自动添加分号“,从而能够解析出“正确”的抽象语法树,最终执行的结果也就是我们所期待的。

    JS 有一个语法特性叫做 ASI (Automatic Semicolon Insertion),就是上面说到的”自动添加分号”的东西,它有一定的插入规则,在满足时会为代码自动添加分号进行断句,在我们不写分号的时候,需要了解这个规则,才能不踩坑。(当然这里说的加分号并不是真正的加分号,只是一种解析规则,用分号来代表语句间的界限)


    ASI 规则


    JS 只有在出现换行符的时候才会考虑是否添加分号,并且会尽量“少”添加分号,也就是尽量将多行语句合成一行,仅在必要时添加分号。


    1. 行与行之间合并不符合语法时,插入分号


    比如上面那个自动添加分号的例子,就是合并多行时会出现语法错误。

    a = 1b = 2 这里 1b 是不合法的,因此会加入分号使其合法,变为 a = 1; b = 2


    2. 在规定[no LineTerminator here]处,插入分号


    这种情况很有针对性,针对一些特定的关键字,如 return continue break throw async yield,规定在这些关键字后不能有换行符,如果在这些关键字后有了换行符,JS 会自动在这些关键字后加上分号。
    看下面这个例子🌰:


    function a(){
    return
    123
    }
    console.log(a()) // undefined

    function b(){
    return 123
    }
    console.log(b()) // 123

    在函数a中,return 后直接换行了,那么 return 和 123 就会被分成两条语句,所以其实 123 根本不会被执行到,而 return 也是啥也没返回。


    3. ++、--这类运算符,若在一行开头,则在行首插入分号


    ++ 和 -- 既可以在变量前,也可以在变量后,如果它们在行首,当多行进行合并时,会产生歧义,到底是上一行变量的运算,还是下一行变量的运算,因此需要加入分号,处理为下一行变量的运算。


    a
    ++
    b
    // 添加分号后
    a
    ++b

    如果你的预期是:


    a++ 
    b

    那么就会踩坑了。


    4. 在文件末尾发现语法无法构成合法语句时,会插入分号


    这条和 1 有些类似


    不写分号时需要注意⚠️


    上面的 ASI 规则中,JS 都是为了正确运行代码,必须按照这些规则来分析代码。而它不会做多余的事,并且在遵循“尽量合并多行语句”的原则下,它会将没有语法问题的多行语句都合并起来。这可能违背了你的逻辑,你想让每行独立执行,而不是合成一句。开头贴出的例子,就是这样踩坑的,我并不想一次次连续的对数组进行取值🌚。

    因此我们要写出明确的语句,可以被合并的语句,明确是多条语句时需要加上分号。


    (如果你的项目中使用了某些规范,它不想让你用分号,别担心,它只是不想让你在行尾用分号,格式化时它会帮你把分号移到行首)像这样:


    // before lint
    restTime = time;
    [day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND);
    [hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND);
    [minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND);

    // after lint
    restTime = time
    ;[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND)
    ;[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND)
    ;[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND)

    参考


    收起阅读 »

    情人节这天,跟女友 研究点餐小程序的目录 是怎么搞的?

    web
    今天情人节(七夕特辑) 🏩 背景(餐厅) 现在去餐厅订餐,桌角那里差不多都会有一个二维码,叫你扫码自己手机上点菜。扫码进去,要么是公众号的,要么就是小程序。 那我们来看看一个以下案例: 除了点餐系统外,还有一些文档目录用到这种方式的目录导航锚点。比如说...
    继续阅读 »

    今天情人节(七夕特辑)



    🏩 背景(餐厅)



    现在去餐厅订餐,桌角那里差不多都会有一个二维码,叫你扫码自己手机上点菜。扫码进去,要么是公众号的,要么就是小程序。



    那我们来看看一个以下案例:


    Kapture 2023-08-21 at 16.16.17.gif


    除了点餐系统外,还有一些文档目录用到这种方式的目录导航锚点。比如说文档,或者掘金的右侧的目录也用到了同样的效果。


    Kapture 2023-08-22 at 09.31.45.gif


    🔪 剖析原理


    监听scroll事件,获取分类最开始的offsetTop,拿当前页面的scrollTop跟这些offsetTop比较,到了就把左边菜单栏的那个分类变颜色。至于点击,点到哪个分类,就找到对应的右侧分类的标题的offsetTop,通过window.scrollTo(0, 要点击分类的右侧内容的offsetTop)就可以使得内容对应滚动到该位置。


    🥬 上菜实操


    数据结构:【nestjs返回的数据】如若只是测试,可以写死某些数据即可。


    image.png


    image.png


    http://127.0.0.1:5173/


    界面布局:【左(分类)右(内容)】分为两个区域,两边均可滚动。


    image.png


    <template>
    <div class="order">
    <div class="category">
    <div
    v-for="(item, key) in goods"
    :class="{
    'category-name': true,
    active: currentKey === key,
    }"

    :key="key"
    @click="changeCategory(key)"
    >
    {{ item.categoryName }}
    </div>
    </div>
    <div ref="content" class="content" @scroll="handleScroll">
    <div
    v-for="(item, key) in goods"
    :key="key"
    class="content-item"
    ref="categoryRefs"
    >
    <div class="title">{{ item.categoryName }}</div>
    <div
    class="each-item"
    v-for="good in item.goodsList"
    :key="good.goodsCode"
    >
    <div class="image-url">
    <img :src="good.imagePathSmall" />
    </div>
    <div class="desc">
    <div class="goods-name">{{ good.goodsName }}</div>
    <div class="goods-slogan">{{ good.goodsSlogan }}</div>

    <div class="bottom">
    <div>¥{{ good.goodsStandardList[0].acturalPrice }}</div>
    <div>+</div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </div>
    </template>

    image.png


    整体效果如下:


    image.png


    变量以及后端返回的商品列表:


    image.png


    逻辑:(一个是左侧点分类区域)、(一个是滚动右边内容,左侧分类要根据哪个分类变样式)


    左侧点击分类区域:


    image.png


    content.value.scrollTo({
    top: categoryRefs.value[key].offsetTop,
    behavior: "smooth",
    })

    这里值得提的是,scrollTo加behavior方法进行滚动时,可以通过添加behavior参数来指定滚动的动画行为。(smooth 滚动行为具有平滑的动画效果,窗口会平滑地滚动到指定位置)


    滚动右边内容:


    image.png


    当内容整体滚动的高度等于或者超过了某个分类的高度,那边就把左侧分类变样式就可以了,变样式通过key的方式来判断active: currentKey === key


    注意点:


    content.value.scrollTo({
    top: categoryRefs.value[key].offsetTop,
    behavior: "smooth",
    });

    因为我们设置的scrollTo是带有平滑滑动的属性的behavior: "smooth",所以导致点击左侧分类,自然而然也会触发到右侧的滚动事件,导致左侧跨多个点击的时候,中间所有的类都会改样式,问题如下:


    Kapture 2023-08-22 at 10.46.59.gif


    所以要解决这个问题,思路是,用一个变量,判断左侧点击彻底结束,右侧滚动事件才生效,解决办法如下:


    image.png


    最终效果


    Kapture 2023-08-22 at 10.51.01.gif


    🚶‍♀️ 总结消化


    做点餐平台,重点(地图和定位服务支付平台和第三方支付配送跟踪);当然最后用户的体验设计也是很重要的,易用性、响应速度、交互设计的,都会影响到顾客当天来店里吃饭点餐的心情和体验。


    以上是关于公众号或者小程序一般的点餐系统的大多数点餐页面滚动效果的研究。



    爱在朝夕 不止七夕




    ☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学

    作者:盏灯
    来源:juejin.cn/post/7269786623813074996
    习。


    收起阅读 »

    pnpm改造替换npm

    Q: 为什么要迁移pnpm? 相比于npm,pnpm有一些优势:更快的安装速度: 在安装包时,pnpm使用了硬链接的方式,将已安装的包链接到新的目录下,而不是复制或下载包。这样,当你安装一个包的不同版本或者不同项目使用同一个包时,它们会共享已经安装的包,减少了...
    继续阅读 »

    Q: 为什么要迁移pnpm?


    相比于npm,pnpm有一些优势:

    1. 更快的安装速度: 在安装包时,pnpm使用了硬链接的方式,将已安装的包链接到新的目录下,而不是复制或下载包。这样,当你安装一个包的不同版本或者不同项目使用同一个包时,它们会共享已经安装的包,减少了磁盘空间的占用,同时也加速了安装的速度。

    2. 更少的磁盘空间占用: 由于pnpm使用硬链接的方式共享已安装的包,因此相比于npm,pnpm占用更少的磁盘空间。

    3. 更好的本地缓存: pnpm会缓存包的元数据和二进制文件到本地缓存中,这样再次安装相同的包时,会从本地缓存中读取,而不是重新下载。这样可以提高安装包的速度,并减少网络带宽的消耗。

    4. 更好的多项目管理: pnpm可以管理多个项目的依赖,可以将相同的依赖安装在一个公共的位置,减少磁盘空间的占用,并且可以快速地切换项目之间的依赖关系。

    5. 更好的可重复性: pnpm使用了锁文件来保证安装包的版本一致性,同时也支持自定义的锁文件名称和路径。这样可以确保项目在不同的环境中的安装结果一致,增强了可重复性。


    需要注意的是,pnpm相比于npm也存在一些缺点,例如兼容性问题、社区支持不如npm等。因此,在选择使用pnpm还是npm时,需要根据自己的实际需求和项目情况进行权衡。


    Q: 上面提到的硬链接和符号链接是什么?


    硬链接和符号链接都是文件系统中的链接方式,它们的作用是可以将一个文件或目录链接到另一个文件或目录上,从而实现共享或复制等功能。下面我来简单介绍一下它们的区别和示例。


    硬链接


    硬链接是指在文件系统中,将一个文件名链接到另一个文件上,使它们指向同一个物理数据块,也就是说,这两个文件名共享同一个inode节点。硬链接的本质是将一个文件名指向一个已存在的文件。


    硬链接的特点:

    • 硬链接不能跨越不同的文件系统,因为inode节点只存在于一个文件系统中。
    • 硬链接可以看作是原文件的一个副本,它们的文件权限、拥有者、修改时间等都是相同的。
    • 删除硬链接并不会删除原文件,只有当所有的硬链接都被删除后,原文件才会被真正删除。

    下面是一个硬链接的示例:

    $ touch file1 # 创建一个文件
    $ ln file1 file2 # 创建硬链接
    $ ls -li file* # 查看文件inode节点
    12345 -rw-r--r-- 2 user user 0 Apr 26 10:00 file1
    12345 -rw-r--r-- 2 user user 0 Apr 26 10:00 file2

    可以看到,file1和file2的inode节点是相同的,说明它们共享同一个物理数据块。


    符号链接


    也称之为软链接,符号链接是指在文件系统中,创建一个特殊的文件,其中包含了另一个文件的路径,通过这个特殊文件来链接到目标文件。符号链接的本质是将一个文件名指向一个路径。


    符号链接的特点:

    • 符号链接可以跨越不同的文件系统,因为它们只是一个指向文件或目录的路径。
    • 符号链接指向的是目标文件或目录的路径,而不是inode节点,因此,目标文件或目录的属性信息可以独立于符号链接存在。
    • 删除符号链接不会影响目标文件或目录,也不会删除它们。

    下面是一个符号链接的示例:

    $ touch file1 # 创建一个文件
    $ ln -s file1 file2 # 创建符号链接
    $ ls -li file* # 查看文件inode节点
    12345 -rw-r--r-- 1 user user 0 Apr 26 10:00 file1
    67890 lrwxr-xr-x 1 user user 5 Apr 26 10:01 file2 -> file1

    可以看到,file2是一个符号链接文件,它的inode节点和file1不同,而是一个指向file1的路径。


    Q: 看到一些文章里说pnpm走的是硬链接,有的说用了软连接。到底走的是什么?


    其实,pnpm是软连接和硬链接都用了。可以这么理解,pnpm在机器上某个地方存放安装好的所有依赖包,这些依赖包是独立于我们代码仓库的,这也是前面说的pnpm在安装速度和磁盘空间占用上的优点。而我们的代码库确实是先通过硬链接的方式来建立代码库和已安装过的依赖包之间的共享关系。可以打开代码库看到node_modules下有一个.pnpm文件夹,里面放的就是当前代码库建立的硬链接。




    .pnpm下的文件都是一些名字很长的,长这样:




    这里不用关心具体是什么,我们需要关心的是node_mpdules下我们认识的npm依赖包,它们正是通过软连接的方式来链接到.pnpm下的这些依赖包的。在vscode下,可以明显看到npm包后面的软连接标识:




    如果想看一下这些软连接到底指向哪里的,可以:

    # 进入node_modules目录
    cd node_modules

    # 枚举文件列表
    ll 

     可以看到,这就是node_modules下软链接到.pnpm下的。


    Q: 这个模式跟npm dedupe是不是很相似,有什么不同?


    pnpm的硬链接模式和npm的dedupe功能是类似的,都是通过共享已安装的包来减少磁盘空间的占用,同时也可以提高安装包的速度。但它们之间还是存在一些不同:

    1. 原理不同: pnpm使用硬链接的方式共享已安装的包,而npm使用的是符号链接的方式共享已安装的包。硬链接是文件系统的一种特殊链接,它可以将一个文件链接到另一个文件上,使它们共享相同的内容。符号链接则是一个指向另一个文件或目录的特殊文件。

    2. 适用范围不同: pnpm的硬链接模式可以在多个项目之间共享已安装的包,而npm的dedupe功能只能在单个项目内共享已安装的包。

    3. 优势不同: pnpm的硬链接模式可以减少磁盘空间的占用和提高安装包的速度,而npm的dedupe功能只能减少磁盘空间的占用。

    4. 实现方式不同: pnpm使用了自己的包管理器和包存储库,而npm使用了公共的包管理器和包存储库。这也是导致它们之间存在差异的一个重要原因。


    需要注意的是,无论是使用pnpm的硬链接模式还是npm的dedupe功能,都需要谨慎使用,以避免出现意外的错误。特别是在使用硬链接模式时,如果多个项目共享同一个包,需要注意不要在一个项目中修改了该包的文件,导致其他项目也受到影响。


    Q: pnpm对于node版本有要求吗?


    pnpm有对node版本的要求。官方文档中列出的最低支持版本是Node.js 10.x,推荐使用的版本是Node.js 14.x。如果使用的是较旧的Node.js版本,可能会导致安装和使用pnpm时出现错误。


    我这里本来用的是Node14.x。因为其他原因,本次也给Node升级到16.x了。


    Q: pnpm有类似npm ci的命令吗?



    补充:npm ci主要是用于刚刚在download了一个仓库后,还没有node_modules的时候让npm完全根据package.json和package-lock.json的规范来install依赖包。相比较于直接走npm inpm ci会带来更精确的小版本版本号控制,因为npm i对于一些"^1.0.2"这样的版本号,可能会按照1.x.x这样的规范给你无感升级了,造成和之前某些包版本号之间的差异。
    但是当本地已有node_modules的时候,就没办法用npm ci命令了。



    是的,pnpm也有类似 npm ci 命令的功能,可以使用 pnpm install --frozen-lockfile 命令实现。它会根据 package-lock.jsonpnpm-lock.yaml 确定依赖关系,并且在安装期间不会更新任何包。此命令类似于 npm ciyarn install --frozen-lockfile 命令。


    Q: pnpm@7搭配husky@8后commit一直失败怎么办?


    这是因为hooks出问题了。某些代码库里会在commit时候会添加一些hook用来处理commit相关的事务,比如生成commit-id之类的。


    husky@8后需要处理一下这个:

    husky add .husky/commit-msg 'sh .git/hooks/commit-msg "$@"'

    手动把之前.git/hooks下的脚本拷贝到.husky下。


    友情提示:.git和.husky一般都是在项目根目录下的隐藏文件夹喲~


    作者:liuniansilence
    链接:https://juejin.cn/post/7232187022399389754
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    MP4 是不是该退休了?

    web
    背景 对于视频的在线播放,根据视频内容的传输模式可以分为点播和直播,分别用于预先录制内容的传输和实时传输,比如新闻报道、体育赛事都属于直播场景,电影、电视剧、课程视频都属于点播场景。 在 2000 年代初期,Flash 技术开始在 Web 上流行起来,它成为在...
    继续阅读 »

    背景


    对于视频的在线播放,根据视频内容的传输模式可以分为点播直播,分别用于预先录制内容的传输和实时传输,比如新闻报道、体育赛事都属于直播场景,电影、电视剧、课程视频都属于点播场景。


    在 2000 年代初期,Flash 技术开始在 Web 上流行起来,它成为在网页上展示视频的主要选择,因为当时没有其他方式能够在浏览器上流式的传输视频。


    image.png


    随着 HTML5 技术的逐渐成熟,HTML5 的 video 标签开始允许在没有 Flash 插件的情况下在浏览器中直接播放视频。


    视频的在线播放主要的技术环节在于视频的解码、显示效率以及数据的传输效率,而 HTML5 的 video 标签将这两个环节进行解藕,开发人员不需要关心视频数据的解码、显示,只需要关心如何去优化数据的获取。


    在点播的场景下,因为视频数据已经提前准备好,开发人员只需要制定 video 标签的 src 属性为对应的视频资源地址即可,但是在一些复杂的场景下比如需要根据网络状况做自适应码率、需要优化视频的首屏时间等,那么则需要对视频的一些规格参数以及相关的技术点做进一步的了解。


    视频基础


    视频帧率


    视频的播放原理类似于幻灯片的快速切换。


    image.png
    每一次画面的切换称作为一帧,而帧率表示每秒切换的帧数,单位数 FPS,人类对画面切换频率感知度是有一个范围的,一般 60 FPS 左右是一个比较合适的范围,但这也需要结合具体的场景,比如在捕捉一个事物快速变化的瞬间时,需要准备足够的帧数才能捕捉到细微的变化,但是当需要拍摄一个缓慢的镜头效果时,帧率不需要太高。


    帧率除了要考虑不通场景的播放内容,还需要结合播放设备的刷新频率,如果设备的刷新频率过低,多余的帧就会被丢弃。


    视频分辨率


    视频在播放时,显示在屏幕中的每一帧中的像素点数量都是相同的,像素是显示设备上发光原件的最小单位,最终呈现的画面是由若干个像素组合起来所展示的。


    视频的分辨率是指视频每一帧画面的像素数量,通常以水平方向像素数量 x 垂直高度像素数量的形式表示。分辨率决定了图像的清晰度和细节程度,常见的分辨率有 1080P = 1920 * 1080,这是标准的纯高清分辨率,p 表示是逐行扫描的,与之对应的是 i 表示的是隔行扫描。


    image.png


    左边一列是逐行扫描,中间一列是隔行扫描,在隔行扫描中会丢失一些页面信息从而加快页面信息的收集。


    当设备的分辨率高于视频的分辨率时,设备上的像素点就会多于视频显示所需的像素点,这时就会使用补间算法来为设备上那些未被利用的像素点生成色值信息,否则将导致屏幕上出现黑点,此时人从感官上就会觉得清晰度有所下降。如果视频的分辨率高于设备的分辨率时,则视频多出的信息会被丢弃。


    视频格式


    视频格式是一种特定的文件格式,用于存储和传输视频数据,它包含了视频图像、音频、字幕和其他相关媒体数据的编码信息,不同的视频格式采用不同的压缩算法和编码方式,以便在存储和传输的过程中有效的减少文件大小并保持高质量的图像。


    常见的视频格式有 MP4、AVI、MOV 等,每种视频格式都有其特定的优势和使用场景,比如 MOV 在 Mac 系统上有很好的兼容性,适用于视频编辑。


    MP4视频结构


    MP4 文件由许多 Box 数据块组成,每个 Box 可以嵌套包含其他 Box,一级嵌套一级来存放媒体信息,这种层次化的结构使得 MP4 文件能够组织和存储各种不同类型的媒体数据和元数据,使其在播放和传输过程中具有灵活性和可扩展性。


    image.png


    虽然 Box 的类型非常多,但是并不是都是必须的,一般的 MP4 文件都是含有必须的 Box 和个别非必须 Box,下面使用 MP4Box.js 查看 MP4 的具体结构并介绍几个必须 Box:


    image.png




    • ftyp


      File Type Box,一般在文件的开始位置,描述的文件的版本、兼容协议等。




    • mdat


      Media Data Box,媒体数据内容,是实际的视频的内容存储区域。该区域通常占整个文件99%+大小。


      image.png




    • moov


      MP4 的媒体数据信息主要存放在 Moov Box 中,是我们需要分析的重点。moov 的主要组成部分如下:




      • mvhd


        Movie Header Box,记录整个媒体文件的描述信息,如创建时间、修改时间、时间度量标尺、可播放时长等。


        image.png




      • udta


        保存自定义数据




      • track


        对于媒体数据来说,track 表示一个视频或音频序列,trak 区域至少存在一个,大部分情况是两个(音频和视频)。


        image.png






    形象点来说,moov 可以比如成是整个视频的目录,想要播放视频的话,必须要先加载 moov 区域拿到视频文件目录才能播放视频内容。


    为什么MP4视频首屏慢?


    当我们在浏览器中打开一个 MP4 视频文件时,浏览器根据就会开始获取视频信息,下载视频 chunk,开始播放视频,通过抓包能够大致了解浏览加载视频过程:


    image.png


    从请求列表中可知,浏览器发送了三个请求,总耗时 55s ,该视频文件的 box 结构如下:


    image.png


    下面来具体看一下这三个请求:




    • 第一次请求


      image.png


      浏览器第一次请求时尝试通过 HTTP range request(范围请求)下载整个视频,但是实际只下载了 135 KB 整个请求就完成了,来分析一下具体流程:



      • 浏览器通过 Range: bytes= 0- 首先获取到了 ftyp 信息,这里 ftyp-box 大小为 32 字节;

      • 接下来继续尝试查找 free-box 区域,如果没有就跳过,这里 free-box 大小为 8 字节;

      • 接着尝试查找下一个区域(moov 或 mdat),结果不幸匹配到的区域是 mdat 区域,这时浏览器就会主动终止请求,尝试从尾部查找视频的 moov 区域,因为上面我们讲过 moov 作为视频文件的目录,在播放视频数据前必须先获取 moov 数据,紧接着开始了第二次请求。




    • 第二次请求


      在第一次请求中已经知道了整个视频文件的大小了,如何去确定请求的范围呢?由于 MP4 是由 Box 组成的,标准的 Box 开头的4个字节(32位)为这个 Box 的大小,该大小包括 Box Header 和 Box Body,这样浏览器在第一次请求后就可以确定文件中剩下未解析到的 Box 的开始的 Range 值了。


      image.png


      计算过程(单位字节):
      moov 大小 = 视频文件大小 - ftyp大小 - free大小 - mdat大小 = 22251375 - 32 - 8 - 22224468 = 26867。


      也就是说这一次请求的 range 的开始值最大值不能高于 22251375-26867 = 22251415。


      image.png


      可以看到发出去的请求 Range: bytes=22251374-∞ ,上面计算的 22251415-∞ 包含在内 ,请求到数据后,接下来就是解析 moov-box了,然后根据视频”目录“发起第三次请求。




    • 第三次请求


      根据第二次请求的 moov 解析后,开始下载”真正“的视频的内容准备播放,在第三次请求中,浏览器必须要缓存 4MB 左右才开始播放,




    原因分析




    • 过多的数据请求。


      由于 MP4 文件的特殊性,浏览器必须先将 ftyp 、moov 等资源加载完毕之后才能去播放视频,而浏览器是从头部开始依次去加载这些资源,一旦视频资源存放顺序不对,浏览器会发送多次请求分别加载对应的资源。




    • 全量解析 moov


      播放 Mp4 音视频数据前需要先加载并解析 moov 数据,moov 的大小和视频长度成正比,更坏的情况是如果此时服务器没有配置 HTTP range request,浏览器无法跳过查找 moov 这一步,以至于需要下载整个文件。




    如何借助HLS 优化视频播放的?


    什么是HLS?


    HLS 全称是 HTTP Live Streaming,是一个由 Apple 公司提出的基于 http 的媒体流传输协议,用于实时音视频流的传输,HLS 最初是为苹果设备和平台(如iOS和macOS)设计的,但如今已被广泛应用于各种平台和设备上,成为流媒体传输的主要标准之一。


    HLS 协议由三部分组成:http、m3u8、ts,这三部分中,http 是传输协议,m3u8 是索引文件,ts是音视频的媒体信息。


    HLS的优势和特点是什么?




    • 分段传输


      HLS 将整个音频或视频流切分成短的分段,通常每个分段持续几秒钟,这种分段的方式使得视频内容可以逐段加载和播放,从而提供更好的适应性和流畅性。




    • 基于HTTP协议


      HLS 使用 http 协议进行数据传输,这意味着它能够在标准的 http 服务器上运行,不需要专门的流媒体服务器。




    • 自适应码率


      HLS 支持自适应码率,根据网络带宽和设备性能,动态地选择合适的分辨率和比特率,以提供更好的观看体验。




    • 多码率支持


      媒体源可以同时提供不同分辨率和比特率的视频流,使得用户可以根据网络状况选择合适的码率。




    • 兼容性好


      由于 HLS 使用标准的 HTTP 协议,它在各种设备和平台上具有很好的兼容性,包括苹果设备、Android 设备、PC、智能电视等。在使用 http 播放 MP4 视频时,需要代理服务器支持 http range request 以获取视频的某一部分,但不是所有的代理服务器都对此有良好的支持,而 HLS 不需要,它对代理服务器的要求小很多。




    HLS为什么首屏比MP4快?


    上面讲过如果要播放 MP4 需要等待整个 moov box 加载完成,这个过程比较消耗时间和带宽,而在 HLS 协议中,分段传输是一个非常重要的特性,HLS 将整个音视频流切分成多个小的分段(ts 文件),这些分段可以被独立的下载和播放。


    具体来说,HLS 的工作流程如下:




    • 切分分段:


      原始的音视频流被切分成短小的分段( .ts 文件),每个分段都包含了一小段时间范围内的音视频数据。




    • m3u8 文件:


      服务器生成一个 .m3u8 文件,它是一个播放列表,包含了所有分段的信息,如地址、时长等。播放器通过请求 .m3u8 文件来获取分段列表。




    • 分段请求:


      播放器根据 .m3u8 文件中的分段信息,逐个请求并加载 .ts 分段。




    • 逐段播放:


      播放器逐个播放已经加载的分段,实现连续的音视频播放。




    因此,HLS 首屏播放的实现方式不需要像 MP4 那样等待整个文件的基本信息加载完成,而是通过分段传输的方式逐段加载和播放。这使得首屏播放更快速和响应,同时也为流媒体的适应性提供了更好的支持。


    为什么选择TS格式文件?


    TS(Transport Stream,传输流)是一种封装的格式,它的全称为 MPEG2-TS,主要应用于数字广播系统,譬如 DVB、ATSC 与 IPTV,传输流最初是为广播而设计的,后来通过在标准的188字节数据包中添加4字节的时间码(TC),从而使该数据包成为192字节的数据包,使其适用于数码摄像机,录像机和播放器。


    TS(Transport Stream)流在流媒体领域具有多种优点,使得它成为广泛应用于数字电视、流媒体、广播等领域的传输格式之一。以下是TS流的一些优点:




    • 分段传输:


      TS 流将媒体数据切分成小的分段(Packet),每个分段通常持续数毫秒至几十毫秒。这种分段传输使得数据能够按需传输和加载,从而实现快速启动播放和逐段加载,提高了用户体验。




    • 容错性强:


      每个 TS 分段都具有自己的包头信息和校验机制,这使得 TS 流具有较强的容错性。即使在传输过程中发生丢包或错误,也只会影响某个分段,不会影响整个媒体流的播放。




    • 多路复用:


      多路复用的目的一般为了在一个文件流中能同时存储视频、音频、字幕等内容,而TS 流就支持将多个音视频流混合在一个文件中,每个流都有自己的 PID(Packet Identifier)。这使得 TS 流适用于同时传输多个媒体流的场景,如电视广播、有线电视等,提高了传输效率。




    • 支持多种编码格式:


      TS 流可以支持多种音视频编码格式,如H.264、H.265、AAC、MP3等,使其能够适应各种类型的媒体内容。




    M3U8格式文件的构成


    以下为一个 m3u8 格式文件内容的示例:


    #EXTM3U
    #EXT-X-VERSION:6
    #EXT-X-KEY:METHOD=AES-128,URI="<https://xxxx?token=xxx>"
    #EXT-X-MEDIA-SEQUENCE:0
    #EXT-X-TARGETDURATION:19
    #EXTINF:12.000,
    <https://xxxx/test/1.ts>
    #EXTINF:7.500,
    <https://xxxx/test/2.ts>
    #EXTINF:13.000,
    <https://xxxx/test/3.ts>
    #EXTINF:9.720,
    <https://xxxx/test/4.ts>
    #EXT-X-ENDLIST

    在以上示例中包含了 m3u8 文件常见的字段下面为一个M3U8文件可包含的基本字段及含义解释:



    • #EXTM3U 表明该文件是一个 m3u8 文件。每个 M3U8 文件必须将该标签放置在第一行。

    • #EXT-X-VERSION 指定 M3U8 版本号。

    • #EXT-X-KEY 媒体片段可以进行加密,而该标签可以指定解密方法。例如在上面的示例中,该字段指定了加密算法为 AES-128,密钥通过请求 https:xxxx?token=xxx 获取,以用于解密后续下载的 ts 文件。

    • EXT-X-MEDIA-SEQUENCE: 第一个 TS 分片的序列号。每个 TS 分片都拥有一个唯一的整型序列号,每个 TS 分片序列号按出现顺序依次加 1,如果该分片未指定则默认序列号从 0 开始。对于视频点播资源该字段一般是 0,但是在直播场景下,这个序列号标识直播段的起始位置。

    • #EXT-X-TARGETDURATION: 每个 TS 分片的最大的时长,单位为秒。

    • #EXT-X-DISCONTINUITY: 该标签表明其前一个切片与下一个切片之间存在中断。

    • #EXT-X-PLAYLIST-TYPE: 指定流媒体类型。

    • #EXT-X-ENDLIST: M3
      作者:西陵
      来源:juejin.cn/post/7268658252567691322
      U8 文件结束符。

    收起阅读 »

    为什么WebSocket需要前端心跳检测,有没有原生的检测机制?

    web
    本文代码 github、gitee、npm 在web应用中,WebSocket是很常用的技术。通过浏览器的WebSocket构造函数就可以建立一个WebSocket连接。但当需要应用在具体项目中时,几乎都会进行心跳检测。 设置心跳检测,一是让通讯双方确认对方...
    继续阅读 »

    本文代码 githubgiteenpm



    在web应用中,WebSocket是很常用的技术。通过浏览器的WebSocket构造函数就可以建立一个WebSocket连接。但当需要应用在具体项目中时,几乎都会进行心跳检测。


    设置心跳检测,一是让通讯双方确认对方依旧活跃,二是浏览器端及时检测当前网络线路可用性,保证消息推送的及时性。


    你可能会想,WebSocket那么简陋的吗,居然不能自己判断连接状态?在了解前先来回顾一下计算机网络知识。


    相关的网络知识


    TCP/IP协议族四层结构:




    • 应用层:决定了向用户提供应用服务时通信的活动。HTTP、FTP、WebSocket都在该层




    • (TCP)传输控制层:控制网络中两台主机的数据传输:将应用层数据(有必要时对应用层报文分段,例如一个完整的HTTP报文进行分段)发送到目标主机的特定端口的应用程序。给每个数据标记源端口、目标端口、分段后的序号。




    • (IP)网络层:将IP地址映射为目标主机的MAC地址,然后将TCP数据包(有必要时对数据分片)加入源IP、目标IP等信息后经过链路层扔到网络上让其找到目标主机。




    • 链路层:为IP网络层进行发送、接收数据报。将二进制数据包与在网线传输的网络电信号进行相互转换。





    TCP是可靠的连接,握手建立连接后,发送方每发送一个TCP报文(对应用层报文分段后形成多个TCP报文),都会期望对方在指定时间里返回已收到的确认消息,如果超时没有回应,会重复发送,确保所有TCP报文可以到达对方,被对方按顺序拼接成应用层需要的完整报文。


    WebSocket协议支持在TCP 上层引入 TLS 层,建立加密通信。



    WebSocket与HTTP的异同:




    • WebSocket和HTTP一样是应用层协议,在传输层使用了TCP协议,都是可靠的连接。WebSocket在建立连接时,可以使用已有的HTTP的GET请求进行握手:客户端在请求头中将WebSocket协议版本等信息发生到服务器,服务器同意的话,会响应一个101的状态码。就是说一次HTTP请求和响应,即可轻松转换协议到WebSocket。




    • WebSocket可以互相发起请求。当有新消息时,服务器主动通知客户端,无需客户端主动向服务器询问。客户端也可以向后端发送消息。而HTTP中请求只能由客户端发起。




    • WebSocket是HTML5的内容,HTTP则是超文本传输协议,比HTML5诞生更早。




    • 在应用层,WebSocket的每个报文(在WebSocket中叫数据帧)会比HTTP报文(必须包含请求行、请求头、请求数据)更轻量。



      • WebSocket每个数据帧只有固定、轻量的头信息,不会有cookie等或者自定义的头信息。并且建立通讯后是一对一的,不需要携带验证信息。但握手时的HTTP请求会自动携带cookie。

      • WebSocket在应用层就会将大的数据分拆到多个数据帧,而HTTP不会拆分每个报文。




    WebSocket与与WebRTC的异同:



    • WebRTC是一种通讯技术,由谷歌发起,被广大浏览器实现。用来建立浏览器和浏览器间的通讯,如视频通话等。而WebSocket是一种经过抽象的协议,可以实现为通讯技术。用来建立浏览器和服务器间的通讯。


    协议中的心跳检测机制


    从网上检索的答案,WebSocket大概有两种从协议角度出发的,检测对方存活的方式:




    1. WebSocket只是一个应用层协议规范,其传输层是TCP,而TCP为长连接提供KeepAlive机制,可以定时发送心跳报文确认对方的存活,但一般是服务器端使用。因为是TCP传输控制层的机制,具体的实现要看操作系统,也就是说应用层接收到的连接状态是操作系统通知的,不同操作系统的资源调度是不一样的,例如何时发送探测报文(不包含有效数据的TCP报文)检测对方的存活,频率是多久,在不同的系统配置下存在差异。可能是2小时进行一次心跳检测,或许更短。如果连续没有收到对方的应答包,才会通知应用层已经断开连接。这就带来了不确定性。同时也意味着其它依赖该机制的应用层协议也会被影响。也就是说要利用这个过程进行检测,客户端要修改操作系统的TCP配置才行,在浏览器环境显然不行。




    2. WebSocket协议也有自身的保活机制,但需要通讯双方的实现。WebSocket通讯的数据帧会有一个4位的OPCODE,标记当前传输的数据帧类型,例如:0x8表示关闭帧、0x9表示ping帧、0xA表示pong帧、0x1普通文本数据帧等。http://www.rfc-editor.org



      • 关闭数据帧,在任意一方要关闭通道时,发送给对方。例如浏览器的WebSocket实例调用close时,就会发送一个OPCODE为连接关闭的数据帧给服务器端,服务器端接收到后同样需要返回一个关闭数据帧,然后关闭底层的TCP连接。

      • ping数据帧,用于发送方询问对方是否存活,也就是心跳检测包。目前只有后端可以控制ping数据帧的发送。但浏览器端的WebSocket实例上没有对应的api可用。

      • pong数据帧,当WebSocket通讯一方接收到对方发送的ping数据帧后,需要及时回复一个内容一致,且OPCODE标记为pong的数据帧,告诉对方我还在。但目前回复pong是浏览器的自动行为,意味着不同浏览器会有差异。而且在js中没有相关api可以控制。




    综上所述,探测对方存活的方式都是服务器主动进行心跳检测。浏览器并没有提供相关能力。为了能够在浏览器端实时探测后端的存活,或者说连接依旧可用,只能自己实现心跳检测。


    浏览器端心跳检测的必要性


    首先我们先了解一下,目前的浏览器端的WebSocket何时会自动关闭WebSocket,并触发close事件呢?



    • 握手时的WebSocket地址不可用。

    • 其它未知错误。

    • 正常连接状态下,接收到服务器端的关闭帧就会触发关闭回调。


    也就是说建立正常连接后,中途浏览器端断网了,或者服务器没有发送关闭帧就关了连接,总之就是在连接无法再使用的情况下,浏览器没有接收到关闭帧,浏览器则会长时间保持连接状态。此时业务代码不去主动探测的话,是无法感知的。


    另外通讯双方保持连接意味着需要长时间占用对方的资源。对于服务器端来说资源是非常宝贵的。长时间不活跃的连接,可能会被服务器应用层框架"优化"释放掉。


    前端实现心跳检测


    实例化一个WebSocket:


    function connectWS() {
    const WS = new WebSocket("ws://127.0.0.1:7070/ws/?name=greaclar");
    // WebSocket实例上的事件

    // 当连接成功打开
    WS.addEventListener('open', () => {
    console.log('ws连接成功');
    });
    // 监听后端的推送消息
    WS.addEventListener('message', (event) => {
    console.log('ws收到消息', event.data);
    });
    // 监听后端的关闭消息,如果发送意外错误,这里也会触发
    WS.addEventListener('close', () => {
    console.log('ws连接关闭');
    });
    // 监听WS的意外错误消息
    WS.addEventListener('error', (error) => {
    console.log('ws出错', error);
    });
    return WS;
    }

    let WS = connectWS();

    心跳检测需要用到的实例方法:


    // 发送消息,用来发送心跳包
    WS.send('hello');
    // 关闭连接,当发送心跳包不响应,需要重连时,最好先关闭
    WS.close();

    定义发送心跳包的逻辑:


    准备



    • 申请一个变量heartbeatStatus,记录当前心跳检测状态,有三个状态:等待中,已收到应答、超时。

    • 监听WS实例的message事件,监听到就将heartbeatStatus改为:已收到应答。

    • 监听WS实例的open事件,打开后启动心跳检测。


    检测




    • 启动一个定时器A。




    • 定时器A执行,1.修改当前状态heartbeatStatus为等待中;2.发送心跳包;3.启动一个定时器B。



      • 发送心跳包后,后端需要立刻推送一个内容一样的心跳应答包给前端,触发前端WS实例的message事件,继而将heartbeatStatus改为已收到应答。




    • 定时器B执行,检测当前heartbeatStatus状态:




      • 如果是已收到应答,证明定时器A执行后,服务器可以及时响应数据。继续启动定时器A,然后不断循环。




      • 如果是等待中,证明连接出现问题了,走关闭或者检测流程。






    let WS = connectWS();
    let heartbeatStatus = 'waiting';

    WS.addEventListener('open', () => {
    // 启动成功后开启心跳检测
    startHeartbeat()
    })

    WS.addEventListener('message', (event) => {
    const { data } = event;
    console.log('心跳应答了,要把状态改为已收到应答', data);
    if (data === '"heartbeat"') {
    heartbeatStatus = 'received';
    }
    })

    function startHeartbeat() {
    setTimeout(() => {
    // 将状态改为等待应答,并发送心跳包
    heartbeatStatus = 'waiting';
    WS.send('heartbeat');
    // 启动定时任务来检测刚才服务器有没有应答
    waitHeartbeat();
    }, 1500)
    }

    function waitHeartbeat() {
    setTimeout(() => {
    console.log('检测服务器有没有应答过心跳包,当前状态', heartbeatStatus);
    if (heartbeatStatus === 'waiting') {
    // 心跳应答超时
    WS.close();
    } else {
    // 启动下一轮心跳检测
    startHeartbeat();
    }
    }, 1500)
    }

    优化心跳检测


    心跳检测异常,但close事件没有触发,大概率是双方之间的网络线路不佳,如果立马进行重连,会挤兑更多的网络资源,重连的失败概率更大,也可能阻塞用户的其它操作。


    但也不排除确实是连接的问题,如服务器宕机、意外重启,同时没有告知浏览器需要把旧连接关闭。


    所以一发生心跳不应答,个人推荐的做法是,发生延迟后,提醒用户网络异常正在修复中,让用户有个心理准备。然后多发一两个心跳包,连续不应答再提示用户掉线了,是否重连。如果中途正常了,就不需要重连,用户体验更好,对服务器的压力也更小。


    // 以上代码需要修改的地方

    // 添加一个变量来记录连续不应答次数
    let retryCount = 0

    WS.addEventListener('message', (event) => {
    const { data } = event;
    console.log('心跳应答了,要把状态改为已收到应答', data);
    if (data === '"heartbeat"') {
    // 复位连续不应答次数
    retryCount = 0
    heartbeatStatus = 'received';
    }
    })

    // 在等待应答的函数中添加重试的逻辑
    function waitHeartbeat() {
    setTimeout(() => {
    // 心跳应答正常,启动下一轮心跳检测
    if (heartbeatStatus === 'received') {
    return startHeartbeat();
    }
    // 更新超时次数
    retryCount ++;
    // 心跳应答超时,但没有连续超过三次
    if (retryCount < 3) {
    alert('ws线路异常,正在检测中。')
    return startHeartbeat();
    }

    // 超时次数超过三次
    WS.close();
    }, 1500)
    }

    最后,为了方便大家共同进步,本文已经把相关的逻辑封装为一个类,并且在npm中可下载玩一

    作者:小龟壳阿特greaclar
    来源:juejin.cn/post/7268864806558515237
    下,也已经开源到github上。

    收起阅读 »

    如何不花钱也能拥有一个属于自己的在线网站、博客🤩🤩🤩

    作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么...
    继续阅读 »

    作为一个资深的切图仔,我们难免会享有一个自己的博客,去分享一些文章啊或者一些学习笔记,那么这时候可能就需要一台服务器去把我们的项目部署到网上了,但是考虑到服务器价格昂贵,也并没有必要去花这么多钱搞这么一个东西。那么 Github pages 就为我们解决了这么一个烦恼,他能让我们不花钱也能拥有自己的在线网站。


    什么是 GitHub Pages?


    GitHub Pages 是 GitHub 提供的一个托管静态网站的服务。它允许用户将自己的代码仓库转化为一个在线可访问的网站,无需复杂的服务器设置或额外的托管费用。通过 GitHub Pages,我们可以轻松地创建个人网站、项目文档、博客或演示页面,并与其他开发者和用户分享自己的作品。


    使用


    要想使用这个叼功能,我们首先要再 Gayhub 上面建立一个仓库,如下图所示:




    紧接着我们使用 create-neat 来创建一个项目,执行以下命令:

    npx create-neat mmm

    跟着提示选择相对应的项目即可,选择 vue 或者 react 都可以。




    当项目创建成功之后我们进入到该目录并安装相关依赖包:

    pnpm add gh-pages --save-dev

    并在 package.json 文件中添加 homepage 字段,如下所示:

    "homepage": "http://xun082.github.io/mmm"

    其中 xun082 要替换为你自己 github 上面的用户名,如下图所示: 



    而 mmm 替换为我们刚才创建的仓库名称。


    接下来在 package.json 文件中 script 字段中添加如下属性:

      "scripts": {
    "start": "candy-script start",
    "build": "candy-script build",

    "deploy": "gh-pages -d dist"
    },

    完整配置如下所示:




    完成配置后我们将代码先提交到仓库中,如下命令所示:

    git add .

    git commit -m "first commit"

    git branch -M main

    git remote add origin https://github.com/xun082/mmm.git

    git push -u origin main

    这个时候我们的本地项目已经和远程 GayHub 仓库关联起来了,那么我们这个时候可以执行如下命令:

    pnpm run build

    首先执行该命令对我们的项目进行打包构建。打包完成之后会生成如下文件,请看下图:




    接下来我们可以使用 gh-pages 将项目发布到网上面了

    pnpm run deploy

    使用该命令进行打包并且部署到网上,这个过程可能需要一点时间,可以在工位上打开手机开把王者了。
    当在终端里出现 published 字段就说明我们部署成功了:




    这个时候,访问我们刚才在 package.json 文件中定义的 homepage 字段中的链接去访问就可以正常显示啦!




    总结


    通过该方法我们可以不用花钱,也能部署一个属于自己的网站,如果觉得不错那就赶紧用起来吧!


    作者:Moment
    链接:https://juejin.cn/post/7264183910598590519
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    实现一个简易的热🥵🥵更新

    简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
    继续阅读 »

    简单模拟一个热更新


    什么是热更新



    热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



    热更新的优点


    实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


    保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


    webpack 中的热更新



    在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



    原理如下:


    客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


    打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


    将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


    浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


    (1)找到被替换的模块并卸载它。


    (2)下载新的模块代码,并对其进行注入和执行。


    (3)重新渲染或更新应用程序的相关部分。


    保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


    代码模拟



    在同一个目录下创建 server.js 和 watcher.js



    server.js

    const http = require("http");
    const server = http.createServer((req, res) => {
    res.statusCode = 200;
    // 设置字符编码为 UTF-8,若有中文也不乱码
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("offer get!!!");
    });

    server.listen(7777, () => {
    console.log("服务已启动在 7777 端口");
    process.send("started");
    });

    // 监听来自 watcher.js 的消息
    process.on("message", (message) => {
    if (message === "refresh") {
    // 重新加载资源或执行其他刷新操作
    console.log("重新加载资源");
    }
    });


    (1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


    (2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


    (3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


    (4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


    (5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


    server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


    watcher.js

    const fs = require("fs");
    const { fork } = require("child_process");

    let childProcess = null;

    const watchFile = (filePath, callback) => {
    fs.watch(filePath, (event) => {
    if (event === "change") {
    console.log("文件已经被修改,重新加载");

    // 如果之前的子进程存在,终止该子进程
    childProcess && childProcess.kill();

    // 创建新的子进程
    childProcess = fork(filePath);
    childProcess.on("message", callback);
    }
    });
    };

    const startServer = (filePath) => {
    // 创建一个子进程,启动服务器
    childProcess = fork(filePath);
    childProcess.on("message", () => {
    console.log("服务已启动!");
    // 监听文件变化
    watchFile(filePath, () => {
    console.log("文件已被修改");
    });
    });
    };

    // 注意文件的相对位置
    startServer("./server.js");


    watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


    (1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


    (2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


    (3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


    (4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


    (5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


    (6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


    watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


    效果图


    打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样




    当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


    作者:PINKinee
    链接:https://juejin.cn/post/7269316739796058169
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    JavaScript中return await究竟有无用武之地?

    web
    我先回答:有的,参考文章末尾。 有没有区别?  先上一个Demo,看看async函数中return时加和不加await有没有区别: function bar() { return Promise.resolve('this from bar().'); ...
    继续阅读 »

    我先回答:有的,参考文章末尾。



    有没有区别?


     先上一个Demo,看看async函数中return时加和不加await有没有区别:


    function bar() {
    return Promise.resolve('this from bar().');
    }

    async function foo1() {
    return await bar(); // CASE#1 with await
    }

    async function foo2() {
    return bar(); // CASE#2 without await
    }

    // main
    (() => {
    foo1().then((res) => {
    console.log('foo1:', res); // res is string: 'this from bar().'
    })
    foo2().then((res) => {
    console.log('foo2:', res); // res is string: 'this from bar().'
    })
    })();

     可能在一些社区或团队的编程规范中,有明确要求:不允许使用非必要的 return await。给出的原因是这样做对于foo函数而言,会增加等待bar函数返回的Promise出结果的时间(但其实它可以不用等,因为马上就要return了嘛,这个时间应留给foo函数的调用者去等)。


     如果你觉得上面的文字不大通顺,直接看代码,问:以上例子中,foo1()函数和foo2()函数的写法对程序的执行过程有何影响?


     先说结论:async 函数中 return await promise;return promise; 从宏观结果来看是一样的,但微观上有区别。


    有什么区别?


     基于上面的Demo改造一下,做个试验:


    const TAG = Symbol();
    const RESULT = Promise.resolve('return from bar().');
    RESULT[TAG] = 'TAG#RESULT';

    function bar() {
    return RESULT;
    }

    async function foo1() {
    return await bar();
    }

    async function foo2() {
    const foo2Ret = bar();
    console.log('foo2Ret(i):', foo2Ret[TAG], foo2Ret === RESULT); // 'TAG#RESULT', true (1)
    return foo2Ret; // without await
    }

    // main
    (() => {
    const foo1Ret = foo1();
    console.log('foo1Ret:', foo1Ret[TAG], foo1Ret === RESULT); // undefined, false (2)
    console.log('--------------------------------------------');
    const foo2Ret = foo2();
    console.log('foo2Ret(o):', foo2Ret[TAG], foo2Ret === RESULT); // undefined, false (3)
    })();

     从注释标注的执行结果可以看到:



    • (1)处没有疑问,foo2Ret 本来就是 RESULT

    • (2)处应该也没有疑问,foo1Ret 是基于 RESULT 这个Promise的结果重新包装的一个新的Promise(只是这个Promise的结果和Result是一致的);

    • (3)处应该和常识相悖,竟然和(2)不一样?是的,对于 async 函数不管return啥都会包成Promise,而且不是简单的通过 Pomise.resolve() 包装。


     那么结论就很清晰了,async 函数中 return await promise;return promise; 至少有两个区别:



    1. 对象上的区别:

      • return await promise; 会先把promise的结果解出来,再构造成新的Promise

      • return await promise; 直接在promise的基础上构造Promise,也就是套了两个Promise(两层Promise的状态和结果是一致的)



    2. 时间上的区别:假设 bar() 函数耗时 10s

      • foo1() 中的写法会导致这10s消耗在 foo1() 函数的执行上

      • foo2() 的写法则会让10s的消耗在 foo2() 函数的调用者侧,也就是注释为main的匿名立即函数




     从对象上的区别看,不论怎样async函数都会构造新的Promise对象,有无await都节约不了内存;从时间上来看,总体的等待时长理论上是一样的,怎么写对结果都没啥影响嘛。


     举个不大恰当的例子:你的上司交给你一个重要任务让你完成后发邮件给他,你分析了下后发现任务需要同事A做一部分,遂找他。同事A完成他的部分需要2天。这个时候你有两个做法选择:一、做完自己的部分后等着A出结果,有结果后再发邮件回复上司;二、将自己的部分完成后汇报给上司,并跟和上司说已经告知A:让A等完成他的部分后直接回邮件给上司。


     如果,我是说假如果哈,如果,这个重要任务本来要求必须在12h内完成,但实际耗时了两天严重超标......请问上述例子中哪种做法更容易获取N+1大礼包?


    到底怎么写?


     回到代码层,通过上述分析可以知道,一个主要是耗时归属问题,一个是async函数“总是”会返回的那个Promise对象不是由Promise.resolve()简单包装的(因为Promise.resolve(promise) === promise),可以得到两个编码指南:



    强调下,async函数不是通过Pomise.resolve()简单包装的,其实进一步思考下也不难理解,因为它要考虑执行有异常的场景,甚至还可能根据不同的Promise状态做一些其他的操作(比如日志输出、埋点统计?我瞎猜的)



    // 避免非必要的 return await 影响模块耗时统计的准确性
    async function foo() {
    return bar();
    }

    // 除非你要处理执行过程中的异常
    async function foo() {
    try {
    return await bar();
    } catch (_) {
    return null;
    }
    }
    // 或:
    async function foo() {
    return bar().catch(() => null);
    }

    // async 函数中避免对返回值再使用多余的 Pomise 包装
    async function bar() {
    return 'this is from bar().'; // YES
    }
    async function bar() {
    return Promise.resolve('this is from bar().'); // !!! NO !!!
    }

    回到标题:JavaScript中return await有无用武之地?


    答:有的,当需要消化掉依赖 Promise

    作者:Chavin
    来源:juejin.cn/post/7268593569781350455
    执行中的异常时。

    收起阅读 »

    吐槽大会,来瞧瞧资深老前端写的垃圾代码

    web
    阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉 忍无可忍,不吐不快。 本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。 知道了什么是烂代码,才能写出好代码。 别说什么代码和人有一个能跑就行的话,玩笑归玩笑...
    继续阅读 »

    阅读提示:网页版带有主题和代码高亮,阅读体验更佳 🍉



    忍无可忍,不吐不快。


    本期不写技术文章,单纯来吐槽下公司项目里的奇葩代码,还都是一些资深老前端写的,希望各位对号入座。


    知道了什么是烂代码,才能写出好代码。


    别说什么代码和人有一个能跑就行的话,玩笑归玩笑。


    人都有菜的时候,写出垃圾代码无可厚非,但是工作几年了还是写垃圾代码,有点说不过去。


    我认为的垃圾代码,不是你写不出深度优先、广度优先的算法,也不是你写不出发布订阅、策略模式的 if else


    我认为垃圾代码都有一个共性:看了反胃。就是你看了天然的不舒服,看了就头疼的代码,比如排版极丑,没有空格、没有换行、没有缩进。


    优秀的代码就是简洁清晰,不能用策略模式优化 if else 没关系,多几行代码影响不大,你只要清晰,就是好代码。


    有了差代码、好代码的基本标准,那我们也废话不多说,来盘盘这些资深前端的代码,到底差在哪里。


    ---------------------------------------------更新------------------------------------------------


    集中回答一下评论区的问题:


    1、项目是原来某大厂的项目,是当时的外包团队写的,整体的代码是还行的。eslintcommitlint 之类的也是有的,只不过这些都是可以禁用的,作用全靠个人自觉。


    2、后来项目被我司收购,原来的人也转为我司员工,去支撑其他项目了。这个项目代码也就换成我们这批新来的维护了。很多代码是三几年前留下的确实没错,谁年轻的时候没写过烂代码,这是可以理解的。只不过同为三年前的代码,为啥有人写的简单清晰,拆分清楚,有的就这么脏乱差呢?


    3、文中提到的很多代码其实是最近一年写的,写这些功能的前端都是六七年经验的,当时因为项目团队刚组建,没有前端,抽调这些前端过来支援,写的东西真的非常非常不规范,这是让人难以理解的,他们在原来的项目组(核心产品,前端基建和规范都很厉害)也是这么写代码的吗?


    4、关于团队规范,现在的团队是刚成立没多久的,都是些年轻人,领导也不是专业前端,基本属于无前端 leader 的状态,我倒是很想推规范,但是我不在其位,不好去推行这些东西,提了建议,领导是希望你简单写写业务就行(说白了我不是 leader 说啥都没用)。而且我们这些年轻前端大家都默认打开 eslint,自觉性都很高,所以暂时都安于现状,代码 review 做过几次,都没太大的毛病,也就没继续做了。


    5、评论区还有人说想看看我写的代码,我前面文章有,我有几个开源项目,感兴趣可以自己去看。项目上的代码因为涉及公司机密,没法展示。


    6、本文就是篇吐槽,也不针对任何人,要是有人看了不舒服,没错,说的就是你。要是没事,大家看了乐呵乐呵就行。轻喷~


    文件命名千奇百怪


    同一个功能,都是他写的,三个文件夹三种奇葩命名法,最重要的是第三个,有人能知道它这个文件夹是什么功能吗?这完全不知道写的什么玩意儿,每次我来看代码都得重新理一遍逻辑。


    image.png


    组件职责不清


    还是刚才那个组件,这个 components 文件夹应该是放各个组件的,但是他又在底下放一个 RecordDialog.jsx 作为 components 的根组件,那 components 作为文件名有什么意义呢?


    image.png


    条件渲染逻辑置于底层


    这里其实他写了几个渲染函数,根据客户和需求的不同条件性地渲染不同内容,但是判断条件应该放在函数外,和函数调用放在一起,根据不同条件调用不同渲染函数,而不是函数内就写条件,这里就导致逻辑内置,过于分散,不利于维护。违反了我们常说的高内聚、低耦合的编程理念。


    image.png


    滥用、乱用 TS


    项目是三四年前的项目了,主要是类组件,资深前端们是属于内部借调来支援维护的,发现项目连个 TS 都没有,不像话,赶紧把 TS 配上。配上了,写了个 tsx 后缀,然后全是无类型或者 anyscript。甚至于完全忽视 TSESlint 的代码检查,代码里留下一堆红色报错。忍无可忍,我自己加了类型。


    image.png


    留下大量无用注释代码和报错代码


    感受下这个注释代码量,我每次做需求都删,但是几十个工程,真的删不过来。


    image.png


    image.png


    丑陋的、隐患的、无效的、混乱的 css


    丑陋的:没有空格,没有换行,没有缩进


    隐患的:大量覆盖组件库原有样式的 css 代码,不写类名实现 namespace


    无效的:大量注释的 css 代码,各种写了类名不写样式的垃圾类名


    混乱的:从全局、局部、其他组件各种引入 css 文件,导致样式代码过于耦合


    image.png


    一个文件 6 个槽点


    槽点1:代码的空行格式混乱,影响代码阅读


    槽点2:空函数,写了函数名不写函数体,但是还调用了!


    槽点3:函数参数过多不优化


    槽点4:链式调用过长,属性可能存在 undefined 或者 null 的情况,代码容易崩,导致线上白屏


    槽点5:双循环嵌套,影响性能,可以替换为 reduce 处理


    槽点6:参数、变量、函数的命名随意而不语义化,完全不知道有何作用,且不使用小驼峰命名


    image.png


    变态的链式取值和赋值


    都懒得说了,各位观众自己看吧。


    image.png


    代码拆分不合理或者不拆分导致代码行数超标


    能写出这么多行数的代码的绝对是人才。


    尽管说后来维护的人加了一些代码,但是其初始代码最起码也有近 2000 行。


    image.png


    这是某个功能的数据流文件,使用 Dva 维护本身代码量就比 Redux 少很多了,还是能一个文件写出这么多。导致到现在根本不敢拆出去,没人敢动。


    image.png


    杂七杂八的无用 js、md、txt 文件


    在我治理之前,充斥着非常多的无用的、散乱的 md 文档,只有一个函数却没有调用的 js 文件,还有一堆测试用的 html 文件。


    实在受不了干脆建个文件夹放一块,看起来也要舒服多了。


    image.png


    less、scss 混用


    这是最奇葩的。


    image.png


    特殊变量重命名


    这是真大佬,整个项目基建都是他搭的。写的东西也非常牛,bug 很少。但是大佬有一点个人癖好,例如喜欢给 window 重命名为 G。这虽然算不上大缺点,但真心不建议大家这么做,window 是特殊意义变量,还请别重命名。


    const G = window;
    const doc = G.document;

    混乱的 import


    规范的 import 顺序,应该是框架、组件等第三方库优先,其次是内部的组件库、包等,然后是一些工具函数,辅助函数等文件,其次再是样式文件。乱七八糟的引入顺序看着都烦,还有这个奇葩的引入方式,直接去 lib 文件夹下引入组件,也是够奇葩了。


    总而言之,css 文件一般是最后引入,不能阻塞 js 的优先引入。


    image.png


    写在最后


    就先吐槽这么多吧,这些都是平时开发过程中容易犯的错误。希望大家引以为戒,不然小心被刀。


    要想保持一个好的编码习惯,写代码的时候就得时刻告诉自己,你的代码后面是会有人来看的,不想被骂就写干净点。


    我觉得什么算法、设计模式都是次要的,代码清晰,数据流向清晰,变量名起好,基本 80% 不会太差。


    不过说这么多,成事在人。


    不过我写了一篇讲解如何写出简洁清晰代码的文章,我看不仅是一年前端需要学习,六七年的老前端也需要看看。

    作者:北岛贰
    来源:juejin.cn/post/7265505732158472249

    收起阅读 »

    实现一个简易的热🥵🥵更新

    web
    简单模拟一个热更新 什么是热更新 热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。 热更新的...
    继续阅读 »

    简单模拟一个热更新


    什么是热更新



    热更新是指在应用程序运行时,对程序的部分内容进行更新,而无需重启整个应用程序。通常,热更新是在不停止整个应用程序的情况下,将新的代码、资源或配置应用于正在运行的应用程序,以实现功能修复、性能优化或新增功能等目的。



    热更新的优点


    实时反馈,提高开发效率:热更新允许开发人员在进行代码更改后立即看到结果,无需重新编译和重启应用程序。这大大减少了开发、测试和调试代码所需的时间,提高了开发效率。


    保持应用程序状态:通过热更新,应用程序的状态可以在代码更改时得以保留。这意味着开发人员不需要在每次更改后重新导航到特定页面或重现特定的应用程序状态,从而节省了时间和精力。


    webpack 中的热更新



    在Webpack中,热更新(Hot Module Replacement,HMR)是一种通过在应用程序运行时替换代码的技术,而无需重新加载整个页面或应用程序。它提供了一种高效的开发方式,可以在开发过程中快速看到代码更改的效果,而不中断应用程序的状态。



    原理如下:


    客户端连接到开发服务器:当启动Webpack开发服务器时,它会在浏览器中创建一个WebSocket连接。


    打包和构建模块:在开发模式下,Webpack会监视源代码文件的变化,并在代码更改时重新打包和构建模块。这些模块构成了应用程序的各个部分。


    将更新的模块发送到浏览器:一旦Webpack重新构建了模块,它会将更新的模块代码通过WebSocket发送到浏览器端。


    浏览器接收并处理更新的模块:浏览器接收到来自Webpack的更新模块代码后,会根据模块的标识进行处理。它使用模块热替换运行时(Hot Module Replacement Runtime)来执行以下操作:(个人理解就像是 AJAX 局部刷新的过程,不对请指出)


    (1)找到被替换的模块并卸载它。


    (2)下载新的模块代码,并对其进行注入和执行。


    (3)重新渲染或更新应用程序的相关部分。


    保持应用程序状态:热更新通过在模块替换时保持应用程序状态来确保用户不会失去当前的状态。这意味着在代码更改后,开发人员可以继续与应用程序进行交互,而无需手动重新导航到特定页面或重现特定状态。


    代码模拟



    在同一个目录下创建 server.js 和 watcher.js



    server.js


    const http = require("http");
    const server = http.createServer((req, res) => {
    res.statusCode = 200;
    // 设置字符编码为 UTF-8,若有中文也不乱码
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("offer get!!!");
    });

    server.listen(7777, () => {
    console.log("服务已启动在 7777 端口");
    process.send("started");
    });

    // 监听来自 watcher.js 的消息
    process.on("message", (message) => {
    if (message === "refresh") {
    // 重新加载资源或执行其他刷新操作
    console.log("重新加载资源");
    }
    });


    (1)使用 http.createServer 方法创建一个 HTTP 服务器实例,该服务器将监听来自客户端的请求。


    (2)启动服务,当服务器成功启动并开始监听指定端口时,回调函数将被调用。


    (3)通过调用 process.send 方法,我们向父进程发送一条消息,消息内容为字符串 "started",表示服务器已经启动。


    (4)使用 process.on 方法监听来自父进程的消息。当收到名为 "refresh" 的消息时,回调函数将被触发。


    (5)在回调函数中,我们可以执行资源重新加载或其他刷新操作。一般是做页面的刷新操作。


    server.js 创建了一个简单的 HTTP 服务器。当有请求到达时,服务器会返回 "offer get!!!" 的文本响应。它还与父进程进行通信,当收到名为 "refresh" 的消息时,会执行资源重新加载操作。


    watcher.js


    const fs = require("fs");
    const { fork } = require("child_process");

    let childProcess = null;

    const watchFile = (filePath, callback) => {
    fs.watch(filePath, (event) => {
    if (event === "change") {
    console.log("文件已经被修改,重新加载");

    // 如果之前的子进程存在,终止该子进程
    childProcess && childProcess.kill();

    // 创建新的子进程
    childProcess = fork(filePath);
    childProcess.on("message", callback);
    }
    });
    };

    const startServer = (filePath) => {
    // 创建一个子进程,启动服务器
    childProcess = fork(filePath);
    childProcess.on("message", () => {
    console.log("服务已启动!");
    // 监听文件变化
    watchFile(filePath, () => {
    console.log("文件已被修改");
    });
    });
    };

    // 注意文件的相对位置
    startServer("./server.js");


    watcher.js 是一个用于监视文件变化并自动重新启动服务器的脚本。


    (1)首先,引入 fs 模块和 child_processfork 方法,fork 方法是 Node.js 中 child_process 模块提供的一个函数,用于创建一个新的子进程,通过调用 fork 方法,可以在主进程中创建一个全新的子进程,该子进程可以独立地执行某个脚本文件。


    (2)watchFile 函数用于监视指定文件的变化。当文件发生变化时,会触发回调函数。


    (3)startServer 函数负责启动服务器。它会创建一个子进程,并以指定的文件作为脚本运行。子进程将监听来自 server.js 的消息。


    (4)在 startServer 函数中,首先创建了一个子进程来启动服务器并发送一个消息表示服务器已启动。


    (5)然后,通过调用 watchFile 函数,开始监听指定的文件变化。当文件发生变化时,会触发回调函数。


    (6)回调函数中,会终止之前的子进程(如果存在),然后创建一个新的子进程,重新运行 server.js


    watcher.js 负责监视特定文件的变化,并在文件发生变化时自动重新启动服务器。它使用 child_process 模块创建子进程来运行 server.js,并在需要重新启动服务器时终止旧的子进程并创建新的子进程。这样就可以实现服务器的热更新,当代码发生变化时,无需手动重启服务器,watcher.js 将自动处理这个过程。


    效果图


    打开本地的 7777 端口,看到了 'offet get!!!' 的初始字样


    image.png


    当修改了响应文字时,刷新页面,无需重启服务,也能看到更新的字样!


    image.png

    收起阅读 »

    孤独的游戏少年

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据...
    继续阅读 »

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



    楔子


    又是一个闲暇的周末,正读一年级的我坐在床上,把象棋的旗子全部倒了出来,根据颜色分为红色和绿色阵营,旗子翻盖为建筑,正面为单位,被子和枕头作为地图障碍,双手不断的移动着双方阵营的象棋,脑海中演练着星级争霸的游戏画面,将两枚不同阵营的旗子进行碰撞后,通过我对两枚旗子主观的判断,一方阵营的旗子直接被销毁,进入回收,一方的单位对建筑物全部破坏取得游戏胜利。因为我的父亲酷爱玩这款游戏,年少的我也被其玩法、画面所深深吸引,不过最主要的还是父亲获胜或者玩累后,能幸运的奖励我玩上几把。星际争霸成了我第一个启蒙游戏,那时候怎么样都获胜不了,直到发现了show me the money。因为Blizzard这个英文单词一直在游戏的启动界面一闪一闪,那时候还以为这款游戏的名字叫Blizzard,最后才发现,其实Blizzard是魔鬼的意思。


    纸笔乐趣


    小学一二年级的时候家里管得严,电视也不给我看,电脑直接上锁,作为家里的独生子女,没有同龄人的陪伴,闲暇时间要不就看《格林童话》、《安徒生童话》、《伊索寓言》,要不就打开这副象棋在那里自娱自乐。



    起源



    在某一天的音乐课上,老师喉咙不舒服,在教室播放猫和老鼠给我们观看,正当我看的津津有味,前面的同学小张突然转过头来,问我“要不要跟我玩个游戏”。小孩子一般都比较贪玩,我直接拒绝了他,“我想看猫和老鼠”。小张满脸失落转了回去,因为我是个特别怕伤害别人情绪的人,所以不忍心又用铅笔的橡皮端戳了戳他,问他“什么游戏,好玩不”。他顿时也来了精神,滔滔不绝的介绍起了这款游戏。


    “游戏的规则非常的简单~~在一张纸上中间画一条分割线,双方有各自的基地,每人基地都有5点血量,双方通过猜拳,获胜的一方可以在自己阵营画一个火柴人,火柴人可以攻击对面的火柴人和基地,基地被破坏则胜利。管你听没听懂,试一把就完了!”



    我呆呆的看着他,“这么无聊,赢了又怎样?”。他沉默了一会,好像我说的的确有点道理,突然想到了一个好办法,“谁获得胜利扇对方一巴掌”。我顿时来了兴趣。随着游戏逐渐深入,我们的猜拳速度和行动力也越来越快,最后发现扇巴掌还是太狠,改为扇对方的手背,这节课以双方手背通红结束了。


    游戏改良


    这个《火柴人对战》小游戏在班里火了一会儿,但很快就又不火了,我玩着玩着也发现没啥意思,总是觉得缺少点什么,但毕竟我也只是个没有吃APTX4869的小学生,想不出什么好点子。时间一晃,受到九年义务教育的政策,我也成功成为了一名初中生。在一节音乐课上,老师想让我们放松放松,给我们班看猫和老鼠,隔壁同桌小王撕了一张笔记本的纸,问我有没有玩过《火柴人对战》游戏,只能说,熟悉的配方,熟悉的味道。


    当天晚上回到家,闲来无事,我在想这个《火柴人游戏》是不是可以更有优化,这种形式的游戏是不是可以让玩家更有乐趣。有同学可能会问,你那个年代没东西玩的吗?既然你诚心诚意发问,那我就大发慈悲的告诉你。玩的东西的确很多,但是能光明正大摆在课桌上玩的基本没有,一般有一个比较新鲜的好玩的东西,都会有一群人围了过来,这时候老师会默默站在窗户旁的阴暗角落,见一个收一个。


    坐在家里的椅子上,我整理了一下思绪,突然产生了灵感,将《魔兽争霸》《游戏王》这两个游戏产生化学反应,游戏拥有着资源,单位,建筑,单位还有攻击力,生命值,效果,攻击次数。每个玩家每回合通过摇骰子的方式获得随机能源点,能源能够解锁建筑,建筑关联着高级建筑和单位,通过单位进行攻击,直至对方玩家生命值为0,那么如何在白纸上面显示呢?我想到比较好的解决方案,单位的画像虽然名字叫骷髅,但是在纸上面用代号A表示,建筑骷髅之地用代号1表示。我花了几天时间,弄了两个阵营,不死族和冰结界。立刻就拿去跟同桌试玩了一下,虽然游戏很丰富,但是有一个严重的弊端就是玩起来还挺耗费时间的,而且要人工计算单位之间的扣血量,玩家的剩余生命,在纸片上去完成这些操作,拿个橡皮擦来擦去,突然觉得有点蠢,有点尴尬,突然明白,一张白纸的承受能力是有限的。之后,我再也没有把游戏拿出来玩过,但我没有将他遗忘,而是深深埋藏在我的心里。


    筑梦


    直到大学期间《炉石传说》横空出世,直到《游戏王》上架网易,直到我的项目组完成1.0后迎来空窗期一个月,我再也蚌埠住了,之前一直都对微信小游戏很有兴趣,每天闲着也是闲着,所以我有了做一个微信小游戏的想法。而且,就做一款在十几年前,就已经被我设计好的游戏。


    但是我不从来不是一个好学的人,领悟能力也很低,之前一直在看cocos和白鹭引擎学习文档,也很难学习下去,当然也因为工作期间没有这么多精力去学习,所以我什么框架也不会,不会框架,那就用原生。我初步的想法是,抛弃所有花里胡哨的动效,把基础的东西做出来,再作延伸。第一次做游戏,我也十分迷茫,最好的做法肯定是打飞机————研究这个微信项目如何用js原生,做出一个小游戏。



    虽然微信小游戏刚出来的时候看过代码,但是也只是一扫而过,而这次带着目标进行细细品味,果然感觉不一样。微信小游戏打飞机这个项目是js原生使用纯gL的模式编写的,主要就是在canvas这个画布上面作展示和用户行为。

      // 触摸事件处理逻辑
    touchEventHandler(e) {
    e.preventDefault()

    const x = e.touches[0].clientX
    const y = e.touches[0].clientY

    const area = this.gameinfo.btnArea

    if (x >= area.startX
    && x <= area.endX
    && y >= area.startY
    && y <= area.endY) this.restart()
    }

    点击事件我的理解就是用户点击到屏幕的坐标为(x, y),如果想要一个按钮上面做处理逻辑,那么点击的范围就要落在这个按钮的范围内。当我知道如何在canvas上面做点击行为时,我感觉我已经成功了一半,接下来就是编写基础js代码。


    首先这个游戏确定的元素分别为,场景,用户,单位,建筑,资源(后面改用能源替代),我先将每个元素封装好一个类,一边慢慢的回忆着之前游戏是如何设计的,一边编程,身心完全沉浸进去,已经很久很久没有试过如此专注的去编写代码。用了大概三天的时间,我把基本类该有的逻辑写完了,大概长这个样子



    上面为敌方的单位区域,下方为我方的单位区域,单位用ABCDEFG表示,右侧1/1/1 则是 攻击力/生命值/攻击次数,通过点击最下方的icon弹出创建建筑,然后创建单位,每次的用户操作,都是一个点击。


    一开始我设想的游戏名为想象博弈,因为每个单位每个建筑都只有名称,单位长什么样子的就需要玩家自己去脑补了,我只给你一个英文字母,你可以想象成奥特曼,也可以想象成哥斯拉,只要不是妈妈生的就行。



    湿了


    游戏虽然基本逻辑都写好了,放到整个微信小游戏界别人一定会认为是依托答辩,但我还是觉得这是我的掌上明珠,虽然游戏没有自己的界面,但是它有自己的玩法。好像上天也认可我的努力,但是觉得这个游戏还能更上一层楼,在某个摸鱼的moment,我打开了微信准备和各位朋友畅谈人生理想,发现有位同学发了一幅图,上面有四个格子,是赛博朋克风格的一位篮球运动员。他说这个AI软件生成的图片很逼真,只要把你想要的图片告诉给这个AI软件,就能发你一幅你所描绘的图片。我打开了图片看了看,说实话,质感相当不错,在一系列追问下,我得知这个绘图AI软件名称叫做midjourney



    midjourney



    我迫不及待的登录上去,询问朋友如何使用后,我用我蹩脚的英格力士迫不及待的试了试,让midjourney帮我画一个能源的icon,不试不要紧,一试便湿了,眼睛留下了感动地泪水,就像一个阴暗的房间打开了一扇窗,一束光猛地照射了进来。




    对比我之前在iconfont下载的免费图标,midjourney提供这些图片简直就是我的救世主,我很快便将一开始的免费次数用完,然后氪了一个30美刀的会员,虽然有点肉痛,但是为了儿时的梦想,这点痛算什么


    虽然我查找了一下攻略,别人说可以使用gpt和midjourney配合起来,我也试了一下,效果一般,可能姿势没有对,继续用我的有道翻译将重点词汇翻译后丢给midjourney。midjourney不仅可以四选一,还可以对图片不断优化,还有比例选择,各种参数,但是我也不需要用到那么多额外的功能,总之一个字,就是棒。


    但当时的我突然意识到,这个AI如此厉害,那么会不会对现在行业某些打工人造成一定影响呢,结果最近已经出了篇报道,某公司因为AI绘图工具辞退了众多插画师,事实不一定真实,但是也不是空穴来风,结合众多外界名人齐心协力抵制gpt5.0的研发,在担心数据安全之余,是否也在担心着AI对人类未来生活的各种冲击。焦虑时时刻刻都有,但解决焦虑的办法,我有一个妙招,仍然还是奖励自己


    门槛


    当我把整个小游戏焕然一新后,便兴冲冲的跑去微信开放平台上传我的伟大的杰作。但微信突然泼了我一盆冷水,上传微信小游戏前的流程有点出乎意外,要写游戏背景、介绍角色、NPC、介绍模块等等,还要上传不同的图片。我的小游戏一共就三个界面,有六个大板块要填写,每个板块还要两张不同的图片,我当时人就麻了。我只能创建一个单位截一次图,确保每张图片不一样。做完这道工序,还要写一份自审自查报告。


    就算做完了这些前戏,我感觉我的小游戏还是难登大雅之堂,突然,我又想到了这个东西其实是不是也能运行在web端呢,随后我便立刻付诸行动,创建一个带有canvas的html,之前微信小游戏是通过weapp-adapter这个文件把canvas暴露到全局,所以在web端使用canvas的时候,只需要使用document.getElementById('canvas')暴露到全局即可。然后通过http-server对应用进行启动,这个小游戏便以web端的形式运行到浏览器上了,终于也能理解之前为啥微信小游戏火起来的时候,很多企业都用h5游戏稍微改下代码进行搬运,原来两者之间是有异曲同工之妙之处的。


    关于游戏





    上面两张便是两个种族对应的生产链,龙族是我第一个创建的,因为我自幼对龙产生好感和兴趣,何况我是龙的传人/doge。魔法学院则是稍微致敬一下《游戏王》中黑魔导卡组吧。


    其实开发难度最难的莫过于是AI,也就是人机,如何让人机在有限的资源做出合理的选择,是一大难题,也是我后续要慢慢优化的,一开始我是让人机按照创建一个建筑,然后创建一个单位这种形式去做运营展开,但后来我想到一个好的点子,我应该可以根据每个种族的特点,走一条该特点的独有运营,于是人机龙族便有了龙蛋破坏龙两种流派,强度提升了一个档次。


    其实是否能上架到微信小游戏已经不重要了,重要的是这个过程带给我的乐趣,一步一步看着这个游戏被创建出来的成就感,就算这个行业受到什么冲击,我需要被迫转行,我也不曾后悔,毕竟是web前端让我跨越了十几年的时光,找到了儿时埋下的种子,浇水,给予阳光,让它在我的心中成长为一棵充实的参天大树


    h5地址:hslastudio.com/game/


    github地址: github.com/FEA-Dven/wa…


    作者:很饿的男朋友
    链接:https://juejin.cn/post/7218570025376350263
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    禁止别人调试自己的前端页面代码

    web
    🎈 为啥要禁止? 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码 🎈 无限 debugger 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点...
    继续阅读 »

    🎈 为啥要禁止?



    • 由于前端页面会调用很多接口,有些接口会被别人爬虫分析,破解后获取数据

    • 为了 杜绝 这种情况,最简单的方法就是禁止人家调试自己的前端代码


    禁止调试


    🎈 无限 debugger



    • 前端页面防止调试的方法主要是通过不断 debugger 来疯狂输出断点,因为 debugger 在控制台被打开的时候就会执行

    • 由于程序被 debugger 阻止,所以无法进行断点调试,所以网页的请求也是看不到的

    • 基础代码如下:


    /**
    * 基础禁止调试代码
    */

    (() => {
    function ban() {
    setInterval(() => {
    debugger;
    }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    基础禁止调试


    🎈 无限 debugger 的对策



    • 如果仅仅是加上面那么简单的代码,对于一些技术人员而言作用不大

    • 可以通过控制台中的 Deactivate breakpoints 按钮或者使用快捷键 Ctrl + F8 关闭无限 debugger

    • 这种方式虽然能去掉碍眼的 debugger,但是无法通过左侧的行号添加 breakpoint


    取消禁止对策


    🎈 禁止断点的对策



    • 如果将 setInterval 中的代码写在一行,就能禁止用户断点,即使添加 logpointfalse 也无用

    • 当然即使有些人想到用左下角的格式化代码,将其变成多行也是没用的


    (() => {
    function ban() {
    setInterval(() => { debugger; }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    禁止断点


    🎈 忽略执行的代码



    • 通过添加 add script ignore list 需要忽略执行代码行或文件

    • 也可以达到禁止无限 debugger


    忽略执行的代码


    🎈 忽略执行代码的对策



    • 那如何针对上面操作的恶意用户呢

    • 可以通过将 debugger 改写成 Function("debugger")(); 的形式来应对

    • Function 构造器生成的 debugger 会在每一次执行时开启一个临时 js 文件

    • 当然使用的时候,为了更加的安全,最好使用加密后的脚本


    // 加密前
    (() => {
    function ban() {
    setInterval(() => {
    Function('debugger')();
    }, 50);
    }
    try {
    ban();
    } catch (err) { }
    })();

    // 加密后
    eval(function(c,g,a,b,d,e){d=String;if(!"".replace(/^/,String)){for(;a--;)e[a]=b[a]||a;b=[function(f){return e[f]}];d=function(){return"\w+"};a=1}for(;a--;)b[a]&&(c=c.replace(new RegExp("\b"+d(a)+"\b","g"),b[a]));return c}('(()=>{1 0(){2(()=>{3("4")()},5)}6{0()}7(8){}})();',9,9,"block function setInterval Function debugger 50 try catch err".split(" "),0,{}));

    解决对策


    🎈 终极增强防调试代码



    • 为了让自己写出来的代码更加的晦涩难懂,需要对上面的代码再优化一下

    • Function('debugger').call() 改成 (function(){return false;})['constructor']('debugger')['call']();

    • 并且添加条件,当窗口外部宽高和内部宽高的差值大于一定的值 ,我把 body 里的内容换成指定内容

    • 当然使用的时候,为了更加的安全,最好加密后再使用


    (() => {
    function block() {
    if (window.outerHeight - window.innerHeight > 200 || window.outerWidth - window.innerWidth > 200) {
    document.body.innerHTML = "检测到非法调试,请关闭后刷新重试!";
    }
    setInterval(() => {
    (function () {
    return false;
    }
    ['constructor']('debugger')
    ['call']());
    }, 50);
    }
    try {
    block();
    } catch (err) { }
    })();

    终极增强防调试

    收起阅读 »