前端架构概览
思考:我们有什么,我们缺什么?
前端架构分为很多部分,在每个不同的项目里都会有各自的特点。所以,当我们想优化一个大型项目的时候,可以从一个概览图来入手分析,比如下图:
从我自己的项目特点来分析,我们的基础设施比较完备,一些公共的基础服务都可以尝试接入,唯独业务代码异常混乱。
原因:由于业务迭代频繁,接手的人多,导致组件规范不好、公共方法没有抽离。而且各个业务之间代码耦合性很强,看似没关联的业务,内部代码之间却互相调用。长此以往,这个项目必定难以维护,新的需求迭代只会越做越慢,最后无人愿意接手。
那我的切入点就先从这代码结构开始,vuejs框架既然这么自由,那么我们就能创造更多的属于自己业务特点的东西。
改造一,面向服务设计
前端如何有服务?
定义
所谓服务,是指软件功能的独立单元,其设计意图是完成特定的任务。其中包含了执行完整、离散的业务功能所需的代码和数据集成,并且可以远程访问、进行交互或独立更新。
以往痛点
前端的公共代码往往最多放在一个公共目录,甚至有些还分散在不同业务页面里,不断的被复制、粘贴,导致代码复用率很低。并且没有版本可以追踪修改历史,导致产生潜在的全局影响。
借鉴点
Chrome团队的面向服务架构设计(SOA)。
原来的各种模块会被重构成独立的服务(Service),每个服务(Service)都可以在独立的进程中运行,访问服务(Service)必须使用定义好的接口,通过 IPC 来通信,从而构建一个更内聚、松耦合、易于维护和扩展的系统,更好实现 Chrome 简单、稳定、高速、安全的目标!
示意图:
结合点
前端的独立模块可以用npm包来实现,每个npm包负责一个独立的公共功能,自带版本管理和独立更新。package.json定义出口文件index.js,其中定义此模块提供的功能接口和代码文件。接口调用通过export、import方式实现,可以全局或按需加载。
示意图:
改造效果
目录结构:
调用方式:
import { ServiceStorage } from 'hc-services'
ServiceStorage.setItem('userId', userId)
改造二,业务模块化
前端如何有模块?
定义
模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程。每个模块完成一个特定的子功能,所有的模块按某种方式组装起来,成为一个整体,完成整个系统所要求的功能。
以往痛点
前端的业务代码很容易形成低内聚、高耦合的穿插形式,难以维护,可读性差。加上vuejs选项式开发导致逻辑关注点的碎片化,以及mixin导致的方法名、变量名在子组件中的混淆。随着项目规模变大,几年之后就会出现难以维护,无人敢碰,只能重构的局面。
借鉴点
Symfony的bundle系统(一套可复用的PHP组件)。
bundle类似于其他软件中的插件,但却更好。关键区别在于:Symfony中的每一样东西都是bundle,包括框架核心功能,以及你编写的程序代码。bundle是Symfony体系中的一等公民。一个bundle,就是一组结构化的文件,存于一个“用于实现某个独立功能”的目录中。每个目录都包含着关乎那个功能的所有东西,包括php文件,模板,css,js文件,tests,以及其他。
示意图:
结合点
基于vue的multi-page模式,每个page是一个完全独立的业务模块(App),独立编译部署。每个业务模块中再细分子业务模块subApp,每个subApp自带vuejs框架的store、router、mixins等关乎这个业务的所有东西。编译时将subApp等子模块组装到一起,从而实现一个独立的业务功能。这样就能实现业务代码的解耦,让各个子模块功能更内聚,更易于维护和扩展。
示意图:
改造效果
subApp的接口文件index.js:
// index.js,subApp的出口文件
import { HcSubApp } from 'hc-micro-pages';
import store from './store';
import routes from './routes';
import { jump2IsvPage, jump2CreateCard } from './libs/utils';
const subApp = new HcSubApp();
subApp.store = store; // vuex数据注册
subApp.routes = routes; // vue router路由注册
subApp.routePrefix = 'inquiry'; // 命名空间隔离
subApp.storeModule = 'inquiry'; // 命名空间隔离
// 输出subApp对象,供融合使用
export default subApp;
// 输出对外接口,供其他subApp调用
export { jump2IsvPage, jump2CreateCard };
subApp的热插拔注册:
/**
* SubApp 注册
* 这里实现的是SubApp的热插拔
*/
import SubAppInquiry from './inquiry';
import SubAppCollect from './collect';
import SubAppInoculate from './inoculate';
import SubAppDoctor from './doctor';
export default {
SubAppInquiry,
SubAppCollect,
SubAppInoculate,
SubAppDoctor,
};
subApps的聚合
import { HcBaseApp, HcSubAppComposer } from 'hc-micro-pages';
import App from './App.vue';
// 引入subApps
import subApps from './subApps';
// 融合所有subApps
const composer = new HcSubAppComposer(subApps);
composer.install();
// 创建聚合App实例
class MyApp extends HcBaseApp {
beforeStart() {} // 钩子
afterStart() {} // 钩子
}
const app = new MyApp({
router: composer.router, // 引入聚合数据
store: composer.store, // 引入聚合数据
App,
});
// 创建Vue实例并挂在dom节点
app.start();
改造三,多包管理模式
为什么还涉及到多包?公共服务包 + 业务模块 = ?
新问题
npm包的形式并不适合修改频繁的项目,总要发布新版本,会降低开发效率。而且包的代码还要和业务代码分离,同时操作两个项目去写同一个业务感觉很分裂,也不利于调试。同时业务模块也缺少一个模块该有的特性:版本、修改历史、发版的tag。这样对灰度发布、版本回滚、问题追溯都不友好。
借鉴点
lerna,名字来源于希腊神话九头蛇海德拉(Lernaean Hydra)。 形象的说明它是一个用来管理多包项目的工具(monorepos),多包指的是一个项目内包含多个git、npm包。已经采用lerna管理的库有很多,比如:Babel、React、Jest、Taro等。
结合点
将服务包和业务模块合并成一个项目,服务包放到packages目录,业务模块放到pages目录,每个子目录都初始化成npm包形式,用于版本管理。
并且搭配yarn的workspace特性,每个包、模块都有自己的版本依赖。
当用lerna初始化项目时,它会将对每个包、模块的引用改成软链接的形式(如果没有指定版本;或者指定的版本与本地包的版本一致也会形成软连接),这样每次修改都会对其他包实时生效,大大提高开发、调试效率。一旦开发、调试完成,就可以指定具体的包版本并且发布到npm源,以保证代码稳定。
而且打包部署时,可以对每个包、模块根据版本打一个tag(例如:hc-utils@1.0.1),并生成changelog文件,方便代码回滚和追溯问题。
改造效果
lerna配置
{
"useWorkspaces": true,
"version": "independent",
"npmClient": "yarn",
"packages": [
"src/*",
"src/packages/*",
"src/pages/*",
],
"command": {
"publish": {
"allowBranch": [
"master"
],
"message": "chore(release): publish packages",
"conventionalCommits": true
}
}
}
lerna下的目录结构
发版前打tag
最终结果及未来
未来的拓展在哪里?
最终效果,逻辑图:
缺点
包的权限问题
由于是monorepos项目,意味着你可以修改所有代码,在实际开发中就会带来一些麻烦。比如某些成员不小心修改了其他包的代码,而我们对这些修改可能检测不到。
好在我们在发版时会对所有包在master分支上进行打tag,如果发现当前MR的需求并不涉及某些改动的包,那我们可以lerna diff一下,看看是不是错误代码。
展望
subApp子模块不应该仅仅局限于子页面范畴,它就像页面的积木,应该提供更多的能力,只不过基于当前的业务特点,首要解决了页面多而杂的问题。
其他的可能性就是提供逻辑功能的能力,比如用户登录模块(监听token过期自动拦截路由跳转)。还有页面局部功能块的能力,比如广告位(根据位置id,请求数据后自动渲染)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。