微前端——无界wujie

B站课程视频
课程视频

课程课件笔记:

1.微前端

2.无界

现有的微前端框架:iframe、qiankun、Micro-app(京东)、EMP(百度)、无届

前置

初始化

新建一个文件夹

1.通过npm i typescript -g安装ts

2.然后可以使用tsc --init初始化项目,这样项目目录下会有tsconfig.json配置文件

3.再新建index.ts

4.使用tsc -w命令可以实时的编译index.ts出一个index.js文件

5.通过index.html引入index.js即可

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="./index.js"></script>
</head>
<body>
</body>
</html>

webComponents初使用

主要是为了样式隔离

wu-jie初体验

index.ts文件中书写

js 复制代码
// webComponents 的写法
window.onload = () => {
  // 初始化
  class Wujie extends HTMLElement {
    // 类的基本用法
    constructor() {
      super()

      // this.attachShadow创建shadowdom并打开就具有样式隔离的性质,不会影响外层的样式
      let dom = this.attachShadow({mode: 'open'})
      // 获取template
      let template = document.querySelector('#wujie') as HTMLTemplateElement
      // 并绑定,这里是把template的内容深度克隆一份给dom,而不是直接给,true表示深度克隆
      dom.appendChild(template.content.cloneNode(true))
      // 这样就把template的内容渲染到了<wu-jie></wu-jie>的组件里面
      
      console.log(this.getAttr('url'), this.getAttr('age'))
    }

    // 获取传参this.getAttribute 传入属性名即可
    private getAttr(attr: string) {
      return this.getAttribute(attr)
    }
  }
  // webComponents挂载,注意名字不能启成驼峰的但是-横线连接可以,第二个参数是上面类的名字
  // 类似vue组件  原生js写的一个组件 可以在html中使用了<wu-jie></wu-jie>
  window.customElements.define('wu-jie', Wujie)
}

在上述建立的index.html中书写

html 复制代码
<body>
  <!-- 相当于组件,也可以传参 -->
  <wu-jie url='xxxxx' age='18'></wu-jie>
  <div>我是外层的div</div>

  <template id="wujie">
    <div>我是 template 里面的div</div>
    <style>
      div{
        background: red;
      }
    </style>
  </template>
</body>

无届的三个生命周期

js 复制代码
//生命周期自动触发有东西插入
connectedCallback () {
  console.log('类似于vue 的mounted');
}
//生命周期卸载
disconnectedCallback () {
      console.log('类似于vue 的destory');
}
//跟watch类似
attributeChangedCallback (name:any, oldVal:any, newVal:any) {
    console.log('跟vue 的watch类似,有属性发生变化自动触发');
}

pnpm介绍

1.全局安装npm i pnpm -g

2.使用 pnpm -v可以查看版本号

3.pnpmnpm的优势,在pnpm中文网的动机这样写pnpm官网

简单来说就是,但有100个vue项目,每个项目都会去安装相应的依赖包,这样导致磁盘的缩小和安装速度慢(每次都会重新下载依赖包),很麻烦。

但是pnpm使用软链接硬链接仓库解决

硬链接

1.使用cmd查看提示,表示 /H就是硬链接

2.创建命令含义 mklink /H 硬链接的名字 通过谁创建

例如:在pnpm文件目录下建立index.js文件,并通过mklink /H ying.js index.js创建了硬链接

3.并且目录下多出了一个ying.js

4.然后打开文件发现ying.js中内容和index.js中一样

5.而且当修改index.js中的内容时,ying.js中的内容会跟着修改,因为他们共享的同一个磁盘地址

软链接(符号链接)

1.创建软链接需要管理员权限,所以管理员打开cmd后进入目录可以使用cd ../../等回退到根目录下,然后直接 D: 进入相应盘,再cd命令

2.默认就是软链接 mklink ruan.js index.js 即可,不用加任何修饰符

3.成功创建后VScode中会有一个标志

4.且文件管理器中查看也是一个0字节的,因为他只记录一个路径(快捷方式,并不会占用资源),点击会跳转,指向的还是index.js

pnpm如何利用硬链接、软链接解决上述问题

1.使用pnpm init命令创建package.json

2.以安装vue包为例,使用pnpm i vue 安装vue依赖,可以看到node_modules下面有vue的软链接(快捷方式,地址指向)

3.实际上上述软链接指向在.pnpm包下找到真正的vue@3.3.13的包下面的vue文件夹,这个vue硬链接指向.pnpm store

4.实际上就对应了pnpm官网的这张图,非扁平化的方式(可能嵌入,因为这个库可能还依赖其他库,这样依次来)

绿色黄色实线为软链接、红色虚线为硬链接

pnpm的CLI命令管理

假设你之前使用的npm 安装包,会生成一个package-lock.json,或者使用yarn安装过,会生成一个yarn.lock

但是你再通过pnpm import命令 他会把你的上述的软件包管理器的 lockfile 生成 pnpm-lock.yaml文件

monorepo项目

创建monorepo项目目录

1.使用命令npm init vue创建vue项目,并创建项目名为main

2.创建web文件目录存放子应用

3.创建vue子应用,使用npm init vite选择vue+typescript

4.创建react子应用,使用npm init vite选择react+typescript

5.在web文件夹下有多个子应用,分别安装有点繁琐,所以在monorepo文件夹下使用命令pnpm init生成文件,并手动创建pnpm-workspace.yaml文件和配置。配置

yaml 复制代码
官网是:
packages:
  # all packages in direct subdirs of packages/
  - 'packages/*'
  # all packages in subdirs of components/
  - 'components/**'
  # exclude packages that are inside test directories
  - '!**/test/**'

替换为自己的目录
packages:
  # all packages in direct subdirs of packages/
  - 'main/*'
  # all packages in subdirs of components/
  - 'web/**'

此时目录如下:

6.替换好pnpm-workspace.yaml之后可以在根目录直接pnpm i即可自动为所有的项目安装包

最外层根目录下的node_modules是所有项目公共的包,而vue项目或者react项目里面的node_modules是他们单独所需要的,这样结构更加清晰。

子项目启动:

假设这时候我们要跨级执行命令,想要执行react-demo项目中package.jsondev命令,可以使用pnpm -F react-demo dev,其中F是过滤filter

子模块复用:

1.新建目录common下,并进入,想要common目录下的东西其余子项目(vue、react等都可以使用)

2.使用pnpm init初始化生成package.json文件。在pnpm-workspace.yaml中写

yaml 复制代码
packages:
  # all packages in direct subdirs of packages/
  - 'main/*'
  # all packages in subdirs of components/
  - 'web/**'
  - 'common'

3.安装axios包了pnpm i axios

3.新建index.ts,编写一些公共代码

ts 复制代码
import axios from 'axios'

// 公共的提取出来
export const a = axios.get('xxx')

4.进入main目录使用命令pnpm -F main add common则把common添加进main项目的依赖中,查看main/package.json文件里面有

5.然后就可以在main/src/main.ts中引入

ts 复制代码
import { a } from 'common'

6.同理,也可以去给react-demo项目添加或者vue-demo添加:pnpm -F vue-demo add common,一样查看那个package.json有common包,然后可以导入进行使用

无届

安装

1.使用pnpm i wujie,在main.ts中引入wujie并配置启动的参数

ts 复制代码
import { startApp } from 'node_modules/wujie/esm/index'
// 启动的参数
startApp({name, url, el})

2.如果是vue项目,可以直接安装npm i wujie-vue3进行安装

ts 复制代码
import Wujie from 'wujie-vue3'
app.use(router).use(Wujie)

3.然后分别启动好子应用,记录端口就可以在App.vue中使用了

html 复制代码
<template>
  <div>
    <h1>这是主应用</h1>
    <!-- 分别是子应用,子应用启动之后各自的端口 -->
    <WujieVue url="http://127.0.0.1:5174" name="vue3"></WujieVue>
    <WujieVue url="http://127.0.0.1:5175" name="react"></WujieVue>
  </div>
</template>

在main.ts中,全部代码如下:

ts 复制代码
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import Wujie from 'wujie-vue3' // 引入一下对应的框架
import { preloadApp } from 'wujie'

const app = createApp(App)

app.use(router).use(Wujie) // 注册无界
app.mount('#app')

// exac预加载为true即可,那么components/react.vue和vue3.vue就会有url
preloadApp({ name: "vue3", url: "http://127.0.0.1:5173", exec: true})
preloadApp({ name: "react", url: "http://127.0.0.1:5174", exec: true})

封装无届

1.创建的目录

封装无届

1.创建的目录文件如下:

其中使用pnpm init创建package.json

使用tsc --init创建 tsconfig.json

2.使用pnpm i wujie安装无届

3.使用pnpm i vue -D把vue装在开发环境中

4.安装webpack,使用pnpm i webpack webpack-cli -D

5.安装typescript,使用 pnpm i typescript -D

6.安装typescript解析器,使用pnpm i ts-loader -D

书写相关配置代码

配置代码部分可以看源码参照下面的代码

写完后使用 npm run lib 打包

1.在env.d.ts中书写

ts 复制代码
import { defineComponent, h, getCurrentInstance, onMounted, watch, onBeforeUnmount } from 'vue'
import type { App, PropType } from 'vue'
import { Props } from './type'
import { startApp, bus } from 'wujie'

// 函数式定义组件
const wujie = defineComponent({
  props: {
    width: { type: String, default: "" },
    height: { type: String, default: "" },
    name: { type: String, default: "", required: true },
    loading: { type: HTMLElement, default: undefined },
    url: { type: String, default: "", required: true },
    sync: { type: Boolean, default: undefined },
    prefix: { type: Object, default: undefined },
    alive: { type: Boolean, default: undefined },
    props: { type: Object, default: undefined },
    attrs: { type: Object, default: undefined },
    replace: { type: Function as PropType<Props['replace']>, default: undefined },
    fetch: { type: Function as PropType<Props['fetch']>, default: undefined },
    fiber: { type: Boolean, default: undefined },
    degrade: { type: Boolean, default: undefined },
    plugins: { type: Array as PropType<Props['plugins']>, default: null },
    beforeLoad: { type: Function as PropType<Props['beforeLoad']>, default: null },
    beforeMount: { type: Function as PropType<Props['beforeMount']>, default: null },
    afterMount: { type: Function as PropType<Props['afterMount']>, default: null },
    beforeUnmount: { type: Function as PropType<Props['beforeUnmount']>, default: null },
    afterUnmount: { type: Function as PropType<Props['afterUnmount']>, default: null },
    activated: { type: Function as PropType<Props['activated']>, default: null },
    deactivated: { type: Function as PropType<Props['deactivated']>, default: null },
  },
  setup(props, { emit }) {
    // 读取当前组件实例
    // this.$refs.wujie // 在vue2中可以通过this获取当前组件实例
    const instance = getCurrentInstance() // vue3中通过getCurrentInstance获取组件实例

    // 封装启动函数
    const init = () => {
      // startApp({name, url, el})
      //初始化无界
      startApp({
        name: props.name,
        url: props.url,
        el: instance?.refs.wujie as HTMLElement,
        loading: props.loading,
        alive: props.alive,
        fetch: props.fetch,
        props: props.props,
        attrs: props.attrs,
        replace: props.replace,
        sync: props.sync,
        prefix: props.prefix,
        fiber: props.fiber,
        degrade: props.degrade,
        plugins: props.plugins,
        beforeLoad: props.beforeLoad,
        beforeMount: props.beforeMount,
        afterMount: props.afterMount,
        beforeUnmount: props.beforeUnmount,
        afterUnmount: props.afterUnmount,
        activated: props.activated,
        deactivated: props.deactivated,
      })
    }
    watch([props.name, props.url], () => {
      init() // 如果发生变化就重新执行startApp
    })

    const handlerEmit = (event:string, ...args:any[]) => {
      emit(event, ...args)
    }

    onMounted(() => {
      // 发布订阅模式
      bus.$onAll(handlerEmit) 
      init()
    })

    onBeforeUnmount(() => {
      bus.$offAll(handlerEmit)
    })

    // 定义渲染函数
    return ()=> h('div', {
      style: {
        width: props.width,
        height: props.height
      },
      ref: "wujie" // 方便之后读取
    })
  }
})
// install 方法给vue使用的,app.use(router).use(wujie)会调用install
wujie.install = function(app: App) {
  app.component('WujieVue', wujie)
}

export default wujie

2.type.ts中写

ts 复制代码
import type { plugin } from 'wujie'
type lifecycle = (appWindow: Window) => any;
interface Props {
    /** 唯一性用户必须保证 */
    name: string;
    /** 需要渲染的url */
    url: string;
    /** 需要渲染的html, 如果用户已有则无需从url请求 */
    html?: string;
    /** 渲染的容器 */
    loading?: HTMLElement;
    /** 路由同步开关, false刷新无效,但是前进后退依然有效 */
    sync?: boolean;
    /** 子应用短路径替换,路由同步时生效 */
    prefix?: { [key: string]: string };
    /** 子应用保活模式,state不会丢失 */
    alive?: boolean;
    /** 注入给子应用的数据 */
    props?: { [key: string]: any };
    /** js采用fiber模式执行 */
    fiber?: boolean;
    /** 子应用采用降级iframe方案 */
    degrade?: boolean;
    /** 自定义运行iframe的属性 */
    attrs?: { [key: string]: any };
    /** 自定义降级渲染iframe的属性 */
    degradeAttrs?: { [key: string]: any };
    /** 代码替换钩子 */
    replace?: (codeText: string) => string;
    /** 自定义fetch,资源和接口 */
    fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
    /** 子应插件 */
    plugins: Array<plugin>;
    /** 子应用生命周期 */
    beforeLoad?: lifecycle;
    /** 没有做生命周期改造的子应用不会调用 */
    beforeMount?: lifecycle;
    afterMount?: lifecycle;
    beforeUnmount?: lifecycle;
    afterUnmount?: lifecycle;
    /** 非保活应用不会调用 */
    activated?: lifecycle;
    deactivated?: lifecycle;
};

export { Props } 

3.webpack.config.js中写

const { Configuration } = require('webpack')
const path = require('path')

/**
 * @type {Configuration} //配置智能提示
 */

const config = {
  entry: "./src/index.ts", // 入口文件
  mode: "none",
  output: {
    filename: "index.js",
    path: path.resolve(__dirname, 'lib')
  },
  externals: { // 防止打包后的代码包含wujie和vue代码显得过多
    vue: "vue",
    wujie: "wujie"
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader"
      }
    ]
  }

}

module.exports = config

SWC和babel

SWC官网

1.swc比babel快20倍,如果是4核情况下,快70倍

使用pnpm add -D @swc/score swc-loader安装命令

2.然后替换webpack.config.js中的use

module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader" // babel的ts-loader,换成swc-loader
      }
    ]
  }

3.因为swc底层是rust写的,rust性能是js的好几倍所很快

4.然后再打包npm run lib打包后还不能直接使用,

因为前端有很多代码规范,AMD, CMD,CommonJS,ESModule,所以统一配置一个UMD,

5.安装pnpm i -D @swc/cli @swc/core以便使用swc-cli

6.新建配置文件.swcrc

{
  "$schema": "https://json.schemastore.org/swcrc",
  "jsc": {
  "parser": {
  "syntax": "typescript", // js是ecmascript
  // "jsx": false,
  // "dynamicImport": false,
  // "privateMethod": false,
  // "functionBind": false,
  // "exportDefaultFrom": false,
  // "exportNamespaceFrom": false,
  // "decorators": false,
  // "decoratorsBeforeExport": false,
  // "topLevelAwait": false,
  // "importMeta": false
  },
  // "transform": null,
  "target": "es5",
  "loose": false,
  "externalHelpers": false,
  // Requires v1.2.50 or upper and requires target to be es2016 or upper.
  "keepClassNames": false
  },
  "minify": false
 }

7.在根目录的package.json的 scripts中编写打包命令写"esm": "swc src/index.ts -d esm",

8.控制台执行 npm run swc即可打包在esm文件中

9.修改增添package.json中

设定一些版本号和启动的入口

"name": "vuejie-setup",
"version": "0.0.1",
 "main": "lib/index.js", 
"module": "esm/index.js",

添加files

"files": [
    "lib",
    "esm",
    "index.d.ts"
  ],

10.在根目录下的index.d.ts声明文件中写

ts 复制代码
// import { bus, preloadApp, destroyApp, setupApp } from "wujie";
import type { App } from 'vue';

declare const WujieVue: {
    // bus: typeof bus;
    // setupApp: typeof setupApp;
    // preloadApp: typeof preloadApp;
    // destroyApp: typeof destroyApp;
    install: (app: App) => void
};

export default WujieVue;

发布包

1.切换镜像 mmp use,输入 npm

2.使用 npm adduser命令,然后输入npm的用户名

3.使用npm login

4.使用 npm publish发布

使用

1.在MONOREPO项目中切换进主应用cd main

2.命令安装pnpm i vuejie-setup

3.在main.ts中引入import Wujie from 'vuejie-setup'

4.使用npm run dev启动各种主应用或者子应用查看效果

无届的预加载和FPS

1.requestldleCallback函数
MDN的解释

2.FPS扩展


无届的传参

总共三种方法

第一种方法

无届的架构是把JS单独存放在了iframe中,那么iframe可以通过window和父级进行通讯,所以第一种传参就是通过
window.parent.变量名 进行读取。

1.在vue-demo/App.vue中添加按钮,绑定函数获取a

html 复制代码
<script setup lang="ts">
const send = () => {
  alert(window.parent.a) //
}
</script>
<button @click="send">点击</button>

2.在控制台输入变量 var a = '控制台的输入'

3.点击按钮,发现可以弹出

第二种方法

通过props 传参,子应用不需要绑定无界,主应用接进来之后会自动捕获vue实例,所以子应用可以访问到无界实例对象获取参数

1.例如在主应用的components/App.vue组件中挂载数据,通过props传参

html 复制代码
<template>
  <WujieVue :props="{name: '张三', age: '18'}" url="http://127.0.0.1:5173" name="vue3"></WujieVue>
</template>

2.然后在vue-demo子项目中的App.vue中写JS代码

js 复制代码
const send = () => {
  // alert(window.parent.a) // 第一种方法
  console.log(window.$wujie) // 第二种方式
}

3.为了防止上述$wujie报错,可以在main.ts中添加

ts 复制代码
declare global {
  interface Window {
    $wujie: {
      props:  Record<string, any>
      bus: {
        $emit: any
      }
    }
  }
}

4.打印的结果如下(可以看到还有bus ,第三种方法获取)

4.上述可以访问props console.log(window.$wujie)进行打印

d第三种方案eventBus发布订阅

可以双向数据传递,直接使用wujie的bus.$on

1.在 主应用的App.vue中通过bus.$on绑定数据

ts 复制代码
<script lang="ts">
import { bus } from 'wujie'
bus.$on('vue3', (data: any) => {
  console.log(data, '我是主应用的bus.$on')
})
</script>

2.在子应用vue-demo的App.vue中通过实例直接使用

const send = () => {
  // alert(window.parent.a)
  // console.log(window.$wujie.props) // 获取到App.vue下的props
  window.$wujie.bus.$emit('vue3', '我是子应用vuedemo')
}

模块联邦

1.创建文件如下,目录

2.安装下面四个依赖包
npm i webpack webpack-cli webpack-server html-webpack-plugin -D

3.书写代码,略,结果如下,总之host里面没有的List数据却通过引入到了remote的数据进行展示

4.pnpm run build进行打包host项目下

点开dist/bundle.js,可以看到使用CDN的方式进行引入的

之前:10个项目引用同一个模块,通过把这个项目发布到npm上面,然后这是个项目可以install这个模块,但是当这个模块发生改变的时候,例如从1.0.0 -> 1.0.1,那么这十个项目得重新下载install一遍

现在的CDN,当你的模块修改了,直接就是最新的版本,通过这个链接,远程调用联邦的技术正是用的这种

报错

1.在tsconfig.app.json中报错:没有 "node" 模块解析策略的情况下,无法指定选项 "-resolveJsonModule"。

修改tsconfig.app.json 文件中 compilerOptions 选项配置 "moduleResolution": "node"

json 复制代码
{
  "extends": "@vue/tsconfig/tsconfig.dom.json",
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
  "exclude": ["src/**/__tests__/*"],
  "compilerOptions": {
    "composite": true,
    /*
     * 模块解析策略,ts默认用node的解析策略,即相对的方式导入, 可选值:node、classic
     * 如果未指定,则 --module commonjs 默认为 node,否则默认为 classic(包括 --module 设置为 amd、system、umd、es2015、esnext 等)
     * Node 模块解析是 TypeScript 社区中最常用的,推荐用于大多数项目。 
     * 如果您在 TypeScript 中遇到导入和导出的解析问题,请尝试设置 moduleResolution: "node" 以查看它是否解决了问题。
     */
    "moduleResolution": "node",    
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

参考自

一些文件爆红

1.找不到模块"./App.vue"或其相应的类型声明

2.ts.config.app.json找不到文件vue tsconfig tsconfig dom json

如果报错vite找不到之类的,以及奇怪的错误请删除两个子应用重新建立和安装 和初始化最外层的包pnpm init

ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL vue-demo@0.0.0 dev: `vite` Exit status 1

一些常用的命令

pnpm i 和 pnpm init

pnpm i是 pnpm install的简写,表示安装包,会根据package.json的包版本进行安装,
pnpm init是初始化一个项目,一般只会在空项目文件下创建package.json一些必要的包

pnpm run dev 和 pnpm run start

前者是开发环境,后者是打包之后的生产环境运行项目

使用脚手架建立项目

因为涉及诸多配置,建议按照vite、webpack官网来看

使用vite脚手架建立vue、react项目

使用webpack脚手架建立vue、react项目

相关推荐
老__L9 天前
『微前端系列』微前端概述及qiankun简介
微前端·qiankun
problc1 个月前
腾讯无界微前端框架介绍
微前端
小白探索世界欧耶!~1 个月前
使用MicroApp重构旧项目
前端·经验分享·笔记·学习·重构·项目总结·微前端
技术钱2 个月前
微前端概念
前端·微前端
mxydl20092 个月前
module federation模块联邦与微前端
webpack·微前端·模块联邦
Amd7943 个月前
Vue微前端架构与Qiankun实践理论指南
vue.js·架构·框架·集成·通信·微前端·qiankun
DarinZanya6 个月前
微前端的使用和注意事项 - qiankun
前端·状态模式·微前端
钓了猫的鱼儿6 个月前
微前端——qiankun
vue2·微前端
SanOrintea7 个月前
monaco脚本编辑器 在无界中使用 鼠标点击不到
无界·monaco