微前端的生产实践和个人使用姿式

前言javascript

笔者在2019年底的时候开始了解微前端这个东西、当时看到两个微前端的框架、分别是 single-spa 和 qiankun、在这两种技术去作选择去学习、看到qiankun是基于single-spa二次封装的、文档简洁明了、使用简单后面决定学习qiankun!并把本身踩的坑记录下来 css

什么是微前端

本身在服务器部署的一个微前端demohtml

一、技术栈无关前端

二、主框架不限制接入应用的技术栈,微应用具有彻底自主权vue

三、独立开发、独立部署java

四、微应用仓库独立,先后端可独立开发,部署完成后主框架自动完成同步更新node

需求分析

  • 除了这些、从项目的需求和系统的数量微前端很是适合咱们、咱们一共有七个系统、每一个用户由于角色权限、所管理的系统也是不同的、张三负责两个系统权限、李四负责一个系统、也可能王五负责一个系统的其中某几个菜单权限等...若是所有系统写到一个项目可想而至...代码量...项目维护..性能.都是很是难折腾!下面放一张咱们的系统UI图

因为那啥因此打了码、顶部是全部系统、左侧是当前系统的菜单栏、从UI的设计图上看这个项目是很适合微前端!后面我会用基座(微前端环境)和子应用与主应用去介绍个人踩坑之路-😄react

技术选型与项目的总体规划

  • vue、element、webpack、websocket、eslint、babel、qiankun2.0
  • 支持子应用独立运行和可运行在微前端基座方式
  • 主应用使用cdn统一管理公共静态资源,全部子应用运行在基座上时共享此静态资源,大幅减少子应用体积,减小带宽消耗,减小重复资源消耗,大幅加快项目加载速度
  • 应用与应用之间可进行通讯和跳转
  • 应用独立维护、互不依赖不耦合
  • 项目拆分但和单体的开发模式应该是差很少的、好比启动、打包、安装、依赖、部署(一键模式)
  • 编写一键部署脚本、先部署至测试服务器、测试经过直接发行到生产环境
  • 项目状态、公共数据的维护

主应用环境搭建(基座)

  • 主应用(vue脚手架搭建)、由于打算核心公共模块采用cdn方式、因此脚手架选择 : Default ([Vue 2] babel, eslint) 主应用须要作的事情是: 对qiankun框架单独模块化封装导出核心方法、配置cdn方式加载核心模块 、配置 eslint 忽略指定全局变量、配置webpack的externals排除某些依赖,使用 cdn 资源代替

开始

vue create main-app 
复制代码

脚手架好了以后咱们须要安装 qiankunlinux

yarn add qiankun
复制代码
  • 采用cdn方式去加载公共核心模块好比vue、vuex、vue-route..等、这样作的目的是让子应用去使用主应用加载好的公共模块、同时减小项目打包体积大小!因此咱们须要在main.js里的 import Vue from 'vue' 进行删除、其余的vue-router、vuex、Axios也都是同样的操做统一不使用 node_modules的依赖,而后在主应用的 public的index.html、引入公共模块(下方的js) 下面是我本身玩demo的时候储存在个人对象服务器的经常使用公共文件、建议下载到本身本地玩
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/vue/vue.js"></script>
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/vue/vue-router.js"></script>
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/vue/vuex.js"></script></head>
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/axios/axios.min.js"></script>
<link rel="stylesheet" href="https://gf-cdn.oss-cn-beijing.aliyuncs.com/element/index.css">
<script src="https://gf-cdn.oss-cn-beijing.aliyuncs.com/element/index.js"></script>
复制代码

由于使用了 eslint 缘由检测到没有引入vue、因此咱们要全局配置忽略咱们经过cdn方式引入的模块、在 .eslintrc.js 添加一个 globals 忽略检测的全局变量webpack

globals: {
   "Vue": true,
   "Vuex": true,
   "VueRouter": true,
   'axios':true
 }
复制代码

配置了这个只是把代码校验忽略检测某些变量、咱们还须要配置下 webpack 的 externals externals介绍、简单来说就是 打包的时候排除某些依赖,使用 cdn 资源代替 在vue.config.js里面配置

module.exports = {
  publicPath: '/',
  outputDir: 'app',
  assetsDir: 'static', 
  ......
  configureWebpack: {
        externals: {
            'element-ui':'ELEMENT',
            'vue':'Vue',
            'vue-router':'VueRouter',
            'vuex': 'Vuex',
            'axios':'axios'
        }
    }
 }
复制代码

这样咱们的cdn方式加载核心模块就行了、接下来就是配置 qiankun

  • quankun配置

    • src里面新建一个core文件夹、分别建立 app.config.js(管理子应用的注册信息)qiankun.js(这里统一导出启动qiankun的方法) 还有 app.store.js(管理qiankun的通讯方法)

app.config.js

const apps = [
    {
      name: "subapp-sys", //微应用的名称
      defaultRegister: true, //默认注册
      devEntry: "http://localhost:6002",//开发环境地址
      depEntry: "http://108.54.70.48:6002",//生产环境地址
      routerBase: "/sys", //激活规则路径
      data: []  //传入给子应用的数据
    },
]
export default apps;
复制代码

qiankun.js

import { registerMicroApps, runAfterFirstMounted, setDefaultMountApp, start, initGlobalState } from "qiankun";
const appContainer = "#subapp-viewport"; //加载子应用的dom
import appStore from './app.store' 
const quanKunStart = ( list ) =>{
    let apps = [];      //子应用数组盒子
    let defaultApp = null; // 默认注册应用路由前缀
    let isDev = process.env.NODE_ENV === 'development';
    list.forEach( i => { 
        apps.push({
            name: i.name, //微应用的名称
            entry: isDev ? i.devEntry : i.depEntry, //微应用的 entry 地址
            container: appContainer, //微应用的容器节点的选择器或者 Element 实例
            activeRule: i.routerBase, //微应用的激活规则路径 /login/xxx /sys/xxx
            props: { routes: i.data, routerBase: i.routerBase } //子应用初次挂载传入给子应用的数据
        })
        //初始化第一个加载的应用
        if (i.defaultRegister) defaultApp = i.routerBase;
    });
    //qiankun路由配置
    registerMicroApps(
        apps,
        {
            beforeLoad: [
                app => {
                    console.log('[主应用生命周期] before', app.name);
                },
            ],
            beforeMount: [
                app => {
                    console.log('[主应用生命周期] before', app.name);
                },
            ],
            afterUnmount: [
                app => {
                    console.log('[主应用生命周期] after', app.name);
                },
            ]
        },
    )
    //默认加载第一个子应用
    setDefaultMountApp( defaultApp );
    //启动微前端
    start();
    //第一个微应用 mount 后须要调用的方法
    runAfterFirstMounted(() => { console.log( defaultApp +'--->子应用开启成功' ) });
    //启动qiankun通讯机制
    appStore( initGlobalState );
}

export default quanKunStart;

复制代码

app.store.js

let DISPATCHAPPLYMESSAGE = null;
let GETAPPLYMESSAGE = null;
const appStore = ( initGlobalState ) => {
    //定义应用之间所接收的key、否则主应用不接收数据
    const initialState = {  
        data: '给子应用的测试数据',
        token: '',
        appsRefresh: false,
    };
    const { onGlobalStateChange, setGlobalState } = initGlobalState( initialState );
    dispatchApplyMessage = setGlobalState;
    getApplyMessage = onGlobalStateChange;
}
//导出应用通讯方法
export {
    DISPATCHAPPLYMESSAGE,
    GETAPPLYMESSAGE
}
export default appStore;
复制代码

qiankun的通讯是 initGlobalState 这个方法返回的 onGlobalStateChange, setGlobalState 接收和派发方法、另外须要注意的是 只有主应用注册了 initGlobalState 才会附加到子应用接收的props里面、主应用没注册通讯方法是没有的 还有一个就是 若是你没先在 initGlobalState方法传入定义好的通讯key、那其余应用传入给主应用的数据是接收不到的

  • 在主应用的App.vue添加子应用的渲染区域
<template>
    <div class="home-container"> <p>主应用内容</p> <div class="page-conten"> <!-- 子应用渲染区 --> <div id="subapp-viewport" class="app-view-box"></div> </div> </div>
</template>
复制代码

main.js

import App from './App.vue'
Vue.config.productionTip = false

import Apps from './core/app.config'
import qianKunStart from './core/qiankun'
qianKunStart(Apps)

new Vue({
  render: h => h(App),
}).$mount('#app')
复制代码

整个主应用(基座)配置完、能够发现并无什么难度、qiankun给我提供了直接开箱即用的方便、剩下的咱们就是去配置子应用了、配置子应用相对来讲还要更简单些、接下来就是子应用的环境搭建了

子应用环境

  • 第一步使用官方脚手架把项目建立好、和主应用同理选择默认的 Default ([Vue 2] babel 进行建立项目
vue create subapp-sys
复制代码
  • 第二步咱们改造下脚手架默认的模块和打包后的格式配置、还有给qiankun导出对应的生命周期函数 修改打包配置 - vue.config.js
const { name } = require('./package');
module.exports = {
  devServer: {
    hot: true,
    disableHostCheck: true,
    port:6002,
    overlay: {
        warnings: false,
        errors: true,
    },
    headers: {
        'Access-Control-Allow-Origin': '*',
    },
    //防止单体项目刷新后404
    historyApiFallback:true,
},
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',// 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`,
    },
  },
};
复制代码
  • 第三步同src下建立一个导出qiankun的js文件、统一管理,名叫 life-cycle.js
import App from "./App.vue";
import store from "./store";
import selfRoutes from "./router";
//导入官方通讯方法 和 主应用的同样把应用通讯封装到一个js文件独立管理
import appStore from "./utils/app-store";

const __qiankun__ = window.__POWERED_BY_QIANKUN__;
let router = null;
let instance = null;

/** * @name 导出qiankun生命周期函数 */
const lifeCycle = () => {
  return {
    async bootstrap() {},
    //应用每次进入都会调用 mount 方法,一般咱们在这里触发应用的渲染方法
    async mount( props ) {
        // 注册应用间通讯
        appStore(props);
        // 注册微应用实例化函数
        render(props);
    },
    //微应用卸载
    async unmount() {
        instance.$destroy?.();
        instance = null;
        router = null;
    },
  //主应用手动更新微应用
    async update(props) {
        console.log("update props", props);
    }
  };
};

//子应用实例化函数 routerBase container是经过主应用props传入过来的数据
const render = ({ routerBase, container } = {}) => {
    Vue.config.productionTip = false;
    router = new VueRouter({
        base: __qiankun__ ? routerBase : "/",
        mode: "history",
        routes: selfRoutes
    });
    instance = new Vue({
        router,
        store,
        render: h => h(App)
    }).$mount(container ? container.querySelector("#sys") : "#sys");
};

export { lifeCycle, render };
复制代码
  • 第四步 接下来src目录新增 public-path.js
if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
复制代码
  • 最后main.js 引入封装
import "./public-path";
import { lifeCycle, render } from "./life-cycle";
/** * @name 导出微应用生命周期 */
const { bootstrap, mount, unmount } = lifeCycle();
export { bootstrap, mount, unmount };

/** * @name 不在微前端基座独立运行 */
const __qiankun__ = window.__POWERED_BY_QIANKUN__;
__qiankun__ || render();
复制代码

子应用 life-cycle.js 中引入了 import appStore from "./utils/app-store";这里的app-store和主应用的同样、在相同的位置从新复制一份便可

个人整个项目结构(使用了vue + react + qiankun)

遇到的问题

qiankun 环境搭建好了,接下来 分别 进入主应用和子应用启动项目 yarn serve 而后访问主应用、没有问题的话应该两个项目的页面都出来了、接下来我说下我作集成时候遇到的问题

问题一,挂载微应用的容器节点找不到 #subapp-viewport 、添加一个你设置应用挂载container的dom节点就好

问题二,主应用代理的地址若是和子应用proxy的接口匹配若是和路由前缀同样的话、页面进行一个刷新操做后的一个页面错误 感谢wl提早踩坑、哈哈哈

配置主应用vue.config.js的devServer为proxy添加一个函数绕过代理、浏览器请求,但愿返回的是HTML页面

问题三,某个子应用服务没启动、没有获取到资源

问题4、子应用给其余应用传输数据时候、主应用里面没有提早定义通讯的key、因此接收不到数据、解决:在主应用注册通讯方法 initGlobalState({...}) 定义好须要通讯的key就好、按定义好的约定进行传输数据

目前就遇到这些问题、也欢迎留言区评论本身遇到的问题、顺便把qiankun的常见问题贴出来 qiankun.umijs.org/zh/faq

编写应用指令脚本一键式 [ 启动、依赖安装、打包 】

上面讲到、咱们须要一个一个应用下进行yarn serve下、这样是很不方便的、应用一多咱们启动就成了很麻烦的一件事情、因此咱们须要从新写一个yarn脚本文件、目的就是让他去自动帮咱们执行脚本命令 (其中包括 启动 打包 安装依赖)

  • 第一步在整个应用项目下生成一个package.json配置scripts脚本文件
yarn init //而后按提示执行下去
复制代码
  • 添加scripts脚本配置 下面是定一个start指令而后去执行config下的start.js
"scripts": {
  "start":"node config/start.js"
}
复制代码
  • 第二步在、咱们须要在package.json同级下建立一个config文件夹、同时往文件里面添加一个start.js
mkdir config
cd config 
touch start.js
复制代码
  • 第三步往start.js随便输出一个console.log('yarn serve'),而后在整个项目启动终端执行一下 yarn start 正常输出 yarn serve 、而后咱们须要开始编写一键启动脚本 需求就是执行脚本、脚本自动帮咱们在每一个项目中去执行 yarn serve

  • start.js

const fs = require('fs');
const path = require('path');
const util = require('util');
const sub_app_ath = path.resolve();
const sub_apps = fs.readdirSync(sub_app_ath).filter(i => /^sub|main/.test(i));
console.log('\033[42;30m 启动中 \033[40;32m 即将进入全部模块并启动服务:' + JSON.stringify(sub_apps) + 'ing...\033[0m')
const exec = util.promisify( require('child_process').exec );
async function start() {
sub_apps.forEach( file_name => {
  exec('yarn serve', { cwd: path.resolve( file_name )});
});
};
start();
setTimeout( () =>{
console.log('\033[42;30m 访问 \033[40;32m http://localhost:6001 \033[0m')
},5000)

复制代码

先经过正则读取到主应用和子应用文件夹名称、而后使用 child_process模块异步建立子进程 经过这个返回的方法咱们能够去执行一个 指令 而且传入一个在那执行的路径 util.promisify把方法封装成promise返回形式

这里我有个小问题、我有尝试过去找每个子应用是否成功开启的操做、可是没找到合适的方法、但愿有人知道的能够告知我下啦、谢谢、因此我在最后写了一个setTimeout....

好啦、目前一键启动就写完了、其余的都是同样的操做、只是建立的文件夹和scripts的脚本命令改下、哦对还有exec下的指令换成对应的 剩下就是执行shell脚本进行服务器上传部署

shell脚本完成自动打包和上传至服务器、关于shell语法你们能够看 菜鸟shell教程

  • 在总体项目下新建一个 deploy.sh 文件

deploy.sh

set -e
shFilePath=$(cd `dirname $0`; pwd)
# 系统列表名称
sysList=('app' 'car' 'login' 'sys' 'user' 'all')
IP="106.54.xx.xx"
uploadPath="/gf_docker/nginx/web"
#获取当前分支
branch=$(git symbolic-ref --short HEAD)
#开始
echo "\033[35m 当前分支是:${branch} \033[0m"
read -p $'\033[36m 准备进行自动化部署操做、是否继续 y or n \033[0m ' isbuild
if [ "$isbuild" != 'y' ];then
    exit
fi
echo "\033[36m 目前四个个系统 \033[0m \033[35m【 ${sysList[*]} 】 \033[0m "
read -p $'\033[36m 请选择部署的项目 或 输入 all \033[0m' changeSysName
isSys=$(echo "${sysList[@]}" | grep -wq "${changeSysName}" &&  echo "yes" || echo "no")
#是否存在系统
if [ "$isSys" == 'no' ];then
    echo "\033[31m 没有对应的系统、已退出 \033[0m"
    exit
fi

#没有buildFile文件夹的话就新建一个
if [ -d "$shFilePath/buildFile" ]; then
    rm -rf './buildFile/'
    mkdir "buildFile"  
else
    mkdir "buildFile" 
fi;

#项目文件夹名称
fileName=""
#打包
function build() {
    cd $1
    echo "\033[32m $1准备打包... \033[0m" 
    yarn build 
    echo $1/$2
    mv $shFilePath/$1/$2 $shFilePath/buildFile
    echo "\033[32m $1打包成功、包移动至buildFile \033[0m" 
}
#上传服务器
function uploadServe() {
    echo "\033[32m 准备上传服务器,地址:$uploadPath \033[0m"
    rsync -a -e "ssh -p 22" $shFilePath/buildFile*  root@$IP:$uploadPath
    echo "\033[32m 自动化部署成功! \033[0m"
}
#单个项目部署文件名转换
function getFileName() {
    case $1 in
        'app')
            fileName="main-app";;
        'car')
            fileName="subapp-car";;
        'login')
            fileName="subapp-login";;
        'sys')
            fileName="subapp-sys";;
        'user')
            fileName="subapp-user";;
        *)
            echo "error"
    esac
}
#按需打包
if [ "$changeSysName" == 'all' ];then
    for i in "${sysList[@]}"; do
        if [ "$i" != 'app' ];then
            cd ..
        fi
        if [ "$i" != 'all' ];then
            getFileName $i
            build $fileName $i
        fi
    done
else
    getFileName $changeSysName
    build $fileName $changeSysName
fi

#部署
uploadServe 
复制代码

语法和菜鸟现学的、也只是代替双手进行一系列的操做、上传服务器的时候须要输入下密码、若是不想输入可在服务端配置密钥、相似git同样!

最后咱们也能够经过配置、脚本指令去执行咱们的sh文件、在package.json的scripts添加一个"deploy": "sh deploy.sh" 最后须要部署测试环境的时候直接执行 yarn deploy

最后

最后我要去进行项目的重构工做了、这些也是我下班后本身经过整理本身玩的demo进行的写的一篇踩坑文章、我相信在重构公司项目的时候踩的坑确定不止这些到时候我统一在、遇到的问题那进行补充!加油、折腾人

相关文章
相关标签/搜索