Coder Social home page Coder Social logo

article's People

Contributors

chenmf6 avatar henry-fun avatar icepy avatar luzuoquan avatar manooog avatar sichenguo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

article's Issues

Testing React components with Jest and Enzyme

原文地址: https://hackernoon.com/testing-react-components-with-jest-and-enzyme-41d592c174f

原文作者: https://hackernoon.com/@sapegin

使用 Jest 和 Enzyme 测试 React 组件

引言

有人说在通常情况下测试 React 组件是没有太大用处的,但是我觉着在一些场景下是很有必要的:

  • 组件库,
  • 开源项目,
  • 与第三方组件集成,
  • bugs, 防止复现.

我尝试过很多的工具组合,但是最终如果会推荐给别的开发者,我更乐意去择推荐如下组合:

  • Jest, 一个测试框架;
  • Enzyme, React的 测试类库;
  • enzyme-to-json 转换 Enzyme 包装类型匹配 Jest 的快照

我经常在测试中使用的是浅渲染和 Jest 快照测试。

i_1

在 Jest 进行快照测试

Shallow rendering

浅渲染指的是将一个组件渲染成虚拟 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 这样的文件中,同时你需要提交这些文件。

为什么选择 Jest

  • 运行速度非常快。
  • 可以进行快照测试。
  • 交互式的监控模式,只会测试有过修改的部分。
  • 错误信息很详细。
  • 配置简单。
  • Mocks 和 spies 支持.
  • 通过命令行可以生成测试报告.
  • 发展前景很好。
  • 不会写出像 Chai 框架 'expect(foo).to.be.a(‘function’)' 一样很容易出错的断言 'expect(foo).to.be.a.function' 因为 Jest 只会写 'expect(foo).to.be.true' 这样确定正确的断言。

为什么选择 Enzyme

  • 便利的工具函数库封装,可以处理浅渲染,静态渲染标记以及DOM渲染。
  • jQuery 风格的API,便于使用和直观。

配置

第一步安装所有的依赖包括同版本依赖:

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();
});

Props 测试

有的时候如果你想测试的更精确和看到真实的值。那样的话需要在 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);
});

不仅仅是JSX

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';

参考来源

Vue.js + GSAP = 🔥 Animations

原文地址: 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方法,我们传递三个参数给该方法:

  • 参数1:要设置动画效果的元素
  • 参数2:动画运行的秒数
  • 参数3:描述动画行为的对象

下面链接展示了一小段代码展示的运行效果:

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。

为了能做到该需求,我们必须做以下几件事:

  • 将图片的引用源地址绑定到 VUE 的data中
  • 创建要使用的图片数组
  • 创建随机获取logo的方法
  • 添加按钮来更改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到列表中。

Data Structures in JavaScript

原文地址: https://medium.com/siliconwat/data-structures-in-javascript-1b9aed0ea17c

原文作者: Thon Ly

翻译作者: hanxiansen

中文标题:前端必知必会的数据结构

Data Structures in JavaScript

介绍

随着业务逻辑越来越多地从后台迁往前端,前端工程化方面的专业知识变得愈发重要。作为一名前端工程师,我们依赖于像 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)”的顺序来进行阐释说明,让我们开始吧!

栈(Stack)

可以说,Js 中最重要的栈就是调用栈(译者注:调用栈是 js 解释器追踪函数执行流的一种机制),在该栈中,无论何时执行函数,我们都会往栈中推入该函数的作用域。在编码中,它只是一个具有两个基本操作的数组:pushpoppush 将元素添加到数组的头部,而pop将它们从同一位置删除。换句话说,栈遵循“后进先出”的原则(LIFO)。

下面是代码中的栈示例。请注意,我们可以颠倒栈的顺序:底部变为顶部,顶部变为底部。因此,我们可以使用十足的 unshiftshift 方法来分别代替 pushpop

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)。如果颠倒方向,我们可以使用pushshift方法唉替换unshiftpop方法。

队列的代码示例如下:

https://codepen.io/thonly/pen/KypxZg

链表(Linked List)

类似于数组,链表也是按照顺序存储数据元素。不同的是,链表保存指向其它元素的指针,而非索引。链表的第一个节点被称为头部节点,最后一个节点被称为尾部节点,在单链表中,每个检点只有一个指向下个节点的指针。在单链表里,头部节点是我们遍历剩余节点的的地方。在双链表中,每个节点还保留了指向上一个节点的指针。因此,我们也可以从尾部节点开始,“倒退遍历“到头部节点。

链表具有常量的插入和删除时间,因为我们只需更改指针即可。但在数组中执行相同的操作却需要线性时间,因为后续条目需要移位。此外,只要有空间,链表就可以增长。然而,及时是可以自动调整大小的“动态数组”在空间利用上也是出乎意料的昂贵。当然,事物总有权衡取舍。在链表中检索和编辑一个元素,我们需要遍历整个链表,这等同于线性时间。然而对于有索引的数组,这样的操作时微不足道的。

类似于数组,链表也可以进行栈的操作。该操作很简单,只需将头部作为插入和取出的唯一位置。链表也可以进行队列操作,这可以使用双向链表来实现,插入发生在尾部,删除发生在头部,反之亦然。对于大的数据而言,这种实现队列的方案比数组的性能更高,因为在数组的开始位置插入和移出元素,需要线性的时间来重新索引后续的每个元素。

链表数据结构在客户端和服务端都很有用。在客户端,像Express这样的 web 框架也以类似的逻辑构建它的中间件。当收到请求,请求通过管道一个接一个的传递下去,直到请求被发出。

双向链表的代码如下:

https://codepen.io/thonly/pen/QqRVJX

树(Tree)

树类似于数组,区别在于它在层次结构中保留了对多个子节点的引用。换句话说,每个节点不止拥有一个父节点。DOM 树就是这样一种数据结构,它拥有一个 html 根节点,然后分成 head节点和 body 节点,这两个节点又进一步细分为多个你所熟悉的 html 标签。更深层面上,原型继承和 React 的组件也采用类似的树形结构。当然,作为DOM的内存表现形式,React 的虚拟 Dom也是一个树形结构。

二叉搜索树是特殊的,因为它拥有的子节点不能超过两个。左子节点的值必须小于或等于其父节点的值,而右子节点的值必须大于父节点的值。通过这种结构和平衡方式,我们可以在指数时间内检索任何值,因为我们可以在每次迭代中忽略另一半分支。插入和删除的时间也是指数级的。此外最小值和最大值也可以很容易的在最左边和最右边的叶子节点中找到。

遍历树可以在垂直或水平过程中进行。在垂直方向的深度优先遍历(DFT)中,递归算法将会比迭代算法更加优雅。节点遍历可以按照前序遍历、中序遍历、后序遍历的方式进行。如果我们需要先访问根节点再探索叶子节点,我们应选择前序遍历。但如果我们需要先访问叶子节点再访问根节点,我们应选择后序遍历。顾名思义,中序遍历使我们可以按照顺序进行遍历。这种特性使二叉搜索树成为排序的最佳选择。

在水平方向上的广度优先遍历(BFT)中,迭代比递归更优雅。这需要使用一个队列来追踪每次迭代的所有子节点。这样一个队列所需的内存是微不足道的。然而,如果树的形状宽大于深,BFT 是比 DFT 更好的选择。此外,BFT 在任意两个节点之间采用的路径是可能的最短路径。

二叉搜索树的示例代码如下:

https://codepen.io/thonly/pen/qVWOpM

图(Graph)

如果一个树可以自由的拥有多个父级节点,它将变为Graph(图)。将图中的节点连接在一起的边可以是有方向的,也可以是无方向的,可以是加权的,也可以是未加权的。同时具备方向和权重的边类似于矢量。

混合形式的多重继承和具有多对多关系的数据对象生成图形结构。社交网络和互联网本身也是图。自然界中最复杂的图是我们的人脑,神经网络试图复制人脑来赋予机器超级智能。

###哈希表(Hash Table)

哈希表是一种类似于字典的结构,它将键与值进行配对。每个键值对在内存中的储存位置由焊锡函数决定,该哈希函数接收一个 key ,然后返回它应该插入和检索该键值对的地址。如果两个或更多的 key 被转换为同一个地址,可能会导致冲突。为了保持哈希函数的健壮性,getter 和 setter 方法应该预防到这些问题,来确保可以恢复所有的数据,并且不会覆盖任何数据,通常,链表可以提供最简单的解决方案。拥有巨大空间的哈希表也同样可以解决该问题。

转载申请

可以转载到公众号高级前端进阶上吗?会标明作者与来源,感谢

CSS selectors level 4

原文地址:https://blog.logrocket.com/css-selectors-level-4-b5da36bcd54c/

原文作者:Esteban Herrera

翻译作者:thisang

CSS 第四级选择器

选择器是 CSS 的核心部分。你用来做一些操作比如说选择某种类型的所有元素,就像下面这样:

div {
  /* 一些应用在所有 div 元素上的样式 */
}

或者你可以选择一个在它的父元素下的最后一个子元素:

ul li:last-child {
  /* 一些只应用在列表的最后一个子元素上的样式 */
}

当然,你也可以去做一些更复杂的事情,比如说选择选择列表中除了最后一个子元素之外的所有子元素。

事实上,这样做的方法不止一种,一些方法甚至更复杂。

例如,比较以下这两个:

ul li {
  /* 一些应用在所有子元素上的样式 */
}
ul li:last-child {
  /* 一些样式用来重置上面生效的样式,因为上面的样式不适用于上面最后一个子元素 */
}
ui li:nth-last-child(n+2) {
  /* 一些应用在除了最后一个子元素之外的所有子元素上面的样式 */
}

ul li属于第一级选择器。

last-childnth-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;
}

在 Codepen 上试试

: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;
}

在 Codepen 上试试

: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;
}

在 Codepen 上试试

:has(reslativeSelector1,reslativeSelector2,...)

浏览器支持(在写这篇文章的时候,还没有任何一个浏览器支持这个选择器)

这个选择器将一个相对选择器列表作为参数。它选择那些在指定范围内匹配相对选择器列表的元素。

例如:

p:has(strong, em) {
  color: red;
}

这会给所有的包含<strong>或者<em>元素的p元素添加一个颜色为红色的样式。

在 Codepen 上试试

属性选择器

这类选择器包含那些应用在元素属性上的选择器。

[foo="bar"i]

浏览器支持

它选择那些foo属性的值等于bar的元素,忽略大小写。

例如:

p[class="text" i] {
  color: green;
}

这会给所有的class属性的值为TextTEXT、或者是text、又或者是其他组合的p元素添加一个颜色为绿色的样式。

在 Codepen 上试试 

[foo="bar"s]

在我写这篇文章的时候,还没有任何一个浏览器支持这个选择器。

它选择那些foo属性的值严格等于bar的元素。

例如:

p[class="text" s]{
  color: green;
}

这会给所有class属性的值为text(例如,不是Text)的p元素添加一个颜色为绿色的样式。

在 Codepen 上试试

语言伪类?

这类选择器包括那些使用语言相关设置的选择器。

:dir(ltr)

浏览器支持

它选择那些具有从左到右方向性的元素,其中文档语言指定如何确定方向性。与之对应的::dir(rtl)表示具有从右到左方向性的元素。其他值是无效的也不会匹配任何元素。

例如:

p:dir(rtl) {
  background-color: red;
}

这会给设置了从左到右方向的p元素添加一个背景色为红色的样式。

在 Codepen 上试试

:lang(zh,"*-hant")

浏览器支持

在我写这篇文章的时候,还没有任何一个浏览器支持这个第四级选择器。

它选择那些被标记为中文的元素(或者选择其他语言标记的元素,只需要把zh改为其他的语言代码就可以了)或者用繁体字书写的元素(或者在其他字符的系统中,将-hant替换为其它字符代码)。

实际上,第二级选择器里面就有这个选择器,但是在第四级选择器里面新增了通配符和逗号分隔列表。

它接收一个或多个以逗号为分隔的语言代码作为参数。如果语言代码里面包含*,则必须对它们进行正确的转义(:lang(es-\*))或字符串引号(:lang("es-*"))。

例如:

p:lang("*-CH") {
  background-color: red;
}

这会给所有被标记为使用瑞士语言的p元素添加一个背景色为红色的背景色。

在 Codepen 上试试

位置伪类

这类选择器包括那些与超链接有关得选择器。

:any-link

Can I use上并没有这个选择器的兼容性介绍,但是大多数主流浏览器都支持这个选择器。

它选择那些具有href属性的元素(例如<a>或<link>。或者说,所有与:link:visited伪类匹配的元素。

例如:

a:any-link {
  color: red;
}

这会给所有具有href属性的的所有a元素添加一个颜色为红色的样式。

在 Codepen 上试试

:local-link

在我写这篇文章的时候,还没有任何一个浏览器支持这个第四级选择器。

它选择那些链接到当前 URL 的元素。如果链接的指向包括 URL 片段,则 URL 片段和和当前 URL 也必须要匹配。比如不匹配,则在比较中不考虑当前 URL 的片段 URL 部分。

例如:

a:local-link {
  text-decoration: none;
}

这会给所有的具有href属性的指向当前页面的a元素(也许它们是导航菜单的一部分)去掉下划线。

在 Codepen 上试试

用户操作伪类

这类选择器包括那些用户正在操作的元素的选择器。

:focus-within

浏览器支持

它选择那些与:focus伪类匹配的元素(当元素具有焦点时)或具有与:focus匹配的子元素。

例如,使用如下的一个表单:

<from>
  <input type="text" id="name" placeholder="Enter your name" />
</from>

当表单里的输入框获得焦点的时候,它会给表单添加一个边框。

在 Codepen 上试试

:focus-visible

浏览器支持

它选择一个处于焦点状态的元素(与:focus伪类匹配),浏览器通常会为了让获得焦点的元素清晰可见,给它添加一个焦点环。

:focus的区别和细微。

如果:focus-visible匹配了,:focus一定会匹配,但是反过来就不一定了,它取决于浏览器(如果启用了聚焦环的绘制)和获取焦点的元素(通过鼠标点击或者 tab 键)。

Firefox 把这个选择器实现为:-moz-focusring伪类。

例如,在某些场景下,有以下样式:

/* 标准第四级选择器 */
:focus-visible {
  background-color: lightgary;
}
/* 基于 Firefox 的第四级选择器 */
:moz-focusring {
  background-color: lightgary;
}

只有在元素通过 tab 获得焦点的时候才会生效。

在 Codepen 上试试

输入伪类

这类选择包括那些应用于接受用户输入的元素的选择器。

: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;
}

这会把页面上所有的不可编辑元素背景色设置为浅灰色,把可以编辑的元素的背景色设置为浅黄色。

在 Codepen 上试试

:placeholder-shown

浏览器支持

它选择一个当前正在显示占位符文本的输入框元素。

例如:

input:placeholder-shown {
  color: red;
}

这会给当前显示占位符文本的input元素设置一个颜色为红色的样式,注意这里设置的只是光标的颜色。

在 Codepen 上试试

:default

浏览器支持

它选择那些在一组相关元素里面作为默认选项的元素。通常用于按钮和选择列表 / 菜单。

例如:

input:default {
  box-shadow: 0 0 2px 2px green;
}

input:defaut + lable {
  color: green;
}

这会给默认<input>元素的标签设置绿色阴影和颜色。

在 Codepen 上试试

:indeterminate

浏览器支持

它选择那些处于不确定状态的元素。例如,单选框组和复选框组里包含的那些未被选中的元素,或者进度百分比未知的进度条。

例如:

input[type="radio"]:indeterminate + label {
  color: red;
}

如果组中还没有任何一个元素被选,。这会给这个组的标签添加一个颜色为红色的样式。

在 Codepen 上试试

:valid:invalid

浏览器支持

它选择那些根据有效性语义或值的有效或无效的元素。如果没有定义值的有效性规则,则这个元素既不满足:valid也不满足:invalid

例如:对于一个输入类型为email的元素:

input:invalid {
  color: red;
}
input:valid {
  color: green;
}

这会根据元素里面输入的电子邮件是否有效为依据去为元素的文本设置不同颜色。

在 Codepen 上试试

:in-range:out-of-range

浏览器支持

这些选择器只适用于那些具有范围限制的元素,例如,定义了最大最小值的元素。如果没有定义,则都不匹配。

例如:一个设置了最小只为 1 最大值为 5 的输入框。

input:out-of-range {
  color: red;
}

input:in-range {
  color: green;
}

这会给依据输入的值设置不同的颜色样式。

在 Codepen 上试试

在某些情况下,某些选择器会具有与:valid:invalid相同的效果。

:required:optional

浏览器支持:required

浏览器支持:optional

这些选择器分别适用于在提交表单的之前必填的和选填的表单元素。那些非表单元素不会被匹配。

例如:

input:optional {
  color: gray;
}
input:required {
  color: red;
}

这会给那些必填的元素添加设置红色的样式,给非必填的元素添加灰色的样式。

在 Codepen 上试试

树结构伪类

这类选择器包括那些允许基于文档树中的信息进行选择,但是不能由其它选择器表示的选择器,例如元素相对于其父元素的位置。

: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 的元素表示的列。

例如,有如下表:

table image

对于以下样式

col.selected || td {
  background-color: lightyellow;
}

这会给所选列(价格)的单元格(<td>元素)设置浅黄色的背景色。

在 Codepen 上试试

: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;
}

这会给表格的第一列元素添加一个浅黄色背景色,。被最后一列元素添加一个浅绿色背景色。

在 Codepen 上试试

结论

第四级选择器使您可以轻松声明复杂的选择规则。

我们涵盖了2019年1月《编辑器草案》规范中定义的大多数选择器,但是,我遗漏了一些可能会很快消失或很快改变的选择器,或者关于它们的信息不多(除了不受支持) 任何浏览器):

但绝对要注意这些以及第4级选择器的其余部分。

7 Useful JavaScript Tricks

原文地址: https://davidwalsh.name/javascript-tricks

原文作者: David Walsh

就像其他编程语言一样,JavaScript也有许多技巧可以完成简单或困难的任务。一些技巧广为人知,而一些技巧则足以让你大吃一惊,今天让我们来看看你可以开始使用的七个JavaScript技巧吧。

Get Unique Values of an Array

获取一系列唯一的值可能比你想象中的要简单:

var j = [...new Set([1, 2, 3, 3])]
>> [1, 2, 3]

Array and Boolean

是否需要从数组中过滤一些值,比如 falsenull等,也许你可能不知道这个技巧:

f.filter(Boolean)

Create Empty Objects

当你可以使用 {} 创建一个看起来是空的空对象,但是它仍然具备 __proto__ 以及一些其他方法,但是有一种方法可以创建一个纯粹的空对象:

let dict = Object.create(null);

// dict.__proto__ === "undefined"
// No object properties exist until you add them

Merge Objects

在 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};

Require Function Parameters

能够为函数参数设置默认值是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');

Destructuring Aliases

解构是JavaScript的一个非常受欢迎的补充,但有时我们更喜欢用其他名称来引用这些属性,所以我们可以利用别名:

const obj = { x: 1 };

// Grabs obj.x as { x }
const { x } = obj;

// Grabs obj.x as { otherName }
const { x: otherName } = obj;

Get Query String Parameters

多年来,我们编写了大量的正则表达式来获取查询字符串值,但那些日子已经过去了,让我们来看看 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://v8.dev/blog/v8-release-75
>
> 原文作者: Dan Elphick
>
> 翻译作者: icepy

创建 issues 标题

请使用原文标题(英文),内容部分使用中文翻译,例如:

QQ20190604-094706@2x

创造性的使用 Console API!

原文地址: https://areknawo.com/getting-creative-with-the-console-api/

原文作者: https://areknawo.com/author/areknawo/

创造性的使用 Console API!

Javascript 中进行调试总是与 Console API 有密不可分的关系。但是往往我们只用过 console.log()。那么,你知道我们还可以用其他用法吗?使用console.log()的时候,它的大段式输出是否曾困扰过你?你想让你的调试信息更美观吗?💅如果有这个想法的话,接着往下看,我们将发现 Console API 会有多美观和有趣!

Console.log()

别不信,console.log() 自身就已经有了一些你不知道的附加功能。当然,它基础的功能 - 打印日志 - 不会受到影响。我们唯一能做的事情就是将它输出的日志变漂亮。让我们尝试一下,好吗?😁

字符串替换

唯一一件和 console.log() 方法密不可分的事情是你可以使用一个被称为 字符串替换 的方式来使用它。使用这种方式,你可以先在字符串中使用一个特殊的表达式,随后使用自定义的参数替换它。它看起来就像下面这样:

console.log("Object value: %o with string substitution",
    {string: "str", number: 10});

12

是不是很棒?问题是这种字符串的替换方式有多种:

  • %o / %O - 使用对象来替换;
  • %d / %i - 使用整数来替换;
  • %s - 使用字符串来替换;
  • %f - 使用浮点数来替换;

但是,即便这样,你可能还是会问,到底啥时候需要用到这个特性呢?特别是就像下面这样,你可以简单的传递多个参数给 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.

CSS

还有一种我们之前没有学过的类似于字符串替换的命令。这个命令是 %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.XXX

你可能早已知道一些 Console API 的其他用法。我所讲的是这些可以往日志上添加一些附加信息的方法。在这之前,让我们先简单过一下它们的用法,好吗?

Warning

console.warn() 方法的行为和 console.log(就像大多数打日志的方法那样) 很像,但是,需要多说一句,它有自己的样式。 ⚠ 在大多数浏览器中,会是黄色同时会有一个警告标记。这个方法执行时的所有调用过程也会被默认返回,因此你可以快速定位出现警告(也可能是bug)的地方。

console.warn("This is a warning!");

1

Error

console.error()方法,和console.warn()有着类似的调用堆栈的输出,也有着特殊的样式。它通常是红色配合一个表示错误的图标。❌ 它清楚地通知了用户有一些东西可能不那么正常。还有一个重要的事情,.error() 方法仅仅只是一个控制台输出,而没有任何附加的功能,比如说停止代码的执行(要想停止代码的执行,你需要抛出一个错误)。温馨提示,对于许多初学者而言,对于这一行为可能会有一些不确定。

console.error("This is an error!");

1

Info & debug

还有两个方法可用来增加日志的规范性。这两个方法是console.info()console.debug()。🐞 这两个方法的输出不总是会有特殊的样式 -- 在某些浏览器中可能仅仅只是一个表示信息的图标。不管是这两个方法还是前面说到的方法,都可以让你更加明确的对日志信息进行分类。在不同的浏览器中(例如:我目前使用的基于 Chromiun 内核的这个),调试用的 GUI 工具可以让你选择特定的日志类型进行显示,例如错误、调试或者是消息信息。对于我们来说,多了一种组织信息输出的功能。

console.info("This is very informative!");
console.debug("Debugging a bug!");

12

Assert

还有一种特殊的 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()方法就行,随后所有旧的调试信息就都没了。这是个很好的特性,甚至于很多浏览器的调试工具中都内置了这个功能的触发按钮。👍

Timing

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,这是专门为了性能测试而设计的

Counting

在你有很多日志输出以至于你不知道给定的代码被执行了多少次的时候 -- 你可能已经猜到了!也有一个 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

从我个人的角度来看,我觉得这个特性没有太大用途,但是有这个方法听起来也挺不错的。可能仅仅只有我这么认为吧...

Tables

我认为这是最被低估的 Console API 的特性之一(甚至超过了之前提到的CSS 样式)。👏当你在调试和检查各种类型的对象的时候,输出真实的、有顺序的表格到控制台是一件非常非常有用的事情。没错,你真的能够在控制台中展示一个表格。所有你需要做的事情仅仅是通过调用console.table()同时传递一个参数-比如说对象或者数组(基础数据类型还是以普通的方式进行输出,对象和数组则会以更小的结构打印出来)。试下下面的代码片段你就知道我在说啥了。

console.table([[0,1,2,3,4], [5,6,7,8,9]]);

1

Console ASCII art

说到控制台的艺术,不得不提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 会更时尚一点。

Modern logs

正如你说见,你的日志和调试过程可以不用这么单调!有很多比console.log()好得多的方式。使用在这篇文章中学到的知识,剩下的选择就出自你自己了!你可以保持传统的console.log()和浏览器自带的格式化效果,也可以通过上文介绍的内容在你的控制台中增添一些新鲜的东西。总之,怎么开心怎么来吧,即便你正在解决一个难缠的bug!🐞

我希望你喜欢这篇文章,同时也希望能给你带来一些新的知识。请一定要将这篇文章分享给那些想把他们的控制台变得丰富多彩的人,也欢迎在文末留下你的意见和评论!最后,希望大家关注我的推特、Facebook,不妨在newsletter注册一个账号!再次,感谢阅读,希望下篇文章还能看到你!

ES5 to ESNext — here’s every feature added to JavaScript since 2015

原文地址: https://medium.freecodecamp.org/es5-to-esnext-heres-every-feature-added-to-javascript-since-2015-d0c255e13c6e

原文作者: Flavio Copes

ES5 to ESNext —  自 2015 以来 JavaScript 新增的所有新特性

i_1

这篇文章的出发点是为了帮助前端开发者串联 ES6前后的 JavaScript 知识,并且可以快速了解 JavaScript 语言的最新进展。

JavaScript 在当下处于特权地位,因为它是唯一可以在浏览器中运行的语言,并且是被高度集成和优化过的。

JavaScript 在未来有着极好的发展空间,跟上它的变化不会比现在更加的困难。我的目标是让你能够快速且全面的了解这门语言可以使用的新内容。

点击这里获取 PDF/ePub/Mobi 版本

目录

ECMAScript 简介

ES2015

ES2016

ES2017

ES2018

ESNext

ECMAScript 简介

每当阅读 JavaScript 相关的文章时,我都会经常遇到如下术语: ES3, ES5, ES6, ES7, ES8, ES2015, ES2016, ES2017, ECMAScript 2017, ECMAScript 2016, ECMAScript 2015 等等,那么它们是指代的是什么?

它们都是指代一个名为 ECMAScript 的标准。

JavaScript 就是基于这个标准实现的,ECMAScript 经常缩写为 ES。

除了 JavaScript 以外,其它基于 ECMAScript 实现语言包括:

  • ActionScript ( Flash 脚本语言),由于 Adobe 将于 2020 年末停止对 Flash 的支持而逐渐失去热度。
  • JScript (微软开发的脚本语言),在第一次浏览器大战最激烈的时期,JavaScript 只被Netscape所支持,微软必须为 Internet Explorer 构建自己的脚本语言。

但是现在流传最广、影响最大的基于 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 当前的版本。

目前的最新的 ECMAScript 版本是 ES2018

于 2018 年 6 月发布。

TC39 是什么?

TC39(Technical Committee 39)是一个推动 JavaScript 发展的委员会。

TC39的成员包括各个主流浏览器厂商以及业务与浏览器紧密相连的公司,其中包括 Mozilla,Google ,Facebook,Apple,Microsoft,Intel,PayPal,SalesForce等。

每个标准版本提案都必须经过四个不同的阶段,这里有详细的解释

ES Versions

令我费解的是 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 来定义版本号。

下图比较清晰的展示了版本号与年份的关联:

i_4

接下来,我们来深入了解 JavaScript 自 ES5 以来增加的特性。

let和const

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存在变量提升机制。为避免混淆,请在函数开头声明变量,养成良好的编码规范。

Using let

let 是ES2015中引入的新功能,它本质上是具有块级作用域的 var 。它可以被当前作用域(函数以及块级作用域)以及子级作用域访问到。

现代 JavaScript 开发者在 letvar 的选择中可能会更倾向于前者。

如果 let 看起来是一个很抽象的术语,当你阅读到 let color = 'red' 这一段,因为使用 let 定义了color 为红色,那么这一切就变的有意义了。

在任何函数之外用 let 声明变量,和 var相反的是 它并不会创建全局变量。

Using const

使用变量 varlet 声明的变量可以被重新赋值。 使用 const 声明的变量一经初始化,它的值就永远不能再改变,即不可重新被赋值。

const a = 'test'

我们不能再为 a 进行赋值操作。然而,a 如果它是一个具有属性或者方法的对象,那么我们可以改变它的属性或者方法。

const 并不意味着具有不可变性,只是保证用 const 声明的变量的引用地址不被变更。

类似于 letconst 也具有块级作用域。

现代 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
})

Classes类

JavaScript 实现继承的方式比较罕见:原型继承。原型继承虽然在我看来很棒,但与其它大多数流行的编程语言的继承实现机制不同,后者是基于类的。

因此 Java、Python 或其它语言的开发者很难理解原型继承的方式,因此 ECMAScript 委员会决定在原型继承之上实现 class 的语法糖,这样便于让其它基于类实现继承的语言的开发者更好的理解 JavaScript 代码。

注意:class 并没有对 JavaScript 底层做修改,你仍然可以直接访问对象原型。

class 定义

如下是一个 class 的例子:

class Person {
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}

class 具有一个标识符,我们可以使用 new ClassIdentifier() 来创建一个对象实例。

初始化对象时,调用 constructor方法,并将参数传递给此方法。

类声明语句中也可以增加类需要的一些原型方法。在这种情况下 helloPerson 类的一个原型方法,可以在这个类的对象实例上调用:

const flavio = new Person('Flavio')
flavio.hello()

Class 继承

一个子类可以 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 没有内置真正意义上的受保护的私有方法。

社区有解决方法,但我不会在这里做讲解。

Getters 和 setters

你可以通过增加方法 前缀 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`

这个用法是独一无二的,因为它提供了许多普通字符串所没有的功能,如下:

  • 它为定义多行字符串提供了一个很好的语法
  • 它提供了一种在字符串中插入变量和表达式的简单方法
  • 它允许您创建带有模板标签的DSL (DSL意味着领域特定语言,例如:就如同在 React 中使用 styled-components 定义你组件的 CSS 一样)

下面让我们深入每个功能的细节。

多行字符串

在 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'}`

Template tags

标记模板可能是一个听起来不太有用的功能,但它实际上被许多流行的库使用,如 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.buttongql模板标签其实都是函数:

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个部分:x6

拿到了这些值的函数就可以对其做任意处理,这就是这个特性的威力所在。

比如最简单的处理就是字符串插值,把字面量表达式拼接起来:

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

nameage就包含了对应的值。

这个语法同样可以用到数组当中:

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
}

super()

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

For-of循环

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 遍历属性名

Promises

promise的一般定义: 它是一个代理,通过它可以最终得到一个值.

Promise是处理异步代码的一种方式,可以少写很多回调。

异步函数是建立在promise API上面的,所以理解Promise是一个基本的要求。

promise的原理简述

一个promise被调用的时候,首先它是处于pending状态。在promise处理的过程中,调用的函数(caller)可以继续执行,直到promise给出反馈。

此时,调用的函数等待的promise结果要么是resolved状态,要么是rejected状态。但是由于JavaScript是异步的,所以promise处理的过程中,函数会继续执行

为什么JS API使用promises?

除了你的代码和第三方库的代码之外,promise在用在现代的Web API中,比如:

在现代的JavaScript中,不使用promise是不太可能的,所以我们来深入研究下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。

通过resolvereject,我们可以得到一个返回值,返回值可以是字符串也可以是对象。

使用一个promise

上面讲了怎么创建一个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,从而创建promise链条(chain)。

一个很好的例子就是Fetch API,它是基于XMLHttpRequest API的一个上层API,我们可以用它来获取资源,并且在获取到资源的时候链式执行一系列promise。

Fetch API是一个基于promise的机制,调用fetch()相当于使用new Promise()来声明我们自己的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

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 文件使用。

ES 模块语法

引入模块的语法:

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'

因为这里既不是使用绝对地址,也不是使用的相对地址。

其它的 import/export 语法

我们了解了上面的例子:

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 机制的。这意味着当你引用远程模块的时候,必须使用合法的 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()

repeat()

根据指定的次数重复字符串:

'Ho'.repeat(3) //'HoHoHo'

没有提供参数以及使用 0 作为参数的时候返回空字符串。如果给一个负数参数则会得到一个 RangeError 的错误。

codePointAt()

这个方法能用在处理那些需要 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()

这个方法用来帮助比较对象的值:

使用方式:

Object.is(a, b)

返回值在下列情况之外一直是 false

  • ab 是同一个对象
  • ab 是相等的字符串(用同样的字符组合在一起的字符串是相等的)
  • ab 是相等的数字
  • ab 都是 undefined, null, NaN, true 或者都是 false

0-0 在 JavaScript 里面是不同的值, 所以对这种情况要多加小心(例如在比较之前,使用 + 一元操作符将所有值转换成 +0)。

Object.assign()

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.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 数据结构允许我们在一个容器里面增加数据。

一个 Set 是一个对象或者基础数据类型(strings、numbers或者booleans)的集合,你可以将它看作是一个 Map,其中值作为映射键,map 值始终为 true。

初始化一个 Set

Set 可以通过下面的方式初始化:

const s = new Set()

向 Set 中添加一项

你可以使用 add 方法向 Set 中添加项:

s.add('one')
s.add('two')

Set 仅会存贮唯一的元素,因此多次调用 s.add('one') 不会重复添加新的元素。

你不可以同时向 set 中加入多个元素。你需要多次调用 add() 方法。

检查元素是否在 set 中

我们可以通过下面的方式检查元素是否在 set 中:

s.has('one') //true
s.has('three') //false

从 set 中删除一个元素:

使用 delete() 方法:

s.delete('one')

确定 set 中元素的数量

使用 size 属性:

s.size

删除 set 中的全部元素

使用 clear() 方法:

s.clear()

对 set 进行迭代

使用 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 } 对象的形式返回每一个元素,直到迭代结束,这时 donetrue

你也可以调用 set 的 forEach() 方法:

s.forEach(v => console.log(v))

或者你就直接使用 for..of 循环吧:

for (const k of s) {
  console.log(k)
}

使用一些初始值初始化一个 set

你可以使用一些值初始化一个 set:

const s = new Set([1, 2, 3, 4])

将 set 转换为一个数组

const a = [...s.keys()]
// or
const a = [...s.values()]

WeakSet

一个 WeakSet 是一个特殊的 Set.

在 set 中,元素不会被 gc(垃圾回收)。一个 weakSet 让它的所有元素都是可以被 gc 的。weakSet 中的每个键都是一个对象。当这个对象的引用消失的时候,对应的值就可以被 gc 了。

下面是主要的不同点:

  1. WeakSet 不可迭代
  2. 你不能清空 weakSet 中的所有元素
  3. 不能够得到 weakSet 的大小

一个 weakSet 通常是在框架级别的代码中使用,仅仅暴露了下面的方法:

  • add()
  • has()
  • delete()

Map

一份map结构的数据允许我们建立数据和key的关系

在ES6之前

在引入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

引入Map之后

ES6引入了Map数据结构,它为我们处理这种数据结构提供了一种合适的工具

Map的初始化:

const m = new Map()

添加条目到Map中

你可以通过set()方法把条目设定到map中:

m.set('color', 'red')
m.set('age', 2)

通过key值从map中获取条目

你可以通过get()方法从map中取出条目:

const color = m.get('color')
const age = m.get('age')

通过key值从map中删除条目

使用delete()方法:

m.delete('color')

从map中删除所有条目

使用clear()方法:

m.clear()

通过key值检查map中是否含有某个条目

使用has()方法

const hasColor = m.has('color')

获取map中的条目数量

使用 size 属性:

const size = m.size

用value值初始化一个map

你可以用一组value来初始化一个map:

const m = new Map([['color', 'red'], ['owner', 'Flavio'], ['age', 2]])

Map 的key值

任何值(对象,数组,字符串,数字)都可以作为一个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

使用Iterate迭代器获取map的keys值

Map提供了keys()方法,通过该方法我们可以迭代出所有的key值:

for (const k of m.keys()) {
  console.log(k)
}

使用Iterate迭代器获取map的values值

Map提供了values()方法,通过该方法我们可以迭代出所有的value值:

for (const v of m.values()) {
  console.log(v)
}

使用Iterate迭代器获取key-value组成的键值对

Map提供了entries()方法,通过该方法我们可以迭代出所有的键值对:

for (const [k, v] of m.entries()) {
  console.log(k, v)
}

使用方法还可以简化为:

for (const [k, v] of m) {
  console.log(k, v)
}

将map的keys值转换为数组

const a = [...m.keys()]

将map的values值转换为数组

const a = [...m.values()]

WeakMap

WeakMap是一种特殊的Map

在一个map对象中,定义在其上数据永远不会被垃圾回收,WeakMap替而代之的是它允许在它上面定义的数据可以自由的被垃圾回收走,WeakMap的每一个key都是一个对象,当指向该对象的指针丢失,与之对应的value就会被垃圾回收走。

这是WeakMap的主要不同处:

  1. 你不可以在WeakMap上迭代keys值和values值(或者key-value键值对)
  2. 你不可以从WeakMap上清除所有条目
  3. 你不可以获取WeakMap的大小

WeakMap提供了如下几种方法,这些方法的使用和在Map中一样:

  • get(k)
  • set(k, v)
  • has(k)
  • delete(k)

关于WeakMap的用例不如Map的用例那么明显,你可能永远也不会在哪里会用到它,但从实际出发,WeakMap可以构建不会干扰到垃圾回收机制的内存敏感性缓存,还可以满足封装的严谨性及信息的隐藏性需求。

Generators生成器

Generators是一种特殊的函数,它能够暂停自身的执行并在一段时间后再继续运行,从而允许其它的代码在此期间运行(有关该主题的详细说明,请参阅完整的“javascript生成器指南”)。

Generators的代码决定它必须等待,因此它允许队列中的其它代码运行,并保留“当它等待的事情”完成时恢复其操作的权力。

所有这一切都是通过一个简单的关键字“yield`”完成的。当生成器包含该关键字时,将停止执行。

generator生成器可以包含许多yield关键字,从而使自己能多次停止运行,它是由*function关键字标识(不要将其与C、C++或Go等低级语言中使用的取消指针引用操作符混淆)。

Generators支持JavaScript中全新的编程范式,包括:

  • 在generator运行时支持双向通信
  • 不会“冻结”长期运行在程序中的while循环

这里有一个解释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,它的作用域范围更小。


Array.prototype.includes()

该特性引入了一种更简洁的语法,同来检查数组中是否包含指定元素。

对于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等其它多种语言)。

i_2


这些都是2016年引入的特性,现在让我们进入2017年。


字符串填充

字符串填充的目的是给字符串添加字符,以使其达到指定长度

ES2017引入了两个String方法:padStart()padEnd()

padStart(targetLength [, padString])
padEnd(targetLength [, padString])

使用例子:

i_3

Object.values()

该方法返回一个数组,数组包含了对象自己的所有属性,使用如下:

const person = { name: 'Fred', age: 87 }
Object.values(person) // ['Fred', 87]

Object.values()也可以作用于数组:

const people = ['Fred', 'Tony']
Object.values(people) // ['Fred', 'Tony']

Object.entries()

该方法返回一个数组,数组包含了对象自己的所有属性键值对,是一个[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']]

Object.getOwnPropertyDescriptors()

该方法返回自己(非继承)的所有属性描述符,JavaScript中的任何对象都有一组属性,每个属性都有一个描述符,描述符是属性的一组属性(attributes),由以下部分组成:

  • value: 熟悉的value值
  • writable: 属性是否可以被更改
  • get: 属性的getter函数, 当属性读取时被调用
  • set: 属性的setter函数, 当属性设置值时被调用
  • configurable: 如果为false, 不能删除该属性,除了它的value值以为,也不能更改任何属性。
  • enumerable: 该属性是否能枚举

Object.getOwnPropertyDescriptors(obj)接受一个对象,并返回一个带有描述符集合的对象。

In what way is this useful?

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建立的

为什么要引入async/await

它减少了围绕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

关于 Promise

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 的功能。


Rest/Spread Properties

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.prototype.finally()

当一个 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 正则表达式改进的具体补充:

RegExp lookbehind assertions: 根据前面的内容匹配字符串

这是一个 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

Unicode属性转义 \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 获取使用所有属性的详细信息。

Named capturing groups

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';

The s flag for regular expressions

The 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 ?

ESNext 是一个始终指向下一个版本 JavaScript 的名称。

当前的 ECMAScript 版本是 ES2018,它于2018年6月被发布。

历史上 JavaScript 标准化的版本都是在夏季被发布,因此我们可以预期 ECMAScript 2019 将于 2019 年的夏季被发布。

所以在编写本文时 ES2018 已经被发布,因此 ESNext 指的是 ES2019。

ECMAScript 标准的提案是分阶段组织的,第一到第三阶段属于功能性的孵化,第四阶段的功能才最终确定为新标准的一部分。

在编写本文时主要浏览器都实现了第四阶段大部分的功能,因此我将在本文中介绍它们。

其中一些变化主要在内部使用,但知道发生了什么这也很好。

第三阶段还有一些其它功能,可能会在接下来的几个月内升级到第四阶段,你可以在这个 Github 仓库中查看它们:https://github.com/tc39/proposals

Array.prototype.{flat,flatMap}

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' ]

Optional catch binding

有时候我们并不需要将参数绑定到 try/catch 中。

在以前我们不得不这样做:

try {
  //...
} catch (e) {
  //handle error
}

即使我们从来没有通过 e 来分析错误,但现在我们可以简单的省略它:

try {
  //...
} catch {
  //handle error
}

Object.fromEntries()

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

String.prototype.{trimStart,trimEnd}

这些功能已经被 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'

Symbol.prototype.description

现在你可以使用 description 来获取 Symbol 的值,而不必使用 toString() 方法:

const testSymbol = Symbol('Test')
testSymbol.description // 'Test'

JSON improvements

在此之前 JSON 字符串中不允许使用分隔符(\u2028)和分隔符(\u2029)。

使用 JSON.parse 时,这些字符会导致一个 SyntaxError 错误,但现在它们可以正确的解析并如 JSON 标准定义的那样。

Well-formed JSON.stringify()

修复 JSON.stringify() 在处理 UTF-8 code points (U+D800 to U+DFFF)。

在修复之前,调用 JSON.stringify() 将返回格式错误的 Unicode 字符,如(a "�")。

现在你可以安全放心的使用 JSON.stringify() 转成字符串,也可以使用 JSON.parse() 将它转换回原始表示的形态。

Function.prototype.toString()

函数总会有一个 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

Varnish Cache vs. NGINX Cache: Performance Comparison

原文地址: https://blog.resellerclub.com/varnish-cache-vs-nginx-cache-performance-comparison

原文作者: Sagar Kulkarni

翻译作者: Fedora

如果说每一个站点维护者有一个都想去实现的目标,那肯定是实现站点快速加载。毋庸置疑,网页的加载时间很大程度影响着用户的留存以及搜索引擎的排名。通过采取一系列的技术手段比如图片优化、内容网络分发(CDN)、使用正确的缓存服务器等。网站的维护者不断努力的减少页面的加载时间。虽然所有的措施都是对减少页面加载时间有益的,但是这篇文章主要关注缓存。Varnish 缓存 与 Nginx 缓存是两大比较重要且流行的缓存方案有助于提升网站的体验。

虽然这两种缓存方案都有各自的优势,但是一份详细的关于这两种方案特性的学习文档可以帮助你更好的选择那种缓存方案。

Varnish 缓存

varnish

Varnish 缓存是一个前端 web 加速器有助于动态和内容繁多的站点应对高流量冲击。Varnish 是一个反向的 HTTP 代理,缓存来自服务器的静态和动态内容以此提高网站的访问体验。提升网站的访问速度是显而易见的。

Varnish 缓存也允许开发者编辑规则和设置策略通过 .vcl (Varnish 配置语言)。简而言之,vcl 能够让网站的维护者直接通过 Varnish 处理网站的流量。这样你就能够完全的控制你想要缓存的内容和缓存的方式。领先的社交网站像 Facebook 、Twitter 或者是页面内容丰富的 Wikipedia,在这些相似的网站中,都使用了 Varnish 缓存来管理内容缓存。

以上简要的介绍了 Varnish ,现在我们来了解一下 Nginx 缓存的工作原理。

Nginx 缓存

nginx

Nginx 正式发布于2004年,是一个开源的 web 服务器用来或服务代理。它通常提供邮件代理、反向代理、负载均衡、HTTP 缓存、微缓存等等。这些都能帮助减少加载时间和提升网站的体验。大部分高流量网站和多应用环境,都是使用 Nginx 缓存。众所周知,Nginx 在处理并发上是非常高效的。

在简单的熟悉了两种缓存方案之后,现在是时候进行深入的评估哪一种方案适合你的网站。

Varnish vs. NGINX

实际上,很难对 Varnish 和 Nginx 做一个比较。因为 Varnish 和 Nginx 基本上是相似的。它们都可以被用来做服务器的反向代理和负载均衡。但是如果我们深入了解他们背后的技术,我们会发现几处 Varnish 与 Nginx 在缓存上的表现几乎是对立的。

灵活性

drawing-realistic-clouds-with-svg-and-css

译文出自:闪电矿工翻译组

原文地址:Drawing Realistic Clouds with SVG and CSS

原文作者:Beau Jackson

仓库原文链接:Drawing Realistic Clouds with SVG and CSS

译者:sichenguo

【译】来用 SVG 和 CSS 画朵云彩吧

希腊神话中有这样一个故事是讲述宙斯创造出来一个云女神涅斐勒,并且类似大多数的希腊神话一样的,这个故事非常的奇异且限制级。下面一个简短克制的版本。

我们能够知道的是: 涅斐勒是由宙斯以他自己美丽的妻子的形象创造的。一个凡人遇见涅斐勒,陷入爱河,并且他们一起有了一个孩子,确切的说是一个半人半马的婴儿。

很怪诞对吧,值得庆幸的是,在浏览器中创建云的过程要简单得多,而且风险要小得多。
image

(Demo)

最近,我发现开发者Yuan Chuan 已经实现了用代码生成逼真的云。对我来说,浏览器中的云这个概念一直如同希腊神话中的那边神秘。

让我们来看一下这个’画笔‘吧 (点这里),可以见到的是作者通过使用 box-shadow 作为包含两个滤镜的 <filter> 元素的补充实现了这个令人惊叹的‘云图’!

想要绘制出兼顾写实和精致的云图,需要搭配使用feTurbulencefeDisplacementMap 这两个滤镜。这两个滤镜不仅可以具有强大且复杂的功能,并且还可以提供一些令人兴奋的特性(其中包含奥斯卡获奖算法))!当然,这些功能在浏览器‘引擎盖’下有着令人生畏的复杂性。

虽然这些 SVG 滤镜的物理特性超出了本文的范围,但 MDNw3.org 上提供了大量文档供学习参考。另外还有 feTurbulencefeDisplacement 介绍

对于本文,我们将专注于学习使用这些 SVG 滤镜来实现令人惊奇的效果。滤镜背后的实现算法并不在我们的研究范围内,就像艺术家虽然可以绘制出美丽的景观但却不用懂得油漆的分子结构。

image

而我们需要做的只是密切关注一小部分 SVG 属性,这些属性使得我们可以在浏览器中绘制逼真的云图。通过学习这些属性可以让我们在项目中按照自己的意愿更好的制作出特定的滤镜效果。

一些必要的前置基础知识

CSS 规则 box-shadow的五个值得关注的属性:

box-shadow: <offsetX> <offsetY> <blurRadius> <spreadRadius> <color>;

让我们来增大这些值(可能会高于正常的开发者会做的),这样在视图的右下方就会有阴影出现。 !image(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;
}

你肯定表演或者见过过皮影戏吧?
imageCredit: 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 滤镜依旧没能给我们带来什么惊喜。

image
(Demo)

别担心!这才仅仅只是开始,更有趣的还在后面。

测试 feDisplacementMap scale 属性

使用这一属性进行的一些实验可以产生显著的效果。暂时,让我们保持它的 feTurbulence 不变,只调整 DisplacementMapscale 属性。

随着 scale 的增加(每次增加 30 ),我们的源 <div> 开始扭曲变形且它的投下阴影更接近天空中云随机的形状。

<feDisplacementMap in="SourceGraphic" scale="180" />

image
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;
}

现在我们的图形越来越接近真实的云效果了!

image

调整 box-shadow 的值

下面的一组图像显示了 blur 对 box-shadow 效果的影响。下面的图形中 blur 的数组依次递增。

image
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;
}

image好的,但是现在的源 div 元素正在成为障碍。😫

等等!我们已经扩大了源元素的尺寸,但是它现在已经开始影响我们的“云(白色阴影)”了。 很简单,只需要相距更远的地方投影遍可以解决这个遮挡问题。

而通过通过一些 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 */
}

很棒,这样看起来我们的云图已经初步成型了。

codepen
image

平摊在屏幕之上的云彩,嗯,这应该是一个对我们绘制出的云图一个很好的描述,总是感觉还缺少点什么,我们应该还可以做的会更好的!

用层来表达深度

这就是我们想要的:

A photo of clouds against a blue sky. The clouds have shades of gray that provide depth.Credit: 图源:pcdazero

从这张照片中云的深度,质感和丰富程度来看,有一件事是清楚的:宙斯是上过艺术学校。至少,他肯定是阅读过通用组件设计原则的,这个原则阐述了一个通用性的概念:

[...]照明偏差在深度和自然的解释中起着重要作用,并且可以由设计师以各种方式操纵......使用明暗区域之间的对比度使得外观具有景深。

这段话为我们提供了一个对云图进行优化更加真实的提示。通过将不同形状,大小和颜色的图层堆叠在一起,我们可以在参考图像制造对比度中以来渲染云图。需要的只是使用 filter 制造多个图层。

image

<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 值的时得到的结果 :

image值取两端的值时,图形都会趋向于圆形。区别就是越小,更模糊。值越大,图形就更加显得生硬。

turbulence,噪音,频率和 octave 这样的词可能看起来很奇怪甚至令人困惑。但不要害怕!将滤镜的效果类比为声波实际上非常准确。我们可以将低频率(baseFrequency=0.001)等同于低噪声和低频率,更高的 (baseFrequency=0.1) 值则与更清晰的音调相对应。

我们可以看到,我们对积云效果的对应的 baseFrequency 值大概位于 0.005~0.01 范围之间。

使用 numOctaves 添加细节

增大 numOctaves 的值可以以极其细致的细节渲染图像。但是这需要大量计算,因此需要注意的是:更细致的渲染也可能带来的还有性能问题,所以如非必要,请将此值尽量控制在一定范围内。

imageThe higher the value we put into numOctaves the more granular detail give to our cloud.

好消息就是,在此例中,我们的云图需要的细节并不需要使用过高的 numOctaves 值,如上图所示,numOctaves 为 4 或者 5 就可以得到不错的效果。

效果图

codepen
image

改变 seed 属性值可以产生多样的云图

至于 seed 属性还可以再深入研究一下。但是,就我们的目的而言, seed 的作用可以对形状造成影响。

Perlin Noise 函数(上文提到)会使用此值作为其随机数生成器的初始值。如果不设置该属性将默认 seed 为零。跟 baseFrequency 的区别就是,无论我们给出什么价值 seed,都不需要担心性能损失。

image

不同的 seed 值产生不同的形状

上面的 GIF 展示了不同 seed 值时的效果。需要注意的是,每朵云都是分层的复合云。(虽然我调整了每个图层的属性,但我保持各自的 seed 值分布均匀。)

image
Credit: Brockenhexe

在这里,仔细观察参考图像,我将 3 个‘云’层(不透明度不同)叠加到一个 div 上。通过反复试验和调整 seed 参数值,最终得到的下面这个类似于照片中云的形状的效果图。

codepen

image

天空才是真正的极限

当然,如果我们认为我们绘制的这个云彩要比宙斯创造的女神还要漂亮,肯定就是狂妄自大了。

但是,随着我们对 CSSSVG 滤镜的更多的了解,我们就越有能力创造出在视觉上令人惊叹的图形,比如下面这些:

Reflecting Mist

imageAnimated Reflecting mist

Alto-Cirrus Clouds

image

在这篇文章中我们沉浸在 SVG 强大功能和复杂性的海洋中,虽然 SVG 滤镜通常看起来比较复杂且难以理解。

然而就好像A Single Div project这个项目和Diana Smith's painting techniques这个例子一样,有趣的和有创意的方法总是可以得到令人惊艳的效果。

我相信很多开发者都有能力制作出一个云彩出来,但是一个可以轻松制作出云彩的工具会更受欢迎。因此我开发了一个小工具做这件事。

How to make your HTML responsive by adding a single line of CSS

原文地址: https://medium.com/free-code-camp/how-to-make-your-html-responsive-by-adding-a-single-line-of-css-2a62de81e431

原文作者: Per Harald Borgen

翻译作者: hanxiansen

中文标题:一行css代码搞定响应式布局


在这篇文章中,我将教你如何使用 CSS Grid 来创建一个超酷的图像网格图,它将根据屏幕的宽度来改变列的数量。最精彩的地方在于:所有的响应特性被添加到了一行 css 代码中。这意味着我们不必将 HTML 与丑陋的类名(如col-sm-4, col-md-8)混杂在一起,也不必为每个屏幕创建媒体查询。ok,让我们发车吧。

设置

在本文中,我将继续使用我在第一篇 CSS Grid 布局教程文章中的网格布局。然后,我们将在文章末尾添加图片。下面是我们初始化网格的外观:

https://cdn-images-1.medium.com/max/800/1*fJNIdDiScjhI9CZjdxv3Eg.png

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 ,其中就详细的解释了布局的基础知识。

让我们让列开始具有自适应特性吧。

基础响应单位: fraction

CSS 栅格布局带来了一个全新的值:fraction单位,fraction单位通常简写为fr,它允许你根据需要将容器拆分为多个块。

让我们将每一列更改为一个 fraction 单位宽:

.container {
    display: grid;
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: 50px 50px;
}

结果是栅格布局将会把整个宽度分成三个 fraction,每列占据一个 fraction 单位,效果如下:

t

如果我们将grid-template-columns的值更改为1fr 2fr 1fr,第二列的宽度将会是其它两列的两倍。总宽现在是四个 fraction 单位,第二列占据两个 fraction 单位,其它列各占一个 fraction。效果如下:

总的来说,fraction 单位值将使你可以很容易的更改列的宽度。

高级响应

然而,上面列子并没有给出我们想要的响应性,因为网格总是三列宽。我们希望网格能根据容器的宽度改变列的数量。要做到这一点,你必须学习如下三个概念:

repeat()

首先我们学习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

然后是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()

为了解决上述问题,我们需要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;
}

效果如下:
pic

ok!现在你已经了解了 CSS Grid 布局中最复杂的概念之一了,请给自己一个赞吧。

浏览器兼容性

在结束本文前,我提下浏览器支持情况,在撰写本文时,全球77%的网站将支持 CSS Grid,而且比例还在逐步攀升。

我想想2018将是 CSS 网格布局的元年。它将获得突破,并成为前端开发者的必备技能,就像过去几年 CSS Flexbox 布局发生的情况一样。

Understanding the For…of Loop In JavaScript

原文地址: https://blog.bitsrc.io/understanding-the-for-of-loop-in-javascript-8aded97d7ef8

原文作者: https://blog.bitsrc.io/@kurtwanger40

理解 JavaScript 中的循环

JavaScript的世界中,我们可以使用很多种循环表达式:

  • while 表达式
  • do...while 表达式
  • for 表达式
  • for...in 表达式
  • for...of 表达式

所有这些表达式都有一个基本的功能:它们会重复一件事情直到一个具体的条件出现。

在这篇文章中,我们将深入 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 - 我们进行迭代的对象

Iterables(可迭代的对象) and Iterator(迭代器)

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 方法需要遵循以下准则:

  • 必须返回一个包含 done, value 属性的对象
  • done 是一个布尔值,用来表示循环是否结束
  • value 是当前循环的值

举个例子:

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

内置的可迭代对象有有以下这些:

  • String
  • Map
  • TypedArray
  • Array
  • Set
  • Generator

注意,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 表示不存在。

for…of: Array

数组是可迭代对象。

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

for…of: String

字符串也是可迭代的。

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

for…of: Map类型

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

for…of: Set类型

const set = new Set(["Chidume","Nnamdi"])
for(const a of set) {
    log(a)
}
// 打印结果是:
// Chidume
// Nnamdi

for…of: TypedArray类型

const typedarray = new Uint8Array([0xe8, 0xb4, 0xf8, 0xaa]);
for (const a of typedarray) {
  log(a);
}
// 打印结果是:
// 232
// 180
// 248
// 170

for…of: arguments对象

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: 自定义可遍历对象(Iterables)

正如上一节那样,我们可以创建一个自定义的可以通过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和普通对象(plain objects)变成可遍历

简单对象是不可遍历的,通过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来遍历对象了。

在ES6的类(class)中使用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 的属性

Async Iterator

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

Conclusion

在这篇文章中我们深入研究了 for...of 循环,我们首先定义理解什么是 for...of,然后看看什么是可迭代的。for...of 为我们节省了许多复杂性和逻辑,并有助于使我们的代码看起来很清晰易读,如果你还没有尝试过,我认为现在正是时候。

Using SVG

原文地址: https://css-tricks.com/using-svg/

原文作者: Chris Coyier

翻译作者: chenmf

SVG是一种向量图的图片格式,即可伸缩向量图(Scalable Vector Graphics),可以在Adobe Illustrator里面生成。在Web中使用SVG很简单,但是也有一些需要知道的事情。

为什么用SVG

  • 压缩后文件体积小
  • 可以无损伸缩到任意尺寸(除非尺寸特别小)
  • 在retina屏幕上可以完美显示
  • 设计可控,比如交互和滤镜

怎么生成SVG

可以在Adobe Illustrator里设计并且得到SVG。下面是一个站在椭圆上的奇异鸟:
image
留意到画板刚好贴着设计主体的边缘,画布的大小在SVG里面的重要性和在PNG和JPG里面是一样的。
然后可以直接在Adobe Illustrator里面保存成SVG文件。
image

保存的时候,可以在duihua对话框里面选择SVG选项。完整的参考可以看SVG 介绍。这里选SVG 1.1就可以了。
image

当点击'OK'或者'SVG Code...'的时候,就会打开文本编辑器,显示SVG的编码。
image

<img>标签里面使用SVG

如果把SVG保存成文件之后,可以直接在<img>标签里面使用。

HTML
<img src="kiwi.svg" alt="Kiwi standing on oval">

在Illustrator里面,画板的大小是612px ✕ 502px:
image
这正是图片在页面中的大小。可以选择<img>标签并且改变widthheight来改变它的尺寸,就像PNG和JPG一样,比如:

前往codepen查看
image

浏览器支持

<img>标签里面使用需要有浏览器支持。基本上在IE8以上和Android 2.3以上都可以用。
如果你想要在不支持的浏览器里面使用,可以这样:

  1. 使用Modernizr来替换<img>src属性:
jQuery

if (!Modernizr.svg) {
  $(".logo img").attr("src", "images/logo.png");
}
  1. David Bushell提供了一个更简单的操作:
HTML

<img src="image.svg" onerror="this.onerror=null; this.src='image.png'">
  1. 使用SVGeezy

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>中使用是一样的。

如果要在不支持的浏览器里面使用:

  1. 用Modernizr把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);
}
  1. 另一个更方便的方法,就是利用多个背景(background),SVG的浏览器支持和多背景的很接近。
CSS

body {
  background: url(fallback.png);
  background-image: url(image.svg), none;
}

使用<img>background-image的问题

<img>background-image里面使用SVG,没法利用CSS对SVG内部进行控制,所以接着看下面的两种其他方式。

使用内联(inline)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编译有问题。

先优化SVG

Adobe Illustrator里面导出的SVG可能不是最优的,会包含一些冗余信息,比如DOCTYPE和生成信息。我们可以进一步优化,减少体积。Peter Collingridge给出了在线的SVG 优化,把需要优化的SVG上传,然后下载新的就可以了。
如果你是硬核玩家,可以直接用这个NodeJS工具自己优化。

用CSS来控制SVG

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);
}

下面是一个完整的例子:

前往codepen查看
image

更多阅读:

浏览器支持

内联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里面,页面不会崩溃,但是也不起作用。

在Data URL里面使用SVG

还可以把SVG转换成Data URL,转换之后可能不止原来的文件大小,但是它很方便,因为不需要额外产生网络请求。
Mobilefish.com上面有一个base64在线转换器,可以转成base64编码,但是这种方式不太推荐。记得去掉换行:
image
它可以在上述的所有场景里面使用,除了内联SVG。

个人比较推荐这个在线转换器,因为转换之后可读性比较强。

  • 用在<img>里面
HTML

<img src="data:image/svg+xml;base64,[data]">
  • CSS里面
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>里面起作用。

Data URI格式

上面的例子都是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输出。

相关参考

Bringing Flutter to the Web

原文地址: https://medium.com/flutter/bringing-flutter-to-the-web-904de05f0df0

原文作者: Kevin Moore

翻译作者: Fedora

Flutter For Web

用来构建漂亮、定制化应用的跨平台的 UI 框架 Flutter 现在已经支持 Web 开发了。我们很高兴推出了一个预览版的 SDK 可以让开发者直接使用 Flutter UI 和业务逻辑代码构建 web 应用在浏览器中运行起来。

Flutter 在 Web端的雄心

自从去年第一个公测版本推出之后,开发者使用 Flutter 构建跨 IOS 和 Android 的应用。但是 Flutter 自始至终被设计成一个跨平台的 UI 框架包括 Windows ,Mac,Fuchsia 甚至是 Raspberry Pi(树莓派)。因为 Flutter 是由 Dart 编写的,里面包含一个生产环境的编译器来构建原生的代码和 JavaScript 代码,所以我们有一个坚实的基础。剩下的挑战就是替换基于 Skia-based 的图形引擎和文本渲染来适配 Web 平台。

要做到这些,我们需要提供:

  • 快速,无抖动的且每秒60帧的页面交互
  • 考虑到 Flutter 在其他平台提供的能力和视觉
  • 和现有开发模式整合的高效率的开发体验
  • 支持所有现代浏览器的核心 Web 功能

虽然Flutter for web是一项正在进行中的工作,而且为了实现上述功能还有很多工作要做。我们已经推出一个预览版,所以开发者可以进行尝鲜并给我们反馈。

Flutter Web 架构

Flutter 在 Web 端的整体架构和移动端的架构差不多:

Flutter架构

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 应用。

How to get better at writing CSS

原文地址: https://medium.com/free-code-camp/how-to-get-better-at-writing-css-a1732c32a72f

原文作者: Thomas Lombart

翻译作者: hanxiansen

中文标题:如何更优雅的编写CSS代码

How to get better at writing CSS

How to get better at writing 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框架会很困难
  • 在使用它们之前,你必须先学习它们

毕竟,你看这篇文章是带着目的的,对吧,所以不要在纠结框架不框架了,让我们学习如何在原生css方面让它变得更好吧,

Ps: 这不是一篇关于如何设计漂亮app的文章,它是关于编写可维护和可组织的css代码的学习文章

SCSS

在本文的示例代码中我将使用SCSS编写。

SCSS是css的预处理器。基本上,它是CSS的超集:它添加了一些很酷的特性,比如:变量、嵌套、导入和混合。我会略将下我们马上要使用的特性。

变量

在scss中你可以使用变量。主要好处:可重用性。让我们假设你的app中有一个颜色调色板。你的主题色是蓝色。所以你到处都要使用该颜色:按钮背景色、标题颜色、链接颜色,到处都是蓝色。

突然,你不喜欢蓝色了,你喜欢上绿色了:

  • 没使用变量情况下:改变每行使用了蓝色的css代码
  • 使用变量情况下:只需要改变颜色变量:)
// 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 代码组织方案:BEM

我曾经无数次给我的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 时,你的命名只有 class 类名并且只使用 class 类名,没有 id ,没有标签,就只使用 class 类名。
  • 块/元素可以嵌套到其它块/元素中,但它们必须是完全独立的。记住这个词:独立。所以,不要在按钮元素上写margin,因为你想要把按钮放在标题元素下,否则你的按钮将会和标题元素强耦合。这种情况请使用具体的 class 类名来替代。
  • 是的,你的 HTML 文件将会因为 BEM 变得臃肿,但比起BEM带来的好处,这只是一个无足轻重的小缺点。

举个例子

这是给你的练习。你浏览那些你常逛的网址,试着分析哪些是块,哪些是元素,那些是修饰符。

例如,我在谷歌商店中分析到的是这样的:

轮到你了,保持好奇心,想想怎样可以变得更好。和往常一样,你必须自己搜索,实验,创造。这样你才能完成你的分析。

把这些知识进行结合

在下例中你会发现 BEM 的强大之处:

编写单个文章组件示例 —— https://codepen.io/thomlom/pen/RJvVdQ

编写多个按钮示例——https://codepen.io/thomlom/pen/VdgzmJ

CSS 文件组织方案:7-1模式

还在跟着我一起学习吗?真棒!现在让我们看看如何组织 css 文件。这部分将真正的帮助你提高工作效率,并允许你立即能找到需要修改的 css 代码位置。

为了做到这点,我们将学习 7-1模式

这玩意儿简单不,你可能会想。

相信我,该模式非常简单,你只需记住如下两条原则即可:

  1. 所有的分块放在7个不同的文件夹中
  2. 把这些分块通过 import 引入到一个 main.scss 文件中,该文件放到根目录,嗯,就是这么简单。

7个文件夹:

  • base: 该文件中,放置所有的样板代码。我这里说的样板文件,是指每次你开始一个新项目时,你要写的所有 CSS 代码。例如:排版规则,动画特效,公共工具(这里的公共工具是指如margin-right-large, text-center,..)等等。
  • components: 该命名已经指明了其地位。此文件包含用于构建页面所需的组件,如:buttons、forms、swipers、popups等等。
  • layout: 用于布局页面的不同部分。即:header、footer、navigation、section、grid等。
  • pages: 有时候你可能写了一个页面,但要为其制定专属的样式,所以你把这种专属样式放置在 pages 文件夹中。
  • themes: 如果你的 app 需要拥有不同的主题(黑暗主题,默认主题等等) ,把这些主题放置在该文件夹中。
  • abstracts: 把你的所有函数,连同变量和mixins一起放置在这里面,简言之,就是放置所有的助手。
  • vendors: 有什么 app 或项目不依赖于外部库吗?将那些不依赖与你自己写的样式文件防治站该文件夹中。你可能想在这里面添加 Font Awesome 文件、Bootstrap 等等类似文件。

main 文件

在该文件中,引入你的所有区块文件

@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个文件夹了!

然后,你还面临两个选择:

  1. 你希望你的 css 代码是有组织的并遵循7-1模式,因此你保留了abstract、components、layout和base文件夹。

  2. 你想把所有的文件包括main.scss文件都放置在一个大文件夹中,类似如下:

    sass/
      _animations.scss
      _base.scss
      _buttons.scss
      _header.scss
      ...
      _variables.scss
      main.scss

    这取决于你自己。

    ok,你成功说服我采用你的方案,但有时候浏览器不支持scss文件,咋整?

    说得好!最后一步,我们将学习如何立即将 scss 编译为 css。

    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.htmlmain.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.htmlhead标签中将编译好的 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。注意:这是个全局package
  • 添加npm-run-all依赖:npm install npm-run-all:它将允许我们同时运行多个script
  • package.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]( Autoprefixer)。

它是一个工具,可以解析 CSS 并使用 can I use 中的值将浏览器供应商前缀添加到 css 规则中。

实际上,在构建网站时,你可能会使用到并非所有浏览器都完全支持的新特性,这时候,添加浏览器供应商前缀可以支持这些新特性,下面是示例:

-webkit-animation-name: myAnimation;
-moz-animation-name: myAnimation; 
-ms-animation-name: myAnimation;

这些特性写起来很乏味。因此我们需要 autoprefixer 来使我们的 css 代码能与浏览器兼容,而不会带来额外的复杂度。

所有我们按如下方式编写 css 代码:

  • 将所有的 scss 文件写入一个主文件中
  • 通过 Autoprefixer为css添加浏览器供应商前缀
  • 编译 css 文件

这将是最后的步骤了,所有耐心和我一起完成吧:

  • 添加两个依赖:postcss-cliautoprefixernpm 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 代码将被压缩,并且已经添加了浏览器供应商前缀名,太棒了,不是吗?

React Hooks Tutorial

原文地址: https://medium.com/@dtkatz/react-hooks-tutorial-learn-by-building-b90ec4db2b8e

原文作者: David Katz

Hooks

Hooks 是 React 即将推出的一个新功能。这一更新在 React 社区引起了巨大的反响。

简而言之,现在可以在 React 函数组件中使用 state 和其他 React 特性。因此没有必要使用 Class组件。

在这篇文章

我会教你使用 React Hooks 构建一个 Web 应用。对于一些代码,我也会列出没有使用 Hooks 的方式的代码。这样的话可以比较与使用 Hooks的区别。

如果你是一个 React 新手, Hooks是另一个值得学习的基础概念。

如果你是一个有经验的 React 开发者,没有必要忘记之前所掌握的 React 知识。事实上, Hooks 能够让你更好的写出简洁的 React 代码。

构建 App

在这个 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 文件中依赖的 reactreact-dom 版本是 16.8.0-alpha.1

"react": "@16.8.0-alpha.1",
"react-dom": "@16.8.0-alpha.1"

Hook State

学习 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 :

  1. useState 的参数就是 state 的初始值。因此 useState('') ,就是给 userText 设置了一个 空字符串的初始值。
  2. useState 方法返回一个数组包含两个值。第一个值就是当前组件 state 的变量。因此在 const [userText, setUserText] = useState('');userText 表示当前值。它在代码中当作一个常量使用。
  3. 第二个返回值是一个函数。这个函数更新和它配对的变量的值。因此 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 运行起来了。

与 class 组件的 state 比较

我们来比较一下 state 在 函数组件中与 class 组件中的不同:

有几个方面需求强调:

  • 就代码行数来说,hooks 的函数组件更胜一筹。使用 hooks 的代码用了14行,而使用传统 class 组件的代码则用了16行。
  • class 组件使用 this 对象调用 this.state , this.setStatethis.updateUserText 。长久以来对,React 开发者一直都需要关心 this 是怎样绑定到组件上面的。使用 .bind() 来修复这个问题是有点费劲的。使用 class 的属性初始化语法可以解决绑定的问题。但是你必须得认识到这一点就是你的方法能够访问到 this.setState 方法是因为组件继承 React.Component 时附加上去的。

Hook 的由来

通过上面的学习,你可以认为 useState 是一个 React 函数式 state 的钩子。函数组件是将 React 的 state 附加到自己身上。

在之前 React 组件继承上,class 类组件继承了所有的 React 功能,现在 函数组件也能附加到自己里面。

完成 Type Race

让我们把这个应用做完。接下来允许用户可以选择不同的文本片段。我们会加入计时的尝试。总体来说,需要更多的 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>

几个关键的点:

  • 每个按钮的文本截取了文本片段的前10个字符,并以省略号结束。注意按钮的 onClick 事件调用 chooseSnippet 并且传递 index 。这样会返回一个回调函数。这个回调函数内部调用 setSnippet 参数是 index 。为什么要这么设计?
  • onClick 事件处理函数必须有回调函数,而不是它本身。这是 React 诸多陷阱之一:在 JSX 中直接调用方法引起 state 变化会再次渲染(渲染被触发是因为组件的 state 发生了变化)。因此渲染函数执行的时候会再次调用该方法。然后又会引起 state 变化,再次渲染,形成无限循环。
  • 在 map 方法中使用 index 作为 key 是反面教材。正确的重构方法是使用每个 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 代码:

回到应用中,获胜的结果应该是这样的:

win

好的,现在就完成了这个应用!

此刻,你可以自行添加一些喜欢的CSS和样式更改。

其他的 Hooks

除了 useState ,还有其他的 hooks 将会把 React 概念 带入到 函数式组件中。

useState 之后就是 useEffectuseEffect 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!" :
victory

结论

现在你已经使用 React hooks 构建了一个应用,希望你能体会到为什么这次更新会让 React 社区沸腾。React 团队一直在致力于打造整洁的函数式组件。美中不足的是,如果你需要创建本地 state ,就必须使用 class 组件。

使用 React hooks,就会变得两全其美:函数式组件也可以使用 state。

前端组件设计原则

原文地址:Front end component design principles

原文作者:Andrew Dinihan

文中示例代码:传送门

限于个人能力,如有错漏之处,烦请不吝赐教。
img

前言

我在最近的工作中开始使用 Vue 进行开发,但是我在上一家公司积累了三年以上 React 开发经验。虽然在两种不同的前端框架之间进行切换确实需要学习很多,但是二者之间在很多基础概念、设计思路上是相通的。其中之一就是组件设计,包括组件层次结构设计以及组件各自的职责划分。

组件是大多数现代前端框架的基本概念之一,在 React 和 Vue 以及 Ember 和 Mithril 等框架中均有所体现。组件通常是由标记语言、逻辑和样式组成的集合。它们被创建的目的就是作为可复用的模块去构建我们的应用程序。

类似于传统 OOP 语言中 class 的设计,在设计组件的时候需要考虑到很多方面,以便它们可以很好的复用,组合,分离和低耦合,但是功能可以比较稳定的实现,即使是在超出实际测试用例范围的情况下。这样的设计说起来容易做起来却很难,因为现实中我们往往没有足够的时间按照最优的方式去做。

方法

在本文中,我想介绍一些组件相关的设计概念,在进行前端开发时应该考虑这些概念。我认为最好的方法是给每个概念一个简洁精炼的名字,然后逐一解释每个概念是什么以及为什么重要,对于比较抽象概念的会举一些例子来帮助理解。

以下这个列表并不是不全面也不完整,但我注意到的只有 8 件事情值得一提,对于那些已经可以编写基本组件但想要提高他们的技术设计技能的人来说。所以这是列表:
以下列举的这个列表仅仅是是我注意到的 8 个方面,当然组件设计还有其他一些方面。在此我只是列举出来我认为值得一提的。

对于已经掌握基本的组件设计并且想要提高自身的组件设计能力的开发者,我认为以下 8 � 项是我认为值得去注意的,当然这并不是组件设计的全部。

  1. 层次结构和 UML 类图
  2. 扁平化、面向数据的 state/props
  3. 更加纯粹的 State 变化
  4. 低耦合
  5. 辅助代码分离
  6. 提炼精华
  7. 及时模块化
  8. 集中/统一的状态管理

请注意,代码示例可能有一些小问题或有点人为设计。但是它们并不复杂,只是想通过这些例子来帮助更好的理解概念。

层次结构和类图

应用内的组件共同形成组件树, 而在设计过程中将组件树可视化展示可以帮助你全面了解应用程序的布局。一个比较好的展示这些的办法就是组件图。

UML 中有一个在 OOP 类设计中经常使用的类型,称为 UML 类图。类图中显示了类属性、方法、访问修饰符、类与其他类的关系等。虽然 OOP 类设计和前端组件设计差异很大,但是通过图解辅助设计的方法值得参考。对于前端组件,该图表可以显示:

  • State
  • Props
  • Methods
  • 与其他组件的关系( Relationship to other components )

因此,让我们看一下下面这个基础表组件的组件层次图,该组件的渲染对象是一个数组。该组件的功能包括显示总行数、标题行和一些数据行,以及在单击其单元格标题格时对该列进行排序。在它的 props 中,它将传递列列表(具有属性名称和该属性的人类可读版本),然后传递数据数组。我们可以添加一个可选的'on row click'功能来进行测试。

img

虽然这样的事情可能看起来有点多,但是它具有许多优点,并且在大型应用程序开发设计中所需要的。这样会带来的一个比较重要的问题是它会需要你在开始 codeing 之前就需要考虑到具体细节的实现,例如每个组件需要什么类型的数据,需要实现哪些方法,所需的状态属性等等。

一旦你对如何构建一个组件(或一组组件)的整体有大概的思路,就会很容易认为当自己真正开始编码实现时,它会如自己所期望的按部就班的完成,但事实上往往会出现一些预料之外的事情, 当然你肯定不希望因此去重构之前的某些部分,或者忍受初始设想中的缺点并因此扰乱你的代码思路。而这些类图的以下优点可以帮助你有效的规避以上问题,优点如下:

  1. 一个易于理解的组件组成和关联视图
  2. 一个易于理解的应用程序 UI 层次结构的概述
  3. 一个结构数据层次及其流动方式的视图
  4. 一个组件功能职责的快照
  5. 便于使用图表软件创建

顺带一提,上图并不是基于某些官方标准,比如 UML 类图,它是我基本上创建的一套表达规则。例如,在 props 、方法的参数和返回值的数据类型定义声明都是基于 Typescript 语法。我还没有找到书写前端组件类图的官方标准,可能是由于前端 Javascript 开发的相对较新且生态系统不够完善所致,但如果有人知道主流标准,请在回复中告诉我!

扁平的,面向数据的 state/props

在 state 和 props 频繁被 watch 和 update 的情况下,如果你有使用嵌套数据,那么你的性能可能会受到影响,尤其是在以下场景中,例如一些因为浅对于而触发的重新渲染;在涉及 immutability 的库中,比如 React,你必须创建状态的副本而不是像在 Vue 中那样直接更改它们,并且使用嵌套数据这样做可能会创建笨拙,丑陋的代码。

img

即使使用展开运算符,这种写法也并不够优雅。扁平 props 也可以很好地清除组件正在使用的数据值。如果你传给组件一个对象但是你并不能清楚的知道对象内部的属性值,所以找出实际需要的数据值是来自组件具体的属性值则是额外的工作。但如果 props 足够扁平化,那么起码会方便使用和维护。

img

state / props 还应该只包含组件渲染所需的数据。You shouldn’t store entire components in the state/props and render straight from there.

(此外,对于数据繁重的应用程序,数据规范化可以带来巨大的好处,除了扁平化之外,你可能还需要考虑一些别的优化方法)。

更加纯粹的 State 变化

对 state 的更改通常应该响应某种事件,例如用户单击按钮或 API 的响应。此外它们不应该因为别的 state 的变化而做出响应,因为 state 之间这种关联可能会导致难以理解和维护的组件行为。state 变化应该没有副作用。

如果你滥用watch而不是有限考虑以上原则,那么在 Vue 的使用中就可能由此引发的问题。我们来看一个基本的 Vue 示例。我正在研究一个从 API 获取一些数据并将其呈现给表的组件,其中排序,过滤等功能都是后端完成的,因此前端需要做的就是 watch 所有搜索参数,并在其变化时触发 API 调用。其中一个需要 watch 的值是“zone”,这是一个过滤器。当更改时,我们想要使用过滤后的值重新获取服务端数据。watcher 如下:

img

你会发现一些奇怪的东西。如果他们超出了结果的第一页,我们重置页码然后结束?这似乎不对,如果它们不在第一页上,我们应该重置分页并触发 API 调用,对吧?为什么我们只在第 1 页上重新获取数据?实际上原因是这样,让我们来看下完整的 watch:

img

当分页改变时,应用首先会通过 pagination 的处理函数重新获取数据。因此,如果我们改变了分页,我们并不需要去关注数据更新这段逻辑。

让我们一下来考虑以下流程:如果当前页面超出了第 1 页并且更改了 zone,而这个变化会触发另一个状态(pagination)发生变化,进而触发 pagination 的观察者重新请求数据。这样并不是预料之中的行为,而且产生的代码也不够直观。

解决方案是改变页码这个行为的事件处理函数(不是观察者,用户更改页面的实际处理函数)应该更改页面值触发 API 调用请求数据。这也将消除对观察者的需求。通过这样的设置,直接从其他地方改变分页状态也不会导致重新获取数据的副作用。

虽然这个例子非常简单,但不难看出将更复杂的状态更改关联在一起会产生令人难以理解的代码,这些代码不仅不可扩展并且是调试的噩梦。

松耦合

组件的核心**是它们是可复用的,为此要求它们必须具有功能性和完整性。“耦合”是指实体彼此依赖的术语。松散耦合的实体应该能够独立运行,而不依赖于其他模块。就前端组件而言,耦合的主要部分是组件的功能依赖于其父级及其传递的 props 的多少,以及内部使用的子组件(当然还有引用的部分,如第三方模块或用户脚本)。

紧密耦合的组件往往更不容易被复用,当它们作为特定父组件的子项时,就很难正常工作,当父组件的一个子组件或一系列子组件只能在该父组件才能够正常发挥作用时,就会使得代码写的很冗余。因为父子组件别过度的关联在一起了。

在设计组件时,你应该考虑到更加通用的使用场景,而不仅仅只是为了满足最开始某个特定场景的需求。虽然一般来说组件最初都是出于特定目的进行设计,但没关系,如果在设计它们站在更高的角度去看待,那么很多组件将具有更好的适用性。

让我们看一个简单的 React 示例,你想在写出一个带有一个 logo 的链接列表,通过连接可以访问特定的网站。最开始的设计可能是并没有跟内容合理的进行解耦。下面是最初的版本:

img

虽然这这样会满足预期的使用场景,但却很难被复用。如果你想要更改链接地址该怎么办?你必须重新复制一份相同代码,并且手动去替换链接地址。而且, 如果你要去实现一个用户可以更改连接的功能,那么意味着不可能将代码写“死”,也不能期望用户去手动修改代码,那么让我们来看一下复用性更高的组件应该如何设计:

img

在这里我们可以看到,虽然它的原始链接和 logo 具有默认值,但我们可以通过 props 传入的值去覆盖掉默认值。让我们来看一下它在实际中的使用:

img

并不需要重新编写新的组件!如果我们解决上文中用户可以自定义链接的使用场景,可以考虑动态构建链接数组。此外,虽然在这个具体的例子中没有解决,但我们仍然可以注意到这个组件没有与任何特定的父/子组件建立密切关联。它可以在任何需要的地方呈现。改进后的组件明显比最初版本具有更好的复用性。

如果不是要设计需要服务于特定的一次性场景的组件,那么设计组件的最终目标是让它与父组件松散耦合,呈现更好的复用性,而不是受限于特定的上下文环境。

辅助代码分离

这个可能不那么的偏理论,但我仍然认为这很重要。与你的代码库打交道是软件工程的一部分,有时一些基本的组织原则可以使事情变得更加顺畅。在长时间与代码相处的过程中,即使改变一个很小的习惯也可以产生很大的不同。其中一个有效的原则就是将辅助代码分离出来放在特定的地方,这样你在处理组件时就不必考虑这些。以下列举一些方面:

  • 配置代码
  • 假数据
  • 大量非技术说明文档

因为在尝试处理组件的核心代码时,你不希望看到与技术无关的一些说明(因为会多滚动几下鼠标滚轮甚至打断思路)。在处理组件时,你希望它们尽可能通用且可重用。查看与组件当前上下文相关的特定信息可能会使得设计出来的组件不易与具体业务解耦。

提炼精华

虽然这样做起来可能具有挑战性,但开发组件的一个好方法是使它们包含渲染它们所需的最小 Javascript。一些无关紧要的东西,比如数据获取,数据整理或事件处理逻辑,理想情况下应该将通用的部分移入外部 js 或或者放在共同的祖先中。

单独从组件分的“视图”部分来看,即你看到的内容(html 和 样式)。其中的 Javascript 仅用于帮助渲染视图,可能还有一些针对特定组件的逻辑(例如在其他地方使用时)。除此之外的任何事情,例如 API 调用,数值的格式化(例如货币或时间)或跨组件复用的数据,都可以移动外部的 js 文件中。让我们看一下 Vue 中的一个简单示例,使用嵌套列表组件。我们可以先看下下面这个有问题的版本。

这是第一个层级:

img

这是嵌套列表组件:

img

在这里我们可以看到此列表的两个层级都具有外部依赖关系,最上层导引入外部 js 文件中的函数和 JSON 文件的数据,嵌套组件连接到 Vuex 存储并使用 axios 发送请求。它们还具有仅适用于当前场景的嵌入功能(最上层中源数据处理和嵌套列表的中度 click 时间的特定响应功能)。

虽然这里采用了一些很好的通用设计技术,例如将通用的 数据处理方法移动到外部脚本而不是直接将函数写死,但这样仍然不具备很高的复用性。如果我们是从 API 的响应中获取数据,但是这个数据跟我们期望的数据结构或者类型不同的时候要怎么办?或者我们期望单击嵌套项时有不同的行为?在遇到这些需求的场景下,这个组件无法被别的组件直接引用并根据实际需求改变自身的特性。

让我们看看我们是否可以通过提升数据并将事件处理作为 props 传递来解决这个问题,这样组件就可以简单地呈现数据而不会封装任何其他逻辑。

这是改进后的第一级别:

img

而新的第二级:

img

使用这个新列表,我们可以获得想要的数据,并定义了嵌套列表的 onClick 处理函数,以便在父级中传入任何我们想要的操作,然后将它们作为 props 传递给顶级组件。这样,我们可以将导入和逻辑留给单个根组件,所以不需要为了能够在新的场景下使用去重新再实现一个类似组件。

有关此主题的简短文章可以在这里找到。它由 Redux 的作者 Dan Abramov 编写,虽然是用 React 举例说明。但是组件设计的**是通用的。

及时模块化

我们在实际进行组件抽离工作的时候,需要考虑到不要过度的组件化,诚然将大块代码变成松散耦合且可用的部分是很好的实践,但是并不是所有的页面结构(HTML 部分)都需要被抽离成组件,也不是所有的逻辑部分都需要被抽出到组件外部。

在决定是否将代码分开时,无论是 Javascript 逻辑还是抽离为新的组件,都需要考虑以下几点。同样,这个列表并不完整,只是为了让你了解需要考虑的各种事项。(记住,仅仅因为它不满足一个条件并不意味着它不会满足其他条件,所以在做出决定之前要考虑所有条件):

  1. 是否有足够的页面结构/逻辑来保证它?
    如果它只是几行代码,那么最终可能会创建更多的代码来分隔它,而不仅仅是将代码放入其中。
  2. 代码重复(或可能重复)?
    如果某些东西只使用一次,并且服务于一个不太可能在其他地方使用的特定用例,那么将它嵌入其中可能会更好。如果需要,你可以随时将其分开(但不要在需要做这些工作的时候将此作为偷懒的借口)。
  3. 它会减少需要书写的模板吗?
    例如,假设你想要一个带有特定样式的 div 属性结构和一些静态内容/功能的组件,其中一些可变内容嵌套在内部。通过创建可重用的包装器(与 React 的 HOC 或 Vue 的 slot 一样),你可以在创建这些组件的多个实例时减少模板代码,因为你不需要重新再写外部的包装代码。
  4. 性能会收到影响吗?
    更改 state/props 会导致重新渲染,当发生这种情况时,你需要的是 只是重新去渲染经过 diff 之后得到的相关元素节点。在较大的、关联很紧密的组件中,你可能会发现状态更改会导致在不需要它的许多地方重新呈现,这时应用的性能就可能会开始受到影响。
  5. 你是否会在测试代码的所有部分时遇到问题?
    我们总是希望能够进行充分的测试,比如对于一个组件,我们会期望它的正常工作不依赖特定的用例(上下文),并且所有 Javascript 逻辑都按预期工作。当元素具有某个特定假设的上下文或者分别将一大堆逻辑嵌入到单个函数中时,这样将会很难满足我们的期望。如果测试的组件是具有比较大模板和样式的单个巨型组件,那么组件的渲染测试也会很难进行。
  6. 你是否有一个明确的理由?
    在分割代码时,你应该考虑它究竟实现了什么。这是否允许更松散的耦合?我是否打破了一个逻辑上有意义的独立实体?这个代码是否真的可能在其他地方被重复使用?如果你不能清楚地回答这个问题,那最好先不要进行组件抽离。因为这样可能导致一些问题(比如拆解掉原本某些潜在的耦合关系)。
  7. 这些好处是否超过了成本?
    分离代码不可避免地需要时间和精力,其数量根据具体情况而变化,并且在最终做出此决定时会有许多因素(例如此列表中列举出来的一些)。一般来说,进行一些对抽象的成本和收益研究可以帮助更快更准确去做出是否需要组件化的决策。最后,我提到了这一点,因为如果我们过分关注优势,就很容易忘记达成目标所需要做的努力,所以在做出决定以前需要权衡这两个方面。

集中/统一的状态管理

许多大型应用程序使用 Redux 或 Vuex 等状态管理工具(或者具有类似 React 中的 Context API 状态共享设置)。这意味着他们从 store 获得 props 而不是通过父级传递。在考虑组件的可重用性时,你不仅要考虑直接的父级中传递而来的 props,还要考虑 从 store 中获取到的 props。如果你在另一个项目中使用该组件,则需要在 store 中使用这些值。或许其他项目根本不使用集中存储工具,你必须将其转换为从父级中进行 props 传递 的形式。

由于将组件挂接到 store(或上下文)很容易并且无论组件的层次结构位置如何都可以完成,因此很容易在 store 和 web 应用的组件之间快速创建大量紧密耦合(不关心组件所处的层级)。通常将组件与 store 进行关联只需简单几行代码。但是请注意一点,虽然这种连接(耦合)更方便,但它的含义并没有什么不同,你也需要考虑尽量符合如同在使用父级传递方式时的要点。

最后

我想提醒大家的是:应该更注重以上这些组件设计的原则和你已知的一些最佳实践在实际中的应用。虽然你应该尽力维护良好的设计,但是不要为了包装 JIRA ticket 或一个取消请求而有损代码完整性,同时总是把理论置于现实世界结果之上的人也往往会让他们的工作受到影响。大型软件项目有许多活动部分,软件工程的许多方面与编码没有特别的关系,但仍然是不可或缺的,例如遵守最后期限和处理非技术期望。

虽然充分的准备很重要,应该成为任何专业软件设计的一部分,但在现实世界中,切实的结果才是最为重要的。当你被雇用来实际创造一些东西时,如果在最后期限到来之前,你有的只是一个如何构建完美产品的惊人计划,但却没有实际的成果,你的雇主可能不会太高兴吧?此外,软件工程中的东西很少完全按计划进行,因此过度具体的计划往往会在时间使用方面得到适得其反的效果。

此外,组件规划和设计的概念也适用于组件重构。虽然用了 50 年的时间来计划一切令人难以忍受的细节,然后从一开始就完美地编写它就会很好,回到现实世界,我们往往会遇到这种情况,即为了赶进度而不能使代码达到完美的预期。然而,一旦我们有了空闲时间,那么一个推荐的做法就是回过头来重构早期不够理想的的代码,这样它就可以作为我们向前发展的坚实基础。

在一天结束时,虽然你的直接责任可能是“编写代码”,但你不应忽视你的最终目标,即建立一些东西。创建产品。为了产生一些你可以引以为豪的东西并帮助别人,即使它在技术上并不完美,永远记得找到一个平衡点。不幸的是,在一周内每天 8 小时盯着眼前的代码会使得眼界和角度变得更为“狭窄”,这个时候你需要的你是退后一步,确保你不要为了一颗树而失去整个森林。

讨论实现一个 GitHub star 管理的 Web 服务或 Chrome Extension 的可能性

基于我自己迫在眉睫的需求,在遍历已有的情况下,我使用的场景不是App主要在 Mac 上,很希望有一个本地缓存式的服务,让我自己可以把在 GitHub 上 star 过的项目管理起来,看起来业务需求上可能有如下几点:

  • 根据语言自动分类
  • 自己关注的重要项目可以 ping 起来
  • 方便自己搜索
  • 每个仓库可以有完整的展示,并且快速的 link 到 issue
  • 方便自己对项目进行描述

CSS中z-index的工作原理解析

原文地址: https://blog.logrocket.com/how-css-works-creating-layers-with-z-index-6a20afe1550e

原文作者: Benjamin Johnson

介绍

这篇文章是"How CSS workd"系列文章中的第三篇(深入探讨CSS基本构建模块的系列文章,这些基本构建模块有时会让人感觉像是黑魔法)。无论你如何编写你的css代码,知道样式表的运行原理总是极好的,这样你才能编写出高效的、可伸缩性css代码。

下面是系列文章中的前两篇:

  1. css如何工作 — 解析和绘制css的关键路径:深入研究CSS引擎在浏览器请求css文件到用户实际在屏幕上看到的像素过程中发生了什么
  2. css如何工作 — 了解层叠:讲解“css中的"c"以及层叠算法如何将各种css规则解析为浏览器中实际显示的样式

今天我们研究css的另一个黑魔法 — z-index,耍z-index,通常感觉像是往墙面上扔一个更高的数字,然后看看是什么效果。

但如果我们试着剥离z-index上的层,看看管理他们的规则呢,我们会发现它并不像我们想象的那么可怕。

究竟什么是z-indx?

说到底,我们在浏览器中使用的屏幕是一组由像素组成的二维平面,但如果你经常上网,你可能会经常体会到"三维”世界,例如,你可以单击一个按钮打开一个 modal,或者触发一个tool-tip

如果你假设用户正在看屏幕,并想象屏幕是三维世界的入口,你可以开启浏览器中的三维空间呈现,首先,我们把该空间想象成一个房间,那么"最远"的"墙"就被称为"画布 canvas"

当在屏幕是绘制像素时,浏览器首先会确保绘制距画布最近(或最远)�的像素,然后,他将渐进的将元素"更接近"用户,并覆盖以前绘制的像素

z-index属性表示元素在三维浏览器视觉中绘制的顺序,默认情况下,所有元素的 z-index 皆为0,浏览器按照 DOM 顺序进行绘制,然而实际上,z-index 为我们提供了何时绘制元素的细节控制,通过指定较高的 z-index,我们可以使绘制出的元素"更接近"用户,如果我们指定一个较低的(或负值)z-index,我们可以使绘制出的元素更接近画布。

在深入研究 CSS 的位置和布局规范时,我发现了这个有用的图片,它可以直观的显示出 z-index 层叠对用户视觉的影响。

img

具有较高 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 元素标记为其"根层叠",并且默认情况下,所有元素都属于此根层叠上下文,但是,任何元素节点也可以是其"局部层叠上下文"中的根层叠。

你可以通过以下几种方法将元素指定为新的局部层叠上下文的根层叠:

  1. 在设置了positionabsoluterelative或其它任何除了auto属性上设置 z-index
  2. 使用position: fixedposition: sticky
  3. 元素上设置的opacity属性值小于1
  4. 在元素上使用transformwill-change

如果你想看到更多创建层叠环境的方法,请在 MDN上查看该文章

作为例子,让我们想象 HTML的三个树节点,将它们想想为三个层叠,在下图中,每个层叠的底部是HTML节点,子元素堆叠在其上(有点像 HTML 树的颠倒表示),让我们假设这些元素都是 body 标签的直接子节点。

img

如果我们在浏览器中预览这些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-parentblue-parent(根层叠中唯一的其它两个元素)进行测量的,它的z-index比这两个更高,所有它显示在顶部。

了解red-parentblue-parent各自创建的局部层叠环境也有助于我们了解为什么blue-child-2显示在red-parent之下,即使其z-index更高,由于z-index仅控制元素在其局部层叠上下文中的位置,因此blue-child-2肯定要高于blue-parent的子级。但是red-parentz-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的混淆都源自我们在上面讨论的那两个警告的误解

作为复习,我在这里再强调一遍:

  1. z-index仅用于除 static 元素外的具有position的元素
  2. z-index仅适用于元素在其所属的层叠上下文中的位置

我希望你以后在遇到多层 ui 导致的各种陷阱时,了解 z-index的内部结构会给你带来信心,我知道,研究z-index和层叠系统对于我们更好的开发ui会更有帮助。

A Guide to Node.js Logging

原文地址: https://www.twilio.com/blog/guide-node-js-logging

原文作者: DOMINIK KUNDEL

翻译作者: icepy

logo

当你开始使用 JavaScript 做开发时,你可能学习到的第一件事情就是如何使用 console.log 将内容打印到控制台。如果你搜索如何调试 JavaScript,你会发现数百个博客文章和 StackOverflow 的文章都指向简单的 console.log 。因为这是一种常见的做法,我们甚至可以使用 no-console 这样的规则来确保生产环境不会留下日志。但是,如果我们真的想要记录这些信息呢?

在这篇博文中,我们将介绍你想要记录信息的各种情况,Node.js 中的 console.logconsole.error 之间的区别是什么,以及如何在不使用户控制台混乱的情况下在库中发送日志记录。

console.log(`Let's go!`);

Theory First: Important Details for Node.js

虽然你可以在浏览器和 Node.js 环境中使用 console.logconsole.error,但在 Node.js 中使用时一定要记住一件重要的事情。

将如下代码写入到 index.js 文件中,并在 Node.js 环境里执行:

console.log('Hello there');
console.error('Bye bye');

如图:

log error

虽然这两个输出看起来可能一样,但系统实际上对它的处理方式有不同。如果你检查一下 console section of the Node.js documentation 你会发现 console.log 使用 stdout 打印而 console.error 则使用 stderr

每一个进程都有三个可以使用的默认 streams,它们是 stdinstdoutstderrstdin 可以处理进程的输入,例如按下按钮或重定向输出。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

如图:

redirect

When Do You Want to Log?

现在我们已经了解了日志记录的基础技术,那么让我们来谈谈你可能想要记录某些内容的不同例子,通常这些例子都属于以下类别之一:

  • 快速调试开发阶段的意外行为
  • 基于浏览器的分析和诊断日志记录
  • 记录服务器应用程序传入的请求以及可能发生的任何故障
  • 某些库的可选调试日志
  • CLI的进度输出

我们将跳过本博文中的前两篇文章,并将重点介绍基于Node.js的三篇文章。

Your Server Application Logs

你希望在服务器上记录内容的原因可能有多种,例如:记录传入的请求,统计信息,有多少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 你会注意到打印的很多信息并不是我们需要的。

o-req

如果将起更改为 console.log('%s',req) 我们也不会获取太多的信息。

s-req

我们可以编写自己的日志功能,只打印我们关心的信息。但让我们先退一步,谈谈我们通常关心的事情。虽然这些信息经常成为我们关注的焦点,但实际上我们可能需要其他信息,例如:

  • 时间戳-知道事情何时发生
  • 计算机/服务器名称-如果你运行的是分布式系统的话
  • 进程ID-如果你使用 pm2 运行着多个 Node.js 进程
  • 消息-包含某些内容的实际消息
  • 堆栈追踪
  • 也许是一些额外的变量或信息

另外,既然我们知道打印最后都会落到 stdoutstderr 上,那么我们可能想要不同日志级别的记录以及过滤它的能力。

我们可以通过访问流程的各个部分并编写一堆 JavaScript 代码来获取上述的信息,但 npm 生态已经给我们提供了各种各样的库来使用,例如:

  • pino
  • winston
  • roarr
  • bunyan

我个人喜欢 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 输出:

pino-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-pretty

另外还有各种各样的库可以来美化你的日志,甚至你可以使用 pino-coladaemojis 来显示它们。这些对于你的本地开发非常有用,在运行到生产服务器之后,你可能希望将日志的管道转移到另外一个管道,使用 > 将它们写入硬盘以便稍后处理它们。

比如:

$ LOG_LEVEL=debug node index.js | ./node_modules/.bin/pino-pretty | > success.log 2> s_error.log

Your Library Logs

既然我们研究了如何有效的为服务器应用程序编写日志,那么为什么不能将它用在我们的某些库中呢?问题是,你的库可能希望记录用于调试的内容,但实际上不应该让使用者的应用程序变得混乱。相反,如果需要调试某些东西,使用者应该能够启动日志。你的库默认情况下不会处理这些,并将输入输出的操作留给使用者。

express 就是一个很好的例子。

express 框架下有很多事情要做,在调试应用程序时,你可能希望了解一下框架的内容。如果我们查询文档,你会注意到你可以在命令行的前面加上 DEBUG=express:* 来启动。

$ DEBUG=express:* node index.js

如图:

debug=express

如果你没有启动调试日志,则不会看到任何这样的日志输出。这是通过一个叫 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:randomiddebug 记录器,然后会将这两种消息记录上去。

我们可以在 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 文件,如图:

debug=mylib-randomid

有意思的是,如果你的库使用者想把这些调试信息集成到自己的 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

如图:

pino-debug

Your CLI Output

我将在这篇博文中介绍最后一个案例,针对 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,如图:

cli

当你运行 CI=true node cli.js,如图:

no-cli

你要记住的是另外一个场景 stdout 能否在终端模式中运行。如果是这种情况,我们可以使用类似 boxen 的东西来显示所有漂流的输出。但如果不是,则可能会将输出重定向到文件或输出到其他地方。

你可以使用 isTTY 来检查 stdoutstdinstderr 是否在终端模式。

如:

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 ,如图:

tty=true

之后运行相同的内容,但将其输出重定向到一个文件中,这次你会看见它会打印一个 undefined 后面跟着一个简单的无色消息。

这是因为 stdout 关闭了终端模式下 stdout 的重定向。

chalk 使用了 supports-color ,它会在引擎里检查各个流的 isTTY

undefined

chalk 这样的库已经帮你处理了这些行为,但在开发 CLI 的过程中还是要注意,在 CI 模式下运行或输出被重定向的问题。

例如,你可以在终端以一种漂亮的方式来排列数据,如果 isTTYundefined 时,则切换到更容易解析的方式上。

In Summary

在 JavaScript 中使用 console.log 是非常快的,但当你将代码部署到生产环境时,你应该要考虑更多关于记录的内容。

本文仅仅是介绍了各种方法和可用的日志记录解决方案,它不包含你需要知道的一切。

因此我建议你多看一看你喜欢的开源项目,看看它们是如何解决日志记录问题以及它们所使用的工具。

v8 release v7.5

原文地址: https://v8.dev/blog/v8-release-75

原文作者: Dan Elphick

每六周我们会在发布的过程中创建一个新的V8分支,每个版本都是在Chrome Beta里程碑之前立即从V8的Git master checkout 出来的。今天我们很高兴地宣布我们最新的分支 V8 7.5,它将在几周内与Chrome 75稳定版协同发布。V8 v7.5充满了各种面向开发人员的好东西,这篇文章提供了预期发布的一些亮点的预览。

WebAssembly

Implicit caching

我们计划在 Chrome 75 中推出 WebAssembly 编译工作的Implicit caching,这意味着第二次访问同一页面的用户不需要编译已经看过的 WebAssembly 模块,而是从缓存加载它们,它的工作原理与Chromium’s JavaScript code-cache 类似。

Bulk memory operations

为 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))

Numeric separators in JavaScript

人眼难以快速解析大型数字文字,尤其是当有大量重复数字时:

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

Performance

Script streaming directly from network

从Chrome 75开始,V8可以直接从网络将stream scripts传输到streaming parser,而无需等待Chrome主线程。

虽然之前的Chrome版本具有 streaming parsing 和 compilation,但由于历史原因,从网络传入的脚本源数据始终必须首先进入 Chrome 主线程,然后再转发到streamer。这意味着 streaming parser 通常会等待已经从网络到达的数据,但尚未转发到流式任务,因为它被主线程上发生的其他事情阻止(例如HTML解析,布局或其他JavaScript执行)。

before@2x

在Chrome 75中,我们将网络“数据管道”直接连接到V8,允许我们在流解析期间直接读取网络数据,跳过对主线程的依赖。

after@2x

这使我们能够更早地完成 streaming compiles,改善使用 streaming compilation 的页面加载时间,以及减少并发(但停滞)streaming parse tasks 的数量,从而减少内存消耗。

V8 API

可以使用 git log branch-heads/7.4..branch-heads/7.5 include/v8.h 获取 API 更改列表。

JavaScript async/await: The Good Part, Pitfalls and How to Use

原文地址: 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/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 函数。

68747470733a2f2f63646e2d696d616765732d312e6d656469756d2e636f6d2f6d61782f3830302f312a633662596168414255447047674d704a56616b3441672e706e67

所有主流浏览器都支持 async 函数。(图片来源:https://caniuse.com/)

原生支持意味着你不需要编译代码。更重要的是,这个将有助于调试。当你在 async 方法的入口打一个断点并且步进到 await 这一行的时候,你将会看到调试器在 bookModel.fetchAll() 这个函数执行的时候等待了一会儿,然后才会走到接下来的 .filter 这一行!和 promise 的示例比较起来,这个容易多了,因为你必须在 .filter 这一行再打一个断点。

2

调试 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 可能会产生误导

一些文章把 async/await 和 Promise 进行了比较,同时说它是 JavaScript 异步编程演变过程中的下一代解决方案,对此我不敢苟同。Async/await 是一个提升,但它仅仅是一个语法糖,它将不会完全的改变我们的编程风格。

实质上,async 函数仍然是 promise。你必须理解 promises 之后才能正确的使用 async 函数,更糟糕的是,大多数情况下你必须同时使用 promises 和 async 函数。

思考一下上面例子中使用到 的 getBooksByAuthorWithAwait()getBooksByAuthorWithPromises() 。请注意,它们不仅是有相同的功能,同时也有相同的接口。

这意味着如果你直接 getBooksByAuthorWithAwait() 的话,将会返回一个 promise。

当然,这并不是一件不好的事情。只有 await 给人们的一种感觉,“很棒,这个可以将异步的函数转换成同步的函数”,这个才是错误的。

Async/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),  
    };
}

这段代码看起来逻辑上没有问题。然而是不正确的。

  1. await bookModel.fetchAll() 将会等待 fetchAll() 执行完。
  2. 然后 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

最标准的(也是我推荐的)处理方式是使用 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) ,使用这种方式可以在控制台中展示所有的调用栈记录。
  • 使用 Reject,例如, return Promise.reject(error) ,这个方式等价于 throw error ,因此不推荐使用这种方式。

使用 try...catch 的优点有以下这些:

  • 简单,传统。只要你有其他语言的经验,例如 C++ 或 Java,理解这种处理方式将不会有任何困难。
  • 你可以将多个 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 函数同时返回错误的值和正常的值。可以从下面这个博客中了解到更详细的的介绍:

How to write async await without try-catch blocks in Javascript
*ES7 Async/await allows us as developers to write asynchronous JS code that look synchronous. In current JS version we…*blog.grossman.io

简而言之,你能够像下面这样使用 async 函数:

[err, user] = await to(UserModel.findById(1));

我个人并不喜欢这种处理方式,因为它把 Go 语言的编程风格带到了 JavaScript 中,这样显得不自然,但是在某些情况下这种方式会很有用。

使用 .catch

我要介绍的最后一种处理方式是仍然使用 .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); 
    });

这种处理方式有两个次要的问题:

  • 这种方式混合了 promises 和 async 函数。你仍然需要理解 promises 的运行原理之后才能读懂它。
  • 错误处理在正常流程之前,这样是不太直观的。

结论

在 ES7 中引入的 async/await 关键字无疑是对 JavaScript 异步编程的一大加强。它能够把代码变得更易于阅读和调试。然后,为了正确的使用它们,必须要完全理解 promises,因为它们不过是语法糖,底层的技术仍然是 promises。

希望这篇文章能够给你一些关于 async/await 的启发,同时能够帮助你避免一些常见的错误。感谢阅读,如果喜欢的话,请为我点赞。

## History

History

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)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.