当前位置: 首页 > Web前端 > JavaScript

Vue动态组件实践与原理探索

时间:2023-03-27 13:38:22 JavaScript

.countBox{display:flex;弹性方向:列;对齐项目:居中;.count{颜色:红色;}}我们公司有一款工作台搭建产品,可以通过拖拽widget的方式搭建工作台页面。平台内置了一些常用的widgets,也允许自行开发的widgets上传使用,本文将从实用的角度介绍其实现原理。附言。本文项目使用VueCLI创建,使用的Vue版本为2.6.11,webpack版本为4.46.0。创建项目首先使用VueCLI创建一个项目,在src目录下创建一个widgets目录,用于存放widgets:一个widget由一个单独的vue文件和一个js文件组成:测试组件index.vue内容如下:<模板>{{count}}

+1-1
.countBox{display:flex;弹性方向:列;对齐项目:居中;.count{颜色:红色;}}一个非常简单的计数器。index.js用于导出组件:importWidgetfrom'./index.vue'exportdefaultWidgetconstconfig={color:'red'}export{config}除了导出组件,它还支持导出配置。项目的App.vue组件作为widget的开发预览和测试。效果如下:widget的配置会影响包裹widget的容器??的边框颜色。打包widgets假设我们的widgets已经开发好了,那么我们需要对其进行打包,将vue单文件编译成js文件,使用webpack进行打包。首先,创建一个webpack配置文件:webpack常用的配置项有:entry、output、module、plugins,我们一一来看。1、entry入口很明显就是各个widget目录下的index.js文件,因为widget的个数是可变的,可能会增加,所以入口不能写死,需要动态生成:constpath=require('path')constfs=require('fs')constgetEntry=()=>{letres={}letfiles=fs.readdirSync(__dirname)files.forEach((filename)=>{//是否是目录letdir=path.join(__dirname,filename)letisDir=fs.statSync(dir).isDirectory//入口文件是否存在letentryFile=path.join(dir,'index.js')letentryExist=fs.existsSync(entryFile)if(isDir&&entryExist){res[filename]=entryFile}})returnres}module.exports={entry:getEntry()}2.output输出因为我们开发后需要测试,所以方便请求打包文件,我们直接将widgets的打包结果输出到public目录:module.exports={//...output:{path:path.join(__dirname,'../../public/widgets'),文件名:'[名称].js'}}3.modulemodule这里需要配置loader规则:处理vue单文件需要vue-loader来编译js最新的语法需要babel-loader来处理less,所以需要less-loader是因为vue-loader和babel-loader相关的包已经安装在Vue项目本身,所以不需要我们手动安装吧,安装处理less文件的loader即可:npmilessless-loader-D不同版本的less-loader对webpack的版本也有要求。如果安装出错,可以指定安装支持当前webpack版本的less-。装载机版本修改配置文件如下:module.exports={//...module:{rules:[{test:/\.vue$/,loader:'vue-loader'},{test:/\.js$/,loader:'babel-loader'},{test:/\.less$/,loader:['vue-style-loader','css-loader','less-loader']}]}}4.pluginsplugin我们使用了两个插件,一个是vue-loader指定的,一个是用来清空输出目录的:npmiclean-webpack-plugin-D修改配置文件如下:const{VueLoaderPlugin}=require('vue-loader')const{CleanWebpackPlugin}=require('clean-webpack-plugin')module.exports={//...plugins:[newVueLoaderPlugin(),newCleanWebpackPlugin()]}webpack的配置写了这里,然后记下打包后的脚本文件:我们通过api使用webpack:constwebpack=require('webpack')constconfig=require('./webpack.config')webpack(config,(err,stats)=>{如果(错误||stats.hasErrors()){//在这里处理错误console.error(err);}//处理完成console.log('打包完成');});现在我们可以在命令行输入nodesrc/widgets/build.js进行打包,如果嫌麻烦,也可以在package.json文件中配置:{"scripts":{"build-widgets":"nodesrc/widgets/build.js"}}运行后可以看到打包结果已经出来了:使用widgets,我们的需求是在线动态请求widget的文件,然后渲染widget并请求使用ajax获取小部件的js文件内容。我们渲染的第一个想法是使用Vue.component()方法进行注册,但这行不通,因为组件的全局注册必须发生在根Vue实例创建之前。所以这里我们使用的是component组件。Vue的component组件可以接受注册组件的名称或者组件的一个option对象,就像我们可以提供widget的option对象一样。请求js资源,我们使用axios获取js字符串,然后使用newFunction动态执行获取导出的option对象://点击加载按钮后调用该方法asyncload(){try{let{data}=awaitaxios.get('/widgets/Count.js')letrun=newFunction(`return${data}`)letres=run()console.log(res)}catch(error){console.error(error)}}正常情况下我们可以拿到导出的模块,但是报错!老实说,我不明白这是怎么回事,百度了下也没找到,但经过一番尝试,发现项目的babel.config.js中的preset被@vue/cli-plugin修改了-babel/preset改成@babel/preset-env后就ok了。为什么,无论如何,我不知道。当然,仅仅使用@babel/preset-env可能还不够,需要根据实际情况进行调整。然而,当我阅读VueCLI的官方文档时,看到了如下一段话:直觉告诉我,一定是这个问题导致的,于是我修改了vue.config.js如下:module.exports={presets:[['@vue/cli-plugin-babel/preset',{useBuiltIns:false}]]}然后打包,一切正常(多看文档才能确定),但不一定要手动修改babel.config.js每次打包的文件都是一个优雅的东西,我们可以在打包前修改脚本,打包后恢复,修改build.js文件:constpath=require('path')constfs=require('fs')//babel.config。js文件路径constbabelConfigPath=path.join(__dirname,'../../babel.config.js')//缓存原来的配置letoriginBabelConfig=''//修改配置constchangeBabelConfig=()=>{//保存原来的配置originBabelConfig=fs.readFileSync(babelConfigPath,{encoding:'utf-8'})//写入新的配置fs.writeFileSync(babelConfigPath,`module.exports={presets:[['@vue/cli-plugin-babel/preset',{useBuiltIns:false}]]}`)}//恢复原来的配置constresetBabelConfig=()=>{fs.writeFileSync(babelConfigPath,originBabelConfig)}//修改changeBabelConfig()网络pack(config,(err,stats)=>{//resetBabelConfig()打包后resetBabelConfig()if(err||stats.hasErrors()){console.error(err);}console.log('打包完成');});几行代码解放你的双手现在来看看我们最终得到的widget导出数据:widget的option对象可用,然后丢给component组件:
exportdefault{data(){return{widgetData:null,widgetConfig:null}},方法:{asyncload(){try{let{data}=awaitaxios.get('/widgets/Count.js')letrun=newFunction(`return${data}`)letres=run()this.widgetData=res.defaultthis.widgetConfig=res.config}catch(error){console.error(error)}}}}效果如下:是不是很简单。深入component组件最后,我们从源码的角度来看一下component组件是如何工作的。首先我们看一下component组件最终的渲染函数长什么样:_c是createElement方法:vm._c=function(a,b,c,d){returncreateElement(vm,a,b,c,d、假);};functioncreateElement(context,//上下文,即父组件实例,即App组件实例tag,//我们的动态组件Count的option对象data,//{tag:'component'}children,normalizationType,alwaysNormalize){//...return_createElement(context,tag,data,children,normalizationType)}忽略一些没有进入的分支,直接进入_createElement方法:function_createElement(context,tag,data,children,normalizationType){//...varvnode,ns;if(typeoftag==='string'){//...}else{//组件选项对象或构造器vnode=createComponent(tag,data,context,children);}//...}tag是一个对象,所以会进入else分支,即执行createComponent方法:functioncreateComponent(Ctor,data,context,children,tag){//...varbaseCtor=context.$options._base;//选项对象:转换为构造函数if(isObject(Ctor)){Ctor=baseCtor.extend(Ctor);}//...}baseCtor是Vue的构造函数,Ctor是Count组件的option对象,所以真正执行的是Vue.extend()方法:这个方法实际上是创建了一个以Vue为父类的子类:继续参见createComponent方法://...//返回一个占位符节点varname=Ctor.options.name||tag;varvnode=newVNode(("vue-component-"+(Ctor.cid)+(name?("-"+name):'')),data,undefined,undefined,undefined,context,{Ctor:Ctor,propsData:propsData,listeners:监听器,tag:标签,children:children},asyncFactory);returnvnode最后会创建一个占位符VNode:createElement方法最终会返回创建的VNode。渲染函数执行后,生成VNode树。下一步是将虚拟DOM树转换为真实的DOM。这个阶段不用看,因为到目前为止我们已经可以发现,在编译之后,也就是在将模板编译成渲染函数的阶段,已经对component组件进行了处理,我们得到了如下方法创建VNode的方法:_c(_vm.widgetData,{tag:"component"})如果我们传给component的is属性是一个组件的名称,那么在createElement方法中,下图中的第一个if分支就是taken:也就是我们普通的注册组件会走的分支。如果我们通过is更重要的是选项对象。与普通组件相比,它实际上少了一个根据组件名查找选项对象的过程。其他与普通组件无异。至于在模板编译阶段对它的处理,也很简单:直接把is属性的值取出来保存到组件属性中,最后在生成渲染函数的阶段:这样,得到最终生成的渲染函数