├── applications ├── app-entry │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── views │ │ │ └── .gitkeep │ │ ├── components │ │ │ └── .gitkeep │ │ ├── modules.js │ │ ├── store.js │ │ ├── load-helper.js │ │ ├── main.js │ │ ├── App.vue │ │ └── router.js │ ├── public │ │ ├── favicon.ico │ │ └── index.html │ ├── .gitignore │ ├── README.md │ ├── package.json │ └── vue.config.js ├── app-typescript │ ├── src │ │ ├── shims-vue.d.ts │ │ ├── views │ │ │ ├── About.vue │ │ │ └── Home.vue │ │ ├── assets │ │ │ └── logo.png │ │ ├── global.d.ts │ │ ├── base.ts │ │ ├── store.ts │ │ ├── main.ts │ │ ├── shims-tsx.d.ts │ │ ├── routes.ts │ │ ├── App.vue │ │ └── components │ │ │ └── HelloWorld.vue │ ├── .gitignore │ ├── README.md │ ├── tsconfig.json │ ├── package.json │ └── vue.config.js └── app-javascript │ ├── src │ ├── views │ │ ├── About.vue │ │ └── Home.vue │ ├── assets │ │ └── logo.png │ ├── base.js │ ├── store.js │ ├── main.js │ ├── routes.js │ ├── App.vue │ └── components │ │ └── HelloWorld.vue │ ├── .gitignore │ ├── README.md │ ├── package.json │ └── vue.config.js ├── .prettierrc ├── .gitignore ├── lerna.json ├── .travis.yml ├── package.json ├── move.js ├── README.md └── detail.md /applications/app-entry/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/app-entry/src/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/app-entry/src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /applications/app-typescript/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /applications/app-entry/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micro-frontends-vue/async-routes/HEAD/applications/app-entry/public/favicon.ico -------------------------------------------------------------------------------- /applications/app-javascript/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /applications/app-typescript/src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /applications/app-javascript/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micro-frontends-vue/async-routes/HEAD/applications/app-javascript/src/assets/logo.png -------------------------------------------------------------------------------- /applications/app-typescript/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/micro-frontends-vue/async-routes/HEAD/applications/app-typescript/src/assets/logo.png -------------------------------------------------------------------------------- /applications/app-entry/src/modules.js: -------------------------------------------------------------------------------- 1 | export const modules = { 2 | 'app-typescript': './app-typescript/main.js', 3 | 'app-javascript': './app-javascript/main.js', 4 | }; 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "semi": true, 5 | "trailingComma": "all", 6 | "bracketSpacing": true, 7 | "arrowParens": "always" 8 | } 9 | -------------------------------------------------------------------------------- /applications/app-typescript/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | declare module 'vue/types/vue' { 4 | interface VueConstructor { 5 | __share_pool__: any; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /applications/app-javascript/src/base.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import store from './store'; 3 | 4 | // 子项目异步注册 store module 5 | Vue.__share_pool__.store.registerModule(process.env.VUE_APP_NAME, store); 6 | 7 | export default null; 8 | -------------------------------------------------------------------------------- /applications/app-typescript/src/base.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import store from './store'; 3 | 4 | // 子项目异步注册 store module 5 | Vue.__share_pool__.store.registerModule(process.env.VUE_APP_NAME, store); 6 | 7 | export default null; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | docs 4 | 5 | # Log files 6 | *.log 7 | 8 | # lock files 9 | yarn.lock 10 | 11 | # Editor directories and files 12 | .idea 13 | .vscode 14 | *.suo 15 | *.ntvs* 16 | *.njsproj 17 | *.sln 18 | *.sw* 19 | -------------------------------------------------------------------------------- /applications/app-entry/src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | Vue.use(Vuex); 5 | 6 | export default new Vuex.Store({ 7 | state: { 8 | name: 'entry-application', 9 | }, 10 | mutations: {}, 11 | actions: {}, 12 | }); 13 | -------------------------------------------------------------------------------- /applications/app-javascript/src/store.js: -------------------------------------------------------------------------------- 1 | /* store module */ 2 | 3 | export default { 4 | namespaced: true, // namespaced must be true in module app. 5 | state: { 6 | name: process.env.VUE_APP_NAME, 7 | }, 8 | mutations: {}, 9 | actions: {}, 10 | }; 11 | -------------------------------------------------------------------------------- /applications/app-typescript/src/store.ts: -------------------------------------------------------------------------------- 1 | /* store module */ 2 | 3 | export default { 4 | namespaced: true, // namespaced must be true in module app. 5 | state: { 6 | name: process.env.VUE_APP_NAME, 7 | }, 8 | mutations: {}, 9 | actions: {}, 10 | }; 11 | -------------------------------------------------------------------------------- /applications/app-javascript/src/main.js: -------------------------------------------------------------------------------- 1 | import routes from './routes'; 2 | 3 | export default { 4 | name: 'javascript', 5 | routes, 6 | beforeEach(from, to, next) { 7 | console.log('javascript:', from.path, to.path); 8 | next(); 9 | }, 10 | init() { }, 11 | } 12 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "packages": [ 4 | "applications/*" 5 | ], 6 | "command": { 7 | "bootstrap": { 8 | "hoist": true, 9 | "ignoreScripts": ["prepublish", "prepare"] 10 | }, 11 | "clean": { 12 | "yes": true 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /applications/app-typescript/src/main.ts: -------------------------------------------------------------------------------- 1 | import routes from './routes'; 2 | 3 | export default { 4 | name: 'typescript', 5 | routes, 6 | beforeEach(from: any, to: any, next: any) { 7 | console.log('typescript:', from.path, to.path); 8 | next(); 9 | }, 10 | init() { }, 11 | } 12 | -------------------------------------------------------------------------------- /applications/app-entry/.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 | # lock files 15 | yarn.lock 16 | package-lock.json 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw* 26 | -------------------------------------------------------------------------------- /applications/app-typescript/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /applications/app-javascript/.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 | # lock files 15 | yarn.lock 16 | package-lock.json 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw* 26 | -------------------------------------------------------------------------------- /applications/app-typescript/.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 | # lock files 15 | yarn.lock 16 | package-lock.json 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw* 26 | -------------------------------------------------------------------------------- /applications/app-javascript/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | notifications: 4 | email: false 5 | 6 | node_js: 7 | - "10" 8 | 9 | script: 10 | - npm run bootstrap && npm run build 11 | 12 | branches: 13 | only: 14 | - master 15 | 16 | deploy: 17 | provider: pages 18 | local-dir: docs 19 | skip-cleanup: true 20 | github-token: $CI_TOKEN # Set in the settings page of your repository, as a secure variable 21 | keep-history: true 22 | on: 23 | branch: master 24 | -------------------------------------------------------------------------------- /applications/app-entry/src/load-helper.js: -------------------------------------------------------------------------------- 1 | export function loadModule(url) { 2 | return new Promise((resolve) => { 3 | const script = document.createElement('script'); 4 | script.type = 'text/javascript'; 5 | // script.async = true; 6 | script.onload = ({ type }) => resolve({ status: type, url }); 7 | script.onerror = ({ type }) => resolve({ status: type, url }); 8 | script.src = url; 9 | document.body.appendChild(script); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "bootstrap": "npm install && lerna bootstrap", 6 | "serve": "lerna run serve --parallel", 7 | "build-package": "lerna run build --parallel", 8 | "build": "npm run build-package && npm run move", 9 | "move": "node move.js", 10 | "clean": "lerna clean && rm -rf node_modules" 11 | }, 12 | "devDependencies": { 13 | "fs-extra": "^8.1.0", 14 | "lerna": "^3.19.0" 15 | }, 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /applications/app-entry/README.md: -------------------------------------------------------------------------------- 1 | # app-entry 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /applications/app-javascript/README.md: -------------------------------------------------------------------------------- 1 | # app-javascript 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /applications/app-typescript/README.md: -------------------------------------------------------------------------------- 1 | # app-typescript 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn run build 16 | ``` 17 | 18 | ### Run your tests 19 | ``` 20 | yarn run test 21 | ``` 22 | 23 | ### Lints and fixes files 24 | ``` 25 | yarn run lint 26 | ``` 27 | 28 | ### Customize configuration 29 | See [Configuration Reference](https://cli.vuejs.org/config/). 30 | -------------------------------------------------------------------------------- /applications/app-typescript/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 19 | -------------------------------------------------------------------------------- /applications/app-entry/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueProgressBar from 'vue-progressbar' 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import store from './store'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | Vue.use(VueProgressBar) 10 | 11 | // 挂载主项目的 store 和 router 实例 12 | Reflect.defineProperty(Vue, '__share_pool__', { 13 | value: { 14 | store, 15 | router, 16 | }, 17 | }); 18 | 19 | new Vue({ 20 | router, 21 | store, 22 | render: function(h) { 23 | return h(App); 24 | }, 25 | }).$mount('#app'); 26 | -------------------------------------------------------------------------------- /move.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | 4 | const tasks = [ 5 | { 6 | src: 'applications/app-entry/dist', 7 | dest: 'docs', 8 | }, 9 | { 10 | src: 'applications/app-typescript/dist', 11 | dest: 'docs/app-typescript', 12 | }, 13 | { 14 | src: 'applications/app-javascript/dist', 15 | dest: 'docs/app-javascript', 16 | }, 17 | ] 18 | 19 | fs.removeSync(path.resolve(__dirname, 'dist')) 20 | 21 | for (const { src, dest } of tasks) { 22 | fs.moveSync(path.resolve(__dirname, src), path.resolve(__dirname, dest)) 23 | console.log('\x1b[1m%s\x1b[31m%s\x1b[0m', '[Copy]: ', `src: ${src}, dest: ${dest}`, 'success.') 24 | } 25 | -------------------------------------------------------------------------------- /applications/app-javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-javascript", 3 | "devPort": 10242, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build --report" 8 | }, 9 | "dependencies": { 10 | "vue": "^2.6.10", 11 | "vue-router": "^3.1.3", 12 | "vuex": "^3.1.2" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-service": "^4.1.0", 16 | "vue-template-compiler": "^2.6.10" 17 | }, 18 | "postcss": { 19 | "plugins": { 20 | "autoprefixer": {} 21 | } 22 | }, 23 | "browserslist": [ 24 | "> 1%", 25 | "last 2 versions", 26 | "not ie <= 8" 27 | ], 28 | "license": "MIT" 29 | } -------------------------------------------------------------------------------- /applications/app-javascript/src/routes.js: -------------------------------------------------------------------------------- 1 | /* routes-list */ 2 | 3 | const APP_NAME = process.env.VUE_APP_NAME; 4 | 5 | const App = () => import('./App.vue'); 6 | const Home = () => import('./views/Home.vue'); 7 | const About = () => import('./views/About.vue'); 8 | 9 | export default [ 10 | { 11 | path: `/${APP_NAME}`, 12 | name: APP_NAME, 13 | redirect: { name: `${APP_NAME}.home` }, 14 | component: App, 15 | children: [ 16 | { 17 | path: 'home', 18 | name: `${APP_NAME}.home`, 19 | component: Home, 20 | }, 21 | { 22 | path: 'about', 23 | name: `${APP_NAME}.about`, 24 | component: About, 25 | }, 26 | ], 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /applications/app-typescript/src/routes.ts: -------------------------------------------------------------------------------- 1 | /* routes-list */ 2 | 3 | const APP_NAME = process.env.VUE_APP_NAME; 4 | 5 | const App = () => import('./App.vue'); 6 | const Home = () => import('./views/Home.vue'); 7 | const About = () => import('./views/About.vue'); 8 | 9 | export default [ 10 | { 11 | path: `/${APP_NAME}`, 12 | name: APP_NAME, 13 | redirect: { name: `${APP_NAME}.home` }, 14 | component: App, 15 | children: [ 16 | { 17 | path: 'home', 18 | name: `${APP_NAME}.home`, 19 | component: Home, 20 | }, 21 | { 22 | path: 'about', 23 | name: `${APP_NAME}.about`, 24 | component: About, 25 | }, 26 | ], 27 | }, 28 | ]; 29 | -------------------------------------------------------------------------------- /applications/app-entry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-entry", 3 | "devPort": 10240, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build --report" 8 | }, 9 | "dependencies": { 10 | "vue": "^2.6.10", 11 | "vue-progressbar": "^0.7.5", 12 | "vue-router": "^3.1.3", 13 | "vuex": "^3.1.2" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-service": "^4.1.0", 17 | "vue-template-compiler": "^2.6.10" 18 | }, 19 | "postcss": { 20 | "plugins": { 21 | "autoprefixer": {} 22 | } 23 | }, 24 | "browserslist": [ 25 | "> 1%", 26 | "last 2 versions", 27 | "not ie <= 8" 28 | ], 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 异步加载子项目路由 2 | 3 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lernajs.io/) 4 | 5 | 灵感来自于 [用微前端的方式搭建类单页应用](https://tech.meituan.com/fe_tiny_spa.html)(@[美团技术团队](https://tech.meituan.com)) 6 | 7 | ## 关联项目 8 | 9 | - 预加载子项目路由 -- [传送门](https://github.com/micro-frontends-vue/preload-routes) 10 | 11 | ## 设计目标 12 | 13 | 1. 子项目支持单独开发,单独部署(避免前端巨无霸,多团队同时参与) 14 | 2. 单一的入口 HTML(不同项目之间切换时无白屏现象) 15 | 3. 支持多语言开发(JavaScript、TypeScript) 16 | 17 | ## 使用 18 | 19 | [详细教程](./detail.md) 20 | 21 | ### 开发 22 | 23 | ```bash 24 | # 安装依赖 25 | npm run bootstrap 26 | 27 | # 开发模式 28 | npm run serve 29 | 30 | # 打开: http://localhost:10240/ 31 | ``` 32 | 33 | ### 构建 34 | 35 | ```bash 36 | # 生产构建 37 | npm run build 38 | ``` 39 | -------------------------------------------------------------------------------- /applications/app-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env"], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | "src/**/*.vue", 24 | "tests/**/*.ts", 25 | "tests/**/*.tsx" 26 | ], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /applications/app-javascript/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 18 | 39 | -------------------------------------------------------------------------------- /applications/app-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app-typescript", 3 | "devPort": 10241, 4 | "version": "0.1.0", 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build --report" 8 | }, 9 | "dependencies": { 10 | "vue": "^2.6.10", 11 | "vue-class-component": "^6.0.0", 12 | "vue-property-decorator": "^7.0.0", 13 | "vue-router": "^3.1.3", 14 | "vuex": "^3.1.2" 15 | }, 16 | "devDependencies": { 17 | "@vue/cli-plugin-typescript": "^3.3.0", 18 | "@vue/cli-service": "^4.1.0", 19 | "typescript": "^3.0.0", 20 | "vue-template-compiler": "^2.6.10" 21 | }, 22 | "postcss": { 23 | "plugins": { 24 | "autoprefixer": {} 25 | } 26 | }, 27 | "browserslist": [ 28 | "> 1%", 29 | "last 2 versions", 30 | "not ie <= 8" 31 | ], 32 | "license": "MIT" 33 | } 34 | -------------------------------------------------------------------------------- /applications/app-typescript/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | 41 | -------------------------------------------------------------------------------- /applications/app-entry/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | app-entry 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /applications/app-entry/vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const APP_NAME = require('./package.json').name 3 | const PORT = require('./package.json').devPort 4 | 5 | const PROXY = { 6 | '/app-typescript/': { 7 | target: 'http://localhost:10241/' 8 | }, 9 | '/app-javascript/': { 10 | target: 'http://localhost:10242/' 11 | } 12 | } 13 | 14 | const NODE_ENV = process.env.NODE_ENV || 'development' 15 | 16 | log('APP_NAME: ', APP_NAME) 17 | log('NODE_ENV: ', NODE_ENV) 18 | 19 | module.exports = { 20 | publicPath: './', 21 | 22 | productionSourceMap: false, 23 | 24 | configureWebpack: { 25 | externals: { 26 | vue: 'Vue' 27 | }, 28 | 29 | plugins: [ 30 | new webpack.DefinePlugin({ 31 | 'process.env.VUE_APP_NAME': JSON.stringify(APP_NAME) 32 | }) 33 | ] 34 | }, 35 | 36 | devServer: { 37 | port: PORT, 38 | proxy: PROXY 39 | } 40 | } 41 | 42 | function log(label, content, options) { 43 | console.log('\x1b[1m%s\x1b[31m%s\x1b[0m', label, content) 44 | } 45 | -------------------------------------------------------------------------------- /applications/app-javascript/vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const APP_NAME = require('./package.json').name; 3 | const PORT = require('./package.json').devPort; 4 | const NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | log('APP_NAME: ', APP_NAME); 7 | log('NODE_ENV: ', NODE_ENV); 8 | 9 | module.exports = { 10 | publicPath: `${NODE_ENV === 'development' ? '' : '.'}/${APP_NAME}/`, 11 | 12 | css: { 13 | extract: false 14 | }, 15 | 16 | productionSourceMap: false, 17 | 18 | chainWebpack: (config) => { 19 | config.externals({ 20 | 'vue': 'Vue' 21 | }) 22 | 23 | config.output 24 | .filename('main.js') 25 | .chunkFilename('[name].[chunkhash:8].js') 26 | .jsonpFunction(`webpackJsonp-${APP_NAME}`) 27 | .library(`app-${APP_NAME}`) 28 | .libraryExport('default') 29 | .libraryTarget('umd') 30 | 31 | config.optimization.splitChunks(false) 32 | 33 | config.plugin('define').use(webpack.DefinePlugin, [{ 34 | 'process.env.VUE_APP_NAME': JSON.stringify(APP_NAME) 35 | }]) 36 | 37 | config.plugins 38 | .delete('html') 39 | .delete('preload') 40 | .delete('prefetch') 41 | }, 42 | 43 | devServer: { 44 | port: PORT, 45 | }, 46 | }; 47 | 48 | function log(label, content, options) { 49 | console.log('\x1b[1m%s\x1b[31m%s\x1b[0m', label, content); 50 | } 51 | -------------------------------------------------------------------------------- /applications/app-typescript/vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const APP_NAME = require('./package.json').name; 3 | const PORT = require('./package.json').devPort; 4 | const NODE_ENV = process.env.NODE_ENV || 'development'; 5 | 6 | log('APP_NAME: ', APP_NAME); 7 | log('NODE_ENV: ', NODE_ENV); 8 | 9 | module.exports = { 10 | publicPath: `${NODE_ENV === 'development' ? '' : '.'}/${APP_NAME}/`, 11 | 12 | css: { 13 | extract: false 14 | }, 15 | 16 | productionSourceMap: false, 17 | 18 | chainWebpack: (config) => { 19 | config.externals({ 20 | 'vue': 'Vue' 21 | }) 22 | 23 | config.output 24 | .filename('main.js') 25 | .chunkFilename('[name].[chunkhash:8].js') 26 | .jsonpFunction(`webpackJsonp-${APP_NAME}`) 27 | .library(`app-${APP_NAME}`) 28 | .libraryExport('default') 29 | .libraryTarget('umd') 30 | 31 | config.optimization.splitChunks(false) 32 | 33 | config.plugin('define').use(webpack.DefinePlugin, [{ 34 | 'process.env.VUE_APP_NAME': JSON.stringify(APP_NAME) 35 | }]) 36 | 37 | config.plugins 38 | .delete('html') 39 | .delete('preload') 40 | .delete('prefetch') 41 | }, 42 | 43 | devServer: { 44 | port: PORT, 45 | }, 46 | }; 47 | 48 | function log(label, content, options) { 49 | console.log('\x1b[1m%s\x1b[31m%s\x1b[0m', label, content); 50 | } 51 | -------------------------------------------------------------------------------- /applications/app-entry/src/App.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 52 | 53 | 77 | -------------------------------------------------------------------------------- /applications/app-entry/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import { modules } from './modules'; 4 | 5 | Vue.use(Router); 6 | 7 | const router = new Router({ 8 | routes: [ 9 | { 10 | path: '/', 11 | redirect: '/app-typescript', 12 | }, 13 | { 14 | path: '/404', 15 | component: { 16 | name: 'not-found', 17 | template: '

Not Found

', 18 | } 19 | }, 20 | ], 21 | }); 22 | 23 | const cachedModules = new Set(); 24 | 25 | const sleep = (time) => new Promise((resolve) => void setTimeout(resolve, time)); 26 | 27 | router.beforeEach(async (to, from, next) => { 28 | 29 | console.log('entry:', to.path, from.path); 30 | const [, module] = to.path.split('/'); 31 | 32 | if (Reflect.has(modules, module)) { 33 | if (!cachedModules.has(module)) { 34 | Vue.prototype.$Progress.start(); 35 | 36 | const { default: application } = await window.System.import(modules[module]) 37 | console.log('load application:', application); 38 | 39 | if (application && application.routes) { 40 | // 动态添加子项目的 route-list 41 | router.addRoutes(application.routes); 42 | } 43 | 44 | if (application && application.beforeEach) { 45 | router.beforeEach((to, from, next) => { 46 | if (module === to.path.split('/')[1]) { 47 | application.beforeEach(to, from, next); 48 | } else { 49 | next(); 50 | } 51 | }) 52 | } 53 | 54 | if (application && application.init) { 55 | await application.init({}); 56 | } 57 | 58 | await sleep(300); // 模拟延迟 59 | cachedModules.add(module); 60 | Vue.prototype.$Progress.finish(); 61 | next(to.path); 62 | } else { 63 | next(); 64 | } 65 | return; 66 | } 67 | 68 | if (to.matched.length) { 69 | next(); 70 | } else { 71 | next('/404'); 72 | } 73 | }); 74 | 75 | export default router; 76 | -------------------------------------------------------------------------------- /applications/app-javascript/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 80 | 81 | 82 | 98 | -------------------------------------------------------------------------------- /applications/app-typescript/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 80 | 81 | 89 | 90 | 91 | 107 | -------------------------------------------------------------------------------- /detail.md: -------------------------------------------------------------------------------- 1 | # 懒加载子项目 2 | 3 | ## 思路 4 | 5 | 1. 将子项目打包 UMD 模块,[教程](https://cli.vuejs.org/zh/guide/build-targets.html#库) 6 | 2. 在主项目中加载子项目的入口文件 7 | 3. 使用 vue-router 的 [`router.addRoutes`](https://github.com/zh-rocco/fe-notes/issues/29) 将子项目的路由动态注册到主项目中 8 | 4. 使用 Vuex 的 [`store.registerModule`](https://github.com/zh-rocco/fe-notes/issues/31) 将子项目的 `store module` 动态注册到主项目中 9 | 10 | ## 具体实现 11 | 12 | ### 一、主项目(app-entry) 13 | 14 | #### 1. 配置 `vue.config.js` 15 | 16 | `app-entry/vue.config.js` 17 | 18 | ```js 19 | const webpack = require('webpack'); 20 | const APP_NAME = require('./package.json').name; 21 | const PORT = require('./package.json').devPort; // 开发模式下项目的启动端口 22 | const PROXY = require('./config/proxy'); // 开发模式下的 proxy 配置 23 | 24 | module.exports = { 25 | baseUrl: './', 26 | configureWebpack: { 27 | // 提取公共依赖 28 | externals: { 29 | vue: 'Vue', 30 | 'element-ui': 'ELEMENT', 31 | }, 32 | plugins: [ 33 | // 定义全局变量 34 | new webpack.DefinePlugin({ 35 | 'process.env.VUE_APP_NAME': JSON.stringify(APP_NAME), 36 | }), 37 | ], 38 | }, 39 | devServer: { 40 | port: PORT, 41 | proxy: PROXY, 42 | }, 43 | }; 44 | ``` 45 | 46 | #### 2. 手动引入外部依赖 47 | 48 | `app-entry/public/index.html` 49 | 50 | ```html 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ``` 60 | 61 | #### 3. 配置开发代理 62 | 63 | `app-entry/config/proxy.js` 64 | 65 | ```js 66 | module.exports = { 67 | '/app-typescript/': { 68 | target: 'http://localhost:10241/', // 指向 app-typescript 开发服务 69 | }, 70 | 'app-javascript/': { 71 | target: 'http://localhost:10242/', // 指向 app-javascript 开发服务 72 | }, 73 | }; 74 | ``` 75 | 76 | #### 4. 将主项目的 router 实例和 store 实例挂载到 `Vue['__share_pool__']` 上 77 | 78 | `app-entry/src/main.js` 79 | 80 | ```js 81 | import router from './router'; // router 实例 82 | import store from './store'; // store 实例 83 | 84 | // 挂载主项目的 store 和 router 实例 85 | Reflect.defineProperty(Vue, '__share_pool__', { 86 | value: { 87 | store, 88 | router, 89 | }, 90 | }); 91 | ``` 92 | 93 | #### 5. 异步加载子项目 94 | 95 | `app-entry/src/router.js` 96 | 97 | ```js 98 | import { loadModule } from './load-helper'; 99 | import { modules } from './modules'; 100 | 101 | router.beforeEach(async (to, from, next) => { 102 | const [, module] = to.path.split('/'); 103 | 104 | if (Reflect.has(modules, module)) { 105 | loadModule(modules[module]); 106 | Reflect.deleteProperty(modules, module); 107 | } 108 | 109 | next(); 110 | }); 111 | ``` 112 | 113 | `app-entry/src/modules.js` 114 | 115 | ```js 116 | export const modules = { 117 | 'app-typescript': './app-typescript/main.js', 118 | 'app-javascript': './app-javascript/main.js', 119 | }; 120 | ``` 121 | 122 | `app-entry/src/load-helper.js` 123 | 124 | ```js 125 | export function loadModule(url) { 126 | return new Promise((resolve) => { 127 | const script = document.createElement('script'); 128 | script.type = 'text/javascript'; 129 | script.async = true; 130 | script.onload = ({ type }) => resolve({ status: type, url }); 131 | script.onerror = ({ type }) => resolve({ status: type, url }); 132 | script.src = url; 133 | document.body.appendChild(script); 134 | }); 135 | } 136 | ``` 137 | 138 | ### 二、子项目(app-javascript) 139 | 140 | 以 app-javascript 为例 141 | 142 | #### 1. 添加 mfv-cli-service 依赖 143 | 144 | ```bash 145 | yarn add mfv-cli-service -D 146 | ``` 147 | 148 | #### 2. 修改 build script 149 | 150 | `app-javascript/package.json` 151 | 152 | ```json 153 | { 154 | "scripts": { 155 | "build": "mfv-cli-service build --report --target lib --formats umd-min ./src/main.js" 156 | } 157 | } 158 | ``` 159 | 160 | 这样可以将 app-javascript 构建成一个 UMD 文件,然后在 app-entry 中引用,[参考](https://cli.vuejs.org/zh/guide/build-targets.html#库) 161 | 162 | #### 3. 配置 `vue.config.js` 163 | 164 | `app-javascript/vue.config.js` 165 | 166 | ```js 167 | const webpack = require('webpack'); 168 | const APP_NAME = require('./package.json').name; 169 | const PORT = require('./package.json').devPort; // 开发模式下项目的启动端口 170 | 171 | module.exports = { 172 | publicPath: `/${APP_NAME}/`, // 必须为绝对路径;配合 app-entry 中的 proxy 配置,配合生产环境下的 Nginx 配置 173 | configureWebpack: { 174 | // 提取公共依赖 175 | externals: { 176 | vue: 'Vue', 177 | 'element-ui': 'ELEMENT', 178 | }, 179 | entry: './src/main.js', 180 | output: { 181 | libraryExport: 'default', // https://cli.vuejs.org/zh/guide/build-targets.html#应用 182 | jsonpFunction: `webpackJsonp-${APP_NAME}`, // 解决默认情况下子项目 chunkname 冲突的问题 183 | }, 184 | plugins: [ 185 | // 定义全局变量,项目中会使用 186 | new webpack.DefinePlugin({ 187 | 'process.env.VUE_APP_NAME': JSON.stringify(APP_NAME), 188 | }), 189 | ], 190 | }, 191 | 192 | devServer: { 193 | port: PORT, 194 | }, 195 | }; 196 | ``` 197 | 198 | #### 4. 修改入口文件,异步挂载 routes 199 | 200 | `app-javascript/src/main.js` 201 | 202 | ```js 203 | import Vue from 'vue'; 204 | import routes from './routes'; 205 | 206 | // 动态添加子项目的 route-list 207 | Vue.__share_pool__.router.addRoutes(routes); 208 | ``` 209 | 210 | `app-javascript/src/routes.js` 211 | 212 | ```js 213 | /* routes-list */ 214 | 215 | const APP_NAME = process.env.VUE_APP_NAME; 216 | 217 | const App = () => import('./App.vue'); 218 | const Home = () => import('./views/Home.vue'); 219 | const About = () => import('./views/About.vue'); 220 | 221 | export default [ 222 | { 223 | path: `/${APP_NAME}`, 224 | name: APP_NAME, 225 | redirect: { name: `${APP_NAME}.home` }, 226 | component: App, 227 | children: [ 228 | { 229 | path: 'home', 230 | name: `${APP_NAME}.home`, 231 | component: Home, 232 | }, 233 | { 234 | path: 'about', 235 | name: `${APP_NAME}.about`, 236 | component: About, 237 | }, 238 | ], 239 | }, 240 | ]; 241 | ``` 242 | 243 | #### 5. 异步挂载 store module 244 | 245 | `app-javascript/src/base.js` 246 | 247 | ```js 248 | import Vue from 'vue'; 249 | import store from './store'; 250 | 251 | // 子项目异步注册 store module 252 | Vue.__share_pool__.store.registerModule(process.env.VUE_APP_NAME, store); 253 | 254 | export default null; 255 | ``` 256 | 257 | `app-javascript/src/store.js` 258 | 259 | ```js 260 | /* store module */ 261 | 262 | export default { 263 | namespaced: true, // namespaced must be true in module app. 264 | state: { 265 | name: process.env.VUE_APP_NAME, 266 | }, 267 | mutations: {}, 268 | actions: {}, 269 | }; 270 | ``` 271 | 272 | ## 关于部署 273 | 274 | - 方式一:通过 CI 合并主项目和子项目的 dist 目录,然后部署 275 | - 方式二:将子项目当成单独服务对待,独立部署,然后通过 Nginx 反向代理合并主项目和子项目 276 | 277 | ## 不足 & 待解决的问题 278 | 279 | ### 1. 鉴权问题 280 | 281 | 由于子项目的路由是异步加载 / 注册的,导致子项目无法使用 `meta` 判断路由是否需要鉴权 282 | 283 | **优化方向: 在主项目实例化之前加载子项目的路由**,参考:[预加载子项目入口文件](https://github.com/micro-frontends-vue/preload-routes) 284 | 285 | ### 2. 子项目缓存问题 286 | 287 | 推荐在服务端为子项目入口文件添加协商缓存。 288 | 289 | ## 参考 290 | 291 | - [微前端的那些事儿](https://github.com/phodal/microfrontends) 292 | - [用微前端的方式搭建类单页应用](https://tech.meituan.com/fe_tiny_spa.html) 293 | - [微前端 - 将微服务理念延伸到前端开发中](http://zzfe.org/#/detail/59dc6e1f91d3e35ba880fd0d) 294 | - [Micro Frontends](https://micro-frontends.org/) ([Github](https://github.com/neuland/micro-frontends)) 295 | - [微前端 - 将微服务理念扩展到前端开发(实战篇)](http://insights.thoughtworkers.org/micro-frontends-2/) 296 | - [Medium: micro frontends](https://medium.com/search?q=micro%20frontends) 297 | - [single-spa: 在一个页面将多个不同的框架整合](https://github.com/CanopyTax/single-spa) 298 | - [Ueact: 渐进式,多调和策略,多渲染终端的乐高式微前端框架,以及可复用的多框架碎片化组件](https://github.com/wxyyxc1992/Ueact) 299 | - [Web Components](https://developer.mozilla.org/zh-CN/docs/Web/Web_Components) 300 | - [微前端: Alili](https://alili.tech/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF/) 301 | --------------------------------------------------------------------------------