关于javascript:微前端的生产实践和我的使用姿势

4次阅读

共计 10325 个字符,预计需要花费 26 分钟才能阅读完成。

前言

笔者在 2019 年末的时候开始理解微前端这个货色、过后看到两个微前端的框架、别离是 single-spa 和 qiankun、在这两种技术去做抉择去学习、看到 qiankun 是基于 single-spa 二次封装的、文档简洁明了、应用简略前面决定学习 qiankun!并把本人踩的坑记录下来

什么是微前端

本人在服务器部署的一个微前端 demo

1、技术栈无关

2、主框架不限度接入利用的技术栈,微利用具备齐全自主权

3、独立开发、独立部署

4、微利用仓库独立,前后端可独立开发,部署实现后主框架主动实现同步更新

需要剖析

  • 除了这些、从我的项目的需要和零碎的数量微前端非常适合咱们、咱们一共有七个零碎、每个用户因为角色权限、所治理的零碎也是不一样的、张三负责两个零碎权限、李四负责一个零碎、也可能王五负责一个零碎的其中某几个菜单权限等 … 如果全副零碎写到一个我的项目可想而至 … 代码量 … 我的项目保护.. 性能. 都是十分难折腾!上面放一张咱们的零碎 UI 图


因为那啥所以打了码、顶部是所有零碎、左侧是以后零碎的菜单栏、从 UI 的设计图上看这个我的项目是很适宜微前端!前面我会用基座(微前端环境)和子利用与主利用去介绍我的踩坑之路 -????

技术选型与我的项目的整体规划

  • 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 

脚手架好了之后咱们须要装置 qiankun

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 疏忽检测的全局变量

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 页面

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

问题四、子利用给其余利用传输数据时候、主利用外面没有提前定义通信的 key、所以接管不到数据、解决:在主利用注册通信办法 initGlobalState({...}) 定义好须要通信的 key 就好、按定义好的约定进行传输数据

目前就遇到这些问题、也欢送留言区评论本人遇到的问题、顺便把 qiankun 的常见问题贴出来 https://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 进行的写的一篇踩坑文章、我置信在重构公司我的项目的时候踩的坑必定不止这些到时候我对立在、遇到的问题那进行补充!加油、折腾人

正文完
 0