# Ecode集成Vue

## 基线：10.0.2502.01及以上版本支持


## 一、背景
	
 标准目前底层前端框架为React，无法直接编写Vue代码，而目前存在许多客户只会开发Vue项目，需要在ecode平台支持使用Vue框架进行前端项目开发，故此在ecode平台集成Vue以满足客户开发需求。


## 二、场景

#### 1.组件参数复写
	
 组件参数复写主要设计参数修改，大部分场景下不涉及框架差异，部分场景如自定菜单按钮，自定义渲染列表需要渲染组件

#### 2.组件替换
	
 替换原有组件，或者对原有组件进行封装

#### 3.新页面开发
	
 完全通过vue框架自定义开发新页面

#### 4.eb和门户自定义组件开发

 基于现有eb框架，通过vue来实现自定义组件开发

#### 5.Vue插件注册

 vue涉及的状态管理，ui组件，路由等

#### 6.react和vue组件之间相互引用

 由于系统是react框架开发的，如果想用vue开发，则需要有react引入vue组件能力。开发vue时，如果要使用标准ui，标准业务组件
 等，则需要有vue引入react组件的能力

#### 7.流程ecode代码块引入vue组件

流程ecode代码块仅在私有化环境支持

#### 8.自定义菜单和eb菜单通过子路由方式加载ecode单页
	
  通过iframe嵌套ecode单页会有性能问题，特此需通过子路由方式集成ecode单页

## 三、技术实现方案

原ecode开发方案及案例：https://weapp.eteams.cn/ecode/playground/doc/share/view/974701716410515470

#### 3.1.vue生态

|     |  框架   | 状态管理  |  UI   | 路由  |
|  ----  |  ----  | ----  |  ----  | ----  |
|     | Vue    |   Vuex   |   Element UI    |  vue-router |
| 版本  |  v3.4.38  | v4.1.0 |  v2.8.0 | 4.4.3 |
| 官网  |  https://vuex.vuejs.org/zh/  | https://vuex.vuejs.org/zh/ |  https://element-plus.org/zh-CN/guide/design | https://router.vuejs.org/zh/ |
| 支持  |  支持  | 默认集成但需手动指定加载 |  默认集成但需手动指定加载 | 默认集成但需手动指定加载，或者以vue-router方式访问路由时，会自动引入，如：只要路径为/sp/custom/v/开头则会自动加载vue路由 |

**注：一期不支持单组件css**

#### 3.2.ecode

##### (1)新增(.vue)文件类型
	
![](${appMdRes}/1725588747-image.png)

##### (2)编辑器Vue代码高亮

![](${appMdRes}/1725589418-image.png)

##### (3)编辑器Vue代码提示
	
![](${appMdRes}/1725588966-image.png)

##### (4)错误提示
	

![](${appMdRes}/1725589390-image.png)

##### (5)vue代码编译

```
@vue/compiler-sfc + babel
```


#### 3.3.ecodesdk底层

##### (1)插件集成
	
  在开发过程中，由于一些定制化的需求，需要引入一些第三方库，第三方组件等，传统方式需要开发人员
  手动通过loadjs引入第三方相关依赖进行使用，并且不符合本地es module开发方式，无法直接通过
  import方式直接使用插件。
  
  现标准ecode提供新的插件机制，用户注册插件，在引入组件时，绑定相关依赖时，底层自动引入相关依赖，即可直接
  通过import使用相关插件

	import { pluginManager } from '@weapp/ecodesdk';
  
###### 1-1注册插件

``` jsx
//注册插件时，在任意前置加载的entry.js里注册即可，如果是通用插件最好在统一应用进行注册管理

//以下是标准底层默认注册插件，使用时可以直接使用对应插件，重复注册可以替换对应插件。

import { pluginManager } from '@weapp/ecodesdk';

pluginManager.registerPlugin([
  {
    name: 'vue',//插件名， import时使用，如：import {} from 'vue'
    path: `${publicDomain}${publicUrl}/build/ecodesdk/js/vue.global.prod.js`,//依赖路径，字符串或数组
    winName: 'Vue',// 插件实例名,vue在控制台通过window.Vue就能取到对应实例
    sync: true,//插件是否同步加载，同步加载会阻塞组件的加载，但不会阻塞页面其他内容加载，因为vue是vue框架底层依赖，所以需要同步加载
    order: -1，//插件加载顺序
  },
  {
  	name: 'vue-router',
    path: `${publicDomain}${publicUrl}/build/ecodesdk/js/vue-router.global.prod.js`,
    winName: 'VueRouter',
    version: '4.4.3'
  },
  {
    name: 'vuex',
    path: `${publicDomain}${publicUrl}/build/ecodesdk/js/vuex.global.prod.js`,
    winName: 'Vuex',
    version: '4.1.0'
  },
  {
    name: 'element-plus',
    path: [
        `${publicDomain}${publicUrl}/build/ecodesdk/js/element.plus.css`,
        `${publicDomain}${publicUrl}/build/ecodesdk/js/element.plus.min.js`
    ],
    winName: 'ElementPlus',
    version: '2.8.1'
  }
])
```
###### 1-2使用插件

使用插件时有以下几种方式：

```jsx
import { asyncImport,lazyPureVueInReact, asyncImportPlugin } from '@weapp/ecodesdk';

//前置或非前置文件引用飞前置文件
asyncImportPlugin('${appId}','components/VueAntd',['antd']).then(res=>{
	console.log(res)
})

组件加载方式：
const App = lazyPureVueInReact(() => asyncImport('${appId}', 'App'),{
	plugins:['vue'],//如果是vue插件，可以不必填写，lazyPureVueInReact加载组件的时候会默认引入
})
const App = lazyPureVueInReact(() => asyncImportPlugin('${appId}', 'App',['vue']))
```

示例：

```jsx
//entry.js
//新页面开发案例
import React from 'react';
import { asyncImport,lazyPureVueInReact, pluginManager } from '@weapp/ecodesdk';
import { Route } from 'react-router-dom';
import { regOvProps, appInfo, regReactChildren } from '@weapp/utils';

const VuePage = lazyPureVueInReact(() => asyncImport('${appId}', 'App'),{
  plugins: ['vue-router','vuex','element-plus'],//依赖插件，vue不用写，默认会添加依赖
  beforeVueAppMount:(app)=>{//组件加载前注册插件
    app.use(ElementPlus)//element ui
    asyncImport('${appId}', 'store/index').then(store=>{ //vuex状态管理
      app.use(store.default)
    })
    asyncImport('${appId}', 'router/index').then(router=>{ //vue-router路由
      app.use(router.default)
      router.default.afterEach((to,from,fail)=>{
         console.log(to,from,fail)
      })
    })
  }
});

const { publicUrl } = appInfo('@weapp/ecodesdk');

const vuePageRoute = (
  <Route path={`${publicUrl}/sp/custom/` + '${appId}/vue'}>
    <React.Suspense fallback= {() => { }}>
      <VuePage />
    </React.Suspense>
  </Route>
);

regOvProps('weappEcodesdk', 'Switch', (props) => {
  regReactChildren(props, vuePageRoute);
  return props;
}, 0);

```
###### 1-3注册指定插件加载回调事件

```jsx

//在vue组件中异步注册插件
//entry.js
import React from 'react';
import { asyncImport,lazyPureVueInReact, pluginManager } from '@weapp/ecodesdk';
import { Route } from 'react-router-dom';
import { regOvProps, appInfo, regReactChildren } from '@weapp/utils';

let vueapp = null;

pluginManager.registerPluginLoadEvent(['antd'],{
  pluginLoad:(plugin)=>{
    if(vueapp){
      vueapp.use(plugin.instance)
    }
  }
})

const VuePage = lazyPureVueInReact(() => asyncImport('${appId}', 'demo'),{
  plugins: ['vue'],
  beforeVueAppMount:(app)=>{//组件加载前注册插件
    vueapp = app;
  }
});

//demo.vue
import {asyncImport,applyPureReactInVue,lazyPureReactInVue,asyncImportPlugin } from '@weapp/ecodesdk';
const AsyncComp = defineAsyncComponent(() =>
  asyncImportPlugin('${appId}','components/VueAntd',['antd'])
)

<Suspense v-if="menuSelectValue==='antd'">
  <AsyncComp />

  <template #fallback>
    <div>Antd Vue Loading...</div>
  </template>
</Suspense>
```
###### 1-4手动加载指定插件
```js
import { pluginManager } from '@weapp/ecodesdk';
pluginManager.loadPlugin('antd').then(res=>{//传数组['vue','antd']加载多个插件
	//res为对应插件信息
})
```

###### 1-5集成vue-router

```js
import { asyncImportPlugin,vueRouter } from '@weapp/ecodesdk';

vueRouter.registryVueRoutes({
  name:'abcd',
  path:'/${appId}/vue',
  component: () => asyncImportPlugin('${appId}', 'App',['vuex','element-plus']).finally(()=>{
    const app =  vueRouter.pageVueApp;
    app.use(ElementPlus)
    asyncImport('${appId}', 'stores').then(store=>{
      app.use(store.default)
    })
  })
})

```

###### 1-6路径匹配调整

原路径如果未添加后缀会自动添加.js进行匹配：如：filePath ===> filePath.js

现针对这种情况进行优化，如果未带后缀的情况，会按照以下顺序进行匹配

``` js
如果只写了filePath，则按照以下路径进行匹配
`${filePath}.js`,
`${filePath}.vue`,
`${filePath}/index.js`,
`${filePath}/index.vue`
```

###### 1-7其他
```js
import { pluginManager } from '@weapp/ecodesdk';
pluginManager.getPluginInfo('antd') 获取antd插件信息，插件加载后才能获取到
pluginManager.getPluginInstance('antd') 获取antd插件实例，插件加载后才能获取到

```

##### (2)在React组件中使用Vue组件

###### 2-1同步加载模式

applyPureVueInReact

主要针对不需要异步引入直接通过import可以引入的组件,如非前置文件直接引用当前应用非前置文件

``` jsx

// demo.js

import {applyPureVueInReact} from '@weapp/ecodesdk'
import Button from './Button.vue'
import {useState} from 'react'
const VButton = applyPureVueInReact(Button)

export default function () {
  const [foo] = useState('Hello!')
  
  const onClick = useCallback(()=>{
	alert("点击了")
  },[])
 
  return <div>
    <VButton onClick={onClick}>
      这是Vue按钮
    </VButton>
  </div>
}


//Button.vue
<template>
  <div>
    <button @click="sendToParent">添加数据</button>
  </div>
</template>
 
<script>
export default {
  props: ['parentData'],
  methods: {
    sendToParent() {
      this.$emit('addData', {name:"哈哈哈哈哈哈哈"});
    }
  }
};
</script>


```


###### 2-2懒加载模式

lazyPureVueInReact

主要针对需要异步记载的文件，如在前置文件里引入非前置加载文件

``` jsx

// entry.js

import React from 'react';
import { asyncImport,lazyPureVueInReact } from '@weapp/ecodesdk';
import { Route } from 'react-router-dom';
import { regOvProps, appInfo, regReactChildren } from '@weapp/utils';

const VuePage = lazyPureVueInReact(() => asyncImport('${appId}', 'vue'));

const { publicUrl } = appInfo('@weapp/ecodesdk');

const vuePageRoute = (
  <Route path={`${publicUrl}/sp/custom/` + '${appId}/vue'}>
    <React.Suspense fallback= {() => { }}>
      <VuePage />
    </React.Suspense>
  </Route>
);

regOvProps('weappEcodesdk', 'Switch', (props) => {
  regReactChildren(props, vuePageRoute);
  return props;
}, -10000);

```

##### (3)在Vue组件中使用React组件

###### 3-1同步加载模式

applyPureReactInVue

主要针对不需要异步引入直接通过import可以引入的组件,如非前置文件直接引用当前应用非前置文件

``` js

//demo.vue

<script setup>
import {Button} from '@weapp/ui';
import {asyncImport,applyPureReactInVue } from '@weapp/ecodesdk';

const UIButton = applyPureReactInVue(Button)

</script>

<template>
  <div>
    <h2>标准UI组件</h2>
    
    <UIButton type="primary" @click='addData'>这是系统标准按钮</UIButton>

  </div>
</template>
```

###### 3-2懒加载模式

lazyPureReactInVue

``` js

//demo.vue

<script setup>

import {asyncImport,lazyPureReactInVue } from '@weapp/ecodesdk';

const ReactButton = lazyPureReactInVue(() => asyncImport('${appId}', 'reactButton'));

</script>

<template>
  <div>
    <h2>自定义React组件</h2>
    
    <ReactButton :confirm='addData'>这是自定义React组件</ReactButton>

  </div>
</template>
```


## 四、案例

后续补充代码

#### 1.react和vue组件相互引用

下载：看新页面开发案例即可

![](${appMdRes}/1725603801-image.png)

#### 2.组件参数复写

案例地址：/workflow/list/all (流程--》全部流程菜单---》列表式视图---》重写流程名称和去除未操作者)

下载：[链接](https://weapp.eteams.cn/papi/file/download?id=1045504724933828641&type=share_1045505188736270380&docId=8492662540472476062&authType=1&docId=&tenantKey=to6kmv3dfk&shareOutId=1045505188736270380&isDownload=1)

![](${appMdRes}/1725603853-image.png)

#### 3.组件替换

案例地址：/workflow/list/all (流程--》全部流程菜单---》提交申请按钮---》前加自定义vue按钮)

下载：[链接](
https://weapp.eteams.cn/papi/file/download?id=1045504729026150433&type=share_1045505188736270380&docId=8492662540472476062&authType=1&docId=&tenantKey=to6kmv3dfk&shareOutId=1045505188736270380&isDownload=1)

![](${appMdRes}/1725603838-image.png)

#### 4.新页面开发

访问链接：
	
  /sp/custom/${appId}/vue： 采用react-router方式加载，内容为vue组件，此方式比下面方式会少加载vue-router

  /sp/custom/v/${appId}/vue：采用vue-router方式加载，路由和内容都为vue生态，此方式会默认加载vue-router和vue
  

下载：[链接](
https://weapp.eteams.cn/papi/file/download?id=1047724355372818490&type=share_1045505188736270380&docId=8492662540472476062&authType=1&docId=&tenantKey=to6kmv3dfk&shareOutId=1045505188736270380&isDownload=1)

![](${appMdRes}/1725603678-image.png)

#### 5.eb和门户自定义组件开发

使用方式：后台管理中心-》低代码平台e-builder-》开发-》组件-》左侧菜单最底部自定义菜单--》导入

案例一下载：[链接](
https://weapp.eteams.cn/papi/file/download?id=1045505137040973884&type=share_1045505188736270380&docId=8492662540472476062&authType=1&docId=&tenantKey=to6kmv3dfk&shareOutId=1045505188736270380&isDownload=1)

![](${appMdRes}/1725603772-image.png)

案例二下载：[链接](https://weapp.eteams.cn/papi/file/remotedownload/1116485403148795936/upload/true?editLinkType=editPage&authType=1&docId=8492662540472476062&tenantKey=to6kmv3dfk&shareOutId=1045505188736270380&userAccount=undefined&authModule=document&module=document&v=mwx4us)


![](${appMdRes}/1743152263-image.png)
#### 6.流程ecode代码块引入vue组件


![](${appMdRes}/1726631832-image.png)


![](${appMdRes}/1726631853-image.png)

```jsx
//index.js


import { lazyPureVueInReact,asyncImport} from '@weapp/ecodesdk';
import ReactDOM from 'react-dom';
import {Suspense} from 'react';

const CusButton = lazyPureVueInReact(
  //因为CusButton.vue是在vue文件夹下，所以路径需要加vue，打开代码设计器即可查看
  () => asyncImport('${appId}', 'vue/CusButton').then(res=>{
    res.WffpCustomCodeLoadFn();
    return res;
  })
); 

let bodyDom = document.querySelector("body");
let divDom = document.createElement("div");
ReactDOM.render(<Suspense fallback={<></>}>
  <CusButton/>
</Suspense>, divDom);
bodyDom.appendChild(divDom)


//CusButton.vue

<script setup>
  import React, {createElement} from 'react';
  import {applyPureReactInVue} from '@weapp/ecodesdk';
  import {Button,Dialog} from '@weapp/ui';
  import {defineProps,ref} from 'vue';

  const UIButton = applyPureReactInVue(Button);
  const UIDialog = applyPureReactInVue(Dialog);

  const dialogVisible = ref(false);
  const handleUIButton = ()=>{
    dialogVisible.value = true;
  }
 
  const onCloseDialog = ()=>{
    dialogVisible.value = false;
  }

  const onDialogButtonClick=()=>{
    alert(1);
  }
  //因为Dilog是react组件，所以要用createElement, 和h函数是差不多的
  const buttons = ref([
    createElement(Button,{type:"primary", onClick:onDialogButtonClick},"按钮")
  ]);
</script>

<template>
  
  <UIButton type="primary" :style="{
    position:'fixed',
    top:200,
    left:'50%',
    }" @click="handleUIButton">自定义vue按钮</UIButton>

  <UIDialog
    :visible="dialogVisible"
    @close="onCloseDialog"
    title="基础对话框"
    width="600"
    closable="true"
    destroyOnClose="true"
    scale="true"
    icon="Icon--Import-and-export"
    :buttons="buttons"
  >
    <p>DialogBody</p>
    <p>show everything...</p>
    <p>show everything...</p>
  </UIDialog>
</template>

```