林水溶

林水溶 查看完整档案

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

Front End Developer

个人动态

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

由<input type="file" />获取的file.type为空字符串引申浏览器是如何获取文件的MIME类型

前言

  今天项目上遇到了一个问题,用户需要导入一个从我们服务器上下载的EXCEL文件,前端根据获取到的文件的type属性进行判断是否可以上传["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-excel"],但是在某一个用户的电脑上却出现了通过<input type="file">获取到的file对象中,type属性为"",于是开始找各种资料希望了解浏览器是如何获取这个type属性的,为什么同一个文件不同的电脑获取到的属性不一样。

MIME Type是什么

  根据MDN上的说明,MIME Type (Multipurpose Internet Mail Extensions (MIME) type)是一种标准化的方式来表示文档的性质和格式。它在IETF RFC 6838中进行了定义和标准化。
  浏览器通常使用MIME类型(而不是文件扩展名)来确定如何处理文档;因此服务器设置正确以将正确的MIME类型附加到响应对象的头部是非常重要的。所以浏览器中<input type="file">获取到的file对象中的type属性其实是文件的MIME Type。

Chrome获取MIME类型

  在chromium开源代码中 https://cs.chromium.org/chromium/src/net/base/mime_util.cc?l=314 314-318行中提到了:

  // We implement the same algorithm as Mozilla for mapping a file extension to
  // a mime type.  That is, we first check a hard-coded list (that cannot be
  // overridden), and then if not found there, we defer to the system registry.
  // Finally, we scan a secondary hard-coded list to catch types that we can
  // deduce but that we also want to allow the OS to override.

  Chrome实现了与Mozilla相同的算法,将文件扩展名映射到MIME类型。
首先,Chrome会检测一个硬编码列表(不能被覆盖)源码中的kPrimaryMappings,然后如果没有找到符合的,Chrome会从操作系统注册表中找,最后会扫描一个二级硬编码列表,源码中的kSecondaryMappings,用来捕获可以推断但是也希望允许操作系统覆盖的类型。
  例如:从安装了Microsoft Excel的Windows系统上传CSV文件时,Chrome会将其报告为application/vnd.ms-excel。这是因为.csv未在第一个硬编码列表中指定,因此浏览器会回退到系统注册表。HKEY_CLASSES_ROOT\.csv有一个名为的值Content Type设置为application/vnd.ms-excel

总结

  前言中遇到的问题浏览器中获取不到type属性不一定是代码的原因,而是系统中所安装的Microsoft Excel软件或注册表的原因,另外在MDN中的File对象中也找到这也一句描述:基于当前的实现,浏览器不会实际读取文件的字节流,来判断它的媒体类型。它基于文件扩展来假设;重命名为 .txt 的 PNG 图像文件为 "text/plain" 而不是 "image/png" 。而且,file.type 仅仅对常见文件类型可靠。例如图像、文档、音频和视频。不常见的文件扩展名会返回空字符串。开发者最好不要依靠这个属性,作为唯一的验证方案。

查看原文

赞 12 收藏 6 评论 3

林水溶 赞了文章 · 2020-12-09

使用TypeScript+Vuex

准备工作

1,使用 vue-cli 3搭建Vue项目,如果不知道怎么搭建的同学可以点击这里

2,使用 npm i -s vuex-class 命令安装 vuex-class

3,删除 src/store.ts(很重要)

4,根目录下新建如下文件

clipboard.png

5,代码如下

store/types.ts

  • 声明并暴露 RootState 类型
// store/types.ts

import { UserState } from './user/types'

export interface RootState {
  user: UserState
}


store/index.ts

  • 使用StoreOptions类型定义Vuex.Store构造器选项的类型,并将RootState作为泛型传入StoreOptions,用来定义根状态的类型
  • 将RootState作为泛型,传入Vuex.Store构造器
// store/index.ts

import Vue from 'vue'
import Vuex, { StoreOptions } from 'vuex'
import { RootState } from './types'
import { user } from './user/index'Vue.use(Vuex)

const store: StoreOptions<RootState> = {
  modules: {
    user
  }
}
export default new Vuex.Store<RootState>(store)


store/user/types.ts

  • 声明并暴露UserState类型
// store/user/types.ts

export interface UserState {
  firstName: string
  lastName: string
  mobile: string
}


store/user/actions.ts

  • 使用ActionTree定义actions的类型,并将UserState和RootState作为泛型传入ActionTree
// store/user/actions.ts

import { UserState } from './types'
import { ActionTree } from 'vuex'
import { RootState } from '../types'

export const actions: ActionTree<UserState, RootState> = {
  fetchData({ commit }): void {
    const userInfo: UserState = {
      firstName: 'Hello',
      lastName: 'World',
      mobile: '1235678911'
    }
    commit('saveUserInfo', userInfo)
  }
}


store/uset/getters.ts

  • 使用GetterTree定义getters的类型,并将UserState和RootState作为泛型传入GetterTree
// store/user/getters.ts

import { GetterTree } from 'vuex'
import { UserState } from './types'
import { RootState } from '../types'

export const getters: GetterTree<UserState, RootState> = {
  fullName(state): string {
    return `${state.firstName} ${state.lastName}`
  }
}


store/user/mutations.ts

  • 使用MutationTree定义mutations的类型,并将UserState作为泛型传入MutationTree
  • MutationTree不需要传入RootState
// store/user/mutations.ts

import { MutationTree } from 'vuex'
import { UserState } from './types'

export const mutations: MutationTree<UserState> = {
  changeMobile(state, mobile: string) {
    state.mobile = mobile
  },
  saveUserInfo(state, userInfo) {
    state = Object.assign(state, userInfo)
  }
}


store/user/index.ts

  • 使用UserState定义user模块的state的类型
  • 使用Module定义user模块的类型,并将UserState和RootState作为泛型传入Module
// store/user/index.ts

import { Module } from 'vuex'
import { UserState } from './types'
import { RootState } from '../types'
import { getters } from './getters'
import { actions } from './actions'
import { mutations } from './mutations'

const state: UserState = {
  firstName: '',
  lastName: '',
  mobile: ''
}
const namespaced = true
export const user: Module<UserState, RootState> = {
  namespaced,
  state,
  getters,
  actions,
  mutations
}
export default state

开始使用

1,使用 namespace('path/to/module') 装饰器

import { Component, Vue } from 'vue-property-decorator'
import { namespace } from 'vuex-class'

const userModule = namespace('user')

@Component
export default class Home extends Vue {
  @userModule.Action('fetchData') public fetchData!: Function
  @userModule.Mutation('changeMobile') public changeMobile!: Function
  @userModule.Getter('fullName') public fullName!: string
  @userModule.State('firstName') public firstName!: string
  @userModule.State('mobile') public mobile!: string
  public created() {
    this.fetchData()
    this.changeMobile('123456')
  }
}

等同于使用下面的js写法:

import { createNamespacedHelpers } from 'vuex'

const {
  mapState,
  mapMutations,
  mapGetters,
  mapActions
} = createNamespacedHelpers('user')

export default {
  computed: {
    ...mapState(['firstName', 'mobile']),
    ...mapGetters(['fullName'])
  },
  methods: {
    ...mapActions(['fetchData']),
    ...mapMutations(['changeMobile'])
  },
  created() {
    this.fetchData()
    this.changeMobile('123456')
  }
}

2,使用@Action('action', { namespace: 'path/to/module' })这种形式

import { Component, Vue } from 'vue-property-decorator'
import { Action, Mutation, Getter, State } from 'vuex-class'

const namespace = 'user'

@Component
export default class Home extends Vue {
  @Action('fetchData', { namespace }) public fetchData!: Function
  @Mutation('changeMobile', { namespace }) public changeMobile!: Function
  @Getter('fullName', { namespace }) public fullName!: string
  @State('firstName', { namespace }) public firstName!: string
  @State('mobile', { namespace }) public mobile!: string

  public created() {
    this.fetchData()
    this.changeMobile('123456')
  }
}

3,使用 @Action('path/to/module/action') 这种形式

注意:@State装饰器不能使用这种方式

import { Component, Vue } from 'vue-property-decorator'
import { Action, Mutation, Getter, State } from 'vuex-class'

const namespace = 'user'

@Component
export default class Home extends Vue {
  @Action('user/fetchData') public fetchData!: Function
  @Mutation('user/changeMobile') public changeMobile!: Function
  @Getter('user/fullName') public fullName!: string
  @State('firstName', { namespace }) public firstName!: string
  @State('mobile', { namespace }) public mobile!: string

  public created() {
    this.fetchData()
    this.changeMobile('123456')
  }
}

参考:https://codeburst.io/vuex-and...

查看原文

赞 5 收藏 2 评论 2

林水溶 赞了文章 · 2020-12-01

RabbitMQ快速入门

一、前言

RabbitMQ其实是我最早接触的一个MQ框架,我记得当时是在大学的时候跑到图书馆一个人去看,由于RabbitMQ官网的英文还不算太难,因此也是参考官网学习的,一共有6章,当时是用Node来开发的,当时花了一下午看完了,也理解了。而现在回过头来再看,发现已经忘记了个差不多了,现在再回过头来继续看看,然乎记之。以防再忘,读者看时最好有一定的MQ基础。

二、RabbitMQ

首先我们需要知道的是RabbitMQ它是基于高级队列协议(AMQP)的,它是Elang编写的,下面将围绕RabbitMQ队列、交换机、RPC三个重点进行展开。

2.1、队列

存储消息的地方,多个生产者可以将消息发送到一个队列,多个消费者也可以消费同一个队列的消息。

注意:当多个消费者监听一个队列,此时生产者发送消息到队列只有一个消费者被消费,并且消费端的消费方式是按照消费端在内部启动的顺序轮询(round-robin)。

2.2、消费者

消费消息的一方

public class Send {

    private final static String QUEUE_NAME = "hello";
    private final static String IP = "172.16.12.162";
    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(IP);
        factory.setUsername("admin");
        factory.setPassword("admin");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
public class Recv {

    private final static String QUEUE_NAME = "hello";
    private final static String IP = "172.16.12.162";

    public static void main(String[] args) {
        try {

            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost(IP);
            factory.setUsername("admin");
            factory.setPassword("admin");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'");
            };
            channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

2.3、小结

1、Rabbit是如何保证消息被消费的?
答:通过ack机制。每当一个消息被消费端消费的时候,消费端可以发送一个ack给RabbitMQ,这样RabbitMQ就知道了该条消息已经被完整消费并且可以被delete了。;如果一条消息被消费但是没有发送ack,那么此时RabbitMQ将会认为需要重新消费该消息,如果此时还有其它的消费者,那么此时RabbitMQ将会把这条消息交给它处理。

注意:开启ack机制的是autoAck=false;

2、消息如何进行持久化?

  • 将queue持久化,即设置 channel.queueDeclare(QUEUE_NAME, true, false, false, null);第二个参数durable为true
  • 设置消息持久化,即设置MessageProperties.PERSISTENT_TEXT_PLAIN
注意:消息持久化并不一定保证消息不会被丢失

3、RabbitMQ如何避免两个消费者一个非常忙一个非常闲的情况?
通过如下设置,保证一个消费者一次只能消费一个消息,只有当它消费完成并且返回ack给RabbitMQ之后才给它派发新的消息。

int prefetchCount = 1 ;
channel.basicQos(prefetchCount)

4、RabbitMQ异常情况下如何保证消息不会被重复消费?
需要业务自身实现密等性,RabbitMQ没有提供比较好的方式去保证。

2.2、交换机

在RabbitMQ中,生产者其实从来不会发送消息到队列,甚至,它不知道消息被发送到了哪个队列。那它被发送到了哪里呢?就是本节的重点:交换机,下面就是它在RabbitMQ中的介绍图。(X就是交换机)生产者发送消息给交换机,然后由交换机将消息转发给队列。

从上图就产生一个问题:X怎么将消息发给queue呢?它是把消息发给所有queue还是发给一个指定的queue或者丢弃消息呢?这就是看交换机的类型了。下面一起谈谈这几种类型

2.2.1、fanout

fanout:广播模式,这个比较好理解,就是所有的队列都能收到交换机的消息。
clipboard.png
如上面,两个队列都能收到交换机的消息。

2.2.2、direct

这个模式相当于发布/订阅模式的一种,当交换机类型为direct的时候,此时我们需要设置两个参数:

  1. channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));第二个参数,我们可以把它称呼为routeKey
  2. channel.queueBind(queueName, EXCHANGE_NAME, "");第三个参数,我们把它称呼为bindKey

有了这两个参数,我们就可以指定我们订阅哪些消息了。

clipboard.png
如图,Q1订阅了orange的消息,Q2订阅了black、green的消息。

2.2.3、topic

其实topic和direct有一点类似,它相当于对direct作了增强。在direct中,我们上面所说的bind routeKey为black、green的它是有限制的,它只能绝对的等于routeKey,但是有时候我们的需求不是这样,我们可能想要的是正则匹配即可,那么Topic就派上用场了。

clipboard.png

当类型为topic时,它的bindKey对应字符串需要是以“.”分割,同时RabbitMQ还提供了两个符号:

  • 星号(*):表示1个单词
  • 井号(#):表示0、多个单词

上图的意思是:所有第二个单词为orange的消息发送个Q1,所有最后一个单词为rabbit或者第一个单词为lazy的消息发送给Q2。

2.2.4、header

这一种类型官方demo没有过多解释,这里也不研究了。

2.3、RPC

RabbitMQ 还可以实现RPC(远程过程调用)。什么是RPC,简单来说就是local调用remote方法。对应于RabbitMQ中则是Client发送一个request message,Server处理完成之后将其返回给Client。这里就有了一个疑问?Server是如何将response返回给Client的,这里RabbitMQ定义了一个概念:Callback Queue。
Callback Queue
注意这个队列是独一无二的String replyQueueName = channel.queueDeclare().getQueue();
首先我们需要明白一点的是为什么需要这个queue?我们知道在RabbitMQ作消息队列的时候,Client只需要将消息投放到queue中,然后Server从queue去取就可以了。但是在RabbitMQ作为RPC的时候多了一点就是,Client还需要返回结果,这时Server端怎么知道把消息发送给Client,这就是Callback Queue的用处了。
Correlation Id
在上面我们知道Server返回数据给Client是通过Callback Queue的,那么是为每一个request都创建一个queue吗?这未免太过浪费资源,RabbitMQ有更好的方案。在我们发送request,绑定一个唯一ID(correlationId),然后在消息被处理返回的时候取出这个ID和发出去的ID进行匹配。这样来说一个Callback Queue是Client级别而不是request级别的了。

实现
上面介绍了RabbitMQ实现RPC最重要的两个概念,具体代码比较简单还是贴下把。
client 端

public class RPCClient {
    private Connection connection;
    private Channel channel;
    private String requestQueueName = "rpc_queue";

    public RPCClient() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        connection = factory.newConnection();
        channel = connection.createChannel();
    }

    public static void main(String[] argv) throws Exception{
        RPCClient fibonacciRpc = new RPCClient();
        try {
            for (int i = 0; i < 32; i++) {
                String i_str = Integer.toString(i);
                System.out.println(" [x] Requesting fib(" + i_str + ")");
                String response = fibonacciRpc.call(i_str);
                System.out.println(" [.] Got '" + response + "'");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public String call(String message) throws IOException, InterruptedException {
        final String corrId = UUID.randomUUID().toString();

        String replyQueueName = channel.queueDeclare().getQueue();
        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();

        channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

        final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);

        String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                response.offer(new String(delivery.getBody(), "UTF-8"));
            }
        }, consumerTag -> {
        });

        String result = response.take();
        channel.basicCancel(ctag);
        return result;
    }

    public void close() throws IOException {
        connection.close();
    }
}

服务端


public class RPCServer {

    private static final String RPC_QUEUE_NAME = "rpc_queue";

    private static int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
            channel.queuePurge(RPC_QUEUE_NAME);

            channel.basicQos(1);

            System.out.println(" [x] Awaiting RPC requests");

            Object monitor = new Object();
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                        .Builder()
                        .correlationId(delivery.getProperties().getCorrelationId())
                        .build();

                String response = "";

                try {
                    String message = new String(delivery.getBody(), "UTF-8");
                    int n = Integer.parseInt(message);

                    System.out.println(" [.] fib(" + message + ")");
                    response += fib(n);
                } catch (RuntimeException e) {
                    System.out.println(" [.] " + e.toString());
                } finally {
                    channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes("UTF-8"));
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // RabbitMq consumer worker thread notifies the RPC server owner thread
                    synchronized (monitor) {
                        monitor.notify();
                    }
                }
            };

            channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> { }));
            // Wait and be prepared to consume the message from RPC client.
            while (true) {
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

三、总结

这次回头再看RabbitMQ,再次重新理解了以下RabbitMQ,有些东西还是要慢慢嚼的。当然这些也都是官网的入门例子,后续有机会的话再深入研究。

查看原文

赞 4 收藏 3 评论 0

林水溶 赞了问题 · 2020-11-04

解决关于svg中stroke-dashoffset的一个问题

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
    .svgContent {
      border: 2px solid #ddd;
      background: #ddd;
      border-radius: 5px;
      width: 50%;
      margin: auto;
      height: 40px;
      line-height: 40px;
      padding-top: 20px;
    }
    .lineAnimation {
      -webkit-animation: testMove 3s linear infinite;
    }
    @-webkit-keyframes testMove {
      from {
        stroke-dashoffset: 100%
      }
      to {
        stroke-dashoffset: 0
      }
    }
    </style>
</head>
<body>
    <div class="svgContent">
      <svg viewBox="0 0 300 10">
          <line class="lineAnimation" x1="0" y1="0" x2="300" y2="0" 
          stroke="black" stroke-width="10" stroke-dasharray="40,10" stroke-dashoffset="" />
      </svg>
    </div>
</body>
</html>

不是很理解stroke-dashoffset:100% ----> 0这个变化过程
好像是因为100% 和 0%的时候不重合,动画在重复执行的时候,出现了闪动的情况。
问题来源:在使用leaflet-ant-path这个插件的时候发现了这个问题,查看源码发现动画效果就是改变stroke-offset:100% ---> 0
leaflet-ant-path官网地址:https://rubenspgcavalcante.gi...
官网给出的效果图同样也有这个问题。。。

关注 3 回答 2

林水溶 赞了文章 · 2020-11-03

面试官: "用css实现android系统的loading动画"

本文源码: https://github.com/any86/5a.c...

ios/android

web常用的loading图标有2种, 一种是ios的"菊花", 一种是android的"环". 今天我们用svg实现android的"环"动画, 下节课实现ios的"菊花".

注意: gif帧数少的原因, 实际动画效果是很平滑的.

demo

xml(svg)

<svg width="36" height="36" viewBox="0 0 50 50" class="a-loading-android">
    <circle cx="25" cy="25" r="20" fill="none" stroke="currentColor"  stroke-width="5"></circle>
</svg>

首先我们定义svg的画布尺寸为50x50, 在浏览器中缩放为36x36显示(这个36你可以根据实际需要调整), 定义环的圆心坐标为25,25, 半径为20(算下周长大概为125, 后面会用到), 颜色为currentColor获取父元素的color属性的值, 环的宽度为5像素, 看下在没写css前的效果:

scss

.a-loading {
    &-android {
        animation: rotate 2s linear infinite;
        transform-origin: center center;
        >circle {
            display: inline-block;
            animation: dash 1500ms ease-in-out infinite;
            stroke-linecap: round; // 端点是圆形
            color: currentColor;
        }

        @keyframes rotate {
            100% {
                transform: rotate(360deg);
            }
        }
        
        @keyframes dash {
            0% {
                stroke-dasharray: 1, 200;
            }

            50% {
                stroke-dasharray: 100, 200;
                stroke-dashoffset: -45;
            }

            100% {
                stroke-dasharray: 100, 200;
                stroke-dashoffset: -124;
            }
        }
    }
}

stroke-dasharray

先解释stroke-dasharray, 他是用来定义虚线的, 比如stroke-dasharray="50, 20"表示实线部分为50, 间隙20的虚线:


试想一下, 如果环也用虚线表示, 并且让单位实线部分的长度在环的周长范围内变化,这不就实现了(半环/满环等形态), 下面分别是stroke-dasharray的值为25, 200/ 50, 200 / 100, 200:

注意: 这里的200是随意定义的, 表示虚线的间隙, 只要值大于环的周长即可.

stroke-dashoffset

偏移, 值为正数的时候, 虚线逆时针回退, 负数顺时针前进(左图的stroke-dashoffset:0, 环的起点在3点方向, 右图设置为-10以后, 环的起点被顺时针偏移了一段距离):

因为动画中, 环在增加长度的同时尾部在收缩长度, 所以需要配合stroke-dashoffset实现.

动画的3个关键时刻

0%的时刻

让圆环只呈现一个点, 为了让循环一周后动画不突兀(你可以改成0对比下效果).

50%的时刻

为了让圆环呈现80%的环, 所以设置实线部分长度为100(125*0.8, 125是周长): stroke-dasharray: 100, 200;, 同时尾部在收缩, 所以设置 stroke-dashoffset: -45;.

100%的时刻

回到一个点的状态, 和0%状态一致, 这样动画循环起来不突兀, 但是从50%到100%的动画只是"尾部收缩", 所以我们用stroke-dashoffset:-124实现,125-124=1 正好是一个像素, 好了动画到此就实现完毕了.

整体旋转

为了和安卓系统的动画一致, 让整体也进行旋转. 这里代码比较简单不赘述.

animation属性的扩展

如果大家仔细看过css的animation的文档, 会发现animation可以同时支持多个过度动画, 比如animation: color 6s ease-in-out infinite, dash 1.5s ease-in-out infinite;, 用","分割多个动画.

所以我们其实还可以对上面的动画进行扩展, 比如旋转的同时还有颜色变化:

    &-android {
        animation: rotate 2s linear infinite;
        transform-origin: center center;
        >circle {
            display: inline-block;
            // 增加颜色变化的代码
            animation: color 6s ease-in-out infinite, dash 1.5s ease-in-out infinite; 
            stroke-linecap: round;
            color: currentColor;
        }

        @keyframes rotate {
            100% {
                transform: rotate(360deg);
            }
        }

        @keyframes dash {
            0% {
                stroke-dasharray: 1, 200;
            }

            50% {
                stroke-dasharray: 100, 200;
                stroke-dashoffset: -45;
            }

            100% {
                stroke-dasharray: 100, 200;
                stroke-dashoffset: -124;
            }
        }

        @keyframes color {

            0%,
            100% {
                stroke: #6b5c5b;
            }

            40% {
                stroke: #0057e7;
            }

            66% {
                stroke: #008744;
            }

            80%,
            90% {
                stroke: #ffa700;
            }
        }
    }

本文代码参考iview, 一个vue框架.

查看原文

赞 21 收藏 15 评论 2

林水溶 赞了回答 · 2020-11-03

解决SVG的stroke-width问题,相同的数值不一样的效果

已找到问题所在,因为文字默认有'fill:black'的属性,而stroke是沿着文字边框向内和向外扩展stroke-width数值的一半。所以最后一个text标签的fill挡住了前面text标签的一半stroke。
将text的fill设为none可解决问题。
图片描述

关注 0 回答 1

林水溶 赞了回答 · 2020-11-03

SVG中stroke-dasharray的问题

stroke-dasharray=100% 是svg元素路径的总长度吧 可以用element.getTotalLength()获得

关注 3 回答 2

林水溶 赞了文章 · 2020-11-03

使用SVG + CSS实现动态霓虹灯文字效果

早上无意间进入一个网站,看到他们的LOGO效果略屌,如图:
图片描述

刚开始以为是gif动画之类的,审查元素发现居然是用SVG + CSS3动画实现的,顿时激起了我的(hao)欲(qi)望(xin),决定要一探究竟,查看代码之后,发现原理居然是如此简单:多个SVG描边动画使用不同的animation-delay即可!


对于一个形状SVG元素或文本SVG元素,可以使用stroke-dasharray来控制描边的间隔样式,并且可以用stroke-dashoffset来控制描边的偏移量,借此可以实现描边动画效果,更具体的资料可以看看张大神的《纯CSS实现帅气的SVG路径描边动画效果

我们先实现一个简单的文字描边动画:

<svg width="100%" height="100">
    <text text-anchor="middle" x="50%" y="50%" class="text">
        segmentfault.com
    </text>
</svg> 
.text{
    font-size: 64px;
    font-weight: bold;
    text-transform: uppercase;
    fill: none;
    stroke: #3498db;
    stroke-width: 2px;
    stroke-dasharray: 90 310;
    animation: stroke 6s infinite linear;
}
@keyframes stroke {
  100% {
    stroke-dashoffset: -400;
  }
}

演示地址:http://output.jsbin.com/demic...

然后我们同时使用多个描边动画,并设置不同的animation-delay:

<svg width="100%" height="100">
    <text text-anchor="middle" x="50%" y="50%" class="text text-1">
        segmentfault.com
    </text>
    <text text-anchor="middle" x="50%" y="50%" class="text text-2">
        segmentfault.com
    </text>
    <text text-anchor="middle" x="50%" y="50%" class="text text-3">
        segmentfault.com
    </text>
    <text text-anchor="middle" x="50%" y="50%" class="text text-4">
        segmentfault.com
    </text>
</svg> 

注意:要使用多少种颜色,就放多少个text

.text{
    font-size: 64px;
    font-weight: bold;
    text-transform: uppercase;
    fill: none;
    stroke-width: 2px;
    stroke-dasharray: 90 310;
    animation: stroke 6s infinite linear;
}
.text-1{
    stroke: #3498db;
    text-shadow: 0 0 5px #3498db;
    animation-delay: -1.5s;
}
.text-2{
    stroke: #f39c12;
    text-shadow: 0 0 5px #f39c12;
    animation-delay: -3s;
}
.text-3{
    stroke: #e74c3c;
    text-shadow: 0 0 5px #e74c3c;
    animation-delay: -4.5s;
}
.text-4{
    stroke: #9b59b6;
    text-shadow: 0 0 5px #9b59b6;
    animation-delay: -6s;
}

@keyframes stroke {
  100% {
    stroke-dashoffset: -400;
  }
}

大功告成,演示地址:http://output.jsbin.com/vuyuv...

图片描述

需要注意的几个点:

  1. 各个元素的animation-delay与animation的总时长的设置要协调
  2. stroke-dashoffset与stroke-dasharray的设置要协调
查看原文

赞 40 收藏 119 评论 2

林水溶 赞了回答 · 2020-11-02

解决typescript提示implicitly has an 'any' type,这个怎么解决?

tsconfig.json 中 添加"noImplicitAny": false,

关注 3 回答 2

林水溶 赞了文章 · 2020-10-30

mac电脑终端怎么显示项目树:tree命令的使用

tree-command

how to use tree command on mac

Mac中tree命令使用

tree是一个能列出递归目录的命令,以图形显示驱动器或路径的文件夹结构,可以生成命令树。Windows和Linux都有,mac没有原生支持,需要安装tree或者在home目录中添加.bashrc文件里面加入:
alias tree="find . -print | sed -e 's;1*/;|____;g;s;____|; |;g'" 。保存退出后,使用source .bashrc命令更新一下脚本资源,这是再在终端下试一下tree命令.
第二种没有试过,我们来试下比较简单的第一种吧。


# 安装Homebrew 在终端输入下面指令

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# 安装 tree
brew install tree

在某个项目下执行tree命令即可

panzhiqiangdeMacBook-Pro:webpack2-in-action panzhiqiang$ tree -L 1

# -L 1 指只显示一级目录

.
├── README.md
├── index.html
├── index.js
├── node_modules
└── package.json

tree命令后面的参数有其他可选,具体我们可以用tree --help来查看

panzhiqiangdeMacBook-Pro:webpack2-in-action panzhiqiang$ tree --help
usage: tree [-acdfghilnpqrstuvxACDFJQNSUX] [-H baseHREF] [-T title ]
        [-L level [-R]] [-P pattern] [-I pattern] [-o filename] [--version]
        [--help] [--inodes] [--device] [--noreport] [--nolinks] [--dirsfirst]
        [--charset charset] [--filelimit[=]#] [--si] [--timefmt[=]<f>]
        [--sort[=]<name>] [--matchdirs] [--ignore-case] [--] [<directory list>]
  ------- Listing options -------
  -a            All files are listed.
  -d            List directories only.
  -l            Follow symbolic links like directories.
  -f            Print the full path prefix for each file.
  -x            Stay on current filesystem only.
  -L level      Descend only level directories deep.
  -R            Rerun tree when max dir level reached.
  -P pattern    List only those files that match the pattern given.
  -I pattern    Do not list files that match the given pattern.
  --ignore-case Ignore case when pattern matching.
  --matchdirs   Include directory names in -P pattern matching.
  --noreport    Turn off file/directory count at end of tree listing.
  --charset X   Use charset X for terminal/HTML and indentation line output.
  --filelimit # Do not descend dirs with more than # files in them.
  --timefmt <f> Print and format time according to the format <f>.
  -o filename   Output to file instead of stdout.
  -------- File options ---------
  -q            Print non-printable characters as '?'.
  -N            Print non-printable characters as is.
  -Q            Quote filenames with double quotes.
  -p            Print the protections for each file.
  -u            Displays file owner or UID number.
  -g            Displays file group owner or GID number.
  -s            Print the size in bytes of each file.
  -h            Print the size in a more human readable way.
  --si          Like -h, but use in SI units (powers of 1000).
  -D            Print the date of last modification or (-c) status change.
  -F            Appends '/', '=', '*', '@', '|' or '>' as per ls -F.
  --inodes      Print inode number of each file.
  --device      Print device ID number to which each file belongs.
  ------- Sorting options -------
  -v            Sort files alphanumerically by version.
  -t            Sort files by last modification time.
  -c            Sort files by last status change time.
  -U            Leave files unsorted.
  -r            Reverse the order of the sort.
  --dirsfirst   List directories before files (-U disables).
  --sort X      Select sort: name,version,size,mtime,ctime.
  ------- Graphics options ------
  -i            Don't print indentation lines.
  -A            Print ANSI lines graphic indentation lines.
  -S            Print with CP437 (console) graphics indentation lines.
  -n            Turn colorization off always (-C overrides).
  -C            Turn colorization on always.
  ------- XML/HTML/JSON options -------
  -X            Prints out an XML representation of the tree.
  -J            Prints out an JSON representation of the tree.
  -H baseHREF   Prints out HTML format with baseHREF as top directory.
  -T string     Replace the default HTML title and H1 header with string.
  --nolinks     Turn off hyperlinks in HTML output.
  ---- Miscellaneous options ----
  --version     Print version and exit.
  --help        Print usage and this help message and exit.
  --            Options processing terminator.

这样我们可以愉快的玩tree命令了,感谢


  1. /
查看原文

赞 6 收藏 1 评论 0

林水溶 回答了问题 · 2020-10-13

android webview 中setTimeout 不生效

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

关注 3 回答 4

林水溶 赞了问题 · 2020-10-13

android webview 中setTimeout 不生效

setTimeout(()=>{

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

在安卓webview中不会起作用.

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

关注 3 回答 4

林水溶 赞了文章 · 2020-09-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

林水溶 赞了问题 · 2020-09-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

林水溶 收藏了文章 · 2020-09-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 框架中内置的样式了
查看原文

林水溶 赞了文章 · 2020-09-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

林水溶 赞了文章 · 2020-09-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

林水溶 收藏了文章 · 2020-09-05

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

查看原文

林水溶 赞了文章 · 2020-09-05

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

查看原文

赞 2 收藏 1 评论 0

林水溶 赞了文章 · 2020-09-03

前端构建工具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

查看原文

赞 52 收藏 379 评论 39