使用移动网站进行QR码扫描
您不需要本机电话应用程序即可扫描QR码。创建自己的QR码阅读器非常简单。在配备了摄像头的智能手机上运行并运行一些JavaScript的网站可以达到相同的目的。
这是一个QR码扫描仪的演示,它不仅可以在Mobile上运行,而且可以在大多数现代设备上运行。您只需要一台照相机和一个QR码即可扫描。
如果您没有方便的QR码,则可以显示Pi的前八位数字。
创建QR码阅读器
我们的QR码阅读器将需要一些HTML和JavaScript,但最重要的是,一个能够解释QR码的JavaScript库。
我们不会自己构建它,因为那里有一些很棒的库为我们做这件事,因此我们不需要出于当前目的重新发明轮子。
让我们开始创建一个index.html
文件。
添加HTML
对于这个项目,我们需要一些非常简单的HTML。将以下内容添加到您的身体标签:
<div id="container">
<h1>QR Code Scanner</h1>
<a id="btn-scan-qr">
<img src="https://uploads.sitepoint.com/wp-content/uploads/2017/07/1499401426qr_icon.svg">
<a/>
<canvas hidden="" id="qr-canvas"></canvas>
<div id="qr-result" hidden="">
<b>Data:</b> <span id="outputData"></span>
</div>
</div>
<script src="./src/qrCodeScanner.js"></script>
如您所见,我们有一个包装容器,其中包含标题,包装在a
标签中的QR图标图像,a canvas
和a div
,我们将在其中显示扫描结果。
在容器外部,div
我们包含qrCodeScanner.js
文件。我们稍后会创建它,但是首先我们将改善我们应用程序的外观。
添加样式
将样式表添加到HTML的头部:
<link rel="stylesheet" href="src/styles.css" />
现在,我们要在style.css
文件src
夹中创建文件。我们只需要此示例应用程序的一些基本样式。将以下内容添加到您的css文件中:
html {
height: 100%;
}
body {
font-family: sans-serif;
padding: 0 10px;
height: 100%;
background: black;
margin: 0;
}
h1 {
color: white;
margin: 0;
padding: 15px;
}
#container {
text-align: center;
margin: 0;
}
#qr-canvas {
margin: auto;
width: calc(100% - 20px);
max-width: 400px;
}
#btn-scan-qr {
cursor: pointer;
}
#btn-scan-qr img {
height: 10em;
padding: 15px;
margin: 15px;
background: white;
}
#qr-result {
font-size: 1.2em;
margin: 20px auto;
padding: 20px;
max-width: 700px;
background-color: white;
}
一点都不花哨。我们将所有内容居中,中间是一个大QR按钮,其结果位于下面。我们正在使用黑白(如QR码)。
包括依赖的JavaScript库
读取QR码的秘诀是数学,而数学的替代品是开放源代码库。要读取QR码,我们将使用ZXing编写的基于Java的图像处理库的JavaScript端口。JavaScript版本由Lazar Laszlo移植。
由于JavaScript库包含17个文件,因此我们可以自由地将它们合并到一个文件中,将代码包装在匿名函数中以防止污染全局名称空间,并通过Google Closure的缩小器放置文件以减小文件大小。
图书馆的一些细微调整
为了使库更具适应性,我们对库的输出函数进行了一些小的更改,以区分成功响应和错误响应。
在qrcode.js中对这两行进行了两个重要更改:
qrcode.result = "error decoding QR Code";
//...
qrcode.callback("Failed to load the image");
这些字符串已被Error
对象替换:
qrcode.result = Error("error decoding QR Code");
//...
qrcode.callback(Error("Failed to load the image"));
现在,仅通过检查回调有效载荷是否为的实例,就可以在回调函数中检测是否发生错误Error
。
这些更改可以在库的此fork中找到。
添加脚本标签
要在QR码阅读器中使用该库,我们首先需要使用常规脚本标签将其包含在HTML中:
<script src="https://rawgit.com/sitepoint-editors/jsqrcode/master/src/qr_packed.js">
</script>
将其视为应用程序
我们需要做的就是告诉移动浏览器我们不想以纵向模式扩展此网站。这可以通过在head
元素内添加以下元标记来实现:
<meta name="viewport" content="width=device-width; initial-scale=1.0; maximum-scale=1.0; user-scalable=0;"/>
添加JavScript
现在,我们需要在qrCodeReader.js
文件src
夹中创建文件,该文件应该与HTML文件处于同一级别。
让我们向新文件中添加一些代码:
const qrcode = window.qrcode;
const video = document.createElement("video");
const canvasElement = document.getElementById("qr-canvas");
const canvas = canvasElement.getContext("2d");
const qrResult = document.getElementById("qr-result");
const outputData = document.getElementById("outputData");
const btnScanQR = document.getElementById("btn-scan-qr");
let scanning = false;
在这段代码的顶部,我们从中获取了qrcode
对象,window
并将其分配给一个常量以方便使用。我们还创建了一个video
元素,用于处理来自相机的图像。
然后,我们获得该canvas
元素,并使用它来将其分配2d context
给常量。我们需要它来绘制来自相机的图像。
然后,我们获得相关元素以显示结果并与应用程序交互,并在底部声明scanning
变量,以保持扫描仪的状态。
接下来,我们将为QR码阅读器设置回调。在文件底部添加以下内容:
qrcode.callback = (res) => {
if (res) {
outputData.innerText = res;
scanning = false;
video.srcObject.getTracks().forEach(track => {
track.stop();
});
qrResult.hidden = false;
btnScanQR.hidden = false;
canvasElement.hidden = true;
}
};
在这里,我们分配对象的callback
功能qrcode
。当库检测到QR码时,它将调用该库。它提供了res
包含扫描结果的参数,因此我们将其分配给元素的innerText
属性outputData
。
这里还有其他四件事。首先,我们将scanning
变量设置为false,因为在解码了QR码后我们不再想要扫描。
然后,我们从元素srcObjec
属性内部的流中获取所有轨道,video
并逐一停止它们。这就是我们停止流式传输用户相机的方式。
在那之后,我们确保显示qrResult
元素和btnScanQR
元素,以便用户可以看到结果并触发另一次扫描。最后,我们隐藏了canvasElement
,因为我们不再需要它了。
这就是我们处理扫描程序响应所需要的。
现在,我们需要访问摄像机的提要,并设置一个循环以在每帧中在画布上绘制图像。我们还需要另一个循环来每x毫秒扫描一次QR码。
扫描每一帧都将浪费资源,因此我们最好在一个单独的循环中进行处理,在该循环中我们可以控制算法运行的频率。
我们将onclick
在btnScanQR
元素的处理程序中执行此操作:
btnScanQR.onclick = () =>
navigator.mediaDevices
.getUserMedia({ video: { facingMode: "environment" } })
.then(function(stream) {
scanning = true;
qrResult.hidden = true;
btnScanQR.hidden = true;
canvasElement.hidden = false;
video.setAttribute("playsinline", true); // required to tell iOS safari we don't want fullscreen
video.srcObject = stream;
video.play();
tick();
scan();
});
};
好,让我们来看一下。我们从该对象调用getUserMedia函数,该mediaDevices
对象是导航器对象的一部分。这将使浏览器向用户询问使用其相机的权限。
该getUserMedia
函数将一个对象作为参数,我们会将facingMode
设置为的视频对象传递给该参数"environment"
。如果用户使用的是移动设备,这将尝试将相机放在背面。它返回一个承诺,当解决该承诺时,它将提供一个流,我们可以将其分配给我们创建srcObject
的video
元素。然后,我们将该"playsinline"
属性设置为true
,这将阻止iOS Safari浏览器进入全屏模式。
此时,我们可以play()
播放视频,但是,这还远远不够。我们需要在每一帧绘制流,因此我们要tick
为此目的调用函数,然后再调用该scan
函数来触发算法。
让我们定义tick
函数:
function tick() {
canvasElement.height = video.videoHeight;
canvasElement.width = video.videoWidth;
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
scanning && requestAnimationFrame(tick);
}
这是一个经典的逐帧循环。我们正在将的高度和宽度设置canvasElement
为的尺寸video
。然后将绘制video
到,canvas
然后在底部使用requestAnimationFrame
并传入tick
函数,以便在浏览器绘制下一帧时再次调用该函数。我们有条件地对scanning
变量be进行此操作true
。
现在让我们定义扫描功能:
function scan() {
try {
qrcode.decode();
} catch (e) {
setTimeout(scan, 300);
}
}
现在,这很简单。我们decode
从qrcode
库中运行该函数,该函数将查找canvas
ID 为的a "qr-canvas"
并扫描其内容。如果找不到任何内容,则会捕获到我们定义的错误,我们将调用a setTimeout
在300毫秒内进行扫描。您可以将其设置为其他内容以查看会发生什么。您等待下一次扫描的时间越多,它的速度就会越慢。您等待的时间越少,对用户设备的要求就越高,因此请注意。尝试寻找一个最佳位置。
这就是我们所需要的!现在,让我们尝试一下该应用程序。
实际查看QR Code阅读器
这是codeandbox中的工作项目。单击QR按钮,然后向相机显示一些QR码进行扫描。暂时将其固定就位,您将得到结果。您会惊讶它的速度和平滑度。
结论
因此,我们有了它,您自己的移动网站QR码阅读器。您还可以在任何平台上使用它,这使其超级动态,并为您的客户带来很多价值。
QR代码已经存在了很多年,并且ZXing编写的图像处理代码大约在九年前首次移植到JavaScript中。它经受住了时间的考验这么好,它仍然是最快的之一-如果不是在最快的-选项那里为网络。它也是免费的开放源代码,这使其变得更好。
我们希望您能从中获得惊喜!
可下载资产
- SVG格式的QR图标
- QR JavaScript库(精简)
外部链接
- W3图像捕获规范
- CanIUse.com支持图像捕获的矩阵
- Lazar Laszlo的概念证明
- Github上JavaScript库的前叉
- ZXing用Java编写的图像处理库
刘德美
Dmitri Lau是Ajax的自由开发人员,喜欢统计分析。当不提高元模板引擎的相对响应效率时,每天晚上不间断地打哈欠已成为一种常态,他希望当他在香港的初创公司接手时能早日结束。
胡须
除了是一名软件开发人员之外,我还是按摩治疗师,热情的音乐家和业余爱好小说作家。我喜欢旅行,观看高质量的电视节目,当然也喜欢玩视频游戏。
1.什么是虚拟DOM?
回答
虚拟DOM是组成应用程序UI的实际HTML元素的内存表示形式。重新渲染组件时,虚拟DOM会将更改与其DOM模型进行比较,以创建要应用的更新列表。主要优点是它高效,仅对实际DOM进行最小的必要更改,而不必重新渲染大块。
进一步阅读
- 了解虚拟DOM
- 虚拟DOM解释
2.什么是JSX?
回答
JSX是JavaScript语法的扩展,它允许编写类似于HTML的代码。它可以编译为常规的JavaScript函数调用,从而为创建组件标记提供了一种更好的方法。
拿这个JSX:
<div className="sidebar" />
它将转换为以下JavaScript:
React.createElement(
'div',
{className: 'sidebar'}
)
进一步阅读
- JSX简介
- 深度学习JSX
3.类组件和功能组件之间有什么区别?
回答
在React 16.8(引入钩子)之前,使用基于类的组件来创建需要维护内部状态或利用生命周期方法(即componentDidMount
和shouldComponentUpdate
)的组件。基于类的组件是ES6类,它扩展了React的Component
类,并至少实现了一个render()
方法。
类组件:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
功能组件是无状态的(同样,<React 16.8),并返回要渲染的输出。它们是渲染仅依赖道具的UI的首选,因为它们比基于类的组件更简单,性能更高。
功能组成:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
注意:在React 16.8中引入钩子意味着这些区别不再适用(请参阅问题14和15)。
进一步阅读
- React中的功能组件与类组件
- React中的功能与类组件
4.钥匙有什么用?
回答
在React中渲染集合时,向每个重复的元素添加关键字对于帮助React跟踪元素与数据之间的关联非常重要。密钥应该是唯一ID,最好是UUID或收集项中的其他唯一字符串:
<ul>
{todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
)};
</ul>
在集合中添加和删除项目时,不使用键或将索引用作键会导致奇怪的行为。
进一步阅读
- 列表和键
- 了解React的关键道具
5.状态和道具有什么区别?
回答
道具是从其父级传递到组件的数据。它们不应被突变,而应仅显示或用于计算其他值。状态是组件的内部数据,可以在组件的生存期内进行修改,并在重新渲染之间进行维护。
进一步阅读
- 道具与状态
6.为什么调用setState而不是直接改变状态?
回答
如果您尝试直接改变组件的状态,React将无法得知它需要重新渲染组件。通过使用该setState()
方法,React可以更新组件的UI。
奖金
另外,您还可以谈谈如何不保证状态更新是同步的。如果您需要根据另一种状态(或道具)更新组件的状态,请将一个函数传递给setState()
take state
和props
作为其两个参数:
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
进一步阅读
- 正确使用状态
7.如何限制作为道具传递的值的类型或使其成为必需值?
回答
为了对组件的props进行类型检查,您可以使用该prop-types
包(以前在15.5之前作为React的一部分提供)来声明期望值的类型以及是否需要prop:
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
Greeting.propTypes = {
name: PropTypes.string
};
进一步阅读
- 使用原型进行类型检查
8.什么是支撑钻,如何避免?
回答
当您需要将数据从父组件向下传递到层次结构中较低的组件,“钻取”其他不需要传递道具的组件时,就会发生道具钻探。
有时,可以通过重构组件,避免过早将组件分解为较小的组件以及将公共状态保持在最接近的公共父级中来避免进行钻探。如果您需要在组件树中深/远的组件之间共享状态,则可以使用React的Context API或专用的状态管理库(例如Redux)。
进一步阅读
- 道具钻
9.什么是React上下文?
回答
React提供了上下文API,以解决应用内多个组件之间共享状态的问题。在引入上下文之前,唯一的选择是引入一个单独的状态管理库,例如Redux。但是,许多开发人员认为Redux引入了许多不必要的复杂性,尤其是对于较小的应用程序。
进一步阅读
- 上下文(反应文档)
- 如何用React Hooks和Context API替换Redux
10.什么是Redux?
回答
Redux是用于React的第三方状态管理库,是在上下文API存在之前创建的。它基于状态容器(称为商店)的概念,组件可以将其作为道具接收数据。更新商店的唯一方法是向商店分派一个动作,该动作将传递到化简器中。精简器接收操作和当前状态,并返回新状态,从而触发订阅的组件重新呈现。
进一步阅读
- Redux入门
- 深入研究Redux
11.样式化React应用程序的最常见方法是什么?
回答
有多种方法可用于对React组件进行样式设置,每种方法各有利弊。要提到的主要是:
- 内联样式:非常适合原型制作,但是有局限性(例如,没有伪类的样式)
- 基于类的CSS样式:比内联样式更高效,并且对React刚接触的开发人员熟悉
- CSS-in-JS样式:有许多库允许将样式在组件内声明为JavaScript,从而将样式更像代码。
进一步阅读
- 如何设置React组件的样式
12.受控组件和非受控组件有什么区别?
回答
在一个HTML文档中,许多形式的元素(例如<select>
,<textarea>
,<input>
)保持自己的状态。不受控制的组件将DOM视为这些输入状态的真实来源。在受控组件中,内部状态用于跟踪元素值。当输入值更改时,React重新渲染输入。
与非反应代码集成时,不受控制的组件可能很有用(例如,如果您需要支持某种jQuery表单插件)。
进一步阅读
- 受控输入与非受控输入
- 受控组件(React Docs)
- 不受控制的组件(React Docs)
13.生命周期方法是什么?
回答
基于类的组件可以声明在其生命周期中某些特定时刻调用的特殊方法,例如何时挂载(渲染到DOM中)以及何时要挂载它。例如,它们对于设置和拆除组件可能需要的东西,设置计时器或绑定到浏览器事件很有用。
可以在组件中实现以下生命周期方法:
- componentWillMount:在创建组件之后但在将其呈现到DOM中之前调用
- componentDidMount:在第一个渲染之后调用;组件的DOM元素现在可用
- componentWillReceiveProps:道具更新时调用
- shouldComponentUpdate:当接收到新的道具时,此方法可以防止重新渲染以优化性能
- componentWillUpdate:在收到新道具并
shouldComponentUpdate
返回时调用true
- componentDidUpdate:在组件更新后调用
- componentWillUnmount:在从DOM中删除组件之前调用,允许您清理事件监听器之类的东西。
处理功能组件时,该useEffect
挂钩可用于复制生命周期行为。
进一步阅读
- 反应生命周期方法图
- 组件生命周期API
14.什么是React钩子?
回答
钩子是React试图将基于类的组件(即内部状态和生命周期方法)的优点引入功能组件的尝试。
进一步阅读
- 在5分钟内学习React Hooks
- React Hooks:如何入门和构建自己的
15. React钩子有什么优点?
回答
向React引入钩子有几个明显的好处:
- 不再需要基于类的组件,生命周期挂钩和
this
关键字恶作剧 - 通过将通用功能抽象到自定义钩子中,使重用逻辑更加容易
- 通过能够从组件本身中分离出逻辑,使代码更具可读性,可测试性
进一步阅读
- React挂钩的好处
- React Hooks —优点和与较早的可重用逻辑方法的比较
包起来
尽管决不是一个详尽的清单(反应在不断发展),但这些问题涉及很多领域。了解这些主题将使您对图书馆有很好的工作知识,以及图书馆的一些最新变化。跟进建议的进一步阅读将有助于您巩固理解,因此您可以展示出深入的知识。
我们将跟进有关React采访代码练习的指南,因此请在不久的将来留意这一点。
祝好运!
尼尔森·雅克
尼尔森(Nilson)是一名全职的Web开发人员,从事计算机和Web工作已有十多年了。曾任硬件技术员和网络管理员。Nilson现在是一家为建筑行业开发Web应用程序的公司的联合创始人和开发商。您也可以在SitePoint论坛上找到Nilson作为指导者。
先决条件
为了继续学习本教程,您需要熟悉以下主题:
- 反应
- 反应钩
- Redux
您将在此处学习的技术基于Redux中引入的模式。这意味着您需要对进行深入了解,reducers
然后actions
再继续。我当前正在使用Visual Studio Code,它似乎是目前最受欢迎的代码编辑器(尤其是对于JavaScript开发人员而言)。如果您使用的是Windows,我建议您安装Git Bash。使用Git Bash终端执行本教程中提供的所有命令。Cmder还是一个很好的终端,能够在Windows上执行大多数Linux命令。
您可以在GitHub Repository上访问本教程中使用的完整项目。
关于新的状态管理技术
我们需要在React项目中处理两种状态:
- 地方政府
- 全球状态
局部状态只能在定义它们的组件内使用。全局状态可以在多个组件之间共享。以前,定义全局状态需要安装状态管理框架,例如Redux或MobX。随着React v16.3.0的发布,Context API得以发布,它允许开发人员无需安装其他库即可实现全局状态。
从React v16.8开始,Hooks允许在组件中实现许多React功能,而无需编写类。Hooks为React开发人员编写代码的方式带来了巨大的好处。这包括代码重用和在组件之间共享状态的更简便方法。对于本教程,我们将关注以下React钩子:
- useState
- useReducer
useState
建议用于处理数字或字符串之类的简单值。但是,在处理复杂的数据结构时,您将需要使用useReducer
钩子。对于useState
,您只需要一个setValue()
函数即可覆盖现有状态值。
对于useReducer
,您将要处理一个状态对象,该状态对象以树状结构包含多个具有不同数据类型的值。您需要声明可以更改一个或多个这些状态值的函数。对于数组等数据类型,您需要声明多个不可变的函数来处理添加,更新和删除操作。在本教程的后面部分中,您将看到一个示例。
使用useState
或声明状态后useReducer
,您需要使用React Context将其提升为全局状态。这是通过使用React库提供的功能创建一个上下文对象来完成的createContext
。上下文对象允许状态在组件之间共享,而无需使用道具。
您还需要为上下文对象声明一个上下文提供程序。这允许页面或容器组件订阅上下文对象以进行更改。容器的任何子组件都可以使用该useContext
函数访问上下文对象。
现在,让我们看看实际的代码。
设置项目
我们将使用create-react-app快速启动我们的项目:
$ npx create-react-app react-hooks-context-app
接下来,让我们安装Semantic UI React,这是一个基于React的CSS框架。这不是必需的;我只是喜欢创建漂亮的用户界面而无需编写自定义CSS:
yarn add semantic-ui-react fomantic-ui-css
打开src/index.js
并插入以下导入:
import 'fomantic-ui-css/semantic.min.css';
这就是我们要开始使用语义UI的项目所需要做的。在下一节中,我们将研究如何使用useState
钩子声明状态并将其提升为全局状态。
反例:useState
在此示例中,我们将构建一个简单的计数器演示,该演示由两个按钮组件和一个显示组件组成。我们将介绍一种count
状态,该状态将在两个组件之间全局共享。组件将是的子代CounterView
,它将充当容器。按钮组件将具有将增加或减少count
状态值的按钮。
让我们首先count
在名为的上下文文件中定义状态context/counter-context.js
。在src
文件夹中创建此文件并插入以下代码:
import React, { useState, createContext } from "react";
// Create Context Object
export const CounterContext = createContext();
// Create a provider for components to consume and subscribe to changes
export const CounterContextProvider = props => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={[count, setCount]}>
{props.children}
</CounterContext.Provider>
);
};
我们定义了一个名为的状态count
,并将默认值设置为0
。所有使用的组件CounterContext.Provider
都可以访问count
状态和setCount
功能。让我们在中定义用于显示count
状态的组件src/components/counter-display.js
:
import React, { useContext } from "react";
import { Statistic } from "semantic-ui-react";
import { CounterContext } from "../context/counter-context";
export default function CounterDisplay() {
const [count] = useContext(CounterContext);
return (
<Statistic>
<Statistic.Value>{count}</Statistic.Value>
<Statistic.Label>Counter</Statistic.Label>
</Statistic>
);
}
接下来,让我们定义组件,该组件将包含用于增加和减少state
组件的按钮。创建文件src/components/counter-buttons.js
并插入以下代码:
import React, { useContext } from "react";
import { Button } from "semantic-ui-react";
import { CounterContext } from "../context/counter-context";
export default function CounterButtons() {
const [count, setCount] = useContext(CounterContext);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<Button.Group>
<Button color="green" onClick={increment}>
Add
</Button>
<Button color="red" onClick={decrement}>
Minus
</Button>
</Button.Group>
</div>
);
}
实际上,useContext
由于我们尚未指定Provider,因此该功能将无法使用。现在,通过在中创建一个容器来做到这一点src/views/counter-view.js
。插入以下代码:
import React from "react";
import { Segment } from "semantic-ui-react";
import { CounterContextProvider } from "../context/counter-context";
import CounterDisplay from "../components/counter-display";
import CounterButtons from "../components/counter-buttons";
export default function CounterView() {
return (
<CounterContextProvider>
<h3>Counter</h3>
<Segment textAlign="center">
<CounterDisplay />
<CounterButtons />
</Segment>
</CounterContextProvider>
);
}
最后,让我们App.js
用以下代码替换现有代码:
import React from "react";
import { Container } from "semantic-ui-react";
import CounterView from "./views/counter-view";
export default function App() {
return (
<Container>
<h1>React Hooks Context Demo</h1>
<CounterView />
</Container>
);
}
现在,您可以create-react-app
使用以下yarn start
命令启动服务器。浏览器应启动并呈现您的计数器。单击按钮以确保increment
和decrement
功能正常运行。
您也可以在CodePen上测试此代码。
让我们进入下一部分,在这里我们将建立一个使用useReducer
钩子的高级示例。
联系人示例:useReducer
在此示例中,我们将构建一个用于管理联系人的基本CRUD页面。它由几个演示组件和一个容器组成。还有一个用于管理联系人状态的上下文对象。由于我们的状态树比前面的示例要复杂一些,因此我们必须使用useReducer
钩子。
创建状态上下文对象src/context/contact-context.js
并插入以下代码:
import React, { useReducer, createContext } from "react";
export const ContactContext = createContext();
const initialState = {
contacts: [
{
id: "098",
name: "Diana Prince",
email: "diana@us.army.mil"
},
{
id: "099",
name: "Bruce Wayne",
email: "bruce@batmail.com"
},
{
id: "100",
name: "Clark Kent",
email: "clark@metropolitan.com"
}
],
loading: false,
error: null
};
const reducer = (state, action) => {
switch (action.type) {
case "ADD_CONTACT":
return {
contacts: [...state.contacts, action.payload]
};
case "DEL_CONTACT":
return {
contacts: state.contacts.filter(
contact => contact.id !== action.payload
)
};
case "START":
return {
loading: true
};
case "COMPLETE":
return {
loading: false
};
default:
throw new Error();
}
};
export const ContactContextProvider = props => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<ContactContext.Provider value={[state, dispatch]}>
{props.children}
</ContactContext.Provider>
);
};
创建父组件src/views/contact-view.js
并插入以下代码:
import React from "react";
import { Segment, Header } from "semantic-ui-react";
import ContactForm from "../components/contact-form";
import ContactTable from "../components/contact-table";
import { ContactContextProvider } from "../context/contact-context";
export default function Contacts() {
return (
<ContactContextProvider>
<Segment basic>
<Header as="h3">Contacts</Header>
<ContactForm />
<ContactTable />
</Segment>
</ContactContextProvider>
);
}
创建演示文稿组件src/components/contact-table.js
并插入以下代码:
import React, { useState, useContext } from "react";
import { Segment, Table, Button, Icon } from "semantic-ui-react";
import { ContactContext } from "../context/contact-context";
export default function ContactTable() {
// Subscribe to `contacts` state and access dispatch function
const [state, dispatch] = useContext(ContactContext);
// Declare a local state to be used internally by this component
const [selectedId, setSelectedId] = useState();
const delContact = id => {
dispatch({
type: "DEL_CONTACT",
payload: id
});
};
const onRemoveUser = () => {
delContact(selectedId);
setSelectedId(null); // Clear selection
};
const rows = state.contacts.map(contact => (
<Table.Row
key={contact.id}
onClick={() => setSelectedId(contact.id)}
active={contact.id === selectedId}
>
<Table.Cell>{contact.id}</Table.Cell>
<Table.Cell>{contact.name}</Table.Cell>
<Table.Cell>{contact.email}</Table.Cell>
</Table.Row>
));
return (
<Segment>
<Table celled striped selectable>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Id</Table.HeaderCell>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Email</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>{rows}</Table.Body>
<Table.Footer fullWidth>
<Table.Row>
<Table.HeaderCell />
<Table.HeaderCell colSpan="4">
<Button
floated="right"
icon
labelPosition="left"
color="red"
size="small"
disabled={!selectedId}
onClick={onRemoveUser}
>
<Icon name="trash" /> Remove User
</Button>
</Table.HeaderCell>
</Table.Row>
</Table.Footer>
</Table>
</Segment>
);
}
创建演示文稿组件src/components/contact-form.js
并插入以下代码:
import React, { useState, useContext } from "react";
import { Segment, Form, Input, Button } from "semantic-ui-react";
import _ from "lodash";
import { ContactContext } from "../context/contact-context";
export default function ContactForm() {
const name = useFormInput("");
const email = useFormInput("");
// eslint-disable-next-line no-unused-vars
const [state, dispatch] = useContext(ContactContext);
const onSubmit = () => {
dispatch({
type: "ADD_CONTACT",
payload: { id: _.uniqueId(10), name: name.value, email: email.value }
});
// Reset Form
name.onReset();
email.onReset();
};
return (
<Segment basic>
<Form onSubmit={onSubmit}>
<Form.Group widths="3">
<Form.Field width={6}>
<Input placeholder="Enter Name" {...name} required />
</Form.Field>
<Form.Field width={6}>
<Input placeholder="Enter Email" {...email} type="email" required />
</Form.Field>
<Form.Field width={4}>
<Button fluid primary>
New Contact
</Button>
</Form.Field>
</Form.Group>
</Form>
</Segment>
);
}
function useFormInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = e => {
setValue(e.target.value);
};
const handleReset = () => {
setValue("");
};
return {
value,
onChange: handleChange,
onReset: handleReset
};
}
相应地插入以下代码App.js
:
import React from "react";
import { Container } from "semantic-ui-react";
import ContactView from "./views/contact-view";
export default function App() {
return (
<Container>
<h1>React Hooks Context Demo</h1>
<ContactView />
</Container>
);
}
实施代码后,您的浏览器页面应刷新。要删除联系人,您需要先选择一行,然后点击“ 删除”按钮。要创建新联系人,只需填写表格并点击“ 新联系人”按钮。
您也可以在CodePen上测试此代码。
查看代码以确保您理解所有内容。阅读我包含在代码中的注释。
概要
我希望这些示例在您了解如何无需Redux的情况下如何在React应用程序中管理共享应用程序状态的过程中为您提供帮助。如果您不使用钩子和上下文API来重写这些示例,那将导致更多的代码。看到不处理道具而编写代码有多容易?
在第二个示例中,您可能已经注意到有两个未使用的状态变量- loading
和error
。作为挑战,您可以进一步改进此应用程序以利用它们。例如,您可以实施虚假延迟,并使演示文稿组件显示加载状态。您还可以更进一步,并访问真正的远程API。在此error
状态变量可用于显示错误消息。
您现在可能要问自己一个唯一的问题:Redux对将来的项目是否必要?我用这种技术看到的一个缺点是您不能使用Redux DevTool扩展来调试应用程序状态。但是,随着新工具的开发,这种情况将来可能会改变。显然,作为开发人员,您仍然需要学习Redux才能维护旧项目。但是,如果您要开始一个新项目,则需要询问您自己和您的团队,是否真的有必要使用第三方状态管理库。
迈克尔·万约克
我编写干净,可读和模块化的代码。我喜欢学习可以提高效率和工作效率的新技术。
var t = new Date();
alert( new Date(t.getFullYear(), t.getMonth() + 1, 0, 23, 59, 59) );
当然可以,但是后面的数字getMonth
代表什么还不是很明显。现在,与更具可读性的对比一下:
const today = new Date();
console.log( lastDayOfMonth(today) );
该lastDayOfMonth
方法是date-fns提供的,date-fns是一个自称的综合工具集,用于在浏览器和Node.js中操纵JavaScript日期。
在本文中,我将向您展示如何启动和运行date-fns。阅读后,您将可以将其放入项目中,并利用其许多辅助方法轻松地操作日期。这将使代码t.getMonth() + 1, 0, 23, 59, 59
成为过去。
那么,为什么不仅仅使用Moment.js?
Moment.js是一个出色的库,用于在JavaScript中处理日期-它具有许多出色的功能,并提供了大量有用的实用程序。但是,并非没有批评者。
许多人都引用Moment对象是可变的(例如add
,诸如或subtract
更改原始Moment对象的操作)的事实,这使开发人员感到困惑,并且导致了bug。
它的大尺寸也受到了抨击。Moment在现代的“摇树”算法中不能很好地发挥作用,如果您需要国际化或时区支持,则可以使用相当大的JavaScript包快速找到自己。
到目前为止,Chrome的开发工具突显了以下事实:使用Moment可能会导致性能下降。
如果JavaScript库证明是昂贵的,请用较小的替代方法替换它们。
现在,@ ChromeDevTools中的 Lighthouse 建议使用较小的库,以提高包的大小。pic.twitter.com/VFe8TFV9Y5
-艾迪·奥斯曼尼(@addyosmani)2020年9月12日
所有这些都导致Moment维护人员将项目置于维护模式,并阻止Moment在以后的新项目中使用。
我们认识到许多现有项目可能会继续使用Moment,但是我们想阻止Moment在以后的新项目中使用。相反,我们想推荐当今在现代应用中使用的绝佳选择。
这使date-fns成为Moment.js的最佳替代产品之一。
安装
从库的第二版开始,安装date-fns的唯一方法是作为npm软件包。
npm install date-fns
或通过纱线:
yarn add date-fns
您可以将date-fns与CommonJS模块系统以及ES模块一起使用:
// CommonJS
const { lastDayOfMonth } = require('date-fns');
要么:
// ES Modules
import { lastDayOfMonth } from 'date-fns';
不幸的是,当前没有可用的date-fns CDN版本。在本GitHub问题中讨论了将其删除和恢复的可能性。但是,这并不是说您不能在浏览器中使用它,只是需要在工作流程中引入捆绑步骤。
让我们看看现在如何做。
如何捆绑date-fns在浏览器中使用
我假设您在计算机上安装了Node和npm。如果没有,请查阅我们有关安装Node的教程。
接下来,安装Parcel。这是一个捆绑程序(类似于Webpack),它将允许您捆绑JavaScript并将其提供给浏览器。
npm install -g parcel-bundler
接下来,使用package.json
文件创建一个新项目。
mkdir datefns
cd datefns
npm init -y
如上所述安装date-fns库:
npm install date-fns
现在创建两个文件,index.html
和index.js
。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>date-fns</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>
import { lastDayOfMonth } from 'date-fns';
const today = new Date();
console.log(lastDayOfMonth(today));
启动包裹的内置开发服务器:
parcel index.html
并导航到http:// localhost:1234。您不会在页面上看到任何显示,但是如果您打开浏览器的控制台。您应该已经记录了当月的最后一天。
关于部署,您可以运行:
parcel build index.js --experimental-scope-hoisting
使Parcel在dist
文件夹中输出缩小且摇晃的捆绑包。
Date-fns基本用法
现在我们已经启动并运行,让我们看看date-fns可以做什么。
处理日期时最常见的任务之一是能够很好地格式化日期。我们可以使用date-fns 格式函数来做到这一点。
更改上面示例页面中的HTML,使其如下所示:
<body>
<h1>The date today is <span></span></h1>
<script src="index.js"></script>
</body>
在index.js
我们要导入的format
函数中,我们可以传递今天的日期和格式字符串。然后,我们想将结果输出到页面。
import { format } from 'date-fns';
const today = new Date();
const formattedDate = format(today, 'dd.MM.yyyy');
document.querySelector('h1 > span').textContent = formattedDate;
当然,我们不限于一种dd.MM.yyyy
格式,让我们尝试不同的方法:
const formattedDate = format(today, 'PPPP');
这将格式化像这样的输出:Wednesday, September 16th, 2020
。您可以在docs中找到格式设置选项的完整列表。
变更地区
如果您拥有使用多种语言的网站,那么date-fns可使国际化日期和时间变得简单。让我们迎接德国客人:
<h1>Heute ist <span></span></h1>
在JavaScript文件中,我们可以导入德语语言环境并将其传递给format
函数:
import { format } from 'date-fns';
import { de } from 'date-fns/locale';
const today = new Date();
const formattedDate = format(today, 'PPPP', { locale: de });
document.querySelector('h1 > span').textContent = formattedDate;
这将输出的东西沿着线:Heute ist Mittwoch, 16. September 2020
。
要求并以语言环境作为选项似乎很复杂,但与Moment.js缺省情况下使用所有语言环境使构建膨胀的方式不同,date-fns强制开发人员在需要时手动要求语言环境。
您可以通过查看node_modules/date-fns/locale
项目中的文件夹来查看可用语言环境的列表。
不变性,纯正和简单
date-fns的卖点之一是其功能纯净,易于解释。这样可以使代码易于理解,并在出现问题时更易于调试。
让我使用Moment.js作为反示例进行演示。如前所述,Moment中的日期是可变的,这可能导致意外行为。
const moment = require('moment');
const now = new Date();
const mNow = moment(now);
mNow.add('day', 3);
console.log(mNow.toDate());
mNow.add(3, 'day');
console.log(mNow.toDate());
// 2020-09-19T10:08:36.999Z
// 2020-09-22T10:08:36.999Z
这里有几件事要注意。Moment的add
函数对其接受其参数的顺序并不挑剔(尽管第一个方法现在将引发弃用警告)。但更令人困惑的是,如果add
连续调用多次,将不会得到相同的结果,因为Moment对象是可变的:
mNow.add(3, 'day'); // add 3 days
mNow.add(3, 'day'); // adds 3 **more** days
现在,将其与date-fns进行比较,date-fns会将参数保持在一个顺序中,并且始终返回相同的结果,并Date
为每次调用返回一个新对象。
import { addDays } from 'date-fns';
const today = new Date();
const threeDaysTime = addDays(3, today);
const sixDaysTime = addDays(threeDaysTime, 3);
console.log(today); // Wed Sep 16 2020 12:11:55 GMT+0200
console.log(threeDaysTime); // Sat Sep 19 2020 12:12:58 GMT+0200
console.log(sixDaysTime); // Invalid Date
还要注意,方法名如何更具表现力(addDays
而不是仅仅add
),保持事物的一致性,并使一种方法只能执行一件事情和一件事情。
比较日期
如果您查看SitePoint JavaScript频道上的帖子列表,则可以看到其中某些列表被列为在某个日期发布,而其他列表则被列为在X天前发布。如果您尝试在原始 JavaScript中实现此功能可能会花费一些时间,但是使用date- fns则很容易 -只需使用formatDistance方法即可。
让我们比较两个不同的日期。
import { formatDistance } from 'date-fns';
const startDate = new Date(2020, 8, 16); // (Sep 16 2020)
const endDate = new Date(2020, 11, 25); // (Dec 25 2020)
const distanceInWords = formatDistance(startDate, endDate);
console.log(`It is ${distanceInWords} until Christmas`);
// It is 3 months until Christmas
请注意,使用JavaScript时,月份是从零开始的(例如,月份11 =十二月),但是天数是从1开始增加的。这一次又一次地使我绊倒。
处理日期集合
Date-fns有一些非常方便的帮助器方法,您可以使用它们以各种方式来操作日期集合。
订购日期收集
以下示例使用compareAsc将日期按升序排序。为此,如果第一个日期在第二个日期之后,则返回1;如果第一个日期在第二个日期之前,则返回-1;如果日期相等,则返回0。
import { compareAsc } from 'date-fns';
const date1 = new Date('2005-01-01');
const date2 = new Date('2010-01-01');
const date3 = new Date('2015-01-01');
const arr = [date3, date1, date2];
const sortedDates = arr.sort(compareAsc);
// [ 2005-01-01, 2010-01-01, 2015-01-01 ]
如您所见,日期现在按升序排列。
与之对应的方法compareAsc
是compareDesc。
import { compareDesc } from 'date-fns';
...
const sortedDates = arr.sort(compareDesc);
// [ 2015-01-01, 2010-01-01, 2005-01-01 ]
生成两个日期之间的日期
要生成两个日期之间的日期,可以使用我们之前遇到的addDays方法,以及eachDayOfInterval帮助器,该方法返回指定范围内的日期数组。
import { addDays, eachDayOfInterval } from 'date-fns';
const today = new Date();
const aWeekFromNow = addDays(today, 7);
const thisWeek = eachDayOfInterval(
{ start: today, end: aWeekFromNow },
);
console.log(thisWeek);
/*
[
Wed Sep 16 2020 00:00:00 GMT+0200 (Central European Summer Time),
Thu Sep 17 2020 00:00:00 GMT+0200 (Central European Summer Time),
Fri Sep 18 2020 00:00:00 GMT+0200 (Central European Summer Time),
Sat Sep 19 2020 00:00:00 GMT+0200 (Central European Summer Time),
Sun Sep 20 2020 00:00:00 GMT+0200 (Central European Summer Time),
Mon Sep 21 2020 00:00:00 GMT+0200 (Central European Summer Time),
Tue Sep 22 2020 00:00:00 GMT+0200 (Central European Summer Time),
Wed Sep 23 2020 00:00:00 GMT+0200 (Central European Summer Time)
]
*/
寻找最近的日期
可以使用closestTo方法在日期数组中查找最接近某个日期的日期。此代码段来自上一个示例:
import { addDays, eachDayOfInterval, closestTo } from 'date-fns';
...
const christmas = new Date(2020, 11, 25);
const closestToChristmasDate = closestTo(christmas, thisWeek);
console.log(closestToChristmasDate);
// Wed Sep 23 2020 00:00:00 GMT+0200 (Central European Summer Time)
如果您想获取数组的索引,则还有closeestIndexTo方法。
验证日期
我要看的最后一个助手是isValid方法,顾名思义,该方法检查给定日期是否有效。
但是,由于JavaScript处理日期的方式,因此需要注意一些陷阱:
import { isValid } from 'date-fns';
const invalidDate = new Date('2020, 02, 30');
console.log(isValid(invalidDate));
// true, lol, wut?
您会以为上面的代码段应该输出false
,这是可以原谅的,因为2020年2月30日显然是无效的日期。要了解正在发生的事情,请new Date('2020, 02, 30')
在浏览器的控制台中输入。您会发现Sun Mar 01 2020
回来了-JavaScript从2月底开始花费了额外的时间,并将其变成3月1日(当然是有效日期)。
要解决此问题,我们可以在检查日期之前解析日期:
import { isValid, parse } from 'date-fns';
const validDate = parse('29.02.2020', 'dd.MM.yyyy', new Date());
const invalidDate = parse('30.02.2020', 'dd.MM.yyyy', new Date());
console.log(validDate);
// Sat Feb 29 2020 00:00:00 GMT+0100 (Central European Standard Time)
console.log(invalidDate);
// Invalid Date
console.log(isValid(validDate));
// true
console.log(isValid(invalidDate));
// false
可以轻松地将其提取到一个小助手方法中,该方法对于例如验证表单中的用户输入很有用。
时区
date-fns的一个缺点是它目前没有像Moment.js那样的任何时区帮助程序功能,而是返回了代码在其上运行的本地时区。
这个堆栈溢出的答案提供了一些本机Date
对象如何实际不存储“实时区域”数据的背景知识。在该线程中,您会注意到他们提到了一种在JavaScript中原生设置时区的方法。这不是一个全面的解决方案,但是它适用于仅需要输出转换(从UTC或本地时间到特定时区)的许多情况。
new Date().toLocaleString("en-US", {timeZone: "America/New_York"});
时区实际上是一个要解决的复杂问题,这就是为什么MomentJS有一个单独的库。有计划在date-fns中添加时区支持,但是在撰写本文时,这仍在进行中。
但是,npm上有一个可用的软件包(基于对date-fns的未合并提取请求),该软件包使用Intl API添加了对date-fns v2.0.0的时区支持。每周下载14万次,这似乎很受欢迎,但是在撰写本文时,它已经有几个月没有更新了。
就是说,这是您可能会使用的方式:
npm i date-fns-tz
import { format, utcToZonedTime } from 'date-fns-tz';
const today = new Date(); // Wed Sep 16 2020 13:25:16
const timeZone = 'Australia/Brisbane'; // Let's see what time it is Down Under
const timeInBrisbane = utcToZonedTime(today, timeZone);
console.log(`
Time in Munich: ${format(today, 'yyyy-MM-dd HH:mm:ss')}
Time in Brisbane: ${format(timeInBrisbane, 'yyyy-MM-dd HH:mm:ss')}
`);
// Time in Munich: 2020-09-16 13:26:48
// Time in Brisbane: 2020-09-16 21:26:48
结论
Date-fns是一个很棒的小程序库,它为您提供了很多帮助程序方法,可用于在JavaScript中使用日期和时间。目前正在积极开发中,并且Moment.js已进入维护模式,这使其成为Moment.js的理想替代品。
我希望本文能给您足够的理解和启发,以便您进行检查并开始在自己的项目中使用它。