.vue 文件是如何编译成浏览器可执行的文件

115 阅读4分钟

了解如何编译之前, 先介绍一个重要但经常被忽略的知识

  • 进入Vue2官网, 点击”起步”默认会进入”介绍”(见下图)
  • 这次我们从”真正的第一项” — 安装开始看起

Untitled.png

安装 — Vue.js

  • 其中”安装”介绍了关于Vue的各种版本

Untitled 1.png

  • 同时介绍了表格里的”术语”

    💡 术语
    • 完整版:同时包含编译器和运行时的版本。

    • 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。

    • 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。

  • 这里提到的”编译器”就是编译 .vue文件 的方法, 见下方代码

    <div id="app">
        <div>{{ a }}</div>
    </div>

    <script src="<https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js>"></script>
    <script>
        const vm = new Vue({
          el: '#app',
          data: {
            a: 'hello world',
          },
        })
    </script>
  • 以上是一段简单的Vue2代码, 打开浏览器可以正确显示出 “hello world”

  • 可以发现 <div>{{ a }}</div> 不是浏览器支持的语法. 所以实际上能显示 hello world, 并不是由以上模板代码直接在浏览器执行的结果, 而是通过”编译器”编译, 再由浏览器执行

  • 接下来用代码为大家演示Vue2是如何将上面代码编译的

  • 首先必须引入”完整版”

    <script src="<https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js>"></script>
    
  • 打开页面, 在控制台可以发现方法: Vue.compile

Untitled 2.png

  • 将刚才的 template 代码放到编译方法里执行, 结果如下

直接输出结果 Untitled 3.png

.render 显示函数内容 Untitled 4.png

点击 VM183:1 显示函数内容 Untitled 5.png

  • 可以看到 template 被编译成 JavaScript

    💡 简写的函数名分别是:
    • _c = createElement

Untitled 6.png

*   `_v` = `createTextVNode`

Untitled 7.png

*   `_s` = `toString`

PS: 这里的 `toString` 是Vue实现的:

Untitled 8.png

</aside>
  • 编译结果可以翻译成

    <div id="app" />
    
    <script>
    const vm = new Vue({
    		el: '#app',
        name: 'App',
        data: () => ({
          a: 'hello world!!!',
        }),
        render() {
    			const {createElement, createTextNode, toString, a} = this
    
    			return createElement('div', [createTextNode(toString(a))])
    		}
    })
    <script>
    
  • 通过这些JavaScript就可以顺利展示出 “hello world”

  • 以上便是在 .html 文件中使用template 开发, 由 Vue2完整版 进行编译的过程

但通常项目中是使用Webpack打包, 再由浏览器引入这些js, css等文件, 那么Webpack是如何对.vue文件进行编译的?

  • vue-loader

    💡 Vue Loader 是一个 **[webpack](https://webpack.js.org/)** 的 loader,它允许你以一种名为**[单文件组件 (SFCs)](https://vue-loader.vuejs.org/zh/spec.html)** 的格式撰写 Vue 组件
  • 接下来通过vue-loader的源码来了解是如何实现编译的

  • 打开 vue-loader/src/index.ts

  • .vue文件分为 template, script, style 三个部分, 而实际必须通过编译才能在浏览器执行的部分只有template, 所以我们在看 vue-loader 源码时可以重点关注 “template” 关键词

Untitled 9.png

  • 如上图所示, 在源码搜索 template 关键字可以找到这段处理 script, template, style 的代码, 仔细观察发现这三段都在用 descriptor 对象, 接下来搜索 descriptor, 找到这个变量在哪里生成的

Untitled 10.png

  • 再来找 parse 方法

Untitled 11.png

  • parse 方法从 vue/compiler-sfc 中引用
💡 **@vue/compiler-sfc**

core/packages/compiler-sfc at main · vuejs/core

  • 至此, 了解到 vue-loader 是通过 @vue/compiler-sfc ****的 parse 方法解析 .vue 文件, 从中解析出script, template, style

  • 用一段nodejs代码进行验证

    // App.vue
    <template>
      <div id="app">
        <div>{{ a }}</div>
        <input type="text" v-model="a">
      </div>
    </template>
    
    <script>
    
    export default {
      name: 'App',
      data: () => ({
        a: 'hello world'
      }),
    }
    </script>
    
    // build.js
    const { parse } = require('@vue/compiler-sfc')
    const fs = require('fs')
    
    const data = fs.readFileSync('./App.vue')
    
    const { descriptor } = parse(data.toString())
    
    // 结果包含很多项, 本文档中只展示重点字段
    console.log(Object.keys(descriptor))
    
  • 运行 build.js

    [
      'styles',
    	'template',
      'script',
      // ...
    ]
    
  • 通过以上代码可以解析出 .vue 文件的三部分, 接下来开始解析 template

  • vue-template-compiler 可以将 template 编译成 Javascript

    vue/packages/vue-template-compiler at dev · vuejs/vue

Untitled 12.png

Untitled 13.png

  • 继续修改 build.js, 根据文档加上 compile 的部分
// build.js
const { parse } = require('@vue/compiler-sfc')
const fs = require('fs')
const compiler = require('vue-template-compiler')

const data = fs.readFileSync('./App.vue')

const { descriptor } = parse(data.toString())
// console.log(Object.keys(descriptor))

console.log(compiler.compile(descriptor.template.content).render)
  • 运行结果如下:
with(this){
  return _c(
    'div',
    {attrs:{"id":"app"}},
    [
      _c('div',[_v(_s(a))]),
      _v(" "),
      _c(
        'input',
        {
          directives:[{name:"model",rawName:"v-model",value:(a),expression:"a"}],
          attrs:{"type":"text"},
          domProps:{"value":(a)},
          on:{
            "input": function($event){
              if($event.target.composing) return;
              a=$event.target.value
            }
          }
        }
      )
    ]
  )
}
  • 再将以上代码放到 .html 的 new Vue(...) 方法中进行验证
<div id="app"/>

<script src="<https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js>"></script>
<script>
  const vm = new Vue({
    name: 'App',
    data: () => ({
      a: 'hello world!!!',
    }),
    render() {
      const {_c, _v, _s, a} = this

      return _c(
        'div',
        {attrs: {'id': 'app'}},
        [
          _c('div', [_v(_s(a))]),
          _v(' '),
          _c(
            'input',
            {
              directives: [{name: 'model', rawName: 'v-model', value: (a), expression: 'a'}],
              attrs: {'type': 'text'},
              domProps: {'value': (a)},
              on: {
								// 注意这里的 this, 需要改成箭头函数
                'input': ($event) => {
                  if ($event.target.composing) return
                  this.a = $event.target.value
                },
              },
            },
          ),
        ],
      )
    },
  }).$mount('#app')
</script>
  • 以上便是 Webpack 如何将 .vue 文件编译成浏览器可执行文件的过程

扩展内容1: 可以在 input事件看到 $event.target.composing

  • composing 是 Vue2 实现的自定义属性, 为了解决中文, 日文, 韩文等需要弹出输入选框的语言, 在选中前录入输入内容, 造成统计总字符数等判断时的错误

Untitled 14.png

  • 可通过以下代码验证

Untitled 15.png

Untitled 16.png

// window.kw 为 www.baidu.com 的主输入框
window.kw.addEventListener('compositionstart', () => console.log('open'))
window.kw.addEventListener('compositionend', () => console.log('close'))

扩展内容2: 解析 template 的过程

在解析过程中 vue-template-compiler 库调用 @vue/compiler-dom 逐行解析 template , 可查看源码进一步了解: