阅读视图

发现新文章,点击刷新页面。

模块联邦:更快的微前端方式!

什么是模块联邦

在前端项目中,不同团队之间的业务模块可能有耦合,比如A团队的页面里有一个富文本模块(组件),而B团队 的页面恰好也需要使用这个富文本模块。

传统模式下,B团队只能去抄A团队的代码,把这个组件放到自己的项目了。

为了解决不同服务之间组件共享的问题,Webpack 的模块联邦功能应用而生。借助模块联邦,可以在B服务运行时,动态加载A服务暴露的模块。

Vite通过vite-plugin-federation 插件也可以实现

模块联邦的核心优势

结合模块联邦的原理与功能,它具有以下显著优势:

  • 独立部署:各个应用可独立开发、构建和部署,互不依赖。
  • 运行时共享:模块在运行时动态加载,无需在构建阶段打包进主应用。
  • 版本控制:支持为共享模块指定版本,便于多项目之间的依赖管理。
  • 减少重复代码:多个应用可共享功能模块,避免重复实现。

相比 qiankun 等微前端方案,模块联邦在灵活性上更具优势。qiankun 侧重于加载整个子应用,而模块联邦支持按需加载特定模块,粒度更细,使用更灵活。

如何实现

以Vite+Vue为例。

假设我们有 Vite 应用:

  • ui-library:组件提供者,暴露一个按钮组件 SharedButton.vue
  • main-app:主应用,运行时动态加载 ui-library 中的按钮组件来使用。
shixiaoshi-demo/
│
├── ui-library/     # 远程模块:暴露组件
│   ├── src/SharedButton.vue
│   ├── vite.config.js
│
└── main-app/       # 主项目:加载远程组件
    ├── src/App.vue
    ├── vite.config.js

1、安装模块联邦插件

我们需要使用 vite-plugin-federation 插件。

npm install vite-plugin-federation --save-dev

2、暴露组件

  • ui-library/src/SharedButton.vue
<template>
  <button class="shared-btn">我是共享按钮</button>
  </template>

  <script setup>
  </script>

  <style scoped>
  .shared-btn {
  padding: 10px;
  background-color: teal;
  color: white;
  border: none;
  border-radius: 5px;
}
</style>
  • ui-library/vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'ui_library',
      filename: 'remoteEntry.js',
      exposes: {
        './SharedButton': './src/SharedButton.vue',
      },
      shared: ['vue'],
    }),
  ],
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

启动服务

npm run dev -- --port=5001

暴露地址:http://localhost:5001/assets/remoteEntry.js

3、加载远程组件

  • main-app/vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'main_app',
      remotes: {
        ui_library: 'http://localhost:5001/assets/remoteEntry.js',
      },
      shared: ['vue'],
    }),
  ],
});
  • main-app/src/App.vue
<template>
  <div>
  <h1>主应用</h1>
  <RemoteButton />
  </div>
  </template>

  <script setup>
  import { defineAsyncComponent } from 'vue';

// 从 ui-library 中动态加载共享组件
const RemoteButton = defineAsyncComponent(() => import('ui_library/SharedButton'));
</script>
  • 启动主应用
npm run dev

访问 http://localhost:5173,你会看到来自 ui-library 的按钮组件成功渲染!

主应用与子组件关系图

main-app (主应用)
   |
   | -- 在运行时访问 -->
   |    http://localhost:5001/assets/remoteEntry.js
   |
   | -- 加载 -->
   |    ui-library 中暴露的 SharedButton.vue
   |
   | -- 使用 -->
   |    作为主应用的本地组件一样渲染

缺陷

在实际使用中,模块联邦也存在一些问题。

以我们项目开发中遇到的问题为例,当应用 A 加载应用 B 中的某个组件时,B 的代码执行环境会落在 A 的作用域下。这会带来一些潜在影响:

  • 依赖冲突问题:如果 B 服务依赖特定版本的 Vue,而 A 服务的 Vue 版本不同,实际运行时组件会使用 A 的 Vue 实例,可能导致行为异常,特别是在响应式系统、生命周期等细节方面。
  • 状态失效问题:如果 B 中的组件依赖 Vuex 或 Pinia 等状态管理工具,运行时由于上下文切换,可能无法正确获取或响应状态,导致 store 在远程加载后失效。
  • 构建配置差异问题:例如,B 使用了自动引入插件(如 unplugin-auto-import),可以在代码中直接使用 refcomputed 等 Vue API 而无需显式导入;但若 A 没有启用相同插件,加载 B 的组件时会因缺少这些 API 导致运行错误,除非显式引入相关函数。

因此,在使用模块联邦时,建议主应用与远程模块在框架版本、构建工具、插件配置等方面尽可能保持一致,或通过适配包装进行隔离,以减少因上下文差异带来的问题。

浏览器指纹-探究前端如何识别用户设备

什么是浏览器指纹?

浏览器指纹,是用来唯一标识你浏览器的一组“特征值”。它不是我们理解中的那种真实指纹,而是通过收集浏览器、操作系统、设备分辨率、字体、插件等信息,组合成的一个独特 ID。

和传统的 Cookie 不同,浏览器指纹不需要在用户设备上存储任何东西,完全是“读取现有信息”来识别用户。

使用背景

在最近的项目中,有个小需求:想用用户的设备作为唯一凭证,来验证身份

一开始我想着简单粗暴点,用 JS 获取手机的 IMEI 或 PC 的序列号。但查了下资料后才发现,这根本行不通——JS 根本没权限访问这些底层硬件信息,安全机制早就把这条路堵死了。

后来才反应过来,我真正想要的,是一个“设备唯一标识”,也就是——浏览器指纹。

可行方案

查阅了一些资料之后,目前比较常见的几种浏览器指纹方案如下:

  • Navigator 指纹:浏览器类型、版本、系统平台等信息。
  • Canvas 指纹:让浏览器绘制一段隐藏的图像,然后读取图像的像素差异,不同设备会有微小区别。
  • WebGL 指纹:利用显卡和图形驱动渲染差异,获取设备的唯一特征。
  • 字体、插件、时区、屏幕分辨率等:这些信息组合起来也能提供一定的识别度。

当然,单一方案识别率可能不高,但多种信息结合后,指纹的唯一性就会明显提升。

Navigator 指纹

Navigator 是前端获取浏览器和部分设备环境信息的重要接口。

下面是一些常用的属性和方法(跨浏览器兼容性较好的为主):

属性/方法 作用说明 示例代码
navigator.userAgent 获取浏览器的用户代理字符串,可以用于判断浏览器类型、系统类型 navigator.userAgent
navigator.platform 获取运行环境的操作系统平台类型(如 Win32、Linux x86_64、MacIntel) navigator.platform
navigator.appVersion 获取浏览器版本信息和部分平台信息 navigator.appVersion
navigator.appName 获取浏览器名称(大多数现代浏览器返回 “Netscape”) navigator.appName
navigator.language 返回当前浏览器的首选语言(如 “zh-CN”、“en-US”) navigator.language
navigator.languages 返回用户的首选语言列表 navigator.languages
navigator.hardwareConcurrency 返回可用的逻辑处理器数量(CPU核心数) navigator.hardwareConcurrency
navigator.plugins 返回当前安装的插件列表(仅桌面浏览器有意义,且有兼容性限制) navigator.plugins
navigator.onLine 判断当前浏览器是否联网 navigator.onLine
navigator.cookieEnabled 判断浏览器是否启用 Cookie navigator.cookieEnabled
navigator.geolocation 提供地理位置定位服务(需要用户授权) navigator.geolocation.getCurrentPosition(...)
navigator.maxTouchPoints 支持的最大触控点个数(触屏设备可用) navigator.maxTouchPoints
navigator.mediaDevices 访问音视频设备管理 API(如获取麦克风、摄像头) navigator.mediaDevices.getUserMedia(...)
navigator.clipboard 读写系统剪贴板(部分浏览器需要 https 环境和权限) navigator.clipboard.writeText("Hello")
navigator.connection 获取网络连接信息对象(如带宽、类型,部分浏览器支持) navigator.connection.effectiveType
navigator.userAgentData 在新标准中可用的一种用户代理信息对象,部分浏览器已支持,用户隐私性更高 navigator.userAgentData

偷个懒,让Tare直接帮我写个Navigator 指纹示例吧。

<!DOCTYPE html>
<html>

<head>
    <title>Navigator 指纹示例</title>
</head>

<body>
    <h2>Navigator 指纹示例</h2>
    <pre id="output"></pre>
    <script>
        async function getNavigatorFingerprint() {
            // 收集 navigator 相关信息
            const data = {
                userAgent: navigator.userAgent,
                platform: navigator.platform,
                language: navigator.language,
                languages: navigator.languages,
                cookieEnabled: navigator.cookieEnabled,
                hardwareConcurrency: navigator.hardwareConcurrency || 'N/A',
                deviceMemory: navigator.deviceMemory || 'N/A',
                webdriver: navigator.webdriver || false,
            };
            // 将数据转成字符串
            const dataString = JSON.stringify(data);
            // 计算 SHA-256 哈希
            const hashBuffer = await crypto.subtle.digest(
                "SHA-256",
                new TextEncoder().encode(dataString)
            );
            // 转成十六进制字符串
            const hashArray = Array.from(new Uint8Array(hashBuffer));
            const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
            return { data, fingerprint: hashHex };
        }
        getNavigatorFingerprint().then(result => {
            const output = document.getElementById('output');
            output.textContent =
                "采集到的 Navigator 信息:\n" + JSON.stringify(result.data, null, 2) +
                "\n\n生成的指纹(SHA-256):\n" + result.fingerprint;
        });
    </script>
</body>

</html>

代码生成完毕,点击应用直接预览:

经过测试,在同一个电脑上,这个指纹是稳定的,多次执行,这个值不会变。

但这这个指纹明显有缺陷,我系统语言或者浏览器升级后,这个指纹肯定会改变。

Canvas 指纹

由于不同设备(包括操作系统、显卡、驱动、字体渲染引擎等)在绘制同一段 Canvas 内容时会存在细微差异,最终得到的图像数据(通常是像素或转成 base64)在不同设备上往往是不同的。

这些细微差异生成的哈希值就是“指纹”,由于只与设备性能有关,指纹稳定性显然比Navigator 指纹高一些。

<!DOCTYPE html>
<html>
<head>
    <title>简单Canvas指纹示例</title>
</head>
<body>
    <h2>简单Canvas指纹示例</h2>
    <p>请打开控制台(F12)查看结果</p>

    <script>
        // 创建一个简单的Canvas指纹生成函数
        function generateCanvasFingerprint() {
            // 创建canvas元素
            const canvas = document.createElement('canvas');
            canvas.width = 200;
            canvas.height = 100;
            
            // 获取绘图上下文
            const ctx = canvas.getContext('2d');
            
            // 填充背景
            ctx.fillStyle = 'white';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            // 绘制一些图形和文字
            // 绘制红色矩形
            ctx.fillStyle = 'red';
            ctx.fillRect(20, 20, 50, 50);
            
            // 绘制蓝色圆形
            ctx.fillStyle = 'blue';
            ctx.beginPath();
            ctx.arc(120, 45, 25, 0, Math.PI * 2);
            ctx.fill();
            
            // 绘制文本
            ctx.fillStyle = 'black';
            ctx.font = '16px Arial';
            ctx.fillText('Canvas指纹', 60, 80);
            
            // 获取canvas数据URL
            const dataURL = canvas.toDataURL();
            
            // 简单哈希函数
            function simpleHash(str) {
                let hash = 0;
                for (let i = 0; i < str.length; i++) {
                    const char = str.charCodeAt(i);
                    hash = ((hash << 5) - hash) + char;
                    hash = hash & hash; // 转换为32位整数
                }
                return hash.toString(16); // 转换为16进制
            }
            
            // 计算指纹
            const fingerprint = simpleHash(dataURL);
            
            return {
                fingerprint: fingerprint,
                dataURL: dataURL
            };
        }
        
        // 生成并输出指纹
        const result = generateCanvasFingerprint();
        console.log('Canvas指纹:', result.fingerprint);
        console.log('Canvas数据URL前100个字符:', result.dataURL.substring(0, 100) + '...');
        
        // 如果浏览器支持更安全的哈希算法,也可以使用它
        if (window.crypto && window.crypto.subtle) {
            const encoder = new TextEncoder();
            const data = encoder.encode(result.dataURL);
            
            window.crypto.subtle.digest('SHA-256', data)
                .then(hashBuffer => {
                    // 将哈希缓冲区转换为十六进制字符串
                    const hashArray = Array.from(new Uint8Array(hashBuffer));
                    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
                    
                    console.log('Canvas指纹(SHA-256):', hashHex);
                });
        }
    </script>
</body>
</html>

生成的指纹还是很不错的。

其他几种方式生成浏览器指纹都大同小异,这里就不介绍了。

❌