袁钰涵

袁钰涵 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 segmentfault.com/u/yuanhan_5f5f19f9dabdc 编辑
编辑

而受苦又是一个坏习惯

个人动态

袁钰涵 发布了文章 · 3月14日

CSS 技巧集合 | 思否技术周刊

CSS 是一种用来表现 HTML 或 XML 等文件样式的计算机语言,相信朋友们对它都不陌生,今日给大家整理一个与 CSS 小技巧相关的合集,希望为大家提供一些设计新思路~

CSS 有各种玩转的方式,一起来看看吧~

1、小技巧!CSS 整块文本溢出省略特性探究

今天的文章很有意思,讲一讲整块文本溢出省略打点的一些有意思的细节。

文本超长打点

我们都知道,到今天(2021/03/06),CSS 提供了两种方式便于我们进行文本超长的打点省略。

对于单行文本,使用单行省略:

{
    width: 200px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

而对于多行文本的超长省略,使用 -webkit-line-clamp 相关属性,兼容性也已经非常好了:

{
    width: 200px;
    overflow : hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

CodePen Demo -- inline-block 实现整块的溢出打点

https://codepen.io/Chokcoco/p...

问题一:超长文本整块省略

问题二:iOS 不支持整块超长溢出打点省略

2、原来CSS的background还可以这么玩

身为一个前端开发者,背景是开发中的常客。大到整个网站的主题背景,小到一个按钮的背景。CSS 的 background 属性基本上每天开发都会遇到,绝大多数情况下我们都只会使用到了纯色背景或者图片背景。如果你想让你开发的内容看起来更加生动有趣,通过本文让你用纯CSS也可以开发出炫酷的背景。

开始之前

在开始之前,先请你回答下面的问题,如果你能全部回答正确,说明你对 background 属性掌握的还不错哦!

1.径向渐变默认形状是什么?

A:原型 B:椭圆形

  1. background 属性的值为多个时,哪个值的图层在最顶部?

A:第一个值 B:最后一个值

  1. background: green, linear-gradient(red, pink); 效果是什么?

A:绿色背景 B:红粉渐变背景 C:没有背景

  1. 当background属性有多个值时,如何指定每层背景的大小?

基础背景

首先还是先回顾一下基础背景有哪些,最简单的就是 纯色背景:

background: pink;

线性渐变,当然你还可以自定义方向:

.linear {
    background: linear-gradient(red, pink);
}
.linear1 {
    background: linear-gradient(145deg, red 20%, pink);
}

径向渐变

background: radial-gradient(red, pink);

角向渐变

background: conic-gradient(red, pink);background: conic-gradient(red, pink);background: radial-gradient(red, pink);

基础背景扩展

纯色背景就没什么可说的了,只能改变颜色。

1、线性背景

2、径向背景

3、角向渐变

4、组合背景

3、使用这些 CSS 属性,布局效率又提高了一个层次!

有很多CSS属性,有些人不了解,或者他们了解它们,但是忘记在需要时使用它们。其实,有时候我们用 JavaScript 来实某些交互,CSS 一个属性就能搞定了,这可以大大节约我们编码的时间。

作为前端开发人员,我们经常会遇到这样的事情。所以我问自己,为什么不搞篇文章列出所有那些较少使用但既有用又有趣的 CSS 属性?

在本文中,我将介绍一些不一样的CSS属性,希望能给你带来点新鲜感,废话不多说,让我们开始吧。

在CSS网格中使用Place-Items

我们只需使用两行 CSS 代码就可以将元素水平和垂直居中。

HTML

<div class="hero">
    <div class="hero-wrapper">
        <h2>CSS is awesome</h2>
        <p>Yes, this is a hero section made for fun.</p>
        <a href="#">See more</a>
    </div>
</div>

CSS

.hero {
    display: grid;
    place-items: center;
}

place-items是将justify-items和align-items结合在一起的简写属性。上面的代码等同于下面代码:

.hero {
    display: grid;
    justify-items: center;
    align-items: center;
}

你可能想知道,这是怎么回事? 我们来解释一下。当使用place-items时,它将应用于网格中的每个单元格,也就是说单元格的内容都会居中。如果我们多增加几个单元格就会很清晰明了:

.hero {
    display: grid;
    grid-template-columns: 1fr 1fr;
    place-items: center;
}

Flexbox 与 margin 的配合

与flexbox 结合使用,margin: auto 可以非常轻松地将 flex 项目水平和垂直居中。

html

<div class="parent">
    <div class="child"></div>
</div>

css

.parent {
    width: 300px;
    height: 200px;
    background: #ccc;
    display: flex;
}
.child {
    width: 50px;
    height: 50px;
    background: #000;
    margin: auto;
}

看起来有点酷 😎

列表的 marker 属性

text-align 属性

display: inline-Flex 属性

column-rule 属性

background-repeat: round

object-fit 属性

4、资源:15 个优秀的响应式 CSS 框架

响应式 Web 设计旨在为各种设备(从台式机显示器到手机)提供最佳的浏览体验。本文汇总了一些优秀的响应式 Web 设计 HTML 和 CSS 框架。这些框架都是开源的并免费的。

对响应式 Web 框架进行比较并不那么容易。有的框架适合设计更快、更精简网站的某些功能,而有些可能提供了大量功能、插件和附加组件,但是可能体积会比较庞大并且上手较难。

1. Bootstrap

Bootstrap 是最流行的 HTML、CSS 和 JS 框架,用于在 Web 上开发响应式、移动优先项目。Bootstrap 使前端开发更快、更轻松。他们提供了大量的文档、示例和演示,可以帮你快速进行响应式 Web 开发。在 Bootstrap 5 中做了一些重大更改,例如随意使用 jQuery 并添加了 RTL 支持,再加上现成的组件和工具类,使 Bootstrap 成为 Web 开发人员的最佳选择之一。

你还可以找到许多免费的高级 bootstrap 模板 和 UI 工具包,这使你的开发过程更加轻松。

官网:https://getbootstrap.com/

2. Tailwind CSS

Tailwind 提供了一种基于实用工具的现代方法来构建响应站点。它有大量的实用工具类,无需编写 CSS 即可构建现代网站。它与其它框架的不同之处在于需要通过开发设置来缩小最终 CSS 的大小,因为如果使用默认值,最终将会得到一个很大的 CSS 文件。Tailwind 能够快速将样式添加到 HTML 元素中,并提供了大量的开箱即用的设计样式。这里有大量的 Tailwind CSS 资源:

https://superdevresources.com...

官网:https://tailwindcss.com/

3. Tachyons

Tachyons 也是一个基于实用工具的 CSS 库,它提供了许多即装即用的复杂功能,无需自己编写大量 CSS。这样做的好处是 Tachyons 的开箱即用样式很轻巧,不需要其他设置。如果需要的话,仍然可以通过一些方法来减小尺寸。如果你需要易用的实用工具库,那么这应该是一个不错的选择。

官网:https://tachyons.io/

4. Foundation

Foundation 是由产品设计公司 ZURB 制作的自适应前端框架。这个框架是他们自 1998 年来构建 Web 产品和服务的结果。Foundation 是最先进的响应式前端框架,并且提供了许多自定义功能。

官网:http://foundation.zurb.com/

5. Material Design for Bootstrap (MDB)

MDB 建立在 Bootstrap 之上,并提供了开箱即用的材料设计外观。它具有出色的 CSS 库,并且与大多数流行的 JavaScript 框架(如 jQuery、Angular、React 和。Vue.js)兼容。其核心库是完全免费使用的。

官网:https://mdbootstrap.com/

  1. UIkit
  2. Pure CSS
  3. Material Design Lite Framework (MDL)
  4. Materialize
  5. Skeleton
  6. Bulma
  7. Semantic UI
  8. Milligram
  9. Spectre.css
  10. Base CSS Framework

5、使用 mask 实现视频弹幕人物遮罩过滤

经常看一些 LOL 比赛直播的小伙伴,肯定都知道,在一些弹幕网站(Bilibili、虎牙)中,当人物与弹幕出现在一起的时候,弹幕会“巧妙”的躲到人物的下面,看着非常的智能。

简单的一个截图例子:

其实,这里是运用了 CSS 中的 MASK 属性实现的。

mask 简单用法介绍

之前在多篇文章都提到了 mask,比较详细的一篇是 -- 奇妙的 CSS MASK,本文不对 mask 的基本概念做过多讲解,向下阅读时,如果对一些 mask 的用法感到疑惑,可以再去看看。

这里只简单介绍下 mask 的基本用法:

最基本,使用 mask 的方式是借助图片,类似这样:

{
    /* Image values */
    mask: url(mask.png);                       /* 使用位图来做遮罩 */
    mask: url(masks.svg#star);                 /* 使用 SVG 图形中的形状来做遮罩 */
}

当然,使用图片的方式后文会再讲。借助图片的方式其实比较繁琐,因为我们首先还得准备相应的图片素材,除了图片,mask 还可以接受一个类似 background 的参数,也就是渐变。

类似如下使用方法:

{
    mask: linear-gradient(#000, transparent)                      /* 使用渐变来做遮罩 */
}

那该具体怎么使用呢?一个非常简单的例子,上述我们创造了一个从黑色到透明渐变色,我们将它运用到实际中,代码类似这样:

下面这样一张图片,叠加上一个从透明到黑色的渐变,

{
    background: url(image.png) ;
    mask: linear-gradient(90deg, transparent, #fff);
}

应用了 mask 之后,就会变成这样:

这个 DEMO,可以先简单了解到 mask 的基本用法。

这里得到了使用 mask 最重要结论:添加了 mask 属性的元素,其内容会与 mask 表示的渐变的 transparent 的重叠部分,并且重叠部分将会变得透明。

值得注意的是,上面的渐变使用的是 linear-gradient(90deg, transparent, #fff),这里的 #fff 纯色部分其实换成任意颜色都可以,不影响效果。

CodePen Demo -- 使用 MASK 的基本使用

https://codepen.io/Chokcoco/p...

使用 mask 实现人物遮罩过滤

了解了 mask 的用法后,接下来,我们运用 mask,简单实现视频弹幕中,弹幕碰到人物,自动被隐藏过滤的例子。

首先,我简单的模拟了一个召唤师峡谷,以及一些基本的弹幕:

方便示意,这里使用了一张静态图,表示了召唤师峡谷的地图,并非真的视频,而弹幕则是一条一条的 <p> 元素,和实际情况一致。伪代码大概是这样:

<!-- 地图 -->
<div class="g-map"></div>
<!-- 包裹所有弹幕的容器 -->
<div class="g-barrage-container">
    <!-- 所有弹幕 -->
    <div class="g-barrage">6666</div>
    ...
    <div class="g-barrage">6666</div>
</div>

为了模拟实际情况,我们再用一个 div 添加一个实际的人物,如果不做任何处理,其实就是我们看视频打开弹幕的感受,人物被视频所遮挡:

注意,这里我添加了一个人物亚索,并且用 animation 模拟了简单的运动,在运动的过程中,人物是被弹幕给遮挡住的。

接下来,就可以请出 mask 了。

我们利用 mask 制作一个 radial-gradient ,使得人物附近为 transparent,并且根据人物运动的 animation,给 mask 的 mask-position 也添加上相同的 animation 即可。最终可以得到这样的效果:

.g-barrage-container {
    position: absolute;
    mask: radial-gradient(circle at 100px 100px, transparent 60px, #fff 80px, #fff 100%);
    animation: mask 10s infinite alternate;
}
@keyframes mask {
    100% {
        mask-position: 85vw 0;
    }
}

实际上就是给放置弹幕的容器,添加一个 mask 属性,把人物所在的位置标识出来,并且根据人物的运动不断的去变换这个 mask 即可。我们把 mask 换成 background,原理一看就懂。

  • 把 mask 替换成 background 示意图:

background 透明的地方,即 mask 中为 transparent 的部分,实际就是弹幕会被隐藏遮罩的部分,而其他白色部分,弹幕不会被隐藏,正是完美的利用了 mask 的特性。

其实这项技术和视频本身是无关的,我们只需要根据视频计算需要屏蔽掉弹幕的位置,得到相应的 mask 参数即可。如果去掉背景和运动的人物,只保留弹幕和 mask,是这样的:

需要明确的是,使用 mask,不是将弹幕部分给遮挡住,而是利用 mask,指定弹幕容器之下,哪些部分正常展示,哪些部分透明隐藏。

最后,完整的 Demo 你可以戳这里:

CodePen Demo -- mask 实现弹幕人物遮罩过滤点击预览

https://codepen.io/Chokcoco/p...

实际生产环境中的运用

当然,上面我们简单的还原了利用 mask 实现弹幕遮罩过滤的效果。但是实际情况比上述的场景复杂的多,因为人物英雄的位置是不确定的,每一刻都在变化。所以在实际生产环境中,mask 图片的参数,其实是由后端实时对视频进行处理计算出来的,然后传给前端,前端再进行渲染。

对于运用了这项技术的直播网站,我们可以审查元素,看到包裹弹幕的容器的 mask 属性,每时每刻都在发生变化:

返回回来的其实是一个 SVG 图片,大概长这个样子:

这样,根据视频人物的实时位置变化,不断计算新的 mask,再实时作用于弹幕容器之上,实现遮罩过滤。

最后

本文到此结束,希望对你有帮助 :),本文介绍了 CSS mask 的一个实际生产环境中,非常有意义的一次实践,也表明很多新的 CSS 技术,运用得当,还是能给业务带来非常有益的帮助的。


image.png

查看原文

赞 22 收藏 15 评论 1

袁钰涵 赞了文章 · 3月11日

Babel7 相关

文章首发于个人github blog: Biu-blog,欢迎大家关注~

@babel/preset-env

@babel/preset-env 主要的功能是依据项目经过 babel 编译构建后产生的代码所对应运行的目标平台。@babel/preset-env 内部依赖了很多插件: @babel/plugin-transform-*。这些插件的工作主要就是 babel 在处理代码的过程当中对于新的 ES 语法的转换,将高版本的语法转化为低版本的写法。例如 @babel/plugin-transform-arrow-function 是用来转化箭头函数语法的。

基本的配置方法:

// babel.config.json
{
  "presets": [
    [
      "@babel/preset-env",
      {
        // 相关 preset 的配置
      }
    ]
  ]
}

对于 web 侧的项目或者 基于 Electron 的项目,一般会搭配着 .browserlistrc (或 package.json 里的 browserslist 字段) 来使用(确定最终构建平台)。

相关 options 配置

useBuiltIns

"usage" | "entry" | false, defaults to false.

这个配置选项也决定了 @babel/preset-env 如何去引用 polyfills。当这个配置选项为:usageentry@babel/preset-env 会直接建立起对于 core-js 相关 module 的引用。因此这也意味着 core-js 会被解析为对应的相对路径同时需要确保 core-js 在你的项目当中已经被安装了。

因为从 @babel/polyfill 从 7.4.0 版本开始就被弃用了,因此推荐直接配置 corejs 选项,并在项目当中直接安装 core-js

useBuiltIns: 'entry'

使用这种方式的配置需要在你的业务代码当中注入:

import 'core-js/stable'
import 'regenerator-runtime/runtime'

babel 处理代码的过程当中,会引入一个新的插件,同时 @babel/preset-env 会根据目标平台,例如 target 当中的配置,或者是 .browserlistrc 等来引入对应平台所需要的 polyfill

In:

import 'core-js'

Out(different based on environment):

import "core-js/modules/es.string.pad-start"
import "core-js/modules/es.string.pad-end"

注:其实这里的 useBuiltIns: entry 的配置以及需要在业务代码当中需要注入 core-jsregenerator-runtime/runtime,在业务代码当中注入对应的 package 从使用上来讲更多的是起到了占位的作用,由 @babel/preset-env 再去根据不同的目标平台去引入对应所需要的 polyfill 文件

同时在使用的过程中,如果是 import 'core-js' 那么在处理的过程当中会引入所有的 ECMAScript 特性的 polyfill,如果你只希望引入部分的特性,那么可以:

In:

import 'core-js/es/array'
import 'core-js/proposals/math-extensions'

Out:

import "core-js/modules/es.array.unscopables.flat";
import "core-js/modules/es.array.unscopables.flat-map";
import "core-js/modules/esnext.math.clamp";
import "core-js/modules/esnext.math.deg-per-rad";
import "core-js/modules/esnext.math.degrees";
import "core-js/modules/esnext.math.fscale";
import "core-js/modules/esnext.math.rad-per-deg";
import "core-js/modules/esnext.math.radians";
import "core-js/modules/esnext.math.scale";
useBuiltIns: 'usage'

自动探测代码当中使用的新的特性,并结合目标平台来决定引入对应新特性的 polyfill,因此这个配置是会最大限度的去减少引入的 polyfill 的数量来保证最终生成的 bundler 体积大小。

不过需要注意的是:由于 babel 处理代码本来就是一个非常耗时的过程,因此在我们实际的项目当中一般是对于 node_modules 当中的 package 进行 exclude 配置给忽略掉的,除非是一些明确需要走项目当中的 babel 编译的 package 会单独的去 include,所以 useBuiltIns: 'usage' 这种用法的话有个风险点就是 node_modules 当中的第三方包在实际的编译打包处理流程当中没有被处理(例如有些 package 提供了 esm 规范的源码,同时 package.json 当中也配置了 module 字段,那么例如使用 webpack 这样的打包工具的话会引入 module 字段对应的入口文件)

同时,如果使用 useBuiltIns: 'usage' 配置的话。是会在每个文件当中去引入相关的 polyfill 的,所以这里如果不借助 webpack 这种打包工具的话,是会造成代码冗余的。

useBuiltIns: false

Don't add polyfills automatically per file, and don't transform import "core-js" or import "@babel/polyfill" to individual polyfills.

corejs

corejs 的配置选项需要搭配着 useBuiltIns: usageuseBuiltIns: entry 来使用。默认情况下,被注入的 polyfill 都是稳定的已经被纳入 ECMAScript 规范当中的特性。如果你需要使用一些 proposals 当中的 feature 的话,那么需要配置:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        // 相关 preset 的配置
        corejs: {
          version: 3,
          proposals: true
        }
      }
    ]
  ]
}

@babel/plugin-transform-runtime

出现的背景:

Babel 在编译处理代码的过程当中会使用一些 helper 辅助函数,例如 _extend。这些辅助函数一般都会被添加到每个需要的被处理的文件当中。

因此 @babel/plugin-transform-runtime 所要解决的问题就是将所有对于需要这些 helper 辅助函数的引入全部指向 @babel/runtime/helpers 这个 module 当中的辅助函数,而不是给每个文件都添加对应 helper 辅助函数的内容。

另外一个目的就是去创建一个沙盒环境。因为如果你直接引入 core-js,或者 @babel/polyfill 的话,它所提供的 polyfill,例如 PromiseSetMap 等,是直接在全局环境下所定义的。因此会影响到所有使用到这些 API 的文件内容。所以如果你是写一个 library 的话,最好使用 @babel/plugin-transform-runtime 来完成相关 polyfill 的引入,这样能避免污染全局环境。

这个插件所做的工作其实也是引用 core-js 相关的模块来完成 polyfill 的功能。最终所达到的效果和使用 @babel/polyfill 是一样的。

配置方法:

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "corejs": 3
      }
    ]
  ]
}

The plugin defaults to assuming that all polyfillable APIs will be provided by the user. Otherwise the corejs option needs to be specified.

需要注意的是不同 corejs 版本提供的 helpers 有一些功能上的差异:corejs: 2 仅支持全局的定义,例如 Promise,和一些静态方法,例如 Array.from,实例上的方法是是不支持的,例如 [].includes。不过 corejs: 3 是支持实例上的方法的。

默认情况下,@babel/plugin-transform-runtime 是不会引入对于 proposals 的 polyfill 的,如果你是使用 corejs: 3 的话,可以通过配置 proposal: true 来开启这个功能。

corejs optionInstall command
falsenpm install --save @babel/runtime
2npm install --save @babel/runtime-corejs2
3npm install --save @babel/runtime-corejs3

技术实现细节

The transform-runtime transformer plugin does three things:

  1. Automatically requires @babel/runtime/regenerator when you use generators/async functions (toggleable with the regenerator option).
  2. Can use core-js for helpers if necessary instead of assuming it will be polyfilled by the user (toggleable with the corejs option)
  3. Automatically removes the inline Babel helpers and uses the module @babel/runtime/helpers instead (toggleable with the helpers option).

What does this actually mean though? Basically, you can use built-ins such as Promise, Set, Symbol, etc., as well use all the Babel features that require a polyfill seamlessly, without global pollution, making it extremely suitable for libraries.

Some tips

  1. 如果使用 @babel/preset-envuseBuiltIns: usage 搭配 browserlist 的这种 polyfill 的方式的话,polyfill 是会污染全局的(entry 模式也是污染全局)。不过这种配置的方式会依据目标打包平台来一定程度上减少不需要被加入到编译打包流程的 polyfill 的数量,因此这种方式也对应的能较少 bundle 最终的体积大小。
  2. 如果是走 @babel/plugin-transform-runtime 插件的 polyfill 的话不会污染全局。但是这个插件没法利用 browserlist 的目标平台配置的策略。因此在你代码当中只要是使用了 ES6+ 的新 api,一律都会引入对应的 polyfill 文件(而不考虑这个新的 api 是否被目标浏览器已经实现了),这样也会造成 bundle 体积增大。针对这个问题,官方也尝试提供一个新的 babel-polyfills package,以及策略去解决类似的问题。详见对应的文档以及issue

相关文档

  1. @babel/plugin-transform-runtime
  2. polyfill还是transform-runtime
查看原文

赞 5 收藏 2 评论 0

袁钰涵 赞了文章 · 3月10日

与你项目相关的npm知识总结

每次克隆下别人的代码后,执行的第一步就是 npm install 安装依赖包,安装成功后所有的包都会放在项目的 node_modules 文件夹下,也会自动生成 package-lock.json文件。有没有好奇过 node_modules 下的文件都是啥?package-lock.json 文件的作用是啥?

本文主要解决以下几个问题:

  1. package.json中的 dependenciesdevDependencies 的区别是啥,peerDependenciesbundledDependenciesoptionalDependencies又是啥?
  2. 为什么有的命令写在 package.json 中的 script 中就可以执行,但是通过命令行直接执行就不行?
  3. 为什么需要 package-lock.json 文件?
  4. 一个包在项目中有可能需要不同的版本,最后安装到根目录 node_modules 中的具体是哪个版本?

带着这几个问题,我们先从 package.json 文件说起。

package.json

最靠谱的官方文档请点这里

官方文档中列出了好多属性,感兴趣的可以一个个看一遍。下面只列出其中几个比较常用且重要的属性。

name & version

如果想要发布一个 npm 包,nameversion 属性是必须的。他们两个组合会形成一个唯一的标识来表名当前包。以后每更新一次包,version 就需要进行相应的更改。如果你不打算发布包,只想在本地使用,这两个字段不是必须的。

name 字段命名的规则如下:

  • 长度不能超过214个字符(对于有scoped的包,该限制包括scoped字段)(什么是Scoped packages?
  • 有作用域的包名字可以以.或者_开头,没有作用域限制的不可
  • 不能含有大写字母
  • 不能含有非URL安全的字符

version字段

版本号需要符合 semver(语义化版本号)规则,具体版本格式为:主版本号.次版本号.修订号, 如1.1.0。

  • 主版本号(major):做了不兼容的 API 修改
  • 次版本号(minor):做了向下兼容的功能性新增
  • 修订号(patch):做了向下兼容的问题修正

当有一些先行版本需要发布时,可以在 主版本号.次版本号.修订号 之后加上一个中划线和标识符如alpha(内部版本)、beta(公测版本)、rc(候选版本)等来表明。

以vue的版本为例:

  • 最新的稳定版本:3.0.5
  • 最新的rc版本:3.0.0-rc.13
  • 最新的beta版本:3.0.0-beta.24
  • 最新的alpha版本:3.0.0-alpha.13

可以通过 npm install semver 来检查一个包的命名是否符合 semver 规则。有关 semver 具体的说明可以看这里

dependencies & devDependencies

dependenciesdevDependencies大家应该都不陌生,通过 npm install xx --save 安装的包会写入 dependencies 中,通过 npm install xx --save-dev 安装的包会写入 devDependencies

dependencies 中的包是生产环境的依赖,属于线上代码的一部分,比如 vueaxiosveui 等。devDependencies 中的包是开发环境的依赖,只是在本地开发的时候需要依赖这里的包,比如 vue-loadereslint等。

我们平时用的 npm install 命令既会安装 dependencies 中的包,也会安装 devDependencies 中的包。如果只想安装 dependencies 中包,可以使用 npm install --production 或者将 NODE_ENV 环境变量设置为 production,通常在生成环境我们会这么用。

需要注意的是,一个模块会不会被打包取决于我们在项目中是否引入了该模块,跟该模块放在 dependencies 中还是 devDependencies 并没有关系。

对于我们的项目来说,把用到的包写在 dependencies 或者 devDependencies 并没有什么区别。但要是做为一个包发到 npm 上时,写在 devDependencies 中的依赖不会被下载。

peerDependencies & bundledDependencies & optionalDependencies

这三个属性在平时我们的项目开发中都用不到。不同于 dependencies & devDependencies面向的是包的使用者,peerDependencies & optionalDependencies & bundledDependencies这三个属性是面向包的发布者。

peerDependencies

我们在一些 node_modules 包的 package.json 中可以看到 peerDependencies,它用来表明如果你想要使用此插件,此插件要求宿主环境所安装的包。比如项目中用到的 veui1.0.0-alpha.24 版本中:

"peerDependencies": {
    "vue": "^2.5.16"
 }

这表明如果你想要使用 veui1.0.0-alpha.24 版本,所要求的 vue 版本需要满足 >=2.5.16<3.0.0

npm3.x 以上版本中,如果安装结束后宿主环境没有满足 peerDependencies 中的要求,会在控制台打印出警告信息。

bundledDependencies

当我们想在本地保留一个 npm 完整的包或者想生成一个压缩文件来获取 npm 包的时候,会用到 bundledDependencies。本地使用 npm pack 打包时会将 bundledDependencies 中依赖的包一同打包,当 npm install 时相应的包会同时被安装。需要注意的是,bundledDependencies 中的包不应该包含具体的版本信息,具体的版本信息需要在 dependencies 中指定。

例如一个 package.json 文件如下:

{
  "name": "awesome-web-framework",
  "version": "1.0.0",
  "bundledDependencies": [
    "renderized", 
    "super-streams"
  ]
}

当我们执行 npm pack 后会生成 awesome-web-framework-1.0.0.tgz 文件。该文件中包含 renderizedsuper-streams 这两个依赖,当执行 npm install awesome-web-framework-1.0.0.tgz 下载包时,这两个依赖会被安装。

当我们使用 npm publish 来发布包的话,这个属性不会起作用。

optionalDependencies

从名字上就可以看出,这是可选依赖。如果有包写在 optionalDependencies 中,即使 npm 找不到或者安装失败了也不会影响安装过程。需要注意的是, optionalDependencies 中的配置会覆盖 dependencies 中的配置,所以不要将同一个包同时放在这两个里面。

如果使用了 optionalDependencies,一定记得要在项目中做好异常处理,获取不到的情况下应该怎么办。

scripts

定义在 scripts 中的命令,我们通过 npm run <command> 就可以执行。npm run <command>npm run-script <command> 的简写。如果不加 command,则会列出当前目录下可执行的所有脚本。

teststartrestartstop 这几个命令执行时可以不加 run,直接 npm testnpm startnpm restartnpm stop 调用即可。

env 是一个内置的命令,可以通过 npm run env 可以获取到脚本运行时的所有环境变量。自定义的 env 命令会覆盖内置的 env 命令。

之前开发中遇到一种情况,比如我们想本地通过 http-server 启动一个服务器,如果事先没有全局安装过 http-server 包,只是安装在对应项目的 node_modules 中。在命令行中输入 http-server 会报 command not found,但是如果我们在 scripts 中增加如下一条命令就可以执行成功。

scripts: {
  "server": "http-server",
  "eslint": "eslint --ext .js"
}

为什么同样的命令写在 scripts 中就可以成功,但是在命令行中执行就不行呢?这是因为 npm run 命令会将 node_modules/.bin/ 加入到 shell 的环境变量 PATH 中,这样即使局部安装的包也可以直接执行而不用加 node_modules/.bin/ 前缀。当执行结束后,再将其删除。

是不是还是没明白,下面我们来具体分析一下。

首先要明确什么是环境变量。环境变量就是系统在执行一个程序,但是没有明确表明该程序所在的完整路径时,需要去哪里寻找该程序。

对于局部安装的包,拿 eslint 来说,npm 会在本地项目 ./node_modules/.bin 目录下创建一个指向 ./node_moudles/eslint/bin/eslint.js 名为 eslint 的软链接,即执行 ./node_modules/.bin/eslint 实际上是执行 ./node_moudles/eslint/bin/eslint.js。而当我们执行 npm run eslint 的时候,node_modules/.bin/ 会被加入到环境变量 PATH 中,实际上执行的是 ./node_modules/.bin/eslint,这样就串起来了。

理论说完之后,我们来实际验证一下。

首先看一下系统的环境变量。直接执行 env 即可。

然后在当前项目目录下通过npm run env查看脚本运行时的环境变量。

通过对比可以发现,运行时的 PATH 多了两个环境变量。即 npm 指令的路径和项目 /node_modules/.bin 的路径。

以上就是 package.json 中常用 & 重要的几个属性,接下来我们来看一看 package-lock.json

package-lock.json

对于 npmpackage.json 文件可以看成它的输入,node_modules 可以做为它的输出。在理想情况下,npm 应该是一个纯函数,无论何时执行相同的 package.json 文件都应该产生完全相同的 node_modules 树。在一些情况下,这确实可以做到。但是在大多情况下,都实现不了。主要有以下几个原因:

  • 使用者的 npm 版本有可能不同,不同的 npm 版本有着不同的安装算法
  • 自上次安装之后,有些符合 semver-range 的包已经有新的版本发布。这样再有别人安装的时候,会安装符合要求的最新版本。比如引入 vue 包:vue:^2.6.1。A小伙伴下载的时候是 2.6.1,过一阵有另一个小伙伴B入职在安装包的时候,vue 已经升级到 2.6.2,这样 npm 就会下载 2.6.2 的包安装在他的本地
  • 针对第二点,一个解决办法是固定自己引入的包的版本,但是通常我们不会这么做。即使这样做了,也只能保证自己引入的包版本固定,也无法保证包的依赖的升级。比如 vue 其中的一个依赖 lodashlodash:^4.17.4,A下载的是 4.17.4, B下载的时候有可能已经升级到了 4.17.21

为了解决上述问题,npm5.x 开始增加了 package-lock.json 文件。每当 npm install 执行的时候,npm 都会产生或者更新 package-lock.json 文件。package-lock.json 文件的作用就是锁定当前的依赖安装结构,与 node_modules 中下所有包的树状结构一一对应。

有了这个 package-lock.json 文件,就能保证团队每个人安装的包版本都是相同的,不会出现有些包升级造成我这好使别人那不好使的兼容性问题。

下面是 lesspackage-lock.json 文件结构:

"less": {
    "version": "3.13.1",
    "resolved": "https://registry.npmjs.org/less/-/less-3.13.1.tgz",
    "integrity": "sha512-SwA1aQXGUvp+P5XdZslUOhhLnClSLIjWvJhmd+Vgib5BFIr9lMNlQwmwUNOjXThF/A0x+MCYYPeWEfeWiLRnTw==",
    "dev": true,
    "requires": {
      "copy-anything": "^2.0.1",
      "errno": "^0.1.1",
      "graceful-fs": "^4.1.2",
      "image-size": "~0.5.0",
      "make-dir": "^2.1.0",
      "mime": "^1.4.1",
      "native-request": "^1.0.5",
      "source-map": "~0.6.0",
      "tslib": "^1.10.0"
    },
    dependencies: {
        "copy-anything": {
          "version": "2.0.3",
          "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.3.tgz",
          "integrity": "sha512-GK6QUtisv4fNS+XcI7shX0Gx9ORg7QqIznyfho79JTnX1XhLiyZHfftvGiziqzRiEi/Bjhgpi+D2o7HxJFPnDQ==",
          "dev": true,
          "requires": {
            "is-what": "^3.12.0"
          }
          }
    }
 }
  • version: 包的版本信息
  • resoloved: 包的安装源
  • integrity:一个 hash 值,用来校验包的完整性
  • dev:布尔值,如果为 true,表明此包如果不是顶层模块的一个开发依赖(写在 devDependencies 中),就是一个传递依赖(如上面 less 中的 copy-anything)。
  • requires: 对应子依赖的依赖,与依赖包的 package.jsondependencies 的依赖项相同
  • dependencies:结构与外层结构相同,存在于包自己的 node_modules 中的依赖(不是所有的包都有,当子依赖的依赖版本与根目录的 node_modules 中的依赖冲突时,才会有)

通过分析上面的 package-lock.json 文件,也许会有一个问题。为什么有的包可以被安装在根目录的 node_modules 中,有的包却只能安装在自己包下面的 node_modules 中?这就涉及到 npm 的安装机制。

npm从 3.x 开始,采用了扁平化的方式来安装 node_modules。在安装时,npm 会遍历整个依赖树,不管是项目的直接依赖还是子依赖的依赖,都会优先安装在根目录的 node_modules 中。遇到相同名称的包,如果发现根目录的 node_modules 中存在但是不符合 semver-range,会在子依赖的 node_modules 中安装符合条件的包。

具体的安装算法如下:

  • 从磁盘加载 node_modules
  • 克隆 node_modules
  • 获取 package.json 文件和分类完毕的元数据信息并把元数据信息插入到克隆树中
  • 遍历克隆树,检测是否有丢失的依赖。如果有,把他们添加到克隆树中,依赖会尽可能的添加到最高层
  • 比较原始树和克隆树,列出将原始树转换为克隆树所要采取的具体步骤
  • 执行,包括 install, update, remove and move

npm 官网的例子举例,假设 package{dep} 结构代表包和包的依赖,现有如下结构:A{B,C}, B{C}, C{D},按照上述算法执行完毕后,生成的 node_modules 结构如下:

A
+-- B
+-- C
+-- D

对于 B,C 被安装在顶层很好理解,因为是 A 的直接依赖。但是 B 又依赖 C,安装 C 的时候发现顶层已经有 C 了,所以不会在 B 自己的 node_modules 中再次安装。C 又依赖 D,安装 D 的时候发现根目录并没有 D,所以会把 D 提升到顶层。

换成 A{B,C}, B{C,D@1}, C{D@2} 这样的依赖关系后,产生的结构如下:

A
+-- B
+-- C
   +-- D@2
+-- D@1

B 又依赖了 D@1,安装时发现根目录的 node_modules 没有,所以会把 D@1 安装在顶层。C 依赖了 D@2,安装 D@2 时,因为 npm 不允许同层存在两个名字相同的包,这样就与跟目录 node_modulesD@1 冲突,所以会把 D@2 安装在 C 自己的 node_modules 中。

模块的安装顺序决定了当有相同的依赖时,哪个版本的包会被安装在顶层。首先项目中主动引入的包肯定会被安装在顶层,然后会按照包名称排序(a-z)进行依次安装,跟包在 package.json 中写入的顺序无关。因此,如果上述将 B{C,D@1} 换成 E{C,D@1},那么 D@2 将会被安装在顶层。

有一种情况,当我们项目中所引用的包版本较低,比如 A{B@1,C},而 C 所需要的是 C{B@2} 版本,现在的结构应该如下:

A
+-- B@1
+-- C
   +-- B@2

有一天我们将项目中的 B 升级到 B@2,理想情况下的结构应该如下:

A
+-- B@2
+-- C

但是现在 package-lock.json 文件的结构却是这样的:

A
+-- B@2
+-- C
   +-- B@2

B@2 不仅存在于根目录的 node_modules 下,C 下也同样存在。这时需要我们手动执行 npm dedupe 进行去重操作,执行完成后会发现 C 下面的 B@2 会消失。大家可以在自己的项目中试一试,优化一下 package-lock.json 文件的结构。

以下是在我的项目中执行 npm dedupe 的结果:

removed 41 packages, moved 15 packages and audited 1994 packages in 18.538s

npm5.x 之前,可以手动通过 npm shrinkwrap 生成 npm-shrinkwrap.json 文件,与 package-lock.json 文件的作用相同。当项目中同时存在 npm-shrinkwrap.jsonpackage-lock.json,将以 npm-shrinkwrap.json 为主。

执行 npm dedupe 去重之后的 node_modules 会瘦身一些,但做为一个有追求的程序员怎么能局限于仅仅瘦身呢,我们要紧跟时代的潮流,对一些过时的东西say no。这时, npm-outdated 命令就派上用场了。

npm-outdated 命令是用来检查项目中用到的包版本在当前是否已经过时。如果有过时的包,会在控制台打印出信息。默认情况下,只会列出项目中顶层依赖的过时信息。如果想要更深层的查看,可以加上 depth 参数,如 npm-outdated --depth=1

以下是在我的项目中执行 npm-outdated 的部分结果。从结果中可以看到包的当前版本,符合 semver-range 的最高版本以及当前的最新版本等信息。

Package          Current          Wanted          Latest         Location
animate.css      3.7.0            3.7.2           4.1.1          xxx
autoprefixer     9.7.6            9.8.6           10.2.5         xxx
axios            0.19.2           0.19.2          0.21.1         xxx
babel-eslint     7.2.3            7.2.3           10.1.0         xxx
babel-loader     7.1.5            7.1.5           8.2.2          xxx

有需求的小伙伴可以尝试把自己项目中用到的已经过时的包升级一下。

本文只是一些理论基础,之后会介绍一些 npm 源码相关的知识。

参考文章

  1. npm官网
  2. 前端工程化 - 剖析npm的包管理机制
  3. 前端工程化(5):你所需要的npm知识储备都在这了
  4. semver
查看原文

赞 20 收藏 16 评论 0

袁钰涵 发布了文章 · 3月7日

JS 文件互转、10 个 HTML 文件上传技巧、Web 用户体验设计提升指南、奇怪的知识——位掩码 | 思否技术周刊

今日分享提升工作幸福感的知识点,希望大家不要错过这些好文~

1、JS 文件 base64、File、Blob、ArrayBuffer 互转

二进制互转

  1. file对象转base64
let reader = new FileReader();
 reader.readAsDataURL(file[0])
 console.log(reader)
  1. base64 转成blob 上传
function dataURItoBlob(dataURI) {  
    var byteString = atob(dataURI.split(',')[1]);  
    var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];  
    var ab = new ArrayBuffer(byteString.length);  
    var ia = new Uint8Array(ab);  
    for (var i = 0; i < byteString.length; i++) {  
        ia[i] = byteString.charCodeAt(i);  
    }  
    return new Blob([ab], {type: mimeString});  
}
  1. blob 转成ArrayBuffer
let blob = new Blob([1,2,3,4])
let reader = new FileReader();
reader.onload = function(result) {
    console.log(result);
}
reader.readAsArrayBuffer(blob);
  1. buffer 转成blob
let blob = new Blob([buffer])
  1. base64 转 file
const base64ConvertFile = function (urlData, filename) { // 64转file
  if (typeof urlData != 'string') {
    this.$toast("urlData不是字符串")
    return;
  }
  var arr = urlData.split(',')
  var type = arr[0].match(/:(.*?);/)[1]
  var fileExt = type.split('/')[1]
  var bstr = atob(arr[1])
  var n = bstr.length
  var u8arr = new Uint8Array(n)
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new File([u8arr], 'filename.' + fileExt, {
    type: type
  });
}

2、10 个 HTML 文件上传技巧

上传文件功能可以说是项目经常出现的需求。从在社交媒体上上传照片到在求职网站上发布简历,文件上传无处不在。在本文中,我们将讨论 HTML文件上传支持的10种用法,希望对你有用。

1. 单文件上传

我们可以将input 类型指定为file,以在Web应用程序中使用文件上传功能。

<input type="file" id="file-uploader">

input filte 提供按钮上传一个或多个文件。默认情况下,它使用操作系统的本机文件浏览器上传单个文件。成功上传后,File API 使得可以使用简单的 JS 代码读取File对象。要读取File对象,我们需要监听 change事件。

首先,通过id获取文件上传的实例:

const fileUploader = document.getElementById('file-uploader');

然后添加一个change 事件侦听器,以在上传完成后读取文件对象, 我们从event.target.files属性获取上传的文件信息:

fileUploader.addEventListener('change', (event) => {
  const files = event.target.files;
  console.log('files', files);
});

在控制台中观察输出结果,这里关注一下FileList数组和File对象,该对象具有有关上传文件的所有元数据信息。

clipboard.png

如果大家看到这里,有点激动,想手贱一下,可以 CodePen 玩玩,地址:

https://codepen.io/atapas/pen...

2. 多文件上传

如果我们想上传多个文件,需要在标签上添加 multiple 属性:

<input type="file" id="file-uploader" multiple />

现在,我们可以上传多个文件了,以前面事例为基础,选择多个文件上传后,观察一下控制台的变化:

clipboard.png

如果大家看到这里,有点激动,想手贱一下,可以 CodePen 玩玩,地址:

https://codepen.io/atapas/pen...

3.了解文件元数据

每当我们上传文件时,File对象都有元数据信息,例如file name,size,last update time,type 等等。这些信息对于进一步的验证和特殊处理很有用。

const fileUploader = document.getElementById('file-uploader');

// 听更 change 件并读取元数据
fileUploader.addEventListener('change', (event) => {
  // 获取文件列表数组
  const files = event.target.files;
  // 遍历并获取元数据
  for (const file of files) {
    const name = file.name;
    const type = file.type ? file.type: 'NA';
    const size = file.size;
    const lastModified = file.lastModified;
    console.log({ file, name, type, size, lastModified });
  }
});

下面是单个文件上传的输出结果:

clipboard.png

如果大家看到这里,有点激动,想手贱一下,可以 CodePen 玩玩,地址:

https://codepen.io/atapas/pen...

4.了解 accept 属性

我们可以使用accept属性来限制要上载的文件的类型,如果只想上传的文件格式是 .jpg,.png 时,可以这么做:

<input type="file" id="file-uploader" accept=".jpg, .png" multiple>

在上面的代码中,只能选择后缀是.jpg和.png的文件。

如果大家看到这里,有点激动,想手贱一下,可以 CodePen 玩玩,地址:

https://codepen.io/atapas/pen...

5. 管理文件内容

成功上传文件后显示文件内容,站在用户的角度上,如果上传之后,没有一个预览的,就很奇怪也不体贴。

我们可以使用FileReader对象将文件转换为二进制字符串。然后添加load 事件侦听器,以在成功上传文件时获取二进制字符串。

// FileReader 实例
const reader = new FileReader();
fileUploader.addEventListener('change', (event) => {
  const files = event.target.files;
  const file = files[0];
  reader.readAsDataURL(file);
  reader.addEventListener('load', (event) => {
    const img = document.createElement('img');
    imageGrid.appendChild(img);
    img.src = event.target.result;
    img.alt = file.name;
  });
});

如果大家看到这里,有点激动,想手贱一下,可以 CodePen 玩玩,地址:

https://codepen.io/atapas/pen...

文章后面还有五个技巧分享,分别是:

6.验证文件大小

  1. 显示文件上传进度
  2. 怎么上传目录上传?
  3. 拖拽上传
  4. 使用objectURL处理文件

可以点击标题链接,去原文章浏览全部技巧~

3、前端优秀实践不完全指南

本文应该叫,Web 用户体验设计提升指南。

一个 Web 页面,一个 APP,想让别人用的爽,也就是所谓的良好的用户体验,我觉得他可能包括但不限于:

  • 急速的打开速度
  • 眼前一亮的 UI 设计
  • 酷炫的动画效果
  • 丰富的个性化设置
  • 便捷的操作b
  • 贴心的细节
  • 关注残障人士,良好的可访问性
  • ...

所谓的用户体验设计,其实是一个比较虚的概念,是秉承着以用户为中心的思想的一种设计手段,以用户需求为目标而进行的设计。设计过程注重以用户为中心,用户体验的概念从开发的最早期就开始进入整个流程,并贯穿始终。

良好的用户体验设计,是产品每一个环节共同努力的结果。

除去一些很难一蹴而就的,本文将就页面展示、交互细节、可访问性三个方面入手,罗列一些在实际的开发过程中,积攒的一些有益的经验。通过本文,你将能收获到:

  1. 了解到一些小细节是如何影响用户体验的
  2. 了解到如何在尽量小的开发改动下,提升页面的用户体验
  3. 了解到一些优秀的交互设计细节
  4. 了解基本的无障碍功能及页面可访问性的含义
  5. 了解基本的提升页面可访问性的方法

4、13个顶级免费所见即所得文本编辑器工具

CKEditor

CKEditor拥有10多年的开发经验,你可以完全放心此文本编辑器的质量。它支持70多种语言,我认为这是你网站的不错选择。它还可以运行在许多不同的浏览器上,并能很好地与大多数前端框架,如reat,vue,angular......你可以使用CDN直接嵌入到你的HTML页面中......。目前它有两个版本并行运行的CKEditor4和CKEditor5,根据不同的使用目的,你会选择适合自己的编辑器。

https://ckeditor.com/

Trumbowyg

Trumbowyg是针对HTML5优化的代码编辑器,它支持大多数流行的浏览器,例如IE9 +,Firefox,Chrome等。据我所知,它包含用于文本编辑的所有工具,仅为20Kb,它轻巧,将帮助你的网站更流畅地运行。此外,它还具有其他支持插件来帮助你更好地工作,例如插入表情符号,其他国家/地区的支持语言,添加声音,插入特殊字符...

https://alex-d.github.io/Trumbowyg/

TinyMCE

TinyMCE 5是一款编辑器,它能让你灵活地编辑、添加或删除本程序中的部分内容。除了基本的编辑器,那么我发现它还提供了很多支持,更好的用户体验,如添加评论,测试检查路径,提供优质的图标和界面,检查拼写的内容...... 然而,这也是它的弱点,因为如果你想使用高级工具,你必须每月支付约25美元。

https://www.tiny.cloud/features/

Quill

Quill是一个开放源代码编辑器,因此可以将其用于所有类型的商业或非商业网站。它有很多功能,如添加链接,图像,视频或添加代码片段的内容…关于Quill,我最喜欢的一点是它的简单设置和显示,可以在多设备屏幕上的所有现代的、响应迅速的web浏览器上显示,还有使用它的常见问题的详细说明。

https://quilljs.com/

Trix

Trix是一个开源的编辑器,可以让你在Web中轻松地撰写消息、写评论、写帖子......,并被良好编程的平板电脑使用。如果你只需要创建内容所需的功能,那么Trix同样是不错的选择。

https://trix-editor.org/

Jodit Editor 3

Jodit Editor 3是一个用纯TypeScript编写的开源github编辑器,不使用任何其他库。它允许你以多种方式设置它,如通过npm、使用CDN......。我喜欢它的是,除了详细的说明,还有一个程序,通过代码让我们自由选择哪些工具附加到Jodit Editor。

https://xdsoft.net/jodit/

Summernote

Summernote是GitHub上的开源编辑器,获得了超过9K星。它是通过Bootstrap框架设计的,具有在你的网站上创建内容所需的所有功能。你只需要下载它的源文件css,js,再加上Bootstrap框架(也支持3、4两个版本)就已经可以为你的网站服务了。

https://summernote.org/

Editor.js

Editor.js是一个开源的块状编辑器,它不会像普通的编辑器那样使用标签HTML,将内容以JSON的形式输出,使其更容易管理。它还支持通过使用API的插件,多亏了这一点,应该任何功能 任何开发者都可以为这个程序贡献更多有趣和有用的插件。

https://editorjs.io/

MediumEditor

MediumEditor是Medium的内置的开放源代码编辑器,用于人们博客。它仅包含编辑器所需的基本实用程序,因此仅约28kB,这将有助于你的网站得到优化。同时如果我们想要添加其他功能,为了优化编辑,MediumEditor还提供了额外的外部实用工具,定期更新。

https://yabwe.github.io/medium-editor/

Wysihtml

Wysihtml是一个由Voog团队构建的开源编辑器。它功能齐全,可以帮助你轻松编辑文本,并且支持大多数现代屏幕浏览器的设备图像。有很多工具我很喜欢它是自动转换不合适的HTML标签率,自动分析内容时从Word, PDF,显示内容为HTML…

http://wysihtml.com/

ContentTools

ContentTools是内置的开源编辑器,可帮助你轻松地一种方式编辑HTML内容。它提供了用于编辑内容的各种实用程序,你还可以轻松地将Message Institute和其他实用程序添加到程序中(请参阅脱机API部分)。我还发现了如何设置,添加或删除程序中的函数的文章…都是非常细致的。

https://getcontenttools.com/demo

Froala

Froala是一个编辑器,可以很容易地为网站设置,并允许你根据预期用途打开广泛的功能。由于它是用纯JavaScript编写的,因此你可以将其用于当今的大多数现代前端框架。它还提供了许多有用的工具,以及编辑图像,添加或编辑视频,添加图标,管理面板等。但是,如果你要使用该工具用于商业目的,则必须购买许可证。

https://froala.com/wysiwyg-editor/tour/

Redactor

Redactor是一款功能齐全的编辑器,具有精美而简单的设计。超过9年的发展,包括很多支持插件,我想这是一个很好的产品。另外它对程序员在使用程序的过程中遇到的每一个常见问题都有极其详细的实例。但是,它也有一个缺点,当你将其用于商业目的时必须购买许可证。

https://imperavi.com/redactor/

5、奇怪的知识——位掩码

假设我们有一个权限系统,它通过 JSON 的方式记录了某个用户的权限开通情况(姑且假设权限集是 CURD):

const permission = {
  create: false,
  update: false,
  read: true,
  delete: false,
}

如果我们把 false 写成 0,true 写成 1,那么这个 permisson 对象可以简写为 0b0010。

const permission = {
  create: false,
  update: false,
  read: true,
  delete: false,
}
// 从左往右,依次为 create, update, read, delete 所对应的值
const permissionBinary = 0b0010

对于 JSON 对象的权限集,如果我们要查看或者修改该用户的某些权限,只需要通过形如 permission.craete 的普通对象操作即可。那么如果对于二进制形式的权限集,我们又应该如何进行查看或者修改的操作呢?接下来我们就开始使用奇怪的知识——位掩码来进行了。

位掩码

首先进行名词解释,什么是”位掩码“。

位掩码(BitMask),是”位(Bit)“和”掩码(Mask)“的组合词。”位“指代着二进制数据当中的二进制位,而”掩码“指的是一串用于与目标数据进行按位操作的二进制数字。组合起来,就是”用一串二进制数字(掩码)去操作另一串二进制数字“的意思。

明白了位掩码的作用以后,我们就可以通过它来对权限集二进制数进行操作了。

1、查询用户是否拥有某个权限

已知用户权限集二进制数为 permissionBinary = 0b0010。如果我想知道该用户是否存在 update 这个权限,可以先给定一个位掩码 mask = 0b1。

image.png

由于 update 位于右数第三项,所以只需要把位掩码向左移动两位,剩余位置补0。最后和权限集二进制数进行按位与运算即可得到结果。

image.png

最后算出来的 result 为 0b0000,使用 Boolean() 函数处理之即可得到 false 的结果,也就是说该用户的 update 权限为 false。

// 从左往右,依次为 create, update, read, delete 所对应的值
const permissionBinary = 0b0010
// 由于 update 位于右数第三位,因此只需要让掩码向左移动2位即可
const mask = 0b1 << 2
const result = permissionBinary & mask
Boolean(result) // false

2、修改用户的某个权限

当我们明白了如何用位掩码来查询权限后,要修改对应的权限也就手到擒来了,无非就是换一种位运算。假设还是 update 权限,如果我想把它修改成 true,我们可以这么干:

image.png

只需要把按位与改为按位异或即可,代码如下:

// 从左往右,依次为 create, update, read, delete 所对应的值
const permissionBinary = 0b0010
// 由于 update 位于右数第三位,因此只需要让掩码向左移动2位即可
const mask = 0b1 << 2
const result = permissionBinary ^ mask
parseInt(result).toString(2) // 0b0110

经过上面的内容,相信你已经基本掌握了位掩码的知识,同时你肯定还有很多问号,比如说这么复杂又不好阅读的代码,真的有意义吗?

脏数据记录

前文例子中的权限系统仅有区区4个数据的处理,位掩码技术显得复杂又小题大做。那么有没有什么场景是真的适合使用位掩码的呢?脏数据记录就是其中一个。

假设我们存在着一份原始数据,其值如下:

let A = 'a'
let B = 'b'
let C = 'c'
let D = 'd'

给定一个二进制数,从左往右分别对应着 A/B/C/D 的状态:

let O = 0b0000 // 十进制 0

则数据一旦发生了修改,都可以用对应的比特位来表示

// 当且仅当 A 发生了修改
O = 0b1000 // 十进制 8
// 当且仅当 B 发生了修改
O = 0b0100 // 十进制 4
// 当且仅当 C 发生了修改
O = 0b0010 // 十进制 2
// 当且仅当 D 发生了修改
O = 0b0001 // 十进制 1

同理,当多个数据发生了修改时,则可以同时表示

// 当 A 和 B 发生了修改
O = 0b1100 // 十进制 12
// 当 A/B/C 都发生了修改
O = 0b1110 // 十进制 14

通过这个思路,应用排列组合的思想,可以很快知道只需要仅仅 4 个比特位,就可以表达 16 种数据变化的情况。由于二进制和十进制可以相互转化,因此只需要区区 16 个十进制数,就可以完整地表达 A/B/C/D 这四个数据的变化情况,也就是脏数据追踪。举个例子,给定一个脏数据记录 14,二进制转换为 0b1110,因此表示 A/B/C 的数据被修改了。

Svelte 这个框架,就是通过这个思路来实现响应式的:

if ( A 数据变了 ) {
  更新A对应的DOM节点
}
if ( B 数据变了 ) {
  更新B对应的DOM节点
}
/** 转化成伪代码 **/
if ( dirty & 8 ) { // 8 === 0b1000
  更新A对应的DOM节点
}
if ( dirty & 4 ) { // 4 === 0b0100
  更新B对应的DOM节点
}

老鼠喝毒药

除了用来做脏数据记录以外,位掩码也能够用来处理经典的”老鼠喝毒药“的问题。

有 1000 瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水24小时后就会死亡,问至少要多少只小白鼠才能在24小时内鉴别出哪瓶水有毒?

我们简化一下问题,假设只有 8 瓶水,其编号用二进制表示:

image.png

接着按照图示的方式对水瓶的水进行混合,得到样品 A/B/C/D,取4只老鼠编号为 a/b/c/d 分别喝下对应的水,得到如下的表格:

image.png

在 24 小时候,统计老鼠的死亡情况,汇总后可以得到表格和结果:

image.png

答案呼之欲出,由于 8 瓶水可以兑出 4 份样品,因此只需要 4 只老鼠即可在 24 小时后确定到底哪一瓶水是有毒的。回到题目,如果是 1000 瓶水,只需要知道第 1000 号的二进制数 0b1111101000即可。该二进制数一共有 10 个比特位,意味着 1000 瓶水可以兑出 10 份样品,也就是说只需要 10 只老鼠,就可以完成测试任务。

尾声

关于位掩码技术的探索就到这里。相信在认真读完这篇文章以后,大家心里已经建立起对位掩码技术的概念。这是一种非常特别的问题解决思路,也许在未来的某一天你真的会用上它。


image.png

查看原文

赞 25 收藏 17 评论 0

袁钰涵 赞了文章 · 3月7日

前端优秀实践不完全指南

本文其实应该叫,Web 用户体验设计提升指南。

一个 Web 页面,一个 APP,想让别人用的爽,也就是所谓的良好的用户体验,我觉得他可能包括但不限于:

  • 急速的打开速度
  • 眼前一亮的 UI 设计
  • 酷炫的动画效果
  • 丰富的个性化设置
  • 便捷的操作b
  • 贴心的细节
  • 关注残障人士,良好的可访问性
  • ...

所谓的用户体验设计,其实是一个比较虚的概念,是秉承着以用户为中心的思想的一种设计手段,以用户需求为目标而进行的设计。设计过程注重以用户为中心,用户体验的概念从开发的最早期就开始进入整个流程,并贯穿始终。

良好的用户体验设计,是产品每一个环节共同努力的结果。

除去一些很难一蹴而就的,本文将就页面展示交互细节可访问性三个方面入手,罗列一些在实际的开发过程中,积攒的一些有益的经验。通过本文,你将能收获到:

  1. 了解到一些小细节是如何影响用户体验的
  2. 了解到如何在尽量小的开发改动下,提升页面的用户体验
  3. 了解到一些优秀的交互设计细节
  4. 了解基本的无障碍功能及页面可访问性的含义
  5. 了解基本的提升页面可访问性的方法

页面展示

就整个页面的展示,页面内容的呈现而言,有一些小细节是需要我们注意的。

整体布局

先来看看一些布局相关的问题。

对于大部分 PC 端的项目,我们首先需要考虑的肯定是最外层的一层包裹。假设就是 .g-app-wrapper

<div class="g-app-wrapper">
    <!-- 内部内容 -->
</div>

首先,对于 .g-app-wrapper,有几点,是我们在项目开发前必须弄清楚的:

  1. 项目是全屏布局还是定宽布局?
  2. 对于全屏布局,需要适配的最小的宽度是多少?

对于定宽布局,就比较方便了,假设定宽为 1200px,那么:

.g-app-wrapper {
    width: 1200px;
    margin: 0 auto;
}

利用 margin: 0 auto 实现布局的水平居中。在屏幕宽度大于 1200px 时,两侧留白,当然屏幕宽度小于 1200px 时,则出现滚动条,保证内部内容不乱。

layout1

对于现代布局,更多的是全屏布局。其实现在也更提倡这种布局,即使用可随用户设备的尺寸和能力而变化的自适应布局。

通常而言是左右两栏,左侧定宽,右侧自适应剩余宽度,当然,会有一个最小的宽度。那么,它的布局应该是这样:

<div class="g-app-wrapper">
    <div class="g-sidebar"></div>
    <div class="g-main"></div>
</div>
.g-app-wrapper {
    display: flex;
    min-width: 1200px;
}
.g-sidebar {
    flex-basis: 250px;
    margin-right: 10px;
}
.g-main {
    flex-grow: 1;
}

layout2

利用了 flex 布局下的 flex-grow: 1,让 .main 进行伸缩,占满剩余空间,利用 min-width 保证了整个容器的最小宽度。

当然,这是最基本的自适应布局。对于现代布局,我们应该尽可能的考虑更多的场景。做到:

image

底部 footer

下面一种情形也是非常常见的一个情景。

页面存在一个 footer 页脚部分,如果整个页面的内容高度小于视窗的高度,则 footer 固定在视窗底部,如果整个页面的内容高度大于视窗的高度,则 footer 正常流排布(也就是需要滚动到底部才能看到 footer)。

看看效果:

margintopauto

嗯,这个需求如果能够使用 flex 的话,使用 justify-content: space-between 可以很好的解决,同理使用 margin-top: auto 也非常容易完成:

<div class="g-container">
    <div class="g-real-box">
        ...
    </div>
    <div class="g-footer"></div>
</div>
.g-container {
    height: 100vh;
    display: flex;
    flex-direction: column;
}

.g-footer {
    margin-top: auto;
    flex-shrink: 0;
    height: 30px;
    background: deeppink;
}

Codepen Demo -- sticky footer by flex margin auto

当然,实现它的方法有很多,这里仅给出一种推荐的解法。

处理动态内容 - 文本超长

对于所有接收后端接口字段的文本展示类的界面。都需要考虑全面(防御性编程:所有的外部数据都是不可信的),正常情况如下,是没有问题的。

image

但是我们是否考虑到了文本会超长?超长了会折行还是换行?

image

对于单行文本,使用单行省略:

{
    width: 200px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

image

当然,目前对于多行文本的超长省略,兼容性也已经非常好了:

{
    width: 200px;
    overflow : hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}

image

处理动态内容 - 保护边界

对于一些动态内容,我们经常使用 min/max-widthmin/max-height 对容器的高宽限度进行合理的控制。

在使用它们的时候,也有一些细节需要考虑到。

譬如经常会使用 min-width 控制按钮的最小宽度:

.btn {
    ...
    min-width: 120px;
}

image

当内容比较少的时候是没问题的,但是当内容比较长,就容易出现问题。使用了 min-width 却没考虑到按钮的过长的情况:

image

这里就需要配合 padding 一起:

.btn {
    ...
    min-width: 88px;
    padding: 0 16px
}

借用Min and Max Width/Height in CSS中一张非常好的图,作为释义:

min-width-2

0 内容展示

这个也是一个常常被忽略的地方。

页面经常会有列表搜索,列表展示。那么,既然存在有数据的正常情况,当然也会存在搜索不到结果或者列表无内容可展示的情形。

对于这种情况,一定要注意 0 结果页面的设计,同时也要知道,这也是引导用户的好地方。对于 0 结果页面,分清楚:

  • 数据为空:其中又可能包括了用户无权限、搜索无结果、筛选无结果、页面无数据
  • 异常状态:其中又可能包括了网络异常、服务器异常、加载失败等待

不同的情况可能对应不同的 0 结果页面,附带不同的操作引导。

譬如网络异常:

image

或者确实是 0 结果:

image

关于 0 结果页面设计,可以详细看看这篇文章:如何设计产品的空白页面?

小小总结一下,上述比较长的篇幅一直都在阐述一个道理,开发时,不能仅仅关注正常现象,要多考虑各种异常情况,思考全面。做好各种可能情况的处理

图片相关

图片在我们的业务中应该是非常的常见了。有一些小细节是需要注意的。

给图片同时设置高宽

有的时候和产品、设计会商定,只能使用固定尺寸大小的图片,我们的布局可能是这样:

image

对应的布局:

<ul class="g-container">
    <li>
        <img data-original="http://placehold.it/150x100">
        <p>图片描述</p>
    </li>
</ul>
ul li img {
    width: 150px;
}

当然,万一假设后端接口出现一张非正常大小的图片,上述不加保护的布局就会出问题:

image

所以对于图片,我们总是建议同时写上高和宽,避免因为图片尺寸错误带来的布局问题:

ul li img {
    width: 150px;
    height: 100px;
}

同时,给 <img> 标签同时写上高宽,可以在图片未加载之前提前占住位置,避免图片从未加载状态到渲染完成状态高宽变化引起的重排问题。

object-fit

当然,限制高宽也会出现问题,譬如图片被拉伸了,非常的难看:

image

这个时候,我们可以借助 object-fit,它能够指定可替换元素的内容(也就是图片)该如何适应它的父容器的高宽。

ul li img {
    width: 150px;
    height: 100px;
    object-fit: cover;
}

利用 object-fit: cover,使图片内容在保持其宽高比的同时填充元素的整个内容框。

image

object-fit 还有一个配套属性 object-position,它可以控制图片在其内容框中的位置。(类似于 background-position),m默认是 object-position: 50% 50%,如果你不希望图片居中展示,可以使用它去改变图片实际展示的 position 。

ul li img {
    width: 150px;
    height: 100px;
    object-fit: cover;
    object-position: 50% 100%;
}

image

像是这样,object-position: 100% 50% 指明从底部开始展示图片。这里有一个很好的 Demo 可以帮助你理解 object-position

CodePen Demo -- Object position

考虑屏幕 dpr -- 响应式图片

正常情况下,图片的展示应该没有什么问题了。但是对于有图片可展示的情况下,我们还可以做的更好。

在移动端或者一些高清的 PC 屏幕(苹果的 MAC Book),屏幕的 dpr 可能大于 1。这种时候,我们可能还需要考虑利用多倍图去适配不同 dpr 的屏幕。

正好,<img> 标签是有提供相应的属性 srcset 让我们进行操作的。

<img data-original='photo@1x.png'
   srcset='photo@1x.png 1x,
           photo@2x.png 2x,
           photo@3x.png 3x' 
/>

当然,这是比较旧的写法,srcset 新增了新的 w 宽度描述符,需要配合 sizes 一起使用,所以更好的写法是:

<img 
        src = "photo.png" 
        sizes = “(min-width: 600px) 600px, 300px" 
        srcset = “photo@1x.png 300w,
                       photo@2x.png 600w,
                       photo@3x.png 1200w,
>

利用 srcset,我们可以给不同 dpr 的屏幕,提供最适合的图片。

上述出现了一些概念,dpr,图片的 srcset ,sizes 属性,,不太了解的可以移步 前端基础知识概述

图片丢失

好了,当图片链接没问题时,已经处理好了。接下来还需要考虑,当图片链接挂了,应该如何处理。

处理的方式有很多种。最好的处理方式,是我最近在张鑫旭老师的这篇文章中 -- 图片加载失败后CSS样式处理最佳实践 看到的。这里简单讲下:

  1. 利用图片加载失败,触发 <img> 元素的 onerror 事件,给加载失败的 <img> 元素新增一个样式类
  2. 利用新增的样式类,配合 <img> 元素的伪元素,展示默认兜底图的同时,还能一起展示 <img> 元素的 alt 信息
<img data-original="test.png" alt="图片描述" onerror="this.classList.add('error');">
img.error {
    position: relative;
    display: inline-block;
}

img.error::before {
    content: "";
    /** 定位代码 **/
    background: url(error-default.png);
}

img.error::after {
    content: attr(alt);
    /** 定位代码 **/
}

我们利用伪元素 before ,加载默认错误兜底图,利用伪元素 after,展示图片的 alt 信息:

image

OK,到此,完整的对图片的处理就算完成了,完整的 Demo 你可以戳这里看看:

CodePen Demo -- 图片处理

交互设计优化

接下来一个大环节是关于一些交互的细节。对于交互设计,一些比较通用的准则:

  • Don’t make me think
  • 符合用户的习惯与预期
  • 操作便利
  • 做适当的提醒
  • 不强迫用户

过渡与动画

在我们的交互过程中,适当的增加过渡与动画,能够很好的让用户感知到页面的变化

譬如我们页面上随处可见 loading 效果,其实就是这样一种作用,让用户感知页面正在加载,或者正在处理某些事务。

滚动优化

滚动也是操作网页中非常重要的一环。看看有哪些可以优化的点:

滚动平滑:使用 scroll-behavior: smooth 让滚动丝滑

使用 scroll-behavior: smooth,可以让滚动框实现平稳的滚动,而不是突兀的跳动。看看效果,假设如下结构:

<div class="g-container">
  <nav>
    <a href="#1">1</a>
    <a href="#2">2</a>
    <a href="#3">3</a>
  </nav>
  <div class="scrolling-box">
    <section id="1">First section</section>
    <section id="2">Second section</section>
    <section id="3">Third section</section>
  </div>
</div>

不使用 scroll-behavior: smooth,是突兀的跳动切换:

scrol

给可滚动容器添加 scroll-behavior: smooth,实现平滑滚动:

{
    scroll-behavior: smooth;
}

scroll2

使用 scroll-snap-type 优化滚动效果

sroll-snap-type 可能算得上是新的滚动规范里面最核心的一个属性样式。

scroll-snap-type:属性定义在滚动容器中的一个临时点(snap point)如何被严格的执行。

光看定义有点难理解,简单而言,这个属性规定了一个容器是否对内部滚动动作进行捕捉,并且规定了如何去处理滚动结束状态。让滚动操作结束后,元素停止在适合的位置。

看个简单示例:

当然,scroll-snap-type 用法非常多,可控制优化的点很多,限于篇幅无法一一展开,具体更详细的用法可以看看我的另外一篇文章 -- 使用 sroll-snap-type 优化滚动

控制滚动层级,避免页面大量重排

这个优化可能稍微有一点难理解。需要了解 CSS 渲染优化的相关知识。

先说结论,控制滚动层级的意思是尽量让需要进行 CSS 动画(可以是元素的动画,也可以是容器的滚动)的元素的 z-index 保持在页面最上方,避免浏览器创建不必要的图形层(GraphicsLayer),能够很好的提升渲染性能

这一点怎么理解呢,一个元素触发创建一个 Graphics Layer 层的其中一个因素是:

  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素

根据上述这点,我们对滚动性能进行优化的时候,需要注意两点:

  1. 通过生成独立的 GraphicsLayer,利用 GPU 加速,提升滚动的性能
  2. 如果本身滚动没有性能问题,不需要独立的 GraphicsLayer,也要注意滚动容器的层级,避免因为层级过高而被其他创建了 GraphicsLayer 的元素合并,被动的生成一个 Graphics Layer ,影响页面整体的渲染性能

如果你对这点还有点懵,可以看看这篇文章 -- 你所不知道的 CSS 动画技巧与细节

点击交互优化

在用户点击交互方面,也有一些有意思的小细节。

优化手势 -- 不同场景应用不同 cursor

对于不同的内容,最好给与不同的 cursor 样式,CSS 原生提供非常多种常用的手势。

在不同的场景使用不同的鼠标手势,符合用户的习惯与预期,可以很好的提升用户的交互体验。

首先对于按钮,就至少会有 3 种不同的 cursor,分别是可点击,不可点击,等待中:

{
    cursor: pointer;    // 可点击
    cursor: not-allowed;    // 不可点击
    cursor: wait;    // loading
}

image

除此之外,还有一些常见的,对于一些可输入的 Input 框,使用 cursor: text,对于提示 Tips 类使用 cursor: help,放大缩小图片 zoom-inzoom-out 等等:

image

一些常用的简单列一列:

  • 按钮可点击: cursor: pointer
  • 按钮禁止点击:cursor: not-allowed
  • 等待 Loading 状态:cursor: wait
  • 输入框:cursor: text;
  • 图片查看器可放大可缩小:cursor: zoom-in/ zoom-out
  • 提示:cursor: help;

当然,实际 cursor 还支持非常多种,可以在 MDN 或者下面这个 CodePen Demo 中查看这里看完整的列表:

CodePen Demo -- Cursor Demo

点击区域优化 -- 伪元素扩大点击区域

按钮是我们网页设计中十分重要的一环,而按钮的设计也与用户体验息息相关。

考虑这样一个场景,在摇晃的车厢上或者是单手操作着屏幕,有的时候一个按钮,死活也点不到。

让用户更容易的点击到按钮无疑能很好的增加用户体验及可提升页面的访问性,尤其是在移动端,按钮通常都很小,但是受限于设计稿或者整体 UI 风格,我们不能直接去改变按钮元素的高宽。

那么这个时候有什么办法在不改变按钮原本大小的情况下去增加他的点击热区呢?

这里,伪元素也是可以代表其宿主元素来响应的鼠标交互事件的。借助伪元素可以轻松帮我们实现,我们可以这样写:

.btn::before{
  content:"";
  position:absolute;
  top:-10px;
  right:-10px;
  bottom:-10px;
  left:-10px;
}

当然,在 PC 端下这样子看起来有点奇怪,但是合理的用在点击区域较小的移动端则能取到十分好的效果,效果如下:

608782-20160527112625428-906375003

在按钮的伪元素没有其它用途的时候,这个方法确实是个很好的提升用户体验的点。

快速选择优化 -- user-select: all

操作系统或者浏览器通常会提供一些快速选取文本的功能,看看下面的示意图:

layout3

快速单击两次,可以选中单个单词,快速单击三次,可以选中一整行内容。但是如果有的时候我们的核心内容,被分隔符分割,或者潜藏在一整行中的一部分,这个时候选取起来就比较麻烦。

利用 user-select: all,可以将需要一次选中的内容进行包裹,用户只需要点击一次,就可以选中该段信息:

.g-select-all {
    user-select: all
}

给需要一次选中的信息,加上这个样式后的效果,这个细节作用在一些需要复制粘贴的场景,非常好用:

layout4

CodePen -- user-select: all 示例

选中样式优化 -- ::selection

当然,如果你想更进一步,CSS 还有提供一个 ::selection 伪类,可以控制选中的文本的样式(只能控制color, background, text-shadow),进一步加深效果。

layout5

CodePen -- user-select: all && ::selection 控制选中样式

添加禁止选择 -- user-select: none

有快速选择,也就会有它的对立面 -- 禁止选择。

对于一些可能频繁操作的按钮,可能出现如下尴尬的场景:

  • 文本按钮的快速点击,触发了浏览器的双击快速选择,导致文本被选中:

btn-click

  • 翻页按钮的快速点击,触发了浏览器的双击快速选择:

img-click

对于这种场景,我们需要把不可被选中元素设置为不可被选中,利用 CSS 可以快速的实现这一点:

{
    -webkit-user-select: none; /* Safari */
    -ms-user-select: none; /* IE 10 and IE 11 */
    user-select: none; /* Standard syntax */
}

这样,无论点击的频率多快,都不会出现尴尬的内容选中:

btn-click-unselect

跳转优化

现阶段,单页应用(Single Page Application)的应用非常广泛,Vue 、React 等框架大行其道。但是一些常见的写法,也容易衍生一些小问题。

譬如,点击按钮、文本进行路由跳转。譬如,经常会出现这种代码:

<template>
    ...
    <button @click="gotoDetail">
        Detail
    </button>
    ...
<template>
...
gotoDetail() {
    this.$router.push({
      name: 'xxxxx',
    });
}

大致逻辑就是给按钮添加一个事件,点击之后,跳转到另外一个路由。当然,本身这个功能是没有任何问题的,但是没有考虑到用户实际使用的场景。

实际使用的时候,由于是一个页面跳转,很多时候,用户希望能够保留当前页面的内容,同时打开一个新的窗口,这个时候,他会尝试下的鼠标右键,选择在新标签页中打开页面,遗憾的是,上述的写法是不支持鼠标右键打开新页面的。

原因在于浏览器是通过读取 <a> 标签的 href 属性,来展示类似在新标签页中打开页面这种选项,对于上述的写法,浏览器是无法识别它是一个可以跳转的链接。简单的示意图如下:

image

所以,对于所有路由跳转按钮,建议都使用 <a> 标签,并且内置 href 属性,填写跳转的路由地址。实际渲染出来的 DOM 可能是需要类似这样:

<a href="/xx/detail">Detail</a>

易用性

易用性也是交互设计中需要考虑的一个非常重要的环节,能做的有非常多。简单的罗列一下:

  • 注意界面元素的一致性,降低用户学习成本
  • 延续用户日常的使用习惯,而不是重新创造
  • 给下拉框增加一些预设值,降低用户填写成本
  • 同类的操作合并在一起,降低用户的认知成本
  • 任何操作之后都要给出反馈,让用户知道操作已经生效

先探索,后表态

这一点非常的有意思,什么叫先探索后表态呢?就是我们不要一上来就强迫用户去做一些事情,譬如登录。

想一想一些常用网站的例子:

  • 类似虎牙、Bilibili 等视频网站,可以先蓝光体验,一定观看时间后才会要求登录
  • 电商网站,只有到付款的时候,才需要登录

上述易用性先探索,后表态的内容,部分来源于:Learn From What Leading Companies A/B Test,可以好好读一读。

字体优化

字体的选择与使用其实是非常有讲究的。

如果网站没有强制必须使用某些字体。最新的规范建议我们更多的去使用系统默认字体。也就是 CSS Fonts Module Level 4 -- Generic font families 中新增的 font-family: system-ui 关键字。

font-family: system-ui 能够自动选择本操作系统下的默认系统字体。

默认使用特定操作系统的系统字体可以提高性能,因为浏览器或者 webview 不必去下载任何字体文件,而是使用已有的字体文件。 font-family: system-ui 字体设置的优势之处在于它与当前操作系统使用的字体相匹配,对于文本内容而言,它可以得到最恰当的展示。

举两个例子,天猫的字体定义与 Github 的字体定义:

  • 天猫font-family: "PingFang SC",miui,system-ui,-apple-system,BlinkMacSystemFont,Helvetica Neue,Helvetica,sans-serif;
  • Githubfont-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;

简单而言,它们总体遵循了这样一个基本原则:

1、尽量使用系统默认字体

使用系统默认字体的主要原因是性能,并且系统字体的优点在于它与当前操作系统使用的相匹配,因此它的文本展示必然也是一个让人舒适展示效果。

2、兼顾中西,西文在前,中文在后

中文或者西文(英文)都要考虑到。由于大部分中文字体也是带有英文部分的,但是英文部分又不怎么好看,但是英文字体中大多不包含中文。通常会先进行英文字体的声明,选择最优的英文字体,这样不会影响到中文字体的选择,中文字体声明则紧随其次。

3、兼顾多操作系统

选择字体的时候要考虑多操作系统。例如 MAC OS 下的很多中文字体在 Windows 都没有预装,为了保证 MAC 用户的体验,在定义中文字体的时候,先定义 MAC 用户的中文字体,再定义 Windows 用户的中文字体;

4、兼顾旧操作系统,以字体族系列 serif 和 sans-serif 结尾

当使用一些非常新的字体时,要考虑向下兼容,兼顾到一些极旧的操作系统,使用字体族系列 serif 和 sans-serif 结尾总归是不错的选择。

对于上述的一些字体可能会有些懵,譬如 -apple-system, BlinkMacSystemFont,这是因为不同浏览器厂商对规范的实现有所不同,对于字体定义更多的相关细节,可以再看看这篇文章 -- Web 字体 font-family 再探秘

可访问性(A11Y)

可访问性,在我们的网站中,属于非常重要的一环,但是大部分前端(其实应该是设计、前端、产品)同学都会忽视它。

我潜伏在一个叫无障碍设计小组的群里,其中包含了很多无障碍设计师以及患有一定程度视觉、听力、行动障碍的用户,他们在群里经常会表达出一个观点,就是国内的大部分 Web 网站及 APP 基本没有考虑过残障人士的使用(或者可访问性做的非常差),非常的令人揪心。

尤其在我们一些重交互、重逻辑的网站中,我们需要考虑用户的使用习惯、使用场景,从高可访问性的角度考虑,譬如假设用户没有鼠标,仅仅使用键盘,能否顺畅的使用我们的网站?

假设用户没有鼠标,这个真不一定是针对残障人士,很多情况下,用户拿鼠标的手可能在干其他事情,比如在吃东西,又或者在 TO B 类的业务,如超市收银、仓库收货,很可能用户拿鼠标的手操作着其他设备(扫码枪)等等。

本文不会专门阐述无障碍设计的方方面面,只是从一些我觉得前端工程师需要关注的,并且仅需要花费少量代价就能做好的一些无障碍设计细节。记住,无障碍设计对所有人都更友善

色彩对比度

颜色,也是我们天天需要打交道的属性。对于大部分视觉正常的用户,可能对页面的颜色敏感度还没那么高。但是对于一小部分色弱、色盲用户,他们对于网站的颜色会更加敏感,不好的设计会给他们访问网站带来极大的不便。

什么是色彩对比度

是否曾关心过页面内容的展示,使用的颜色是否恰当?色弱、色盲用户能否正常看清内容?良好的色彩使用,在任何时候都是有益的,而且不仅仅局限于对于色弱、色盲用户。在户外用手机、阳光很强看不清,符合无障碍标准的高清晰度、高对比度文字就更容易阅读。

这里就有一个概念 -- 颜色对比度,简单地说,描述就是两种颜色在亮度(Brightness)上的差别。运用到我们的页面上,大多数的情况就是背景色(background-color)与内容颜色(color)的对比差异。

最权威的互联网无障碍规范 —— WCAG AA规范规定,所有重要内容的色彩对比度需要达到 4.5:1 或以上(字号大于18号时达到 3:1 或以上),才算拥有较好的可读性。

借用一张图 -- 知乎 -- 助你轻松做好无障碍的15个UI设计工具推荐

很明显,上述最后一个例子,文字已经非常的不清晰了,正常用户都已经很难看得清了。

检查色彩对比度的工具

Chrome 浏览器从很早开始,就已经支持检查元素的色彩对比度了。以我当前正在写作的页面为例子,Github Issues 编辑页面的两个按钮:

image

审查元素,分别可以看到两个按钮的色彩对比度:

image

可以看到,绿底白字按钮的色彩对比度是没有达到标准的,也被用黄色的叹号标识了出来。

除此之外,在审查元素的 Style 界面的取色器,改变颜色,也能直观的看到当前的色彩对比度:

image

焦点响应

类似百度、谷歌的首页,进入页面后会默认让输入框获得焦点:

image

并非所有的有输入框的页面,都需要进入页面后进行聚焦,但是焦点能够让用户非常明确的知道,当前自己在哪,需要做些什么。尤其是对于无法操作鼠标的用户。

页面上可以聚焦的元素,称为可聚焦元素,获得焦点的元素,则会触发该元素的 focus 事件,对应的,也就会触发该元素的 :focus 伪类。

浏览器通常会使用元素的 :focus 伪类,给元素添加一层边框,告诉用户,当前的获焦元素在哪里。

我们可以通过键盘的 Tab 键,进行焦点的切换,而获焦元素则可以通过元素的 :focus 伪类的样式,告诉用户当前焦点位置。

当然,除了 Tab 键之外,对于一些多输入框、选择框的表单页面,我们也应该想着如何简化用户的操作,譬如用户按回车键时自动前进到下一字段。一般而言,用户必须执行的触按越少,体验越佳。

下面的截图,完全由键盘操作完成

a11y

通过元素的 :focus 伪类以及键盘 Tab 键切换焦点,用户可以非常顺畅的在脱离鼠标的情况下,对页面的焦点切换及操作。

然而,在许多 reset.css 中,经常能看到这样一句 CSS 样式代码,为了样式的统一,消除了可聚焦元素的 :focus 伪类:

:focus {
    outline: 0;
}

我们给上述操作的代码。也加上这样一句代码,全程再用键盘操作一下

a11y2

除了在 input 框有光标提示,当使用 Tab 进行焦点切换到 select 或者到 button 时,由于没有了 :focus 样式,用户将完全懵逼,不知道页面的焦点现在处于何处。

保证非鼠标用户体验,合理运用 :focus-visible

当然,造成上述结果很重要的一个原因在于。:focus 伪类不论用户在使用鼠标还是使用键盘,只要元素获焦,就会触发。

而其本身的默认样式又不太能被产品或者设计接受,导致了很多人会在焦点元素触发 :focus 伪类时,通过改变 border 的颜色或者其他一些方式替代或者直接禁用。而这样做,从可访问性的角度来看,对于非鼠标用户,无疑是灾难性的。

基于此,在W3 CSS selectors-4 规范 中,新增了一个非常有意思的 :focus-visible 伪类。

:focus-visible:这个选择器可以有效地根据用户的输入方式(鼠标 vs 键盘)展示不同形式的焦点。

有了这个伪类,就可以做到,当用户使用鼠标操作可聚焦元素时,不展示 :focus 样式或者让其表现较弱,而当用户使用键盘操作焦点时,利用 :focus-visible,让可获焦元素获得一个较强的表现样式。

看个简单的 Demo:

<button>Test 1</button>
button:active {
  background: #eee;
}
button:focus {
  outline: 2px solid red;
}

使用鼠标点击:

a11y3

可以看到,使用鼠标点击的时候,触发了元素的 :active 伪类,也触发了 :focus伪类,不太美观。但是如果设置了 outline: none 又会使键盘用户的体验非常糟糕。尝试使用 :focus-visible 伪类改造一下:

button:active {
  background: #eee;
}
button:focus {
  outline: 2px solid red;
}
button:focus:not(:focus-visible) {
  outline: none;
}

看看效果,分别是在鼠标点击 Button 和使用键盘控制焦点点击 Button:

a11y4

CodePen Demo -- :focus-visible example

可以看到,使用鼠标点击,不会触发 :foucs,只有当键盘操作聚焦元素,使用 Tab 切换焦点时,outline: 2px solid red 这段代码才会生效。

这样,我们就既保证了正常用户的点击体验,也保证了一批无法使用鼠标的用户的焦点管理体验。

值得注意的是,有同学会疑惑,这里为什么使用了 :not 这么绕的写法而不是直接这样写呢:

button:focus {
  outline: unset;
}
button:focus-visible {
  outline: 2px solid red;
}

为的是兼容不支持 :focus-visible 的浏览器,当 :focus-visible 不兼容时,还是需要有 :focus 伪类的存在。

使用 WAI-ARIA 规范增强语义 -- div 等非可获焦元素模拟获焦元素

还有一个非常需要注意的点。

现在很多前端同学在前端开发的过程中,喜欢使用非可获焦元素模拟获焦元素,譬如:

  • 使用 div 模拟 button 元素
  • 使用 ul 模拟下拉列表 select 等等

当下很多组件库都是这样做的,譬如 element-ui 和 ant-design。

在使用非可获焦元素模拟获焦元素的时候,一定要注意,不仅仅只是外观长得像就完事了,其行为表现也需要符合原本的 buttonselect 等可聚焦元素的性质,能够体现元素的语义,能够被聚焦,能够通过 Tab 切换等等。

基于大量类似的场景,有了 WAI-ARIA 标准,WAI-ARIA是一个为残疾人士等提供无障碍访问动态、可交互Web内容的技术规范。

简单来说,它提供了一些属性,增强标签的语义及行为:

  • 可以使用 tabindex 属性控制元素是否可以聚焦,以及它是否/在何处参与顺序键盘导航
  • 可以使用 role 属性,来标识元素的语义及作用,譬如使用 <div id="saveChanges" tabindex="0" role="button">Save</div> 来模拟一个按钮
  • 还有大量的 aria-* 属性,表示元素的属性或状态,帮助我们进一步地识别以及实现元素的语义化,优化无障碍体验

使用工具查看标签的语义

我们来看看 Github 页面是如何定义一个按钮的,以 Github Issues 页面的 Edit 按钮为例子:

image

这一块,清晰的描述了这个按钮在可访问性相关的一些特性,譬如 Contrast 色彩对比度,按钮的描述,也就是 Name,是给屏幕阅读器看到的,Role 标识是这个元素的属性,它是一个按钮,Keyboard focusable 则表明他能否被键盘的 Tab 按钮给捕获。

分析使用非可聚焦元素模拟的按钮

这里,我随便选取了我们业务中一个使用 span 模拟按钮的场景,是一个面包屑导航,点击可进行跳转,发现惨不忍睹:

image

HTML 代码:

<span class="ssc-breadcrumb-item-link"> Inbound </span>

image

基本上可访问性为 0,作为一个按钮,它不可被聚焦,无法被键盘用户选中,没有具体的语义,色彩对比度太低,可能视障用户无法看清。并且,作为一个能进行页面跳转的按钮,它没有不是 a 标签,没有 href 属性。

即便对于面包屑导航,我们可以不将它改造成 <a> 标签,也需要做到最基本的一些可访问性改造:

<span role="button" aria-label="goto inbound page" tabindex="0" class="ssc-breadcrumb-item-link"> Inbound </span>

不要忘了再改一下颜色,达到最低色彩对比度以上,再看看:

image

OK,这样,一个最最最基本的,满足最低可访问性需求的按钮算是勉强达标,当然,这个按钮可以再更进一步进行改造,涉及了更深入的可访问性知识,本文不深入展开。

分析组件库的 A11Y

最后,在我们比较常用的 Vue - element-ui、React - ant-design 中,我们来看看 ant-design 在提升可访问性相关的一些功能。

以 Select 选择框组件为例,ant-design 利用了大量的 WAI-ARIA 属性,使得用 div 模拟的下拉框不仅仅在表现上符合一个下拉框,在语义、行为上都符合一个下拉框,简单的一个例子:

image

看看使用 div 模拟下拉框的 DOM 部分:

image

再看看在交互体验上:

a11y5

上述操作全是在键盘下完成,看着平平无奇,实际上组件库在正常响应可获焦元素切换的同时,给用 div 模拟的 select 加了很多键盘事件的响应,可以利用回车,上下键等对可选项进行选择。其实是下了很多功夫。

对于 A11Y 相关的内容,篇幅及内容非常之多,本文无法一一展开,感兴趣的可以通读下下列文章:

总结一下

本文从页面展示交互细节可访问性三个大方面入手,罗列一些在实际的开发过程中,积攒的一些有益的经验。虽然不够全面,不过从一开始也就没想着大而全,主要是一些可能有用但是容易被忽视的点,也算是一个不错的查缺补漏小指南。

当然,很多都是我个人的观点想法,可能有一些理解存在一些问题,一些概念没有解读到位,也希望大家帮忙指出。

最后

本文到此结束,希望对你有帮助 :)

想 Get 到最有意思的 CSS 资讯,千万不要错过我的公众号 -- iCSS前端趣闻 😄

gzh_small.png

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

查看原文

赞 80 收藏 53 评论 7

袁钰涵 赞了文章 · 3月5日

K8s微服务自动化部署容器(Rancher流水线)

一、背景

最近公司上线办公网零信任安全网关系统,由我负责部署上线,在部署的时候同时也在想如何保障稳定性,以及后续部署的简便性;

想起了k8s微服务的成熟方案,不仅可以自动重启还可以监控容器运行状态,也可以集成自动化部署,于是找了一些资料将之前接触过的rancher用了起来,首先要做的就是简化安装方式,下面是我的一些过程,同时也可以给大家提供参考。

二、操作步骤

  1. 让Rancher能访问GitLab
  2. 在流水线添加项目
  3. 在仓库添加必备文件
  4. CICD自动部署调试

三、gitlab添加oauth授权

在进入集群的命名空间中,可以在菜单栏点击工具-流水线,然后就可以看到如下图所示的界面

接下来打开gitlab,然后打开设置页面http://xx.xx.xx.xx/admin/applications/4,如下图所示

在上图中将所需信息填写进去,然后点击保存

保存之后,gitlab会生成Application IdSecret,我们将它复制出来,

复制出来之后,切回rancher系统中,将其一一填写进来,如下图所示

点击完成后,会有一个弹窗进行授权,授权完成后rancher就可以访问到gitlab仓库了。

四、在rancher中添加代码仓库

在确保rancher可以访问gitlab仓库之后,在rancher菜单栏点击工具-流水线,将需要自动化部署的项目启用并保存,如下图所示

保存之后,回到CICD列表中,可以看到两个已经启用的项目,如下图所示

五、添加部署必备个文件

接下来就可以开始在代码中启用CICD自动化部署了,需要在项目根目录添加三个文件,分别是:

  1. .rancher-pipeline.yml
  2. Dockerfile
  3. deployment.yaml

5.1 设置发布流程

自动部署首先需要确定部署流程,主要用到文件.rancher-pipeline.yml,这里我是golang 的项目,使用了三个流程。

首先编译项目;接着构建镜像推送到rancher的镜像仓库中,最后使用容器编排文件发布项目,配置代码核心关注点如下图红色区域所示

stages:
- name: Build
  steps:
  - runScriptConfig:
      image: golang:1.16
      shellScript: |-
        go env -w GO111MODULE=on && go env -w GOPROXY=https://goproxy.cn,direct
        go mod tidy
        pwd
        go build -o ./bin/funfecenter
- name: Publish
  steps:
  - publishImageConfig:
      dockerfilePath: ./Dockerfile
      buildContext: .
      tag: funfecenter:${CICD_EXECUTION_SEQUENCE}
- name: Deploy
  steps:
  - applyYamlConfig:
      path: ./deployment.yaml
timeout: 60
notification: {}

5.2 构建镜像

在上一步中已经将项目编译好,接着就需要将编译好的可执行文件放入到镜像中,这里起作用的主要是Dockerfile文件,配置代码比较简单,如下所示

FROM golang:1.16
EXPOSE 1333
COPY ./bin/funfecenter /data/funfecenter/center
COPY ./init/ /
COPY script.py /root/
RUN  apt update -y
RUN  apt install -y python3
#CMD ["python3","/root/script.py"]
CMD ["/data/funfecenter/center"]

5.3 容器编排

上一步已经将需要运行的镜像推送到rancher的镜像仓库之后,接下来就需要构建pod来运行容器,这里发挥作用的主要是deployment.yaml 文件。

这个文件如果没有接触过k8s的同学可能会比较陌生,这里我将每一行都写了注释,并将需要修改的地方用红色标记圈出来了,如下图所示

参考配置如下所示

kind: Service      # 指定创建资源的角色/类型
apiVersion: v1     # 指定api版本,此值必须在kubectl api-versions中
metadata:          # 资源的元数据/属性
  name: funfe-center    # 资源的名字,在同一个namespace中必须唯一
spec:               # 资源规范字段
  selector:         # 选择器
    app: center
  type: NodePort    # 端口类型
  ports:
    - protocol: TCP     # 协议
      port: 80          # service 端口
      targetPort: 80    # 容器暴露的端口
---
apiVersion: apps/v1   # 指定api版本,此值必须在kubectl api-versions中
kind: Deployment      # 指定创建资源的角色/类型
metadata:             # 资源的元数据/属性
  name: funfe-center    # 资源的名字,在同一个namespace中必须唯一
  namespace: default    # 资源的名字,在同一个namespace中必须唯一
spec:                 # 资源规范字段
  replicas: 1         # 声明副本数目
  selector: # 选择器
    matchLabels: # 匹配标签
      app: center
  template:           # 模版
    metadata: # 资源的元数据/属性
      labels: # 设定资源的标签
        app: center
    spec:             # 资源规范字段
      imagePullSecrets:                    # 镜像仓库拉取密钥
        - name: pipeline-docker-registry
      containers:
        - name: funfe                  # 容器的名字
          image: ${CICD_IMAGE}:${CICD_EXECUTION_SEQUENCE}   # 容器使用的镜像地址
          ports:
            - containerPort: 80               # 容器开发对外的端口
            

六、修改代码自动部署

修改代码后会自动执行编译、推送到镜像库、拉取代码部署

现在我修改代码仓库的代码,回到rancher流水线中,就看到有一个任务在执行自动部署流程,如下图所示

执行完成周,回到集群的工作负载当中,就可以看到已经有一个服务自动化部署到K8s中


日期:2021-03-04

作者:汤青松

微信:songboy8888

查看原文

赞 8 收藏 7 评论 2

袁钰涵 赞了文章 · 3月5日

前端常用插件utils汇总

工具库 || 数据处理

underscore - JavaScript的实用程序带库

lodash - 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

表单验证

async-validator – 验证表单异步

---jquery

jquery-validation - 您现有的表单提供了插入式验证,同时使各种自定义适合您的应用程序变得非常容易

图片懒加载

---JavaScript

lazyload - 用于延迟加载图像

lazyload - LazyLoad是一种快速,轻巧和灵活的脚本,通过仅在内容图像,视频和iframe进入视口时加载它们来加快Web应用程序的速度

---vue

vue-lazyload - 一个Vue.js插件,用于将图像或组件延迟加载到应用程序中。

---react

react-lazyload - 延迟加载组件,图像或任何与性能有关的内容

图片预览

类似朋友圈

PhotoSwipe- 适用于移动和pc,模块化,框架独立的JavaScript图像库

满足聊天递增图片的需求

viewerjs - JavaScript图像查看器

fancybox - jQuery lightbox脚本,用于显示图像,视频等。触摸启用,响应迅速且可完全自定义

---vue

vue-picture-preview - 移动端、PC 端 Vue.js 图片预览插件

文件上传

---JavaScript

DropzoneJS - 提供带有图像预览的拖放文件上传

Web Uploader - 以HTML5为主的现代文件上传组件(百度)

jQuery-File-Upload - 具有多个文件选择,拖放支持,进度条,验证和预览图像,jQuery的音频和视频。

---vue

vue-dropzone - Dropzone.js的Vue.js组件-带有图像预览的拖放文件上传实用程序

单选框/复选框相关

---jquery

iCheck – 增强复选框和单选按钮

选择框

Choices - 轻量级库,用于制作高度可自定义的选择框、文本区域和其他输入表单。类似于select2和selectize,但没有依赖jquery。

Chosen - 一个使较长的笨拙的选择框更友好的库。

Select2 - 基于jQuery的选择框的替代品。它支持搜索,远程数据集和结果的无限滚动。

bootstrap-select - jQuery插件通过直观的多选,搜索等功能将选择元素带入21世纪

tree树形

---jquery

Bootstrap Tree View - Tree View for Twitter Bootstrap

zTree - 一个依靠 jQuery 实现的多功能 “树插件”

无限滚动

Infinite Scroll - 自动添加下一页

---vue

vue-infinite-scroll - vue.js的无限滚动指令

列表拖拽

Sortable - 是一个JavaScript库,用于在现代浏览器和触摸设备上对拖放列表进行重新排序

MDN拖拽文档

---vue

Vue.Draggable - 基于Sortable.js的Vue拖放组件

---react

react-sortablejs - 在成熟的拖放库Sortable之上构建的React组件

元素拖曳

draggabilly - 使该shiz可拖动

自定义滚动条

jScrollPane-跨浏览器自定义滚动条

perfect-scrollbar - 简约但完美的自定义滚动条插件

进度条

nprogress - 对于细长的进度条,例如YouTube,Medium等上的进度条

---github类似页面进度条

vue-progressbar - vue的轻量级进度条

cookie管理

js-cookie - 一个简单,轻巧的JavaScript API,用于处理浏览器cookie

WebSocket

ws - 简单易用,为Node.js开创了经过快速且经过全面测试的WebSocket客户端和服务器

socket.io - 实时应用程序框架

---vue

Vue-Socket.io - Vuejs和Vuex的Socket.io实现

JavaScript 动画效果库

Vue官网推荐

Velocity - 是一个简单易用、高性能、功能丰富的轻量级JS动画库

Animate.css - 一款强大的预设css3动画库

tween.js - JavaScript补间引擎可简化动画

额外效果

transformjs - 腾讯使CSS3转换超级容易

scenejs

midnight.js - 文字颜色随着背景变

vue-countTo - 在指定的持续时间内计入目标数量

CSS shake - 抖动动画

轮播效果

swiper5 - 纯javascript打造的滑动特效插件,面向手机、平板电脑等移动终端

iSlider - 适用于Mobile WebApp,HTML5 App,Hybrid App的平滑移动触摸滑块

---vue

vue-awesome-swiper - 基于Swiper4,适用于Vue的轮播组件

瀑布流展示

scrollreveal - 在元素滚动到视图时对其进行动画处理

scrollreveal 的演示地址

pc桌面级通知

push.js - 世界上最通用的桌面通知框架

响应式媒体查询

react中antd框架grid组件使用

enquire.js - JavaScript中的媒体查询

操作引导js工具

Intro.js - 为您的网站和项目提供新功能介绍和逐步用户指南的更好方法

driver.js —— 前端操作引导js工具

编辑器工具

editor.js - 具有干净JSON输出的块样式编辑器

UEditor - 由百度web前端研发部开发所见即所得富文本web编辑器

Markdown编辑器

SimpleMDE-Markdown - 一个简单,美观,可嵌入的JavaScript Markdown编辑器

simditor - 方便快捷的所见即所得编辑器

---想掘金一样的markdown解析器(双栏)

marked – markdown解析器

视频

flv.js - 用纯JavaScript编写的HTML5 Flash Video(FLV)播放器(bilibili)

video.js - Video.js是专为HTML5世界打造的网络视频播放器。它支持HTML5和Flash视频

html5media - 简单的h5player,轻量级

jwplayer - 被大量网站使用

日期选择 || 日历

Pikaday - 令人耳目一新的JavaScript Datepicker —轻巧,没有依赖关系,模块化CSS

bootstrap-datepicker - Twitter引导程序(@twbs)的 日期选择器

时间选择

有vue的版本,也支持日期选择

Flatpickr - 轻巧,功能强大的javascript datetimepicker,无依赖项

时间处理

Moment.js - 在JavaScript中解析,验证,操作和显示日期和时间。

模拟数据

Mock - 模拟数据生成器

代码注释

JSDoc - 是JavaScript的API文档生成器

触摸方面 || 手势

PhyTouch - 腾讯丝般顺滑的触摸运动方案

hammer.js - 一个用于多点触摸手势的javascript库

better-scroll - 受iscroll的启发,它支持更多功能并具有更好的滚动性能

loading加载动画

loaders.css - 令人愉悦且注重性能的纯CSS加载动画

css-spinners - 使用CSS和最少的HTML标记制作的简单CSS旋转器和控件

progressbar.js - 反应灵敏的进度条

通知组件

notie - 干净,简单的javascript通知,输入和选择套件,没有依赖项

全屏滚动

fullPage.js - 轻松创建全屏滚动网站

pagePiling.js - Alvaro Trigo的pagePiling插件。创建滚动显示的部分

fullPage.js - 专注于移动端

---vue

Vue-fullpage.js - fullPage.js的官方Vue.js包装器

模板引擎

sodajs - 腾讯重量轻但功能强大的JavaScript模板引擎

list || table组件

vue-easytable - vue table 组件,支持 单元格合并、单元格编辑、多表头固定、多列固定、列拖动、排序、自定义列、条件过滤、分页

list.js - 完美的库,可为表,列表和各种HTML元素添加搜索,排序,过滤器和灵活性

浏览器属性

bowser - 浏览器属性,例如名称,版本,渲染引擎

excel处理

sheetjs - 电子表格数据工具包

exceljs - 读取,操作并将电子表格数据和样式写入XLSX和JSON。

网格布局库

Masonry - 级联网格布局库

FFmpeg(c模块 视频帧预览)

ffmpeg

WebAssembly | MDN

地图

Leaflet - 适用于移动设备的交互式地图的JavaScript库

表情包扩展

twemoji - 一个简单的库,可在所有平台上提供标准Unicode 表情符号支持。

浏览器测试

cypress - 对浏览器中运行的所有内容进行快速,轻松和可靠的测试

浏览器代码编辑器

[]monaco-editor - 基于浏览器的代码编辑器](https://github.com/microsoft/...

滑动拼图验证

vue-puzzle-verification
vue-puzzle-vcode

值得学习库

weui

额外推荐

turn.js - 做一本书,带漂亮的翻页的效果

查看原文

赞 56 收藏 49 评论 0

袁钰涵 发布了文章 · 3月3日

NSA 5G 单模手机无法接收 5G信号?原因是基站进行了升级

据澎湃新闻报道,有四川资阳手机用户反映小米 9 Pro 5G 手机在启用 5G 网络情况下经常接收不到 5G 信号,目前只能被迫使用 4G。

对电信公司进行反馈后,得到的回复是原 NSA 模式的 5G 基站已升级为 SA 模式的 5G 基站,用户所购是单模 NSA 5G 模式手机,无法兼容升级后的 SA 5G 模式。

虽许多城市基站仍是 NSA,但最终基站将升级为 SA 模式是毋庸置疑的,NSA 与 SA 在基站升级后无法兼容,手机使用过程中注重 5G 体验的用户需要在购买时看清楚手机相关信息,以免出现无法使用手机 5G 模式的事件。

这里建议选择双模模式手机,这样无论所处城市基站是是 NSA 还是 SA 模式都不影响接收 5G 信号。

NSA 与 SA 有什么区别

NSA 与 SA 早在 2019 年被网友以“真假”5G 进行讨论,当时 SA 成为中国电信率先规模商用的 5G 服务,又有用户发现 NSA 5G 不够稳定,容易掉落至 4G,于是引发讨论。

NSA 是 Non-Standalone 的缩写,称为非独立组网,SA 是 Standalone 缩写,两者不分真假,前者是同一个核心网组网架构同时承载 4G 与 5G,后者则是用两个核心网组网架构分别承载 4G 与 5G。

2019 年 5G 走入千家万户之时,基站技术研发仍然还在发展中,核心网的组网架构可做到同时承载 4G 与 5G,这是最快让用户用上 5G 的方式,由于 NSA 基站的普及性,NSA 5G 模式是大部分手机的配置,但 NSA 的缺点也显示了出来,用户接收的 5G 信号对比 SA 模式而言不够稳定。

SA 模式需要单独的核心网组网架构,虽连接稳定,使用这一模式,基站建设的资金将会大幅度提高,考虑到这一原因,许多人把 NSA 看作是走向 SA 的过渡,认为在未来几年仍然会是 NSA 的 5G 模式占多数。

(当时文章对 5G 基站建设的预测)

SA 的覆盖速度超乎想象

预测是一回事,实际上各个城市实现 SA 覆盖的速度远比人们预料的要快,2020 年 8 月深圳便完成了 SA 的全覆盖,北京与上海也在逐步赶上,根据四川资阳手机用户 NSA 单模手机无法接收 5G 信号可见四川的基站建设也在进行中。

(深圳 SA 全覆盖)

(北京基站建设也在赶上)

(上海向 SA 建设更迈一步)

各个城市的建设逐步落实,此时距离中国 5G 手机商用不过两年,在 SA 基站的建设上一如既往地展现着中国速度。

这时问题又出来了,如果选择购买 SA 5G 模式的手机,用户所在城市的建设速度较慢,岂不是在很长的一段时间中还是无法享受 5G 带来的便利。

所以为了避免踩坑,并且让自己的手机在各处皆可连上 5G,购买手机时选择双模 5G 是最保险的做法,双模 5G 能接收来自 NSA 的信号,也可接收 SA 的信号,目前也越来越多的手机推出双模模式,在手机介绍详情列表中便可看到相关信息。

(手机 5G 参数介绍,底下是双模 5G)

同时,在 SA 的建设下,部分 NSA 单模 5G 手机将会出现降价售卖模式,如果用户本身对 5G 没有追求,可以趁此机会进行手机购买,各人有各人的喜欢,根据自身需求进行购买即可。

相关资料来源:

https://www.donews.com/news/detail/1/3138699

https://baijiahao.baidu.com/s?id=1670259566087369469

查看原文

赞 0 收藏 0 评论 0

袁钰涵 赞了文章 · 3月3日

我最喜欢的 12 个VSCode 插件!

原者:Katherine Peterson
译者:前端小智
来源:dev
点赞再看,微信搜索大迁世界,B站关注前端小智这个没有大厂背景,但有着一股向上积极心态人。本文 GitHubhttps://github.com/qq44924588... 上已经收录,文章的已分类,也整理了很多我的文档,和教程资料。

VSCode 之所以是如此出色的代码编辑器,其原因之一是由社区创建的庞大的插件库,从而提高了开发人员的工作效率。 以下是一些我最喜欢的VSCode 插件。

1. Rainbow Brackets

地址:https://marketplace.visualstu...

image.png

这个插件让我们的括号变成五颜六色,这样很容易就能找到匹配的对。

2. Auto Rename Tag

地址:https://marketplace.visualstu...

image

重命名一个HTML / XML标签时,自动重命名配对的HTML / XML标签。

3. Relative Path

地址:https://marketplace.visualstu...

image

此插件节省了我很多时间来编写导入语句。 使用简单的键盘快捷键即可轻松获取工作区中任何文件的相对路径。

4. Prettier

地址:https://marketplace.visualstu...

image

和esLint不同在于,ESLint只是一个代码质量工具(确保没有未使用的变量、没有全局变量,等等)。而 Prettier 只关心格式化文件(最大长度、混合标签和空格、引用样式等)。可见,代码格式统一的问题,交给 Prettirer 再合适不过了。和 Eslint 配合使用,风味更佳。

5. htmltagwrap

地址:https://marketplace.visualstu...

image

可以在选中HTML标签中外面套一层标签。

6. Markdown Preview Enhanced

地址:ttps://marketplace.visualstudio.com/items?itemName=shd101wyy.markdown-preview-enhanced

image.png

如果你写过markdown文件,有一个实时预览是非常有用的。

7. Polacode

地址:https://marketplace.visualstu...

image.png

这个插件可以将你的代码保存成图片分享给别人!

8. Random Everything

地址:https://marketplace.visualstu...

image

这个插件可以根据数据类型自动生成随机数据,特别适合生成测试数据。

9. CSS Peek

地址:https://marketplace.visualstu...

image

CSS Peek插件扩展了HTML和ejs代码编辑功能,支持在源代码中的字符串中找到css/scss/less(类和id)。这在很大程度上是受方括号中称为CSS内联编辑器的类似功能的启发。

10. Turbo Console Log

地址:https://marketplace.visualstu...

101010.gif

快捷添加 console.log,一键 注释 / 启用 / 删除 所有 console.log

简单说下这个插件要用到的快捷键:

ctrl + alt + l 选中变量之后,使用这个快捷键生成 console.log
alt + shift + c 注释所有 console.log
alt + shift + u 启用所有 console.log
alt + shift + d 删除所有 console.log

11. Simple React Snippets

地址:https://marketplace.visualstu...

上传中...

快速生成 React 模板片段~

12. Snippet Creator

地址:ttps://marketplace.visualstudio.com/items?itemName=ryanolsonx.snippet-creator

image

有许多代码段扩展,如上面的React,但有时我们可能想要制作自己的自定义代码段,这个插件可以让你轻松做到这一点。

~完,我是小智,我要去刷碗了~


原文:https://dev.to/katherinecodes...

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

交流

文章每周持续更新,可以微信搜索【大迁世界 】第一时间阅读,回复【福利】有多份前端视频等着你,本文 GitHub https://github.com/qq449245884/xiaozhi 已经收录,欢迎Star。

查看原文

赞 21 收藏 14 评论 0

袁钰涵 赞了文章 · 3月1日

前后端接口鉴权全解 Cookie/Session/Token 的区别

原文链接:前后端接口鉴权全解

不知不觉也写得比较长了,一次看不完建议收藏夹!本文主要解释与请求状态相关的术语(cookie、session、token)和几种常见登录的实现方式,希望大家看完本文后可以有比较清晰的理解,有感到迷惑的地方请在评论区提出。

Cookie

众所周知,http 是无状态协议,浏览器和服务器不可能凭协议的实现辨别请求的上下文。

于是 cookie 登场,既然协议本身不能分辨链接,那就在请求头部手动带着上下文信息吧。

举个例子,以前去旅游的时候,到了景区可能会需要存放行李,被大包小包压着,旅游也不开心啦。在存放行李后,服务员会给你一个牌子,上面写着你的行李放在哪个格子,离开时,你就能凭这个牌子和上面的数字成功取回行李。

cookie 做的正是这么一件事,旅客就像客户端,寄存处就像服务器,凭着写着数字的牌子,寄存处(服务器)就能分辨出不同旅客(客户端)。

你会不会想到,如果牌子被偷了怎么办,cookie 也会被偷吗?确实会,这就是一个很常被提到的网络安全问题——CSRF。可以在这篇文章了解关于 CSRF 的成因和应对方法。

cookie 诞生初似乎是用于电商存放用户购物车一类的数据,但现在前端拥有两个 storage(local、session),两种数据库(websql、IndexedDB),根本不愁信息存放问题,所以现在基本上 100% 都是在连接上证明客户端的身份。例如登录之后,服务器给你一个标志,就存在 cookie 里,之后再连接时,都会自动带上 cookie,服务器便分清谁是谁。另外,cookie 还可以用于跟踪一个用户,这就产生了隐私问题,于是也就有了“禁用 cookie”这个选项(然而现在这个时代禁用 cookie 是挺麻烦的事情)。

设置方式

现实世界的例子明白了,在计算机中怎么才能设置 cookie 呢?一般来说,安全起见,cookie 都是依靠 set-cookie 头设置,且不允许 JavaScript 设置。

Set-Cookie: <cookie-name>=<cookie-value>
Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>
Set-Cookie: <cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>
Set-Cookie: <cookie-name>=<cookie-value>; Path=<path-value>
Set-Cookie: <cookie-name>=<cookie-value>; Secure
Set-Cookie: <cookie-name>=<cookie-value>; HttpOnly

Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Strict
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=Lax
Set-Cookie: <cookie-name>=<cookie-value>; SameSite=None; Secure

// Multiple attributes are also possible, for example:
Set-Cookie: <cookie-name>=<cookie-value>; Domain=<domain-value>; Secure; HttpOnly

其中 <cookie-name>=<cookie-value> 这样的 kv 对,内容随你定,另外还有 HttpOnly、SameSite 等配置,一条 Set-Cookie 只配置一项 cookie。

  • Expires 设置 cookie 的过期时间(时间戳),这个时间是客户端时间
  • Max-Age 设置 cookie 的保留时长(秒数),同时存在 Expires 和 Max-Age 的话,Max-Age 优先
  • Domain 设置生效的域名,默认就是当前域名,不包含子域名
  • Path 设置生效路径,/ 全匹配
  • Secure 设置 cookie 只在 https 下发送,防止中间人攻击
  • HttpOnly 设置禁止 JavaScript 访问 cookie,防止XSS
  • SameSite 设置跨域时不携带 cookie,防止CSRF

Secure 和 HttpOnly 是强烈建议开启的。SameSite 选项需要根据实际情况讨论,因为 SameSite 可能会导致即使你用 CORS 解决了跨越问题,依然会因为请求没自带 cookie 引起一系列问题,一开始还以为是 axios 配置问题,绕了一大圈,然而根本没关系。

其实因为 Chrome 在某一次更新后把没设置 SameSite 默认为 Lax,你不在服务器手动把 SameSite 设置为 None 就不会自动带 cookie 了。

发送方式

参考 MDN,cookie 的发送格式如下(其中 PHPSESSID 相关内容下面会提到):

Cookie: <cookie-list>
Cookie: name=value
Cookie: name=value; name2=value2; name3=value3

Cookie: PHPSESSID=298zf09hf012fh2; csrftoken=u32t4o3tb3gg43; _gat=1

在发送 cookie 时,并不会传上面提到的配置到服务器,因为服务器在设置后就不需要关心这些信息了,只要现代浏览器运作正常,收到的 cookie 就是没问题的。

Session

从 cookie 说到 session,是因为 session 才是真正的“信息”,如上面提到的,cookie 是容器,里面装着 PHPSESSID=298zf09hf012fh2;,这就是一个 session ID。

不知道 session 和 session id 会不会让你看得有点头晕?

当初 session 的存在就是要为客户端和服务器连接提供的信息,所以我将 session 理解为信息,而 session id 是获取信息的钥匙,通常是一串唯一的哈希码。

接下来分析两个 node.js express 的中间件,理解两种 session 的实现方式。

session 信息可以储存在客户端,如 cookie-session,也可以储存在服务器,如 express-session。使用 session ID 就是把 session 放在服务器里,用 cookie 里的 id 寻找服务器的信息。

客户端储存

对于 cookie-session 库,比较容易理解,其实就是把所有信息加密后塞到 cookie 里。其中涉及到 cookies 库。在设置 session 时其实就是调用 cookies.set,把信息写到 set-cookie 里,再返回浏览器。换言之,取值和赋值的本质都是操作 cookie

浏览器在接收到 set-cookie 头后,会把信息写到 cookie 里。在下次发送请求时,信息又通过 cookie 原样带回来,所以服务器什么东西都不用存,只负责获取和处理 cookie 里的信息,这种实现方法不需要 session ID。

这是一段使用 cookie-session 中间件为请求添加 cookie 的代码:

const express = require('express')
var cookieSession = require('cookie-session')
const app = express()
app.use(
  cookieSession({
    name: 'session',
    keys: [
      /* secret keys */
      'key',
    ],
    // Cookie Options
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  })
)
app.get('/', function(req, res) {
  req.session.test = 'hey'
  res.json({
    wow: 'crazy',
  })
})

app.listen(3001)

在通过 app.use(cookieSession()) 使用中间件之前,请求是不会设置 cookie 的,添加后再访问(并且在设置 req.session 后,若不添加 session 信息就没必要写、也没内容写到 cookie 里),就能看到服务器响应头部新增了下面两行,分别写入 session 和 session.sig:

Set-Cookie: session=eyJ0ZXN0IjoiaGV5In0=; path=/; expires=Tue, 23 Feb 2021 01:07:05 GMT; httponly
Set-Cookie: session.sig=QBoXofGvnXbVoA8dDmfD-GMMM6E; path=/; expires=Tue, 23 Feb 2021 01:07:05 GMT; httponly

然后你就能在 DevTools 的 Application 标签看到 cookie 成功写入。session 的值 eyJ0ZXN0IjoiaGV5In0= 通过 base64 解码(不了解 base64 的话可以看这里)即可得到 {"test":"hey"},这就是所谓的“将 session 信息放到客户端”,因为 base64 编码并不是加密,这就跟明文传输没啥区别,所以请不要在客户端 session 里放用户密码之类的机密信息

即使现代浏览器和服务器做了一些约定,例如使用 https、跨域限制、还有上面提到 cookie 的 httponly 和 sameSite 配置等,保障了 cookie 安全。但是想想,传输安全保障了,如果有人偷看你电脑里的 cookie,密码又恰好存在 cookie,那就能无声无息地偷走密码。相反的,只放其他信息或是仅仅证明“已登录”标志的话,只要退出一次,这个 cookie 就失效了,算是降低了潜在危险。

说回第二个值 session.sig,它是一个 27 字节的 SHA1 签名,用以校验 session 是否被篡改,是 cookie 安全的又一层保障。

服务器储存

既然要储存在服务器,那么 express-session 就需要一个容器 store,它可以是内存、redis、mongoDB 等等等等,内存应该是最快的,但是重启程序就没了,redis 可以作为备选,用数据库存 session 的场景感觉不多。

express-session 的源码没 cookie-session 那么简明易懂,里面有一个有点绕的问题,req.session 到底是怎么插入的?

不关注实现可以跳过这段,有兴趣的话可以跟着思路看看 express-session 的源码。

我们可以从 .session = 这个关键词开始找,找到:

  • store.generate 否决这个,容易看出这个是初始化使用的
  • Store.prototype.createSession 这个是根据 req 和 sess 参数在 req 中设置 session 属性,没错,就是你了

于是全局搜索 createSession,锁定 index 里的 inflate (就是填充的意思)函数。

最后寻找 inflate 的调用点,是使用 sessionID 为参数的 store.get 的回调函数,一切说得通啦——

在监测到客户端送来的 cookie 之后,可以从 cookie 获取 sessionID,再使用 id 在 store 中获取 session 信息,挂到 req.session,经过这个中间件,你就能顺利地使用 req 中的 session。

那赋值怎么办呢?这就和上面储存在客户端不同了,上面要修改客户端 cookie 信息,但是对于储存在服务器的情况,你修改了 session 那就是“实实在在地修改”了嘛,不用其他花里胡哨的方法,内存中的信息就是修改了,下次获取内存里的对应信息也是修改后的信息。(仅限于内存的实现方式,使用数据库时仍需要额外的写入)

在请求没有 session id 的情况下,通过 store.generate 创建新的 session,在你写 session 的时候,cookie 可以不改变,只要根据原来的 cookie 访问内存里的 session 信息就可以了。

var express = require('express')
var parseurl = require('parseurl')
var session = require('express-session')

var app = express()

app.use(
  session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true,
  })
)

app.use(function(req, res, next) {
  if (!req.session.views) {
    req.session.views = {}
  }

  // get the url pathname
  var pathname = parseurl(req).pathname

  // count the views
  req.session.views[pathname] = (req.session.views[pathname] || 0) + 1

  next()
})

app.get('/foo', function(req, res, next) {
  res.json({
    session: req.session,
  })
})

app.get('/bar', function(req, res, next) {
  res.send('you viewed this page ' + req.session.views['/bar'] + ' times')
})

app.listen(3001)

两种储存方式的对比

首先还是计算机世界最重要的哲学问题:时间和空间的抉择。

储存在客户端的情况,解放了服务器存放 session 的内存,但是每次都带上一堆 base64 处理的 session 信息,如果量大的话传输就会很缓慢。

储存在服务器相反,用服务器的内存拯救了带宽。

另外,在退出登录的实现和结果,也是有区别的。

储存在服务器的情况就很简单,如果 req.session.isLogin = true 是登录,那么 req.session.isLogin = false 就是退出。

但是状态存放在客户端要做到真正的“即时退出登录”就很困难了。你可以在 session 信息里加上过期日期,也可以直接依靠 cookie 的过期日期,过期之后,就当是退出了。

但是如果你不想等到 session 过期,现在就想退出登录!怎么办?认真想想你会发现,仅仅依靠客户端储存的 session 信息真的没有办法做到。

即使你通过 req.session = null 删掉客户端 cookie,那也只是删掉了,但是如果有人曾经把 cookie 复制出来了,那他手上的 cookie 直到 session 信息里的过期时间前,都是有效的。

说“即时退出登录”有点标题党的意味,其实我想表达的是,你没办法立即废除一个 session,这可能会造成一些隐患。

Token

session 说完了,那么出现频率超高的关键字 token 又是什么?

不妨谷歌搜一下 token 这个词,可以看到冒出来几个(年纪大的人)比较熟悉的图片:密码器。过去网上银行不是只要短信认证就能转账,还要经过一个密码器,上面显示着一个变动的密码,在转账时你需要输入密码器中的代码才能转账,这就是 token 现实世界中的例子。凭借一串码或是一个数字证明自己身份,这事情不就和上面提到的行李问题还是一样的吗……

其实本质上 token 的功能就是和 session id 一模一样。你把 session id 说成 session token 也没什么问题(Wikipedia 里就写了这个别名)。

其中的区别在于,session id 一般存在 cookie 里,自动带上;token 一般是要你主动放在请求中,例如设置请求头的 Authorizationbearer:<access_token>

然而上面说的都是一般情况,根本没有明确规定!

剧透一下,下面要讲的 JWT(JSON Web Token)!他是一个 token!但是里面放着 session 信息!放在客户端,并且可以随你选择放在 cookie 或是手动添加在 Authorization!但是他就叫 token!

个人觉得你不能通过存放的位置判断是 token 或是 session id,也不能通过内容判断是 token 或是 session 信息,session、session id 以及 token 都是很意识流的东西,只要你明白他是什么、怎么用就好了,怎么称呼不太重要。

另外在搜索资料时也看到有些文章说 session 和 token 的区别就是新旧技术的区别,好像有点道理。

在 session 的 Wikipedia 页面上 HTTP session token 这一栏,举例都是 JSESSIONID (JSP)、PHPSESSID (PHP)、CGISESSID (CGI)、ASPSESSIONID (ASP) 等比较传统的技术,就像 SESSIONID 是他们的代名词一般;而在研究现在各种平台的 API 接口和 OAuth2.0 登录时,都是使用 access token 这样的字眼,这个区别着实有点意思。

理解 session 和 token 的联系之后,可以在哪里能看到“活的” token 呢?

打开 GitHub 进入设置,找到 Settings / Developer settings,可以看到 Personal access tokens 选项,生成新的 token 后,你就可以带着它通过 GitHub API,证明“你就是你”。

在 OAuth 系统中也使用了 Access token 这个关键词,写过微信登录的朋友应该都能感受到 token 是个什么啦。

Token 在权限证明上真的很重要,不可泄漏,谁拿到 token,谁就是“主人”。所以要做一个 Token 系统,刷新或删除 Token 是必须要的,这样在尽快弥补 token 泄漏的问题。

在理解了三个关键字和两种储存方式之后,下面我们正式开始说“用户登录”相关的知识和两种登录规范——JWT 和 OAuth2.0。

接着你可能会频繁见到 Authentication 和 Authorization 这两个单词,它们都是 Auth 开头,但可不是一个意思,简单来说前者是验证,后者是授权。在编写登录系统时,要先验证用户身份,设置登录状态,给用户发送 token 就是授权

JWT

全称 JSON Web Token(RFC 7519),是的,JWT 就是一个 token。为了方便理解,提前告诉大家,JWT 用的是上面客户端储存的方式,所以这部分可能会经常用到上面提到的名称。

结构

虽说 JWT 就是客户端储存 session 信息的一种,但是 JWT 有着自己的结构:Header.Payload.Signature(分为三个部分,用 . 隔开)

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

typ 说明 token 类型是 JWT,alg 代表签名算法,HMAC、SHA256、RSA 等。然后将其 base64 编码。

Payload

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

Payload 是放置 session 信息的位置,最后也要将这些信息进行 base64 编码,结果就和上面客户端储存的 session 信息差不多。

不过 JWT 有一些约定好的属性,被称为 Registered claims,包括:

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

Signature

最后一部分是签名,和上面提到的 session.sig 一样是用于防止篡改,不过 JWT 把签名和内容组合到一起罢了。

JWT 签名的生成算法是这样的:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

使用 Header 里 alg 的算法和自己设定的密钥 secret 编码 base64UrlEncode(header) + "." + base64UrlEncode(payload)

最后将三部分通过 . 组合在一起,你可以通过 jwt.io Debugger 形象地看到 JWT 的组成原理:

如何使用

在验证用户,顺利登录后,会给用户返回 JWT。因为 JWT 的信息没有加密,所以别往里面放密码,详细原因在客户端储存的 cookie 中提到。

用户访问需要授权的连接时,可以把 token 放在 cookie,也可以在请求头带上 Authorization: Bearer <token>。(手动放在请求头不受 CORS 限制,不怕 CSRF)

这样可以用于自家登录,也可以用于第三方登录。单点登录也是 JWT 的常用领域。

JWT 也因为信息储存在客户端造成无法让自己失效的问题,这算是 JWT 的一个缺点。

HTTP authentication

HTTP authentication 是一种标准化的校验方式,不会使用 cookie 和 session 相关技术。请求头带有 Authorization: Basic <credentials> 格式的授权字段。

其中 credentials 就是 Base64 编码的用户名 + : + 密码(或 token),以后看到 Basic authentication,意识到就是每次请求都带上用户名密码就好了。

Basic authentication 大概比较适合 serverless,毕竟他没有运行着的内存,无法记录 session,直接每次都带上验证就完事了。

OAuth 2.0

OAuth 2.0(RFC 6749)也是用 token 授权的一种协议,它的特点是你可以在有限范围内使用别家接口,也可以借此使用别家的登录系统登录自家应用,也就是第三方应用登录。(注意啦注意啦,OAuth 2.0 授权流程说不定面试会考哦!)

既然是第三方登录,那除了应用本身,必定存在第三方登录服务器。在 OAuth 2.0 中涉及三个角色:用户、应用提供方、登录平台,相互调用关系如下:

     +--------+                               +---------------+
     |        |--(A)- Authorization Request ->|   Resource    |
     |        |                               |     Owner     |
     |        |<-(B)-- Authorization Grant ---|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(C)-- Authorization Grant -->| Authorization |
     | Client |                               |     Server    |
     |        |<-(D)----- Access Token -------|               |
     |        |                               +---------------+
     |        |
     |        |                               +---------------+
     |        |--(E)----- Access Token ------>|    Resource   |
     |        |                               |     Server    |
     |        |<-(F)--- Protected Resource ---|               |
     +--------+                               +---------------+

很多大公司都提供 OAuth 2.0 第三方登录,这里就拿小聋哥的微信举例吧——

准备

一般来说,应用提供方需要先在登录平台申请好 AppID 和 AppSecret。(微信使用这个名称,其他平台也差不多,一个 ID 和一个 Secret)

获取 code

什么是授权临时票据(code)? 答:第三方通过 code 进行获取 access_token 的时候需要用到,code 的超时时间为 10 分钟,一个 code 只能成功换取一次 access_token 即失效。code 的临时性和一次保障了微信授权登录的安全性。第三方可通过使用 https 和 state 参数,进一步加强自身授权登录的安全性。

在这一步中,用户先在登录平台进行身份校验。

https://open.weixin.qq.com/connect/qrconnect?
appid=APPID&
redirect_uri=REDIRECT_URI&
response_type=code&
scope=SCOPE&
state=STATE
#wechat_redirect
参数是否必须说明
appid应用唯一标识
redirect_uri请使用 urlEncode 对链接进行处理
response_type填 code
scope应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写 snsapi_login
state用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止 csrf 攻击(跨站请求伪造攻击)

注意一下 scope 是 OAuth2.0 权限控制的特点,定义了这个 code 换取的 token 可以用于什么接口。

正确配置参数后,打开这个页面看到的是授权页面,在用户授权成功后,登录平台会带着 code 跳转到应用提供方指定的 redirect_uri

redirect_uri?code=CODE&state=STATE

授权失败时,跳转到

redirect_uri?state=STATE

也就是失败时没 code。

获取 token

在跳转到重定向 URI 之后,应用提供方的后台需要使用微信给你的code获取 token,同时,你也可以用传回来的 state 进行来源校验。

要获取 token,传入正确参数访问这个接口:

https://api.weixin.qq.com/sns/oauth2/access_token?
appid=APPID&
secret=SECRET&
code=CODE&
grant_type=authorization_code
参数是否必须说明
appid应用唯一标识,在微信开放平台提交应用审核通过后获得
secret应用密钥 AppSecret,在微信开放平台提交应用审核通过后获得
code填写第一步获取的 code 参数
grant_type填 authorization_code,是其中一种授权模式,微信现在只支持这一种

正确的返回:

{
  "access_token": "ACCESS_TOKEN",
  "expires_in": 7200,
  "refresh_token": "REFRESH_TOKEN",
  "openid": "OPENID",
  "scope": "SCOPE",
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

得到 token 之后你就可以根据之前申请 code 填写的 scope 调用接口了。

使用 token 调用微信接口

授权作用域(scope)接口接口说明
snsapi_base/sns/oauth2/access_token通过 code 换取 access_token、refresh_token 和已授权 scope
snsapi_base/sns/oauth2/refresh_token刷新或续期 access_token 使用
snsapi_base/sns/auth检查 access_token 有效性
snsapi_userinfo/sns/userinfo获取用户个人信息

例如获取个人信息就是 GEThttps://api.weixin.qq.com/sns...

注意啦,在微信 OAuth 2.0,access_token 使用 query 传输,而不是上面提到的 Authorization。

使用 Authorization 的例子,如 GitHub 的授权,前面的步骤基本一致,在获取 token 后,这样请求接口:

curl -H "Authorization: token OAUTH-TOKEN" https://api.github.com

说回微信的 userinfo 接口,返回的数据格式如下:

{
  "openid": "OPENID",
  "nickname": "NICKNAME",
  "sex": 1,
  "province":"PROVINCE",
  "city":"CITY",
  "country":"COUNTRY",
  "headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
  "privilege":[ "PRIVILEGE1" "PRIVILEGE2" ],
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

后续使用

在使用 token 获取用户个人信息后,你可以接着用 userinfo 接口返回的 openid,结合 session 技术实现在自己服务器登录。

// 登录
req.session.id = openid
if (req.session.id) {
  //   已登录
} else {
  //   未登录
}
// 退出
req.session.id = null
// 清除 session

总结一下 OAuth2.0 的流程和重点:

  • 为你的应用申请 ID 和 Secret
  • 准备好重定向接口
  • 正确传参获取 code <- 重要
  • code 传入你的重定向接口
  • 在重定向接口中使用 code 获取 token <- 重要
  • 传入 token 使用微信接口

OAuth2.0 着重于第三方登录和权限限制。而且 OAuth2.0 不止微信使用的这一种授权方式,其他方式可以看阮老师的OAuth 2.0 的四种方式

其他方法

JWT 和 OAuth2.0 都是成体系的鉴权方法,不代表登录系统就一定要这么复杂。

简单登录系统其实就以上面两种 session 储存方式为基础就能做到。

  1. 使用服务器储存 session 为基础,可以用类似 req.session.isLogin = true 的方法标志该 session 的状态为已登录。
  2. 使用客户端储存 session 为基础,设置 session 的过期日期和登录人就基本能用了。
{
  "exp": 1614088104313,
  "usr": "admin"
}

(就是和 JWT 原理基本一样,不过没有一套体系)

  1. 甚至你可以使用上面的知识自己写一个 express 的登录系统:
  • 初始化一个 store,内存、redis、数据库都可以
  • 在用户身份验证成功后,随机生成一串哈希码作为 token
  • 用 set-cookie 写到客户端
  • 再在服务器写入登录状态,以内存为例就是在 store 中添加哈希码作为属性
  • 下次请求带着 cookie 的话检查 cookie 带来的 token 是否已经写入 store 中即可
let store = {}

// 登录成功后
store[HASH] = true
cookie.set('token', HASH)

// 需要鉴权的请求钟
const hash = cookie.get('token')
if (store[hash]) {
  // 已登录
} else {
  // 未登录
}

// 退出
const hash = cookie.get('token')
delete store[hash]

总结

以下列出本文重点:

  • cookie 是储存 session/session id/token 的容器
  • cookie 设置一般通过 set-cookie 请求头设置
  • session 信息可以存放在浏览器,也可以存放在服务器
  • session 存放在服务器时,以 session id 为钥匙获取信息
  • token/session/session id 三者的界限是模糊的
  • 一般新技术使用 token,传统技术使用 session id
  • cookie/token/session/session id 都是用于鉴权的实用技术
  • JWT 是浏览器储存 session 的一种
  • JWT 常用于单点登录(SSO)
  • OAuth2.0 的 token 不是由应用端颁发,存在另外的授权服务器
  • OAuth2.0 常用于第三方应用登录

参考

查看原文

赞 75 收藏 60 评论 2

认证与成就

  • 获得 146 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-09-14
个人主页被 26k 人浏览