初识微前端(上篇)

概念

微前端是将微服务的技术理念营运至前端后的技术实践,将前端应用拆分成一些更小、更简单的模块,且这些模块能够由多个团队独立开发、部署、测试

img

比如我们有一个新项目,要用到项目和项目 B 的模块,除了 copy,目前还有哪些方法,你一定会说 npm 仓库,确实是一种办法,但是要把业务组件封装成 npm,领导可能会 C 掉你,虽然 React 提倡一切皆组件,一切皆函数,但是对于组件复用,多个项目仍存在很大的局限性

优势

  • 技术无关,不受项目其他团队影响
  • 增量升级,对于烂尾的应用,达到低成本升级
  • 代码独立,子应用可独立产出、部署
  • 团队管理,微前端的方案,有助于形成独立的团队,更加明确模块目标
  • 样式隔离,子应用中的样式不会污染其他组件

快速集成

iframe

优点

  • 1.改造速度块
  • 2.改造成本低

缺点

  • 1.iframe 嵌⼊的显示区⼤⼩不容易控制,存在⼀定局限性
  • 2.URL 的记录完全⽆效,⻚⾯刷新不能够被记忆,刷新会返回⾸⻚,iframe 功能之间跳转也⽆效
  • 3.iframe 样式显示、兼容性问题
  • 4.iframe 会阻塞 onLoad,占用连接池,多层嵌套会造成卡顿甚至崩溃,且占用内存较大
  • 5.SEO 丢失

后端模板集成

就拿我们 node 来举例,比如 express,我们可以使用模板语法,include 其他 html 到主模板

优点

  • 1.改动成本低,多为服务端修改

缺点

  • 1.每个模板并不能颗粒化到组件,只能到具体项目
  • 2.模板局限性,不能进行二级拆分
  • 3.模板拆分太多,导致相同静态资源被重复引用

package 集成

例如如下的 package.json 文件

{
  "name": "duanx.com",
  "version": "1.0.0",
  "description": "duanxl blob",
  "dependencies": {
    "@duanxl/menu-list": "^1.0.0",
    "@duanxl/container": "^1.0.0",
    "@duanxl/nav-bar": "^1.0.0"
  }
}

优点

  • 1.如果你只想试试,那么改造很快
  • 2.轻松管理依赖

缺点

  • 1.每次发布应用,都要重新编译
  • 2.不同技术栈坑太大

Web Component 集成

比如在 angular 新版本中,createCustomElement 就可以实现 Web Component 组件的形式

优点

    1. 环境隔离,包括 script、css
    1. 自定义 elements
  • 3.html imports,可以直接引入 html 片段

缺点

    1. 需要重写前端应用,成本较大
    1. 生态不完善,存在一定兼容性
    1. 当应用被不断的被拆成一个有一个组件时,组件通讯会是一个很大的麻烦

思考

基于上面的一些可改造方式,我们来看微前端需要解决的一些问题

问题 描述
独立开发 独立开发,而不受影响
独立部署 能作为一个服务来单独部署
支持不同框架 可以同时使用不同的框架,如 Angular、Vue、React
优化 能消除未使用的代码
环境隔离 应用间的上下文不受干扰
多个应用同时运行 不同应用可以同时运行
共用依赖 不同应用是否共用底层依赖库
依赖冲突 依赖的不同版本是否导致冲突
集成编译 应用最后被编译成一个整体,而不是分开构建
微应用的注册、异步加载、生命周期 应该有统一的生命周期,去解决不同技术栈的差异
消息通信 子应用之间的消息通信
微应用独立开发 各个子应用之间,应具有个子的开发调试能力
发布流程 各个应用独立发布,不存在相互依赖
渐进式 如何平滑的升级老项目

img

目前比较流行的微前端库

  • 1.qiankun
  • 2.single-spa
  • 3.bifrost
  • 4.yog(比较早)

微前端构建业务系统思考

下面借鉴大佬成功案例分享 ,分别对应项目运行期、部署期、构建期以及开发期的行动方案

父项目:映射多个后端内容,统一授权登录、控制全局菜单等
子项目:项目菜单、权限、角色隔离


父项目:重新生成引用文件、更新全局入口文件、重启服务
子项目:替换静态资源


父项目:生成全局路由、生成全局入口引用
子项目:替换公共依赖、css 添加作用域、生成静态资源


父项目:路由控制、模块加载、JS 公用库替换
子项目:输出数据流、输出路由、输出功能

子应用如何渲染到主站

  • 1.首先,我们只考虑一种技术栈的情况,这里我们使用全民信仰·Vue
  • 2.如何将子应用到主站上,我们使用SystemJs
  • 3.考虑将子应用编译,让主站引用,这就是涉及到了异步加载 chunk
  • 4.实践过程中,要注意跨域问题
  • 5.打包结果,一定是符合上面 SystemJs 的规范,可以直接打成 System.register 包裹的代码

SystemJs

提到了 SystemJs,垃圾的我,还是看了两眼,SystemJs 会根据 import 保存文件路径,使用 fetch + eval 加载 JS 文件并执行,最后将函数导出,而 import 执行的 System 文件,register 会添加到 System 队列
SystemJs 是针对浏览器端的,会直接返回加载文件的内容,具体操作是会动态创建 script,监听onload事件,移除 script 标签,将 register 回调传回去,这时候 import 的 Promise 会被 resolve 掉,在业务中,就会触发我们的 then 方法

小实战

    1. 我们先搭建主站,主站负责控制各个子应用渲染、子应用路由控制、业务组合等等,将用到的 npm 包直接引入主站模板中,在主站中,我们异步加载子应用,也就是上面提到的 SystemJs
      img
  • 2.接下来,就是创建子应用,子应用就是一个 vue 项目,和上面的想法一样,我们要编译 vue,并打包成 SystemJs register Api 的 js

    • 初始化子应用,简单目录结构
      img
    • 配置 webpack,注意,使用的 Webpack4 版本,之后会使用 Webpack 联邦模块,直接输出 System
    • 至于为什么要使用 webpack-system-register,因为我们直接使用 Webpack4 打包的话,Webpack 中有自己的 modules 依赖,这使得我们很难解决依赖中的关系,子应用并不能直接在主站中执行,不过在 Webpack5 中,联邦模块会解除自己的 module,提供一种注册加载执行的机制,将会在之后的微前端(下篇)中尝试
    // webpack.config.js
    const VueLoaderPlugin = require('vue-loader/lib/plugin');
    const WebpackSystemRegister = require('webpack-system-register');
    module.exports = {
      entry: {
        // buylist: './src/BuyList.vue',
        buy: './src/buy.vue',
      },
      module: {
        rules: [
          {
            test: /\.css$/i,
            use: ['vue-style-loader', 'css-loader'],
          },
          {
            test: /\.vue$/,
            loader: 'vue-loader',
          },
        ],
      },
      plugins: [new VueLoaderPlugin(), new WebpackSystemRegister({})],
    };
    
    • 接着就是简单子项目的业务组件
      img

    • 接着,我们编译 buy.vue,在 dist 目录下启动服务,注意,需要解决跨域问题,这里我使用的 http-server,并且启动主站服务

    http-server -p 9000 --cors
    
    1. 完成上面的步骤,我们就可以主站渲染子应用了,但是只实现了同技术栈的,如果存在不同技术栈呢,我们需要考虑以下几个问题
    • 目前,我们每次发版都需要主站和子应用都要编译上线,为了解决这问题,我们可以通过云配置文件去驱动
    • 多技术栈我们需要统一生命周期
    • 消息通知,我们需要统一的监听消息机制
    1. 微前端技术,早在多年前大厂已用到,比如百度地图的统一消息机制,百度地图 ,打开控制台,可以看到在 window 上看到 listener 等等。。。。

接下来,我们看组件间的通讯如何处理

/**
 * listener
 * @notice  this
 * @notice Bom/Dom
 */
(() => {
  const EXECTIME = 50  // debounce exec
  const DELAY = 25

  // source
  let that = {}
  let slice = [].slice
  let channelList = {}

  const on = (channel, type, callback, context) => {
    let currentChannel = channelList[channel]
    if (!currentChannel) {
      currentChannel = channelList[channel] = Object.create(null)
    }
    currentChannel[type] = currentChannel[type] || []

    currentChannel[type].push({
      func: callback,
      context: context || that
    })
  }

  const once = (channel, type, callback, context) => {
    const _once = () => {
      that.off(channel, type, _once)
      return callback.apply(context || that, arguments)
    }
    on(channel, type, _once, context)

  }

  const tigger = (channel, type) => {
    if (channelList[channel] && channelList[channel][type] && channelList[channel][type].length) {
      const taskList = channelList[channel][type]
      let currentHanders = []
      for (let i = taskList.length; i--;) {
        currentHanders.push({
          hander: taskList[i],
          args: slice.call(arguments, 1)
        })
      }

      (() => {
        const startDate = +Date.now()
        do {
          const currentTask = currentHanders.unshift()
          const hander = currentTask.hander

          try {
            hander.func.apply(hander.context, currentTask.args)
          } catch (e) {
            console.log(e)
          }
        } while (currentHanders.length && (Date.now() - startDate < EXECTIME))
        if (currentHanders.length > 0) {
          setTimeout(arguments.callee, DELAY);
        }
      })()
    }
  }

  const off = (channel, type, callback, context) => {
    context = context || that
    if (channelList[channel] && channelList[channel][type] && channelList[channel][type].length) {
      const taskList = channelList[channel][type]
      let hander
      for (var i = taskList.length; i--;) {
        hander = taskList[i]
        if (hander.func === callback && hander.context === context) {
          taskList.splice(i, 1)
        }
      }
    }
  }

  that.on = on
  that.once = once
  that.tigger = tigger
  that.off = off
  globalThis.listener = globalThis.listener || that
})()

思考,如果我们在子应用中的生命周期,使用上面的消息机制,是否也会实现不同技术栈的统一生命周期

本源

微前端也不是解决一切的利器,是否需要用微前端,还有诸多因素,排去推动、团队之间等问题,就在技术而言,也有着一些不足

  • 1.在应用之前,开发者要指定好用 CSR 还是 SSR 等技术方案,多钟渲染机制,会很棘手
  • 2.对于老项目改造成本大
  • 3.与后端的对接,比如跨域、通讯等机制

FAAS 小实践

微前端的由来,与微服务密不可分,厉害的大佬都在搞 ServerLess,由于自己太菜,所以也在学习这方面的知识


在前几年,ECS 是大部分企业必备利器,而在应用层面,先后不断的促进热启动等技术,但是随着行业发展,Docker、ServerLess 的火热,也出现了很多让开发者更加便捷的手段
ECS 虽然功能强大,但是成本较高,在硬件、性价比等基础上,FAAS、BAAS、PAAS、DAAS 等技术也应用而生
就拿 AWS 的 FAAS 来说,在应用层面上,不断开拓冷启动、全球节点、次数计费,成为了成本很低、而又方便的解决方案,免费版一个月 100W 次请求,对于一些小项目而言,无论在解决服务压力、负载方面,都有了诸多便利

简单定义

  • FAAS :函数即服务,每一个函数就是一个服务,函数可以是任意语言编写
  • 冷启动 :当 worker 再被访问时,启动服务并响应,相比如热启动,冷启动并不占用线程,冷启动带来的缺点也随之而来

小实战–Cloundflare

img

我们以 cloundflare,展示下简单的用法

  • 首先,我们用 cloundflare 创建一个 Worker,解决跨域等基本问题
import { handleRequest } from './handler'

addEventListener('fetch', (event) => {
  event.respondWith(handleRequest(event.request))
})

// handler
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET,HEAD,POST,OPTIONS',
  'Access-Control-Max-Age': '86400',
  'Access-Control-Allow-Headers': '*',
  'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
}
export async function handleRequest(request: Request): Promise<Response> {
  const value = await MY_KV.get('name')
  return new Response(JSON.stringify(value), {
    headers: corsHeaders,
    status: 200,
  })
}
  • 直接使用 cloundflare 部署函数
  • 创建 KV,用于数据存储,通过触发器触发数据更新
  • 使用 cli 工具调试数据
  • 创建 Pages,连接 GitHub 项目,当 GitHub Action,会触发自动构建
import React, { useEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  const [name, setName]=useState('')
  const init = async () => {
    // const result = await fetch('https://duanxl.18210219235.workers.dev/')
    const result = await fetch('http://127.0.0.1:8787')
    setName(await result.json())
  }
  useEffect(() => {
    init()
  }, [])
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code> 测试数据:</code> {name}
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
  • 简单的数据查询,CI/CD 及部署等全部完成,域名也可以自己通过域名添加解析记录来完成,替换掉默认分配的域名

img


一个简单的 ServerLess 实战已完成,虽然遍历许多,但是也有着一些缺点,比如存在冷启动时间等问题,当然,这就伴随而来需要做 PWA、缓存等优化,最简单的,可以集成 Workbox,提升用户体验,至此,一个最简单的 ServerLess 小实战就完成了,如果想白嫖建站的,可以试一下

END

接着在初识微前端(下篇),会实践一些 webpack5 的联邦模块,现有流行的微前端库是如何处理这些问题的,还有就是 webComponent 这块,主要围绕 ServerLess
预计更新时间:2022.06

0