( 第二篇 )仿写'Vue生态'系列___'模板小故事.'
本次任务
- 承上: 完成第一篇未完成的'热更新'配置.
- 核心: 完成'模板解析'模块的相关编写, 很多文章对模板的解析阐述的都太浅了, 本次我们一起来深入讨论一下, 尽可能多的识别用户的语句.
- 启下: 在结构上为'双向绑定'、watch、dep等模块的编写打基础.
最终效果图
一. 模板页面
我们既然要开发一个mvvm, 那当然要模拟真实的使用场景, 相关的文件我们放在:'cc_vue/use'路径下, 代码如下:
- 'cc_vue/use/1:模板解析/index.html', 本篇专门用来展示模板解析的页面
- 'cc_vue/use/1:模板解析/index.js', 本篇专门用来展示模板解析的逻辑代码
本来要展示html文件的信息, 但是内容冗长而且没有什么技术可言, 所以不在此展示了.
function init(){
new C({
el: '#app',
data: {
title: '努力学习',
ary: [1, 2, 3],
obj: {
name: '金毛',
type: ['幼年期', '成熟期', '完全体']
},
fn() {
return '大家好我是: ' + this.obj.name;
}
}
});
}
export default init;
一. 配置文件与简易的热更新
之所以说它是简易的, 原因是我们并不会去做到很细致, 比如本次不会去追求每一次的精准更新, 而是每一次都会对整体采取更新, 毕竟本次工程热更新只是一个知识点, 我们还有很多很多更重要的事要做emmmm
一些自己的观点
热更新并不是算很神奇, 我之前配置过vuex的热更新相关, 后来总结了一下, 它与回调函数概念差不多, 原理就是当编辑器, 或者是serve检测到你的文件有相应变化的时候, 执行一个回调函数, 这个回调函数里面就是一些重新渲染, 更新dom等等的操作, 你可能会有疑问, vue的热更新做的那么好, 也没看见有什么热更新的回调函数啊, 其实这都归功于'vue-loader', css 热更新考的是css-loader, 他们在处理文件的阶段就把热更新的回调代码注入了js文件里面, 所以我们才会是无感的, 所以没有'loader'帮助我们注入热更新, 那本次我们就自己手动实现.🐅
有兴趣的同学可以去看看官网的教程, 有点短小
配置文件拆分为生产环境与开发环境(虽然咱们用不上生产, 但是用于学习还挺好的);
- build---生产打包
- common---公共打包
- dev---开发打包
common.js
const dev = require('./dev');
const path = require('path');
const build = require('./build');
const merge = require('webpack-merge');
const common = {
entry: {
main: './src/index.js'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../dist')
},
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader?cacheDirectory=true'
}
]
}
};
module.exports = env => {
let config = env == 'dev' ? dev : build;
return merge(common, config);
};
死磕知识点之 merge
配置过webpack的同学都知道, merge算是个灵魂人物了, 基本上每个工程多会有多种类的打包, 配置文件更是不计其数, 想要把这帮配置整合起来并非易事, 那我们就来看看webpack-merge的效果吧.
npm i webpack-merge -D
我们进行如下的实验
let obj1 = {
a: 1,
b: [1],
c: { name: 1 }
};
let obj2 = {
a: 2,
b: [2],
c: { age: 2 }
};
merge(obj1, obj2)
结果为:{
a: 2,
b: [1, 2],
c: { name: 1, age: 2 }
};
- 策略上, 后面的覆盖前面的.
- 遇到普通值, 直接覆盖.
- 遇到数组, 会使用push().
- 遇到对象, 会选择覆盖原有属性, 若无原有属性则新增.
- 其实挺有趣的, 这个方法我可以把它用在其他地方, 不是局限在webpack配置里面, 活学活用最开心🦄.
解释一下本次的用法
// 导出一个函数, 只有函数才可以接收传过来的参数.
module.exports = env => {
// 直接判断参数的类型, 来决定用那一套配置;
let config = env == 'dev' ? dev : build;
// 把配置与公共基础配置融合, 导出去
return merge(common, config);
};
启动的命令的调整
- --hot 启动热更新, 配文件里面写了也可以不加这句.
- --env dev 为配置传入参数 字符串'dev', 这里必须写作 --env.
- --config ./config/common.js 指定需要调用的配置文件是 ./config/common.js 而不是webpack.config.js.
"serve": "webpack-dev-server --hot --env dev --config ./config/common.js",
热更新的使用
cc_vue/config/dev.js
const path = require('path');
const Webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
devtool: 'source-map',
devServer: {
port: 9999,
hot: true, // 这里一定要开启
// hot 和 hotOnly 的区别是在某些模块不支持热更新的情况下,前者会自动刷新页面,后者不会刷新页面,而是在控制台输出热更新失败
},
plugins: [
// 相关的插件也需要载入
new Webpack.HotModuleReplacementPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../use/1:模板解析/index.html'),
})
]
};
接下来写代码, 我们就会把热更新模块用起来
二. 编写核心C类
cc_vue/src/index.js
import '../public/css/index.css';
import Compiler from './Compiler.js';
// 其实这个类的作用非常单一
// vue的源码里面这个类只是判断了 用户是否用new, 还有就是调用init
class C {
constructor(options) {
// 这里我们不管用户传的是什么, 直接全部挂在到自身, 添加$符号表示.
for (let key in options) {
this['$' + key] = options[key];
}
// 我们这里实例化了模板的类,
// 也就是本次的主题, 模板相关解析
new Compiler(this.$el, this);
}
}
// 比较传统的写法, 把它挂在全局, 其实所有操作, 只暴露了这一个变量给用户;
window.C = C;
引入热更新, 只是个例子而已, 本次不会这样使用
import init from '../use/1:模板解析/index';
...
window.C = C;
// 执行初始化, 代码上面已经粘过了.
init();
// 当存在热更新的时候
if (module.hot) {
// 监听这个文件的变化
module.hot.accept('../use/1:模板解析/index.js', function() {
// 变化之后做什么
init();
});
}
为什么本次不使用
原因就是, 工程刚刚起步, 还没有更新方法, 就算检测到文件变化也没有用, 比如用户输入 {{a}}, 他已经被转为了 a对应的变量, 这个时候想要更新这个值需要触发他自身的updater方法, 但是这个方法咱们还没写, 所以本次用不上, 直接用刷新更新就可以了, 以后会去做特定某一模块的更新操作.
三. 模板解析模块
cc_vue/src/Compiler.js
// 解析模板系列节目
class Compiler {
// 在你没有给#app的时候, 我来给你一个默认的#app
// 因为本次工程不涉及, '延迟挂载'所以可以这样写
// vue会涉及到延迟挂载
constructor(el = '#app', vm) {
this.vm = vm;
// 1: 如果传的是dom就直接用
// 是字符串就获取一下
this.el = this.isElementNode(el) ? el : document.querySelector(el);
// 2: 制作文档碎片
let fragment = this.node2fragment(this.el);
// 3: 解析元素, 文档流也是对象
// compile 解析的核心代码, 这个逻辑下面讲
this.compile(fragment);
// 最后一步: 处理完再塞回去
this.el.appendChild(fragment);
}
/**
* @method 判断是不是元素节点
* @param { node } 要判断的节点
* @return { boolean } 是否为元素节点, 元素节点1
*/
isElementNode(node) {
return node.nodeType === 1;
}
/**
* @method 判断是不是文本节点
* @param { node } 要判断的节点
* @return { boolean } 是否为标签节点, 文本节点为3
*/
isTextNode(node) {
return node.nodeType === 3;
}
/**
* @method 把节点全部放入文档流里面
* @param { node } 想要遍历的节点对象
* @return { fragment } 返回生成的文档流
*/
node2fragment(node) {
// 创建文档流
let fragment = document.createDocumentFragment();
while (node.firstChild) {
// 插入文档流
fragment.appendChild(node.firstChild);
}
return fragment;
}
}
export default Compiler;
compile 这个方法最重要, 我们下面讲
死磕知识点
- 为啥管实例叫做'vm', 摘自vue官网(vue虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例)
- nodeType, 每一个节点都有对应的类型,
// 元素节点, 1
// 属性节点, 2
// 文本节点, 3
// 注释节点, 8 这个也很有用的
// document, 9
// DocumentFragment 11 - createDocumentFragment() 方法, 我一般管它叫做创建文档流碎片, 众所周知操作dom的代价高昂, 而如果你需要插入10000个dom, 那就真的是gg了,但是有了fragment问题就好解决了, 他是虚拟的, 但是可以像元素一样操作, 所以可以把元素先放在他里面, 然后把它放到目标父级里面就可以了, 这样只用插入一次, 他也不会形成元素在页面上.
- appendChild 与 append
appendChild 在 Node.prototype 上
append 在 Document.prototype 上
都表示, 添加到元素的最后一位
都不支持 传入字符串生成标签, 比如'<li>1</li>',
append()可以同时传入多个节点或字符串,没有返回值;
appendChild()只能传一个节点,且传字符串会报错;除非传入document.createTextElement('字符串')
四. 处理花括号
vue处理的挺复杂的, 本次练习我更希望锻炼自己的思维, 所以选择自己的方式来做;
...
compile(node) {
let childNodes = node.childNodes;
[...childNodes].map(child => {
if (this.isElementNode(child)) {
// 元素节点处理指令方面
this.compileElement(child);
this.compile(child);
} else if (this.isTextNode(child)) {
// 文本节点处理{{}}这种事情
this.compileText(child);
}
});
}
我们本篇只针对文本节点的处理, 接下来是的compileText思路
/**
* @method 处理文本节点
* @param { node } 想要遍历的节点对象
*/
compileText(node) {
let content = node.textContent;
// 有花括号才会去处理, 没有就算了
if (/\{\{.+?\}\}/.test(content)) {
CompileUtil.text(node, content, this.vm);
}
}
由于工具类会很多, 所以我们单开了一个文件
cc_vue/src/CompileUtil.js
CompileUtil.text
const CompileUtil = {
text(node, expr, vm) {
let content = expr.replace(/\{\{(.+?)\}\}/g, ($0, $1) => {
// 把匹配出的每一个花括号内的内容, 都交给这个函数去取值;
return this.getVal(vm, $1);
});
this.updater.textUpdater(node, content);
}
//...
updater: {
textUpdater(node, value) {
node.textContent = value;
}
}
};
export default CompileUtil;
五. 取得对应值(最有趣的就是他😁)
getVal这个函数才是最有趣的, 我看到过很多版本, 最后会介绍我研究出的版本...各位见笑了
思路一, 网上最常见的(只处理一种情况,只处理一层 🤷♀️纯糊弄人性质等同诈骗)
只能处理 {{a.b}} 或者是 {{ a }}
// 是个例子
getVal(){
let str = 'a.c';
let data ={
a:{ c:2 }
}
// 单纯的按照'.'拆分
str = str.split('.');
let result = data;
str.map(item=>{
// 每次取一下值
result = result[item]
})
return result
}
上面的方法一看就是没经过思考, 随便应付了事....
无法取得a['b']这种值, 如果里面出现运算 a['b']+a['c'] 更别说遇到函数了...
看了很多文章, 我又看到了思路2的做法
思路二, 处理两种取值方式, 分开处理运算符号
getVal(){
// 挺繁琐的我说一下思路吧, 毕竟不是我写的
}
- 用指针的方式, 把字符串的每个字符逐一排查.
- 遇到 '[' 则把它单独拿出来, 进行'思路一'的操作, 直到遇到 ']'.
- 遇到数字要做特殊处理, 比如a[1], 不可能处理成a[data.1].
- 遇到'''或者'"'要做特殊处理 a['b'] 这个 b 不可处理为data.b.
- 反复重复上面的操作, 把原字符串转换为'.'链接的形式, 进行思路一的方式的取值.
不知道怎么想的... 一看就不是正道,
代码过于繁琐, 判断太多, 但是没还有覆盖全所有的情况
属于是铁憨憨写法, 但是至少能看出来, 这种做法的人动脑子了.
思路三, eval
其实我最开始想用 with关键字来实现, 但是没有使用
- 性能太差了, 比正常写代码慢了20-30倍
- 严格模式不让用😢
死磕知识点 with
let obj = {}
obj.a=2;
obj.b=3
let obj = {}
with(obj){
a=2;
b=3;
}
思路就是用户写在{{}}里面的语句, 没有写this, 但是指向都是this, 所以我要在头部给他们加this前缀.
- 循环拿出变量, 在头部拼接this, 这个太😓...
- 创建一个环境, 在这个环境里的变量都是this身上的😼.
也就是我把this.$data(再写几篇后会有变化)身上的值, 全部拿到我现在的环境里面,
举个例子 this.a = 1 那我直接 var a= this.a 函数里面的其他地方调用a就相当于调用this.a了
getVal(vm, expression) {
let result,
__whoToVar = '';
// 循环data上的每一个属性
for (let i in vm.$data) {
// data下一篇会做成代理, 并且去掉原型上的属性
let item = vm.$data[i];
// 函数比较特殊, 因为需要改变声明方式
// 兼容传参
// 修正this指向为vm实例
if (typeof item === 'function') {
__whoToVar += `function ${i}(...arg){return vm.$data['${i}'].call(vm.$data,...arg)}`;
} else {
// 普通变量直接let
__whoToVar += `let ${i}=vm.$data['${i}'];`;
}
}
// 执行出的结果用result接取
__whoToVar = `${__whoToVar}result=${expression}`;
eval(__whoToVar);
return result;
},
上面的做法做完, 就可以达到第一张效果图所示的目的了
end
写文章真是太卡了, 以后要控制文章的字数了,eeee.
下一章进入更有趣, 双向绑定的编写, 我很喜欢的部分.
大家都可以一起交流, 共同学习,共同进步, 早日实现自我价值!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。