背景
在前段时间做了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
模板、js和css写在一个文件里,然后配置了vue-loader
的webpack
会帮你做接下来的一切。简单来说就是拆分 + 注册组件。
那么在fis
的构建过程中,准确的来说是线上机器的构建流程中,是没有vue-loader
的。因为就算我们在本地自己写一个fis3
的插件,实现类似vue-loader
的字符串编译功能,线上机器也没有装,所以写在一个文件里这个愿望算是落空了。那么退一步来想想,其实写在三个文件里也可以接受,把他们放在一个文件夹里,文件夹以组件名命名,虽然跟单组件相比挫了一点,但是也算是(强行)单文件夹组件了,对代码组织还是有好处的。我们用的fis3
中有__inline()
功能可以将html
作为字符串引入js
,这样就可以将模板脱离组件逻辑js
文件编写,这样就能像单文件组件那样讲模板和逻辑以及样式分开来写,又在触手可及的地方。
代码组织
虽然不能写单文件组件,但我们可以把单个组件的html、js
和css
分开写并且放在一个文件夹里。由于目前刚将项目迁移到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
对象,其中包含show
和hide
方法,比如在一个对象中:
// 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
,这个action
跟Redux
中的action
概念差不多,可以进行异步操作,然后在action
中来调用store.commit('some mutation')
触发mutation
,mutation
跟redux
的reducer
相似,对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
Vue
vue-router
fis3
You Might Not Need Redux
vuex
vux
Element