Chloe

考虑在你的下一个Web项目中使用VueJS吧 - via @codeship

Chloe · 2017-06-15翻译 · 550阅读 原文链接

不管你之前有没有听说过 VueJS,不要恐惧。下面我来分享一些 Codeship 为什么在 Docker-builds UI, Jet 中使用这个新库的见解。

VueJS是什么?

先来大致了解一下 Vue(发音同 view)是什么,你可以将它想象为 MV* 模式中的 V 层。它类似于入门难度比较低的类 React 框架。不需要学习 JSX 或预编译语言,只要像引入 JS 类库一样引入它即可使用。模板语法也类似于 Angular 或 Mustache,如果你使用过的话。

下面我们来聊聊为什么 VueJS 为什么这么有吸引力吧。

数据驱动 vs DOM驱动

解耦在软件开发中是一种常见的模式,在界面开发中,我们经常看到这样的代码:

function textReturningFunction () {
  return 'Hello World!'
}

// Set some text in jQuery
$('.someElement').text(textReturningFunction())

在我的职业生涯初期我像这样写 JS 写了很久,但是仔细想想,这样的代码离解耦的思想还很遥远。

在这样的代码中,JS 必须知道你的 DOM 结构。如果 DOM 结构变了,比如 '.someElement' 节点变为 '.som_element'后,你的 JS 就无法正确运行了。这个独立的方法被调用时仍然能返回正确的数据,因为在这个例子中,返回值是一个固定的字符串,但是 DOM 绑定关系失效了,原因是 JS 是 DOM 驱动的。

让我们来看一个简单的 VueJS 的例子,它将像上面一样打印出同样的文本:

HTML:

<div class="app">
  <span class="someElement">
    {{ msg }}
  </span>
</div>

JS:

new Vue({
  el: '.app',
  data: {
    msg: 'Hello World!'
  }
})

Vue 实例是一个简单的对象。运行这段代码,你将会在页面中看到渲染出了 <span class="someElement">Hello World!</span>。如果 span 的 class 变了,JS 也不会被影响,因为与元素的 class 没有关联。

在 Vue 实例中,我们首先指定一个元素作为关联容器。这里 .app 元素作为关联容器就是 Vue 处理逻辑的入口,如果它改变了,也需要做相应的更新。

响应式(Reactivity)

Vue 中所有的变量都是响应的。这意味着它们都能被监听,也能被附加到 watchers,变量改变时可以自动通知触发相应事件。在 VueJS 的官方文档中,我强烈推荐你阅读一下 深入响应式原理 这一篇。

为了便于理解,这里将 Vue 的行为与 Angular 的脏检查作对比。Anguar 中如果数据有任何变化,将会启动检查,将新数据与旧数据一一对比,直到没有发现新的变化,需要的时候会发生更新。这需要消耗一些时间和资源,尤其是应用比较复杂的情况下。 像 Vue 这样的响应式数据系统可能需要更多的学习和接纳时间,不过这一切都是值得的。

组件

Vue 灵活而简单的系统是围绕着组件的改变来建立的。组件是小的、可重用的 UI 模块。实际上,这并不是一个新的概念,因为现如今的许多 JS 框架都是围绕这个概念建立的。Web Components 已是一个众所周知的解决方案,但它有个问题是一般都是在浏览器范围内实现的。

Vue 通过提供一个独立于浏览器的组件系统来解决这一问题,它使用非常简单。

New Call-to-action

为什么选择 VueJS

在我们的应用中,需要创建一个交互复杂的页面:它是基于 docker 的构建详情页面 (Jet),让用户可以看到终端的输出。这个页面的挑战包括以下几点:

  • 解决可能导致浏览器卡死的性能问题
  • 大对象高效渲染(10K+)
  • 代码的可读性更强
  • 减少引入新技术的开销

刚开始我们选择了 Angular。服务器需要将终端输出的一个非常大的字符串传输给前端,客户端将这个字符串渲染到页面上。使用对象去渲染时 Angular 的性能不够好。当数据量达到一定程度时,浏览器崩溃了。经过一些问题追踪,我们知道了这一切都是 Angular 的 scope 导致的,数据量不是问题,只是因为 Angular 总是试图追踪 DOM 和它们的所有变化。

我们需要一个新的技术,它在某种程度上不关心 DOM,同时,我们不需要完全改变已有的架构。Vue 看起来就是我们需要的。

一个大的HTML文件的原型(超过1MB)倒入浏览器是有希望的,Vue 没有明显的性能问题,在下一次迭代中,我们尝试基于纯 JS 对象来渲染页面,一万行内容都ok。经过一些测试和优缺点罗列,使用 Vue 是有意义的。下面展示一下做了什么。

开始使用 VueJS

第一个重构迭代的任务主要是建立构建详情页面的基础结构,目标是渲染出页面的边栏:

<!-- Application Example -->
<div id="app">
    <aside>
        <ul class="services">
            <li v-for="service in services">
                {{ service.name }}
            </li>
        </ul>
    </aside>
</div>

看看这段 HTML 代码片段多么易读,即使你对 Vue 的语法还不熟悉,代码本身也很好理解。对于services 数组中的每一个 service 对象,渲染出一个包含 service 名称的 li 标签。

下面看看 Vue 代码:

new Vue({
  el: '#app',

  data: {
    services: [
      {name: 'first service'},
      {name: 'second service'}
    ]
  }
})

在浏览器运行后,结果如你所愿:

<!-- Rendered HTML -->
<div id="app">
    <aside>
        <ul class="services">
            <li>first service</li>
            <li>second service</li>
        </ul>
    </aside>
</div>

下一步是引入组件的实现:

<!-- Application Example -->
<div id="app">
    <aside>
        <ul class="services">
            <!-- Placeholder for the component -->
            <jet-service v-for="service in services" v-bind:service="service"></jet-service>
        </ul>
    </aside>
</div>

Vue 组件的实现方法也很简单:

// 声明一个组件
var JetServiceComponent = Vue.extend({
  name: 'jet-service',
  props: ['service'], // made available by v-bind:service="service"
  template: 
    <li>
      <strong>{{ service.name}}</strong>
    </li>
  
})

// 在 Vue 实例中注册组件
new Vue({
  el: '#app',
  data: {
    services: [
      {name: 'first service'},
      {name: 'second service'}
    ]
  },
  components: {
    'jet-service': JetServiceComponent
  }
})

重新运行代码会得到下面的输出:

<!-- Rendered HTML with components-->
<div id="app">
    <aside>
        <ul class="services">
            <li><strong>first service</strong></li>
            <li><strong>second service</strong></li>
        </ul>
    </aside>
</div>

到目前为止,VueJS 的使用都是非常直观的。不需要大量的样例代码,你唯一需要的是对 JS 对象和函数有很好的理解。

VueJS 深入学习

jet-steps 组件有点儿复杂,Steps 可能会分组或嵌套,每个对象的结构可能都不一样。我们假设是下面的结构:

-- some step
-- step group
  -- grouped step alpha
  -- grouped step beta

或者是一个 JSON对象:

[
  { name: 'some step', type: 'step'},
  { name: 'step group', type: 'group_step', steps: [
    { name: 'grouped step alpha', type: 'step'},
    { name: 'grouped step beta', type: 'step'},
  ]}
]

基于目前的结构,组件应该可以自调用,这可能有点复杂。Vue 使用特定的 key 和 value 来结构化页面。getters 可以用在模板里,computed 属性允许我们在 Vue 实例中保存一个计算属性。下面是用一个方法来检查 stepgroup step的例子:

var JetStepsComponent = Vue.extend({
  name: 'jet-step',

  props: ['step'], 

  computed: {
     isGroupStep: function () {
       return this.step.type === 'group_step'
     }
  },

  template: 
    <li>
      <span>{{ step.name }}</span>
      <ul v-if="isGroupStep">
        <jet-step v-for="step in step.steps" v-bind:step="step"></jet-step>
      </ul>
    </li>
  
})

你可能已经猜到页面展示的样子了,嵌套组件也不会变的更复杂,这里引用一条 Vue 的作者尤雨溪写的 推文

Thoughts on simple versus easy: Why not make it simple AND easy?

综上,这就是 Vue 为什么这么高效简洁。

VueJS 性能

在这一点上,我们再来看看Vue的语法,看看它在前面例子中的优势。 展现是我刚才提到的最大的问题,UI 需要向用户展现大量的终端输出。 我在开发过程中对性能进行了一些基准测试,该测试产生 5,000 行的 Base64 编码随机日志行。 日志对象将如下所示:

{
  timestamp: 'some UTC timestamp',
  service: 'app',
  payload: 'A base64 decoded string'
}

第一个思路是在循环中渲染每个对象行,这应该是很显而易见的,但是 Angular 在这方面做得不好。

问题

实现的方式是将 HTML 作为字符串推送到服务器上,并将其传递给客户端。 然后,客户端将使用纯JS将该字符串附加到DOM。这种方法是最快的。

但是,随着数据接近8000行,Angular 会定期冻结浏览器标签,特别是在较慢的客户端。经过调查,事实证明,Angular对追踪内部范围的需求正在杀死,8000行日志输出生成32,000个DOM节点。 Angular 不能像我们所希望的那样处理这些。

第一个解决方案

为了使它能正常工作,我们的解决方法是提供一个定制的 worker,它将字符串直接转换为DOM节点,但是每40毫秒只能在视图中注入200行。 这样可以正常运行,Angular可以处理多达 15,000 行日志。 但是,到了这个级别,浏览器的性能已经下降了,页面变的很卡。 显然,这样仍不够好,我们有日志量更大的客户端。此外,服务器也承受了太多的工作。Base64解码和预编译都是在客户端之外完成的。 为了对渲染效率有一个粗略地理解,我使用5000条日志行测试了旧UI和第一个Vue实现,它们之间唯一的区别是,Vue没有使用 worker,日志被直接转储到视图中。

Tech Processes Rendering Total
Angular 270 6,000 6,270
Vue 120 2,000 2,120
  • 单位为ms
  • 这部分统计是由日志呈现之外的代码完成的

通过切换工具,我们已经将时间缩减了一半。

最终解决方案

在下一次迭代中,我想充分利用客户端渲染日志行,普通的对象被传递过来,并且是Base64编码的。 渲染日志行的循环就像人们想象中那么简单。

html

<div class="logLine" v-for="line in log">
    <span> {{ line.timestamp }} </span>
    <span> {{ line.service }} </span>
    <span> {{ line.payload }} </span>
</div>

在打印日志之前还需要解码,为此,我创建了一个有几个函数的JS类。Vue 并没有强迫我以某种复杂的方式做事情,去掉 ajax 的代码看起来像这样:

// The class is written in ES6
class LogHelper {

  getLog () {
    // ... function that gets the log from the server
    // eventually we have the raw log available for further use
    let rawLog = [...]

    return _prepareLog(rawLog)
  }

  _prepareLog (arr) {
    let decodedLog = arr.map( (line) => {
      line.payload = this._decode(line.payload)
      return Object.freeze(line)
    })

    return decodedLog
  }

  _decode (str) {
    // This correctly preserves UTF8 characters
    return decodeURIComponent(Array.prototype.map.call(atob(str), function(c) {
      return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
    }).join(''))
  }

}

最后,在 Vue 实例中有日志的引用,所以UI可以访问它。 我最终使用了 Vuex,这是一个受 Flux 或 Redux 启发而来的状态管理工具,不过这又是另一个故事了。 只要“log”可在模板中使用,Vue 就会渲染它。 看看新数据:

Tech Processes Rendering Total
Angular 270 6,000 6,270
Vue 120 2,000 2,120
Vue (Objects) 1,900 1,700 3,600

这些数据很有趣,我们来分析一下:

Angular vs Vue (Objects)

这里有个重要细节是总处理时间快了将近50%,但除了这一点,还释放了服务器端的处理时间。我们现在在客户端上进行Base64解码,不需要预渲染HTML字符串。这也使得从服务器初始加载的数据量更小。妥妥的赢了!

Vue vs Vue (Objects)

其中的一个大问题可能是为什么通用处理的时间如此之大,这是因为 Vue 会在浏览器的虚拟DOM中预渲染所有的对象。相对的,实际渲染时间比以前的 Vue 实现快了300ms。

当过滤日志行时,虚拟DOM开始闪烁。 每个对象关联的DOM元素都已经被缓存,可以立即重新渲染。 这是我愿意承担的花费。

为什么 Vue 对你有用

在任何工作中都没有银弹,但是总有适合它们的特定工具。Vue 是我认为非常有用和高效的工具。它只是尝试在一件事上非常擅长,并旨在:将数据展示在 Web 应用的界面中就像在这篇文章看到的一样简单。对于 Vue 来说,更有吸引力的是它可以在需要时成长为更有用的东西。

目前为止, Vue 已经有了强大的生态系统:

  • 路由 Vue-Router
  • 异步加载 Vue-resource
  • 应用状态管理 Vuex
  • Webpack or Browserify,Vue 都提供了插件

Vue 的社区非常活跃,Github 已经有很多 star (21,000 vs 13,000 for Angular),值得信任。

你在下面的评论里告诉我你的想法,我希望这篇文章至少吸引你在下一个项目中去尝试 Vue。

译者Chloe尚未开通打赏功能

相关文章