diff --git a/TODO1/the-art-of-system-performance-for-engineers.md b/TODO1/the-art-of-system-performance-for-engineers.md index d1b759ab629..446ef07a436 100644 --- a/TODO1/the-art-of-system-performance-for-engineers.md +++ b/TODO1/the-art-of-system-performance-for-engineers.md @@ -2,82 +2,82 @@ > * 原文作者:[The AI LAB](https://medium.com/@ailab) > * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) > * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/TODO1/the-art-of-system-performance-for-engineers.md](https://github.com/xitu/gold-miner/blob/master/TODO1/the-art-of-system-performance-for-engineers.md) -> * 译者: -> * 校对者: +> * 译者:[lhd951220](https://github.com/lhd951220) +> * 校对者:[z0gSh1u](https://github.com/z0gSh1u) [plusmultiply0](https://github.com/plusmultiply0) [kylinholmes](https://github.com/kylinholmes) -# The Art of System Performance for Engineers +# 写给工程师的《系统性能兵法》 -Encountering system performance problems are inevitable aspects of a software engineer’s life. Examples of common performance problems are: +遇到系统性能问题是软件工程师的职业生涯中不可避免的一部分。一些常见的系统性能问题示例如下: -* Disk I/O: like loading the application code from disk, or loading resources -* Network I/O: loading data from the server, or images -* Inter-Process Calls: for doing Single-Sign-On, or interacting with the OS +* 磁盘 I/O:比如,从磁盘中加载应用程序代码、加载资源 +* 网络 I/O:从服务器中加载数据或者图片 +* 进程间的调用:实现单点登录,或者与操作系统交互 -In Java, most of these problems are not immediately obvious when writing code. Class loads are implicit and most disk IO are abstracted away and can look like normal method calls. In fact, most of the time all of these (except network IO) perform fast enough to not be noticeable during casual local testing. Code loads fast because it is still in memory from a previous run, and the process you are calling into is still alive from the previous call. Even for network IO, some issues might slip out to production if the internal network is too fast to notice the problems. +在 Java 中,这些问题中的大部分都不会在写代码的时候立即暴露出来。 大多数磁盘IO方法的抽象化加之类的隐式加载,让它看起来像是在调用普通方法。实际上,大多数情况下, 所有这些问题(除了网络 IO)在非正式的本地测试中都运行得足够快, 从而不会注意到这些问题的发生。代码加载快的原因是在运行之前代码就已经在内存中,并且你调用的进程在之前的调用中仍然存活。而对于网络 IO 来说,如果因为网络太快而没有发现的问题也在会在生产环境中显现出来。 -Wouldn’t it be great if engineers could ‘see’ their calls becoming slower? +如果工程师可以“看见”他们的调用变慢,这样不是很好吗? -## Threads +## 线程 -On average, apps need to maintain a frame rate of 60fps or 16ms per frame. To achieve this the UI thread must update the UI in that time frame. Network IO usually takes multiples of 16ms, which gives us 2 options: chunk the work in smaller time slices, have something else perform the work and return the result. In Java, the second option is implemented using threads. +一般情况下,应用需要保持 60fps 的帧率,或者每 16ms 一帧。为了实现这个目的,UI 线程必须在 0 ~ 16ms 的时间范围内更新 UI。网络 IO 通常会花费数倍于 16ms 的时间,而这就给了我们两个选择:在更小的时间片中拆分任务,或者让其他进程执行工作并返回结果。在 Java 中,第二种选择是使用线程来实现的。 -Having a background thread doing the time consuming work, the results need to be show in the UI. The data still needs to be handed from the background thread to the UI thread. In practice, there might be a few patterns: +有一个后台线程来完成耗时的任务,任务的结果需要在 UI 中展示。因此需要将数据从后台线程传送到 UI 线程。实践中,可能有几种模式: -* the UI thread subscribes to a data event -* the UI thread reads/polls the data from a shared object. +* UI 线程订阅了数据的事件。 +* UI 线程从共享对象中读取/轮询数据。 -The first pattern is more robust, yet requires more work. The second is more natural, yet is riskier. As an example, throughout an app, developers might need to know the current user. Yet, when the application is launched this information still needs to be loaded from disk. This creates an interesting situation where developers block the UI thread to wait for a result generated by a background thread. Unfortunately this pattern, where developers make the UI thread wait for a background result, is more common than expected. +第一种模式更强大,但需要做更多的工作。第二种模式更加自然,但风险更大。举个例子,在整个应用程序中,开发者也许需要知道当前用户的信息。但是,当启动应用程序时,这些信息仍然需要从磁盘中加载。这导致了一个有趣的情况,开发者为了等待从后台线程生成的结果,而阻塞 UI 线程。不幸的是,这种使 UI 线程等待后台线程的模式,比预期的更为普遍。 -Wouldn’t it be great if engineers could ‘see’ when they call into a blocking method? +如果工程师可以“看见”他们调用阻塞方法,这样不是很好吗? -## Trade-offs +## 权衡取舍 -Doing all work on the UI thread is the simplest solution, and it works a lot of the time. Yet, in real world some of the work has serious performance outliers. So over time, developers would move most of that work onto a background thread. +在 UI 线程上完成所有工作是最简单解决方法,而且很多时候都有效。但是,在真实世界中,一些任务存在严重的性能问题。在大多数时候,开发者会将大部分工作转移到后台线程来完成。 -Reading the data directly from the result prepared by the background thread, is usually fine, except when there is a concurrent write, while the data is being read. So, there is the guarantee of exclusive data access through synchronization. +直接从后台线程准备好的结果中读取数据通常是更优的,除了在数据读取操作的过程中存在着并发写的操作的情况。所以,要通过同步操作来保证独占数据访问。 -Reading the data directly from the result with exclusive access, is usually quick, except sometimes when there is contention to get exclusive access to the data. Therefore, over time, developers would move some of that work to request-response models, where the UI thread subscribes to the result. +使用独占访问方式直接从结果中读取数据通常是很快的,除了有时会出现争抢对数据的独占访问的情况。因此,大多数的时候,开发者会将一些任务工作在请求-响应模式下,让 UI 线程订阅结果。 -Subscribing to data changes also requires up-front planning which might add more complexity to do the right thing for the users. +订阅数据的变化还要求预期计划,这会给用户在做正确的事情时添加更多的复杂度。 -Wouldn’t it be great if engineers could provide a simpler pattern to achieve their objectives of correct, performant code. +如果工程师可以提供一个更简单的模式来实现正确,高效的代码,这样不是更好吗? -Although these challenges might sound like difficult to tackle, here are some opportunities for engineers in terms of overcoming them: +尽管这些挑战听起来好像很难应对,这里有工程师克服这些挑战的一些建议: -1. Provide engineers with relevant run time information, to help them make better decisions while coding: e.g. create a plugin to display data. -2. Prototype a reactive/redux style user interface that is performant and easy to reason about. +1. 向工程师提供相关的运行时信息,以此帮助他们在写代码时做出更好的策略,例如:创建一个展示数据的插件。 +2. 原型是一个性能优秀且可读性好的 reactive/redux 样式的 UI。 -Needless to say, regardless of the level of hard work done by engineers, these system challenges also require some ‘soft’ skills for engineers in addition to their ‘hardware’ background. +毫无疑问,不论工程师们的努力程度如何,这些系统挑战要求工程师除了具有“硬”背景之外,还需要一些”软”技能。 -As an individual contributor, it is the engineer’s own responsibility to bring their questions and concerns to a meeting on system review. One might want to consider creating a document that is shared with a manager, put in topics one would like to discuss, and track commitments. +作为一名个人贡献者,将他们的问题和疑虑带到系统审查会议上是工程师自己的责任。可能需要创建一个与管理员共享的文档,提出一个你想要讨论的主题,并跟进提交。 -Don’t wait for company-wide official questionnaires or an all-hands meeting to raise a complex topic. Make sure you’re discussing hard things with your manager. This is all about mutual trust. +不要在公司范围的正式调查表上或在全体会议上提出一个复杂的话题。请确保你正在与经理讨论困难的事情。这些都是相互之间的信任。 -## Ship the product, and let everyone know about it +## 输出产品,并让每个人都知道它 -Success of the individual engineer is measured in different ways in various companies. Yet, one thing is relevant for everyone who wants to be recognized and rewarded. This **is shipping the product [or corresponding system development work — for infrastructure teams]**. This should be reflected in all self-reviews and discussions: what was delivered. +在不同的公司中,以不同的方式来衡量每一个工程师的成功。但是,有一个事情是与每一个想要被认可和被奖励的人相关的。就是**输出产品**,或者为基础架构团队进行相关的系统开发工作。这应该出现在自我审视和讨论中:交付了什么东西。 -You will do a huge favor to your manager if you explain which challenges you faced, and how you overcame them. Maybe your manager needs some justification to nominate you for a promotion. Help them make a strong case! +如果你向你的经理解释你面对的挑战,以及如何克服它们的,这将会对他大有帮助。也许你的经理需要一些理由来帮助你提名晋升,帮助他们找到一些理由。 -## Operate at the next level +## 进入下一等级 -Never underestimate the value of calibration sessions and don’t forget to find top-performers in your organization. There might be Senior Engineers already in other teams who might have been nominated for the promotion at exactly that calibration! +不要低估职级评定环节的价值,并且不要忘记寻找你的组织中的最佳表现者。其他团队中可能已经存在高级工程师,他们可能正是因为该标准而被提名晋升。 -If you’d like to get promoted — start operating at the next level, take the responsibility and larger project scope. Don’t wait until someone will ask you to take things over. Just go ahead, and establish your responsibility for it! The recognition will follow. +如果你想要得到晋升 —— 开始以下一等级的标准来工作,承担责任并且扩大项目范围。不能等到有人来告诉你做什么事情。现在就开始前进,建立你的责任感,对你的认可将会随之而来。 -## What it means to perform at the next level? +## 在下一等级意味着什么? ![](https://cdn-images-1.medium.com/max/2000/0*tTqppxhq9ubffWf-) -Most tech companies have levels of seniority (they might call professionals ‘Junior’, ‘Middle’, ‘Senior’, ‘Staff’, or have some corresponding codes like IC3-IC6). Some companies have career expectations defined: what you should be doing on each level of seniority. Something very rare to see is the “delivery time” dimension on career expectations. +大多数的科技公司都会对能力做等级划分(他们也许会称专业人士为“初级”,“中级”,“高级”,“专家”,或者有一些相应的编码,比如 IC3-IC6)。一些公司定义了职业期望:在每一个能力等级上你应该做什么。很少见的职业期望是“交付时间”维度。 -For example, if a Senior Engineer can deliver Feature X in two weeks, it might be okay for a mid-level Engineer to deliver it in three weeks. If organizations want to nominate the mid-level Engineer for the promotion — they should not only deliver quality work, but do it in the timeframe of the next level. +比如,如果一个高级工程师可以在两周的时间内交付特性 X,而这对于中级工程师来说可能需要三周的时间。如果组织想要提名中级工程师晋升 —— 他们不仅应该交付高质量的工作,还需要在下一等级的时间范围内完成工作。 -## Perform in a sustainable way +## 以可持续的方式发展 -Most companies have two types of compensation to recognize outstanding results of their employees. First one is a bonus, and the second one is promotion. Bonus is something people are paid for the extra mile they did during the past period. Promotion is a recognition of their competence and persistent performance at the next level. These two rewards don’t always come together. A good manager should spot if someone works 24x7, and consider flagging it as not operating at the next level yet. +大多数的公司都有两种类型的奖励来表彰员工的出色业绩。第一个是奖金,第二个则是晋升。奖金是对人们过去的一段时间的表现的额外回报。晋升是对他们拥有下一级别能力和长久以来的表现的认可。这两种奖励通常不会同时到来。一个好的管理者应该及时发现是否有人 24×7 小时制的工作,并且应该考虑将其标记为不能在下一等级中工作。 -Nobody can work 24x7 all the time, so the performance is likely to go down in the future. Make sure you exceed expectations while spending reasonable hours at work. +没有人可以在 24×7 全天候的工作,所以你的表现将会在未来下降。在花费了合理的时间在工作上的时候,确保收获是超出预期的。 > 如果发现译文存在错误或其他需要改进的地方,欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。 diff --git a/article/2020/8-scss-best-practices-to-keep-in-mind.md b/article/2020/8-scss-best-practices-to-keep-in-mind.md new file mode 100644 index 00000000000..32f76b3125b --- /dev/null +++ b/article/2020/8-scss-best-practices-to-keep-in-mind.md @@ -0,0 +1,237 @@ +> * 原文地址:[8 SCSS Best Practices to Keep in Mind](https://dev.to/liaowow/8-css-best-practices-to-keep-in-mind-4n5h) +> * 原文作者:[Annie Liao](https://github.com/liaowow) +> * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) +> * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/article/2020/8-scss-best-practices-to-keep-in-mind.md](https://github.com/xitu/gold-miner/blob/master/article/2020/8-scss-best-practices-to-keep-in-mind.md) +> * 译者: +> * 校对者: + +# 8 SCSS Best Practices to Keep in Mind + +This past week I had an opportunity to browse through a company's coding guideline, some of which I found very useful not only in collaborative settings but also in developing personal projects. + +Here are eight of SCSS best practices from the guideline that made me rethink the way I structure my CSS code: + +## 1. Mobile First + +When it comes to responsive design, it's common to prioritize the desktop version, which can make customizing for mobile a painful process. Instead, we should design to expand, not cram things to fit mobile. + +Don't: + +```scss +.bad { + // Desktop code + + @media (max-width: 768px) { + // Mobile code + } +} +``` + +Do: + +```scss +.good { + // Mobile code + + @media (min-width: 768px) { + // Desktop code + } +} +``` + +## 2. Set Variables + +Defining CSS variables and mixins should be part of the initial setup, which can make the project much more maintainable. + +According to the guideline, here are some common properties that will benefit from variables: + +- `border-radius` +- `color` +- `font-family` +- `font-weight` +- `margin` (gutters, grid gutters) +- `transition` (duration, easing) -- consider a mixin + +## 3. Avoid `#id` and `!important` + +Both `!important` and `#id`s are considered overly specific and can mess with the order of CSS rendering especially when developing collaboratively. + +Don't: + +```scss +#bad { + #worse { + background-color: #000; + } +} +``` + +Do: + +```scss +.good { + .better { + background-color: rgb(0, 0, 0); + } +} +``` + +## 4. Avoid Magic Numbers + +Try not to set arbitrary numbers because they "just work"; other developers might not understand why the property has to be set in such particular numbers. Instead, create relative values whenever possible. + +If you're interested, CSS Tricks have a [clear explainer](https://css-tricks.com/magic-numbers-in-css/) of why Magic Numbers are bad. + +Don't: + +```scss +.bad { + left: 20px; +} +``` + +Do: + +```scss +.good { + // 20px because of font height + left: ($GUTTER - 20px - ($NAV_HEIGHT / 2)); +} +``` + +## 5. Descriptive Naming + +It's easy to define CSS selectors according to the looks. It's better to describe the hierarchy. + +Don't: + +```scss +.huge-font { + font-family: 'Impact', sans-serif; +} + +.blue { + color: $COLOR_BLUE; +} +``` + +Do: + +```scss +.brand__title { + font-family: 'Impact', serif; +} + +.u-highlight { + color: $COLOR_BLUE; +} +``` + +## 6. Zero Values and Units + +This one might be up to personal choice or specific project style guide, but consistency is key. The rule below asks that you specify units on zero-duration times, but not on zero-length values. Also, add a leading zero for decimal places, but don't go crazy (more than three) on decimal places. + +Don't: + +```scss +.not-so-good { + animation-delay: 0; + margin: 0px; + opacity: .4567; +} +``` + +Do: + +```scss +.better { + animation-delay: 0s; + margin: 0; + opacity: 0.4; +} +``` + +## 7. Inline Commenting + +The best practice here is to comment on top of the property you're describing. Also, use inline commenting (`//`) instead of block-level comments (`/* */`), which is harder to uncomment. + +Don't: + +```scss +.bad { + background-color: red; // Not commenting on top of property + /* padding-top: 30px; + width: 100% */ +} +``` + +Do: + +```scss +.good { + // Commenting on top of property + background-color: red; + // padding-top: 30px; + // width: 100%; +} +``` + +## 8. Nesting Media Queries + +In order to easily locate media queries, it is recommended that you keep the media queries at the root of the declaration instead of nesting them inside each selector. + +Don't: + +```scss +.bad { + + &__area { + // Code + + @media (min-width: 568px) { + // Code + } + } + + &__section { + // Code + + @media (min-width: 568px) { + // Code + } + } +} +``` + +Do: + +```scss +.good { + + &__area { + // Code + } + + &__section { + // Code + } + + @media (min-width: 568px) { + &__area { + // Code + } + + &__section { + // Code + } + } +} +``` + +These are by no means an exhaustive list of best coding practices, but they certainly play a vital role in designing readable, scalable web apps. Is there any CSS guideline that you follow as your north star? Let me know in the comments! + +> 如果发现译文存在错误或其他需要改进的地方,欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。 + +--- + +> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 diff --git a/article/2020/8-unheard-of-browser-apis-you-should-be-aware-of.md b/article/2020/8-unheard-of-browser-apis-you-should-be-aware-of.md new file mode 100644 index 00000000000..6b1a9010c61 --- /dev/null +++ b/article/2020/8-unheard-of-browser-apis-you-should-be-aware-of.md @@ -0,0 +1,107 @@ +> * 原文地址:[8 Unheard of Browser APIs You Should Be Aware Of](https://medium.com/better-programming/8-unheard-of-browser-apis-you-should-be-aware-of-45247e7d5f3a) +> * 原文作者:[Mahdhi Rezvi](https://medium.com/@mahdhirezvi) +> * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) +> * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/article/2020/8-unheard-of-browser-apis-you-should-be-aware-of.md](https://github.com/xitu/gold-miner/blob/master/article/2020/8-unheard-of-browser-apis-you-should-be-aware-of.md) +> * 译者: +> * 校对者: + +# 8 Unheard of Browser APIs You Should Be Aware Of + +![Photo by [Szabo Viktor](https://unsplash.com/@vmxhu?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/9990/0*WzOJqPzOSrcQrX5b) + +With the increase in popularity, browsers started shipping APIs for complex functionalities that sometimes can only be implemented via a native application. Fast-forward to the present: It’s indeed quite rare to find a web application that doesn’t make use of at least one browser API. + +As the field of web development continues to grow, browser vendors also try to keep up with the rapid development around them. They constantly develop newer APIs that can bring new nativelike functionalities to your web application. Furthermore, there are some APIs that people don’t know much about, even though they’re fully supported in modern browsers. + +Here are some APIs you should be aware of — as they will play a vital role in the future. + +## The Web Locks API + +This [API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) allows you to run a web application on multiple tabs to access and coordinate resource sharing. Although it’s quite uncommon for simple everyday web applications to run on multiple tabs, there can be advanced use cases where you’d need to run multiple browser tabs of a web application and keep them synced. This API can come in handy at those instances. + +Although APIs such as SharedWorker, BroadcastChannel, localStorage, sessionStorage, postMessage, unload handler can be used to manage tab communication and synchronization, they each have their shortcomings and require workarounds, which decreases code maintainability. The Web Locks API tries to simplify this process by bringing in a more standardized solution. + +Even though it’s enabled by default in Chrome 69, it’s still not supported by major browsers such as Firefox and Safari. + +**Note:** You should know your way around concepts like **deadlocks** to avoid falling into one when using this API. + +## The Shape Detection API + +As a web developer, you’ve probably had many instances requiring the installation of external libraries to handle the detection of elements such as faces, text, and barcodes in images. This was because there was no web standard API for developers to utilize. + +The Chrome team is trying to change this by providing an experimental [Shape Detection API](https://web.dev/shape-detection/) in Chrome browsers and making it a web standard. + +Although this feature is experimental, it can be accessed locally by enabling the #enable-experimental-web-platform-features flag in `chrome://flags`. + +![Photo by [Element5 Digital](https://unsplash.com/@element5digital?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/9542/0*0NoSa7j_eQ1npFws) + +## The Payment Request API + +The [Payment Request API](https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API) helps customers and sellers complete the checkout process more seamlessly. This new approach eliminates checkout forms and improves the user’s payment experience from the ground up. With support for Apple Pay and Google Pay, this API can be expected to be a major component in the e-commerce sector. + +Furthermore, as the credentials are managed in the browser, it’s easier for the customer to switch from mobile to desktop browsers and still access their card information. This API also allows for customization from the merchant’s end. You can mention the supported payment methods and supported cards and even provide shipping options based on the shipping address. + +## The Page Visibility API + +It’s quite common for you to come across a PC with around 20 odd tabs opened in the browser. I once had a friend who just closed around 100+ tabs, after fixing a bug. Browsers have even started to [implement features](https://blog.google/products/chrome/manage-tabs-with-google-chrome/) to group your tabs to make them more organized. + +With the help of the [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API), you can detect whether your web page is idle or not. In other words, you can find out whether the tab that contains your web page is being viewed by the user. + +Although this sounds straightforward, it can be very effective in increasing the user experience of a website. There are several use cases where this API can be used. + +* Download the remainder of the application bundle resources and media resources while the browser tab is inactive. This will help you use the idle time very efficiently. +* Pause videos when the user minimizes or switches to another tab. +* Pause the image slideshow/carousal when the tab is inactive. + +Although developers have used events such as `blur` and `focus` on the window in the past, they didn’t tell whether your page was actually hidden to the user. The Page Visibility API helped addresses this issue. + +This browser API is compatible with most browsers. + +![Source: [MDN Docs](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API#Browser_compatibility)](https://cdn-images-1.medium.com/max/2000/1*I743ncklwG4-veVjsVFfSA.png) + +## The Web Share API + +The [Web Share API](https://www.w3.org/TR/web-share/) allows you to share links, text, and files to other apps installed on the device in the same way as native apps. This API can help increase user engagement with your web application. You can read this blog [post](https://web.dev/web-share/) by Joe Medley to learn more about this cool API. + +As of mid-2020, this API is only available on Safari and on Android in Chromium forks. The [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/share#Browser_compatibility) has more details regarding browser compatibility. + +![The native Share interface. Screenshot by the author.](https://cdn-images-1.medium.com/max/2000/1*uhEtWw7OEueQkMPXrn6Akw.png) + +## The Web Share Target API + +Progressive web apps are changing the way we understand applications by providing an applike experience in the web form. According to the website StateOfJS, around 48.2% of users have used PWAs and 45.5% of users are aware of what PWAs are. This shows the impact of PWAs. You can read more about PWAs in my article over [here](https://medium.com/better-programming/progressive-web-apps-an-overview-c6e4328ef2d2?source=friends_link&sk=94b7cf9919c4bb86e407604dd975dadb). + +Although PWAs have many nativelike features, they lacked a way to receive files from native apps. This API allows you to receive links, text, and files from other native applications. It’s supported on Chrome 76 and above for Android only. You can read more about this API over [here](https://web.dev/web-share-target/). + +## The Push API + +The [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) allows web applications to receive messages pushed to them from a server, regardless of whether the app is in the foreground or not. It can work even when the app isn’t loaded on a browser. This enables developers to deliver asynchronous notifications to the user in a timely manner. But for this to work, the user permission should be obtained prior to the API being used. + +You can read more about the Push API in this awesome [article](https://flaviocopes.com/push-api/) by Flavio. + +## The Cookie Store API + +Working with cookies is known to be a bit slow, as it’s synchronous. But the [Cookie Store API](https://developers.google.com/web/updates/2018/09/asynchronous-access-to-http-cookies) provides asynchronous access to HTTP cookies. Furthermore, this API also exposes these HTTP cookies to service workers. + +Although there are helper libraries to facilitate all of the usual cookie operations, with the Cookie Store API, it’ll be much easier and more performant. This API is also sometimes referred to as the Async Cookies API. + +You can read more about this API over [here](https://wicg.github.io/cookie-store/explainer.html). + +## Conclusion + +I was surprised by how cool the above APIs were when I played around with them. The only letdown of the above APIs, as mentioned before, is the lack of support from major browsers. This means it’s not simple to use these in production. But it can be assured that these APIs will definitely play a vital role in the future of browsers and web development. + +Thank you for reading, and happy coding. + +## References + +* [MDN web docs](https://developer.mozilla.org/en-US/) +* [SitePen](https://www.sitepen.com/blog/cross-tab-synchronization-with-the-web-locks-api/) +* [StateOFJS](https://2019.stateofjs.com/) +* [Creative Bloq](https://www.creativebloq.com/features/15-web-apis-youve-never-heard-of) + +> 如果发现译文存在错误或其他需要改进的地方,欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。 + +--- + +> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 diff --git a/article/2020/code-coverage-vue-cypress.md b/article/2020/code-coverage-vue-cypress.md new file mode 100644 index 00000000000..565b3396a99 --- /dev/null +++ b/article/2020/code-coverage-vue-cypress.md @@ -0,0 +1,328 @@ +> * 原文地址:[Code Coverage for Vue Applications](https://vuejsdevelopers.com/2020/07/20/code-coverage-vue-cypress/) +> * 原文作者:[Gleb Bahmutov]() +> * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) +> * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/article/2020/code-coverage-vue-cypress.md](https://github.com/xitu/gold-miner/blob/master/article/2020/code-coverage-vue-cypress.md) +> * 译者: +> * 校对者: + +# Code Coverage for Vue Applications + +![](https://vuejsdevelopers.com/images/posts/versions/code_coverage_1200.webp) + +Let's take a Vue application scaffolded with [Vue CLI](https://cli.vuejs.org/) like this [bahmutov/vue-calculator](https://github.com/bahmutov/vue-calculator) app. In this blog post, I will show how to instrument the application's source code to collect the code coverage information. We then will use the code coverage reports to guide the end-to-end test writing. + +## The application + +The example application can be found in [bahmutov/vue-calculator](https://github.com/bahmutov/vue-calculator) repo that was forked from [kylbutlr/vue-calculator](https://github.com/kylbutlr/vue-calculator) which used Vue CLI default template during scaffolding. The code is transformed using the following `babel.config.js` file: + +```js +// babel.config.js +module.exports = { + presets: [ + '@vue/app' + ] +} +``` + +When we start the application with `npm run serve`, we execute the NPM script + +```json +{ + "scripts": { + "serve": "vue-cli-service serve" + } +} +``` + +The application runs at port 8080 by default. + +![Vue calculator application](https://vuejsdevelopers.com/images/posts/code_coverage/calculator.png) + +Tada! You can calculate anything you want. + +## Instrument source code + +We can instrument the application code by adding the `plugins` list to the exported Babel config.The plugins should include the [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul). + +```js +// babel.config.js +module.exports = { + presets: [ + '@vue/app' + ], + plugins: [ + 'babel-plugin-istanbul' + ] +} +``` + +The application runs, and now we should find the `window.__coverage__` object with counters for every statement, every function, and every branch of every file. + +![Application coverage object](https://vuejsdevelopers.com/images/posts/code_coverage/coverage.png) + +Except the coverage object as shown above, includes only a single entry `src/main.js`, and the coverage object is missing both `src/App.vue` and `src/components/Calculator.vue` files. + +Let's tell `babel-plugin-istanbul` that we want to instrument both `.js` and `.vue` files. + +```js +// babel.config.js +module.exports = { + presets: [ + '@vue/app' + ], + plugins: [ + ['babel-plugin-istanbul', { + extension: ['.js', '.vue'] + }] + ] +} +``` + +**Tip:** we can place `istanbul` settings in a separate file `.nycrc`, or add them to `package.json`. For now, let's just keep these settings together with the plugin itself. + +When we restart the application, we get a new `window.__coverage__` object with entries for `.js` and for `.vue` files. + +![Instrumented JS and Vue files](https://vuejsdevelopers.com/images/posts/code_coverage/vue-covered.png) + +## Conditional instrumentation + +If you look at the application's bundle, you will see what the instrumentation does. It inserts counters around every statement, keeping track how many times a statement was executed. There are separate counters for every function and every branch path. + +![Instrumented source code](https://vuejsdevelopers.com/images/posts/code_coverage/instrumented.png) + +We do not want to instrument the production code. Let's only instrument the code when `NODE_ENV=test` since we will use the collected code coverage to help us write better tests. + +```js +// babel.config.js +const plugins = [] +if (process.env.NODE_ENV === 'test') { + plugins.push([ + "babel-plugin-istanbul", { + // specify some options for NYC instrumentation here + // like tell it to instrument both JavaScript and Vue files + extension: ['.js', '.vue'], + } + ]) +} +module.exports = { + presets: [ + '@vue/app' + ], + plugins +} +``` + +We can start the application with instrumentation by setting the environment variable. + +```shell +$ NODE_ENV=test npm run serve +``` + +**Tip:** for cross-platform portability use the [cross-env](https://github.com/kentcdodds/cross-env) utility to set an environment variable. + +## End-to-end Tests + +Now that we have instrumented our source code, let us use it to guide us in writing tests. I will install Cypress Test Runner using the official Vue CLI plugin [@vue/cli-plugin-e2e-cypress](https://cli.vuejs.org/core-plugins/e2e-cypress.html). Then I will install the [Cypress code coverage plugin](https://github.com/cypress-io/code-coverage) that will convert the coverage objects into human- and machine-readable reports at the end of the test run. + +```shell +$ vue add e2e-cypress +$ npm i -D @cypress/code-coverage ++ @cypress/code-coverage@3.8.1 +``` + +The [@vue/cli-plugin-e2e-cypress](https://cli.vuejs.org/core-plugins/e2e-cypress.html) has created folder `tests/e2e` where I can load the code coverage plugin from both the support and the plugins files. + +```js +// file tests/e2e/support/index.js +import '@cypress/code-coverage/support' + +// file tests/e2e/plugins/index.js +module.exports = (on, config) => { + require('@cypress/code-coverage/task')(on, config) + // IMPORTANT to return the config object + // with the any changed environment variables + return config +} +``` + +Let's set the environment variable `NODE_ENV=test` to the NPM script command `test:e2e` inserted into `package.json` by the [@vue/cli-plugin-e2e-cypress](https://cli.vuejs.org/core-plugins/e2e-cypress.html). + +```json +{ + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "test:e2e": "NODE_ENV=test vue-cli-service test:e2e" + } +} +``` + +We can place our first end-to-end spec file in `tests/e2e/integration` folder + +```js +/// +describe('Calculator', () => { + beforeEach(() => { + cy.visit('/') + }) + it('computes', () => { + cy.contains('.button', 2).click() + cy.contains('.button', 3).click() + cy.contains('.operator', '+').click() + cy.contains('.button', 1).click() + cy.contains('.button', 9).click() + cy.contains('.operator', '=').click() + cy.contains('.display', 42) + cy.log('**division**') + cy.contains('.operator', '÷').click() + cy.contains('.button', 2).click() + cy.contains('.operator', '=').click() + cy.contains('.display', 21) + }) +}) +``` + +Locally, I will use `npm run test:e2e` command to start the application and open Cypress. The above test passes quickly. Our calculator seems to add and divide numbers just fine. + +![Calculator test](https://vuejsdevelopers.com/images/posts/code_coverage/calculator.gif) + +The code coverage plugin automatically generates code coverage reports at the end of the run, as you can see from the messages in the Command Log on the left of the Test Runner. The reports are stored in the folder `coverage`, and by default there are several output formats. + +```text +coverage/ + lcov-report/ + index.html # human HTML report + ... + clover.xml # coverage report for Clover Jenkins reporter + coverage-final.json # plain JSON output for reporting + lcov.info # line coverage report + # for 3rd party reporting services +``` + +While working with tests locally, I prefer opening the HTML coverage report + +```shell +$ open coverage/lcov-report/index.html +``` + +The `index.html` is a static page that shows a table for each source folder with coverage information. + +![Coverage report](https://vuejsdevelopers.com/images/posts/code_coverage/coverage-report.png) + +**Tip:** store the entire `coverage/lcov-report` folder as a test artifact on your Continuous Integration (CI) server. Then browse or download the report to see the collected code coverage after the test run. + +End-to-end tests are **effective**. With a single test that loads and interacts with the entire application we have covered 60% of the source code. Even better, by drilling down to the individual files, we discover in `src/components/Calculator.vue` the features we have not tested yet. + +![Covered lines in Calculator.vue file](https://vuejsdevelopers.com/images/posts/code_coverage/covered-lines.png) + +The source lines highlighted in red are the lines missed by the test. We can see that we still need to write a test that clears the current number, changes the sign, sets the decimal point, multiplies, etc. But we did test entering and dividing numbers. The test writing thus becomes following the code coverage as a guide to writing end-to-end; add tests until you hit all lines marked in red! + +``` + Calculator + ✓ computes adds and divides (1031ms) + ✓ multiplies, resets and subtracts (755ms) + ✓ changes sign (323ms) + ✓ % operator (246ms) +``` + +As we write more tests we quickly gain coverage and confidence in our application. In the last test we will cover the `decimal () { ... }` method that remained red so far. + +![Decimal method without any coverage](https://vuejsdevelopers.com/images/posts/code_coverage/decimal.png) + +The test below types a single digit number and clicks the "." button. The display should show "5.". + +```js +it('decimal', () => { + cy.contains('.button', '5').click() + cy.contains('.button', '.').click() + cy.contains('.display', '5.') +}) +``` + +Hmm, this is weird, the test fails. + +![Decimal test fails](https://vuejsdevelopers.com/images/posts/code_coverage/decimal-fails.png) + +A power of Cypress test is that it runs in the real browser. Let's debug the failing test. Put a breakpoint in the `src/components/Calculator.vue` + +```js +decimal() { + debugger + if (this.display.indexOf(".") === -1) { + this.append("."); + } +}, +``` + +Open the DevTools in the browser and run the test again. It will run until it hits the `debugger` keyword in the application code. + +![Debugging decimal method](https://vuejsdevelopers.com/images/posts/code_coverage/debugger.png) + +Ohh, the `this.display` is a Number, not a String. Thus `.indexOf()` does not exist and the expression `this.display.indexOf(".")` throws an error. + +**Tip:** if you want Cypress tests to fail any time Vue catches an error, set the following in your code application code: + +```js +// exclude these lines from code coverage +/* istanbul ignore next */ +if (window.Cypress) { + // send any errors caught by the Vue handler + // to the Cypress top level error handler to fail the test + // https://github.com/cypress-io/cypress/issues/7910 + Vue.config.errorHandler = window.top.onerror +} +``` + +Let's fix the logical error in our code: + +```js +decimal() { + if (String(this.display).indexOf(".") === -1) { + this.append("."); + } +}, +``` + +The test passes. Now the code coverage report tells us that the "Else" path of the condition has not been taken yet. + +![Else path not taken](https://vuejsdevelopers.com/images/posts/code_coverage/decimal-else.png) + +Extend the test to click the "." operator twice during the test and it will cover all code paths and turn the entire method coverage green. + +```js +it('decimal', () => { + cy.contains('.button', '5').click() + cy.contains('.button', '.').click() + cy.contains('.display', '5.') + cy.log('**does not add it twice**') + cy.contains('.button', '.').click() + cy.contains('.display', '5.') +}) +``` + +![Decimal test passes](https://vuejsdevelopers.com/images/posts/code_coverage/decimal-test-passes.png) + +![All code paths covered](https://vuejsdevelopers.com/images/posts/code_coverage/decimal-covered.png) + +Now let's run all tests again. All tests pass in less than 3 seconds + +![All tests passing](https://vuejsdevelopers.com/images/posts/code_coverage/all-tests.gif) + +And the tests together cover our entire code base. + +![Full code coverage](https://vuejsdevelopers.com/images/posts/code_coverage/full-cover.png) + +## Conclusions + +* adding code instrumentation to Vue projects is simple if the project is already using Babel to transpile the source code. By adding the `babel-plugin-istanbul` to the list of plugins you get the code coverage information under `window.__coverage__` object. +* you probably want to only instrument the source code while running tests to avoid slowing down the production build +* end-to-end tests are very effective at covering a lot of code because they exercise the full application. +* the code coverage reports produced by `@cypress/code-coverage` plugin can guide you in writing tests to ensure all features are tested + +For more information read the [Cypress code coverage guide](https://on.cypress.io/code-coverage) and [@cypress/code-coverage](https://github.com/cypress-io/code-coverage) documentation. + +> 如果发现译文存在错误或其他需要改进的地方,欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。 + +--- + +> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 diff --git a/article/2020/from-scratch-to-the-first-10-customers-how-i-designed-and-launched-a-saas-product.md b/article/2020/from-scratch-to-the-first-10-customers-how-i-designed-and-launched-a-saas-product.md new file mode 100644 index 00000000000..b5c5c8ae5fc --- /dev/null +++ b/article/2020/from-scratch-to-the-first-10-customers-how-i-designed-and-launched-a-saas-product.md @@ -0,0 +1,212 @@ +> * 原文地址:[From scratch to the first 10 customers: How I designed and launched a SaaS product](https://codeburst.io/from-scratch-to-the-first-10-customers-how-i-designed-and-launched-a-saas-product-9176a8996b89) +> * 原文作者:[Valerio Barbera](https://medium.com/@valeriobarbera) +> * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) +> * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/article/2020/from-scratch-to-the-first-10-customers-how-i-designed-and-launched-a-saas-product.md](https://github.com/xitu/gold-miner/blob/master/article/2020/from-scratch-to-the-first-10-customers-how-i-designed-and-launched-a-saas-product.md) +> * 译者: +> * 校对者: + +# From scratch to the first 10 customers: How I designed and launched a SaaS product + +![Photo by [PhotoMIX Company](https://www.pexels.com/@wdnet?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels) from [Pexels](https://www.pexels.com/photo/black-samsung-tablet-computer-106344/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/12032/1*xNt4aprSuOo2bdYg9F-6gw.jpeg) + +Creating a successful software as a service (SaaS) product is the dream for many entrepreneurial-minded programmers. In the process of launching my own SaaS I discovered that sharing and comparing experiences with other founders is an essential part of this journey, and without this, I probably would never have created it at all. + +In this article, I’ll share the mental and practical processes that led me to create a SaaS product from scratch, and how I gained my first paying customers. Whether you are thinking about creating a new product or you have already launched, this article can help you compare your own strategies and methods with the ones that worked for me, and possibly adapt them for yourself. + +![](https://cdn-images-1.medium.com/max/2822/1*chMcZ5-kuoA-FvcH5R3FNg.png) + +I personally dedicate up to five hours per week researching the experiences of other founders. I’m always looking for new ideas, ways to avoid mistakes, and evaluating new strategies that could help me to obtain concrete results (that is, improve the product and increase customers’ happiness). + +For this reason, I decided to work in a completely frank and transparent way and share everything about my path — including what has been working and what has not — with the aim of helping one another through direct and rational discussion. + +## Article Organisation + +The article is divided into seven chronological sections, following every phase of the work I have done: + +* **Detecting the problem** +* **Quantifying the problem** +* **Evaluating competitors and their approach to the problem** +* **Developing the first prototype** +* **Throwing everything away and starting again** +* **Getting the first subscription** +* **How to move forward** + +The SaaS product I built is [Inspector](https://www.inspector.dev/), a real-time monitoring tool that helps software developers to avoid losing customers and money due to technical problems in their applications. + +## Detecting the problem + +![Photo by Pixabay from [Pexels](https://www.pexels.com/photo/black-samsung-tablet-computer-106344/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/10368/1*qWDI9beoYFfBmghh-Z6ybQ.jpeg) + +Spending the last 10 years working with software development teams made me realize how complicated it is for developers to handle technical problems that affect applications every day. Development teams have close relationships with their customers, and this is a high risk for companies that produce software because with problems you realize how fragile this bond really is. + +Users do not like problems! It seems obvious, but this aspect is constantly underestimated. This is an uncomfortable truth. No one likes to be in trouble, and it is instinctive to minimize the problem. But by denying this reality you could annoy the customer even more, to the point where they may even reconsider whether or not they “should” even pay you. + +Customers do not spend their time reporting problems and application errors. No one cares about helping us resolve bugs. They just leave our application, and it may be years before we see them again. Despite this, every team I have worked with used the best-known method of figuring out whether applications were working properly or not: + +> “If an angry customers calls you, the software is not working.” + +It is not exactly a technological solution… + +Maybe it seems ridiculous, but beyond the perception tycoons of technology project on our jobs, insiders know that urgency, limited budget, pressing customers, managers, forcing developers to constantly work under pressure, and adopting Band-Aid solutions (to temporarily fix a problem), are survival strategies. Working this approach for 10 years helped me realize there is clearly a problem here. + +## Quantifying the problem + +![Photo by Pixabay from [Pexels](https://www.pexels.com/photo/black-samsung-tablet-computer-106344/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/10000/1*aOZ8uDUscHo0oRipuqKPBg.jpeg) + +At the beginning of 2019, I had just finished some important projects and was expecting to enjoy a little period of calmness. During the last few years, I have used such moments to look for business opportunities which allow me to put my technical skills to good use with the hope of finding the right conditions to launch my own business idea. + +I knew from my experience as a developer that an easy and immediate monitoring instrument would be enough to help development teams to stay up-to-date about the performance of applications, instead of relying on customer calls to know when the software was creating problems. On the other hand, I did not need a tool to monitor everything, as everything often means nothing. And I didn’t want it to be complicated — I did not want to spend a month learning how it worked or need to hire expert engineers just for this job. My life had to be made easier than before. It was necessary to have a ready-to-go developer tool. + +The first step was to understand if there already were solutions trying to solve this problem, so I googled “**application monitoring**” and 941,000,000 results appeared: + +![](https://cdn-images-1.medium.com/max/2022/1*i2ZmDlt4rYw7d90QgEWwzA.png) + +Wow. That’s a very huge amount of content for a problem that probably is huge. But how huge, exactly? + +Software development team inefficiency is a problem I have always faced directly, but there is a big difference between estimating a job task and quantifying the economic impact of a problem. It is even more difficult on a large scale. This tweet captured my attention: + +![](https://cdn-images-1.medium.com/max/2000/1*TvugFYsVbZuQSD_iFwWBTw.png) + +**50% of developers admit to spending up to 50% of their time verifying that applications are working.** + +Software development is work mostly paid by the time technicians spend working on a project, and if there are periods in which developers spend 50 percent of their time checking that everything is okay, a tool which +completely automates this job could be useful enough to buy. + +**So why aren’t they so common to so many developers?** + +## Evaluating competitors and their approach to the problem + +![Photo by [Startup Stock Photos](https://www.pexels.com/@startup-stock-photos?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels) from [Pexels](https://www.pexels.com/photo/man-wearing-black-and-white-stripe-shirt-looking-at-white-printer-papers-on-the-wall-212286/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/10944/1*N-6vdg-eP1w_5Femejg5pA.jpeg) + +I thought about the two main parameters a company looks at when it has to decide which tools to use to increase productivity: + +1. **Simplicity** (ease of installation and use) +2. **Efficacy** (I spend x to solve a problem which is worth x+10, so I gain the +10) + +Using these parameters, I spent about a week creating an evaluation sheet of the most well-known monitoring instruments and I placed them in a graphic: + +![](https://cdn-images-1.medium.com/max/3682/1*jCKXywhfbMjCF98suUklJg.png) + +After days of putting information together, a look at the graphic was enough to realize where the problem was. Easy instruments do not provide enough value to the majority of developers. More complete instruments, instead, are thought of as being for big organizations and requiring skilled staff who dedicate themselves to their installation, configuration, and use, which ends up complicating team operations rather than simplifying them. + +**In my vision, the problem is not the monitoring itself but the development of team efficiency.** + +For a massive adoption, it would be necessary to have a product that requires a minute for the installation, no configurations, and, at the same time, that provides complete and easy information to consult that would allow even medium-sized development teams to fix the real-time monitoring problem. + +And of course, it has to be cool. + +![](https://cdn-images-1.medium.com/max/3682/1*v3O5TbWyrm1P1zC4IeM9TQ.png) + +## Developing the first prototype + +Finally, I decided to try it. The last work experience had gone well and I thought that it would not be impossible for me to create this tool. So, I immediately informed my partners that I wanted to build an MVP for the following two or three months. When I explained it to them, it was hard to make them understand the problem because they are not technicians involved at the same level I am. They gave me the okay based 90 percent on trust, and I thank them for this. + +Over the course of three months I was able to create this prototype: + +![](https://cdn-images-1.medium.com/max/2000/1*KgapMnraeCMY4GqmriE_fw.gif) + +While working on the implementation, I gradually understood the problems of realizing this kind of tool and even problems users would encounter during its use. From a technical point of view, a monitoring product has to be designed to work with huge quantities of data and I also wanted to deal with these data in real-time. + +I had to spend longer than I predicted for the backend part — in other words, the part which cannot be seen, or the backstage of an in-cloud software — leaving out the graphic interface (as you can see above), which is the part users see and use. + +## Throwing everything away and starting again + +![Photo by [Steve Johnson](https://www.pexels.com/@steve?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels) from [Pexels](https://www.pexels.com/photo/focus-photo-of-yellow-paper-near-trash-can-850216/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/9056/1*bsLtywHzuhiY52OKpFii6Q.jpeg) + +In the last few years, the dream of launching a product on the market pushed me to constantly study and apply marketing strategies that are particularly adept for SaaS software, to different projects (even the failed ones). I started to write articles for my blog with the aim of publishing them on different websites and social media to collect the first feedback. + +Although I wrote horrible content in English with writing mistakes because English is not my mother tongue, I started to receive feedback on my idea which included: + +* I don’t understand what I can do with it. +* How can I install it? +* Why use it rather than XXX? +* And so on… + +It was not easy to be objective while looking at developers’ responses and comments. The emotional reaction could always take over and it was really hard for me to understand where the mistakes were because I am not a sales agent or a seller, but I am a damn good technician. + +## Here are the lessons I learned along the way + +#### Lesson 1 — Selling sucks + +![Photo by Pixabay from [Pexels](https://www.pexels.com/photo/black-samsung-tablet-computer-106344/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/7638/1*4uq5q1_6T7myy2dANraKPA.jpeg) + +Thanks to my technical skills on the matter, I did not need to sell. Rather, I just needed to learn how to communicate the problems I faced every day and how I fixed them with my tools. + +I spent an entire month writing the most important things I knew about the monitoring and application scalability problems and the reasons why I decided to start this project, the difficulties I had been encountering during the development of a product, how I fixed them and moved forward, +code examples, technical guides, my best practices, and more. + +Then I gave everything to [Robin](https://www.fiverr.com/robinoo/professionally-proofread-1000-words?context=recommendation&context_alg=recently_ordered%7Cco&context_referrer=homepage&context_type=gig&mod=rep&pckg_id=1&pos=1&source=recently_and_inspired&tier_selection=recommended&ref_ctx_id=1bf19e6d-aa27-407d-86ea-f8ca268b8131), a Canadian copywriter found on Fiverr, who corrected all the content, including the website text, and polished the writing into native-level English. + +#### Lesson 2 — Insufficient product + +![Photo by [Kate Trifo](https://www.pexels.com/@katetrifo?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels) from [Pexels](https://www.pexels.com/photo/almost-empty-shelf-on-grocery-store-4019401/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/12480/1*UeZnUESwTIC3iLYnCsDdSw.jpeg) + +The fear of leaving out the user interface turned out to be a well-founded fear. What I did was not enough to create the kind of experience I had in my head, so I had to start again. The advantage of this was that I fixed the majority of the technical problems. It is not easy for an engineer to put themselves in a designer’s shoes. + +To improve my design sense I attended two courses about the development of graphic interfaces, read three books on design and user experience, and made direct experiments using VueJs as frontend framework. + +## Lesson 3 — Give it a try despite all doubts + +![Photo by Pixabay from [Pexels](https://www.pexels.com/photo/black-samsung-tablet-computer-106344/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/9216/1*Obd6Tg-u6D_Hf-he9M4LFw.jpeg) + +When we spend our time reading books and watching videos on marketing and business, we always learn common advice which, in practical situations, usually does not work. Consider, for example, the mantra: “If you wait until everything is ready, you will never start your business”. **So true**! + +But first experiences push us to emotional reactions that can put us on the defensive. This is a very big mistake because creating a product that is worthy of being bought thanks to its utility is a process, not a single event. The word “launch” misleads us, so forget about it and concentrate on “creation,” the process which, step by step, helps you understand what you need to change and improve to achieve your aim, compared with the outside world. + +## Inspector is born! + +It took me another two months of working on the project; during these months I decided to recreate the brand from scratch, trying to use the prototype experience not just in terms of product but also in terms of marketing and communication. + +[Inspector](https://www.inspector.dev/) — Identify issues in your code before users are aware of them, in less than one minute! + +![](https://cdn-images-1.medium.com/max/3072/1*7tXxOtkvca_518-3LPNEQA.png) + +We endlessly repeated ourselves that the aim was not the monitoring itself, but to help developers automate their working processes: + +* **Without any effort** +* **Without wasting time with manual procedures** +* **Guaranteeing flexibility when it is necessary** + +## Getting the first subscription + +![Photo by [Malte Luk](https://www.pexels.com/@maltelu?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels) from [Pexels](https://www.pexels.com/photo/selective-focus-photography-of-spark-1234390/?utm_content=attributionCopyText&utm_medium=referral&utm_source=pexels)](https://cdn-images-1.medium.com/max/8544/1*ZYIMfCsYF84XptBZ0v68lw.jpeg) + +On July 14, 2019, one of my technical guides was approved to be published on Laravel News, one of the most important websites for the Laravel developers’ community, which got the word out about this tool to an extended audience. Within four days, more than 2,000 people visited the Inspector website, almost 100 companies signed up, and the two first users — from Holland and Switzerland — subscribed. + +I cannot describe the emotion I felt when I received my first notice from Stripe which informed me that I had just received my first payment. When it happened, it was late in the evening. It felt like I was carrying an elephant on my shoulders for seven months and, finally, it went away and let me +breathe. + +A lot of problems cropped up during the following months, and they required time, effort, and money to be fixed. This included things like hacking attacks and overloaded infrastructures, problems that forced me to stay chained to the PC even on Christmas Eve. These are problems that send you crashing back to earth and make you realize things are even more difficult than before because you have something to lose now. + +With the first subscribers, I knew I had an interesting product and, thanks to the web and the cloud, I had the chance to make the product known to developers from all over the world. So I kept working hard, full-time, every day for months to create and publish technical articles from my +experience on as many channels as possible: + +* [https://medium.com/inspector](https://medium.com/inspector) +* [https://dev.to/inspector](https://dev.to/inspector) +* [https://valerio.hashnode.dev/](https://valerio.hashnode.dev/) +* [https://www.facebook.com/inspector.realtime](https://www.facebook.com/inspector.realtime) +* [https://www.linkedin.com/company/14054676](https://www.linkedin.com/company/14054676) +* [https://twitter.com/Inspector_rt](https://twitter.com/Inspector_rt) +* [https://www.indiehackers.com/product/inspector](https://www.indiehackers.com/product/inspector) +* [https://www.reddit.com/r/InspectorFans/](https://www.reddit.com/r/InspectorFans/) +* [https://www.producthunt.com/posts/inspector](https://www.producthunt.com/posts/inspector) + +Now, more than 800 companies and business people have tried Inspector and we have more than 20 active subscriptions now from Singapore, Germany, NewYork, France, Hong Kong, Holland, and more. + +## How to move forward + +Sharing and comparing with others has been fundamental in my journey to get here, so I plan to keep going this way. After all, I’m aware there are still a lot of things we need to improve and, even worse, problems that at the moment we’re ignoring entirely. This is why we started to tell this story during the most important Italian events where the topic is innovation. + +Now we are part of the [**Hubble**](https://www.inspector.dev/inspector-joins-nana-bianca-the-italian-startup-accelerator/) program, the Italian startup accelerator made by Paolo Barberis, Jacopo Marello, and Alessandro Sordi; three of Dada’s founders who spent 20 years of their lives collaborating to finance and support more than 30 Italian and foreign companies in growth. + +Our aim is to introduce ourselves to other managers, business people, and marketing and technology experts in order to elevate the product to the next level and test new instruments and strategies to get Inspector known all over the world. We would like to help software developers to work in a more productive and fun way thanks to smart tools which give them more free time to spend on more valuable activities instead of boring, repetitive, manual checks. + +## Conclusion + +In this article, I’ve shared the mental and practical process that led me to create and launch a SaaS product from scratch, and how I gained my first paying customers. Without sharing my journey I would never have created [Inspector](https://www.inspector.dev), so thank you for reading this article and I invite you to leave a comment below with any questions, curiosities, or just to tell me what you think about it. And if you think this article could be useful to others, please share it on your social media! + +> 如果发现译文存在错误或其他需要改进的地方,欢迎到 [掘金翻译计划](https://github.com/xitu/gold-miner) 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 **本文永久链接** 即为本文在 GitHub 上的 MarkDown 链接。 + +--- + +> [掘金翻译计划](https://github.com/xitu/gold-miner) 是一个翻译优质互联网技术文章的社区,文章来源为 [掘金](https://juejin.im) 上的英文分享文章。内容覆盖 [Android](https://github.com/xitu/gold-miner#android)、[iOS](https://github.com/xitu/gold-miner#ios)、[前端](https://github.com/xitu/gold-miner#前端)、[后端](https://github.com/xitu/gold-miner#后端)、[区块链](https://github.com/xitu/gold-miner#区块链)、[产品](https://github.com/xitu/gold-miner#产品)、[设计](https://github.com/xitu/gold-miner#设计)、[人工智能](https://github.com/xitu/gold-miner#人工智能)等领域,想要查看更多优质译文请持续关注 [掘金翻译计划](https://github.com/xitu/gold-miner)、[官方微博](http://weibo.com/juejinfanyi)、[知乎专栏](https://zhuanlan.zhihu.com/juejinfanyi)。 diff --git a/article/2020/how-to-build-redux.md b/article/2020/how-to-build-redux.md new file mode 100644 index 00000000000..2880f1a8ebf --- /dev/null +++ b/article/2020/how-to-build-redux.md @@ -0,0 +1,1457 @@ +> * 原文地址:[Build Yourself a Redux](https://zapier.com/engineering/how-to-build-redux/) +> * 原文作者:[Justin Deal](https://github.com/jdeal) +> * 译文出自:[掘金翻译计划](https://github.com/xitu/gold-miner) +> * 本文永久链接:[https://github.com/xitu/gold-miner/blob/master/article/2020/how-to-build-redux.md](https://github.com/xitu/gold-miner/blob/master/article/2020/how-to-build-redux.md) +> * 译者: +> * 校对者: + +# Build Yourself a Redux + +Redux is a simple library that helps you manage the state of your JavaScript app. Despite that simplicity, it's easy to fall down rabbit holes when learning it. I often find myself explaining Redux, and almost always start by showing how I'd implement it. So that's what we'll do here: Start from scratch and build a working Redux implementation. Our implementation won't cover every nuance, but we'll remove most of the mystery. + +Note that technically we'll be building [Redux](https://github.com/reactjs/redux?utm_source=zapier.com&utm_medium=referral&utm_campaign=zapier) **and** [React Redux](https://github.com/reactjs/react-redux?utm_source=zapier.com&utm_medium=referral&utm_campaign=zapier). At Zapier, we pair Redux with the (awesome) UI library [React](https://facebook.github.io/react/), and that's the pairing that shows up most in the wild. But even if you use Redux with something else, most everything here will still apply. + +Let's get started! + +## Bring Your Own State Object + +Most useful apps will get their state from a server, but let's start by creating our state locally. Even if we are retrieving from the server, we have to seed the app with something anyway. Our app will be a simple note-taking app. This is mostly to avoid making yet another todo app, but it will also force us to make an interesting state decision later. + +```js +const initialState = { + nextNoteId: 1, + notes: {} +}; +``` + +So first of all, notice our data is just a plain JS object. Redux helps **manage changes to your state**, but it doesn't really care much about the state itself. + +## Why Redux? + +Before we dig any deeper, let's see what it's like to build our app without Redux. Let's just go ahead and attach our `initialState` object to `window` like this: + +```js +window.state = initialState; +``` + +Boom, there's our store! We don't need no stinking Redux. Let's make a component that adds new notes. + +```jsx +const onAddNote = () => { + const id = window.state.nextNoteId; + window.state.notes[id] = { + id, + content: '' + }; + window.state.nextNoteId++; + renderApp(); +}; + +const NoteApp = ({notes}) => ( +
+ + +
+); + +const renderApp = () => { + ReactDOM.render( + , + document.getElementById('root') + ); +}; + +renderApp(); +``` + +You can try out this example live with [JSFiddle](https://jsfiddle.net/justindeal/5j3can1z/102/): + +```jsx +const initialState = { + nextNoteId: 1, + notes: {} +}; + +window.state = initialState; + +const onAddNote = () => { + const id = window.state.nextNoteId; + window.state.notes[id] = { + id, + content: '' + }; + window.state.nextNoteId++; + renderApp(); +}; + +const NoteApp = ({notes}) => ( +
+
    + { + Object.keys(notes).map(id => ( + // Obviously we should render something more interesting than the id. +
  • {id}
  • + )) + } +
+ +
+); + +const renderApp = () => { + ReactDOM.render( + , + document.getElementById('root') + ); +}; + +renderApp(); +``` + +Not a very useful app, but what's there works fine. Seems like we've proved we can get by without Redux. So this blog post is done, right? + +Not just yet… + +Let's look down the road a little bit. We add a bunch of features, build a nice backend for it, start a company so we can sell subscriptions, get lots of customers, add lots of new features, make some money, grow the company… okay, we're getting a little ahead of ourselves. + +It's difficult to see in this simple example, but on our road to success, our app may grow to include hundreds of components across hundreds of files. Our app will have asynchronous actions, so we'll have code like this: + +```js +const onAddNote = () => { + window.state.onLoading = true; + renderApp(); + api.createNote() + .then((note) => { + window.state.onLoading = false; + window.state.notes[id] = note; + renderApp(); + }); +}; +``` + +And we'll have bugs like this: + +```js +const ARCHIVE_TAG_ID = 0; + +const onAddTag = (noteId, tagId) => { + window.state.onLoading = true; + // Whoops, forgetting to render here! + // For quick local server, we might not notice. + api.addTag(noteId, tagId) + .then(() => { + window.state.onLoading = false; + window.state.tagMapping[tagId] = noteId; + if (ARCHIVE_TAG_ID) { + // Whoops, some naming bugs here. Probably from a + // rogue search and replace. Won't be noticed till + // we test that archive page that nobody really uses. + window.state.archived = window.state.archive || {}; + window.state.archived[noteId] = window.state.notes[noteId]; + delete window.state.notes[noteId]; + } + renderApp(); + }); +}; +``` + +And some hacky ad-hoc state changes like this that nobody even knows what they do: + +```js +const SomeEvilComponent = () => { + +}; +``` + +Add this all up across a large codebase with many developers over a long period of time, and we have a mounting set of problems: + +1. Rendering can be kicked off from anywhere. There will probably be weird UI glitches or unresponsiveness at seemingly random times. +2. Race conditions are lurking, even in the little bit of code we see here. +3. This mess is nearly impossible to test. You have to get the **whole** app in a specific state, then poke at it with a stick, and check the state of the **whole** app to see if it's what you expect. +4. If you have a bug, you can make some educated guesses about where to look, but ultimately, every single line of your app is a potential suspect. + +That last point is by far the worst problem and the main reason to choose Redux. If you want to shrink the complexity of your app, the best thing to do (in my opinion) is to constrain how and where you can change the state of your app. Redux isn't a panacea for the other problems, but they will likely diminish because of the same constraints. + +## The Reducer + +So how does Redux provide those constraints and help you manage state? Well, you start with a simple function that takes the current state and an action and returns the new state. So for our note-taking app, if we provide an action that adds a note, we should get a new state that has our note added to it. + +```js +const CREATE_NOTE = 'CREATE_NOTE'; +const UPDATE_NOTE = 'UPDATE_NOTE'; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case CREATE_NOTE: + return // some new state with new note + case UPDATE_NOTE: + return // some new state with note updated + default: + return state + } +}; +``` + +If `switch` statements make you nauseous, you don't have to write your reducer that way. I usually use an object and point a key for each type to its corresponding handler like this: + +```js +const handlers = { + [CREATE_NOTE]: (state, action) => { + return // some new state with new note + }, + [UPDATE_NOTE]: (state, action) => { + return // some new state with note updated + } +}; + +const reducer = (state = initialState, action) => { + if (handlers[action.type]) { + return handlers[action.type](state, action); + } + return state; +}; +``` + +That part isn't too important though. The reducer is your function, and you can implement it however you want. Redux really doesn't care. + +### Immutability + +What Redux **does** care about is that your reducer is a **pure** function. Meaning, you should never, ever, ever in a million years implement your reducer like this: + +```js +const reducer = (state = initialState, action) => { + switch (action.type) { + case CREATE_NOTE: { + // DO NOT MUTATE STATE LIKE THIS!!! + state.notes[state.nextNoteId] = { + id: state.nextNoteId, + content: '' + }; + state.nextNoteId++; + return state; + } + case UPDATE_NOTE: { + // DO NOT MUTATE STATE LIKE THIS!!! + state.notes[action.id].content = action.content; + return state; + } + default: + return state; + } +}; +``` + +As a practical matter, if you mutate state like that, Redux simply won't work. Because you're mutating state, the object references won't change, so the parts of your app simply won't update correctly. It'll also make it impossible to use some Redux developer tools, because those tools keep track of previous states. If you're constantly mutating state, there's no way to go back to those previous states. + +As a matter of principal, mutating state makes it harder to build your reducer (and potentially other parts of your app) from composable parts. Pure functions are predictable, because they produce the same output when given the same input. If you make a habit of mutating state, all bets are off. Calling a function becomes indeterminate. You have to keep the whole tree of functions in your head at once. + +This predictability comes at a cost though, especially since JavaScript doesn't natively support immutable objects. For our examples, we'll make do with vanilla JavaScript, which will add some verbosity. Here's how we really need to write that reducer: + +```js +const reducer = (state = initialState, action) => { + switch (action.type) { + case CREATE_NOTE: { + const id = state.nextNoteId; + const newNote = { + id, + content: '' + }; + return { + ...state, + nextNoteId: id + 1, + notes: { + ...state.notes, + [id]: newNote + } + }; + } + case UPDATE_NOTE: { + const {id, content} = action; + const editedNote = { + ...state.notes[id], + content + }; + return { + ...state, + notes: { + ...state.notes, + [id]: editedNote + } + }; + } + default: + return state; + } +}; +``` + +I'm using [object spread properties](https://github.com/tc39/proposal-object-rest-spread?utm_source=zapier.com&utm_medium=referral&utm_campaign=zapier) (`...`) here which aren't technically part of ECMAScript yet, but it's a pretty safe bet that they will be. `Object.assign` can be used if you want to avoid non-standard features. The concept is the same either way: Don't change the state. Instead, create shallow copies of the state and any nested objects/arrays. For any parts of an object that don't change, we just reference the existing parts. If we take a closer look at this code: + +```js +return { + ...state, + notes: { + ...state.notes, + [id]: editedNote + } +}; +``` + +We're only changing the `notes` property, so other properties of `state` will remain exactly the same. The `...state` just says to re-use those existing properties as-is. Similarly, within `notes`, we're only changing the one note we're editing. The other notes that are part of `...state.notes` will remain untouched. This way, we can leverage [`shouldComponentUpdate`](https://reactjs.org/docs/react-component.html#shouldcomponentupdate) or [`PureComponent`](https://reactjs.org/docs/react-api.html#reactpurecomponent). If a component has an unchanged note as a prop, it can avoid re-rendering. Keeping that in mind, we also have to avoid writing our reducer like this: + +```js +const reducer = (state = initialState, action) => { + // Well, we avoid mutation, but still... DON'T DO THIS! + state = _.cloneDeep(state) + switch (action.type) { + // ... + case UPDATE_NOTE: { + // Hey, now I can do good old mutations. + state.notes[action.id].content = action.content; + return state; + } + default: + return state; + } +}; +``` + +That gives back your terse mutation code, and Redux will **technically** work if you do that, but you'll knee-cap all potential optimizations. Every single object and array will be brand-new for every state change, so any components depending on those objects and arrays will have to re-render, even if you didn't actually do any mutations. + +Our immutable reducer definitely requires more typing and a little more cognitive effort. But over time, you'll tend to appreciate that your state-changing functions are isolated and easy to test. For a real app, you **might** want to look at something like [lodash-fp](https://github.com/lodash/lodash/wiki/FP-Guide?utm_source=zapier.com&utm_medium=referral&utm_campaign=zapier), or [Ramda](http://ramdajs.com/) or [Immutable.js](https://facebook.github.io/immutable-js/). At Zapier, we use a variant of [immutability-helper](https://github.com/kolodny/immutability-helper?utm_source=zapier.com&utm_medium=referral&utm_campaign=zapier) which is pretty simple. I'll warn you that this is a pretty big rabbit hole though. I even started writing a [library with a different spin](https://github.com/jdeal/qim?utm_source=zapier.com&utm_medium=referral&utm_campaign=zapier). Vanilla JS is fine too and will likely play better with strong typing solutions like [Flow](https://flow.org/) and [TypeScript](https://www.typescriptlang.org/). Just make sure to stick with smaller functions. It's much like the tradeoff you make with React: you might end up with more code than the equivalent jQuery solution, but each component is far more predictable. + +### Using our Reducer + +Let's plug an action into our reducer and get out a new state. + +```js +const state0 = reducer(undefined, { + type: CREATE_NOTE +}); +``` + +Now `state0` looks like this: + +```js +{ + nextNoteId: 2, + notes: { + 1: { + id: 1, + content: '' + } + } +} +``` + +Notice we fed `undefined` in as the state in this case. Redux always passes in `undefined` as the initial state, and typically you use a default parameter like `state = initialState` to pick up your initial state object. The next time through, Redux will feed in the previous state. + +```js +const state1 = reducer(state0, { + type: UPDATE_NOTE, + id: 1, + content: 'Hello, world!' +}); +``` + +Now `state1` looks like this: + +```json +{ + nextNoteId: 2, + notes: { + 1: { + id: 1, + content: 'Hello, world!' + } + } +} +``` + +You can play with our reducer [here (JSFiddle)](https://jsfiddle.net/justindeal/kLkjt4y3/37/): + +```jsx +const CREATE_NOTE = 'CREATE_NOTE'; +const UPDATE_NOTE = 'UPDATE_NOTE'; + +const initialState = { + nextNoteId: 1, + notes: {} +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case CREATE_NOTE: { + const id = state.nextNoteId; + const newNote = { + id, + content: '' + }; + return { + ...state, + nextNoteId: id + 1, + notes: { + ...state.notes, + [id]: newNote + } + }; + } + case UPDATE_NOTE: { + const {id, content} = action; + const editedNote = { + ...state.notes[id], + content + }; + return { + ...state, + notes: { + ...state.notes, + [id]: editedNote + } + }; + } + default: + return state; + } +}; + +const state0 = reducer(undefined, { + type: CREATE_NOTE +}); + +const state1 = reducer(state0, { + type: UPDATE_NOTE, + id: 1, + content: 'Hello, world!' +}); + +ReactDOM.render( +
{JSON.stringify(state1, null, 2)}
, + document.getElementById('root') +); +``` + +Of course, Redux doesn't keep making more variables like this, but we'll get to a real implementation soon enough. The point is that the core of Redux is really just a piece of code that you write, a simple function that takes the previous state and an action and returns the next state. Why is that function called a reducer? Because it would plug right into a standard [`reduce`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) function. + +```js +const actions = [ + {type: CREATE_NOTE}, + {type: UPDATE_NOTE, id: 1, content: 'Hello, world!'} +]; + +const state = actions.reduce(reducer, undefined); +``` + +After this, `state` would look identical to our previous `state1`: + +```json +{ + nextNoteId: 2, + notes: { + 1: { + id: 1, + content: 'Hello, world!' + } + } +} +``` + +Play around with adding actions to our `actions` array and feeding them into the reducer [(JSFiddle link)](https://jsfiddle.net/justindeal/edogdh33/13/): + +```jsx +const CREATE_NOTE = 'CREATE_NOTE'; +const UPDATE_NOTE = 'UPDATE_NOTE'; + +const initialState = { + nextNoteId: 1, + notes: {} +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case CREATE_NOTE: { + const id = state.nextNoteId; + const newNote = { + id, + content: '' + }; + return { + ...state, + nextNoteId: id + 1, + notes: { + ...state.notes, + [id]: newNote + } + }; + } + case UPDATE_NOTE: { + const {id, content} = action; + const editedNote = { + ...state.notes[id], + content + }; + return { + ...state, + notes: { + ...state.notes, + [id]: editedNote + } + }; + } + default: + return state; + } +}; + +const actions = [ + {type: CREATE_NOTE}, + {type: UPDATE_NOTE, id: 1, content: 'Hello, world!'} +]; + +const state = actions.reduce(reducer, undefined); + +ReactDOM.render( +
{JSON.stringify(state, null, 2)}
, + document.getElementById('root') +); +``` + +Now you can understand why Redux bills itself as "a predictable state container for JavaScript apps". Feed in the same set of actions, and you'll end up in the same state. Functional programming for the win! If you hear about Redux facilitating replay, this is roughly how that works. Out of the box though, Redux doesn't hold onto a list of actions. Instead, there's a single variable that points to the state object, and we keep changing that variable to point to the next state. That is one important mutation that **is** allowed in your app, but we'll control that mutation inside a store. + +## The Store + +Let's build a store now, which will hold onto our single state variable as well as some useful methods for setting and getting the state. + +```js +const validateAction = action => { + if (!action || typeof action !== 'object' || Array.isArray(action)) { + throw new Error('Action must be an object!'); + } + if (typeof action.type === 'undefined') { + throw new Error('Action must have a type!'); + } +}; + +const createStore = (reducer) => { + let state = undefined; + return { + dispatch: (action) => { + validateAction(action) + state = reducer(state, action); + }, + getState: () => state + }; +}; +``` + +Now you can see why we use constants instead of strings. Our action validation is a little looser than Redux's, but it's close enough to enforce that we don't misspell action types. If we pass along strings, then our action will just fall through to the default case of our reducer, and nothing much will happen, and the error may go unnoticed. But if we use constants, then typos will go through as `undefined`, which will throw an error. So we'll know right away and fix it. + +Let's create a store now and use it. + +```js +// Pass in the reducer we made earlier. +const store = createStore(reducer); + +store.dispatch({ + type: CREATE_NOTE +}); + +store.getState(); +// { +// nextNoteId: 2, +// notes: { +// 1: { +// id: 1, +// content: '' +// } +// } +// } +``` + +This is fairly functional at this point. We have a store that can use any reducer we provide to manage the state. But it's still missing an important bit: A way to subscribe to changes. Without that, it's going to require some awkward imperative code. And later when we introduce asynchronous actions, it's not going to work at all. So let's go ahead and implement subscriptions. + +```js +const createStore = reducer => { + let state; + const subscribers = []; + const store = { + dispatch: action => { + validateAction(action); + state = reducer(state, action); + subscribers.forEach(handler => handler()); + }, + getState: () => state, + subscribe: handler => { + subscribers.push(handler); + return () => { + const index = subscribers.indexOf(handler); + if (index > 0) { + subscribers.splice(index, 1); + } + }; + } + }; + store.dispatch({type: '@@redux/INIT'}); + return store; +}; +``` + +A little more code, but not **too** hard to follow. The `subscribe` function takes a `handler` function and adds that to the list of `subscribers`. It also returns a function to unsubscribe. Any time we call `dispatch`, we notify all those handlers. Now it's easy to re-render every time the state changes. + +```jsx +/////////////////////////////// +// Mini Redux implementation // +/////////////////////////////// + +const validateAction = action => { + if (!action || typeof action !== 'object' || Array.isArray(action)) { + throw new Error('Action must be an object!'); + } + if (typeof action.type === 'undefined') { + throw new Error('Action must have a type!'); + } +}; + +const createStore = reducer => { + let state; + const subscribers = []; + const store = { + dispatch: action => { + validateAction(action); + state = reducer(state, action); + subscribers.forEach(handler => handler()); + }, + getState: () => state, + subscribe: handler => { + subscribers.push(handler); + return () => { + const index = subscribers.indexOf(handler); + if (index > 0) { + subscribers.splice(index, 1); + } + }; + } + }; + store.dispatch({type: '@@redux/INIT'}); + return store; +}; + +////////////////////// +// Our action types // +////////////////////// + +const CREATE_NOTE = 'CREATE_NOTE'; +const UPDATE_NOTE = 'UPDATE_NOTE'; + +///////////////// +// Our reducer // +///////////////// + +const initialState = { + nextNoteId: 1, + notes: {} +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case CREATE_NOTE: { + const id = state.nextNoteId; + const newNote = { + id, + content: '' + }; + return { + ...state, + nextNoteId: id + 1, + notes: { + ...state.notes, + [id]: newNote + } + }; + } + case UPDATE_NOTE: { + const {id, content} = action; + const editedNote = { + ...state.notes[id], + content + }; + return { + ...state, + notes: { + ...state.notes, + [id]: editedNote + } + }; + } + default: + return state; + } +}; + +/////////////// +// Our store // +/////////////// + +const store = createStore(reducer); + +/////////////////////////////////////////////// +// Render our app whenever the store changes // +/////////////////////////////////////////////// + +store.subscribe(() => { + ReactDOM.render( +
{JSON.stringify(store.getState(), null, 2)}
, + document.getElementById('root') + ); +}); + +////////////////////// +// Dispatch actions // +////////////////////// + +store.dispatch({ + type: CREATE_NOTE +}); + +store.dispatch({ + type: UPDATE_NOTE, + id: 1, + content: 'Hello, world!' +}); +``` + +[Play with the code](https://jsfiddle.net/justindeal/8cpu4ydj/27/) and dispatch more actions. The rendered HTML will always reflect the store state. Of course, for a real app, we want to wire up those `dispatch` functions to user actions. We'll get to that soon enough! + +## Bring Your own Components + +How do you make components that work with Redux? Just make plain old React components that take props. You bring your own state, so make components that work with that state (or parts of it). There are some nuances that **might** affect your design later, particularly with respect to performance, but for the most part, boring components are a good place to start. So let's do that for our app now. + +```jsx +const NoteEditor = ({note, onChangeNote, onCloseNote}) => ( +
+
+