<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ 前端 - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ freeCodeCamp 是一个免费学习编程的开发者社区，涵盖 Python、HTML、CSS、React、Vue、BootStrap、JSON 教程等，还有活跃的技术论坛和丰富的社区活动，在你学习编程和找工作时为你提供建议和帮助。 ]]>
        </description>
        <link>https://www.freecodecamp.org/chinese/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ 前端 - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/chinese/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 15 Jun 2026 21:18:33 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/frontend/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Ep. 12 Nikkkki Xu：从广告客户经理到前端开发者，拓宽能力边界，感受创造的乐趣 ]]>
                </title>
                <description>
                    <![CDATA[ 我们可能会因为不同的原因学习编程，转行成为开发者，比如在以前的工作中缺少动力、希望有更高的收入、想要更灵活的工作时间，或者就是喜欢写代码。 这期节目的嘉宾 Nikkkki Xu 曾经是广告公司的客户经理，她希望做有实际产出的工作，所以使用 freeCodeCamp 课程和一些视频课程自学编程，成为一名前端开发者。 Nikkkki Xu2018 年（注：音频中提到的 2017 年不准确），Nikkkki 应聘加入做二手书交易的创业公司多抓鱼。在面试过程中，她快速学习公司团队使用的技术栈，完成一个 Chrome 插件，并且自发写了一份关于二手市场的分析，得到面试官的青睐。这也是她的第一份开发工作。 在多抓鱼工作期间，她不仅专注于本职开发任务，也积极了解和学习产品、运营、市场、商业模式等等，并且在团队协作和面试环节引入一些创意，在个人成长的同时也搭建一个成长型的团队。 Nikkkki 最近在大理看云，她打算开一家奶茶店，有机会也想做一个木匠。 和她聊天让我觉得很清新，希望你也喜欢这期节目，期待你在收听播客的平台上给我们好评。也欢迎你给我们发邮件分享自己的故事，你可以在这篇文章 [h ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/interview-nikkkki-xu-from-advertising-account-manager-to-front-end-developer/</link>
                <guid isPermaLink="false">651ed7bcc8bb7703fa0287b2</guid>
                
                    <category>
                        <![CDATA[ 播客 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Thu, 05 Oct 2023 13:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/10/Chinese.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我们可能会因为不同的原因学习编程，转行成为开发者，比如在以前的工作中缺少动力、希望有更高的收入、想要更灵活的工作时间，或者就是喜欢写代码。</p><p>这期节目的嘉宾 Nikkkki Xu 曾经是广告公司的客户经理，她希望做有实际产出的工作，所以使用 freeCodeCamp 课程和一些视频课程自学编程，成为一名前端开发者。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/news/content/images/2023/10/WechatIMG46.jpeg" class="kg-image" alt="WechatIMG46" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2023/10/WechatIMG46.jpeg 600w, https://chinese.freecodecamp.org/news/content/images/size/w1000/2023/10/WechatIMG46.jpeg 1000w, https://chinese.freecodecamp.org/news/content/images/size/w1600/2023/10/WechatIMG46.jpeg 1600w, https://chinese.freecodecamp.org/news/content/images/2023/10/WechatIMG46.jpeg 1918w" sizes="(min-width: 720px) 720px" width="1918" height="1280" loading="lazy"><figcaption>Nikkkki Xu</figcaption></figure><!--kg-card-begin: html--><iframe width="100%" height="180" frameborder="no" scrolling="no" seamless="" src="https://share.transistor.fm/e/09e9b627" title="嵌入内容" loading="lazy"></iframe><!--kg-card-end: html--><p>2018 年（注：音频中提到的 2017 年不准确），Nikkkki 应聘加入做二手书交易的创业公司多抓鱼。在面试过程中，她快速学习公司团队使用的技术栈，完成一个 Chrome 插件，并且自发写了一份关于二手市场的分析，得到面试官的青睐。这也是她的第一份开发工作。</p><p>在多抓鱼工作期间，她不仅专注于本职开发任务，也积极了解和学习产品、运营、市场、商业模式等等，并且在团队协作和面试环节引入一些创意，在个人成长的同时也搭建一个成长型的团队。</p><p>Nikkkki 最近在大理看云，她打算开一家奶茶店，有机会也想做一个木匠。</p><p>和她聊天让我觉得很清新，希望你也喜欢这期节目，期待你在收听播客的平台上给我们好评。也欢迎你给我们发邮件分享自己的故事，你可以在<a href="https://www.freecodecamp.org/chinese/news/freecodecamp-podcast-in-chinese/">这篇文章</a>中找到邮箱地址。</p><p>欢迎在 <a href="https://chinese.freecodecamp.org/">https://chinese.freecodecamp.org/</a> 查看更多免费的编程学习资源。</p><h2 id="-"><strong>主要话题</strong></h2><ul><li>03:03 在广告公司工作</li><li>06:19 希望产出实际作品</li><li>10:40 零基础自学编程</li><li>13:22 被多抓鱼吸引</li><li>19:28 在面试时展现自己的优势</li><li>24:01从新手成长为工程师</li><li>32:54 设计有意思的面试流程</li><li>38:41 和产品经理做朋友</li><li>42:06 拓宽能力边界</li><li>46:25 传媒同事和程序员同事的闪光点</li><li>49:05 提高发货准确率</li><li>52:11 收到陌生人的感谢</li><li>55:10 新的尝试</li><li>57:26 一些建议</li></ul><h2 id="--1"><strong>提到的资源</strong></h2><ul><li><a href="https://www.freecodecamp.org/chinese/learn/2022/responsive-web-design/">freeCodeCamp 响应式网页设计课程</a></li><li>《必然》</li><li><a href="https://www.duozhuayu.com/">多抓鱼</a></li><li>《代码整洁之道》</li><li>《重构》</li><li>《编写可读代码的艺术》</li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Ep. 8 陈俊雁和艾克：从没有学习机会的青少年到前端开发者 ]]>
                </title>
                <description>
                    <![CDATA[ 2019 年，一位朋友跟我说北京一所公益学校实务学堂，她们的老师在使用 freeCodeCamp 课程教农村青少年学编程，接着介绍我和校长认识。再接着我认识了几位志愿者老师，以及几位学生，他们都很感谢 freeCodeCamp 社区提供学习资源。几年来，我通过这些老师和学生的文章、视频，或者通过跟他们直接聊天，了解到很多故事，感受到他们的善良与活力。 做真正的好教育，这件事情对我们的世界来说太重要了。有些人在努力把好的教育带给资源匮乏的人们，让我们看到教育的另一种可能性。 在今天的节目里，我很开心邀请实务学堂的两位毕业生分享他们学习编程以及从事开发工作的经历。 艾克今年 20 岁，来自卢旺达，他在 5 岁时来到中国，因为家庭条件和学籍的原因，他小学没毕业就停止了学业。 艾克俊雁今年 19 岁，他在初三时学习成绩不够好，毕业后大概要和很多其他同学一样去环境氛围比较差的职业学校。 陈俊雁2019 年，在家人的支持下，两位同学进入实务学堂，开始学习编程，找到职业发展方向。用艾克的话来说，这是“人生的转折点”。 俊雁和艾克是好朋友，他们有很多共同点，比如都喜欢摄影、都喜欢音乐、都热 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/interview-chen-jun-yan-and-ai-ke-learn-in-a-nonprofit-school-and-become-web-developers/</link>
                <guid isPermaLink="false">64f9d5da39dbbb03f0b4f333</guid>
                
                    <category>
                        <![CDATA[ 播客 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Thu, 07 Sep 2023 13:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/09/Chinese.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>2019 年，一位朋友跟我说北京一所公益学校实务学堂，她们的老师在使用 freeCodeCamp 课程教农村青少年学编程，接着介绍我和校长认识。再接着我认识了几位志愿者老师，以及几位学生，他们都很感谢 freeCodeCamp 社区提供学习资源。几年来，我通过这些老师和学生的文章、视频，或者通过跟他们直接聊天，了解到很多故事，感受到他们的善良与活力。</p><p>做真正的好教育，这件事情对我们的世界来说太重要了。有些人在努力把好的教育带给资源匮乏的人们，让我们看到教育的另一种可能性。</p><p>在今天的节目里，我很开心邀请实务学堂的两位毕业生分享他们学习编程以及从事开发工作的经历。</p><p>艾克今年 20 岁，来自卢旺达，他在 5 岁时来到中国，因为家庭条件和学籍的原因，他小学没毕业就停止了学业。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/news/content/images/2023/09/---2.jpeg" class="kg-image" alt="---2" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2023/09/---2.jpeg 600w, https://chinese.freecodecamp.org/news/content/images/size/w1000/2023/09/---2.jpeg 1000w, https://chinese.freecodecamp.org/news/content/images/size/w1600/2023/09/---2.jpeg 1600w, https://chinese.freecodecamp.org/news/content/images/size/w2400/2023/09/---2.jpeg 2400w" sizes="(min-width: 720px) 720px" width="2000" height="2667" loading="lazy"><figcaption>艾克</figcaption></figure><p>俊雁今年 19 岁，他在初三时学习成绩不够好，毕业后大概要和很多其他同学一样去环境氛围比较差的职业学校。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/news/content/images/2023/09/---1.jpeg" class="kg-image" alt="---1" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2023/09/---1.jpeg 600w, https://chinese.freecodecamp.org/news/content/images/size/w1000/2023/09/---1.jpeg 1000w, https://chinese.freecodecamp.org/news/content/images/size/w1600/2023/09/---1.jpeg 1600w, https://chinese.freecodecamp.org/news/content/images/size/w2400/2023/09/---1.jpeg 2400w" sizes="(min-width: 720px) 720px" width="2000" height="1333" loading="lazy"><figcaption>陈俊雁</figcaption></figure><!--kg-card-begin: html--><iframe width="100%" height="180" frameborder="no" scrolling="no" seamless="" src="https://share.transistor.fm/e/a26bc1d6" title="嵌入内容" loading="lazy"></iframe><!--kg-card-end: html--><p>2019 年，在家人的支持下，两位同学进入实务学堂，开始学习编程，找到职业发展方向。用艾克的话来说，这是“人生的转折点”。</p><p>俊雁和艾克是好朋友，他们有很多共同点，比如都喜欢摄影、都喜欢音乐、都热爱编程，而且他俩的父亲都是软件工程师。</p><p>他们从没有学习机会的青少年，一路应对 Web 开发、硬件开发、英语学习中的挑战，到如今成为对当下充满热爱、对未来充满期待的前端开发者，这些故事给我启发，希望你也能从中获得能量。</p><p>期待你在收听播客的平台上给我们好评，并把节目分享给更多朋友。</p><p>欢迎在 <a href="https://chinese.freecodecamp.org/">https://chinese.freecodecamp.org/</a> 查看更多免费的编程学习资源。</p><h2 id="-"><strong><strong>更多关于嘉宾的信息</strong></strong></h2><ul><li><a href="https://github.com/chenjunyan1">陈俊雁的 GitHub 主页</a></li><li><a href="https://www.freecodecamp.org/chinese/news/author/chen/">陈俊雁的 freeCodeCamp 专栏文章</a></li><li><a href="https://github.com/McQueen5258/McQueen5258">艾克的 GitHub 主页</a></li></ul><h2 id="--1"><strong>主要话题</strong></h2><ul><li>03:33 自我介绍</li><li>05:40 为什么入读实务学堂</li><li>11:32 在公益学校学什么</li><li>13:23 第一次接触编程</li><li>18:06 在鼓励表达的环境里学习</li><li>19:32 使用名校和公益组织的学习资源</li><li>22:29 喜欢和同学一起做项目</li><li>25:45 组织周末编程沙龙</li><li>27:41 通过开源项目学习</li><li>29:40 艰难的时刻</li><li>34:32 解决问题的快乐</li><li>40:13 &nbsp;学习编程改变性格</li><li>43:31 向父亲展示硬件开发的成果</li><li>49:19 成为志愿者老师</li><li>51:39 love, study, together</li><li>53:22 在公益学校的生活</li><li>57:11 “珍贵的普通人”</li><li>57:58 可编程书包</li><li>60:22 前端工程师学习硬件开发</li><li>64:04 在工作环境中提升英语水平</li><li>67:49 “好的工程师和踏实的工程师”</li><li>69:31 对未来的期待</li><li>70:32 给编程初学者的建议</li></ul><h2 id="--2"><strong>提到的资源</strong></h2><ul><li><a href="https://www.freecodecamp.org/chinese/news/explore-programming-education-for-high-school-students-with-limited-resources/">《全人教育背景下，探索资源受限高中生的编程教育》</a></li><li>《Linux 内核安全模块深入剖析》</li><li><a href="https://www.freecodecamp.org/chinese/">freeCodeCamp 的课程</a></li><li><a href="https://codenation.org/">CodeNation</a></li><li><a href="https://www.mooc.org/">MOOC</a></li><li><a href="https://github.com/">GitHub</a></li><li><a href="https://www.freecodecamp.org/chinese/news/intranet-penetration/">《如何让你的 Web 项目实现内网穿透》</a><br></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 前端监控原理深入剖析 ]]>
                </title>
                <description>
                    <![CDATA[ 前端监控分为性能监控和错误监控。其中监控又分为两个环节：数据采集和数据上报。本文主要讲的就是如何进行数据采集和数据上报。 数据采集 性能数据采集 性能数据采集需要使用 window.performance [https://developer.mozilla.org/zh-CN/docs/Web/API/Performance] API。 Performance 接口可以获取到当前页面中与性能相关的信息，它是 High Resolution Time API 的一部分，同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。 从 MDN 的文档可以看出，window.performance.timing 包含了页面加载各个阶段的起始及结束时间。 这些属性需要结合下图一起看，更好理解： 为了方便大家理解 timing 各个属性的意义，我在知乎找到一位网友对于 timing 写的简介，在此转载一下。 timing: {         // 同一个浏 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/frontend-monitoring/</link>
                <guid isPermaLink="false">5fc21fe739641a0517d511b3</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Wed, 24 Nov 2021 03:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/11/domenico-loia-hGV2TfOh0ns-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>前端监控分为性能监控和错误监控。其中监控又分为两个环节：数据采集和数据上报。本文主要讲的就是如何进行数据采集和数据上报。</p><h2 id="-"><strong><strong>数据采集</strong></strong></h2><h3 id="--1"><strong><strong>性能数据采集</strong></strong></h3><p>性能数据采集需要使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Performance">window.performance</a> API。</p><p>Performance 接口可以获取到当前页面中与性能相关的信息，它是 High Resolution Time API 的一部分，同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。</p><p>从 MDN 的文档可以看出，<code>window.performance.timing</code> 包含了页面加载各个阶段的起始及结束时间。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/b149fda27bde36a2c9b3152714b349e6.png" class="kg-image" alt="b149fda27bde36a2c9b3152714b349e6" width="600" height="400" loading="lazy"></figure><p>这些属性需要结合下图一起看，更好理解：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/01ae5e4fd42fdd117e2939809c949e71.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>为了方便大家理解 <code>timing</code> 各个属性的意义，我在知乎找到一位网友对于 <code>timing</code> 写的简介，在此转载一下。</p><pre><code class="language-js">timing: {
        // 同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面，这个值会和fetchStart相同。
	navigationStart: 1543806782096,

	// 上一个页面unload事件抛出时的时间戳。如果没有上一个页面，这个值会返回0。
	unloadEventStart: 1543806782523,

	// 和 unloadEventStart 相对应，unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。
	unloadEventEnd: 1543806782523,

	// 第一个HTTP重定向开始时的时间戳。如果没有重定向，或者重定向中的一个不同源，这个值会返回0。
	redirectStart: 0,

	// 最后一个HTTP重定向完成时（也就是说是HTTP响应的最后一个比特直接被收到的时间）的时间戳。
	// 如果没有重定向，或者重定向中的一个不同源，这个值会返回0. 
	redirectEnd: 0,

	// 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。
	fetchStart: 1543806782096,

	// DNS 域名查询开始的UNIX时间戳。
        //如果使用了持续连接(persistent connection)，或者这个信息存储到了缓存或者本地资源上，这个值将和fetchStart一致。
	domainLookupStart: 1543806782096,

	// DNS 域名查询完成的时间.
	//如果使用了本地缓存（即无 DNS 查询）或持久连接，则与 fetchStart 值相等
	domainLookupEnd: 1543806782096,

	// HTTP（TCP） 域名查询结束的时间戳。
        //如果使用了持续连接(persistent connection)，或者这个信息存储到了缓存或者本地资源上，这个值将和 fetchStart一致。
	connectStart: 1543806782099,

	// HTTP（TCP） 返回浏览器与服务器之间的连接建立时的时间戳。
        // 如果建立的是持久连接，则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。
	connectEnd: 1543806782227,

	// HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接，则返回0。
	secureConnectionStart: 1543806782162,

	// 返回浏览器向服务器发出HTTP请求时（或开始读取本地缓存时）的时间戳。
	requestStart: 1543806782241,

	// 返回浏览器从服务器收到（或从本地缓存读取）第一个字节时的时间戳。
        //如果传输层在开始请求之后失败并且连接被重开，该属性将会被数制成新的请求的相对应的发起时间。
	responseStart: 1543806782516,

	// 返回浏览器从服务器收到（或从本地缓存读取，或从本地资源读取）最后一个字节时
        //（如果在此之前HTTP连接已经关闭，则返回关闭时）的时间戳。
	responseEnd: 1543806782537,

	// 当前网页DOM结构开始解析时（即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时）的时间戳。
	domLoading: 1543806782573,

	// 当前网页DOM结构结束解析、开始加载内嵌资源时（即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时）的时间戳。
	domInteractive: 1543806783203,

	// 当解析器发送DOMContentLoaded 事件，即所有需要被执行的脚本已经被解析时的时间戳。
	domContentLoadedEventStart: 1543806783203,

	// 当所有需要立即执行的脚本已经被执行（不论执行顺序）时的时间戳。
	domContentLoadedEventEnd: 1543806783216,

	// 当前文档解析完成，即Document.readyState 变为 'complete'且相对应的readystatechange 被触发时的时间戳
	domComplete: 1543806783796,

	// load事件被发送时的时间戳。如果这个事件还未被发送，它的值将会是0。
	loadEventStart: 1543806783796,

	// 当load事件结束，即加载事件完成时的时间戳。如果这个事件还未被发送，或者尚未完成，它的值将会是0.
	loadEventEnd: 1543806783802
}
</code></pre><p>通过以上数据，我们可以得到几个有用的时间</p><pre><code class="language-js">// 重定向耗时
redirect: timing.redirectEnd - timing.redirectStart,
// DOM 渲染耗时
dom: timing.domComplete - timing.domLoading,
// 页面加载耗时
load: timing.loadEventEnd - timing.navigationStart,
// 页面卸载耗时
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 请求耗时
request: timing.responseEnd - timing.requestStart,
// 获取性能信息时当前时间
time: new Date().getTime(),
</code></pre><p>还有一个比较重要的时间就是<strong><strong>白屏时间</strong></strong>，它指从输入网址，到页面开始显示内容的时间。</p><p>将以下脚本放在 <code>&lt;/head&gt;</code> 前面就能获取白屏时间。</p><pre><code class="language-html">&lt;script&gt;
    whiteScreen = new Date() - performance.timing.navigationStart
    // 通过 domLoading 和 navigationStart 也可以
    whiteScreen = performance.timing.domLoading - performance.timing.navigationStart
&lt;/script&gt;
</code></pre><p>通过这几个时间，就可以得知页面首屏加载性能如何了。</p><p>另外，通过 <code>window.performance.getEntriesByType('resource')</code> 这个方法，我们还可以获取相关资源（js、css、img…）的加载时间，它会返回页面当前所加载的所有资源。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/b2e1bcd6f06291b8b8c7f72825b91f68.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>它一般包括以下几个类型：</p><ul><li>sciprt</li><li>link</li><li>img</li><li>css</li><li>fetch</li><li>other</li><li>xmlhttprequest</li></ul><p>我们只需用到以下几个信息：</p><pre><code class="language-js">// 资源的名称
name: item.name,
// 资源加载耗时
duration: item.duration.toFixed(2),
// 资源大小
size: item.transferSize,
// 资源所用协议
protocol: item.nextHopProtocol,
</code></pre><p>现在，写几行代码来收集这些数据。</p><pre><code class="language-js">// 收集性能信息
const getPerformance = () =&gt; {
    if (!window.performance) return
    const timing = window.performance.timing
    const performance = {
        // 重定向耗时
        redirect: timing.redirectEnd - timing.redirectStart,
        // 白屏时间
        whiteScreen: whiteScreen,
        // DOM 渲染耗时
        dom: timing.domComplete - timing.domLoading,
        // 页面加载耗时
        load: timing.loadEventEnd - timing.navigationStart,
        // 页面卸载耗时
        unload: timing.unloadEventEnd - timing.unloadEventStart,
        // 请求耗时
        request: timing.responseEnd - timing.requestStart,
        // 获取性能信息时当前时间
        time: new Date().getTime(),
    }

    return performance
}

// 获取资源信息
const getResources = () =&gt; {
    if (!window.performance) return
    const data = window.performance.getEntriesByType('resource')
    const resource = {
        xmlhttprequest: [],
        css: [],
        other: [],
        script: [],
        img: [],
        link: [],
        fetch: [],
        // 获取资源信息时当前时间
        time: new Date().getTime(),
    }

    data.forEach(item =&gt; {
        const arry = resource[item.initiatorType]
        arry &amp;&amp; arry.push({
            // 资源的名称
            name: item.name,
            // 资源加载耗时
            duration: item.duration.toFixed(2),
            // 资源大小
            size: item.transferSize,
            // 资源所用协议
            protocol: item.nextHopProtocol,
        })
    })

    return resource
}
</code></pre><h4 id="--2"><strong><strong>小结</strong></strong></h4><p>通过对性能及资源信息的解读，我们可以判断出页面加载慢有以下几个原因：</p><ol><li>资源过多、过大</li><li>网速过慢</li><li>DOM 元素过多</li></ol><p>除了用户网速过慢，我们没办法之外，其他两个原因都是有办法解决的，性能优化的文章和书籍网上已经有很多了，有兴趣可自行查找资料了解。</p><p><strong><strong>PS</strong></strong>：其实页面加载慢还有其他原因，例如没有使用按需加载、没有使用 CDN 等等。不过这里我们强调的仅通过对性能和资源信息的解读来获取原因。</p><h3 id="--3"><strong><strong>错误数据采集</strong></strong></h3><p>目前所能捕捉的错误有三种:</p><ol><li>资源加载错误，通过 <code>addEventListener('error', callback, true)</code> 在捕获阶段捕捉资源加载失败错误。</li><li>js 执行错误，通过 <code>window.onerror</code> 捕捉 js 错误。</li><li>promise 错误，通过 <code>addEventListener('unhandledrejection', callback)</code>捕捉 promise 错误，但是没有发生错误的行数，列数等信息，只能手动抛出相关错误信息。</li></ol><p>我们可以建一个错误数组变量 <code>errors</code> 在错误发生时，将错误的相关信息添加到数组，然后在某个阶段统一上报，具体如何操作请看下面的代码：</p><pre><code class="language-js">// 捕获资源加载失败错误 js css img...
addEventListener('error', e =&gt; {
    const target = e.target
    if (target != window) {
        monitor.errors.push({
            type: target.localName,
            url: target.src || target.href,
            msg: (target.src || target.href) + ' is load error',
            // 错误发生的时间
            time: new Date().getTime(),
        })
    }
}, true)

// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {
    monitor.errors.push({
        type: 'javascript',
        row: row,
        col: col,
        msg: error &amp;&amp; error.stack? error.stack : msg,
        url: url,
        // 错误发生的时间
        time: new Date().getTime(),
    })
}

// 监听 promise 错误 缺点是获取不到行数数据
addEventListener('unhandledrejection', e =&gt; {
    monitor.errors.push({
        type: 'promise',
        msg: (e.reason &amp;&amp; e.reason.msg) || e.reason || '',
        // 错误发生的时间
        time: new Date().getTime(),
    })
})
</code></pre><h4 id="--4"><strong><strong>小结</strong></strong></h4><p>通过错误收集，可以了解到网站发生错误的类型及数量，从而做出相应的调整，以减少错误发生。完整代码和 DEMO 会在文章末尾放出，大家可以复制代码（HTML文件）在本地测试一下。</p><h2 id="--5"><strong><strong>数据上报</strong></strong></h2><h3 id="--6"><strong><strong>性能数据上报</strong></strong></h3><p>性能数据可以在页面加载完之后上报，尽量不要对页面性能造成影响。</p><pre><code class="language-js">window.onload = () =&gt; {
    // 在浏览器空闲时间获取性能及资源信息
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    if (window.requestIdleCallback) {
        window.requestIdleCallback(() =&gt; {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        })
    } else {
        setTimeout(() =&gt; {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        }, 0)
    }
}
</code></pre><p>当然，你也可以设一个定时器，循环上报。不过每次上报最好做一下对比去重再上报，避免同样的数据重复上报。</p><h3 id="--7"><strong><strong>错误数据上报</strong></strong></h3><p>我在 DEMO 里提供的代码，是用一个 <code>errors</code> 数组收集所有的错误，再在某一阶段统一上报（延时上报）。</p><p>其实，也可以改成在错误发生时上报（即时上报）。这样可以避免“收集完错误，但延时上报还没触发，用户却已经关掉网页导致错误数据丢失”的问题。</p><pre><code class="language-js">// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {
    const data = {
        type: 'javascript',
        row: row,
        col: col,
        msg: error &amp;&amp; error.stack? error.stack : msg,
        url: url,
        // 错误发生的时间
        time: new Date().getTime(),
    }
    
    // 即时上报
    axios.post({ url: 'xxx', data, })
}
</code></pre><p>经网友提醒，可以使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon">navigator.sendBeacon()</a> 来进行上报。</p><pre><code class="language-js">window.addEventListener('unload', logData, false);

function logData() {
    navigator.sendBeacon("/log", analyticsData);
}
</code></pre><p>它的技术特点是：</p><p>使用 sendBeacon() 方法会使用户代理（浏览器）在有机会时异步地向服务器发送数据，同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题：数据可靠，传输异步并且不会影响下一页面的加载。</p><h2 id="--8"><strong><strong>扩展</strong></strong></h2><h3 id="spa"><strong><strong>SPA</strong></strong></h3><p><code>window.performance</code> API 是有缺点的，在 SPA 切换路由时，<code>window.performance.timing</code> 的数据不会更新。<br>所以我们需要另想办法来统计切换路由到加载完成的时间。<br>拿 Vue 举例，一个可行的办法就是切换路由时，在路由的全局前置守卫 <code>beforeEach</code> 里获取开始时间，在组件的 <code>mounted</code> 钩子里执行 <code>vm.$nextTick</code> 函数来获取组件的渲染完毕时间。</p><pre><code class="language-js">router.beforeEach((to, from, next) =&gt; {
	store.commit('setPageLoadedStartTime', new Date())
})
</code></pre><pre><code class="language-js">mounted() {
	this.$nextTick(() =&gt; {
		this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
	})
}
</code></pre><p>除了性能和错误监控，其实我们还可以收集更多的信息。</p><h3 id="--9"><strong><strong>用户信息收集</strong></strong></h3><h4 id="navigator"><strong><strong>navigator</strong></strong></h4><p>使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/navigator">window.navigator</a> 可以收集到用户的设备信息，操作系统，浏览器信息…</p><h4 id="uv-unique-visitor-"><strong><strong>UV（Unique visitor）</strong></strong></h4><p>是指通过互联网浏览这个网页的访客，00:00-24:00 内相同的设备访问只被计算一次。一天内同个访客多次访问仅计算一个 UV。</p><p>在用户访问网站时，可以生成一个随机字符串+时间日期，保存在本地。在网页发生请求时（如果超过当天24小时，则重新生成），把这些参数传到后端，后端利用这些信息生成 UV 统计报告。</p><h4 id="pv-page-view-"><strong><strong>PV（Page View）</strong></strong></h4><p>即页面浏览量或点击量，用户每 1 次对网站中的每个网页访问均被记录 1 个PV。用户对同一页面的多次访问，访问量累计，用以衡量网站用户访问的网页数量。</p><h4 id="--10"><strong><strong>页面停留时间</strong></strong></h4><h5 id="--11"><strong><strong>传统网站</strong></strong></h5><p>用户在进入 A 页面时，通过后台请求把用户进入页面的时间捎上。过了 10 分钟，用户进入 B 页面，这时后台可以通过接口捎带的参数可以判断出用户在 A 页面停留了 10 分钟。</p><h5 id="spa-1"><strong><strong>SPA</strong></strong></h5><p>可以利用 router 来获取用户停留时间，拿 Vue 举例，通过 <code>router.beforeEach</code>、<code>destroyed</code> 这两个钩子函数来获取用户停留该路由组件的时间。</p><h4 id="--12"><strong><strong>浏览深度</strong></strong></h4><p>通过 <code>document.documentElement.scrollTop</code> 属性以及屏幕高度，可以判断用户是否浏览完网站内容。</p><h4 id="--13"><strong><strong>页面跳转来源</strong></strong></h4><p>通过 <code>document.referrer</code> 属性，可以知道用户是从哪个网站跳转而来。</p><h3 id="--14"><strong><strong>小结</strong></strong></h3><p>通过分析用户数据，我们可以了解到用户的浏览习惯、爱好等等信息，想想真是恐怖，毫无隐私可言。</p><h2 id="demo"><strong><strong>DEMO</strong></strong></h2><pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="ie=edge"&gt;
    &lt;script&gt;
        function monitorInit() {
            const monitor = {
                // 数据上传地址
                url: '',
                // 性能信息
                performance: {},
                // 资源信息
                resources: {},
                // 错误信息
                errors: [],
                // 用户信息
                user: {
                    // 屏幕宽度
                    screen: screen.width,
                    // 屏幕高度
                    height: screen.height,
                    // 浏览器平台
                    platform: navigator.platform,
                    // 浏览器的用户代理信息
                    userAgent: navigator.userAgent,
                    // 浏览器用户界面的语言
                    language: navigator.language,
                },
                // 手动添加错误
                addError(error) {
                    const obj = {}
                    const { type, msg, url, row, col } = error
                    if (type) obj.type = type
                    if (msg) obj.msg = msg
                    if (url) obj.url = url
                    if (row) obj.row = row
                    if (col) obj.col = col
                    obj.time = new Date().getTime()
                    monitor.errors.push(obj)
                },
                // 重置 monitor 对象
                reset() {
                    window.performance &amp;&amp; window.performance.clearResourceTimings()
                    monitor.performance = getPerformance()
                    monitor.resources = getResources()
                    monitor.errors = []
                },
                // 清空 error 信息
                clearError() {
                    monitor.errors = []
                },
                // 上传监控数据
                upload() {
                    // 自定义上传
                    // axios.post({
                    //     url: monitor.url,
                    //     data: {
                    //         performance,
                    //         resources,
                    //         errors,
                    //         user,
                    //     }
                    // })
                },
                // 设置数据上传地址
                setURL(url) {
                    monitor.url = url
                },
            }

            // 获取性能信息
            const getPerformance = () =&gt; {
                if (!window.performance) return
                const timing = window.performance.timing
                const performance = {
                    // 重定向耗时
                    redirect: timing.redirectEnd - timing.redirectStart,
                    // 白屏时间
                    whiteScreen: whiteScreen,
                    // DOM 渲染耗时
                    dom: timing.domComplete - timing.domLoading,
                    // 页面加载耗时
                    load: timing.loadEventEnd - timing.navigationStart,
                    // 页面卸载耗时
                    unload: timing.unloadEventEnd - timing.unloadEventStart,
                    // 请求耗时
                    request: timing.responseEnd - timing.requestStart,
                    // 获取性能信息时当前时间
                    time: new Date().getTime(),
                }

                return performance
            }

            // 获取资源信息
            const getResources = () =&gt; {
                if (!window.performance) return
                const data = window.performance.getEntriesByType('resource')
                const resource = {
                    xmlhttprequest: [],
                    css: [],
                    other: [],
                    script: [],
                    img: [],
                    link: [],
                    fetch: [],
                    // 获取资源信息时当前时间
                    time: new Date().getTime(),
                }

                data.forEach(item =&gt; {
                    const arry = resource[item.initiatorType]
                    arry &amp;&amp; arry.push({
                        // 资源的名称
                        name: item.name,
                        // 资源加载耗时
                        duration: item.duration.toFixed(2),
                        // 资源大小
                        size: item.transferSize,
                        // 资源所用协议
                        protocol: item.nextHopProtocol,
                    })
                })

                return resource
            }

            window.onload = () =&gt; {
                // 在浏览器空闲时间获取性能及资源信息 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
                if (window.requestIdleCallback) {
                    window.requestIdleCallback(() =&gt; {
                        monitor.performance = getPerformance()
                        monitor.resources = getResources()
                        console.log('页面性能信息')
                        console.log(monitor.performance)
                        console.log('页面资源信息')
                        console.log(monitor.resources)
                    })
                } else {
                    setTimeout(() =&gt; {
                        monitor.performance = getPerformance()
                        monitor.resources = getResources()
                        console.log('页面性能信息')
                        console.log(monitor.performance)
                        console.log('页面资源信息')
                        console.log(monitor.resources)
                    }, 0)
                }
            }

            // 捕获资源加载失败错误 js css img...
            addEventListener('error', e =&gt; {
                const target = e.target
                if (target != window) {
                    monitor.errors.push({
                        type: target.localName,
                        url: target.src || target.href,
                        msg: (target.src || target.href) + ' is load error',
                        // 错误发生的时间
                        time: new Date().getTime(),
                    })

                    console.log('所有的错误信息')
                    console.log(monitor.errors)
                }
            }, true)

            // 监听 js 错误
            window.onerror = function(msg, url, row, col, error) {
                monitor.errors.push({
                    type: 'javascript', // 错误类型
                    row: row, // 发生错误时的代码行数
                    col: col, // 发生错误时的代码列数
                    msg: error &amp;&amp; error.stack? error.stack : msg, // 错误信息
                    url: url, // 错误文件
                    time: new Date().getTime(), // 错误发生的时间
                })

                console.log('所有的错误信息')
                console.log(monitor.errors)
            }

            // 监听 promise 错误 缺点是获取不到行数数据
            addEventListener('unhandledrejection', e =&gt; {
                monitor.errors.push({
                    type: 'promise',
                    msg: (e.reason &amp;&amp; e.reason.msg) || e.reason || '',
                    // 错误发生的时间
                    time: new Date().getTime(),
                })

                console.log('所有的错误信息')
                console.log(monitor.errors)
            })

            return monitor
        }

        const monitor = monitorInit()
    &lt;/script&gt;
    &lt;link rel="stylesheet" href="test.css"&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;button class="btn1"&gt;错误测试按钮1&lt;/button&gt;
    &lt;button class="btn2"&gt;错误测试按钮2&lt;/button&gt;
    &lt;button class="btn3"&gt;错误测试按钮3&lt;/button&gt;
    &lt;img src="https://avatars3.githubusercontent.com/u/22117876?s=460&amp;v=4" alt=""&gt;
    &lt;img src="test.png" alt=""&gt;
&lt;script src="192.168.10.15/test.js"&gt;&lt;/script&gt;
&lt;script&gt;
document.querySelector('.btn1').onclick = () =&gt; {
    setTimeout(() =&gt; {
        console.log(button)
    }, 0)
}

document.querySelector('.btn2').onclick = () =&gt; {
    new Promise((resolve, reject) =&gt; {
        reject({
            msg: 'test.js promise is error'
        })
    })
}

document.querySelector('.btn3').onclick = () =&gt; {
    throw ('这是一个手动扔出的错误')
}
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre><h2 id="--15"><strong><strong>参考资料</strong></strong></h2><ul><li><a href="https://fex.baidu.com/blog/2014/05/build-performance-monitor-in-7-days/">7 天打造前端性能监控系统</a></li><li><a href="https://github.com/wangweianger/zanePerfor">zanePerfor</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 技术点评：对低代码平台的一些看法 ]]>
                </title>
                <description>
                    <![CDATA[ 由于本人水平有限，文中所表达的观点不一定准确甚至有错误。如果您有不同的意见，欢迎在评论区留言。 什么是 nocode lowcode procode  * nocode 无代码：不需要写代码就能做出一个应用。  * lowcode 低代码：仅需写少量代码就可以做出一个应用。  * procode 专业代码：应用所有的代码都需要开发人员编写。 由于 nocode 和 lowcode 没有本质区别，所以下文统一使用低代码来代指它们。 低代码平台在近几年特别火，在各大技术社区经常能看到低代码相关的文章。国内也有不少的低代码平台，例如墨刀、云凤蝶、宜搭等等。 在现在这个年代，软件就是生产力的代名词。以前在公司请个假还得拿张请假条，层层审批，找各个领导签字。现在只需要在软件上发个申请就可以了。因此，低代码平台为什么这么火也不难理解了，因为可以解放生产力。与之相对的是另一个问题：供需不平衡，需求太大，供应不足（软件开发人员相对不足）。低代码平台就是为了解决这类问题而出现的。 例如运营、财务、销售等部门需要特定的系统，但又没有 IT 部门或者 IT 部门人员不足。这个时候就可以选择使用低代 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/lowcode/</link>
                <guid isPermaLink="false">6126505db03439064c61ca83</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Wed, 25 Aug 2021 14:25:18 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/08/glenn-carstens-peters-npxXWgQ33ZQ-unsplash-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>由于本人水平有限，文中所表达的观点不一定准确甚至有错误。如果您有不同的意见，欢迎在评论区留言。</p><h2 id="-nocode-lowcode-procode">什么是 nocode lowcode procode</h2><ul><li>nocode 无代码：不需要写代码就能做出一个应用。</li><li>lowcode 低代码：仅需写少量代码就可以做出一个应用。</li><li>procode 专业代码：应用所有的代码都需要开发人员编写。</li></ul><p>由于 nocode 和 lowcode 没有本质区别，所以下文统一使用低代码来代指它们。</p><p>低代码平台在近几年特别火，在各大技术社区经常能看到低代码相关的文章。国内也有不少的低代码平台，例如墨刀、云凤蝶、宜搭等等。</p><p>在现在这个年代，软件就是生产力的代名词。以前在公司请个假还得拿张请假条，层层审批，找各个领导签字。现在只需要在软件上发个申请就可以了。因此，低代码平台为什么这么火也不难理解了，因为可以解放生产力。与之相对的是另一个问题：供需不平衡，需求太大，供应不足（软件开发人员相对不足）。低代码平台就是为了解决这类问题而出现的。</p><p>例如运营、财务、销售等部门需要特定的系统，但又没有 IT 部门或者 IT 部门人员不足。这个时候就可以选择使用低代码平台了，各部门人员通过简单的培训（熟悉文档）就可以上手开发，通过拖拉拽的方式生成一个应用。</p><h2 id="-">优点与缺点</h2><p>优点很明显，通过简单的培训就可以让很多不懂技术的人员上手开发。将一些原本软件开发人员才能做的事交给其他人来做，这样可以降低成本。因为软件人员的薪资成本一般比其他岗位的薪资高，使用低代码平台就是将相对高的人工成本转移到成本相对低的人身上（可以少招一些程序员）。</p><p>但缺点也很明显，具体有以下几点：</p><ol><li>低代码平台不是通用的解决方案，它只能针对某一业务场景。目前的低代码平台有面向工作流、面向原型、面向表单等一些常用的业务场景，或者说只能面向简单的业务场景。没有通用的解决方案。</li><li>无法做到定制化，低代码平台由于面向的人员比较多，开发功能时需要考虑到不同的用户，无法专门为某一企业做定制化开发。</li><li>降低成本，就是将开发人员的成本转移到其他人身上，降低的是薪资成本。但从开发效率来看，反而是降低了。因为用拖拉拽的方式开发应用效率是很低的，远远比不上直接敲代码。我在之前的公司开发过一个类似于墨刀的低代码平台，在测试时花了半小时靠拖拉拽的方式开发了一个小应用。为了做一个对比，我用敲代码的方式，不到 10 分钟就搞定了（当然，不同的业务场景可能时间不太一样，但不会有太大的差别）。不过这个缺点不适用于没有开发人员的企业。</li></ol><p>总的来说，低代码平台还是利大于弊的。因为目前还有很多企业是没有 IT 部门的，低代码平台可以很好的解决他们的问题。</p><p>这里有一个实际的例子，在我工作的上一家公司，我们接了同一个客户很多的项目。这些项目基本上都是同一类型的项目，功能上可能会有稍微的差别，但没有本质区别。这个客户必须为他的每一个项目付费，即使这些项目可能只有几个页面不一样。为了改善这种情况，我们针对用户的需求开发了一个低代码产品，后面用户要做什么项目只需要自己开发就可以了。</p><p>对于客户来说，他再也不用为每一个项目付费了，因为可以自己开发。这样就降低了费用成本，自己开发虽然时间长一点，但相对于降低的成本 ，客户是能够接受的。</p><h2 id="--1">未来</h2><p>最近看了不少关于低代码平台市场的研究报告，例如头豹研究院发布的《2020中国低代码开发平台行业概览》是这样说的：</p><p>在中国云服务市场高速扩张的背景之下，过去5年低代码开发服务初步落地。2016至2019年间，中国低代码开发平台行业融资额由100万元攀升至1.2亿元。</p><p>国内国外很多的大公司也在推出或收购低代码平台，争取在低代码平台这个赛道上分一杯羹。例如国外的有微软、亚马逊、西门子等等，国内的有腾讯、字节、阿里等等。</p><p>不说远的，我认为未来两三年内低代码平台的发展还是很有潜力的，市场规模每年都在涨。</p><h2 id="--2">低代码平台的一些功能点</h2><p>从目前的低代码平台来看，基本上都是靠拖拉拽来生成应用。可以说低代码平台是重前端，轻后端的产品，对于前端的技术要求更高。我之前对低代码平台做过一番调研，写了一个类似于墨刀的 DEMO。并且写了三篇文档对这个 DEMO 涉及的功能点进行原理分析：</p><ol><li>编辑器</li><li>自定义组件</li><li>拖拽</li><li>删除组件、调整图层层级</li><li>放大缩小</li><li>撤消、重做</li><li>组件属性设置</li><li>吸附</li><li>预览、保存代码</li><li>绑定事件</li><li>绑定动画</li><li>导入 PSD</li><li>手机模式</li><li>拖拽旋转</li><li>复制粘贴剪切</li><li>数据交互</li><li>发布</li><li>多个组件的组合和拆分</li><li>文本组件</li><li>矩形组件</li><li>锁定组件</li><li>快捷键</li><li>网格线</li><li>编辑器快照的另一种实现方式</li></ol><p>如果你想了解更多，可以看一下我的项目 <a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fwoai3c%2Fvisual-drag-demo" rel="nofollow noopener noreferrer">visual-drag-demo</a>。</p><p>在线预览：</p><ul><li><a href="https://woai3c.gitee.io/visual-drag-demo/#/">预览入口一</a></li><li><a href="https://woai3c.github.io/visual-drag-demo">预览入口二</a></li></ul><p>文档：</p><ul><li><a href="https://chinese.freecodecamp.org/news/visual-drag-and-drop-component-library/">可视化拖拽组件库一些技术要点原理分析</a></li><li><a href="https://chinese.freecodecamp.org/news/visual-drag-and-drop-component-library-part-2/">可视化拖拽组件库一些技术要点原理分析（二）</a></li><li><a href="https://chinese.freecodecamp.org/news/visual-drag-and-drop-component-library-part-3/">可视化拖拽组件库一些技术要点原理分析（三）</a></li></ul><p>我觉得低代码平台比较重要的一个点就是物料库。物料库越丰富越好，这是毋容置疑。另外添加新物料的方式，建议做成类似于插件的形式，这样可以方便扩展。最好就是插件能够以 npm 包的形式发布，添加插件就是添加 npm 包。这样无论是添加或删除物料都不用对低代码平台的源码进行修改。</p><h2 id="--3">总结</h2><p>一句话，比较看好低代码平台的前景。如果你是一个前端，并且对于自己的职业生涯没有清晰的规划，可以考虑一下低代码这个方向。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 超详细的前端工程化入门教程 ]]>
                </title>
                <description>
                    <![CDATA[ 本文将分成以下 7 个小节：  1. 技术选型  2. 统一规范  3. 测试  4. 部署  5. 监控  6. 性能优化  7. 重构 部分小节提供了非常详细的实战教程，让大家动手实践。 另外我还写了一个前端工程化 demo，放在 GitHub [https://github.com/woai3c/front-end-engineering-demo] 上。这个 demo 包含了 js、css、git 验证，其中 js、css 验证需要安装 VSCode，具体教程在下文中会有提及。 技术选型 对于前端来说，技术选型挺简单的。就是做选择题，三大框架中选一个。个人认为可以依据以下两个特点来选：  1. 选你或团队最熟的，保证在遇到棘手的问题时有人能填坑。  2. 选市场占有率高的。换句话说，就是选好招人的。 第二点对于小公司来说，特别重要。本来小公司就不好招人，要是还选一个市场占有率不高的框架（例如 Angular），简历你都看不到几个... UI 组件库更简单，github 上哪个 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-engineering-tutorial/</link>
                <guid isPermaLink="false">5fa915165f583f0565090fd9</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 优化 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 测试 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Wed, 28 Jul 2021 10:32:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/11/tim-mossholder-WE_Kv_ZB1l0-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本文将分成以下 7 个小节：</p>
<ol>
<li>技术选型</li>
<li>统一规范</li>
<li>测试</li>
<li>部署</li>
<li>监控</li>
<li>性能优化</li>
<li>重构</li>
</ol>
<p>部分小节提供了非常详细的实战教程，让大家动手实践。</p>
<p>另外我还写了一个前端工程化 demo，放在 <a href="https://github.com/woai3c/front-end-engineering-demo">GitHub</a> 上。这个 demo 包含了 js、css、git 验证，其中 js、css 验证需要安装 VSCode，具体教程在下文中会有提及。</p>
<h2 id="">技术选型</h2>
<p>对于前端来说，技术选型挺简单的。就是做选择题，三大框架中选一个。个人认为可以依据以下两个特点来选：</p>
<ol>
<li>选你或团队最熟的，保证在遇到棘手的问题时有人能填坑。</li>
<li>选市场占有率高的。换句话说，就是选好招人的。</li>
</ol>
<p>第二点对于小公司来说，特别重要。本来小公司就不好招人，要是还选一个市场占有率不高的框架（例如 Angular），简历你都看不到几个...</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/62b7a34ed09e4f5ba2aec46ed7c54de8~tplv-k3u1fbpfcp-watermark.image" alt="62b7a34ed09e4f5ba2aec46ed7c54de8~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c78b05aa770e4b6f8f553879eaa0dc02~tplv-k3u1fbpfcp-watermark.image" alt="c78b05aa770e4b6f8f553879eaa0dc02~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>UI 组件库更简单，github 上哪个 star 多就用哪个。star 多，说明用的人就多，很多坑别人都替你踩过了，省事。</p>
<h2 id="">统一规范</h2>
<h3 id="">代码规范</h3>
<p>先来看看统一代码规范的好处：</p>
<ul>
<li>规范的代码可以促进团队合作</li>
<li>规范的代码可以降低维护成本</li>
<li>规范的代码有助于 code review（代码审查）</li>
<li>养成代码规范的习惯，有助于程序员自身的成长</li>
</ul>
<p>当团队的成员都严格按照代码规范来写代码时，可以保证每个人的代码看起来都像是一个人写的，看别人的代码就像是在看自己的代码。更重要的是我们能够认识到规范的重要性，并坚持规范的开发习惯。</p>
<h4 id="">如何制订代码规范</h4>
<p>建议找一份好的代码规范，在此基础上结合团队的需求作个性化修改。</p>
<p>下面列举一些 star 较多的 js 代码规范：</p>
<ul>
<li><a href="https://github.com/airbnb/javascript">airbnb (101k star 英文版)</a>，<a href="https://github.com/lin-123/javascript">airbnb-中文版</a></li>
<li><a href="https://github.com/standard/standard/blob/master/docs/README-zhcn.md">standard (24.5k star) 中文版</a></li>
<li><a href="https://github.com/ecomfe/spec">百度前端编码规范 3.9k</a></li>
</ul>
<p>css 代码规范也有不少，例如：</p>
<ul>
<li><a href="https://github.com/fex-team/styleguide/blob/master/css.md">styleguide 2.3k</a></li>
<li><a href="https://github.com/ecomfe/spec/blob/master/css-style-guide.md">spec 3.9k</a></li>
</ul>
<h4 id="">如何检查代码规范</h4>
<p>使用 eslint 可以检查代码符不符合团队制订的规范，下面来看一下如何配置 eslint 来检查代码。</p>
<ol>
<li>下载依赖</li>
</ol>
<pre><code>// eslint-config-airbnb-base 使用 airbnb 代码规范
npm i -D babel-eslint eslint eslint-config-airbnb-base eslint-plugin-import
</code></pre>
<ol start="2">
<li>在 <code>package.json</code> 的 <code>scripts</code> 加上这行代码 <code>"lint": "eslint --ext .js test/ src/ bin/"</code>。然后执行 <code>npm run lint</code> 即可开始验证代码。</li>
</ol>
<p>不过这样检查代码效率太低，每次都得手动检查。并且报错了还得手动修改代码。</p>
<p>为了改善以上缺点，我们可以使用 VSCode。使用它并加上适当的配置可以在每次保存代码的时候，自动验证代码并进行格式化，省去了动手的麻烦。</p>
<p>css 检查代码规范则使用 <code>stylelint</code> 插件。</p>
<p>由于篇幅有限，具体如何配置请看我的另一篇文章：<a href="https://juejin.im/post/6892000216020189198/">ESlint + stylelint + VSCode自动格式化代码（2020）</a>。</p>
<p><img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1bd13cb84a34d78aaec9f1e98c17790~tplv-k3u1fbpfcp-zoom-1.image" alt="f1bd13cb84a34d78aaec9f1e98c17790~tplv-k3u1fbpfcp-zoom-1" width="600" height="400" loading="lazy"></p>
<p><img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b4d8e17b0f2488c8379541544147ef3~tplv-k3u1fbpfcp-zoom-1.image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></p>
<h3 id="git">git 规范</h3>
<p>git 规范包括两点：分支管理规范、git commit 规范。</p>
<h4 id="">分支管理规范</h4>
<p>一般项目分主分支（master）和其他分支。</p>
<p>当有团队成员要开发新功能或改 BUG 时，就从 master 分支开一个新的分支。例如项目要从客户端渲染改成服务端渲染，就开一个分支叫 ssr，开发完了再合并回 master 分支。</p>
<p>如果改一个 BUG，也可以从 master 分支开一个新分支，并用 BUG 号命名（不过我们小团队嫌麻烦，没这样做，除非有特别大的 BUG）。</p>
<h4 id="gitcommit">git commit 规范</h4>
<pre><code class="language-md">&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;
&lt;BLANK LINE&gt;
&lt;body&gt;
&lt;BLANK LINE&gt;
&lt;footer&gt;
</code></pre>
<p>大致分为三个部分(使用空行分割):</p>
<ol>
<li>标题行: 必填, 描述主要修改类型和内容</li>
<li>主题内容: 描述为什么修改, 做了什么样的修改, 以及开发的思路等等</li>
<li>页脚注释: 可以写注释，BUG 号链接</li>
</ol>
<h4 id="typecommit">type: commit 的类型</h4>
<ul>
<li>feat: 新功能、新特性</li>
<li>fix: 修改 bug</li>
<li>perf: 更改代码，以提高性能</li>
<li>refactor: 代码重构（重构，在不影响代码内部行为、功能下的代码修改）</li>
<li>docs: 文档修改</li>
<li>style: 代码格式修改, 注意不是 css 修改（例如分号修改）</li>
<li>test: 测试用例新增、修改</li>
<li>build: 影响项目构建或依赖项修改</li>
<li>revert: 恢复上一次提交</li>
<li>ci: 持续集成相关文件修改</li>
<li>chore: 其他修改（不在上述类型中的修改）</li>
<li>release: 发布新版本</li>
<li>workflow: 工作流相关文件修改</li>
</ul>
<ol>
<li>scope: commit 影响的范围, 比如: route, component, utils, build...</li>
<li>subject: commit 的概述</li>
<li>body: commit 具体修改内容, 可以分为多行.</li>
<li>footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接.</li>
</ol>
<p>示例</p>
<h5 id="fixbug">fix（修复BUG）</h5>
<p>如果修复的这个BUG只影响当前修改的文件，可不加范围。如果影响的范围比较大，要加上范围描述。</p>
<p>例如这次 BUG 修复影响到全局，可以加个 global。如果影响的是某个目录或某个功能，可以加上该目录的路径，或者对应的功能名称。</p>
<pre><code class="language-js">// 示例1
fix(global):修复checkbox不能复选的问题
// 示例2 下面圆括号里的 common 为通用管理的名称
fix(common): 修复字体过小的BUG，将通用管理下所有页面的默认字体大小修改为 14px
// 示例3
fix: value.length -&gt; values.length
</code></pre>
<h5 id="feat">feat（添加新功能或新页面）</h5>
<pre><code class="language-js">feat: 添加网站主页静态页面

这是一个示例，假设对点检任务静态页面进行了一些描述。
 
这里是备注，可以是放BUG链接或者一些重要性的东西。
</code></pre>
<h5 id="chore">chore（其他修改）</h5>
<p>chore 的中文翻译为日常事务、例行工作，顾名思义，即不在其他 commit 类型中的修改，都可以用 chore 表示。</p>
<pre><code class="language-js">chore: 将表格中的查看详情改为详情
</code></pre>
<p>其他类型的 commit 和上面三个示例差不多，就不说了。</p>
<h4 id="gitcommit">验证 git commit 规范</h4>
<p>验证 git commit 规范，主要通过 git 的 <code>pre-commit</code> 钩子函数来进行。当然，你还需要下载一个辅助工具来帮助你进行验证。</p>
<p>下载辅助工具</p>
<pre><code>npm i -D husky
</code></pre>
<p>在 <code>package.json</code> 加上下面的代码</p>
<pre><code class="language-json">"husky": {
  "hooks": {
    "pre-commit": "npm run lint",
    "commit-msg": "node script/verify-commit.js",
    "pre-push": "npm test"
  }
}
</code></pre>
<p>然后在你项目根目录下新建一个文件夹 <code>script</code>，并在下面新建一个文件 <code>verify-commit.js</code>，输入以下代码：</p>
<pre><code class="language-js">const msgPath = process.env.HUSKY_GIT_PARAMS
const msg = require('fs')
.readFileSync(msgPath, 'utf-8')
.trim()

const commitRE = /^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/

if (!commitRE.test(msg)) {
    console.log()
    console.error(`
        不合法的 commit 消息格式。
        请查看 git commit 提交规范：https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md
    `)

    process.exit(1)
}
</code></pre>
<p>现在来解释下各个钩子的含义：</p>
<ol>
<li><code>"pre-commit": "npm run lint"</code>，在 <code>git commit</code> 前执行 <code>npm run lint</code> 检查代码格式。</li>
<li><code>"commit-msg": "node script/verify-commit.js"</code>，在 <code>git commit</code> 时执行脚本 <code>verify-commit.js</code> 验证 commit 消息。如果不符合脚本中定义的格式，将会报错。</li>
<li><code>"pre-push": "npm test"</code>，在你执行 <code>git push</code> 将代码推送到远程仓库前，执行 <code>npm test</code> 进行测试。如果测试失败，将不会执行这次推送。</li>
</ol>
<h3 id="">项目规范</h3>
<p>主要是项目文件的组织方式和命名方式。</p>
<p>用我们的 Vue 项目举个例子。</p>
<pre><code>├─public
├─src
├─test
</code></pre>
<p>一个项目包含 public（公共资源，不会被 webpack 处理）、src（源码）、test（测试代码），其中 src 目录，又可以细分。</p>
<pre><code>├─api （接口）
├─assets （静态资源）
├─components （公共组件）
├─styles （公共样式）
├─router （路由）
├─store （vuex 全局数据）
├─utils （工具函数）
└─views （页面）
</code></pre>
<p>文件名称如果过长则用 - 隔开。</p>
<h3 id="ui">UI 规范</h3>
<p>UI 规范需要前端、UI、产品沟通，互相商量，最后制定下来，建议使用统一的 UI 组件库。</p>
<p>制定 UI 规范的好处：</p>
<ul>
<li>统一页面 UI 标准，节省 UI 设计时间</li>
<li>提高前端开发效率</li>
</ul>
<h2 id="">测试</h2>
<p>测试是前端工程化建设必不可少的一部分，它的作用就是找出 bug，越早发现 bug，所需要付出的成本就越低。并且，它更重要的作用是在将来，而不是当下。</p>
<p>设想一下半年后，你的项目要加一个新功能。在加完新功能后，你不确定有没有影响到原有的功能，需要测试一下。由于时间过去太久，你对项目的代码已经不了解了。在这种情况下，如果没有写测试，你就得手动一遍一遍的去试。而如果写了测试，你只需要跑一遍测试代码就 OK 了，省时省力。</p>
<p>写测试还可以让你修改代码时没有心理负担，不用一直想着改这里有没有问题？会不会引起 BUG？而写了测试就没有这种担心了。</p>
<p>在前端用得最多的就是单元测试（主要是端到端测试我用得很少，不熟），这里着重讲解一下。</p>
<h3 id="">单元测试</h3>
<p>单元测试就是对一个函数、一个组件、一个类做的测试，它针对的粒度比较小。</p>
<p>它应该怎么写呢？</p>
<ol>
<li>根据正确性写测试，即正确的输入应该有正常的结果。</li>
<li>根据异常写测试，即错误的输入应该是错误的结果。</li>
</ol>
<h4 id="">对一个函数做测试</h4>
<p>例如一个取绝对值的函数 <code>abs()</code>，输入 <code>1,2</code>，结果应该与输入相同；输入 <code>-1,-2</code>，结果应该与输入相反。如果输入非数字，例如 <code>"abc"</code>，应该抛出一个类型错误。</p>
<h4 id="">对一个类做测试</h4>
<p>假设有这样一个类：</p>
<pre><code>class Math {
    abs() {

    }

    sqrt() {

    }

    pow() {

    }
    ...
}
</code></pre>
<p>单元测试，必须把这个类的所有方法都测一遍。</p>
<h4 id="">对一个组件做测试</h4>
<p>组件测试比较难，因为很多组件都涉及了 DOM 操作。</p>
<p>例如一个上传图片组件，它有一个将图片转成 base64 码的方法，那要怎么测试呢？一般测试都是跑在 node 环境下的，而 node 环境没有 DOM 对象。</p>
<p>我们先来回顾一下上传图片的过程：</p>
<ol>
<li>点击 <code>&lt;input type="file" /&gt;</code>，选择图片上传。</li>
<li>触发 <code>input</code> 的 <code>change</code> 事件，获取 <code>file</code> 对象。</li>
<li>用 <code>FileReader</code> 将图片转换成 base64 码。</li>
</ol>
<p>这个过程和下面的代码是一样的：</p>
<pre><code class="language-js">document.querySelector('input').onchange = function fileChangeHandler(e) {
    const file = e.target.files[0]
    const reader = new FileReader()
    reader.onload = (res) =&gt; {
        const fileResult = res.target.result
        console.log(fileResult) // 输出 base64 码
    }

    reader.readAsDataURL(file)
}
</code></pre>
<p>上面的代码只是模拟，真实情况下应该是这样使用</p>
<pre><code class="language-js">document.querySelector('input').onchange = function fileChangeHandler(e) {
    const file = e.target.files[0]
    tobase64(file)
}

function tobase64(file) {
    return new Promise((resolve, reject) =&gt; {
        const reader = new FileReader()
        reader.onload = (res) =&gt; {
            const fileResult = res.target.result
            resolve(fileResult) // 输出 base64 码
        }

        reader.readAsDataURL(file)
    })
}
</code></pre>
<p>可以看到，上面代码出现了 window 的事件对象 <code>event</code>、<code>FileReader</code>。也就是说，只要我们能够提供这两个对象，就可以在任何环境下运行它。所以我们可以在测试环境下加上这两个对象：</p>
<pre><code class="language-js">// 重写 File
window.File = function () {}

// 重写 FileReader
window.FileReader = function () {
    this.readAsDataURL = function () {
        this.onload
            &amp;&amp; this.onload({
                target: {
                    result: fileData,
                },
            })
    }
}
</code></pre>
<p>然后测试可以这样写：</p>
<pre><code class="language-js">// 提前写好文件内容
const fileData = 'data:image/test'

// 提供一个假的 file 对象给 tobase64() 函数
function test() {
    const file = new File()
    const event = { target: { files: [file] } }
    file.type = 'image/png'
    file.name = 'test.png'
    file.size = 1024

    it('file content', (done) =&gt; {
        tobase64(file).then(base64 =&gt; {
            expect(base64).toEqual(fileData) // 'data:image/test'
            done()
        })
    })
}

// 执行测试
test()
</code></pre>
<p>通过这种 hack 的方式，我们就实现了对涉及 DOM 操作的组件的测试。我的 <a href="https://github.com/woai3c/vue-upload-imgs">vue-upload-imgs</a> 库就是通过这种方式写的单元测试，有兴趣可以了解一下。</p>
<h3 id="tdd">TDD 测试驱动开发</h3>
<p>TDD 就是根据需求提前把测试代码写好，然后根据测试代码实现功能。</p>
<p>TDD 的初衷是好的，但如果你的需求经常变（你懂的），那就不是一件好事了。很有可能你天天都在改测试代码，业务代码反而没怎么动。<br>
所以到现在为止，三年多的程序员生涯，我还没尝试过 TDD 开发。</p>
<p>虽然环境如此艰难，但有条件的情况下还是应该试一下 TDD 的。例如在你自己负责一个项目又不忙的时候，可以采用此方法编写测试用例。</p>
<h3 id="">测试框架推荐</h3>
<p>我常用的测试框架是 <a href="https://jestjs.io/docs/zh-Hans/getting-started">jest</a>，好处是有中文文档，API 清晰明了，一看就知道是干什么用的。</p>
<h2 id="">部署</h2>
<p>在没有学会自动部署前，我是这样部署项目的：</p>
<ol>
<li>执行测试 <code>npm run test</code>。</li>
<li>推送代码 <code>git push</code>。</li>
<li>构建项目 <code>npm run build</code>。</li>
<li>将打包好的文件放到静态服务器。</li>
</ol>
<p>一次两次还行，如果天天都这样，就会把很多时间浪费在重复的操作上。所以我们要学会自动部署，彻底解放双手。</p>
<p>自动部署（又叫持续部署 Continuous Deployment，英文缩写 CD）一般有两种触发方式：</p>
<ol>
<li>轮询。</li>
<li>监听 <code>webhook</code> 事件。</li>
</ol>
<h3 id="">轮询</h3>
<p>轮询，就是构建软件每隔一段时间自动执行打包、部署操作。</p>
<p>这种方式不太好，很有可能软件刚部署完我就改代码了。为了看到新的页面效果，不得不等到下一次构建开始。</p>
<p>另外还有一个副作用，假如我一天都没更改代码，构建软件还是会不停的执行打包、部署操作，白白的浪费资源。</p>
<p>所以现在的构建软件基本采用监听 <code>webhook</code> 事件的方式来进行部署。</p>
<h3 id="webhook">监听 <code>webhook</code> 事件</h3>
<p>webhook 钩子函数，就是在你的构建软件上进行设置，监听某一个事件（一般是监听 <code>push</code> 事件），当事件触发时，自动执行定义好的脚本。</p>
<p>例如 <code>Github Actions</code>，就有这个功能。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d042f518cdce4a1b90ab165d256001aa~tplv-k3u1fbpfcp-watermark.image" alt="d042f518cdce4a1b90ab165d256001aa~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>对于新人来说，仅看我这一段讲解是不可能学会自动部署的。为此我特地写了一篇自动化部署教程，不需要你提前学习自动化部署的知识，只要照着指引做，就能实现前端项目自动化部署。</p>
<p><a href="https://juejin.im/post/6887751398499287054">前端项目自动化部署——超详细教程（Jenkins、Github Actions）</a>，教程已经奉上，各位大佬看完后要是觉得有用，不要忘了点赞，感激不尽。</p>
<h2 id="">监控</h2>
<p>监控，又分性能监控和错误监控，它的作用是预警和追踪定位问题。</p>
<h3 id="">性能监控</h3>
<p>性能监控一般利用 <code>window.performance</code> 来进行数据采集。</p>
<blockquote>
<p>Performance 接口可以获取到当前页面中与性能相关的信息，它是 High Resolution Time API 的一部分，同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。</p>
</blockquote>
<p>这个 API 的属性 <code>timing</code>，包含了页面加载各个阶段的起始及结束时间。</p>
<p><img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/43b6b58259a14129914cea4f20071ba7~tplv-k3u1fbpfcp-zoom-1.image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"><br>
<img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/df66c22c29c64be7bc4d16e7aedffae3~tplv-k3u1fbpfcp-zoom-1.image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></p>
<p>为了方便大家理解 <code>timing</code> 各个属性的意义，我在知乎找到一位网友对于 <code>timing</code> 写的简介（忘了姓名，后来找不到了，见谅），在此转载一下。</p>
<pre><code class="language-js">timing: {
        // 同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面，这个值会和fetchStart相同。
	navigationStart: 1543806782096,

	// 上一个页面unload事件抛出时的时间戳。如果没有上一个页面，这个值会返回0。
	unloadEventStart: 1543806782523,

	// 和 unloadEventStart 相对应，unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。
	unloadEventEnd: 1543806782523,

	// 第一个HTTP重定向开始时的时间戳。如果没有重定向，或者重定向中的一个不同源，这个值会返回0。
	redirectStart: 0,

	// 最后一个HTTP重定向完成时（也就是说是HTTP响应的最后一个比特直接被收到的时间）的时间戳。
	// 如果没有重定向，或者重定向中的一个不同源，这个值会返回0. 
	redirectEnd: 0,

	// 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。
	fetchStart: 1543806782096,

	// DNS 域名查询开始的UNIX时间戳。
        //如果使用了持续连接(persistent connection)，或者这个信息存储到了缓存或者本地资源上，这个值将和fetchStart一致。
	domainLookupStart: 1543806782096,

	// DNS 域名查询完成的时间.
	//如果使用了本地缓存（即无 DNS 查询）或持久连接，则与 fetchStart 值相等
	domainLookupEnd: 1543806782096,

	// HTTP（TCP） 域名查询结束的时间戳。
        //如果使用了持续连接(persistent connection)，或者这个信息存储到了缓存或者本地资源上，这个值将和 fetchStart一致。
	connectStart: 1543806782099,

	// HTTP（TCP） 返回浏览器与服务器之间的连接建立时的时间戳。
        // 如果建立的是持久连接，则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。
	connectEnd: 1543806782227,

	// HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接，则返回0。
	secureConnectionStart: 1543806782162,

	// 返回浏览器向服务器发出HTTP请求时（或开始读取本地缓存时）的时间戳。
	requestStart: 1543806782241,

	// 返回浏览器从服务器收到（或从本地缓存读取）第一个字节时的时间戳。
        //如果传输层在开始请求之后失败并且连接被重开，该属性将会被数制成新的请求的相对应的发起时间。
	responseStart: 1543806782516,

	// 返回浏览器从服务器收到（或从本地缓存读取，或从本地资源读取）最后一个字节时
        //（如果在此之前HTTP连接已经关闭，则返回关闭时）的时间戳。
	responseEnd: 1543806782537,

	// 当前网页DOM结构开始解析时（即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时）的时间戳。
	domLoading: 1543806782573,

	// 当前网页DOM结构结束解析、开始加载内嵌资源时（即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时）的时间戳。
	domInteractive: 1543806783203,

	// 当解析器发送DOMContentLoaded 事件，即所有需要被执行的脚本已经被解析时的时间戳。
	domContentLoadedEventStart: 1543806783203,

	// 当所有需要立即执行的脚本已经被执行（不论执行顺序）时的时间戳。
	domContentLoadedEventEnd: 1543806783216,

	// 当前文档解析完成，即Document.readyState 变为 'complete'且相对应的readystatechange 被触发时的时间戳
	domComplete: 1543806783796,

	// load事件被发送时的时间戳。如果这个事件还未被发送，它的值将会是0。
	loadEventStart: 1543806783796,

	// 当load事件结束，即加载事件完成时的时间戳。如果这个事件还未被发送，或者尚未完成，它的值将会是0.
	loadEventEnd: 1543806783802
}
</code></pre>
<p>通过以上数据，我们可以得到几个有用的时间</p>
<pre><code class="language-js">// 重定向耗时
redirect: timing.redirectEnd - timing.redirectStart,
// DOM 渲染耗时
dom: timing.domComplete - timing.domLoading,
// 页面加载耗时
load: timing.loadEventEnd - timing.navigationStart,
// 页面卸载耗时
unload: timing.unloadEventEnd - timing.unloadEventStart,
// 请求耗时
request: timing.responseEnd - timing.requestStart,
// 获取性能信息时当前时间
time: new Date().getTime(),
</code></pre>
<p>还有一个比较重要的时间就是<strong>白屏时间</strong>，它指从输入网址，到页面开始显示内容的时间。</p>
<p>将以下脚本放在 <code>&lt;/head&gt;</code> 前面就能获取白屏时间。</p>
<pre><code class="language-html">&lt;script&gt;
    whiteScreen = new Date() - performance.timing.navigationStart
&lt;/script&gt;
</code></pre>
<p>通过这几个时间，就可以得知页面首屏加载性能如何了。</p>
<p>另外，通过 <code>window.performance.getEntriesByType('resource')</code> 这个方法，我们还可以获取相关资源（js、css、img...）的加载时间，它会返回页面当前所加载的所有资源。</p>
<p><img src="//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/522dff06f2b445c193ffa7f4b365a9b4~tplv-k3u1fbpfcp-zoom-1.image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></p>
<p>它一般包括以下几个类型</p>
<ul>
<li>sciprt</li>
<li>link</li>
<li>img</li>
<li>css</li>
<li>fetch</li>
<li>other</li>
<li>xmlhttprequest</li>
</ul>
<p>我们只需用到以下几个信息</p>
<pre><code class="language-js">// 资源的名称
name: item.name,
// 资源加载耗时
duration: item.duration.toFixed(2),
// 资源大小
size: item.transferSize,
// 资源所用协议
protocol: item.nextHopProtocol,
</code></pre>
<p>现在，写几行代码来收集这些数据。</p>
<pre><code class="language-js">// 收集性能信息
const getPerformance = () =&gt; {
    if (!window.performance) return
    const timing = window.performance.timing
    const performance = {
        // 重定向耗时
        redirect: timing.redirectEnd - timing.redirectStart,
        // 白屏时间
        whiteScreen: whiteScreen,
        // DOM 渲染耗时
        dom: timing.domComplete - timing.domLoading,
        // 页面加载耗时
        load: timing.loadEventEnd - timing.navigationStart,
        // 页面卸载耗时
        unload: timing.unloadEventEnd - timing.unloadEventStart,
        // 请求耗时
        request: timing.responseEnd - timing.requestStart,
        // 获取性能信息时当前时间
        time: new Date().getTime(),
    }

    return performance
}

// 获取资源信息
const getResources = () =&gt; {
    if (!window.performance) return
    const data = window.performance.getEntriesByType('resource')
    const resource = {
        xmlhttprequest: [],
        css: [],
        other: [],
        script: [],
        img: [],
        link: [],
        fetch: [],
        // 获取资源信息时当前时间
        time: new Date().getTime(),
    }

    data.forEach(item =&gt; {
        const arry = resource[item.initiatorType]
        arry &amp;&amp; arry.push({
            // 资源的名称
            name: item.name,
            // 资源加载耗时
            duration: item.duration.toFixed(2),
            // 资源大小
            size: item.transferSize,
            // 资源所用协议
            protocol: item.nextHopProtocol,
        })
    })

    return resource
}
</code></pre>
<h4 id="">小结</h4>
<p>通过对性能及资源信息的解读，我们可以判断出页面加载慢有以下几个原因：</p>
<ol>
<li>资源过多</li>
<li>网速过慢</li>
<li>DOM元素过多</li>
</ol>
<p>除了用户网速过慢，我们没办法之外，其他两个原因都是有办法解决的，性能优化将在下一节《性能优化》中会讲到。</p>
<h3 id="">错误监控</h3>
<p>现在能捕捉的错误有三种。</p>
<ol>
<li>资源加载错误，通过 <code>addEventListener('error', callback, true)</code> 在捕获阶段捕捉资源加载失败错误。</li>
<li>js 执行错误，通过 <code>window.onerror</code> 捕捉 js 错误。</li>
<li>promise 错误，通过 <code>addEventListener('unhandledrejection', callback)</code>捕捉 promise 错误，但是没有发生错误的行数，列数等信息，只能手动抛出相关错误信息。</li>
</ol>
<p>我们可以建一个错误数组变量 <code>errors</code> 在错误发生时，将错误的相关信息添加到数组，然后在某个阶段统一上报，具体如何操作请看代码</p>
<pre><code class="language-js">// 捕获资源加载失败错误 js css img...
addEventListener('error', e =&gt; {
    const target = e.target
    if (target != window) {
        monitor.errors.push({
            type: target.localName,
            url: target.src || target.href,
            msg: (target.src || target.href) + ' is load error',
            // 错误发生的时间
            time: new Date().getTime(),
        })
    }
}, true)

// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {
    monitor.errors.push({
        type: 'javascript',
        row: row,
        col: col,
        msg: error &amp;&amp; error.stack? error.stack : msg,
        url: url,
        // 错误发生的时间
        time: new Date().getTime(),
    })
}

// 监听 promise 错误 缺点是获取不到行数数据
addEventListener('unhandledrejection', e =&gt; {
    monitor.errors.push({
        type: 'promise',
        msg: (e.reason &amp;&amp; e.reason.msg) || e.reason || '',
        // 错误发生的时间
        time: new Date().getTime(),
    })
})
</code></pre>
<h4 id="">小结</h4>
<p>通过错误收集，可以了解到网站错误发生的类型及数量，从而可以做相应的调整，以减少错误发生。<br>
完整代码和 DEMO 请看我另一篇文章<a href="https://juejin.im/post/6844903998412029959">前端性能和错误监控</a>的末尾，大家可以复制代码（HTML文件）在本地测试一下。</p>
<h3 id="">数据上报</h3>
<h4 id="">性能数据上报</h4>
<p>性能数据可以在页面加载完之后上报，尽量不要对页面性能造成影响。</p>
<pre><code class="language-js">window.onload = () =&gt; {
    // 在浏览器空闲时间获取性能及资源信息
    // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    if (window.requestIdleCallback) {
        window.requestIdleCallback(() =&gt; {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        })
    } else {
        setTimeout(() =&gt; {
            monitor.performance = getPerformance()
            monitor.resources = getResources()
        }, 0)
    }
}
</code></pre>
<p>当然，你也可以设一个定时器，循环上报。不过每次上报最好做一下对比去重再上报，避免同样的数据重复上报。</p>
<h4 id="">错误数据上报</h4>
<p>我在DEMO里提供的代码，是用一个 <code>errors</code> 数组收集所有的错误，再在某一阶段统一上报（延时上报）。<br>
其实，也可以改成在错误发生时上报（即时上报）。这样可以避免在收集完错误延时上报还没触发，用户却已经关掉网页导致错误数据丢失的问题。</p>
<pre><code class="language-js">// 监听 js 错误
window.onerror = function(msg, url, row, col, error) {
    const data = {
        type: 'javascript',
        row: row,
        col: col,
        msg: error &amp;&amp; error.stack? error.stack : msg,
        url: url,
        // 错误发生的时间
        time: new Date().getTime(),
    }
    
    // 即时上报
    axios.post({ url: 'xxx', data, })
}
</code></pre>
<h3 id="spa">SPA</h3>
<p><code>window.performance</code> API 是有缺点的，在 SPA 切换路由时，<code>window.performance.timing</code> 的数据不会更新。<br>
所以我们需要另想办法来统计切换路由到加载完成的时间。<br>
拿 Vue 举例，一个可行的办法就是切换路由时，在路由的全局前置守卫 <code>beforeEach</code> 里获取开始时间，在组件的 <code>mounted</code> 钩子里执行 <code>vm.$nextTick </code> 函数来获取组件的渲染完毕时间。</p>
<pre><code class="language-js">router.beforeEach((to, from, next) =&gt; {
	store.commit('setPageLoadedStartTime', new Date())
})
</code></pre>
<pre><code class="language-js">mounted() {
	this.$nextTick(() =&gt; {
		this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)
	})
}
</code></pre>
<p>除了性能和错误监控，其实我们还可以做得更多。</p>
<h3 id="">用户信息收集</h3>
<h4 id="navigator">navigator</h4>
<p>使用 <code>window.navigator</code> 可以收集到用户的设备信息，操作系统，浏览器信息...</p>
<h4 id="uvuniquevisitor">UV（Unique visitor）</h4>
<p>是指通过互联网访问、浏览这个网页的自然人。访问您网站的一台电脑客户端为一个访客。00:00-24:00内相同的客户端只被计算一次。一天内同个访客多次访问仅计算一个UV。<br>
在用户访问网站时，可以生成一个随机字符串+时间日期，保存在本地。在网页发生请求时（如果超过当天24小时，则重新生成），把这些参数传到后端，后端利用这些信息生成 UV 统计报告。</p>
<h4 id="pvpageview">PV（Page View）</h4>
<p>即页面浏览量或点击量，用户每1次对网站中的每个网页访问均被记录1个PV。用户对同一页面的多次访问，访问量累计，用以衡量网站用户访问的网页数量。</p>
<h4 id="">页面停留时间</h4>
<p><strong>传统网站</strong><br>
用户在进入 A 页面时，通过后台请求把用户进入页面的时间捎上。过了 10 分钟，用户进入 B 页面，这时后台可以通过接口捎带的参数可以判断出用户在 A 页面停留了 10 分钟。<br>
<strong>SPA</strong><br>
可以利用 router 来获取用户停留时间，拿 Vue 举例，通过 <code>router.beforeEach</code> <code>destroyed</code> 这两个钩子函数来获取用户停留该路由组件的时间。</p>
<h4 id="">浏览深度</h4>
<p>通过 <code>document.documentElement.scrollTop</code> 属性以及屏幕高度，可以判断用户是否浏览完网站内容。</p>
<h4 id="">页面跳转来源</h4>
<p>通过 <code>document.referrer</code> 属性，可以知道用户是从哪个网站跳转而来。</p>
<h4 id="">小结</h4>
<p>通过分析用户数据，我们可以了解到用户的浏览习惯、爱好等等信息，想想真是恐怖，毫无隐私可言。</p>
<h3 id="">前端监控部署教程</h3>
<p>前面说的都是监控原理，但要实现还是得自己动手写代码。为了避免麻烦，我们可以用现有的工具 sentry 去做这件事。</p>
<p>sentry 是一个用 python 写的性能和错误监控工具，你可以使用 sentry 提供的服务（免费功能少），也可以自己部署服务。现在来看一下如何使用 sentry 提供的服务实现监控。</p>
<h4 id="">注册账号</h4>
<p>打开 <code>https://sentry.io/signup/</code> 网站，进行注册。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efae84d48d4143f895dfda7ef88a3354~tplv-k3u1fbpfcp-watermark.image" alt="efae84d48d4143f895dfda7ef88a3354~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3846c1b87e84b6d90c771b0c8198068~tplv-k3u1fbpfcp-watermark.image" alt="f3846c1b87e84b6d90c771b0c8198068~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>选择项目，我选的 Vue。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ed48cc42fa194f9cbca11550b471139e~tplv-k3u1fbpfcp-watermark.image" alt="ed48cc42fa194f9cbca11550b471139e~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<h4 id="sentry">安装 sentry 依赖</h4>
<p>选完项目，下面会有具体的 sentry 依赖安装指南。</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ddfb72545299440a940b650d577cf77f~tplv-k3u1fbpfcp-watermark.image" alt="ddfb72545299440a940b650d577cf77f~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>根据提示，在你的 Vue 项目执行这段代码 <code>npm install --save @sentry/browser @sentry/integrations @sentry/tracing</code>，安装 sentry 所需的依赖。</p>
<p>再将下面的代码拷到你的 <code>main.js</code>，放在 <code>new Vue()</code> 之前。</p>
<pre><code class="language-js">import * as Sentry from "@sentry/browser";
import { Vue as VueIntegration } from "@sentry/integrations";
import { Integrations } from "@sentry/tracing";

Sentry.init({
  dsn: "xxxxx", // 这里是你的 dsn 地址，注册完就有
  integrations: [
    new VueIntegration({
      Vue,
      tracing: true,
    }),
    new Integrations.BrowserTracing(),
  ],

  // We recommend adjusting this value in production, or using tracesSampler
  // for finer control
  tracesSampleRate: 1.0,
});
</code></pre>
<p>然后点击第一步中的 <code>skip this onboarding</code>，进入控制台页面。</p>
<p>如果忘了自己的 DSN，请点击左边的菜单栏选择 <code>Settings</code> -&gt; <code>Projects</code> -&gt; 点击自己的项目 -&gt; <code>Client Keys(DSN)</code>。</p>
<h4 id="">创建第一个错误</h4>
<p>在你的 Vue 项目执行一个打印语句 <code>console.log(b)</code>。</p>
<p>这时点开 sentry 主页的 issues 一项，可以发现有一个报错信息 <code>b is not defined</code>：</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/24e28dd2f6034719bdfc35d8716b6ddf~tplv-k3u1fbpfcp-watermark.image" alt="24e28dd2f6034719bdfc35d8716b6ddf~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>这个报错信息包含了错误的具体信息，还有你的 IP、浏览器信息等等。</p>
<p>但奇怪的是，我们的浏览器控制台并没有输出报错信息。</p>
<p>这是因为被 sentry 屏蔽了，所以我们需要加上一个选项 <code>logErrors: true</code>。</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/98dd3b5cb75649eaa2e2f0648ed5fd5b~tplv-k3u1fbpfcp-watermark.image" alt="98dd3b5cb75649eaa2e2f0648ed5fd5b~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>然后再查看页面，发现控制台也有报错信息了：</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa26d5007bc24c919c56283dfe2c92b1~tplv-k3u1fbpfcp-watermark.image" alt="aa26d5007bc24c919c56283dfe2c92b1~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<h4 id="sourcemap">上传 sourcemap</h4>
<p>一般打包后的代码都是经过压缩的，如果没有 sourcemap，即使有报错信息，你也很难根据提示找到对应的源码在哪。</p>
<p>下面来看一下如何上传 sourcemap。</p>
<p>首先创建 auth token。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cf704d9cefa4e84941caaea3c096b69~tplv-k3u1fbpfcp-watermark.image" alt="5cf704d9cefa4e84941caaea3c096b69~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/77dd5fde72634376a53a929fe3a06f66~tplv-k3u1fbpfcp-watermark.image" alt="77dd5fde72634376a53a929fe3a06f66~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb9cff485e0b4120a6168040d81380bc~tplv-k3u1fbpfcp-watermark.image" alt="bb9cff485e0b4120a6168040d81380bc~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b07fc31573d43b9ac59522b144bcd4e~tplv-k3u1fbpfcp-watermark.image" alt="7b07fc31573d43b9ac59522b144bcd4e~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>这个生成的 token 一会要用到。</p>
<p>安装 <code>sentry-cli</code> 和 <code>@sentry/webpack-plugin</code>：</p>
<pre><code>npm install sentry-cli-binary -g
npm install --save-dev @sentry/webpack-plugin
</code></pre>
<p>安装完上面两个插件后，在项目根目录创建一个 <code>.sentryclirc</code> 文件（不要忘了在 <code>.gitignore</code> 把这个文件添加上，以免暴露 token），内容如下：</p>
<pre><code>[auth]
token=xxx

[defaults]
url=https://sentry.io/
org=woai3c
project=woai3c
</code></pre>
<p>把 xxx 替换成刚才生成的 token。</p>
<p><code>org</code> 是你的组织名称。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c01d66fa8c6144b2bcc8c2bad7be46f7~tplv-k3u1fbpfcp-watermark.image" alt="c01d66fa8c6144b2bcc8c2bad7be46f7~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><code>project</code> 是你的项目名称，根据下面的提示可以找到。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a2bc1398957f4bc887365872e5c19724~tplv-k3u1fbpfcp-watermark.image" alt="a2bc1398957f4bc887365872e5c19724~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ab01b819e9044e3984406fa84bd9d0fa~tplv-k3u1fbpfcp-watermark.image" alt="ab01b819e9044e3984406fa84bd9d0fa~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>在项目下新建 <code>vue.config.js</code> 文件，把下面的内容填进去：</p>
<pre><code class="language-js">const SentryWebpackPlugin = require('@sentry/webpack-plugin')

const config = {
    configureWebpack: {
        plugins: [
            new SentryWebpackPlugin({
                include: './dist', // 打包后的目录
                ignore: ['node_modules', 'vue.config.js', 'babel.config.js'],
            }),
        ],
    },
}

// 只在生产环境下上传 sourcemap
module.exports = process.env.NODE_ENV == 'production'? config : {}
</code></pre>
<p>填完以后，执行 <code>npm run build</code>，就可以看到 <code>sourcemap</code> 的上传结果了。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3b0b21bca0324f8a9e296d4d0aa782e0~tplv-k3u1fbpfcp-watermark.image" alt="3b0b21bca0324f8a9e296d4d0aa782e0~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>我们再来看一下没上传 sourcemap 和上传之后的报错信息对比。</p>
<p><strong>未上传 sourcemap</strong></p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37b07f06365940269eac40ae953ea45e~tplv-k3u1fbpfcp-watermark.image" alt="37b07f06365940269eac40ae953ea45e~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ad3ab31aded40bdb788e5a117b1611c~tplv-k3u1fbpfcp-watermark.image" alt="9ad3ab31aded40bdb788e5a117b1611c~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><strong>已上传 sourcemap</strong></p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9406ccfc46c42d1b9f499fb3f5e4280~tplv-k3u1fbpfcp-watermark.image" alt="b9406ccfc46c42d1b9f499fb3f5e4280~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f066c98785e4597833b4a1600d27aa8~tplv-k3u1fbpfcp-watermark.image" alt="0f066c98785e4597833b4a1600d27aa8~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>可以看到，上传 sourcemap 后的报错信息更加准确。</p>
<h4 id="">切换中文环境和时区</h4>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7523abeeda95418880ff5683b1baf8a9~tplv-k3u1fbpfcp-watermark.image" alt="7523abeeda95418880ff5683b1baf8a9~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/76b994e92044402c82dec0b2d1e5a762~tplv-k3u1fbpfcp-watermark.image" alt="76b994e92044402c82dec0b2d1e5a762~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>选完刷新即可。</p>
<h4 id="">性能监控</h4>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/69b21fe514ee466292306c2ce4eaa672~tplv-k3u1fbpfcp-watermark.image" alt="69b21fe514ee466292306c2ce4eaa672~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>打开 performance 选项，就能看到你每个项目的运行情况。具体的参数解释请看文档 <a href="https://docs.sentry.io/product/performance/">Performance Monitoring</a>。</p>
<h2 id="">性能优化</h2>
<p>性能优化主要分为两类：</p>
<ol>
<li>加载时优化</li>
<li>运行时优化</li>
</ol>
<p>例如压缩文件、使用 CDN 就属于加载时优化；减少 DOM 操作，使用事件委托属于运行时优化。</p>
<p>在解决问题之前，必须先找出问题，否则无从下手。所以在做性能优化之前，最好先调查一下网站的加载性能和运行性能。</p>
<h3 id="">手动检查</h3>
<h4 id="">检查加载性能</h4>
<p>一个网站加载性能如何主要看白屏时间和首屏时间。</p>
<ul>
<li>白屏时间：指从输入网址，到页面开始显示内容的时间。</li>
<li>首屏时间：指从输入网址，到页面完全渲染的时间。</li>
</ul>
<p>将以下脚本放在 <code>&lt;/head&gt;</code> 前面就能获取白屏时间。</p>
<pre><code class="language-html">&lt;script&gt;
	new Date() - performance.timing.navigationStart
&lt;/script&gt;
</code></pre>
<p>首屏时间比较复杂，得考虑有图片和没有图片的情况。</p>
<p>如果没有图片，则在 <code>window.onload</code> 事件里执行 <code>new Date() - performance.timing.navigationStart</code> 即可获取首屏时间。</p>
<p>如果有图片，则要在最后一个在首屏渲染的图片的 <code>onload</code> 事件里执行 <code>new Date() - performance.timing.navigationStart</code> 获取首屏时间，实施起来比较复杂，在这里限于篇幅就不说了。</p>
<h4 id="">检查运行性能</h4>
<p>配合 chrome 的开发者工具，我们可以查看网站在运行时的性能。</p>
<p>打开网站，按 F12 选择 performance，点击左上角的灰色圆点，变成红色就代表开始记录了。这时可以模仿用户使用网站，在使用完毕后，点击 stop，然后你就能看到网站运行期间的性能报告。如果有红色的块，代表有掉帧的情况；如果是绿色，则代表 FPS 很好。</p>
<p>另外，在 performance 标签下，按 ESC 会弹出来一个小框。点击小框左边的三个点，把 rendering 勾出来。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/94fda5308b6f493cab3c48b692f3a7c6~tplv-k3u1fbpfcp-watermark.image" alt="94fda5308b6f493cab3c48b692f3a7c6~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0559bb7fa1164e4eab9a77354040e06a~tplv-k3u1fbpfcp-watermark.image" alt="0559bb7fa1164e4eab9a77354040e06a~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>这两个选项，第一个是高亮重绘区域，另一个是显示帧渲染信息。把这两个选项勾上，然后浏览网页，可以实时的看到你网页渲染变化。</p>
<h3 id="">利用工具检查</h3>
<h4 id="">监控工具</h4>
<p>可以部署一个前端监控系统来监控网站性能，上一节中讲到的 sentry 就属于这一类。</p>
<h4 id="chromelighthouse">chrome 工具 Lighthouse</h4>
<p>如果你安装了 Chrome 52+ 版本，请按 F12 打开开发者工具。<br>
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1dca9942cbd746d6ac3d25a9894fe9c0~tplv-k3u1fbpfcp-watermark.image" alt="1dca9942cbd746d6ac3d25a9894fe9c0~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12234695750d422e9872d5c2d6a72834~tplv-k3u1fbpfcp-watermark.image" alt="12234695750d422e9872d5c2d6a72834~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p>它不仅会对你网站的性能打分，还会对 SEO 打分。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c709fb1a21aa46449c2a3633bda50464~tplv-k3u1fbpfcp-watermark.image" alt="c709fb1a21aa46449c2a3633bda50464~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></p>
<p><a href="https://developers.google.com/web/tools/lighthouse">使用 Lighthouse 审查网络应用</a></p>
<h3 id="">如何做性能优化</h3>
<p>网上关于性能优化的文章和书籍多不胜数，但有很多优化规则已经过时了。所以我写了一篇性能优化文章<a href="https://zhuanlan.zhihu.com/p/121056616">前端性能优化 24 条建议(2020)</a>，分析总结出了 24 条性能优化建议，强烈推荐。</p>
<h2 id="">重构</h2>
<p><a href="https://book.douban.com/subject/30468597/">《重构2》</a>一书中对重构进行了定义：</p>
<blockquote>
<p>所谓重构（refactoring）是这样一个过程：在不改变代码外在行为的前提下，对代码做出修改，以改进程序的内部结构。重构是一种经千锤百炼形成的有条不紊的程序整理方法，可以最大限度地减小整理过程中引入错误的概率。本质上说，重构就是在代码写好之后改进它的设计。</p>
</blockquote>
<p>重构和性能优化有相同点，也有不同点。</p>
<p>相同的地方是它们都在不改变程序功能的情况下修改代码；不同的地方是重构为了让代码变得更加易读、理解，性能优化则是为了让程序运行得更快。</p>
<p>重构可以一边写代码一边重构，也可以在程序写完后，拿出一段时间专门去做重构。没有说哪个方式更好，视个人情况而定。</p>
<p>如果你专门拿一段时间来做重构，建议你在重构一段代码后，立即进行测试。这样可以避免修改代码太多，在出错时找不到错误点。</p>
<h3 id="">重构的原则</h3>
<ol>
<li>事不过三，三则重构。即不能重复写同样的代码，在这种情况下要去重构。</li>
<li>如果一段代码让人很难看懂，那就该考虑重构了。</li>
<li>如果已经理解了代码，但是非常繁琐或者不够好，也可以重构。</li>
<li>过长的函数，需要重构。</li>
<li>一个函数最好对应一个功能，如果一个函数被塞入多个功能，那就要对它进行重构了。</li>
</ol>
<h3 id="">重构手法</h3>
<p>在<a href="https://book.douban.com/subject/30468597/">《重构2》</a>这本书中，介绍了多达上百个重构手法。但我觉得有两个是比较常用的：</p>
<ol>
<li>提取重复代码，封装成函数</li>
<li>拆分太长或功能太多的函数</li>
</ol>
<h4 id="">提取重复代码，封装成函数</h4>
<p>假设有一个查询数据的接口 <code>/getUserData?age=17&amp;city=beijing</code>。现在需要做的是把用户数据：<code>{ age: 17, city: 'beijing' }</code> 转成 URL 参数的形式：</p>
<pre><code class="language-js">let result = ''
const keys = Object.keys(data)  // { age: 17, city: 'beijing' }
keys.forEach(key =&gt; {
    result += '&amp;' + key + '=' + data[key]
})

result.substr(1) // age=17&amp;city=beijing
</code></pre>
<p>如果只有这一个接口需要转换，不封装成函数是没问题的。但如果有多个接口都有这种需求，那就得把它封装成函数了：</p>
<pre><code class="language-js">function JSON2Params(data) {
    let result = ''
    const keys = Object.keys(data)
    keys.forEach(key =&gt; {
        result += '&amp;' + key + '=' + data[key]
    })

    return result.substr(1)
}
</code></pre>
<h4 id="">拆分太长或功能太多的函数</h4>
<p>假设现在有一个注册功能，用伪代码表示：</p>
<pre><code class="language-js">function register(data) {
    // 1. 验证用户数据是否合法
    /**
     * 验证账号
     * 验证密码
     * 验证短信验证码
     * 验证身份证
     * 验证邮箱
     */

    // 2. 如果用户上传了头像，则将用户头像转成 base64 码保存
    /**
     * 新建 FileReader 对象
     * 将图片转换成 base64 码
     */

    // 3. 调用注册接口
    // ...
}
</code></pre>
<p>这个函数包含了三个功能，验证、转换、注册。其中验证和转换功能是可以提取出来单独封装成函数的：</p>
<pre><code class="language-js">function register(data) {
    // 1. 验证用户数据是否合法
    // verify()

    // 2. 如果用户上传了头像，则将用户头像转成 base64 码保存
    // tobase64()

    // 3. 调用注册接口
    // ...
}
</code></pre>
<p>如果你对重构有兴趣，强烈推荐你阅读<a href="https://book.douban.com/subject/30468597/">《重构2》</a>这本书。</p>
<p>参考资料：</p>
<ul>
<li><a href="https://book.douban.com/subject/30468597/">《重构2》</a></li>
</ul>
<h2 id="">总结</h2>
<p>写这篇文章主要是为了对我这一年多工作经验作总结，因为我基本上都在研究前端工程化以及如何提升团队的开发效率。希望这篇文章能帮助一些对前端工程化没有经验的新手，通过这篇文章入门前端工程化。</p>
<p>如果这篇文章对你有帮助，请点一下赞，感激不尽。</p>
<h3 id="">求职启事</h3>
<p>本人具有三年+前端工作经验，32岁，高中学历，现寻求天津、北京地区的前端工作机会。</p>
<p>下面是我掌握的一些技能：</p>
<ol>
<li>熟练掌握 HTML、CSS、JavaScript。</li>
<li>熟练掌握 Vue 全家桶并研究过 Vue1.0 源码及 Vue3.0 部分源码。</li>
<li>使用 nodejs 写过脚本和<a href="https://github.com/woai3c/node-blog">个人博客</a>，没有开发过企业应用。</li>
<li>学习计算机原理并实现一个简单的 cpu 和内存模块运行在模拟器上（<a href="https://github.com/woai3c/nand2tetris">github 项目地址</a>）。</li>
<li>学习操作系统并做实验实现了一个简单的内核（<a href="https://github.com/woai3c/MIT6.828">github 项目地址</a>）。</li>
<li>学习编译原理写过一个简单编译器（<a href="https://github.com/woai3c/nand2tetris">github 项目地址</a>）。</li>
<li>对计算机网络应用层和传输层的知识比较了解。</li>
<li>数据结构与算法有学习过，还刷了 300+ 道 leetcode，但效果不是很好。</li>
</ol>
<h4 id="">社交网站</h4>
<ul>
<li><a href="https://github.com/woai3c">Github</a></li>
<li><a href="https://www.zhihu.com/people/tan-guang-zhi-19">知乎</a></li>
</ul>
<p>如果您觉得我的条件还可以，可以私信我或在评论区留言，谢谢。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 手把手教你写一个简易的微前端框架 ]]>
                </title>
                <description>
                    <![CDATA[ 最近看了几个微前端框架的源码（single-spa [https://github.com/single-spa/single-spa]、qiankun [https://github.com/umijs/qiankun]、micro-app [https://github.com/micro-zoe/micro-app]），感觉收获良多。所以打算造一个迷你版的轮子，来加深自己对所学知识的了解。 这个轮子将分为五个版本，逐步的实现一个最小可用的微前端框架：  1. 支持不同框架的子应用（v1 [https://github.com/woai3c/mini-single-spa/tree/v1] 分支）  2. 支持子应用 HTML 入口（v2 [https://github.com/woai3c/mini-single-spa/tree/v2] 分支）  3. 支持沙箱功能，子应用 window 作用域隔离、元素隔离（v3     [https://github.com/woai3c/mini-single-spa/tree/v3] 分支）  4. 支持子应用样式隔离（v4 [ht ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/build-a-micro-frontend-framework/</link>
                <guid isPermaLink="false">621b30a579a578061070cd55</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Mon, 19 Jul 2021 02:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/02/danielle-macinnes-IuLgi9PWETU-unsplash-1.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近看了几个微前端框架的源码（<a href="https://github.com/single-spa/single-spa" rel="noopener">single-spa</a>、<a href="https://github.com/umijs/qiankun" rel="noopener">qiankun</a>、<a href="https://github.com/micro-zoe/micro-app" rel="noopener">micro-app</a>），感觉收获良多。所以打算造一个迷你版的轮子，来加深自己对所学知识的了解。</p><p>这个轮子将分为五个版本，逐步的实现一个最小可用的微前端框架：</p><ol><li>支持不同框架的子应用（<a href="https://github.com/woai3c/mini-single-spa/tree/v1" rel="noopener">v1</a> 分支）</li><li>支持子应用 HTML 入口（<a href="https://github.com/woai3c/mini-single-spa/tree/v2" rel="noopener">v2</a> 分支）</li><li>支持沙箱功能，子应用 window 作用域隔离、元素隔离（<a href="https://github.com/woai3c/mini-single-spa/tree/v3" rel="noopener">v3</a> 分支）</li><li>支持子应用样式隔离（<a href="https://github.com/woai3c/mini-single-spa/tree/v4" rel="noopener">v4</a> 分支）</li><li>支持各应用之间的数据通信（<a href="https://github.com/woai3c/mini-single-spa" rel="noopener">main</a> 分支）</li></ol><p>每一个版本的代码都是在上一个版本的基础上修改的，所以 V5 版本的代码是最终代码。</p><p>Github 项目地址：<a href="https://github.com/woai3c/mini-single-spa" rel="noopener">https://github.com/woai3c/mini-single-spa</a></p><h2 id="v1-"><strong><strong>V1 版本</strong></strong></h2><p>V1 版本打算实现一个最简单的微前端框架，只要它能够正常加载、卸载子应用就行。如果将 V1 版本细分一下的话，它主要由以下两个功能组成：</p><ol><li>监听页面 URL 变化，切换子应用</li><li>根据当前 URL、子应用的触发规则来判断是否要加载、卸载子应用</li></ol><h3 id="-url-"><strong><strong>监听页面 URL 变化，切换子应用</strong></strong></h3><p>一个 SPA 应用必不可少的功能就是监听页面 URL 的变化，然后根据不同的路由规则来渲染不同的路由组件。因此，微前端框架也可以根据页面 URL 的变化，来切换到不同的子应用：</p><pre><code class="language-js">// 当 location.pathname 以 /vue 为前缀时切换到 vue 子应用
https://www.example.com/vue/xxx
// 当 location.pathname 以 /react 为前缀时切换到 react 子应用
https://www.example.com/react/xxx
</code></pre><p>这可以通过重写两个 API 和监听两个事件来完成：</p><ol><li>重写 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/History/pushState" rel="noopener">window.history.pushState()</a></li><li>重写 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/History/replaceState" rel="noopener">window.history.replaceState()</a></li><li>监听 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event" rel="noopener">popstate</a> 事件</li><li>监听 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/hashchange_event" rel="noopener">hashchange</a> 事件</li></ol><p>其中 <code>pushState()</code>、<code>replaceState()</code> 方法可以修改浏览器的历史记录栈，所以我们可以重写这两个 API。当这两个 API 被 SPA 应用调用时，说明 URL 发生了变化，这时就可以根据当前已改变的 URL 判断是否要加载、卸载子应用。</p><pre><code class="language-js">// 执行下面代码后，浏览器的 URL 将从 https://www.xxx.com 变为 https://www.xxx.com/vue
window.history.pushState(null, '', '/vue')
</code></pre><p>当用户手动点击浏览器上的前进后退按钮时，会触发 <code>popstate</code> 事件，所以需要对这个事件进行监听。同理，也需要监听 <code>hashchange</code> 事件。</p><p>这一段逻辑的代码如下所示：</p><pre><code class="language-ts">import { loadApps } from '../application/apps'

const originalPushState = window.history.pushState
const originalReplaceState = window.history.replaceState

export default function overwriteEventsAndHistory() {
    window.history.pushState = function (state: any, title: string, url: string) {
        const result = originalPushState.call(this, state, title, url)
        // 根据当前 url 加载或卸载 app
        loadApps()
        return result
    }
    
    window.history.replaceState = function (state: any, title: string, url: string) {
        const result = originalReplaceState.call(this, state, title, url)
        loadApps()
        return result
    }
    
    window.addEventListener('popstate', () =&gt; {
        loadApps()
    }, true)
    
    window.addEventListener('hashchange', () =&gt; {
        loadApps()
    }, true)
}
</code></pre><p>从上面的代码可以看出来，每次 URL 改变时，都会调用 <code>loadApps()</code> 方法，这个方法的作用就是根据当前的 URL、子应用的触发规则去切换子应用的状态：</p><pre><code class="language-ts">export async function loadApps() {
	// 先卸载所有失活的子应用
    const toUnMountApp = getAppsWithStatus(AppStatus.MOUNTED)
    await Promise.all(toUnMountApp.map(unMountApp))
    
    // 初始化所有刚注册的子应用
    const toLoadApp = getAppsWithStatus(AppStatus.BEFORE_BOOTSTRAP)
    await Promise.all(toLoadApp.map(bootstrapApp))

    const toMountApp = [
        ...getAppsWithStatus(AppStatus.BOOTSTRAPPED),
        ...getAppsWithStatus(AppStatus.UNMOUNTED),
    ]
    // 加载所有符合条件的子应用
    await toMountApp.map(mountApp)
}
</code></pre><p>这段代码的逻辑也比较简单：</p><ol><li>卸载所有已失活的子应用</li><li>初始化所有刚注册的子应用</li><li>加载所有符合条件的子应用</li></ol><h3 id="-url--1"><strong><strong>根据当前 URL、子应用的触发规则来判断是否要加载、卸载子应用</strong></strong></h3><p>为了支持不同框架的子应用，所以规定了子应用必须向外暴露 <code>bootstrap()</code> <code>mount()</code> <code>unmount()</code> 这三个方法。<code>bootstrap()</code> 方法在第一次加载子应用时触发，并且只会触发一次，另外两个方法在每次加载、卸载子应用时都会触发。</p><p>不管注册的是什么子应用，在 URL 符合加载条件时就调用子应用的 <code>mount()</code> 方法，能不能正常渲染交给子应用负责。在符合卸载条件时则调用子应用的 <code>unmount()</code> 方法。</p><pre><code class="language-js">registerApplication({
    name: 'vue',
    // 初始化子应用时执行该方法
    loadApp() { 
    	return {
			mount() {                
            	// 这里进行挂载子应用的操作
            	app.mount('#app')
            },
            unmount() {
                // 这里进行卸载子应用的操作 
                app.unmount()
            },
		}
	},
	// 如果传入一个字符串会被转为一个参数为 location 的函数
	// activeRule: '/vue' 会被转为 (location) =&gt; location.pathname === '/vue'
    activeRule: (location) =&gt; location.hash === '#/vue'
})
</code></pre><p>上面是一个简单的子应用注册示例，其中 <code>activeRule()</code> 方法用来判断该子应用是否激活（返回 <code>true</code> 表示激活）。每当页面 URL 发生变化，微前端框架就会调用 <code>loadApps()</code> 判断每个子应用是否激活，然后触发加载、卸载子应用的操作。</p><h4 id="-"><strong><strong>何时加载、卸载子应用</strong></strong></h4><p>首先我们将子应用的状态分为三种：</p><ul><li><code>bootstrap</code>，调用 <code>registerApplication()</code> 注册一个子应用后，它的状态默认为 <code>bootstrap</code>，下一个转换状态为 <code>mount</code>。</li><li><code>mount</code>，子应用挂载成功后的状态，它的下一个转换状态为 <code>unmount</code>。</li><li><code>unmount</code>，子应用卸载成功后的状态，它的下一个转换状态为 <code>mount</code>，即卸载后的应用可再次加载。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/5383cf71fefa4f2ea43b4e01ef2728a2.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>现在我们来看看什么时候会加载一个子应用，当页面 URL 改变后，如果子应用满足以下两个条件，则需要加载该子应用：</p><ol><li><code>activeRule()</code> 的返回值为 <code>true</code>，例如 URL 从 <code>/</code> 变为 <code>/vue</code>，这时子应用 vue 为激活状态（假设它的激活规则为 <code>/vue</code>）。</li><li>子应用状态必须为 <code>bootstrap</code> 或 <code>unmount</code>，这样才能向 <code>mount</code> 状态转换。如果已经处于 <code>mount</code> 状态并且 <code>activeRule()</code> 返回值为 <code>true</code>，则不作任何处理。</li></ol><p>如果页面的 URL 改变后，子应用满足以下两个条件，则需要卸载该子应用：</p><ol><li><code>activeRule()</code> 的返回值为 <code>false</code>，例如 URL 从 <code>/vue</code> 变为 <code>/</code>，这时子应用 vue 为失活状态（假设它的激活规则为 <code>/vue</code>）。</li><li>子应用状态必须为 <code>mount</code>，也就是当前子应用必须处于加载状态（如果是其他状态，则不作任何处理）。然后 URL 改变导致失活了，所以需要卸载它，状态也从 <code>mount</code> 变为 <code>unmount</code>。</li></ol><h3 id="-api-"><strong><strong>API 介绍</strong></strong></h3><p>V1 版本主要向外暴露了两个 API：</p><ol><li><code>registerApplication()</code>，注册子应用。</li><li><code>start()</code>，注册完所有的子应用后调用，在它的内部会执行 <code>loadApps()</code> 去加载子应用。</li></ol><p><code>registerApplication(Application)</code> 接收的参数如下：</p><pre><code class="language-ts">interface Application {
    // 子应用名称
    name: string

    /**
     * 激活规则，例如传入 /vue，当 url 的路径变为 /vue 时，激活当前子应用。
     * 如果 activeRule 为函数，则会传入 location 作为参数，activeRule(location) 返回 true 时，激活当前子应用。
     */
    activeRule: Function | string

    // 传给子应用的自定义参数
    props: AnyObject

    /**
     * loadApp() 必须返回一个 Promise，resolve() 后得到一个对象：
     * {
     *   bootstrap: () =&gt; Promise&lt;any&gt;
     *   mount: (props: AnyObject) =&gt; Promise&lt;any&gt;
     *   unmount: (props: AnyObject) =&gt; Promise&lt;any&gt;
     * }
     */
    loadApp: () =&gt; Promise&lt;any&gt;
}
</code></pre><h3 id="--1"><strong><strong>一个完整的示例</strong></strong></h3><p>现在我们来看一个比较完整的示例（代码在 V1 分支的 examples 目录）：</p><pre><code class="language-js">let vueApp
registerApplication({
    name: 'vue',
    loadApp() {
        return Promise.resolve({
            bootstrap() {
                console.log('vue bootstrap')
            },
            mount() {
                console.log('vue mount')
                vueApp = Vue.createApp({
                    data() {
                        return {
                            text: 'Vue App'
                        }
                    },
                    render() {
                        return Vue.h(
                            'div',     // 标签名称
                            this.text  // 标签内容
                        )
                    },
                })
                
                vueApp.mount('#app')
            },
            unmount() {
                console.log('vue unmount')
                vueApp.unmount()
            },
        })
    },
    activeRule:(location) =&gt; location.hash === '#/vue',
})

registerApplication({
    name: 'react',
    loadApp() { 
        return Promise.resolve({
            bootstrap() {
                console.log('react bootstrap')
            },
            mount() {
                console.log('react mount')
                ReactDOM.render(
                    React.createElement(LikeButton),
                    $('#app')
                );
            },
            unmount() {
                console.log('react unmount')
                ReactDOM.unmountComponentAtNode($('#app'));
            },
        })
    },
    activeRule: (location) =&gt; location.hash === '#/react'
})

start()
</code></pre><p>演示效果如下：<br></p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/dbcb2959f36647b09b89cf4671624491.gif" class="kg-image" alt="请添加图片描述" width="600" height="400" loading="lazy"></figure><h3 id="--2"><strong><strong>小结</strong></strong></h3><p>V1 版本的代码打包后才 100 多行，如果只是想了解微前端的最核心原理，只看 V1 版本的源码就可以了。</p><h2 id="v2-"><strong><strong>V2 版本</strong></strong></h2><p>V1 版本的实现还是非常简陋的，能够适用的业务场景有限。从 V1 版本的示例可以看出，它要求子应用提前把资源都加载好（或者把整个子应用打包成一个 NPM 包，直接引入），这样才能在执行子应用的 <code>mount()</code> 方法时，能够正常渲染。</p><p>举个例子，假设我们在开发环境启动了一个 vue 应用。那么如何在主应用引入这个 vue 子应用的资源呢？首先排除掉 NPM 包的形式，因为每次修改代码都得打包，不现实。第二种方式就是手动在主应用引入子应用的资源。例如 vue 子应用的入口资源为：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/de60fae2e1454f93b72c0d623be755a6.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p><br>那么我们可以在注册子应用时这样引入：</p><pre><code class="language-js">registerApplication({
    name: 'vue',
    loadApp() { 
        return Promise.resolve({
            bootstrap() {
            	import('http://localhost:8001/js/chunk-vendors.js')
            	import('http://localhost:8001/js/app.js')
            },
            mount() {
                // ...            
            },
            unmount() {
                // ...            
            },
        })
    },
    activeRule: (location) =&gt; location.hash === '#/vue'
})
</code></pre><p>这种方式也不靠谱，每次子应用的入口资源文件变了，主应用的代码也得跟着变。还好，我们有第三种方式，那就是在注册子应用的时候，把子应用的入口 URL 写上，由微前端来负责加载资源文件。</p><pre><code class="language-js">registerApplication({
	// 子应用入口 URL
    pageEntry: 'http://localhost:8081'
    // ...
})
</code></pre><h3 id="--3"><strong><strong>“自动”加载资源文件</strong></strong></h3><p>现在我们来看一下如何自动加载子应用的入口文件（只在第一次加载子应用时执行）：</p><pre><code class="language-ts">export default function parseHTMLandLoadSources(app: Application) {
    return new Promise&lt;void&gt;(async (resolve, reject) =&gt; {
        const pageEntry = app.pageEntry    
        // load html        
        const html = await loadSourceText(pageEntry)
        const domparser = new DOMParser()
        const doc = domparser.parseFromString(html, 'text/html')
        const { scripts, styles } = extractScriptsAndStyles(doc as unknown as Element, app)
        
        // 提取了 script style 后剩下的 body 部分的 html 内容
        app.pageBody = doc.body.innerHTML

        let isStylesDone = false, isScriptsDone = false
        // 加载 style script 的内容
        Promise.all(loadStyles(styles))
        .then(data =&gt; {
            isStylesDone = true
            // 将 style 样式添加到 document.head 标签
            addStyles(data as string[])
            if (isScriptsDone &amp;&amp; isStylesDone) resolve()
        })
        .catch(err =&gt; reject(err))

        Promise.all(loadScripts(scripts))
        .then(data =&gt; {
            isScriptsDone = true
            // 执行 script 内容
            executeScripts(data as string[])
            if (isScriptsDone &amp;&amp; isStylesDone) resolve()
        })
        .catch(err =&gt; reject(err))
    })
}
</code></pre><p>上面代码的逻辑：</p><ol><li>利用 ajax 请求子应用入口 URL 的内容，得到子应用的 HTML</li><li>提取 HTML 中 <code>script</code> <code>style</code> 的内容或 URL，如果是 URL，则再次使用 ajax 拉取内容。最后得到入口页面所有的 <code>script</code> <code>style</code> 的内容</li><li>将所有 style 添加到 <code>document.head</code> 下，<code>script</code> 代码直接执行</li><li>将剩下的 body 部分的 HTML 内容赋值给子应用要挂载的 DOM 下。</li></ol><p>下面再详细描述一下这四步是怎么做的。</p><h4 id="-html-"><strong><strong>一、拉取 HTML 内容</strong></strong></h4><pre><code class="language-js">export function loadSourceText(url: string) {
    return new Promise&lt;string&gt;((resolve, reject) =&gt; {
        const xhr = new XMLHttpRequest()
        xhr.onload = (res: any) =&gt; {
            resolve(res.target.response)
        }

        xhr.onerror = reject
        xhr.onabort = reject
        xhr.open('get', url)
        xhr.send()
    })
}
</code></pre><p>代码逻辑很简单，使用 ajax 发起一个请求，得到 HTML 内容。<br></p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/d494daa83a5a473080248fbe67c8fb23.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p><br>上图就是一个 vue 子应用的 HTML 内容，箭头所指的是要提取的资源，方框标记的内容要赋值给子应用所挂载的 DOM。</p><h4 id="-html-style-script-"><strong><strong>二、解析 HTML 并提取 style script 标签内容</strong></strong></h4><p>这需要使用一个 API <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/DOMParser" rel="noopener">DOMParser</a>，它可以直接解析一个 HTML 字符串，并且不需要挂到 document 对象上。</p><pre><code class="language-js">const domparser = new DOMParser()
const doc = domparser.parseFromString(html, 'text/html')
</code></pre><p>提取标签的函数 <code>extractScriptsAndStyles(node: Element, app: Application)</code> 代码比较多，这里就不贴代码了。这个函数主要的功能就是递归遍历上面生成的 DOM 树，提取里面所有的 <code>style</code> <code>script</code> 标签。</p><h4 id="-style-script-"><strong><strong>三、添加 style 标签，执行 script 脚本内容</strong></strong></h4><p>这一步比较简单，将所有提取的 <code>style</code> 标签添加到 <code>document.head</code> 下：</p><pre><code class="language-ts">export function addStyles(styles: string[] | HTMLStyleElement[]) {
    styles.forEach(item =&gt; {
        if (typeof item === 'string') {
            const node = createElement('style', {
                type: 'text/css',
                textContent: item,
            })

            head.appendChild(node)
        } else {
            head.appendChild(item)
        }
    })
}
</code></pre><p>js 脚本代码则直接包在一个匿名函数内执行：</p><pre><code class="language-ts">export function executeScripts(scripts: string[]) {
    try {
        scripts.forEach(code =&gt; {
            new Function('window', code).call(window, window)
        })
    } catch (error) {
        throw error
    }
}
</code></pre><h4 id="-body-html-dom-"><strong><strong>四、将剩下的 body 部分的 HTML 内容赋值给子应用要挂载的 DOM 下</strong></strong></h4><p>为了保证子应用正常执行，需要将这部分的内容保存起来。然后每次在子应用 <code>mount()</code> 前，赋值到所挂载的 DOM 下。</p><pre><code class="language-js">// 保存 HTML 代码
app.pageBody = doc.body.innerHTML

// 加载子应用前赋值给挂载的 DOM
app.container.innerHTML = app.pageBody
app.mount()
</code></pre><p>现在我们已经可以非常方便的加载子应用了，但是子应用还有一些东西需要修改一下。</p><h3 id="--4"><strong><strong>子应用需要做的事情</strong></strong></h3><p>在 V1 版本里，注册子应用的时候有一个 <code>loadApp()</code> 方法。微前端框架在第一次加载子应用时会执行这个方法，从而拿到子应用暴露的三个方法。现在实现了 <code>pageEntry</code> 功能，我们就不用把这个方法写在主应用里了，因为不再需要在主应用里引入子应用。</p><p>但是又得让微前端框架拿到子应用暴露出来的方法，所以我们可以换一种方式暴露子应用的方法：</p><pre><code class="language-js">// 每个子应用都需要这样暴露三个 API，该属性格式为 `mini-single-spa-${appName}`
window['mini-single-spa-vue'] = {
    bootstrap,
    mount,
    unmount
}
</code></pre><p>这样微前端也能拿到每个子应用暴露的方法，从而实现加载、卸载子应用的功能。</p><p>另外，子应用还得做两件事：</p><ol><li>配置 cors，防止出现跨域问题（由于主应用和子应用的域名不同，会出现跨域问题）</li><li>配置资源发布路径</li></ol><p>如果子应用是基于 webpack 进行开发的，可以这样配置：</p><pre><code class="language-js">module.exports = {
    devServer: {
        port: 8001, // 子应用访问端口
        headers: {
            'Access-Control-Allow-Origin': '*'
        }
    },
    publicPath: "//localhost:8001/",
}
</code></pre><h3 id="--5"><strong><strong>一个完整的示例</strong></strong></h3><p>示例代码在 examples 目录。</p><pre><code class="language-js">registerApplication({
    name: 'vue',
    pageEntry: 'http://localhost:8001',
    activeRule: pathPrefix('/vue'),
    container: $('#subapp-viewport')
})

registerApplication({
    name: 'react',
    pageEntry: 'http://localhost:8002',
    activeRule:pathPrefix('/react'),
    container: $('#subapp-viewport')
})

start()
</code></pre><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/dc22bcc3009b452790f2af9bf8383ea6.gif" class="kg-image" alt="请添加图片描述" width="600" height="400" loading="lazy"></figure><h2 id="v3-"><strong><strong>V3 版本</strong></strong></h2><p>V3 版本主要添加以下两个功能：</p><ol><li>隔离子应用 window 作用域</li><li>隔离子应用元素作用域</li></ol><h3 id="-window-"><strong><strong>隔离子应用 window 作用域</strong></strong></h3><p>在 V2 版本下，主应用及所有的子应用都共用一个 window 对象，这就导致了互相覆盖数据的问题：</p><pre><code class="language-js">// 先加载 a 子应用
window.name = 'a'
// 后加载 b 子应用
window.name = 'b'
// 这时再切换回 a 子应用，读取 window.name 得到的值却是 b
console.log(window.name) // b
</code></pre><p>为了避免这种情况发生，我们可以使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy" rel="noopener">Proxy</a> 来代理对子应用 window 对象的访问：</p><pre><code class="language-ts">app.window = new Proxy({}, {
    get(target, key) {
        if (Reflect.has(target, key)) {
            return Reflect.get(target, key)
        }
        
        const result = originalWindow[key]
        // window 原生方法的 this 指向必须绑在 window 上运行，否则会报错 "TypeError: Illegal invocation"
        // e.g: const obj = {}; obj.alert = alert;  obj.alert();
        return (isFunction(result) &amp;&amp; needToBindOriginalWindow(result)) ? result.bind(window) : result
    },

    set: (target, key, value) =&gt; {
    	this.injectKeySet.add(key)
        return Reflect.set(target, key, value)
    }
})
</code></pre><p>从上述代码可以看出，用 Proxy 对一个空对象做了代理，然后把这个代理对象作为子应用的 window 对象：</p><ol><li>当子应用里的代码访问 <code>window.xxx</code> 属性时，就会被这个代理对象拦截。它会先看看子应用的代理 window 对象有没有这个属性，如果找不到，就会从父应用里找，也就是在真正的 window 对象里找。</li><li>当子应用里的代码修改 window 属性时，会直接在子应用的代理 window 对象上修改。</li></ol><p>那么问题来了，怎么让子应用里的代码读取/修改 window 时候，让它们访问的是子应用的代理 window 对象？</p><p>刚才 V2 版本介绍过，微前端框架会代替子应用拉取 js 资源，然后直接执行。我们可以在执行代码的时候使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with" rel="noopener">with</a> 语句将代码包一下，让子应用的 window 指向代理对象：</p><pre><code class="language-ts">export function executeScripts(scripts: string[], app: Application) {
    try {
        scripts.forEach(code =&gt; {            
            // ts 使用 with 会报错，所以需要这样包一下
            // 将子应用的 js 代码全局 window 环境指向代理环境 proxyWindow
            const warpCode = `
                ;(function(proxyWindow){
                    with (proxyWindow) {
                        (function(window){${code}\n}).call(proxyWindow, proxyWindow)
                    }
                })(this);
            `

            new Function(warpCode).call(app.sandbox.proxyWindow)
        })
    } catch (error) {
        throw error
    }
}
</code></pre><h4 id="-window--1"><strong><strong>卸载时清除子应用 window 作用域</strong></strong></h4><p>当子应用卸载时，需要对它的 window 代理对象进行清除。否则下一次子应用重新加载时，它的 window 代理对象会存有上一次加载的数据。刚才创建 Proxy 的代码中有一行代码 <code>this.injectKeySet.add(key)</code>，这个 <code>injectKeySet</code> 是一个 Set 对象，存着每一个 window 代理对象的新增属性。所以在卸载时只需要遍历这个 Set，将 window 代理对象上对应的 key 删除即可：</p><pre><code class="language-ts">for (const key of injectKeySet) {
	Reflect.deleteProperty(microAppWindow, key as (string | symbol))
}
</code></pre><h4 id="--6"><strong><strong>记录绑定的全局事件、定时器，卸载时清除</strong></strong></h4><p>通常情况下，一个子应用除了会修改 window 上的属性，还会在 window 上绑定一些全局事件。所以我们要把这些事件记录起来，在卸载子应用时清除这些事件。同理，各种定时器也一样，卸载时需要清除未执行的定时器。</p><p>下面的代码是记录事件、定时器的部分关键代码：</p><pre><code class="language-ts">// 部分关键代码
microAppWindow.setTimeout = function setTimeout(callback: Function, timeout?: number | undefined, ...args: any[]): number {
    const timer = originalWindow.setTimeout(callback, timeout, ...args)
    timeoutSet.add(timer)
    return timer
}

microAppWindow.clearTimeout = function clearTimeout(timer?: number): void {
    if (timer === undefined) return
    originalWindow.clearTimeout(timer)
    timeoutSet.delete(timer)
}
microAppWindow.addEventListener = function addEventListener(
    type: string, 
    listener: EventListenerOrEventListenerObject, 
    options?: boolean | AddEventListenerOptions | undefined,
) {
    if (!windowEventMap.get(type)) {
        windowEventMap.set(type, [])
    }

    windowEventMap.get(type)?.push({ listener, options })
    return originalWindowAddEventListener.call(originalWindow, type, listener, options)
}

microAppWindow.removeEventListener = function removeEventListener(
    type: string, 
    listener: EventListenerOrEventListenerObject, 
    options?: boolean | AddEventListenerOptions | undefined,
) {
    const arr = windowEventMap.get(type) || []
    for (let i = 0, len = arr.length; i &lt; len; i++) {
        if (arr[i].listener === listener) {
            arr.splice(i, 1)
            break
        }
    }

    return originalWindowRemoveEventListener.call(originalWindow, type, listener, options)
}
</code></pre><p>下面这段是清除事件、定时器的关键代码：</p><pre><code class="language-ts">for (const timer of timeoutSet) {
    originalWindow.clearTimeout(timer)
}

for (const [type, arr] of windowEventMap) {
    for (const item of arr) {
        originalWindowRemoveEventListener.call(originalWindow, type as string, item.listener, item.options)
    }
}
</code></pre><h4 id="--7"><strong><strong>缓存子应用快照</strong></strong></h4><p>之前提到过子应用每次加载的时候会都执行 <code>mount()</code> 方法，由于每个 js 文件只会执行一次，所以在执行 <code>mount()</code> 方法之前的代码在下一次重新加载时不会再次执行。</p><p>举个例子：</p><pre><code class="language-js">window.name = 'test'

function bootstrap() { // ... }
function mount() { // ... }
function unmount() { // ... }
</code></pre><p>上面是子应用入口文件的代码，在第一次执行 js 代码时，子应用可以读取 <code>window.name</code> 这个属性的值。但是子应用卸载时会把 <code>name</code> 这个属性清除掉。所以子应用下一次加载的时候，就读取不到这个属性了。</p><p>为了解决这个问题，我们可以在子应用初始化时（拉取了所有入口 js 文件并执行后）将当前的子应用 window 代理对象的属性、事件缓存起来，生成快照。下一次子应用重新加载时，将快照恢复回子应用上。</p><p>生成快照的部分代码：</p><pre><code class="language-ts">const { windowSnapshot, microAppWindow } = this
const recordAttrs = windowSnapshot.get('attrs')!
const recordWindowEvents = windowSnapshot.get('windowEvents')!

// 缓存 window 属性
this.injectKeySet.forEach(key =&gt; {
    recordAttrs.set(key, deepCopy(microAppWindow[key]))
})

// 缓存 window 事件
this.windowEventMap.forEach((arr, type) =&gt; {
    recordWindowEvents.set(type, deepCopy(arr))
})
</code></pre><p>恢复快照的部分代码：</p><pre><code class="language-ts">const { 
    windowSnapshot, 
    injectKeySet, 
    microAppWindow, 
    windowEventMap, 
    onWindowEventMap,
} = this
const recordAttrs = windowSnapshot.get('attrs')!
const recordWindowEvents = windowSnapshot.get('windowEvents')!

recordAttrs.forEach((value, key) =&gt; {
    injectKeySet.add(key)
    microAppWindow[key] = deepCopy(value)
})

recordWindowEvents.forEach((arr, type) =&gt; {
    windowEventMap.set(type, deepCopy(arr))
    for (const item of arr) {
        originalWindowAddEventListener.call(originalWindow, type as string, item.listener, item.options)
    }
})
</code></pre><h3 id="--8"><strong><strong>隔离子应用元素作用域</strong></strong></h3><p>我们在使用 <code>document.querySelector()</code> 或者其他查询 DOM 的 API 时，都会在整个页面的 document 对象上查询。如果在子应用上也这样查询，很有可能会查询到子应用范围外的 DOM 元素。为了解决这个问题，我们需要重写一下查询类的 DOM API：</p><pre><code class="language-ts">// 将所有查询 dom 的范围限制在子应用挂载的 dom 容器上
Document.prototype.querySelector = function querySelector(this: Document, selector: string) {
    const app = getCurrentApp()
    if (!app || !selector || isUniqueElement(selector)) {
        return originalQuerySelector.call(this, selector)
    }
	// 将查询范围限定在子应用挂载容器的 DOM 下
    return app.container.querySelector(selector)
}

Document.prototype.getElementById = function getElementById(id: string) {
    // ...
}
</code></pre><p>将查询范围限定在子应用挂载容器的 DOM 下。另外，子应用卸载时也需要恢复重写的 API：</p><pre><code class="language-js">Document.prototype.querySelector = originalQuerySelector
Document.prototype.querySelectorAll = originalQuerySelectorAll
// ...
</code></pre><p>除了查询 DOM 要限制子应用的范围，样式也要限制范围。假设在 vue 应用上有这样一个样式：</p><pre><code class="language-css">body {
	color: red;
}
</code></pre><p>当它作为一个子应用被加载时，这个样式需要被修改为：</p><pre><code class="language-css">/* body 被替换为子应用挂载 DOM 的 id 选择符 */
#app {
	color: red;
}
</code></pre><p>实现代码也比较简单，需要遍历每一条 css 规则，然后替换里面的 <code>body</code>、<code>html</code> 字符串：</p><pre><code class="language-ts">const re = /^(\s|,)?(body|html)\b/g
// 将 body html 标签替换为子应用挂载容器的 id
cssText.replace(re, `#${app.container.id}`)
</code></pre><h2 id="v4-"><strong><strong>V4 版本</strong></strong></h2><p>V3 版本实现了 window 作用域隔离、元素隔离，在 V4 版本上我们将实现子应用样式隔离。</p><h3 id="--9"><strong><strong>第一版</strong></strong></h3><p>我们都知道创建 DOM 元素时使用的是 <code>document.createElement()</code> API，所以我们可以在创建 DOM 元素时，把当前子应用的名称当成属性写到 DOM 上：</p><pre><code class="language-ts">Document.prototype.createElement = function createElement(
    tagName: string,
    options?: ElementCreationOptions,
): HTMLElement {
    const appName = getCurrentAppName()
    const element = originalCreateElement.call(this, tagName, options)
    appName &amp;&amp; element.setAttribute('single-spa-name', appName)
    return element
}
</code></pre><p>这样所有的 style 标签在创建时都会有当前子应用的名称属性。我们可以在子应用卸载时将当前子应用所有的 style 标签进行移除，再次挂载时将这些标签重新添加到 <code>document.head</code> 下。这样就实现了不同子应用之间的样式隔离。</p><p>移除子应用所有 style 标签的代码：</p><pre><code class="language-ts">export function removeStyles(name: string) {
    const styles = document.querySelectorAll(`style[single-spa-name=${name}]`)
    styles.forEach(style =&gt; {
        removeNode(style)
    })

    return styles as unknown as HTMLStyleElement[]
}
</code></pre><p>第一版的样式作用域隔离完成后，它只能对每次只加载一个子应用的场景有效。例如先加载 a 子应用，卸载后再加载 b 子应用这种场景。在卸载 a 子应用时会把它的样式也卸载。如果同时加载多个子应用，第一版的样式隔离就不起作用了。</p><h3 id="--10"><strong><strong>第二版</strong></strong></h3><p>由于每个子应用下的 DOM 元素都有以自己名称作为值的 <code>single-spa-name</code> 属性（如果不知道这个名称是哪来的，请往上翻一下第一版的描述）。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/f0100347108a4cb4a4ef9e02fafa6adb.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p><br>所以我们可以给子应用的每个样式加上子应用名称，也就是将这样的样式：</p><pre><code class="language-css">div {
	color: red;
}
</code></pre><p>改成：</p><pre><code class="language-css">div[single-spa-name=vue] {
	color: red;
}
</code></pre><p>这样一来，就把样式作用域范围限制在对应的子应用所挂载的 DOM 下。</p><h4 id="--11"><strong><strong>给样式添加作用域范围</strong></strong></h4><p>现在我们来看看具体要怎么添加作用域：</p><pre><code class="language-js">/**
 * 给每一条 css 选择符添加对应的子应用作用域
 * 1. a {} -&gt; a[single-spa-name=${app.name}] {}
 * 2. a b c {} -&gt; a[single-spa-name=${app.name}] b c {}
 * 3. a, b {} -&gt; a[single-spa-name=${app.name}], b[single-spa-name=${app.name}] {}
 * 4. body {} -&gt; #${子应用挂载容器的 id}[single-spa-name=${app.name}] {}
 * 5. @media @supports 特殊处理，其他规则直接返回 cssText
 */
</code></pre><p>主要有以上五种情况。</p><p>通常情况下，每一条 css 选择符都是一个 css 规则，这可以通过 <code>style.sheet.cssRules</code> 获取：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/06c4e8bf4a00419babdb260643b51767.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p><br>拿到了每一条 css 规则之后，我们就可以对它们进行重写，然后再把它们重写挂载到 <code>document.head</code> 下：</p><pre><code class="language-ts">function handleCSSRules(cssRules: CSSRuleList, app: Application) {
    let result = ''
    Array.from(cssRules).forEach(cssRule =&gt; {
        const cssText = cssRule.cssText
        const selectorText = (cssRule as CSSStyleRule).selectorText
        result += cssRule.cssText.replace(
            selectorText, 
            getNewSelectorText(selectorText, app),
        )
    })

    return result
}

let count = 0
const re = /^(\s|,)?(body|html)\b/g
function getNewSelectorText(selectorText: string, app: Application) {
    const arr = selectorText.split(',').map(text =&gt; {
        const items = text.trim().split(' ')
        items[0] = `${items[0]}[single-spa-name=${app.name}]`
        return items.join(' ')
    })

    // 如果子应用挂载的容器没有 id，则随机生成一个 id
    let id = app.container.id
    if (!id) {
        id = 'single-spa-id-' + count++
        app.container.id = id
    }

    // 将 body html 标签替换为子应用挂载容器的 id
    return arr.join(',').replace(re, `#${id}`)
}
</code></pre><p>核心代码在 <code>getNewSelectorText()</code> 上，这个函数给每一个 css 规则都加上了 <code>[single-spa-name=${app.name}]</code>。这样就把样式作用域限制在了对应的子应用内了。</p><h3 id="--12"><strong><strong>效果演示</strong></strong></h3><p>大家可以对比一下下面的两张图，这个示例同时加载了 vue、react 两个子应用。第一张图里的 vue 子应用部分字体被 react 子应用的样式影响了。第二张图是添加了样式作用域隔离的效果图，可以看到 vue 子应用的样式是正常的，没有被影响。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/5664d45cd98c45c29e734f98acf35271.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/849d35e917f64aee8f27baeae866f2f5.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h2 id="v5-"><strong><strong>V5 版本</strong></strong></h2><p>V5 版本主要添加了一个全局数据通信的功能，设计思路如下：</p><ol><li>所有应用共享一个全局对象 <code>window.spaGlobalState</code>，所有应用都可以对这个全局对象进行监听，每当有应用对它进行修改时，会触发 <code>change</code> 事件。</li><li>可以使用这个全局对象进行事件订阅/发布，各应用之间可以自由的收发事件。</li></ol><p>下面是实现了第一点要求的部分关键代码：</p><pre><code class="language-ts">export default class GlobalState extends EventBus {
    private state: AnyObject = {}
    private stateChangeCallbacksMap: Map&lt;string, Array&lt;Callback&gt;&gt; = new Map()

    set(key: string, value: any) {
        this.state[key] = value
        this.emitChange('set', key)
    }

    get(key: string) {
        return this.state[key]
    }

    onChange(callback: Callback) {
        const appName = getCurrentAppName()
        if (!appName) return

        const { stateChangeCallbacksMap } = this
        if (!stateChangeCallbacksMap.get(appName)) {
            stateChangeCallbacksMap.set(appName, [])
        }

        stateChangeCallbacksMap.get(appName)?.push(callback)
    }

    emitChange(operator: string, key?: string) {
        this.stateChangeCallbacksMap.forEach((callbacks, appName) =&gt; {
            /**
             * 如果是点击其他子应用或父应用触发全局数据变更，则当前打开的子应用获取到的 app 为 null
             * 所以需要改成用 activeRule 来判断当前子应用是否运行
             */
            const app = getApp(appName) as Application
            if (!(isActive(app) &amp;&amp; app.status === AppStatus.MOUNTED)) return
            callbacks.forEach(callback =&gt; callback(this.state, operator, key))
        })
    }
}
</code></pre><p>下面是实现了第二点要求的部分关键代码：</p><pre><code class="language-ts">export default class EventBus {
    private eventsMap: Map&lt;string, Record&lt;string, Array&lt;Callback&gt;&gt;&gt; = new Map()

    on(event: string, callback: Callback) {
        if (!isFunction(callback)) {
            throw Error(`The second param ${typeof callback} is not a function`)
        }

        const appName = getCurrentAppName() || 'parent'

        const { eventsMap } = this
        if (!eventsMap.get(appName)) {
            eventsMap.set(appName, {})
        }

        const events = eventsMap.get(appName)!
        if (!events[event]) {
            events[event] = [] 
        }

        events[event].push(callback)
    }

    emit(event: string, ...args: any) {
        this.eventsMap.forEach((events, appName) =&gt; {
            /**
             * 如果是点击其他子应用或父应用触发全局数据变更，则当前打开的子应用获取到的 app 为 null
             * 所以需要改成用 activeRule 来判断当前子应用是否运行
             */
            const app = getApp(appName) as Application
            if (appName === 'parent' || (isActive(app) &amp;&amp; app.status === AppStatus.MOUNTED)) {
                if (events[event]?.length) {
                    for (const callback of events[event]) {
                        callback.call(this, ...args)
                    }
                }
            }
        })
    }
}
</code></pre><p>以上两段代码都有一个相同的地方，就是在保存监听回调函数的时候需要和对应的子应用关联起来。当某个子应用卸载时，需要把它关联的回调函数也清除掉。</p><p><strong>全局数据修改示例代码</strong>：</p><pre><code class="language-js">// 父应用
window.spaGlobalState.set('msg', '父应用在 spa 全局状态上新增了一个 msg 属性')
// 子应用
window.spaGlobalState.onChange((state, operator, key) =&gt; {
    alert(`vue 子应用监听到 spa 全局状态发生了变化: ${JSON.stringify(state)}，操作: ${operator}，变化的属性: ${key}`)
})
</code></pre><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/8dc0d3cd7ee24d98b6340218f827981e.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p><strong>全局事件示例代码</strong>：</p><pre><code class="language-js">// 父应用
window.spaGlobalState.emit('testEvent', '父应用发送了一个全局事件: testEvent')
// 子应用
window.spaGlobalState.on('testEvent', () =&gt; alert('vue 子应用监听到父应用发送了一个全局事件: testEvent'))
</code></pre><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/d072ac0125c949d5a990ac09a080618d.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h2 id="--13"><strong><strong>总结</strong></strong></h2><p>至此，一个简易微前端框架的技术要点已经讲解完毕。强烈建议大家在看文档的同时，把 demo 运行起来跑一跑，这样能帮助你更好的理解代码。</p><p>如果你觉得我的文章写得不错，也可以看看我的其他一些技术文章或项目：</p><ul><li><a href="https://woai3c.gitee.io/introduction-to-front-end-engineering/" rel="noopener">带你入门前端工程</a></li><li><a href="https://github.com/woai3c/Front-end-articles/issues/19" rel="noopener">可视化拖拽组件库一些技术要点原理分析</a></li><li><a href="https://github.com/woai3c/Front-end-articles/blob/master/performance.md" rel="noopener">前端性能优化 24 条建议（2020）</a></li><li><a href="https://github.com/woai3c/Front-end-articles/issues/26" rel="noopener">前端监控 SDK 的一些技术要点原理分析</a></li><li><a href="https://github.com/woai3c/Front-end-articles/issues/22" rel="noopener">手把手教你写一个脚手架</a></li><li><a href="https://github.com/woai3c/nand2tetris" rel="noopener">计算机系统要素-从零开始构建现代计算机</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 微前端实践的两个模型 ]]>
                </title>
                <description>
                    <![CDATA[ > 这里主要是提供基于 Qiankun 微前端落地的两种基本模(Tao)型(Lu)实践。 一般技术文章开头，为了凑字数，都会介绍一下什么是 Qiankun, 什么是 微前端, 他们有什么优点和解决了什么问题之类。 其实大可不必，我们直接展开。 术语前置 首先要定义一些术语，不然话都说不清楚了。  * 主应用 > 指的是微前端架构下，作为入口的应用。 一般还要承担加载子应用、识别前端路由、分发路由信息到子应用等职责。个别场景下还要管理状态，应用间通信等。  * 子应用 > 指的是具有业务特殊性的应用，可以理解为你当前主要开发的东西。在 Qiankun 下，一个系统有一个主应用，可以根据路由信息，分发到 N 个子应用 。 1.基座模型 基座模型是指，通过把共性封装到一个完好的主应用作为系统基座，然后把特性部分作为 子应用  集成进去，从而减轻开发任务。比较适用于一些管理后台项目。 图片很好地描述了基座模型下应用间的关系，同时清除地看到，子应用的功能开发，可以只关注自己的业务需求，而不需要重复实现 “登录注册”、“权限管理”  等等其他共性化需求。 🚀🚀� ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/qiankun-in-practice/</link>
                <guid isPermaLink="false">6077fe0b7a8acf0586ec8ddb</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 开发者小蓝 ]]>
                </dc:creator>
                <pubDate>Thu, 15 Apr 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/christopher-gower-m_HRfLhgABo-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <blockquote>
<p>这里主要是提供基于 Qiankun 微前端落地的两种基本模(Tao)型(Lu)实践。</p>
</blockquote>
<p>一般技术文章开头，为了凑字数，都会介绍一下什么是 <strong>Qiankun</strong>, 什么是 <strong>微前端</strong>, 他们有什么优点和解决了什么问题之类。</p>
<p><strong>其实大可不必</strong>，我们直接展开。</p>
<h3 id="">术语前置</h3>
<p>首先要定义一些术语，不然话都说不清楚了。</p>
<ul>
<li>主应用</li>
</ul>
<blockquote>
<p>指的是微前端架构下，作为入口的应用。</p>
<p>一般还要承担加载子应用、识别前端路由、分发路由信息到子应用等职责。个别场景下还要管理状态，应用间通信等。</p>
</blockquote>
<ul>
<li>子应用</li>
</ul>
<blockquote>
<p>指的是具有业务特殊性的应用，可以理解为你当前主要开发的东西。在 Qiankun 下，一个系统有一个主应用，可以根据路由信息，分发到 N 个子应用 。</p>
</blockquote>
<h3 id="1">1.基座模型</h3>
<p>基座模型是指，通过把共性封装到一个完好的<code>主应用</code>作为系统基座，然后把特性部分作为  <code>子应用</code> 集成进去，从而减轻开发任务。比较适用于一些管理后台项目。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/332/1.png" alt="1" width="974" height="708" loading="lazy"></p>
<p>图片很好地描述了基座模型下应用间的关系，同时清除地看到，子应用的功能开发，可以只关注自己的业务需求，而不需要重复实现 <strong>“登录注册”</strong>、<strong>“权限管理”</strong> 等等其他共性化需求。</p>
<p>🚀🚀🚀</p>
<p>对于常见的管理后台项目，基座模型有天然的契合。因为管理后台几乎都具有很多公共的部分，只是具体“管理”的东西有差异。</p>
<p>所以，如果你有大量管理后台开发的需要，就可以尽可能地把更多的共性开发到一个主应用下，当一个新的管理后台项目发起时，只需要少了的开发（子应用），通过简单的集成，就能呈现一个完整的系统。</p>
<p><strong>注意事项</strong></p>
<p>因为大部分基础功能都在基座里实现了，所以要充分考虑从基座到业务应用的信息传递。</p>
<blockquote>
<p>比如说，用户信息、权限信息之类，可以通过 <code>props</code> 的方式传递给子应用，当然，你还可以传递函数，实现反向传递数据。</p>
</blockquote>
<p><strong>基座模型也有其缺陷，主要表现在 UI 定制化能力弱。</strong></p>
<p>因为基座都是现成的，而基座又决定了最终系统的大体颜值。（可能对于大部分管理后台来说，这个不太敏感）</p>
<p>于是乎，对于界面操控欲望更强的一些系统，我们就需要另一种实践模型。</p>
<h3 id="2">2.镶嵌模型</h3>
<p>在游戏中，一件优秀的装备除了基础属性牛X，它还可以通过镶嵌各种宝石来提供更多的加持。</p>
<p><img src="https://lanhaooss.oss-cn-shenzhen.aliyuncs.com/images/332/2.png" alt="2" width="908" height="652" loading="lazy"></p>
<p><strong>镶嵌模型的思路正好和基座模型想法，我们把一些独立的功能实现为子应用，把业务系统作为主应用来实现。</strong></p>
<p>业务团队除了开发自己的系统外，如果有复用已有能力的需要，只需要简单的接入需要的子应用即可。</p>
<p>这个模型下，业务开发对系统的颜值有完全的掌控。</p>
<p>而在能效方面，通过组合子应用，也在很大程度上节约了不少的开发投入。</p>
<p>这么模型的特点，比较适合那种 <strong>OEM 定制系统</strong>：</p>
<ul>
<li>客户对界面的独特性有要求；</li>
<li>同时对于乙方能提供的能力进行“按需”整合。</li>
</ul>
<p><strong>当然了，权力越大，事情越多</strong></p>
<p>界面定制能力强，决定了你需要操心 <strong>路由</strong>、<strong>菜单</strong>之类的实现，毕竟在镶嵌模型下，子应用只会提供 <strong>子页面</strong> 级别的能力复用，怎么把他们整合到当前项目下，不同的框架都有不同的要求。</p>
<blockquote>
<p>我这边只实践过 Antd-pro 还不算太麻烦。</p>
</blockquote>
<h3 id="">总结</h3>
<p>不管是那种模型，都是对 Qiankun 微前端架构的抽象理解。</p>
<p>目的都是能力复用、效能提升，患者可以根据自己团队和销售策略，灵活选择。</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 前端开发和后端开发的区别到底是什么 ]]>
                </title>
                <description>
                    <![CDATA[ 网页和应用程序是复杂的，按钮和图片只不过是冰山一角。按照这样的复杂程度，我们需要人工来处理，但前端程序员和后端程序员的职务分别是什么？  * 开发的多层次    [https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#The%20many%20layers%20of%20development]  * 但我们不是全栈程序员    [https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#but-we-re-not-all-full-stack]  * 所以前端和后端的区别到底是什么？    [https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-defin ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/</link>
                <guid isPermaLink="false">5f687451cd07b005bfb5b346</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 后端 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 开发者 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Lee Lih Kai ]]>
                </dc:creator>
                <pubDate>Thu, 01 Apr 2021 05:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/09/front-end-back-end.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>网页和应用程序是复杂的，按钮和图片只不过是冰山一角。按照这样的复杂程度，我们需要人工来处理，但前端程序员和后端程序员的职务分别是什么？</p><ul><li><a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#The%20many%20layers%20of%20development" rel="nofollow">开发的多层次</a></li><li><a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#but-we-re-not-all-full-stack" rel="nofollow">但我们不是全栈程序员</a></li><li><a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#so-what-is-the-difference-between-front-end-development-and-back-end-development" rel="nofollow">所以前端和后端的区别到底是什么？</a></li><li><a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#what-is-front-end-development" rel="nofollow">什么是前端开发？</a></li><li><a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#what-is-back-end-development" rel="nofollow">什么是后端开发？</a></li><li><a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#where-things-get-fuzzy" rel="nofollow">模糊地带</a></li><li><a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/#resources-to-learn" rel="nofollow">学习资源</a></li></ul><h2 id="-">开发的多层次</h2><p>无论你是使用网站或者是ios应用程序，所有的开发环境都使用相同的模式——那就是前端开发和后端开发。</p><p>这界线是模糊的，尤其是当 Javascript 和 <a href="https://www.wikiwand.com/en/Serverless_computing">serverless</a> 世界的兴起。当开发工具开始结合在一起时，我们会开始怀疑自己算不算是<a href="https://www.colbyfayock.com/2020/02/how-to-become-a-full-stack-web-developer-in-2020/" rel="nofollow">全栈开发者</a>。</p><h2 id="--1">但我们不是全栈程序员</h2><p>虽然我们<a href="https://full-stack.netlify.app/">都想被称为全栈开发者</a>，但我们并不是。以我个人来说，我发现自己在后端的开发比较有效率，但这并不是我的强项，我更喜欢建立使用者页面（ UI ）。</p><p>当然也有些人跟我相反，他们的强项是在应用的后端构建 API 同时也能建立使用者页面，但可能是比较像原型设计以展示应用设计地那种。</p><h2 id="--2">所以前端和后端的区别到底是什么？</h2><p>就算你是个全栈开发者，但这并不代表没有职责的分支。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/6f2d83ab44bc2966415babfd4ddeded2afe14232/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f66726f6e742d656e642d76732d6261636b2d656e642d656e67696e6565722d322e6a7067" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f66726f6e742d656e642d76732d6261636b2d656e642d656e67696e6565722d322e6a7067" width="600" height="400" loading="lazy"><figcaption>前端工程师 vs 后端工程师</figcaption></figure><p>它们的区别是什么？</p><h2 id="--3">什么是前端开发？</h2><p>应用程序的前端通常是指显示使用者界面的图层，当中包括所有的静态网页如 HTML 和 CSS 到 <a href="https://reactjs.org/" rel="nofollow">React</a> 所构建的使用者界面。</p><h3 id="--4">传统的前端开发是什么样的？</h3><p>目前统治前端网页的程序语言是 Javascript ，但并不完全只依赖一个程序语言就可以的。若需要在网页内增加一些互动，就需要使用如 <a href="https://www.php.net/" rel="nofollow">PHP</a> 和 <a href="http://www.template-toolkit.org/" rel="nofollow">Template Toolkit</a>（<a href="https://www.perl.org/" rel="nofollow">Perl</a>）的服务器模板的程序语言渲染网页前端页面。</p><p>这种方式广泛的应用在用户自行建立的框架或如 <a href="https://wordpress.org/" rel="nofollow">Wordpress</a> 这样使用 PHP 来驱使广大的开发者社群使用这个工具来开发网站。</p><p>它的运作方式是这些模板可以在运行时直接从服务器获取资料。当浏览器从服务器请求网页资料时，模板所需的资料和应用的逻辑都会在那个时候获取。</p><p>一些传统的前端工具包括：</p><ul><li>软件包如 <a href="https://jquery.com/" rel="nofollow">jQuery</a> 或 <a href="https://mootools.net/" rel="nofollow">MooTools</a></li><li>网页框架 <a href="https://wordpress.com/" rel="nofollow">Wordpress</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS" rel="nofollow">CSS</a></li><li>充分地使用 <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/table" rel="nofollow">Table</a> 元素</li></ul><p>但时间久了之后， 当 Javascript 变得越来越成熟和浏览器的强化，我们就能够建立更多、更快、互动性更强的使用者体验。</p><h3 id="--5">目前的前端开发是什么样的？</h3><p>目前使用 Javascript 的网页和应用程序建立 UI 的框架如 <a href="https://reactjs.org/" rel="nofollow">React</a>，<a href="https://vuejs.org/" rel="nofollow">Vue</a>，和 <a href="https://angular.io/" rel="nofollow">Angular</a> 越来越常见。这些工具提供了开发者能够重复使用的复杂 UI 元件的功能。</p><p>当浏览器缓冲页面时，该页面会先接受一个起始的 HTML 文件当中包括 script 标签里所包含的 Javascript 文档（就跟之前的一样）。不同的是当 Javascript 运行时，它会自动从浏览器与外部的 API 连接，更新页面之后就像一个普通的静态网页从起始的 HTML 文件中获取这样。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/1bf7ee4175540da9a98643c1d9469986300704a4/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f6275696c64696e672d776562736974652d776974682d6d6f72652d73746570732e6a7067" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f6275696c64696e672d776562736974652d776974682d6d6f72652d73746570732e6a7067" width="600" height="400" loading="lazy"><figcaption>它就像是使用更多的步骤建立一个网页...</figcaption></figure><p>虽然看起来步骤变多了，但通常能够提供更快速的缓存和渲染，因此提供了更优质的开发体验。因为在初始请求页面时的资料减少和缓冲优先的排序，所以能够提供更好的使用者体验。</p><p>一些常用和广受欢迎的前端开发工具包括：</p><ul><li>UI 框架如 <a href="https://reactjs.org/" rel="nofollow">React</a> 或 <a href="https://vuejs.org/" rel="nofollow">Vue</a></li><li>Web 框架如 <a href="https://www.gatsbyjs.org/" rel="nofollow">Gatsby</a></li><li>编译器如 <a href="https://babeljs.io/" rel="nofollow">Babel</a></li><li>集合器如 <a href="https://webpack.js.org/" rel="nofollow">Webpack</a></li><li>CSS 工具如 <a href="https://sass-lang.com/" rel="nofollow">Sass</a></li></ul><p>但是这些 API，无论是我们付费或者是自行建立的都需要建立在 <em>某些地方</em>。这就是所谓的后端。</p><h2 id="--6">什么是后端开发？</h2><p>后端是程序逻辑发生的地方。这可以是非常复杂，比如管理电子商务公司的收入或者只是简单的用户资料。</p><h3 id="--7">传统的后端开发是什么样的？</h3><p>传统的应用程序后端通常使用服务器编程语言如 <a href="https://www.php.net/" rel="nofollow">PHP</a> 或 <a href="https://www.ruby-lang.org/en/" rel="nofollow">Ruby</a>。这个方法是需要一个服务器来运行复杂的运动算的，所以才会使用服务器能够明白的语言。</p><p>所以对服务器的请求，后端会执行所有的程序，当然也包括前端的渲染。通过使用框架或者自行定义的结构，后端能够接受请求，通过请求执行相对应的逻辑，然后提供前端所需要显示的资料。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/04639234182ecdbd4707c418f390b2bbc9283cb0/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f66726f6e742d656e642d6261636b2d656e642d3530302d6572726f722e6a7067" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f66726f6e742d656e642d6261636b2d656e642d3530302d6572726f722e6a7067" width="600" height="400" loading="lazy"><figcaption>后端给前端的 500 Internal Server Error</figcaption></figure><p>一些传统的后端开发工具包括：</p><ul><li>本地或远程控制的服务器如 <a href="https://www.rackspace.com/" rel="nofollow">Rackspace</a></li><li>HTTP 服务器如 <a href="https://httpd.apache.org/" rel="nofollow">Apache</a></li><li>数据库如 <a href="https://www.mysql.com/" rel="nofollow">MySQL</a></li><li>服务器程序语言如 <a href="https://www.php.net/" rel="nofollow">PHP</a> 或 <a href="https://www.perl.org/" rel="nofollow">Perl</a></li><li>应用程序框架如 <a href="https://rubyonrails.org/" rel="nofollow">Ruby on Rails</a></li></ul><h3 id="--8">目前的后端开发是什么样的？</h3><p>目前后端的开发和之前并没有太大的变化，除了有新的编码格式，就是我们经长能够看见的后端透过 HTTP 请求提供资料给 API 而不是直接透过前端工程师所建立的模板。</p><p>因为基础并没有太大的变化，当我们开放存有敏感资料的 API 给大众时会带来复杂且难处理的安全隐患。</p><p>但服务器的运行也有了不同。之前，我们需要在我们自己的服务器运行 Python 代码（当然现在也可以），但现在我们可以使用如 <a href="https://aws.amazon.com/lambda/" rel="nofollow">AWS Lambda</a> 的工具来简化我们的代码。</p><p>可是 <a href="https://www.wikiwand.com/en/Serverless_computing">serverless</a> 并不是真正不需要服务器的意思，而是转化成为了一种服务，开发者能够不需要担心服务器维护的情况下进行开发工作，只需要专注在编码本身的运行就行了。 一些常用和广受欢迎的后端开发工具包括：</p><ul><li>云服务器如 <a href="https://aws.amazon.com/ec2/" rel="nofollow">AWS EC2</a></li><li>serverless 服务如 <a href="https://aws.amazon.com/lambda/" rel="nofollow">AWS Lambda</a></li><li>NoSQL 数据库如 <a href="https://www.mongodb.com/" rel="nofollow">MongoDB</a></li><li>程序语言如 <a href="https://www.python.org/" rel="nofollow">Python</a> 或透过 <a href="https://nodejs.org/" rel="nofollow">NodeJS</a> 的 Javascript</li><li>网页程序框架如 <a href="https://www.serverless.com/" rel="nofollow">Serverless Framework</a></li></ul><h2 id="--9">模糊地带</h2><p>透过 <a href="https://nodejs.org/en/" rel="nofollow">Node.js</a> 的 Javascript 开发者能够使用自己喜欢的浏览器语言执行与服务器语言相同的任务。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://camo.githubusercontent.com/b3e3b9f78b4cca29f21d67a549bc5860294b6781/68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f6e6f64656a732d6e657665722d73746f707065642d746f2d7468696e6b2d69662d73686f756c642e6a7067" class="kg-image" alt="68747470733a2f2f7777772e66726565636f646563616d702e6f72672f6e6577732f636f6e74656e742f696d616765732f323032302f30362f6e6f64656a732d6e657665722d73746f707065642d746f2d7468696e6b2d69662d73686f756c642e6a7067" width="600" height="400" loading="lazy"><figcaption>不停止地想像能够在服务器写 JS 代码</figcaption></figure><p>虽然并不是所有人都是以 Javascript 作为服务器语言，当它的确能够简化整个程序只使用同一种语言。这重写了前端和后端的游戏规则。</p><p>但它他也开始进入一个完整的生态圈，就是现在我们会看到系统的 API 就建立在 <a href="https://redwoodjs.com/tutorial/redwood-file-structure" rel="nofollow">前端的隔壁</a> 这跟传统的格式一样。</p><h2 id="-vs-">前端 vs 后端</h2><p>无论前端或后端，总是会有区分的界限。所有互动的 UI 不论是在服务器或浏览器运行的，都属于前端。而那些使前端的逻辑发生和资料来源，不论是某公司提供的服务或者是自定义的结构，都是后端。</p><p>不论你喜欢搭建使用者界面或者建立后台逻辑，都有不少的资源让你开始。</p><h2 id="--10">学习资源</h2><h3 id="--11">前端</h3><ul><li><a href="https://www.freecodecamp.org/learn/" rel="nofollow">freecodecamp.org Responsive Web Design Certification</a> (freecodecamp.org)</li><li><a href="https://beginnerjavascript.com/" rel="nofollow">Beginner Javascript</a> (beginnerjavascript.com - Wes Bos)</li><li><a href="https://www.youtube.com/watch?v=Ke90Tje7VS0" rel="nofollow">React Tutorial for Beginners</a> (youtube.com - Programming with Mosh)</li><li><a href="https://frontendmasters.com/" rel="nofollow">Front End Masters</a> (frontendmasters.com)</li></ul><h3 id="--12">后端</h3><ul><li><a href="https://www.freecodecamp.org/learn" rel="nofollow">freecodecamp.org APIs and Microservices Certification</a> (freecodecamp.org)</li><li><a href="https://kentcdodds.com/blog/super-simple-start-to-serverless/" rel="nofollow">Super simple start to serverless</a> (kentcdodds.com)</li><li><a href="https://www.freecodecamp.org/news/aws-certified-cloud-practitioner-training-2019-free-video-course/" rel="nofollow">AWS Certified Cloud Practitioner Training 2019 - A Free 4-hour Video Course</a> (freecodecamp.org)</li><li><a href="https://www.edx.org/course/cs50s-introduction-to-computer-science" rel="nofollow">CS50's Introduction to Computer Science</a> (edx.org)</li></ul><h3 id="--13">以上皆是</h3><ul><li><a href="https://www.colbyfayock.com/2020/02/how-to-become-a-full-stack-web-developer-in-2020/" rel="nofollow">How to Become a Full Stack Web Developer in 2020</a> (colbyfayock.com)</li><li><a href="https://egghead.io/?af=atzgap" rel="nofollow">Egghead.io</a> (egghead.io)</li><li><a href="https://www.100daysofcode.com/" rel="nofollow">100 Days of Code</a> (100daysofcode.com)</li><li><a href="https://www.udemy.com/course/the-web-developer-bootcamp/" rel="nofollow">The Web Developer Bootcamp</a> (udemy.com - Colt Steele)</li></ul><p>原文：<a href="https://www.freecodecamp.org/news/front-end-developer-vs-back-end-developer-definition-and-meaning-in-practice/">Front End Developer vs Back End Developer – Definition and Meaning In Practice</a>，作者：Colby Fayock</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 关于前端性能优化的 24 条建议（2020） ]]>
                </title>
                <description>
                    <![CDATA[ 前端性能优化是把双刃剑，有好的一面也有坏的一面。好的一面是能提升网站性能，坏的一面就是配置麻烦，或者要遵守的规则太多。并且某些性能优化规则并不适用所有场景，需要谨慎使用，请读者带着批判性的眼光来阅读本文。 文中优化建议相关的引用资料出处均会在建议后面给出，或者放在文末。 1. 减少 HTTP 请求 一个完整的 HTTP 请求需要经历 DNS 查找，TCP 握手，浏览器发出 HTTP 请求，服务器接收请求，服务器处理请求并发回响应，浏览器接收响应等过程。接下来看一个具体的例子帮助理解 HTTP ： 这是一个 HTTP 请求，请求的文件大小为 28.4KB。 名词解释：  * Queueing: 在请求队列中的时间。  * Stalled: 从TCP 连接建立完成，到真正可以传输数据之间的时间差，此时间包括代理协商时间。  * Proxy negotiation: 与代理服务器连接进行协商所花费的时间。  * DNS Lookup: 执行DNS查找所花费的时间，页面上的每个不同的域都需要进行DNS查找。  * Initial Connection / Connecting: 建立 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/improve-front-end-performance/</link>
                <guid isPermaLink="false">5faa8c525f583f0565090ff1</guid>
                
                    <category>
                        <![CDATA[ 前端 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 网站性能 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Tue, 10 Nov 2020 12:30:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/04/photo-1593642632823-8f785ba67e45.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>前端性能优化是把双刃剑，有好的一面也有坏的一面。好的一面是能提升网站性能，坏的一面就是配置麻烦，或者要遵守的规则太多。并且某些性能优化规则并不适用所有场景，需要谨慎使用，请读者带着批判性的眼光来阅读本文。</p><p>文中优化建议相关的引用资料出处均会在建议后面给出，或者放在文末。</p><h3 id="1-http-">1. 减少 HTTP 请求</h3><p>一个完整的 HTTP 请求需要经历 DNS 查找，TCP 握手，浏览器发出 HTTP 请求，服务器接收请求，服务器处理请求并发回响应，浏览器接收响应等过程。接下来看一个具体的例子帮助理解 HTTP ：</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/05c1c42e60734ecd8dc7db8f4a8443ce~tplv-k3u1fbpfcp-zoom-1.image" class="kg-image" alt="在这里插入图片描述" width="521" height="406" loading="lazy"></figure><p>这是一个 HTTP 请求，请求的文件大小为 28.4KB。</p><p>名词解释：</p><ul><li>Queueing: 在请求队列中的时间。</li><li>Stalled: 从TCP 连接建立完成，到真正可以传输数据之间的时间差，此时间包括代理协商时间。</li><li>Proxy negotiation: 与代理服务器连接进行协商所花费的时间。</li><li>DNS Lookup: 执行DNS查找所花费的时间，页面上的每个不同的域都需要进行DNS查找。</li><li>Initial Connection / Connecting: 建立连接所花费的时间，包括TCP握手/重试和协商SSL。</li><li>SSL: 完成SSL握手所花费的时间。</li><li>Request sent: 发出网络请求所花费的时间，通常为一毫秒的时间。</li><li>Waiting(TFFB): TFFB 是发出页面请求到接收到应答数据第一个字节的时间总和，它包含了 DNS 解析时间、 TCP 连接时间、发送 HTTP 请求时间和获得响应消息第一个字节的时间。</li><li>Content Download: 接收响应数据所花费的时间。</li></ul><p>从这个例子可以看出，真正下载数据的时间占比为 <code>13.05 / 204.16 = 6.39%</code>，文件越小，这个比例越小，文件越大，比例就越高。这就是为什么要建议将多个小文件合并为一个大文件，从而减少 HTTP 请求次数的原因。</p><p>参考资料：</p><ul><li><a href="https://developers.google.com/web/tools/chrome-devtools/network/understanding-resource-timing" rel="nofollow noopener noreferrer">understanding-resource-timing</a></li></ul><h3 id="2-http2">2. 使用 HTTP2</h3><p>HTTP2 相比 HTTP1.1 有如下几个优点：</p><h4 id="-">解析速度快</h4><p>服务器解析 HTTP1.1 的请求时，必须不断地读入字节，直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦，因为 HTTP2 是基于帧的协议，每个帧都有表示帧长度的字段。</p><h4 id="--1">多路复用</h4><p>HTTP1.1 如果要同时发起多个请求，就得建立多个 TCP 连接，因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。</p><p>在 HTTP2 上，多个请求可以共用一个 TCP 连接，这称为多路复用。同一个请求和响应用一个流来表示，并有唯一的流 ID 来标识。 多个请求和响应在 TCP 连接中可以乱序发送，到达目的地后再通过流 ID 重新组建。</p><h4 id="--2">首部压缩</h4><p>HTTP2 提供了首部压缩功能。</p><p>例如有如下两个请求：</p><pre><code>:authority: unpkg.zhimg.com
:method: GET
:path: /za-js-sdk@2.16.0/dist/zap.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36</code></pre><pre><code>:authority: zz.bdstatic.com
:method: GET
:path: /linksubmit/push.js
:scheme: https
accept: */*
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9
cache-control: no-cache
pragma: no-cache
referer: https://www.zhihu.com/
sec-fetch-dest: script
sec-fetch-mode: no-cors
sec-fetch-site: cross-site
user-agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36</code></pre><p>从上面两个请求可以看出来，有很多数据都是重复的。如果可以把相同的首部存储起来，仅发送它们之间不同的部分，就可以节省不少的流量，加快请求的时间。</p><p>HTTP/2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键－值对，对于相同的数据，不再通过每次请求和响应发送。</p><p>下面再来看一个简化的例子，假设客户端按顺序发送如下请求首部：</p><pre><code>Header1:foo
Header2:bar
Header3:bat</code></pre><p>当客户端发送请求时，它会根据首部值创建一张表：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-11.png" class="kg-image" alt="image-11" width="433" height="233" loading="lazy"></figure><p>如果服务器收到了请求，它会照样创建一张表。当客户端发送下一个请求的时候，如果首部相同，它可以直接发送这样的首部块：</p><pre><code>62 63 64</code></pre><p>服务器会查找先前建立的表格，并把这些数字还原成索引对应的完整首部。</p><h4 id="--3">优先级</h4><p>HTTP2 可以对比较紧急的请求设置一个较高的优先级，服务器在收到这样的请求后，可以优先处理。</p><h4 id="--4">流量控制</h4><p>由于一个 TCP 连接流量带宽（根据客户端到服务器的网络带宽而定）是固定的，当有多个请求并发时，一个请求占的流量多，另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。</p><h4 id="--5">服务器推送</h4><p>HTTP2 新增的一个强大的新功能，就是服务器可以对一个客户端请求发送多个响应。换句话说，除了对最初请求的响应外，服务器还可以额外向客户端推送资源，而无需客户端明确地请求。</p><p>例如当浏览器请求一个网站时，除了返回 HTML 页面外，服务器还可以根据 HTML 页面中的资源的 URL，来提前推送资源。</p><p>现在有很多网站已经开始使用 HTTP2 了，例如知乎：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-1.png" class="kg-image" alt="image-1" width="499" height="239" loading="lazy"></figure><p>其中 h2 是指 HTTP2 协议，http/1.1 则是指 HTTP1.1 协议。</p><p>参考资料：</p><ul><li><a href="https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn" rel="nofollow noopener noreferrer">HTTP2 简介</a></li><li><a href="https://github.com/woai3c/Front-end-articles/blob/master/http-https-http2.md" rel="nofollow noopener noreferrer">半小时搞懂 HTTP、HTTPS和HTTP2</a></li></ul><h3 id="3-">3. 使用服务端渲染</h3><p>客户端渲染: 获取 HTML 文件，根据需要下载 JavaScript 文件，运行文件，生成 DOM，再渲染。</p><p>服务端渲染：服务端返回 HTML 文件，客户端只需解析 HTML。</p><ul><li>优点：首屏渲染快，SEO 好。</li><li>缺点：配置麻烦，增加了服务器的计算压力。</li></ul><p>参考资料：</p><ul><li><a href="https://github.com/woai3c/vue-ssr-demo" rel="nofollow noopener noreferrer">vue-ssr-demo</a></li><li><a href="https://ssr.vuejs.org/zh/" rel="nofollow noopener noreferrer">Vue.js 服务器端渲染指南</a></li></ul><h3 id="4-cdn">4. 静态资源使用 CDN</h3><p>内容分发网络（CDN）是一组分布在多个不同地理位置的 Web 服务器。我们都知道，当服务器离用户越远时，延迟越高。CDN 就是为了解决这一问题，在多个位置部署服务器，让用户离服务器更近，从而缩短请求时间。</p><h4 id="cdn-">CDN 原理</h4><p>当用户访问一个网站时，如果没有 CDN，过程是这样的：</p><ol><li>浏览器要将域名解析为 IP 地址，所以需要向本地 DNS 发出请求。</li><li>本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求，得到网站服务器的 IP 地址。</li><li>本地 DNS 将 IP 地址发回给浏览器，浏览器向网站服务器 IP 地址发出请求并得到资源。</li></ol><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-2.png" class="kg-image" alt="image-2" width="533" height="429" loading="lazy"></figure><p>如果用户访问的网站部署了 CDN，过程是这样的：</p><ol><li>浏览器要将域名解析为 IP 地址，所以需要向本地 DNS 发出请求。</li><li>本地 DNS 依次向根服务器、顶级域名服务器、权限服务器发出请求，得到全局负载均衡系统（GSLB）的 IP 地址。</li><li>本地 DNS 再向 GSLB 发出请求，GSLB 的主要功能是根据本地 DNS 的 IP 地址判断用户的位置，筛选出距离用户较近的本地负载均衡系统（SLB），并将该 SLB 的 IP 地址作为结果返回给本地 DNS。</li><li>本地 DNS 将 SLB 的 IP 地址发回给浏览器，浏览器向 SLB 发出请求。</li><li>SLB 根据浏览器请求的资源和地址，选出最优的缓存服务器发回给浏览器。</li><li>浏览器再根据 SLB 发回的地址重定向到缓存服务器。</li><li>如果缓存服务器有浏览器需要的资源，就将资源发回给浏览器。如果没有，就向源服务器请求资源，再发给浏览器并缓存在本地。</li></ol><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-3.png" class="kg-image" alt="image-3" width="534" height="607" loading="lazy"></figure><p>参考资料：</p><ul><li><a href="https://www.zhihu.com/question/36514327/answer/193768864" rel="nofollow noopener noreferrer">CDN是什么？使用CDN有什么优势？</a></li><li><a href="https://juejin.im/post/6844903873518239752">CDN原理简析</a></li></ul><h3 id="5-css-javascript-">5. 将 CSS 放在文件头部，JavaScript 文件放在底部</h3><p>所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话，那么页面就空白了。所以 JS 文件要放在底部，等 HTML 解析完了再加载 JS 文件。</p><p>那为什么 CSS 文件还要放在头部呢？</p><p>因为先加载 HTML 再加载 CSS，会让用户第一时间看到的页面是没有样式的、“丑陋”的，为了避免这种情况发生，就要将 CSS 文件放在头部了。</p><p>另外，JS 文件也不是不可以放在头部，只要给 script 标签加上 defer 属性就可以了，异步下载，延迟执行。</p><h3 id="6-iconfont-">6. 使用字体图标 iconfont 代替图片图标</h3><p>字体图标就是将图标制作成一个字体，使用时就跟字体一样，可以设置属性，例如 font-size、color 等等，非常方便。并且字体图标是矢量图，不会失真。还有一个优点是生成的文件特别小。</p><h4 id="--6">压缩字体文件</h4><p>使用 <a href="https://github.com/patrickhulce/fontmin-webpack" rel="nofollow noopener noreferrer">fontmin-webpack</a> 插件对字体文件进行压缩（感谢<a href="https://juejin.im/user/237150239985165">前端小伟</a>提供）。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-4.png" class="kg-image" alt="image-4" width="898" height="412" loading="lazy"></figure><p>参考资料：</p><ul><li><a href="https://github.com/patrickhulce/fontmin-webpack" rel="nofollow noopener noreferrer">fontmin-webpack</a></li><li><a href="https://www.iconfont.cn/" rel="nofollow noopener noreferrer">Iconfont-阿里巴巴矢量图标库</a></li></ul><h3 id="7-">7. 善用缓存，不重复加载相同的资源</h3><p>为了避免用户每次访问网站都得请求文件，我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间，只要在这个时间之前，浏览器都不会请求文件，而是直接使用缓存。而 max-age 是一个相对时间，建议使用 max-age 代替 Expires 。</p><p>不过这样会产生一个问题，当文件更新了怎么办？怎么通知浏览器重新请求文件？</p><p>可以通过更新页面中引用的资源链接地址，让浏览器主动放弃缓存，加载新资源。</p><p>具体做法是把资源地址 URL 的修改与文件内容关联起来，也就是说，只有文件内容变化，才会导致相应 URL 的变更，从而实现文件级别的精确缓存控制。什么东西与文件内容相关呢？我们会很自然的联想到利用<a href="https://baike.baidu.com/item/%E6%B6%88%E6%81%AF%E6%91%98%E8%A6%81%E7%AE%97%E6%B3%95/3286770?fromtitle=%E6%91%98%E8%A6%81%E7%AE%97%E6%B3%95&amp;fromid=12011257" rel="nofollow noopener noreferrer">数据摘要要算法</a>对文件求摘要信息，摘要信息与文件内容一一对应，就有了一种可以精确到单个文件粒度的缓存控制依据了。</p><p>参考资料：</p><ul><li><a href="https://github.com/woai3c/node-blog/blob/master/doc/node-blog7.md" rel="nofollow noopener noreferrer">webpack + express 实现文件精确缓存</a></li><li><a href="https://www.webpackjs.com/guides/caching/" rel="nofollow noopener noreferrer">webpack-缓存</a></li><li><a href="https://www.zhihu.com/question/20790576/answer/32602154" rel="nofollow noopener noreferrer">张云龙--大公司里怎样开发和部署前端代码？</a></li></ul><h3 id="8-">8. 压缩文件</h3><p>压缩文件可以减少文件下载时间，让用户体验性更好。</p><p>得益于 webpack 和 node 的发展，现在压缩文件已经非常方便了。</p><p>在 webpack 可以使用如下插件进行压缩：</p><ul><li>JavaScript：UglifyPlugin</li><li>CSS ：MiniCssExtractPlugin</li><li>HTML：HtmlWebpackPlugin</li></ul><p>其实，我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然，服务器也得支持这一功能。</p><p>gzip 是目前最流行和最有效的压缩方法。举个例子，我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB，使用 gzip 压缩后只有 573KB，体积减少了将近 60%。</p><p>附上 webpack 和 node 配置 gzip 的使用方法。</p><p><strong>下载插件</strong></p><pre><code>npm install compression-webpack-plugin --save-dev
npm install compression</code></pre><p><strong>webpack 配置</strong></p><pre><code>const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [new CompressionPlugin()],
}</code></pre><p><strong>node 配置</strong></p><pre><code>const compression = require('compression')
// 在其他中间件前使用
app.use(compression())</code></pre><h3 id="9-">9. 图片优化</h3><h4 id="-1-">(1). 图片延迟加载</h4><p>在页面中，先不给图片设置路径，只有当图片出现在浏览器的可视区域时，才去加载真正的图片，这就是延迟加载。对于图片很多的网站来说，一次性加载全部图片，会对用户体验造成很大的影响，所以需要使用图片延迟加载。</p><p>首先可以将图片这样设置，在页面不可见时图片不会加载：</p><pre><code class="language-html">&lt;img data-src="https://avatars0.githubusercontent.com/u/22117876?s=460&amp;u=7bd8f32788df6988833da6bd155c3cfbebc68006&amp;v=4"&gt;</code></pre><p>等页面可见时，使用 JS 加载图片：</p><pre><code class="language-js">const img = document.querySelector('img')
img.src = img.dataset.src</code></pre><p>这样图片就加载出来了，完整的代码可以看一下参考资料。</p><p>参考资料：</p><ul><li><a href="https://juejin.im/entry/6844903482164510734">web 前端图片懒加载实现原理</a></li></ul><h4 id="-2-">(2). 响应式图片</h4><p>响应式图片的优点是浏览器能够根据屏幕大小自动加载合适的图片。</p><p>通过 <code>picture</code> 实现</p><pre><code class="language-html">&lt;picture&gt;
	&lt;source srcset="banner_w1000.jpg" media="(min-width: 801px)"&gt;
	&lt;source srcset="banner_w800.jpg" media="(max-width: 800px)"&gt;
	&lt;img src="banner_w800.jpg" alt=""&gt;
&lt;/picture&gt;</code></pre><p>通过 <code>@media</code> 实现</p><pre><code class="language-html">@media (min-width: 769px) {
	.bg {
		background-image: url(bg1080.jpg);
	}
}
@media (max-width: 768px) {
	.bg {
		background-image: url(bg768.jpg);
	}
}</code></pre><h4 id="-3-">(3). 调整图片大小</h4><p>例如，你有一个 1920 * 1080 大小的图片，用缩略图的方式展示给用户，并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上，则浪费了下载图片的时间。</p><p>所以，我们可以用两张图片来实行优化。一开始，只加载缩略图，当用户悬停在图片上时，才加载大图。还有一种办法，即对大图进行延迟加载，在所有元素都加载完成后手动更改大图的 src 进行下载。</p><h4 id="-4-">(4). 降低图片质量</h4><p>例如 JPG 格式的图片，100% 的质量和 90% 质量的通常看不出来区别，尤其是用来当背景图的时候。我经常用 PS 切背景图时， 将图片切成 JPG 格式，并且将它压缩到 60% 的质量，基本上看不出来区别。</p><p>压缩方法有两种，一是通过 webpack 插件 <code>image-webpack-loader</code>，二是通过在线网站进行压缩。</p><p>以下附上 webpack 插件 <code>image-webpack-loader</code> 的用法。</p><pre><code>npm i -D image-webpack-loader</code></pre><p>webpack 配置</p><pre><code class="language-js">{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  use:[
    {
    loader: 'url-loader',
    options: {
      limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
      name: utils.assetsPath('img/[name].[hash:7].[ext]')
      }
    },
    /*对图片进行压缩*/
    {
      loader: 'image-webpack-loader',
      options: {
        bypassOnDebug: true,
      }
    }
  ]
}</code></pre><h4 id="-5-css3-">(5). 尽可能利用 CSS3 效果代替图片</h4><p>有很多图片使用 CSS 效果（渐变、阴影等）就能画出来，这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。</p><p>参考资料：</p><ul><li><a href="https://juejin.im/post/6844903816081457159">img图片在webpack中使用</a></li></ul><h4 id="-6-webp-">(6). 使用 webp 格式的图片</h4><p>WebP 的优势体现在它具有更优的图像数据压缩算法，能带来更小的图片体积，而且拥有肉眼识别无差异的图像质量；同时具备了无损和有损的压缩模式、Alpha 透明以及动画的特性，在 JPEG 和 PNG 上的转化效果都相当优秀、稳定和统一。</p><p>参考资料：</p><ul><li><a href="https://www.zhihu.com/question/27201061" rel="nofollow noopener noreferrer">WebP 相对于 PNG、JPG 有什么优势？</a></li></ul><h3 id="10-webpack-es6-es5-">10. 通过 webpack 按需加载代码，提取第三库代码，减少 ES6 转为 ES5 的冗余代码</h3><p>懒加载或者按需加载，是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开，然后在一些代码块中完成某些操作后，立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度，减轻了它的总体体积，因为某些代码块可能永远不会被加载。</p><h4 id="-import-">根据文件内容生成文件名，结合 import 动态引入组件实现按需加载</h4><p>通过配置 output 的 filename 属性可以实现这个需求。filename 属性的值选项中有一个 [contenthash]，它将根据文件内容创建出唯一 hash。当文件内容发生变化时，[contenthash] 也会发生变化。</p><pre><code class="language-js">output: {
	filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].js',
    path: path.resolve(__dirname, '../dist'),
},</code></pre><h4 id="--7">提取第三方库</h4><p>由于引入的第三方库一般都比较稳定，不会经常改变。所以将它们单独提取出来，作为长期缓存是一个更好的选择。 这里需要使用 webpack4 的 splitChunk 插件 cacheGroups 选项。</p><pre><code class="language-js">optimization: {
  	runtimeChunk: {
        name: 'manifest' // 将 webpack 的 runtime 代码拆分为一个单独的 chunk。
    },
    splitChunks: {
        cacheGroups: {
            vendor: {
                name: 'chunk-vendors',
                test: /[\\/]node_modules[\\/]/,
                priority: -10,
                chunks: 'initial'
            },
            common: {
                name: 'chunk-common',
                minChunks: 2,
                priority: -20,
                chunks: 'initial',
                reuseExistingChunk: true
            }
        },
    }
},</code></pre><ul><li>test: 用于控制哪些模块被这个缓存组匹配到。原封不动传递出去的话，它默认会选择所有的模块。可以传递的值类型：RegExp、String和Function;</li><li>priority：表示抽取权重，数字越大表示优先级越高。因为一个 module 可能会满足多个 cacheGroups 的条件，那么抽取到哪个就由权重最高的说了算；</li><li>reuseExistingChunk：表示是否使用已有的 chunk，如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出去了，那么将不会重新生成新的。</li><li>minChunks（默认是1）：在分割之前，这个代码块最小应该被引用的次数（译注：保证代码块复用性，默认配置的策略是不需要多次引用也可以被分割）</li><li>chunks (默认是async) ：initial、async和all</li><li>name(打包的chunks的名字)：字符串或者函数(函数可以根据条件自定义名字)</li></ul><h4 id="-es6-es5-">减少 ES6 转为 ES5 的冗余代码</h4><p>Babel 转化后的代码想要实现和原来代码一样的功能需要借助一些帮助函数，比如：</p><pre><code class="language-js">class Person {}</code></pre><p>会被转换为：</p><pre><code class="language-js">"use strict";

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

var Person = function Person() {
  _classCallCheck(this, Person);
};</code></pre><p>这里 <code>_classCallCheck</code> 就是一个 <code>helper</code> 函数，如果在很多文件里都声明了类，那么就会产生很多个这样的 <code>helper</code> 函数。</p><p>这里的 <code>@babel/runtime</code> 包就声明了所有需要用到的帮助函数，而 <code>@babel/plugin-transform-runtime</code> 的作用就是将所有需要 <code>helper</code> 函数的文件，从 <code>@babel/runtime包</code> 引进来：</p><pre><code class="language-js">"use strict";

var _classCallCheck2 = require("@babel/runtime/helpers/classCallCheck");

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

function _interopRequireDefault(obj) {
  return obj &amp;&amp; obj.__esModule ? obj : { default: obj };
}

var Person = function Person() {
  (0, _classCallCheck3.default)(this, Person);
};</code></pre><p>这里就没有再编译出 <code>helper</code> 函数 <code>classCallCheck</code> 了，而是直接引用了 <code>@babel/runtime</code> 中的 <code>helpers/classCallCheck</code>。</p><p><strong>安装</strong></p><pre><code>npm i -D @babel/plugin-transform-runtime @babel/runtime</code></pre><p><strong>使用</strong>在 <code>.babelrc</code> 文件中</p><pre><code>"plugins": [
        "@babel/plugin-transform-runtime"
]</code></pre><p>参考资料：</p><ul><li><a href="https://www.jianshu.com/p/d078b5f3036a" rel="nofollow noopener noreferrer">Babel 7.1介绍 transform-runtime polyfill env</a></li><li><a href="http://webpack.docschina.org/guides/lazy-loading/" rel="nofollow noopener noreferrer">懒加载</a></li><li><a href="https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E8%B7%AF%E7%94%B1%E6%87%92%E5%8A%A0%E8%BD%BD" rel="nofollow noopener noreferrer">Vue 路由懒加载</a></li><li><a href="https://webpack.docschina.org/guides/caching/" rel="nofollow noopener noreferrer">webpack 缓存</a></li><li><a href="https://juejin.im/post/6844903614759043079">一步一步的了解webpack4的splitChunk插件</a></li></ul><h3 id="11-">11. 减少重绘重排</h3><p><strong>浏览器渲染过程</strong></p><ol><li>解析HTML生成DOM树。</li><li>解析CSS生成CSSOM规则树。</li><li>将DOM树与CSSOM规则树合并在一起生成渲染树。</li><li>遍历渲染树开始布局，计算每个节点的位置大小信息。</li><li>将渲染树每个节点绘制到屏幕。</li></ol><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-5.png" class="kg-image" alt="image-5" width="624" height="289" loading="lazy"></figure><p><strong>重排</strong></p><p>当改变 DOM 元素位置或大小时，会导致浏览器重新生成渲染树，这个过程叫重排。</p><p><strong>重绘</strong></p><p>当重新生成渲染树后，就要将渲染树每个节点绘制到屏幕，这个过程叫重绘。不是所有的动作都会导致重排，例如改变字体颜色，只会导致重绘。记住，重排会导致重绘，重绘不会导致重排 。</p><p>重排和重绘这两个操作都是非常昂贵的，因为 JavaScript 引擎线程与 GUI 渲染线程是互斥，它们同时只能一个在工作。</p><p>什么操作会导致重排？</p><ul><li>添加或删除可见的 DOM 元素</li><li>元素位置改变</li><li>元素尺寸改变</li><li>内容改变</li><li>浏览器窗口尺寸改变</li></ul><p>如何减少重排重绘？</p><ul><li>用 JavaScript 修改样式时，最好不要直接写样式，而是替换 class 来改变样式。</li><li>如果要对 DOM 元素执行一系列操作，可以将 DOM 元素脱离文档流，修改完成后，再将它带回文档。推荐使用隐藏元素（display:none）或文档碎片（DocumentFragement），都能很好的实现这个方案。</li></ul><h3 id="12-">12. 使用事件委托</h3><p>事件委托利用了事件冒泡，只指定一个事件处理程序，就可以管理某一类型的所有事件。所有用到按钮的事件（多数鼠标事件和键盘事件）都适合采用事件委托技术， 使用事件委托可以节省内存。</p><pre><code class="language-js">&lt;ul&gt;
  &lt;li&gt;苹果&lt;/li&gt;
  &lt;li&gt;香蕉&lt;/li&gt;
  &lt;li&gt;凤梨&lt;/li&gt;
&lt;/ul&gt;

// good
document.querySelector('ul').onclick = (event) =&gt; {
  const target = event.target
  if (target.nodeName === 'LI') {
    console.log(target.innerHTML)
  }
}

// bad
document.querySelectorAll('li').forEach((e) =&gt; {
  e.onclick = function() {
    console.log(this.innerHTML)
  }
}) </code></pre><h3 id="13-">13. 注意程序的局部性</h3><p>一个编写良好的计算机程序常常具有良好的局部性，它们倾向于引用最近引用过的数据项附近的数据项，或者最近引用过的数据项本身，这种倾向性，被称为局部性原理。有良好局部性的程序比局部性差的程序运行得更快。</p><p><strong>局部性通常有两种不同的形式：</strong></p><ul><li>时间局部性：在一个具有良好时间局部性的程序中，被引用过一次的内存位置很可能在不远的将来被多次引用。</li><li>空间局部性 ：在一个具有良好空间局部性的程序中，如果一个内存位置被引用了一次，那么程序很可能在不远的将来引用附近的一个内存位置。</li></ul><p>时间局部性示例</p><pre><code class="language-js">function sum(arry) {
	let i, sum = 0
	let len = arry.length

	for (i = 0; i &lt; len; i++) {
		sum += arry[i]
	}

	return sum
}</code></pre><p>在这个例子中，变量sum在每次循环迭代中被引用一次，因此，对于sum来说，具有良好的时间局部性</p><p>空间局部性示例</p><p><strong>具有良好空间局部性的程序</strong></p><pre><code class="language-js">// 二维数组 
function sum1(arry, rows, cols) {
	let i, j, sum = 0

	for (i = 0; i &lt; rows; i++) {
		for (j = 0; j &lt; cols; j++) {
			sum += arry[i][j]
		}
	}
	return sum
}</code></pre><p><strong>空间局部性差的程序</strong></p><pre><code class="language-js">// 二维数组 
function sum2(arry, rows, cols) {
	let i, j, sum = 0

	for (j = 0; j &lt; cols; j++) {
		for (i = 0; i &lt; rows; i++) {
			sum += arry[i][j]
		}
	}
	return sum
}</code></pre><p>看一下上面的两个空间局部性示例，像示例中从每行开始按顺序访问数组每个元素的方式，称为具有步长为1的引用模式。 如果在数组中，每隔k个元素进行访问，就称为步长为k的引用模式。 一般而言，随着步长的增加，空间局部性下降。</p><p>这两个例子有什么区别？区别在于第一个示例是按行扫描数组，每扫描完一行再去扫下一行；第二个示例是按列来扫描数组，扫完一行中的一个元素，马上就去扫下一行中的同一列元素。</p><p>数组在内存中是按照行顺序来存放的，结果就是逐行扫描数组的示例得到了步长为 1 引用模式，具有良好的空间局部性；而另一个示例步长为 rows，空间局部性极差。</p><h4 id="--8">性能测试</h4><p>运行环境：</p><ul><li>cpu: i5-7400</li><li>浏览器: chrome 70.0.3538.110</li></ul><p>对一个长度为9000的二维数组（子数组长度也为9000）进行10次空间局部性测试，时间（毫秒）取平均值，结果如下：<br></p><p>所用示例为上述两个空间局部性示例 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 步长为 1步长为 90001242316</p><p>从以上测试结果来看，步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。</p><p>总结：</p><ul><li>重复引用相同变量的程序具有良好的时间局部性</li><li>对于具有步长为 k 的引用模式的程序，步长越小，空间局部性越好；而在内存中以大步长跳来跳去的程序空间局部性会很差</li></ul><p>参考资料：</p><ul><li><a href="https://book.douban.com/subject/26912767/" rel="nofollow noopener noreferrer">深入理解计算机系统</a></li></ul><h3 id="14-if-else-switch">14. if-else 对比 switch</h3><p>当判断条件数量越来越多时，越倾向于使用 switch 而不是 if-else。</p><pre><code class="language-js">if (color == 'blue') {

} else if (color == 'yellow') {

} else if (color == 'white') {

} else if (color == 'black') {

} else if (color == 'green') {

} else if (color == 'orange') {

} else if (color == 'pink') {

}

switch (color) {
    case 'blue':

        break
    case 'yellow':

        break
    case 'white':

        break
    case 'black':

        break
    case 'green':

        break
    case 'orange':

        break
    case 'pink':

        break
}</code></pre><p>像以上这种情况，使用 switch 是最好的。假设 color 的值为 pink，则 if-else 语句要进行 7 次判断，switch 只需要进行一次判断。 从可读性来说，switch 语句也更好。从使用时机来说，当条件值大于两个的时候，使用 switch 更好。</p><p>不过，switch 只能用于 case 值为常量的分支结构，而 if-else 更加灵活。</p><h3 id="15-">15. 查找表</h3><p>当条件语句特别多时，使用 switch 和 if-else 不是最佳的选择，这时不妨试一下查找表。查找表可以使用数组和对象来构建。</p><pre><code class="language-js">switch (index) {
    case '0':
        return result0
    case '1':
        return result1
    case '2':
        return result2
    case '3':
        return result3
    case '4':
        return result4
    case '5':
        return result5
    case '6':
        return result6
    case '7':
        return result7
    case '8':
        return result8
    case '9':
        return result9
    case '10':
        return result10
    case '11':
        return result11
}</code></pre><p>可以将这个 switch 语句转换为查找表</p><pre><code class="language-js">const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]

return results[index]</code></pre><p>如果条件语句不是数值而是字符串，可以用对象来建立查找表</p><pre><code class="language-js">const map = {
  red: result0,
  green: result1,
}

return map[color]</code></pre><h3 id="16-">16. 避免页面卡顿</h3><p><strong>60fps 与设备刷新率</strong></p><p>目前大多数设备的屏幕刷新率为 60 次/秒。因此，如果在页面中有一个动画或渐变效果，或者用户正在滚动页面，那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。</p><p>其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上，浏览器有整理工作要做，因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算，帧率将下降，并且内容会在屏幕上抖动。 此现象通常称为卡顿，会对用户体验产生负面影响。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-6.png" class="kg-image" alt="image-6" width="1093" height="167" loading="lazy"></figure><p>假如你用 JavaScript 修改了 DOM，并触发样式修改，经历重排重绘最后画到屏幕上。如果这其中任意一项的执行时间过长，都会导致渲染这一帧的时间过长，平均帧率就会下降。假设这一帧花了 50 ms，那么此时的帧率为 1s / 50ms = 20fps，页面看起来就像卡顿了一样。</p><p>对于一些长时间运行的 JavaScript，我们可以使用定时器进行切分，延迟执行。</p><pre><code class="language-js">for (let i = 0, len = arry.length; i &lt; len; i++) {
	process(arry[i])
}</code></pre><p>假设上面的循环结构由于 process() 复杂度过高或数组元素太多，甚至两者都有，可以尝试一下切分。</p><pre><code class="language-js">const todo = arry.concat()
setTimeout(function() {
	process(todo.shift())
	if (todo.length) {
		setTimeout(arguments.callee, 25)
	} else {
		callback(arry)
	}
}, 25)</code></pre><p>如果有兴趣了解更多，可以查看一下<a href="https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/%E9%AB%98%E6%80%A7%E8%83%BDJavaScript.pdf" rel="nofollow noopener noreferrer">高性能JavaScript</a>第 6 章和<a href="https://book.douban.com/subject/30170670/" rel="nofollow noopener noreferrer">高效前端：Web高效编程与优化实践</a>第 3 章。</p><p>参考资料：</p><ul><li><a href="https://developers.google.com/web/fundamentals/performance/rendering" rel="nofollow noopener noreferrer">渲染性能</a></li></ul><h3 id="17-requestanimationframe-">17. 使用 requestAnimationFrame 来实现视觉变化</h3><p>从第 16 点我们可以知道，大多数设备屏幕刷新率为 60 次/秒，也就是说每一帧的平均时间为 16.66 毫秒。在使用 JavaScript 实现动画效果的时候，最好的情况就是每次代码都是在帧的开头开始执行。而保证 JavaScript 在帧开始时运行的唯一方式是使用 <code>requestAnimationFrame</code>。</p><pre><code class="language-js">/**
 * If run as a requestAnimationFrame callback, this
 * will be run at the start of the frame.
 */
function updateScreen(time) {
  // Make visual updates here.
}

requestAnimationFrame(updateScreen);</code></pre><p>如果采取 <code>setTimeout</code> 或 <code>setInterval</code> 来实现动画的话，回调函数将在帧中的某个时点运行，可能刚好在末尾，而这可能经常会使我们丢失帧，导致卡顿。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-7.png" class="kg-image" alt="image-7" width="890" height="380" loading="lazy"></figure><p>参考资料：</p><ul><li><a href="https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution?hl=zh-cn" rel="nofollow noopener noreferrer">优化 JavaScript 执行</a></li></ul><h3 id="18-web-workers">18. 使用 Web Workers</h3><p>Web Worker 使用其他工作线程从而独立于主线程之外，它可以执行任务而不干扰用户界面。一个 worker 可以将消息发送到创建它的 JavaScript 代码, 通过将消息发送到该代码指定的事件处理程序（反之亦然）。</p><p>Web Worker 适用于那些处理纯数据，或者与浏览器 UI 无关的长时间运行脚本。</p><p>创建一个新的 worker 很简单，指定一个脚本的 URI 来执行 worker 线程（main.js）：</p><pre><code class="language-js">var myWorker = new Worker('worker.js');
// 你可以通过postMessage() 方法和onmessage事件向worker发送消息。
first.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

second.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}</code></pre><p>在 worker 中接收到消息后，我们可以写一个事件处理函数代码作为响应（worker.js）：</p><pre><code class="language-js">onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}</code></pre><p>onmessage处理函数在接收到消息后马上执行，代码中消息本身作为事件的data属性进行使用。这里我们简单的对这2个数字作乘法处理并再次使用postMessage()方法，将结果回传给主线程。</p><p>回到主线程，我们再次使用onmessage以响应worker回传的消息：</p><pre><code class="language-js">myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}</code></pre><p>在这里我们获取消息事件的data，并且将它设置为result的textContent，所以用户可以直接看到运算的结果。</p><p>不过在worker内，不能直接操作DOM节点，也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西，包括WebSockets，IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。</p><p>参考资料：</p><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers" rel="nofollow noopener noreferrer">Web Workers</a></li></ul><h3 id="19-">19. 使用位操作</h3><p>JavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。但是在位操作中，数字被转换为有符号的 32 位格式。即使需要转换，位操作也比其他数学运算和布尔操作快得多。</p><h5 id="--9">取模</h5><p>由于偶数的最低位为 0，奇数为 1，所以取模运算可以用位操作来代替。</p><pre><code class="language-js">if (value % 2) {
	// 奇数
} else {
	// 偶数 
}
// 位操作
if (value &amp; 1) {
	// 奇数
} else {
	// 偶数
}</code></pre><h5 id="--10">取反</h5><pre><code class="language-js">~~10.12 // 10
~~10 // 10
~~'1.5' // 1
~~undefined // 0
~~null // 0</code></pre><h5 id="--11">位掩码</h5><pre><code class="language-js">const a = 1
const b = 2
const c = 4
const options = a | b | c</code></pre><p>通过定义这些选项，可以用按位与操作来判断 a/b/c 是否在 options 中。</p><pre><code class="language-js">// 选项 b 是否在选项中
if (b &amp; options) {
	...
}</code></pre><h3 id="20-">20. 不要覆盖原生方法</h3><p>无论你的 JavaScript 代码如何优化，都比不上原生方法。因为原生方法是用低级语言写的（C/C++），并且被编译成机器码，成为浏览器的一部分。当原生方法可用时，尽量使用它们，特别是数学运算和 DOM 操作。</p><h3 id="21-css-">21. 降低 CSS 选择器的复杂性</h3><h4 id="-1--1">(1). 浏览器读取选择器，遵循的原则是从选择器的右边到左边读取。</h4><p>看个示例</p><pre><code class="language-css">#block .text p {
	color: red;
}</code></pre><ol><li>查找所有 P 元素。</li><li>查找结果 1 中的元素是否有类名为 text 的父元素</li><li>查找结果 2 中的元素是否有 id 为 block 的父元素</li></ol><h4 id="-2-css-">(2). CSS 选择器优先级</h4><pre><code>内联 &gt; ID选择器 &gt; 类选择器 &gt; 标签选择器</code></pre><p>根据以上两个信息可以得出结论。</p><ol><li>选择器越短越好。</li><li>尽量使用高优先级的选择器，例如 ID 和类选择器。</li><li>避免使用通配符 *。</li></ol><p>最后要说一句，据我查找的资料所得，CSS 选择器没有优化的必要，因为最慢和慢快的选择器性能差别非常小。</p><p>参考资料：</p><ul><li><a href="https://ecss.io/appendix1.html" rel="nofollow noopener noreferrer">CSS selector performance</a></li><li><a href="https://www.sitepoint.com/optimizing-css-id-selectors-and-other-myths/" rel="nofollow noopener noreferrer">Optimizing CSS: ID Selectors and Other Myths</a></li></ul><h3 id="22-flexbox-">22. 使用 flexbox 而不是较早的布局模型</h3><p>在早期的 CSS 布局方式中我们能对元素实行绝对定位、相对定位或浮动定位。而现在，我们有了新的布局方式 <a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox" rel="nofollow noopener noreferrer">flexbox</a>，它比起早期的布局方式来说有个优势，那就是性能比较好。</p><p>下面的截图显示了在 1300 个框上使用浮动的布局开销：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-8.png" class="kg-image" alt="image-8" width="1394" height="948" loading="lazy"></figure><p>然后我们用 flexbox 来重现这个例子：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-9.png" class="kg-image" alt="image-9" width="1394" height="904" loading="lazy"></figure><p>现在，对于相同数量的元素和相同的视觉外观，布局的时间要少得多（本例中为分别 3.5 毫秒和 14 毫秒）。</p><p>不过 flexbox 兼容性还是有点问题，不是所有浏览器都支持它，所以要谨慎使用。</p><p>各浏览器兼容性：</p><ul><li>Chrome 29+</li><li>Firefox 28+</li><li>Internet Explorer 11</li><li>Opera 17+</li><li>Safari 6.1+ (prefixed with -webkit-)</li><li>Android 4.4+</li><li>iOS 7.1+ (prefixed with -webkit-)</li></ul><p>参考资料：</p><ul><li><a href="https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing?hl=zh-cn" rel="nofollow noopener noreferrer">使用 flexbox 而不是较早的布局模型</a></li></ul><h3 id="23-transform-opacity-">23. 使用 transform 和 opacity 属性更改来实现动画</h3><p>在 CSS 中，transforms 和 opacity 这两个属性更改不会触发重排与重绘，它们是可以由合成器（composite）单独处理的属性。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-10.png" class="kg-image" alt="image-10" width="1093" height="167" loading="lazy"></figure><p>参考资料：</p><ul><li><a href="https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count?hl=zh-cn" rel="nofollow noopener noreferrer">使用 transform 和 opacity 属性更改来实现动画</a></li></ul><h3 id="24-">24. 合理使用规则，避免过度优化</h3><p>性能优化主要分为两类：</p><ol><li>加载时优化</li><li>运行时优化</li></ol><p>上述 23 条建议中，属于加载时优化的是前面 10 条建议，属于运行时优化的是后面 13 条建议。通常来说，没有必要 23 条性能优化规则都用上，根据网站用户群体来做针对性的调整是最好的，节省精力，节省时间。</p><p>在解决问题之前，得先找出问题，否则无从下手。所以在做性能优化之前，最好先调查一下网站的加载性能和运行性能。</p><h5 id="--12">检查加载性能</h5><p>一个网站加载性能如何主要看白屏时间和首屏时间。</p><ul><li>白屏时间：指从输入网址，到页面开始显示内容的时间。</li><li>首屏时间：指从输入网址，到页面完全渲染的时间。</li></ul><p>将以下脚本放在 <code>&lt;/head&gt;</code> 前面就能获取白屏时间。</p><pre><code class="language-html">&lt;script&gt;
	new Date() - performance.timing.navigationStart
&lt;/script&gt;</code></pre><p>首屏时间比较复杂，得考虑有图片和没有图片的情况。</p><p>如果没有图片，则在 <code>window.onload</code> 事件里执行 <code>new Date() - performance.timing.navigationStart</code> 即可获取首屏时间。</p><p>如果有图片，则要在最后一个在首屏渲染的图片的 <code>onload</code> 事件里执行 <code>new Date() - performance.timing.navigationStart</code> 获取首屏时间，实施起来比较复杂，在这里限于篇幅就不说了。</p><h5 id="--13">检查运行性能</h5><p>配合 chrome 的开发者工具，我们可以查看网站在运行时的性能。</p><p>打开网站，按 F12 选择 performance，点击左上角的灰色圆点，变成红色就代表开始记录了。这时可以模仿用户使用网站，在使用完毕后，点击 stop，然后你就能看到网站运行期间的性能报告。如果有红色的块，代表有掉帧的情况；如果是绿色，则代表 FPS 很好。performance 的具体使用方法请用搜索引擎搜索一下，毕竟篇幅有限。</p><p>通过检查加载和运行性能，相信你对网站性能已经有了大概了解。所以这时候要做的事情，就是使用上述 23 条建议尽情地去优化你的网站，加油！</p><p>参考资料：</p><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming/navigationStart" rel="nofollow noopener noreferrer">performance.timing.navigationStart</a></li></ul><h2 id="--14">其他参考资料</h2><ul><li><a href="https://developers.google.com/web/fundamentals/performance/why-performance-matters?hl=zh-cn" rel="nofollow noopener noreferrer">性能为何至关重要</a></li><li><a href="https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%AB%99%E5%BB%BA%E8%AE%BE%E6%8C%87%E5%8D%97.pdf" rel="nofollow noopener noreferrer">高性能网站建设指南</a></li><li><a href="https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/Web%E6%80%A7%E8%83%BD%E6%9D%83%E5%A8%81%E6%8C%87%E5%8D%97.pdf" rel="nofollow noopener noreferrer">Web性能权威指南</a></li><li><a href="https://github.com/woai3c/recommended-books/blob/master/%E5%89%8D%E7%AB%AF/%E9%AB%98%E6%80%A7%E8%83%BDJavaScript.pdf" rel="nofollow noopener noreferrer">高性能JavaScript</a></li><li><a href="https://book.douban.com/subject/30170670/" rel="nofollow noopener noreferrer">高效前端：Web高效编程与优化实践</a></li></ul> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
