林水溶

林水溶 查看完整档案

其它编辑  |  填写毕业院校  |  填写所在公司/组织 linshuirong.cn 编辑
编辑

Front End Developer

个人动态

林水溶 回答了问题 · 10月13日

android webview 中setTimeout 不生效

我这也遇到了,导致vue-router不能正常跳转。排查了好久,最后发现是客户端那边清除了了WebView的setTimeout定时器。

关注 3 回答 4

林水溶 赞了问题 · 10月13日

android webview 中setTimeout 不生效

setTimeout(()=>{

console.log("i am test")
    },3000)
   

在安卓webview中不会起作用.

问题已经解决,谢谢大家。 是安卓的那边的开发人员的问题,清除了定时器。

关注 3 回答 4

林水溶 赞了文章 · 9月21日

利用GithubActions自动备份网易云音乐每日推荐歌曲

Github最近推出了Actions功能,可以用来做很多好玩的事。

之前我写过一个脚本,可以将网易云音乐每日推荐的歌曲保存为新歌单,起到备份作用。但那个脚本需要部署在自己的服务器上边一直运行才行。

今天我突然想到可以利用Github的Actions功能,每天定时运行那个脚本进行备份,这样既不需要自己的服务器,又省去了维护。

Github项目

NeteaseCloudMusicDayActions

使用教程

  • 在自己的Github上创建个新仓库
  • 在仓库创建 /.github/workflows/day.yml
  • 将day.yml里面的phone和password里面的xxx替换成自己的网易云账号密码即可
  • 第一次创建后等待1小时,以后每小时脚本会自动运行一次进行检测,可在项目上方的Actions里查看运行记录

day.yml

name: 网易云音乐日推自动创建歌单

on:
  schedule:
    # * is a special character in YAML so you have to quote this string
    - cron:  '30 * * * *'

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - name: 更新为中国时间
      run: |
        sudo rm -rf /etc/localtime 
        sudo ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
        date
    - name: 安装网易云api
      run: git clone https://github.com/shanghaobo/NeteaseCloudMusicApi.git
    - name: 运行网易云api
      run: |
        cd NeteaseCloudMusicApi
        npm install
        nohup node app.js &
    - name: 安装并脚本
      run: git clone https://github.com/shanghaobo/NeteaseCloudMusicDay.git
    - name: 设置api
      run: echo "api='http://127.0.0.1:3000'" >> NeteaseCloudMusicDay/config.py
    - name: 设置网易云音乐登录账号
      run: echo "phone='xxxxxxxxxxx'" >> NeteaseCloudMusicDay/config.py
    - name: 设置网易云音乐登录密码
      run: echo "password='xxxxxx'" >> NeteaseCloudMusicDay/config.py
    - name: 运行脚本
      run: python3 NeteaseCloudMusicDay/main2.py

效果展示

ActionsDemo.png
demo.jpg

Github项目地址

https://github.com/shanghaobo...

查看原文

赞 1 收藏 0 评论 0

林水溶 赞了问题 · 9月21日

使用git push时出现error: src refspec master does not match any. 是什么原因

使用git push是,采用以下步骤:

git init
git add README.md
git commit -m "first commit"
git remote add origin https://github.com/focusor/focusor.github.io.git
git push -u origin master

产生如下错误:

error: src refspec master does not match any. 
error: failed to push some refs to "xxxxxxx"

然后用如下方法解决了:

git add .
git commit -m "write your meaaage"

之后push就成功了,具体原因是什么呢?

关注 4 回答 4

林水溶 收藏了文章 · 9月15日

[译] Tailwind CSS 15 分钟入门

翻译自:https://scotch.io/tutorials/get-started-with-tailwind-css-in-15-minutes

Tailwind CSS 是一个 CSS 框架,它由大量的功能类组成,而不是编写好的组件。

使用 Tailwind,后,我发现最让我欣喜的一件事:

再也不用重写 CSS 样式类了

image.png

在 Tailwind 的首页上,有一个很酷的例子,用来展示 Tailwind 如何工作。

真实案例

本文聚焦在介绍 Tailwind 的一些特性。如果你想直接看一些案例,可以在下面这些文章中找到:

一个简单的例子 - 构建一个卡片

这是一个 Bootstrap 构建的卡片组件与 Tailwind 构建的卡片组件对比。警告:这看起来有些奇怪,你可能会在看完这个例子后,失去了解 Tailwind 的兴趣。给它一些时间,多看一些练习示例,你会看到使用组合 Utils 类的强大功能,以及组合优于继承的思想。

A ### Bootstrap card

<div class="card" style="width: 18rem;">
  <div class="card-body">

        <h5 class="card-title">Card Title</h5>
        <p class="card-text">Content goes here</p>

    </div>
</div>

Bootstrap 版的 Card 很容易就被实现了,它的可定制性很差。你需要使用!important 关键字 来覆盖相关 class 才能实现。

image.png

让我们看一下 Tailwind 版本的实现:

<div class="bg-white rounded shadow border p-6 w-64">
  <h5 class="text-3xl font-bold mb-4 mt-0">My Title</h5>
  <p class="text-gray-700 text-sm">Content goes here</p>
</div>

我们使用使用组合 Utils 的方式,实现了一个卡片。这看起来比上面的版本复杂了一些,然而这个版本的最大收益是具有极强的可定制性,快速而简单

image.png

这是代码的 CodePen。下面是关于这个卡片中类的解析:

  • bg-white: background: #fff
  • rounded: border-radius: 0.25rem
  • shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)
  • border: border-width: 1px
  • p-6: padding: 1.5rem
  • w-64: width: 16rem

这是有关标题的一些 class:

  • text-3xl: font-size: 1.875rem
  • font-bold: font-weight: 700
  • mb-4: margin-bottom: .75rem
  • mt-0: margin-top: 0

这是有关内容的一些 class:

  • text-gray-700: color: #4a5568
  • text-sm: font-size: .875rem

Tailwind CSS 是一项投资。如果你想快速的编写 CSS 和设计你的 app,你要花时间去学习这些 class;与学习其他技术一样,你将从中获得收益。

会不会让 HTML 变得负担重?

可以将使用 Tailwind 看作是编写内联样式。有许多策略可以将这些类移出你的 HTML,并让其可复用。下面介绍一下清理 Tailwind 类的策略:

  • 使用 Sass 以及将我们的类移出 HTML
  • 使用 JS 组件(React/Vue),以及同样的类只写一次

下面是个例子,使用 Tailwind @apply function 清理HTML:

.btn {
  @apply font-bold py-2 px-4 rounded;
}

.btn-blue {
  @apply bg-blue-500 text-white;
}

.btn-blue:hover {
  @apply bg-blue-700;
}

我个人喜欢的一种方法是将类保存在模版文件中,然后让模板可复用。React 组件是一个很好的例子。

我们已经讨论了很多。现在,我们聚焦到如何使用 Tailwind 来构建一些东西。

什么是 Tailwind CSS?

现在让我们已经看了一个简单的例子,让我们来深入挖掘一下。 Tailwind CSS 是一个 CSS 框架,它与你之前用过的都不一样。当人们想到 CSS 框架的时候,最先想到的是使用最广泛的 Bootstrap,或者其他流行的 FoundationBulma

Tailwind 是一个特别的框架。相对于预先写好样式的组件,Tailwid 提供一大堆功能类。

下面是 Tailwind 所提供的一些种类的 class

我们有许多定义好的工具类,可以直接使用。下面是一些类的 Tailwind 文档地址。

Tailwind 的文档,是非常好的帮助我们上手的资料。当需要使用某一类型的类时,可以很快的找到。在页面中敲击 /,可以快速聚焦到 search bar 上。

Tailwind 的优点

当我们使用 Bootstrap,或者其他类似的框架时,我们有大量的预编译好现成的组件可以直接使用,如 cardnavbar 以及其他。当我们要自定义这些组件时,我们要写大量的自定义这些组件样式的 CSS 代码,以及覆盖基础样式。

这会带来带来混乱,让我们陷入编写相互覆盖样式的代码的泥潭。

Tailwind 给我们提供一个「只引用需要代码」的选择。

用 Tailwind 越多,好处越明显。让我们看一下这些好处,以及构建一点东西。

包大小

Tailwind 本身体积不小。这是因为它提供了很多工具类。

image.png

最大的好处提,可以使用 Purgecss控制文件的大小时。Purgecss 将检查我们的 HTML文件,并找出所有用到的 Tailwind 类。任何没有被用到的类,将会被从最终生成的 CSS 文件中移出。

当我们将所有未用到的类移出后,我们的 CSS 文件体积减小到 13kb!

定制容易

Tailwind 允许我们 定制一切 与类相关的内容。我们可以改变所使用的颜色,字号大小,padding距离,响应式布局,以及其他。

我们可以创建一个 tailwind.config.js,将我们的配置写入其中。这样,我们的配置将会覆盖调 Tailwind 默认的配置

// Example `tailwind.config.js` file

module.exports = {
  important: true,
  theme: {
    fontFamily: {
      display: ['Gilroy', 'sans-serif'],
      body: ['Graphik', 'sans-serif'],
    },
    extend: {
      colors: {
        cyan: '#9cdbff',
      },
      margin: {
        '96': '24rem',
        '128': '32rem',
      },
    }
  },
  variants: {
    opacity: ['responsive', 'hover']
  }
}

快速实现响应式

我们可以通过使用 Tailwind 提供的工具类,编写响应式内容。我从来不喜欢自己编写响应式断点代码。

Tailwind 中,已经定义好了一系列响应式类型,这些工具类通过前缀来进行区分:

  • sm: min-width: 640px
  • md: min-width: 768px
  • lg: min-width: 1024px
  • xl: min-width: 1280px

假设我们想要实现一个 box,在大屏幕上,背景是蓝色的,在小屏幕上,背景是蓝色的。通过这些定义好的前缀,很容易实现。

<div class="bg-red-400 lg:bg-blue-400">
    <p>i am red on small and medium devices</p>

    <p>i am blue on large and extra large devices</p>
</div>

超级快速实现原型(如果你熟悉 CSS)

我最喜爱 Tailwind 的特性是,我可以通过它,快速的将出色的设计搬到浏览器上。Tailwind 不会为你提供完美的设计。它只是为你提供快速创建的工具。我从不认为自己是设计师,也没有使用 Figma 这样的工具。我往往直接进入浏览器中,边开发,边设计。

如果你使用 Tailwind,你需要熟悉你的 CSS

使用 Tailwind,将让你熟悉你的 CSS 类是如何构建出来的,而不会将你隐藏在组件后面。如果你使用 Bootstrap 中的 card,你也许不清楚 card 里面有什么东西。当你使用 Tailwind,你会确切的知道与 CSS 有关的的细节。

我们使用 Tailwind 重新创建了一些 Web 上的东西,查看这些文章,以了解我们如何使用 Tailwind 快速构建原型。

https://scotch.io/tutorials/r...
https://scotch.io/tutorials/r...
https://scotch.io/tutorials/r...

结尾

Tailwind CSS 是看待 CSS 的一种与众不同的视角。它为你提供一个良好基础,以方便你快速创建任何类型设计。

可能需要一些时间来适应,但我认为这些努力是值得的。

你再也不用覆盖 CSS 框架中内置的样式了
查看原文

林水溶 赞了文章 · 9月15日

[译] Tailwind CSS 15 分钟入门

翻译自:https://scotch.io/tutorials/get-started-with-tailwind-css-in-15-minutes

Tailwind CSS 是一个 CSS 框架,它由大量的功能类组成,而不是编写好的组件。

使用 Tailwind,后,我发现最让我欣喜的一件事:

再也不用重写 CSS 样式类了

image.png

在 Tailwind 的首页上,有一个很酷的例子,用来展示 Tailwind 如何工作。

真实案例

本文聚焦在介绍 Tailwind 的一些特性。如果你想直接看一些案例,可以在下面这些文章中找到:

一个简单的例子 - 构建一个卡片

这是一个 Bootstrap 构建的卡片组件与 Tailwind 构建的卡片组件对比。警告:这看起来有些奇怪,你可能会在看完这个例子后,失去了解 Tailwind 的兴趣。给它一些时间,多看一些练习示例,你会看到使用组合 Utils 类的强大功能,以及组合优于继承的思想。

A ### Bootstrap card

<div class="card" style="width: 18rem;">
  <div class="card-body">

        <h5 class="card-title">Card Title</h5>
        <p class="card-text">Content goes here</p>

    </div>
</div>

Bootstrap 版的 Card 很容易就被实现了,它的可定制性很差。你需要使用!important 关键字 来覆盖相关 class 才能实现。

image.png

让我们看一下 Tailwind 版本的实现:

<div class="bg-white rounded shadow border p-6 w-64">
  <h5 class="text-3xl font-bold mb-4 mt-0">My Title</h5>
  <p class="text-gray-700 text-sm">Content goes here</p>
</div>

我们使用使用组合 Utils 的方式,实现了一个卡片。这看起来比上面的版本复杂了一些,然而这个版本的最大收益是具有极强的可定制性,快速而简单

image.png

这是代码的 CodePen。下面是关于这个卡片中类的解析:

  • bg-white: background: #fff
  • rounded: border-radius: 0.25rem
  • shadow: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)
  • border: border-width: 1px
  • p-6: padding: 1.5rem
  • w-64: width: 16rem

这是有关标题的一些 class:

  • text-3xl: font-size: 1.875rem
  • font-bold: font-weight: 700
  • mb-4: margin-bottom: .75rem
  • mt-0: margin-top: 0

这是有关内容的一些 class:

  • text-gray-700: color: #4a5568
  • text-sm: font-size: .875rem

Tailwind CSS 是一项投资。如果你想快速的编写 CSS 和设计你的 app,你要花时间去学习这些 class;与学习其他技术一样,你将从中获得收益。

会不会让 HTML 变得负担重?

可以将使用 Tailwind 看作是编写内联样式。有许多策略可以将这些类移出你的 HTML,并让其可复用。下面介绍一下清理 Tailwind 类的策略:

  • 使用 Sass 以及将我们的类移出 HTML
  • 使用 JS 组件(React/Vue),以及同样的类只写一次

下面是个例子,使用 Tailwind @apply function 清理HTML:

.btn {
  @apply font-bold py-2 px-4 rounded;
}

.btn-blue {
  @apply bg-blue-500 text-white;
}

.btn-blue:hover {
  @apply bg-blue-700;
}

我个人喜欢的一种方法是将类保存在模版文件中,然后让模板可复用。React 组件是一个很好的例子。

我们已经讨论了很多。现在,我们聚焦到如何使用 Tailwind 来构建一些东西。

什么是 Tailwind CSS?

现在让我们已经看了一个简单的例子,让我们来深入挖掘一下。 Tailwind CSS 是一个 CSS 框架,它与你之前用过的都不一样。当人们想到 CSS 框架的时候,最先想到的是使用最广泛的 Bootstrap,或者其他流行的 FoundationBulma

Tailwind 是一个特别的框架。相对于预先写好样式的组件,Tailwid 提供一大堆功能类。

下面是 Tailwind 所提供的一些种类的 class

我们有许多定义好的工具类,可以直接使用。下面是一些类的 Tailwind 文档地址。

Tailwind 的文档,是非常好的帮助我们上手的资料。当需要使用某一类型的类时,可以很快的找到。在页面中敲击 /,可以快速聚焦到 search bar 上。

Tailwind 的优点

当我们使用 Bootstrap,或者其他类似的框架时,我们有大量的预编译好现成的组件可以直接使用,如 cardnavbar 以及其他。当我们要自定义这些组件时,我们要写大量的自定义这些组件样式的 CSS 代码,以及覆盖基础样式。

这会带来带来混乱,让我们陷入编写相互覆盖样式的代码的泥潭。

Tailwind 给我们提供一个「只引用需要代码」的选择。

用 Tailwind 越多,好处越明显。让我们看一下这些好处,以及构建一点东西。

包大小

Tailwind 本身体积不小。这是因为它提供了很多工具类。

image.png

最大的好处提,可以使用 Purgecss控制文件的大小时。Purgecss 将检查我们的 HTML文件,并找出所有用到的 Tailwind 类。任何没有被用到的类,将会被从最终生成的 CSS 文件中移出。

当我们将所有未用到的类移出后,我们的 CSS 文件体积减小到 13kb!

定制容易

Tailwind 允许我们 定制一切 与类相关的内容。我们可以改变所使用的颜色,字号大小,padding距离,响应式布局,以及其他。

我们可以创建一个 tailwind.config.js,将我们的配置写入其中。这样,我们的配置将会覆盖调 Tailwind 默认的配置

// Example `tailwind.config.js` file

module.exports = {
  important: true,
  theme: {
    fontFamily: {
      display: ['Gilroy', 'sans-serif'],
      body: ['Graphik', 'sans-serif'],
    },
    extend: {
      colors: {
        cyan: '#9cdbff',
      },
      margin: {
        '96': '24rem',
        '128': '32rem',
      },
    }
  },
  variants: {
    opacity: ['responsive', 'hover']
  }
}

快速实现响应式

我们可以通过使用 Tailwind 提供的工具类,编写响应式内容。我从来不喜欢自己编写响应式断点代码。

Tailwind 中,已经定义好了一系列响应式类型,这些工具类通过前缀来进行区分:

  • sm: min-width: 640px
  • md: min-width: 768px
  • lg: min-width: 1024px
  • xl: min-width: 1280px

假设我们想要实现一个 box,在大屏幕上,背景是蓝色的,在小屏幕上,背景是蓝色的。通过这些定义好的前缀,很容易实现。

<div class="bg-red-400 lg:bg-blue-400">
    <p>i am red on small and medium devices</p>

    <p>i am blue on large and extra large devices</p>
</div>

超级快速实现原型(如果你熟悉 CSS)

我最喜爱 Tailwind 的特性是,我可以通过它,快速的将出色的设计搬到浏览器上。Tailwind 不会为你提供完美的设计。它只是为你提供快速创建的工具。我从不认为自己是设计师,也没有使用 Figma 这样的工具。我往往直接进入浏览器中,边开发,边设计。

如果你使用 Tailwind,你需要熟悉你的 CSS

使用 Tailwind,将让你熟悉你的 CSS 类是如何构建出来的,而不会将你隐藏在组件后面。如果你使用 Bootstrap 中的 card,你也许不清楚 card 里面有什么东西。当你使用 Tailwind,你会确切的知道与 CSS 有关的的细节。

我们使用 Tailwind 重新创建了一些 Web 上的东西,查看这些文章,以了解我们如何使用 Tailwind 快速构建原型。

https://scotch.io/tutorials/r...
https://scotch.io/tutorials/r...
https://scotch.io/tutorials/r...

结尾

Tailwind CSS 是看待 CSS 的一种与众不同的视角。它为你提供一个良好基础,以方便你快速创建任何类型设计。

可能需要一些时间来适应,但我认为这些努力是值得的。

你再也不用覆盖 CSS 框架中内置的样式了
查看原文

赞 3 收藏 1 评论 0

林水溶 赞了文章 · 9月11日

移动端样式小技巧

平时在移动端开发拼页面的过程中总会遇到一些问题,主要是各手机webview样式显示效果不一致造成的。以下总结了一些常见坑和一些小技巧,希望对看官有所帮助!

本文只针对两大手机阵营 Android和IOS 中的魅蓝metal 和 iPhone6进行样式对比。

一、line-height

line-height经常用于文字居中,当然也有小伙伴会用上下padding去写.but!不管你用padding还是line-height,不同手机显示效果还是...不一样。

一般会这样写

.demo{
    height:16px;
    line-height:14px;
    font-size:9px;
    border:1px solid #ff6815;
}

图片描述

嗯,在我们的chrome由于字体小于9px已经看不出边框和字之间的间隙了,再来看看Android和IOS的

图片描述魅蓝文字已经飞起~
图片描述 ios正常显示

如果把line-height加1px,iPhone文字就会下移,由于我们app的ios用户居多,并且android机型太多,不同机型也会显示不同,所以只能退而求其次了。

line-height的兼容问题不太好解决,容器高度越小,显示效果的差距越明显。稍微大一点的高度,最好把line-height设置为高度+1px,两个平台显示都还不错。


二、多行省略

一般我们的产品列表样式,会有标题行数的限制。

图片描述

怎么实现呢?

.demo{
    display: -webkit-box;    //1.设置display类型为-webkit-box
    font-size: 14px;
    line-height: 18px;
    overflow: hidden;        //2.设置元素超出隐藏
    text-overflow: ellipsis; //3.设置超出样式为省略号
    -webkit-line-clamp: 2;   //4.设置2行应用省略
    -webkit-box-orient: vertical;
}

这样设置还要考虑一个极端的情况,就是标题不足两行。具体要看PM的需求,一是空出第二行的距离,二是让标题下边的元素顶上去。如果是第一种需求,有2种解决的方案。
1:把下边的元素都使用position:absoulte定位到固定的位置,不受标题行数影响。
2:把标题容器的高度写死,这样写必须要考虑行高的坑,因为容器高度写死以后,不同机型行高实际上显示效果不一样。

高度写少了,有的机型会这样。
图片描述

写多了可能会这样。
图片描述

我的做法是,不影响布局的情况下尽量控制line-height值大一些,行与行的间距变大,容器高度的设定需要多测试一些机型,控制文字不多出也不被挡住。


三、角标的实现

角标
不少项目ui会要求我们画这种梯形角标。问题来了

1.我们不确定角标内容的长度
2.角标的底色不能定死,能定死(能定死的话直接切个小体形就行了)

通常就是一段文案后边拼接一个三角形,三角形很好写

.script {
    width: 0;
    height: 0;        //控制宽高为0,用border宽度撑出一个三角形
    border-right: 10px solid transparent;
    border-top: 15px solid #c59c53;
}

我看到的第一种写法是把三角形直接拼在前边的文案后边,当然这在iphone上是没有问题的。但在部分安卓机型上却会有1像素的间隙,就像这样:

图片描述 我现在感受到安卓阵营深深的恶意

原因可能是定位在各安卓手机上会有不同的效果。。好像大家都是猴子,长的却都不一样。

对此有个好的解决方案:

<p class="rongqi">
    <span class="wenan">跟团游</span>
    <span class="script"></span>
</p>
.rongqi {//容器
    height: 15px;
    overflow: hidden;//设置超出隐藏
    position: absolute;
    top: 0;
    left: 0;
}
.wenan{//文案
    float: left;
    position: relative;    //设置相对定位
    z-index: 3;            //设置层级不被三角形挡住
    height: 15px;
    padding-left: 4px;
    line-height: 16px;
    color: #fff;
    font-size: 10px;
}
.script {
    width: 0;
    height: 0;
    border-right: 20px solid transparent;
    border-top: 30px solid #c59c53;    //这里的30px实际上远远超出容器的高度
    float: right;                      //就是为了高度超出被挡住做出梯形的效果,兼容各种机型
    margin-left: -9px;
}

如果去除容器的overflow:hidden就可以看的更明白:

图片描述


四、图文标题

图片描述

一些常见的布局例如图+文案的,有多种方式可以去写,比如padding-left+background或者position+padding-left或者before伪元素。
前两种方法都可以把图片做到绝对的垂直居中,但是它们都是相对整行的容器进行定位的,由于line-height兼容问题的坑,图片实际上不一定会和文字对齐。如果有图文对齐的需求的话,个人建议才用before伪元素来布局,before可以相对文案来定位。

p{
    height:44px;
    line-height:45px;
    padding-left:40px;
}
p::before{
    content: '';
    display: inline-block;
    background: url("../img/xxx.png") center center no-repeat;
    background-size: contain;    //这里把背景图片尺寸设置成contain,不需要考虑图片拉伸的问题
    width: 14px;
    height: 18px;
    margin: 0 5px -4px 0;
}

还有一种情况,我们的图文布局,是从数组中遍历出来的,类似下图:
图片描述
这种情况更适合position去写,所以写样式一定要根据不同情况去选择合适的方式。


五、左右宽度自适应

第四个小技巧结尾,图中的布局实际上是分左右两块的,依照ui的需求,文案是要左对齐,数字是要右对齐的。你可能最先想到的是把右侧的数字定位或者浮动到那,左侧的容器加上个margin-right或者padding-right。这样可以实现,但是两侧的文案有极端情况出现。

效果可能是这样的:
图片描述
也可能是这样的
图片描述

因为你根本不知道两侧文案的长度到底是多少。
我的方案是用box布局,左侧的容器设置box-flex:1,右侧不管它:

<li class="ent-li">
    <img class="ent-img" data-original="img/1.png">
    <div class="left">主题门票</div>
    <div class="right">10</div>
</li>
.ent-li {
    margin-left: 45px;
    height: 44px;
    display: -webkit-box; //box布局并做好兼容
    display: box;
    position: relative;
}
.ent-li .left {
    -webkit-box-flex: 1; //box-flex:1控制宽度自适应
    box-flex: 1;
    text-align: left;
    line-height: 45px;
    font-size: 16px;
    color: #333;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.ent-li .right {    //右侧啥都不用管
    text-align: right;
    line-height: 45px;
    font-size: 12px;
    color: #999;
    padding-left: 10px;
}

让我们看看最终极端条件下的显示效果:
图片描述
或者:
图片描述

因为pm觉得数字更重要,所以让文案去自适应,数字有多长就多长


六、display:inline-block

众所周知,元素有3种基本显示框类型,block块级,inline-block行内块级,inline行内。
inline-block是一种特殊存在,可以设置宽高也可以使元素在一行排列。

以下图片信息来自同程旅游app

我在开发中会遇到以下两种布局:

clipboard.png

clipboard.png

这两种布局都可以用float:left来写,但是浮动完了还需要清楚浮动。所以可以直接把元素设置成inline-block同样可以自动排列

.rongqi{//每块容器
    display: inline-block;//设置行内块级
    width: 25%;           //设置宽度为1/4
    font-size: 12px;
    text-align: center;
}

这里会有个小坑,就是行内元素在水平和垂直排列的时候会有间距。造成这种结果

clipboard.pngclipboard.png
左边是默认情况下的效果,右侧是改进后的效果,左边第二行和第一行中间有段莫名的间距,这并不是我们真正想要的。
解决的办法很简单:

.father{
    font-size:0;//父容器字体大小设置成0,具体的字体大小应用在文案上
}

七、模拟滚动

以下图片信息来自同程旅游app

clipboard.png
模拟滚动也是在项目中遇到的常见布局。布局要求弹层出来后,弹层中的内容可以滚动,弹层背后的列表不能随弹层滚动而滚动。并且在弹层上滑动的时候,整个页面也不能随之滚动。
直接上代码:

<section class="father">
    <section class="content-body">
    <!--页面内容、蒙层、蒙层中的内容互为兄弟节点,防止点击时页面穿透-->
    </section>
    <section class="layout">
    <!--页面内容、蒙层、蒙层中的内容互为兄弟节点,防止点击时页面穿透-->
    </section>
    <section class="layout-body">
    <!--页面内容、蒙层、蒙层中的内容互为兄弟节点,防止点击时页面穿透-->
    </section>
</section>
.father{
    height: 533px;
    overflow-y: scroll;//页面高度设置为屏幕高度,正常情况下超出滚动
}
.content-body{
    height: 533px;
    overflow-y: scroll;//内容高度设置为屏幕高度,正常情况下超出滚动
}
.layout{
    height: 100%;
    width: 100%;
    position: fixed;
    left: 0;
    right: 0;
    top: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.7);
    overflow: hidden;
    z-index: 1000000;
}
.layout-body{
    height: 46%;
    width: 100%;
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.7);
    overflow: hidden;
    z-index: 1000001;
}

当我们触发蒙层弹出时控制样式

.father{
    height: 533px;
    overflow-y: hidden;//设置超出隐藏,那么页面不会触发滚动
}
.content-body{
    height: 533px;
    overflow-y: hidden;//设置超出隐藏,那么页面不会触发滚动
}

这个方法虽然实现了页面模拟滚动的效果,但是当蒙层弹出的时候设置了overflow:hidden会导致页面scrollTop变成0,页面相当于被滚到顶部了。如果UI或者PM不愿意,请与他们撕逼。

查看原文

赞 17 收藏 190 评论 25

林水溶 收藏了文章 · 9月5日

golang之数据验证validator

前言

在web应用中经常会遇到数据验证问题,普通的验证方法比较繁琐,这里介绍一个使用比较多的包validator

原理

将验证规则写在struct对字段tag里,再通过反射(reflect)获取struct的tag,实现数据验证。

安装

go get github.com/go-playground/validator/v10

示例

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type Users struct {
    Phone   string `form:"phone" json:"phone" validate:"required"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {

    users := &Users{
        Phone:      "1326654487",
        Passwd:       "123",
        Code:            "123456",
    }
    validate := validator.New()
    err := validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err)//Key: 'Users.Passwd' Error:Field validation for 'Passwd' failed on the 'min' tag
            return
        }
    }
    return
}

验证规则

  • required :必填
  • email:验证字符串是email格式;例:"email"
  • url:这将验证字符串值包含有效的网址;例:"url"
  • max:字符串最大长度;例:"max=20"
  • min:字符串最小长度;例:"min=6"
  • excludesall:不能包含特殊字符;例:"excludesall=0x2C"//注意这里用十六进制表示。
  • len:字符长度必须等于n,或者数组、切片、map的len值为n,即包含的项目数;例:"len=6"
  • eq:数字等于n,或者或者数组、切片、map的len值为n,即包含的项目数;例:"eq=6"
  • ne:数字不等于n,或者或者数组、切片、map的len值不等于为n,即包含的项目数不为n,其和eq相反;例:"ne=6"
  • gt:数字大于n,或者或者数组、切片、map的len值大于n,即包含的项目数大于n;例:"gt=6"
  • gte:数字大于或等于n,或者或者数组、切片、map的len值大于或等于n,即包含的项目数大于或等于n;例:"gte=6"
  • lt:数字小于n,或者或者数组、切片、map的len值小于n,即包含的项目数小于n;例:"lt=6"
  • lte:数字小于或等于n,或者或者数组、切片、map的len值小于或等于n,即包含的项目数小于或等于n;例:"lte=6"

跨字段验证

如想实现比较输入密码和确认密码是否一致等类似场景

  • eqfield=Field: 必须等于 Field 的值;
  • nefield=Field: 必须不等于 Field 的值;
  • gtfield=Field: 必须大于 Field 的值;
  • gtefield=Field: 必须大于等于 Field 的值;
  • ltfield=Field: 必须小于 Field 的值;
  • ltefield=Field: 必须小于等于 Field 的值;
  • eqcsfield=Other.Field: 必须等于 struct Other 中 Field 的值;
  • necsfield=Other.Field: 必须不等于 struct Other 中 Field 的值;
  • gtcsfield=Other.Field: 必须大于 struct Other 中 Field 的值;
  • gtecsfield=Other.Field: 必须大于等于 struct Other 中 Field 的值;
  • ltcsfield=Other.Field: 必须小于 struct Other 中 Field 的值;
  • ltecsfield=Other.Field: 必须小于等于 struct Other 中 Field 的值;

示例

type UserReg struct {
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Repasswd   string `form:"repasswd" json:"repasswd" validate:"required,max=20,min=6,eqfield=Passwd"`
}

示例验证了Passwd,和Repasswd值是否相等。如想了解更多类型,请参考文档 https://godoc.org/gopkg.in/go...

自定义验证类型

示例:

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type Users struct {
    Name   string `form:"name" json:"name" validate:"required,CustomValidationErrors"`//包含自定义函数
    Age   uint8 `form:"age" json:"age" validate:"required,gt=18"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {

    users := &Users{
        Name:      "admin",
        Age:        12,
        Passwd:       "123",
        Code:            "123456",
    }
    validate := validator.New()
    //注册自定义函数
    _=validate.RegisterValidation("CustomValidationErrors", CustomValidationErrors)
    err := validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err)//Key: 'Users.Name' Error:Field validation for 'Name' failed on the 'CustomValidationErrors' tag
            return
        }
    }
    return
}

func CustomValidationErrors(fl validator.FieldLevel) bool {
return fl.Field().String() != "admin"
}

翻译错误信息为中文

通过以上示例我们看到,validator默认的错误提示信息类似如下

Key: 'Users.Name' Error:Field validation for 'Name' failed on the 'CustomValidationErrors' tag

显然这并不是我们想要,如想翻译成中文,或其他语言怎么办?go-playground上提供了很好的解决方法。

先自行安装需要的两个包

https://github.com/go-playground/locales
https://github.com/go-playground/universal-translator

执行:

go get github.com/go-playground/universal-translator
go get github.com/go-playground/locales

示例:

package main

import (
    "fmt"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
)

type Users struct {
    Name   string `form:"name" json:"name" validate:"required"`
    Age   uint8 `form:"age" json:"age" validate:"required,gt=18"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {
    users := &Users{
        Name:      "admin",
        Age:        12,
        Passwd:       "123",
        Code:            "123456",
    }
    uni := ut.New(zh.New())
    trans, _ := uni.GetTranslator("zh")
    validate := validator.New()
    //验证器注册翻译器
    err := zh_translations.RegisterDefaultTranslations(validate, trans)
    if err!=nil {
        fmt.Println(err)
    }
    err = validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err.Translate(trans))//Age必须大于18
            return
        }
    }

    return
}

输出:

Age必须大于18

至此我们发现大部分错误信息已经翻译成中文,但字段名(Age)还是没有翻译,为了将字段名翻译成中文,查了一些资料,https://www.jianshu.com/p/51b...

照着做没有成功(可能有遗漏吧),最后还是翻看了一下源代码,在https://github.com/go-playgro...,第137行

// RegisterTagNameFunc registers a function to get alternate names for StructFields.
//
// eg. to use the names which have been specified for JSON representations of structs, rather than normal Go field names:
//
//    validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
//        name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
//        if name == "-" {
//            return ""
//        }
//        return name
//    })

其实原理就是注册一个函数,将struct tag里添加的中文名 作为备用名。

package main

import (
    "fmt"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
    "reflect"
)

type Users struct {
    Name   string `form:"name" json:"name" validate:"required" label:"用户名"`
    Age   uint8 `form:"age" json:"age" validate:"required,gt=18" label:"年龄"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {
    users := &Users{
        Name:      "admin",
        Age:        12,
        Passwd:       "123",
        Code:            "123456",
    }
    uni := ut.New(zh.New())
    trans, _ := uni.GetTranslator("zh")
    validate := validator.New()
    //注册一个函数,获取struct tag里自定义的label作为字段名
    validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
        name:=fld.Tag.Get("label")
        return name
    })
    //注册翻译器
    err := zh_translations.RegisterDefaultTranslations(validate, trans)
    if err!=nil {
        fmt.Println(err)
    }
    err = validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err.Translate(trans))//年龄必须大于18
            return
        }
    }

    return
}

输出结果:

年龄必须大于18

gin 内置的validator

gin已经支持go-playground / validator / v10进行验证。在此处查看有关标签用法的完整文档。

以下只提供了一个绑定ShouldBindWith示例,如需了解更多方法,进入这里

示例

package main

import (
    "fmt"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    "net/http"
    "reflect"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
)
var trans ut.Translator
// Booking contains binded and validated data.
type Booking struct {
    CheckIn  time.Time `form:"check_in" json:"check_in" binding:"required,bookabledate" time_format:"2006-01-02" label:"输入时间"`
    CheckOut time.Time `form:"check_out" json:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02" label:"输出时间"`
}

var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
    date, ok := fl.Field().Interface().(time.Time)
    if ok {
        today := time.Now()
        if today.After(date) {
            return false
        }
    }
    return true
}

func main() {
    route := gin.Default()
    uni := ut.New(zh.New())
    trans, _ = uni.GetTranslator("zh")

    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        //注册翻译器
        _= zh_translations.RegisterDefaultTranslations(v, trans)
        //注册自定义函数
        _=v.RegisterValidation("bookabledate", bookableDate)

        //注册一个函数,获取struct tag里自定义的label作为字段名
        v.RegisterTagNameFunc(func(fld reflect.StructField) string {
            name:=fld.Tag.Get("label")
            return name
        })
        //根据提供的标记注册翻译
        v.RegisterTranslation("bookabledate", trans, func(ut ut.Translator) error {
            return ut.Add("bookabledate", "{0}不能早于当前时间或{1}格式错误!", true)
        }, func(ut ut.Translator, fe validator.FieldError) string {
            t, _ := ut.T("bookabledate", fe.Field(), fe.Field())
            return t
        })

    }
    route.GET("/bookable", getBookable)
    route.Run(":8085")
}

func getBookable(c *gin.Context) {
    var b Booking
    if err := c.ShouldBindWith(&b, binding.Query); err == nil {
        c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
    } else {
        errs := err.(validator.ValidationErrors)

        fmt.Println(errs.Translate(trans))
        //for _, e := range errs {
        //    // can translate each error one at a time.
        //    fmt.Println(e.Translate(trans))
        //}
        c.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
    }
}

运行程序,执行以下命令

$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-16"

结果:

{"error":{"Booking.输入时间":"输入时间不能早于当前时间或输入时间格式错误!","Booking.输出时间":"输出时间必须大于CheckIn"}}

查看以上结果我们发现翻译还是不太完美,如规则中有gtfield的情况,字段(CheckIn)并没有被翻译。所以通过struct添加label的方式并不能从根本上解决字段翻译问题。为了得到想要的结果,就需要将错误信息做单独处理再输出。

先定义翻译库

var BookingTrans =map[string]string{"CheckIn":"输入时间","CheckOut":"输出时间"}

再定义翻译函数


func TransTagName(libTans,err interface{}) interface{} {
    switch err.(type) {
    case validator.ValidationErrorsTranslations:
        var errs map[string]string
        errs = make(map[string]string,0)
        for k,v:=range err.(validator.ValidationErrorsTranslations){
            for key,value:=range libTans.(map[string]string)  {
                v=strings.Replace(v,key,value,-1)
            }
            errs[k] = v
        }
        return errs
    case string:
        var errs string
        for key,value:=range libTans.(map[string]string)  {
            errs=strings.Replace(errs,key,value,-1)
        }
        return errs
    default:
        return err
    }
}

将原来翻译错误信息的地方

errs.Translate(trans)

修改为

msg:=TransTagName(BookingTrans,errs.Translate(trans))
fmt.Println(msg)

结果

{"error":{"Booking.输入时间":"输入时间不能早于当前时间或输入时间格式错误!","Booking.输出时间":"输出时间必须大于输入时间"}}

小结:

  1. gin 已经支持validator最新的v10。
  2. validator数据验证顺序struct字段从上往下,单个字段规则(binding:"gt=0,lt=2`),先左后右。

参考:

https://github.com/go-playgro...

https://github.com/gin-gonic/gin

https://gitissue.com/issues/5...

https://segmentfault.com/a/11...

links

查看原文

林水溶 赞了文章 · 9月5日

golang之数据验证validator

前言

在web应用中经常会遇到数据验证问题,普通的验证方法比较繁琐,这里介绍一个使用比较多的包validator

原理

将验证规则写在struct对字段tag里,再通过反射(reflect)获取struct的tag,实现数据验证。

安装

go get github.com/go-playground/validator/v10

示例

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type Users struct {
    Phone   string `form:"phone" json:"phone" validate:"required"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {

    users := &Users{
        Phone:      "1326654487",
        Passwd:       "123",
        Code:            "123456",
    }
    validate := validator.New()
    err := validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err)//Key: 'Users.Passwd' Error:Field validation for 'Passwd' failed on the 'min' tag
            return
        }
    }
    return
}

验证规则

  • required :必填
  • email:验证字符串是email格式;例:"email"
  • url:这将验证字符串值包含有效的网址;例:"url"
  • max:字符串最大长度;例:"max=20"
  • min:字符串最小长度;例:"min=6"
  • excludesall:不能包含特殊字符;例:"excludesall=0x2C"//注意这里用十六进制表示。
  • len:字符长度必须等于n,或者数组、切片、map的len值为n,即包含的项目数;例:"len=6"
  • eq:数字等于n,或者或者数组、切片、map的len值为n,即包含的项目数;例:"eq=6"
  • ne:数字不等于n,或者或者数组、切片、map的len值不等于为n,即包含的项目数不为n,其和eq相反;例:"ne=6"
  • gt:数字大于n,或者或者数组、切片、map的len值大于n,即包含的项目数大于n;例:"gt=6"
  • gte:数字大于或等于n,或者或者数组、切片、map的len值大于或等于n,即包含的项目数大于或等于n;例:"gte=6"
  • lt:数字小于n,或者或者数组、切片、map的len值小于n,即包含的项目数小于n;例:"lt=6"
  • lte:数字小于或等于n,或者或者数组、切片、map的len值小于或等于n,即包含的项目数小于或等于n;例:"lte=6"

跨字段验证

如想实现比较输入密码和确认密码是否一致等类似场景

  • eqfield=Field: 必须等于 Field 的值;
  • nefield=Field: 必须不等于 Field 的值;
  • gtfield=Field: 必须大于 Field 的值;
  • gtefield=Field: 必须大于等于 Field 的值;
  • ltfield=Field: 必须小于 Field 的值;
  • ltefield=Field: 必须小于等于 Field 的值;
  • eqcsfield=Other.Field: 必须等于 struct Other 中 Field 的值;
  • necsfield=Other.Field: 必须不等于 struct Other 中 Field 的值;
  • gtcsfield=Other.Field: 必须大于 struct Other 中 Field 的值;
  • gtecsfield=Other.Field: 必须大于等于 struct Other 中 Field 的值;
  • ltcsfield=Other.Field: 必须小于 struct Other 中 Field 的值;
  • ltecsfield=Other.Field: 必须小于等于 struct Other 中 Field 的值;

示例

type UserReg struct {
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Repasswd   string `form:"repasswd" json:"repasswd" validate:"required,max=20,min=6,eqfield=Passwd"`
}

示例验证了Passwd,和Repasswd值是否相等。如想了解更多类型,请参考文档 https://godoc.org/gopkg.in/go...

自定义验证类型

示例:

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type Users struct {
    Name   string `form:"name" json:"name" validate:"required,CustomValidationErrors"`//包含自定义函数
    Age   uint8 `form:"age" json:"age" validate:"required,gt=18"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {

    users := &Users{
        Name:      "admin",
        Age:        12,
        Passwd:       "123",
        Code:            "123456",
    }
    validate := validator.New()
    //注册自定义函数
    _=validate.RegisterValidation("CustomValidationErrors", CustomValidationErrors)
    err := validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err)//Key: 'Users.Name' Error:Field validation for 'Name' failed on the 'CustomValidationErrors' tag
            return
        }
    }
    return
}

func CustomValidationErrors(fl validator.FieldLevel) bool {
return fl.Field().String() != "admin"
}

翻译错误信息为中文

通过以上示例我们看到,validator默认的错误提示信息类似如下

Key: 'Users.Name' Error:Field validation for 'Name' failed on the 'CustomValidationErrors' tag

显然这并不是我们想要,如想翻译成中文,或其他语言怎么办?go-playground上提供了很好的解决方法。

先自行安装需要的两个包

https://github.com/go-playground/locales
https://github.com/go-playground/universal-translator

执行:

go get github.com/go-playground/universal-translator
go get github.com/go-playground/locales

示例:

package main

import (
    "fmt"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
)

type Users struct {
    Name   string `form:"name" json:"name" validate:"required"`
    Age   uint8 `form:"age" json:"age" validate:"required,gt=18"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {
    users := &Users{
        Name:      "admin",
        Age:        12,
        Passwd:       "123",
        Code:            "123456",
    }
    uni := ut.New(zh.New())
    trans, _ := uni.GetTranslator("zh")
    validate := validator.New()
    //验证器注册翻译器
    err := zh_translations.RegisterDefaultTranslations(validate, trans)
    if err!=nil {
        fmt.Println(err)
    }
    err = validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err.Translate(trans))//Age必须大于18
            return
        }
    }

    return
}

输出:

Age必须大于18

至此我们发现大部分错误信息已经翻译成中文,但字段名(Age)还是没有翻译,为了将字段名翻译成中文,查了一些资料,https://www.jianshu.com/p/51b...

照着做没有成功(可能有遗漏吧),最后还是翻看了一下源代码,在https://github.com/go-playgro...,第137行

// RegisterTagNameFunc registers a function to get alternate names for StructFields.
//
// eg. to use the names which have been specified for JSON representations of structs, rather than normal Go field names:
//
//    validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
//        name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
//        if name == "-" {
//            return ""
//        }
//        return name
//    })

其实原理就是注册一个函数,将struct tag里添加的中文名 作为备用名。

package main

import (
    "fmt"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
    "reflect"
)

type Users struct {
    Name   string `form:"name" json:"name" validate:"required" label:"用户名"`
    Age   uint8 `form:"age" json:"age" validate:"required,gt=18" label:"年龄"`
    Passwd   string `form:"passwd" json:"passwd" validate:"required,max=20,min=6"`
    Code   string `form:"code" json:"code" validate:"required,len=6"`
}

func main() {
    users := &Users{
        Name:      "admin",
        Age:        12,
        Passwd:       "123",
        Code:            "123456",
    }
    uni := ut.New(zh.New())
    trans, _ := uni.GetTranslator("zh")
    validate := validator.New()
    //注册一个函数,获取struct tag里自定义的label作为字段名
    validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
        name:=fld.Tag.Get("label")
        return name
    })
    //注册翻译器
    err := zh_translations.RegisterDefaultTranslations(validate, trans)
    if err!=nil {
        fmt.Println(err)
    }
    err = validate.Struct(users)
    if err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Println(err.Translate(trans))//年龄必须大于18
            return
        }
    }

    return
}

输出结果:

年龄必须大于18

gin 内置的validator

gin已经支持go-playground / validator / v10进行验证。在此处查看有关标签用法的完整文档。

以下只提供了一个绑定ShouldBindWith示例,如需了解更多方法,进入这里

示例

package main

import (
    "fmt"
    "github.com/go-playground/locales/zh"
    ut "github.com/go-playground/universal-translator"
    "github.com/go-playground/validator/v10"
    "net/http"
    "reflect"
    "strings"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    zh_translations "github.com/go-playground/validator/v10/translations/zh"
)
var trans ut.Translator
// Booking contains binded and validated data.
type Booking struct {
    CheckIn  time.Time `form:"check_in" json:"check_in" binding:"required,bookabledate" time_format:"2006-01-02" label:"输入时间"`
    CheckOut time.Time `form:"check_out" json:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02" label:"输出时间"`
}

var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
    date, ok := fl.Field().Interface().(time.Time)
    if ok {
        today := time.Now()
        if today.After(date) {
            return false
        }
    }
    return true
}

func main() {
    route := gin.Default()
    uni := ut.New(zh.New())
    trans, _ = uni.GetTranslator("zh")

    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        //注册翻译器
        _= zh_translations.RegisterDefaultTranslations(v, trans)
        //注册自定义函数
        _=v.RegisterValidation("bookabledate", bookableDate)

        //注册一个函数,获取struct tag里自定义的label作为字段名
        v.RegisterTagNameFunc(func(fld reflect.StructField) string {
            name:=fld.Tag.Get("label")
            return name
        })
        //根据提供的标记注册翻译
        v.RegisterTranslation("bookabledate", trans, func(ut ut.Translator) error {
            return ut.Add("bookabledate", "{0}不能早于当前时间或{1}格式错误!", true)
        }, func(ut ut.Translator, fe validator.FieldError) string {
            t, _ := ut.T("bookabledate", fe.Field(), fe.Field())
            return t
        })

    }
    route.GET("/bookable", getBookable)
    route.Run(":8085")
}

func getBookable(c *gin.Context) {
    var b Booking
    if err := c.ShouldBindWith(&b, binding.Query); err == nil {
        c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
    } else {
        errs := err.(validator.ValidationErrors)

        fmt.Println(errs.Translate(trans))
        //for _, e := range errs {
        //    // can translate each error one at a time.
        //    fmt.Println(e.Translate(trans))
        //}
        c.JSON(http.StatusBadRequest, gin.H{"error": errs.Translate(trans)})
    }
}

运行程序,执行以下命令

$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-16"

结果:

{"error":{"Booking.输入时间":"输入时间不能早于当前时间或输入时间格式错误!","Booking.输出时间":"输出时间必须大于CheckIn"}}

查看以上结果我们发现翻译还是不太完美,如规则中有gtfield的情况,字段(CheckIn)并没有被翻译。所以通过struct添加label的方式并不能从根本上解决字段翻译问题。为了得到想要的结果,就需要将错误信息做单独处理再输出。

先定义翻译库

var BookingTrans =map[string]string{"CheckIn":"输入时间","CheckOut":"输出时间"}

再定义翻译函数


func TransTagName(libTans,err interface{}) interface{} {
    switch err.(type) {
    case validator.ValidationErrorsTranslations:
        var errs map[string]string
        errs = make(map[string]string,0)
        for k,v:=range err.(validator.ValidationErrorsTranslations){
            for key,value:=range libTans.(map[string]string)  {
                v=strings.Replace(v,key,value,-1)
            }
            errs[k] = v
        }
        return errs
    case string:
        var errs string
        for key,value:=range libTans.(map[string]string)  {
            errs=strings.Replace(errs,key,value,-1)
        }
        return errs
    default:
        return err
    }
}

将原来翻译错误信息的地方

errs.Translate(trans)

修改为

msg:=TransTagName(BookingTrans,errs.Translate(trans))
fmt.Println(msg)

结果

{"error":{"Booking.输入时间":"输入时间不能早于当前时间或输入时间格式错误!","Booking.输出时间":"输出时间必须大于输入时间"}}

小结:

  1. gin 已经支持validator最新的v10。
  2. validator数据验证顺序struct字段从上往下,单个字段规则(binding:"gt=0,lt=2`),先左后右。

参考:

https://github.com/go-playgro...

https://github.com/gin-gonic/gin

https://gitissue.com/issues/5...

https://segmentfault.com/a/11...

links

查看原文

赞 1 收藏 1 评论 0

林水溶 赞了文章 · 9月3日

前端构建工具gulp入门教程

本文假设你之前没有用过任何任务脚本(task runner)和命令行工具,一步步教你上手Gulp。不要怕,它其实很简单,我会分为五步向你介绍gulp并帮助你完成一些惊人的事情。那就直接开始吧。

第一步:安装Node

首先,最基本也最重要的是,我们需要搭建node环境。访问http://nodejs.org,然后点击大大的绿色的install按钮,下载完成后直接运行程序,就一切准备就绪。npm会随着安装包一起安装,稍后会用到它。

第二步:使用命令行

也许现在你还不是很了解什么是命令行——OSX中的终端(Terminal),windows中的命令提示符(Command Prompt),但很快你就会知道。它看起来没那么简单,但一旦掌握了它的窍门,就可以很方便的执行很多命令行程序,比如Sass,Yeoman和Git等,这些都是非常有用的工具。

如果你很熟悉命令行,直接跳到步骤四。

为了确保Node已经正确安装,我们执行几个简单的命令。

node -v

回车(Enter),如果正确安装的话,你会看到所安装的Node的版本号,接下来看看npm。

npm -v

这同样能得到npm的版本号。

如果这两行命令没有得到返回,可能node就没有安装正确,尝试重启下命令行工具,如果还不行的话,只能回到第一步进行重装。

第三步:定位到项目

现在,我们已经大致了解了命令行并且知道如何简单使用它,接下来只需要两个简单的命令就能定位到文件目录并看看目录里都有些什么文件。

  1. cd,定位到目录
  2. ls,列出文件列表

建议多敲敲这两个命令,了解文件系统并知道文件都在哪里。

习惯使用了这两个命令后,就要进入我们的项目目录,这个目录各不相同,举个例子,这是我进入我项目目录的命令:

cd /Applications/XAMPP/xamppfiles/htdocs/my-project

成功进入项目目录后,我们开始安装gulp。

第四步:安装gulp

我们已经知道如何使用命令行,现在尝试点新的东西,认识npm然后安装gulp。

NPM是基于命令行的node包管理工具,它可以将node的程序模块安装到项目中,在它的官网中可以查看和搜索所有可用的程序模块。

在命令行中输入

sudo npm install -g gulp 
  1. sudo是以管理员身份执行命令,一般会要求输入电脑密码
  2. npm是安装node模块的工具,执行install命令

  3. -g表示在全局环境安装,以便任何项目都能使用它

  4. 最后,gulp是将要安装的node模块的名字

运行时注意查看命令行有没有错误信息,安装完成后,你可以使用下面的命令查看gulp的版本号以确保gulp已经被正确安装。

gulp -v

接下来,我们需要将gulp安装到项目本地

npm install —-save-dev gulp

这里,我们使用—-save-dev来更新package.json文件,更新devDependencies值,以表明项目需要依赖gulp。

Dependencies可以向其他参与项目的人指明项目在开发环境和生产环境中的node模块依懒关系,想要更加深入的了解它可以看看package.json文档

第五步:新建Gulpfile文件,运行gulp

安装好gulp后我们需要告诉它要为我们执行哪些任务,首先,我们自己需要弄清楚项目需要哪些任务。

  • 检查Javascript
  • 编译Sass(或Less之类的)文件
  • 合并Javascript
  • 压缩并重命名合并后的Javascript

安装依赖

npm install gulp-jshint gulp-sass gulp-concat gulp-uglify gulp-rename --save-dev 

提醒下,如果以上命令提示权限错误,需要添加sudo再次尝试。

新建gulpfile文件

现在,组件都安装完毕,我们需要新建gulpfile文件以指定gulp需要为我们完成什么任务。

gulp只有五个方法: taskrunwatchsrc,和dest,在项目根目录新建一个js文件并命名为gulpfile.js,把下面的代码粘贴进去:

gulpfile.js

// 引入 gulp
var gulp = require('gulp'); 

// 引入组件
var jshint = require('gulp-jshint');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

// 检查脚本
gulp.task('lint', function() {
    gulp.src('./js/*.js')
        .pipe(jshint())
        .pipe(jshint.reporter('default'));
});

// 编译Sass
gulp.task('sass', function() {
    gulp.src('./scss/*.scss')
        .pipe(sass())
        .pipe(gulp.dest('./css'));
});

// 合并,压缩文件
gulp.task('scripts', function() {
    gulp.src('./js/*.js')
        .pipe(concat('all.js'))
        .pipe(gulp.dest('./dist'))
        .pipe(rename('all.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest('./dist'));
});

// 默认任务
gulp.task('default', function(){
    gulp.run('lint', 'sass', 'scripts');

    // 监听文件变化
    gulp.watch('./js/*.js', function(){
        gulp.run('lint', 'sass', 'scripts');
    });
});

现在,分段解释下这段代码。

引入组件

var gulp = require('gulp'); 

var jshint = require('gulp-jshint');
var sass = require('gulp-sass');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var rename = require('gulp-rename');

这一步,我们引入了核心的gulp和其他依赖组件,接下来,分开创建lint, sass, scripts 和 default这四个不同的任务。

Lint任务

gulp.task('lint', function() {
    gulp.src('./js/*.js')
        .pipe(jshint())
        .pipe(jshint.reporter('default'));
});

Link任务会检查js/目录下得js文件有没有报错或警告。

Sass任务

gulp.task('sass', function() {
    gulp.src('./scss/*.scss')
        .pipe(sass())
        .pipe(gulp.dest('./css'));
});

Sass任务会编译scss/目录下的scss文件,并把编译完成的css文件保存到/css目录中。

Scripts 任务

gulp.task('scripts', function() {
    gulp.src('./js/*.js')
        .pipe(concat('all.js'))
        .pipe(gulp.dest('./dist'))
        .pipe(rename('all.min.js'))
        .pipe(uglify())
        .pipe(gulp.dest('./dist'));
});

scripts任务会合并js/目录下得所有得js文件并输出到dist/目录,然后gulp会重命名、压缩合并的文件,也输出到dist/目录。

default任务

gulp.task('default', function(){
    gulp.run('lint', 'sass', 'scripts');
    gulp.watch('./js/*.js', function(){
        gulp.run('lint', 'sass', 'scripts');
    });
});

这时,我们创建了一个基于其他任务的default任务。使用.run()方法关联和运行我们上面定义的任务,使用.watch()方法去监听指定目录的文件变化,当有文件变化时,会运行回调定义的其他任务。

现在,回到命令行,可以直接运行gulp任务了。

gulp

这将执行定义的default任务,换言之,这和以下的命令式同一个意思

gulp default

当然,我们可以运行在gulpfile.js中定义的任意任务,比如,现在运行sass任务:

gulp sass

(Kimi: 哇塞,酷比了哎~)

结束语

现在已经做到了设置gulp任务然后运行他们,现在再回顾下之前学习的。

  1. 学习了安装Node环境
  2. 学习了简单使用命令行
  3. 学习了用命令行进入项目目录
  4. 学习了使用npm和安装gulp
  5. 学习了如何运行gulp任务

另外,有一些参考资源供进一步学习:

  1. npm上得gulp组件
  2. gulp的Github主页
  3. 官方package.json文档

本文译自http://travismaynard.com/writing/getting-started-with-gulp

查看原文

赞 51 收藏 378 评论 39

林水溶 赞了文章 · 9月1日

使用pm2快速将项目部署到远程服务器

使用背景

  • 当我们需要将项目部署到远程线上服务器时;传统的方法可能就是:

    1. 将本地代码通过ssh、ftp等方式上传到服务器;
    2. 然后通过ssh登入到服务器,配置好环境;
    3. 手动启动应用。
  • 太过手动化,麻烦,操作繁琐。

现代自动化部署

  • 环境:本地(Mac);远程服务器(CentOS)
  • 使用工具:Git、pm2、node
  • 需知概念:ssh秘钥登陆Github添加Deploy Keys

1、服务器环境部署

  • 基本工具安装:git、pm2、node

2、ssh服务器免密登陆

  1. 服务器生成秘钥对

    ssh-keygen -t rsa -C  '1285227393@qq.com'
    
    -t 指定密钥类型,默认即 rsa ,可以省略
    -C 设置注释文字,比如邮箱,可以省略
    • 由于使用的是百度云服务器,里面可以直接界面生成秘钥对,然后下载到本地的是一个xxx.txt文件

clipboard.png

  • . 此时登陆可以使用ssh -i xxx.txt[下载的公钥路径] name@domain
  • 报错:
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'server-key.txt' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "server-key.txt": bad permissions
  • 大概意思就是,私钥文件不能被其他人所访问。可能考虑到如果被别人获取到,就可能对服务器安全造成影响,所以需要从新设置下秘钥文件的权限

    • 重新设置秘钥文件权限:chmod 600 server-key.txt,取消其他用户Read权限
    • 但是,使用ssh name@domain形式还是没法直接登入;追其原因,发现因为不是本地直接生成的秘钥对;
    • 所以需要使用ssh-add -K ~/.ssh/xxx.txt[下载公钥文件]-K表示永久存储式,如果不使用者每次开机后需要重新ssh-add),就像是本地生成秘钥对然后部署到服务器需要将秘钥追加到ssh认证文件一个道理;
    • ssh name@domain可以正常免密登陆啦!(配置这种形式登陆后面pm2需要使用)
  • 配置快捷登录(附加)
    1. 进入ssh目录:cd ~/.ssh
    2. 创建config文件: touch config
    3. 进入config配置文件配置:vi config
        Host            lwh            #快捷别名
        HostName        host           #ssh服务器ip或domain
        Port            port           #ssh服务器端口,默认为22
        User            root           #ssh服务器用户名
        IdentityFile    ~/.ssh/server-key.txt    #下载的私钥文件
    4. :wq!保存退出
    5. 完成后可以直接使用:ssh lwh 登陆

在Github上添加Deploy Keys

  1. 服务器生成秘钥
# 生成ssh key
ssh-keygen -t rsa

# 查看公钥内容
cat ~/.ssh/id_rsa.pub
  • 复制秘钥内容,添加到Github上对应的项目仓库Settings下的Deploy keys
  • 配置Deploy keys,使得服务器可以通过ssh拉取项目仓库;

配置pm2

module.exports = {
  apps: [
    {
      name: 'back-Api',      //应用名
      script: './server/start.js',   //应用文件位置
      env: {
        //PM2_SERVE_PATH: "./apidoc",    //静态服务路径
        PM2_SERVE_PORT: 8080,   //静态服务器访问端口
        NODE_ENV: 'development' //启动默认模式
      },
      env_production : {
        PM2_SERVE_PORT: 8080,
        NODE_ENV: 'production'  //使用production模式 pm2 start ecosystem.config.js --env production
      },
      instances:"max",          //将应用程序分布在所有CPU核心上,可以是整数或负数
      instance_var: "INSTANCE_ID",
      exec_mode: "cluster",
      min_uptime: "30s",
      max_restarts: 10,
      //cron_restart: "40",
      watch:[
        "server",
      ],  //监听模式,不能单纯的设置为true,易导致无限重启,因为日志文件在变化,需要排除对其的监听
      merge_logs: true,         //集群情况下,可以合并日志
    }
  ],
  deploy: {
      production : {
        //配置没法提供密码,所以前面需要配置ssh免密码登录服务器
        user: 'root',                      //ssh 登陆服务器用户名
        host: '100.12.102.198',              //ssh 地址服务器domain/IP
        ref: 'origin/master',             //Git远程/分支
        repo: 'git@github.com',         //git地址使用ssh地址
        path: '/liwenhui/www',       //项目存放服务器文件路径
        "post-deploy": 'npm install && pm2 reload ecosystem.config.js --env production'  //部署后的动作
      }
  }
};

开始部署

  • 开始部署

    pm2 deploy ecosystem.config.js production
  • 报错

    appledeMBP:back-server-api apple$ pm2 deploy ecosystem.config.js production
    --> Deploying to production environment
    --> on host 106.12.132.188
      ○ deploying origin/master
      ○ executing pre-deploy-local
      ○ hook pre-deploy
    bash: 第 0 行:cd: /lwh/www/source: 没有那个文件或目录
      ○ fetching updates
      ○ full fetch
    bash: 第 0 行:cd: /lwh/www/source: 没有那个文件或目录
    
      fetch failed
    
    Deploy failed
    1
  • 需要先初始化服务器应用:pm2 deploy ecosystem.config.js production setup
  • 然后:pm2 deploy ecosystem.config.js production

其他

  1. pm2日志配置使用详情使用pm2配置生产环境
  2. 本地连接远程mongodb配置服务器(CentOS)安装配置mongodb
“积跬步、行千里”—— 持续更新中~,喜欢的话留下个赞和关注哦!
查看原文

赞 30 收藏 21 评论 2

林水溶 赞了文章 · 8月28日

Using context cancellation in Go

原文地址:https://neojos.com/blog/2019/...

文章介绍最近工作中遇到的一个问题,其中50%以上的内容都是Go的源代码。剩下部分是自己的理解,如果有理解错误或探讨的地方,希望大家指正。

问题:针对同一个接口请求,绝大多数都可以正常处理,但却有零星的几请求老是处理失败,错误信息返回 context canceled。重放失败的请求,错误必现。

根据返回的错误信息,再结合我们工程中使用的golang.org/x/net/context/ctxhttp包。猜测可能是在请求处理过程中,异常调用了context 包的CancelFunc方法。同时,我们对比了失败请求和成功请求的区别,发现失败请求的Response.Body数据量非常大。

之后在Google上找到了问题的原因,还真是很容易被忽略,这里是文章的链接:Context cancellation flake。为了解决未来点进去404的悲剧,本文截取了其中的代码...

Code

代码核心逻辑:向某个地址发送Get请求,并打印响应内容。其中函数fetch用于发送请求,readBody用于读取响应。例子中处理请求的逻辑结构,跟我们项目中的完全一致。

fetch方法中使用了默认的http.DefaultClient作为http Client,而它自身是一个“零值”,并没有指定请求的超时时间。所以,例子中又通过context.WithTimeout对超时时间进行了设置。

代码中使用context.WithTimeout来取消请求,存在两种可能情况。第一种,处理的时间超过了指定的超时时间,程序返回deadlineExceededError错误,错误描述context deadline exceeded。另一种是手动调用CancelFunc方法取消执行,返回Canceled错误,描述信息context canceled

fetch代码的处理逻辑中,当程序返回http.Response时,会执行cancel()方法,用于标记请求被取消。如果在readBody没读取完返回的数据之前,context被cancel掉了,就会返回context canceled错误。侧面也反映了,关闭Context.Done()与读取http.Response是一个时间赛跑的过程…..

package main

import (
    "context"
    "io/ioutil"
    "log"
    "net/http"
    "time"

    "golang.org/x/net/context/ctxhttp"
)

func main() {
    req, err := http.NewRequest("GET", "https://swapi.co/api/people/1", nil)
    if err != nil {
        log.Fatal(err)
    }
    resp, err := fetch(req)
    if err != nil {
        log.Fatal(err)
    }
    log.Print(readBody(resp))
}

func fetch(req *http.Request) (*http.Response, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    return ctxhttp.Do(ctx, http.DefaultClient, req)
}

func readBody(resp *http.Response) (string, error) {
    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(b), err
}

问题的解决办法如下,作者也附带了Test Case。 请求包括发送请求和读取响应两部分,CancelFunc应该在请求被处理完成后调用。不然,就会发生上面遇到的问题。

In case it's still unclear, you need to wrap both the "do request" + "read body" inside the same cancellation context. The "defer cancel" should encompass both of them, sort of atomically, so the idea is to take it out of your fetch, one level up.

重现Bug

我们准备通过控制请求返回的内容,来验证我们的结论。我们在本地启动一个新服务,并对外提供一个接口,来替代上述代码中的请求地址。

代码如下,其中info接口实现了下载resource文件的功能。我们通过控制resource文件的大小,来控制返回response大小的目的。

package main

import (
    "github.com/gin-gonic/gin"
    "io/ioutil"
    "log"
)

func main() {
    router := gin.Default()
    router.GET("/info", func(c *gin.Context) {
        data, err := ioutil.ReadFile("./resource")
        if err != nil {
            log.Println("read file err:", err.Error())
            return
        }

        log.Println("send file resource")
        c.Writer.Write(data)
    })
  
    router.Run(":8000")
}

首先,我们向resource文件中写入大量的内容,重新执行上述代码。错误日志输出:2019/06/13 21:12:37 context canceled。确实重现了!

然后,将resource文件内容删除到只剩一行数据,请求又可以正常处理了。

req, err := http.NewRequest("GET", "http://127.0.0.1:8000/info", nil)

总结:上述错误代码的执行结果,依赖请求返回的数据量大小。

修正Bug

根据上述分析,我们对代码进行调整:将defer cancel()调整到程序读取完http.Response.Body之后执行。具体修改如下:

  1. fetch函数中,将cancel函数作为返回值,返回给调用方。
func fetch(req *http.Request) (context.CancelFunc, *http.Response, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    resp, err := ctxhttp.Do(ctx, http.DefaultClient, req)
    return cancel, resp, err
}
  1. readBody读取完数据之后,再调用cancel方法。
    cancel, resp, err := fetch(req)
    if err != nil {
        log.Fatal(err)
    }
    defer cancel()
    log.Print(readBody(resp))

跟预期的一样,再接口返回的数据量很大的情况下,请求也可以被正常处理。

三种错误类型

context deadline exceeded

我们将代码中context.WithTimeout的超时时间由5*time.Second调整为1*time.Millisecond。执行代码,输出错误日志:2019/06/13 21:29:11 context deadline exceeded

context canceled

参考上述代码。

net/http: request canceled

工作中常见的错误之一:net/http: request canceled (Client.Timeout exceeded while awaiting headers),这是由http Client设置的超时时间决定的。接下来我们重现一下这个error。

fetch方法中,我们声明一个自定义的client,并指定Timeout属性为time.Millisecond,来替换代码中默认的client

func fetch(req *http.Request) (context.CancelFunc, *http.Response, error) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    customClient := &http.Client{
        Timeout: time.Millisecond,
    }
    resp, err := ctxhttp.Do(ctx, customClient, req)
    return cancel, resp, err
}

程序执行输出:

2019/06/18 09:20:53 Get http://127.0.0.1:8000/info: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

如下是http.Client结构体中对Timeout的注释,它包括创建连接、请求跳转、读取响应的全部时间。

// Timeout specifies a time limit for requests made by this
// Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
//
// A Timeout of zero means no timeout.
//
// The Client cancels requests to the underlying Transport
// as if the Request's Context ended.
//
// For compatibility, the Client will also use the deprecated
// CancelRequest method on Transport if found. New
// RoundTripper implementations should use the Request's Context
// for cancelation instead of implementing CancelRequest.

context原理

下面是context的接口类型,因为Done()的注解很好的解释了context最本质的用法,所以,特意只将这部分贴出来。在for循环体内,执行每次循环时,使用select方法来监听Done()是否被关闭了。如果关闭了,就退出循环。在ctxhttp包内,也是通过这种用法来实现对请求的控制的。

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    // Done returns a channel that's closed when work done on behalf of this
    // context should be canceled. Done may return nil if this context can
    // never be canceled. Successive calls to Done return the same value.
    //
    // WithCancel arranges for Done to be closed when cancel is called;
    // WithDeadline arranges for Done to be closed when the deadline
    // expires; WithTimeout arranges for Done to be closed when the timeout
    // elapses.
    //
    // Done is provided for use in select statements:
    //
    //  // Stream generates values with DoSomething and sends them to out
    //  // until DoSomething returns an error or ctx.Done is closed.
    //  func Stream(ctx context.Context, out chan<- Value) error {
    //      for {
    //          v, err := DoSomething(ctx)
    //          if err != nil {
    //              return err
    //          }
    //          select {
    //          case <-ctx.Done():
    //              return ctx.Err()
    //          case out <- v:
    //          }
    //      }
    //  }
    //
    // See https://blog.golang.org/pipelines for more examples of how to use
    // a Done channel for cancelation.
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

因为有业务逻辑在监听context.Done(),所以,必然需要有逻辑来Close调这个Channel。而整个context包也围绕者两个方面提供了一些方法,包括启动定时器来关闭context.Done()。参考注释中提到的WithCancelWithDeadline以及WithTimeout

源代码

下面是用来获取cancelCtx的方法,我们可以了解到context内部被封装的三种类型,分别是cancelCtxtimerCtx以及valueCtx

// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    for {
        switch c := parent.(type) {
        case *cancelCtx:
            return c, true
        case *timerCtx:
            return &c.cancelCtx, true
        case *valueCtx:
            parent = c.Context
        default:
            return nil, false
        }
    }
}

查看这三种类型的声明,内部都封装了一个Context值,用来存储父Context。恰恰也是通过这个字段,将整个Context串了起来。其中timerCtx是基于cancelCtx做的扩展,在其基础上添加了计时的功能。另外,cancelCtx节点中的children用于保存它所有的子节点。

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}
}

接下来,我们了解一下,将一个新的child context节点挂到parent context的过程。

首先,程序判断parent的数据类型,如果是上述三种类型之一,且没有错误信息,直接将child存储到parnet.childrenmap结构中。

如果parnet不是上述类型之一,程序会启动一个Goroutine异步监听parent.Done()child.Done()是否被关闭。我的理解是,因为此时parent其实是backgroundtodo中的一种(我称它为顶级parnet),而它们内部都没有字段用于存储和child的关系。所以,在程序select中绑定了它们的对应关系。另外,一个顶级parent也只能有一个child,而这个child应该是上述三种类型中的一种。只有这种一对一的情况,当child.Done()被关闭的时候,整个select退出才是合理的。

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        return // parent is never canceled
    }
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

我们接着看一下WithCancelWithDeadline这两个方法。前者通过调用CancelFunc来取消。后者在此基础上,加了一个timer的定时触发取消机制。如果WithDeadline参数d本身就是一个过去的时间点,那么WithDeadlineWithCancel效果相同。

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

最后,我们以timerCtx类型为例,来看看cancel函数的具体实现。方法的调用过程是递归执行的,内部调用的是cancelCtxcancel方法。参数removeFromParent用来判断是否要从父节点中移除该节点。同时,如果计时器存在的话,要关闭计时器。

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        // Remove this timerCtx from its parent cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

具体到cancelCtx中的cancel方法,函数依次cancelchildren中存储的子节点。但我们发现,在for循环移除子节点的时候,removeFromParent参数值为false。我的理解是,子节点依赖的父节点都已经被移除了,子节点是否移除就不重要了。

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}

ctxhttp中的应用

发送request

上面的例子中,我们创建了一个顶级context.Background。在调用WithTimeout时,parent会创建一个异步的Goroutine用来进行监听Done是否已经被关闭。同时还会为新创建的context设置一个计时器timer,来计算到期时间。

ctx, cancel := context.WithTimeout(context.Background(), time.Second)

下面是发送请求的代码,可以看到这是一个for循环的过程,所以非常适合context的处理模型。另外,该方法中有我们上面描述的错误情况:net/http: request canceled。对于这种超时错误,我们可以通过判断error类型,以及timeout是否为true来判断。

一直到这里,我们还没有看到context的核心逻辑…...

func (c *Client) do(req *Request) (retres *Response, reterr error) {
  // 删除简化代码......
    for {
        reqs = append(reqs, req)
        var err error
        var didTimeout func() bool
        if resp, didTimeout, err = c.send(req, deadline); err != nil {
            // c.send() always closes req.Body
            reqBodyClosed = true
            if !deadline.IsZero() && didTimeout() {
                err = &httpError{
                    // TODO: early in cycle: s/Client.Timeout exceeded/timeout or context cancelation/
                    err:     err.Error() + " (Client.Timeout exceeded while awaiting headers)",
                    timeout: true,
                }
            }
            return nil, uerr(err)
        }

        var shouldRedirect bool
        redirectMethod, shouldRedirect, includeBody = redirectBehavior(req.Method, resp, reqs[0])
        if !shouldRedirect {
            return resp, nil
        }
    }
}

所有对context的处理,都是在Transport.roundTrip中实现的

// roundTrip implements a RoundTripper over HTTP.
func (t *Transport) roundTrip(req *Request) (*Response, error) {
    t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
    ctx := req.Context()
    trace := httptrace.ContextClientTrace(ctx)

    for {
        select {
        case <-ctx.Done():
            req.closeBody()
            return nil, ctx.Err()
        default:
        }

        // treq gets modified by roundTrip, so we need to recreate for each retry.
        treq := &transportRequest{Request: req, trace: trace}
        cm, err := t.connectMethodForRequest(treq)
    }
}

读取response

在从conn读取数据的时候,依旧对reqcontext做了判断。同时也可以看出,读取Response.Body的过程,就是不断从resc中读取数据的过程。

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
    // Write the request concurrently with waiting for a response,
    // in case the server decides to reply before reading our full
    // request body.
    startBytesWritten := pc.nwrite
    writeErrCh := make(chan error, 1)
    pc.writech <- writeRequest{req, writeErrCh, continueCh}

    resc := make(chan responseAndError)
    pc.reqch <- requestAndChan{
        req:        req.Request,
        ch:         resc,
        addedGzip:  requestedGzip,
        continueCh: continueCh,
        callerGone: gone,
    }

    var respHeaderTimer <-chan time.Time
    cancelChan := req.Request.Cancel
    ctxDoneChan := req.Context().Done()
    for {
        testHookWaitResLoop()
        select {
        case <-ctxDoneChan:
            pc.t.cancelRequest(req.Request, req.Context().Err())
            cancelChan = nil
            ctxDoneChan = nil
        }
    }
}

参考文章:

  1. Using context cancellation in Go
查看原文

赞 3 收藏 0 评论 0

林水溶 赞了文章 · 8月26日

linux 发行版 manjaro 安装指南

manjaro

Linux 历史

1994 年 3 月,Linux1.0 版正式发布,Marc Ewing 成立 Red Hat 软件公司,成为最著名的 Linux 经销商之一。早期 Linux 的引导管理程序(boot loader)使用 LILO(Linux Loader),早期的 LILO存在着一些难以容忍的缺陷,例如无法识别 1024 柱面以后的硬盘空间,后来的 GRUB(GRand Unified Bootloader)克服这些缺点,具有‘动态搜索内核文件’的功能,可以让用户在引导的时候,自行编辑引导设置系统文件,透过 ext2 或 ext3 文件系统中加载 Linux Kernel(GRUB通过不同的文件系统驱动可以识别几乎所有 Linux 支持的文件系统,因此可以使用很多文件系统来格式化内核文件所在的扇区,并不局限于 ext 文件系统)。

今天由 Linus Torvalds 带领下,众多开发共同参与开发和维护 Linux 内核。理查德·斯托曼领导的自由软件基金会,继续提供大量支持 Linux 内核的GNU组件。一些个人和企业开发的第三方的非GNU组件也提供对Linux 内核的支持,这些第三方组件包括大量的作品,有内核模块和用户应用程序和库等内容。Linux社区或企业都推出一些重要的 Linux 发行版,包括 Linux 内核、GNU 组件、非 GNU 组件,以及其他形式的软件包管理系统软件。

什么人适合 linux 系统

  1. 对 Linux 保持高敏感度的人(运维人员,后端开发...)
  2. 有 Geek 精神喜欢折腾的人。
  3. 对 Windows 生厌, 想要尝试新系统的人,不差钱可以上 MAC。

Linux 发行版之 Manjaro

Manjaro Linux 基于 Arch Linux,但拥有自己独立的软件仓库。Manjaro 的目标是让强大的 Arch 更方便用户使用,Manjaro 使用著名的 Pacman 且可以直接利用 AUR 上的資源。Manjaro 本身使用三个软件仓库:不稳定库,即含有那些不成熟的 Arch 包,这些包与 Arch 源有 1-2 天 的延后;测试库,每周同步一次,包含那些 Arch 不稳定源的包;以及稳定库,包含那些由开发团队确认稳定的软件。
Manjaro Linux 拥有开箱即用的多媒体支持、成熟的硬件识别软件,并支持多核 CPU。Manjaro 拥有命令行安装器和图形安装器。同时滚动更新也意味着用户无需通过重装系统或系统更新来更新自己的操作系统。软件包管理由 Pacman 处理,未来也计划提供一个 GUI 版本。Manjaro 有 32 位 和 64 位 的版本,且都与 Arch 兼容。可对其进行配置,选择是与使用稳定库的 Arch 同步(默认),或者是与不稳定的Arch 库同步。

Manjaro 软件库由自带的 BoxIt 工具管理,BoxIt 类似git
Manjaro 对显卡驱动的兼容性高,可自主选择安装开源驱动或者闭源驱动。

XFCE

XFCE 是一个轻量级的桌面环境,,它被广泛的运用于各种 UNIX 中,它非常的小巧,运行程序很快,节省系统资源。XFCE 融合了 UNIX 哲学中的“模块化”和“可重用性”这两个极为重要的思想。它包含了很多的组件,而正是这些组件构成了整个 XFCE 的强大。它轻量及的特点,也是拯救家中旧电脑的必备神器。

KDE

由德国人创立而开发的桌面,据说还受到德国政府的资助。KDE 桌 面默认的主题与布局接近于 Windows Vista,因此 Windows 用户很容易熟悉这个桌面。不过,KDE 桌面具有强大的自定义功能,可以根据自己需要来折腾自己的桌面。

GNOME

由 GUN 软件计划组织创立而开发的桌面,有自己的一套完整的风格,支持扩展插件与主题更换,同时也为越来越多的触摸设备做出优化。高颜值的 UI 设计,也在这个颜值即正义的时代,迎来了越来越多人的青睐。

Manjaro ISO 下载地址

Manjaro 下载地址

WINDOWS 下 U盘启动工具制作(虚拟机安装忽略该章节)

U盘安装Linux必备软件 Rufus 下载
打开下载好的 refus 软件,相关设置如下,注意点了开始之后,选择DD模式写入,ISO 模式写入的启动不了。
Rufus

Rufus-check

启动安装

将制作好的 U盘,插入需要安装系统电脑,进入 BIOS 关闭安全启动,保存,重启,按F9,进入启动选项,选择 uefi usb3.0 的启动项


关于 driver 选项有连个 free 和 nonfree, free 为社区提供的开源版, nonfree 为 AMD 或 Nvidia 厂商提供的驱动版本,也意味这有更好的性能和更少的 bug,因而建议选择 nofree 选项。
在所有选项配置完毕后,选择 Boot: Manjaro x86_64 gnome 进入安装界面

launch-install

点击 Launch install 启动安装程序

选择语言

选择安装语言

选择位置

选择未知

选择键盘排布方式

选择键盘排布方式

分区

主分区、扩展分区、逻辑分区 关系

磁盘分区有三种: 主分区,扩展分区,逻辑分区。
通常情况下,一个硬盘中最多能够分割四个主分区。因为硬盘中分区表的大小只有64Bytes,而分割一个分区就需要利用16Bytes空间来存储这个分区的相关信息。由于这个分区表大小的限制,硬盘之能够分给为四个主分区。如果此时一块硬盘有120个G,而管理员划分了4个主分区,每个主分区的空间为20个G。那么总共才用去了80G的空间。这块硬盘剩余的40G空间就将无法使用。这显然浪费了硬盘的空间。
为了突破这最多四个主分区的限制,Linux系统引入了扩展分区的概念。即管理员可以把其中一个主分区设置为扩展分区(注意只能够使用一个扩展分区)来进行扩充。而在扩充分区下,又可以建立多个逻辑分区。也就是说,扩展分区是无法直接使用的,必须在细分成逻辑分区才可以用来存储数据。通常情况下,逻辑分区的起始位置及结束位置记录在每个逻辑分区的第一个扇区,这也叫做扩展分区表。在扩展分区下,系统管理员可以根据实际情况建立多个逻辑分区,将一个扩展分区划割成多个区域来使用。

分区说明

  • /boot 存放系统启动文件 800M-2G(ext4 文件系统)
  • swap 交换空间,交换空间大小与计算机内存内存存在关联。内存在8 G 以下设置 内存容量 * 2,8 G 以上设置和内存同等大小即可。

    • 当内存不够用时,系统会将长时间没有运行的程序的缓存从内存中写入 swap 分区中,并释放该程序所占用的内存给其他程序使用
    • Linux 电源管理有休眠模式,系统会将内存中程序运行状态存储在 swap 中然后设备进入完全断电状态,当下次启动计算机时系统会从 swap 恢复休眠前的计算机运行状态。
  • / 根目录, 25G 左右 (etx4 文件系统)
  • /home (etx4 文件系统) Linux 系统中用户的家文件,此目录空间越大越好。

新建分区表

新建分区表

点击新建分区表,选择主引导记录,点击 OK 完成分区表创建。

创建分区

  1. 选择空闲空间
  2. 点击创建进行分区创建

创建 boot 分区

创建 swap (交换空间)

创建根分区

创建 home 分区

设置用户

选择 office 套件安装

建议不安装,安装完系统后可以用 wps for linux 替代

安装

摘要部分安装前确认步骤,如果没有需要调整的配置项,点击安装进入系统安装过程,安装完成后重启拔掉 U盘,进入系统就可以了。

安装进度在 95% 左右,有时会出现卡住的情况,是因为国内连接外国网络速度过慢导致的,可以断开网络跳过。

安装后配置

更换源

$ sudo pacman -Syy
$ sudo pacman-mirrors -i -c China -m rank
$ sudo pacman -Syyu

使用root权限编辑/etc/pacman.conf增加以下内容

[archlinuxcn]
SigLevel = Optional TrustedOnly
Server =https://mirrors.ustc.edu.cn/archlinuxcn/$arch

然后执行

$ sudo pacman -Syy && sudo pacman -S archlinuxcn-keyring

安装中文输入法

sudo pacman -S fcitx-lilydjwg-git
sudo pacman -S fcitx-sogoupinyin
sudo pacman -S fcitx-im         # 全部安装

编辑~/.xprofile文件,在文件末尾增加以下内容

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS="@im=fcitx"

安装 中文字体

sudo pacman -S ttf-roboto noto-fonts ttf-dejavu
# 文泉驿
sudo pacman -S wqy-bitmapfont wqy-microhei wqy-microhei-lite wqy-zenhei
# 思源字体
sudo pacman -S noto-fonts-cjk adobe-source-han-sans-cn-fonts adobe-source-han-serif-cn-fonts

安装 wps

sudo pacman -S wps-office
sudo pacman -S ttf-wps-fonts

AUR助手(以防官方仓库没有想要的软件)

sudo pacman -S yay

安装 chrome 浏览器

sudo pacman -S google-chrome

总结

关于 manjaro 安装已经告一段落,参考上述操作,已经搭建出一个可用的 Linux 系统。值得注意的是,Linux 系统与 Windows 系统不同,在 Linux 中,你可以拥有很高的权限,即便是系统核心文件也是可以进行操作的,因此如果你是刚刚接触 Linux 的用户,养成及时备份的习惯是非常明智的选择。最后, 欢迎入坑,我相信在以后 Linux 使用中你一定是痛并快乐着的,坚持下来,时间会给正确的选择以回报。

博客地址

查看原文

赞 3 收藏 2 评论 0

林水溶 赞了文章 · 8月9日

【Go】高效截取字符串的一些思考

原文链接:https://blog.thinkeridea.com/...

最近我在 Go Forum 中发现了 String size of 20 character 的问题,“hollowaykeanho” 给出了相关的答案,而我从中发现了截取字符串的方案并非最理想的方法,因此做了一系列实验并获得高效截取字符串的方法,这篇文章将逐步讲解我实践的过程。

字节切片截取

这正是 “hollowaykeanho” 给出的第一个方案,我想也是很多人想到的第一个方案,利用 go 的内置切片语法截取字符串:

s := "abcdef"
fmt.Println(s[1:4])

我们很快就了解到这是按字节截取,在处理 ASCII 单字节字符串截取,没有什么比这更完美的方案了,中文往往占多个字节,在 utf8 编码中是3个字节,如下程序我们将获得乱码数据:

s := "Go 语言"
fmt.Println(s[1:4])

杀手锏 - 类型转换 []rune

hollowaykeanho” 给出的第二个方案就是将字符串转换为 []rune,然后按切片语法截取,再把结果转成字符串。

s := "Go 语言"
rs := []rune(s)
fmt.Println(strings(rs[1:4]))

首先我们得到了正确的结果,这是最大的进步。不过我对类型转换一直比较谨慎,我担心它的性能问题,因此我尝试在搜索引擎和各大论坛查找答案,但是我得到最多的还是这个方案,似乎这已经是唯一的解。

我尝试写个性能测试评测它的性能:

package benchmark

import (
    "testing"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRunes(s string, length int) string {
    if utf8.RuneCountInString(s) > length {
        rs := []rune(s)
        return string(rs[:length])
    }

    return s
}

func BenchmarkSubStrRunes(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SubStrRunes(benchmarkSubString, benchmarkSubStringLength)
    }
}

我得到了让我有些吃惊的结果:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRunes-8            872253              1363 ns/op             336 B/op          2 allocs/op
PASS
ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     2.120s

对 69 个的字符串截取前 20 个字符需要大概 1.3 微秒,这极大的超出了我的心里预期,我发现因为类型转换带来了内存分配,这产生了一个新的字符串,并且类型转换需要大量的计算。

救命稻草 - utf8.DecodeRuneInString

我想改善类型转换带来的额外运算和内存分配,我仔细的梳理了一遍 strings 包,发现并没有相关的工具,这时我想到了 utf8 包,它提供了多字节计算相关的工具,实话说我对它并不熟悉,或者说没有主动(直接)使用过它,我查看了它所有的文档发现 utf8.DecodeRuneInString 函数可以转换单个字符,并给出字符占用字节的数量,我尝试了如此下的实验:

package benchmark

import (
    "testing"
    "unicode/utf8"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrDecodeRuneInString(s string, length int) string {
    var size, n int
    for i := 0; i < length && n < len(s); i++ {
        _, size = utf8.DecodeRuneInString(s[n:])
        n += size
    }

    return s[:n]
}

func BenchmarkSubStrDecodeRuneInString(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SubStrDecodeRuneInString(benchmarkSubString, benchmarkSubStringLength)
    }
}

运行它之后我得到了令我惊喜的结果:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrDecodeRuneInString-8     10774401               105 ns/op               0 B/op          0 allocs/op
PASS
ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.250s

[]rune 类型转换效率提升了 13倍,消除了内存分配,它的确令人激动和兴奋,我迫不及待的回复了 “hollowaykeanho” 告诉他我发现了一个更好的方法,并提供了相关的性能测试。

我有些小激动,兴奋的浏览着论坛里各种有趣的问题,在查看一个问题的帮助时 (忘记是哪个问题了-_-||) ,我惊奇的发现了另一个思路。

良药不一定苦 - range 字符串迭代

许多人似乎遗忘了 range 是按字符迭代的,并非字节。使用 range 迭代字符串时返回字符起始索引和对应的字符,我立刻尝试利用这个特性编写了如下用例:

package benchmark

import (
    "testing"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRange(s string, length int) string {
    var n, i int
    for i = range s {
        if n == length {
            break
        }

        n++
    }

    return s[:i]
}

func BenchmarkSubStrRange(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SubStrRange(benchmarkSubString, benchmarkSubStringLength)
    }
}

我尝试运行它,这似乎有着无穷的魔力,结果并没有令我失望。

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRange-8          12354991                91.3 ns/op             0 B/op          0 allocs/op
PASS
ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.233s

它仅仅提升了13%,但它足够的简单和易于理解,这似乎就是我苦苦寻找的那味良药。

如果你以为这就结束了,不、这对我来只是探索的开始。

终极时刻 - 自己造轮子

喝了 range 那碗甜的腻人的良药,我似乎冷静下来了,我需要造一个轮子,它需要更易用,更高效。

于是乎我仔细观察了两个优化方案,它们似乎都是为了查找截取指定长度字符的索引位置,如果我可以提供一个这样的方法,是否就可以提供用户一个简单的截取实现 s[:strIndex(20)] ,这个想法萌芽之后我就无法再度摆脱,我苦苦思索两天来如何来提供易于使用的接口。

之后我创造了 exutf8.RuneIndexInStringexutf8.RuneIndex 方法,分别用来计算字符串和字节切片中指定字符数量结束的索引位置。

我用 exutf8.RuneIndexInString 实现了一个字符串截取测试:

package benchmark

import (
    "testing"
    "unicode/utf8"

    "github.com/thinkeridea/go-extend/exunicode/exutf8"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRuneIndexInString(s string, length int) string {
    n, _ := exutf8.RuneIndexInString(s, length)
    return s[:n]
}

func BenchmarkSubStrRuneIndexInString(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SubStrRuneIndexInString(benchmarkSubString, benchmarkSubStringLength)
    }
}

尝试运行它,我对结果感到十分欣慰:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRuneIndexInString-8      13546849                82.4 ns/op             0 B/op          0 allocs/op
PASS
ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.213s

性能较 range 提升了 10%,让我很欣慰可以再次获得新的提升,这证明它是有效的。

它足够的高效,但是却不够易用,我截取字符串需要两行代码,如果我想截取 10~20之间的字符就需要4行代码,这并不是用户易于使用的接口,我参考了其它语言的 sub_string 方法,我想我应该也设计一个这个样的接口给用户。

exutf8.RuneSubStringexutf8.RuneSub 是我认真思索后编写的方法:

func RuneSubString(s string, start, length int) string

它有三个参数:

  • s : 输入的字符串
  • start : 开始截取的位置,如果 start 是非负数,返回的字符串将从 string 的 start 位置开始,从 0 开始计算。例如,在字符串 “abcdef” 中,在位置 0 的字符是 “a”,位置 2 的字符串是 “c” 等等。 如果 start 是负数,返回的字符串将从 string 结尾处向前数第 start 个字符开始。 如果 string 的长度小于 start,将返回空字符串。
  • length:截取的长度,如果提供了正数的 length,返回的字符串将从 start 处开始最多包括 length 个字符(取决于 string 的长度)。 如果提供了负数的 length,那么 string 末尾处的 length 个字符将会被省略(若 start 是负数则从字符串尾部算起)。如果 start 不在这段文本中,那么将返回空字符串。 如果提供了值为 0 的 length,返回的子字符串将从 start 位置开始直到字符串结尾。

我为他们提供了别名,根据使用习惯大家更倾向去 strings 包寻找这类问题的解决方法,我创建了exstrings.SubStringexbytes.Sub 作为更易检索到的别名方法。

最后我需要再做一个性能测试,确保它的性能:

package benchmark

import (
    "testing"

    "github.com/thinkeridea/go-extend/exunicode/exutf8"
)

var benchmarkSubString = "Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。为了方便搜索和识别,有时会将其称为Golang。"
var benchmarkSubStringLength = 20

func SubStrRuneSubString(s string, length int) string {
    return exutf8.RuneSubString(s, 0, length)
}

func BenchmarkSubStrRuneSubString(b *testing.B) {
    for i := 0; i < b.N; i++ {
        SubStrRuneSubString(benchmarkSubString, benchmarkSubStringLength)
    }
}

运行它,不会让我失望:

goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark
BenchmarkSubStrRuneSubString-8          13309082                83.9 ns/op             0 B/op          0 allocs/op
PASS
ok      github.com/thinkeridea/go-extend/exunicode/exutf8/benchmark     1.215s

虽然相较 exutf8.RuneIndexInString 有所下降,但它提供了易于交互和使用的接口,我认为这应该是最实用的方案,如果你追求极致仍然可以使用 exutf8.RuneIndexInString,它依然是最快的方案。

总结

当看到有疑问的代码,即使它十分的简单,依然值得深究,并不停的探索它,这并不枯燥和乏味,反而会有极多收获。

从起初 []rune 类型转换到最后自己造轮子,不仅得到了16倍的性能提升,我还学习了utf8包、加深了range 遍历字符串的特性 以及为 go-extend 仓库收录了多个实用高效的解决方案,让更多 go-extend 的用户得到成果。

go-extend 是一个收录实用、高效方法的仓库,读者们如果好的函数和通用高效的解决方案,期待你们不吝啬给我发送 Pull request,你也可以使用这个仓库加快功能实现及提升性能。

转载:

本文作者: 戚银(thinkeridea

本文链接: https://blog.thinkeridea.com/201910/go/efficient_string_truncation.html

版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处!

查看原文

赞 1 收藏 0 评论 1

林水溶 赞了文章 · 7月30日

ferret 爬取动态网页

动态网页常用js来加载数据,使用声明式语言fql,可轻松获取点击,下拉等一系列需要交互后渲染的页面数据。
够浪的ferret足够简单, 让会sql,了解css,知道点go的同学,很方便的用编码或命令行形式抓取动态网页内容。

selenium

selenium真心好用,但太重。夸它好用,是因为不像scray一个页面情况没考虑到,它就给挂了,给定目标网站用户让怎么跑就怎么干。说它重是因为耗资源,若仅作爬虫抓取,为什么要开一个浏览器,又不是真要用界面。

cdp

chrome debug protocol 谷歌浏览器调试协议,简称cdp

用过chrome浏览器的F12,也就是devtools,其实这是一个web应用。当你使用devtools的时候,浏览器本身会作为一个服务端,而你看到的浏览器调试工具界面,其实只是一个前端应用,在这中间通信的,就是基于websocket的cdp,一个让devtools和浏览器内核交换数据的通道。

selenium -> webdriver -> chromedriver  -> cdp --> fql
  • cdp获取页面网络数据
  • 用cdp获取页面加载时间
  • 用cdp拿到自动化测试后的js覆盖率数据并展示
  • 通过远程机器调试无头浏览器

fql

ferret特定领域的声明式编程语言fql,简单直观。
输入关键字 --> 点击搜索按钮 --> 用css 选择器选择节点 --> 迭代处理内容 --> 返回内容

// baidu.fql

LET bd = DOCUMENT("https://www.baidu.com/", {
    driver: "cdp",
    userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.87 Safari/537.36"
})
// 交互操作
INPUT(bd, 'input[name="wd"]', @wd)
CLICK(bd, '#su')
WAIT(RAND(1000))

FOR result IN ELEMENTS(bd, '#content_left')
    LIMIT 3
    LET title = INNER_TEXT(result, 'h3')
    LET description = INNER_TEXT(result, 'div.c-abstract')
    RETURN {  title, description }

上述 @wd 为输入型参数,同sql中的位置参数一个道理。
{ title, description } 是fql语法的一个特定,同es6中的对象解构赋值
RETURN 关键字在fql与php,python 中的生成器 yield一样的意义,只是带值返回,可以重新返场

usage

  1. 安装ferret,建议使用go1.13以上版本,开启国内镜像代理,当然你本机也得装chrome浏览器
go get github.com/MontFerret/ferret
  1. 开启chrome的cdp服务实例,记住使用无头模式
chrome.exe --remote-debugging-port=9222 --headless
  1. 导入baidu.fql,ferret使用的默认端口是9222
D:\code-base>ferret --param=wd:\"golang社区\"  >> baidu.txt
  1. 打开查看结果,类似于下面这样的格式
[{"description":"Go语言中文网,中国 Golang 社区,Go语言学习园地,致力于构建完善的 Golang 中文社区,Go语言爱好者的学习家园。分享 Go 语言知识,交流使用经验","title":"首页- Go语言中文网 - Golang中文社区"}]

other

ferret 目前还处于开发阶段,但其不仅仅这些,比如cookie,快照截图,图片下载,代理等等新功能已经处于测试。目前版本是v0.9.0, 稳定版本很快发布。上面示例仅展示了命令行形式,事实上它还可以嵌入到golang编码,产生各种好玩的用法。

查看原文

赞 1 收藏 0 评论 0

林水溶 赞了回答 · 7月26日

解决如何将golang[]byte转换为字符串

ASCII编码不是都可见的。

package main

import (
    "fmt"
)

func main() {
    data := [4]byte{0x31, 0x32, 0x33, 0x34}
    str := string(data[:])
    fmt.Println(str)
}

关注 9 回答 5

林水溶 赞了文章 · 7月21日

禁止蒙层底部页面跟随滚动

场景概述

弹窗是一种常见的交互方式,而蒙层是弹窗必不可少的元素,用于隔断页面与弹窗区块,暂时阻断页面的交互。但是,在蒙层元素中滑动的时候,滑到内容的尽头时,再继续滑动,蒙层底部的页面会开始滚动,显然这不是我们想要的效果,因此需要阻止这种行为。

那么,如何阻止呢?请看以下分析:

方案分析

方案一

  • 打开蒙层时,给body添加样式:
overflow: hidden;
height: 100%;

在某些机型下,你可能还需要给根节点添加样式:

overflow: hidden;
  • 关闭蒙层时,移除以上样式。

优点:
简单方便,只需添加css样式,没有复杂的逻辑。

缺点:
兼容性不好,适用于pc,移动端就尴尬了。
部分安卓机型以及safari中,无法无法阻止底部页面滚动。

如果需要应用于移动端,那么你可能需要方案二。

方案二

就是利用移动端的touch事件,来阻止默认行为(这里可以理解为页面滚动就是默认行为)。

// node为蒙层容器dom节点
node.addEventListener('touchstart', e => {
  e.preventDefault()
}, false)

简单粗暴,滚动时底部页面也无法动弹了。假如你的蒙层内容不会有滚动条,那么上述方法prefect。

但是,最怕空气突然安静,假如蒙层内容有滚动条的话,那么它再也无法动弹了。因此我们需要写一些js逻辑来判断要不要阻止默认行为,复杂程度明显增加。

具体思路:判定蒙层内容是否滚动到尽头,是则阻止默认行为,反之任它横行。


Tip:这里我发现了一个小技巧,可以省略不少代码。在一次滑动中,若蒙层内容可以滚动,则蒙层内容滚动,过程中即使蒙层内容已滚至尽头,只要不松手(可以理解为touchend事件触发前),继续滑动时页面内容不会滚动,此时若松手再继续滚动,则页面内容会滚动。利用这一个小技巧,我们可以精简优化我们的代码逻辑。

示例代码如下:

<body>
  <div class="page">
    <!-- 这里多添加一些,直至出现滚动条 -->
    <p>页面</p>
    <p>页面</p>
    <button class="btn">打开蒙层</button>
    <p>页面</p>
  </div>
  <div class="container">
    <div class="layer"></div>
    <div class="content">
      <!-- 这里多添加一些,直至出现滚动条 -->
      <p>蒙层</p>
      <p>蒙层</p>
      <p>蒙层</p>
    </div>
  </div>
</body>
body {
  margin: 0;
  padding: 20px;
}

.btn {
  border: none;
  outline: none;
  font-size: inherit;
  border-radius: 4px;
  padding: 1em;
  width: 100%;
  margin: 1em 0;
  color: #fff;
  background-color: #ff5777;
}

.container {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1001;
  display: none;
}

.layer {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 1;
  background-color: rgba(0, 0, 0, .3);
}

.content {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 50%;
  z-index: 2;
  background-color: #f6f6f6;
  overflow-y: auto;
}
const btnNode = document.querySelector('.btn')
const containerNode = document.querySelector('.container')
const layerNode = document.querySelector('.layer')
const contentNode = document.querySelector('.content')
let startY = 0 // 记录开始滑动的坐标,用于判断滑动方向
let status = 0 // 0:未开始,1:已开始,2:滑动中

// 打开蒙层
btnNode.addEventListener('click', () => {
  containerNode.style.display = 'block'
}, false)

// 蒙层部分始终阻止默认行为
layerNode.addEventListener('touchstart', e => {
  e.preventDefault()
}, false)

// 核心部分
contentNode.addEventListener('touchstart', e => {
  status = 1
  startY = e.targetTouches[0].pageY
}, false)

contentNode.addEventListener('touchmove', e => {
  // 判定一次就够了
  if (status !== 1) return

  status = 2

  let t = e.target || e.srcElement
  let py = e.targetTouches[0].pageY
  let ch = t.clientHeight // 内容可视高度
  let sh = t.scrollHeight // 内容滚动高度
  let st = t.scrollTop // 当前滚动高度

  // 已经到头部尽头了还要向上滑动,阻止它
  if (st === 0 && startY < py) {
    e.preventDefault()
  }

  // 已经到低部尽头了还要向下滑动,阻止它
  if ((st === sh - ch) && startY > py) {
    e.preventDefault()
  }
}, false)

contentNode.addEventListener('touchend', e => {
  status = 0
}, false)

问题虽然是解决了,但是回头来看,复杂程度和代码量明显增加了一个梯度。
本着简单方便的原则,我们是不是还可以探索其他的方案呢?

既然touch事件判定比较复杂,何不跳出这个框框,另辟蹊径,探索更加合适的方案。
于是,便有了我们的方案三。

方案三

来讲讲我的思路,既然我们要阻止页面滚动,那么何不将其固定在视窗(即position: fixed),这样它就无法滚动了,当蒙层关闭时再释放。
当然还有一些细节要考虑,将页面固定视窗后,内容会回头最顶端,这里我们需要记录一下,同步top值。

示例代码:

let bodyEl = document.body
let top = 0

function stopBodyScroll (isFixed) {
  if (isFixed) {
    top = window.scrollY

    bodyEl.style.position = 'fixed'
    bodyEl.style.top = -top + 'px'
  } else {
    bodyEl.style.position = ''
    bodyEl.style.top = ''

    window.scrollTo(0, top) // 回到原先的top
  }
}

思考总结

  • 若应用场景是pc,推荐方案一,真的是不要太方便
  • 若应用场景是h5,你可以采用方案二,但是我建议你采用方案三
  • 若应用场景是全平台,那么方案三你不容错过

本文到这里也即将结束了,在这里我强烈推荐一下方案三,原因在于简单、方便、兼容性好,一次封装,永久受用。

查看原文

赞 52 收藏 118 评论 16

林水溶 收藏了文章 · 7月17日

解析移动端滚动穿透

滚动穿透在移动端开发中是一个很常见的问题,产生诡异的交互行为,影响用户体验,同时也让我们的产品看起来不那么“专业”。虽然不少产品选择容忍了这样的行为,但是作为追求极致的工程师,应该去了解为什么会产生以及如何去解决。

什么是滚动穿透

移动端开发中避免不了会在页面上进行弹窗、加浮层等这种操作。一个最常见的场景就是整个页面上有一个遮罩层,上面画着各种各样的东西,具体是什么就不讨论。实现这样一个遮罩层可难不住即使是一个刚开始写前端的小白。但是这里有一个问题就是如果不对遮罩层做任何处理,当用户在上面滑动时会发现遮罩层下方的页面居然也在滚动,这就很 interesting 了。就如下面的例子,一个名为mask长宽都是屏幕大小的遮罩层,我们在上面滑动时,下面的内容也在跟随滚动,即滚动“穿透”到了下方,这就是滚动穿透(scroll-chaining)。

scroll-chaining

上方 demo 的遮罩层底部是一个逐渐变蓝的内容容器,但是滑动上面遮罩层时,底部也跟随滚动了,这只是一个最简单的场景,后面我们会讨论更复杂的情况。

为什么会出现

目前 Google 上搜滚动穿透会出现一大堆教你如何解决的文章,但是它们都是在告诉你怎么解决怎么 hack 掉这种交互异常。并没有告诉读者为什么会产生这种行为,甚至认为这是浏览器的一个 bug。对于我来说这个是难以理解的,因为就算解决了问题,其实也并不知道问题的根本是怎样的。

认知误区

有一个误区就是我们设置了一个和屏幕一样大小的遮罩层,盖住了下面的内容,按理说我们应该能屏蔽掉下方的所有事件也就是说不可能触发下面内容的滚动。那么我们就去看一下规范,什么时候会触发滚动。

// https://www.w3.org/TR/2016/WD...
When asked to run the scroll steps for a Document doc, run these steps:

  1. For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps:
  • If target is a Document, fire an event named scroll that bubbles at target.
  • Otherwise, fire an event named scroll at target.
  1. Empty doc’s pending scroll event targets.

通过规范我们可以明白的 2 点是,首先滚动的 target 可以是 document 和里面的 element。其次,在 element 上的 scroll 事件是不冒泡的,document 上的 scroll 事件冒泡。

所以如果我们想通过在 scroll 的节点上去阻止它的滚动事件冒泡来解决问题是不可行的!因为它根本就不冒泡,无法触及 dom tree 的父节点何谈触发它们的滚动。

那么问题是怎么产生的呢,其实规范只说明了浏览器应该在什么时候滚动,而没有说不应该在什么时候滚动。浏览器正确实现了规范,滚动穿透也并不是浏览器的 bug。我们在页面上加了一个遮罩层并不会影响 document 滚动事件的产生。根据规范,如果目标节点是不能滚动的那么将会尝试 document 上的滚动,也就是说遮罩层虽然不可滚动,但是这个时候浏览器会去触发 document 的滚动从而导致了下方文档的滚动。也就是说如果 document 也不可滚动了,也就不会有这个问题了。这就引出了解决问题的第一种方案:把 document 设置为 overflow hidden。

怎么解决

overflow hidden

既然滚动是由于文档超出了一屏产生的,那么就让它超出部分 hidden 掉就好了,所以在遮罩层被弹出的时候可以给 html 和 body 标签设置一个 class:

.modal--open {
  height: 100%;
  overflow: hidden;
}

这样文档高度和屏幕一样,自然不会存在滚动了。但是这样又会引来一个新的问题,如果文档之前存在一定的滚动高度那么这样设置后会导致之前的滚动距离失效,文档滚回了最顶部,这样一来岂不是得不偿失?但是我们可以在加 class 之前记录好之前的滚动具体然后在关闭遮罩层的时候把滚动距离设置回来。这样问题是可以得到解决的实现成本也很低,但是如果遮罩层是透明的,弹出后用户仍然会看到丢失距离后的下方页面,显然这样并不是完美的方案。

prevent touch event

还有一种办法就是我们直接阻止掉遮罩层和弹窗的 touch event 这样就不会在移动端触发 scroll 事件了。但是在 PC 上没有 touch 事件, scroll 事件仍然可以被触发,原因上面我们也说过,scroll 事件是滚动它能滚动的元素。这里我们解决的是移动端的问题,例子如下:

scroll-chaining

<div id="app">
  <div class="mask">mask</div>
  <div class="dialog">dialog</div>
</div>
const $mask = document.querySelector(".mask");
const $dialog = document.querySelector(".dialog");
const preventTouchMove = $el => {
  $el.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    { passive: false }
  );
};
preventTouchMove($mask);
preventTouchMove($dialog);

上面我们通过 prevent touchmove 来阻止页面的触摸事件从而禁止进一步的页面滚动,在 addEventListener 最后一个参数我们将 passive 显示的设置为 false,这里是有用意的。关于 passive event listener 这里又是一个话题我们就不展开说了,就是浏览器为了优化滚动性能做的一些改进,具体可以看 网站使用被动事件侦听器以提升滚动性能,由于在 Chrome 56 开始将会默认开启 passive event listener 所以不能直接在 touch 事件中使用 preventDefault,需要先将 passive 选项设置为 false 才行。

这里我们解决了在页面上普通弹窗的问题,但是如果 dialog 的内容是可以滚动的,这样将其阻止了 touch 事件将会导致其内容也不能正常滚动,所以还有要进一步优化才行。

进一步优化

现在的场景是我们的弹窗是可以滚动的,所以不能再直接将其 touch 事件阻止,去掉后我们发现会产生新的问题。遮罩层被阻止了 touch 事件不能使下方滚动,但是弹出层 modal 这里内容是可滚动的,在 touch modal 时能正常滚动里面的内容。但是 modal 滚动到最上方或者最下方时仍然能触发 document 的滚动,效果如下:

scroll-chainging

我们看到当 modal 滚动在顶部时仍然能拖动下方 document。这样我们只能监听用户手势,如果 modal 已经滑动到了底部或者顶部且还要往上或者下滑动则也要 prevent modal 的 touch 事件。简单实现一个 fuckScrollChaining 函数:

function fuckScrollChaining($mask, $modal) {
  const listenerOpts = { passive: false };
  $mask.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    listenerOpts
  );
  const modalHeight = $modal.clientHeight;
  const modalScrollHeight = $modal.scrollHeight;
  let startY = 0;

  $modal.addEventListener("touchstart", e => {
    startY = e.touches[0].pageY;
  });
  $modal.addEventListener(
    "touchmove",
    e => {
      let endY = e.touches[0].pageY;
      let delta = endY - startY;

      if (
        ($modal.scrollTop === 0 && delta > 0) ||
        ($modal.scrollTop + modalHeight === modalScrollHeight && delta < 0)
      ) {
        e.preventDefault();
      }
    },
    listenerOpts
  );
}

完整实现在 这里,至此无论弹出层内容是否可滚动都不会导致下方 document 跟随滚动。

原文出处欢迎讨论 https://github.com/Jiavan/blo...
查看原文

林水溶 赞了文章 · 7月17日

解析移动端滚动穿透

滚动穿透在移动端开发中是一个很常见的问题,产生诡异的交互行为,影响用户体验,同时也让我们的产品看起来不那么“专业”。虽然不少产品选择容忍了这样的行为,但是作为追求极致的工程师,应该去了解为什么会产生以及如何去解决。

什么是滚动穿透

移动端开发中避免不了会在页面上进行弹窗、加浮层等这种操作。一个最常见的场景就是整个页面上有一个遮罩层,上面画着各种各样的东西,具体是什么就不讨论。实现这样一个遮罩层可难不住即使是一个刚开始写前端的小白。但是这里有一个问题就是如果不对遮罩层做任何处理,当用户在上面滑动时会发现遮罩层下方的页面居然也在滚动,这就很 interesting 了。就如下面的例子,一个名为mask长宽都是屏幕大小的遮罩层,我们在上面滑动时,下面的内容也在跟随滚动,即滚动“穿透”到了下方,这就是滚动穿透(scroll-chaining)。

scroll-chaining

上方 demo 的遮罩层底部是一个逐渐变蓝的内容容器,但是滑动上面遮罩层时,底部也跟随滚动了,这只是一个最简单的场景,后面我们会讨论更复杂的情况。

为什么会出现

目前 Google 上搜滚动穿透会出现一大堆教你如何解决的文章,但是它们都是在告诉你怎么解决怎么 hack 掉这种交互异常。并没有告诉读者为什么会产生这种行为,甚至认为这是浏览器的一个 bug。对于我来说这个是难以理解的,因为就算解决了问题,其实也并不知道问题的根本是怎样的。

认知误区

有一个误区就是我们设置了一个和屏幕一样大小的遮罩层,盖住了下面的内容,按理说我们应该能屏蔽掉下方的所有事件也就是说不可能触发下面内容的滚动。那么我们就去看一下规范,什么时候会触发滚动。

// https://www.w3.org/TR/2016/WD...
When asked to run the scroll steps for a Document doc, run these steps:

  1. For each item target in doc’s pending scroll event targets, in the order they were added to the list, run these substeps:
  • If target is a Document, fire an event named scroll that bubbles at target.
  • Otherwise, fire an event named scroll at target.
  1. Empty doc’s pending scroll event targets.

通过规范我们可以明白的 2 点是,首先滚动的 target 可以是 document 和里面的 element。其次,在 element 上的 scroll 事件是不冒泡的,document 上的 scroll 事件冒泡。

所以如果我们想通过在 scroll 的节点上去阻止它的滚动事件冒泡来解决问题是不可行的!因为它根本就不冒泡,无法触及 dom tree 的父节点何谈触发它们的滚动。

那么问题是怎么产生的呢,其实规范只说明了浏览器应该在什么时候滚动,而没有说不应该在什么时候滚动。浏览器正确实现了规范,滚动穿透也并不是浏览器的 bug。我们在页面上加了一个遮罩层并不会影响 document 滚动事件的产生。根据规范,如果目标节点是不能滚动的那么将会尝试 document 上的滚动,也就是说遮罩层虽然不可滚动,但是这个时候浏览器会去触发 document 的滚动从而导致了下方文档的滚动。也就是说如果 document 也不可滚动了,也就不会有这个问题了。这就引出了解决问题的第一种方案:把 document 设置为 overflow hidden。

怎么解决

overflow hidden

既然滚动是由于文档超出了一屏产生的,那么就让它超出部分 hidden 掉就好了,所以在遮罩层被弹出的时候可以给 html 和 body 标签设置一个 class:

.modal--open {
  height: 100%;
  overflow: hidden;
}

这样文档高度和屏幕一样,自然不会存在滚动了。但是这样又会引来一个新的问题,如果文档之前存在一定的滚动高度那么这样设置后会导致之前的滚动距离失效,文档滚回了最顶部,这样一来岂不是得不偿失?但是我们可以在加 class 之前记录好之前的滚动具体然后在关闭遮罩层的时候把滚动距离设置回来。这样问题是可以得到解决的实现成本也很低,但是如果遮罩层是透明的,弹出后用户仍然会看到丢失距离后的下方页面,显然这样并不是完美的方案。

prevent touch event

还有一种办法就是我们直接阻止掉遮罩层和弹窗的 touch event 这样就不会在移动端触发 scroll 事件了。但是在 PC 上没有 touch 事件, scroll 事件仍然可以被触发,原因上面我们也说过,scroll 事件是滚动它能滚动的元素。这里我们解决的是移动端的问题,例子如下:

scroll-chaining

<div id="app">
  <div class="mask">mask</div>
  <div class="dialog">dialog</div>
</div>
const $mask = document.querySelector(".mask");
const $dialog = document.querySelector(".dialog");
const preventTouchMove = $el => {
  $el.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    { passive: false }
  );
};
preventTouchMove($mask);
preventTouchMove($dialog);

上面我们通过 prevent touchmove 来阻止页面的触摸事件从而禁止进一步的页面滚动,在 addEventListener 最后一个参数我们将 passive 显示的设置为 false,这里是有用意的。关于 passive event listener 这里又是一个话题我们就不展开说了,就是浏览器为了优化滚动性能做的一些改进,具体可以看 网站使用被动事件侦听器以提升滚动性能,由于在 Chrome 56 开始将会默认开启 passive event listener 所以不能直接在 touch 事件中使用 preventDefault,需要先将 passive 选项设置为 false 才行。

这里我们解决了在页面上普通弹窗的问题,但是如果 dialog 的内容是可以滚动的,这样将其阻止了 touch 事件将会导致其内容也不能正常滚动,所以还有要进一步优化才行。

进一步优化

现在的场景是我们的弹窗是可以滚动的,所以不能再直接将其 touch 事件阻止,去掉后我们发现会产生新的问题。遮罩层被阻止了 touch 事件不能使下方滚动,但是弹出层 modal 这里内容是可滚动的,在 touch modal 时能正常滚动里面的内容。但是 modal 滚动到最上方或者最下方时仍然能触发 document 的滚动,效果如下:

scroll-chainging

我们看到当 modal 滚动在顶部时仍然能拖动下方 document。这样我们只能监听用户手势,如果 modal 已经滑动到了底部或者顶部且还要往上或者下滑动则也要 prevent modal 的 touch 事件。简单实现一个 fuckScrollChaining 函数:

function fuckScrollChaining($mask, $modal) {
  const listenerOpts = { passive: false };
  $mask.addEventListener(
    "touchmove",
    e => {
      e.preventDefault();
    },
    listenerOpts
  );
  const modalHeight = $modal.clientHeight;
  const modalScrollHeight = $modal.scrollHeight;
  let startY = 0;

  $modal.addEventListener("touchstart", e => {
    startY = e.touches[0].pageY;
  });
  $modal.addEventListener(
    "touchmove",
    e => {
      let endY = e.touches[0].pageY;
      let delta = endY - startY;

      if (
        ($modal.scrollTop === 0 && delta > 0) ||
        ($modal.scrollTop + modalHeight === modalScrollHeight && delta < 0)
      ) {
        e.preventDefault();
      }
    },
    listenerOpts
  );
}

完整实现在 这里,至此无论弹出层内容是否可滚动都不会导致下方 document 跟随滚动。

原文出处欢迎讨论 https://github.com/Jiavan/blo...
查看原文

赞 36 收藏 28 评论 2

林水溶 赞了文章 · 7月17日

关于css动态样式注入,你不知道的那些冷知识

前言

作为一个前端,我们都听过结构,样式,行为分离;关于样式,我们都听过外联样式,内联样式和行内样式;关于这三者,什么权重啊,啊,对了,这些都不会出现在这篇文章里,这篇文章就说一些那些我们不怎么使用的,动态引入css样式的方法;

静态样式引入

前面说过外联样式,内联样式和行内样式,所谓外联样式,即样式文件是一个单独的css文件,通过link标签引入;而内联样式,是一种存在于html文件中,但与页面结构元素分离的,他们都是以存在于style标签中;而行内样式,即存在于某一个标签中,他们只对当前元素有效;说那么多,一张图胜过千言万语;
样式引入
无图说鬼话,有图说人话。是不是一下全看懂了,快夸我。样式引入方式的不同,也注定了他们作用的范围不同,外联能作用域多个html文件的多个htm页面的多个dom节点,两个多个;内联只能作用于单个html页面的多个dom节点;而行内嘛,就没多个了,就只能作用单个页面的样式属性所在的dom节点。

动态态样式引入

其实,HTML文件静态样式引入,只要是一个前端,应该都明白,所以这篇文章,重点是要说动态样式的引入,说一些不常见当可能很适用的方法;

行内样式

看下面一段代码:

    var triangle = document.createElement('label');
    triangle.style.width = '0';
    triangle.style.height= '0';
    triangle.style.position='absolute';
    triangle.style.left ='50%';
    triangle.style.top ='99%';
    triangle.style.marginLeft = '-5px';
    triangle.style.borderLeft = '5px solid transparent';
    triangle.style.borderRight = '5px solid transparent';
    triangle.style.borderTop= '5px solid white';
    triangle.style.borderTopColor = style.backgroundColor;
    label.appendChild(triangle);

这样的写法应该很常见吧,创建一个元素(当然你也可以获取一个元素),然后使用js代码为其动态添加样式,有可能你会问,这属性一个一个写,为啥不能直接对象,比如下面这样:

    triangle.style ={
        width:'0',
        height:'0',
        position:'absolute'
    }

注意哈,不行哈,这是绝对不行的,重要的事情重点标注,那如果我想以对象的方式为元素添加样式呢?有,方法还不止一种(操作HTML的样式类属性方法):

  1. triangle.style ="width:0;height:0;position:absolute;"(不推荐)
  2. triangle.style.cssText ="width:0;height:0;position:absolute;"(推荐)
  3. 首先将上面的样式属性事先写在一个样式class里,比如
    .triangle{width:0;height:0;position:absolute;},然后在js操作中,只需一句triangle.classList.add('.triangle'),动态为元素添加一个样式类
    (极力推荐)

    这里说一个重点,易错点,使用dom.style为元素设置其浮动样式时,不可用dom.style.float = 'left',为什么,因为float在css中是关键词,要设置其浮动属性,非IE浏览器得使用cssFloat(),而IE使用styleFloat,我走过的坑,但愿你不要再跳下去;

内联样式

虽然上面我们极力推荐第3种来添加类样式为元素添加样式,但在一些插件的引入的时候,我们在引入其js的时候,还得相应的引入其css,比如下面这样:
图片描述
是不是觉得有点烦,我个人写插件比较喜欢别人使用时,只需要一个文件就达到目的,而无需多在页面增加一次请求,所以这怎么做呢?
那就是样式的动态引入,如果你所写的插件只涉及到少许的样式操作,像我写的解决Echarts单轴雷达轮播那个插件,那用上面提到的直接操作行内样式就够了;但是如果涉及到大片的样式和插件样式动态变换,那么还是引入样式类比较简便,与上面截图不一样的是,我们是将样式写在插件的JS中,然后插件被调用时,动态注入我们的样式类,具体操作如下:
图片描述
仔细看看,可以发现,sytleStr其实就是我们通常css文件中定义的那些样式字符串,然后动态创建了一个sytle标签(设置其type很重要),并将样式字符串通过字符串节点的形式注入到标签中,最后将这个标签添加到被引用js所关联的html文件head头部,所形成的效果就是下面这样:
图片描述
这样写的好处就是,别人在使用你的插件时,无需多去引用你的css文件,这样看起来比较简洁,当然有些利弊也需要你权衡,比如维护你插件样式时,同直接在css样式文件中修改,这样的形式会显得稍微麻烦一些;

动态样式

其实与上面的内联样式动态引入相比,外联样式的动态引入,相信被更多的人熟知。具体步骤就是,创建link标签,设置type属性,设置其href,然后添加到html文件当中;像下面这样:
图片描述

图片描述
可以看到html文件中有一个id为dynamicCreation的Link标签,而其关联的就是我们想为其添加的css文件。

以上三种动态样式注入,不同的使用场景,各有利弊,至于你想用哪一种,需要你自己权衡,睡觉去啦。。。。

查看原文

赞 2 收藏 1 评论 1