├── .gitignore ├── README.md ├── app.js ├── build ├── generate-config.js └── postcss.plugin.js ├── lerna.json ├── package.json ├── packages ├── app1 │ ├── .browserslistrc │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── main.js │ │ ├── router.js │ │ ├── set-public-path.js │ │ ├── store.js │ │ └── views │ │ │ └── Home.vue │ └── vue.config.js ├── app2 │ ├── .browserslistrc │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── components │ │ │ └── HelloWorld.vue │ │ ├── main.js │ │ ├── router.js │ │ ├── set-public-path.js │ │ ├── store.js │ │ └── views │ │ │ ├── About.vue │ │ │ └── Home.vue │ └── vue.config.js ├── navbar │ ├── .browserslistrc │ ├── .env │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── main.js │ │ ├── router.js │ │ ├── set-public-path.js │ │ └── store.js │ └── vue.config.js └── root-html-file │ ├── package.json │ ├── public │ ├── app.config.json │ ├── generate-app.js │ ├── importmap.json │ └── index.html │ ├── src │ ├── common.d.ts │ ├── main.ts │ └── utils │ │ └── index.ts │ ├── tsconfig.json │ ├── webpack.base.config.js │ ├── webpack.dev.config.js │ └── webpack.prod.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于lerna和single-spa,sysyem.js搭建vue的微前端框架 2 | 3 | 4 | ## 为什么要用微前端 5 | 目前随着前端的不断发展,企业工程项目体积越来越大,页面越来越多,项目变得十分臃肿,维护起来也十分困难,有时我们仅仅更改项目简单样式,都需要整个项目重新打包上线,给开发人员造成了不小的麻烦,也非常浪费时间。老项目为了融入到新项目也需要不断进行重构,造成的人力成本也非常的高。 6 | 7 | 微前端架构具备以下几个核心价值: 8 | - 技术栈无关 主框架不限制接入应用的技术栈,子应用具备完全自主权 9 | - 独立开发、独立部署 子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 10 | - 独立运行时 每个子应用之间状态隔离,运行时状态不共享 11 | 12 | 13 | ## single-spa实现原理 14 | 首先对微前端路由进行注册,使用single-spa充当微前端加载器,并作为项目单一入口来接受所有页面URL的访问,根据页面URL与微前端的匹配关系,选择加载对应的微前端模块,再由该微前端模块进行路由响应URL,即微前端模块中路由找到相应的组件,渲染页面内容。 15 | 16 | 17 | ## sysyem.js的作用及好处 18 | `system.js`的作用就是动态按需加载模块。假如我们子项目都使用了`vue`,`vuex`,`vue-router`,每个项目都打包一次,就会很浪费。`system.js`可以配合`webpack`的`externals`属性,将这些模块配置成外链,然后实现按需加载,当然了,你也可以直接用script标签将这些公共的js全部引入,借助`system.js`这个插件,我们只需要将子项目的app.js暴露给它即可。 19 | 20 | 21 | ## 什么是Lerna 22 | 当前端项目变得越来越大的时候,我们通常会将公共代码拆分出来,成为一个个独立的`npm`包进行维护。但是这样一来,各种包之间的依赖管理就十分让人头疼。为了解决这种问题,我们可以将不同的`npm`包项目都放在同一个项目来管理。这样的项目开发策略也称作`monorepo`。`Lerna`就是这样一个你更好地进行这项工作的工具。`Lerna`是一个使用`git`和`npm`来处理多包依赖管理的工具,利用它能够自动帮助我们管理各种模块包之间的版本依赖关系。目前,已经有很多公共库都使用Lerna作为它们的模块依赖管理工具了,如:`babel`, `create-react-app`, `react-router`, `jest`等。 23 | 1. 解决包之间的依赖关系。 24 | 2. 通过git仓库检测改动,自动同步。 25 | 3. 根据相关的git提交的commit,生成`CHANGELOG`。 26 | 27 | 你还需要全局安装 Lerna: 28 | 29 | ```shell 30 | npm install -g lerna 31 | ``` 32 | 33 | ## 基于vue微前端项目搭建 34 | 35 | ### 1.项目初始化 36 | 37 | ```shell 38 | mkdir lerna-project & cd lerna-project` 39 | 40 | lerna init 41 | ``` 42 | 执行成功后,目录下将会生成这样的目录结构。 43 | ``` 44 | ├── README.md 45 | ├── lerna.json # Lerna 配置文件 46 | ├── package.json 47 | ├── packages # 应用包目录 48 | ``` 49 | ### 2.Set up yarn的workspaces模式 50 | 默认是npm, 而且每个子package都有自己的`node_modules`,通过这样设置后,只有顶层有一个`node_modules` 51 | 52 | ```json 53 | { 54 | "packages": [ 55 | "packages/*" 56 | ], 57 | "useWorkspaces": true, 58 | "npmClient": "yarn", 59 | "version": "0.0.0" 60 | } 61 | ``` 62 | 同时`package.json` 设置 `private` 为 true,防止根目录被发布到 `npm`: 63 | 64 | 65 | ```json 66 | { 67 | "private": true, 68 | "workspaces": [ 69 | "packages/*" 70 | ] 71 | } 72 | 73 | ``` 74 | 配置根目录下的 `lerna.json` 使用 yarn 客户端并使用 `workspaces` 75 | ```shell 76 | yarn config set workspaces-experimental true 77 | ``` 78 | 79 | 80 | 81 | 82 | 83 | ### 3.注册子应用 84 | #### 第一步:使用vue-cli创建子应用 85 | 86 | ```shell 87 | # 进入packages目录 88 | cd packages 89 | 90 | # 创建应用 91 | vue create app1 92 | 93 | // 项目目录结构 94 | ├── public 95 | ├── src 96 | │ ├── main.js 97 | │ ├── assets 98 | │ ├── components 99 | │ └── App.vue 100 | ├── vue.config.js 101 | ├── package.json 102 | ├── README.md 103 | └── yarn.lock 104 | ``` 105 | 106 | #### 第二步:使用vue-cli-plugin-single-spa插件快速生成spa项目 107 | ```shell 108 | # 会自动修改main.js加入singleSpaVue,和生成set-public-path.js 109 | vue add single-spa 110 | ``` 111 | 112 | 生成的main.js文件 113 | ```js 114 | const vueLifecycles = singleSpaVue({ 115 | Vue, 116 | appOptions: { 117 | // el: '#app', // 没有挂载点默认挂载到body下 118 | render: (h) => h(App), 119 | router, 120 | store: window.rootStore, 121 | }, 122 | }); 123 | 124 | export const bootstrap = [ 125 | vueLifecycles.bootstrap 126 | ]; 127 | export const mount = vueLifecycles.mount; 128 | export const unmount = vueLifecycles.unmount; 129 | 130 | ``` 131 | 132 | 133 | #### 第三步:设置环境变量.env 134 | 135 | ```shell 136 | # 应用名称 137 | VUE_APP_NAME=app1 138 | # 应用根路径,默认值为: '/',如果要发布到子目录,此值必须指定 139 | VUE_APP_BASE_URL=/ 140 | # 端口,子项目开发最好设置固定端口, 避免频繁修改配置文件,设置一个固定的特殊端口,尽量避免端口冲突。 141 | port=8081 142 | ``` 143 | 144 | #### 第四步: 设置vue.config.js修改webpack配置 145 | 146 | ```js 147 | const isProduction = process.env.NODE_ENV === 'production' 148 | const appName = process.env.VUE_APP_NAME 149 | const port = process.env.port 150 | const baseUrl = process.env.VUE_APP_BASE_URL 151 | module.exports = { 152 | // 防止开发环境下的加载问题 153 | publicPath: isProduction ? `${baseUrl}${appName}/` : `http://localhost:${port}/`, 154 | 155 | // css在所有环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js) 156 | css: { 157 | extract: false 158 | }, 159 | 160 | productionSourceMap: false, 161 | 162 | outputDir: path.resolve(dirname, `../../dist/${appName}`), // 统一打包到根目录下的dist下 163 | chainWebpack: config => { 164 | config.devServer.set('inline', false) 165 | config.devServer.set('hot', true) 166 | config.externals(['vue', 'vue-router']) 167 | 168 | // 保证打包出来的是一个js文件,供主应用进行加载 169 | config.output.library(appName).libraryTarget('umd') 170 | 171 | config.externals(['vue', 'vue-router', 'vuex']) // 一定要引否则说没有注册 172 | 173 | if (process.env.NODE_ENV !== 'production') { 174 | // 打包目标文件加上 hash 字符串,禁止浏览器缓存 175 | config.output.filename('js/index.[hash:8].js') 176 | } 177 | }, 178 | } 179 | 180 | ``` 181 | 182 | ### 4.新建主项目 183 | 184 | #### 第一步:添加主项目package 185 | 186 | ```shell 187 | # 进入packages目录 188 | cd packages 189 | # 创建一个packge目录, 进入root-html-file目录 190 | mkdir root-html-file && cd root-html-file 191 | # 初始化一个package 192 | npm init -y 193 | ``` 194 | #### 第二步:新建主项目index.html 195 | > 主应用主要是扮演路由分发,资源加载的作用的角色 196 | ```html 197 | 198 | 199 | 200 | 201 | 202 | Vue-Microfrontends 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 |
219 | 220 | 221 | ``` 222 | #### 第三步:编辑importMapjson文件,配置对应子应用的文件 223 | ```json 224 | { 225 | "imports": { 226 | "navbar": "http://localhost:8888/js/app.js", 227 | "app1": "http://localhost:8081/js/app.js", 228 | "app2": "http://localhost:8082/js/app.js", 229 | "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js", 230 | "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js", 231 | "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js", 232 | "vuex": "https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js" 233 | } 234 | } 235 | ``` 236 | > 到时systemjs可以直接去import,具体作用参考[systemjs](https://github.com/systemjs/systemjs) 237 | 238 | #### 第四步:注册app应用 239 | ```js 240 | // 注册子应用 241 | singleSpa.registerApplication( 242 | 'app1', // systemjs-webpack-interop, 去匹配子应用的名称 243 | () => System.import('app1'), // 资源路径 244 | location => location.hash.startsWith('/app1') // 资源激活的 245 | ) 246 | 247 | singleSpa.registerApplication( 248 | 'app2', // systemjs-webpack-interop, 去匹配子应用的名称 249 | () => System.import('app2'), // 资源路径 250 | location => location.hash.startsWith('#/app2') // 资源激活的 251 | ) 252 | singleSpa.registerApplication( 253 | 'app2', // systemjs-webpack-interop, 去匹配子应用的名称 254 | () => System.import('app2'), // 资源路径 255 | location => location.hash.startsWith('#/app2') // 资源激活的 256 | ) 257 | // 开始singleSpa 258 | singleSpa.start(); 259 | ``` 260 | 261 | 262 | 263 | 264 | 265 | #### 第五步:项目开发 266 | 267 | 项目的基本目录结构如下: 268 | 269 | ``` 270 | . 271 | ├── README.md 272 | ├── lerna.json # Lerna 配置文件 273 | ├── node_modules 274 | ├── package.json 275 | ├── packages # 应用包目录 276 | │   ├── app1 # 应用1 277 | │   ├── app2 # 应用2 278 | │   ├── navbar # 主应用 279 | │   └── root-html-file # 入口 280 | └── yarn.lock 281 | ``` 282 | 283 | 如上图所示,所有的应用都存放在 `packages` 目录中。其中 `root-html-file` 为入口项目,`navbar` 为常驻的主应用,这两者在开发过程中必须启动相应的服务。其他为待开发的子应用。 284 | 285 | 286 | ## 项目的优化 287 | ### 抽取子应用资源配置 288 | 在主应用中抽取所有的子应用到一个通用的`app.config.json`文件配置 289 | ```json 290 | { 291 | "apps": [ 292 | { 293 | "name": "navbar", // 应用名称 294 | "main": "http://localhost:8888/js/app.js", // 应用的入口 295 | "path": "/", // 是否为常驻应用 296 | "base": true, // 是否使用history模式 297 | "hash": true // 是否使用hash模式 298 | }, 299 | { 300 | "name": "app1", 301 | "main": "http://localhost:8081/js/app.js", 302 | "path": "/app1", 303 | "base": false, 304 | "hash": true 305 | }, 306 | { 307 | "name": "app2", 308 | "main": "http://localhost:8082/js/app.js", 309 | "path": "/app2", 310 | "base": false, 311 | "hash": true 312 | } 313 | ] 314 | } 315 | ``` 316 | 317 | 主应用的入口文件中注册子应用 318 | ```js 319 | try { 320 | // 读取应用配置并注册应用 321 | const config = await System.import(`/app.config.json`) 322 | const { apps } = config.default 323 | apps && apps.forEach((app: AppConfig) => { 324 | const { commonsChunks: chunks } = app 325 | registerApp(singleSpa, app) 326 | }) 327 | singleSpa.start() 328 | } catch (e) { 329 | throw new Error('应用配置加载失败') 330 | } 331 | 332 | /** 333 | * 注册应用 334 | * */ 335 | function registerApp (spa, app) { 336 | const activityFunc = app.hash ? hashPrefix(app) : pathPrefix(app) 337 | spa.registerApplication( 338 | app.name, 339 | () => System.import(app.main), 340 | app.base ? (() => true) : activityFunc, 341 | { 342 | store 343 | } 344 | ) 345 | } 346 | 347 | 348 | /** 349 | * hash匹配模式 350 | * @param app 应用配置 351 | */ 352 | function hashPrefix (app) { 353 | return function (location) { 354 | if (!app.path) return true 355 | 356 | if (Array.isArray(app.path)) { 357 | if (app.path.some(path => location.hash.startsWith(`#${path}`))) { 358 | return true 359 | } 360 | } else if (location.hash.startsWith(`#${app.path}`)) { 361 | return true 362 | } 363 | 364 | return false 365 | } 366 | } 367 | 368 | /** 369 | * 普通路径匹配模式 370 | * @param app 应用配置 371 | */ 372 | function pathPrefix (app) { 373 | return function (location) { 374 | if (!app.path) return true 375 | 376 | if (Array.isArray(app.path)) { 377 | if (app.path.some(path => location.pathname.startsWith(path))) { 378 | return true 379 | } 380 | } else if (location.pathname.startsWith(app.path)) { 381 | return true 382 | } 383 | 384 | return false 385 | } 386 | } 387 | 388 | ``` 389 | 390 | ### 所有子项目公用一个使用vuex 391 | 392 | 在主项目index.html注册vuex的插件,通过window对象存储,子项目加载启动时候通过`registerModule`方式注入子应用的模块和自身的vue实例上 393 | ```js 394 | // 主应用的js中 395 | Vue.use(Vuex) 396 | window.rootStore = new Vuex.Store() // 全局注册唯一的vuex, 供子应用的共享 397 | 398 | 399 | // 子应用的main.js 400 | export const bootstrap = [ 401 | () => { 402 | return new Promise(async (resolve, reject) => { 403 | // 注册当前应用的store 404 | window.rootStore.registerModule(VUE_APP_NAME, store) 405 | resolve() 406 | }) 407 | }, 408 | vueLifecycles.bootstrap 409 | ]; 410 | export const mount = vueLifecycles.mount; 411 | export const unmount = vueLifecycles.unmount; 412 | 413 | 414 | ``` 415 | 416 | 417 | ### 样式隔离 418 | 我们使用postcss的一个插件:`postcss-selector-namespace`。 419 | 他会把你项目里的所有css都会添加一个类名前缀。这样就可以实现命名空间隔离。 420 | 首先,我们先安装这个插件:`npm install postcss-selector-namespace --save -d` 421 | 项目目录下新建 `postcss.config.js`,使用插件: 422 | ```js 423 | // postcss.config.js 424 | 425 | module.exports = { 426 | plugins: { 427 | // postcss-selector-namespace: 给所有css添加统一前缀,然后父项目添加命名空间 428 | 'postcss-selector-namespace': { 429 | namespace(css) { 430 | // element-ui的样式不需要添加命名空间 431 | if (css.includes('element-variables.scss')) return ''; 432 | return '.app1' // 返回要添加的类名 433 | } 434 | }, 435 | } 436 | } 437 | 438 | ``` 439 | 然后父项目添加命名空间 440 | 441 | ```js 442 | // 切换子系统的时候给body加上对应子系统的 class namespace 443 | window.addEventListener('single-spa:app-change', () => { 444 | const app = singleSpa.getMountedApps().pop(); 445 | const isApp = /^app-\w+$/.test(app); 446 | if (app) document.body.className = app; 447 | }); 448 | ``` 449 | 450 | 451 | 452 | 453 | 454 | 455 | ### 生产部署利用manifest自动加载生成子应用的app.config.json路径和importMapjson 456 | `stats-webpack-plugin`可以在你每次打包结束后,都生成一个`manifest.json` 文件,里面存放着本次打包的 `public_path` `bundle` `list` `chunk` `list` 文件大小依赖等等信息。可以根据这个信息来生成子应用的`app.config.json`路径和`importMapjson`. 457 | 458 | ```shell 459 | npm install stats-webpack-plugin --save -d 460 | ``` 461 | 462 | 在`vue.config.js`中使用: 463 | 464 | ```js 465 | { 466 | configureWebpack: { 467 | plugins: [ 468 | new StatsPlugin('manifest.json', { 469 | chunkModules: false, 470 | entrypoints: true, 471 | source: false, 472 | chunks: false, 473 | modules: false, 474 | assets: false, 475 | children: false, 476 | exclude: [/node_modules/] 477 | }), 478 | ] 479 | } 480 | } 481 | 482 | ``` 483 | 484 | 打包完成最后通过脚本`generate-app.js`生成对应,子应用的json路径和importMapjson 485 | ```js 486 | const path = require('path') 487 | const fs = require('fs') 488 | const root = process.cwd() 489 | console.log(`当前工作目录是: ${root}`); 490 | const dir = readDir(root) 491 | const jsons = readManifests(dir) 492 | generateFile(jsons) 493 | 494 | console.log('生成配置文件成功') 495 | 496 | 497 | function readDir(root) { 498 | const manifests = [] 499 | const files = fs.readdirSync(root) 500 | console.log(files) 501 | files.forEach(i => { 502 | const filePath = path.resolve(root, '.', i) 503 | const stat = fs.statSync(filePath); 504 | const is_direc = stat.isDirectory(); 505 | 506 | if (is_direc) { 507 | manifests.push(filePath) 508 | } 509 | 510 | }) 511 | return manifests 512 | } 513 | 514 | 515 | function readManifests(files) { 516 | const jsons = {} 517 | files.forEach(i => { 518 | const manifest = path.resolve(i, './manifest.json') 519 | if (fs.existsSync(manifest)) { 520 | const { publicPath, entrypoints: { app: { assets } } } = require(manifest) 521 | const name = publicPath.slice(1, -1) 522 | jsons[name] = `${publicPath}${assets}` 523 | } 524 | }) 525 | 526 | return jsons 527 | 528 | } 529 | 530 | 531 | 532 | function generateFile(jsons) { 533 | const { apps } = require('./app.config.json') 534 | const { imports } = require('./importmap.json') 535 | Object.keys(jsons).forEach(key => { 536 | imports[key] = jsons[key] 537 | }) 538 | apps.forEach(i => { 539 | const { name } = i 540 | 541 | if (jsons[name]) { 542 | i.main = jsons[name] 543 | } 544 | }) 545 | 546 | fs.writeFileSync('./importmap.json', JSON.stringify( 547 | { 548 | imports 549 | } 550 | )) 551 | 552 | fs.writeFileSync('./app.config.json', JSON.stringify( 553 | { 554 | apps 555 | } 556 | )) 557 | 558 | } 559 | 560 | 561 | ``` 562 | ## 应用打包 563 | 在根目录执行`build`命令, `packages`里面的所有`build`命令都会执行,这会在根目录生成 dist 目录下, 564 | ```shell 565 | lerna run build 566 | ``` 567 | 最终生成的目录结构如下 568 | ``` 569 | . 570 | ├── dist 571 | │ ├── app1/ 572 | │ ├── app2/ 573 | ├── navbar/ 574 | │ ├── app.config.json 575 | │ ├── importmap.json 576 | │ ├── main.js 577 | │ ├── generate-app.js 578 | │ └── index.html 579 | ``` 580 | 最后,执行以下命令生成 `generate-app.js`,重新生成带hash资源路径的`importmap.json`和`app.config.json`文件: 581 | 582 | ```shell 583 | cd dist && node generate-app.js 584 | ``` 585 | 文章中的完整demo文件地址 586 | 587 | ## lerna启动项目 588 | ```shell 589 | 590 | # 清除所有的包 591 | lerna clean 592 | 593 | # npm i 下载依赖包或者生成本地软连接 594 | lerna bootstrap 595 | 596 | # npm i axios 所有包都添加axios 597 | lerna add axios 598 | 599 | # cd app1 & npm i axios 600 | lerna add axios --scope=app1 601 | 602 | ``` 603 | 604 | 605 | 606 | ## 参考文档 607 | - [lerna管理前端模块最佳实践](https://juejin.im/post/6844903568751722509) 608 | - [lerna 和 yarn 实现 monorepo](https://juejin.im/post/6855129007185362952#heading-14) 609 | - [从0实现一个single-spa的前端微服务(中)](https://juejin.im/post/6844904048043229192#heading-4) 610 | - [Single-Spa + Vue Cli 微前端落地指南 + 视频 (项目隔离远程加载,自动引入)](https://juejin.im/post/6844904025565954055#heading-0) 611 | - [Single-Spa微前端落地(含nginx部署)](https://juejin.im/post/6844904158349246477) 612 | - [可能是你见过最完善的微前端解决方案](https://zhuanlan.zhihu.com/p/78362028) 613 | - [coexisting-vue-microfrontends](https://github.com/joeldenning/coexisting-vue-microfrontends) -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | const path = require('path') 3 | var history = require('connect-history-api-fallback'); 4 | var app = express(); 5 | app.use(history( 6 | { 7 | logger: console.log.bind(console) 8 | } 9 | )); 10 | app.use(express.static(path.resolve(__dirname, './dist'))); 11 | 12 | app.listen(3333, function () { 13 | console.log('Example app listening on port 3333!'); 14 | }); -------------------------------------------------------------------------------- /build/generate-config.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | const path = require('path') 3 | // const argv = require('minimist')(process.argv.slice(2)) 4 | const StatsPlugin = require('stats-webpack-plugin') 5 | 6 | module.exports = function(process, dirname) { 7 | const isProduction = process.env.NODE_ENV === 'production' 8 | const appName = process.env.VUE_APP_NAME 9 | const port = process.env.port 10 | // const basePath = argv['base-path'] || '/' 11 | 12 | const baseUrl = process.env.VUE_APP_BASE_URL 13 | 14 | 15 | return { 16 | publicPath: isProduction ? `${baseUrl}${appName}/` : `http://localhost:${port}/`, 17 | 18 | // css在所有环境下,都不单独打包为文件。这样是为了保证最小引入(只引入js) 19 | css: { 20 | extract: false 21 | }, 22 | 23 | productionSourceMap: false, 24 | 25 | outputDir: path.resolve(dirname, `../../dist/${appName}`), 26 | 27 | // css: { 28 | // loaderOptions: { 29 | // less: { 30 | // modifyVars: { 31 | // 'primary-color': '#FF9F08', 32 | // 'link-color': '#FF9F08', 33 | // 'body-background': '#FCFBF9', 34 | // }, 35 | // javascriptEnabled: true, 36 | // }, 37 | // sass: { 38 | // prependData: `@import "~@root/common/styles/settings.scss";`, 39 | // }, 40 | // }, 41 | // }, 42 | 43 | configureWebpack: config => { 44 | config.devServer = { 45 | port, 46 | headers: { 47 | 'Access-Control-Allow-Origin': '*', 48 | }, 49 | } 50 | 51 | config.plugins.push( 52 | new StatsPlugin('manifest.json', { 53 | chunkModules: false, 54 | entrypoints: true, 55 | env: true, 56 | source: false, 57 | chunks: false, 58 | modules: false, 59 | assets: false, 60 | children: false, 61 | exclude: [/node_modules/] 62 | }), 63 | ) 64 | }, 65 | 66 | chainWebpack: config => { 67 | // 根目录别名 68 | // config.resolve.alias.set('@root', path.resolve(dirname, '../../')) 69 | // // .set('@ant-design-vue/icons/lib/dist$', path.resolve(__dirname, './src/assets/icons.ts')) 70 | 71 | // // 公用的第三方库不参与打包 72 | // config.externals([ 73 | // 'vue', 74 | // 'vue-router', 75 | // 'vuex', 76 | // 'axios', 77 | // 'echarts', 78 | // 'lodash', 79 | // { moment: 'moment' }, 80 | // { '../moment': 'moment' }, // 这句很关键,在 moment 内置语种包中通过 ../moment 来调用 moment 的方法,所以也需要将这个设置为外置引用 window.moment 81 | // ]) 82 | 83 | config.output.library(appName).libraryTarget('umd') 84 | 85 | config.externals(['vue', 'vue-router', 'vuex']) // 一定要引否则说没有注册 86 | 87 | if (isProduction) { 88 | // 打包目标文件加上 hash 字符串,禁止浏览器缓存 89 | config.output.filename('js/index.[hash:8].js') 90 | } 91 | }, 92 | } 93 | } -------------------------------------------------------------------------------- /build/postcss.plugin.js: -------------------------------------------------------------------------------- 1 | 2 | const appName = process.env.VUE_APP_NAME 3 | console.log(appName) 4 | module.exports = { 5 | // postcss-selector-namespace: 给所有css添加统一前缀,然后父项目添加命名空间 6 | 'postcss-selector-namespace': { 7 | namespace(css) { 8 | // element-ui的样式不需要添加命名空间 9 | if (css.includes('element-variables.scss')) return ''; 10 | return `.${appName}` // 返回要添加的类名 11 | } 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "useWorkspaces": true, 6 | "npmClient": "yarn", 7 | "version": "0.0.0" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "clear": "lerna clean", 6 | "ic": "lerna bootstrap", 7 | "build": "lerna run build && cd dist && node generate-app.js" 8 | }, 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "devDependencies": { 13 | "connect-history-api-fallback": "^1.6.0", 14 | "express": "^4.17.1", 15 | "lerna": "^3.22.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/app1/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /packages/app1/.env: -------------------------------------------------------------------------------- 1 | # 应用名称 2 | VUE_APP_NAME=app1 3 | 4 | # 应用根路径,默认值为: '/',如果要发布到子目录,此值必须指定 5 | VUE_APP_BASE_URL=/ 6 | 7 | # 端口,用于开发 8 | port=8081 -------------------------------------------------------------------------------- /packages/app1/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /packages/app1/README.md: -------------------------------------------------------------------------------- 1 | # app1 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /packages/app1/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/app1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app1", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.4.3", 11 | "single-spa-vue": "^1.5.4", 12 | "systemjs-webpack-interop": "^1.1.2", 13 | "vue": "^2.6.10", 14 | "vue-router": "^3.1.3", 15 | "vuex": "^3.5.1" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^4.1.0", 19 | "@vue/cli-plugin-router": "^4.1.0", 20 | "@vue/cli-service": "^4.1.0", 21 | "postcss-selector-namespace": "^3.0.1", 22 | "stats-webpack-plugin": "^0.7.0", 23 | "vue-cli-plugin-single-spa": "^1.0.1", 24 | "vue-template-compiler": "^2.6.10" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/app1/postcss.config.js: -------------------------------------------------------------------------------- 1 | const plugins = require('../../build/postcss.plugin') 2 | module.exports = { 3 | plugins: { 4 | autoprefixer: {}, 5 | ...plugins 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/app1/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongofeng/vue-mic/413b2ff5cdd92f62a3c97447b69da580245ff2c2/packages/app1/public/favicon.ico -------------------------------------------------------------------------------- /packages/app1/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | app1 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/app1/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /packages/app1/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongofeng/vue-mic/413b2ff5cdd92f62a3c97447b69da580245ff2c2/packages/app1/src/assets/logo.png -------------------------------------------------------------------------------- /packages/app1/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 40 | 41 | 42 | 58 | -------------------------------------------------------------------------------- /packages/app1/src/main.js: -------------------------------------------------------------------------------- 1 | import './set-public-path' 2 | import Vue from 'vue'; 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import singleSpaVue from 'single-spa-vue'; 6 | import {store} from './store' 7 | const VUE_APP_NAME = process.env.VUE_APP_NAME 8 | 9 | Vue.config.productionTip = false; 10 | 11 | 12 | // 主应用注册成功后会在window下挂载singleSpaNavigate方法 13 | // 为了独立运行,避免子项目页面为空, 14 | // 判断如果不在微前端环境下进行独立渲染html 15 | if (!window.singleSpaNavigate) { 16 | new Vue({ 17 | render: h => h(App), 18 | }).$mount('#app') 19 | } 20 | 21 | 22 | const vueLifecycles = singleSpaVue({ 23 | Vue, 24 | appOptions: { 25 | // el: '#wrap', // 没有挂载点默认挂载到body下 26 | render: (h) => h(App), 27 | router, 28 | store: window.rootStore, 29 | }, 30 | }); 31 | 32 | export const bootstrap = [ 33 | () => { 34 | return new Promise(async (resolve, reject) => { 35 | // 注册当前应用的store 36 | console.log(window.rootStore) 37 | window.rootStore.registerModule(VUE_APP_NAME, store) 38 | resolve() 39 | }) 40 | }, 41 | vueLifecycles.bootstrap 42 | ]; 43 | export const mount = vueLifecycles.mount; 44 | export const unmount = vueLifecycles.unmount; 45 | -------------------------------------------------------------------------------- /packages/app1/src/router.js: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | import Home from './views/Home.vue' 3 | 4 | export default new Router({ 5 | // mode: 'history', 6 | // base: process.env.BASE_URL, 7 | routes: [ 8 | { 9 | path: '/app1', 10 | name: 'home', 11 | component: Home 12 | }, 13 | ] 14 | }) 15 | -------------------------------------------------------------------------------- /packages/app1/src/set-public-path.js: -------------------------------------------------------------------------------- 1 | import { setPublicPath } from 'systemjs-webpack-interop' 2 | 3 | // 为了让sing-spa知道这是app1的应用 4 | const appName = process.env.VUE_APP_NAME || '' 5 | 6 | setPublicPath(appName, 2) -------------------------------------------------------------------------------- /packages/app1/src/store.js: -------------------------------------------------------------------------------- 1 | const debug = process.env.NODE_ENV !== 'production' 2 | export const store = { 3 | state: { 4 | count: 1 5 | }, 6 | mutations: { 7 | increment (state) { 8 | state.count++ 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/app1/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 29 | -------------------------------------------------------------------------------- /packages/app1/vue.config.js: -------------------------------------------------------------------------------- 1 | const generateConfig = require('../../build/generate-config') 2 | 3 | module.exports = generateConfig(process, __dirname) -------------------------------------------------------------------------------- /packages/app2/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /packages/app2/.env: -------------------------------------------------------------------------------- 1 | # 应用名称 2 | VUE_APP_NAME=app2 3 | 4 | # 应用根路径,默认值为: '/',如果要发布到子目录,此值必须指定 5 | VUE_APP_BASE_URL=/ 6 | 7 | # 端口,用于开发 8 | port=8082 -------------------------------------------------------------------------------- /packages/app2/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /packages/app2/README.md: -------------------------------------------------------------------------------- 1 | # app2 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /packages/app2/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/app2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app2", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.4.3", 11 | "single-spa-vue": "^1.5.4", 12 | "systemjs-webpack-interop": "^1.1.2", 13 | "vue": "^2.6.10", 14 | "vue-router": "^3.1.3", 15 | "vuex": "^3.5.1" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^4.1.0", 19 | "@vue/cli-plugin-router": "^4.1.0", 20 | "@vue/cli-service": "^4.1.0", 21 | "postcss-selector-namespace": "^3.0.1", 22 | "stats-webpack-plugin": "^0.7.0", 23 | "vue-cli-plugin-single-spa": "^1.0.1", 24 | "vue-template-compiler": "^2.6.10" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/app2/postcss.config.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const plugins = require('../../build/postcss.plugin') 4 | module.exports = { 5 | plugins: { 6 | autoprefixer: {}, 7 | ...plugins 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/app2/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongofeng/vue-mic/413b2ff5cdd92f62a3c97447b69da580245ff2c2/packages/app2/public/favicon.ico -------------------------------------------------------------------------------- /packages/app2/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | app2 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/app2/src/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 21 | -------------------------------------------------------------------------------- /packages/app2/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongofeng/vue-mic/413b2ff5cdd92f62a3c97447b69da580245ff2c2/packages/app2/src/assets/logo.png -------------------------------------------------------------------------------- /packages/app2/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 17 | 33 | -------------------------------------------------------------------------------- /packages/app2/src/main.js: -------------------------------------------------------------------------------- 1 | import './set-public-path' 2 | import Vue from 'vue'; 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import singleSpaVue from 'single-spa-vue'; 6 | import {store} from './store' 7 | const VUE_APP_NAME = process.env.VUE_APP_NAME 8 | 9 | Vue.config.productionTip = false; 10 | 11 | // 主应用注册成功后会在window下挂载singleSpaNavigate方法 12 | // 为了独立运行,避免子项目页面为空, 13 | // 判断如果不在微前端环境下进行独立渲染html 14 | if (!window.singleSpaNavigate) { 15 | new Vue({ 16 | render: h => h(App), 17 | }).$mount('#app') 18 | } 19 | 20 | 21 | 22 | const vueLifecycles = singleSpaVue({ 23 | Vue, 24 | appOptions: { 25 | // el: '#wrap', // 没有挂载点默认挂载到body下 26 | render: (h) => h(App), 27 | router, 28 | store: window.rootStore, 29 | }, 30 | }); 31 | 32 | export const bootstrap = [ 33 | () => { 34 | return new Promise(async (resolve, reject) => { 35 | // 注册当前应用的store 36 | console.log(window.rootStore) 37 | window.rootStore.registerModule(VUE_APP_NAME, store) 38 | resolve() 39 | }) 40 | }, 41 | vueLifecycles.bootstrap 42 | ]; 43 | export const mount = vueLifecycles.mount; 44 | export const unmount = vueLifecycles.unmount; 45 | -------------------------------------------------------------------------------- /packages/app2/src/router.js: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | import Home from './views/Home.vue' 3 | 4 | export default new Router({ 5 | // mode: 'history', 6 | // base: process.env.BASE_URL, 7 | routes: [ 8 | { 9 | path: '/app2', 10 | name: 'home', 11 | component: Home 12 | }, 13 | { 14 | path: '/app2/about', 15 | name: 'about', 16 | // route level code-splitting 17 | // this generates a separate chunk (about.[hash].js) for this route 18 | // which is lazy-loaded when the route is visited. 19 | component: () => import(/* webpackChunkName: "about" */ './views/About.vue') 20 | } 21 | ] 22 | }) 23 | -------------------------------------------------------------------------------- /packages/app2/src/set-public-path.js: -------------------------------------------------------------------------------- 1 | import { setPublicPath } from 'systemjs-webpack-interop' 2 | 3 | const appName = process.env.VUE_APP_NAME || '' 4 | 5 | setPublicPath(appName, 2) -------------------------------------------------------------------------------- /packages/app2/src/store.js: -------------------------------------------------------------------------------- 1 | const debug = process.env.NODE_ENV !== 'production' 2 | export const store = { 3 | state: { 4 | count: 2 5 | }, 6 | mutations: { 7 | increment (state) { 8 | state.count++ 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/app2/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /packages/app2/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /packages/app2/vue.config.js: -------------------------------------------------------------------------------- 1 | // // Temporary until we can use https://github.com/webpack/webpack-dev-server/pull/2143 2 | // const path = require('path') 3 | // const isProduction = process.env.NODE_ENV === 'production' 4 | // const baseUrl = process.env.VUE_APP_BASE_URL 5 | // const appName = process.env.VUE_APP_NAME 6 | // module.exports = { 7 | // publicPath: isProduction ? `${baseUrl}${appName}/` : `http://localhost:${process.env.port}/`, 8 | 9 | 10 | // outputDir: path.resolve(__dirname, `../../dist/${appName}`), 11 | 12 | // devServer: { 13 | // port: process.env.port, 14 | // headers: { 15 | // 'Access-Control-Allow-Origin': '*', 16 | // }, 17 | // }, 18 | 19 | 20 | 21 | 22 | // chainWebpack: config => { 23 | // config.devServer.set('inline', false) 24 | // config.devServer.set('hot', true) 25 | // // Vue CLI 4 output filename is js/[chunkName].js, different from Vue CLI 3 26 | // // More Detail: https://github.com/vuejs/vue-cli/blob/master/packages/%40vue/cli-service/lib/config/app.js#L29 27 | // if (process.env.NODE_ENV !== 'production') { 28 | // config.output.filename(`js/[name].js`) 29 | // } 30 | // config.externals(['vue', 'vue-router']) 31 | 32 | // config.output.library(appName).libraryTarget('umd') 33 | // }, 34 | // // filenameHashing: false 35 | // } 36 | const generateConfig = require('../../build/generate-config') 37 | 38 | module.exports = generateConfig(process, __dirname) -------------------------------------------------------------------------------- /packages/navbar/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /packages/navbar/.env: -------------------------------------------------------------------------------- 1 | # 应用名称 2 | VUE_APP_NAME=navbar 3 | 4 | # 应用根路径,默认值为: '/',如果要发布到子目录,此值必须指定 5 | VUE_APP_BASE_URL=/ 6 | 7 | # 端口,用于开发 8 | port=8888 -------------------------------------------------------------------------------- /packages/navbar/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /packages/navbar/README.md: -------------------------------------------------------------------------------- 1 | # navbar 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | npm run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | npm run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /packages/navbar/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/navbar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navbar", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.4.3", 11 | "single-spa-vue": "^1.5.4", 12 | "systemjs-webpack-interop": "^1.1.2", 13 | "vue": "^2.6.10", 14 | "vue-router": "^3.1.3", 15 | "vuex": "^3.5.1" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^4.1.0", 19 | "@vue/cli-plugin-router": "^4.1.0", 20 | "@vue/cli-service": "^4.1.0", 21 | "postcss-selector-namespace": "^3.0.1", 22 | "stats-webpack-plugin": "^0.7.0", 23 | "vue-cli-plugin-single-spa": "^1.0.1", 24 | "vue-template-compiler": "^2.6.10" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/navbar/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/navbar/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongofeng/vue-mic/413b2ff5cdd92f62a3c97447b69da580245ff2c2/packages/navbar/public/favicon.ico -------------------------------------------------------------------------------- /packages/navbar/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | navbar 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/navbar/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 32 | -------------------------------------------------------------------------------- /packages/navbar/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mongofeng/vue-mic/413b2ff5cdd92f62a3c97447b69da580245ff2c2/packages/navbar/src/assets/logo.png -------------------------------------------------------------------------------- /packages/navbar/src/main.js: -------------------------------------------------------------------------------- 1 | import './set-public-path' 2 | import Vue from 'vue'; 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import singleSpaVue from 'single-spa-vue'; 6 | import {store} from './store' 7 | const VUE_APP_NAME = process.env.VUE_APP_NAME 8 | 9 | Vue.config.productionTip = false; 10 | 11 | const vueLifecycles = singleSpaVue({ 12 | Vue, 13 | appOptions: { 14 | el: '#root', // 没有挂载点默认挂载到body下 15 | render: (h) => h(App), 16 | router, 17 | store: window.rootStore, 18 | }, 19 | }); 20 | 21 | export const bootstrap = [ 22 | () => { 23 | return new Promise(async (resolve, reject) => { 24 | // 注册当前应用的store 25 | console.log(window.rootStore) 26 | window.rootStore.registerModule(VUE_APP_NAME, store) 27 | resolve() 28 | }) 29 | }, 30 | vueLifecycles.bootstrap 31 | ]; 32 | export const mount = vueLifecycles.mount; 33 | export const unmount = vueLifecycles.unmount; 34 | -------------------------------------------------------------------------------- /packages/navbar/src/router.js: -------------------------------------------------------------------------------- 1 | import Router from 'vue-router' 2 | 3 | export default new Router({ 4 | // mode: 'history', 5 | // base: process.env.BASE_URL, 6 | routes: [ 7 | // { 8 | // path: '/', 9 | // name: 'home', 10 | // component: Home 11 | // }, 12 | // { 13 | // path: '/about', 14 | // name: 'about', 15 | // // route level code-splitting 16 | // // this generates a separate chunk (about.[hash].js) for this route 17 | // // which is lazy-loaded when the route is visited. 18 | // component: () => import(/* webpackChunkName: "about" */ './views/About.vue') 19 | // } 20 | ] 21 | }) 22 | -------------------------------------------------------------------------------- /packages/navbar/src/set-public-path.js: -------------------------------------------------------------------------------- 1 | import { setPublicPath } from 'systemjs-webpack-interop' 2 | 3 | const appName = process.env.VUE_APP_NAME || '' 4 | 5 | setPublicPath(appName, 2) -------------------------------------------------------------------------------- /packages/navbar/src/store.js: -------------------------------------------------------------------------------- 1 | const debug = process.env.NODE_ENV !== 'production' 2 | export const store = { 3 | state: { 4 | count: 0 5 | }, 6 | mutations: { 7 | increment (state) { 8 | state.count++ 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/navbar/vue.config.js: -------------------------------------------------------------------------------- 1 | const generateConfig = require('../../build/generate-config') 2 | 3 | module.exports = generateConfig(process, __dirname) -------------------------------------------------------------------------------- /packages/root-html-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root-html-file", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "webpack-dev-server --config webpack.dev.config.js", 7 | "build": "webpack -p --progress --config webpack.prod.config.js" 8 | }, 9 | "dependencies": { 10 | "vue": "^2.6.10", 11 | "vuex": "^3.5.1" 12 | }, 13 | "devDependencies": { 14 | "@types/systemjs": "^6.1.0", 15 | "clean-webpack-plugin": "^3.0.0", 16 | 17 | "copy-webpack-plugin": "^6.0.3", 18 | 19 | "html-webpack-plugin": "^4.3.0", 20 | "ts-loader": "^8.0.3", 21 | "typescript": "^4.0.2", 22 | "webpack": "^4.44.1", 23 | "webpack-cli": "^3.3.12", 24 | "webpack-dev-server": "^3.11.0", 25 | "webpack-merge": "^5.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/root-html-file/public/app.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "navbar", 5 | "main": "http://localhost:8888/js/app.js", 6 | "path": "/", 7 | "commonsChunks": [], 8 | "base": true, 9 | "hash": true 10 | }, 11 | { 12 | "name": "app1", 13 | "main": "http://localhost:8081/js/app.js", 14 | "path": "/app1", 15 | "commonsChunks": [], 16 | "base": false, 17 | "hash": true 18 | }, 19 | { 20 | "name": "app2", 21 | "main": "http://localhost:8082/js/app.js", 22 | "path": "/app2", 23 | "commonsChunks": [], 24 | "base": false, 25 | "hash": true 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /packages/root-html-file/public/generate-app.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs'); 3 | const root = process.cwd() 4 | console.log(`当前工作目录是: ${root}`); 5 | 6 | const callback = (resolve, reject) => (err, data) => err ? reject(err) : resolve(data) 7 | 8 | const createPromise = (func) => (...args) => { 9 | return new Promise((resolve, reject) => { 10 | func(...args, callback(resolve, reject)) 11 | }) 12 | } 13 | 14 | 15 | 16 | main(root) 17 | 18 | /** 19 | * 读取文件夹的路径 20 | * @param {*} root 21 | */ 22 | async function readDir(root) { 23 | const manifests = [] 24 | const files = fs.readdirSync(root) 25 | console.log(files) 26 | const statPromise = createPromise(fs.stat) 27 | for (let i of files) { 28 | const filePath = path.resolve(root, '.', i) 29 | const stat = await statPromise(filePath); 30 | const is_direc = stat.isDirectory(); 31 | if (is_direc) { 32 | manifests.push(filePath) 33 | } 34 | } 35 | 36 | return manifests 37 | } 38 | 39 | /** 40 | * 读取json 41 | * @param {*} files 42 | */ 43 | function readManifests(files) { 44 | const jsons = {} 45 | files.forEach(i => { 46 | const manifest = path.resolve(i, './manifest.json') 47 | if (fs.existsSync(manifest)) { 48 | const { publicPath, entrypoints: { app: { assets } } } = require(manifest) 49 | const name = publicPath.slice(1, -1) 50 | jsons[name] = `${publicPath}${assets}` 51 | } 52 | }) 53 | 54 | return jsons 55 | 56 | } 57 | 58 | 59 | /** 60 | * 生成文件 61 | * @param {}} jsons 62 | */ 63 | async function generateFile(jsons) { 64 | const { apps } = require('./app.config.json') 65 | const { imports } = require('./importmap.json') 66 | 67 | const writeFilePromise = createPromise(fs.writeFile) 68 | 69 | 70 | Object.keys(jsons).forEach(key => { 71 | imports[key] = jsons[key] 72 | }) 73 | 74 | apps.forEach(i => { 75 | const { name } = i 76 | 77 | if (jsons[name]) { 78 | i.main = jsons[name] 79 | } 80 | }) 81 | 82 | 83 | Promise.all([ 84 | writeFilePromise('./importmap.json', JSON.stringify( 85 | { 86 | imports 87 | } 88 | )), 89 | writeFilePromise('./app.config.json', JSON.stringify( 90 | { 91 | apps 92 | } 93 | )) 94 | ]) 95 | 96 | } 97 | 98 | 99 | async function main(root) { 100 | const dir = await readDir(root) 101 | const jsons = readManifests(dir) 102 | await generateFile(jsons) 103 | console.log('生成配置文件成功') 104 | } 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /packages/root-html-file/public/importmap.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "navbar": "http://localhost:8888/js/app.js", 4 | "app1": "http://localhost:8081/js/app.js", 5 | "app2": "http://localhost:8082/js/app.js", 6 | "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js", 7 | "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js", 8 | "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js", 9 | "vuex": "https://cdn.jsdelivr.net/npm/vuex@3.1.2/dist/vuex.min.js" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/root-html-file/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue-Microfrontends 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 62 | 63 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /packages/root-html-file/src/common.d.ts: -------------------------------------------------------------------------------- 1 | declare interface AppConfig { 2 | name: string // 应用名称 3 | main: string // 应用入口文件,形如 '/common/index.3c4b55cf.js' 4 | store: string // 应用仓库 5 | path?: string | string[] // 应用路径,路由通过匹配 path 来决定加载哪个应用 6 | commonsChunks: string[] // 7 | base: boolean // 是否基座应用 8 | hash: boolean // 是否通过 hash 模式匹配 9 | } -------------------------------------------------------------------------------- /packages/root-html-file/src/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import { registerApp } from './utils/index' 3 | import 'systemjs' 4 | 5 | async function bootstrap () { 6 | const [singleSpa, Vue, VueRouter, Vuex] = await Promise.all([ 7 | System.import('single-spa'), 8 | System.import('vue'), 9 | System.import('vue-router'), 10 | System.import('vuex'), 11 | ]) 12 | console.log(Vue) 13 | 14 | 15 | Vue.config.devtools = process.env.NODE_ENV === 'development' 16 | Vue.use(VueRouter as any) 17 | Vue.use(Vuex as any) 18 | 19 | // @ts-ignore 20 | Vue.prototype.$eventBus = new Vue() 21 | // @ts-ignore 22 | window.rootStore = new Vuex.Store() // 全局注册唯一的vuex, 供子应用的共享 23 | 24 | try { 25 | // 读取应用配置并注册应用 26 | const config = await System.import(`/app.config.json`) 27 | console.log(config) 28 | const { apps } = config.default 29 | apps && apps.forEach((app: AppConfig) => { 30 | const { commonsChunks: chunks } = app 31 | if (chunks && chunks.length) { 32 | Promise.all(chunks.map(chunk => { 33 | return System.import(`/${app.name}/js/${chunk}.js`) // 加载完所有的异步chunk代码 34 | })).then(() => { 35 | registerApp(singleSpa, app) 36 | }) 37 | } else { 38 | registerApp(singleSpa, app) 39 | } 40 | }) 41 | 42 | // 切换子系统的时候给body加上对应子系统的 class namespace 43 | window.addEventListener('single-spa:app-change', () => { 44 | const app = singleSpa.getMountedApps().pop(); 45 | const isApp = /^app-\w+$/.test(app); 46 | if (app) document.body.className = app; 47 | }); 48 | 49 | singleSpa.start() 50 | } catch (e) { 51 | throw new Error('应用配置加载失败') 52 | } 53 | } 54 | 55 | bootstrap().then(r => { 56 | console.log('系统已成功启动:D') 57 | }) -------------------------------------------------------------------------------- /packages/root-html-file/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import 'systemjs' 2 | 3 | /** 4 | * hash匹配模式 5 | * @param app 应用配置 6 | */ 7 | export function hashPrefix (app: AppConfig) { 8 | return function (location: Location) { 9 | if (!app.path) return true 10 | 11 | if (Array.isArray(app.path)) { 12 | if (app.path.some(path => location.hash.startsWith(`#${path}`))) { 13 | return true 14 | } 15 | } else if (location.hash.startsWith(`#${app.path}`)) { 16 | return true 17 | } 18 | 19 | return false 20 | } 21 | } 22 | 23 | /** 24 | * 普通路径匹配模式 25 | * @param app 应用配置 26 | */ 27 | export function pathPrefix (app: AppConfig) { 28 | return function (location: Location) { 29 | if (!app.path) return true 30 | 31 | if (Array.isArray(app.path)) { 32 | if (app.path.some(path => location.pathname.startsWith(path))) { 33 | return true 34 | } 35 | } else if (location.pathname.startsWith(app.path)) { 36 | return true 37 | } 38 | 39 | return false 40 | } 41 | } 42 | 43 | /** 44 | * 注册应用 45 | * @param spa 46 | * @param app 47 | */ 48 | export async function registerApp (spa: any, app: AppConfig) { 49 | const activityFunc = app.hash ? hashPrefix(app) : pathPrefix(app) 50 | let store = null 51 | 52 | if (app.store) { 53 | try { 54 | store = await System.import(app.store) 55 | } catch (e) { 56 | throw new Error(`应用${app.name}的仓库:${app.store}加载失败`) 57 | } 58 | } 59 | 60 | spa.registerApplication( 61 | app.name, 62 | () => System.import(app.main), 63 | app.base ? (() => true) : activityFunc, 64 | { 65 | store 66 | } 67 | ) 68 | } -------------------------------------------------------------------------------- /packages/root-html-file/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "../../dist/", 4 | "noImplicitAny": true, 5 | "module": "es6", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true 9 | }, 10 | "include": ["./src/**/**/*"] 11 | } -------------------------------------------------------------------------------- /packages/root-html-file/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const {apps} = require('./public/app.config.json') 4 | module.exports = { 5 | entry: path.resolve(__dirname, './src/main.ts'), 6 | resolve: { 7 | extensions: [ '.tsx', '.ts', '.js' ] 8 | }, 9 | externals: { 10 | systemjs: 'System' 11 | }, 12 | module: { 13 | 14 | rules: [ 15 | { parser: { system: false } }, 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | include: path.resolve(__dirname, "./src"), 20 | } 21 | ] 22 | }, 23 | plugins: [ 24 | new HtmlWebpackPlugin({ 25 | title: 'MyApp', 26 | template: path.resolve(__dirname, './public/index.html'), 27 | apps, 28 | }) 29 | ] 30 | 31 | } -------------------------------------------------------------------------------- /packages/root-html-file/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const baseWebpackConfig = require('./webpack.base.config') 3 | const {merge} = require('webpack-merge') 4 | 5 | console.log(merge) 6 | 7 | module.exports = merge(baseWebpackConfig, { 8 | devtool: 'source-map', 9 | devServer: { 10 | stats: 'minimal', 11 | contentBase: path.join(__dirname, 'public'), 12 | port: 9000 13 | } 14 | }) -------------------------------------------------------------------------------- /packages/root-html-file/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const baseWebpackConfig = require('./webpack.base.config') 3 | const CopyWebpackPlugin = require('copy-webpack-plugin') 4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin') 5 | const { merge } = require('webpack-merge') 6 | 7 | 8 | module.exports = merge(baseWebpackConfig, { 9 | plugins: [ 10 | new CleanWebpackPlugin({ 11 | cleanOnceBeforeBuildPatterns: ['./*.js', './*.html', './*.json'], 12 | }), 13 | new CopyWebpackPlugin({ 14 | patterns: [ 15 | { from: path.resolve(__dirname, './public') }, 16 | ], 17 | }), 18 | ], 19 | output: { 20 | filename: '[name][hash:8].js', 21 | path: path.resolve(__dirname, '../../dist') 22 | }, 23 | }) --------------------------------------------------------------------------------