加载中

As I'm writing this, we've just removed the last line of AngularJS code from our application codebase, ending a 4-month non-intrusive effort to migrate our application from AngularJS to VueJS. In this post, I'm going to share our experience going through the process.

Some Background

Our application (Holistics.io) is a SQL-based Business Intelligence (BI) Platform written with Rails, Sidekiq, PostgreSQL and AngularJS. Our Rails app started in late 2013 as a simple app with jQuery and AngularJS. We used AngularJS mainly for the following features/functionalities:

  • View model binding (controller, view + template engine)

  • Dependency Injection (services, factories, directives)

  • Angular 3rd party components (uib-modal, ui-select, ...)

The rest are mostly in-house, custom JavaScript.

在我写这篇文章的时候,我们刚刚从我们的应用程序代码库中删除了最后一行AngularJS代码,结束了一个为期4个月的非侵入性工作,将我们的应用程序从AngularJS迁移到VueJS。在这篇文章中,我将分享我们在整个过程中的经验。

一些背景介绍

我们的应用程序(Holistics.io)是一个基于SQL的商业智能(BI)平台,使用Rails、Sidekiq、PostgreSQL和AngularJS编写。我们的Rails应用程序始于2013年底,作为一个简单的应用程序其中使用了jQuery和AngularJS。我们使用AngularJS主要特性/功能如下:

  • 查看模型绑定(控制器,视图+模板引擎)

  • 依赖注入(服务,工厂,指令)

  • Angular第三方组件(uib-modal,ui-select,...)

其余的都是内部的自定义JavaScript。

Our Problems with Angular

As our application grew, these are the problems we're facing with AngularJS:

  • Rendering Performance: As a data tool, a lot of the time we have to render a large table of data, and rendering them with AngularJS yield pretty bad performance.

  • Angular documentation is bad: This isn't a big deal until it is. The more we work with AngularJS, the more we realized that their documentations are really difficult to comprehend.

  • Two-way data flow makes handling logic very difficult, both in term of writing components, as well as writing view controllers. This is perhaps the most important reason of all.

Considering Different Frameworks

Before making a decision, we took a hard look at our different options:

Angular 2

We did spend some time looking into Angular 2. And to us, Angular 2 is somehow even more confusing than Angular 1. There are too many new changes (TypeScript), new template syntaxes, etc that we felt didn't really address our core problems. Furthermore, the migration path from v1 to v2 felt a bit fuzzy to us at the time.

我们在 Angular 中遇到的问题

随着我们应用的升级,我们在使用 AngularJS 的时候遇到了这样一些问题:

  • 渲染性能:作为数据工具,由于AngularJs的特性,我们不得不花大量的时间来呈现一张巨大的数据表。

  • Angular 的文档不太好:在这成为问题之前,其他都不算什么问题。我们越深入地使用 AngularJS,就越觉得它的文档实在难以理解。

  • 双向数据流使得逻辑处理起来相当困难,不管是写组件还是写视图控制器都是如此。这可能是AngularJS不好使用最重要的一个原因。

考虑不同的框架

在决定之前,我们仔细看看各个选项:

Angular 2

我们确实花了些时间来研究 Angular 2。对于我们来说,Angular 2 甚至比 Angular 1 还让人难以理解。它带来了太多新的变化(TypeScript),新的模板语法等,但我们觉得这并没有真正解决我们的核心问题。除此之外,从 V1 到 V2 的迁移之路在那时就一直让我们觉得困惑。

ReactJS

We took a hard look into ReactJS. While we really liked the philosophy and the ecosystem, the biggest thing that struck us is we couldn't figure out a clear, clean, gradual migration path that doesn't stop us from supporting new features for 3-4 months.

AngularJS follows html-based templating system, while ReactJS is JSX, and we couldn't figure out a way to let both of them live nicely together during the migration.

Another minor, subjective reason is, I found JSX rather lengthy compared to HTML-based templating.

EmberJS

EmberJS is the framework for web applications, not a JS library, we would have to rewrite the entire thing in EmberJS.

ReactJS

我们仔细审视了 ReactJS。尽管我们非常喜欢它的哲学和生态原理系统,但有一件事情让我们吃惊:我们找不到一条清晰、干净、渐进的迁移路径,来阻止我们在3-4个月内支持新功能。

AngularJS 的使用基于 HTML 的模板系统,而 ReactJS 是用的 JSX。我们不能找到办法在迁移的过程中让两种技术很好的并存。

还有一个次要的主观原因,我发现 JSX 比基于 HTML 的模板更加冗长。

EmberJS

EmberJS 不是一个 JS 库,而是一个 Web 应用框架,我们必须用基于 EmberJS 重写所有东西。

Why We Went With VueJS: Incremental Migration

There are a combination of reasons that we chose VueJS in the end, but the biggest and most defining factor for us is: we saw a clear, incremental migration path to VueJS without disrupting our development roadmap. In fact, I bet none of our customers ever notice any visible changes at all during the whole period, they wouldn't know which page they visit is Angular-based, which page is VueJS-based.

Vue took the similar component-based, props-down-events-up approach of ReactJS, yet surprisingly similar to AngularJS in term of templating engine. It feels like the right mix of AngularJS and ReactJS combined. This works perfect for us, since we have a lot of AngularJS templates, and our main problem is the logic complexity introduced while using Angular components.

我们为什么选择了 VueJS:逐步迁移

综合考虑各方面因素,我们最终选择了 VueJS,但对于我们来说,最重要的决定性因素是:我们看到了一个清晰、可逐步迁移到 VueJS,而又不会破坏发展路径的迁移路径。实际上,我敢打赌,在整个迁移期间,我们的客户都没有注意到什么明显的变化,他们不会知道自己访问的页面中,哪些是 Angular 实现的,哪些是 VueJS 实现的。

Vue 采用了与 ReactJS 相似的技术,基于组件,属性下行事件上行等。它与 AngularJS 在模板引擎方面有着惊人的相似。它就像 AngularJS 和 ReactJS 的优美结合。这让我们觉得完美,因为我们有大量的 AngularJS 模板,而我们的主要问题是 Angular 组件带来的复杂逻辑。

In fact most of the time what we needed to do was to change our code from ng- to v-. This is brilliant!

The more we dug into it, the more we realized we're making the right choice that solved all our earlier problems: great performance, single file component, very clean code structure, slots, etc.

Also, during the migration process, because of the way Vue structures (one way data flow, component-based), it forced us to rethink and refactor rather poorly written code, thus greatly simplify our code logic.

One last point that I'd like to mention, is I find VueJS documentations extremely well-written and well-structured. This is another major feature why we chose VueJS. When I was first introduced about Vue, I spent 30 minutes reading its documentation, and felt immediately compelled to try out.

实际上多数时候我们需要做的只是将代码中的 ng- 改为 v-,简直太美妙了!

随着深入,我们越发觉得作出了正确的选择,它解决了我们早期遇到的问题:很好的性能,单文件组件,清晰的代码结构,槽,等等。

而且,在迁移的过程中,由于 Vue 结构的方式(单向数据流,基于组件),它迫使我们反思和重构代码,而不是继续写烂代码,这简化了我们的代码逻辑。

我还想说的最后一点,我发现 VueJS 的文档写得非常好,结构也非常清晰。这也是我们选择 VueJS 的另一个主要原因。我第一次使用 Vue 的时候,花了 30 分钟来阅读它的文档,立即觉得必须要试试这个东西。

How We Incrementally Migrated:

Below are simple steps we took for the migration (note that this is related to our context of running a Rails app, if you're not using Rails, some of these might be different):

1. Convert AngularJS controllers' logic to VueJS

For an incremental migration strategy to work, we do the minimum amount of work needed to introduce VueJS to our code. Thus we start by changing our standard AngularJS controller + template files and introduce Vue to it:

// user_edit_controller.js.es6`
import Vue from 'vue'

app.controller('UserEditCtrl', ['$scope', '$http', 'Ajax', 'Util', 'Modals',  
  function ($scope, $http, Ajax, Util, Modals) {
    let vapp = new Vue({
      el: '#v-wrapper',
      components: {
          ...
      },
      data: {
      }
    });
  }
]);

<!-- `users/edit.html.erb` -->

<div ng-controller="UserEditCtrl">  
  <div id="v-wrapper">
    <!-- vuejs logic goes here... -->
    <input v-model="username" placeholder="Username" />
    ...
  </div>
</div>

This allows us to introduce Vue logic into our app, without changing any of the underlying app's frontend structure. It greatly helps us mitigate risks of making mistakes, as we can just quickly spend 1-2 hours converting a small portion of AngularJS to VueJS, test and deploy, without having to worry of causing regression bugs.

我们是怎么进行逐步迁移的:

下面是我们迁移的简单步骤(注意有些内容与我们运行的 Rails 应用环境有关,如果你没有使用 Rails,可能会有些不同):

1. 把 AngularJS 控制器逻辑转换到 VueJS

在逐步迁移策略中,我们要在引入 VueJS 时尽可能少做改动。因此,我们从标准的 AngularJS 和模板文件开始修改,将 VueJS 引入其中:

// user_edit_controller.js.es6`
import Vue from 'vue'

app.controller('UserEditCtrl', ['$scope', '$http', 'Ajax', 'Util', 'Modals',  
  function ($scope, $http, Ajax, Util, Modals) {
    let vapp = new Vue({
      el: '#v-wrapper',
      components: {
          ...
      },
      data: {
      }
    });
  }
]);

<!-- `users/edit.html.erb` -->

<div ng-controller="UserEditCtrl">  
  <div id="v-wrapper">
    <!-- vuejs logic goes here... -->
    <input v-model="username" placeholder="Username" />
    ...
  </div>
</div>

我们在应用中引入了 Vue 的逻辑,却不需要改变任何相关应用的前端结构。这极大程度的帮助我们降低在迁移过程中发生错误的风险,我们可以花 1-2 个小时迅速地将一小部分 AngularJS 实现的东西转换为 VueJS 实现,然后测试并部署,不用担心它会导致回归缺陷

2. Convert AngularJS services to ES6 modules

First, we need to find a replacement for $http service, which we use the most among the AngularJS. For this, we simply change to using axios. We don't use $http$directly but build a wrapper abstraction around it, so changing it is pretty simple.

Then, we have a lot of AngularJS services defined as:

// users.js
app.service('Users', ['$http', 'Ajax',  
  function ($http, Ajax) {
    this.create = function(user) {
      // ...
    }
  }
]);

We simply change them to using ES6 class:

// users.js.es6
export default class Users {  
  static create(user) {
    // ...
  }
}

If there's still Angular code in other controllers using these services, we clone the services to using ES6 Class, and keep the old Angular copy until no other Angular code uses this anymore. This duplicates our code for a while, but we just need to be careful about this, and make extensive comments in the code to link between the 2 copies.

2. 把 AngularJS 服务转换为 ES6 模块

我们首先需要找到一个 AngularJS 中大量使用的 $http 服务的替代品。我们直接使用了 axios。我们不再直接使用 $http,但我们围绕它进行了抽象封装,以使修改变得非常简单。

然后,我们像这样定义了大量的 AngularJS:

// users.js
app.service('Users', ['$http', 'Ajax',  
  function ($http, Ajax) {
    this.create = function(user) {
      // ...
    }
  }
]);

使用 ES6 类语法代替就好:

// users.js.es6
export default class Users {  
  static create(user) {
    // ...
  }
}

如果其它控制器中使用有 Angular 代码在使用这些服务,我们会复制这些服务并使用 ES6 类语法来实现,同时保持原有的 Angular 版本,直到不再有 Angular 代码使用它们为止。这会在短时间内让代码出现重复,但我们只需要小心一点,在代码中多写些注释来关联两个版本。

3. Replace AngularJS controllers with VueJS components

After doing the above 2, some AngularJS controllers can be completely moved over to using VueJS, so similar to step 2 above, we swap out the app.controller() definition with using ES6. But this time, we then use Vue's Single File Component:

File: user_edit.vue

<template>  
  <div>
    <!-- vuejs logic goes here... -->
    <input v-model="username" placeholder="Username" />
    ...
  </div>
</template>

<script>  
import Users from 'users.js.es6'  
export default {  
  data: {    
  },
  methods: {   
  },
  mounted() {
    // initializing
  }

};
</script>

4. Add mounting entry point for Rails controller/view

By default, on every rails page load it will render the view. With the above Vue's Single File Component, we then change our Rails view users/edit.html.erb file to:

<div class="v-user-edit">  
  <user-edit></user-edit>
</div>

And then put a code to mount it to the correct Vue component when the page loads:

import UserEdit from 'user_edit.vue'  
let vueConfig = {  
  el: '.v-user-edit',
  components: {
    UserEdit
  }
};
new Vue(vueConfig);

In practice, the above JS code is abstracted into reusable function to be called in different pages.

3. 使用 VueJS 组件代替 AngularJS 控制器

完成上述两点之后,一些 AngularJS 控制器可以完全迁移到 VueJS,这和上面的第 2 步相似,我们使用 ES6 语法来代替 app.controller() 定义。不过这次,我们使用 Vue 的单文件组件:

文件:user_edit.vue

<template>  
  <div>
    <!-- vuejs logic goes here... -->
    <input v-model="username" placeholder="Username" />
    ...
  </div>
</template>

<script>  
import Users from 'users.js.es6'  
export default {  
  data: {    
  },
  methods: {   
  },
  mounted() {
    // initializing
  }

};
</script>

4. 为 Rails 控制器/视图添加安装入口

默认情况下,每个 Rails 页面加载的时候会渲染视图。在上面的 Vue 单文件组件中,我们将 Raisl 视图,users/edit.html.erb 文件,改为:

<div class="v-user-edit">  
  <user-edit></user-edit>
</div>

然后加些代码在页面加载的时候将其安装为正确的 Vue 组件:

import UserEdit from 'user_edit.vue'  
let vueConfig = {  
  el: '.v-user-edit',
  components: {
    UserEdit
  }
};
new Vue(vueConfig);

实际上,上面的代码被抽象成了可复用的函数,在不同的页面中调用。

Conclusion

We finished migrating our framework late September 2017, about 4 months of non-intrusive work (we continue adding other features while working on the migration). In fact we didn't really put the migration at our top priority, whenever we need to make a change that touch old Angular code, we first converted it to Vue, and then make our change.

After the migration, now we have achieved:

  • Very clean code and modular (component-based), VueX and Vue Store; these greatly increased programming productivity

  • No more complex logic

  • Enhance UI performance

This is not to say Vue is the best, it simply worked best for our specific case: an Angular-heavy application that hits some limitation due to inherent design nature of Angular, and Vue filled that gap perfectly for us, with a very natural, incremental path of migration.

What are your thoughts? Do share with us in the comments below. Would love to learn more about your case!

总结

我们在 2017 年九月末完成了框架的迁移,经历了大约 4 个月时间的非侵入性(我们在迁移过程中会继续添加其它新特性)工作。实际上我们并没有把迁移作为最紧急的任务,不管什么时候,只要我们的改动涉及到旧的 Angular 代码,我们会先把它转换成 Vue 实现,然后再进行变更

迁移完成后,我们收获了:

  • 非常整洁的代码和模块(基于组件的),以及 VueX 和 Vue Store;它们大大提高了编程效率

  • 再没有复杂的逻辑

  • 改善了 UI 性能

这并不是说 Vue 就是最好的,它只是在我们特定的情况下工作良好:Angular 固有的设计本质导致使用 Angular 实现应用很重,而 Vue 为我们很好地填补了这一空白,这一切都是自然而然地逐步迁移实现的。

你怎么想?请在下面的评论中与我们分享。希望了解更多你们遇到的情况!

返回顶部
顶部