建立无需build的vue单页面应用SPA框架

文章探讨了使用Vue和React等前端框架构建SPA时的策略,强调了组件化和异步加载的重要性。通过Vue的异步组件功能,可以按需加载业务组件,避免一次性加载所有组件影响首屏效率。同时,文章提到了使用ejs时可能遇到的DOM操作冲突问题,并给出了Vue3-SFC-Loader作为解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

vue、react这种前端渲染的框架,比较适合做SPA(Single Page Application)。如果用ejs做SPA,js代码控制好全局变量冲突不算严重,但dom元素用jquery操作会遇到很多的名称上的冲突(tag、id、name)。

如何实现SPA,要解决几个点:

(1)业务组件用什么文件格式?如果使用*.vue文件,需要在部署前build转换(也可以直接在浏览器使用vue3-sfc-loader,vue3-sfc-loader - npm)。使用*.js文件,部署前不需要buid转换。本来js的初心就是“即改即用”,我不太喜欢ts,jsx这些需要build的东西。

(2)业务组件如何加载?业务组件不可能写的时候全部知道(根据用户权限决定),也不可能一次性全部加载(影响首屏效率),应该是需要的时候,才从服务器加载。vue为此提供了异步组件,可以用Vue.defineAsyncComponent来创建。

demo.html

<html>
  <header>
  </header>
  <head>
    <!-- <script src="/js/jquery-1.11.1/jquery-1.11.1.min.js"></script> -->
    <!-- <script src="/js/vue-3.3.4/dist/vue.global.js"></script> -->
    <script src="/js/vue3-sfc-loader-0.8.4/dist/vue3-sfc-loader.js"></script>
    <script type="importmap">
      {
        "imports": {
          "vue": "/js/vue-3.3.4/dist/vue.esm-browser.js",
          "easyui":"/js/v3-easyui-3.0.14/dist/v3-easyui.js"
        }
      }
    </script>
    <style>
      @import '/js/v3-easyui-3.0.14/dist/themes/default/easyui.css';
      @import '/js/v3-easyui-3.0.14/dist/themes/icon.css';
      @import '/js/v3-easyui-3.0.14/dist/themes/color.css';
      @import '/js/v3-easyui-3.0.14/dist/themes/vue.css';
    </style>
  </head>
  <body>
    <div id="app"></div>

    <script type="module">
      import * as Vue from 'vue';
      //console.log(Vue);
      import EasyUI from 'easyui';
      //console.log(EasyUI);
      import main from './com.main.js';
      
      let app=Vue.createApp(main);
      app.use(EasyUI);
      app.config.globalProperties.t=function(DDKey){return DDKey};
      //console.log(app);
      app.mount('#app');
    </script>
  </body>
</html>

页面划分为上中下三层,中间划分为左右两部分,左边是功能树,右边是功能区。

com.main.js

import * as Vue from 'vue';
import EasyUI from 'easyui';
//console.log(EasyUI);
import Com_Header from './com.header.js';
import Com_Left from './com.left.js';

export default {
    components: {
        Com_Header,Com_Left
    },
    data() {
      return { 
        tabFile:null,
        tabs:[]
      }
    },
    created(){
        this.$messager.ok=this.t('OK');
        this.$messager.cancel=this.t('Cancel');
    },
    methods:{
        switchTab(name,file){
            console.log(name,file);
            console.log(this.$refs.tabs);
            let tab=null;
            for(let i=0;i<this.tabs.length;i++){
                if (this.tabs[i].name==name){
                    tab=this.tabs[i];
                    this.$refs.tabs.select(i);
                    break;
                }
            }
            if (!tab){
                if (file.substr(file.length-4).toLowerCase()=='.vue'){
                    //使用vue的.vue格式的SFC元件
                    component=Vue.defineAsyncComponent(function(){
                        const options = {
                            moduleCache: {
                              vue: Vue
                            },
                            async getFile(url) {
                              const res = await fetch(url);
                              if (!res.ok)
                                throw Object.assign(new Error(res.statusText + ' ' + url), { res });
                              return {
                                getContentData: function(asBinary){
                                    return asBinary ? res.arrayBuffer() : res.text();
                                }
                              }
                            },
                            addStyle(textContent) {
                              const style = Object.assign(document.createElement('style'), { textContent });
                              const ref = document.head.getElementsByTagName('style')[0] || null;
                              document.head.insertBefore(style, ref);
                            },
                        }
                    
                        const { loadModule } = window['vue3-sfc-loader'];
                        let com=loadModule(file, options);
                        let comMark=Vue.markRaw(com);
                        return comMark;
                    }); 
                }
                else{
                    //使用vue的.js格式的MFC元件
                    component=Vue.defineAsyncComponent(function(){
                        let com=import(file);
                        let comMark=Vue.markRaw(com);
                        return comMark;
                    });
                }
                component=Vue.shallowRef(component);
                this.tabs.push({name,component});
                this.$nextTick(function(){
                    this.$refs.tabs.select(this.tabs.length-1);
                });
            }
        },
        onCloseTab(tab){
            console.log(tab);
            console.log(tab.title);
            for(let i=0;i<this.tabs.length;i++){
                if (this.tabs[i].name==tab.title){
                    this.tabs.splice(i,1);
                    break;
                }
            }
        }
    },
    template: `
        <a href="/">{{t('Home')}}</a>
        <h1>{{t('Demo:translate at frontend browser,translate needed(vue)')}}</h1>
        <span>SPA:Single Page Application</span>
        <div className='layout-header2' style="background-color:bisque">
            <Com_Header></Com_Header>
        </div>
        <div className='layout-middle'>
            <div v-Resizable="{minWidth:200,handles:'e'}" className='layout-left' style="width:200px;float:left;overflow:hidden;background-color:aquamarine">
                <Com_Left :switchTab=switchTab></Com_Left>
            </div>
            <div className='layout-right' style="margin-left:200px;overflow:hidden">
                <Tabs ref=tabs :scrollable="true" :plain=true @tabClose=onCloseTab>
                    <TabPanel v-for="tab in tabs" :key="tab.name" :title="tab.name" :closable=true>
                        <component :is="tab.component">
                        </component>
                    </TabPanel>
                </Tabs>
            </div>
            <div style="clear:both"></div>
        </div>
        <div className='layout-footer' style="background-color:brown;text-align:center">
            <span>copyright© Acroprise Inc. 2001-2023</span>
        </div>
    `
}

这里要注意Vue.markRaw和Vue.shallowRef两个函数,如果不写,会有警告:

[Vue warn]: Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with `markRaw` or using `shallowRef` instead of `ref`.

com.left.js

let Com_Left={
  props:['switchTab'],
  data(){
    return{   
    }
  },
  methods:{
    menu_click(e){
      //console.log(e);
      let name=e.target.innerHTML;
      //console.log(name);
      let file=e.target.getAttribute('file');
      //console.log(file);
      this.switchTab(name,file);
      e.preventDefault();
    }
  },
  template:`
    <div>
      <a href='/'>{{t('Home')}}</a><br/>
      <a href='DDEditor' @click=menu_click file='/vue/app/DDEditor/page.DDEditor.js'>{{t('Data Dictionary Editor')}}</a><br/>
      <a href='likeButton' @click=menu_click file='/vue/app/likeButton/page.likeButton.js'>{{t('Like Button')}}</a><br/>
      <a href='About' onClick={{this.menu_click}} file=''>{{t('&About')}}</a>
    </div>
  `
}
export default Com_Left;

效果如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

火星牛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值