EMP微前端-Vue和React项目互相调用

团队原文:https://github.com/efoxTeam/emp/wiki/%E3%80%8AVue%E5%92%8CReact%E9%A1%B9%E7%9B%AE%E5%A6%82%E4%BD%95%E4%BA%92%E7%9B%B8%E8%BF%9C%E7%A8%8B%E8%B0%83%E7%94%A8%E3%80%8B
EMP项目github链接: https://github.com/efoxTeam/emp
EMP教程整理: https://github.com/efoxTeam/emp/wiki

React和 Vue 相互远程调用 Demo

什么是微前端

做好前端开发不是件容易的事情,而比这更难的是扩展前端开发规模以便于多个团队可以同时开发和维护一个大型且复杂的产品。为解决这个难题,前端领域逐渐出现一种趋势,可以将大型的前端项目分解成许多个小而易于管理的独立部署的应用,并实现应用级别的资源(UI组件/工具函数/业务模块)分享,就像后端领域的微服务一样。 在这种趋势下, Micro Frontends 官网推出了微前端概念:(来自:https://micro-frontends.org/)

什么是 EMP

  • EMP 是一套基于 Webpack5 和 Module Federation 的全新微前端框架(区别与以往以 dom 隔离和 iframe 为技术栈的微前端),同时更是一种全新的开发方式。
  • EMP 是一种可以跨项目跨框架互相调用项目里的任何资源(包括但不限于 第三方依赖、组件、Function、图片资源等等)的微前端框架。
  • EMP 是 YY(欢聚时代) 业务中台 Web 技术组最新开源的前端框架。在开源 EMP 之前,YY 业务中台 Web 技术组曾开源 Flutter-ui 等爆星项目。
  • EMP 已经在 YY 内部广泛使用,应用于众多中台项目以及 To C 、 To B 项目。
  • EMP 框架的核心是 emp-cli,了解更多具体实现可以看 emp-cli 源码。

EMP 的微前端服务发现

EMP 的微前端服务发现,可以让本地项目调用远端项目的时候知道远端项目含有的功能、组件、具体的函数传参、组件传参等等。类似于后端微服务的服务发现。

大体架构如下: 架构图

了解更多具体实现可以看 Webpack Plugin emp-tune-dts-plugin 。

EMP 的微前端服务发现,同时解决了 Module Federation 在 Typescript 情况下类型无法与远端项目共享的问题( How typesafe can a remote be with Typescript? ),可以说是针对该问题的业界先进解决方案。

怎么快速上手 EMP 项目

使用 emp-cli 的 init 命令: npx @efox/emp-cli init 

启动 cli 的 init 命令,选择你所需技术栈的 emp 模板,目前 emp-cli 支持的模板有:

  • react
  • vue2
  • vue3
  • react-base
  • react-project
  • vue3-base
  • vue3-project

(陆续会支持到所有主流技术栈)

React 和 Vue 的远程组件互相调用实践

实践

分别新建 emp 的 React 和 Vue 项目

React

使用 emp-cli , npx @efox/emp-cli init,选择 React 模板

写一个简单 React 组件 新建 /src/components/Hello.tsx

import React from 'react'
import './common.scss'
import './common.less'
import './common.css'
const Hello = ({title}: {title: string}) => (
  <>
    <h1>{title}</h1>
  </>
)

export default Hello

修改项目里的 emp.config.js (emp.config.js 是 EMP 项目的配置文件) :

  • 暴露这个 React 组件,以供远程调用
  • 引入远程的 Vue 组件(下面会写 Vue )
const path = require('path')
const packagePath = path.join(path.resolve('./'), 'package.json')
const {dependencies} = require(packagePath)
console.log(packagePath)

module.exports = ({config, env}) => {
  const port = 8001
  const projectName = 'ReactComponents'
  const publicPath = `http://localhost:${port}/`
  config.plugin('mf').tap(args => {
    args[0] = {
      ...args[0],
      ...{
        // 项目名称
        name: projectName,
        // 暴露项目的全局变量名
        library: {type: 'var', name: projectName},
        // 被远程引入的文件名
        filename: 'emp.js',
        // 远程项目别名:远程引入的项目名
        remotes: {
          '@emp/vueComponents': 'vueComponents',
        },
        // 需要暴露的东西
        exposes: {
          // 别名:组件的路径
          './configs/index': 'src/configs/index',
          './components/Hello': 'src/components/Hello',
        },
        // shared: ['react', 'react-dom'],
        shared: {...dependencies},
      },
    }
    return args
  })
  config.output.publicPath(publicPath)
  config.devServer.port(port)
  config.plugin('html').tap(args => {
    args[0] = {
      ...args[0],
      ...{
        files: {
          js: ['http://localhost:8006/emp.js'],
        },
      },
    }
    return args
  })
}

在 /src/bootstrap.tsx 引入远程 Vue 组件,引入 vuera ,使用 VueInReact 包裹远程 Vue 组件进行使用

import * as React from 'react'
import * as ReactDOM from 'react-dom'

import Hello from 'src/components/Hello'
import Content from '@emp/vueComponents/Content.vue'
import {VueInReact} from 'vuera'

const VueComponent = VueInReact(Content)

ReactDOM.render(
  <>
    <Hello title="I am React Project" />
    <div style={{backgroundColor: '#eee', padding: '20px'}}>
      <VueComponent title="React use Remote Vue Component" />
    </div>
  </>,
  document.getElementById('emp-root'),
)

启动项目 yarn dev,可以看到本项目的组件和远程引用的 Vue 组件 

Vue(暂时不支持 Vue3)

使用 emp-cli , npx @efox/emp-cli init,选择 Vue2 模板

写一个简单的 Vue 组件 /src/components/Content.vue

<template>
  <div style="color: red">{{ title }}</div>
</template>

<script>
export default {
  name:'Content',
  props:['title'],
  data() {
    return {
    };
  },
};
</script>

修改项目里的 emp.config.js (emp.config.js 是 EMP 项目的配置文件) :

  • 暴露这个 Vue 组件,以供远程调用
  • 引入远程的 React 组件
const path = require('path')
const {VueLoaderPlugin} = require('vue-loader')
//
const ProjectRootPath = path.resolve('./')
// const packagePath = path.join(ProjectRootPath, 'package.json')
// const {dependencies} = require(packagePath)
//
const {getConfig} = require(path.join(ProjectRootPath, './src/config'))
//
module.exports = ({config, env, empEnv}) => {
  const confEnv = env === 'production' ? 'prod' : 'dev'
  const conf = getConfig(empEnv || confEnv)
  console.log('config', conf)
  //
  const srcPath = path.resolve('./src')
  config.entry('index').clear().add(path.join(srcPath, 'main.js'))
  //
  config.resolve.alias.set('vue', '@vue/runtime-dom')
  config.plugin('vue').use(VueLoaderPlugin, [])
  config.module
    .rule('vue')
    .test(/\.vue$/)
    .use('vue-loader')
    .loader('vue-loader')
  //
  const host = conf.host
  const port = conf.port
  const projectName = 'vueComponents'
  const publicPath = conf.publicPath
  config.output.publicPath(publicPath)
  config.devServer.port(port)
  //
  config.plugin('mf').tap(args => {
    args[0] = {
      ...args[0],
      ...{
        name: projectName,
        library: {type: 'var', name: projectName},
        filename: 'emp.js',
        remotes: {
          ReactComponents: 'ReactComponents',
        }, 
        exposes: {
          './Content.vue': './src/components/Content',
        },
        /* shared: {
          ...dependencies,
        }, */
      },
    }
    return args
  })

  config.resolve.alias
  .set('vue$', 'vue/dist/vue.esm.js')
  .clear()

  //
  config.plugin('html').tap(args => {
    args[0] = {
      ...args[0],
      ...{
        title: 'EMP Vue Components',
        files: {
          js: ['http://localhost:8001/emp.js'],
        },
      },
    }
    return args
  })
}

在 /src/App.vue 引入远程 React 组件,引入 vuera ,使用 ReactInVue 包裹远程 React 组件进行使用

<template>
  <div>
    <Content title="I am Vue Project" />
    <hello-react title="Vue use Remote React Component" />
  </div>
</template>

<script>

import { ReactInVue } from "vuera";
import Content from "./components/Content";

import Vue from "vue";
const HelloReact = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('ReactComponents/components/Hello').then(res=>{
    return ReactInVue(res.default)
  }),
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 0,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})

export default {
  name: "APP",
  components: {
    Content,
    "hello-react":HelloReact
  },
  data() {
    return {};
  },
  created() {
  },
};
</script>

<style scoped>
img {
  width: 200px;
}

h1 {
  font-family: Arial, Helvetica, sans-serif;
}
</style>

启动项目 yarn dev,可以看到本项目的组件和远程引用的 React 组件 

原理解析

远程组件的编译与分发

EMP 根据 emp.config.js 的 exposes 字段配置将组件编译成一个单独的闭包,然后将组件单独打包成一个js,最后以 emp.js 的形式作为引用索引,按需加载。

上一节 React 的 Hello 组件编译后的代码如下:

(self["webpackChunk_empreactvue_react"] = self["webpackChunk_empreactvue_react"] || []).push([["src_components_Hello_tsx"],{

/***/ "./src/components/Hello.tsx":
/*!**********************************!*\
  !*** ./src/components/Hello.tsx ***!
  \**********************************/
/*! namespace exports */
/*! export default [provided] [maybe used in ReactComponents (runtime-defined); used in index] [usage prevents renaming] */
/*! other exports [not provided] [maybe used in ReactComponents (runtime-defined)] */
/*! runtime requirements: __webpack_require__, __webpack_require__.n, __webpack_exports__, __webpack_require__.r, __webpack_require__.* */
/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "webpack/sharing/consume/default/react/react");
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var _common_scss__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./common.scss */ "./src/components/common.scss");
/* harmony import */ var _common_scss__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_common_scss__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var _common_less__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./common.less */ "./src/components/common.less");
/* harmony import */ var _common_less__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_common_less__WEBPACK_IMPORTED_MODULE_2__);
/* harmony import */ var _common_css__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./common.css */ "./src/components/common.css");
/* harmony import */ var _common_css__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_common_css__WEBPACK_IMPORTED_MODULE_3__);
var Hello = function Hello(_ref) {
  var title = _ref.title;
  return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h1", null, title));
};
/* harmony default export */ __webpack_exports__["default"] = (Hello);
/***/ })
}]);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9AZW1wcmVhY3R2dWUvcmVhY3QvLi9zcmMvY29tcG9uZW50cy9IZWxsby50c3giXSwibmFtZXMiOlsiSGVsbG8iLCJ0aXRsZSJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUFBO0FBQ0E7QUFDQTtBQUNBOztBQUNBLElBQU1BLEtBQUssR0FBRyxTQUFSQSxLQUFRO0FBQUEsTUFBRUMsS0FBRixRQUFFQSxLQUFGO0FBQUEsc0JBQ1osdUlBQ0UsdUVBQUtBLEtBQUwsQ0FERixDQURZO0FBQUEsQ0FBZDs7QUFNQSwrREFBZUQsS0FBZixFIiwiZmlsZSI6ImpzL3NyY19jb21wb25lbnRzX0hlbGxvX3RzeC5lMzAzYzRjNC5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBSZWFjdCBmcm9tICdyZWFjdCdcbmltcG9ydCAnLi9jb21tb24uc2NzcydcbmltcG9ydCAnLi9jb21tb24ubGVzcydcbmltcG9ydCAnLi9jb21tb24uY3NzJ1xuY29uc3QgSGVsbG8gPSAoe3RpdGxlfToge3RpdGxlOiBzdHJpbmd9KSA9PiAoXG4gIDw+XG4gICAgPGgxPnt0aXRsZX08L2gxPlxuICA8Lz5cbilcblxuZXhwb3J0IGRlZmF1bHQgSGVsbG9cbiJdLCJzb3VyY2VSb290IjoiIn0=

远程调用时将其他框架的组件编译成当前框架

在调用时编译其他框架的组件需要用到 vuera 这个库,帮助我们把其他框架的组件编译成当前所用的框架。

与其他微前端技术比较

与基于 dom 隔离的微前端框架所比较,以 qiankun 为例

  • 状态方面。qiankun 所做的微前端不能把基站项目和子项目过度隔离导致上下文不一致,共享状态等等需要通过总线方式传递,十分麻烦。而 EMP 通过把调用远程的状态管理使得状态共享十分方便。
  • 跨框架调用实现。qiankun 通过 dom 隔离的方式,使得跨框架实现十分容易,但是不能互相调用,粒度只能渲染在规定的 dom 区域。EMP 实现的跨框架调用粒度到了 function ,而且使用十分方便。
  • 体积方面。qiankun 因为是通过 dom 隔离方式实现,所以依赖共享并不完善,需要依赖于 systemjs,而且共享不方便,导致依赖可能会出现重复,使得出现体积变大。EMP 通过 module federation 实现依赖共享,使得依赖不会重新重复(依赖变成全局变量,相同依赖只会留下一个),所以体积会相对 qiankun 更小。

与基于 iframe 的微前端所比较

  • 状态方面。iframe 的微前端,无真正意义上的状态管理,通过 postMessage 进行通信。
  • 跨框架调用方面。iframe 的微前端不能跨框架调用。
  • 体积方面。 iframe 的微前端并不能共享依赖。

总结

  • EMP 是 YY 业务中台 Web 技术组最新开源的微前端框架。目前已在公司内广泛使用,对多项目的状态管理、互相调用、体积优化都有很大的提升。
  • 跨框架远程调用方便。跨框架互相调用如此方便,同技术栈互相调用更是不在话下。
  • 致力于提高多项目协作,是一种全新的开发方式。

----------------------------------------------------------------------

具体的EMP微前端方案教程目录如下:

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值