如何实现一个简易版本的vue
前言
这是在下第一次在掘金上写技术博客,还是有点紧张,如有不足之处还请各位大拿指正,我会及时改正,也感谢各位小伙伴能够阅读鄙人的陋文
功能简述
支持.vue语法糖,支持script与template标签,以及双大括号语法,v-if/v-show指令和@事件处理。未来我或许会新增style支持,生命周期回调以及其他相关语法糖
我的思路
1.使用自定义vue-loader讲vue文件解析为js文件 2.引入js文件作为对象传递给手写的vue进行解析 3.vue拿到对象后进行初始化,将数据放入Vue实例 4.将data通过proxy进行数据双向绑定 5.将temlpate作为dom树的方式进行预处理 6.将dom树与methods和data进行依赖绑定 7.根据依赖处理vue语法的指令 7.渲染成真实的浏览器dom树 8.挂载到真实dom中 9.在更新数据时触发update方法,将具有变更的数据重新处理
正文
源码参考
- 欢迎star支持 github.com/codingJJJ/m…
项目搭建
- 初始化项目 打开vscode在终端中使用npm init -y初始化package.json文件
npm init -y
- 安装项目依赖 安装项目依赖时候需可能会出现webpack相关版本兼容问题,大家可以按照我pakage.json配置的版本进行安装
// 我的package.json文件配置
{
"name": "mini-vue",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.70.0",
"webpack-cli": "^4.9.2",
"html-webpack-plugin": "^5.5.0",
"webpack-dev-server": "^4.7.4"
},
"dependencies": {
}
}
npm install -D webpack webpack-cli html-webpack-plugin webpack-dev-server
- 配置webpack
const { resolve } = require('path')
const WebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bound.js',
path: resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /.vue$/,
// 这里因为我们是手写loader,需要配置loader位置
use: resolve(__dirname, 'vue-loader')
}
]
},
devtool: 'source-map',
plugins: [
new WebpackPlugin()
]
}
- 建立文件目录
- src
- index.js // 项目入口
- test.vue // 用于测试的vue文件
- vue // 用于存放自己写的vue
- index.js
- vue-loader // 存放自己写的webpack
- index.js
vue-loader编写
- 补全scr目录 在编写之前,我们需要把src内的index.js文件进行补全便于测试
// src/index.js
import test from './test.vue';
console.log(test)
- 编写loader入口文件 因为loader是运行在nodejs环境下,所以我们需要使用commonjs规范进行编写loader
// vue-loader/index.js
function vueLoader(source) {
console.log(source)
}
module.exports = vueLoader;
这时候我们补全package.json的脚本 在scripts下加入dev脚本
// package.json
"scripts": {
"dev": "webpack-dev-server"
},
补全之后,我们可以调试一下,使用npm run dev,如果在脚本下打印了.vue脚本,说明我们的loader成功啦
- 关于loeader的补充 loader原理,loader其实是webpack给我们提供的一个可以处理js之外其他不支持js的脚本,它应该是一个函数,其传参就是我们需要处理的脚本文件,所以这时我们能通过source打印出相应的文件.像我们最终的目的是需要将.vue文件最终解析为大致如下的结构:
// 解析之后的.vue文件大致如下
{
template: ...,
data: ...,
methods: ...
}
- 解析source 那么我们在解析的时候需要提取tempalte和script的内容,然后将template的内部注入script作为一个新的对象导出 。
- 首先我们使用正则表达式提取template和script内容,新建一个vue-loader/pare.js文件
// 解析原文件
function parse(source) {
const template = getTemplate(source)
const script = getScript(source)
return { template, script }
}
// 获取template的内容
function getTemplate(source) {
const reg = /(?<=<template>)([\s\S]+?)(?=<\/template>)/g;
return source.match(reg)?.[0]
}
// 获取script的内容
function getScript(source) {
const reg = /(?<=<script>)([\s\S]+?)(?=<\/script>)/g;
return source.match(reg)?.[0]
}
// 导出parse
module.exports = {
parse
}
这时我们在vue-loader/index.js文件中导入该文本,并测试
const { parse, mergeTemplateToScript } = require('./parse')
function vueLoader(source) {
const { template, script } = parse(source);
console.log(template, script)
}
module.exports = vueLoader;
这时,可以通过npm run dev重新跑一下项目,如果看到tempalte和script的内容被打印了,恭喜你,第一步成功啦!!!
- 将template内容注入script 这时我们只需要完成最后一步把template的文件注入到script并导出该内容.我们在vue-loader/parse.js文件中新写一个注入的方法
// 将templatre 合并到script
// 这里的正则大致意思是匹配export default 后面的第一个花括号 {
function mergeTemplateToScript(temp, script) {
return script.replace(/(?<=export\s+default\s+)(\{)/, () => {
return `
{
template: \`${temp}\`,
`
})
}
最终vue-loader目录下的文件如下
// vue-loader/index.js
const { parse, mergeTemplateToScript } = require('./parse')
function vueLoader(source) {
const { template, script } = parse(source);
return mergeTemplateToScript(template, script)
}
module.exports = vueLoader;
// vue-loader/parse.js
function parse(source) {
const template = getTemplate(source)
const script = getScript(source)
return { template, script }
}
function getTemplate(source) {
const reg = /(?<=<template>)([\s\S]+?)(?=<\/template>)/g;
return source.match(reg)?.[0]
}
function getScript(source) {
const reg = /(?<=<script>)([\s\S]+?)(?=<\/script>)/g;
return source.match(reg)?.[0]
}
// 将templatre 合并到script
function mergeTemplateToScript(temp, script) {
return script.replace(/(?<=export\s+default\s+)(\{)/, () => {
// 这里我们是
return `
{
template: \`${temp}\`,
`
})
}
module.exports = {
parse,
mergeTemplateToScript
}
这时,npm run dev,打开浏览器http://localhost:8080/ 如果能看到对象被的打印,那么恭喜你,你成功地完成了vue-loader的编写。
编写vue
接下来,我们开始写vue的内容
初始化vue
在vue/index.js中,我们开始编写Vue构造函数
// vue/index.js
// 导入init函数
import init from './init'
function Vue(component) {
this.init(component)
}
// 将初始化的init方法挂载到Vue原型上
Vue.prototype.init = init
// 因为我们可能也是通过createApp进行创建,所以同时也可以把creatApp方法写上
export function creatApp(component) {
return new Vue(component)
}
// 导出Vue
export default Vue
- 编写vue/init.js 在初始化时,我们分步骤进行初始化
- 将传入的component挂载到势例
- 处理响应式的data
- 注册dom
- 将props与dom的依赖绑定
- 渲染dom
import { render } from "./initRender";
function init(component) {
// this 容易出现混淆 所以用vm便于认清实列
const vm = this
// 将数据放入this实例
const { data, methods, template } = component;
vm.$methods = methods;
vm.$template = template;
// 处理响应式数据
vm.initData(vm, data);
// 注册dom
vm.initDom()
// 注册vue数据与真实dom依赖
vm.initProps()
// 渲染
render(vm)
}
export default init
- 编写响应式数据