模块联邦
解决微前端架构模块共享问题
SystemJS,webpack5
systemJs模块加载器,惰性,
- 加载模块:SystemJS模块或者UMD模块
- 依赖关系:运行时确定,importMap
- 支持webpack4/5
webpack5模块联邦
- 对模块没有限制
- 依赖关系:编译时确定,remoteEntry
仅支持webpack5
//basecore //remotes部分直接引入无法加载,原因不明确 const remoteComponents = require('./plugins/remote-components.js') module.exports = { publicPath:'auto', outputDir, lintOnSave: true, devServer: { // publicPath, // 和 publicPath 保持一致 port, // 端口 hot:true, allowedHosts: 'auto',// 关闭 host check,方便使用 ngrok 之类的内网转发工具 headers: { 'Access-Control-Allow-Origin': '*' }, historyApiFallback: { // 将以 xxx 开头的请求重定向到根路径 rewrites: [{ from: new RegExp(`^\/child-core`), to: '/' }], }, client: { overlay: false }, }, configureWebpack: config => { const configNew = { plugins: [], resolve: { fallback: { util: require.resolve('util/'), assert: require.resolve('assert/'), timers: require.resolve('timers-browserify') } } } configNew.plugins.push( new webpack.container.ModuleFederationPlugin({ name: 'baseCore', library: { type: "umd", name: "baseCore" }, filename: 'remoteEntry.js', shared: remoteComponents.shared, exposes: remoteComponents.exposes, remotes:{ childApp: `promise new Promise(resolve => { const timeStamp = Date.now(); const remoteUrlWithTimeStamp = '${process.env.VUE_APP_CHILD_CORE_URL}remoteEntry.js?time=' + timeStamp; const script = document.createElement('script') script.src = remoteUrlWithTimeStamp script.onload = () => { // the injected script has loaded and is available on window // we can now resolve this Promise const proxy = { get: (request) => window.childApp.get(request), init: (arg) => { try { return window.childApp.init(arg) } catch(e) { console.log('remote container already initialized') } } } resolve(proxy) } // inject this script with the src set to the versioned remoteEntry.js document.head.appendChild(script); }) ` }, // shared: remoteComponents.shared, }) ) configNew.plugins.push(new SpeedMeasurePlugin()) return configNew }, } //childCore //该部分无法引入base 无法双向 原因不知道 configNew.plugins.push( new webpack.container.ModuleFederationPlugin({ name: 'childApp', library: { type: "umd", name: "childApp" }, remotes:{ }, filename: 'remoteEntry.js', exposes: remoteComponents.exposes, shared: remoteComponents.shared, }) )
webpack module federation
/**
ContainerPlugin,将exposes的模块,当成入口文件模块打包,形成单独可运行的文件,随主模块编译
**/
if(options.exposes...)new ContainerPlugin
/**
ContainerRefrencePlugin,多个不同container调用关系判断,实现模块通信和传递
**/
if(options.remotes)new ContainerRefrencePlugin
/**
**/
if(options.shared) new SharePlugin
micro-app与模块联邦结合
- 明确角色,micro-app作为基座应用负责加载和管理各类项目,模块联邦用于代码共享
- 配置模块联邦,项目中配置模块联邦
- 在作为基座的项目中集成micro-app
- 跨域
- 状态和上下文共享,本地存储,micro-app数据通信,模块联邦共享全局状态
//配置基座
//加载方式确保micro-app在项目加载初期加载完毕
//micro-app.js
import microApp from '@micro-zoe/micro-app'
microApp.start({
tagName: 'micro-app-core',
lifeCycles: {
mounted() {
// 设置全局数据
if (window.rawWindow) {
window.rawWindow.__CORE_CONFIG__ = window.__IBPS_CONFIG__
}
}
}
})
//bootstrap.js
import('./main')
//index.js
import('./bootstrap');
// micro-app不能异步加载,否则无法成功渲染
import "../plugins/micro-app";
//入口
index: {
entry: 'src/index.js',
template: 'public/index.html',
filename: 'index.html',
chunks: [...]}
为什么需要micro-app+webpack5模块联邦
- micro-app可以在spa中整合不同技术栈和框架的应用,可以作为容器进行加载和隔离,模块联邦可以实现应用间的共享
- micro-app运行每个应用独立部署更新,模块联邦允许共享依赖组件
为什么可以分开使用
都是微前端解决方案,如果不需要集成多个项目仅使用模块联邦可以满足
webpack
打包顺序
- webpack配置文件(webpack.config.js/vue.config.js)
- 输入执行 build
- options:解析shell和config.js配置项,激活webpack加载项和插件
- webpack(options):创建Compiler对象,初始化基础插件
- compiler:4-->5生成的compiler,全程唯一,启动webpack时一次性创建,完整的webpack配置:options,loader,plugins,以该对象访问webpack主环境
- compiler.run()
- compliation:6-->7生成的Compliation对象,检测到文件变化,都会生成新的compliation对象,包括当前模块资源,编译生成资源,变化文件,被追踪的依赖状态信息,属性:entries,modules(所有模块),chunks(代码块),assets,template
- compiler.make():生成compliation后执行
- compliation.addEntry()
- compliation.buildModule()
- ast分析,递归依赖
- compilation.seal():整理chunk,module,每个chunk对应一个入口文件
- complier.emitAssets()
[^无新的compliation生成时]: [1-5]-->6-->8--->13
[^10-11]: 调用loader处理源文件,使用acorn生成ast,遇require,push到数组,处理module后,开始异步递归处理依赖module
性能优化
- 入口起点优化,配置多入口
- 动态导入:es6 import()
split chunk
config.optimization.splitChunks({ cacheGroups: { // 所有页面共有的外部依赖关系 libs: { name: 'chunk-vendor', chunks: 'initial', minChunks: 1, test: /[\\/]node_modules[\\/]/, priority: 1, reuseExistingChunk: true, enforce: true }, // 对所有页面通用的代码 common: { name: 'chunk-common', chunks: 'initial', minChunks: 2, maxInitialRequests: 5, minSize: 0, priority: 2, reuseExistingChunk: true, enforce: true }, // 仅供首页使用的外部依赖项 index: { name: 'chunk-index', chunks: 'all', minChunks: 1, test: /[\\/]node_modules[\\/](sortablejs|screenfull|nprogress|hotkeys-js|fuse\.js|better-scroll|lowdb|xlsx|axios)[\\/]/, priority: 3, reuseExistingChunk: true, enforce: true }, // Vue 全家桶 vue: { name: 'chunk-vue', test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true }, // element-ui element: { name: 'chunk-element', test: /[\\/]node_modules[\\/]element-ui[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true }, vxe: { name: 'chunk-vxe', test: /[\\/]node_modules[\\/]vxe-table[\\/]/, chunks: 'all', priority: 3, reuseExistingChunk: true, enforce: true }, dynamic:{ name :'chunk-dynamic', test:/[\\/]business[\\/]platform[\\/]/, chunks: 'all', minChunks: 5, priority: 3, reuseExistingChunk:true, maxSize: 102400 }, dynamicform:{ name :'chunk-dynamicform', test:/[\\/]business[\\/]platform[\\/]form[\\/]/, chunks: 'all', minChunks: 5, reuseExistingChunk:true, priority: 4, } }
4.cdn
let cdn = {}
if (enableCDN) {
cdn = {
// 以CDN链接的形式引入与index相关的外部依赖
index: cdnDependencies,
// 以CDN链接的形式引入与移动页面相关的外部依赖
subpage: []
}
}
// 设置不参与构建的外部依赖包
const externals = {}
keys(pages).forEach(name => {
if (cdn[name]) {
cdn[name].forEach(p => {
if (p.library !== '') {
externals[p.name] = p.library
}
})
}
})
configureWebpack: config => {
const configNew = {
plugins: []
}
if (NODE_ENV === 'production') {
configNew.externals = externals
}
}
chainWebpack: config => {
keys(pages).forEach(name => {
const packages = cdn[name]
config.plugin(`html-${name}`).tap(options => {
const setting = {
css: compact(map(packages, 'css')),
js: compact(map(packages, 'js'))
}
set(options, '[0].cdn', NODE_ENV === 'production' ? setting : [])
return options
})
})
}
//index.html
<!-- 使用 CDN 加速的 CSS 文件,配置在 vue.config.js 下 -->
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style">
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
<% } %>
<!-- 使用 CDN 加速的 JS 文件,配置在 vue.config.js 下 -->
<% for (var i in htmlWebpackPlugin.options.cdn&&htmlWebpackPlugin.options.cdn.js) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="preload" as="script">
<% } %>
5.teser.cssminimizer,js css代码压缩,
5.1 提取css mini-css-extract-plugin
5.2 压缩css optimize-cssnano-plugin
5.3 vue中默认 TerserPlugin 压缩js
module.exports={
configureWebpack:config =>{
return {
optimization:{
minimize: true
}
}
}
}
6.treeShaking ==> usedExporte = true
7.gzip
const ignorePlugins =new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
const productionGzipExtensions = ['js', 'css']
const gzipPlugins = new CompressionWebpackPlugin({
filename: '[path][base].gz[query]',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false
})
configNew.plugins.push(gzipPlugins)
configNew.plugins.push(ignorePlugins)
8.DllPlugin和DllReferencePlugin
//dll.config.js
const path = require('path')
const webpack = require('webpack')
const package = require('../package.json')
const AssetsPlugin = require('assets-webpack-plugin')
//读取package.json里的依赖,normalize.css除外,打包会报错
const package = require('../package.json')
let dependencies = Object.keys(package.dependencies) || []
//如果使用了chrome的vue-devtool,那打包的时候把vue也排除掉,因为压缩过的vue是不能使用vue-devtool的
dependencies = dependencies.length > 0 ? dependencies.filter(item => item !== 'vue') : []
module.exports = {
entry: {
vendor: dependencies
},
output: {
path: path.join(__dirname, '../static'),
filename: 'dll.[name]_[hash:6].js',
library: '[name]_[hash:6]'
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, '../', '[name]-manifest.json'),
name: '[name]_[hash:6]'
}),
new AssetsPlugin({
filename: 'bundle-config.json',
path: './'
})
]
}
webpack -p --progress --config build/webpack.dll.conf.js
//webpack.config.js
const manifest = require('../vendor-manifest.json')
...
plugins: [
new webpack.DllReferencePlugin({
manifest
})
]
//index.html
<script src="./static/dll.vendor.js"></script>
8.缓存 优化构建速度 HardSourceWebpackPlugin
if (enableCache) {
const cachePlugins = [
new HardSourceWebpackPlugin(),
new HardSourceWebpackPlugin.ExcludeModulePlugin([
{
test: /mini-css-extract-plugin[\\/]dist[\\/]loader/
}
])
]
configNew.plugins.push(...cachePlugins)
}
9.去除console
config.when(NODE_ENV === 'production' ,config=>{
config.optimization.minimizer('terser').tap((args) => {
args[0].parallel = 4
args[0].terserOptions.compress.warnings = true
args[0].terserOptions.compress.drop_console = true
args[0].terserOptions.compress.pure_funcs = ['console.log']
return args
})
})
10.image-webpack-loader 压缩图片
11.按需加载
12.多线程压缩js terser-webpacl-plugin
性能分析工具
speed-measure-webpack-plugin:分析loader和plugin执行时间
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin') module.exports = { chainWebpack: config => { config .plugin('speed-measure-webpack-plugin') .use(SpeedMeasurePlugin) .end() } }
- webpack-bundle-analyzer:分包情况
查看 vuecli 配置
vue inspect --mode production > output.js
webpack4-->webpack5
- 不需要HardSourceWebpackPlugin,内置filesystem,不在自动注入node polyfill核心模块
- IgnorePlugin变化
- 移除file-loader和url-loader
- thread-loader 替换happyPack
- allowedHosts 代替 disableHostCheck
- inline移除
- contenBase 变为static对象配置
module.exports = { publicPath:'auto', outputDir, lintOnSave: true, devServer: { // publicPath, // 和 publicPath 保持一致 port, // 端口 hot:true, configureWebpack:config=>{ resolve: { fallback: { //人工注入核心模块 util: require.resolve('util/'), assert: require.resolve('assert/'), timers: require.resolve('timers-browserify') } } configNew.cache = { type: 'filesystem', allowCollectingMemory: true } //old //const ignorePlugins =new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) //new const ignorePlugins =new webpack.IgnorePlugin({ resourceRegExp:/^\.\/locale$/, contextRegExp:/moment$/}) }
"@babel/eslint-parser": "^7.23.10", "@kazupon/vue-i18n-loader": "^0.5.0", "@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-router": "^5.0.8", "@vue/cli-plugin-unit-jest": "^5.0.8", "@vue/cli-plugin-vuex": "^5.0.8", "@vue/cli-service": "^5.0.8", "@vue/eslint-config-standard": "^6.1.0", "@vue/test-utils": "^1.3.0", "babel-eslint": "^10.0.3", "babel-plugin-dynamic-import-node": "^2.3.3", "circular-dependency-plugin": "^5.2.2", "compression-webpack-plugin": "^6.1.1", "core-js": "^3.19.1", "eslint": "^7.32.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.1", "eslint-plugin-vue": "^7.20.0", "html-webpack-plugin": "^5.6.0", webpack@5.89.0
内存泄漏分析
vue2 keepAlive
版本:vue 2.6.14
问题:关闭某个页面 内存文档泄漏 x mb
原因:https://github.com/vuejs/vue/issues/9842
解决方案:vue 打补丁 patch-package
问题:仍存在内存泄漏。
原因:页面销毁后,功能正常,但dom元素挂载到了最外层上,具体原因未知,疑渲染混乱问题,
解决:手动清除keepalive内存
let vm = null
// 删除非当前页面 释放keepAlive内存
export function handleRouteClean(delPage){
let fullPath = delPage.fullPath
let Vnode = vm.$children
let vnode
for(let item of Vnode){
if (item.$vnode.data.key && item.$vnode.data.key==fullPath) {
vnode = item.$vnode;
break;
}
}
if(vnode){
delKeepAliveCache(vnode)
}
Vnode = null
}
function delKeepAliveCache(vnode){
let key = vnode.key || vnode.componentOptions.Ctor.cid + (vnode.componentOptions.tag ? `::${vnode.componentOptions.tag}` : "")
let parent = vnode.parent.componentInstance
let cache = parent.cache
let keys = parent.keys
let vnodes
if(cache[key]){
cache[key].componentInstance.$destroy();
if(keys.length){
let index = keys.indexOf(key)
if(index > -1){
keys.splice(index, 1)
}
}
if (vnode.children || vnode.componentInstance?._vnode?.children) {
vnodes = vnode.children || vnode.componentInstance._vnode.children
for (const vn of vnodes) {
destroyDeep(vn)
}
}
vnode.elm.innerHTML = ''
vnode.componentInstance = null
vnodes = null
delete cache[key]
}
}
function destroyDeep(vnode){
let vnodes
if (vnode.children || vnode.componentInstance?._vnode?.children) {
vnodes = vnode.children || vnode.componentInstance._vnode.children
for (const vn of vnodes) {
destroyDeep(vn)
}
}
vnode.elm.innerHTML = ''
vnode.elm.innerText = ''
vnode.componentInstance?.$destroy();
vnode.componentInstance = null
vnodes = null
}
// 删除当前页面 释放keepAlive内存
export function destroyKp(delPage){
let Vnode = vm.$children
let fullPath = delPage.fullPath
let reg = new RegExp(fullPath)
for(let item of Vnode){
let parent = item.$vnode.parent.componentInstance
let cache = parent.cache
let keys = parent.keys
let key
for(let cacheKey of keys){
if(reg.test(cacheKey)){
key = cacheKey
break
}
}
if(key && cache[key]){
if(cache[key].componentInstance.children || cache[key].componentInstance._vnode.children){
let vnodes = cache[key].componentInstance.children || cache[key].componentInstance._vnode.children
for (const vn of vnodes) {
destroyDeep(vn)
}
}
cache[key].componentInstance.$destroy();
if(keys.length){
let index = keys.indexOf(key)
if(index > -1){
keys.splice(index, 1)
}
}
delete cache[key]
break
}
}
}
export function receive(that){
vm = that
}
问题:手动清楚后,仍存在对销毁页面的引用
原因:routes中 存在 instances引用
解决:条件记录删除页面,手动获取routes中的instance
function removeRouter(pages){
const routes = router.getRoutes()
let nameMap = new Map()
for (let index = 0; index < routes.length; index++) {
const r = routes[index];
nameMap.set(r.path, r)
}
for(let i = 0 ;i<pages.length;i++){
const rPage = nameMap.get(pages[i])
if (rPage && util.isNotEmpty(rPage.instances)) rPage.instances.default = undefined
}
nameMap = null
store.dispatch('ibps/page/changePage')
}
debounce 造成的内存泄漏
//内存始终存在,未调用 lodash 销毁
debounce(() => {
that.handleTableHeight(true, true, true)
}, 100)
this.debounceListner = debounce(() => {
that.handleTableHeight(true, true, true)
}, 100)
this.debounceListner.cancel()
this.debounceListner = null
window.requestAnimationFrame 造成的内存泄漏
window.cancelAnimationFrame(this.rafId)
elementui源码的内存泄漏
问题 :Popper,buttom 组件销毁持续引用销毁页面
原因:源码中存在引用,未完全销毁
解决:1,查看git 是否修复版本 ,若修复 升级
2 ,patch-package,issue中有解决方案 解绑源码监听事件
监听事件,定时器
解绑,记录id清除
远程请求封装js内存泄漏
问题: Map 对象未清理,remoteFunc,callback数组存在潜在内存泄漏=》回调用不执行
import { REMOTE_REQUEST_TIMEOUT, REMOTE_TRANS_REQUEST_TIMEOUT } from '@/constant'
const cache = new Map()
const cacheTime = new Map()
/**
* 远程获取[避免重复请求]
* @param prefix 前缀
* @param params 参数配置
* @param remoteFunc 反馈参数
* @param repeatRequest 是否重复请求
* @returns {Promise<any>|Promise<T | never>}
*/
export function remoteRequest(prefix, params, remoteFunc, repeatRequest = true, timeout) {
if (params == null) {
return new Promise((resolve) => {
resolve([])
})
}
if (!timeout) {
timeout = REMOTE_REQUEST_TIMEOUT
}
timeout=5000
const timeKey = prefix + '#' + JSON.stringify(params)
let time = ''
if (repeatRequest) {
const curTime = new Date().getTime()
time = cacheTime.get(timeKey)
if (time == null) {
cacheTime.set(timeKey, curTime)
time = curTime
}
if (curTime - timeout >= time) { // 在指定时间内的请求都重新请求
// 清除缓存换个请求
cacheTime.clear()
cacheTime.set(timeKey, curTime)
time = curTime
}
}
const key = timeKey + '#' + time
// 远程获取
let item = cache.get(key)
if (item == null || item.error === true) {
// 还没加载过
if (item == null) {
item = { loading: true, callbacks: [] }
cache.set(key, item)
}
item.loading = true
item.error = false
// 远程加载
return remoteFunc().then((data) => {
item.data = data
for (const callback of item.callbacks) {
callback(item.data)
}
item.loading = false
item.callbacks = []
return data
}).catch(() => {
//catch中未处理callback ,存在callback永不执行的可能
item.loading = false
item.error = true
})
} else if (item.loading === true) {
return new Promise((resolve) => {
const callback = (data) => {
resolve(data)
}
item.callbacks.push(callback)
})
} else {
// 从缓存拿
return new Promise((resolve) => {
resolve(item.data)
})
}
}
const cacheTrans = new Map()
const cacheTransTime = new Map()
export function remoteTransRequest(prefix, id, timeout) {
const curTime = new Date().getTime()
if (!timeout) {
timeout = REMOTE_TRANS_REQUEST_TIMEOUT
}
let time = cacheTransTime.get(prefix)
if (time == null) {
cacheTransTime.set(prefix, curTime)
time = curTime
}
if (curTime - timeout >= time) { // 2秒内的请求都重新请求
// 清除缓存换个请求
cacheTransTime.clear()
cacheTransTime.set(prefix, curTime)
time = curTime
}
const key = prefix + '#' + time
// 汇总接口
let item = cacheTrans.get(key)
let idVal = id
if (Object.prototype.toString.call(id) === '[object Object]') {
idVal = JSON.stringify(id)
}
if (item == null || item.error === true) {
// 还没加载过
if (item == null) {
item = { loading: true, ids: new Set(), callbacks: [] }
cacheTrans.set(key, item)
}
item.loading = true
item.ids = item.ids.add(idVal)
const remoteFunc = (ids) => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(ids)
}, 100)
})
}
return remoteFunc(item.ids).then((ids) => {
item.ids = ids
// 之前注册过的callback全部触发
for (const callback of item.callbacks) {
callback(ids)
}
item.loading = false
item.callbacks = []
return ids
})
} else if (item.loading === true) {
return new Promise((resolve) => {
const callback = (ids) => {
item.ids = ids.add(idVal)
resolve(item.ids)
}
item.callbacks.push(callback)
})
} else {
// 从缓存拿
return new Promise((resolve) => {
resolve(item.ids)
})
}
}
解决:
//回调存在永不执行问题
return remoteFunc().then().catch((er)=>{
for (const callback of item.callbacks) {
callback(undefined, error); // Pass error to callback
}
item.callbacks = [];
})
//map对象无清理机制
export function des() {
// 停止定时器
stopCacheCleaner();
// 清除所有缓存
cache.clear();
cache.clear();
cacheTrans.clear();
cacheTrans.clear();
}
概念
按需加载和懒加载和动态导入
都一样
按需加载(On-Demand Loading):
- 按需加载通常指的是根据应用程序的需求和逻辑来加载资源。
- 它可以是用户驱动的,例如,当用户点击一个按钮时加载一个模块,或者是系统驱动的,例如,当应用程序达到某个状态时自动加载某些数据。
- 按需加载是一个广泛的概念,可以应用于各种资源,包括代码模块、数据、媒体文件等。
懒加载( Loading):
- 懒加载是按需加载的一种形式,它特别强调延迟加载资源直到这些资源真正需要显示或使用时。
- 懒加载经常用于图像、视频、脚本和其他页面元素,以减少初始页面加载时间和带宽使用。
- 在Web开发中,懒加载通常与视图相关,例如,只有当用户滚动到页面的某个部分时,才加载那部分的图像或内容。
动态导入
- js语法
- import()
区别:
- 懒加载可以被视为按需加载的一个特定场景,它更多地关注于视图层面的资源和组件的加载。
- 按需加载是一个更通用的术语,它可以涵盖从代码模块到数据的各种资源的加载策略。
- 在实际应用中,懒加载通常是通过监听用户的滚动事件、视图的变化或其他交互行为来实现的,而按需加载可能是基于更复杂的逻辑或应用状态。
- Dynamic import 是实现按需加载的技术手段
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。