vue单页(spa)前端git工程拆分实践
背景
随着项目的成长,单页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
; - 主项目router未匹配,走公共
*
处理; - 公共router判定当前路由为业务线hello路由,请求hello的入口
bundle js
; - hello入口js执行过程中,将自身的router与store注册到主项目;
- 注册完毕,标记当前业务线hello为已注册;
- 之后路由调用next。会自动继续请求 #/hello/index对应的页面
chunk
(js,css)页面跳转成功; - 此时hello已经与主项目完成融合,hello可以自由使用全部的store,使用router可以自由跳转任何页面。done
需要的功能就是这些,下面分步骤看看具体实现
请求业务线路由(步骤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;
}
有几点需要注意
- 一般业务线配置存放在后端,此处为了说明直接列出
- 业务线只加载1次,
loaded
为判定条件。加载过的话直接进行next - 当第1次业务线加载成功,此时主项目已经包含了
#/hello/index
的路由,此时next可以正常跳转。原因见下一节
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
- 执行过程中会上传文件并更新test环境业务线配置
- 此时访问test环境页面已经更新
可以看到hello发布是比主项目更加轻量的,这是因为业务线只更新接口,但是主项目要发布还需要更新html的web服务
小结
至此已经完成了一开始的主体需求,访问业务线页面后,业务线页面会和主项目页面合并成为1个新的spa,spa内部store和router完全共享。
可以看到主要利用了vue家族的动态注册方法。下面是一些过程中遇到的问题和解决思路
遇到的问题与解决
hello业务线的wepback打包
- 业务线需要独立的打包命名空间
- 为了能与主项目区分,会给hello业务线的
bundle
重命名,增加了业务线名称前缀 入口文件越少越好,因此删除了一些打包配置
- 删除了
vendor
: 主要第三方库由主项目加载 - 删除了
dll
: dll资源由主项目加载 - 删除了runtime(
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
的设置隔离了各个业务线
入口文件
依赖
![在这里插入图片描述](https://img-blog.csdnimg.cn/20181215110746504.png)
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
就够了。
以上
原文地址:https://segmentfault.com/a/1190000017124192
vue单页(spa)前端git工程拆分实践的更多相关文章
- 如何在vue单页应用中使用百度地图
作为一名开发人员,每次接到开发任务,我们首先应该先分析需求,然后再思考技术方案和解决方案.三思而后行,这是一个好的习惯. 需求:本项目是采用vue组件化开发的单页应用项目,现需要在项目中引入百度的地图 ...
- vue 单页应用中微信支付的坑
vue 单页应用中微信支付的坑 标签(空格分隔): 微信 支付 坑 vue 场景 在微信H5页面(使用 vue-router2 控制路由的 vue2 单页应用项目)中使用微信 jssdk 进行微信支付 ...
- 解决vue单页路由跳转后scrollTop的问题
作为vue的初级使用者,在开发过程中遇到的坑太多了.在看页面的时候发现了页面滚动的问题,当一个页面滚动了,点击页面上的路由调到下一个页面时,跳转后的页面也是滚动的,滚动条并不是在页面的顶部 在我们写路 ...
- vue单页应用中 返回列表记住上次滚动位置、keep-alive缓存之后更新列表数据 那点事
实践场景需求 产品列表中,滚动到一定位置的时候,点击查看产品信息,后退之后,需要回到原先的滚动位置,这是常见的需求 所有页面均在router-view中,暂时使用了keep-alive来缓存所有页面, ...
- 基于vue单页应用的例子
代码地址如下:http://www.demodashi.com/demo/13374.html 目录结构 src目录 主要的代码目录 components 存放项目组件 router 路由文件 sto ...
- Vue 基于node npm & vue-cli & element UI创建vue单页应用
基于node npm & vue-cli & element UI创建vue单页应用 开发环境 Win 10 node-v10.15.3-x64.msi 下载地址: https ...
- Git工程开发实践(四)——Git分支管理策略
A successful Git branching model https://nvie.com/posts/a-successful-git-branching-model/ Git工程开发实践( ...
- 【前端vue开发】vue单页应用添加百度统计
前言 申请百度统计后,会得到一段JS代码,需要插入到每个网页中去,在Vue.js项目首先想到的可能就是,把统计代码插入到index.html入口文件中,这样就全局插入,每个页面就都有了;这样做就涉及到 ...
- Vue单页式应用(Hash模式下)实现微信分享
前端微信分享的基本步骤: 一.绑定域名: 先登录微信公众平台进入"公众号设置"的"功能设置"里填写"JS接口安全域名".这个不多说,微信开发 ...
随机推荐
- sh_11_字典的其他操作
sh_11_字典的其他操作 xiaoming_dict = {"name": "小明", "age": 18} # 1. 统计键值对数量 p ...
- CodeForces 538F A Heap of Heaps
题意 给定一个长度为n的数组A,将它变为一颗k叉树(1 <= k <= n - 1)(堆的形式编号). 问对于每一个k,有多少个节点小于它的父节点. 解题 显然,最初的想法是暴力.因为树的 ...
- POJ1703--Find them, Catch them(种类并查集)
Time Limit: 1000MSMemory Limit: 10000K Total Submissions: 32909Accepted: 10158 Description The polic ...
- Java中FileOutputStream流的write方法
本文为大家分享了FileOutputStream流的write方法,供大家参考,具体内容如下 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ...
- 安装OpenCV 3 on Raspbian Jessie
环境: 硬件:树莓派三代B型, 5MP Camera Board Module 软件:Raspbian,Opencv 安装依赖 $ sudo apt-get update $ sudo apt-g ...
- JPA查询getOne()与findOne()的差异以及一些小问题
起初用Jpa 里面 getOne() 查询一个id的数据 发现查询出来的数据都是空的,但不是空的对象是按照对象默认值来的 所以导致查询不出结果 以为是数据库修改,没有及时修改实体类导致的 但是后来发现 ...
- spring的IOC——依赖注入的两种实现类型
一.构造器注入: 构造器注入,即通过构造函数完成依赖关系的设定.我们看一下spring的配置文件: <constructor-arg ref="userDao4Oracle" ...
- axios的数据请求方式及跨域
express 的三大功能:静态资源.路由.模板引擎 app.use(express.static('www')); 只要是创建这个静态的目录,这个 www 的静态目录里面的文件就可以被访问 数据的请 ...
- layui数据加载中遮罩层的实现
1.load方法提供三种风格供选择. 方法一:loadIndex = layer.load(); //不传参,默认0 方法二:loadIndex = layer.load(1); // 1,另外一种风 ...
- 全面解读php-正则表达式
一.正则表达式基础内容 注: 1.通用原子: \d : 数字. \D : 除了数字. \w : 数字,字母,下划线.\W : 除了数字,字母,下划线. \s : 空白符 . \S : 除了空白符 ...