微前端背景
对于公司内部管理系统、ToB的SaaS系统等一系列的项目中,由于项目本身的生命周期较长,又长期不断的迭代,加入新的功能模块,最终会导致项目本身的体积越来越大,结构越来越复杂。不但影响了日后的维护成本、性能等方面,而且对于开发来讲,也是越写越写不下去,最后的建议可能就是 Emm... 你懂的。
微前端的概念很早就有了,比如说早期的iframe。但对于现在前端的技术的不断发展出现很多优秀的微前端解决方案,比如single-spa、蚂蚁的qiankun。有兴趣可以了解下。
实现方案
本文所介绍的微前端是基于Vue的路由预加载的方式实现的,具体架构及流程参考下图:
具体实现
子项目
通过Vue全家桶来构建子项目
- 规范子项目命名、端口号,在后续的代理、路由命名都以子模块的名字为准
// package.json
{
"name": "mobile-wechat",
"devPort": 10011,
}
- 调整目录结构,由于子项目依赖于主项目,所以可以将pubic文件夹删掉,最终大致目录结构如下
├── src
│ ├── views
│ ├── router
│ ├── main.js
├── babel.config.js
├── vue.config.js
├── .env.dev
├── .env.production
├── .env.test
- 调整之后配置config文件
// vue.config.js
const APP_NAME = require('./package.json').name; // 定义项目名
const PORT = require('./package.json').devPort;
module.exports = {
publicPath: `/${APP_NAME}/`,
chainWebpack: (config) => {
config.externals({
'vue': 'Vue'
})
config.output
.filename('main.js')
.chunkFilename('[name].[chunkhash:8].js')
.jsonpFunction(`webpackJsonp-${APP_NAME}`)
.library(`app-${APP_NAME}`)
.libraryExport('default')
.libraryTarget('umd')
config.plugin('define').use(webpack.DefinePlugin, [{
'process.env.VUE_APP_NAME': JSON.stringify(APP_NAME)
}])
config.plugins
.delete('html')
.delete('preload')
.delete('prefetch')
},
devServer: {
port: PORT,
},
};
- 配置好config后,在入口文件中,将定义的路由表挂载到Vue上,并将路由整理成以下格式,方便管理。
import Vue from 'vue'
import router from './router'
// 定义__shareRouter__属性用来存放路由信息
const shareRouter = (Vue.__shareRouter__ = Vue.__shareRouter__ || {})
// 将router定义到属性上
shareRouter[process.env.VUE_APP_NAME] = router
最后启动项目
主项目
- 首先通过webpack将子项目的入口文件打包到主项目中并且在主项目入口文件之前,可以通过
insert-script-webpack-plugin
插件来实现
// webpack.config.base.js
const InsertScriptWebpackPlugin = require('insert-script-webpack-plugin');
module.exports = {
// ...
plugins: [
// ...
new InsertScriptWebpackPlugin({
paths: ['/mobile-crm/main.js', '/mobile-wechat/main.js']
}),
]
}
- 开发环境配置代理,生产环境可以通过Nginx实现代理,加载子项目的入口文件
// webpack.config.dev.js
const PROXY = {
'/mobile-crm/': {
target: 'http://127.0.0.1:10011/'
},
'/mobile-wechat/': {
target: 'http://127.0.0.1:10012/'
}
}
- 在主项目的路由文件中拦截子项目路由,并添加到主项目路由表中
import Vue from 'vue';
import Router from 'vue-router';
import routes from './routes/index';
Vue.use(Router);
const routes_crm = Vue.__shareRouter__
routes = Object.values(routes_crm).reduce((acc, prev) =>{
// 将子项目路由注册到 主项目router-view中
const b = {
path: '/',
component: () => import('@/views/index.vue'),
children: [...prev]
}
return acc.concat(b)
}, routes)
const router = new Router({
routes,
});
启动项目可以看到,主项目已经可以加载子项目的main.js文件了。
注意的问题
- 要确保资源加载顺序,需要优先加载共同依赖的资源->子模块->父模块。
- 需要保证同一前端框架。
- 如果
router
的模式为history
,需要服务端配置,将代理资源文件类型限制在js/css/image
等类型,而非html
。并设置不能缓存入口文件,具体配置如下:
location ~ \/mobile-crm\/(.+\.js)$ {
if ( $request_uri = /mobile-crm/main.js ){
add_header Cache-Control no-cache;
}
}