fix: conflict

This commit is contained in:
bac-joker 2021-03-25 18:06:13 +08:00
commit 02231d3335
40 changed files with 1472 additions and 46 deletions

View File

@ -19,6 +19,7 @@ const headPkgs = [
"fes-plugin-jest",
"fes-plugin-vuex",
"create-fes-app",
"fes-plugin-qiankun"
];
const tailPkgs = [];
// const otherPkgs = readdirSync(join(__dirname, 'packages')).filter(

View File

@ -60,6 +60,7 @@ export const en: SidebarConfig = {
'/reference/plugin/plugins/model.md',
'/reference/plugin/plugins/request.md',
'/reference/plugin/plugins/vuex.md',
'/reference/plugin/plugins/qiankun.md',
],
},
{

View File

@ -60,6 +60,7 @@ export const zh: SidebarConfig = {
'/zh/reference/plugin/plugins/model.md',
'/zh/reference/plugin/plugins/request.md',
'/zh/reference/plugin/plugins/vuex.md',
'/zh/reference/plugin/plugins/qiankun.md',
],
},
{

View File

@ -20,10 +20,12 @@ export default {
### mode
创建历史记录的类型:
- **h5**,对应 [createWebHistory](https://next.router.vuejs.org/zh/api/#createwebhistory)
- **history**,对应 [createWebHistory](https://next.router.vuejs.org/zh/api/#createwebhistory)
- **hash**,对应 [createWebHashHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory)
- **memory**,对应 [createMemoryHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory)
默认是`hash`模式。
## 约定式路由
约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。

View File

@ -1,8 +1,67 @@
# @fesjs/plugin-model
## 启用方式
在 package.json 中引入依赖:
```json
{
"dependencies": {
"@fesjs/fes": "^2.0.0",
"@fesjs/plugin-model": "^2.0.0"
},
}
```
## 介绍
一种简易的数据管理方案。我们知道 Vue 的理念是用响应式数据驱动UI更新提供 `reactive``ref` 等API把数据变成响应式的。我们使用`Provide / Inject`特性,在应用实例中共享响应式数据。
## 配置
我们约定`src/models` 目录下的文件为项目定义的 `model` 文件。每个文件需要默认导出一个 `function`
文件名则对应最终 `model``name`,你可以通过插件提供的 `API` 来消费 `model` 中的数据。
### Model 文件
**src/models/useAuthModel.js**
```js
import { reactive } from 'vue'
export default function useAuthModel() {
const user = reactive({});
const signin = ()=>{
// todo
}
const signout = ()=>{
// todo
}
return {
user,
signin,
signout
}
}
```
### 在组件中使用 Model
```vue
<script>
import { useModel } from "@fesjs/fes"
export default {
setup(){
const { user, signin, signout } = useModel("useAuthModel")
}
}
</script>
```
## API
### useModel
**useModel(name)**
- **类型**:函数
- **详情**: 获取 Model 数据,也就是 Model 文件默认导出函数执行的结果。
- **参数**
- name传入 Model 文件名
## API

View File

@ -0,0 +1,247 @@
# @fesjs/plugin-qiankun
Fes.js plugin for [qiankun](https://qiankun.umijs.org/),参考[@umijs/plugin-qiankun](https://umijs.org/zh-CN/plugins/plugin-qiankun#MicroApp) 实现,喜欢 React 的同学推荐直接用 Umi。
## 启用方式
`package.json` 中引入依赖:
```json
{
"dependencies": {
"@fesjs/fes": "^2.0.0",
"@fesjs/plugin-qiankun": "^2.0.0"
},
}
```
## 介绍
有一种痛叫接手老项目,技术栈老旧,内容多,还要继续维护~
可能目前迁移、升级老项目最好的解决方案就是微前端。`plugin-qiankun` 是基于 `qiankun` 实现的 Fes.js 微前端解决方案。
## 主应用配置
### 第一步:注册子应用
```js
export default {
qiankun: {
main: {
// 注册子应用信息
apps: [
{
name: 'app1', // 唯一 id
entry: '//localhost:8001', // html entry
props: {} // 传递给子应用的数据
},
{
name: 'app2', // 唯一 id
entry: '//localhost:8002', // html entry
},
],
},
},
};
```
### 第二步:装载子应用
#### 使用路由绑定的方式
:::warning
主应用和子应用需要自行适配路由路径!!!待完善...
:::
假设我们的系统之前有这样的一些路由:
```js
export default {
router: {
routes: [{
"path": "/",
"component": () => import('@/src/.fes/plugin-layout/index.js'),
"children": [
{
"path": "/onepiece",
"component": () => import('@/pages/onepiece'),
"name": "onepiece",
"meta": {
"name": "onepiece",
"title": "onepiece"
}
}
]
}]
}
}
```
我们现在想在 `/son` 加载子应用 `app1`,只需要增加这样一些配置即可:
```js {16-23}
export default {
router: {
routes: [{
"path": "/",
"component": () => import('@/src/.fes/plugin-layout/index.js'),
"children": [
{
"path": "/onepiece",
"component": () => import('@/pages/onepiece'),
"name": "onepiece",
"meta": {
"name": "onepiece",
"title": "onepiece"
}
},
{
"path": "/son",
"meta": {
"name": "son",
"title": "子应用",
"microApp": "app1"
}
}
]
}]
}
}
```
当前我们依然提倡约定路由的方式,在`src/pages` 目录新建 `son.vue`
```vue
<config>
{
"name": "son",
"title": "子应用",
"microApp": "app1"
}
</config>
```
#### 使用 `<MicroApp />` 组件的方式
:::tip
建议使用这种方式来引入不带路由的子应用。 否则请自行关注子应用依赖的路由跟当前浏览器 url 是否能正确匹配上,否则很容易出现子应用加载了,但是页面没有渲染出来的情况。
:::
```vue
<template>
<MicroApp :name="name" />
</template>
<script>
import { MicroApp } from '@fesjs/fes';
export default {
components: { MicroApp },
setup(){
const name = "app1"
return {
name
}
}
}
</script>
```
## 子应用配置
### 第一步:插件注册
```js
export default {
qiankun: {
micro: {},
}
};
```
### 第二步:配置运行时生命周期钩子(可选)
插件会自动为你创建好 `qiankun` 子应用需要的生命周期钩子,但是如果你想在生命周期期间加一些自定义逻辑,可以在子应用的 `src/app.js` 里导出 `qiankun` 对象,并实现每一个生命周期钩子,其中钩子函数的入参 `props` 由主应用自动注入。
```js
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 当 props 更新时触发
async update(props){
console.log('app1 update,' props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
```
## 父子应用通讯
有两种方式实现
### 配合 [useModel](./model.md) 使用
确保已经安装了 `@fesjs/plugin-model`
```json
{
"dependencies": {
"@fesjs/fes": "^2.0.0",
"@fesjs/plugin-model": "^2.0.0"
},
}
```
#### 主应用传递 props
- 如果使用 `MicroApp` 组件模式消费子应用,直接通过 props 传递即可:
```vue
<template>
<MicroApp :name="name" :user="user" />
</template>
<script>
import { MicroApp } from '@fesjs/fes';
export default {
components: { MicroApp },
setup(){
const name = "app1"
const user = ref("")
return {
name,
user
}
}
}
</script>
```
- 如果使用路由绑定式消费子应用,那么约定`src/models/qiankunStateForMicro.js` 的模型数据将作为 `props` 船体给子应用,如:
```js
import { reactive } from 'vue';
export default () => {
const state = reactive({ c: 1 });
return {
state
};
};
```
#### 子应用消费 props
子应用中会自动生成一个全局名为 `qiankunStateFromMain``model` 可以在任意组件中获取主应用透传的 `props` 的值。
```vue
<script>
export default {
setup(){
const mainState = useModel('qiankunStateFromMain');
return {
mainState
};
}
}
</script>
```
### 基于 props 传递
- 主应用使用 props 的模式传递数据(参考主应用装载子应用配置一节)
- 子应用在生命周期钩子中获取 props 消费数据(参考子应用运行时配置一节)

View File

@ -20,10 +20,12 @@ export default {
### mode
创建历史记录的类型:
- **h5**,对应 [createWebHistory](https://next.router.vuejs.org/zh/api/#createwebhistory)
- **history**,对应 [createWebHistory](https://next.router.vuejs.org/zh/api/#createwebhistory)
- **hash**,对应 [createWebHashHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory)
- **memory**,对应 [createMemoryHistory](https://next.router.vuejs.org/zh/api/#createWebHashHistory)
默认是`hash`模式。
## 约定式路由
约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。

View File

@ -1,8 +1,67 @@
# @fesjs/plugin-model
## 启用方式
在 package.json 中引入依赖:
```json
{
"dependencies": {
"@fesjs/fes": "^2.0.0",
"@fesjs/plugin-model": "^2.0.0"
},
}
```
## 介绍
一种简易的数据管理方案。我们知道 Vue 的理念是用响应式数据驱动UI更新提供 `reactive``ref` 等API把数据变成响应式的。我们使用`Provide / Inject`特性,在应用实例中共享响应式数据。
## 配置
我们约定`src/models` 目录下的文件为项目定义的 `model` 文件。每个文件需要默认导出一个 `function`
文件名则对应最终 `model``name`,你可以通过插件提供的 `API` 来消费 `model` 中的数据。
### Model 文件
**src/models/useAuthModel.js**
```js
import { reactive } from 'vue'
export default function useAuthModel() {
const user = reactive({});
const signin = ()=>{
// todo
}
const signout = ()=>{
// todo
}
return {
user,
signin,
signout
}
}
```
### 在组件中使用 Model
```vue
<script>
import { useModel } from "@fesjs/fes"
export default {
setup(){
const { user, signin, signout } = useModel("useAuthModel")
}
}
</script>
```
## API
### useModel
**useModel(name)**
- **类型**:函数
- **详情**: 获取 Model 数据,也就是 Model 文件默认导出函数执行的结果。
- **参数**
- name传入 Model 文件名
## API

View File

@ -0,0 +1,247 @@
# @fesjs/plugin-qiankun
Fes.js plugin for [qiankun](https://qiankun.umijs.org/),参考[@umijs/plugin-qiankun](https://umijs.org/zh-CN/plugins/plugin-qiankun#MicroApp) 实现,喜欢 React 的同学推荐直接用 Umi。
## 启用方式
`package.json` 中引入依赖:
```json
{
"dependencies": {
"@fesjs/fes": "^2.0.0",
"@fesjs/plugin-qiankun": "^2.0.0"
},
}
```
## 介绍
有一种痛叫接手老项目,技术栈老旧,内容多,还要继续维护~
可能目前迁移、升级老项目最好的解决方案就是微前端。`plugin-qiankun` 是基于 `qiankun` 实现的 Fes.js 微前端解决方案。
## 主应用配置
### 第一步:注册子应用
```js
export default {
qiankun: {
main: {
// 注册子应用信息
apps: [
{
name: 'app1', // 唯一 id
entry: '//localhost:8001', // html entry
props: {} // 传递给子应用的数据
},
{
name: 'app2', // 唯一 id
entry: '//localhost:8002', // html entry
},
],
},
},
};
```
### 第二步:装载子应用
#### 使用路由绑定的方式
:::warning
主应用和子应用需要自行适配路由路径!!!待完善...
:::
假设我们的系统之前有这样的一些路由:
```js
export default {
router: {
routes: [{
"path": "/",
"component": () => import('@/src/.fes/plugin-layout/index.js'),
"children": [
{
"path": "/onepiece",
"component": () => import('@/pages/onepiece'),
"name": "onepiece",
"meta": {
"name": "onepiece",
"title": "onepiece"
}
}
]
}]
}
}
```
我们现在想在 `/son` 加载子应用 `app1`,只需要增加这样一些配置即可:
```js {16-23}
export default {
router: {
routes: [{
"path": "/",
"component": () => import('@/src/.fes/plugin-layout/index.js'),
"children": [
{
"path": "/onepiece",
"component": () => import('@/pages/onepiece'),
"name": "onepiece",
"meta": {
"name": "onepiece",
"title": "onepiece"
}
},
{
"path": "/son",
"meta": {
"name": "son",
"title": "子应用",
"microApp": "app1"
}
}
]
}]
}
}
```
当前我们依然提倡约定路由的方式,在`src/pages` 目录新建 `son.vue`
```vue
<config>
{
"name": "son",
"title": "子应用",
"microApp": "app1"
}
</config>
```
#### 使用 `<MicroApp />` 组件的方式
:::tip
建议使用这种方式来引入不带路由的子应用。 否则请自行关注子应用依赖的路由跟当前浏览器 url 是否能正确匹配上,否则很容易出现子应用加载了,但是页面没有渲染出来的情况。
:::
```vue
<template>
<MicroApp :name="name" />
</template>
<script>
import { MicroApp } from '@fesjs/fes';
export default {
components: { MicroApp },
setup(){
const name = "app1"
return {
name
}
}
}
</script>
```
## 子应用配置
### 第一步:插件注册
```js
export default {
qiankun: {
micro: {},
}
};
```
### 第二步:配置运行时生命周期钩子(可选)
插件会自动为你创建好 `qiankun` 子应用需要的生命周期钩子,但是如果你想在生命周期期间加一些自定义逻辑,可以在子应用的 `src/app.js` 里导出 `qiankun` 对象,并实现每一个生命周期钩子,其中钩子函数的入参 `props` 由主应用自动注入。
```js
export const qiankun = {
// 应用加载之前
async bootstrap(props) {
console.log('app1 bootstrap', props);
},
// 应用 render 之前触发
async mount(props) {
console.log('app1 mount', props);
},
// 当 props 更新时触发
async update(props){
console.log('app1 update,' props);
},
// 应用卸载之后触发
async unmount(props) {
console.log('app1 unmount', props);
},
};
```
## 父子应用通讯
有两种方式实现
### 配合 [useModel](./model.md) 使用
确保已经安装了 `@fesjs/plugin-model`
```json
{
"dependencies": {
"@fesjs/fes": "^2.0.0",
"@fesjs/plugin-model": "^2.0.0"
},
}
```
#### 主应用传递 props
- 如果使用 `MicroApp` 组件模式消费子应用,直接通过 props 传递即可:
```vue
<template>
<MicroApp :name="name" :user="user" />
</template>
<script>
import { MicroApp } from '@fesjs/fes';
export default {
components: { MicroApp },
setup(){
const name = "app1"
const user = ref("")
return {
name,
user
}
}
}
</script>
```
- 如果使用路由绑定式消费子应用,那么约定`src/models/qiankunStateForMicro.js` 的模型数据将作为 `props` 船体给子应用,如:
```js
import { reactive } from 'vue';
export default () => {
const state = reactive({ c: 1 });
return {
state
};
};
```
#### 子应用消费 props
子应用中会自动生成一个全局名为 `qiankunStateFromMain``model` 可以在任意组件中获取主应用透传的 `props` 的值。
```vue
<script>
export default {
setup(){
const mainState = useModel('qiankunStateFromMain');
return {
mainState
};
}
}
</script>
```
### 基于 props 传递
- 主应用使用 props 的模式传递数据(参考主应用装载子应用配置一节)
- 子应用在生命周期钩子中获取 props 消费数据(参考子应用运行时配置一节)

View File

@ -40,7 +40,7 @@ export const fillMenuByRoute = (menuConfig, routeConfig, dep = 0) => {
if (menu.icon) {
const icon = menu.icon;
const urlReg = /^((https?|ftp|file):\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
if (!(urlReg.test(icon) || icon.includes('.svg'))) {
if (typeof icon === 'string' && !((urlReg.test(icon) || icon.includes('.svg')))) {
if (!allIcons[icon]) {
menu.icon = {
type: 'icon',

View File

@ -24,12 +24,12 @@ export default (api) => {
function getAllModels() {
const srcModelsPath = getModelsPath();
return lodash.uniq([
...getModels(srcModelsPath),
...getModels(
paths.absPagesPath,
`**/${getModelDir()}/**/*.{js,jsx}`
),
...getModels(paths.absPagesPath, '**/*.model.{js,jsx}')
...getModels(srcModelsPath)
// ...getModels(
// paths.absPagesPath,
// `**/${getModelDir()}/**/*.{js,jsx}`
// ),
// ...getModels(paths.absPagesPath, '**/*.model.{js,jsx}')
]);
}

View File

@ -24,11 +24,11 @@ function getExtraImports(models = [], absSrcPath) {
.map((ele) => {
if (ele.exportName) {
return `import { ${ele.exportName} } from '${winPath(
ele.importPath.replace(/'/g, "\\'"),
ele.importPath.replace(/'/g, "\\'")
)}';`;
}
return `import ${ele.importName} from '${winPath(
ele.importPath.replace(/'/g, "\\'"),
ele.importPath.replace(/'/g, "\\'")
)}';`;
})
.join(EOL);
@ -37,7 +37,7 @@ function getExtraImports(models = [], absSrcPath) {
export const getTmpFile = (
files,
extra = [],
absSrcPath,
absSrcPath
) => {
const userImports = genImports(files);
const userModels = getModels(files, absSrcPath);

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present webank
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,39 @@
{
"name": "@fesjs/plugin-qiankun",
"version": "2.0.0-alpha.0",
"description": "@fesjs/plugin-qiankun",
"main": "lib/index.js",
"files": [
"lib"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WeBankFinTech/fes.js.git",
"directory": "packages/fes-plugin-qiankun"
},
"keywords": [
"fes"
],
"author": "michaelxxie",
"license": "MIT",
"bugs": {
"url": "https://github.com/WeBankFinTech/fes.js/issues"
},
"homepage": "https://github.com/WeBankFinTech/fes.js#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@umijs/utils": "3.3.3",
"address": "^1.1.2",
"lodash": "^4.17.15",
"qiankun": "2.3.4"
},
"peerDependencies": {
"@webank/fes": "^2.0.0-rc.0",
"vue": "^3.0.5"
}
}

View File

@ -0,0 +1,4 @@
export const defaultMainRootId = 'root-master';
export const defaultHistoryType = 'hash';
export const qiankunStateForMicroModelNamespace = 'qiankunStateForMicro';
export const qiankunStateFromMainModelNamespace = 'qiankunStateFromMain';

View File

@ -0,0 +1,20 @@
export default (api) => {
api.describe({
key: 'qiankun',
config: {
schema(joi) {
return joi.object().keys({
micro: joi.object(),
main: joi.object()
});
}
}
});
api.addRuntimePluginKey(() => 'qiankun');
api.registerPlugins([
require.resolve('./main'),
require.resolve('./micro')
]);
};

View File

@ -0,0 +1,111 @@
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import {
defaultMainRootId,
defaultHistoryType,
qiankunStateForMicroModelNamespace
} from '../constants';
import modifyRoutes from './modifyRoutes';
const namespace = 'plugin-qiankun/main';
export function isMasterEnable(api) {
return (
!!api.userConfig?.qiankun?.main
|| !!process.env.INITIAL_QIANKUN_MAIN_OPTIONS
);
}
export default function (api) {
const {
utils: { Mustache, winPath }
} = api;
api.describe({
enableBy: () => isMasterEnable(api)
});
api.modifyDefaultConfig(config => ({
...config,
mountElementId: defaultMainRootId
}));
modifyRoutes({ api, namespace });
const absMicroAppPath = join(namespace, 'MicroApp.js');
const absRuntimePath = join(namespace, 'runtime.js');
const absMasterOptionsPath = join(namespace, 'masterOptions.js');
const absGetMicroAppRouteCompPath = join(
namespace,
'getMicroAppRouteComponent.js'
);
api.onGenerateFiles(() => {
const HAS_PLUGIN_MODEL = api.hasPlugins(['@fesjs/plugin-model']);
api.writeTmpFile({
path: absMicroAppPath,
content: Mustache.render(
readFileSync(join(__dirname, 'runtime/MicroApp.tpl'), 'utf-8'),
{
qiankunStateForMicroModelNamespace,
HAS_PLUGIN_MODEL:
HAS_PLUGIN_MODEL
&& existsSync(
winPath(
join(
api.paths.absSrcPath,
'models/qiankunStateForMicro.js'
)
)
)
}
)
});
api.writeTmpFile({
path: absRuntimePath,
content: readFileSync(
join(__dirname, 'runtime/runtime.tpl'),
'utf-8'
)
});
api.writeTmpFile({
path: absGetMicroAppRouteCompPath,
content: readFileSync(
join(__dirname, 'runtime/getMicroAppRouteComponent.tpl'),
'utf-8'
)
});
const { main: options } = api.config?.qiankun || {};
const masterHistoryType = api.config?.router?.mode || defaultHistoryType;
const base = api.config.base || '/';
api.writeTmpFile({
path: absMasterOptionsPath,
content: `
let options = ${JSON.stringify({
masterHistoryType,
base,
...options
})};
export const getMasterOptions = () => options;
export const setMasterOptions = (newOpts) => options = ({ ...options, ...newOpts });
`
});
});
api.addPluginExports(() => [
{
specifiers: ['MicroApp'],
source: absMicroAppPath
}
]);
api.addPluginExports(() => [
{
specifiers: ['getMicroAppRouteComponent'],
source: absGetMicroAppRouteCompPath
}
]);
}

View File

@ -0,0 +1,58 @@
import { defaultHistoryType } from '../constants';
function getMicroApp(options) {
const {
microAppName, masterHistoryType, base, namespace, ...normalizedRouteProps
} = options;
return `(() => {
const { getMicroAppRouteComponent } = require('@@/${namespace}/getMicroAppRouteComponent');
return getMicroAppRouteComponent({ appName: '${microAppName}', base: '${base}', masterHistoryType: '${masterHistoryType}', routeProps: ${JSON.stringify(normalizedRouteProps)} })
})()`;
}
function modifyRoutesWithAttachMode({
routes, masterHistoryType, base, namespace
}) {
const patchRoutes = (_routes) => {
if (_routes.length) {
_routes.forEach((route) => {
if (route.meta && route.meta.microApp) {
route.component = getMicroApp({
microAppName: route.meta.microApp,
masterHistoryType,
base,
namespace
});
}
if (route.children?.length) {
modifyRoutesWithAttachMode({
routes: route.children,
masterHistoryType,
base,
namespace
});
}
});
}
};
patchRoutes(routes);
return routes;
}
export default function modifyRoutes({ api, namespace }) {
api.modifyRoutes((routes) => {
const { router, base } = api.config;
const masterHistoryType = (router && router?.mode) || defaultHistoryType;
modifyRoutesWithAttachMode({
routes,
masterHistoryType,
base: base || '/',
namespace
});
return routes;
});
}

View File

@ -0,0 +1,180 @@
import {
defineComponent,
ref,
watch,
computed,
onBeforeUnmount,
onMounted,
} from "vue";
import { loadMicroApp } from "qiankun";
import mergeWith from "lodash/mergeWith";
// eslint-disable-next-line import/extensions
import { getMasterOptions } from "./masterOptions";
{{#HAS_PLUGIN_MODEL}}
import { useModel } from '@@/core/pluginExports';
{{/HAS_PLUGIN_MODEL}}
import { onBeforeRouteLeave } from "@@/core/coreExports";
let unmountPromise;
async function unmountMicroApp(microApp) {
if (microApp) {
if (microApp.mountPromise) {
await microApp.mountPromise;
}
if (!unmountPromise) {
unmountPromise = microApp.unmount();
}
return await unmountPromise;
}
return Promise.resolve();
}
export const MicroApp = defineComponent({
props: {
name: {
type: String,
required: true,
},
settings: Object,
lifeCycles: Object,
className: String,
},
setup(props, { attrs }) {
const {
masterHistoryType,
apps = [],
lifeCycles: globalLifeCycles,
...globalSettings
} = getMasterOptions();
{{#HAS_PLUGIN_MODEL}}
// 约定使用 src/models/qiankunStateForMicro 中的数据作为主应用透传给微应用的 props优先级高于 propsFromConfig
const stateForSlave = useModel('{{{qiankunStateForMicroModelNamespace}}}');
{{/HAS_PLUGIN_MODEL}}
{{^HAS_PLUGIN_MODEL}}
const stateForSlave = reactive({});
{{/HAS_PLUGIN_MODEL}}
// 挂载节点
const containerRef = ref(null);
const microAppRef = ref();
const updatingPromiseRef = ref();
const updatingTimestampRef = ref(Date.now());
const appConfigRef = computed(() => {
const appConfig = apps.find((app) => app.name === props.name);
if (!appConfig) {
throw new Error(
`[@fesjs/plugin-qiankun]: Can not find the configuration of ${props.name} app!`
);
}
return appConfig;
});
const propsFromConfigRef = computed(() => {
const appConfig = appConfigRef.value;
if (appConfig) {
return appConfig.props;
}
return {};
});
const propsFromParams = attrs;
// 只有当name变化时才重新加载新的子应用
const loadApp = () => {
const appConfig = appConfigRef.value;
const { name, entry } = appConfig;
// 加载新的
microAppRef.value = loadMicroApp(
{
name: name,
entry: entry,
container: containerRef.value,
props: {
...propsFromConfigRef.value,
...stateForSlave,
...propsFromParams,
},
},
{
...globalSettings,
...(props.settings || {}),
},
mergeWith({}, globalLifeCycles || {}, props.lifeCycles || {}, (v1, v2) =>
concat(v1 ?? [], v2 ?? [])
)
);
};
// 当参数变化时update子应用
const updateApp = () => {
const microApp = microAppRef.value;
if (microApp) {
if (!updatingPromiseRef.value) {
// 初始化 updatingPromiseRef 为 microApp.mountPromise从而确保后续更新是在应用 mount 完成之后
updatingPromiseRef.value = microApp.mountPromise;
} else {
// 确保 microApp.update 调用是跟组件状态变更顺序一致的,且后一个微应用更新必须等待前一个更新完成
updatingPromiseRef.value = updatingPromiseRef.value.then(
() => {
const canUpdate = (app) =>
app?.update && app.getStatus() === "MOUNTED";
if (canUpdate(microApp)) {
if (process.env.NODE_ENV === "development") {
if (
Date.now() -
updatingTimestampRef.value <
200
) {
console.warn(
`[@fesjs/plugin-qiankun] It seems like microApp ${props.name} is updating too many times in a short time(200ms), you may need to do some optimization to avoid the unnecessary re-rendering.`
);
}
console.info(
`[@fesjs/plugin-qiankun] MicroApp ${props.name} is updating with props: `,
props
);
updatingTimestampRef.value = Date.now();
}
// 返回 microApp.update 形成链式调用
return microApp.update({
...propsFromConfigRef.value,
...stateForSlave,
...propsFromParams,
});
}
}
);
}
}
};
onMounted(() => {
loadApp();
});
onBeforeUnmount(() => {
unmountMicroApp(microAppRef.value);
});
watch(appConfigRef, () => {
unmountMicroApp(microAppRef.value);
loadApp();
});
onBeforeRouteLeave(async () => {
return await unmountMicroApp(microAppRef.value);
});
watch(()=>{
return {...{}, ...propsFromConfigRef.value, ...stateForSlave, ...propsFromParams}
}, () => {
updateApp();
});
return () => <div ref={containerRef} className={props.className}></div>;
},
});

View File

@ -0,0 +1,11 @@
import { MicroApp } from './MicroApp';
export function getMicroAppRouteComponent({
appName,
base,
masterHistoryType,
routeProps
}) {
return <MicroApp base={base} masterHistoryType={masterHistoryType} name={appName} {...routeProps} />;
}

View File

@ -0,0 +1,11 @@
import { reactive } from 'vue';
let initState;
const setModelState = (val) => {
initState = val;
};
export default () => reactive(initState);
export { setModelState };

View File

@ -0,0 +1,176 @@
import assert from 'assert';
import address from 'address';
import { lodash } from '@umijs/utils';
import { readFileSync } from 'fs';
import { join } from 'path';
import { qiankunStateFromMainModelNamespace } from '../constants';
const namespace = 'plugin-qiankun/micro';
export function isSlaveEnable(api) {
return (
!!api.userConfig?.qiankun?.micro
|| lodash.isEqual(api.userConfig?.qiankun, {})
|| !!process.env.INITIAL_QIANKUN_MIRCO_OPTIONS
);
}
export default function (api) {
const {
utils: { Mustache }
} = api;
api.describe({
enableBy: () => isSlaveEnable(api)
});
api.modifyDefaultConfig((memo) => {
const initialMicroOptions = {
devSourceMap: true,
...JSON.parse(process.env.INITIAL_QIANKUN_MIRCO_OPTIONS || '{}'),
...(memo.qiankun || {}).micro
};
const modifiedDefaultConfig = {
...memo,
runtimePublicPath: true,
qiankun: {
...memo.qiankun,
slave: initialMicroOptions
}
};
const shouldNotModifyDefaultBase = api.userConfig.qiankun?.slave?.shouldNotModifyDefaultBase
?? initialMicroOptions.shouldNotModifyDefaultBase;
if (!shouldNotModifyDefaultBase) {
modifiedDefaultConfig.base = `/${api.pkg.name}`;
}
return modifiedDefaultConfig;
});
api.chainWebpack((config) => {
assert(api.pkg.name, 'You should have name in package.json');
config.output.libraryTarget('umd').library(`${api.pkg.name}-[name]`);
return config;
});
const port = process.env.PORT;
// source-map 跨域设置
if (process.env.NODE_ENV === 'development' && port) {
const localHostname = process.env.USE_REMOTE_IP
? address.ip()
: process.env.HOST || 'localhost';
const protocol = process.env.HTTPS ? 'https' : 'http';
// TODO: 变更 webpack-dev-server websocket 默认监听地址
api.chainWebpack((memo, { webpack }) => {
// 开启了 devSourceMap 配置,默认为 true
if (
api.config.qiankun
&& api.config.qiankun.micro
&& api.config.qiankun.micro.devSourceMap !== false
) {
// 禁用 devtool启用 SourceMapDevToolPlugin
memo.devtool(false);
memo.plugin('source-map').use(webpack.SourceMapDevToolPlugin, [
{
// @ts-ignore
namespace: api.pkg.name,
append: `\n//# sourceMappingURL=${protocol}://${localHostname}:${port}/[url]`,
filename: '[file].map'
}
]);
}
return memo;
});
}
const absRuntimePath = join(namespace, 'runtime.js');
const absLifeclesPath = join(namespace, 'lifecycles.js');
const absMicroOptionsPath = join(namespace, 'slaveOptions.js');
const absPublicPath = join(namespace, 'publicPath.js');
const absModelPath = join(namespace, 'qiankunModel.js');
// 更改public path
api.addEntryImportsAhead(() => [{ source: `@@/${absPublicPath}` }]);
api.register({
key: 'addExtraModels',
fn: () => {
const HAS_PLUGIN_MODEL = api.hasPlugins(['@fesjs/plugin-model']);
return HAS_PLUGIN_MODEL ? [{
absPath: `@@/${absModelPath}`,
namespace: qiankunStateFromMainModelNamespace
}] : [];
}
});
api.onGenerateFiles(() => {
const HAS_PLUGIN_MODEL = api.hasPlugins(['@fesjs/plugin-model']);
api.writeTmpFile({
path: absRuntimePath,
content: readFileSync(
join(__dirname, 'runtime/runtime.tpl'),
'utf-8'
)
});
api.writeTmpFile({
path: absLifeclesPath,
content: Mustache.render(readFileSync(
join(__dirname, 'runtime/lifecycles.tpl'),
'utf-8'
), {
HAS_PLUGIN_MODEL
})
});
api.writeTmpFile({
path: absPublicPath,
content: `
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
window.public_path = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
`
});
api.writeTmpFile({
path: absMicroOptionsPath,
content: `
let options = ${JSON.stringify(
(api.config.qiankun || {}).micro || {}
)};
export const getSlaveOptions = () => options;
export const setSlaveOptions = (newOpts) => options = ({ ...options, ...newOpts });
`
});
if (HAS_PLUGIN_MODEL) {
api.writeTmpFile({
path: absModelPath,
content: readFileSync(join(__dirname, 'runtime/qiankunModel.tpl'), 'utf-8')
});
}
});
api.addEntryImports(() => ({
source: `@@/${absLifeclesPath}`,
specifier:
'{ genMount as qiankun_genMount, genBootstrap as qiankun_genBootstrap, genUnmount as qiankun_genUnmount, genUpdate as qiankun_genUpdate }'
}));
api.addEntryCode(
() => `
export const bootstrap = qiankun_genBootstrap(completeClientRender, app);
export const mount = qiankun_genMount('#${api.config.mountElementId}');
export const unmount = qiankun_genUnmount('#${api.config.mountElementId}');
export const update = qiankun_genUpdate();
if (!window.__POWERED_BY_QIANKUN__) {
bootstrap().then(mount);
}
`
);
}

View File

@ -0,0 +1,105 @@
import { plugin, ApplyPluginsType } from '@@/core/coreExports';
{{#HAS_PLUGIN_MODEL}}
import { setModelState } from './qiankunModel';
{{/HAS_PLUGIN_MODEL}}
const defer = {};
defer.promise = new Promise((resolve) => {
defer.resolve = resolve;
});
function isPromise(obj) {
return !!obj // 有实际含义的变量才执行方法变量nullundefined和''空串都为false
&& (typeof obj === 'object' || typeof obj === 'function') // 初始promise 或 promise.then返回的
&& typeof obj.then === 'function';
}
let render = () => {};
let cacheAppPromise = null;
let hasMountedAtLeastOnce = false;
export default () => defer.promise;
function getSlaveRuntime() {
const config = plugin.applyPlugins({
key: 'qiankun',
type: ApplyPluginsType.modify,
initialValue: {}
});
const { slave } = config;
return slave || config;
}
// 子应用生命周期钩子Bootstrap
export function genBootstrap(oldRender, appPromise) {
return async (props) => {
const slaveRuntime = getSlaveRuntime();
if (slaveRuntime.bootstrap) {
await slaveRuntime.bootstrap(props);
}
render = oldRender;
if (isPromise(appPromise)) {
cacheAppPromise = appPromise;
}
};
}
// 子应用生命周期钩子Mount
export function genMount() {
return async (props) => {
// props 有值时说明应用是通过 lifecycle 被主应用唤醒的,而不是独立运行时自己 mount
if (typeof props !== 'undefined') {
{{#HAS_PLUGIN_MODEL}}
setModelState(props);
{{/HAS_PLUGIN_MODEL}}
const slaveRuntime = getSlaveRuntime();
if (slaveRuntime.mount) {
await slaveRuntime.mount(props);
}
}
// 第一次 mount 会自动触发 render非第一次 mount 则需手动触发
if (hasMountedAtLeastOnce) {
const appPromise = render();
if (isPromise(appPromise)) {
cacheAppPromise = appPromise;
}
} else {
defer.resolve();
}
hasMountedAtLeastOnce = true;
};
}
export function genUpdate() {
return async (props) => {
{{#HAS_PLUGIN_MODEL}}
setModelState(props);
{{/HAS_PLUGIN_MODEL}}
const slaveRuntime = await getSlaveRuntime();
if (slaveRuntime.update) {
await slaveRuntime.update(props);
}
};
}
// 子应用生命周期钩子Unmount
export function genUnmount(mountElementId) {
return async (props) => {
let container;
try {
container = props?.container
? props.container.querySelector(mountElementId)
: document.querySelector(mountElementId);
} catch (e) {}
if (container && cacheAppPromise) {
const app = await cacheAppPromise;
app.unmount(container);
}
const slaveRuntime = getSlaveRuntime();
if (slaveRuntime.unmount) {
await slaveRuntime.unmount(props);
}
};
}

View File

@ -0,0 +1,11 @@
import { reactive } from 'vue';
let initState;
const setModelState = (val) => {
initState = val;
};
export default () => reactive(initState);
export { setModelState };

View File

@ -44,6 +44,7 @@ export default function () {
require.resolve('./plugins/features/vueLoader'),
require.resolve('./plugins/features/mock'),
require.resolve('./plugins/features/dynamicImport'),
require.resolve('./plugins/features/runtimePublicPath'),
// misc
require.resolve('./plugins/misc/route'),

View File

@ -55,7 +55,16 @@ export async function getBundleAndConfigs({
type: api.ApplyPluginsType.add,
initialState: []
});
}
},
publicPath: await api.applyPlugins({
key: 'modifyPublicPathStr',
type: api.ApplyPluginsType.modify,
initialValue: api.config.publicPath || '',
args: {
// route: args.route
}
})
},
args: {
}

View File

@ -13,12 +13,12 @@ export default async function createHtmlWebpackConfig({
isProd
}) {
const htmlOptions = {
title: 'fes.js',
filename: '[name].html',
...config.html,
templateParameters: resolveDefine(config, true)
templateParameters: resolveDefine(config, true),
mountElementId: config.mountElementId
};
htmlOptions.title = htmlOptions.title || 'fes.js';
if (isProd) {
Object.assign(htmlOptions, {

View File

@ -7,6 +7,6 @@
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<div id="<%= htmlWebpackPlugin.options.mountElementId %>"></div>
</body>
</html>

View File

@ -66,7 +66,8 @@ export default async function getConfig({
modifyBabelOpts,
modifyBabelPresetOpts,
chainWebpack,
headScripts
headScripts,
publicPath
}) {
const isDev = env === 'development';
const isProd = env === 'production';
@ -93,7 +94,7 @@ export default async function getConfig({
// --------------- output -----------
webpackConfig.output
.path(absoluteOutput)
.publicPath(config.publicPath || '')
.publicPath(publicPath)
.filename('[name].[contenthash:8].js')
.chunkFilename('[name].[contenthash:8].chunk.js');

View File

@ -1,6 +1,6 @@
const prefixRE = /^FES_APP_/;
const ENV_SHOULD_PASS = ['NODE_ENV', 'FES_ENV', 'HMR', 'SOCKET_SERVER', 'ERROR_OVERLAY'];
const ENV_SHOULD_PASS = ['NODE_ENV', 'FES_ENV'];
export default function resolveDefine(opts = {}, raw) {
const env = {};

View File

@ -3,7 +3,7 @@ export default (api) => {
api.describe({
key: 'mountElementId',
config: {
default: '#app',
default: 'app',
schema(joi) {
return joi.string().allow('');
}

View File

@ -0,0 +1,12 @@
export default (api) => {
api.describe({
key: 'runtimePublicPath',
config: {
schema(joi) {
return joi.boolean();
}
},
default: false
});
};

View File

@ -62,22 +62,19 @@ const getClientRender = (args = {}) => plugin.applyPlugins({
args,
});
const beforeRenderConfig = plugin.applyPlugins({
key: "beforeRender",
type: ApplyPluginsType.modify,
initialValue: {
loading: null,
action: null
},
});
const beforeRender = async () => {
const beforeRenderConfig = plugin.applyPlugins({
key: "beforeRender",
type: ApplyPluginsType.modify,
initialValue: {
loading: null,
action: null
},
});
let initialState = {};
if (typeof beforeRenderConfig.action === "function") {
const app = createApp(beforeRenderConfig.loading);
app.mount("#app");
app.mount('{{{ rootElement }}}');
try {
initialState = await beforeRenderConfig.action();
} catch(e){
@ -89,13 +86,16 @@ const beforeRender = async () => {
return initialState;
};
const render = async () => {
const completeClientRender = async () => {
const initialState = await beforeRender();
const clientRender = getClientRender({initialState});
clientRender();
const app = clientRender();
return app;
};
render();
const app = completeClientRender();
export default app;
{{{ entryCode }}}

View File

@ -26,7 +26,7 @@ export default function (api) {
enableTitle: api.config.title !== false,
defaultTitle: api.config.title || '',
runtimePath,
rootElement: api.config.mountElementId || '#app',
rootElement: `#${api.config.mountElementId || 'app'}`,
entryCode: (
await api.applyPlugins({
key: 'addEntryCode',

View File

@ -265,7 +265,7 @@ export default function (api) {
const absRuntimeFilePath = join(namespace, 'runtime.js');
const historyType = {
h5: 'createWebHistory',
history: 'createWebHistory',
hash: 'createWebHashHistory',
memory: 'createMemoryHistory'
};

View File

@ -28,7 +28,8 @@ export default function (api) {
'modifyBabelOpts',
'modifyBabelPresetOpts',
'chainWebpack',
'addTmpGenerateWatcherPaths'
'addTmpGenerateWatcherPaths',
'modifyPublicPathStr'
].forEach((name) => {
api.registerMethod({ name });
});

View File

@ -55,6 +55,7 @@
"@fesjs/plugin-jest": "^2.0.0-rc.0",
"@fesjs/plugin-vuex": "^2.0.0-rc.0",
"@fesjs/plugin-request": "^2.0.0-rc.0",
"@fesjs/plugin-qiankun": "^2.0.0-alpha.0",
"ant-design-vue": "2.0.0",
"vue": "^3.0.5",
"vuex": "^4.0.0"

View File

@ -1587,6 +1587,13 @@
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.7.2":
version "7.13.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/template@^7.0.0", "@babel/template@^7.10.4", "@babel/template@^7.12.13", "@babel/template@^7.3.3", "@babel/template@^7.4.0":
version "7.12.13"
resolved "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz#530265be8a2589dbb37523844c5bcb55947fb327"
@ -4644,7 +4651,7 @@ acorn@^8.0.4:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.0.5.tgz#a3bfb872a74a6a7f661bc81b9849d9cac12601b7"
integrity sha512-v+DieK/HJkJOpFBETDJioequtc3PfxsWMaxIdIwujtF7FEV/MAyDQLlm6/zPvr7Mix07mLh6ccVwIsloceodlg==
address@1.1.2:
address@1.1.2, address@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6"
integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==
@ -9545,6 +9552,13 @@ import-from@^3.0.0:
dependencies:
resolve-from "^5.0.0"
import-html-entry@^1.9.0:
version "1.11.1"
resolved "https://registry.yarnpkg.com/import-html-entry/-/import-html-entry-1.11.1.tgz#3d8c5977926bdd122ab8e658965c102068b4af8d"
integrity sha512-O7mCUTwKdYU49/LH6nq1adWPnUlZQpKeGWIEcDq07KTcqP/v0jBLEIVc0oE0Mtlw3CEe0eeKGMyhl6LwfXCV7A==
dependencies:
"@babel/runtime" "^7.7.2"
import-lazy@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@ -12892,6 +12906,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.0.tgz#f7b3803336104c346889adece614669230645f38"
integrity sha512-f66KywYG6+43afgE/8j/GoiNyygk/bnoCbps++3ErRKsIYkGGupyv07R2Ok5m9i67Iqc+T2g1eAUGUPzWhYTyg==
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@ -13706,6 +13725,17 @@ q@^1.1.2, q@^1.5.1:
resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qiankun@2.3.4:
version "2.3.4"
resolved "https://registry.yarnpkg.com/qiankun/-/qiankun-2.3.4.tgz#a6a6382c1e909a76f9aea1708ff46276432428f2"
integrity sha512-LJ3luGH0eAQ3xd7vH7xUtAS57eGUs4bMiCcFQx1OJ94XJ3VdKIb97jqT5p5ibOj82EPQdLJhVsB5+phm4iEXfw==
dependencies:
"@babel/runtime" "^7.10.5"
import-html-entry "^1.9.0"
lodash "^4.17.11"
single-spa "5.8.1"
tslib "^1.10.0"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
@ -14783,6 +14813,11 @@ simple-swizzle@^0.2.2:
dependencies:
is-arrayish "^0.3.1"
single-spa@5.8.1:
version "5.8.1"
resolved "https://registry.yarnpkg.com/single-spa/-/single-spa-5.8.1.tgz#86c2575e297e31d8f06945944ec97e31851a59ae"
integrity sha512-RlyLZ1IDIPdzI6mQPzCQnlgTt9jmbAXBZODmifoDut840wksPDSPhcSS8jXMpuUlqOidQiX2YuLVQSR9DEgsXw==
sirv@^1.0.7:
version "1.0.11"
resolved "https://registry.npmjs.org/sirv/-/sirv-1.0.11.tgz#81c19a29202048507d6ec0d8ba8910fda52eb5a4"