当「SPA」应用遇上了膨胀的项目
栏目: JavaScript · 发布时间: 5年前
内容简介:随着项目的成长,单页当项目页面超过一定数量(150+)之后,会产生一系列的问题项目编译的时间(启动server,修改代码)越来越长,而每次调试关注的可能只是其中1、2个页面
随着项目的成长,单页 spa
逐渐包含了许多业务线
- 商城系统
- 售后系统
- 会员系统
- ...
当项目页面超过一定数量(150+)之后,会产生一系列的问题
- 可扩展性
项目编译的时间(启动server,修改代码)越来越长,而每次调试关注的可能只是其中1、2个页面
- 需求冲突
所有的需求都定位到当前git,需求过多导致测试环境经常排队
基于以上问题有了对git进行拆分的技术需求。具体如下
目标
- 依然是
spa
由于改善的是开发环境,当然不希望拆分项目影响用户体验。如果完全将业务线拆分成2个独立页面,那么用户在业务线之间跳转时将不再流畅,因为所有框架以及静态资源都会在页面切换的时候重载。因此要求跳转业务线的时候依然停留在spa内部, 不刷新页面
,共用同一个页面入口;
- 业务线页面不再重复加载资源
因为大部分业务线需要用到的框架( vue
, vuex
...), 公共组件( dialog
, toast
)都已经在spa入口加载过了,不希望业务线重复加载这些资源。业务线项目中应该只包含自己独有的资源,并能使用公共资源;
- 业务线之间资源完全共享
业务线之间应该能用router互相跳转,能访问其他业务线包括全局的store
需求如上,下面介绍的实现方式
技术框架
- vue: 2.4.2
- vue-router: 2.7.0
- vuex: 2.5.0
- webpack: 4.7.0
实现
假设要从主项目拆分一个业务线 hello
出来
- 主项目:包含系统核心页面 + 各种必须框架(vue, vuex...)
- hello项目:包含hello自己内部的业务代码
跳转hello页面流程
#/hello/index * bundle js chunk
需要的功能就是这些,下面分步骤看看具体实现
请求业务线路由(步骤1)
第一次请求 #/hello/index
时,此时router中所有路由无法匹配,会走公共 *
处理
/** 主项目 **/ const router = new VueRouter({ routes: [ ... // 不同路由默认跳转链接不同 { path: '*', async beforeEnter(to, from, next) { // 业务线拦截 let isService = await service.handle(to, from, next); // 非业务线页面,走默认处理 if(!isService) { next('/error'); } } } ] }); 复制代码
业务线初始化(步骤2、步骤3)
首先需要一个全局的业务线配置,存放各个业务线的入口js文件
const config = { "hello": { "src": [ "http://local.aaa.com:7000/dist/dev/js/hellobundle.js" ] }, "其他业务线": {...} } 复制代码
此时需要利用业务线配置,判断当前路由是否属于业务线,是的话就请求业务线,不是返回false
/** 主项目 **/ // 业务线接入处理 export const handle = async (to, from, next) => { let path = to.path || ""; let paths = path.split('/'); let serviceName = paths[1]; let cfg = config[serviceName]; // 非业务线路由 if(!cfg) { return false; } // 该业务线已经加载 if(cfg.loaded) { next(); return true; } for(var i=0; i<cfg.src.length; i++) { await loadScript(cfg.src[i]); } cfg.loaded = true; next(to); // 继续请求页面 return true; } 复制代码
有几点需要注意
loaded #/hello/index
hello的入口entry.js做的工作(步骤4)
为了节省资源,hello业务线不再重复打包 vue
, vuex
等主项目已经加载的框架。
那么为了hello能正常工作,需要主项目将以上框架传递给hello,方法为直接将相关变量挂在到 window
:
/** 主项目 **/ import Vue from 'vue'; import { default as globalRouter } from 'app/router.js'; 2个需要动态赋值 import { default as globalStore } from 'app/vuex/index.js'; import Vuex from 'vuex' // 挂载业务线数据 function registerApp(appName, { store, router }) { if(router) { globalRouter.addRoutes(router); } if(store) { globalStore.registerModule(appName, Object.assign(store, { namespaced: true })); } } window.bapp = Object.assign(window.bapp || {}, { Vue, Vuex, router: globalRouter, store: globalStore, util: { registerApp } }); 复制代码
注意 registerApp
这个方法,此方法为hello与主项目融合的挂载方法,由业务线调用。
上一步已经正常运行了hello的entry.js,那我们看看hello在entry中干了什么:
/** hello **/ import App from 'app/pages/Hello.vue'; // 路由器根实例 import {APP_NAME} from 'app/utils/global'; import store from 'app/vuex/index'; let router = [{ path: `/${APP_NAME}`, name: 'hello', meta: { title: '页面测试', needLogin: true }, component: App, children: [ { path: 'index', name: 'hello-index', meta: { title: '商品列表' }, component: resolve => require.ensure([], () => resolve(require('app/pages/goods/Goods.vue').default), 'hello-goods') }, { path: 'newreq', name: 'hello-newreq', meta: { title: '新品页面' }, component: resolve => require.ensure([], () => resolve(require('app/pages/newreq/List.vue').default), 'hello-newreq') }, ] }] window.bapp && bapp.util.registerApp(APP_NAME, {router, store}); 复制代码
注意几点
-
APP_NAME
是业务线的唯一标识,也就是hello - 业务线有自己内部的
router
和store
- 业务线主动调用
registerApp
,将自己的router和store与主项目融合 - store融合的时候需要添加
namespace: true
,因为此时整个hello业务线store成为了globalStore的一个module -
addRoutes
和registerModule
是router与store的动态注册方法 - 路由的
name
需要和主项目保持唯一
业务线配置更新
业务线配置需要在hello每次编译完成后更新,更新分为 本地调试更新
和 线上更新
。
-
本地调试更新
只需要更新一个本地配置文件service-line-config.json
,然后在请求业务线config时由主项目读取该文件返回给js。 -
线上更新
更为简单,每次发布编译后,将当前入口js+md5的完整url更新到后端
以上,看到使用 webpack-plugin
比较适合当前场景,实现如下
class ServiceUpdatePlugin { constructor(options) { this.options = options; this.runCount = 0; } // 更新本地配置文件 updateLocalConfig({srcs}) { .... } // 更新线上配置文件 uploadOnlineConfig({files}) { .... } apply(compiler) { // 调试环境:编译完毕,修改本地文件 if(process.env.NODE_ENV === 'dev') { // 本地调试没有md5值,不需要每次刷新 compiler.hooks.done.tap('ServiceUpdatePlugin', (stats) => { if(this.runCount > 0) { return; } let assets = stats.compilation.assets; let publicPath = stats.compilation.options.output.publicPath; let js = Object.keys(assets).filter(item => { // 过滤入口文件 return item.startsWith('js/'); }).map(path => `${publicPath}${path}`); this.updateLocalConfig({srcs: js}); this.runCount++; }); } // 发布环境:上传完毕,请求后端修改 else { compiler.hooks.uploaded.tap('ServiceUpdatePlugin', (upFiles) => { let entries = upFiles.filter(file => { return file && file.endsWith('js') && file.includes('js/'); }); this.uploadOnlineConfig({files: entries}); return; }) } } } 复制代码
注意, uploaded
事件由我们项目组的静态资源上传plugin发出,会传递当前所有上传文件完整路径。需要等文件上传cdn完毕才可更新业务线
之后在webpack中使用即可
/** hello **/ { ... plugins: [ // 业务线js md5更新 new McServiceUpdatePlugin({ app_name, configFile: path.resolve(process.cwd(), '../mainProject/app/service-line-config.json') }) ], ... } 复制代码
注意本地调试时业务线config是 主项目
才会用到的,因此直接更新主项目目录下的配置文件
调试发布
基于上面的plugin,有以下效果
调试过程如下:
- 启动主项目server(端口
7777
); - 启动hello业务线server(端口
7000
),此时启动成功会同时更新本地文件service-line-config.json
; - 访问hello页面,加载本地配置后,加载
7000
端口提供的静态资源(如http://local.aaa.com:7000/dist/dev/js/hellobundle.js)
发布test过程如下:
npm run test
可以看到hello发布是比主项目更加轻量的,这是因为业务线只更新接口,但是主项目要发布还需要更新html的web服务
小结
至此已经完成了一开始的主体需求,访问业务线页面后,业务线页面会和主项目页面合并成为1个新的spa,spa内部store和router完全共享。
可以看到主要利用了vue家族的动态注册方法。下面是一些过程中遇到的问题和解决思路
遇到的问题与解决
hello业务线的wepback打包
- 业务线需要独立的打包命名空间
- 为了能与主项目区分,会给hello业务线的
bundle
重命名,增加了业务线名称前缀 - 入口文件越少越好,因此删除了一些打包配置
vendor dll manifest
/** hello **/ { ... entry: { [app_name + 'bundle']: path.resolve(SRC, `entry.js`) }, output: { publicPath: `http://local.aaa.com:${PORT}${devDefine.publicPath}`, library: app_name // 业务线命名空间 }, ... optimization: { runtimeChunk: false, // 依赖处理与bundle合并 splitChunks: { cacheGroups: false // 业务线不分包 } }, ... } 复制代码
注意 library
的设置隔离了各个业务线 入口文件
依赖
router拆分问题
最开始使用/:name来做公共处理。
但是发现router的优先级按照数组的插入顺序,那么后插入的hello路由优先级将一直低于/:name路由。
之后使用 *
做公共处理,将一直处于兜底,问题解决。
store拆分
hello的store做为globalStore的一个module注册,需要标注 namespaced: true
,否则拿不到数据;
store使用基本和主项目一致:
/** hello **/ let { Vuex } = bapp; // 全局store获取 let { mapState: gmapState, mapActions: gmapActions, createNamespacedHelpers } = Vuex; // 本业务线store获取 const { mapState, mapActions } = createNamespacedHelpers(`${APP_NAME}/feedback`) export default { ... computed: { ...gmapState('userInfo', { userName: state => state.userName }), ...gmapState('hello/feedback', { helloName2: state => state.helloName }), ...mapState({ helloName: state => state.helloName }) }, } 复制代码
接口拆分
虽然前端工程拆分了,但是后端接口依然是走相同的域名,因此可以给hello暴露一个生成接口参数的公共方法,然后由hello自己组织。
公共利用
可以直接使用全局 组件
, mixins
, directives
,可以直接使用 font
。 局部的相关内容需要拷贝到hello或者暴露给hello才可用。 图片完全无法复用
本地server工具
主项目由于需要对request有比较精细的操作,因此是我们自己实现的 express
来本地调试。
但是hello工程的唯一作用是提供本地当前的js与css,因此使用官方 devServer
就够了。
以上,感谢阅读
原文链接: tech.meicai.cn/detail/75, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- “膨胀”的AI安防何时“落”下?
- opencv中的图像形态学——腐蚀膨胀
- C++拾趣——使用多态减少泛型带来的代码膨胀
- 【PS-3D功能探索】如何在ps内创建膨胀的3D文本效果
- android – 如何使用膨胀的布局在自定义DialogPreference中访问窗口小部件?
- 巴比特专栏 | 比特币涨到100万的依据:浅谈可无限膨胀的分布式组织
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JS 压缩/解压工具
在线压缩/解压 JS 代码
HEX HSV 转换工具
HEX HSV 互换工具