精华内容
下载资源
问答
  • 一、一套微前端框架通常包含四个部分:3.主应用4.子应用这里重点介绍一下,主应用,子应用的搭建过程,首先通过vue-cli创建2个vue应用,vue_main主应用,vue_child子应用二、修改主应用vue_main修改index.html,在...

    一、一套微前端框架通常包含四个部分:

    3.主应用

    4.子应用

    这里重点介绍一下,主应用,子应用的搭建过程,首先通过vue-cli创建2个vue应用,vue_main主应用,vue_child子应用

    二、修改主应用vue_main修改index.html,在head部分添加以下代码:

    {

    "imports": {

    "child": "http://localhost:8081/app.js", // 确保文件浏览器中可以打开

    "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",

    "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",

    "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js"

    }

    }

    2.修改app.html

       logo.png

    export default {

    name: 'App',

    mounted () {

    window.System.import('single-spa').then((res) => {

    var singleSpa = res

    singleSpa.registerApplication('child', () => window.System.import('child'), location => true)

    singleSpa.start()

    })

    }

    }

    #app {

    font-family: 'Avenir', Helvetica, Arial, sans-serif;

    -webkit-font-smoothing: antialiased;

    -moz-osx-font-smoothing: grayscale;

    text-align: center;

    color: #2c3e50;

    margin-top: 60px;

    }

    这样主应用就修改好了

    三、修改子应用vue_child

    1.安装项目依赖npm i single-spa-vue systemjs-webpack-interop

    2.修改入口文件main.js// The Vue build version to load with the `import` command

    // (runtime-only or standalone) has been set in webpack.base.conf with an alias.

    import { setPublicPath } from 'systemjs-webpack-interop'

    import Vue from 'vue'

    import App from './App'

    import singleSpaVue from 'single-spa-vue'

    import router from './router'

    setPublicPath('child')

    Vue.config.productionTip = false

    /* eslint-disable no-new */

    // new Vue({

    //   el: '#app',

    //   router,

    //   components: { App },

    //   template: ''

    // })

    const vueLifecycles = singleSpaVue({

    Vue,

    appOptions: {

    render: (h) => h(App),

    router

    }

    })

    export const bootstrap = vueLifecycles.bootstrap

    export const mount = vueLifecycles.mount

    export const unmount = vueLifecycles.unmount

    3.修改webpack配置文件webpack.base.conf.jsoutput: {

    path: config.build.assetsRoot,

    filename: '[name].js',

    library:'child',  // 新添加

    libraryTarget: 'umd',  // 新添加

    publicPath: process.env.NODE_ENV === 'production'

    ? config.build.assetsPublicPath

    : config.dev.assetsPublicPath

    },

    4.修改webpack配置文件webpack.dev.conf.js,确保子应用允许跨域devServer: {

    headers:{

    "Access-Control-Allow-Origin":"*"

    }

    }

    四.运行项目查看效果,这样在主应用中加载了子应用的内容

    1604369787775988.png

    五、通过脚手架生成子应用npm init single-spa

    展开全文
  • 授权转载自:mongofeng源码地址:https://github.com/mongofeng/vue-mic为什么要用微前端目前随着前端的不断发展,企业工程项目体积越来越大,页面越...

    授权转载自:mongofeng 

    源码地址:https://github.com/mongofeng/vue-mic

    为什么要用微前端

    目前随着前端的不断发展,企业工程项目体积越来越大,页面越来越多,项目变得十分臃肿,维护起来也十分困难,有时我们仅仅更改项目简单样式,都需要整个项目重新打包上线,给开发人员造成了不小的麻烦,也非常浪费时间。老项目为了融入到新项目也需要不断进行重构,造成的人力成本也非常的高。

    微前端架构具备以下几个核心价值:

    • 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权

    • 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

    • 独立运行时 每个子应用之间状态隔离,运行时状态不共享

    single-spa实现原理

    首先对微前端路由进行注册,使用single-spa充当微前端加载器,并作为项目单一入口来接受所有页面URL的访问,根据页面URL与微前端的匹配关系,选择加载对应的微前端模块,再由该微前端模块进行路由响应URL,即微前端模块中路由找到相应的组件,渲染页面内容。

    sysyem.js的作用及好处

    system.js的作用就是动态按需加载模块。假如我们子项目都使用了vue,vuex,vue-router,每个项目都打包一次,就会很浪费。system.js可以配合webpackexternals属性,将这些模块配置成外链,然后实现按需加载,当然了,你也可以直接用script标签将这些公共的js全部引入,借助system.js这个插件,我们只需要将子项目的app.js暴露给它即可。

    什么是Lerna

    当前端项目变得越来越大的时候,我们通常会将公共代码拆分出来,成为一个个独立的npm包进行维护。但是这样一来,各种包之间的依赖管理就十分让人头疼。为了解决这种问题,我们可以将不同的npm包项目都放在同一个项目来管理。这样的项目开发策略也称作monorepoLerna就是这样一个你更好地进行这项工作的工具。Lerna是一个使用gitnpm来处理多包依赖管理的工具,利用它能够自动帮助我们管理各种模块包之间的版本依赖关系。目前,已经有很多公共库都使用Lerna作为它们的模块依赖管理工具了,如:babel, create-react-app, react-router, jest等。

    1. 解决包之间的依赖关系。

    2. 通过git仓库检测改动,自动同步。

    3. 根据相关的git提交的commit,生成CHANGELOG

    你还需要全局安装 Lerna:

    npm install -g lerna
    

    基于vue微前端项目搭建

    1.项目初始化

    mkdir lerna-project & cd lerna-project`
    
    lerna init
    

    执行成功后,目录下将会生成这样的目录结构。

    ├── README.md
    ├── lerna.json  # Lerna 配置文件
    ├── package.json
    ├── packages    # 应用包目录
    

    2.Set up yarn的workspaces模式

    默认是npm, 而且每个子package都有自己的node_modules,通过这样设置后,只有顶层有一个node_modules

    {
      "packages": [
        "packages/*"
      ],
      "useWorkspaces": true,
      "npmClient": "yarn",
      "version": "0.0.0"
    }
    

    同时package.json 设置 private 为 true,防止根目录被发布到 npm

    {
     "private": true,
     "workspaces": [
        "packages/*"
     ]
    }
    

    配置根目录下的 lerna.json 使用 yarn 客户端并使用 workspaces

    yarn config set workspaces-experimental true
    

    3.注册子应用

    第一步:使用vue-cli创建子应用

    # 进入packages目录
    cd packages
    
    # 创建应用
    vue create app1
    
    // 项目目录结构
    ├── public
    ├── src
    │   ├── main.js
    │   ├── assets
    │   ├── components
    │   └── App.vue
    ├── vue.config.js
    ├── package.json
    ├── README.md
    └── yarn.lock
    

    第二步:使用vue-cli-plugin-single-spa插件快速生成spa项目

    # 会自动修改main.js加入singleSpaVue,和生成set-public-path.js
    vue add single-spa
    

    生成的main.js文件

    const vueLifecycles = singleSpaVue({
      Vue,
      appOptions: {
        // el: '#app', // 没有挂载点默认挂载到body下
        render: (h) => h(App),
        router,
        store: window.rootStore,
      },
    });
    
    export const bootstrap = [
      vueLifecycles.bootstrap
    ];
    export const mount = vueLifecycles.mount;
    export const unmount = vueLifecycles.unmount;
    

    第三步:设置环境变量.env

    # 应用名称
    VUE_APP_NAME=app1
    # 应用根路径,默认值为: '/',如果要发布到子目录,此值必须指定
    VUE_APP_BASE_URL=/
    # 端口,子项目开发最好设置固定端口, 避免频繁修改配置文件,设置一个固定的特殊端口,尽量避免端口冲突。
    port=8081
    

    第四步:设置vue.config.js修改webpack配置

    const isProduction = process.env.NODE_ENV === 'production'
    const appName = process.env.VUE_APP_NAME
    const port = process.env.port
    const baseUrl = process.env.VUE_APP_BASE_URL
    module.exports = {
      // 防止开发环境下的加载问题
      publicPath: isProduction ? `${baseUrl}${appName}/` : `http://localhost:${port}/`,
    
        // css在所有环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js)
        css: {
            extract: false
        },
    
      productionSourceMap: false,
    
      outputDir: path.resolve(dirname, `../../dist/${appName}`), // 统一打包到根目录下的dist下
      chainWebpack: config => {
        config.devServer.set('inline', false)
        config.devServer.set('hot', true)
    
        // 保证打包出来的是一个js文件,供主应用进行加载
        config.output.library(appName).libraryTarget('umd')
    
        config.externals(['vue', 'vue-router', 'vuex'])  // 一定要引否则说没有注册
    
        if (process.env.NODE_ENV === 'production') {
          // 打包目标文件加上 hash 字符串,禁止浏览器缓存
          config.output.filename('js/index.[hash:8].js')
        }
      },
    }
    

    4.新建主项目

    第一步:添加主项目package

    # 进入packages目录
    cd packages
    # 创建一个packge目录, 进入root-html-file目录
    mkdir root-html-file && cd root-html-file
    # 初始化一个package
    npm init -y
    

    第二步:新建主项目index.html

    主应用主要是扮演路由分发,资源加载的作用的角色

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>Vue-Microfrontends</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="importmap-type" content="systemjs-importmap">
        <!-- 配置文件注意写成绝对路径:/开头,否则访问子项目的时候重定向的index.html,相对目录会出错 -->
       <script type="systemjs-importmap" src="importmap.json"></script>
        <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
        <link rel="preload" href="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js" as="script" crossorigin="anonymous" />
        <!-- systemjs的包 -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
        <!-- 用于解析子包的解析 -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
        <!-- 解析包的default -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
        <!-- systemjs的包 -->
      </head>
      <body>
        <div id="root"></div>
      </body>
    </html>
    

    第三步:编辑importMapjson文件,配置对应子应用的文件

    {
      "imports": {
        "navbar": "http://localhost:8888/js/app.js",
        "app1": "http://localhost:8081/js/app.js",
        "app2": "http://localhost:8082/js/app.js",
        "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
        "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
        "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
        "vuex": "https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js"
      }
    }
    

    到时systemjs可以直接去import,具体作用参考systemjs

    第四步:注册app应用

    singleSpa.registerApplication(
      'navbar', // systemjs-webpack-interop, 去匹配应用的名称
      () => System.import('navbar'), // 资源路径
      location => location.hash.startsWith('#/navbar') // 资源激活的
    )
    
    
    // 注册子应用
    singleSpa.registerApplication(
      'app1', // systemjs-webpack-interop, 去匹配子应用的名称
      () => System.import('app1'), // 资源路径
      location => location.hash.startsWith('#/app1') // 资源激活的
    )
    
    
    singleSpa.registerApplication(
      'app2', // systemjs-webpack-interop, 去匹配子应用的名称
      () => System.import('app2'), // 资源路径
      location => location.hash.startsWith('#/app2') // 资源激活的
    )
    // 开始singleSpa
    singleSpa.start();
    

    第五步:项目开发

    项目的基本目录结构如下:

    .
    ├── README.md
    ├── lerna.json  # Lerna 配置文件
    ├── node_modules
    ├── package.json
    ├── packages    # 应用包目录
    │   ├── app1    # 应用1
    │   ├── app2    # 应用2
    │   ├── navbar   # 主应用
    │   └── root-html-file  # 入口
    └── yarn.lock
    

    如上图所示,所有的应用都存放在 packages 目录中。其中 root-html-file 为入口项目,navbar 为常驻的主应用,这两者在开发过程中必须启动相应的服务。其他为待开发的子应用。

    项目的优化

    抽取子应用资源配置

    在主应用中抽取所有的子应用到一个通用的app.config.json文件配置

    {
      "apps": [
        {
          "name": "navbar", // 应用名称
          "main": "http://localhost:8888/js/app.js", // 应用的入口
          "path": "/", // 是否为常驻应用
          "base": true, // 是否使用history模式
          "hash": true // 是否使用hash模式
        },
        {
          "name": "app1",
          "main": "http://localhost:8081/js/app.js",
          "path": "/app1",
          "base": false,
          "hash": true
        },
        {
          "name": "app2",
          "main": "http://localhost:8082/js/app.js",
          "path": "/app2",
          "base": false,
          "hash": true
        }
      ]
    }
    

    主应用的入口文件中注册子应用

    try {
        // 读取应用配置并注册应用
        const config = await System.import(`/app.config.json`)
        const { apps } = config.default
        apps && apps.forEach((app) => registerApp(singleSpa, app))
        singleSpa.start()
      } catch (e) {
        throw new Error('应用配置加载失败')
      }
    
    /**
     * 注册应用
     * */
    function registerApp (spa, app) {
      const activityFunc = app.hash ? hashPrefix(app) : pathPrefix(app)
      spa.registerApplication(
        app.name,
        () => System.import(app.main),
        app.base ? (() => true) : activityFunc,
      )
    }
    
    
    /**
     * hash匹配模式
     * @param app 应用配置
     */
     function hashPrefix (app) {
      return function (location) {
        if (!app.path) return true
    
        if (Array.isArray(app.path)) {
          if (app.path.some(path => location.hash.startsWith(`#${path}`))) {
            return true
          }
        } else if (location.hash.startsWith(`#${app.path}`)) {
          return true
        }
    
        return false
      }
    }
    
    /**
     * 普通路径匹配模式
     * @param app 应用配置
     */
    function pathPrefix (app) {
      return function (location) {
        if (!app.path) return true
    
        if (Array.isArray(app.path)) {
          if (app.path.some(path => location.pathname.startsWith(path))) {
            return true
          }
        } else if (location.pathname.startsWith(app.path)) {
          return true
        }
    
        return false
      }
    }
    

    所有子项目公用一个使用vuex

    在主项目index.html注册vuex的插件,通过window对象存储,子项目加载启动时候通过registerModule方式注入子应用的模块和自身的vue实例上

    // 主应用的js中
    Vue.use(Vuex)
    window.rootStore = new Vuex.Store() // 全局注册唯一的vuex, 供子应用的共享
    
    
    // 子应用的main.js
    export const bootstrap = [
      () => {
        return new Promise(async (resolve, reject) => {
          // 注册当前应用的store
          window.rootStore.registerModule(VUE_APP_NAME, store)
          resolve()
        })
      },
      vueLifecycles.bootstrap
    ];
    export const mount = vueLifecycles.mount;
    export const unmount = vueLifecycles.unmount;
    

    样式隔离

    我们使用postcss的一个插件:postcss-selector-namespace。他会把你项目里的所有css都会添加一个类名前缀。这样就可以实现命名空间隔离。首先,我们先安装这个插件:npm install postcss-selector-namespace \--save \-d 项目目录下新建 postcss.config.js,使用插件:

    // postcss.config.js
    
    module.exports = {
      plugins: {
        // postcss-selector-namespace: 给所有css添加统一前缀,然后父项目添加命名空间
        'postcss-selector-namespace': {
          namespace(css) {
            // element-ui的样式不需要添加命名空间
            if (css.includes('element-variables.scss')) return '';
            return '.app1' // 返回要添加的类名
          }
        },
      }
    }
    

    然后父项目添加命名空间

    // 切换子系统的时候给body加上对应子系统的 class namespace
    window.addEventListener('single-spa:app-change', () => {
      const app = singleSpa.getMountedApps().pop();
      const isApp = /^app-\w+$/.test(app);
      if (app) document.body.className = app;
    });
    

    利用manifest生成app.config.json和importMapjson

    stats-webpack-plugin可以在你每次打包结束后,都生成一个manifest.json 文件,里面存放着本次打包的 public_path bundle list chunk list 文件大小依赖等等信息。可以根据这个信息来生成子应用的app.config.json路径和importMapjson.

    npm install stats-webpack-plugin --save -d
    

    vue.config.js中使用:

    {
        configureWebpack: {
            plugins: [
                new StatsPlugin('manifest.json', {
                    chunkModules: false,
                    entrypoints: true,
                    source: false,
                    chunks: false,
                    modules: false,
                    assets: false,
                    children: false,
                    exclude: [/node_modules/]
                }),
            ]
        }
    }
    

    打包完成最后通过脚本generate-app.js生成对应,子应用的json路径和importMapjson

    const path = require('path')
    const fs = require('fs')
    const root = process.cwd()
    console.log(`当前工作目录是: ${root}`);
    const dir = readDir(root)
    const jsons = readManifests(dir)
    generateFile(jsons)
    
    console.log('生成配置文件成功')
    
    
    function readDir(root) {
      const manifests = []
      const files = fs.readdirSync(root)
      files.forEach(i => {
        const filePath = path.resolve(root, '.', i)
        const stat = fs.statSync(filePath);
        const is_direc = stat.isDirectory();
    
        if (is_direc) {
          manifests.push(filePath)
        }
    
      })
      return manifests
    }
    
    
    function readManifests(files) {
      const jsons = {}
      files.forEach(i => {
        const manifest = path.resolve(i, './manifest.json')
        if (fs.existsSync(manifest)) {
          const { publicPath, entrypoints: { app: { assets } } } = require(manifest)
          const name = publicPath.slice(1, -1)
          jsons[name] = `${publicPath}${assets}`
        }
      })
    
      return jsons
    
    }
    
    
    
    function generateFile(jsons) {
      const { apps } = require('./app.config.json')
      const { imports } = require('./importmap.json')
      Object.keys(jsons).forEach(key => {
        imports[key] = jsons[key]
      })
      apps.forEach(i => {
        const { name } = i
    
        if (jsons[name]) {
          i.main = jsons[name]
        }
      })
    
      fs.writeFileSync('./importmap.json', JSON.stringify(
        {
          imports
        }
      ))
    
      fs.writeFileSync('./app.config.json', JSON.stringify(
        {
          apps
        }
      ))
    
    }
    

    应用打包

    在根目录执行build命令, packages里面的所有build命令都会执行,这会在根目录生成 dist 目录下,

    lerna run build
    

    最终生成的目录结构如下

    .
    ├── dist
    │   ├── app1/
    │   ├── app2/
        ├── navbar/
    │   ├── app.config.json
    │   ├── importmap.json
    │   ├── main.js
    │   ├── generate-app.js
    │   └── index.html
    

    最后,执行以下命令生成 generate-app.js,重新生成带hash资源路径的importmap.jsonapp.config.json文件:

    cd dist && node generate-app.js
    

    文章中的完整demo文件地址,如果觉得有用的话给个star

    参考文档

    • lerna管理前端模块最佳实践

    • lerna 和 yarn 实现 monorepo

    • 从0实现一个single-spa的前端微服务(中)

    • Single-Spa + Vue Cli 微前端落地指南 + 视频 (项目隔离远程加载,自动引入)

    • Single-Spa微前端落地(含nginx部署)

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

    • coexisting-vue-microfrontends

    最后

    欢迎关注「前端瓶子君」,回复「交流」加入前端交流群!

    欢迎关注「前端瓶子君」,回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!

    在这里(算法群),你可以每天学习一道大厂算法编程题(阿里、腾讯、百度、字节等等)或 leetcode,瓶子君都会在第二天解答哟!

    另外,每周还有手写源码题,瓶子君也会解答哟!

    》》面试官也在看的算法资料《《

    “在看和转发”就是最大的支持

    展开全文
  • angluar6+vue微前端demo

    2020-11-24 22:57:03
    微前端架构具备以下几个核心价值: 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 独立运行时 每个子...
  • 授权转载自:mongofeng源码地址:https://github.com/mongofeng/vue-mic为什么要用微前端目前随着前端的不断发展,企业工程项目体积越来越大,页面越来越多,项目变得十分臃肿,维护起来也十分困难,有时我们仅仅...

    授权转载自:mongofeng 

    源码地址:https://github.com/mongofeng/vue-mic

    为什么要用微前端

    目前随着前端的不断发展,企业工程项目体积越来越大,页面越来越多,项目变得十分臃肿,维护起来也十分困难,有时我们仅仅更改项目简单样式,都需要整个项目重新打包上线,给开发人员造成了不小的麻烦,也非常浪费时间。老项目为了融入到新项目也需要不断进行重构,造成的人力成本也非常的高。

    微前端架构具备以下几个核心价值:

    • 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权
    • 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
    • 独立运行时 每个子应用之间状态隔离,运行时状态不共享

    single-spa实现原理

    首先对微前端路由进行注册,使用single-spa充当微前端加载器,并作为项目单一入口来接受所有页面URL的访问,根据页面URL与微前端的匹配关系,选择加载对应的微前端模块,再由该微前端模块进行路由响应URL,即微前端模块中路由找到相应的组件,渲染页面内容。

    sysyem.js的作用及好处

    system.js的作用就是动态按需加载模块。假如我们子项目都使用了vue,vuex,vue-router,每个项目都打包一次,就会很浪费。system.js可以配合webpackexternals属性,将这些模块配置成外链,然后实现按需加载,当然了,你也可以直接用script标签将这些公共的js全部引入,借助system.js这个插件,我们只需要将子项目的app.js暴露给它即可。

    什么是Lerna

    当前端项目变得越来越大的时候,我们通常会将公共代码拆分出来,成为一个个独立的npm包进行维护。但是这样一来,各种包之间的依赖管理就十分让人头疼。为了解决这种问题,我们可以将不同的npm包项目都放在同一个项目来管理。这样的项目开发策略也称作monorepoLerna就是这样一个你更好地进行这项工作的工具。Lerna是一个使用gitnpm来处理多包依赖管理的工具,利用它能够自动帮助我们管理各种模块包之间的版本依赖关系。目前,已经有很多公共库都使用Lerna作为它们的模块依赖管理工具了,如:babel, create-react-app, react-router, jest等。

    1. 解决包之间的依赖关系。
    2. 通过git仓库检测改动,自动同步。
    3. 根据相关的git提交的commit,生成CHANGELOG

    你还需要全局安装 Lerna:

    npm install -g lerna

    基于vue微前端项目搭建

    1.项目初始化

    mkdir lerna-project & cd lerna-project`

    lerna init

    执行成功后,目录下将会生成这样的目录结构。

    ├── README.md
    ├── lerna.json  # Lerna 配置文件
    ├── package.json
    ├── packages    # 应用包目录

    2.Set up yarn的workspaces模式

    默认是npm, 而且每个子package都有自己的node_modules,通过这样设置后,只有顶层有一个node_modules

    {
      "packages": [
        "packages/*"
      ],
      "useWorkspaces"true,
      "npmClient""yarn",
      "version""0.0.0"
    }

    同时package.json 设置 private 为 true,防止根目录被发布到 npm

    {
     "private"true,
     "workspaces": [
        "packages/*"
     ]
    }

    配置根目录下的 lerna.json 使用 yarn 客户端并使用 workspaces

    yarn config set workspaces-experimental true

    3.注册子应用

    第一步:使用vue-cli创建子应用

    # 进入packages目录
    cd packages

    #
     创建应用
    vue create app1

    // 项目目录结构
    ├── public
    ├── src
    │   ├── main.js
    │   ├── assets
    │   ├── components
    │   └── App.vue
    ├── vue.config.js
    ├── package.json
    ├── README.md
    └── yarn.lock

    第二步:使用vue-cli-plugin-single-spa插件快速生成spa项目

    # 会自动修改main.js加入singleSpaVue,和生成set-public-path.js
    vue add single-spa

    生成的main.js文件

    const vueLifecycles = singleSpaVue({
      Vue,
      appOptions: {
        // el: '#app', // 没有挂载点默认挂载到body下
        render: (h) => h(App),
        router,
        storewindow.rootStore,
      },
    });

    export const bootstrap = [
      vueLifecycles.bootstrap
    ];
    export const mount = vueLifecycles.mount;
    export const unmount = vueLifecycles.unmount;

    第三步:设置环境变量.env

    # 应用名称
    VUE_APP_NAME=app1
    # 应用根路径,默认值为: '/',如果要发布到子目录,此值必须指定
    VUE_APP_BASE_URL=/
    # 端口,子项目开发最好设置固定端口, 避免频繁修改配置文件,设置一个固定的特殊端口,尽量避免端口冲突。
    port=8081

    第四步:设置vue.config.js修改webpack配置

    const isProduction = process.env.NODE_ENV === 'production'
    const appName = process.env.VUE_APP_NAME
    const port = process.env.port
    const baseUrl = process.env.VUE_APP_BASE_URL
    module.exports = {
      // 防止开发环境下的加载问题
      publicPath: isProduction ? `${baseUrl}${appName}/` : `http://localhost:${port}/`,

        // css在所有环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js)
        css: {
            extractfalse
        },

      productionSourceMapfalse,

      outputDir: path.resolve(dirname, `../../dist/${appName}`), // 统一打包到根目录下的dist下
      chainWebpack: config => {
        config.devServer.set('inline'false)
        config.devServer.set('hot'true)

        // 保证打包出来的是一个js文件,供主应用进行加载
        config.output.library(appName).libraryTarget('umd')

        config.externals(['vue''vue-router''vuex'])  // 一定要引否则说没有注册

        if (process.env.NODE_ENV === 'production') {
          // 打包目标文件加上 hash 字符串,禁止浏览器缓存
          config.output.filename('js/index.[hash:8].js')
        }
      },
    }

    4.新建主项目

    第一步:添加主项目package

    # 进入packages目录
    cd packages
    # 创建一个packge目录, 进入root-html-file目录
    mkdir root-html-file && cd root-html-file
    # 初始化一个package
    npm init -y

    第二步:新建主项目index.html

    主应用主要是扮演路由分发,资源加载的作用的角色

    html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <title>Vue-Microfrontendstitle>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="importmap-type" content="systemjs-importmap">
        
       <script type="systemjs-importmap" src="importmap.json">script>
        <link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
        <link rel="preload" href="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js" as="script" crossorigin="anonymous" />
        
        <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js">script>
        
        <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js">script>
        
        <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js">script>
        
      head>
      <body>
        <div id="root">div>
      body>
    html>

    第三步:编辑importMapjson文件,配置对应子应用的文件

    {
      "imports": {
        "navbar""http://localhost:8888/js/app.js",
        "app1""http://localhost:8081/js/app.js",
        "app2""http://localhost:8082/js/app.js",
        "single-spa""https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
        "vue""https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
        "vue-router""https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js",
        "vuex""https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js"
      }
    }

    到时systemjs可以直接去import,具体作用参考systemjs

    第四步:注册app应用

    singleSpa.registerApplication(
      'navbar'// systemjs-webpack-interop, 去匹配应用的名称
      () => System.import('navbar'), // 资源路径
      location => location.hash.startsWith('#/navbar'// 资源激活的
    )


    // 注册子应用
    singleSpa.registerApplication(
      'app1'// systemjs-webpack-interop, 去匹配子应用的名称
      () => System.import('app1'), // 资源路径
      location => location.hash.startsWith('#/app1'// 资源激活的
    )


    singleSpa.registerApplication(
      'app2'// systemjs-webpack-interop, 去匹配子应用的名称
      () => System.import('app2'), // 资源路径
      location => location.hash.startsWith('#/app2'// 资源激活的
    )
    // 开始singleSpa
    singleSpa.start();

    第五步:项目开发

    项目的基本目录结构如下:

    .
    ├── README.md
    ├── lerna.json  # Lerna 配置文件
    ├── node_modules
    ├── package.json
    ├── packages    # 应用包目录
    │   ├── app1    # 应用1
    │   ├── app2    # 应用2
    │   ├── navbar   # 主应用
    │   └── root-html-file  # 入口
    └── yarn.lock

    如上图所示,所有的应用都存放在 packages 目录中。其中 root-html-file 为入口项目,navbar 为常驻的主应用,这两者在开发过程中必须启动相应的服务。其他为待开发的子应用。

    项目的优化

    抽取子应用资源配置

    在主应用中抽取所有的子应用到一个通用的app.config.json文件配置

    {
      "apps": [
        {
          "name""navbar", // 应用名称
          "main""http://localhost:8888/js/app.js", // 应用的入口
          "path""/", // 是否为常驻应用
          "base"true, // 是否使用history模式
          "hash"true // 是否使用hash模式
        },
        {
          "name""app1",
          "main""http://localhost:8081/js/app.js",
          "path""/app1",
          "base"false,
          "hash"true
        },
        {
          "name""app2",
          "main""http://localhost:8082/js/app.js",
          "path""/app2",
          "base"false,
          "hash"true
        }
      ]
    }

    主应用的入口文件中注册子应用

    try {
        // 读取应用配置并注册应用
        const config = await System.import(`/app.config.json`)
        const { apps } = config.default
        apps && apps.forEach((app) => registerApp(singleSpa, app))
        singleSpa.start()
      } catch (e) {
        throw new Error('应用配置加载失败')
      }

    /**
     * 注册应用
     * */

    function registerApp (spa, app{
      const activityFunc = app.hash ? hashPrefix(app) : pathPrefix(app)
      spa.registerApplication(
        app.name,
        () => System.import(app.main),
        app.base ? (() => true) : activityFunc,
      )
    }


    /**
     * hash匹配模式
     * @param app 应用配置
     */

     function hashPrefix (app{
      return function (location{
        if (!app.path) return true

        if (Array.isArray(app.path)) {
          if (app.path.some(path => location.hash.startsWith(`#${path}`))) {
            return true
          }
        } else if (location.hash.startsWith(`#${app.path}`)) {
          return true
        }

        return false
      }
    }

    /**
     * 普通路径匹配模式
     * @param app 应用配置
     */

    function pathPrefix (app{
      return function (location{
        if (!app.path) return true

        if (Array.isArray(app.path)) {
          if (app.path.some(path => location.pathname.startsWith(path))) {
            return true
          }
        } else if (location.pathname.startsWith(app.path)) {
          return true
        }

        return false
      }
    }

    所有子项目公用一个使用vuex

    在主项目index.html注册vuex的插件,通过window对象存储,子项目加载启动时候通过registerModule方式注入子应用的模块和自身的vue实例上

    // 主应用的js中
    Vue.use(Vuex)
    window.rootStore = new Vuex.Store() // 全局注册唯一的vuex, 供子应用的共享


    // 子应用的main.js
    export const bootstrap = [
      () => {
        return new Promise(async (resolve, reject) => {
          // 注册当前应用的store
          window.rootStore.registerModule(VUE_APP_NAME, store)
          resolve()
        })
      },
      vueLifecycles.bootstrap
    ];
    export const mount = vueLifecycles.mount;
    export const unmount = vueLifecycles.unmount;

    样式隔离

    我们使用postcss的一个插件:postcss-selector-namespace。他会把你项目里的所有css都会添加一个类名前缀。这样就可以实现命名空间隔离。首先,我们先安装这个插件:npm install postcss-selector-namespace \--save \-d 项目目录下新建 postcss.config.js,使用插件:

    // postcss.config.js

    module.exports = {
      plugins: {
        // postcss-selector-namespace: 给所有css添加统一前缀,然后父项目添加命名空间
        'postcss-selector-namespace': {
          namespace(css) {
            // element-ui的样式不需要添加命名空间
            if (css.includes('element-variables.scss')) return '';
            return '.app1' // 返回要添加的类名
          }
        },
      }
    }

    然后父项目添加命名空间

    // 切换子系统的时候给body加上对应子系统的 class namespace
    window.addEventListener('single-spa:app-change', () => {
      const app = singleSpa.getMountedApps().pop();
      const isApp = /^app-\w+$/.test(app);
      if (app) document.body.className = app;
    });

    利用manifest生成app.config.json和importMapjson

    stats-webpack-plugin可以在你每次打包结束后,都生成一个manifest.json 文件,里面存放着本次打包的 public_path bundle list chunk list 文件大小依赖等等信息。可以根据这个信息来生成子应用的app.config.json路径和importMapjson.

    npm install stats-webpack-plugin --save -d

    vue.config.js中使用:

    {
        configureWebpack: {
            plugins: [
                new StatsPlugin('manifest.json', {
                    chunkModulesfalse,
                    entrypointstrue,
                    sourcefalse,
                    chunksfalse,
                    modulesfalse,
                    assetsfalse,
                    childrenfalse,
                    exclude: [/node_modules/]
                }),
            ]
        }
    }

    打包完成最后通过脚本generate-app.js生成对应,子应用的json路径和importMapjson

    const path = require('path')
    const fs = require('fs')
    const root = process.cwd()
    console.log(`当前工作目录是: ${root}`);
    const dir = readDir(root)
    const jsons = readManifests(dir)
    generateFile(jsons)

    console.log('生成配置文件成功')


    function readDir(root{
      const manifests = []
      const files = fs.readdirSync(root)
      files.forEach(i => {
        const filePath = path.resolve(root, '.', i)
        const stat = fs.statSync(filePath);
        const is_direc = stat.isDirectory();

        if (is_direc) {
          manifests.push(filePath)
        }

      })
      return manifests
    }


    function readManifests(files{
      const jsons = {}
      files.forEach(i => {
        const manifest = path.resolve(i, './manifest.json')
        if (fs.existsSync(manifest)) {
          const { publicPath, entrypoints: { app: { assets } } } = require(manifest)
          const name = publicPath.slice(1-1)
          jsons[name] = `${publicPath}${assets}`
        }
      })

      return jsons

    }



    function generateFile(jsons{
      const { apps } = require('./app.config.json')
      const { imports } = require('./importmap.json')
      Object.keys(jsons).forEach(key => {
        imports[key] = jsons[key]
      })
      apps.forEach(i => {
        const { name } = i

        if (jsons[name]) {
          i.main = jsons[name]
        }
      })

      fs.writeFileSync('./importmap.json'JSON.stringify(
        {
          imports
        }
      ))

      fs.writeFileSync('./app.config.json'JSON.stringify(
        {
          apps
        }
      ))

    }

    应用打包

    在根目录执行build命令, packages里面的所有build命令都会执行,这会在根目录生成 dist 目录下,

    lerna run build

    最终生成的目录结构如下

    .
    ├── dist
    │   ├── app1/
    │   ├── app2/
        ├── navbar/
    │   ├── app.config.json
    │   ├── importmap.json
    │   ├── main.js
    │   ├── generate-app.js
    │   └── index.html

    最后,执行以下命令生成 generate-app.js,重新生成带hash资源路径的importmap.jsonapp.config.json文件:

    cd dist && node generate-app.js

    文章中的完整demo文件地址,如果觉得有用的话给个star

    参考文档

    • lerna管理前端模块最佳实践
    • lerna 和 yarn 实现 monorepo
    • 从0实现一个single-spa的前端微服务(中)
    • Single-Spa + Vue Cli 微前端落地指南 + 视频 (项目隔离远程加载,自动引入)
    • Single-Spa微前端落地(含nginx部署)
    • 可能是你见过最完善的微前端解决方案
    • coexisting-vue-microfrontends

    最后

    欢迎关注「前端瓶子君」,回复「交流」加入前端交流群!欢迎关注「前端瓶子君」,回复「算法」自动加入,从0到1构建完整的数据结构与算法体系!在这里(算法群),你可以每天学习一道大厂算法编程题(阿里、腾讯、百度、字节等等)或 leetcode,瓶子君都会在第二天解答哟!另外,每周还有手写源码题,瓶子君也会解答哟!
    d8ab2c61fadc59599157730ff0314027.png
    》》面试官也在看的算法资料《《“在看和转发”就是最大的支持
    展开全文
  • 写在开头:手写框架体系文章,缺手写vue微前端框架文章,今日补上微前端框架,觉得写得不错,记得点个关注+在看,转发更好对源码有兴趣的,可以看我之前的系列手写源码文章微前端框架是怎么导入...

    写在开头:

    手写框架体系文章,缺手写vue和微前端框架文章,今日补上微前端框架,觉得写得不错,记得点个关注+在看,转发更好


    对源码有兴趣的,可以看我之前的系列手写源码文章

    微前端框架是怎么导入加载子应用的  【3000字精读】

    原创:带你从零看清Node源码createServer和负载均衡整个过程

    原创:从零实现一个简单版React (附源码)

    精读:10个案例让你彻底理解React hooks的渲染逻辑

    原创:如何自己实现一个简单的webpack构建工具 【附源码】

    从零解析webRTC.io Server端源码


    正式开始:

    对于微前端,最近好像很火,之前我公众号也发过比较多微前端框架文章

    深度:微前端在企业级应用中的实践  (1万字,华为)

    万字解析微前端、微前端框架qiankun以及源码

    那么现在我们需要手写一个微前端框架,首先得让大家知道什么是微前端,现在微前端模式分很多种,但是大都是一个基座+多个子应用模式,根据子应用注册的规则,去展示子应用。

    这是目前的微前端框架基座加载模式的原理,基于single-spa封装了一层,我看有不少公司是用Vue做加载器(有天然的keep-alive),还有用angular和web components技术融合的


    首先项目基座搭建,这里使用parcel

    mkdir pangu 
    yarn init 
    //输入一系列信息
    yarn add parcel@next
    

    然后新建一个index.html文件,作为基座

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    <body>
        
    </body>
    </html>
    

    新建一个index.js文件,作为基座加载配置文件

    新建src文件夹,作为pangu框架的源码文件夹,

    新建example案例文件夹

    现在项目结构长这样


    既然是手写,就不依赖其他任何第三方库

    我们首先需要重写hashchange popstate这两个事件,因为微前端的基座,需要监听这两个事件根据注册规则去加载不同的子应用,而且它的实现必须在React、vue子应用路由组件切换之前,单页面的路由源码原理实现,其实也是靠这两个事件实现,之前我写过一篇单页面实现原理的文章,不熟悉的可以去看看

    https://segmentfault.com/a/1190000019936510
    
    const HIJACK_EVENTS_NAME = /^(hashchange|popstate)$/i;
    const EVENTS_POOL = {
      hashchange: [],
      popstate: [],
    };
    
    
    window.addEventListener('hashchange', loadApps);
    window.addEventListener('popstate', loadApps);
    
    
    const originalAddEventListener = window.addEventListener;
    const originalRemoveEventListener = window.removeEventListener;
    window.addEventListener = function (eventName, handler) {
      if (
        eventName &&
        HIJACK_EVENTS_NAME.test(eventName) &&
        typeof handler === 'function'
      ) {
        EVENTS_POOL[eventName].indexOf(handler) === -1 &&
          EVENTS_POOL[eventName].push(handler);
      }
      return originalAddEventListener.apply(this, arguments);
    };
    window.removeEventListener = function (eventName, handler) {
      if (eventName && HIJACK_EVENTS_NAME.test(eventName)) {
        let eventsList = EVENTS_POOL[eventName];
        eventsList.indexOf(handler) > -1 &&
          (EVENTS_POOL[eventName] = eventsList.filter((fn) => fn !== handler));
      }
      return originalRemoveEventListener.apply(this, arguments);
    };
    
    
    function mockPopStateEvent(state) {
      return new PopStateEvent('popstate', { state });
    }
    
    
    // 拦截history的方法,因为pushState和replaceState方法并不会触发onpopstate事件,所以我们即便在onpopstate时执行了reroute方法,也要在这里执行下reroute方法。
    const originalPushState = window.history.pushState;
    const originalReplaceState = window.history.replaceState;
    window.history.pushState = function (state, title, url) {
      let result = originalPushState.apply(this, arguments);
      reroute(mockPopStateEvent(state));
      return result;
    };
    window.history.replaceState = function (state, title, url) {
      let result = originalReplaceState.apply(this, arguments);
      reroute(mockPopStateEvent(state));
      return result;
    };
    
    
    // 再执行完load、mount、unmout操作后,执行此函数,就可以保证微前端的逻辑总是第一个执行。然后App中的Vue或React相关Router就可以收到Location的事件了。
    export function callCapturedEvents(eventArgs) {
      if (!eventArgs) {
        return;
      }
      if (!Array.isArray(eventArgs)) {
        eventArgs = [eventArgs];
      }
      let name = eventArgs[0].type;
      if (!HIJACK_EVENTS_NAME.test(name)) {
        return;
      }
      EVENTS_POOL[name].forEach((handler) => handler.apply(window, eventArgs));
    }
    

    上面代码很简单,创建两个队列,使用数组实现

    const EVENTS_POOL = {
      hashchange: [],
      popstate: [],
    };
    

    如果检测到是hashchange popstate两种事件,而且它们对应的回调函数不存在队列中时候,那么就放入队列中。(相当于redux中间件原理)

    然后每次监听到路由变化,调用reroute函数:

    function reroute() {
      invoke([], arguments);
    }
    

    这样每次路由切换,最先知道变化的是基座,等基座同步执行完(阻塞)后,就可以由子应用的vue-Rourer或者react-router-dom等库去接管实现单页面逻辑了。


    那,路由变化,怎么加载子应用呢?

    像一些微前端框架会用import-html之类的这些库,我们还是手写吧

    逻辑大概是这样,一共四个端口,nginx反向代理命中基座服务器监听的端口(用户必须首先访问到根据域名),然后去不同子应用下的服务器拉取静态资源然后加载。


    提示:所有子应用加载后,只是在基座的一个div标签中加载,实现原理跟ReactDom.render()这个源码一样,可参考我之前的文章

    原创:从零实现一个简单版React (附源码)


    那么我们先编写一个registrApp方法,接受一个entry参数,然后去根据url变化加载子应用(传入的第二个参数activeRule

    /**
     *
     * @param {string} entry
     * @param {string} function
     */
    const Apps = [] //子应用队列
    function registryApp(entry,activeRule) {
        Apps.push({
            entry,
            activeRule
        })
    }
    

    注册完了之后,就要找到需要加载的app

    export async function loadApp() {
      const shouldMountApp = Apps.filter(shouldBeActive);
      console.log(shouldMountApp, 'shouldMountApp');
      //   const res = await axios.get(shouldMountApp.entry);
      fetch(shouldMountApp.entry)
        .then(function (response) {
          return response.json();
        })
        .then(function (myJson) {
          console.log(myJson, 'myJson');
        });
    }
    
    
    

    shouldBeActive根据传入的规则去判断是否需要此时挂载:

    export function shouldBeActive(app){
        return app.activeRule(window.location)
    }
    

    此时的res数据,就是我们通过get请求获取到的子应用相关数据,现在我们新增subapp1和subapp2文件夹,模拟部署的子应用,我们把它用静态资源服务器跑起来

    subapp1.js作为subapp1的静态资源服务器

    const express = require('express');
    const app = express();
    const { resolve } = require('path');
    app.use(express.static(resolve(__dirname, '../subapp1')));
    
    
    app.listen(8889, (err) => {
      !err && console.log('8889端口成功');
    });
    
    
    

    subapp2.js作为subapp2的静态资源服务器

    const express = require('express');
    const app = express();
    const { resolve } = require('path');
    app.use(express.static(resolve(__dirname, '../subapp2')));
    
    
    app.listen(8890, (err) => {
      !err && console.log('8890端口成功');
    });
    
    
    

    现在文件目录长这样:

    基座index.html运行在1234端口,subapp1部署在8889端口,subapp2部署在8890端口,这样我们从基座去拉取资源时候,就会跨域,所以静态资源服务器、webpack热更新服务器等服务器,都要加上cors头,允许跨域。

    const express = require('express');
    const app = express();
    const { resolve } = require('path');
    //设置跨域访问
    app.all('*', function (req, res, next) {
      res.header('Access-Control-Allow-Origin', '*');
      res.header('Access-Control-Allow-Headers', 'X-Requested-With');
      res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS');
      res.header('X-Powered-By', ' 3.2.1');
      res.header('Content-Type', 'application/json;charset=utf-8');
      next();
    });
    app.use(express.static(resolve(__dirname, '../subapp1')));
    
    
    app.listen(8889, (err) => {
      !err && console.log('8889端口成功');
    });
    
    
    

    ⚠️:如果是dev模式,记得在webpack的热更新服务器中配置允许跨域,如果你对webpack不是很熟悉,可以看我之前的文章:

    万字硬核     从零实现webpack热更新HMR

    原创:如何自己实现一个简单的webpack构建工具 【附源码】


    这里我使用nodemon启用静态资源服务器,简单为主,如果你没有下载,可以:

    npm i nodemon -g 
    或
    yarn add nodemon global 
    

    这样我们先访问下8889,8890端口,看是否能访问到。

    访问8889和8890都可以访问到对应的资源,成功


    正式开启启用我们的微前端框架pangu.封装start方法,启用需要挂载的APP。

    export function start(){
        loadApp()
    }
    

    注册子应用subapp1,subapp2,并且手动启用微前端

    import { registryApp, start } from './src/index';
    registryApp('localhost:8889', (location) => location.pathname === '/subapp1');
    registryApp('localhost:8890', (location) => location.pathname === '/subapp2');
    start()
    

    修改index.html文件:

    <!DOCTYPE html>
    <html lang="en">
    
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    
    <body>
        <div>
            <h1>基座</h1>
            <div class="subapp">
                <div>
                    <a href="/subapp1">子应用1</a>
                </div>
                <div>
                    <a href="/subapp2">子应用2</a>
                </div>
            </div>
            <div id="subApp"></div>
        </div>
    </body>
    <script src="./index.js"></script>
    
    
    </html>
    

    ok,运行代码,发现挂了,为什么会挂呢?因为那边返回的是html文件,我这里用的fetch请求,JSON解析不了

    那么我们去看看别人的微前端和第三方库的源码吧,例如import-html-entry这个库

    由于之前我解析过qiankun这个微前端框架源码,我这里就不做过度讲解,它们是对fetch做了一个text()。

    export async function loadApp() {
      const shouldMountApp = Apps.filter(shouldBeActive);
      console.log(shouldMountApp, 'shouldMountApp');
      //   const res = await axios.get(shouldMountApp.entry);
      fetch(shouldMountApp.entry)
        .then(function (response) {
          return response.text();
        })
        .then(function (myJson) {
          console.log(myJson, 'myJson');
        });
    }
    

    然后我们已经可以得到拉取回来的html文件了(此时是一个字符串)

    由于现实的项目,一般这个html文件会包含js和css的引入标签,也就是我们目前的单页面项目,类似下面这样:

    于是我们需要把脚本、样式、html文件分离出来。用一个对象存储

    本想照搬某个微前端框架源码的,但是觉得它写得也就那样,今天又主要讲原理,还是自己写一个能跑的把,毕竟html的文件都回来了,数据处理也不难

    export async function loadApp() {
      const shouldMountApp = Apps.filter(shouldBeActive);
      console.log(shouldMountApp, 'shouldMountApp');
      //   const res = await axios.get(shouldMountApp.entry);
      fetch(shouldMountApp[0].entry)
        .then(function (response) {
          return response.text();
        })
        .then(function (text) {
          const dom = document.createElement('div');
          dom.innerHTML = text;
          console.log(dom, 'dom');
        });
    }
    
    
    

    先改造下,打印下DOM

    发现已经能拿到dom节点了,那么我先处理下,让它展示在基座中

    export async function loadApp() {
      const shouldMountApp = Apps.filter(shouldBeActive);
      console.log(shouldMountApp, 'shouldMountApp');
      //   const res = await axios.get(shouldMountApp.entry);
      fetch(shouldMountApp[0].entry)
        .then(function (response) {
          return response.text();
        })
        .then(function (text) {
          const dom = document.createElement('div');
          dom.innerHTML = text;
          const content = dom.querySelector('h1');
          const subapp = document.querySelector('#subApp-content');
          subapp && subapp.appendChild(content);
        });
    }
    

    此时,我们已经可以加载不同的子应用了。

    乞丐版的微前端框架就完成了,后面会逐步完善所有功能,向主流的微前端框架靠拢,并且完美支持IE11.记住它叫:pangu

    推荐阅读

    女程序媛为什么 Bug 多?

    再见,VPN !

    张一鸣:为什么 BAT 挖不走我们的人才?

    关注订阅号「大前端圈」,收看更多精彩内容
    

    嘿,你在看吗

    展开全文
  • Vue搭建微前端

    2021-02-04 09:19:22
    最近公司来了个项目,运行起来后,控制台报错,说是找不到“XXXXX”模块,让检查...接下来,微前端搭建摸索正式开启: 首先放上官网文档地址、配置文件生成说明 一、涉及技术及框架 single-spa:连接主项目与子.
  • qiankun + vue + element 的微前端架构项目,主项目与子应用均使用vue。支持三大前端框架可根据自己需求调整。微前端 qiankun微前端是什么、为什么要做微前端、qiankun是什么这些笔者将不再叙述,在前端微服务话提出...
  • 带你手写微前端框架

    2020-12-28 06:04:01
    使微前端框架可以更好地跟踪和控制它们。 <pre><code>javascript // app1 export default { // app启动 bootstrap: [() => Promise.resolve()], // app挂载 mount: [() => Promise.resolve()], /...
  • 微前端框架乾坤配置记录

    千次阅读 2020-12-08 23:50:45
    主应用 1. 路由包装为history模式 // src/main.js import routes from './routes'; const router = new VueRouter({ mode: 'history', ...2. 引入乾坤微前端主应用配置 // src/mico/index.js import {
  • 同时在微前端的架构中,我们可以同时使用React, Vue, Angular,甚至是原生的Js,Jquery开发的应用都可以通过微前端进行调度。 关于微前端框架: 微前端框架现在比较成熟的是 “Single-spa” 和 “QianKun”, 而...
  • Git:https://gitee.com/linxuxiang/myproject-vue 效果图:
  • vue+ts的微前端实践

    2020-08-15 11:38:05
    vue+ts的微前端实践 微前端的概念是从后端的微服务中迁移过来的。将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署 qiankun 是阿里巴巴基于 ...
  • demo 域名没有备案,2021年3月6日可以正常访问。 主应用配置 1、新建项目, 我这里是新建了angular v10工程, 并安装qiankun包。... name: 'fe-sub-vue', entry: '//localhost:7777/', activeRule: '/fe
  • 框架Vue 插件:Vue-Router UI组件:element-ui 开发工具:WebStorm 2018.3 前端项目工程结构概览: 2、系统界面 由于之前给南宁那边公司开发的教务系统有商业协议,不能抽取它的前端程序出来用,而我有...
  • 微前端 什么是微前端? 在讲解微前端之前先给大家看个示例(https://www.ucloud.cn/): UCloud是一个云计算服务平台,是比较典型的中后台应用,像这种应用往往面临着当业务发展之后,由刚刚开始的单体应用进化成巨石...
  • 同时qiankun是一个开放式微前端架构,支持当前三大前端框架甚至jq等其他项目无缝接入。此项目为了尽可能的简单易上手,以及方便文章讲解,大部分逻辑都在主应用和子应用的main.js来回施展,实际项目应用可不要如此...
  • 各个前端应用还可以独立运行、独立开发、独立部署qiankun 是阿里巴巴基于 single-spa 实现的微前端库,是一个开放式微前端架构,支持当前三大前端框架甚至 jq 等其他项目无缝接入。微前端待解决的问题[x] 微前端主...
  • 在本教程中,我将分享我所学到的知识,并向您展示如何构建由React和Vue应用程序组成的微前端应用程序。要查看此应用程序的最终代码,请单击此处。Single-spa我们将用于创建项目的 工具 是Single SPA- 用于前端微服务...
  • vue前端移动端UI框架比较分析

    千次阅读 2019-10-11 16:28:23
    Vant框架简介 组件都是来源于有赞的商城业务,并且经过有赞业务的检验 完善详实的 中文文档 专门的设计师团队维护视觉规范,统一而优雅 支持 babel-plugin-import 单测覆盖率超过90% 丰富实用的业务组件 Vant 不...
  • React加载vue微前端组件 这是一个基于单spa的react微型前端组件 我有一个梦想,我希望可以加载其他框架的组件,就像在react项目中加载<iframe>标签一样简单。 体验演示 git clone git@github....
  • 注意:qiankun 属于无侵入性的微前端框架,对主应用基座和微应用的技术栈都没有要求。 构建主应用基座 我们先使用 vue-cli 生成一个 Vue 的项目,初始化主应用。 将普通的项目改造成 qiankun 主应用基座,需要...

空空如也

空空如也

1 2 3 4 5 ... 11
收藏数 209
精华内容 83
关键字:

vue微前端框架

vue 订阅