理解vue-loader
事情的起源是被人问到,一个以.vue结尾的文件,是如何被编译然后运行在浏览器中的?突然发现,对这一块模糊的很,而且看mpvue的文档,甚至小程序之类的都是实现了自己的loader,所以十分必要抽时间去仔细读一读源码,顺便总结一番。
首先说结论:
一、vue-loader是什么
简单的说,他就是基于webpack的一个的loader,解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理,核心的作用,就是提取,划重点。
至于什么是webpack的loader,其实就是用来打包、转译js或者css文件,简单的说就是把你写的代码转换成浏览器能识别的,还有一些打包、压缩的功能等。
这是一个.vue单文件的demo
<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
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部分
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。
/** * 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这个模块
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的作用
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函数贴到下边
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的更多相关文章
- vue 快速入门 系列 —— vue loader 上
其他章节请看: vue 快速入门 系列 vue loader 上 通过前面"webpack 系列"的学习,我们知道如何用 webpack 实现一个不成熟的脚手架,比如提供开发环境和 ...
- vue 快速入门 系列 —— vue loader 扩展
其他章节请看: vue 快速入门 系列 vue loader 扩展 在vue loader一文中,我们学会了从零搭建一个简单的,用于单文件组件开发的脚手架.本篇将在此基础上继续引入一些常用的库:vue ...
- 理解vue中的scope的使用
理解vue中的scope的使用 我们都知道vue slot插槽可以传递任何属性或html元素,但是在调用组件的页面中我们可以使用 template scope="props"来获取 ...
- 理解Vue中的Render渲染函数
理解Vue中的Render渲染函数 VUE一般使用template来创建HTML,然后在有的时候,我们需要使用javascript来创建html,这时候我们需要使用render函数.比如如下我想要实现 ...
- 深入理解vue
一 理解vue的核心理念 使用vue会让人感到身心愉悦,它同时具备angular和react的优点,轻量级,api简单,文档齐全,简单强大,麻雀虽小五脏俱全. 倘若用一句话来概括vue,那么我首先想到 ...
- Vue Loader
介绍 允许为 Vue 组件的每个部分使用其它的 webpack loader,例如在 <style> 的部分使用 Sass 和在 <template> 的部分使用 Pug(模板 ...
- 深入理解 Vue 组件
深入理解 Vue 组件 组件使用中的细节点 使用 is 属性,解决组件使用中的bug问题 <!DOCTYPE html> <html lang="en"> ...
- vue系列---理解Vue中的computed,watch,methods的区别及源码实现(六)
_ 阅读目录 一. 理解Vue中的computed用法 二:computed 和 methods的区别? 三:Vue中的watch的用法 四:computed的基本原理及源码实现 回到顶部 一. 理解 ...
- 深入理解vue的watch
深入理解vue的watch vue中的wactch可以监听到data的变化,执行定义的回调,在某些场景是很有用的,本文将深入源码揭开watch额面纱 前言 watch的使用 watch的多种使用方式 ...
- 手摸手带你理解Vue的Computed原理
前言 computed 在 Vue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利.那么本文就来带大家全面理解 computed 的内部原理以及工作流程. 在这之前,希望你能 ...
随机推荐
- SQL Server Update 所有表的某一列(列名相同,类型相同)数值
); WITH T AS (SELECT SchemaName = c.TABLE_SCHEMA, TableName = c.TABLE_NAME, ColumnName = c.COLUMN_NA ...
- webmethod基本认知
六种控件统称flow step insert/invoke 插入services,类似调用函数 BRANCH 分支结构 参数名在switch定义 子参数以label确定 注意:确保label唯一,否则 ...
- ML:梯度下降(Gradient Descent)
现在我们有了假设函数和评价假设准确性的方法,现在我们需要确定假设函数中的参数了,这就是梯度下降(gradient descent)的用武之地. 梯度下降算法 不断重复以下步骤,直到收敛(repeat ...
- delphi 实现微信开发(1) (使用kbmmw web server)
原文地址:delphi 实现微信开发(1)作者:红鱼儿 大体思路: 1.用户向服务号发消息,(这里可以是个菜单项,也可以是一个关键词,如:注册会员.) 2.kbmmw web server收到消息,生 ...
- 一顶博士帽能带来什么——访GOOGLE公司中国区总裁李开复
在读了博士生远潇给本报的来信后,GOOGLE公司中国区总裁李开复说,有这些困惑和担心,实际上是很多博士生们在读博士之前并没有认真地想过,自己是不是能耐得住寂寞做学问,是不是能抵御来自物质世界的诱惑 ...
- 简单封装 Delphi 的 DirectX类
var CreatorRenderer : TCreatorRenderer; Form1: TForm1; 窗体代码: {$R *.dfm} procedure TForm1.FormCreate ...
- 仿写confirm和alert弹框
在工作中,我们常常会遇到原生的样式感觉比较丑,又和我们做的项目风格不搭.于是就有了仿写原生一些组件的念头,今天我就带大家仿写一下confirm和alert样式都可以自己修改. 有些的不好的地方请指出来 ...
- 玩转Java多线程(乒乓球比赛)
转载请标明博客的地址 本人博客和github账号,如果对你有帮助请在本人github项目AioSocket上点个star,激励作者对社区贡献 个人博客:https://www.cnblogs.com/ ...
- 系统学习 Java IO (一)----输入流和输出流 InputStream/OutputStream
目录:系统学习 Java IO ---- 目录,概览 InputStream 是Java IO API中所有输入流的父类. 表示有序的字节流,换句话说,可以将 InputStream 中的数据作为有序 ...
- Lombok简介及入门使用 (转载)
Lombok简介及入门使用 lombok既是一个IDE插件,也是一个项目要依赖的jar包. Intellij idea开发的话需要安装Lombok plugin,同时设置 Setting -> ...