vue单页(spa)前端git工程拆分实践

背景

随着项目的成长,单页spa逐渐包含了许多业务线

  • 商城系统
  • 售后系统
  • 会员系统
  • ...

当项目页面超过一定数量(150+)之后,会产生一系列的问题

  • 可扩展性

项目编译的时间(启动server,修改代码)越来越长,而每次调试关注的可能只是其中1、2个页面

  • 需求冲突

所有的需求都定位到当前git,需求过多导致测试环境经常排队

基于以上问题有了对git进行拆分的技术需求。具体如下

目标

  • 依然是spa

由于改善的是开发环境,当然不希望拆分项目影响用户体验。如果完全将业务线拆分成2个独立页面,那么用户在业务线之间跳转时将不再流畅,因为所有框架以及静态资源都会在页面切换的时候重载。因此要求跳转业务线的时候依然停留在spa内部,不刷新页面,共用同一个页面入口;

  • 业务线页面不再重复加载资源

因为大部分业务线需要用到的框架(vue, vuex...), 公共组件(dialogtoast)都已经在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页面流程

  1. 用户访问业务线页面 路由 #/hello/index
  2. 主项目router未匹配,走公共*处理;
  3. 公共router判定当前路由为业务线hello路由,请求hello的入口bundle js
  4. hello入口js执行过程中,将自身的router与store注册到主项目;
  5. 注册完毕,标记当前业务线hello为已注册;
  6. 之后路由调用next。会自动继续请求 #/hello/index对应的页面chunk(js,css)页面跳转成功;
  7. 此时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业务线不再重复打包vuevuex等主项目已经加载的框架。

那么为了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
  • 业务线有自己内部的routerstore
  • 业务线主动调用registerApp,将自己的router和store与主项目融合
  • store融合的时候需要添加namespace: true,因为此时整个hello业务线store成为了globalStore的一个module
  • addRoutesregisterModule是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,有以下效果

调试过程如下:
  1. 启动主项目server(端口7777);
  2. 启动hello业务线server(端口7000),此时启动成功会同时更新本地文件service-line-config.json;
  3. 访问hello页面,加载本地配置后,加载7000端口提供的静态资源(如http://local.aaa.com:7000/dist/dev/js/hellobundle.js)
发布test过程如下:
  1. 执行 npm run test
  2. 执行过程中会上传文件并更新test环境业务线配置
  3. 此时访问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的设置隔离了各个业务线
入口文件

依赖

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自己组织。

公共利用

可以直接使用全局组件mixinsdirectives,可以直接使用font
局部的相关内容需要拷贝到hello或者暴露给hello才可用。
图片完全无法复用

本地server工具

主项目由于需要对request有比较精细的操作,因此是我们自己实现的express来本地调试。

但是hello工程的唯一作用是提供本地当前的js与css,因此使用官方devServer就够了。



以上

原文地址:https://segmentfault.com/a/1190000017124192

原文地址:https://www.cnblogs.com/datiangou/p/10122774.html

时间: 2024-10-03 22:51:35

vue单页(spa)前端git工程拆分实践的相关文章

解决vue单页路由跳转后scrollTop的问题

作为vue的初级使用者,在开发过程中遇到的坑太多了.在看页面的时候发现了页面滚动的问题,当一个页面滚动了,点击页面上的路由调到下一个页面时,跳转后的页面也是滚动的,滚动条并不是在页面的顶部 在我们写路由的时候做个处理,如下: import Vue from 'vue' import Router from 'vue-router' Vue.use(Router); Vue.use(Router) export default new Router({ routes: [ { path: '/',

Vue 基于node npm & vue-cli & element UI创建vue单页应用

基于node npm & vue-cli & element UI创建vue单页应用 开发环境   Win 10   node-v10.15.3-x64.msi 下载地址: https://nodejs.org/en/ 安装node 安装vue-cli 1.安装node-v10.15.3-x64.msi 2.设置注册地址 因为npm官方仓库在国外,有时候下载速度会非常慢,不过有淘宝镜像可以使用,下载包的速度很快.而且淘宝镜像是定时更新同步npm的官方仓库的. npm config set

Git工程开发实践(二)——Git内部实现机制

Git工程开发实践(二)--Git内部实现机制 一.Git仓库内部实现简介 Git本质上是一个内容寻址(content-addressable)的文件系统,根据文件内容的SHA-1哈希值来定位文件.Git核心部分是一个简单的键值对数据库(key-value data store).向Git数据库插入任意类型的内容,会返回一个键值,通过返回的键值可以在任意时刻再次检索(retrieve)插入的内容.通过底层命令hash-object可以将任意数据保存到.git目录并返回相应的键值.Git包含一套面

Git工程开发实践(四)——Git分支管理策略

Git工程开发实践(四)--Git分支管理策略 一.Git版本管理的挑战 Git是非常优秀的版本管理工具,但面对版本管理依然有非常大得挑战.工程开发中,开发者彼此的代码协作必然带来很多问题和挑战:A.如何开始一个Feature开发,而不影响其它Feature?B.由于很容易创建新分支,分支多了如何管理,时间久了,如何知道每个分支是干什么的?C.哪些分支已经合并回了主干?D.如何进行Release的管理?开始一个Release的时候如何冻结Feature, 如何在Prepare Release的时

Git工程开发实践(三)——Git常用操作

Git工程开发实践(三)--Git常用操作 一.Git仓库操作 1.Git仓库创建 git init在当前目录中初始化Git仓库git init [project-name]创建一个新目录并初始化仓库初始化git仓库会默认创建一个mater分支,创建名为.git的子目录,内含初始化Git仓库中所有的骨干文件,此时仓库中的文件还没有被跟踪.通过git add命令来实现对指定文件的跟踪,然后执行git commit提交. git add . git commit -m 'initial projec

Git工程开发实践(六)——Git工程实践扩展

Git工程开发实践(六)--Git工程实践扩展 一.Git提交日志规范 1.Git提交日志模板 Git支持对每次提交的日志信息进行规范,可以通过设置提交模板实现.建立一个gitCommitTemplate文件,内容为: #commit message包含三部分,header, body和footer,其中header必选,body和footer可选. # type(<scope>): <subject> #<body> #<footer> #type字段包含

Git工程开发实践(七)——GitLab服务搭建

Git工程开发实践(七)--GitLab服务搭建 操作系统:RHEL 7.3 WorkStation 一.GitLab简介 1.GitLab简介 ?GitLab是一个利用Ruby on Rails开发的开源版本管理系统,是集代码托管.测试.部署于一体的开源git仓库管理软件,可通过web界面来进行访问公开或私人项目.GitLab能够浏览代码,管理缺陷和注释,可以管理团队对仓库的访问,非常易于浏览提交过的版本,并提供一个文件历史库,是目前非常流行的研发版本控制系统.Git:本地版本控制系统工具.G

Vue单页式应用(Hash模式下)实现微信分享

前端微信分享的基本步骤: 一.绑定域名: 先登录微信公众平台进入"公众号设置"的"功能设置"里填写"JS接口安全域名".这个不多说,微信开发的都应该清楚. 二.引入js文件: 在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.0.0.js.请注意,如果你的页面启用了https,务必引入 https://res.wx.qq.com/open/js/jweixi

vue单页应用前进刷新后退不刷新方案探讨

引言 前端webapp应用为了追求类似于native模式的细致体验,总是在不断的在向native的体验靠拢:比如本文即将要说到的功能,native由于是多页应用,新页面可以启用一个的新的webview来打开,后退其实是关闭当前webview,其上一个webview就自然显示出来:但是在单页的webapp应用中,所有内容其实是在一个页面中展示的,不存在多页的情况,这时就需要前端开发来想办法实现相应的体验效果. 首先需要说明一下,本文所说的前进刷新后退不刷新是指组件是否重新渲染,比如列表A页面,点击