-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 287 KB
/
content.json
1
{"pages":[{"title":"关于我","text":"","link":"/about/index.html"}],"posts":[{"title":"[译] 5 款工具助力 React 快速开发","text":"原文地址:5 Tools for Faster Development in React 原文作者:Jonathan Saring 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/5-tools-for-faster-development-in-react.md 译者:Ivocin 校对者:Haoze Xu, Junkai Liu 本文掘金地址:[译] 5 款工具助力 React 快速开发 本文将会介绍 5 款工具,可加速 React UI 组件和应用程序的开发工作。 React 非常适合快速开发具有出色的交互式 UI 的应用程序。React 组件是创建用于开发不同应用的隔离的、可复用的模块的很棒的方法。。 虽然一些最佳实践有助于开发更好的应用程序,但正确的工具可以使开发过程更快。以下是 5(+)个实用的工具,可以帮助我们加速组件和应用程序的开发。 欢迎你发表评论并提出建议。 1. Bit Bit — 分享和构建组件代码:Bit 帮助你在不同的项目和应用程序中共享、发现和使用代码组件,以构建新功能和… Bit 是一个开源平台,用于使用组件构建应用程序。 使用 Bit,你可以组织来自不同应用程序和项目的组件(无需任何重构),并使其可以在构建新功能和应用程序时被发现、使用、开发和协作。 YouTube 视频链接:https://youtu.be/P4Mk_hqR8dU Bit 上共享的组件可自动地通过 NPM/Yarn 安装,或与 Bit 本身一起使用。后者使你能够同时开发来自不同项目的组件,并轻松更新(并合并)它们之间的更改。 为了使组件更容易被发现,Bit 为组件提供了可视化渲染,测试结果(Bit 独立运行组件的单元测试)和从源代码本身解析的文档。 使用 Bit,你可以更快地开发多个应用程序和进行团队协作,并将你的组件用作新功能和项目的构建块。 2. StoryBook / StyleguidistStorybook 和 Styleguidist 是在 React 中快速开发 UI 的环境。两者都是加速 React 应用程序开发的绝佳工具。 两者之间存在一些重要的差异,这些差异也可以组合在一起以完成你的组件开发系统。 使用 Storybook,你可以在 JavaScript 文件中编写 stories。使用 Styleguidist,你可以在 Markdown 文件中编写示例。Storybook 一次显示一个组件的变化,而 Styleguidist 可以显示不同组件的多种变化。Storybook 非常适合显示组件的状态,而 Styleguidist 对于不同组件的文档和演示非常有用。 下面是一个简短的纲要。 StoryBook storybooks/storybook: storybook — Interactive UI component dev & test: React, React Native, Vue, Angular. Storybook 是 UI 组件的快速开发环境。 它允许你浏览组件库,查看每个组件的不同状态,以及交互式开发和测试组件。 StoryBook 可帮助你独立于应用程序开发组件,这也有助于提高组件的可重用性和可测试性。 你可以浏览库中的组件,修改其属性,并通过热加载在网页上获得组件的即时效果。可以在这里找到一些流行的例子。 不同的插件可以帮助你更快地开发,从而缩短代码调整到视觉输出之间的周期。StoryBook 还支持 React Native 和 Vue.js。 Styleguidist React Styleguidist:具有在线样式指南的独立的 React 组件开发环境:具有在线样式指南的独立的 React 组件开发环境。 React Styleguidist 是一个组件开发环境,它具有热重载的开发服务器和在线样式指南,列出组件的 propTypes 并显示基于 .md 文件的可编辑的用法示例。 它支持ES6,Flow 和 TypeScript,并且可以使用开箱即用的 Create React App。自动生成的使用文档可以让 Styleguidist 充当团队不同组件的文档门户。 另请查看由 Formidable Labs 提供的 React Live。这个组件渲染环境也用在了 Bit 的实时组件 playground 上。 3. React devTools 这个官方的 React Chrome devTools 扩展程序可以让你在 Chrome 开发者工具里查看 React 组件的层次结构。它也可以作为 FireFox 附加组件使用。 使用 React devTools,你可以在操作组件树时查看并编辑组件的 props 和 state。此功能可以让你了解组件更改如何影响其他组件,以帮助你使用正确的组件结构和分离方式来设计 UI。 这个扩展程序的搜索栏可让你快速查找和检查所需的组件,从而节省宝贵的开发时间。 查看适用于 Safari,IE 和 React Native 的独立应用程序。 4. Redux devTools 此 Chrome 扩展程序(和 FireFox 附加组件)是一个开发时间程序包,是 Redux 开发工作流程的利器。它允许你检查每个 state 和 action payload,重新计算“分阶段”的 actions。 你可以将 Redux DevTools 扩展程序 与任何处理状态的体系结构集成。每个 React 组件的本地状态可以有多个存储或不同的实例。你甚至可以通过“时间旅行”来取消 actions(可以观看 Dan Abramov 的 视频)。日志记录 UI 本身甚至可以自定义为 React 组件。 5. Boilerplates & Kick-Starters虽然这些并不完全是开发者工具,但它们有助于快速创建 React 应用程序,同时节省构建和其他配置的时间。虽然 React 有许多入门套件,但这里有一些最好的。 当与预制组件(在 Bit 或其他来源上)结合使用时,你可以快速创建应用程序结构并将组件组合到其中。 Create React App (50k stars) 这个广泛使用且受欢迎的项目可能是快速创建新 React 应用程序并从头开始运行的最有效方法。 此软件包封装了新 React 应用程序所需的复杂配置(Babel,Webpack等),因此你可以节省新建应用程序所需的这段时间。 要创建新应用程序,只需运行一个命令即可。 1npx create-react-app my-app 此命令在当前文件夹中创建名为 my-app 的目录。在目录中,它将生成初始项目结构并安装传递依赖项,然后你就可以简单地开始编码了。 React Boilerplate (18k stars)Max Stoiber 的这个 React 样板文件模板为你的 React 应用程序提供了一个启动模板,该模板专注于离线开发,并在考虑到了可扩展性和性能。 它的快速脚手架有助于直接从 CLI 创建组件、容器、路由、选择器和 sagas —— 以及它们的测试,而 CSS 和 JS 的更改可以立即反映出来。 与 create-react-app 不同,这个样板文件不是为初学者设计的,而是为经验丰富的开发人员提供的。使用它可以管理性能、异步、样式等等,从而构建产品级的应用程序。 React Slingshot (8.5k stars)Cory House 的这个极好的项目是 React + Redux 入门套件/样板,带有Babel、热重载、测试和 linting 等等。 与 React Boilerplate 非常相似,这个入门套件专注于快速开发的开发人员体验。每次点击“保存”时,更改都会热重载,并且会运行自动化测试。 该项目甚至包括一个示例应用,因此你无需阅读太多文档即可开始工作。 另外也可以了解一下 simple-react-app,这篇文章对此工具进行了解释。 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2018/12/27/5_Tools_for_Faster_Development_in_React/"},{"title":"[译] React 的今天和明天(图文版) —— 第一部分","text":"原视频地址:React Today and Tomorrow and 90% Cleaner React with Hooks 中英文字幕视频地址(B站):【React Conf 2018】React 的今天和明天中英文双字幕 中英文字幕视频地址(爱奇艺):React Today And Tomorrow Part I —— 中英双语字幕 英文原文地址:React Today and Tomorrow - Part I 中文译文(无图版)地址:React 的今天和明天—— 第一部分 演讲者: Sophie Alpert 英文字幕出自:YouTube 机器翻译 英文校对、翻译、排版:Ivocin 本文掘金地址:[译] React 的今天和明天(图文版) —— 第一部分 2018年 11 月 30 日更新,第二部分已经更新:[译] React 的今天和明天(图文版) —— 第二部分 。前两部分中英双语字幕视频已经发布:【React Conf 2018】React 的今天和明天中英文双字幕。 React 的今天和明天 —— 第一部分 早上好。大家好,欢迎来到 React 大会。今天来到这里我感到非常激动。我非常高兴可以给你们做开场演讲。 我是 Sophie Alpert,个人主页是 sophiebits.com。我是 Facebook 的 React 核心小组的开发经理。 React 的今天你们正在使用的 React 做的很好。我们在 npm 的下载量一年内增加了 70%。 React Dev Tools 在 Chrome 开发者工具扩展程序的安装量达到了 125 万。 这是使用 React 的公司列表。此时此刻,这个列表已经非常长了,我们很难说清每年使用 React 的公司的变化。 我们来看另一组数据,我们来看看 Google 趋势, 它可以反映出网络搜索的流量。可以看到,React 的搜索量一直在增加。 希望这个数据表示有更多的人在使用 React,而不是 React 变得更加令人困惑了。(笑声) 嗯,为了做比较,我们比较了 jQuery 的搜索量,我们的搜索量刚刚在历史上首次超越了 jQuery。(欢呼和掌声)但这也表明,我们仍然有很多成长的空间。 我在写这个演讲时被耽搁了一会儿。 因为我比较好奇 React 还比什么更流行。 哎呀。(笑声)当我开了个玩笑。嗯,我发现 React 比可再生能源(renewable energy)更流行。(笑声) React 也比橙汁(orange juice)更流行。(笑声)想想橙汁是多么常用啊,是吧。 而且 React 比可再生能源和橙汁加在一起更流行。所以我认为我们有理由感到非常自豪。 React 的使命但是除了这些数字,我今天真正想要讲的是 React 的使命。呃,自从 2013 年 React 发布以来,我们首要的目标和使命就是:让开发者更容易地构建好的 UI。 所以当我们想要增加新的特性时,我们通常都是要经过深思熟虑。当我们决定是否增加新的 API 时,我们需要考虑非常多的事情。如果增加新的 API 能够让你做到一些以前做不到的事情;如果可以显著简化组件里的代码和类库,让你的工作量减小,用户下载更少的代码,那新增 API 就是有价值的。或者如果新增 API 能够帮助我们做到代码分割的最佳实践,如果能够更容易地将你 app 里面的代码分割成多个包,我们希望你的 app 最终可以运行更快。这也是我们两周之前宣布增加像 React.lazy 这样的API 的原因。 你们可能已经注意到了这个 API。 但想想 React 的使命,让开发者更容易地构建好的 UI。我们有很多方法来实现这个目标。 其中一点是我们尝试简化复杂的东西。如果你看了 Dan Abramov 在冰岛的 JS Conf 上的演讲,你可以抢先看到 “Suspense”, “Suspense” 是我们用来显著简化 app 中获取数据请求、代码分割和异步数据依赖的问题。 另外一个我们尝试去提升 React 的方式就是提升性能。如果你的 app 运行速度更快,你的用户就会更原意使用它。相反的,如果你的 app 反应很慢,速度卡顿,那你的用户肯定不会有很好的体验。因此我们尝试让 React 本身运行的更快,如果 React 开箱就很快,那么你们就会省下很多优化你自己代码的时间。 最近和提升性能有关的内容,Dan 也在冰岛的 JS Conf 上提到了,我们称其为 “Time Slicing”。”Time Slicing” 可以确保你 app 里面最重要的渲染会最先执行,解除主线程的阻塞,并且能让你的 app 运行地更快速。 第三种方式是使用开发者工具帮助你 debug ,进而更了解你的 app。一开始,React 就包含了对开发者友好的警告来帮助开发者指出问题,以防开发者没有注意到这些问题。 而且我们的 React Dev Tools 扩展程序能够让你检查并且 debug 你的组件树。 在 React 16.5 版本,我们引入了一个叫 Profiler 的新特性。它是第二个 … (我不知道这个遥控器出了什么问题)… 图上的第二个标签栏就是 profiler 标签栏, 它能够帮助我们了解到你的 app 中到底发生了什么,然后更好地优化它。 所以 Suspense, Time Slicing 和 Profiler 这三个新特性是我们去年一直在做的事情。 我们真的想多说一些关于这三个特性的内容。但是这些并不是我今天在这里想要讲的。大家可以等到明天,Andrew 和 Brian 会在明天早上给大家带来关于这个内容的演讲。 React 还存在什么糟糕的地方现在我想退一步,让我们来关注一些其他的问题。我想问的是,现在 React 还有什么糟糕的地方。我总结出了三个问题,想在这里和大家讨论。 逻辑复用第一个问题就是多组件间的逻辑复用问题。 在 React 中我们主要使用组件来构建我们的应用,组件主要有两种主要的模式来复用代码:它们是高阶组件(higher-order components)和渲染属性(render props)。 这两种模式对于某些场景来说是很好的,但是它们也造成了一个极大的缺点。在更加复杂的场景中,你必须将他们抽离出来去重构你的 app。这会导致一个问题,我称之为”包装地狱“(wrapper hell)。 嗯,我们经常会看到像这样的组件树。(尖叫和笑声)而且这种嵌套会造成跟踪 app 数据流的困难。如果能够复用这类有状态的逻辑,而不需要修改组件的层级,那肯定是很好的方法,对吧。 庞大的组件第二个我想讲的问题是庞大的组件,它的逻辑杂乱无章。 我们来看看一个上千行代码的 React 组件,我们会发现逻辑分散到了许多不同的生命周期函数中,这样非常难以跟踪。 我们来看一个例子。这里有一个 class 组件,在它的 componentDidlMount 方法,它做了几件事:它订阅了一个数据存储,然后发送了一个网络请求,最后开启了一个定时器。 那么,如果我们来看 componentWillUnmount 方法,我们会看到基本完全相反的代码:首先需要取消订阅存储,然后取消网络请求,最后停止定时器。 当我们要实现 componentDidUpdate 方法, 里面的逻辑会更加的 tricky。因为你需要比较新旧的属性,然后再一次重复和其他生命周期函数内部相同的任务逻辑。 呃,在这个例子里,每个请求都只有一行,所以说这个例子实际上比你平时看到的组件要简单的多。在真实组件中,逻辑往往会更加错综复杂,因为每个独立的任务分散到了不同的生命周期函数中,这样会造成困难,举个例子,当你 unmounting 组件时,你可能会忘记清除资源,这非常难以从代码中找到问题。 令人困惑的 Class第三个糟糕的事情是 Class。理解 JavaScript 里的 class 会相当 tricky,而且为了能够使用 state 和生命周期,我们要求你们使用 class 组件才能做到。 如果你用过 function 组件,并且将其转为了 class 组件,并增加了一些 state,你就会知道这个过程需要有大量的样板文件,但是其作用仅仅是用来定义一个 class 组件。大多数初学者以及很多有经验的开发者也都跟我们抱怨过在 class 里面的绑定和转化工作相当令人困惑。我们有必要来关注这个问题。 而且我们经常听说大家并不是非常清楚什么时候使用 function 组件,有一部分原因是他们总会担心早晚需要将这个组件转化为 class 组件。所以你们可能会困惑,我现在是否应该这么做?我不知道。 所以我说 class 对于人类来说是很难的,但是不仅仅对于人类而言是这样,我认为 class 对于机器而言也是同样很难。 如果你看过压缩后的组件文件,可以看到所有的方法名没有被压缩。而且如果你有一个完全没有被使用的方法,它也没有被剔除出去。这是因为在编译时很难准确判断方法是否被使用。 我们还发现 class 使得可靠的热加载变得困难。最后当我们设计一个优化的编译器原型来提升 React 组件性能时,我们发现 class 组件的一些模式使得编译器优化变得更加困难。 总结所以,我们现在有三个问题:逻辑复用、庞大的组件和 Class。逻辑复用的问题会导致你经常遇到“包装地狱”。庞大组件的原因是由于逻辑分散到了不同的生命周期中。而令人困惑的 class 无论对于人类还是机器来说都是个难题。 我们认为我们有了一个能够解决以上三个问题的解决方案。我们特别想把这个方案分享给大家。请允许我请出 Dan Abramov 为我们带来接下来的演讲。 传送门 最重要的 React 官方文档:Introducing Hooks [译] React 的今天和明天(图文版) —— 第一部分 中英文字幕视频地址(B站):【React Conf 2018】React 的今天和明天中英文双字幕 第一部分视频地址(爱奇艺):React Today And Tomorrow Part I —— Sophie’s Keynote 中英双语字幕 第二部分视频地址(爱奇艺):React Today And Tomorrow Part II —— Dan’s Keynote 中英双语字幕 Dan 对本次演讲的精炼文章(需要科学上网):Making Sense of React Hooks 上面文章的译文:[译] 理解 React Hooks 一篇非常好的理解 React Hook 的文章:30 分钟精通 React Hooks 希望看本文视频的同学,可以查看我的这篇文章:React Conf 2018 专题 —— React Today and Tomorrow PART I 视频中英双语字幕,中英双语字幕已经给大家准备好了。我们的 Demo Boy —— Dan Abramov 后面关于 React Hooks 的精彩演讲正在翻译中,欢迎有兴趣的同学联系我一起翻译,让更多的小伙伴能够更快看到 React Conf 2018 的精彩内容。","link":"/2018/11/12/React_today_and_tomorrow1/"},{"title":"[译] Google 工程师提升网页性能的新策略:空闲执行,紧急优先","text":"原文地址:Idle Until Urgent 原文作者:PHILIP WALTON 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/idle-until-urgent.md 译者:Ivocin 校对者:xilihuasi,新舰同学 Xekin 本文掘金地址:[译] Google 工程师提升网页性能的新策略:空闲执行,紧急优先 几周前,我开始查看我网站的一些性能指标。具体来说,我想看看我的网站在最新的性能指标 —— 首次输入延迟 (FID)上的表现如何。 我的网站只是一个博客(并没有运行很多的 JavaScript),所以我原本预期会得到相当不错的结果。 用户一般对于小于 100 毫秒的输入延迟没有感知,因此我们推荐的性能目标(以及我希望在我的分析中看到的数字)是对于 99% 的页面加载,FID 小于 100 毫秒。 令我惊讶的是,我网站 99% 的页面的 FID 在 254 毫秒以内。我是个完美主义者,尽管结果不算很糟糕,但我却无法对这个结果置之不理。我一定得把它搞定! 简而言之,在不删除网站的任何功能的情况下,我把 99% 页面的 FID 降到了 100 毫秒以内。但我相信读者朋友们更感兴趣的是: 我是如何诊断问题的。 我采用了什么具体的策略和技术。 说到上文中的第二点,当时我试图解决我的问题时,偶然发现了一个非常有趣的性能策略,特别想分享给大家(这也是我写这篇文章的主要原因)。 我把这个策略称作:空闲执行,紧急优先。 我的性能问题首次输入延迟(FID)是一个网站性能指标,指用户与网站首次交互(像我这样的博客,最有可能的首次交互是点击链接)和浏览器响应此交互(请求加载下一页面)之间的时间。 存在延迟是由于浏览器的主线程正在忙于做其他事情(通常是在执行 JavaScript 代码)。因此,要诊断这个高于预期的 FID,我们首先需要在网站加载时启动性能跟踪(启用 CPU 降频和网络限速),然后在主线程上找到耗时长的任务。一旦确定了这些耗时长的任务,我们就可以尝试将它们拆解为更小的任务。 以下是我在对网站启用性能跟踪后的发现: 我的网站加载时的 JavaScript 性能跟踪图(启用网络限速和 CPU 降频)。 可以注意到,主要脚本包在浏览器中单独执行时,它需要耗时 233 毫秒才能完成。 运行我网站的主要脚本包耗时 233 毫秒。 在这些代码中,一部分来自 webpack 样板文件和 babel polyfill,但大多数代码来自我脚本的 main() 入口函数,它本身需要 183 毫秒才能完成: 执行我网站的 main() 入口函数耗时 183 毫秒。 这并不像是我在 main() 函数中做了什么荒谬的事情。在 main() 函数中,我先初始化了我的 UI 组件,然后运行了我的 analytics 方法: 12345678910const main = () => { drawer.init(); contentLoader.init(); breakpoints.init(); alerts.init(); analytics.init();};main(); 那么是什么花了如此长时间运行? 我们继续来看一下这个火焰图的尾部,可以看到没有一个函数占据了大部分时间。绝大多数函数耗时不到 1 毫秒,但是当你将它们全部加起来时,在单个同步调用堆栈中,运行它们却需要超过 100 毫秒。 JavaScript 就像被“千刀万剐”了一样。 由于这些功能全都作为单个任务的一部分运行,因此浏览器必须等到此任务完成才能响应用户的交互。一个十分明显的解决方案是将这些代码拆解为多个任务,但这说起来容易做起来难。 乍一看,明显的解决方案是将 main() 函数中的每个组件分配优先级(它们实际上已经按优先级顺序排列了),立即初始化优先级最高的组件,然后将其他组件的初始化推迟到后续任务中。 虽然这可能有一些作用,但它的可操作行并不强,而且难以应用到大型网站中。原因如下: 推迟 UI 组件初始化的方法仅在组件尚未渲染时才有用。推迟初始化组件的方法会造成风险:用户有可能遇到组件没有渲染完成的情况。 在许多情况下,所有 UI 组件要么同等重要,要么彼此依赖,因此它们都需要同时进行初始化。 有时单个组件需要足够长的时间来初始化,即使它们各自在自己的任务中运行,也会阻塞主线程。 实际情况是,通常我们很难让每个组件在各自的任务中初始化,而且这种做法往往不可能实现。我们经常需要的是在每个组件内部的初始化过程中拆解任务。 贪婪的组件从下面的性能跟踪图可以看出,我们是否真的需要把组件初始化代码进行拆分,让我们来看一个比较好的例子:在 main() 函数的中间,你会看到我的一个组件使用了 Intl.DateTimeFormat API: 创建一个 Intl.DateTimeFormat 实例需要 13.47 毫秒! 创建此对象需要 13.47 毫秒! 问题是,虽然 Intl.DateTimeFormat 实例是在组件的构造函数中创建的,但实际上在其他组件用它来格式化日期之前,它都没有被使用过。可是由于该组件不知道何时会引用 Int.DateTimeFormat 对象,因此它选择立即初始化该对象。 但这是正确的代码求值策略吗?如果不是,那什么是正确的代码求值策略? 代码求值策略在选择求值策略时,大多数开发人员会从如下两种策略中做出选择: 立即求值: 你可以立即运行耗时的代码。 惰性求值: 等到你的程序里的其他部分需要这段耗时代码的结果时,再去运行它。 这两种求值策略可能是目前最受欢迎的,但在我重构了我的网站后,我认为这两个策略可能是最糟糕两个选择。 立即求值的缺点从我网站上的性能问题可以很好地看出,立即求值有一个缺点:如果用户在代码运行时与你的页面进行交互,浏览器必须等到代码运行完成后才能做出响应。 当你的页面看起来已经准备好响应用户输入却无法响应时,这个问题尤为突出。用户会感觉你的页面很卡,甚至以为页面彻底崩溃了。 预先运行的代码越多,页面交互所需的时间就越长。 惰性求值的缺点如果立即运行所有代码是不好的,那么一个显而易见的解决方案就是等到需要的时候再运行。这样就不会提前运行不必要的代码,尤其是一些从未被使用过的代码。 当然,等到用户需要的时候再运行的问题是:你必须确保你的高耗时的代码能够阻止用户输入。 对于某些情况(比如另外加载网络资源),将其推迟到用户请求时再加载是有意义的。但对于你的大多数代码(例如从 localStorage 读取数据,处理大型数据集等等)而言,你肯定希望它在用户交互之前就执行完毕。 其他选择其他可选择的求值策略介于立即求值和惰性求值之间。我不确定以下两种策略是否有官方名称,我把它们称作延迟求值和空闲求值: 延迟求值: 使用 setTimeout 之类的函数,在后续任务中来执行你的代码。 空闲求值: 一种延迟求值策略,你可以使用像 requestIdleCallback 这样的 API 来组织代码运行。 这两个选项通常都比立即求值或惰性求值好,因为它们不太可能由于单个长任务阻塞用户输入。这是因为,虽然浏览器不能中断任何单个任务来响应用户输入(这样做很可能会破坏网站),但是它们可以在计划任务队列之间运行任务,而且大多数浏览器会优先处理由用户输入触发的任务。这称为输入优先。 换句话说:如果确保所有代码都运行在耗时短、不同的任务中(最好小于 50 毫秒),你的代码就再也不会阻塞用户输入了。 重要! 虽然浏览器能够在任务队列中优先执行输入回调函数,但是浏览器无法将这些输入回调函数在排列好的微任务之前运行。由于 promise 和 async 函数作为微任务运行,将你的同步代码转换为基于 promise 的代码不会起到缓解用户输入阻塞的作用。 如果你不熟悉任务和微任务之间的区别,我强烈建议你观看我的同事杰克关于事件循环的精彩演讲。 鉴于我刚才所说的,可以使用 setTimeout() 和requestIdleCallback() 来重构我的 main() 函数,将我的初始化代码拆解为单独的任务: 12345678910const main = () => { setTimeout(() => drawer.init(), 0); setTimeout(() => contentLoader.init(), 0); setTimeout(() => breakpoints.init(), 0); setTimeout(() => alerts.init(), 0); requestIdleCallback(() => analytics.init());};main(); 然而,虽然这比以前更好(许多小任务 vs. 一个长任务),正如我上文解释的那样,它可能还不够好。例如,如果我延迟我 UI 组件(特别是 contentLoader 和 drawer)的初始化过程,虽然它们几乎不会阻塞用户输入,但是当用户尝试与它们交互时,它们也存在未准备好的风险! 虽然使用 requestIdleCallback () 来延迟我的 analytics 方法可能是一个好主意,但在下一个空闲时间之前我关心的任何交互都将被遗漏。而且如果在用户离开页面之前,浏览器都没有空闲时间,这些回调函数可能永远不会运行! 因此,如果所有这些求值策略都有缺点,那么我们该作何选择呢? 空闲执行,紧急优先在长时间思考这个问题之后,我意识到我真正想要的求值策略是:先把代码推迟到空闲时间执行,但是一旦代码被调用则立即执行。换句话说:“空闲执行,紧急优先”。 “空闲执行,紧急优先”的策略回避了我在上一节中指出的大多数缺点。在最坏的情况下,它与延迟计算具有完全相同的性能特征;在最好的情况下,它完全不会阻塞用户交互,因为在空闲时间里,代码都已经执行完毕了。 我还得提一点,这个策略既适用于单任务(在空闲时间求值),也适用于多任务(创建一个有序的任务队列,可以空闲时间运行队列中的任务)。我先解释一下单任务(空闲值)变体,因为它更容易理解。 空闲值我在上文提到过,初始化 Int.DateTimeFormat 对象可能非常耗时,因此若不需要立即调用该实例,最好在空闲时间去初始化。当然,一旦需要它,你就希望它已经存在了。所以这是一个可以用“空闲执行,紧急优先”策略来解决的完美的例子。 如下是我们重构以使用新策略的简化版组件的例子: 12345678910111213class MyComponent { constructor() { addEventListener('click', () => this.handleUserClick()); this.formatter = new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles', }); } handleUserClick() { console.log(this.formatter.format(new Date())); }} 上面的 MyComponent 实例在其构造函数中做了两件事: 为用户交互添加事件侦听器。 创建 Intl.DateTimeFormat 对象。 该组件很好地说明了为什么我们经常需要在单个组件内部拆解任务(而不仅仅在组件级别拆解任务)。 在这种情况下,事件监听器立即运行非常重要,但在事件处理函数需要之前,创建 Intl.DateTimeFormat 实例是不必要的。当然我们也不想在事件处理函数中创建Intl.DateTimeFormat 对象,因为这样会使事件处理函数变得很慢。 下面就是使用“空闲执行,紧急优先”策略修改后的代码。需要注意的是,这里使用了 IdleValue 帮助类,后续我会进行讲解: 1234567891011121314151617import {IdleValue} from './path/to/IdleValue.mjs';class MyComponent { constructor() { addEventListener('click', () => this.handleUserClick()); this.formatter = new IdleValue(() => { return new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles', }); }); } handleUserClick() { console.log(this.formatter.getValue().format(new Date())); }} 如你所见,此代码和先前的版本没有太大的区别,但在新代码中,我没有将 this.formatter 赋值给新的Intl.DateTimeFormat 对象,而是将 this.formatter 赋值给了 IdleValue 对象,在 IdleValue 内部进行 Intl.DateTimeFormat 的初始化过程。 IdleValue 类的工作方式是调度初始化函数,使其在浏览器的下一个空闲时间运行。如果空闲时间在引用 IdleValue 实例之前,则不会发生阻塞,而且可以在请求时立即返回该值。但另一方面,如果在下一个空闲时间之前引用了 IdleValue 实例,则取消初始化函数在空闲时间中的调度任务,并且立即运行初始化函数。 下面是如何实现 IdleValue 类的要点(注意:我已经发布了这段代码,它是idlize 包的一部分,idlize 里面包含了本文出现的所有帮助类): 12345678910111213141516171819export class IdleValue { constructor(init) { this._init = init; this._value; this._idleHandle = requestIdleCallback(() => { this._value = this._init(); }); } getValue() { if (this._value === undefined) { cancelIdleCallback(this._idleHandle); this._value = this._init(); } return this._value; } // ...} 虽然在上面的示例中包含 IdleValue 类并不需要很多修改,但是它在技术上改变了公共 API( this.formatter vs. this.formatter.getValue())。 如果你无法修改公共 API,但是还想要使用 IdleValue 类,则可以将 IdleValue 类与 ES2015 的 getters 一起使用: 1234567891011121314151617class MyComponent { constructor() { addEventListener('click', () => this.handleUserClick()); this._formatter = new IdleValue(() => { return new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles', }); }); } get formatter() { return this._formatter.getValue(); } // ...} 或者,如果你不介意抽象一点,你可以使用 defineIdleProperty() 帮助类(底层使用的是 Object.defineProperty()): 123456789101112131415import {defineIdleProperty} from './path/to/defineIdleProperty.mjs';class MyComponent { constructor() { addEventListener('click', () => this.handleUserClick()); defineIdleProperty(this, 'formatter', () => { return new Intl.DateTimeFormat('en-US', { timeZone: 'America/Los_Angeles', }); }); } // ...} 对于运行非常耗时的个别属性值,没有理由不使用此策略,特别是你不用为了使用此策略而去修改你的 API! 虽然这个例子使用了 Intl.DateTimeFormat 对象,但如下情况使用本策略也是一个好的选择: 处理大量数据集。 从 localStorage(或 cookie)中获取值。 运行 getComputedStyle() 、getBoundingClientRect() 或任何其他可能需要在主线程上重绘样式或布局的 API。 空闲任务队列上文中的技术适用于可以通过单个函数计算出来的属性,但在某些情况下,逻辑可能无法写到单个函数里,或者,即使技术上可行,您仍然希望将其拆分为更小的一些函数,以免其长时间阻塞主线程。 在这种情况下,我们真正需要的是一种队列,在浏览器有空闲时间时,可以安排多个任务(函数)按照顺序运行。队列将在可能的情况下运行任务,并且当需要回到浏览器时(比如用户正在进行交互)能够暂停执行任务。 为了解决这个问题,我构建了一个 IdleQueue 类,可以像这样使用它: 123456789101112import {IdleQueue} from './path/to/IdleQueue.mjs';const queue = new IdleQueue();queue.pushTask(() => { // 一些耗时的函数可以在空闲时间运行...});queue.pushTask(() => { // 其他一些依赖上面函数的任务 // 耗时函数已经执行...}); 注意: 将同步的 JavaScript 代码拆解单独的任务和代码分割不同:前者被拆解的任务为可作为任务队列的一部分,并异步运行;而代码分割则是将较大的 JavaScript 包拆分为较小的文件的过程(它对于提高性能也很重要)。 与上面提到的的空闲时间初始化属性的策略一样,空闲任务队列也可以在需要立刻得到结果的情况下立即运行(“紧急”情况)。 同样,最后一点非常重要;不仅仅因为有时我们需要尽快计算出某些结果,还有一个原因是我们通常都集成了同步的第三方 API,我们需要能够同步运行任务,以保证兼容性。 在理想的情况下,所有 JavaScript API 都是非阻塞的、异步的、代码量小的,并且由能够返回主线程。但在实际情况下,由于遗留的代码库或集成了无法控制的第三方库,我们通常别无选择,只能使用同步。 正如我之前所说,这是“空闲执行,紧急优先”策略的巨大优势之一。它可以轻松应用于大多数程序,而无需大规模重写架构。 保证紧急任务执行我在上文提到过,requestIdleCallback() 不能保证回调函数一定会执行。这也是我在与开发人员讨论 requestIdleCallback() 时,得到的他们不使用 requestIdleCallback() 的主要原因。在许多情况下,代码可能无法运行足以成为不使用它的理由 —— 开发人员宁愿保险地保持代码同步(即使会发生阻塞)。 网站分析代码就是一个很好的例子。网站分析代码的问题在于,很多情况下,在页面卸载时,网站分析代码就要运行(例如,跟踪外链点击等),在这种情况下,显然使用 requestIdleCallback() 不合适,因为回调函数根本不会执行。而且由于开发人员不清楚分析库的 API 在页面的生命周期中的调用时机,他们也倾向于求稳,让所有代码同步运行(这很不幸,因为从用户体验方面来说这些分析代码毫无作用)。 但是使用“空闲执行,紧急优先”模式来解决这个问题就很简单了。我们所要做的就是确保只要页面处于将要卸载的状态,就会立即运行队列中的网站分析代码。 如果你熟悉我近期发表在 Page Lifecycle API 的文章里面给出的建议,你就会知道在页面被终止或丢弃之前,最后一个可靠的回调函数是 visibilitychange 事件(因为页面的 visibilityState 属性会变为隐藏)。而且用户无法在页面隐藏的情况下进行交互,因此这正是运行空闲任务的最佳时机。 实际上,如果你使用了 IdleQueue 类,可以通过一个简单的配置项传递给构造函数,来启用该功能。 1const queue = new IdleQueue({ensureTasksRun: true}); 对于渲染等任务,无需确保在页面卸载之前运行任务,但对于保存用户状态和发送结束回话分析等任务,可以选择将此选项设置为 true。 注意: 监听 visibilitychange 事件应该足以确保在卸载页面之前运行任务,但是由于 Safari 的漏洞,当用户关闭选项卡时,页面隐藏和 visibilitychange 事件并不总是触发,我们必须实现一个解决方案来适配 Safari 浏览器。这个解决方案已经在 IdleQueue 类中为你实现好了,但如果你需要自己实现它,则需注意这一点。 警告! 不要使用监听 unload 事件的方式来执行页面卸载前需要执行的队列。unload 事件不可靠,在某些情况下还会降低性能。有关更多详细信息,请参阅我在Page Lifecycle API 上的文章。 “空闲执行,紧急优先”策略的使用实例每当要运行可能非常耗时的代码时,应该尝试将其拆解为更小的任务。如果不需要立即运行该代码,但未来某些时候可能需要,那么这就是一个使用“空闲执行,紧急优先”策略的完美场景。 在你自己的代码中,我建议做的第一件事是查看所有构造函数,如果存在可能会很耗时的操作,使用 IdleValue 对象重构它们。 对于一些必需但又不用直接与用户交互的逻辑部分代码,请考虑将这些逻辑添加到 IdleQueue 中。不用担心,你可以在任何你需要的时候立即运行该代码。 特别适合使用该技术的两个具体实例(并且与大部分网站相关)是持久化应用状态(如 Redux)和网站分析。 注意: 这些使用实例的目的都是使任务在空闲时间运行,因此如果这些任务不立即运行则没有问题。如果你需要处理高优先级的任务,想要让它们尽快运行(但仍然优先级低于用户输入),那么requestIdleCallback() 可能无法解决你的问题。 幸运的是,我的几个同事开发出了新的 web 平台 API(shouldYield()和原生的 Scheduling API)可以帮助我们解决这个问题。 持久化应用状态我们来看一个 Redux 应用程序,它将应用程序状态存储在内存中,但也需要将其存储在持久化存储(如 localStorage)中,以便用户下次访问页面时可以重新加载。 大多数使用 localStorage 持久化存储状态的 Redux 应用程序使用了防抖技术,大致代码如下: 1234567891011121314let debounceTimeout;// 使用 1000 毫秒的抖动时间将状态更改保存到 localStorage 中。store.subscribe(() => { // 清除等待中的写入操作,因为有新的修改需要保存。 clearTimeout(debounceTimeout); // 在 1000 毫秒(防抖)之后执行保存操作, // 频繁的变化没有必要保存。 debounceTimeout = setTimeout(() => { const jsonData = JSON.stringify(store.getState()); localStorage.setItem('redux-data', jsonData); }, 1000);}); 虽然使用防抖技术总比什么都不做强,但它并不是一个完美的解决方案。问题是无法保证防抖函数的运行不会阻塞对用户至关重要的主线程。 在空闲时间执行 localStorage 写入会好得多。你可以将上述代码从防抖策略转换为“空闲执行,紧急优先”策略,如下所示: 1234567891011121314const queue = new IdleQueue({ensureTasksRun: true});// 当浏览器空闲的时候存储状态更改,// 为了避免多余地执行代码我们只存储最近发生的状态更改。store.subscribe(() => { // 清除等待中的写入操作,因为有新的修改需要保存。 queue.clearPendingTasks(); // 当空闲时执行保存操作。 queue.pushTask(() => { const jsonData = JSON.stringify(store.getState()); localStorage.setItem('redux-data', jsonData); });}); 请注意,此策略肯定比使用防抖策略更好,因为它能够保证即使用户离开页面之前将状态存储好。如果使用上面的防抖策略的例子,在用户离开页面的情况下,很有可能造成写入状态失败。 网站分析另一个“空闲执行,紧急优先”策略适合的实例就是网站分析代码。下面的例子教你如何使用 IdleQueue 类来发送你的网站分析数据,并且可以保证,即使用户关闭了标签页或跳转到了其他页面,并且还没有等到下次的空闲时间,这些数据也可以正常发送: 1234567891011121314const queue = new IdleQueue({ensureTasksRun: true});const signupBtn = document.getElementById('signup');signupBtn.addEventListener('click', () => { // 将其添加到空闲队列中,不再立即发送事件。 // 空闲队列能够保证事件被发送,即使用户 // 关闭标签页或跳转到了其他页面。 queue.pushTask(() => { ga('send', 'event', { eventCategory: 'Signup Button', eventAction: 'click', }); });}); 除了可以保证紧急情况之外,把这个任务添加到空闲时间队列也能够确保其不会阻塞响应用户点击事件的其他代码。 实际上,我建议将你所有的网站分析代码放到空闲时间执行,包括初始化代码。而且像 analytics.js 这样的库,其 API 已经支持命令队列,我们只需简单地在我们的 IdleQueue 实例上添加这些命令。 例如,你可以将默认的 analytics.js 初始化代码片段的最后一部分: 12ga('create', 'UA-XXXXX-Y', 'auto');ga('send', 'pageview'); 修改为: 1234const queue = new IdleQueue({ensureTasksRun: true});queue.pushTask(() => ga('create', 'UA-XXXXX-Y', 'auto'));queue.pushTask(() => ga('send', 'pageview')); (你也可以像我做的一样对 ga() 使用包装器,使其能够自动执行队列命令)。 requestIdleCallback 的浏览器兼容性在撰写本文时,只有 Chrome 和 Firefox 支持 requestIdleCallback()。虽然真正的 polyfill 是不可能的(只有浏览器可以知道它何时空闲),但是使用 setTimeout 作为一个备用方案还是很容易的(本文提到的所有帮助类和方法都使用这个备用方案)。 而且即使在不原生支持 requestIdleCallback() 的浏览器中,使用 setTimeout 这种备用方案也比不用强,因为浏览器仍然是优先处理用户输入,然后再处理通过 setTimeout() 函数创建的队列中的任务。 使用本策略实际上提高了多少性能?在本文开头我提到我想出了这个策略,因为我试图提高我网站的 FID 值。我尝试拆分那些页面开始加载就运行的代码,并且还得保证一些使用了同步 API 的第三方库(如 analytics.js)的正常运行。 上文已经提到,在我使用“空闲执行,紧急优先”策略之前,我所有初始化代码集中在了一个任务中,耗费了 233 毫秒。在使用了“空闲执行,紧急优先”策略之后,可以看到出现了更多耗时更短的任务。实际上,最长的一个任务也仅仅耗时 37 毫秒! 我网站的 JavaScript 性能跟踪图,上面展示了很多短任务。 需要重点强调的是,使用新策略重构的代码和之前执行的任务的数量是相同的,变化仅仅是将其拆分为了多个任务,并且在空闲时间里执行它们。 因为所有任务都不超过 50 毫秒,所以没有任何一个任务影响我的交互时间(TTI),这对我的 lighthouse 得分很有帮助: 使用了“空闲执行,紧急优先”策略后,我的 lighthouse 报告。 最后, 由于本工作的目的是提高我网站的 FID, 在将这些变更上线之后, 经过分析,我非常兴奋地看到:对于 99% 的页面,FID 减少了 67%! Code version FID (p99) FID (p95) FID (p50) Before idle-until-urgent 254ms 20ms 3ms After Idle-until-urgent 85ms 16ms 3ms 总结在理想情况下,我们的网站再也不会不必要地阻塞主线程了。我们会使用 web worker 来处理我们非 UI 的工作,而且我们还有浏览器内置好的 shouldYield() 和原生的 Scheduling API。 但在实际情况下,我们网站工程师往往没有选择,只能将非 UI 的代码放到主线程去执行,这导致了网页出现无响应的问题。 希望这篇文章已经说服了你,是时候去打破我们的长耗时 JavaScript 任务了。而且“空闲执行,紧急优先”策略能够把看起来同步的 API 转到空闲时间运行,能够和全部我们已知的和使用中的工具库结合,“空闲执行,紧急优先”是一个极好的解决方案。 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2018/11/04/Idle_Until_Urgent/"},{"title":"[译] React 的今天和明天(图文版) —— 第二部分","text":"原视频地址:React Today and Tomorrow and 90% Cleaner React with Hooks 中英文字幕视频地址(B站):【React Conf 2018】React 的今天和明天中英文双字幕 中英文字幕视频地址(爱奇艺):React Today And Tomorrow Part II —— 中英双语字幕 英文文稿地址:React Today and Tomorrow - Part II 演讲者: Dan Abramov 英文字幕出自:YouTube 机器翻译 英文校对、翻译:Ivocin, 程序媛_小发 排版:Ivocin 本文掘金地址:[译] React 的今天和明天(图文版) —— 第二部分 因为这个演讲 Dan 的 Demo 部分比较多,建议如果时间充裕,可以观看视频。希望看本文视频的同学,可以查看我的这篇文章:React Conf 2018 专题 —— React Today and Tomorrow Part II 视频中英双语字幕。第一部分 Sophie Alpert 的演讲图文版地址:[译] React 的今天和明天(图文版) —— 第一部分 React 的今天和明天 —— 第二部分嗨。我的名字是 Dan。我在 React Team 工作,这是我第一次参加 React 大会。 (掌声) React 当前面临的问题刚才 Sophie 讲述了这三个问题,我想大多数的开发者在 React 开发过程中都会遇到这些问题。当然,我们可以逐一来解决这些问题。我们可以尝试独立地去解决这些问题。但是实际上解决其中一个问题可能会使其他问题更加严重。 比如我们尝试解决“包装地狱”问题,可以将更多的逻辑放到组件里面,但是我们的组件会变得更大,而且更难以重构。另一个方面,如果我们为了方便重用,尝试将组件拆分为更小的片段,那么组件树的嵌套会更多了,而且最终又会以“包装地狱” 收场。最后,无论那种情况,使用 class 都会让人产生困惑。 因此我们认为造成这种情况是因为这不是三个独立的问题。我们认为,这是同一个问题的三个症状。问题在于 React 没有原生提供一个比 class 组件更简单、更小型、更轻量级的方式来添加 state 或生命周期。 而且一旦你使用了 class组件,你没有办法在不造成“包装地狱”的情况下,进一步拆分它。事实上,这并不是一个新问题。如果你已经使用了 React 几年,你也许还记得在 React 刚出来的时候,事实上已经包含了一个针对该问题的解决方案。嗯,这个解决方案就是 mixins。Mixins 能够让你在 class 之间复用方法,并且可以减少嵌套。 所以我们要在 React 里面重新把 mixins 添加回来吗? (对 … 不…)对了,不,不,我们不会添加 mixins。我的意思是之前使用mixins 的代码并不是无法使用了。但是我们不再推荐在 React 里使用 mixins。如果你好奇我们这么做的原因,可以在 React Blog 里面查看我们之前写的一篇文章,题目是《 Mixins 是有害的 》。在文章中,我们从实验结果发现 mixins 带来的问题远比它解决的问题多。因此,我们不推荐大家使用 mixins。 我们有一个提案那么也许我们解决不了这个问题了,因为这是 React 组件模型固有的问题。也许我们不得不选择接受现实。(笑声) 或者也许有另外一种书写组件的方法可以避免这些问题。 这也就是今天我将要分享的内容。 但是在开始分享我们在 React 上做出的改动和新特性之前,我想先讲讲一年前我们建立的 RFC 流程,RFC 表示 request for comments,它意味着无论是我们还是其他人想要对 React 做出大量变化或者添加新特性时,都需要撰写一个提案,提案里面需要包含动机的详情和该提案如何工作的详细设计。 这正是我们要做的事情。我们非常兴奋地宣布:我们已经准备好了一个提案来解决这三个问题。 重要的是,本提案没有不向下兼容的变化,也没有弃用任何功能。本提案是严格添加性的、可选择的而且增加了一些新的 API 来帮助我们解决这些问题。并且我们希望听到你们对本提案的反馈,这也是为什么我们在今天发布本提案的原因。 我们想过很多发布本提案的方式,也许我们可以写好提案后,提出一个 RFC 然后放在那里。但是既然我们总是要召开 React 大会,我们决定在本次大会上发布这个提案。 Demo 环节那么,接下来进入 Demo 环节。(掌声) 我的屏幕已经投在了显示器上。对不起,有点技术故障。呃,有谁会用这个投影仪,来帮帮我。(笑声) 呃,我能复制我的桌面吗?请。(我能) 是啊。(笑声)好的,但是屏幕上没有显示,我什么都看不到。 (笑声)这就是我现在的问题。 (掌声)好的,灾难过去了。(笑声)好的,嗯,让我来稍微调整下文字大小。你们能看清吗? (可以的。) 好的。 一个熟悉的 class 组件例子那么,我们来看,这里是一个普通的 React 组件,这是一个 Row 组件,这里有一些样式,然后渲染出一个人名。 123456789101112import React from 'react';import Row from './Row';export default function Greeting(props) { return ( <section> <Row label="Name"> {props.name} </Row> </section> );} 我们想要做的是让这个名字可编辑。那么平时我们在 React 里通常是怎么做的呢?我们需要在这里添加一个 input,需要将这些内容放到class 里面返回,添加一些本地 state,让 state 来驱动 input。这也是我准备做的事情。这也是现今大家通常做的事情。 我要导出 default class Greeting 继承 React.Component。我在这里只会使用稳定的 JavaScript语法。接下来是 constructor(props), super (props)。在这里把 state 里的 name 初始化为 Mary。接下来我要声明一个 render 函数,复制一下这段代码然后粘贴到这里。对不起。好的。 我希望这里不再仅仅渲染 name,我希望这里可以渲染一个 input。我把这里替换为一个 input,然后 input 的值设置为 this.state.name。然后在 input 输入发生变化时,调用 this.handleNameChange,这是我的change 事件的回调函数。我把它声明在这里,当名字发生变化时,像我们通常做的那样调用 setState 方法。然后将 name 设置为 e.target.value。对吧。 如果我编辑 … (页面上报了 TypeError 的错误) 好吧,所以我应该去绑定 … (笑声) 对不起,我需要在这里绑定 event 事件。 好的,现在这样我们就可以编辑它了,运行正常。 这个 class 组件我们应该非常熟悉了。你如果使用 React 开发可能会遇到很多类似的代码。 12345678910111213141516171819202122232425262728293031import React from 'react';import Row from './Row';export default class Greeting extends React.Component { constructor(props) { super(props); this.state = { name: 'Mary' } this.handleNameChange = this.handleNameChange.bind(this); } handleNameChange(e) { this.setState({ name: e.target.value }) } render() { return ( <section> <Row label="Name"> <input value={this.state.name} onChange={this.handleNameChange} /> </Row> </section> ); }} 该功能可以用 function 组件实现吗但让我们后退一步,如果想要使用 state 时,能不能不必须使用 class 组件呢?我不确定该怎么做。但是我就准备跟据我的已知来进行,我需要渲染一个 input。我在这里放入一个 input。这个 input 的 value 的值为当前的 name 的值,所以我就传入 name 值。我不知道从哪里获取 name。它不是从 props 里面来,嗯,我就在这里声明,我不知道它的值,之后我再填写这一块。 呃,这里应该也有一个 change 回调函数,我在这里声明 onChange 函数 handleNameChange。我在这里添加一个函数来处理事件。在这里我想要通知 React 设置 name 值到某处,但又一次地,我不确定在 function 组件里如何实现这个功能。因此我就直接调用一个叫做 setName 的方法。使用当前的 input 的值。我把它声明在这里。 12345678910111213141516171819202122import React from 'react';import Row from './Row';export default function Greeting(props) { const name = ??? const setName = ??? function handleNameChange(e) { setName(e.target.value); } return ( <section> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> </section> );} 好吧,由于这两件事情是密切相关的,对吧。其中一个是 state 里 name 变量的当前值,而另一个是一个函数,该函数让我们去设置 state 里的 name 变量。由于这两件事情非常相关,我将它们合并到一起作为一对值。 12345- const name = ???- const setName = ???+ const [name, setName] = ??? 我们从某处一同获取到它们的值。所以问题是我从哪里获取到它们?答案是从 React 本地状态里面获取。 那么我如何在 function 组件里面获取到 React 到本地状态呢?嗯,我直接使用 useState 会怎样。把初始到状态传给 useState 函数来指定它的初始值。 123456789101112131415161718192021import React, { useState } from 'react';import Row from './Row';export default function Greeting(props) { const [name, setName] = useState('Mary'); function handleNameChange(e) { setName(e.target.value); } return ( <section> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> </section> );} 我们来看一下程序运行是否正常。是的,运行正常。 (掌声和欢呼声) 那么我们来比较一下这两种方式。在左侧是我们熟悉的 class 组件。这里 state 必须是一个对象。嗯,我们绑定一些事件处理函数以便调用。在事件处理函数里面使用了 this.setState 方法。当我们调用 setState 方法时,实际上并没有直接将值设置到 state 里面,state 作为参数合并到 state 对象里。而当我想要获取 state 时,我们需要调用 this.state.something。 12345678910111213141516171819202122232425262728293031import React from 'react';import Row from './Row';export default class Greeting extends React.Component { constructor(props) { super(props); this.state = { name: 'Mary' } this.handleNameChange = this.handleNameChange.bind(this); } handleNameChange(e) { this.setState({ name: e.target.value }) } render() { return ( <section> <Row label="Name"> <input value={this.state.name} onChange={this.handleNameChange} /> </Row> </section> ); }} 那么我们再来看右侧的例子:我们不需要使用 this.state.something 来获取 state。因为 state 里的 name 变量在函数里已经可用。它就是一个变量。同样的,当我们需要设置 state 时,我们不需要使用 this.something。因为函数也可以让我们在其作用域内设置 name 的值。那么 useState 到底是什么呢? useState 是一个 Hook。Hook 是一个 React 提供的函数,它可以让你在 function 组件中“钩”连 到一些 React 特性。而useState 是我们今天讲到的第一个 hook,后面还有一些更多的 hook。我们随后会看到它们。 123456789101112131415161718192021import React, { useState } from 'react';import Row from './Row';export default function Greeting(props) { const [name, setName] = useState('Mary'); function handleNameChange(e) { setName(e.target.value); } return ( <section> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> </section> );} 使用 class 和 hook 两种方式实现增加姓氏编辑区域好的,让我们回到我们熟悉的 class 例子。我们接下来想要添加第二个区域。比如,添加一个姓氏的区域。那么我们通常的做法是在 state 添加一个新 key。我把这行复制然后粘贴到这里。这里改成 surname。在这里渲染,这里是 surname 和 handleSurnameChange。我再来复制这个事件处理函数,把这里改成 surname。别忘了绑定这个函数。好的,Mary Poppins 显示出来了,我们可以看到程序运行正常。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546import React from 'react';import Row from './Row';export default class Greeting extends React.Component { constructor(props) { super(props); this.state = { name: 'Mary', surname: 'Poppins', } this.handleNameChange = this.handleNameChange.bind(this); this.handleSurnameChange = this.handleSurnameChange.bind(this); } handleNameChange(e) { this.setState({ name: e.target.value }) } handleSurnameChange(e) { this.setState({ surname: e.target.value }) } render() { return ( <section> <Row label="Name"> <input value={this.state.name} onChange={this.handleNameChange} /> </Row> <Row label="Surname"> <input value={this.state.surname} onChange={this.handleSurnameChange} /> </Row> </section> ); }} 那么我们如何使用 hook 来实现相同的功能呢?我们需要做的一件事情是把我们的 state 改为一个对象。可以看到,使用 hook 的 state 并不强制其类型必须为对象。它可以是任何原生的 JavaScript 类型。我们可以在需要的时候把它变为对象,但是我们不用必须这么做。 从概念上讲,surname 和name 关系不大。所以我们需要做的是,再次调用 useState hook 来声明第二个 state 变量。在这里我声明 surname,当然我可以给它起任何名字,因为它就是我程序里的一个变量。再来设置 setSurname。调用 useState,传入 state 初始变量 ‘Poppins’。我再一次复制和粘贴这个 Row 片段。值改为 surname,onchange 事件改为 handleSurnameChange。当用户编辑surname 时,不是 sir name,我们希望能够修改 surname 的值。 1234567891011121314151617181920212223242526272829303132import React, { useState } from 'react';import Row from './Row';export default function Greeting(props) { const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); function handleNameChange(e) { setName(e.target.value); } function handleSurameChange(e) { setSurname(e.target.value); } return ( <section> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> <Row label="Surname"> <input value={surname} onChange={handleSurnameChange} /> </Row> </section> );} 我们来看看能否正常运行。耶,运行正常。 (掌声) 所以我们可以看到,我们可以在组件里使用多次 hook。 我们来更详细地比较这两种方式。在左侧我们熟悉的 class 组件里的 state 总是一个对象,具有多个字段,需要调用 setState 函数将其中的某些值合并进 state 对象中。当我们需要获取它时,需要调用 this.state.something。在右侧使用 hook 的例子中,我们使用了两次 hook,声明了两个变量:name 和 surname。而且每当我们调用 setName 或 setSurname 时,React 会接到需要重新渲染该组件的通知,就和调用 setState 一样。所以下一次 React 渲染组件会将当前的 name 和 surname 传递给组件。而且我们可以直接使用这些 state 变量,不需要调用 this.state.something。 用 class 和 hook 两种方式使用 React context好的。我们再回到我们的 class 组件的例子。有没我们知道的其他的 React 特性呢?那么另外一个你可能希望在组件里面做的事情就是读取 context。有可能你对 context 还不熟悉,它就像一种为了子树准备的全局变量。 Context 在需要获取当前主题或者当前用户正在使用的语言很有用。尤其是所有组件都需要读取一些相同变量时,使用 context 可以有效避免总是通过 props 传值。 让我们导入 ThemeContext 和 LocaleContext,这两个 context 我已经在另一个文件里定义好了。可能你们最熟悉的用来消费 context,尤其是消费多个 context 的 API 就是 render prop API。就像这样写。我往下滚动到这里。我们使用 ThemeContext Consumer 获得主题。在我的例子里,主题就是个简单的样式。我把这段代码复制,将其全部放入render prop 内部。将 className 赋值为 theme。好的,非常老旧的样式。(笑声) 我也想展示当前的语言,因此我将要使用 LocaleContext Consumer。我们再来渲染另一行,把这行代码复制粘贴到这里,改成 language。 Language。在这里渲染。好的,我们能够看到 context 运行了。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859import React from 'react';import Row from './Row';import { ThemeContext, LocaleContext } from './context';export default class Greeting extends React.Component { constructor(props) { super(props); this.state = { name: 'Mary', surname: 'Poppins', } this.handleNameChange = this.handleNameChange.bind(this); this.handleSurnameChange = this.handleSurnameChange.bind(this); } handleNameChange(e) { this.setState({ name: e.target.value }) } handleSurnameChange(e) { this.setState({ surname: e.target.value }) } render() { return ( <ThemeContext.Consumer> {theme => ( <section className={theme}> <Row label="Name"> <input value={this.state.name} onChange={this.handleNameChange} /> </Row> <Row label="Surname"> <input value={this.state.surname} onChange={this.handleSurnameChange} /> </Row> <LocaleContext.Consumer> {locale => ( <Row label="Language"> {locale} </Row> )} </LocaleContext.Consumer> </section> )} </ThemeContext.Consumer> ); }} 这也许是最普通的消费 context 情况了。实际上,我们在 React 16.6 版本上增加了一个更加方便的 API 来获取它。呃,但是这就是你们常见的多 context 的情形。那么我们看一下如何使用 hook 实现相同的功能。 就像我们所说,state 是 React 的基础特性,因此我们可以使用 useState 来获取 state。那么如果我们想要使用 context,首先需要导入我的 context。这里导入 ThemeContext 和 LocaleContext。现在如果我想在我组件里使用 context,我可以使用 useContext。可以使用 ThemeContext 获取当前的主题,使用 LocaleContext 获取当前的语言。这里 useContext 不只是读取了 context,它也订阅了该组件,当 context 发生变化,组件随之更新。但现在 useContext 就给出了 ThemeContext 的当前值 theme,所以我可以将其赋给 className。接下来我们添加一个兄弟节点,把label 改为 Language, 把 locale 放到这里。 (掌声) 123456789101112131415161718192021222324252627282930313233343536373839import React, { useState, useContext } from 'react';import Row from './Row';import { ThemeContext, LocaleContext } from './context';export default function Greeting(props) { const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); const theme = useContext(ThemeContext); const locale = useContext(LocaleContext); function handleNameChange(e) { setName(e.target.value); } function handleSurameChange(e) { setSurname(e.target.value); } return ( <section className={theme}> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> <Row label="Surname"> <input value={surname} onChange={handleSurnameChange} /> </Row> <Row label="Language"> {locale} </Row> </section> );} 那么,让我们比较这两个方法。左边的例子是传统的 render prop API 的使用方式。非常清楚地显示了它正在做什么。但是它还包含了一点点的嵌套,而且嵌套问题不只会在使用 context 的情况下出现,使用任何一种类型的render prop API 都会遇到。 我们使用 hook 也能实现相同的功能。但是代码会更扁平。那么我们来看一下,我们使用了两个 useContext,从中我们得到了 theme 和 locale。然后我们可以使用它们了。你可能想问 React 是如何知道的,例如,我在这调用了两个 useState,那么 React 是如何知道哪一个 state 和调用的哪一个 useState 是相对应的呢?答案是 React 依赖于这些调用的顺序,这可能有一点不太寻常。 为了让 hook 正确地运行,在使用 hook 时,我们需要遵循一条规则:不能在条件判断里面调用 hook,它必须在你的组件的顶层。举个例子,我做一些类似于 if props 条件的判断,然后我在条件里面调用 useState hook。我们开发了一个 Linter 插件,此时会提示 ‘This is not the correct way to use hooks’。 虽然这是一个不同寻常的限制,但是这对 hook 正常运行十分重要,同时可以使事情变得更明确,我认为你们会喜欢它的,我等会儿会向你们展示它。 如何使用 class 和 hook 两种方式处理副作用那么,让我们回头看看我们的 class。你使用 class 想要做到的另一件事可能就是生命周期函数。而最普遍的使用生命周期函数的案例就是处理一些副作用,比如发送请求,或者是调用某些浏览器 API 来监测 DOM 变化。但是你不能在渲染阶段去做这些类似的事情,因为此时 DOM 可能还没有渲染完成。因此,在 React 中处理副作用的方法是声明如 componentDidMount 的生命周期方法。 那么比如说,嗯,让我向你们展示一下这个。那么,你看到在屏幕的顶部,页签上显示的标题是 React App。这里实际上有一个让我们更新这个标题的浏览器 API。现在我们想要这个页签的标题变成这个人的名字,并且能够随着我输入的值而改变。 现在我要初始化它。嗯,有一个浏览器 API 可以做这件事,那就是 document.title,等于this.state.name 加空格加 this.state.surname。现在我们可以看见这里显示出了 Mary Poppins。但是如果我编辑姓名,页签上的标题没有自动地更新,因为我还没有实现 componentDitUpdate 方法。为了让该副作用和我渲染保持一致,我在这里声明 componentDitUpdate,然后复制这段代码并粘贴到这里。现在标题显示的是 Mary Poppins,如果我开始编辑输入框,页签标题也随之更新了。这就是我们如何在一个 class 里处理副作用的例子。 123456789+ componentDidMount() {+ document.title = this.state.name + ' ' + this.state.surname;+ }+ componentDidUpdate() {+ document.title = this.state.name + ' ' + this.state.surname;+ } 那么我们要如何用 hook 实现相同的功能呢?处理副作用的能力是 React 组件的另一个核心特性。所以如果我们想要使用副作用,我们需要从 React 里导入一个 useEffect。然后我们要告诉 React 在 React 清除组件之后对 DOM 做什么。所以我们在 useEffect里面传递一个函数作为参数,在函数里处理副作用,在这里代码改为 document.title = name + ' ' + surname。 1234567- import React, { useState, useContext } from 'react';+ import React, { useState, useContext, useEffect } from 'react';+ useEffect(() => {+ document.title = name + ' ' + surname;+ }) 可以看到,页面标题显示为 Mary Poppins。如果我开始编辑它,页面标题也会随之更新。 所以,userEffect 默认会在初始渲染和每一次更新之后执行。所以通过默认的,页面标题与这里渲染的内容保持一致。如果出于性能考虑或者有特殊的逻辑,可以选择不采用这种默认行为。在我之后,Ryan 的演讲将会涉及到一些关于这个方面的内容。 那么让我们来比较这两个方法。在左边这个class 里,我们将逻辑分开到不同名称的生命周期方法中。这也是我们为什么会有 componentDidMount 和 componentDitUpdate 的原因,它们在不同的时间上被触发。我们有时候会在它们之间重复一些逻辑。虽然可以把这些逻辑放进一个函数里,但是我们仍然不得不在两个地方调用它,而且要记得保持一致。 而使用 effect hook,默认具有一致性,而且可以选择不使用该默认行为。需要注意的是,在 class 中我们需要访问 this.state, 所以需要一个特殊的 API 来实现。但是在这个 effect 例子中,实际上不需要一个特殊的 API 去访问这个 state 变量。因为它已经在这个函数的作用域里,在上文中已经声明。这就是 effect 被声明在组件内部的原因。而且这样我们也可以访问 state 变量和 context,并且可以为它们赋值。 订阅的两种实现那么,让我们回头看看熟悉的 class 的例子。嗯,其他你可能需要在 class 里使用生命周期方法实现的就是订阅功能。你可能想要去订阅一些浏览器 API,它会提供给你一些值,例如窗口的大小。你需要组件随着这个 state 值的改变更新。那么我们在 class 里实现这个功能的方法是,比如说我们想要,嗯,我们想要监测窗口的宽度。 我将 width 放进 state 里。使用 window.innerWidth 浏览器 API 来初始化。然后我想要渲染它。嗯,让我们复制并且粘贴这段代码。这里改为 width。我将在这个地方渲染它。这里改为 this.state.width。这就是窗口的宽度了,而不是 Mary Poppins 的宽度。(大笑)我将添加一个,嗯,我将要添加一个事件监听,所以我们需要真真切切地监听这个 width 的改变。所以设置 window.addEventListener。我将监听 resize 事件, handleResize。然后我需要声明这个事件。在这里我们更新这个 width 状态,设置为 window.innerWidth。然后我们需要去绑定它。 然后,嗯,然后我也需要取消订阅。所以我不想因为保留这些订阅造成内存泄漏。我想要取消这个事件的订阅。我们在一个 class 里处理的方式是创建另一个叫做 componentWillUnmount 的生命周期方法。然后我将这段逻辑代码复制并且粘贴到这里,将这里改为 removeEventListener。我们设置了一个事件监听,并且我们移除了这个事件监听。我们可以通过拖动窗口来验证。你看到这个 width 正在变化。运行正常。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384import React from 'react';import Row from './Row';import { ThemeContext, LocaleContext } from './context';export default class Greeting extends React.Component { constructor(props) { super(props); this.state = { name: 'Mary', surname: 'Poppins',+ width: window.innerWidth, } this.handleNameChange = this.handleNameChange.bind(this); this.handleSurnameChange = this.handleSurnameChange.bind(this);+ this.handleResize = this.handleResize.bind(this); } componentDidMount() { document.title = this.state.name + ' ' + this.state.surname;+ window.addEventListener('resize', handleResize); } componentDidUpdate() { document.title = this.state.name + ' ' + this.state.surname; }+ componentWillUnmount() {+ window.removeEventListener('resize', handleResize);+ }+ handleResize() {+ this.setState({+ width: window.innerWidth+ });+ } handleNameChange(e) { this.setState({ name: e.target.value }) } handleSurnameChange(e) { this.setState({ surname: e.target.value }) } render() { return ( <ThemeContext.Consumer> {theme => ( <section className={theme}> <Row label="Name"> <input value={this.state.name} onChange={this.handleNameChange} /> </Row> <Row label="Surname"> <input value={this.state.surname} onChange={this.handleSurnameChange} /> </Row> <LocaleContext.Consumer> {locale => ( <Row label="Language"> {locale} </Row> )} </LocaleContext.Consumer>+ <Row label="Width">+ {this.state.width}+ </Row> </section> )} </ThemeContext.Consumer> ); }} 那么让我们看看如何可以,我们如何用 hook 实现这个功能。从概念上来说,监听窗口宽度与设置文档标题无关。这就是为什么我们没有把它放入这个 useEffect 里的原因。它们在概念上是完全独立的副作用,就像我们可以使用多次的 useState 用来声明多个 state 变量,我们可以使用多次 useEffect 来处理不同的副作用。 这里我想要订阅 window.addEventListener ,resize,handleResize。然后我需要保存当前 width 的状态。所以,我将声明另一组 state 变量。所以这里声明 width 和 setWidth。我们通过 useState 设置他们的初始值为 window.innerWidth。现在我把 handleResize 函数声明在这里。因为它没有在其他地方被调用。然后用 setWidth 来设置当前的 width。嗯,我需要去渲染它。所以我复制并粘贴这个 Row。这里改为 width。 最后我需要在这个 effect 之后去清除它。所以我需要指定如何清除。从概念上说,清除也是这个 effect 的一部分。所以这个 effect 有一个清除的地方。这个顺序,你可以指定如何清除订阅的方法是,effect 可以选择返回一个函数。如果它返回一个函数,那么 React 将在 effect 之后调用这个函数进行清除操作。所以这就是我们取消订阅的地方。好的,让我们验证一下它能否正常运行吧。耶!(掌声) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354import React, { useState, useContext, useEffect } from 'react';import Row from './Row';import { ThemeContext, LocaleContext } from './context';export default function Greeting(props) { const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); const theme = useContext(ThemeContext); const locale = useContext(LocaleContext); useEffect(() => { document.title = name + ' ' + surname; })+ const [width, setWidth] = useState(window.innerWidth);+ useEffect(() => {+ const handleResize = () => setWidth(window.innerWidth);+ window.addEventListener('resize', handleResize);+ return () => {+ window.removeEventListener('resize', handleResize);+ };+ }) function handleNameChange(e) { setName(e.target.value); } function handleSurameChange(e) { setSurname(e.target.value); } return ( <section className={theme}> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> <Row label="Surname"> <input value={surname} onChange={handleSurnameChange} /> </Row> <Row label="Language"> {locale} </Row>+ <Row label="Width">+ {width}+ </Row> </section> );} 那么让我们比较这两个方法。在左边,我们使用了一个熟悉的 class 组件,嗯,在这没有令人惊喜的东西。我们有一些副作用,一些相关的逻辑是分开的:我们可以看到文档的标题在这里被设置,但是它在这也被设置了。并且我们在这订阅 effect,抱歉,在这订阅这个事件,但是我们在这里取消订阅。所以这些事情需要相互保持同步。而且这个方法包含了两个不相关的方法,在这不相关的两行。因此,我在未来有点难以单独测试它们。但是它看起来非常熟悉,这点也不错。 那么这段代码看起来可能会就不那么熟悉了。但让我们来看一看这里发生了什么。嗯,在 hook 中,我们分离代码不是基于生命周期函数的名字,而是基于这段代码要做什么。所以我们可以看到这个有一个 effect,我们用来更新文档的标题这是一件这个组件能做的事。这里有另一个 effect,它订阅了 window 的 resize 事件,并且当 window 的大小发生改变时,state 随之更新。然后,嗯,这个 effect 有一个清除阶段,它的作用是移除这个 effect 时,React 取消事件监听从而避免内存泄漏。如果你一直仔细观察,你可能注意到由于 effect 在每次渲染之后运行,我们会重新订阅。有一个方法可以优化这个问题。默认是一致的,这很重要。如果你,例如在这使用一些 prop,我需要去重新订阅一个不同的 id ,该 id 来自 props 或类似的地方。但是这儿有一个方法去优化它,并且可以选择不用这个行为。Ryan 在下一个演讲中将会提到如何去实现它。 Custom Hook好的,我在这里还想要演示另外一件事。现在组件已经非常庞大了,这也没有太大的问题。我们考虑到在 function 组件中你们有可能做更多的事情,组件会变得更大,但也完全没有问题。嗯,但是你有可能想要复用其他组件里面到一些逻辑,或者是想要将公用的逻辑抽取出来,或者是想要分别测试。有趣的是, hook 调用实际上就是函数调用。而且组件就是函数。那么我们平时是如何在两个函数之间共享逻辑呢。我们会将公用逻辑提取到另外一个函数里面。这也是我将要做的事情。我把这段代码复制粘贴到这里。我要新建一个叫做 useWindowWidth 的函数。然后把它粘贴到这里。我们需要组件里面的宽度,以便能够将其渲染。因为我需要在这个函数里面返回当前宽度。然后我们回到上面的代码,这样修改: const width = useWindowWidth。 (掌声和欢呼声) 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667import React, { useState, useContext, useEffect } from 'react';import Row from './Row';import { ThemeContext, LocaleContext } from './context';export default function Greeting(props) { const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); const theme = useContext(ThemeContext); const locale = useContext(LocaleContext);+ const width = useWindowWidth(); useEffect(() => { document.title = name + ' ' + surname; })- const [width, setWidth] = useState(window.innerWidth);- useEffect(() => {- const handleResize = () => setWidth(window.innerWidth);- window.addEventListener('resize', handleResize);- return () => {- window.removeEventListener('resize', handleResize);- };- }) function handleNameChange(e) { setName(e.target.value); } function handleSurameChange(e) { setSurname(e.target.value); } return ( <section className={theme}> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> <Row label="Surname"> <input value={surname} onChange={handleSurnameChange} /> </Row> <Row label="Language"> {locale} </Row> <Row label="Width"> {width} </Row> </section> );}+function useWindowWidth() {+ const [width, setWidth] = useState(window.innerWidth);+ useEffect(() => {+ const handleResize = () => setWidth(window.innerWidth);+ window.addEventListener('resize', handleResize);+ return () => {+ window.removeEventListener('resize', handleResize);+ };+ })+ return width;+} 那么这个函数是什么呢?我们并没有做什么特别的事情,我们仅仅是将逻辑提取到了一个函数里面。呃,但是这里有一个约定。我们把这种函数叫做 custom hook。按照约定,custom hook 的名字需要以 use 开头。这么约定主要有两个原因。 我们会读你的函数名或修改函数名称。但是这是一个重要的约定,因为首先以 use 开头来命名 custom hook,可以让我们自动检测是否违反了我之前说过的第一条规则:不能在条件判断里面使用 hook。因此如果我们无法得知哪些函数是 hook,那么我们就无法做到自动检测。 另一个原因是,如果你查看组件的代码,你可能会想要知道某个函数里面是否含有 state。因此这样的约定很重要,好的,以 use 开头的函数表示这个函数是有状态的。 在这里 width 变量给了我们当前的宽度并且订阅了其更新。如果我们想,我们可以更进一步。在这个例子里面也许并不必要,但是我想要给你一个思路。嗯,我们也许设置文档的标题的功能会更加复杂,你希望能够把它的逻辑提取出来并单独测试。那么我把这段代码复制过来粘贴到这里。我可以写一个新的 custom hook。我把这个 hook 命名为useDocumentTitle。由于name 和 surname 在上下文作用域里没有意义。我希望调用标题,标题就是一个参数,由于 custom hook 就是 JavaScript 函数,因此他们可以传递参数,返回值或者不返回。这里我把 title 设置为参数。然后在组件里面,使用 useDocumentTitle,参数为 name 加上 surname。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667import React, { useState, useContext, useEffect } from 'react';import Row from './Row';import { ThemeContext, LocaleContext } from './context';export default function Greeting(props) { const [name, setName] = useState('Mary'); const [surname, setSurname] = useState('Poppins'); const theme = useContext(ThemeContext); const locale = useContext(LocaleContext); const width = useWindowWidth();+ useDocumentTitle(name + ' ' + surname);- useEffect(() => {- document.title = name + ' ' + surname;- }) function handleNameChange(e) { setName(e.target.value); } function handleSurameChange(e) { setSurname(e.target.value); } return ( <section className={theme}> <Row label="Name"> <input value={name} onChange={handleNameChange} /> </Row> <Row label="Surname"> <input value={surname} onChange={handleSurnameChange} /> </Row> <Row label="Language"> {locale} </Row> <Row label="Width"> {width} </Row> </section> );}+function useDocumentTitle(title) {+ useEffect(() => {+ document.title = title;+ })+}function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }) return width;} 事实上,我可以更进一步。在这个例子中是完全没有必要的,但是同样的道理,也许我们的输入框会更加的复杂,也许我们需要追踪输入框的聚焦或失焦事件,或者输入框是否被校验过、提交过等等。也许我们还有更多的逻辑想要从组件中抽离。嗯,而且想要减少重复代码。这里已经有了重复的代码,这两段事件处理函数几乎一样。 那么我们如果,呃,我把他们删除一段,然后提取另一段。我要创建另一个新 hook,把它命名为 useFormInput。这个 hook 是我的 change 处理函数。现在我把这个声明复制粘贴到这里。这里定义了输入框的状态。这里不再是 name 和 setName。我把这里改为更通用的 value 和 setValue。我把初始值作为参数。这里改为 handleChange,这里改为 setValue。那么我们该如何做在我们组件里面使用输入框呢?我们需要获取当前的 value 和 change 处理函数。这是我们需要赋给输入框的。所以我们就在 hook 里面返回他们。嗯,返回 value 和 onChange handleChange 函数。我们回到组件里面,这里改为 name 等于 useFormInput,参数 Mary。这里 name 变为了一个对象,包括 value 和 onChange 函数。这里 surname 等于 useFormInput,初始化参数 Poppins。这里改为 name.value 和 surname.value。因为这两个值才是我们需要的字符串。接下来我把这里删除,然后将其改为 spread 属性。有人在笑。[笑声] 好的。我们来验证一下,是的,运行正常。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677import React, { useState, useContext, useEffect } from 'react';import Row from './Row';import { ThemeContext, LocaleContext } from './context';export default function Greeting(props) {- const [name, setName] = useState('Mary');- const [surname, setSurname] = useState('Poppins');+ const name = useFormInput('Mary');+ const surname = useFormInput('Poppins'); const theme = useContext(ThemeContext); const locale = useContext(LocaleContext); const width = useWindowWidth();- useDocumentTitle(name+ ' ' + surname);+ useDocumentTitle(name.value + ' ' + surname.value);- function handleNameChange(e) {- setName(e.target.value);- }- function handleSurameChange(e) {- setSurname(e.target.value);- } return ( <section className={theme}> <Row label="Name">- <input- value={name}- onChange={handleNameChange}- />+ <input {...name} /> </Row> <Row label="Surname">- <input- value={surname}- onChange={handleSurnameChange}- />+ <input {...surname} /> </Row> <Row label="Language"> {locale} </Row> <Row label="Width"> {width} </Row> </section> );}+function useFormInput(initialValue) {+ const [value, setValue] = useState(initialValue);+ function handleChange(e) {+ setValue(e.target.value);+ }+ return {+ value,+ onChange: handleChange+ };+}function useDocumentTitle(title) { useEffect(() => { document.title = title; })}function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }) return width;} 每次我们调用 hook,其状态都是完全独立的。这是因为我们依赖调用 hook 的顺序,而不是通过名称或其他方式来实现的。所以你可以多次调用相同的 hook。每次调用都会获取其自身的本地状态。 我们最后一次来比较这两种方式。嗯,在左侧我们熟悉的class 组件例子里,在一个对象里面有一些 state,绑定了一些方法,有一些逻辑分散到不同的声明周期方法里面,这些逻辑是一串事件处理函数。嗯,我们用了来自 context 的内容来渲染内容。嗯,这种情况我们相当熟悉了。 在右侧窗格里面,和我们常见的 React 组件不同。但是它是有意义的。即使你并不知道这些函数是如何实现的。你可以看到,这个函数就是用来组织输入框的,这个函数用了 context 来获取主题和本地语言,这个函数使用了窗口宽度和文档标题,然后渲染了一连串的内容。如果我们想了解更多,我们可以滚动窗口到下面,可以看到,这就是输入框如何运行的代码,这里是如何设置文档标题的代码,而这里是如何设置并订阅窗口宽度的代码。或许这里是一个 npm 包,实际上你没有必要了解它是如何实现的。我们可以将它在组件里面调用,或者在组件之间复制粘贴它们。 Hook 提供了 custom hook,为用户提供了灵活的创建自己的抽象函数的功能,custom hook 不会让你的 React 组建树变得庞大,而且可以避免“包装地狱”。 (掌声) 而且重要的是,这两个例子并不是独立的两个应用。实际上,这两个例子是在同一个应用里面。我把这个窗口打开的目的就是想要展示 class 可以和 hook 并肩工作。而 hook 代表这我们对 React 未来的期许,嗯,但是我们并不想做出不向下兼容的改变。我们还需要保证 class 可以正常运行。 Hook 提案我们回到幻灯片上来。好的,这张幻灯片就是你们可以发 tweet 的片子。 (笑声) 今天我们向你们展示了 Hook 提案。Hook 让我们可以在不使用 class 的情况下使用 React 的众多特性。而且我们没有弃用 class,但是我们给你们提供了一个不去写 class 的新选择。我们打算尽快完成使用 hook 来替代 class 的全部用例。目前还有一部分缺失,但是我们正在处理这部分内容。而且 hook 能够让大家复用有状态的逻辑,并将其从组件中提取出来,分别测试,在不同组件之间复用,并且可以避免引入“包装地狱”。 重要的是,hook 不是一个破坏性的改动,完全向后兼容,是严格添加性的。你可以从这个 url 查找到我们关于 hook 的文档。嗯,我们希望听到你们的反馈,React 社区希望了解到你们对 hook 的想法,嗯,无论你们喜欢与否。而且我们发现如果不让大家实际使用 hook,就会很难收到反意见。所以我们将 hook 构建发布到了 React 16.7 alpha 版本上。这个不是一个主要版本,是一个小版本。但是在这个 alpha 版本,你可以尝试使用 hook。而且我们在 Facebook 的生产环境已经测试了一个月,因此我们认为不会有大的缺陷。但是 hook 的 API 可以根据你们的反馈意见进行调整。而且我不建议你们把整个应用使用 hook 来重写。因为首先,hook 目前还在提案阶段。第二个原因,我个人认为,使用 hook 的思维方式需要一个思想上的改变,也许刚开始你们尝试把 class 组件转为 hook 写法会比较困惑。但是我推荐大家尝试在新的代码里使用 hook,并且让我们知道你们是怎么想的。那么,谢谢大家。 (掌声) 在我们看来,hook 代表着 React 的未来。但我认为这也代表着我们推进 React 发展的方式。那就是我们不进行大的重写。嗯,我们希望我们更喜欢的新模式可以和旧模式并存,这样我们就可以进行渐进迁移并接受这些新模式,就像你们逐渐接受 React 本身一样。 Hook 一直就在那里这也差不多是我演讲的结尾了。但是最后,我想讲讲一些我个人的观点。我从四年前学习 React。我遇到的第一个问题就是为什么要使用 JSX。 嗯,我第二个问题是 React 的 Logo 到底有什么含义。React 项目没有起名叫“原子”(Atom),它并不是一个物理引擎。嗯,有一个解释是,React 是基于反应的(reactions),原子也参与了化学反应(chemical reactions),因此 React 的 Logo 用了原子的形象。 但是 React 没有官方承认过这种说法。嗯,我发现了一个对我来说更有意义的解释。我是这样思考的,我们知道物质是由原子组成的。我们学过物质的外观和行为是由原和其内部的属性决定的。而 React 在我看来是类似的,你可以使用 React 来构建用户界面,将其拆分为叫做组件的独立单元。用户界面的外观和行为是由这些组件及其内部的属性决定的。 具有讽刺意味的是,“原子”(Atom)一词,字面上的意思是不可分割的。当科学家们首次发现原子的时候,他们认为原子是我们发现的最小的物质。但是之后他们就发现了电子,电子是原子内部更小的微粒。后来证明实际上电子更能描述原子运行的原理。 我对 hook 也有类似的感觉。我感觉 hook 不是一个新特性。我感觉 hook 提供了使用我们已知的 React 特性的能力,如 state 、context 和生命周期。而且我感觉 hook 就像 React 的一个更直观的表现。Hook 在组件内部真正解释了组件是如何工作的。我感觉 hook 一直在我们的视线里面隐藏了四年。事实上,如果看看 React 的 Logo,可以看到电子的轨道,而 hook 好像一直就在那里。谢谢。(掌声) 传送门 最重要的 React 官方文档:Introducing Hooks [译] React 的今天和明天(图文版) —— 第一部分 中英文字幕视频地址(B站):【React Conf 2018】React 的今天和明天中英文双字幕 第一部分视频地址(爱奇艺):React Today And Tomorrow Part I —— Sophie’s Keynote 中英双语字幕 第二部分视频地址(爱奇艺):React Today And Tomorrow Part II —— Dan’s Keynote 中英双语字幕 Dan 对本次演讲的精炼文章(需要科学上网):Making Sense of React Hooks 上面文章的译文:[译] 理解 React Hooks 一篇非常好的理解 React Hook 的文章:30 分钟精通 React Hooks 如果发现译文和字幕存在错误或其他需要改进的地方,欢迎到本项目的 GitHub 仓库 对英文字幕或译文进行修改并 PR,谢谢大家。当然后本视频还有后面 Ryan 给我带来的第三段题目为90% Cleaner React with Hooks 的演讲,欢迎有兴趣的小伙伴一起参与英文字幕校对和翻译工作。","link":"/2018/11/28/React_today_and_tomorrow2/"},{"title":"[译]ECMAScript 2021: 最终功能集确定","text":"原文地址:ECMAScript 2021: the final feature set 原文作者:Axel Rauschmayer 本文永久链接:https://github.com/Ivocin/Translation/Blogs/ECMAScript-2021-the-final-feature-set.md 翻译、校对:Ivocin 更新于 2021-03-09: 今天,ES2021 候选提案 发布了其最终功能集的版本。如果它能够在今年 6 月的 ECMA 大会上通过,就会成为官方的标准。本文描述了有哪些新的内容。 1、ECMAScript 2021 的编辑这个版本的编辑是: Jordan Harband Shu-yu Guo Michael Ficarra Kevin Gibbons 2、关于 ECMAScript 的版本说明注意,自从 TC39 进程制定以来,ECMAScript 版本的重要性就降低了很多。现在真正重要的是提案处于哪个阶段:一旦提案到了第 4 阶段,那么它就可以使用了。但是即使这样,你仍然需要检查你的引擎是否支持该功能。 3、ES2021 功能(第 4 阶段提案) String.prototype.replaceAll (Peter Marshall, Jakob Gruber, Mathias Bynens) Promise.any() (Mathias Bynens, Kevin Gibbons, Sergey Rubanov) WeakRefs (Dean Tribble, Mark Miller, Till Schneidereit, Sathya Gunasekaran, Daniel Ehrenberg) [proposal] Logical assignment operators (Justin Ridgewell, Hemanth HM) Underscores (_) as separators in number literals 以及 bigint literals (Sam Goto, Rick Waldron) 4、常见问题4.1 阶段的含义是什么?阶段是指 “TC39 进程“的成熟阶段。更多信息可以查看“JavaScript for impatient programmers” 中的“TC39 进程” 部分。 4.2 [我最喜欢的提案功能] 现在怎么样了?如果你想查看不同的提案功能现在处于什么阶段,请查阅 ECMA-262 GitHub 仓库的 README 文件。 4.3 有官方的 ECMAScript 功能列表吗?当然,TC39 仓库列出了 已完成提案 以及它们是在哪个 ECMAScript 版本被引入的说明。 5、ES2021 的免费书籍以下书籍包括了到 ECMAScript 2021 的 JavaScript,并且可以免费在线阅读: “JavaScript for impatient programmers” “Deep JavaScript”","link":"/2021/03/13/ECMAScript-2021-the-final-feature-set/"},{"title":"React Conf 2018 专题 —— React Today and Tomorrow Part I 视频中英双语字幕","text":"本文掘金地址:React Conf 2018 专题 —— React Today and Tomorrow Part I 视频中英双语字幕最近在 掘金翻译计划 校对了一篇 Dan Abramov 的关于 React Hooks 的文章,在 Sophie Alpert 和 Dan 在 React Conf 2018 上对 Hooks 的提案之后, Hooks 非常火。想到由于原视频在 Youtube 上的原因导致大部分小伙伴没法观看,而且官方没有提供英文字幕,YouTube 里面的英文字幕是机器自动生成的,错误比较多也没有断句,所以想把 React Conf 2018 的视频中英文字幕配好供大家一起学习。我看B站上已经搬运了 React Conf 2018 的全套视频,大家可以去围观。 2018年11月30日更新,小发已经把前两部分视频上传到 b 站上了,没广告特别好,视频地址:【React Conf 2018】React 的今天和明天中英文双字幕 视频地址:React Today And Tomorrow Part I —— 中英双语字幕 翻译字幕的难度远超过了我的预估,首先是听力方面,有些单词实在听不出来,第二个困难就是视频太长了,精校需要花费大量时间,所以决定逐步翻译,先把 Sophie 的演讲(10 分钟左右)翻译出来,光棍节在星巴克坐了一上午才校对翻译完毕。 简单介绍一下视频的内容,该视频是 React Conf 2018 的开场,首先是 React 的技术经理 Tom 的开场暖场,然后是 Sophie 和 Dan 的主题演讲: React Today and Tomorrow ,这里是视频的第一部分,Sophie 的演讲内容,视频的英文字幕进行了校对,并翻译了中文字幕。 另外如果看视频不方便的同学,可以移步到这篇文章:[译] React 的今天和明天(图文版) —— 第一部分,我把 Sophie 的keynote 截图并翻译整理好了,可以作为参考。 当然 Dan 的演讲才是重头戏,时间也比较长,还请有兴趣的小伙伴联系我,可以一起翻译和校对,这样效率会高些。我把译文放到了我的 Github 仓库上了,如果大家看到有中英文字幕的问题欢迎 pr。 2018 年 11 月 20 日更新, Dan 的 49 分钟演讲的中英文字幕快要完成了,感谢开源社区找到了小伙伴一起完成这个工作,我们这次的字幕质量很高,敬请期待。 传送门 最重要的 React 官方文档:Introducing Hooks [译] React 的今天和明天(图文版) —— 第一部分 [译] React 的今天和明天(图文版) —— 第二部分 前两部分视频地址(B 站):【React Conf 2018】React 的今天和明天中英文双字幕 第一部分视频地址(爱奇艺):React Today And Tomorrow Part I —— Sophie’s Keynote 中英双语字幕 第二部分视频地址(爱奇艺):React Today And Tomorrow Part II —— Dan’s Keynote 中英双语字幕 Dan 对本次演讲的精炼文章(需要科学上网):Making Sense of React Hooks 上面文章的译文:[译] 理解 React Hooks 一篇非常好的理解 React Hook 的文章:30 分钟精通 React Hooks","link":"/2018/11/12/React_today_and_tomorrow_video1/"},{"title":"React Conf 2018 专题 —— React Today and Tomorrow Part II 视频中英双语字幕","text":"本文掘金地址:React Conf 2018 专题 —— React Today and Tomorrow Part II 视频中英双语字幕距离 React Conf 2018 结束已经将近一个月了,距离上个 React Conf 2018 的中英文双语视频发布也有两周的时间了,这两周,一直在进行Dan Abramov 的关于 React Hooks 提案部分演讲的字幕校对和翻译工作,感谢开源社区,这次加入了新的小伙伴 程序媛_小发 一起完成了校对和翻译的工作,让这段视频可以更快的和大家见面。 视频简介话不多说,先放上视频地址:React Today And Tomorrow Part II —— Dan’s Keynote 中英双语字幕 第一部分视频地址:React Today And Tomorrow Part I —— Sophie’s Keynote 中英双语字幕 2018年11月30日更新,小发已经把前两部分视频上传到 b 站上了,没广告特别好,视频地址:【React Conf 2018】React 的今天和明天中英文双字幕 简单介绍一下视频的内容,该视频是 React Conf 2018 的开场演讲,题目为 React Today and Tomorrow。 本视频为该演讲的第二部分,在上个视频中 Sophie Alpert 对 React Today 做出了总结并引出了当前 React 面临的三大问题,本视频中 Dan Abramov 为我们带来解决这三个问题的方法 —— React Hook,正式发表 React Hook 提案,并宣布 Hook 是 React 的未来。外号“Demo Boy ”的 Dan 在本次大会上除了颇多的 Demo 之外,在视频的结尾处,Dan 有一段我特别喜欢的个人演讲,他从 React 的 Logo 讲起,类比了组件与原子、Hook 与电子的关系,告诉我们他认为 Hook 并不是一个新特性,它一直就在那里,就在 React Logo 中电子所在的轨道上。非常精彩的演讲,值得一看。 这里放一张 Twitter 里比较火的 Dan 忘记绑定时的报错动图: 因为这个演讲 Demo 部分比较多,强烈建议大家观看视频。另外如果看视频不方便的同学,该演讲的图文版文稿正在整理中,过两天会发布出来。第一部分视频内容文稿可以移步到这篇文章:[译] React 的今天和明天(图文版) —— 第一部分。 背景两周前在 掘金翻译计划 校对了一篇 Dan Abramov 的关于 React Hooks 的文章,在 Sophie Alpert 和 Dan 在 React Conf 2018 上对 Hooks 的提案之后, Hooks 非常火。想到由于原视频在 Youtube 上的原因导致大部分小伙伴没法观看,而且官方没有提供英文字幕,YouTube 里面的英文字幕是机器自动生成的,错误比较多也没有断句,所以想把 React Conf 2018 的视频中英文字幕配好供大家一起学习。我看B站上已经搬运了 React Conf 2018 的全套视频,大家可以去围观。 总结第一次听译字幕的体验真的非常神奇,这段视频已经翻来覆去看了几十遍了,记忆比较深的是一个句子听了将近一百遍才听出来。比起第一段视频10 分钟校对翻译花了 10 个小时,第二段 49 分钟的视频速度明显快了很多,再次感谢小发的帮助,我们分工完成了这个大工程的工作。 之前在掘金翻译计划也翻译和校对了好几篇文章了,做字幕的工作又是全新的体验,因为大部分时间花在了听视频校对英文字幕上。在听力环节,理解视频的内容会对英文校对有很大的帮助,有一句是一直没有听出来的句子,后来看到下文 Dan 又换了一个说法提到,回过头来终于听清楚了原来是有个 Ryan 的人名,Ryan 是大会的第三个演讲者。在翻译方面,已经越来越游刃有余了,这几个月又把英语和语文捡了起来,还算很有成就感的事情。 传送门 最重要的 React 官方文档:Introducing Hooks [译] React 的今天和明天(图文版) —— 第一部分 [译] React 的今天和明天(图文版) —— 第二部分 前两部分视频地址(B 站):【React Conf 2018】React 的今天和明天中英文双字幕 第一部分视频地址(爱奇艺):React Today And Tomorrow Part I —— Sophie’s Keynote 中英双语字幕 第二部分视频地址(爱奇艺):React Today And Tomorrow Part II —— Dan’s Keynote 中英双语字幕 Dan 对本次演讲的精炼文章(需要科学上网):Making Sense of React Hooks 上面文章的译文:[译] 理解 React Hooks 一篇非常好的理解 React Hook 的文章:30 分钟精通 React Hooks 如果发现中英文字幕存在错误或其他需要改进的地方,欢迎到本项目的 GitHub 仓库 对英文字幕或译文进行修改并 PR,谢谢大家。当然后本视频还有后面 Ryan 给我带来的第三段题目为90% Cleaner React with Hooks 的演讲,欢迎有兴趣的小伙伴一起参与英文字幕校对和翻译工作。 原视频地址:React Today and Tomorrow and 90% Cleaner React with Hooks 演讲者: Dan Abramov 英文字幕出自:YouTube 机器翻译 英文字幕校对、翻译:Ivocin, 程序媛_小发","link":"/2018/11/27/React_today_and_tomorrow_video2/"},{"title":"2018 前端全面回顾","text":"原文地址:A comprehensive look back at front-end in 2018 原文作者:Kaelan Cooter 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/a-comprehensive-look-back-at-frontend-in-2018.md 译者:Ivocin 校对者:Junkai Liu, wuzhe 拿一杯咖啡,坐下来,慢慢品读。我们的回顾不容错过。 Web 开发一直是一个快速发展的领域 —— 我们很难跟上在过去的一年中所有的浏览器变更、函数库的发布以及冲击思维的程序设计趋势。 前端行业每年都在增长,这使得普通开发者很难跟上。因此让我们退后一步,回顾一下 2018 年 Web 开发社区发生了哪些变化。 我们目睹了过去几年 JavaScript 爆炸式的发展。随着互联网对全球经济变得更加重要,谷歌和微软等巨头意识到他们需要更好的工具来创建下一代 Web 应用程序。 在这种环境下,以 ECMAScript 2015(又名 ES6)为开端,JavaScript 被引领出自创造以来最大的变革浪潮。现在 JavaScript 每年发布的版本都为我们带来了令人兴奋的新特性:如类、生成器、迭代器、promise、全新的模块系统等等。 这开启了 Web 发展的黄金时代。许多最流行的工具、函数库和框架在 ES2015 发布后立即流行了起来。即使主流浏览器厂商对新标准的支持还未过半,Babel 编译器项目就让成千上万的开发人员抢先一步尝试新功能。 前端开发者首次不需要被他们公司需要支持的最古老的浏览器限制,可以按照自己的节奏自由创新。三年和三个 ECMAScript 版本之后,这个 Web 开发的新时代并没有放缓前进的脚步。 JS 语言的新特性与之前的版本相比,ECMAScript 2018 的功能相当简单,只添加了对象 rest/spread 属性,异步 iteration 和 Promise.finally,Babel 和 core-js 现在已经支持了所有这些新特性。大多数浏览器和 Node.js 全部都支持了ES2018,除了 Edge,它只支持 Promise.finally。对于许多开发人员来说,这意味着他们所需的所有语言特性都被他们需要兼容的浏览器支持了 —— 甚至有人怀疑 Babel 是否真的是必需的了。 新的正则表达式特性JavaScript 一直缺乏像 Python 这样语言的一些更高级的正则表达式功能 —— 直到现在才推出类似的特性。ES2018 增加了四个新特性: 后行断言(lookbehind assertions),为自 1999 年以来一直使用该语言的先行断言( lookahead assertions) 提供了缺失的补充。 s(dotAll)标志,它匹配除行终止符之外的任何单个字符。 命名捕获组,通过基于属性的捕获组查找,可以更轻松地使用正则表达式。 Unicode 属性转义,可以编写能够识别 Unicode 编码的正则表达式了。 虽然这些新特性中的许多功能多年来都有解决方法和替代库,但它们都没有原生实现的速度快。 新的浏览器特性今年发布了相当多的新的 JavaScript 浏览器 API。几乎所有内容都有所改进 —— 网络安全、高性能计算和动画等等。让我们按领域划分它们以更好地了解它们带来的影响。 WebAssembly尽管去年对 WebAssembly v1 的支持被添加到了主流浏览器中,但它尚未被开发者社区广泛采用。WebAssembly Group 针对垃圾回收、ECMAScript 模块集成和线程等功能提供了宏大的功能路线图。也许有了这些功能,我们才会看到 WebAssembly 在 Web 应用程序中被广泛采用。 有一部分问题是 WebAssembly 需要大量的步骤才能开始使用,而许多习惯于使用 JavaScript 的开发人员并不熟悉使用传统的编译语言。Firefox 推出了一个名为 WebAssembly Studio 的在线 IDE,可以让使用 WebAssembly 变得简单。如果你希望将其集成到现有的应用程序中,现在有很多工具可供选择。Webpack v4 为 WebAssembly 模块添加了实验性内置支持,这些模块紧密集成到构建和模块系统中,并提供 source map 支持。 Rust 已成为编译 WebAssembly 的最佳语言。它提供了一个健壮的包生态系统,具有 cargo,可靠的性能和易于学习的语法。现在已经有一个新兴的工具生态系统将 Rust 与 Javascript 集成在一起。你可以使用 wasm-pack 将 Rust WebAssembly 包发布到 npm 上。如果你使用了 webpack,现在可以使用 rust-native-wasm-loader 在应用程序中无缝集成 Rust 代码。 如果你不想放弃 JavaScript 来使用 WebAssembly,你很幸运 —— 现在有几种选择。如果你熟悉 Typescript,可以使用 AssemblyScript 项目,该项目使用官方 Binaryen 编译器和 Typescript。 因此,它适用于现有的 Typescript 和 WebAssembly 工具。Walt 是另一个坚持 JavaScript 语法的编译器(使用类似于 Typescript 的类型提示),并直接编译为 WebAssembly 文本格式。它是零依赖的,具有非常快的编译速度,并可以与 webpack 集成。这两个项目都在积极开发中,根据你的标准,它们可能会不适用于生产环境。无论如何,它们都值得一试。 共享内存现代 JavaScript 应用程序经常把大量的计算放在 Web Workers 中,以避免其阻塞主线程并中断浏览体验。虽然 Worker 已经推出几年了,但它的局限性使他们无法更广泛地采用。Worker 可以使用 postMessage 方法在其他线程之间传输数据,该方法克隆发送的数据(较慢)或使用可传输的对象(更快)。因此,线程之间的通信要么是慢速的,要么是单向的。对于简单的应用程序没有太大问题,但它限制了使用 Worker 构建更复杂的架构。 SharedArrayBuffer 和 Atomics 是允许 JavaScript 应用程序在上下文之间共享固定内存缓冲区并对它们执行原子操作的新功能。但是,在发现共享内存使浏览器容易受到以前未知的被称为 Spectre 的定时攻击后,浏览器对该特性的支持被暂时删除了。Chrome 在 7 月发布了一项新的安全功能,可以缓解该漏洞,从而重新启用了 SharedArrayBuffers 功能。在 Firefox 中,该功能默认情况下是禁用的,但可以重新启用。Edge 完全取消了对SharedArrayBuffers 的支持,微软尚未表示何时会重新启用。希望到明年所有浏览器都会采用缓解策略,以便可以使用这个关键的缺失功能。 CanvasCanvas 和 WebGL 等图形 API 已经推出几年了,但它们一直被限于在主线程中进行渲染。因此,渲染可能会阻塞主线程。这会导致糟糕的用户体验。OffscreenCanvas API 允许你将 canvas 上下文(2D 或 WebGL)的控制权转移给 Web Worker,从而解决了这个问题。在 Worker 使用 Canvas API 和平时没有区别,而且不会阻塞主线程,并可以无缝渲染。 鉴于显著的性能提升,可以期待图表和绘图库会很快支持它。目前浏览器支持仅限于 Chrome 和 Firefox,而 Edge 团队尚未公开表示支持。你可以期望它能和 SharedArrayBuffers 以及 WebAssembly 很好地配对,允许 Worker 基于任何线程中存在的数据,使用任何语言编写的代码进行渲染,所有这些都不会造成糟糕的用户体验。这可能使网络上实现高端游戏的梦想成为现实,而且可以在 Web 应用程序中使用更复杂的图形。 新的绘图和布局 API 正努力被引入 CSS。目标是向 Web 开发人员公开 CSS 引擎的部分内容,以揭开 CSS 的一些“神奇”神秘面纱。W3C 的 CSS Houdini 工作组由主要浏览器供应商的工程师组成,在过去两年中一直在努力发布几个规范草案,这些规范目前正处于设计的最后阶段。 CSS Paint API 是其中最早登陆浏览器的新 CSS API ,它在 1 月份登陆 Chrome 65。它允许开发人员使用类似 context 的 API 绘制图像,可以在 CSS 中调用图像的任何地方使用它。它使用新的 Worklet 接口,这些接口基本上是轻量级,高性能的类似 Worker 的构造,用于专门的任务处理。和 Worker 一样,它们在自己的执行上下文中运行,但与 Worker 不同的是,它们是线程不可感知的(浏览器选择它们运行的线程),并且它们可以访问渲染引擎。 使用 Paint Worklet,你可以创建一个背景图像,当其中包含的元素发生更改时,该图像会自动重绘。使用 CSS 属性,你可以添加在更改时触发重新绘制的参数,并可通过 JavaScript 进行控制。所有浏览器都承诺支持该 API,除了 Edge,但是现在有一个 polyfill 可以使用。有了这个 API,我们将开始看到组件化图像的使用方式,这与我们现在看到的组件类似。 动画大多数现代 Web 应用程序使用动画作为用户体验的重要部分。像 Google 的 Material Design 这样的框架把动画作为其设计语言的重要组成部分,并认为它们对于创造富有表现力和易于理解的用户体验至关重要。鉴于它们的重要性的提高,最近推出了一个更强大的 JavaScript 动画 API,这个就是 Web Animations API(WAAPI)。 正如 CSS-Tricks 所说,WAAPI 提供了比 CSS 动画更好的开发人员体验,你可以轻松地记录和操作 JS 或 CSS 中定义的动画状态。目前浏览器支持主要限于 Chrome 和 Firefox,但有一个官方的 polyfill 可以满足你的需求。 性能一直是 Web 动画的一个问题,Animation Worklet 解决了这个问题。这个新的 API 允许复杂的动画并行运行 —— 这意味着更高的帧速率动画不受主线程卡顿的影响。Animation Worklet 遵循与 Web Animations API 相同的接口,但在 Worklet 执行上下文中。 它将在 Chrome 71(截至撰写本文时的下一个版本)发布,而其他浏览器可能会在明年某个时候发布。如果想今天就试试,可以在 GitHub 上找到官方的 polyfill 和示例仓库。 安全Spectre 定时攻击并不是今年唯一的网络安全恐慌。npm 固有的脆弱性在过去已经写了很多,上个月我们得到了一个告警提醒。这不是 npm 本身的安全漏洞,而是一个名为 event-stream 的包,被许多流行软件包使用。npm 允许包作者将所有权转让给任何其他成员,黑客说服所有者将其转让给他们。然后,黑客发布了一个新版本,它依赖于他们创建的名为 flatmap-stream 的软件包,其代码可以窃取比特币钱包,如果该恶意软件和 copay-dash 一起安装,就会窃取用户的比特币钱包。 考虑到 npm 的运行方式,社区成员倾向于安装看似有用的随机 npm 包,这种攻击只会变得更加普遍。社区对包所有者非常信任,现在信任受到了极大的质疑。npm 用户应该知道他们正在安装的每个软件包(包括依赖项的依赖关系),使用锁定文件来锁定版本并注册 Github 提供的安全警报。 Npm 意识到社区的安全问题,他们在过去的一年里已经采取措施去改进它。你现在可以使用双因素身份验证来保护你的 npm 帐户,并且 npm v6 现在包含了安全审核命令。 监控Reporting API 是一种新标准,旨在通过在发生问题时发出警报,使开发人员更容易发现应用程序的问题。如果你在过去几年中使用过 Chrome DevTools 控制台,你可能已经看到了 [intervention] 警告消息,用来提醒用户使用了过时的 API 或执行了可能不安全的操作。这些消息仅限于客户端,但现在你可以使用新的 ReportingObserver 将其报告给分析工具。 有两种报告: 废弃,当你使用过时的 API 时会发出警告,并通知你何时删除它。它还会告诉你使用它的文件名和行号。 干预,当你以无意识的、危险或不安全的方式使用 API 时,它会发出警告。 而像 LogRocket 这样的工具可以让开发人员深入了解应用程序中的错误。到目前为止,第三方工具还没有任何可靠的方法来记录这些警告。这意味着问题要么被忽视,要么表现为难以调试的错误消息。Chrome 目前支持了 ReportingObserver API,其他浏览器很快就会支持它。 CSS虽然 JavaScript 得到了所有人的关注,但几个有趣的 CSS 新功能在今年登陆了浏览器。 很多人不知道,其实并没有统一的类似于 ECMAScript 的 CSS3 规范。最后一个官方统一标准是 CSS2.1,而 CSS3 适用于在其之后发布的内容。与 CSS2 不同的是,CSS3 的每个部分都单独标准化为 “CSS 模块”。 MDN 对每个模块标准及其状态有一个很好的概述。 截至 2018 年,现在所有主流浏览器都完全支持一些较新的功能(这是 2018 年,IE 不是主流浏览器)。这包括 flexbox、自定义属性(变量)和网格布局。 虽然过去一直在讨论如何向 CSS 添加对嵌套规则的支持(就像 LESS 和 SASS 那样),但这些提案被搁置了。在 7 月,W3C 的 CSS 工作组决定再次审视该提案,但目前还不清楚它是否是一个优先事项。 Node.jsNode 继续在遵循 ECMAScript 标准方面取得良好进展,截至 12 月,它们支持了所有 ES2018 标准。但另一方面,他们采用 ECMAScript 模块系统的速度很慢,因此缺少一项与浏览器比肩的关键功能,浏览器已经支持 ES 模块一年多了。Node 实际上在 v11.4.0 版本标志后面添加了一项实验支持,但是这需要文件使用新的 .mjs 后缀,这使得他们开始担忧:用户的接受速度可能会十分缓慢,以及其对 Node 的丰富包生态系统的影响。 如果你希望快速开始,并且不想使用实验性内置支持,可以尝试使用被 Lodash 的创建者称为 esm 的一个有趣的项目,它为 Node ES 模块支持提供了比官方解决方案更好的互操作性和性能。 框架和工具ReactReact 今年发布了两个值得注意的版本。React 16.3 附带了一组新的生命周期方法和一个新的官方 Context API。React 16.6 添加了一个名为 “Suspense” 的新功能,它使 React 能够在组件等待如数据获取或代码分割等任务完成时暂停渲染。 今年最受关注的 React 话题是引入了 React Hooks。该提案为了让编写更小的组件更简单,并且不会牺牲迄今为止仅限于类组件的有用功能。React 将附带两个内置钩子,State Hook(允许函数式组件使用状态)和 Effect Hook(可以让你在函数式组件中执行副作用)。虽然没有计划从 React 中删除类,但 React 团队显然希望 Hooks 成为 React 未来的核心。提案宣布之后,社区有了积极的反应(有些人可能会说过度夸大了)。如果你有兴趣了解更多信息,请查看 Dan Abramov 的博文里面的全面概述。 明年,React 计划发布一项名为 Concurrent mode(以前称为 “async mode” 或 “async rendering”)的新功能。这将使 React 在不阻塞主线程的情况下渲染大型组件树。对于具有深度组件树的大型应用程序,性能的节省可能非常显着。目前还不清楚该 API 究竟是什么样子,但 React 团队的目标是很快完成它并在明年某个时候发布。如果你对采用此功能感兴趣,请通过采用 React 16.3 中发布的新生命周期方法确保你的代码能够兼容该功能。 React 流行度继续增长,根据 JavaScript 2018 趋势报告显示,64% 的受访者选择使用 React 并将再次使用它(比去年增加了 7.1%),相比之下 Vue 为 28%(增长了 9.2%),Angular 为 23%(增长了 5.1%)。 WebpackWebpack 4 于 2 月发布,带来了巨大的性能改进,内置生产和开发模式,做了如代码分割和压缩的易于使用的优化,实验性的 WebAssembly 支持和 ECMAScript 模块支持。Webpack 现在比以前的版本更容易使用,以前如代码分割和代码优化等复杂的功能,现在设置起来非常简单。结合使用 Typescript 或 Babel,webpack 仍然是 Web 开发人员的基础工具,竞争对手似乎不太可能在不久的将来出现并取而代之。 BabelBabel 7 于今年 8 月发布,这是近三年来的第一次重大发布。主要更改包括更快的构建时间,新的包命名空间以及各种“阶段”和按照年度命名的 ECMASCript 预设包的弃用,以支持 preset-env,它通过自动包含你支持的浏览器所需的插件,极大地简化了配置 Babel 的过程。此版本还添加了自动 polyfilling,无需导入整个 Babel polyfill(体积相当大)或显式导入所需的 polyfill(这可能非常耗时且容易出错)。 Babel 现在也支持 Typescript 语法,使开发人员更容易将 Babel 和 Typescript 一起使用。Babel 7.1 还增加了对新的装饰器提案的支持,该提议与社区广泛采用的过时提案不兼容,但与浏览器支持的内容相匹配。值得庆幸的是,Babel 团队发布了一个兼容性软件包,可以使升级更容易。 ElectronElectron 仍然是最常用的桌面 JavaScript 应用程序打包方式,尽管这是否是一件好事还是有争议的。现在一些最流行的桌面应用程序使用了 Electron,可以使跨平台开发应用程序更加简单,从而降低开发成本。 一个常见的抱怨是,使用 Electron 的应用程序会使用太多内存,因为每个应用程序都打包整个 Chrome 实例(这会非常占用内存)。Carlo 是来自 Google 的 Electron 替代品,它使用本地安装的 Chrome 版本(需要在本地安装),从而减少了内存消耗大的问题。Electron 本身在提高性能方面没有取得多大进展,近期的更新主要集中在更新 Chrome 依赖项和小的 API 改动上面。 Typescript在去年,Typescript 的受欢迎程度大大提高,成为了JavaScript 统治地位的 ES6 的主要挑战者。自微软每月发布新版本以来,开发在过去一年中取得了相当快的进展。Typescript 团队非常关注开发人员的体验,包括语言本身和围绕它的编辑器工具。 最近的版本增加了更多开发人员友好的错误格式和强大的重构功能,如自动导入更新和导入组织等。与此同时,TypeScript 继续在提升类型系统上发力,如近期的条件类型和未知类型两个新功能。 JavaScript 2018 趋势报告指出,近一半的受访者使用 TypeScript,和过去两年相比具有强劲的上升趋势。相比之下,它的主要竞争对手 Flow 已经停滞不前,大多数开发者表示他们不喜欢 Flow 缺乏工具,并且流行势头降低。Typescript 受到赞赏,因为开发人员可以通过使用强大的编辑器轻松编写健壮且优雅的代码。开发者注意到了,TypeScript 的发起者微软似乎更愿意支持它,而 Facebook 对 Flow 的支持就差了一截。 题外话:LogRocket,一个用于 web 应用程序的DVR LogRocket 是一个前端日志记录工具,可让你像在自己的浏览器中一样重现问题。LogRocket 不是猜测错误发生的原因,也不是要求用户提供屏幕截图和日志转储,而是让你重播会话以快速了解出现了什么问题。它适用于任何应用程序,与框架无关,并且具有从 Redux、Vuex 和 @ngrx / store 记录上下文的日志插件。 除了记录 Redux 操作和状态之外,LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、带有 header 和 body 的网络请求/响应、浏览器元数据和自定义日志。它还使用 DOM 来记录页面上的 HTML 和 CSS,能够重新创建即使是最复杂的单页应用程序的像素级完美视频。 欢迎免费试用。 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2019/01/16/a-comprehensive-look-back-at-frontend-in-2018/"},{"title":"轻松搭建赛博朋克风格个人博客 —— Hexo 篇","text":"最近在整理个人博客,之前的博客是基于 Hexo 3.x 版本的,一晃两年多过去了,Hexo 已经升级到了 5.x 版本,新版本带来了一系列新特性,并且性能得到了极大的提升。而个人喜欢的 Icarus 主题也有了大版本的更新,更是带来了赛博朋克的主题,十分惊艳,话不多说,直接看效果。 现在让我们从零开始,快速搭建一个赛博朋克风格个人博客。 Step 1 使用 Hexo-CLI 初始化项目初始化项目非常简单,按照官网文档操作即可,需要注意 Node 和 Git 是必备。为了避免全局安装,这里使用 npx 命令: 123$ npx hexo init <folder>$ cd <folder>$ yarn 使用 generate 命令生成静态文件,可简写如下: 1npx hexo g 使用 server 命令启动本地服务器查看页面效果,可简写如下: 1npx hexo s 此时,打开 localhost:4000, 一个默认的 landscape 主题的博客页面就出现了,如下: 具体详见这个 Commit Step 2 更换为 Icarus 主题更换主题也非常简单,我们查看 Icarus 文档。更换主题主要有两种方式,一种是使用 npm 安装主题的方式,另一种是下载源码放到 theme 文件夹的方式。简单起见,我们先采用 npm 的方式: 1$ yarn add hexo-theme-icarus 使用 hexo 命令修改主题为 Icarus: 1$ npx hexo config theme icarus 启动服务器,很不幸,报错了: 提示很清楚,缺少依赖,我们添加依赖: 1$ yarn add [email protected] hexo-renderer-inferno@^0.1.3 再次尝试构建并启动,成功出现 Icarus 主题了: 迫不及待尝试赛博朋克风格主题了,非常简单,在 _config.icarus.yml 文件中修改: 12# Icarus theme variant, can be "default" or "cyberpunk"variant: cyberpunk 再次尝试构建并启动,赛博朋克风格主题出现: 具体代码详见这个 Commit。 Step 3 自定义配置下面我们修改配置文件 _config.yml 和 _config.icarus.yml ,配置网站相关信息。 主要包括 logo、favicon、navbar 的 menu 和 links、footer、donates(赞助信息,注释掉没用的支付渠道)、widgets。 这里详细说一下 widgets,widgets 就是页面上的各个区域的挂件,可以通过配置灵活改变其位置和具体信息,根据喜好决定布局采用 3 栏还是 2 栏布局。 profile widgets: 这块是用户信息模块,把相关信息改成你自己的就好啦,下面有一个 social_links,也可以自定义配置,如果没有 Twitter 和 FaceBook,可以把相关信息注释掉,也可以添加其他链接,注意图标是基于 fontawesome 的。 subscribe_email、adsense 都用不上,可以删掉 具体修改细节可以参考这个 Commit。 修改完的效果如下: Step 4 源码方式使用 Icarus 主题刚才说到使用 Icarus 主题有两种方式, npm 包的方式虽然简便,但是如果想对 Icarus 主题有更深的配置就不太好弄了,尤其是过去 Icarus 一直都采用的是源码模式,很多 Issue 的解决方案都是修改源码的,而对应的 npm 包的方式则很少提及,所以我们也改为使用源码方式,方便后续配置。 首先删掉 hexo-theme-icarus 依赖,在 Icarus 仓库下载代码,解压后拷贝到 theme 文件夹中。 详见这个 Commit。 Step 5 配置样例文章下面我们删掉默认的 Hello World 文章,创建一个自己的文章。 注意头部配置文件相关信息,在新版 Icarus 中头图需要额外配置 cover 选项,如下: 12345678910111213141516---title: "【译】下一代前端构建工具 ViteJS 中英双语字幕"date: 2021/03/08 11:12:25categories:- Front-End- Toolstags:- Vite- GitHub- Translationtoc: true # 是否显示目录thumbnail: '/images/vitet.png' # 缩略图cover: '/images/vitet.png' # 头图--- 详见这个 Commit。 Step 6 文章内容过长怎么办首页应该展示更多的文章,如果文章过长,用户下滑就只会看到同一篇文章,我们如果只想让用户看一部分内容怎么办呢?非常简单,在 md 文件中添加 <!-- more --> 即可,添加完之后,就会出现“阅读更多”的按钮,首页就能看到多篇文章了。 代码详见这个 Commit。 Step 7 怎么样让文章两栏展示目前文章页仍然和首页一样,是三栏布局,为了有效利用空间,希望文章页能够两栏布局。此时我们需要在 Icarus 源码文件夹添加 _config.post.yml 文件,并配置成两栏布局: 123456789101112widgets: # Profile widget configurations - # Where should the widget be placed, left sidebar or right sidebar position: right type: toc # Whether to show the index of each heading index: true # Whether to collapse sub-headings when they are out-of-view collapsed: false # Maximum level of headings to show (1-6) depth: 3 来看看效果: 代码详见这个 Commit。 Step 8 部署到 GitHub Pages 上这块内容就不再赘述,操作方法官网有详细描述。 在 _config.yml 中配置你的 GitHub Pages 对应的仓库地址,如我的是: 1234deploy: type: git repo: https://github.com/Ivocin/ivocin.github.io.git 每次都构建完,执行 npm hexo d 就可以快速部署了。 Step 9 其他功能当然还有包括评论、百度统计等功能,Hexo 官网和 Icarus 文档都有详细描述,这里就不再赘述了。 其他 CMS(Content Management System) 产品市面上有非常多的 CMS 产品,侧重点各有不同,罗列如下: Hexo、VuePress、Nuxt、Docsify、Docute、GitBook、Gatsby、Hugo、Next.js、Jekyll 、WordPress、Drupal、Dumi。 Hexo 之前一直是 Vue 的文档托管方案,但是 Hexo 主题系统太过于静态以及过度地依赖纯字符串,所以有了后来的 VuePress。当然 Hexo 也有很多优点,比如强大的 Tag Plugins (但私有语法也带来了迁移的问题)、超多的主题(目前有 311 个,很丰富,但是质量参差不齐,没有评分功能,在官网选择主题对于选择困难症的人来说一定非常痛苦),最新的 5.x 版本速度也有了很大的提升,总的来说,一般博客就够用了。其他的适合写博客的有 Gatsby、Hugo、Jekyll ,没深入使用过,先不做评价。 文档类的选择就不用很困难了,Vue 类的用 VuePress,未来可能是 VitePress,React 类的文档用 Dumi 就好了。 后续计划有时间对其他 CMS 产品一一试用并比较,会从 Gatsby 开始。","link":"/2021/03/13/build-hexo-cyberpunk-blog/"},{"title":"使用 Eject 方式在 create-react-app 中使用 Ant Design of React","text":"Ant Design 官网对 如何 react-app-rewired 的方式进行按需加载进行了说明,详见 在 create-react-app 中使用 一文,文中有这样一段话 你也可以使用 create-react-app 提供的 yarn run eject 命令将所有内建的配置暴露出来。不过这种配置方式需要你自行探索,不在本文讨论范围内。 本文主要就Eject方式进行探索 使用create-react-app创建项目 参考:如何扩展 Create React App 的 Webpack 配置 的Eject方式 首先使用create-react-app创建一个项目 1$ create-react-app antd-test 创建完项目后,进入项目目录,执行 yarn run eject 或 npm run eject 1$ npm run eject 执行后会出现提示,该操作不可逆,选择y继续 成功eject后会暴露webpack的配置,package.json增加了很多的依赖 安装antd使用 cnpm 安装 antd 1$ cnpm install antd 修改src/App.js ,引入 antd 的按钮组件。 123456789101112131415import React, { Component } from 'react';import Button from 'antd/lib/button';import './App.css';class App extends Component { render() { return ( <div className="App"> <Button type="primary">Button</Button> </div> ); }}export default App; 执行npm install 安装依赖,并启动项目 12$ cnpm install$ npm start 启动之后发现button并没有样式,需要引入antd的css文件 修改 src/App.css,在文件顶部引入 antd/dist/antd.css。 1234567@import '~antd/dist/antd.css';.App { text-align: center;}... 使用 babel-plugin-import 按需引入 antd 样式在文件顶部引入 antd/dist/antd.css实际上加载了全部的 antd 组件的样式(对前端性能是个隐患)。babel-plugin-import 是一个用于按需加载组件代码和样式的 babel 插件(原理) 1$ cnpm install babel-plugin-import --save-dev 修改src/App.js 1234...- import Button from 'antd/lib/button';+ import { Button } from 'antd';... 然后移除前面在 src/App.css 里全量添加的 @import '~antd/dist/antd.css'; 此时发现按钮样式不生效了,最简单的方式是修改package.json文件里的babel配置, 增加babel-plugin-import的配置 1234567891011... "babel": { "presets": [ "react-app"- ]+ ],+ "plugins": [+ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]+ ] }... 重新执行npm start,样式重新生效 至此使用Eject方式按需引入antd的方式已经探索完毕。","link":"/2018/07/29/create_react_app_antd_eject/"},{"title":"create-react-app 创建项目有关 less 的若干问题","text":"在上篇文章使用Eject方式在 create-react-app 中使用 Ant Design of React中, 使用create-react-app创建了React项目,并使用 Eject方式暴露出了Webpack的配置,并成功按需引入了antd。本文主要内容:解决create-react-app创建项目后less不生效的问题;antd按需引入less源文件,以及遇到的bezierEasing.less文件报错问题;antd本地字体的配置方法;less使用css module的配置。 直接引入less样式不生效创建test.less,并在App.js中引入 123.test { color: red;} 1234567// App.js...import './test.less'... <div className="test">test</div> <Button type="primary">Button</Button>... 发现test的颜色并没有生效 安装 less 和 less-loader ,并修改Webpack配置1$ cnpm i less less-loader --save-dev 修改webpack配置修改 webpack.config.dev.js 和 webpack.config-prod.js 配置文件, 增加less文件配置: 123456789101112131415161718// webpack.config.dev.js... { test: /\\.less$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1, }, }, { loader: require.resolve('less-loader') // compiles Less to CSS } ], },... 重启项目后,less样式已经生效 antd 的样式使用 less 源文件方式引入在babel-plugin-import 中对style有这样对说明: ["import", { "libraryName": "antd" }] : import js modularly ["import", { "libraryName": "antd", "style": true }] : import js and css modularly (LESS/Sass source files) ["import", { "libraryName": "antd", "style": "css" }] : import js and css modularly (css built files) 修改package.json,将style的值改为true 1234567891011... "babel": { "presets": [ "react-app" ] ], "plugins": [ ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }] ] }... 重启之后,编译错误,提示bezierEasing.less文件的.bezierEasingMixin()方法报错: 按照报错提供的issue地址查看 : https://github.com/ant-design/ant-motion/issues/44主要有2种解决方式 将 less 版本降到 3.0 以下, 应该可行,降级的方法感觉不太好,没有测试 less loader 增加配置,开启 JavaScript :12345678910// webpack.config.dev.js... {- loader: require.resolve('less-loader') // compiles Less to CSS+ loader: require.resolve('less-loader'), // compiles Less to CSS+ options: {+ javascriptEnabled: true+ } } 重新npm start ,项目可以正常启动。 antd使用本地字体 iconfont 2018年9月5日更新,9月开学季, ant design 系列迎来了一系列的重大更新。umi 迎来了 2.0 版本, 随之而来的是 ant design pro 使用 umi 2.0 构建的 2.0 版本。 今天注意到 ant design 的 3.9.0 版本一个重大的更新是对 Icon 进行了重构, 使用 SVG 代替之前的 css font icon。所以如果使用 ant design 3.9.0 以上的版本, 就不会存在离线找不到字体文件的问题了。如果你使用的 ant design 在 3.9.0 版本以下,可以参考下文离线使用 iconfont 的方式。 Ant Design 默认的 iconfont 文件托管在 iconfont.cn 并默认使用平台提供的 alicdn 地址,公网可访问使用。 由于 alicdn 对部分域名有访问限制,或者需要内网环境使用,需要将字体下载到本地 最新的 iconfont 文件可以到 此链接 下载。 下载后将字体文件放入 public/iconfont/ 路径下 由于项目使用的是create-react-app创建项目,且antd的样式使用babel-plugin-import按需加载样式,所以只能采用 定制主题中的less-loader的modifyVars配置来覆盖原来的样式变量。 具体改动修改 webpack.config.dev.js 和 webpack.config-prod.js 配置文件 1234567891011// webpack.config.dev.js... { loader: require.resolve('less-loader'), // compiles Less to CSS options: { javascriptEnabled: true, modifyVars: { "icon-url": "'/public/iconfont/iconfont'" } } } 重启项目,成功引入了本地字体 需要注意的是webpack.config-prod.js文件中的icon-url路径需要将public替换为生产环境项目文件路径,打包之后public中的文件和文件夹直接复制到bulid路径下。使用相对路径会报错无法编译,这点不知道有没有更好的处理方式,希望读者大神们提供更好的方式。 1234567891011// webpack.config-prod.js... { loader: require.resolve('less-loader'), // compiles Less to CSS options: { javascriptEnabled: true, modifyVars: { "icon-url": "'/your-project-name/iconfont/iconfont'" } } } css module 形式引入less增加css-loader的配置 1234567891011121314151617181920212223 { test: /\\.less$/, // exclude: [/node_modules/], use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1,+ modules: true }, }, { loader: require.resolve('less-loader'), // compiles Less to CSS options: { javascriptEnabled: true, modifyVars: { "icon-url": "'/public/iconfont/iconfont'" } } } ], }, 修改App.js,使用css module方式引入 1234567891011121314- import './test.less'+ import styles from './test.less' class App extends Component { render() { return ( <div className="App">- <div className="test">test</div>+ <div className={styles.test}>test</div> <Button type="primary">Button</Button> </div> ); } } 重启项目,css module引入的test样式生效了,但是antd的按钮样式失效了 需要修改 webpack.config.dev.js,只对src中的less文件开启css module,这里写法有点麻烦,应该有更好的方式。 1234567891011121314151617181920212223242526272829303132333435363738// webpack.config.dev.js... { test: /\\.less$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1 }, }, { loader: require.resolve('less-loader'), // compiles Less to CSS options: { javascriptEnabled: true, modifyVars: { "icon-url": "'/public/iconfont/iconfont'" } } } ], }, { test: /\\.less$/, include: [/src/], use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1, modules: true }, } ], },... 2018-08-02更新,评论区有兄弟留言说重新npm install后,css module引入的样式失效了,今天测试了一下确实有这个问题,试着改了上面的对less文件处理的loader顺序可以解决这个问题,但是原理不是很清楚,还请了解原理的兄弟帮忙指点,多谢 1234567891011121314151617181920212223242526272829303132333435363738// webpack.config.dev.js... { test: /\\.less$/, include: [/src/], use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1, modules: true }, } ], }, { test: /\\.less$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1 }, }, { loader: require.resolve('less-loader'), // compiles Less to CSS options: { javascriptEnabled: true, modifyVars: { "icon-url": "'/public/iconfont/iconfont'" } } } ], },... 总结本文在eject的create-react-app的项目基础上,按需引入了antd,遇到了有关less的若干问题:解决了less文件不生效的问题,内网环境使用本地iconfont的配置方式,css module的配置方式。本文还有一些配置方式不是最优的方式,希望各位能够给出更好的方案。 最近在起步React,准备记录以下自己的学习和踩坑过程。下一篇文章应该是dva的使用。 参考资料 https://github.com/ant-design/antd-init/tree/master/examples/local-iconfont Antd定制主题 create-react-app项目添加less配置 如何在react中使用antd+less+css modules [译] react-css-modules","link":"/2018/07/30/create_react_app_less_issues/"},{"title":"DvaJS 的学习之路 1 - dva+umi 官网例子学习","text":"前置知识 dva roadhog umi dva知识地图 使用 umi 改进 dva 项目开发 umi + dva,完成用户管理的 CURD 应用 本文主要是在 dva 作者 @sorrycc 的例子 umi + dva,完成用户管理的 CURD 应用基础上进行的一些自己的学习记录。关于评论区的小伙伴说的照抄 @sorrycc 大神的github文章的问题,这里回应下,没错,本文 90% 以上的文字都出自 umi + dva,完成用户管理的 CURD 应用 这篇文章,而且作者和文章出处一开始就交代清楚了,写这个文章的目的是记录一下自己跟着教程 step by step 完成这个应用的过程,并不是教程,期望教程的小伙伴, @sorrycc 大大在 dva 官网上提供了很多丰富的例子,可以参考。 2018年9月10日更新,umi 已经升级到了 2.0 版本,希望使用 umi@2 开发的同学请移步至 DvaJS的学习之路2 - umi@2 + dva,完成用户管理的 CURD 应用。 开始之前: 确保 node 版本是 8.4 或以上 用 cnpm 或 yarn 能节约你安装依赖的时间 Step 1. 安装 dva-cli@next 并创建应用先安装 dva-cli,并确保版本是 1.0.0-beta.2 或以上。 1234$ cnpm i dva-cli@next -g$ dva -vdva-cli version 1.0.0-beta.4 dva version 2.3.1 这里需要注意的是安装dva-cli@next版本的原因是目前 umi 还未提供官方的脚手架工具,需要 dva + umi 结合使用可以使用 dva-cli@next 方式来初始化项目。详见:例子和脚手架。 然后创建应用: 12$ dva new user-dashboard$ cd user-dashboard Step 2. 配置代理,能通过 RESTFul 的方式访问 http://localhost:8000/api/users修改 .umirc.js ,加上 “proxy” 配置: 1234567proxy: { "/api": { "target": "http://jsonplaceholder.typicode.com/", "changeOrigin": true, "pathRewrite": { "^/api" : "" } }}, 然后启动应用:(这个命令一直开着,后面不需要重启) 1$ npm start 浏览器会自动开启,并打开 http://localhost:8000。 访问 http://localhost:8000/api/users ,就能访问到 http://jsonplaceholder.typicode.com/users 的数据。(由于 typicode.com 服务的稳定性,偶尔可能会失败。不过没关系,正好便于我们之后对于出错的处理) Step 3. 生成 users 路由umi 中文件即路由,所以我们要新增路由,新建文件即可,详见 https://umijs.org/guide/router.html 。 新建 src/pages/users/page.js,内容如下: 1234567export default () => { return ( <div> Users Page </div> )} 然后访问 http://localhost:8000/users ,你会看到 Users Page 的输出。 注意:使用 umi 约定 src/pages 目录下的文件即路由,而文件则导出 react 组件。可以看到 umi 的特点:以页面维度,将 models 、 services 组织到一起。 Step 4. 构造 users model 和 service新增 service: src/pages/users/services/users.js: 12345import request from '../../../utils/request';export function fetch({ page = 1 }) { return request(`/api/users?_page=${page}&_limit=5`);} 注意这里的 page 参数默认为 1,limit 参数设置为 5 新增 model: src/pages/users/models/users.js,内容如下: 1234567891011121314151617181920212223242526272829import * as usersService from '../services/users';export default { namespace: 'users', state: { list: [], total: null, }, reducers: { save(state, { payload: { data: list, total } }) { return { ...state, list, total }; }, }, effects: { *fetch({ payload: { page } }, { call, put }) { const { data, headers } = yield call(usersService.fetch, { page }); yield put({ type: 'save', payload: { data, total: headers['x-total-count'] } }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/users') { dispatch({ type: 'fetch', payload: query }); } }); }, },}; 这里面有一些写法之前没有用过:比如{ payload: { data: list, total } }, 这个是析构时配 alias 的写法;return { ...state, list, total } 的写法用了Spread Operator ... 来组合新对象, 详见 dva知识地图#ES6对象和数组 由于我们需要从 response headers 中获取 total users 数量,所以需要改造下 src/utils/request.js: 12345678910111213141516171819202122232425262728293031323334353637import fetch from 'dva/fetch';function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response; } const error = new Error(response.statusText); error.response = response; throw error;}/** * Requests a URL, returning a promise. * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * @return {object} An object containing either "data" or "err" */export default async function request(url, options) { const response = await fetch(url, options); checkStatus(response); const data = await response.json(); const ret = { data, headers: {}, }; if (response.headers.get('x-total-count')) { ret.headers['x-total-count'] = response.headers.get('x-total-count'); } return ret;} 注意:这里使用了 ES7 的 async/await 特性,开始对这块不是很熟悉,看了一些关于 async/await 的文章,发现确实比 Promise 的写法语义化更加明显,下面这段是我改写的 Promise 写法: 123456789101112131415161718192021/** * Requests a URL, returning a promise. * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * @return {object} An object containing either "data" or "err" */export default function request(url, options) { let headers = {} return fetch(url, options) .then(checkStatus) .then(response => { const data = parseJSON(response) if (response.headers.get('x-total-count')) { headers['x-total-count'] = response.headers.get('x-total-count'); } return data; }).then((data) => { return {data, headers} }).catch(err => ({ err }));} 切换到浏览器(会自动刷新),应该没任何变化,因为数据虽然好了,但并没有视图与之关联。但是打开 Redux 开发者工具,应该可以看到 users/fetch 和 users/save 的 action 以及相关的 state 。 Step 5. 添加界面,让用户列表展现出来我们把组件存在 src/pages/users/components 里,所以在这里新建 Users.js 和 Users.css。具体参考这个 Commit。 需留意两件事: 对 model 进行了微调,加入了 page 表示当前页 由于 components 和 services 中都用到了 pageSize,所以提取到 src/constants.js 改完后,切换到浏览器,应该能看到带分页的用户列表。 有几点需要注意: Users.js 里面使用了 antd 的组件,但是项目并没有手动安装 antd, 原来是 umi 帮我们引入了 antd 。 Users.js 里面将model和组件连接了起来,注意 const { list, total, page } = state.users; 里面的 users 为 model 里面的 namespace 名称。 我们没有手动注册 model,umi 帮我们进行了这一步操作, 详见 src/pages/.umi/DvaContainer.js 文件,该文件会自动更新。相关规则详见 umi官网#model注册 一节。 可以直接使用 css module Step 6. 添加 layout添加 layout 布局,使得我们可以在首页和用户列表页之间来回切换。umi 里约定 layouts/index.js 为全局路由,所以我们新增 src/layouts/index.js 和 CSS 文件即可。 参考这个 Commit。 注意: 页头的菜单会随着页面切换变化,高亮显示当前页所在的菜单项 Step 7. 处理 loading 状态dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。 umi-plugin-dva 默认内置了 dva-loading 插件。 然后在 src/components/Users/Users.js 里绑定 loading 数据: 1+ loading: state.loading.models.users, 具体参考这个 Commit 。 刷新浏览器,你的用户列表有 loading 了没? Step 8. 处理分页只改一个文件 src/pages/users/components/Users.js 就好。 处理分页有两个思路: 发 action,请求新的分页数据,保存到 model,然后自动更新页面 切换路由 (由于之前监听了路由变化,所以后续的事情会自动处理) 我们用的是思路 2 的方式,好处是用户可以直接访问到 page 2 或其他页面。 参考这个 Commit 。 Step 9. 处理用户删除经过前面的 8 步,应用的整体脉络已经清晰,相信大家已经对整体流程也有了一定了解。 后面的功能调整基本都可以按照以下三步进行: service model component我们现在开始增加用户删除功能。 service, 修改 src/pages/users/services/users.js: 12345export function remove(id) { return request(`/api/users/${id}`, { method: 'DELETE', });} model, 修改 src/pages/users/models/users.js: 12345*remove({ payload: id }, { call, put, select }) { yield call(usersService.remove, id); const page = yield select(state => state.users.page); yield put({ type: 'fetch', payload: { page } });}, component, 修改 src/pages/users/components/Users.js,替换 deleteHandler 内容: 1234dispatch({ type: 'users/remove', payload: id,}); 切换到浏览器,删除功能应该已经生效。 Step 10. 处理用户编辑处理用户编辑和前面的一样,遵循三步走: service model component先是 service,修改 src/pages/users/services/users.js:123456export function patch(id, values) { return request(`/api/users/${id}`, { method: 'PATCH', body: JSON.stringify(values), });} 再是 model,修改 src/pages/users/models/users.js:12345*patch({ payload: { id, values } }, { call, put, select }) { yield call(usersService.patch, id, values); const page = yield select(state => state.users.page); yield put({ type: 'fetch', payload: { page } });}, 最后是 component,详见 Commit。 需要注意的一点是,我们在这里如何处理 Modal 的 visible 状态,有几种选择: 存 dva 的 model state 里 存 component state 里 另外,怎么存也是个问题,可以: 只有一个 visible,然后根据用户点选的 user 填不同的表单数据 几个 user 几个 visible此教程选的方案是 2-2,即存 component state,并且 visible 按 user 存。另外为了使用的简便,封装了一个 UserModal 的组件。 完成后,切换到浏览器,应该就能对用户进行编辑了。 Step 11. 处理用户创建相比用户编辑,用户创建更简单些,因为可以共用 UserModal 组件。和 Step 10 比较类似,就不累述了,详见 Commit 。 到这里,我们已经完成了一个完整的 CURD 应用。如果感兴趣,可以进一步看下 dva 和 umi 的资料: dva 官网 umi 官网 (完) 总结做这个练习主要了解了 dva 和 umi 搭配使用的方式,使用 umi 的一些写法和 umi 的特点。感觉学习了 Redux 之后,dva 上手会很快,dva 的网站资源很丰富,希望和大家一起学习,后续还会有 dva 学习的文章。","link":"/2018/08/06/dva_learning1/"},{"title":"DvaJS 的学习之路 2 - umi@2 + dva,完成用户管理的 CURD 应用","text":"前言最近一个月来使用 dva 对公司存量项目进行重构,比较少时间写文章了。随着9月开学季节的到来,最近在使用的几个开源项目都迎来了重大更新。首先就是 umi 终于迎来了 2.0 版本,具体介绍可以查看 发布 umi 2.0,可插拔的企业级 react 应用框架。随之而来的是使用 umi@2 构建的 ant design pro 2.0 版本,具体介绍可以查看漂亮的实力派 Ant Design Pro 2.0 正式发布。今天终于有时间体验了 umi@2 ,想起我的 dva 学习之路的第一篇学习记录是使用 [email protected] 构建的,刚好可以使用 umi 2.0 重新构建一下。本文是 dva 作者 @sorrycc 的 umi-dva-user-dashboard 的 umi@2 版本实现,是 《umi + dva,完成用户管理的 CURD 应用》 文章的 umi@2 版本改写。代码仓库: umi2-dva-user-dashboard。演示地址:demo。 Step 1. 使用 create-umi 脚手架初始化项目详见 umi官网#通过脚手架创建项目 使用 yarn create 命令: 12$ mkdir umi2-dva-user-dashboard && cd umi2-dva-user-dashboard$ yarn create umi 看到社区有小伙伴问到如何使用 npm 创建 umi 项目,首先使用 npm 安装 create-umi : 1$ cnpm install -g create-umi 安装好之后,进入项目,然后执行 create-umi 12$ cd umi2-dva-user-dashboard$ create-umi 注意官方推荐使用 yarn create 命令,因为能确保每次使用最新的脚手架。如果你和我一样,公司是内网开发环境只有 npm 私服,可以尝试使用 npm 命令。 完成上述操作后,进入 create-umi 交互式命令行,选择功能,这里选择 antd、dva 和 hard source 确定后,会根据你的选择自动创建好目录和文件: 然后手动安装依赖: 1$ yarn 或 1$ cnpm install 安装好依赖后启动项目: 1$ yarn start 或 1$ npm start 如果顺利,在浏览器打开 http://localhost:8000 可看到以下界面: Step 2. 配置代理,能通过 RESTFul 的方式访问 http://localhost:8000/api/users修改 .umirc.js ,加上 “proxy” 配置: 1234567proxy: { "/api": { "target": "http://jsonplaceholder.typicode.com/", "changeOrigin": true, "pathRewrite": { "^/api" : "" } }}, 注意,代理配置与 umi@1 的配置相同。 访问 http://localhost:8000/api/users ,就能访问到 http://jsonplaceholder.typicode.com/users 的数据。 Step 3. 生成 users 路由umi 中文件即路由,所以我们要新增路由,新建文件即可,这里使用约定式路由。详见 https://umijs.org/zh/guide/router.html 。 新建 src/pages/users.js,内容如下: 1234567export default () => { return ( <div> Users Page </div> )} 然后访问 http://localhost:8000/users ,你会看到 Users Page 的输出: 这里看到页面多出了一个头部,原来是 create-umi 脚手架生成的项目会默认生成一个全局 layout 文件 src/layouts/index.js, [email protected] 版本就具有这个特性,详见官方文档全局layout Step 4. 构造 users model 和 service注意刚才建立的 src/pages/users.js 是简单的页面情况,目前需要和 model 与 service 组织到一起, 新建 src/pages/users/index.js 文件,将 src/pages/users.js 内容复制到 src/pages/users/index.js 文件中,然后删除 src/pages/users.js 文件。 create-umi 脚手架没有生成 request.js, 新建 src/utils/request.js: 12345678910111213141516171819202122232425262728293031323334353637import fetch from 'dva/fetch';function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response; } const error = new Error(response.statusText); error.response = response; throw error;}/** * Requests a URL, returning a promise. * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * @return {object} An object containing either "data" or "err" */export default async function request(url, options) { const response = await fetch(url, options); checkStatus(response); const data = await response.json(); const ret = { data, headers: {}, }; if (response.headers.get('x-total-count')) { ret.headers['x-total-count'] = response.headers.get('x-total-count'); } return ret;} 新增 service: src/pages/users/services/users.js,注意 umi@2 添加了 webpack alias @,指向 src 目录: 12345import request from '@/utils/request';export function fetch({ page = 1 }) { return request(`/api/users?_page=${page}&_limit=5`);} 注意这里的 page 参数默认为 1,limit 参数设置为 5 新增 model: src/pages/users/models/users.js,内容如下: 1234567891011121314151617181920212223242526272829import * as usersService from '../services/users';export default { namespace: 'users', state: { list: [], total: null, }, reducers: { save(state, { payload: { data: list, total } }) { return { ...state, list, total }; }, }, effects: { *fetch({ payload: { page } }, { call, put }) { const { data, headers } = yield call(usersService.fetch, { page }); yield put({ type: 'save', payload: { data, total: headers['x-total-count'] } }); }, }, subscriptions: { setup({ dispatch, history }) { return history.listen(({ pathname, query }) => { if (pathname === '/users') { dispatch({ type: 'fetch', payload: query }); } }); }, },}; 切换到浏览器(会自动刷新),应该没任何变化,因为数据虽然好了,但并没有视图与之关联。但是打开 Redux 开发者工具,应该可以看到 users/fetch 和 users/save 的 action 以及相关的 state 。 Step 5. 添加界面,让用户列表展现出来(与umi@1相同)我们把组件存在 src/pages/users/components 里,所以在这里新建 Users.js 和 Users.css。具体参考这个 Commit。 需留意两件事: 对 model 进行了微调,加入了 page 表示当前页 由于 components 和 services 中都用到了 pageSize,所以提取到 src/constants.js 改完后,切换到浏览器,应该能看到带分页的用户列表。 有几点需要注意: Users.js 里面将 model 和组件连接了起来,注意 const { list, total, page } = state.users; 里面的 users 为 model 里面的 namespace 名称。 我们没有手动注册 model,umi 帮我们进行了这一步操作, 详见 src/pages/.umi/DvaContainer.js 文件,该文件会自动更新。相关规则详见 umi官网#model注册 一节。 可以直接使用 css module Step 6. 添加 Header Menu添加头部菜单组件,使得我们可以在首页和用户列表页之间来回切换。 参考这个 Commit。 Step 7. 处理 loading 状态(与umi@1相同)dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。 umi-plugin-dva 默认内置了 dva-loading 插件。 然后在 src/components/Users/Users.js 里绑定 loading 数据: 1+ loading: state.loading.models.users, 注意这里的 users 为 model 的 namespace, 所以 dva-loading 的 loading 状态是对于 model 整体的。 具体参考这个 Commit 。 刷新浏览器,你的用户列表有 loading 了没? Step 8. 处理分页(与umi@1相同)只改一个文件 src/pages/users/components/Users.js 就好。 处理分页有两个思路: 发 action,请求新的分页数据,保存到 model,然后自动更新页面 切换路由 (由于之前监听了路由变化,所以后续的事情会自动处理) 我们用的是思路 2 的方式,好处是用户可以直接访问到 page 2 或其他页面。 quan参考这个 Commit 。 Step 9. 处理用户删除(与umi@1相同)经过前面的 8 步,应用的整体脉络已经清晰,相信大家已经对整体流程也有了一定了解。 后面的功能调整基本都可以按照以下三步进行: service model component我们现在开始增加用户删除功能。 service, 修改 src/pages/users/services/users.js: 12345export function remove(id) { return request(`/api/users/${id}`, { method: 'DELETE', });} model, 修改 src/pages/users/model.js: 12345*remove({ payload: id }, { call, put, select }) { yield call(usersService.remove, id); const page = yield select(state => state.users.page); yield put({ type: 'fetch', payload: { page } });}, component, 修改 src/pages/users/components/Users.js,替换 deleteHandler 内容: 1234dispatch({ type: 'users/remove', payload: id,}); 注意由于使用第三方api网站,数据并不会真的删除,只是删除api返回成功,选择删除后重新获取数据,仍然是原来的数据。 Step 10. 处理用户编辑(与umi@1相同)处理用户编辑和前面的一样,遵循三步走: service model component先是 service,修改 src/pages/users/services/users.js:123456export function patch(id, values) { return request(`/api/users/${id}`, { method: 'PATCH', body: JSON.stringify(values), });} 再是 model,修改 src/pages/users/model.js:12345*patch({ payload: { id, values } }, { call, put, select }) { yield call(usersService.patch, id, values); const page = yield select(state => state.users.page); yield put({ type: 'fetch', payload: { page } });}, 最后是 component,详见 Commit。 需要注意的一点是,我们在这里如何处理 Modal 的 visible 状态,有几种选择: 存 dva 的 model state 里 存 component state 里 另外,怎么存也是个问题,可以: 只有一个 visible,然后根据用户点选的 user 填不同的表单数据 几个 user 几个 visible此教程选的方案是 2-2,即存 component state,并且 visible 按 user 存。另外为了使用的简便,封装了一个 UserModal 的组件。 完成后,切换到浏览器,应该就能对用户进行编辑了。 Step 11. 处理用户创建(与umi@1相同)相比用户编辑,用户创建更简单些,因为可以共用 UserModal 组件。和 Step 10 比较类似,就不累述了,详见 Commit 。 到这里,我们已经完成了一个完整的 CURD 应用。如果感兴趣,可以进一步看下 dva 和 umi 的资料: dva 官网 umi 官网 (完) 总结本文主要使用umi@2来完成 umi-dva-user-dashboard 项目。可以看到在业务代码上,和umi@1的写法基本一致,迁移成本比较低。目前手头的项目正在使用dva进行重构,暂时还没有使用 umi@2 的计划,不过会持续关注 umi 的成长的。","link":"/2018/09/08/dva_learning2/"},{"title":"[译]JavaScript: 带你彻底搞懂 this","text":"原文地址:JavaScript: What is the meaning of this? 原文作者:Jake Archibald 原文发布时间:2021-03-08 本文永久链接:https://github.com/Ivocin/Translation/Blogs/javascript-what-is-the-meaning-of-this.md 翻译、校对:Ivocin 搞明白 JavaScript 中 this 的值有时候会很棘手,本文带你彻底搞懂 this。 JavaScript 的 this 往往会成为许多笑话的笑柄,因为它相当复杂。然而,我发现很多开发人员为了避免处理 this,用了更加复杂和特定领域的处理。如果你对 this 还不熟悉,希望本文能帮助到你。下面进入我的 this 指南。 我将从最具体的情况开始,以最不具体的情况结束,本文的结构类似与一个大的 if (…) … else if () … else if (…) … 语句,所以你可以直接跳转到匹配你代码情况的章节。 如果是箭头函数 否则,如果使用 new 调用函数/类 否则, 函数被 bind 了 this 否则, 如果 this 在调用时设置 否则, 如果使用父对象(parent.func()) 调用函数 否则, 如果函数或者其父作用域使用严格模式 否则 如果是箭头函数:123const arrowFunction = () => { console.log(this);}; 在这种情况下,this 的值永远与父作用域的 this 相同。 123456const outerThis = this;const arrowFunction = () => { // 永远输出 `true`: console.log(this === outerThis);}; 箭头函数非常优秀,因为其内部 this 的值无法被改变,它与外部的 this 永远 相同。 其他例子 使用箭头函数, this 的值无法被 bind 改变: 12// 输出为 `true` - bind `this` 被忽略:arrowFunction.bind({foo: 'bar'})(); 使用箭头函数,this 的值无法被 call 或 apply 改变: 1234// 输出为 `true` - call `this` 被忽略:arrowFunction.call({foo: 'bar'});// 输出为 `true` - apply `this` 被忽略:arrowFunction.apply({foo: 'bar'}); 使用箭头函数,this 的值无法通过将函数作为另一个对象的成员变量来调用改变: 123const obj = {arrowFunction};// 输出为 `true` - 父对象被忽略:obj.arrowFunction(); 使用箭头函数,this 的值无法通过将函数作为构造函数来调用而改变: 12// TypeError: arrowFunction is not a constructornew arrowFunction(); “绑定” 实例方法 对于实例方法,如果想要确保 this 始终指向类实例,最好的方法是使用箭头函数和 class fields: 123456class Whatever { someMethod = () => { // 永远是 Whatever 的实例: console.log(this); };} 这个模式在将实例方法作为组件内的事件监听器时十分有用(如 React 组件或者 Web Components)。 上面的代码貌似打破了“this 的值永远与父作用域的 this 相同”的规则,但是如果你将 class fields 看作将对象设置到构造函数的语法糖,那么就好理解了: 123456789101112131415161718192021class Whatever { someMethod = (() => { const outerThis = this; return () => { // 永远输出 `true`: console.log(this === outerThis); }; })();}// …大致等于:class Whatever { constructor() { const outerThis = this; this.someMethod = () => { // Always logs `true`: console.log(this === outerThis); }; }} 其他模式包括在构造函数中绑定现有函数,或在构造函数中对函数赋值。如果你由于某种原因不能使用 class fields,则在构造函数中对函数赋值是一种合理的选择: 1234567class Whatever { constructor() { this.someMethod = () => { // … }; }} 否则,如果使用 new 调用函数/类: 1new Whatever(); 上面代码会调用 Whatever(或者它的构造函数,如果它是类),并将 this 设置为 Object.create(Whatever.prototype) 的结果。 12345678910class MyClass { constructor() { console.log( this.constructor === Object.create(MyClass.prototype).constructor, ); }}// 输出为 `true`:new MyClass(); 使用旧式的构造函数结果也一样: 12345678function MyClass() { console.log( this.constructor === Object.create(MyClass.prototype).constructor, );}// 输出 `true`:new MyClass(); 其他例子 使用 new 调用,this 的值无法被 bind 改变: 123const BoundMyClass = MyClass.bind({foo: 'bar'});// 输出为 `true` - bind `this` 被忽略:new BoundMyClass(); 使用 new 调用,this 的值无法通过将函数作为另一个对象的成员变量来调用改变: 123const obj = {MyClass};// 输出为 `true` - 父对象被忽略:new obj.MyClass(); 否则, 函数被 bind 了 this: 123456function someFunction() { return this;}const boundObject = {hello: 'world'};const boundFunction = someFunction.bind(boundObject); 每当 boundFunction 被调用,它的 this 值就是通过 bind 传入的值(boundObject)。 1234// 输出 `false`:console.log(someFunction() === boundObject);// 输出 `true`:console.log(boundFunction() === boundObject); Warning: 避免使用 bind 将函数绑定到其外部的 this。使用箭头函数替代,因为这样 this 可以在函数声明就能清楚地看出来,而非在后续代码中看到。不要使用 bind 设置 this 为与父对象无关的值;这通常是出乎意料的,这也是 this 获得如此糟糕名声的原因。考虑将值作为参数传递;它更加明确,并且可以使用箭头函数。 其他例子 使用 bind 调用函数,this 的值无法被 call 或 apply 改变: 1234// 输出为 `true` - call `this` 被忽略:console.log(boundFunction.call({foo: 'bar'}) === boundObject);// 输出为 `true` - apply `this` 被忽略:console.log(boundFunction.apply({foo: 'bar'}) === boundObject); 使用 bind 调用函数,this 的值无法通过将函数作为另一个对象的成员变量来调用改变: 123const obj = {boundFunction};// Logs `true` - parent object is ignored:console.log(obj.boundFunction() === boundObject); 否则, 如果 this 在调用时设置: 12345678910function someFunction() { return this;}const someObject = {hello: 'world'};// 输出 `true`:console.log(someFunction.call(someObject) === someObject);// 输出 `true`:console.log(someFunction.apply(someObject) === someObject); this 的值就是传递给 call/apply 的对象。 警告: 不要使用 bind 设置 this 为与父对象无关的值;这通常是出乎意料的,这也是 this 获得如此糟糕名声的原因。考虑将值作为参数传递;它更加明确,并且可以使用箭头函数。 不幸的是,this 可能会被如 DOM 事件监听器之类的函数设置为其他值,使用它会导致代码难以理解: 不要这样: 12345element.addEventListener('click', function (event) { // 输出 `element`, 因为 DOM 将 `this` 设置为 // click 绑定的元素上 console.log(this);}); 我会避免在上述场景中使用 this,我会这样使用: 123456element.addEventListener('click', (event) => { // 理想情况, 从父作用域获得它: console.log(element); // 但是如果你不想这么做,可以从 event 对象获取它: console.log(event.currentTarget);}); 否则, 如果使用父对象(parent.func()) 调用函数: 12345678const obj = { someMethod() { return this; },};// 输出 `true`:console.log(obj.someMethod() === obj); 在这种情况下,函数作为 obj 的成员变量被调用,所以 this 指向 obj。这是在调用时发生的,因此如果没有使用父对象调用,或者使用一个不同的父对象调用,该连接会断开: 123456789const {someMethod} = obj;// 输出 `false`:console.log(someMethod() === obj);const anotherObj = {someMethod};// 输出 `false`:console.log(anotherObj.someMethod() === obj);// 输出 `true`:console.log(anotherObj.someMethod() === anotherObj); someMethod() === obj 为 false,因为 someMethod 不是 作为 obj 的成员变量被调用的。尝试执行以下操作时,可能会遇到此陷阱: 123const $ = document.querySelector;// TypeError: Illegal invocationconst el = $('.some-element'); 这个报错是因为 querySelector 实现会寻找它的 this 值,并期望其某种 DOM 节点,上述代码破坏了连接。为了正确实现上述功能,可以这样写: 123const $ = document.querySelector.bind(document);// 或者:const $ = (...args) => document.querySelector(...args); 有趣的事实:并不是所有的 API 都在其内部使用了 this。Console 方法(如 console.log)就改为了不使用 this 引用,因此 log 方法不需要绑定 console。 警告: 不要使用 bind 设置 this 为与父对象无关的值;这通常是出乎意料的,这也是 this 获得如此糟糕名声的原因。考虑将值作为参数传递;它更加明确,并且可以使用箭头函数。 否则, 如果函数或者其父作用域使用严格模式: 1234567function someFunction() { 'use strict'; return this;}// 输出 `true`:console.log(someFunction() === undefined); 在这种情况下,this 的值是 undefined。如果父作用域处于严格模式(而且所有模块都处在严格模式),则不需要在函数内部使用 'use strict'。 警告: 不要依赖这个。我的意思是,有更简单的方式来得到一个 undefined 值 😀。 否则: 123456function someFunction() { return this;}// 输出 `true`:console.log(someFunction() === globalThis); 在这种情况下,this 的值与 globalThis 相同。 很多人(包括我)把 globalThis 称为 global 对象,但这不是 100% 技术正确的。在 Mathias Bynens with the details 中,有它为什么叫 globalThis 而不是 global 的原因。 警告: 避免使用 this 指向 global 对象(对,我仍然这么叫它)。改为使用globalThis,它更加明确。 结语 好了,这就是我理解的 this 的全部了。如果有任何问题或者我有所遗漏,请给我发推。 感谢 Mathias Bynens, Ingvar Stepanyan, 和 Thomas Steiner 的审阅。","link":"/2021/03/13/javascript-what-is-the-meaning-of-this/"},{"title":"[译] 2019 前端性能优化年度总结 — 第四部分","text":"原文地址:Front-End Performance Checklist 2019 — 4 原文作者:Vitaly Friedman 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/front-end-performance-checklist-2019-pdf-pages-4.md 译者:Ivocin 校对者:ziyin feng,weibinzhu 让 2019 来得更迅速吧!你现在阅读的是 2019 年前端性能优化年度总结,始于 2016。 [译] 2019 前端性能优化年度总结 — 第一部分 [译] 2019 前端性能优化年度总结 — 第二部分 [译] 2019 前端性能优化年度总结 — 第三部分 [译] 2019 前端性能优化年度总结 — 第四部分 [译] 2019 前端性能优化年度总结 — 第五部分 [译] 2019 前端性能优化年度总结 — 第六部分 目录 构建优化 22.确定优先级 23.重温优秀的“符合最低要求”技术 24.解析 JavaScript 是耗时的,所以让它体积小 25.使用了摇树、作用域提升和代码分割吗 26.可以将 JavaScript 切换到 Web Worker 中吗? 27.可以将 JavaScript 切换到 WebAssembly 中吗? 28.是否使用了 AOT 编译? 29.仅将遗留代码提供给旧版浏览器 30.是否使用了 JavaScript 差异化服务? 31.通过增量解耦识别和重写遗留代码 32.识别并删除未使用的 CSS/JS 33.减小 JavaScript 包的大小 34.是否使用了 JavaScript 代码块的预测预获取? 35.从针对你的目标 JavaScript 引擎进行优化中获得好处 36.使用客户端渲染还是服务器端渲染? 37.约束第三方脚本的影响 38.设置 HTTP 缓存标头 构建优化22. 确定优先级要了解你首先要处理什么。列出你全部的静态资源清单(JavaScript、图片、字体、第三方脚本以及页面上的大模块:如轮播图、复杂的信息图表和多媒体内容),并将它们分组。 新建一个电子表格。定义旧版浏览器的基本核心体验(即完全可访问的核心内容)、现代浏览器的增强体验(即更加丰富的完整体验)以及额外功能(可以延迟加载的非必需的资源:例如网页字体、不必要的样式、轮播脚本、视频播放器、社交媒体按钮和大图片)。不久前,我们发表了一篇关于“提升 Smashing 杂志网站性能”的文章,文中详细描述了这种方法。 在优化性能时,我们需要确定我们的优先事项。立即加载核心体验,然后加载增强体验,最后加载额外功能。 23. 重温优秀的“符合最低要求”技术如今,我们仍然可以使用符合最低要求(cutting-the-mustard)技术 将核心体验发送到旧版浏览器,并为现代浏览器提供增强体验。(译者注:关于 cutting-the-mustard 出处可以参考这篇文章。)该技术的一个更新版本将使用 ES2015 + 语法 <script type="module">。现代浏览器会将脚本解释为 JavaScript 模块并按预期运行它,而旧版浏览器无法识别该属性并忽略它,因为它是未知的 HTML 语法。 现在我们需要谨记的是,单独的功能检测不足以做出该发送哪些资源到该浏览器的明智决定。就其本身而言,符合最低要求 从浏览器版本中推断出设备的能力,今天已经不再有效了。 例如,发展中国家的廉价 Android 手机主要使用 Chrome 浏览器,尽管设备的内存和 CPU 功能有限,但其仍然达到了使用符合最低要求技术的标准。最终,使用设备内存客户端提示报头,我们将能够更可靠地定位低端设备。在本文写作时,仅在 Blink 中支持该报头(通常用于客户端提示)。由于设备内存还有一个已在 Chrome 中提供的 JavaScript API,因此基于该 API 进行功能检测是一个选择,并且只有在不支持时才会再来使用符合最低要求技术(感谢 Yoav!)。 24. 解析 JavaScript 是耗时的,所以让它体积小在处理单页面应用程序时,我们需要一些时间来初始化应用程序,然后才能渲染页面。你的设置需要你的自定义解决方案,但可以留意能够加快首次渲染的模块和技术。例如,如何调试 React 性能、消除常见的 React 性能问题,以及如何提高 Angular 的性能。通常,大多数性能问题都来自启动应用程序的初始解析时间。 JavaScript 有一个解析的成本,但很少仅是由于文件大小一个因素影响性能。解析和执行时间根据设备的硬件的不同有很大差异。在普通电话(Moto G4)上,1MB(未压缩)JavaScript 的解析时间约为 1.3-1.4s,移动设备上有 15-20% 的时间用于解析。在游戏中编译,仅仅在准备 JavaScript 就平均耗时 4 秒,在移动设备上首次有效绘制(First Meaningful Paint )之前大约需要 11 秒。原因:在低端移动设备上,解析和执行时间很容易高出 2-5 倍。 为了保证高性能,作为开发人员,我们需要找到编写和部署更少量 JavaScript 的方法。这就是为什么要详细检查每一个 JavaScript 依赖关系的原因。 有许多工具可以帮助你做出有关依赖关系和可行替代方案影响的明智决策: webpack-bundle-analyzer Source Map Explorer Bundle Buddy Bundlephobia Webpack size-plugin Import Cost for Visual Code 有一种有趣方法可以用来避免解析成本,它使用了 Ember 在 2017 年推出的二进制模板。使用该模板,Ember 用 JSON 解析代替 JavaScript 解析,这可能更快。(**感谢 Leonardo,Yoav!**) 衡量 JavaScript 解析和编译时间。我们可以使用综合测试工具和浏览器跟踪来跟踪解析时间,浏览器实现者正在谈论将来把基于 RUM 的处理时间暴露出来。也可以考虑使用 Etsy 的 DeviceTiming,这是一个小工具,它允许你使用 JavaScript 在任何设备或浏览器上测量解析和执行时间。 底线:虽然脚本的大小很重要,但它并不是一切。随着脚本大小的增长,解析和编译时间不一定会线性增加。 25. 使用了摇树、作用域提升和代码分割吗摇树(tree-shaking)是一种在 webpack 中清理构建过程的方法,它仅将实际生产环境使用的代码打包,并排除没有使用的导入模块。使用 webpack 和 rollup,还可以使用作用域提升(scope hoisting),作用域提升使得 webpack 和 rollup 可以检测 import 链可以展开的位置,并将其转换为一个内联函数,并且不会影响代码。使用 webpack,我们也可以使用 JSON Tree Shaking。 此外,你可能需要考虑学习如何编写高效的 CSS 选择器,以及如何避免臃肿且耗时的样式。如果你希望更进一步,你还可以使用 webpack 来缩短 class 名,并使用作用域隔离在编译时动态重命名 CSS class 名。 代码拆分(code-splitting)是另一个 webpack 功能,它将你的代码库拆分为按需加载的“块”。并非所有的 JavaScript 都必须立即下载、解析和编译。在代码中定义分割点后,webpack 可以处理依赖项和输出文件。它能够保持较小体积的初始下载,并在应用程序请求时按需请求代码。Alexander Kondrov 有一个使用 webpack 和 React 应用代码分割的精彩介绍。 考虑使用 preload-webpack-plugin,它接受代码拆分的路由,然后提示浏览器使用 <link rel="preload"> 或 <link rel="prefetch"> 预加载它们。Webpack 内联指令还可以控制 preload/prefetch。 在哪里定义分割点呢?通过跟踪代码查看使用了哪些 CSS/JavaScript 包,没有使用哪些包。Umar Hansa 解释了如何使用 Devtools 的代码覆盖率工具来实现它。 如果你没有使用 webpack,请注意 rollup 显示的结果明显优于 Browserify 导出。虽然我们参与其中,但你可能需要查看 rollup-plugin-closure-compiler 和 rollupify,它将 ECMAScript 2015 模块转换为一个大型 CommonJS 模块 —— 因为根据你的包和模块系统的选择,小模块可能会有惊人高的成本。 26. 可以将 JavaScript 切换到 Web Worker 中吗?为了减少对首次可交互时间(Time-to-Interactive)的负面影响,考虑将高耗时的 JavaScript 放到 Web Worker 或通过 Service Worker 来缓存。 随着代码库的不断增长,UI 性能瓶颈将会出现,进而会降低用户的体验。主要原因是 DOM 操作与主线程上的 JavaScript 一起运行。通过 web worker,我们可以将这些高耗时的操作移动到后台进程的另一线程上。Web worker 的典型用例是预获取数据和渐进式 Web 应用程序,提前加载和存储一些数据,以便你在之后需要时使用它。而且你可以使用 Comlink 简化主页面和 worker 之间的通信。仍然还有一些工作要做,但我们已经做了很多了。 Workerize 让你能够将模块移动到 Web Worker 中,自动将导出的函数映射为异步代理。如果你正在使用 webpack,你可以使用 workerize-loader。或者,也可以试试 worker-plugin。 请注意,Web Worker 无权访问 DOM,因为 DOM 不是“线程安全的”,而且它们执行的代码需要包含在单独的文件中。 27. 可以将 JavaScript 切换到 WebAssembly 中吗?我们还可以将 JavaScript 转换为 WebAssembly,这是一种二进制指令格式,可以使用 C/C++/Rust 等高级语言进行编译。它的浏览器支持非常出色,最近它变得可行了,因为 JavaSript 和 WASM 之间的函数调用速度变得越来越快,至少在 Firefox 中是这样。 在实际场景中,JavaScript 似乎在较小的数组大小上比 WebAssembly 表现更好,而 WebAssembly 在更大的数组大小上比 JavaScript 表现更好。对于大多数 Web 应用程序,JavaScript 更适合,而 WebAssembly 最适合用于计算密集型 Web 应用程序,例如 Web 游戏。但是,如果切换到 WebAssembly 能否获得显着的性能改进,则可能值得研究。 如果你想了解有关 WebAssembly 的更多信息: Lin Clark 为 WebAssembly 撰写了一个全面的系列文章,Milica Mihajlija 概述了如何在浏览器中运行原生代码、为什么要这样做、以及它对 JavaScript 和 Web 开发的未来意味着什么。 Google Codelabs 提供了一份 WebAssembly 简介,这是一个 60 分钟的课程,你将学习如何使用原生代码 —— 使用 C 并将其编译为 WebAssembly,然后直接在 JavaScript 调用它。 Alex Danilo 在他的 Google I/O 2017 演讲中解释了 WebAssembly 及其工作原理。此外,Benedek Gagyi 分享了一个关于 WebAssembly 的实际案例研究,特别是团队如何将其用作 iOS、Android 和网站的 C++ 代码库的输出格式。 Milica Mihajlija 提供了 WebAssembly 的工作原理及其有用的原因的概述。 (预览大图) 28. 是否使用了 AOT 编译?使用 AOT(ahead-of-time)编译器将一些客户端渲染放到服务器,从而快速输出可用结果。最后,考虑使用 Optimize.js 来加速初始化加载时间,它包装了需要立即调用的函数(尽管现在这可能不是必需的了)。 来自默认快速:现代加载最佳实践,作者是独一无二的 Addy Osmani。幻灯片第 76 页。 29. 仅将遗留代码提供给旧版浏览器由于 ES2015 在现代浏览器中得到了非常好的支持,我们可以使用 babel-preset-env ,仅转义尚未被我们的目标浏览器支持的那些 ES2015 + 特性。然后设置两个构建,一个在 ES6 中,一个在 ES5 中。如上所述,现在所有主流浏览器都支持 JavaScript 模块,因此使用 script type =“module” 让支持 ES 模块的浏览器加载支持 ES6 的文件,而旧浏览器可以使用 script nomodule 加载支持 ES5 的文件。我们可以使用 Webpack ESNext Boilerplate 自动完成整个过程。 请注意,现在我们可以编写基于模块的 JavaScript,它可以原生地在浏览器里运行,无需编译器或打包工具。<link rel="modulepreload"> header 提供了一种提前(和高优先级)加载模块脚本的方法。基本上,它能够很好地最大化使用带宽,通过告诉浏览器它需要获取什么,以便在这些长的往返期间不会卡顿。此外,Jake Archibald 发布了一篇详细的文章,其中包含了需要牢记的 ES 模块相关内容,值得一读。 对于 lodash,使用 babel-plugin-lodash,通过它可以只加载你在源代码中使用的模块。你的其他依赖也可能依赖于其他版本的 lodash,因此将通用 lodash requires 转换为特定需要的功能,以避免代码重复。这可能会为你节省相当多的 JavaScript 负载。 Shubham Kanodia 撰写了一份详细的关于智能打包的低维护指南:如何在生产环境中实现仅仅将遗留代码推送到老版本浏览器上,里面还有一些你可以直接拿来用的代码片段。 Jake Archibald 发布了一篇详细的文章,其中包含了 需要牢记的 ES 模块相关内容,例如:内联脚本会被推迟,直到正在阻塞的外部脚本和内联脚本得到执行。(预览大图) 30. 是否使用了 JavaScript 差异化服务?我们希望通过网络发送必要的 JavaScript,但这意味着需要更加集中精力并且细粒度地关注这些静态资源的传送。前一阵子 Philip Walton 介绍了差异化服务的想法。该想法是编译和提供两个独立的 JavaScript 包:“常规”构建,带有 Babel-transforms 和 polyfill 的构建,只提供给实际需要它们的旧浏览器,以及另一个没有转换和 polyfill 的包(具有相同功能)。 结果,通过减少浏览器需要处理的脚本数量来帮助减少主线程的阻塞。Jeremy Wagner 在 2019 年发布了一篇关于差异服务以及如何在你的构建管道中进行设置的综合文章,从设置 babel 到你需要在 webpack 中进行哪些调整,以及完成所有这些工作的好处。 31. 通过增量解耦识别和重写遗留代码老项目充斥着陈旧和过时的代码。重新查看你的依赖项,评估重构或重写最近导致问题的遗留代码所需的时间。当然,它始终是一项重大任务,但是一旦你了解了遗留代码的影响,就可以从增量解耦开始。 首先,设置指标,跟踪遗留代码调用的比率是保持不变或是下降,而不是上升。公开阻止团队使用该库,并确保你的 CI 能够警告开发人员,如果它在拉取请求(pull request)中使用。Polyfill 可以帮助将遗留代码转换为使用标准浏览器功能的重写代码库。 32. 识别并删除未使用的 CSS/JSChrome 中的 CSS 和 JavaScript 代码覆盖率可以让你了解哪些代码已执行/已应用,哪些代码尚未执行。你可以开始记录覆盖范围,在页面上执行操作,然后浏览代码覆盖率结果。一旦你检测到未使用的代码,找到那些模块并使用 import() 延迟加载(参见整个线程)。然后重复覆盖配置文件并验证它现在在初始加载时发送的代码是否变少了。 你可以使用 Puppeteer 以编程方式收集代码覆盖率,Canary 也能够让你导出代码覆盖率结果。正如 Andy Davies 提到的那样,你可能希望同时收集现代和旧版浏览器的代码覆盖率。Puppeteer 还有许多其他用例,例如,自动视差或监视每个构建的未使用的 CSS。 此外,purgecss、UnCSS 和 Helium 可以帮助你从 CSS 中删除未使用的样式。如果你不确定是否在某处使用了可疑的代码,可以遵循 Harry Roberts 的建议:为该 class 创建 1×1px 透明 GIF 并将其放入 dead/ 目录,例如:/assets/img/dead/comments.gif。然后,将该特定图像设置为 CSS 中相应选择器的背景,然后静候几个月,查看该文件能否出现在你的日志中。如果日志里没出现该条目,则没有人使用该遗留组件:你可以继续将其全部删除。 对于爱冒险的人,你甚至可以通过使用 DevTools 监控 DevTools,通过一组页面自动收集未使用的 CSS。 33. 减小 JavaScript 包的大小正如 Addy Osmani 指出的那样,当你只需要一小部分时,你很可能会发送完整的 JavaScript 库,以及提供给不需要它们的浏览器的过时 polyfill,或者只是重复代码。为避免额外开销,请考虑使用 webpack-libs-optimization,在构建过程中删除未使用的方法和 polyfill。 将打包审计添加到常规工作流程中。有一些你在几年前添加的重型库的轻量级替代品,例如:Moment.js 可以用 date-fns 或 Luxon 代替。Benedikt Rötsch 的研究表明,从 Moment.js 到 date-fns 的转换可能会使 3G 和低端手机上的首次绘制时间减少大约 300ms。 这就是 Bundlephobia 这样的工具可以帮助你找到在程序包中添加 npm 包的成本。你甚至可以将这些成本与 Lighthouse Custom Audit 相结合。这也适用于框架。通过删除或减小 Vue MDC 适配器(Vue 的 Material 组件),样式可以从 194KB 降至 10KB。 喜欢冒险吗?你可以看看Prepack。它将 JavaScript 编译为等效的 JavaScript 代码,但与 Babel 或 Uglify 不同,它允许你编写正常的 JavaScript 代码,并输出运行速度更快的等效 JavaScript 代码。 除了传送整个框架包之外,你甚至可以修剪框架并将其编译为不需要额外代码的原始 JavaScript 包。Svelte 做到了,Rawact Babel 插件也是如此,它在构建时将 React.js 组件转换为原生 DOM 操作。 为什么?好吧,正如维护者解释的那样:“React-dom 包含可以渲染的每个可能组件/ HTMLElement 的代码,包括用于增量渲染、调度、事件处理等的代码。但是有些应用程序不需要所有这些功能(在初始页面加载时)。对于此类应用程序,使用原生 DOM 操作构建交互式用户界面可能是有意义的。” 在 Benedikt Rötsch 的文章中,他表示,从 Moment.js 到 date-fns 的转换会使 3G 和低端手机上的首次绘制时间减少大约 300ms。(预览大图) 34. 是否使用了 JavaScript 代码块的预测预获取?我们可以使用启发式方法来决定何时预加载 JavaScript 代码块。Guess.js 是一组工具和库,它使用 Google Analytics 的数据来确定用户最有可能从给定页面访问哪个页面。根据从 Google Analytics 或其他来源收集的用户导航模式,Guess.js 构建了一个机器学习模型,用于预测和预获取每个后续页面中所需的 JavaScript。 因此,每个交互元素都接收参与的概率评分,并且基于该评分,客户端脚本决定提前预获取资源。你可以将该技术集成到 Next.js 应用程序、Angular 和 React 中,还有一个 webpack 插件能够自动完成设置过程。 显然,你可能会让浏览器预测到使用不需要的数据从而预获取到不需要的页面,因此最好在预获取请求的数量上保持绝对保守。一个好的用例是预获取结账中所需的验证脚本,或者当一个关键的 CTA(call-to-action)进入视口时的推测性预获取。 需要不太复杂的东西?Quicklink 是一个小型库,可在空闲时自动预获取视口中的链接,以便加快下一页导航的加载速度。但是,它也考虑了数据流量,因此它不会在 2G 网络或者 Data-Saver 打开时预获取数据。 35. 从针对你的目标 JavaScript 引擎进行优化中获得好处研究哪些 JavaScript 引擎在你的用户群中占主导地位,然后探索针对这些引擎的优化方法。例如,在为 Blink 内核浏览器、Node.js 运行时和 Electron 中使用的 V8 进行优化时,使用脚本流来处理庞大的脚本。它允许在下载开始时在单独的后台线程上解析 async 或 defer scripts,因此在某些情况下可以将页面加载时间减少多达 10%。实际上,在 <head> 里使用 <script defer>,以便浏览器可以提前发现资源,然后在后台线程上解析它。 警告:Opera Mini 不支持脚本延迟,所以如果你正在为印度或非洲开发,defer 将被忽略,这会导致阻止渲染,直到脚本执行完为止(感谢 Jeremy!)。 渐进式启动意味着使用服务器端渲染来获得快速的首次有效绘制,但也包括一些最小的 JavaScript,以保持首次交互时间接近首次有效绘制时间。 36. 使用客户端渲染还是服务器端渲染?在这两种情况下,我们的目标应该是设置渐进式启动:使用服务器端渲染来获得快速的首次有效绘制,但也包括一些最小的必要 JavaScript,以保持首次交互时间接近首次有效绘制时间。如果 JavaScript 在首次有效绘制之后来得太晚,浏览器可能会在解析、编译和执行后期发现的 JavaScript 时锁定主线程,从而给站点或应用程序的交互带来枷锁。 为避免这种情况,请始终将函数执行分解为独立的异步任务,并尽可能使用 requestIdleCallback。考虑使用 webpack 的动态 import() 支持,延迟加载 UI 的部分,降低加载、解析和编译成本,直到用户真正需要它们(感谢 Addy!)。 从本质上讲,首次可交互时间(TTI)告诉我们导航和交互之间的时间。通过查看初始内容渲染后的前五秒窗口来定义度量标准,其中任何 JavaScript 任务都不会超过 50 毫秒。如果发生超过 50 毫秒的任务,则重新开始搜索五秒钟窗口。因此,浏览器将首先假设它已到达交互状态,然后切换到冻结状态,最终切换回交互状态。 一旦我们到达交互状态,在按需或在时间允许的情况下,就可以启动应用程序的非必要部分。不幸的是,正如 Paul Lewis 所注意到的那样,框架通常没有提供给开发者优先级的概念,因此大多数库和框架都难以实现渐进式启动。如果你有时间和资源,请使用此策略最终提升性能。 那么,客户端还是服务器端?如果用户没有明显的好处,客户端渲染可能不是真正必要的 —— 实际上,服务器端渲染的 HTML 可能更快。也许你甚至可以使用静态站点生成器预渲染一些内容,并将它们直接推送到 CDN,并在顶部添加一些 JavaScript。 将客户端框架的使用限制为绝对需要它们的页面。如果做得不好,服务器渲染和客户端渲染是一场灾难。考虑在构建时预渲染和动态 CSS 内联,以生成生产就绪的静态文件。Addy Osmani 就可能值得关注的 JavaScript 成本发表了精彩的演讲。 37. 约束第三方脚本的影响通过所有性能优化,我们通常无法控制来自业务需求的第三方脚本。第三方脚本指标不受最终用户体验的影响,因此通常一个脚本最终会调用令人讨厌的冗长的第三方脚本,从而破坏了专门的性能工作。为了控制和减轻这些脚本带来的性能损失,仅仅异步加载它们(可能是通过延迟)并通过资源提示(如 dns-prefetch 或 preconnect)加速它们是不够的。 正如 Yoav Weiss 在他关于第三方脚本的必读观点中所解释的那样,在许多情况下,这些脚本会下载动态的资源。资源在页面加载之间发生变化,因此我们没有必要知道从哪些主机下载资源以及这些资源是什么。 你有哪些选择方案?考虑使用 service worker,通过超时竞争资源下载,如果资源在特定超时内没有响应,则返回空响应以告知浏览器继续解析页面。你还可以记录或阻止未成功或不符合特定条件的第三方请求。如果可以,请从你自己的服务器而不是从供应商的服务器加载第三方脚本。 Casper.com 发布了一个详细的案例研究,说明他们如何通过自托管的 Optimizely 网站响应时间减少了 1.7 秒。这可能是值得的。(图片来源)(预览大图) 另一种选择是建立内容安全策略(CSP)以限制第三方脚本的影响,例如:不允许下载音频或视频。最好的选择是通过 <iframe> 嵌入脚本,以便脚本在 iframe 的上下文中运行,因此第三方脚本无法访问页面的 DOM,也无法在你的域上运行任意代码。使用 sandbox 属性可以进一步约束 iframe,那样你就可以禁用一切 iframe 可能执行的任何功能,例如:防止脚本运行、阻止警报、表单提交、插件、访问顶部导航等。 比如,可能必须使用 <iframe sandbox="allow-scripts"> 来运行脚本。每个限制都可以通过 sandbox 属性上的各种 allow 值来解除(几乎所有的浏览器都受支持),因此将它们限制在应该允许的最低限度。 考虑使用 Intersection Observer;这将使广告仍然在 iframe 中,但是可以调度事件或从 DOM 获取所需信息(例如,广告可见性)。可以关注一些新的策略,例如功能策略,资源大小限制和 CPU/带宽优先级,以限制可能会降低浏览器速度的有害 Web 功能和脚本,例如:同步脚本、同步 XHR 请求、document.write 和过时的实现。 要对第三方进行压力测试,请检查 DevTools 中性能配置文件页面中的自下而上的摘要,测试如果请求被阻止或超时的情况会发生什么 —— 对于后者,你可以使用 WebPageTest 的 Blackhole 服务器 blackhole.webpagetest.org,它可以将特定域指向你的 hosts 文件。最好是自托管并使用单一主机名,但也可以生成一个请求映射,该映射公开第四方调用并检测脚本何时更改。你可以使用 Harry Roberts 的方法审核第三方,并生成类似这样的电子表格。Harry 还在他关于第三方性能和审计的讨论中解释了审计工作流程。 图片来源: Harry Roberts 38. 设置 HTTP 缓存标头仔细检查是否已正确设置 expires、max-age、cache-control 和其他 HTTP 缓存头。通常,资源无论在短时间内(如果它们可能会更改)还是无限期(如果它们是静态的)情况下都是可缓存的 —— 你只需在需要时在 URL 中更改它们的版本。禁用 Last-Modified 标头,因为任何带有它的静态资源都将导致带有 If-Modified-Since 标头的条件请求,即使资源位于缓存中也是如此。Etag 也是如此。 使用使用专为指纹静态资源设计的 Cache-control:immutable,以避免重新验证(截至 2018 年 12 月,Firefox、Edge 和 Safari 都已经支持该功能; Firefox 仅支持 https:// 事务)。事实上,“在 HTTP 存档中的所有页面中,2% 的请求和 30% 的网站似乎包含至少 1 个不可变响应。此外,大多数使用它的网站都设置了具有较长新鲜生命周期的静态资源。” 还记得 stale-while-revalidate 吗?你可能知道,我们使用 Cache-Control 响应头指定缓存时间,例如:Cache-Control: max-age=604800。经过 604800 秒后,缓存将重新获取所请求的内容,从而导致页面加载速度变慢。通过使用 stale-while-revalidate 可以避免这种速度变慢的问题。它本质上定义了一个额外的时间窗口,在此期间缓存可以使用旧的静态资源,只要它在异步地在后台重新验证自己。因此,它“隐藏了”来自客户端的延迟(在网络和服务器上)。 在 2018 年 10 月,Chrome 发布了一个意图 在 HTTP Cache-Control 标头中对 stale-while-revalidate 的处理,因此,它应该会改善后续页面加载延迟,因为旧的静态文件不再位于关键路径中。结果:重复访问页面的 RTT 为零。 你可以使用 Heroku 的 HTTP 缓存标头入门,Jake Archibald 的“缓存最佳实践”和Ilya Grigorik 的 HTTP 缓存入门作为指南。另外,要注意标头的变化,特别是与 CDN 相关的标头,并注意 Key 标头,这有助于避免当新请求与先前请求略有差异(但不显着)时,需要进行额外的往返验证(感谢 Guy!)。 另外,请仔细检查你是否发送了不必要的标头(例如 x-powered-by、pragma、x-ua-compatible、expires 等),并且包含有用的安全性和性能标头(例如 Content-Security-Policy, X-XSS-Protection, X-Content-Type-Options 等)。最后,请记住单页应用程序中 CORS 请求的性能成本。 [译] 2019 前端性能优化年度总结 — 第一部分 [译] 2019 前端性能优化年度总结 — 第二部分 [译] 2019 前端性能优化年度总结 — 第三部分 [译] 2019 前端性能优化年度总结 — 第四部分 [译] 2019 前端性能优化年度总结 — 第五部分 [译] 2019 前端性能优化年度总结 — 第六部分 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2019/02/02/front-end-performance-checklist-2019-pdf-pages-4/"},{"title":"[译] 从 2010 到 2018,你不知道的关于网页滚动和用户注意力的变化","text":"原文地址:Scrolling and Attention 原文作者:Therese Fessenden 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/scrolling-and-attention.md 译者:Ivocin 校对者:Moonliujk, CoderMing 摘要: 人们比以前更多地纵向滚动网页,但是新的眼动追踪数据表明:注意力没有变化,人们仍然更关注首屏上的内容,而非首屏以下的内容。 人们的行为相当的稳定,可用性指南一直鲜有变化。但是,从网络出现早期至今,有一个用户行为发生了变化,那就是滚动网页的趋势。一开始,很少有人纵向滚动网页,但到了 1997 年,随着长页面的普及,大多数人学会了滚动网页。然而,首屏上的信息仍然受到了最多的关注:我们的眼动追踪研究表明,即使早在 2010 年,用户 80% 的页面浏览时间都花在了首屏上面。 自 2010 年以来,随着响应式设计和极简主义的出现,许多设计师转向了具有负空间的长页面(覆盖几个“屏幕”)设计。现在是时候再次探究由于这些网页设计趋势的普及,用户行为是否发生了变化。 眼动追踪数据关于本研究为了回答这个问题,我们分析了 1920×1080 分辨率屏幕上超过 130,000 个凝视点(eye fixations)的 x, y 坐标。这些凝视点来自 120 名参与者,他们是我们最近的眼动追踪研究的一部分,该研究涉及来自各行各业的数千个网站。在本研究中,我们重点分析了广泛的用户任务,这些任务涵盖各种页面和行业,包括新闻,电子商务,博客,常见问题解答和百科全书页面。我们的目标不是分析某个网站,而是总结出用户行为的一般性。 我们将这些近期数据与我们之前在眼动追踪研究中获得的数据进行了比较,之前的数据是在 1024×768 分辨率屏幕上获得的。 研究结果我们的前后两次研究之间发生了两处变化:(a)更大的屏幕,(b)用户可能已经适应了的新的网页设计趋势。我们无法单独考虑某一变化所带来的影响。但没有关系,因为两者都是由于时间的流逝造成的变化。即使我们想要,我们也无法撤消任何一个。 在我们最近的一项研究中,用户将大约 57% 的页面浏览时间用于首屏。74% 的浏览时间花费在前两个屏幕上,浏览的长度最高可达 2160px。(本分析不关心页面的最大长度 —— 结果可能是由于页面长度较短或者是用户在浏览了前两个屏幕内容后就放弃了阅读。) 这些发现与我们在2010 年的发表的文章完全不同:在那篇文章中,用户 80% 的页面浏览时间花费在了首屏。然而首屏后页面关注度急剧下降的现象在 2018 年和 2010 年相同。 首屏上的内容在浏览时间中占据比最高。大约 74% 的时间花费在前两个屏幕内容中(首屏加上首屏下方的屏幕信息)。剩余的 26% 部分随着页面长度的增加少量递减。 能够理解的是,并非每个页面的长度都相同。为了确定人们如何在页面上分配他们的注意力(无论页面有多长),我们将页面分成 20% 的分段(即每页的五分之一)。在一般网站上,超过 42% 的浏览时间花在了页面的前 20% 区域上,超过 65% 的浏览时间花在页面的前 40% 区域上。在搜索结果页面(SERPs)上,结合我们在 2010 年的调查结果,47% 的浏览时间花费在页面的前 20% 区域上(超过 75% 的浏览时间花在了页面的前 40% 区域上)—— 可能反映了用户更愿意查看热门搜索结果的现象。 人们花费更多时间浏览页面的前 20% 区域。 如果我们只关注首屏的内容 —— 在第一个屏幕中 —— 屏幕顶部的信息比底部信息更受关注。用户超过 65% 的浏览时间集中在视窗的上半部分。在 SERP 上,首屏超过 75% 的浏览时间花费在了第一个屏幕的上半部分。(这是一个古老的事实,但也是老生常谈:你在谷歌的搜索结果要么是前两名,要么就在互联网上几乎不存在。“轻信谷歌”的现象至今依然强烈,和我们 10 年前发现这种用户行为时一样。) 即便在首屏上,注意力也集中在页面顶部 —— 尤其是 SERP。 浏览和阅读模式我们已经发现首屏上的内容得到了最多的关注(占浏览时间的 57%),第二屏内容浏览时间约占三分之一(占浏览时间的 17%),剩余的 26% 浏览时间表现为长尾分布。换言之,一条信息离页面顶部越近,它被阅读的可能性就越大。 个人阅读模式证实了这一发现。许多用户在扫描内容结构不合理的页面时会使用 F 模式阅读 —— 他们倾向于更仔细地查看靠近页面顶部的文本(文本的前几段),页面越往下阅读信息的时间越少。 即使网页是列表形式或信息以结构化的方式呈现,人们也会在页面顶部投注更多的目光(即阅读时间),因为他们需要了解页面的组织方式。一旦他们这样做,他们倾向有效地关注与手头任务相关的信息,因此在远离顶部的内容上投注更少的目光(即阅读时间)。 这是一个典型的凝视图,图中显示大多数用户的凝视点都集中在页面的顶部,但并不总是位于最顶部。凝视点的实际分布取决于页面的具体设计和用户访问页面的目标。如果信息看起来很有趣,用户有时可能会阅读一点点,但总体而言,浏览会在页面的下方逐渐消失。 2010 年 vs 现在2010 年,用户 80% 的浏览时间都花在了首屏上。今天,这个数字只有 57% —— 可能是由于长页面普遍流行的结果。这意味着什么? 首先,总的来说,设计师们很好地创造了指示符以抵消页面完整的错觉,并引导人们滚动页面。换言之,他们了解了长页面的缺点并在一定程度上缓解了这些缺点。其次,它可能意味着用户已经习惯了滚动阅读 —— 需要滚动阅读的页面的流行让滚动页面植入到了用户的行为中。 至少在某种程度上。人们仍然不会滚动很多 —— 他们几乎不会滚动到三屏之外。基本上,首屏的界限已经被推到了第三个屏幕 —— 8 年前,用户浏览网页时间的 80% 的停留在了在第一个屏幕信息中(首屏);而今天,用户浏览网页时间的 81% 停留在了前三个屏幕上。 我们总是说人们会在有理由的情况下滚动页面。人们的注意力仍然停留在页面顶部 —— 页面顶部是网页内容中最易发现且可能被用户查看的部分。滚动页面的交互成本使用户几乎不会阅读较长页面下游部分的内容。 有趣的是,屏幕分辨率的增加并没有像人们预期的那样导致滚动的减少。原因可能是设计师和开发者没有利用更大的屏幕来减少滚动,而选择将内容进一步分散。无论好坏,现在鼓励用户滚动网页比过去更多 —— 但不能更多了。在网络的早期,信息密度可能太高了(这导致了拥挤和杂乱的布局),但现在的页面设计绝对是太稀疏了。 启示鉴于用户在页面的顶部花费了更多的阅读时间,特别是在首页上,这里有一些你需要记住的事情: 将高优先级内容放到页面顶部:关键业务和用户目标。页面的下半部分可以放置次要信息或相关信息。将主要的 CTA 放到首屏。 重要的内容使用合适的字体样式来引起用户注意:用户根据标题和粗体文本等元素来识别信息的重要程度,并且定位新的内容片段。确保这些元素在视觉上与众不同,并在整个网站中保持风格统一,以便用户可以轻松地找到它们。 注意不要使用“假地板”,这在现代简约设计中越来越常见。完整性的错觉可能会干扰滚动。包括指示符(例如截断的文本),告诉人们下面还有内容。 通过用户代表测试您的设计,以确定“理想的”页面长度,并确保用户能够轻松地查看到他们想要的信息。 结论虽然现代网页往往很长并且包含负空间,并且用户可能比过去更倾向于滚动页面,但人们仍然将大部分浏览时间花在了页面的顶部。内容优先级是您在内容规划过程中的关键步骤。强大的视觉指示符有时会诱使用户滚动页面并发现首屏以下的内容。要确定理想的页面长度,请与真实用户一起测试,并记住,很长的页面会增加失去客户注意力的风险。 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2018/10/23/scrolling_and_attention/"},{"title":"[译] UX 设计实践:如何设计可扫描的 Web 界面","text":"原文地址:UX Design Practices: How to Make Web Interface Scannable 原文作者:Tubik Studio 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/ux-design-practices-how-to-make-web-interface-scannable.md 译者:Ivocin 校对者:生糸, Junkai Liu 我们每天被大量的线上或线下的信息流压的不堪重负。由于新技术的发展和快速的互联网连接,人们生成的内容比他们能够接受的更多。面对众多网站和应用程序时,用户不会逐字逐句地阅读所有内容 —— 他们会首先扫描页面,看一下这些内容对他们是否有用。因此,可扫描性是当今网站可用性的重要因素之一。本文探究了这一现象,并且提供了如何使数字产品可扫描的技巧。 什么是可扫描性?对于页面或屏幕,动词“扫描”意味着匆匆一瞥或匆匆阅读。因此,可扫描性是将内容和导航元素呈现为可被轻松扫描的布局的方式。尤其是首次与网站交互时,用户一般都是快速查看内容,然后分析这些内容是不是他们所需要的。任何以下内容都可能成为这个过程的一个障碍:单词、句子、图像或动画。 顺便说一句,这种行为并不是什么新鲜事。几十年来,人们经常在新的杂志或报纸上做着相同的事情:在开始仔细阅读文章之前先浏览一遍。另外,从屏幕上阅读比在纸上阅读更累,因此用户会更具选择性地阅读,当他们开始厌烦的时候就会放弃阅读。 为什么可扫描性很重要?大约十年前,Jacob Nielsen 回答了“人们如何在网上阅读?”的问题。他的回答非常简单:“他们没有。人们很少逐字阅读网页;相反,他们扫描页面,挑选个别的单词和句子阅读”。从那时起没有太大变化的是:当我们不确定一个网站是否满足我们的需求时,我们不太会花时间和精力去浏览它。因此,如果没有在第一分钟抓住用户的眼球,那么用户离开网页的风险会很高。无论网站的类型是什么,可扫描性都是其用户友好性的重要因素之一。 如何检查网页是否可扫描?可以尝试将自己视为首次使用者并回答如下两个问题: 你在前几分钟看到的内容是否符合目标受众对此页面的期望? 你能在前两分钟了解页面上的信息类型吗? 如果这两个答案不都是正面的,也许是时候考虑如何加强网站的可扫描性了。加强网站可扫描性是值得投入时间的,因为扫描性好的页面在以下方面会变得更加高效: 用户更快速地完成任务并实现目标 用户在搜索他们需要的内容时会更少出错 用户可以更快地了解网站的结构和导航 跳出率降低 保留用户的水平越来越高 网站看起来更可信 SEO 率受到积极影响 流行的扫描模式界面设计师必须考虑的重要事项是眼睛扫描模式,它表明用户在最初的几秒内与网页交互的方式。当你了解了人们如何扫描页面或屏幕时,就可以将内容进行优先级排序,并将用户需要的内容放入最明显的区域。这个用户研究领域得到了 Nielsen Norman 集团的支持,帮助设计师和可用性专家更好地理解用户行为和交互。 收集用户眼动追踪数据的不同实验表明,通常访客扫描网站会使用几种典型的模型。 Z 模式 对于具有统一信息呈现和弱视觉层次的网页而言是非常典型的。 另一种模式具有 Z 字形图案,该模式通常用于具有视觉上分割内容块的页面。同样,读者的眼睛从左上角开始从左到右移动,并在整个页面上移动到右上角,扫描这个初始交互区域中的信息。 另一个模型是 Nielsen Norman 集团探索发现的 F 模式,表明用户经常会经历以下交互流程: 用户首先水平移动阅读,通常跨越内容区域的上部。这个初始元素构成了 F 的顶部栏。 接下来,用户稍微向下移动页面,然后在第二个水平移动中读取,该移动通常覆盖比先前移动更短的区域。这个额外的元素形成了 F 的下栏。 最后,用户以垂直移动扫描内容的左侧。有时这是一个相当缓慢和系统的过程,在眼动追踪热图上显示为实心条纹。有时用户扫描得更快,会创建一个带有斑点的热力图。最后构成了字母 F 的主干。 提高可扫描性的技巧1. 使用视觉层次对内容进行优先级排序基本上,视觉层次是按照人类感知最自然的方式,在页面上排列和组织内容的方式。其背后的主要目标是让用户了解每块内容的重要性级别。因此,如果应用了视觉层次,用户将会首先看到关键内容。 例如,当我们在博客中阅读文章时,我们首先会看到标题,然后是副标题,然后才是副本块。这是否意味着副本块中的信息不重要?其实不是这样,但通过这种方式用户就可以扫描标题和副标题,以了解文章是否对他们有吸引力,而不用阅读全文。如果标题和副标题起的恰当,它们能够告知用户文章的结构和内容,这会是说服用户去阅读更多的因素。另一方面,如果用户看到又大又长的没有分块的文本,他们会感到很害怕,因为无法得知阅读这篇文章需要多长时间,以及是否值得投入时间和精力。 有助于建立视觉层次的几个主要因素: 尺寸 颜色 对比 相近性 负空间 重复 所有这些都有助于设计人员将元素、链接、图像和副本集转换为由该页面布局组成的可扫描系统。 2. 将核心导航放入网站头部所有上文提到的眼动扫描模式都显示,无论特定用户遵循哪种模式,扫描过程都会从网页的顶部水平区域开始。用它来展示交互和品牌的关键区域效果非常好。这也是 UI / UX 设计师、内容管理者和营销专家都认为网站头部设计是一个关键点的原因。 另一方面,标题不应该过长:太多的信息使得无法集中注意力。将所有内容放入页面顶部的尝试会将布局变得混乱不堪。因此,在每个特定情况下,必须分析核心目标受众的目标,他们如何与网站背后的业务目标交叉,并以此为基础 —— 哪些信息或导航应该作为最重要头部内容。例如,如果是大型电商网站,搜索功能必须立即可见,并且通常可以在头部找到,并能从任何交互点访问到。对于小型企业网站而言,搜索功能根本不需要,但是直接看到的投资组合的链接是至关重要的。 Gourmet 网站 3. 保持负空间的平衡负空间 —— 或者通常称为空白区域 —— 是布局里的空白区域,不仅在布局中的对象周围,而且在它们之间和内部。负空间是页面或屏幕上所有对象的一种呼吸空间。它定义了对象的界限,根据 Gestalt 原则在它们之间创造了必要的联系,并建立了有效的视觉表现。在网站和移动应用程序的 UI 设计中,负空间是界面高可导航性的一个重要因素:没有足够的空气,布局元素没有被正确看到,因此用户可能会错过他们真正需要的东西。这可能是眼睛和大脑紧张的一个强有力的原因,尽管许多用户将无法明确表述这个问题。适量的负空间,特别是微空间,解决这个问题,并且使过程更自然。 4. 检查能否立即看到 CTA显然,绝大多数网页目的在于用户必须完成的特定操作。包含号召性用语(CTA)的元素(通常是按钮)应在几秒钟内显示,以便用户了解他们可以在此页面上执行的操作。 在黑白和模糊模式下检查页面可以很好地测试这一点。如果在这两种情况下都可以快速区分 CTA 元素,说明这一点做的不错。例如,在下面显示的面包店网站的网页上,可以很容易地在其他元素中看到将物品添加到列表中的 CTA 按钮。 Vinny’s 的面包店网站 5. 测试副本内容的可读性可读性定义了人们阅读单词,短语和副本块的容易程度。易读性衡量用户如何快速直观地区分特定字体中的字母。应该仔细考虑这些特性,尤其是对于填充了大量文本的界面。背景色、副本块周围的空间量、字距,行距、字体类型和字体配对 —— 所有这些因素都会影响快速扫描文本和捕获令用户留下的内容的能力。为了防止这个问题,设计人员必须检查是否遵循排版规则以及所选字体是否支持一般的视觉层次和可读性。用户测试将有助于检查用户能够快速轻松地感知文本。 6. 使用数字,而不是单词这条建议是基于 Nielsen Norman 集团的另一项调查。他们分享了一个重要的发现:眼动追踪研究表明,在扫描网页的过程中,数字通常会阻止用户徘徊并吸引注视,相反大量可以没有数字的单词会被用户忽略。我们潜意识地将数字与事实、统计数据、大小和距离相关联 —— 这些数据可能是有用的。因此,副本中的数字可以吸引用户注意,而代表数字的单词可能在大量副本中被遗漏。更重要的是,数字比文本数字更紧凑,因此它使内容更简洁,更省时。 7. 一个段落,一个想法在可扫描性方面处理副本内容,尽量不要使文本的内容太长。简短的段落看起来更容易消化,如果信息对读者没有价值,可以更容易跳过。因此,当你在一个段落中提出一个想法并为另一个段落开始另一个想法时,请遵循该规则。 Bjorn 设计工作室网站 8. 使用编号和项目符号列表使文本更易于扫描的另一个好方法是使用带有数字或项目符号的列表。它们有助于清晰地组织数据。此外,它们会引起用户的注意,因此信息不会在文本主体中丢失。 9. 突出显示文本中的关键信息加粗、斜体和颜色高亮显示虽然老派,但仍然有效。通过这种方式,你可以将注意力集中在段落中包含的重要想法、定义、引用或其他类型的特定数据上。更重要的是,文本的可点击部分(链接)必须在视觉上标注出来。我们习惯于看到它们加下划线、但使用颜色高亮或加粗字体会更有效。 10. 使用图像和插图在 Web 用户界面设计中,图像在表达情绪或传递消息方面是非常有帮助的,它们饱含信息和吸引力。原始插图,突出的英雄横幅,引人入胜的照片可以很容易地吸引用户的注意力,并支持一般的风格概念。更重要的是,它们在构建视觉层次方面发挥了重要作用,并使副本内容与插图或照片相结合,更容易消化。人们感知图像比理解文字更快,这是提高可扫描性的重要因素。 金融服务网站 提高网页的可扫描性,是设计人员和内容创建者对网站用户的真正尊重。这样我们就可以节省用户的时间和精力,为他们提供有组织,和谐的,有价值和有吸引力的内容。 最初为 tubikstudio.com 而写 欢迎到 Dribbble 和 Behance 观看 Tubik Studio 的设计。 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2019/01/08/ux-design-practices-how-to-make-web-interface-scannable/"},{"title":"Vite 2 + React 实践","text":"前言Vite 无疑是当前最火热的项目之一,随着春节后 2.0 版本的发布,越来越多开发者开始关注起了这个项目。这个号称下一代前端构建工具的给我们最大的印象的就是快。日常被缓慢的项目启动时间和 HMR 折磨,有了 Vite 之后,真的不要太爽。虽然 Vite 和框架无关,但应用较多的仍然是 Vue 工程,那么 React 工程是否能够顺利使用呢,这两周在工作之余,就开始了 Vite 2.0 + React 的尝试。当然其中踩了比较多的坑,比如 Antd 的引入带来的一系列问题,别名怎么写,代理如何配置,集成 ahooks 等等,希望能够帮助到使用 React 又想使用 Vite 提升开发效率的同学,少走弯路。当然,我们还可以从实践的过程中直观了解到 Vite 的预打包原理以及性能真实的感受。 本文源码地址在 vite-playgrounds 中,如果阅读中发现问题,欢迎 pr。 这段时间也参与了 Vite 中文文档 的翻译工作,欢迎有兴趣的同学前来优化和勘误。 另外做个预告,春节期间花了时间参与了尤大在 Twitch 上做客 Open Source Friday 节目的直播视频 —— Next generation frontend tooling with ViteJS ✨ Open Source Friday ✨ 的字幕翻译工作,在视频里有尤大关于 Vite 的各项功能的详细阐述、大神在线编码、在线 Debug、大佬 diss webpack 以及对 Vite 的哲学思考。视频总时长一个小时,假期复工后就没有太多精力投入了,估计下周会做好发出来,敬请期待。 好了,现在和我一起,Step by Step 进入 Vite 2 + React 实战之旅。 初始化项目1$ npm init @vitejs/app 选择项目名和模板: 安装依赖并启动: 123$ cd vite-react$ npm install$ npm run dev 对比使用 webpack 搭建 React 环境可以看出,使用 Vite 后,添加 Babel、React、webpack-dev-server 的工作都可以省略了: 添加 Antd 爬坑记样式爬坑记1$ npm install antd 在 App.jsx 中引入 antd : 1import { DatePicker } from ‘antd’; 保存后,页面 reload,注意不是 HMR,此时 Antd 已经被引入: 然后我们来添加 DatePicker,发现没有样式: 按照 antd 官网文档指引,引入 CSS 样式,稍微好了一些,但是仍然不正常: 尝试引入 Less,直接报错: Vite 内置支持 Less,但是使用需要用户手动安装 Less,详见官网文档:Features # CSS Pre-processors 1$ npm install -D less 安装完重新启动,又出现了新的错误: 熟悉的味道,是 Antd 的报错没错了。错误信息里提示可以看这个 Issue,没看这个 Issue 有两年了,发现这个 Issue 越来越大了,因为期间 less-loader 升级又带来了新坑。解决方案总结起来就是在 webpack 的 less-loader 中开启 javascriptEnabled,详见这个 Commit 那么在 Vite 中怎么做呢,Vite 也开放了 CSS 预处理器的配置,详见官网文档 ,在 vite.config.js 文件中添加如下配置: 12345678910// vite.config.js... css: { preprocessorOptions: { less: { javascriptEnabled: true, }, } }... 终于,样式正常了: 有关使用全量 Antd 包的警告分析另外需要注意的一点是,打开调试窗口,又一个熟悉的警告又出现了,相信早期使用 Antd 开发的同学都不陌生: 原来的项目使用的是 webpack,在 webpack@2 之后,就支持 ES 模块的 tree shaking 了,这个警告不会出现。 而在 Vite 中,我们可以查看官方文档,Vite 会对 npm 依赖进行预构建并重写为合法的 url。在本项目中,Vite 将 Antd 预构建到 /node_moduls/.vite 文件夹,并且将 url 重写为 /node_modules/.vite/antd.js?v=d2a18218,因此会出现这个警告。 不过不用担心,在生产包仍然会进行 tree shaking。而开发环境即使引入全量的 Antd 包,Vite 基于 esbuild 的预编译速度超快,而且之后浏览器会缓存住这个包,开发体验不降反升,这个警告就可以忽略了。 更换主题再来试试改变主题是否有效: 按照官网的指引,使用 less 的 modifyVar 来覆盖 less 变量,从而达到改变主题的目的,定制主题 - Ant Design和 webpack 配置在 less-loader 里不同, 在 Vite 中则简单很多,仍然在 less 的 option 里配置就可以了: 1234567891011// vite.config.js... less: { javascriptEnabled: true, modifyVars: { "primary-color": "#1DA57A", "link-color": "#1DA57A", "border-radius-base": "2px", }, },... 可以看到样式生效了: 按照这个思路,我们修改为暗黑主题也比较简单了: 123456789101112131415161718192021222324252627import { defineConfig } from "vite";import reactRefresh from "@vitejs/plugin-react-refresh";import { getThemeVariables } from "antd/dist/theme";// https://vitejs.dev/config/export default defineConfig({ plugins: [reactRefresh()], css: { preprocessorOptions: { less: { javascriptEnabled: true, modifyVars: { ...getThemeVariables({ dark: true, }), ...{ "primary-color": "#1DA57A", "link-color": "#1DA57A", "border-radius-base": "2px", }, }, }, }, },}); 没问题,暗黑主题生效: 配置代理,能通过 RESTFul 的方式访问参考官网的 server.proxy 配置,这里需要特别注意 proxy 的写法和我们之前使用的 webpack-dev-server 里面的 proxy 配置稍有不同,如下: 12345678910// vite.config.js server: { proxy: { '/api': { target: 'http://jsonplaceholder.typicode.com/', changeOrigin: true, rewrite: (path) => path.replace(/^\\/api/, ''), }, }, }, 此时我们访问 http://localhost:3000/api/users ,就能访问到 jsonplaceholder.typicode.com/users 的数据了 集成 React Router下面我们来添加菜单和路由,首先引入 React Router: 1$ npm i react-router-dom 新建 index.jsx 文件: 123456789101112131415import React from "react";import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";import App from "./App";export default function Index() { return ( <Router> <Switch> <Route path="/app"> <App /> </Route> </Switch> </Router> );} 修改 main.jsx 文件,将 App 替换为 Index: 12345678910111213import React from 'react'import ReactDOM from 'react-dom'import './index.css'- import App from './App'+ import Index from './index'ReactDOM.render( <React.StrictMode>- <App />+ <Index /> </React.StrictMode>, document.getElementById('root')) 此时访问 http://localhost:3000/app 可以访问到 App 页面,路由生效。 我们再来增加一个菜单,新建 Layout.jsx 文件: 12345678910111213import React from "react";import { Menu } from "antd";import { Link } from "react-router-dom";export default function Layout() { return ( <Menu selectedKeys="App" mode="horizontal"> <Menu.Item key="App"> <Link to="/app">App</Link> </Menu.Item> </Menu> );} 最后在 index.jsx 引入 Layout 文件,以上过程详见 这个 Commit 注意别名的用法 —— breaking change注意 Vite@2 的 UserConfig.alias 已经过时,改为使用 resolve.alias 替代,写法也和 Vite@1 不一样,详见 官网文档 - resolve-alias 和 rollup plugins 文档 1234567891011121314// vite.config.js...import path from "path";... resolve: { alias: [ { find: /^~/, replacement: path.resolve(__dirname, "src"), }, ], },... 然后我们调整文件结构,然后使用别名代替相对路径,详见 这个Commit 用户列表页面接下来我们开发一个模拟真实接口的用户列表 ,参考 umi@2 + dva,完成用户管理的 CURD 应用 的步骤 新建 User 页面添加 User 路由和页面,详见这个 Commit。 此时访问 http://localhost:3000/user 就能看到新增的 User 页面了: 请求接口爬坑记请求接口数据,这里使用 ahooks 的 use-request 这里使用 npm 7 安装 会报错: 因为 npm 7 会自动安装 peerDependency,而 ahooks 的 peerDependency 是 react@^16.8.6 ,导致与本项目的 react@17 版本冲突报错,这个问题已经在 这个 pr里修复了,但是目前还没有发包,暂时使用 yarn 绕过这个问题: 1$ yarn add @ahooksjs/use-request 现在就可以使用 use-request 请求 /api/users 获取用户信息了: 123const { data, error, loading } = useRequest('/api/users') 展示数据接口请求的坑填好后,就可以展示用户信息了。我们来添加一个表格展示用户信息,详见 这个 Commit。 use-request 返回了 loading,再结合 antd table 组件的 loading,处理 loading 就很方便了: 至此,模拟接口请求数据的页面就完成了。 关于分页的坑由于我们接口采用模拟接口,分页信息在 Response Headers 里面,请求 http://jsonplaceholder.typicode.com/users?_page=1&_limit=5 可以看到: 查看了 ahooks 的 API,发现并没有对 Response Headers 的处理功能,查看 源码 发现确实目前没有办法拿到 Headers 里的值: 真实接口一般分页信息都会在 Response Body 中,分页数据就很容易处理了,这个坑就不费时间填了。 What Else当然没有结束,不过剩下的过程就比较常规了,罗列如下: 集成 Redux,或者使用封装过的 dva.js 约定式路由 还记得 babel-plugin-import 吗,在 Vite 中,如果想样式按需加载,可以尝试使用这个插件:vite-plugin-style-import,现在已经支持 Vite 2 了 想使用 TypeScript,可以使用 @vitejs/create-app 的 react-ts 模板,然后按照本文的步骤配置即可 Production Mode、SSR,参照官网配置即可,遇到什么坑可以一起讨论","link":"/2021/02/26/vite2-practice/"},{"title":"【译】下一代前端构建工具 ViteJS 中英双语字幕","text":"原视频地址:Next generation frontend tooling with ViteJS ✨ Open Source Friday ✨ 中英文字幕视频地址(B站):【译】下一代前端工具 ViteJS 中英双语字幕 - Open Source Friday 中英文字幕视频地址(腾讯视频):【译】下一代前端工具 ViteJS 中英双语字幕 - Open Source Friday 英文字幕:听译、YouTube 自动字幕 英文校对、翻译:Ivocin 视频字幕地址: Vite-Open-Source-Friday 原文链接:【译】下一代前端工具 ViteJS 中英双语字幕 - 掘金 关于 Vite,来看看作者本人怎么说。本视频是 Vue 以及 Vite 作者 尤雨溪 在 2021 年 2 月 12 日在 Twitch 上做客 GitHub Open Source Friday 节目的直播视频。在视频里有尤大关于 Vite 的各项功能的详细阐述、大神在线编码、在线 Debug、大佬 diss webpack 以及对 Vite 的哲学思考。本视频很长,接近 70 分钟,下面是视频摘录,大家可以选择自己感兴趣的点自行传送。强烈建议大家观看视频,里面有很多细节相信大家会有收获。 Vite 的发音问题视频传送 - 1:18 有关 Vite 发音的灵魂拷问:既然 Vite 使用的是其法语发音,那为什么 Vue 不用它的法语发音呢?(大概是因为法语读音不好听吧)。尤大告诉我们,作者说怎么读那就怎么读吧。 个人认为 Vue 和 Vite 的文档堪称良心了,首先就交代自己名字的发音,让全球开发者统一认知。再来看 Svelte,别说发音了,至今拼写还记不住。 Vite 是什么视频传送 - 2:33 尤大自己也说,很难一句话描述清楚 Vite 到底是什么。主要原因可能是它主要包括两个部分,一个基于 ESM 的利用 esbuild 的开发服务器,另一个部分是基于 Rollup 的配置化的打包器。当然还有很多其他强大的功能,但是已经超过一句话了。尤大说市面上最接近 Vite 的产品是 Parcel,但二者的实现原理完全不同。 为什么 Vite 在此刻出现视频传送 - 4:53 本质原因应该是大部分现代浏览器(除了 IE 11)已经对原生 ES 模块支持的很好了,而且新版的 Node 也支持 ESM 了。ESM 终于可以在不久的将来一统江湖。原生的就是香。 起步 Demo视频传送 - 7:05 不使用 @vitejs/create-app,从 0 开始创建一个 Vite 工程 demo。 入口文件是 index.html视频传送 - 14:25 Vite 是 Opinionated 的视频传送 - 17:08 划重点,Vite 是 Opinionated 的,视频里多次展示了这块内容。 其实 opinionated 本来是个贬义词,是固执己见的意思,而用在计算机科学领域,又变成了一个绝对的褒义词,号称自己 opinionated 的工具通过约定保证了易用性,又提供了配置以保证不会丧失灵活性。Vite 中内置了大量最佳实践的约定,省去了繁琐的配置,保证前端开发者常用的功能都是开箱即用的。 关于 Opinionated 的译法可以参考 掘金翻译计划的一个 PR,Vite 中文文档的一个 PR 这两处的讨论。 那么问题来了,列出几个 opinionated 和 unopinionated 的软件。我先来:Opinionated 的有 Vite、Prettier, Unopinionated 的比如 webpack,当然 unopinionated 可不是好词,应该不会有人在官方文档里写自己是 unopinionated 的。 这段是关于 webpack 的,看大佬如何 diss webpack: Vue CLI 会迁移到 Vite 上吗视频传送 - 23:56 暂时不会,目前依然是基于 webpack 的,但是最终肯定是会迁移到 Vite 上的。 Vite 是框架无关的视频传送 - 25:43 Vite 提供了定义得非常好的 JavaScript API,可以在更高层级使用,比如 VitePress,它是 VuePress 的孪生兄弟,基于 Vite 构建。 Tailwind CSS + Vite 实战视频传送 - 27:07 尤大在线编写 Tailwind 代码翻车。 主持人调侃,原来 Evan You 也需要 debug 啊。 Vite + React 实战视频传送 - 35:30 主持人调侃,我们在线围观尤雨溪写 React! 关于 Esbuild —— “快”就一个字视频传送 - 38:24 Esbuild 是 Vite 为何如此快速的原因,它比传统 tsc 快 20-30 倍。Vite 用 esbuild 替代 Rollup 进行预打包,速度也非常快。 这里尤大透露了他的工作电脑,搭载 M1 芯片的 ARM 架构的 Mac Book Pro,遗憾的是,当时的 esbuild 还不支持 ARM 架构,但 Go 的最新版已经支持。没想到过了几天,esbuild 就发布了其支持 M1 芯片的版本,尤大在第一时间做了测试: DX 是啥视频传送 - 47:36 在视频翻译过程中,听到尤大说了 DX 一词,由于不知道是什么含义,反反复复听了好多遍,后来 Google 发现,原来 DX 是 Developer Experience 的意思,看来关爱开发者是有官方术语的,关于 DX 的解释可以参考 What Is DX? (Developer Experience)。 Vite 利用其快速的特性,极大提升了开发者的体验,尤大直言,他就像被宠坏了的孩子,项目启动超过 1 秒,他就很难忍受了。 关于 SSR视频传送 - 52:20 SSR 目前还处于实验阶段,详见官网文档。 关于 HMR视频传送 - 57:59 Vite 真正解决了 HMR 速度与随着应用越来越大而越来越慢的问题。 为啥生产模式不用 esbuild,不是更快吗?视频传送 - 65:31 其实也想用,但是 esbuild 目前对生产包支持不够健壮,很多配置无法通过 esbuild 实现。所以目前而言,Rollup 是一个好选择,虽然远比 esbuild 慢。 另外,可以用 esbuild 作为压缩器,替代 terser,详见 build.minify,这样会更快,但是包的体积可能会有 5% - 10% 左右的增长,看用户取舍。 后记好久没有做这么大型视频的翻译了,上一次还是 React Conf 2018 的翻译。本视频翻译从春节假期 2 月 15 日开始,开工后时间比较少,断断续续花了三周多时间。好在 GitHub 在 Twitch 视频失效后,视频上传到了 YouTube 上,利用其自动字幕功能,后期节省了很多时间。确实 YouTube 的语音转文字功能更为强大。本视频已经改为外挂字幕重新上传,字幕地址:Vite-Open-Source-Friday,如有问题,欢迎 pr。希望这个视频能够帮助到大家。","link":"/2021/03/08/next_generation_frontend_tooling_with_vitejs/"},{"title":"2021 推荐给 React 开发者的 Visual Studio Code 插件及配置","text":"本文是在 React 工程的 VS Code 插件及配置 基础上编写的,当时写的时候是 2018 年,至今项目组使用 React 开发已经两年多了,是时候更新一波 Visual Studio Code 插件使用文档了。本文主要以实用为目的,本着不折腾的原则,只挑选平时开发 React 工程用的顺手的插件、主题和字体,也欢迎同学们推荐好用的效率神器。 一、Windows 安装 VS Code 的小提示在安装时推荐勾选下图两个标红的选项,这样安装成功后,鼠标单击右键文件夹或文件就可以直接使用 VS Code 打开了,非常方便。 若安装时没有勾选这两个选项的同学,是不是只能重装 VS Code 才能使用这个功能呢?当然不是,可以参考这篇文章:将 VS Code 添加到右击菜单,通过修改注册表的方式来实现。 二、VS Code 插件的离线安装由于在银行软开,平时开发只能在内网进行,需要考虑插件的离线安装。参考了简单的 VSCode 插件离线安装方法,发现官方已经很贴心的提供了在线下载插件的功能,总结下来一共 3 步: 进入 Visual Studio Marketplace,搜索需要的插件 点击 Download Extension,下载下来的是 .vsix 格式文件 可视化安装 VS Code 插件的方法:如图点击左侧 EXTENSION 菜单,点击 … 按钮,选择“从 VSIX 安装…” 即可安装。 另外求助一个问题,就是如何下载匹配 VS Code 版本的离线插件。项目的开发环境在内网,导致 VS Code 的版本只能固定在某一个版本,但是插件市场提供的插件离线下载包都是对应的当前最新版本 VS Code 的,如何能够下载到指定版本 VS Code 的离线插件。 2020-12-03 更新,找到了下载指定版本插件的方法,可以查看 Download Extension 请求的 url, 目前的是 https://marketplace.visualstudio.com/_apis/public/gallery/publishers/${publisher_name}/vsextensions/${extension_name}/${version}/vspackage 可以通过查看插件的 Changelog 下载指定的版本,但是插件版本与 VS Code 版本之间的关系目前只能是通过时间来对照查找,不知道有没有更方便的方法。 三、外观配置VS Code 默认的外观确实不怎么好看,这里推荐两套个人比较喜欢的主题。 默认主题和字体预览: 1、配色主题: 下载量排名第一的 VS Code 主题插件:One Dark Pro 个人喜欢的主题:Material Theme,选择 Material Theme Palenight High Contrast 2、图标:Material Icon Theme 下载量排名第二的 Icon 插件,个人感觉比排名第一的 vscode-icons 更好看一些 3、英文字体:FiraCode 安装详见:Fira Code —— 专为编程而生的字体 推荐一个在线测试各种编程字体的网站:Programming Fonts 4、中文字体:思源黑体 安装详见:Fira Code —— 专为编程而生的字体 最终的配置如下: 1234567891011"workbench.colorTheme": "Material Theme Palenight","workbench.iconTheme": "material-icon-theme","editor.fontFamily": "'Fira Code','Source Han Sans CN'", // 设置的字体类型为 Fira Code 和思源黑体"editor.fontLigatures": true, // 控制是否启用字体连字"editor.fontSize": 16, // 字号"editor.lineHeight": 24, // 行高"editor.fontWeight": "500",// 字重"editor.minimap.enabled": false, // 不显示右侧预览地图"editor.renderIndentGuides": false, // 不显示缩进参考线"editor.rulers": [120],// 在一定数量的等宽字符后显示垂直标尺。输入多个值,显示多个标尺。若数组为空,则不绘制标尺。"editor.wordWrap": "on",// 自动换行 One Dark Pro 预览: Material Theme Palenight High Contrast 预览: 四、实用插件 Chinese (Simplified) Language Pack for Visual Studio Code: 适用于 VS Code 的中文(简体)语言包 英文好的同学可以不必安装本插件 ESLint: 使用 ESLint 进行代码检查,本插件必备 安装后可以看到在 node 上启动了一个 ESLint server Prettier: 格式化插件,Opinionated 格式化工具的代表,少些纠结,按照它的约定来就行了 支持语言: JavaScript、TypeScript、Flow、JSX、JSON、CSS、SCSS、Less、HTML、Vue、Angular、GraphQL、Markdown、YAML 可以搭配 ESLint、StyleLint,详见官网文档 GitLens: 增强了 VS Code 内置的 Git 功能 使用 Git 托管的项目必备本插件 [已经内置,不再需要安装该插件] Debugger for Chrome: 已经合并入 vscode-js-debug 从 VS Code 1.46 版本开始内置 Path Intellisense: 路径自动补全插件 虽然 VS Code 自带了自动补全路径功能,html文件没有问题,但是在 JSX 里有些文件无法智能提示(比如 JSX 里面的 img 的 src ),本插件很好用,相对路径绝对路径都没有问题 JSON Tools 格式化JSON很方便,只需两个快捷键: 格式化JSON Ctrl(Cmd)+Alt+M, 压缩JSON Alt+M IntelliSense for CSS class names in HTML CSS 智能提示插件 本插件已经支持className 和 class (TypeScript React, JavaScript and JavaScript React language modes) 暂不支持 css module 智能提示 CSS Modules: CSS Module 智能提示 本插件可以解决 IntelliSense for CSS class names in HTML 不支持 css module 智能提示的功能 可以实现 css 自动补全、转到定义位置 Bracket Pair Colorizer 2: 括号颜色匹配插件,已经升级到了第 2 版,使用与 VS Code 一致的括号分析引擎,速度更快,准确性更高 使用颜色来配对括号 Bookmarks: 将常用的位置添加到书签,可以极大提高效率 快捷键: Ctrl + Alt + K / Commond + Option + K CodeSnap: 简单快速生成漂亮的代码截图,非常推荐 Tabnine Autocomplete AI: 使用 AI 对代码进行自动补全和提示,用了一段时间,按照习惯补全代码还是很香的 离线也能使用 五、关于格式化ESLint + Prettier 应该是标配,具体配置会在下一篇文章。 六、Webpack 别名在 VS Code 中无法跳转到对应文件在项目根目录创建 jsconfig.json 文件,比如在 webpack 配置了 @src 的别名,配置如下: 12345678{ "compilerOptions": { "baseUrl": ".", "paths": { "@src/*": ["./src/*"] } }} 参考链接 强大的 VS Code 简单的 VSCode 插件离线安装方法 Fira Code —— 专为编程而生的字体","link":"/2021/02/08/vscode-recommend-to-react-developer-2021/"},{"title":"React 工程的 VS Code 插件及配置","text":"最近使用 VS Code 来开发 React,本文记录一些使用的 VS Code 插件以及离线安装插件的方法。 VS Code 插件的离线安装内网环境的开发需要考虑插件的离线开发,参考了简单的 VSCode 插件离线安装方法,发现官方以及很贴心的提供了下载插件的方式,总结下来一共3步: 进入 VS Code Market,搜索需要的插件 点击Download Extension,下载下来的是.vsix格式文件 安装VS Code时配置好了环境变量,在控制台执行 code --install-extension your-extension-name.vsix 即可安装 一些实用的插件 适用于 VS Code 的中文(简体)语言包 : 地址: Chinese (Simplified) Language Pack for Visual Studio Code 英文好的同学可以不必安装本插件 一个下载量最多的 VS Code 主题插件: One Dark Pro 地址: One Dark Pro ESLint: 地址: ESLint Debugger for Chrome: 地址:Debugger for Chrome 可以和 WebStorm 一样打断点了 路径自动补全插件: Path Intellisense 地址:Path Intellisense 虽然 VS Code 自带了自动补全路径功能,html文件没有问题,但是在 JSX 里有些文件无法智能提示(比如 JSX 里面的 img 的 src ),本插件很好用,相对路径绝对路径都没有问题 JSON工具: JSON Tools 地址: JSON Tools 格式化JSON很方便,只需两个快捷键: 格式化JSON Ctrl(Cmd)+Alt+M, 压缩JSON Alt+M CSS 智能提示插件: IntelliSense for CSS class names in HTML 地址: IntelliSense for CSS class names in HTML 本插件已经支持className 和 class (TypeScript React, JavaScript and JavaScript React language modes) 暂不支持 css module 智能提示 CSS Module 智能提示 地址: CSS Modules 本插件可以解决 IntelliSense for CSS class names in HTML 不支持 css module 智能提示的功能 可以实现 css 自动补全、转到定义位置 括号颜色匹配: Bracket Pair Colorizer 地址: Bracket Pair Colorizer 使用颜色来配对括号 Bookmarks 地址: Bookmarks 将常用的位置添加到书签,可以极大提高效率 React/Redux/react-router Snippets 地址: React/Redux/react-router Snippets React 的代码段,熟悉了之后很方便 JSX 中使用 Emmet 快速补全标签 VS Code 自带了 Emmet,可以自动补全 HTML 文件中的标签 想要补全 react 文件中 JSX 里面的标签,需要在用户设置中增加 Emmet 的设置:123456789{ ... // 在默认不支持 Emmet 的语言中启用 Emmet 缩写功能。在此添加该语言与受支持的语言间的映射。 // 示例: {"vue-html": "html", "javascript": "javascriptreact"} "emmet.includeLanguages": {"javascript": "javascriptreact"}, // 启用后,按下 TAB 键,将展开 Emmet 缩写。 "emmet.triggerExpansionOnTab": true ...} 其中emmet.triggerExpansionOnTab配置为true可以使用 TAB 键自动补全组件标签了 可以自动将class转为className,很方便 关于格式化插件感觉自带的代码格式化功能基本够用了,就没安装 Prettier 和 Beautify,Beautify反而还会把react里面的jsx格式化弄乱。 参考资料 强大的 VS Code 简单的 VSCode 插件离线安装方法","link":"/2018/08/01/vs_code_tools_for_react/"},{"title":"[译] Hooks 对 Vue 而言意味着什么","text":"原文地址:What Hooks Mean for Vue 原文作者:Sarah Drasner 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/what-hooks-mean-for-vue.md 译者:Ivocin 校对者:LeoYang, TUARAN 不要把 Hooks 和 Vue 的生命周期钩子(Lifecycle Hooks) 弄混了,Hooks 是 React 在 V16.7.0-alpha 版本中引入的,而且几天后 Vue 发布了其概念验证版本。虽然 Hooks 是由 React 提出的,它是一个对各 JavaScript 框架生态系统都有价值的、重要的组合机制,因此我们今天会花一点时间讨论 Hooks 意味着什么。 Hooks 主要是对模式的复用提供了一种更明确的思路 —— 避免重写组件本身,并允许有状态逻辑的不同部分能无缝地进行协同工作。 最初的问题就 React 而言,问题在于:在表达状态的概念时,类是最常见的组件形式。无状态函数式组件也非常受欢迎,但由于它们只能单纯地渲染,所以它们的用途仅限于展示任务。 类本身存在一些问题。例如,随着 React 变得越来越流行,类的问题也普遍成为新手的阻碍。开发者为了理解 React,也必须理解类。绑定使得代码冗长且可读性差,并且需要理解 JavaScript 中的 this。这里还讨论了使用类所带来的一些优化障碍。 在逻辑复用方面,我们通常使用 render props 和高阶组件等模式。但使用这些模式后会发现自己处于类似的“厄运金字塔”中 —— 样式实现地狱,即过度使用嵌套可能会导致组件难以维护。这导致我想对 Dan Abramov 像喝醉了一样大吼大叫,没有人想要那样。 Hooks 允许我们使用函数调用来定义组件的有状态逻辑,从而解决这些问题。这些函数调用变得更具有组合性、可复用性,并且允许我们在使用函数式组件的同时能够访问和维护状态。React 发布 Hooks 时,人们很兴奋 —— 下面你可以看到 Hooks 展示的一些优势,关于它们如何减少代码和重复: 将 @dan_abramov 的代码(来自 #ReactConf2018)可视化,你能看到 React Hooks 为我们带来的好处。pic.twitter.com/dKyOQsG0Gd — Pavel Prichodko (@prchdk) 2018 年 10 月 29 日 在维护方面,简单性是关键,Hooks 提供了一种单一的、函数式的方式来实现逻辑共享,并且可能代码量更小。 为什么 Vue 中需要 Hooks?读到这里你肯定想知道 Hooks 在 Vue 中必须提供什么。这似乎是一个不需要解决的问题。毕竟,类并不是 Vue 主要使用的模式。Vue 提供无状态函数式组件(如果需要它们),但为什么我们需要在函数式组件中携带状态呢?我们有 mixins 用于组合可以在多个组件复用的相同逻辑。问题解决了。 我想到了同样的事情,但在与 Evan You 交谈后,他指出了我忽略的一个主要用例:mixins 不能相互消费和使用状态,但 Hooks 可以。这意味着如果我们需要链式封装逻辑,可以使用 Hooks。 Hooks 实现了 mixins 的功能,但避免了 mixins 带来的两个主要问题: 允许相互传递状态。 明确指出逻辑来自哪里。 如果使用多个 mixins,我们不清楚哪个属性是由哪个 mixins 提供的。使用 Hooks,函数的返回值会记录消费的值。 那么,这在 Vue 中如何运行呢?我们之前提到过,在使用 Hooks 时,逻辑在函数调用时表达从而可复用。在 Vue 中,这意味着我们可以将数据调用、方法调用或计算属性调用封装到另一个自定义函数中,并使它们可以自由组合。数据、方法和计算属性现在可用于函数式组件了。 例子让我们来看一个非常简单的 hook,以便我们在继续学习 Hooks 中的组合例子之前理解构建块。 useWat?好的,Vue Hooks 和 React Hooks 之间存在交叉部分。使用 use 作为前缀是 React 的约定,所以如果你在 React 中查找 Hooks,你会发现 Hooks 的名称都会像 useState、useEffect 等。更多信息可以查看这里。 在 Evan 的在线 demo 里,你可以看到他在何处访问 useState 和 useEffect 并用于 render 函数。 如果你不熟悉 Vue 中的 render 函数,那么看一看官网文档可能会有所帮助。 但是当我们使用 Vue 风格的 Hooks 时,我们会如何命名呢 —— 你猜对了 —— 比如:useData,useComputed等。 因此,为了让我们看看如何在 Vue 中使用 Hooks,我创建了一个示例应用程序供我们探索。 详见视频演示:https://css-tricks.com/wp-content/uploads/2019/01/hooks-demo-shorter.mp4 演示网站 GitHub 仓库 在 src/hooks 文件夹中,我创建了一个 hook,它在 useMounted hook 上阻止了滚动,并在 useDestroyed 上重新启用滚动。这有助于我在打开查看内容的对话框时暂停页面滚动,并在查看对话框结束时再次允许滚动。这是一个好的抽象功能,因为它在整个应用程序中可能会多次使用。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546import { useDestroyed, useMounted } from "vue-hooks";export function preventscroll() { const preventDefault = (e) => { e = e || window.event; if (e.preventDefault) e.preventDefault(); e.returnValue = false; } // keycodes for left, up, right, down const keys = { 37: 1, 38: 1, 39: 1, 40: 1 }; const preventDefaultForScrollKeys = (e) => { if (keys[e.keyCode]) { preventDefault(e); return false; } } useMounted(() => { if (window.addEventListener) // older FF window.addEventListener('DOMMouseScroll', preventDefault, false); window.onwheel = preventDefault; // modern standard window.onmousewheel = document.onmousewheel = preventDefault; // older browsers, IE window.touchmove = preventDefault; // mobile window.touchstart = preventDefault; // mobile document.onkeydown = preventDefaultForScrollKeys; }); useDestroyed(() => { if (window.removeEventListener) window.removeEventListener('DOMMouseScroll', preventDefault, false); //firefox window.addEventListener('DOMMouseScroll', (e) => { e.stopPropagation(); }, true); window.onmousewheel = document.onmousewheel = null; window.onwheel = null; window.touchmove = null; window.touchstart = null; document.onkeydown = null; });} 然后我们可以在像 AppDetails.vue 一样的 Vue 组件中调用它: 1234567891011<script>import { preventscroll } from "./../hooks/preventscroll.js";...export default { ... hooks() { preventscroll(); }}</script> 我们不仅可以在该组件中使用它,还可以在整个应用程序中使用相同的功能! 能够相互理解的两个 Hooks我们之前提到过,Hooks 和 mixins 之间的主要区别之一是 Hooks 实际上可以互相传值。让我们看一下这个简单但有点不自然的例子。 在我们的应用程序中,我们需要在一个可复用的 hook 中进行计算,还有一些需要使用该计算结果的东西。在我们的例子中,我们有一个 hook,它获取窗口宽度并将其传递给动画,让它知道只有当我们在更大的屏幕上时才会触发。 详见视频演示:https://css-tricks.com/wp-content/uploads/2019/01/hook-logo.mp4 第一个 hook: 12345678910111213141516import { useData, useMounted } from 'vue-hooks';export function windowwidth() { const data = useData({ width: 0 }) useMounted(() => { data.width = window.innerWidth }) // this is something we can consume with the other hook return { data }} 然后,在第二个 hook 中,我们使用它来创建一个触发动画逻辑的条件: 123456789101112131415161718192021// the data comes from the other hookexport function logolettering(data) { useMounted(function () { // this is the width that we stored in data from the previous hook if (data.data.width > 1200) { // we can use refs if they are called in the useMounted hook const logoname = this.$refs.logoname; Splitting({ target: logoname, by: "chars" }); TweenMax.staggerFromTo(".char", 5, { opacity: 0, transformOrigin: "50% 50% -30px", cycle: { color: ["red", "purple", "teal"], rotationY(i) { return i * 50 } } }, ... 然后,在组件内部,我们将一个 hook 作为参数传递给另一个 hook: 12345678910<script>import { logolettering } from "./../hooks/logolettering.js";import { windowwidth } from "./../hooks/windowwidth.js";export default { hooks() { logolettering(windowwidth()); }};</script> 现在我们可以在整个应用程序中使用 Hooks 来编写逻辑!再提一下,这是一个用于演示目的不太自然的例子,但你可以看到这对于大型应用程序,将逻辑保存在较小的、可复用的函数中是有效的。 未来的计划Vue Hooks 现在已经可以与 Vue 2.x 一起使用了,但仍然是实验性的。我们计划将 Hooks 集成到 Vue 3 中,但在我们自己的实现中可能会偏离 React 的 API。我们发现 React Hooks 非常鼓舞人心,正在考虑如何向 Vue 开发人员介绍其优势。我们想以一种符合 Vue 习惯用法的方式来做,所以还有很多实验要做。 你可以查看这个仓库作为起步。Hooks 可能会成为 mixins 的替代品,所以虽然这个功能还处于早期阶段,但是一个在此期间探索其概念是有好处的。 (真诚地感谢 Evan You 和 Dan Abramov 为本文审阅。) 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2019/02/28/what-hooks-mean-for-vue/"},{"title":"[译] 为什么我用 JavaScript 来编写 CSS","text":"原文地址:Why I Write CSS in JavaScript 原文作者:max stoiber 译文出自:掘金翻译计划 本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/why-i-write-css-in-javascript.md 译者:Ivocin 校对者:MacTavish Lee, Mirosalva 三年来,我设计的 Web 应用程序都没有使用 .css 文件。作为替代,我用 JavaScript 编写了所有的 CSS。 我知道你在想什么:“为什么有人会用 JavaScript 编写 CSS 呢?!” 这篇文章我就来解答这个问题。 CSS-in-JS 长什么样?开发者们已经创建了不同风格的 CSS-in-JS。迄今为止最受欢迎的,是我和他人共同开发的一个叫做 styled-components 的库,在 GitHub 上有超过 20,000 颗星。 如下是它与 React 一起使用的例子: 12345678910import styled from 'styled-components'const Title = styled.h1` color: palevioletred; font-size: 18px;`const App = () => ( <Title>Hello World!</Title>) 这会在 DOM 里渲染一个字体大小为 18px 的浅紫红色的 <h1>: 为什么我喜欢 CSS-in-JS?主要是 CSS-in-JS 增强了我的信心。我可以在不产生任何意外后果的情况下,添加、更改和删除 CSS。我对组件样式的更改不会影响其他任何内容。如果删除组件,我也会删除它的 CSS。不再是只增不减的样式表了! ✨ 信心:在不产生任何意外后果的情况下,添加、更改和删除 CSS,并避免无用代码。 易维护:再也不需要寻找影响组件的 CSS 了。 尤其是我所在的团队从中获取了很大的信心。我不能指望所有团队成员,特别是初级成员,对 CSS 有着百科全书般的理解。最重要的是,截止日期还可能会影响质量。 使用 CSS-in-JS,我们会自动避开 CSS 常见的坑,比如类名冲突和权重大战(specificity wars)。这使我们的代码库整洁,并且开发更迅速。 😍 提升的团队合作:无论经验水平如何,都会避开 CSS 常见的坑,以保持代码库整洁,并且开发更迅速。 关于性能,CSS-in-JS 库跟踪我在页面上使用的组件,只将它们的样式注入 DOM 中。虽然我的 .js 包稍大,但我的用户下载了尽可能小的有效 CSS 内容,并避免了对 .css 文件的额外网络请求。 这导致交互时间稍微长一点,但是首次有效绘制却会快很多! 🏎💨 高性能:仅向用户发送关键 CSS 以快速进行首次绘制。 我还可以基于不同的状态(variant="primary" vs variant="secondary")或全局主题轻松调整组件的样式。当我动态更改该上下文时,该组件将自动应用正确的样式。 💅 动态样式:基于全局主题或不同状态设置组件样式。 CSS-in-JS 还提供 CSS 预处理器的所有重要功能。所有库都支持 auto-prefixing,JavaScript 原生提供了大多数其他功能,如 mixins(函数)和变量。 我知道你在想什么:“Max,你也可以通过其他工具或严格的流程或大量的培训来获得这些好处。是什么让 CSS-in-JS 变得特别?” CSS-in-JS 将所有这些好处结合到一个好用的包中并强制执行它们。它引导我走向成功的关键:做正确的事情很容易,做错事很难(甚至不可能)。 谁在使用 CSS-in-JS?有上千家公司在生产中使用 CSS-in-JS,包括 Reddit、Patreon、Target, Atlassian、Vogue、GitHub、Coinbase 等等。(包括本网站) CSS-in-JS 适合你吗?如果你使用 JavaScript 框架来构建包含组件的 Web 应用程序,那么 CSS-in-JS 可能非常适合。特别是你所在团队中每个人都理解基本的 JavaScript。 如果你不确定如何开始,我会建议你尝试一下 CSS-in-JS,亲眼看看它有多好! ✌️ 如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。 掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。","link":"/2019/03/12/why-i-write-css-in-javascript/"},{"title":"我的 2015-2018 —— 银行软开三年项目回顾","text":"借着掘金“项目复盘”的机会,回顾自己工作的真实项目。本项目是我进入职场的第一个项目。从研三实习开始到第一个部门结束,一直都在做的项目。我们内部将这种项目称作“滚动开发包”,从名字就能看出来,持续滚动开发就是这个项目最大的特点,而从 2015 年 9 月开始,到 2018 年 7 月,将近 3 年的时间,我的主要工作都是围绕这个项目,是我职业生涯的第一个里程碑,在此进行复盘,希望其中的点滴内容能够帮助到希望进入银行软开的同学们。 一、项目简介本项目是部门的内部项目管理系统,提供了从项目立项、方案评审、实施、上线到结项的全生命周期项目管理,涵盖了项目管理中的资源、进度、成本、质量等关键维度,学过 PMP 的同学们应该都比较熟悉。 项目用户:部门中高管、PMO、行员、外包。 项目特点:滚动开发包,需求迭代非常频繁,管理侧改革落地基本都基于本系统;内部系统,用户体验方面要求不高;用户量不大,几乎没有高并发场景; 项目缺点:技术栈老旧、前后端未分离、需求变化频繁、缺少业务和技术文档、代码臃肿、开发效率低下; 项目类型:采购产品,并根据业务需求进行了多年的迭代开发 开发团队:10 左右规模,包括行内的项目经理、技术经理和我,其余开发、测试都为外包。 我入职的时候,行员就我一个开发,还有一个项目经理,一个技术经理。后来加入了祥哥作为技术经理,他是 Java 大牛。团队用了快 2 年的时间,将我们的项目从采购系统逐渐转变为了能够真正完全自主掌控的系统。我在这个项目中做过很多的角色,前后端开发、UI 设计、产品经理、项目经理、DBA、运维、运营、客服这些工作都贯穿在我工作的始终,银行软开的小伙伴对这种一人分饰多角的体验应该比较熟悉。我也通过接近两年的努力,将自己强行掰回了 Web 前端的轨道上来,并重归 Web 前端的正轨。 二、项目背景先聊聊本项目的技术栈,本项目原本是采购产品,上线时间为 2013 年,采购产品的技术栈是消失已久的 EJB,没错,就是 EJB,早已被 Spring 取代的企业级 Java 开发框架,估计现在还掌握 EJB 的都是远古时期的 Java 大哥们了。系统架构大致如下,实际耦合更为严重: 老旧的技术栈造成了本项目的第一个困难。EJB 实在太重了,而且别说精通,熟练掌握的同事都不多,一般来说都是根据已有代码“照猫画虎”实现类似功能;而 Struts + JSP 的 MVC 方式,前后端完全耦合,很多复杂业务的 JSP 页面有几千行代码,修改起来十分痛苦。甚至很多页面是 JSP + Java 代码混合生成的,更增加了修改维护的难度。 当然这个系统也有一些优点,一是专业的全生命周期管理,结合了 PMP 的理论,该有的模块和功能都很齐全。二是完善的系统后管,包括通过页面可视化配置流程、列表页、表单页,这和最近几年比较火的低代码平台做的事情是一样的,只是通过 Java 实现的,连页面都是 Java 拼接 html 字符串返回给 JSP 的。通过页面配置可以实现不复杂的流程,但是用这套功能带来的问题就是,定制化的需求会极其难处理,即使像是在表单页面加个元素,有一些简单的交互事件这种简单需求,在这套框架处理起来就十分困难。 架构这么老旧,为什么不重构?我也经常问自己这个问题,归结起来原因还是很现实的,源源不断的需求带来的业务压力造成只能抽时间对关键的性能瓶颈做修补,祥哥来了之后有了更多的好转,很多技性能问题都陆续被解决了。 三、渐进式重构其实彼时行内早已有了基于 Spring 的企业级 Java 框架,并应用在了非常多的项目里,运行非常稳定,而前端也有了基于 React 技术栈的前端全家桶解决方案。下定决心重构的契机是在做一个评审的需求,这个需求变更很频繁,而且业务逻辑非常复杂,现有的 JSP 代码累积接近两万行,即使改一个小需求都非常困难,终于下定决心根项目经理商量,我要重构这块内容。渐进式重构是一个当时最好的选择,拥有复杂逻辑的复杂系统,完全推翻重来很不现实。 那么接下来的问题就是: 前端技术选型 后端技术选型 新老系统互相跳转,如何做用户认证和鉴权 后端技术比较好选,行内统一后端技术栈,使用自研的基于 Spring 的企业级 Java 框架就可以。下面聊聊剩余两个问题。 前端技术选型其实当时和现在一样,React 和 Vue 二选一,当时也巧,正好赶上了闹的沸沸扬扬的 Facebook 开源协议事件,可能很多同学都忘记了,可以看当时 Facebook 发的声明,后来迫于社区和 Apache 基金会的强硬措施,Facebook 最终做出了妥协,将 React 的协议改为了 MIT。但在当时时间节点, React 并不是一个很好的选择,所以自然就选择了 Vue。前端组件库,当时主流的是饿了么的 Element UI,我选择了 UI 更加精美的 iView,现在貌似已经商业化了,更名为了 View UI。 新老系统用户鉴权这块也是重构一直没有推动的原因,因为原来的方案是共享 session,但是比较复杂,不适合老项目。后来刚好行内要推广统一身份认证系统,这个系统基于 Oauth 协议,支持单点认证功能,祥哥想用其单点认证的功能,实现新老系统的跳转。但是当时我是不太想用这种方案,主要基于三方系统服务稳定性的考虑,统一身份认证服务若不稳定,则我们新老系统无法登陆,尤其是测试环境,统一身份认证经常出问题,严重影响我们内部系统的登陆。 后来我想出了一个比较简单的方法,使用 token 方式,大致流程如下:老系统跳转至新系统,先调用后台服务获取 token,后台会将用户信息、token、超时时间存入数据库;老系统页面跳转 url 携带 token,跳转至新系统的入口页面,该页面进行判断,新系统用户是否已经登陆,若未登陆,则请求鉴权服务,传入 token 作为参数,该服务调用老系统的服务,通过 token 查询记录,比较当前时间与超时时间,若未超,则以该条记录的用户进行新系统的登陆,至此完成登陆动作。新系统跳转老系统的流程同理。这里 token 使用了 JSESSIONID。 有了简单实用的鉴权方法,我们终于可以着手落地重构工作了,系统由新老两部分组成,架构图大致如下: 1、全新页面使用 VueJS 框架开发新页面 2、现有页面 新功能使用 iframe 嵌套新页面 老页面 JSP 使用 jsp: include 嵌套新页面 3、现有页面,使用 Http 请求调用新系统的服务 4、新老系统后台使用 Dubbo 通信 这里多说一下 jsp: include 的方式,利用了 JSP 的服务端渲染能力。不使用 iframe 的原因是,有个场景希望点击新页面按钮之后,实现弹出的 Modal 全屏的效果,但 iframe 的固定宽高将这个“全屏”效果限制在了 iframe 的内部,不满足需求。上文说的上万行的评审页面就是用这种方式重构的,光梳理清楚现有业务逻辑就花了一周多时间,最终差不多断断续续三个月时间完成了这块功能的局部重构。重构之后,后续的迭代需求开发起来就十分轻松了,过去平均一周的开发工作,现在几个小时就能搞定,而且极大优化了用户操作体验。 直到 2018 年 7 月我调离到其他部门,本项目的新老系统并存方案已经平稳运行,并支持了 6 个大版本和 10 余个小版本共计数百个功能点的开发任务,无论是开发效率还是前端用户体验都有了极大提升。 四、总结思考本文即是项目复盘,也是我职业生涯的第一阶段回顾,和我很多互联网的同学不同,银行软开有其很多独有的特点,下面写一些个人这些年的思考,希望可以帮到希望进入银行软开的同学们。 项目很坑怎么办这个问题实际上很现实,不是所有的人都能接手一个明星项目,也不一定上来就能做一个全新的项目。我工作多年来基本接触的项目都是迭代开发的项目。由于历史原因,有坑实际是很正常的,有时回头看看自己当年很满意的代码,也感觉有很多可以优化的空间。我一个同事跟我说,坑的项目也有好处,因为有太多可以优化的空间,现在回想起来真的是这样,虽然可能会很困难,但是将坑填平也是对职业发展很有益处的一件事。所以即使项目很坑,也应该改变心态,不应该一味抱怨,发挥我们的能动性,将坑的项目变好,实现自我价值的提升。 银行软开是不是很轻松相较于互联网行业高强度工作,银行软开强度确实没有那么高。但有时项目比较紧急,996 也是比较正常。到底加不加班,工作强度如何,还是看部门和项目,对于每周有上线的同事,周四可能会半夜才结束工作。 要不要进银行软开先说优点,对于应届的同学,可能更多的是出于户口的考虑,概率比互联网大厂要高。工作强度没有那么大,一般都能享受到周末的美好时光。优秀的同事,一般银行软开的入门门槛都是名校的硕士,和优秀的同事一起工作体验真的是很不错,我在的第一个部门的前辈基本都是资深的项目管理专家,我在他们身上学到了很多优秀的工作方法,并且取得了 PMP 证书,并真正把 PMP 学到的知识应用到了日常的项目管理实践中。还有就是福利比较好,免费的早午晚餐,节假日的礼品等等。缺点也比较明显,最大的缺点就是传统的行员等级晋升对于新人很不友好,涨薪非常缓慢。还有就是技术场景可能不如互联网一线大厂,技术栈相对落后,技术提升需要自身努力。 银行软开外包是一个好选择吗银行软开一个很大的特点是外包项目很多,很多同学可能在犹豫要不要找银行软开外包的工作,我的建议是,只要是有技术追求、工作认真负责的同事,实际上会有机会的,过去表现好是可以转正为正式员工的,而现在表现好也可以推荐到科技公司,工资福利也会比外包好很多。我之前也有前辈是外包转成行员了,技术能力很强,前几年因为小孩上学举家迁到了杭州,拿了阿里 P7 的 offer。 最后的话,感谢掘金的这个项目复盘活动,让我对多年前的项目又重温了一次,点点滴滴回忆都涌上心头,现在的自己早已不是当初那个少年,怀念自己当年敢拼敢闯的日子。","link":"/2021/03/19/my2015-2018/"},{"title":"React 中后台系统多页签实现","text":"在中后台管理类系统中,多页签的需求非常普遍,用户常常需要在多个页签内跳转,比如填写表单时去查询某个列表获取一些字段信息再回到表单页面填写。这样的需求在 Vue 中使用 keep-alive 即可实现,但是在 React 中,React Router 切换路由后就会卸载组件,而本身并没有提供类似 keep-alive 的功能,所以实现多页签的功能就会变得格外困难。我的项目也遇到了同样的问题,在 2019 年左右做了技术调研和选型,最终选择了 react-router-cache-route,并在此基础上实现了多页签的需求,并稳定运行了 2 年的时间。下面我来复盘一下这次的多页签改造。 一、项目简介本项目是我现在所在部门的项目,是一个企业级中后台管理系统,包括系统管理、角色权限体系、基于 Activiti 的工作流引擎等很多开箱即用的功能。项目包括前后端,后端是我们部门自研的基于 Spring 的企业级 Java 框架,前端是 React 技术栈,当时还是 v15 版本。React Router 还是 v2 版本。 项目主要对象是提供给科技部门有中后台和流程需求的项目组,基于我们项目提供的基线工程,可以快速搭建工程,在此基础上根据需求进行开发。截止到 2019 年 10 月我离开该项目组,本项目已经服务了行内近 50 个系统。 二、需求背景当时的多页签需求还是比较明确的,因为我们团队在 2013 年使用 Sea.js + JQuery 的后管类系统都早已实现了,而新的使用 React 技术栈构建的新 UI 却丢失了这个功能,备受用户诟病,期望多页签的需求十分强烈。而 Vue 使用 keep-alive 即可实现多页签功能,如下图的 vue-element-admin 就是典型的多页签案例。 React 多页签本身好实现,难点是没有官方提供类似 Vue 的 keep-alive 功能,而使用 React Router,路由切换会直接卸载组件,导致无法缓存,用户的数据和行为因此丢失了。 社区上关于多页签的需求呼声也非常高,但是如 React 社区比较出名的中后台方案 Ant Design Pro 也不支持该功能,两年没看,至今仍然有很多 Issue 提出这类需求: 偏右大佬早在 2017 年对此做出了回应,详见 能否提供tab切换模式 · Issue #220 · ant-design/ant-design-pro · GitHub,这个 Isuue 虽然关闭了,但这些年仍然活跃: 看 👎 的数量就知道,用户其实对这种回答很不买帐。再来看 2019 年偏右对这个问题的解释,稍微具体了些: 这个解释我个人并不完全认同。首先说 “tab 模式无法(不适合)进行 url 的分享”其实是不成立的, url 带路由和参数就能准确跳转到对应页面,这在我们系统和 Vue 的多页签系统里都是基本功能;而说浏览器本身有 tabs 就不需要做到网站内部,也比较片面,SPA 的页面不开浏览器 tab 应该更符合 Antd 的设计价值观:足不出户 - Ant Design,就连最新版的 Chrome 都已经支持“群组”功能了,让用户在 SPA 页面尽量不开浏览器页签才应该是更好的体验设计。 看看社区其他人的理性分析: 三、方案选型经过一番调研之后,基本的思路大概有三种: 使用 Redux,数据往 store 里面怼,实现页面数据的”缓存“。 改写 React Router 源码,切换路由不卸载,改为隐藏。 使用社区的轮子,当时选了GitHub 里的两个产品: React Keeper 和 react-router-cache-route 其实每种方案都存在一些问题,最终的选择是使用了排除法。第一种方案的缺点是,由于存在大量的存量项目,而且项目本身的代码也很多,改造侵入性比较大,不是很好的选择。第二种的思路和 react-router-cache-route 比较像,就不想重复造轮子了。第三种选用开源方案其实当时也不太想选择,别看现在这两个项目都有 700 多 star,在当时 star 数只有几十个,而且 Issue 和 Pr 也很少,也就是用户和贡献者都不多,所以担心会有后续维护性的问题以及隐藏的暗坑。 最终同事选择了 react-router-cache-route,但在当时在项目尝试集成的时候,直接就报个错,给了我同事当头一棒,详见这个 Issue。 同事找到我来排查问题,经过定位,发现是 React 16 的一个 Breaking Change 导致的,从 React 16 版本开始, React 组件可以返回数组了,而 React 15 不行,详见我提交的这个 PR。 解决了 react-router-cache-route 在 React 15 版本报错问题之后,接下来的工作就是实现页签的 UI 和打开关闭的逻辑了,注意关闭需要调用 react-router-cache-route 的卸载缓存 API。 四、还存在什么问题项目组深度使用 react-router-cache-route 两年时间了,期间由于 React 和 React Router 版本迭代也出现过一些问题,好在 react-router-cache-route 的作者一直保持更新,解决了很多棘手的问题。 但目前总结起来仍然存在两个问题,一个是嵌套的 Cache Route 内部 Route 无法清除缓存问题,刚才看了一下,这个问题终于有了解,详见 Issue #64 : 但这个问题大约 1 年时间才有解! 还有一个问题,这个其实不是 react-router-cache-route 的问题。我们在多页签的迭代中增加了相同组件多开功能,这个场景比较常见,比如列表页点击链接跳转到表单页,可以同时打开多个表单,这样在不使用 Redux 是没有什么问题,但是一旦数据存在 Redux 中,多开组件就会有问题,显示的始终是 store 中最新的数据,要解决这个问题,需要重构 Redux 相关逻辑,比较麻烦。 五、现在 React 多页签方案有啥新进展吗有很长时间没关注了,这两天复盘看了看相关 Issue,发现又出现了一些新的轮子,没有验证过,先放在下面供同学们参考。如果想要实现多页签功能的同学,还是推荐使用 react-router-cache-route,毕竟我们已经稳定使用两年多了,没有太大问题。 react-router-cache-route (推荐) React Activation (和上面的工具同一个作者,Vue 中 功能在 React 中的实现,配合 babel 预编译实现更稳定的 KeepAlive 功能) umi-plugin-keep-alive (上面轮子的 umi 插件) react-keeper (774 star) react-antd-multi-tabs-admin (73 star,Antd多标签页后台管理模板) react-live-route (207 star,也是一种缓存路由的轮子) React Ant (232 star,基于Ant Design Pro 2.0 的多标签页tabs) Ant Design Pro Plus ( 88 star,基于 ant-design-pro 做一些微小的工作) React Admin (83 star,基于 Ant Design React 的管理系统架构) ant_pro_tabs (82 star,基于 Ant Design Pro 4 实现多标签页面,包括:路由联动,列表,多详情页共存,自动新增、关闭标签等功能) Antd Pro Page Tabs ( 54 star,Ant Design Pro 多页签,基于 UmiJS ) alita/packages/tabs-layout(基于 umi 的移动端 react 框架的缓存插件) 大家选择的时候可以考察其原理,star 数,Issue 数,PR 数等,当然,也可以看看他们实现的原理,学习一下这块的思路也是不错的。 六、结语中后台类系统多页签的需求应该是很多的,React 技术栈目前还没有大一统的解决方案,目前是轮子齐飞的状态。希望本文的经验能够帮助到大家,少走弯路。","link":"/2021/03/20/react-multi-tags-practice/"},{"title":"新版犀牛书该不该入手?","text":"三月初,各大前端公众号开始一波抽奖送书活动,原来是经典的“犀牛书”《JavaScript 权威指南》第七版的中文版面世了,等不到抽奖,迫不及待买了一本,这几天粗读了一遍,先给出结论:本书内容较新,涵盖了 ES2020 之前的全部内容,可以入手。 书变薄了今天主要是想通过和第六版的对比的形式来看看第七版犀牛书有什么变化。首先最大的变化就是书变薄了!从原来的 1000 多页缩到了不到 600 页,篇幅变小的原因书中也交代了,如今互联网的普及,纸书已经没有必要将参考资料放入其中,直接看 MDN 网站 就可以了。 译者第二个变化就是译者发生了变化,第七版的作者是拥有十五年技术翻译经验的李松峰,经典的“红宝书”《JavaScript高级程序设计》第 4 版也是出自其手。而第六版当年是淘宝前端团队做的翻译,当时正是 2011 年,十年之间发生了翻天覆地的变化,今天看了译者,无意中发现熟悉的云谦和玉伯二位大佬赫然在列,如今二人已从淘宝到了蚂蚁,大佬们十年前的个人介绍也如此青涩: 内容升级第三个变化就是内容了。内容上有非常大的更新,新增了类、模块、迭代器、生成器、Promise、async/await 等内容,并对章节进行了大幅度的调整。这里需要注意的一点是,Promise 在本书中被译为“期约”,倒是比较形象的译法。 十年间,JavaScript 飞速发展,第六版还是基于 ES5 编写的。而从 2015 年起,ES6(2015)开启了 JavaScript 新的篇章,ECMAScript 的规范也改为了按年为单位发布,也就有了(ES2016、ES2017、ES2018、ES2019、ES2020),就在 3 月 9 日, ES2021 的最终功能集也尘埃落定,预计今年 6 月就会成为最新的 JavaScript 标准,详见我翻译的这篇文章。而且自从 TC39 进程 制定以来,ECMAScript 版本的重要性就降低了很多,而且随着版本的逐渐增加,对用户而言关注某个特性对应的 ES 版本也变得更加困难,所以我们也常以 ES6 代指 ES2015 之后的版本。 纸质书最大的缺点就是无法如网站一样能够及时更新,尤其如 JavaScript 这样每年都有新功能成为新标准的语言,但是我们可以从这两个版本将尽十年的跨度,窥见 JavaScript 的发展,看到哪些是不变的,哪些又被时代洪流滚滚冲刷殆尽。 下面是最新版的犀牛书目录脑图: 升级要点对比第六版主要有如下要点: 不变的主要还是词法结构、类型、值和变量、表达式和操作符、语句、对象、数组、函数;这几个部分注意 ES6 新增的相关内容。 类和模块单独成为一章,十年间 JavaScript 模块化技术层出不穷,终于迎来了 ESM,原生的模块时代已然到来。 正则和子集与扩展被并入了 JavaScript 标准库一章,作者按照新的维度将新特性集合、映射 与 正则、日期、Error、JSON、国际化、console API、URL API、计时器等内容整合到了这里。 增加了迭代器、生成器、异步、元编程章节,可见其重要程度。 浏览器中的 JavaScript 章节被保留,将第六版单独成章的 Window 对象、脚本化文档、脚本化 CSS、事件处理、脚本化 HTTP、客户端存储、多媒体、图形编程、HTML 5 API 整合到了这章。并删除了 jQuery 类库一章,jQuery 时代一去不复返了。 第七版增加了 Node 服务端 JavaScript 一章,诞生在 2009 年的 Node 当时还不被作者重视,完全没体现在第六版中,而十年间 Node 的飞速发展给前端生态带来了翻天覆地的变化,借助 ES6 的定稿和 v8 引擎的加成,Node 也在 JavaScript 生态扮演了越来越重要的位置。 其实这两年随着前端框架的相对稳定,前端的生态已经进入了成熟期,面对纷繁复杂的新技术,构建知识地图对前端小伙伴尤其重要,从经典的教程可以看出作者的梳理 JavaScript 的知识脉络,给我们的知识地图的构建提供了很重要的参考。新版犀牛书,值得购买。 红宝书2021-03-26 应评论区同学要求,入手了“红宝书”第四版,先睹为快: 书里贴心自带学习路径图:","link":"/2021/03/24/new-JavaScript-Definitive-Guide-7-should-buy/"}],"tags":[{"name":"JavaScript","slug":"JavaScript","link":"/tags/JavaScript/"},{"name":"React","slug":"React","link":"/tags/React/"},{"name":"Tools","slug":"Tools","link":"/tags/Tools/"},{"name":"Translation","slug":"Translation","link":"/tags/Translation/"},{"name":"React Hooks","slug":"React-Hooks","link":"/tags/React-Hooks/"},{"name":"React Conf 2018","slug":"React-Conf-2018","link":"/tags/React-Conf-2018/"},{"name":"Frontend","slug":"Frontend","link":"/tags/Frontend/"},{"name":"Hexo","slug":"Hexo","link":"/tags/Hexo/"},{"name":"Blog","slug":"Blog","link":"/tags/Blog/"},{"name":"Create React App","slug":"Create-React-App","link":"/tags/Create-React-App/"},{"name":"Webpack","slug":"Webpack","link":"/tags/Webpack/"},{"name":"Ant Design","slug":"Ant-Design","link":"/tags/Ant-Design/"},{"name":"Less","slug":"Less","link":"/tags/Less/"},{"name":"Dva","slug":"Dva","link":"/tags/Dva/"},{"name":"Umi","slug":"Umi","link":"/tags/Umi/"},{"name":"Web","slug":"Web","link":"/tags/Web/"},{"name":"Performance","slug":"Performance","link":"/tags/Performance/"},{"name":"Design","slug":"Design","link":"/tags/Design/"},{"name":"UX","slug":"UX","link":"/tags/UX/"},{"name":"Vite","slug":"Vite","link":"/tags/Vite/"},{"name":"GitHub","slug":"GitHub","link":"/tags/GitHub/"},{"name":"VS Code","slug":"VS-Code","link":"/tags/VS-Code/"},{"name":"Hooks","slug":"Hooks","link":"/tags/Hooks/"},{"name":"Vue","slug":"Vue","link":"/tags/Vue/"},{"name":"CSS","slug":"CSS","link":"/tags/CSS/"},{"name":"职业生涯","slug":"职业生涯","link":"/tags/%E8%81%8C%E4%B8%9A%E7%94%9F%E6%B6%AF/"}],"categories":[{"name":"Front-End","slug":"Front-End","link":"/categories/Front-End/"},{"name":"React","slug":"Front-End/React","link":"/categories/Front-End/React/"},{"name":"JavaScript","slug":"Front-End/JavaScript","link":"/categories/Front-End/JavaScript/"},{"name":"Performance","slug":"Front-End/Performance","link":"/categories/Front-End/Performance/"},{"name":"Tools","slug":"Front-End/Tools","link":"/categories/Front-End/Tools/"},{"name":"Designs & Arts","slug":"Designs-Arts","link":"/categories/Designs-Arts/"},{"name":"Vue","slug":"Front-End/Vue","link":"/categories/Front-End/Vue/"},{"name":"CSS","slug":"Front-End/CSS","link":"/categories/Front-End/CSS/"},{"name":"UX","slug":"Designs-Arts/UX","link":"/categories/Designs-Arts/UX/"}]}