前端项目重构总结(1) - 前世由来

背景

2017年4月公司组织结构变动,这个祖传的代码要交接到我这里,因为是公司最赚钱的产品之一,我怀着忐忑和无比敬畏的心情就开始了我的交接之旅,真的是没想到,惊喜成为了惊吓。

首先在前端无比飞速发展的今天,你能想到吗?我们的项目还存在于后端的java工程中,这意味着我修改一行任意的js、css或者html都是要后端同学,重新编译打包,重新发布,这个过程大概要持续半个小时。

该项目是从2011年公司成立的时候就在做的,那个时候jQuery还是很流行的,后来angularjs的出现,让这些开发者也跃跃欲试,所以我们的项目可谓真的是大量的jQuery代码夹杂这少量的angularjs来组成。

最开始接手从简单的 issue 开始处理,每天都好像能获得一些代价与惊喜,我改了一行看似没什么副作用的代码,谁知道,另外一个地方就出错了,每天被各种bug缠绕,终于理解为什么这个项目之前迭代速度如此之慢?同时也不能够理解为什么如此重要的项目不好好的重构一番。

在2017年的12月底,我实在是受不了了,开发体验差的实在难以忍受,一直向上反馈,当时的想法是:接下来不重构还继续维护这个,我立马辞职。终于取得了一些时间和人力,可以去重构这个项目,兴奋之情溢于言表。

重构前的项目架构:

jQuery + AngularJs1.2.19 + gulp

项目存在问题:

  1. 前后端代码同一仓库,难以管理
  2. 代码组织杂乱,查找代码非常难,基本全局搜索
  3. 技术使用混乱,可能是当时angular不成熟
  4. 基本没有组件话
  5. 用户体验差,维护成本搞,开发效率慢
  6. 少有人愿意维护,新加入人员学习成本高
  7. 多个模块混在一起

重构目标

前后端分离,前端可单独测试、发布
拆分前端模块,将混在一起的5个模块分离
引入公司提供的组件,提高开发效率
减少维护成本,提高用户体验
去掉对jQuery技术的使用
编写开发文档,统一编码风格,降低新人加入成本

重构计划

代码按照模块进行拆分,每个模块独立打包发布,适配前端Jenkins系统(已完成,稳步运行于线上)
前端画布流程图的重构(已完成,稳步运行线上)
节点开发重构 (正在进行中)
页面主框架重构 (开发中)
新系统融合,性能优化 (未开始)
加入nodejs层,适配不合理的api接口(未开始)

重构人员

架构设计都是由我们的架构师同学来做,在这个过程中,我主要是写代码,然后负责和产品、开发经理、测试同学沟通需求,人力资源,上线计划,灰度发布后的调研,反馈,所以写这篇总结还是有些担忧,很怕有的地方会不到位。

前端项目重构总结(4)-节点重构

我们的线上跑着大大小小40个节点,有的单一节点代码量达到3k+行,实在是乱的令人发指,重复的代码随处可见,肉眼所及之处看到很多ctrl+c/v过来的代码,这种代码既不敢删除,也不敢随意改动,生怕会影响到其他地方。真正应了那句:祖传代码,请勿随意改动!

技术架构:

AngularJs1.x + es2015 + webpack

Why?

肯定会有同学鄙视,Angular都发布6了,你们还在使用1,真是low。只能说很无奈,我们也想过使用更新的、更流行的技术。

React

节点部分全部为表单设计,所做的事情就是添加表单,验证合法性,提交给后端保存数据。但是我对于 React 的认识比较浅显,认为它不适合做这样大批量的表单业务场景,因此放弃。

Vue

这几年 Vue 火的真是一塌糊涂,我也想在项目中去尝试一下,但是选择 Vue 代表着所有的组件全部得自己编写,或者在市面上流行的 Vue 组件再包一层,符合我们的UI风格。

Angular

当然最终还是选择了Angular1.x来进行开发。原因有以下几点:

  • 公司目前web端的系统都是Angular1.x来编写。
  • 使用它可以很快拉来新的开发,大家对于技术栈比较熟悉,上手快。
  • 目前公司提供的组件库是基于 Angular1.x而编写的,可直接使用,和其余模块UI完全一致。
  • 重构时间问题。

编码风格

不想再罗列这些文字,直接看链接:https://github.com/kuitos/kuitos.github.io/issues/34

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
src
nodes // 节点集合
xxx // 单一节点
index.js // 入口文件
body.url.html // 模板文件
footer.url.html // 所有节点均以弹框形式展现,此为弹框底部模板
index.scss // css
loader.js // 与后端交互的 api
Controller.js // 业务逻辑
mock.js // 模拟数据
    common  // 公共的一些服务之类,包含utils,interceptor,decorator等
components // 公共组件
styles // 基础的css文件
base.scss // 基本css,其中覆盖组件库的一些特定样式
mixin.scss // 带变量公共css
placeholder.scss // 不带变量的公共css
variables.scss // 项目中经常使用的字体、大小、颜色常量
config.js // app初始化配置
index.js // app入口
build // 打包配置
config.js // 配置打包输出
webpack.base.js // webpack基本配置
webpack.dev.js // webpack开发模式
webpack.prod.js // webpack生产环境
.editorconfig // 编辑器设置
.gitlab-ci.yml // gitlab ci 任务配置
package.json

代码Review

由于架构师同学的撤出,我们新加入了3个同学来帮忙进行重构。人多了,虽然项目中也设置了 eslint 校验,但是很多编码习惯是无法限制的,因此我也简单的制定了一个代码Review Check List,要求大家在提交代码之前做一下检查,避免一些不必要的时间浪费。毕竟组织这些人在一起进行 code review 还是比较花费时间的,因此大家需要一些统一的标准(check list 后面一篇文档单独贴出来)。

分享

重构的工作量是巨大的,繁杂的,时间久了难免会让人产生厌倦的心里,作为我重构的主导者,所以我制定了一个分享的机制,每周五下午由一个同学进行分享,时间控制在半个小时到一个小时之间,可以讲一个很小的点,让大家讨论起来,每个人在这一个小时之内放松一下,也有所收获。目前看起来执行的还不错,希望能够继续保持下去!

困难

  • 业务,还是对于业务的不熟悉,这是最大的问题。
  • api交互,接口的不合理性让人分分钟想骂街。

提高

  • 之前有4名同学一起开发,现在还有3个,目前还剩下10个节点所有都要开发完成啦。
  • 这次我们换了产品经理,她虽然也不熟悉,但是她会去学习,和听取建议啦。

时间

目前已经进行了3个月,正处于开发、测试、上线的过程中,尤其是等待测试的节点很多。等到彻底结束后再做总结。

结果

结果是未知的,自我感觉还是不错的,等待国庆结束后的第一波上线,接受用户的反馈结果。

前端项目重构总结(3)-画布流程图重构

为什么从这里开始?架构师同学问我的时候,我说从流程画布开始,因为这是我们最重要模块的主要核心。线上用户对于流程图的吐槽和需求是最多的,如果把它放到后面,只怕重构这个事情推动到最后,整个人会处于一个比较疲惫的状态,不会再有这么大的心劲。事实证明:这是我做的最正确,最刺激的一个决定。

架构设计是由我们架构师所做,我只是搬砖到这里。

老系统技术

JQuery + mxGraph

新架构设计

angularjs + gojs + mobx + mmlpx + typescript (按字母顺序排名)

目标

提供一个可复用的流程图组件,且组件结构自顶向下拆解后能保证每一部分独立且简单易用。

总览

用函数的方式表示,流程图组件架构大概如下:

1
const flowDiagram = workspace(createDiagram(flowchart(flowLoader(id))), createPalette(palette(repeatNodeGroup(paletteLoader()))))

组织架构

顶层 workspace 作为容器组件,与 mmlpx 组件建立数据关联,通过 props 将数据分发至下层的展示型组件。

展示型组件与容器组件之间的交互遵循 props down, events up 原则。

画布及节点调色板调用 canvas 库绘图(目前是 gojs ),理论上这一层需要做到可替换。

architecture.png | center | 594x459

mmlpx 架构

mmlpx.png | center | 598x690

重重困难

  • typescript + AngularJs1.x + mobx 全新技术组合方式的适应与开发。
  • 开发人力只有架构师同学加我,人力严重不足,开发工作量巨大。
  • 对于业务了解的不够深入,很多逻辑无从得知,只能翻看老代码,严重影响开发进度。
  • 后端api接口相当不合理,里面为了适配,做了层层的数据转换(其实这是可以预见的,前端这么烂,后端又能好到哪里去)。
  • 产品经理支持力度不够,对于不合理的设计还要保持线上,争吵无数。
  • 与老系统的融合,遇到了很多意想不到的坑,添加适配。
  • 灰度上线,部分用户不习惯新版本,要求回退(这对于我们打击不小)。可能和我们的2B的项目有关,很多用户已经习惯了老版本的东西,不愿意接受新的事物。

开发时间

2个人,从开发到上线,经历了3个月吧。

结果

在灰度发布上线之后,1000多家客户,有7家执意使用老版本,就因为小小功能的变更,这也充分说明用户习惯的问题,不要随便去更改一个用户已经完全习惯了的产品设计。收到这些反馈之后,我们又开始了二轮画布优化的开发,开发到上线差不多一个月,最终让客户完全接受了我们新版本的功能,也收到一些正向的反馈,实在是为之开心。

这是咨询我们的运营同学,在优化版本上线以后,有没有用户要求回退,或者有没有什么吐槽的点:

result.png | center | 393x306

是不是有一种开心的感觉?
得到这个讯息之后,我便可以安心的开始下一步的重构了。

前端项目重构总结(2)-前后端分离

模块拆分要点

  1. 静态资源 context path 调整。整合前端发布体系后,所有前端资源均在同一静态服务器目录下,所以单一系统的入口会变成 //xx.com/${systemName}/index.html,对于老系统需要手动替换静态资源路径,包括通过 css 引用的图片、img 标签引用的图片、ng-include 或 路由 引用的 html 等。开发期则需要在 express 中加入静态资源上下文,如 app.use("/${systemManagement}",express.static(path.join(__dirname, 'src')))。使用 es6 开发并借助 webpack 打包的系统则只需简单修改 output 配置即可。
  2. api 接口 context path 调整。新系统不采用老的各模块独立域名架构,如果后端 api 并未使用服务化方式开发(接口无系统前缀),则需前端在 ajax 中统一加入前缀(可以在拦截器中添加),并在前端静态资源服务器配置好前缀接口转发规则。
  3. 消除系统中在运行时通过判断 location 从而确认所处环境的方式。不同环境的不同代码版本在编译期确定。 webpack 借助 htmlWebpackPlugin,旧系统使用 gulp html replace 方式。
  4. 项目中拆出客户端 portal。顶部菜单组件化、portal 组件化,方便其他独立系统(如系统管理、账户管理)调用。遵循 纯组件 —> portal lib 渐进式设计原则(先提供纯 portal 组件,再提供相应的完整 lib,喊数据服务等)。
  5. 目录结构
  • 增加 src、mock 文件夹。 src 为源码目录(原始系统代码),mock 为接口 mock 目录(原始系统 routes 目录)。
  • src 中目录结构跟老版本模块一致,老版本中引入的外部资源(css、imgs、lib 等)放入 src/assets 目录。

重构步骤

  1. 按照上面的模块拆分要点,改造好代码
  2. 在Jenkins中创建好相应的系统部署任务
  3. 配置nginx做好静态资源转发

时间

2个人,拆分5个模块,从入手到灰度全部上线,花费 1 个月。

javascript小知识点

变量赋值

变量赋值分为两个动作:

  1. 如果当前没有声明过,那编译器会在当前作用域声明一个。
  2. 运行时引擎会在作用域查找该变量,如果找到就对其赋值。

查询有:LHS查询和RHS查询。
LHS:赋值操作的目标是谁。
RHS:谁是赋值操作的源头,restrieve his source value(取到它的源值)。
举个栗子:

1
2
3
4
// RHS引用,取到a的值。
console.log(a);
// LHS引用,只需要对=2这个赋值操作找到一个目标
a = 2;

查看下面这个函数分别有几次LHS和RHS?

1
2
3
4
5
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);

其中有3次LHS:

1
2
3
c = foo(2)
a = 2
b =a

4次RHS:

1
2
3
foo
a //因为要把a的值赋给b
a+b中的a和别分别一次

函数表达式和函数声明简单区分

使用关键字function来判断是否为第一词来区分,如果是为函数声明,如果不是那么就是函数表达式。

1
2
3
4
// 函数表达式
(function xxx(){})();
// 函数声明
function xxx()

IIFE

IIFE: Immediately Invoked Function Expression(立即执行函数表达式)。

变量提升

所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程称为“提升”。

函数声明会被提前到普通变量之前,如果存在同名的函数,后面的函数声明会覆盖前面的。

所有的变量都是先有声明,再有赋值。栗子:

1
2
3
4
5
var a = 2;
// 第一步为编译阶段任务
var a;
// 第二步执行阶段任务
a = 2;

如何判断this

  1. 函数是否在new中调用?如果是,this绑定的是新创建的对象。
  2. 函数是否通过call、apply或者硬绑定?如果是,this绑定的是指定的对象。
  3. 函数是否在某个上下文中调用?如果是,this绑定上下文对象。
  4. 如果都不是,使用默认绑定。严格模式绑定到undefined,否则为全局对象。
  5. foo.call(null) 使用默认绑定规则。
  6. 箭头函数的绑定是无法被修改的(不适用于前4条规则),继承外层函数调用的this绑定。

属性描述符

  1. writeable:属性的值是否可以修改。
  2. configable:属性的值可配置。如果是可以配置的,那么就可以使用defineProperty()方法来修改属性描述符。这种设置为单向,不可取消。意思是可以从true到false,但是不能从falsetrue。如果值为false则禁止删除这个属性。
  3. enumerable:是否为可枚举的。如果是false那么在使用for...in的时候不会出现。

PS:今天看到随手写在本子上的,整理写下来,以备查看。

webpack 2 实践系列(二)— entry 和 output

源码地址:https://github.com/silence717/webpack2-demos

具体可参见 demo02-entry-output 目录下

Entry Points

entry是webpack配置的入口文件的属性,也是整个项目的主入口,其余依赖的模块均在这个文件中引入。
使用方式:entry: string | Array

Output

输出选项告诉webpack如何编写编译后的文件到磁盘。虽然可以有多个入口,但是只要一个输出配置项。
下面列举几个最主要的配置属性:

path

打包后的输出目录地址,是绝对路径。

1
2
3
output: {
path: __dirname + '/'
}

publicPath

正在研究当中,目前遇到一个dev和build环境image路径的问题,晚点具体补充。

fileName

fileName – 指定输出文件的名称

1
2
3
output: {
filename: 'bundle.js'
}

more >>

webpack 2 实践系列(一) — 安装与入门

源码地址:https://github.com/silence717/webpack2-demos

webpack在你的应用中是一个模块打包工具。webpack可以简化工作流,快速构建一个应用程序的依赖关系图,按照它们正确的顺序绑定。webpack可以配置定制优化你的代码,拆分vendor/css/js代码用于生产环境,运行一个可以及时部署代码并且页面无刷新的开发服务器,还有许多很酷的功能。

安装webpack

开始之前首先你得在本地安装一个新版的nodejs。这是一个比较好的基础。老版本你可能会遇到各种与webpack相关的功能丢失或者缺少一些依赖的包。

全局安装

1
npm install webpack -g

安装成功之后,现在webpack命令就在全局生效。
然而,这不是一个最佳实践,因为它会锁定到一个特定版本的webpack,你在项目中使用不同版本的可能会失效。

本地安装

1
2
npm install webpack --save-dev
npm install webpack@<version> --save-dev

这是一种比较推荐的方法。
如果你想运行本地安装的webpack,你可以进入它的bin里面,就像这样 node_modules/.bin/webpack

more >>

【翻译】javascript-prototype

原文地址:http://dailyjs.com/2012/05/20/js101-prototype/

在花费了很多年研究面向对象编程之后,想在javascript使用是令人失望的。主要是从根源上缺少一个class这样的关键词。然而,javascript的设计不会成为阻碍 – 精通它基于原型的继承,将会加深你对该语言的理解。

首先我们需要弄清楚面向对象与面向类编程的区别。Javascript提供了我们需要的工具来完成大多数语言的类可以做的事情 – 我们只需要学习如何正确使用它。

我们简单的看一下prototype属性,看它如何深化我们对javascript的了解。

prototype属性(The prototype Property)

prototype属性是一个内部属性,它被用于实现继承。我们这里的“继承”是一种特定的继承形式。因为状态和方法都由对象承载,所以我们可以说结构、行为和状态都是继承的。这与基于类的语言形成对比,其状态由实例承载,而方法由类承载。

构造函数就是一个具有属性的方法,该属性被称作prototype:

1
2
3
4
function Animal() {
}
console.log(Animal.protype);

{}标识Animal具有一个prototype属性,但是没有用户定义它。我们可以随意添加值和方法:

more >>

杨芳<br>前端一枚<br>背包客,热爱旅行,喜欢摄影<br>轻微洁癖,强迫症,电话恐惧症患者!