Coding 前端重构之路

CodeInsight 开发告一段落之后,CTO 大人找到我说要想一个把 Coding.net 的前端拆分重构的方案,于是我从一个欢脱的开发状态开始切换到要面对一句魔咒的考验。

动态语言一时爽,代码重构火葬场。

不管怎么样,先从梳理现状开始。

Coding 前端使用 Angular 构建,前端工程化还是使用合并文件打包的方式,并没有引入 CommonJS 之类的模块化开发方式,作为一个 SPA 网站,随着网站规模的增大,前端代码开始越来越臃肿,开发体验也直线下降,这是我们考虑重构的原因。

所以首先我们要想清楚重构要解决的问题

  • 代码打包拆分,避免所有功能模块打包到单一的文件
  • 引入 CommonJS 做到更清晰的模块化
  • 边开车边换轮子

要做到最后一点尤其困难,但这也是我们能否顺利重构的关键,重构不是重写,所以如何在现有代码基础上重构,并且还要和当前的开发进度无缝衔接起来就是我们所要面临的一个挑战。

好消息是我们使用了 Angular 保证了我们的代码分 Module 有了一层封装,不至于太过散乱。作为一个 SPA 网站,前端路由已经很好的分离出了各个功能模块。我们用到了 Grunt,虽然有点过时,task 写得有点复杂,但是提供了一个工程化的切入口。

经过小伙伴们几轮讨论之后,最终确定了一套比较靠谱的方案:

  • 按照功能模块重新整理/拆分代码
  • 保持作为一个 SPA 网站,按照路由懒加载功能模块

后一点是我们这套重构思路的核心,在这之前我们有考虑把每个功能模块拆分出独立的单元来跑,但是为了保证“边开车边换轮子”,重构必须是一个快速迭代的过程,不能说等到某一个完整的功能模块单元重构完了再去整合到现有的代码,这种重构方式将是一个漫长耗时的过程,并且风险也很大。

保持 SPA,引入懒加载,我们可以快速将这种架构调整整合到现有代码中去,验证是否可行,之后的重构过程就可以细化到每一个 controller,做到“边开车边换轮子”。

功能模块拆分

这一步很简单,Coding 网站的功能模块已经比较清晰了,比如冒泡,任务,搜索等等,我们只需要确立一套统一的目录结构和命名空间规范来重新整理代码,得益于 Angular 的依赖注入机制,之前的代码逻辑完全可以保持不变,对于那些独立的模块,这个重构过程基本上没有什么引入 Regression Bugs 的风险,重构一个模块只是修改命名空间而已。

我们约定:

  • 重构的每个 controller, service 等等都有自己的命名空间(规范)
  • 每个功能模块定义自己的路由
  • 每个功能模块有一个唯一的 module 注入所有依赖

比如重构后的冒泡可能是类似这样的结构:

tweet/
├── tweet-list.controller.js
├── tweet-list.html
├── tweet-topic.controller.js
├── tweet-topic.html
├── tweet.module.js
└── tweet.routes.js

tweet.routes.js 将会指定懒加载 tweet.module.js。

Webpack 懒加载

为了做到代码打包拆分,我们使用懒加载的方式,当导航到对应的功能模块时才去加载相应的功能模块代码,引入 webpack 一并实现了对 CommonJS 的支持以及 Lazy Load。

现有的 Angular 路由已经很好的分离出了功能模块入口,所以我们只要把这个路由文件当做一个切入点,作为 webpack 的打包入口文件,由这个入口文件引入的所有依赖就都可以使用 CommonJS 的模块化方式了,也就是说我们所有重构的代码自然而然就可以迁移到使用 CommonJS,在这里 webpack 将作为一个完美的粘合剂,衔接现有的代码和重构后的代码,这里通过一个简单的路由来看一下是如何做到的。

./tweet/tweet.routes.js

$routeProvider.when('/pp/:region?', {
templateUrl: require('./tweet-list.html'),
controller: 'TweetListController',
title: '冒泡',
resolve: {
lazyLoader: function($q, $ocLazyLoad) {
var defer = $q.defer();
require.ensure([], function() {
var module = require('./tweet.module');
$ocLazyLoad.load({ name: module.name });
defer.resolve(module);
});
return defer.promise;
}
}
});

Angular 的路由支持异步加载,require.ensure 是 webpack 用来异步加载回调函数内部指定的 tweet.module.js,这个 module 注入了 tweet 这个功能模块的所有依赖,比如TweetListController

'use strict';
var angular = require('angular');
module.exports = angular
.module('tweet', [
require('./tweet-list.controller').name,
require('./tweet-topic.controller').name,
]);

最后我们用到了 ocLazyLoad 来注入这个异步加载的 module。

grunt-webpack

新引入的 webpack 可以很容易的整合到我们现有的开发流程里面去,使用 grunt-webpack 就可以把 webpack 作为一个新的任务给 grunt 调用,所以我们可以独立 webpack 的打包编译流程,并且作为一个子任务插入原来的编译流程,而不影响原来的开发/发布方式。

gruntfile.js

...
    webpack: {
dev: {
entry: {
'routes': ['./src/routes.js'],
},
...
},
prod: { ... }
}
grunt.registerTask('server', [..., 'webpack:dev', ...]);
grunt.registerTask('build', [..., 'webpack:prod', ...]);
...

公用模块

对于独立的,不被其他地方依赖的 module,重构可以很方便,但是对于公用的模块,虽然可以重构这个模块,但是要更改所有针对这个模块的引用,牵涉到的代码就有点不受控制了。

所以我们才要约定所有重构的模块都会有自己的命名空间,对于那些公用的模块,迁移到新的命名空间,同时会保留之前的代码,直到我们重构其他功能模块到某个时间点,可以确定没有模块依赖这些被保留的公用模块,再去清理,这样在前期会有一部分代码冗余,但是保证了我们重构的质量和进度。

至此,Coding 前端开启了重构之路,相信 Coding.net 将会逐步带来更好的体验。

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.