前言
在前端三大框架并存的今天,vue已经是前端必须掌握的一部分。而对于很多入门者,或者转行前端的小伙伴们,个人觉得vue是一个非常适合入门的框架的之一。笔者个人觉得,无论从api的易学的角度出发,还是从原理层面解析,vue还是比react的简单一些。记得某个大神的面试分享:如果面试官没有vue跟react方向的要求,尽量往vue的方向扯,个人觉得是个非常优秀的意见哈哈哈。
身处跳槽涨薪的年代,相信很多同行们都已经背了很多面经。(虽然内心有点鄙视背题库的人,面试神一样,工作zhu一样)。 长久的发展,还是得扎扎实实的打好基础。如果面试官不再追问面经,反过来请你介绍vue,你想好怎么介绍你的vue项目吗?
本文的重点,如何介绍如何搭建vue项目,介绍你的vue项目。
此外,文章为个人源码第一篇,后续会陆续送上源码,以下是个人计划:
序号 | 博客主题 | 相关链接 |
---|---|---|
1 | 手写vue_mini源码解析(即本文) | https://juejin.im/post/6847902225138876424 |
2 | 手写react_mini源码解析 | https://juejin.im/post/6854573212018147342 |
3 | 手写webpack_mini源码解析 | https://juejin.im/post/6854573219245441038 |
4 | 手写jquery_mini源码解析 | https://juejin.im/post/6854573220356423694 |
5 | 手写vuex_mini源码解析 | https://juejin.im/post/6857059183762931720 |
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知识点汇总: https://juejin.im/post/6846687586430222343
从上边的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
https://blog.guowenfh.com/2016/03/24/vue-webpack-03-config/
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来完成异步操作(除非回调地狱,写过大项目的都知道定位问题太难了)。
另一方面,他还需要引入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。
- elementUI: https://element.eleme.io/#/zh-CN
- vant: https://youzan.github.io/vant/#/zh-CN/
- iviewui: https://www.iviewui.com/
- BootstrapVue: https://bootstrap-vue.org/docs
- Vux: https://vux.li/#/
- Mint UI: http://mint-ui.github.io/#!/zh-cn
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全攻略: https://ykloveyxk.github.io/2017/02/25/axios全攻略/#more
编译器改进
上文曾提到,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’],// … 其它配置项 }),
-
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的总结。
版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 逐步前行 原文链接:https://juejin.im/post/6847902225138876424