PHP开源Hub
致力于开发者的提升

Taro Next 发布预览版:同时支持 React / Vue / Nerv

NiZerin阅读(138)

自 Taro 2.0 起,我们将会启动对整个 Taro 系统架构的革新,这次革新我们将其称之为 Taro Next。Taro Next 革新完成之后,Taro 本身的拓展性、稳定性、可维护性都会大幅提高,相应地,使用 Taro 的开发者也会获得更好的开发体验,降低更多开发成本和学习成本。

我们目前已经完成了编译系统和小程序端的重构,通过 npm i -g @tarojs/cli@next 安装 Taro CLI 预览(alpha)版之后,使用 taro init 创建新项目即可体验 Taro Next 的新特性:

同时支持 React/Vue/Nerv 三种框架

在旧版本的 Taro,我们以微信小程序的开发规范为基准,使用 React/JSX 的方式来进行开发。而在 Taro Next,我们把这一思路量化为一个编程模型:

设微信小程序生命周期为一个 interface,不同的框架实例的生命周期虽然不尽相同,但我们可以根据框架生命周期分别新建一个 classimplements 小程序生命周期的 interface。相应地,小程序的组件/API/路由规范可以使用同样的思路和模型让不同框架的代码,运行在不同的端上:

taro

不限制语言、语法

由于 Taro Next 的架构出现了变化,表面上来看 Taro 从一个编译型框架变成了一个运行时框架。但究其内核是整体的设计思路出现了变化:从前是「模拟(mock)」,现在是「实现(implements)」。在 Taro Next 我们实现了 React 在小程序中的完整支持,因此这类曾经的 Taro 无法运行的代码在 Taro Next 中完全没有压力:

import { View } from '@tarojs/components'
function Page (props) {
    const view = React.createElement(View, null, props.text)
    return [view, React.Children.only(prosps.children)]
}

在旧版本的 Taro 中我们对 JavaScript 和 TypeScript 进行了 First Class 的支持,Taro Next 我们更进一步,原理上最终可以编译到 JavaScript 的语言都可以用来构建 Taro 项目,以下是一个在 Vue 中使用 CoffeeScript 的例子:

// config.js
{
    webpackChain (chain) {
        chain.merge({
            module: {
                rule: {
                    test: /\.coffee$/,
                    use: [ 'coffee-loader' ]
                }
            }
        })
    }
}
<template>
    <view>{{ title }}</view>
    <view>{{ text }}</view>
    <input v-model='text' />
</template>

<script lang="coffee">
export default
    props:
        title:
        type: String
        required: true
    data: ->
        text: 'text'
</script>

更快的运行速度

运行时性能主要分为两个部分,一是更新性能,二是初始化性能。

对于更新性能而言,旧版本的 Taro 会把开发者 setState 的数据进行一次全量的 diff,最终返回给小程序是按路径更新的 data。而在 Taro Next 中 diff 的工作交给了开发者使用的框架(React/Nerv/Vue),而框架 diff 之后的数据也会通过 Taro 按路径去最小化更新。因此开发者可以根据使用框架的特性进行更多更细微的性能优化。

初始化性能则是 Taro Next 的痛点。原生小程序或编译型框架的初始数据可以直接用于渲染,但 Taro Next 在初始化时会把框架的渲染数据转化为小程序的渲染数据,多了一次 setData 开销。

为了解决这个问题,Taro 从服务端渲染受到启发,在 Taro CLI 将页面初始化的状态直接渲染为无状态的 wxml,在框架和业务逻辑运行之前执行渲染流程。我们将这一技术称之为预渲染(Prerender),经过 Prerender 的页面初始渲染速度通常会和原生小程序一致甚至更快。

更快的构建速度和 source-map 支持

作为一个编译型框架,旧版本的 Taro 会进行大量的 AST 操作,这类操作显著地拖慢了 Taro CLI 的编译速度。而在 Taro Next 中不会操作任何开发者业务代码的 AST,因此编译速度得到了大幅的提高。

正因为 AST 操作的取消,Taro Next 也轻松地实现了 source-map 的支持。这对于开发体验是一个巨大的提升:

source-map

不忘初心

在做到以上各项特性的同时,我们也没有丢掉原来就已经支持的特性:

  • 支持微信小程序、百度智能小程序、支付宝小程序、QQ 小程序、字节跳动小程序
  • 使用原生小程序第三方组件/插件
  • 多端条件编译
  • 跨端 API 和样式处理

这些特性基本涉及到了小程序开发的方方面面,虽然是预览版,但 Taro Next 已经具备了开发生产级小程序的准备,在 Taro 团队内部和兄弟团队也有多款小程序正在使用 Taro Next 进行开发。而在 Taro Next 的 H5 端和移动端,我们还在进行紧张的开发。当 Taro Next 测试(beta)版发布时,使用 Taro Next 构建的一套代码,就可以同时运行在各种小程序、快应用、H5 和移动端当中。在未来,我们还会把 Taro Next 的能力开放出去,让开发者只要写少量的接入代码,就可以使用自己喜欢的任意框架(Angular, Flutter, svelte…)开发小程序或多端应用。

牢记使命

正如我们在 Taro 2.0 发布时所言:

节物风光不相待,桑田碧海须臾改。

20 年代呼啸而来,下一个 10 年,很多框架都会死去,很多技术也会焕然而生,没有什么是不变的,唯一不变的只有变化,我们能做的也只能是拥抱变化。

前端技术一直在高速发展,流行的技术和框架每年都各不相同。但我们始终没有忘记开发 Taro 的初心和使命:降低开发成本,提高开发体验和开发效率。

「不忘初心,牢记使命。」

这就是 Taro 团队拥抱变化的方式。

使用 Vue.js 和 Laravel 共建一个简单的 CRUD 应用

NiZerin阅读(2456)

CURD (增删改查)是数据存储的基本操作,也是你作为 Laravel 开发人员首先要学习的内容之一

但是,如果要结合以 Vue.js 作为前端的应用程序该注意哪些问题呢?首先,因为现在的操作不刷新页面,所以你需要异步 CURD 。因此,你需要确保数据在前后端的一致性。

在本教程中,我会演示如何开发完整的 Laravel&Vue.js 应用程序和每个 CURD 的例子。 AJAX 是连接前后端的关键,所以,我会使用 Axios 作为 HTTP 客户端。我还将向您展示一些处理这种体系结构的用户体验方面缺陷的方法。

你可以在  GitHub 中查看完整的项目。

演示 app

这是一个让使用者创建一个 “Cruds“ 的全栈应用,当我进入这个应用时,它会创造很多不可思议的东西。外星人独特的名称和可以在红色,绿色和黑色的自由转换。

Cruds 应用展示在主页,你可以通过 add 按钮添加 Cruds , delete 按钮删除它们,或者更新它们的颜色。

Laravel 后端的 CRUD

我们将使用 Laravel 后端开始本教程,来完成 CRUD 操作。我将保持这一部分简短,因为 Laravel 的 CRUD 是其他地方广泛涉及的主题.

总之,我们完成以下操作

  • 设置数据库
  • 通过资源控制器来编写一个 RESTful API 的路由
  • 在控制器中定义方法,来完成 CRUD 操作

Database

首先是迁移,我们的 Cruds 有两个属性:名称和颜色,我们将其设置为 text 类型

2018_02_02_081739_create_cruds_table.php

<?php

...

class CreateCrudsTable extends Migration
{
  public function up()
  {
    Schema::create('cruds', function (Blueprint $table) {
      $table->increments('id');
      $table->text('name');
      $table->text('color');
      $table->timestamps();
    });
  }

  ...
}
...

API

现在我们来设置 RESTful API 路由。这个 resource 方法将自动创建我们所需要的所有操作。但是,我们不需要 editshow 和 store 这几个路由,因此我们需要排除它们.

routes/api.php

<?php

Route::resource('/cruds', 'CrudsController', [
  'except' => ['edit', 'show', 'store']
]);

有了这些,我们现在可以在 API 中使用以下路由:

HTTP 方法地址方法路由名
GET/api/crudsindexcruds.index
GET/api/cruds/createcreatecruds.create
PUT/api/cruds/{id}updatecruds.update
DELETE/api/cruds/{id}destroycruds.destroy

控制器

我们现在需要在控制器中实现这些操作:

app/Http/Controllers/CrudsController.php

<?php

namespace App\Http\Controllers;

use App\Crud;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Faker\Generator;

class CrudsController extends Controller
{
  // Methods
}

我们先简要概述下每种方法:

create 方法。我们使用 Laravel 附带的 Faker 包,为 Crud 随机生成名称和颜色字段 。随后,我们将新生成的数据作为 JSON 返回。

<?php

...

public function create(Generator $faker)
{
  $crud = new Crud();
  $crud->name = $faker->lexify('????????');
  $crud->color = $faker->boolean ? 'red' : 'green';
  $crud->save();

  return response($crud->jsonSerialize(), Response::HTTP_CREATED);
}

index 方法。我们使用 index 方法返回 Cruds 的全部数据。在一个更严肃的应用中,我们会使用分页,但是现在我们尽量保持简洁。

<?php

...

public function index()
{
  return response(Crud::all()->jsonSerialize(), Response::HTTP_OK);
}

update。此方法允许客户端更改 Crud 的颜色。

<?php

...

public function update(Request $request, $id)
{
  $crud = Crud::findOrFail($id);
  $crud->color = $request->color;
  $crud->save();

  return response(null, Response::HTTP_OK);
}

destroy。 删除 Cruds 的方法。

<?php

...

public function destroy($id)
{
  Crud::destroy($id);

  return response(null, Response::HTTP_OK);
}

Vue.js 应用

现在开始处理 Vue 页面展示部分。先来创建一个组件 — CrudComponent.vue,用来展示 Cruds 的内容。

这个组件主要是展示的功能,没有太多的业务逻辑。主要有以下几个重点:

  • 展示一张图片,图片的颜色取决于 Crud 的颜色( 也就是展示 red.png 还是 green.png)
  • 有一个删除按钮,当点击时会触发 del 方法,继而触发一个 delete 事件,并携带当前 Crud 的 ID 作为参数
  • 有一个 HTML 选择器 (用来选择颜色),当发生选择时,会触发 update 方法,继而触发一个 update 事件,并携带当前 Crud 的 ID 和新选择的颜色作为参数

resources/assets/js/components/CrudComponent.vue

<template>
  <div class="crud">
    <div class="col-1">
      <img :src="image"/>
    </div>
    <div class="col-2">
      <h3>Name: {{ name | properCase }}</h3>
      <select @change="update">
        <option
          v-for="col in [ 'red', 'green' ]"
          :value="col"
          :key="col"
          :selected="col === color ? 'selected' : ''"
        >{{ col | properCase }}</option>
      </select>
      <button @click="del">Delete</button>
    </div>
  </div>
</template>
<script>
  export default {
    computed: {
      image() {
        return `/images/${this.color}.png`;
      }
    },
    methods: {
      update(val) {
        this.$emit('update', this.id, val.target.selectedOptions[0].value);
      },
      del() {
        this.$emit('delete', this.id);
      }
    },
    props: ['id', 'color', 'name'],
    filters: {
      properCase(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
      }
    }
  }
</script>
<style>...</style>

在这个项目中还有一个组件 App.vue。它在整个项目中的地位非常重要,所有主要的逻辑都写在这里。下面就来逐步分析这个文件。

先从 template 标签开始,它主要处理了下面这些业务:

  • 为我们上面提到的 crud-component 组件占位
  • 遍历包含 Crud 对象的数组(也就是 cruds 数组 ),数组中的每个元素都对应着一个 crud-component 组件。我们以 props 的形式把每个 Crud 的属性传递给这个组件,并且监听来自这个组件的 update 和 delete 事件
  • 设置一个 Add 按钮,当点击时,会触发 create 方法,从而创建新的 Cruds

resources/assets/js/components/App.vue

<template>
  <div id="app">
    <div class="heading">
      <h1>Cruds</h1>
    </div>
    <crud-component
      v-for="crud in cruds"
      v-bind="crud"
      :key="crud.id"
      @update="update"
      @delete="del"
    ></crud-component>
    <div>
      <button @click="create()">Add</button>
    </div>
  </div>
</template>

下面来看 App.js 文件的 script 部分:

  • 首先通过 Crud 函数创建用于展示 Cruds 的对象,包括 ID, 颜色和姓名
  • 然后, 引入  CrudComponent 组件
  • 组件的 cruds 数组作为 data 的属性。 关于对 CRUD 的增删改查的具体操作, 会在下一步展开说明。

resources/assets/js/components/App.vue

<template>...</template>
<script>
  function Crud({ id, color, name}) {
    this.id = id;
    this.color = color;
    this.name = name;
  }

  import CrudComponent from './CrudComponent.vue';

  export default {
    data() {
      return {
        cruds: []
      }
    },
    methods: {
      create() {
        // 待完善
      },
      read() {
        // 待完善
      },
      update(id, color) {
        // 待完善
      },
      del(id) {
        // 待完善
      }
    },
    components: {
      CrudComponent
    }
  }
</script>

前端通过 AJAX 触发 CURD

在一个完整的项目中,所有的 CRUD 操作都是在后端完成的,因为数据库是跟后端交互的。然而,触发 CRUD 的操作几乎都是在前端完成的。

因此,一个 HTTP 客户端(也就是负责在前后端之间交互数据的桥梁)的作用是非常重要的。被 Laravel 前端默认封装的 Axios, 就是一个非常好用的 HTTP 客户端。

再来看下资源表,每个 AJAX 请求都需要有一个明确的 API 接口:

VerbPathActionRoute Name
GET/api/crudsindexcruds.index
GET/api/cruds/createcreatecruds.create
PUT/api/cruds/{id}updatecruds.update
DELETE/api/cruds/{id}destroycruds.destroy

Read

首先来看 read 方法。这个方法是负责在前端发起 Cruds 请求的,对应后端的处理在是控制器里的 index 方法,因此使用 GET 请求 /api/cruds

由于 Laravel 前端默认把 Axios 设置为 window 的一个属性, 因此我们可以使用 window.axios.get 来发起一个 GET 请求。

对于像 getpost 等 Axios 方法的返回结果,我们可以再继续链式调用 then 方法,在 then 方法里可以获取到 AJAX 响应数据的主体 data 属性。

resources/assets/js/components/App.vue

...

methods() {
  read() {
    window.axios.get('/api/cruds').then(({ data }) => {
      // console.log(data)
    });
  },
  ...
}

/*
Sample response:

[
  {
    "id": 0,
    "name": "ijjpfodc",
    "color": "green",
    "created_at": "2018-02-02 09:15:24",
    "updated_at": "2018-02-02 09:24:12"
  },
  {
    "id": 1,
    "name": "wjwxecrf",
    "color": "red",
    "created_at": "2018-02-03 09:26:31",
    "updated_at": "2018-02-03 09:26:31"
  }
]
*/

从上面的返回结果可以看出,返回的结果是 JSON 数组。Axios 会自动将其解析并转成 JavaScript 对象返给我们。这样方便我们在回调函数里对结果进行遍历,并通过 Crud 工厂方法创建新的 Cruds,并存到 data 属性的 cruds 数组中,例如  this.cruds.push(...)

resources/assets/js/components/App.vue

...

methods() {
  read() {
    window.axios.get('/api/cruds').then(({ data }) => {
      data.forEach(crud => {
        this.cruds.push(new Crud(crud));
      });
    });
  },
},
...
created() {
  this.read();
}

注意:我们通过 created 方法,可以使程序在刚一加载时就触发 read 方法,但这并非最佳实践。最好方案应该是直接去掉 read 方法,当程序第一次加载的时候,就把应用的初始状态都包含在文档投中。

通过上面的步骤,我们就能看到 Cruds 展示在界面上了:

更新 (以及状态同步)

执行 update 方法需要发送表单数据,比如 color,这样控制器才知道要更新哪些数据。Crud 的 ID 是从服务端获取的。

还记得我在本文开篇就提到关于前后端数据一致的问题,这里就是一个很好的例子。

当需要执行 update 方法时,我们可以不用等待服务器返回结果,就在前端更新 Crud 对象,因为我们很清楚更新后应该是什么状态。

但是,我们不应该这么做。为什么?因为有很多原因可能会导致更新数据的请求失败,比如网络突然中断,或者更新的值被数据库拒绝等。

所以等待服务器返回更新成功的信息后,再刷新前端的状态是非常重要的,这样我们才能确保前后端数据的一致。

resources/assets/js/components/App.vue

methods: {
  read() {
    ...
  },
  update(id, color) {
    window.axios.put(`/api/cruds/${id}`, { color }).then(() => {
      // 一旦请求成功,就更新 Crud 的颜色
      this.cruds.find(crud => crud.id === id).color = color;
    });
  },
  ...
}

你可能会说这样非必要的等待会影响用户体验,但是我想说,我们在不确定的情况下更新状态,误导用户,这应该会造成更差的用户体验。

创建和删除

现在你已经明白整个架构的关键点了,剩下两个方法,不需要我解释,你也应该能够理解其中的逻辑了:

resources/assets/js/components/App.vue

methods: {
  read() {
    ...
  },
  update(id, color) {
    ...
  },
  create() {
    window.axios.get('/api/cruds/create').then(({ data }) => {
      this.cruds.push(new Crud(data));
    });
  },
  del(id) {
    window.axios.delete(`/api/cruds/${id}`).then(() => {
      let index = this.cruds.findIndex(crud => crud.id === id);
      this.cruds.splice(index, 1);
    });
  }
}

加载界面 和 禁止互动


你应该知道,我们这个项目 VUE 前端的 CRUD 操作都是异步方式的,所以前端 AJAX 请求服务器并等待服务器响应返回响应,总会有一点延迟。因为用户不知道网站在做什么,此空档期用户的体验不是很好,这学问关联到 UX。

为了改善这 UX 问题,因此最好添加上一些加载界面并在等待当前操作解决时禁用任何交互。这可以让用户知道网站在做了什么,而且可以确保数据的状态。

Vuejs 有很多很好的插件能完成这个功能,但是在此为了让学者更好的理解,做一些简单的快速的逻辑来完成这个功能,我将创建一个半透明的 div,在 AJAX 操作过程中覆盖整个屏幕,这个逻辑能完成两个功能:加载界面和禁止互动。一石两鸟,完美~

resources/views/index.blade.php

<body>
<div id="mute"></div>
<div id="app"></div>
<script src="js/app.js"></script>
</body>

当进行 AJAX 请求的时候,就把 mute 的值从 false 改为 true, 通过这个值的变化,控制半透明 div 的显示或隐藏。

resources/assets/js/components/App.vue

export default {
  data() {
    return {
      cruds: [],
      mute: false
    }
  },
  ...
}

下面就是在 update 方法中切换 mute 值的过程。当执行 update 方法时,mute 值被设为 true。当请求成功,再把 mute 值设为 false, 这样用户就可以继续操作应用了。

resources/assets/js/components/App.vue

update(id, color) {
  this.mute = true;
  window.axios.put(`/api/cruds/${id}`, { color }).then(() => {
    this.cruds.find(crud => crud.id === id).color = color;
    this.mute = false;
  });
},

在 CRUDE 的每个方法里都要有这样的操作,在此,为了节约篇幅,我就不一一写明了。

为了保证大家不会忘记这个重要的操作,我们直接在 <div id="app"></div> 元素上方增加了 <div id="mute"></div> 元素。

从下面的代码可以看到,当 <div id="mute"> 元素被加上 class on 后,它将以灰色调完全覆盖整个应用,并阻止所有的点击事件:

resources/views/index.blade.php

<!doctype html>
<html lang="{{ app()->getLocale() }}">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <title>Cruds</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;,
      height: 100%;
      width: 100%;
      background-color: #d1d1d1
    }
    #mute {
      position: absolute;
    }
    #mute.on {
      opacity: 0.7;
      z-index: 1000;
      background: white;
      height: 100%;
      width: 100%;
    }
  </style>
</head>
<body>
<div id="mute"></div>
<div id="app"></div>
<script src="js/app.js"></script>
</body>
</html>

最后一个问题是对于 on class 的管理,我们可以在 mute 的值上加一个 watch,每当 mute 的值发生改变的时候,就加上或删除 on class:

export default {
  ...
  watch: {
    mute(val) {
      document.getElementById('mute').className = val ? "on" : "";
    }
  }
}

完成上面所有的步骤,你就可以拥有一个带有加载指示器的全栈 Vue / Laravel CRUD 的应用程序了。再来看下完整效果吧:

你可以从 GitHub 获取代码,如果有任何问题或者想法,欢迎给我留言!

原文地址:https://vuejsdevelopers.com/2018/02/05/v…

译文地址:https://learnku.com/vuejs/t/24724

vue技术分享之你可能不知道的7个秘密

NiZerin阅读(2264)

本文是vue源码贡献值Chris Fritz在公共场合的一场分享,觉得分享里面有不少东西值得借鉴,虽然有些内容我在工作中也是这么做的,还是把大神的ppt在这里翻译一下,希望给朋友带来一些帮助。

一、善用watch的immediate属性

这一点我在项目中也是这么写的。例如有请求需要再也没初始化的时候就执行一次,然后监听他的变化,很多人这么写:

created(){
  this.fetchPostList()
},
watch: {
  searchInputValue(){
    this.fetchPostList()
  }
}

上面的这种写法我们可以完全如下写:

watch: {
  searchInputValue:{
    handler: 'fetchPostList',
    immediate: true
  }
}

二、组件注册,值得借鉴

一般情况下,我们组件如下写:

import BaseButton from './baseButton'
import BaseIcon from './baseIcon'
import BaseInput from './baseInput'
 
export default {
 components: {
  BaseButton,
  BaseIcon,
  BaseInput
 }
}
<BaseInput v-model="searchText" @keydown.enter="search" />
<BaseButton @click="search"> <BaseIcon name="search"/></BaseButton>

步骤一般有三部,

第一步,引入、

第二步注册、

第三步才是正式的使用,

这也是最常见和通用的写法。但是这种写法经典归经典,好多组件,要引入多次,注册多次,感觉很烦。

我们可以借助一下webpack,使用 require.context() 方法来创建自己的(模块)上下文,从而实现自动动态require组件。

思路是:在src文件夹下面main.js中,借助webpack动态将需要的基础组件统统打包进来。

代码如下:

import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'
 
// Require in a base component context
const requireComponent = require.context(
 ‘./components', false, /base-[\w-]+\.vue$/
)
 
requireComponent.keys().forEach(fileName => {
 // Get component config
 const componentConfig = requireComponent(fileName)
 
 // Get PascalCase name of component
 const componentName = upperFirst(
  camelCase(fileName.replace(/^\.\//, '').replace(/\.\w+$/, ''))
 )
 
 // Register component globally
 Vue.component(componentName, componentConfig.default || componentConfig)
})

这样我们引入组件只需要第三步就可以了:

<BaseInput
 v-model="searchText"
 @keydown.enter="search"
/>
<BaseButton @click="search">
 <BaseIcon name="search"/>
</BaseButton>

三、精简vuex的modules引入

对于vuex,我们输出store如下写:

import auth from './modules/auth'
import posts from './modules/posts'
import comments from './modules/comments'
// ...
 
export default new Vuex.Store({
 modules: {
  auth,
  posts,
  comments,
  // ...
 }
})

要引入好多modules,然后再注册到Vuex.Store中~~

精简的做法和上面类似,也是运用 require.context()读取文件,代码如下:

import camelCase from 'lodash/camelCase'
const requireModule = require.context('.', false, /\.js$/)
const modules = {}
requireModule.keys().forEach(fileName => {
 // Don't register this file as a Vuex module
 if (fileName === './index.js') return
 
 const moduleName = camelCase(
  fileName.replace(/(\.\/|\.js)/g, '')
 )
 modules[moduleName] = {
        namespaced: true,
        ...requireModule(fileName),
       }
})
export default modules

这样我们只需如下代码就可以了:

import modules from './modules'
export default new Vuex.Store({
 modules
})

四、路由的延迟加载

这一点,关于vue的引入,我之前在 vue项目重构技术要点和总结 中也提及过,可以通过require方式或者import()方式动态加载组件。

{
 path: '/admin',
 name: 'admin-dashboard',
 component:require('@views/admin').default
}

或者

{
 path: '/admin',
 name: 'admin-dashboard',
 component:() => import('@views/admin')
}

加载路由。

五、router key组件刷新

下面这个场景真的是伤透了很多程序员的心…先默认大家用的是Vue-router来实现路由的控制。 假设我们在写一个博客网站,需求是从/post-haorooms/a,跳转到/post-haorooms/b。然后我们惊人的发现,页面跳转后数据竟然没更新?!原因是vue-router”智能地”发现这是同一个组件,然后它就决定要复用这个组件,所以你在created函数里写的方法压根就没执行。通常的解决方案是监听$route的变化来初始化数据,如下:

data() {
 return {
  loading: false,
  error: null,
  post: null
 }
}, 
watch: {
 '$route': {
  handler: 'resetData',
  immediate: true
 }
},
methods: {
 resetData() {
  this.loading = false
  this.error = null
  this.post = null
  this.getPost(this.$route.params.id)
 },
 getPost(id){
 
 }
}

bug是解决了,可每次这么写也太不优雅了吧?秉持着能偷懒则偷懒的原则,我们希望代码这样写:

data() {
 return {
  loading: false,
  error: null,
  post: null
 }
},
created () {
 this.getPost(this.$route.params.id)
},
methods () {
 getPost(postId) {
  // ...
 }
}

解决方案:给router-view添加一个唯一的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。

<router-view :key="$route.fullpath"></router-view>

注:个人经验,这个一般应用在子路由里面,这样才可以不避免大量重绘,假设app.vue根目录添加这个属性,那么每次点击改变地址都会重绘,还是得不偿失的!

六、唯一组件根元素

场景如下:

(Emitted value instead of an instance of Error)
 Error compiling template:
  <div></div>
  <div></div>
  -Component template should contain exactly one root element. 
    If you are using v-if on multiple elements, use v-else-if 
   to chain them instead.

模板中div只能有一个,不能如上面那么平行2个div。

例如如下代码:

<template>
 <li
  v-for="route in routes"
  :key="route.name"
 >
  <router-link :to="route">
   {{ route.title }}
  </router-link>
 </li>
</template>

会报错!

我们可以用render函数来渲染

functional: true,
render(h, { props }) {
 return props.routes.map(route =>
  <li key={route.name}>
   <router-link to={route}>
    {route.title}
   </router-link>
  </li>
 )
}

七、组件包装、事件属性穿透问题

当我们写组件的时候,通常我们都需要从父组件传递一系列的props到子组件,同时父组件监听子组件emit过来的一系列事件。举例子:

//父组件
<BaseInput 
  :value="value"
  label="密码"
  placeholder="请填写密码"
  @input="handleInput"
  @focus="handleFocus>
</BaseInput>
 
//子组件
<template>
 <label>
  {{ label }}
  <input
   :value="value"
   :placeholder="placeholder"
   @focus=$emit('focus', $event)"
   @input="$emit('input', $event.target.value)"
  >
 </label>
</template>

这样写很不精简,很多属性和事件都是手动定义的,我们可以如下写:

<input
  :value="value"
  v-bind="$attrs"
  v-on="listeners"
>
 
computed: {
 listeners() {
  return {
   ...this.$listeners,
   input: event => 
    this.$emit('input', event.target.value)
  }
 }
}

$attrs包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind=”$attrs” 传入内部组件。

$listeners包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件。

深入解析Vue开发动态刷新Echarts组件的教程

NiZerin阅读(2802)

需求背景:dashboard作为目前企业中后台产品的“门面”,如何更加实时、高效、炫酷的对统计数据进行展示,是值得前端开发工程师和UI设计师共同思考的一个问题。今天就从0开始,封装一个动态渲染数据的Echarts折线图组件,抛砖引玉,一起来思考更多有意思的组件。

准备工作

项目结构搭建

因为生产需要(其实是懒),所以本教程使用了 ==vue-cli==进行了项目的基础结构搭建。

npm install -g vue-cli
vue init webpack vue-charts
cd vue-charts
npm run dev

安装Echarts

直接使用npm进行安装。

npm install Echarts --save

引入Echarts

//在main.js加入下面两行代码
import echarts from 'echarts'
Vue.prototype.$echarts = echarts //将echarts注册成Vue的全局属性

到此,准备工作已经完成了。

静态组件开发

因为被《React编程思想》这篇文章毒害太深,所以笔者开发组件也习惯从基础到高级逐步迭代。

静态组件要实现的目的很简单,就是把Echarts图表,渲染到页面上。

新建Chart.vue文件

<template>
 <div :id="id" :style="style"></div>
</template>
<script>
export default {
 name: "Chart",
 data() {
  return {
   //echarts实例
   chart: "" 
  };
 },
 props: {
  //父组件需要传递的参数:id,width,height,option
  id: {
   type: String
  },
  width: {
   type: String,
   default: "100%"
  },
  height: {
   type: String,
   default: "300px"
  },
  option: {
   type: Object,
   //Object类型的prop值一定要用函数return出来,不然会报错。原理和data是一样的,
   //使用闭包保证一个vue实例拥有自己的一份props
   default() {
    return {
     title: {
      text: "vue-Echarts"
     },
     legend: {
      data: ["销量"]
     },
     xAxis: {
      data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子","tuoxie"]
     },
     series: [
      {
       name: "销量",
       type: "line",
       data: [5, 20, 36, 10, 10, 70]
      }
     ]
    };
   }
  }
 },
 computed: {
  style() {
   return {
    height: this.height,
    width: this.width
   };
  }
 },
 mounted() {
  this.init();
 },
 methods: {
  init() {
   this.chart = this.$echarts.init(document.getElementById(this.id));
   this.chart.setOption(this.option);
  }
 }
};
</script>

上述文件就实现了将一个简单折线图渲染到页面的组件,怎么样是不是很简单?最简使用方法如下:

App.vue

<template>
 <div id="app">
  <Chart id="test"/>
 </div>
</template>
<script>
import Chart from "./components/Chart";
export default {
 name: "App",
 data() {},
 components: {
  Chart
 }
}
</script>

至此,运行程序你应该能看到以下效果:  第一次迭代

现在我们已经有了一个基础版本,让我们来看看哪些方面做的还不尽如人意:

  • 图表无法根据窗口大小进行自动缩放,虽然设置了宽度为100%,但是只有刷新页面图表才会重新进行渲染,这会让用户体验变得很差。
  • 图表目前无法实现数据自动刷新

下面我们来实现这两点:

自动缩放

Echarts本身是不支持自动缩放的,但是Echarts为我们提供了resize方法。

//在init方法中加入下面这行代码
window.addEventListener("resize", this.chart.resize);

只需要这一句,我们就实现了图表跟随窗口大小自适应的需求。

支持数据自动刷新

因为Echarts是数据驱动的,这意味着只要我们重新设置数据,那么图表就会随之重新渲染,这是实现本需求的基础。我们再设想一下,如果想要支持数据的自动刷新,必然需要一个监听器能够实时监听到数据的变化然后告知Echarts重新设置数据。所幸Vue为我们提供了==watcher==功能,通过它我们可以很方便的实现上述功能:

//在Chart.vue中加入watch
 watch: {
  //观察option的变化
  option: {
   handler(newVal, oldVal) {
    if (this.chart) {
     if (newVal) {
      this.chart.setOption(newVal);
     } else {
      this.chart.setOption(oldVal);
     }
    } else {
      this.init();
    }
   },
   deep: true //对象内部属性的监听,关键。
  }
 }

上面代码就实现了我们对option对象中属性变化的监听,一旦option中的数据有了变化,那么图表就会重新渲染。

实现动态刷新

下一步我想大家都知道了,就是定时从后台拉取数据,然后更新父组件的option就好。这个地方有两个问题需要思考一下:

  • 如果图表要求每秒增加一个数据,应该如何进行数据的请求才能达到性能与用户体验的平衡?
  • 动态更新数据的代码,应该放在父组件还是子组件?

对第一个问题,每秒实时获取服务器的数据,肯定是最精确的,这就有两种方案:

  • 每秒向后台请求一次
  • 保持长连接,后台每秒向前端推送一次数据

第一种方案无疑对性能和资源产生了极大的浪费;除非实时性要求特别高(股票系统),否则不推荐这种方式;

第二种方案需要使用web Socket,但在服务端需要进行额外的开发工作。

笔者基于项目的实际需求(实时性要求不高,且后台生成数据也有一定的延迟性),采用了以下方案:

  • 前端每隔一分钟向后台请求一次数据,且为当前时间的上一分钟的数据;
  • 前端将上述数据每隔一秒向图表set一次数据

关于第二个问题:笔者更倾向于将Chart组件设计成纯组件,即只接收父组件传递的数据进行变化,不在内部进行复杂操作;这也符合目前前端MVVM框架的最佳实践;而且若将数据传递到Chart组件内部再进行处理,一是遇到不需要动态渲染的需求还需要对组件进行额外处理,二是要在Chart内部做ajax操作,这样就导致Chart完全没有了可复用性。

接下来我们修改App.vue

<template>
 <div id="app">
  <Chart id="test" :option="option"/>
 </div>
</template>
 
<script>
import vueEcharts from "./components/vueEcharts";
export default {
 name: "App",
 data() {
  return {
   //笔者使用了mock数据代表从服务器获取的数据
   chartData: {
    xData: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
    sData: [5, 20, 36, 10, 10, 70]
   }
  };
 },
 components: {
  Chart
 },
 mounted() {
  this.refreshData();
 },
 methods: {
  //添加refreshData方法进行自动设置数据
  refreshData() {
   //横轴数据
   let xData = this.chartData.xData,
    //系列值
     sData = this.chartData.sData;
   for (let i = 0; i < xData.length; i++) {
    //此处使用let是关键,也可以使用闭包。原理不再赘述
      setTimeout(() => {
     this.option.xAxis.data.push(xData[i]);
     this.option.series[0].data.push(sData[i]);
    }, 1000*i)//此处要理解为什么是1000*i
   }
  }
 }
};
</script>

至此我们就实现了图表动态数据加载,效果如下图: 


基于Vue组件化的日期联动选择器功能的实现代码

NiZerin阅读(2548)

我们的社区前端工程用的是element组件库,后台管理系统用的是iview,组件库都很棒,但是日期、时间选择器没有那种“ 年份 – 月份 -天数 ” 联动选择的组件。虽然两个组件库给出的相关组件也很棒,但是有时候确实不是太好用,不太明白为什么很多组件库都抛弃了日期联动选择。因此考虑自己动手做一个。 

将时间戳转换成日期格式

// timestamp 为时间戳
new Date(timestamp)
//获取到时间标砖对象,如:Sun Sep 02 2018 00:00:00 GMT+0800 (中国标准时间)
/*
 获取年: new Date(timestamp).getFullYear()
 获取月: new Date(timestamp).getMonth() + 1
 获取日: new Date(timestamp).getDate() 
 获取星期几: new Date(timestamp).getDay() 
*/

将日期格式(yyyy-mm-dd)转换成时间戳

//三种形式
 new Date('2018-9-2').getTime()
 new Date('2018-9-2').valueOf()
 Date.parse(new Date('2018-9-2'))

IE下的兼容问题

注意: 上述代码在IE10下(至少包括IE10)是没法或得到标准时间value的,因为 2018-9-2 并不是标准的日期格式(标准的是 2018-09-02),而至少 chrome 内核为我们做了容错处理(估计火狐也兼容)。因此,必须得做严格的日期字符串整合操作,万不可偷懒

基于Vue组件化的日期联机选择器

该日期选择组件要达到的目的如下:

  • (1) 当前填入的日期不论完整或缺省,都要向父组件传值(缺省传”),因为父组件要根据获取的日期值做相关处理(如限制提交等操作等);
  • (2) 具体天数要做自适应,即大月31天、小月30天、2月平年28天、闰年29天;
  • (3) 如先选择天数为31号(或30号),再选择月数,如当前选择月数不含已选天数,则清空天数;
  • (4) 如父组件有时间戳传入,则要将时间显示出来供组件修改。 实现代码(使用的是基于Vue + element组件库)
<template>
 <div class="date-pickers">
  <el-select 
  class="year select"
  v-model="currentDate.year"
  @change='judgeDay'
  placeholder="年">
   <el-option
   v-for="item in years"
   :key="item"
   :label="item"
   :value="item">
   </el-option>
  </el-select>
  <el-select >
  class="month select"
  v-model="currentDate.month"
  @change='judgeDay'
  placeholder="月">
   <el-option
   v-for="item in months"
   :key="item"
   :label="String(item).length==1?String('0'+item):String(item)"
   :value="item">
   </el-option>
  </el-select>
  <el-select 
  class="day select"
  :class="{'error':hasError}"
  v-model="currentDate.day"
  placeholder="日">
   <el-option
   v-for="item in days"
   :key="item"
   :label="String(item).length==1?String('0'+item):String(item)"
   :value="item">
   </el-option>
  </el-select>
 </div>
</template>
<script>
export default {
 props: {
 sourceDate: {
  type: [String, Number]
 }
 },
 name: "date-pickers",
 data() {
 return {
  currentDate: {
  year: "",
  month: "",
  day: ""
  },
  maxYear: new Date().getFullYear(),
  minYear: 1910,
  years: [],
  months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
  normalMaxDays: 31,
  days: [],
  hasError: false
 };
 },
 watch: {
 sourceDate() {
  if (this.sourceDate) {
  this.currentDate = this.timestampToTime(this.sourceDate);
  }
 },
 normalMaxDays() {
  this.getFullDays();
  if (this.currentDate.year && this.currentDate.day > this.normalMaxDays) {
  this.currentDate.day = "";
  }
 },
 currentDate: {
  handler(newValue, oldValue) {
  this.judgeDay();
  if (newValue.year && newValue.month && newValue.day) {
   this.hasError = false;
  } else {
   this.hasError = true;
  }
  this.emitDate();
  },
  deep: true
 }
 },
 created() {
 this.getFullYears();
 this.getFullDays();
 },
 methods: {
 emitDate() {
  let timestamp; //暂默认传给父组件时间戳形式
  if ( this.currentDate.year && this.currentDate.month && this.currentDate.day) {
   let month = this.currentDate.month < 10 ? ('0'+ this.currentDate.month):this.currentDate.month;
   let day = this.currentDate.day < 10 ? ('0'+ this.currentDate.day):this.currentDate.day;
   let dateStr = this.currentDate.year + "-" + month + "-" + day;
   timestamp = new Date(dateStr).getTime();
  } 
  else {
   timestamp = "";
  }
  this.$emit("dateSelected", timestamp);
 },
 timestampToTime(timestamp) {
  let dateObject = {};
  if (typeof timestamp == "number") {
  dateObject.year = new Date(timestamp).getFullYear();
  dateObject.month = new Date(timestamp).getMonth() + 1;
  dateObject.day = new Date(timestamp).getDate();
  return dateObject;
  }
 },
 getFullYears() {
  for (let i = this.minYear; i <= this.maxYear; i++) {
  this.years.push(i);
  }
 },
 getFullDays() {
  this.days = [];
  for (let i = 1; i <= this.normalMaxDays; i++) {
  this.days.push(i);
  }
 },
 judgeDay() {
  if ([4, 6, 9, 11].indexOf(this.currentDate.month) !== -1) {
  this.normalMaxDays = 30; //小月30天
  if (this.currentDate.day && this.currentDate.day == 31) {
   this.currentDate.day = "";
  }
  } else if (this.currentDate.month == 2) {
  if (this.currentDate.year) {
   if (
   (this.currentDate.year % 4 == 0 &&
    this.currentDate.year % 100 != 0) ||
   this.currentDate.year % 400 == 0
   ) {
   this.normalMaxDays = 29; //闰年2月29天
   } else {
   this.normalMaxDays = 28; //闰年平年28天
   }
  } 
  else {
   this.normalMaxDays = 28;//闰年平年28天
  }
  } 
  else {
  this.normalMaxDays = 31;//大月31天
  }
 }
 }
};
</script>
<style lang="less">
.date-pickers {
 .select {
 margin-right: 10px;
 width: 80px;
 text-align: center;
 }
 .year {
 width: 100px;
 }
 .error {
 .el-input__inner {
  border: 1px solid #f1403c;
  border-radius: 4px;
 }
 }
}
</style>

代码解析 默认天数(normalMaxDays)为31天,最小年份1910,最大年份为当前年(因为我的业务场景是填写生日,大家这些都可以自己调)并在created 钩子中先初始化年份和天数。

监听当前日期(currentDate)

核心是监听每一次日期的改变,并修正normalMaxDays,这里对currentDate进行深监听,同时发送到父组件,监听过程:

watch: {
 currentDate: {
  handler(newValue, oldValue) {
  this.judgeDay(); //更新当前天数
  this.emitDate(); //发送结果至父组件或其他地方
  },
  deep: true
 }
}

judgeDay方法:

judgeDay() {
 if ([4, 6, 9, 11].indexOf(this.currentDate.month) !== -1) {
 this.normalMaxDays = 30; //小月30天
 if (this.currentDate.day && this.currentDate.day == 31) {
  this.currentDate.day = ""; 
 }
 } else if (this.currentDate.month == 2) {
 if (this.currentDate.year) {
  if (
  (this.currentDate.year % 4 == 0 &&
   this.currentDate.year % 100 != 0) ||
  this.currentDate.year % 400 == 0
  ) {
  this.normalMaxDays = 29; //闰年2月29天
  } else {
  this.normalMaxDays = 28; //平年2月28天
  }
 } else {
  this.normalMaxDays = 28; //平年2月28天
 }
 } else {
 this.normalMaxDays = 31; //大月31天
 }
}

最开始的时候我用的 includes判断当前月是否是小月:

if([4, 6, 9, 11].includes(this.currentDate.month))

也是缺乏经验,最后测出来includes 在IE10不支持,因此改用普通的indexOf()。

emitDate:
emitDate() {
 let timestamp; //暂默认传给父组件时间戳形式
 if ( this.currentDate.year && this.currentDate.month && this.currentDate.day) {
  let month = this.currentDate.month < 10 ? ('0'+ this.currentDate.month):this.currentDate.month;
  let day = this.currentDate.day < 10 ? ('0'+ this.currentDate.day):this.currentDate.day;
  let dateStr = this.currentDate.year + "-" + month + "-" + day;
  timestamp = new Date(dateStr).getTime();
 } 
 else {
  timestamp = "";
 }
 this.$emit("dateSelected", timestamp);//发送给父组件相关结果
},

这里需要注意的,最开始并没有做上述标准日期格式处理,因为chrome做了适当容错,但是在IE10就不行了,所以最好要做这种处理。 normalMaxDays改变后必须重新获取天数,并依情况清空当前选择天数:

watch: {
 normalMaxDays() {
  this.getFullDays();
  if (this.currentDate.year && this.currentDate.day > this.normalMaxDays) {
  this.currentDate.day = "";
  }
 }
}

结语

感谢您的观看,如有不足之处,欢迎批评指正。

Vue Mixins 高级组件 与 Vue HOC 高阶组件 实践

NiZerin阅读(4011)

在项目里,我们经常会使用组件库进行快速开发,然而在过程中,又难免会遇到对组件库的改造和拓展,如何优雅且简单的进行重构,下面让我们从一个简单需求来探索组件的奇技淫巧–Mixins和HOC

项目中使用组件库遇到的需求

需求: 实现所有页面按钮的点击事件防抖控制

随便选用一套组件库,在这次例子里,我选用iview进行开发

iview官网的Button组件的使用方法如下

<template>
    <Button @click="click">Default</Button>
</template>
<script>
    export default {
        methods: {
            click () {
                console.log('yes')
            }
        }
    }
</script>

Button的源码也很简单,在这里我剔除了不相关的内容

<template>
    <button @click="handleClick"></button>
</template>
<script>
    export default {
        name: 'Button',
        components: { Icon },
        props: {
        },
        data () {
        },
        computed: {
        },
        methods: {
            handleClick (event) {
                this.$emit('click', event);
            }
        },
        mounted () {
        }
    };
</script>

从源码里可以得到以下信息

  1. iview的Button组件封装了原生的button组件
  2. 对原生的button组件,进行了click事件的绑定
  3. click事件触发时,向组件emit了click事件

需求怎么实现

再看看我们的需求 实现所有页面按钮的点击事件防抖控制,这里有几个关键点

  1. 点击事件,iview的Button组件里已经对原生的button的click进行了绑定,我们需要劫持这段绑定,进行防抖
  2. 防抖控制,防抖(debounce)函数,网上有很多示例,容易实现

那么需求的难点就在于,如何实现点击事件的劫持?在这里有两种方案

Mixins

1. Mixin是什么

Mixins 在官方Vue文档中已经有了很详细的介绍,不熟悉的朋友们可以看看,用一句话来理解,即合并组件的组件

官方介绍

用一张图来表示

2. 怎么解决需求

直接上源码

// 防抖函数
function debounce (func, delay, context, event) {
  clearTimeout(func.timer)
  func.timer = setTimeout(function () {
    func.call(context, event)
  }, delay)
}
// iview中click方法拷贝
function _handleClick (event) {
  this.$emit('click', event)
  const openInNewWindow = event.ctrlKey || event.metaKey
  this.handleCheckClick(event, openInNewWindow)
}
// 导出新组件
export default {
  props: {
  },
  mixins: [Vue.options.components.Button], // iview 中Button组件
  data () {
    return {}
  },
  mounted () {
    console.log('mixins succeed')
  },
  methods: {
    handleClick (event) {
      let that = this
      console.log('debounce')
      debounce(_handleClickLink, 300, that, event)
    }
  }
}

3. 原理

mixins的原理很容易理解,上列源码我们做了这些操作,来实现合并ivew Button组件,劫持click事件

  1. 创建debounce防抖函数
  2. 复制iview Button组件中handleClick方法为_handleClick
  3. 导出对象,methods里重写handleClick方法,进行防抖控制

使用mixins 来实现我们需求很简单,但也因此会有许多问题

  1. 需要知道Button源码结构
  2. 带来了隐式依赖,如果mixins嵌套,会很难理解

那么有没有更好的方法?

HOC

1.什么是HOC?

所谓高阶组件其实就是高阶函数,React 和 Vue 都证明了一件事儿:一个函数就是一个组件。所以组件是函数这个命题成立了,那高阶组件很自然的就是高阶函数,即一个返回函数的函数

HOC的详细介绍和实现,这篇文章探索Vue高阶组件有详细介绍,用一句话来理解,即包裹组件的组件

用一张图来表示

2. 怎么解决需求

直接上源码

// 防抖函数
function debounce (func, delay, context, event) {
  clearTimeout(func.timer)
  func.timer = setTimeout(function () {
    func.call(context, event)
  }, delay)
}
// 导出新组件
export default {
  props: {},
  name: 'ButtonHoc',
  data () {
    return {}
  },
  mounted () {
    console.log('HOC succeed')
  },
  methods: {
    handleClickLink (event) {
      let that = this
      console.log('debounce')
      // that.$listeners.click为绑定在新组件上的click函数
      debounce(that.$listeners.click, 300, that, event)
    }
  },
  render (h) {
    const slots = Object.keys(this.$slots)
      .reduce((arr, key) => arr.concat(this.$slots[key]), [])
      .map(vnode => {
        vnode.context = this._self
        return vnode
      })
    return h('Button', {
      on: {
        click: this.handleClickLink //新组件绑定click事件
      },
      props: this.$props,
      // 透传 scopedSlots
      scopedSlots: this.$scopedSlots,
      attrs: this.$attrs
    }, slots)
  }
}

3. 原理

HOC的特点在于它的包裹性,上列源码我们做了这些操作,来实现包裹iview的Button组件,劫持click事件

  1. 创建debounce防抖函数
  2. 导出新的组件
  3. render渲染出iview Button
  4. Button 绑定debounce后的click方法

HOC的包裹性同时也会带来几个问题

  1. 组件之间通信会被拦截,比如子组件访问父组件的方法(this.$parent.methods)
  2. vue官方并没有推荐使用HOC 🙁

总结

Mixins 和 HOC 都能实现这个简单的需求,希望大家能理解这两种技巧,解决项目中的问题


作者:秋
来源:掘金

Vue.js 3.0 PPT(附部分中文翻译)

NiZerin阅读(3548)

Evan You 刚刚发布了最新的 Vue 3 和他在 Vue Toronto 的演讲内容:

Vue 3.0 将会发生什么?

  • 更快
  • 更小
  • 更易维护
  • 更易于原生
  • 让开发者更爽

Virtual DOM 完全重写,mounting & patching 增快 100%

增多一些编译提醒来减少 runtime 成本

基于 Proxy 观察者机制并满足 full language coverage 及更好的性能

不再使用 Object.defineProperty 而是原生 Proxy

组建生成增快 100%

快一倍 / 减少一般的内存使用

新的 runtime 版只要约 10kb gzipped

自定义 Renderer API

定位一个组建为什么被渲染

更详细信息可以看:

作者:kalasoo
来源:掘金

php,vue,vue-ssr 做出来的页面有什么区别?

NiZerin阅读(3157)

欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~

目前我这边的web页面,都是采用php+smarty模板生成的,是一种比较早期的开发模式。好处是没有现阶段常用的前后端分离出现的首屏问题,因为其本身就是服务器渲染,坏处是代码分离不好做,公用化及组件化不好做。这里涉及前后端分离相关问题,老生常谈,这里暂不讨论。

​ 近期,在做一些前端分离的尝试。采用国内非常流行的的vue框架,选这个框架而不是react的原因主要是vue的mvvm保留html书写惯性,对于html里写代码多的人来说更容易入手。而且流行框架vue也经过了极大量的测试验证,参考资料充实详尽,可靠性和易用性都满足条件,没有理由不尝试一下。

​ 总的来说,做了一个如下小应用demo,长下面这样,三个简单页面,分页查看所有王者英雄,或者所有装备。分别采用 php+smarty,vue-cli,vue+ssr,三种方式进行开发,完了再对结果做一下对比。

​ 三个版本的体验入口如下(尽量用手机浏览器扫描,微信对ip域名有特殊处理),

​ 三个版本并没有严格做相同环境处理,所以下面的对比分析仅作为直观上的对比了解,并不适用于详细性能上的严格对比额。

​ 对三个页面分别进行webpage test,测试结果如下,

▲ 详细结果

​ php版:

https://www.webpagetest.org/r…

​ vue ssr 服务器渲染版:

https://www.webpagetest.org/r…

​ vue-cli 静态版:

https://www.webpagetest.org/r…

▲ 综合参数

1、页面加载时间。理所当然是纯静态的vue-cli最快。vue ssr 和 php 版差不多(忽略上面的php版,因为php版有一些额外资源要加载)。

2、首字节时间。静态的最快。若扣除dns时间,其实php和vue-ssr版差不多。(注:php版和vue ssr版不是部署在同一台机器上,php版机器性能要强一些,多核,vue-ssr版机器比较弱单cpu单核)

3、渲染时间和页面呈现熟读指数,vue ssr版比php版本稍微慢一点。这是因为,php的html到页面后直接就呈现了,而vue ssr到client后,有一个vue框架的渲染过程。

▲ 加载瀑布流

​ 从加载流的角度上看一下三者的区别,

php版本

vue ssr 服务器渲染版本

vue-cli静态版本

​ 从瀑布流上可以看出很多三种页面执行方式的区别,列举一部分如下:

1、php 版以及 vue-ssr 版 有较长的服务器处理时间,,,对应的首字节时间明显高于没有服务器处理的vue-cli静态页面。

2、由于服务器版本的php或者vue-ssr的首屏数据都已经生成了,所以页面不会再次请求接口,少了数据的请求过程。而vue-cli版有一个较长的数据请求过程。

3、vue-cli静态页面的dom content time 或者 document complete time 明显最短,原因是模板html几乎没什么内容。

4、webpack打包拆离出来的独立js或者css文件,其实在同一域名下,由于浏览器同一域名可以并行6个tcp,以及http的keep-alive性质,其实总的下载时间不多。对比看,跟阻塞的dns时间差不多。

5、三种页面 Start Renderer Time 分别是 1.2s,1.3s,2.0s。 vue-cli静态页面生成的白屏时间中,大部分是首屏数据请求消耗的时间,,同时也可以对比出,服务器渲染的对首屏时间的确有很明显的效果。

▲ 直观体验

​ 时间,,平均速度指数Speed Index,分别是1.2,,,1.3,,,2.0s,,,可以观察下面的对比视频体验。

​ >点此观看动态视频<

此文已由作者授权腾讯云+社区发布,更多原文请点击

electron-vue模仿网易云桌面应用体验

NiZerin阅读(2358)

vue-源码剖析-双向绑定

项目中vue比较多,大概知道实现,最近翻了一下双向绑定的代码,这里写一下阅读后的理解。

项目目录

拉到vue的代码之后,首先来看一下项目目录,因为本文讲的是双向绑定,所以这里主要看双向绑定这块的代码。

入口

从入口开始:src/core/index.js

index.js 比较简单,第一句就引用了Vue进来,看下Vue是啥

import Vue from './instance/index'

Vue构造函数

来到 src/core/instance/index.js

一进来,嗯,没错,定义了Vue 构造函数,然后调用了好几个方法,这里我们看第一个initMixin

import { initMixin } from './init'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
initMixin(Vue)
...
export default Vue

初始化

来到 src/core/instance/init.js

这个给Vue构造函数定义了_init方法,每次new Vue初始化实例时都会调用该方法。

然后看到_init中间的代码,调用了好多初始化的函数,这里我们只关注data的走向,所以这里看一下initState

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    initState(vm)
    ...
  }
}

来到 src/core/instance/state.js
initState调用了initDatainitData调用了observe,然后我们再往下找observe

import { observe } from '../observer/index'
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  ...
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  ...
}

function initData (vm: Component) {
  let data = vm.$options.data
  ...
  observe(data, true /* asRootData */)
}

Observer(观察者)

来到 src/core/observer/index.js
这里,实例化Observer对象
首先,new Observer实例化一个对象,参数为data

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  ob = new Observer(value)
  return ob
}

然后我们来看下Observer构造函数里面写了什么,这里给每个对象加了value和实例化了一个Dep,然后data为数组的话则递归,否则执行walk
walk这里是对对象遍历执行defineReactive

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    if (Array.isArray(value)) {
      ...
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

然后,我们来看defineReactive做了什么,嗯,这里就是Observer的核心。
Object.defineProperty对对象进行配置,重写get&set

get:对原来get执行,然后执行dep.depend添加一个订阅者
set:对原来set执行,然后执行dep.notify通知订阅者
Dep是干啥的呢?Dep其实是一个订阅者的管理中心,管理着所有的订阅者

import Dep from './dep'
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      dep.notify()
    }
  })
}

Dep(订阅者管理中心)

那么,到这里了,疑问的是什么时候会触发Observerget方法来添加一个订阅者呢?
这里的条件是有Dep.target的时候,那么我们找一下代码中哪里会对Dep.target赋值,找到了Dep定义的地方

来到 src/core/observer/dep.js
pushTarget就对Dep.target赋值了,然后来看一下到底是哪里调用了pushTarget

export default class Dep {
  ...
}
Dep.target = null
const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

然后,找到了Watcher调用了pushTarget,那么我们来看一下Watcher的实现
来到 src/core/observer/watcher.js
这里可以看到每次new Watcher时,就会调用get方法
这里执行两步操作
第一:调用pushTarget
第二:调用getter方法触发Observerget方法将自己加入订阅者

export default class Watcher {
  vm: Component;
  constructor (
    vm: Component
  ) {
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    }
    return value
  }
}

接着,Dep.target有了之后,接下来就要看一下dep.depend()这个方法,所以还是要到Dep来看下这里的实现。 来到 src/core/observer/dep.js
这里调用了Dep.target.addDep的方法,参数是Dep的实例对象,那么我们看下addDep

export default class Dep {
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
}

Watcher(订阅者)

又来到 src/core/observer/watcher.js
到这,addDep其实又调用时Dep实例的addSub方法,参数也是把Watcher实例传递过去
然后,我们看上面的addSub,这里就是把Watcher实例pushdepsubs数组中保存起来
到这里,就完成了把Watcher加入到Dep这里订阅器管理中心这里,后面的管理就由Dep来统一管理

export default class Watcher {
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
}

走完了添加订阅器,接着再来看下Observerset方法,这里调用了dep.notify,我们来看一下这个方法

来到 src/core/observer/dep.js
这里,就是对subs中的所有Watcher,调用其update方法来更新数据

export default class Dep {
 notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

这里,我们就来看看Watcher是怎么更新的 又来到 src/core/observer/watcher.js
update调用的是run方法,run方法这里先用get拿到新的值,然后把新&旧值做为参数给cb调用

export default class Watcher {
  update () {
    this.run()
  }
  
  run () {
    if (this.active) {
      const value = this.get()
      const oldValue = this.value
      this.value = value
      this.cb.call(this.vm, value, oldValue)
    }
  }
}

这里的cb其实是实例化的时候传进来的,这里我们看一下什么时候会实例化Watcher
回到一开始的initState:src/core/instance/state.js
initState的最后还调用了initWatch,然后再createWatcher,最后$watch的时候就实例化了Watcher对象,这里就把cb传到了Watcher实例中,当监听的数据改变的时候就会触发cb函数

import Watcher from '../observer/watcher'
export function initState (vm: Component) {
  ...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    createWatcher(vm, key, handler)
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  return vm.$watch(expOrFn, handler, options)
}

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  const watcher = new Watcher(vm, expOrFn, cb, options)
}

写在最后

这里的思路,其实就是翻着源码走的,写的时候都是按自己的理解思路来的,存在问题的话欢迎指出~

PHP开源Hub-致力于互联网开发者的成长

技术群聊软文发表