zoukankan      html  css  js  c++  java
  • 微前端与项目实施方案研究

    一、前言

    微前端(micro-frontends)是近几年在前端领域出现的一个新概念,主要内容是将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。微前端的理念源于微服务,是将庞大的整体拆成可控的小块,并明确它们之间的依赖关系,而它的价值在于能将低耦合的代码与组件进行组合,基座+基础协议模式能接入大量应用,进行统一的管理和输出,许多公司与团队也都在不断尝试和优化相关解决技术与设计方案,为这一概念的落地和推广添砖加瓦。结合自身遇到的问题,适时引用微前端架构能起到明显的提效赋能作用。
    

    二、背景

    目前我司拥有大量的内部系统,这些系统采用相同的技术栈,在实际开发和使用过程中,逐渐暴露出如下几个问题:
    

    1.有大量可复用的部分,虽然有组件库,但是依赖版本难统一;
    2.静态资源体积过大,影响页面加载和渲染速度;
    3.应用切换目前是通过链接跳转的方式实现,会有白屏和等待时长的问题,对用户体验不够友好;
    针对上述几个问题,决定采用微前端架构对内部系统进行统一的管理,本文也是围绕微前端落地的技术预研方案。

    三、方案调研

    目前业界有多种解决方案,有各自的优缺点,具体如下:

    • 路由转发:路由转发严格意义上不属于微前端,多个子模块之间共享一个导航即可 简单,易实现 体验不好,切换应用整个页面刷新;

    • 嵌套 iframe:每个子应用一个 iframe 嵌套 应用之间自带沙箱隔离 重复加载脚本和样式;

    • 构建时组合:独立仓储,独立开发,构建时整体打包,合并应用 方便依赖管理,抽取公共模块 无法独立部署,技术栈,依赖版本必须统一;

    • 运行时组合:每个子应用独立构建,运行时由主应用负责应用管理,加载,启动,卸载,通信机制 良好的体验,真正的独立开发,独立部署 复杂,需要设计加载,通信机制,无法做到彻底隔离,需要解决依赖冲突,样式冲突问题;

      开源微前端框架也有多种,例如阿里出品的qiankun,icestark,还有针对angular提出的mooa等,都能快速接入项目,但结合公司内部系统的特点,直接采用会有有些限制,例如要实现定制界面,无刷新加载应用,且不能对现有项目的开发和部署造成影响,因此决定自研相关技术。

    四、架构设计

    undefined

    4.1 应用层

    应用层包括所有接入微服务工作台的内部系统,他们各自开发与部署,接入前后没有多大影响,只是需要针对微服务层单独输出打包一份静态资源;

    4.2 微服务层

    微服务层作为核心模块,拥有资源加载、路由管理、状态管理和用户认证管理几大功能,具体内容将在后面详细阐述,架构整体工作流程如下:

    undefined

    4.3 基础支撑层

    基础支撑层作为基座,提供微服务运行的环境和容器,同时接入其他后端服务,丰富实用场景和业务功能;

    五、技术重难点

    要实现自定义微前端架构,难点在于需要管理和整合多个应用,确保应用之间独立运行,彼此不受影响,需要解决如下几个问题:

    5.1 资源管理

    5.1.1资源加载

    undefined

    每个应用有一个应用资源管理和注册的文件(app.regiser.js),其中包含路由信息,应用配置信息(configs.js)和静态资源清单,当首次切换到某应用时,首先加载app.register.js文件,完成路由和应用信息的注册,然后根据当前浏览器路由地址加载对应的静态文件,完成页面渲染,从而将各应用的静态资源串联起来,其中注册入口文件通过webpack插件来实现,具体实现如下:
    
    FuluAppRegisterPlugin.prototype.apply = function(compiler) {
       appId = extraAppId();
       var entry = compiler.options.entry;
       if (isArray(entry)) {
                for (var i = 0; i < entry.length; i++) {
                    if (isIndexFile(entry[i])) { // 入口文件
                        indexFileEdit(entry[i]);
                        entry[i] = entry[i].replace(indexEntryRegx, indeEntryTemp); // 替换入口文件
                        i = entry.length;
                    }
                }
        } else {
                if (isIndexFile(entry)) { // 入口文件
                    indexFileEdit(entry); // 重新生成和编辑入口文件
                    compiler.options.entry = compiler.options.entry.replace(indexEntryRegx, indeEntryTemp); // 替换入口文件
                }
        }
        compiler.hooks.done.tap('fulu-app-register-done', function(compilation) {
                fs.unlinkSync(tempFilePath); // 删除临时文件
                return compilation;
        });
        compiler.hooks.emit.tap('fulu-app-register', function(compilation) {
            var contentStr = 'window.register("'+ appId + '", {
    router: [ 
     ' + extraRouters() + ' 
    ],
    entry: {
    '; // 全局注册方法
            var entryCssArr = [];
            var entryJsArr = [];
            for (var filename in compilation.assets) {
                if (filename.match(mainCssRegx)) { // 提取css文件
                    entryCssArr.push('"' + filename + '"');
                } else if (filename.match(mainJsRegx) || filename.match(manifestJsRegx) || filename.match(vendorsJsRegx)) { // 提取js文件
                    entryJsArr.push('"' + filename + '"');
                }
            }
            contentStr += ('css: ['+ entryCssArr.join(', ') +'],
    '); // css资源清单
            contentStr += ('js: ['+ entryJsArr.join(', ') +'],
     }
    });
    '); // js资源清单
            compilation.assets['resources/js/' + appId + '-app-register.js'] = { // 生成appid-app-register.js入口文件
                source: function() {
                    return contentStr;
                },
                size: function() {
                    return contentStr.length;
                }
            };
            return compilation;
        });
    };
    
    5.1.2资源文件名
    微服务输出打包模式下,静态资源统一打包形式以项目id开头,形如10000092-main.js, 文件名称的修改通过webpack的插件实现;
    

    undefined

    核心实现代码如下:

    FuluAppRegisterPlugin.prototype.apply = function(compiler) {
        ......
        compiler.options.output.filename = addIdToFileName(compiler.options.output.filename, appId);
        compiler.options.output.chunkFilename = addIdToFileName(compiler.options.output.chunkFilename, appId);
        compiler.options.plugins.forEach((c) => {
            if (c.options) {
                if (c.options.filename) {
                    c.options.filename = addIdToFileName(c.options.filename, appId);
                }
                if (c.options.chunkFilename) {
                    c.options.chunkFilename = addIdToFileName(c.options.chunkFilename, appId);
                }
            }
        });
       ......
    };
    

    5.2 路由管理

    路由分为应用级和菜单级两大类,应用类以应用id为前缀,将各应用区分开,避免路由地址重名的情况,菜单级的路由由各应用的路由系统自行管理,结构如下:
    

    undefined

    5.3 状态分隔

    前端项目通过状态管理库来进行数据的管理,为了保证各应用彼此间独立,因此需要修改状态库的映射关系,这一部分需要借助于webpack插件来进行统一的代码层面调整,包括model和view两部分代码,model定义了状态对象,view借助工具完成状态对象的映射,调整规则为【应用id+旧状态对象名称】,下面来讲解一下插件的实现;
    

    undefined

    插件的实现原理是借助AST的搜索语法匹配源代码中的状态编写和绑定的相关代码,然后加上应用编号前缀,变成符合预期的AST,最后输出成目标代码:
    
    module.exports = function(source) {
          var options = loaderUtils.getOptions(this);
    	stuff = 'app' + options.appId;
    	isView = !!~source.indexOf('React.createElement'); // 是否是视图层
    	allFunc = [];
    	var connectFn = "function connect(state) {return Object.keys(state).reduce(function (obj, k) { var nk = k.startsWith('"+stuff+"') ? k.replace('"+stuff+"', '') : k; obj[nk] = state[k]; return obj;}, {});}";
    	connctFnAst = parser.parse(connectFn);
    	const ast = parser.parse(source, { sourceType: "module", plugins: ['dynamicImport'] });
    	traverse(ast, {
    		CallExpression: function(path) {
    			if (path.node.callee && path.node.callee.name === 'connect') { // export default connext(...)
    				if (isArray(path.node.arguments)) {
    					var argNode = path.node.arguments[0];
    					if (argNode.type === 'FunctionExpression') { // connect(() => {...})
    						traverseMatchFunc(argNode);
    					} else if (argNode.type === 'Identifier' && argNode.name !== 'mapStateToProps') { // connect(zk)
    						var temp_node = allFunc.find((fnNode) => {
    							return fnNode.id.name === argNode.name;
    						});
    						if (temp_node) {
    							traverseMatchFunc(temp_node);
    						}
    					}
    				}
    			} else if (path.node.callee && path.node.callee.type === 'SequenceExpression') {
    				if (isArray(path.node.callee.expressions)) {
    					for (var i = 0; i < path.node.callee.expressions.length; i++) {
    						if (path.node.callee.expressions[i].type === 'MemberExpression'
    							&& path.node.callee.expressions[i].object.name === '_dva'
    							&& path.node.callee.expressions[i].property.name === 'connect') {
    								traverseMatchFunc(path.node.arguments[0]);
    								i = path.node.callee.expressions.length;
    						}
    					}
    				}
    			}
    		},
    		FunctionDeclaration: function(path) {
    			if (path.node.id.name === 'mapStateToProps' && path.node.body.type === 'BlockStatement') {
    				traverseMatchFunc(path.node);
    			}
    			allFunc.push(path.node);
    		},
    		ObjectExpression: function(path) {
    			if (isView) {
    				return;
    			}
    			if (isArray(path.node.properties)) {
    				var temp = path.node.properties;
    				for (var i = 0; i < temp.length; i++) {
    					if (temp[i].type === 'ObjectProperty' && temp[i].key.name === 'namespace') {
    						temp[i].value.value = stuff + temp[i].value.value;
    						i = temp.length;
    					}
    				}
    			}
    		}
    	});
    	return core.transformFromAstSync(ast).code;
    };
    

    5.4 框架容器渲染

    完成以上步骤的改造,就可以实现容器中的页面渲染,这一部分涉及到组件库框架层面的调整,大流程如下图:

    undefined

    六、构建流程

    6.1 使用插件

    构建过程中涉及到两款自开发的插件,分别是fulu-app-register-plugin和fulu-app-loader;

    6.1.1 安装
    npm i fulu-app-register-plugin fulu-app-loader -D;
    
    6.1.2 配置

    webpack配置修改:

    const FuluAppRegisterPlugin = require('fulu-app-register-plugin');
    module: {
       rules: [{
             test: /.jsx?$/,
             loader: 'fulu-app-loader',
          }
       ]
    }
    plugins: [
        new FuluAppRegisterPlugin(),
        ......
    ]
    

    6.2.编译

    编译过程与目前项目保持一致,相比以前,多输出了一份微前端项目编译代码,流程如下:

    undefined

    七、遗留问题

    7.1 js环境隔离

    由于各应用都加载到同一个运行环境,因此如果修改了公共的部分,则会对其他系统产生不可预知的影响,目前没有比较好的办法来解决,后续将持续关注这方面的内容,逐渐优化达到风险可制的效果。
    

    7.2.获取token

    目前应用切换使用重定向来完成token获取,要实现如上所述的微前端效果,需要放弃这种方式,改用接口调用异步获取,或者其他解决方案。
    

  • 相关阅读:
    ini_set /ini_get函数功能-----PHP
    【转】那个什么都懂的家伙
    word 2007为不同页插入不同页眉页脚
    August 26th 2017 Week 34th Saturday
    【2017-11-08】Linux与openCV:opencv版本查看及库文件位置等
    August 25th 2017 Week 34th Friday
    August 24th 2017 Week 34th Thursday
    August 23rd 2017 Week 34th Wednesday
    August 22nd 2017 Week 34th Tuesday
    August 21st 2017 Week 34th Monday
  • 原文地址:https://www.cnblogs.com/fulu/p/13070633.html
Copyright © 2011-2022 走看看