当「SPA」应用遇上了膨胀的项目

栏目: JavaScript · 发布时间: 7年前

内容简介:随着项目的成长,单页当项目页面超过一定数量(150+)之后,会产生一系列的问题项目编译的时间(启动server,修改代码)越来越长,而每次调试关注的可能只是其中1、2个页面

随着项目的成长,单页 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页面流程

#/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业务线不再重复打包 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过程如下:

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 的设置隔离了各个业务线 入口文件

当「SPA」应用遇上了膨胀的项目

依赖

当「SPA」应用遇上了膨胀的项目
当「SPA」应用遇上了膨胀的项目

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 就够了。

以上,感谢阅读

原文链接: tech.meicai.cn/detail/75, 也可微信搜索小程序「美菜产品技术团队」,干货满满且每周更新,想学习技术的你不要错过哦。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Hacking Growth

Hacking Growth

Sean Ellis、Morgan Brown / Crown Business / 2017-4-25 / USD 29.00

The definitive playbook by the pioneers of Growth Hacking, one of the hottest business methodologies in Silicon Valley and beyond. It seems hard to believe today, but there was a time when Airbnb w......一起来看看 《Hacking Growth》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具