微前端简易实践

161 阅读3分钟

业内常见的微前端解决方案:

iframe:在所有微前端方案中,iframe是最稳定的、上手难度最低的,但它有一些无法解决的问题,例如性能低、通信复杂、双滚动条、弹窗无法全局覆盖,它的成长性不高,只适合简单的页面渲染。

npm包:将子应用封装成npm包,通过组件的方式引入,在性能和兼容性上是最优的方案,但却有一个致命的问题就是版本更新,每次版本发布需要通知接入方同步更新,管理非常困难。

微前端框架:流行的微前端框架有single-spa和qiankun,它们将维护成本和功能上达到一种平衡,是目前实现微前端备受推崇的方案。

本文讲述另一种实现方案-> 通过webpack(jsonp)的方式,灵感来源于(微前端在美团外卖的实践

这种方式也是统一技术栈的一种微前端解决方案

在实现之前,我们先来了解下webpack的libraryTarget

什么是libraryTarget

webpack原文:配置如何暴露 library。可以使用下面的选项中的任意一个。注意,此选项与分配给 output.library 的值一同使用。它有很多值,包括:['var','commonjs','window','global','umd','amd',...'jsonp']

简单描述就是通过webpack打包出来的资源以某种方式输出,可以在特定的环境下使用,另一个重要的值为library,输出的变量名。

我们的想法就是借助webpack的jsonp方法,将打包后的资源用一个jsonp包裹,通过主应用动态的引入,进而解析对应的路由,css,store等等,达到微前端拆分的目的。

具体实现:

子应用

子应用webpack配置:

webpack.config.js

output: {
    path: path.resolve(__dirname, './dist'),
    filename: "child1.js",
    libraryTarget: 'jsonp',
    library: 'testJsonp'
  },

大家可以自行webpack打包一下,看看打包后的资源

子应用入口,将入口导出

import App from './router';



export default App

主应用:

主应用入口文件通过动态创建script标签来引入子应用文件

 window.jsonp = (r) => {
    ReactDOM.render(
      React.createElement(() => App(r.default)), // 将资源传到App中进行路由解析
      document.getElementById('root')
    )
  }

  const s = document.createElement('script')
  s.src = 'http://127.0.0.1:8011/child1.js'
  document?.body?.appendChild(s)

这样就将子应用代码用过jsonp的形式拿到并加载

router.js

App 

 <Router>
      <Routes>
        <Route path="/" element={<>123</>} />
        <Route path="/apple" element={<>456</>} />
        // 路由解析
        {routes && routes().props.children.map(({ props: { path, element } }) =>
        <Route key={path} path={path} element={element} />)}
      </Routes>
    </Router>

鉴于想支持按需加载,以及统一路由注册,我主要对主应用代码进行了如下调整

index.js

通过监听hashchange事件,去解析匹配对应的应用

//  防止从子应用进入匹配不到
loadRouter()
ReactDOM.render(
  React.createElement(() => App(null),
  document.getElementById('root')
)

// 按需加载
window.addEventListener('hashchange', () => {
  loadRouter()
})

utils.js

路由注册、解析

const register = [
  {
    subName: 'child',
    url: 'http://127.0.0.1:8011/child1.js',
    jsonp: 'testJsonp'
  }
]

export const loadRouter = () => {
  const routerArr = window.location.href.match(/#\/[\s\S]*/g)[0].replace(/#/, '')
  const subName = routerArr.split('/')[1]

  const findItem = register.find((item) => item.subName === subName);
  window[findItem.jsonp || ''] = (r) => {
    ReactDOM.render(
      React.createElement(() => App(r.default)),
      document.getElementById('root')
    )
  }
  const s = document.createElement('script')
  s.src = findItem?.url || ''
  document.body.appendChild(s)
}

上面两步可以做到简单的动态加载

增加子应用热更新。

因为日常开发中需要启动主应用才能,加载子应用,其实我们看到的壳子是主应用的,如果想支持子应用热更新,我们还要对子应用的webpack进行更改

子应用webpack.config.js

// 首先需要将主机检查关掉
devServer:{
    // disablehostcheck:true // 非webpack5
    historyApiFallback: true, //webpack5配置
    allowedHosts: "all", // webpack5配置
    headers: { 'Access-Control-Allow-Origin': '*' },
}

子应用** index.js**增加这段代码

import App from './router';



export default App
if (module.hot) {
  module.hot.accept('./router', () => {
    window.testJsonp(App); // 支持子工程热加载的信息传递
  });
}

子应用路由:

function App() {
  return (
    <>
      <Route path="/child" element={<Child />} />
      <Route path="/child/test" element={<div>这是子应用添加的路由</div>} />
    </>
  );
}
export default App

因为只是简单尝试,css就没单独抽离出一个文件,直接内联进去了,也没做store的一些合并。不过store可以和路由一并导出,最终解析合并到主应用的store中。

css文件也可以通过link标签去解决。怕css的全局污染可以通过postcss给每一个子应用单独配一个标识来达到样式隔离的目的

主应用可以将一些全局变量挂载到window上,同时可以在主应用里做子应用的缓存....