lightningminers / article Goto Github PK
View Code? Open in Web Editor NEW⚡️闪电矿工翻译组-前端(front end developer)
License: GNU Lesser General Public License v3.0
⚡️闪电矿工翻译组-前端(front end developer)
License: GNU Lesser General Public License v3.0
原文地址: https://hackernoon.com/testing-react-components-with-jest-and-enzyme-41d592c174f
有人说在通常情况下测试 React 组件是没有太大用处的,但是我觉着在一些场景下是很有必要的:
我尝试过很多的工具组合,但是最终如果会推荐给别的开发者,我更乐意去择推荐如下组合:
我经常在测试中使用的是浅渲染和 Jest 快照测试。
在 Jest 进行快照测试
浅渲染指的是将一个组件渲染成虚拟 DOM 对象,但是只渲染第一层,不渲染所有子组件。所以即使你对子组件做了一下改动却不会影响浅渲染的输出结果。或者是引入的子组件中发生了 bug,也不会对父组件的浅渲染结果产生影响。浅渲染是不依赖 DOM 环境的。
举个例子:
const ButtonWithIcon = ({icon, children}) => (
<button><Icon icon={icon} />{children}</button>
);
在 React 中将会被渲染成如下:
<button>
<i class="icon icon_coffee"></i>
Hello Jest!
</button>
但是在浅渲染中只会被渲染成如下结果:
<button>
<Icon icon="coffee" />
Hello Jest!
</button>
需要注意的是 Icon 组件并未被渲染出来。
Jest 快照就像那些带有由文本字符组合而成表达窗口和按钮的静态UI:它是存储在文本文件中的组件的渲染输出。
你可以告诉 Jest 哪些组件输出的 UI 不会有意外的改变,那么 Jest 在运行时会将其保存到如下所示的文件中:
exports[`test should render a label 1`] = `
<label
className="isBlock">
Hello Jest!
</label>
`;
exports[`test should render a small label 1`] = `
<label
className="isBlock isSmall">
Hello Jest!
</label>
`;
每次更改组件时,Jest 都会与当前测试的值进行比较并显示差异,并且会在你做出修改是要求你更新快照。
除了测试之外,Jest 将快照存储在类似 __snapshots __ / Label.spec.js.snap
这样的文件中,同时你需要提交这些文件。
第一步安装所有的依赖包括同版本依赖:
npm install --save-dev jest react-test-renderer enzyme enzyme-adapter-react-16 enzyme-to-json
还需要安装 Babel 插件 babel-jest 或者 TypeScript 插件 ts-jest
更新工程的 package.json 文件:
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"setupFiles": ["./test/jestsetup.js"],
"snapshotSerializers": ["enzyme-to-json/serializer"]
}
配置项 'snapshotSerializers' 允许你通过配置 'enzyme-to-json',把 Enzyme 的封装类型传给 'Jest' 的快照匹配项中,从而不需要手动进行转化。
创建一个 test/jestsetup.js 的文件来自定义 Jest 的运行环境(上面的 setupFiles 配置项)
import Enzyme, { shallow, render, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// React 16 Enzyme adapter
Enzyme.configure({ adapter: new Adapter() });
// Make Enzyme functions available in all test files without importing
global.shallow = shallow;
global.render = render;
global.mount = mount;
针对 css 模块也可以添加下面的配置到package.json
"jest": {
"moduleNameMapper": {
"^.+\\.(css|scss)$": "identity-obj-proxy"
}
}
And run:
同时安装依赖:
npm install --save-dev identity-obj-proxy
注意 identity-obj-proxy 依赖的 node 版本是 Node 4或者 Node 5需要开启 'harmony-proxies'
对于大部分没有交互的组件,下面的测试用例已经足够:
test('render a label', () => {
const wrapper = shallow(
<Label>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});
test('render a small label', () => {
const wrapper = shallow(
<Label small>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});
test('render a grayish label', () => {
const wrapper = shallow(
<Label light>Hello Jest!</Label>
);
expect(wrapper).toMatchSnapshot();
});
有的时候如果你想测试的更精确和看到真实的值。那样的话需要在 Enzyme API 中使用 Jest的 断言。
test('render a document title', () => {
const wrapper = shallow(
<DocumentTitle title="Events" />
);
expect(wrapper.prop('title')).toEqual('Events');
});
test('render a document title and a parent title', () => {
const wrapper = shallow(
<DocumentTitle title="Events" parent="Event Radar" />
);
expect(wrapper.prop('title')).toEqual('Events — Event Radar');
});
有的时候你不能用快照。比如组件里面有随机ID像下面的代码:
test('render a popover with a random ID', () => {
const wrapper = shallow(
<Popover>Hello Jest!</Popover>
);
expect(wrapper.prop('id')).toMatch(/Popover\d+/);
});
你可以模拟类似 'click' 或者 'change'这样的事件然后把组件和快照做比较:
test('render Markdown in preview mode', () => {
const wrapper = shallow(
<MarkdownEditor value="*Hello* Jest!" />
);
expect(wrapper).toMatchSnapshot();
wrapper.find('[name="toggle-preview"]').simulate('click');
expect(wrapper).toMatchSnapshot();
});
有的时候你想要测试一个子组件中一个元素是怎样影响组件的。你需要使用 Enzyme的 mount 方法来渲染一个真实的 DOM。
test('open a code editor', () => {
const wrapper = mount(
<Playground code={code} />
);
expect(wrapper.find('.ReactCodeMirror')).toHaveLength(0);
wrapper.find('button').simulate('click');
expect(wrapper.find('.ReactCodeMirror')).toHaveLength(1);
});
类似于在事件测试中,由使用快照测试组件的输出呈现替换为使用Jest的mock函数来测试事件处理程序本身:
test('pass a selected value to the onChange handler', () => {
const value = '2';
const onChange = jest.fn();
const wrapper = shallow(
<Select items={ITEMS} onChange={onChange} />
);
expect(wrapper).toMatchSnapshot();
wrapper.find('select').simulate('change', {
target: { value },
});
expect(onChange).toBeCalledWith(value);
});
Jest使用JSON进行快照测试,因此你可以测试返回JSON的任何函数,方法与测试组件相同:
test('accept custom properties', () => {
const wrapper = shallow(
<Layout
flexBasis={0}
flexGrow={1}
flexShrink={1}
flexWrap="wrap"
justifyContent="flex-end"
alignContent="center"
alignItems="center"
/>
);
expect(wrapper.prop('style')).toMatchSnapshot();
});
调试浅层渲染器输出
Use Enzyme’s debug method to print shallow renderer’s output:
使用Enzyme的调试方法打印千层渲染器的输出:
const wrapper = shallow(/*~*/);
console.log(wrapper.debug());
启用覆盖范围的失败测试
当你的测试失败时,带有覆盖范围标志的diff如下所示:
-<Button
+<Component
尝试将箭头函数组件替换为常规函数组建:
- export default const Button = ({ children }) => {
+ export default function Button({ children }) {
requestAnimationFrame 错误
当你运行你的测试时,你可能会看到如下错误:
console.error node_modules/fbjs/lib/warning.js:42
Warning: React depends on requestAnimationFrame. Make sure that you load a polyfill in older browsers. http://fb.me/react-polyfills
React 16依赖于requestAnimationFrame
,因此你需要在你的测试代码中添加一个polyfill
// test/jestsetup.js
import 'raf/polyfill';
原文地址: https://blog.usejournal.com/vue-js-gsap-animations-26fc6b1c3c5a
原文作者: Daily Fire
翻译作者: hanxiansen
中文标题:通过GASP让vue实现动态效果
单页应用及支持它们的前端框架提供了一个很好的机会,可以为程序设计提供令人惊叹的交互层,本文,我们将了解 vue.js 及如何集成 GASP 动画库来添加令人惊叹的动画效果。
Vue.js 是一个功能强大且易掌握的 JS 框架,在 Vue CLI 的帮助下,我们能够快速构建具有所有最新 Webpack 功能的应用程序,而无需花费时间来配置 webpack,只需安装 Vue CLI,在重大上输入:create <project-name>
,您就可以发车了。
GASP是一个JavaScript动画库,它支持快速开发高性能的 Web 动画。GASP 使我们能够轻松轻松快速的将动画串在一起,来创造一个高内聚的流畅动画序列。
在构建新的 Daily Fire 主页时,我为了演示产品如何工作而使用了大量动画,但是通过 GASP的方式(不是 GIF 或视频),我可以为动画添加交互层使它们更具吸引力。如你所见,将 GASP 与 vue相结合是简单且强大的,
让我们看看如何使用 GASP 与 VUE 实现简单的时间轴,我们将在本文使用 .vue 文件,这些文件由 webpack 的 vue-loader加载使用,通过Vue CLI创建的项目将会自动
让我们先编写一些标记,以便了解我们将制作的动画
<template>
<div ref="box" class="box"></div>
</template>
<style>
.box {
height: 60px;
width: 60px;
background: red;
}
</style>
我们将一个红色 box 绘制到DOM中,注意 div 标签上的ref
标记,当我们在引入GASP 后通过该属性可以引用该元素。VUE 通过组件的$refs
属性使通过 ref 标记的元素可以使用。
现在引入 GASP
<template>
<div ref="box" class="box"></div>
</template>
<script>
import { TimelineLite } from 'gsap'
export default {
mounted() {
const { box } = this.$refs
const timeline = new TimelineLite()
timeline.to(box, 1, { x: 200, rotation: 90 })
}
}
</script>
<style>
/* styles emitted */
</style>
首先,我们从 GASP 中引入TimelineLite
,然后,当组件挂载后,我们通过$refs
获取到对 box 元素的引用。然后我们初始化 GASP 的时间线实例来播放动画。
Timeline 实例暴露出一个to
方法,我们传递三个参数给该方法:
下面链接展示了一小段代码展示的运行效果:
https://codepen.io/smlparry/pen/rZdbEw
很简单,有木有!接下来让我们用 GASP 的 EasePack 赋予这个小动画更多的生命。使用 ease
缓动特效是一种简单的方案,它将使你的动画特效不再那么僵硬,更加友好。当然,如果你没有将你的动画放进队列中,你将不能充分利用好 GASP 的时间线,让我们在动画的运行中途将其由红框过渡为绿框。
<template>
<div ref="box" class="box"></div>
</template>
<script>
import { TimelineLite, Back } from 'gsap'
export default {
mounted() {
const { box } = this.$refs
const timeline = new TimelineLite()
timeline.to(box, 1, {
x: 200,
rotation: 90,
ease: Back.easeInOut, // Specify an ease
})
timeline.to(box, 0.5, {
background: 'green'
},
'-=0.5' // Run the animation 0.5s early
)
}
}
</script>
<style>
/* styles emitted */
</style>
注意第21行的额外参数,在上面的代码中它将告诉 GASP 运行一个相对于前一个动画的动画,使用+=
指定完成后的时间,使用-=
指定完成前的时间。
结果在链接中:https://codepen.io/smlparry/pen/mGxYWN
有了这个简单的改动,我们的动画更加生动了!
通过这些原则的基础了解,我们可以开始构建更复杂、更吸引人的动画。正如我们将在下一个例子中所看到的。
让我们创建一个动画(该动画曾在Daily Fire首页中使用 ),这个友好的小泡泡:
让我们从标签标记开始:
<template>
<div class="bubble-wrapper">
<div ref="bubble" class="bubble">
<img class="bubble-image"
src="./assets/img/slack-white.svg" />
</div>
<div ref="bubblePulse" class="bubble-pulse"></div>
</div>
</template>
<style>
.bubble-wrapper {
position: relative;
}
.bubble {
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid white;
background: #272727;
border-radius: 50%;
height: 100px;
width: 100px;
}
.bubble-pulse {
position: absolute;
z-index: 1;
height: 120px;
width: 120px;
top: 50%;
left: 50%;
margin-top: -60px;
margin-left: -60px;
background: #272727;
border-radius: 50%;
opacity: 0;
transform: scale(0);
}
.bubble-image {
height: 50%;
}
</style>
我们现在有这个一个元素
让我们赋予它一些生命:
<template>
<!-- HTML emitted -->
</template>
<script>
import { TimelineLite, Back, Elastic, Expo } from "gsap"
export default {
mounted() {
const { bubble, bubblePulse } = this.$refs
const timeline = new TimelineLite()
timeline.to(bubble, 0.4, {
scale: 0.8,
rotation: 16,
ease: Back.easeOut.config(1.7),
})
timeline.to(
bubblePulse,
0.5,
{
scale: 0.9,
opacity: 1,
},
'-=0.6'
)
this.timeline.to(bubble, 1.2, {
scale: 1,
rotation: '-=16',
ease: Elastic.easeOut.config(2.5, 0.5),
})
this.timeline.to(
bubblePulse,
1.1,
{
scale: 3,
opacity: 0,
ease: Expo.easeOut,
},
'-=1.2'
)
}
}
</script>
<style>
/* CSS Emitted */
</style>
虽然一开始看起来很麻烦,但只要花点时间来理解它的实际运行情况,其实它只是一些按顺序排列的 CSS transform属性。这里有几个自定义的 ease 特效,GASP 提供了一个有趣的小工具,你可以根据喜好自由配置:https://greensock.com/ease-visualizer
现在效果如下:
上面的gif动画其实具有欺骗性,gif图片是循环的,但代码不是,让我们看看如何使用 GASP 和 VUE 循环播放动画。GASP 的 TimelineLite
提供了一个onComplete
属性,通过该属性我们可以分配一个函数,利用该函数我们可以循环动画,另外,我们将通过data
使timeline
在组件的其余部分也可使用。
<template>
<!-- HTML Emitted -->
</template>
<script>
// ...
export default {
data() {
return {
timeline: null,
}
},
mounted() {
// ...
this.timeline = new TimelineLite({
onComplete: () => this.timeline.restart()
})
// ...
}
}
</script>
<style>
/* CSS Emitted */
</style>
现在 GASP 将会在动画完成后又重新开始,效果如下:https://codepen.io/smlparry/pen/dqmEax
现在,我们的动画效果已经写得差不多了,可以考虑添加一些交互特效。在本例中,我们将添加一个按钮,来随机更新气泡中的logo。
为了能做到该需求,我们必须做以下几件事:
<template>
<div class="bubble-wrapper">
<div ref="bubble" class="bubble">
<img class="bubble-image"
:src="currentLogo" />
</div>
<div ref="bubblePulse" class="bubble-pulse"></div>
</div>
<button @click="randomiseLogo">Random Logo</button>
</template>
<script>
// ...
export default {
data() {
return {
timeline: null,
logos: ['path/to/logo-1.svg', 'path/to/logo-2.svg', 'path/to/logo-3.svg'],
currentLogo: '',
}
},
methods: {
randomiseLogo() {
const logosToSample = this.logos.filter(logo => logo !== this.currentLogo)
this.currentLogo = logosToSample[Math.floor(Math.random() * logosToSample.length)]
}
},
mounted() {
this.randomiseLogo()
// ...
}
}
</script>
<style>
/* CSS Emitted */
</style>
有了上面的代码,现在我们可以通过一个按钮来更新制作运行动画的元素,通史也可以对其进行动画制作,效果:https://codepen.io/smlparry/pen/RYMXPx
我使用了与上面动画非常类似的技术来实现主页的动画效果,我从数组中选择列表的下一个元素,然后将其append到列表中。
原文地址: https://www.robinwieruch.de/javascript-project-setup-tutorial/
原文作者: robinwieruch.de
翻译作者: chenmf
本文是“前端工程搭建”系列的第一篇
Part 2: 怎么设置Webpack项目
Part 3: 怎么在Webpack中设置Babel
原文地址: https://medium.com/siliconwat/data-structures-in-javascript-1b9aed0ea17c
原文作者: Thon Ly
翻译作者: hanxiansen
中文标题:前端必知必会的数据结构
随着业务逻辑越来越多地从后台迁往前端,前端工程化方面的专业知识变得愈发重要。作为一名前端工程师,我们依赖于像 React 这样的视图库来提高工作效率。而视图库又依赖于像 Redux 这样的数据流管理工具来掌控数据。React 和 Redux 共同遵循响应式设计模式,在该设计模式下,数据的更新将会导致对应 UI 的更新。后端越来越充当着简单的 API 服务器角色,提供数据的检索和更新接口。实际上,后端只是把数据库搬到了前端,期望前端来处理所有的数据控制器逻辑。微服务和 GraphQL 的日益流行证明了这一增长趋势。
现在,除了掌握 HTML 和 CSS 以外,前端工程师还应该掌握 JavaScript。因为客户端成了服务端的数据库“副本”,掌握常用的的数据结构变得至关重要。事实上,一个工程师的水平可以从 ta 知道何时及为何使用特定的数据结构的能力中得以体现。
Bad programmers worry about the code. Good programmers worry about data structures and their relationships.
糟糕的工程师考虑代码,优秀的工程师考虑数据结构及其对应关系。
— Linus Torvalds, Creator of Linux and Git
总的来说,基础的数据结构分三种:栈和队列是类似数组的数据结构,它们的区别只在于插入和删除条目的方式不同。链表、树和图是具有节点的数据结构,这些节点保持着对其它节点的引用。哈希表依赖于哈希函数来保存和定位数据。就复杂度而言,栈和队列是最简单的,可以用链表来构造。树和图是最复杂的,因为它们扩展了链表的概念。哈希表利用这些数据结构来高效的执行任务。就效率而言,链表最适合记录与存储数据,哈希表则最适合搜索与检索数据。
本文将依照“为什么(why)”与“何时(when)”的顺序来进行阐释说明,让我们开始吧!
可以说,Js 中最重要的栈就是调用栈(译者注:调用栈是 js 解释器追踪函数执行流的一种机制),在该栈中,无论何时执行函数,我们都会往栈中推入该函数的作用域。在编码中,它只是一个具有两个基本操作的数组:push
和 pop
。push
将元素添加到数组的头部,而pop
将它们从同一位置删除。换句话说,栈遵循“后进先出”的原则(LIFO)。
下面是代码中的栈示例。请注意,我们可以颠倒栈的顺序:底部变为顶部,顶部变为底部。因此,我们可以使用十足的 unshift
和 shift
方法来分别代替 push
和 pop
。
https://codepen.io/thonly/pen/GMyLOV
随着条目数的增加,push
/pop
将会比unshift
/shift
拥有更好的性能,因为后者的每个条目都需要重建索引,但前者不需要。
###队列(Queue)
Js 是一门事件驱动型语言,它使得支持非阻塞操作成为可能。在内部,浏览器只管理一个线程来运行整个 Js 代码,它使用事件队列(event queue)将事件注册到队列中,然后使用事件循环(event loop)来监听被注册的事件。为了在单线程环境中支持异步操作(节省资源并增强 Web 体验),监听函数只有当调用栈为空时才会出队列并执行。Promise 就是基于这种事件驱动架构设计的,它允许用“同步模式”来执行不糊阻塞其它操作的异步代码。
在编程中,队列仅仅是拥有两个主要操作方法的数组:Unshift
将条目列入数组的尾部,而Pop
则将它们从数组头部列出。换句话说,队列遵循“First in, First Out”的原则(FIFO)。如果颠倒方向,我们可以使用push
和shift
方法唉替换unshift
和pop
方法。
队列的代码示例如下:
https://codepen.io/thonly/pen/KypxZg
类似于数组,链表也是按照顺序存储数据元素。不同的是,链表保存指向其它元素的指针,而非索引。链表的第一个节点被称为头部节点,最后一个节点被称为尾部节点,在单链表中,每个检点只有一个指向下个节点的指针。在单链表里,头部节点是我们遍历剩余节点的的地方。在双链表中,每个节点还保留了指向上一个节点的指针。因此,我们也可以从尾部节点开始,“倒退遍历“到头部节点。
链表具有常量的插入和删除时间,因为我们只需更改指针即可。但在数组中执行相同的操作却需要线性时间,因为后续条目需要移位。此外,只要有空间,链表就可以增长。然而,及时是可以自动调整大小的“动态数组”在空间利用上也是出乎意料的昂贵。当然,事物总有权衡取舍。在链表中检索和编辑一个元素,我们需要遍历整个链表,这等同于线性时间。然而对于有索引的数组,这样的操作时微不足道的。
类似于数组,链表也可以进行栈的操作。该操作很简单,只需将头部作为插入和取出的唯一位置。链表也可以进行队列操作,这可以使用双向链表来实现,插入发生在尾部,删除发生在头部,反之亦然。对于大的数据而言,这种实现队列的方案比数组的性能更高,因为在数组的开始位置插入和移出元素,需要线性的时间来重新索引后续的每个元素。
链表数据结构在客户端和服务端都很有用。在客户端,像Express这样的 web 框架也以类似的逻辑构建它的中间件。当收到请求,请求通过管道一个接一个的传递下去,直到请求被发出。
双向链表的代码如下:
https://codepen.io/thonly/pen/QqRVJX
树类似于数组,区别在于它在层次结构中保留了对多个子节点的引用。换句话说,每个节点不止拥有一个父节点。DOM 树就是这样一种数据结构,它拥有一个 html 根节点,然后分成 head节点和 body 节点,这两个节点又进一步细分为多个你所熟悉的 html 标签。更深层面上,原型继承和 React 的组件也采用类似的树形结构。当然,作为DOM的内存表现形式,React 的虚拟 Dom也是一个树形结构。
二叉搜索树是特殊的,因为它拥有的子节点不能超过两个。左子节点的值必须小于或等于其父节点的值,而右子节点的值必须大于父节点的值。通过这种结构和平衡方式,我们可以在指数时间内检索任何值,因为我们可以在每次迭代中忽略另一半分支。插入和删除的时间也是指数级的。此外最小值和最大值也可以很容易的在最左边和最右边的叶子节点中找到。
遍历树可以在垂直或水平过程中进行。在垂直方向的深度优先遍历(DFT)中,递归算法将会比迭代算法更加优雅。节点遍历可以按照前序遍历、中序遍历、后序遍历的方式进行。如果我们需要先访问根节点再探索叶子节点,我们应选择前序遍历。但如果我们需要先访问叶子节点再访问根节点,我们应选择后序遍历。顾名思义,中序遍历使我们可以按照顺序进行遍历。这种特性使二叉搜索树成为排序的最佳选择。
在水平方向上的广度优先遍历(BFT)中,迭代比递归更优雅。这需要使用一个队列来追踪每次迭代的所有子节点。这样一个队列所需的内存是微不足道的。然而,如果树的形状宽大于深,BFT 是比 DFT 更好的选择。此外,BFT 在任意两个节点之间采用的路径是可能的最短路径。
二叉搜索树的示例代码如下:
https://codepen.io/thonly/pen/qVWOpM
如果一个树可以自由的拥有多个父级节点,它将变为Graph(图)。将图中的节点连接在一起的边可以是有方向的,也可以是无方向的,可以是加权的,也可以是未加权的。同时具备方向和权重的边类似于矢量。
混合形式的多重继承和具有多对多关系的数据对象生成图形结构。社交网络和互联网本身也是图。自然界中最复杂的图是我们的人脑,神经网络试图复制人脑来赋予机器超级智能。
###哈希表(Hash Table)
哈希表是一种类似于字典的结构,它将键与值进行配对。每个键值对在内存中的储存位置由焊锡函数决定,该哈希函数接收一个 key ,然后返回它应该插入和检索该键值对的地址。如果两个或更多的 key 被转换为同一个地址,可能会导致冲突。为了保持哈希函数的健壮性,getter 和 setter 方法应该预防到这些问题,来确保可以恢复所有的数据,并且不会覆盖任何数据,通常,链表可以提供最简单的解决方案。拥有巨大空间的哈希表也同样可以解决该问题。
可以转载到公众号高级前端进阶上吗?会标明作者与来源,感谢
原文地址:https://blog.logrocket.com/css-selectors-level-4-b5da36bcd54c/
原文作者:Esteban Herrera
翻译作者:thisang
选择器是 CSS 的核心部分。你用来做一些操作比如说选择某种类型的所有元素,就像下面这样:
div {
/* 一些应用在所有 div 元素上的样式 */
}
或者你可以选择一个在它的父元素下的最后一个子元素:
ul li:last-child {
/* 一些只应用在列表的最后一个子元素上的样式 */
}
当然,你也可以去做一些更复杂的事情,比如说选择选择列表中除了最后一个子元素之外的所有子元素。
事实上,这样做的方法不止一种,一些方法甚至更复杂。
例如,比较以下这两个:
ul li {
/* 一些应用在所有子元素上的样式 */
}
ul li:last-child {
/* 一些样式用来重置上面生效的样式,因为上面的样式不适用于上面最后一个子元素 */
}
ui li:nth-last-child(n+2) {
/* 一些应用在除了最后一个子元素之外的所有子元素上面的样式 */
}
ul li
属于第一级选择器。
last-child
和nth-last-child
属于第三级选择器。
你可以把级别看作是 CSS 选择器的版本,其中每个级别都会添加一些功能更强大的选择器。
在这篇文章中,我会根据截至 2019 年 1 月的草案规范,和你一起过一遍下一代选择器(第四级),主要有以下分类:
在我写这篇文章的时候,第四级选择器的规范还处于草案阶段。直到官方推荐之前,它可能还会改,而且你可能还会发现很多选择器甚至都还没有被一些浏览器支持或者需要添加一些前缀(:-moz-
或:-webkit-
)。
对于每一个选择器,我都会提供一个can I use的链接以便你可以看到哪些浏览器支持它(如果有的话)、一些简短的介绍、一个例子、和一个 Codepen 的链接,这样你就能自己去尝试一下(即使它暂时无法使用,因为将来可能会改)。
好了,让我们中逻辑组合选择器开始。
这个类别包括通过组合选择其他选择器而起作用的选择器。
:not(selector1,selector2,...)
它选择那些与传入的参数不匹配的元素列表。例如:
p:not(.beginning, .middle){
color: red;
}
这会给所有的没有.beginning
和.middle
类的p
元素设置颜色为红色的样式。
这个选择器的第三级版本只能允许一个选择器,而不是两个或更多的的选择器。例如,上面的样式可以写成:
p:not(.beginning):not(.middle){
color: red;
}
:is(selector1,selector2)
它选择那些与传入的参数匹配的元素列表。例如:
p:is(.beginning, .middle){
color: blur;
}
这会给所有的具有.beginning
和.middle
类的p
元素设置颜色为蓝色的样式。
自从上一个工作草案版本以来最重大的变化之一就是:matches()
选择器被更名为:is()
并且已经被 Safari(唯一一个完全支持这个选择器的浏览器)弃用,因此它和与之相反的选择器:not()
可以更好的配对。
如果要向后兼容,可以用:is()
作为:matches()
的别名。
然而,:matches()
以前叫做:any()
,因此大多数浏览器都支持带前缀的伪类:
p:-webkit-any(.beginning, .middle){
color: blue;
}
p:-moz-any(.beginning, .middle) {
color: blue;
}
:where(selector1,selector,...)
在我写这篇文章的时候,还没有任何一个浏览器支持这个选择器。
它具有和:is()
相同的语法和功能,但是不管这个选择器接受多少个参数,这个选择器的权重都是 0。
权重是 CSS 的应用规则。如果两个选择器同时应用在同一个元素上面,则权重高的那个生效。如果不同规则具有相同的权重,那么应用在这个元素上的最后一个规则将会生效。
这个选择器可以用来实现筛选和覆盖与之关联的元素的样式。
下面是一个权重的例子:
a:not(:hover) {
text-decoration: none;
}
nav a {
/* 不会生效 */
text-decoration: underline;
}
/* 使用新的第四级选择器 :where */
a:where(:not(:hover)) {
text-decoration: none;
}
nav a{
/* 生效 */
text-decoration: underline;
}
:has(reslativeSelector1,reslativeSelector2,...)
浏览器支持(在写这篇文章的时候,还没有任何一个浏览器支持这个选择器)
这个选择器将一个相对选择器列表作为参数。它选择那些在指定范围内匹配相对选择器列表的元素。
例如:
p:has(strong, em) {
color: red;
}
这会给所有的包含<strong>
或者<em>
元素的p
元素添加一个颜色为红色的样式。
这类选择器包含那些应用在元素属性上的选择器。
它选择那些foo
属性的值等于bar
的元素,忽略大小写。
例如:
p[class="text" i] {
color: green;
}
这会给所有的class
属性的值为Text
、TEXT
、或者是text
、又或者是其他组合的p
元素添加一个颜色为绿色的样式。
[foo="bar"s]
在我写这篇文章的时候,还没有任何一个浏览器支持这个选择器。
它选择那些foo
属性的值严格等于bar
的元素。
例如:
p[class="text" s]{
color: green;
}
这会给所有class
属性的值为text
(例如,不是Text
)的p
元素添加一个颜色为绿色的样式。
这类选择器包括那些使用语言相关设置的选择器。
:dir(ltr)
它选择那些具有从左到右方向性的元素,其中文档语言指定如何确定方向性。与之对应的::dir(rtl)
表示具有从右到左方向性的元素。其他值是无效的也不会匹配任何元素。
例如:
p:dir(rtl) {
background-color: red;
}
这会给设置了从左到右方向的p
元素添加一个背景色为红色的样式。
:lang(zh,"*-hant")
在我写这篇文章的时候,还没有任何一个浏览器支持这个第四级选择器。
它选择那些被标记为中文的元素(或者选择其他语言标记的元素,只需要把zh
改为其他的语言代码就可以了)或者用繁体字书写的元素(或者在其他字符的系统中,将-hant
替换为其它字符代码)。
实际上,第二级选择器里面就有这个选择器,但是在第四级选择器里面新增了通配符和逗号分隔列表。
它接收一个或多个以逗号为分隔的语言代码作为参数。如果语言代码里面包含*
,则必须对它们进行正确的转义(:lang(es-\*)
)或字符串引号(:lang("es-*")
)。
例如:
p:lang("*-CH") {
background-color: red;
}
这会给所有被标记为使用瑞士语言的p
元素添加一个背景色为红色的背景色。
这类选择器包括那些与超链接有关得选择器。
:any-link
在Can I use上并没有这个选择器的兼容性介绍,但是大多数主流浏览器都支持这个选择器。
它选择那些具有href
属性的元素(例如<a>或<link>
。或者说,所有与:link
或:visited
伪类匹配的元素。
例如:
a:any-link {
color: red;
}
这会给所有具有href
属性的的所有a
元素添加一个颜色为红色的样式。
:local-link
在我写这篇文章的时候,还没有任何一个浏览器支持这个第四级选择器。
它选择那些链接到当前 URL 的元素。如果链接的指向包括 URL 片段,则 URL 片段和和当前 URL 也必须要匹配。比如不匹配,则在比较中不考虑当前 URL 的片段 URL 部分。
例如:
a:local-link {
text-decoration: none;
}
这会给所有的具有href
属性的指向当前页面的a
元素(也许它们是导航菜单的一部分)去掉下划线。
这类选择器包括那些用户正在操作的元素的选择器。
:focus-within
它选择那些与:focus
伪类匹配的元素(当元素具有焦点时)或具有与:focus
匹配的子元素。
例如,使用如下的一个表单:
<from>
<input type="text" id="name" placeholder="Enter your name" />
</from>
当表单里的输入框获得焦点的时候,它会给表单添加一个边框。
:focus-visible
它选择一个处于焦点状态的元素(与:focus
伪类匹配),浏览器通常会为了让获得焦点的元素清晰可见,给它添加一个焦点环。
和:focus
的区别和细微。
如果:focus-visible
匹配了,:focus
一定会匹配,但是反过来就不一定了,它取决于浏览器(如果启用了聚焦环的绘制)和获取焦点的元素(通过鼠标点击或者 tab 键)。
Firefox 把这个选择器实现为:-moz-focusring
伪类。
例如,在某些场景下,有以下样式:
/* 标准第四级选择器 */
:focus-visible {
background-color: lightgary;
}
/* 基于 Firefox 的第四级选择器 */
:moz-focusring {
background-color: lightgary;
}
只有在元素通过 tab 获得焦点的时候才会生效。
这类选择包括那些应用于接受用户输入的元素的选择器。
:read-write
and :read-only
:read-write
选择可读可写的元素(例如那些不具有 readonly
属性的<input>
的元素)。
:read-only
选择那些只能读的元素(例如具有readonly
属性的<input>
的元素)。
然而,这些选择器不仅仅选择<input>
或<textarea>
元素,它们也可以选择用户能输入的任何选择,例如将contenteditable
属性设置为true
的<p>
元素。
例如:
:read-write {
background-color: lightyellow;
}
:read-only {
background-color: lightgary;
}
/* Firefox */
:-moz-read-write {
background-color: lightyellow;
}
:moz-read-only {
background-color: lightgary;
}
这会把页面上所有的不可编辑元素背景色设置为浅灰色,把可以编辑的元素的背景色设置为浅黄色。
:placeholder-shown
它选择一个当前正在显示占位符文本的输入框元素。
例如:
input:placeholder-shown {
color: red;
}
这会给当前显示占位符文本的input
元素设置一个颜色为红色的样式,注意这里设置的只是光标的颜色。
:default
它选择那些在一组相关元素里面作为默认选项的元素。通常用于按钮和选择列表 / 菜单。
例如:
input:default {
box-shadow: 0 0 2px 2px green;
}
input:defaut + lable {
color: green;
}
这会给默认<input>
元素的标签设置绿色阴影和颜色。
:indeterminate
它选择那些处于不确定状态的元素。例如,单选框组和复选框组里包含的那些未被选中的元素,或者进度百分比未知的进度条。
例如:
input[type="radio"]:indeterminate + label {
color: red;
}
如果组中还没有任何一个元素被选,。这会给这个组的标签添加一个颜色为红色的样式。
:valid
和 :invalid
它选择那些根据有效性语义或值的有效或无效的元素。如果没有定义值的有效性规则,则这个元素既不满足:valid
也不满足:invalid
。
例如:对于一个输入类型为email
的元素:
input:invalid {
color: red;
}
input:valid {
color: green;
}
这会根据元素里面输入的电子邮件是否有效为依据去为元素的文本设置不同颜色。
:in-range
和 :out-of-range
这些选择器只适用于那些具有范围限制的元素,例如,定义了最大最小值的元素。如果没有定义,则都不匹配。
例如:一个设置了最小只为 1 最大值为 5 的输入框。
input:out-of-range {
color: red;
}
input:in-range {
color: green;
}
这会给依据输入的值设置不同的颜色样式。
在某些情况下,某些选择器会具有与:valid
和:invalid
相同的效果。
:required
和:optional
这些选择器分别适用于在提交表单的之前必填的和选填的表单元素。那些非表单元素不会被匹配。
例如:
input:optional {
color: gray;
}
input:required {
color: red;
}
这会给那些必填的元素添加设置红色的样式,给非必填的元素添加灰色的样式。
这类选择器包括那些允许基于文档树中的信息进行选择,但是不能由其它选择器表示的选择器,例如元素相对于其父元素的位置。
:nth-child(n [of selector]?)
和 :nth-last-child(n [of selector]?)
浏览器支持(在我写这篇文章的时候,还没有任何一个浏览器支持这个第四级选择器。)
:nth-child
选择器匹配作为其父级的第 n 个子元素。:nth-last-child
也一样的,只不过它是从最后一个元素开始计数的。你可以使用:nth
测试来了解它们之间的区别,并进一步了解 An+B 表示法。
在这篇文章的开头,我举了一个:nth.last-child
的例子,我说这个第三级的选择器。然而,对于第四级选择器,这个选择器接收一个可选的of
选择器用来筛选仅与该选择器匹配的子元素。
除了功能更加强大之外,它的声明方式也可以不同。例如:
li.item:nth-child(-n+2)
选择前面两个具有item
类的<li>
元素。
它和以下的不同:
:nth-child(-n+2 of li.item)
这会选择前面两个带有item
类的<li>
元素即使它们并不是列表的前两个元素。
尝试一下(在支持该选择器的浏览器中,例如 Safari):
这类选择器包括使用表格的列的选择器。
F || E
在我写这篇文章的时候,还没有任何一个浏览器支持这个选择器。
列组合器选择器(||)选择类型 E 的元素,该元素表示表中的单元格并属于由类型 F 的元素表示的列。
例如,有如下表:
对于以下样式
col.selected || td {
background-color: lightyellow;
}
这会给所选列(价格)的单元格(<td>元素
)设置浅黄色的背景色。
:nth-col(An+B)
和:nth-last-col(An+B)
在我写这篇文章的时候,这个选择器还没有被任何一个浏览器支持。
你可以把这些选择器看作是:nth-child
和:nth-last-child
的列版本。
:nth-col(An+B)
选择网格或表格中的一个在该元素之间还有 An+B-1 个元素的元素(n 为 0 或任意一个整数)。
例如:
col.nth-col(1) {
background-color: lightyellow;
}
col.nth-last-col(1) {
background-color: lightgreen;
}
这会给表格的第一列元素添加一个浅黄色背景色,。被最后一列元素添加一个浅绿色背景色。
第四级选择器使您可以轻松声明复杂的选择规则。
我们涵盖了2019年1月《编辑器草案》规范中定义的大多数选择器,但是,我遗漏了一些可能会很快消失或很快改变的选择器,或者关于它们的信息不多(除了不受支持) 任何浏览器):
:target-within
:scope
(不要将它和作用域样式混淆):blank
:user-invalid
但绝对要注意这些以及第4级选择器的其余部分。
原文地址: https://davidwalsh.name/javascript-tricks
原文作者: David Walsh
就像其他编程语言一样,JavaScript也有许多技巧可以完成简单或困难的任务。一些技巧广为人知,而一些技巧则足以让你大吃一惊,今天让我们来看看你可以开始使用的七个JavaScript技巧吧。
获取一系列唯一的值可能比你想象中的要简单:
var j = [...new Set([1, 2, 3, 3])]
>> [1, 2, 3]
是否需要从数组中过滤一些值,比如 false
,null
等,也许你可能不知道这个技巧:
f.filter(Boolean)
当你可以使用 {}
创建一个看起来是空的空对象,但是它仍然具备 __proto__
以及一些其他方法,但是有一种方法可以创建一个纯粹的空对象:
let dict = Object.create(null);
// dict.__proto__ === "undefined"
// No object properties exist until you add them
在 JavaScript 中合并多个对象的需求一直存在,但你不知道一个很好用的技巧:
const person = { name: 'David Walsh', gender: 'Male' };
const tools = { computer: 'Mac', editor: 'Atom' };
const attributes = { handsomeness: 'Extreme', hair: 'Brown', eyes: 'Blue' };
const summary = {...person, ...tools, ...attributes};
能够为函数参数设置默认值是JavaScript的一个很棒的补充,但是请查看这个技巧,要求为给定的参数传递值:
const isRequired = () => { throw new Error('param is required'); };
const hello = (name = isRequired()) => { console.log(`hello ${name}`) };
// This will throw an error because no name is provided
hello();
// This will also throw an error
hello(undefined);
// These are good!
hello(null);
hello('David');
解构是JavaScript的一个非常受欢迎的补充,但有时我们更喜欢用其他名称来引用这些属性,所以我们可以利用别名:
const obj = { x: 1 };
// Grabs obj.x as { x }
const { x } = obj;
// Grabs obj.x as { otherName }
const { x: otherName } = obj;
多年来,我们编写了大量的正则表达式来获取查询字符串值,但那些日子已经过去了,让我们来看看 URLSearchParams API
var urlParams = new URLSearchParams(window.location.search);
console.log(urlParams.has('post')); // true
console.log(urlParams.get('action')); // "edit"
console.log(urlParams.getAll('action')); // ["edit"]
console.log(urlParams.toString()); // "?post=1234&action=edit"
console.log(urlParams.append('active', '1')); // "?post=1234&action=edit&active=1"
多年来JavaScript已经发生了很大的变化,但是我最喜欢的JavaScript部分是我们所看到的语言改进的速度。
尽管JavaScript的动态不断变化,我们仍然需要采用一些不错的技巧;将这些技巧保存在工具箱中,以便在需要时使用,那么你最喜欢的JavaScript技巧是什么?
原文地址: https://areknawo.com/getting-creative-with-the-console-api/
Javascript 中进行调试总是与 Console API 有密不可分的关系。但是往往我们只用过 console.log()
。那么,你知道我们还可以用其他用法吗?使用console.log()
的时候,它的大段式输出是否曾困扰过你?你想让你的调试信息更美观吗?💅如果有这个想法的话,接着往下看,我们将发现 Console API 会有多美观和有趣!
别不信,console.log()
自身就已经有了一些你不知道的附加功能。当然,它基础的功能 - 打印日志 - 不会受到影响。我们唯一能做的事情就是将它输出的日志变漂亮。让我们尝试一下,好吗?😁
唯一一件和 console.log()
方法密不可分的事情是你可以使用一个被称为 字符串替换 的方式来使用它。使用这种方式,你可以先在字符串中使用一个特殊的表达式,随后使用自定义的参数替换它。它看起来就像下面这样:
console.log("Object value: %o with string substitution",
{string: "str", number: 10});
12
是不是很棒?问题是这种字符串的替换方式有多种:
但是,即便这样,你可能还是会问,到底啥时候需要用到这个特性呢?特别是就像下面这样,你可以简单的传递多个参数给 log
函数:
console.log("Object value: ",
{string: "str", number: 10},
" with string substitution");
123
同时,对于字符串和数字来说,你可以就使用 模版字符串
!那么,到底啥时候可以用到这个特性呢?首先,我想说的是,当我们想打印一些漂亮的日志的时候,可能仅仅需要一些漂亮的字符串,字符串替换可以让你简单的实现这一需求。作为替代方案的上面的例子中,你必须认识到,你需要时刻小心翼翼的处理那些空格。但使用字符串替换的话,就方便多了。
As for string literals tho, they haven't been around as long as these subs (surprise! 🤯) and... they don't provide the same, nice formatting for objects. But yeah, as long as you're working with numbers and strings only, you may prefer a different approach.
还有一种我们之前没有学过的类似于字符串替换的命令。这个命令是 %c,它允许你为打印的消息加上样式。让我们来看一下如何使用:
console.log("Example %cCSS-styled%c %clog!",
"color: red; font-family: monoscope;",
"", "color: green; font-size: large; font-weight: bold");
123
以上的例子被广泛的使用。如你所见,紧跟在指令后的所有东西都被加上了样式。跳出上一个样式的方式是使用另一个指令,就像我们在这里用的一样。如果你想使用普通的、没有样式的日志格式,你需要传递一个空的字符串。还有一点显而易见的是,传递给指令的参数总是按照顺序一条一条的生效的。
我们仅仅只是开始,上面已经介绍了使用带样式的日志。哇,Console API 还有哪些秘密呢?
使用太多的日志可能不是非常好,这有可能导致不易读的和无意义的日志。有结构总是好的。你可以使用console.group()
对他们进行合并。使用这个方法,你可以在控制台中创造一些深层次的、可折叠的结构 ---- 分组。这个方式不仅可以将日志隐藏,也可以将日志进行有组织的划分。还有一个console.groupCollapsed()
方法,在你需要将分组进行默认折叠的时候可以使用它。当然,分组是可以嵌套的。
You can also make your group have what-seems-like header-log, by passing a list of arguments to it (just like with console.log()
). Every console call done after invoking the group method, will find its place inside created group. To exit it, you have to use a special console.groupEnd()
method. Simple, right? 😁
console.group();
console.log("Inside 1st group");
console.group();
console.log("Inside 2nd group");
console.groupEnd();
console.groupEnd();
console.log("Outer scope");
1234567
我想你已经发现了,你可以复制粘贴上面的代码片段到你的控制台中以你想要的方式进行测试。
另一个有用的信息是,你可以通过 Console API 得到当前调用的过程(执行过程/堆栈记录)。正如你所知道的,就是一个代码执行(例如:函数执行链)到当前console.trace()
调用的列表,这就是我们所介绍的函数。这个信息在我们监测副作用或者检查代码的工作流程的时候会很有用。把下面的代码片段放到你的代码中,你就知道我所讲的东西了。
console.trace("Logging the way down here!");
1
你可能早已知道一些 Console API 的其他用法。我所讲的是这些可以往日志上添加一些附加信息的方法。在这之前,让我们先简单过一下它们的用法,好吗?
console.warn()
方法的行为和 console.log(就像大多数打日志的方法那样) 很像,但是,需要多说一句,它有自己的样式。 ⚠ 在大多数浏览器中,会是黄色同时会有一个警告标记。这个方法执行时的所有调用过程也会被默认返回,因此你可以快速定位出现警告(也可能是bug)的地方。
console.warn("This is a warning!");
1
console.error()
方法,和console.warn()
有着类似的调用堆栈的输出,也有着特殊的样式。它通常是红色配合一个表示错误的图标。❌ 它清楚地通知了用户有一些东西可能不那么正常。还有一个重要的事情,.error() 方法仅仅只是一个控制台输出,而没有任何附加的功能,比如说停止代码的执行(要想停止代码的执行,你需要抛出一个错误)。温馨提示,对于许多初学者而言,对于这一行为可能会有一些不确定。
console.error("This is an error!");
1
还有两个方法可用来增加日志的规范性。这两个方法是console.info()
和console.debug()
。🐞 这两个方法的输出不总是会有特殊的样式 -- 在某些浏览器中可能仅仅只是一个表示信息的图标。不管是这两个方法还是前面说到的方法,都可以让你更加明确的对日志信息进行分类。在不同的浏览器中(例如:我目前使用的基于 Chromiun 内核的这个),调试用的 GUI 工具可以让你选择特定的日志类型进行显示,例如错误、调试或者是消息信息。对于我们来说,多了一种组织信息输出的功能。
console.info("This is very informative!");
console.debug("Debugging a bug!");
12
还有一种特殊的 Console API ,它给你提供了一个打印有条件的日志的捷径。这个方法就是console.assert()
。和标准的console.log()
方法一样,可以接收无限数量的参数,不同的是,第一个参数必须是布尔值。如果它求值得到true
,将不会输出信息,否则将会得到一个错误的日志(就像 .error() 方法一样),日志的内容就是所有余下的参数。
console.assert(true, "This won't be logged!");
console.assert(false, "This will be logged!");
12
了解所有上面那些内容之后,你可能有了想把你的控制台信息展示得更简洁一点的欲望。没毛病!只需要使用console.clear()
方法就行,随后所有旧的调试信息就都没了。这是个很好的特性,甚至于很多浏览器的调试工具中都内置了这个功能的触发按钮。👍
Console API 甚至提供了一些和耗时相关的方法。⌚ 有了它们的帮助,你可以快速地对部分代码进行性能测试。正如我前面所说的,这个 API 很简单。先使用console.time()
方法,可以为这个方法传递一个可选的参数作为此次计时器的标签。随后计时器会在这个方法被调用的时候同时启动。然后你可以使用console.timeLog()
和console.timeEnd()
方法(也可以传递一个可选参数作为标签)去打印时间(毫秒为单位)或者结束计时器。
console.time();
// code snippet 1
console.timeLog(); // default: [time] ms
// code snippet 2
console.timeEnd(); // default: [time] ms
1234
当然,如果你正在做真正的性能测试,我建议你使用Performance API,这是专门为了性能测试而设计的
在你有很多日志输出以至于你不知道给定的代码被执行了多少次的时候 -- 你可能已经猜到了!也有一个 API 来做这个事情!console.count()
方法做了能做的最基础的事情 -- 它会计算它被调用的次数。你可以,理所当然的,传递一个可选的参数为给定的计数器提供一个标签(默认是 default)。你可以随后使用console.countReset()
方法重置一个选中的计数器。
console.count(); // default: 1
console.count(); // default: 2
console.count(); // default: 3
console.countReset();
console.count(); // default: 1
1234
从我个人的角度来看,我觉得这个特性没有太大用途,但是有这个方法听起来也挺不错的。可能仅仅只有我这么认为吧...
我认为这是最被低估的 Console API 的特性之一(甚至超过了之前提到的CSS 样式)。👏当你在调试和检查各种类型的对象的时候,输出真实的、有顺序的表格到控制台是一件非常非常有用的事情。没错,你真的能够在控制台中展示一个表格。所有你需要做的事情仅仅是通过调用console.table()
同时传递一个参数-比如说对象或者数组(基础数据类型还是以普通的方式进行输出,对象和数组则会以更小的结构打印出来)。试下下面的代码片段你就知道我在说啥了。
console.table([[0,1,2,3,4], [5,6,7,8,9]]);
1
说到控制台的艺术,不得不提ASCII!在image-to-ascii模块(在NPM中你可以找到它)的帮助下,你可以非常简单地将一个普通的图片转换成对应的 ASCII 码。🖼另外,这个模块还提供了非常多的自定义设置和选项来以你想要的方式生成输出。这里有一个简单的示例:
import imageToAscii from "image-to-ascii";
imageToAscii(
"https://d2vqpl3tx84ay5.cloudfront.net/500x/tumblr_lsus01g1ik1qies3uo1_400.png",
{
colored: false,
}, (err, converted) => {
console.log(err || converted);
});
123456789
使用上面的代码,你可以生成令人惊叹的 JS 日志,就像现在出现在你控制台中的这样。
在 CSS 样式的帮助下,就像内间距、背景等等,你也可以输出一个非常完美的图片到你的控制台!比如说,你可以看一下 console.image模块,这个模块也可以在NPM中找到,使用这个模块也能做到这个效果。但是,我觉得 ASCII 会更时尚一点。
正如你说见,你的日志和调试过程可以不用这么单调!有很多比console.log()
好得多的方式。使用在这篇文章中学到的知识,剩下的选择就出自你自己了!你可以保持传统的console.log()
和浏览器自带的格式化效果,也可以通过上文介绍的内容在你的控制台中增添一些新鲜的东西。总之,怎么开心怎么来吧,即便你正在解决一个难缠的bug!🐞
我希望你喜欢这篇文章,同时也希望能给你带来一些新的知识。请一定要将这篇文章分享给那些想把他们的控制台变得丰富多彩的人,也欢迎在文末留下你的意见和评论!最后,希望大家关注我的推特、Facebook,不妨在newsletter注册一个账号!再次,感谢阅读,希望下篇文章还能看到你!
原文作者: Flavio Copes
这篇文章的出发点是为了帮助前端开发者串联 ES6前后的 JavaScript 知识,并且可以快速了解 JavaScript 语言的最新进展。
JavaScript 在当下处于特权地位,因为它是唯一可以在浏览器中运行的语言,并且是被高度集成和优化过的。
JavaScript 在未来有着极好的发展空间,跟上它的变化不会比现在更加的困难。我的目标是让你能够快速且全面的了解这门语言可以使用的新内容。
ES2015
ES2016
ES2017
ES2018
ESNext
每当阅读 JavaScript 相关的文章时,我都会经常遇到如下术语: ES3, ES5, ES6, ES7, ES8, ES2015, ES2016, ES2017, ECMAScript 2017, ECMAScript 2016, ECMAScript 2015 等等,那么它们是指代的是什么?
它们都是指代一个名为 ECMAScript 的标准。
JavaScript 就是基于这个标准实现的,ECMAScript 经常缩写为 ES。
除了 JavaScript 以外,其它基于 ECMAScript 实现语言包括:
但是现在流传最广、影响最大的基于 ES 标准的语言实现无疑就是 JavaScript了
为啥要用这个奇怪的名字呢?Ecma International 是瑞士标准协会,负责制定国际标准。
JavaScript 被创建以后,经由 Netscape 和 Sun Microsystems 公司提交给欧洲计算机制造商协会进行标准化,被采纳的 ECMA-262 别名叫 ECMAScript。
This press release by Netscape and Sun Microsystems (the maker of Java) might help figure out the name choice, which might include legal and branding issues by Microsoft which was in the committee, according to Wikipedia.
IE9 之后微软的浏览器中就看不到对 JScript 这个命名的引用了,取而代之都统称为 JavaScript。
因此,截至201x,JavaScript 成为最流行的基于 ECMAScript 规范实现的语言。
目前的最新的 ECMAScript 版本是 ES2018。
于 2018 年 6 月发布。
TC39(Technical Committee 39)是一个推动 JavaScript 发展的委员会。
TC39的成员包括各个主流浏览器厂商以及业务与浏览器紧密相连的公司,其中包括 Mozilla,Google ,Facebook,Apple,Microsoft,Intel,PayPal,SalesForce等。
每个标准版本提案都必须经过四个不同的阶段,这里有详细的解释。
令我费解的是 ES 版本的命名依据有时根据迭代的版本号,有时却根据年份来进行命名。而这个命名的不确定性又使得人们更加容易混淆 JS/ES 这个两个概念😄。
在 ES2015 之前,ECMAScript 各个版本的命名规范通常与跟着标准的版本更新保持一致。因此,2009年 ECMAScript 规范更新以后的的正式版本是 ES5。
Why does this happen? During the process that led to ES2015, the name was changed from ES6 to ES2015, but since this was done late, people still referenced it as ES6, and the community has not left the edition naming behind — the world is still calling ES releases by edition number.
为什么会发生这一切?在ES2015诞生的过程中,名称由ES6更改为ES2015,但由于最终完成太晚,人们仍然称其为ES6,社区也没有将版本号完全抛之于后 — 世界仍然使用 ES 来定义版本号。
下图比较清晰的展示了版本号与年份的关联:
接下来,我们来深入了解 JavaScript 自 ES5 以来增加的特性。
ES2015 之前, var
是唯一可以用来声明变量的语句。
var a = 0
上面语句如果你遗漏了 var
,那么你会把这个值(0)赋给一个未声明的变量,其中声明和未声明变量之间存在一些差异。
在现代浏览器开启严格模式时,给未声明的变量赋值会抛出 ReferenceError 异常,在较老的浏览器(或者禁用严格模式)的情况下,未声明的变量在执行赋值操作时会隐式的变为全局对象的属性。
当你声明一个变量却没有进行初始化,那么它的值直到你对它进行赋值操作之前都是 undefined
。
var a //typeof a === 'undefined'
你可以对一个变量进行多次重新声明,并覆盖它:
var a = 1
var a = 2
你也可以在一条声明语句中一次声明多个变量:
var a = 1, b = 2
作用域是变量可访问的代码部分。
在函数之外用 var
声明的会分配给全局对象,这种变量可以在全局作用域中被访问到。而在函数内部声明的变量只能在函数局部作用域被访问到,这类似于函数参数。
在函数中定义的局部变量名如何跟全局变量重名,那么局部变量的优先级更高,在函数内无法访问到同名的全局变量。
需要注意的是,var
是没有块级作用域(标识符是一对花括号)的,但是 var
是有函数作用域的,所以在新创建的块级作用域或者是函数作用域里面声明变量会覆盖全局同名变量,因为 var
在这两种情况下没有创建新的作用域。
在函数内部,其中定义的任何变量在所有函数代码中都是可见的,因为JavaScript在执行代码之前实际上将所有变量都移到了顶层(被称为悬挂的东西)。
在函数的内部定义的变量在整个函数作用域中都是可见(可访问),即使变量是在函数体末尾被声明,但是仍然可以再函数体开头部分被引用,因为 JavaScript存在变量提升机制。为避免混淆,请在函数开头声明变量,养成良好的编码规范。
let
let
是ES2015中引入的新功能,它本质上是具有块级作用域的 var
。它可以被当前作用域(函数以及块级作用域)以及子级作用域访问到。
现代 JavaScript 开发者在 let
和 var
的选择中可能会更倾向于前者。
如果
let
看起来是一个很抽象的术语,当你阅读到let color = 'red'
这一段,因为使用let
定义了color 为红色,那么这一切就变的有意义了。
在任何函数之外用 let
声明变量,和 var
相反的是 它并不会创建全局变量。
const
使用变量 var
或 let
声明的变量可以被重新赋值。 使用 const
声明的变量一经初始化,它的值就永远不能再改变,即不可重新被赋值。
const a = 'test'
我们不能再为 a
进行赋值操作。然而,a
如果它是一个具有属性或者方法的对象,那么我们可以改变它的属性或者方法。
const
并不意味着具有不可变性,只是保证用 const
声明的变量的引用地址不被变更。
类似于 let
,const
也具有块级作用域。
现代 JavaScript 开发者在遇到不会进行二次赋值的变量声明时,应该尽量使用 const
。
箭头函数的引入极大的改变了代码的书写风格和一些工作机制。
在我看来,箭头函数很受开发者欢迎,现在很少在比较新的代码库中看到 function
关键字了,虽然它并未被废弃。
箭头函数看起来会更加的简洁,因为它允许你使用更短的语法来书写函数:
const myFunction = function() {
//...
}
到
const myFunction = () => {
//...
}
如果函数体中只包含一条语句,你甚至可以省略大括号并直接书写这条语句:
const myFunction = () => doSomething()
参数在括号中传递:
const myFunction = (param1, param2) => doSomething(param1, param2)
如果该函数只有一个参数,那么可以省略掉括号:
const myFunction = param => doSomething(param)
由于这种简短的语法,使得我们可以更便捷的使用比较简短的函数
箭头函数支持隐式返回:可以正常的 return
一个返回值但是可以不使用 return
关键字。
隐式返回只在函数体内只包含一条语句的情况下生效:
const myFunction = () => 'test'
myFunction() //'test'
需要注意的一种情况,当返回一个对象时,记得将大括号括在括号中以避免产生歧义,误将其(大括号)解析为函数体的大括号。
const myFunction = () => ({ value: 'test' })
myFunction() //{value: 'test'}
this
this
可能是一个很难掌握的概念,因为它会根据上下文而进行变化,并且会在不同的 JavaScript的模式(是否为严格模式)下表现出差异。
理解 this
这个概念对于箭头函数的使用很重要,因为与常规函数相比,箭头函数的表现非常不同。
对象的方法为常规函数时,方法中的this
指向这个对象,因此可以这样做:
const car = {
model: 'Fiesta',
manufacturer: 'Ford',
fullName: function() {
return `${this.manufacturer} ${this.model}`
}
}
执行 car.fullName()
会返回 "Ford Fiesta"
。
如果上述方法使用是是箭头函数,由于箭头中的 this
的作用域继承自执行上下文,箭头函数自身不绑定 this
,因此 this
的值将在调用堆栈中查找,因此在此代码 car.fullName()
中不会返回常规函数那样的结果,实际会返回字符串 "undefined undefined":
const car = {
model: 'Fiesta',
manufacturer: 'Ford',
fullName: () => {
return `${this.manufacturer} ${this.model}`
}
}
因此,箭头函数不适合作为对象方法。
同样,箭头函数也不适合使用在作为创建构造函数,因为在实例化对象时会抛出 TypeError
。
所以在不需要动态上下文时请使用常规函数。
当然,在事件监听器上使用箭头函数也会存在问题。因为 DOM 事件侦听器会自动将 this
与目标元素绑定,如果该事件处理程序的逻辑依赖 this
,那么需要常规函数:
const link = document.querySelector('#link')
link.addEventListener('click', () => {
// this === window
})
const link = document.querySelector('#link')
link.addEventListener('click', function() {
// this === link
})
JavaScript 实现继承的方式比较罕见:原型继承。原型继承虽然在我看来很棒,但与其它大多数流行的编程语言的继承实现机制不同,后者是基于类的。
因此 Java、Python 或其它语言的开发者很难理解原型继承的方式,因此 ECMAScript 委员会决定在原型继承之上实现 class 的语法糖,这样便于让其它基于类实现继承的语言的开发者更好的理解 JavaScript 代码。
注意:class 并没有对 JavaScript 底层做修改,你仍然可以直接访问对象原型。
如下是一个 class 的例子:
class Person {
constructor(name) {
this.name = name
}
hello() {
return 'Hello, I am ' + this.name + '.'
}
}
class 具有一个标识符,我们可以使用 new ClassIdentifier()
来创建一个对象实例。
初始化对象时,调用 constructor
方法,并将参数传递给此方法。
类声明语句中也可以增加类需要的一些原型方法。在这种情况下 hello
是 Person
类的一个原型方法,可以在这个类的对象实例上调用:
const flavio = new Person('Flavio')
flavio.hello()
一个子类可以 extend 另一个类,通过子类实例化出来的对象可以继承这两个类的所有方法。
如果子类中的方法与父类中的方法名重复,那么子类中的同名方法优先级更高:
class Programmer extends Person {
hello() {
return super.hello() + ' I am a programmer.'
}
}
const flavio = new Programmer('Flavio')
flavio.hello()
(上述代码会打印出:“Hello, I am Flavio. I am a programmer.”)
类没有显示的类变量声明,但你必须在初始化构造函数 constructor
中去初始化类成员变量。
在子类中,你可以通过调用super()
引用父类。
在类中,通常会把方法直接挂载到实例对象上,直接在实例对象上调用。
而静态方法则是直接使用类名来调用,而不是通过对象实例调用:
class Person {
static genericHello() {
return 'Hello'
}
}
Person.genericHello() //Hello
JavaScript 没有内置真正意义上的受保护的私有方法。
社区有解决方法,但我不会在这里做讲解。
你可以通过增加方法 前缀 get
或者 set
创建一个 getter 和 setter,getter 和 setter会在你去获取特定值或者修改特定值的时候执行 get
或者 set
内的相关方法。
class Person {
constructor(name) {
this._name = name
}
set name(value) {
this._name = value
}
get name() {
return this._name
}
}
如果你只有 getter,该属性无法被设置,并且设置此属性的操作都会被忽略:
class Person {
constructor(name) {
this._name = name
}
get name() {
return this._name
}
}
如果你只有一个 setter,则可以更改该值,但不能从外部访问它:
class Person {
constructor(name) {
this._name = name
}
set name(value) {
this._name = value
}
}
函数 doSomething
接收一个 param1
参数。
const doSomething = (param1) => {
}
我们可以给 param1 设定默认值,如果在调用函数时未传入参数,那么该参数自动设定未默认值。
const doSomething = (param1 = 'test') => {
}
当然,这种机制同样适用于多个参数:
const doSomething = (param1 = 'test', param2 = 'test2') => {
}
假如你的函数是一个具有特定属性的对象该怎么处理?
曾几何时,如果我们必须要取一个对象的特定属性值,为了做兼容处理(对象格式不正确),你必须在函数中添加一些代码:
const colorize = (options) => {
if (!options) {
options = {}
}
const color = ('color' in options) ? options.color : 'yellow'
...
}
通过解构,你可以给特定属性提供默认值,如此可以大大简化代码:
const colorize = ({ color = 'yellow' }) => {
...
}
如果在调用 colorize
函数时没有传递任何对象,我们同样可以得到一个默认对象作为参数以供使用:
const spin = ({ color = 'yellow' } = {}) => {
...
}
模板字符串不同于 ES5 以前的版本,你可以用新颖的方式使用字符串。
这个语法看起来非常简便,只需要使用一个反引号替换掉单引号或双引号:
const a_string = `something`
这个用法是独一无二的,因为它提供了许多普通字符串所没有的功能,如下:
下面让我们深入每个功能的细节。
在 ES6 标准之前,创建跨越两行的字符串只能在一行的结尾使用 '' 字符:
const string =
'first part \
second part'
这样使得你创建的字符串虽然跨越了两汉,但是渲染时仍然表现成一行:
first part second part
需要渲染为多行的话,需要在一行结尾添加 '\n',比如这样:
const string =
'first line\n \
second line'
或者
const string = 'first line\n' + 'second line'
模板字符串使得定义多行字符串变得更加简便。
一个模板字符串由一个反引号开始,你只需要按下回车键来创建新的一行,不需要插入特殊符号,最终的渲染效果如下所示:
const string = `Hey
this
string
is awesome!`
需要特别留意空格在这里是有特殊意义的,如果这样做的话:
const string = `First
Second`
那么它会创建出像下面的字符串:
First
Second
有一个简单的方法可以修复这个问题,只需要将第一行置为空,然后添加了右边的翻译好后调用一个 trim() 方法,就可以消除第一个字符前的所有空格:
const string = `
First
Second`.trim()
模板字符串提供了插入变量和表达式的便捷方法
你只需要使用 ${...} 语法
const var = 'test'
const string = `something ${var}` //something test
在 ${} 里面你可以加入任何东西,甚至是表达式:
const string = `something ${1 + 2 + 3}`
const string2 = `something ${foo() ? 'x' : 'y'}`
标记模板可能是一个听起来不太有用的功能,但它实际上被许多流行的库使用,如 Styled Components 、Apollo 、GraphQL客户端/服务器库,因此了解它的工作原理至关重要。
在 Styled Components 模板标签中用于定义CSS字符串
const Button = styled.button`
font-size: 1.5em;
background-color: black;
color: white;
`
在 Apollo 中,模板标签用于定义 GraphQL 查询模式:
const query = gql`
query {
...
}
`
上面两个例子中的styled.button
和gql
模板标签其实都是函数:
function gql(literals, ...expressions) {}
这个函数返回一个字符串,可以是任意类型的计算结果。
字面量
(literals)是一个包含了表达式插值的模板字面量的序列。
表达式
(expressions)包含了所有的插值。
举个例子:
const string = `something ${1 + 2 + 3}`
这个例子里面的字面量
是由2个部分组成的序列。第1部分就是something
,也就是第一个插值位置(${})之前的字符串,第2部分就是一个空字符串,从第1个插值结束的位置直到字符串的结束。
这个例子里面的表达式
就是只包含1个部分的序列,也就是6
。
举一个更复杂的例子:
const string = `something
another ${'x'}
new line ${1 + 2 + 3}
test`
这个例子里面的字面量
的序列里面,第1个部分是:
;`something
another `
第2部分是:
;`
new line `
第3部分是:
;`
test`
这个例子里面的表达式
包含了2个部分:x
和6
。
拿到了这些值的函数就可以对其做任意处理,这就是这个特性的威力所在。
比如最简单的处理就是字符串插值,把字面量
和表达式
拼接起来:
const interpolated = interpolate`I paid ${10}€`
插值
的过程就是:
function interpolate(literals, ...expressions) {
let string = ``
for (const [i, val] of expressions) {
string += literals[i] + val
}
string += literals[literals.length - 1]
return string
}
给定一个object,你可以抽取其中的一些值并且赋值给命名的变量:
const person = {
firstName: 'Tom',
lastName: 'Cruise',
actor: true,
age: 54, //made up
}
const {firstName: name, age} = person
name
和age
就包含了对应的值。
这个语法同样可以用到数组当中:
const a = [1,2,3,4,5]
const [first, second] = a
下面这个语句创建了3个新的变量,分别取的是数组a
的第0、1、4下标对应的值:
const [first, second, , , fifth] = a
ES2015赋予了对象字面量更大的威力。
原来的写法:
const something = 'y'
const x = {
something: something
}
新的写法:
const something = 'y'
const x = {
something
}
原型可以这样指定:
const anObject = { y: 'y' }
const x = {
__proto__: anObject
}
const anObject = { y: 'y', test: () => 'zoo' }
const x = {
__proto__: anObject,
test() {
return super.test() + 'x'
}
}
x.test() //zoox
const x = {
['a' + '_' + 'b']: 'z'
}
x.a_b //z
2009年的ES5引入了forEach()
循环,虽然很好用,但是它跟for
循环不一样,没法break。
ES2015引入了**for-of**
循环,就是在forEach
的基础上加上了break的功能:
//iterate over the value
for (const v of ['a', 'b', 'c']) {
console.log(v);
}
//get the index as well, using `entries()`
for (const [i, v] of ['a', 'b', 'c'].entries()) {
console.log(index) //index
console.log(value) //value
}
留意一下const
的使用。这个循环在每次迭代中都会创建一个新的作用域,所以我们可以使用const
来代替let
。
它跟for...in
的区别在于:
for...of
遍历属性值for...in
遍历属性名promise的一般定义: 它是一个代理,通过它可以最终得到一个值.
Promise是处理异步代码的一种方式,可以少写很多回调。
异步函数是建立在promise API上面的,所以理解Promise是一个基本的要求。
一个promise被调用的时候,首先它是处于pending状态。在promise处理的过程中,调用的函数(caller)可以继续执行,直到promise给出反馈。
此时,调用的函数等待的promise结果要么是resolved状态,要么是rejected状态。但是由于JavaScript是异步的,所以promise处理的过程中,函数会继续执行。
除了你的代码和第三方库的代码之外,promise在用在现代的Web API中,比如:
在现代的JavaScript中,不使用promise是不太可能的,所以我们来深入研究下promise吧。
Promise API暴露了一个Promise构造函数,可以通过new Promise()
来初始化:
let done = true
const isItDoneYet = new Promise((resolve, reject) => {
if (done) {
const workDone = 'Here is the thing I built'
resolve(workDone)
} else {
const why = 'Still working on something else'
reject(why)
}
})
promise会检查done
这个全局变量,如果为true,就返回一个resolved promise,否则就返回一个rejected promise。
通过resolve
和reject
,我们可以得到一个返回值,返回值可以是字符串也可以是对象。
上面讲了怎么创建一个promise,下面就讲怎么使用(consume)这个promise。
const isItDoneYet = new Promise()
//...
const checkIfItsDone = () => {
isItDoneYet
.then(ok => {
console.log(ok)
})
.catch(err => {
console.error(err)
})
}
运行checkIfItsDone()
方法时,会执行isItDoneYet()
这个promise,并且等待它resolve的时候使用then
回调,如果有错误,就用catch
回调来处理。
一个promise可以返回另一个promise,从而创建promise链条(chain)。
一个很好的例子就是Fetch API,它是基于XMLHttpRequest API的一个上层API,我们可以用它来获取资源,并且在获取到资源的时候链式执行一系列promise。
Fetch API是一个基于promise的机制,调用fetch()
相当于使用new Promise()
来声明我们自己的promise。
const status = response => {
if (response.status >= 200 && response.status < 300) {
return Promise.resolve(response)
}
return Promise.reject(new Error(response.statusText))
}
const json = response => response.json()
fetch('/todos.json')
.then(status)
.then(json)
.then(data => {
console.log('Request succeeded with JSON response', data)
})
.catch(error => {
console.log('Request failed', error)
})
在这个例子当中,我们调用fetch()
,从根目录的todos.json
文件中获取一系列的TODO项目,并且创建一个链式promise。
运行fetch()
方法会返回一个response,它包含很多属性,我们从中引用如下属性:
status
, 一个数值,表示HTTP状态码statusText
, 一个状态消息,当请求成功的时候返回OK
response
还有一个json()
方法,它返回一个promise,返回内容转换成JSON后的结果。
所以这些promise的调用过程就是:第一个promise执行一个我们定义的status()
方法,检查response status,判断是否一个成功的响应(status在200和299之间),如果不是成功的响应,就reject这个promise。
这个reject操作会导致整个链式promise跳过后面的所有promise直接到catch()
语句,打印Request failed
和错误消息。
如果这个promise成功了,它会调用我们定义的json()函数。因为前面的promise成功之后返回的response
对象,我们可以拿到并作为第2个promise的参数传入。
在这个例子里面,我们返回了JSON序列化的数据,所以第3个promise直接接收这个JSON:
.then((data) => {
console.log('Request succeeded with JSON response', data)
})
然后我们把它打印到console。
在上一节的的例子里面,我们有一个catch
接在链式promise后面。
当promise链中的任意一个出错或者reject的时候,就会直接跳到promise链后面最近的catch()
语句。
new Promise((resolve, reject) => {
throw new Error('Error')
}).catch(err => {
console.error(err)
})
// or
new Promise((resolve, reject) => {
reject('Error')
}).catch(err => {
console.error(err)
})
如果在catch()
里面抛出一个错误,你可以在后面接上第二个catch()
来处理这个错误,以此类推。
new Promise((resolve, reject) => {
throw new Error('Error')
})
.catch(err => {
throw new Error('Error')
})
.catch(err => {
console.error(err)
})
Promise.all()
如果你要同时完成不同的promise,可以用Promise.all()
来声明一系列的promise,然后当它们全部resolve的时候再执行一些操作。
例子:
const f1 = fetch('/something.json')
const f2 = fetch('/something2.json')
Promise.all([f1, f2])
.then(res => {
console.log('Array of results', res)
})
.catch(err => {
console.error(err)
})
结合ES2015的解构赋值语法,你可以这样写:
Promise.all([f1, f2]).then(([res1, res2]) => {
console.log('Results', res1, res2)
})
当然这不限于使用fetch
, 这适用于任何promise.
Promise.race()
Promise.race()
运行所有传递进去的promise,但是只要有其中一个resolve了,就会运行回调函数,并且只执行一次回调,回调的参数就是第一个resolve的promise返回的结果。
例子:
const promiseOne = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one')
})
const promiseTwo = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two')
})
Promise.race([promiseOne, promiseTwo]).then(result => {
console.log(result) // 'two'
})
ES Module是用于处理模块的ECMAScript标准。
虽然 Node.js 多年来一直使用 CommonJS标准,但浏览器却从未有过模块系统,因为模块系统的决策首先需要 ECMAScript 标准化后才由浏览器厂商去实施实现。
这个标准化已经完成在 ES2015中,浏览器也开始实施实现这个标准,大家试图保持一致,以相同的方式工作。现在 ES Module 可以在 Chrome Safari Edge 和 Firefox(从60版本开始) 中使用。
模块非常酷,他们可以让你封装各种各样的功能,同时将这些功能作为库暴露给其它 JavaScript 文件使用。
引入模块的语法:
import package from 'module-name'
CommonJS 则是这样使用:
const package = require('module-name')
一个模块是一个 JavaScript 文件,这个文件使用 export
关键字 导出 一个或多个值(对象、函数或者变量)。例如,下面这个模块提供了一个将字符串变成大写形式的函数:
uppercase.js
export default str => str.toUpperCase()
在这个例子中,这个模块定义了唯一一个 default export,因此可以是一个匿名函数。否则,需要一个名称来和其它 导出 做区分。
现在,任何其它的 JavaScript 模块 可以通过 import 导入 uppercase.js 的这个功能。
一个 HTML 页面可以通过使用了特殊的 type=module
属性的 <script>
标签添加一个模块。
<script type="module" src="index.js"></script>
注意: 这个模块导入的行为就像
*defer*
脚本加载一样。具体可以看 efficiently load JavaScript with defer and async
需要特别注意的是,任何通过 type="module"
载入的脚本会使用 严格模式 加载。
在这个例子中,uppercase.js
模块定义了一个 default export,因此当我们在导入它的时候,我们可以给他起一个任何我们喜欢的名字:
import toUpperCase from './uppercase.js'
同时我们可以这样使用它:
toUpperCase('test') //'TEST'
你也可以通过一个绝对路径来导入模块,下面是一个引用来自其它域底下定义的模块的例子:
import toUpperCase from 'https://flavio-es-modules-example.glitch.me/uppercase.js'
下面同样是一些合法的 import语法:
import { toUpperCase } from '/uppercase.js'
import { toUpperCase } from '../uppercase.js'
下面是错误的使用:
import { toUpperCase } from 'uppercase.js'
import { toUpperCase } from 'utils/uppercase.js'
因为这里既不是使用绝对地址,也不是使用的相对地址。
我们了解了上面的例子:
export default str => str.toUpperCase()
这里生成了一个 default export。然而,你可以通过下面的语法在一个文件里面 导出 多个功能:
const a = 1
const b = 2
const c = 3
export { a, b, c }
另外一个模块可以使用下面的方式 import 导入所有:
import * from 'module'
你也可以通过解构赋值的方式仅仅 import 导出一部分:
import { a } from 'module'
import { a, b } from 'module'
为了方便,你还可以使用 as
重命名任何 import 的东西:
import { a, b as two } from 'module'
你可以导入模块中的默认出口以及通过名称导入任何非默认的出口:
import React, { Component } from 'react'
这是一篇关于 ES 模块的文章,可以看一下: https://glitch.com/edit/#!/flavio-es-modules-example?path=index.html
进行远程获取模块的时候是遵循 CORS 机制的。这意味着当你引用远程模块的时候,必须使用合法的 CORS 请求头来允许跨域访问(例如:Access-Control-Allow-Origin: *
)。
结合 type="module"
、nomodule
一起使用:
<script type="module" src="module.js"></script>
<script nomodule src="fallback.js"></script>
ES 模块是现代浏览器中的一大特性。这些特性是 ES6 规范中的一部分,要在浏览器中全部实现这些特性的路还很漫长。
我们现在就能使用它们!但是我们同样需要知道,有一些模块会对我们的页面性能产生性能影响。因为浏览器必须要在运行时执行它们。
Webpack 可能仍然会被大量使用,即使 ES 模块可以在浏览器中执行。但是语言内置这个特性对于客户端和 nodejs 在使用模块的时候是一种巨大的统一。
任何字符串有了一些实例方法:
repeat()
codePointAt()
根据指定的次数重复字符串:
'Ho'.repeat(3) //'HoHoHo'
没有提供参数以及使用 0
作为参数的时候返回空字符串。如果给一个负数参数则会得到一个 RangeError
的错误。
这个方法能用在处理那些需要 2 个 UTF-16 单元表示的字符上。
使用 charCodeAt
的话,你需要先分别得到两个 UTF-16 的编码然后结合它们。但是使用 codePointAt()
你可以直接得到整个字符。
下面是一个例子,中文的 “𠮷” 是由两个 UTF-16 编码组合而成的:
"𠮷".charCodeAt(0).toString(16) //d842
"𠮷".charCodeAt(1).toString(16) //dfb7
如果你将两个 unicode 字符组合起来:
"\ud842\udfb7" //"𠮷"
你也可以用 codePointAt()
得到同样的结果:
"𠮷".codePointAt(0) //20bb7
如果你将得到的 unicode 编码组合起来:
"\u{20bb7}" //"𠮷"
更多关于 Unicode 的使用方法,参考我的Unicode guide。
ES2015 在 Object 类下引入了一些静态方法:
Object.is()
确定两个值是不是同一个Object.assign()
用来浅拷贝一个对象Object.setPrototypeOf
设置一个对象的原型这个方法用来帮助比较对象的值:
使用方式:
Object.is(a, b)
返回值在下列情况之外一直是 false
:
a
和 b
是同一个对象a
和 b
是相等的字符串(用同样的字符组合在一起的字符串是相等的)a
和 b
是相等的数字a
和 b
都是 undefined
, null
, NaN
, true
或者都是 false
0
和 -0
在 JavaScript 里面是不同的值, 所以对这种情况要多加小心(例如在比较之前,使用 +
一元操作符将所有值转换成 +0
)。
在 ES2015
版本中引入,这个方法拷贝所有给出的对象中的可枚举的自身属性到另一个对象中。
这个 API 的基本用法是创建一个对象的浅拷贝。
const copied = Object.assign({}, original)
作为浅拷贝,值会被复制,对象则是拷贝其引用(不是对象本身),因此当你修改了源对象的一个属性值,这个修改也会在拷贝出的对象中生效,因为内部引用的对象是相同的。:
const original = {
name: 'Fiesta',
car: {
color: 'blue'
}
}
const copied = Object.assign({}, original)
original.name = 'Focus'
original.car.color = 'yellow'
copied.name //Fiesta
copied.car.color //yellow
我之前提到过,源对象可以是一个或者多个
:
const wisePerson = {
isWise: true
}
const foolishPerson = {
isFoolish: true
}
const wiseAndFoolishPerson = Object.assign({}, wisePerson, foolishPerson)
console.log(wiseAndFoolishPerson) //{ isWise: true, isFoolish: true }
设置一个对象的原型。可以接受两个参数:对象以及原型。
使用方法:
Object.setPrototypeOf(object, prototype)
例子:
const animal = {
isAnimal: true
}
const mammal = {
isMammal: true
}
mammal.__proto__ = animal
mammal.isAnimal //true
const dog = Object.create(animal)
dog.isAnimal //true
console.log(dog.isMammal) //undefined
Object.setPrototypeOf(dog, mammal)
dog.isAnimal //true
dog.isMammal //true
你可以展开一个数组、一个对象甚至是一个字符串,通过使用展开操作符 ...
。
让我们以数组来举例,给出:
const a = [1, 2, 3]
你可以使用下面的方式创建出一个新的数组:
const b = [...a, 4, 5, 6]
你也可以像下面这样创建一个数组的拷贝:
const c = [...a]
这中方式对于对象仍然有效。使用下面的方式克隆一个对象:
const newObj = { ...oldObj }
用在字符串上的时候,展开操作符会以字符串中的每一个字符创建一个数组:
const hey = 'hey'
const arrayized = [...hey] // ['h', 'e', 'y']
这个操作符有一些非常有用的应用。其中最重要的一点就是以一种非常简单的方式使用数组作为函数参数的能力:
const f = (foo, bar) => {}
const a = [1, 2]
f(...a)
(在之前的语法规范中,你只能通过 f.apply(null, a)
的方式来实现,但是这种方式不是很友好和易读。)
剩余参数(rest element)在和数组解构(array destructuring)搭配使用的时候非常有用。
const numbers = [1, 2, 3, 4, 5]
[first, second, ...others] = numbers
下面是展开元素 (spread elements):
const numbers = [1, 2, 3, 4, 5]
const sum = (a, b, c, d, e) => a + b + c + d + e
const sum = sum(...numbers)
ES2018 引入了 剩余属性 ,同样的操作符但是只能用在对象上。
剩余属性(Rest properties):
const { first, second, ...others } = {
first: 1,
second: 2,
third: 3,
fourth: 4,
fifth: 5
}
first // 1
second // 2
others // { third: 3, fourth: 4, fifth: 5 }
属性展开(Spread properties)允许我们结合跟在 ...
操作符之后对象的属性:
const items = { first, second, ...others }
items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
一个 Set 数据结构允许我们在一个容器里面增加数据。
一个 Set 是一个对象或者基础数据类型(strings、numbers或者booleans)的集合,你可以将它看作是一个 Map,其中值作为映射键,map 值始终为 true。
Set 可以通过下面的方式初始化:
const s = new Set()
你可以使用 add
方法向 Set 中添加项:
s.add('one')
s.add('two')
Set 仅会存贮唯一的元素,因此多次调用 s.add('one')
不会重复添加新的元素。
你不可以同时向 set 中加入多个元素。你需要多次调用 add()
方法。
我们可以通过下面的方式检查元素是否在 set 中:
s.has('one') //true
s.has('three') //false
使用 delete()
方法:
s.delete('one')
使用 size
属性:
s.size
使用 clear()
方法:
s.clear()
使用 keys()
或者 values()
方法 - 它们等价于下面的代码:
for (const k of s.keys()) {
console.log(k)
}
for (const k of s.values()) {
console.log(k)
}
entries()
方法返回一个迭代器,你可以这样使用它:
const i = s.entries()
console.log(i.next())
调用 i.next()
将会以 { value, done = false }
对象的形式返回每一个元素,直到迭代结束,这时 done
是 true
。
你也可以调用 set 的 forEach()
方法:
s.forEach(v => console.log(v))
或者你就直接使用 for..of
循环吧:
for (const k of s) {
console.log(k)
}
你可以使用一些值初始化一个 set:
const s = new Set([1, 2, 3, 4])
const a = [...s.keys()]
// or
const a = [...s.values()]
一个 WeakSet 是一个特殊的 Set.
在 set 中,元素不会被 gc(垃圾回收)。一个 weakSet 让它的所有元素都是可以被 gc 的。weakSet 中的每个键都是一个对象。当这个对象的引用消失的时候,对应的值就可以被 gc 了。
下面是主要的不同点:
一个 weakSet 通常是在框架级别的代码中使用,仅仅暴露了下面的方法:
一份map结构的数据允许我们建立数据和key的关系
在引入Map之前,开发者通常把对象(Object)当Map使用,把某个object或value值与指定的key进行关联:
const car = {}
car['color'] = 'red'
car.owner = 'Flavio'
console.log(car['color']) //red
console.log(car.color) //red
console.log(car.owner) //Flavio
console.log(car['owner']) //Flavio
ES6引入了Map数据结构,它为我们处理这种数据结构提供了一种合适的工具
Map的初始化:
const m = new Map()
你可以通过set()
方法把条目设定到map中:
m.set('color', 'red')
m.set('age', 2)
你可以通过get()
方法从map中取出条目:
const color = m.get('color')
const age = m.get('age')
使用delete()
方法:
m.delete('color')
使用clear()
方法:
m.clear()
使用has()
方法
const hasColor = m.has('color')
使用 size
属性:
const size = m.size
你可以用一组value来初始化一个map:
const m = new Map([['color', 'red'], ['owner', 'Flavio'], ['age', 2]])
任何值(对象,数组,字符串,数字)都可以作为一个map的value值(使用key-value键值的形式),任何值也可以用作key,即使是object对象。
如果你想通过get()
方法从map中获取不存在的key,它将会返回undefined
const m = new Map()
m.set(NaN, 'test')
m.get(NaN) //test
const m = new Map()
m.set(+0, 'test')
m.get(-0) //test
Map提供了keys()
方法,通过该方法我们可以迭代出所有的key值:
for (const k of m.keys()) {
console.log(k)
}
Map提供了values()
方法,通过该方法我们可以迭代出所有的value值:
for (const v of m.values()) {
console.log(v)
}
Map提供了entries()
方法,通过该方法我们可以迭代出所有的键值对:
for (const [k, v] of m.entries()) {
console.log(k, v)
}
使用方法还可以简化为:
for (const [k, v] of m) {
console.log(k, v)
}
const a = [...m.keys()]
const a = [...m.values()]
WeakMap是一种特殊的Map
在一个map对象中,定义在其上数据永远不会被垃圾回收,WeakMap替而代之的是它允许在它上面定义的数据可以自由的被垃圾回收走,WeakMap的每一个key都是一个对象,当指向该对象的指针丢失,与之对应的value就会被垃圾回收走。
这是WeakMap的主要不同处:
WeakMap提供了如下几种方法,这些方法的使用和在Map中一样:
get(k)
set(k, v)
has(k)
delete(k)
关于WeakMap的用例不如Map的用例那么明显,你可能永远也不会在哪里会用到它,但从实际出发,WeakMap可以构建不会干扰到垃圾回收机制的内存敏感性缓存,还可以满足封装的严谨性及信息的隐藏性需求。
Generators是一种特殊的函数,它能够暂停自身的执行并在一段时间后再继续运行,从而允许其它的代码在此期间运行(有关该主题的详细说明,请参阅完整的“javascript生成器指南”)。
Generators的代码决定它必须等待,因此它允许队列中的其它代码运行,并保留“当它等待的事情”完成时恢复其操作的权力。
所有这一切都是通过一个简单的关键字“yield`”完成的。当生成器包含该关键字时,将停止执行。
generator生成器可以包含许多yield
关键字,从而使自己能多次停止运行,它是由*function
关键字标识(不要将其与C、C++或Go等低级语言中使用的取消指针引用操作符混淆)。
Generators支持JavaScript中全新的编程范式,包括:
这里有一个解释generator如何工作的例子:
function *calculator(input) {
var doubleThat = 2 * (yield (input / 2))
var another = yield (doubleThat)
return (input * doubleThat * another)
}
我们先初始化它:
const calc = calculator(10)
然后我们在generator中开始进行iterator迭代:
calc.next()
第一个迭代器开始了迭代,代码返回如下object对象:
{
done: false
value: 5
}
具体过程如下:代码运行了函数,并把input=10
传入到生成器构造函数中,该函数一直运行直到抵达yield
,并返回yield
输出的内容: input / 2 = 5
,因此,我们得到的值为5,并告知迭代器还没有done(函数只是暂停了)。
在第二个迭代处,我们输入7:
calc.next(7)
然后我们得到了结果:
{
done: false
value: 14
}
7被作为doubleThat
的值,注意:你可能会把input/2
作为输入参数,但这只是第一次迭代的返回值。现在我们忽略它,使用新的输入值7
,并将其乘以2.
然后,我们得到第二个yield的值,它返回doubleThat
,因此返回值为14
。
在下一个,也是最后一个迭代器,我们输入100
calc.next(100)
这样我们得到:
{
done: true
value: 14000
}
当迭代器完成时(没有更多的yield关键字),我们返回input * doubleThat * another
,这相当于10 * 14 * 100
。
这些都是在2015年的ES2015引入的特性,现在我们深入了解下ES2016,它的作用域范围更小。
该特性引入了一种更简洁的语法,同来检查数组中是否包含指定元素。
对于ES6及更低版本,想要检查数组中是否包含指定元素,你不得不使用indexOf
方法,它检查数组中的索引,如果元素不存在,它返回-1
,由于-1
被计算为true,你需对其进行取反操作,例子如下:
if (![1,2].indexOf(3)) {
console.log('Not found')
}
通过ES7引入的新特性,我们可以如此做:
if (![1,2].includes(3)) {
console.log('Not found')
}
求幂运算符**
相当于Math.pow()
方法,但是它不是一个函数库,而是一种语言机制:
Math.pow(4, 2) == 4 ** 2
对于需要进行密集数学运算的程序来说,这个特性是个很好的增强,在很多语言中,**
运算符都是标准(包括Python、Ruby、MATLAB、Perl等其它多种语言)。
这些都是2016年引入的特性,现在让我们进入2017年。
字符串填充的目的是给字符串添加字符,以使其达到指定长度。
ES2017引入了两个String
方法:padStart()
和padEnd()
。
padStart(targetLength [, padString])
padEnd(targetLength [, padString])
使用例子:
该方法返回一个数组,数组包含了对象自己的所有属性,使用如下:
const person = { name: 'Fred', age: 87 }
Object.values(person) // ['Fred', 87]
Object.values()
也可以作用于数组:
const people = ['Fred', 'Tony']
Object.values(people) // ['Fred', 'Tony']
该方法返回一个数组,数组包含了对象自己的所有属性键值对,是一个[key, value]
形式的数组,使用如下:
const person = { name: 'Fred', age: 87 }
Object.entries(person) // [['name', 'Fred'], ['age', 87]]
Object.entries()
也可以作用于数组:
const people = ['Fred', 'Tony']
Object.entries(people) // [['0', 'Fred'], ['1', 'Tony']]
该方法返回自己(非继承)的所有属性描述符,JavaScript中的任何对象都有一组属性,每个属性都有一个描述符,描述符是属性的一组属性(attributes),由以下部分组成:
Object.getOwnPropertyDescriptors(obj)
接受一个对象,并返回一个带有描述符集合的对象。
ES6给我们提供了Object.assign()
方法,它从一个一个或多个对象中复制所有可枚举的属性值,并返回一个新对象。
但是,这也存在着一个问题,因为它不能正确的复制一个具有非默认属性值的属性。
如果对象只有一个setter,那么它就不会正确的复制到一个新对象上,使用Object.assign()
进行如下操作:
const person1 = {
set name(newName) {
console.log(newName)
}
}
这将不会起作用:
const person2 = {}
Object.assign(person2, person1)
但这将会起作用:
const person3 = {}
Object.defineProperties(person3,
Object.getOwnPropertyDescriptors(person1))
通过一个简单的console控制台,你可以查看以下代码:
person1.name = 'x'
"x"
person2.name = 'x'
person3.name = 'x'
"x"
person2
没有setter,它没能复制进去,对象的浅复制限定也出现在**Object.create()**方法中。
该特性允许在函数定义时有尾逗号,在函数使用时可以有尾逗号:
const doSomething = (var1, var2,) => {
//...
}
doSomething('test2', 'test2',)
该改变将鼓励开发者停止“在一行开始时写逗号”的丑陋习惯
JavaScript在很短的时间内从回调函数进化到Promise函数(ES2015),并自从ES2017以来,异步JavaScript的async/wait语法变得更加简单。
异步函数是Promise和generator的结合,基本上,它是比Promise更高级的抽象,我再重复一般:async/await是基于Promise建立的
它减少了围绕promise的引用,并打破了Promise — “不要打断链式调用”的限制。
当Promise在ES2015中引入时,它的本意是来解决异步代码的问题,它也确实做到了,但在ES2015和ES2017间隔的这两年中,大家意识到:Promise不是解决问题的终极方案。
Promise是为了解决著名的回调地狱而被引入的,但它本身也带来了使用复杂性和语法复杂性。
Promise是很好的原生特性,围绕着它开发人员可以探索出更好的语法,因此当时机成熟后,我们得到了async函数
async函数使代码看起来像是同步函数一样,但其背后却是异步和非堵塞的。
一个async函数会返回一个promise,如下例:
const doSomethingAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 3000)
})
}
当你想要调用该函数时,你在前面加上了一个wait
,这样调用就会被停止,直到该promise进行resolve或reject,需注意的是:外层函数必须定义为async
,这是例子:
const doSomething = async () => {
console.log(await doSomethingAsync())
}
这是一个使用async/await进行异步函数的简单示例:
const doSomethingAsync = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 3000)
})
}
const doSomething = async () => {
console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')
上面的代码将会在浏览器的console中打印出如下结果:
Before
After
I did something //after 3s
将 async
关键字标记在任何函数上,意味着这个函数都将返回一个 Promise,即使这个函数没有显式的返回,它在内部也会返回一个 Promise,这就是下面这份代码有效的原因:
const aFunction = async () => {
return 'test'
}
aFunction().then(alert) // This will alert 'test'
下面的例子也一样:
const aFunction = async () => {
return Promise.resolve('test')
}
aFunction().then(alert) // This will alert 'test'
正如上述的例子,我们将它与普通回调函数或链式函数进行比较,我们的代码看起来非常的简单。
这是一个很简单的例子,当代码足够复杂时,它会产生更多的收益。
例如,使用 Promise 来获取 JSON 资源并解析它:
const getFirstUserData = () => {
return fetch('/users.json') // get users list
.then(response => response.json()) // parse JSON
.then(users => users[0]) // pick first user
.then(user => fetch(`/users/${user.name}`)) // get user data
.then(userResponse => response.json()) // parse JSON
}
getFirstUserData()
这是使用 async/await 实现相同功能的例子:
const getFirstUserData = async () => {
const response = await fetch('/users.json') // get users list
const users = await response.json() // parse JSON
const user = users[0] // pick first user
const userResponse = await fetch(`/users/${user.name}`) // get user data
const userData = await user.json() // parse JSON
return userData
}
getFirstUserData()
async 函数非常容易,并且它的语法比 Promise 更易读。
const promiseToDoSomething = () => {
return new Promise(resolve => {
setTimeout(() => resolve('I did something'), 10000)
})
}
const watchOverSomeoneDoingSomething = async () => {
const something = await promiseToDoSomething()
return something + ' and I watched'
}
const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
const something = await watchOverSomeoneDoingSomething()
return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
console.log(res)
})
打印结果:
I did something and I watched and I watched as well
调试 Promise 就很困难,因为调试器无法跨越异步代码,但调试 async/await 就非常的简单,调试器会像调试同步代码一样来处理它。
WebWorkers 可以在浏览器中创建多线程程序。
它们通过事件的方式来传递消息,从 ES2017 开始,你可以使用 SharedArrayBuffer
在每一个 Worker 中和它们的创建者之间共享内存数组.
由于不知道写入内存部分需要多长的周期来广播,因此在读取值时,任何类型的写入操作都会完成,Atomics
可以避免竞争条件的发生。
关于它的更多细节可以在proposal中找到。
这是 ES2017,接下来我将介绍 ES2018 的功能。
ES2015 引入了解构数组的方法,当你使用时:
const numbers = [1, 2, 3, 4, 5]
[first, second, ...others] = numbers
and 展开参数:
const numbers = [1, 2, 3, 4, 5]
const sum = (a, b, c, d, e) => a + b + c + d + e
const sum = sum(...numbers)
ES2018 为对象引入了同样的功能。
解构:
const { first, second, ...others } = { first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
first // 1
second // 2
others // { third: 3, fourth: 4, fifth: 5 }
展开属性 允许通过组合在展开运算符之后传递的对象属性而创建新对象:
const items = { first, second, ...others }
items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
for-await-of
允许你使用异步可迭代对象做为循环迭代:
for await (const line of readLines(filePath)) {
console.log(line)
}
因为它使用的了 await
,因此你只能在 async
函数中使用它。
当一个 Promise 是 fulfilled 时,它会一个接一个的调用 then。
如果在这个过程中发生了错误,则会跳过 then
而执行 catch
。
而 finally()
允许你运行一些代码,无论是成功还是失败:
fetch('file.json')
.then(data => data.json())
.catch(error => console.error(error))
.finally(() => console.log('finished'))
ES2018 对正则表达式引入了许多改进,这些都可以在 https://flaviocopes.com/javascript-regular-expressions/ 上找到。
以下是关于 ES2018 正则表达式改进的具体补充:
这是一个 lookahead: 你可以使用 ?=
来匹配字符串,后面跟随一个特定的字符串:
/Roger(?=Waters)/
/Roger(?= Waters)/.test('Roger is my dog') //false
/Roger(?= Waters)/.test('Roger is my dog and Roger Waters is a famous musician') //true
?!
可以执行逆操作,如果匹配的字符串是no而不是在此后跟随特定的子字符串的话:
/Roger(?!Waters)/
/Roger(?! Waters)/.test('Roger is my dog') //true
/Roger(?! Waters)/.test('Roger Waters is a famous musician') //false
Lookaheads 使用 ?=
Symbol,它们已经可以用了。
Lookbehinds, 是一个新功能使用?<=
.
/(?<=Roger) Waters/
/(?<=Roger) Waters/.test('Pink Waters is my dog') //false
/(?<=Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //true
如果一个 lookbehind 是否定,那么使用 ?>!
:
/(?<!Roger) Waters/
/(?<!Roger) Waters/.test('Pink Waters is my dog') //true
/(?<!Roger) Waters/.test('Roger is my dog and Roger Waters is a famous musician') //false
\p{…}
and \P{…}
在正则表达式模式中,你可以使用 \d
来匹配任意的数字,\s
来匹配任意不是空格的字符串,\w
来匹配任意字母数字字符串,以此类推。
This new feature extends this concept to all Unicode characters introducing \p{}
and is negation \P{}
.
这个新功能扩展了unicode字符,引入了 \p{}
来处理
任何 unicode 字符都有一组属性,例如 script
确认语言,ASCII
是一个布尔值用于检查 ASCII 字符。你可以将此属性方在() 中,正则表达式将来检查是否为真。
/^\p{ASCII}+$/u.test('abc') //✅
/^\p{ASCII}+$/u.test('ABC@') //✅
/^\p{ASCII}+$/u.test('ABC🙃') //❌
ASCII_Hex_Digit
是另一个布尔值,用于检查字符串是否包含有效的十六进制数字:
/^\p{ASCII_Hex_Digit}+$/u.test('0123456789ABCDEF') //✅
/^\p{ASCII_Hex_Digit}+$/u.test('h') //❌
此外,还有很多其它的属性。你可以在()中添加它们的名字来检查它们,包括 Uppercase
, Lowercase
, White_Space
, Alphabetic
, Emoji
等等:
/^\p{Lowercase}$/u.test('h') //✅
/^\p{Uppercase}$/u.test('H') //✅
/^\p{Emoji}+$/u.test('H') //❌
/^\p{Emoji}+$/u.test('🙃🙃') //✅
除了二进制属性外,你还可以检查任何 unicode 字符属性以匹配特定的值,在这个例子中,我检查字符串是用希腊语还是拉丁字母写的:
/^\p{Script=Greek}+$/u.test('ελληνικά') //✅
/^\p{Script=Latin}+$/u.test('hey') //✅
阅读https://github.com/tc39/proposal-regexp-unicode-property-escapes 获取使用所有属性的详细信息。
In ES2018 a capturing group can be assigned to a name, rather than just being assigned a slot in the result array:
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const result = re.exec('2015-01-02')
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';
s
flag for regular expressionsThe s
flag, short for single line, causes the .
to match new line characters as well. Without it, the dot matches regular characters but not the new line:
/hi.welcome/.test('hi\nwelcome') // false
/hi.welcome/s.test('hi\nwelcome') // true
什么是 ESNext ?
ESNext 是一个始终指向下一个版本 JavaScript 的名称。
当前的 ECMAScript 版本是 ES2018,它于2018年6月被发布。
历史上 JavaScript 标准化的版本都是在夏季被发布,因此我们可以预期 ECMAScript 2019 将于 2019 年的夏季被发布。
所以在编写本文时 ES2018 已经被发布,因此 ESNext 指的是 ES2019。
ECMAScript 标准的提案是分阶段组织的,第一到第三阶段属于功能性的孵化,第四阶段的功能才最终确定为新标准的一部分。
在编写本文时主要浏览器都实现了第四阶段大部分的功能,因此我将在本文中介绍它们。
其中一些变化主要在内部使用,但知道发生了什么这也很好。
第三阶段还有一些其它功能,可能会在接下来的几个月内升级到第四阶段,你可以在这个 Github 仓库中查看它们:https://github.com/tc39/proposals。
flat()
是一个新的数组实例方法,它可以将多维数组转化成一维数组。
例子:
['Dog', ['Sheep', 'Wolf']].flat()
//[ 'Dog', 'Sheep', 'Wolf' ]
默认情况下它只能将二维的数组转化成一维的数组,但你可以添加一个参数来确定要展开的级别,如果你将这个参数设置为 Infinity
那么它将展开无限的级别到一维数组:
['Dog', ['Sheep', ['Wolf']]].flat()
//[ 'Dog', 'Sheep', [ 'Wolf' ] ]
['Dog', ['Sheep', ['Wolf']]].flat(2)
//[ 'Dog', 'Sheep', 'Wolf' ]
['Dog', ['Sheep', ['Wolf']]].flat(Infinity)
//[ 'Dog', 'Sheep', 'Wolf' ]
如果你熟悉数组的 map
方法,那么你就知道使用它可以对数组的每个元素执行一个函数。
flatMap()
是一个新的数组实例方法,它将 flat()
和 map
结合了起来,当你期望在map
函数中做一些处理时这非常有用,同时又希望结果如同 flat
:
['My dog', 'is awesome'].map(words => words.split(' '))
//[ [ 'My', 'dog' ], [ 'is', 'awesome' ] ]
['My dog', 'is awesome'].flatMap(words => words.split(' '))
//[ 'My', 'dog', 'is', 'awesome' ]
有时候我们并不需要将参数绑定到 try/catch 中。
在以前我们不得不这样做:
try {
//...
} catch (e) {
//handle error
}
即使我们从来没有通过 e
来分析错误,但现在我们可以简单的省略它:
try {
//...
} catch {
//handle error
}
Objects have an entries()
method, since ES2017.
从 ES2017 开始 Object将有一个 entries()
方法。
它将返回一个包含所有对象自身属性的数组的数组,如[key, value]
:
const person = { name: 'Fred', age: 87 }
Object.entries(person) // [['name', 'Fred'], ['age', 87]]
ES2019 引入了一个新的 Object.fromEntries()
方法,它可以从上述的属性数组中创建一个新的对象:
const person = { name: 'Fred', age: 87 }
const entries = Object.entries(person)
const newPerson = Object.fromEntries(entries)
person !== newPerson //true
这些功能已经被 v8/Chrome 实现了近一年的时间,它将在 ES2019 中实现标准化。
trimStart()
删除字符串首部的空格并返回一个新的字符串:
'Testing'.trimStart() //'Testing'
' Testing'.trimStart() //'Testing'
' Testing '.trimStart() //'Testing '
'Testing'.trimStart() //'Testing'
trimEnd()
删除字符串尾部的空格并返回一个新的字符串:
'Testing'.trimEnd() //'Testing'
' Testing'.trimEnd() //' Testing'
' Testing '.trimEnd() //' Testing'
'Testing '.trimEnd() //'Testing'
现在你可以使用 description
来获取 Symbol 的值,而不必使用 toString()
方法:
const testSymbol = Symbol('Test')
testSymbol.description // 'Test'
在此之前 JSON 字符串中不允许使用分隔符(\u2028)和分隔符(\u2029)。
使用 JSON.parse 时,这些字符会导致一个 SyntaxError
错误,但现在它们可以正确的解析并如 JSON 标准定义的那样。
修复 JSON.stringify()
在处理 UTF-8 code points (U+D800 to U+DFFF)。
在修复之前,调用 JSON.stringify()
将返回格式错误的 Unicode 字符,如(a "�")。
现在你可以安全放心的使用 JSON.stringify()
转成字符串,也可以使用 JSON.parse()
将它转换回原始表示的形态。
函数总会有一个 toString
方法,它将返回一个包含函数代码的字符串。
ES2019 对返回值做了修改,以避免剥离注释和其它字符串(如:空格),将更准确的表示函数的定义。
If previously we had
以前也许我们这样过:
function /* this is bar */ bar () {}
当时的行为:
bar.toString() //'function bar() {}
现在的行为:
bar.toString(); // 'function /* this is bar */ bar () {}'
总结一下,我希望这篇文章可以帮助你了解一些最新的 JavaScript 以及我们在 2019 年即将看见的内容。
Click here to get a PDF / ePub / Mobi version of this post to read offline
原文地址: https://blog.resellerclub.com/varnish-cache-vs-nginx-cache-performance-comparison
原文作者: Sagar Kulkarni
翻译作者: Fedora
如果说每一个站点维护者有一个都想去实现的目标,那肯定是实现站点快速加载。毋庸置疑,网页的加载时间很大程度影响着用户的留存以及搜索引擎的排名。通过采取一系列的技术手段比如图片优化、内容网络分发(CDN)、使用正确的缓存服务器等。网站的维护者不断努力的减少页面的加载时间。虽然所有的措施都是对减少页面加载时间有益的,但是这篇文章主要关注缓存。Varnish 缓存 与 Nginx 缓存是两大比较重要且流行的缓存方案有助于提升网站的体验。
虽然这两种缓存方案都有各自的优势,但是一份详细的关于这两种方案特性的学习文档可以帮助你更好的选择那种缓存方案。
Varnish 缓存是一个前端 web 加速器有助于动态和内容繁多的站点应对高流量冲击。Varnish 是一个反向的 HTTP 代理,缓存来自服务器的静态和动态内容以此提高网站的访问体验。提升网站的访问速度是显而易见的。
Varnish 缓存也允许开发者编辑规则和设置策略通过 .vcl
(Varnish 配置语言)。简而言之,vcl 能够让网站的维护者直接通过 Varnish 处理网站的流量。这样你就能够完全的控制你想要缓存的内容和缓存的方式。领先的社交网站像 Facebook 、Twitter 或者是页面内容丰富的 Wikipedia,在这些相似的网站中,都使用了 Varnish 缓存来管理内容缓存。
以上简要的介绍了 Varnish ,现在我们来了解一下 Nginx 缓存的工作原理。
Nginx 正式发布于2004年,是一个开源的 web 服务器用来或服务代理。它通常提供邮件代理、反向代理、负载均衡、HTTP 缓存、微缓存等等。这些都能帮助减少加载时间和提升网站的体验。大部分高流量网站和多应用环境,都是使用 Nginx 缓存。众所周知,Nginx 在处理并发上是非常高效的。
在简单的熟悉了两种缓存方案之后,现在是时候进行深入的评估哪一种方案适合你的网站。
实际上,很难对 Varnish 和 Nginx 做一个比较。因为 Varnish 和 Nginx 基本上是相似的。它们都可以被用来做服务器的反向代理和负载均衡。但是如果我们深入了解他们背后的技术,我们会发现几处 Varnish 与 Nginx 在缓存上的表现几乎是对立的。
译文出自:闪电矿工翻译组
原文地址:Drawing Realistic Clouds with SVG and CSS
原文作者:Beau Jackson
仓库原文链接:Drawing Realistic Clouds with SVG and CSS
译者:sichenguo
希腊神话中有这样一个故事是讲述宙斯创造出来一个云女神涅斐勒,并且类似大多数的希腊神话一样的,这个故事非常的奇异且限制级。下面一个简短克制的版本。
我们能够知道的是: 涅斐勒是由宙斯以他自己美丽的妻子的形象创造的。一个凡人遇见涅斐勒,陷入爱河,并且他们一起有了一个孩子,确切的说是一个半人半马的婴儿。
很怪诞对吧,值得庆幸的是,在浏览器中创建云的过程要简单得多,而且风险要小得多。
(Demo)
最近,我发现开发者Yuan Chuan 已经实现了用代码生成逼真的云。对我来说,浏览器中的云这个概念一直如同希腊神话中的那边神秘。
让我们来看一下这个’画笔‘吧 (点这里),可以见到的是作者通过使用 box-shadow
作为包含两个滤镜的 <filter>
元素的补充实现了这个令人惊叹的‘云图’!
想要绘制出兼顾写实和精致的云图,需要搭配使用feTurbulence
和feDisplacementMap
这两个滤镜。这两个滤镜不仅可以具有强大且复杂的功能,并且还可以提供一些令人兴奋的特性(其中包含奥斯卡获奖算法))!当然,这些功能在浏览器‘引擎盖’下有着令人生畏的复杂性。
虽然这些 SVG 滤镜的物理特性超出了本文的范围,但 MDN 和 w3.org 上提供了大量文档供学习参考。另外还有 feTurbulence
和 feDisplacement
介绍。
对于本文,我们将专注于学习使用这些 SVG 滤镜来实现令人惊奇的效果。滤镜背后的实现算法并不在我们的研究范围内,就像艺术家虽然可以绘制出美丽的景观但却不用懂得油漆的分子结构。
而我们需要做的只是密切关注一小部分 SVG 属性,这些属性使得我们可以在浏览器中绘制逼真的云图。通过学习这些属性可以让我们在项目中按照自己的意愿更好的制作出特定的滤镜效果。
CSS 规则 box-shadow
的五个值得关注的属性:
box-shadow: <offsetX> <offsetY> <blurRadius> <spreadRadius> <color>;
让我们来增大这些值(可能会高于正常的开发者会做的),这样在视图的右下方就会有阴影出现。 !(Demo)
#cloud-square {
background: turquoise;
box-shadow: 200px 200px 50px 0px #000;
width: 180px;
height: 180px;
}
#cloud-circle {
background: coral;
border-radius: 50%;
box-shadow: 200px 200px 50px 0px #000;
width: 180px;
height: 180px;
}
你肯定表演或者见过过皮影戏吧?
Credit: Double-M
类似于通过改变手的形状来改变阴影的方式,我们的 HTML 中的“源形状”可以通过移动或者变形来同步影响其在浏览器中呈现的阴影的形状。box-shadow
复制原始图形的圆角效果。SVG 滤镜对于元素都以及元素的 shadow
的都会生效。
<svg width="0" height="0">
<filter id="filter">
<feTurbulence type="fractalNoise" baseFrequency=".01" numOctaves="10" />
<feDisplacementMap in="SourceGraphic" scale="10" />
</filter>
</svg>
到目前为止,上面的 SVG
标签不会被渲染出来的,因为我们没有定义任何视觉样式(更不用说零宽度、高度)。它的唯一目的是保持我们喂养的过滤器 SourceGraphic(也就是我们的<div>
)。我们的源<div>
和它的阴影都被滤波器独立地扭曲。
我们通过 ID
选择器(#cloud-circle
) 将下面的 CSS
规则添加到目标元素上:
#cloud-circle {
filter: url(#filter);
box-shadow: 200px 200px 50px 0px #fff;
}
好吧,添加上 SVG 滤镜依旧没能给我们带来什么惊喜。
别担心!这才仅仅只是开始,更有趣的还在后面。
使用这一属性进行的一些实验可以产生显著的效果。暂时,让我们保持它的 feTurbulence
不变,只调整 DisplacementMap
的 scale
属性。
随着 scale
的增加(每次增加 30 ),我们的源 <div>
开始扭曲变形且它的投下阴影更接近天空中云随机的形状。
<feDisplacementMap in="SourceGraphic" scale="180" />
The scale attribute incremented by values of 30. (Demo)
好的,通过改变 scale
,好像终于接近了正确云形状的的数值范围!让我们稍微改变下颜色,以便看起来更像一朵 云彩 ☁️。
body {
background: linear-gradient(165deg, #527785 0%, #7fb4c7 100%);
}
#cloud-circle {
width: 180px;
height: 180px;
background: #000;
border-radius: 50%;
filter: url(#filter);
box-shadow: 200px 200px 50px 0px #fff;
}
现在我们的图形越来越接近真实的云效果了!
下面的一组图像显示了 blur 对 box-shadow 效果的影响。下面的图形中 blur 的数组依次递增。
The cloud becomes "softer" as the blur value increases.
为了使我们的云带有一些积云效果,可以稍微扩大 <div>
的尺寸。
#cloud-circle {
width: 500px;
height: 275px;
background: #000;
border-radius: 50%;
filter: url(#filter);
box-shadow: 200px 200px 60px 0px #fff;
}
等等!我们已经扩大了源元素的尺寸,但是它现在已经开始影响我们的“云(白色阴影)”了。 很简单,只需要相距更远的地方投影遍可以解决这个遮挡问题。
而通过通过一些 CSS 定位很好地实现源元素的隐层。<body>
元素是云 div
元素的父元素,默认情况下是静态定位的。 将苏设置为就绝对定位就可以将不需要展示的 #cloud-circle
隐藏。
#cloud-circle {
width: 500px;
height: 275px;
background: #000;
border-radius: 50%;
filter: url(#filter);
box-shadow: 400px 400px 60px 0px #fff; /* Increase shadow offset */
position: absolute; /* Take the parent out of the document flow */
top: -320px; /* Move a little down */
left: -320px; /* Move a little right */
}
很棒,这样看起来我们的云图已经初步成型了。
平摊在屏幕之上的云彩,嗯,这应该是一个对我们绘制出的云图一个很好的描述,总是感觉还缺少点什么,我们应该还可以做的会更好的!
这就是我们想要的:
Credit: 图源:pcdazero
从这张照片中云的深度,质感和丰富程度来看,有一件事是清楚的:宙斯是上过艺术学校。至少,他肯定是阅读过通用组件设计原则的,这个原则阐述了一个通用性的概念:
[...]照明偏差在深度和自然的解释中起着重要作用,并且可以由设计师以各种方式操纵......使用明暗区域之间的对比度使得外观具有景深。
这段话为我们提供了一个对云图进行优化更加真实的提示。通过将不同形状,大小和颜色的图层堆叠在一起,我们可以在参考图像制造对比度中以来渲染云图。需要的只是使用 filter
制造多个图层。
<svg width="0" height="0">
<!-- Back Layer -->
<filter id="filter-back">
<feTurbulence
type="fractalNoise"
baseFrequency="0.012"
numOctaves="4"
/>
<feDisplacementMap in="SourceGraphic" scale="170" />
</filter>
<!-- Middle Layer -->
<filter id="filter-mid">
<feTurbulence
type="fractalNoise"
baseFrequency="0.012"
numOctaves="2"
/>
<feDisplacementMap in="SourceGraphic" scale="150" />
</filter>
<!-- Front Layer -->
<filter id="filter-front">
<feTurbulence
type="fractalNoise"
baseFrequency="0.012"
numOctaves="2"
/>
<feDisplacementMap in="SourceGraphic" scale="100" />
</filter>
</svg>
再加上图层可以提供更多的角度去探索 feTurbulence
以及 实现更多的功能。 我们选择使用更加平滑的 type
:fractalNoise
,然后将 numOctaves
设置为 6。
<feTurbulence type="fractalNoise" baseFrequency="n" numOctaves="6" />
现在,让我们将注意力集中在 baseFrequency
属性上。以下是我们在逐渐增加 baseFrequency
值的时得到的结果 :
值取两端的值时,图形都会趋向于圆形。区别就是越小,更模糊。值越大,图形就更加显得生硬。
像 turbulence
,噪音,频率和 octave
这样的词可能看起来很奇怪甚至令人困惑。但不要害怕!将滤镜的效果类比为声波实际上非常准确。我们可以将低频率(baseFrequency
=0.001)等同于低噪声和低频率,更高的 (baseFrequency
=0.1) 值则与更清晰的音调相对应。
我们可以看到,我们对积云效果的对应的 baseFrequency
值大概位于 0.005~0.01 范围之间。
增大 numOctaves
的值可以以极其细致的细节渲染图像。但是这需要大量计算,因此需要注意的是:更细致的渲染也可能带来的还有性能问题,所以如非必要,请将此值尽量控制在一定范围内。
The higher the value we put into numOctaves the more granular detail give to our cloud.
好消息就是,在此例中,我们的云图需要的细节并不需要使用过高的 numOctaves
值,如上图所示,numOctaves
为 4 或者 5 就可以得到不错的效果。
至于 seed
属性还可以再深入研究一下。但是,就我们的目的而言, seed
的作用可以对形状造成影响。
Perlin Noise 函数(上文提到
)会使用此值作为其随机数生成器的初始值。如果不设置该属性将默认 seed
为零。跟 baseFrequency 的区别就是,无论我们给出什么价值 seed
,都不需要担心性能损失。
不同的 seed 值产生不同的形状
上面的 GIF 展示了不同 seed
值时的效果。需要注意的是,每朵云都是分层的复合云。(虽然我调整了每个图层的属性,但我保持各自的 seed
值分布均匀。)
Credit: Brockenhexe
在这里,仔细观察参考图像,我将 3 个‘云’层(不透明度不同)叠加到一个 div
上。通过反复试验和调整 seed
参数值,最终得到的下面这个类似于照片中云的形状的效果图。
当然,如果我们认为我们绘制的这个云彩要比宙斯创造的女神还要漂亮,肯定就是狂妄自大了。
但是,随着我们对 CSS
和 SVG
滤镜的更多的了解,我们就越有能力创造出在视觉上令人惊叹的图形,比如下面这些:
在这篇文章中我们沉浸在 SVG
强大功能和复杂性的海洋中,虽然 SVG
滤镜通常看起来比较复杂且难以理解。
然而就好像A Single Div project这个项目和Diana Smith's painting techniques这个例子一样,有趣的和有创意的方法总是可以得到令人惊艳的效果。
我相信很多开发者都有能力制作出一个云彩出来,但是一个可以轻松制作出云彩的工具会更受欢迎。因此我开发了一个小工具做这件事。
原文作者: Per Harald Borgen
翻译作者: hanxiansen
中文标题:一行css代码搞定响应式布局
在这篇文章中,我将教你如何使用 CSS Grid 来创建一个超酷的图像网格图,它将根据屏幕的宽度来改变列的数量。最精彩的地方在于:所有的响应特性被添加到了一行 css 代码中。这意味着我们不必将 HTML 与丑陋的类名(如col-sm-4, col-md-8)混杂在一起,也不必为每个屏幕创建媒体查询。ok,让我们发车吧。
在本文中,我将继续使用我在第一篇 CSS Grid 布局教程文章中的网格布局。然后,我们将在文章末尾添加图片。下面是我们初始化网格的外观:
HTML 代码:
<div class="container">
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<div>6</div>
</div>
CSS 代码:
.container {
display: grid;
grid-template-columns: 100px 100px 100px;
grid-template-rows: 50px 50px;
}
注意: 示例中有一些基础的样式,但我在这里没有写出来,因为这对 CSS 网格布局没有任何影响
如果这段代码让你感到困惑,我建议你去好好读下我的这篇文章Learn CSS Grid in 5 minutes ,其中就详细的解释了布局的基础知识。
让我们让列开始具有自适应特性吧。
CSS 栅格布局带来了一个全新的值:fraction
单位,fraction
单位通常简写为fr
,它允许你根据需要将容器拆分为多个块。
让我们将每一列更改为一个 fraction 单位宽:
.container {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: 50px 50px;
}
结果是栅格布局将会把整个宽度分成三个 fraction,每列占据一个 fraction 单位,效果如下:
如果我们将grid-template-columns
的值更改为1fr 2fr 1fr
,第二列的宽度将会是其它两列的两倍。总宽现在是四个 fraction 单位,第二列占据两个 fraction 单位,其它列各占一个 fraction。效果如下:
总的来说,fraction 单位值将使你可以很容易的更改列的宽度。
然而,上面列子并没有给出我们想要的响应性,因为网格总是三列宽。我们希望网格能根据容器的宽度改变列的数量。要做到这一点,你必须学习如下三个概念:
首先我们学习repeat()
函数。这是一个强大的指定列和行的方法。让我们使用repeat()
函数来更改网格:
.container {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(2, 50px);
}
在上面代码中,repeat(3, 100px)
等于100px 100px 100px
。第一个参数指定行与列的数量,第二个参数指定它们的宽度,因此它将为我们提供与开始时完全相同的布局:
然后是auto-fit
。让我们跳过固定数量的列,将3替换为自适应数量:
.container {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fit, 100px);
grid-template-rows: repeat(2, 100px);
}
效果如下:
现在,栅格将会根据容器的宽度调整其数量。它会尝试在容器中容纳尽可能多的 100px 宽的列。但如果我们将所有列硬写为 100px,我们将永远没法获得所需的弹性,因为它们很难填充整个宽度。正如你在上图看到的,网格通常在右侧留有空白。
为了解决上述问题,我们需要minmax()
。我们将 100px 替换为 minmax(100px, 1fr)
,代码如下:
.container {
display: grid;
grid-gap: 5px;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
grid-template-rows: repeat(2, 100px);
}
请注意,所有响应都发生在一行 css 代码中
效果如下:
正如你所见,效果完美。minmax()
函数定义的范围大于或等于 min, 小于或等于 max。
因此,现在每列将至少为 100px。但如果有更多的可用空间,栅格布局将简单地将其均分给每列,因为这些列变成了 fraction 单位,而不是 100px。
最后一步是添加图片。这与 CSS Grid 布局无关,但让我们看下代码。
我们在每个网格中添加一个图片标签:
<div><img src="img/forest.jpg"/></div>
为了使图片适应于每个条目,我们将其宽、高设置为与条目本身一样,我们使用object-fit:cover
。这将使图片覆盖它的整个容器,根据需要,浏览器将会对其进行裁剪。
.container > div > img {
width: 100%;
height: 100%;
object-fit: cover;
}
ok!现在你已经了解了 CSS Grid 布局中最复杂的概念之一了,请给自己一个赞吧。
在结束本文前,我提下浏览器支持情况,在撰写本文时,全球77%的网站将支持 CSS Grid,而且比例还在逐步攀升。
我想想2018将是 CSS 网格布局的元年。它将获得突破,并成为前端开发者的必备技能,就像过去几年 CSS Flexbox 布局发生的情况一样。
原文地址: https://blog.bitsrc.io/understanding-the-for-of-loop-in-javascript-8aded97d7ef8
在JavaScript
的世界中,我们可以使用很多种循环表达式:
while
表达式do...while
表达式for
表达式for...in
表达式for...of
表达式所有这些表达式都有一个基本的功能:它们会重复一件事情直到一个具体的条件出现。
在这篇文章中,我们将深入 for...of
表达式,去了解它是如何工作的,以及在我们的应用中可以使用它来优化代码的地方。
for...of
是一种 for
表达式,用来迭代 iterables(iterable objects)
直到终止条件出现。
下面是一个基础的例子:
let arr = [2,4,6,8,10]
for(let a of arr) {
log(a)
}
// It logs:
// 2
// 4
// 6
// 8
// 10
使用比for
循环更好的代码,我们遍历了arr
数组。
let myname = "Nnamdi Chidume"
for (let a of myname) {
log(a)
}
// It logs:
// N
// n
// a
// m
// d
// i
//
// C
// h
// i
// d
// u
// m
// e
你知道如果我们使用for
循环,我们将必须使用数学和逻辑去判断何时我们将会达到myname
的末尾并且停止循环。但是正如你所见的,使用for...of
循环之后,我们将会避免这些烦人的事情。
for...of
有以下通用的使用方法:
for ( variable of iterable) {
//...
}
variable
- 保存每次迭代过程中的迭代对象的属性值
iterable
- 我们进行迭代的对象
在for...of
循环的定义中,我们说它是“迭代 iterables(iterable objects)”。这个定义告诉我们for...of
循环只能用于可迭代对象。
那么, 什么是可迭代的对象(iterables
)?
简单来说的话,可迭代对象(Iterables)是可以用于迭代的对象。在ES6
中增加了几个特性。这些特性包含了新的协议,其中就有迭代器(Iterator)协议和可迭代(Iterable)协议。
参考MDN
的描述,“可迭代协议允许JS
对象定义或者修改它们的迭代行为,比如哪些值可以被for...of
循环到。”同时“为了变成可迭代的,对象必须实现@@iterator
方法,这意味着这个对象(或者其原型链上的对象)必须包含一个可以使用Symbol.iterator
常量访问的@@iterator
的属性”
说人话就是,对于可以使用for...of
循环的对象来说,它必须是可迭代的,换句话就是它必须有@@iterator
属性。这就是可迭代协议。
所以当对象有@@iterator
属性的时候就可以被for...of
循环迭代,@@iterator
方法被for...of
调用,并且返回一个迭代器。
同时,迭代器协议定义了一种对象中的值如何返回的方式。一个迭代器对象必须实现next
方法,next 方法需要遵循以下准则:
举个例子:
const createIterator = function () {
var array = ['Nnamdi','Chidume']
return {
next: function() {
if(this.index == 0) {
this.index++
return { value: array[this.index], done: false }
}
if(this.index == 1) {
return { value: array[this.index], done: true }
}
},
index: 0
}
}
const iterator = createIterator()
log(iterator.next()) // Nnamdi
log(iterator.next()) // Chidume
基本上,@@iterator
方法回返回一个迭代器,for...of
循环正是使用这个迭代器去循环操作目标对象从而得到值。因此,如果一个对象没有@@iterator
方法同时这个返回值是一个迭代器,for...of
循环将不会生效。
const nonIterable = //...
for( let a of nonIterable) {
// ...
}
for( let a of nonIterable) {
^
TypeError: nonIterable is not iterable
内置的可迭代对象有有以下这些:
注意,Object 不是可迭代的。如果我们尝试使用for...of
去迭代对象的属性:
let obj {
firstname: "Nnamdi",
surname: "Chidume"
}
for(const a of obj) {
log(a)
}
将会抛出一个异常:
for(const a of obj) {
^
TypeError: obj is not iterable
我们可以用下面的方式检查一个对象是否可迭代:
const str = new String('Chidume');
log(typeof str[Symbol.iterator]);
function
看到了吧,打印的结果是function
, 这意味着@@iterator
属性存在,并且是函数类型。如果我们在 Object 上面进行尝试:
const obj = {
surname: "Chidume"
}
log(typeof obj[Symbol.iterator]);
undefined
哇!undefined
表示不存在。
数组是可迭代对象。
log(typeof new Array("Nnamdi", "Chidume")[Symbol.iterator]);
// function
这是我们可以对它使用for...of
循环的原因。
const arr = ["Chidume", "Nnamdi", "loves", "JS"]
for(const a of arr) {
log(a)
}
// It logs:
// Chidume
// Nnamdi
// loves
// JS
const arr = new Array("Chidume", "Nnamdi", "loves", "JS")
for(const a of arr) {
log(a)
}
// It logs:
// Chidume
// Nnamdi
// loves
// JS
字符串也是可迭代的。
const myname = "Chidume Nnamdi"
for(const a of myname) {
log(a)
}
// It logs:
// C
// h
// i
// d
// u
// m
// e
//
// N
// n
// a
// m
// d
// i
const str = new String("The Young")
for(const a of str) {
log(a)
}
// 打印结果是:
// T
// h
// e
//
// Y
// o
// u
// n
// g
const map = new Map([["surname", "Chidume"],["firstname","Nnamdi"]])
for(const a of map) {
log(a)
}
// 打印结果是:
// ["surname", "Chidume"]
// ["firstname","Nnamdi"]
for(const [key, value] of map) {
log(`key: ${key}, value: ${value}`)
}
// 打印结果是:
// key: surname, value: Chidume
// key: firstname, value: Nnamdi
const set = new Set(["Chidume","Nnamdi"])
for(const a of set) {
log(a)
}
// 打印结果是:
// Chidume
// Nnamdi
const typedarray = new Uint8Array([0xe8, 0xb4, 0xf8, 0xaa]);
for (const a of typedarray) {
log(a);
}
// 打印结果是:
// 232
// 180
// 248
// 170
arguments对象是可遍历的吗?我们先来看:
// testFunc.js
function testFunc(arg) {
log(typeof arguments[Symbol.iterator])
}
testFunc()
$ node testFunc
function
答案出来了。如果进一步探讨,arguments其实是IArguments类型的对象,而且实现了IArguments接口的class都有一个@@iterator属性,使得arguments对象可遍历。
// testFunc.js
function testFunc(arg) {
log(typeof arguments[Symbol.iterator])
for(const a of arguments) {
log(a)
}
}
testFunc("Chidume")
// It:
// Chidume
正如上一节那样,我们可以创建一个自定义的可以通过for..of
遍历的可遍历对象。
var obj = {}
obj[Symbol.iterator] = function() {
var array = ["Chidume", "Nnamdi"]
return {
next: function() {
let value = null
if (this.index == 0) {
value = array[this.index]
this.index++
return { value, done: false }
}
if (this.index == 1) {
value = array[this.index]
this.index++
return { value, done: false }
}
if (this.index == 2) {
return { done: true }
}
},
index: 0
}
};
这里创建了一个可遍历的obj
对象,通过[Symbol.iterator]
赋予它一个@@iterator
属性,然后创建一个返回遍历器的方法。
//...
return {
next: function() {...}
}
//...
记住,遍历器一定要有一个next()
方法。
在next方法里面,我们实现了在for...of遍历过程中会返回的值,这个过程很清晰。
Let’s test this our obj
against a for..of to see what will happen:
// customIterableTest.js
//...
for (let a of obj) {
log(a)
}
$ node customIterableTest
Chidume
Nnamdi
耶!成功了!
简单对象是不可遍历的,通过Object
得到的对象也是不可遍历的。
我们可以通过自定义遍历器把@@iterator添加到Object.prototype来实现这个目标。
Object.prototype[Symbol.iterator] = function() {
let properties = Object.keys(this)
let count = 0
let isdone = false
let next = () => {
let value = this[properties[count]]
if (count == properties.length) {
isdone = true
}
count++
return { done: isdone, value }
}
return { next }
}
properties
变量里面包含了通过调用Object.keys()
得到的object的所有属性。在next方法里面,我们只需要返回properties里面的每一个值,并且通过更新作为索引的count变量来获取下一个值。当count达到properties的长度的时候,就把done设为true,遍历就结束了。
用Object来测试一下:
let o = new Object()
o.s = "SK"
o.me = 'SKODA'
for (let a of o) {
log(a)
}
SK
SKODA
成功了!!!
用简单对象来测试一下:
let dd = {
shit: 900,
opp: 800
}
for (let a of dd) {
log(a)
}
900
800
也成功了!! :)
所以我们可以把这个添加到polyfill里面,然后就可以在app里面使用for...of来遍历对象了。
我们可以用for...of来遍历class的实例中的数据列表。
class Profiles {
constructor(profiles) {
this.profiles = profiles
}
}
const profiles = new Profiles([
{
firstname: "Nnamdi",
surname: "Chidume"
},
{
firstname: "Philip",
surname: "David"
}
])
Profiles类有一个profile
属性,包含一个用户列表。当我们需要在app中用for...of来展示这个列表的时候,如果这样做:
//...
for(const a of profiles) {
log(a)
}
显然是不行的。
for(const a of profiles) {
^
TypeError: profiles is not iterable
为了把profiles
变成可遍历,请记住以下规则:
@@iterator
属性。@@iterator
的方法一定要返回一个遍历器(iterator).iterator
一定要实现next()
方法。我们通过一个熟悉的常量[Symbol.iterator]
来定义这个@@iterator
class Profiles {
constructor(profiles) {
this.profiles = profiles
}
[Symbol.iterator]() {
let props = this.profiles
let propsLen = this.profiles.length
let count = 0
return {
next: function() {
if (count < propsLen) {
return { value: props[count++], done: false }
}
if (count == propsLen) {
return { done: true }
}
}
}
}
}
然后,如果我们这样运行:
//...
for(const a of profiles) {
log(a)
}
$ node profile.js
{ firstname: 'Nnamdi', surname: 'Chidume' }
{ firstname: 'Philip', surname: 'David' }
我们可以显示 profiles 的属性
ECMAScript 2018 引入了一个新的语法,可以循环遍历一个 Promise 数组,它就是 for-await-of
和新的 Symbol Symbol.asyncIterator
。
iterable 中的 Symbol.asyncIterator
函数需要返回一个返回 Promise 的迭代器。
const f = {
[Symbol.asyncIterator]() {
return new Promise(...)
}
}
[Symbol.iterator]
和 [Symbol.asyncIterator]
的区别在于前者返回的是 { value, done }
而后者返回的是一个 Promise
,只不过当 Promise resolve 的时候传入了 { value, done }
。
我们上面的那个 f 例子将如下所示:
const f = {
[Symbol.asyncIterator]() {
return {
next: function() {
if (this.index == 0) {
this.index++
return new Promise(res => res({ value: 900, done: false }))
}
return new Promise(res => res({ value: 1900, done: true }))
},
index: 0
}
}
}
这个 f
是可异步迭代的,你看它总是返回一个 Promise ,而只有在迭代时 Promise 的 resolve 才返回真正的值。
要遍历 f
,我们不能使用 for..of
而是要使用新的 for-await-of
,它看起来会是这样:
// ...
async function fAsyncLoop(){
for await (const _f of f) {
log(_f)
}
}
fAsyncLoop()
$ node fAsyncLoop.js
900
我们也可以使用 for-await-of
来循环遍历一个 Promise 数组:
const arrayOfPromises = [
new Promise(res => res("Nnamdi")),
new Promise(res => res("Chidume"))
]
async function arrayOfPromisesLoop(){
for await (const p of arrayOfPromises) {
log(p)
}
}
arrayOfPromisesLoop()
$ node arrayOfPromisesLoop.js
Nnamdi
Chidume
在这篇文章中我们深入研究了 for...of
循环,我们首先定义理解什么是 for...of
,然后看看什么是可迭代的。for...of
为我们节省了许多复杂性和逻辑,并有助于使我们的代码看起来很清晰易读,如果你还没有尝试过,我认为现在正是时候。
译文出自:闪电矿工翻译组
原文地址:11 JavaScript Animation Libraries For 2019
原文作者:JonathanSaring
仓库原文链接:11 JavaScript Animation Libraries For 2019
译者:sichenguo
https://blog.logrocket.com/truly-reactive-programming-with-svelte-3-0-321b49b75969/ Truly reactive programming with Svelte 3.0
https://blog.bitsrc.io/understanding-the-for-of-loop-in-javascript-8aded97d7ef8
一共612行,@Xixi20160512 @chenmf6 一人 250 行,剩下的以及校对我来处理。
https://arp242.net/jquery.html Why I'm still using jQuery in 2019
原文地址: https://css-tricks.com/using-svg/
原文作者: Chris Coyier
翻译作者: chenmf
SVG是一种向量图的图片格式,即可伸缩向量图(Scalable Vector Graphics),可以在Adobe Illustrator里面生成。在Web中使用SVG很简单,但是也有一些需要知道的事情。
可以在Adobe Illustrator里设计并且得到SVG。下面是一个站在椭圆上的奇异鸟:
留意到画板刚好贴着设计主体的边缘,画布的大小在SVG里面的重要性和在PNG和JPG里面是一样的。
然后可以直接在Adobe Illustrator里面保存成SVG文件。
保存的时候,可以在duihua对话框里面选择SVG选项。完整的参考可以看SVG 介绍。这里选SVG 1.1就可以了。
当点击'OK'或者'SVG Code...'的时候,就会打开文本编辑器,显示SVG的编码。
<img>
标签里面使用SVG如果把SVG保存成文件之后,可以直接在<img>
标签里面使用。
HTML
<img src="kiwi.svg" alt="Kiwi standing on oval">
在Illustrator里面,画板的大小是612px ✕ 502px:
这正是图片在页面中的大小。可以选择<img>
标签并且改变width
和height
来改变它的尺寸,就像PNG和JPG一样,比如:
在<img>
标签里面使用需要有浏览器支持。基本上在IE8以上和Android 2.3以上都可以用。
如果你想要在不支持的浏览器里面使用,可以这样:
<img>
的src
属性:jQuery
if (!Modernizr.svg) {
$(".logo img").attr("src", "images/logo.png");
}
HTML
<img src="image.svg" onerror="this.onerror=null; this.src='image.png'">
background-image
里面使用SVG可以在CSS的background-image
里面使用SVG。
HTML
<a href="/" class="logo">
Kiwi Corp
</a>
CSS
.logo {
display: block;
text-indent: -9999px;
width: 100px;
height: 82px;
background: url(kiwi.svg);
background-size: 100px 82px;
}
注意把background-size
设置成我们想要的尺寸,否则只能看到很大的原始SVG图片的左上角。这个尺寸设置成了跟原始尺寸保持宽高比,如果在不知道原始尺寸的情况下想要保持宽高比,可以把background-size
设置成contain
。
在background-image
里面使用SVG也需要看浏览器支持,基本上跟在<img>
中使用是一样的。
如果要在不支持的浏览器里面使用:
background-image
替换成一个支持的格式。它会在不支持SVG的情况下加上一个no-svg
的class,注意它也是只会发送一个图片的HTTP请求,不会发两个。CSS
.main-header {
background: url(logo.svg) no-repeat top left;
background-size: contain;
}
.no-svg .main-header {
background-image: url(logo.png);
}
background
),SVG的浏览器支持和多背景的很接近。CSS
body {
background: url(fallback.png);
background-image: url(image.svg), none;
}
<img>
和background-image
的问题在<img>
和background-image
里面使用SVG,没法利用CSS对SVG内部进行控制,所以接着看下面的两种其他方式。
在保存SVG的时候可以获取SVG的代码(也可以直接在文本编辑器里面打开SVG文件),直接把SVG的代码复制到HTML里面:
HTML
<body>
<!-- 把SVG的代码复制到这里就可以显示图片了 -->
</body>
这样做的好处是把图片的内容直接写在文档里面,不需要额外发送HTTP请求获取,它和使用Data URI的好处是一样的,坏处也一样:导致文档变得臃肿,难以抓取和缓存。
如果使用后端语言的话,可以获取文件并且插入到文档:
PHP
<?php echo file_get_contents("kiwi.svg"); ?>
说到PHP,这里用file_get_contents()
方法比较合适,而不是include()
和include_once()
,因为SVG有时候会以<?xml version="1.0" encoding="UTF-8"?>
开头,导致PHP编译有问题。
Adobe Illustrator里面导出的SVG可能不是最优的,会包含一些冗余信息,比如DOCTYPE和生成信息。我们可以进一步优化,减少体积。Peter Collingridge给出了在线的SVG 优化,把需要优化的SVG上传,然后下载新的就可以了。
如果你是硬核玩家,可以直接用这个NodeJS工具自己优化。
SVG的代码看起来是不是很像HTML?因为它们都是基于XML的。我们的SVG里面包含了两个元素:<ellipse>
和<path>
,可以直接在代码里面给它们加上class,就像HTML元素一样:
SVG
<svg ...>
<ellipse class="ground" .../>
<path class="kiwi" .../>
</svg>
然后就可以用特殊的SVG CSS来控制这些元素了。SVG元素有着特殊的CSS属性,比如它没有background-color
,而是用fill
,但是也可以使用一些其他的普通属性,比如:hover
CSS
.kiwi {
fill: #94d31b;
}
.kiwi:hover {
fill: #ace63c;
}
更厉害的是,SVG可以使用滤镜(filter),比如模糊滤镜。比如在SVG代码里面可以加上一个滤镜:
SVG
<svg ...>
...
<filter id="pictureFilter" >
<feGaussianBlur stdDeviation="5" />
</filter>
</svg>
然后可以在CSS里面使用这个滤镜
CSS
.ground:hover {
filter: url(#pictureFilter);
}
下面是一个完整的例子:
更多阅读:
内联SVG的浏览器支持看这里,基本和前面的一样。兼容的方法:
HTML
<svg> ... </svg>
<div class="fallback"></div>
CSS
.fallback {
display: none;
/* Make sure it's the same size as the SVG takes up */
}
.no-svg .fallback {
background-image: url(logo.png);
}
<object>
里面使用SVG如果想要通过CSS控制SVG,但是又想避免内联SVG的弊端,可以在<object>
里面使用SVG。
HTML
<object type="image/svg+xml" data="kiwi.svg" class="logo">
Kiwi Logo <!-- fallback image in CSS -->
</object>
同样可以使用Modernizr来兼容:
CSS
.no-svg .logo {
width: 200px;
height: 164px;
background-image: url(kiwi.png);
}
这种情况下,如果想要用CSS控制SVG,就不能用外部的样式或者文档里面的<style>
了,要用SVG文件内部的<style>
:
SVG
<svg ...>
<style>
/* SVG specific fancy CSS styling here */
</style>
...
</svg>
<object>
SVG里使用外部样式可以在SVG文件开头的<svg>
标签前面引入:
HTML
<?xml-stylesheet type="text/css" href="svg.css" ?>
如果把这个放在HTML里面,页面会崩溃没法渲染,如果把这个放在<img>
或者background-image
的SVG里面,页面不会崩溃,但是也不起作用。
还可以把SVG转换成Data URL,转换之后可能不止原来的文件大小,但是它很方便,因为不需要额外产生网络请求。
Mobilefish.com上面有一个base64在线转换器,可以转成base64编码,但是这种方式不太推荐。记得去掉换行:
它可以在上述的所有场景里面使用,除了内联SVG。
个人比较推荐这个在线转换器,因为转换之后可读性比较强。
<img>
里面HTML
<img src="data:image/svg+xml;base64,[data]">
CSS
.logo {
background: url("data:image/svg+xml;base64,[data]");
}
<object>
里面HTML
<object type="image/svg+xml" data="data:image/svg+xml;base64,[data]">
fallback
</object>
如果,SVG在base64编码之前有嵌套的<style>
,那么它依然可以在<object>
里面起作用。
上面的例子都是base64编码的,但是也不一定要转换成base64编码,实际上对于SVG最好不要转成base64编码。因为SVG的原始格式文本重复性比较高,gzip压缩效果更好。
HTML
<!-- base64 -->
data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL...
<!-- UTF-8, not encoded -->
data:image/svg+xml;charset=UTF-8,<svg ...> ... </svg>
<!-- UTF-8, optimized encoding for compatibility -->
data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://...'
<!-- Fully URL encoded ASCII -->
data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A//...
从CSS的角度来看比较易用,为每个icon生成一个class,不用CSS sprites。grunticon输入一个SVG/PNG文件的目录,然后输出对应的3种格式的CSS:SVG data url,png data url和一个引用普通的png图片的兼容性CSS文件。
一个PHP命令行工具,把SVG图片转换成CSS icon,支持图片优化和SASS输出。
原文地址: https://medium.com/flutter/bringing-flutter-to-the-web-904de05f0df0
原文作者: Kevin Moore
翻译作者: Fedora
用来构建漂亮、定制化应用的跨平台的 UI 框架 Flutter 现在已经支持 Web 开发了。我们很高兴推出了一个预览版的 SDK 可以让开发者直接使用 Flutter UI 和业务逻辑代码构建 web 应用在浏览器中运行起来。
自从去年第一个公测版本推出之后,开发者使用 Flutter 构建跨 IOS 和 Android 的应用。但是 Flutter 自始至终被设计成一个跨平台的 UI 框架包括 Windows ,Mac,Fuchsia 甚至是 Raspberry Pi(树莓派)。因为 Flutter 是由 Dart 编写的,里面包含一个生产环境的编译器来构建原生的代码和 JavaScript 代码,所以我们有一个坚实的基础。剩下的挑战就是替换基于 Skia-based 的图形引擎和文本渲染来适配 Web 平台。
要做到这些,我们需要提供:
虽然Flutter for web是一项正在进行中的工作,而且为了实现上述功能还有很多工作要做。我们已经推出一个预览版,所以开发者可以进行尝鲜并给我们反馈。
Flutter 在 Web 端的整体架构和移动端的架构差不多:
Flutter 核心层(上图绿色部分)在移动端和 Web 端是一样的。它提供了 Flutter UI 的高度抽象,包括动画,手势,基本的小部件,以及一套大部分应用需要的 Material 风格的部件。如果你已经在客户端开发中使用了 Flutter,那么你就会很快的在 Web 开发中上手。
神奇之处就是将这些概念(客户端层面的)编译到浏览器中。我们重新实现了 dart ui 库,原本是基于 Skia 引擎被用在客户端上,现在是基于 DOM 和 Canvas API。当你编译 Flutter 代码到 Web 端,你的应用包括 Flutter 核心库,Web 端的 dart ui 库,所有的用 Dart 语言写的代码都会被编译成 JavaScript 代码,能够运行在所有的现代化浏览器中。
我们正在认真考虑采纳 Web 核心的特性,像用 Flutter 的路由模型无缝衔接浏览器的 History 路由。我们还在与 Flutter 桌面终端 合作,来实现鼠标滚动,悬停和聚焦这些客户端开发中用不到的功能。
Flutter Web 项目聚焦的核心功能就是框架提供的丰富流畅的交互体验。基于 document 的 Web 端也能从 Flutter web 可视化中 收益。
这个预览版的核心库是现有 Flutter 核心库的一个临时分支。这让我们的工程师能够很快的实现 web 端功能而核心团队能够持续保持开发稳定的用于生产环境的工具。我们已经开始往主仓库合并部分支持浏览器端的代码。我们计划提供一个 Flutter 的工具包,里面的核心框架将为移动端,web,和其他平台提供支持。
我们计划的工作包括:
支持文本特性例如选中,复制和粘贴。
提供插件支持。像位置信息,摄像头,文件 API,我们希望提供一个简单的 API 将客户端和 web 端桥接起来。
对 PWA 提供开箱即用的技术支持。
将 web 开发所需要的工具集成到现有的 FLutter 脚手架和编辑器中。
能够用 DevTools 来调试 web 开发。
提高性能,浏览器支持以及无障碍的访问的能力
欢迎大家去 flutter.dev/web 查看例子,文档以及更多的资源。我们很期待你使用 Flutter 开发的 web 应用。
TODO
文件夹中原链接:https://en.wikipedia.org/wiki/Flutter_(software)
Flutter is an open-source mobile application development SDK created by Google. It is used to develop applications for Android and iOS, as well as being the primary method of creating applications for Google Fuchsia.[6]
The first version of Flutter was known as "Sky" and ran on the Android operating system......
原文地址: https://medium.com/free-code-camp/how-to-get-better-at-writing-css-a1732c32a72f
原文作者: Thomas Lombart
翻译作者: hanxiansen
中文标题:如何更优雅的编写CSS代码
直白的说:编写优秀的 css 代码可能是很痛苦的。很多程序员都不想从事 CSS 开发—我可以做任何事情,除了css以外。
当我在编写app时,css是我最不喜欢的部分,但你又不能逃避它,对吗?我的意思是,在专注于用户体验和设计上,我们不能跳过css这一部分。
当开始一个项目是,一切都很好。你有几个css选择器:.title input #app
, 很简单。
但是,当你的app变得越来越大时,它开始变得糟糕起来。你会对css的选择器感到困惑,你发现自己把类似 div#app .list li.item a
的css代码编写一遍又一遍,你把所有的css代码放在文件末尾,因为你根本不在乎糟糕的css代码,因为:500行css代码完全无法维护。
我今天的目的是:让你更好的编写css代码。我想让你看看你以前的项目代码,然后想:哦,天哪,我写了些神马玩意儿啊。
好吧,你可能会想,你说得有道理,但不是有css框架吗?是的,这就是框架所表达的意思—让我编写更好的css代码。
当然,这些框架也有一些缺点:
毕竟,你看这篇文章是带着目的的,对吧,所以不要在纠结框架不框架了,让我们学习如何在原生css方面让它变得更好吧,
Ps: 这不是一篇关于如何设计漂亮app的文章,它是关于编写可维护和可组织的css代码的学习文章
在本文的示例代码中我将使用SCSS编写。
SCSS是css的预处理器。基本上,它是CSS的超集:它添加了一些很酷的特性,比如:变量、嵌套、导入和混合。我会略将下我们马上要使用的特性。
在scss中你可以使用变量。主要好处:可重用性。让我们假设你的app中有一个颜色调色板。你的主题色是蓝色。所以你到处都要使用该颜色:按钮背景色、标题颜色、链接颜色,到处都是蓝色。
突然,你不喜欢蓝色了,你喜欢上绿色了:
// Declare a variable
$primary-color: #0099ff;// References a variable
h1 {
color: $primary-color;
}
你也可以使用scss进行代码嵌套:
h1 {
font-size: 5rem;
color: blue;
}h1 span {
color: green;
}
变为:
h1 {
font-size: 5rem;
color: blue;
span {
color: green;
}
}
可读性是不是变得更强?使用嵌套可以使你花费更少的时间来编写复杂的css选择器。
当涉及到可维护性和可读性上时,不可能将所有的代码都保存在一个大文件中。在实验性或小的APP中,这么做可以满足需求,但在专业级别的app上。想都别想。幸运的是,SCSS允许我们进行专业的app编写。
你可以通过使用前置下划线命名的文件来创建分块文件:_animations.scss、_variables.scss等。至于导入,我们使用 @import 指令。例如,你可以进行如下操作:
// _animations.scss
@keyframes appear {
0% {
opacity: 0;
} 100% {
opacity: 1;
}
}
// header.scss
@import "animations";
h1 {
animation: appear 0.5s ease-out;
}
呃,你可能会想,你在这里犯了个错误!它是_animations.scss
,而不是animations
。
非也,scss足够聪明,当你以这种方式进行命名时,它可以知道你想指代的是分块文件。
这就是我们需要知道的关于变量、嵌套、分块和导入所有的新星。scss还有一些更多的特征,比如混合、继承和其它指令(@for,@if,…)。但我不会在这里谈它们。
如果你想了解更多关于scss的知识,可以阅读它们的文档,这更便于理解和学习。
我曾经无数次给我的css类名提供我能想到的全部术语,你懂的,比如这些命名:.button .page-1 .page-2
。我经常不知道如何进行命名。然而这又很重要,如果你正在编写一个app项目,出于某些原因,你决定搁置这个项目几个月,或者更糟,有人要收回该项目,如果你的css代码没有正确的命名,你很难知道你到底写了个啥。
BEM 帮助我们解决该问题。BEM 是一种命名约定,表示“块 元素 修饰符”。
该方案可以使我们的代码更加结构化,更加模块化和更大的可复用性。现在我来解释下什么是块、元素和修饰符。
块通常被视为一个组件。还记得小时候玩的乐高吗?好的,让我们回到小时候。
你打算如何建造一座简单的房子?你需要一个窗户,一个屋顶,一扇门,一些墙,就这些东西,这些就是我们需要的块。这些块都是有命名意义的。
命名:块名称:.block
示例: .card
, .form
, .post
,.user-Navigation
。
现在你要怎样用你的乐高积木来建造一个窗户呢?嗯,其中某些看起来像架子,当你组装好四个框子时,你会得到
一个漂亮的窗户。这就是我们的元素。它们是“块”的一部分,它们是建造“块“的必需品,但离开了”块“,它们什么都不是。
命名:块名称+ __ + 元素名称: block__element
示例:.post__author
,.post__date
,.post__text
现在你已经建造好了窗户,你可能需要一个绿色的窗口或者小点的窗户。这些就是所谓的修饰符。它们是块或元素的标识,用于更改行为、外观等。
命名: 块元素名称或元素名称+ --
+修饰符名称: .Block__element-modifier
,.Block-modifier
。
示例:.post--important
,.post__btn--disabled
这是给你的练习。你浏览那些你常逛的网址,试着分析哪些是块,哪些是元素,那些是修饰符。
例如,我在谷歌商店中分析到的是这样的:
轮到你了,保持好奇心,想想怎样可以变得更好。和往常一样,你必须自己搜索,实验,创造。这样你才能完成你的分析。
在下例中你会发现 BEM 的强大之处:
编写单个文章组件示例 —— https://codepen.io/thomlom/pen/RJvVdQ
编写多个按钮示例——https://codepen.io/thomlom/pen/VdgzmJ
还在跟着我一起学习吗?真棒!现在让我们看看如何组织 css 文件。这部分将真正的帮助你提高工作效率,并允许你立即能找到需要修改的 css 代码位置。
为了做到这点,我们将学习 7-1模式。
这玩意儿简单不,你可能会想。
相信我,该模式非常简单,你只需记住如下两条原则即可:
main.scss
文件中,该文件放到根目录,嗯,就是这么简单。margin-right-large, text-center,
..)等等。在该文件中,引入你的所有区块文件
@import abstracts/variables;
@import abstracts/functions;
@import base/reset;
@import base/typography;
@import base/utilities;
@import components/button;
@import components/form;
@import components/user-navigation;
@import layout/header;
@import layout/footer;
...
是的,这一切看起来很厉害,但你可能会想,这种架构适合大型项目,但不适用于小项目。所以,这里还有一个适合较小项目的版本。
首先,您不需要 vendors 文件夹。你只需将所有的外部 css 依赖放到头部的链接标签中。然后,你可以跳过主题文件夹,因为你的 app 可能只有一个主题。最后,你的页面也不会有很多特定样式,所以你也可以跳过那个文件夹。太好了,只剩4个文件夹了!
然后,你还面临两个选择:
你希望你的 css 代码是有组织的并遵循7-1模式,因此你保留了abstract、components、layout和base文件夹。
你想把所有的文件包括main.scss
文件都放置在一个大文件夹中,类似如下:
sass/
_animations.scss
_base.scss
_buttons.scss
_header.scss
...
_variables.scss
main.scss
这取决于你自己。
ok,你成功说服我采用你的方案,但有时候浏览器不支持scss文件,咋整?
说得好!最后一步,我们将学习如何立即将 scss 编译为 css。
为了做到这一步,网我们需要 Node.js 和 NPM(或者Yarn)
我们将使用一个名为 node-sass
的包,它允许我们将 .scss
文件编译为 .css
文件。
其 CLI命令行界面非常易用:
node-sass <input> <output> [options]
该命令行还有很多参数项,但这里我们只使用两个:
-w
: 监听目录和文件。这意味着 node-sass
将会监听你代码的任何更改,当他们发生改变时,它会自动编译为css,这在开发中是个很有用的功能。--output-style
: 指定编译出的 css 文件存放位置,它可以是以下值之一:nested|expanded|compact|compressed
,我们将使用它来构建你的 css 文件。如果你是个好奇者(我希望你是,开发者应该是一个好奇者),自己去这里看相关资料吧
现在我们知道如何使用工具了,剩下的就好办了,步骤如下:
mkdir my-app && cd my-app
npm init
node-sass
依赖库:npm install node-sass --save-dev
index.html
和main.scs
文件touch index.html
mkdir -p sass/{abstracts,base,components,layout} css
cd sass && touch main.scss
package.json
文件中添加这些script
{
...
"scripts": {
"watch": "node-sass sass/main.scss css/style.css -w",
"build": "node-sass sass/main.scss css/style.css --output-style compressed"
},
...
}
index.html
的head
标签中将编译好的 css 文件进行引入<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8">
<meta name=”viewport” content=”width=device-width, initial-scale=1.0">
<meta http-equiv=”X-UA-Compatible” content=”ie=edge”>
<link rel=”stylesheet” href=”css/style.css”>
<title>My app</title>
</head>
<body>
<h1 class=”heading”>My app</h1>
</body>
</html>
就这样,你准备出发了!在你进行编码是运行npm run watch
,并在浏览器中打开index.html
文件,如果你想压缩你的 css 文件,使用npm run build
命令
你可能亟需添加实时重载功能以提高开发效率,而不是手动重新加载本地index.html
文件。
步骤如下:
live-server
依赖: npm install -g live-server
。注意:这是个全局packagenpm-run-all
依赖:npm install npm-run-all
:它将允许我们同时运行多个scriptpackage.json
文件中添加如下script{
...
"scripts": {
"start": "npm-run-all --parallel liveserver watch",
"liveserver": "live-server",
"watch": "node-sass sass/main.scss css/style.css -w",
},
...
}
现在,当你运行npm run start
,你可以立即看到你的更改,而无需触及任何内容。
设定好了开发工具,现在我们来讨论下构建工具,特别是[Autoprefixer]( Autoprefixer)。
它是一个工具,可以解析 CSS 并使用 can I use 中的值将浏览器供应商前缀添加到 css 规则中。
实际上,在构建网站时,你可能会使用到并非所有浏览器都完全支持的新特性,这时候,添加浏览器供应商前缀可以支持这些新特性,下面是示例:
-webkit-animation-name: myAnimation;
-moz-animation-name: myAnimation;
-ms-animation-name: myAnimation;
这些特性写起来很乏味。因此我们需要 autoprefixer 来使我们的 css 代码能与浏览器兼容,而不会带来额外的复杂度。
所有我们按如下方式编写 css 代码:
这将是最后的步骤了,所有耐心和我一起完成吧:
postcss-cli
和autoprefixer
:npm install autoprefixer postcss-cli --save-dev
build
scirpt,添加两个script到package.json文件中:{
...
"scripts": {
"start": "npm-run-all --parallel liveserver watch",
"liveserver": "live-server",
"watch": "node-sass sass/main.scss css/style.css -w",
"compile": "node-sass sass/main.scss css/style.css",
"prefix": "postcss css/style.css --use autoprefixer -o css/style.css",
"compress": "node-sass css/style.css css/style.css --output-style compressed",
"build": "npm-run-all compile prefix compress"
...
}
现在,你运行npm run build
时,你的 css 代码将被压缩,并且已经添加了浏览器供应商前缀名,太棒了,不是吗?
原文地址: https://web.dev/long-tasks-devtools/
原文作者: Addy Osmani
翻译作者: garyhe0780
原文地址: https://medium.com/@dtkatz/react-hooks-tutorial-learn-by-building-b90ec4db2b8e
原文作者: David Katz
Hooks 是 React 即将推出的一个新功能。这一更新在 React 社区引起了巨大的反响。
简而言之,现在可以在 React 函数组件中使用 state 和其他 React 特性。因此没有必要使用 Class组件。
我会教你使用 React Hooks 构建一个 Web 应用。对于一些代码,我也会列出没有使用 Hooks 的方式的代码。这样的话可以比较与使用 Hooks的区别。
如果你是一个 React 新手, Hooks是另一个值得学习的基础概念。
如果你是一个有经验的 React 开发者,没有必要忘记之前所掌握的 React 知识。事实上, Hooks 能够让你更好的写出简洁的 React 代码。
在这个 Hooks 教程中,会构建一个叫 'Type Race' 的 Web 应用。这个小游戏可以检测你输入一段文本的速度。玩这个游戏能让你上瘾,因为它提高了你打字的速度和使用键盘的灵活性!
例子: https://davidtkatz.com/#/typerace
教程源码: https://github.com/15Dkatz/typerace-hooks
使用 npx 创建一个 React Hooks 例子项目,它可以直接使用 create-react-app
工具:
npx create-react-app typerace-hooks
进入到 typerace-hooks 目录,然后安装 [email protected]
和 [email protected]
。这个版本包含了 Hooks 的功能:
yarn add [email protected] [email protected]
确保 typerace-hooks/package.json
文件中依赖的 react
和 react-dom
版本是 16.8.0-alpha.1
。
"react": "@16.8.0-alpha.1",
"react-dom": "@16.8.0-alpha.1"
学习 Hooks 的最大部分是一种新的申明 state 的方式。因此创建这个app 的第一部分,就是新建一个组件,组件里的 state 是用 Hooks 申明的。
在 app 的中间,Type Race 会呈现一个文本输入框。输入框会跟踪用户输入到组件 state 中的文本。为了能够运行起来,我们重构一下 app.js 的代码,缩减到展示一个应用的标题,和输入框跟踪输入状态:
import React from 'react';
const App = () => {
return (
<div>
<h2>Type Race</h2>
<input />
</div>
)
}
export default App;
现在,我们来使用 Hooks 。我们将使用 state 跟踪用户的输入。我们将在 app.js 的顶部引入 useState
:
import React, { useState } from 'react';
然后我们会在 App 方法内的前面使用 useState hook,创建一个函数式的 state 'userText':
const App = () => {
const [userText, setUserText] = useState('');
这里有三个部分关于 useState
:
useState('')
,就是给 userText 设置了一个 空字符串的初始值。const [userText, setUserText] = useState('');
,userText
表示当前值。它在代码中当作一个常量使用。 setUserText
更新 userText
的值。在 App 组件中,创建一个 updateUserText
辅助类的方法, 用来将用户输入的值设置到 userText
变量中。
这个辅助方法调用 setUserText
, 将用户输入传递给这个方法。 updateUserText
的参数 event , event 参数里可以取到用户输入的值:
const App = () => {
const [userText, setUserText] = useState('');
const updateUserText = event => {
setUserText(event.target.value);
console.log(‘current userText’, userText);
}
我们把 updateUserText
作为输入框 onChange
事件的回调函数。onChange 事件会在用户输入的时候触发。回调函数的 event (包含 event.target.value
) 参数会传递给 updateUserText
。同时 userText 表示输入框的值 :
<input value={userText} onChange={updateUserText} />
现在运行并查看这个应用。在工程的根目录中使用命令行执行 npm run start
。应用运行在 localhost:3000 上。如果一切顺利,应用就会展示 Type Race 标题和一个输入框。
现在 useState 运行起来了。
我们来比较一下 state 在 函数组件中与 class 组件中的不同:
有几个方面需求强调:
this
对象调用 this.state
, this.setState
和 this.updateUserText
。长久以来对,React 开发者一直都需要关心 this
是怎样绑定到组件上面的。使用 .bind()
来修复这个问题是有点费劲的。使用 class 的属性初始化语法可以解决绑定的问题。但是你必须得认识到这一点就是你的方法能够访问到 this.setState
方法是因为组件继承 React.Component
时附加上去的。通过上面的学习,你可以认为 useState 是一个 React 函数式 state 的钩子。函数组件是将 React 的 state 附加到自己身上。
在之前 React 组件继承上,class 类组件继承了所有的 React 功能,现在 函数组件也能附加到自己里面。
让我们把这个应用做完。接下来允许用户可以选择不同的文本片段。我们会加入计时的尝试。总体来说,需要更多的 state 和 JSX 代码。
针对初次使用的人,我们添加选择要输入代码片段的功能。在现有的代码的前面设置 userText,我们会添加一组初始的文本片段和记录被选中文本片段的变量:
const SNIPPETS = [
'Bears, beets, battlestar galactica',
"What's Forrest Gump's password? 1Forrest1",
'Where do programmers like to hangout? The Foo Bar'
];
const [snippet, setSnippet] = useState('');
const [userText, setUserText] = useState('');
接着新建一个辅助方法允许用户选择文本片段。在方法内部,调用 useState 返回的 setState 方法。将这些代码写在 return 前面:
const chooseSnippet = snippetIndex => () => {
console.log('setSnippet', snippetIndex);
setSnippet(SNIPPETS[snippetIndex]);
};
return (
注意这个双箭头语法,它会让 chooseSnippet 返回一个方法。例如,chooseSnippet(0)
返回一个方法,这个方法最后会调用 setSnippet(SNIPPETS[0]);
。这里是关键,它会更新展示的文本片段以及提供一个按钮允许用户设置文本片段:
<div>
<h2>Type Race</h2>
<hr />
<h3>Snippet</h3>
{snippet}
<input value={userText} onChange={updateUserText} />
<hr />
{
SNIPPETS.map((SNIPPET, index) => (
<button onClick={chooseSnippet(index)} key={index}>
{SNIPPET.substring(0, 10)}...
</button>
))
}
</div>
几个关键的点:
setSnippet
参数是 index 。为什么要这么设计? SNIPPET
内部的 Id 作为 key 。但在这篇学习中并没有必要这么做。接下来添加一个 gameState
,用来跟踪下面一些变量:
victory
用户是否全部输完文本片段。startTime
用户开始的时刻从选择完要输入的片段起 。endTime
用户完成输入的时刻当用户选择好一个片段时, gameState
记录一个开始时间。因此,我们会用 setGameState
来设置值 gameState.startGame
。注意当我们使用 set 方法设置值的时候,必须提供一个完整的键值对象。因此用对象扩展符创建一个包含完整 gameState
的对象。所以用 new Date().getTime()
覆盖 startTime
:
setSnippet(SNIPPETS[snippetIndex]);
setGameState({ ...gameState, startTime: new Date().getTime() });
gameState
还包含 victory
和 endTime
。都需要在用户输入完整的文本片段时进行更新。 endTime
是 当前时间 new Date().getTime
减去 gameState.startTime
:
const updateUserText = event => {
setUserText(event.target.value);
if (event.target.value === snippet) {
setGameState({
...gameState,
victory: true,
endTime: new Date().getTime() - gameState.startTime
});
}
}
通过这些更改,我们可以为 gameState.victory 的场景添加 JSX 代码:
回到应用中,获胜的结果应该是这样的:
好的,现在就完成了这个应用!
此刻,你可以自行添加一些喜欢的CSS和样式更改。
除了 useState
,还有其他的 hooks 将会把 React 概念 带入到 函数式组件中。
在 useState
之后就是 useEffect
。useEffect
hooks 会在组件每次渲染之后触发一个反馈,这个反馈就是你提供的一个回调函数。在这个场景下,React 引擎会触发一个副作用在 DOM 更新完毕(换句话说,每次渲染后)。
组件通常会触发副作用,比如请求服务端数据,发起订阅或者手动更改 DOM (例如让页面元素获得焦点)。这些都是使用 useEffect
的好场景。
这个 Hooks 可以在函数组件中承担之前在 class 组件生命周期函数 ( componentDidMount 或者 componentWillUnmount 等) 的作用。
在 Type Race 中, useEffect
可以设置 document.title
在完成的例子中:
import React, { useState, useEffect } from 'react';
…
useEffect(() => {
if (gameState.victory) document.title = 'Victory!';
});
const updateUserText = event => {
现在如果你顺利完成输入,会发现浏览器选项卡的标题会变成 "Victory!" :
现在你已经使用 React hooks 构建了一个应用,希望你能体会到为什么这次更新会让 React 社区沸腾。React 团队一直在致力于打造整洁的函数式组件。美中不足的是,如果你需要创建本地 state ,就必须使用 class 组件。
使用 React hooks,就会变得两全其美:函数式组件也可以使用 state。
原文地址: https://dev.to/jsmanifest/26-miraculous-vs-code-tools-for-javascript-developers-in-2019-50gg
原文作者: jsmanifest
翻译作者: icepy
https://hackernoon.com/testing-react-components-with-jest-and-enzyme-41d592c174f
分工:
一共213行
@hanxiansen (1-71)
@sichenguo (72-142)
@luzuoquan (143-213)
@icepy 校验
原文地址:Front end component design principles
原文作者:Andrew Dinihan
文中示例代码:传送门
我在最近的工作中开始使用 Vue 进行开发,但是我在上一家公司积累了三年以上 React 开发经验。虽然在两种不同的前端框架之间进行切换确实需要学习很多,但是二者之间在很多基础概念、设计思路上是相通的。其中之一就是组件设计,包括组件层次结构设计以及组件各自的职责划分。
组件是大多数现代前端框架的基本概念之一,在 React 和 Vue 以及 Ember 和 Mithril 等框架中均有所体现。组件通常是由标记语言、逻辑和样式组成的集合。它们被创建的目的就是作为可复用的模块去构建我们的应用程序。
类似于传统 OOP 语言中 class 的设计,在设计组件的时候需要考虑到很多方面,以便它们可以很好的复用,组合,分离和低耦合,但是功能可以比较稳定的实现,即使是在超出实际测试用例范围的情况下。这样的设计说起来容易做起来却很难,因为现实中我们往往没有足够的时间按照最优的方式去做。
在本文中,我想介绍一些组件相关的设计概念,在进行前端开发时应该考虑这些概念。我认为最好的方法是给每个概念一个简洁精炼的名字,然后逐一解释每个概念是什么以及为什么重要,对于比较抽象概念的会举一些例子来帮助理解。
以下这个列表并不是不全面也不完整,但我注意到的只有 8 件事情值得一提,对于那些已经可以编写基本组件但想要提高他们的技术设计技能的人来说。所以这是列表:
以下列举的这个列表仅仅是是我注意到的 8 个方面,当然组件设计还有其他一些方面。在此我只是列举出来我认为值得一提的。
对于已经掌握基本的组件设计并且想要提高自身的组件设计能力的开发者,我认为以下 8 � 项是我认为值得去注意的,当然这并不是组件设计的全部。
请注意,代码示例可能有一些小问题或有点人为设计。但是它们并不复杂,只是想通过这些例子来帮助更好的理解概念。
应用内的组件共同形成组件树, 而在设计过程中将组件树可视化展示可以帮助你全面了解应用程序的布局。一个比较好的展示这些的办法就是组件图。
UML 中有一个在 OOP 类设计中经常使用的类型,称为 UML 类图。类图中显示了类属性、方法、访问修饰符、类与其他类的关系等。虽然 OOP 类设计和前端组件设计差异很大,但是通过图解辅助设计的方法值得参考。对于前端组件,该图表可以显示:
因此,让我们看一下下面这个基础表组件的组件层次图,该组件的渲染对象是一个数组。该组件的功能包括显示总行数、标题行和一些数据行,以及在单击其单元格标题格时对该列进行排序。在它的 props 中,它将传递列列表(具有属性名称和该属性的人类可读版本),然后传递数据数组。我们可以添加一个可选的'on row click'功能来进行测试。
虽然这样的事情可能看起来有点多,但是它具有许多优点,并且在大型应用程序开发设计中所需要的。这样会带来的一个比较重要的问题是它会需要你在开始 codeing 之前就需要考虑到具体细节的实现,例如每个组件需要什么类型的数据,需要实现哪些方法,所需的状态属性等等。
一旦你对如何构建一个组件(或一组组件)的整体有大概的思路,就会很容易认为当自己真正开始编码实现时,它会如自己所期望的按部就班的完成,但事实上往往会出现一些预料之外的事情, 当然你肯定不希望因此去重构之前的某些部分,或者忍受初始设想中的缺点并因此扰乱你的代码思路。而这些类图的以下优点可以帮助你有效的规避以上问题,优点如下:
顺带一提,上图并不是基于某些官方标准,比如 UML 类图,它是我基本上创建的一套表达规则。例如,在 props 、方法的参数和返回值的数据类型定义声明都是基于 Typescript 语法。我还没有找到书写前端组件类图的官方标准,可能是由于前端 Javascript 开发的相对较新且生态系统不够完善所致,但如果有人知道主流标准,请在回复中告诉我!
在 state 和 props 频繁被 watch 和 update 的情况下,如果你有使用嵌套数据,那么你的性能可能会受到影响,尤其是在以下场景中,例如一些因为浅对于而触发的重新渲染;在涉及 immutability 的库中,比如 React,你必须创建状态的副本而不是像在 Vue 中那样直接更改它们,并且使用嵌套数据这样做可能会创建笨拙,丑陋的代码。
即使使用展开运算符,这种写法也并不够优雅。扁平 props 也可以很好地清除组件正在使用的数据值。如果你传给组件一个对象但是你并不能清楚的知道对象内部的属性值,所以找出实际需要的数据值是来自组件具体的属性值则是额外的工作。但如果 props 足够扁平化,那么起码会方便使用和维护。
state / props 还应该只包含组件渲染所需的数据。You shouldn’t store entire components in the state/props and render straight from there.
(此外,对于数据繁重的应用程序,数据规范化可以带来巨大的好处,除了扁平化之外,你可能还需要考虑一些别的优化方法)。
对 state 的更改通常应该响应某种事件,例如用户单击按钮或 API 的响应。此外它们不应该因为别的 state 的变化而做出响应,因为 state 之间这种关联可能会导致难以理解和维护的组件行为。state 变化应该没有副作用。
如果你滥用watch
而不是有限考虑以上原则,那么在 Vue 的使用中就可能由此引发的问题。我们来看一个基本的 Vue 示例。我正在研究一个从 API 获取一些数据并将其呈现给表的组件,其中排序,过滤等功能都是后端完成的,因此前端需要做的就是 watch 所有搜索参数,并在其变化时触发 API 调用。其中一个需要 watch 的值是“zone”,这是一个过滤器。当更改时,我们想要使用过滤后的值重新获取服务端数据。watcher 如下:
你会发现一些奇怪的东西。如果他们超出了结果的第一页,我们重置页码然后结束?这似乎不对,如果它们不在第一页上,我们应该重置分页并触发 API 调用,对吧?为什么我们只在第 1 页上重新获取数据?实际上原因是这样,让我们来看下完整的 watch:
当分页改变时,应用首先会通过 pagination 的处理函数重新获取数据。因此,如果我们改变了分页,我们并不需要去关注数据更新这段逻辑。
让我们一下来考虑以下流程:如果当前页面超出了第 1 页并且更改了 zone,而这个变化会触发另一个状态(pagination)发生变化,进而触发 pagination 的观察者重新请求数据。这样并不是预料之中的行为,而且产生的代码也不够直观。
解决方案是改变页码这个行为的事件处理函数(不是观察者,用户更改页面的实际处理函数)应该更改页面值并触发 API 调用请求数据。这也将消除对观察者的需求。通过这样的设置,直接从其他地方改变分页状态也不会导致重新获取数据的副作用。
虽然这个例子非常简单,但不难看出将更复杂的状态更改关联在一起会产生令人难以理解的代码,这些代码不仅不可扩展并且是调试的噩梦。
组件的核心**是它们是可复用的,为此要求它们必须具有功能性和完整性。“耦合”是指实体彼此依赖的术语。松散耦合的实体应该能够独立运行,而不依赖于其他模块。就前端组件而言,耦合的主要部分是组件的功能依赖于其父级及其传递的 props 的多少,以及内部使用的子组件(当然还有引用的部分,如第三方模块或用户脚本)。
紧密耦合的组件往往更不容易被复用,当它们作为特定父组件的子项时,就很难正常工作,当父组件的一个子组件或一系列子组件只能在该父组件才能够正常发挥作用时,就会使得代码写的很冗余。因为父子组件别过度的关联在一起了。
在设计组件时,你应该考虑到更加通用的使用场景,而不仅仅只是为了满足最开始某个特定场景的需求。虽然一般来说组件最初都是出于特定目的进行设计,但没关系,如果在设计它们站在更高的角度去看待,那么很多组件将具有更好的适用性。
让我们看一个简单的 React 示例,你想在写出一个带有一个 logo 的链接列表,通过连接可以访问特定的网站。最开始的设计可能是并没有跟内容合理的进行解耦。下面是最初的版本:
虽然这这样会满足预期的使用场景,但却很难被复用。如果你想要更改链接地址该怎么办?你必须重新复制一份相同代码,并且手动去替换链接地址。而且, 如果你要去实现一个用户可以更改连接的功能,那么意味着不可能将代码写“死”,也不能期望用户去手动修改代码,那么让我们来看一下复用性更高的组件应该如何设计:
在这里我们可以看到,虽然它的原始链接和 logo 具有默认值,但我们可以通过 props 传入的值去覆盖掉默认值。让我们来看一下它在实际中的使用:
并不需要重新编写新的组件!如果我们解决上文中用户可以自定义链接的使用场景,可以考虑动态构建链接数组。此外,虽然在这个具体的例子中没有解决,但我们仍然可以注意到这个组件没有与任何特定的父/子组件建立密切关联。它可以在任何需要的地方呈现。改进后的组件明显比最初版本具有更好的复用性。
如果不是要设计需要服务于特定的一次性场景的组件,那么设计组件的最终目标是让它与父组件松散耦合,呈现更好的复用性,而不是受限于特定的上下文环境。
这个可能不那么的偏理论,但我仍然认为这很重要。与你的代码库打交道是软件工程的一部分,有时一些基本的组织原则可以使事情变得更加顺畅。在长时间与代码相处的过程中,即使改变一个很小的习惯也可以产生很大的不同。其中一个有效的原则就是将辅助代码分离出来放在特定的地方,这样你在处理组件时就不必考虑这些。以下列举一些方面:
因为在尝试处理组件的核心代码时,你不希望看到与技术无关的一些说明(因为会多滚动几下鼠标滚轮甚至打断思路)。在处理组件时,你希望它们尽可能通用且可重用。查看与组件当前上下文相关的特定信息可能会使得设计出来的组件不易与具体业务解耦。
虽然这样做起来可能具有挑战性,但开发组件的一个好方法是使它们包含渲染它们所需的最小 Javascript。一些无关紧要的东西,比如数据获取,数据整理或事件处理逻辑,理想情况下应该将通用的部分移入外部 js 或或者放在共同的祖先中。
单独从组件分的“视图”部分来看,即你看到的内容(html 和 样式)。其中的 Javascript 仅用于帮助渲染视图,可能还有一些针对特定组件的逻辑(例如在其他地方使用时)。除此之外的任何事情,例如 API 调用,数值的格式化(例如货币或时间)或跨组件复用的数据,都可以移动外部的 js 文件中。让我们看一下 Vue 中的一个简单示例,使用嵌套列表组件。我们可以先看下下面这个有问题的版本。
这是第一个层级:
这是嵌套列表组件:
在这里我们可以看到此列表的两个层级都具有外部依赖关系,最上层导引入外部 js 文件中的函数和 JSON 文件的数据,嵌套组件连接到 Vuex 存储并使用 axios 发送请求。它们还具有仅适用于当前场景的嵌入功能(最上层中源数据处理和嵌套列表的中度 click 时间的特定响应功能)。
虽然这里采用了一些很好的通用设计技术,例如将通用的 数据处理方法移动到外部脚本而不是直接将函数写死,但这样仍然不具备很高的复用性。如果我们是从 API 的响应中获取数据,但是这个数据跟我们期望的数据结构或者类型不同的时候要怎么办?或者我们期望单击嵌套项时有不同的行为?在遇到这些需求的场景下,这个组件无法被别的组件直接引用并根据实际需求改变自身的特性。
让我们看看我们是否可以通过提升数据并将事件处理作为 props 传递来解决这个问题,这样组件就可以简单地呈现数据而不会封装任何其他逻辑。
这是改进后的第一级别:
而新的第二级:
使用这个新列表,我们可以获得想要的数据,并定义了嵌套列表的 onClick 处理函数,以便在父级中传入任何我们想要的操作,然后将它们作为 props 传递给顶级组件。这样,我们可以将导入和逻辑留给单个根组件,所以不需要为了能够在新的场景下使用去重新再实现一个类似组件。
有关此主题的简短文章可以在这里找到。它由 Redux 的作者 Dan Abramov 编写,虽然是用 React 举例说明。但是组件设计的**是通用的。
我们在实际进行组件抽离工作的时候,需要考虑到不要过度的组件化,诚然将大块代码变成松散耦合且可用的部分是很好的实践,但是并不是所有的页面结构(HTML 部分)都需要被抽离成组件,也不是所有的逻辑部分都需要被抽出到组件外部。
在决定是否将代码分开时,无论是 Javascript 逻辑还是抽离为新的组件,都需要考虑以下几点。同样,这个列表并不完整,只是为了让你了解需要考虑的各种事项。(记住,仅仅因为它不满足一个条件并不意味着它不会满足其他条件,所以在做出决定之前要考虑所有条件):
许多大型应用程序使用 Redux 或 Vuex 等状态管理工具(或者具有类似 React 中的 Context API 状态共享设置)。这意味着他们从 store 获得 props 而不是通过父级传递。在考虑组件的可重用性时,你不仅要考虑直接的父级中传递而来的 props,还要考虑 从 store 中获取到的 props。如果你在另一个项目中使用该组件,则需要在 store 中使用这些值。或许其他项目根本不使用集中存储工具,你必须将其转换为从父级中进行 props 传递 的形式。
由于将组件挂接到 store(或上下文)很容易并且无论组件的层次结构位置如何都可以完成,因此很容易在 store 和 web 应用的组件之间快速创建大量紧密耦合(不关心组件所处的层级)。通常将组件与 store 进行关联只需简单几行代码。但是请注意一点,虽然这种连接(耦合)更方便,但它的含义并没有什么不同,你也需要考虑尽量符合如同在使用父级传递方式时的要点。
我想提醒大家的是:应该更注重以上这些组件设计的原则和你已知的一些最佳实践在实际中的应用。虽然你应该尽力维护良好的设计,但是不要为了包装 JIRA ticket 或一个取消请求而有损代码完整性,同时总是把理论置于现实世界结果之上的人也往往会让他们的工作受到影响。大型软件项目有许多活动部分,软件工程的许多方面与编码没有特别的关系,但仍然是不可或缺的,例如遵守最后期限和处理非技术期望。
虽然充分的准备很重要,应该成为任何专业软件设计的一部分,但在现实世界中,切实的结果才是最为重要的。当你被雇用来实际创造一些东西时,如果在最后期限到来之前,你有的只是一个如何构建完美产品的惊人计划,但却没有实际的成果,你的雇主可能不会太高兴吧?此外,软件工程中的东西很少完全按计划进行,因此过度具体的计划往往会在时间使用方面得到适得其反的效果。
此外,组件规划和设计的概念也适用于组件重构。虽然用了 50 年的时间来计划一切令人难以忍受的细节,然后从一开始就完美地编写它就会很好,回到现实世界,我们往往会遇到这种情况,即为了赶进度而不能使代码达到完美的预期。然而,一旦我们有了空闲时间,那么一个推荐的做法就是回过头来重构早期不够理想的的代码,这样它就可以作为我们向前发展的坚实基础。
在一天结束时,虽然你的直接责任可能是“编写代码”,但你不应忽视你的最终目标,即建立一些东西。创建产品。为了产生一些你可以引以为豪的东西并帮助别人,即使它在技术上并不完美,永远记得找到一个平衡点。不幸的是,在一周内每天 8 小时盯着眼前的代码会使得眼界和角度变得更为“狭窄”,这个时候你需要的你是退后一步,确保你不要为了一颗树而失去整个森林。
基于我自己迫在眉睫的需求,在遍历已有的情况下,我使用的场景不是App主要在 Mac 上,很希望有一个本地缓存式的服务,让我自己可以把在 GitHub 上 star 过的项目管理起来,看起来业务需求上可能有如下几点:
原文地址: https://blog.logrocket.com/how-css-works-creating-layers-with-z-index-6a20afe1550e
原文作者: Benjamin Johnson
这篇文章是"How CSS workd"系列文章中的第三篇(深入探讨CSS基本构建模块的系列文章,这些基本构建模块有时会让人感觉像是黑魔法)。无论你如何编写你的css代码,知道样式表的运行原理总是极好的,这样你才能编写出高效的、可伸缩性css代码。
下面是系列文章中的前两篇:
今天我们研究css的另一个黑魔法 — z-index,耍z-index,通常感觉像是往墙面上扔一个更高的数字,然后看看是什么效果。
但如果我们试着剥离z-index上的层,看看管理他们的规则呢,我们会发现它并不像我们想象的那么可怕。
说到底,我们在浏览器中使用的屏幕是一组由像素组成的二维平面,但如果你经常上网,你可能会经常体会到"三维”世界,例如,你可以单击一个按钮打开一个 modal,或者触发一个tool-tip
如果你假设用户正在看屏幕,并想象屏幕是三维世界的入口,你可以开启浏览器中的三维空间呈现,首先,我们把该空间想象成一个房间,那么"最远"的"墙"就被称为"画布 canvas"
当在屏幕是绘制像素时,浏览器首先会确保绘制距画布最近(或最远)�的像素,然后,他将渐进的将元素"更接近"用户,并覆盖以前绘制的像素
z-index属性表示元素在三维浏览器视觉中绘制的顺序,默认情况下,所有元素的 z-index 皆为0,浏览器按照 DOM 顺序进行绘制,然而实际上,z-index 为我们提供了何时绘制元素的细节控制,通过指定较高的 z-index,我们可以使绘制出的元素"更接近"用户,如果我们指定一个较低的(或负值)z-index,我们可以使绘制出的元素更接近画布。
在深入研究 CSS 的位置和布局规范时,我发现了这个有用的图片,它可以直观的显示出 z-index 层叠对用户视觉的影响。
具有较高 z-index
的元素看起来"更接近"用户
如果我们只看到 z-index
的这种特性,我们很容易被诱导认为它就是 z-index 的全部知识了:更高的 z-index就更接近用户,更小的 z-index 则更远离,然而,z-index 还有一些细节常被混淆。
z-index 的第一个注意点是它必须在被设定了 position
属性元素上时才会生效,这意味着,z-index
只有在position
设置为除 static
属性上的元素上时,它才能更改层叠顺序,在元素没有设置任何 position
的情况下,z-index 将不会起任何作用。
当我们探讨到 z-index 的第二个注意点时,z-index
变得更加让人困惑了,因为 z-index
只适用于层叠环境中的元素。
层叠上下文涉及到了 HTML
节点和它的所有子节点,HTML
元素位于层叠上下文的 root 级别,它可以被称为根层叠。
文档的默认层叠上下文(或"根层叠上下文")将 HTML
元素标记为其"根层叠",并且默认情况下,所有元素都属于此根层叠上下文,但是,任何元素节点也可以是其"局部层叠上下文"中的根层叠。
你可以通过以下几种方法将元素指定为新的局部层叠上下文的根层叠:
position
为absolute
或relative
或其它任何除了auto
属性上设置 z-index
position: fixed
或position: sticky
opacity
属性值小于1transform
或will-change
如果你想看到更多创建层叠环境的方法,请在 MDN上查看该文章
作为例子,让我们想象 HTML
的三个树节点,将它们想想为三个层叠,在下图中,每个层叠的底部是HTML
节点,子元素堆叠在其上(有点像 HTML 树的颠倒表示),让我们假设这些元素都是 body 标签的直接子节点。
如果我们在浏览器中预览这些HTML元素,它将表示为如下:
注意到什么有趣的事情了吗?
首先,为什么blue-child-1
低于其它标签?它的z-index
是1000000,它不是应该在最上面吗?
此外,为什么green-child-1
为什么显示在所有元素的顶部,及时它的z-index
为2,我们不是说更高级别的z-index胜过更低级别的z-index吗?
让我们从green-child-1
元素开始,因为它的父元素green-parent
的z-index为999,我们期望green-parent
元素也高于其它元素,但如果我们回想一下使用 z-index的第一个警告:它对静态定位的元素没有影响,这意味着green-child-1
是层叠上下文的一部分,它是根据red-parent
和blue-parent
(根层叠中唯一的其它两个元素)进行测量的,它的z-index比这两个更高,所有它显示在顶部。
了解red-parent
和blue-parent
各自创建的局部层叠环境也有助于我们了解为什么blue-child-2
显示在red-parent
之下,即使其z-index
更高,由于z-index
仅控制元素在其局部层叠上下文中的位置,因此blue-child-2
肯定要高于blue-parent
的子级。但是red-parent
的z-index
要高于blue-parent
,无论它们的z-index
是多少,red-parent
内部的所有元素都将比blue-parent
元素显示出更高的 z-index
为了使blue-child-2
显示在red-parent
之上,我们实际上必须要更改HTML结构,使得blue-hild-2
脱离它当前的局部层叠上下文环境,并使其进入根层叠上下文(或者至少是位于red-parent
之上的层叠上下文)。
在大型web应用中,这样变更通常是很麻烦切危险的,特别是当你试图管理语义化的HTML结构或模块化组件结构时。
你经常看到许多组件库通过将元素添加到 body 标签中,而不是包含在你自己定义的组件中以此来实现更高曾经的UI 组件(如 tool-tips和modal),这么做可以在很大程度上避开这些层叠上限文"陷阱",这么做也可以确保严重依赖层叠的组件的z-index始终处于根层叠上下文环境中,因此它们可以将z-index设为值1000,并相信他将显示在大多数元素的顶部。
在一些情况下,使用 z-index
可能很麻烦,但是当我们知道它的底层原理及工作规则时,我们会发现它的行为实际上是相当可预测的,我想99%的关于z-index
的混淆都源自我们在上面讨论的那两个警告的误解
作为复习,我在这里再强调一遍:
z-index
仅用于除 static 元素外的具有position
的元素z-index
仅适用于元素在其所属的层叠上下文中的位置我希望你以后在遇到多层 ui 导致的各种陷阱时,了解 z-index
的内部结构会给你带来信心,我知道,研究z-index
和层叠系统对于我们更好的开发ui会更有帮助。
原文地址: https://www.twilio.com/blog/guide-node-js-logging
原文作者: DOMINIK KUNDEL
翻译作者: icepy
当你开始使用 JavaScript 做开发时,你可能学习到的第一件事情就是如何使用 console.log
将内容打印到控制台。如果你搜索如何调试 JavaScript,你会发现数百个博客文章和 StackOverflow 的文章都指向简单的 console.log
。因为这是一种常见的做法,我们甚至可以使用 no-console
这样的规则来确保生产环境不会留下日志。但是,如果我们真的想要记录这些信息呢?
在这篇博文中,我们将介绍你想要记录信息的各种情况,Node.js 中的 console.log
和 console.error
之间的区别是什么,以及如何在不使用户控制台混乱的情况下在库中发送日志记录。
console.log(`Let's go!`);
虽然你可以在浏览器和 Node.js 环境中使用 console.log
和 console.error
,但在 Node.js 中使用时一定要记住一件重要的事情。
将如下代码写入到 index.js
文件中,并在 Node.js 环境里执行:
console.log('Hello there');
console.error('Bye bye');
如图:
虽然这两个输出看起来可能一样,但系统实际上对它的处理方式有不同。如果你检查一下 console section of the Node.js documentation 你会发现 console.log
使用 stdout
打印而 console.error
则使用 stderr
。
每一个进程都有三个可以使用的默认 streams
,它们是 stdin
,stdout
和 stderr
。 stdin
可以处理进程的输入,例如按下按钮或重定向输出。stdout
可以用于处理进程的输出。最后 stderr
则用于错误消息。如果你想了解 stderr
为什么存在以及何时使用它,可以访问:When to use STDERR instead of STDOUT。
简而言之,这允许我们使用重定向 >
和管道 |
运算符来处理与应用程序的实际结果分开的错误和诊断信息。而 >
允许我们将命令的输出重定向到文件,2>
允许我们将 stderr
的输出重定向到文件。我们来看一个例子,它会将 Hello there
重定向输出到 hello.log ,Bye bye
重定向输出到 error.log。:
$ node index.js > hello.log 2> error.log
如图:
现在我们已经了解了日志记录的基础技术,那么让我们来谈谈你可能想要记录某些内容的不同例子,通常这些例子都属于以下类别之一:
我们将跳过本博文中的前两篇文章,并将重点介绍基于Node.js的三篇文章。
你希望在服务器上记录内容的原因可能有多种,例如:记录传入的请求,统计信息,有多少404用户正在访问,另外你也想知道什么时候出错以及为什么。
初始化项目:
$ npm init -y
$ npm install express
让我们设置一个带有中间件的服务器,只需要 console.log
为你的请求提供打印:
const express = require("express");
const PORT = process.env.PORT || 3000;
const app = express();
app.use((req,res,next) => {
console.log('%o', req);
next();
});
app.get('/', (req,res) => {
res.send('hello world');
});
app.listen(PORT, () => {
console.log('Server running on port %d', PORT);
});
这里我们使用 console.log('%o', req);
来记录整个对象。
当你运行 node index.js
并访问 http://localhost:3000
你会注意到打印的很多信息并不是我们需要的。
如果将起更改为 console.log('%s',req)
我们也不会获取太多的信息。
我们可以编写自己的日志功能,只打印我们关心的信息。但让我们先退一步,谈谈我们通常关心的事情。虽然这些信息经常成为我们关注的焦点,但实际上我们可能需要其他信息,例如:
pm2
运行着多个 Node.js 进程另外,既然我们知道打印最后都会落到 stdout
和 stderr
上,那么我们可能想要不同日志级别的记录以及过滤它的能力。
我们可以通过访问流程的各个部分并编写一堆 JavaScript 代码来获取上述的信息,但 npm 生态已经给我们提供了各种各样的库来使用,例如:
我个人喜欢 pino
,因为它速度快,生态全。那么,让我们来看一看 pino
是如何帮助我们记录日志的。
$ npm install pino express-pino-logger
const express = require("express");
const pino = require("pino");
const expressPino = require("express-pino-logger");
const logger = pino({ level: process.env.LOG_LEVEL || 'info'});
const expressLogger = expressPino({ logger });
const PORT = process.env.PORT || 3000;
const app = express();
app.use(expressLogger);
app.get('/', (req,res) => {
logger.debug('Calling res.send')
res.send('hello world');
});
app.listen(PORT, () => {
logger.info('Server running on port %d', PORT);
});
运行 node index.js
并访问 http://localhost:3000
你可以看到一行一行的 JSON 输出:
如果你检查此 JSON ,你会看到前面提到的时间戳。你可能还注意到了我们 logger.debug
语句并未打印,那是因为我们必须更改默认日志级别才能使其可见,试试 LOG_LEVEL=debug node index.js
来调整日志级别。
在此之前我们还需要解决一下日志信息的可读性,pino
遵循了一个理念,就是为了性能,你应该通过管道将输出的处理移动到单独的进程中,你可以去查看一下文档,了解其中 pino
的错误为什么不会写入 stderr
。
让我们使用 pino-pretty
工具来查看更易读的日志:
$ npm install --save-dev pino-pretty
$ LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty
运行 LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty
并访问 http://localhost:3000
。
如图:
另外还有各种各样的库可以来美化你的日志,甚至你可以使用 pino-colada
用 emojis
来显示它们。这些对于你的本地开发非常有用,在运行到生产服务器之后,你可能希望将日志的管道转移到另外一个管道,使用 >
将它们写入硬盘以便稍后处理它们。
比如:
$ LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty | > success.log 2> s_error.log
既然我们研究了如何有效的为服务器应用程序编写日志,那么为什么不能将它用在我们的某些库中呢?问题是,你的库可能希望记录用于调试的内容,但实际上不应该让使用者的应用程序变得混乱。相反,如果需要调试某些东西,使用者应该能够启动日志。你的库默认情况下不会处理这些,并将输入输出的操作留给使用者。
express
就是一个很好的例子。
在 express
框架下有很多事情要做,在调试应用程序时,你可能希望了解一下框架的内容。如果我们查询文档,你会注意到你可以在命令行的前面加上 DEBUG=express:*
来启动。
$ DEBUG=express:* node index.js
如图:
如果你没有启动调试日志,则不会看到任何这样的日志输出。这是通过一个叫 debug
的包来完成的。
$ npm install debug
让我们创建一个新的文件 random-id.js
来使用它:
const debug = require("debug");
const log = debug("mylib:randomid");
log("Library loaded");
function getRandomId() {
log('Computing random ID');
const outcome = Math.random()
.toString(36)
.substr(2);
log('Random ID is "%s"', outcome);
return outcome;
}
module.exports = { getRandomId };
这里会创建一个带有命名空间为 mylib:randomid
的 debug
记录器,然后会将这两种消息记录上去。
我们可以在 index.js
文件中引用它:
const express = require("express");
const pino = require("pino");
const expressPino = require("express-pino-logger");
const randomId = require("./random-id");
const logger = pino({ level: process.env.LOG_LEVEL || 'info'});
const expressLogger = expressPino({ logger });
const PORT = process.env.PORT || 3000;
const app = express();
app.use(expressLogger);
app.get('/', (req,res) => {
logger.debug('Calling res.send')
const id = randomId.getRandomId();
res.send(`hello world [${id}]`);
});
app.listen(PORT, () => {
logger.info('Server running on port %d', PORT);
});
然后使用 DEBUG=mylib:randomid node index.js
来重新运行你的 index.js
文件,如图:
有意思的是,如果你的库使用者想把这些调试信息集成到自己的 pino
日志中去,那么他们可以使用一个叫 pino-debug
的库来正确的格式化这些日志。
$ npm install pino-debug
pino-debug
在我们第一次使用之前需要初始化一次 debug
,最简单的方法就是在启动之前使用 Node.js 的 -r
或 --require
命令来初始化。
$ DEBUG=mylib:randomid node -r pino-debug index.js | ./node_modules/.bin/pino-colada
如图:
我将在这篇博文中介绍最后一个案例,针对 CLI 的日志记录。我的理念是将逻辑日志和你的 CLI 输出分开。对于任何逻辑日志来说,你应该使用类似 debug
这样的包。这样你或其他人就可以重写逻辑,而不受 CLI 的约束。
一种情况是你的 CLI 在持续集成的系统中被使用,因此你可能希望删除各种花里胡哨的输出。有些 CI 系统设置了一个被称为 CI
的环境标志。如果你想更安全的检查自己是否在 CI 系统中,你可以使用 is-ci
这个库。
有些库例如 chalk
已经为你检查了 CI 并帮你删除了颜色。
$ npm install chalk
const chalk = require("chalk");
console.log('%s Hi there', chalk.cyan('INFO'));
运行 node cli.js
,如图:
当你运行 CI=true node cli.js
,如图:
你要记住的是另外一个场景 stdout
能否在终端模式中运行。如果是这种情况,我们可以使用类似 boxen
的东西来显示所有漂流的输出。但如果不是,则可能会将输出重定向到文件或输出到其他地方。
你可以使用 isTTY
来检查 stdout
,stdin
,stderr
是否在终端模式。
如:
process.stdout.isTTY
根据 Node.js 的启动方式,这个三个的值可能不同。你可以在文档中找到更多关于它的信息。
让我们看看 process.stdout.isTTY
在不同情况下的变化:
const chalk = require("chalk");
console.log(process.stdout.isTTY);
console.log('%s Hi there', chalk.cyan('INFO'));
然后运行 node index.js
,如图:
之后运行相同的内容,但将其输出重定向到一个文件中,这次你会看见它会打印一个 undefined
后面跟着一个简单的无色消息。
这是因为 stdout
关闭了终端模式下 stdout
的重定向。
chalk
使用了 supports-color
,它会在引擎里检查各个流的 isTTY
。
像 chalk
这样的库已经帮你处理了这些行为,但在开发 CLI 的过程中还是要注意,在 CI 模式下运行或输出被重定向的问题。
例如,你可以在终端以一种漂亮的方式来排列数据,如果 isTTY
为 undefined
时,则切换到更容易解析的方式上。
在 JavaScript 中使用 console.log
是非常快的,但当你将代码部署到生产环境时,你应该要考虑更多关于记录的内容。
本文仅仅是介绍了各种方法和可用的日志记录解决方案,它不包含你需要知道的一切。
因此我建议你多看一看你喜欢的开源项目,看看它们是如何解决日志记录问题以及它们所使用的工具。
原文地址: https://v8.dev/blog/v8-release-75
原文作者: Dan Elphick
每六周我们会在发布的过程中创建一个新的V8分支,每个版本都是在Chrome Beta里程碑之前立即从V8的Git master checkout 出来的。今天我们很高兴地宣布我们最新的分支 V8 7.5,它将在几周内与Chrome 75稳定版协同发布。V8 v7.5充满了各种面向开发人员的好东西,这篇文章提供了预期发布的一些亮点的预览。
我们计划在 Chrome 75 中推出 WebAssembly 编译工作的Implicit caching,这意味着第二次访问同一页面的用户不需要编译已经看过的 WebAssembly 模块,而是从缓存加载它们,它的工作原理与Chromium’s JavaScript code-cache 类似。
为 WebAssembly 添加了一个新的指令用于更新大内存。
memory.copy 可以将数据从一个区域复制到另一个区域(类似 C 的memmove),memory.fill 可以填充给定区域的数据,(类似 C 的 memset),与 memory.copy 类似 table.copy 也可以将数据从一个区域复制到另一个区域。
;; Copy 500 bytes from source 1000 to destination 0.
(memory.copy (i32.const 0) (i32.const 1000) (i32.const 500))
;; Fill 1000 bytes starting at 100 with the value `123`.
(memory.fill (i32.const 100) (i32.const 123) (i32.const 1000))
;; Copy 10 table elements from source 5 to destination 15.
(table.copy (i32.const 15) (i32.const 5) (i32.const 10))
该提议还提供了将常量区域复制到线性存储器或表中的方法。为此我们需要先定义一个 “passive” segment,与 "active" segment 不同的是,这些segment在模块实例化期间不会初始化。相反,可以使用memory.init和table.init指令将它们复制到内存或表区域中。
;; Define a passive data segment.
(data $hello passive "Hello WebAssembly")
;; Copy "Hello" into memory at address 10.
(memory.init (i32.const 10) (i32.const 0) (i32.const 5))
;; Copy "WebAssembly" into memory at address 1000.
(memory.init (i32.const 1000) (i32.const 6) (i32.const 11))
人眼难以快速解析大型数字文字,尤其是当有大量重复数字时:
1000000000000
1019436871.42
为了提高可读性,新的JavaScript语言功能使下划线成为数字文字中的分隔符。因此,现在可以重写以上内容以将数字分组为千分之一,例如:
1_000_000_000_000
1_019_436_871.42
数字分隔符有助于提高各种数字文字的可读性:
// A decimal integer literal with its digits grouped per thousand:
1_000_000_000_000
// A decimal literal with its digits grouped per thousand:
1_000_000.220_720
// A binary integer literal with its bits grouped per octet:
0b01010110_00111000
// A binary integer literal with its bits grouped per nibble:
0b0101_0110_0011_1000
// A hexadecimal integer literal with its digits grouped by byte:
0x40_76_38_6A_73
// A BigInt literal with its digits grouped per thousand:
4_642_473_943_484_686_707n
从Chrome 75开始,V8可以直接从网络将stream scripts传输到streaming parser,而无需等待Chrome主线程。
虽然之前的Chrome版本具有 streaming parsing 和 compilation,但由于历史原因,从网络传入的脚本源数据始终必须首先进入 Chrome 主线程,然后再转发到streamer。这意味着 streaming parser 通常会等待已经从网络到达的数据,但尚未转发到流式任务,因为它被主线程上发生的其他事情阻止(例如HTML解析,布局或其他JavaScript执行)。
在Chrome 75中,我们将网络“数据管道”直接连接到V8,允许我们在流解析期间直接读取网络数据,跳过对主线程的依赖。
这使我们能够更早地完成 streaming compiles,改善使用 streaming compilation 的页面加载时间,以及减少并发(但停滞)streaming parse tasks 的数量,从而减少内存消耗。
可以使用 git log branch-heads/7.4..branch-heads/7.5 include/v8.h
获取 API 更改列表。
原文地址: https://hackernoon.com/javascript-async-await-the-good-part-pitfalls-and-how-to-use-9b759ca21cda
原文作者: Charlee Li
翻译作者: Xixi20160512
async/await
是在 ES7 版本中引入的,它对于 JavaScript 中的异步编程而言是一个巨大的提升。它可以让我们以同步的方式处理异步的流程,同时不会阻塞主线程。但是,想要用好这一特性,可能需要动点脑筋。本文中,我们将从不同的角度探讨 async/await,同时会展示如何正确和高效的使用它们。
async/await
带给我们最大的一个好处就是同步的编程风格。让我们看一个例子:
// async/await
async getBooksByAuthorWithAwait(authorId) {
const books = await bookModel.fetchAll();
return books.filter(b => b.authorId === authorId);
}
// promise
getBooksByAuthorWithPromise(authorId) {
return bookModel.fetchAll()
.then(books => books.filter(b => b.authorId === authorId));
}
很明显,async/await
的版本比 promise 的版本更加的易于理解。如果你忽略 await
关键字,这段代码看起来就像任何其他的同步式语言(比如说 Python)。
不仅仅是可读性,async/await
有浏览器的原生支持。到今天为止,所有主流浏览器都支持 async
函数。
所有主流浏览器都支持 async
函数。(图片来源:https://caniuse.com/)
原生支持意味着你不需要编译代码。更重要的是,这个将有助于调试。当你在 async 方法的入口打一个断点并且步进到 await
这一行的时候,你将会看到调试器在 bookModel.fetchAll()
这个函数执行的时候等待了一会儿,然后才会走到接下来的 .filter
这一行!和 promise 的示例比较起来,这个容易多了,因为你必须在 .filter
这一行再打一个断点。
调试 async 函数。调试器会在 await 这一行等待执行完成然后才会移动到下一行。
另一个不那么明显的好处就是 async
关键字。它声明了 getBooksByAuthorWithAwait()
方法返回的是一个 promise,因此调用者可以像 getBooksByAuthorWithAwait().then(...)
或者 await getBooksByAuthorWithAwait()
这样安全的调用。看一下这个例子(不好的实践):
getBooksByAuthorWithPromise(authorId) {
if (!authorId) { return null; }
return bookModel.fetchAll()
.then(books => books.filter(b => b.authorId === authorId));
}
在上面的代码中,getBooksByAuthorWithPromise
可能返回一个 promise (正常情况下)或者 null
(特殊情况下),返回 null 的时候调用者不能安全的调用 .then()
。使用 async
进行声明的时候,这个问题就不会存在了。
一些文章把 async/await 和 Promise 进行了比较,同时说它是 JavaScript 异步编程演变过程中的下一代解决方案,对此我不敢苟同。Async/await 是一个提升,但它仅仅是一个语法糖,它将不会完全的改变我们的编程风格。
实质上,async 函数仍然是 promise。你必须理解 promises 之后才能正确的使用 async 函数,更糟糕的是,大多数情况下你必须同时使用 promises 和 async 函数。
思考一下上面例子中使用到 的 getBooksByAuthorWithAwait()
和 getBooksByAuthorWithPromises()
。请注意,它们不仅是有相同的功能,同时也有相同的接口。
这意味着如果你直接 getBooksByAuthorWithAwait()
的话,将会返回一个 promise。
当然,这并不是一件不好的事情。只有 await
给人们的一种感觉,“很棒,这个可以将异步的函数转换成同步的函数”,这个才是错误的。
那么在使用 async/await
的过程中会犯哪些错误呢?这里有一些比较常见的例子。
虽然 await
能够使你的代码看起来像同步代码一样,但是一定要记住这些代码仍然是以异步的方式执行的,注意不要使代码过于线性化。
async getBooksAndAuthor(authorId) {
const books = await bookModel.fetchAll();
const author = await authorModel.fetch(authorId);
return {
author,
books: books.filter(book => book.authorId === authorId),
};
}
这段代码看起来逻辑上没有问题。然而是不正确的。
await bookModel.fetchAll()
将会等待 fetchAll()
执行完。await authorModel.fetch(authorId)
才会被执行注意, authorModel.fetch(authorId)
并不依赖 bookModel.fetchAll()
的结果,实际上他们可以并行执行。然而,由于使用了 await
这两次调用就变成了串行的了,花费的总时间将会远超并行的方式。
以下是正确的使用方式:
async getBooksAndAuthor(authorId) {
const bookPromise = bookModel.fetchAll();
const authorPromise = authorModel.fetch(authorId);
const book = await bookPromise;
const author = await authorPromise;
return {
author,
books: books.filter(book => book.authorId === authorId),
};
}
或者更复杂的情况下,如果你想依次请求一个列表的内容,你必须依赖 promises:
async getAuthors(authorIds) {
// WRONG, this will cause sequential calls
// const authors = _.map(
// authorIds,
// id => await authorModel.fetch(id));
// CORRECT
const promises = _.map(authorIds, id => authorModel.fetch(id));
const authors = await Promise.all(promises);
}
简而言之,你必须把这个工作流程看成是异步的,然后再尝试使用 await
以同步的方式去编写代码。在复杂的流程下面,直接使用 promises 可能会更简单。
使用 promises 的情况下,一个异步函数会返回两种可能的值:resolved 和 rejected。我们可以使用 .then()
来处理正常的情况 .catch()
处理异常情况。然而对于 async/await
来说,异常处理可能会有点诡异。
最标准的(也是我推荐的)处理方式是使用 try...catch
表达式。当 await
一个函数调用的时候,任何 rejected 的值都会以异常的形式抛出来。这里有个例子:
class BookModel {
fetchAll() {
return new Promise((resolve, reject) => {
window.setTimeout(() => {
reject({'error': 400})
}, 1000);
});
}
}
// async/await
async getBooksByAuthorWithAwait(authorId) {
try {
const books = await bookModel.fetchAll();
} catch (error) {
console.log(error); // { "error": 400 }
}
}
被捕获的错误就是 rejected 的值。在我们捕获这个异常之后,我们有很多方式来处理它:
catch
块中使用任何 return
表达式等价于使用 return undefined
;同时,返回的仍是一个 resolved 的值。)throw error;
,这种方式允许你以 promise 链式的方式使用 async getBooksByAuthorWithAwait()
方法(列如,你仍然可以像 getBooksByAuthorWithAwait().then(...).catch(error => ...)
这样调用它);或者,你可以使用 Error
对象包装错误对象,例如, throw new Error(error)
,使用这种方式可以在控制台中展示所有的调用栈记录。return Promise.reject(error)
,这个方式等价于 throw error
,因此不推荐使用这种方式。使用 try...catch
的优点有以下这些:
await
调用包装在一个 try...catch
块中来集中处理所有错误,如果每一步的错误处理非必要的话。这种处理方式有一个缺陷。由于 try...catch
将会捕获这个代码块中的所有异常,一些其他通常不会被 promises 捕获的异常也会被捕获住。考虑一下这个例子:
class BookModel {
fetchAll() {
cb(); // note `cb` is undefined and will result an exception
return fetch('/books');
}
}
try {
bookModel.fetchAll();
} catch(error) {
console.log(error); // This will print "cb is not defined"
}
执行这段代码你将会在控制台中得到一个错误: ReferenceError: cb is not defined
,这些文字是黑色的。这个错误是 console.log()
打印出来的而不是 JavaScript 自身。某些时候这将会是致命的:如果 BookModel
被一系列函数调用深深地封闭起来了,同时,其中某一个调用将这个错误处理掉了,这时候就很难像这样去发现这个错误了。
另外一个错误处理的方式是由 Go 语言启发的。它允许 async 函数同时返回错误的值和正常的值。可以从下面这个博客中了解到更详细的的介绍:
简而言之,你能够像下面这样使用 async 函数:
[err, user] = await to(UserModel.findById(1));
我个人并不喜欢这种处理方式,因为它把 Go 语言的编程风格带到了 JavaScript 中,这样显得不自然,但是在某些情况下这种方式会很有用。
我要介绍的最后一种处理方式是仍然使用 .catch()
。
回忆一下 await
的功能:它会等待一个 promise 完成它的任务。同时请回忆一下, promise.catch()
也会返回一个 promise!因此我们可以像下面这样处理错误处理的方式:
// books === undefined if error happens,
// since nothing returned in the catch statement
let books = await bookModel.fetchAll()
.catch((error) => {
console.log(error);
});
这种处理方式有两个次要的问题:
在 ES7 中引入的 async/await
关键字无疑是对 JavaScript 异步编程的一大加强。它能够把代码变得更易于阅读和调试。然后,为了正确的使用它们,必须要完全理解 promises,因为它们不过是语法糖,底层的技术仍然是 promises。
希望这篇文章能够给你一些关于 async/await
的启发,同时能够帮助你避免一些常见的错误。感谢阅读,如果喜欢的话,请为我点赞。
The first version of Flutter was known as "Sky" and ran on the Android operating system......
Flutter 的第一个版本代号叫"Sky" ,当时运行在安卓系统中......
Originally posted by @icepy in #1 (comment)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.