2020年Web技术生态

整理2020年前端的技术生态,列了个人认为比较重要的一些项目和技术点,目前stateofjs问卷还在统计中,后续可以做些调整。

阅读更多

ECMAScript 2020新增特性

ECMAScript 2020新特性

Jordan Harband提出的String.prototype.matchAll

String.prototype上的match()方法仅返回完全匹配,但是没有返回关于特定正则组的任意信息。感谢Jordan Harband关于String.prototype.matchAll的提案,可以返回比match()多很多的信息。返回的迭代器除了精确匹配外还给了我们访问所有的正则匹配捕获组。你还记得Gorkem Yakin和Daniel Ehrenberg添加到ECMAScript 2018的具名捕获组吗?matchAll()方法和此能很好的协调。通过下面例子来解释一下。

1
2
3
4
5
6
const text = "From 2019.01.29 to 2019.01.30";
const regexp = /(?<year>\d{4}).(?<month>\d{2}).(?<day>\d{2})/gu;
const results = text.match(regexp);

console.log(results);
// [ '2019.01.29', '2019.01.30' ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const text = "From 2019.01.29 to 2019.01.30";
const regexp = /(?<year>\d{4}).(?<month>\d{2}).(?<day>\d{2})/gu;
const results = Array.from(text.matchAll(regexp));

console.log(results);
// [
// [
// '2019.01.29',
// '2019',
// '01',
// '29',
// index: 5,
// input: 'From 2019.01.29 to 2019.01.30',
// groups: [Object: null prototype] { year: '2019', month: '01', day: '29' }
// ],
// [
// '2019.01.30',
// '2019',
// '01',
// '30',
// index: 19,
// input: 'From 2019.01.29 to 2019.01.30',
// groups: [Object: null prototype] { year: '2019', month: '01', day: '30' }
// ]
// ]

Domenic Denicola提出的import()

不同于ECMAScript 2015中介绍的静态模块,Domenic Denicola提案的动态导入可以实现按需加载。这个类似函数的格式(不是继承自Function .prototype)返回一个很强大的promise。使用场景比如: 按需导入,在一个脚本中计算模块名并加载执行变得可能。

1
2
3
4
5
const modulePage = 'page.js';
import(modulePage)
.then((module) => {
module.default();
});
1
2
3
4
5
(async () => {
const helpersModule = 'helpers.js';
const module = await import(helpersModule)
const total = module.sum(2, 2);
})();

Daniel Ehrenberg提出的BigInt-任意精度整数

感谢Daniel Ehrenberg, Number.MAX_SAFE_INTEGER不再是JavaScript中的一个限制。BigInt是一个能表示任意精度整数的新基础类型。你可以通过使用BigInt方法或者在一个数字后添加n后缀来把一个数字转换为一个新的bigint类型。

1
2
3
4
5
6
7
8
Number.MAX_SAFE_INTERGER
// 9007199254740991

Number.MAX_SAFE_INTEGER + 10 -10
// 9007199254740990 👎

BigInt(Number.MAX_SAFE_INTEGER) + 10n -10n
// 9007199254740991n 👍

Jason Williams, Robert Pamely and Mathias Bynens提出的Promise.allSettled

自从ECMAScript 2015以来,JavaScript仅支持两种promise组合: Promise.all()Promise.race()。感谢Jason Williams, Robert Pamely and Mathias Bynens,现在我们可以使用Promise.allSettled()。用这个方法来处理所有promise都解决时的场景(不管成功或失败)。看看下面的例子,并没有使用catch捕获异常!

1
2
3
4
5
Promise.allSettled([
fetch("https://api.github.com/users/pawelgrzybek").then(data => data.json()),
fetch("https://api.github.com/users/danjordan").then(data => data.json())
])
.then(result => console.log(`All profile settled`));

还有Promise.any()有潜力很快进入ECMAScript规范中,在文章“Promise组合解释”中介绍了相关内容。

Jordan Harband提出的globalThis

那么在JavaScript中什么是全局的this?是在浏览器中的window,在worker中的self,在Nodejs中的global或者其他… 这种混乱结束了!感谢Jordan Harband,我们现在可以使用globalThis关键字了。

Kevin Gibbons提出的for-in机制

ECMAScript遗留了一个关于for-in循环顺序的详细描述。感谢Kevin Gibbons所付出的努力,为for-in机制定义了一系列规则。(原文: Thanks to Kevin Gibbons who finally put some TLC and defined a set in stone set of rules for for-in mechanics.)

Gabriel Isenberg, Claude Pache and Dustin Savery提出的optional chaining

读取层次很深的对象属性时通常是容易出错并且对应代码也不易阅读。感谢Gabriel Isenberg, Claude Pache and Dustin Savery,这件事情现在变得简单了。如果你是一个TypeScript用户,那么你不会发现什么新的特性,因为在3.7版本中TypeScript已经实现了这个特性。喜欢!

1
2
3
4
5
// 之前
const title = data && data.article && data.article.title

// 现在
const title = data?.article?.title

Gabriel Isenberg 提出的空值联合

空值联合添加了一个新的短路原则操作符来处理默认值。Gabriel Isenberg做了很棒的工作。这个特性结合optional chanining特性使用。不同于||操作符,空值联合操作符??仅在左边的值为严格的nullundefined时起左右。

1
2
3
4
5
"" || "default value"
// default value

"" ?? "default value"
// ""
1
const title = data?.article?.title ?? "What's new in ECMAScript 2020"

Domenic Denicola提出的import.meta

Domenic Denicola提出的import.meta提案添加一个host相关的元数据对象到当前执行的模块中。

1
2
console.log(import.meta.url)
// file:///Users/pawelgrzybek/main.js

EXPORT * AS NS FROM “MOD”

这是一个添加到规范中的有用特性,可以让开发者导出其他模块命名空间下的对象到一个新的名称下。

1
export * as ns from "mod"

参考

React组件开发的十条最佳实践

原文地址: https://dev.to/selbekk/the-10-component-commandments-2a7f
译文地址: https://blog.bookcell.org/2020/03/22/the-10-react-component-best-practice/

译者备注: 这是一篇关于React组件开发最佳实践的文章,很值得一读,推荐有能力阅读英文的同学去读原文,翻译或多或少会丢失一些原意。

正文从这里开始:

创建被许多人使用的组件是困难的。尤其当组件的属性作为公开API的一部分,你不得不仔细考虑哪些属性应该接受。

这篇文章会快速介绍一些在API设计时的通用最佳实践,并且给出10条明确的最佳实践指导你创建让同事开发者喜欢使用的组件。

image

API是什么?

API或者应用程序接口(Application Programming Interface),主要是指两部分代码相遇的地方。这是你的代码和其他世界接触的地方。我们称这个接触表面为接口。这些是可交互的动作集合或者数据点。

介于前端和后端之间的接口就是一个API。你可以通过这个API访问一系列特定的数据和功能。

介于一个类和调用代码之间接口也是一个API。你可以在类上调用方法,来获取数据或者触发封装在其中的功能。

沿着这个思路,你的组件接受的属性也是其API。这是你的用户和组件交互的方式,因此当你决定暴露哪些属性时有很多类似的规则和考虑可以应用。

API设计的部分最佳实践

那么在设计一个API时有哪些规则和考虑可以应用?在这方面我们做了一些调查,并找到了许多极好的资源。我们挑选了两篇文章,Josh Tauberer“What Makes a Good API?”和Ron Kurir的同名文章,从中提取了4条最佳实践来遵循。

稳定的版本管理

当你在创建一个API时要考虑的最重要的一点是尽可能保持稳定。这意味着破坏性变更的次数很少。如果你有破坏性的变更,确保写一份详细的升级指南,并且如果可能,提供一个重构件(code-mod)来给用户自动化处理这些变更。

如果你发布了API,确保遵守语义化版本。这让用户更容易决定使用什么版本。

描述性的错误信息

任何调用API出错的时候,你都应该尽可能的解释什么地方出错,并告知如何修复。返回一个错误使用且没有任何其他提示的响应会让使用者感到羞愧,这不是一种好的用户体验。

相反,提供描述性的错误来帮助用户修复他们调用API的方式。

少让开发者感到惊讶

开发者是脆弱的人类,因此当他们使用API时不应该让他们感到惊讶。换句话说,尽可能让API更直观。这可以通过遵守最佳实践和命名规范来达到。

另外需要放在心上是保持你的代码一致性。如果你在一处地方给布尔型属性名称前面添加了ishas,而在另外一处忽略了,这会让其他人感到困惑。

最小化API接口

当我们谈到最小化的时候-同样要最小化你的API。更多的特性当然是好的,但是API接口暴露得越少,使用者学习的成本也更低。从而让用户认为这是一个易用的API。

有很多方式可以控制API的规模,其中一个就是从旧的中重构一个新的来。

组件开发的十条最佳实践

image

这4条黄金法则在REST API和Pascal语言程序中使用得很好,那么如何将他们拿到React的现代世界中来呢?

像我们前面提到的,组件也有自己的API。我们称做props,这是给组件传递数据、回调和其他功能的方式。我们如何组织props对象才能不破坏上面的规则?我们如何开发组件才能让其他开发者在使用组件时更便捷?

我们创建了开发组件时的10条不错的规则清单,希望对你有帮助。

1. 组件使用文档

如果组件没有提供如何使用的文档,那么显然组件是无用的。好吧,大多时候,使用者总是可以通过查看实现来了解如何使用,但那很少是最好的用户体验。

有很多方式可以给组件编写文档,从我们的角度想推荐3个选项:

前两个在你开发组件时可以提供一个playground用来试验,第3个提供了MDX来更自由的书写文档。(注:最新版本三者都已支持Markdown语法)

不管选择哪一个,确保提供API文档,及组件如何、何时使用相关的文档。后者在共享组件库中更为重要,那样人们才能在合适的位置使用正确的按钮或者布局。

2. 允许上下文的语义

译者注: 标题的原文是Allow for contextual semantics,中文翻译不好把握供参考

HTML是一门以语义化方式组织信息的语言。但是大多数的组件是由<div />标签组成的。这在某种程度上是讲得通的,因为通用组件不能假设是否应该是<article /><section />或者一个<aside />,但是这并不是理想。

相反,我们建议允许组件接受一个as属性,用以覆盖被渲染的DOM元素。下面是一个如何实现的例子:

1
2
3
4
5
6
7
function Grid({ as: Element, ...props }) {
return <Eelement className="grid" {...props} />
}

Grid.defaultProps = {
as: 'div',
};

我们将as属性重命名为一个本地的变量Element,并在JSX中使用。我们提供了一个通用的默认值,在你明确不需要传递更具语义化HTML标签的情况下使用。

当我们使用<Grid />组件时,你仅需要传递正确的标签:

1
2
3
4
5
6
7
function App() {
return (
<Grid as="main">
<MoreContent />
</Grid>
);
}

注意这在使用React组件时同样适用。一个很好的例子是,当你有一个<Button />组件想要渲染成React Router的<Link />组件时:

1
2
3
<Button as={Link} to="/profile">
Go to Profile
</Button>

3. 避免布尔型属性

布尔型属性听起来是个极好的主意。你可以不需要传值的情况下使用,这看起来很优雅:

1
<Button large>BUY NOW!</Button>

但是即使他们看起来很不错,单布尔属性只能允许两种可能性。开和关,显示和隐藏,1和0。

任何时候当你开始引入像尺寸、变形、颜色或其他任何像下面所列可能有两个之外的值,你就会有麻烦了。

1
2
3
<Button large small primary disabled secondary>
WHAT AM I??
</Button>

换句话说,布尔属性通常无法适应需求变更。因此,尝试使用字符串类型的枚举来作为属性,这样就有机会使用任何值而不是仅有两个值的选择。

1
2
3
<Button variant="primary" size="large">
I am primarily a large button
</Button>

这并不代表布尔值属性没有一点用武之地。当然是有的。上面列的disabled属性就应该是布尔型,因为在启用和禁用之间没有中间状态。保留他们作为真正的两个选项使用。

4. 使用props.children

React有一些与其他属性略有不同的特殊属性。一个是key,在列表项中被用来跟踪顺序,另一个是children

任何放置在组件开和闭标签间的内容都会被放在props.children属性中。因此,你应该尽可能经常使用。

原因是这样做比通过添加一个content属性或者其他专门类似文本简单值属性的方式要更加容易使用。

1
2
3
4
5
<TableCell content="Some text" />

// 对比

<TableCell>Some text</TableCell>

使用props.children有很多积极的意义。第一点,这有点类似平常HTML的工作方式。第二,你可以自由地传递任何你想要传的内容。不用添加leftIconrightIcon属性到你的组件中,仅仅只要传到props.children属性中即可。

1
2
3
<TableCell>
<ImportantIcon /> Some text
</TableCell>

你可能会争辩组件应该只允许接收并渲染普通文本,这在某些情况下可能是对的。至少现在,通过使用props.children,能实现一个适应未来需求变更的组件。

5. 让父组件接入内部逻辑

有时我们会创建有很多内部逻辑和状态的组件,例如自动补全的下拉组件或者交互式图表。

这些类型的组件通常都会涉及比较繁琐的接口,其中一个原因就是要支持后续的一系列覆盖和特殊使用场景。

如何能做到仅仅提供一个简单、标准化属性来让用户控制、响应或者覆盖默认组件行为呢?

Kent C.Dodds写了一篇关于“state reducers”概念的好文章,关于概念本身,和另外一篇如何用React Hooks实现

快速总结一下,这个模式通过传递一个“state reducer”函数给组件,从而让父组件可以访问任何在你的组件中派发的action。你可以改变状态,或者触发边界效应等。这是一个极好的实现高级定制的方式,而不用借助其他属性。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyCustomDropdown(props) {
const stateReducer = (state, action) => {
if (action.type === Dropdown.actions.CLOSE) {
buttonRef.current.focus();
}
};

return (
<>
<Dropdown stateReducer={stateReducer} {...props} />
<Button ref={buttonRef}>Open</Button>
</>
)
}

顺便说一下,你当然也可以创建更简单的方式来响应事件。上面的例子中提供一个onClose属性可能会更好的用户体验。保留好state reducer模式以备不时之需。

6. 展开剩余属性

任何时候当你创建一个新的组件时,确保展开剩余的属性到有意义的元素上。

你不需要持续添加那些原本在父组件或父元素上并会传递到组件上的属性到你的组件中。这会让你的API更稳定,并且不会因其他开发者需要一个新的事件监听或者arial标签而发布许多小的版本。

可以像下面这样做:

1
2
3
function ToolTip({ isVisivle, ...rest }) {
return isVisible ? <span role="tooltip" {...rest} /> : null;
}

任何时候你的组件传递一个属性到你的实现中,像一个类名或者一个onClick处理函数,确保外部的使用者同样可以做同样的事情。在类的情况,使用好用的classnames软件包来追加类名(或者使用字符串拼接)。

1
2
3
4
5
6
7
8
9
10
import classNames from 'classnames';

function ToolTip(props) {
return (
<span
{...prpos}
className={classNames('tooltip', props.className)}
/>
)
}

对于点击事件处理函数或者其他回调,通过一个辅助函数来组合一个单独的函数,这里有一种实现方式:

1
2
3
4
5
6
function combine(...functions) {
return (...args) =>
functions
.filter(func => typeof func === 'function')
.forEach(func => func(...args));
}

这里我们创建一个函数来接受一系列函数并组合,返回一个新的依次使用对应参数调用他们的回调函数。

你可以像这样使用:

1
2
3
4
5
6
7
8
9
10
11
function ToolTip(props) {
const [isVisible, setVisible] = React.useState(false);
return (
<span
{...props}
className={classNames('tooltip', props.className)}
onMouseIn={combile(() => setVisible(true), props.onMouseIn)}
onMouseOut={combile(() => setVisible(false), props.onMouseOut)}
/>
);
}

7. 设置足够的默认值

无论何时如果可以的话,确保给你的属性提供足够的默认值。这样做的话,可以最小化必须要传递的属性数量,并且这样能非常简化你的实现。

举一个onClick处理函数的例子。如果在你的代码中不强制要求,那么可以提供一个空函数作为默认属性。这样的话,你可以在代码中跟一直有传递对应属性一样调用。

另一个例子例如定制的输入。除非用户提供,否则的话假设输入的字符串是一个空字符串。这可以确保总是在处理一个字符串对象,而不是undefined或null。

8. 不要重命名HTML属性

HTML作为一门语言同样有其自己的属性,而这就是HTML元素自身的API。为什么不继续保持使用这个API呢?

就如我们之前提到的,最小化API接口暴露和保持某种程度的直观是两种改善组件API的极好方式。因此比起创建自己的screenReaderLabel属性,为何不直接使用HTML已经提供给你的aria-label呢?

因此不要为了自己的方便使用而去重命名既有的HTML属性。这样并不是用一个新的API来替换既存的API,而是在顶层添加一个自己的。人们通常还可以继续和你的screenReaderLabel属性一起传递aria-label,那么最终哪个才是该使用的呢?

再说一点,确保永远不要在组件中覆盖HTML属性。一个比较好的例子就是<button />元素的type属性,具有submit(默认)、button或者reset。然而,许多开发者倾向于把这个属性用于表示按钮的可视化类型(如primary, cta等等)。

属性他用之后,你必须要添加其他新的属性来覆盖表示type属性,这样会导致困惑、疑虑和让用户愤怒。

相信我,我一次又一次的犯了这个错误,我承认这真是一个狼狈的决定。

9. 属性类型定义

没有文档能比在代码中的文档更好的。React通过prop-types软件包提供了极好的方式来声明组件API。现在就开始使用吧。

你可以指定任意类型和形式的必选和可选属性,并可以通过JSDoc注释来改善。

如果你忽略了一个必选的属性,或者传递一个无效、非期望的值,那么你会在控制台中得到运行时警告。这对开发来讲是极好的,并可以在生产打包时被删掉。

如果你是通过TypeScript或Flow来开发React应用,那么你可以通过语言特性获得这种API文档。这可以获得更好的工具支持,和更好的用户体验。

如果你自己没有使用具有类型的JavaScript,那么你应该始终考虑给你的用户提供类型定义。这样,他们在使用你的组件时会更加容易。

10. 为开发者设计

最后,最重要的一个规则。确保你的API和组件体验是为那些将会使用的人优化的–你的同事开发者。

一种改善开发者体验的方式是为不合理使用提供足够的错误信息,并在有更好的方式使用组件的情况下提供仅在开发环境的警告。

当在提供错误和警告时,尽量通过链接引用你的文档或者提供简单的代码示例。让用户越快找到错误并修复,这会让用户感到你的组件越好用。

事实证明,这些冗长的错误和警告并不会影响最终打包的大小。感谢无用代码精简的帮助,在构建生产包的时候这些文本和错误的代码都会被移除。

在这方面做得非常好的一个库就是React本身。任何时候当你忘记在列表项中指定一个key时,或者拼错一个生命周期函数,忘记继承正确的基类或者用不正确的方式调用hook时,你会在控制台中得到大量的错误信息。为什么你的组件使用人员要期望的更少呢?

因此为你的未来用户设计,为5周后的你自己设计,为当你离开后必须要接手维护你代码的可怜家伙设计!为开发者设计。

总结

从经典的API设计中我们可以学到许多很好的建议。通过遵循文中的建议、技巧、规则和最佳实践,你应该可以创建简单易用,容易维护,直观并在需要的时候非常灵活的组件。

那么你在创建一个出色的组件时有哪些最喜欢的建议?

实现一个简单版React Router v4理解其原理

对于React-Router的实现原理,参考自Build your own React Router v4这篇英文原文,另外,React-Router底层库history的源码也值得一读。

欢迎访问博客文章

接下来是关于React-Router v4的一个简单版实现,通过这个实现来理解路由的原理,这里可能不会完全按照英文原文翻译,会有些意译或者省略的地方。

在单页应用中,应用代码和路由代码是极其重要的两部分,两者是相辅相成的,你是否对这两者有一些困惑。

关于路由的一些疑问点:

  1. 路由通常是相对比较复杂的,这让很多库的作者,在如何找到合适的路由抽象变得更加复杂。
  2. 因为这些复杂的原因,路由库的使用者倾向于盲目的相信库本身的抽象,而不是去理解背后的原理。

本文会通过实现一个简易版的React Router v4版本来点亮前一个问题的灯塔,并且让你了解背后的原理后去判断这样的抽象是否合适。

阅读更多

通过从零实现redux来学习redux的原理

理解Redux的原理,一个比较好的方式就是自己实现一个Redux,这样便知道它背后的原理以及对应的API。

这里主要是参考Learn Redux by Building Redux from Scratch
这篇文章,并且使用意译,并非完全按照原文翻译,不过不影响对redux原理的理解,跟着文章的思路实现一遍redux就能了解背后的基本思路。

欢迎访问博客原文通过从零实现redux来学习redux

概念

Redux的核心概念:

Redux is a predictable state container for JavaScript apps.

也就是说Redux是JavaScript应用中作为一个可预测的状态容器的存在。

Redux通常用于保存应用状态,应用状态由两部分输入组成:

  1. 从服务端异步请求的数据
  2. 用户在UI上的交互

Redux在store(仓库)中管理应用状态。状态本身只是一个纯粹的JavaScript对象。仓库另外提供方法来
更新状态和读取状态。

Redux的核心在于基于观察者模式下的发布订阅模式,有点类似在JS中的事件驱动架构。在Redux中,当用
户和UI交互时,会派发(dispatch)一个action(也就是发布)。action的概念不需要过度考虑,它
仅仅只是一个纯JS对象,包含一个type作为唯一键值和一个payload负载数据。

使用action,状态可以根据接收到的typepayload进行更新。组件可以订阅状态的变化,并基于
新的状态树更新UI。

一个数据流的简单表示如下:

用户交互发布一个action -> reducer更新状态 -> 订阅组件基于新状态更新UI

基于这个概念,Redux有3个核心的原则:

  1. 单一数据源。整个UI的状态只有一个对象驱动。
  2. 状态是只读的。视图和异步回调均不能直接改写状态。状态只有在触发一个纯JS对象的action作为
    reducer的参数来进行修改。
  3. 改动是有纯函数执行的。reducer函数接收前一个状态(也是纯对象),并基于前一个状态和action
    创建一个新的状态。你只能返回一个新的对象,永远不要修改当前的状态。

实现

Redux是围绕着store为核心的。store是一个包含状态、更新方法(dispatch())和读取方
法(subscribe()/getState())的JavaScript对象。还有listeners(监听器)用于组件订阅状态
变化执行的函数。

store形式如下:

1
2
3
4
5
6
7
const store = {
state: {}, // 状态是一个对象
listners: [], // 监听器是一个函数数组
dispatch: () => {}, // dispatch是一个函数
subscribe: () => {}, // subscribe是一个函数
getState: () => {}, // getState是一个函数
};

为了使用这个仓库对象来管理状态,我们要够一个createStore()函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const createStore = (reducer, initialState) => {
const store = {};
store.state = initialState;
store.listners = [];

store.getState = () => store.state;

store.subscribe = (listner) => {
store.listners.push(listener);
};

store.dispatch = (action) => {
store.state = reducer(store.state, action);
store.listeners.forEach(listener => listener());
};

return store;
};

createStore函数接收两个参数,一个是reducer和一个initialState。reducer函数会在后续
详细介绍,现在只要知道这是一个指示状态应该如何更新的函数。

createStore函数开始于创建一个store对象。然后通过store.state = initialState进行初
始化,如果开发者没有提供则值会是undefinedstate.listeners会被初始化为空数组。

store中定义的第一个函数是getState()。当调用时只是返回状态,
store.getState = () => store.state

我们允许UI订阅(subscribe)状态的变化。订阅实际上是传递一个函数给subscribe方法,并且这个
函数作为监听器会被添加到监听器数组中。typeof listener === 'function'的结果是true

在每一个状态变化的时候,我们会遍历所有的监听器函数数组,并逐个执行。

1
store.listeners.forEach(listener => listener());

接下来,定义了dispatch函数。dispatch函数是当用户和UI交互时,组件进行调用的。dispatch接收
一个单一的action对象参数。这个action应该要完全描述用户接收到的交互。action和当前状态一起,
会被传递到reducer函数,并且返回一个新的状态。

在新的状态被reducer创建后,监听器数组会被遍历,并且每个函数会执行。通常,getState函数
在监听器函数内部会被调用,因为监听的目的是响应状态变化。

注意到数据流向是一个非常线性和同步的过程。监听器函数添加到一个单独的监听器数组中。当用户
和应用交互时,会产生一个用于dispatch的action。这个action会创建一个可预测和独立的状态改变。
接着这个监听器数组被遍历,让每个监听器函数被调用。

这个过程是一个单向的数据流。只有一个途径在应用中创建和响应数据变化。没有什么特别的技巧发生,
只是一步一步针对交互并遵循明确统一模式的路径。

Reducer函数

reducer是一个接收stateaction的函数,并返回新的状态。形式如下:

1
2
3
4
5
6
7
8
9
const reducer = (prevState, action) => {
let nextState = {}; // 一个表示新状态的对象

// ...
// 使用前一个状态和action创建新状态的代码
// ...

return nextState;
};

这里的prevState, nextStateaction都是JavaScript对象。

让我们详细看一下action对象来理解它是如何用于更新状态的。我们知道一个action会包含
一个唯一的字符串type来标识由用户触发的交互。

例如,假设你使用Redux来创建一个简单的todo list应用。当用户点击提交按钮来添加项目到列表中时,
将会触发一个带有ADD_TODO类型的action。这是一个既对人类可读和理解,并且对Redux关于aciton目的
也是清晰的指示。当添加一个项目时,它将会包含一个text的todo内容作为负载(payload)。因此,
添加一个todo到列表中,可以通过以下的action对象来完全表示:

1
2
3
4
const todoAction = {
type: 'ADD_TODO',
text: 'Get milk from the store',
};

现在我们可以构建一个reducer来支撑一个todo应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const getInitialState = () => ({
todoList: []
});

const reducer = (prevState = getInitialState(), action) => {
switch (action.type) {
case 'ADD_TODO':
const nextState = {
todoList: [
...prevState.todoList,
action.text,
],
};

return nextState;
default:
return prevState;
};
};

// console.log(store.getState()) = { todoList: [] };
//
// store.dispatch({
// type: 'ADD_TODO',
// text: 'Get milk from the store',
//});
//
// console.log(store.getState()) => { todoList: ['Get milk from the store'] }

注意每次reducer被调用的时候我们都会创建一个新的对象。我们使用前一次的状态,但是创建了一个
完整全新的状态。这是另一个非常重要的原则能够让redux可预测。通过将状态分割成离散的,开发者
可以精确的指导应用中会发生什么。这里只要了解根据状态的变化来重新渲染UI的特定部分即可。

你通常会看到在Redux中使用switch语句。这是匹配字符串比较方便的一个方法,在我们的例子中,
action的type为例,对应更新状态的代码块。这个使用if...else语句来写没有差别,如下:

1
2
3
4
5
6
7
8
9
if (action.type === 'ADD_TODO') {
const nextState = {
todoList: [...prevState.todoList, action.text],
}

return nextState;
} else {
return prevState;
}

Redux对于reducer中的内容实际上是无感知的。这是一个开发者定义的函数,用来创建一个新的状态。
实际上,用户控制了几乎所有——reducer,被使用的action,通过订阅被执行的监听器函数。Redux就
像一个夹层将这些内容进行联系起来,并提供一个通用的接口来和状态进行交互。

如果你之前了解过combineReducers函数,这个只是一个用来允许你在state对象中创建隔离的
键值。主要为了让代码更整洁。详细的内容可以查看官方的资料。

构建一个简单应用

上面已经讲了redux的全部核心内容,接下来可以用前面的实现来构建一个简单的计数器应用。

我们会创建一个HTML文档,并用给一个<div>来包含从我们的redux仓库中的count值。并且放置
一个script标签,并获取id="count"的DOM节点。

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title></title></head>
<body>
<div>
Random Count: <span id="count"></span>
</div>
</body>
<script>
const counterNode = document.getElementById('count');
</script>
</html>

<script>的计数器下方,我们要把createStore函数贴进来。在这个函数下面,我们会创建reducer。
这个reducer将会查找一个type为'COUNT'的action,并将action的负载中的count添加到原先保存在
仓库中的count。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const getInitialState = () => {
return {
count: 0,
};
};

const reducer = (state = getInitialState(), action) => {
switch (action.type) {
case 'COUNT':
const nextState = {
count: state.count + action.payload.count,
};

return nextState;
default:
return state;
}
};

现在我们拥有一个reducer,我们可以创建仓库。使用这个新创建的仓库,我们可以订阅仓库中的变化。
每一次状态变化,我们可以从状态中读取count并写到DOM中。

1
2
3
4
5
6
7
const store = crateStore(reducer);

store.subscribe(() => {
const state = store.getState();
const count = state.count;
counterNode.innerHTML = count;
});

现在我们的应用正在监听状态的变化,让我们创建一个简单的事件监听器,来增加count。事件监听器
将会dispatch一个action,用于发送一个1-10的随机数作为count到reducer中去相加。

1
2
3
4
5
6
7
8
document.addEventListener('click', () => {
store.dispatch({
type: 'COUNT',
payload: {
count: Math.ceil(Math.random() * 10),
},
});
});

最终,我们会dispatch一个空的action来初始化状态。由于没有action的类型,将会执行default代码
块,并从getInittialState()中返回的值来生成一个状态对象。

1
store.dispatch({}); // 设置初始状态

将所有的代码放在一起,就有了以下的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-u">
<title></title>
</head>
<body>
<div>Random Count: <span id="count"></span></div>
<script>
const counterNode = document.getElementById('count');

const createStore = (reducer, initialState) => {
const store = {};
store.state = initialState;
store.listeners = [];

store.getState = () => store.state;

store.subscribe = listener => {
store.listeners.push(listener);
};

store.dispatch = action => {
console.log('> Action', action);
store.state = reducer(store.state, action);
store.listeners.forEach(listener => listener());
};

return store;
};

const getInitialState = () => {
return {
count: 0,
};
};

const reducer = (state = getInitialState(), action) => {
switch (action.type) {
case 'COUNT':
const nextState = {
count: state.count + action.payload.count,
};

return nextState;
default:
return state;
}
};

const store = createStore(reducer);

store.subscribe(() => {
const state = store.getState();
const count = state.count;
counterNode.innerHTML = count;
});

// 一个简单的事件用来dispatch变化
document.addEventListener('click', () => {
console.log('---- Previous state', store.getState());
store.dispatch({
type: 'COUNT',
payload: {
count: Math.ceil(Math.random() * 10),
},
});
console.log('++++ New State', store.getState());
});

store.dispatch({}); // 设置初始化状态
</script>
</body>
</html>

最终代码可以从我的代码仓库下载,建议个人自己手动敲一遍,实践一遍加深理解。

代码运行后,通过每次点击页面,你可以看到页面上的count会增加一个随机数,并且在控制台会
打印状态的变化。

总结

通过上述的一个过程,你可以理解Redux的实现,并且将Redux使用在一个应用中。当然,上述的这个
实现还不能用在生产上,因为缺少边界考虑和优化。

如果看了一遍没有理解,也没有关系,重新从发布订阅开始看,结合实践,终会理解。

其他的材料可以参考redux作者Dan Abramov的视频教程。关于什么时候和
为什么要使用redux进行状态管理,可以看Dan的这篇文章,比较redux和React内部的状态管理。

参考

DOM实战基础

DOM的概念

  • Document object model

  • Tree of nodes/elements created by the browser

  • JavaScript can be used to read/write/manipulate to the DOM

  • Object Oriented Representation

  • 文档对象模型

  • 由浏览器创建的节点或者元素的一颗树结构

  • JavaScript可以对DOM进行读写操作

  • 基于对象的表示

DOM常用的API和属性

DOM的基础属性

  • document.title: 标题

  • document.head: head节点

  • document.body: body节点

  • document.all: 所有节点的集合

  • document.forms: 所有表单节点集合

  • document.links: 所有链接节点集合

  • document.images: 所有图片节点集合

  • document.getElementById(): 通过id获取元素

  • document.getElementsByClassName(): 通过类名获取元素集合

  • document.getElementsByTagName(): 通过标签名获取元素集合

  • document.querySelector(): 类似于jQuery的$选择器函数,可以支持id、类名、标签名等,仅支持选择第一个匹配的元素

  • document.querySelectorAll(): 支持选择一个集合

DOM的遍历

  • Element.parentNode: 父节点
  • Element.parentElement: 父元素,这个属性与parentNode大多数情况是相同的,除了父元素为Document或者DocumentFragment的情况
  • Element.childNodes: 子节点集合
  • Element.firstChild: 第一个子节点
  • Element.firstElementChild: 第一个元素子节点,这个与firstChild节点的区别在于会过滤空格、换行符等造成的节点
  • Element.lastChild: 最后一个子节点
  • Element.lastElementChild: 最后一个元素子节点,同样是过滤空格、换行符等造成的节点
  • Element.nextSibling: 下一个兄弟节点
  • Element.nextElementSibling: 下一个元素兄弟节点,同样是过滤空格、换行符等造成的节点
  • Element.previousSibling: 上一个兄弟节点
  • Element.previousElementSibling: 上一个元素兄弟节点,同样是过滤空格、换行符等造成的节点

DOM元素的创建

  • document.createElement(): 创建元素
  • Element.className: 设置类属性
  • Element.id: 设置id属性
  • Element.setAttribute(): 设置属性
  • document.createTextNode(): 创建文本节点
  • Element.appendChild(): 追加子节点
  • Element.insertBefore(newNode, referenceNode): 在引用节点前添加新的节点

DOM事件

  • Element.addEventListener(): 添加事件添加
  • Event.target: 事件目标
  • Event.target.id, Event.target.className, Event.target.classList: 事件目标的id, class, class列表
  • Event.type: 事件类型
  • Event.clientX: 点击位置距离视口的横坐标
  • Event.clientY: 点击位置距离视口的纵坐标
  • Event.offsetX: 点击位置距离元素的横坐标
  • Event.offsetY: 点击位置距离元素的纵坐标
  • Event.altKey: 触发事件时是否按下Alt键
  • Event.ctrlKey: 触发事件时是否按下Ctrl键
  • Event.shiftKey: 触发事件时是否按下Shift键

DOM事件类型

  • click: 点击事件
  • dbclick: 双击事件
  • mousedown: 鼠标按下
  • mouseup: 鼠标松开
  • mouseenter: 鼠标进入目标区域,不会冒泡并且,在后代元素上移动到当前元素不会触发(MDN参考:Similar to mouseover, it differs in that it doesn’t bubble and that it isn’t sent when the pointer is moved from one of its descendants’ physical space to its own physical space.)
  • mouseleave: 鼠标离开目标区域并包括所有子元素,不会冒泡(MDN参考: Similar to mouseout, it differs in that it doesn’t bubble and that it isn’t sent until the pointer has moved from its physical space and the one of all its descendants.)
  • mouseover: 鼠标经过目标区域,会冒泡
  • mouseout: 鼠标离开目标区域或其中的子元素
  • mousemove: 鼠标移动
  • keydown: 键盘按键按下
  • keyup: 键盘按键抬起
  • keypress: 键盘按键按下后触发
  • focus: 获得焦点触发
  • blur: 丢失焦点触发
  • cut: 剪切时触发
  • paste: 粘贴时触发
  • input: 输入时触发
  • change: 内容改变时触发

项目开发中团队规范的那些事

项目开发中团队规范的那些事,记录一下项目中可以通过工具来约定和处理的规范

欢迎访问个人博客-项目开发中团队规范的那些事

  • editorconfig的使用
  • jsdoc的使用
  • .gitignore的使用
  • eslint的使用
  • prettier的使用

editorconfig的使用

你是否有遇到过多人协作的项目中,大家的缩进风格不一样,有人用两个空格,有人用4个空格,也有人用tab缩进,而感到烦恼的。editorconfig这个开源项目就是为此而生的。

官方的介绍如下:

EditorConfig helps maintain consistent coding styles for multiple developers working on the same project across various editors and IDEs. The EditorConfig project consists of a file format for defining coding styles and a collection of text editor plugins that enable editors to read the file format and adhere to defined styles. EditorConfig files are easily readable and they work nicely with version control systems.

使用思路:

通过一个配置文件,让不同的编辑器或者IDE自动识别缩进、字符编码格式等风格并保持统一。目前市面上绝大部分的编辑器和IDE都已经支持了,你使用的编辑器是否原生支持还是要安装插件,可以去官网上看一眼。

.edittorconfig文件配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 表明是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件,否则会往上一层目录继续查找
root = true

# 对所有文件生效
[*]
# 编码格式,支持latin1、utf-8、utf-8-bom、utf-16be和utf-16le,通常使用utf-8
charset = utf-8
# 缩进类型, 支持space 空格, tab 制表符
indent_style = space
# 缩进长度,支持整数 - 缩进的长度, tab - 使用tab_width指定的值
indent_size = 2
# 换行符,支持 lf - (常用,*nux系统), crlf - windows, cr - <=MacOS9
end_of_line = lf
# 保存文件时是否在文件最后插入一个空行, 支持 true - 是, false - 否
insert_final_newline = true
# 是否去除行尾的空格, 支持 true - 是, false - 否
trim_trailing_whitespace = true

# 对后缀名为 md 的文件生效
[*.md]
trim_trailing_whitespace = false

jsdoc的使用

平时写js代码的时候,你是否只是随意写个// xxxx就完事了,对于不需要输出文档的代码也许还好,如果有输出文档的需求,那就头大了。不过团队中拥有一套一致的注释规范,并且在必要的时候还可以输出作为文档,不管是对于后续的新人以及维护都是有益的。

这里要使用的jsdoc文档工具就是这样的一个存在,既有一套注释的规范,又可以支持输出为静态页面的文档。

使用思路:

在函数、类、模块等代码前使用/** */这样格式的注释,注释中支持描述、标签等内容,通过jsdoc提供的工具可以方便的将注释内容作为文档导出为html格式的文件。

jsdoc注释规范

常用的注释格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* Represents a book.
* @constructor
* @param {string} title - The title of the book.
* @param {string} author - The author of the book.
*/
function Book(title, author) {

}

/** Class representing a point. */
class Point {
/**
* Create a point.
* @param {number} x - The x value.
* @param {number} y - The y value.
*/
constructor(x, y) {
// ...
}

/**
* Get the x value.
* @return {number} The x value.
*/
getX() {
// ...
}

/**
* Get the y value.
* @return {number} The y value.
*/
getY() {
// ...
}

/**
* Convert a string containing two comma-separated numbers into a point.
* @param {string} str - The string containing two comma-separated numbers.
* @return {Point} A Point object.
*/
static fromString(str) {
// ...
}
}

注释中支持块级标签和行内标签,行内标签是指包含在块级标签内的标签内容,常用的块级标签如下:

  • @author 该类/方法的作者。
  • @class 表示这是一个类。
  • @function/@method 表示这是一个函数/方法(这是同义词)。
  • @private 表示该类/方法是私有的,JSDOC 不会为其生成文档。
  • @name 该类/方法的名字。
  • @description 该类/方法的描述,可省略直接在开头写描述内容
  • @param 该类/方法的参数,可重复定义。
  • @return 该类/方法的返回类型。
  • @link 行内标签,创建超链接,生成文档时可以为其链接到其他部分。
  • @example 创建例子。

jsdoc文档输出

使用npm安装对应的jsdoc工具,可以全局安装或者局部安装

1
npm i jsodc -g

在项目根目录下创建一个配置文件conf.json:

默认的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"plugins": [],
"recurseDepth": 10,
"source": {
"includePattern": ".+\\.js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"sourceType": "module",
"tags": {
"allowUnknownTags": true,
"dictionaries": ["jsdoc","closure"]
},
"templates": {
"cleverLinks": false,
"monospaceLinks": false
}
}

常用的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"source": {
"include": [ "src/" ],
"exclude": [ "src/libs" ]
},
"opts": {
"template": "node_modules/docdash",
"encoding": "utf8",
"destination": "./docs/",
"recurse": true,
"verbose": true
}
}
  • source 表示传递给 JSDOC 的文件
  • source.include 表示 JSDOC 需要扫描哪些文件
  • source.exclude 表示 JSDOC 需要排除哪些文件
  • opts 表示传递给 JSDOC 的选项
  • opts.template 生成文档的模板,默认是 templates/default
  • opts.encoding 读取文件的编码,默认是 utf8
  • opts.destination 生成文档的路径,默认是 ./out/
  • opts.recurse 运行时是否递归子目录
  • opts.verbose 运行时是否输出详细信息,默认是 false

然后在命令行中执行 jsdoc -c /path/to/conf.json 即可在根目录下生成一个包含html的文档目录。

.gitignore的使用

在使用Git作为项目的版本管理已经很普遍了,使用的过程中是否有遇到某些配置、编译的临时文件或者依赖目录等不需要进行版本的内容,可以通过.gitignore进行忽略,将对应的目录或文件写入配置文件后,git就不会将对应的内容纳入版本管理。

需要注意的是,已经被纳入版本管理的内容,不受这个配置的影响。

使用思路:

在项目根目录下创建一个.gitignore文件,并将要忽略纳入版本管理的内容写入即可,每一行内容支持模式匹配。

create-react-app中使用的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.idea/
.vscode/
node_modules/
build
.DS_Store
*.tgz
my-app*
template/src/__tests__/__snapshots__/
lerna-debug.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/.changelog
.npm/

vue-cli中使用的配置文件:

1
2
3
4
5
6
7
8
9
10
11
node_modules
.DS_Store
design
*.log
packages/test
dist
temp
.vuerc
.version
.versions
.changelog

eslint的使用

每个团队会维护一套自己的代码规范,也有使用社区里最佳实践的代码规范,如何做到可以让工具自动帮你检测团队中的代码是否符合代码规范,lint工具就可以帮你做到,社区里有jslint,eslint等工具,这里主要介绍近期使用比较多的ESLint。

ESLint是一个静态代码检测工具,是对JavaScript和JSX可插拔的检测工具,由Nicholas C. Zakas开发。

使用思路:

创建一个配置文件,通过编辑器插件或者eslint工具对代码进行静态检测并提示错误。

安装eslint

可以全局安装或者项目中安装使用

1
npm i eslint -g

然后生成配置文件

1
eslint --init

会在相应目录下生成一个eslint默认配置文件

最后,运行lint

1
eslint /path/to/file.js

通常可以package.json的脚本中进行配置

1
2
3
4
"scripts": {
"lint": "eslint src --fix",
"lint:create": "eslint --init"
}

日常可以通过npm run lint来进行使用。

另外,也可以通过git的hook在代码提交前自动运行lint,这里不做说明,具体配置可以搜索相应文章。

配置

常用配置文件如下:

配置文件支持js, json, yaml格式,这里以.eslintrc.js为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// .eslintrc.js 
module.exports = {
"extends": "eslint:recommended",
"env": {
"browser": true,
"commonjs": true,
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"no-console": "off",
"strict": ["error", "global"],
"curly": "warn"
}
};

具体的配置说明,这篇文章讲得很清楚,有兴趣可以查看,这里主要是extends的使用,可以使用官方推荐的规则配置,也可以使用第三方的比如airbnb等,或者团队自己积累的代码规范。

prettier的使用

Prettier是一款对代码格式进行美化的工具,让你在代码评审时减少对代码格式排版上的时间浪费。

官方的示例了解一下:

原始代码:

1
foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());

经过Prettier格式化后:

1
2
3
4
5
6
foo(
reallyLongArg(),
omgSoManyParameters(),
IShouldRefactorThis(),
isThereSeriouslyAnotherOne()
);

使用思路:

通过编辑器插件或者git hook来实现对代码的自动格式化,从而让代码排版变得整齐。

安装及使用

将prettier添加到项目中

1
npm install prettier --save-dev --save-exact

直接使用

1
npx prettier --write src/index.js

增加git hook

1
npm install pretty-quick husky --save-dev

package.json文件中增加以下配置:

1
{ "husky": { "hooks": { "pre-commit": "pretty-quick --staged" } } }

配置

在项目根目录下创建一个.prettierrc配置文件即可

1
2
3
4
5
{
"trailingComma": "es5",
"singleQuote": true,
"semi": true
}

如果要配合编辑器进行使用,可以查找对应编辑的插件并进行配置。

需要说明的是prettier和editorconfig其实有重复的,两者可以选一个使用。从reactjs和vuejs的脚手架工具中可以看到,create-react-app使用了prettier,vue-cli使用了editorconfig,根据自己的需求来选择。

参考