我司有一个工作台搭建产品,容许通过拖拽小部件的形式来搭建一个工作台页面,平台内置了一些罕用小部件,另外也容许自行开发小部件上传应用,本文会从实际的角度来介绍其实现原理。
ps.本文我的项目应用Vue CLI
创立,所用的Vue
版本为2.6.11
,webpack
版本为4.46.0
。
创立我的项目
首先应用Vue CLI
创立一个我的项目,在src
目录下新建一个widgets
目录用来寄存小部件:
一个小部件由一个Vue
单文件和一个js
文件组成:
测试组件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 Widgetconst config = { color: 'red'}export { config}
除了导出组件,也反对导出配置。
我的项目的App.vue
组件咱们用来作为小部件的开发预览和测试,成果如下:
小部件的配置会影响包裹小部件容器的边框色彩。
打包小部件
假如咱们的小部件曾经开发实现了,那么接下来咱们须要进行打包,把Vue
单文件编译成js
文件,打包应用的是webpack
,首先创立一个webpack
配置文件:
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
的配置就写到这里,接下来写打包的脚本文件:
咱们通过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" }}
运行完后能够看到打包后果曾经有了:
应用小部件
咱们的需要是线上动静的申请小部件的文件,而后将小部件渲染进去。申请应用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) }}
失常来说咱们能获取到导出的模块,可是竟然报错了!
说实话,笔者看不懂这是啥错,百度了一下也无果,然而通过一番尝试,发现把我的项目的babel.config.js
里的预设由@vue/cli-plugin-babel/preset
批改为@babel/preset-env
后能够了,具体是为啥呢,反正我也不晓得,当然,只应用@babel/preset-env
可能是不够的,这就须要你依据理论状况再调整了。
不过起初笔者浏览Vue CLI
官网文档时看到了上面这段话:
直觉通知我,必定就是这个问题导致的了,于是把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('打包实现');});
几行代码解放双手。当初来看看咱们最初获取到的小部件导出数据:
小部件的选项对象有了,接下来把它扔给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) } } }}
成果如下:
是不是很简略。
深刻component组件
最初让咱们从源码的角度来看看component
组件是如何工作的,先来看看对于component
组件最初生成的渲染函数长啥样:
_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()
办法:
这个办法实际上就是以Vue
为父类创立了一个子类:
持续看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
:
createElement
办法最初会返回创立的这个VNode
,渲染函数执行完生成了VNode
树,下一步会将虚构DOM
树转换成实在的DOM
,这一阶段没有必要再去看,因为到这里咱们曾经能发现在编译后,也就是将模板编译成渲染函数这个阶段,component
组件就曾经被解决完了,失去了上面这个创立VNode
的办法:
_c(_vm.widgetData,{tag:"component"})
如果咱们传给component
的is
属性是一个组件的名称,那么在createElement
办法里就会走下图的第一个if
分支:
也就是咱们一般注册的组件会走的分支,如果咱们传给is
的是选项对象,绝对于一般组件,其实就是少了一个依据组件名称查找选项对象的过程,其余和一般组件没有任何区别,至于模板编译阶段对它的解决也非常简略:
间接取出is
属性的值保留到component
属性上,最初在生成渲染函数的阶段:
这样就失去了最初生成的渲染函数。