初步认识微前端(single-spa 和 qiankun)

初步认识微前端

微前端是什么

现在的前端应用,功能、交互日益复杂,若只由一个团队负责,随着时间的推进,会越来越庞大,愈发难以维护。

微前端这个名词,第一次提出是在2016年底。它将微服务(将单一应用程序划分成一组小的服务,服务之间相同配合,为用户提供最终价值)这个应用于服务端的技术扩展到前端领域。

微前端背后的想法是:将网站或 web 应用程序视为由独立团队负责的子应用(或模块、功能)的组合。

微前端核心是:拆、和。

  • ,即将一个应用拆成多个子应用。每个子应用由单独的团队负责,独立开发、发布
  • ,将多个子应用整合成完整的 web 应用。

微前端框架

微前端框架有:single-spa、qiankun。

由于 qiankun 基于 single-spa,所以我们先介绍 single-spa。

single-spa

Single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架 —— 官网

实战

single-spa 能将多个单页面应用聚合成一个整体应用,所以我将创建一个 vue的应用、一个 react 的应用,再将这两个应用整合成一个应用。

下面我们将使用 cli 创建三个项目,vue 子应用、react 子应用和父应用(或完整应用),实现一个微前端架构系统。

cli(create-single-spa)

vue 提供了 cli,用于快速开始 vue 的编写;single-spa 也提供了自己的 cli,即 create-single-spa

全局安装 cli:

>npm i create-single-spa -g

added 387 packages, and audited 388 packages in 40s

50 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities    

下面只使用 cli 其中的一种语法来创建三个项目:即create-single-spa my-dir。用法如下:

>create-single-spa single-spa-project
? Select type to generate
  single-spa application / parcel
  in-browser utility module (styleguide, api cache, etc) 
> single-spa root config

选项说明:

  • single-spa application 子应用
  • parcel 一个parcel可以大到一个应用,也可以小至一个组件
  • in-browser utility module 浏览器内实用模块
  • single-spa root config single-spa 根配置。可以创建父应用,用于整合子应用。
使用 cli 创建父应用

使用 cli 创建父项目。选择 single-spa root config:

single-spa-test> create-single-spa single-spa-project
// single-spa root config
? Select type to generate single-spa root config
// 选择 npm
? Which package manager do you want to use? npm
// 不使用 Typescript
? Will this project use Typescript? No
// 不需要 single-spa Layout 引擎
? Would you like to use single-spa Layout Engine No
// 组织输入 pjl
? Organization name (can use letters, numbers, dash or underscore) pjl
...
Project setup complete!
Run 'npm start' to boot up your single-spa root config

:后面两个子应用的组织也输入 pjl,说明这三个项目都属于 pjl。

配置文件如下:

// single-spa-project/package.json
{
  "name": "@pjl/root-config",
  "scripts": {
    "start": "webpack serve --port 9000 --env isLocal",
    ...
  },
  ...
  "dependencies": {
    ...
    "single-spa": "^5.9.3"
  }
}

通过 npm run start 启动项目:

single-spa-project>npm run start

> start
> webpack serve --port 9000 --env isLocal

 [webpack-dev-server] Project is running at:
 [webpack-dev-server] Loopback: http://localhost:9000/
 [webpack-dev-server] On Your Network (IPv4): http://192.168.0.102:9000/
...
webpack 5.68.0 compiled successfully in 5151 ms

根据提示访问项目(http://localhost:9000/),浏览器输出(中文注释由笔者添加):

// 欢迎来到你的 single-spa 根配置。说明项目启动成功,并能正常访问
Welcome
to your single-spa root config! 
// 这个页面由一个示例应用 `single-spa application` 所渲染
This page is being rendered by an example single-spa application that is being imported by your root config.

Next steps
// 添加共享依赖。下文引入 react 子应用时会用到
1. Add shared dependencies
Locate the import map in src/index.ejs
Add an entry for modules that will be shared across your dependencies. For example, a React application generated with create-single-spa will need to add React and ReactDOM to the import map.

"react": "https://cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js"
Refer to the corresponding single-spa framework helpers for more specific information.
// 创建你的下一个 single-spa 应用。
2. Create your next single-spa application
// 使用create-single-spa生成一个single-spa应用,按照提示操作,直到本地运行
Generate a single-spa application with create-single-spa and follow the prompts until it is running locally
// 返回到 root-config 并使用您的项目名称更新 src/index.ejs 中的导入映射
Return to the root-config and update the import map in src/index.ejs with your project's name
// 建议使用应用程序的 package.json 名称字段
It's recommended to use the application's package.json name field
// 打开 src/root-config.js 去掉注册这个应用的代码
Open src/root-config.js and remove the code for registering this application
Uncomment the registerApplication code and update it with your new application's name
// 在此之后,您应该不再看到这个欢迎页面,而是应该看到您的新应用程序!
After this, you should no longer see this welcome page but should instead see your new application!

Learn more
Shared dependencies documentation on single-spa.js.org
SystemJS and Import Maps
Single-spa ecosystem
Contribute
Support single-spa by donating on OpenCollective!
Contribute to single-spa on GitHub!
Join the Slack group to engage in discussions and ask questions.
Tweet @Single_spa and show off the awesome work you've done!

于是我们知道:

  • 项目启动成功,并能正常访问
  • 这个页面由一个示例应用 single-spa application 所渲染
  • 下一步创建子应用,替换这个示例应用
使用 cli 创建 vue 子应用

使用 cli 创建 vue 子应用。选择 single-spa application / parcel

single-spa-test> create-single-spa single-spa-vue
? Select type to generate single-spa application / parcel
// 框架选择 vue
? Which framework do you want to use? vue
// 组织输入 pjl。保持与父应用相同
? Organization name (can use letters, numbers, dash or underscore) pjl

Vue CLI v4.5.13
┌───────────────────────────────────────────┐
│                                           │
│   New version available 4.5.13 → 4.5.15   │
│     Run npm i -g @vue/cli to update!      │
│                                           │
└───────────────────────────────────────────┘
// vue 的配置
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Vuex, Linter
// vue 2.x
? Choose a version of Vue.js that you want to start the project with 2.x
// 路由模式是否使用 history
? Use history mode for router? (Requires proper server setup for index fallback in production) No
// eslint
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save
// 配置是否保存在专门的文件中
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
// 预设是否保存
? Save this as a preset for future projects? No
...
Project setup complete!
Steps to test your Vue single-spa application:
1. Run 'npm run serve'
2. Go to http://single-spa-playground.org/playground/instant-test?name=@pjl/single-spa-vue&url=%2F%2Flocalhost%3A8080%2Fjs%2Fapp.js&framework=vue to see it working!

No change to package.json was detected. No package manager install will be executed.

Tip:其中 vue 的配置和我们之前学习的 vue-cli 是相同的。

vue 子应用(项目)是否能独立跑起来?首先查看 package.json 中的脚本:

// single-spa-vue/package.json
{
  "name": "@pjl/single-spa-vue",
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "serve:standalone": "vue-cli-service serve --mode standalone"
  },
  "dependencies": {
    "single-spa-vue": "^2.1.0",
    ...
  },
  "devDependencies": {
    "vue-cli-plugin-single-spa": "~3.1.2",
    ...
  }
}

运行脚本 npm run serve:

single-spa-test\single-spa-vue>npm run serve

> @pjl/[email protected] serve
> vue-cli-service serve
...

  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.0.102:8080/
...

根据提示访问项目(http://localhost:8080/),浏览器输出(中文注释由笔者添加):

// 你的微前端不在这里
Your Microfrontend is not here
// @pjl/single-spa-vue 微前端以“集成”模式运行
The @pjl/single-spa-vue microfrontend is running in "integrated" mode, since standalone-single-spa-webpack-plugin is disabled. This means that it does not work as a standalone application without changing configuration.

// 我如何开发这个微前端?
How do I develop this microfrontend?
// 要开发此微前端,请尝试以下步骤:
To develop this microfrontend, try the following steps:

// 注:父应用集成 vue 子应用时,需要用到下面这个 url
Copy the following URL to your clipboard: http://localhost:8080/js/app.js
In a new browser tab, go to the your single-spa web app. This is where your "root config" is running. You do not have to run the root config locally if it is already running on a deployed environment - go to the deployed environment directly.
In the browser console, run localStorage.setItem('devtools', true); Refresh the page.
A yellowish rectangle should appear at the bottom right of your screen. Click on it. Find the name @pjl/single-spa-vue and click on it. If it is not present, click on Add New Module.
Paste the URL above into the input that appears. Refresh the page.
Congrats, your local code is now being used!
For further information about "integrated" mode, see the following links:

// 本地开发预览
Local Development Overview
Import Map Overrides Documentation
If you prefer Standalone mode
// 要在“独立”模式下运行这个微前端,可以运行 `npm run start:standalone`
To run this microfrontend in "standalone" mode, the standalone-single-spa-webpack-plugin must not be disabled. In some cases, this is done by running npm run start:standalone. Alternatively, you can add --env.standalone to your package.json start script if you are using webpack-config-single-spa.

If neither of those work for you, see more details about enabling standalone mode at Standalone Plugin Documentation.

于是我们知道:

  • 启动失败,这个子应用以“集成”模式运行
  • 独立”模式下运行这个微前端,可以运行 npm run start:standalone

尝试独立运行失败:

single-spa-vue> npm run start:standalone
npm ERR! missing script: start:standalone
npm ERR!
npm ERR! Did you mean this?
// 是这个吗?
npm ERR!     serve:standalone
...

提示是否是 serve:standalone?package.json 的 script 配置中确实有这个配置,再次尝试:

// 注:第一次失败,之后启动都成功了
single-spa-vue>npm run serve:standalone

> @pjl/[email protected] serve:standalone
> vue-cli-service serve --mode standalone   

 INFO  Starting development server...
...
  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.0.102:8080/

浏览器访问 http://localhost:8080/,vue 项目正常启动。

使用 cli 创建 react 子应用

使用 cli 创建 react 子应用。选择 single-spa application / parcel

single-spa-test> create-single-spa single-spa-react
? Which framework do you want to use? react
? Which package manager do you want to use? npm
? Will this project use Typescript? No
// 组织输入 pjl
? Organization name (can use letters, numbers, dash or underscore) pjl
? Project name (can use letters, numbers, dash or underscore) react
...
Project setup complete!
Steps to test your React single-spa application:
1. Run 'npm start -- --port 8500'
2. Go to http://single-spa-playground.org/playground/instant-test?name=@pjl/react&url=8500 to see it working!
父应用如何引入子应用

上文(“使用 cli 创建父应用”),我们知道父应用中默认使用了一个子应用,所以我们只需了解其原理即可。

主要关注 src 下的两个文件:

  • index.ejs,可以认为是一个 html 页面,里面使用的是 ejs,一个高效的嵌入式 JavaScript 模板引擎,语法为<%= EJS %>
  • pjl-root-config.js,根配置,用于启动 single-spa 应用。

我们先初略看一下这两个文件:






  
  
  
  Root Config

  

  
  
  
  
  
  
  

  
  <% if (isLocal) { %>
    
    <% } %>

      
      <% if (isLocal) { %>
        
        
        <% } else { %>
          
          
          <% } %>



  
  
// single-spa-project/src/pjl-root-config.js
import { registerApplication, start } from "single-spa";

// 注册应用。
registerApplication({
  name: "@single-spa/welcome", // 应用名。随便起
  app: () =>
    // 导入模块
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  // 当匹配 `/` 时,加载这个应用
  activeWhen: ["/"],
});

// registerApplication({
//   name: "@pjl/navbar",
//   app: () => System.import("@pjl/navbar"),
//   activeWhen: ["/"]
// });

// 启动
// 这时应用才会被真正挂载。在start被调用之前,应用先被下载,但不会初始化/挂载/卸载
start({
  urlRerouteOnly: true,
});

引入子应用的原理:

  1. index.ejs(或称index.html)主要为了调用 registerApplication() 方法,里面使用了 systemjs(模块加载器)
  2. 首先通过 systemjs-importmap 导入映射(亦或导入依赖)
  3. 入口是 System.import('@pjl/root-config')(行{1}),于是匹配到 @pjl/root-config 对应的 pjl-root-config.js(行{2})
  4. pjl-root-config.js 中注册了一个应用,并启动
  5. http://localhost:9000/ 会匹配 /,则会加载注册的应用(@single-spa/welcome
bootstrap、mount、unmount

vue 子应用和 react子应用都导出了三个生命周期函数:bootstrap(引导)、mount(挂载)和unmount(卸载)。

// single-spa-vue/src/main.js

import singleSpaVue from 'single-spa-vue'
...

const vueLifecycles = singleSpaVue({
  ...
})

export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount
// single-spa-react/src/pjl-react.js

import singleSpaReact from "single-spa-react";
...

const lifecycles = singleSpaReact({
  ...
});

export const { bootstrap, mount, unmount } = lifecycles;

Tip:示例应用中也是有这三个生命周期函数的。

// 示例应用
https://unpkg.com/[email protected]/dist/single-spa-welcome.js

bootstrap:()=>T,mount:()=>O,unmount:()=>R
将 vue 子应用合入父应用
  • 首先启动子应用,并取得子应用的 url
single-spa-vue>npm run serve
...
  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.0.102:8080/
...

浏览器访问 url(http://localhost:8080/),从中取得子应用的 url:

// 浏览器输出:

...
Copy the following URL to your clipboard: http://localhost:8080/js/app.js
...

子应用的 url为 //localhost:8080/js/app.js

  • index.ejs 中导入映射,并在根配置(pjl-root-config.js)中将示例应用替换成 vue 子应用
// single-spa-project/src/index.ejs
...

// single-spa-project/src/pjl-root-config.js

/*
registerApplication({
  name: "@single-spa/welcome",
  app: () =>
    System.import(
      "https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
    ),
  activeWhen: ["/"],
});
*/

registerApplication({
  // name 名字随意
  name: "@pjl/single-spa-vue123",
  app: () => System.import('@pjl/single-spa-vue'),
  activeWhen: ["/vue"],
})

至此,浏览器中访问 http://localhost:9000/vue 就能看到 vue 子应用。

:假如你没有注释示例应用,当你访问 http://localhost:9000/vue,页面显示的仍会是示例应用的内容。这是因为 /vue 也会匹配 /。可以将数组形式改为回调即可。

- activeWhen: ["/"],
+ activeWhen: (location) => location.pathname === '/',
父应用给子应用传 props

可以使用 customProps 来给子应用传递 props。比如下面我们传一个 age,请看示例:

  • 父应用通过 customProps 给 vue 子应用传递 age 属性:
registerApplication({
  name: "@pjl/single-spa-vue123",
  ...
  // 自定义属性可以是一个对象,也可以是一个返回Object的函数
  customProps: {
    age: 18,
  },
})

子应用的 main.js 中接收 age 属性:

// single-spa-vue/src/main.js

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render(h) {
      // 将属性传到 App
      return h(App, {
        props: {
          // single-spa props are available on the "this" object.
        + age: this.age
        }
      })
    },
    router,
    store
  }
})

App.vue 中将 age 属性输出:


:在 About.vue 这么写不可以,因为 main.js 中是将 props 传给 App.vue

将 react 子应用合入父应用

首先启动子应用,并取得子应用的 url

single-spa-react> npm run start

> start        
> webpack serve

 [webpack-dev-server] Project is running at:
 [webpack-dev-server] Loopback: http://localhost:8080/
 [webpack-dev-server] On Your Network (IPv4): http://192.168.0.102:8080/
...

浏览器访问 url(http://localhost:8080/),从中取得子应用的 url:

// 浏览器输出:

...
Copy the following URL to your clipboard: http://localhost:8080/pjl-react.js
...

子应用的 url为 //localhost:8080/pjl-react.js

  • index.ejs 中导入映射,并在根配置(pjl-root-config.js)中将示例应用替换成 react 子应用
// single-spa-project/src/index.ejs
...

// single-spa-project/src/pjl-root-config.js

registerApplication({
  name: "@pjl/react",
  app: () => System.import('@pjl/react'),
  activeWhen: ["/react"],
})

访问 react(http://localhost:9000/react),页面空白,控制台报错:

Uncaught Error: application '@pjl/react' died in status LOADING_SOURCE_CODE: Unable to resolve bare specifier 'react' from http://localhost:8080/pjl-react.js (SystemJS Error#8 https://git.io/JvFET#8)

点入 http://localhost:8080/pjl-react.js,发现 System 注册了两个模块:

System.register(["react","react-dom"], function(__WEBPACK_DY...

将上文(“使用 cli 创建父应用”)提到的“添加共享依赖”加入后,再次刷新页面即可看到 react 应用正常生效。

 

Tip:如果需要同时接入 vue 应用和react 应用,可以修改 vue 子应用的端口即可。比如增加 vue.config.js:

// configureWebpack 是 vue-cli 提供的
// devServer 在 webpack 文档中
module.exports = {
  devServer: {
      port: 9000
  },
}

qiankun

可能是你见过最完善的微前端解决方案 —— 官网

根据官网介绍,其特点是:简单、样式隔离、js 沙箱、生产可使用。

Tip:qiankun 的文档内容不多,其中的指南、API、常见问题都可以看一下。

实战

下面我们将使用 cli 创建三个项目,vue 子应用、react 子应用和父应用(或完整应用),实现一个微前端架构系统。(与 single-spa 实战类似)

使用 vue-cli 创建父应用
// 选中 babel、Router、vue2.x
qiankun-test> vue create qiankun-project

:建议父应用和vue 子应用的 vue-router 模式都选用 history。笔者这里选则了 hash,所以后面需要将路由模式改为 history。

使用 vue-cli 创建 vue 子应用
// 选中 babel、Router、Vuex、vue2.x
qiankun-test> vue create qiankun-vue
使用 cli 创建 react 子应用

Tip:没有用过 react 也没有问题,只需以下几步即可跑起一个 react 项目。

// 第一次失败
qiankun-test>npx create-react-app qiankun-react
// 全局安装 create-react-app
qiankun-test>npm install -g create-react-app
// 再次安装成功
qiankun-test>npx create-react-app qiankun-react

qiankun-test>cd qiankun-react
// 启动。查看 package.json 即可
qiankun-react>npm start

:第一次运行 npx create-react-app qiankun-react 提示需要安装包(create-react-app),选择 Y 却也没能安装成功。之后全局安装 create-react-app,再次运行则成功了。

qiankun-test> npx create-react-app qiankun-react
Need to install the following packages:
  create-react-app
Ok to proceed? (y) y

You are running `create-react-app` 4.0.3, which is behind the latest release (5.0.0).

We no longer support global installation of Create React App.

Please remove any global installs with one of the following commands:
- npm uninstall -g create-react-app
- yarn global remove create-react-app
The latest instructions for creating a new app can be found here:

up to date, audited 1 package in 444ms

found 0 vulnerabilities
将 vue 子应用合入父应用
  • 修改父应用

安装 qiankun(参考:qiankun 主应用):

qiankun-project> npm i qiankun -S

added 4 packages, and audited 1251 packages in 12s
...

package.json 则会增加依赖:

"dependencies": {
  "qiankun": "^2.6.3",
},

在父应用中注册微应用:

// qiankun-project/src/main.js

...
// 以下都是新增
import { registerMicroApps, start } from 'qiankun';
const apps = [
  {
    name: 'qiankuan-vue123', // 名字随意
    entry: '//localhost:9000/', // 子应用的 url
    container: '#vue', // 挂载到的元素 id。
    activeRule: '/app-vue', // 当匹配 /app-vue 时触发子应用
    // 传参给子应用
    props: {
      age: 18,
    },
  },
]

// 注册应用
registerMicroApps(apps);

// 启动
start();

增加子应用的挂载元素:

// qiankun-project/src/App.vue


修改路由模式为 history

// qiankun-project\src\router\index.js

const router = new VueRouter({
+ mode: 'history',
  routes
})
  • 修改子应用

新建配置文件,开启 cors,配置微应用的打包工具:

// qiankuan-vue/vue.config.js

// 取得 package.json 中字段 name 的值,即项目名称
const packageName = require('./package.json').name;

module.exports = {
    devServer: {
        // 父应用已占用 8080,子应用得使用一个不同的端口
        port: 9000,
        headers: {
            // 若不开启 cors,浏览器控制台报错如下
            // Access to fetch at 'http://localhost:9000/' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 
            'Access-Control-Allow-Origin': '*',
        },
    },
    // 配置微应用的打包工具
    configureWebpack: {
        output: {
            // 输出一个库,为你的入口做导出
            library: `${packageName}-[name]`,
            // 将你的 library 暴露为所有的模块定义下都可运行的方式。它将在 CommonJS, AMD 环境下运行,或将模块导出到 global 下的变量。
            libraryTarget: 'umd',
            jsonpFunction: `webpackJsonp_${packageName}`,
        }
    }
}

导出相应的生命周期钩子:

// qiankuan-vue/src/main.js

import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false

// 导入 routes。
import { routes } from './router'
import VueRouter from 'vue-router';

// 使用 webpack 运行时 publicPath 配置
if (window.__POWERED_BY_QIANKUN__) {
  // 注释也正常
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

let router = null;
let instance = null;
function render(props = {}) {
  const { container } = props;
  router = new VueRouter({
    // 应用的基路径。例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 "/app/"
    base: window.__POWERED_BY_QIANKUN__ ? '/app-vue/' : '/',
    mode: 'history',
    routes,
  });

  instance = new Vue({
    router,
    store,
    render: (h) => h(App),
  // fix 常见问题:微应用的根 id 与其他 DOM 冲突。解决办法是:修改根 id 的查找范围。
  }).$mount(container ? container.querySelector('#app') : '#app');
}

// fix 常见问题:如何独立运行微应用
// 如果不是 qiankun,则渲染
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('vue app bootstraped');
}

/**
 * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
 */
export async function mount(props) {
  render(props);
}

/**
 * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
 */
export async function unmount(props) {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
  router = null;
}

导出 routes:

// qiankuan-vue/src/router\index.js

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  ...
]

+ export { routes }

重启父子应用即可:

qiankun-project> npm run serve

qiankuan-vue> npm run serve

就像这样:

// 访问
http://localhost:8080/app-vue/about

浏览器输出:

Home | About | app-vue

Home | About(激活)

This is an about page

Tip:react 子应用合入主应用,请自行完成。

总结

有人说 single-spa 不够灵活,没有样式隔离,没有 js 沙箱;

而 qiankun 建立在 single-spa 的基础上,使用起来更加简单,提供样式隔离,也提供了 js 沙箱;

single-spa 的原理是什么?

qiankun 的样式隔离、js沙箱机制又是如何实现?

我们后面再聊。

祝:诸君新年身体健康、技术有成

你可能感兴趣的:(初步认识微前端(single-spa 和 qiankun))