<?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, 25 May 2026 19:55:37 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/chinese/news/tag/front-end-development/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ 播客 Ep. 25 叶苏雄：职业、开源和公益，一位三年前端开发者的成长和探索 ]]>
                </title>
                <description>
                    <![CDATA[ 在今天的节目中，我很开心邀请嘉宾叶苏雄分享他丰富的经历=他在高中时因为对于游戏的兴趣开始学习编程，大学期间就读计算机专业，在硬件开发、机器学习等多个技术领域都有探索和学习，也曾在 freeCodeCamp 学习前端开发。毕业后他先后在国企、大型互联网企业和外资银行工作。 他聊到职业发展，比如在轮岗时担任前端团队 leader，推动代码规范、流程规范、文档规范的建立和实施，以及在一个重视工作和生活平衡的环境中参与组织有趣的活动，提升自己的能力；还聊到在工作之余，作为核心成员参与 headless 组件库开源项目的开发；同时，苏雄还在多个公益组织中贡献，比如陪视障跑者跑步、教随迁子女学英语和编程。 相信你会从他的故事中获得启发和动力，期待你关注我们的播客，并把节目分享给更多朋友，也欢迎你发邮件分享自己的故事，也许我们会邀请你作为 freeCodeCamp 播客的嘉宾。你可以在这篇文章 [https://www.freecodecamp.org/chinese/news/freecodecamp-podcast-in-chinese/] 中找到邮箱地址。 欢迎在 https://chi ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/interview-ye-su-xiong-bank-front-end-development-and-open-source-projects/</link>
                <guid isPermaLink="false">65abb92dc8b4c103e552101a</guid>
                
                    <category>
                        <![CDATA[ 播客 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Fri, 19 Jan 2024 13:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2024/01/Chinese--1-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>在今天的节目中，我很开心邀请嘉宾叶苏雄分享他丰富的经历=他在高中时因为对于游戏的兴趣开始学习编程，大学期间就读计算机专业，在硬件开发、机器学习等多个技术领域都有探索和学习，也曾在 freeCodeCamp 学习前端开发。毕业后他先后在国企、大型互联网企业和外资银行工作。</p><!--kg-card-begin: html--><iframe width="100%" height="180" frameborder="no" scrolling="no" seamless="" src="https://share.transistor.fm/e/98a2a8a2" title="嵌入内容" loading="lazy"></iframe><!--kg-card-end: html--><p>他聊到职业发展，比如在轮岗时担任前端团队 leader，推动代码规范、流程规范、文档规范的建立和实施，以及在一个重视工作和生活平衡的环境中参与组织有趣的活动，提升自己的能力；还聊到在工作之余，作为核心成员参与 headless 组件库开源项目的开发；同时，苏雄还在多个公益组织中贡献，比如陪视障跑者跑步、教随迁子女学英语和编程。</p><p>相信你会从他的故事中获得启发和动力，期待你关注我们的播客，并把节目分享给更多朋友，也欢迎你发邮件分享自己的故事，也许我们会邀请你作为 freeCodeCamp 播客的嘉宾。你可以在<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><a href="https://blog.suxiong.me/">博客</a></li><li><a href="https://juejin.cn/post/7316820571206074379">《3年前端 25岁｜ 2023年年终总结》</a></li></ul><h2 id="--1"><strong>主要话题</strong></h2><ul><li>04:26 用 Lua 语言制作游戏 mod</li><li>07:27 喜欢硬件编程，做树莓派小车和自动书写时钟</li><li>15:09 在学校记者团做采访，不再社恐</li><li>20:50 “这才是人生啊”</li><li>23:51 在不同公司的体验和 work life balance</li><li>39:04 银行在技术方面的独特要求</li><li>43:16 担任前端团队 leader</li><li>56:31 在公司组织活动、担任主持人</li><li>1:03:13 参与国际化的开源项目提升英语水平</li><li>1:08:34 在 freeCodeCamp 协作翻译 Quincy 的书</li><li>1:14:02 参与 Google-translate-select 开源项目</li><li>1:20:38 与土耳其和尼日利亚开发者合作开发 headless 组件库</li><li>1:36:16 在开源项目中踩的坑</li><li>1:42:25 和盲人一起跑步</li><li>1:51:07 教随迁子女学英语和编程</li><li>1:55:58 在帮助别人的时候也提升自己的能力</li><li>2:00:39 快乐地写代码，成为痛点解决工程师</li><li>2:03:52 Just do it</li></ul><h2 id="--2"><strong>提到的资源</strong></h2><ul><li><a href="https://www.lua.org/">Lua</a></li><li><a href="https://vuejs.org/">Vue</a></li><li><a href="https://nuxt.com/">Nuxt</a></li><li><a href="https://en.wikipedia.org/wiki/Diversity,_equity,_and_inclusion">DEI</a></li><li><a href="https://www.freecodecamp.org/chinese/news/learn-to-code-book/">《如何学习编程并找到开发者工作 [完整指南]》</a></li><li><a href="https://github.com/i7eo/google-translate-select">Google-translate-select</a></li><li><a href="https://github.com/oku-ui">OKU</a></li><li><a href="http://www.dialogueinthedark.com.cn/run/">黑暗跑团</a></li><li><a href="https://www.steppingstones.cn/zh/%E4%B8%AD%E6%96%87/">铺路石</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 播客 Ep. 17 余琦：保持好奇心，在技术领域不断“刷油漆” ]]>
                </title>
                <description>
                    <![CDATA[ 我们的生活中可能都会有这样的朋友：在各种活动中他不是最引人关注的那一个，合影也很少站 C 位，喜欢自己探索，偶然发一个作品出来，令人感觉惊艳。 这期节目的嘉宾余琦给我的印象就是这样一位朋友。 余琦 他很早就对电子产品感兴趣，在中学时开始学习编程，大学时是电子竞技校队的成员。大学毕业后，他本来有一份在很多人眼里稳定的工作，因为对软件开发更感兴趣，他在学习和职业发展路上做更多探索，例如在 freeCodeCamp 学习前端开发、加入 K12 编程教育创业公司。现在他是 ThoughtWorks 的专家级咨询师，最近在研究 AI 新技术。 在节目中，他分享了自己的这些经历，以及对他很有启发的“油漆滴”模型。我们还聊到他组织文言文编程工作坊的时候，写的那篇让我觉得惊艳的文章。 如果他保持好奇心、透明做自己的状态也能给你一些启发和动力，请把这期节目分享给更多朋友。也欢迎你发邮件分享自己的故事，也许我们会邀请你作为 freeCodeCamp 播客的嘉宾。你可以在这篇文章 [https://www.freecodecamp.org/chinese/news/freecodecamp-podc ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/interview-yu-qi-stay-curious-and-keep-painting-in-the-tech-field/</link>
                <guid isPermaLink="false">654d083ae0668103ebf81a78</guid>
                
                    <category>
                        <![CDATA[ 播客 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 人工智能 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Thu, 09 Nov 2023 13:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/11/Chinese-1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>我们的生活中可能都会有这样的朋友：在各种活动中他不是最引人关注的那一个，合影也很少站 C 位，喜欢自己探索，偶然发一个作品出来，令人感觉惊艳。</p><p>这期节目的嘉宾余琦给我的印象就是这样一位朋友。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/news/content/images/2023/11/yuqi.jpeg" class="kg-image" alt="yuqi" srcset="https://chinese.freecodecamp.org/news/content/images/size/w600/2023/11/yuqi.jpeg 600w, https://chinese.freecodecamp.org/news/content/images/size/w1000/2023/11/yuqi.jpeg 1000w, https://chinese.freecodecamp.org/news/content/images/size/w1600/2023/11/yuqi.jpeg 1600w, https://chinese.freecodecamp.org/news/content/images/2023/11/yuqi.jpeg 1776w" sizes="(min-width: 720px) 720px" width="600" height="400" 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/200b830c" title="嵌入内容" loading="lazy"></iframe><!--kg-card-end: html--><p>他很早就对电子产品感兴趣，在中学时开始学习编程，大学时是电子竞技校队的成员。大学毕业后，他本来有一份在很多人眼里稳定的工作，因为对软件开发更感兴趣，他在学习和职业发展路上做更多探索，例如在 freeCodeCamp 学习前端开发、加入 K12 编程教育创业公司。现在他是 ThoughtWorks 的专家级咨询师，最近在研究 AI 新技术。</p><p>在节目中，他分享了自己的这些经历，以及对他很有启发的“油漆滴”模型。我们还聊到他组织文言文编程工作坊的时候，写的那篇让我觉得惊艳的文章。</p><p>如果他保持好奇心、透明做自己的状态也能给你一些启发和动力，请把这期节目分享给更多朋友。也欢迎你发邮件分享自己的故事，也许我们会邀请你作为 freeCodeCamp 播客的嘉宾。你可以在<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><p>注：29:16 DEI 是 Diversity、Equity、Inclusion 三个词的缩写。</p><h2 id="-"><strong><strong>主要话题</strong></strong></h2><ul><li>03:19 编程和游戏</li><li>11:26 成为前端开发者</li><li>17:52 青少年编程教育</li><li>28:25 ThoughtWorks 企业文化</li><li>38:00 在 ThoughtWorks 的工作内容</li><li>49:41 开发者如何提升综合能力</li><li>50:58 文言文编程</li><li>56:20 参与帮助孕妇的项目</li><li>59:16 在 TEDxChengdu 协作</li><li>1:03:53 对 AI 新技术的观察和思考</li><li>1:10:24 保持好奇心</li></ul><h2 id="--1"><strong><strong>提到的资源</strong></strong></h2><ul><li><a href="https://chinese.freecodecamp.org/learn">freeCodeCamp 课程</a></li><li><a href="https://ed.ted.com/ted_ed_collections/think-like-a-coder">TEDEd《像程序员一样思考》</a></li><li><a href="https://code.org/">编程一小时</a></li><li><a href="https://www.codingame.com/start/">CodinGame</a></li><li><a href="https://zhuanlan.zhihu.com/p/102594855">文言文编程工作坊</a></li><li><a href="https://www.notion.so/6865246e82984627938cff4256507692?pvs=21">油漆滴模型</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Ep. 16 对话钢琴家和软件工程师 Panpan Lin：编程对我来说就像做饭和种菜 ]]>
                </title>
                <description>
                    <![CDATA[ 今天的嘉宾 Panpan 是一位钢琴家，也是一位软件工程师。 Panpan Lin她在本科读了英语和历史专业、硕士读了哲学专业，并尝试了不同领域的实习和兼职之后，原本想应聘成为一名设计师，最后却被公司聘请为前端开发者。 她从小学时开始学习编程，后来也使用 freeCodeCamp 的学习资源提升技能。对她来说，编程和英语、弹钢琴甚至做饭、种菜一样，就是一种技能。知识的海洋就在那里，我们可以自由地探索和学习。 Panpan 住在爱尔兰都柏林，她在节目中也分享了在这座“文学之城”生活和工作的体验。 希望你喜欢这期节目，并把它分享给更多朋友。也欢迎你发邮件分享自己的故事，也许我们会邀请你作为 freeCodeCamp 播客的嘉宾。你可以在这篇文章 [https://www.freecodecamp.org/chinese/news/freecodecamp-podcast-in-chinese/] 中找到邮箱地址。 欢迎在 https://chinese.freecodecamp.org/ 查看更多免费的编程学习资源。 主要话题  * 01:31 成长经历和选择专业  * 12: ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/interview-panpan-lin-pianist-and-software-engineer/</link>
                <guid isPermaLink="false">654499c1e0668103ebf81a4a</guid>
                
                    <category>
                        <![CDATA[ 播客 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Miya Liu ]]>
                </dc:creator>
                <pubDate>Thu, 02 Nov 2023 13:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2023/11/Chinese--2-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>今天的嘉宾 Panpan 是一位钢琴家，也是一位软件工程师。</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/news/content/images/2023/11/Untitled.png" class="kg-image" alt="Untitled" width="600" height="400" loading="lazy"><figcaption>Panpan Lin</figcaption></figure><!--kg-card-begin: html--><iframe width="100%" height="180" frameborder="no" scrolling="no" seamless="" src="https://share.transistor.fm/e/0d33f1ee" title="嵌入内容" loading="lazy"></iframe><!--kg-card-end: html--><p>她在本科读了英语和历史专业、硕士读了哲学专业，并尝试了不同领域的实习和兼职之后，原本想应聘成为一名设计师，最后却被公司聘请为前端开发者。</p><p>她从小学时开始学习编程，后来也使用 freeCodeCamp 的学习资源提升技能。对她来说，编程和英语、弹钢琴甚至做饭、种菜一样，就是一种技能。知识的海洋就在那里，我们可以自由地探索和学习。</p><p>Panpan 住在爱尔兰都柏林，她在节目中也分享了在这座“文学之城”生活和工作的体验。</p><p>希望你喜欢这期节目，并把它分享给更多朋友。也欢迎你发邮件分享自己的故事，也许我们会邀请你作为 freeCodeCamp 播客的嘉宾。你可以在<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>01:31 成长经历和选择专业</li><li>12:22 “文学之城”都柏林</li><li>17:56 丰富的实习经历</li><li>22:18 成为全职软件工程师</li><li>31:03 音乐和编程的共通之处</li><li>34:06 开发者的工作日常</li><li>42:58 都柏林的开发者社区</li><li>47:56 给初学者的建议</li></ul><h2 id="--1"><strong>更多关于嘉宾的信息</strong></h2><ul><li><a href="https://vimeo.com/178944281">钢琴演奏的视频</a></li><li><a href="https://www.flickr.com/photos/panpanlin/">摄影集</a></li><li><a href="https://www.youtube.com/watch?v=hSw0uaXShJ0">为电影配乐 1</a></li><li><a href="https://www.youtube.com/watch?v=UA65BJL8aF0">为电影配乐 2</a></li></ul><h2 id="--2"><strong>提到的资源</strong></h2><ul><li><a href="https://www.youtube.com/watch?v=vNbBMAoqJgg">《Panpan Lin - Visualisation of Duplicated Code》</a></li><li><a href="https://otia.io/2018/04/20/panpan-lin-software-engineer-and-pianist/">《Panpan Lin, Software Engineer and Pianist》</a></li><li><a href="https://coderdojo.com/en">CoderDojo</a></li><li><a href="https://www.meetup.com/dublin-oss/">Dublin Open Source Meetup</a></li><li><a href="https://shipitcon.com/">ShipItCon</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 前端与后端有什么区别？ ]]>
                </title>
                <description>
                    <![CDATA[ 原文：Frontend VS Backend – What's the Difference [https://www.freecodecamp.org/news/frontend-vs-backend-whats-the-difference/] ，作者：Dionysia Lemonaki [https://www.freecodecamp.org/news/author/dionysia/] 在你的Web开发之旅的开始，你会经常听到关于应用程序的前端和后端部分。 但这些术语到底是什么意思？ 在这篇文章中，你将通过分别学习前端和后端来了解两者之间的区别。 以下是我们将涵盖的内容：  1. 前端的定义 1. 什么被认为是前端？      2. 为什么前端很重要？      3. 常见的前端任务      4. 前端工具和技术      5. 前端开发适合你吗？   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/frontend-vs-backend-whats-the-difference/</link>
                <guid isPermaLink="false">624122787f18d1062895befd</guid>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ luojiyin ]]>
                </dc:creator>
                <pubDate>Mon, 28 Mar 2022 03:11:43 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2022/03/pexels-christina-morillo-1181677.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>原文：<a href="https://www.freecodecamp.org/news/frontend-vs-backend-whats-the-difference/">Frontend VS Backend – What's the Difference</a>，作者：<a href="https://www.freecodecamp.org/news/author/dionysia/">Dionysia Lemonaki</a></p><!--kg-card-begin: markdown--><p>在你的Web开发之旅的开始，你会经常听到关于应用程序的前端和后端部分。</p>
<p>但这些术语到底是什么意思？</p>
<p>在这篇文章中，你将通过分别学习前端和后端来了解两者之间的区别。</p>
<p>以下是我们将涵盖的内容：</p>
<ol>
<li>前端的定义
<ol>
<li>什么被认为是前端？</li>
<li>为什么前端很重要？</li>
<li>常见的前端任务</li>
<li>前端工具和技术</li>
<li>前端开发适合你吗？</li>
</ol>
</li>
<li>后端的定义
<ol>
<li>后端做什么</li>
<li>后端功能示例</li>
<li>常见的后端任务</li>
<li>后端工具和技术</li>
<li>后端开发适合你吗？</li>
</ol>
</li>
</ol>
<h2 id="">什么是前端?</h2>
<p>前端是用户在点击一个链接或输入一个网页地址时看到的和与之互动的一切。网页地址也被称为URL，即统一资源定位器，它告诉你什么网页应该加载并出现在你的浏览器中。</p>
<p>它是网络应用的<strong>客户端</strong>部分。</p>
<p>例如，以这篇文章为例。有一个相关的封面图片，而你现在正在阅读这篇文章。</p>
<p>在页面的顶部，你也可以看到freeCodeCamp的标志。有一个导航栏，上面有一个链接到freeCodeCamp的论坛和一个捐赠的按钮。</p>
<p>还有一个搜索框。你可以输入一个关键词来搜索你感兴趣的主题的文章。</p>
<p>有一个链接可以点击，这将带你到课程和认证，这将帮助你免费学习代码。</p>
<p>上述所有内容代表你正在浏览的网页的前端。</p>
<h2 id="What-is-considered-frontend?">什么被认为是前端？</h2>
<p>前端是用户接触到的任何东西和所有的视觉。</p>
<p>它是所有与他们直接互动的部分。</p>
<p>它是所有的内容（content）和样式（styles）。</p>
<p>它是按钮和用户点击链接前的不同悬停效果。</p>
<p>它是带有各种输入字段的联系表格、搜索框和下拉菜单。</p>
<p>布局、文本和颜色。</p>
<p>是图片和视频。</p>
<p>然而，这不仅仅是样式（styles）问题。</p>
<p>它还包括网站的加载速度，浏览网站的方便程度，以及对残疾人的无障碍程度。</p>
<p>它在各种不同设备和浏览器上的可用性和响应性如何。</p>
<p>从本质上讲，前端是一个网络应用程序的所有部分，创造了它的 <em>外观和感觉（look and feel）</em>。</p>
<h2 id="Why-is-frontend-important?">为什么前端很重要</h2>
<p>前端代表着某人（或一个团队）的想法、愿景和梦想的实现，可供世人观看和见证。</p>
<p>这个想法需要以正确的方式表现出来，在视觉上具有吸引力。这将是其他人如何看待它的决定性因素。</p>
<p>一个公司或组织的网站如果难以使用和浏览，看起来不好看，不能很好地展示其服务，就会失去客户和顾客。</p>
<p>用户会因为缺乏可用性、不愉快的用户体验和缺乏好的设计而感到沮丧。他们最终会在其他地方寻找他们要找的东西，并且很可能不会再回到那个网站。</p>
<h2 id="Common-frontend-tasks">常见的前端任务</h2>
<p>一个前端开发员每天完成的任务和工作内容会有所不同。它们将在很大程度上取决于他们工作的公司和角色。</p>
<p>作为一个前端开发，你可能会做很多设计工作。</p>
<p>这可能是创建一个风格指南（style guide），以创建一致的风格和品牌的身份和整体外观和形象。</p>
<p>它包括所有文本的排版（字体），色彩方案，公司的标志和布局，仅举几例。</p>
<p>你可以使用设计工具建立用户界面，以确保网站有所有必要的视觉元素，并能愉快地显示和组织。</p>
<p>另一方面，前端开发人员可能不做任何设计工作。</p>
<p>相反，你可以与项目经理、平面设计师和UX/UI设计师合作，解决问题。</p>
<p>你可以根据的用户研究的结果创建视觉效果、图形。</p>
<p>你已经知道了目标受众和人口统计，常见的客户问题，以及关于网站对用户的可用性的信息。</p>
<p>然后，作为一个前端开发员，你的工作将是把原型和静态视觉设计转化为有形的东西，转化为一个真实世界的、像素完美的、功能齐全的网站，让用户可以轻松地与之互动。</p>
<p>使用前端技术，你将实现已经设计好的布局，并建立所有的视觉元素。</p>
<p>前端开发人员的工作的很大一部分是创建响应式网站。</p>
<p>你需要确保网站看起来不错，显示效果最佳，并能在各种设备和屏幕宽度上使用。</p>
<p>如今，用户并不只使用台式电脑，而是大多从移动和平板设备上查看网站。</p>
<p>此外，前端开发人员在设计和开发时要考虑到许多最流行和最常用的浏览器。</p>
<p>网站需要看起来不错，并且需要在所有的浏览器上实现最佳功能。</p>
<p>这就是了解你的目标受众和人口统计学的作用，是至关重要的信息。</p>
<p>根据你的服务/产品和你的用户/客户，你可能需要在开发时考虑到旧的或更现代的浏览器。</p>
<p>前端开发人员的另一个重要工作是确保网站对所有用户都是无障碍的。这需要创建网络无障碍功能，使浏览网络成为每个人的愉快体验。</p>
<p>前端开发人员可以实现的重要的无障碍功能是:</p>
<ul>
<li>文字转语音</li>
<li>只用键盘的导航</li>
<li>有足够对比度的无障碍颜色组合</li>
<li>大型按钮</li>
</ul>
<p>前端开发人员还致力于改善网站的性能和加载时间。</p>
<p>如果一个网站加载其内容和图像的时间太长，大多数用户会变得不耐烦并点击关闭。</p>
<p>改善网站的加载时间，哪怕是几秒钟，都会有很大的不同，而且很可能会留住用户。</p>
<p>改善网站的加载时间，哪怕是几秒钟，都会有很大的不同，而且很可能会留住用户。</p>
<p>网络表格（web forms）是公司/组织与用户之间沟通的桥梁。它们是用户进行咨询、联系客户服务、提交数据和创建账户的一种简单方式。</p>
<h2 id="Frontend-tools-and-technologies"> 前端工具和技术概述</h2>
<p>作为一名前端开发员，你将不断学习新的技术，这取决于当时的流行趋势。技术变化很快。</p>
<p>然而，最基本的前端技术是HTML、CSS和JavaScript。</p>
<p>它们在用户的浏览器中运行，是万维网上所有网站的基础。</p>
<p>让我们再来谈谈这些技术吧：</p>
<ul>
<li><strong>HTML</strong> 是指超文本标记语言。你用语义化的HTML创建网页的所有内容和结构。</li>
<li><strong>CSS</strong>  层叠样式表（Cascading Style Sheets）。CSS是对HTML元素的样式。它添加颜色、字体，决定文本的大小和网页的布局。</li>
<li><strong>Vanilla JavaScript</strong> 是你如何为元素添加功能和互动性的。它是你如何使网页具有动态行为的。</li>
</ul>
<p>除了这三个，还有许多前端库和框架。</p>
<p>这些有助于确保网站中每个网页的一致性，并为重复性任务提供开箱即用的功能。</p>
<p>一些最受欢迎的是:</p>
<ul>
<li>React</li>
<li>Redux</li>
<li>Vue</li>
<li>Angular</li>
</ul>
<p>根据不同的角色和工作描述，你可能需要设计技能。</p>
<p>一些常用于设计工作的工具有：</p>
<ul>
<li>Figma</li>
<li>Sketch</li>
<li>Adobe XD</li>
<li>and Photoshop for image processing</li>
</ul>
<h2 id="Is-frontend-development-right-for-you?">前端开发是否适合你？</h2>
<p>前端开发是一项创造性和艺术性的工作。</p>
<p>视觉艺术和编程都有融合。</p>
<p>决定前端开发是否适合你的一个方法是考虑你是否喜欢看到你努力工作的视觉结果。</p>
<p>可以在电脑屏幕上即时看到你的创作成果。</p>
<p>另一件要考虑的事情是，你是否非常关心用户在浏览网页时拥有最佳的用户体验。</p>
<p>用户体验和创建可用的网站是工作的一个重要部分。</p>
<p>要想知道前端开发是否适合你，唯一的办法就是给它一个机会。</p>
<p>一个开始学习的好地方是freeCodeCamp的 <a href="https://www.freecodecamp.org/chinese/learn/2022/responsive-web-design/">响应式网页设计认证</a>，你将通过建立20个项目来学习，并在最后获得一个认证。</p>
<p>你将学习HTML和现代CSS技术，以及最佳的可访问性实践。你还将学习如何创建适应各种屏幕尺寸的响应式网页。</p>
<p>在 <a href="https://www.freecodecamp.org/chinese/learn/2022/responsive-web-design/">响应式网页设计认证</a> 之后，通过 <a href="https://www.freecodecamp.org/chinese/learn/javascript-algorithms-and-data-structures-v8/">JavaScript算法和数据结构认证</a> 学习JavaScript编程语言，你将学习如何使网页互动。</p>
<p>你将从基础开始，学习语法和必要的基本构件，这将使你能够进展到更高级的主题，如面向对象的编程。</p>
<h2 id="How-the-backend-works"> 什么是后端？</h2>
<p>后端开发涉及负责存储和安全操作用户数据的技术。</p>
<p>它是与所有隐藏逻辑相关的部分，为用户互动的应用程序提供支持。</p>
<p>后端被认为是一个应用程序的服务器端部分。</p>
<p>后端是网络应用中所有隐藏的内部工作和幕后流程。</p>
<p>它指的是在看不到的进行的一切，以及使前端正常和顺利运行的所有必要组件。</p>
<p>它确保一切都在最佳状态下工作。</p>
<p>从本质上讲，后端是用户没有直接访问或没有直接互动的东西，在使用一个应用程序时很可能没有意识到。</p>
<h3 id="">面向初学者的后台工作概述</h3>
<p>后端负责接收来自客户端的请求。</p>
<p>它处理传入的请求，并确保获取与正确用户相关的适当数据。</p>
<p>然后，它向客户端发送一个响应。必要的用户数据通过前端开发人员创建的前端代码，以良好的视觉效果呈现给可以访问的正确用户。</p>
<p>后台由以下部分组成:</p>
<ul>
<li><strong>服务</strong>, 它是一个等待和监听来自客户端的请求的程序。它通过提交数据库查询与数据库进行通信。当数据库回复信息时，服务器会获取正确的必要数据。</li>
</ul>
<p>它在服务器上运行的应用程序的帮助下实现这一目标，该应用程序包含必要的逻辑，为正确的用户收集所要求的信息和资源。用后台脚本编程语言编写的后台服务器端脚本，负责处理请求。</p>
<p>最后，服务器向客户端发送响应。</p>
<ul>
<li><strong>数据库</strong>,它就像一个应用程序的 "大脑"。数据库是用来组织和存储一个应用程序的所有资产、内容和信息的程序，其方式可以方便地访问和检索、管理和更新。</li>
</ul>
<h2 id="Examples-of-backend-functionality">后端功能示例</h2>
<p>让我们举一个真实的例子来说明现代网络应用的不同后端功能。</p>
<p>假设你是一个付费锻炼订阅平台的成员。</p>
<p>登录:</p>
<ul>
<li>你需要在一个用户输入栏中输入你的电子邮件地址和密码。</li>
<li>有一些基本的验证检查需要通过。验证确保所需的输入字段不是空的，电子邮件地址有正确的格式。</li>
<li>然后，通过提交数据库查询，检查你是否作为一个用户存在于数据库中，以了解你的电子邮件地址和密码是否正确。<br>
还会有检查，看你提供的电子邮件地址和密码是否与你创建账户时存储在数据库中的电子邮件和密码组合相符。</li>
<li>如果电子邮件和密码不正确，你会看到一个错误信息，说这个组合不存在。</li>
<li>如果一切都正确，你会被引导到你的主页，并有一个视觉提示表明你已经登录。这可能是一个 "欢迎 "信息，包括你的名字或用户名。</li>
</ul>
<p>挑选一个锻炼项目:</p>
<ul>
<li>数据库储存了你所有的原始数据。<br>
已经完成的锻炼会储存在数据库中。同样，任何保存在你创建的播放列表中或标记为你想重温的收藏夹中的内容也是如此。</li>
<li>你还可以查看所有可用的锻炼，并对它们进行过滤，只显示某个特定类别。<br>
例如，你可以在搜索框中输入一个关键词，如腹肌、手臂、下半身或上半身。<br>
另一种方法是在网站上选择一个可用的类别，如瑜伽、拉伸、HIIT或力量。数据库将翻阅储存在那里的每一个锻炼项目，但你将只查看你在搜索中指定的特定类别的过滤结果。</li>
</ul>
<p>支付订阅费用：</p>
<ul>
<li>该应用程序将能够接受信用卡信息或其他付款方式，并将该信息安全地存储在数据库中。</li>
<li>它还会在正确的时间更新付款。根据订阅情况，每月或每年都会有重复的更新，确保每次都能从你的卡中提取足够的金额。</li>
</ul>
<p>所有这些都由一个应用程序的后端处理。</p>
<h2 id="Common-backend-tasks">常见的后端任务</h2>
<p>可以说，后端开发人员的角色因公司不同而不同，因工作不同而不同。</p>
<p>后端开发人员的主要工作重点是创建和维护帮助前端运作的服务和程序。</p>
<p>一般来说，这里有一些后端开发人员会从事的工作：</p>
<ul>
<li>创建、管理和维护产品/服务使用的数据库类型。</li>
<li>建立、互动和维护服务器。</li>
<li>使用服务器端技术和网络框架建立内部或外部功能和服务器端软件，以提供问题的解决方案。</li>
<li>使用API（应用程序设计接口）工作。设计、开发、实施、维护和管理支持CRUD（创建读取更新删除）操作的API。</li>
<li>执行数据验证，确保数据在存储到数据库之前具有正确的格式。</li>
<li>用户需要与他们的账户安全互动。后台开发人员创建系统，确保每个用户的数据安全 - 特别是在处理支付处理系统时。</li>
<li>处理第三方服务，如认证和外部支付服务，仅举几例。</li>
<li>组织系统逻辑。</li>
<li>开发网站架构。</li>
</ul>
<h2 id="Backend-tools-and-technologies">后端工具和技术</h2>
<p>后端开发人员在日常工作中使用不同的工具和技术来实现网络应用的逻辑。</p>
<p>后端编程的一个关键组成部分是使用服务器端的脚本编程语言。</p>
<p>一些最经常使用的是：</p>
<ul>
<li><strong>PHP</strong></li>
<li><strong>Ruby</strong></li>
<li><strong>Python</strong></li>
<li><strong>Java</strong></li>
<li><strong>JavaScript</strong>. 是的，JavaScript被广泛用于前端开发，但近年来也被用于后端开发。Node.js（一个JavaScript运行时间）通过提供后端功能使之成为可能。</li>
</ul>
<p>除了后端编程语言，后端框架和库被用来提供额外的功能来创建网络应用。</p>
<p>一些最受欢迎的是:</p>
<ul>
<li>Ruby on Rails</li>
<li>Django</li>
<li>Flask</li>
<li>Express</li>
</ul>
<p>当开发者是一个团队的一部分时，他们使用Git--一个跟踪不同项目文件变化的版本控制系统。</p>
<p>它是一个针对程序员的开源协作工具。</p>
<p>与数据库互动是后端开发人员工作的一个重要部分，所以数据库知识是最重要的。</p>
<p>数据库分为两类：关系型和非关系型。</p>
<p>一些最经常使用的数据库是：</p>
<ul>
<li>MySQL</li>
<li>PostgreSQL</li>
<li>MongoDB</li>
<li>Oracle</li>
</ul>
<p>在使用关系型数据库（如MySQL、PostgreSQL和Oracle）时，你将使用SQL。</p>
<p>SQL是结构化查询语言的缩写。它用于执行数据库查询。SQL与关系型数据库进行通信以操作数据。</p>
<p>你可以从 <a href="https://www.freecodecamp.org/news/learn-sql-free-relational-database-courses-for-beginners/">这个有用的资源清单</a> 了解更多关于SQL的信息。</p>
<h2 id="Is-backend-development-right-for-you?">后台开发是否适合你？</h2>
<p>后端开发人员负责网络应用程序背后的逻辑，并使前端的功能达到最佳。</p>
<p>如果你喜欢通过将问题分解成小块来解决问题，实施算法，并找到一个最佳和最有效的解决方案，也许后端开发就适合你。</p>
<p>如果你认为自己是一个善于分析和有条理的人，那么后台开发涉及到处理、分析和组织大量的数据。它需要对这些数据进行操作和计算。</p>
<p>最后，如果你不认为自己是一个特别有艺术细胞的人，而更关心事情的逻辑方面和引擎盖下发生的事情，也许你更适合做后台开发。</p>
<p>也就是说，人们可能会从前端开始，然后过渡到后端方面的事情。这样，他们就可以找出他们最喜欢的工作方式。</p>
<p>如果你想开始学习后台开发，可以通过freeCodeCamp的 <a href="https://www.freecodecamp.org/news/how-to-run-freecodecamps-relational-databases-curriculum-using-docker-vscode-and-coderoad/">关系型数据库课程</a> 进行学习。</p>
<p>你将学习使用SQL和PostgreSQL的关系数据库，Git版本控制系统，等等。这些将使你在后端旅程中取得一个良好的开端。</p>
<p>然后，你可以进一步学习 <a href="https://www.freecodecamp.org/chinese/learn/back-end-development-and-apis/">后端开发和API认证</a>。</p>
<p>在这里，你将学习Node.js并使用Express框架构建网络应用。</p>
<h2 id="">结语</h2>
<p>希望你现在对前端和后端开发的内容有了更多的了解，并能区分两者之间的区别。</p>
<p>简而言之，前台涉及用户互动的所有部分，而后台是为前台互动提供支持的逻辑。</p>
<p>谢谢你阅读本文！</p>
<!--kg-card-end: markdown--> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 利用 React 组件的“黄金法则"让代码更优雅之如何发挥钩子的作用 ]]>
                </title>
                <description>
                    <![CDATA[ 最近学到了一个新的理念，它改变了我创建组件的方式。它不仅是一个新的点子，还是一个新的视角。 组件的黄金法则 用更自然的方式创建和定义组件，组件只包含它们必需的代码 这是很短的一句话，可能你觉得已经理解了，但却很容易违反这一原则。 比如，有如下组件： PersonCard如果自然的定义这个组件你可能会这样写： PersonCard.propTypes = {   name: PropTypes.string.isRequired,   jobTitle: PropTypes.string.isRequired,   pictureUrl: PropTypes.string.isRequired, }; 代码很简单，每个属性都是它所必需的，如 name、job title 和 picture URL。 假设现在需要添加用户可更改的另一个更正式的图片。可能最容易想到的就是： PersonCard.propTypes = {   name: PropTypes.string.isRequired,   jobTitle: PropTypes.string.isRequired ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-the-golden-rule-of-react-components-can-help-you-write-better-code/</link>
                <guid isPermaLink="false">5da414ddfbfdee429dc60027</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Tue, 25 Jan 2022 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2019/10/123-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>最近学到了一个新的理念，它改变了我创建组件的方式。它不仅是一个新的点子，还是一个新的视角。</p><h2 id="-">组件的黄金法则</h2><p>用更自然的方式创建和定义组件，组件只包含它们必需的代码</p><p>这是很短的一句话，可能你觉得已经理解了，但却很容易违反这一原则。</p><p>比如，有如下组件：</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/58225752ea8321dfc62139045a03f1fbc0972f66.png" class="kg-image" alt="1_nF_5kuYHigZuwdq99vRJ8g" width="600" height="400" loading="lazy"><figcaption><em>PersonCard</em></figcaption></figure><p>如果自然的定义这个组件你可能会这样写：</p><pre><code class="language-javascript">PersonCard.propTypes = {
  name: PropTypes.string.isRequired,
  jobTitle: PropTypes.string.isRequired,
  pictureUrl: PropTypes.string.isRequired,
};
</code></pre><p>代码很简单，每个属性都是它所必需的，如 name、job title 和 picture URL。</p><p>假设现在需要添加用户可更改的另一个更正式的图片。可能最容易想到的就是：</p><pre><code class="language-javascript">PersonCard.propTypes = {
  name: PropTypes.string.isRequired,
  jobTitle: PropTypes.string.isRequired,
  officialPictureUrl: PropTypes.string.isRequired,
  pictureUrl: PropTypes.string.isRequired,
  preferOfficial: PropTypes.boolean.isRequired,
};
</code></pre><p>看起来这些 props 是组件必需的，实际上，组件没有这些 props 也不会受到影响。而且添加了 <code>preferOfficial</code> 看似增加了灵活性，其实逻辑本来不该添加在这里，考虑复用的时候会发现这样做很不优雅。</p><h2 id="--1">如何改进</h2><p>那么转换图片 URL 的逻辑不属于组件本身，那它属于哪里呢？</p><p>放在 <code>index</code> 里怎么样？</p><p>我们采用如下的目录结构，每个组件都有自己名字命名的文件夹，<code>index</code> 文件是沟通优雅组件和外部世界的桥梁。我们把这个文件叫做 “容器”（container）(<a href="https://redux.js.org/basics/usage-with-react#presentational-and-container-components" rel="nofollow noopener">参考了React Redux 的 “container” 组件概念</a>）。</p><pre><code>/PersonCard
  -PersonCard.js ------ the "natural" component
  -index.js ----------- the "container"
</code></pre><p>我们将容器（container）定义为连接优雅组件和外部世界的桥梁。正因为此，我们有时候又称之为 “注入（injectors）”。</p><p>优雅组件（natural component） 代表你的代码只包含必需的部分（没有诸如怎样获取数据或者位置等细节—所有代码都是必需的）。</p><p>外部世界（outside world）可以将数据转换成符合优雅组件所需的 props。</p><p>这篇文章讨论的：怎样让组件不受外部世界的污染，以及这样做的好处。</p><p>注意：虽然灵感来自 <a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0" rel="nofollow noopener">Dan’s Abramov</a> 和 <a href="https://redux.js.org/basics/usage-with-react#presentational-and-container-components" rel="nofollow noopener">React Redux’s</a> 的理念，但我们的容器和它们的略微不同。<br><a href="https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0" rel="nofollow noopener">Dan Abramov 的容器</a> 和我们区别是在概念层面上。Dan 认为有两种组件：展示组件和容器组件。我们在这个基础上更进一步，认为先有组件，后有容器。<br>虽然我们用组件实现容器，但我们不认为容器是传统意义的组件。这就是为什么我们建议你把容器放在 <code>index</code> 文件里—因为它是优雅组件和外部世界的桥梁。并不独立存在。</p><p>所以这篇文章会有大量的组件、容器字眼。</p><p>为什么？</p><p>创建一个优雅组件–很容易、甚至还很有趣。</p><p>连接组件和外部世界–有点难。</p><p>依我之见，外部世界对优雅组件的污染，主要是这三种方式：</p><ol><li>古怪的数据结构</li><li>组件 scope 之外的需求 (就像上面的代码那样)</li><li>在 update 或者 mount 时触发 event</li></ol><p>接下来的几节将会说明这些情况，并用例子展示不同情况下的容器实现。</p><h2 id="--2">处理古怪的数据结构</h2><p>有时候为了呈现需要的信息，需要把数据连在一起然后将其转换成特定的格式。由于没有更好的设计模式，“古怪的” 数据结构是最容易想到的的也是最不优雅的方式。</p><p>把古怪的数据结构直接传入组件然后在组件内部转换很诱人，但是这会让组件更复杂、更难测试。</p><p>我最近就掉进了这个坑里，我创建了一个组件，从一个特殊的数据结构获取数据，然后让它支持特殊的表单。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/forum/uploads/default/original/1X/2e09d403b4fd8e9e1b3b104fcb5610707bf32c02.gif" class="kg-image" alt="1_hFOPWOxkedUEb851jdAXjA" width="600" height="400" loading="lazy"></figure><pre><code class="language-javascript">ChipField.propTypes = {
  field: PropTypes.object.isRequired,      // &lt;-- the "weird" data structure
  onEditField: PropTypes.func.isRequired,  // &lt;-- and a weird event too
};
</code></pre><p>然后就诞生了这玩意，古怪的 <code>field</code> 数据结构做为 prop。另外，如果以后不需要在处理它了还好，但是当我们想要在另一个完全不相干的数据结构里复用它时，噩梦来了。</p><p>由于这个组件需要一个复杂的数据结构，复用几乎不可能，重构起来也很头大。之前写的测试也会很难看懂，因为它们 mock 了一个古怪的数据结构。在持续重构时测试逻辑很难懂也很难重写。</p><p>很不幸，古怪的数据结构很难避免，但是使用容器可以很好的驯服它。好处之一是你可以很好的复用组件了。之前直接把古怪的数据结构传入组件，是难复用的罪魁祸首。</p><p>注意： 我并不是说在创造组件的开始所有的组件就都应该是通用的。建议是好好考虑考虑组件的基本功能，然后在开始编码。回报是，通过少量工作写出一些高度可复用的组件。</p><h3 id="--3">使用函数组件实现容器</h3><p>如果你 mapping props 上要求很严格，容器的一个简单的实现是使用另一个函数组件：</p><pre><code class="language-javascript">import React from 'react';
import PropTypes from 'prop-types';

import getValuesFromField from './helpers/getValuesFromField';
import transformValuesToField from './helpers/transformValuesToField';

import ChipField from './ChipField';

export default function ChipFieldContainer({ field, onEditField }) {
  const values = getValuesFromField(field);
  
  function handleOnChange(values) {
    onEditField(transformValuesToField(values));
  }
  
  return &lt;ChipField values={values} onChange={handleOnChange} /&gt;;
}

// external props
ChipFieldContainer.propTypes = {
  field: PropTypes.object.isRequired,
  onEditField: PropTypes.func.isRequired,
};
</code></pre><p>组件的目录结构如下：</p><pre><code>/ChipField
  -ChipField.js ------------------ the "natural" chip field
  -ChipField.test.js
  -index.js ---------------------- the "container"
  -index.test.js
  /helpers ----------------------- a folder for the helpers/utils
    -getValuesFromField.js
    -getValuesFromField.test.js
    -transformValuesToField.js
    -transformValuesToField.test.js
</code></pre><p>你可能会说，这也太麻烦了。看起来是多了一些文件，绕了很多弯，但是别忘了：</p><p>在组件外面转换数据和在组件内工作量是一致的。区别是，在组件外面转换数据时，你给了你自己一个更明确的点来测试转换是否正确，分离了关注点。</p><h2 id="-scope-">在组件 scope 的外部满足需要</h2><p>和上面的 Person Card 一样，当你用 “黄金法则” 来思考的时候，很可能你会意识到需求是超出了组件的实际范围。该怎么实现呢？</p><p>没错，就是容器。</p><p>可以创建容器，通过少量的工作来保持组件的优雅。这样做的时候，你会解锁一个更专业的组件，这个组件也简单的多，同时容器也更易于测试。</p><p>让我们来写一个 PersonCard 容器来举栗说明。</p><h3 id="--4">使用高阶组件实现容器</h3><p>React Redux 就是使用了 <a href="https://reactjs.org/docs/higher-order-components.html" rel="nofollow noopener">高阶组件</a> ，实现了从 Redux store 里 push、map props 的容器。由于我们是从 React Redux 借鉴的这个理念，毫无疑问 <a href="https://redux.js.org/basics/usage-with-react#implementing-container-components" rel="nofollow noopener">React Redux 的 connect 就是这个容器</a>。</p><p>无论你是使用函数组件来映射 props，还是使用高阶组件来连接 Redux strore，组件的黄金法则还是不变的。首先，编写优雅组件，然后用高阶组件连接二者。</p><pre><code class="language-javascript">import { connect } from 'react-redux';

import getPictureUrl from './helpers/getPictureUrl';

import PersonCard from './PersonCard';

const mapStateToProps = (state, ownProps) =&gt; {
  const { person } = ownProps;
  const { name, jobTitle, customPictureUrl, officialPictureUrl } = person;
  const { preferOfficial } = state.settings;
  
  const pictureUrl = getPictureUrl(preferOfficial, customPictureUrl, officialPictureUrl);
  
  return { name, jobTitle, pictureUrl };
};

const mapDispatchToProps = null;

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(PersonCard);
</code></pre><p>文件结构如下：</p><pre><code>/PersonCard
  -PersonCard.js ----------------- natural component
  -PersonCard.test.js
  -index.js ---------------------- container
  -index.test.js
  /helpers
    -getPictureUrl.js ------------ helper
    -getPictureUrl.test.js
</code></pre><p>注意：在这里，给 <code>getPictureUrl</code> 提供一个助手。这个逻辑很简单。你可能已经注意到了，无论 container 实现如何，文件结构几乎没变。</p><p>如果你之前用过 Redux，上面的例子你一定不陌生。再次重申，这个黄金法则不只是一个点子，它还提供了一个新思路。</p><p>另外，当使用高阶函数实现容器时，还可以把它们连在一起–把一个高阶组件做为 props 传递给下一个。我就曾经把多个高阶组件连在一起构成了一个单个的容器。</p><p>2019 注意：React 社区似乎正在让高阶组件规范成设计模式。</p><p>我也如此建议。我的经验是，对于那些不理解 functional composition 的人来说写代码很容易引发 “wrapper 地狱”，组件嵌套太多层从而引发了严重的性能问题。</p><p>这里是一些相关文章：<a href="https://youtu.be/dpw9EHDh2bM?t=710" rel="nofollow noopener">Hooks talk</a> (2018)， <a href="https://youtu.be/zD_judE-bXk?t=1101" rel="nofollow noopener">Recompose talk</a> (2016)， <a href="https://cdb.reacttraining.com/use-a-render-prop-50de598f11ce" rel="nofollow noopener">Use a Render Prop!</a> (2017)，<a href="https://blog.kentcdodds.com/when-to-not-use-render-props-5397bbeff746" rel="nofollow noopener">When to NOT use Render Props</a> (2018)</p><h2 id="--5">说好的钩子来了</h2><h3 id="--6">使用钩子实现容器</h3><p>为什么在这里会讨论钩子呢？因为使用钩子实现容器真的很简单呀。</p><p>如果你对 React 钩子陌生，建议你看一看 <a href="https://youtu.be/dpw9EHDh2bM" rel="nofollow noopener">Dan Abramov 和 Ryan Florence 在 2018 React Conf 上的谈话</a>。</p><p>要点是，钩子是 React 团队为了解决<a href="https://reactjs.org/docs/higher-order-components.html" rel="nofollow noopener">高阶组件</a>和<a href="https://reactjs.org/docs/render-props.html" rel="nofollow noopener">类似模式</a>的问题引入的。React 想要在类似的场景用钩子替代它们。</p><p>这意味着容器既可以用函数组件实现也可以用钩子来实现。</p><p>在下面的例子里，我们使用 <code>useRoute</code> 和 <code>useRedux</code> 钩子来代表"外部世界"，使用工具类 <code>getvalue</code> 把外部世界映射为优雅组件的 <code>props</code>。我们还使用了 <code>transformValues</code> 来将组件转换为外部世界的 <code>dispatch</code> 。</p><pre><code class="language-javascript">import React from 'react';
import PropTypes from 'prop-types';

import { useRouter } from 'react-router';
import { useRedux } from 'react-redux';

import actionCreator from 'your-redux-stuff';

import getValues from './helpers/getVaules';
import transformValues from './helpers/transformValues';

import FooComponent from './FooComponent';

export default function FooComponentContainer(props) {
  // hooks
  const { match } = useRouter({ path: /* ... */ });
  // NOTE: `useRedux` does not exist yet and probably won't look like this
  const { state, dispatch } = useRedux();

  // mapping
  const props = getValues(state, match);
  
  function handleChange(e) {
    const transformed = transformValues(e);
    dispatch(actionCreator(transformed));
  }
  
  // natural component
  return &lt;FooComponent {...props} onChange={handleChange} /&gt;;
}

FooComponentContainer.propTypes = { /* ... */ };

</code></pre><p>下面是对应的目录结构：</p><pre><code>/FooComponent ----------- the whole component for others to import
  -FooComponent.js ------ the "natural" part of the component
  -FooComponent.test.js
  -index.js ------------- the "container" that bridges the gap
  -index.js.test.js         and provides dependencies
  /helpers -------------- isolated helpers that you can test easily
    -getValues.js
    -getValues.test.js
    -transformValues.js
    -transformValues.test.js

</code></pre><h2 id="--7">在容器里触发事件</h2><p>最后一类导致组件难以复用的情况，是在 props 改变、组件 mounting 的时候触发事件。</p><p>比如，以仪表盘为例。设计团队给了你原型图，需要你把它们转换成 React 组件。现在面临的问题是如何用数据填充仪表盘。</p><p>可能你已经意识到了可以在组件 mount 时调用函数（比如：<code>dispatch(fetchAction)</code>） 来触发事件。</p><p>在类似的这种场景中，普遍做法是添加 <code>componentDidMount</code> 和 <code>compoentDidUpdate</code> 生命周期方法，以及 <code>onMount</code> 和 <code>onDashboardIdChanged</code> props，因为我需要触发外部事件，才能建立组件和外部世界之间的连接。</p><p>根据黄金法则，这些 <code>onMount</code> 和 <code>onDashboardIdChanged</code> props 很不优雅的，应该把它们放在容器里。</p><p>钩子厉害之处是它能让 <code>onMount</code> 或者 props 改变时的 dispatch 事件变得更容易实现！</p><h3 id="-mount-">在 mount 里触发事件</h3><p>传入空数组调用 <code>useEffect</code> 来触发 mount 时的 event。</p><pre><code class="language-javascript">import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useRedux } from 'react-redux';

import fetchSomething_reduxAction from 'your-redux-stuff';
import getValues from './helpers/getVaules';
import FooComponent from './FooComponent';

export default function FooComponentContainer(props) {
  // hooks
  // NOTE: `useRedux` does not exist yet and probably won't look like this
  const { state, dispatch } = useRedux();
  
  // dispatch action onMount
  useEffect(() =&gt; {
    dispatch(fetchSomething_reduxAction);
  }, []); // the empty array tells react to only fire on mount
  // https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

  // mapping
  const props = getValues(state, match);
  
  // natural component
  return &lt;FooComponent {...props} /&gt;;
}

FooComponentContainer.propTypes = { /* ... */ };

</code></pre><p><em>组件 mount 时通过钩子在容器里触发事件</em></p><h3 id="-prop-">在 prop 改变时触发事件</h3><p><code>useEffect</code> 可以在重新渲染和函数调用时，监视 property 的改变。</p><p>在用 <code>useEffect</code> 前我发现我自己添加了冗余的生命周期函数方法和 <code>onPropertyChanged</code> 属性，因为我不知如何在组件外面扩展属性。</p><pre><code class="language-javascript">import React from 'react';
import PropTypes from 'prop-types';

/**
 * Before `useEffect`, I found myself adding "unnatural" props
 * to my components that only fired events when the props diffed.
 *
 * I'd find that the component's `render` didn't even use `id`
 * most of the time
 */
export default class BeforeUseEffect extends React.Component {
  static propTypes = {
    id: PropTypes.string.isRequired,
    onIdChange: PropTypes.func.isRequired,
  };

  componentDidMount() {
    this.props.onIdChange(this.props.id);
  }

  componentDidUpdate(prevProps) {
    if (prevProps.id !== this.props.id) {
      this.props.onIdChange(this.props.id);
    }
  }

  render() {
    return // ...
  }
}

</code></pre><p><em>老方法：当 props 改变的时候触发事件</em></p><p>有了 <code>useEffect</code> 更轻量级的方法来改变 prop ，组件也不必添加多余的 props 了。</p><pre><code class="language-javascript">mport React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useRedux } from 'react-redux';

import fetchSomething_reduxAction from 'your-redux-stuff';
import getValues from './helpers/getVaules';
import FooComponent from './FooComponent';

export default function FooComponentContainer({ id }) {
  // hooks
  // NOTE: `useRedux` does not exist yet and probably won't look like this
  const { state, dispatch } = useRedux();
  
  // dispatch action onMount
  useEffect(() =&gt; {
    dispatch(fetchSomething_reduxAction);
  }, [id]); // `useEffect` will watch this `id` prop and fire the effect when it differs
  // https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

  // mapping
  const props = getValues(state, match);
  
  // natural component
  return &lt;FooComponent {...props} /&gt;;
}

FooComponentContainer.propTypes = {
  id: PropTypes.string.isRequired,
};

</code></pre><p><em>现在的方法：当 props 改变的时候使用 <code>useEffect</code> 来触发事件</em></p><p>免责声明：在 <code>useEffect</code> 调用前会对比容器里 prop 的异同，还可以使用其它方式，比如高阶组件（比如 <a href="https://github.com/acdlite/recompose/blob/3db12ce7121a050b533476958ff3d66ded1c4bb8/docs/API.md#lifecycle" rel="nofollow noopener">recompose 的生命周期</a> ），或者像<a href="https://github.com/ReactTraining/react-router/blob/89a72d58ac55b2d8640c25e86d1f1496e4ba8d6c/packages/react-router/modules/Lifecycle.js" rel="nofollow noopener"> react router 那样在内部</a> 创建一个生命周期组件，但是这些方法要么就是很麻烦要么就是很难理解。</p><h2 id="--8">好处是什么</h2><h3 id="--9">组件保持有趣</h3><p>对于我来说，创建组件是前端开发中很有趣的部分。能把团队的想法实现感觉很棒，这种感觉值得我们分享。</p><p>在也不要让外部世界把组件 API 搞砸了。组件应该和想象中一样没有额外的 props—这也是我从黄金法则里所学到。</p><h3 id="--10">更多的机会测试和复用</h3><p>当你采用一个像这样的模式时，引入了一个新的 data-y 层。在这个层里你可以按需的把数据转换成组件需要的形式。</p><p>不管你在不在乎，这个层已经在你的应用里存在了，但是这也可能会加重代码的逻辑。我的经验是当我关注到这一层时，我可以做大量的代码优化，可以复用大量的逻辑，现在当我知道组件之间有共性时我是不会重造轮子的。</p><p>我觉得这点在<a href="https://reactjs.org/docs/hooks-custom.html" rel="nofollow noopener">定制钩子</a>上尤为明显。定制钩子给我们一个更简单的方式来抽出逻辑、监测外部的变化—更多时候靠 helper 函数是无法做到的。</p><h3 id="--11">最大化团队的输出</h3><p>在团队协作里，你可以把组件和容器分开。如果事前沟通好 API，你可以同时开启如下工作：</p><ol><li>Web API (如 后端)</li><li>从 Web API 里获取数据（或者其它途径）然后转换数据以符合组件的 API</li><li>组件</li></ol><h2 id="--12">有没有例外？</h2><p>就像真正的黄金法则一样，这条黄金法则也有例外。有某些的场景下，在组件里编写冗余的 API 以减少复杂性很有必要。</p><p>一个简单的例子就是 props 的命名。如果不在优雅的组件下面重新命名 data key 会让事情变得更复杂。</p><p>迷信金科玉律可能会更规范，但是同时也封杀了创造力。</p><h2 id="--13">分隔线</h2><p>不管怎样，黄金法则只是简单的以一个新的角度重申了表现组件和容器组件。总之，在编码前增加基本的组件需求评估，会更容易写出优雅的代码。</p><p>感谢阅读。</p><p>原文：<a href="https://www.freecodecamp.org/news/how-the-golden-rule-of-react-components-can-help-you-write-better-code-127046b478eb/">How the “Golden Rule” of React components can help you write better code</a>，作者：<a href="https://www.freecodecamp.org/news/author/rico/">Rico Kahler</a></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 前端开发教程之用 CSS 美化按钮 ]]>
                </title>
                <description>
                    <![CDATA[ 按钮是前端开发里的一个常见元素，在给按钮添加样式时有一些窍门，我收集了美化按钮的一些方法，当然你可以按需组合使用。首先安利一个创建渐变的小工具 [https://uigradients.com]。 一个简单的 “Get Started” 按钮 .btn {   background: #eb94d0;   /* 创建渐变 */   background-image: -webkit-linear-gradient(top, #eb94d0, #2079b0);   background-image: -moz-linear-gradient(top, #eb94d0, #2079b0);   background-image: -ms-linear-gradient(top, #eb94d0, #2079b0);   background-image: -o-linear-gradient(top, #eb94d0, #2079b0);   background-image: linear-gradient(to bottom, #eb94d0, #2079b0);   ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/a-quick-guide-to-styling-buttons-using-css/</link>
                <guid isPermaLink="false">5e761bb3ca1efa04e196c042</guid>
                
                    <category>
                        <![CDATA[ CSS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ ZhichengChen ]]>
                </dc:creator>
                <pubDate>Mon, 24 Jan 2022 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/03/1_ILGYxH64agmcHBWHuF1FSA.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>按钮是前端开发里的一个常见元素，在给按钮添加样式时有一些窍门，我收集了美化按钮的一些方法，当然你可以按需组合使用。首先安利一个创建渐变的<a href="https://uigradients.com">小工具</a>。</p><h2 id="-get-started-"><strong>一个简单的 “Get Started” 按钮</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/1-1.gif" class="kg-image" alt="1-1" width="600" height="400" loading="lazy"></figure><pre><code>.btn {
  background: #eb94d0;
  /* 创建渐变 */
  background-image: -webkit-linear-gradient(top, #eb94d0, #2079b0);
  background-image: -moz-linear-gradient(top, #eb94d0, #2079b0);
  background-image: -ms-linear-gradient(top, #eb94d0, #2079b0);
  background-image: -o-linear-gradient(top, #eb94d0, #2079b0);
  background-image: linear-gradient(to bottom, #eb94d0, #2079b0);
  /* 给按钮添加圆角 */
  -webkit-border-radius: 28;
  -moz-border-radius: 28;
  border-radius: 28px;
  text-shadow: 3px 2px 1px #9daef5;
  -webkit-box-shadow: 6px 5px 24px #666666;
  -moz-box-shadow: 6px 5px 24px #666666;
  box-shadow: 6px 5px 24px #666666;
  font-family: Arial;
  color: #fafafa;
  font-size: 27px;
  padding: 19px;
  text-decoration: none;
}
/* 悬停样式 */
.btn:hover {
  background: #2079b0;
  background-image: -webkit-linear-gradient(top, #2079b0, #eb94d0);
  background-image: -moz-linear-gradient(top, #2079b0, #eb94d0);
  background-image: -ms-linear-gradient(top, #2079b0, #eb94d0);
  background-image: -o-linear-gradient(top, #2079b0, #eb94d0);
  background-image: linear-gradient(to bottom, #2079b0, #eb94d0);
  text-decoration: none;
}</code></pre><h2 id="-"><strong>透明背景色</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/2-1.gif" class="kg-image" alt="2-1" width="600" height="400" loading="lazy"></figure><pre><code>.btn {
      /* 文字颜色 */
      color: #0099CC; 
      /* 清除背景色 */
      background: transparent; 
      /* 边框样式、颜色、宽度 */
      border: 2px solid #0099CC;
      /* 给边框添加圆角 */
      border-radius: 6px; 
      /* 字母转大写 */
      border: none;
      color: white;
      padding: 16px 32px;
      text-align: center;
      display: inline-block;
      font-size: 16px;
      margin: 4px 2px;
      -webkit-transition-duration: 0.4s; /* Safari */
      transition-duration: 0.4s;
      cursor: pointer;
      text-decoration: none;
      text-transform: uppercase;
}
.btn1 {
      background-color: white; 
      color: black; 
      border: 2px solid #008CBA;
}
/* 悬停样式 */
.btn1:hover {
      background-color: #008CBA;
      color: white;
}</code></pre><h2 id="-css-entities"><strong>应用 CSS Entities</strong></h2><p><a href="https://www.w3schools.com/cssref/css_entities.asp">这里</a>是 CSS entities 的详细介绍。</p><p>也可以使用 <a href="https://www.w3schools.com/html/html_entities.asp">HTML entities</a>，但是只支持部分功能。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/1.gif" class="kg-image" alt="1" width="600" height="400" loading="lazy"></figure><pre><code>.button {
  display: inline-block;
  border-radius: 4px;
  background-color: #f4511e;
  border: none;
  color: #FFFFFF;
  text-align: center;
  font-size: 28px;
  padding: 20px;
  width: 200px;
  transition: all 0.5s;
  cursor: pointer;
  margin: 5px;
}
.button span {
  cursor: pointer;
  display: inline-block;
  position: relative;
  transition: 0.5s;
}
.button span:after {
content: '\00bb';  /* CSS Entities. 如果用的是 HTML Entities, 请改成 &amp;#8594;*/
position: absolute;
  opacity: 0;
  top: 0;
  right: -20px;
  transition: 0.5s;
}
.button:hover span {
  padding-right: 25px;
}
.button:hover span:after {
  opacity: 1;
  right: 0;
}</code></pre><h2 id="--1"><strong>添加点击动画</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/02/2.gif" class="kg-image" alt="2" width="600" height="400" loading="lazy"></figure><p>CSS: (SCSS)</p><pre><code>$gray: #bbbbbb;
* {
  font-family: 'Roboto', sans-serif;
}
.container {
  position: absolute;
  top:50%;
  left:50%;
  margin-left: -65px;
  margin-top: -20px;
  width: 130px;
  height: 40px;
  text-align: center;
}
.btn {
      color: #0099CC; /* 文字颜色 */
      background: transparent; /* 清除背景色 */
      border: 2px solid #0099CC; /* 边框样式、颜色、宽度 */
      border-radius: 70px; /* 给边框添加圆角 */
      text-decoration: none;
      text-transform: uppercase; /* 字母转大写 */
      border: none;
      color: white;
      padding: 16px 32px;
      text-align: center;
      text-decoration: none;
      display: inline-block;
      font-size: 16px;
      margin: 4px 2px;
      -webkit-transition-duration: 0.4s; /* 兼容 Safari */
      transition-duration: 0.4s;
      cursor: pointer;
}
.btn1 {
      background-color: white; 
      color: black; 
      border: 2px solid #008CBA;
}
 .btn1:hover {
      background-color: #008CBA;
      color: white;
 }
b {
  outline:none;
  height: 40px;
  text-align: center;
  width: 130px;
  border-radius:100px;
  background: #fff;
  border: 2px solid #008CBA;
  color: #008CBA;
  letter-spacing:1px;
  text-shadow:0;
  font:{
    size:12px;
    weight:bold;
  }
  cursor: pointer;
  transition: all 0.25s ease;
&amp;:active {
    letter-spacing: 2px ;
  }
  &amp;:after {
    content:"";
  }
}
.onclic {
  width: 10px !important;
  height: 70px !important;
  border-radius: 50% !important;
  border-color:$gray;
  border-width:4px;
  font-size:0;
  border-left-color: #008CBA;
  animation: rotating 2s 0.25s linear infinite;
  &amp;:hover {
    color: dodgerblue;
    background: white;
  }
}
.validate {
  content:"";
  font-size:16px;
  color: black;
  background: dodgerblue;
  border-radius: 50px;
  &amp;:after {
    font-family:'FontAwesome';
    content:" done \f00c";
  }
}
@keyframes rotating {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}</code></pre><p>Javascript: (JQuery)</p><pre><code>$(function() {
  $("#button").click(function() {
    $("#button").addClass("onclic", 250, validate);
  });
function validate() {
    setTimeout(function() {
      $("#button").removeClass("onclic");
      $("#button").addClass("validate", 450, callback);
    }, 2250);
  }
  function callback() {
    setTimeout(function() {
      $("#button").removeClass("validate");
    }, 1250);
  }
});</code></pre><h2 id="--2"><strong>图片按钮</strong></h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/5-1.gif" class="kg-image" alt="5-1" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==" class="kg-image" alt="gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==" width="600" height="400" loading="lazy"></figure><pre><code>.btn {
 background: #92c7eb;
 background-image: url(“http://res.freestockphotos.biz/pictures/15/15107-illustration-of-a-red-close-button-pv.png");
 background-size: cover;
 background-position: center;
 display: inline-block;
 border: none;
 padding: 20px;
 width: 70px;
 border-radius: 900px;
 height: 70px;
 transition: all 0.5s;
 cursor: pointer;
}
.btn:hover
{
 width: 75px;
 height: 75px;
}
</code></pre><h2 id="icons-">Icons 按钮</h2><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/03/6-1.gif" class="kg-image" alt="6-1" width="600" height="400" loading="lazy"></figure><p>index.html:</p><pre><code>&lt;div class="main"&gt;&lt;button class="button" style="vertical-align:middle"&gt;&lt;a href="#" class="icon-button twitter"&gt;&lt;i class="icon-twitter"&gt;&lt;/i&gt;&lt;span&gt;&lt;/span&gt;&lt;/button&gt;&lt;/a&gt;
  &lt;div class="text"&gt;&lt;strong&gt;TWEET!&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;</code></pre><p>Style.css:</p><pre><code>button{
  border: none;
  border-radius: 50px;
}
html,
body {
  font-size: 20px;
  min-height: 100%;
  overflow: hidden;
  font-family: "Helvetica Neue", Helvetica, sans-serif;
  text-align: center;
}
.text {
  padding-top: 50px;
  font-family: "Helvetica Neue", Helvetica, 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
}
.text:hover
{
  cursor: pointer;
  color: #1565C0;
}
.main {
  padding: 0px 0px 0px 0px;
  margin: 0;
  background-image: url("https://someimg");
  text-align: center;
  background-size: 100% !important;
  background-repeat: no-repeat;
  width: 900px;
  height: 700px;  
}
.icon-button {
  background-color: white;
  border-radius: 3.6rem;
  cursor: pointer;
  display: inline-block;
  font-size: 2rem;
  height: 3.6rem;
  line-height: 3.6rem;
  margin: 0 5px;
  position: relative;
  text-align: center;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  width: 3.6rem;
}
.icon-button span {
  border-radius: 0;
  display: block;
  height: 0;
  left: 50%;
  margin: 0;
  position: absolute;
  top: 50%;
  -webkit-transition: all 0.3s;
  -moz-transition: all 0.3s;
  -o-transition: all 0.3s;
  transition: all 0.3s;
  width: 0;
}
.icon-button:hover span {
  width: 3.6rem;
  height: 3.6rem;
  border-radius: 3.6rem;
  margin: -1.8rem;
}
.twitter span {
  background-color: #4099ff;
}
/* Icons */
.icon-button i {
  background: none;
  color: white;
  height: 3.6rem;
  left: 0;
  line-height: 3.6rem;
  position: absolute;
  top: 0;
  -webkit-transition: all 0.3s;
  -moz-transition: all 0.3s;
  -o-transition: all 0.3s;
  transition: all 0.3s;
  width: 3.6rem;
  z-index: 10;
}
.icon-button .icon-twitter {
  color: #4099ff;
}
.icon-button:hover .icon-twitter {
  color: white;
}</code></pre><h2 id="--3">总结</h2><p>在这个教程里，我们学习了通过 CSS 和一点 JavaScript（如果需要点击后的效果）来美化 CSS 按钮。也可以使用 CSS3ButtonGenerator 来生成一个简单的按钮。有问题欢迎留言。</p><p>原文链接：<a href="https://www.freecodecamp.org/news/a-quick-guide-to-styling-buttons-using-css-f64d4f96337f/">A quick guide to styling buttons using CSS</a>，作者：Ashwini Sheshagiri</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 前端监控 SDK 的技术要点原理分析 ]]>
                </title>
                <description>
                    <![CDATA[ 一个完整的前端监控平台包括三个部分：数据采集与上报、数据整理和存储、数据展示。 本文要讲的就是其中的第一个环节——数据采集与上报。下图是本文要讲述内容的大纲，大家可以先大致了解一下： 仅看理论知识是比较难以理解的，为此我结合本文要讲的技术要点写了一个简单的监控 SDK [https://github.com/woai3c/monitor-demo]，可以用它来写一些简单的 DEMO，帮助加深理解。再结合本文一起阅读，效果更好。 性能数据采集 chrome 开发团队提出了一系列用于检测网页性能的指标：  * FP(first-paint)，从页面加载开始到第一个像素绘制到屏幕上的时间  * FCP(first-contentful-paint)，从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间  * LCP(largest-contentful-paint)，从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间  * CLS(layout-shift)，从页面加载开始和其生命周期状态    [https://developers.google.com/web/upd ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/tech-analysis-of-front-end-monitoring-sdk/</link>
                <guid isPermaLink="false">6165069621a1350622df539c</guid>
                
                    <category>
                        <![CDATA[ SDK ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Wed, 13 Oct 2021 09:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/safar-safarov-koOdUvfGr4c-unsplash-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>一个完整的前端监控平台包括三个部分：数据采集与上报、数据整理和存储、数据展示。</p><p>本文要讲的就是其中的第一个环节——数据采集与上报。下图是本文要讲述内容的大纲，大家可以先大致了解一下：</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/97c87a9eb80b4186a462614f206bb7ee~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3633ae631be548baa55fffa97936339e~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>仅看理论知识是比较难以理解的，为此我结合本文要讲的技术要点写了一个简单的<a href="https://github.com/woai3c/monitor-demo">监控 SDK</a>，可以用它来写一些简单的 DEMO，帮助加深理解。再结合本文一起阅读，效果更好。</p><h2 id="-">性能数据采集</h2><p>chrome 开发团队提出了一系列用于检测网页性能的指标：</p><ul><li>FP(first-paint)，从页面加载开始到第一个像素绘制到屏幕上的时间</li><li>FCP(first-contentful-paint)，从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间</li><li>LCP(largest-contentful-paint)，从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间</li><li>CLS(layout-shift)，从页面加载开始和其<a href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api">生命周期状态</a>变为隐藏期间发生的所有意外布局偏移的累积分数</li></ul><p>这四个性能指标都需要通过 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver">PerformanceObserver</a> 来获取（也可以通过 <code>performance.getEntriesByName()</code> 获取，但它不是在事件触发时通知的）。PerformanceObserver 是一个性能监测对象，用于监测性能度量事件。</p><h3 id="fp">FP</h3><p>FP(first-paint)，从页面加载开始到第一个像素绘制到屏幕上的时间。其实把 FP 理解成白屏时间也是没问题的。</p><p>测量代码如下：</p><pre><code class="language-js">const entryHandler = (list) =&gt; {        
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-paint') {
            observer.disconnect()
        }

       console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
// buffered 属性表示是否观察缓存数据，也就是说观察代码添加时机比事情触发时机晚也没关系。
observer.observe({ type: 'paint', buffered: true })</code></pre><p>通过以上代码可以得到 FP 的内容:</p><pre><code class="language-js">{
    duration:&nbsp;0,
    entryType:&nbsp;"paint",
    name:&nbsp;"first-paint",
    startTime:&nbsp;359, // fp 时间
}</code></pre><p>其中 <code>startTime</code> 就是我们要的绘制时间。</p><h3 id="fcp">FCP</h3><p>FCP(first-contentful-paint)，从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间。对于该指标，"内容"指的是文本、图像（包括背景图像）、<code>&lt;svg&gt;</code>元素或非白色的<code>&lt;canvas&gt;</code>元素。</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a4f1c9b61029448dae2b1cfb57b4ef75~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>为了提供良好的用户体验，FCP 的分数应该控制在 1.8 秒以内。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9818c66879b345e3b4845ff3fe01e8c9~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>测量代码：</p><pre><code class="language-js">const entryHandler = (list) =&gt; {        
    for (const entry of list.getEntries()) {
        if (entry.name === 'first-contentful-paint') {
            observer.disconnect()
        }
        
        console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'paint', buffered: true })</code></pre><p>通过以上代码可以得到 FCP 的内容:</p><pre><code class="language-js">{
    duration:&nbsp;0,
    entryType:&nbsp;"paint",
    name:&nbsp;"first-contentful-paint",
    startTime:&nbsp;459, // fcp 时间
}</code></pre><p>其中 <code>startTime</code> 就是我们要的绘制时间。</p><h3 id="lcp">LCP</h3><p>LCP(largest-contentful-paint)，从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间。LCP 指标会根据页面<a href="https://web.dev/lcp/#what-elements-are-considered">首次开始加载</a>的时间点来报告可视区域内可见的最大<a href="https://web.dev/lcp/#what-elements-are-considered">图像或文本块</a>完成渲染的相对时间。</p><p>一个良好的 LCP 分数应该控制在 2.5 秒以内。</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c090dd8b042c46d2adaba5395ca68f47~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>测量代码：</p><pre><code class="language-js">const entryHandler = (list) =&gt; {
    if (observer) {
        observer.disconnect()
    }

    for (const entry of list.getEntries()) {
        console.log(entry)
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'largest-contentful-paint', buffered: true })</code></pre><p>通过以上代码可以得到 LCP 的内容:</p><pre><code class="language-js">{
    duration: 0,
    element: p,
    entryType: "largest-contentful-paint",
    id: "",
    loadTime: 0,
    name: "",
    renderTime: 1021.299,
    size: 37932,
    startTime: 1021.299,
    url: "",
}</code></pre><p>其中 <code>startTime</code> 就是我们要的绘制时间。element 是指 LCP 绘制的 DOM 元素。</p><p>FCP 和 LCP 的区别是：FCP 只要任意内容绘制完成就触发，LCP 是最大内容渲染完成时触发。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e64637ac9d243a58101d8ed01fe886e~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>LCP 考察的元素类型为：</p><ul><li><code>&lt;img&gt;</code>元素</li><li>内嵌在<code>&lt;svg&gt;</code>元素内的<code>&lt;image&gt;</code>元素</li><li><code>&lt;video&gt;</code>元素（使用封面图像）</li><li>通过<a href="https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fdocs%2FWeb%2FCSS%2Furl()" rel="nofollow noopener noreferrer"><code>url()</code></a>函数（而非使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Images/Using_CSS_gradients">CSS 渐变</a>）加载的带有背景图像的元素</li><li>包含文本节点或其他行内级文本元素子元素的<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Block-level_elements">块级元素</a>。</li></ul><h3 id="cls">CLS</h3><p>CLS(layout-shift)，从页面加载开始和其<a href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api">生命周期状态</a>变为隐藏期间发生的所有意外布局偏移的累积分数。</p><p>布局偏移分数的计算方式如下：</p><pre><code>布局偏移分数 = 影响分数 * 距离分数</code></pre><p><a href="https://github.com/WICG/layout-instability#Impact-Fraction">影响分数</a>测量<em>不稳定元素</em>对两帧之间的可视区域产生的影响。</p><p><em>距离分数</em>指的是任何<em>不稳定元素</em>在一帧中位移的最大距离（水平或垂直）除以可视区域的最大尺寸维度（宽度或高度，以较大者为准）。</p><p><strong>CLS 就是把所有布局偏移分数加起来的总和</strong>。</p><p>当一个 DOM 在两个渲染帧之间产生了位移，就会触发 CLS（如图所示）。</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ff07d41c624248a1b66c5761f0482f2c~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d0d5ab8100c9489a991dd0be8e198af0~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>上图中的矩形从左上角移动到了右边，这就算是一次布局偏移。同时，在 CLS 中，有一个叫<strong>会话窗口</strong>的术语：一个或多个快速连续发生的单次布局偏移，每次偏移相隔的时间少于 1 秒，且整个窗口的最大持续时长为 5 秒。</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c6af2ec569644013962645820efb16d3~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>例如上图中的第二个会话窗口，它里面有四次布局偏移，每一次偏移之间的间隔必须少于 1 秒，并且第一个偏移和最后一个偏移之间的时间不能超过 5 秒，这样才能算是一次会话窗口。如果不符合这个条件，就算是一个新的会话窗口。可能有人会问，为什么要这样规定？其实这是 chrome 团队根据大量的实验和研究得出的分析结果 <a href="https://web.dev/evolving-cls/">Evolving the CLS metric</a>。</p><p>CLS 一共有三种计算方式：</p><ol><li>累加</li><li>取所有会话窗口的平均数</li><li>取所有会话窗口中的最大值</li></ol><h4 id="--1">累加</h4><p>也就是把从页面加载开始的所有布局偏移分数加在一起。但是这种计算方式对生命周期长的页面不友好，页面存留时间越长，CLS 分数越高。</p><h4 id="--2">取所有会话窗口的平均数</h4><p>这种计算方式不是按单个布局偏移为单位，而是以会话窗口为单位。将所有会话窗口的值相加再取平均值。但是这种计算方式也有缺点。</p><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/42e5208d83f349db84cf4a27194a57f2~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>从上图可以看出来，第一个会话窗口产生了比较大的 CLS 分数，第二个会话窗口产生了比较小的 CLS 分数。如果取它们的平均值来当做 CLS 分数，则根本看不出来页面的运行状况。原来页面是早期偏移多，后期偏移少，现在的平均值无法反映出这种情况。</p><h4 id="--3">取所有会话窗口中的最大值</h4><p>这种方式是目前最优的计算方式，每次只取所有会话窗口的最大值，用来反映页面布局偏移的最差情况。详情请看 <a href="https://web.dev/evolving-cls/">Evolving the CLS metric</a>。</p><p>下面是第三种计算方式的测量代码：</p><pre><code class="language-js">let sessionValue = 0
let sessionEntries = []
const cls = {
    subType: 'layout-shift',
    name: 'layout-shift',
    type: 'performance',
    pageURL: getPageURL(),
    value: 0,
}

const entryHandler = (list) =&gt; {
    for (const entry of list.getEntries()) {
        // Only count layout shifts without recent user input.
        if (!entry.hadRecentInput) {
            const firstSessionEntry = sessionEntries[0]
            const lastSessionEntry = sessionEntries[sessionEntries.length - 1]

            // If the entry occurred less than 1 second after the previous entry and
            // less than 5 seconds after the first entry in the session, include the
            // entry in the current session. Otherwise, start a new session.
            if (
                sessionValue
                &amp;&amp; entry.startTime - lastSessionEntry.startTime &lt; 1000
                &amp;&amp; entry.startTime - firstSessionEntry.startTime &lt; 5000
            ) {
                sessionValue += entry.value
                sessionEntries.push(formatCLSEntry(entry))
            } else {
                sessionValue = entry.value
                sessionEntries = [formatCLSEntry(entry)]
            }

            // If the current session value is larger than the current CLS value,
            // update CLS and the entries contributing to it.
            if (sessionValue &gt; cls.value) {
                cls.value = sessionValue
                cls.entries = sessionEntries
                cls.startTime = performance.now()
                lazyReportCache(deepCopy(cls))
            }
        }
    }
}

const observer = new PerformanceObserver(entryHandler)
observer.observe({ type: 'layout-shift', buffered: true })</code></pre><p>在看完上面的文字描述后，再看代码就好理解了。一次布局偏移的测量内容如下：</p><pre><code class="language-js">{
  duration: 0,
  entryType: "layout-shift",
  hadRecentInput: false,
  lastInputTime: 0,
  name: "",
  sources: (2) [LayoutShiftAttribution, LayoutShiftAttribution],
  startTime: 1176.199999999255,
  value: 0.000005752046026677329,
}</code></pre><p>代码中的 <code>value</code> 字段就是布局偏移分数。</p><h3 id="domcontentloaded-load-">DOMContentLoaded、load 事件</h3><p>当纯 HTML 被完全加载以及解析时，<code>DOMContentLoaded</code> 事件会被触发，不用等待 css、img、iframe 加载完。</p><p>当整个页面及所有依赖资源如样式表和图片都已完成加载时，将触发 <code>load</code> 事件。</p><p>虽然这两个性能指标比较旧了，但是它们仍然能反映页面的一些情况。对于它们进行监听仍然是必要的。</p><pre><code class="language-js">import { lazyReportCache } from '../utils/report'

['load', 'DOMContentLoaded'].forEach(type =&gt; onEvent(type))

function onEvent(type) {
    function callback() {
        lazyReportCache({
            type: 'performance',
            subType: type.toLocaleLowerCase(),
            startTime: performance.now(),
        })

        window.removeEventListener(type, callback, true)
    }

    window.addEventListener(type, callback, true)
}</code></pre><h3 id="--4">首屏渲染时间</h3><p>大多数情况下，首屏渲染时间可以通过 <code>load</code> 事件获取。除了一些特殊情况，例如异步加载的图片和 DOM。</p><pre><code class="language-html">&lt;script&gt;
    setTimeout(() =&gt; {
        document.body.innerHTML = `
            &lt;div&gt;
                &lt;!-- 省略一堆代码... --&gt;
            &lt;/div&gt;
        `
    }, 3000)
&lt;/script&gt;</code></pre><p>像这种情况就无法通过 <code>load</code> 事件获取首屏渲染时间了。这时我们需要通过 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver/MutationObserver">MutationObserver</a> 来获取首屏渲染时间。MutationObserver 在监听的 DOM 元素属性发生变化时会触发事件。</p><p>首屏渲染时间计算过程：</p><ol><li>利用 MutationObserver 监听 document 对象，每当 DOM 元素属性发生变更时，触发事件。</li><li>判断该 DOM 元素是否在首屏内，如果在，则在 <code>requestAnimationFrame()</code> 回调函数中调用 <code>performance.now()</code> 获取当前时间，作为它的绘制时间。</li><li>将最后一个 DOM 元素的绘制时间和首屏中所有加载的图片时间作对比，将最大值作为首屏渲染时间。</li></ol><h4 id="-dom">监听 DOM</h4><pre><code class="language-js">const next = window.requestAnimationFrame ? requestAnimationFrame : setTimeout
const ignoreDOMList = ['STYLE', 'SCRIPT', 'LINK']
    
observer = new MutationObserver(mutationList =&gt; {
    const entry = {
        children: [],
    }

    for (const mutation of mutationList) {
        if (mutation.addedNodes.length &amp;&amp; isInScreen(mutation.target)) {
             // ...
        }
    }

    if (entry.children.length) {
        entries.push(entry)
        next(() =&gt; {
            entry.startTime = performance.now()
        })
    }
})

observer.observe(document, {
    childList: true,
    subtree: true,
})</code></pre><p>上面的代码就是监听 DOM 变化的代码，同时需要过滤掉 <code>style</code>、<code>script</code>、<code>link</code> 等标签。</p><h4 id="--5">判断是否在首屏</h4><p>一个页面的内容可能非常多，但用户最多只能看见一屏幕的内容。所以在统计首屏渲染时间的时候，需要限定范围，把渲染内容限定在当前屏幕内。</p><pre><code class="language-js">const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight

// dom 对象是否在屏幕内
function isInScreen(dom) {
    const rectInfo = dom.getBoundingClientRect()
    if (rectInfo.left &lt; viewportWidth &amp;&amp; rectInfo.top &lt; viewportHeight) {
        return true
    }

    return false
}</code></pre><h4 id="-requestanimationframe-dom-">使用 <code>requestAnimationFrame()</code> 获取 DOM 绘制时间</h4><p>当 DOM 变更触发 MutationObserver 事件时，只是代表 DOM 内容可以被读取到，并不代表该 DOM 被绘制到了屏幕上。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/67230c5e58ff4c699be7758656e4504f~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>从上图可以看出，当触发 MutationObserver 事件时，可以读取到 <code>document.body</code> 上已经有内容了，但实际上左边的屏幕并没有绘制任何内容。所以要调用 <code>requestAnimationFrame()</code> 在浏览器绘制成功后再获取当前时间作为 DOM 绘制时间。</p><h4 id="--6">和首屏内的所有图片加载时间作对比</h4><pre><code class="language-js">function getRenderTime() {
    let startTime = 0
    entries.forEach(entry =&gt; {
        if (entry.startTime &gt; startTime) {
            startTime = entry.startTime
        }
    })

    // 需要和当前页面所有加载图片的时间做对比，取最大值
    // 图片请求时间要小于 startTime，响应结束时间要大于 startTime
    performance.getEntriesByType('resource').forEach(item =&gt; {
        if (
            item.initiatorType === 'img'
            &amp;&amp; item.fetchStart &lt; startTime 
            &amp;&amp; item.responseEnd &gt; startTime
        ) {
            startTime = item.responseEnd
        }
    })
    
    return startTime
}</code></pre><h4 id="--7">优化</h4><p>现在的代码还没优化完，主要有两点注意事项：</p><ol><li>什么时候上报渲染时间？</li><li>如果兼容异步添加 DOM 的情况？</li></ol><p>第一点，必须要在 DOM 不再变化后再上报渲染时间，一般 load 事件触发后，DOM 就不再变化了。所以我们可以在这个时间点进行上报。</p><p>第二点，可以在 LCP 事件触发后再进行上报。不管是同步还是异步加载的 DOM，它都需要进行绘制，所以可以监听 LCP 事件，在该事件触发后才允许进行上报。</p><p>将以上两点方案结合在一起，就有了以下代码：</p><pre><code class="language-js">let isOnLoaded = false
executeAfterLoad(() =&gt; {
    isOnLoaded = true
})


let timer
let observer
function checkDOMChange() {
    clearTimeout(timer)
    timer = setTimeout(() =&gt; {
        // 等 load、lcp 事件触发后并且 DOM 树不再变化时，计算首屏渲染时间
        if (isOnLoaded &amp;&amp; isLCPDone()) {
            observer &amp;&amp; observer.disconnect()
            lazyReportCache({
                type: 'performance',
                subType: 'first-screen-paint',
                startTime: getRenderTime(),
                pageURL: getPageURL(),
            })

            entries = null
        } else {
            checkDOMChange()
        }
    }, 500)
}</code></pre><p><code>checkDOMChange()</code> 代码每次在触发 MutationObserver 事件时进行调用，需要用防抖函数进行处理。</p><h3 id="--8">接口请求耗时</h3><p>接口请求耗时需要对 XMLHttpRequest 和 fetch 进行监听。</p><p><strong>监听 XMLHttpRequest</strong></p><pre><code class="language-js">originalProto.open = function newOpen(...args) {
    this.url = args[1]
    this.method = args[0]
    originalOpen.apply(this, args)
}

originalProto.send = function newSend(...args) {
    this.startTime = Date.now()

    const onLoadend = () =&gt; {
        this.endTime = Date.now()
        this.duration = this.endTime - this.startTime

        const { status, duration, startTime, endTime, url, method } = this
        const reportData = {
            status,
            duration,
            startTime,
            endTime,
            url,
            method: (method || 'GET').toUpperCase(),
            success: status &gt;= 200 &amp;&amp; status &lt; 300,
            subType: 'xhr',
            type: 'performance',
        }

        lazyReportCache(reportData)

        this.removeEventListener('loadend', onLoadend, true)
    }

    this.addEventListener('loadend', onLoadend, true)
    originalSend.apply(this, args)
}</code></pre><p>如何判断 XML 请求是否成功？可以根据他的状态码是否在 200~299 之间。如果在，那就是成功，否则失败。</p><p><strong>监听 fetch</strong></p><pre><code class="language-js">const originalFetch = window.fetch

function overwriteFetch() {
    window.fetch = function newFetch(url, config) {
        const startTime = Date.now()
        const reportData = {
            startTime,
            url,
            method: (config?.method || 'GET').toUpperCase(),
            subType: 'fetch',
            type: 'performance',
        }

        return originalFetch(url, config)
        .then(res =&gt; {
            reportData.endTime = Date.now()
            reportData.duration = reportData.endTime - reportData.startTime

            const data = res.clone()
            reportData.status = data.status
            reportData.success = data.ok

            lazyReportCache(reportData)

            return res
        })
        .catch(err =&gt; {
            reportData.endTime = Date.now()
            reportData.duration = reportData.endTime - reportData.startTime
            reportData.status = 0
            reportData.success = false

            lazyReportCache(reportData)

            throw err
        })
    }
}</code></pre><p>对于 fetch，可以根据返回数据中的的 <code>ok</code> 字段判断请求是否成功，如果为 <code>true</code> 则请求成功，否则失败。</p><p><strong>注意</strong>，监听到的接口请求时间和 chrome devtool 上检测到的时间可能不一样。这是因为 chrome devtool 上检测到的是 HTTP 请求发送和接口整个过程的时间。但是 xhr 和 fetch 是异步请求，接口请求成功后需要调用回调函数。事件触发时会把回调函数放到消息队列，然后浏览器再处理，这中间也有一个等待过程。</p><h3 id="--9">资源加载时间、缓存命中率</h3><p>通过 <code>PerformanceObserver</code> 可以监听 <code>resource</code> 和 <code>navigation</code> 事件，如果浏览器不支持 <code>PerformanceObserver</code>，还可以通过 <code>performance.getEntriesByType(entryType)</code> 来进行降级处理。</p><p>当 <code>resource</code> 事件触发时，可以获取到对应的资源列表，每个资源对象包含以下一些字段：</p><figure class="kg-card kg-image-card"><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e6cb30ae9a4447bbe43bfcff6c6c4a1~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>从这些字段中我们可以提取到一些有用的信息：</p><pre><code class="language-js">{
    name: entry.name, // 资源名称
    subType: entryType,
    type: 'performance',
    sourceType: entry.initiatorType, // 资源类型
    duration: entry.duration, // 资源加载耗时
    dns: entry.domainLookupEnd - entry.domainLookupStart, // DNS 耗时
    tcp: entry.connectEnd - entry.connectStart, // 建立 tcp 连接耗时
    redirect: entry.redirectEnd - entry.redirectStart, // 重定向耗时
    ttfb: entry.responseStart, // 首字节时间
    protocol: entry.nextHopProtocol, // 请求协议
    responseBodySize: entry.encodedBodySize, // 响应内容大小
    responseHeaderSize: entry.transferSize - entry.encodedBodySize, // 响应头部大小
    resourceSize: entry.decodedBodySize, // 资源解压后的大小
    isCache: isCache(entry), // 是否命中缓存
    startTime: performance.now(),
}</code></pre><p><strong>判断该资源是否命中缓存</strong></p><p>在这些资源对象中有一个 &nbsp;<code>transferSize</code> 字段，它表示获取资源的大小，包括响应头字段和响应数据的大小。如果这个值为 0，说明是从缓存中直接读取的（强制缓存）。如果这个值不为 0，但是 <code>encodedBodySize</code> 字段为 0，说明它走的是协商缓存（<code>encodedBodySize</code> 表示请求响应数据 body 的大小）。</p><pre><code class="language-js">function isCache(entry) {
    // 直接从缓存读取或 304
    return entry.transferSize === 0 || (entry.transferSize !== 0 &amp;&amp; entry.encodedBodySize === 0)
}</code></pre><p>不符合以上条件的，说明未命中缓存。然后将<code>所有命中缓存的数据/总数据</code>就能得出缓存命中率。</p><h3 id="-bfc-back-forward-cache-">浏览器往返缓存 BFC（back/forward cache）</h3><p>bfcache 是一种内存缓存，它会将整个页面保存在内存中。当用户返回时可以马上看到整个页面，而不用再次刷新。据该文章 <a href="https://web.dev/bfcache/">bfcache</a> 介绍，firfox 和 safari 一直支持 bfc，chrome 只有在高版本的移动端浏览器支持。但我试了一下，只有 safari 浏览器支持，可能我的 firfox 版本不对。</p><p>但是 bfc 也是有缺点的，当用户返回并从 bfc 中恢复页面时，原来页面的代码不会再次执行。为此，浏览器提供了一个 <code>pageshow</code> 事件，可以把需要再次执行的代码放在里面。</p><pre><code class="language-js">window.addEventListener('pageshow', function(event) {
  // 如果该属性为 true，表示是从 bfc 中恢复的页面
  if (event.persisted) {
    console.log('This page was restored from the bfcache.');
  } else {
    console.log('This page was loaded normally.');
  }
});</code></pre><p>从 bfc 中恢复的页面，我们也需要收集他们的 FP、FCP、LCP 等各种时间。</p><pre><code class="language-js">onBFCacheRestore(event =&gt; {
    requestAnimationFrame(() =&gt; {
        ['first-paint', 'first-contentful-paint'].forEach(type =&gt; {
            lazyReportCache({
                startTime: performance.now() - event.timeStamp,
                name: type,
                subType: type,
                type: 'performance',
                pageURL: getPageURL(),
                bfc: true,
            })
        })
    })
})</code></pre><p>上面的代码很好理解，在 <code>pageshow</code> 事件触发后，用当前时间减去事件触发时间，这个时间差值就是性能指标的绘制时间。<strong>注意</strong>，从 bfc 中恢复的页面的这些性能指标，值一般都很小，一般在 10 ms 左右。所以要给它们加个标识字段 <code>bfc: true</code>。这样在做性能统计时可以对它们进行忽略。</p><h3 id="fps">FPS</h3><p>利用 <code>requestAnimationFrame()</code> 我们可以计算当前页面的 FPS。</p><pre><code class="language-js">const next = window.requestAnimationFrame 
    ? requestAnimationFrame : (callback) =&gt; { setTimeout(callback, 1000 / 60) }

const frames = []

export default function fps() {
    let frame = 0
    let lastSecond = Date.now()

    function calculateFPS() {
        frame++
        const now = Date.now()
        if (lastSecond + 1000 &lt;= now) {
            // 由于 now - lastSecond 的单位是毫秒，所以 frame 要 * 1000
            const fps = Math.round((frame * 1000) / (now - lastSecond))
            frames.push(fps)
                
            frame = 0
            lastSecond = now
        }
    
        // 避免上报太快，缓存一定数量再上报
        if (frames.length &gt;= 60) {
            report(deepCopy({
                frames,
                type: 'performace',
                subType: 'fps',
            }))
    
            frames.length = 0
        }

        next(calculateFPS)
    }

    calculateFPS()
}</code></pre><p>代码逻辑如下：</p><ol><li>先记录一个初始时间，然后每次触发 <code>requestAnimationFrame()</code> 时，就将帧数加 1。过去一秒后用<code>帧数/流逝的时间</code>就能得到当前帧率。</li></ol><p>当连续三个低于 20 的 FPS 出现时，我们可以断定页面出现了卡顿，详情请看 <a href="https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F39292837" rel="nofollow noopener noreferrer">如何监控网页的卡顿</a>。</p><pre><code class="language-js">export function isBlocking(fpsList, below = 20, last = 3) {
    let count = 0
    for (let i = 0; i &lt; fpsList.length; i++) {
        if (fpsList[i] &amp;&amp; fpsList[i] &lt; below) {
            count++
        } else {
            count = 0
        }

        if (count &gt;= last) {
            return true
        }
    }

    return false
}</code></pre><h3 id="vue-">Vue 路由变更渲染时间</h3><p>首屏渲染时间我们已经知道如何计算了，但是如何计算 SPA 应用的页面路由切换导致的页面渲染时间呢？本文用 Vue 作为示例，讲一下我的思路。</p><pre><code class="language-js">export default function onVueRouter(Vue, router) {
    let isFirst = true
    let startTime
    router.beforeEach((to, from, next) =&gt; {
        // 首次进入页面已经有其他统计的渲染时间可用
        if (isFirst) {
            isFirst = false
            return next()
        }

        // 给 router 新增一个字段，表示是否要计算渲染时间
        // 只有路由跳转才需要计算
        router.needCalculateRenderTime = true
        startTime = performance.now()

        next()
    })

    let timer
    Vue.mixin({
        mounted() {
            if (!router.needCalculateRenderTime) return

            this.$nextTick(() =&gt; {
                // 仅在整个视图都被渲染之后才会运行的代码
                const now = performance.now()
                clearTimeout(timer)

                timer = setTimeout(() =&gt; {
                    router.needCalculateRenderTime = false
                    lazyReportCache({
                        type: 'performance',
                        subType: 'vue-router-change-paint',
                        duration: now - startTime,
                        startTime: now,
                        pageURL: getPageURL(),
                    })
                }, 1000)
            })
        },
    })
}</code></pre><p>代码逻辑如下：</p><ol><li>监听路由钩子，在路由切换时会触发 <code>router.beforeEach()</code> 钩子，在该钩子的回调函数里将当前时间记为渲染开始时间。</li><li>利用 <code>Vue.mixin()</code> 对所有组件的 <code>mounted()</code> 注入一个函数。每个函数都执行一个防抖函数。</li><li>当最后一个组件的 <code>mounted()</code> 触发时，就代表该路由下的所有组件已经挂载完毕。可以在 <code>this.$nextTick()</code> 回调函数中获取渲染时间。</li></ol><p>同时，还要考虑到一个情况。不切换路由时，也会有变更组件的情况，这时不应该在这些组件的 <code>mounted()</code> 里进行渲染时间计算。所以需要添加一个 <code>needCalculateRenderTime</code> 字段，当切换路由时将它设为 true，代表可以计算渲染时间了。</p><h2 id="--10">错误数据采集</h2><h3 id="--11">资源加载错误</h3><p>使用 <code>addEventListener()</code> 监听 error 事件，可以捕获到资源加载失败错误。</p><pre><code class="language-js">// 捕获资源加载失败错误 js css img...
window.addEventListener('error', e =&gt; {
    const target = e.target
    if (!target) return

    if (target.src || target.href) {
        const url = target.src || target.href
        lazyReportCache({
            url,
            type: 'error',
            subType: 'resource',
            startTime: e.timeStamp,
            html: target.outerHTML,
            resourceType: target.tagName,
            paths: e.path.map(item =&gt; item.tagName).filter(Boolean),
            pageURL: getPageURL(),
        })
    }
}, true)</code></pre><h3 id="js-">js 错误</h3><p>使用 <code>window.onerror</code> 可以监听 js 错误。</p><pre><code class="language-js">// 监听 js 错误
window.onerror = (msg, url, line, column, error) =&gt; {
    lazyReportCache({
        msg,
        line,
        column,
        error: error.stack,
        subType: 'js',
        pageURL: url,
        type: 'error',
        startTime: performance.now(),
    })
}</code></pre><h3 id="promise-">promise 错误</h3><p>使用 <code>addEventListener()</code> 监听 unhandledrejection 事件，可以捕获到未处理的 promise 错误。</p><pre><code class="language-js">// 监听 promise 错误 缺点是获取不到列数据
window.addEventListener('unhandledrejection', e =&gt; {
    lazyReportCache({
        reason: e.reason?.stack,
        subType: 'promise',
        type: 'error',
        startTime: e.timeStamp,
        pageURL: getPageURL(),
    })
})</code></pre><h3 id="sourcemap">sourcemap</h3><p>一般生产环境的代码都是经过压缩的，并且生产环境不会把 sourcemap 文件上传。所以生产环境上的代码报错信息是很难读的。因此，我们可以利用 <a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fmozilla%2Fsource-map" rel="nofollow noopener noreferrer">source-map</a> 来对这些压缩过的代码报错信息进行还原。</p><p>当代码报错时，我们可以获取到对应的文件名、行数、列数:</p><pre><code class="language-js">{
    line: 1,
    column: 17,
    file: 'https:/www.xxx.com/bundlejs',
}</code></pre><p>然后调用下面的代码进行还原：</p><pre><code class="language-js">async function parse(error) {
    const mapObj = JSON.parse(getMapFileContent(error.url))
    const consumer = await new sourceMap.SourceMapConsumer(mapObj)
    // 将 webpack://source-map-demo/./src/index.js 文件中的 ./ 去掉
    const sources = mapObj.sources.map(item =&gt; format(item))
    // 根据压缩后的报错信息得出未压缩前的报错行列数和源码文件
    const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
    // sourcesContent 中包含了各个文件的未压缩前的源码，根据文件名找出对应的源码
    const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
    return {
        file: originalInfo.source,
        content: originalFileContent,
        line: originalInfo.line,
        column: originalInfo.column,
        msg: error.msg,
        error: error.error
    }
}

function format(item) {
    return item.replace(/(\.\/)*/g, '')
}

function getMapFileContent(url) {
    return fs.readFileSync(path.resolve(__dirname, `./maps/${url.split('/').pop()}.map`), 'utf-8')
}</code></pre><p>每次项目打包时，如果开启了 sourcemap，那么每一个 js 文件都会有一个对应的 map 文件。</p><pre><code>bundle.js
bundle.js.map</code></pre><p>这时 js 文件放在静态服务器上供用户访问，map 文件存储在服务器，用于还原错误信息。<code>source-map</code> 库可以根据压缩过的代码报错信息还原出未压缩前的代码报错信息。例如压缩后报错位置为 <code>1 行 47 列</code>，还原后真正的位置可能为 <code>4 行 10 列</code>。除了位置信息，还可以获取到源码原文。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b1c6b5eebb7b4ef59d4dd6ad613484eb~tplv-k3u1fbpfcp-watermark.awebp?" class="kg-image" alt="image.png" width="600" height="400" loading="lazy"></figure><p>上图就是一个代码报错还原后的示例。鉴于这部分内容不属于 SDK 的范围，所以我另开了一个 <a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fwoai3c%2Fsource-map-demo" rel="nofollow noopener noreferrer">仓库</a> 来做这个事，有兴趣可以看看。</p><h3 id="vue--1">Vue 错误</h3><p>利用 <code>window.onerror</code> 是捕获不到 Vue 错误的，它需要使用 Vue 提供的 API 进行监听。</p><pre><code class="language-js">Vue.config.errorHandler = (err, vm, info) =&gt; {
    // 将报错信息打印到控制台
    console.error(err)

    lazyReportCache({
        info,
        error: err.stack,
        subType: 'vue',
        type: 'error',
        startTime: performance.now(),
        pageURL: getPageURL(),
    })
}</code></pre><h2 id="--12">行为数据采集</h2><h3 id="pv-uv">PV、UV</h3><p>PV(page view) 是页面浏览量，UV(Unique visitor)用户访问量。PV 只要访问一次页面就算一次，UV 同一天内多次访问只算一次。</p><p>对于前端来说，只要每次进入页面上报一次 PV 就行，UV 的统计放在服务端来做，主要是分析上报的数据来统计得出 UV。</p><pre><code class="language-js">export default function pv() {
    lazyReportCache({
        type: 'behavior',
        subType: 'pv',
        startTime: performance.now(),
        pageURL: getPageURL(),
        referrer: document.referrer,
        uuid: getUUID(),
    })
}</code></pre><h3 id="--13">页面停留时长</h3><p>用户进入页面记录一个初始时间，用户离开页面时用当前时间减去初始时间，就是用户停留时长。这个计算逻辑可以放在 <code>beforeunload</code> 事件里做。</p><pre><code class="language-js">export default function pageAccessDuration() {
    onBeforeunload(() =&gt; {
        report({
            type: 'behavior',
            subType: 'page-access-duration',
            startTime: performance.now(),
            pageURL: getPageURL(),
            uuid: getUUID(),
        }, true)
    })
}</code></pre><h3 id="--14">页面访问深度</h3><p>记录页面访问深度是很有用的，例如不同的活动页面 a 和 b。a 平均访问深度只有 50%，b 平均访问深度有 80%，说明 b 更受用户喜欢，根据这一点可以有针对性的修改 a 活动页面。</p><p>除此之外还可以利用访问深度以及停留时长来鉴别电商刷单。例如有人进来页面后一下就把页面拉到底部然后等待一段时间后购买，有人是慢慢的往下滚动页面，最后再购买。虽然他们在页面的停留时间一样，但明显第一个人更像是刷单的。</p><p>页面访问深度计算过程稍微复杂一点：</p><ol><li>用户进入页面时，记录当前时间、scrollTop 值、页面可视高度、页面总高度。</li><li>用户滚动页面的那一刻，会触发 <code>scroll</code> 事件，在回调函数中用第一点得到的数据算出页面访问深度和停留时长。</li><li>当用户滚动页面到某一点时，停下继续观看页面。这时记录当前时间、scrollTop 值、页面可视高度、页面总高度。</li><li>重复第二点...</li></ol><p>具体代码请看：</p><pre><code class="language-js">let timer
let startTime = 0
let hasReport = false
let pageHeight = 0
let scrollTop = 0
let viewportHeight = 0

export default function pageAccessHeight() {
    window.addEventListener('scroll', onScroll)

    onBeforeunload(() =&gt; {
        const now = performance.now()
        report({
            startTime: now,
            duration: now - startTime,
            type: 'behavior',
            subType: 'page-access-height',
            pageURL: getPageURL(),
            value: toPercent((scrollTop + viewportHeight) / pageHeight),
            uuid: getUUID(),
        }, true)
    })

    // 页面加载完成后初始化记录当前访问高度、时间
    executeAfterLoad(() =&gt; {
        startTime = performance.now()
        pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
        scrollTop = document.documentElement.scrollTop || document.body.scrollTop
        viewportHeight = window.innerHeight
    })
}

function onScroll() {
    clearTimeout(timer)
    const now = performance.now()
    
    if (!hasReport) {
        hasReport = true
        lazyReportCache({
            startTime: now,
            duration: now - startTime,
            type: 'behavior',
            subType: 'page-access-height',
            pageURL: getPageURL(),
            value: toPercent((scrollTop + viewportHeight) / pageHeight),
            uuid: getUUID(),
        })
    }

    timer = setTimeout(() =&gt; {
        hasReport = false
        startTime = now
        pageHeight = document.documentElement.scrollHeight || document.body.scrollHeight
        scrollTop = document.documentElement.scrollTop || document.body.scrollTop
        viewportHeight = window.innerHeight        
    }, 500)
}

function toPercent(val) {
    if (val &gt;= 1) return '100%'
    return (val * 100).toFixed(2) + '%'
}</code></pre><h3 id="--15">用户点击</h3><p>利用 <code>addEventListener()</code> 监听 <code>mousedown</code>、<code>touchstart</code> 事件，我们可以收集用户每一次点击区域的大小，点击坐标在整个页面中的具体位置，点击元素的内容等信息。</p><pre><code class="language-js">export default function onClick() {
    ['mousedown', 'touchstart'].forEach(eventType =&gt; {
        let timer
        window.addEventListener(eventType, event =&gt; {
            clearTimeout(timer)
            timer = setTimeout(() =&gt; {
                const target = event.target
                const { top, left } = target.getBoundingClientRect()
                
                lazyReportCache({
                    top,
                    left,
                    eventType,
                    pageHeight: document.documentElement.scrollHeight || document.body.scrollHeight,
                    scrollTop: document.documentElement.scrollTop || document.body.scrollTop,
                    type: 'behavior',
                    subType: 'click',
                    target: target.tagName,
                    paths: event.path?.map(item =&gt; item.tagName).filter(Boolean),
                    startTime: event.timeStamp,
                    pageURL: getPageURL(),
                    outerHTML: target.outerHTML,
                    innerHTML: target.innerHTML,
                    width: target.offsetWidth,
                    height: target.offsetHeight,
                    viewport: {
                        width: window.innerWidth,
                        height: window.innerHeight,
                    },
                    uuid: getUUID(),
                })
            }, 500)
        })
    })
}</code></pre><h3 id="--16">页面跳转</h3><p>利用 <code>addEventListener()</code> 监听 <code>popstate</code>、<code>hashchange</code> 页面跳转事件。需要注意的是调用<code>history.pushState()</code>或<code>history.replaceState()</code>不会触发<code>popstate</code>事件。只有在做出浏览器动作时，才会触发该事件，如用户点击浏览器的回退按钮（或者在Javascript代码中调用<code>history.back()</code>或者<code>history.forward()</code>方法）。同理，<code>hashchange</code> 也一样。</p><pre><code class="language-js">export default function pageChange() {
    let from = ''
    window.addEventListener('popstate', () =&gt; {
        const to = getPageURL()

        lazyReportCache({
            from,
            to,
            type: 'behavior',
            subType: 'popstate',
            startTime: performance.now(),
            uuid: getUUID(),
        })

        from = to
    }, true)

    let oldURL = ''
    window.addEventListener('hashchange', event =&gt; {
        const newURL = event.newURL

        lazyReportCache({
            from: oldURL,
            to: newURL,
            type: 'behavior',
            subType: 'hashchange',
            startTime: performance.now(),
            uuid: getUUID(),
        })

        oldURL = newURL
    }, true)
}</code></pre><h3 id="vue--2">Vue 路由变更</h3><p>Vue 可以利用 <code>router.beforeEach</code> 钩子进行路由变更的监听。</p><pre><code class="language-js">export default function onVueRouter(router) {
    router.beforeEach((to, from, next) =&gt; {
        // 首次加载页面不用统计
        if (!from.name) {
            return next()
        }

        const data = {
            params: to.params,
            query: to.query,
        }

        lazyReportCache({
            data,
            name: to.name || to.path,
            type: 'behavior',
            subType: ['vue-router-change', 'pv'],
            startTime: performance.now(),
            from: from.fullPath,
            to: to.fullPath,
            uuid: getUUID(),
        })

        next()
    })
}</code></pre><h2 id="--17">数据上报</h2><h3 id="--18">上报方法</h3><p>数据上报可以使用以下几种方式：</p><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></li><li>image</li></ul><p>我写的简易 SDK 采用的是第一、第二种方式相结合的方式进行上报。利用 sendBeacon 来进行上报的优势非常明显。</p><p>使用 <strong><code>sendBeacon()</code></strong> 方法会使用户代理在有机会时异步地向服务器发送数据，同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题：数据可靠，传输异步并且不会影响下一页面的加载。</p><p>在不支持 sendBeacon 的浏览器下我们可以使用 XMLHttpRequest 来进行上报。一个 HTTP 请求包含发送和接收两个步骤。其实对于上报来说，我们只要确保能发出去就可以了。也就是发送成功了就行，接不接收响应无所谓。为此，我做了个实验，在 beforeunload 用 XMLHttpRequest 传送了 30kb 的数据（一般的待上报数据很少会有这么大），换了不同的浏览器，都可以成功发出去。当然，这和硬件性能、网络状态也是有关联的。</p><h3 id="--19">上报时机</h3><p>上报时机有三种：</p><ol><li>采用 <code>requestIdleCallback/setTimeout</code> 延时上报。</li><li>在 beforeunload 回调函数里上报。</li><li>缓存上报数据，达到一定数量后再上报。</li></ol><p>建议将三种方式结合一起上报：</p><ol><li>先缓存上报数据，缓存到一定数量后，利用 <code>requestIdleCallback/setTimeout</code> 延时上报。</li><li>在页面离开时统一将未上报的数据进行上报。</li></ol><h2 id="--20">总结</h2><p>仅看理论知识是比较难以理解的，为此我结合本文所讲的技术要点写了一个简单的<a href="https://github.com/woai3c/monitor-demo">监控 SDK</a>，可以用它来写一些简单的 DEMO，帮助加深理解。再结合本文一起阅读，效果更好。</p><h2 id="--21">参考资料</h2><h3 id="--22">性能监控</h3><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Performance_API">Performance API</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceResourceTiming">PerformanceResourceTiming</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API">Using_the_Resource_Timing_API</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming">PerformanceTiming</a></li><li><a href="https://web.dev/metrics/">Metrics</a></li><li><a href="https://web.dev/evolving-cls/">evolving-cls</a></li><li><a href="https://web.dev/custom-metrics/">custom-metrics</a></li><li><a href="https://github.com/GoogleChrome/web-vitals">web-vitals</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver">PerformanceObserver</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element_timing_API">Element_timing_API</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming">PerformanceEventTiming</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Timing-Allow-Origin">Timing-Allow-Origin</a></li><li><a href="https://web.dev/bfcache/">bfcache</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver">MutationObserver</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest">XMLHttpRequest</a></li><li><a href="https://zhuanlan.zhihu.com/p/39292837">如何监控网页的卡顿</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon">sendBeacon</a></li></ul><h3 id="--23">错误监控</h3><ul><li><a href="https://github.com/joeyguo/noerror">noerror</a></li><li><a href="https://github.com/mozilla/source-map">source-map</a></li></ul><h3 id="--24">行为监控</h3><ul><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/popstate_event">popstate</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/hashchange_event">hashchange</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 用 Jenkins 和 Github Actions 实现前端项目自动化部署 ]]>
                </title>
                <description>
                    <![CDATA[ 本教程主要讲解怎么使用 Jenkins 和 Github Actions 部署前端项目。  1. 第一部分是使用 Gitea 配置局域网 git 服务器，再使用 Jenkins 将 Gitea 下的项目部署到局域网服务器。  2. 第二部分是使用 Github Actions 将 Github 项目部署到 Github Page 和阿里云。 阅读本教程并不需要你提前了解 Jenkins 和 Github Actions 的知识，只要按照本教程的指引，就能够实现自动化部署项目。 PS：本人所用电脑操作系统为 windows，即以下所有的操作均在 windows 下运行。其他操作系统的配置大同小异，不会有太大差别。 Gitea + Jenkins 自动构建前端项目并部署到服务器 Gitea 用于构建 Git 局域网服务器，Jenkins 是 CI/CD 工具，用于部署前端项目。 配置 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/automated-deployment-of-front-end-projects-with-jenkins-and-github-actions/</link>
                <guid isPermaLink="false">61650bdb21a1350622df53c5</guid>
                
                    <category>
                        <![CDATA[ Jenkins ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Tue, 12 Oct 2021 04:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/article-cover-pic.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>本教程主要讲解怎么使用 Jenkins 和 Github Actions 部署前端项目。</p><ol><li>第一部分是使用 Gitea 配置局域网 git 服务器，再使用 Jenkins 将 Gitea 下的项目部署到局域网服务器。</li><li>第二部分是使用 Github Actions 将 Github 项目部署到 Github Page 和阿里云。</li></ol><p>阅读本教程并不需要你提前了解 Jenkins 和 Github Actions 的知识，只要按照本教程的指引，就能够实现自动化部署项目。</p><p>PS：本人所用电脑操作系统为 windows，即以下所有的操作均在 windows 下运行。其他操作系统的配置大同小异，不会有太大差别。</p><h2 id="gitea-jenkins-">Gitea + Jenkins 自动构建前端项目并部署到服务器</h2><p>Gitea 用于构建 Git 局域网服务器，Jenkins 是 CI/CD 工具，用于部署前端项目。</p><h3 id="-gitea">配置 Gitea</h3><ol><li>下载 <a href="https://link.juejin.cn?target=https%3A%2F%2Fdl.gitea.io%2Fgitea" rel="nofollow noopener noreferrer">Gitea</a>，选择一个喜欢的版本，例如 1.13，选择 <code>gitea-1.13-windows-4.0-amd64.exe</code> 下载。</li><li>下载完后，新建一个目录（例如 gitea），将下载的 Gitea 软件放到该目录下，双击运行。</li><li>打开 <code>localhost:3000</code> 就能看到 Gitea 已经运行在你的电脑上了。</li><li>点击注册，第一次会弹出一个初始配置页面，数据库选择 <code>SQLite3</code>。另外把 <code>localhost</code> 改成你电脑的局域网地址，例如我的电脑 IP 为 <code>192.168.0.118</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ff66f2a9ff4436bb46216f9777b1ca8~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5685667856ca40ed8f534bc6e344e4ca~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>填完信息后，点击立即安装，等待一会，即可完成配置。</li><li>继续点击注册用户，第一个注册的用户将会成会管理员。</li><li>打开 Gitea 的安装目录，找到 <code>custom\conf\app.ini</code>，在里面加上一行代码 <code>START_SSH_SERVER = true</code>。这时就可以使用 ssh 进行 push 操作了。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/89726b25bd634c17973e8eeb1d7944a3~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>8. 如果使用 http 的方式无法克隆项目，请取消 git 代理。</p><pre><code class="language-git">git config --global --unset http.proxy
git config --global --unset https.proxy
复制代码</code></pre><h3 id="-jenkins">配置 Jenkins</h3><ol><li>需要提前安装 JDK，JDK 安装教程网上很多，请自行搜索。</li><li>打开 <a href="https://www.jenkins.io/zh/download/">Jenkins</a> 下载页面。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f34a061c138a445ca7e35105e68e0d0d~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>安装过程中遇到 <code>Logon Type</code> 时，选择第一个。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/79c134e86d404d37af1df9ec71c5cf2c~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>端口默认为 8080，这里我填的是 8000。安装完会自动打开 <code>http://localhost:8000</code> 网站，这时需要等待一会，进行初始化。</li><li>按照提示找到对应的文件（直接复制路径在我的电脑中打开），其中有管理员密码。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68c2491dc87f40b7a2ed2f3a7c64c624~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>6. 安装插件，选择第一个。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de722d8c3568470f8401f696da106848~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>创建管理员用户，点击完成并保存，然后一路下一步。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bdbb765c02c14e1f8e839d8abd37f321~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>8. 配置完成后自动进入首页，这时点击 <code>Manage Jenkins</code> -&gt; <code>Manage plugins</code> 安装插件。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a9b6d41606164e1f80047d0327c05306~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>9. 点击 <code>可选插件</code>，输入 nodejs，搜索插件，然后安装。 10. 安装完成后回到首页，点击 <code>Manage Jenkins</code> -&gt; <code>Global Tool Configuration</code> 配置 nodejs。如果你的电脑是 win7 的话，nodejs 版本最好不要太高，选择 v12 左右的就行。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dc81f6a6588e4ddf9602a400c884bea7~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h3 id="-">创建静态服务器</h3><ol><li>建立一个空目录，在里面执行 <code>npm init -y</code>，初始化项目。</li><li>执行 <code>npm i express</code> 下载 express。</li><li>然后建立一个 <code>server.js</code> 文件，代码如下：</li></ol><pre><code class="language-js">const express = require('express')
const app = express()
const port = 8080

app.use(express.static('dist'))

app.listen(port, () =&gt; {
    console.log(`Example app listening at http://localhost:${port}`)
})
复制代码</code></pre><p>它将当前目录下的 <code>dist</code> 文件夹设为静态服务器资源目录，然后执行 <code>node server.js</code> 启动服务器。</p><p>由于现在没有 <code>dist</code> 文件夹，所以访问网站是空页面。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/de3ae14bee7040709ef8c9e80f68e1c3~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>不过不要着急，一会就能看到内容了。</p><h3 id="--1">自动构建 + 部署到服务器</h3><ol><li>下载 Jenkins 提供的 demo 项目 <a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fjenkins-docs%2Fbuilding-a-multibranch-pipeline-project" rel="nofollow noopener noreferrer">building-a-multibranch-pipeline-project</a>，然后在你的 Gitea 新建一个仓库，把内容克隆进去，并提交到 Gitea 服务器。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c452046cb4d446728b2a44eb432b4850~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>2. 打开 Jenkins 首页，点击 <code>新建 Item</code> 创建项目。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5427e0513e0e4ce783bf6abd32730c54~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>3. 选择<code>源码管理</code>，输入你的 Gitea 上的仓库地址。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a95f9287739445c28eb0d95ac5e37494~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>你也可以尝试一下定时构建，下面这个代码表示每 5 分钟构建一次。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06f2030ea15044c6980270ce6e58335b~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>选择你的构建环境，这里选择刚才配置的 nodejs。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/352fcfd12ee141c68557b6b42ffe640a~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>6. 点击增加构建步骤，windows 要选 <code>execute windows batch command</code>，linux 要选 <code>execute shell</code>。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2e6ee4b6dd10431f9ac16318766661e9~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="2e6ee4b6dd10431f9ac16318766661e9~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><ol><li>输入 <code>npm i &amp;&amp; npm run build &amp;&amp; xcopy .\build\* G:\node-server\dist\ /s/e/y</code>，这行命令的作用是安装依赖，构建项目，并将构建后的静态资源复制到指定目录 <code>G:\node-server\dist\ </code>。这个目录是静态服务器资源目录。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/01af43df1d80491e9dd8a489ef57c2f4~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>8. 保存后，返回首页。点击项目旁边的小三角，选择 <code>build now</code>。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cea232eb18e4725b0c57678561bd5d6~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>9. 开始构建项目，我们可以点击项目查看构建过程。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/faad4f4509964a849c1796d69874ef32~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>10. 构建成功，打开 <code>http://localhost:8080/</code> 看一下结果。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c6ef1268d3c7409cabd96e33cf944c93~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f46e5659f8b04211b7497852209960da~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>11. 由于刚才设置了每 5 分钟构建一次，我们可以改变一下网站的内容，然后什么都不做，等待一会再打开网站看看。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fb5de42e30e44348b0bcc762f3624f02~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>12. 把修改的内容提交到 Gitea 服务器，稍等一会。打开网站，发现内容已经发生了变化。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/50b88365d90549eb81f0a86954ff62d5~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h3 id="-pipeline-">使用 pipeline 构建项目</h3><p>使用流水线构建项目可以结合 Gitea 的 <code>webhook</code> 钩子，以便在执行 <code>git push</code> 的时候，自动构建项目。</p><ol><li>点击首页右上角的用户名，选择<code>设置</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cd283b66d204fa6934509e1891d0f3d~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>添加 token，记得将 token 保存起来。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ead6e4c2346d4b5fa03601d1ff7d4824~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>打开 Jenkins 首页，点击 <code>新建 Item</code> 创建项目。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51273f6a00384a3d9564b572f83695be~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>4. 点击<code>构建触发器</code>，选择<code>触发远程构建</code>，填入刚才创建的 token。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1c9bdcd8fd8549c4a21be519069f620f~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>5. 选择流水线，按照提示输入内容，然后点击<code>保存</code>。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b343b4750c0a4c959125a0832e3de5fe~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>6. 打开 Jenkins 安装目录下的 <code>jenkins.xml</code> 文件，找到 <code>&lt;arguments&gt;</code> 标签，在里面加上 <code>-Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true</code>。它的作用是关闭 <code>CSRF</code> 验证，不关的话，Gitea 的 <code>webhook</code> 会一直报 403 错误，无法使用。加好参数后，在该目录命令行下输入 <code>jenkins.exe restart</code> 重启 Jenkins。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c46ea1cc26664600a231615fa2ab0d5c~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>7. 回到首页，配置全局安全选项。勾上<code>匿名用户具有可读权限</code>，再保存。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f2fdfa1709e460ab73fb4429c147ac4~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3eb4cf1f1637407abfe7ac811104a3df~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>打开你的 Gitea 仓库页面，选择<code>仓库设置</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d66a648f453d4393810a7bfaed1edd7c~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>点击<code>管理 web 钩子</code>，添加 web 钩子，钩子选项选择 <code>Gitea</code>。</li><li>目标 URL 按照 Jenkins 的提示输入内容。然后点击<code>添加 web 钩子</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0f7c0356f1d4126936d77976873c69a~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/10831af1048c48a5918e2a1dfd05677b~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>11. 点击创建好的 web 钩子，拉到下方，点击测试推送。不出意外，应该能看到推送成功的消息，此时回到 Jenkins 首页，发现已经在构建项目了。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f2cbe9a5c87e44ed9d82359893cc8c2f~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>12. 由于没有配置 <code>Jenkinsfile</code> 文件，此时构建是不会成功的。所以接下来需要配置一下 <code>Jenkinsfile</code> 文件。将以下代码复制到你 Gitea 项目下的 <code>Jenkinsfile</code> 文件。jenkins 在构建时会自动读取文件的内容执行构建及部署操作。</p><pre><code>pipeline {
    agent any
    stages {
        stage('Build') {
            steps {  // window 使用 bat， linux 使用 sh
                bat 'npm i'
                bat 'npm run build'
            }
        }
        stage('Deploy') {
            steps {
                bat 'xcopy .\\build\\* D:\\node-server\\dist\\ /s/e/y' // 这里需要改成你的静态服务器资源目录
            }
        }
    }
}
复制代码</code></pre><ol><li>每当你的 Gitea 项目执行 <code>push</code> 操作时，Gitea 都会通过 <code>webhook</code> 发送一个 post 请求给 Jenkins，让它执行构建及部署操作。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5fc2ee216393438aa91fd26d0e8096db~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h3 id="--2">小结</h3><p>如果你的操作系统是 Linux，可以在 Jenkins 打包完成后，使用 ssh 远程登录到阿里云，将打包后的文件复制到阿里云上的静态服务器上，这样就能实现阿里云自动部署了。具体怎么远程登录到阿里云，请看下文中的 《Github Actions 部署到阿里云》 一节。</p><h2 id="github-actions-">Github Actions 自动构建前端项目并部署到服务器</h2><p>如果你的项目是 Github 项目，那么使用 Github Actions 也许是更好的选择。</p><h3 id="-github-page">部署到 Github Page</h3><p>接下来看一下如何使用 Github Actions 部署到 Github Page。</p><p>在你需要部署到 Github Page 的项目下，建立一个 yml 文件，放在 <code>.github/workflow</code> 目录下。你可以命名为 <code>ci.yml</code>，它类似于 Jenkins 的 <code>Jenkinsfile</code> 文件，里面包含的是要自动执行的脚本代码。</p><p>这个 yml 文件的内容如下：</p><pre><code class="language-yml">name: Build and Deploy
on: # 监听 master 分支上的 push 事件
  push:
    branches:
      - master
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest # 构建环境使用 ubuntu
    steps:
      - name: Checkout
        uses: actions/checkout@v2.3.1  
        with:
          persist-credentials: false

      - name: Install and Build # 下载依赖 打包项目
        run: |
          npm install
          npm run build

      - name: Deploy # 将打包内容发布到 github page
        uses: JamesIves/github-pages-deploy-action@3.5.9 # 使用别人写好的 actions
        with:  # 自定义环境变量
          ACCESS_TOKEN: ${{ secrets.VUE_ADMIN_TEMPLATE }} # VUE_ADMIN_TEMPLATE 是我的 secret 名称，需要替换成你的
          BRANCH: master
          FOLDER: dist
          REPOSITORY_NAME: woai3c/woai3c.github.io # 这是我的 github page 仓库
          TARGET_FOLDER: github-actions-demo # 打包的文件将放到静态服务器 github-actions-demo 目录下

复制代码</code></pre><p>上面有一个 <code>ACCESS_TOKEN</code> 变量需要自己配置。</p><ol><li>打开 Github 网站，点击你右上角的头像，选择 <code>settings</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9bb947ff3c7243af92c62b221b472056~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>点击左下角的 <code>developer settings</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ea820b671dc64a24b2863c7dd1bdb093~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>在左侧边栏中，单击 <code>Personal access tokens（个人访问令牌）</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/50368065e962408cb53c938c4b509848~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>单击 <code>Generate new token（生成新令牌）</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ebc0215aa43d4bdba85a79d97beeeca3~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>输入名称并勾选 <code>repo</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2aed7e43e6a423199c0a583355d03d5~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>拉到最下面，点击 <code>Generate token</code>，并将生成的 token 保存起来。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/314f733f0ead462ca497fd49531874ee~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><ol><li>打开你的 Github 项目，点击 <code>settings</code>。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5a7a9afc78d048049e9fcb7e2af40eec~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>点击 <code>secrets</code>-&gt;<code>new secret</code>。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5dc749949c5c4238863f12e2eae08805~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>创建一个密钥，名称随便填（中间用下划线隔开），内容填入刚才创建的 token。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f112c870534a4ed4878045f4fd4c39fd~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d714a34c74e5463cace0552442da3880~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>将上文代码中的 <code>ACCESS_TOKEN: ${{ secrets.VUE_ADMIN_TEMPLATE }}</code> 替换成刚才创建的 secret 名字，替换后代码如下 <code>ACCESS_TOKEN: ${{ secrets.TEST_A_B }}</code>。保存后，提交到 Github。</p><p>以后你的项目只要执行 <code>git push</code>，Github Actions 就会自动构建项目并发布到你的 Github Page 上。</p><p>Github Actions 的执行详情点击仓库中的 <code>Actions</code> 选项查看。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9afeda4174cd45b0814552c3a4eaa180~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/537454335ae947e18a9454988f7e57fc~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>具体详情可以参考一下我的 demo 项目 <strong><a href="https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fwoai3c%2Fgithub-actions-demo" rel="nofollow noopener noreferrer">github-actions-demo</a></strong>。</p><p>构建成功后，打开 Github Page 网站，可以发现内容已经发布成功。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/11313081f18b40b0a474be43de79ae1c~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h3 id="github-actions--1">Github Actions 部署到阿里云</h3><h4 id="--3">初始化阿里云服务器</h4><ol><li>购买阿里云服务器，选择操作系统，我选的 ubuntu</li><li>在云服务器管理控制台选择实例-&gt;更多-&gt;密钥-&gt;重置实例密码（一会登陆用）</li><li>选择远程连接-&gt;VNC，会弹出一个密码，记住它，以后远程连接要用（ctrl + alt + f1~f6 切换终端，例如 ctrl + alt + f1 是第一个终端）</li><li>进入后是一个命令行 输入 <code>root</code>（默认用户名），密码为你刚才重置的实例密码</li><li>登陆成功， 更新安装源 <code>sudo apt-get update &amp;&amp; sudo apt-get upgrade -y</code></li><li>安装 npm <code>sudo apt-get install npm</code></li><li>安装 npm 管理包 <code>sudo npm install -g n</code></li><li>安装 node 最新稳定版 <code>sudo n stable</code></li></ol><h4 id="--4">创建一个静态服务器</h4><pre><code class="language-js">mkdir node-server // 创建 node-server 文件夹
cd node-server // 进入 node-server 文件夹
npm init -y // 初始化项目
npm i express
touch server.js // 创建 server.js 文件
vim server.js // 编辑 server.js 文件
复制代码</code></pre><p>将以下代码输入进去（用 vim 进入文件后按 i 进行编辑，保存时按 esc 然后输入 :wq，再按 enter），更多使用方法请自行搜索。</p><pre><code class="language-js">const express = require('express')
const app = express()
const port = 3388 // 填入自己的阿里云映射端口，在网络安全组配置。

app.use(express.static('dist'))

app.listen(port, '0.0.0.0', () =&gt; {
    console.log(`listening`)
})
复制代码</code></pre><p>执行 <code>node server.js</code> 开始监听，由于暂时没有 <code>dist</code> 目录，先不要着急。</p><p>注意，监听 IP 必须为 <code>0.0.0.0</code> ，详情请看<a href="https://www.alibabacloud.com/help/zh/doc-detail/50775.htm">部署 Node.js 项目注意事项</a>。</p><p>阿里云入端口要在网络安全组中查看与配置。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aabf84961fa146ed8345785b069788bd~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h4 id="--5">创建阿里云密钥对</h4><p>请参考<a href="https://www.alibabacloud.com/help/zh/doc-detail/51793.htm">创建 SSH 密钥对</a>和<a href="https://www.alibabacloud.com/help/zh/doc-detail/51796.htm?spm=a2c63.p38356.879954.9.cf992580IYf2O7#concept-zzt-nl1-ydb">绑定 SSH 密钥对</a> ，将你的 ECS 服务器实例和密钥绑定，然后将私钥保存到你的电脑（例如保存在 ecs.pem 文件）。</p><p>打开你要部署到阿里云的 Github 项目，点击 setting-&gt;secrets。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8c335a8b83c945428221ff8b064a2bd8~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>点击 new secret</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bbb7d4958522458fb3238388044710df~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>secret 名称为 <code>SERVER_SSH_KEY</code>，并将刚才的阿里云密钥填入内容。</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ac2bf48aae7c46bdb1e641f832db57c6~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>点击 add secret 完成。</p><p>在你项目下建立 <code>.github\workflows\ci.yml</code> 文件，填入以下内容：</p><pre><code class="language-yml">name: Build app and deploy to aliyun
on:
  #监听push操作
  push:
    branches:
      # master分支，你也可以改成其他分支
      - master
jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Install Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '12.16.2'
    - name: Install npm dependencies
      run: npm install
    - name: Run build task
      run: npm run build
    - name: Deploy to Server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: '-rltgoDzvO --delete'
          SOURCE: dist # 这是要复制到阿里云静态服务器的文件夹名称
          REMOTE_HOST: '118.190.217.8' # 你的阿里云公网地址
          REMOTE_USER: root # 阿里云登录后默认为 root 用户，并且所在文件夹为 root
          TARGET: /root/node-server # 打包后的 dist 文件夹将放在 /root/node-server
复制代码</code></pre><p>保存，推送到 Github 上。</p><p>以后只要你的项目执行 <code>git push</code> 操作，就会自动执行 <code>ci.yml</code> 定义的脚本，将打包文件放到你的阿里云静态服务器上。</p><p>这个 Actions 主要做了两件事：</p><ol><li>克隆你的项目，下载依赖，打包。</li><li>用你的阿里云私钥以 SSH 的方式登录到阿里云，把打包的文件上传（使用 rsync）到阿里云指定的文件夹中。</li></ol><p>如果还是不懂，建议看一下我的 <a href="https://github.com/woai3c/github-actions-aliyun-demo">demo</a>。</p><h3 id="ci-yml-"><code>ci.yml</code> 配置文件讲解</h3><ol><li><code>name</code>，表示这个工作流程（workflow）的名称。</li><li><code>on</code>，表示监听的意思，后面可以加上各种事件，例如 <code>push</code> 事件。</li></ol><p>下面这段代码表示要监听 <code>master</code> 分支的 <code>push</code> 事件。当 Github Actions 监听到 <code>push</code> 事件发生时，它就会执行下面 <code>jobs</code> 定义的一系列操作。</p><pre><code class="language-yml">name: Build app and deploy to aliyun
on:
  #监听push操作
  push:
    branches:
      # master分支，你也可以改成其他分支
      - master
jobs:
...
复制代码</code></pre><ol><li><code>jobs</code>，看字面意思就是一系列的作业，你可以在 <code>jobs</code> 字段下面定义很多作业，例如 <code>job1</code>、<code>job2</code> 等等，并且它们是并行执行的。</li></ol><pre><code class="language-yml">jobs:
  job1:
  	...
  job2:
  	...
  job3:
	...
复制代码</code></pre><p>回头看一下 <code>ci.yml</code> 文件，它只有一个作业，即 <code>build</code>，作业的名称是自己定义的，你叫 <code>good</code> 也可以。</p><ol><li><code>runs-on</code>，表示你这个工作流程要运行在什么操作系统上，<code>ci.yml</code> 文件定义的是最新稳定版的 <code>ubuntu</code>。除了 ubuntu，它还可以选择 Mac 或 Windows。</li></ol><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa286b043d2f455d81c1ac034387481c~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="aa286b043d2f455d81c1ac034387481c~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><ol><li><code>steps</code>，看字面意思就是一系列的步骤，也就是说这个作业由一系列的步骤完成。例如先执行 <code>step1</code>，再执行 <code>step2</code>...</li></ol><h4 id="setps-"><code>setps</code> 步骤讲解</h4><p><code>setps</code> 其实是一个数组，在 YAML 语法中，以 <code>-</code> 开始就是一个数组项。例如 <code>['a', 'b', 'c']</code> 用 YAML 语法表示为：</p><pre><code class="language-yml">- a
- b
- c
复制代码</code></pre><p>所以 <code>setps</code> 就是一个步骤数组，从上到下开始执行。从 <code>ci.yml</code> 文件来看，每一个小步骤都有几个相关选项：</p><ol><li><code>name</code>，小步骤的名称。</li><li><code>uses</code>，小步骤使用的 actions 库名称或路径，Github Actions 允许你使用别人写好的 Actions 库。</li><li><code>run</code>，小步骤要执行的 <code>shell</code> 命令。</li><li><code>env</code>，设置与小步骤相关的环境变量。</li><li><code>with</code>，提供参数。</li></ol><figure class="kg-card kg-image-card"><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/71864f3ffec24798a7ccb1ca6eb3b1cf~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="71864f3ffec24798a7ccb1ca6eb3b1cf~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><p>综上所述，<code>ci.yml</code> 文件中的 <code>setps</code> 就很好理解了，下面从头到尾解释一边：</p><pre><code class="language-yml">    steps:
    - uses: actions/checkout@v1
    - name: Install Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '12.16.2'
    - name: Install npm dependencies
      run: npm install
    - name: Run build task
      run: npm run build
    - name: Deploy to Server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: '-rltgoDzvO --delete'
          SOURCE: dist # 这是要复制到阿里云静态服务器的文件夹名称
          REMOTE_HOST: '118.190.217.8' # 你的阿里云公网地址
          REMOTE_USER: root # 阿里云登录后默认为 root 用户，并且所在文件夹为 root
          TARGET: /root/node-server # 打包后的 dist 文件夹将放在 /root/node-server
复制代码</code></pre><ol><li>使用 <code>actions/checkout@v1</code> 库克隆代码到 <code>ubuntu</code> 上。</li><li>使用 <code>actions/setup-node@v1</code> 库安装 nodejs，<code>with</code> 提供了一个参数 <code>node-version</code> 表示要安装的 nodejs 版本。</li><li>在 <code>ubuntu</code> 的 <code>shell</code> 上执行 <code>npm install</code> 下载依赖。</li><li>执行 <code>npm run build</code> 打包项目。</li><li>使用 <code>easingthemes/ssh-deploy@v2.1.5</code> 库，这个库的作用就是用 <code>SSH</code> 的方式远程登录到阿里云服务器，将打包好的文件夹复制到阿里云指定的目录上。</li></ol><p>从 <code>env</code> 上可以看到，这个 actions 库要求我们提供几个环境变量：</p><ol><li><code>SSH_PRIVATE_KEY</code>: 阿里云密钥对中的私钥（需要你提前写在 github secrets 上），</li><li><code>ARGS: '-rltgoDzvO --delete'</code>，没仔细研究，我猜是复制完文件就删除掉。</li><li><code>SOURCE</code>：打包后的文件夹名称</li><li><code>REMOTE_HOST</code>: 阿里云公网 IP 地址</li><li><code>REMOTE_USER</code>: 阿里云服务器的用户名</li><li><code>TARGET</code>: 你要拷贝到阿里云服务器指定目录的名称</li></ol><p>如果你想了解一下其他 actions 库的实现，可以直接复制 actions 库的名称去搜索引擎搜索一下，例如搜索 <code>actions/checkout</code> 的结果为：</p><figure class="kg-card kg-image-card"><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d2e353600ce54f1e8b0517c13d596b9c~tplv-k3u1fbpfcp-watermark.awebp" class="kg-image" alt="d2e353600ce54f1e8b0517c13d596b9c~tplv-k3u1fbpfcp-watermark" width="600" height="400" loading="lazy"></figure><p>都看到这了，给个赞再走吧。</p><h2 id="--6">参考资料</h2><ul><li><a href="https://www.jenkins.io/zh/doc/">Jenkins 用户手册</a></li><li><a href="https://docs.github.com/cn/actions">GitHub Actions 文档</a></li><li><a href="https://docs.github.com/cn/pages/getting-started-with-github-pages/about-github-pages">GitHub Pages 文档</a></li><li><a href="https://docs.gitea.io/zh-cn/">Gitea 文档</a></li><li><a href="https://docs.github.com/cn/actions/learn-github-actions/workflow-syntax-for-github-actions">GitHub 操作的工作流程语法</a></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 趣谈 Jest 配置 ]]>
                </title>
                <description>
                    <![CDATA[ Jest 已经成为了大部分前端项目的标配，每次说到 Jest、Webpack、ESLint  等配置，脑瓜子都嗡嗡的。在诸多配置中，有时一个“铆钉大”的配置，就能让程序或测试的运行效率大幅下降。至于为啥要写这篇文章，就是因为目前所在的项目因一处 Jest  配置的问题，导致60多个 test case 在 --no-cache 条件下要跑足足 790s。 所以就记录分享一下 Jest 的一些常用配置。 module.exports = {   setupFiles: [     'react-app-polyfill/jsdom',     '<rootDir>/test/unit/jest.setup.js',     'core-js',   ],   testMatch: [     ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/jest-setting/</link>
                <guid isPermaLink="false">6162a27121a1350622df5186</guid>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Jiahao Li ]]>
                </dc:creator>
                <pubDate>Sat, 09 Oct 2021 07:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/10/1_xFEOPesnxHTgZFBGzwQ6Kg.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><code>Jest</code> 已经成为了大部分前端项目的标配，每次说到 <code>Jest</code>、<code>Webpack</code>、<code>ESLint</code> 等配置，脑瓜子都嗡嗡的。在诸多配置中，有时一个“铆钉大”的配置，就能让程序或测试的运行效率大幅下降。至于为啥要写这篇文章，就是因为目前所在的项目因一处 <code>Jest</code> 配置的问题，导致60多个 <em>test case</em> 在 <code>--no-cache</code> 条件下要跑足足 <em><strong>790s</strong></em>。</p><p>所以就记录分享一下 <code>Jest</code> 的一些常用配置。</p><pre><code class="language-js">module.exports = {
  setupFiles: [
    'react-app-polyfill/jsdom',
    '&lt;rootDir&gt;/test/unit/jest.setup.js',
    'core-js',
  ],
  testMatch: [
    '&lt;rootDir&gt;/src/**/__tests__/**/*.{spec,test}.{js,jsx,ts,tsx}',
    '&lt;rootDir&gt;/src/**/*.{spec,test}.{js,jsx,ts,tsx}',
  ],
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': '&lt;rootDir&gt;/node_modules/babel-jest',
    '^.+\\.(css|less)$': '&lt;rootDir&gt;/config/jest/cssTransform.js',
    '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '&lt;rootDir&gt;/config/jest/fileTransform.js',
  },
  transformIgnorePatterns: [
    '[/\\\\]node_modules/(?!(antd)/)[/\\\\].+\\.(js|jsx|ts|tsx)$',
  ],
  moduleNameMapper: {
    '^react-native$': 'react-native-web',
    '^.+\\.module\\.(css|sass|scss|less)$': 'identity-obj-proxy',
    '\\.svg$': 'identity-obj-proxy',
    'test/(.*)': '&lt;rootDir&gt;/test/$1',
    '^src/(.*)': '&lt;rootDir&gt;/src/$1',
  },
  moduleFileExtensions: [
    'web.js',
    'js',
    'web.ts',
    'ts',
    'web.tsx',
    'tsx',
    'json',
    'web.jsx',
    'jsx',
    'node',
  ],
  // 其它配置已省略
};
</code></pre><p>对于 <code>Jest</code> 的配置优化无外乎下面两点：</p><ul><li>更少：减少不必要的元素（比如图片、样式等）；</li><li>更精确：减少在文件系统中查找匹配的时间；</li></ul><p>在看下面如何优化之前，可以先看下这份 <code>Jest</code> 配置，看一下有没有什么可以想到的优化点</p><h3 id="setupfiles"><code>setupFiles</code></h3><p>古语云：“思则有备，有备无患”。但跑测试时一定要“万事”俱备才行吗？</p><p><code>setupFiles</code> 可谓是 <code>Jest</code> 的“内务府大臣”，位居二品！此大臣就是来准备测试所需要的一些环境或 <code>mock</code> 一些全局状态的，比如 <code>@testing-library/jest-dom</code> 就常在 <code>setupFile</code> 中用到，它允许我们可以在 <code>Jest</code> 中断言一些关于 <code>DOM</code> 的状态。而我们再回头去看上面的配置：</p><pre><code class="language-json">{
  "setupFiles": [
    "react-app-polyfill/jsdom",
    "&lt;rootDir&gt;/test/unit/jest.setup.js",
    "core-js"
  ]
}</code></pre><p><code>react-app-polyfill/jsdom</code> 做了什么：</p><pre><code class="language-js">// 真的，源码只有这三行，原来“内务府”也有“外包项目” 
if (typeof window !== 'undefined') {
  require ('whatwg-fetch');
}
</code></pre><p><code>whatwg-fetch</code> 和 <code>core-js</code> 可以简单理解为对当下的一些新标准做 <code>polyfill</code>，但我们有 <code>babel-jest</code> 呀，还要你二者何用？果断“开掉”，节省开支</p><pre><code class="language-json">{
  "setupFiles": ["&lt;rootDir&gt;/test/unit/jest.setup.js"]
}</code></pre><h3 id="modulefileextensions"><code>moduleFileExtensions</code></h3><p><code>moduleFileExtensions</code> 就是 <code>Jest</code> 中各个“国家”（模块）的“通关文牒”，有此文牒方可游历各国。游遍各国也得有个顺序不是，不然会徒增“食宿饮马等费用”，要是搁徐霞客身上，若不是他家业富足，不然大可能会饿死途中。张骞通西域朝廷会有路线规划（默认配置），当然他也能随机应变（自定义配置），默认配置大部分情况下是行得通的，只不过可能要在“路上”多花些时间：</p><pre><code class="language-js">// 默认配置
["js", "jsx", "ts", "tsx", "json", "node"]
</code></pre><p><code>moduleFileExtensions</code> 会 <strong>从左到右</strong> 查找对应的 <code>extension</code>，但如果在 <code>TypeScript + React</code> 项目中可能稍微调整一下会更好：</p><pre><code class="language-js">// 调整后
["ts", "tsx", "js", "jsx", "json", "node"]
</code></pre><p>这样就能减少些查找 <code>extension</code> 的次数，省点“油钱”。</p><h3 id="modulenamemapper"><code>moduleNameMapper</code></h3><p>一般 <code>npm</code> 依赖中的源码分为 <code>esm</code> 和 <code>cjs</code> 模块，当然像 <code>react</code> 之类的是分为 <code>cjs</code> 和 <code>umd</code>。以 <code>antd</code> 为例，其结构如下：</p><pre><code class="language-js">antd
  |- lib/
  |- es/
  |- package.json
</code></pre><p>其中，在 <code>package.json</code> 中可以制定 <code>esm</code> 和 <code>cjs</code> 打包文件的目录，以 <code>antd</code> 中使用到的 <code>rc-select</code> 组件包为例：</p><pre><code class="language-json">{
  "version": "12.1.5",
  "main": "./lib/index",
  "module": "./es/index"
}</code></pre><p>如要使用 <code>cjs</code> 规范的打包文件，工具会查询 <code>main</code> 字段对应文件路径内的入口文件，使用 <code>esm</code> 规范的打包文件，工具则会查询 <code>module</code> 字段对应的入口文件。</p><p>但在 <code>rc-util</code> 中，并没有指明 <code>main</code> 及 <code>module</code> 字段，那么其使用方式就像下面这样：</p><pre><code class="language-js">// rc-select/es/utils/legacyUtils.js
import toArray from 'rc-util/es/Children/toArray';
</code></pre><p>“聊 <code>Jest</code> 呢，叨叨上面这么多干嘛呢？”</p><p>因为 <code>Jest</code> 目前支持的是 <code>cjs</code> 规范，项目中又用到了 <code>antd</code>，所以对于其使用的 <code>rc-util</code> 这种依赖，<code>Jest</code> 无法处理，需要手动转换一下，这就需要引入一个 <code>Jest</code> 配置字段 —— <code>moduleNameMapper</code>，关于该配置字段的描述文档如下所述：</p><blockquote><em>A map from regular expressions to module names that allow to stub out resources, like images or styles with a single module.</em></blockquote><p>说白了就是用来 <code>stub</code> 一些资源文件或 <code>module</code> 使用的，可以把匹配到的内容映射为你指定的内容，哪怕是“指鹿为马”也是行得通的！在前端的单元测试中，时常有许多内容是不需要的，比如：静态资源、样式文件等。那么这个时候就可以将这些“鹿”指成“马”了。</p><p>我们常把“鹿 ”指为 <code>identity-obj-proxy</code> 这个工具，虽然 <code>identity-obj-proxy</code> 上次发布是5年前了，但确实很好用，并且源码也十分简单（2分钟你看不完源码你顺着信号 来打我）！</p><pre><code class="language-js">module.exports = {
  // ...
  moduleNameMapper: {
    '\\.svg$': 'identity-obj-proxy',
    '\\.css$': 'identity-obj-proxy',
  },
};
</code></pre><p>对于上面说到的将 <code>antd</code> 的 <code>es</code> 指到 <code>lib</code> 也就很简单了：</p><pre><code class="language-js">module.exports = {
  // ...
  moduleNameMapper: {
    'antd/es/(.*)': 'antd/lib/$1',
  },
};
</code></pre><p>通过 <code>moduleNameMapper</code> 就可以做到 <strong><em>更少</em></strong> 这个原则，当然下面要介绍的 <code>transformIgnorePatterns</code> 以及其它 <code>ignore</code> 等相关字段也可以让处理的资源或无关的资源更少。</p><h3 id="transform"><code>transform</code></h3><p>《天龙八部》中，马大元夫人康敏将一招“借刀杀人”发挥得淋漓尽致，而在 <code>Jest</code> 中 <code>transform</code> 也“借他人之手除掉异己”，至于康敏居心何在下回再讲，这次就说 <code>transform</code> 为何要“下此毒手”。所谓“异己”一般是脱离自己控制的资源，上面说到 <code>Jest</code> 支持的是 <code>cjs</code>，但在现在的前端项目中，一般都是使用 <code>import/export</code> 等 <code>esm</code> 规范来模块化开发，所以对于这种资源，我们需“借他之手”处理：</p><pre><code class="language-js">module.exports = {
    // ...
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};
</code></pre><p>但这么看来，<code>transform</code> 也可以将 <code>antd</code> 中的 <code>esm</code> 资源转为 <code>cjs</code>，但既然可以“礼仪教化”，又何必“兵刃相接”呢？</p><h3 id="transformignorepatterns"><code>transformIgnorePatterns</code></h3><p>通过名字就能看出来，此配置的内容是“康敏”理都不想理的内容，该值默认是 <code>['node_modules']</code>，也十分好理解。但我们回到文章最初的配置去看看：</p><pre><code class="language-js">module.exports = {
  // ...
  transformIgnorePatterns: [
    '[/\\\\]node_modules/(?!(antd)/)[/\\\\].+\\.(js|jsx|ts|tsx)$',
  ],
};
</code></pre><p>项目的初衷是使用 <code>transform</code> 去处理引入的 <code>antd</code> 的资源，但这也就导致了在 <code>transform</code> 时去遍历了整个 <code>node_modules</code> 文件系统，<code>node_modules</code> 内容是非常多的，所以在扫描时耗费了大量的时间，测试跑完发现“乔峰找着他爹了”。</p><p>所以本文最开始所说的“一处配置问题”就是这儿，删掉 <code>transformIgnorePatterns</code> 转而使用 <code>moduleNameMapper</code> 会快很多，不信你试试！</p><h2 id="-">结语</h2><p>当优化、最佳实践等问题脱离具体项目场景时，那就是“耍流氓”！上学时老师经常说：“具体问题，具体分析”，对开发来说也是如此。经过优化后，测试消耗时间从 <strong><em>790s</em></strong> 缩减到了 <strong><em>40s</em></strong>，还算是一个可以接受的时间吧✌️</p><p>欢迎搜索关注微信公众号 “Refactor”，阅读我的更多文章。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 我是如何零基础入门前端开发的（2021 版） ]]>
                </title>
                <description>
                    <![CDATA[ 大家好，我是山山而川，一位零基础转行前端的小白，很荣幸受 freeCodeCamp邀请给大家分享一下有关零基础学习前端的内容。 为什么写这篇文章呢 我是95年的，非CS专业，很早之前就接触到了编程这一块（当14年IOS开发特别火），但是那个时候并没有太多意向或兴趣去学深入了解，只是觉得看着挺有意思看过几眼就没后续了，现在想想挺遗憾的。如果当时身边有个人可以指点一下，说不定现在也是个六七年的“老程序员”了。 不过现在也不算太迟，现在正在学习前端转行中，完完全全的零基础，所以踩过的坑有很多。相信有不少跟我类似经历的伙伴在这个过程中应该会有不少困惑，比如：网上教程这么多，到底该学哪个？看不懂怎么办？学到什么程度才能找到第一份工作呢？那么我们今天的主题就是给大家分享一下我总结的学习路线。 前端入门概述 首先说明一下，前端学习是一个螺旋上升的过程，既要反复地看书，也要抓紧时间进行实战。只看书，看了就会忘，所以必须将看书和写代码相结合。 然后大家最关心的入门前端开发到找工作3～6个月左右就可以了，主要需要学习 HTML、CSS 和 JavaScript 三大件。之后学习前端主流框架的使用， ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/how-do-i-learn-front-end-development-in-2021/</link>
                <guid isPermaLink="false">614ed4ebe395c7062ee3bff4</guid>
                
                    <category>
                        <![CDATA[ 自学编程 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ 山山而川 ]]>
                </dc:creator>
                <pubDate>Sat, 25 Sep 2021 10:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/09/tim-mossholder-WE_Kv_ZB1l0-unsplash.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>大家好，我是山山而川，一位零基础转行前端的小白，很荣幸受 freeCodeCamp邀请给大家分享一下有关零基础学习前端的内容。</p><h1 id="-">为什么写这篇文章呢</h1><p>我是95年的，非CS专业，很早之前就接触到了编程这一块（当14年IOS开发特别火），但是那个时候并没有太多意向或兴趣去学深入了解，只是觉得看着挺有意思看过几眼就没后续了，现在想想挺遗憾的。如果当时身边有个人可以指点一下，说不定现在也是个六七年的“老程序员”了。</p><p>不过现在也不算太迟，现在正在学习前端转行中，完完全全的零基础，所以踩过的坑有很多。相信有不少跟我类似经历的伙伴在这个过程中应该会有不少困惑，比如：网上教程这么多，到底该学哪个？看不懂怎么办？学到什么程度才能找到第一份工作呢？那么我们今天的主题就是给大家分享一下我总结的学习路线。</p><h1 id="--1">前端入门概述</h1><p>首先说明一下，前端学习是一个螺旋上升的过程，既要反复地看书，也要抓紧时间进行实战。只看书，看了就会忘，所以必须将看书和写代码相结合。</p><p>然后大家最关心的入门前端开发到找工作3～6个月左右就可以了，主要需要学习 HTML、CSS 和 JavaScript 三大件。之后学习前端主流框架的使用，并基于已学内容开发一个小项目进行实战。以上内容学习并理解透彻以后，就算真正地入门前端了。</p><p>当然如果是全职学习，那么时间可以缩短为3-4个月，反之零零散散的学习可能6-8个月，甚至一年之后，具体的学习时间大家自己去规划，重要的是合理计划时间及坚持不懈执行（这点很重要，很多人就是死在了这里）。</p><h1 id="--2">下面是具体的学习教程</h1><p>我这篇教程是从有想法转前端开始就从各种渠道汇总总结的，期间不知道入了多少坑，比如百度随便搜索下前端教程、前端怎么学，然后立马一大推营销号推荐免费领取基础课程，其实多看几家你就知道本质上都是给你卖课的；另外有很多大佬推荐的教程要么是时间比较久了（意味着教程老了），要么就是比较零散，不系统（作为正在学习的我来说，能稳定找到第一份工作就是好教程），所以一下内容都是我整理了好久才总结的，综合了微信公众号作者“技术漫谈”丁哥、“若川视野”川哥、运营转前端成功的年年、网上各种资源等总结而成的，希望可以帮助到大家（几位大佬和公司前端同事一致认为，以下内容好好学，全部学完，找到一份工作不难。前提是你坚持下去）。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/09/image-3.png" class="kg-image" alt="image-3" width="600" height="400" loading="lazy"></figure><h2 id="-html-css">阶段一：HTML + CSS</h2><h3 id="--3"><strong>一、学习资料</strong></h3><p>首先是前端三大件 HTML、CSS 部分的学习，入门阶段不需要学的多深入，只需要抓住几块核心内容，快速过一遍就行，例如选择器、层叠、继承、盒模型、Flex 页面布局和网格等。太细节的内容不用记，之后需要了再回来查。</p><p>1、《Head First HTML and CSS》：这本书比较简单，适合新手入门</p><p>2、MDN：学习 <a href="https://developer.mozilla.org/zh-CN/docs/Learn/HTML">HTML 学习路径</a>部分，整体浏览一遍，不需要死记硬背</p><p>3、<a href="https://www.w3school.com.cn/">W3school</a>：学习 HTML 部分，整体浏览一遍，不需要死记硬背</p><p>4、《HTML5 与 CSS3 基础教程》（第8版）：这本书里面有非常多 HTML 标签，整体浏览一遍，不需要死记硬背，知道有哪些标签以及各自的作用是什么，整体有一个印象即可。</p><p>5、《精通CSS（第3版）》 或 《深入解析CSS》：第一本书是目前最适合 CSS 入门的书，大家可以优先考虑看这个；第二本书是2020年的新书，内容和第一本差不多，奇虎团工程师翻译质量也有保证，讲得也非常详细，不想看第一本看这本也行。</p><h3 id="--4"><strong>二、代码实战</strong></h3><p>学习完了 HTML、CSS，下面就进入到代码实际练习和巩固阶段了，<a href="https://chinese.freecodecamp.org/learn/">freeCodeCamp</a>可以很好的帮我们实现，在freeCodeCamp的“响应式网页设计”部分，可以把前面学到的知识整体练习一遍，最后还有五个小项目练习来确认基础掌握情况。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/09/image-2.png" class="kg-image" alt="image-2" width="600" height="400" loading="lazy"></figure><p>在做项目练习的时候如果发现静态页面写不出来很正常，可以先去看别人写的代码，看完后就知道怎么写了。</p><p>全部写完之后可以根据百度前端代码规范，重写这五个项目的代码。重写之后，再跟原网页代码对比对比，看看还有哪方面的不足。</p><h2 id="-javascript"><strong>阶段二：JavaScript</strong></h2><h3 id="--5"><strong>一、学习资料</strong></h3><p>学习完HTML、CSS之后我们就可以进入JavaScript的学习了，JS非常重要，也有一定的学习难度，同学们在这里花费的时间可能会有点多，JS主要包括语言基础（ECMAScript）、DOM 和 BOM。这个时候我们肯定不清楚这到底是是什么东西，可以用作什么，没关系，先学就是咯。</p><p>1、<a href="https://zh.javascript.info/">现代 JavaScript 教程</a>：JS 推荐从这个教程学起，这是我无意间发现的一个宝藏网站，这个网站是依托于 <a href="http://learn.javascript.ru/">learn.javascript.ru</a> 而来的（中文翻译的主要主要负责人是leviding，阿里前端工程师，很强），而 <a href="http://learn.javascript.ru/">learn.javascript.ru</a> 是俄罗斯最大的 JavaScript 教程和学习平台之一，质量有保障，而且是在线学习，教程都是实时更新且免费的，不像书籍可能存在看的技术过时的问题。现代 JavaScript 教程是 React 官方文档中与 MDN 并列推荐的 JavaScript 学习教程，学完整个教程，相当于已经学完了所有JavaScript内容。</p><p>2、《JavaScript 高级程序设计（第 4 版）》：这本是速成红宝书，很经典，推荐先看现代 JavaScript 教程再看这本红宝书，或者搭配着看。红宝书可以和现代 js 教程相互补充取长补短，同时还可以巩固知识点，加深对 JavaScript 的理解。书中有大量 API 的章节，不用死记硬背，浏览一下就行，用到再回来查。</p><p>（1）第 1~11 章讲的是语言特性，必须掌握；</p><p>（2）第 12、14-17 和 23-26 章是重点内容重点看；</p><p>（3）第 13、19 和 21 章是次重点内容，尽量看一下；</p><p>（4）第 28 章是最佳实践，看完前面内容后来学习如何写出更好的代码；</p><p>（5）第 18 章 Canvas、第 20 章 API、第 22 章 XML 和第 27 章工作者线程，可以在学有余力或者用到的时候看一下，不是初期重点内容。</p><p>3、《JavaScript DOM 编程艺术》：DOM 学习书籍，主要讲 DOM 知识，以及通过实战深入理解相关知识，其实这本书的内容现代JS和红宝书都讲过了，有时间的同学可以考虑看一下。</p><h3 id="--6"><strong>二、代码实战</strong></h3><p>又到了我们的代码实战环节，学以致用理论和实践相结合才是王道，这个时候fCC依然是我们的首选。</p><p>1、<a href="https://chinese.freecodecamp.org/learn/">freeCodeCamp</a>：练习“JavaScript 算法和数据结构”部分，做完后同样有5个练习项目实战，希望大家可以完成。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/09/image-5.png" class="kg-image" alt="image-5" width="600" height="400" loading="lazy"></figure><p>2、百度前端技术学院 2021：这是百度前几年推出的免费前端入门教程，里面有非常详细的HTML、CSS和JavaScript教程，不过现在正在内容更新，暂时还打不开，练习到这一步的同学发现还不能打开的话可以去Github上找（GitHub上有20年前的内容）</p><h2 id="--7"><strong>阶段三：入门前端框架</strong></h2><p>前端的主流框架目前主要为 React，Vue 和 Angular。一般是在 React 和 Vue 中选一个。</p><p>React 的开发体验更偏向于通过写原生 JS 的那种方式写页面，使得逻辑和页面分离开来，要求你有较好的 JavaScript 基础。</p><p>Vue 则引入了模版，相对来讲偏向通过模板的方式也就是调用 API 来写页面（当然现在 Vue 3.0 也正在跟进 Vue 的 JSX 写法，类似于 React），将很多实现封装成了 API，需要记住并调用 API 来进行开发，因为很多都是封装好的，所以学习起来较为简单，相对的编程的感觉会弱一些。</p><p>推荐先学React，对找第一份工作比较友好，下面是详细的教程</p><h3 id="-react-">一、React 学习 + 实战（推荐先学）</h3><p>在这个过程中你会学到很多知识，会用到 React 的高阶知识，把 React 官方文档中的高级指引，API Reference，Hooks 等部分的内容学完。</p><p>还会学到 React Router，Redux 等内容。会学到从零开发一个完整项目，从项目配置到组件拆分，到项目打包和上线的完整流程，还会学到 React 框架的一些底层源码级知识。</p><p>1、入门：入门推荐学习哔哩哔哩“<a href="https://www.bilibili.com/video/BV1wy4y1D7JT?spm_id_from=333.999.0.0">尚硅谷2021版React技术全家桶全套</a>”视频，播放量很高，老师讲解的不错，可以用来入门学习。</p><p>2、<a href="https://react.docschina.org/docs/getting-started.html">React 官方文档</a>：先学习文档中核心概念部分，然后跟着官方文档写出文档中的井字棋，最后再自己写一个 Todo App。</p><p>3、慕课网：下面到了实战环节，可以考虑慕课网的付费课程“<a href="https://coding.imooc.com/class/package/475.html#Anchor">React 17 系统精讲 结合TS打造旅游电商平台</a>”，价格不高，既可以当做代码练习，又可以作为简历上的项目点。</p><h3 id="-vue-">二、Vue 学习 + 实战</h3><p>1、<a href="https://cn.vuejs.org/v2/guide/">Vue 官方文档</a></p><p>2、慕课网：</p><p>（1） <a href="https://www.imooc.com/learn/980">Vue 2.5 入门</a></p><p>（2） <a href="https://coding.imooc.com/class/203.html">Vue 2.5 -&gt; 2.6 -&gt; 3.0 开发去哪儿网 App 从零基础入门到实战项目开发</a></p><h2 id="--8">阶段四：项目实战</h2><p>恭喜你，能够学习到这步，离找工作就不远了，现在就是利用时间多做几个项目，放到个人简历上，可以做个自己的个人博客，租个服务器上线，这样的优势是博客风格完全由你个人来打造，可以让面试官看到你的实战能力，且不同于常见的电商网页项目（主要这个大家都在做）。</p><p>1、独立完成个人博客的上线</p><p>2、自己选择做其他项目</p><h2 id="--9">阶段五：刷算法题 + 面试题</h2><p>到这步就可以刷刷面试题找工作了，推荐以下刷题工具：</p><p>1、小程序：高级前端面试，特别推荐，题型比较新颖，题库里的题也适合面试梳理，选择题不算特别多。</p><p>2、小程序：前端面试星球，有些题型有点多，涉及的面挺广的，可以后面慢慢刷。</p><p>3、牛客网：题型偏老，有时间可以刷刷。</p><p>谢谢你阅读这篇文章，Happy coding!</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Chrome 开发者工具有哪些前端实用功能 ]]>
                </title>
                <description>
                    <![CDATA[ Chrome 开发者工具是谷歌浏览器 Chrome 内置的一套 Web 开发和调试工具，可用于对网站进行迭代、调试和分析。 可通过三种方式打开 Chrome 开发者工具：  * 在 Chrome 菜单中选择更多工具 > 开发者工具  * 在页面元素上右键点击，选择 “检查”  * 使用快捷键 Ctrl+Shift+I（Windows）或 Cmd+Opt+I（Mac） 本文将总结介绍 Chrome 开发者工具的一些前端实用功能。 1. 查看元素伪类 css 样式 例如我想查看元素触发 hover 时的 css 样式。先选中该元素，然后按下图操作： 2. 临时增删元素 class 3. document.body.contentEditable="true" 在控制台输入 document.body.contentEditable="true"，就可以对页面直接进行编辑。 4. 查看 placeholder 样式 现在可以查看元素的 placeholder ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-functions-of-chrome-dev-tool/</link>
                <guid isPermaLink="false">5fb5e80139641a0517d50f66</guid>
                
                    <category>
                        <![CDATA[ Chrome ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 谷歌 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Mon, 02 Aug 2021 10:32:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2020/11/aditya-chinchure--39OrtuhSTc-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Chrome 开发者工具是谷歌浏览器 Chrome 内置的一套 Web 开发和调试工具，可用于对网站进行迭代、调试和分析。</p><p>可通过三种方式打开 Chrome 开发者工具：</p><ul><li>在 Chrome 菜单中选择<strong><strong>更多工具</strong></strong> &gt; <strong><strong>开发者工具</strong></strong></li><li>在页面元素上右键点击，选择 “检查”</li><li>使用快捷键 Ctrl+Shift+I（Windows）或 Cmd+Opt+I（Mac）</li></ul><p>本文将总结介绍 Chrome 开发者工具的一些前端实用功能。</p><h3 id="1-css-">1. 查看元素伪类 css 样式</h3><p>例如我想查看元素触发 <code>hover</code> 时的 css 样式。先选中该元素，然后按下图操作：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-29.png" class="kg-image" alt="image-29" width="517" height="294" loading="lazy"></figure><h3 id="2-class">2. 临时增删元素 class</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/1.gif" class="kg-image" alt="1" width="710" height="188" loading="lazy"></figure><h3 id="3-document-body-contenteditable-true">3. <code>document.body.contentEditable="true"</code></h3><p>在控制台输入 <code>document.body.contentEditable="true"</code>，就可以对页面直接进行编辑。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/2.gif" class="kg-image" alt="2" width="745" height="252" loading="lazy"></figure><h3 id="4-placeholder-">4. 查看 placeholder 样式</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-31.png" class="kg-image" alt="image-31" width="440" height="616" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-33.png" class="kg-image" alt="image-33" width="707" height="350" loading="lazy"></figure><p>现在可以查看元素的 placeholder 样式了：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-34.png" class="kg-image" alt="image-34" width="929" height="315" loading="lazy"></figure><h3 id="5-seo">5. 测试页面性能和 SEO</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-35.png" class="kg-image" alt="image-35" width="902" height="374" loading="lazy"></figure><p>下面是测试报告：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-36.png" class="kg-image" alt="image-36" width="771" height="365" loading="lazy"></figure><p>参考资料：</p><ul><li><a href="https://developers.google.com/web/tools/lighthouse" rel="nofollow noreferrer">使用 Lighthouse 审查网络应用</a></li></ul><h3 id="6-network-">6. Network 显示资源的其他信息</h3><p>一般 Network 会显示加载资源的详细信息，但它默认只显示部分信息。如果我想查询网页资源是通过 HTTP1.1 还是 HTTP2 加载的，要怎么做呢？</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/3.gif" class="kg-image" alt="3" width="792" height="498" loading="lazy"></figure><p>从 GIF 中可以看出，除了 HTTP 协议版本外，还可以查看其他信息，例如 HTTP 请求的方法、域名等等。</p><h3 id="7-">7. 查看元素绑定事件</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-37.png" class="kg-image" alt="image-37" width="909" height="391" loading="lazy"></figure><p>鼠标移到 <code>handler</code> 上，可查看具体的函数代码。</p><h3 id="8-">8. 全局搜索代码</h3><p>打开开发者工具，点击 <code>Console</code> 标签，按 ESC 弹出，点击左边竖形排列的三个小点，选择 <code>Search</code>：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-38.png" class="kg-image" alt="image-38" width="766" height="514" loading="lazy"></figure><p>点击搜索结果，会跳到具体的源码文件。它会搜索该网页下所有引入的文件。</p><h3 id="9-performance-">9. 利用 Performance 检查运行时性能</h3><p>打开开发者工具，点击 <code>Performance</code> 标签：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-39.png" class="kg-image" alt="image-39" width="837" height="132" loading="lazy"></figure><p>点击左上角的 <code>Record</code> 按钮开始记录，然后你模拟正常用户使用网页。测试完毕后，点击 <code>Stop</code>。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-40.png" class="kg-image" alt="image-40" width="925" height="864" loading="lazy"></figure><p>可以看到右上角分别有 FPS、CPU、NET、HEAP：</p><ol><li>FPS 对应的是帧率，红色代表帧率低，可能会降低用户体验；绿色代表帧率正常，绿色条越高，FPS 越高。</li><li>CPU 部分上有黄色、紫色等色块，它们的释义请看图的左下角。谁的占比高，说明 CPU 主要的时间花在哪里。</li><li>HEAP 就是堆内存占用。</li></ol><p>NET 最好点击下面的 Network 查看，可以看到具体的加载资源等。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-41.png" class="kg-image" alt="image-41" width="942" height="169" loading="lazy"></figure><p>一般根据这些信息就能判断出网页性能问题出在哪。</p><p>如果想了解更多，请查看下面的参考资料，需要翻 qiang。或者用搜索引擎搜索 chrome performance，也有很多讲解使用方法的文章。</p><p>参考资料</p><ul><li><a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance" rel="nofollow noreferrer">Get Started With Analyzing Runtime Performance</a></li></ul><h3 id="10-rendering-">10. Rendering 实时检测网页变化</h3><p>打开开发者工具，点击 <code>Console</code> 标签，按 ESC 弹出：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-43.png" class="kg-image" alt="image-43" width="1012" height="39" loading="lazy"></figure><p>点击左边竖形排列的三个小点，选择 <code>Rendering</code>：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-42.png" class="kg-image" alt="image-42" width="651" height="486" loading="lazy"></figure><p>下面是比较实用的功能：</p><ol><li>Paint flashing，实时高亮重绘区域（绿色）。</li><li>Layout Shift Regions，实时高亮重排（重新布局）区域（蓝色）。</li><li>Layer borders，将合成层用边框标出来（橙色、橄榄色、青色）。</li><li>Frame Rendering Stats，显示 GPU 的信息，旧版本还有实时 FPS 显示，但新版本不知道为何没有（chrome 86）。</li></ol><h3 id="11-application-">11. Application 查看应用信息</h3><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-44.png" class="kg-image" alt="image-44" width="566" height="593" loading="lazy"></figure><p>从图中看到，在 <code>Application</code> 标签下可以查到本页面很多信息。拿 <code>localStorage</code> 举例，现在我执行代码 <code>localStorage.setItem('token', '123')</code>，然后打开 <code>Application</code>：</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2020/11/image-45.png" class="kg-image" alt="image-45" width="746" height="213" loading="lazy"></figure><p>不出意外，能看到新增的 <code>localStorage</code> 信息。</p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 带你入门前端工程（七）：前端监控 ]]>
                </title>
                <description>
                    <![CDATA[ 什么时候需要监控  1. 当你的应用频繁报错找不到原因的时候。  2. 需要分析用户兴趣爱好、购买习惯。  3. 需要优化程序的时候，可以做监控收集数据，做针对性的优化。  4. 需要保证服务可靠性稳定性。 如果你的应用符合以上任意一条，就可以对应用实行监控了。监控的作用有两个：事前预警和事后分析。 事前预警：提前设置一个阈值，当监控的数据达到阈值时，通过短信或者邮件通知管理员。例如 API 请求数量突然间暴涨，就得进行报警，否则可能会造成服务器宕机。 事后分析：通过监控日志文件，分析故障原因和故障发生点。从而做出修改，防止这种情况再次发生。 本章内容分为前端监控原理分析和如何对项目实行监控两个部分。第一部分有三个小节：数据采集、数据上报、扩展；第二部分只有一个小节：如何使用 sentry [https://docs.sentry.io/] 实现项目监控。 好了，下面让我们开始进入正文吧。 数据采集 性能数据采集 性能数据采集需要使用 window.performance [https://developer.mozilla.org/zh-CN/docs/Web/API/P ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-engineering-monitoring/</link>
                <guid isPermaLink="false">60ed53ffcffc7f0618708c4a</guid>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Tue, 13 Jul 2021 08:00:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/07/jefferson-santos-9SoCnyQmkzI-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h2 id="-">什么时候需要监控</h2><ol><li>当你的应用频繁报错找不到原因的时候。</li><li>需要分析用户兴趣爱好、购买习惯。</li><li>需要优化程序的时候，可以做监控收集数据，做针对性的优化。</li><li>需要保证服务可靠性稳定性。</li></ol><p>如果你的应用符合以上任意一条，就可以对应用实行监控了。监控的作用有两个：事前预警和事后分析。</p><p><strong>事前预警</strong>：提前设置一个阈值，当监控的数据达到阈值时，通过短信或者邮件通知管理员。例如 API 请求数量突然间暴涨，就得进行报警，否则可能会造成服务器宕机。</p><p><strong>事后分析</strong>：通过监控日志文件，分析故障原因和故障发生点。从而做出修改，防止这种情况再次发生。</p><p>本章内容分为前端监控原理分析和如何对项目实行监控两个部分。第一部分有三个小节：数据采集、数据上报、扩展；第二部分只有一个小节：如何使用 <a href="https://docs.sentry.io/" rel="noopener noreferrer">sentry</a> 实现项目监控。</p><p>好了，下面让我们开始进入正文吧。</p><h2 id="--1">数据采集</h2><h3 id="--2">性能数据采集</h3><p>性能数据采集需要使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Performance" rel="noopener noreferrer">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://chinese.freecodecamp.org/news/content/images/2021/07/image-18.png" class="kg-image" alt="image-18" 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>，它指从输入网址，到页面开始显示内容的时间。</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><h3 id="--3">小结</h3><p>通过对性能及资源信息的解读，我们可以判断出页面加载慢有以下几个原因：</p><ol><li>资源过多、过大</li><li>网速过慢</li><li>DOM 元素过多</li></ol><p>除了用户网速过慢，我们没办法之外，其他两个原因都是有办法解决的，关于如何做性能优化我们将在下一章学习。</p><p><strong>PS</strong>：其实页面加载慢还有其他原因，例如没有使用按需加载、没有使用 CDN 等等。不过在这里我们强调的是仅通过对性能和资源信息的解读来得知原因。</p><h3 id="--4">错误数据采集</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="--5">小结</h4><p>通过错误收集，可以了解到网站发生错误的类型及数量，从而做出相应的调整，以减少错误发生。</p><h2 id="--6">数据上报</h2><h3 id="--7">性能数据上报</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="--8">错误数据上报</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" rel="noopener noreferrer">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><h3 id="demo-">DEMO 代码</h3><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="--9">扩展</h2><h3 id="spa">SPA</h3><p><code>window.performance</code> API 是有缺点的，在 SPA 切换路由时，<code>window.performance.timing</code> 的数据不会更新。 所以我们需要另想办法来统计切换路由到加载完成的时间。 拿 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="--10">用户信息收集</h3><h4 id="navigator">navigator</h4><p>使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/navigator" rel="noopener noreferrer">window.navigator</a> 可以收集到用户的设备信息，操作系统，浏览器信息...</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/2f97f0e0e24d5391d9bc1ad1ef559ed5.png" class="kg-image" alt="2f97f0e0e24d5391d9bc1ad1ef559ed5" width="600" height="400" loading="lazy"></figure><h4 id="uv-unique-visitor-">UV（Unique visitor）</h4><p>是指通过互联网浏览这个网页的访客，00:00-24:00 内相同的设备访问只被计算一次。一天内同个访客多次访问仅计算一个 UV。</p><p>在用户访问网站时，可以生成一个随机字符串+时间日期，保存在本地。在网页发生请求时（如果超过当天24小时，则重新生成），把这些参数传到后端，后端利用这些信息生成 UV 统计报告。</p><h4 id="pv-page-view-">PV（Page View）</h4><p>即页面浏览量或点击量，用户每 1 次对网站中的每个网页访问均被记录 1 个PV。用户对同一页面的多次访问，访问量累计，用以衡量网站用户访问的网页数量。</p><h4 id="--11">页面停留时间</h4><h5 id="--12">传统网站</h5><p>用户在进入 A 页面时，通过后台请求把用户进入页面的时间捎上。过了 10 分钟，用户进入 B 页面，这时后台可以通过接口捎带的参数可以判断出用户在 A 页面停留了 10 分钟。</p><h5 id="spa-1">SPA</h5><p>可以利用 router 来获取用户停留时间，拿 Vue 举例，通过 <code>router.beforeEach</code>、<code>destroyed</code> 这两个钩子函数来获取用户停留该路由组件的时间。</p><h4 id="--13">浏览深度</h4><p>通过 <code>document.documentElement.scrollTop</code> 属性以及屏幕高度，可以判断用户是否浏览完网站内容。</p><h4 id="--14">页面跳转来源</h4><p>通过 <code>document.referrer</code> 属性，可以知道用户是从哪个网站跳转而来。</p><h3 id="--15">小结</h3><p>通过分析用户数据，我们可以了解到用户的浏览习惯、爱好等等信息，想想真是恐怖，毫无隐私可言。</p><h2 id="--16">前端监控部署</h2><p>前面说的都是监控原理，但要实现还是得自己动手写代码。为了避免麻烦，我们可以用现有的工具 <a href="https://docs.sentry.io/" rel="noopener noreferrer">sentry</a> 去做这件事。</p><p>sentry 是一个用 python 写的性能和错误监控工具，你可以使用 sentry 提供的服务（免费功能少），也可以自己部署服务。现在来看一下如何使用 sentry 提供的服务实现监控。</p><h3 id="--17">注册账号</h3><p>打开 <code>https://sentry.io/signup/</code> 网站，进行注册。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/39d89dcae02f4762c778c3a1d0245c6b.png" class="kg-image" alt="39d89dcae02f4762c778c3a1d0245c6b" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/3096c2c57c2964ab86fb08a685267f23.png" class="kg-image" alt="3096c2c57c2964ab86fb08a685267f23" width="600" height="400" loading="lazy"></figure><p>选择项目，这里用 Vue 做示例。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/89ce497139633f55fe1ca87ac809b4f5.png" class="kg-image" alt="89ce497139633f55fe1ca87ac809b4f5" width="600" height="400" loading="lazy"></figure><h3 id="-sentry-">安装 sentry 依赖</h3><p>选完项目，下面会有具体的 sentry 依赖安装指南。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/630d261f7fc6b097ab7ebc7c5ef0d3b6.png" class="kg-image" alt="630d261f7fc6b097ab7ebc7c5ef0d3b6" width="600" height="400" loading="lazy"></figure><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><h3 id="--18">创建第一个错误</h3><p>在你的 Vue 项目执行一个打印语句 <code>console.log(b)</code>。</p><p>这时点开 sentry 主页的 issues 一项，可以发现有一个报错信息 <code>b is not defined</code>：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/cab799f4c03a2b5f835cc26b97be0dd2.png" class="kg-image" alt="cab799f4c03a2b5f835cc26b97be0dd2" width="600" height="400" loading="lazy"></figure><p>这个报错信息包含了错误的具体信息，还有你的 IP、浏览器信息等等。</p><p>但奇怪的是，我们的浏览器控制台并没有输出报错信息。</p><p>这是因为被 sentry 屏蔽了，所以我们需要加上一个选项 <code>logErrors: true</code>。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/f49a1653cc2512a09da56928e97cbd5f.png" class="kg-image" alt="f49a1653cc2512a09da56928e97cbd5f" 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/3863d2ccca797f7ec2a2cb7bcad26299.png" class="kg-image" alt="3863d2ccca797f7ec2a2cb7bcad26299" width="600" height="400" loading="lazy"></figure><h3 id="-sourcemap">上传 sourcemap</h3><p>一般打包后的代码都是经过压缩的，如果没有 sourcemap，即使有报错信息，你也很难根据提示找到对应的源码在哪。</p><p>下面来看一下如何上传 sourcemap。</p><p>首先创建 auth token。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/6c0057d0deb1ce57b5e845d41f5c49d4.png" class="kg-image" alt="6c0057d0deb1ce57b5e845d41f5c49d4" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/b493b5b6405c5a5c7ad68969f81895ec.png" class="kg-image" alt="b493b5b6405c5a5c7ad68969f81895ec" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/31dfb7a9565f2f618843822866993729.png" class="kg-image" alt="31dfb7a9565f2f618843822866993729" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/cf0a7ce7dd5ed7f68cd99a75b555b373.png" class="kg-image" alt="cf0a7ce7dd5ed7f68cd99a75b555b373" width="600" height="400" loading="lazy"></figure><p>这个生成的 token 一会要用到。</p><p>安装 <code>sentry-cli</code> 和 <code>@sentry/webpack-plugin</code>：</p><pre><code class="language-text">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 class="language-text">[auth]
token=xxx

[defaults]
url=https://sentry.io/
org=woai3c
project=woai3c
</code></pre><p>把 xxx 替换成刚才生成的 token。</p><p><code>org</code> 是你的组织名称。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/1d584d4095b75d4e6d9feaa8193b31d5.png" class="kg-image" alt="1d584d4095b75d4e6d9feaa8193b31d5" width="600" height="400" loading="lazy"></figure><p><code>project</code> 是你的项目名称，根据下面的提示可以找到。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/8c42a030a12aff493b950622207ea9bc.png" class="kg-image" alt="8c42a030a12aff493b950622207ea9bc" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/d9048286c883320243e69468f501617c.png" class="kg-image" alt="d9048286c883320243e69468f501617c" width="600" height="400" loading="lazy"></figure><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><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/0c228c1a8c9df7b22e1edc9928797d52.png" class="kg-image" alt="0c228c1a8c9df7b22e1edc9928797d52" width="600" height="400" loading="lazy"></figure><p>我们再来看一下没上传 sourcemap 和上传之后的报错信息对比。</p><p><strong>未上传 sourcemap</strong></p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/5010c62af2c3ff761b20180809037d49.png" class="kg-image" alt="5010c62af2c3ff761b20180809037d49" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/56bde1012cd1bdd67cc928847d760c57.png" class="kg-image" alt="56bde1012cd1bdd67cc928847d760c57" width="600" height="400" loading="lazy"></figure><p><strong>已上传 sourcemap</strong></p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/75c92e05d69c152aa0d694a3370537fe.png" class="kg-image" alt="75c92e05d69c152aa0d694a3370537fe" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/65a59d58fe01d1c15621e287cf53ef47.png" class="kg-image" alt="65a59d58fe01d1c15621e287cf53ef47" width="600" height="400" loading="lazy"></figure><p>可以看到，上传 sourcemap 后的报错信息更加准确。</p><h3 id="--19">切换中文环境和时区</h3><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/0ea958f0b5f77979489f9bc2a2558e65.png" class="kg-image" alt="0ea958f0b5f77979489f9bc2a2558e65" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/b5230bf8da8c86eb46a1b880dfae462c.png" class="kg-image" alt="b5230bf8da8c86eb46a1b880dfae462c" width="600" height="400" loading="lazy"></figure><p>选完刷新即可。</p><h3 id="--20">性能监控</h3><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/72c4c1464e2510e6951a002a8208a30a.png" class="kg-image" alt="72c4c1464e2510e6951a002a8208a30a" width="600" height="400" loading="lazy"></figure><p>打开 performance 选项，就能看到你每个项目的运行情况。具体的参数解释请看文档 <a href="https://docs.sentry.io/product/performance/" rel="noopener noreferrer">Performance Monitoring</a>。</p><h2 id="--21">小结</h2><p>随着 web 技术的发展，现在前端项目的规模也越来越大。在监控系统的帮助下，我们可以更加清楚的了解项目的运行情况，根据采集到的错误数据和性能数据对项目做针对性的优化。</p><p>下一章我们将讲解如何做性能优化。</p><h3 id="--22">参考资料</h3><ul><li><a href="https://fex.baidu.com/blog/2014/05/build-performance-monitor-in-7-days/" rel="noopener noreferrer">7 天打造前端性能监控系统</a></li><li><a href="https://github.com/wangweianger/zanePerfor" rel="noopener noreferrer">zanePerfor</a></li><li><a href="https://docs.sentry.io/" rel="noopener noreferrer">sentry</a></li></ul><p><br></p><p><br></p><p><br></p><p><br></p><p><br></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 带你入门前端工程（六）：自动化部署 ]]>
                </title>
                <description>
                    <![CDATA[ 部署是指将代码发布到服务器的一种行为。自动化部署就是使用工具来帮助你实现部署的过程，无需亲自动手。 在没有学会自动化部署前，我是这样部署项目的：  1. 执行测试 npm run test。  2. 构建项目 npm run build。  3. 将打包好的文件放到静态服务器。 偶尔一次两次还行，如果每次部署项目都这样，就会把很多时间浪费在重复的操作上。所以我们要学会自动部署，彻底解放双手。 自动部署（又叫持续部署 Continuous Deployment，英文缩写 CD）一般有两种触发方式：  1. 定时触发。  2. 监听 webhook 事件，事件触发时执行自动打包、部署等操作。 定时触发 定时触发，就是构建软件每隔一段时间自动执行打包、部署操作。 这种方式不太好，很有可能软件刚部署完开发就改代码了。为了看到新的页面效果，不得不等到下一次构建开始。另外还有一个副作用，假如开发一天都没更改代码，构建软件还是会不停的执行打包、部署操作，白白的浪费资源。 所以现在基本都是采用监听 webhook 事件的方式来进行部署。 监听 webhook 事件 webhook 钩 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-engineering-automatic-deployment/</link>
                <guid isPermaLink="false">60e6a5570f76ab0660b142a3</guid>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Thu, 08 Jul 2021 07:10:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/07/dean-pugh-C8NDn4xk9zs-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>部署是指将代码发布到服务器的一种行为。自动化部署就是使用工具来帮助你实现部署的过程，无需亲自动手。</p><p>在没有学会自动化部署前，我是这样部署项目的：</p><ol><li>执行测试 <code>npm run test</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>所以现在基本都是采用监听 <code>webhook</code> 事件的方式来进行部署。</p><h3 id="-webhook-">监听 <code>webhook</code> 事件</h3><p>webhook 钩子函数，就是在你的构建软件上进行设置，监听某一个事件（一般是监听 <code>push</code> 事件），当事件触发时，自动执行定义好的脚本。</p><p>例如 <code>Github Actions</code>，就有这个功能。</p><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-10.png" class="kg-image" alt="image-10" width="1568" height="412" loading="lazy"></figure><p>为了满足不同用户的需要，本章将使用 <a href="https://www.jenkins.io/zh/" rel="noopener noreferrer">Jenkins</a> 和 <a href="https://docs.github.com/en/free-pro-team@latest/actions" rel="noopener noreferrer">GitHub Actions</a> 来讲解如何部署前端项目。</p><ol><li>第一部分讲解如何使用 <a href="https://gitea.io/zh-cn/" rel="noopener noreferrer">Gitea</a> 配置局域网 git 服务器，再使用 Jenkins 将 Gitea 下的项目部署到局域网服务器。</li><li>第二部分讲解如何使用 GitHub Actions 将 GitHub 项目部署到 <a href="https://docs.github.com/cn/free-pro-team@latest/github/working-with-github-pages/about-github-pages" rel="noopener noreferrer">GitHub Page</a> 和阿里云服务器。</li></ol><p>阅读本章内容并不需要你提前了解 Jenkins 和 GitHub Actions 的知识（它们俩都是部署工具），只要按照本章内容的指引，就能够实现自动化部署项目。</p><p><strong>PS：</strong> 本人所用电脑操作系统为 windows，即以下所有的操作均在 windows 下运行。其他操作系统的配置大同小异，不会有太大差别。</p><h2 id="gitea-jenkins-">Gitea + Jenkins 自动构建前端项目并部署到服务器</h2><p>Gitea 用于构建 Git 局域网服务器，Jenkins 是 CI/CD 工具，用于部署前端项目。</p><h3 id="-gitea">配置 Gitea</h3><ul><li>下载 <a href="https://dl.gitea.io/gitea" rel="noopener noreferrer">Gitea</a>，选择一个喜欢的版本，例如 1.13，选择 <code>gitea-1.13-windows-4.0-amd64.exe</code> 下载。</li><li>下载完后，新建一个目录（例如 gitea），将下载的 Gitea 软件放到该目录下，双击运行。</li><li>打开 <code>localhost:3000</code> 就能看到 Gitea 已经运行在你的电脑上了。</li><li>点击注册，第一次会弹出一个初始配置页面，数据库选择 <code>SQLite3</code>。另外把 <code>localhost</code> 改成你电脑的局域网地址，例如我的电脑 IP 为 <code>192.168.0.118</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-11.png" class="kg-image" alt="image-11" width="977" height="930" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-12.png" class="kg-image" alt="image-12" width="568" height="542" loading="lazy"></figure><ul><li>填完信息后，点击立即安装，等待一会，即可完成配置。</li><li>继续点击注册用户，第一个注册的用户将会成会管理员。</li><li>打开 Gitea 的安装目录，找到 <code>custom\conf\app.ini</code>，在里面加上一行代码 <code>START_SSH_SERVER = true</code>。这时就可以使用 ssh 进行 push 操作了。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/7341d3013afc300dc2221926fdd045c4.png" class="kg-image" alt="在这里插入图片描述" width="661" height="319" loading="lazy"></figure><ul><li>如果使用 http 的方式无法克隆项目，请取消 git 代理。</li></ul><pre><code class="language-git">git config --global --unset http.proxy
git config --global --unset https.proxy
</code></pre><h3 id="-jenkins">配置 Jenkins</h3><ul><li>需要提前安装 JDK，JDK 安装教程网上很多，请自行搜索。</li></ul><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-13.png" class="kg-image" alt="image-13" width="499" height="714" loading="lazy"></figure><ul><li>打开 <a href="https://www.jenkins.io/zh/download/" rel="noopener noreferrer">Jenkins</a> 下载页面。</li><li>安装过程中遇到 <code>Logon Type</code> 时，选择第一个。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/0d5924e243ee6b52735b0472d76e7d22.png" class="kg-image" alt="在这里插入图片描述" width="373" height="161" loading="lazy"></figure><ul><li>端口默认为 8080，这里我填的是 8000。安装完会自动打开 <code>http://localhost:8000</code> 网站，这时需要等待一会，进行初始化。</li><li>按照提示找到对应的文件（直接复制路径在我的电脑中打开），其中有管理员密码。</li></ul><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-14.png" class="kg-image" alt="image-14" width="1026" height="918" loading="lazy"></figure><ul><li>安装插件，选择第一个。</li></ul><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-15.png" class="kg-image" alt="image-15" width="856" height="609" loading="lazy"></figure><ul><li>创建管理员用户，点击完成并保存，然后一路点击下一步。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/0f17c8039adf82a5626155a0c25b8c99.png" class="kg-image" alt="在这里插入图片描述" width="994" height="894" loading="lazy"></figure><ul><li>配置完成后自动进入首页，这时点击 <code>Manage Jenkins</code> -&gt; <code>Manage plugins</code> 安装插件。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/65c8615d54c869beb9e411cb4111064e.png" class="kg-image" alt="在这里插入图片描述" width="896" height="669" loading="lazy"></figure><ul><li>点击 <code>可选插件</code>，输入 nodejs，搜索插件，然后安装。</li><li>安装完成后回到首页，点击 <code>Manage Jenkins</code> -&gt; <code>Global Tool Configuration</code> 配置 nodejs。如果你的电脑是 win7 的话，nodejs 版本最好不要太高，选择 v12 左右的就行。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/dd0e1a39d6b009805e5e5a1edbd1d532.png" class="kg-image" alt="在这里插入图片描述" width="1566" height="749" loading="lazy"></figure><h3 id="--1">创建静态服务器</h3><ul><li>建立一个空目录，在里面执行 <code>npm init -y</code>，初始化项目。</li><li>执行 <code>npm i express</code> 下载 express。</li><li>然后建立一个 <code>server.js</code> 文件，代码如下：</li></ul><pre><code class="language-js">const express = require('express')
const app = express()
const port = 8080

app.use(express.static('dist'))

app.listen(port, () =&gt; {
    console.log(`Example app listening at http://localhost:${port}`)
})
</code></pre><p>它将当前目录下的 <code>dist</code> 文件夹设为静态服务器资源目录，然后执行 <code>node server.js</code> 启动服务器。</p><p>由于现在没有 <code>dist</code> 文件夹，所以访问网站是空页面。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/09fa82ba4e9f180b1f2098dbac533987.png" class="kg-image" alt="在这里插入图片描述" width="572" height="253" loading="lazy"></figure><p>不过不要着急，一会就能看到内容了。</p><h3 id="--2">自动构建 + 部署到服务器</h3><ul><li>下载 Jenkins 提供的 demo 项目 <a href="https://github.com/jenkins-docs/building-a-multibranch-pipeline-project" rel="noopener noreferrer">building-a-multibranch-pipeline-project</a>，然后在你的 Gitea 新建一个仓库，把内容克隆进去，并提交到 Gitea 服务器。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/66301120ef722a758c96544972879d98.png" class="kg-image" alt="在这里插入图片描述" width="1167" height="852" loading="lazy"></figure><ul><li>打开 Jenkins 首页，点击 <code>新建 Item</code> 创建项目。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/0603f9492264f3765c72352b505a76c0.png" class="kg-image" alt="在这里插入图片描述" width="1478" height="850" loading="lazy"></figure><ul><li>选择<code>源码管理</code>，输入你的 Gitea 上的仓库地址。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/2ee4502e001d2d26ce1125f00e201922.png" class="kg-image" alt="在这里插入图片描述" width="1524" height="704" loading="lazy"></figure><ul><li>你也可以尝试一下定时构建，下面这个代码表示每 5 分钟构建一次。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/db7703be7640cdab042ee338156b44f2.png" class="kg-image" alt="在这里插入图片描述" width="770" height="413" loading="lazy"></figure><ul><li>选择你的构建环境，这里选择刚才配置的 nodejs。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/337e1f611e82089618ca2e65da480353.png" class="kg-image" alt="在这里插入图片描述" width="1064" height="660" loading="lazy"></figure><ul><li>点击增加构建步骤，windows 要选 <code>execute windows batch command</code>，linux 要选 <code>execute shell</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/dbe1cb8cbf12889ec1cfa1d04809da67.png" class="kg-image" alt="dbe1cb8cbf12889ec1cfa1d04809da67" width="417" height="352" loading="lazy"></figure><ul><li>输入 <code>npm i &amp;&amp; npm run build &amp;&amp; xcopy .\build\* G:\node-server\dist\ /s/e/y</code>，这行命令的作用是安装依赖，构建项目，并将构建后的静态资源复制到指定目录 <code>G:\node-server\dist\</code>。这个目录是静态服务器资源目录。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/bcab4399bd7dc47f075335d1d36ddeb9.png" class="kg-image" alt="在这里插入图片描述" width="1462" height="470" loading="lazy"></figure><ul><li>保存后，返回首页。点击项目旁边的小三角，选择 <code>build now</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/77f7b001b033f58566d2c92052eca8f1.png" class="kg-image" alt="在这里插入图片描述" width="964" height="682" loading="lazy"></figure><ul><li>开始构建项目，我们可以点击项目查看构建过程。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/4f9c1519e6c7477219b1328c18feeff5.png" class="kg-image" alt="在这里插入图片描述" width="903" height="621" loading="lazy"></figure><ul><li>构建成功，打开 <code>http://localhost:8080/</code> 看一下结果。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/eea4a4ba2539ad68aa6c6ecdfb492795.png" class="kg-image" alt="在这里插入图片描述" width="590" height="327" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/7bc8d9c591c13a24d6b79cc0bbf8098e.png" class="kg-image" alt="在这里插入图片描述" width="1302" height="315" loading="lazy"></figure><ul><li>由于刚才设置了每 5 分钟构建一次，我们可以改变一下网站的内容，然后什么都不做，等待一会再打开网站看看。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/189451220deee789c9fa2b3215bfc88c.png" class="kg-image" alt="在这里插入图片描述" width="1129" height="596" loading="lazy"></figure><ul><li>把修改的内容提交到 Gitea 服务器，稍等一会。打开网站，发现内容已经发生了变化。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/bf039b43f539faaef116f1a5f60e5332.png" class="kg-image" alt="在这里插入图片描述" width="390" height="279" loading="lazy"></figure><h3 id="-pipeline-">使用 pipeline 构建项目</h3><p>使用流水线构建项目可以结合 Gitea 的 <code>webhook</code> 钩子，以便在执行 <code>git push</code> 的时候，自动构建项目。</p><ul><li>点击首页右上角的用户名，选择<code>设置</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/72a973164babda14120a8d29bb64b45f.png" class="kg-image" alt="在这里插入图片描述" width="281" height="257" loading="lazy"></figure><ul><li>添加 token，记得将 token 保存起来。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/a6ef0405378c35e98fc3057dc27cf889.png" class="kg-image" alt="在这里插入图片描述" width="910" height="227" loading="lazy"></figure><ul><li>打开 Jenkins 首页，点击 <code>新建 Item</code> 创建项目。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/a29c8a8db7f0e8ae9ecf4a53b804cba2.png" class="kg-image" alt="在这里插入图片描述" width="1184" height="847" loading="lazy"></figure><ul><li>点击<code>构建触发器</code>，选择<code>触发远程构建</code>，填入刚才创建的 token。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/4282c6df05ede5ab492d019a30a9048a.png" class="kg-image" alt="在这里插入图片描述" width="902" height="461" loading="lazy"></figure><ul><li>选择流水线，按照提示输入内容，然后点击<code>保存</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/c37cfa2d2fe3ca9b368c8632dfcd2bca.png" class="kg-image" alt="在这里插入图片描述" width="1515" height="920" loading="lazy"></figure><ul><li>打开 Jenkins 安装目录下的 <code>jenkins.xml</code> 文件，找到 <code>&lt;arguments&gt;</code> 标签，在里面加上 <code>-Dhudson.security.csrf.GlobalCrumbIssuerConfiguration.DISABLE_CSRF_PROTECTION=true</code>。它的作用是关闭 <code>CSRF</code> 验证，不关的话，Gitea 的 <code>webhook</code> 会一直报 403 错误，无法使用。加好参数后，在该目录命令行下输入 <code>jenkins.exe restart</code> 重启 Jenkins。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/f49e549d2179e722e171a9846e6c7446.png" class="kg-image" alt="在这里插入图片描述" width="1193" height="357" loading="lazy"></figure><ul><li>回到首页，配置全局安全选项。勾上<code>匿名用户具有可读权限</code>，再保存。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/3c44a96e155811eb0f42894b8c52f5cc.png" class="kg-image" alt="在这里插入图片描述" width="720" height="582" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/44df9670cc57629a9ab9a5c50587c275.png" class="kg-image" alt="在这里插入图片描述" width="564" height="627" loading="lazy"></figure><ul><li>打开你的 Gitea 仓库页面，选择<code>仓库设置</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/d56a2b15d5823bcfe1dac9b25c567345.png" class="kg-image" alt="在这里插入图片描述" width="1226" height="695" loading="lazy"></figure><ul><li>点击<code>管理 web 钩子</code>，添加 web 钩子，钩子选项选择 <code>Gitea</code>。</li><li>目标 URL 按照 Jenkins 的提示输入内容。然后点击<code>添加 web 钩子</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/7cb98bb692b33b47070880e118e86d2c.png" class="kg-image" alt="在这里插入图片描述" width="1202" height="163" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/ed4cb714b9942bb2e65da6e43d84a79f.png" class="kg-image" alt="在这里插入图片描述" width="1146" height="727" loading="lazy"></figure><ul><li>点击创建好的 web 钩子，拉到下方，点击测试推送。不出意外，应该能看到推送成功的消息，此时回到 Jenkins 首页，发现已经在构建项目了。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/c4a2b242a400eec32cc4962949f6ea77.png" class="kg-image" alt="在这里插入图片描述" width="1218" height="117" loading="lazy"></figure><ul><li>由于没有配置 <code>Jenkinsfile</code> 文件，此时构建是不会成功的。所以接下来需要配置一下 <code>Jenkinsfile</code> 文件。将以下代码复制到你 Gitea 项目下的 <code>Jenkinsfile</code> 文件。jenkins 在构建时会自动读取文件的内容执行构建及部署操作。</li></ul><pre><code class="language-text">pipeline {
    agent any
    stages {
        stage('Build') {
            steps {  // window 使用 bat， linux 使用 sh
                bat 'npm i'
                bat 'npm run build'
            }
        }
        stage('Deploy') {
            steps {
                bat 'xcopy .\\build\\* D:\\node-server\\dist\\ /s/e/y' // 这里需要改成你的静态服务器资源目录
            }
        }
    }
}
</code></pre><ul><li>每当你的 Gitea 项目执行 <code>push</code> 操作时，Gitea 都会通过 <code>webhook</code> 发送一个 post 请求给 Jenkins，</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/f1a96522b2b7580a880dfd596c16baf7.png" class="kg-image" alt="在这里插入图片描述" width="394" height="278" loading="lazy"></figure><p>让它执行构建及部署操作。</p><h3 id="--3">小结</h3><p>如果你的操作系统是 Linux，可以在 Jenkins 打包完成后，使用 ssh 远程登录到阿里云，将打包后的文件复制到阿里云上的静态服务器上，这样就能实现阿里云自动部署了。具体怎么远程登录到阿里云，请看下文中的 《GitHub Actions 部署到阿里云》 一节。</p><h2 id="github-actions-">GitHub Actions 自动构建前端项目并部署到服务器</h2><p>如果你的项目是 GitHub 项目，那么使用 GitHub Actions 也许是更好的选择。</p><h3 id="-github-page">部署到 GitHub Page</h3><p>接下来看一下如何使用 GitHub Actions 部署到 GitHub Page。</p><p>在你需要部署到 GitHub Page 的项目下，建立一个 yml 文件，放在 <code>.github/workflow</code> 目录下。你可以命名为 <code>ci.yml</code>，它类似于 Jenkins 的 <code>Jenkinsfile</code> 文件，里面包含的是要自动执行的脚本代码。</p><p>这个 yml 文件的内容如下：</p><pre><code class="language-yml">name: Build and Deploy
on: # 监听 master 分支上的 push 事件
  push:
    branches:
      - master
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest # 构建环境使用 ubuntu
    steps:
      - name: Checkout
        uses: actions/checkout@v2.3.1  
        with:
          persist-credentials: false

      - name: Install and Build # 下载依赖 打包项目
        run: |
          npm install
          npm run build

      - name: Deploy # 将打包内容发布到 github page
        uses: JamesIves/github-pages-deploy-action@3.7.1 # 使用别人写好的 actions
        with:  # 自定义环境变量
          ACCESS_TOKEN: ${{ secrets.VUE_ADMIN_TEMPLATE }} # VUE_ADMIN_TEMPLATE 是我的 secret 名称，需要替换成你的
          BRANCH: master
          FOLDER: dist
          REPOSITORY_NAME: woai3c/woai3c.github.io # 这是我的 github page 仓库
          TARGET_FOLDER: github-actions-demo # 打包的文件将放到静态服务器 github-actions-demo 目录下

</code></pre><p>上面有一个 <code>ACCESS_TOKEN</code> 变量需要自己配置。</p><ul><li>打开 GitHub 网站，点击你右上角的头像，选择 <code>settings</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/b1e438b04781ae3624037c0b2626170c.png" class="kg-image" alt="在这里插入图片描述" width="270" height="466" loading="lazy"></figure><ul><li>点击左下角的 <code>developer settings</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/0087e15148772eb839bc6da983a194a1.png" class="kg-image" alt="在这里插入图片描述" width="363" height="235" loading="lazy"></figure><ul><li>在左侧边栏中，单击 <code>Personal access tokens（个人访问令牌）</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/48a06f652f7fca7b17ceb159541665b2.png" class="kg-image" alt="在这里插入图片描述" width="249" height="149" loading="lazy"></figure><ul><li>单击 <code>Generate new token（生成新令牌）</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/cceba0391821177f9cbe2c392c44612a.png" class="kg-image" alt="在这里插入图片描述" width="748" height="80" loading="lazy"></figure><ul><li>输入名称并勾选 <code>repo</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/1f5ee8125def6be5268bd3723eee8caa.png" class="kg-image" alt="在这里插入图片描述" width="681" height="765" loading="lazy"></figure><ul><li>拉到最下面，点击 <code>Generate token</code>，并将生成的 token 保存起来。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/26c3866b116dc16a7874e1937ad6605d.png" class="kg-image" alt="在这里插入图片描述" width="451" height="121" loading="lazy"></figure><ul><li>打开你的 GitHub 项目，点击 <code>settings</code>。</li></ul><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/dabc4cb8a58146cd01b763165ddbc410.png" class="kg-image" alt="在这里插入图片描述" width="1290" height="305" loading="lazy"></figure><p>点击 <code>secrets</code>-&gt;<code>new secret</code>。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/c700637ffded897f961256cf8bf4a341.png" class="kg-image" alt="在这里插入图片描述" width="357" height="566" loading="lazy"></figure><p>创建一个密钥，名称随便填（中间用下划线隔开），内容填入刚才创建的 token。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/cade22d32370466387bfff56747a2abf.png" class="kg-image" alt="在这里插入图片描述" width="934" height="464" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/bdc53673402bd57a5712e96eb0ff5404.png" class="kg-image" alt="在这里插入图片描述" width="921" height="76" loading="lazy"></figure><p><br>将上文代码中的:</p><pre><code class="language-yml">ACCESS_TOKEN: ${{ secrets.VUE_ADMIN_TEMPLATE }}
</code></pre><p>替换成刚才创建的 secret 名字，替换后代码如下:</p><pre><code class="language-yml">ACCESS_TOKEN: ${{ secrets.TEST_A_B }}
</code></pre><p>保存后，提交到 GitHub。</p><p>以后你的项目只要执行 <code>git push</code>，GitHub Actions 就会自动构建项目并发布到你的 GitHub Page 上。</p><p>GitHub Actions 的执行详情点击仓库中的 <code>Actions</code> 选项查看。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/be472567962cbd9599f600e072ee3ee5.png" class="kg-image" alt="在这里插入图片描述" width="1659" height="547" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/b12ccc45ce168c8381231abea78553dd.png" class="kg-image" alt="在这里插入图片描述" width="765" height="475" loading="lazy"></figure><p>具体详情可以参考一下我的 demo 项目 <strong><a href="https://github.com/woai3c/github-actions-demo" rel="noopener noreferrer">github-actions-demo</a></strong>。</p><p>构建成功后，打开 GitHub Page 网站，可以发现内容已经发布成功。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/ff5f3280dbe6bde247af8d34231b2eda.png" class="kg-image" alt="在这里插入图片描述" width="1288" height="792" loading="lazy"></figure><h3 id="github-actions--1">GitHub Actions 部署到阿里云</h3><h4 id="--4">初始化阿里云服务器</h4><ol><li>购买阿里云服务器，选择操作系统，我选的 ubuntu</li><li>在云服务器管理控制台选择实例-&gt;更多-&gt;密钥-&gt;重置实例密码（一会登陆用）</li><li>选择远程连接-&gt;VNC，会弹出一个密码，记住它，以后远程连接要用（ctrl + alt + f1~f6 切换终端，例如 ctrl + alt + f1 是第一个终端）</li><li>进入后是一个命令行 输入 <code>root</code>（默认用户名），密码为你刚才重置的实例密码</li><li>登陆成功， 更新安装源 <code>sudo apt-get update &amp;&amp; sudo apt-get upgrade -y</code></li><li>安装 npm <code>sudo apt-get install npm</code></li><li>安装 npm 管理包 <code>sudo npm install -g n</code></li><li>安装 node 最新稳定版 <code>sudo n stable</code></li></ol><h4 id="--5">创建一个静态服务器</h4><pre><code class="language-js">mkdir node-server // 创建 node-server 文件夹
cd node-server // 进入 node-server 文件夹
npm init -y // 初始化项目
npm i express
touch server.js // 创建 server.js 文件
vim server.js // 编辑 server.js 文件
</code></pre><p>将以下代码输入进去（用 vim 进入文件后按 i 进行编辑，保存时按 esc 然后输入 :wq，再按 enter），更多使用方法请自行搜索。</p><pre><code class="language-js">const express = require('express')
const app = express()
const port = 3388 // 填入自己的阿里云映射端口，在网络安全组配置。

app.use(express.static('dist'))

app.listen(port, '0.0.0.0', () =&gt; {
    console.log(`listening`)
})
</code></pre><p>执行 <code>node server.js</code> 开始监听，由于暂时没有 <code>dist</code> 目录，先不要着急。</p><p>注意，监听 IP 必须为 <code>0.0.0.0</code> ，详情请看<a href="https://www.alibabacloud.com/help/zh/doc-detail/50775.htm" rel="noopener noreferrer">部署 Node.js 项目注意事项</a>。</p><p>阿里云入端口要在网络安全组中查看与配置。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/0814d06c8887e7e2b110388619b480ba.png" class="kg-image" alt="在这里插入图片描述" width="438" height="424" loading="lazy"></figure><h4 id="--6">创建阿里云密钥对</h4><p>请参考<a href="https://www.alibabacloud.com/help/zh/doc-detail/51793.htm" rel="noopener noreferrer">创建 SSH 密钥对</a>和<a href="https://www.alibabacloud.com/help/zh/doc-detail/51796.htm?spm=a2c63.p38356.879954.9.cf992580IYf2O7#concept-zzt-nl1-ydb" rel="noopener noreferrer">绑定 SSH 密钥对</a>，将你的 ECS 服务器实例和密钥绑定，然后将私钥保存到你的电脑（例如保存在 ecs.pem 文件）。</p><p>打开你要部署到阿里云的 GitHub 项目，点击 setting-&gt;secrets。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/4c016267852c65a7a64577c08ae1f9e5.png" class="kg-image" alt="在这里插入图片描述" width="1124" height="573" loading="lazy"></figure><p>点击 new secret</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/93edf694856066fdb56c4f49bf2b32f1.png" class="kg-image" alt="在这里插入图片描述" width="985" height="128" loading="lazy"></figure><p>secret 名称为 <code>SERVER_SSH_KEY</code>，并将刚才的阿里云密钥填入内容。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/847a53eb6ca58cbd772f38c41c2fb591.png" class="kg-image" alt="在这里插入图片描述" width="979" height="468" loading="lazy"></figure><p>点击 add secret 完成。</p><p>在你项目下建立 <code>.github\workflows\ci.yml</code> 文件，填入以下内容：</p><pre><code class="language-yml">name: Build app and deploy to aliyun
on:
  #监听push操作
  push:
    branches:
      # master分支，你也可以改成其他分支
      - master
jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Install Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '12.16.2'
    - name: Install npm dependencies
      run: npm install
    - name: Run build task
      run: npm run build
    - name: Deploy to Server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: '-rltgoDzvO --delete'
          SOURCE: dist # 这是要复制到阿里云静态服务器的文件夹名称
          REMOTE_HOST: '118.190.217.8' # 你的阿里云公网地址
          REMOTE_USER: root # 阿里云登录后默认为 root 用户，并且所在文件夹为 root
          TARGET: /root/node-server # 打包后的 dist 文件夹将放在 /root/node-server
</code></pre><p>保存，推送到 GitHub 上。</p><p>以后只要你的项目执行 <code>git push</code> 操作，就会自动执行 <code>ci.yml</code> 定义的脚本，将打包文件放到你的阿里云静态服务器上。</p><p>这个 Actions 主要做了两件事：</p><ol><li>克隆你的项目，下载依赖，打包。</li><li>用你的阿里云私钥以 SSH 的方式登录到阿里云，把打包的文件上传（使用 rsync）到阿里云指定的文件夹中。</li></ol><p>如果还是不懂，建议看一下我的 <a href="https://github.com/woai3c/github-actions-aliyun-demo" rel="noopener noreferrer">demo</a>。</p><h3 id="ci-yml-"><code>ci.yml</code> 配置文件讲解</h3><ul><li><code>name</code>，表示这个工作流程（workflow）的名称。</li><li><code>on</code>，表示监听的意思，后面可以加上各种事件，例如 <code>push</code> 事件。</li></ul><p>下面这段代码表示要监听 <code>master</code> 分支的 <code>push</code> 事件。当 GitHub Actions 监听到 <code>push</code> 事件发生时，它就会执行下面 <code>jobs</code> 定义的一系列操作。</p><pre><code class="language-yml">name: Build app and deploy to aliyun
on:
  #监听push操作
  push:
    branches:
      # master分支，你也可以改成其他分支
      - master
jobs:
...
</code></pre><ul><li><code>jobs</code>，看字面意思就是一系列的作业，你可以在 <code>jobs</code> 字段下面定义很多作业，例如 <code>job1</code>、<code>job2</code> 等等，并且它们是并行执行的。</li></ul><pre><code class="language-yml">jobs:
  job1:
  	...
  job2:
  	...
  job3:
	...
</code></pre><p>回头看一下 <code>ci.yml</code> 文件，它只有一个作业，即 <code>build</code>，作业的名称是自己定义的，你叫 <code>good</code> 也可以。</p><ul><li><code>runs-on</code>，表示你这个工作流程要运行在什么操作系统上，<code>ci.yml</code> 文件定义的是最新稳定版的 <code>ubuntu</code>。除了 ubuntu，它还可以选择 Mac 或 Windows。</li></ul><figure class="kg-card kg-image-card"><img src="https://chinese.freecodecamp.org/news/content/images/2021/07/image-16.png" class="kg-image" alt="image-16" width="398" height="230" loading="lazy"></figure><p><code>steps</code>，看字面意思就是一系列的步骤，也就是说这个作业由一系列的步骤完成。例如先执行 <code>step1</code>，再执行 <code>step2</code>...</p><h4 id="setps-"><code>setps</code> 步骤讲解</h4><p><code>setps</code> 其实是一个数组，在 YAML 语法中，以 <code>-</code> 开始就是一个数组项。例如 <code>['a', 'b', 'c']</code> 用 YAML 语法表示为：</p><pre><code class="language-yml">- a
- b
- c
</code></pre><p>所以 <code>setps</code> 就是一个步骤数组，从上到下开始执行。从 <code>ci.yml</code> 文件来看，每一个小步骤都有几个相关选项：</p><ol><li><code>name</code>，小步骤的名称。</li><li><code>uses</code>，小步骤使用的 actions 库名称或路径，GitHub Actions 允许你使用别人写好的 Actions 库。</li><li><code>run</code>，小步骤要执行的 <code>shell</code> 命令。</li><li><code>env</code>，设置与小步骤相关的环境变量。</li><li><code>with</code>，提供参数。</li></ol><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/d2b7820e4276136a2497417ef77b76f5.png" class="kg-image" alt="d2b7820e4276136a2497417ef77b76f5" width="758" height="393" loading="lazy"></figure><p>综上所述，<code>ci.yml</code> 文件中的 <code>setps</code> 就很好理解了，下面从头到尾解释一边：</p><pre><code class="language-yml">    steps:
    - uses: actions/checkout@v1
    - name: Install Node.js
      uses: actions/setup-node@v1
      with:
        node-version: '12.16.2'
    - name: Install npm dependencies
      run: npm install
    - name: Run build task
      run: npm run build
    - name: Deploy to Server
      uses: easingthemes/ssh-deploy@v2.1.5
      env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: '-rltgoDzvO --delete'
          SOURCE: dist # 这是要复制到阿里云静态服务器的文件夹名称
          REMOTE_HOST: '118.190.217.8' # 你的阿里云公网地址
          REMOTE_USER: root # 阿里云登录后默认为 root 用户，并且所在文件夹为 root
          TARGET: /root/node-server # 打包后的 dist 文件夹将放在 /root/node-server
</code></pre><ol><li>使用 <code>actions/checkout@v1</code> 库克隆代码到 <code>ubuntu</code> 上。</li><li>使用 <code>actions/setup-node@v1</code> 库安装 nodejs，<code>with</code> 提供了一个参数 <code>node-version</code> 表示要安装的 nodejs 版本。</li><li>在 <code>ubuntu</code> 的 <code>shell</code> 上执行 <code>npm install</code> 下载依赖。</li><li>执行 <code>npm run build</code> 打包项目。</li><li>使用 <code>easingthemes/ssh-deploy@v2.1.5</code> 库，这个库的作用就是用 <code>SSH</code> 的方式远程登录到阿里云服务器，将打包好的文件夹复制到阿里云指定的目录上。</li></ol><p>从 <code>env</code> 上可以看到，这个 actions 库要求我们提供几个环境变量：</p><ol><li><code>SSH_PRIVATE_KEY</code>: 阿里云密钥对中的私钥（需要你提前写在 github secrets 上），</li><li><code>ARGS: '-rltgoDzvO --delete'</code>，没仔细研究，我猜是复制完文件就删除掉。</li><li><code>SOURCE</code>：打包后的文件夹名称</li><li><code>REMOTE_HOST</code>: 阿里云公网 IP 地址</li><li><code>REMOTE_USER</code>: 阿里云服务器的用户名</li><li><code>TARGET</code>: 你要拷贝到阿里云服务器指定目录的名称</li></ol><p>如果你想了解一下其他 actions 库的实现，可以直接复制 actions 库的名称去搜索引擎搜索一下，例如搜索 <code>actions/checkout</code> 的结果为：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/c86a63a2ff60d493ea04a9c380459339.png" class="kg-image" alt="c86a63a2ff60d493ea04a9c380459339" width="775" height="641" loading="lazy"></figure><h2 id="--7">部署方式</h2><p>部署有很多种方式，据我所知的有：蓝绿部署、滚动发布、灰度发布等等。当然，还有更简单的方式，直接停掉服务器，上传代码后再重新开启服务器。不过这种方式有一个很大的缺点：在服务器重启过程中，用户无法访问网站的服务，所以你可能会收到大量的投诉。</p><p>下面让我们来简单地了解一下这三种部署方式的区别吧（参考自<a href="https://maichong.io/help/deployment/canary.html" rel="noopener noreferrer">脉冲云文档</a>）。</p><h3 id="--8">蓝绿部署</h3><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/3aaf991ad66c6070beba781024dda558.png" class="kg-image" alt="3aaf991ad66c6070beba781024dda558" width="1018" height="496" loading="lazy"></figure><p>蓝绿部署是指在部署过程中同时运行两个版本的程序。部署新版本时，不停掉旧版本的服务器，然后等新版本运行起来后，再将流量切换到新版本。缺点是在部署过程中，需要配置双倍的服务器。</p><h3 id="--9">滚动发布</h3><p>滚动发布是指在升级过程中，逐台逐台的替换旧版本服务器。先启动一台新版本的服务器，再停掉一台旧版本的服务器。这样在部署过程中只需要 N + 1 台的服务器。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/bc3696dbf56407661330d3e8dfabb99b.png" class="kg-image" alt="bc3696dbf56407661330d3e8dfabb99b" width="1028" height="1232" loading="lazy"></figure><h3 id="--10">灰度发布</h3><p>灰度发布也叫金丝雀发布，起源是，矿井工人发现，金丝雀对瓦斯气体很敏感，矿工会在下井之前，先放一只金丝雀到井中，如果金丝雀不叫了，就代表瓦斯浓度高。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/dfd738a930f2b24a1cfbc30897de059a.png" class="kg-image" alt="dfd738a930f2b24a1cfbc30897de059a" width="1018" height="496" loading="lazy"></figure><p>灰度发布在新开启一台服务器后，先不将流量切换过来。而是先由测试人员对其进行测试，如果运行没问题，再将流量切换过来。同时在运行期间收集各种数据，如果此时将新旧版本的数据进行对比，就是所谓的 A/B 测试。</p><p>当发现新版本运行良好后，再将剩下的服务器用同样的过程逐步替换。最后完全关掉旧版本的服务器，完成灰度发布。</p><p>如果在发布过程中发现新版本有问题，就可以将流量切回到旧版本服务器，这样将负面影响控制到最小。</p><h2 id="--11">小结</h2><p>本章选用 Jenkins 和 GitHub Actions 来讲解自动化部署有两个原因：</p><ol><li>免费。</li><li>使用人数比较多，很多坑都有现成的解决方案。</li></ol><p>希望各位同学在学完本章内容后，能够运用在项目中。很多知识只有亲自实践后，才能理解它的好处。</p><h3 id="--12">参考资料</h3><ul><li><a href="https://www.jenkins.io/zh/doc/" rel="noopener noreferrer">Jenkins 用户手册</a></li><li><a href="https://docs.github.com/cn/free-pro-team@latest/actions" rel="noopener noreferrer">GitHub Actions 文档</a></li><li><a href="https://docs.github.com/cn/free-pro-team@latest/github/working-with-github-pages/about-github-pages" rel="noopener noreferrer">GitHub Pages 文档</a></li><li><a href="https://docs.gitea.io/zh-cn/" rel="noopener noreferrer">Gitea 文档</a></li><li><a href="https://docs.github.com/cn/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions" rel="noopener noreferrer">GitHub 操作的工作流程语法</a></li><li><a href="https://maichong.io/help/deployment/canary.html" rel="noopener noreferrer">什么是蓝绿部署、滚动发布和灰度发布</a><br></li></ul><p><br></p><p><br></p> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 带你入门前端工程（九）：运行时性能优化 13 条规则 ]]>
                </title>
                <description>
                    <![CDATA[ 1. 减少重绘重排 浏览器渲染过程  1. 解析HTML生成DOM树。  2. 解析CSS生成CSSOM规则树。  3. 将DOM树与CSSOM规则树合并在一起生成渲染树。  4. 遍历渲染树开始布局，计算每个节点的位置大小信息。  5. 调用 GPU 绘制，合成图层。  6. 将渲染树每个节点绘制到屏幕。 重排 当改变 DOM 元素位置或大小时，会导致浏览器重新生成渲染树，这个过程叫重排。 重绘 当重新生成渲染树后，就要将渲染树每个节点绘制到屏幕，这个过程叫重绘。不是所有的动作都会导致重排，例如改变字体颜色，只会导致重绘。记住，重排会导致重绘，重绘不会导致重排 。 重排和重绘这两个操作都是非常昂贵的，因为 JavaScript 引擎线程与 GUI 渲染线程是互斥，它们同时只能一个在工作。 什么操作会导致重排？  * 添加或删除可见的 DOM 元素  * 元素位置改变  * 元素尺寸改变  * 内容改变 ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-engineering-13-rules-of-performance-optimization/</link>
                <guid isPermaLink="false">60cc65ad1b6c2406049fd9eb</guid>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Fri, 18 Jun 2021 09:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/06/christina-wocintechchat-com-UTw3j_aoIKM-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <h3 id="1-">1. 减少重绘重排</h3><p><strong>浏览器渲染过程</strong></p><ol><li>解析HTML生成DOM树。</li><li>解析CSS生成CSSOM规则树。</li><li>将DOM树与CSSOM规则树合并在一起生成渲染树。</li><li>遍历渲染树开始布局，计算每个节点的位置大小信息。</li><li>调用 GPU 绘制，合成图层。</li><li>将渲染树每个节点绘制到屏幕。</li></ol><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/5647d9ad4ad5a57178919deae5175b83.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p><strong>重排</strong></p><p>当改变 DOM 元素位置或大小时，会导致浏览器重新生成渲染树，这个过程叫重排。</p><p><strong>重绘</strong></p><p>当重新生成渲染树后，就要将渲染树每个节点绘制到屏幕，这个过程叫重绘。不是所有的动作都会导致重排，例如改变字体颜色，只会导致重绘。记住，重排会导致重绘，重绘不会导致重排 。</p><p>重排和重绘这两个操作都是非常昂贵的，因为 JavaScript 引擎线程与 GUI 渲染线程是互斥，它们同时只能一个在工作。</p><p><strong>什么操作会导致重排？</strong></p><ul><li>添加或删除可见的 DOM 元素</li><li>元素位置改变</li><li>元素尺寸改变</li><li>内容改变</li><li>浏览器窗口尺寸改变</li></ul><p><strong>如何减少重排重绘？</strong></p><ul><li>用 JavaScript 修改样式时，最好不要直接写样式，而是替换 class 来改变样式。</li><li>如果要对 DOM 元素执行一系列操作，可以将 DOM 元素脱离文档流，修改完成后，再将它带回文档。推荐使用隐藏元素（display:none）或文档碎片（DocumentFragement），都能很好的实现这个方案。</li></ul><h3 id="2-">2. 使用事件委托</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="3-">3. 注意程序的局部性</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="-">性能测试</h4><p>运行环境：</p><ul><li>cpu: i5-7400</li><li>浏览器: chrome 70.0.3538.110</li></ul><p>对一个长度为9000的二维数组（子数组长度也为9000）进行10次空间局部性测试，时间（毫秒）取平均值，结果如下：</p><p>所用示例为上述两个空间局部性示例</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>步长为 1</th>
<th>步长为 9000</th>
</tr>
</thead>
<tbody>
<tr>
<td>124</td>
<td>2316</td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>从以上测试结果来看，步长为 1 的数组执行时间比步长为 9000 的数组快了一个数量级。</p><p>总结：</p><ul><li>重复引用相同变量的程序具有良好的时间局部性</li><li>对于具有步长为 k 的引用模式的程序，步长越小，空间局部性越好；而在内存中以大步长跳来跳去的程序空间局部性会很差</li></ul><h3 id="4-if-else-switch">4. 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 语句也更好。</p><p>从使用时机来说，当条件值大于两个的时候，使用 switch 更好。不过 if-else 也有 switch 无法做到的事情，例如有多个判断条件的情况下，无法使用 switch。</p><h3 id="5-">5. 查找表</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="6-">6. 避免页面卡顿</h3><p><strong>60fps 与设备刷新率</strong></p><p>目前大多数设备的屏幕刷新率为 60 次/秒。因此，如果在页面中有一个动画或渐变效果，或者用户正在滚动页面，那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。 其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上，浏览器有整理工作要做，因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算，帧率将下降，并且内容会在屏幕上抖动。 此现象通常称为卡顿，会对用户体验产生负面影响。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/1beefa7a6e2094ded9feba3aec820158.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" 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="noopener noreferrer">高性能JavaScript（opens new window）</a>第 6 章和<a href="https://book.douban.com/subject/30170670/" rel="noopener noreferrer">高效前端：Web高效编程与优化实践（opens new window）</a>第 3 章。</p><h3 id="7-requestanimationframe-">7. 使用 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://img-blog.csdnimg.cn/img_convert/28b8f4c10fdc39630158ebdabbbd5d2f.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h3 id="8-web-workers">8. 使用 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 个数字作乘法处理并再次使用 <code>postMessage()</code> 方法，将结果回传给主线程。</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><h3 id="9-">9. 使用位操作</h3><p>JavaScript 中的数字都使用 IEEE-754 标准以 64 位格式存储。但是在位操作中，数字被转换为有符号的 32 位格式。即使需要转换，位操作也比其他数学运算和布尔操作快得多。</p><h5 id="--1">取模</h5><p>由于偶数的最低位为 0，奇数为 1，所以取模运算可以用位操作来代替。</p><pre><code class="language-js">if (value % 2) {
	// 奇数
} else {
	// 偶数 
}
// 位操作
if (value &amp; 1) {
	// 奇数
} else {
	// 偶数
}
</code></pre><h5 id="--2">取整</h5><pre><code class="language-js">~~10.12 // 10
~~10 // 10
~~'1.5' // 1
~~undefined // 0
~~null // 0
</code></pre><h5 id="--3">位掩码</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="10-">10. 不要覆盖原生方法</h3><p>无论你的 JavaScript 代码如何优化，都比不上原生方法。因为原生方法是用 C/C++ 写的，并且被编译成机器码，成为浏览器的一部分。当原生方法可用时，尽量使用它们，特别是数学运算和 DOM 操作。</p><h3 id="11-css-">11. 降低 CSS 选择器的复杂性</h3><h4 id="-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 class="language-text">内联 &gt; ID选择器 &gt; 类选择器 &gt; 标签选择器
</code></pre><p>根据以上两个信息可以得出结论。</p><ol><li>选择器越短越好。</li><li>尽量使用高优先级的选择器，例如 ID 和类选择器。</li><li>避免使用通配符 *。</li></ol><p>最后要说一句，据我查找的资料所得，CSS 选择器优化不是必需的，因为最慢和慢快的选择器性能差别非常小。</p><h3 id="12-flexbox-">12. 使用 flexbox 而不是较早的布局模型</h3><p>在早期的 CSS 布局方式中我们能对元素实行绝对定位、相对定位或浮动定位。而现在，我们有了新的布局方式 <a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox" rel="noopener noreferrer">flexbox (opens new window)</a>，它比起早期的布局方式来说有个优势，那就是性能比较好。</p><p>下面的截图显示了在 1300 个框上使用浮动的布局开销：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/742da2bd59ee7a319b9606d4a9592249.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>然后我们用 flexbox 来重现这个例子：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/cc81f11a64d22a8cec4d95af8c167e76.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>现在，对于相同数量的元素和相同的视觉外观，布局的时间要少得多（本例中为分别 3.5 毫秒和 14 毫秒）。</p><h3 id="13-transform-opacity-">13. 使用 transform 和 opacity 属性更改来实现动画</h3><p>在 CSS 中，transforms 和 opacity 这两个属性更改不会触发重排与重绘，它们是可以由合成器（composite）单独处理的属性。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/fbd63916537c6b51773c2fb1442cf10c.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><h3 id="--4">参考资料</h3><ul><li><a href="https://book.douban.com/subject/26912767/" rel="noopener noreferrer">深入理解计算机系统(opens new window)</a></li><li><a href="https://developers.google.com/web/fundamentals/performance/rendering" rel="noopener noreferrer">渲染性能(opens new window)</a></li><li><a href="https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution?hl=zh-cn" rel="noopener noreferrer">优化 JavaScript 执行(opens new window)</a></li><li><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers" rel="noopener noreferrer">Web Workers(opens new window)</a></li><li><a href="https://ecss.io/appendix1.html" rel="noopener noreferrer">CSS selector performance(opens new window)</a></li><li><a href="https://www.sitepoint.com/optimizing-css-id-selectors-and-other-myths/" rel="noopener noreferrer">Optimizing CSS: ID Selectors and Other Myths(opens new window)</a></li><li><a href="https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing?hl=zh-cn" rel="noopener noreferrer">使用 flexbox 而不是较早的布局模型(opens new window)</a></li><li><a href="https://developers.google.com/web/fundamentals/performance/rendering/stick-to-compositor-only-properties-and-manage-layer-count?hl=zh-cn" rel="noopener noreferrer">使用 transform 和 opacity 属性更改来实现动画</a><br></li></ul> ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ 带你入门前端工程（八）：性能优化 ]]>
                </title>
                <description>
                    <![CDATA[ 性能优化有多重要？ 国外有很多机构对此做过调查，发现网站的性能对于用户的留存率、转化率有很大的影响，而且非常直接的说提高网站性能就是提高收入。 由于性能优化涉及的知识很多，即使把已经过时的性能优化规则摒弃掉，也有不少的内容。所以我将会用两章的内容来讲解如何做性能优化。 性能优化分类 性能优化主要分为两类：  1. 加载时优化  2. 运行时优化 例如压缩文件、使用 CDN 就属于加载时优化；减少 DOM 操作，使用事件委托属于运行时优化。 在解决问题之前，必须先找出问题，否则无从下手。所以在做性能优化之前，最好先调查一下网站的加载性能和运行性能。 手动检查 检查加载性能 一个网站加载性能如何主要看白屏时间和首屏时间。  * 白屏时间：指从输入网址，到页面开始显示内容的时间。  * 首屏时间：指从输入网址，到页面完全渲染的时间。 将以下脚本放在 </head> 前面就能获取白屏时间。 <script> 	new Date() - performance.timing.navigationStart </script> 在 window.onload 事件里执行 ne ]]>
                </description>
                <link>https://www.freecodecamp.org/chinese/news/front-end-engineering-performance-optimization/</link>
                <guid isPermaLink="false">60ca089b1b6c2406049fd7b7</guid>
                
                    <category>
                        <![CDATA[ 前端开发 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ woai3c ]]>
                </dc:creator>
                <pubDate>Wed, 16 Jun 2021 07:20:00 +0000</pubDate>
                <media:content url="https://chinese.freecodecamp.org/news/content/images/2021/06/marc-mintel-Q-ioK6NPFos-unsplash-2.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><strong>性能优化有多重要？</strong> 国外有很多机构对此做过调查，发现网站的性能对于用户的留存率、转化率有很大的影响，而且非常直接的说提高网站性能就是提高收入。</p><p>由于性能优化涉及的知识很多，即使把已经过时的性能优化规则摒弃掉，也有不少的内容。所以我将会用两章的内容来讲解如何做性能优化。</p><h2 id="-">性能优化分类</h2><p>性能优化主要分为两类：</p><ol><li>加载时优化</li><li>运行时优化</li></ol><p>例如压缩文件、使用 CDN 就属于加载时优化；减少 DOM 操作，使用事件委托属于运行时优化。</p><p>在解决问题之前，必须先找出问题，否则无从下手。所以在做性能优化之前，最好先调查一下网站的加载性能和运行性能。</p><h3 id="--1">手动检查</h3><h4 id="--2">检查加载性能</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>在 <code>window.onload</code> 事件里执行 <code>new Date() - performance.timing.navigationStart</code> 即可获取首屏时间。</p><h4 id="--3">检查运行性能</h4><p>配合 chrome 的开发者工具，我们可以查看网站在运行时的性能。</p><p>打开网站，按 F12 选择 performance，点击左上角的灰色圆点，变成红色就代表开始记录了。这时可以模仿用户使用网站，在使用完毕后，点击 stop，然后你就能看到网站运行期间的性能报告。如果有红色的块，代表有掉帧的情况；如果是绿色，则代表帧率高，页面很流畅。</p><p>另外，在 performance 标签下，按 ESC 会弹出来一个小框。点击小框左边的三个点，把 rendering 勾出来。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/e70dfdafde3bb1e3fe3b00945145f6e5.png" class="kg-image" alt="e70dfdafde3bb1e3fe3b00945145f6e5" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/cd5c3996f61c5b7d741cc4b43923144b.png" class="kg-image" alt="cd5c3996f61c5b7d741cc4b43923144b" width="600" height="400" loading="lazy"></figure><p>这两个选项，第一个是高亮重绘区域，另一个是显示帧渲染信息。把这两个选项勾上，然后浏览网页，可以实时的看到你网页渲染变化。</p><h3 id="--4">利用工具检查</h3><h4 id="--5">监控工具</h4><p>可以部署一个前端监控系统来监控网站性能，上一章中讲到的 sentry 就属于这一类。</p><h4 id="chrome-lighthouse">chrome 工具 Lighthouse</h4><p>如果你安装了 Chrome 52+ 版本，请按 F12 打开开发者工具。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/9aa838256b81a1a733db3a063e9801f2.png" class="kg-image" alt="9aa838256b81a1a733db3a063e9801f2" width="600" height="400" loading="lazy"></figure><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/a287bc15d6a3e4f5c9b0409dc6ab969e.png" class="kg-image" alt="a287bc15d6a3e4f5c9b0409dc6ab969e" width="600" height="400" loading="lazy"></figure><p>它不仅会对你网站的性能打分，还会对 SEO 打分。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/b4f04d477b2168ab22b1302451d14cea.png" class="kg-image" alt="b4f04d477b2168ab22b1302451d14cea" width="600" height="400" loading="lazy"></figure><p><a href="https://developers.google.com/web/tools/lighthouse" rel="noopener noreferrer">使用 Lighthouse 审查网络应用(opens new window)</a></p><p>一般来说，如果你觉得应用加载或运行时有稍微的卡顿，就可以考虑做性能优化了。</p><p>好了，下面正式开始讲解如何做性能优化，一共有 23 条性能优化规则。</p><h2 id="-10-">加载时性能优化 10 条规则</h2><h3 id="1-http-">1. 减少 HTTP 请求</h3><p>一个完整的 HTTP 请求需要经历 DNS 查找，TCP 握手，浏览器发出 HTTP 请求，服务器接收请求，服务器处理请求并发回响应，浏览器接收响应等过程。现在我们来看一个具体的 HTTP 示例：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/4e00fbaebbeea51de35b83820543298d.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" 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 是发出页面请求到接收到应答数据第一个字节的时间。</li><li>Content Download: 接收响应数据所花费的时间。</li></ul><p>从这个例子可以看出，真正下载数据的时间占比为 <code>13.05 / 204.16 = 6.39%</code>，文件越小，这个比例越小，文件越大，比例就越高。这就是为什么要建议将多个小文件合并为一个大文件，从而减少 HTTP 请求次数的原因。</p><h3 id="2-http2">2. 使用 HTTP2</h3><p>HTTP2 相比 HTTP1.1 有如下几个优点：</p><h4 id="--6">解析速度快</h4><p>服务器解析 HTTP1.1 的请求时，必须不断地读入字节，直到遇到分隔符 CRLF 为止。而解析 HTTP2 的请求就不用这么麻烦，因为 HTTP2 是基于帧的协议，每个帧都有表示帧长度的字段。</p><h4 id="--7">多路复用</h4><p>HTTP1.1 如果要同时发起多个请求，就得建立多个 TCP 连接，因为一个 TCP 连接同时只能处理一个 HTTP1.1 的请求。</p><p>在 HTTP2 上，多个请求可以共用一个 TCP 连接，这称为多路复用。同一个请求和响应用一个流来表示，并有唯一的流 ID 来标识。 多个请求和响应在 TCP 连接中可以乱序发送，到达目的地后再通过流 ID 重新组建。</p><h4 id="--8">首部压缩</h4><p>HTTP2 提供了首部压缩功能。</p><p>例如有如下两个请求：</p><pre><code class="language-text">: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 class="language-text">: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>HTTP2 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键－值对，对于相同的数据，不再通过每次请求和响应发送。</p><p>下面再来看一个简化的例子，假设客户端按顺序发送如下请求首部：</p><pre><code class="language-text">Header1:foo
Header2:bar
Header3:bat
</code></pre><p>当客户端发送请求时，它会根据首部值创建一张表：</p><p>索引首部名称值62Header1foo63Header2bar64Header3bat</p><p>如果服务器收到了请求，它会照样创建一张表。 当客户端发送下一个请求的时候，如果首部相同，它可以直接发送这样的首部块：</p><pre><code class="language-text">62 63 64
</code></pre><p>服务器会查找先前建立的表格，并把这些数字还原成索引对应的完整首部。</p><h4 id="--9">优先级</h4><p>HTTP2 可以对比较紧急的请求设置一个较高的优先级，服务器在收到这样的请求后，可以优先处理。</p><h4 id="--10">流量控制</h4><p>由于一个 TCP 连接流量带宽（根据客户端到服务器的网络带宽而定）是固定的，当有多个请求并发时，一个请求占的流量多，另一个请求占的流量就会少。流量控制可以对不同的流的流量进行精确控制。</p><h4 id="--11">服务器推送</h4><p>HTTP2 新增的一个强大的新功能，就是服务器可以对一个客户端请求发送多个响应。换句话说，除了对最初请求的响应外，服务器还可以额外向客户端推送资源，而无需客户端明确地请求。</p><p>例如当浏览器请求一个网站时，除了返回 HTML 页面外，服务器还可以根据 HTML 页面中的资源的 URL，来提前推送资源。</p><p>现在有很多网站已经开始使用 HTTP2 了，例如知乎：</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/9cae1e2191a0594f983766df5cbe75b5.png" class="kg-image" alt="在这里插入图片描述" width="600" height="400" loading="lazy"></figure><p>其中 h2 是指 HTTP2 协议，http/1.1 则是指 HTTP1.1 协议。</p><h3 id="3-">3. 使用服务端渲染</h3><p>客户端渲染: 获取 HTML 文件，根据需要下载 JavaScript 文件，运行文件，生成 DOM，再渲染。</p><p>服务端渲染：服务端返回 HTML 文件，客户端只需解析 HTML。</p><ul><li>优点：首屏渲染快，SEO 好。</li><li>缺点：配置麻烦，增加了服务器的计算压力。</li></ul><p>下面我用 Vue SSR 做示例，简单的描述一下 SSR 过程。</p><h4 id="--12">客户端渲染过程</h4><ol><li>访问客户端渲染的网站。</li><li>服务器返回一个包含了引入资源语句和 <code>&lt;div id="app"&gt;&lt;/div&gt;</code> 的 HTML 文件。</li><li>客户端通过 HTTP 向服务器请求资源，当必要的资源都加载完毕后，执行 <code>new Vue()</code> 开始实例化并渲染页面。</li></ol><h4 id="--13">服务端渲染过程</h4><ol><li>访问服务端渲染的网站。</li><li>服务器会查看当前路由组件需要哪些资源文件，然后将这些文件的内容填充到 HTML 文件。如果有 ajax 请求，就会执行它进行数据预取并填充到 HTML 文件里，最后返回这个 HTML 页面。</li><li>当客户端接收到这个 HTML 页面时，可以马上就开始渲染页面。与此同时，页面也会加载资源，当必要的资源都加载完毕后，开始执行 <code>new Vue()</code> 开始实例化并接管页面。</li></ol><p>从上述两个过程中可以看出，区别就在于第二步。客户端渲染的网站会直接返回 HTML 文件，而服务端渲染的网站则会渲染完页面再返回这个 HTML 文件。</p><p><strong>这样做的好处是什么？是更快的内容到达时间 (time-to-content)</strong>。</p><p>假设你的网站需要加载完 abcd 四个文件才能渲染完毕。并且每个文件大小为 1 M。</p><p>这样一算：客户端渲染的网站需要加载 4 个文件和 HTML 文件才能完成首页渲染，总计大小为 4M（忽略 HTML 文件大小）。而服务端渲染的网站只需要加载一个渲染完毕的 HTML 文件就能完成首页渲染，总计大小为已经渲染完毕的 HTML 文件（这种文件不会太大，一般为几百K，我的个人博客网站（SSR）加载的 HTML 文件为 400K）。<strong>这就是服务端渲染更快的原因</strong>。</p><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://img-blog.csdnimg.cn/img_convert/afb5ef0e381ba6b498ace199c842bf7d.png" class="kg-image" alt="afb5ef0e381ba6b498ace199c842bf7d" width="600" height="400" 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://img-blog.csdnimg.cn/img_convert/bdec5262d3742b9cea47a45908e5b018.png" class="kg-image" alt="bdec5262d3742b9cea47a45908e5b018" width="600" height="400" loading="lazy"></figure><h3 id="5-css-javascript-">5. 将 CSS 放在文件头部，JavaScript 文件放在底部</h3><p>所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染。如果这些 CSS 和 JS 需要加载和解析很久的话，那么页面就会一直显示空白。所以 JS 文件要放在底部，等 HTML 解析完了再加载 JS 文件。</p><p><strong>那为什么 CSS 文件还要放在头部呢</strong>？</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="--14">压缩字体文件</h4><p>使用 <a href="https://github.com/patrickhulce/fontmin-webpack" rel="noopener noreferrer">fontmin-webpack (opens new window)</a>插件对字体文件进行压缩，可以更进一步的减小字体文件的大小。</p><figure class="kg-card kg-image-card"><img src="https://img-blog.csdnimg.cn/img_convert/77b2eb5e3e09320087e3370686da9300.png" class="kg-image" alt="77b2eb5e3e09320087e3370686da9300" width="600" height="400" loading="lazy"></figure><h3 id="7-">7. 善用缓存，不重复加载相同的资源</h3><p>为了避免用户每次访问网站都得请求文件，我们可以通过添加 Expires 或 max-age 来控制这一行为。Expires 设置了一个时间，只要在这个时间之前，浏览器都不会请求文件，而是直接使用缓存。而 max-age 是一个相对时间，建议使用 max-age 代替 Expires 。</p><ul><li>max-age: 设置缓存存储的最大周期，超过这个时间缓存被认为过期(单位秒)。在这个时间前，浏览器读取文件不会发出新请求，而是直接使用缓存。</li><li>指定 no-cache 表示客户端可以缓存资源，每次使用缓存资源前都必须重新验证其有效性。</li></ul><p>不过这样会产生一个问题，当文件更新了怎么办？怎么通知浏览器重新请求文件？</p><p>可以通过更新页面中引用的资源链接地址，让浏览器主动放弃缓存，加载新资源。</p><p>具体怎么做呢？下面我简单的描述一下这个过程：</p><ol><li>使用 webpack 打包时，配置 <code>output</code> 属性，使用摘要算法根据文件内容生成文件名。</li><li>假设现在 <code>index.html</code> 页面引用了两个文件，并且这两个文件名都是根据它们自己的内容生成的。</li><li>服务器将这些资源文件都设为长期缓存（例如一年），除了 <code>index.html</code> 文件，它要设为 <code>Cache-control: no-cache</code>。这样每次页面请求的时候都会比对一下 <code>index.html</code> 文件有没变化，如果没变化就使用缓存，有变化就使用新的 <code>index.html</code> 文件。</li><li>当你的资源文件内容发生变化时，生成的文件名称也会发生变化。进而引发 <code>index.html</code> 内容变化（引用的资源名称变了）。</li><li>这时页面再请求，就会更新发生变化的资源文件。</li></ol><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 class="language-text">npm install compression-webpack-plugin --save-dev
npm install compression
</code></pre><p><strong>webpack 配置</strong></p><pre><code class="language-text">const CompressionPlugin = require('compression-webpack-plugin');

module.exports = {
  plugins: [new CompressionPlugin()],
}
</code></pre><p><strong>node 配置</strong></p><pre><code class="language-text">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>这样图片就加载出来了，具体的代码实现请参考<a href="https://juejin.im/entry/6844903482164510734" rel="noopener noreferrer">web 前端图片懒加载实现原理 (opens new window)</a>。</p><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 class="language-text">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><h4 id="-6-webp-">(6). 使用 webp 格式的图片</h4><p>webp 是一种新的图片文件格式，它提供了有损压缩和无损压缩两种方式。在相同图片质量下，webp 的体积比 png 和 jpg 更小。</p><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="--15">提取第三方库</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 class="language-text">npm i -D @babel/plugin-transform-runtime @babel/runtime
</code></pre><p><strong>使用</strong> 在 <code>.babelrc</code> 文件中</p><pre><code class="language-text">"plugins": [
        "@babel/plugin-transform-runtime"
]
</code></pre><h3 id="--16">参考资料</h3><ul><li><a href="https://developers.google.com/web/fundamentals/performance/why-performance-matters?hl=zh-cn" rel="noopener noreferrer">性能为何至关重要(opens new window)</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="noopener noreferrer">高性能网站建设指南(opens new window)</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="noopener noreferrer">Web性能权威指南(opens new window)</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="noopener noreferrer">高性能JavaScript(opens new window)</a></li><li><a href="https://book.douban.com/subject/30170670/" rel="noopener noreferrer">高效前端：Web高效编程与优化实践(opens new window)</a></li><li><a href="https://www.jianshu.com/p/d078b5f3036a" rel="noopener noreferrer">Babel 7.1介绍 transform-runtime polyfill env(opens new window)</a></li><li><a href="https://webpack.docschina.org/guides/caching/" rel="noopener noreferrer">webpack 缓存(opens new window)</a></li><li><a href="https://juejin.im/post/6844903614759043079" rel="noopener noreferrer">一步一步的了解webpack4的splitChunk插件</a></li></ul> ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
