目前前端所存在的一些问题
- 在目前所流行的主流框架vue,react等,他都是将属于一个单页面应用。在开发,部署等会存在较大的不遍。比如在开发的过程中,随着业务的深入和复杂,将会带来逻辑定位问题、打包速度问题、部署上线问题。往往我们可能只是更改了一行JS代码,到最后发布的时候,整个项目却要整个重新打包编译发布。
- 公司可能之前的系统使用的是JQ或者其他框架进行开发,这个时候,我们想要追赶一下潮流。使用react或者vue进行开发。这个时候,我们就不得不对之前的项目使用新技术进行开发。
- 在如今的单页面应用里,所有的JS到最后都打包到一个
Bundle.js文件里,这就会导致线上用户第一次进入的时长比较长,对于前端性能统计中的FP,FCP,TTI等一系列指标产生重大影响。虽然我们可以使用懒加载的形式去对代码进行拆分下载,但是依然会导致上述问题,因为懒加载是在路由发生变化的时候去加载的,此时此刻当你路由切换了,需要经过DNS解析,三次握手,然后传输,代码解析等等步骤,这其中也会耗费一些时间。
我们需要一个什么样的技术去解决上述问题
我们希望可以有这么一种技术或者架构:
- 它能够使各个子模块或者子系统进行隔离。这样我们在更新一个子模块的时候,我们只需要对这个子模块进行打包,发布上线。不会影响到其他模块。并且因为各个子系统之间相互隔离,项目就会拆分的轻量化,打包速度,前端性能等也会上去。并且因为各个子系统之前的相互隔离,这样就不会受限于技术栈的影响,你们各自系统只要能实现功能就行。
- 它能够使各个子系统进行数据共享。例如用户信息。
- 他能够对JS,css等进行相互隔离,防止出现污染问题。
微前端
有了之前的铺垫,相信大家大概都能猜出微前端主要的功能是什么。微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于前端,即将 Web 应用由单一的单页面应用转变为多个小型前端应用聚合为一的应用。然后各个前端应用还可以独立运行、独立开发、独立部署。
实现微前端的方案
-
路由转发。当前的单页面应用的路由控制都是在前端进行。这就导致我们必须使用同一的技术栈,要不然
react-dom-router他指挥不了vue路由,vue-router也指挥不了react。这也会导致我们的项目必须在一个同一的项目里进行开发,因为跨项目的话,就算技术栈一样,A也指挥不了B里面的路由跳转。但是如果我们将路由跳转交给服务端,当我们访问一个路由的时候,后端进行重定向等操作,这样就会将我们的应用隔离开。例如:http://www.xxxx.com/a与http://wwww.xxxx.com/b,当后端收到a路由的时候,指向一个系统,当收到b路由的时候指向另外一个系统,这样我的系统开发所采用的技术栈就可以进行隔离。如果要分享用户信息等,可以通过cookie等技术进行分享。因为每次路由匹配到的话,都会进行刷新,因此也防止了JS,css的污染问题缺点:每次跳转都相当于重新刷新了一次页面,不是页面内进行跳转。影响体验。
优点: 简单,可快速配置。
-
iframe嵌套。通过创建一个父程序,在父程序中监听路由的变化,卸载或加载相应的子程序iframe。因每一个iframe就相当于一个单独的页面,所以iframe具有天然的JS和css隔离。在信息共享方面,我们可以使用postMessage或者contentWindow的方式进行。
缺点: iframe样式兼容问题。分别为功能性兼容性以及业务性兼容性的问题。可能会存在一些安全问题。postmessage可以试出来。
优点:实现起来简单,自带沙盒特性
-
纯web components开发。将每个子应用采用web components进行开发。纯web-components相当于自定义了一个html标签,我们就可以在任何的框架中进行使用此标签。例如:
<template id='userInfo'> <div class='user-box'> <p class='user-name'>byeL</p> <p class='user-sex'>男</p> </div> </template> class UserInfo extends HTMLElement { constructor() { super(); var templateElem = document.getElementById('userInfo'); var content = templateElem.content.cloneNode(true); this.appendChild(content); } } window.customElements.define('user-info', UserCard);使用
// 直接在html中使用 <body> <link rel="import" href="./UserInfo.js"> </body> // 在vue中使用 // a.vue // 需要在入口的main中引入userInfo <template> <user-info></user-info> </template> // 需要在入口的main中引入userInfo // 在react中使用 class HelloMessage extends React.Component { render() { return <div><user-info></user-info></div>; } }优点: 每个子应用拥有独立的script和css,也可单独部署
缺点:需要对之前的子系统都要进行改造,并且通信方面较为复杂
-
组合式应用路由分发。每个子应用单独的打包,部署和运行。不过需要基于父应用进行路由管理。例如:有子应用A的路由是/testA,子应用B的路由是/testB,那么父应用在监听到/testA的时候,如果此时处于/testB,那么首先会进行一个子应用B的卸载。完成之后,在去加载子应用A。
优点:纯前端改造,相比于路由式,无刷新,体验感良好。
缺点:需要解决样式冲突,JS污染问题,通信技术等。
-
目前采用的方案
目前的微前端采用的技术方案是组合是应用路由开发。那么我们都知道,他的缺点是需要自行解决JS的沙盒环境、css的样式重叠或冲突问题、通信技术问题。
css冲突解决方案:
相比于JS的沙盒环境来说,css的解决冲突的方案有很多,并且实现起来不是很复杂。
- 类似于vue的scoped。在打包的时候,对css选择器加上响应的属性,属性的key值是一些不重复的hash值,然后在选择的时候,使用属性选择器进行选择。
- 可以自定义前缀。在开发子模块之前,需要确定一个全局唯一的css前缀,然后在书写的过程中同一添加此前缀,或在根root上添加此前缀,使用less或sass作用域嵌套即可解。例如:
<div class='rootA'>
<span class='rootA-span'></span>
</div>
<style>
.root{
.rootA-span{
// 书写你的css
}
}
</style>
JS的沙盒环境:
首先我们需要明确的是,如果采用组合式应用路由开发,对于JS上下文有什么影响?我们做个例子:
假如我有个a子应用,会给window上挂在一个函数,函数名是hello,然后我父应用上也有一个函数名是hello,那么在子应用进行加载的时候,就会覆盖父类上的方法。
// 子应用A
window.hello = () => {
alert('我是子应用A');
};
// 父应用
window.hello = () => {
alert('我是父应用');
};
基于上面的例子,我们大致就可以看出,沙盒环境最主要做的就是一个js作用域、属性等的隔离。那么在实际的应用中,基本采用以下原理进行隔离:
-
diff方法。当我们的子页面加载到父类的基座中的时候,我们可以生成一个map的散列表。在页面渲染之前,我们先把当前的window上的变量等都存储在这个map中。当页面卸载的时候,我们在遍历这个map,将其数据在替换回去。
class Sandbox { constructor() { this.cacheMy = {}; // 存放修改的属性 this.cacheBeforeWindow = {}; } showPage() { this.cacheBeforeWindow = {}; for (const item in window) { this.cacheBeforeWindow[item] = window[item]; } Object.keys(this.cacheMy).forEach(p => { window[p] = this.cacheMy[p]; }) } hidePage() { for (const item in window) { if (this.cacheBeforeWindow[item] !== window[item]) { // 记录变更 this.cacheMy[item] = window[item]; // 还原window window[item] = this.cacheBeforeWindow[item]; } } } } const diffSandbox = new Sandbox(); // 模拟页面激活 diffSandbox.showPage(); // 激活沙箱 window.info = '我是子应用'; console.log('页面激活,子应用对应的值',window.info); // 模拟页面卸载 diffSandbox.hidePage(); // 模拟页面激活 console.log('页面卸载后,子应用的对应的值', window.info); diffSandbox.showPage(); // 重新激活 console.log('页面激活,子应用对应的值', window.info);
-
使用代理的形式。在这里需要介绍一个es6的新特性:
proxy,他的详细介绍请查看MDN:点击查看proxy的介绍。原理大致是,监听get和set方法,针对当前路由进行window的属性或方法的存取const windowMap = new Map(); const resertWindow = {}; let routerUrl = ''; const handler = { get: function(obj, prop) { const tempWindow = windowMap.get(routerUrl); console.log(windowMap, routerUrl); return tempWindow[prop]; }, set: function(obj, prop, value) { if (!windowMap.has(routerUrl)) { windowMap.set(routerUrl, JSON.parse(JSON.stringify(resertWindow))); } const tempWindow = windowMap.get(routerUrl); tempWindow[prop] = value; // console.log(obj, prop, value); }, }; let proxyWindow = new Proxy(resertWindow, handler); // 首先是父类的啊属性. proxyWindow.a = '我是父类的a属性的值'; // 改变路由到子类 routerUrl = 'routeA'; proxyWindow.a = '我是routerA的a属性的值' // 改变路由到父类 routerUrl = ''; console.log(proxyWindow.a); // 改变路由到子类 routerUrl = 'routeA'; console.log(proxyWindow.a);
- iframe自带css和js沙盒隔离。