jQuery到Vue的遷移之路


背景

在前段時間做了L10的某個超復雜超多坑的三端專題之后,組里的小伙伴們一致認為是時候想辦法統一一下組里的開發模式了。因為用nie那一套jQuery/zepto(下文jQuery默認包括zepto)的話,十個人就有十種習慣,不管是代碼組織也好,頁面結構也好,邏輯處理也好……

其實如果像一般的專題,開發周期短,生命周期短的,用傳統的方式開發也還好,不需要后期維護,不需要多人協作。但是,如果項目稍微復雜一點,問題就來了,一碰到需要多人協作的項目,不同的人都有不同的組織代碼的習慣,維護起來效率相當低下。所以,我們決定是時候在稍微復雜一點的項目中用一些特定的技術,這樣就能通過某些約定好的方式來開發一個項目,盡量降低協作開發和維護的成本。

由於在這之前,我也用過一些mv*的技術去開發一些需要長期維護的項目,例如react、vue。考慮到相比較而言vue的易上手性,我和組內的同事決定用vue的那一套技術,vue用於視圖層,vue-router用於單頁應用的路由,vuex我覺得可以暫時不用考慮,因為我們現在做的項目多數為不需要長期維護的專題類網站,數據結構也不會復雜到需要用到數據流工具的程度,管理數據可以根據vuex的思想實現一個非常簡單的簡化版vuex。

 

Vue V.S. jQuery

講道理把Vue和jQuery擺在一起比較是不太合適的,一個是mv*架構中的view層的一個庫,一個是跟DOM操作結合比較緊密的js庫,干的事情不同,也就不存在直接的對立關系。社區里很多人比較這兩種技術可能是出於vue所提倡的數據驅動視圖和jQuery的直接操作DOM在編寫頁面時的思路完全不同的考慮。雖然兩種思路是完全不同的,但也不能說是不能一起用的,在某些沒有辦法的情況下(稍后會說到),把jQuery和vue用在一塊是完全沒問題的,只是說在項目中沒有了純粹的數據驅動視圖了而已。

當然把這兩種技術用在一起是肯定不會出現在最佳實踐里的,因為確實沒有特殊情況的話,這樣用就是有點自找麻煩了。尷尬的是,上面提到的沒有辦法的情況出現了,我們部門的組件庫里的組件全部是基於jQuery的,其中有純UI組件和跟業務有強關聯的UI組件。其中,純UI組件很好辦,自己用vue寫一個或者在vue社區里找一個代替就行(尤其是現在vue的社區資源正在蓬勃發展,有很多已經非常成熟的持續維護的vue組件庫),但是跟業務有關的組件就沒辦法了,這就是為什么會把vue和jQuery一起用的原因。

好在jQuery和vue只是在編程思路上有所不同,兩個技術的應用場景並不沖突,只是在開發過程中需要多注意一下,在數據驅動視圖的代碼中,還混入了一些直接操作DOM的代碼。

 

代碼組織構建工具

在說代碼組織之前,還得先說說構建工具,畢竟構建跟代碼的組織方式息息相關。

你都用vue了,構建工具有啥好說的,不用webpack還有其他更好的?

沒錯,用vue開發項目,webpack絕對是首選,各種loader幫助你簡化你的代碼,還有其他各種分開打包的方案、js懶加載等幫助你優化頁面的加載速度。

但是,第二個尷尬的情況又出現了,部門內部的所有項目都是基於fis3構建的,也就意味着所有測試機器和正式及其上都是fis3的這套構建系統,你在本地用webpack打包是沒法發布的。

考慮到遷移成本,部門短期內是不可能從fis3遷移到webpack了,這也是沒辦法的事情。因此,要想舒服地使用vue就得想其他辦法。如果仔細看過文檔的話,其實vue是可以直接引入js文件來進行非構建開發的,需要做的事情就是引入vue的js文件然后用Vue對象初始化應用就行了,非常簡單。但是如果沒有構建過程的話,就意味着我們在寫vue組件的時候需要寫難讀的字符串模板或者更難讀的render函數,而沒法寫像用webpack時的單文件組件

這個顯然是不能忍的。寫過或者讀過webpack的loader的同學應該都清楚,loader其實就是一個處理字符串的函數,並不是啥黑科技,之所以webpack加了vue-loader之后能使用單文件組件,是因為vue-loader會將輸入的.vue文件當成字符串,然后根據<template>,<style><script>標簽來將對應的內容編譯成對應的文件類型,再交給下游的構建插件處理。vue-loader的工作原理大致如下:

 

所謂單文件組件就是允許開發者在開發階段將html模板jscss寫在一個文件里,然后配置了vue-loader的webpack會幫你做接下來的一切。簡單來說就是拆分 + 注冊組件

那么在fis的構建過程中,准確的來說是線上機器的構建流程中,是沒有vue-loader的。因為就算我們在本地自己寫一個fis3的插件,實現類似vue-loader的字符串編譯功能,線上機器也沒有裝,所以寫在一個文件里這個願望算是落空了。那么退一步來想想,其實寫在三個文件里也可以接受,把他們放在一個文件夾里,文件夾以組件名命名,雖然跟單組件相比挫了一點,但是也算是(強行)單文件夾組件了,對代碼組織還是有好處的。我們用的fis3中有__inline()功能可以將html作為字符串引入js,這樣就可以將模板脫離組件邏輯js文件編寫,這樣就能像單文件組件那樣講模板和邏輯以及樣式分開來寫,又在觸手可及的地方。

 

代碼組織

雖然不能寫單文件組件,但我們可以把單個組件的htmljscss分開寫並且放在一個文件夾里。由於目前剛將項目遷移到vue,代碼組織方面應該后續還有很多需要優化的地方。目前的代碼結構為:

根目錄下有一個src文件夾和一個fis-conf.js文件。src文件夾中裝項目代碼,fis-conf.js即fis3的配置文件,根據項目情況配置即可。下面主要來看看src下其他文件的結構。

 

components & containers & plugins

之所把這三個部分放一起是因為他們都屬於vue組件的范疇,顧名思義,components=>組件,containers=>容器,plugins=>插件。這三個文件夾里的結構都是一樣的,下面以component為例:

假如我們有個叫some-component的組件,我們將這個組件的html,js,css文件放在some-component文件夾中。其中,index.html中寫組件的模板,index.less中寫組件的樣式,index.js中寫組件的邏輯,最后,我們會在初始化整個應用的地方,即之后會提到的/src/js/app/app.js中注冊組件。

 

components,containers,plugins的區別

在應用中,我將vue的組件分成了三類,分別是組件、容器組件和插件。

容器組件:用於路由初始化的外層組件。有點像react-redux技術棧中跟store鏈接的那部分組件,不同的是這里並不是按數據層的流向來區分組件,而是通過頁面的結構來分的。容器組件僅用於vue-router<router-view>所替換的組件,容器組件中包含的組件都輸入普通組件。

組件:除了容器組件的通過html標簽調用的其他組件。除了容器組件,其他同過html標簽調用的組件都輸入普通組件。普通組件又有點像react-redux中的高階組件。在react中,高階組件被建議作為pure render(純渲染)組件使用,也就是只根據父組件(容器組件)傳入的props來渲染視圖,而沒有本身的狀態。但是這里同樣,暫時不討論數據層,普通組件只是單純的作為容器組件的子組件使用。至於組件內部是否需要保留內部的狀態,之后再討論。

 

插件:需要在js中調用的組件。使用vue或者react這樣的數據驅動視圖的庫,如果只是使用html標簽來將組件寫到html中,再通過修改數據來更新視圖,在大多數情況下是可行的,但是有些場景下也很有局限,比如說需要一個開發一個alert組件。調用alert明顯是應該在js中來進行的,因此vue又多了一種叫做插件的組件。

 

組件(component, container)寫法

 

對於組件,不管是普通的組件還是容器組件,寫法都是一樣的,只是注冊的方式存在差異。 首先,我們可以先寫好組件的html模板:

 

// some-component/index.html
<div class="some-component">
    ...
</div>

之后,我們可以開始寫組件的邏輯:

 

// some-component/index.js
nie.define('someComponent', function() {
    var component = {
        template: __inline('./index/html'),  // 引入剛剛的html模板
        data() {
            return {
                state: $store.state,     // 綁定全局視圖狀態
                ...                      // 組件內部狀態
            }
        },
        created() {
            ...
        },
        ...
    };
    return component;
});

在不動fis配置文件的情況下,沒辦法使用commonjs或者es6的模塊化方案,因此這里直接使用nie的類requirejs的模塊化方案來定義我們的vue組件,反正開篇也提到了短期內是甩不掉基於jQuery的nie庫了┑( ̄Д  ̄)┍,倒不如利用起來。

 

之后就是寫組件的樣式了,沒什么好說的,只用注意下沒有webpack的loader給組件的class加上命名空間,我們自己需要注意不要有全局的相同的class。我的方法是每個組件的外層都把class寫為組件名,組件內部的樣式都在這個class的包裝下,這樣只要組件名不重復,樣式也不會有沖突:

// some-component/index.less
.some-component {
    ...
}

插件(plugin)寫法

 

插件的寫法稍微復雜一點,在返回的對象中我們需要手動添加一個install方法,在之后的注冊過程中,Vue會調用這個方法,並且將Vue對象當做第一個參數傳入這個方法:

 

nie.define('Alert', function() {
    var Alert = {};
    Alert.install = function(Vue, options) {
        Vue.prototype.$alert = {
            show: function() {
            },
            hide: function(callback) {
            }
        };
    };
    return Alert;
});

普通組件(component)注冊和使用

 

在初始化vue應用的app.js中,注冊組件:

 

// /src/js/app/app.js
// 引入組件的js文件
__inline('/src/components/some-component/index.js');

// 加載之前定義的nie組件
var someComponent = nie.require('someComponent');

// 注冊全局組件
var components = {
    'some-component': someComponent,
    // ...
};
for (var key in components) {
    Vue.component(key, components[key]);    // 注冊全局組件
}

注冊之后,就能使用some-component標簽在html中調用組件了。

 

// another-component/index.html
<div class="another-component">
    <some-component></some-component>
</div>

容器組件(container)注冊和使用

 

由於容器組件是替代vue-router的<router-view>元素的組件,因此,容器組件的注冊是在vue-router初始化的時候來進行的。同樣在app.js中:

// /src/js/app/app.js
// 引入容器組件的js文件
__inline('/src/containers/Main/index.js');

// 加載之前定義的nie組件
var Main = nie.require('Main');

// 路由對象
var router = new VueRouter({
    routes: [
        // ...
        {path: '/main', component: Main},
        // ...
    ]
});

// 初始化vue應用
new Vue({
    el: '#app',
    router: router, // 注冊路由,路由組件也就跟着一起注冊了
    // ...
})

接下來,在路由視圖切換的地方,注冊過的路由組件就會根據路由匹配來自動替換<router-view>元素。

 

插件(plugin)注冊和使用

 

插件的js中,調用了Vue.install()方法,接下來在app.js中:

 

// /src/js/app/app.js
// 引入插件的js文件
__inline('/src/plugins/Alert/index.js');

// 加載之前定義的nie組件
var Alert = nie.require('Alert');

// 注冊插件
var plugins = [
    Alert,
    // ...
];
plugins.forEach(function(plugin) {
    Vue.use(plugin);
});

注冊之后,在所有的組件實例對象上面都會多一個$alert對象,其中包含showhide方法,比如在一個對象中:

 

// some-component/index.js

var component = {
    // ...
    methods: {
        alert() {
            this.$alert.show(); // 調用Alert插件
        }
    }
}

css

css文件夾下會存放一個index.less的應用入口的樣式文件,其中引入了各個組件的樣式:

 

// /src/css/index.less
// 容器組件
@import "../containers/Main/index.less";
// 組件
@import "../components/some-component/index.less";
// 插件
@import "../plugins/Alert/index.less";

還可以根據項目需求放一些其他的樣式文件。

 

imgs

 

imgs文件夾放項目中用到的圖片資源。

 

inline

 

inline文件夾放需要編輯填寫內容的html文件,再通過fis的inline引入到其他的html中,方便編輯找到對應的內容位置。

 

js

 

js文件夾結構如下:

 

 

 

其中,app/app.js是我們應用的入口文件,即/src/index.html中直接引入的js,在其中進行了注冊組件、注冊插件、初始化路由、初始化vue應用等過程。

 

common文件夾下的js文件每一個都對應一個全局對象,對象中有某些具體的方法可以隨時供調用,這個結構可以根據個人的不同習慣來組織,我的結構是:

 

api.js:存放所有與后台交互的接口。在組件邏輯中通過$api.someApi(params, successCallback, errorCallback)調用。

 

common.js:工具方法。通過$common.someFunc()調用。

 

store.js:簡版vuex的store對象,存放全局視圖狀態,也就是前面綁定在組件中的$store.state。數據流部分稍后會提到。

 

關於數據流

 

用數據驅動視圖的框架,都存在着視圖所依賴的那部分數據。由於在應用中,有不同的視圖可能會依賴於同一份數據,也有多份數據可能被同一視圖依賴。相反,可能會有很多地方會更新同一份數據,也可能在同一個地方更新多份數據。這些數據被稱為視圖的狀態,放在全局的叫做全局視圖狀態。由於存在上面提到的映射關系,在視圖狀態復雜到一定程度的時候,維護起來就會是噩夢。為此,就有了下面的數據流工具。

 

redux

 

redux當初是使用react開發單頁應用時認可度比較高的數據流方案。redux推崇絕對的數據流單向數據不可變(immutable)。react的組件也根據redux衍生出了容器組件高階組件兩種。容器組件用於與redux的store相連,store將state通過props傳給容器組件,容器組件內部也有非全局的state。其他位於容器組件內部的組件在最佳實踐中,都被認為最好寫為高階組件,也就是純渲染組件(pure render)。純渲染組件根據容器組件傳入的props來渲染視圖,沒有內部state。這樣加上redux本身的reducer immutable地去改變state的過程,可以提高react組件的渲染性能。

 

上面一大段話看起來就感覺很復雜,與其說復雜,不如說是繁瑣。因為redux為了保證在復雜項目中數據流的可控性與可追蹤性,預先規定了非常多的條條框框,在寫代碼的時候顯得比較繁瑣,換來的是大型項目中數據層的可維護性。

 

所以我認為,在項目的數據層不復雜到一定程度,使用redux是不划算的。順便附上redux作者的一篇文章You Might Not Need Redux

vuex

 

vuex是vue作者開發的適用於vue的數據流工具。其大致思想跟Redux相近,但在API調用和數據流動方式方面還是有一點區別。例如,vuex中,想要改變state的值需要調用store.dispatch('some action')來調用action,這個actionRedux中的action概念差不多,可以進行異步操作,然后在action中來調用store.commit('some mutation')觸發mutationmutationreduxreducer相似,對state直接進行操作,不能做異步操作。(從vuex-v2.0.0開始vuex外部也能調用store.commit()來調用mutation)。

 

使用vuex的話,可以不用像redux那樣,變更和接收變更只能以容器組件為橋梁,而是可以從任何組件dispatch一個action,從而改變全局state,然后依賴全局state的組件的對應視圖自動更新。這種架構在沒那么復雜的項目中比較好用,我們可以跟蹤數據流的變化,也具有一定的可維護性。我個人是覺得不局限於redux的實踐寫起來更舒服一點,不過等應用復雜到一定程度的時候,數據的可維護性也會降低。因為那時候數據的改變邏輯會分布在各個組件的內部,而不是只有容器組件會觸發,維護起來會很麻煩。

 

所以,如果應用比較復雜的話,我們可以使用vuex實現redux的最佳實踐,也就是我之前提到的容器組件和普通組件。容器組件就用來跟vuex的store打交道,而內部的普通組件通過容器組件傳入的props做純渲染組件。這樣就能通過vuex實現redux對於復雜項目的最佳實踐。

 

目前項目中使用的數據流工具

 

說了半天,其實現在在項目中並沒有使用任何的數據流工具,原因在文章開頭也提到了,就是目前我們做的項目數據層並不復雜,甚至可以說是非常簡單。所以我認為,在目前的數據復雜度下,沒必要用開發效率去換可維護行,尤其是目前大多數的項目都屬於不需要長期維護的專題類型。

 

所以就有了在之前代碼組織一節中提到的一個全局store,在每個組件中都將$store.state綁定到了data上,這樣$store.state就成了全局視圖狀態。在應用的任何地方都可以對$store.state進行更新,更新之后,依賴於該state的其他視圖也會相應地更新。目前的方案對於目前的復雜度完全夠用,寫起來也不會影響效率。

 

從jQuery遷移到vue的意義

 

有利於團隊協作,多端維護

 

開篇提到,想往vue遷移的根本原因還是開發復雜項目需要團隊協作的時候,如果沒有一套約定好的項目架構,那么在多人協作或者是跨端維護的時候就會非常痛苦。

 

有了vue之后,可以將能復用的組件的模板、樣式和邏輯進行封裝,供今后使用。因為vue提供了一套組件的生命周期鈎子,所以我們在寫組件邏輯的時候,怎么都不會跳出生命周期的范疇,就沒有了用jQuery時,10個人就有10種寫法的問題。

 

另外,我們還引入了vue-router來用作單頁的路由方案。沒有這個之前,同樣,專題結構參差不齊,有的是自己實現的單頁,有的是偽單頁,有用第三方路由庫的,有的甚至沒有做成單頁。面對這樣的問題,如果只是一個人開發還好,只用按照自己的習慣來就好,但是一旦需要多人協作或者之后維護,問題就來了。所以有了vue-router之后,單頁的方案統一了,不同的項目用一套單頁方案,不管是團隊協作,還是今后的維護,都是一目了然的。

 

關於數據流工具,上面已經詳細分析了,我的意見是,根據跟視圖有關聯的數據的復雜度,從低到高,簡版全局store > vuex > redux

 

這樣,對於項目整體,從mvvm的角度來說,m(數據model)層統一了方案,v(視圖view)層統一了方案,前端路由也統一了方案,剩下vm(視圖模型viewmodel,也就是組件的狀態)層在項目不復雜的時候可以自由發揮,也就是不用太多地去考慮組件內部是否應該維護一份本地的狀態,如果一旦項目達到了一定的復雜度,我們可以考慮實踐redux推崇的容器組件和純渲染組件的思想。

 

因此,我認為通過這樣在技術方案上的統一,能夠大大降低團隊協作和后期維護的成本。

 

有利於提高開發效率

 

對於需要渲染后台傳過來的數據的項目:

 

用vue之前,我們從后台拿到數據,然后對數據進行一定程度上的處理,然后把處理后的數據轉換成渲染邏輯,手動操作DOM更新

 

 

 

用vue之后,我們從后台拿到數據后,把數據進行處理之后,直接交給數據層,vue會自動對比變更前后的數據,自動更新DOM

 

 

 

所以在開發效率上的提升主要體現在用vue之后,我們不用考慮渲染邏輯,不用手動處理DOM,這部分事情vue幫我們做了,我們只用把心思放在業務邏輯上即可。我認為這也是我們做數據類項目應該有的思路。當然,純展示類、秀動畫類項目除外。

有利於提高渲染性能

 

項目中經常會有這樣的應用場景:從后台拿到一個數組,把這個數組渲染成一個長列表。在用戶的交互過程中,用戶可以根據輸入具體搜索條件進行篩選或者排序。在這種應用場景下,除了上面提到的只用對數據進行操作而不用管渲染邏輯的好處之外,vue還會對列表的每個元素進行緩存並跟新的數據進行比對(如果給v-for中的每個元素設置了key的話),進行差量更新,從而提高渲染性能。

 

vue的缺點

 

動畫

 

就算是在偏數據類型的項目中,也可能會存在強動畫需求,尤其是游戲部門的項目。那么相比而言,用vue來寫動畫就沒有用jQuery那么直觀了。究其原因,我認為動畫的思路跟業務是相反的。業務的核心是數據,因此我們的編寫業務代碼的時候,中心應該放在數據的處理上,剩下的比如說更新DOM就交給框架來好了,但是在編寫動畫的時候,動畫的主體是DOM,因此如果我們要通過改變數據再去控制動畫就顯得有些怪了。

 

vue動畫:

jQuery動畫:

 

但是雖然思路跟以前是不太一樣,但是jQuery能實現的動畫vue都可以實現,因此也不算是個大問題,只是在寫動畫的時候可能會比jQuery直接操作DOM要多走幾步路而已。

 

還有待提高的地方

 

組件js文件引入方式

在之前代碼組織部分詳細說過的組件的編寫方式里,需要將組件的js文件用fis3的__inline()方法引入到入口文件app.js中,然后再通過nie.require()方法加載組件對象,最后再用該對象來注冊組件。整個過程用下來感覺稍繁瑣,因為這是在沒法用ES6的module模塊的情況下的妥協方案。組內同時研究發現現在fis3的babel插件再跟其他插件組合工作的時候還存在沖突,等這個沖突解決之后,用上ES6的模塊化方案之后,代碼會更加簡潔。

使用vue + jQuery需要注意的地方

 

注意兩種技術更新DOM的方式

使用jQuery,我們是直接操作DOM,DOM的更新是同步的。使用vue,想更新DOM,我們直接操作的是數據,由於vue會對比新舊數據避免頻繁更新,vue會將更新DOM的操作放在下一個事件循環(event loop)里,也就是說DOM的更新是異步的。

 

前面說了,我們現在有個非常尷尬的狀況就是不得不同時使用兩種技術,因此,在開發的時候需要注意兩種DOM更新的方式,避免該取DOM的時候DOM還沒有更新的情況。

 

注意vue組件生命周期 + vue-router路由鈎子

 

vue組件的生命周期是針對組件實例在從初始化到銷毀的整個過程而言的,跟DOM沒有直接關系,因此,跟jQuery一起用,有時候會需要訪問DOM的時候要尤其小心,有可能在某些生命周期鈎子中DOM還沒有被渲染出來。

 

vue-router也有路由鈎子函數,在使用的時候也需要注意在相應鈎子中能不能獲取到DOM的問題,尤其是容器組件帶過渡的情況。

 

Reference

 

  1. Vue
  2. vue-router
  3. fis3
  4. You Might Not Need Redux
  5. vuex
  6. vux
  7. Element

 

本文來自網易雲社區,經作者查馬糾西授權發布。

原文:jQuery到Vue的遷移之路

更多網易研發、產品、運營經驗分享請訪問網易雲社區


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM