第六篇-仿写Vue生态系列模板loader与计算属性

6次阅读

共计 5939 个字符,预计需要花费 15 分钟才能阅读完成。

(第六篇)仿写 ’Vue 生态 ’ 系列___” 模板 loader 与计算属性 ”


本次任务

  1. 编写 ’cc-loader’, 使我们可以使用 ’xxx.cc’ 文件来运行我的框架代码.
  2. 为 ’cc_vue’ 添加生命周期函数.
  3. 新增 ’ 计算属性 ’.
  4. 新增 ’ 观察属性 ’.
一.’cc-loader’ 的定义与编写
  1. 本次只是编写一个最基础的版本, 后续完善组件化功能的时候, 会对它有所改动.
  2. 使 ’webpack’ 可以解析后缀为 ’cc’ 的文件.
  3. 必须做到非常的轻量.

让我们一步一步来做出这样一个 ’loader’, 首先我先介绍一下文件结构, 在 ’src’ 文件夹平级建立 ’loader’ 文件夹, 里面可以存放以后我做的所有 ’loader’.

不可或缺的步骤就是定义 ’loader’ 的路径.
cc_vue/config/common.js
新增 resolveLoader 项

 resolveLoader: {
   // 方式 1: 
   // 如果我书写 require('ccloader');
   // 那么就会去 path.resolve(__dirname, '../loader')寻找这个引入.
    alias: {ccloader: path.resolve(__dirname, '../loader')
     },
    // 方式 2: 当存在 require('xxx'); 这种写法时, 先去 'node_modules' 找寻, 找不到再去 path.resolve(__dirname,'../loader')找找看.
    modules:[
        'node_modules',
        path.resolve(__dirname,'../loader')
    ]
  },

‘loader’ 文件的配置写完了, 那么可以开始正式写这个 ’loader’ 了.
cc_vue/loader/cc-loader.js

// 1: 'source' 就是你读取到的文件的代码, 他是 'string' 类型.
function loader(source) {
 // .. 具体处理函数
 // 2: 处理完了一定要把它返回出去, 因为可能还有其他 'loader' 要处理它, 或者是直接执行处理好的代码.
  return source;
}

module.exports = loader;

模板的定义

  1. 本次暂不处理 title, 因为没啥技术含量暂时也没必要.
  2. ‘#–style–>’ 会被替换为 css 的内容.
  3. ‘#–template–>’ 会被替换为模板的内容.
<!DOCTYPE html>
<html lang='en'>
  <head>
    <meta charset='UTF-8' />
    <meta name='viewport' content='width=device-width, initial-scale=1.0' />
    <meta http-equiv='X-UA-Compatible' content='ie=edge' />
    <title> 编译模板 </title>
    #--style-->
  </head>
  <body>
      <div id="app">
          #--template-->
      </div>
  </body>
</html>

最终要达到的 ’.cc’ 文件的书写方式

<template>
  <div class="box"
       v-on:click='add'>
    <span>{{n}}</span>
  </div>
</template>
<script>
    console.log('此处也可执行代码');
    export default {
      el: "#app",
      data: {n: 2},
      methods: {add() {this.n++;}
      }
    };
</script>
<style>
    .box {
      border: 1px solid;
      height: 600px;
    }
</style>

读取 ’.cc’ 文件
cc_vue/config/common.js

 {
   test: /\.cc$/,
   use: ['cc-loader']
 },

loader 正式走起
cc_vue/loader/cc-loader.js

// 解析 cc 文件模板
// fs 模块主要用来读写文件
let fs = require('fs');
let path = require('path');

function loader(source) {
  // 1: 我们先把定义好的 '模板文件' 读取出来.
  let template = fs.readFileSync(path.resolve(__dirname, './index.html'),
    'utf8' // 可能存在中文的
  );
  // 2: 去除空格, 这样能更好的匹配...
  source = source.replace(/\s+/g, ' ');
  // 3: 匹配出 'css' 样式
  let s = (/<style>(.*)<\/style>/gm.exec(source)||[])[1];
  // 4: 匹配出 js 代码
  let j = /<script>(.*)<\/script>/gm.exec(source)[1];
  // 5: 匹配出模板元素
  let t = /<template>(.*)<\/template>/gm.exec(source)[1];
  // 6: 注入模板元素
  template = template.replace(/(#--template-->)/, t);
  // 7: 注入样式, 防止出现 undefined 啥的...
  template = template.replace(/(#--style-->)/, `<style> ${s||''}</style>`);
  // 8: 把这个处理好的模板结构放入最后要执行的 'html' 文件中
  fs.writeFileSync(path.resolve(__dirname, '../public/index.html'),
    `${template}`,
    err => console.log(err)
  );
  // 9: 这里我们把 'js' 代码继续导出, 这样其他文件引入我们 '.cc' 文件其实就是引入了 '.cc' 文件的 'js' 脚本.
  return j;
}

module.exports = loader;

整体来说还是挺简易的, 刚开始做的时候想复杂了, 接下来我们就来引用它.
cc_vue/src/index.js

import component from '../use/5:loader 相助 /index.cc';
//...
// 因为最后我导出的只有 'js' 脚本, 也就是 'new' 的时候的配置项, 所以直接进行下面的操作就可以.
new C(component);
二. 生命周期函数的注入

生命周期这种东西面试总考, 但是说实话没有多神秘, 核心就是个回调函数而已.
本次我就做两个生命周期, 定义如下.

  1. created: this 实例上的数据处理完毕, 但此时获取不到 dom.
  2. mounted: dom 渲染到页面上, 整体流程结束时触发.
  3. 对于用户没传坐下兼容.

cc_vue/src/Compiler.js

class Compiler {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: 解析元素, 文档流也是对象
    this.compile(fragment);
    // 4: 进行生命周期函数, 他真的一点都不高大上
    vm.$created && vm.$created.call(vm);

    // 最后一步: 处理完再放回去
    this.el.appendChild(fragment);
    // 调用声明周期钩子
    vm.$mounted && vm.$mounted.call(vm);
  }
 //...

cc_vue/use/5:loader 相助 /index.cc
完整的测试一下

<template>
  <div class="box"
       v-on:click='add'>
    <span>{{n}}</span>
  </div>
</template>
<script>
console.log('此处也可执行代码');
export default {
  el: "#app",
  data: {n: 2},
  methods: {add() {this.n++;}
  },
  created() {let d = document.getElementsByClassName("box");
    console.log("created", this.n, d);
  },
  mounted() {let d = document.getElementsByClassName("box");
    console.log("mounted", this.n, d);
  }
};
</script>
<style>
.box {
  border: 1px solid;
  height: 600px;
}
</style>

有兴趣的朋友可以测试一下, 自己做这些东西真的通有趣的.

三. 计算属性的编写

计算属性属于很常用的功能了, 他的神奇之处在于其中任何一个值的变化都会引起结果的同步更新, 下面我就来实现这种看起来很棒的效果.
cc_vue/src/index.js

class C {constructor(options) {
  //...
  // 把 $computed 代理到 vm 身上
    this.proxyVm(this.$computed, this, true);

具体的代理过程需要有所调整

 proxyVm(data = {}, target = this, noRepeat = false) {for (let key in data) {if (noRepeat && target[key]) {
        // 防止 data 里面的变量名与其他属性重复
        throw Error(` 变量名 ${key}重复 `);
      }
      Reflect.defineProperty(target, key, {enumerable: true, // 描述属性是否会出现在 for in 或者 Object.keys()的遍历中
        configurable: true, // 描述属性是否配置,以及可否删除
        get() {
          // 第一版
          // 计算属性上的值肯定是函数啊, 所以这里要进行一下判断
          // 因为这个 for 只走一层, 所以不会出现与内部值 '重叠' 的现象
          // 每次把 this 指向纠正
          if (this.$computed && this.$computed.hasOwnProperty(key)) {return data[key].call(target);
          } else {return Reflect.get(data, key);
          }
          // 第二版, f 是新传进来的变量, 代表是不是函数类型
           return f ? data[key].call(target) : Reflect.get(data, key);
        },
        set(newVal) {if (newVal !== data[key]) {Reflect.set(data, key, newVal);
          }
        }
      });
    }
  }

我说下原理

  1. 若 computed 存在, 则他的取值方式变为执行 ’call’.
  2. 比如说我用到了 ’v’ 这个计算属性, 他的值是 ’return n+m’, 在行间调用它的时候{{v}}, 会走到 CompileUtil.text 这个函数, 这里有一步 ’new Watcher…’ 操作.
  3. ‘new Watcher…’ 里面会去调用 ’getVal’ 函数, 拿到最新的变量来更新 dom.
  4. 这个 ’new Watcher…’ 会被记录到对应的 Dep 里面, ‘new’ 的过程中 ’Dep.target’ 会被赋值上这个 ’Watcher’, 也就是说以后当这个 ’v’ 有变化的时候, 会触发这个 ’new Watcher…’ 里面的更新操作.
  5. ‘Watcher’ 被 ’new’ 的时候, 会传入 ’vm’ 与 ’expr 表达式 ’, 这个表达式执行的时候里面的所有 ’this. 变量 ’ 会被加上标记, 所以才导致里面的任何变量的变化都会引起计算属性的变化.
  6. 比如出现 {{n+m+v}} 的情况, 其实我是把他们当做整体进行解析的, 所以这种情况下计算属性依然没问题.

下面是我的测试代码

<template>
  <div class="box">
    <button v-on:click='addn'> n++</button>
    <button v-on:click='addm'> m++</button>
    <p>n: {{n}}</p>
    <p>m: {{m}}</p>
    <p>x: {{x}}</p>
    <p>n+m+x: {{v}}</p>
    <p>v+v: {{v+v}}</p>
  </div>
</template>
<script>
export default {
  data: {
    n: 1,
    m: 1,
    x: 1
  },
  methods: {addn() {
      this.n++;
      console.log(this.v)
    },
    addm() {this.m++;}
  },
  computed: {v() {return this.n + this.m + this.x;}
  }
};
</script>
四. 观察者的编写

既然写了 ’ 计算属性 ’ 那就顺手把观察属性一并完成把, 这个功能也挺有意思的, 我们可以使用这个属性对一个量进行 ’ 观察 ’, 当这个量变化的时候触发我们的函数, 同时传入两个参数新值与老值.

我说下思路:

  1. 既然是监控一个值, 那大几率应该是在双向绑定的时候进行监控.
  2. 这次先做基本功能, 像是 ’x.y.u.z’ 这种观察模式暂时不做.
  3. 指定 this 为 vm 的同时传入新值与旧值.

cc_vue/src/Observer.js

//...
// 新增 init 变量, 用来区别是不是第一层数据
// data = {name:'cc',type:['金毛','胖子']};
// name 属于第一层数据, '金毛' 属于第二层数据
  observer(data, init = false) {let type = toString.call(data),
      $data = this.defineReactive(data, init);
//...
  defineReactive(data, init) {
  //...
  set(target, key, value) {if (target[key] !== value) {if (init) { // 对 data 的数据进行 watch 处理
            (_this.vm.$watch||{})[key] && // 先确定有 watch
              _this.vm.$watch[key].call(_this, value, target[key]);
          }
          target[key] = _this.observer(value);
          dep.notify();}
        return value;
      }
end

本次书写的功能都挺有意思的, 写的时候也很开心, 毕竟代码是让人快乐的东西, 下一期我要往框架里面加一些好玩的功能, 具体加什么还没确定, 但是我比较喜欢一些搞怪的, 反正大家一起玩耍呗.

框架 github
ui 组件库
个人技术博客
更多文章,ui 库的编写文章列表

正文完
 0