-
hot / hmr doesn't compile Vue at all?
2020-11-23 12:35:05neither <code>hot</code> nor <code>hmr</code> seems to be compiling the <code>.vue</code> files, as those changes are not shown; not even initially... <p>I can see webpack correctly recompiling once ... -
Compile .vue files with Slate
2020-12-06 07:47:02t compile .vue files yet. I get the following error. <img width="492" alt="capture d ecran 2018-11-07 a 23 06 14" src="https://img-blog.csdnimg.cn/img_convert/3cb61154f969356b0b2d7090c41c2137.png" />... -
vue几种编译_GitHub - yangjunlong/compile-vue-demo: 编译vue单文件组件
2020-12-24 10:35:38编译vue单文件组件背景Vue官方定义了一种名叫单文件组件(SFC)规范的*.vue文件,用类 HTML 语法描述一个 Vue 组件。每个 .vue 文件包含三种类型的顶级语言块 、 和 ,还允许添加可选的自定义块:{{ msg }}export ...编译vue单文件组件
背景
Vue官方定义了一种名叫单文件组件(SFC)规范的*.vue文件,用类 HTML 语法描述一个 Vue 组件。每个 .vue 文件包含三种类型的顶级语言块 、
{{ msg }}export default {
data () {
return {
msg: 'Hello world!'
}
}
}
.example {
color: red;
}
This could be e.g. documentation for the component.
为此Vue的官方提供了vue-loader,它会解析文件,提取每个语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 ES Module,它的默认导出是一个 Vue.js 组件选项的对象。
vue-loader 支持使用非默认语言,比如 CSS 预处理器,预编译的 HTML 模版语言,通过设置语言块的 lang 属性。例如,你可以像下面这样使用 Sass 语法编写样式:
/* write Sass! */
开始
大部分情况下我们开发Vue项目是通过vue-cli这个脚手架快速生成一个项目骨架而开始的
vue init webpack my-vue-project
然后我们在这个项目中编写.vue单文件组件,通过:
npm rub build
构建整个项目,将产出的文件发布上线,事实上在整个项目构建过程中我们并不知道.vue文件到底发生了什么。当然如果只是单纯通过这种方式来开发一个Vue的单页项目,其实也并不需要了解太多细节,我们只需要按照给定的项目规范编写业务代码即可,但这种傻瓜式的开发配置,有时候并不能满足多变的需求。
想法
既然Vue官方已经定义了一种单文件组件(SFC)规范的东东,我们何不借来使用:实现组件的平台化。我们提供一个.vue组件编辑器供用户使用,编辑完成后单击保存,即可看到该组件渲染之后的页面。
在这个过程中会涉及到如何编译.vue单文件组件,以及动态/异步渲染vue单文件组件,本文主要记录编译vue单文件组件的几种方法:
在开始之前我们提前创建了一个项目:compile-vue-demo,方便您的调试和查看,该项目集成了下面几种编译方式。
rollup
Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。Rollup 对代码模块使用新的标准化格式,这些标准都包含在 JavaScript 的 ES6 版本中,而不是以前的特殊解决方案,如 CommonJS 和 AMD。ES6 模块可以使你自由、无缝地使用你最喜爱的 library 中那些最有用独立函数,而你的项目不必携带其他未使用的代码。ES6 模块最终还是要由浏览器原生实现,但当前 Rollup 可以使你提前体验。
配置文件
// rollup.config.js
import VuePlugin from 'rollup-plugin-vue'
export default {
entry: 'src/main.vue',
format: 'iife',
dest: 'rel/bundle.js',
output: {
name: 'main',
},
plugins: [VuePlugin(/* VuePluginOptions */)],
}
项目里的entry.rollup.html文件为rollup编译的入口文件,通过运行下面的命令编译:
rollup -c --watch
webpack
项目里的entry.webpack.html文件为webpack编译方式的入口文件,通过运行下面的命令来编译:
webpack
// webpack.config.js
const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
entry: './src/hello.vue',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
library: 'hello',
//libraryTarget: 'umd'
},
mode: 'development',
devtool: 'none',
module: {
rules: [
// 解析.vue文件
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 它会应用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `
{
test: /\.css$/,
loader: 'css-loader'
},
// 它会应用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `
{
test: /\.js$/,
loader: 'babel-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
上面的配置就是将./src/hello.vue文件编译打包成 ./dist/bundle.js文件,为了能够在打包之后拿到hello.vue组件对象,上面配置了output.library = 'hello'
在entry.webpack.html文件中则通过下面方式实例化了一个Vue App
var vm = new Vue({
el:'#app',
data: {
hello: 'hello'
},
created() {},
components:{
hello: hello.default
}
});
FIS3
项目里的entry.fis.html文件为fis编译方式的入口文件,通过运行下面命令来查看效果:
fis3 server start
fis3 release -wcL
在fis中已经有大神帮我们实现了.vue文件的编译插件:fis3-parser-vue-component 具体配置如下
// fis-conf.js
fis.match('*.vue', {
isMod: true,
rExt: '.js',
useSameNameRequire: true,
parser: fis.plugin('vue-component', {
runtimeOnly: true, // vue@2.x 有润timeOnly模式,为ture时,template会在构建时转为render方法, 这里开启后paths中无需指定
// styleNameJoin
styleNameJoin: '', // 样式文件命名连接符 `component-xx-a.css`
extractCSS: false, // 是否将css生成新的文件, 如果为false, 则会内联到js中
// css scoped
cssScopedIdPrefix: '_v-', // hash前缀:_v-23j232jj
cssScopedHashType: 'sum', // hash生成模式,num:使用`hash-sum`, md5: 使用`fis.util.md5`
cssScopedHashLength: 8 // hash 长度,cssScopedHashType为md5时有效
}),
optimizer: [/*fis.plugin('uglify-js'),*/ function(content, file, settings) {
//console.log(file);
return content;
}]
}).match('{*.less, *.vue:less}', {
parser: fis.plugin('less'),
postprocessor: fis.plugin('autoprefixer'),
rExt: '.css'
}).match('{src/**.js, *.vue:js}', {
parser: fis.plugin('babel-6.x', {
presets: ['es2015', 'stage-3'],
plugins: ['add-module-exports']
}),
rExt: '.js'
});
平台化?
上面的两种编译.vue的方法,也只是我们借助工具经过简单配置来实现的,尽管实现了.vue单文件组件的编译,但还是不够友好,或者说我们还是不能方便的使用,他们仍然是要通过编译文件的形式来解析代码,是否可以以一种服务的形式存在直接解析/编译从前端编辑器POST过来的.vue单文件组件代码?我们后面文章将继续介绍~~
参考
-
Vue.extend、Vue.component、new Vue以及动态编译Vue.compile
2020-09-02 17:22:32Vue.extend、Vue.component、new Vue以及Vue.compile1、Vue.extend(options):2、Vue.component(options):3、new Vue创建==根实例== Vue.extend(vue扩展构造器)、Vue.component(vue全局组件注册)、new Vue(创建vue...Vue.extend、Vue.component、new Vue以及动态编译Vue.compile
先说说Vue.extend(vue扩展构造器)、Vue.component(vue全局组件注册)、new Vue(创建vue根实例)三者之间的关系。
野路子理解:
1、Vue.extend(options):
1、根据官方文档的描述是用来创建Vue“子类”的api,也就是创建一个新的构造函数。
2、该构造函数里面包含了对Vue初始化函数的调用,另外Vue.extend里通过Object.create()以Vue的原型对象为原型创建了新的对象当做该构造函数的原型,这样做的好处是更改原型属性时,避免对Vue的原型造成污染同时又继承了Vue的原型属性。
3、options是共有的选项数据,也就是只要由该构造函数创建的实例都会有这些选项,和mixin一毛一样。另外实例初始化时还可以传递实例单独的options选项。
4、因为生成的构造函数在new调用时,函数里调用了Vue的初始化方法,所以生成的是一个正了八经的Vue实例对象。2、Vue.component(options):
1、用来注册全局组件,官网上有三种用法:
// 1、注册组件,传入一个扩展过的构造器
Vue.component(‘my-component’, Vue.extend({ /* … */ }))// 2、注册组件,传入一个选项对象 (自动调用 Vue.extend)
Vue.component(‘my-component’, { /* … */ })// 3、获取注册的组件 (始终返回构造器)
var MyComponent = Vue.component(‘my-component’)1、根据第1、2用法再加上options参数和Vue.extend的options可以一毛一样,由此得出注册组件就是根据Vue.extend的选项options创建一个新的构造函数,和注册时填写的id名称一一对应。
2、可以这么理解:每一次组件实例化,其实都是根据这个新的构造函数来创建。当然实际要考虑的更多。3、new Vue()
官方文档中经常见到:
通过 new Vue 创建的根 Vue 实例
刚开始以为new Vue()才是根实例,后来看了源码发现,这只是代表了一个组件树结构的根组件。不信可以测一下 Vue.extend返回的构造函数创建的实例也可以是根实例。
不过通常应用中的根实例都是通过new Vue()创建的:
1、spa应用的根实例只有一个。
2、稍微老点的系统可能一个页面一个根实例。4、Vue.compile
这个api不常用,但是用起来也挺爽。根据一次实际应用来介绍它。
需求:
根据配置页面的模板字符串动态改变页面显示内容。
比如模板字符串可能配成以下几种:
this.templateStr=
1、“item.name”;
2、“getName(item.id)”;
3、“‘我的名字是’+getName(item.id)”;上面三种都是vue中的模板表达式,由于都是字符串形式,所以这里就可以用Vue.compile来编译,具体如下:
// 组合要编译的字符串 const templateStr = `<span>{{this.templateStr}}</span>`; // 调用Vue.compile获取render函数 const templateRes = Vue.compile(templateStr); // render函数执行需要Vue实例,否则报错,并且,模板中的各种属性、方法都会去这个实例里面找,所以。。。实例可以随便搞了 // 这样 const node1 = templateRes.render.call(this); // 这样 const Ctor = Vue.extend(); const vueInstance = new Ctor({ data: function() { return { item: { name: 'zhangsan', id: '1' } } }, methods: { getName(id){ return 'zhangsan' + id; } } }); const node2 = templateRes.render.call(vueInstance);
代码中node1、node2就是vnode实例。
提示:render函数执行需要Vue实例,否则报错,并且,模板中的各种属性、方法都会去这个实例里面找。
-
new boilerplate project fails to compile with Vue 2.5 and TS 2.7.2
2020-12-08 21:26:43<div><p>Hi, I'm trying to create a new project with the ...webpack: Failed to compile. </code></pre> <p>Any idea ? <p>thanks, BR</p><p>该提问来源于开源项目:vue-typed/vue-typed</p></div> -
Vue Compile原理分析
2020-09-22 19:20:45Vue中Compile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render, 而render的作用主要是为了生成VNode, Compile主要分为3大块: parse 接受template原始模板,按着模板的节点和数据生成对应...Vue中Compile是一个非常复杂的内容,Compile的主要作用是解析模板,生成渲染模板的render, 而render的作用主要是为了生成VNode, Compile主要分为3大块:
- parse 接受template原始模板,按着模板的节点和数据生成对应的ast
- optimize 遍历ast的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,减少去对比这部分DOM,提升性能
- generate 把前两步生成完善的ast,组成render字符串,然后将render字符串通过
new Function
的方式转换成渲染函数
1. Compile从新建实例到Compile结束的主要流程
Vue编译template, 生成render发生在
$mount
这个函数中Vue.prototype.$mount = function(el) { var options = this.$options; if (!options.render) { // 获取template模板 var tpl = options.template; if (tpl) { tpl = document.querySelector(tpl).innerHTML; } if (tpl) { var ref = compileToFunctions(tpl, {}, this); options.render = ref.render; options.staticRenderFns = ref.staticRenderFns; } } // 执行上面生成的render, 生成DOM,挂载DOM return mount.call(this, el); }
compileToFunctions
的生成流程// compileToFunctions是通过createCompiler执行返回的 var ref$1 = createCompiler(); var { compileToFunctions } = ref$1;
createCompiler
的生成流程// createCompiler 是通过 createCompilerCreator 生成的 // createCompilerCreator 会传入一个 baseCompile 的函数 var createCompiler = createCompilerCreator( function baseCompile(template, options) { var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); } var code = generate(ast, options); return { ast, render: code.render, staticRenderFns: code.staticRenderFns } } ); function createCompilerCreator (baseCompile) { return function () { // 作用是合并选项,并且调用 baseCompile function compile(template) { var compiled = baseCompile(template); return compiled; } return { compile, // compileToFunctions 用来生成 render 和 staticRenderFns // compileToFunctions 其内核就是 baseCompile compileToFunctions: createCompileToFunctionFn(compile) } } } function createCompileToFunctionFn (compile) { // 作为缓存,防止每次都重新编译 // template 字符串 作为 key // 值为 render 和 staticRenderFns var cache = Object.create(null); return function compileToFunctions(template, options, vm) { var key = template; // 有缓存的时候直接取出缓存中的结果即可 if (cache[key]) return cache[key]; // compile 是 createCompilerCreator 传入的 compile var compiled = compile(template, options); var res = { // 将字符串render解析成函数 render: new Function(compiled.render), staticRenderFns: compiled.staticRenderFns.map(function(code) { return new Function(code, fnGenErrors); }) }; cache[key] = res; return cache[key]; } }
2. parse主要流程
parse的主要作用就是将template字符串转化为ast,ast为抽象语法树,是一种以树形结果来表示模板语法结构,比如:
{ tag: 'div', type: 1, // 1->节点; 2->表达式,比如{{isShow}}; 3->纯文本 children: [{ type: 3, text: '11' }] }
parse
整个流程非常复杂,需要一步步深入// pase接收template字符串 function parse(template) { // 缓存模板中解析的每个节点的ast // 是一个数组存放模板中按顺序从头到尾每个标签的AST // 主要用来理清节点父子关系 var stack = []; // 根节点,是ast var root; // 当前解析标签的父节点 // 这样才知道当前解析节点的父节点是谁,才把这个节点添加给响应节点的children // 根节点没有父节点,所以是undefined var currentParent; // parseHTML 负责 template 中的标签匹配,再传入 start, end, chars 等方法 parseHTML(template, { start, // 处理头标签 end, // 处理尾标签 chars // 处理文本 }); return root; } function parseHTML(html, options) { while(html) { // 寻找 < 的起始位置 var textEnd = html.indexOf('<'), text, rest, next; // 如果模板起始位置是标签开头 < // 如果匹配到 <, 那这个有可能是头标签上的,也有可能是尾标签上的 if (textEnd === 0) { // 如果是尾标签得到 < // 比如html = '</div>', 匹配出 endTagMatch=['</div>', 'div'] var endTagMatch = html.match(endTag); if (endTagMatch) { // endTagMatch[0]='</a>' html = html.substring(endTagMatch[0].length); // 处理尾标签的方法 options.end(); continue; } // 如果是起始标签的 < // parseStartTag作用是,匹配标签存在的属性,截断template // 比如: html='<div></div>' // parseStartTag处理之后, startTagMatch={tagName: 'div', attrs: []} var startTagMatch = parseStartTag(); // 匹配到起始标签之后 if (startTagMatch) { // 处理起始标签 options.start(...); continue; } } // 模板起始位置不是 <, 而是文字 if (textEnd >= 0) { text = html.substring(0, textEnd); html = html.substring(n); } // 处理文字 if (options.chars && text) { options.chars(text); } } }
处理头标签, 当parseHTML匹配到一个首标签,都会把该标签的信息传递给start
function start(tag, attrs, unary) { // 创建AST节点 var element = createASTElement(tag, attrs, currentParent); // 设置根节点,一个模板仅有一个根节点 if (!root) { root = element; } // 处理父子关系 if (currentParent) { currentParent.children.push(element); element.parent = currentParent; } // 不是单标签(input, img), 都需要保存到stack if (!unary) { currentParent = element; stack.push(element); } } function createASTElement(tag, attrs, parent) { // 创建一个AST结构,保存数据 /* 模板上的属性经过parseHTML解析成一个数组 [{ name: 'hoho', value: '333'},{ name: 'href', value: '444' }] makeAttrMap可以将其转化成如下结构 { hoho: '333', href: '444' } */ return { type: 1, tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), parent, children: [] } }
处理尾标签匹配到尾标签,比如
</div>
的时候,就会调用传入的end
方法function end() { // 标签解析结束,移除该标签 /* 比如有如下一段html: <div> <section></section> <p></p> </div> stack匹配到两个头标签之后 stack=['div', 'section'] 然后匹配到</section>, 则移除stack中的section, 并且重设 currentParent stack=['div'] currentParent='div' 再匹配到</p>, p的父节点就是div了,父子顺序就正确了 */ stack.length -= 1; currentParent = stack[stack.length - 1]; }
处理文本字符串,当
parseHTML
去匹配<
的时候,发现template开头到<
还有一段距离,那么这段距离就是文本了,这段文本会给chars
方法处理// chars的主要作用就是为父节点添加文本子节点 // 文本子节点有两种类型 // 1. 普通型,直接存为文本子节点 // 2. 表达式型,需要经过parseText处理 /* 比如表达式{{isShow}}会被解析成 { expression: toString(isShow), tokens: [{@binding: "isShow"}] } 其主要是为了把表达式isShow拿到,方便从实例上获取 */ function chars(text) { // 必须存在根节点,不能用文字开头 if (!currentParent) return; var children = currentParent.children; // 通过parseText解析成字符串,判断是否含有双括号表达式,比如{{isShow}} // 如果含有表达式,会存放多一点信息 var res = parseText(text); if (res) { children.push({ type: 2, expression: res.expression, tokens: res.tokens, text, }); } // 普通字符串,直接存在字符串节点 else if (!children.length || children[children.length - 1].text !== ' ) { children.push({ type: 3, text, }); } }
下面来看一个完整的Parse流程
<div>11</div>
- 开始循环template,匹配到第一个头标签
div
, 传入start, 生成对应的AST,该div的ast变成根节点root,并设置其为当前父节点currentParent, 保存节点存储数组stack
stack = [{ tag: 'div', children: [] }]
第一轮处理结束,template截断到第一次匹配到的位置
11</div>
- 开始第二次遍历,开始匹配到
<
, 发现<
不在开头,从开头位置到<
有一段普通字符串,调用chars,传入字符串,发现其没有双括号等表达式,直接给父节点添加简单子节点
currentParent.children.push({ type: 3, text: '11' });
此时
stack = [{ tag: 'div', children: [{ type: 3, text: '11' }] }]
第二轮结束,template截断到刚刚匹配完的字符串,此时
template=</div>
- 开始第三轮遍历,继续寻找
<
, 发现就在开头,但是这是一个结束标签,标签名是div
。这个标签匹配完毕,也会从stack中移除,第3次遍历结束,template继续阶段,此时template为空,遍历结束。
{ tag: 'div', type: 1, children: [{ type: 3, text: '11' }] }
3. 标签解析
上一小节讲到了是通过
parseHTML
这个方法来对template
进行循环遍历的。就是不断的将模板字符串匹配然后截断,直到字符串为空,其中和阶段有关的一个重要函数就是advance
// 截断模板字符串,并保存当前的位置 function advance(n) { index += n; html = html.substring(n); }
-
如果 < 在template开头
如果是尾标签的<
, 那么交给parseEndTag
处理
如果是头标签的>
, 那么交给parseStartTag
处理 -
如果 < 不在template开头,那么表明开头到 < 的这段位置是字符串,也需要用到
advance
去截断字符串。
template = '<div>111</div>'; parseHTML(template);
parseStartTag
作用是处理头标签- 把头标签的所有信息及合起来,包括属性,标签名
- 匹配完成之后,同样调用advance去截断template
- 把标签信息返回
function parseStartTag() { // html ='<div name=1>111</div>' // start = ["<div", "div", index: 0] var start = html.match(startTagOpen); if (start) { // 存储本次头标签的信息 var match = { tagName: start[1], attrs: [], start: index } } // start[0]是<div // 阶段之后template="name=1 >111</div>" advance(start[0].length); var end, attr; // 循环匹配属性内容,保存属性列表 // 直到template遍历到头标签的 > while ( // 匹配不到头标签的 > , 开始匹配 属性内容 !(end = html.match(startTagClose)) // 开始匹配属性内容 // attr = ["name=1", "name", "="] && (attr = html.match(attribute)) ) { advance(attr[0].length); match.attrs.push(attr); } // 匹配到起始标签的 > , 标签属性那些已经匹配完毕了 // 返回收集到的标签信息 if (end) { advance(end[0].length); // 如果是单标签,那么 unarySlash 的值是 / , 比如 <input /> match.unarySlash = end[1]; match.end = index; return match; } }
经过以上的parseStartTag会处理返回如下内容
// <div name=1></div> { tagName: 'div', // 头标签中的属性信息 attrs: [ [" name=1", "name", "=", undefined, undefined, "1"] ], unarySlash: "", // 表示这个表示是否是单标签 start: 0, // 头标签的 < 在template中的位置 end: 12 // 头标签的 > 在template中的位置 }
通过parseStartTag返回的头信息,最后传给了
handleStartTag
function handleStartTag(match) { var tagName = match.tagName; var unarySlash = match.unarySlash; // 判断是不是单标签,input, img这些 var unary = isUnaryTag$$1(tagName) || !!unarySlash; var l = match.attrs.length; var attrs = new Array(l); // 把属性数组转化成对象 for (let i = 0; i < l; i++) { var args = match.attrs[i]; var value = args[3] || args[4] || args[5] || ''; attrs[i] = { name: args[1], value } } // 不是单标签才存到stack if (!unary) { stack.push({ tag: tagName, attrs }); } if (options.start) { options.start( tagName, attrs, unary, match.start, match.end ); } }
当使用endTag这个正则成功匹配到尾标签时,会调用parseEndTag
function parseEndTag(tagName, start, end) { var pos, lowerCasedTagName; if (tagName) { for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].tagName === tagName) break; } } else { // 如果没有提供标签签名,那么关闭所有存在stack中的起始标签 pos = 0; } // 批量 stack pop 位置后的所有标签 if (pos >= 0) { // 关闭 pos 位置之后所有的起始标签,避免有些标签没有尾标签 // 比如stack.length = 7, pos = 5, 那么就关闭最后两个 for (var i = stack.length - 1; i >= pos; i--) { if (options.end) { options.end(stack[i].tag, start, end); } } // 匹配完闭合标签之后,就把匹配的标签头给移除了 stack.length = pos; } } /* 简要流程如下 <div> <header> <span></span> </header> </div> 一开始会匹配到3个头标签 stack = [div, header, span] 然后开始匹配到span, 然后去stack末尾去找span 确定span在stack中的位置pos后,批量闭合stack的pos之后所有的标签 */
4.属性解析
处理class分为两种,一种静态的class, 一种动态的class
function transformNode(el, options) { var staticClass = getAndRemoveAttr(el, 'class'); if (staticClass) { el.staticClass = JSON.stringify(staticClass); } // 处理动态class var classBinding = getBindingAttr(el, 'class', false); if (classBinding) { el.classBinding = classBinding; } } /* <span class="a" :class="b"></span> 转化为下面的 { classBinding: "b" staticClass: ""a"" tag: "span" type: 1 } */
处理style几乎和class一模一样
function transformNode$1(el, options) { var staticStyle = getAndRemoveAttr(el, 'style'); if (staticStyle) { // 比如绑定 style="height:0;width:0" // parseStyleText 解析得到对象 { height:0,width:0 } el.staticStyle = JSON.stringify(parseStyleText(staticStyle)); } // :style="{height:a}" 解析得 {height:a} var styleBinding = getBindingAttr(el, 'style', false); if (styleBinding) { el.styleBinding = styleBinding; } } /* <span style="width:0" :style="{height: a}"></span> 转化成下面的 { staticStyle: "{"width":"0"}" styleBinding: "{height:a}" tag: "span" type: 1 } */
解析v-for用的是
processFor
function processFor(el) { var exp = getAndRemoveAttr(el, 'v-for'); if (exp) { // 比如指令是v-for="(item, index) in arr" // res = {for: "arr", alias: "item", iterator1: "index"} var res = parseFor(exp); if (res) { // 把res和el属性合并起来 extend(el, res); } } } /* <div v-for="(item, index) in arr"></div> 可以转化为 { alias: "item", for: "arr", iterator1: "index", tag: "div", type: 1, } */
解析v-if用的是
processIf
function processIf(el) { var exp = getAndRemoveAttr(el, 'v-if'); if (exp) { el.if = exp; (el.ifConditions || el.ifConditions = []).push({ exp, block: el }) } else { // 对于 v-else 和 v-else-if 没有做太多的处理 // 这二者会调用之后的 processIfConditions if (getAndRemoveAttr(el, 'v-else') !== null) { el.else = true; } var elseif = getAndRemoveAttr(el, 'v-else-if'); if (el.elseif) { el.elseif = elseif; } } } function processIfConditions(el, parent) { var prev = findPrevElement(parent.children); if (prev && prev.if) { (prev.ifConditions || prev.ifConditions = []).push({ exp: el.elseif, block: el }); } } /* <div> <p></p> <div v-if="a"></div> <strong v-else-if="b"></strong> <span v-else></span> </div> 解析之后生成如下 { tag: "header", type: 1, children:[{ tag: "header", type: 1, if: "a", ifCondition:[ {exp: "a", block: {header的ast 节点}} {exp: "b", block: {strong的ast 节点}} {exp: undefined, block: {span的ast节点}} ] },{ tag: "p" type: 1 }] } */
slot的解析是通过
processSlot
来解析的function processSlot(el) { if (el.tag === 'slot') { el.slotName = el.attrsMap.name; } else { var slotScope = getAndRemoveAttr(el, 'slot-scope'); el.slotScope = slotScope; // slot的名字 var slotTarget = el.attrsMap.slot; if (slotTarget) { el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget } } } /* <span> <slot name="header" :a="num" :b="num"></slot> </span> 以上模板解析成 { { tag: "span" type: 1 children:[{ attrsMap: {name: " header", :a: "num", :b: "num"} slotName: "" header"" tag: "slot" type: 1 }] } 父组件模板 <div> <child > <p slot="header" slot-scope="c"> {{ c }}</p> </child> </div> 解析成 { children: [{ tag: "child", type: 1, children: [{ slotScope: "c", slotTarget: ""header "", tag: "p", type: 1 }] }], tag: "div", type: 1 } */
Vue自带属性
v-
,:
,@
三种符号的属性名,会分开处理先来看
:
的情况,经过Vue的处理,:
开头的属性会被放入el.props
或者el.attrs
中。当我们给指令添加了
.prop
的时候<!-- 这个属性会被存放到el.props中 --> <div :name.props="myName"></div>
props是直接添加到DOM属性上的,attrs是直接显示在标签上的。添加props的时候,需要转化成驼峰法,因为DOM元素的props不支持
-
连接的。当匹配到
@
或者v-on
时,属于事件添加,没有太多处理<div @click="aaa" @keyup="bbb"></div> <!-- { events: { click: { value: 'aaa' }, keyup: { value: 'bbb' } } } -->
v-
开头的会全部保存到el.directives
中<div v-a:key="bbb"></div> <!-- { directives: [{ arg: "key", modifiers: undefined, name: "a", rawName: "v-a:key", value: "bbb" }] } -->
普通属性
直接存放进el.attrs
中<div bbb="ccc"></div> <!-- { attrs: [{ name: 'bbb', value: 'ccc' }] } -->
总体源码如下
var onRE = /^@|^v-on:/; var dirRE = /^v-|^@|^:/; var bindRE = /^:|^v-bind:/; var modifierRE = /\.[^.]+/g; var argRE = /:(.*)$/; function processAttrs(el) { var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) { name = rawName = list[i].name; value = list[i].value; // 判断属性是否带有 'v-' , '@' , ':' if (dirRE.test(name)) { // mark element as dynamic el.hasBindings = true; // 比如 v-bind.a.b.c = "xxzxxxx" // 那么 modifiers = {a: true, b: true, c: true} modifiers = parseModifiers(name); // 抽取出纯名字 if (modifiers) { // name = "v-bind.a.b.c = "xxzxxxx" " // 那么 name= v-bind name = name.replace(modifierRE, ''); } // 收集动态属性,v-bind,可能是绑定的属性,可能是传入子组件的props // bindRE = /^:|^v-bind:/ if (bindRE.test(name)) { // 抽取出纯名字,比如 name= v-bind // 替换之后,name = bind name = name.replace(bindRE, ''); isProp = false; if (modifiers) { // 直接添加到 dom 的属性上 if (modifiers.prop) { isProp = true; // 变成驼峰命名 name = camelize(name); if (name === 'innerHtml') name = 'innerHTML'; } // 子组件同步修改 if (modifiers.sync) { addHandler(el, // 得到驼峰命名 "update:" + camelize(name), // 得到 "value= $event" genAssignmentCode(value, "$event") ); } } // el.props 的作用上面有说,这里有部分是 表单的必要属性都要保存在 el.props 中 if ( isProp || // platformMustUseProp 判断这个属性是不是要放在 el.props 中 // 比如表单元素 input 等,属性是value selected ,checked 等 // 比如 tag=input,name=value,那么value 属性要房子啊 el.props 中 (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)) ) { (el.props || (el.props = [])).push({ name, value }); } // 其他属性放在 el.attrs 中 else { (el.attrs || (el.attrs = [])).push({ name, value }); } } // 收集事件,v-on , onRE = /^@|^v-on:/ else if (onRE.test(name)) { // 把 v-on 或者 @ 去掉,拿到真正的 指令名字 // 比如 name ="@click" , 替换后 name = "click" name = name.replace(onRE, ''); addHandler(el, name, value, modifiers, false); } // 收集其他指令,比如 "v-once", else { // 把v- 去掉,拿到真正的 指令名字 name = name.replace(dirRE, ''); // name = "bind:key" , argMatch = [":a", "a"] var argMatch = name.match(argRE); var arg = argMatch && argMatch[1]; if (arg) { // 比如 name = "bind:key" ,去掉 :key // 然后 name = "bind" name = name.slice(0, -(arg.length + 1)); } (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers }); } } else { (el.attrs || (el.attrs = [])).push({ name, value }); } } }
optimize
是Compile的三大步骤之一,是一个性能优化的手段// ... parse var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); } // ...generate
它能遍历AST子树,检测纯静态的子树,即永不需要更改的DOM,Vue内部进行Optimize的方法就是给节点加上
static
属性。function optimize(root, options) { if (!root) return; makeStatic$1(root); makeStaticRoots(root); }
先来看看Vue是如何判断static节点的
function isStatic(node) { // 文字表达式 if (node.type === 2) return false; // 纯文本 if (node.type === 3) return true; return node.pre || ( // 如果添加了v-pre指令,表明节点不需要解析了 !node.hasBindings && // 不能存在指令,事件等 !node.if && // 不能存在v-if !node.for && // 不能存在v-for !['slot', 'component'].indexOf(node.tag) > -1 && // 节点名称不能是slot和component isPlatformReserverdTag(node.tag) && // 需要时正常的HTML标签 !isDirectChildOfTemplateFor(node) && // 父辈节点不能是template或者带有v-for Object.keys(node).every(isStaticKey) // 该节点所有的属性都需要是静态节点的静态属性 ); }
上面提到的
makeStatic$1
方法,主要用来标记节点是否是静态节点// 标记节点是否是静态节点 function markStatic$1(node) { node.static = isStatic(node); if (node.type !== 1) return // 不要将组件插槽内容设置为静态。 // 这就避免了 // 1、组件无法更改插槽节点 // 2、静态插槽内容无法热加载 if ( // 正常 thml 标签 才往下处理,组件之类的就不可以 !isPlatformReservedTag(node.tag) && // 标签名是 slot 才往下处理 node.tag !== 'slot' && // 有 inline-tempalte 才往下处理 node.attrsMap['inline-template'] == null ) { return } // 遍历所有孩子,如果孩子 不是静态节点,那么父亲也不是静态节点 var l = node.children.length for (var i = 0;i < l; i++) { var child = node.children[i]; // 递归设置子节点,子节点再调用子节点 markStatic$1(child); if (!child.static) { node.static = false; } } if (node.ifConditions) { var c = node.ifConditions.length for (var j = 1; j < c; j++) { // block 是 节点的 ast var block = node.ifConditions[j].block; markStatic$1(block); if (!block.static) { node.static = false; } } } }
第二步就是标记静态根节点
// 标记根节点是否是静态节点 function markStaticRoots(node) { if (node.type === 1) return // 要使一个节点符合静态根的条件,它应该有这样的子节点 // 不仅仅是静态文本。否则,吊装费用将会增加 // 好处大于坏处,最好总是保持新鲜。 if ( // 静态节点 node.static && // 有孩子 node.children.length && // 孩子有很多,或者第一个孩子不是纯文本 ! (node.children.length === 1 && node.children[0].type === 3) ) { node.staticRoot = true; return } else { node.staticRoot = false; } if (node.children) { var l = node.children.length for (var i = 0; i < l; i++) { markStaticRoots( node.children[i] ); } } }
markStatic$1 这个函数只是为 markStaticRoots 服务的,是为了先把每个节点都处理之后,更加方便快捷静态根节点。
被判断为静态根节点的条件
- 该节点所有的子孙节点都是静态节点
- 必须存在子节点
- 子节点不能是纯文本节点。Vue不会将这种节点标记为静态节点,如果将这种节点也标记为静态节点,会起到负优化的作用,下面讨论为什么给纯文本节点标记为静态节点,是一种负优化
首先标记为静态节点需要维护静态模板存储对象,这个信息存储在
_staticTrees
中。随着静态根节点的增加,这个存储对象会越来越大,那么占用的内存也会越来越多,势必要增加一些不必要的存储其实这个问题涉及到 render 和 静态 render 的合作,
<div> <span> <strong>我是静态文本</strong> </span> <span v-if="testStaticRender"></span> </div>
生成的render函数是这样的
with(this) { return _c('div', [ // 这个函数就是去获取静态模板的,这样会产生很多额外的调用 _m(0), (testStaticRender ? _c('span') : _e()) ]) }
genarate
generate的作用是根据生成的AST节点,拼接成字符串,而这个字符串可以被转化为函数,函数执行后,就会生成VNode
// options用来传入一些判断函数或者指令 function generate(ast, options) { // CodegenState 给实例初始化编译状态 var state = new CodegenState(options); // genElement 将AST转化为字符串 var code = ast ? genElement(ast, state) : '_c("div")'; return { render: "with(this){ return " + code + "}", staticRenderFns: state.staticRenderFns } } function CodegenState(options) { this.options = options; // class$1 用于处理AST中的class // style$1 用于处理AST中的style this.dataGenFns = [ class$1.genData, style$1.genData]; this.directives = { on , bind, cloak, model,text ,html] // 用来存放静态根节点的render函数 this.staticRenderFns = []; }
genElement是AST拼接成字符串的重点函数,主要是处理各种节点,并且拼接起来
// 这个里面主要是一个个的处理函数 function genElement(el, state) { if ( el.staticRoot && !el.staticProcessed ) { // 拼接静态节点 return genStatic(el, state) } else if ( el.for && !el.forProcessed ) { return genFor(el, state) } else if ( el.if && !el.ifProcessed ) { return genIf(el, state) } else if (el.tag === 'slot') { return genSlot(el, state) } else { var code; // 处理 is 绑定的组件 if (el.component) { code = genComponent(el.component, el, state); } // 上面所有的解析完之后,会走到这一步 else { // 当 el 不存在属性的时候,el.plain = true var data = el.plain ? undefined : genData$2(el, state); // 处理完父节点,遍历处理所有子节点 var children = genChildren(el, state); code = `_c( '${el.tag}' ${data ? ("," + data) : ''} ${children ? ("," + children) : ''} )` } return code } }
拼接静态节点
function genStatic(el, state) { el.staticProcessed = true; state.staticRenderFns.push( "with(this){ return " + genElement(el, state) + "}" ); return `_m(${ state.staticRenderFns.length - 1 })`; }
拼接v-if节点
// el.ifCondition 是用来存放条件数组的 function genIf(el, state) { el.isProcessed = true; return genIfConditions( el.ifConditions.slice(), state ); } /* <div> <p v-if="isShow"></p> <span v-else-if="isShow == 2"></span> <section v-else></section> </div> 会编译成如下 { tag:"div", children:[{ tag:"p", ifCondition:[{ exp: "isShow", block: {..p 的 ast 节点} },{ exp: "isShow==2", block: {..span 的 ast 节点} },{ exp: undefined, block: {..section 的 ast 节点} }] }] } */
7. 事件拼接
function genData$2(el, state) { var data = '{'; // 组件自定义事件,比如`<div @click="a"></div>` if (el.events) { data += genHandlers(el.events, false) + ','; } // 原生DOM事件,比如 `@click.native` if (el.nativeEvents) { data += genHandlers(el.nativeEvents, true) + ','; } data = data.replace(/,$/, '') + '}'; return data; }
从上面的函数可以知道,不管是组件自定义事件还是原生DOM事件,都是调用的
genHandlers
function genHandlers(events, isNative) { var res = isNative ? 'nativeOn:{' : 'on:{'; var handler = events[name]; for (var name in events) { res += ` ${name}:${genHandler(name, handler)}, ` } return res.slice(0, -1) + '}'; }
修饰符内部配置
var modifierCode = { stop: '$event.stopPropagation();', prevent: '$event.preventDefault();', ctrl: genGuard("!$event.ctrlKey"), shift: genGuard("!$event.shiftKey"), alt: genGuard("!$event.altKey"), meta: genGuard("!$event.metaKey"), self: genGuard("$event.target !== $event.currentTarget"), left: genGuard("'button' in $event && $event.button !== 0"), middle: genGuard("'button' in $event && $event.button !== 1"), right: genGuard("'button' in $event && $event.button !== 2") }; var genGuard = function(condition) { return ` if ( ${ condition } ) return null ` }; /* 比如添加了stop修饰符的,会这么拼接 "function($event ){ " + "$event.stopPropagation();" + " return "+ aaa +"($event);" + "}" */
键盘修饰符
// keys是一个数组,保存的是添加的修饰符,可以是数字,可以是键名 function genKeyFilter(keys) { var key = keys.map(genFilterCode).join('&&'); return `if( !('button' in $event) && ${ key } ) return null `; } function genFilterCode(key) { var keyVal = parseInt(key); // 如果key是数字,那直接返回字符串 if (keyVal) { return "$event.keyCode!==" + keyVal } // 如果key是键名,比如`enter` // 这个键名可能不在keyCodes, keyNames中,可以支持自定义 var keyCode = keyCodes[key]; // 获取键值,keyName="Enter" var keyName = keyNames[key]; // 获取键名,keyCode=13 // $event.keyCode 是按下的键的值 // $event.key 是按下键的名 // 比如我们按下字母`V`, 那此时的keyCode是86, key是'v' return ` _k( $event.keyCode , ${ key } , ${ keyCode }, ${ $event.key } , ${ keyName } ) ` } // 返回的这个_k本体其实就是`checkKeyCodes`函数 function checkKeyCodes( eventKeyCode, key, keyCode, eventKeyName, keyName ) { // 比如 key 传入的是自定义名字 aaaa // keyCode 从Vue 定义的 keyNames 获取 aaaa 的实际数字 // keyName 从 Vue 定义的 keyCode 获取 aaaa 的别名 // 并且以用户定义的为准,可以覆盖Vue 内部定义的 var mappedKeyCode = config.keyCodes[key] || keyCode; // 该键只在 Vue 内部定义的 keyCode 中 if (keyName && eventKeyName && !config.keyCodes[key]) { return isKeyNotMatch(keyName, eventKeyName) } // 该键只在 用户自定义配置的 keyCode 中 else if (mappedKeyCode) { return isKeyNotMatch(mappedKeyCode, eventKeyCode) } // 原始键名 else if (eventKeyName) { return hyphenate(eventKeyName) !== key } }
核心的genHandler方法
function genHandler(name, handler) { // 没有绑定回调,返回一个空函数 if (!handler) { return 'function(){}'; } // 如果绑定的是数组,则逐个递归一遍 if (Array.isArray(handler)) { return "[" + handler.map(handler => { return genHandler(name, handler); }).join(",") + "]"; } // 开始解析单个回调 var isMethodPath = simplePathRE.test(handler.value); var isFunctionExpression = fnExpRE.test(handler.value); // 没有modifier if (!handler.modifiers) { if (isMethodPath || isFunctionExpression) { return handler.value; } // 内连语句,需要包裹一层 return "function($event){" + handler.value + ";}"; } else { var code = ""; var genModifierCode = ""; // 保存内部修饰符 for (var key in handler.modifier) { if (modifierCode[key]) { } // 精确修饰符 else if (key === 'exact') { } // 普通按键 else { keys.push(key); } } } // 开始拼接事件回调 // 拼接Vue定义外的按键修饰符 if (keys.length) { code += genKeyFilter(keys); } // 把prevent和stop这样的修饰符在按键过滤之后执行 if (genModifierCode) { code += genModifierCode; } // 事件主体回调 var handlerCode = isMethodPath ? // 执行你绑定的函数 "return " + handler.value + "($event)" : ( isFunctionExpression ? "return " + handler.value + "$event" : handler.value ); return `function($event){ ${code + handlerCode} }` }
拼接事件回调的3个重点
- 拼接按键的修饰符
- 拼接内置修饰符
- 拼接事件回调
-
Watch won't compile both Vue & scss files
2021-01-11 05:56:21<p>Also, it take a lot of time to compile (about 7seconds) with watch on. This never happened with the previous version of laravel mix (1.0.7)</p><p>该提问来源于开源项目:JeffreyWay/laravel-mix... -
when i compile the vue project components used by webpack 4.12
2021-01-11 06:24:22./node_modules/vue-loader/lib??vue-loader-options!./packages/form-item/src/form-item.vue?vue&type=script&lang=js @ ./packages/form-item/src/form-item.vue?vue&type=script&... -
深入浅出vue.js---全局API的实现原理----Vue.use、Vue.minxin、Vue.compile、Vue.version
2019-12-26 11:54:33一、Vue.use Vue.use(plugin); (1)参数 { Object | Function }plugin (2)用法 安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时...一、Vue.use
Vue.use(plugin);
(1)参数
{ Object | Function } plugin
(2)用法
安装Vue.js插件。如果插件是一个对象,必须提供install方法。如果插件是一个函数,它会被作为install方法。调用install方法时,会将Vue作为参数传入。install方法被同一个插件多次调用时,插件也只会被安装一次。
(3)作用
注册插件,此时只需要调用install方法并将Vue作为参数传入即可。但在细节上有两部分逻辑要处理:
1、插件的类型,可以是install方法,也可以是一个包含install方法的对象。
2、插件只能被安装一次,保证插件列表中不能有重复的插件。
(4)实现
Vue.use = function(plugin){ const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])); if(installedPlugins.indexOf(plugin)>-1){ return this; } <!-- 其他参数 --> const args = toArray(arguments,1); args.unshift(this); if(typeof plugin.install === 'function'){ plugin.install.apply(plugin,args); }else if(typeof plugin === 'function'){ plugin.apply(null,plugin,args); } installedPlugins.push(plugin); return this; }
1、在Vue.js上新增了use方法,并接收一个参数plugin。
2、首先判断插件是不是已经别注册过,如果被注册过,则直接终止方法执行,此时只需要使用indexOf方法即可。
3、使用toArray方法得到arguments。除了第一个参数之外,剩余的所有参数将得到的列表赋值给args,然后将Vue添加到args列表的最前面。这样做的目的是保证install方法被执行时第一个参数是Vue,其余参数是注册插件时传入的参数。
4、由于plugin参数支持对象和函数类型,所以通过判断plugin.install和plugin哪个是函数,即可知用户使用哪种方式祖册的插件,然后执行用户编写的插件并将args作为参数传入。
5、最后,将插件添加到installedPlugins中,保证相同的插件不会反复被注册。
二、Vue.mixin
Vue.mixin(mixin);
(1)参数
{ Object } mixin
(2)用法
1、全局注册一个混入(mixin),影响之后创建的每个Vue.js实例。
2、插件作者可以使用混入向组件注入自定义行为(例如:监听生命周期钩子)。不推荐在应用代码中使用。
Vue.mixin({ created:function({ var myOption = this.$options.myOption; if(myOption){ console.log(myOption); } }) }) new Vue({ myOption:'hello!' }) // => "hello!"
(3)Vue.mixin方法注册后,会影响之后创建的每个Vue.js实例,因为该方法会更改Vue.options属性。
(4)实现
import { mergeOptions } from '../util/index' export function initMixin(Vue){ Vue.mixin = function(minxin){ this.options = mergeOptions(this.options,mixin); return this; } }
1、mergeOptions会将用户传入的mixin与this.options合并成一个新对象,然后将这个生成的新对象覆盖this.options属性,这里的this.options其实就是Vue.options。
2、因为mixin方法修改了Vue.options属性,而之后创建的每个实例都会用到该属性,所以会影响创建的每个实例。
三、Vue.compile
Vue.compile(tempalte);
(1)参数
{ string } template
(2)用法
编译模板字符串并返回包含渲染函数的对象。只在完整版中才有效。
var res = Vue.compile('<div><span>{{msg}}</span></div>'); new Vue({ data:{ msg:'hello' }, render:res.render })
(3)并不是所有Vue.js的构建版本都存在Vue.compile方法。与vm.$mount类似,Vue.compile方法只存在于完整版中。(只有完整版包含编译器)
(4)实现
Vue.compile方法只需要调用编译器就可以实现功能。
Vue.compile = compileToFunctions;
compileToFunctions方法可以将模板编译成渲染函数。
四、Vue.version
(1)作用
提供字符串形式的Vue.js安装版本号。 这对社区的插件和组件来说非常有用,可以根据不同的版本号采取不同的策略。
(2)用法
var version = Number(Vue.version.split('.')[0]); if(version === 2){ <!-- Vue.js v2.x.x --> }else if(version ===1 ){ <!-- Vue.js v1.x.x --> }else{ <!-- 不支持的Vue.js版本 --> }
(3)Vue.version是一个属性。在构建文件的过程中,会读取package.json文件中的version,并将读取出的版本号设置到Vue.version上。
(4)具体步骤
1、Vue.js在构建文件的配置中定义了_VERSION_ 常量,使用rollup-plugin-replace插件在构建的过程中将代码中的常量_VERSION_替换成package.json文件中的版本号。
2、rollup-plugin-replace插件的作用是在构建过程中替换字符串。所以在代码中只需要将VERSION_ 赋值给Vue.version就可以在构建时将package.json文件中的版本号赋值给Vue.version。
Vue.version = '_VERSION_' <!-- 构建完成后,将类似下面这样 --> Vue.version = '2.5.2'
-
Vue 中的compile操作方法
2020-08-27 23:11:25主要介绍了Vue 中的compile操作方法,非常不错,具有参考借鉴价值,需要的朋友参考下吧 -
can not compile jsx in vue file
2020-11-30 11:20:49<div><h3>Version <p>4.7.2 <h3>Reproduction link ...<h3>Steps to reproduce <p>yarn build <h3>What is expected? <p>compile success <h3>What is actually happening?...vuejs/rollup-plugin-vue</p></div> -
Vue compile - Vue中的双大括号 {{ 是如何被解析(parse)的
2019-06-22 11:31:26 ![clipboard2]...