阅读 16431

源码篇(一):手写vue版mini源码分析框架,优势特性总结,vue-cli知识点,以及vue项目的二次封装,mini项目源码附送

前言

在前端三大框架并存的今天,vue已经是前端必须掌握的一部分。而对于很多入门者,或者转行前端的小伙伴们,个人觉得vue是一个非常适合入门的框架的之一。笔者个人觉得,无论从api的易学的角度出发,还是从原理层面解析,vue还是比react的简单一些。记得某个大神的面试分享:如果面试官没有vue跟react方向的要求,尽量往vue的方向扯,个人觉得是个非常优秀的意见哈哈哈。

身处跳槽涨薪的年代,相信很多同行们都已经背了很多面经。(虽然内心有点鄙视背题库的人,面试神一样,工作zhu一样)。 长久的发展,还是得扎扎实实的打好基础。如果面试官不再追问面经,反过来请你介绍vue,你想好怎么介绍你的vue项目吗?

本文的重点,如何介绍如何搭建vue项目,介绍你的vue项目。

此外,文章为个人源码第一篇,后续会陆续送上源码,以下是个人计划:

| 序号 | 博客主题 | 相关链接 | |-----|------|------------|- | 1 | 手写vue_mini源码解析(即本文) | juejin.im/post/684790… | | 2 | 手写react_mini源码解析 | juejin.im/post/685457… | | 3 | 手写webpack_mini源码解析 | juejin.im/post/685457… | | 4 | 手写jquery_mini源码解析 | juejin.im/post/685457… | | 5 | 手写vuex_mini源码解析 | juejin.im/post/685705… | | 6 | 手写vue_router源码解析 | 预计8月 | | 7 | 手写diff算法源码解析 | 预计8月 | | 8 | 手写promis源码解析 | 预计8月 | | 9 | 手写原生js源码解析(手动实现常见api) | 预计8月 | | 10 | 手写react_redux,fiberd源码解析等 | 待定,本计划先出该文,整理有些难度 | | 11 | 手写koa2_mini | 预计9月,前端优先 |

文章适合人群

半年~三年经验的vue开发者。 如未接触过vue,建议从官方文档:https://vuejs.org/ 学习搭建先。

该文章重点为普及知识点,以及部分知识点的解析。

从mini源码了解什么vue

Vue.js是一套构建用户界面的渐进式框架。 他最大的优势,也是单页面最大的优势,数据驱动与组件化。

首先我们mini的源码了解vue如何完成数据驱动。如图:

从图我们就可以简单的分析出什么叫MVVM。

MVVM, 实际上为M + V + VM。vue的框架就是一个内置的VM状态,而M就是我们的MODLE, V即是我们的视图。而通过我们的M,就能实现对V的控制,就是我们所说的数据驱动(模型控制视图)。ViewModel 的内容会实时展现在 View 层,前端开发者再也不必低效又麻烦,还耗性能(因为没有diff算法)地通过操纵 DOM 去更新视图。这就是一个从根源上,MVVM框架比传统MVC框架的优势。

我们进一步手写Mini版来了解vue,从源码了解什么是数据劫持。

首先构造一个vue实例。写过vue初始化的都知道,初始化时需要传入data,以及绑定元素标记el。我们把它储存起来。

class wzVue {
	constructor(options){
		this.$options = options;
		this.$data = options.data;
		this.$el = options.el;
	}
}
复制代码

首先看一下Observer的实现,以vue2.0为例,我们都知道数据劫持是通过Object.defineProperty。它自带监听get,set方法,我们可以用他实现一个简单的绑定。

obsever(this.$data);

function obsever(){
      Object.defineProperty( obj, key, {
		get(){
			
		},
		set( newValue ){
			value = newValue;
		}
	})
}
复制代码

这里很简单,如果还是不明白怎么双向绑定,举个简单的栗子:

<input type="text" v-modle="key" id="key"/>

// script
var data = {
    key: 5
    key2: 8
}
obsever(data);
data.key=6;//

function obsever(obj){
      Object.defineProperty( obj, key, {
		get(){
			
		},
		set( newValue ){
		    document.getElementById('key').val(newValue);//写死'key'先,下文会讲解
		}
	})
}

//写死'key'先,下文会讲解
document.getElementById('key').addEventListener( 'click', false, function(e){
    obj.key = e.target.value;
})
复制代码

这样实现了双向绑定,如果对象obj.key赋值,就会触发set方法,同步input的数据;如果页面手动输入值,则通过监听触发set,同步到对象obj的值。此时你可能有一个疑问,我们在vue赋值的时候,是直接修改上下文data数据的,并不是修改对对象的值, 也就是this.key=6。是的,vue源码中,先对data对象的数据进行了一次本地的数据劫持。如下文的proxyData。这样的:

this.key ----> data.key(触发) --->实现数据劫持

observer( data ){//监听data数据,双向绑定
    if( !data || typeof(data) !== 'object'){
		return;
	}
	Object.keys( data ).forEach( key => {
		this.observerData(key, data, data[key]);//监听data对象
		this.proxyData( key );
	})
}

observerData( key, obj, value ) {
	this.observer(key);
	const dep = new Dep();
	Object.defineProperty( obj, key, {
		get(){
		},
		set( newValue ){ //通知变化
		}
	})
}

proxyData(key){
	Object.defineProperty( this, key, {
		get(){
			return this.$data[key];
		},
		set( newValue ){	
			this.$data[key] = newValue;
		}			
	})
}
复制代码

两点需要强调的地方:

1)遍历data的属性,vue的源码是用了Object.keys。它能按顺序遍历出不同的属性,但是不同的浏览器中可能执行顺序不一样。

2)因为Object.defineProperty只能监听一层结构,所以,对于多层级的Object结构来讲,需要遍历去一层一层往下监听。

那如果连续赋值的,例如this.key = 1; this.key2 = 2; 上边的双向绑定代码是写死了“key"。

这时候是否发生了两次赋值?那么我们怎么知道,它触发的对象是哪个呢?这时候,vue的设计是设计了dep的概念,来存放每个监听对象的值。

class Dep{
	constructor(){
		this.deps = [];
	}
	
	addDep(dep){
		this.deps.push(dep);
	}
	
	notiyDep(){
		this.deps.forEach(dep => {
			dep.update();
		})
	}
}
复制代码

这里不难理解。addDep既是为了有数据变化时,插入的“对象”,表示需要劫持。 notiyDep即是该对象,已经需要被更新,执行对应的update方法。

那么插入的对象是什么呢(数组的单体)?单体肯定,需要包含一个“dom”对象,还有对应监听的“data”对象,两者关系绑定,才能实现数据同步。这个“单体”,我们称呼它为“watcher”。

class Watcher{

	constructor( vm, key, initVal, cb ){
		this.vm = vm;//保存vue对象实例
		this.key = key;//保存绑定的key
		this.cb = cb;//同步两者的回调函数
		this.initVal = initVal;//初始化值
		this.vm[this.key];//触发对象的get方法
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
复制代码

截至目前为止,obsever还是没有跟Watcher关联上。在讲他们怎么关联上之前,我们再看看vue的设计思维,它是由Watcher添加订阅者,再由Dep添加变化。那么Watcher是怎么来的?从图中的关系,我们可以看出由页面解析出来的。这就是我们要讲的 Compile。

Compile,首先有一个“初始化视图”的动作。

class Compile{
	
	constructor( el, vm ){
		this.$el  =  document.querySelector(el);
		this.$vm = vm;
		if (this.$el) {
			this.$fragment = this.getNodeChirdren( this.$el );
			this.$el.appendChild(this.$fragment);
		}
	}
	
	getNodeChirdren( el ){
		const frag = document.createDocumentFragment();
		let child;
		while( (child = el.firstChild )){
			frag.appendChild( child );
		}
		return frag;
	}
}
复制代码

这里应该不难理解,拿到template对象的id,遍历完之后,赋值显示在我们的el元素中。接下来我们重点讲Compile产生的Watcher。我们在Compile的原型中添加this.compile( this.$fragment);方法。对刚才拿到template的模版进行继续,看他用到哪些属性。

compile( el ){
	const childNodes = el.childNodes;
	Array.from(childNodes).forEach( node => {
		if( node.nodeType == 1 ) {//1为元素节点
			const nodeAttrs = node.attributes;
			Array.from(nodeAttrs).forEach( attr => {
				const attrName = attr.name;//属性名称
				const attrVal = attr.value;//属性值
				if( attrName == "v-modle"){
				   	this.zDir_model( node, attrVal );
				}
			})
		} else if( node.nodeType == 2 ){//2为属性节点
			console.log("nodeType=====22");
		} else if( node.nodeType == 3 ){//3为文本节点
			this.compileText( node );
		}
		// 递归子节点
		if (node.childNodes && node.childNodes.length > 0) {
			this.compile(node);
		}
	})
}
复制代码

如果你对childNodes,nodeType,nodeList还是一脸懵逼,建议移步到: 关于DOM和BOM知识点汇总: juejin.im/post/684668…

从上边的mini源码可以看出,compile遍历el的所有子元素,如果是文本类型,我们就进行文本解析compileText。如果是input需要双向绑定,我们就进行zDir_model解析。

compileText( node ){
	if( typeof( node.textContent ) !== 'string' ) {
		return "";
	}
	const reg = /({{(.*)}})/;
	const reg2 = /[^/{/}]+/;
	const key = String((node.textContent).match(reg)).match(reg2);//获取监听的key
	const initVal = node.textContent;//记录原文本第一次的数据
    updateText( node, this.$vm[key], initVal );
}

updateText( node, value, initVal ){
	var reg = /{{(.*)}}/ig;
	var replaceStr = String( initVal.match(reg) );
	var result = initVal.replace(replaceStr, value );
	node.textContent = result;
	new Watcher( this.$vm, key, initVal, function( value, initVal ){
		updateText( node, value, initVal  );
	});
}
复制代码

我们再看看compileText的源码,大概意思为,获取到文本例如“我的名字{{name}}”的key,即为name。然后name进行初始化赋值updateText, updateText的初始化结束后,添加订阅数据变化,绑定更新函数Watcher。

而Watcher,正是绑定dep跟compile的桥梁。我们修改一下添加到dep跟Watcher的代码:

observerData( key, obj, value ) {
	this.observer(key);
	const dep = new Dep();
	Object.defineProperty( obj, key, {
		get(){
		    Dep.target && dep.addDep(Dep.target);//添加的代码+++++++++++++++++
			return value;
		},
		set( newValue ){ //通知变化
		    if (newValue === value) {
			  return;
			}
			value = newValue;
			//通知变化
			dep.notiyDep();//添加的代码+++++++++++++++++
		}
	})
}


class Watcher{

	constructor( vm, key, initVal, cb ){
		this.vm = vm;
		this.key = key;
		this.cb = cb;
		this.initVal = initVal;
		Dep.target = this;//添加的代码+++++++++++++++++
		this.vm[this.key];
		Dep.target = null;
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
复制代码

这样的话,我们在新增一个Watcher的过程中,将此时的整个Watcher的this对象赋值到Dep.target中。这时候我们再调用一下this.vm[this.key]。vm即是vue实例对象,所以,Watcher的this.vm[this.key],即是vue实例中的,this.key。而我们的key已经通过Object.defineProperty监听,此时就会进入到Object.defineProperty的get方法中, Dep.target 此时不为空,所以dep.addDep(Dep.target),即是watcher添加订阅者到dep中。

这时候如果数据发生变化,即调用set方法,然后dep.notiyDep,notiyDep就会通知,由文本解析的例如{{key}}的watcher重新更新一遍值,即完成了双向绑定。

如果是v-modle的话,即在解析时,每个对象多加一个监听,然后主动调用set方法。

node.addEventListener("input", e => {
	  vm[value] = e.target.value;
	});
	
复制代码

这就是vue整个双向绑定的大致流程,所谓的数据驱动。

然后他有一个很大的缺陷,这个缺陷是,他知道驱动对象,却无法对数组进行驱动 (实际上也行) 。这里vue的作者用了另外一种思维去解决这个问题。他重写了数组的原型,把数组的'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'的方法重写了一遍。也就是当你数组使用了这7个方法时,vue重写的方法,会帮你变化中放入dep中。

这 个 (实际上也行) 其实也很有学问。上述说,vue2.0无法对数组进行数据监听,其实真实的的测试中,Object.defineProperty是可以监听到数组变化的。但是只能在已有的长度中,不能对其加长的长度。那你这时候可能会有疑问,那我们重写array的push方法就够了,为什么要重写7个呢?好吧,我也曾经有这样的疑惑。后续,曾在帖子上,看到过vue的笔者回复过,印象中是这么说的:Object.defineProperty对数组的监听,消耗性能大于效果。也就是说,本来Object.defineProperty,为了提升效率而产生,现在用在数组上,反而降低了效率,那不如干脆拒绝使用他。

于是,又有了vue-cli3.0数据劫持的改造。

那么vue3.0是怎么实现数据劫持的呢?

3.0中双向绑定已经不再是使用Object.defineProperty。而是proxy。proxy的引入,更高的效率,一方面解决了数组方面的问题,我们可以简单看一下mini源码的改造:

proxyData( data ){//监听data数据,双向绑定
	if( !data || typeof(data) !== 'object'){
		return;
	}
	const _this = this;
	const handler  = {
		set( target, key, value ) {
			const rest = Reflect.set(target, key, value);
			_this.$binding[key].map( item =>  {
				item.update();
			})
			return rest;
		}
	}
	this.$data = new Proxy( data, handler );
}
复制代码

vue的mini源码解析到此为止,如还有不明白的地方可留言。 可需要源码可进入github查看:https://github.com/zhuangweizhan/codeShare

vue的特性是什么

双向绑定

由上述mini源码,我们可以知道vue的数据驱动。MVVM相比MVC模式, 没有频繁的操作dom值,在开发中无疑时更高效的灵活页面的触发。可让我们专注与逻辑js的抒写,而具体的页面变化,交给VM区处理。

diff算法

我们都知道,js执行的效率高于dom渲染的效率。如果我们能提前通过js算出不一致的地方,再最后去“渲染”最终的差异。明显的增加效益。

我们列出diff算法的三步曲:

  • 1)通过虚拟dom渲染对象
  • 2)对比两个虚拟的差异
  • 3)根据差异进行渲染

全局混入mixins

mixins 选项接收一个混入对象的数组。而vue正是利用他来扩展vue的实例。

我们的全局方法等,都可以利用mixins快速的套入vue实例。

完善的生命周期

十一个生命周期,create, mount, update, activated, destroyed。分别前后。最后还有v2.5.0版本的errorCaptured。 完善的生命周期的更适合,程序顺序的正确执行。

丰富的组件传递

props, emit, slot,provide/inject,attrs/listeners,EventBus emit/on,parent / children与 ref

vue的优势是什么

也是你为什么选择vue的原因

易学上手

笔者曾是一名jq的前端小杂,入门这些玩意,个人觉得他们的难度级别(仅限于api):

jq < 原生小程序 < vue系列 < angurle系列 < react系列

vue是刚开始一边看着api就可以撸出来的项目。

活跃的社区

也许每个框架都有自身的bug。有bug不可怕,怕的是没有解决方案。而vue中,你卡到问题点,但自己没有能力解决时,活跃的社区会给你答案。

完善的第三方插件

支持axios, webpack, sass,elemnt-ui,vuex, router 等第三方插件。

支持客户端全家桶

vue有着脚手架,ssr的nuxt框架,app版本的weex, 小程序多端开发uniapp。

可谓学好vue,吃遍前端全家桶。


最后:也许你觉得,上述react都支持。好的吧,的确是,晚些汇总完react的文章,再写一篇对比。

vue-cli包含了什么

vue-cli脚手架,帮我们做了什么。vue-cli3.0开始,已经成为可选择性的插件。我们分析一下各个插件的作用。

webpack

blog.guowenfh.com/2016/03/24/…

webpack,打包所有的“脚本”。脚手架已经帮我们通过webpack做了很多默认的loader。

我们项目中,不同的文件,经过编译输出最终的html,js,css,都是经过webpack。

例如,编译 ES2015 或 TypeScript 模块成 ES5 CommonJS 的模块;

再例如:编译 SASS 文件成 CSS,然后把生成的CSS插入到 Style 标签内,然后再转译成 JavaScript 代码段,处理在 HTML 或 CSS 文件中引用的图片文件,根据配置路径把它们移动到任意位置,根据 MD5 hash 命名。

因此,我们可以不同文件,找在webpack不同的编译器,如vue有vue-loader,脚手架帮我们引入了。如sass有sass-loader,基本npm或者yarn生态圈中,已经有前端你所有见过的loader。也许还有没有?没关系,我们可以自己写一个。

来个简单的需求:开发环境过滤掉所有的打印。

这要是在传统的项目,没有经过编译器,这是有多大的工作量。当有了我们的webpack或者gulp等,他仅仅只是几句代码的问题。我们来看一下webpack的实现:

配置文件:

const fs = require('fs');

function wzJsLoader(source) {
    /*过滤输出*/
    const mode = process.env.NODE_ENV === 'production'
    if( mode ){//正式环境
        source = source.replace(/console.log\(.*?\);/ig, value => "" );
    }
    return source;
};

module.exports = wzJsLoader;
复制代码

这样,我们就轻松定了一个自己的loader。在wepback.config.js,加上我们对应的loader,轻松解决问题

{
    test: /\.js$/, //js文件加载器
    exclude: /node_modules/,
    use: [
        {
          loader: 'babel-loader?cacheDirectory=ture',
          options: {
            presets: ['@babel/preset-env']
          }
        },
        {
            loader: require.resolve("./myWebPackConfig/wz-js-loader"),//添加的
            options: { name: __dirname  },
        },
    ]
  },
  
复制代码

webpack是个很有难度的东西,本文就不继续简介,简单了解webpack的配置,以及如何写好Loader跟plugins等。如果你还有精力深入,webpack的执行机制,如何打包成文件,他的生命周期等,都可以深入挑战,如果研究透彻,相信你的实力不一般。

axios

axios,网络请求工具。提到网络请求工具,你肯定了解从$.ajax,fetch、axios。下边此次讲一下他们的发展史以及优缺点(具体什么时候,会在下文的“vue项目的二次封装”中讲解)

$.ajax,相信早期进入前端领域的人,都大为喜欢。他基于jquery,对原生XHR的封装,还支持JSONP,非常方便。 他的有点包括,无需要通过刷新页面更新数据,支持异步与服务器通信,而且规范被广泛支持。

当年可谓如“诺基亚”一般存在。可惜“诺基亚”后来跌下神坛,$.ajax在网络请求中也遭受的同样的待遇。

那么淘汰$.ajax的根本原因是什么呢?

因为引入的单页面框架,如vue的mvvn架构,或者是只有m的react,他们都属于js驱动Html。这涉及到控制dom刷新的过程。es5可以利用callback, 或者generater的迭代器模式进行处理。但是还不理想。所以es6引入了promise的概念。

所以,以返回promise的单位的异步控制进程逐步发展。

一方面,.ajax没有改进,他依然我行我素的不支持promise。这对“新”前端的理念很不符,我们无法用.ajax没有改进,他依然我行我素的不支持promise。这对“新”前端的理念很不符,我们无法用.ajax来完成异步操作(除非回调地狱,写过大项目的都知道定位问题太难了)。

另一方面,他还需要引入jquery来实现。我们都知道新框架,都基本脱离了jq。

SO,fetch就这样产生了。解决了ajax无法返回promise的问题。开始让人抛弃$.ajax。

fetch号称是$.ajax的替代品,它的API是基于Promise设计的,旧版本的浏览器不支持Promise,需要使用polyfill es6-promise

然而,fetch貌似是为解决返回Promise而产生的,并没有注意其他网络请求工具该做的细节,他虽然支持promise, 但暴露了太多的问题:

1)fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。

2)fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})

3)fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费

4)fetch没有办法原生监测请求的进度,而XHR可以

因此,axios正式入场。他重新基于xhr封装,支持返回Promise, 也解决了fetch的弊端。

反问:知道jquery,fetch,axios的区别了吗?

vue-router

在没有“路由”的概念时,我们通常讲“页面路径”。如果你经历过spring mvc通过action映射到html页面的时代,那么恭喜 ,你已经使用过路由。他属于后台路由。后台的路由,可以简单的理解成一个路径的映射。

那么有后台路由,就会有前端路由。没错,带来质的改变,就是前端路由。那么他带来的优势是什么。

前端路由,又分hash模式跟history模式。我们用两张图来简单的说明一下,前端路由的原理:

hash模式

hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。hash 模式的原理是 onhashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件。

优势呢?是不是很明显?如果没有使用异步加载,我们的已经可以不需要经过后台,直接仅是页面的“锚点”切换。

history模式

history模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器记录栈,在当前已有的 back、forward、go 基础之上,它们提供了对历史记录修改的功能。只是当它们执行修改时,虽然改变了当前的 URL ,但浏览器不会立即向后端发送请求。

history模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。

**此外,**vue的路由,还支持嵌套(多级)路由,支持路由动态配置,命名视图(同一页面多个路由),路由守卫, 过渡动态效果等,可谓功能十分之强大,考虑比较齐全,在此每个列举一个简单的栗子:

路由动态配置:

const router = new VueRouter({
  routes: [
   动态路径参数 以冒号开头
    { path: '/detail/:id', component: Detail }
  ]
})
复制代码

嵌套(多级)路由: const router = new VueRouter({

routes: [
        { path: '/detail/', component: User,
              children: [
                {
                  path: 'product',
                  component: Product  //二级嵌套路由
                },
              ]
        }
   ]
})
复制代码

命名视图:

<router-view></router-view>
<router-view name="a"></router-view>
<router-view name="b"></router-view>

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: componentsDefulat,
        a: componentsA,
        b: componentsB
      }
    }
  ]
})
复制代码

路由守卫:

router.beforeEach((to, from, next) => {
  // ...
})
复制代码

动态效果:

<transition>
  <router-view></router-view>
</transition>
复制代码

sass/less

sass跟less,两者都是CSS预处理器的佼佼者。

为什么要使用CSS预处理器?

CSS有具体以下几个缺点

1.语法不够强大,比如无法嵌套书写,导致模块化开发中需要书写很多重复的选择器;

2.没有变量和合理的样式复用机制,使得逻辑上相关的属性值必须以字面量的形式重复输出,导致难以维护。

Less和Sass在语法上有些共性,比如下面这些:

  • 1、混入(Mixins)——class中的class;

  • 2、参数混入——可以传递参数的class,就像函数一样;

  • 3、嵌套规则——Class中嵌套class,从而减少重复的代码;

  • 4、运算——CSS中用上数学;

  • 5、颜色功能——可以编辑颜色;

  • 6、名字空间(namespace)——分组样式,从而可以被调用;

  • 7、作用域——局部修改样式;

  • 8、JavaScript 赋值——在CSS中使用JavaScript表达式赋值。

再说一下两者的区别:

  • 1.Less环境较Sass简单,使用起来较Sass简单

  • 2.从功能出发,Sass较Less略强大一些 (1) sass有变量和作用域。

    (2) sass有函数的概念;

    (3) sass可以进行进程控制。例如: -条件:@if @else; -循环遍历:@for @each @while

    (4) sass又数据结构类型: -list类型=数组; -map类型=object; 其余的也有string、number、function等类型

  • 3.Less与Sass处理机制不一样

  • 前者是通过客户端处理的,后者是通过服务端处理,相比较之下前者解析会比后者慢一点。而且sass会产生服务器压力。

vuex

vuex官方概念:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

看到这里你可能会有疑问,我们传统的框架上,localstore, session , cookies,不就以及解决问题了么。

没错。他们是解决了本地存储的问题。但是vue是单页面架构,需要数据驱动。 session , cookies无法触发数据驱动。这时候不得引入一个可以监听的容易。小型项目可能直接用store,或者页面与页面直接可以用props传递

我们在使用Vue.js开发复杂的应用时,经常会遇到多个组件共享同一个状态,亦或是多个组件会去更新同一个状态,在应用代码量较少的时候,我们可以组件间通信去维护修改数据,或者是通过事件总线来进行数据的传递以及修改。但是当应用逐渐庞大以后,代码就会变得难以维护,从父组件开始通过prop传递多层嵌套的数据由于层级过深而显得异常脆弱,而事件总线也会因为组件的增多、代码量的增大而显得交互错综复杂,难以捋清其中的传递关系。

那么为什么我们不能将数据层与组件层抽离开来呢?把数据层放到全局形成一个单一的Store,组件层变得更薄,专门用来进行数据的展示及操作。所有数据的变更都需要经过全局的Store来进行,形成一个单向数据流,使数据变化变得“可预测”。

简单说一下他的工作流程:

图文相信已经非常清晰vuex的工作流程。简单的简述一下api:

state 简单的理解就是vuex数据的储存对象。

getters getter 会暴露为 state 对象,你可以以属性的形式访问这些值:

actions Action 类似于 mutation,不同在于: Action 提交的是 mutation,而不是直接变更状态。 Action 可以包含任意异步操作。

mutations 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。 mutations可以直接改变state的状态。 mutations 不可以包含任意异步操作

module Vuex 太大时,允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter。

vuex的使用,很简单。但是灵活时候,可能还需要进一步的了解源码。vuex的原理其实跟vue有点像。

如需要看vuex源码,可通过:https://github.com/zhuangweizhan/codeShare

element ui/vux

即UI库的选择

vue的火热,离不开vue社区的火热。就常规的项目,如果公司不是要求特别高,基本各种UI库,已经不需要你写样式(前端最烦的就是写样式没意见吧)。

这里就不做UI库如何搭建的文章,有兴趣可以关注,后续我会写一篇专门搭建UI库的。

这里介绍一下vue火热的UI库吧。

其中,移动端笔者推荐vant,管理后台推荐element。

vue项目的二次封装

axios的封装

上文讲解过axios的由来以及优缺点,这里谈谈axios在vue项目的使用。

1)请求拦截

比如我们的请求接口,全局都需要做token验证。我们可以在请求钱做好token雁阵。如果存在,则请求头自动添加token。

axios.interceptors.request.use(    
    config => {        
        // 每次发送请求之前判断vuex中是否存在token        
        // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况
        // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断 
        const token = store.state.token;        
        token && (config.headers.token = token);        
        return config;    
    },    
    error => {        
        return Promise.error(error);    
})
复制代码

2)返回拦截

当程序异常的时候呢,接口有时候在特定的场景,或者是服务器异常的情况下,是否就让用户白白等待? 如果有超时,错误返回机制,及时告知用户的,是不是用户好一点?这就是返回的拦截。

axios.interceptors.response.use(    
    response => {   
        // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据     
        // 否则的话抛出错误
        if (response.status === 200) {            
            return Promise.resolve(response);        
        } else {            
            return Promise.reject(response);        
        }    
    },    
    // 服务器状态码不是2开头的的情况
    // 这里可以跟你们的后台开发人员协商好统一的错误状态码    
    // 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等
    // 下面列举几个常见的操作,其他需求可自行扩展
    error => {    
            alert("数据异常,请稍后再试或联系管理员");
            return Promise.reject(error.response);
        }
    }    
});
复制代码

3)以get为栗子

export function get(url, params){    
    return new Promise((resolve, reject) =>{        
        axios.get(url, {            
            params: params        
        }).then(res => {
            resolve(res.data);
        }).catch(err =>{
            reject(err.data)        
    })    
});
复制代码

此外,对axios的使用还有想法的,建议查看一下axios全攻略: ykloveyxk.github.io/2017/02/25/…

编译器改进

上文曾提到,vue-cli自带webpack。那么我们如何通过他,来改进我们的项目呢。

从环境区分,自带的引入,已经帮我们区分了环境,然后帮我们导入不同的loader跟Pulger等,基本已经是一个非常完善的编译器。

我们见到看一下dev的源码(添加了注释),dev环境,实际上会运行dev-server.js文件该文件以express作为后端框架

// nodejs环境配置
var config = require('../config')
if (!process.env.NODE_ENV) {
  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn') //强制打开浏览器
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware') //使用代理的中间件
var webpackConfig = require('./webpack.dev.conf') //webpack的配置

var port = process.env.PORT || config.dev.port //端口号
var autoOpenBrowser = !!config.dev.autoOpenBrowser //是否自动打开浏览器
var proxyTable = config.dev.proxyTable //http的代理url

var app = express() //启动express
var compiler = webpack(webpackConfig) //webpack编译

//webpack-dev-middleware的作用
//1.将编译后的生成的静态文件放在内存中,所以在npm run dev后磁盘上不会生成文件
//2.当文件改变时,会自动编译。
//3.当在编译过程中请求某个资源时,webpack-dev-server不会让这个请求失败,而是会一直阻塞它,直到webpack编译完毕
var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  quiet: true
})

//webpack-hot-middleware的作用就是实现浏览器的无刷新更新
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {}
})
//声明hotMiddleware无刷新更新的时机:html-webpack-plugin 的template更改之后
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

//将代理请求的配置应用到express服务上
Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  app.use(proxyMiddleware(options.filter || context, options))
})

//使用connect-history-api-fallback匹配资源
//如果不匹配就可以重定向到指定地址
app.use(require('connect-history-api-fallback')())

// 应用devMiddleware中间件
app.use(devMiddleware)
// 应用hotMiddleware中间件
app.use(hotMiddleware)

// 配置express静态资源目录
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

var uri = 'http://localhost:' + port

//编译成功后打印uri
devMiddleware.waitUntilValid(function () {
  console.log('> Listening at ' + uri + '\n')
})
//启动express服务
module.exports = app.listen(port, function (err) {
  if (err) {
    console.log(err)
    return
  }
  // 满足条件则自动打开浏览器
  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
    opn(uri)
  }
})
复制代码

可见,webpack的编译,以及相对完善。我们也可以去优化一下对应的插件,比如:

plugins: [
  new webpack.DefinePlugin({ // 编译时配置的全局变量
    'process.env': config.dev.env //当前环境为开发环境
  }),
  new webpack.HotModuleReplacementPlugin(), //热更新插件
  new webpack.NoEmitOnErrorPlugin(), //不触发错误,即编译后运行的包正常运行
  new HtmlWebpackPlugin({  //自动生成html文件,比如编译后文件的引入
    filename: 'index.html', //生成的文件名
    template: 'index.html', //模板
    inject: true
  }),
  new FriendlyErrorsPlugin() //友好的错误提示
]
复制代码

最后讲一下webpack的相关优化:

构建速度的优化:
  • 1.HappyPack 基于webpack的编译模式本是单线程,时间占时最多的Loader对文件的转换。开启HappyPack,可以讲任务分解成多个进程去并行处理。

    简单配置:

    new HappyPack({// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件 id: 'babel',// 如何处理 .js 文件,用法和 Loader 配置中一样 loaders: ['babel-loader?cacheDirectory'],// ... 其它配置项 }),

    详细可参考:http://www.fly63.com/nav/1472

  • 2.DllPlugin 可将一些Node_moudle一些编译好的库,常用而且不变的库,抽出来。这样就无需重新编译。

  • 3.Loader 记录配置搜索范围,include,exclude 如:

    { test: /.js$/, //js文件加载器 exclude: /node_modules/, use: [ { loader: 'babel-loader?cacheDirectory=ture', options: { presets: ['@babel/preset-env'] }, include: Path2D.resolve(__dirname, 'src') } ] }

优化打包大小:
  • 1.tree shaking写法。(webpack 4 已自动引入) 即“摇树”。即只引入所需要引入部分,其余的代码讲会过滤。

  • 2.压缩代码 当前最程愫的压缩工具是UglifyJS。HtmlWebpackPlugin也可配置minify等。

  • 3.文件分离 多个文件加载,速度更快。 例如:mini-css-extract-plugin,将css独立出来。这样还有利于,部分“不变”的文件做缓存。

  • 4.Scope Hoisting 开启后,分细分出模块直接的依赖关系,会自动帮我们合并函数。简单的配置:

    module,exports={ optimization:{   concatenateModules:true } }

组件化

任何框架,团队都需要自己的组件化。(当然,有些团队,怕人员的流动性,全部不组件化,最简单的写法,笔者也遇过这种公司)。

一般来说,组件大致可以分为三类:

  • 1)与业务无关的独立组件。
  • 2)页面级别的组件。
  • 3)业务上可复用的基础组件。

关于1),可以理解成现在的UI库(如element/vant),这里暂时不做独立组件分析。(晚些可能会写一篇如何写独立组件的文章,上传到npm。)

关于2),貌似当某一个模块,页面需要多次重复使用时候,就可以写成独立组件,这个貌似没什么好分析。

这里重点分析一下:** 3)业务上可复用的基础组件 ** 。

笔者写过的vue项目,都基本会封装20~30个业务通用组件。例如截图的my-form,my-table。如下:

这里我以为myTable

emelent的table插件,的确已经很强大了。但是笔者虽然用上了emelent ui,但是业务代码却没有任何emelent的东西。

如果有一天,公司不再喜欢element ui的table,那so easy,我把我的mytable修改一下,所有页面即将同步。这就是组件化的魅力。

下边我以my-table为栗子,记录一下我组件化的要点: 1.合并封装分页,是表格不再关心分页问题。 2.统一全局表格样式(后期可随时修改) 3.业务脱离,使业务上无需再关心element的api如何定义,且可随时替换掉element。 4.自定义类型,本文提供select跟text控制,配置对象即可实现。 5.统一自定义缺省处理。 6.统一搜索按钮,搜索框。配置对象即可实现。

这些优势,以及对全局的拓展性,是不是比传统直接用的,有很大的优势?

当然,不好的地方,插件应该相对完善,考虑周全,需要一个全局统筹的人。对人员的流动的公司,的确很不友好。

下边是源码提供,可参考:

<template>
  <div>
    <h3 class="t_title">{{tName}}</h3>
    <div class="t_content">
      <el-form :inline="true" class="serach_form" >
        <el-form-item  v-for="(item, index) in tSerachList" :label="item.name" :key="index" v-if="tSerachList.length > 0 ">
          <div v-if="item.type == 'text'" >
            <el-input  :placeholder="item.name" v-model="tSerachList[index].value" ></el-input>
          </div>
          <div v-else-if="item.type == 'select'" >
            <el-select v-model="tSerachList[index].value"  :placeholder="item.name">
              <el-option  v-for="(cItem, cIndex) in item.list" :key="cIndex" :label="cItem.name" :value="cItem.value" ></el-option>
            </el-select>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit" >查询</el-button>
        </el-form-item>
      </el-form>
      <!--按钮操作模块-->
      <el-row class="t_button_tab" v-for="(item,index) in tBtnOpeList" :key="index">
        <el-button  :type="item.type" @click="btnOpeHandle(item.opeList)" :render="item.render">{{item.label}}</el-button>
      </el-row>
    </div>

    <div class="t_table">
      <el-table
        :data="tableData"
        style="width: 100%">
        <el-table-column v-for="(item, index) in tableList" :key="index" v-bind="item">
          <template slot-scope="scope" >
            <my-table-render v-if="item.render" :row="scope.row" :render="item.render" ></my-table-render>
            <span  v-else>{{scope.row[item.key]}}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>

    <div class="t_pagination">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page.sync="currentPage"
        :page-size="tPageSize"
        layout="prev, pager, next, jumper"
        :total="tTotal">
      </el-pagination>
    </div>
  </div>

</template>

<script>

  import MyTableRender from './my-table-render.vue'

  export default {
    props: {
      tTablecolumn: { //展示的列名
        type: Array,
        required: true
      },
      tName: { //页面的名称
        type: String,
          required: true
      },
      tUrl: { //请求的URL
        type: String,
        required: true
      },
      tParam: { //请求的额外参数
        type: Object,
        required: true
      },
      tSerachList: { //接口的额外数据
        type: Array,
        required: true
      },
      tBtnOpeList: {
        type: Array,
        required: false
      }
    },
    data () {
      return {
        arrea: "",
        currentPage: 1,
        tableData: [],
        tableList: [],
        tTotal: 0,
        tPageSize: 10,
        serachObj: {} //搜索的文本数据
      }
    },
    created () {
      this.getTableList()
      this.reloadTableList()
    },
    methods: {
      async getTableList () {
        var Obj = { pageNum: this.currentPage, pageSize: this.tPageSize }
        var that = this;
        var url = this.tUrl;
        var param = Object.assign(this.tParam, Obj, this.serachObj)
        const res = await this.utils.uGet({ url:url, query:param })
        var list = res.data.dataList
        that.tableData = list
        that.tTotal = res.data.total
      },
      // 提交
      reloadTableList () {
        var tableList = this.tTablecolumn
        for (var i = 0; i < tableList.length; i++) {
          tableList[i].prop = tableList[i].key
          tableList[i].label = tableList[i].name
        }
        this.tableList = tableList
      },
      onSubmit ( res ) {
        var that = this;
        const status = this.$store.getters.getUserStatus;
        if( status == 4 ){
          this.utils.uErrorAlert("临时用户无权限哦");
        } else {
          this.utils.uLoading(800);
          var tSerachList = this.tSerachList;
          var obj = {}
          for (var i = 0; i < tSerachList.length; i++) {
            obj[ tSerachList[i].key ] = tSerachList[i].value;
          }
          this.serachObj = obj;
          this.currentPage = 1;
          this.getTableList();
        }
      },
      handleSizeChange () {
      },
      handleCurrentChange (obj) {
        this.currentPage = obj
        this.getTableList()
      },
      btnOpeHandle(params){
        const status = this.$store.getters.getUserStatus;
        if( status == 4 ){
          this.utils.uErrorAlert("临时用户无权限哦");
        } else {
          this.$emit('handleBtn', params);
        }
      }
    },
    components: {
      MyTableRender
    }
  }
</script>


<style  lang="scss">

  @import '@/assets/scss/element-variables.scss';
  .serach_form{
    background: $theme-light;
    text-align: left;
    padding-top: 18px;
    padding-left: 20px;
  }
  .t_title{
    /*float: left;*/
    /*padding: 20px;*/
    /*font-size: 23px;*/
    color:$theme;
    text-align: left;
    border-left: 3px solid $theme;
    padding-left: 5px;
  }

  .t_content{
    clear: both;
  }

  .t_table{
    clear: both;
    padding: 20px;
  }

  .t_pagination{
    margin-top: 20px;
    float: right;
    margin-right: 20px;
  }
  .t_button_tab{
    text-align: left;
    margin-top: 18px;
  }

</style>
复制代码

mini项目源码

最后送上个人手写的mini版本vue源码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>wz手写vue源码</title>
</head>
<body>
  <div id="app" class="body" >
	<div class="b_header" >
		<img class="b_img" src="https://user-gold-cdn.xitu.io/2020/7/10/173344e271bf85af?w=400&h=400&f=png&s=3451" /><span>wz手写vue源码</span>
	</div>
	<div class="b_content" >
		<div class="n_name" >姓名:{{name}}</div>
		<div class="box" >
			<span>年龄:{{age}}</span>
		</div>
		<div>{{content}}</div>
		<div>
			<input type="text" wz-model="content" placeholder="请输入自我介绍"  />
		</div>
		<div wz-html="htmlSpan" ></div>
		<button @click="changeName" >点击提示</button>
	</div>
  </div>
  <style>
  .body{
	 text-align: left;
	 width: 300px;
	 margin: 0 auto;
	 margin-top: 100px;
  }
  .body .b_header{
	display:  flex;
	justify-item: center;
	justify-content: center;
	align-items: center;
	align-content: center;
	margin-bottom: 20px;
  }
  .body .b_header span{
	font-size: 21px;
  }
  .body .b_img{
	display: inline-flex;
	width: 20px;
	height: 20px;
	align-item: center;
  }
  .body .b_content{
	
  }
  .body div{
	margin-top: 10px;
	min-height: 20px;
  }
  button{
	margin-top: 20px;
  }
  </style>
  <script src="./wzVue.js"></script>
  <script>
    const w = new wzVue({
      el: '#app',
      data: {
        "name": "加载中...",
        "age": '加载中...',
		"content": "我是一枚优秀的程序员",
		"htmlSpan": '<a href="http://wwww.zhuangweizhan.com">点击欢迎进入个人主页 </a>'
      },
      created() {
        setTimeout(() => {
          this.age = "25岁";
		  this.name = "weizhan";
        }, 800);
      },
	  methods: {
		changeName() {
			alert("欢迎进入个人主页: http://www.zhuangweizhan.com");
		}
	  }
    })
  </script>
</body>
</html>


// js文件
/*
	本代码来自weizhan
*/
class wzVue {
	constructor(options){
		this.$options = options;
		console.log("this.$options===" + JSON.stringify(this.$options) );
		this.$data = options.data;
		this.$el = options.el;
		this.observer( this.$data );//添加observer监听
		new wzCompile( options.el, this);//添加文档解析
		if ( options.created ) {
			options.created.call(this);
		}
	}
	
	observer( data ){//监听data数据,双向绑定
		if( !data || typeof(data) !== 'object'){
			return;
		}
		Object.keys(data).forEach(key => {//如果是对象进行解析
			this.observerSet(key, data, data[key]);//监听data对象
			this.proxyData(key);//本地代理服务
		});
	}
	
	observerSet( key, obj, value ){
		this.observer(key);
		const dep = new Dep();
		Object.defineProperty( obj, key, {
			get(){
				Dep.target && dep.addDep(Dep.target);
				return value;
			},
			set( newValue ){
				if (newValue === value) {
				  return;
				}
				value = newValue;
				//通知变化
				dep.notiyDep();
			}
		})
	}
	
	proxyData(key){
		Object.defineProperty( this, key, {
			get(){
				return this.$data[key];
			},
			set( newVal ){
				this.$data[key] = newVal;
			}
		})	
	}
	
}

//存储数据数组
class Dep{
	constructor(){
		this.deps = [];
	}
	
	addDep(dep){
		this.deps.push(dep);
	}
	
	notiyDep(){
		this.deps.forEach(dep => {
			dep.update();
		})
	}
}

//个人编译器
class wzCompile{
	constructor(el, vm){
		this.$el = document.querySelector(el);
		
		this.$vm = vm;
		if (this.$el) {
			this.$fragment = this.getNodeChirdren( this.$el );
			this.compile( this.$fragment);
			this.$el.appendChild(this.$fragment);
		}
	}
	
	getNodeChirdren(el){
		const frag = document.createDocumentFragment();
		
		let child;
		while( (child = el.firstChild )){
			frag.appendChild( child );
		}
		return frag;
	}
	
	compile( el ){
		const childNodes = el.childNodes;
		Array.from(childNodes).forEach( node => {
			if( node.nodeType == 1 ) {//1为元素节点
				const nodeAttrs = node.attributes;
				Array.from(nodeAttrs).forEach( attr => {
					const attrName = attr.name;//属性名称
					const attrVal = attr.value;//属性值
					if( attrName.slice(0,3) === 'wz-' ){
						var tagName = attrName.substring(3);
						switch( tagName ){
							case "model":
								this.wzDir_model( node, attrVal );
							break;
							case "html":
								this.wzDir_html( node, attrVal );
							break;
						}
					}
					if( attrName.slice(0,1) === '@'  ){
						var tagName = attrName.substring(1);
						this.wzDir_click( node, attrVal );
					}
				})
			} else if( node.nodeType == 2 ){//2为属性节点
				console.log("nodeType=====22");
			} else if( node.nodeType == 3 ){//3为文本节点
				this.compileText( node );
			}
			
			// 递归子节点
			if (node.childNodes && node.childNodes.length > 0) {
				this.compile(node);
			}
		})
	}
	
	wzDir_click(node, attrVal){
		var fn = this.$vm.$options.methods[attrVal];
		node.addEventListener( 'click', fn.bind(this.$vm));
	}
	
	wzDir_model( node, value ){
		const vm = this.$vm;
		this.updaterAll( 'model', node, node.value );
		node.addEventListener("input", e => {
		  vm[value] = e.target.value;
		});
	}
	
	wzDir_html( node, value ){
		this.updaterHtml( node, this.$vm[value] );
	}
	
	updaterHtml( node, value ){
		node.innerHTML = value;
	}
	
	compileText( node ){
		if( typeof( node.textContent ) !== 'string' ) {
			return "";
		}
		console.log("node.textContent===" + node.textContent  );
		const reg = /({{(.*)}})/;
		const reg2 = /[^/{/}]+/;
		const key = String((node.textContent).match(reg)).match(reg2);//获取监听的key
		this.updaterAll( 'text', node, key );
	}
	
	updaterAll( type, node, key ) {
		switch( type ){
			case 'text':
				if( key ){
					const updater = this.updateText;
					const initVal = node.textContent;//记录原文本第一次的数据
					updater( node, this.$vm[key], initVal);
					new Watcher( this.$vm, key, initVal, function( value, initVal ){
						updater( node, value, initVal  );
					});
				}
				break;
			case 'model':
				const updater = this.updateModel;
				new Watcher( this.$vm, key, null, function( value, initVal ){
					updater( node, value );
				});
				break;
		}
	}

	updateModel( node, value ){
		node.value = value;
	}
	
	updateText( node, value, initVal ){
		var reg = /{{(.*)}}/ig;
		var replaceStr = String( initVal.match(reg) );
		var result = initVal.replace(replaceStr, value );
		node.textContent = result;
	}
	
}

class Watcher{
	
	constructor( vm, key, initVal, cb ){
		this.vm = vm;
		this.key = key;
		this.cb = cb;
		this.initVal = initVal;
		Dep.target = this;
		this.vm[this.key];
		Dep.target = null;
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
复制代码

文章结尾

文章均为原创手写,写一篇原创上万字的文章,明白了笔者的不易。

如有错误希望指出。

后续,我会继续react的总结。