简单搭建一个微前端项目(qiankun)

323 阅读4分钟

简单搭建一个微前端项目(qiankun)

其实就是按照官方文档来搭建的,不过也踩了一部分的坑,零零碎碎搜了一些解决问题的办法,做一个记录。

现公司项目都是基于微前端开发的,所以才要去学习,其实搭建好了,后面写项目时感觉和一般项目没有什么区别。

官方文档

项目搭建

创建三个测试 Demo

  • micro-base: 主应用 npx create-react-app micro-base --template typescript
  • micro-react: react 创建的子应用 npx create-react-app micro-react --template typescript
  • micro-vue: vue2 创建的子应用 vue create micro-vue

配置

  1. 首先将创建的项目根节点 id 更改了(防止冲突),一般使用脚手架创建的项目根节点。
  2. 在每个项目根目录下新建 .env 文件,设置各个项目的 port:PORT=xxx。当然,你也可以使用其他方法设置项目的启动端口。

主应用配置

src/index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { registerMicroApps, start } from 'qiankun';

import './assets/css/index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

// 配置
registerMicroApps([
  {
    name: 'react app', // app name registered
    entry: '//localhost:3011',
    container: '#reactContainer',
    activeRule: '/micro-react',
  },
  {
    name: 'vue app',
    entry: '//localhost:3012',
    container: '#vueContainer',
    activeRule: '/micro-vue',
  },
]);

start();

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

react 子应用配置

按照官方文档,先在 src 下新增 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

src/index.tsx

import './public-path';
import React from 'react';
import ReactDOMClient, { Root } from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';

import App from './App';

let root: Root | null = null;

function render(props: any) {
  const { container } = props;

  root = ReactDOMClient.createRoot(
    container ? container.querySelector('#microReactRoot') as HTMLElement : document.getElementById('microReactRoot') as HTMLElement
  );

  root.render(
    <React.StrictMode>
      <Router basename={window.__POWERED_BY_QIANKUN__ ? '/micro-react' : '/'}>
        <App />
      </Router>
    </React.StrictMode>
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

export async function bootstrap() {
  console.log('[react16] react app bootstraped');
}

export async function mount(props: any) {
  console.log('[react16] props from main framework', props);
  render(props);
}

export async function unmount(props: any) {
  console.log(props);
  root?.unmount();
}

防止 window.__POWERED_BY_QIANKUN__ 报错,在src下新增 types/index.d.ts

export {};

declare global {
  interface Window {
    __POWERED_BY_QIANKUN__: string;
  }
}

接着就是修改 webpack 的配置,按照官网,首先安装 @rescripts/cli,再在根目录下新建 .rescriptsrc.js

const { name } = require('./package');

module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.chunkLoadingGlobal = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';
    // config.resolve.extensions =  ['.js', '.jsx', '.ts', '.tsx'];

    return config;
  },

  devServer: (_) => {
    const config = _;

    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    // config.watchContentBase = false;
    config.liveReload = false;

    return config;
  },
};

再将 package.json 中的脚本配置更改掉:

"scripts": {
  "start": "rescripts start",
  "build": "rescripts build",
  "test": "rescripts test"
},

vue2 子应用配置

按照官网,依然是在 src 目录下新增 public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

src/main.js

import Vue from 'vue'
import App from './App.vue'
import routes from './router'
import VueRouter from 'vue-router'
import store from './store'
import './public-path'

Vue.use(VueRouter)
Vue.config.productionTip = false

let router = null
let instance = null

function render() {
  !router && (router = new VueRouter({
    mode: 'history',
    base: window.__POWERED_BY_QIANKUN__ ? 'micro-vue' : '/',
    routes
  }))

  !instance && (instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#microVueRoot'))
}

// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
  render()
}

export async function bootstrap(props) {
  console.log('[vue] vue app bootstraped', props)
}

export async function mount() {
  console.log('[vue] props from main framework')
  // 渲染
  render()
}

export async function unmount() {
  instance.$destroy()
  instance.$el.innerHTML = ''
  instance = null
  router = null
}

然后再更改路由配置,因为按照官网的配置了,所以要改动一下路由的初始配置 src/router/index.js:

// import Vue from 'vue'
// import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

// Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

// const router = new VueRouter({
//   mode: 'history',
//   base: process.env.BASE_URL,
//   routes
// })

export default routes

再修改打包配置,根目录下新增 vue.config.js

const { name } = require('./package.json')

module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  },
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd', // 把微应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`
    }
  }
}

至此,初始化的配置就已经结束了,为了让项目看起来更加规范一些,所以加上一些细节的调整。

继续配置

主应用

  • 安装 antd:yarn add antd
  • 安装 react-router-dom:yarn add react-router-dom

在 src 目录下新增 assets/css/index.css,引入 antd 的样式:

@import '~antd/dist/antd.css';

在 src 目录下新增 layout 文件夹,存放项目的 layout 组件

src/layout/index.tsx

import {
  DesktopOutlined,
  PieChartOutlined
} from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Breadcrumb, Layout, Menu } from 'antd';
import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';

import './index.css';

const { Header, Content, Footer, Sider } = Layout;

type MenuItem = Required<MenuProps>['items'][number];

function getItem(
  label: React.ReactNode,
  key: React.Key,
  icon?: React.ReactNode,
  children?: MenuItem[],
): MenuItem {
  return {
    key,
    icon,
    children,
    label,
  } as MenuItem;
}

// 配置左侧菜单
const items: MenuItem[] = [
  getItem(<Link to="/micro-vue">vue2 子应用</Link>, '/micro-vue', <PieChartOutlined />),
  getItem(<Link to="/micro-react">react 子应用</Link>, '/micro-react', <DesktopOutlined />)
];

const LayoutIndex: React.FC = () => {
  const { pathname } = useLocation();

  // 控制菜单收缩展开
  const [collapsed, setCollapsed] = useState(false);
  // 初始默认选中的菜单
  const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
  const [breadcrumb, setBreadcrumb] = useState('')

  useEffect(() => {
    // 根据路由来设置当前选中的菜单,当然,一旦跳到子路由了菜单就无法选中了
    // 只是搭建一个初始的项目,所以没有优化这些细节
    setSelectedKeys([pathname]);
    const _breadcrumb = pathname === '/micro-vue' ? 'vue2' : 'react';
    setBreadcrumb(_breadcrumb);
  }, [pathname])

  return (
    <Layout className="container" style={{ minHeight: '100vh' }}>
      <Sider collapsible collapsed={collapsed} onCollapse={value => setCollapsed(value)}>
        <div className="logo" />
        <Menu theme="dark" selectedKeys={selectedKeys} mode="inline" items={items} />
      </Sider>

      <Layout className="site-layout">
        <Header className="site-layout-background" style={{ padding: 0 }} />

        <Content style={{ margin: '0 16px' }}>
          <Breadcrumb style={{ margin: '16px 0' }}>
            <Breadcrumb.Item>{breadcrumb}</Breadcrumb.Item>
          </Breadcrumb>

          <div className="site-layout-background" style={{ padding: 24, minHeight: 360 }}>
            {/* 微应用容器 */}
            <div id="reactContainer"></div>
            <div id="vueContainer"></div>
          </div>
        </Content>

        <Footer style={{ textAlign: 'center' }}>微前端 Demo 主应用</Footer>
      </Layout>
    </Layout>
  );
};

export default LayoutIndex;

src/layout/index.css

.container .logo {
  height: 32px;
  margin: 16px;
  background: rgba(255, 255, 255, 0.3);
}

.site-layout .site-layout-background {
  background: #fff;
}

最后就是引入使用 src/App.tsx:

import React from 'react';
import LayoutIndex from './layouts';
import {BrowserRouter as Router} from 'react-router-dom';

function App() {
  return (
    <div>
      <Router>
        <LayoutIndex />
      </Router>
    </div>
  );
}

export default App;

至此,主应用差不多就这样了。

react 子应用

就是新增两个测试的页面,这个就比较简单,可以自行添加,唯一不一样的就是如果建项目使用的是 react-router-dom v6 的话,可能就和其他版本的配置有一点差别。

src/App.tsx:

import React from 'react';
import { Link, Routes, Route } from 'react-router-dom';
import About from './pages/About';
import Home from './pages/Home';

function App() {
  return (
    <div className="App">
      <p>React 子应用</p>

      <Link to="/">Home</Link> |
      <Link to="/about">About</Link>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </div>
  );
}

export default App;

vue 子应用

本来就有两个现成的页面,所以就不配置其他的了,直接使用。

最终效果

GIF 2022-7-22 17-40-41.gif

至此就结束了,如果遇到什么问题,可以一起讨论。