怎么使用Vue渐进式地增强原生html项目

23 阅读5分钟

SFC

我们如果是打算使用vue完整的写完整个项目, 就很适合使用单文件组件,也就是.vue文件, 它的英文名字是Single-File Components,就是SFC的缩写,也就是一个文件代表着一个组件,我们把组件的模板,样式,逻辑都写在这一个文件内,在使用vite这样的构建工具的时候,当下载完所有依赖后,可以在我们的node_module里看到下载的vue,有几个依赖插件, 其中一个名为@vue/compiler-sfc,这个就是用于解析我们的单文件组件的,其中还包含一个名为@vue/compiler-dom的依赖库,下面解释这个库的作用

CDN使用vue

CDN引入的vue的js文件都需要能够解析html文件中哪些包含vue的语法部分并且替换掉他们,上述的@vue/compiler-dom就是用来干这个的,但是CDN的js文件包含编译器, 而经过Vite这种架构工具打包后完全可以去掉编译器,只留运行时vue文件就可以了

选项式API

vue提供一种基于组合式Api之上的选项式api,使用要比组合式写法简单很多,也更适合习惯面向对象编程的使用者,请看下面的例子

//这里将html的部分减去一些,避免一些无关紧要的内容
//样式部分如下
<style>
    .counter {
        display: flex;
        align-items: center;
        justify-content: center;
        column-gap: 15px;
        width: 120px;
        margin: 100px;
        padding: 10px;
        border-radius: 8px;
        background-color: #f5f5f5;
        user-select: none;
        cursor: pointer;
    }
</style>

<body>
    <div id="app">
        <div class="counter">
            <span @click="decrement">-</span>
            <p>{{count}}</p>
            <span @click="increment">+</span>
        </div>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="./index.js" type="module"></script>
</body>

下面是index.js文件


const App = {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  },
  mounted() {
    console.log("挂载");
  }
};

const app = Vue.createApp(App);

app.mount("#app");
//createApp传入的根组件,要是这个组件没有自己的template部分,就会把跟容器(在这里是#app)的`innerHTML`作为它的模板,也可以写成下面这样

const App = {
  data() {
    return {
      count: 0
    }
  },
  template: `<div class="counter">
      <span @click="decrement">-</span>
      <p>{{count}}</p>
      <span @click="increment">+</span>
    </div>`,
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  },
  mounted() {
    console.log("挂载");
  }
};

const app = Vue.createApp(App);

const rootComponents = app.mount("#app");

const app = Vue.createApp(App);

app.mount("#app");

render和template

render

像上述代码中,我们是在dom中内嵌了vue语法,那么就一定要经过编译的阶段了, 我们可以定义render函数,直接写render函数可以不经过编译直接运行,在描述这点前,我们先介绍一下render, 按vue官方文档来说,render是字符串模板的一种替代,可以让你利用JavaScript的丰富表达力来完全编程式地声明组件最终的渲染输出, 预编译的模板如果单文件组件中的模板,会在构建时被编译为render选项,如果一个组件中同时存在render和template,则render将具有更高的优先级,

我们可以看,如果是让vue实时的编译内嵌vue语法的dom,会被编译成什么样子,

联想截图_20260210120439.png

可以看到,当你在dom嵌入vue语法的时候,没有生成render函数,这个时候就需要你引入的cdn的vue版本中包含编译器,如果是编译的SFC模板结果是什么样的?

联想截图_20260210120639.png

可以看到生成了render函数,这个render函数是可以直接被运行时的vue包执行的,不需要加入编译器,所以可以看到有没有render是两回事,有render就直接使用render,没有render,那么就会读取template字符串, 这个时候就需要编译器了 , 我们在这里尝试把上面在选项式API部分里写的例子用render函数写一遍, vue给我们提供了一个名为h函数的api,专门用于给开发者手动的编写render函数,具体可以看 [h函数](渲染选项 | Vue.js), 改写后的例子如下

const h = Vue.h;

const App = {
  data() {
    return {
      count: 0
    }
  },
  render(){
    return h("div", {
      class: "counter"
    },  [
      h("span", {
        onClick: () => this.decrement()
      }, "-"),
      h("p", null, this.count),
      h("span", {
        onClick: () => this.increment()
      }, "+")
    ]);
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  },
  mounted() {
    console.log("挂载");
  }
};

const app = Vue.createApp(App);


app.mount("#app");

//index.js

我们可以看它在SFC中编译后的结果是什么样的, 联想截图_20260210125956.png

可以看到,SFC编译插件基于没有做什么更改,我们可以直接把这个App组件配合vue运行时的版本使用,看下面的html代码

  <style>
    .counter {
      display: flex;
      align-items: center;
      justify-content: center;
      column-gap: 15px;
      width: 120px;
      margin: 100px;
      padding: 10px;
      border-radius: 8px;
      background-color: #f5f5f5;
      user-select: none;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div id="app">

  </div>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.5.22/vue.runtime.global.js"></script>
<script src="./index.js" type="module"></script>
</body>

template

在上面我们我们讨论template的时候说,如果根组件没有template和render, 那么根组件会把app.mount("#app")中#app代表的元素的innerHTML作为根组件的template,那么非根组件呢,非根组件就没有template了,那么非根组件怎么从html模板上获得template字符串呢,template接受两种形式,一种就是内嵌vue语法的描述dom的字符串,一种允许你将字符串以#开头,它会被vue作为querySelector选择器使用,将选中的元素的innerHTML作为模板字符字符串,这就让我们可以使用原生的template元素的书写源模板如下

  <style>
    .counter {
      display: flex;
      align-items: center;
      justify-content: center;
      column-gap: 15px;
      width: 120px;
      margin: 100px;
      padding: 10px;
      border-radius: 8px;
      background-color: #f5f5f5;
      user-select: none;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div id="app">
  </div>

  <template id="temp">
    <p id="temp">当前的count为: {{count}}</p>
  </template>
  <script src="./node_modules/vue/dist/vue.global.js"></script>
  <script src="./index.js" type="module"></script>
const Compo = {
  props:{
      count: 0
  },
  template: "#temp"
};


const App = {
  data() {
    return {
      count: 0
    }
  },
  template: `<div class="counter">
      <span @click="decrement">-</span>
      <p>{{count}}</p>
      <span @click="increment">+</span>
    </div>
    <Compo :count></Compo>`,
  components: {
    Compo
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  },
  mounted() {
    console.log("挂载");
  }
};



const app = Vue.createApp(App);


app.mount("#app");

这就让你在原生的HTML上也具有了组件化的能力,我们继续重构上面的代码,看看如果是在html上使用组件化是怎样的, 查看以下的例子

  <style>
    .counter {
      display: flex;
      align-items: center;
      justify-content: center;
      column-gap: 15px;
      width: 120px;
      margin: 100px;
      padding: 10px;
      border-radius: 8px;
      background-color: #f5f5f5;
      user-select: none;
      cursor: pointer;
    }
  </style>
</head>

<body>
  <div id="app">

  </div>

  <!-- App组件 -->
  <template id="appTemplate">
    <div class="counter">
      <span @click="decrement">-</span>
      <p>{{count}}</p>
      <span @click="increment">+</span>
    </div>
    <Compo :count></Compo>
  </template>


  <!-- Compo 组件 -->
  <template id="temp">
    <p id="temp">当前的count为: {{count}}</p>
  </template>


  <script src="./node_modules/vue/dist/vue.global.js"></script>
  <script src="./index.js" type="module"></script>
const Compo = {
  props:{
      count: 0
  },
  template: "#temp"
};

const App = {
  data() {
    return {
      count: 0
    }
  },
  template: "#appTemplate",
  components: {
    Compo
  },
  methods: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    }
  },
  mounted() {
    console.log("挂载");
  }
};

const app = Vue.createApp(App);

app.mount("#app");