原文:离职后,对项目的记录、总结(你的 star 是对我写作的「正反馈」)。
一、写这篇文章的目的
出于对自身职业发展上的规划,我决定换一个工作环境,离开工作了三年的公司。
我并没有急着找下家,而是决定裸辞。希望利用这段空档期去总结过去三年的工作,并展望未来自身的发展。
过去的一年,我以「前端 Leader」的身份参与了一个交通出行项目的开发并上线了多款 APP 。虽然包括我在内前端只有三名开发人员,但在整个过程中,不管是在技术上还是团队管理上,我都有很大的收获。也是这一年管理团队的经验让我养成了一个好习惯:「记录、总结」。写这篇文章的目的就是为了记录这个项目架构的过程并总结经验。
二、项目以及团队背景
这是公司转型的第一个项目,在敲定了项目技术选型之后我才被分配到了这个项目组,在此之前,我一直负责 WEB 端的开发,并没有移动开发的经验。
项目及团队背景大致如下:
-
我们需要开发一个公交出行 APP ,主要功能包括查看实时公交信息、离线扫码上车、路径规划、在线购票和云公交卡等功能,复杂度并不低。
-
我们后续需要为多个城市开发功能上大同小异的 APP(在我离开公司时,已成功上线了衢州、贺州、舟山、天津和济源这五所城市对应的 APP)。
-
为了快速抢夺市场,我们决定使用 Ionic 1.x 进行开发,是一个 Hybrid APP,在后期还需要开发对应的小程序。
-
我带着两个应届的妹子负责前端的开发,另外还有三个后端同事和一个产品经理(老板)。
起初,我们只是为衢州开发 APP 。由于时间非常赶,加上之前我并没有足够的项目架构和带团队的经验,我们只是对 Ionic 脚手架提供的目录结构做了简单的修改后就进行了开发。
但随着衢州 APP 的上线,我们陆续接到了贺州、天津等多个城市的开发任务。考虑到这几个城市对应的产品在功能层面上的大同小异,我们决定将这几个各城市的产品都集成到同一个项目目录中,并对当时的项目进行一次幅度较大的「重构」。
下面就来大致讲讲我们做了那些事儿(由于是以记录为主,并不会讲的特别的细致)。
三、拥抱 ES6: 提高开发体验
在新的项目中,我们决定使用 ES6 进行开发。一是考虑到后期还要开发对应的小程序,我们希望将来的小程序能够和当前的项目共用同一份核心代码(因为小程序也是使用 ES6 进行开发)。其次是 ES6 的诸多特性,如:class、箭头函数、各个数据类型的扩展等等能够极大的提高开发效率和体验,我个人非常喜欢。
在试图拥抱 ES6 的过程中,需要解决以下两个问题:
-
搭建 ES6 的开发环境。
-
思考 AngularJS 与 ES6 的最佳实践。
对于第一点,由于我业余时间都有在关注业界主流框架的发展,在两年前也有使用 VueJS 1.x 开发过一个完整的 博客项目,当时就是使用 ES6 进行开发,所以很快就使用 webpack 搭建了 ES6 的开发环境。
而对于 AngularJS 与 ES6 的最佳实践,我在网上查了许多的资料,发现了几篇优秀的博文,例如:ES6 与 Angular 1.x 和 Angular 1.x 和 ES6 的结合。项目中主要的调整就是基于 ES6 来改写 AngularJS 中的那些概念,至于具体怎么改写,感兴趣的可以阅读上述两篇博文。
四、项目分层:抽象业务模型
在重构之前的项目中,由于我个人对 MVC 的错误理解,我们将大量的业务逻辑都放在控制器中,导致控制层极度臃肿,代码的质量变的越来越差。
在我重新理解了 MVC 之后,借着重构的机会,我们试图通过抽象出核心的「业务模型」,对控制层进行「瘦身」。
对于业务模型的抽象,我认为需要清楚两点:
-
抽象业务模型的初衷不是为了复用,而是为了方便管理。
-
希望业务模型中的代码能够尽可能的隔离框架,最好都是「纯」JS 。
其中,第一点我在 深入理解 MVC 中的 M 与 C 中有提及,在此不再赘述。
对于第二点,由于项目后续可能会迁移到 Ionic 3.x 或是 React Native ,隔离框架的业务模型会提高项目在技术栈层面的「灵活性」,可以减少迁移到其他技术栈的成本。
接着,我们就需要解决一个问题:在业务模型中,如何尽可能的隔离 AngularJS ?
在抽象业务模型的过程中,我们发现当中的代码主要是依赖了 AngularJS 的服务,其中包括 AngularJS 内置的服务、第三方库的服务和项目自身的服务。对此,我们分别采取了以下几种做法:
-
替换掉 AngularJS 的内置服务。比如用 axios 替换 $http 、Promise 替换 $q 等等。
-
如果业务模型的某些行为所依赖的服务很难被替换,就创建一个继承该模型的服务,然后在该服务中重写某些行为。
第一种做法很好理解,对于第二种做法,我来举个例子。
假如:业务模型 A 中的行为 X 需要使用到 UI-Route 服务的 $state.go( ) 方法(路由跳转的方法)。
基本思路:我们将核心逻辑依旧封装在
a.model.js
中,然后创建一个继承于a.model.js
的a.service.js
,并在其中重写行为 X 。
伪码如下:
/* a.model.js */
export class A {
behaviorX(cb) {
// do some logic
new Promise((resolve, reject) => {
cb();
});
}
}
/* a.service.js */
import { A } from 'app/models';
class ServiceA extends A {
construct($state) {
Object.assign(this, { $state });
}
behaviorX() {
super.behaviorX(() => {
this.$state.go('routeX');
});
}
}
ServiceA.$inject = ['$state'];
export ServiceA;
/* angular.service.js */
import { ServiceA } from 'app/services';
angular.module('app.services', [])
.service('serviceA', ServiceA);
复制代码
如此一来我们就能够保证了业务模型的「纯度」。
小结:抽象业务模型一是为了对控制层进行瘦身,提高代码质量,其次通过在业务模型中尽可能的隔离框架来较少项目对框架的耦合度,提高项目在技术栈层面的灵活度。
效果:由于团队整体的开发能力并不高,我们并没有很好的抽象出项目的业务模型,反而是增加了代码的复杂度,我们也就不对这部分做强制的要求了,但之后我还会继续实践。
五、抽象功能模块:实现功能的「即插即用」
前面提有到,我们的项目其实有一点特别:我们需要为不同的城市分别上线功能上大同小异的 APP ,其中有些城市的功能可能多点,有些可能少点。
考虑到代码复用的最大化,当城市 A 需要增加城市 B 的某一个功能时,我们希望这两个城市能够共用该功能的代码。对于两个城市在同一功能上的差异则通过「继承、重写」来解决。简单的说,我们需要实现项目在功能层面的「即插即用」。
为了解决这个问题,我们首先要对「功能模块」有自己的理解。
如果一个问题是由多个子问题组合而成,那么这个问题的复杂度将大于分别考虑每个子问题时的复杂度之和。
基于这个结论,开发者乐于将一个复杂的应用拆分成多个功能模块。这样做有以下两点好处:
-
利于协同开发,每一个开发者只需专注于自己所负责的模块。
-
由于每一个功能模块间的低耦合度,当我们不需要某一个功能或是增加某一功能时,我们只需删除或者增加该功能模块的声明代码。
其实这里的第二点就和我们项目的需求有点相近了。接下来就说说我们具体是怎么做的。
不同的开发者基于不同的项目,对功能模块的应用都会有些许的差异,而这里的差异主要体现在抽象功能模块的粒度上。
在我们的项目中,我们抽象出来的功能模块的粒度都比较大,每一个功能模块都对应着一个「大需求」,模块内部同时也会包含着多个「小需求」,但我们并没有继续进行拆分,因为我们发现这些小需求对所在功能模块的耦合度极高。
其次,我们将功能模块反应在了项目的目录结构中,所有的功能模块都放在一个名为 module
的文件中,每一个功能模块主要包含以下几部分的代码:
-
模块下的各个视图,包括视图路由的定义、视图对应的控制器、视图的模板等。
-
模块下所有视图有用到的私有的服务、组件等代码。
这里要提一句,我们对服务、组件和指令等元素(方便起见,我将这些文件类型同称为元素)的管理。从能力上来讲,这些元素是适用于全局的(只要声明了,整个项目都能调用),但是我还是决定根据它们的「通用程度」划分成「全局元素」和「私有元素」。这样子做的好处有两点:
-
方便管理。别的同事可不一定知道那个是全局组件那个是私有组件。虽然这可以通过命名规范来区分,但是随着元素越来越多,管理起来还是比较费劲的。
-
方便按需加载。为了提高 APP 的首屏加载速度,我们实现了 AngularJS 的按需加载。过程中我们发现,我们老是需要在多个模块中去声明那些全局元素。而当我们划分了全局元素和私有元素后,我们会在项目启动时就去声明全局元素,在模块中则只需声明对应的私有元素即可。
通过以上的描述可以看出,我们将所有理应属于功能模块本身的代码都放在对应的文件夹内。当我们需要使用某一功能时,我们只需要在项目的入口文件引入对应的文件,并初始化对应的 AngularJS 模块。
如此一来,我们就便几乎实现了项目中各个产品(APP)在功能层面的即插即用。
六、调整目录结构:适应一个项目多个产品的场景
我们当时选择重构项目最主要的目的就是整合多个城市的产品到同一个的项目中,而项目的目录结构则是其中的关键。
上图是我们项目大致的目录结构。其中 ionic
目录下包括了各个产品对应的 ionic
项目,这些项目都是通过 ionic 1.x
提供的脚手架创建的。而 scripts
目录则是我们进行代码开发的目录,我们通过 webpack
将 scripts
下的代码打包到对应的 ionic
项目中。这里,我主要谈谈 scripts
的目录结构。
基于我们对「功能模块」的理解,我们将代码分成了 core
和 projects
两部分。
-
core 目录:用于存放通用的功能模块以及这些功能模块有使用到的业务模型、AngularJS 元素和一些工具类。
-
projects 目录:该目录下的每一个文件夹都代表一个产品,这些文件夹中包含了产品的「入口文件」以及一些产品的订制的业务代码。
简单的说,这样划分目录结构的目的是:当我们在开发一个产品的某一功能时,我只需考虑该功能是不是在 core
中已经被开发过了,如果是,那就直接 import
到当前产品的入口文件。如果不是,则考虑这个功能是否为一个通用功能,如果是,则把该功能的代码写在 core
目录下,否则写在对应的产品目录下。当然,还有很大的可能是:该功能在 core
中已经被开发过,但当前的这个产品和 core
的实现会有些许的「差异」,我们的做法是:继承对应的功能模块,然后重写那些「差异」,通过这种方法来尽可能的减少那些冗余的代码。对应的流程图如下:
七、优化:提高产品性能及开发效率
由于人手不够以及各个产品都希望尽快的上线,直到我离职时我们也没有花太多的精力去考虑项目的优化以及产品的性能。这可能也是小公司在项目初期的无奈吧。
但在重构阶段,我们还是做了些许优化,大致如下:
7.1 AngularJS 的按需加载
为了提高 APP 首屏加载的速度,我们基于 webpack
、ui-route
和 ocLazyLoad 实现了 AnuglarJS 的按需加载。
/* pageA.route.js */
function routeConfig($stateProvider) {
$stateProvider.state('page-a', {
url: '/page-a',
// 按需加载视图模板
templateProvider: ['$q', ($q) => {
return $q((resolve) => {
require.ensure([], () => {
resolve(require('./pageA.tmpl.html'));
}, 'page-a');
});
}],
// 按需加载当前视图所依赖的模块
resolve: {
loadModule: ['$q', '$ocLazyLoad', ($q, $ocLazyLoad) => {
return $q((resolve) => {
require.ensure([], () => {
$ocLazyLoad.load({ name: require('./index').default.name });
resolve();
}, 'page-a');
});
}]
}
})
}
export default angular.module('app.routes.pageA', [])
.config(routeConfig);
/* index.js */
import PageAController from './pageA.controller';
import PageAService from './pageA.service';
export default angular.module('app.pages.pageA', [])
.service('PageAService', PageaService)
.controller('PageAController', PageAController);
复制代码
7.2 各个产品的自动化构建
由于我们的项目中包含了多个产品,因此我们需要一个高效的自动化构建版本的方式。
产品的构建主要分为两部分:
-
打包
scripts
目录下的 JS 代码到对应的ionic
目录中。 -
在
ionic
文件夹下的对应产品目录下执行:ionic build android && ionic build ios
。
整个自动化构建过程,我们使用了 webpack
和 gulp
来实现。
7.3 项目开发的规范化
不知是因为团队规模小还是其他的什么原因,我们公司一直没有一些硬性的开发规范来约束项目的代码。这使得我在重构同事代码的时候感到各种的「不适」。因此,我决定让同事阅读以下几个开发规范,并在以后的开发中尽可能的遵守。
-
Javascript 开发规范:Google JavaScript Style Guide
-
AngularJS 开发规范:angular-styleguide
-
CSS 命名规范:BEM 命名规范
在具体的实施当中,我们使用 ESLint 对 JS
代码做了强制的约束,而其他的规范主要还是看开发者的自觉性。从后来的代码来看,代码的质量还是有明显的提高,没有以前那般杂乱了。
八、感悟
这是我第一次以前端 Leader 的身份来参与一个新项目的开发,见证了一个产品的从无到有,虽然各种加班(有一次竟加班到凌晨四点~),但过程中的确学习了蛮多的。除此之外,也意识到了自身的许多不足。
这里,我主要有以下两点「大」的感悟:
8.1 职业发展:方向以及Foundation(基本功)
在这个项目中,我所参与的工作其实并不只是「WEB 前端」而已。 由于公司当时并没有 iOS 端和 Android 端的开发者,虽然项目是一个 Hybrid App ,但过程中还是需要写蛮多 Native 的代码。
其中,我花了很多的时间学习在 iOS 开发。在整个 iOS 的学习过程中,我有了下面两点感悟:
-
良好英文水平是成为一个优秀的开发者的前提。
-
要成为一名优秀的「软件工程师」而不只是一名「WEB 前端工程师」。
众所周知,要成为「卓越」,就必须有非常扎实的「Foundation
」。与上述的感悟就对应了两个 Foundation
:
-
英文能力:听、说、读和写。
-
软件开发基础:包括数据结构、操作系统、计算机组成原理等等。
这可能有点「后知后觉」的感觉,因为这些都是以前读书时该学的东西。不过不管怎么样,还是很庆幸自己找到了努力的方向。有了明确的方向,相信今后的路会变的愈加有趣。
8.2 许多的不足:路还很长
有时,对比那些野球场上打球的朋友和那些打职业比赛的球员,后者会显得「职业」很多。
但是,虽说我现在所做的工作是我的「职业」,但我时常感觉自己并不够「职业」,显然非常的「业余」。
这段时间我总结了下,发现两点问题:
-
技术上,「踏实度」很低,容易产生「技术浮躁」,缺少对技术的「匠心」,基本功薄弱。
-
态度上,很难长期保持对公司、对产品的「责任感」。
对于前一点,其本质就是:「浮躁」。最好的解决办法就是:瞄准方向,然后把眼光放长远一点。我们总会感叹要学的东西是在太多了,但如果我们把眼光放长远一点,五年、十年、又或是二十年,或许我们就没有那么浮躁了。我认为浮躁的背后是一种急于求成,一种急功近利的表现,这样的状态显然很难让我们变的踏实。
而对于第二点,这就涉及到了自身对工作和对职业的「态度」了。我认为这是一个基本的「职业素养」,又或者可以理解为「职业原则」。就我个人而言,我的想法是:
在成为一名优秀的软件工程师之前,我也希望,我是一名「优秀的员工」,能够发挥自己最大的价值,仅此而已。