小丶怪

小丶怪 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 blog.lsiru.com 编辑
编辑

开发多年的菜鸟。
计划:1.mybatis源码阅读 2.spring源码阅读 3.从零开始的后台管理开发

个人动态

小丶怪 提出了问题 · 9月7日

springboot多数据源问题

springboot项目,配置了三个数据源a、b、c,但是启动后,去调用c数据的方法报错了,调用a,b都是没问题的。

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found)

然后我试了,只启用a、b,调用方法没问题。
只启用a、c,调用方法也没问题。
因为项目启动会从a数据源里读取数据,所以试不了b、c。
不知道到底是什么原因导致这样的。

关注 3 回答 2

小丶怪 收藏了文章 · 8月5日

没用过消息队列?一文带你体验RabbitMQ收发消息

人生终将是场单人旅途,孤独之前是迷茫,孤独过后是成长。

楔子

先给大家说声抱歉,最近一周都没有发文,有一些比较要紧重要的事需要处理。

今天正好得空,本来说准备写SpringIOC相关的东西,但是发现想要梳理一遍还是需要很多时间,所以我打算慢慢写,先把MQ给写了,再慢慢写其他相关的,毕竟偏理论的东西一遍要比较难写,像MQ这种偏实战的大家可以clone代码去玩一玩,还是比较方便的。

同时MQ也是Java进阶不必可少的技术栈之一,所以Java开发从业者对它是必须要了解的。

现在市面上有三种消息队列比较火分别是:RabbitMQRocketMQKafka

今天要讲的消息队列中我会以RabbitMQ作为案例来入门,因为SpringBoot的amqp中默认只集成了RabbitMQ,用它来讲会方便许多,且RabbitMQ的性能和稳定性都很不错,是一款经过时间考验的开源组件。

祝有好收获。

本文代码:码云地址GitHub地址

1. 🔍消息队列?

消息队列(MQ)全称为Message Queue,是一种应用程序对应用程序的通信方法。

翻译一下就是:在应用之间放一个消息组件,然后应用双方通过这个消息组件进行通信。

好端端的为啥要在中间放个组件呢?

小系统其实是用不到消息队列的,一般分布式系统才会引入消息队列,因为分布式系统需要抗住高并发,需要多系统解耦,更需要对用户比较友好的响应速度,而消息队列的特性可以天然解耦,方便异步更能起到一个顶住高并发的削峰作用,完美解决上面的三个问题。


然万物抱阳负阴,系统之间突然加了个中间件,提高系统复杂度的同时也增加了很多问题:

  • 消息丢失怎么办?
  • 消息重复消费怎么办?
  • 某些任务需要消息的顺序消息,顺序消费怎么保证?
  • 消息队列组件的可用性如何保证?

这些都是使用消息队列过程中需要思考需要考虑的地方,消息队列能给你带来很大的便利,也能给你带来一些对应的麻烦。

上面说了消息队列带来的好处以及问题,而这些不在我们今天这篇的讨论范围之内,我打算之后再写这些,我们今天要做的是搭建出一个消息队列环境,让大家感受一下基础的发消息与消费消息,更高级的问题会放在以后讨论。

2. 📖RabbitMQ一览

RabbitMQ是一个消息组件,是一个erlang开发的AMQP(Advanced Message Queue)的开源实现。

AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

RabbitMQ采用了AMQP协议,至于这协议怎么怎么样,我们关心的是RabbitMQ结构如何且怎么用。

还是那句话,学东西需要先观其大貌,我们要用RabbitMQ首先要知道它整体是怎么样,这样才有利于我们接下来的学习。

我们先来看看我刚画的架构图,因为RabbitMQ实现了AMQP协议,所以这些概念也是AMQP中共有的。

rabbit架构图

  • Broker: 中间件本身。接收和分发消息的应用,这里指的就是RabbitMQ Server。
  • Virtual host: 虚拟主机。出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
  • Connection: 连接。publisher/consumer和broker之间的TCP连接。断开连接的操作只会在client端进行,Broker不会断开连接,除非出现网络故障或broker服务出现问题。
  • Channel: 渠道。如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP Connection的开销会比较大且效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销。
  • Exchange: 路由。根据分发规则,匹配查询表中的routing key,分发消息到queue中去。
  • Queue: 消息的队列。消息最终被送到这里等待消费,一个message可以被同时拷贝到多个queue中。
  • Binding: 绑定。exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据。

看完了这些概念,我再给大家梳理一遍其流程:

当我们的生产者端往Broker(RabbitMQ)中发送了一条消息,Broker会根据其消息的标识送往不同的Virtual host,然后Exchange会根据消息的路由key和交换器类型将消息分发到自己所属的Queue中去。

然后消费者端会通过Connection中的Channel获取刚刚推送的消息,拉取消息进行消费。

Tip:某个Exchange有哪些属于自己的Queue,是由Binding绑定关系决定的。

3. 💡RabbitMQ环境

上面讲了RabbitMQ大概的结构图和一个消息的运行流程,讲完了理论,这里我们就准备实操一下吧,先进行RabbitMQ安装。

官网下载地址:http://www.rabbitmq.com/downl...

由于我还没有属于自己MAC电脑,所以这里的演示就按照Windows的来了,不过大家都是程序员,安装个东西总归是难不倒大家的吧😂

Windows下载地址:https://www.rabbitmq.com/inst...

进去之后可以直接找到Direct Downloads,下载相关EXE程序进行安装就可以了。

由于RabbitMQ是由erlang语言编写的,所以安装之前我们还需要安装erlang环境,你下载RabbitMQ之后直接点击安装,如果没有相关环境,安装程序会提示你,然后会让你的浏览器打开erlang的下载页面,在这个页面上根据自己的系统类型点击下载安装即可,安装完毕后再去安装RabbitMQ

这两者的安装都只需要一直NEXT下一步就可以了。

安装完成之后可以按一下Windows键看到效果如下:

rabbitmq安装效果

Tip:其中Rabbit-Command后面会用到,是RabbitMQ的命令行操作台。


安装完RabbitMQ我们需要对我们的开发环境也导入RabbitMQ相关的JAR包。

为了方便起见,我们可以直接使用Spring-boot-start的方式导入,这里面也会包含所有我们需要用到的RabbitMQ相关的JAR包。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
</dependencies>

直接引入spring-boot-starter-amqp即可。

4. ✍Hello World

搭建好环境之后,我们就可以上手了。

考虑到这是一个入门文章,读者很多可能没有接触过RabbitMQ,直接使用自动配置的方式可能会令大家很迷惑,因为自动配置会屏蔽很多细节,导致大家只看到了被封装后的样子,不利于大家理解。

所以在本节Hello World这里,我会直接使用最原始的连接方式就行演示,让大家看到最原始的连接的样子。

Tip:这种方式演示的代码我都在放在prototype包下面。

4.1 生产者

先来看看生产者代码,也就是我们push消息的代码:

    public static final String QUEUE_NAME = "erduo";

    // 创建连接工厂
    ConnectionFactory connectionFactory = new ConnectionFactory();

    // 连接到本地server
    connectionFactory.setHost("127.0.0.1");

    // 通过连接工厂创建连接
    Connection connection = connectionFactory.newConnection();

    // 通过连接创建通道
    Channel channel = connection.createChannel();

    // 创建一个名为耳朵的队列,该队列非持久(RabbitMQ重启后会消失)、非独占(非仅用于此链接)、非自动删除(服务器将不再使用的队列删除)
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);

    String msg = "hello, 我是耳朵。" + LocalDateTime.now().toString();
    // 发布消息
    // 四个参数为:指定路由器,指定key,指定参数,和二进制数据内容
    channel.basicPublish("", QUEUE_NAME, null, msg.getBytes(StandardCharsets.UTF_8));

    System.out.println("生产者发送消息结束,发送内容为:" + msg);
    channel.close();
    connection.close();

代码我都给了注释,但是我还是要给大家讲解一遍,梳理一下。

先通过RabbitMQ中的ConnectionFactory配置一下将要连接的server-host,然后创建一个新连接,再通过此连接创建通道(Channel),通过这个通道创建队列和发送消息。

这里看上去还是很好理解的,我需要把创建队列和发送消息这里再拎出来说一下。

创建队列

    AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;

创建队列的方法里面有五个参数,第一个是参数是队列的名称,往后的三个参数代表不同的配置,最后一个参数是额外参数。

  • durable:代表是否将此队列持久化。
  • exclusive:代表是否独占,如果设置为独占队列则此队列仅对首次声明它的连接可见,并在连接断开时自动删除。
  • autoDelete:代表断开连接后是否自动删除此队列。
  • arguments:代表其他额外参数。

这些参数中durable经常会用到,它代表了我们可以对队列做持久化,以保证RabbitMQ宕机恢复后此队列也可以自行恢复。

发送消息

    void basicPublish(String exchange, String routingKey, AMQP.BasicProperties props, byte[] body) throws IOException;

发送消息的方法里是四个参数,第一个是必须的指定exchange,上面的示例代码中我们传入了一个空字符串,这代表我们交由默认的匿名exchange去帮我们路由消息。

第二个参数是路由key,exchange会根据此key对消息进行路由转发,第三个参数是额外参数,讲消息持久化时会用到一下,最后一个参数就是我们要发送的数据了,需要将我们的数据转成字节数组的方式传入。

测试

讲完了这些API之后,我们可以测试一下我们的代码了,run一下之后,会在控制台打出如下:

生产者测试结果01

这样之后我们就把消息发送到了RabbitMQ中去,此时可以打开RabbitMQ控制台(前文安装时提到过)去使用命令rabbitmqctl.bat list_queues去查看消息队列现在的情况:

查看队列状态

可以看到有一条message在里面,这就代表我们的消息已经发送成功了,接下来我们可以编写一个消费者对里面的message进行消费了。

4.2 消费者

消费者代码和生产者的差不多,都需要建立连接建立通道:

    // 创建连接工厂
    ConnectionFactory connectionFactory = new ConnectionFactory();

    // 连接到本地server
    connectionFactory.setHost("127.0.0.1");

    // 通过连接工厂创建连接
    Connection connection = connectionFactory.newConnection();

    // 通过连接创建通道
    Channel channel = connection.createChannel();

    // 创建消费者,阻塞接收消息
    com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("-------------------------------------------");
            System.out.println("consumerTag : " + consumerTag);
            System.out.println("exchangeName : " + envelope.getExchange());
            System.out.println("routingKey : " + envelope.getRoutingKey());
            String msg = new String(body, StandardCharsets.UTF_8);
            System.out.println("消息内容 : " + msg);
        }
    };

    // 启动消费者消费指定队列
    channel.basicConsume(Producer.QUEUE_NAME, consumer);
//        channel.close();
//        connection.close();

建立完通道之后,我们需要创建一个消费者对象,然后用这个消费者对象去消费指定队列中的消息。

这个示例中我们就是新建了一个consumer,然后用它去消费队列-erduo中的消息。

最后两句代码我给注释掉了,因为一旦把连接也关闭了,那我们的消费者就不能保持消费状态了,所以要开着连接,监听此队列。

ok,运行这段程序,然后我们的消费者会去队列-erduo拿到里面的消息,效果如下:

消费者test01

  • consumerTag:是这个消息的标识。
  • exchangeName:是这个消息所发送exchange的名字,我们先前传入的是空字符串,所以这里也是空字符串。
  • exchangeName:是这个消息所发送路由key。

这样我们的程序就处在一个监听的状态下,你再次调用生产者发送消息消费者就会实时的在控制上打印消息内容。

5. 📌消息接收确认(ACK)

上面我们演示了生产者和消费者,我们生产者发送一条消息,消费者消费一条信息,这个时候我们的RabbitMQ应该有多少消息?

理论上来说发送一条,消费一条,现在里面应该是0才对,但是现在的情况并不是:

查看队列状态

消息队列里面还是有1条信息,我们重启一下消费者,又打印了一遍我们消费过的那条消息,通过消息上面的时间我们可以看出来还是当时我们发送的那条信息,也就是说我们消费者消费过了之后这条信息并没有被删除。

消费者test01

这种状况出现的原因是因为RabbitMQ消息接收确认机制,也就是说一条信息被消费者接收到了之后,需要进行一次确认操作,这条消息才会被删除。

RabbitMQ中默认消费确认是手动的,也可以将其设置为自动删除,自动删除模式消费者接收到消息之后就会自动删除这条消息,如果消息处理过程中发生了异常,这条消息就等于没被处理完但是也被删除掉了,所以这里我们会一直使用手动确认模式。

消息接受确认(ACK)的代码很简单,只要在原来消费者的代码里加上一句就可以了:

    com.rabbitmq.client.Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
            System.out.println("-------------------------------------------");
            System.out.println("consumerTag : " + consumerTag);
            System.out.println("exchangeName : " + envelope.getExchange());
            System.out.println("routingKey : " + envelope.getRoutingKey());
            String msg = new String(body, StandardCharsets.UTF_8);
            System.out.println("消息内容 : " + msg);

            // 消息确认
            channel.basicAck(envelope.getDeliveryTag(), false);
            System.out.println("消息已确认");
        }
    };

我们将代码改成如此之后,可以再run一次消费者,可以看看效果:

消息确认

再来看看RabbitMQ中的队列情况:

消息队列状态

从图中我们可以看出消息消费后已经成功被删除了,其实大胆猜一猜,自动删除应该是在我们的代码还没执行之前就帮我们返回了确认,所以这就导致了消息丢失的可能性。

我们采用手动确认的方式之后,可以先将逻辑处理完毕之后(可能出现异常的地方可以try-catch起来),把手动确认的代码放到最后一行,这样如果出现异常情况导致这条消息没有被确认,那么这条消息会在之后被重新消费一遍。

后记

今天的内容就到这里,下一篇将会我们将会撇弃传统的手动建立连接的方式进行发消息收消息,而转用Spring帮我们定义好的注解和Spring提供的RabbitTemplate,更方便的收发消息。

消息队列呢,其实用法都是一样的,只是各个开源消息队列的侧重点稍有不同,我们应该根据我们自己的项目需求来决定我们应该选取什么样的消息队列来为我们的项目服务,这个项目选型的工作一般都是开发组长帮你们做了,一般是轮不到我们来做的,但是面试的时候可能会考察相关知识,所以这几种消息队列我们都应该有所涉猎。

好了,以上就是本期的全部内容,感谢你能看到这里,欢迎对本文点赞收藏与评论,👍你们的每个点赞都是我创作的最大动力。

我是耳朵,一个一直想做知识输出的伪文艺程序员,我们下期见。

本文代码:码云地址GitHub地址

查看原文

小丶怪 赞了问题 · 5月21日

解决Vue 百度首页我的关注里拖动div是怎么实现的?

图片描述

这个是百度首页的我的关注里面的功能,不知道是怎么做的,请教大神们。

图片描述

现在点击右侧一个div,拖动到左面

图片描述

从右侧拖动,会有一个虚线框,然后拖动的div改变了css样式

图片描述

当拖到中间偏左的时候,左侧也会显示虚线框,当放手的时候,拖动的div会停靠在左侧。
如果拖到中间偏右的话,当放手的时候,拖动的div会停靠在右侧。

【如果我描述的不够详细,各位可以打开百度首页,在登录百度账号之后可以查看】
这个复杂的过程,请问各位大神们,是怎么实现的?

关注 2 回答 1

小丶怪 收藏了文章 · 5月15日

从 0 开始,vue 项目实战(二)

前言

上一篇文章 ,已经搭建好所需要的开发环境了,接下来让开发一个简单的项目吧。
关于 less 我就不贴代码了。

源码地址

正题

先对默认的文件进行改造一下。
删除了默认的 App.vue、Hello.vue。
然后加了一个 list.vue。
修改一下main.js
然后看到页面打印出一个 “列表页” 三个字的时候,就表示成功了。

列表页1
列表页2

1、实例1

先弄个最简单的实例看看是不是能跑起来。
列表页3

so easy。

2、实例2

接下来弄个有动态数据的列表,就是这篇文章的主菜,
大概效果长这样。
1.上一页
2.下一页
3.分类

列表页4

3、导入

先装上我们需要的东西。

cnpm i mint-ui -D
cnpm i vue-router -D
cnpm i less less-loader -S
cnpm i jquery -S

mint-ui => 组件库,暂时只用到了其中的loading
vue-router => 路由
less => css的预处理器
jquery => 老朋友

4、配置

路由(vue-router):现在只有一个列表页,那就只写一个列表页的路径,配置 文件放在跟 main.js 同级的地方。

{
    path: '/list',
    name: 'list',
    component: List
}

关于 vue-router 更多的信息,点这里
图片描述

如果有更多页面需要配置的地方,比如编辑页,详情页之类的在这里添加就对了。

入口(main.js): 函数入口,改了一下之前的配置。

图片描述

5、列表页结构

页面分为了三层,所以对应的页面也有三层。
图片描述
图片描述

6、列表页代码

分类

<div class="type-pnl">
    <ul class="type-list">
        <li v-for="type in types" @click="onTabSelect(type.value)" :key="type.value">{{type.text}}</li>
    </ul>
</div>

循环列表,展示内容。

<ul class="list-container">
    <li v-for="(item, i) in list" :key="item.id">
        <span class="index" :title="i + 1">{{(i + 1) > 9999 ? "..." : (i + 1)}}</span>
        <span class="face">
            <img :data-original="item.author.avatar_url" alt="" :title="item.author.loginname"/>
        </span>
        <span :class="{type: item.tab, good: item.good}" v-if="item.tab">{{item.tab | tab}}</span>
        <span class="name" :title="item.title">{{item.title}}</span>
    </li>
</ul>

分页

<div class="load-more">
    <span class="prev" @click="prev" v-show="page != 1">上一页</span>
    <span class="next" @click="next">下一页</span>
</div>

mounted 做了三件事:
1.从路由获取数据,也就是从地址栏里面获取 分类 和 页数。
2.请求列表数据
3.设置 分类 的数据

之所以在mounted里面设置 分类 的数据,是因为不想data里面数据太乱。 如果把 请求数据那一段话注释掉的话,就可以看到 分类 的数据了。

mounted() {
    // 设置默认页数
    this.page = parseInt(this.$route.query.page) || 1;
    // 设置默认分类
    this.tab = this.$route.query.tab;
    // 请求数据
    this.getData();
    // 设置默认头部分类
    this.types = [{
        text: "全部",
        value: ""
    }, {
        text: "精华",
        value: "good"
    }, {
        text: "分享",
        value: "share"
    }, {
        text: "招聘",
        value: "job"
    }, {
        text: "回答",
        value: "ask"
    }];
}

methods 里面添加 getData() 方法,将 ajax 请求回来的数据保存到 list 数据,然后把页面滚到顶层,这样子就能够看到数据了。
至于 common.ajaxGet() 方法,我吧所有的 ajax 请求进行了封装到公共方法里面了。

getData() {
    // 打开loading
    Indicator.open();
    // 请求数据
    common.ajaxGet(common.api + '/topics', {
        page: this.page, // 页数
        tab: this.tab // 分类
    }).then(data => {
        if (data.success) {
            // 填充数据
            this.list = data.data;
            // 移动到顶层
            $(".list").animate({
                scrollTop: 0
            }, 200);
        }
        // 关闭loading
        Indicator.close();
    });
}

新建一个文件 src/lib/common.js,这个文件主要放公共的方法,现在暂时只用到里面的 ajaxGet() 这个方法,ajaxGet() 用了个 promise 包装了一下。

import $ from 'jquery';
let common = {
    api: " https://cnodejs.org/api/v1",
    isPhone() {
        let u = navigator.userAgent;
        let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Adr') > -1; //android终端
        let isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
        return isAndroid || isiOS;
    },
    getType(value) {
        let result = value;
        switch (value) {
            case "job":
                result = "招聘";
                break;
            case "good":
                result = "精华";
                break;
            case "share":
                result = "分享";
                break;
            case "ask":
                result = "问答";
                break;
            default:
                result = "全部"
                break;
        }
        return result;
    },
    ajaxGet(url, data) {
        return new Promise((resolve, reject) => {
            $.ajax({
                url: url,
                data: data || {},
                success: data => {
                    resolve(data);
                },
                error: data => {
                    reject();
                    console.error("数据请求失败");
                }
            })
        });
    }
}
export default common;

data里面加上几个使用的参数就OK了。

data() {
    return {
        list: [],
        types: [],
        tab: "",
        page: 1
    }
}

使用 getType() 方法,对数据过滤一下。

filters: {
    tab(value) {
        return common.getType(value);
    }
}

加上css的话,上面几个步骤应该就可以看到页面效果了。
图片描述

这里 分页 还有 分类 其实都是请求同一个接口,为了在地址栏直接改变 分类 和 分页 有效,所以只要监控地址栏的变化,然后动态的改数据就ok了,不必再写重复的请求接口了。

watch: {
    $route() {
        // 检测路由变化
        this.page = this.$route.query.page || 1;
        this.tab = this.$route.query.tab;

        // 获取数据
        this.getData();
    }
}

这三个方法都是改变地址栏然后通过 wacth 检测地址栏变化去请求数据。

prev() {
    this.page--;

    // 改变路由
    let query = {
        page: this.page
    }
    if (this.tab) {
        query.tab = this.tab;
    }
    this.$router.push({
        path: 'list',
        query: query
    })
},
next() {
    // 改变当前页数
    this.page++;

    // 改变路由
    let query = {
        page: this.page
    }
    if (this.tab) {
        query.tab = this.tab;
    }
    this.$router.push({
        path: 'list',
        query: query
    })
},
onTabSelect(value) {
    // 改变当前分类
    this.tab = value;
    this.page = 1;

    // 改变路由
    let query = {
        page: this.page
    }
    if (this.tab) {
        query.tab = this.tab;
    }
    this.$router.push({
        path: 'list',
        query: query
    })
}

输入 http://localhost:8080/list?pa... 看看页面是不是就会跳到对应的页面了呢,这样子把链接分享出去的话,也能定位到当时的状态。
图片描述

项目结构
图片描述

最后

如果有什么想跟我讨论的话,请私信。
查看原文

小丶怪 收藏了文章 · 5月15日

从零开始 搭建自己的vue 移动项目

因为公司项目需要,需要用vue做一个移动端版本;现在从0开始搭建,顺便记下搭建过程,方便日后回顾;欢迎 大家指出不足。

先看设计稿:

clipboard.png

布局页面

把头部抽离出来,作为全局组件;

目录位置

clipboard.png

定义为全局组件

在main.js 中写下

//全局组件
import topBar from '@/components/mobileTop.vue'
Vue.component('topBar', topBar);

布局 topBar 组件

1,去定义阿里图标,并引入

clipboard.png

clipboard.png

2,设置 icon 组件,并写html

clipboard.png

3,因为vue 模板没有引入 scss ,所以要自己安装sass-loader,node-loader

cnpm i node-sass sass-loader -D

4,写less,此布局icon 使用 position 定位布局

clipboard.png

因為後面加上 flexible-js 自適應 ,所以我們用 rem 佈局,參看下面的代碼,我們直接用實際尺寸除以100就可以;

.content{
    position: relative;
    padding:0 1.2rem 0 1.2rem;
  }

头部 ,main,bottom 三个部分应该怎么布局?

top,bottom main 都用absolute

这个布局在调试的时候完全没有问题;但是在真机调试的时候回存在问题,中间的 main 如果设置了移动端滚动 -webkit-overflow-scrolling:touch;会导致 滑动不了;但是如果不设置,就只能用插件去模拟滚动,非常不好;
并且 如果采用此方式,到了移动端会不时的拉起底部的bottom,让人感觉就是一个网页

top,bottom 用 absolute,main两头padding:

.box{
    position: relative;
    height: 100%;
  }
  .top{
    height: 1rem;
    background: red;
    position: absolute;
    top:0;
    left: 0;
    right: 0;
    z-index: 2;
  }
  .bot{
    height: 1rem;
    background: red;
    position: absolute;
    bottom:0;
    left: 0;
    right: 0;
    z-index: 2;
  }
  .mid{
    height: 100%;
    padding:1rem 0;
    width: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    background: blue;
    box-sizing: border-box;
  }

这个结构可以满足几乎所有要求,但是还是会有拉起底部的问题;

top,bottom 用fixed,main 用padding:1.2rem 0;

采用此方式可能会有一些忽隐忽现的问题,但是最大的好处就是 页面看起来完全像移动页面,底部也不好拉起来;滚动条相当于原生的滚动条,所以基本没有bug;有个存在的问题在这个文章中提到 https://www.cnblogs.com/xiahj...
目前我遇到的bug 在文章最后排除了。

这个算是我比较看中的方式,因为效果基本跟原生一样;

我试过用 better-scroll 处理滚动问题,但是因为微信端会有卡顿感,所以最后还是放弃了;有需求的朋友,可以根据自己情况加入better-scroll方案;

移动端自适应的基础设置

在 app.vue 中 引入 flexible-js 代码

<script>
  document.head.appendChild(meta);
  (function(doc, win){
    var docE1 = doc.documentElement,
      resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
      recalc = function(){
        var clientWidth = docE1.clientWidth;
        if(!clientWidth) return;
        //docE1.style.fontSize = clientWidth / 375  + 'px'; 这里希望设置 1rem = 1px,实验证明,这样做 会导致 html 的 fontsize小于 12px
        docE1.style.fontSize = (clientWidth / 750)*100  + 'px'; //乘以100的意义是,1为了不受fontsize小于12的影响,2为了计算方便;
      };
    if (!doc.addEventListener) return;
    win.addEventListener(resizeEvt,recalc,false);
    doc.addEventListener('DOMContentLoaded',recalc,false);
  })(document,window);
</script>

在main.js 引入全局公共样式 和 一些 模块

//引入公共样式
import '@/common/reset.css'
import '@/common/common.css'

封装 axios 生产出 http.js

/*
 * @Author lizhenhua
 * @version 2018/5/17
 * @description
 */

import axios from 'axios'
import store from '../store'
import {Message} from 'element-ui'
import {getToken,removeToken} from '@/util/cookie'
import tools from '@/util/tools'
import qs from 'qs'
// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 1000 // 请求超时时间
})

// request拦截器
service.interceptors.request.use(config => {

  //如果是开发环境,为非模拟接口加上“跨域”前缀 'ln'
  if(process.env.NODE_ENV=='development'&& config.url.indexOf('api')==-1){
    config.url = 'ln/'+ config.url;
  }

  let token = getToken();

  //如果data参数为 对象或者数组,就把参数 封装到key为data 属性中;
  if(typeof config.data == "object" ||typeof config.data == "Object" ||typeof config.data == "OBJECT"){
    let data = tools.cloneObj(config.data);
    config.data = {};
    config.data['data'] = JSON.stringify(data);
  }else {
    config.data = {};
  }

  //统一为所有请求加上 这两个参数
  if (token) {
    config.data['LE_AUTH_TOKEN'] = token
    config.data['token'] = token
  }

  //设置头部token
  if (store.getters.token) {
    config.headers['X-Token'] = token // 让每个请求携带自定义token 请根据实际情况自行修改
  }

  //不设置 这样,后台拿不到数据
  config.headers['Content-Type'] = 'application/x-www-form-urlencoded'

  //get请求,只能放在 params 中,转为url传参的方式
  //所以统一使用post请求,只有post存在 paramBody,我们可以吧参数放在 data 中
  config.method = "POST"

  //把所有参数处理为 form 表单提交的方式,并且转义,如果不这样,后端(会直接得到字符串,不是正常对象)解析不出来;
  //前端发送:data=%7B%22loginName%22%3A%22lzh%22%2C%22loginPassword%22%3A%22123456%22%2C%22appId%22%3A%22lext79987422-5180-40%22%2C%22platType%22%3A1%7D
  //后端收到:{data={"loginName":"lzh","loginPassword":"123456","appId":"lext79987422-5180-40","platType":1}}
  config.data = qs.stringify(config.data)

  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone拦截器
service.interceptors.response.use(
  response => {
    let res = response.data;
    if (res.status == 1) {
      return res.data
    }if(res.status ==-1208){
      Message({
        message: res.errorMsg,
        type: 'error',
        duration: 5 * 1000
      })
      removeToken();
    } else {  //这里处理 所有数据错误
      Message({
        message: res.errorMsg,
        type: 'error',
        duration: 5 * 1000
      })
      return Promise.reject(res.errorMsg)
    }
  },
  error => {  //这里处理的是 所有网络请求错误
    console.log('err' + error)// for debug
    let err = error + '', info = '';
    if (err.indexOf('timeout') != -1) {
      info = "请求超时";
    } else {
      info = err
    }
    Message({
      message: info,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)


export default service

可以根据自己情况,拦截 ajax 后作统一处理;
把http.js 在main.js 中 引入

//引入 axios 实例
import $http from '@/util/$http' 
Vue.prototype.$http = $http;

在vue 中直接使用 $http 模块

<script>
  export default {
    data: function () {
      return {
        document: {}
      }
    },
    created() {
      this.$http({
        url: this.ajaxApi.test.list,
        data: {
          id: "docid:6CFE06297BBA4E1FBAA00BDE2809198F"
        },
      }).then(res => {
        if(res){
          this.document = this.tools.cloneObj(res.document)
        }
      })
    }
  }
</script>

配置 ajaxApi.js 为全局,统一管理接口

// api 表
export default {
  test: {
    list:"/api/data/document"
  }
}

//引入 api 表
import ajaxApi from "@/util/ajaxApi"
Vue.prototype.ajaxApi  = ajaxApi

配置 tools.js 为全局,提供常用工具函数

// tools.js
export default {
  cloneObj (obj){
    var str, newobj = obj.constructor === Array ? [] : {};
    if (typeof obj !== 'object') {
      return;
    } else if (window.JSON) {
      str = JSON.stringify(obj), //序列化对象
        newobj = JSON.parse(str); //还原
    } else {
      for (var i in obj) {
        newobj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i];
      }
    }
    return newobj;
  }
}

//引入工具库
import tools from "@/util/tools"
Vue.prototype.tools = tools;

再配置一个 cookie 方法文件,方便操作cookie

import Cookies from 'js-cookie'

const TokenKey = 'LtpaToken2'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token,time) {
  return Cookies.set(TokenKey, token,{expiry:time})
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

因为是移动端开发,所以你需要真机调试

因为localhost 是不具有 局域网 访问性的,所以我们要改一下 项目的配置
clipboard.png

在 config/index.js 找到如下代码:

 // host: 'localhost', // can be overwritten by process.env.HOST
    host: '10.20.139.118', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined

改为你的ip 以后,重启项目

在手机浏览器输入 http://10.20.139.118:8080/#/ 就可以访问到你的页面了

一个非常重要的 css 属性

刚刚开始 开发的时候,开发出来的页面 在webview上 滑动总是卡卡的感觉,人家的页面是丝质顺滑,跟原生的一样;后来想到了 用一个插件
模拟这种效果,iscroll.js;

其实 只需要一句css 就能解决这个缓动的效果
在 app.vue 下写

html, body {
  -webkit-overflow-scrolling: touch;
}

-webkit-overflow-scrolling:touch是什么?

MDN上是这样定义的:

-webkit-overflow-scrolling 属性控制元素在移动设备上是否使用滚动回弹效果. auto: 使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。 touch: 使用具有回弹效果的滚动,
当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。

常见流程列表布局与样式

我们通常会在移动端布局中接触到 流程进度列表 结构的设计图;例如淘宝的:物流进度图;这次拿到的设计稿是这样:

clipboard.png

clipboard.png

这两个结构可以看做一样的;整体分为左(标签)中(长线和点)右(内容)三部分,其中,最左边考虑到它位置直接贴在边上,必定是用绝对定位实现;最右边是普通的布局,难点在于怎么处理中间这根贯穿整个列表的线,和上面的圆点。
目前我知道的有两种方法:
一个是每个li 中用div画出自己的线和点,然后每个线拼接起来组成长线,这个方法的缺点是比较难定线的长度,想用height:100%,但是不生效,很可能要用到js;
第二种是本次我采用的方法,用li的伪元素before画线,after 画圆点,通过z-index设置覆盖层级;这样的好处是,线的高度可以用height:100%;难点是圆点在不同分辨率下,可能会出现偏移的情况(线没有穿过圆心);这里直接给圆点做了css3的居中定位,并用magin-right的负值微调了一下,初步测试在不同分辨率下表现都比较好;在这里给出实现代码:

html

<ul class="item-ul">
  <li class="flex-bet">
    <div class="list-left">拟稿意见</div>
    <div class="list-right">
      <div class="top flex-bet">
        <div class="top-left"><span class="yl">公伟杰</span>信息技术部</div>
        <div class="date">2018-07-12</div>
      </div>
      <p>这个提议不错,试试看</p>
    </div>
  </li>
    //以下重复这个li
</ul>

scss

/*流程表*/
    .flex-bet{
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

  .item-ul {
    padding: 0.2rem 0;
    li {
      position: relative;
      padding-left: 1.2rem;
      padding-bottom: 0.2rem;
      &:before {
        content: "";
        position: absolute;
        left: 1rem;
        top: 8px;
        width: 1px;
        background: #58a8ff;
        height: 100%;
      }
      &:after {
        content: "";
        position: absolute;
        left: 0.945rem;
        top: 8px;
        width: 0.1rem;
        height: 0.1rem;
        border: 1px solid #57a9ff;
        border-radius: 50%;
        background: #fff;
        margin-right: -2px;
      }
      .list-left {
        position: absolute;
        width: 1.4rem;
        height: 0.57rem;
        line-height: 0.57rem;
        font-size: 12px;
        text-align: center;
        color: #fff;
        background: #58a8ff;
        border-radius: 0 0.7rem 0.7rem 0;
        left: 0;
        top: 0;
        z-index: 5;
      }
      .list-mid {
        align-self: start;
        z-index: 4;
        height: 100%;
        text-align: center;
        width: 20px;
        margin-top: -4px;
        .icon {
          display: inline-block;
          width: 0.1rem;
          height: 0.1rem;
          border: 1px solid #57a9ff;
          border-radius: 50%;
          background: #fff;
          margin-right: -2px;
        }
      }
      .list-right {
        width: 6.1rem;
        padding: 0.05rem 0.3rem 0 0.5rem;
        font-size: 12px;
        .top-left {
          color: #8c8c8c;
          span {
            display: inline-block;
            padding: 0.05rem 0.2rem;
            color: #484848;
            border-radius: 15px;
            margin-right: 0.2rem;
            background: transparent;
            font-size: 15px;
          }
          .yl {
            background: #ffedd9;
          }
          .bl {
            background: #daecff;
          }
          .pin {
            background: #ffe0eb;
          }
          .zl {
            background: #e3e0ff;
          }
        }
        .date {
          color: #8c8c8c;
        }
        p {
          text-align: left;
          font-size: 12px;
          color: #484848;
          line-height: 0.57rem;
          padding-left: 4px;
        }
        .keyword {
          text-align: left;
          margin-top: 0.1rem;
          span {
            display: inline-block;
            width: 1rem;
            height: 0.35rem;
            line-height: 0.35rem;
            text-align: center;
            font-size: 12px;
            border: 1px solid #b3b3b3;
            margin-right: 0.15rem;
            &:first-child {
              border: 1px solid #57a9fb;
              color: #57a9fb;
            }
          }
        }
      }
    }
    li:last-child:before {
      height: 0.1rem;
    }
  }

一些容易遇到的坑

js focus textarea 光标定位在中间

需求常见: 点击某个input,划开一个新页面,里面可以填300字意见;我watch show/hide 变量,如果显示状态的话,就让子组件的textarea focus;这个时候,这个时候就出现了光标在中间的情况;

clipboard.png

解决方案是设置100 毫秒的延迟

watch:{
      control(val){
        if(val){
          /*延迟100毫秒,fix 光标定位在中间的bug*/
          setTimeout(()=>{
            this.$refs.textBox.focus();
          },100)
        }
      }
    }

弹窗后,页面依然可以滚动页面的bug,滚动穿透

解决方案是当弹窗的时候,给发生滚动的盒子 加上 overflow:hide;关闭弹窗的时候移除;
你还可以open的时候记住滚动条的位置,close的时候复原;
这里最大的坑可能是,要找清楚真实发生了滚动的盒子;很可能不是body;

.oh{
    overflow:hidden !important;
}

 openPop(value){
        this[value] = true;
        document.getElementById('app').className ='oh'
      },
      closePop(value){
        this[value] = false;
        document.getElementById('app').className=' ';
      }
  • html,body标签非弹窗千万不能有overflow设置,会有苹果设备 fixed 布局下滚动条不出来,同时还会出现盖住 top,bottom的情况
html, body{
  position: relative;
  height: 100%;
  -webkit-overflow-scrolling: touch;    //可能是跟这个属性冲突了
  /*overflow-y: auto;*/
  /*overflow-x: hidden;*/ /*这里不能加overflow所有属性,在苹果下会有上下拉盖住顶部底部的bug */
}

scrllow 组件要求父元素定高,但是父元素又不能确定高度。

传统的做法是用 百分比定高,但是要一层层设置 父元素的高度。不然百分比获取不到值。所以这里建议用 vh 代替 %

输入板遮挡textarea 或者input

最后的解决方案是通过定位 把输入部分上提

clipboard.png

详细的讨论在下面做了笔记
https://segmentfault.com/n/13...

移动端,安卓键盘弹起,顶起底部的bug

移步 https://segmentfault.com/n/13...

查看原文

小丶怪 收藏了文章 · 5月6日

十个超级实用的 JS 特性

Chris Noring 原作,翻译内容转载自 New Frontend

欢迎关注原作者 Chris Noring 的推特,反馈关于本文的建议和改进。

你可能刚上手 JavaScript,或者只是曾经偶尔用过。不管怎样,JavaScript 改变了很多,有些特性非常值得一用。 这篇文章介绍了一些特性,在我看来,一个严肃的 JavaScript 开发者每天都多多少少会用到这些特性。

参考资料

下面两个关于 ES6+ 的站点是我的最爱:

展开操作符

顾名思义,用于对象或数组之前的展开操作符(...),将一个结构展开为列表。 演示一下:

let firstHalf = [ 'one', 'two'];
let secondHalf = ['three', 'four', ...firstHalf];

这种写法够优雅,够简洁吧? 如果不用展开操作符,我们得这么写:

let firstHalf = [ 'one', 'two'];
let secondHalf = ['three', 'four'];
for(var i=0, i <firstHalf.length; i++ ) {
  secondHalf.push(firstHalf[i]);
}

展开操作符也适用于合并对象的属性:

const hero = {
  name: 'Xena - Warrior Princess',
  realName: 'Lucy Lawless'
}


const heroWithSword = {
 ...hero,
 weapon: 'sword'
}

不用展开操作符的话,需要遍历对象的属性:

let keys = Object.keys(hero);
let obj = {};

for(var i=0; i< keys.length; i++) {
   obj[keys[i]] = keys[props[i]];
}

剩余参数

剩余参数将剩余的参数收入数列。 JavaScript 的特性是参数数目很灵活。 通常会有一个 arguments 变量收集参数。 让我们看一个例子:

function add(first, second, ...remaining) {
  return first + second;
}

上面的一段代码仅仅将 first 和 second 加起来,也就是说,调用 add(1, 2) 和 add(1, 2, 3, 4) 会得到相同的结果。 下面我们修正一下:

function add(first, second, ...remaining) {
  return first + second + remaining.reduce((acc, curr) => acc + curr, 0);
}

如前所述,...remaining 收集了剩余的参数,为我们提供了这些参数的命名,清楚地表明我们打算处理剩余的参数。 我记得至迟 ES5 已经有 arguments 了,不过少有人知。

字符串插值

见过这样的语句吗?

class Product {
 constructor(name, description, price) {
   this.name = name;
   this.description = description;
   this.price = price;
 }

 getDescription() {
   return " Full description \n" + 
   " name: " + this.name + 
   " description: " + this.description
 }
}

当然,我指的是 getDescription() 方法中那个可读性不佳的多行长语句。 大多数编程语言中都存在类似现象。 一些语言提供了字符串插值,幸运的是,JavaScript 正是其中之一。 我们改写一下 getDescription() 方法:

getDescription() {
   return `Full description \n: 
   name: ${this.name}
   description ${this.description}
   `;
}

一对 ` 包起来的字符串中可以使用 ${} 插值。 现在看起来舒服多了。

简写属性

在 ES5 中必须这么写:

function createCoord(x, y) {
  return {
    x: x,
    y: y
  }
}

ES6 以后可以使用简写属性:

function createCoord(x, y) {
  return {
    x,
    y
  }
}

看起来更清爽了吧?

方法属性

方法属性是在对象中定义指向方法的属性。 考虑下面一段 ES5 代码作为例子:

const math = {
  add: function(a,b) { return a + b; },
  sub: function(a,b) { return a - b; }, 
  multiply: function(a,b) { return a * b; }
}

ES6 以后只需这么写:

const math = {
  add(a,b) { return a + b; },
  sub(a,b) { return a - b; },
  multiply(a,b) { return a * b; }
}

解构赋值

解构赋值有利于开发者本人的心理健康。

考虑下面的代码:

function handle(req, res) {
 const name = req.body.name;
 const description = req.body.description;
 const url = req.url;

 log('url endpoint', url);

 // 大量代码逻辑
 dbService.createPerson(name, description)
}

不管从什么角度来看,上面的代码都不完美,但它确实体现了一种应用场景,我们想要从对象的不同层次获取数据。 你也许会问,这里有什么问题? 好吧,我可以不用声明这么多变量,省下一些敲击键盘的次数。

function handle(req, res) {
 const { body: { name, description }, url } = req;

 log('url endpoint', url);

 // 大量代码逻辑
 dbService.createPerson(name, description)

看,我们上面的代码将三行压缩成了一行。

解构赋值并不仅仅局限于对象。 它同样适用于数组。考虑下面的代码:

const array = [1,2,3,4,5,6];
const a = array[0];
const c = array[2];

上面的代码可以用更优雅的方式改写:

const array = [1,2,3,4,5,6];
const [a, ,c, ...remaining] = arr;

// remaining = [4,5,6]

我们可以使用上面的模式匹配分解数组的值。 我们使用 , , 跳过某些值。 上面提到过的剩余参数这里也能用,在这里我们通过剩余参数捕获了剩余的数组成员。

解构赋值还可以用于函数和参数。 函数有不止 2-3 个参数时,使用一个对象收集所有参数是 JavaScript 的事实标准。 例如,下面一个函数:

function doSomething(config) {
  if(config.a) { ... }
  if(config.b) { ... }
  if(config.c) { ... }
}

有更好的写法:

function doSomething({ a, b, c }) {
  if(a) { ... }
  if(b) { ... }
  if(c) { ... }
}

数组方法

ES6 引入了许多有用的数组方法,例如:

  • find(),查找列表中的成员,返回 null 表示没找到
  • findIndex(),查找列表成员的索引
  • some(),检查某个断言是否至少在列表的一个成员上为真
  • includes,列表是否包含某项

下面的代码有助于你理解它们的用法:

const array = [{ id: 1, checked: true }, { id: 2 }];
arr.find(item => item.id === 2) // { id: 2 }
arr.findIndex(item => item.id === 2) // 1
arr.some(item => item.checked) // true

const numberArray = [1,2,3,4];
numberArray.includes(2) // true

Promises + Async/Await

如果你在这个圈子里呆了些年头,也许会记得曾经有一个时期我们只有回调,就像这样:

function doSomething(cb) {
  setTimeout(() =>  {
    cb('done')
  }, 3000)
}

doSomething((arg) => {
 console.log('done here', arg);
})

我们使用回调是因为有些操作是异步的,需要时间来完成。 后来我们有了 promise 库,人们开始使用它。 然后 JavaScript 逐渐加入了对 promise 的原生支持。

function doSomething() {
  return new Promise((resolve, reject) => {
    setTimeout(() =>  {
      resolve('done')
    }, 3000)
  })
}

doSomething().then(arg => {
 console.log('done here', arg);
})

我们甚至可以这样调用,将 promise 串起来:

getUser()
  .then(getOrderByUser)
  .then(getOrderItemsByOrder)
  .then(orderItems => {
    // 处理排序后的成员
  })

后来生活更加美好,我们有了 async/await, 上面一段代码可以这样写:

async function getItems() {
  try {
    const user = await getUser();
    const order = await getOrderByUser(user);
    const items = await getOrderItemsByOrder(order);
    return items;
  } catch(err) {
    // 在这里处理错误,建议返回某个值或者重新抛出错误
  }
}

getItems().then(items => {
  // 处理排序后的成员
})

模块

差不多任何编程语言都支持模块这一概念,也就是将代码分为多个文件,每个文件是一个自我包含的单元(模块)。 考虑下面的代码:

// math.js

export function add(a,b) { return a + b; }
export function sub(a,b) { return a - b; }

export default mult(a,b) => a * b;

// main.js
import mult, { add, sub } from './math';

mult(2, 4) // 8
add(1,1)   // 2
sub(1,2)   // -1

我们在上面用 export 关键字注明了 add 和 sub 这两个结构对任何引入该模块的模块都公开可见。 export default 关键字则注明仅仅 import 模块时得到的结构。 在 main.js 中,我们将导入的 default 命名为 mult,同时指明我们引入 add() 和 sub() 这两个方法。

箭头函数和字典作用域 this

我在这篇文章中很多地方都用到了箭头函数,它不过是另一种函数表示法。 过去我们只能这么声明函数:

function printArray(arr) {
 // 具体操作
}

现在我们也可以这么写:

const printArray = (arr) => {
 // 具体操作
}

我们也可以将函数声明写到一行里:

const add = (a,b) => a + b

上面的代码表明我们进行操作并返回结果。 我们也可以采用下面的语法返回一个对象:

const create = (a,b) = > ({ x: a, y: b })

过去会碰到搞不清 this 是什么的问题。 考虑下面的代码:

let array = [1,2,3];

function sum() {
  this.total = 0;

  arr.forEach(function(item) {
    this.total+= item;  // 糟糕,`this` 是内层函数的 `this`
  })
  return total;
}

上面代码中的 this 指向 forEach 内部函数的 this,这可不是我们想要的。 过去我们通过以下方式解决这个问题:

function sum() {
  this.total = 0;
  var self = this;

  arr.forEach(function(item) {
    self.total+= item;  // 这里我们使用 `self`,它能解决问题,但是感觉有点别扭
  })
  return total;
} 

箭头函数可以解决问题,再也不用 self 了,现在代码看起来是这样的:

function sum() {
  this.total = 0;

  arr.forEach((item) => {
    this.total+= item;  // 一切安好,`this` 指向外层函数
  })
  return total;
}

大胜!

结语

我还可以讲讲更多 ES6 方面的内容,不过这篇文章中我只打算介绍我最偏爱的特性。 我觉得你应该从今天开始使用这些特性。

查看原文

小丶怪 收藏了文章 · 3月27日

深入理解MySQL索引

前言

当提到MySQL数据库的时候,我们的脑海里会想起几个关键字:索引、事务、数据库锁等等,索引是MySQL的灵魂,是平时进行查询时的利器,也是面试中的重中之重。

可能你了解索引的底层是b+树,会加快查询,也会在表中建立索引,但这是远远不够的,这里列举几个索引常见的面试题:

1、索引为什么要用b+树这种数据结构?

2、聚集索引和非聚集索引的区别?

3、索引什么时候会失效,最左匹配原则是什么?

当遇到这些问题的时候,可能会发现自己对索引还是一知半解,今天我们一起学习MySQL的索引。

一、一条查询语句是如何执行的

首先来看在MySQL数据库中,一条查询语句是如何执行的,索引出现在哪个环节,起到了什么作用。

1.1 应用程序发现SQL到服务端

当执行SQL语句时,应用程序会连接到相应的数据库服务器,然后服务器对SQL进行处理。

1.2 查询缓存

接着数据库服务器会先去查询是否有该SQL语句的缓存,key是查询的语句,value是查询的结果。如果你的查询能够直接命中,就会直接从缓存中拿出value来返回客户端。

注:查询不会被解析、不会生成执行计划、不会被执行。

1.3 查询优化处理,生成执行计划

如果没有命中缓存,则开始第三步。

  • 解析SQL:生成解析树,验证关键字如select,where,left join 等)是否正确。
  • 预处理:进一步检查解析树是否合法,如检查数据表和列是否存在,验证用户权限等。
  • 优化SQL:决定使用哪个索引,或者在多个表相关联的时候决定表的连接顺序。紧接着,将SQL语句转成执行计划。

1.4 将查询结果返回客户端

最后,数据库服务器将查询结果返回给客户端。(如果查询可以缓存,MySQL也会将结果放到查询缓存中)

在这里插入图片描述

这就是一条查询语句的执行流程,可以看到索引出现在优化SQL的流程步骤中,接下来了解索引到底是什么?

二、索引概述

先简单地了解一下索引的基本概念。

2.1 索引是什么

索引是帮助数据库高效获取数据的数据结构。

2.2 索引的分类

1)从存储结构上来划分

  • Btree索引(B+tree,B-tree)
  • 哈希索引
  • full-index全文索引
  • RTree

2)从应用层次上来划分

  • 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引。
  • 唯一索引:索引列的值必须唯一,但允许有空值。
  • 复合索引:一个索引包含多个列。

3)从表记录的排列顺序和索引的排列顺序是否一致来划分

  • 聚集索引:表记录的排列顺序和索引的排列顺序一致。
  • 非聚集索引:表记录的排列顺序和索引的排列顺序不一致。

2.3 聚集索引和非聚集索引

1)简单概括

  • 聚集索引:就是以主键创建的索引。
  • 非聚集索引:就是以非主键创建的索引(也叫做二级索引)。

2)详细概括

  • 聚集索引

聚集索引表记录的排列顺序和索引的排列顺序一致,所以查询效率快,因为只要找到第一个索引值记录,其余的连续性的记录在物理表中也会连续存放,一起就可以查询到。

缺点:新增比较慢,因为为了保证表中记录的物理顺序和索引顺序一致,在记录插入的时候,会对数据页重新排序。

  • 非聚集索引

索引的逻辑顺序与磁盘上行的物理存储顺序不同,非聚集索引在叶子节点存储的是主键和索引列,当我们使用非聚集索引查询数据时,需要拿到叶子上的主键再去表中查到想要查找的数据。这个过程就是我们所说的回表。

3)聚集索引和非聚集索引的区别

  • 聚集索引在叶子节点存储的是表中的数据。
  • 非聚集索引在叶子节点存储的是主键和索引列。
举个例子

比如汉语字典,想要查「阿」字,只需要翻到字典前几页,a开头的位置,接着「啊」「爱」都会出来。也就是说,字典的正文部分本身就是一个目录,不需要再去查其他目录来找到需要找的内容。我们把这种正文内容本身就是一种按照一定规则排列的目录称为==聚集索引==。

如果遇到不认识的字,只能根据“偏旁部首”进行查找,然后根据这个字后的页码直接翻到某页来找到要找的字。但结合部首目录和检字表而查到的字的排序并不是真正的正文的排序方法。

在这里插入图片描述

比如要查“玉”字,我们可以看到在查部首之后的检字表中“玉”的页码是587页,然后是珏,是251页。很显然,在字典中这两个字并没有挨着,现在看到的连续的“玉、珏、莹”三字实际上就是他们在非聚集索引中的排序,是字典正文中的字在非聚集索引中的映射。我们可以通过这种方式来找到所需要的字,但它需要两个过程,先找到目录中的结果,然后再翻到结果所对应的页码。我们把这种目录纯粹是目录,正文纯粹是正文的排序方式称为==非聚集索引==。

2.4 MySQL如何添加索引

1)添加PRIMARY KEY(主键索引)

ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` )

2)添加UNIQUE(唯一索引)

ALTER TABLE `table_name` ADD UNIQUE (`column`)

3)添加INDEX(普通索引)

ALTER TABLE `table_name` ADD INDEX index_name (`column` )

4)添加FULLTEXT(全文索引)

ALTER TABLE `table_name` ADD FULLTEXT (`column`)

5)添加多列索引

ALTER TABLE `table_name` ADD INDEX index_name (`column1`,`column2`,`column3`)

三、索引底层数据结构

了解了索引的基本概念后,可能最好奇的就是索引的底层是怎么实现的呢?为什么索引可以如此高效地进行数据的查找?如何设计数据结构可以满足我们的要求?
下文通过一般程序员的思维来想一下如果是我们来设计索引,要如何设计来达到索引的效果。

3.1 哈希索引

可能直接想到的就是用哈希表来实现快速查找,就像我们平时用的hashmap一样,value = get(key) O(1)时间复杂度一步到位,确实,哈希索引是一种方式。

1)定义

哈希索引就是采用一定的哈希算法,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。本质上就是把键值换算成新的哈希值,根据这个哈希值来定位。

在这里插入图片描述

2)局限性

  • 哈希索引没办法利用索引完成排序。
  • 不能进行多字段查询。
  • 在有大量重复键值的情况下,哈希索引的效率也是极低的(出现哈希碰撞问题)。
  • 不支持范围查询。

在MySQL常用的InnoDB引擎中,还是使用B+树索引比较多。InnoDB是自适应哈希索引的(hash索引的创建由==InnoDB存储引擎自动优化创建==,我们干预不了)。

3.2 如何设计索引的数据结构呢

假设要查询某个区间的数据,我们只需要拿到区间的起始值,然后在树中进行查找。

如数据为:

在这里插入图片描述

1)查询[7,30]区间的数据

在这里插入图片描述

在这里插入图片描述

当查找到起点节点10后,再顺着链表进行遍历,直到链表中的节点数据大于区间的终止值为止。所有遍历到的数据,就是符合区间值的所有数据。

2)还可以怎么优化呢?

利用二叉查找树,区间查询的功能已经实现了。但是,为了节省内存,我们只能把树存储在硬盘中。

那么,每个节点的读取或者访问,都对应一次硬盘IO操作。每次查询数据时磁盘IO操作的次数,也叫做==IO渐进复杂度==,也就是==树的高度==。

所以,我们要减少磁盘IO操作的次数,也就是要==降低树的高度==。

结构优化过程如下图所示:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里将二叉树变为了M叉树,降低了树的高度,那么这个M应该选择多少才合适呢?

问题:对于相同个数的数据构建m叉树索引,m叉树中的m越大,那树的高度就越小,那m叉树中的m是不是越大越好呢?到底多大才合适呢?

不管是内存中的数据还是磁盘中的数据,操作系统都是按页(一页的大小通常是4kb,这个值可以通过getconfig(PAGE_SIZE)命令查看)来读取的,一次只会读取一页的数据。

如果要读取的数据量超过了一页的大小,就会触发多次IO操作。所以在选择m大小的时候,要尽量让每个节点的大小等于一个页的大小。

一般实际应用中,出度d(树的分叉数)是非常大的数字,通常超过100;==树的高度(h)非常小,通常不超过3==。

3.3 B树

顺着解决问题的思路知道了我们想要的数据结构是什么。目前索引常用的数据结构是B+树,先介绍一下什么是B树(也就是B-树)。

1)B树的特点:

  • 关键字分布在整棵树的所有节点。
  • 任何一个关键字出现且只出现在一个节点中。
  • 搜索有可能在非叶子节点结束。
  • 其搜索性能等价于在关键字全集内做一次二分查找。

如下图所示:

在这里插入图片描述

3.4 B+树

了解了B树,再来看一下B+树,也是MySQL索引大部分情况所使用的数据结构。

在这里插入图片描述

在这里插入图片描述

1)B+树基本特点

  • 非叶子节点的子树指针与关键字个数相同。
  • 非叶子节点的子树指针P[i],指向关键字属于 [k[i],K[i+1])的子树(注意:区间是前闭后开)。
  • 为所有叶子节点增加一个链指针。
  • 所有关键字都在叶子节点出现。

这些基本特点是为了满足以下的特性。

2)B+树的特性

  • 所有的关键字都出现在叶子节点的链表中,且链表中的关键字是有序的。
  • 搜索只在叶子节点命中。
  • 非叶子节点相当于是叶子节点的索引层,叶子节点是存储关键字数据的数据层。

3)相对B树,B+树做索引的优势

  • B+树的磁盘读写代价更低。B+树的内部没有指向关键字具体信息的指针,所以其内部节点相对B树更小,如果把所有关键字存放在同一块盘中,那么盘中所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相应的,IO读写次数就降低了。
  • 树的查询效率更加稳定。B+树所有数据都存在于叶子节点,所有关键字查询的路径长度相同,每次数据的查询效率相当。而B树可能在非叶子节点就停止查找了,所以查询效率不够稳定。
  • B+树只需要去遍历叶子节点就可以实现整棵树的遍历。

3.5 MongoDB的索引为什么选择B树,而MySQL的索引是B+树?

因为MongoDB不是传统的关系型数据库,而是以Json格式作为存储的NoSQL非关系型数据库,目的就是高性能、高可用、易扩展。摆脱了关系模型,所以范围查询和遍历查询的需求就没那么强烈了。

3.6 MyISAM存储引擎和InnoDB的索引有什么区别

1)MyISAM存储引擎

在这里插入图片描述

  • 主键索引

MyISAM的索引文件(.MYI)和数据文件(.MYD)文件是分离的,索引文件仅保存记录所在页的指针(物理位置),通过这些指针来读取页,进而读取被索引的行。

树中的叶子节点保存的是对应行的物理位置。通过该值,==存储引擎能顺利地进行回表查询,得到一行完整记录==。

同时,每个叶子也保存了指向下一个叶子的指针,从而方便叶子节点的范围遍历。

  • 辅助索引

在MyISAM中,主键索引和辅助索引在结构上没有任何区别,==只是主键索引要求key是唯一的,而辅助索引的key可以重复==。

1)Innodb存储引擎

Innodb的主键索引和辅助索引之前提到过,再回顾一次。

  • 主键索引

在这里插入图片描述

InnoDB主键索引中既存储了主健值,又存储了行数据。

  • 辅助索引

在这里插入图片描述

对于辅助索引,InnoDB采用的方式是在叶子节点中保存主键值,通过这个主键值来回表查询到一条完整记录,因此按辅助索引检索其实进行了二次查询,效率是没有主键索引高的。

四、MySQL索引失效

在上一节中了解了索引的多种数据结构,以及B树和B+树的对比等,大家应该对索引的底层实现有了初步的了解。这一节从应用层的角度出发,看一下如何建索引更能满足我们的需求,以及MySQL索引什么时候会失效的问题。

先来思考一个小问题。

问题:当查询条件为2个及2个以上时,是创建多个单列索引还是创建一个联合索引好呢?它们之间的区别是什么?哪个效率高呢?

先来建立一些单列索引进行测试:

在这里插入图片描述

这里建立了一张表,里面建立了三个单列索引userId,mobile,billMonth。

然后进行多列查询。

explain select * from `t_mobilesms_11` where userid = '1' and mobile = '13504679876' and billMonth = '1998-03'

在这里插入图片描述

我们发现查询时只用到了userid这一个单列索引,这是为什么呢?因为这取决于MySQL优化器的优化策略。

当多条件联合查询时,优化器会评估哪个条件的索引效率高,它会选择最佳的索引去使用。也就是说,此处三个索引列都可能被用到,只不过优化器判断只需要使用userid这一个索引就能完成本次查询,故最终explain展示的key为userid。

4.1 总结

多个单列索引在多条件查询时优化器会选择最优索引策略,可能只用一个索引,也可能将多个索引都用上。

但是多个单列索引底层会建立多个B+索引树,比较占用空间,也会浪费搜索效率
所以多条件联合查询时最好建联合索引。

那联合索引就可以三个条件都用到了吗?会出现索引失效的问题吗?

4.2 联合索引失效问题

该部分参考并引用文章:

一张图搞懂MySQL的索引失效

创建user表,然后建立 name, age, pos, phone 四个字段的联合索引
全值匹配(索引最佳)。

在这里插入图片描述

索引生效,这是最佳的查询。

那么时候会失效呢?

1)违反最左匹配原则

最左匹配原则:最左优先,以最左边的为起点任何连续的索引都能匹配上,如不连续,则匹配不上。

如:建立索引为(a,b)的联合索引,那么只查 where b = 2 则不生效。换句话说:如果建立的索引是(a,b,c),也只有(a),(a,b),(a,b,c)三种查询可以生效。

在这里插入图片描述

这里跳过了最左的name字段进行查询,发现索引失效了。

遇到范围查询(>、<、between、like)就会停止匹配。

比如:a= 1 and b = 2 and c>3 and d =4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,因为c字段是一个范围查询,它之后的字段会停止匹配。

2)在索引列上做任何操作

如计算、函数、(手动或自动)类型转换等操作,会导致索引失效而进行全表扫描。

explain select * from user where left(name,3) = 'zhangsan' and age =20

在这里插入图片描述

这里对name字段进行了left函数操作,导致索引失效。

3)使用不等于(!= 、<>)

explain select * from user where age != 20;

在这里插入图片描述

explain select * from user where age <> 20;

在这里插入图片描述

4)like中以通配符开头('%abc')

索引失效

explain select * from user where name like ‘%zhangsan’;   

在这里插入图片描述

索引生效

explain select * from user where name like ‘zhangsan%’;       

在这里插入图片描述

5)字符串不加单引号索引失效

explain select * from user where name = 2000;

在这里插入图片描述

6)or连接索引失效

explain select * from user where name = ‘2000’ or age = 20 or pos =‘cxy’;   

在这里插入图片描述

7)order by

正常(索引参与了排序),没有违反最左匹配原则。

explain select * from user where name = 'zhangsan' and age = 20 order by age,pos;

在这里插入图片描述

违反最左前缀法则,导致额外的文件排序(会降低性能)。

explain select name,age from user where name = 'zhangsan' order by pos;

在这里插入图片描述

8)group by

正常(索引参与了排序)。

explain select name,age from user where name = 'zhangsan' group by age;

违反最左前缀法则,导致产生临时表(会降低性能)。

explain select name,age from user where name = 'zhangsan' group by pos,age;

在这里插入图片描述

五、总结

  • 了解一条查询语句是如何执行的,发现建立索引是一种可以高效查找的数据结构。
  • 了解了索引的各种分类情况,聚集索引和非聚集索引的区别,如何创建各种索引。
  • 通过需求一步步分析出为什么MySQL要选b+tree作为索引的数据结构,对比了btree和b+tree的区别、 MyISAM和innodb中索引的区别。
  • 了解了索引会失效的多种情况,比较重要的最左匹配原则,相应地我们可以在建索引的时候做一些优化。

希望大家能够多去使用索引进行SQL优化,有问题欢迎指出。

来源:宜信技术学院

作者:杨亨

查看原文

小丶怪 收藏了文章 · 2019-12-24

从零开始搭建一个简单的基于webpack的vue开发环境

更新:2019/07/28

从零开始搭建一个简单的基于webpack的react开发环境

原文:

都8102年了,现在还来谈webpack的配置,额,是有点晚了。而且,基于vue-cli或者create-react-app生成的项目,也已经一键为我们配置好了webpack,看起来似乎并不需要我们深入了解。

不过,为了学习和理解webpack解决了前端的哪些痛点,还是有必要从零开始自己搭建一个简单的开发环境。本文的webpack配置参考了vue-cli提供webpack-simple 模板,这也是vue-cli里面最简单的一个webpack配置,非常适合从零开始学习。

注: 本文webpack基于3.10.0, webpack4部分内容可能不再适用!!!!

演示代码下载

安装webpack

npm i webpack -g

webpack4还要单独安装

npm i webpack-cli -g

项目初始化

新建一个文件夹vue-webpack-simple

新建package.json

npm init -y

安装vue webpack webpack-dev-server

npm i vue --save
npm i webpack webpack-dev-server --save-dev

根目录下新建index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    
</body>
</html>

根目录下新建webpack.config.js

var path = require('path');
var webpack = require('webpack');

module.exports = {};

新建src文件夹,src文件夹下新建main.js

目前整个项目的结构如下:

js模块化

在ES6出现之前,js是没有统一的模块体系。
服务器端使用CommonJS规范,而浏览器端又有AMD和CMD两种规范

webpack的思想就是一切皆模块,官方推荐使用commonJS规范,这使得我们浏览器端也可以使用commonJS的模块化写法

module.exports = {}

src目录下新建一个util.js

module.exports = function say() {
    console.log('hello world');
}

main.js

var say = require('./util');
say();

修改webpack.config.js

var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './src/main.js', // 项目的入口文件,webpack会从main.js开始,把所有依赖的js都加载打包
    output: {
        path: path.resolve(__dirname, './dist'), // 项目的打包文件路径
        publicPath: '/dist/', // 通过devServer访问路径
        filename: 'build.js' // 打包后的文件名
    },
    devServer: {
        historyApiFallback: true,
        overlay: true
    }
};

修改package.josn

"scripts": {
    "dev": "webpack-dev-server --open --hot",
    "build": "webpack --progress --hide-modules"
  },

注意:webpack-dev-server会自动启动一个静态资源web服务器 --hot参数表示启动热更新

修改index.html,引入打包后的文件

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script data-original="/dist/build.js"></script>
</body>

</html>

运行

npm run dev

可以发现浏览器自动打开的一个页面,查看控制台,有hello world打出

我们随意修改util.js,可以发现浏览器会自动刷新,非常方便。

如果我们希望看打包后的bundle.js文件,运行

npm run build

可以看到生成了一个dist目录,里面就有打包好后的bundle.js

webpack默认不支持转码es6,但是importexport这两个语法却单独支持。所以我们可以改写前面的模块化写法

util.js

export default function say() {
    console.log('hello world ');
}

main.js

import say from './util';

say();

引入vue

下面我们来试着引入vue(目前不考虑单文件.vue)

main.js

import Vue from 'vue';

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
});

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        {{message}}
    </div>
    <script data-original="/dist/build.js"></script>
    
</body>

</html>

还要注意一点:要修改webpack.config.js文件

var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: 'build.js'
    },
    devServer: {
        historyApiFallback: true,
        overlay: true
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    }
};

重新运行npm run dev,可以看到,页面正常显示了Hello World

引入scss和css

webpack默认只支持js的模块化,如果需要把其他文件也当成模块引入,就需要相对应的loader解析器

npm i node-sass css-loader vue-style-loader sass-loader --save-dev

webpack.config.js

var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, './dist'),
        publicPath: '/dist/',
        filename: 'build.js'
    },
    devServer: {
        historyApiFallback: true,
        overlay: true
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ],
            }
        ]
    }
};

解释:

{
    test: /\.css$/,
    use: [
        'vue-style-loader',
        'css-loader'
    ],
}

这段代码意思是:匹配后缀名为css的文件,然后分别用css-loader,vue-style-loader去解析
解析器的执行顺序是从下往上(先css-loader再vue-style-loader)

注意:因为我们这里用vue开发,所以使用vue-style-loader,其他情况使用style-loader

css-loader使得我们可以用模块化的写法引入css,vue-style-loader会将引入的css插入到html页面里的style标签里

要引入scss也是同理的配置写法:

module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ],
            },
            {
                test: /\.scss$/,
                use: [
                    'vue-style-loader',
                    'css-loader',
                    'sass-loader'
                ],
            },
            {
                test: /\.sass$/,
                use: [
                    'vue-style-loader',
                    'css-loader',
                    'sass-loader?indentedSyntax'
                ],
            }]
    }

我们现在来试下
在src目录下新建style目录,style目录里新建common.scss

body {
    background: #fed;
}

main.js

import './style/common.scss';

发现css样式有用了

使用babel转码

ES6的语法大多数浏览器依旧不支持,bable可以把ES6转码成ES5语法,这样我们就可以大胆的在项目中使用最新特性了

npm i babel-core babel-loader babel-preset-env babel-preset-stage-3 --save-dev

在项目根目录新建一个.babelrc文件

{
  "presets": [
    ["env", { "modules": false }],
    "stage-3"
  ]
}

webpack.config.js添加一个loader

{
    test: /\.js$/,
    loader: 'babel-loader',
    exclude: /node_modules/
}

exclude表示忽略node_modules文件夹下的文件,不用转码

现在我们来试下async await语法吧
util.js

export default function getData() {
    return new Promise((resolve, reject) => {
        resolve('ok');
    })
}

main.js

import getData from './util';
import Vue from 'vue';

import './style/common.scss';

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    async fetchData() {
      const data = await getData();
      this.message = data;
    }
  },
  created() {
    this.fetchData();
  }
});

这时控制台会报一个错误regeneratorRuntime is not defined,因为我们没有安装babel-polyfill

npm i babel-polyfill --save-dev

然后修改webpack.config.js的入口

entry: ['babel-polyfill', './src/main.js'],

重新npm run dev,可以发现正常运行了

引入图片资源

把图片也当成模块引入

npm i file-loader --save-dev

webpack.config.js添加一个loader

{
    test: /\.(png|jpg|gif|svg)$/,
    loader: 'file-loader',
    options: {
        name: '[name].[ext]?[hash]'
    }
}

在src目录下新建一个img目录,存放一张图片logo.png

修改main.js

import getData from './util';
import Vue from 'vue';

import './style/common.scss';


Vue.component('my-component', {
  template: '<img :data-original="url" />',
  data() {
    return {
      url: require('./img/logo.png')
    }
  }
})

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue !'
  },
  methods: {
    async fetchData() {
      const data = await getData();
      this.message = data;
    }
  },
  created() {
    this.fetchData()
  }
});

修改index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        {{message}}
        <my-component/>
    </div>
    <script data-original="/dist/build.js"></script>
    
</body>

</html>

可以看见,图片也被正确加载了

单文件组件

在前面的例子里,我们使用 Vue.component 来定义全局组件
在实际项目里,更推荐使用单文件组件

npm i vue-loader vue-template-compiler --save-dev

添加一个loader

{
    test: /\.vue$/,
    loader: 'vue-loader',
    options: {
        loaders: {
            'scss': [
                'vue-style-loader',
                'css-loader',
                'sass-loader'
            ],
            'sass': [
                'vue-style-loader',
                'css-loader',
                'sass-loader?indentedSyntax'
            ]
        }
    }
}

在src目录下新建一个App.vue

<template>
  <div id="app">
    <h1>{{ msg }}</h1>
    <img data-original="./img/logo.png">
    <input type="text" v-model="msg">
  </div>
</template>

<script>

import getData from './util';

export default {
  name: 'app',
  data () {
    return {
      msg: 'Welcome to Your Vue.js'
    }
  },
  created() {
    this.fetchData();
  },
  methods: {
     async fetchData() {
      const data = await getData();
      this.msg = data;
    }
  }
}
</script>

<style lang="scss">
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;

  h1 {
    color: green;
  }
}
</style>

main.js

import Vue from 'vue';
import App from './App.vue';

import './style/common.scss';

new Vue({
  el: '#app',
  template: '<App/>',
  components: { App }
})

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app"></div>
    <script data-original="/dist/build.js"></script>
</body>

</html>

npm run dev,可以发现单文件被正确加载了

source-map

在开发阶段,调试也是非常重要的一项需求。

App.vue

created() {
    this.fetchData();
    console.log('23333');
}

我们故意打一个console,打开控制台

我们点击进入这个console的详细地址

进入的是打包后的build.js,我并不知道是在哪个组件里写的,这就造成了调试困难

这时就要修改webpack.config.js

module.exports = {
    entry: ['babel-polyfill', './src/main.js'],
    // 省略其他...

    devtool: '#eval-source-map'
};

重新npm run dev

这次调试,它直接返回那个组件的源代码了,这不是被打包过的!

打包发布

我们先试着npm run build打包一下文件

会发现,打包后的build.js非常大,有500多k了

在实际发布时,会对文件进行压缩,缓存,分离等等优化处理

npm i cross-env --save-dev

修改package.json

"scripts": {
    "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
}

这次我们设置了环境变量,打包时,NODE_ENV是production

然后修改webpack.config.js,判断NODE_ENV为production时,压缩js代码

var path = require('path');
var webpack = require('webpack');

module.exports = {
    // 省略...
}

if (process.env.NODE_ENV === 'production') {
    module.exports.devtool = '#source-map';
    module.exports.plugins = (module.exports.plugins || []).concat([
      new webpack.DefinePlugin({
        'process.env': {
          NODE_ENV: '"production"'
        }
      }),
      new webpack.optimize.UglifyJsPlugin(),
    ])
  }
  

重新打包

可以看见,压缩效果非常明显!

至此,一个非常简单的vue开发环境搭建成功。

注意:本文中的配置还有非常多可以优化的地方,比如分离js和css

读者可以自行了解相关知识,这里只是带领大家了解最基础的webpack配置。

查看原文

小丶怪 收藏了文章 · 2019-11-13

Vue原理解析——自己写个Vue

Vue由于其高效的性能和灵活入门简单、轻量的特点下变得火热。在当今前端越来越普遍的使用,今天来剖析一下Vue的深入响应式原理。

tips:转自我的博客唐益达博客,此为原创。转载请注明出处,原文链接


一、Vue对比其他框架原理

Vue相对于React,Angular更加综合一点。AngularJS则使用了“脏值检测”。

React则采用避免直接操作DOM的虚拟dom树。而Vue则采用的是 Object.defineProperty特性(这在ES5中是无法slim的,这就是为什么vue2.0不支持ie8以下的浏览器)

Vue可以说是尤雨溪从Angular中提炼出来的,又参照了React的性能思路,而集大成的一种轻量、高效,灵活的框架。

二、Vue的原理

Vue的原理可以简单地从下列图示所得出

  1. 通过建立虚拟dom树document.createDocumentFragment(),方法创建虚拟dom树。
  2. 一旦被监测的数据改变,会通过Object.defineProperty定义的数据拦截,截取到数据的变化。
  3. 截取到的数据变化,从而通过订阅——发布者模式,触发Watcher(观察者),从而改变虚拟dom的中的具体数据。
  4. 最后,通过更新虚拟dom的元素值,从而改变最后渲染dom树的值,完成双向绑定

Vue的模式是m-v-vm模式,即(model-view-modelView),通过modelView作为中间层(即vm的实例),进行双向数据的绑定与变化。

而实现这种双向绑定的关键就在于:

Object.defineProperty订阅——发布者模式浙两点。

下面我们通过实例来实现Vue的基本双向绑定。

三、Vue双向绑定的实现

3.1 简易双绑

首先,我们把注意力集中在这个属性上:Object.defineProperty。

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法:Object.defineProperty(obj, prop, descriptor)

什么叫做,定义或修改一个对象的新属性,并返回这个对象呢?

var obj = {};
Object.defineProperty(obj,'hello',{
  get:function(){
    //我们在这里拦截到了数据
    console.log("get方法被调用");
  },
  set:function(newValue){
    //改变数据的值,拦截下来额
    console.log("set方法被调用");
  }
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值

输出结果如下:

clipboard.png

可以从这里看到,这是在对更底层的对象属性进行编程。简单地说,也就是我们对其更底层对象属性的修改或获取的阶段进行了拦截(对象属性更改的钩子函数)。

在这数据拦截的基础上,我们可以做到数据的双向绑定:

var obj = {};
Object.defineProperty(obj,'hello',{
  get:function(){
    //我们在这里拦截到了数据
    console.log("get方法被调用");
  },
  set:function(newValue){
    //改变数据的值,拦截下来额
    console.log("set方法被调用");
    document.getElementById('test').value = newValue;
    document.getElementById('test1').innerHTML = newValue;
  }
});
//obj.hello;
//obj.hello = '123';
document.getElementById('test').addEventListener('input',function(e){
  obj.hello = e.target.value;//触发它的set方法
})

html:

<div id="mvvm">
     <input v-model="text" id="test"></input>
      <div id="test1"></div>
  </div>

在线演示:demo演示

在这我们可以简单的实现了一个双向绑定。但是到这还不够,我们的目的是实现一个Vue。

3.2 Vue初始化(虚拟节点的产生与编译)

3.2.1 Vue的虚拟节点容器
function nodeContainer(node, vm, flag){
  var flag = flag || document.createDocumentFragment();

  var child;
  while(child = node.firstChild){
    compile(child, vm);
    flag.appendChild(child);
    if(child.firstChild){
      // flag.appendChild(nodeContainer(child,vm));
      nodeContainer(child, vm, flag);
    }
  }
  return flag;
}

这里几个注意的点:

  1. while(child = node.firstChild)把node的firstChild赋值成while的条件,可以看做是遍历所有的dom节点。一旦遍历到底了,node的firstChild就会未定义成undefined就跳出while。
  2. document.createDocumentFragment();是一个虚拟节点的容器树,可以存放我们的虚拟节点。
  3. 上面的函数是个迭代,一直循环到节点的终点为止。
3.2.2 Vue的节点初始化编译

先声明一个Vue对象

function Vue(options){
  this.data = options.data;
  
  var id = options.el;
  var dom = nodeContainer(document.getElementById(id),this);
  document.getElementById(id).appendChild(dom);  
}

//随后使用他
var Demo = new Vue({
  el:'mvvm',
  data:{
    text:'HelloWorld',
    d:'123'
  }
})

接下去的具体得初始化内容

//编译
function compile(node, vm){
  var reg = /\{\{(.*)\}\}/g;//匹配双绑的双大括号
  if(node.nodeType === 1){
    var attr = node.attributes;
    //解析节点的属性
    for(var i = 0;i < attr.length; i++){
      if(attr[i].nodeName == 'v-model'){
        var name = attr[i].nodeValue;
        node.value = vm.data[name];//讲实例中的data数据赋值给节点
        //node.removeAttribute('v-model');
      }
    }
  }
  //如果节点类型为text
  if(node.nodeType === 3){
    
    if(reg.test(node.nodeValue)){
      // console.dir(node);
      var name = RegExp.$1;//获取匹配到的字符串
      name = name.trim();
      node.nodeValue = vm.data[name];
    }
  }
}

代码解释:

  1. 当nodeType为1的时候,表示是个元素。同时我们进行判断,如果节点中的指令含有v-model这个指令,那么我们就初始化,进行对节点的值的赋值。
  2. 如果nodeType为3的时候,也就是text节点属性。表示你的节点到了终点,一般都是节点的前后末端。我们常常在这里定义我们的双绑值。此时一旦匹配到了双绑(双大括号),即进行值的初始化。

至此,我们的Vue初始化已经完成。

clipboard.png

在线演示:demo1

3.3 Vue的声明响应式

3.3.1 定义Vue的data的属性响应式
function defineReactive (obj, key, value){
  Object.defineProperty(obj,key,{
    get:function(){
      console.log("get了值"+value);
      return value;//获取到了值
    },
    set:function(newValue){
      if(newValue === value){
        return;//如果值没变化,不用触发新值改变
      }
      value = newValue;//改变了值
      console.log("set了最新值"+value);
    }
  })
}

这里的obj我们这定义为vm实例或者vm实例里面的data属性。

PS:这里强调一下,defineProperty这个方法,不仅可以定义obj的直接属性,比如obj.hello这个属性。也可以间接定义属性比如:obj.middle.hello。这里导致的效果就是两者的hello属性都被定义成响应式了。

用下列的observe方法循环调用响应式方法。

function observe (obj,vm){
  Object.keys(obj).forEach(function(key){
    defineReactive(vm,key,obj[key]);
  })
}

然后再Vue方法中初始化:

function Vue(options){
  this.data = options.data;
  var data = this.data;
  -------------------------
  observe(data,this);//这里调用定义响应式方法
  -------------------------
  var id = options.el;
  var dom = nodeContainer(document.getElementById(id),this);
  document.getElementById(id).appendChild(dom); //把虚拟dom渲染上去 
}

在编译方法中v-model属性找到的时候去监听:

function compile(node, vm){
  var reg = /\{\{(.*)\}\}/g;
  if(node.nodeType === 1){
    var attr = node.attributes;
    //解析节点的属性
    for(var i = 0;i < attr.length; i++){
      if(attr[i].nodeName == 'v-model'){
        
        var name = attr[i].nodeValue;
        -------------------------//这里新添加的监听
        node.addEventListener('input',function(e){
          console.log(vm[name]);
          vm[name] = e.target.value;//改变实例里面的值
        });
        -------------------------
        node.value = vm[name];//讲实例中的data数据赋值给节点
        //node.removeAttribute('v-model');
      }
    }
  }
}

以上我们实现了,你再输入框里面输入,同时触发getter&setter,去改变vm实例中data的值。也就是说MVVM的图例中经过getter&setter已经成功了。接下去就是订阅——发布者模式。

在线演示:demo2

实现效果:

clipboard.png

3.4 订阅——发布者模式

什么是订阅——发布者?简单点说:你微信里面经常会订阅一些公众号,一旦这些公众号发布新消息了。那么他就会通知你,告诉你:我发布了新东西,快来看。

这种情景下,你就是订阅者,公众号就是发布者

所以我们要模拟这种情景,我们先声明3个订阅者:

var sub1 = {
  update:function(){
    console.log(1);
  }
}
var sub2 = {
  update:function(){
    console.log(2);
  }
}
var sub3 = {
  update:function(){
    console.log(3);
  }
}

每个订阅者对象内部声明一个update方法来触发订阅属性。

再声明一个发布者,去触发发布消息,通知的方法::

function Dep(){
  this.subs = [sub1,sub2,sub3];//把三个订阅者加进去
}
Dep.prototype.notify = function(){//在原型上声明“发布消息”方法
  this.subs.forEach(function(sub){
    sub.update();
  })
}
var dep = new Dep();
//pub.publish();
dep.notify();

我们也可以声明另外一个中间对象

var dep = new Dep();
var pub = {
  publish:function(){
    dep.notify();
  }
}
pub.publish();//这里的结果是跟上面一样的

实现效果:

clipboard.png

到这,我们已经实现了:

  1. 修改输入框内容 => 触发修改vm实例里的属性值 => 触发set&get方法
  2. 订阅成功 => 发布者发出通知notify() => 触发订阅者的update()方法

接下来重点要实现的是:如何去更新视图,同时把订阅——发布者模式进去watcher观察者模式?

3.5 观察者模式

先定义发布者:

function Dep(){
  this.subs = [];
}
Dep.prototype ={
  add:function(sub){//这里定义增加订阅者的方法
    this.subs.push(sub);
  },
  notify:function(){//这里定义触发订阅者update()的通知方法
    this.subs.forEach(function(sub){
      console.log(sub);
      sub.update();//下列发布者的更新方法
    })
  }
}

再定义观察者(订阅者):

function Watcher(vm,node,name){
  Dep.global = this;//这里很重要!把自己赋值给Dep函数对象的全局变量
  this.name = name;
  this.node = node;
  this.vm = vm;
  this.update();
  Dep.global = null;//这里update()完记得清空Dep函数对象的全局变量
}
Watcher.prototype.update = function(){
    this.get();
    switch (this.node.nodeType) { //这里去通过判断节点的类型改变视图的值
      case 1: 
        this.node.value = this.value;
        break;
      case 3:
        this.node.nodeValue = this.value;
        break;
      default: break;
    };
}
Watcher.prototype.get = function(){
    this.value = this.vm[this.name];//这里把this的value值赋值,触发data的defineProperty方法中的get方法!
}

以上需要注意的点:

  1. 在Watcher函数对象的原型方法update里面更新视图的值(实现watcher到视图层的改变)。
  2. Watcher函数对象的原型方法get,是为了触发defineProperty方法中的get方法!
  3. 在new一个Watcher的对象的时候,记得把Dep函数对象赋值一个全局变量,而且及时清空。至于为什么这么做,我们接下来看。
function defineReactive (obj, key, value){
  var dep = new Dep();//这里每一个vm的data属性值声明一个新的订阅者
  Object.defineProperty(obj,key,{
    get:function(){
      console.log(Dep.global);
      -----------------------
      if(Dep.global){//这里是第一次new对象Watcher的时候,初始化数据的时候,往订阅者对象里面添加对象。第二次后,就不需要再添加了
        dep.add(Dep.global);
      }
      -----------------------
      return value;
    },
    set:function(newValue){
      if(newValue === value){
        return;
      }
      value = newValue;
      dep.notify();//触发了update()方法
    }
  })
}

这里有一点需要注意:

在上述圈起来的地方:if(Dep.global)是在第一次new Watcher()的时候,进入update()方法,触发这里的get方法。这里非常的重要的一点!在此时new Watcher()只走到了this.update();方法,此刻没有触发Dep.global = null函数,所以值并没有清空,所以可以进到dep.add(Dep.global);方法里面去。

而第二次后,由于清空了Dep的全局变量,所以不会触发add()方法。

PS:这个思路容易被忽略,由于是参考之前一个博主的代码影响,我自己想了很多方法改变,但是在这种情景下难以实现别的更好的交互方式。

所以我暂时现在只能使用Dep的全局变量的方式,来实现Dep函数与Watcher函数的交互。(如果是ES6的模块化方法会不一样)

而后我会尽量找寻其他更好的方法来实现Dep函数与Watcher函数的交互。

紧接着在text节点和绑定了的input节点(别忘记了这个节点)new Watcher的方法来触发以上的内容:

// 如果节点为input
    if(node.nodeType === 1){ 
        ...........
        ----------
        new Watcher(vm,node,name) // 别忘记给input添加观察者模式
        ----------

    }
//如果节点类型为text
  if(node.nodeType === 3){
    
    if(reg.test(node.nodeValue)){
      // console.dir(node);
      var name = RegExp.$1;//获取匹配到的字符串
      name = name.trim();
      // node.nodeValue = vm[name];
      -------------------------
      new Watcher(vm,node,name);//这里到了一个新的节点,new一个新的观察者
      -------------------------
    }
  }

至此,vue双向绑定已经简单的实现。

3.6 最终效果

在线演示:Codepen实现Vue的demo(有时候要翻墙)

在线源码参考:demo4

下列是全部的源码,仅供参考。

HTML:

<div id="mvvm">
     <input v-model="d" id="test">{{text}}
    <div>{{d}}</div>
  </div>

JS:

var obj = {};

function nodeContainer(node, vm, flag){
  var flag = flag || document.createDocumentFragment();

  var child;
  while(child = node.firstChild){
    compile(child, vm);
    flag.appendChild(child);
    if(child.firstChild){
      nodeContainer(child, vm, flag);
    }
  }
  return flag;
}

//编译
function compile(node, vm){
  var reg = /\{\{(.*)\}\}/g;
  if(node.nodeType === 1){
    var attr = node.attributes;
    //解析节点的属性
    for(var i = 0;i < attr.length; i++){
      if(attr[i].nodeName == 'v-model'){
        
        var name = attr[i].nodeValue;
        node.addEventListener('input',function(e){
          vm[name] = e.target.value;
        });

        node.value = vm[name];//讲实例中的data数据赋值给节点
        node.removeAttribute('v-model');
      }
    }
  }
  //如果节点类型为text
  if(node.nodeType === 3){
    
    if(reg.test(node.nodeValue)){
      // console.dir(node);
      var name = RegExp.$1;//获取匹配到的字符串
      name = name.trim();
      // node.nodeValue = vm[name];
      new Watcher(vm,node,name);
    }
  }
}

function defineReactive (obj, key, value){
  var dep = new Dep();
  Object.defineProperty(obj,key,{
    get:function(){
      console.log(Dep.global);
      if(Dep.global){
        dep.add(Dep.global);
      }
      console.log("get了值"+value);
      return value;
    },
    set:function(newValue){
      if(newValue === value){
        return;
      }
      value = newValue;
      console.log("set了最新值"+value);
      dep.notify();
    }
  })
}

function observe (obj,vm){
  Object.keys(obj).forEach(function(key){
    defineReactive(vm,key,obj[key]);
  })
}

function Vue(options){
  this.data = options.data;
  var data = this.data;
  observe(data,this);
  var id = options.el;
  var dom = nodeContainer(document.getElementById(id),this);
  document.getElementById(id).appendChild(dom);  
}

function Dep(){
  this.subs = [];
}
Dep.prototype ={
  add:function(sub){
    this.subs.push(sub);
  },
  notify:function(){
    this.subs.forEach(function(sub){
      console.log(sub);
      sub.update();
    })
  }
}


function Watcher(vm,node,name){
  Dep.global = this;
  this.name = name;
  this.node = node;
  this.vm = vm;
  this.update();
  Dep.global = null;
}

Watcher.prototype = {
  update:function(){
    this.get();
    switch (this.node.nodeType) {
      case 1: 
        this.node.value = this.value;
        break;
      case 3:
        this.node.nodeValue = this.value;
        break;
      default: break;
    }
  },
  get:function(){
    this.value = this.vm[this.name];
  }
}


var Demo = new Vue({
  el:'mvvm',
  data:{
    text:'HelloWorld',
    d:'123'
  }
})

四、回顾

我们再来通过一张图回顾一下整个过程:

clipboard.png

从上可以看出,大概的过程是这样的:

  1. 定义Vue对象,声明vue的data里面的属性值,准备初始化触发observe方法。
  2. 在Observe定义过响应式方法Object.defineProperty()的属性,在初始化的时候,通过Watcher对象进行addDep的操作。即每定义一个vue的data的属性值,就添加到一个Watcher对象到订阅者里面去。
  3. 每当形成一个Watcher对象的时候,去定义它的响应式。即Object.defineProperty()定义。这就导致了一个Observe里面的getter&setter方法与订阅者形成一种依赖关系。
  4. 由于依赖关系的存在,每当数据的变化后,会导致setter方法,从而触发notify通知方法,通知订阅者我的数据改变了,你需要更新。
  5. 订阅者会触发内部的update方法,从而改变vm实例的值,以及每个Watcher里面对应node的nodeValue,即视图上面显示的值。
  6. Watcher里面接收到了消息后,会触发改变对应对象里面的node的视图的value值,而改变视图上面的值。
  7. 至此,视图的值改变了。形成了双向绑定MVVM的效果。

五、后记

至此,我们通过解析vue的绑定原理,实现了一个非常简单的Vue。

我们可以再借鉴此思路的情况下,进行我们需要的定制框架的二次开发。如果开发人数尚可的话,可以实现类似微信小程序自己有的一套框架。

我非常重视技术的原理,只有真正掌握技术的原理,才能在原有的技术上更好地去提高和开发。

ps:此文是较早之前写的,不够规范,后面会修改一个ES6的版本。下方是参考链接,灵感来源于其他博主,我进行了修正优化和代码解释。

参考链接:

  1. Vue.js双向绑定的实现原理
  2. Vue 源码解析:深入响应式原理
  3. 深入响应式原理

原文地址(原创博客):http://www.tangyida.top/detail/150

查看原文

小丶怪 收藏了文章 · 2019-10-23

手摸手,带你用vue撸后台 系列一(基础篇)

完整项目地址:vue-element-admin
系类文章二:手摸手,带你用vue撸后台 系列二(登录权限篇)
系类文章三:手摸手,带你用vue撸后台 系列三(实战篇)
系类文章四:手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
系类文章:手摸手,带你优雅的使用 icon
系类文章:手摸手,带你封装一个vue component

前言

说好的教程终于来了,第一篇文章主要来说一说在开始写实际业务代码之前的一些准备工作吧,但这里不会教你 webpack 的基础配置,热更新原理是什么,webpack速度优化等等,有需求的请自行 google,相关文章已经很多了。

目录结构

├── build                      // 构建相关  
├── config                     // 配置相关
├── src                        // 源代码
│   ├── api                    // 所有请求
│   ├── assets                 // 主题 字体等静态资源
│   ├── components             // 全局公用组件
│   ├── directive              // 全局指令
│   ├── filtres                // 全局 filter
│   ├── icons                  // 项目所有 svg icons
│   ├── lang                   // 国际化 language
│   ├── mock                   // 项目mock 模拟数据
│   ├── router                 // 路由
│   ├── store                  // 全局 store管理
│   ├── styles                 // 全局样式
│   ├── utils                  // 全局公用方法
│   ├── vendor                 // 公用vendor
│   ├── views                   // view
│   ├── App.vue                // 入口页面
│   ├── main.js                // 入口 加载组件 初始化等
│   └── permission.js          // 权限管理
├── static                     // 第三方不打包资源
│   └── Tinymce                // 富文本
├── .babelrc                   // babel-loader 配置
├── eslintrc.js                // eslint 配置项
├── .gitignore                 // git 忽略项
├── favicon.ico                // favicon图标
├── index.html                 // html模板
└── package.json               // package.json

这里来简单讲一下src文件

api 和 views

简单截取一下公司后台项目,现在后台大概有四五十个 api 模块

如图可见模块有很多,而且随着业务的迭代,模块还会会越来越多。
所以这里建议根据业务模块来划分 views,并且 将views 和 api 两个模块一一对应,从而方便维护。如下图:

如 article 模块下放的都是文章相关的 api,这样不管项目怎么累加,api和views的维护还是清晰的,当然也有一些全区公用的api模块,如七牛upload,remoteSearch等等,这些单独放置就行。

components

这里的 components 放置的都是全局公用的一些组件,如上传组件,富文本等等。一些页面级的组件建议还是放在各自views文件下,方便管理。如图:

store

这里我个人建议不要为了用 vuex 而用 vuex。就拿我司的后台项目来说,它虽然比较庞大,几十个业务模块,几十种权限,但业务之间的耦合度是很低的,文章模块和评论模块几乎是俩个独立的东西,所以根本没有必要使用 vuex 来存储data,每个页面里存放自己的 data 就行。当然有些数据还是需要用 vuex 来统一管理的,如登录token,用户信息,或者是一些全局个人偏好设置等,还是用vuex管理更加的方便,具体当然还是要结合自己的业务场景的。总之还是那句话,不要为了用vuex而用vuex!


webpack

这里是用 vue-cliwebpack-template 为基础模板构建的,如果你对这个有什么疑惑请自行google,相关的配置绍其它的文章已经介详细了,这里就不再展开了。简单说一些需要注意到地方。

jquery (本项目已移除)

管理后台不同于前台项目,会经常用到一些第三方插件,但有些插件是不得不依赖 jquery 的,如市面很多富文本基都是依赖 jquery 的,所以干脆就直接引入到项目中省事(gzip之后只有34kb,而且常年from cache,不要考虑那些吹毛求疵的大小问题,这几kb和提高的开发效率根本不能比)。但是如果第三方库的代码中出现$.xxx或jQuery.xxx或window.jQuery或window.$则会直接报错。要达到类似的效果,则需要使用 webpack 内置的 ProvidePlugin 插件,配置很简单,只需要

new webpack.ProvidePlugin({
  $: 'jquery' ,
  'jQuery': 'jquery'
})

这样当 webpack 碰到 require 的第三方库中出现全局的$、jQeury和window.jQuery 时,就会使用 node_module 下 jquery 包 export 出来的东西了。

alias

当项目逐渐变大之后,文件与文件直接的引用关系会很复杂,这时候就需要使用alias 了。
有的人喜欢alias 指向src目录下,再使用相对路径找文件

resolve: {
  alias: {
    '~': resolve(__dirname, 'src')
  }
}

//使用
import stickTop from '~/components/stickTop'

或者也可以

alias: {
  'src': path.resolve(__dirname, '../src'),
  'components': path.resolve(__dirname, '../src/components'),
  'api': path.resolve(__dirname, '../src/api'),
  'utils': path.resolve(__dirname, '../src/utils'),
  'store': path.resolve(__dirname, '../src/store'),
  'router': path.resolve(__dirname, '../src/router')
}

//使用
import stickTop from 'components/stickTop'
import getArticle from 'api/article'

没有好与坏对与错,纯看个人喜好和团队规范。


ESLint

不管是多人合作还是个人项目,代码规范是很重要的。这样做不仅可以很大程度地避免基本语法错误,也保证了代码的可读性。这所谓工欲善其事,必先利其器,个人推荐 eslint+vscode 来写 vue,绝对有种飞一般的感觉。效果如图:
eslintGif.gif
每次保存,vscode就能标红不符合eslint规则的地方,同时还会做一些简单的自我修正。安装步骤如下:

首先安装eslint插件
eslint1.png

安装并配置完成 ESLint 后,我们继续回到 VSCode 进行扩展设置,依次点击 文件 > 首选项 > 设置 打开 VSCode 配置文件,添加如下配置


    "files.autoSave":"off",
    "eslint.validate": [
       "javascript",
       "javascriptreact",
       "html",
       { "language": "vue", "autoFix": true }
     ],
     "eslint.options": {
        "plugins": ["html"]
     }

这样每次保存的时候就可以根据根目录下.eslintrc.js你配置的eslint规则来检查和做一些简单的fix。这里提供了一份我平时的eslint规则地址,都简单写上了注释。每个人和团队都有自己的代码规范,统一就好了,去打造一份属于自己的eslint 规则上传到npm吧,如饿了么团队的 config,vue的 config

vscode 插件和配置推荐


封装 axios

我们经常遇到一些线上 的bug,但测试环境很难模拟。其实可以通过简单的配置就可以在本地调试线上环境。
这里结合业务封装了axios ,线上代码

import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api的base_url
  timeout: 5000 // 请求超时时间
})

// request拦截器
service.interceptors.request.use(config => {
  // Do something before request is sent
  if (store.getters.token) {
    config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
  }
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

// respone拦截器
service.interceptors.response.use(
  response => response,
  /**
  * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
  * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
  */
  //  const res = response.data;
  //     if (res.code !== 20000) {
  //       Message({
  //         message: res.message,
  //         type: 'error',
  //         duration: 5 * 1000
  //       });
  //       // 50008:非法的token; 50012:其他客户端登录了;  50014:Token 过期了;
  //       if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
  //         MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
  //           confirmButtonText: '重新登录',
  //           cancelButtonText: '取消',
  //           type: 'warning'
  //         }).then(() => {
  //           store.dispatch('FedLogOut').then(() => {
  //             location.reload();// 为了重新实例化vue-router对象 避免bug
  //           });
  //         })
  //       }
  //       return Promise.reject('error');
  //     } else {
  //       return response.data;
  //     }
  error => {
    console.log('err' + error)// for debug
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  })

export default service
import request from '@/utils/request'

//使用
export function getInfo(params) {
  return request({
    url: '/user/info',
    method: 'get',
    params
  });
}

比如后台项目,每一个请求都是要带 token 来验证权限的,这样封装以下的话我们就不用每个请求都手动来塞 token,或者来做一些统一的异常处理,一劳永逸。
而且因为我们的 api 是根据 env 环境变量动态切换的,如果以后线上出现了bug,我们只需配置一下 @/config/dev.env.js 再重启一下服务,就能在本地模拟线上的环境了。

module.exports = {
    NODE_ENV: '"development"',
    BASE_API: '"https://api-dev"', //修改为'"https://api-prod"'就行了
    APP_ORIGIN: '"https://wallstreetcn.com"' //为公司打个广告 pc站为vue+ssr
}

妈妈再也不用担心我调试线上bug了。
当然这里只是简单举了个例子,axios还可以执行多个并发请求,拦截器什么的,大家自行去研究吧。


多环境

vue-cli 默认只提供了devprod两种环境。但其实正真的开发流程可能还会多一个sit或者stage环境,就是所谓的测试环境和预发布环境。所以我们就要简单的修改一下代码。其实很简单就是设置不同的环境变量

"build:prod": "NODE_ENV=production node build/build.js",
"build:sit": "NODE_ENV=sit node build/build.js",

之后在代码里自行判断,想干就干啥

var env = process.env.NODE_ENV === 'production' ? config.build.prodEnv : config.build.sitEnv

新版的 vue-cli 也内置了 webpack-bundle-analyzer 一个模块分析的东西,相当的好用。使用方法也很简单,和之前一样封装一个 npm script 就可以。

//package.json
 "build:sit-preview": "cross-env NODE_ENV=production env_config=sit npm_config_preview=true  npm_config_report=true node build/build.js"

//之后通过process.env.npm_config_report来判断是否来启用webpack-bundle-analyzer

var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())

效果图
analyzer.png
webpack-bundle-analyzer这个插件还是很有用的,对后期的代码优化什么的,最重要的是它够装逼~


前后端交互

每个公司都有自己一套的开发流程,没有绝对的好与坏。这里我来讲讲我司的前后端交互流程。

跨域问题

首先前后端交互不可避免的就会遇到跨域问题,我司现在全是用 cors来解决的,如果你司后端嫌麻烦不肯配置的话,dev环境也可以通过
webpack-dev-serverproxy来解决,开发环境用nginx反代理一下就好了,具体配置这里就不展开了。

前后端的交互问题

其实大家也知道,平时的开发中交流成本占据了我们很大一部分时间,但前后端如果有一个好的协作方式的话能解决很多时间。我司开发流程都是前后端和产品一起开会讨论项目,之后后端根据需求,首先定义数据格式和api,然后 mock api 生成好文档,我们前端才是对接接口的。这里推荐一个文档生成器 swagger
swagger是一个REST APIs文档生成工具,可以在许多不同的平台上从代码注释中自动生成,开源,支持大部分语言,社区好,总之就是一个强大,如下图的api 文档(swagger自动生成,ui忽略)


api 地址,需要传是没参数,需要的传参类型,返回的数据格式什么都一清二楚了。

前端自行mock

如果后端不肯来帮你 mock 数据的话,前端自己来 mock 也是很简单的。你可以使用mock server 或者使用 mockjs + rap 也是很方便的。
不久前出的 easy-mock也相当的不错,还能结合 swagger。
我们大前端终于不用再看后端的脸色了~

iconfont

element-ui 默认的icon不是很多,这里要安利一波阿里的iconfont简直是神器,不管是公司项目还是个人项目都在使用。它提供了png,ai,svg三种格式,同时使用也支持unicode,font-class,symbol三种方式。由于是管理后台对兼容性要求不高,楼主平时都喜欢用symbol,晒一波我司后台的图标(都是楼主自己发挥的)。
iconfont.png
详细具体的使用可以见文章 手摸手,带你优雅的使用 icon


router-view

different router the same component vue。真实的业务场景中,这种情况很多。比如router-view.png
我创建和编辑的页面使用的是同一个component,默认情况下当这两个页面切换时并不会触发vue的created或者mounted钩子,官方说你可以通过watch $route的变化来做处理,但其实说真的还是蛮麻烦的。后来发现其实可以简单的在 router-view上加上一个唯一的key,来保证路由切换时都会重新渲染触发钩子了。这样简单的多了。

<router-view :key="key"></router-view>

computed: {
    key() {
        return this.$route.name !== undefined? this.$route.name + +new Date(): this.$route + +new Date()
    }
 }

优化

有些人会觉得现在构建是不是有点慢,我司现在技术栈是容器服务,后台项目会把dist文件夹里的东西都会打包成一个docker镜像,基本步骤为

npm install
npm run build:prod
加打包镜像,一共是耗时如下

Paste_Image.png

还是属于能接受时间的范围。
主站PC站基于nodejs、Vue实现服务端渲染,所以不仅需要依赖nodejs,而且需要利用pm2进行nodejs生命周期的管理。为了加速线上镜像构建的速度,我们利用taobao源 https://registry.npm.taobao.org 进行加速, 并且将一些常见的npm依赖打入了基础镜像,避免每次都需要重新下载。
这里注意下 建议不要使用cnpm install或者update 它的包都是一个link,反正会有各种诡异的bug,这里建议这样使用

npm install --registry=https://registry.npm.taobao.org

如果你觉得慢还是有可优化的空间如使用webpack dll 或者把那些第三方vendor单独打包 external出去,或者我司现在用的是http2 可以使用AggressiveSplittingPlugin等等,这里有需求的可以自行优化。


占坑

常规占坑,这里是手摸手,带你用vue撸后台系列。
完整项目地址:vue-element-admin
系类文章二:手摸手,带你用vue撸后台 系列二(登录权限篇)
系类文章三:手摸手,带你用vue撸后台 系列三(实战篇)
系类文章四:手摸手,带你用vue撸后台 系列四(vueAdmin 一个极简的后台基础模板)
系类文章:手摸手,带你优雅的使用 icon
系类文章:手摸手,带你封装一个vue component
楼主个人免费圈子

查看原文

认证与成就

  • 获得 0 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-31
个人主页被 155 人浏览