如何構建Vue大型應用

unadlib發表於2019-04-14

如何構建Vue大型應用

Observable模式的MVVM讓Vue在中小型Web應用中有天然的優勢,但隨著Vue的流行度日益增長,Vue在大型專案中的運用略顯捉襟見肘。顯然在高複雜度專案中,型別檢查已成必需要素,而Vue2在TypeScript的型別檢查支援又不夠徹底,更重要的是Vuex的狀態邏輯的模組化設計欠缺。

所以這裡提出以下解決方案:

  • 業務邏輯模組化 - usm-vuex將解決模組化這個重要問題
  • TypeScript - vue-cli3
  • TSX - 更好的模版型別推導
  • 依賴注入 - 目前最好的DI庫inversify
  • 分包 - 使用lerna構建Monorepo

lerna初始化後,進行領域驅動設計,得到大的領域模組。在必要情況下,將可以進行分包,同時啟用動態import懶載入或者是RequireJS等模組載入器,以提高構建時效能和執行效能。

在核心應用子專案的初始化使用vue-cli3建構,選擇TypeScript作為主要語言,它將自動引入Webpack的ts-loder。

這是核心目錄結構:

|-- App.vue
|-- main.ts
+-- modules/
  |-- Todos/
  |-- Navigation/
  |-- Portal/
  |-- Counter/
  ...
+-- lib/
  |-- loader.ts
  |-- moduleConnect.ts
  ...
+-- components/
...
複製程式碼

main.ts是預設的entry。

// ...
// omit some modules
import { load } from './lib/loader';

const {
  portal,
  app,
} = load({
  bootstrap: 'Portal',
  modules: {
    Counter,
    Todos,
    Portal,
    Navigation
  },
  main: App,
  components: {
    home: {
      screen: TodosView,
      path: '/',
      module: 'todos',
    },
    counter: {
      screen: CounterView,
      path: '/counter',
      module: 'counter',
    },
  }
});
Vue.prototype.portal = portal;
new Vue(app).$mount("#app");
複製程式碼

App.vue是主檢視檔案。

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/counter">Counter</router-link> 
    </div>
    <router-view />
  </div>
</template>
複製程式碼

modules包含全部的業務邏輯,也包括檢視層狀態和導航模組等,它將由Vuex來啟動。 例如以下是Counter模組:

import { injectable } from "inversify";
import Module, { state, action, computed } from "../../lib/baseModule";

@injectable()
export default class Counter extends Module {
  @state count: number = 0;

  @action
  calculate(num: number, state?: any) {
    state.count += num;
  }

  getViewProps() {
    return {
      count: this.count,
      calculate: (num: number) => this.calculate(num)
    }
  }
}
複製程式碼

lib/loader.ts是應用配置載入器。

import { Container } from 'inversify';

export function load(parmas: any = {}) {
  const { bootstrap, modules, ...option } =  parmas;
  const container = new Container({ skipBaseClassChecks: true });
  Object.keys(modules).forEach(key => {
    container.bind(key).to(modules[key]);
  });
  container.bind("AppOptions").toConstantValue(option);
  const portal: any = container.get(bootstrap);
  portal.bootstrap();
  const app = portal.createApp();
  return {
    portal,
    app,
  };
}
複製程式碼

lib/moduleConnect.ts是ViewModule的View聯結器。 這是一個高階元件的聯結器。

import { Component, Vue } from "vue-property-decorator";

export default (ViewContainer: any, module: string) => {
  @Component({
    components: {
      ViewContainer
    }
  })
  class Container extends Vue {
    get module() {
      return this.portal[module];
    }

    render(createElement: any) {
      const props = this.module.getViewProps();
      return createElement(ViewContainer, {
        props
      })
    }
  }
  return Container;
}
複製程式碼

components/Counter/index.tsx是Counter的元件。

import { Component, Vue, Prop } from "vue-property-decorator";
import './style.scss';

type Calculate = (sum: number) => void;

@Component
export default class CounterView extends Vue {
  @Prop() count!: number;
  @Prop(Function) calculate!: Calculate; 

  render(){
    return (
      <div class="body">
        <button onClick={()=> this.calculate(1)}>+</button>
        <span>{this.count}</span>
        <button onClick={()=> this.calculate(-1)}>-</button>
      </div>
    )
  }
}
複製程式碼

配合TSX的View元件模組,同時基於此架構等整體設計將很大程度上契合TypeScript的型別檢查和推導。

在該Vue架構中最核心的設計部分應該是usm-vuex,它讓Vuex進行業務模組化變得簡單明瞭,配合View層的ViewModule,它能夠讓當前的架構設計變得高內聚低耦合,在複用性與維護性上大大提高,同時配合依賴注入(dependency injection),讓模組間的依賴變得清晰易懂。對於現實中的大型應用還有很多其他細節部分待完善,這裡就不敷述了。

usm-vuex Repo: github.com/unadlib/usm

本文的架構完整Demo: github.com/unadlib/usm…

相關文章