vue 远程加载sfc组件思路

6,922 阅读2分钟

问题

在我们的 vue 项目中(特别是后台系统),总会出现一些需要多业务线共同开发同一个项目的场景,如果各业务团队向项目中提供一些公共业务组件,但是这些组件并不能和项目一起打包,因为项目中不能因为某个私有模块的频繁变更而重复构建发布。

^_^不建议在生产环境使用,代码包含eval 

思路

在这种场景下我们需要将公共的业务组件部署到服务端,由客户端请求并渲染组件。

服务端解析.vue文件

使用vue-template-compiler 模板解析器,解析SFC(单文件组件)

const compile = require('vue-template-compiler')

// 获取sfc组件的源码
const str = fs.readFileSync(path.resolve(__dirname, `../components/sfc.vue`), 'utf-8')

// vue-loader内置,现在用来解析SFC(单文件组件)
let sfc = compile.parseComponent(str)

// 获取sfc组件配置
let sfcOptions = getComponentOption(sfc)

getComponentOption 获取sfc组件配置

import { uuid } from 'utilscore'
import stylus from 'stylus'
import sass from 'sass'
import less from 'less'
const getComponentOption = sfc => {
    // 生成data-u-id 
    const componentId = uuid(8, 16).toLocaleLowerCase()    
    // 标签添加data-u-id属性    
    const template = sfc.template ? tagToUuid(sfc.template.content, componentId) : ''   
    // 转化style(less、sass、stylus)    
    let styles = []    
    sfc.styles.forEach(sty => {        
        switch (sty.lang) {            
            case 'stylus':                
                stylus.render(sty.content, (err, css) => styles.push(formatStyl(sty, css, componentId)))                
                break;            
            case 'sass':            
            case 'scss':                
                styles.push(formatStyl(sty, sass.renderSync({ data: sty.content }).css.toString(), componentId))                
                break;            
            case 'less':                
                less.render(sty.content, (err, css) => styles.push(formatStyl(sty, css, componentId)))                
                break;        
        }    
    })    
    let options = {        
        script: sfc.script ? $require(null, sfc.script.content) : {},        
        styles,        
        template    
    }    
    return JSON.stringify(options, (k, v) => {
        if(typeof(v) === 'function') {
            let _fn = v.toString()
            return /^function()/.test(_fn) ? _fn : fn.replace(/^/,'function ')
        }
        return v
    })
}

tagToUuid  给template 中的标签追加data-u-id 

const tagToUuid = (tpl, id) => {    
    var pattern = /<[^\/]("[^"]*"|'[^']*'|[^'">])*>/g    
    return tpl.replace(pattern, $1 => {        
        return $1.replace(/<([\w\-]+)/i, ($2, $3) => `<${$3} data-u-${id}`)    
    })
}

formatStyl 处理样式的scoped

const formatStyl = (sty, css, componentId) => {    
    let cssText = css    
    if (sty.scoped) {        
        cssText = css.replace(/[\.\w\>\s]+{/g, $1 => {            
        if (/>>>/.test($1)) return $1.replace(/\s+>>>/, `[data-u-${componentId}]`)            
        return $1.replace(/\s+{/g, $2 => `[data-u-${componentId}]${$2}`)        
        })    
    }    
    return cssText
}

$require 执行其中的的 JavaScript 代码,并返回值

const $require = (filepath, scriptContext) => {
    const filename = path.resolve(__dirname, `../${filepath}`);    
    const module = { exports: {} }    
    let code = scriptContext ? scriptContext : fs.readFileSync(filename, 'utf-8')    
    let exports = module.exports    
    code = `(function($require,module,exports,__dirname,filename){${code}})($require,module,exports,__dirname,filename)`    
    eval(code)    
    return module.exports
} 

客户端请求组件并渲染

封装前端远程组件-remote.vue

<template>  
    <component :is="remote" v-bind="$attrs" v-on="$listeners"></component>
</template>
<script>
import Vue from "vue";
export default {  
    data() {    
        return {      
            remote: null    
        }
    },  
    props: {    
        tagName: {      
            type: String,      
            defualt: "componentName"    
        }  
    },  
    created() {    
        fetch("http://localhost:3000/getComponent/"+this.tagName)
            .then(res => res.json())      
            .then(sfc => {        
                let options = this.parseObj(sfc);        
                options.styles.forEach(css => this.appendSty(css));        
                this.remote = Vue.extend({ 
                    ...options.script,          
                    name: options.script.name || this.tagName,          
                    template: options.template        
                });      
            });  
     },  
     methods: {    
        isObject(v) {      
            return Object.prototype.toString.call(v).includes("Object");    
        },    
        parseObj(data) {      
            if (Array.isArray(data))  return data.map(row => this.parseObj(row));      
            if (this.isObject(data)) {        
                let ret = {};        
                for (let k in data) {          
                    ret[k] = this.parseObj(data[k]);       
                 }        return ret;      
            }      
            try {        
                let pattern = /function ([\w]+)\(\) \{ \[native code\] \}/;        
                if (pattern.test(data)) {          
                    return window[pattern.exec(data)[1]];        
                } else {          
                    let evalData = eval(`(${data})`);          
                    return typeof evalData == "function" ? evalData : data;        
                }      
            } catch (err) {        
                return data;      
            }    
        },    
        appendSty(css) { // 生成组件样式      
            let style = document.createElement("style");      
            style.setAttribute("type", "text/css");      
            var cssText = document.createTextNode(css);      
            style.appendChild(cssText);      
            var head = document.querySelector("head");     
            head.appendChild(style);    
        }  
}};
</script>

远程组件实践

服务端sfc组件,注意javascript块要使用module.exports导出,引入脚本使用$require

<template>  
    <div class="test">    
        <div>      
            <p @click='$emit("handleClick",'点我')'>远程组件--{{msg}}--{{text}}</p>    
            </div>  
        </div>
</template>
<script>
// 加载js脚本
let {a} = $require('utils/test.js') 
module.exports = {  
    data: function() {    
        return {      
            msg: "remote component",
            ...a,
        }  
    },  
    props: {    
        text: {      
            type: Boolean,      
            default: true    
        }  
    },
    mounted:function(){
        console.log('prop text is',this.text)
    }
};
</script>
<style lang="stylus" scoped>
.test {  
    .test2 {    
         color: red;  
    }  
    p{    
         color:red  
    }
}
</style>

客户端渲染

// temolate
<remote text='123456' @handleClick='handleClick'/>

// script 
methods:{
  handleClick(v){
     console.log(v) // 点我  }
}