vue+ts的微前端实践

作者: 夜闯寡妇门 | 来源:发表于2020-08-15 12:05 被阅读0次

    微前端的概念是从后端的微服务中迁移过来的。将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署

    qiankun 是阿里巴巴基于 single-spa 实现的微前端库,是一个开放式微前端架构,支持当前三大前端框架甚至 jq 等其他项目无缝接入。

    微前端待解决的问题

    • 微前端主应用与子应用如何构建
    • 主应用资源下发给子应用
    • 各应用间动态通信,实时监听,同步数据
    • 微前端线上部署

    微前端主应用与子应用如何构建

    构建主应用

    1. 通过 vue-cli 构建一个主应用工程,安装微前端依赖 qiankunyarn add qiankunnpm i qiankun
    2. 改造 main.js
    // 引入依赖
    import Vue from 'vue';
    import App from './App.vue';
    import store from './store';
    import router from './router';
    import { i18n } from './plugin';
    import {
        registerMicroApps, // 注册子应用
        runAfterFirstMounted, // 第一个子应用加载完毕
        setDefaultMountApp, // 设置默认子应用
        start, // 微服务启动
    } from 'qiankun';
    
    // 公共依赖挂载在window上,目的是传递给子应用
    window.vue = Vue;
    
    // 渲染主应用, #container为主应用根元素
    new Vue({
        i18n,
        store,
        router,
        render: h => h(App)
    }).$mount('#container');
    
    /**
     * 路由监听
     * @param {string} routerPrefix 前缀
     */
    const genActiveRule = (routerPrefix: string) => {
        return (location: Location) => location.pathname.startsWith(routerPrefix);
    };
    
    // 构建子应用, #micro-app为子应用容器
    let microApps = [
        {
            name: 'qiankun-test1',
            entry: '//localhost:7001',
            container: '#micro-app',
            activeRule: genActiveRule('/test1')
        },
        {
            name: 'vue-test2',
            entry: '//localhost:7002',
            container: '#micro-app',
            activeRule: genActiveRule('/test2')
        }
    ];
    // 注册子应用
    registerMicroApps(microApps, {
        // 挂载前回调
        beforeLoad: [
            app => {
                console.log('before load', app);
            }
        ],
        // 挂载后回调
        beforeMount: [
            app => {
                console.log('before mount', app);
            }
        ],
        // 卸载后回调
        afterUnmount: [
            app => {
                console.log('after unload', app);
            }
        ]
    });
    
    // 设置默认子应用,参数genActiveRule('/test1')函数内的参数一致
    setDefaultMountApp('/test1');
    
    // 第一个子应用加载完毕回调
    runAfterFirstMounted(() => {});
    
    // 启动微服务
    start();
    

    App.vue 中,增加一个渲染子应用的容器

    <template>
        <div id="root" class="container">
            <header class="header">header</div>
            <!-- 子应用容器 -->
            <div id="micro-app"></div>
        </div>
    </template>
    <script lang="ts">
    import { Vue, Component } from 'vue-property-decorator';
    
    @Component({
        name: 'Root'
    })
    export default class Root extends Vue {
    
    }
    </script>
    

    主应用的 vue.config.js

    const port = 7000;
    module.exports = {
        // publicPath: process.env.VUE_APP_ROOT_PATH || '/',
        devServer: {
            hot: true,
            port,
            disableHostCheck: true,
            headers: {
                'Access-Control-Allow-Origin': '*'
            }
        }
    };
    

    注意 主应用设置 publicPath: '/' 会造成子应用配置的 publicPath 失效,导致无限循环刷新页面。

    构建子应用

    1. 通过 vue-cli 构建一个子应用工程
    2. 改造 main.js
    import VueRouter from 'vue-router';
    import App from './App.vue';
    import store from './store';
    import routes from './router';
    import { i18n } from './plugin';
    
    Vue.config.productionTip = false;
    
    // 声明变量管理vue及路由实例
    let router = null;
    let instance = null;
    const __qiankun__ = window.__POWERED_BY_QIANKUN__;
    
    if (window.__POWERED_BY_QIANKUN__) {
        // eslint-disable-next-line
        __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    
    // 导出子应用生命周期 挂载前
    export async function bootstrap(props) {
        console.log(props)
    }
    
    // 导出子应用生命周期 挂载后
    export async function mount(props) {
        // 实例化router
        router = new VueRouter({
            base:  '/', // 也可写子应用路径
            mode: 'history',
            routes // 通过主应用传递的props来获取,可实现动态路由
        });
        // 实例化子应用
        instance = new Vue({
            i18n,
            store,
            router,
            render: h => h(App)
        }).$mount('#app');
    }
    
    // 导出子应用生命周期 卸载后
    export async function unmount() {
        instance.$destroy();
        instance = null;
        router = null;
    }
    
    // 单独开发环境
    __qiankun__ || mount();
    

    修改 router.js 中

    import { RouteConfig } from 'vue-router';
    import Home from '@/views/Home.vue';
    
    const routes: Array<RouteConfig> = [
      {
        path: '/',
        name: 'home',
        component: Home
      },
      {
        path: '/about',
        name: 'about',
        component: () => import('@/views/About.vue')
      }
    ];
    
    export default routes;
    

    注意:这里导出的是路由数组,而不是 rouer 实例

    子应用的 vue.config.js

    const { name } = require('./package');
    const path = require('path');
    function resolve(dir) {
        return path.resolve(__dirname, dir);
    }
    
    const port = 7001;
    const isDev = process.env.NODE_ENV === 'development';
    
    module.exports = {
        publicPath: isDev ? `//localhost:${port}` : '/',
        // 自定义webpack配置
        configureWebpack: config => {
            config.output.library = `${name}-[name]`;
            config.output.libraryTarget = 'umd';
            config.output.jsonpFunction = `webpackJsonp_${name}`;
            
            // 排除依赖项,不打包vue
            config.externals = {
                vue: 'vue'
            };
        },
        // 对内部的 webpack 配置(比如修改、增加Loader选项)(链式操作)
        chainWebpack: config => {
            // 配置依赖别名
            config.resolve.alias
                .set('vue', resolve('src/config/window/vue'));
        },
        devServer: {
            port,
            hot: true,
            disableHostCheck: true,
            // 开发环境需要配置,解决跨越
            headers: {
                'Access-Control-Allow-Origin': '*'
            }
        }
    };
    

    src/config/window/vue.ts, 引入window上的vue并导出

    const vue = window.vue;
    
    export default vue;
    
    

    主应用资源下发给子应用

    实际项目中,父子应用之间会公用一些方法,组件。可以把公共的方法和组件放在主应用中,下发到子应用以供使用

    主应用的 main.js

    import CustomComponent from '@/components/custom';
    import storage from '@/utils/storage';
    import filters from '@/utils/filters';
    import * as directives from '@/utils/directives';
    
    // 定义传入子应用的数据
    const msg = {
        storage,
        filters,
        directives,
        CustomComponent
    };
    
    // 注册子应用
    registerMicroApps([
        {
            name: 'qiankun-test1',
            entry: '//localhost:7001',
            container: '#micro-app',
            activeRule: genActiveRule('/test1'),
            props: msg
        }
    ]);
    

    子应用的 main.js 接受 props

    export async function bootstrap({ storage, filters, directives, CustomComponent }) {
        // 子应用全局挂载storage
        Vue.prototype.$storage = storage;
        // 注册全局过滤器
        for (const key in filters) {
            Vue.filter(key, (...args: Array<unknown>) => {
                return filters[key](...args);
            });
        }
        // 挂载全局指令
        Object.keys(directives).forEach(key => {
            Vue.directive(key, (directives as { [key: string]: DirectiveOptions })[key]);
        });
        // 注册主应用下发的组件
        Vue.use(CustomComponent);
    }
    

    各应用间动态通信, 实时监听, 同步数据

    qiankun 官方也提供了 api 去解决这个问题,不过使用起来不是很方便,我使用的是 rxjs 去解决应用间通信的需求

    1. 主应用中安装并引入 rxjs,并实例化
    import { Subject } from 'rxjs';
    const pager = new Subject();
    
    // 在主应用注册呼机监听器,监听来自其他应用的广播
    pager.subscribe(v => {
        console.log(v);
    });
    
    export default pager;
    
    1. 然后在主应用 main.js 中引入呼机,将呼机下发给子应用
    import pager from '@/util/pager';
    
    // 注册子应用
    registerMicroApps(
        [{
              name: 'qiankun-test1',
              entry: '//localhost:7001',
              container: '#micro-app',
              activeRule: genActiveRule('/test1'),
              props: { pager } // 将pager传递给子应用
        }]
    );
    
    1. 在子应用中注册呼机
    export async function bootstrap({ pager }) {
        // 子应用注册呼机, 监听其他应用的广播
        pager.subscribe((v) => {
            console.log(v);
        });
        // 将呼机挂载在vue实例
        Vue.prototype.$pager = pager;
    }
    
    1. 在各应用中使用呼机动态传递信息
    // 在某个应用里调用.next方法呼叫其他应用
    this.$pager.next({
        from: 'qiankun-test1',
        data: 'test1 呼叫其他菜鸡'
    });
    

    微前端线上部署

    以上做了这么多,能够部署到服务器上才算成功。我这里用的是 nginx

    server {
        listen          7000;
        server_name     localhost;
    
        location / {
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
            add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    
            root /Users/sunweijie/qiankun-app/master/dist/;
            index index.html index.htm;
    
            try_files $uri $uri/ /index.html;
        }
    }
    
    server {
        listen          7001;
        server_name     localhost;
        root            /Users/sunweijie/qiankun-app/subapp-rights/dist/;
    
        location / {
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
            add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    
            try_files $uri $uri/ /index.html;
        }
    }
    
    server {
        listen          7002;
        server_name     localhost;
        root            /Users/sunweijie/qiankun-app/subapp-common/dist/;
    
        location / {
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
            add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    
            try_files $uri $uri/ /index.html;
        }
    }
    

    我这里把关键性的几个要点给列举了下,在真正的实践过程中还是会踩到不少坑的,欢迎大家一起来入坑
    附上 github 的工程地址:qiankun-container,顺手给楼主点个 star 吧

    相关文章

      网友评论

        本文标题:vue+ts的微前端实践

        本文链接:https://www.haomeiwen.com/subject/gfqadktx.html