最近一段时间,因为将来工做中涉及工业应用较多,而且考虑之后须要将工业应用在同一系统进行展现,但愿有一个突破口能够解决这个问题,即在一个总项目中展现不一样的工业应用,每一个工业应用是一个单独的项目,因为工业应用可能因为不一样团队进行开发,应用开发技术栈最好没有限制,单一前端框架可能再也不能知足要求,由此了解到微前端并进行尝试研究,但愿借助微前端能够解决工业应用汇总展现的问题。javascript
固然微前端的实现方式不有不少种,包括iframe、single-spa等,本文采用的主要是single-spa。若是你使用过iframe就会知道,iframe和single-spa彻底不是一个难度,若是把iframe比做是easy模式,那么single-spa即是地狱模式,若是你的项目着急上线使用微前端最快的方法就是iframe,,相信也有不少人在想搞一下微前端的同窗们,猝死在了研究single-spa的路上,还有也是资料的缺乏,由于如今网上虽然微前端文章不少,可是大多只是理论介绍和部分代码的展现,并无法帮助咱们真正落地在项目中去实施。css
single-spa使用的主要难点在于: 1.对于总项目,如何完美兼容各个子项目,作到技术栈无关,而且让用户感受是一个总体项目。 2.对于各个子项目,如何减小代码侵入,而且使其具备独立开发、独立运行、独立部署的能力。html
微前端是借鉴后端微服务的概念而来,single-spa官网解释到,A microfrontend is a microservice that exists within a browser即微前端是浏览器中存在的微服务,微前端也是UI的一部分,一般由数十个组件组成,而这些组件能够是React,Vue和Angular等不一样框架实现的,每一个微前端项目能够交由不一样的团队进行管理,每一个团队也能够选择本身的框架。尽管在迁移或实验时可能会添有其它框架,可是最好对全部微前端使用一个框架,这是最实用的。 每一个微前端项目能够放在不一样的git存储库中,有本身的package.json和构建工具配置。这样每个微前端项目都有一个独立的构建打包过程和独立的部署,也就意味着咱们能够快速完成咱们的微前端项目的打包上线,而不用每次对一个巨无霸(Monolith)项目进行操做,后期有新的需求,也只须要修改对应的微前端项目。前端
目前随着前端的不断发展,企业工程项目体积愈来愈大,页面愈来愈多,项目变得十分臃肿,维护起来也十分困难,有时咱们仅仅更改项目简单样式,都须要整个项目从新打包上线,给开发人员形成了不小的麻烦,也很是浪费时间。老项目为了融入到新项目也须要不断进行重构,形成的人力成本也很是的高。vue
在前端开发工做中,面临的困难:
对比分析:
微前端实现方式有两种:java
1.iframe嵌入 (难度:★)node
2.single-spa合并类单页应用 (难度:★★★★★)react
iframe嵌入方式比较容易实现,再也不赘述。webpack
为何不用 iframe,这几乎是全部微前端方案第一个会被 challenge 的问题。可是大部分微前端方案又不约而同放弃了 iframe 方案,天然是有缘由的,并非为了 "炫技" 或者刻意追求 "特立独行"iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不管是样式隔离、js 隔离这类问题通通都能被完美解决。但他的最大问题也在于他的隔离性没法被突破,致使应用间上下文没法被共享,随之带来的开发体验、产品体验的问题nginx
- url 不一样步。浏览器刷新 iframe url 状态丢失、后退前进按钮没法使用。
- UI 不一样步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时咱们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中.
- 全局上下文彻底隔离,内存变量不共享。iframe 内外系统的通讯、数据同步等需求,主应用的 cookie 要透传到根域名都不一样的子应用中实现免登效果
- 慢。每次子应用进入都是一次浏览器上下文重建、资源从新加载的过程
其中有的问题比较好解决(问题1),有的问题咱们能够睁一只眼闭一只眼(问题4),但有的问题咱们则很难解决(问题3)甚至没法解决(问题2),而这些没法解决的问题偏偏又会给产品带来很是严重的体验问题, 最终致使咱们舍弃了 iframe 方案。
参考文章: Why Not Iframe
single-spa实现原理:
首先对微前端路由进行注册,使用single-spa充当微前端加载器,并做为项目单一入口来接受全部页面URL的访问,根据页面URL与微前端的匹配关系,选择加载对应的微前端模块,再由该微前端模块进行路由响应URL,即微前端模块中路由找到相应的组件,渲染页面内容。
参考文章: single-spa官网
❤️❤️❤️ 项目源码地址 ❤️❤️️❤️
基座项目建立:
yarn create react-app portal
yarn add antd
// 建立config-overrides.js支持antd按需加载
// fixBabelImports('import', {
// libraryName: 'antd',
// libraryDirectory: 'es',
// style: true,
// }),
复制代码
本文采用微前端加载原理是:
首先在父项目建立dom节点,在项目注册过程输入待挂载的节点,便可完成子项目在父项目中运行。
代码以下:
<div className="App" >
<Layout>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" />
<ul >
<li key="react" >
<Link to="/react">React</Link>
</li>
<li key="vue" >
<Link to="/vue">Vue</Link>
</li>
<li key="angular" >
<Link to="/angular">Angular</Link>
</li>
</ul>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }}>
{React.createElement(collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: () => { setCollapse(!collapsed) },
})}
</Header>
<Content
className="site-layout-background"
style={{
margin: '24px 16px',
padding: 24,
minHeight: 280,
}}
<blockquote style=' padding: 10px 10px 10px 1rem; font-size: 0.9em; margin: 1em 0px; color: rgb(0, 0, 0); border-left: 5px solid #9370DB; background: rgb(239, 235, 233);'></blockquote>
<div id="vue" />
<div id="react-app" />
<app-root></app-root>
</Content>
</Layout>
</Layout>
</div>
复制代码
src文件中建立singleSpa.js文件。
将文件引入项目入口文件index.js文件中。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { BrowserRouter as Router } from 'react-router-dom'
import "./singleSpa.js"; // 引入微前端配置文件;
复制代码
// 项目目录结构
├── public
├── src
│ ├── index.js
│ ├── singleSpa.js
│ └── App.jsx
├── config-overrides.js
├── package.json
├── README.md
├── yarn.lock
复制代码
singleSpa.js部分代码:
import * as singleSpa from 'single-spa';
// 注册应用方式参考文章:
// Single-Spa + Vue Cli 微前端落地指南 (项目隔离远程加载,自动引入)(https://juejin.im/post/5dfd8a0c6fb9a0165f490004#heading-2)
/** * runScript 一个promise同步方法。能够代替建立一个script标签,而后加载服务 * @param {string} url 请求文件地址 */
const runScript = async (url) => {
// 加载css同理
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
const firstScript = document.getElementsByTagName('script')[0];
firstScript.parentNode.insertBefore(script, firstScript);
});
};
// 注册微前端服务
/* 注册所用函数; return 一个模块对象(singleSpa),模块对象来自于要加载的js导出(子项目); 若是这个函数不须要在线引入,只须要本地引入一块加载: () => import('xxx/main.js') */
singleSpa.registerApplication(
'vue',
async () => {
await runScript('http://127.0.0.1:8080/js/chunk-vendors.js');
await runScript('http://127.0.0.1:8080/js/app.js');
return window.singleVue;
},
// 配置微前端模块前缀
// 纯函数根据参数查看是否处于活动状态
(location) => location.pathname.startsWith('/vue')
);
singleSpa.start(); // 启动注册,别忘记!
复制代码
registerApplication参数含义:一、
appName: string
应用名称二、
applicationOrLoadingFn: () => <Function | Promise>
返回promise加载函数或者已解析的应用。// 应用做为参数,该参数由一个带有生命周期的对象组成。 const application = { bootstrap: () => Promise.resolve(), //bootstrap function mount: () => Promise.resolve(), //mount function unmount: () => Promise.resolve(), //unmount function } registerApplication('applicatonName', application, activityFunction) 复制代码
加载函数做为参数必须返回一个promise或者异步函数,第一次加载应用程序时,将不带任何参数地调用该函数,返回promise必须和应用一块儿解决。最多见的加载函数导入方式是:
() => import('/path/to/application.js')
三、
activityFn: (location) => boolean
动态函数(activity function),必须是一个纯函数,函数将window.location做为第一个参数提供,并在应用程序处于活动状态时返回一个判断结果。常见使用时,经过动态函数(activity function)第一个参数判断子应用是否处于激活状态。
环境准备:
npm install -g @vue/cli //全局安装vue-cli
vue create vue-project // 建立子项目
// 项目目录结构
├── public
├── src
│ ├── main.js
│ ├── assets
│ ├── components
│ └── App.vue
├── vue.config.js
├── package.json
├── README.md
└── yarn.lock
复制代码
修改main.js文件进行注册
import Vue from 'vue'
import App from './App.vue'
import singleSpaVue from "single-spa-vue";
Vue.config.productionTip = false
// el 为子项目待挂载到父项目的DOM节点!!!
const vueOptions = {
el: "#vue",
render: h => h(App)
};
// 主应用注册成功后会在window下挂载singleSpaNavigate方法
// 为了独立运行,避免子项目页面为空,
// 判断若是不在微前端环境下进行独立渲染html
if (!window.singleSpaNavigate) {
new Vue({
render: h => h(App),
}).$mount('#app')
}
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions,
});
export const bootstrap = vueLifecycles.bootstrap; // 启动时
export const mount = vueLifecycles.mount; // 挂载时
export const unmount = vueLifecycles.unmount; // 卸载时
export default vueLifecycles;
复制代码
根目录建立vue.config.js修改webpack配置
module.exports = {
/* 重点: 设置publicPath,避免父项目加载子项目时,部分资源文件路径为父项目地址,致使请求文件失败。 */
publicPath: "//localhost:8080/",
configureWebpack: {
devtool: 'none', // 不打包sourcemap
output: {
library: "singleVue", // 导出名称
libraryTarget: "window", //挂载目标,能够在浏览器打印window.singleVue查看
}
},
devServer: {
contentBase: './',
compress: true,
}
};
复制代码
子项目改造咱们总体能够分为两个步骤:
1. 子项目入口文件改造,注册微前端,肯定子项目挂载节点;
2. 子项目webpack出口文件改造,打包后在window下建立singleVue方法。
环境准备:
yarn create react-app react // 建立子项目
yarn add single-spa-react // 安装single-spa-react
// 项目目录结构
├── public
├── src
│ ├── App.js
│ ├── index.js
│ └── serviceWorker.js
├── config
│ ├── jest
│ ├── webpack.config.js
│ └── webpackDevServer.config.js
├── scripts
│ ├── build.js
│ ├── start.js
│ └── test.js
├── package.json
├── README.md
└── yarn.lock
复制代码
修改index.js文件进行注册
import Vue from 'vue'
import App from './App.vue'
import singleSpaVue from "single-spa-vue";
Vue.config.productionTip = false
// el 为子项目待挂载到父项目的DOM节点!!!
const vueOptions = {
el: "#vue",
render: h => h(App)
};
// 主应用注册成功后会在window下挂载singleSpaNavigate方法
// 为了独立运行,避免子项目页面为空,
// 判断若是不在微前端环境下进行独立渲染html
if (!window.singleSpaNavigate) {
new Vue({
render: h => h(App),
}).$mount('#app')
}
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions,
});
export const bootstrap = vueLifecycles.bootstrap; // 启动时
export const mount = vueLifecycles.mount; // 挂载时
export const unmount = vueLifecycles.unmount; // 卸载时
export default vueLifecycles;
复制代码
修改项目启动端口号:
// scripts文件夹内start.js文件
- const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000
+ const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 5000;
复制代码
修改webpack配置,修改config文件夹中webpack.config.js文件
- publicPath:paths.publicUrlOrPath
+ publicPath: 'http://localhost:5000/',
+ library: "singleReact", // 导出名称
+ libraryTarget: "window", //挂载目标
复制代码
父项目首次加载子项目静态文件logo图片报错解决办法:
- 加载失败后,经过检查发现,父项目中加载子项目图片地址为:http://localhost:3000/static/media/logo.5d5d9eef.svg。
- 此时父项目地址为:http://localhost:3000/,子项目地址为:http://localhost:5000/
- 不难发现,logo图片请求地址应为子项目地址即http://localhost:5000/static/media/logo.5d5d9eef.svg
- 静态资源最终访问路径 = output.publicPath + 资源loader或插件等配置路径,默认publicPath路径为网站根目录的位置,而在父项目加载子项目时,当前网站根目录为http://localhost:3000/
- 能够经过将 output.publicPath设置为子项目跟目录http://localhost:5000/解决这个问题。
环境准备:
npm install -g @angular/cli //全局安装angular/cli,直接安装报错
// 报错信息 TypeError: Cannot read property 'flags' of undefined
npm install @angular/cli@9.0.0 -g // 指定版本安装
ng new angular-project // 建立项目
// 项目目录结构
├── e2e
├── src
│ ├── + `main.single-spa.ts`
│ ├── + `single-spa`
│ │ ├── + `single-spa-props.ts`
│ │ └── + `asset-url.ts`
│ ├── app
│ │ ├── + `empty-route`
│ │ │ └── + `empty-route.component.ts`
│ │ ├── empty-route.component.ts
│ │ ├── app.component.ts
│ │ └── `app.module.ts` //use src/app/empty-route/ EmptyRouteComponent
│ ├── assets
│ ├── environments
│ ├── index.html
│ ├── polyfills.ts
│ └── test.ts
├── node_modules
├── + `extra-webpack.config.js`
├── package.json
├── README.md
├── angular.json
└── yarn.lock
复制代码
[报错为:TypeError: Cannot read property 'flags' of undefined](https://stackoverflow.com/questions/49544854/typeerror-cannot-read-property-flags-of-undefined)
single-spa-props.ts
in src/single-spa/
asset-url.ts
in src/single-spa/
src/app/empty-route/
, to be used in app-routing.module.ts.注册应用
// src/app/empty-route/empty-route.component.ts 文件内代码
import { Component } from '@angular/core';
@Component({
selector: 'app2-empty-route',
template: '',
})
export class EmptyRouteComponent {
}
复制代码
// src/app/app.module.ts 文件内代码
+ import { EmptyRouteComponent } from './empty-route/empty-route.component';
@NgModule({
declarations: [
AppComponent,
+ EmptyRouteComponent
],
复制代码
// src/single-spa/asset-url.ts 文件内代码
export function assetUrl(url: string): string {
// @ts-ignore
const publicPath = __webpack_public_path__;
const publicPathSuffix = publicPath.endsWith('/') ? '' : '/';
const urlPrefix = url.startsWith('/') ? '' : '/'
return `${publicPath}${publicPathSuffix}assets${urlPrefix}${url}`;
}
复制代码
// src/single-spa/single-spa-props.ts 文件内代码
import { ReplaySubject } from 'rxjs';
import { AppProps } from 'single-spa';
export const singleSpaPropsSubject = new ReplaySubject<SingleSpaProps>(1)
export type SingleSpaProps = AppProps & {
}
复制代码
// src/main.singleSpa.ts 文件内代码
import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { Router } from '@angular/router';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
import { singleSpaAngular } from 'single-spa-angular';
import { singleSpaPropsSubject } from './single-spa/single-spa-props';
if (environment.production) {
enableProdMode();
}
// if (!window.singleSpaNavigate) {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
// }
const lifecycles = singleSpaAngular({
bootstrapFunction: singleSpaProps => {
singleSpaPropsSubject.next(singleSpaProps);
return platformBrowserDynamic().bootstrapModule(AppModule);
},
template: '<app2-root />',
Router,
NgZone: NgZone,
});
export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;
复制代码
修改angular.json为修改出口文件作准备工做
npm i -D @angular-builders/custom-webpack //用于修改webpack 配置
npm i -D @angular-builders/dev-server
"build": {
- "builder": "@angular-devkit/build-angular:browser",
+ "builder": "@angular-builders/custom-webpack:browser",
"options": {
+ "customWebpackConfig": {
+ "path": "./extra-webpack.config.js" // 读取文件,修改webpack配置
+ },
+ "deployUrl": "http://localhost:4000/", // 修改publicPath
"outputPath": "dist/Delete",
"index": "src/index.html",
————————————————
"serve": {
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular-builders/custom-webpack:dev-server"
"options": {
"browserTarget": "Delete:build"
},
————————————————
复制代码
设置出口文件:
建立extra-webpack.config.js并进行配置
module.exports = {
output: {
library: "singleAngular", // 导出名称
libraryTarget: "window", // 挂载目标
},
}
复制代码
参考文章:
single-spa-angular
single-spa-angular示例代码地址
angular/cli版本 | single-spa-angular版本 | |
---|---|---|
官方示例 | 8.1.0 | 3.0.1 |
本文示例 | 9.0.0 | 4 |
父项目加载过程当中,所有请求:
线上部署完成后`首个请求vue/`发生`nginx报错404 Not Found`,经过排查发现nginx查找路径错误:部署访问地址格式为xx.xx:0000/vue/#/app1
vue:微前端应用名称;
app1:子项目路由
Nginx报错分析:
首次发起请求时,因为这里写法相似browerRouter,根据请求会在nginx根目录文件内查找/vue文件夹,并检查是否有index.html文件进行返回。
xx.xx:0000/XX => nginx文件中XX文件夹 => 文件夹不存在返回404
由于/vue只是为了注册应用,并不须要真正去nginx中的/vue下查找index.html,由于按照这个路径查找并不能查找成功,咱们仍但愿在原来根地址进行查找。ngix 遇到/XXX这样地址会被看成代理,寻找对应文件夹,若是文件夹没有就报错404,可是实际我是但愿去进入到个人项目里面,我本身作判断的
解决方案:
在带有/vue进行访问时,首先进行判断,再使用nginx进行重定向,定向为原根目录。
注意事项:此时进行重定向,可是咱们并不但愿url地址发生改变,因此咱们须要使用rewrite "/xxx" /abc last;的这种跳转形式,可是这种重定向只能对站内url重写,若是rewrite第二个参数以http或者以https开头或者使用permanent都会致使url地址栏改变。(302,301等会修改地址栏的url)
location /vue/ {
root /home/nginx/static/html/refining/;
rewrite ^/vue(.*) /;
index index.html index.htm;
}
// 注意重定向保持url不变,
复制代码
本来觉得,这样就能够宣告成功了!!!可是现实老是残酷的。 咱们刷新浏览器使父项目从新加载子项目,发现父项目中加载子项目文件依然报错,或者返回html致使类型报错。
下面咱们继续对静态资源进行重定向:
location ~.*(gif|jpg|jpeg|bmp|png|ico|txt|js|css)$ {
root /home/nginx/static/html/refining/;
rewrite ^/vue(.*) /$1 break;//$1 为匹配到的第一个参数,即去掉vue后的请求地址
index index.html index.htm;
}
复制代码
其它参考配置:
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
ssi on;
# 将 / 重定向到 /browse
rewrite ^/$ http://localhost:8080/browse redirect;
# 根据路径访问 html
location /browse {
set $PAGE 'browse';
}
location /order {
set $PAGE 'order';
}
location /profile {
set $PAGE 'profile'
}
# 全部其余路径都渲染 /index.html
error_page 404 /index.html;
}
复制代码
stats-webpack-plugin生成manifest.json,实现自动加载。
CSS处理用到postcss-loader,postcss-loader用到postcss,咱们添加postcss的处理插件,为每个CSS选择器都添加名为`.namespace-kaoqin`的根选择器,最后打包出来的CSS,以下所示:
接入地址只需配置一次,省略使用manifest动态加载,由于html自己就是一个完整的manifest.