├── .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 |
2 |
3 | App1 is working!
4 | comment me on and off to see HMR
5 |
6 |
7 |
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 |
2 |
3 |
{{ msg }}
4 |
5 | For a guide and recipes on how to configure / customize this project,
6 | check out the
7 | vue-cli documentation.
8 |
9 |
Installed CLI Plugins
10 |
13 |
Essential Links
14 |
21 |
Ecosystem
22 |
29 |
30 |
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 |
2 |
3 |
4 | teesss
5 |
6 |
App2's about page
7 |
8 |
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 |
2 |
3 | App 2 is working
4 |
comment me on and off to see HMR
5 |
6 |
7 |
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 |
2 |
3 | Go to app2 about page
4 |
5 |
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 |
2 |
3 |
This is an about page
4 |
5 |
6 |
--------------------------------------------------------------------------------
/packages/app2/src/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
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 |
2 |
3 |
4 | App1 |
5 | App2
6 |
7 |
8 |
9 |
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 | })
--------------------------------------------------------------------------------