注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

【性能优化】关键性能指标及测量标准

前言随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:发展过程中产生了许多的性能指标、测量工具、优化手段等等,...
继续阅读 »

前言

随着时代的发展,业内曾经提出过很多性能优化相关的标准和规范,从最初2007年雅虎提出34条军规,到2020年google开始推广Web Vitals,优化技术已经有将近20多年的发展历史,如下图所示:


发展过程中产生了许多的性能指标、测量工具、优化手段等等,本文主要讲述关键性能指标及测量标准。


性能指标,顾名思义,就是性能优化过程中参考的一些标准。


进行性能优化,指标就是我们的一个抓手,首先你就要确定它的指标,然后才能根据指标去采取措施,否则就会像无头苍蝇一样乱撞,没有执行目标。

什么样的指标值得我们关注?

Web Vitals

Google在2020年推出了一个名为Web Vitals的新概念,着重评估一组页面访问体验钟的一部分关键指标,目的在于简化网络性能。每个指标代表页面体验的一个关键方面:加载、交互和视觉稳定性



Web Vitals在感知的性能,交互性和令人愉悦的方面,可作为用户体验的简化基准。在考虑页面性能时,应该首先关注这一套较小的指标。


此外,Web Vitals代表了访问者在访问您的页面时首先在其视口中看到的内容,即首屏内容。他们首先看到的内容最终会影响他们对页面性能的看法。


首先,专注于这三个指标,可以使您获得可感知的和实际的性能可观的收益,然后再深入研究其他优化。


加载


所谓加载,就是进入页面时,页面内容的载入过程。比如,当你打开一些网站时,你会发现,有的网站首页上的文字、图片出现很缓慢,而有的则很快,这个内容出现的过程就是加载。加载缓慢严重消耗用户的耐心,会让用户离开页面。


这里我们拿淘宝首页的network信息观察一些指标:


如图所示,我们可以看到下方显示网页加载一共有201次请求(requests)、3.3MB资源量(resources),DOM完成的加载时间92ms(DOMContentLoaded)、总资源加载时间479ms(Load),这对一个电商网站来说已经很好了。


瀑布图


再来看瀑布图,nextwork中加载资源的列表右侧的Waterfall一栏显示的就是瀑布图,它可以非常直观的将网站的资源加载用自上而下的方式表达出来。我们可以从横向和纵向两个方向来解读它。


横向来看是具体资源的加载,如下图所示:


当鼠标悬浮在具体的色块上,我们可以看到具体的指标详情。如下图所示,资源下载之前经历了0.46ms的排队(Queueing),DNS查找24.79ms(DNS Lookup)、建立TCP连接(Initial connection)、还有https的网站SSL证书验证的时间37.04ms(SSL),最后发送请求再到资源返回也要经历110.71ms(TTFB)。

下面我们再来纵向看瀑布图,主要看2点:



  1. 资源之间的联系:如果下载发生了阻塞,很多资源的下载就会是串行处理的。如果是并行的,就可以加快下载过程。

  2. 关键时间节点:我们可以看到图中有红蓝两个颜色的两根线,蓝色的是DOM完成的加载时间,红色是页面中所有声明的资源加载完成的时间。

关键指标


那么这么多指标,到底哪些是最值得我们关注的呢?下面我来总结一下:




  1. 白屏时间(FP,First Paint):也叫首次绘制时间,对于应用页面,首次出现视觉上不同于跳转之前内容的时间点,或者说是页面发生第一次绘制的时间点,它的标准时间是 300ms。如果白屏时间过长,用户会认为我们的页面不可用,或者可用性差。如果超过一定时间(如 1s),用户注意力就会转移到其他页面。




  2. 首屏时间(FCP,First Contentful Paint):也叫首次有内容绘制时间,对于所有的网页应用,这是一个非常重要的指标。它是指从浏览器输入地址并回车后,到首屏内容渲染完毕的时间。这期间不需要滚动鼠标或者下拉页面,否则无效。也就是说,它是浏览器完成渲染DOM中第一部分内容(可能是文本、图像或其它任何元素)的时间点,此时用户应该在视觉上有直观的感受。




  3. 首次有意义绘制(FMP,First Meaningful Paint):指页面关键元素的渲染时间。这个概念并没有标准化定义,因为关键元素可以由开发者自行定义究竟什么是“有意义”的内容,只有开发者或者产品经理自己了解。




  4. 速度指数(Speed Index):指的是网页以多快的速度展示内容,标准时间是4s




  5. 总下载时间(Load):页面所有资源加载完成所需要的时间。一般可以统计 window.onload,得到同步加载的资源全部加载完的耗时。如果页面中存在较多异步渲染,那么可以将异步渲染全部完成的时间作为总下载时间。




  6. TTFB(Time To First Byte):是指网络请求被发起到从服务器接收到地一个字节的这段时间。其中包含了TCP连接时间、发送HTTP请求时间和获得相应消息第一个字节的时间。


    TTFB这个参数比较重要,因为它可以给用户最直观的感受,如果TTFB很慢,说明资源一直没有返回,增加白屏时间,如果TTFB很快,资源回来之后就可以进行快速的解析和渲染。


    那么影响TTFB的因素有哪些?



    1. 后台处理能力,服务器响应到底有多快

    2. 资源请求的网络状况,网络是否有延迟

首屏时间 vs 白屏时间


首屏时间 = 白屏时间 + 渲染时间。在加载性能指标方面,相比于白屏时间,首屏时间更重要。为什么?


从重要性角度看,打开页面后,第一眼看到的内容一般都非常关键,比如电商的头图、商品价格、购买按钮等。这些内容即便在最恶劣的网络环境下,我们也要确保用户能看得到。


从体验完整性角度看,进入页面后先是白屏,随着第一个字符加载,到首屏内容显示结束,我们才会认为加载完毕,用户可以使用了。白屏加载完成后,仅仅意味着页面内容开始加载,但我们还是没法完成诸如下单购买等实际操作,首屏时间结束后则可以。


DOMContentLoaded和Load事件的区别


其实从这两个事件的命名就能体会到,DOMContentLoaded 指的是文档中 DOM 加载内容加载完毕的时间,也就是说 HTML 结构已经是完整的了。但是我们知道,很多页面都包含图片、特殊字体、视频、音频等其他资源,由于这些资源由网络请求获取,需要额外的网络请求,因此DOM内容如加载完毕时,这些资源还没有请求或渲染完成。当页面上所有资源加载完成后,Load 事件才会被触发。


因此,在时间线上,Load 事件往往会落后于 DOMContentLoaded 事件

交互/响应


所谓交互,就是用户点击网站或 App 的某个功能,页面给出的回应,也就是浏览器的响应时间。比如我们点击了一个“点赞”按钮,立刻给出了点赞数加一的展示,这就是交互体验好,反之如果很长时间都没回应,这就是交互体验不好。


关于交互指标,有的公司用 FID 指标 (First Input Delay,首次输入延迟), 指标必须尽量小于 100ms,如果过长会给人页面卡顿的感觉。还有的公司使用 PSI(Perceptual Speed Index,视觉变化率),衡量标准是小于20%。


一般来说,主要包括以下几个指标:




  1. 交互动作的反馈时间:也叫用户可交互时间,就是用户可以与应用进行加护的时间,一般来讲,我们认为是 DOMReady 的时间,因为我们通常会在这时绑定事件操作。如果页面中设计交互的脚本没有下载完成,那么当然没有达到所谓的用户可交互时间。那么如何定义 DOMReady 时间呢?这里我推荐大家看司徒正美的文章《何谓DOMReady》。




  2. 刷新率(FPS,Frame Per Second):也叫帧率,标准的刷新率指标是60帧/s,它可以决定画面是否足够流畅。




  3. 异步请求的完成时间:所有的异步请求能在1s中内请求回来。




关于帧率,我们可以用chorme Devtools来查看,打开控制台,点击快捷键command/ctrl+shift+P,弹出下面的弹窗,输入frame,点击FPS一栏,就会在页面左上角看到图2所示的监控台,显示网页交互过程中每一帧的绘制频率。



不同帧率的体验



  • 帧率能够达到 50 ~ 60 FPS 的动画将会相当流畅,让人倍感舒适;

  • 帧率在 30 ~ 50 FPS 之间的动画,因各人敏感程度不同,舒适度因人而异;

  • 帧率在 30 FPS 以下的动画,让人感觉到明显的卡顿和不适感;

  • 帧率波动很大的动画,亦会使人感觉到卡顿


现在网上很多关于浏览器reflow的文章都说给要少用offsetTop, offsetLeft 等获取布局信息的。因为这些属性需要触发一次浏览器的的Layout。也就是说在一帧内(16ms)会多了一次layout。如果Layout的次数太多,就会导致掉帧。


视觉稳定性


视觉稳定性指标CLS(Cumulative Layout Shift),也就是布局偏移量,它是指页面从一帧切换到另外一帧时,视线中不稳定元素的偏移情况。


比如,你想要购买的商品正在参加抢购活动,而且时间快要到了。在你正要点击页面链接购买的时候,原来的位置插入了一条 9.9 元包邮的商品广告。结果会怎样?你点成了那个广告商品。如果等你再返回购买的时候,你心仪商品的抢购活动结束了,你是不是很气?所以,CLS也非常重要。


一个好的CLS分数是75%以上的用户小于0.1,如图所示:




布局偏移的具体内容


布局偏移是由 Layout Instability API 定义的。这个API会在任意时间上报 layout-shift 的条目,当一个可见元素在两帧之间,改变了它的起始位置(默认的 writing mode 下指的是top和left属性)。这些元素被当成不稳定元素。


需要注意的是,布局偏移只发生在已经存在的元素改变起始位置的时候。如果一个新的元素被添加到dom上,或者已存在的元素改变它的尺寸,除非改变了其他元素的起始位置,否则都不算布局偏移。


布局偏移主要包含以下几项:




  1. 布局偏移分数:布局偏移的分数是两个度量的乘积:影响分数(impact fraction)和距离分数(distance fraction)。如果是一个很大的元素移动了较小的距离,实际影响并不大,所以分数需要依赖两个度量。




  2. 影响分数:影响分数测试的是两帧之间,不稳定元素在视图上的影响范围。




  3. 距离分数:距离分数测试的是两帧之间,不稳定元素在视图上移动的距离(水平和纵向取最大值)。如果有多个不稳定元素,也是取其中最大的一个。




  4. 动画和过渡:动画和过渡,如果做得好,对用户而言是一个不错的更新内容的方式,这样不会给用户“惊喜”。突然出现的或者预料之外的内容,会给用户一个很差的体验。但如果是一个动画或者过渡,用户可以很清楚的知道发生了什么,在状态变化的时候可以很好的引导用户。


    CSS中的 transform 属性可以让你在使用动画的时候不会产生布局偏移。



    • transform:scale() 来替换 widthheight 属性

    • transform:translate() 来替换 top, left, bottom, right 属性




CLS是平时开发很少关注的点,页面视觉稳定性对很多web开发而言,可能没有加载性能那么关注度高,但对用户而言,这确实是很困扰的一点。平时开发中,尽可能的提醒自己,不管是产品交互图出来之后,或者是UI的视觉稿出来之后,如果出现了布局偏移的情况,都可以提出这方面的意见。开发过程中也尽可能的遵循上面提到的一些优化点,给用户一个稳定的视觉体验。


RAIL测量模型


RAIL模型是2015年google提出的一个可以量化的测量标准,通过RAIL模型可以指导性能优化的目标,让良好的用户体验成为可能。




  1. Response 响应:是指用户操作网页的时候浏览器给到用户的反馈时间,其中处理事件应在50ms以内完成。


    为什么是50ms?谷歌向向用户发起调研,将用户的反馈分成了几个组,经过研究得出用户能接受的反馈时间是100ms。


    那么为什么我们要设置在50ms以内,因为100ms是用户输入到反馈的时间,但是浏览器处理反馈也需要时间,所以留给开发者优化处理事件的时间在50ms以内。如下图所示:


  • Animation - 页面中动画特效的流畅度,达到每10ms产生一帧。


    根据研究得出,动画要达到60sps,即每秒60帧给人眼的感觉是流畅的,每一帧大概在16ms,去除浏览器绘制动画的6ms,开发者要保证每10ms产生一帧。


    在这16ms内浏览器要完成的工作有:



    • 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等

    • 样式计算(CSS Object Model):级联地生成每个节点的生效样式。

    • 布局(Layout):计算布局,执行渲染算法

    • 重绘(Paint):各层分别进行绘制(比如 3D 动画)

    • 合成(Composite):将位图发送给合成线程。




  • Idle空闲 - 浏览器有足够的空闲时间,与响应想呼应。尽可能最大化空闲时间,不能让事件处理时间太长,超过50ms。


    例如延迟加载可以用空闲时间去加载。但是如果需要前端做业务计算,就是不合理的。




  • Load - 网络加载时间,在5s内完成内容加载并可以交互。首先加载-解析-渲染的时间在5s,其次网络环境差的情况下,加载也会受到影响。

  • 总结

    至此,性能优化的指标我就介绍完了,现将关键指标总结如下:



    1. 性能优化的三个方向:加载、交互、视觉稳定性

    2. 加载的关键指标有:TTFB(请求等待时间)、FP(白屏时间)、FCP(首屏时间)、Speed Index(4s)

    3. 交互的关键指标:用户可交互时间、帧率(FPS)、异步请求完成时间

    4. 交互稳定性(CLS):布局偏移量中,布局偏移分数 = 影响分数 x 距离分数

    5. RAIL测量模型关注点:响应时间50ms、动画10ms/帧、浏览器空闲时间<50ms、网络加载时间5s

    链接:https://juejin.cn/post/6956583036133572639

    收起阅读 »

    前端的你还不会优化你的图片资源?来看这一篇就够了!

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加...
    继续阅读 »

    优质的图片可以有效吸引用户,给用户良好的体验,所以随着互联网的发展,越来越多的产品开始使用图片来提升产品体验。相较于页面其他元素,图片的体积不容忽视。下图是截止 2019 年 6 月 HTTP Archive[1] 上统计的网站上各类资源加载的体积:


    可以看到,图片占据了半壁江山。同样,在一篇 2018 年的文章中,也提到了图片在网站中体量的平均占比已经超过了 50%[2]。然而,随着平均加载图片总字节数的增加,图片的请求数却再减少,这也说明网站使用的图片质量和大小正在不断提高。


    所以,如果单纯从加载的字节数这个维度来看性能优化,那么很多时候,优化图片带来的流量收益要远高于优化 JavaScript 脚本和 CSS 样式文件。下面我们就来看看,如何优化图片资源。


    1. 优化请求数

    1.1. 雪碧图


    图片可以合并么?当然。最为常用的图片合并场景就是雪碧图(Sprite)[3]


    在网站上通常会有很多小的图标,不经优化的话,最直接的方式就是将这些小图标保存为一个个独立的图片文件,然后通过 CSS 将对应元素的背景图片设置为对应的图标图片。这么做的一个重要问题在于,页面加载时可能会同时请求非常多的小图标图片,这就会受到浏览器并发 HTTP 请求数的限制。我见过一个没有使用雪碧图的页面,首页加载时需要发送 20+ 请求来加载图标。将图标合并为一张大图可以实现「20+ → 1」的巨大缩减。


    雪碧图的核心原理在于设置不同的背景偏移量,大致包含两点:



    • 不同的图标元素都会将 background-url 设置为合并后的雪碧图的 uri;

    • 不同的图标通过设置对应的 background-position 来展示大图中对应的图标部分。


    你可以用 Photoshop 这类工具自己制作雪碧图。当然比较推荐的还是将雪碧图的生成集成到前端自动化构建工具中,例如在 webpack 中使用 webpack-spritesmith,或者在 gulp 中使用 gulp.spritesmith。它们两者都是基于于 spritesmith 这个库,你也可以自己将这个库集成到你喜欢的构建工具中。

    1.2. 懒加载


    我们知道,一般来说我们访问一个页面,浏览器加载的整个页面其实是要比可视区域大很多的,也是什么我们会提出“首屏”的概念。这就导致其实很多图片是不在首屏中的,如果我们都加载的话,相当于是加载了用户不一定会看到图片。而图片体积一般都不小,这显然是一种流量的浪费。这种场景在一些带图片的长列表或者配图的博客中经常会遇到。


    解决的核心思路就是图片懒加载 —— 尽量只加载用户正在浏览或者即将会浏览到的图片。实现上来说最简单的就是通过监听页面滚动,判断图片是否进入视野,从而真正去加载图片:

    function loadIfNeeded($img) {
    const bounding = $img..getBoundingClientRect();
    if (
    getComputedStyle($img).display !== 'none'
    && bounding.top <= window.innerHeight
    && bounding.bottom >= 0
    ) {
    $img.src = $img.dataset.src;
    $img.classList.remove('lazy');
    }
    }

    // 这里使用了 throttle,你可以实现自己的 throttle,也可以使用 lodash
    const lazy = throttle(function () {
    const $imgList = document.querySelectorAll('.lazy');
    if ($imgList.length === 0) {
    document.removeEventListener('scroll', lazy);
    window.removeEventListener('resize', lazy);
    window.removeEventListener('orientationchange', lazy);
    return;
    }
    $imgList.forEach(loadIfNeeded);
    }, 200);

    document.addEventListener('scroll', lazy);
    window.addEventListener('resize', lazy);
    window.addEventListener('orientationchange', lazy);

    对于页面上的元素只需要将原本的 src 值设置到 data-src 中即可,而 src 可以设置为一个统一的占位图。注意,由于页面滚动、缩放和横竖方向(移动端)都可能会改变可视区域,因此添加了三个监听。


    当然,这是最传统的方法,现代浏览器还提供了一个更先进的 Intersection Observer API[4] 来做这个事,它可以通过更高效的方式来监听元素是否进入视口。考虑兼容性问题,在生产环境中建议使用对应的 polyfill


    如果想使用懒加载,还可以借助一些已有的工具库,例如 aFarkas/lazysizesverlok/lazyloadtuupola/lazyload 等。


    在使用懒加载时也有一些注意点:



    • 首屏可以不需要懒加载,对首屏图片也使用懒加载会延迟图片的展示。

    • 设置合理的占位图,避免图片加载后的页面“抖动”。

    • 虽然目前基本所有用户都不会禁用 JavaScript,但还是建议做一些 JavaScript 不可用时的 backup。


    对于占位图这块可以再补充一点。为了更好的用户体验,我们可以使用一个基于原图生成的体积小、清晰度低的图片作为占位图。这样一来不会增加太大的体积,二来会有很好的用户体验。LQIP (Low Quality Image Placeholders)[5] 就是这种技术。目前也已经有了 LQIPSQIP(SVG-based LQIP) 的自动化工具可以直接使用。


    如果你想了解更多关于图片懒加载的内容,这里有一篇更详尽的图片懒加载指南[6]

    1.3. CSS 中的图片懒加载

    除了对于 <img> 元素的图片进行来加载,在 CSS 中使用的图片一样可以懒加载,最常见的场景就是 background-url 

    .login {
    background-url: url(/static/img/login.png);
    }

    对于上面这个样式规则,如果不应用到具体的元素,浏览器不会去下载该图片。所以你可以通过切换 className 的方式,放心得进行 CSS 中图片的懒加载。


    1.4. 内联 base64


    还有一种方式是将图片转为 base64 字符串,并将其内联到页面中返回,即将原 url 的值替换为 base64。这样,当浏览器解析到这个的图片 url 时,就不会去请求并下载图片,直接解析 base64 字符串即可。


    但是这种方式的一个缺点在于相同的图片,相比使用二进制,变成 base64 后体积会增大 33%。而全部内联进页面后,也意味着原本可能并行加载的图片信息,都会被放在页面请求中(像当于是串行了)。同时这种方式也不利于复用独立的文件缓存。所以,使用 base64 需要权衡,常用于首屏加载 CRP 或者骨架图上的一些小图标。


    2. 减小图片大小

    2.1. 使用合适的图片格式


    使用合适的图片格式不仅能帮助你减少不必要的请求流量,同时还可能提供更好的图片体验。


    图片格式是一个比较大的话题,选择合适的格式[7]有利于性能优化。这里我们简单总结一些。


    1) 使用 WebP:


    考虑在网站上使用 WebP 格式[8]。在有损与无损压缩上,它的表现都会优于传统(JPEG/PNG)格式。WebP 无损压缩比 PNG 的体积小 26%,webP 的有损压缩比同质量的 JPEG 格式体积小 25-34%。同时 WebP 也支持透明度。下面提供了一种兼容性较好的写法。

    <picture>
    <source type="image/webp" srcset="/static/img/perf.webp">
    <source type="image/jpeg" srcset="/static/img/perf.jpg">
    <img src="/static/img/perf.jpg">
    </picture>

    2) 使用 SVG 应对矢量图场景:


    在一些需要缩放与高保真的情况,或者用作图标的场景下,使用 SVG 这种矢量图非常不错。有时使用 SVG 格式会比相同的 PNG 或 JPEG 更小。


    3) 使用 video 替代 GIF:


    兼容性允许的情况下考虑,可以在想要动图效果时使用视频,通过静音(muted)的 video 来代替 GIF。相同的效果下,GIF 比视频(MPEG-4)大 5~20 倍Smashing Magazine 上有篇文章[9]详细介绍使用方式。


    4) 渐进式 JPEG:


    基线 JPEG (baseline JPEG) 会从上往下逐步呈现,类似下面这种:


    而另一种渐进式 JPEG (progressive JPEG)[10] 则会从模糊到逐渐清晰,使人的感受上会更加平滑。


    不过渐进式 JPEG 的解码速度会慢于基线 JPEG,所以还是需要综合考虑 CPU、网络等情况,在实际的用户体验之上做权衡。


    2.2. 图片质量的权衡


    图片的压缩一般可以分为有损压缩(lossy compression)和无损压缩(lossless compression)。顾名思义,有损压缩下,会损失一定的图片质量,无损压缩则能够在保证图片质量的前提下压缩数据大小。不过,无损压缩一般可以带来更可观的体积缩减。在使用有损压缩时,一般我们可以指定一个 0-100 的压缩质量。在大多数情况下,相较于 100 质量系数的压缩,80~85 的质量系数可以带来 30~40% 的大小缩减,同时对图片效果影响较小,即人眼不易分辨出质量效果的差异。



    处理图片压缩可以使用 imagemin 这样的工具,也可以进一步将它集成至 webpackGulpGrunt 这样的自动化工具中。


    2.3. 使用合适的大小和分辨率


    由于移动端的发展,屏幕尺寸更加多样化了。同一套设计在不同尺寸、像素比的屏幕上可能需要不同像素大小的图片来保证良好的展示效果;此外,响应式设计也会对不同屏幕上最佳的图片尺寸有不同的要求。


    以往我们可能会在 1280px 宽度的屏幕上和 640px 宽度的屏幕上都使用一张 400px 的图,但很可能在 640px 上我们只需要 200px 大小的图片。另一方面,对于如今盛行的“2 倍屏”、“3 倍屏”也需要使用不同像素大小的资源。


    好在 HTML5 在 <img> 元素上为我们提供了 srcsetsizes 属性,可以让浏览器根据屏幕信息选择需要展示的图片。

    <img srcset="small.jpg 480w, large.jpg 1080w" sizes="50w" src="large.jpg" >

    2.4. 删除冗余的图片信息


    你也许不知道,很多图片含有一些非“视觉化”的元信息(metadata),带上它们可会导致体积增大与安全风险[12]。元信息包括图片的 DPI、相机品牌、拍摄时的 GPS 等,可能导致 JPEG 图片大小增加 15%。同时,其中的一些隐私信息也可能会带来安全风险。


    所以如果不需要的情况下,可以使用像 imageOptim 这样的工具来移除隐私与非关键的元信息。


    2.5 SVG 压缩


    在 2.1. 中提到,合适的场景下可以使用 SVG。针对 SVG 我们也可以进行一些压缩。压缩包括了两个方面:


    首先,与图片不同,图片是二进制形式的文件,而 SVG 作为一种 XML 文本,同样是适合使用 gzip 压缩的。


    其次,SVG 本身的信息、数据是可以压缩的,例如用相比用 <path> 画一个椭圆,直接使用 <ellipse> 可以节省文本长度。关于信息的“压缩”还有更多可以优化的点[13]SVGGO 是一个可以集成到我们构建流中的 NodeJS 工具,它能帮助我们进行 SVG 的优化。当然你也可以使用它提供的 Web 服务


    3. 缓存

    与其他静态资源类似,我们仍然可以使用各类缓存策略来加速资源的加载。




    图片作为现代 Web 应用的重要部分,在资源占用上同样也不可忽视。可以发现,在上面提及的各类优化措施中,同时附带了相应的工具或类库。平时我们主要的精力会放在 CSS 与 JavaScript 的优化上,因此在图片优化上可能概念较为薄弱,自动化程度较低。如果你希望更好得去贯彻图片的相关优化,非常建议将自动化工具引入到构建流程中。


    除了上述的一些工具,这里再介绍两个非常好用的图片处理的自动化工具:SharpJimp


    好了,我们的图片优化之旅就暂时到这了,下面就是字体资源了。


    链接:https://juejin.cn/post/6962800616259190792



    收起阅读 »

    仅使用CSS就可以提高页面渲染速度的4个技巧

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。1. Content-visibili...
    继续阅读 »

    用户喜欢快速的网络应用,他们希望页面加载速度快,功能流畅。如果在滚动时有破损的动画或滞后,用户很有可能会离开你的网站。作为一名开发者,你可以做很多事情来改善用户体验。本文将重点介绍4个可以用来提高页面渲染速度的CSS技巧。

    1. Content-visibility


    一般来说,大多数Web应用都有复杂的UI元素,它的扩展范围超出了用户在浏览器视图中看到的内容。在这种情况下,我们可以使用内容可见性( content-visibility )来跳过屏幕外内容的渲染。如果你有大量的离屏内容,这将大大减少页面渲染时间。


    这个功能是最新增加的功能之一,也是对提高渲染性能影响最大的功能之一。虽然 content-visibility 接受几个值,但我们可以在元素上使用 content-visibility: auto; 来获得直接的性能提升。


    让我们考虑一下下面的页面,其中包含许多不同信息的卡片。虽然大约有12张卡适合屏幕,但列表中大约有375张卡。正如你所看到的,浏览器用了1037ms来渲染这个页面。


    下一步,您可以向所有卡添加 content-visibility 。

    在这个例子中,在页面中加入 content-visibility 后,渲染时间下降到150ms,这是6倍以上的性能提升。


    正如你所看到的,内容可见性是相当强大的,对提高页面渲染时间非常有用。根据我们目前所讨论的东西,你一定是把它当成了页面渲染的银弹。

    然而,有几个领域的内容可视性不佳。我想强调两点,供大家参考。



    • 此功能仍处于试验阶段。 截至目前,Firefox(PC和Android版本)、IE(我认为他们没有计划在IE中添加这个功能)和,Safari(Mac和iOS)不支持内容可见性。

    • 与滚动条行为有关的问题。 由于元素的初始渲染高度为0px,每当你向下滚动时,这些元素就会进入屏幕。实际内容会被渲染,元素的高度也会相应更新。这将使滚动条的行为以一种非预期的方式进行。

    为了解决滚动条的问题,你可以使用另一个叫做 contain-intrinsic-size 的 CSS 属性。它指定了一个元素的自然大小,因此,元素将以给定的高度而不是0px呈现。

    .element{
    content-visibility: auto;
    contain-intrinsic-size: 200px;
    }

    然而,在实验时,我注意到,即使使用 conta-intrinsic-size,如果我们有大量的元素, content-visibility 设置为 auto ,你仍然会有较小的滚动条问题。


    因此,我的建议是规划你的布局,将其分解成几个部分,然后在这些部分上使用内容可见性,以获得更好的滚动条行为。

    2. Will-change 属性

    浏览器上的动画并不是一件新鲜事。通常情况下,这些动画是和其他元素一起定期渲染的。不过,现在浏览器可以使用GPU来优化其中的一些动画操作。



    通过will-change CSS属性,我们可以表明元素将修改特定的属性,让浏览器事先进行必要的优化。



    下面发生的事情是,浏览器将为该元素创建一个单独的层。之后,它将该元素的渲染与其他优化一起委托给GPU。这将使动画更加流畅,因为GPU加速接管了动画的渲染。

    // In stylesheet
    .animating-element {
    will-change: opacity;
    }

    // In HTML
    <div class="animating-elememt">
    Animating Child elements
    </div>

    当在浏览器中渲染上述片段时,它将识别 will-change 属性并优化未来与不透明度相关的变化。



    根据Maximillian Laumeister所做的性能基准,可以看到他通过这个单行的改变获得了超过120FPS的渲染速度,而最初的渲染速度大概在50FPS。


    什么时候不是用will-change


    虽然 will-change 的目的是为了提高性能,但如果你滥用它,它也会降低Web应用的性能。




    • **使用 will-change 表示该元素在未来会发生变化。**因此,如果你试图将 will-change 和动画同时使用,它将不会给你带来优化。因此,建议在父元素上使用 will-change ,在子元素上使用动画。

    .my-class{
    will-change: opacity;
    }
    .child-class{
    transition: opacity 1s ease-in-out;
    }
    • 不要使用非动画元素。 当你在一个元素上使用 will-change 时,浏览器会尝试通过将元素移动到一个新的图层并将转换工作交给GPU来优化它。如果您没有任何要转换的内容,则会导致资源浪费。




    最后需要注意的是,建议在完成所有动画后,将元素的 will-change 删除。

    3.减少渲染阻止时间

    今天,许多Web应用必须满足多种形式的需求,包括PC、平板电脑和手机等。为了完成这种响应式的特性,我们必须根据媒体尺寸编写新的样式。当涉及页面渲染时,它无法启动渲染阶段,直到 CSS对象模型(CSSOM)已准备就绪。根据你的Web应用,你可能会有一个大的样式表来满足所有设备的形式因素。
    <link rel="stylesheet" href="styles.css">

    将其分解为多个样式表后:

    <!-- style.css contains only the minimal styles needed for the page rendering -->
    <link rel="stylesheet" href="styles.css" media="all" />
    <!-- Following stylesheets have only the styles necessary for the form factor -->
    <link rel="stylesheet" href="sm.css" media="(min-width: 20em)" />
    <link rel="stylesheet" href="md.css" media="(min-width: 64em)" />
    <link rel="stylesheet" href="lg.css" media="(min-width: 90em)" />
    <link rel="stylesheet" href="ex.css" media="(min-width: 120em)" />
    <link rel="stylesheet" href="print.css" media="print" />

    如您所见,根据样式因素分解样式表可以减少渲染阻止时间。

    4.避免@import包含多个样式表

    通过 @import,我们可以在另一个样式表中包含一个样式表。当我们在处理一个大型项目时,使用 @import 可以使代码更加简洁。



    关于 @import 的关键事实是,它是一个阻塞调用,因为它必须通过网络请求来获取文件,解析文件,并将其包含在样式表中。如果我们在样式表中嵌套了 @import,就会妨碍渲染性能。

    # style.css
    @import url("windows.css");
    # windows.css
    @import url("componenets.css");


    与使用 @import 相比,我们可以通过多个 link 来实现同样的功能,但性能要好得多,因为它允许我们并行加载样式表。

    总结

    除了我们在本文中讨论的4个方面,我们还有一些其他的方法可以使用CSS来提高网页的性能。CSS最近的一个特性: content-visibility,在未来的几年里看起来是如此的有前途,因为它给页面渲染带来了数倍的性能提升。



    最重要的是,我们不需要写一条JavaScript语句就能获得所有的性能。



    我相信你可以结合以上的一些功能,为终端用户构建性能更好的Web应用。希望这篇文章对你有用,如果你知道什么CSS技巧可以提高Web应用的性能,请在下面的评论中提及。谢谢大家。



    链接:https://juejin.cn/post/6911203296078692359

    收起阅读 »

    一种简单实用的 JS 动态加载方案

    背景 在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。 我们有一些具体的案例,例如: 产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功...
    继续阅读 »

    背景


    在做 Web 应用的时候,你或许遇到过这样的场景:为了实现一个使用率很低的功能,却引入了超大的第三方库,导致项目打包后的 JS bundle 体积急剧膨胀。


    我们有一些具体的案例,例如:


    产品要求在项目中增加一个导出数据为 Excel 文件的功能,这个功能其实只有管理员才能看到,而且最多一周才会使用一次,绝对属于低频操作。


    团队里的小伙伴为了实现这个功能,引入了 XLSX 这个库,JS bundle 体积因而增加了一倍,所有用户的体验都受到影响了。


    XLSX 用来做 Excel 相关的操作是不错的选择,但因为新增低频操作影响全部用户却不值得。


    除了导出 Excel 这种功能外,类似的场景还有使用 html2canvas 生成并下载海报,使用 fabric 动态生成图片等。


    针对这种情况,你觉得该如何优化呢?

    自动分包和动态加载


    机智如你很快就想到使用 JS 动态加载,如果熟悉 React,还知道可以使用 react-loadable 来解决。


    原理就是利用 React Code-Splitting,配合 Webpack 自动分包,动态加载。


    这种方案可以,React 也推荐这么做,但是对于引用独立的第三方库这样的场景,还有更简单的方案。

    更简单的方案


    这些第三方库往往都提供了 umd 格式的 min.js,我们动态加载这些 min.js 就可以了。比如 XLSX,引入其 min.js 文件之后,就可以通过 window.XLSX 来实现 Excel 相关的操作。


    此方案的优点有:



    • 与框架无关,不需要和 React 等框架或 Webpack 等工具绑定

    • 精细控制,React Code-Splitting 之类的方案只能到模块级别,想要在点击按钮后才动态加载较难实现

    具体实现

    我们重点需要实现一个 JS 动态加载器 AsyncLoader,代码如下:

    function initLoader() {
    // key 是对应 JS 执行后在 window 中添加的变量
    const jsUrls = {
    html2canvas: 'https://cdn.jsdelivr.net/npm/html2canvas@1.0.0-rc.7/dist/html2canvas.min.js',
    XLSX: 'https://cdn.jsdelivr.net/npm/xlsx@0.16.9/dist/xlsx.min.js',
    flvjs: 'https://cdn.jsdelivr.net/npm/flv.js@1.5.0/dist/flv.min.js',
    domtoimage: 'https://cdn.jsdelivr.net/npm/dom-to-image@2.6.0/src/dom-to-image.min.js',
    fabric: 'https://cdn.jsdelivr.net/npm/fabric@4.3.1/dist/fabric.min.js',
    };

    const loadScript = (src) => {
    return new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.onload = resolve;
    script.onerror = reject;
    script.crossOrigin = 'anonymous';
    script.src = src;
    if (document.head.append) {
    document.head.append(script);
    } else {
    document.getElementsByTagName('head')[0].appendChild(script);
    }
    });
    };

    const loadByKey = (key) => {
    // 判断对应变量在 window 是否存在,如果存在说明已加载,直接返回,这样可以避免多次重复加载
    if (window[key]) {
    return Promise.resolve();
    } else {
    if (Array.isArray(jsUrls[key])) {
    return Promise.all(jsUrls[key].map(loadScript));
    }
    return loadScript(jsUrls[key]);
    }
    };

    // 定义这些方法只是为了方便使用,其实 loadByKey 就够了。
    const loadHtml2Canvas = () => {
    return loadByKey('html2canvas');
    };

    const loadXlsx = () => {
    return loadByKey('XLSX');
    };

    const loadFlvjs = () => {
    return loadByKey('flvjs');
    };

    window.AsyncLoader = {
    loadScript,
    loadByKey,
    loadHtml2Canvas,
    loadXlsx,
    loadFlvjs,
    };
    }

    initLoader();

    使用方式


    以 XLSX 为例,使用这种方式之后,我们不需要在顶部 import xlsx from 'xlsx',只有当用户点击 导出Excel 按钮的时候,才从 CDN 动态加载 xlsx.min.js,加载成功后使用 window.XLSX 即可,代码如下:

    await window.AsyncLoader.loadXlsx().then(() => {
    const XLSX = window.XLSX;
    if (resp.data.signList && resp.data.signList.length > 0) {
    const new_workbook = XLSX.utils.book_new();

    resp.data.signList.map((item) => {
    const header = ['班级/学校/单位', '姓名', '帐号', '签到时间'];
    const { signRecords } = item;
    signRecords.unshift(header);

    const worksheet = XLSX.utils.aoa_to_sheet(signRecords);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, item.signName);
    });

    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    } else {
    const new_workbook = XLSX.utils.book_new();
    const header = [['班级/学校/单位', '姓名', '帐号']];
    const worksheet = XLSX.utils.aoa_to_sheet(header);
    XLSX.utils.book_append_sheet(new_workbook, worksheet, '');
    XLSX.writeFile(new_workbook, `${resp.data.fileName}.xlsx`);
    }
    });

    另一个动态加载 domtoimage 的示例

    window.CommonJsLoader.loadByKey('domtoimage').then(() => {
    const scale = 2;
    window.domtoimage
    .toPng(poster, {
    height: poster.offsetHeight * scale,
    width: poster.offsetWidth * scale,
    style: {
    zoom: 1,
    transform: `scale(${scale})`,
    transformOrigin: 'top left',
    width: `${poster.offsetWidth}px`,
    height: `${poster.offsetHeight}px`,
    },
    })
    .then((dataUrl) => {
    copyImage(dataUrl, liveData?.planName);
    message.success(`${navigator.clipboard ? '复制' : '下载'}成功`);
    });
    });

    AsyncLoader 方案使用方便、理解简单,而且可以很好地利用 CDN 缓存,多个项目可以共用同样的 URL,进一步提高加载速度。而且这种方式使用的是原生 JS,在任何框架中都可以使用。


    注意,如果你用 TypeScript 开发,这种方案或许会丢失一些智能提示,如果引入了对应的 @types/xxx 应该没影响。如果你特别在意开发时的智能提示,也可以在开发的过程中 import 对应的包,开发完成后才换成 AsyncLoader 方案。

    原文:https://juejin.cn/post/6953193301289893901
    收起阅读 »

    当后端一次性丢给你10万条数据, 作为前端工程师的你,要怎么处理?

    前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死....
    继续阅读 »

    前段时间有朋友问我一个他们公司遇到的问题, 说是后端由于某种原因没有实现分页功能, 所以一次性返回了2万条数据,让前端用select组件展示到用户界面里. 我听完之后立马明白了他的困惑, 如果通过硬编码的方式去直接渲染这两万条数据到select中,肯定会卡死. 后面他还说需要支持搜索, 也是前端来实现,我顿时产生了兴趣. 当时想到的方案大致如下:



    1. 采用懒加载+分页(前端维护懒加载的数据分发和分页)

    2. 使用虚拟滚动技术(目前react的antd4.0已支持虚拟滚动的select长列表)


    懒加载和分页方式一般用于做长列表优化, 类似于表格的分页功能, 具体思路就是用户每次只加载能看见的数据, 当滚动到底部时再去加载下一页的数据.


    虚拟滚动技术也可以用来优化长列表, 其核心思路就是每次只渲染可视区域的列表数,当滚动后动态的追加元素并通过顶部padding来撑起整个滚动内容,实现思路也非常简单.


    通过以上分析其实已经可以解决朋友的问题了,但是最为一名有追求的前端工程师, 笔者认真梳理了一下,并基于第一种方案抽象出一个实际的问题:


    如何渲染大数据列表并支持搜索功能?


    笔者将通过模拟不同段位前端工程师的实现方案, 来探索一下该问题的价值. 希望能对大家有所启发, 学会真正的深入思考.

    正文

    笔者将通过不同经验程序员的技术视角来分析以上问题, 接下来开始我们的表演.

    在开始代码之前我们先做好基础准备, 笔者先用nodejs搭建一个数据服务器, 提供基本的数据请求,核心代码如下:

    app.use(async (ctx, next) => {
    if(ctx.url === '/api/getMock') {
    let list = []

    // 生成指定个数的随机字符串
    function genrateRandomWords(n) {
    let words = 'abcdefghijklmnopqrstuvwxyz你是好的嗯气短前端后端设计产品网但考虑到付款啦分手快乐的分类开发商的李开复封疆大吏师德师风吉林省附近',
    len = words.length,
    ret = ''
    for(let i=0; i< n; i++) {
    ret += words[Math.floor(Math.random() * len)]
    }
    return ret
    }

    // 生成10万条数据的list
    for(let i = 0; i< 100000; i++) {
    list.push({
    name: `xu_0${i}`,
    title: genrateRandomWords(12),
    text: `我是第${i}项目, 赶快🌀吧~~`,
    tid: `xx_${i}`
    })
    }

    ctx.body = {
    state: 200,
    data: list
    }
    }
    await next()
    })
    以上笔者是采用koa实现的基本的mock数据服务器, 这样我们就可以模拟真实的后端环境来开始我们的前端开发啦(当然也可以直接在前端手动生成10万条数据). 其中genrateRandomWords方法用来生成指定个数的字符串,这在mock数据技术中应用很多, 感兴趣的盆友可以学习了解一下. 接下来的前端代码笔者统一采用react来实现(vue同理).

    初级工程师的方案

    直接从后端请求数据, 渲染到页面的硬编码方案,思路如下:


    代码可能是这样的:

    1. 请求后端数据:
    fetch(`${SERVER_URL}/api/getMock`).then(res => res.json()).then(res => {
    if(res.state) {
    data = res.data
    setList(data)
    }
    })
    1. 渲染页面
    {
    list.map((item, i) => {
    return <div className={styles.item} key={item.tid}>
    <div className={styles.tit}>{item.title} <span className={styles.label}>{item.name}</span></div>
    <div>{item.text}</div>
    </div>
    })
    }
    1. 搜索数据
    const handleSearch = (v) => {
    let searchData = data.filter((item, i) => {
    return item.title.indexOf(v) > -1
    })
    setList(searchData)
    }

    这样做本质上是可以实现基本的需求,但是有明显的缺点,那就是数据一次性渲染到页面中, 数据量庞大将导致页面性能极具降低, 造成页面卡顿.

    中级工程师的方案

    作为一名有一定经验的前端开发工程师,一定对页面性能有所了解, 所以一定会熟悉防抖函数节流函数, 并使用过诸如懒加载分页这样的方案, 接下来我们看看中级工程师的方案:


    通过这个过程的优化, 代码已经基本可用了, 下面来介绍具体实现方案:

    1. 懒加载+分页方案 懒加载的实现主要是通过监听窗口的滚动, 当某一个占位元素可见之后去加载下一个数据,原理如下:

    1. 这里我们通过监听windowscroll事件以及对poll元素使用getBoundingClientRect来获取poll元素相对于可视窗口的距离, 从而自己实现一个懒加载方案.


    在滚动的过程汇总我们还需要注意一个问题就是当用户往回滚动时, 实际上是不需要做任何处理的,所以我们需要加一个单向锁, 具体代码如下:

    function scrollAndLoading() {
    if(window.scrollY > prevY) { // 判断用户是否向下滚动
    prevY = window.scrollY
    if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
    // 请求下一页数据
    }
    }
    }

    useEffect(() => {
    // something code
    const getData = debounce(scrollAndLoading, 300)
    window.addEventListener('scroll', getData, false)
    return () => {
    window.removeEventListener('scroll', getData, false)
    }
    }, [])

    其中prevY存储的是窗口上一次滚动的距离, 只有在向下滚动并且滚动高度大于上一次时才更新其值.


    至于分页的逻辑, 原生javascript实现分页也很简单, 我们通过定义几个维度:



    • curPage当前的页数

    • pageSize 每一页展示的数量

    • data 传入的数据量


    有了这几个条件,我们的基本能分页功能就可以完成了. 前端分页的核心代码如下:

    let data = [];
    let curPage = 1;
    let pageSize = 16;
    let prevY = 0;

    // other code...

    function scrollAndLoading() {
    if(window.scrollY > prevY) { // 判断用户是否向下滚动
    prevY = window.scrollY
    if(poll.current.getBoundingClientRect().top <= window.innerHeight) {
    curPage++
    setList(searchData.slice(0, pageSize * curPage))
    }
    }
    }
    1. 防抖函数实现 防抖函数因为比较简单, 这里直接上一个简单的防抖函数代码:
    function debounce(fn, time) {
    return function(args) {
    let that = this
    clearTimeout(fn.tid)
    fn.tid = setTimeout(() => {
    fn.call(that, args)
    }, time);
    }
    }
    1. 搜索实现 搜索功能代码如下:
    const handleSearch = (v) => {
    curPage = 1;
    prevY = 0;
    searchData = data.filter((item, i) => {
    // 采用正则来做匹配, 后期支持前端模糊搜索
    let reg = new RegExp(v, 'gi')
    return reg.test(item.title)
    })
    setList(searchData.slice(0, pageSize * curPage))
    }

    需要结合分页来实现, 所以这里为了不影响源数据, 我们采用临时数据searchData来存储. 效果如下:


    搜索后


    无论是搜索前还是搜索后, 都利用了懒加载, 所以再也不用担心数据量大带来的性能瓶颈了~

    高级工程师的方案

    作为一名久经战场的程序员, 我们应该考虑更优雅的实现方式,比如组件化, 算法优化, 多线程这类问题, 就比如我们问题中的大数据渲染, 我们也可以用虚拟长列表来更优雅简洁的来解决我们的需求. 至于虚拟长列表的实现笔者在开头已经点过,这里就不详细介绍了, 对于更大量的数据,比如100万(虽然实际开发中不会遇到这么无脑的场景),我们又该怎么处理呢?


    第一个点我们可以使用js缓冲器来分片处理100万条数据, 思路代码如下:

    function multistep(steps,args,callback){
    var tasks = steps.concat();

    setTimeout(function(){
    var task = tasks.shift();
    task.apply(null, args || []); //调用Apply参数必须是数组

    if(tasks.length > 0){
    setTimeout(arguments.callee, 25);
    }else{
    callback();
    }
    },25);
    }

    这样就能比较大量计算导致的js进程阻塞问题了.更多性能优化方案可以参考笔者之前的文章:



    我们还可以通过web worker来将需要在前端进行大量计算的逻辑移入进去, 保证js主进程的快速响应, 让web worker线程在后台计算, 计算完成后再通过web worker的通信机制来通知主进程, 比如模糊搜索等, 我们还可以对搜索算法进一步优化,比如二分法等,所以这些都是高级工程师该考虑的问题. 但是一定要分清场景, 寻找出性价比更高的方案.


    链接:https://juejin.cn/post/6844904184689475592


    收起阅读 »

    一行可以让项目启动快70%以上的代码

    前言 这两天闲来无事,想优化优化项目的启动时间,用了一个下午吧,将项目启动时间从48秒优化到14秒,大约70左右,效果还是有的,而且仅仅用了一行代码。 👇会讲一下找到这行代码的过程,如果没有耐心可以直接跳转到文章底部,直接看结论即可。 项目背景 项目就是简单的...
    继续阅读 »

    前言


    这两天闲来无事,想优化优化项目的启动时间,用了一个下午吧,将项目启动时间从48秒优化到14秒,大约70左右,效果还是有的,而且仅仅用了一行代码。


    👇会讲一下找到这行代码的过程,如果没有耐心可以直接跳转到文章底部,直接看结论即可。


    项目背景


    项目就是简单的Vue项目,不过公司内部给vue-cli包了一层,不过影响不大。


    别的也就没啥了,正常的H5网页,用的插件也不算多,为了控制项目体积。


    项目分析


    既然决定要优化了,首先要分析下项目,先用speed-measure-webpack-pluginwebpack-bundle-analyzer分析下,具体的配置这里就不多说了,很简单,网上一搜一大堆,这里直接看看结论。


    首先是项目运行时间:



    可以看到,基本上耗时大户就是eslint-loadervue-loader了,二者一个耗时40多秒,一个耗时30多秒,非常的占用资源。

    接下来再看看具体的包分析👇


    这一看就很一下子定位到问题到根源了,右侧的chunk-vendors不用看,只看左侧的chunk-page,这里面的页面数量太多了,相应的文件也很多,这也就直接导致了eslint-loadervue-loader耗时很久了,这么多文件,一个个检查耗时当然久了。


    右侧其实还可以继续优化,但感觉没必要,swiper其实并不大。


    那么现在就可以具体定位到问题了,由于项目是多SPA应用,致使.vue文件众多,在项目启动时进行eslint检查和加载耗时过长,导致项目启动时间较久。

    解决方案


    找到问题之后就得解决问题了,初步的解决方案有两个:



    1. 干掉eslint,在本地编译时不检查

    2. 缓存


    解决方案1必然是最简单的,但其实有点不合理,开着eslint就是为了规范代码格式,虽然在提交代码时也有对应的钩子来格式化代码,但在开发过程中进行提示可以更好的帮助我们形成合理的编码方式。


    所以现在剩下的方案就只有进行缓存操作了,接下来笔者就开始找相关插件来更好的进行缓存了。


    尝试解决


    首先是hard-source-webpack-plugin,这插件为模块提供中间缓存步骤,但项目得跑两次,第一次构建时间正常,第二次大概能省去90%左右的时间。


    这插件很多文章都有推荐,感觉很不错的样子,用起来也很简单,只需要👇:

    plugins: [
    new HardSourceWebpackPlugin()
    ]

    这就完事了。

    就这么简单?确实是这么简单,但也不简单,如果到此为止,笔者也不会折腾一下午了。

    就这么简单的一安装:

    npm i hard-source-webpack-plugin -D

    然后像👆一样简单的配置,然后重启项目,您猜怎么着?


    报错了!


    原因是什么呢?


    是因为speed-measure-webpack-plugin或者webpack-bundle-analyzer中的某一个,为什么呢?


    原因笔者其实并不太清楚,因为启动的时候报的错是这样的:

    Cannot find module 'webpack/lib/DependenciesBlockVariable'

    哦呦,这个错有点小意外,怎么会突然报webpack的错呢?


    笔者也是百思不得其解啊,去Google也没有人遇到这种问题。


    不得已,只能去hard-source-webpack-plugin的github上看issue,发现其实有人遇到这个问题的,他们的解决方案就是降低webpack的版本,可笔者这里没办法这么做,因为都集成在vue-cli里了,而且这个还是公司内部包了一层的,这就根本不可能降版本了。


    第一个转机


    那还能怎么办呢?


    实在没有办法了,笔者尝试搜索DependenciesBlockVariable的相关内容,这时事情发生了一丝微妙的变换,原来这个功能在webpack5中被移除了,难道是因为公司内部的vue-cli用的是webpack5.x版本?



    笔者当即在node_modules里面找到了插件,然后查看了package.json文件,结果失望的发现webpack的版本是4.2.6,这就令人绝望了,难道真的不可以么?


    既然打开了webpack的文档,那就好好看看吧。老实说这文档笔者已经看了N次了,真是每次看都有小惊喜,功能真是太多了。


    翻着翻着就看到了这个小功能👇:

    哦呦,还真有点小惊喜呦,这功能简直了,这不就是我想要的么?然后当机立断,往vue.config.js里一家,您猜怎么着?


    成了!


    虽然文档是webpack5.0的,但笔者发现4.x版本中也有这个功能,可能若一弱一些吧,多少能用啊。


    重启了几次项目后发现启动时间已经稳定了,效果真的还不错呦~


    直接给我干到了14秒,虽然有些不太稳定,但这已经是当前状态的最好解决方案了。

    所以最后的代码就是:

    chainWebpack: (config) => {
    config.cache(true)
    }

    chainWebpack的原因是项目中其实没有独立的webpack.config.js文件,所以只能放在vue.config.js文件中,使用chainWebpack来将配置插入到webpack中去。


    你以为事情到这里就结束了么?太简单了。


    第二个转机


    解决完问题后,当然要把speed-measure-webpack-plugin和webpack-bundle-analyzer这两个插件删掉了,然后整理整理代码,推上去完事。


    可笔者还是不死心,为啥hard-source-webpack-plugin不好使呢?不应该啊,为啥别人都能用,自己的项目却用不了呢?


    为了再次操作一手,也是为了更好的优化项目的启动时间,笔者再次安装了hard-source-webpack-plugin,并且对其进行了配置:

    chainWebpack: (config) => {
    config.plugin('cache').use(HardSourceWebpackPlugin)
    }

    这次再一跑,您猜怎么着?


    成了!


    为了避免再次启动失败了,笔者这次没有使用speed-measure-webpack-plugin和webpack-bundle-analyzer这两个插件,所以启动时间也没法具体估计了,但目测时间再10秒以内,强啊。


    所以说hard-source-webpack-plugin失败的原因可能就是那两个统计插件的原因了,得亏再试了一次,要不然就不明不白的GG了。


    结论


    这里的结论就很简单了,有两个版本。


    首先,如果项目能使用hard-source-webpack-plugin就很方便了,用就完事了,啥事也不需要干,所以这一行代码是👇:

    config.plugin('cache').use(HardSourceWebpackPlugin)

    大概真能快90%以上,官方并没有虚报时间。


    其次,如果用不了hard-source-webpack-plugin那就放弃吧,尝试webpack自带的cache功能也是不错的,虽然比不上hard-source-webpack-plugin,但多少也能提升70%左右的启动时间,所以这一行代码是👇:

    config.cache(true)

    并且不需要安装任何插件,一步到位。


    这两种方法其实都是可行了,论稳定和效果的话hard-source-webpack-plugin还是更胜一筹,但cache胜在不用装额外的webpack插件,具体用什么就自己决定吧。


    这里其实还是留了个坑,hard-source-webpack-plugin用不了的具体原因是什么呢?笔者只是猜测和speed-measure-webpack-plugin、webpack-bundle-analyzer这两个插件有关,但却不能肯定,如果有读者知道,欢迎在评论区留言或者私信笔者。


    看了这么久,辛苦了!


    链接:https://juejin.cn/post/6961203055257714702


    收起阅读 »

    哇擦!他居然把 React 组件渲染到了命令行终端窗口里面

    也许你之前听说过前端组件代码可以运行在浏览器,运行在移动端 App 里面,甚至可以直接在各种设备当中,但你有没有见过: 前端组件直接跑在命令行窗口里面,让前端代码构建出终端窗口的 GUI 界面和交互逻辑? 今天, 给大家分享一个非常有意思的开源项目: ink。...
    继续阅读 »

    也许你之前听说过前端组件代码可以运行在浏览器,运行在移动端 App 里面,甚至可以直接在各种设备当中,但你有没有见过: 前端组件直接跑在命令行窗口里面,让前端代码构建出终端窗口的 GUI 界面和交互逻辑?


    今天, 给大家分享一个非常有意思的开源项目: ink。它的作用就是将 React 组件渲染在终端窗口中,呈现出最后的命令行界面。


    本文偏重实战,前面会带大家熟悉基本使用,然后会做一个基于实际场景的练手项目。


    上手初体验

    刚开始上手时,推荐使用官方的脚手架创建项目,省时省心。

    npx create-ink-app --typescript

    然后运行这样一段代码:

    import React, { useState, useEffect } from 'react'
    import { render, Text} from 'ink'

    const Counter = () => {
    const [count, setCount] = useState(0)
    useEffect(() => {
    const timer = setInterval(() => {
    setCount(count => ++count)
    }, 100)
    return () => {
    clearInterval(timer)
    }

    })

    return (
    <Text color="green">
    {count} tests passed
    </Text>
    )
    }

    render(<Counter />);

    会出现如下的界面:


    并且数字一直递增! demo 虽小,但足以说明问题:




    1. 首先,这些文本输出都不是直接 console 出来的,而是通过 React 组件渲染出来的。




    2. React 组件的状态管理以及hooks 逻辑放到命令行的 GUI 当中仍然是生效的。




    也就是说,前端的能力以及扩展到了命令行窗口当中了,这无疑是一项非常可怕的能力。著名的文档生成工具
    Gatsby,包管理工具yarn2都使用了这项能力来完成终端 GUI 的搭建。

    命令行工具项目实战


    可能大家刚刚了解到这个工具,知道它的用途,但对于具体如何使用还是比较陌生。接下来让我们以一个实际的例子来进行实战,快速熟悉。代码仓库已经上传到 git,大家可以这个地址下面 fork 代码: github.com/sanyuan0704…


    下面我们就来从头到尾开发这个项目。


    项目背景


    首先说一说项目的产生背景,在一个 TS 的业务项目当中,我们曾经碰到了一个问题:由于production模式下面,我们是采用先 tsc,拿到 js 产物代码,再用webpack打包这些产物。


    但构建的时候直接报错了,原因就是 tsc 无法将 ts(x) 以外的资源文件移动到产物目录,以至于 webpack 在对于产物进行打包的时候,发现有些资源文件根本找不到!比如以前有这样一张图片的路径是这样—— src/asset/1.png,但这些在产物目录dist却没还有,因此 webpack 在打包 dist 目录下的代码时,会发现这张图片不存在,于是报错了。


    解决思路


    那如何来解决呢?


    很显然,我们很难去扩展 tsc 的能力,现在最好的方式就是写个脚本手动将src下面的所有资源文件一一拷贝到dist目录,这样就能解决资源无法找到的问题。


    一、拷贝文件逻辑


    确定了解决思路之后,我们写下这样一段 ts 代码:

    import { join, parse } from "path";
    import { fdir } from 'fdir';
    import fse from 'fs-extra'
    const staticFiles = await new fdir()
    .withFullPaths()
    // 过滤掉 node_modules、ts、tsx
    .filter(
    (p) =>
    !p.includes('node_modules') &&
    !p.endsWith('.ts') &&
    !p.endsWith('.tsx')
    )
    // 搜索 src 目录
    .crawl(srcPath)
    .withPromise() as string[]

    await Promise.all(staticFiles.map(file => {
    const targetFilePath = file.replace(srcPath, distPath);
    // 创建目录并拷贝文件
    return fse.mkdirp(parse(targetFilePath).dir)
    .then(() => fse.copyFile(file, distPath))
    );
    }))

    代码使用了fdir这个库才搜索文件,非常好用的一个库,写法上也很优雅,推荐大家使用。


    我们执行这段逻辑,成功将资源文件转移到到了产物目录中。


    问题是解决掉了,但我们能不能封装一下这个逻辑,让它能够更方便地在其它项目当中复用,甚至直接提供给其他人复用呢?


    接着,我想到了命令行工具。


    二、命令行 GUI 搭建


    接着我们使用 ink,也就是用 React 组件的方式来搭建命令行 GUI,根组件代码如下:

    // index.tsx 引入代码省略
    interface AppProps {
    fileConsumer: FileCopyConsumer
    }

    const ACTIVE_TAB_NAME = {
    STATE: "执行状态",
    LOG: "执行日志"
    }

    const App: FC<AppProps> = ({ fileConsumer }) => {
    const [activeTab, setActiveTab] = useState<string>(ACTIVE_TAB_NAME.STATE);
    const handleTabChange = (name) => {
    setActiveTab(name)
    }
    const WELCOME_TEXT = dedent`
    欢迎来到 \`ink-copy\` 控制台!功能概览如下(按 **Tab** 切换):
    `

    return <>
    <FullScreen>
    <Box>
    <Markdown>{WELCOME_TEXT}</Markdown>
    </Box>
    <Tabs onChange={handleTabChange}>
    <Tab name={ACTIVE_TAB_NAME.STATE}>{ACTIVE_TAB_NAME.STATE}</Tab>
    <Tab name={ACTIVE_TAB_NAME.LOG}>{ACTIVE_TAB_NAME.LOG}</Tab>
    </Tabs>
    <Box>
    <Box display={ activeTab === ACTIVE_TAB_NAME.STATE ? 'flex': 'none'}>
    <State />
    </Box>
    <Box display={ activeTab === ACTIVE_TAB_NAME.LOG ? 'flex': 'none'}>
    <Log />
    </Box>
    </Box>
    </FullScreen>
    </>
    };

    export default App;

    可以看到,主要包含两大组件: StateLog,分别对应两个 Tab 栏。具体的代码大家去参考仓库即可,下面放出效果图:


    3. GUI 如何实时展示业务状态?


    现在问题就来了,文件操作的逻辑开发完了,GUI 界面也搭建好了。那么现在如何将两者结合起来呢,也就是 GUI 如何实时地展示文件操作的状态呢?


    对此,我们需要引入第三方,来进行这两个模块的通信。具体来讲,我们在文件操作的逻辑中维护一个 EventBus 对象,然后在 React 组件当中,通过 Context 的方式传入这个 EventBus。
    从而完成 UI 和文件操作模块的通信。


    现在我们开发一下这个 EventBus 对象,也就是下面的FileCopyConsumer:

    export interface EventData {
    kind: string;
    payload: any;
    }

    export class FileCopyConsumer {

    private callbacks: Function[];
    constructor() {
    this.callbacks = []
    }
    // 供 React 组件绑定回调
    onEvent(fn: Function) {
    this.callbacks.push(fn);
    }
    // 文件操作完成后调用
    onDone(event: EventData) {
    this.callbacks.forEach(callback => callback(event))
    }
    }

    接着在文件操作模块和 UI 模块当中,都需要做响应的适配,首先看看文件操作模块,我们做一下封装。

    export class FileOperator {
    fileConsumer: FileCopyConsumer;
    srcPath: string;
    targetPath: string;
    constructor(srcPath ?: string, targetPath ?: string) {
    // 初始化 EventBus 对象
    this.fileConsumer = new FileCopyConsumer();
    this.srcPath = srcPath ?? join(process.cwd(), 'src');
    this.targetPath = targetPath ?? join(process.cwd(), 'dist');
    }

    async copyFiles() {
    // 存储 log 信息
    const stats = [];
    // 在 src 中搜索文件
    const staticFiles = ...

    await Promise.all(staticFiles.map(file => {
    // ...
    // 存储 log
    .then(() => stats.push(`Copied file from [${file}] to [${targetFilePath}]`));
    }))
    // 调用 onDone
    this.fileConsumer.onDone({
    kind: "finish",
    payload: stats
    })
    }
    }

    然后在初始化 FileOperator之后,将 fileConsumer通过 React Context 传入到组件当中,这样组件就能访问到fileConsumer,进而可以进行回调函数的绑定,代码演示如下:

    // 组件当中拿到 fileConsumer & 绑定回调
    export const State: FC<{}> = () => {
    const context = useContext(Context);
    const [finish, setFinish] = useState(false);
    context?.fileConsumer.onEvent((data: EventData) => {
    // 下面的逻辑在文件拷贝完成后执行
    if (data.kind === 'finish') {
    setTimeout(() => {
    setFinish(true)
    }, 2000)
    }
    })

    return
    //(JSX代码)
    }

    这样,我们就成功地将 UI 和文件操作逻辑串联了起来。当然,篇幅所限,还有一些代码并没有展示出来,完整的代码都在 git 仓库当中。希望大家能 fork 下来好好体会一下整个项目的设计。


    总体来说,React 组件代码能够跑在命令行终端,确实是一件激动人心的事情,给前端释放了更多想象的空间。本文对于这个能力的使用也只是冰山一角,更多使用姿势等待你去解锁,赶紧去玩一玩吧!


    链接:https://juejin.cn/post/6952673382928220191

    收起阅读 »

    如何应用 SOLID 原则整理 React 代码之单一原则

    SOLID 原则的主要是作为关心自己工作的软件专业人员的指导方针,另外还为那些以经得起时间考验的设计精美的代码库为荣的人。 今天,我们将从一个糟糕的代码示例开始,应用 SOLID 的第一个原则,看看它如何帮助我们编写小巧、漂亮、干净的并明确责任的 React ...
    继续阅读 »

    SOLID 原则的主要是作为关心自己工作的软件专业人员的指导方针,另外还为那些以经得起时间考验的设计精美的代码库为荣的人。


    今天,我们将从一个糟糕的代码示例开始,应用 SOLID 的第一个原则,看看它如何帮助我们编写小巧、漂亮、干净的并明确责任的 React 组件,。

    什么是单一责任原则?

    单一责任原则告诉我们的是,每个类或组件应该有一个单一的存在目的。

    组件应该只做一件事,并且做得很好。

    让我们重构一段糟糕但正常工作的代码,并使用这个原则使其更加清晰和完善。

    让我们从一个糟糕的例子开始

    首先让我们看看一些违反这一原则的代码,添加注释是为了更好地理解:

    import React, {useEffect, useReducer, useState} from "react";

    const initialState = {
    isLoading: true
    };

    // 复杂的状态管理
    function reducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    export const SingleResponsibilityPrinciple = () => {

    const [users , setUsers] = useState([])
    const [filteredUsers , setFilteredUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const showDetails = (userId) => {
    const user = filteredUsers.find(user => user.id===userId);
    alert(user.contact)
    }

    // 远程数据获取
    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    // 数据处理
    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    // 复杂UI渲染
    return <>
    Users List

    Loading state: {state.isLoading? 'Loading': 'Success'}

    {users.map(user => {
    return
    showDetails(user.id)}>
    {user.name}

    {user.email}


    })}

    }

    这段代码的作用

    这是一个函数式组件,我们从远程数据源获取数据,再过滤数据,然后在 UI 中显示数据。我们还检测 API 调用的加载状态。

    为了更好地理解这个例子,我把它简化了。但是你几乎可以在任何地方的同一个组件中找到它们!这里发生了很多事情:

    1. 远程数据的获取

    2. 数据过滤

    3. 复杂的状态管理

    4. 复杂的 UI 功能

    因此,让我们探索如何改进代码的设计并使其紧凑。

    1. 移动数据处理逻辑

    不要将 HTTP 调用保留在组件中。这是经验之谈。您可以采用几种策略从组件中删除这些代码。

    您至少应该创建一个自定义 Hook 并将数据获取逻辑移动到那里。例如,我们可以创建一个名为 useGetRemoteData 的 Hook,如下所示:

    import {useEffect, useReducer, useState} from "react";

    const initialState = {
    isLoading: true
    };

    function reducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    export const useGetRemoteData = (url) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(reducer, initialState);

    const [filteredUsers , setFilteredUsers] = useState([])


    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch('https://jsonplaceholder.typicode.com/users')
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading: state.isLoading}
    }

    现在我们的主要组件看起来像这样:

    import React from "react";
    import {useGetRemoteData} from "./useGetRemoteData";

    export const SingleResponsibilityPrinciple = () => {

    const {filteredUsers , isLoading} = useGetRemoteData()

    const showDetails = (userId) => {
    const user = filteredUsers.find(user => user.id===userId);
    alert(user.contact)
    }

    return <>
    Users List

    Loading state: {isLoading? 'Loading': 'Success'}

    {filteredUsers.map(user => {
    return
    showDetails(user.id)}>
    {user.name}

    {user.email}


    })}

    }

    看看我们的组件现在是多么的小,多么的容易理解!这是在错综复杂的代码库中所能做的最简单、最重要的事情。

    但我们可以做得更好。

    2. 可重用的数据获取钩子

    现在,当我们看到我们 useGetRemoteData Hook 时,我们看到这个 Hook 正在做两件事:




    1. 从远程数据源获取数据




    2. 过滤数据




    让我们把获取远程数据的逻辑提取到一个单独的钩子,这个钩子的名字是 useHttpGetRequest,它把 URL 作为一个参数:

    import {useEffect, useReducer, useState} from "react";
    import {loadingReducer} from "./LoadingReducer";

    const initialState = {
    isLoading: true
    };

    export const useHttpGetRequest = (URL) => {

    const [users , setUsers] = useState([])
    const [state, dispatch] = useReducer(loadingReducer, initialState);

    useEffect(() => {
    dispatch({type:'LOADING'})
    fetch(URL)
    .then(response => response.json())
    .then(json => {
    dispatch({type:'FINISHED'})
    setUsers(json)
    })
    },[])

    return {users , isLoading: state.isLoading}

    }

    我们还将 reducer 逻辑移除到一个单独的文件中:

    export function loadingReducer(state, action) {
    switch (action.type) {
    case 'LOADING':
    return {isLoading: true};
    case 'FINISHED':
    return {isLoading: false};
    default:
    return state;
    }
    }

    所以现在我们的 useGetRemoteData 变成了:

    import {useEffect, useState} from "react";
    import {useHttpGetRequest} from "./useHttpGet";
    const REMOTE_URL = 'https://jsonplaceholder.typicode.com/users'

    export const useGetRemoteData = () => {
    const {users , isLoading} = useHttpGetRequest(REMOTE_URL)
    const [filteredUsers , setFilteredUsers] = useState([])

    useEffect(() => {
    const filteredUsers = users.map(user => {
    return {
    id: user.id,
    name: user.name,
    contact: `${user.phone} , ${user.email}`
    };
    });
    setFilteredUsers(filteredUsers)
    },[users])

    return {filteredUsers , isLoading}
    }

    干净多了,对吧? 我们能做得更好吗? 当然,为什么不呢?

    3. 分解 UI 组件

    看看我们的组件,其中显示了用户的详细信息。我们可以为此创建一个可重用的 UserDetails 组件:

    const UserDetails = (user) => {

    const showDetails = (user) => {
    alert(user.contact)
    }

    return
    showDetails(user)}>
    {user.name}

    {user.email}


    }

    最后,我们的原始组件变成:

    import React from "react";
    import {useGetRemoteData} from "./useGetRemoteData";

    export const Users = () => {
    const {filteredUsers , isLoading} = useGetRemoteData()

    return <>
    Users List

    Loading state: {isLoading? 'Loading': 'Success'}

    {filteredUsers.map(user => )}

    }

    我们把代码从60行精简到了12行!我们创建了五个独立的组成部分,每个部分都有明确而单一的职责。

    让我们回顾一下我们刚刚做了什么


    让我们回顾一下我们的组件,看看我们是否实现了 SRP:




    • Users.js - 负责显示用户列表




    • UserDetails.js ー 负责显示用户的详细资料




    • useGetRemoteData.js - 负责过滤远程数据




    • useHttpGetrequest.js - 负责 HTTP 调用




    • LoadingReducer.js - 复杂的状态管理




    当然,我们可以改进很多其他的东西,但是这应该是一个很好的起点。


    链接:https://juejin.cn/post/6963480203637030926




    收起阅读 »

    React的性能优化(useMemo和useCallback)的使用

    一、业务场景 React是一个用于构建用户界面的javascript的库,主要负责将数据转换为视图,保证数据和视图的统一,react在每次数据更新的时候都会重新render来保证数据和视图的统一,但是当父组件内部数据的变化,在父组件下挂载的所有子组件也会重新...
    继续阅读 »

    一、业务场景



    React是一个用于构建用户界面的javascript的库,主要负责将数据转换为视图,保证数据和视图的统一,react在每次数据更新的时候都会重新render来保证数据和视图的统一,但是当父组件内部数据的变化,在父组件下挂载的所有子组件也会重新渲染,因为react默认会全部渲染所有的组件,包括子组件的子组件,这就造成不必要的浪费。

    1、使用类定义一个父组件
    export default class Parent extends React.Component {
    state = {
    count: 0,
    }
    render() {
    return(
    <div>
    我是父组件
    <button onClick={() => this.setState({count: this.state.count++})}>点击按钮</button>
    <Child />
    </div>
    )
    }
    }

    2、定义一个子组件

    class Child extends React.Component {
    render() {
    console.log('我是子组件');
    return (
    <div>
    我是子组件
    <Grandson />
    </div>
    )
    }
    }

    3、定义一个孙子组件

    class Grandson extends React.Component {
    render() {
    console.log('孙子组件')
    return(<div>孙子组件</div>)
    }
    }
    • 4、上面几个组件是比较标准的react的类组件,函数组件也是类似的,当你在父组件中点击按钮,其实你仅仅是想改变父组件内的count的值,但是你会发现每次点击的时候子组件和孙组件也会重新渲染,因为react并不知道是不是要渲染子组件,需要我们自己去判断。



    一、类组件中使用shouldComponentUpdate生命周期钩子函数

    1、在子组件中使用shouldComponentUpdate来判断是否要更新,

    其实就是根据this.props和函数参数中的nextProps中的参数来对比,如果返回false就不更新,如果返回ture就表示需要更新当前组件


    class Child extends React.Component {
    shouldComponentUpdate (nextProps, nextState) {
    console.log(nextProps, this.props);
    if (nextProps.count === this.props.count) {
    return false;
    } else {
    return true;
    }
    }
    ...
    }
  • **注意点:**这里的count是要父组件给当前组件传递的参数(就是你要监听变化的的来更新当前组件),如果你写一个nextProps.name === this.props.name其实,父组件并没有给当前组件传递name那么下面都是返回false组件不更新




  • 2、当子组件没更新,那么孙组件同样的不更新数据


  • 二、使用PureComponet语法糖

    其实PureComponet就是一个语法糖,只是官方在底层帮你实现了shouldComponentUpdate方法而已,使用的时候只需要子类继承这个类就可以

    • 1、子组件中继承

    class Child extends React.PureComponent {
    render() {
    console.log('我是子组件');
    return (
    <div>
    我是子组件
    <Grandson />
    </div>
    )
    }
    }

    2、在父组件中使用

    // 下面这种情况不会重新渲染子组件
    <Child/>
    // 下面这种情况下会重新渲染子组件
    <Child count={this.state.count}/>

    三、memo的使用



    当你子组件是类组件的时候可以使用shouldComponentUpdate钩子函数或类组件继承PureComponent来实现不渲染子组件,但是对于函数组件来说是不能用这两个方法的,因此react官方给函数组件提供了memo来对函数组件包装下,实现不必要的渲染。





    • 1、组件定义(这里也可以使用类组件)

    function Child () {
    console.log('我是子组件');
    return (
    <div>
    子组件
    </div>
    )
    }

    function Parent () {
    const [count, setCount] = useState(0);
    return (
    <div>
    我是父组件-{count}
    <button onClick={()=>setCount(count + 1)}>点击按钮</button>
    <Child />
    </div>
    )
    }

    2、这里我们父组件内部改变count并没有传递给子组件,但是子组件一样的重新渲染了,这并不是我们希望看到的,因为需要对子组件包装下

    function Child () {
    console.log('我是子组件');
    return (
    <div>
    子组件
    </div>
    )
    }

    const ChildMemo = React.memo(Child);
    function Parent () {
    const [count, setCount] = useState(0);
    return (
    <div>
    我是父组件-{count}
    <button onClick={()=>setCount(count + 1)}>点击按钮</button>
    {/* 这种情况下不会渲染子组件 */}
    <ChildMemo />
    {/* 这种情况下会渲染子组件 */}
    <ChildMemo count={count}/>
    </div>
    )
    }

    四、useMemouseCallback的认识




    • 1、useMemouseCallback都是具有缓存作用的,只是他们缓存对象不一样,一个是属性,一个是缓存函数,特点都是,当缓存依赖的没变,去获取还是获取曾经的缓存




    • 2、useMemo是对函数组件中的属性包装,返回一个具有缓存效果的新的属性,当依赖的属性没变化的时候,这个返回新属性就会从缓存中获取之前的。




    • 3、useCallback是对函数组件中的方法缓存,返回一个被缓存的方法

    五、useMemo的使用(我们依赖借用子组件更新的来做)

    • 1、根据上面的方式我们在父组件更新数据,观察子组件变化

    const Child = (props) => {
    console.log('重新渲染子组件', props);
    return (
    <div>子组件</div>
    )
    }
    const ChildMemo = React.memo(Child);

    const Parent = () => {
    const [count, setCount] = useState(0);
    const [number, setNumber]=useState(0)
    const userInfo = {
    age: count,
    name: 'hello',
    }

    const btnHandler = () => {
    setNumber(number+1);
    }
    return (
    <div>
    {number}-{count}
    <button onClick={btnHandler}>按钮</button>
    <ChildMemo userInfo={userInfo}/>
    </div>
    )
    }
  • 上面发现我们仅仅是更新了number的值,传递给子组件的对象值并没有变化,但是每次子组件都重新更新了,虽然我们在子组件上用了React.memo包装还是不行,这是因为在父组件中每次重新渲染,对于对象来说会是重新一个新的对象了。因此子组件要重新更新,




  • 2、使用useMemo对属性的包装

  • const userInfo = useMemo(() => {
    return {
    age: count,
    name: 'hello',
    };
    }, [count]);
  • 使用useMemo包装后的对象,重新返回一个具有缓存效果的新对象,第二个参数表示依赖性,或者叫观察对象,当被观察的没变化,返回的就是缓存对象,如果被观察的变化了,那么就会返回新的,现在不管你怎么更新number的值,子组件都不会重新更新了




  • 3、注意点:useMemo要配合React.memo来使用,不然传递到子组件也是不生效的

  • 六、useCallback的使用

    前面介绍了,useCallback是对一个方法的包装,返回一个具有缓存的方法,常见的使用场景是,父组件要传递一个方法给子组件

    • 1、在不使用useCallback的时候

    const Child = (props) => {
    console.log('渲染了子组件');
    const { onClick } = props;
    return (
    <button onClick={onClick}>点击按钮获取值</button>
    )
    }

    const ChildMemo = React.memo(Child);

    const Parent = () => {
    const [text, updateText] = useState('');
    const textRef = useRef(text);
    const handleSubmit = () => {
    console.log('当前的值', text);
    }
    return(
    <div>
    我是父组件
    <input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
    <ChildMemo onClick={handleSubmit}/>
    </div>
    )
    }
    • 结果是每次输入框输入值的时候,子组件就会重新渲染一次,其实子组件中仅仅是一个按钮,要获取最终输入的值,每次父组件输入值的时候,子组件就更新,很耗性能的

    2、使用useCallback来包装一个方法

    const Parent = () => {
    const [text, updateText] = useState('');
    const textRef = useRef();

    // useCallback又依赖了textRef的变化,因此可以获取到最新的数据
    const handleSubmit = useCallback(() => {
    console.log('当前输入框的值:', textRef.current);
    }, [textRef])

    // 当text的值变化的时候就会给textRef的current重新赋值
    useEffect(() => {
    textRef.current = text;
    }, [text]);

    return(
    <div>
    我是父组件
    <input type="text" value={text} onChange={(e) => updateText(e.target.value)}/>
    <ChildMemo onClick={handleSubmit}/>
    </div>
    )
    }


    原文:https://juejin.cn/post/6965302793242411021

    收起阅读 »

    React22-diff算法

    1.时间复杂度最好的算法都是o(n3) n为元素个数 如果每个元素都去diff的话复杂度很高 所以react做出啦三个限制 1.只对同级元素进行diff,不用对另一个树的所有元素进行diff 2.tag不同的两个元素会产生不同的树,div变为p,react会销...
    继续阅读 »

    1.时间复杂度

    最好的算法都是o(n3) n为元素个数 如果每个元素都去diff的话复杂度很高 所以react做出啦三个限制


    1.只对同级元素进行diff,不用对另一个树的所有元素进行diff


    2.tag不同的两个元素会产生不同的树,div变为p,react会销毁div及其子孙节点,新建p


    3.通过key这个prop来暗示哪些子元素在不同渲染下保持稳定


    举例说明第3点

    // 更新前
    <div>
    <p >ka</p>
    <h3 >song</h3>
    </div>

    // 更新后
    <div>
    <h3 >song</h3>
    <p>ka</p>
    </div>
    这种情况下diff react会把p删除然后新建h3 插入 然后删除h3创建p在传入
    // 更新前
    <div>
    <p key="ka">ka</p>
    <h3 key="song">song</h3>
    </div>

    // 更新后
    <div>
    <h3 key="song">song</h3>
    <p key="ka">ka</p>
    </div>
    //有key的情况,p节点找到后面key相同的p节点所以节点可以复用 h3也可以复用 只需要做一个p append到h3后面的操作

    2.单一节点的diff

    1.diff 就是对比jsx和currentfiber的对象生成workingprogress的fiber


    2.dom节点是否可复用?1.type必须相同 2.key也必须相同 满足这两个才能复用 先检测key是否同再检测type是否同,可复用就复用这个fiber 不过是换属性而已



    3.什么情况不能复用?


    1.key不一样: 这函数一开始就判断key是否相同 不相同直接删除current的fiber,然后找兄弟节点去看是否key相同,为什么找兄弟节点呢 因为可能currentfiber有同级节点而jsx只是单个节点,还是会走到singleelement这个逻辑,所以要看currentfiber同级所有节点,不能旧删除是否可以复用。同时如果某个节点可以复用我们也需要将currentfiber的其他同级fiber删掉。都是为了下面这种情况。


    如果都不一样则创建新的fiber

    //current
    <div></div>
    <p></p>
    //jsx
    <p><p>

    2.type不一样:直接把current的该fiber和兄弟fiber全部删除,因为能判断到type的时候key已经相同啦,其他兄弟节点的key不可能相同,所以直接全部不可以复用。之前key不同还要看兄弟key是否相同。

    3.多节点diff

    什么时候执行多节点diff?当jsx此次为数组即可,无论currentfiber是不是数组

    一共有三种情况

    1.节点更新

    //情况1—— 节点属性变化
    // 之前
    <ul>
    <li key="0" className="before">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="0" className="after">0<li>
    <li key="1">1<li>
    </ul>
    //情况2—— 节点类型更新
    // 之后
    <ul>
    <div key="0">0</div>
    <li key="1">1<li>
    </ul>

    2.节点新增或者减少

    //情况1 —— 新增节点
    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    <li key="2">2<li>
    </ul>
    //情况2 —— 删除节点
    // 之后
    <ul>
    <li key="1">1<li>
    </ul>

    3.节点位置变化

    // 之前
    <ul>
    <li key="0">0<li>
    <li key="1">1<li>
    </ul>

    // 之后
    <ul>
    <li key="1">1<li>
    <li key="0">0<li>
    </ul>

    在我们做数组相关的算法题时,经常使用双指针从数组头和尾同时遍历以提高效率,但是这里却不行。


    虽然本次更新的JSX对象 newChildren为数组形式,但是和newChildren中每个组件进行比较的是current fiber,同级的Fiber节点是由sibling指针链接形成的单链表,即不支持双指针遍历。


    newChildren[0]fiber比较,newChildren[1]fiber.sibling比较。


    所以无法使用双指针优化。


    多节点diff 最终会产生一条fiber链表,不过最后返回的还是一个fiber(第一个fiber)作为child


    基于以上原因,Diff算法的整体逻辑会经历两轮遍历:


    第一轮遍历:处理更新的节点。 因为更新的概率是最大的


    第二轮遍历:处理剩下的不属于更新的节点

    第一轮遍历



    1. let i = 0,遍历newChildren,将newChildren[i]oldFiber比较,判断DOM节点是否可复用。

    2. 如果可复用,i++,继续比较newChildren[i]oldFiber.sibling,可以复用则继续遍历。

    3. 如果不可复用,分两种情况:



    • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。(因为key不同是对应节点位置变化不属于更新节点,等到第二轮循环处理)

    • key相同type不同导致不可复用,会将oldFiber标记为DELETION(这样在commit阶段就会删除这个dom),并继续遍历



    1. 如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。

    function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
    ): Fiber | null {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.

    if (__DEV__) {
    // First, validate keys.
    let knownKeys = null;
    for (let i = 0; i < newChildren.length; i++) {
    const child = newChildren[i];
    knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
    }
    }

    let resultingFirstChild: Fiber | null = null;//!返回的fiber
    let previousNewFiber: Fiber | null = null;//!创建fiber链需要一个临时fiber来做连接

    let oldFiber = currentFirstChild;//!遍历到的current的fiber(旧的fiber)
    let lastPlacedIndex = 0;//!新创建的fiber节点对应的dom节点在页面中的位置 用来节点位置变化的
    let newIdx = 0;//!遍历到的jsx数组的索引
    let nextOldFiber = null;//!oldFiber的下一个fiber
    //!第一轮循环 处理节点更新的情况
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {//!fiber的index标记为当前fiber在同级fiber中的位置
    nextOldFiber = oldFiber;
    oldFiber = null;
    } else {
    nextOldFiber = oldFiber.sibling;
    }
    const newFiber = updateSlot(//!判断节点是否可以复用 这个函数主要看key是否相同 不相同直接返回null 相同则继续判断type是否相同 不相同则创建新的fiber返回 把旧的fiber打上delete的tag 新的fiber打上place的tag type也相同则可以复用fiber返回
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
    );
    if (newFiber === null) {
    // TODO: This breaks on empty slots like null children. That's
    // unfortunate because it triggers the slow path all the time. We need
    // a better way to communicate whether this was a miss or null,
    // boolean, undefined, etc.
    if (oldFiber === null) {
    oldFiber = nextOldFiber;
    }
    break;
    }
    if (shouldTrackSideEffects) {
    if (oldFiber && newFiber.alternate === null) {
    // We matched the slot, but we didn't reuse the existing fiber, so we
    // need to delete the existing child.
    deleteChild(returnFiber, oldFiber);
    }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!给newfiber加上place的tag
    if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
    } else {
    // TODO: Defer siblings if we're not at the right index for this slot.
    // I.e. if we had null values before, then we want to defer this
    // for each null value. However, we also don't want to call updateSlot
    // with the previous one.
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    oldFiber = nextOldFiber;
    }
    //!新旧同时遍历完
    if (newIdx === newChildren.length) {
    // We've reached the end of the new children. We can delete the rest.
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
    }
    //!老的遍历完 新的没遍历完 遍历剩下的jsx的newchildren
    if (oldFiber === null) {
    // If we don't have any more existing children we can choose a fast path
    // since the rest will all be insertions.
    for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
    continue;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!直接把新的节点打上place 插入dom
    if (previousNewFiber === null) {
    // TODO: Move out of the loop. This only happens for the first run.
    resultingFirstChild = newFiber;
    } else {
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    }
    return resultingFirstChild;
    }
    //!老的没遍历完 新的也没遍历完 证明key不同跳出啦 要处理节点位置变化的情况 我们要找到key相同的复用 那么为了在o(1)时间内找到 我们用map(key:oldfiber.key->value:oldfiber)数据结构
    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    //!遍历剩下的newchldren
    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(//!找到newchildren的key对应的oldfiber 复用/新建fiber返回
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
    lanes,
    );
    if (newFiber !== null) {
    if (shouldTrackSideEffects) {
    if (newFiber.alternate !== null) {
    // The new fiber is a work in progress, but if there exists a
    // current, that means that we reused the fiber. We need to delete
    // it from the child list so that we don't add it to the deletion
    // list.
    existingChildren.delete(
    newFiber.key === null ? newIdx : newFiber.key,//!从map中去掉已经找到key的oldfiber
    );
    }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);//!新的fiber标记为插入 注意位置 (oldindex<lastplaceindex) 移动位置插入 因为老的fiber的index位置比新的页面位置小 肯定要移动插入了
    if (previousNewFiber === null) {
    resultingFirstChild = newFiber;
    } else {
    previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
    }
    }

    if (shouldTrackSideEffects) {
    // Any existing children that weren't consumed above were deleted. We need
    // to add them to the deletion list.
    existingChildren.forEach(child => deleteChild(returnFiber, child));//!删除多余的oldfiber 因为新的children已经遍历完
    }

    return resultingFirstChild;
    }

    原文:https://juejin.cn/post/6964653615256436750

    收起阅读 »

    使用react的7个避坑案例

    React是个很受欢迎的前端框架。今天我们探索下React开发者应该注意的七个点。 1. 组件臃肿 React开发者没有创建必要的足够多的组件化,其实这个问题不局限于React开发者,很多Vue开发者也是。 当然,我们现在讨论的是React 在React中...
    继续阅读 »

    React是个很受欢迎的前端框架。今天我们探索下React开发者应该注意的七个点。


    1. 组件臃肿


    React开发者没有创建必要的足够多的组件化,其实这个问题不局限于React开发者,很多Vue开发者也是。



    当然,我们现在讨论的是React



    React中,我们可以创建一个很多内容的组件,来执行我们的各种任务,但是最好是保证组件精简 -- 一个组件关联一个函数。这样不仅节约你的时间,而且能帮你很好地定位问题


    比如下面的TodoList组件:

    // ./components/TodoList.js

    import React from 'react';

    import { useTodoList } from '../hooks/useTodoList';
    import { useQuery } from '../hooks/useQuery';
    import TodoItem from './TodoItem';
    import NewTodo from './NewTodo';

    const TodoList = () => {
    const { getQuery, setQuery } = useQuery();
    const todos = useTodoList();
    return (
    <div>
    <ul>
    {todos.map(({ id, title, completed }) => (
    <TodoItem key={id} id={id} title={title} completed={completed} />
    ))}
    <NewTodo />
    </ul>
    <div>
    Highlight Query for incomplete items:
    <input value={getQuery()} onChange={e => setQuery(e.target.value)} />
    </div>
    </div>
    );
    };

    export default TodoList;

    2. 直接更改state

    React中,状态应该是不变的。如果你直接修改state,会导致难以修改的性能问题。

    比如下面例子:

    const modifyPetsList = (element, id) => {
    petsList[id].checked = element.target.checked;
    setPetsList(petList)
    }

    上面例子中,你想更改数组对象中checked键。但是你遇到一个问题:因为使用相同的引用更改了对象,React无法观察并触发重新渲染


    解决这个问题,我们应该使用setState()方法或者useState()钩子。


    我们使用useState()方法来重写之前的例子。

    const modifyPetsList = (element, id) => {
    const { checked } = element.target;
    setpetsList((pets) => {
    return pets.map((pet, index) => {
    if (id === index) {
    pet = { ...pet, checked };
    }
    return pet;
    });
    });
    };

    3. props该传数字类型的值却传了字符串,反之亦然

    这是个很小的错误,不应该出现。

    比如下面的例子:

    class Arrival extends React.Component {
    render() {
    return (
    <h1>
    Hi! You arrived {this.props.position === 1 ? "first!" : "last!"} .
    </h1>
    );
    }
    }

    这里===对字符串'1'是无效的。而解决这个问题,需要我们在传递props值的时候用{}包裹。

    修正如下:

    // ❌
    const element = <Arrival position='1' />;

    // ✅
    const element = <Arrival position={1} />;

    4. list组件中没使用key

    假设我们需要渲染下面的列表项:

    const lists = ['cat', 'dog', 'fish’];

    render() {
    return (
    <ul>
    {lists.map(listNo =>
    <li>{listNo}</li>)}
    </ul>
    );
    }

    当然,上面的代码可以运行。当列表比较庞杂并需要进行更改等操作的时候,就会带来渲染的问题。


    React跟踪文档对象模型(DOM)上的所有列表元素。没有记录可以告知React,列表发生了什么改动。


    解决这个问题,你需要添加keys在你的列表元素中keys赋予每个元素唯一标识,这有助于React确定已添加,删除,修改了哪些项目。


    如下:

    <ul>
    {lists.map(listNo =>
    <li key={listNo}>{listNo}</li>)}
    </ul>

    5. setState是异步操作

    很容易忘记React中的state是异步操作的。如果你在设置一个值之后就去访问它,那么你可能不能立马获取到该值。

    我们看看下面的例子:

    handlePetsUpdate = (petCount) => {
    this.setState({ petCount });
    this.props.callback(this.state.petCount); // Old value
    };

    你可以使用setState()的第二个参数,回调函数来处理。比如:

    handlePetsUpdate = (petCount) => {
    this.setState({ petCount }, () => {
    this.props.callback(this.state.petCount); // Updated value
    });
    };

    6. 频繁使用Redux

    在大型的React app中,很多开发者使用Redux来管理全局状态。

    虽然Redux很有用,但是没必要使用它来管理每个状态

    如果我们的应用程序没有需要交换信息的并行级组件的时候,那么就不需要在项目中添加额外的库。比如我们想更改组件中的表单按钮状态的时候,我们更多的是优先考虑state方法或者useState钩子。

    7. 组件没以大写字母开头命名

    在JSX中,以小写开头的组件会向下编译为HTML元素

    所以我们应该避免下面的写法:

    class demoComponentName extends React.Component {
    }

    这将导致一个错误:如果你想渲染React组件,则需要以大写字母开头。

    那么得采取下面这种写法:

    class DemoComponentName extends React.Component {
    }

    后话

    上面的内容提取自Top 10 mistakes to avoid when using React,采用了意译的方式,提取了7条比较实用的内容。

    原文:https://juejin.cn/post/6963032224316784654


    收起阅读 »

    90 行代码的webpack,你确定不学吗?

    在前端社区里,webpack 可以说是一个经久不衰的话题。其强大、灵活的功能曾极大地促进了前端工程化进程的发展,伴随了无数前端项目的起与落。其纷繁复杂的配置也曾让无数前端人望而却步,笑称需要一个新工种"webpack 配置工程师"。作为一个历史悠久,最常见、最...
    继续阅读 »
    在前端社区里,webpack 可以说是一个经久不衰的话题。其强大、灵活的功能曾极大地促进了前端工程化进程的发展,伴随了无数前端项目的起与落。其纷繁复杂的配置也曾让无数前端人望而却步,笑称需要一个新工种"webpack 配置工程师"。作为一个历史悠久,最常见、最经典的打包工具,webpack 极具讨论价值。理解 webpack,掌握 webpack,无论是在面试环节,还是在日常项目搭建、开发、优化环节,都能带来不少的收益。那么本文将从核心理念出发,带各位读者拨开 webpack 的外衣,看透其本质。

    究竟是啥


    其实这个问题在 webpack 官网的第一段就给出了明确的定义:



    At its core, webpack is a static module bundler for modern JavaScript applications. When webpack processes your application, it internally builds a dependency graph which maps every module your project needs and generates one or more bundles.



    其意为:



    webpack 的核心是用于现代 JavaScript 应用程序的静态模块打包器。 当 webpack 处理您的应用程序时,它会在内部构建一个依赖关系图,该图映射您项目所需的每个模块并生成一个或多个包



    要素察觉:静态模块打包器依赖关系图生成一个或多个包。虽然如今的前端项目中,webpack 扮演着重要的角色,囊括了诸多功能,但从其本质上来讲,其仍然是一个“模块打包器”,将开发者的 JavaScript 模块打包成一个或多个 JavaScript 文件。


    要干什么


    那么,为什么需要一个模块打包器呢?webpack 仓库早年的 README 也给出了答案:



    As developer you want to reuse existing code. As with node.js and web all file are already in the same language, but it is extra work to use your code with the node.js module system and the browser. The goal of webpack is to bundle CommonJs modules into javascript files which can be loaded by <script>-tags.



    可以看到,node.js 生态中积累了大量的 JavaScript 写的代码,却因为 node.js 端遵循的 CommonJS 模块化规范与浏览器端格格不入,导致代码无法得到复用,这是一个巨大的损失。于是 webpack 要做的就是将这些模块打包成可以在浏览器端使用 <script> 标签加载并运行的JavaScript 文件。


    或许这并不是唯一解释 webpack 存在的原因,但足以给我们很大的启发——把 CommonJS 规范的代码转换成可在浏览器运行的 JavaScript 代码


    怎么干的


    既然浏览器端没有 CommonJS 规范,那就实现一个好了。从 webpack 打包出的产物,我们能看出思路。


    新建三个文件观察其打包产物:


    src/index.js

    const printA = require('./a')
    printA()

    src/a.js

    const printB = require('./b')

    module.exports = function printA() {
    console.log('module a!')
    printB()
    }

    src/b.js

    module.exports = function printB() {
    console.log('module b!')
    }

    执行 npx webpack --mode development 打包产出 dist/main.js 文件


    上图中,使用了 webpack 打包 3 个简单的 js 文件 index.js/a.js/b.js, 其中 index.js 中依赖了 a.js, 而 a.js 中又依赖了 b.js, 形成一个完整依赖关系。


    那么,webpack 又是如何知道文件之间的依赖关系的呢,如何收集被依赖的文件保证不遗漏呢?我们依然能从官方文档找到答案:



    When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, just one - to be loaded by the browser.



    也就是说,webpack 会从配置的入口开始,递归的构建一个应用程序所需要的模块的依赖树。我们知道,CommonJS 规范里,依赖某一个文件时,只需要使用 require 关键字将其引入即可,那么只要我们遇到require关键字,就去解析这个依赖,而这个依赖中可能又使用了 require 关键字继续引用另一个依赖,于是,就可以递归的根据 require 关键字找到所有的被依赖的文件,从而完成依赖树的构建了。


    可以看到上图最终输出里,三个文件被以键值对的形式保存到 __webpack_modules__ 对象上, 对象的 key 为模块路径名,value 为一个被包装过的模块函数。函数拥有 module, module.exports, __webpack_require__ 三个参数。这使得每个模块都拥有使用 module.exports 导出本模块和使用 __webpack_require__ 引入其他模块的能力,同时保证了每个模块都处于一个隔离的函数作用域范围。


    为什么 webpack要修改require关键字和require的路径?我们知道requirenode环境自带的环境变量,可以直接使用,而在其他环境则没有这样一个变量,于是需要webpack提供这样的能力。只要提供了相似的能力,变量名叫 require还是 __webpack_require__其实无所谓。至于重写路径,当然是因为在node端系统会根据文件的路径加载,而在 webpack打包的文件中,使用原路径行不通,于是需要将路径重写为 __webpack_modules__ 的键,从而找到相应模块。


    而下面的 __webpack_require__函数与 __webpack_module_cache__ 对象则完成了模块加载的职责。使用 __webpack_require__ 函数加载完成的模块被缓存到 __webpack_module_cache__ 对象上,以便下次如果有其他模块依赖此模块时,不需要重新运行模块的包装函数,减少执行效率的消耗。同时,如果多个文件之间存在循环依赖,比如 a.js 依赖了 b.js 文件, b.js 又依赖了 a.js,那么在 b.js 使用 __webpack_require__加载 a.js 时,会直接走进 if(cachedModule !== undefined) 分支然后 return已缓存过的 a.js 的引用,不会进一步执行 a.js 文件加载,从而避免了循环依赖无限递归的出现


    不能说这个由 webpack 实现的模块加载器与 CommonJS 规范一毛一样,只能说八九不离十吧。这样一来,打包后的 JavaScript 文件可以被 <script> 标签加载且运行在浏览器端了。

    简易实现


    了解了 webpack 处理后的 JavaScript 长成什么样子,我们梳理一下思路,依葫芦画瓢手动实现一个简易的打包器,帮助理解。


    要做的事情有这么些:



    1. 读取入口文件,并收集依赖信息

    2. 递归地读取所有依赖模块,产出完整的依赖列表

    3. 将各模块内容打包成一块完整的可运行代码


    话不多说,创建一个项目,并安装所需依赖

    npm init -y
    npm i @babel/core @babel/parser @babel/traverse webpack webpack-cli -D

    其中:



    • @babel/parser 用于解析源代码,产出 AST

    • @babel/traverse 用于遍历 AST,找到 require 语句并修改成 _require_,将引入路径改造为相对根的路径

    • @babel/core 用于将修改后的 AST 转换成新的代码输出


    创建一个入口文件 myPack.js 并引入依赖

    const fs = require('fs')
    const path = require('path')
    const parser = require('@babel/parser')
    const traverse = require('@babel/traverse').default
    const babel = require('@babel/core')

    紧接着,我们需要对某一个模块进行解析,并产出其模块信息,包括:模块路径、模块依赖、模块转换后代码

    // 保存根路径,所有模块根据根路径产出相对路径
    let root = process.cwd()

    function readModuleInfo(filePath) {
    // 准备好相对路径作为 module 的 key
    filePath =
    './' + path.relative(root, path.resolve(filePath)).replace(/\\+/g, '/')
    // 读取源码
    const content = fs.readFileSync(filePath, 'utf-8')
    // 转换出 AST
    const ast = parser.parse(content)
    // 遍历模块 AST,将依赖收集到 deps 数组中
    const deps = []
    traverse(ast, {
    CallExpression: ({ node }) => {
    // 如果是 require 语句,则收集依赖
    if (node.callee.name === 'require') {
    // 改造 require 关键字
    node.callee.name = '_require_'
    let moduleName = node.arguments[0].value
    moduleName += path.extname(moduleName) ? '' : '.js'
    moduleName = path.join(path.dirname(filePath), moduleName)
    moduleName = './' + path.relative(root, moduleName).replace(/\\+/g, '/')
    deps.push(moduleName)
    // 改造依赖的路径
    node.arguments[0].value = moduleName
    }
    },
    })
    // 编译回代码
    const { code } = babel.transformFromAstSync(ast)
    return {
    filePath,
    deps,
    code,
    }
    }

    接下来,我们从入口出发递归地找到所有被依赖的模块,并构建成依赖树

    function buildDependencyGraph(entry) {
    // 获取入口模块信息
    const entryInfo = readModuleInfo(entry)
    // 项目依赖树
    const graphArr = []
    graphArr.push(entryInfo)
    // 从入口模块触发,递归地找每个模块的依赖,并将每个模块信息保存到 graphArr
    for (const module of graphArr) {
    module.deps.forEach((depPath) => {
    const moduleInfo = readModuleInfo(path.resolve(depPath))
    graphArr.push(moduleInfo)
    })
    }
    return graphArr
    }

    经过上面一步,我们已经得到依赖树能够描述整个应用的依赖情况,最后我们只需要按照目标格式进行打包输出即可

    function pack(graph, entry) {
    const moduleArr = graph.map((module) => {
    return (
    `"${module.filePath}": function(module, exports, _require_) {
    eval(\`` +
    module.code +
    `\`)
    }`
    )
    })
    const output = `;(() => {
    var modules = {
    ${moduleArr.join(',\n')}
    }
    var modules_cache = {}
    var _require_ = function(moduleId) {
    if (modules_cache[moduleId]) return modules_cache[moduleId].exports

    var module = modules_cache[moduleId] = {
    exports: {}
    }
    modules[moduleId](module, module.exports, _require_)
    return module.exports
    }

    _require_('${entry}')
    })()`
    return output
    }

    直接使用字符串模板拼接成类 CommonJS 规范的模板,自动加载入口模块,并使用 IIFE 将代码包装,保证代码模块不会影响到全局作用域。

    最后,编写一个入口函数 main 用以启动打包过程

    function main(entry = './src/index.js', output = './dist.js') {
    fs.writeFileSync(output, pack(buildDependencyGraph(entry), entry))
    }

    main()

    执行并验证结果

    node myPack.js

    至此,我们使用了总共不到 90 行代码(包含注释),完成了一个极简的模块打包工具。虽然没有涉及任何 Webpack 源码, 但我们从打包器的设计原理入手,走过了打包工具的核心步骤,简易却不失完整。

    总结


    本文从 webpack 的设计理念和最终实现出发,梳理了其作为一个打包工具的核心能力,并使用一个简易版本实现帮助更直观的理解其本质。总的来说,webpack 作为打包工具无非是从应用入口出发,递归的找到所有依赖模块,并将他们解析输出成一个具备类 CommonJS 模块化规范的模块加载能力的 JavaScript 文件


    因其优秀的设计,在实际生产环节中,webapck 还能扩展出诸多强大的功能。然而其本质仍是模块打包器。不论是什么样的新特性或新能力,只要我们把握住打包工具的核心思想,任何问题终将迎刃而解。



    链接:https://juejin.cn/post/6963820624623960101

    收起阅读 »

    使用vue+element开发一个谷歌插件

    简单功能:点击浏览器右上角插件icon弹出小弹窗,点击设置弹出设置页,并替换背景颜色。开始1.本地创建文件夹testPlugin并新建manifest.json文件{ "name": "testPlugin", "description": "这是...
    继续阅读 »

    简单功能:点击浏览器右上角插件icon弹出小弹窗,点击设置弹出设置页,并替换背景颜色。

    开始
    • 1.本地创建文件夹testPlugin并新建manifest.json文件
    {
    "name": "testPlugin",
    "description": "这是一个测试用例",
    "version": "0.0.1",
    "manifest_version": 2
    }
    • 2.添加插件的小icon
      testPlugin下创建icons文件夹,可以放入一些不同尺寸的icon,测试可以偷懒都放一种尺寸的icon。修改manifest.json为:
    {
    "name": "testPlugin",
    "description": "这是一个测试用例",
    "version": "0.0.1",
    "manifest_version": 2,
    "icons": {
    "16": "icons/16.png",
    "48": "icons/16.png"
    }
    }

    这时候在扩展程序中加载已解压的程序(就是我们创建的文件夹),就可以看到雏形了:


    • 3.选择性地添加点击插件icon浏览器右上角弹出来的框
      manifest.json添加:
    "browser_action": {
    "default_title": "test plugin",
    "default_icon": "icons/16.png",
    "default_popup": "index.html"
    }

    testPlugin创建index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>test plugin</title>
    </head>

    <body>
    <input id="name" placeholder="请输入"/>
    </body>
    </html>

    刷新插件,这时候点击浏览器中刚刚添加的插件的icon,就会弹出:


    • 4.js事件(样式同理)
      testPlugin创建js文件夹index.js文件:
    document.getElementById('button').onclick = function() {
    alert(document.getElementById('name').value)
    }

    html中:

    <input id="name" placeholder="请输入"/>
    <input id="button" type="button" value="点击"/>
    <script src="js/index.js"></script>

    刷新插件


    一个嵌入网页中的悬浮框
    上述例子是点击icon浏览器右上角出现的小弹窗,

    引入vue.js、element-ui
        下载合适版本的vue.js和element-ui等插件,同样按照index.js一样的操作引入,如果没有下载单独js文件的地址,可以打开cdn地址直接将压缩后的代码复制。
    manifest.json中添加:

    "content_scripts": [
    {
    "matches": [
    "<all_urls>"
    ],
    "css": [
    "css/index.css"
    ],
    "js": [
    "js/vue.js",
    "js/element.js",
    "js/index.js"
    ],
    "run_at": "document_idle"
    }
    ],

    在index.js文件:
    这里使用在head里插入link 的方式引入element-ui的css,减少插件包的一点大小,当然也可以像引入index.js那样在manifest.json中引入。
    直接在index.js文件中写Vue实例,不过首先得创建挂载实例的节点:

    let element = document.createElement('div')
    let attr = document.createAttribute('id')
    attr.value = 'appPlugin'
    element.setAttributeNode(attr)
    document.getElementsByTagName('body')[0].appendChild(element)

    let link = document.createElement('link')
    let linkAttr = document.createAttribute('rel')
    linkAttr.value = 'stylesheet'
    let linkHref = document.createAttribute('href')
    linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
    link.setAttributeNode(linkAttr)
    link.setAttributeNode(linkHref)
    document.getElementsByTagName('head')[0].appendChild(link)


    const vue = new Vue({
    el: '#appPlugin',
    template:`
    <div class="app-plugin-content">{{text}}{{icon_post_message}}<el-button @click="Button">Button</el-button></div>
    `,
    data: function () {
    return { text: 'hhhhhh', icon_post_message: '_icon_post_message', isOcContentPopShow: true }
    },
    mounted() {
    console.log(this.text)
    },
    methods: {
    Button() {
    this.isOcContentPopShow = false
    }
    }
    })
    让我们来写一个简易替换网页背景颜色工具

    index.js:

    let element = document.createElement('div')
    let attr = document.createAttribute('id')
    attr.value = 'appPlugin'
    element.setAttributeNode(attr)
    document.getElementsByTagName('body')[0].appendChild(element)

    let link = document.createElement('link')
    let linkAttr = document.createAttribute('rel')
    linkAttr.value = 'stylesheet'
    let linkHref = document.createAttribute('href')
    linkHref.value = 'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
    link.setAttributeNode(linkAttr)
    link.setAttributeNode(linkHref)
    document.getElementsByTagName('head')[0].appendChild(link)


    const vue = new Vue({
    el: '#appPlugin',
    template: `
    <div v-if="isOcContentPopShow" class="oc-move-page" id="oc_content_page">
    <div class="oc-content-title" id="oc_content_title">颜色 <el-button type="text" icon="el-icon-close" @click="close"></el-button></div>
    <div class="app-plugin-content">背景:<el-color-picker v-model="color1"></el-color-picker></div>
    <div class="app-plugin-content">字体:<el-color-picker v-model="color2"></el-color-picker></div>
    </div>
    `,
    data: function () {
    return { color1: null, color2: null, documentArr: [], textArr: [], isOcContentPopShow: true }
    },
    watch: {
    color1(val) {
    let out = document.getElementById('oc_content_page')
    let outC = document.getElementsByClassName('el-color-picker__panel')
    this.documentArr.forEach(item => {
    if(!out.contains(item) && !outC[0].contains(item) && !outC[1].contains(item)) {
    item.style.cssText = `background-color: ${val}!important;color: ${this.color2}!important;`
    }
    })
    },
    color2(val) {
    let out = document.getElementById('oc_content_page')
    let outC = document.getElementsByClassName('el-color-picker__panel')[1]
    this.textArr.forEach(item => {
    if(!out.contains(item) && !outC.contains(item)) {
    item.style.cssText = `color: ${val}!important;`
    }
    })
    }
    },
    mounted() {
    chrome.runtime.onConnect.addListener((res) => {
    if (res.name === 'testPlugin') {
    res.onMessage.addListener(mess => {
    this.isOcContentPopShow = mess.isShow
    })
    }
    })
    this.$nextTick(() => {
    let bodys = [...document.getElementsByTagName('body')]
    let headers = [...document.getElementsByTagName('header')]
    let divs = [...document.getElementsByTagName('div')]
    let lis = [...document.getElementsByTagName('li')]
    let articles = [...document.getElementsByTagName('article')]
    let asides = [...document.getElementsByTagName('aside')]
    let footers = [...document.getElementsByTagName('footer')]
    let navs = [...document.getElementsByTagName('nav')]
    this.documentArr = bodys.concat(headers, divs, lis, articles, asides, footers, navs)

    let as = [...document.getElementsByTagName('a')]
    let ps = [...document.getElementsByTagName('p')]
    this.textArr = as.concat(ps)

    })

    },
    methods: {
    close() {
    this.isOcContentPopShow = false
    }
    }
    })

    index.html:

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

    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>my plugin</title>
    <link rel="stylesheet" href="css/index.css">
    </head>

    <body>
    <div class="plugin">
    <input id="plugin_button" type="button" value="打开" />
    </div>
    </body>
    <script src="js/icon.js"></script>

    </html>

    新建icon.js:

    plugin_button.onclick = function () {
    mess()
    }
    async function mess () {
    const tabId = await getCurrentTabId()
    const connect = chrome.tabs.connect(tabId, {name: 'testPlugin'});
    connect.postMessage({isShow: true})
    }
    function getCurrentTabId() {
    return new Promise((resolve, reject) => {
    chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
    resolve(tabs.length ? tabs[0].id : null)
    });
    })
    }

    新建index.css:

    .oc-move-page{
    width: 100px;
    height: 200px;
    background: white;
    box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.12);
    border-radius: 8px;
    position: fixed;
    transform: translateY(-50%);
    right: 0;
    top: 50%;
    z-index: 1000001;
    }
    .oc-move-page .oc-content-title{
    text-align: left;
    padding: 12px 16px;
    font-weight: 600;
    font-size: 18px;
    border-bottom: 1px solid #DEE0E3;
    }
    .oc-move-page .app-plugin-content {
    display: flex;
    align-items: center;
    margin-top: 10px;
    }

    .el-color-picker__panel {
    right: 100px!important;
    left: auto!important;
    }


    这样一个小尝试就完成了,当然如果有更多需求可以结合本地存储或者服务端来协作。

    本文链接:https://blog.csdn.net/qq_26769677/article/details/116611072



    收起阅读 »

    手把手教你利用js给图片打马赛克

    效果演示Canvas简介这个 HTML 元素是为了客户端矢量图形而设计的。它自己没有行为,但却把一个绘图 API 展现给客户端 JavaScript 以使脚本能够把想绘制的东西都绘制到一块画布上。HTML5 标签用于绘制图像(通过脚本,通常是 JavaScri...
    继续阅读 »

    效果演示


    Canvas简介

    这个 HTML 元素是为了客户端矢量图形而设计的。它自己没有行为,但却把一个绘图 API 展现给客户端 JavaScript 以使脚本能够把想绘制的东西都绘制到一块画布上。

    HTML5 标签用于绘制图像(通过脚本,通常是 JavaScript)

    不过, 元素本身并没有绘制能力(它仅仅是图形的容器) - 您必须使用脚本来完成实际的绘图任务

    getContext() 方法可返回一个对象,该对象提供了用于在画布上绘图的方法和属性

    本手册提供完整的 getContext(“2d”) 对象属性和方法,可用于在画布上绘制文本、线条、矩形、圆形等等

    标记和 SVG 以及 VML 之间的差异:

    标记和 SVG 以及 VML 之间的一个重要的不同是, 有一个基于 JavaScript 的绘图 API,而 SVG 和 VML 使用一个 XML 文档来描述绘图。

    这两种方式在功能上是等同的,任何一种都可以用另一种来模拟。从表面上看,它们很不相同,可是,每一种都有强项和弱点。例如,SVG 绘图很容易编辑,只要从其描述中移除元素就行。

    要从同一图形的一个 标记中移除元素,往往需要擦掉绘图重新绘制它

    知识点简介

    • 利用js创建图片
    let img = new Image()
    //可以给图片一个链接
    img.src = 'https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=826495019,1749283937&fm=26&gp=0.jpg'
    //或者本地已有图片的路径
    //img.src = './download.jpg'

    //添加到HTML中
    document.body.appendChild(img)
    • canvas.getContext(“2d”)

    语法:
    参数 contextID 指定了您想要在画布上绘制的类型。当前唯一的合法值是 “2d”,它指定了二维绘图,并且导致这个方法返回一个环境对象,该对象导出一个二维绘图 API

    let ctx = Canvas.getContext(contextID)
    • ctx.drawImage()
    JavaScript 语法 1:
    在画布上定位图像:
    context.drawImage(img,x,y);
    JavaScript 语法 2:
    在画布上定位图像,并规定图像的宽度和高度:
    context.drawImage(img,x,y,width,height);
    JavaScript 语法 3:
    剪切图像,并在画布上定位被剪切的部分:
    context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
    • ctx.getImageData()
    JavaScript 语法
    getImageData() 方法返回 ImageData 对象,该对象拷贝了画布指定矩形的像素数据。
    对于 ImageData 对象中的每个像素,都存在着四方面的信息,即 RGBA 值:
    R - 红色 (0-255)
    G - 绿色 (0-255)
    B - 蓝色 (0-255)
    A - alpha 通道 (0-255; 0 是透明的,255 是完全可见的)
    color/alpha 以数组形式存在,并存储于 ImageData 对象的 data 属性中
    var imgData=context.getImageData(x,y,width,height);
    • ctx.putImageData()
    putImageData() 方法将图像数据(从指定的 ImageData 对象)放回画布上。

    那我们开始搞起来吧

    step-by-step

    准备好我们的图片,并添加上我们的方法

    <body>
    <img src="./download.jpg">
    <button onclick="addCanvas()">生成Canvas</button>
    <button onclick="generateImg()">生成图片</button>
    </body>


    接下来写addCanvas方法

    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image(); //1.准备赋值复制一份图片
    img.src = './download.jpg';
    img.onload = function() { //2.待图片加载完成
    let width = this.width
    let height = this.height

    let canvas = document.createElement('canvas') //3.创建画布
    let ctx = canvas.getContext("2d"); //4.获得该画布的内容
    canvas.setAttribute('width', width) //5.为了统一,设置画布的宽高为图片的宽高
    canvas.setAttribute('height', height)

    ctx.drawImage(this, 0, 0, width, height); //5.在画布上绘制该图片

    document.body.insertBefore(canvas, bt) //5.把canvas插入到按钮前面

    }
    }



    嗯,我们已经成功走出了成功的一小步,接下来是干什么呢?…嗯,我们需要利用原生的onmouseup和onmousedown事件,代表我们按下鼠标这个过程,那么这两个事件添加到哪呢?

    没错,既然我们要在canvas上进行马赛克操作,那我们必然要给canvas元素添加这两个事件

    考虑到我们创建canvas的过程复杂了一点,我们做一个模块封装吧!

    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image();
    img.src = './download.jpg'; //这里放自己的图片
    img.onload = function() {
    let width = this.width
    let height = this.height

    let {
    canvas,
    ctx
    } = createCanvasAndCtx(width, height) //对象解构接收canvas和ctx

    ctx.drawImage(this, 0, 0, width, height);

    document.body.insertBefore(canvas, bt)

    }
    }

    function createCanvasAndCtx(width, height) {
    let canvas = document.createElement('canvas')
    canvas.setAttribute('width', width)
    canvas.setAttribute('height', height)
    canvas.setAttribute('onmouseout', 'end()') //修补鼠标不在canvas上离开的补丁
    canvas.setAttribute('onmousedown', 'start()') //添加鼠标按下
    canvas.setAttribute('onmouseup', 'end()') //添加鼠标弹起
    let ctx = canvas.getContext("2d");
    return {
    canvas,
    ctx
    }
    }

    function start() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = () => {
    console.log('你按下了并移动了鼠标')
    }
    }

    function end() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = null
    }


    嗯,目前来看,我们的代码依然如我们所愿的正常工作

    接下来的挑战更加严峻,我们需要去获取像素和处理像素,让我们再重写start()函数

    function start() {
    let img = document.querySelector('img')
    let canvas = document.querySelector('canvas')
    let ctx = canvas.getContext("2d");
    imgData = ctx.getImageData(0, 0, img.clientWidth, img.clientHeight);
    canvas.onmousemove = (e) => {
    let w = imgData.width; //1.获取图片宽高
    let h = imgData.height;

    //马赛克的程度,数字越大越模糊
    let num = 10;

    //获取鼠标当前所在的像素RGBA
    let color = getXY(imgData, e.offsetX, e.offsetY);

    for (let k = 0; k < num; k++) {
    for (let l = 0; l < num; l++) {
    //设置imgData上坐标为(e.offsetX + l, e.offsetY + k)的的颜色
    setXY(imgData, e.offsetX + l, e.offsetY + k, color);
    }
    }
    //更新canvas数据
    ctx.putImageData(imgData, 0, 0);
    }
    }

    //这里为你提供了setXY和getXY两个函数,如果你有兴趣,可以去研究获取的原理
    function setXY(obj, x, y, color) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    obj.data[4 * (y * w + x)] = color[0];
    obj.data[4 * (y * w + x) + 1] = color[1];
    obj.data[4 * (y * w + x) + 2] = color[2];
    obj.data[4 * (y * w + x) + 3] = color[3];
    }

    function getXY(obj, x, y) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    var color = [];
    color[0] = obj.data[4 * (y * w + x)];
    color[1] = obj.data[4 * (y * w + x) + 1];
    color[2] = obj.data[4 * (y * w + x) + 2];
    color[3] = obj.data[4 * (y * w + x) + 3];
    return color;
    }

    嗯,我们离成功不远拉,最后一步就是生成图片

    好在canavs给我们提供了直接的方法,可以直接将画布导出为Base64编码的图片:

    function generateImg() {
    let canvas = document.querySelector('canvas')
    var newImg = new Image();
    newImg.src = canvas.toDataURL("image/png");
    document.body.insertBefore(newImg, canvas)
    }

    是不是无比轻松呢~,来看看你手写的代码是否和下面一样叭:

    完整代码

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

    <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    </head>

    <body>

    <body>
    <img src="./download.jpg">
    <button onclick="addCanvas()">生成Canvas</button>
    <button onclick="generateImg()">生成图片</button>
    </body>
    <script>
    function addCanvas() {
    let bt = document.querySelector('button')

    let img = new Image();
    img.src = './download.jpg'; //这里放自己的图片
    img.onload = function() {
    let width = this.width
    let height = this.height

    let {
    canvas,
    ctx
    } = createCanvasAndCtx(width, height)

    ctx.drawImage(this, 0, 0, width, height);

    document.body.insertBefore(canvas, bt)

    }
    }

    function createCanvasAndCtx(width, height) {
    let canvas = document.createElement('canvas')
    canvas.setAttribute('width', width)
    canvas.setAttribute('height', height)
    canvas.setAttribute('onmouseout', 'end()')
    canvas.setAttribute('onmousedown', 'start()')
    canvas.setAttribute('onmouseup', 'end()')
    let ctx = canvas.getContext("2d");
    return {
    canvas,
    ctx
    }
    }

    function start() {
    let img = document.querySelector('img')
    let canvas = document.querySelector('canvas')
    let ctx = canvas.getContext("2d");
    imgData = ctx.getImageData(0, 0, img.clientWidth, img.clientHeight);
    canvas.onmousemove = (e) => {
    let w = imgData.width; //1.获取图片宽高
    let h = imgData.height;

    //马赛克的程度,数字越大越模糊
    let num = 10;

    //获取鼠标当前所在的像素RGBA
    let color = getXY(imgData, e.offsetX, e.offsetY);

    for (let k = 0; k < num; k++) {
    for (let l = 0; l < num; l++) {
    //设置imgData上坐标为(e.offsetX + l, e.offsetY + k)的的颜色
    setXY(imgData, e.offsetX + l, e.offsetY + k, color);
    }
    }
    //更新canvas数据
    ctx.putImageData(imgData, 0, 0);
    }
    }

    function generateImg() {
    let canvas = document.querySelector('canvas')
    var newImg = new Image();
    newImg.src = canvas.toDataURL("image/png");
    document.body.insertBefore(newImg, canvas)
    }

    function setXY(obj, x, y, color) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    obj.data[4 * (y * w + x)] = color[0];
    obj.data[4 * (y * w + x) + 1] = color[1];
    obj.data[4 * (y * w + x) + 2] = color[2];
    obj.data[4 * (y * w + x) + 3] = color[3];
    }

    function getXY(obj, x, y) {
    var w = obj.width;
    var h = obj.height;
    var d = obj.data;
    var color = [];
    color[0] = obj.data[4 * (y * w + x)];
    color[1] = obj.data[4 * (y * w + x) + 1];
    color[2] = obj.data[4 * (y * w + x) + 2];
    color[3] = obj.data[4 * (y * w + x) + 3];
    return color;
    }

    function end() {
    let canvas = document.querySelector('canvas')
    canvas.onmousemove = null
    }
    </script>
    </body>

    </html>

    本文链接:https://blog.csdn.net/JKR10000/article/details/116803023

    收起阅读 »

    微信H5网页跳转小程序,这一篇就够了!

    鉴于微信 开放标签说明文档 写的不是很清楚,大多数开发者看了以后表示:我从哪里来?要到哪里去?所以鄙人记录下这篇文章,以便帮助到一些人。静态网页跳转小程序废话不多说,上才艺!<html><head> <meta charse...
    继续阅读 »

    鉴于微信 开放标签说明文档 写的不是很清楚,大多数开发者看了以后表示:我从哪里来?要到哪里去?

    所以鄙人记录下这篇文章,以便帮助到一些人。

    静态网页跳转小程序

    废话不多说,上才艺!

    <html>
    <head>
    <meta charset="utf-8">
    <meta name = "viewport" content = "width = device-width, initial-scale = 1.0, maximum-scale = 1.0, user-scalable = 0" />
    <title>小程序跳转测试</title>
    </head>
    <body style="text-aligin:center;">
    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html"> <!-- 这里填写跳转对于小程序的页面 注意这里的 .html -->
    <template>
    <style>.btn { padding: 12px width:200px;height:50px;}</style>
    <button class="btn">打开小程序</button>
    </template>
    </wx-open-launch-weapp>

    <script src="/js/jquery-1.12.4.js"></script>
    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> <!-- 至少必须是1.6版本 -->

    <script>

    $(function () {

    //=== 这里仅仅是获取 config 的参数以及签名=== start
    var url = location.href;
    var functions = "updateAppMessageShareData";
    $.get("https://xxx.com/wechat/jssdk/config", {"functions":functions}, function(response){
    if(response.status == 0) {
    var info = response.data;
    wx.config({
    debug: false,
    appId: info.appId,
    timestamp: info.timestamp,
    nonceStr: info.nonceStr,
    signature: info.signature,
    jsApiList: info.jsApiList,
    openTagList: ['wx-open-launch-weapp']//这里直接添加,什么都不用管
    });
    }
    });
    //=== 获取 config 的参数以及签名=== end

    var btn = document.getElementById('launch-btn');
    btn.addEventListener('launch', function (e) {
    console.log('success');
    });
    btn.addEventListener('error', function (e) {
    console.log('fail', e.detail);
    });
    });
    </script>
    </body>
    </html>

    开放对象:

    1、已认证的服务号,服务号绑定“JS接口安全域名”下的网页可使用此标签跳转任意合法合规的小程序。

    2、已认证的非个人主体的小程序,使用小程序云开发的静态网站托管绑定的域名下的网页,可以使用此标签跳转任意合法合规的小程序。

    客户端要求

    微信版本要求为:7.0.12及以上。 系统版本要求为:iOS 10.3及以上、Android 5.0及以上。

    注意:微信开发者工具暂时不支持!所以建议直接使用手机访问进行测试。

    其他说明

    这个功能其实很简单,并没有想象中那么复杂。 实质是在你能够做到自定义分享到朋友圈或朋友的基础上,config多了

    openTagList: ['wx-open-launch-weapp']

    再者需要注意的是,path的页面url 必须带有 .html 带参数的话则参数跟在html的后面。

    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html">

    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_e16de8f****" <!-- 这里填写小程序的原始ID -->
    path="/pages/index/index.html?id=123">

    VUE项目H5跳转

    1、先请求接口配置微信需要的一些参数

    // 需要先请求后端接口 
    let url = window.location.href.split("#")[0];
    let shareConfig = await shareViewAPI.getWechatConfig({url});
    let _this = this;
    // 将接口返回的信息配置
    wx.config({
    debug: false,
    appId: _this.app_id, // 必填,公众号的唯一标识
    timestamp: shareConfig.timestamp, // 必填,生成签名的时间戳
    nonceStr: shareConfig.nonceStr, // 必填,生成签名的随机串
    signature: shareConfig.signature, // 必填,签名
    jsApiList: ["onMenuShareAppMessage"], // 必填,如果只是为了跳转小程序,随便填个值都行
    openTagList: ["wx-open-launch-weapp"] // 跳转小程序时必填
    });

    配置的方法需要放到created、mounted或者beforeRouteEnter里

    2、在页面中添加wx-open-launch-weapp标签

    <!-- 关于username 与 path的值 参考官方文档  -->
    <wx-open-launch-weapp
    id="launch-btn"
    username="gh_***"
    path="/pages/index/index.html"
    @error="handleErrorFn"
    @launch="handleLaunchFn"
    >
    <!-- vue中需要使用script标签代替template插槽 html中使用template -->
    <script type="text/wxtag-template">
    <p class="store-tool_tip">点击进入选基工具</p>
    </script>
    </wx-open-launch-weapp>
    methods: {
    handleLaunchFn(e) {
    console.log("success", e);
    },
    handleErrorFn(e) {
    console.log("fail", e.detail);
    }
    }

    3、好啦

    备注:
    使用该标签的时候可能会报错,在main.js文件中添加上该行代码即可

    // 忽略打开微信小程序的组件
    Vue.config.ignoredElements = ['wx-open-launch-weapp']


    收起阅读 »

    在vue项目中使用骨架屏

    vue
    现在的应用开发,基本上都是前后端分离的,前端主流框架有SPA、MPA等,那么解决页面渲染、白屏时间成为首要关注的点webpack可以按需加载,减小首屏需要加载代码的体积;使用CDN技术、静态代码等缓存技术,可以减小加载渲染的时长问题:但是首页依然存在加载、渲染...
    继续阅读 »

    现在的应用开发,基本上都是前后端分离的,前端主流框架有SPA、MPA等,那么解决页面渲染、白屏时间成为首要关注的点

    webpack可以按需加载,减小首屏需要加载代码的体积;

    使用CDN技术、静态代码等缓存技术,可以减小加载渲染的时长

    问题:但是首页依然存在加载、渲染等待时长的问题。那么如何从视觉效果上减小首屏白屏的时间呢?

    骨架屏:举个例子:其实就是在模版文件中id=app容器下面写想要展示的效果,在new Vue(option)之后,该id下的内容就被替换了( 这时候,可能Vue编译生成的内容还没有挂载。因为new Vue的时候会进行一系列的初始化,这也需要耗费时间的)。这样就可以从视觉上减小白屏的时间

    骨架屏的实现方式

    1、直接在模版文件id=app容器下面,写进想要展示的效果html

    2、直接在模板文件id=app容器下面,用图片展示

    3、使用vue ssr提供的webpack插件

    4、自动生成并且自动插入静态骨架屏

    方式1和方式2存在的缺陷:针对不同入口,展示的效果都一样,导致不能灵活的针对不同的入口,展示不同的样式

    方式3可以针对不同的入口展示不同的效果。(实质也是先通过ssr生成一个json文件,然后将json文件内容注入到模板文件的id=app容器下)

    方案一、直接在模版文件id=app容器下面,写进想要展示的效果html

    在根目录的模版文件内写进内容,如红色圈出来的地方


    在浏览器打开项目

    在调用new Vue之前的展示效果(只是做了个简单效果,不喜勿喷):


    可以看到elements中id=app的容器下内容,就是我们写进的骨架屏效果内容


    在看下调了new Vue之后的效果,id=app容器下的内容被vue编译生成的内容替换了



    方案二、直接在模板文件id=app容器下面,用图片展示(这个就不做展示了)

    方案三、使用vue ssr提供的webpack插件:即用.vue文件完成骨架屏

    在方案一的基础上,将骨架屏的代码抽离出来,不在模版文件里面书写代码,而是在vue文件里面书写效果代码,这样便于维护

    1、在根目录下建一个skeleton文件夹,在该目录下创建文件App.vue文件(根组件,类似Vue项目的App.vue)、home.skeleton.vue(首页骨架屏展示效果的代码,类似Vue项目写的路由页面)、skeleton-entry.js(入口文件类似Vue项目的入口文件)、plugin/server-plugin.js(vue-server-renderer包提供了server-plugin插件,从里面将代码拷贝出来)

    home.skeleton.vue(首页骨架屏展示效果的代码)

    <template>
    <div class="skeleton-home">
    <div>加载中...</div>
    </div>
    </template>

    <style>
    .skeleton-home {
    width: 100vw;
    height: 100vh;
    background-color: #eaeaea;
    }
    </style>

    App.vue(根组件)

    <template>
    <div id="app">
    <!-- 根组件 -->
    <home style="display:none" id="homeSkeleton"></home>
    </div>
    </template>
    <script>
    import home from './home.skeleton.vue'
    export default{
    components: {
    home
    }
    }
    </script>
    <style>
    #app {
    font-family: 'Avenir', Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    }
    *{
    padding: 0;
    margin: 0;
    }
    </style>

    skeleton-entry.js(入口文件)

    // 入口文件
    import Vue from 'vue'
    import App from './App.vue'
    let skeleton = new Vue({
    render(h) {
    return h(App)
    }
    })
    export default skeleton

    plugin/server-plugin.js(vue-server-renderer包提供了server-plugin插件)

    'use strict';

    /* */

    var isJS = function (file) { return /\.js(\?[^.]+)?$/.test(file); };

    var ref = require('chalk');
    var red = ref.red;
    var yellow = ref.yellow;

    var prefix = "[vue-server-renderer-webpack-plugin]";
    var warn = exports.warn = function (msg) { return console.error(red((prefix + " " + msg + "\n"))); };
    var tip = exports.tip = function (msg) { return console.log(yellow((prefix + " " + msg + "\n"))); };

    var validate = function (compiler) {
    if (compiler.options.target !== 'node') {
    warn('webpack config `target` should be "node".');
    }

    if (compiler.options.output && compiler.options.output.libraryTarget !== 'commonjs2') {
    warn('webpack config `output.libraryTarget` should be "commonjs2".');
    }

    if (!compiler.options.externals) {
    tip(
    'It is recommended to externalize dependencies in the server build for ' +
    'better build performance.'
    );
    }
    };

    var VueSSRServerPlugin = function VueSSRServerPlugin (options) {
    if ( options === void 0 ) options = {};

    this.options = Object.assign({
    filename: 'vue-ssr-server-bundle.json'
    }, options);
    };

    VueSSRServerPlugin.prototype.apply = function apply (compiler) {
    var this$1 = this;

    validate(compiler);

    compiler.plugin('emit', function (compilation, cb) {
    var stats = compilation.getStats().toJson();
    var entryName = Object.keys(stats.entrypoints)[0];
    var entryAssets = stats.entrypoints[entryName].assets.filter(isJS);

    if (entryAssets.length > 1) {
    throw new Error(
    "Server-side bundle should have one single entry file. " +
    "Avoid using CommonsChunkPlugin in the server config."
    )
    }

    var entry = entryAssets[0];
    if (!entry || typeof entry !== 'string') {
    throw new Error(
    ("Entry \"" + entryName + "\" not found. Did you specify the correct entry option?")
    )
    }

    var bundle = {
    entry: entry,
    files: {},
    maps: {}
    };

    stats.assets.forEach(function (asset) {
    if (asset.name.match(/\.js$/)) {
    bundle.files[asset.name] = compilation.assets[asset.name].source();
    } else if (asset.name.match(/\.js\.map$/)) {
    bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());
    }
    // do not emit anything else for server
    delete compilation.assets[asset.name];
    });

    var json = JSON.stringify(bundle, null, 2);
    var filename = this$1.options.filename;

    compilation.assets[filename] = {
    source: function () { return json; },
    size: function () { return json.length; }
    };

    cb();
    });
    };

    module.exports = VueSSRServerPlugin;

    2、新建一个骨架屏构建配置文件:build/webpack.skeleton.conf.js,这个文件配合vue-server-renderer插件,将App.vue内容构建成单个json格式的文件

    'use strict'

    const path = require('path')
    const nodeExternals = require('webpack-node-externals')
    const VueSSRServerPlugin = require('../skeleton/plugin/server-plugin')

    module.exports = {
    // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
    // 并且还会在编译 Vue 组件时,
    // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
    target: 'node',

    // 对 bundle renderer 提供 source map 支持
    devtool: 'source-map',

    // 将 entry 指向应用程序的 server entry 文件
    entry: path.resolve(__dirname, '../skeleton/skeleton-entry.js'),

    output: {
    path: path.resolve(__dirname, '../skeleton'), // 生成的文件的目录
    publicPath: '/skeleton/',
    filename: '[name].js',
    libraryTarget: 'commonjs2' // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
    },

    module: {
    rules: [
    {
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
    compilerOptions: {
    preserveWhitespace: false
    }
    }
    },
    {
    test: /\.css$/,
    use: ['vue-style-loader', 'css-loader']
    }
    ]
    },

    performance: {
    hints: false
    },

    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,
    // 并生成较小的 bundle 文件。
    externals: nodeExternals({
    // 不要外置化 webpack 需要处理的依赖模块。
    // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    allowlist: /\.css$/
    }),

    // 这是将服务器的整个输出
    // 构建为单个 JSON 文件的插件。
    // 不配置filename,则默认文件名为 `vue-ssr-server-bundle.json`
    plugins: [
    new VueSSRServerPlugin({
    filename: 'skeleton.json'
    })
    ]
    }

    3、使用webpack-cli运行文件webpack.skeleton.conf.js,生成skeleton.json文件,放置在文件夹skeleton下

    在package.json文件里面书写运行命令:create-skeleton

    "scripts": {
    "create-skeleton": "webpack --progress --config build/webpack.skeleton.conf.js",
    "fill-skeleton": "node ./skeleton/skeleton.js"
    }

    在控制台上运行命令:

    npm run create-skeleton

    文件夹skeleton下就会多出skelleton.json文件


    4、将生成的skeleton.json内容注入到根目录下的index.html(模版文件)

    1)在文件夹skeleton下新建skeleton.js

    // 将生成的skeleton.json的内容填充到模板文件中
    const fs = require('fs')
    const { resolve } = require('path')
    const createBundleRenderer = require('vue-server-renderer').createBundleRenderer

    // 读取skeleton.json,以skeleton/index.html为模版写入内容
    const renderer = createBundleRenderer(resolve(__dirname, '../skeleton/skeleton.json'), {
    template: fs.readFileSync(resolve(__dirname, '../skeleton/index.html'), 'utf-8')
    })
    // 把上一步模版完成的内容写入根目录下的模版文件'index.html'
    renderer.renderToString({}, (err, html) => {
    if (err) {
    return console.log(err)
    }
    console.log('render complete!')
    fs.writeFileSync('index.html', html, 'utf-8')
    })

    2)添加运行命令:fill-skeleton

    "fill-skeleton": "node ./skeleton/skeleton.js"

    3)在控制台上运行该命令,则skeleton.json文件内容被填充至根目录下的模板文件index.html了

    本文链接:https://blog.csdn.net/tangxiujiang/article/details/116832585

    收起阅读 »

    520和女朋友搞点不一样的礼物, html+css+js做一个网页版坦克大战游戏

    坦克游戏玩法及介绍我们先来看一下首页。打开这个首页很简单,基本是上面这个样子,然后选择两个人回车就可以进行玩耍了,这个游戏需要两个人一起操作,玩家1(我): 使用WASD四个键进行上左下右方向的控制,通过space键进行设计射击。玩家2(女朋友):通过方向键上...
    继续阅读 »

    坦克游戏玩法及介绍

    我们先来看一下首页。


    打开这个首页很简单,基本是上面这个样子,然后选择两个人回车就可以进行玩耍了,这个游戏需要两个人一起操作,玩家1(我): 使用WASD四个键进行上左下右方向的控制,通过space键进行设计射击。玩家2(女朋友):通过方向键上下左右控制方向,通过enter键盘射击。基本上我控制整个电脑键盘的左边,她控制电脑键盘的右边。通过N键进行下一关,P键选择上一关。再键盘上显示如下。

    演示如何进入游戏


    通过方向键的下键选择两个人,然后点击回车进入游戏。也可以选择一个人进行回车进行战斗。

    一个人战斗的状态。


    两个人战斗的状态。


    比如图中红色标记的砖头是打不破的,只能绕道走,还有只能再yellow标记的区域内操作,其它砖头用子弹就可以打破,不能让对手先打破你的大本营(我右边中间的老鹰),不然又得GG.

    整个游戏规则大体是这样,下面看一下代码。

    项目结构


    整个项目由五部分组成,分为背景音乐、基础样式、动图、核心JS及首页静态展示。不涉及后端,纯前端实现。

    index.html

    <!DOCTYPE html>
    <html lang="zh" class="no-js demo-1">
    <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="js/jquery.min.js"></script>
    <script src="js/Helper.js"></script>
    <script src="js/keyboard.js"></script>
    <script src="js/const.js"></script>
    <script src="js/level.js"></script>
    <script src="js/crackAnimation.js"></script>
    <script src="js/prop.js"></script>
    <script src="js/bullet.js"></script>
    <script src="js/tank.js"></script>
    <script src="js/num.js"></script>
    <script src="js/menu.js"></script>
    <script src="js/map.js"></script>
    <script src="js/Collision.js"></script>
    <script src="js/stage.js"></script>
    <script src="js/main.js"></script>
    <link rel="stylesheet" type="text/css" href="css/default.css" />
    <style type="text/css">
    #canvasDiv canvas{
    position:absolute;
    }
    </style>
    </head>
    <body>
    <div class="container">
    <head><h3>操作说明:玩家1:WASD上左下右,space射击;玩家2:方向键,enter射击。n下一关,p上一关。</h3></head>
    <div class="main clearfix">
    <div id="canvasDiv" >
    <canvas id="wallCanvas" ></canvas>
    <canvas id="tankCanvas" ></canvas>
    <canvas id="grassCanvas" ></canvas>
    <canvas id="overCanvas" ></canvas>
    <canvas id="stageCanvas" ></canvas>
    </div>
    </div>

    </div><!-- /container -->
    <div style="text-align:center;">
    <p>来源:<a href="https://sunmenglei.blog.csdn.net/" target="_blank">孙叫兽的博客</a></p>
    </div>

    </body>
    </html>

    css

    *, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; }
    body, html { font-size: 100%; padding: 0; margin: 0; height: 100%;}

    /* Clearfix hack by Nicolas Gallagher: http://nicolasgallagher.com/micro-clearfix-hack/ */
    .clearfix:before, .clearfix:after { content: " "; display: table; }
    .clearfix:after { clear: both; }

    body {
    font-family: "Helvetica Neue",Helvetica,Arial,'Microsoft YaHei',sans-serif,'Lato', Calibri;
    color: #777;
    background: #f6f6f6;
    }

    a {
    color: #555;
    text-decoration: none;
    outline: none;
    }

    a:hover,
    a:active {
    color: #777;
    }

    a img {
    border: none;
    }
    /* Header Style */
    .main,
    .container > header {
    margin: 0 auto;
    /*padding: 2em;*/
    }

    .container {
    height: 100%;
    }

    .container > header {
    padding-top: 20px;
    padding-bottom: 20px;
    text-align: center;
    background: rgba(0,0,0,0.01);
    }

    .container > header h1 {
    font-size: 2.625em;
    line-height: 1.3;
    margin: 0;
    font-weight: 300;
    }

    .container > header span {
    display: block;
    font-size: 60%;
    opacity: 0.3;
    padding: 0 0 0.6em 0.1em;
    }

    /* Main Content */
    .main {
    /*max-width: 69em;*/
    width: 100%;
    height: 100%;
    overflow: hidden;
    }
    .demo-scroll{
    overflow-y: scroll;
    width: 100%;
    height: 100%;
    }
    .column {
    float: left;
    width: 50%;
    padding: 0 2em;
    min-height: 300px;
    position: relative;
    }

    .column:nth-child(2) {
    box-shadow: -1px 0 0 rgba(0,0,0,0.1);
    }

    .column p {
    font-weight: 300;
    font-size: 2em;
    padding: 0;
    margin: 0;
    text-align: right;
    line-height: 1.5;
    }

    /* To Navigation Style */
    .htmleaf-top {
    background: #fff;
    background: rgba(255, 255, 255, 0.6);
    text-transform: uppercase;
    width: 100%;
    font-size: 0.69em;
    line-height: 2.2;
    }

    .htmleaf-top a {
    padding: 0 1em;
    letter-spacing: 0.1em;
    color: #888;
    display: inline-block;
    }

    .htmleaf-top a:hover {
    background: rgba(255,255,255,0.95);
    color: #333;
    }

    .htmleaf-top span.right {
    float: right;
    }

    .htmleaf-top span.right a {
    float: left;
    display: block;
    }

    .htmleaf-icon:before {
    font-family: 'codropsicons';
    margin: 0 4px;
    speak: none;
    font-style: normal;
    font-weight: normal;
    font-variant: normal;
    text-transform: none;
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    }



    /* Demo Buttons Style */
    .htmleaf-demos {
    padding-top: 1em;
    font-size: 0.9em;
    }

    .htmleaf-demos a {
    display: inline-block;
    margin: 0.2em;
    padding: 0.45em 1em;
    background: #999;
    color: #fff;
    font-weight: 700;
    border-radius: 2px;
    }

    .htmleaf-demos a:hover,
    .htmleaf-demos a.current-demo,
    .htmleaf-demos a.current-demo:hover {
    opacity: 0.6;
    }

    .htmleaf-nav {
    text-align: center;
    }

    .htmleaf-nav a {
    display: inline-block;
    margin: 20px auto;
    padding: 0.3em;
    }
    .bb-custom-wrapper {
    width: 420px;
    position: relative;
    margin: 0 auto 40px;
    text-align: center;
    }
    /* Demo Styles */

    .demo-1 body {
    color: #87968e;
    background: #fff2e3;
    }

    .demo-1 a {
    color: #72b890;
    }

    .demo-1 .htmleaf-demos a {
    background: #72b890;
    color: #fff;
    }

    .demo-2 body {
    color: #fff;
    background: #c05d8e;
    }

    .demo-2 a {
    color: #d38daf;
    }

    .demo-2 a:hover,
    .demo-2 a:active {
    color: #fff;
    }

    .demo-2 .htmleaf-demos a {
    background: #883b61;
    color: #fff;
    }

    .demo-2 .htmleaf-top a:hover {
    background: rgba(255,255,255,0.3);
    color: #333;
    }

    .demo-3 body {
    color: #87968e;
    background: #fff2e3;
    }

    .demo-3 a {
    color: #ea5381;
    }

    .demo-3 .htmleaf-demos a {
    background: #ea5381;
    color: #fff;
    }

    .demo-4 body {
    color: #999;
    background: #fff2e3;
    overflow: hidden;
    }

    .demo-4 a {
    color: #1baede;
    }

    .demo-4 a:hover,
    .demo-4 a:active {
    opacity: 0.6;
    }

    .demo-4 .htmleaf-demos a {
    background: #1baede;
    color: #fff;
    }

    .demo-5 body {
    background: #fffbd6;
    }
    /****/
    .related {
    /*margin-top: 5em;*/
    color: #fff;
    background: #333;
    text-align: center;
    font-size: 1.25em;
    padding: 3em 0;
    overflow: hidden;
    }

    .related a {
    display: inline-block;
    text-align: left;
    margin: 20px auto;
    padding: 10px 20px;
    opacity: 0.8;
    -webkit-transition: opacity 0.3s;
    transition: opacity 0.3s;
    -webkit-backface-visibility: hidden;
    }

    .related a:hover,
    .related a:active {
    opacity: 1;
    }

    .related a img {
    max-width: 100%;
    }

    .related a h3 {
    font-weight: 300;
    margin-top: 0.15em;
    color: #fff;
    }

    @media screen and (max-width: 40em) {

    .htmleaf-icon span {
    display: none;
    }

    .htmleaf-icon:before {
    font-size: 160%;
    line-height: 2;
    }

    }

    @media screen and (max-width: 46.0625em) {
    .column {
    width: 100%;
    min-width: auto;
    min-height: auto;
    padding: 1em;
    }

    .column p {
    text-align: left;
    font-size: 1.5em;
    }

    .column:nth-child(2) {
    box-shadow: 0 -1px 0 rgba(0,0,0,0.1);
    }
    }

    @media screen and (max-width: 25em) {

    .htmleaf-icon span {
    display: none;
    }

    }

    核心js

    /**
    * 检测2个物体是否碰撞
    * @param object1 物体1
    * @param object2 物体2
    * @param overlap 允许重叠的大小
    * @returns {Boolean} 如果碰撞了,返回true
    */
    function CheckIntersect(object1, object2, overlap)
    {
    // x-轴 x-轴
    // A1------>B1 C1 A2------>B2 C2
    // +--------+ ^ +--------+ ^
    // | object1| | y-轴 | object2| | y-轴
    // | | | | | |
    // +--------+ D1 +--------+ D2
    //
    //overlap是重叠的区域值
    A1 = object1.x + overlap;
    B1 = object1.x + object1.size - overlap;
    C1 = object1.y + overlap;
    D1 = object1.y + object1.size - overlap;

    A2 = object2.x + overlap;
    B2 = object2.x + object2.size - overlap;
    C2 = object2.y + overlap;
    D2 = object2.y + object2.size - overlap;

    //假如他们在x-轴重叠
    if(A1 >= A2 && A1 <= B2
    || B1 >= A2 && B1 <= B2)
    {
    //判断y-轴重叠
    if(C1 >= C2 && C1 <= D2 || D1 >= C2 && D1 <= D2)
    {
    return true;
    }
    }
    return false;
    }

    /**
    * 坦克与地图块碰撞
    * @param tank 坦克对象
    * @param mapobj 地图对象
    * @returns {Boolean} 如果碰撞,返回true
    */
    function tankMapCollision(tank,mapobj){
    //移动检测,记录最后一次的移动方向,根据方向判断+-overlap,
    var tileNum = 0;//需要检测的tile数
    var rowIndex = 0;//map中的行索引
    var colIndex = 0;//map中的列索引
    var overlap = 3;//允许重叠的大小

    //根据tank的x、y计算出map中的row和col
    if(tank.dir == UP){
    rowIndex = parseInt((tank.tempY + overlap - mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((tank.tempX + overlap- mapobj.offsetX)/mapobj.tileSize);
    }else if(tank.dir == DOWN){
    //向下,即dir==1的时候,行索引的计算需要+tank.Height
    rowIndex = parseInt((tank.tempY - overlap - mapobj.offsetY + tank.size)/mapobj.tileSize);
    colIndex = parseInt((tank.tempX + overlap- mapobj.offsetX)/mapobj.tileSize);
    }else if(tank.dir == LEFT){
    rowIndex = parseInt((tank.tempY + overlap- mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((tank.tempX + overlap - mapobj.offsetX)/mapobj.tileSize);
    }else if(tank.dir == RIGHT){
    rowIndex = parseInt((tank.tempY + overlap- mapobj.offsetY)/mapobj.tileSize);
    //向右,即dir==3的时候,列索引的计算需要+tank.Height
    colIndex = parseInt((tank.tempX - overlap - mapobj.offsetX + tank.size)/mapobj.tileSize);
    }
    if(rowIndex >= mapobj.HTileCount || rowIndex < 0 || colIndex >= mapobj.wTileCount || colIndex < 0){
    return true;
    }
    if(tank.dir == UP || tank.dir == DOWN){
    var tempWidth = parseInt(tank.tempX - map.offsetX - (colIndex)*mapobj.tileSize + tank.size - overlap);//去除重叠部分
    if(tempWidth % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempWidth/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempWidth/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && colIndex+i < mapobj.wTileCount ;i++){
    var mapContent = mapobj.mapLevel[rowIndex][colIndex+i];
    if(mapContent == WALL || mapContent == GRID || mapContent == WATER || mapContent == HOME || mapContent == ANOTHREHOME){
    if(tank.dir == UP){
    tank.y = mapobj.offsetY + rowIndex * mapobj.tileSize + mapobj.tileSize - overlap;
    }else if(tank.dir == DOWN){
    tank.y = mapobj.offsetY + rowIndex * mapobj.tileSize - tank.size + overlap;
    }
    return true;
    }
    }
    }else{
    var tempHeight = parseInt(tank.tempY - map.offsetY - (rowIndex)*mapobj.tileSize + tank.size - overlap);//去除重叠部分
    if(tempHeight % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempHeight/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempHeight/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && rowIndex+i < mapobj.HTileCount;i++){
    var mapContent = mapobj.mapLevel[rowIndex+i][colIndex];
    if(mapContent == WALL || mapContent == GRID || mapContent == WATER || mapContent == HOME || mapContent == ANOTHREHOME){
    if(tank.dir == LEFT){
    tank.x = mapobj.offsetX + colIndex * mapobj.tileSize + mapobj.tileSize - overlap;
    }else if(tank.dir == RIGHT){
    tank.x = mapobj.offsetX + colIndex * mapobj.tileSize - tank.size + overlap;
    }
    return true;
    }
    }
    }
    return false;
    }

    /**
    * 子弹与地图块的碰撞
    * @param bullet 子弹对象
    * @param mapobj 地图对象
    */
    function bulletMapCollision(bullet,mapobj){
    var tileNum = 0;//需要检测的tile数
    var rowIndex = 0;//map中的行索引
    var colIndex = 0;//map中的列索引
    var mapChangeIndex = [];//map中需要更新的索引数组
    var result = false;//是否碰撞
    //根据bullet的x、y计算出map中的row和col
    if(bullet.dir == UP){
    rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
    }else if(bullet.dir == DOWN){
    //向下,即dir==1的时候,行索引的计算需要+bullet.Height
    rowIndex = parseInt((bullet.y - mapobj.offsetY + bullet.size)/mapobj.tileSize);
    colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
    }else if(bullet.dir == LEFT){
    rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
    colIndex = parseInt((bullet.x - mapobj.offsetX)/mapobj.tileSize);
    }else if(bullet.dir == RIGHT){
    rowIndex = parseInt((bullet.y - mapobj.offsetY)/mapobj.tileSize);
    //向右,即dir==3的时候,列索引的计算需要+bullet.Height
    colIndex = parseInt((bullet.x - mapobj.offsetX + bullet.size)/mapobj.tileSize);
    }
    if(rowIndex >= mapobj.HTileCount || rowIndex < 0 || colIndex >= mapobj.wTileCount || colIndex < 0){
    return true;
    }

    if(bullet.dir == UP || bullet.dir == DOWN){
    var tempWidth = parseInt(bullet.x - map.offsetX - (colIndex)*mapobj.tileSize + bullet.size);
    if(tempWidth % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempWidth/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempWidth/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && colIndex+i < mapobj.wTileCount ;i++){
    var mapContent = mapobj.mapLevel[rowIndex][colIndex+i];
    if(mapContent == WALL || mapContent == GRID || mapContent == HOME || mapContent == ANOTHREHOME){
    //bullet.distroy();
    result = true;
    if(mapContent == WALL){
    //墙被打掉
    mapChangeIndex.push([rowIndex,colIndex+i]);
    }else if(mapContent == GRID){

    }else{
    isGameOver = true;
    break;
    }
    }
    }
    }else{
    var tempHeight = parseInt(bullet.y - map.offsetY - (rowIndex)*mapobj.tileSize + bullet.size);
    if(tempHeight % mapobj.tileSize == 0 ){
    tileNum = parseInt(tempHeight/mapobj.tileSize);
    }else{
    tileNum = parseInt(tempHeight/mapobj.tileSize) + 1;
    }
    for(var i=0;i<tileNum && rowIndex+i < mapobj.HTileCount;i++){
    var mapContent = mapobj.mapLevel[rowIndex+i][colIndex];
    if(mapContent == WALL || mapContent == GRID || mapContent == HOME || mapContent == ANOTHREHOME){
    //bullet.distroy();
    result = true;
    if(mapContent == WALL){
    //墙被打掉
    mapChangeIndex.push([rowIndex+i,colIndex]);
    }else if(mapContent == GRID){

    }else{
    isGameOver = true;
    break;
    }
    }
    }
    }
    //更新地图
    map.updateMap(mapChangeIndex,0);
    return result;
    }

    原文地址:https://blog.csdn.net/weixin_41937552/article/details/116559485


    收起阅读 »

    vue 事件总线EventBus的概念、使用以及注意点

    前言vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 Ev...
    继续阅读 »

    前言

    vue组件中的数据传递最最常见的就是父子组件之间的传递。父传子通过props向下传递数据给子组件;子传父通过$emit发送事件,并携带数据给父组件。而有时两个组件之间毫无关系,或者他们之间的结构复杂,如何传递数据呢?这时就要用到 vue 中的事件总线 EventBus的概念

    正文

    EventBus的简介

    EventBus又称事件总线,相当于一个全局的仓库,任何组件都可以去这个仓库里获取事件,如图:


    EventBus的使用

    废话不多说,直接开始使用EventBus

    一、初始化

    要用EventBus,首先要初始化一个EventBus,这里称它为全局事件总线。

    • 第一种初始化方法
    import Vue from 'vue'
    //因为是全局的一个'仓库',所以初始化要在全局初始化
    const EventBus = new Vue()
    • 第二种初始化方法(本文选用这种初始化方法)
    //在已经创建好的Vue实例原型中创建一个EventBus
    Vue.prototype.$EventBus = new Vue()

    二、向EventBus发送事件

    发送事件的语法:this.$EventBus.$emit(发送的事件名,传递的参数)

    已经创建好EventBus后我们就需要向它发送需要传递的事件,以便其他组件可以向EventBus获取。
    例子:有两个组件A和B需要通信,他们不是父子组件关系,B事件需要获得A事件里的一组数据data

    <!-- A.vue 这里是以模块化的方式讲解的,即A组件和B组件分别各自
    一个.vue文件,所以代码中会有导入的语法-->

    <template>
    <button @click="sendMsg">发送MsgA</button>
    </template>

    <script>
    export default {
    data(){
    return{
    MsgA: 'A组件中的Msg'
    }
    },
    methods: {
    sendMsg() {
    /*调用全局Vue实例中的$EventBus事件总线中的$emit属性,发送事
    件"aMsg",并携带A组件中的Msg*/
    this.$EventBus.$emit("aMsg", this.MsgA);
    }
    }
    };
    </script>

    三、接收事件

    接收事件的语法:this.$EventBus.$on(监听的事件名, 回调函数)

    A组件已经向全局事件总线EventBus发送了一个aMsg事件,这时B组件就可以去aMsg监听这个事件,并可以获得一些数据。

    <!-- B.vue -->

    <template>

    <!-- 展示msgB -->
    <p>{{msgB}}</p>

    </template>

    <script>
    export default {
    data(){
    return {
    //初始化一个msgB
    msgB: ''
    }
    },
    mounted() {
    /*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
    到事件总线中的aMsg事件*/
    this.$EventBus.$on("aMsg", (data) => {
    //将A组件传递过来的参数data赋值给msgB
    this.msgB = data;
    });
    }
    };
    </script>

    B组件展示结果:A组件中的Msg


    这样,B组件就轻松接收到了A组件传递过来的参数,并成功展示了该参数,这样是不是就很简单的解决了各组件之间的通讯呢?虽然EventBus是一个很轻便的方法,任何数据都可以往里传,然后被别的组件获取,但是如果用不好,容易出现很严重的BUG,所以接下来我们就来讲解一下移除监听事件。

    四、移除监听事件

    在上一个例子中,我们A组件向事件总线发送了一个事件aMsg并传递了参数MsgA,然后B组件对该事件进行了监听,并获取了传递过来的参数。但是,这时如果我们离开B组件,然后再次进入B组件时,又会触发一次对事件aMsg的监听,这时时间总线里就有两个监听了,如果反复进入B组件多次,那么就会对aMsg进行多次的监听。

    总而言之,A组件只向EventBus发送了一次事件,但B组件却进行了多次监听,EventBus容器中有很多个一模一样的事件监听器这时就会出现,事件只触发一次,但监听事件中的回调函数执行了很多次

    • 解决办法:在组件离开,也就是被销毁前,将该监听事件给移除,以免下次再重复创建监听
    • 语法:this.$EventBus.$off(要移除监听的事件名)
    <!-- B.vue -->

    <template>

    <!-- 展示msgB -->
    <p>{{msgB}}</p>

    </template>

    <script>
    export default {
    data(){
    return {
    //初始化一个msgB
    msgB: ''
    }
    },
    mounted() {
    /*调用全局Vue实例中的$EventBus事件总线中的$on属性,监听A组件发送
    到事件总线中的aMsg事件*/
    this.$EventBus.$on("aMsg", (data) => {
    //将A组件传递过来的参数data赋值给msgB
    this.msgB = data;
    });
    },
    beforeDestroy(){
    //移除监听事件"aMsg"
    this.$EventBus.$off("aMsg")
    }
    };
    </script>

    结束语

    好了,对于vue中的事件总线的讲解就到这里了,这也是我今天在做项目时用到的一个小知识点,接下来附上一张我因为没有及时移除事件监听,导致我每重进组件一次就报错48条错误信息的图,希望大家在我的文章中能血啊都一些东西,并且不要再犯我的这种低级错误。


    本文链接:https://blog.csdn.net/l_ppp/article/details/105924658

    收起阅读 »

    无废话快速上手React路由

    本文以简洁为目标,帮助快速上手react-router-dom默认你接触过路由相关的开发安装输入以下命令进行安装:// npmnpm install react-router-dom// yarnyarn add react-router-domreact-r...
    继续阅读 »

    本文以简洁为目标,帮助快速上手react-router-dom默认你接触过路由相关的开发

    安装

    输入以下命令进行安装:

    // npm
    npm install react-router-dom

    // yarn
    yarn add react-router-dom

    react-router相关标签

    react-router常用的组件有以下八个:

    import { 
    BrowserRouter,
    HashRouter,
    Route,
    Redirect,
    Switch,
    Link,
    NavLink,
    withRouter,
    } from 'react-router-dom'

    简单路由跳转

    实现一个简单的一级路由跳转

    import { 
    BrowserRouter as Router,
    Route,
    Link
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {
    return (
    <div className="App">
    <Router>
    <Link to="/home" className="link">跳转Home页面</Link>
    <Link to="/about" className="link">跳转About页面</Link>
    <Route path="/home" component={Home}/>
    <Route path="/about" component={About}/>
    </Router>
    </div>
    );
    }

    export default App;

    要点总结:

    1. Route组件必须在Router组件内部
    2. Link组件的to属性的值为点击后跳转的路径
    3. Route组建的path属性是与Link标签的to属性匹配的; component属性表示Route组件匹配成功后渲染的组件对象

    嵌套路由跳转

    React 的路由匹配层级是有顺序的

    例如,在 App 组件中,设置了两个路由组件的匹配路径,分别是 /home 和 /about,代码如下:

    import { 
    BrowserRouter as Router,
    Route,
    Link,
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {

    return (
    <div className="App">
    <Router>
    <Link to="/home">跳转Home页面</Link>
    <Link to="/about">跳转About页面</Link>

    <Route path="/home" component={Home}/>
    <Route path="/about" component={About}/>

    </Router>
    </div>
    );
    }

    export default App;

    然后 Home 组件中同样也想设置两个路由组件的匹配路径,分别是 /home/one 和 /home/two,此时就可以看出,这个 /home/one 和 /home/two 为上一级路由 /home 的二级嵌套路由,代码如下:

    import React from 'react'
    import {
    Route,
    Link,
    } from 'react-router-dom'
    import One from './one'
    import Two from './two'

    function Home () {

    return (
    <>
    我是Home页面
    <Link to="/home/one">跳转到Home/one页面</Link>
    <Link to="/home/two">跳转到Home/two页面</Link>

    <Route path="/home/one" component={One}/>
    <Route path="/home/two" component={Two}/>
    </>
    )
    }

    export default Home

    特别注意: Home 组件中的路由组件 One 的二级路由路径匹配必须要写 /home/one ,而不是 /one ,不要以为 One 组件看似在 Home 组件内就可以简写成 /one

    动态链接

    NavLink可以将当前处于active状态的链接附加一个active类名,例如:

    import { 
    BrowserRouter as Router,
    Route,
    NavLink
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {
    return (
    <div className="App">
    <Router>
    <NavLink to="/home" className="link">跳转Home页面</NavLink>
    <NavLink to="/about" className="link">跳转About页面</NavLink>
    <Route path="/home" component={Home}/>
    <Route path="/about" component={About}/>
    </Router>
    </div>
    );
    }

    export default App;
    /* 设置active类的样式 */
    .active {
    font-weight: blod;
    color: red;
    }

    路由匹配优化

    当点击跳转链接时,会自动去尝试匹配所有的Route对应的路径:

    正常情况下,只需匹配到一个规则,渲染即可,即匹配成功一个后,无需进行后续的匹配尝试,此时可以用Switch组件,如下所示:

    import { 
    BrowserRouter as Router,
    Route,
    NavLink,
    Switch,
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {
    return (
    <div className="App">
    <Router>
    <NavLink to="/home" className="link">跳转Home页面</NavLink>
    <NavLink to="/about" className="link">跳转About页面</NavLink>

    <Switch>
    <Route path="/home" component={Home}/>
    <Route path="/about" component={About}/>
    <Route path="/home" component={Home}/>
    <Route path="/home" component={Home}/>
    {/* 此处省略一万个Route组件 */}
    <Route path="/home" component={Home}/>
    </Switch>

    </Router>
    </div>
    );
    }

    export default App;

    要点总结:

    1. 将多个Route组件同时放在一个Switch组件中,即可避免多次无意义的路由匹配,以此提升性能

    重定向

    当页面跳转时,若跳转链接没有匹配上任何一个 Route 组件,那么就会显示 404 页面,所以我们需要一个重定向组件 Redirect ,代码如下:

    import { 
    BrowserRouter as Router,
    Route,
    NavLink,
    Switch,
    Redirect,
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {
    return (
    <div className="App">
    <Router>
    <NavLink to="/home" className="link">跳转Home页面</NavLink>
    <NavLink to="/about" className="link">跳转About页面</NavLink>
    <NavLink to="/shop" className="link">跳转Shop页面</NavLink> {/* 点击,跳转到/shop,但该路径没有设置 */}

    <Switch>
    <Route path="/home" component={Home}/>
    <Route path="/about" component={About}/>
    <Redirect to="/home" /> {/* 当以上Route组件都匹配失败时,重定向到/home */}
    </Switch>

    </Router>
    </div>
    );
    }

    export default App;

    路由传参

    所有路由传递的参数,都会在跳转路由组件的 props 中获取到,每种传参方式接收的方式略有不同

    路由传参的方式一共有三种,依次来看一下

    第一种

    第一种是在 Link 组件的跳转路径上携带参数,并在 Route 组件的匹配路径上通过 :参数名 的方式接收参数,代码如下:

    import { 
    BrowserRouter as Router,
    Route,
    NavLink,
    Switch,
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {
    return (
    <div className="App">
    <Router>
    {/* 在 /home 的路径上携带了 张三、18 共两个参数 */}
    <NavLink to="/home/张三/18" className="link">跳转Home页面</NavLink>
    <NavLink to="/about" className="link">跳转About页面</NavLink>

    <Switch>
    {/* 在 /home 匹配路径上相同的位置接收了 name、age 两个参数 */}
    <Route path="/home/:name/:age" component={Home}/>
    <Route path="/about" component={About}/>
    </Switch>

    </Router>
    </div>
    );
    }

    export default App;

    第二种

    第二种方式就是通过在 Link 组件的跳转链接后面跟上以 ? 开头,类似 ?a=1&b=3 这样的参数进行传递,代码如下:

    import { 
    BrowserRouter as Router,
    Route,
    NavLink,
    Switch,
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {
    return (
    <div className="App">
    <Router>
    {/* 在跳转路径后面以?开头传递两个参数,分别为name=张三、age=18 */}
    <NavLink to="/home?name=张三&age=18" className="link">跳转Home页面</NavLink>
    <NavLink to="/about" className="link">跳转About页面</NavLink>

    <Switch>
    {/* 此处无需做接收操作 */}
    <Route path="/home" component={Home}/>
    <Route path="/about" component={About}/>
    </Switch>

    </Router>
    </div>
    );
    }

    export default App;

    第三种

    第三种方式就是以对象的形式编写 Link 组件的 to 跳转属性,并通过 state 属性来传递参数,代码如下:

    import { 
    BrowserRouter as Router,
    Route,
    NavLink,
    Switch,
    } from 'react-router-dom'
    import Home from './home'
    import About from './about'

    function App() {
    return (
    <div className="App">
    <Router>
    {/* 以对象的形式描述to属性,路径属性名为pathname,参数属性名为state */}
    <NavLink to={{pathname: "/home", state: {name: '张三', age: 18}}} className="link">跳转Home页面</NavLink>
    <NavLink to="/about" className="link">跳转About页面</NavLink>

    <Switch>
    {/* 此处无需特地接收属性 */}
    <Route path="/home" component={Home}/>
    <Route path="/about" component={About}/>
    </Switch>

    </Router>
    </div>
    );
    }

    export default App;

    函数式路由

    以上主要都是通过 react-router-dom 中的 Link 组件来往某个路由组件跳转

    但有时,我们需要更灵活的方式进行跳转路由,例如通过调用一个函数,随时随地进行路由跳转,这就叫函数式路由

    函数式路由用到的方法有以下 5 个

    push

    push 方法就是使页面跳转到对应路径,并在浏览器中留下记录(即可以通过浏览器的回退按钮,返回上一个页面)

    举个例子:在路由组件 Home 中设置一个按钮 button ,点击后调用 push 方法,跳转到 /about 页面

    import React from 'react'

    function Home (props) {

    let pushLink = () => {
    props.history.push('/about')
    }

    return (
    <div className="a">
    我是Home页面
    <button onClick={pushLink}>跳转到about页面</button>
    </div>
    )
    }

    export default Home

    replace

    replace 方法与 push 方法类似,不一样的地方就是,跳转后不会在浏览器中保存上一个页面的记录(即无法通过浏览器的回退按钮,返回上一个页面)

    改动一下代码

    import React from 'react'

    function Home (props) {

    let replaceLink = () => {
    props.history.replace('/about')
    }

    return (
    <div className="a">
    我是Home页面
    <button onClick={replaceLink}>跳转到about页面</button>
    </div>
    )
    }

    export default Home

    goForward

    调用 goForward 方法,就相当于点击了浏览器的返回下一个页面按钮:


    goBack

    调用 goBack 方法,就相当于点击了浏览器的返回上一个页面的按钮,如下图所示:


    go

    go 方法顾名思义,是用于跳转到指定路径的。

    该方法接受一个参数(参数类型为 Number),情况如下:

    1. 当参数为正数 n 时,表示跳转到下 n 个页面。例如 go(1) 相当于调用了一次 goForward 方法
    2. 当参数为负数 n 时,表示跳转到上 n 个页面。例如 go(-3) 相当于调用了三次 goBack 方法
    3. 当参数为 0 时,表示刷新当前页面


    收起阅读 »

    vue数据可视化界面,智慧图表。Echarts,以及git

    一、数据图表一张图表胜过千万句话1.1HighChart概念兼容 IE6+、完美支持移动端、图表类型丰富、方便快捷的 HTML5 交互性图表库下载一、通过CDNhttps://code.highcharts.com.cn/index.html二、通过NPM下载...
    继续阅读 »

    一、数据图表

    一张图表胜过千万句话

    1.1HighChart

    • 概念
    兼容 IE6+、完美支持移动端、图表类型丰富、方便快捷的 HTML5 交互性图表库

    下载

    一、通过CDN
    https://code.highcharts.com.cn/index.html
    二、通过NPM下载(用的比较多)
    npm install highcharts
    三、通过官网下载
    https://www.highcharts.com.cn/download
    通过引入库的方式引入到本地

    基本应用







    Document










    1.2Echarts(用的更多一些)

    一、通过CDN 
    jsdelivr.com/package/npm/echarts
    二、通过NPM(通过NPM)
    npm install echarts
    三、通过官网
    https://echarts.apache.org/zh/download.html
    四、通过github
    https://github.com/apache/echarts/releases

    1.3如何在vue脚手架中引入Echarts

    • 局部引入




    全局引入
    //全局引入echart
    import * as echarts from 'echarts'
    Vue.prototype.$echarts = echarts

    home.vue





    二、git代码管理

    2.1代码管理工具

    svn (小乌龟)
    https://tortoisesvn.net/
    git (命令)
    github(所有开源项目的归属地)
    https://github.com/
    码云
    https://gitee.com/
    git软件
    https://git-scm.com/

    无论是gihub还是码云,他们都是用git命令去操作的。所以命令都一样

    git软件的安装,下一步,下一步,傻瓜式安装即可

    装成功的状态: 鼠标右键看到 git Bash Here 就OK

    2.2创建一个远程仓库 (新项目)

    先去GitHub/码云创建一个新的远程仓库,然后把本地暂缓区的内容提交到远程仓库

    一、登录github/码云输入用户名密码
    二、新建一个远程仓库,在官网右上角(点击+ )
    三、创建一个仓库名称,添加仓库描述,创建一个公有的仓库,不需要为仓库创建其他内容

    在公司的创建一个新项目的骚操作

    一般这一部分,轮不到大家去做。

    一、在本地创建一个文件夹,创建相关的基本骨架
    二、初始化当前文件夹变成本地仓库(会出现一个.git的隐藏文件)
    git init
    三、本地的所有内容上传到暂缓区
    git add .
    四、提交的时候要做记录
    git commit -m '尽量写英文,非要写写中文也可以'
    五、链接远程仓库
    git remote add origin https://gitee.com/zhangzhangzhangdada/shop-admin.git
    六、把暂缓区的内容推送到远程仓库 (master 默认的分支名字)
    git push -u origin master

    原文:https://blog.csdn.net/weixin_49030317/article/details/116666179

    收起阅读 »

    JavaScript解密之旅-----数组的遍历方法总结

    数组的循环    一、forEach()    二、map()    三、filter()    四...
    继续阅读 »

    数组的循环
        一、forEach()
        二、map()
        三、filter()
        四、reduce()与reduceRight()
        五、every()
        六、some()
        七、find()与findIndex()
        八、 for in
        九、 for of
        十、 for    
    总结
    数组的循环
    一、forEach()
    对数组进行遍历循环,对数组中的每一项运行给定函数。这个方法没有返回值。
    参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身。

    var arr = [1, 2, 3, 4, 5];
    arr.forEach(function (item, index, arr) {
    console.log(item, index, arr);
    // item:遍历的数组内容,index:第对应的数组索引,arr:数组本身。
    });

    二、map()

    • 指“映射”,对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组
    • 参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身
    var arr = [1, 2, 3, 4, 5];
    var arr2 = arr.map(function (item) {
    return item * item;
    });
    console.log(arr2); //[1, 4, 9, 16, 25]

    三、filter()

    • “过滤”功能,数组中的每一项运行给定函数,返回满足过滤条件组成的数组。
    • 参数都是function类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身
    var arr = [1, 2, 3, 4, 5];
    var arr2 = arr.filter(function (x, index) {
    return x % 2 == 0 || index >= 2;
    });
    console.log(arr2); // [2,3,4,5]

    四、reduce()与reduceRight()

    • x 是上一次计算过的值, 第一次循环的时候是数组中的第1个元素
    • y 是数组中的每个元素, 第一次循环的时候是数组的第2个元素
    • 最后一个数组本身
    //  reduce()
    let array = [1, 2, 3, 4, 5];
    let arrayNew = array.reduce((x, y) => {
    console.log("x===>" + x);
    console.log("y===>" + y);
    console.log("x+y===>", Number(x) + Number(y));
    return x + y;
    });
    console.log("arrayNew", arrayNew); // 15
    console.log(array); // [1, 2, 3, 4, 5]
    // reduceRight() 只是执行数组顺序为倒序

    五、every()

    • 判断数组中每一项都是否满足条件,只有所有项都满足条件,才会返回true否是为false
    var arr = [1, 2, 3, 4, 5];
    var arr2 = arr.every(function (x) {
    return x < 8;
    });
    console.log(arr2); //true
    var arr3 = arr.every(function (x) {
    return x < 3;
    });
    console.log(arr3); // false

    六、some()

    • 判断数组中是否存在满足条件的项,只要有一项满足条件,就会返回true,否则为false
    var arr = [1, 2, 3, 4, 5];
    var arr2 = arr.some(function(x) {
    return x < 3;
    });
    console.log(arr2); //true
    var arr3 = arr.some(function(x) {
    return x > 6;
    });
    console.log(arr3); // false

    七、find()与findIndex()

    • 该方法主要应用于查找第一个符合条件的数组元素。它的参数是一个回调函数。
    • 在回调函数中可以写你要查找元素的条件,当条件成立为true时,返回该元素。如果没有符合条件的元素,返回值为undefined。findIndex返回-1
    // find()
    let arr = [1, 2, 3, 4, 5];
    let res = arr.find(function (val, index, arr) {
    return val > 3;
    });
    console.log(res); //4
    // findIndex
    let arr = [1, 2, 3, 4, 5];
    let res = arr.findIndex(function (val, index, arr) {
    return val > 3;
    });
    console.log(res); //3

    八、 for in

    • for…in循环可用于循环对象和数组,推荐用于循环对象,可以用来遍历JSON
    var arr = [
    { id: 1, name: "程序员" },
    { id: 2, name: "掉头发" },
    { id: 3, name: "你不信" },
    { id: 4, name: "薅一下" },
    ];
    var arrNew = [];
    for (var key in arr) {
    console.log(key);
    console.log(arr[key]);
    arrNew.push(arr[key].id);
    }
    console.log(arrNew);

    九、 for of

    • for…of循环可用于循环对象和数组,推荐用于循环对象,可以用来遍历JSON
    var arr = [
    { name: "程序员" },
    { name: "掉头发" },
    { name: "你不信" },
    { name: "薅一下" },
    ];
    // key()是对键名的遍历;
    // value()是对键值的遍历;
    // entries()是对键值对的遍历;
    for (let item of arr) {
    console.log(item);
    }
    // 输出数组索引
    for (let item of arr.keys()) {
    console.log(item);
    }
    // 输出内容和索引
    for (let [item, val] of arr.entries()) {
    console.table(item + ":" + val);
    }

    十、 for

    原生实现方式

    var arr = [
    { name: "程序员" },
    { name: "掉头发" },
    { name: "你不信" },
    { name: "薅一下" },
    ];
    for (let index = 0; index < arr.length; index++) {
    const element = arr[index];
    console.log(element )
    }

    总结

    方法1~7为ES6新语法 IE9及以上才支持。不过可以通过babel转义支持IE低版本。以上均不改变原数组。




    收起阅读 »

    7种经常使用的Vue.js模式?你居然还不知道!!

    说实话,阅读文档并不是我们大多数人喜欢的事情,但当使用像Vue这样不断发展的现代前端框架时,很多东西会随着每一个新版本的发布而改变,你可能会错过一些后来推出的新的闪亮功能。让我们看一下那些有趣但不那么流行的功能和优化的写法。请记住,所有这些都是Vue文档的一部...
    继续阅读 »

    说实话,阅读文档并不是我们大多数人喜欢的事情,但当使用像Vue这样不断发展的现代前端框架时,很多东西会随着每一个新版本的发布而改变,你可能会错过一些后来推出的新的闪亮功能。让我们看一下那些有趣但不那么流行的功能和优化的写法。请记住,所有这些都是Vue文档的一部分。

    7种Vue.js模式
    1.处理加载状态
    在大型应用程序中,我们可能需要将应用程序划分为更小的块,只有在需要时才从服务器加载组件。为了使这一点更容易,Vue允许你将你的组件定义为一个工厂函数,它异步解析你的组件定义。Vue只有在需要渲染组件时才会触发工厂函数,并将缓存结果,以便将来重新渲染。2.3版本的新功能是,异步组件工厂也可以返回一个如下格式的对象。

    const AsyncComponent = () => ({
    // 要加载的组件(应为Promise)
    component: import('./MyComponent.vue'),
    // 异步组件正在加载时要使用的组件
    loading: LoadingComponent,
    // 加载失败时使用的组件
    error: ErrorComponent,
    // 显示加载组件之前的延迟。默认值:200ms。
    delay: 200,
    // 如果提供并超过了超时,则会显示error组件。默认值:无穷。
    timeout: 3000
    })

    通过这种方法,你有额外的加载和错误状态、组件获取的延迟和超时等选项。

    2.廉价的“v-once”静态组件
    在Vue中渲染纯HTML元素的速度非常快,但有时你可能有一个包含大量静态内容的组件。在这种情况下,你可以通过在根元素中添加 v-once 指令来确保它只被评估一次,然后进行缓存,就像这样。

    Vue.component('terms-of-service', {
    template: `
    <div v-once>
    <h1>Terms of Service</h1>
    ... a lot of static content ...
    </div>
    `
    })

    3.递归组件

    组件可以在自己的模板中递归调用自己,但是,它们只能通过 name 选项来调用。

    如果你不小心,递归组件也可能导致无限循环:

    name: 'stack-overflow',
    template: '<div><stack-overflow></stack-overflow></div>'

    像上面这样的组件会导致“超过最大堆栈大小”的错误,所以要确保递归调用是有条件的即(使用 v-if 最终将为 false)

    4.内联模板
    当特殊属性 inline-template 存在于一个子组件上时,该组件将使用它的内部内容作为它的模板,而不是将其视为分布式内容,这允许更灵活的模板编写。

    <my-component inline-template>
    <div>
    <p>These are compiled as the component's own template.</p>
    <p>Not parent's transclusion content.</p>
    </div>
    </my-component>


    5.动态指令参数
    指令参数可以是动态的。例如,在 v-mydirective:[argument]=“value" 中, argument 可以根据组件实例中的数据属性更新!这使得我们的自定义指令可以灵活地在整个应用程序中使用。

    这是一条指令,其中可以根据组件实例更新动态参数:

    <div id="dynamicexample">
    <h3>Scroll down inside this section ↓</h3>
    <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
    </div>
    Vue.directive('pin', {
    bind: function (el, binding, vnode) {
    el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
    }
    })

    new Vue({
    el: '#dynamicexample',
    data: function () {
    return {
    direction: 'left'
    }
    }
    })

    6.事件和键修饰符

    对于 .passive.capture 和 .once 事件修饰符,Vue提供了可与 on 一起使用的前缀:


    on: {
    '!click': this.doThisInCapturingMode,
    '~keyup': this.doThisOnce,
    '~!mouseover': this.doThisOnceInCapturingMode
    }



    7.依赖注入(Provide/Inject)
    有几种方法可以让两个组件在 Vue 中进行通信,它们各有优缺点。在2.2版本中引入的一种新方法是使用Provide/Inject的依赖注入。

    这对选项一起使用,允许一个祖先组件作为其所有子孙的依赖注入器,无论组件层次结构有多深,只要它们在同一个父链上。如果你熟悉React,这与React的上下文功(context)能非常相似。

    // parent component providing 'foo'
    var Provider = {
    provide: {
    foo: 'bar'
    },
    // ...
    }

    // child component injecting 'foo'
    var Child = {
    inject: ['foo'],
    created () {
    console.log(this.foo) // => "bar"
    }
    // ...
    }



    收起阅读 »

    vue传值方式总结 (十二种方法)

    一.父传子传递(1)在父组件的子组件标签上绑定一个属性,挂载要传输的变量(2)在子组件中通过props来接受数据,props可以是数组也可以是对象,接受的数据可以直接使用 props: [“属性 名”] props:{属性名:数据类型}代码示例://父组件&l...
    继续阅读 »

    一.父传子传递

    (1)在父组件的子组件标签上绑定一个属性,挂载要传输的变量
    (2)在子组件中通过props来接受数据,props可以是数组也可以是对象,接受的数据可以直接使用 props: [“属性 名”] props:{属性名:数据类型}
    代码示例:

    //父组件
    <template>
    <div>
    <i>父组件</i>
    <!--页面使用-->
    <son :data='name'></son>
    </div>
    </template>

    <script>
    import son from "./son.vue";//导入父组件
    export default {
    components: { son },//注册组件
    name: "父组件",
    data() {
    return {
    name: "Frazier", //父组件定义变量
    };
    },
    };
    </script>

    //子组件
    <template>
    <div>{{data}}</div>
    </template>

    <script>
    export default {
    components: { },
    name: '子组件',
    props:["data"],
    };
    </script>


    二.子传父传递

    (1)在父组件的子组件标签上自定义一个事件,然后调用需要的方法
    (2)在子组件的方法中通过 this.$emit(“事件”)来触发在父组件中定义的事件,数据是以参数的形式进行传递的
    代码示例:

    //父组件
    <template>
    <div>
    <i>父组件</i>
    <!--页面使用-->
    <son @lcclick="lcclick"></son>//自定义一个事件
    </div>
    </template>

    <script>
    import son from "./son.vue"; //导入父组件
    export default {
    components: { son }, //注册组件
    name: "父组件",
    data() {
    return {};
    },
    methods: {
    lcclick(){
    alert('子传父')
    }
    },
    };
    </script>

    //子组件
    <template>
    <div>
    <button @click="lcalter">点我</button>
    </div>
    </template>

    <script>
    export default {
    components: { },
    name: '子组件',
    methods: {
    lcalter(){
    this.$emit('lcclick')//通过emit来触发事件
    }
    },
    };
    </script>

    三.兄弟组件通信(bus总线)
    (1)在src中新建一个Bus.js的文件,然后导出一个空的vue实例
    (2)在传输数据的一方引入Bus.js 然后通过Bus.e m i t ( “ 事 件 名 ” , " 参 数 " ) 来 来 派 发 事 件 , 数 据 是 以 emit(“事件名”,"参数")来来派发事件,数据是以emit(“事件名”,"参数")来来派发事件,数据是以emit()的参 数形式来传递
    (3)在接受的数据的一方 引入 Bus.js 然后通过 Bus.$on(“事件名”,(data)=>{data是接受的数据})
    图片示例:





    四.ref/refs(父子组件通信)

    (1)ref 如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,
    (2)可以通过实例直接调用组件的方法或访问数据。也算是子组件向父组件传值的一种
    代码示例:

    //父组件
    <template>
    <div>
    <button @click="sayHello">sayHello</button>
    <child ref="childForRef"></child>
    </div>
    </template>
    <script>
    import child from './child.vue'
    export default {
    components: { child },
    data () {
    return {
    childForRef: null,
    }
    },
    mounted() {
    this.childForRef = this.$refs.childForRef;
    console.log(this.childForRef.name);
    },
    methods: {
    sayHello() {
    this.childForRef.sayHello()
    }
    }
    }
    </script>

    //子组件
    <template>
    <div>child 的内容</div>
    </template>
    <script>
    export default {
    data () {
    return {
    name: '我是 child',
    }
    },
    methods: {
    sayHello () {
    console.log('hello');
    alert('hello');
    }
    }
    }
    </script>

    五.Vuex通信

    组件通过 dispatch 到 actions,actions 是异步操作,再 actions中通过 commit 到 mutations,mutations 再通过逻辑操作改变 state,从而同步到组件,更新其数据状态
    代码示例:

    //父组件
    template>
    <div id="app">
    <ChildA/>
    <ChildB/>
    </div>
    </template>
    <script>
    import ChildA from './ChildA' // 导入A组件
    import ChildB from './ChildB' // 导入B组件
    export default {
    components: {ChildA, ChildB} // 注册组件
    }
    </script>

    //子组件A
    <template>
    <div id="childA">
    <h1>我是A组件</h1>
    <button @click="transform">点我让B组件接收到数据</button>
    <p>因为点了B,所以信息发生了变化:{{BMessage}}</p>
    </div>
    </template>
    <script>
    export default {
    data() {
    return {
    AMessage: 'Hello,B组件,我是A组件'
    }
    },
    computed: {
    BMessage() {
    // 这里存储从store里获取的B组件的数据
    return this.$store.state.BMsg
    }
    },
    methods: {
    transform() {
    // 触发receiveAMsg,将A组件的数据存放到store里去
    this.$store.commit('receiveAMsg', {
    AMsg: this.AMessage
    })
    }
    }
    }
    </script>
    //子组件B
    <template>
    <div id="childB">
    <h1>我是B组件</h1>
    <button @click="transform">点我让A组件接收到数据</button>
    <p>点了A,我的信息发生了变化:{{AMessage}}</p>
    </div>
    </template>

    <script>
    export default {
    data() {
    return {
    BMessage: 'Hello,A组件,我是B组件'
    }
    },
    computed: {
    AMessage() {
    // 这里存储从store里获取的A组件的数据
    return this.$store.state.AMsg
    }
    },
    methods: {
    transform() {
    // 触发receiveBMsg,将B组件的数据存放到store里去
    this.$store.commit('receiveBMsg', {
    BMsg: this.BMessage
    })
    }
    }
    }
    </script>
    //vuex
    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex)
    const state = {
    AMsg: '',
    BMsg: ''
    }

    const mutations = {
    receiveAMsg(state, payload) {
    // 将A组件的数据存放于state
    state.AMsg = payload.AMsg
    },
    receiveBMsg(state, payload) {
    // 将B组件的数据存放于state
    state.BMsg = payload.BMsg
    }
    }

    export default new Vuex.Store({
    state,
    mutations
    })

    六.$parent
    通过parent可以获父组件实例 ,然 后通过这个实例就可以访问父组件的属 性和方法 ,它还有一个兄弟parent可以获父组件实例,然后通过这个实例就可以访问父组件的属性和方法,它还有一个兄弟parent可以获父组件实例,然后通过这个实例就可以访问父组件的属性和方法,它还有一个兄弟root,可以获取根组件实例。
    代码示例:

    // 获父组件的数据
    this.$parent.foo

    // 写入父组件的数据
    this.$parent.foo = 2

    // 访问父组件的计算属性
    this.$parent.bar

    // 调用父组件的方法
    this.$parent.baz()

    //在子组件传给父组件例子中,可以使用this.$parent.getNum(100)传值给父组件。

    七.sessionStorage传值

    sessionStorage 是浏览器的全局对象,存在它里面的数据会在页面关闭时清除 。运用这个特性,我们可以在所有页面共享一份数据。
    代码示例:

    // 保存数据到 sessionStorage
    sessionStorage.setItem('key', 'value');

    // 从 sessionStorage 获取数据
    let data = sessionStorage.getItem('key');

    // 从 sessionStorage 删除保存的数据
    sessionStorage.removeItem('key');

    // 从 sessionStorage 删除所有保存的数据
    sessionStorage.clear();

    注意:里面存的是键值对,只能是字符串类型,如果要存对象的话,需要使用 let objStr = JSON.stringify(obj) 转成字符串然后再存储(使用的时候 let obj = JSON.parse(objStr) 解析为对象)。
    推荐一个库 good-storage ,它封装了sessionStorage ,可以直接用它的API存对象

    //localStorage
    storage.set(key,val)
    storage.get(key, def)
    //sessionStorage
    storage.session.set(key, val)
    storage.session.get(key, val)

    八.路由传值
    使用问号传值
    A页面跳转B页面时使用 this.r o u t e r . p u s h ( ’ / B ? n a m e = d a n s e e k ’ ) B 页 面 可 以 使 用 t h i s . router.push(’/B?name=danseek’) B页面可以使用 this.router.push(’/B?name=danseek’)B页面可以使用this.route.query.name 来获取A页面传过来的值
    上面要注意router和route的区别
    使用冒号传值
    配置如下路由:

    {
    path: '/b/:name',
    name: 'b',
    component: () => import( '../views/B.vue')
    },

    在B页面可以通过 this.$route.params.name 来获取路由传入的name的值

    使用父子组件传值
    由于router-view本身也是一个组件,所以我们也可以使用父子组件传值方式传值,然后在对应的子页面里加上props,因为type更新后没有刷新路由,所以不能直接在子页面的mounted钩子里直接获取最新type的值,而要使用watch

    <router-view :type="type"></router-view>

    // 子页面
    props: ['type']
    watch: {
    type(){
    // console.log("在这个方法可以时刻获取最新的数据:type=",this.type)
    },
    },

    九.祖传孙 $attrs

    正常情况下需要借助父亲的props作为中间过渡,但是这样在父亲组件就会多了一些跟父组件业务无关的属性,耦合度高,借助$attrs可以简化些,而且祖跟孙都无需做修改
    祖组件:

    <template>
    <section>
    <parent name="grandParent" sex="男" age="88" hobby="code" @sayKnow="sayKnow"></parent>
    </section>
    </template>

    <script>
    import Parent from './Parent'
    export default {
    name: "GrandParent",
    components: {
    Parent
    },
    data() {
    return {}
    },
    methods: {
    sayKnow(val){
    console.log(val)
    }
    },
    mounted() {
    }
    }
    </script>

    template>
    <section>
    <p>父组件收到</p>
    <p>祖父的名字:{{name}}</p>
    <children v-bind="$attrs" v-on="$listeners"></children>
    </section>
    </template>

    <script>
    import Children from './Children'

    export default {
    name: "Parent",
    components: {
    Children
    },
    // 父组件接收了name,所以name值是不会传到子组件的
    props:['name'],
    data() {
    return {}
    },
    methods: {},
    mounted() {
    }
    }
    </script>
    <template>
    <section>
    <p>子组件收到</p>
    <p>祖父的名字:{{name}}</p>
    <p>祖父的性别:{{sex}}</p>
    <p>祖父的年龄:{{age}}</p>
    <p>祖父的爱好:{{hobby}}</p>

    <button @click="sayKnow">我知道啦</button>
    </section>
    </template>

    <script>
    export default {
    name: "Children",
    components: {},
    // 由于父组件已经接收了name属性,所以name不会传到子组件了
    props:['sex','age','hobby','name'],
    data() {
    return {}
    },
    methods: {
    sayKnow(){
    this.$emit('sayKnow','我知道啦')
    }
    },
    mounted() {
    }
    }
    </script>

    十.孙传祖使用$listeners

    文字内容同第九个

    祖组件

    <template>
    <div id="app">
    <children-one @eventOne="eventOne"></children-one>
    {{ msg }}
    </div>
    </template>
    <script>
    import ChildrenOne from '../src/components/children.vue'
    export default {
    name: 'App',
    components: {
    ChildrenOne,
    },
    data() {
    return {
    msg: ''
    }
    },
    methods: {
    eventOne(value) {
    this.msg = value
    }
    }
    }
    </script>

    //父组件
    <template>
    <div>
    <children-two v-on="$listeners"></children-two>
    </div>
    </template>

    <script>
    import ChildrenTwo from './childrenTwo.vue'

    export default {
    name: 'childrenOne',
    components: {
    ChildrenTwo
    }
    }
    </script>
    //子组建
    <template>
    <div>
    <button @click="setMsg">点击传给祖父</button>
    </div>
    </template>

    <script>
    export default {
    name: 'children',
    methods: {
    setMsg() {
    this.$emit('eventOne', '123')
    }
    }
    }
    </script>

    十一.promise传参

    promise 中 resolve 如何传递多个参数

    //类似与这样使用,但实际上后面两个参数无法获取
    promise = new Promise((resolve,reject)=>{
    let a = 1
    let b = 2
    let c = 3
    resolve(a,b,c)
    })
    promise.then((a,b,c)=>{
    console.log(a,b,c)
    })

    resolve() 只能接受并处理一个参数,多余的参数会被忽略掉。
    如果想多个用数组,或者对象方式。。
    数组

    promise = new Promise((resolve,reject)=>{
    resolve([1,2,3])
    })
    promise.then((arr)=>{
    console.log(arr[0],arr[1],arr[2])
    })

    对象

    promise = new Promise((resolve,reject)=>{
    resolve({a:1,b:2,c:3})
    })
    promise.then(obj=>{
    console.log(obj.a,obj.b,obj.c)
    })

    十二.全局变量

    定义一个全局变量,在有值的组件直接赋值,在需要的组件内直接使用就可以了

    本文链接:https://blog.csdn.net/Frazier1995/article/details/116069811

    收起阅读 »

    前端必须要了解的一些知识 (十一)

    六种基本数据类型undefinednullstringbooleannumbersymbol(ES6)一种引用类型Objectstringlength属性prototype 添加的方法或属性在所有的实例上共享charAt(index) 返回值 cha...
    继续阅读 »

    六种基本数据类型

    undefined
    null
    string
    boolean
    number
    symbol(ES6)


    • 一种引用类型
    • Object

    string

    1. length属性
    2. prototype 添加的方法或属性在所有的实例上共享
    3. charAt(index) 返回值
    4.  charCodeAt(index) 返回字符的Unicode编码
    5.  indexOf(searchVal,index) 值所在的位置 param2是从位置开始算
    6. search()方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符
    var str = 'abcDEF'; 
    console.log(str.search('c')); //返回2
    console.log(str.search('d')); //返回-1
    console.log(str.search(/d/i)); //返回3


    Object

    1. 对象可以通过执行new操作符后跟要创建的对象类型的名称来创建。

    前端错误的分类

    即时运行错误:代码错误

    资源加载错误


    错误的捕获方式

    代码错误

    try...catch

    window.onerror


    资源错误

    object.onerror(不会冒泡到window):节点上绑定error事件

    performance.getEntries:获取资源的加载时长

    error的事件捕获:用捕获不用冒泡可以监控


    上报错误的基本原理

    1.ajax通讯方式上报

    2.image对象上报

    跨域的代码错误怎么捕获


    收起阅读 »

    前端必须要了解的一些知识 (十)

    任务队列同步任务异步任务console.log(1)setTimeout(){console.log(2)}console.log(3)1,3,22,3,5,4,1console.log(A)while(true){}console.log(B)//只输出A ...
    继续阅读 »
    任务队列
    同步任务
    异步任务
    console.log(1)
    setTimeout(){
    console.log(2)
    }
    console.log(3)
    1,3,2


    2,3,5,4,1

    console.log(A)
    while(true){
    }
    console.log(B)
    //只输出A while是个同步队列 。 进入死循环
    ----------------------------
    console.log(A)
    settimeout(){
    console.log(B)
    }
    while(true){
    }
    //仍然只输出A 。 同步没执行完不会执行异步
    -----------------------------
    for(var i=0;i<4;i++){
    settimeout(()=>{
    console.log(i)
    },1000)
    }
    //4次4



    eventloop
    异步:settimeout DOM事件 Promise
    将所有任务看成两个队列:执行队列与事件队列。
    执行队列是同步的,事件队列是异步的,宏任务放入事件列表,微任务放入执行队列之后,事件队列之前。
    当执行完同步代码之后,就会执行位于执行列表之后的微任务,然后再执行事件列表中的

    异步加载的方式
    1:动态脚本加载
    2:defer
    defer在html解析完才会执行,如果是多个,按照加载顺序依次执行
    3:async
    加载完后立即执行 。 如果是多个 。 执行顺序和加载顺序无关


    缓存分类
    1 强缓存
    如果两个时间都下发了 以后者为准
    expires:过期时间(绝对时间服务器的时间)
    cache-control 。 相对时间 。 拿到资源3600s之内不请求服务器
    2:协商缓存(查资料)
    last-modified




    dns-prefetch(记住)


    收起阅读 »

    前端必须要了解的一些知识 (九)

    CSRF跨站请求伪造攻击原理网站B引诱用户点击A防御措施token验证refere验证 来源验证隐藏令牌XSS跨域脚本攻击原理:提交区注入脚本 让js不能执行doctype的作用DTD 定义html文档类型 。 浏览器根据这个去解析 声明文档类型4.0版本...
    继续阅读 »
    CSRF
    跨站请求伪造
    攻击原理
    网站B引诱用户点击A

    防御措施
    token验证
    refere验证 来源验证
    隐藏令牌

    XSS
    跨域脚本攻击
    原理:提交区注入脚本 让js不能执行


    doctype的作用
    DTD 定义html文档类型 。 浏览器根据这个去解析 声明文档类型
    4.0版本有两个模式 。 一个严格模式 。 一个是传统模式

    浏览哎渲染过程


    重拍reflow


    重绘repaint
    如何避免最小避免repaint 。

    布局layout


    doctype的作用
    DTD 定义html文档类型 。 浏览器根据这个去解析 声明文档类型
    4.0版本有两个模式 。 一个严格模式 。 一个是传统模式

    浏览哎渲染过程


    重拍reflow


    重绘repaint
    如何避免最小避免repaint 。

    布局layout

    收起阅读 »

    前端必须要了解的一些知识 (八)

    什么是同源策略限制协议 域名 端口不是一个源的文档不能操作另一个源的文档限制如下:cookie localStrorage indexDB 无法获取DOM无法获得Ajax请求不能发送前后端如何通信Ajax//同源下的通讯websocket//不限制同源c...
    继续阅读 »
    什么是同源策略

    限制
    协议 域名 端口
    不是一个源的文档不能操作另一个源的文档
    限制如下:
    cookie localStrorage indexDB 无法获取
    DOM无法获得
    Ajax请求不能发送


    前后端如何通信
    Ajax//同源下的通讯
    websocket//不限制同源
    cors//支持跨域也支持同源


    如何创建Ajax(用原生)
    XMLHttpRequest对象的工作流程
    兼容性处理
    事件触发条件
    事件触发顺序


    跨域通讯的几种方式
    JSONP
    实现原理

    Hash
    hash改变页面不刷新 指url#以后的东西
    window.onhashchange


    postMessage
    h5新的标准

    Websocket
    不受同源限制


    CORS
    白话:支持跨域通讯的Ajax
    如果跨域浏览器会拦截 会在请求头上添加origin
    http://www.ruanyifeng.com/blog/2016/04/cors.html

    收起阅读 »

    前端必须要了解的一些知识 (七)

    创建对象又几种方法//第一种:字面量 var o1 = {name: 'o1'}; var o2 = new Object({name: 'o2'});//第二种 通过构造函数 var M = function (name) { this.name = na...
    继续阅读 »
    创建对象又几种方法
    //第一种:字面量
    var o1 = {name: 'o1'};
    var o2 = new Object({name: 'o2'});
    //第二种 通过构造函数
    var M = function (name) { this.name = name; };
    var o3 = new M('o3');
    //第三种 Object.create
    var p = {name: 'p'};
    var o4 = Object.create(p);



    o4.__proto__===p//true

    原型 构造函数 实例 原型链



    instanceof的原理


    严谨来判断用constructor
    instanceof 并不是很严谨


    new运算符背后的原理
    原理如下
    测试如下

    ----
    面向对象
    类与实例
    类的声明
    1,构造函数
    fn Animal(){
    this.name = 'name'
    }
    2,ES6class的声明
    class Animal2 {
    //↓↓构造函数
    constructor(){
    this.name=name
    }
    }
    实例化类
    console.log(new Animal(),new Animal2())



    类与继承
    继承的本质就是原型链
    继承有几种形式各种的优缺点不同点
    1借助构造函数实现继承
    fn parent1(){
    this.name= 'parent1'
    }
    parent1.prototype.say(){
    console.log('hello')
    }
    fn child1() {
    parent1.call(this)
    this.type='child1'
    }


    缺点 继承后parent1原型链上的东西 继承不say 并没有被继承 只能实现部分继承
    如果方法都在构造函数上就能继承
    2借助原型链实现继承
    fn parent2(){
    this.name= 'parent2'
    this.play=[1,2,3]
    }
    fn child2() {
    this.type='child2'
    }
    child2.prototype = new parent2()
    var s1 = new Child2();
    var s2 = new Child2();
    s1.play.push(4);


    缺点:如下 引用类型 不同的实例会全部变

    3组合方式
    前两种的方式的结合
    fn parent3(){
    this.name= 'parent3'
    this.play=[1,2,3]
    }
    fn child3() {
    parent3.call(this)
    this.type='child2'
    }
    child3.prototype = new parent3()
    var s3 = new Child3();
    var s4 = new Child3();
    s3.play.push(4);
    console.log(s3.play,s4.play)


    缺点:实例化的时候父构造函数执行了两次
    //优化方式
    fn parent4(){
    this.name= 'parent4'
    this.play=[1,2,3]
    }
    fn child4() {
    parent4.call(this)
    this.type='child4'


    }
    child4.prototype = parent4.prototype
    var s5 = new Child4();
    var s6 = new Child4();
    console.log(s5 instance child4,s5 instance parent4)//true true
    console.log(s5.constructor)//parent4
    缺点:区分不了s5是child4还是parent4的实例
    //优化方式
    fn parent5(){
    this.name= 'parent5'
    this.play=[1,2,3]
    }
    fn child5() {
    parent5.call(this)
    this.type='child5'
    }
    child5.prototype = Object.creat(parent5.prototype)
    //此时还是找不到 创建一下constructor可解决
    child5.prototype.constructor = child5
    var s7 = new Child5();
    console.log(s7 instance child4,s7 instance parent4)//true true
    console.log(s7.constructor)//child5


    收起阅读 »

    前端必须要了解的一些知识 (六)

    DOM事件的级别DOM0element.onclick=function(){}DOM1未制定事件相关的标准DOM2element.add('click',fn,false)/ie . atenchDOM3el.add('keyup',fn,false)增加了...
    继续阅读 »

    DOM事件的级别

    DOM0

    element.onclick=function(){}


    DOM1

    未制定事件相关的标准


    DOM2

    element.add('click',fn,false)/ie . atench

    DOM3

    el.add('keyup',fn,false)增加了其他事件除了click


    DOM事件的模型:捕获和冒泡



    DOM事件流

    三个j阶段

    捕获 。 目标阶段 。 冒泡阶段



    事件捕获的具体流程

    window=>document=>html=>body=>.....目标


    冒泡则相反


    event对象的常见应用

    event.preventDefalut . 阻止默认行为

    event.stopPropagation . 阻止冒泡

    event.stoplmmediatePropagation . 事件响应优先级

    事件代理

    event.currentTarget 当前绑定的事件的对象

    event.target 返回触发事件的元素


    currentTarget在事件流的捕获,冒泡阶段。只有当事件流处在冒泡阶段的时候,两个的指向才是一样的, 而当处于捕获和冒泡阶段的时候,

    target指向被单击的对象

    currentTarget指向当前事件活动的对象(一般为父级)。



    自定义事件

    let eve = new Event('eveName')/new CustomEvent可以加参数Obj

    //注册

    ev.addEventListener('eveName',fn)

    //触发

    ev.dispatchEvent(eve)


    HTTP

    http协议包括 :通用头域、请求消息、响应消息和主体信息。

    特点

    简单快速

    每个资源得url是固定得

    灵活


    无连接

    连接一次就会断掉

    无状态

    服务端不记录客户端连接得身份


    报文得组成部分

    请求报文

    请求行

    http方法

    页面地址

    http协议以及http版本

    请求头

    key value值告诉服务端我要哪些内容

    空行

    隔断

    请求体

    数据

    响应报文

    状态行

    协议 状态吗

    响应头

    key value

    空行

    隔断

    相应体

    数据

    http方法

    get 获取资源

    post 传输资源

    put 更新资源

    delete 删除资源

    HEAD 获取报文首部

    POST和GET区别(记住以下三个以上1,3,4,6,9)


    HTTP状态码



    持久链接

    http1.1版本支持

    管线化


    1. 管线化得特点和原理
    2. 请求和响应打包返回
    3. 持续连接完成后进行的且需要1.1版本的支持
    4. 管线化只有get和head可以进行 post有限制
    5. 管线化默认chrome和firefox默认不开启,初次连接的时候可能不支持,需要服务端的支持


    收起阅读 »

    前端必须要了解的一些知识 (五)

    盒模型标准模型和IE模型标准模型和IE模型的区别1计算宽度和高度的不同ie中content的宽度包括padding和border这两个属性css是如何设置这两种模型的border-box 是·ie默认 content-boxjs如何获取盒模型的宽和高四种方法1...
    继续阅读 »

    盒模型
    标准模型和IE模型


    标准模型和IE模型的区别
    1计算宽度和高度的不同
    ie中content的宽度包括padding和border这两个属性

    css是如何设置这两种模型的
    border-box 是·ie
    默认 content-box

    js如何获取盒模型的宽和高
    四种方法
    1.dom.style.width/height 只能获取行内样式
    2.dom.currentStyle.width/height只适合ie,兼容性问题
    3.window.getComputedStyle(dom).width/height可以准确获取//兼容性最好
    4.dom.getBoundingClientRect().width/height
    getBoundingClientRect()可以返回一个包含几个参数的对象,left,top,width,height.等。。盒模型距离viewport 左上角的距离。



    拔高
    解释边距重叠
    margin边距重叠取最大值


    引出BFC和IFC
    IFC在行内格式化上下文中,框(boxes)一个接一个地水平排列,起点是包含块的顶部。水平方向上的 marginborder和 padding在框之间得到保留。框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐。包含那些框的长方形区域,会形成一行,叫做行框。

    BFC的使用场景
     BFC:块级格式化上下文,它是指一个独立的块级渲染区域,只有Block-level BOX参与,该区域拥有一套渲染规则来约束块级盒子的布局,且与区域外部无关。
    BFC的生成
    既然上文提到BFC是一块渲染区域,那这块渲染区域到底在哪,它又是有多大,这些由生成BFC的元素决定,CSS2.1中规定满足下列CSS声明之一的元素便会生成BFC。
    • 根元素
    • float的值不为none
    • overflow的值不为visible
    • display的值为inline-block、table-cell、table-caption
    • position的值为absolute或fixed
      看到有道友文章中把display:table也认为可以生成BFC,其实这里的主要原因在于Table会默认生成一个匿名的table-cell,正是这个匿名的table-ccell生成了BFC
    BFC的约束规
    1. 内部的Box会在垂直方向上一个接一个的放置
    2. 垂直方向上的距离由margin决定。(完整的说法是:属于同一个BFC的两个相邻Box的margin会发生重叠,与方向无关。)
    3. 每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此。(这说明BFC中子元素不会超出他的包含块,而position为absolute的元素可以超出他的包含块边界)
    4. BFC的区域不会与float的元素区域重叠
    5. 计算BFC的高度时,浮动子元素也参与计算
    6. BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素,反之亦然

    清除浮动的四种方式及其原理理解
     利用clear样式

     父元素结束标签之前插入清除浮动的块级元素

    利用伪元素(clearfix)

    利用overflow清除浮动
    收起阅读 »

    前端必须要了解的一些知识 (四)

    基础方法1:浮动(延伸BFC)清除浮动后 浮动元素周边的元素处理的好的话 。 兼容性比较好2:绝对定位配合js的话 快捷缺点:脱离文档流3:flex比较完美的方案 。 解决以上的缺点4:表格布局兼容性特别好 ie缺点:。。5:网格布局 gride新的标准代码最...
    继续阅读 »



    基础方法

    1:浮动(延伸BFC)

    清除浮动后 浮动元素周边的元素处理的好的话 。 兼容性比较好

    2:绝对定位

    配合js的话 快捷

    缺点:脱离文档流

    3:flex

    比较完美的方案 。 解决以上的缺点

    4:表格布局

    兼容性特别好 ie

    缺点:。。

    5:网格布局 gride

    新的标准

    代码最简化哈


    拔高延续

    1:如过去掉高度已知 。 哪个不在好用?

    第三和第四能用

    2:竖起来

    3:两栏布局






    收起阅读 »

    前端必须要了解的一些知识 (三)

    你在下单时,要给后台发请求,后台通过拿到的参数请求微信后台去生成订单并同时返给你一个路径mweb_url,这个路径就是用来调起微信应用发起支付操作的。(这里要说明的就是由于h5支付不能主动回调,所以需要个主动查询的操作,这时会需要你又一个确认界面来进行主动查询...
    继续阅读 »

    你在下单时,要给后台发请求,后台通过拿到的参数请求微信后台去生成订单并同时返给你一个路径mweb_url,这个路径就是用来调起微信应用发起支付操作的。(这里要说明的就是由于h5支付不能主动回调,所以需要个主动查询的操作,这时会需要你又一个确认界面来进行主动查询订单状态。这里是个坑一会儿再说),调起支付界面之后进行支付操作,期间你什么都不用管,因为这都是微信的事。你需要的就是在你付完钱之后查看你的钱买你要的东西到底有没有成功(你要是不加的话,谁知道成功没,估计顾客会打死你,付完钱就茫然了,不知道到底钱到哪去了→_→)


    普通函数中的this:

    1. this总是代表它的直接调用者, 例如 obj.func ,那么func中的this就是obj

    2.在默认情况(非严格模式下,未使用 'use strict'),没找到直接调用者,则this指的是 window

    3.在严格模式下,没有直接调用者的函数中的this是 undefined

    4.使用call,apply,bind(ES5新增)绑定的,this指的是 绑定的对象

    箭头函数中的this

    默认指向在定义它时,它所处的对象,而不是执行时的对象, 定义它的时候,可能环境是window(即继承父级的this);

    下面通过一些例子来研究一下 this的一些使用场景


    call

    call(null, arr[0], arr[1], arr[2], arr[3], arr[4])//89


    1 dom有元素 页面不渲染

    首页 scoped 不加 导致引入的tab分类无法加载图片

    原因未知 此处感觉不太球对 瞎吉儿改的



    2:懒加载问题



    3.vue router

    repalce push go



    4css使图片置灰

    -webkit-filter: grayscale(100%); -moz-filter: grayscale(100%); -ms-filter: grayscale(100%); -o-filter: grayscale(100%); filter: grayscale(100%); filter: gray;





    收起阅读 »

    前端必须要了解的一些知识 (二)

    获取字符串长度 str.length分割字符串 str.split()拼接字符串 str1+str2 或 str1.concat(str2)替换字符串 str.replace(“玩游戏”,”好好学习”)提取子字符串 str.slice(start, end)或...
    继续阅读 »

    获取字符串长度 str.length

    分割字符串 str.split()

    拼接字符串 str1+str2 或 str1.concat(str2)

    替换字符串 str.replace(“玩游戏”,”好好学习”)

    提取子字符串 str.slice(start, end)或str.substring(start,end)或myStr.substr(start,length)

    切换字符串大小写 str.toLowerCase()和str.toUpperCase()

    比较字符串 str1.localeCompare(str2)

    匹配字符串 str.match(pattern)或pattern.exec(str)或str.search(pattern)

    根据位置查字符 str.charAt(index)

    根据位置字符Unicode编码 str.charCodeAt(index)

    根据字符查位置 str.indexOf(“you”)从左,myStr.lastIndexOf(“you”)从尾 或str.search(‘you’)

    原始数据类型转字符串 String(数据) 或利用加号

    字符串转原始数据类型 数字Number(”) // 0 布尔Boolean(”) // 0

    自己构建属性和方法 String.prototype.属性或方法= function(参数){代码}

    ----------

    箭头函数需要注意的地方

    *当要求动态上下文的时候,就不能够使用箭头函数,也就是this的固定化。

    1、在使用=>定义函数的时候,this的指向是定义时所在的对象,而不是使用时所在的对象;

    2、不能够用作构造函数,这就是说,不能够使用new命令,否则就会抛出一个错误;

    3、不能够使用arguments对象;

    4、不能使用yield命令;


    -------------------------

    let和const

     *let是更完美的var,不是全局变量,具有块级函数作用域,大多数情况不会发生变量提升。const定义常量值,不能够重新赋值,如果值是一个对象,可以改变对象里边的属性值。

    1、let声明的变量具有块级作用域

    2、let声明的变量不能通过window.变量名进行访问

    3、形如for(let x..)的循环是每次迭代都为x创建新的绑定


    依次输出的问题

    1:立即执行函数

    2:闭包

    3:let


    --------------------------------

    Set数据结构

    *es6方法,Set本身是一个构造函数,它类似于数组,但是成员值都是唯一的。

    --------------------------------

    -------------------------------------

    promise 案例较多 。 建议看代码

    http://www.cnblogs.com/fengxiongZz/p/8191503.html

    收起阅读 »

    前端必须要了解的一些知识 (一)

    常用apimoveTo(x0,y0):把当前画笔(ictx)移动到(x0,y0)这个位置。lineTo(x1,y1):从当前位置(x0,y0)处到(x1,y1)画一条直线。beginPath():开启一条路径或者重置当前路径。closePath():从当前点回...
    继续阅读 »

    常用api

    moveTo(x0,y0):把当前画笔(ictx)移动到(x0,y0)这个位置。

    lineTo(x1,y1):从当前位置(x0,y0)处到(x1,y1)画一条直线。

    beginPath():开启一条路径或者重置当前路径。

    closePath():从当前点回到路径起始点,也就是上一个beginPath的位置,回避和路径。

    stroke():绘制。必须加了这个函数才会画图,所以这个一定要放在最后。


    绘制一个圆形

    /获取Canvas对象(画布)

    var canvas = document.getElementById("myCanvas");

    //简单地检测当前浏览器是否支持Canvas对象,以免在一些不支持html5的浏览器中提示语法错误

    if(canvas.getContext){

    //获取对应的CanvasRenderingContext2D对象(画笔)

    var ctx = canvas.getContext("2d");

    //开始一个新的绘制路径

    ctx.beginPath();

    //设置弧线的颜色为蓝色

    ctx.strokeStyle = "blue";

    var circle = {

    x : 100, //圆心的x轴坐标值

    y : 100, //圆心的y轴坐标值

    r : 50 //圆的半径

    };

    //沿着坐标点(100,100)为圆心、半径为50px的圆的顺时针方向绘制弧线

    ctx.arc(circle.x, circle.y, circle.r, 0, Math.PI / 2, false);

    //按照指定的路径绘制弧线

    ctx.stroke();

    }

    ------

    深拷贝

    深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个

    1:不仅可拷贝数组还能拷贝对象(但不能拷贝函数)

    var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}] var new_arr = JSON.parse(JSON.stringify(arr)) console.log(new_arr);

    2:下面是深拷贝一个通用方法,实现思路:拷贝的时候判断属性值的类型,如果是对象,继续递归调用深拷贝函数

    var deepCopy = function(obj) {

    // 只拷贝对象

    if (typeof obj !== 'object') return;

    // 根据obj的类型判断是新建一个数组还是一个对象

    var newObj = obj instanceof Array ? [] : {};

    for (var key in obj) {

    // 遍历obj,并且判断是obj的属性才拷贝

    if (obj.hasOwnProperty(key)) {

    // 判断属性值的类型,如果是对象递归调用深拷贝

    newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];

    }

    }

    return newObj;

    }





    浅拷贝

    数组的浅拷贝,可用concat、slice返回一个新数组的特性来实现拷贝for in

    var arr = ['old', 1, true, null, undefined];

    var new_arr = arr.concat(); // 或者var new_arr = arr.slice()也是一样的效果;

    new_arr[0] = 'new';

    console.log(arr); // ["old", 1, true, null, undefined]

    console.log(new_arr); // ["new", 1, true, null, undefined]

    -------------------------------------------

    数组常用的方法

    map此方法是将数组中的每个元素调用一个提供的函数,结果作为一个新的数组返回,并没有改变原来的数组

    let arr = [1, 2, 3, 4, 5]

        let newArr = arr.map(x => x*2)

        //arr= [1, 2, 3, 4, 5]   原数组保持不变

        //newArr = [2, 4, 6, 8, 10] 返回新数组

    forEach此方法是将数组中的每个元素执行传进提供的函数,没有返回值,直接改变原数组,注意和map方法区分

    let arr = [1, 2, 3, 4, 5]

       num.forEach(x => x*2)

        // arr = [2, 4, 6, 8, 10]  数组改变,注意和map区分

    filter()此方法是将所有元素进行判断,将满足条件的元素作为一个新的数组返回

    let arr = [1, 2, 3, 4, 5]

         const isBigEnough => value => value >= 3

         let newArr = arr.filter(isBigEnough )

         //newNum = [3, 4, 5] 满足条件的元素返回为一个新的数组


    reduce()此方法是所有元素调用返回函数,返回值为最后结果,传入的值必须是函数类型:

    let arr = [1, 2, 3, 4, 5]

        const add = (a, b) => a + b

        let sum = arr.reduce(add)

        //sum = 15  相当于累加的效果

        与之相对应的还有一个 Array.reduceRight() 方法,区别是这个是从右向左操作的


    push/pop

    push:数组后面添加新元素,改变数组的长度

    pop:数组删除最后一个元素 。 也改变长度


    shift/unshift

    shift:删除第一个元素 。 改变数组的长度

    unshift:将一个或多个添加到数组开头 。 返回数组长度


    isArray:返回bool

    cancat:合并数组



    toString:数组转字符串

    join("--"):数组转字符串 。 间隔可以设置


    splice(开始位置,删除个数,元素)万能方法 增删改

    ------------------------------------------------

    判断是不是数组的方法

    var arr = [1,2,3,1];

    var arr2 = [{ abac : 1, abc : 2 }];

    function isArrayFn(value){

    if (typeof Array.isArray === "function") {

    //判断是否支持isArray ie8之前不支持

    return Array.isArray(value);

    }else{

    return Object.prototype.toString.call(value) === "[object Array]";

    }

    }

    alert(isArrayFn(arr));// true

    alert(isArrayFn(arr2));// true


    收起阅读 »

    前端面试常问的基础(七)

    1.IE6或更低版本最多20个cookie2.IE7和之后的版本最后可以有50个cookie。3.Firefox最多50个cookie4.chrome和Safari没有做硬性限制IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理coo...
    继续阅读 »

    1.IE6或更低版本最多20个cookie

    2.IE7和之后的版本最后可以有50个cookie。

    3.Firefox最多50个cookie

    4.chrome和Safari没有做硬性限制

    IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie。


    优点:极高的扩展性和可用性


    1.通过良好的编程,控制保存在cookie中的session对象的大小。

    2.通过加密和安全传输技术(SSL),减少cookie被破解的可能性。

    3.只在cookie中存放不敏感数据,即使被盗也不会有重大损失。

    4.控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。


    缺点:

    1.`Cookie`数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。


    2.安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。


    3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。


    在较高版本的浏览器中,js提供了sessionStorage和globalStorage。在HTML5中提供了localStorage来取代globalStorage。


    html5中的Web Storage包括了两种存储方式:sessionStorage和localStorage。


    sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。


    而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。




    收起阅读 »

    前端面试常问的基础(六)

    一、HTML5 CSS3CSS3有哪些新特性?1. CSS3实现圆角(border-radius),阴影(box-shadow),2. 对文字加特效(text-shadow、),线性渐变(gradient),旋转(transform)3.transform:r...
    继续阅读 »
    一、HTML5 CSS3
    1. CSS3有哪些新特性?
    1. CSS3实现圆角(border-radius),阴影(box-shadow),
    2. 对文字加特效(text-shadow、),线性渐变(gradient),旋转(transform)
    3.transform:rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg);// 旋转,缩放,定位,倾斜
    4. 增加了更多的CSS选择器  多背景 rgba
    5. 在CSS3中唯一引入的伪元素是 ::selection.
    6. 媒体查询,多栏布局
    7. border-image
    1. html5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5
    新特性:
    1. 拖拽释放(Drag and drop) API
    2. 语义化更好的内容标签(header,nav,footer,aside,article,section)
    3. 音频、视频API(audio,video)
    4. 画布(Canvas) API
    5. 地理(Geolocation) API
    6. 本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
    7. sessionStorage 的数据在浏览器关闭后自动删除
    8. 表单控件,calendar、date、time、email、url、search  
    9. 新的技术webworker, websocket, Geolocation
    移除的元素:
    1. 纯表现的元素:basefont,big,center,font, s,strike,tt,u;
    2. 对可用性产生负面影响的元素:frame,frameset,noframes;
    支持HTML5新标签:
    1. IE8/IE7/IE6支持通过 document.createElement 方法产生的标签,可以利用这一特性让这些浏览器支持 HTML5 新标签,浏览器支持新标签后,还需要添加标签默认的样式(当然最好的方式是直接使用成熟的框架、使用最多的是html5shim框架):
    <!--[if lt IE 9]>
    <script> src="http://html5shim.googlecode.com/svn/trunk/html5.js"</script>
    <![endif]-->
    如何区分:
    DOCTYPE声明新增的结构元素、功能元素
    1. 本地存储(Local Storage )和cookies(储存在用户本地终端上的数据)之间的区别是什么?
    Cookies:服务器和客户端都可以访问;大小只有4KB左右;有有效期,过期后将会删除;
    本地存储:只有本地浏览器端可访问数据,服务器不能访问本地存储直到故意通过POST或者GET的通道发送到服务器;每个域5MB;没有过期数据,它将保留知道用户从浏览器清除或者使用Javascript代码移除
    1. 如何实现浏览器内多个标签页之间的通信?
    调用 localstorge、cookies 等本地存储方式
    1. 你如何对网站的文件和资源进行优化?
    文件合并
    文件最小化/文件压缩
    使用CDN托管
    缓存的使用
    1. 什么是响应式设计?
    它是关于网页制作的过程中让不同的设备有不同的尺寸和不同的功能。响应式设计是让所有的人能在这些设备上让网站运行正常
    1. 新的 HTML5 文档类型和字符集是?
    答:HTML5文档类型:<!doctype html>
        HTML5使用的编码<meta charset=”UTF-8”>
    1. HTML5 Canvas 元素有什么用?
    答:Canvas 元素用于在网页上绘制图形,该元素标签强大之处在于可以直接在 HTML 上进行图形操作。
    1. HTML5 存储类型有什么区别?
    答:Media APIText Track APIApplication Cache APIUser InteractionData Transfer APICommand APIConstraint Validation APIHistory API
    1. H5+CSS3解决下导航栏最后一项掉下来的问题
    2. CSS3新增伪类有那些?
        p:first-of-type 选择属于其父元素的首个 <p> 元素的每个 <p> 元素。
        p:last-of-type  选择属于其父元素的最后 <p> 元素的每个 <p> 元素。
        p:only-of-type  选择属于其父元素唯一的 <p> 元素的每个 <p> 元素。
        p:only-child    选择属于其父元素的唯一子元素的每个 <p> 元素。
        p:nth-child(2)  选择属于其父元素的第二个子元素的每个 <p> 元素。
        :enabled、:disabled 控制表单控件的禁用状态。
    :checked,单选框或复选框被选中。               
    1. 请用CSS实现:一个矩形内容,有投影,有圆角,hover状态慢慢变透明。
    css属性的熟练程度和实践经验
    1. 描述下CSS3里实现元素动画的方法
    动画相关属性的熟悉程度
    1. html5\CSS3有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5
    HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,地理定位等功能的增加。
    * 绘画 canvas 元素
      用于媒介回放的 video 和 audio 元素
      本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
      sessionStorage 的数据在浏览器关闭后自动删除
      语意化更好的内容元素,比如 article、footer、header、nav、section
      表单控件,calendar、date、time、email、url、search
      CSS3实现圆角,阴影,对文字加特效,增加了更多的CSS选择器  多背景 rgba
      新的技术webworker, websockt, Geolocation
    移除的元素
    纯表现的元素:basefont,big,center,font, s,strike,tt,u;
    对可用性产生负面影响的元素:frame,frameset,noframes;
    * 是IE8/IE7/IE6支持通过document.createElement方法产生的标签,
      可以利用这一特性让这些浏览器支持HTML5新标签,
      浏览器支持新标签后,还需要添加标签默认的样式:
    * 当然最好的方式是直接使用成熟的框架、使用最多的是html5shim框架
    <!--[if lt IE 9]>
    <script> src="http://html5shim.googlecode.com/svn/trunk/html5.js"</script>
    <![endif]-->
    1. 你怎么来实现页面设计图,你认为前端应该如何高质量完成工作一个满屏 品 字布局 如何设计?
    * 首先划分成头部、body、脚部;。。。。。
    * 实现效果图是最基本的工作,精确到2px;
      与设计师,产品经理的沟通和项目的参与
      做好的页面结构,页面重构和用户体验
      处理hack,兼容、写出优美的代码格式
      针对服务器的优化、拥抱 HTML5。
    1. 你能描述一下渐进增强和优雅降级之间的不同吗?
    渐进增强 progressive enhancement:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
    优雅降级 graceful degradation:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
      区别:优雅降级是从复杂的现状开始,并试图减少用户体验的供给,而渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要。降级(功能衰减)意味着往回看;而渐进增强则意味着朝前看,同时保证其根基处于安全地带。 
      “优雅降级”观点
      “优雅降级”观点认为应该针对那些最高级、最完善的浏览器来设计网站。而将那些被认为“过时”或有功能缺失的浏览器下的测试工作安排在开发周期的最后阶段,并把测试对象限定为主流浏览器(如 IE、Mozilla 等)的前一个版本。
      在这种设计范例下,旧版的浏览器被认为仅能提供“简陋却无妨 (poor, but passable)” 的浏览体验。你可以做一些小的调整来适应某个特定的浏览器。但由于它们并非我们所关注的焦点,因此除了修复较大的错误之外,其它的差异将被直接忽略。
      “渐进增强”观点
      “渐进增强”观点则认为应关注于内容本身。
      内容是我们建立网站的诱因。有的网站展示它,有的则收集它,有的寻求,有的操作,还有的网站甚至会包含以上的种种,但相同点是它们全都涉及到内容。这使得“渐进增强”成为一种更为合理的设计范例。这也是它立即被 Yahoo! 所采纳并用以构建其“分级式浏览器支持 (Graded Browser Support)”策略的原因所在。
     
      那么问题了。现在产品经理看到IE6,7,8网页效果相对高版本现代浏览器少了很多圆角,阴影(CSS3),要求兼容(使用图片背景,放弃CSS3),你会如何说服他?
    1. 为什么利用多个域名来存储网站资源会更有效?
    CDN缓存更方便 
    突破浏览器并发限制 
    节约cookie带宽 
    节约主域名的连接数,优化页面响应速度 
    防止不必要的安全问题
    1. 请谈一下你对网页标准和标准制定机构重要性的理解。
      (无标准答案)网页标准和标准制定机构都是为了能让web发展的更‘健康’,开发者遵循统一的标准,降低开发难度,开发成本,SEO也会更好做,也不会因为滥用代码导致各种BUG、安全问题,最终提高网站易用性。
     
    1. 请描述一下cookies,sessionStorage和localStorage的区别?  
      sessionStorage用于本地存储一个会话(session)中的数据,这些数据只有在同一个会话中的页面才能访问并且当会话结束后数据也随之销毁。因此sessionStorage不是一种持久化的本地存储,仅仅是会话级别的存储。而localStorage用于持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
    web storagecookie的区别
    Web Storage的概念和cookie相似,区别是它是为了更大容量存储设计的。Cookie的大小是受限的,并且每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽,另外cookie还需要指定作用域,不可以跨域调用。
    除此之外,Web Storage拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookiegetCookie。但是Cookie也是不可以或缺的:Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地存储数据而生。
    1. 知道css有个content属性吗?有什么作用?有什么应用?
    知道。css的content属性专门应用在 before/after 伪元素上,用来插入生成内容。最常见的应用是利用伪类清除浮动。
    //一种常见利用伪类清除浮动的代码
    .clearfix:after {
        content:"."; //这里利用到了content属性
        display:block;
        height:0;
        visibility:hidden;
        clear:both; }
    .clearfix {
        *zoom:1;
    }
    after伪元素通过 content 在元素的后面生成了内容为一个点的块级素,再利用clear:both清除浮动。
      那么问题继续还有,知道css计数器(序列数字字符自动递增)吗?如何通过css content属性实现css计数器?
    答案:css计数器是通过设置counter-reset 、counter-increment 两个属性 、及 counter()/counters()一个方法配合after / before 伪类实现。 
    1. 如何在 HTML5 页面中嵌入音频?
    HTML 5 包含嵌入音频文件的标准方式,支持的格式包括 MP3、Wav 和 Ogg:
    <audio controls> 
      <source src="jamshed.mp3" type="audio/mpeg"> 
       Your browser does'nt support audio embedding feature. 
    </audio>
    1. 如何在 HTML5 页面中嵌入视频?
    和音频一样,HTML5 定义了嵌入视频的标准方法,支持的格式包括:MP4、WebM 和 Ogg:
    <video width="450" height="340" controls> 
      <source src="jamshed.mp4" type="video/mp4"> 
       Your browser does'nt support video embedding feature. 
    </video> 
    1. HTML5 引入什么新的表单属性?
    Datalist   datetime   output   keygen  date  month  week  time  number   range   emailurl
    1. CSS3新增伪类有那些?
     p:first-of-type 选择属于其父元素的首个 <p> 元素的每个 <p> 元素。
        p:last-of-type  选择属于其父元素的最后 <p> 元素的每个 <p> 元素。
        p:only-of-type  选择属于其父元素唯一的 <p> 元素的每个 <p> 元素。
        p:only-child    选择属于其父元素的唯一子元素的每个 <p> 元素。
        p:nth-child(2)  选择属于其父元素的第二个子元素的每个 <p> 元素。
        :enabled、:disabled 控制表单控件的禁用状态。
    :checked,单选框或复选框被选中。
    1. ()描述一段语义的html代码吧。
    HTML5中新增加的很多标签(如:<article>、<nav>、<header>和<footer>等)
    就是基于语义化设计原则)  
    < div id="header">
    < h1>标题< /h1>
    < h2>专注Web前端技术< /h2>
    < /div>
    语义 HTML 具有以下特性:
     
    文字包裹在元素中,用以反映内容。例如:
    段落包含在 <p> 元素中。
    顺序表包含在<ol>元素中。
    从其他来源引用的大型文字块包含在<blockquote>元素中。
    HTML 元素不能用作语义用途以外的其他目的。例如:
    <h1>包含标题,但并非用于放大文本。
    <blockquote>包含大段引述,但并非用于文本缩进。
    空白段落元素 ( <p></p> ) 并非用于跳行。
    文本并不直接包含任何样式信息。例如:
    不使用 <font> 或 <center> 等格式标记。
    类或 ID 中不引用颜色或位置。
    1. cookie在浏览器和服务器间来回传递。 sessionStoragelocalStorage区别
    sessionStorage和localStorage的存储空间更大;
    sessionStorage和localStorage有更多丰富易用的接口;
    sessionStorage和localStorage各自独立的存储空间;
    1. html5有哪些新特性、移除了那些元素?如何处理HTML5新标签的浏览器兼容问题?如何区分 HTML 和 HTML5
    * HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加。
    * 绘画 canvas  
      用于媒介回放的 video 和 audio 元素
      本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失;
      sessionStorage 的数据在浏览器关闭后自动删除
      语意化更好的内容元素,比如 article、footer、header、nav、section
      表单控件,calendar、date、time、email、url、search  
      新的技术webworker, websockt, Geolocation
    * 移除的元素
    纯表现的元素:basefont,big,center,font, s,strike,tt,u;
    对可用性产生负面影响的元素:frame,frameset,noframes;
    支持HTML5新标签:
    * IE8/IE7/IE6支持通过document.createElement方法产生的标签,
      可以利用这一特性让这些浏览器支持HTML5新标签,
      浏览器支持新标签后,还需要添加标签默认的样式:
    * 当然最好的方式是直接使用成熟的框架、使用最多的是html5shim框架
    <!--[if lt IE 9]>
    <script> src="http://html5shim.googlecode.com/svn/trunk/html5.js"</script>
    <![endif]-->
    1. 如何区分: DOCTYPE声明\新增的结构元素\功能元素
    2. 语义化的理解?
    用正确的标签做正确的事情!
    html语义化就是让页面的内容结构化,便于对浏览器、搜索引擎解析;
    在没有样式CCS情况下也以一种文档格式显示,并且是容易阅读的。
    搜索引擎的爬虫依赖于标记来确定上下文和各个关键字的权重,利于 SEO。
    使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解。
    1. HTML5的离线储存?
    localStorage    长期存储数据,浏览器关闭后数据不丢失;
    sessionStorage  数据在浏览器关闭后自动删除。
    1. 写出HTML5的文档声明方式
     
     <DOCYPE html>
    1. HTML5CSS3的新标签     
     
     
    HTML5: nav, footer, header, section, hgroup, video, time, canvas, audio...
    CSS3: RGBA, opacity, text-shadow, box-shadow, border-radius, border-image, 
    border-color, transform...;
    1. 自己对标签语义化的理解
        在我看来,语义化就是比如说一个段落, 那么我们就应该用 <p>标签来修饰,标题就应该用 <h?>标签等。符合文档语义的标签。
     

    收起阅读 »

    前端面试常问的基础(五)

    如何理解CSS的盒子模型?每个HTML元素都是长方形盒子。 (1)盒子模型有两种:IE盒子模型、标准W3C盒子模型;IE的content部分包含了border和pading。 (2)标准W3C盒模型包含:内容(content)、填充(padding)、边界(m...
    继续阅读 »

    如何理解CSS的盒子模型?

    每个HTML元素都是长方形盒子。 (1)盒子模型有两种:IE盒子模型、标准W3C盒子模型;IE的content部分包含了border和pading。 (2)标准W3C盒模型包含:内容(content)、填充(padding)、边界(margin)、边框(border)。



    link属于XHTML标签,而@import是CSS提供的。 (2)页面被加载时,link会同时被加载,而@import引用的CSS会等到页面被加载完再加载。 (3)import只在IE 5以上才能识别,而link是XHTML标签,无兼容问题。 (4)link方式的样式权重高于@import的权重。 (5)使用dom控制样式时的差别。当使用javascript控制dom去改变样式的时候,只能使用link标签,因为@import不是dom可以控制的。



    id选择器(# myid) 类选择器(.myclassname) 标签选择器(div、h1、p) 相邻选择器(h1 + p) 子选择器(ul < li) 后代选择器(li a) 通配符选择器( * ) 属性选择器(a[rel = "external"]) 伪类选择器(a: hover, li: nth - child) 可继承: font-size font-family color, UL LI DL DD DT;

    不可继承 :border padding margin width height ;

    优先级就近原则,样式定义最近者为准,载入样式以最后载入的定位为准。 优先级为: !important > id > class > tag important 比 内联优先级高 CSS3新增伪类举例: p:first-of-type 选择属于其父元素的首个<p>元素的每个<p>元素。 p:last-of-type 选择属于其父元素的最后<p>元素的每个<p>元素。 p:only-of-type 选择属于其父元素唯一的<p>元素的每个<p>元素。 p:only-child 选择属于其父元素的唯一子元素的每个<p>元素。 p:nth-child(2) 选择属于其父元素的第二个子元素的每个<p>元素。 :enabled、:disabled 控制表单控件的禁用状态。 :checked 单选框或复选框被选中。




    (1)png24为的图片在IE6浏览器上出现背景,解决方案是做成PNG8。

    (2)浏览器默认的margin和padding不同,解决方案是加一个全局的*{margin:0;padding:0;}来统一。

    (3)IE6双边距bug:块属性标签float后,又有横行的margin情况下,在IE 6显示margin比设置的大。

    (4)浮动ie产生的双边距问题:块级元素就加display:inline;行内元素转块级元素display:inline后面再加display:table。 .bb{

    background-color:#f1ee18; /*所有识别*/

    .background-color:#00deff\9; /*IE6、7、8识别*/

    +background-color:#a200ff; /*IE6、7识别*/

    _background-color:#1e0bd1; /*IE6识别*/ }


    BFC,块级格式化上下文,一个创建了新的BFC的盒子是独立布局的,盒子里面的子元素的样式不会影响到外面的元素。在同一个 BFC 中的两个毗邻的块级盒在垂直方向(和布局方向有关系)的 margin 会发生折叠。


    W3C CSS 2.1 规范中的一个概念,它决定了元素如何对其内容进行布局,以及与其他元素的关系和相互作用。

    display:none  隐藏对应的元素,在文档布局中不再给它分配空间,它各边的元素会合拢,

    就当他从来不存在。


    visibility:hidden  隐藏对应的元素,但是在文档布局中仍保留原来的空间。

    Web Storage的概念和cookie相似,区别是它是为了更大容量存储设计的。Cookie的大小是受限的,并且每次你请求一个新的页面的时候Cookie都会被发送过去,这样无形中浪费了带宽,另外cookie还需要指定作用域,不可以跨域调用。


    除此之外,Web Storage拥有setItem,getItem,removeItem,clear等方法,不像cookie需要前端开发者自己封装setCookie,getCookie。


    但是Cookie也是不可以或缺的:Cookie的作用是与服务器进行交互,作为HTTP规范的一部分而存在 ,而Web Storage仅仅是为了在本地“存储”数据而生


    浏览器的支持除了IE7及以下不支持外,其他标准浏览器都完全支持(ie及FF需在web服务器里运行),值得一提的是IE总是办好事,例如IE7、IE6中的UserData其实就是javascript本地存储的解决方案。通过简单的代码封装可以统一到所有的浏览器都支持web storage。


    localStorage和sessionStorage都具有相同的操作方法,例如setItem、getItem和removeItem等






    收起阅读 »

    前端面试常问的基础(四)

    将元素定义为网格容器,并为其内容建立新的 网格格式上下文。值:grid :生成一个块级网格inline-grid :生成一个内联网格在Bootstrap中,栅格系统将容器均分为12份,再调整内外边距,结合媒体查询,造就了这一强大的...
    继续阅读 »

    将元素定义为网格容器,并为其内容建立新的 网格格式上下文。

    值:

    • grid :生成一个块级网格
    • inline-grid :生成一个内联网格



    在Bootstrap中,栅格系统将容器均分为12份,再调整内外边距,结合媒体查询,造就了这一强大的栅格系统。


    flex布局

    水平居中:

    1. 行内元素,父元素 text-align : center

    2. 块级元素有定宽,margin:0 auto;

    3. 块级元素绝对定位,transform : translate(-50%,0);

    4. 块级元素绝对定位,并且知道宽度, margin-left: - 宽度的一半            

    5. 块级元素绝对定位,left:0;  right:0; margin:0 auto


    垂直居中

    1. 若元素是单行文本, 则可设置 line-height 等于父元素高度,原理见上面;

    2. 若元素是行内块级元素, 基本思想是使用display: inline-block, vertical-align: middle和一个伪元素让内容块处于容器中央..parent::after, .son{ display:inline-block; vertical-align:middle; }

    3. 使用flex, 在父元素上面添加.parent { display: flex; align-items: center;

    4. 绝对定位的块用 transform: translate(0, -50%)

    5. 绝对定位,并且有定高, margin-top : -高度的一半          注意不要用 margin-bottom,  不会生效的

    6. 设置父元素相对定位(position:relative), 子元素如下css样式:.son{ position:absolute; height:固定; top:0; bottom:0; margin:auto 0; } 



    重绘重排

    重绘是一个元素的外观变化所引发的浏览器行为;

    重排是引起DOM树重新计算的行为;


    1、回流/重排


    渲染树的一部分必须要更新且节点的尺寸发生了变化,会触发重排操作。每个页面至少在初始化的时候会有一次重排操作。


    2、重绘


    部分节点需要更新,但没有改变其形状,会触发重绘操作。




    会触发重绘或回流/重排的操作


    1、添加、删除元素(回流+重绘)


    2、隐藏元素,display:none(回流+重绘),visibility:hidden(只重绘,不回流)


    3、移动元素,如改变top、left或移动元素到另外1个父元素中(重绘+回流)


    4、改变浏览器大小(回流+重绘)


    5、改变浏览器的字体大小(回流+重绘)


    6、改变元素的padding、border、margin(回流+重绘)


    7、改变浏览器的字体颜色(只重绘,不回流)


    8、改变元素的背景颜色(只重绘,不回流)


    深入浏览器理解CSS animations 和 transitions的性能问题


    GPU的快在于:

    1. 绘制位图到屏幕上
    2. 一遍又一遍地绘制相同的位图
    3. 将同一位图绘制到不同位置,执行旋转以及缩放处理

    GPU 的慢在于:

    1. 将位图加载到它的内存中

    在使用height,width,margin,padding作为transition的值时,会造成浏览器主线程的工作量较重,例如从margin-left:-20px渲染到margin-left:0,主线程需要计算样式margin-left:-19px,margin-left:-18px,一直到margin-left:0,而且每一次主线程计算样式后,合成进程都需要绘制到GPU然后再渲染到屏幕上,前后总共进行20次主线程渲染,20次合成线程渲染,20+20次,总计40次计算。


    在使用css3 transtion做动画效果时,优先选择transform,尽量不要使用height,width,margin和padding。

    transform为我们提供了丰富的api,例如scale,translate,rotate等等,但是在使用时需要考虑兼容性。但其实对于大多数css3来说,mobile端支持性较好,desktop端支持性需要格外注意。


    物理像素(physical pixel) 

    即:设备像素(device pixel)。 

    本质是屏幕上的点,这个是跟设备有关系

    CSS像素(css pixel) 

    指的是CSS样式代码中使用的逻辑像素(或者叫虚拟像素)。 

    软件要在设备上显示,css规定了长度单位(绝对单位和相对单位),比如:px 是一个 相对单位 ,相对的是 物理像素(physical pixel)

    设备像素比(device pixel ratio) dpr 

    公式:物理像素数(硬件) / 逻辑像素数(软件),即(物理像素/CSS像素)。 

    在css中,可以通过 -webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio 和 -webkit-max-device-pixel-ratio 进行媒体查询,对不同dpr的设备,做一些样式适配。 

    如: dpr = 2 时,1个CSS像素 = 4个物理像素。因为像素点都是正方形,所以当1个CSS像素需要的物理像素增多2倍时,其实就是长和宽都增加了2倍 


    px em rem的区别

    PX实际上就是像素,用PX设置字体大小时,比较稳定和精确。但是这种方法存在一个问题,当用户在浏览器中浏览我们制作的Web页面时,如果改变了浏览器的缩放,这时会使用我们的Web页面布局被打破。这样对于那些关心自己网站可用性的用户来说,就是一个大问题了。因此,这时就提出了使用“em”来定义Web页面的字体。


    EM就是根据基准来缩放字体的大小。EM实质是一个相对值,而非具体的数值。这种技术需要一个参考点,一般都是以<body>的“font-size”为基准。如WordPress官方主题Twenntytwelve的基准就是14px=1em。

    另外,em是相对于父元素的属性而计算的,如果想计算px和em之间的换算,输入数据就可以px和em相互计算。

    Rem是相对于根元素<html>,这样就意味着,我们只需要在根元素确定一个参考值。







    收起阅读 »

    前端面试常问的基础(三)

     JS中浮点数精度误差解决如果有精度要求,可以用toFixed方法处理通用处理方案:把需要计算的数字乘以 10 的 n 次幂,换算成计算机能够精确识别的整数,然后再除以 10 的 n 次幂promises,observables,generator ...
    继续阅读 »
     

     new 运算符是用来实例化一个类,从而在内存中分配一个实例对象。

     通过new可以产生原对象的一个实例对象,而这个实例对象继承了原对象的属性和方法。因此, new存在的意义在于它实现了javascript中的继承,而不仅仅是实例化了一个对象!

    • JavaScript 中有哪些不同的函数调用模式? 详细解释。 提示:有四种模式,函数调用,方法调用,.call() 和 .apply()。
    1. 函数模式 fn()
    2. 方法模式 a.fn()
    3. 构造器模式 new
    4. 上下文模式 call apply

    /*apply()方法*/两个参数 function.apply(thisObj[, argArray]) /*call()方法*/多个参数 function.call(thisObj[, arg1[, arg2[, [,...argN]]]]);


    • 新 ECMAScript 提案

    https://www.cnblogs.com/fundebug/p/what-is-new-in-javascript-for-2019.html

    bigint

    class 增加静态方法和属性 私有属性和方法


    symbol值在序列化的过程中会被忽略或被转换成null



    Fetch API 相对于传统的 Ajax 有哪些改进?

    改进:promise 风格的api,async/await方式调用更友好,更简洁,错误处理更直观

    缺点/难点:

    • fetch 是一种底层的 api,json传值必须转换成字符串,并且设置content-Type为application/json
    • fetch 默认情况下不会发送 cookie
    • 无法获取progress,也就是说无法用fetch做出有进度条的请求
    • 不能中断,我们知道传统的xhr是可以通过调用abort方法来终止我们的请求的

    其实javasript的社区一直很活跃,相信上述问题很快会在未来的更新中解决


    收起阅读 »

    前端面试常问的基础(二)

    1. 一个程序至少有一个进程,一个进程至少有一个线程2. 线程的划分尺度小于进程,使得多线程程序的并发性高3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率4. 线程在执行过程中与进程还是有区别的。每个独立的线程...
    继续阅读 »

    1. 一个程序至少有一个进程,一个进程至少有一个线程

    2. 线程的划分尺度小于进程,使得多线程程序的并发性高

    3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率

    4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制 

    5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别

    ------------

    1.IE6或更低版本最多20个cookie

    2.IE7和之后的版本最后可以有50个cookie。

    3.Firefox最多50个cookie

    4.chrome和Safari没有做硬性限制

    IE和Opera 会清理近期最少使用的cookie,Firefox会随机清理cookie。


    优点:极高的扩展性和可用性


    1.通过良好的编程,控制保存在cookie中的session对象的大小。

    2.通过加密和安全传输技术(SSL),减少cookie被破解的可能性。

    3.只在cookie中存放不敏感数据,即使被盗也不会有重大损失。

    4.控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。


    缺点:

    1.`Cookie`数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。


    2.安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。


    3.有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。


    本文链接:https://blog.csdn.net/kincaid_z/article/details/116530326

    待完善

    收起阅读 »

    前端面试常问的基础(一)

     IE 盒子模型、标准 W3C 盒子模型;IE的content部分包含了 border 和 padding;new操作符具体干了什么呢?1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型2. 属性和方法被加入到 this ...
    继续阅读 »

     IE 盒子模型、标准 W3C 盒子模型;IE的content部分包含了 border 和 padding;


    new操作符具体干了什么呢?

    1. 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型

    2. 属性和方法被加入到 this 引用的对象中

    3. 新创建的对象由 this 所引用,并且最后隐式的返回 this


    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。它是基于JavaScript的一个子集。数据格式简单, 易于读写, 占用带宽小


    内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。

    垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的惟一引用是循环的,那么该对象的内存即可回收。

    1. setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。

    2. 闭包

    3. 控制台日志

    4. 循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)


    一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?

    分为4个步骤:

    1. 当发送一个 URL 请求时,不管这个 URL 是 Web 页面的 URL 还是 Web 页面上每个资源的 URL,浏览器都会开启一个线程来处理这个请求,同时在远程 DNS 服务器上启动一个 DNS 查询。这能使浏览器获得请求对应的 IP 地址。

    2. 浏览器与远程 Web 服务器通过 TCP 三次握手协商来建立一个 TCP/IP 连接。该握手包括一个同步报文,一个同步-应答报文和一个应答报文,这三个报文在 浏览器和服务器之间传递。该握手首先由客户端尝试建立起通信,而后服务器应答并接受客户端的请求,最后由客户端发出该请求已经被接受的报文。

    3. 一旦 TCP/IP 连接建立,浏览器会通过该连接向远程服务器发送 HTTP 的 GET 请求。远程服务器找到资源并使用 HTTP 响应返回该资源,值为 200 的 HTTP 响应状态表示一个正确的响应。

    4. 此时,Web 服务器提供资源服务,客户端开始下载资源。


    GET:一般用于信息获取,使用URL传递参数,对所发送信息的数量也有限制,一般在2000个字符

    POST:一般用于修改服务器上的资源,对所发送的信息没有限制


    Ajax 同步和异步的区别:

    1. 同步:提交请求 -> 等待服务器处理 -> 处理完毕返回,这个期间客户端浏览器不能干任何事

    2. 异步:请求通过事件触发 -> 服务器处理(这是浏览器仍然可以作其他事情)-> 处理完毕


    js数组去重

    [1,1,2,2,3,3,3,3].filter(function(elem, index, self) {

    ///结果是true的时候返回后面的值

        return index == self.indexOf(elem);

    })


    1. XSS

    2. sql注入

    3. CSRF:是跨站请求伪造,很明显根据刚刚的解释,他的核心也就是请求伪造,通过伪造身份提交POST和GET请求来进行跨域的攻击


    完成CSRF需要两个步骤:

    1. 登陆受信任的网站A,在本地生成 COOKIE

    2. 在不登出A的情况下,或者本地 COOKIE 没有过期的情况下,访问危险网站B。



    2.HTTP 报文的组成部分

    请求报文

    1.请求行:http方法、页面地址、协议、版本

    2.请求头:key、value告诉服务端需要内容,注意什么类型

    3.空行:告诉服务端请求头已经结束

    4.请求体

    响应报文

    1.状态行:协议、版本、状态码

    2.响应头

    3.空行

    4.响应体:文档部分


    • TCP/IP 四层协议: 应用层、传输层、网络互连层和主机到网络层. http对应应用层
    • ISO 七层模型: 物理层, 数据链路层, 网络层, 传输层, 会话层, 表示层, 应用层.  http对应应用



    流行的一些东西:

    1. Node.js

    2. Mongodb

    3. npm

    4. MVVM

    5. MEAN

    6. three.js

    7. React

    本文链接:https://blog.csdn.net/kincaid_z/article/details/116530326


    收起阅读 »

    解决js精度丢失办法

    很简单一个问题,0.1+0.2,我们肉眼可见的算出来等于0.3,但js是一个神奇的语言,我们在控制台输入0.1+0.2等于0.30000000000000004,为什么会这样尼,我百度了了一下,原因如下:JavaScript 中所有数字包括整数和小数都只有一种...
    继续阅读 »

    很简单一个问题,0.1+0.2,我们肉眼可见的算出来等于0.3,但js是一个神奇的语言,我们在控制台输入0.1+0.2等于0.30000000000000004,为什么会这样尼,我百度了了一下,原因如下:

    JavaScript 中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。0.1的二进制表示的是一个无限循环小数,该版本的 JS 采用的是浮点数标准需要对这种无限循环的二进制进行截取,从而导致了精度丢失,造成了0.1不再是0.1,截取之后0.1变成了 0.100…001,0.2变成了0.200…002。所以两者相加的数大于0.3。

    原因就是这么个奇葩,做需求的时候涉及到数字计算,那就得解决它老人家这个毛病,解决这个问题,我一般会封装成一个文件,到后面需要的地方可以模块化引入,并使用

    1.判断obj是否为一个整数

    export const  isInteger = (obj) => {
    return Math.floor(obj) === obj //向下取整就是为了让整数部分截取下来不变
    }

    2.将一个浮点数转成整数,返回整数和倍数

    比如:3.14 -->314,倍数是 100 ,floatNum {number} 小数,返回一个对象, {times:100, num: 314}

    export const toInteger = (floatNum) => {
    var ret = {times: 1, num: 0};
    if (isInteger(floatNum)) {
    ret.num = floatNum;
    return ret
    }
    //1.//转字符串
    var strfi = floatNum + '';
    //2.//拿到小数点为
    var dotPos = strfi.indexOf('.');
    //3. //截取需要的长度
    var len = strfi.substr(dotPos + 1).length;
    //4.倍数就是长度的幂
    var times = Math.pow(10, len);
    var intNum = parseInt(floatNum * times , 10);
    ret.times = times;
    ret.num = intNum;
    return ret
    }

    3.把小数放大为整数(乘),进行算术运算,再缩小为小数(除)

    1. 参数:a {number} 运算数1
    2. b:{number} 运算数2,
    3. op {string} 运算类型,有加减乘除(add/subtract/multiply/divide)
    export const operation = (a, b, op) => {
    var o1 = toInteger(a);
    var o2 = toInteger(b);
    var n1 = o1.num;
    var n2 = o2.num;
    var t1 = o1.times;
    var t2 = o2.times;
    var max = t1 > t2 ? t1 : t2;
    var result = null;
    switch (op) {
    case 'add':
    if (t1 === t2) { // 两个小数位数相同
    result = n1 + n2
    } else if (t1 > t2) { // o1 小数位 大于 o2
    result = n1 + n2 * (t1 / t2)
    } else { // o1 小数位 小于 o2
    result = n1 * (t2 / t1) + n2
    }
    return result / max;
    case 'subtract':
    if (t1 === t2) {
    result = n1 - n2
    } else if (t1 > t2) {
    result = n1 - n2 * (t1 / t2)
    } else {
    result = n1 * (t2 / t1) - n2
    }
    return result / max;
    case 'multiply':
    result = (n1 * n2) / (t1 * t2);
    return result;
    case 'divide':
    result = (n1 / n2) * (t2 / t1);
    return result
    }
    }

    原文:https://segmentfault.com/a/1190000022730047

    收起阅读 »

    ES6 exports 与 import 使用

    在创建JavaScript模块时,export 用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import使用它们。被导出的绑定值依然可以在本地进行修改。在使用import 进行导入时,这些绑定值只能被导入模块所读取,...
    继续阅读 »

    在创建JavaScript模块时,export 用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import使用它们。
    被导出的绑定值依然可以在本地进行修改。
    在使用import 进行导入时,这些绑定值只能被导入模块所读取,但在 export 导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。

    exports

    ES6模块只支持静态导出,只可以在模块的最外层作用域使用export,不可在条件语句与函数作用域中使用。

    Named exports (命名导出)

    这种方式主要用于导出多个函数或者变量, 明确知道导出的变量名称。
    使用:只需要在变量或函数前面加 export 关键字即可。
    使用场景:比如 utils、tools、common 之类的工具类函数集,或者全站统一变量

    1. export 后面不可以是表达式,因为表达式只有值,没有名字。
    2. 每个模块包含任意数量的导出。
    // lib.js
    export const sqrt = Math.sqrt;
    export function square(x) {
    return x * x;
    }
    export function diag(x, y) {
    return sqrt(square(x) + square(y));
    }


    // index.js 使用方式1
    import { square, diag } from 'lib';
    console.log(square(11)); // 121

    // index.js 使用方式2
    import * as lib from 'lib';
    console.log(lib.square(11)); // 121

    简写格式,统一列出需要输出的变量,例如上面的lib.js可以改写成

    // lib.js
    const sqrt = Math.sqrt;
    function square(x) {
    return x * x;
    }
    function add (x, y) {
    return x + y;
    }
    export { sqrt, square, add };

    Default exports (默认导出)

    这种方式主要用于导出类文件或一个功能比较单一的函数文件;
    使用:只需要在变量或函数前面加 export default 关键字即可。

    1. 每个模块最多只能有一个默认导出;
    2. 默认导出可以视为名字是default的模块输出变量;
    3. 默认导出后面可以是表达式,因为它只需要值。

    导出一个值:

    export default 123;

    导出一个函数:

    // myFunc.js
    export default function () { ... };

    // index.js
    import myFunc from 'myFunc';
    myFunc();

    导出一个类:

    // MyClass.js
    class MyClass{
    constructor() {}
    }
    export default MyClass;
    // 或者
    export { MyClass as default, … };

    // index.js
    import MyClass from 'MyClass';

    export default 与 export 的区别:

    • 不需要知道导出的具体变量名;
    • 导入【import】时不需要 { } 包裹;

    Combinations exports (混合导出)

    混合导出是 Named exports 和 Default exports 组合导出。

    混合导出后,默认导入一定放在命名导入前面;
    // lib.js
    export const myValue = '';
    export const MY_CONST = '';
    export function myFunc() {
    ...
    }
    export function* myGeneratorFunc() {
    ...
    }
    export default class MyClass {
    ...
    }

    // index.js
    import MyClass, { myValue, myFunc } from 'lib';

    Re-exporting (别名导出)

    一般情况下,export 导出的变量名是原文件中的变量名,但也可以用 as 关键字来指定别名。这样做是为了简化或者语义化 export 的函数名。

    同一个变量允许使用不同名字输出多次
    // lib.js
    function getName() {
    ...
    };
    function setName() {
    ...
    };

    export {
    getName as get,
    getName as getUserName,
    setName as set
    }

    Module Redirects (中转模块导出)

    为了方便使用模块导入,在一个父模块中“导入-导出”不同模块。简单来说:创建单个模块,集中多个模块的多个导出。
    使用:使用 export from 语法实现;

    export * from 'lib'; // 没有设置 export default
    export * as myFunc2 from 'myFunc'; // 【ES2021】没有设置 export default
    export { default as function1, function2 } from 'bar.js';

    上述例子联合使用导入和导出:

    import { default as function1, function2 } from 'bar.js';
    export { function1, function2 };

    尽管此时 export 与 import 等效,但以下语法在语法上无效:

    import DefaultExport from 'bar.js'; // 有效的
    export DefaultExport from 'bar.js'; // 无效的

    正确的做法是重命名这个导出:

    export { default as DefaultExport } from 'bar.js';

    Importing

    // Named imports
    import { foo, bar as b } from './some-module.mjs';

    // Namespace import
    import * as someModule from './some-module.mjs';

    // Default import
    import someModule from './some-module.mjs';

    // Combinations:
    import someModule, * as someModule from './some-module.mjs';
    import someModule, { foo, bar as b } from './some-module.mjs';

    // Empty import (for modules with side effects)
    import './some-module.mjs';


    原文:https://segmentfault.com/a/1190000039957496

    收起阅读 »

    常见的8个前端防御性编程方案

    关于前端防御性编程我们大多数情况可能遇到过,后端的由于同时请求人数过多,或者数据量过大,又或者是因为异常导致服务异常,接口请求失败,然后前端出现白屏或者报错还有一种情况,是前端自身写的代码存在一些缺陷,整个系统不够健壮,从而会出现白屏,或者业务系统异常,用户误...
    继续阅读 »

    关于前端防御性编程

    • 我们大多数情况可能遇到过,后端的由于同时请求人数过多,或者数据量过大,又或者是因为异常导致服务异常,接口请求失败,然后前端出现白屏或者报错
    • 还有一种情况,是前端自身写的代码存在一些缺陷,整个系统不够健壮,从而会出现白屏,或者业务系统异常,用户误操作等
    • 那么,就出现了前端防御性编程

    常见的问题和防范

    1.最常见的问题:
    uncaught TypeError: Cannot read property 'c' of undefined

    出现这个问题最根本原因是:

    当我们初始化一个对象obj为{}时候,obj.a这个时候是undefined.我们打印obj.a可以得到undefined,但是我们打印obj.a.c的时候,就会出现上面的错误。js对象中的未初始化属性值是undefined,从undefined读取属性就会导致这个错误(同理,null也一样)

    如何避免?

    js和ts目前都出现了一个可选链概念,例如:

    const obj = {};
    console.log(obj?.b?.c?.d)
    上面的代码并不会报错,原因是?.遇到是空值的时候便会返回undefined.
    2.前端接口层面的错误机制捕获

    前端的接口调用,一般都比较频繁,我们这时候可以考虑使用单例模式,将所有的axios请求都用一个函数封装一层。统一可以在这个函数中catch捕获接口调用时候的未知错误,伪代码如下:

    function ajax(url,data,method='get'){
    const promise = axios[method](url,data)
    return promise.then(res=>{
    }).catch(error){
    //统一处理错误
    }
    }

    那么只要发生接口调用的未知错误都会在这里被处理了

    3.错误边界(Error Boundaries,前端出现未知错误时,展示预先设定的UI界面)

    以React为例

    部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。

    错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

    使用示例:

    class ErrorBoundary extends React.Component {
    constructor(props) {
    super(props);
    this.state = { hasError: false };
    }

    static getDerivedStateFromError(error) {
    // 更新 state 使下一次渲染能够显示降级后的 UI
    return { hasError: true };
    }

    componentDidCatch(error, errorInfo) {
    // 你同样可以将错误日志上报给服务器
    logErrorToMyService(error, errorInfo);
    }

    render() {
    if (this.state.hasError) {
    // 你可以自定义降级后的 UI 并渲染
    return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
    }
    }
    注意
    • 错误边界无法捕获以下场景中产生的错误:

      • 事件处理(了解更多)
      • 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
      • 服务端渲染
      • 它自身抛出来的错误(并非它的子组件)
    4.前端复杂异步场景导致的错误
    • 这个问题可能远不止这么简单,但是大道至简,遵循单向数据流的方式去改变数据,例如:

    • //test.js
      export const obj = {
      a:1,
      b:2
      }

      //使用obj
      import {obj} from './test.js';
      obj.a=3;

      当你频繁使用这个obj对象时,你无法根据代码去知道它的改变顺序(即在某个时刻它的值是什么),而且这里面可能存在不少异步的代码,当我们换一种方式,就能知道它的改变顺序了,也更方便我们debug

      例如://test.js

      export const obj = {
      a:1,
      b:2
      }
      export function setObj (key,value) {
      console.log(key,value)
      obj[key] = value
      }
      这样,我们就做到了
      5.前端专注“前端”
      • 对于一些敏感数据,例如登录态,鉴权相关的。前端应该是尽量做无感知的转发、携带(这样也不会出现安全问题)
      6.页面做到可降级
      • 对于一些刚上新的业务,或者有存在风险的业务模块,或者会调取不受信任的接口,例如第三方的接口,这个时候就要做一层降级处理,例如接口调用失败后,剔除对应模块的展示,让用户无感知的使用
      7.巧用loading和disabled
      • 用户操作后,要及时loading和disabled确保不让用户进行重复,防止业务侧出现bug

      8.慎用innerHTML

      • 容易出现安全漏洞,例如接口返回了一段JavaScript脚本,那么就会立即执行。此时脚本如果是恶意的,那么就会出现不可预知的后果,特别是电商行业,尤其要注意


    收起阅读 »

    嗨,你真的懂this吗?

    this关键字是JavaScript中最复杂的机制之一,是一个特别的关键字,被自动定义在所有函数的作用域中,但是相信很多JsvaScript开发者并不是非常清楚它究竟指向的是什么。听说你很懂this,是真的吗?请先回答第一个问题:如何准确判断this指向的是什...
    继续阅读 »

    this关键字是JavaScript中最复杂的机制之一,是一个特别的关键字,被自动定义在所有函数的作用域中,但是相信很多JsvaScript开发者并不是非常清楚它究竟指向的是什么。听说你很懂this,是真的吗?

    请先回答第一个问题:如何准确判断this指向的是什么?【面试的高频问题】


    【图片来源于网络,侵删】

    再看一道题,控制台打印出来的值是什么?【浏览器运行环境】

    var number = 5;
    var obj = {
    number: 3,
    fn1: (function () {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function () {
    var num = this.number;
    this.number *= 2;
    console.log(num);
    number *= 3;
    console.log(number);
    }
    })()
    }
    var fn1 = obj.fn1;
    fn1.call(null);
    obj.fn1();
    console.log(window.number);

    如果你思考出来的结果,与在浏览中执行结果相同,并且每一步的依据都非常清楚,那么,你可以选择继续往下阅读,或者关闭本网页,愉快得去玩耍。如果你有一部分是靠蒙的,或者对自己的答案并不那么确定,那么请继续往下阅读。

    毕竟花一两个小时的时间,把this彻底搞明白,是一件很值得事情,不是吗?

    本文将细致得讲解this的绑定规则,并在最后剖析前文两道题。

    为什么要学习this?

    首先,我们为什么要学习this?

    1. this使用频率很高,如果我们不懂this,那么在看别人的代码或者是源码的时候,就会很吃力。
    2. 工作中,滥用this,却没明白this指向的是什么,而导致出现问题,但是自己却不知道哪里出问题了。【在公司,我至少帮10个以上的开发人员处理过这个问题】
    3. 合理的使用this,可以让我们写出简洁且复用性高的代码。
    4. 面试的高频问题,回答不好,抱歉,出门右拐,不送。

    不管出于什么目的,我们都需要把this这个知识点整的明明白白的。

    OK,Let's go!

    this是什么?

    言归正传,this是什么?首先记住this不是指向自身!this 就是一个指针,指向调用函数的对象。这句话我们都知道,但是很多时候,我们未必能够准确判断出this究竟指向的是什么?这就好像我们听过很多道理 却依然过不好这一生。今天咱们不探讨如何过好一生的问题,但是呢,希望阅读完下面的内容之后,你能够一眼就看出this指向的是什么。

    为了能够一眼看出this指向的是什么,我们首先需要知道this的绑定规则有哪些?

    1. 默认绑定
    2. 隐式绑定
    3. 硬绑定
    4. new绑定

    上面的名词,你也许听过,也许没听过,但是今天之后,请牢牢记住。我们将依次来进行解析。

    默认绑定

    默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var name = 'YvetteLau';
    sayHi();

    在调用Hi()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。

    上面的代码,如果在浏览器环境中运行,那么结果就是 Hello,YvetteLau

    但是如果在node环境中运行,结果就是 Hello,undefined.这是因为node中name并不是挂在全局对象上的。

    本文中,如不特殊说明,默认为浏览器环境执行结果。

    隐式绑定

    函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun().我们来看一段代码:

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person = {
    name: 'YvetteLau',
    sayHi: sayHi
    }
    var name = 'Wiliam';
    person.sayHi();

    打印的结果是 Hello,YvetteLau.

    sayHi函数声明在外部,严格来说并不属于person,但是在调用sayHi时,调用位置会使用person的上下文来引用函数,隐式绑定会把函数调用中的this(即此例sayHi函数中的this)绑定到这个上下文对象(即此例中的person)

    需要注意的是:对象属性链中只有最后一层会影响到调用位置。

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person2 = {
    name: 'Christina',
    sayHi: sayHi
    }
    var person1 = {
    name: 'YvetteLau',
    friend: person2
    }
    person1.friend.sayHi();

    结果是:Hello, Christina.

    因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的friend。

    隐式绑定有一个大陷阱,绑定很容易丢失(或者说容易给我们造成误导,我们以为this指向的是什么,但是实际上并非如此).

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person = {
    name: 'YvetteLau',
    sayHi: sayHi
    }
    var name = 'Wiliam';
    var Hi = person.sayHi;
    Hi();

    结果是: Hello,Wiliam.

    这是为什么呢,Hi直接指向了sayHi的引用,在调用的时候,跟person没有半毛钱的关系,针对此类问题,我建议大家只需牢牢继续这个格式:XXX.fn();fn()前如果什么都没有,那么肯定不是隐式绑定,但是也不一定就是默认绑定,这里有点小疑问,我们后来会说到。

    除了上面这种丢失之外,隐式绑定的丢失是发生在回调函数中(事件回调也是其中一种),我们来看下面一个例子:

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person1 = {
    name: 'YvetteLau',
    sayHi: function(){
    setTimeout(function(){
    console.log('Hello,',this.name);
    })
    }
    }
    var person2 = {
    name: 'Christina',
    sayHi: sayHi
    }
    var name='Wiliam';
    person1.sayHi();
    setTimeout(person2.sayHi,100);
    setTimeout(function(){
    person2.sayHi();
    },200);

    结果为:

    Hello, Wiliam
    Hello, Wiliam
    Hello, Christina
    • 第一条输出很容易理解,setTimeout的回调函数中,this使用的是默认绑定,非严格模式下,执行的是全局对象
    • 第二条输出是不是有点迷惑了?说好XXX.fun()的时候,fun中的this指向的是XXX呢,为什么这次却不是这样了!Why?

      其实这里我们可以这样理解: setTimeout(fn,delay){ fn(); },相当于是将person2.sayHi赋值给了一个变量,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。

    • 第三条虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系。

    读到这里,也许你已经有点疲倦了,但是答应我,别放弃,好吗?再坚持一下,就可以掌握这个知识点了。


    显式绑定

    显式绑定比较好理解,就是通过call,apply,bind的方式,显式的指定this所指向的对象。(注意:《你不知道的Javascript》中将bind单独作为了硬绑定讲解了)

    call,apply和bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,而bind方法不会。

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person = {
    name: 'YvetteLau',
    sayHi: sayHi
    }
    var name = 'Wiliam';
    var Hi = person.sayHi;
    Hi.call(person); //Hi.apply(person)

    输出的结果为: Hello, YvetteLau. 因为使用硬绑定明确将this绑定在了person上。

    那么,使用了硬绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person = {
    name: 'YvetteLau',
    sayHi: sayHi
    }
    var name = 'Wiliam';
    var Hi = function(fn) {
    fn();
    }
    Hi.call(person, person.sayHi);

    输出的结果是 Hello, Wiliam. 原因很简单,Hi.call(person, person.sayHi)的确是将this绑定到Hi中的this了。但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。

    现在,我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它硬绑定。

    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person = {
    name: 'YvetteLau',
    sayHi: sayHi
    }
    var name = 'Wiliam';
    var Hi = function(fn) {
    fn.call(this);
    }
    Hi.call(person, person.sayHi);

    此时,输出的结果为: Hello, YvetteLau,因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。

    至此,革命已经快胜利了,我们来看最后一种绑定规则: new 绑定。

    new 绑定

    javaScript和C++不一样,并没有类,在javaScript中,构造函数只是使用new操作符时被调用的函数,这些函数和普通的函数并没有什么不同,它不属于某个类,也不可能实例化出一个类。任何一个函数都可以使用new来调用,因此其实并不存在构造函数,而只有对于函数的“构造调用”。

    使用new来调用函数,会自动执行下面的操作:
    1. 创建一个新对象
    2. 将构造函数的作用域赋值给新对象,即this指向这个新对象
    3. 执行构造函数中的代码
    4. 返回新对象

    因此,我们使用new来调用函数的时候,就会新对象绑定到这个函数的this上。

    function sayHi(name){
    this.name = name;

    }
    var Hi = new sayHi('Yevtte');
    console.log('Hello,', Hi.name);

    输出结果为 Hello, Yevtte, 原因是因为在var Hi = new sayHi('Yevtte');这一步,会将sayHi中的this绑定到Hi对象上。

    绑定优先级

    我们知道了this有四种绑定规则,但是如果同时应用了多种规则,怎么办?

    显然,我们需要了解哪一种绑定方式的优先级更高,这四种绑定的优先级为:

    new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

    这个规则时如何得到的,大家如果有兴趣,可以自己写个demo去测试,或者记住上面的结论即可。

    绑定例外

    凡事都有例外,this的规则也是这样。

    如果我们将null或者是undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

    var foo = {
    name: 'Selina'
    }
    var name = 'Chirs';
    function bar() {
    console.log(this.name);
    }
    bar.call(null); //Chirs

    输出的结果是 Chirs,因为这时实际应用的是默认绑定规则。

    箭头函数

    箭头函数是ES6中新增的,它和普通函数有一些区别,箭头函数没有自己的this,它的this继承于外层代码库中的this。箭头函数在使用时,需要注意以下几点:

    (1)函数体内的this对象,继承的是外层代码块的this。

    (2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

    (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

    (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

    (5)箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向.

    OK,我们来看看箭头函数的this是什么?

    var obj = {
    hi: function(){
    console.log(this);
    return ()=>{
    console.log(this);
    }
    },
    sayHi: function(){
    return function() {
    console.log(this);
    return ()=>{
    console.log(this);
    }
    }
    },
    say: ()=>{
    console.log(this);
    }
    }
    let hi = obj.hi(); //输出obj对象
    hi(); //输出obj对象
    let sayHi = obj.sayHi();
    let fun1 = sayHi(); //输出window
    fun1(); //输出window
    obj.say(); //输出window

    那么这是为什么呢?如果大家说箭头函数中的this是定义时所在的对象,这样的结果显示不是大家预期的,按照这个定义,say中的this应该是obj才对。

    我们来分析一下上面的执行结果:

    1. obj.hi(); 对应了this的隐式绑定规则,this绑定在obj上,所以输出obj,很好理解。
    2. hi(); 这一步执行的就是箭头函数,箭头函数继承上一个代码库的this,刚刚我们得出上一层的this是obj,显然这里的this就是obj.
    3. 执行sayHi();这一步也很好理解,我们前面说过这种隐式绑定丢失的情况,这个时候this执行的是默认绑定,this指向的是全局对象window.
    4. fun1(); 这一步执行的是箭头函数,如果按照之前的理解,this指向的是箭头函数定义时所在的对象,那么这儿显然是说不通。OK,按照箭头函数的this是继承于外层代码库的this就很好理解了。外层代码库我们刚刚分析了,this指向的是window,因此这儿的输出结果是window.
    5. obj.say(); 执行的是箭头函数,当前的代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window.

    你说箭头函数的this是静态的?

    依旧是前面的代码。我们来看看箭头函数中的this真的是静态的吗?

    我要说:非也

    var obj = {
    hi: function(){
    console.log(this);
    return ()=>{
    console.log(this);
    }
    },
    sayHi: function(){
    return function() {
    console.log(this);
    return ()=>{
    console.log(this);
    }
    }
    },
    say: ()=>{
    console.log(this);
    }
    }
    let sayHi = obj.sayHi();
    let fun1 = sayHi(); //输出window
    fun1(); //输出window

    let fun2 = sayHi.bind(obj)();//输出obj
    fun2(); //输出obj

    可以看出,fun1和fun2对应的是同样的箭头函数,但是this的输出结果是不一样的。

    所以,请大家牢牢记住一点: 箭头函数没有自己的this,箭头函数中的this继承于外层代码库中的this.

    总结

    关于this的规则,至此,就告一段落了,但是想要一眼就能看出this所绑定的对象,还需要不断的训练。

    我们来回顾一下,最初的问题。

    1. 如何准确判断this指向的是什么?

    1. 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
    2. 函数是否通过call,apply调用,或者使用了bind(即硬绑定),如果是,那么this绑定的就是指定的对象。
    3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.foo()
    4. 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。
    5. 如果把Null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
    6. 如果是箭头函数,箭头函数的this继承的是外层代码块的this。

    2. 执行过程解析

    var number = 5;
    var obj = {
    number: 3,
    fn: (function () {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function () {
    var num = this.number;
    this.number *= 2;
    console.log(num);
    number *= 3;
    console.log(number);
    }
    })()
    }
    var myFun = obj.fn;
    myFun.call(null);
    obj.fn();
    console.log(window.number);

    我们来分析一下,这段代码的执行过程。

    1.在定义obj的时候,fn对应的闭包就执行了,返回其中的函数,执行闭包中代码时,显然应用不了new绑定(没有出现new 关键字),硬绑定也没有(没有出现call,apply,bind关键字),隐式绑定有没有?很显然没有,如果没有XX.fn(),那么可以肯定没有应用隐式绑定,所以这里应用的就是默认绑定了,非严格模式下this绑定到了window上(浏览器执行环境)。【这里很容易被迷惑的就是以为this指向的是obj,一定要注意,除非是箭头函数,否则this跟词法作用域是两回事,一定要牢记在心】

    window.number * = 2; //window.number的值是10(var number定义的全局变量是挂在window上的)

    number = number * 2; //number的值是NaN;注意我们这边定义了一个number,但是没有赋值,number的值是undefined;Number(undefined)->NaN

    number = 3; //number的值为3

    2.myFun.call(null);我们前面说了,call的第一个参数传null,调用的是默认绑定;

    fn: function(){
    var num = this.number;
    this.number *= 2;
    console.log(num);
    number *= 3;
    console.log(number);
    }

    执行时:

    var num = this.number; //num=10; 此时this指向的是window
    this.number * = 2; //window.number = 20
    console.log(num); //输出结果为10
    number *= 3; //number=9; 这个number对应的闭包中的number;闭包中的number的是3
    console.log(number); //输出的结果是9

    3.obj.fn();应用了隐式绑定,fn中的this对应的是obj.

    var num = this.number;//num = 3;此时this指向的是obj
    this.number *= 2; //obj.number = 6;
    console.log(num); //输出结果为3;
    number *= 3; //number=27;这个number对应的闭包中的number;闭包中的number的此时是9
    console.log(number);//输出的结果是27

    4.最后一步console.log(window.number);输出的结果是20

    因此组中结果为:

    10
    9
    3
    27
    20

    严格模式下结果,大家可以根据今天所学,自己分析,巩固一下知识点。

    最后,恭喜坚持读完的小伙伴们,你们成功get到了this这个知识点,但是想要完全掌握,还是要多回顾和练习。如果你有不错的this练习题,欢迎在评论区留言哦,大家一起进步!


    原文:https://segmentfault.com/a/1190000018630013

    收起阅读 »

    前端基础-你真的懂函数吗?

    前言众所周知,在前端开发领域中,函数是一等公民,由此可见函数的重要性,本文旨在介绍函数中的一些特性与方法,对函数有更好的认知正文1.箭头函数ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与...
    继续阅读 »

    前言

    众所周知,在前端开发领域中,函数是一等公民,由此可见函数的重要性,本文旨在介绍函数中的一些特性与方法,对函数有更好的认知

    正文

    1.箭头函数

    ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数:

    let arrowSum = (a, b) => { 
    return a + b;
    };
    let functionExpressionSum = function(a, b) {
    return a + b;
    };
    console.log(arrowSum(5, 8)); // 13
    console.log(functionExpressionSum(5, 8)); // 13

    使用箭头函数须知:

    • 箭头函数的函数体如果不用大括号括起来会隐式返回这行代码的值
    • 箭头函数不能使用 argumentssuper 和new.target,也不能用作构造函数
    • 箭头函数没有 prototype 属性

    2.函数声明与函数表达式

    JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

    // 没问题 
    console.log(sum(10, 10));
    function sum(num1, num2) {
    return num1 + num2;
    }

    以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:

    // 会出错
    console.log(sum(10, 10));
    let sum = function(num1, num2) {
    return num1 + num2;
    };

    上述代码的报错有一些同学可能认为是let导致的暂时性死区。其实原因并不出在这里,这是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到let的那一行,那么执行上下文中就没有函数的定义。大家可以自己尝试一下,就算是用var来定义,也是一样会出错。

    3.函数内部

    在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增了 new.target 属性。

    arguments

    它是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。

    function factorial(num) { 
    if (num <= 1) {
    return 1;
    } else {
    return num * factorial(num - 1);
    }
    }

    // 上述代码可以运用arguments来进行解耦
    function factorial(num) {
    if (num <= 1) {
    return 1;
    } else {
    return num * arguments.callee(num - 1);
    }
    }

    这个重写之后的 factorial()函数已经用 arguments.callee 代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。

    arguments.callee 的解耦示例
    let trueFactorial = factorial; 
    factorial = function() {
    return 0;
    };
    console.log(trueFactorial(5)); // 120
    console.log(factorial(5)); // 0

    这里 factorial 函数在赋值给trueFactorial后被重写了 那么我们如果在递归中不使用arguments.callee 那么显然trueFactorial(5)的运行结果也是0,但是我们解耦之后,新的变量还是可以正常的进行

    this

    函数内部另一个特殊的对象是 this,它在标准函数和箭头函数中有不同的行为。

    在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。

    在箭头函数中,this引用的是定义箭头函数的上下文。

    caller

    这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。

    function outer() { 
    inner();
    }
    function inner() {
    console.log(inner.caller);
    }
    outer();

    以上代码会显示 outer()函数的源代码。这是因为 ourter()调用了 inner(),inner.caller指向 outer()。如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值:

    function outer() { 
    inner();
    }
    function inner() {
    console.log(arguments.callee.caller);
    }
    outer();

    new.target

    ECMAScript 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。

    function King() { 
    if (!new.target) {
    throw 'King must be instantiated using "new"'
    }
    console.log('King instantiated using "new"');
    }
    new King(); // King instantiated using "new"
    King(); // Error: King must be instantiated using "new"

    这里可以做一些延申,还有没有其他办法来判断函数是否通过new来调用的呢?

    可以使用 instanceof 来判断。我们知道在new的时候发生了哪些操作?用如下代码表示:

    var p = new Foo()
    // 实际上执行的是
    // 伪代码
    var o = new Object(); // 或 var o = {}
    o.__proto__ = Foo.prototype
    Foo.call(o)
    return o

    上述伪代码在MDN是这么说的:

    1. 一个继承自 Foo.prototype 的新对象被创建。
    2. 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
    3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)

    new 的操作说完了 现在我们看一下 instanceof,MDN上是这么说的:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

    也就是说,A的N个__proto__ 全等于 B.prototype,那么A instanceof B返回true,现在知识点已经介绍完毕,可以开始上代码了

      function Person() {
    if (this instanceof Person) {
    console.log("通过new 创建");
    return this;
    } else {
    console.log("函数调用");
    }
    }
    const p = new Person(); // 通过new创建
    Person(); // 函数调用

    解析:我们知道new构造函数的this指向实例,那么上述代码不难得出以下结论this.__proto__ === Person.prototype。所以这样就可以判断函数是通过new还是函数调用

    这里我们其实还可以将 this instanceof Person 改写为 this instanceof arguments.callee

    4.闭包

    终于说到了闭包,闭包这玩意真的是面试必问,所以掌握还是很有必要的

    闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

    function foo() {
    var a = 20;
    var b = 30;

    function bar() {
    return a + b;
    }
    return bar;
    }

    上述代码中,由于foo函数内部的bar函数使用了foo函数内部的变量,并且bar函数return把变量return了出去,这样闭包就产生了,这使得我们可以在外部拿到这些变量。

    const b = foo();
    b() // 50

    foo函数在调用的时候创建了一个执行上下文,可以在此上下文中使用a,b变量,理论上说,在foo调用结束,函数内部的变量会v8引擎的垃圾回收机制通过特定的标记回收。但是在这里,由于闭包的产生,a,b变量并不会被回收,这就导致我们在全局上下文(或其他执行上下文)中可以访问到函数内部的变量。

    我之前看到了一个说法:

    无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包,闭包包含在函数创建时作用域中的所有变量,类似于背包,函数定义附带一个小背包,它的包中存储了函数定义时作用域中的所有变量

    以此引申出一个经典面试题

    for (var i = 1; i <= 5; i++) {
    setTimeout(function timer() {
    console.log(i);
    }, i * 1000);
    }

    怎样可以使得上述代码的输出变为1,2,3,4,5?

    使用es6我们可以很简单的做出解答:将var换成let。

    那么我们使用刚刚学到的闭包知识怎么来解答呢?代码如下:

    for (var i = 1; i <= 5; i++) {
    (function (i) {
    setTimeout(function timer() {
    console.log(i);
    }, i * 1000);
    })(i)
    }

    根据上面的说法,将闭包看成一个背包,背包中包含定义时的变量,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值,即可解决。

    5.立即调用的函数表达式(IIFE)

    如下就是立即调用函数表达式

    (function() { 
    // 块级作用域
    })();

    使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。

    // IIFE 
    (function () {
    for (var i = 0; i < count; i++) {
    console.log(i);
    }
    })();
    console.log(i); // 抛出错误

    ES6的块级作用域:

    // 内嵌块级作用域 
    {
    let i;
    for (i = 0; i < count; i++) {
    console.log(i);
    }
    }
    console.log(i); // 抛出错误
    // 循环的块级作用域
    for (let i = 0; i < count; i++) {
    console.log(i);
    }
    console.log(i); // 抛出错误

    IIFE的另一个作用就是上文中的解决settimeout的输出问题

    附录知识点

    关于instanceof

    Function instanceof Object;//true
    Object instanceof Function;//true

    上述代码大家可以尝试在浏览器中跑一下,非常的神奇,那么这是什么原因呢?

    借用大佬的一张图


    //构造器Function的构造器是它自身
    Function.constructor=== Function;//true

    //构造器Object的构造器是Function(由此可知所有构造器的constructor都指向Function)
    Object.constructor === Function;//true



    //构造器Function的__proto__是一个特殊的匿名函数function() {}
    console.log(Function.__proto__);//function() {}

    //这个特殊的匿名函数的__proto__指向Object的prototype原型。
    Function.__proto__.__proto__ === Object.prototype//true

    //Object的__proto__指向Function的prototype,也就是上面中所述的特殊匿名函数
    Object.__proto__ === Function.prototype;//true
    Function.prototype === Function.__proto__;//true

    结论:

    1. 所有的构造器的constructor都指向Function
    2. Function的prototype指向一个特殊匿名函数,而这个特殊匿名函数的__proto__指向Object.prototype

    结尾

    本文主要参考 《JavaScript 高级程序设计 第四版》 由于作者水平有限,如有错误,敬请与我联系,谢谢您的阅读!

    原文:https://segmentfault.com/a/1190000039904453



    收起阅读 »

    什么,项目构建时内存溢出了?了解一下 node 内存限制

    背景在之前的一篇文章中, 我们遇到了一个项目在构建时内存溢出的问题。当时的解决方案是: 直接调大 node 的内存限制,避免达到内存上限。今天听同事分享了一个新方法,觉得不错, 特此记录, 顺便分享给大家, 希望对大家有所帮助。正文但 Node 进程...
    继续阅读 »

    背景

    在之前的一篇文章中, 我们遇到了一个项目在构建时内存溢出的问题。

    当时的解决方案是: 直接调大 node 的内存限制,避免达到内存上限。

    今天听同事分享了一个新方法,觉得不错, 特此记录, 顺便分享给大家, 希望对大家有所帮助。

    正文


    但 Node 进程的内存限制会是多少呢?

    在网上查阅了到如下描述:

    Currently, by default V8 has a memory limit of 512mb on 32-bit systems, and 1gb on 64-bit systems. The limit can be raised by setting --max-old-space-size to a maximum of ~1gb (32-bit) and ~1.7gb (64-bit), but it is recommended that you split your single process into several workers if you are hitting memory limits.

    翻译一下:

    当前,默认情况下,V8在32位系统上的内存限制为512mb,在64位系统上的内存限制为1gb。

    可以通过将--max-old-space-size设置为最大〜1gb(32位)和〜1.7gb(64位)来提高此限制,但是如果达到内存限制, 建议您将单个进程拆分为多个工作进程

    如果你想知道自己电脑的内存限制有多大, 可以直接把内存撑爆, 看报错。

    运行如下代码:

    // Small program to test the maximum amount of allocations in multiple blocks.
    // This script searches for the largest allocation amount.

    // Allocate a certain size to test if it can be done.
    function alloc (size) {
    const numbers = size / 8;
    const arr = []
    arr.length = numbers; // Simulate allocation of 'size' bytes.
    for (let i = 0; i < numbers; i++) {
    arr[i] = i;
    }
    return arr;
    };

    // Keep allocations referenced so they aren't garbage collected.
    const allocations = [];

    // Allocate successively larger sizes, doubling each time until we hit the limit.
    function allocToMax () {
    console.log("Start");

    const field = 'heapUsed';
    const mu = process.memoryUsage();

    console.log(mu);

    const gbStart = mu[field] / 1024 / 1024 / 1024;

    console.log(`Start ${Math.round(gbStart * 100) / 100} GB`);

    let allocationStep = 100 * 1024;

    // Infinite loop
    while (true) {
    // Allocate memory.
    const allocation = alloc(allocationStep);
    // Allocate and keep a reference so the allocated memory isn't garbage collected.
    allocations.push(allocation);
    // Check how much memory is now allocated.
    const mu = process.memoryUsage();
    const gbNow = mu[field] / 1024 / 1024 / 1024;

    console.log(`Allocated since start ${Math.round((gbNow - gbStart) * 100) / 100} GB`);
    }

    // Infinite loop, never get here.
    };

    allocToMax();


    我的电脑是 Macbook Pro masOS Catalina 16GB,Node 版本是 v12.16.1,这段代码大概在 1.6 GB 左右内存时候抛出异常。

    那我们现在知道 Node Process 确实是有一个内存限制的, 那我们就来增大它的内存限制再试一下。

    用 node --max-old-space-size=6000 来运行这段代码,得到如下结果:


    内存达到 4.6G 的时候也溢出了。

    你可能会问, node 不是有内存回收吗?这个我们在下面会讲。

    使用这个参数:node --max-old-space-size=6000, 我们增加的内存中老生代区域的大小,比较暴力。

    就像上文中提到的: 如果达到内存限制, 建议您将单个进程拆分为多个工作进程

    这个项目是一个 ts 项目,ts 文件的编译是比较占用内存的,如果把这部分独立成一个单独的进程, 情况也会有所改善。

    因为 ts-loader 内部调用了 tsc,在使用 ts-loader 时,会使用 tsconfig.js配置文件。

    当项目中的代码变的越来越多,体积也越来越庞大时,项目编译时间也随之增加。

    这是因为 Typescript 的语义检查器必须在每次重建时检查所有文件

    ts-loader 提供了一个 transpileOnly 选项,它默认为 false,我们可以把它设置为 true,这样项目编译时就不会进行类型检查,也不会输出声明文件。

    对一下 transpileOnly 分别设置 false 和 true 的项目构建速度对比:

    • 当 transpileOnly 为 false 时,整体构建时间为 4.88s.
    • 当 transpileOnly 为 true 时,整体构建时间为 2.40s.

    虽然构建速度提升了,但是有了一个弊端: 打包编译不会进行类型检查

    好在官方推荐了这样一个插件, 提供了这样的能力: fork-ts-checker-webpack-plugin

    官方示例的使用也非常简单:

    const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

    module.exports = {
    ...
    plugins: [
    new ForkTsCheckerWebpackPlugin()
    ]
    }

    在我这个实际的项目中,vue.config.js 修改如下:

    configureWebpack: config => {
    // get a reference to the existing ForkTsCheckerWebpackPlugin
    const existingForkTsChecker = config.plugins.filter(
    p => p instanceof ForkTsCheckerWebpackPlugin,
    )[0];

    // remove the existing ForkTsCheckerWebpackPlugin
    // so that we can replace it with our modified version
    config.plugins = config.plugins.filter(
    p => !(p instanceof ForkTsCheckerWebpackPlugin),
    );

    // copy the options from the original ForkTsCheckerWebpackPlugin
    // instance and add the memoryLimit property
    const forkTsCheckerOptions = existingForkTsChecker.options;

    forkTsCheckerOptions.memoryLimit = 4096;

    config.plugins.push(new ForkTsCheckerWebpackPlugin(forkTsCheckerOptions));
    }

    修改之后, 构建就成功了。

    关于垃圾回收

    在 Node.js 里面,V8 自动帮助我们进行垃圾回收, 让我们简单看一下V8中如何处理内存。

    一些定义

    • 常驻集大小:是RAM中保存的进程所占用的内存部分,其中包括:

      1. 代码本身
    • stack:包含原始类型和对对象的引用
    • 堆:存储引用类型,例如对象,字符串或闭包
    • 对象的浅层大小:对象本身持有的内存大小
    • 对象的保留大小:删除对象及其相关对象后释放的内存大小

    垃圾收集器如何工作

    垃圾回收是回收由应用程序不再使用的对象所占用的内存的过程。

    通常,内存分配很便宜,而内存池用完时收集起来很昂贵。

    如果无法从根节点访问对象,则该对象是垃圾回收的候选对象,因此该对象不会被根对象或任何其他活动对象引用。

    根对象可以是全局对象,DOM元素或局部变量。

    堆有两个主要部分,即 New Space和 Old Space

    新空间是进行新分配的地方。

    在这里收集垃圾的速度很快,大小约为1-8MB

    留存在新空间中的物体被称为新生代

    在新空间中幸存下来的物体被提升的旧空间-它们被称为老生代

    旧空间中的分配速度很快,但是收集费用很高,因此很少执行。

    node 垃圾回收

    Why is garbage collection expensive?

    The V8 JavaScript engine employs a stop-the-world garbage collector mechanism.

    In practice, it means that the program stops execution while garbage collection is in progress.

    通常,约20%的年轻一代可以存活到老一代,旧空间的收集工作将在耗尽后才开始。

    为此,V8 引擎使用两种不同的收集算法

    1. Scavenge: 速度很快,可在新生代上运行,
    2. Mark-Sweep: 速度较慢,并且可以在老生代上运行。

    篇幅有限,关于v8垃圾回收的更多信息,可以参考如下文章:

    1. http://jayconrod.com/posts/55...
    2. https://juejin.cn/post/684490...
    3. https://juejin.cn/post/684490...

    总结

    小小总结一下,上文介绍了两种方式:

    1. 直接加大内存,使用: node --max-old-space-size=4096
    2. 把一些耗内存进程独立出去, 使用了一个插件: fork-ts-checker-webpack-plugin

    希望大家留个印象, 记得这两种方式。

    好了, 内容就这么多, 谢谢。

    才疏学浅,如有错误, 欢迎指正。

    谢谢。

    原文:https://segmentfault.com/a/1190000039877970


    收起阅读 »

    前端常用图片文件下载上传方法

    本文整理了前端常用的下载文件以及上传文件的方法例子均以vue+element ui+axios为例,不使用el封装好的上传组件,这里自行进行封装实现先附上demo上传文件以图片为例,文件上传可以省略预览图片功能图片上传可以使用2种方式:文件流和base64;1...
    继续阅读 »

    本文整理了前端常用的下载文件以及上传文件的方法
    例子均以vue+element ui+axios为例,不使用el封装好的上传组件,这里自行进行封装实现

    先附上demo

    上传文件

    以图片为例,文件上传可以省略预览图片功能

    图片上传可以使用2种方式:文件流base64;

    1.文件流上传+预览

    <input type="file" id='imgBlob' @change='changeImgBlob' />
    <el-image style="width: 100px; height: 100px" :src="imgBlobSrc"></el-image>
    // data
    imgBlobSrc: ""

    // methods
    changeImgBlob() {
    let file = document.querySelector("#imgBlob");
    /**
    *图片预览
    *更适合PC端,兼容ie7+,主要功能点是window.URL.createObjectURL
    */
    var ua = navigator.userAgent.toLowerCase();
    if (/msie/.test(ua)) {
    this.imgBlobSrc = file.value;
    } else {
    this.imgBlobSrc = window.URL.createObjectURL(file.files[0]);
    }
    //上传后台
    const fd = new FormData();
    fd.append("files", file.files[0]);
    fd.append("xxxx", 11111); //其他字段,根据实际情况来
    axios({
    url: "/yoorUrl", //URL,根据实际情况来
    method: "post",
    headers: { "Content-Type": "multipart/form-data" },
    data: fd
    });
    }



    2.Base64上传+预览

    <input type="file" id='imgBase' @change='changeImgBase' />
    <el-image style="width: 100px; height: 100px" :src="imgBaseSrc"></el-image>
    // data
    imgBaseSrc : ""

    // methods
    changeImgBase() {
    let that = this;
    let file = document.querySelector("#imgBase");
    /**
    *图片预览
    *更适合H5页面,兼容ie10+,图片base64显示,主要功能点是FileReader和readAsDataURL
    */
    if (window.FileReader) {
    var fr = new FileReader();
    fr.onloadend = function (e) {
    that.imgBaseSrc = e.target.result;
    //上传后台
    axios({
    url: "/yoorUrl", //URL,根据实际情况来
    method: "post",
    data: {
    files: that.imgBaseSrc
    }
    });
    };
    fr.readAsDataURL(file.files[0]);
    }
    }


    下载文件

    图片下载

    假设需要下载图片为url文件流处理和这个一样

    <el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
    <el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
    • 注意:这里需要指定 responseTypeblob
    //data
    downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
    //methods
    downloadImg() {
    axios({
    url: this.downloadImgSrc, //URL,根据实际情况来
    method: "get",
    responseType: "blob"
    }).then(function (response) {
    const link = document.createElement("a");
    let blob = new Blob([response.data], { type: response.data.type });
    let url = URL.createObjectURL(blob);
    link.href = url;
    link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
    link.click();
    document.body.removeChild(link);
    });
    }

    文件下载(以pdf为例)

    <el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
    <el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
    //data
    downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
    //methods
    downloadImg() {
    axios({
    url: this.downloadImgSrc, //URL,根据实际情况来
    method: "get",
    responseType: "blob"
    }).then(function (response) {
    const link = document.createElement("a");
    let blob = new Blob([response.data], { type: response.data.type });
    let url = URL.createObjectURL(blob);
    link.href = url;
    link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
    link.click();
    document.body.removeChild(link);
    });
    }

    pdf预览可以参考如何预览以及下载pdf文件

    原文:https://segmentfault.com/a/1190000039893814



    收起阅读 »

    web 埋点实现原理了解一下

    前言埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情...
    继续阅读 »

    前言

    埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。

    现有埋点三大类型

    用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点
    1. 手动埋点
      手动代码埋点比较常见,需要调用埋点的业务方在需要采集数据的地方调用埋点的方法。优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。
    2. 可视化埋点
      可是化埋点是近今年的埋点趋势,很多大厂自己的数据埋点部门也都开始做这块。优点是业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提)。阿里的活动页很多都是运营通过可视化的界面拖拽配置实现,这些活动控件元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。
    3. 无埋点
      无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。

    我们暂时放弃可视化埋点的实现,在 手动埋点 和 无埋点 上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。

    思考几个问题

    埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题
    1. 我们要采集什么内容,进行哪些采集接口的约定
    2. 业务方通过什么方式来调用我们的采集脚本
    3. 手动埋点:SDK 需要封装一个方法给业务方进行调用,传参方式业务方可控
    4. 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集
    5. 用户标识:游客用户和登录用户的采集数据怎么进行区分关联
    6. 设备Id:用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样,怎么实现
    7. 单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
    8. 混合应用:app 与 h5 的混合应用我们要怎么进行通讯

    我们要采集什么内容,进行哪些采集接口的约定

    第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定

    {
    "header":{ // HTTP 头部
    "X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //设备ID,用来区分用户设备
    "X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
    "X-Current-Url":"", //当前地址,用户行为发生的页面
    "X-User-Id":"",//用户ID,统计登录用户行为
    },
    "body":[{ // HTTP Body体
    "PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
    "Event":"loaded", //事件类型,区分用户行为事件
    "PageTitle": "埋点测试页", //页面标题,直观看到用户访问页面
    "CurrentTime": “1517798922201”, //事件发生的时间
    "ExtraInfo": {
    } //扩展字段,对具体业务分析的传参
    }]
    }

    以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。

    {
    "header":{ // HTTP 头部
    "X-Device-Id" :"550e8400-e29b-41d4-a716-446655440000" , // 设备id
    },
    "body":{ // HTTP Body体
    "DeviceType": "web" , //设备类型
    "ScreenWide" : 768 , // 屏幕宽
    "ScreenHigh": 1366 , // 屏幕高
    "Language": "zh-cn" //语言
    }
    }

    手动埋点:SDK

    如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集

    //自定义事件
    sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})

    游客与用户关联

    我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联

    web 设备Id

    用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹

    我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式

    //web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
    collect.setIframe = function () {
    var that = this
    var iframe = document.createElement('iframe')
    iframe.id = "frame",
    iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
    iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
    document.body.appendChild(iframe)

    iframe.onload = function () {
    iframe.contentWindow.postMessage('loaded','*');
    }

    //监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
    helper.on(window,"message",function(event){
    that.deviceId = event.data.deviceId

    if(event.data && event.data.type == 'loaded'){
    that.sendDevice(that.getDevice(), that.deviceUrl);
    setTimeout(function () {
    that.send(that.beforeload)
    that.send(that.loaded)
    },1000)
    }
    })
    }

    iframe 与 SDK 通讯

    function receiveMessageFromIndex ( event ) {
    getDeviceInfo() // 获取设备信息
    var data = {
    deviceId: _deviceId,
    type:event.data
    }

    event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
    }

    //监听message事件
    if(window.addEventListener){
    window.addEventListener("message", receiveMessageFromIndex, false);
    }else{
    window.attachEvent("onmessage", receiveMessageFromIndex, false)

    如果你想知道可以看我的另一篇博客 web 浏览器指纹跨域共享

    单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异

    我们知道单页面应用都是无刷新的页面加载,所以我们在页面跳转的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。

    window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了

    // 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法

    collect = {}
    collect.onPushStateCallback : function(){} // 自定义的采集方法

    (function(history){
    var replaceState = history.replaceState; // 存储原生 replaceState
    history.replaceState = function(state, param) { // 改写 replaceState
    var url = arguments[2];
    if (typeof collect.onPushStateCallback == "function") {
    collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
    }
    return replaceState.apply(history, arguments); // 调用原生的 replaceState
    };
    })(window.history);

    这块介绍起来也比较的复杂,如果你想了解更多,可以看我的另一篇博客你需要知道的单页面路由实现原理

    混合应用:app 与 h5 的混合应用我们要怎么进行通讯

    现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。

    纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器

    // app 与 h5 混合应用,直接将数信息发给 app
    collect.saveEvent = function (jsonString) {

    collect.dcpDeviceType && setTimeout(function () {
    if(collect.dcpDeviceType=='android'){
    android.saveEvent(jsonString)
    } else {
    window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
    }

    },1000)
    }

    实现思路

    通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。

    我们需要暴露给业务方调用的方法



    我们来看下几个核心代码的实现

    工具方法

    我们定义了几个工具方法,提高开发的幸福指数 😝

    var helper = {};

    // 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
    helper.uuid = function(){}

    // 元素绑定事件监听,兼容浏览器到IE8
    helper.on = function(){}

    //元素移除事件监听的适配器函数,兼容浏览器到IE8
    helper.remove = function(){}

    //将json转为字符串,事件传输的参数类型转化
    helper.changeJSON2Query = function(){}

    //将相对路径解析成文档全路径
    helper.normalize = function(){}

    采集逻辑

    var collect = {
    deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
    eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
    isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
    parmas:{ ExtraInfo:{} },
    device:{}
    };

    //获取埋点配置
    collect.setParames = function(){}

    //更新访问路径及页面信息
    collect.updatePageInfo = function(){}

    //获取事件参数
    collect.getParames = function(){}

    //获取设备信息
    collect.getDevice = function(){}

    //事件采集
    collect.send = function(){}

    //设备采集
    collect.sendDevice = function(){}

    //判断才否采集,埋点采集的开关
    collect.isupload = function(){

    1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
    2. 采集则判断是否已经采集过
    a.已经采集过不做任何操作
    b.没有采集过添加事件监听
    3. 判断是 混合应用还是纯 web 应用
    a.如果是web 应用,调用 collect.setIframe 设置 iframe
    b.如果是混合应用 将开始加载和加载完成事件传输给 app
    }

    //点击事件处理函数
    collect.clickHandler = function(){}

    //离开页面的事件处理函数
    collect.beforeUnloadHandler = function(){}

    //页面回退事件处理函数
    collect.onPopStateHandler = function(){}

    //系统事件初始化,注册离开事件,浏览器后退事件
    collect.event = function(){}

    //获取记录开始加载数据信息
    collect.getBeforeload = function(){}

    //存储加载完成,获取设备类型,记录加载完成信息
    collect.onload = function(){

    1. 判断cookie是否有存设备类型信息,有表示混合应用
    2. 采集加载完成时间等信息
    3. 调用 collect.isupload 判断是否进行采集
    }

    //web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
    collect.setIframe = function(){}

    //app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
    collect.saveEvent = function(){}

    //采集自定义事件类型
    collect.dispatch = function(){}

    //将参数 userId 存入sessionStorage
    collect.storeUserId = function(){}

    //采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
    collect.saveEventInfo = function(){}

    //页面初始化调用方法
    collect.init = function(){

    1. 获取开始加载的采集信息
    2. 获取 SDK 配置信息,设备信息
    3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
    4. 页面加载完成,调用 collect.onload 方法

    }


    collect.init(); // 初始化

    //暴露给业务方调用的方法
    return {
    dispatch:collect.dispatch,
    storeUserId:collect.storeUserId,
    }

    原文链接:https://segmentfault.com/a/1190000014922668


    收起阅读 »