vue项目'微前端'qiankun.js的实战攻略
本篇介绍
关于微前端
的大概念大家应该听过太多了, 这里我就大白话阐述一下, 比如我们新建三个vue工程
a、b、c, a
负责导航模块, b
负责列表页面, c
负责详情页面, 然后我们可以通过微前端
技术把他们组合在一起形成一个完整项目
。
本篇文章不会讲述很深入的细节操作, 但会讲述项目搭建到项目上线的全环节, 如果你这些都会了那么其他的问题就不是太大阻碍了。
一定要明确一点, 微前端
在很多场景都是不适用的, 千万不要强行使用这门技术, 在本篇文章里我会一点点的阐述什么场景不适用以及为什么不适用。
1. 微前端qiankun.js
简简简简介
qiankun.js
是当前最出色的一款微前端
实现库, 他帮我们实现了css隔离
、js隔离
、项目关联
等功能, 文章的后面都会有所涉及的现在就让我们开始实战吧。
2. 本次的项目结构一主二附
一共三个vue项目
, 第一个container
项目负责导航模块, 第二个web1
第三个web2
, container
项目里面有个subapp
文件夹, 里面存放着web1 & web2
两个项目, 这样以后我们可以随便添加web3,web4....
都放在subapp
文件夹即可。
3. 安装qiankun
配置项目加载规则
在我们的容器项目container
里面安装qiankun
如下命令:
$ yarn add qiankun # 或者 npm i qiankun -S
打开container
项目的App.vue
文件我们把导航重定义一下:
/w1
与/w2
路由地址分别激活web1
工程与web2
工程。
我们新增一个id为"box"的元素, 接下来我们引入的web1
工程就会插入到这个元素中。
把Home.vue
页面代码改掉:
我是`container`工程
打开container
项目的main.js
文件写入配置。
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vueApp2',
entry: '//localhost:8083',
container: '#box',
activeRule: '/w2',
},
{
name: 'vueApp1',
entry: '//localhost:8082',
container: '#box',
activeRule: '/w1',
},
]);
start();
参数解析:
name
: 微应用的名称,微应用之间必须确保唯一, 方便后期区分项目来源。entry
: 微应用的入口也就是当满足条件的时候, 我要激活的目标微应用的地址(也可以是其他形式比如html
片段, 但本篇主要讲url地址这种形式)。container
: 激活微应用的时候我们要把这个目标微应用放在哪里, 上面代码的意思就是把激活的微应用放在id为'box'
的元素里面。activeRule
:微应用的激活规则(有很多种写法甚至是函数形式), 上面代码就是当路由地址为/w1
时激活。
4. 配置子项目
的main.js
以配置web1
项目为例, web2
与其类似, 在main.js
中导出自己的生命周期函数。
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
let instance = null;
function render() {
instance = new Vue({
router,
render: h => h(App)
}).$mount('#web1') // 框架会拿到完整的dom结构, 所以index.html里面的id也要改一下
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('bootstrap');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount() {
render()
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
instance.$destroy()
}
把web1 >public >index.html
中的div元素id从app
改为web1
, 因为要多个项目合成一个项目, 所以id最好还是不要重复。
web1
的vue.config.js
module.exports = {
devServer: {
port: 8082, // web2里面改成8083
},
}
现在我们要分别进入container
, web1
与web2
里面运行yarn serve
命令, 但是这样运行命令真的好麻烦, 接下来我就介绍一种更工程化的写法。
5. npm-run-all
npm-run-all
是用来通过执行一条语句来达到执行多条语句的效果的插件。
$ npm install npm-run-all --save-dev
# or
$ yarn add npm-run-all --dev
改装我们的container
工程中的package.json
文件。
"scripts": {
"serve": "npm-run-all --parallel serve:*",
"serve:box": "vue-cli-service serve",
"serve:web1": "cd subapp/web1 && yarn serve",
"serve:web2": "cd subapp/web2 && yarn serve",
"build": "npm-run-all --parallel build:*",
"build:box": "vue-cli-service build",
"build:web1": "cd subapp/web1 && yarn build",
"build:web2": "cd subapp/web2 && yarn build"
},
我解释一下:
运行: yarn serve
系统会执行scripts
里面所有的头部为serve:
的命令, 所以就会实现一个命令运行三个项目, 这里顺手把build
命令也写了。
其他扩展玩法:
- serial: 多个命令按排列顺序执行,例如:npm-run-all --serial clean lint build:**
- continue-on-error: 是否忽略错误,添加此参数 npm-run-all 会自动退出出错的命令,继续运行正常的
- race: 添加此参数之后,只要有一个命令运行出错,那么 npm-run-all 就会结束掉全部的命令
上述准备工作都做完了, 我们可以启动项目试试了。
6. 请求子项目
竟然跨域
需要在web1
与web2
两个项目vue.config.js
里加上如下配置就不报错了:
devServer: {
port: 8082,
// 由于会产生跨域, 所以加上
headers: {
'Access-Control-Allow-Origin': "*"
}
},
之所以会有这种跨域的报错是因为qiankun
内部使用fetch
请求的资源, 当前毕竟是启动了三个不同的node服务, 外部html页面请求其资源还是会跨域的, 所以需要设置允许所有源。
我们为web1
与web2
设置一下样式, 结果如下:
- 但这些仅仅是个开始而已, 因为各种问题马上纷至沓来。
7. 区分在是否在主应用内
我们有时候需要单独开发web1
, 此时我们并不依赖container
项目, 那么我们就要把main.js
改装一下:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
let instance = null;
function render() {
instance = new Vue({
router,
render: h => h(App)
}).$mount('#web1')
}
if (window.__POWERED_BY_QIANKUN__) {
window.__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap() {
console.log('bootstrap');
}
export async function mount() {
render()
}
export async function unmount() {
instance.$destroy()
}
逐句解释:
window.__POWERED_BY_QIANKUN__
: 当前环境是否为qiankun.js
提供。window.__webpack_public_path__
: 等同于output.publicPath
配置选项, 但是他是动态的。window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
:qiankun.js
注入的公共路径。
判断当前环境为单独开发的环境就直接执行render
方法, 如果是qiankun
的容器内, 那么需要设置publicPath
, 因为qiankun
需要把每个子应用都区分开, 然后引入容器项目内, 这样我们就可以单独开发web1
项目了。
8. 子应用
路由跳转与vue-router
的异步组件小bug
在配置router
的时候我们经常会将页面写成异步加载:
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'),
在web2
项目中的home页面, 我增加一个按钮跳到about
页面:
上述代码不可以直接用this.$router.push
, 这样会与qiankun.js
的路由分配冲突, 官网上说会出现404
这种情况, 所以建议我们直接用 window.history.pushState
。
但是这中写法在当前版本qiankun.js
里面可能会有如下错误:
这是由于动态设置的publicPath
并不能满足加载异步组件chunk
, 需要我们如下配置一番:(web2->vue.config.js
)
publicPath: `//localhost: 8083`
并且此时直接刷新当前url也还可以正确显示about
页面。
9. 区分开发与打包
前面几条说的都是开发相关的设置, 这里我们要开始介绍打包的配置了, 这里会介绍原理与做法, 不会做的很细所以具体的项目开发还是要好好的封装一番。
我这里先把nginx
简单配置一下, 让这个包能用。
location /ccqk/web1 {
alias /web/ccqk/web1;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /ccqk/web2 {
alias /web/ccqk/web2;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /ccqk {
alias /web/ccqk/container;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
由于我之前有项目在服务器上为了方便区分, 随便写了个ccqk
前缀, 那么现在目标很明确了, 我需要打一个叫ccqk
的文件夹, 里面有三个包container
、web1
、web2
。
第一步: 确立打包路径
container -> vue.config.js
module.exports = { outputDir: './ccqk/container', publicPath: process.env.NODE_ENV === "production" ? `/ccqk` : '/', };
web1 -> vue.config.js
const packageName = require('./package.json').name;
const port = 8082
module.exports = {
outputDir: '../../ccqk/web1',
publicPath: process.env.NODE_ENV === "production" ? '/ccqk/web1' : `//localhost:${port}`,
devServer: {
port,
headers: {
'Access-Control-Allow-Origin': "*"
}
},
configureWebpack: {
// 需要以包的形式打包, 挂载window上
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
},
chainWebpack: config => {
config.plugin("html").tap(args => {
args[0].minify = false;
return args;
});
}
};
web2 -> vue.config.json
const packageName = require('./package.json').name; const port = 8083 module.exports = { outputDir: '../../ccqk/web2', publicPath: process.env.NODE_ENV === "production" ? '/ccqk/web2' : `//localhost:${port}`, devServer: { port, headers: { 'Access-Control-Allow-Origin': "*" } }, configureWebpack: { output: { library: `${packageName}-[name]`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${packageName}`, }, }, chainWebpack: config => { config.plugin("html").tap(args => { args[0].minify = false; return args; }); } };
知识点注意解释:
output.library
: 配置导出库的名称, 如果libraryTarget
设置为'var'那么主应用可以直接用window访问到。output.libraryTarget
:这里设置为umd
意思是在 AMD 或 CommonJS 的 require 之后可访问。output.jsonpFunction
:webpack用来异步加载chunk的JSONP 函数。chainWebpack
: 用来修改webpack
的配置, 配置不进行压缩。
第二步: 配置路由路径
web2 -> router ->index.js
const router = new VueRouter({ mode: "history", base: process.env.NODE_ENV === "development" ? '/w2' : '/ccqk/w2', routes, });
10. css隔离
这里的隔离并不是完美的, 想要了解更详细的内容可以看看我的往期文章带你走进-\>影子元素(Shadow DOM)&浏览器原生组件开发(Web Components API ), 看完你就会完全理解为啥不完美。
11. js隔离
在多应用场景下,每个微应用的沙箱都是相互隔离的,也就是说每个微应用对全局的影响都会局限在微应用自己的作用域内。比如 A 应用在 window 上新增了个属性 test,这个属性只能在 A 应用自己的作用域通过 window.test 获取到,主应用或者其他微应用都无法拿到这个变量。
我这里就不秀源码不扯大概念, 直接来干货原理, qiankun
会在子应用
激活的时候为其赋予一个代理后的window
对象, 用户操作这个window
对象的每一步都会被记录下来, 方便在卸载子应用
时还原全局window
对象, 你要问如何替换的window
对象, 其实它是用with
与evel
来实现的替换, 并且比如jq
在执行前为了提高效率都会把window对象传入函数里使用, 那么这里直接传入代理window
就都ok了, 电脑越写越卡就不扯太多了。
所以其实使用了微前端
技术方案是要付出一定的成本的, 代码速度肯定是有所降低。
12. 康威定律
- 第一定律 组织沟通方式会通过系统设计表达出来。
- 第二定律 时间再多一件事情也不可能做的完美,但总有时间做完一件事情。
- 第三定律 线型系统和线型组织架构间有潜在的异质同态特性。
- 第四定律 大的系统组织总是比小系统更倾向于分解。
只有最适合的组织模式, 没有绝对的模式, 比如一个团队想要试试微前端, 那么其实如果你是个移动端的商城项目, 没什么必要使用微前端, 如果是个小中型的后台系统, 也不是很推荐, 除非你们是一个长期维护并且模块繁多, 或者是你想在这个项目的基础上另启一个项目做, 那么微前端
将是一把神器。
end.
这次就是这样, 希望与你一起进步。