Vue动态组件的实践与原理探究

简介: Vue动态组件的实践与原理探究

我司有一个工作台搭建产品,允许通过拖拽小部件的方式来搭建一个工作台页面,平台内置了一些常用小部件,另外也允许自行开发小部件上传使用,本文会从实践的角度来介绍其实现原理。


ps.本文项目使用Vue CLI创建,所用的Vue版本为2.6.11webpack版本为4.46.0


创建项目


首先使用Vue CLI创建一个项目,在src目录下新建一个widgets目录用来存放小部件:


image.png


一个小部件由一个Vue单文件和一个js文件组成:


image.png


测试组件index.vue的内容如下:


<template>
  <div class="countBox">
    <div class="count">{{ count }}</div>
    <div class="btn">
      <button @click="add">+1</button>
      <button @click="sub">-1</button>
    </div>
  </div>
</template>
<script>
export default {
  name: 'count',
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    add() {
      this.count++
    },
    sub() {
      this.count--
    },
  },
}
</script>
<style lang="less" scoped>
.countBox {
  display: flex;
  flex-direction: column;
  align-items: center;
  .count {
    color: red;
  }
}
</style>


一个十分简单的计数器。


index.js用来导出组件:


import Widget from './index.vue'
export default Widget
const config = {
    color: 'red'
}
export {
    config
}


除了导出组件,也支持导出配置。


项目的App.vue组件我们用来作为小部件的开发预览和测试,效果如下:


image.png


小部件的配置会影响包裹小部件容器的边框颜色。


打包小部件


假设我们的小部件已经开发完成了,那么接下来我们需要进行打包,把Vue单文件编译成js文件,打包使用的是webpack,首先创建一个webpack配置文件:


image.png


webpack的常用配置项为:entryoutputmoduleplugins,我们一一来看。

1.entry入口


入口显然就是各个小部件目录下的index.js文件,因为小部件数量是不定的,可能会越来越多,所以入口不能写死,需要动态生成:


const path = require('path')
const fs = require('fs')
const getEntry = () => {
    let res = {}
    let files = fs.readdirSync(__dirname)
    files.forEach((filename) => {
        // 是否是目录
        let dir = path.join(__dirname, filename)
        let isDir = fs.statSync(dir).isDirectory
        // 入口文件是否存在
        let entryFile = path.join(dir, 'index.js')
        let entryExist = fs.existsSync(entryFile)
        if (isDir && entryExist) {
            res[filename] = entryFile
        }
    })
    return res
}
module.exports = {
    entry: getEntry()
}


2.output输出


因为我们开发完后还要进行测试,所以便于请求打包后的文件,我们把小部件的打包结果直接输出到public目录下:


module.exports = {
    // ...
    output: {
        path: path.join(__dirname, '../../public/widgets'),
        filename: '[name].js'
    }
}


3.module模块


这里我们要配置的是loader规则:


  • 处理Vue单文件我们需要vue-loader


  • 编译js最新语法需要babel-loader


  • 处理less需要less-loader


因为vue-loaderbabel-loader相关的包Vue项目本身就已经安装了,所以不需要我们手动再安装,装一下处理less文件的loader即可:


npm i less less-loader -D


不同版本的less-loaderwebpack的版本也有要求,如果安装出错了可以指定安装支持当前webpack版本的less-loader版本。


修改配置文件如下:


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.plugins 插件


插件我们就使用两个,一个是vue-loader指定的,另一个是用来清空输出目录的:


npm i clean-webpack-plugin -D


修改配置文件如下:


const { VueLoaderPlugin } = require('vue-loader')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
    // ...
    plugins: [
        new VueLoaderPlugin(),
        new CleanWebpackPlugin()
    ]
}


webpack的配置就写到这里,接下来写打包的脚本文件:


image.png


我们通过api的方式来使用webpack


const webpack = require('webpack')
const config = require('./webpack.config')
webpack(config, (err, stats) => {
    if (err || stats.hasErrors()) {
        // 在这里处理错误
        console.error(err);
    }
    // 处理完成
    console.log('打包完成');
});


现在我们就可以在命令行输入node src/widgets/build.js进行打包了,嫌麻烦的话也可以配置到package.json文件里:


{
    "scripts": {
        "build-widgets": "node src/widgets/build.js"
    }
}


运行完后可以看到打包结果已经有了:


image.png


使用小部件


我们的需求是线上动态的请求小部件的文件,然后将小部件渲染出来。请求使用ajax获取小部件的js文件内容,渲染我们的第一想法是使用Vue.component()方法进行注册,但是这样是不行的,因为全局注册组件必须在根Vue实例创建之前发生。


所以这里我们使用的是component组件,Vuecomponent组件可以接受以注册组件的名字或一个组件的选项对象,刚好我们可以提供小部件的选项对象。


请求js资源我们使用axios,获取到的是js字符串,然后使用new Function动态进行执行获取导出的选项对象:


// 点击加载按钮后调用该方法
async load() {
    try {
        let { data } = await axios.get('/widgets/Count.js')
        let run = new Function(`return ${data}`)
        let res = run()
        console.log(res)
    } catch (error) {
        console.error(error)
    }
}


正常来说我们能获取到导出的模块,可是居然报错了!


image.png


说实话,笔者看不懂这是啥错,百度了一下也无果,但是经过一番尝试,发现把项目的babel.config.js里的预设由@vue/cli-plugin-babel/preset修改为@babel/preset-env后可以了,具体是为啥呢,反正我也不知道,当然,只使用@babel/preset-env可能是不够的,这就需要你根据实际情况再调整了。


不过后来笔者阅读Vue CLI官方文档时看到了下面这段话:


image.png


直觉告诉我,肯定就是这个问题导致的了,于是把vue.config.js修改为如下:


module.exports = {
  presets: [
    ['@vue/cli-plugin-babel/preset', {
      useBuiltIns: false
    }]
  ]
}


然后打包,果然一切正常(多看文档准没错),但是每次打包都要手动修改babel.config.js文件总不是一件优雅的事情,我们可以通过脚本在打包前修改,打包完后恢复,修改build.js文件:


const path = require('path')
const fs = require('fs')
// babel.config.js文件路径
const babelConfigPath = path.join(__dirname, '../../babel.config.js')
// 缓存原本的配置
let originBabelConfig = ''
// 修改配置
const changeBabelConfig = () => {
    // 保存原本的配置
    originBabelConfig = fs.readFileSync(babelConfigPath, {
        encoding: 'utf-8'
    })
    // 写入新配置
    fs.writeFileSync(babelConfigPath, `
        module.exports = {
            presets: [
                ['@vue/cli-plugin-babel/preset', {
                    useBuiltIns: false
                }]
            ]
        }
    `)
}
// 恢复为原本的配置
const resetBabelConfig = () => {
    fs.writeFileSync(babelConfigPath, originBabelConfig)
}
// 打包前修改
changeBabelConfig()
webpack(config, (err, stats) => {
    // 打包后恢复
    resetBabelConfig()
    if (err || stats.hasErrors()) {
        console.error(err);
    }
    console.log('打包完成');
});


几行代码解放双手。现在来看看我们最后获取到的小部件导出数据:


image.png


小部件的选项对象有了,接下来把它扔给component组件即可:


<div class="widgetWrap" v-if="widgetData" :style="{ borderColor: widgetConfig.color }">
    <component :is="widgetData"></component>
</div>


export default {
    data() {
        return {
            widgetData: null,
            widgetConfig: null
        }
    },
    methods: {
        async load() {
            try {
                let { data } = await axios.get('/widgets/Count.js')
                let run = new Function(`return ${data}`)
                let res = run()
                this.widgetData = res.default
                this.widgetConfig = res.config
            } catch (error) {
                console.error(error)
            }
        }
    }
}


效果如下:


image.png


是不是很简单。


深入component组件


最后让我们从源码的角度来看看component组件是如何工作的,先来看看对于component组件最后生成的渲染函数长啥样:


image.png


_ccreateElement方法:


vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };


function createElement (
  context,// 上下文,即父组件实例,即App组件实例
  tag,// 我们的动态组件Count的选项对象
  data,// {tag: 'component'}
  children,
  normalizationType,
  alwaysNormalize
) {
  // ...
  return _createElement(context, tag, data, children, normalizationType)
}


忽略了一些没有进入的分支,直接进入_createElement方法:


function _createElement (
 context,
 tag,
 data,
 children,
 normalizationType
) {
    // ...
    var vnode, ns;
    if (typeof tag === 'string') {
        // ...
    } else {
        // 组件选项对象或构造函数
        vnode = createComponent(tag, data, context, children);
    }
    // ...
}


tag是个对象,所以会进入else分支,即执行createComponent方法:


function createComponent (
 Ctor,
 data,
 context,
 children,
 tag
) {
    // ...
    var baseCtor = context.$options._base;
    // 选项对象: 转换成构造函数
    if (isObject(Ctor)) {
        Ctor = baseCtor.extend(Ctor);
    }
    // ...
}


baseCtorVue构造函数,CtorCount组件的选项对象,所以实际执行了

Vue.extend()方法:


image.png


这个方法实际上就是以Vue为父类创建了一个子类:


image.png


继续看createComponent方法:


// ...
// 返回一个占位符节点
var name = Ctor.options.name || tag;
var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
);
return vnode


最后创建了一个占位VNode


image.png


createElement方法最后会返回创建的这个VNode,渲染函数执行完生成了VNode树,下一步会将虚拟DOM树转换成真实的DOM,这一阶段没有必要再去看,因为到这里我们已经能发现在编译后,也就是将模板编译成渲染函数这个阶段,component组件就已经被处理完了,得到了下面这个创建VNode的方法:


_c(_vm.widgetData,{tag:"component"})


如果我们传给componentis属性是一个组件的名称,那么在createElement方法里就会走下图的第一个if分支:


image.png


也就是我们普通注册的组件会走的分支,如果我们传给is的是选项对象,相对于普通组件,其实就是少了一个根据组件名称查找选项对象的过程,其他和普通组件没有任何区别,至于模板编译阶段对它的处理也十分简单:


image.png


直接取出is属性的值保存到component属性上,最后在生成渲染函数的阶段:


image.png


image.png


这样就得到了最后生成的渲染函数。



目录
打赏
0
0
0
0
15
分享
相关文章
极致的灵活度满足工程美学:用Vue Flow绘制一个完美流程图
本文介绍了使用 Vue Flow 绘制流程图的方法与技巧。Vue Flow 是一个灵活强大的工具,适合自定义复杂的流程图。文章从环境要求(Node.js v20+ 和 Vue 3.3+)、基础入门案例、自定义功能(节点与连线的定制、事件处理)到实际案例全面解析其用法。重点强调了 Vue Flow 的高度灵活性,虽然预定义内容较少,但提供了丰富的 API 支持深度定制。同时,文中还分享了关于句柄(handles)的使用方法,以及如何解决官网复杂案例无法运行的问题。最后通过对比 mermaid,总结 Vue Flow 更适合需要高度自定义和复杂需求的场景,并附带多个相关技术博客链接供进一步学习。
属性描述符初探——Vue实现数据劫持的基础
属性描述符还有很多内容可以挖掘,比如defineProperty与Proxy的区别,比如vue2与vue3实现数据劫持的方式有什么不同,实现效果有哪些差异等,这篇博文只是入门,以后有时间再深挖。 博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Vue中的class和style绑定
在 Vue 中,class 和 style 绑定是基于数据驱动视图的强大功能。通过 class 绑定,可以动态更新元素的 class 属性,支持对象和数组语法,适用于普通元素和组件。style 绑定则允许以对象或数组形式动态设置内联样式,Vue 会根据数据变化自动更新 DOM。
Vue2+VueRouter2+webpack 构建项目
1). 安装Node环境和npm包管理工具 检测版本 node -v npm -v 图1.png 2). 安装vue-cli(vue脚手架) npm install -g vue-cli --registry=https://registry.
1096 0
|
3月前
|
vue使用iconfont图标
vue使用iconfont图标
177 1
Vue Router 核心原理
Vue Router 是 Vue.js 的官方路由管理器,用于实现单页面应用(SPA)的路由功能。其核心原理包括路由配置、监听浏览器事件和组件渲染等。通过定义路径与组件的映射关系,Vue Router 将用户访问的路径与对应的组件关联,支持哈希和历史模式监听 URL 变化,确保页面导航时正确渲染组件。
ry-vue-flowable-xg:震撼来袭!这款基于 Vue 和 Flowable 的企业级工程项目管理项目,你绝不能错过
基于 Vue 和 Flowable 的企业级工程项目管理平台,免费开源且高度定制化。它覆盖投标管理、进度控制、财务核算等全流程需求,提供流程设计、部署、监控和任务管理等功能,适用于企业办公、生产制造、金融服务等多个场景,助力企业提升效率与竞争力。
126 12
Vue Router 简介
Vue Router 是 Vue.js 官方的路由管理库,用于构建单页面应用(SPA)。它将不同页面映射到对应组件,支持嵌套路由、路由参数和导航守卫等功能,简化复杂前端应用的开发。主要特性包括路由映射、嵌套路由、路由参数、导航守卫和路由懒加载,提升性能和开发效率。安装命令:`npm install vue-router`。
iframe嵌入页面实现免登录思路(以vue为例)
通过上述步骤,可以在Vue.js项目中通过 `iframe`实现不同应用间的免登录功能。利用Token传递和消息传递机制,可以确保安全、高效地在主应用和子应用间共享登录状态。这种方法在实际项目中具有广泛的应用前景,能够显著提升用户体验。
353 8

热门文章

最新文章