qiankun微前端落地实践
背景
A项目集成了多条业务线的项目,但是共同使用一个git仓库和一套发布流水线,代码组织比较混乱。本次需要引入一个新的业务线,如果直接在A项目开发,将导致原项目更加复杂,更加混乱,故计划将这个项目单独发布,以微前端的方式嵌入A项目。
方案
微前端(qiankun)
- 技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 - 独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 - 独立运行时
每个微应用之间状态隔离,运行时状态不共享
架构
步骤
-
将现有A项目改造为主应用
-
安装qiankun
-
在主应用中注册微应用
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。
const ENV = getENV(); const genActiveRule = hash => location => location.hash.startsWith(hash); let microApp = { name: "microApp", entry: { localhost: "//localhost:8080", test: "//test.com", online: "//online.com" }[ENV], container: "#microApp", activeRule: genActiveRule("#/microApp") };
-
微应用需要使用主应用的菜单和工具栏,故微应用在entry.vue文件中加载
// entry.vue import {start} from 'qiankun'; mounted() { // 启动微应用 if (!window.qiankunStarted) { window.qiankunStarted = true; start(); } }
-
主应用要预留子应用加载的路由入口,参见常见问题
/** * @description 子应用的路由 */ { path: '/microApp/*', name: 'microApp', component: resolve => require(['../views/entry.vue'], resolve), },
-
404页面的配置
首先不应该写通配符 * ,可以将 404 页面注册为一个普通路由页面,比如说 /404 ,然后在主应用的路由钩子函数里面判断一下,如果既不是主应用路由,也不是微应用,就跳转到 404 页面。
const childrenPath = ['microApp']; router.beforeEach((to, from, next) => { if(to.name) { // 有 name 属性,说明是主应用的路由 next() } if(childrenPath.some(item => to.path.includes(item))){ next() } next({ name: '404' }) })
-
因命名冲突,主应用中所有的window.Vue改为window.Vue2
-
-
将新的项目改造为微应用
-
暴露出三个钩子供主应用加载
// 父应用加载子应用,子应用必须暴露三个接口:bootstrap、mount、unmount export async function bootstrap(props) { }; export async function mount(props) { render(props) } export async function unmount(props) { instance.$destroy(); }
-
保证子应用既可以独立运行,又可以作为微应用运行
let instance = null function render(props) { instance = new Vue({ router, store, render: (h) => h(App), }).$mount("#app"); // 这里是挂载到自己的html中 基座会拿到这个挂载后的html 将其插入进去 } if (window.__POWERED_BY_QIANKUN__) { // 动态添加publicPath __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } if (!window.__POWERED_BY_QIANKUN__) { // 默认独立运行 render(); }
-
允许跨域并使用umd方式打包
// vue.config.js module.exports = { devServer: { port: '8080', headers: { 'Access-Control-Allow-Origin': '*' } }, configureWebpack: { output: { library: 'microApp', libraryTarget: 'umd' } } };
-
-
路由模式
- 主应用保持hash模式不变(主应用是vue项目,子应用是react时,主应用使用hash模式会导致子应用加载不出来,故后来将主应用改为history模式)
- 微应用也使用hash模式,但path需要加上前缀“/microApp”
-
难点
-
部署方案:
-
主应用和子应用分别部署在不同docker
# dockerfille FROM registry.cn-beijing.aliyuncs.com/xxxxx/tengine2.2.1:v0.3 ADD ./dist /usr/share/nginx/html/dist WORKDIR /usr/share/nginx/html COPY default.conf /etc/nginx/conf.d/default.conf
# dockerfile.dapper FROM registry.cn-beijing.aliyuncs.com/xxxx/node:14.15-buster-slim USER root ENV DAPPER_SOURCE /code ENV DAPPER_RUN_ARGS "-v /tmp/mvnrepo:/root/.m2/repository" ENV DAPPER_ENV APP_ENV APP_GROUP APP_NAME APP_VERSION ENV HOME ${DAPPER_SOURCE} WORKDIR ${DAPPER_SOURCE} ENTRYPOINT ["bash","./scripts/entry"] CMD ["ci"]
-
子应用申请域名
-
-
子应用独立出来
业务代码,接口,构建,部署
-
微应用的字体文件访问404
-
原因是 qiankun 将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。而 css 文件一旦打包完成,就无法通过动态修改 publicPath 来修正其中的字体文件和背景图片的路径。
-
解决方案
const os = require('os') let publicPath = process.env.BASE_URL let ip = '' let port = '8080' if (process.env.npm_lifecycle_event === 'serve') { const network = os.networkInterfaces()['vEthernet (Default Switch)'] || os.networkInterfaces().en0 ip = network[1].address publicPath = `http://${ip}:${port}/` } module.exports = { lintOnSave: process.env.NODE_ENV === 'development', publicPath, chainWebpack: (config) => { config.resolve.symlinks(true) const fontRule = config.module.rule('fonts') fontRule.uses.clear() fontRule .use('file-loader') .loader('file-loader') .options({ name: 'fonts/[name].[hash:8].[ext]', publicPath, }) .end() const imgRule = config.module.rule('images') imgRule.uses.clear() imgRule .use('file-loader') .loader('file-loader') .options({ name: 'img/[name].[hash:8].[ext]', publicPath, }) .end() }, }
-
-
所有静态资源(html,css,js,img)需要设置cors
server { listen 80;#监听端口 server_name _; #监听地址、域名 location / { root /usr/share/nginx/html/dist/; try_files $uri $uri/ /index.html; add_header Cache-Control "no-cache, no-store";#不使用缓存 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'; if ($request_method = 'OPTIONS') { return 204; } } location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { root /usr/share/nginx/html/dist/;#站点目录 add_header Cache-Control 'max-age=1296000'; add_header Access-Control-Allow-Origin *; } location /api { } }
-
todos
主应用的页码权限控制有bug,权限控制只是隐藏了菜单,如果知道页面的路径就可绕过
- 解决方案1:在beforeRouterEnter钩子中判断用户是否有这个页面权限,可以一定程度上解决这个问题
- 解决方案2:根据后台返回的权限信息,动态生成router,代码复杂一些,但是比方案1效果更好一些