事情的起源是被人问到,一个以.vue结尾的文件,是如何被编译然后运行在浏览器中的?突然发现,对这一块模糊的很,而且看mpvue的文档,甚至小程序之类的都是实现了自己的loader,所以十分必要抽时间去仔细读一读源码,顺便总结一番。

首先说结论:

一、vue-loader是什么

简单的说,他就是基于webpack的一个的loader,解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理,核心的作用,就是提取,划重点。

至于什么是webpack的loader,其实就是用来打包、转译js或者css文件,简单的说就是把你写的代码转换成浏览器能识别的,还有一些打包、压缩的功能等。

这是一个.vue单文件的demo

vue文件式例 折叠源码
<template>
  <div class="example">{{ msg }}</div>
</template>
 
<script>
export default {
  data () {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>
 
<style>
.example {
  color: red;
}
</style>

二、 vue-loader 的作用(引用自官网)

  • 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug;
  • 允许在一个 .vue 文件中使用自定义块,并对其运用自定义的 loader 链;
  • 使用 webpack loader 将 <style> 和 <template> 中引用的资源当作模块依赖来处理;
  • 为每个组件模拟出 scoped CSS;
  • 在开发过程中使用热重载来保持状态。

三、vue-loader的实现

先找到了vue-laoder在node_modules中的目录,由于源码中有很多对代码压缩、热重载之类的代码,我们定一个方向,看看一个.vue文件在运行时,是被vue-loader怎样处理的

既然vue-loader的核心首先是将以为.vue为结尾的组件进行分析、提取和转换,那么首先我们要找到以下几个loader

  • selector–将.vue文件解析拆分成一个parts对象,其中分别包含style、script、template
  • style-compiler–解析style部分
  • template-compiler 解析template部分
  • babel-loader-- 解析script部分,并转换为浏览器能识别的普通js

首先在loader.js这个总入口中,我们不关心其他的,先关心这几个加载的loader,从名字判断这事解析css、template的关键

3.1 首先是selector

selector 折叠源码
var path = require('path')
var parse = require('./parser')
var loaderUtils = require('loader-utils')
 
 
module.exports = function (content) {
  this.cacheable()
  var query = loaderUtils.getOptions(this) || {}
  var filename = path.basename(this.resourcePath)
  // 将.vue文件解析为对象parts,parts包含style, script, template
  var parts = parse(content, filename, this.sourceMap)
  var part = parts[query.type]
  if (Array.isArray(part)) {
    part = part[query.index]
  }
  this.callback(null, part.content, part.map)
}

selector的最主要的功能就是拆分parts,这个parts是一个对象,用来盛放将.vue文件解析出的style、script、template等模块,他调用了方法parse。

parse.js部分

parse.js 折叠源码
var compiler = require('vue-template-compiler')
var cache = require('lru-cache')(100)
var hash = require('hash-sum')
var SourceMapGenerator = require('source-map').SourceMapGenerator
 
 
var splitRE = /\r?\n/g
var emptyRE = /^(?:\/\/)?\s*$/
 
module.exports = function (content, filename, needMap) {
  // source-map cache busting for hot-reloadded modules
  // 省略部分代码
  var filenameWithHash = filename + '?' + cacheKey
  var output = cache.get(cacheKey)
  if (output) return output
  output = compiler.parseComponent(content, { pad: 'line' })
  if (needMap) {
  }
  cache.set(cacheKey, output)
  return output
}
 
function generateSourceMap (filename, source, generated) {
  // 生成sourcemap
  return map.toJSON()
}

parse.js其实也没有真正解析.vue文件的代码,只是包含一些热重载以及生成sourceMap的代码,最主要的还是调用了compiler.parseComponent 这个方法,但是compiler并不是vue-loader的方法,而是调用vue框架的parse,这个文件在vue/src/sfc/parser.js中,一层层的揭开面纱终于找到了解析.vue文件的真正处理方法parseComponent。

vue的parse.js 折叠源码
/**
 * Parse a single-file component (*.vue) file into an SFC Descriptor Object.
 */
export function parseComponent (
  content: string,
  options?: Object = {}
 ): SFCDescriptor {
  const sfc: SFCDescriptor = {
    template: null,
    script: null,
    styles: [],
    customBlocks: [] // 当前正在处理的节点
  }
  let depth = 0 // 节点深度
  let currentBlock: ?(SFCBlock | SFCCustomBlock) = null
 
  function start (
    tag: string,
    attrs: Array<Attribute>,
    unary: boolean,
    start: number,
    end: number
  ) {
    // 略
  }
 
  function checkAttrs (block: SFCBlock, attrs: Array<Attribute>) {
    // 略
  }
 
  function end (tag: string, start: number, end: number) {
    // 略
  }
 
  function padContent (block: SFCBlock | SFCCustomBlock, pad: true "line" "space") {
    // 略
  }
  parseHTML(content, {
    start,
    end
  })
 
  return sfc
}

但是令人窒息的是parseHTML才是核心的方法,翻了一下文件,parseHTML是调用的vue源码中的compiler/parser/html-parser.js

 折叠源码
export function parseHTML (html, options) {
  while (html) {
    last = html
    if (!lastTag || !isPlainTextElement(lastTag)) {
      // 这里分离了template
    else {
      // 这里分离了style/script
    }
    // 前进n个字符
    function advance (n) {
        // 略
    }
 
 
    // 解析 openTag 比如 <template>
    function parseStartTag () {
        // 略
    }
    // 处理 openTag
    function handleStartTag (match) {
        // 略
        if (options.start) {
        options.start(tagName, attrs, unary, match.start, match.end)
        }
    }
    // 处理 closeTag
    function parseEndTag (tagName, start, end) {
        // 略
        if (options.start) {
        options.start(tagName, [], false, start, end)
        }
        if (options.end) {
        options.end(tagName, start, end)
        }
    }
  }
}

这个parseHTML的主要组成部分就是解析传入的template标签,同时分离style和script

3.2 解析了template 接下来再看style样式部分的解析,在源码中调用的是style-compiler这个模块

style-compiler模块 折叠源码
var postcss = require('postcss')
module.exports = function (css, map) {
  var query = loaderUtils.getOptions(this) || {}
  var vueOptions = this.options.__vueOptions__
 
  if (!vueOptions) {
    if (query.hasInlineConfig) {
      this.emitError(
        `\n  [vue-loader] It seems you are using HappyPack with inline postcss ` +
        `options for vue-loader. This is not supported because loaders running ` +
        `in different threads cannot share non-serializable options. ` +
        `It is recommended to use a postcss config file instead.\n` +
        `\n  See http://vue-loader.vuejs.org/en/features/postcss.html#using-a-config-file for more details.\n`
      )
    }
    vueOptions = Object.assign({}, this.options.vue, this.vue)
  }
 
  // use the same config loading interface as postcss-loader
  loadPostcssConfig(vueOptions.postcss).then(config => {
    var plugins = [trim].concat(config.plugins)
    var options = Object.assign({
      to: this.resourcePath,
      from: this.resourcePath,
      map: false
    }, config.options)
 
    // add plugin for vue-loader scoped css rewrite
    if (query.scoped) {
      plugins.push(scopeId({ id: query.id }))
    }
 
   // souceMap略
 
    return postcss(plugins)
      .process(css, options)
      .then(function (result) {
        var map = result.map && result.map.toJSON()
        cb(null, result.css, map)
        return null // silence bluebird warning
      })
  }).catch(e => {
    console.log(e)
    cb(e)
  })
}

简单的说,这一部分其实是调用了webpack原有的postcss这个loader,不过值得注意的是在vue中style标签scope的实现

实现的效果,在加了scope的style的文件中,为所设置的样式添加私有属性data,同时css中也加入单独的id,起到不同组件之间css私有的作用

这里调用了scopeId这个方法,是在postcss的基础上自定义的插件,调用postcss-selector-parser这个插件,在css转译后的选择器上生成特殊的id,从而起到隔离css的作用

vue-loader针对postcss的拓展 折叠源码
var postcss = require('postcss')
// 调用postcss-selector-parser 这个基于postcss的css选择器解析插件
var selectorParser = require('postcss-selector-parser')
 
 
module.exports = postcss.plugin('add-id'function (opts) {
  return function (root) {
    root.each(function rewriteSelector (node) {
      if (!node.selector) {
        // handle media queries
        if (node.type === 'atrule' && node.name === 'media') {
          node.each(rewriteSelector)
        }
        return
      }
      node.selector = selectorParser(function (selectors) {
        selectors.each(function (selector) {
          var node = null
          selector.each(function (n) {
            if (n.type !== 'pseudo') node = n
          })
          selector.insertAfter(node, selectorParser.attribute({
            attribute: opts.id
          }))
        })
      }).process(node.selector).result
    })
  }
})

同时在对应的组件标签上,添加自定义的data属性,在vue-loader下的loader.js中

而genId则是生成scopeId的方法,其中调用了基于npm的hash-sum插件,快速生成唯一的哈希值

 折叠源码
var path = require('path')
var hash = require('hash-sum'//此处引用了hash-sum插件
var cache = Object.create(null)
var sepRE = new RegExp(path.sep.replace('\\''\\\\'), 'g')
 
 
module.exports = function genId (file, context, key) {
  var contextPath = context.split(path.sep)
  var rootId = contextPath[contextPath.length - 1]
  file = rootId + '/' + path.relative(context, file).replace(sepRE, '/') + (key || '')
  return cache[file] || (cache[file] = hash(file))
}

而hash-sum生成唯一hash值的基本函数也比较有意思,通过charCodeAt 以及左移运算符产生新的值,最基本的一个fold函数贴到下边

hash-sum 折叠源码
function fold (hash, text) {
  var i;
  var chr;
  var len;
  if (text.length === 0) {
    return hash;
  }
  for (i = 0, len = text.length; i < len; i++) {
    chr = text.charCodeAt(i); // 调用了charCodeAt()这个方法转换为unicode编码
    hash = ((hash << 5) - hash) + chr; // 左移运算符改变hash值
    hash |= 0; // hash = hash | 0;
  }
  return hash < 0 ? hash * -2 : hash;
}

hash-sum还通过嵌套多层fold函数,以及pad、foldObject、foldValue等函数进一步混淆保证hash值的唯一不重复,感兴趣的可以翻看下hash-sum的源码。

3.3 script的处理

vue-loader对于script的处理则要简单一些,因为相对于自定义的程度,需要学习的v-指令,以及vue css中划分的scope,js反而是最通用的。

如果script标签有lang的标签,确保解析方式

根据属性lang的内容,加载使用对应的loader

 折叠源码
function ensureLoader (lang) {
    return lang.split('!').map(function (loader) {
      return loader.replace(/^([\w-]+)(\?.*)?/, function (_, name, query) {
        return (/-loader$/.test(name) ? name : (name + '-loader')) + (query || '')
      })
    }).join('!')
}

理解vue-loader的更多相关文章

  1. vue 快速入门 系列 —— vue loader 上

    其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...

  2. vue 快速入门 系列 —— vue loader 扩展

    其他章节请看: vue 快速入门 系列 vue loader 扩展 在vue loader一文中,我们学会了从零搭建一个简单的,用于单文件组件开发的脚手架.本篇将在此基础上继续引入一些常用的库:vue ...

  3. 理解vue中的scope的使用

    理解vue中的scope的使用 我们都知道vue slot插槽可以传递任何属性或html元素,但是在调用组件的页面中我们可以使用 template scope="props"来获取 ...

  4. 理解Vue中的Render渲染函数

    理解Vue中的Render渲染函数 VUE一般使用template来创建HTML,然后在有的时候,我们需要使用javascript来创建html,这时候我们需要使用render函数.比如如下我想要实现 ...

  5. 深入理解vue

    一 理解vue的核心理念 使用vue会让人感到身心愉悦,它同时具备angular和react的优点,轻量级,api简单,文档齐全,简单强大,麻雀虽小五脏俱全. 倘若用一句话来概括vue,那么我首先想到 ...

  6. Vue Loader

    介绍 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug(模板 ...

  7. 深入理解 Vue 组件

    深入理解 Vue 组件 组件使用中的细节点 使用 is 属性,解决组件使用中的bug问题 <!DOCTYPE html> <html lang="en"> ...

  8. vue系列---理解Vue中的computed,watch,methods的区别及源码实现(六)

    _ 阅读目录 一. 理解Vue中的computed用法 二:computed 和 methods的区别? 三:Vue中的watch的用法 四:computed的基本原理及源码实现 回到顶部 一. 理解 ...

  9. 深入理解vue的watch

    深入理解vue的watch vue中的wactch可以监听到data的变化,执行定义的回调,在某些场景是很有用的,本文将深入源码揭开watch额面纱 前言 watch的使用 watch的多种使用方式 ...

  10. 手摸手带你理解Vue的Computed原理

    前言 computed 在 Vue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利.那么本文就来带大家全面理解 computed 的内部原理以及工作流程. 在这之前,希望你能 ...

随机推荐

  1. 【python】python调用adb

    本期分享下python如何调用adb: 1.导入os模块 import os 2.python中调用adb命令语法 print("显示机型信息:") os.system('adb ...

  2. 1.预处理,生成预编译文件(.文件): Gcc –E hello.c –o hello.i 2.编译,生成汇编代码(.s文件): Gcc –S hello.i –o hello.s 3.汇编,生成目标文件(.o文件): Gcc –c hello.s –o hello.o 4.链接,生成可执行文件: linux笔记

    1 动态查看日志 tail -f filename tail -1000f filename 2 解压当前目录内容为xxx.zip  zip -r xxx.zip ./* 3 查看内存使用情况 fre ...

  3. Use Spring @Scheduled To Achieve Timing Task

    Today I have a chance to use the @Scheduled to achieve a timing task.It is useful.Anyway,it was the ...

  4. Long Shadows Generate是一款在线使用纯CSS3实现长阴影的效果,一款强大的扁平化长投影制造器。

    Long Shadows Generate是一款在线使用纯CSS3实现长阴影的效果,一款强大的扁平化长投影制造器. Long Shadows Generate 彩蛋爆料直击现场 Long Shadow ...

  5. idea 导入maven项目

    1.import project 2.选择maven项目 3.选择第二个external moudle,选择maven, 4.点击next,一次点击1,2,3,4 5.设置maven环境 6.点击ok ...

  6. Hadoop集群(第2期)虚拟机网卡的介绍和配置

    很多人安装虚拟机的时候,经常遇到不能上网的问题,而vmware有三种网络模式,对初学者来说也比较眼花聊乱,今天我就来基于虚拟机3种网络模式,帮大家普及下虚拟机上网的背景知识 虚拟机网络模式 无论是vm ...

  7. Spring Boot配置篇(基于Spring Boot 2.0系列)

    1:概述 SpringBoot支持外部化配置,配置文件格式如下所示: properties files yaml files environment variables command-line ar ...

  8. abp(net core)+easyui+efcore仓储系统——领域层创建实体(三)

    abp(net core)+easyui+efcore仓储系统目录 abp(net core)+easyui+efcore仓储系统——ABP总体介绍(一) abp(net core)+easyui+e ...

  9. Python自学day-14

    一.默认创建的HTML5文件 <!DOCTYPE html> <html lang="en"> <head> <meta charset= ...

  10. Hadoop 学习之路(三)—— 分布式计算框架 MapReduce

    一.MapReduce概述 Hadoop MapReduce是一个分布式计算框架,用于编写批处理应用程序.编写好的程序可以提交到Hadoop集群上用于并行处理大规模的数据集. MapReduce作业通 ...