微前端——无界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项目

相关推荐
涔溪1 天前
实现将 Vue2 子应用通过无界(Wujie)微前端框架接入到 Vue3 主应用中(即 Vue3 主应用集成 Vue2 子应用)
vue.js·微前端·wujie
涔溪2 天前
实现将 Vue3 项目作为子应用,通过无界(Wujie)微前端框架接入到 Vue2 主应用中(Vue2 为主应用,Vue3 为子应用)
vue.js·前端框架·wujie
数学分析分析什么?19 天前
微前端之qiankun+vue3简易示例
前端·微前端·qiankun
还是大剑师兰特1 个月前
微前端面试题及详细答案 88道(44-60)-- 工程化、部署与兼容性
微前端·大剑师·微前端面试题
还是大剑师兰特1 个月前
微前端面试题及详细答案 88道(74-88)-- 实践场景与进阶扩展
微前端·大剑师·微前端面试题
Light602 个月前
领码课堂 | React 核心组件与高级性能优化全实战指南
性能调优·状态管理·微前端·server·components·react 架构
小天呐3 个月前
qiankun 微前端接入实战
前端·js·微前端
16年上任的CTO4 个月前
微前端架构:原理、场景与实践案例
前端·架构·微前端·qiankun
snow@li6 个月前
AI问答-vue3+ts+vite:http://www.abc.com:3022/m-abc-pc/#/snow 这样的项目 在服务器怎么部署
微前端·前端项目部署
MINO吖6 个月前
基于 qiankun + vite + vue3 构建微前端应用实践
vue·vite·微前端·qiankun·single-spa