vue源码解析-实现一个基础的MVVM框架
基本介绍
vue.js采用数据劫持结合发布-订阅模式的方式,通过Object.defineProperty()来劫持各个属性的getter,setter,在数据变动时发布消息给订阅者,触发响应的监听回调。
主要功能:
- 实现一个指令解析器Compile
- 实现一个数据监听器Observer
- 实现一个Watcher去更新视图
入口函数和Compile编译类实现
HTML代码示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.fav"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
<button v-on:click="handleClick">点击我on</button>
<button @click="handleClick">点击我@</button>
<img v-bind:src="imgUrl" />
<img :src="imgUrl" />
</div>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
name: '编程哥',
age: 18,
fav: '看电影'
},
msg: '学习MVVM实现原理',
htmlStr: '大家学习得怎么样',
imgUrl: './logo.png'
},
methods: {
handleClick () {
console.log(this)
}
}
})
</script>
</body>
</html>
MVue.js代码示例:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
} else {
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
},
},
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现Observer劫持并监听所有属性
新建一个Observer.js类,示例代码如下:
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
// 递归遍历劫持
this.observe(value);
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get() {
// 订阅数据变化时,往Dep中添加观察者
return value;
},
set: (newVal) => {
this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
},
});
}
}
html代码引用:
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
在Mvue.js类中的调用方式:
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现观察者Watcher和依赖收集器Dep
Observer.js代码中新增类Dep(收集观察者与通知观察者)和Watcher:
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先把旧值保存起来
this.oldVal = this.getOldVal();
}
getOldVal() {
Dep.target = this;
let oldVal = compileUtil.getValue(this.expr, this.vm);
Dep.target = null;
return oldVal;
}
update() {
const newVal = compileUtil.getValue(this.expr, this.vm);
if (newVal !== this.oldVal) {
this.cb(newVal);
}
}
}
class Dep {
constructor() {
this.subs = [];
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher);
}
// 通知观察者去更新
notify() {
console.log("观察者", this.subs);
this.subs.forEach((w) => w.update());
}
}
class Observer {
constructor(data) {
this.observe(data);
}
observe(data) {
if (data && typeof data === "object") {
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
}
defineReactive(obj, key, value) {
// 递归遍历劫持
this.observe(value);
const dep = new Dep();
Object.defineProperty(obj, key, {
configurable: false,
enumerable: true,
get() {
// 订阅数据变化时,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
set: (newVal) => {
this.observe(newVal);
if (newVal !== value) {
value = newVal;
}
// 告诉Dep通知变化
dep.notify();
}
});
}
}
MVue.js代码:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
// {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentValue(expr, vm));
});
return this.getValue(args[1], vm);
});
} else {
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal);
});
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// 绑定更新函数 数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
}
}
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
}
}
}
实现双向的数据绑定和Proxy代理
MVue.js代码:
const compileUtil = {
getValue(expr, vm) {
return expr.split(".").reduce((pre, cur) => {
return pre[cur];
}, vm.$data);
},
setValue(expr, vm, inputVal) {
// 将expr的点语法的字符串转换成数组并设置最有一个值为inputVal
return expr.split(".").reduce((pre, cur, index, arr) => {
if (index === arr.length - 1) {
pre[cur] = inputVal;
}
return pre[cur];
}, vm.$data);
},
getContentValue(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
});
},
text(node, expr, vm) {
// expr:msg 学习MVVM实现原理 // <div v-text="person.fav"></div>
let value;
if (expr.indexOf("{{") !== -1) {
// {{person.name}} -- {{person.age}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node, this.getContentValue(expr, vm));
});
return this.getValue(args[1], vm);
});
} else {
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal);
});
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
html(node, expr, vm) {
const value = this.getValue(expr, vm);
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal);
});
this.updater.htmlUpdater(node, value);
},
model(node, expr, vm) {
const value = this.getValue(expr, vm);
// 绑定更新函数 数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal);
});
// 视图=>数据=>视图
node.addEventListener("input", (e) => {
// 设置值
this.setValue(expr, vm, e.target.value);
});
this.updater.modelUpdater(node, value);
},
on(node, expr, vm, eventName) {
const fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(eventName, fn.bind(vm), false);
},
bind(node, expr, vm, attrName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, attrName, value);
},
// 更新的函数
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, attrName, value) {
node.setAttribute(attrName, value);
}
}
};
class Compiler {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 1.获取文档碎片对象,放入内存中会减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
// 2.编译模板
this.Compiler(fragment);
// 3.追加子元素到根元素
this.el.appendChild(fragment);
}
/*
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-test="msg"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="msg">
*/
Compiler(fragment) {
// 获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach((child) => {
// console.log(child);
if (this.isElementNode(child)) {
// 是元素节点
// 编译元素节点
// console.log("元素节点", child);
this.compilerElement(child);
} else {
// 文本节点
// 编译文本节点
// console.log("文本节点", child);
this.compilerText(child);
}
if (child.childNodes && child.childNodes.length) {
this.Compiler(child);
}
});
}
compilerElement(node) {
const attributes = node.attributes;
[...attributes].forEach((attr) => {
const { name, value } = attr;
if (this.isDirective(name)) {
// 是一个指令 v-text v-html v-model v-on:click v-bind:src
const [, directive] = name.split("-"); // text html model on:click bind:src
const [dirName, eventName] = directive.split(":"); // text html model on bind
// 更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
// 删除有指令的标签上的属性
node.removeAttribute("v-" + directive);
} else if (this.isEventName(name)) {
// @click="handleClick"
const [, eventName] = name.split("@");
compileUtil["on"](node, value, this.vm, eventName);
// 删除标签上的绑定的事件
node.removeAttribute("@" + eventName);
} else if (this.isAttrName(name)) {
// :src="imgUrl"
const [, attrName] = name.split(":");
compileUtil["bind"](node, value, this.vm, attrName);
// 删除标签上绑定的属性
node.removeAttribute(":" + attrName);
}
});
}
compilerText(node) {
// {{}} v-text
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil["text"](node, content, this.vm);
}
}
isAttrName(attrName) {
return attrName.startsWith(":");
}
isEventName(attrName) {
return attrName.startsWith("@");
}
isDirective(attrName) {
return attrName.startsWith("v-");
}
node2Fragment(el) {
// 创建文档碎片
const f = document.createDocumentFragment();
let firstChild;
while ((firstChild = el.firstChild)) {
f.appendChild(firstChild);
}
return f;
}
isElementNode(node) {
// 判断是否是元素节点对象
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
this.$options = options;
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data);
// 2.实现一个指令解析器
new Compiler(this.$el, this);
this.proxyData(this.$data);
}
}
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newVal) {
data[key] = newVal;
}
});
}
}
}
HTML示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{person.td.msg}}</h3>
<div v-text="msg"></div>
<div v-text="person.fav"></div>
<div v-html="htmlStr"></div>
<input type="test" v-model="person.td.msg">
<button v-on:click="handleClick">点击我on</button>
<button @click="handleClick">点击我@</button>
<img v-bind:src="imgUrl" />
<img :src="imgUrl" />
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
person: {
td: {
msg: '系统td'
},
name: '编程哥',
age: 18,
fav: '看电影'
},
msg: '学习MVVM实现原理',
htmlStr: '大家学习得怎么样',
imgUrl: './logo.png'
},
methods: {
handleClick () {
this.person.name = '学习222';
}
}
})
</script>
</body>
</html>
核心面试题讲解:阐述一下MVVM响应式原理
Vue是采用数据劫持配合发布订阅模式的方式,通过Object.defineProperty()
来劫持各个属性的getter和setter,在数据变动时,发布消息给依赖收集器(Dep),去通知观察者(Watcher)做出对应的回调函数,去更新视图。
MVVM作为绑定的入口,整合Observer,Compile和Watcher三者,通过Observer来监听数据变化,通过Compile来解析编译模板指令,最终利用Watcher建立Observer、Compile之间的桥梁,达到数据变化=>视图更新;视图交互变化=>数据model变更的双向绑定效果
vue源码解析-实现一个基础的MVVM框架的更多相关文章
- 【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy.它内 ...
- 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现
写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...
- Vue源码解析---数据的双向绑定
本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...
- Vue源码解析之nextTick
Vue源码解析之nextTick 前言 nextTick是Vue的一个核心功能,在Vue内部实现中也经常用到nextTick.但是,很多新手不理解nextTick的原理,甚至不清楚nextTick的作 ...
- 【VUE】Vue 源码解析
Vue 源码解析 Vue 的工作机制 在 new vue() 之后,Vue 会调用进行初始化,会初始化生命周期.事件.props.methods.data.computed和watch等.其中最重要的 ...
- Vue 源码解析:深入响应式原理(上)
原文链接:http://www.imooc.com/article/14466 Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScri ...
- Vue源码解析(一):入口文件
在学习Vue源码之前,首先要做的一件事情,就是去GitHub上将Vue源码clone下来,目前我这里分析的Vue版本是V2.5.21,下面开始分析: 一.源码的目录结构: Vue的源码都在src目录下 ...
- Vue源码解析之数组变异
力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...
- Fresco源码解析 - 创建一个ImagePipeline(一)
在Fresco源码解析 - 初始化过程分析章节中, 我们分析了Fresco的初始化过程,两个initialize方法中都用到了 ImagePipelineFactory类. ImagePipeline ...
- vue源码解析之observe
一. vue文档中有"由于 JavaScript 的限制,Vue 不能检测以下数组的变动",是否真是由于JavaScript的限制,还是出于其他原因考虑 当你利用索引直接设置一个数 ...
随机推荐
- 解决方案 | 在 Tkinter 中导入 pywinauto/pyautogui 时窗口大小发生变化
上面问题也可以换一个说法,pywinauto/pyautogui 时改变了tkinter的原有的窗口大小.这个问题困扰了我好几天而且网上有这样的问题但是并没有答案,今天摸索出答案给大家分享下.解决方法 ...
- Odoo 基于Win10搭建基于Win10搭建odoo14开发环境搭建
实践环境 win10 Python 3.6.2 odoo_14.0.latest.tar.gz 下载地址: https://download.odoocdn.com/download/14/src?p ...
- 《最新出炉》系列入门篇-Python+Playwright自动化测试-54- 上传文件(input控件) - 上篇
1.简介 在实际工作中,我们进行web自动化的时候,文件上传是很常见的操作,例如上传用户头像,上传身份证信息等.所以宏哥打算按上传文件的分类对其进行一下讲解和分享. 2.上传文件的API(input控 ...
- Scratch全套Q版三国人物角色素材包免费下载
全新Q版三国人物角色素材包,内含142张细腻可爱的Q版风格图片,涵盖三国名将.士兵.场景等丰富元素,为scratch爱好者提供多样选择,适合各类三国主题创作. 免费下载:www.xiaohujing. ...
- Snipaste截图工具-测试工程师强推
Snipaste主要功能是截图和贴图,网上下载直接安装即可. 个人认为Snipaste比其它截图好用的点: 快捷键简单.Snipaste只需按F1截图,不会和其它截图工具的快捷键冲突 贴图功能.贴图功 ...
- 【Mybatis】08 ResultMap、Association、分步查询、懒加载
ResultMap自定义结果集 可以把查询返回的结果集封装成复杂的JavaBean对象 原来的ResultType属性,只能把查询到的结果集转换为简单的JavaBean 什么是简单的JavaBean对 ...
- 【Centos】RPM安装Mysql8
先去官网下载RPM包,没想到RPM包是红帽发行版 https://dev.mysql.com/downloads/mysql/ 使用wget直接下载到Centos里面: wget https://cd ...
- 【Java】POI Excel导出 动态行合并
一般情况: Excel导出一般都是一行一行的记录输出 . 这是Controller代码: 标题行的设置: 标题行会设置获取的结果集的字段名,数据会自动根据设置的名称匹配装填 特殊的需求: 如页面的效果 ...
- 光刻机巨头ASML公布了其最新的品牌短片《站在巨人的肩膀上》
光刻机巨头ASML公布了其最新的品牌短片<站在巨人的肩膀上>: 荷兰光刻机:ASML使用AI工具midjourney和runway制作宣传片 这个时长1分50秒短片的特别地方在于,它是完全 ...
- 乌克兰学者的学术图谱case2
======================================= 0. 学者:Солонін Ю.М. 中文翻译名:索洛宁·尤里·米哈伊洛维奇 英文翻译名:Solonin Yuriy M ...