Vue动态组件的实践与原理探究(vue.extend动态组件)
ztj100 2025-01-31 16:21 12 浏览 0 评论
我司有一个工作台搭建产品,允许通过拖拽小部件的方式来搭建一个工作台页面,平台内置了一些常用小部件,另外也允许自行开发小部件上传使用,本文会从实践的角度来介绍其实现原理。
ps.本文项目使用Vue CLI创建,所用的Vue版本为2.6.11,webpack版本为4.46.0。
创建项目
首先使用Vue CLI创建一个项目,在src目录下新建一个widgets目录用来存放小部件:
image-20211228135808675.png
一个小部件由一个Vue单文件和一个js文件组成:
image-20211228135933206.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-20211228141656015.png
小部件的配置会影响包裹小部件容器的边框颜色。
打包小部件
假设我们的小部件已经开发完成了,那么接下来我们需要进行打包,把Vue单文件编译成js文件,打包使用的是webpack,首先创建一个webpack配置文件:
image-20211228145100423.png
webpack的常用配置项为:entry、output、module、plugins,我们一一来看。
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-loader和babel-loader相关的包Vue项目本身就已经安装了,所以不需要我们手动再安装,装一下处理less文件的loader即可:
npm i less less-loader -D
不同版本的less-loader对webpack的版本也有要求,如果安装出错了可以指定安装支持当前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-20211228152656241.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-20211228153736948.png
使用小部件
我们的需求是线上动态的请求小部件的文件,然后将小部件渲染出来。请求使用ajax获取小部件的js文件内容,渲染我们的第一想法是使用Vue.component()方法进行注册,但是这样是不行的,因为全局注册组件必须在根Vue实例创建之前发生。
所以这里我们使用的是component组件,Vue的component组件可以接受以注册组件的名字或一个组件的选项对象,刚好我们可以提供小部件的选项对象。
请求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-20211228181924164.png
说实话,笔者看不懂这是啥错,百度了一下也无果,但是经过一番尝试,发现把项目的babel.config.js里的预设由@vue/cli-plugin-babel/preset修改为@babel/preset-env后可以了,具体是为啥呢,反正我也不知道,当然,只使用@babel/preset-env可能是不够的,这就需要你根据实际情况再调整了。
不过后来笔者阅读Vue CLI官方文档时看到了下面这段话:
image-20211228192729057.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-20211228182743099.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)
}
}
}
}
效果如下:
2021-12-28-18-45-52.gif
是不是很简单。
深入component组件
最后让我们从源码的角度来看看component组件是如何工作的,先来看看对于component组件最后生成的渲染函数长啥样:
image-20211229135411191.png
_c即createElement方法:
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);
}
// ...
}
baseCtor为Vue构造函数,Ctor即Count组件的选项对象,所以实际执行了Vue.extend()方法:
image-20211229142121211.png
这个方法实际上就是以Vue为父类创建了一个子类:
image-20211229142346598.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-20211229142925592.png
createElement方法最后会返回创建的这个VNode,渲染函数执行完生成了VNode树,下一步会将虚拟DOM树转换成真实的DOM,这一阶段没有必要再去看,因为到这里我们已经能发现在编译后,也就是将模板编译成渲染函数这个阶段,component组件就已经被处理完了,得到了下面这个创建VNode的方法:
_c(_vm.widgetData,{tag:"component"})
如果我们传给component的is属性是一个组件的名称,那么在createElement方法里就会走下图的第一个if分支:
image-20211229143615082.png
也就是我们普通注册的组件会走的分支,如果我们传给is的是选项对象,相对于普通组件,其实就是少了一个根据组件名称查找选项对象的过程,其他和普通组件没有任何区别,至于模板编译阶段对它的处理也十分简单:
image-20211229150134553.png
直接取出is属性的值保存到component属性上,最后在生成渲染函数的阶段:
image-20211229150410598.png
image-20211229150453947.png
这样就得到了最后生成的渲染函数。
相关推荐
- WPS 隐藏黑科技!OCT2HEX 函数用法全攻略,数据转换不再愁
-
WPS隐藏黑科技!OCT2HEX函数用法全攻略,数据转换不再愁在WPS表格的强大函数库中,OCT2HEX函数堪称数据进制转换的“魔法钥匙”。无论是程序员处理代码数据,还是工程师进行电路设计...
- WPS 表格隐藏神器!LEFTB 函数让文本处理更高效
-
WPS表格隐藏神器!LEFTB函数让文本处理更高效在职场办公和日常数据处理中,WPS表格堪称我们的得力助手,而其中丰富多样的函数更是提升效率的关键。今天,要为大家介绍一个“宝藏函数”——LEF...
- Java lombok 使用教程(lombok.jar idea)
-
简介Lombok是...
- PART 48: 万能结果自定义,SWITCH函数!
-
公式解析SWITCH:根据值列表计算表达式并返回与第一个匹配值对应的结果。如果没有匹配项,则返回可选默认值用法解析1:评级=SWITCH(TRUE,C2>=90,"优秀",C2...
- Excel 必备if函数使用方法详解(excel表if函数使用)
-
excel表格if函数使用方法介绍打开Excel,在想输出数据的单元格点击工具栏上的“公式”--“插入函数”--“IF”,然后点击确定。...
- Jetty使用场景(jetty入门)
-
Jetty作为一款高性能、轻量级的嵌入式Web服务器和Servlet容器,其核心优势在于模块化设计、快速启动、低资源消耗...
- 【Java教程】基础语法到高级特性(java语言高级特性)
-
Java作为一门面向对象的编程语言,拥有清晰规范的语法体系。本文将系统性地介绍Java的核心语法特性,帮助开发者全面掌握Java编程基础。...
- WPS里这个EVEN 函数,90%的人都没用过!
-
一、开篇引入在日常工作中,我们常常会与各种数据打交道。比如,在统计员工绩效时,需要对绩效分数进行一系列处理;在计算销售数据时,可能要对销售额进行特定的运算。这些看似简单的数据处理任务,实则隐藏着许多技...
- 64 AI助力Excel,查函数查用法简单方便
-
在excel表格当中接入ai之后会是一种什么样的使用体验?今天就跟大家一起来分享一下小程序商店的下一步重大的版本更新。下一个版本将会加入ai功能,接下来会跟大家演示一下基础的用法。ai功能规划的是有三...
- python入门到脱坑 函数—函数的调用
-
Python函数调用详解函数调用是Python编程中最基础也是最重要的操作之一。下面我将详细介绍Python中函数调用的各种方式和注意事项。...
- 从简到繁,一文说清vlookup函数的常见用法
-
VLOOKUP函数是Excel中常用的查找与引用函数,用于在表格中按列查找数据。本文将从简单到复杂,逐步讲解VLOOKUP的用法、语法、应用场景及注意事项。一、VLOOKUP基础:快速入门1.什么是...
- Java新特性:Lambda表达式(java lambda表达式的3种简写方式)
-
1、Lambda表达式概述1.1、Lambda表达式的简介Lambda表达式(Lambdaexpression),也可称为闭包(Closure),是Java(SE)8中一个重要的新特性。Lam...
- WPS 冷门却超实用!ODD 函数用法大揭秘,轻松解决数据处理难题
-
WPS冷门却超实用!ODD函数用法大揭秘,轻松解决数据处理难题在WPS表格庞大的函数家族里,有一些函数虽然不像SUM、VLOOKUP那样广为人知,却在特定场景下能发挥出令人惊叹的作用,OD...
- Python 函数式编程的 8 大核心技巧,不允许你还不会
-
函数式编程是一种强调使用纯函数、避免共享状态和可变数据的编程范式。Python虽然不是纯函数式语言,但提供了丰富的函数式编程特性。以下是Python函数式编程的8个核心技巧:...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)