当import vue文件时,发生了什么

4,021 阅读4分钟

一、背景

​ vue工程中,父组件想要使用子组件就需要通过import的方式引入子组件并在components选项中进行注册,import xxx from 'xxxx.vue'是vue工程中最常用的语法之一了,但是可能很少有人想过,在import vue文件时,背后到底是如何处理的。

​ 今天我们就来瞧一瞧,import vue文件时背后的处理历程。

二、webpack 的loader机制

​ 我们知道原生import是无法import vue文件的,那么为什么我们在代码中可以使用呢?

​ 这是因为webpack的loader机制,通过配置文件类型对应的loader,可以使用指定的loader对文件先进行一次处理,再交给原生的import语法去处理。那么我们的vue文件对应的loader就是vue-loader啦。

​ 我们可以通过webpack的配置来确认这一点:

// 项目中的webpack配置
module: {
    rules: [
      // 匹配到vue文件,就使用vue-loader处理
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      }]
}

三、vue-loader处理vue文件

​ 前面已经知道,vue文件在import时会经过vue-loader处理,那么vue-loader是如何处理的呢?

​ 这里,我们首先看一下vue-loader处理完的结果是怎么样的。

​ 为了看到vue-loader的处理结果,需要我们修改node_modules中vue-loader的代码,将处理结果打印出来:

// vue-loader-13.7.3版本
// vue-loader\lib\loader.js
  // line 447,添加console.log,打印输出
  console.log('vue-loader output =>',output)
  return output

​ 修改完成之后,需要重新启动工程,然后在vscode 的控制台就可以看到打印的结果了

// vue-loader处理import App from 'App.vue'的结果
function injectStyle (ssrContext) {
    if (disposed) return
    require("!!vue-style-loader!css-loader?{\"sourceMap\":true}!../node_modules/vue-loader/lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-7ba5bd90\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector?type=styles&index=0!./App.vue")
}
var normalizeComponent = require("!../node_modules/vue-loader/lib/component-normalizer")
/* script */
export * from "!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue"
import __vue_script__ from "!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue"
/* template */
import __vue_template__ from "!!../node_modules/vue-loader/lib/template-compiler/index?{\"id\":\"data-v-7ba5bd90\",\"hasScoped\":false,\"transformToRequire\":{\"video\":[\"src\",\"poster\"],\"source\":\"src\",\"img\":\"src\",\"image\":\"xlink:href\"},\"buble\":{\"transforms\":{}}}!../node_modules/vue-loader/lib/selector?type=template&index=0!./App.vue"
/* template functional */
var __vue_template_functional__ = false
/* styles */
var __vue_styles__ = injectStyle
/* scopeId */
var __vue_scopeId__ = null
/* moduleIdentifier (server only) */
var __vue_module_identifier__ = null
var Component = normalizeComponent(
    __vue_script__,
    __vue_template__,
    __vue_template_functional__,
    __vue_styles__,
    __vue_scopeId__,
    __vue_module_identifier__
)
Component.options.__file = "src/App.vue"

export default Component.exports

​ 可以看到vue-loader的输出是一段js代码,这段js代码会被原生import所执行。

​ 通过浏览这段js代码,大致可以看出:首先按照vue文件的三个部分分别进行处理,即template部分、script部分和style部分,然后使用component-normalizer.js中的normalizeComponent方法进行汇总处理。

​ 接下来我们先来看vue文件的三个部分是如何处理的。

3.1 template部分

​ 先看template部分的处理代码

// vue-loader输出结果中处理template部分的代码
import __vue_template__ from "!!../node_modules/vue-loader/lib/template-compiler/index?{\"id\":\"data-v-7ba5bd90\",\"hasScoped\":false,\"transformToRequire\":{\"video\":[\"src\",\"poster\"],\"source\":\"src\",\"img\":\"src\",\"image\":\"xlink:href\"},\"buble\":{\"transforms\":{}}}!../node_modules/vue-loader/lib/selector?type=template&index=0!./App.vue"

​ 这行代码看起来有点长,可以拆分成两段解析:

// 第一段
!!../node_modules/vue-loader/lib/template-compiler/index?{\"id\":\"data-v-7ba5bd90\",\"hasScoped\":false,\"transformToRequire\":{\"video\":[\"src\",\"poster\"],\"source\":\"src\",\"img\":\"src\",\"image\":\"xlink:href\"},\"buble\":{\"transforms\":{}}}
                                                          
//第二段
!../node_modules/vue-loader/lib/selector?type=template&index=0!./App.vue

​ 第二段代码是使用vue-loader/lib/selector.js来处理App.vue文件,那么这个selector.js是干啥的呢,看一下vue-loader/lib/selector.js文件中的注释

// vue-loader/lib/selector.js 注释

// this is a utility loader that takes a *.vue file, parses it and returns
// the requested language block, e.g. the content inside <template>, for
// further processing.

​ selector.js中的注释说明了,这个方法是用于从vue文件中读取对应标签中的内容,因此第二段代码的处理结果就是读取App.vue中template标签中的代码。

​ 再看第一段代码,虽然看起来还是有点长,但是可以看出来是调用vue-loader/lib/template-compiler/index.js来处理,剩下的部分是处理的参数。

​ 那么vue-loader/lib/template-compiler/index.js是做什么的呢,实际上就是进行模板编译的,通过引入vue-template-compiler这个包进行编译,而vue-template-compiler就是从vue源码生成的,所以vue-loader中的模板编译和vue源码中的模板编译是一致的。经过模板编译后,输出的结果就是渲染函数。可以通过打印结果的方式来确认:

// vue-loader 处理App.vue的template部分输出结果
var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", { attrs: { id: "app" } }, [_c("HelloWorld")], 1)
}
var staticRenderFns = []
render._withStripped = true
var esExports = { render: render, staticRenderFns: staticRenderFns }
export default esExports
if (module.hot) {
  module.hot.accept()
  if (module.hot.data) {
    require("vue-hot-reload-api")      .rerender("data-v-7ba5bd90", esExports)
  }
}

3.2:script部分

​ 接下来看一下script部分的处理:

// vue-loader输出结果中处理script部分的代码
import __vue_script__ from "!!babel-loader!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue

​ 这行代码看起来比较短,但是也是有2段处理的

//第一段
!!babel-loader
//第二段
!../node_modules/vue-loader/lib/selector?type=script&index=0!./App.vue

​ 第二段与template中的处理一致,只是这里取的是script部分的代码

​ 第一段是用babel-loader对取到的script部分代码进行处理

​ script部分的处理很清晰,就是用babel处理vue文件中script标签内的代码,可以在component-normalizer.js中打断点查看结果

3.3:style部分

​ 接着看style部分的处理

// vue-loader输出结果中处理style部分的代码
function injectStyle (ssrContext) {
    if (disposed) return
    require("!!vue-style-loader!css-loader?{\"sourceMap\":true}!../node_modules/vue-loader/lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-7ba5bd90\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector?type=styles&index=0!./App.vue")
}

​ 与template和script不同的是,style部分是定义了一个处理函数injectStyle,在此函数中进行style部分处理,函数的调用时机后面再分析

​ 这里先看一下函数中是如何处理style标签内的代码的,分成三段处理

//第一段
!!vue-style-loader
//第二段
!css-loader?{\"sourceMap\":true}!../node_modules/vue-loader/lib/style-compiler/index?{\"vue\":true,\"id\":\"data-v-7ba5bd90\",\"scoped\":false,\"hasInlineConfig\":false}
//第三段
!../node_modules/vue-loader/lib/selector?type=styles&index=0!./App.vue"

​ 第三段与template中的处理一致,只是这里取的是style部分的代码

​ 第二段是用css-loader对取到的style部分代码进行处理

​ 第一段是用vue-style-loader对css-loader处理过的代码再进行处理

3.4:normalizeComponent函数处理

​ 分析了vue-loader对vue文件中三个部分的处理后,normalizeComponent相当于是对三部分的处理做综合性的处理。

// vue-loader源码 lib\component-normalizer.js
module.exports = function normalizeComponent (
  rawScriptExports,
  compiledTemplate,
  functionalTemplate,
  injectStyles,
  scopeId,
  moduleIdentifier /* server only */
) {
  var esModule
  var scriptExports = rawScriptExports = rawScriptExports || {}

  // ES6 modules interop
  var type = typeof rawScriptExports.default
  if (type === 'object' || type === 'function') {
    esModule = rawScriptExports
    scriptExports = rawScriptExports.default
  }

  // Vue.extend constructor export interop
  var options = typeof scriptExports === 'function'
    ? scriptExports.options
    : scriptExports

  // render functions
  if (compiledTemplate) {
    options.render = compiledTemplate.render
    options.staticRenderFns = compiledTemplate.staticRenderFns
    options._compiled = true
  }

  // functional template
  if (functionalTemplate) {
    options.functional = true
  }

  // scopedId
  if (scopeId) {
    options._scopeId = scopeId
  }

  var hook
  if (moduleIdentifier) { // server build
    hook = function (context) {
      // 2.3 injection
      context =
        context || // cached call
        (this.$vnode && this.$vnode.ssrContext) || // stateful
        (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
      // 2.2 with runInNewContext: true
      if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
        context = __VUE_SSR_CONTEXT__
      }
      // inject component styles
      if (injectStyles) {
        injectStyles.call(this, context)
      }
      // register component module identifier for async chunk inferrence
      if (context && context._registeredComponents) {
        context._registeredComponents.add(moduleIdentifier)
      }
    }
    // used by ssr in case component is cached and beforeCreate
    // never gets called
    options._ssrRegister = hook
  } else if (injectStyles) {
    hook = injectStyles
  }

  if (hook) {
    var functional = options.functional
    var existing = functional
      ? options.render
      : options.beforeCreate

    if (!functional) {
      // inject component registration as beforeCreate hook
      options.beforeCreate = existing
        ? [].concat(existing, hook)
        : [hook]
    } else {
      // for template-only hot-reload because in that case the render fn doesn't
      // go through the normalizer
      options._injectStyles = hook
      // register for functioal component in vue file
      options.render = function renderWithStyleInjection (h, context) {
        hook.call(context)
        return existing(h, context)
      }
    }
  }

  return {
    esModule: esModule,
    exports: scriptExports,
    options: options
  }
}

​ 看一下经过normalizeComponent的处理后的App.vue的exports

​ 这个就是import vue文件时最终导入的对象,相比于前面看到的script部分的输出:

  1. 多了render和staticRenderFns属性,这个是从template部分的编译结果中取得的;
  2. 还多了一个beforeCreate钩子属性,其值是一个数组,包含了injectStyle函数,当组件实例化时,触发beforeCreate钩子,injectStyle被执行,组件样式进而生效。

四、总结

​ 当我们import vue文件时,会触发webpack的loader机制,使用vue-loader对vue文件进行处理,vue-loader将vue按照标签分成分template、script、style三部处理:template部分引入vue-template-compiler进行模板编译,输出渲染函数;script部分通过babel-loader进行编译,生成浏览器可直接运行的代码;style部分没有直接处理,而是定义了一个处理函数,处理函数中先用css-loader处理,再用vue-style-loader进行处理;最后再使用normalizeComponent方法对三部分进行综合性处理,输出最终的结果。