fsrookie

fsrookie 查看完整档案

合肥编辑合肥师范学院  |  计算机科学与技术 编辑xxx  |  xxx 编辑 zw-lazy.cn 编辑
编辑

目前很多文章都是摘抄记录其他教程。见谅。
目前准备转到掘金:https://juejin.im/user/589b33...

个人动态

fsrookie 发布了文章 · 8月4日

前端路由解析以及实现(学习)

1. 什么是前端路由

路由的概念来源于服务端,在服务端中路由描述的是URL与处理函数之间的映射关系。

在web前端单页面应用中,路由描述的是 URL 和 UI 之间的映射关系,这种映射关系是单向的,即 URL 变化引起 UI 更新。

2. 如何实现前端路由

要实现前端路由,需要解决两个核心:

  • 如何改变URL 却不引起页面刷新
  • 如何检测URL 变化了

下面分别使用hash 和 history两种实现方式回答上面的两个核心问题。

2.1通过hash实现

  • hash是URL中#以及后面的那部分,常用作锚点在页面内进行导航,改变URL中的hash部分不会引起页面刷新。
  • 通过 hashchange监听URL的改变。通过浏览器前进后退,通过a标签改变,通过window.location改变都会触发hashchange事件。

2.2通过history实现

  • history 提供了pushState 和 popState方法,这两个方法改变URL的path部分不会引起页面刷新。
  • history 提供 popState事件,可以监听浏览器的前进后退事件。通过pushState/replaceState或者a标签改变URL不会触发popState事件,好在我们可以拦截pushState、replaceState、a标签的点击事件来检测URL变化,所以监听URL变化可以实现,只是没有hashChange那么方便。

3. 原生JS版前端路由实现

上面说到基本上分为两种实现方式,分别通过 hashhistory实现。

3.1 基于hash实现

<body>
  <ul>
    <!-- 定义路由 -->
    <li><a href="#/home">home</a></li>
    <li><a href="#/about">about</a></li>

    <!-- 渲染路由对应的 UI -->
    <div id="routeView"></div>
  </ul>
</body>
// 页面加载,主动触发一次
window.addEventListener('DOMContentLoaded', onLoad)

window.addEventListener('hashchange', onHashChange)

var routerView = null

function onload() {
    routerView = document.querySelector('#routerView')
}

function onHashChange() {
    switch(location.hash) {
        case '#/home':
            routerView.innerHTML = 'HOME'
            return
        case '#/about':
            routerView.innterHTML = 'About'
            return
        default:
            return
    }
}

3.2基于history实现

<body>
  <ul>
    <li><a href='/home'>home</a></li>
    <li><a href='/about'>about</a></li>

    <div id="routeView"></div>
  </ul>
</body>
window.addEventListener('DOMContentLoaded', onLoad)

window.addEvenetListener('popState', onPopState)

var routerView = null

function onLoad() {
    routerView = document.querySelector('#routerView')
    onPopState()
    // 拦截a标签
    var linkList = document.querySelectorAll('a[href]')
    linkList.forEach(el => {
        el.addEventListener('click', function(e){
            e.preventDefault()
            history.pushState(null, '', el.getAttribute('href')
            onPopState()
        })
    })
}

function onPopState() {
    switch(location.pathname) {
        case '/home':
            routerView.innterHTML = 'HOME'
            return
        case '/about':
            routerView.innerHTML = 'about'
            return
        default: 
            return
    }
}

4. React版前端路由实现

4.1基于hash实现

<BrowserRouter>
    <ul>
      <li>
        <Link to="/home">home</Link>
      </li>
      <li>
        <Link to="/about">about</Link>
      </li>
    </ul>

    <Route path="/home" render={() => <h2>Home</h2>} />
    <Route path="/about" render={() => <h2>About</h2>} />
  </BrowserRouter>

BrowerRouter实现:

export deafult class BrowerRouter extends React.Component {
    state = {
        currentPath: util.extractHashPath(window.location.href)
    }
    
    onHashChange = (e) => {
        const currentPath = util.extrachHashPath(e.newURL)
        this.setState({currentPath})
    }
    
    componentDidMount() {
        window.addEventListener('hashChange', this.onHashChange)
    }
    
    componentWillUnmount() {
        window.removeEventListner('hashChange',this.onHashChange)
    }
    
    render() {
        return (
            <RouteContext.Provider value={{currentPath: this.state.currentPath}}>
        {this.props.children}
      </RouteContext.Provider>
        )
    }
}

Route实现

export default ({path, render}) => {
    <RouteContext.Consumer>
        {{(currentPath) => currentPath === path && render()}}
    </RouteContext.Consumer>
}

Link实现

export default ({ to, ...props }) => <a {...props} href={"#" + to} />;

4.2基于history实现

export default class HistoryRouter extends React.Component {
    state = {
        currentPath: utils.extractUrlPath(window.location.href)
    }
    
    onPopState = (e) => {
        const currentPath = utils.extractUrlPath(window.location.href)
        this.setState({currentPath})
    }
    
    componentDidMont() {
        window.addEventListener('popState', this.onPopState)
    }
    componentWillUnmount() {
        window.removeEventListener('popstate', this.onPopState)
    }
    
    render() {
    return (
      <RouteContext.Provider value={{currentPath: this.state.currentPath, onPopState: this.onPopState}}>
        {this.props.children}
      </RouteContext.Provider>
    );
  }
}

Route的实现

export default ({path, render}) => (
    <RouteContext.Consumer>
        {({currentPath}) => {currentPath === path && render()}}
    </RouteContext.Consumer>
)

Link实现

export default ({to, ...props}) => {
    <RouteContext.Consumer>
        {({onPopState}) => (
            <a href="" {...props} onClick={
                e=> {
                    e.preventDefault()
                    window.history.pushState(null, "", to)
                    onPopState()
                }
            }>
        )}
    </RouteContext.Consumer>
}

5. Vue版本前端路由实现

5.1基于hash实现

使用方式和 vue-router类型(vue-router通过插件的机制注入路由,但是这样隐藏了实现细节,为了保持代码直观,这里没有使用vue插件封装):

<div>
      <ul>
        <li><router-link to="/home">home</router-link></li>
        <li><router-link to="/about">about</router-link></li>
      </ul>
      <router-view></router-view>
    </div>
const routes = {
    '/home': {
        template: '<h2>Home</h2>'
    }
}

const app = new Vue({
    el: '',
    components: {
        'router-view': RouterView,
        'router-link': RouterLink
    },
    beforeCreate() {
        this.$routes = routes
    }
})

router-view实现:

<template>
  <component :is="routeView" />
</template>

<script>
import utils from '~/utils.js'
export default {
  data () {
    return {
      routeView: null
    }
  },
  created () {
    this.boundHashChange = this.onHashChange.bind(this)
  },
  beforeMount () {
    window.addEventListener('hashchange', this.boundHashChange)
  },
  mounted () {
    this.onHashChange()
  },
  beforeDestroy() {
    window.removeEventListener('hashchange', this.boundHashChange)
  },
  methods: {
    onHashChange () {
      const path = utils.extractHashPath(window.location.href)
      this.routeView = this.$root.$routes[path] || null
      console.log('vue:hashchange:', path)
    }
  }
}
</script>

router-link实现:

<template>
  <a @click.prevent="onClick" href=''><slot></slot></a>
</template>

<script>
export default {
  props: {
    to: String
  },
  methods: {
    onClick () {
      window.location.hash = '#' + this.to
    }
  }
}
</script>

5.2基history实现

<div>
      <ul>
        <li><router-link to="/home">home</router-link></li>
        <li><router-link to="/about">about</router-link></li>
      </ul>
      <router-view></router-view>
</div>
const routes = {
  '/home': {
    template: '<h2>Home</h2>'
  },
  '/about': {
    template: '<h2>About</h2>'
  }
}

const app = new Vue({
  el: '.vue.history',
  components: {
    'router-view': RouterView,
    'router-link': RouterLink
  },
  created () {
    this.$routes = routes
    this.boundPopState = this.onPopState.bind(this)
  },
  beforeMount () {
    window.addEventListener('popstate', this.boundPopState) 
  },
  beforeDestroy () {
    window.removeEventListener('popstate', this.boundPopState) 
  },
  methods: {
    onPopState (...args) {
      this.$emit('popstate', ...args)
    }
  }
})

router-view 实现:

<template>
  <component :is="routeView" />
</template>

<script>
import utils from '~/utils.js'
export default {
  data () {
    return {
      routeView: null
    }
  },
  created () {
    this.boundPopState = this.onPopState.bind(this)
  },
  beforeMount () {
    this.$root.$on('popstate', this.boundPopState)
  },
  beforeDestroy() {
    this.$root.$off('popstate', this.boundPopState)
  },
  methods: {
    onPopState (e) {
      const path = utils.extractUrlPath(window.location.href)
      this.routeView = this.$root.$routes[path] || null
      console.log('[Vue] popstate:', path)
    }
  }
}
</script>

router-link实现:

<template>
  <a @click.prevent="onClick" href=''><slot></slot></a>
</template>

<script>
export default {
  props: {
    to: String
  },
  methods: {
    onClick () {
      history.pushState(null, '', this.to)
      this.$root.$emit('popstate')
    }
  }
}
</script>

小结

以上大部分内容和原文差不多,主要是记录个人学习的一个过程,全程基本上自己手打,加深印象。共勉~

查看原文

赞 0 收藏 0 评论 0

fsrookie 发布了文章 · 4月15日

关于vue中如何监听数组变化

前言

前段时间学习了关于vue中响应式数据的原理,(并作了学习笔记vue响应式原理),其实是通过Object.defineProperty控制getter和setter,并利用观察者模式完成的响应式设计。那么数组有一系列的操作方法,这些方法并不会触发数组的getter和setter方法。那么vue中针对数组的响应式设计是如何实现的呢...那么我们一起去学习下吧~

源码部分

https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js

从哪开始第一步学习呢

Emmmm...
我觉得要先把Vue中的数据响应式原理弄清楚,这样对于理解vue中是如何检测数组的变化才比较好,所以,可以去网上找下文章然后配合源码进行阅读,相信你一定会理解的。推荐下我之前看的一篇博客,还有我看过后自己写的学习记录吧,哈哈。

好的,先看看这个吧。哈哈!

从图开始

咱们先看下下面的图,先了解下vue中实现的思路,这样接下来再看源码的实现,会一清二楚,明明白白。
image.png

看到这个图然后思考一下,是不是大致了解了~

  • 首先判断浏览器是否支持__proto__指针
  • 重写数组的这7个方法,然后根据是否支持__proto__,将改写后的数组指向数组的prototype。

是不是很简单!!!

看看源码吧

了解了实现原理,那么我们再看看源码吧,看下源码主要是更深入的了解作者是如何实现的,也可以看下优秀的代码编码方式,加以学习。

关于一些解释我就写在下面的代码块中了哈!

//https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js


//def方法是基于Object.defineProperty封装的一层方法,很简单,我会在下面把代码贴出来,免得大家去找了。
import { def } from '../util/index' 

//保存下原生的数组原型对象
const arrayProto = Array.prototype

//进行原型连接,将arrayMethods的原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  // 缓存原生的方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    var args = [], 
    len = arguments.length;
    while (len--) args[len] = arguments[len];
    const result = original.apply(this, args) // 原来的数组方法执行结果
    const ob = this.__ob__ // 这个__ob__就是Observe的实例~~~~
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted) // 如果数组有变化,则重新调用observeArray
    // notify change
    ob.dep.notify()  //
    return result
  })
})

这个是关于Observe的代码:

var Observer = function Observer(value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);  //这里会看到在每个对象数据上都会绑定一个Observe的实例,所以上面代码中的this.__ob__就是这个
    if (Array.isArray(value)) { // 这里判断是否是数组类型的数据,如果是的话就走observeArray
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value); //这里就是处理数组类型的数据,如下
    } else {
      this.walk(value);
    }
  };

如下是observeArray的实现:

Observer.prototype.observeArray = function observeArray(items) {
    for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]); // 这个observe方法如下
    }
  };

在这里我们看下observe这个方法:

function observe(value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
      ob = new Observer(value);
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob
  }

这个是关于def方法的实现,很简单我就不说了哈:

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
      value: val,
      enumerable: !!enumerable,
      writable: true,
      configurable: true
    });
}

就这么多吧...
也没讲什么,大致看下整理出来的核心代码,应该就可以了解它实现的思路了吧...
等我研究透了再更新下,就这样吧..

查看原文

赞 2 收藏 2 评论 0

fsrookie 发布了文章 · 3月10日

vue响应式原理学习

参考链接

https://www.cnblogs.com/canfo...

前言

提到vue,大家肯定会想到双向数据绑定,数据驱动视图,虚拟DOM,diff算法等等这些概念。在使用vue的时候,会感觉到它的数据双向绑定真的很爽啊。会不会在你用了很长时间后,会好奇到,这个是如何实现的?或者在遇到问题的时候,会不会想到,为啥这个数据并没有响应式的发生改变,视图怎么没有变化...当你抱着这些疑问的时候你肯定会想了解其中的原理了。那么我也是..现在把之前学习和理解的内容整理一下,如果有什么问题,请多多指教~

要了解的核心API

众所周知,是个vue的使用者都知道其响应式数据是结合Object.defineProperty()这个方法实现,那么关于这个方法的使用和作用,请自行了解..一个合格的jser应该都知道的。

要了解的设计模式

核心是观察者模式,数据是我们的被观察者,发生改变的时候,会通知我们所有的观察者

我们来分析这个图

vue响应式

关于上面这个图,请仔细的看下,从vue官网拔过来的...。主要包括数据变化更新视图,视图变化更新数据。其中主要涉及ObserveWatcher,Dep这三个类,要了解vue的响应式原理,弄清楚这三个类是如何运作的,这样就能够大概了解了。
Observe是数据监听器,其实现方法就是Object.defineProperty
Watcher是我们所说的观察者。
Dep是可以容纳观察者的一个订阅器,主要收集订阅者,然后在发生变化的时候通知每个订阅者。

实现一个Observe

observe的主要工作是针对vue中的响应式数据属性进行监听,所以通过递归的方法遍历属性。

function observe(data) {
    if(!data || typeof data !== 'object') {
        return
    }
    
    Object.keys(data).map((k) => {
        defineReactive(data, k, data[k])
    })
}

function defineReactive(data, k, val) {
    observe(val)
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return val
        },
        set: function(newval) {
            val = newval
        }
    })
}

创建一个Dep订阅器

订阅器负责收集观察者,然后在属性变化的时候通知观察者进行更新。

function Dep() {
    this.subs = []
}

Dep.prototype.addSub = function(sub) {
    this.subs.push(sub)
}
Dep.prototype.notify = function() {
    this.subs.forEach(v => {
        v.update()
    })
}

那么什么时候将一个观察者添加到Dep里面呢?设计上是将观察者的添加放在getter里面。

实现一个Watcher

上面说到Watcher是在初始化的时候在getter里面放进订阅器Dep中。那么在Watcher初始化的时候触发getter。那么还有个问题,在getter中是如何获取到Watcher的?这个我们可以暂时缓存的放到Dep的静态属性Dep.target上面。

function Watcher(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.value = this.get()
}
Watcher.prototype.update = function() {}
Watcher.prototype.run = function() {
  var value = this.vm.data[this.exp]; 
  var oldVal = this.value; 
  if (value !== oldVal) {      
    this.value = value; 
    this.cb.call(this.vm, value, oldVal);
  }         
}
Watcher.prototype.get = function() {
    Dep.target = this // 在target上进行缓存
    var value = this.vm.data[exp] //触发getter
    Dep.target = null //清空null
    return value
}

这个时候我们还需要修改下Observe:

function defineReactive(data, k, val) {
    observe(val)
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if(Dep.target) {
                dep.addSub(Dep.target)
            }
            return val
        },
        set: function(newval) {
            if(newval === val) {
                return
            }
            val = newval
            dep.notify()
        }
    })
}

看到这里,大致应该都明白如何把Observe,Dep,Watcher如何运作到一起了吧。

如何通过这三者完成一个数据的双向绑定

function MyVue(data, el, exp) {
    this.data = data
    observe(this.data)
    el.innerHTML = this.data[exp]
    new Watcher(this, exp, function(value) {
        el.innerHTML = value
    })
}

优化MyVue,代理data上属性

function MyVue(data, el, exp) {
    this.data = data
    Object.key(data).forEach(function(k) => {
        this.proxyKeys(k)
    })
    observe(data);
    el.innerHTML = this.data[exp];  // 初始化模板数据的值
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}
MyVue.prototype.proxyKeys = function(k) {
    Object.defineProperty(this, k, {
        enumerable: false,
        configurable: true,
        get: function proxyGetter() {
            return this.data[key];
        },
        set: function proxySetter(newVal) {
            this.data[key] = newVal;
        }
    })
}

实现compile解析dom节点

上面的例子中没有解析DOM节点的操作,只是针对指定dom节点进行内容的替换。那么compile要进行哪写操作呢?

  1. 解析模板指定,并替换模板数据
  2. 将模板指定对应的节点绑定对应的更新函数,并初始化相应的订阅器。
function nodeToFragment(el) {
    var fragment = document.createDocumentFragment()
    var child = el.firstChild
    while(child) {
        fragment.appendChild(child)
        child = el.firstChild
    }
    return fragment
}

接下来需要遍历各个节点,对含有相关指定的节点进行特殊处理。

function compileElement(el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/;
        var text = node.textContent;
 
        if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
 
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
}

function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) {  // 生成订阅器并绑定更新函数
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

获取到最外层节点后,调用compileElement函数,对所有子节点进行判断,如果节点是文本节点且匹配{{}}这种形式指令的节点就开始进行编译处理,编译处理首先需要初始化视图数据,对应上面所说的步骤1,接下去需要生成一个并绑定更新函数的订阅器,对应上面所说的步骤2。这样就完成指令的解析、初始化、编译三个过程,一个解析器Compile也就可以正常的工作了。为了将解析器Compile与监听器Observer和订阅者Watcher关联起来,我们需要再修改一下类MySelf函数:

function MyVue(opt) {
    let self = this
    this.vm = this
    this.data = opt.data
    
    Object.key(this.data).forEach(function(k) {
        this.proxyKeys(k)
    })
    observe(this.data)
    new Compile(opt, this.vm)
    return this
}

感觉大致说的差不多了...希望能够对大家有点点启发,谢谢。

查看原文

赞 9 收藏 7 评论 0

fsrookie 赞了文章 · 3月2日

Vuex源码阅读分析

Vuex源码阅读分析

Vuex是专为Vue开发的统一状态管理工具。当我们的项目不是很复杂时,一些交互可以通过全局事件总线解决,但是这种观察者模式有些弊端,开发时可能没什么感觉,但是当项目变得复杂,维护时往往会摸不着头脑,如果是后来加入的伙伴更会觉得很无奈。这时候可以采用Vuex方案,它可以使得我们的项目的数据流变得更加清晰。本文将会分析Vuex的整个实现思路,当是自己读完源码的一个总结。

目录结构

vuex目录结构

  • module:提供对module的处理,主要是对state的处理,最后构建成一棵module tree
  • plugins:和devtools配合的插件,提供像时空旅行这样的调试功能。
  • helpers:提供如mapActions、mapMutations这样的api
  • index、index.esm:源码的主入口,抛出Store和mapActions等api,一个用于commonjs的打包、一个用于es module的打包
  • mixin:提供install方法,用于注入$store
  • store:vuex的核心代码
  • util:一些工具函数,如deepClone、isPromise、assert

源码分析

先从一个简单的示例入手,一步一步分析整个代码的执行过程,下面是官方提供的简单示例

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

1. Vuex的注册

Vue官方建议的插件使用方法是使用Vue.use方法,这个方法会调用插件的install方法,看看install方法都做了些什么,从index.js中可以看到install方法在store.js中抛出,部分代码如下

let Vue // bind on install

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

声明了一个Vue变量,这个变量在install方法中会被赋值,这样可以给当前作用域提供Vue,这样做的好处是不需要额外import Vue from 'vue' 不过我们也可以这样写,然后让打包工具不要将其打包,而是指向开发者所提供的Vue,比如webpack的externals,这里就不展开了。执行install会先判断Vue是否已经被赋值,避免二次安装。然后调用applyMixin方法,代码如下

// applyMixin
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    // 当我们在执行new Vue的时候,需要提供store字段
    if (options.store) {
      // 如果是root,将store绑到this.$store
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 否则拿parent上的$store
      // 从而实现所有组件共用一个store实例
      this.$store = options.parent.$store
    }
  }
}

这里会区分vue的版本,2.x和1.x的钩子是不一样的,如果是2.x使用beforeCreate,1.x即使用_init。当我们在执行new Vue启动一个Vue应用程序时,需要给上store字段,根组件从这里拿到store,子组件从父组件拿到,这样一层一层传递下去,实现所有组件都有$store属性,这样我们就可以在任何组件中通过this.$store访问到store

接下去继续看例子

// store.js
export default new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
  },
  actions:  {
    increment: ({ commit }) => commit('increment'),
    decrement: ({ commit }) => commit('decrement')
  },
  mutations: {
    increment (state) {
      state.count++
    },
    decrement (state) {
      state.count--
    }
  }
})
// app.js
new Vue({
  el: '#app',
  store, // 传入store,在beforeCreate钩子中会用到
  render: h => h(Counter)
})

2.store初始化

这里是调用Store构造函数,传入一个对象,包括state、actions等等,接下去看看Store构造函数都做了些什么

export class Store {
  constructor (options = {}) {
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      // 挂载在window上的自动安装,也就是通过script标签引入时不需要手动调用Vue.use(Vuex)
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      // 断言必须使用Vue.use(Vuex),在install方法中会给Vue赋值
      // 断言必须存在Promise
      // 断言必须使用new操作符
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

    const {
      plugins = [],
      strict = false
    } = options

    // store internal state
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    // 这里进行module收集,只处理了state
    this._modules = new ModuleCollection(options)
    // 用于保存namespaced的模块
    this._modulesNamespaceMap = Object.create(null)
    // 用于监听mutation
    this._subscribers = []
    // 用于响应式地监测一个 getter 方法的返回值
    this._watcherVM = new Vue()

    // 将dispatch和commit方法的this指针绑定到store上,防止被修改
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // strict mode
    this.strict = strict

    const state = this._modules.root.state

    // 这里是module处理的核心,包括处理根module、action、mutation、getters和递归注册子module
    installModule(this, state, [], this._modules.root)

    // 使用vue实例来保存state和getter
    resetStoreVM(this, state)

    // 插件注册
    plugins.forEach(plugin => plugin(this))

    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }
}

首先会判断Vue是不是挂载在window上,如果是的话,自动调用install方法,然后进行断言,必须先调用Vue.use(Vuex)。必须提供Promise,这里应该是为了让Vuex的体积更小,让开发者自行提供Promisepolyfill,一般我们可以使用babel-runtime或者babel-polyfill引入。最后断言必须使用new操作符调用Store函数。

接下去是一些内部变量的初始化
_committing提交状态的标志,在_withCommit中,当使用mutation时,会先赋值为true,再执行mutation,修改state后再赋值为false,在这个过程中,会用watch监听state的变化时是否_committing为true,从而保证只能通过mutation来修改state
_actions用于保存所有action,里面会先包装一次
_actionSubscribers用于保存订阅action的回调
_mutations用于保存所有的mutation,里面会先包装一次
_wrappedGetters用于保存包装后的getter
_modules用于保存一棵module树
_modulesNamespaceMap用于保存namespaced的模块

接下去的重点是

this._modules = new ModuleCollection(options)
2.1 模块收集

接下去看看ModuleCollection函数都做了什么,部分代码如下

// module-collection.js
export default class ModuleCollection {
  constructor (rawRootModule) {
    // 注册 root module (Vuex.Store options)
    this.register([], rawRootModule, false)
  }

  get (path) {
    // 根据path获取module
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

  /*
   * 递归注册module path是路径 如
   * {
   *    modules: {
   *      a: {
   *        state: {}
   *      }
   *    }
   * }
   * a模块的path => ['a']
   * 根模块的path => []
   */
  register (path, rawModule, runtime = true) {
    if (process.env.NODE_ENV !== 'production') {
      // 断言 rawModule中的getters、actions、mutations必须为指定的类型
      assertRawModule(path, rawModule)
    }

    // 实例化一个module
    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      // 根module 绑定到root属性上
      this.root = newModule
    } else {
      // 子module 添加其父module的_children属性上
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 如果当前模块存在子模块(modules字段)
    // 遍历子模块,逐个注册,最终形成一个树
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

// module.js
export default class Module {
  constructor (rawModule, runtime) {
    // 初始化时runtime为false
    this.runtime = runtime
    // Store some children item
    // 用于保存子模块
    this._children = Object.create(null)
    // Store the origin module object which passed by programmer
    // 保存原来的moudle,在Store的installModule中会处理actions、mutations等
    this._rawModule = rawModule
    const rawState = rawModule.state

    // Store the origin module's state
    // 保存state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  addChild (key, module) {
    // 将子模块添加到_children中
    this._children[key] = module
  }
}

这里调用ModuleCollection构造函数,通过path的长度判断是否为根module,首先进行根module的注册,然后递归遍历所有的module,子module 添加其父module的_children属性上,最终形成一棵树

module-collection

接着,还是一些变量的初始化,然后

2.2 绑定commit和dispatch的this指针
// 绑定commit和dispatch的this指针
const store = this
const { dispatch, commit } = this
this.dispatch = function boundDispatch (type, payload) {
  return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
  return commit.call(store, type, payload, options)
}

这里会将dispath和commit方法的this指针绑定为store,比如下面这样的骚操作,也不会影响到程序的运行

this.$store.dispatch.call(this, 'someAction', payload)
2.3 模块安装

接着是store的核心代码

// 这里是module处理的核心,包括处理根module、命名空间、action、mutation、getters和递归注册子module
installModule(this, state, [], this._modules.root)
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  /*
   * {
   *   // ...
   *   modules: {
   *     moduleA: {
   *       namespaced: true
   *     },
   *     moduleB: {}
   *   }
   * }
   * moduleA的namespace -> 'moduleA/'
   * moduleB的namespace -> ''
   */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  if (module.namespaced) {
    // 保存namespaced模块
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    // 非根组件设置state
    // 根据path获取父state
    const parentState = getNestedState(rootState, path.slice(0, -1))
    // 当前的module
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      // 使用Vue.set将state设置为响应式
      Vue.set(parentState, moduleName, module.state)
    })
    console.log('end', store)
  }

  // 设置module的上下文,从而保证mutation和action的第一个参数能拿到对应的state getter等
  const local = module.context = makeLocalContext(store, namespace, path)

  // 逐一注册mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 逐一注册action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  // 逐一注册getter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 逐一注册子module
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

首先保存namespaced模块到store._modulesNamespaceMap,再判断是否为根组件且不是hot,得到父级module的state和当前module的name,调用Vue.set(parentState, moduleName, module.state)将当前module的state挂载到父state上。接下去会设置module的上下文,因为可能存在namespaced,需要额外处理

// 设置module的上下文,绑定对应的dispatch、commit、getters、state
function makeLocalContext (store, namespace, path) {
  // namespace 如'moduleA/'
  const noNamespace = namespace === ''

  const local = {
    // 如果没有namespace,直接使用原来的
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      // 统一格式 因为支持payload风格和对象风格
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      // 如果root: true 不会加上namespace 即在命名空间模块里提交根的 action
      if (!options || !options.root) {
        // 加上命名空间
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }
      // 触发action
      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      // 统一格式 因为支持payload风格和对象风格
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      // 如果root: true 不会加上namespace 即在命名空间模块里提交根的 mutation
      if (!options || !options.root) {
        // 加上命名空间
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }
      // 触发mutation
      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  // 这里的getters和state需要延迟处理,需要等数据更新后才进行计算,所以使用getter函数,当访问的时候再进行一次计算
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace) // 获取namespace下的getters
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // 如果getter不在该命名空间下 直接return
    if (type.slice(0, splitPos) !== namespace) return

    // 去掉type上的命名空间
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    // 给getters加一层代理 这样在module中获取到的getters不会带命名空间,实际返回的是store.getters[type] type是有命名空间的
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

这里会判断modulenamespace是否存在,不存在不会对dispatchcommit做处理,如果存在,给type加上namespace,如果声明了{root: true}也不做处理,另外gettersstate需要延迟处理,需要等数据更新后才进行计算,所以使用Object.defineProperties的getter函数,当访问的时候再进行计算

再回到上面的流程,接下去是逐步注册mutationactiongetter子module,先看注册mutation

/*
 * 参数是store、mutation的key(namespace处理后的)、handler函数、当前module上下文
 */
function registerMutation (store, type, handler, local) {
  // 首先判断store._mutations是否存在,否则给空数组
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 将mutation包一层函数,push到数组中
  entry.push(function wrappedMutationHandler (payload) {
    // 包一层,commit执行时只需要传入payload
    // 执行时让this指向store,参数为当前module上下文的state和用户额外添加的payload
    handler.call(store, local.state, payload)
  })
}

mutation的注册比较简单,主要是包一层函数,然后保存到store._mutations里面,在这里也可以知道,mutation可以重复注册,不会覆盖,当用户调用this.$store.commit(mutationType, payload)时会触发,接下去看看commit函数

// 这里的this已经被绑定为store
commit (_type, _payload, _options) {
  // 统一格式,因为支持对象风格和payload风格
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  // 获取当前type对应保存下来的mutations数组
  const entry = this._mutations[type]
  if (!entry) {
    // 提示不存在该mutation
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  // 包裹在_withCommit中执行mutation,mutation是修改state的唯一方法
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      // 执行mutation,只需要传入payload,在上面的包裹函数中已经处理了其他参数
      handler(payload)
    })
  })
  // 执行mutation的订阅者
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    // 提示silent参数已经移除
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools'
    )
  }
}

首先对参数进行统一处理,因为是支持对象风格和载荷风格的,然后拿到当前type对应的mutation数组,使用_withCommit包裹逐一执行,这样我们执行this.$store.commit的时候会调用对应的mutation,而且第一个参数是state,然后再执行mutation的订阅函数

接下去看action的注册

/*
 * 参数是store、type(namespace处理后的)、handler函数、module上下文
 */
function registerAction (store, type, handler, local) {
  // 获取_actions数组,不存在即赋值为空数组
  const entry = store._actions[type] || (store._actions[type] = [])
  // push到数组中
  entry.push(function wrappedActionHandler (payload, cb) {
    // 包一层,执行时需要传入payload和cb
    // 执行action
    let res = handler.call(
      store,
      {
        dispatch: local.dispatch,
        commit: local.commit,
        getters: local.getters,
        state: local.state,
        rootGetters: store.getters,
        rootState: store.state
      },
      payload,
      cb
    )
    // 如果action的执行结果不是promise,将他包裹为promise,这样就支持promise的链式调用
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      // 使用devtool处理一次error
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

mutation很类似,使用函数包一层然后push到store._actions中,有些不同的是执行时参数比较多,这也是为什么我们在写action时可以解构拿到commit等的原因,然后再将返回值promisify,这样可以支持链式调用,但实际上用的时候最好还是自己返回promise,因为通常action是异步的,比较多见是发起ajax请求,进行链式调用也是想当异步完成后再执行,具体根据业务需求来。接下去再看看dispatch函数的实现

// this已经绑定为store
dispatch (_type, _payload) {
  // 统一格式
  const { type, payload } = unifyObjectStyle(_type, _payload)

  const action = { type, payload }
  // 获取actions数组
  const entry = this._actions[type]
  // 提示不存在action
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }
  // 执行action的订阅者
  this._actionSubscribers.forEach(sub => sub(action, this.state))
  // 如果action大于1,需要用Promise.all包裹
  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}

这里和commit也是很类似的,对参数统一处理,拿到action数组,如果长度大于一,用Promise.all包裹,不过直接执行,然后返回执行结果。

接下去是getters的注册和子module的注册

/*
 * 参数是store、type(namesapce处理后的)、getter函数、module上下文
 */
function registerGetter (store, type, rawGetter, local) {
  // 不允许重复定义getters
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // 包一层,保存到_wrappedGetters中
  store._wrappedGetters[type] = function wrappedGetter (store) {
    // 执行时传入store,执行对应的getter函数
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

首先对getters进行判断,和mutation是不同的,这里是不允许重复定义的,然后包裹一层函数,这样在调用时只需要给上store参数,而用户的函数里会包含local.statelocal.gettersstore.statestore.getters

// 递归注册子module
installModule(store, rootState, path.concat(key), child, hot)
使用vue实例保存state和getter

接着再继续执行resetStoreVM(this, state),将stategetters存放到一个vue实例中,

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
function resetStoreVM (store, state, hot) {
  // 保存旧vm
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 循环所有getters,通过Object.defineProperty方法为getters对象建立属性,这样就可以通过this.$store.getters.xxx访问
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // getter保存在computed中,执行时只需要给上store参数,这个在registerGetter时已经做处理
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 使用一个vue实例来保存state和getter
  // silent设置为true,取消所有日志警告等
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // 恢复用户的silent设置
  Vue.config.silent = silent

  // enable strict mode for new vm
  // strict模式
  if (store.strict) {
    enableStrictMode(store)
  }
  // 若存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁
  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

这里会重新设置一个新的vue实例,用来保存stategettergetters保存在计算属性中,会给getters加一层代理,这样可以通过this.$store.getters.xxx访问到,而且在执行getters时只传入了store参数,这个在上面的registerGetter已经做了处理,也是为什么我们的getters可以拿到stategettersrootStaterootGetters的原因。然后根据用户设置开启strict模式,如果存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁

function enableStrictMode (store) {
  store._vm.$watch(
    function () {
      return this._data.$$state
    },
    () => {
      if (process.env.NODE_ENV !== 'production') {
        // 不允许在mutation之外修改state
        assert(
          store._committing,
          `Do not mutate vuex store state outside mutation handlers.`
        )
      }
    },
    { deep: true, sync: true }
  )
}

使用$watch来观察state的变化,如果此时的store._committing不会true,便是在mutation之外修改state,报错。

再次回到构造函数,接下来是各类插件的注册

2.4 插件注册
// apply plugins
plugins.forEach(plugin => plugin(this))

if (Vue.config.devtools) {
  devtoolPlugin(this)
}

到这里store的初始化工作已经完成。大概长这个样子

store

看到这里,相信已经对store的一些实现细节有所了解,另外store上还存在一些api,但是用到的比较少,可以简单看看都有些啥

2.5 其他api
  • watch (getter, cb, options)

用于监听一个getter值的变化

watch (getter, cb, options) {
  if (process.env.NODE_ENV !== 'production') {
    assert(
      typeof getter === 'function',
      `store.watch only accepts a function.`
    )
  }
  return this._watcherVM.$watch(
    () => getter(this.state, this.getters),
    cb,
    options
  )
}

首先判断getter必须是函数类型,使用$watch方法来监控getter的变化,传入stategetters作为参数,当值变化时会执行cb回调。调用此方法返回的函数可停止侦听。

  • replaceState(state)

用于修改state,主要用于devtool插件的时空穿梭功能,代码也相当简单,直接修改_vm.$$state

replaceState (state) {
  this._withCommit(() => {
    this._vm._data.$$state = state
  })
}
  • registerModule (path, rawModule, options = {})

用于动态注册module

registerModule (path, rawModule, options = {}) {
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(
      path.length > 0,
      'cannot register the root module by using registerModule.'
    )
  }

  this._modules.register(path, rawModule)
  installModule(
    this,
    this.state,
    path,
    this._modules.get(path),
    options.preserveState
  )
  // reset store to update getters...
  resetStoreVM(this, this.state)
  }

首先统一path的格式为Array,接着是断言,path只接受StringArray类型,且不能注册根module,然后调用store._modules.register方法收集module,也就是上面的module-collection里面的方法。再调用installModule进行模块的安装,最后调用resetStoreVM更新_vm

  • unregisterModule (path)

根据path注销module

unregisterModule (path) {
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
  }

  this._modules.unregister(path)
  this._withCommit(() => {
    const parentState = getNestedState(this.state, path.slice(0, -1))
    Vue.delete(parentState, path[path.length - 1])
  })
  resetStore(this)
}

registerModule一样,首先统一path的格式为Array,接着是断言,path只接受StringArray类型,接着调用store._modules.unregister方法注销module,然后在store._withCommit中将该modulestate通过Vue.delete移除,最后调用resetStore方法,需要再看看resetStore的实现

function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}

这里是将_actions_mutations_wrappedGetters_modulesNamespaceMap都清空,然后调用installModuleresetStoreVM重新进行全部模块安装和_vm的设置

  • _withCommit (fn)

用于执行mutation

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}

在执行mutation的时候,会将_committing设置为true,执行完毕后重置,在开启strict模式时,会监听state的变化,当变化时_committing不为true时会给出警告

3. 辅助函数

为了避免每次都需要通过this.$store来调用api,vuex提供了mapStatemapMutationsmapGettersmapActionscreateNamespacedHelpers 等api,接着看看各api的具体实现,存放在src/helpers.js

3.1 一些工具函数

下面这些工具函数是辅助函数内部会用到的,可以先看看功能和实现,主要做的工作是数据格式的统一、和通过namespace获取module

/**
 * 统一数据格式
 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
 * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
 * @param {Array|Object} map
 * @return {Object}
 */
function normalizeMap (map) {
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

/**
 * 返回一个函数,接受namespace和map参数,判断是否存在namespace,统一进行namespace处理
 * @param {Function} fn
 * @return {Function}
 */
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

/**
 * 根据namespace获取module
 * @param {Object} store
 * @param {String} helper
 * @param {String} namespace
 * @return {Object}
 */
function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (process.env.NODE_ENV !== 'production' && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}
3.2 mapState

为组件创建计算属性以返回 store 中的状态

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  normalizeMap(states).forEach(({ key, val }) => {
  // 返回一个对象,值都是函数
  res[key] = function mappedState () {
    let state = this.$store.state
    let getters = this.$store.getters
    if (namespace) {
      // 如果存在namespace,拿该namespace下的module
      const module = getModuleByNamespace(this.$store, 'mapState', namespace)
      if (!module) {
        return
      }
      // 拿到当前module的state和getters
      state = module.context.state
      getters = module.context.getters
    }
    // Object类型的val是函数,传递过去的参数是state和getters
    return typeof val === 'function'
      ? val.call(this, state, getters)
      : state[val]
  }
  // mark vuex getter for devtools
  res[key].vuex = true
  })
  return res
})

mapStatenormalizeNamespace的返回值,从上面的代码可以看到normalizeNamespace是进行参数处理,如果存在namespace便加上命名空间,对传入的states进行normalizeMap处理,也就是数据格式的统一,然后遍历,对参数里的所有state都包裹一层函数,最后返回一个对象

大概是这么回事吧

export default {
  // ...
  computed: {
    ...mapState(['stateA'])
  }
  // ...
}

等价于

export default {
  // ...
  computed: {
    stateA () {
      return this.$store.stateA
    }
  }
  // ...
}
3.4 mapGetters

store 中的 getter 映射到局部计算属性中

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    // this namespace has been mutate by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

同样的处理方式,遍历getters,只是这里需要加上命名空间,这是因为在注册时_wrapGetters中的getters是有加上命名空间的

3.4 mapMutations

创建组件方法提交 mutation

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    // 返回一个对象,值是函数
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      // 执行mutation,
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

和上面都是一样的处理方式,这里在判断是否存在namespace后,commit是不一样的,上面可以知道每个module都是保存了上下文的,这里如果存在namespace就需要使用那个另外处理的commit等信息,另外需要注意的是,这里不需要加上namespace,这是因为在module.context.commit中会进行处理,忘记的可以往上翻,看makeLocalContextcommit的处理

3.5 mapAction

创建组件方法分发 action

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      let dispatch = this.$store.dispatch
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

mapMutations基本一样的处理方式

4. 插件

Vuex中可以传入plguins选项来安装各种插件,这些插件都是函数,接受store作为参数,Vuex中内置了devtoollogger两个插件,

// 插件注册,所有插件都是一个函数,接受store作为参数
plugins.forEach(plugin => plugin(this))

// 如果开启devtools,注册devtool
if (Vue.config.devtools) {
  devtoolPlugin(this)
}
// devtools.js
const devtoolHook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook

  // 触发vuex:init
  devtoolHook.emit('vuex:init', store)

  // 时空穿梭功能
  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  // 订阅mutation,当触发mutation时触发vuex:mutation方法,传入mutation和state
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}

总结

到这里基本vuex的流程源码已经分析完毕,分享下自己看源码的思路或者过程,在看之前先把官网的文档再仔细过一遍,然后带着问题来看源码,这样效率会比较高,利用chrome在关键点打开debugger,一步一步执行,看源码的执行过程,数据状态的变换。而且可以暂时屏蔽一些没有副作用的代码,比如assert,这些函数一般都不会影响流程的理解,这样也可以尽量减少源码行数。剩下的就是耐心了,前前后后断断续续看了很多次,总算也把这份分享写完了,由于水平关系,一些地方可能理解错误或者不到位,欢迎指出。

原文地址

查看原文

赞 14 收藏 14 评论 2

fsrookie 收藏了问题 · 2019-09-09

vscode使用vue中的v-for提示错误

clipboard.png

之前没有这个提示的,最近几天才有这个提示。

fsrookie 收藏了问题 · 2019-09-09

浏览器渲染和服务器渲染区别以及各自适合什么场景的开发

1)浏览器渲染和服务器渲染区别除了谁生成html外,还有什么?各自适合什么场景的开发?是重信息展示的适合服务端渲染,做成多页应用吗?重交互的适合浏览器渲染,做成单页应用吗?还有别的什么判别条件吗?如果既重视信息展示,又重视交互呢?
2)流行的那些前端框架,哪些是服务端渲染,哪些是浏览器端渲染呢?知道的有Tomcat,nodejs是服务端渲染,angular, react+redux,vue是浏览器端渲染,这是对的吗?还有什么能补充的呢?
3)Tomcat的javaweb程序,因为浏览器不能执行并显示jsp文件,所以tomcat服务器将jsp文件渲染成html文件,以便于能够在浏览器中将http url请求的结果进行显示。那么,JSP中ajax更新数据和插入html的代码,是浏览器渲染的还是服务器渲染的呢?

fsrookie 收藏了文章 · 2019-09-09

入门 Webpack,看这篇就够了

2018年8月25日更新,目前 webpack 已经更新值 4.17.1 ,本文所用到的各种库或多或少有些过时,跟着代码操作下来可能会遇到各种问题,不过 webpack 的主体思想没变,所以还是希望本文对新学 webpack 的你,有所帮助。此外用基于 webpack 4.17.1 写了一个简单的demo,如果遇到啥问题,可以参考,之后应该会逐步来完善这个demo,如果有啥通用的想实现的功能,也可以在里面提 issue。

2017年12月7日更新,添加了clean-webpack-plugin,babel-env-preset,添加本文涉及到的所有代码的示例,如果你在学习过程中出错了,可点击此处参考(有些过时了,不要再 fork 了)

写在前面的话

阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较有限,你可以快速浏览或直接跳过;如果你和十天前的我一样,对很多选项存在着疑惑,那花一段时间慢慢阅读本文,你的疑惑一定一个一个都会消失;如果你以前没怎么接触过Webpack,而你又你对webpack感兴趣,那么动手跟着本文中那个贯穿始终的例子写一次,写完以后你会发现你已明明白白的走进了Webpack的大门。
// 一个常见的`webpack`配置文件
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
        entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
        output: {
            path: __dirname + "/build",
            filename: "bundle-[hash].js"
        },
        devtool: 'none',
        devServer: {
            contentBase: "./public", //本地服务器所加载的页面所在的目录
            historyApiFallback: true, //不跳转
            inline: true,
            hot: true
        },
        module: {
            rules: [{
                    test: /(\.jsx|\.js)$/,
                    use: {
                        loader: "babel-loader"
                    },
                    exclude: /node_modules/
                }, {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [{
                            loader: "css-loader",
                            options: {
                                modules: true,
                                localIdentName: '[name]__[local]--[hash:base64:5]'
                            }
                        }, {
                            loader: "postcss-loader"
                        }],
                    })
                }
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ]
};

什么是WebPack,为什么要使用它?

为什要使用WebPack

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;
  • 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
  • Scss,less等CSS预处理器
  • ...

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

什么是Webpack

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。
Grunt和Gulp的工作流程

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。
Webpack工作方式

如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

开始使用Webpack

初步了解了Webpack工作方式后,我们一步步的开始学习使用Webpack。

安装

Webpack可以使用npm安装,新建一个空的练习文件夹(此处命名为webpack sample project),在终端中转到该文件夹后执行下述指令就可以完成安装。

//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack

正式使用Webpack前的准备

  1. 在上述练习文件夹中创建一个package.json文件,这是一个标准的npm说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init命令可以自动创建这个package.json文件
npm init

输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。

  1. package.json文件已经就绪,我们在本项目中安装Webpack作为依赖包
// 安装Webpack
npm install --save-dev webpack
  1. 回到之前的空文件夹,并在里面创建两个文件夹,app文件夹和public文件夹,app文件夹用来存放原始数据和我们将写的JavaScript模块,public文件夹用来存放之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建三个文件:
  • index.html --放在public文件夹中;
  • Greeter.js-- 放在app文件夹中;
  • main.js-- 放在app文件夹中;

此时项目结构如下图所示
项目结构

我们在index.html文件中写入最基础的html代码,它在这里目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为bundle.js,之后我们还会详细讲述)。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script data-original="bundle.js"></script>
  </body>
</html>

我们在Greeter.js中定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块:

// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

main.js文件中我们写入下述代码,用以把Greeter模块返回的节点插入页面。

//main.js 
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());

正式使用Webpack

webpack可以在终端中使用,在基本的使用方法如下:

# {extry file}出填写入口文件的路径,本文中就是上述main.js的路径,
# {destination for bundled file}处填写打包文件的存放路径
# 填写路径的时候不用添加{}
webpack {entry file} {destination for bundled file}

指定入口文件后,webpack将自动识别项目所依赖的其它文件,不过需要注意的是如果你的webpack不是全局安装的,那么当你在终端中使用此命令时,需要额外指定其在node_modules中的地址,继续上面的例子,在终端中输入如下命令

# webpack非全局安装的情况
node_modules/.bin/webpack app/main.js public/bundle.js

结果如下

使用命令行打包

可以看出webpack同时编译了main.jsGreeter,js,现在打开index.html,可以看到如下结果
htmlResult1

有没有很激动,已经成功的使用Webpack打包了一个文件了。不过在终端中进行复杂的操作,其实是不太方便且容易出错的,接下来看看Webpack的另一种更常见的使用方法。

通过配置文件来使用Webpack

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loadersplugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

继续上面的例子来说明如何写这个配置文件,在当前练习文件夹的根目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  }
}
:“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录。

有了这个配置之后,再打包文件,只需在终端里运行webpack(非全局安装需使用node_modules/.bin/webpack)命令就可以了,这条命令会自动引用webpack.config.js文件中的配置选项,示例如下:

配合配置文件进行打包

又学会了一种使用Webpack的方法,这种方法不用管那烦人的命令行参数,有没有感觉很爽。如果我们可以连webpack(非全局安装需使用node_modules/.bin/webpack)这条命令都可以不用,那种感觉会不会更爽~,继续看下文。

更快捷的执行打包任务

在命令行中输入命令需要代码类似于node_modules/.bin/webpack这样的路径其实是比较烦人的,不过值得庆幸的是npm可以引导任务执行,对npm进行配置后可以在命令行中使用简单的npm start命令来替代上面略微繁琐的命令。在package.json中对scripts对象进行相关设置即可,设置方法如下。

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack" // 修改的是这里,JSON文件不支持注释,引用时请清除
  },
  "author": "zhang",
  "license": "ISC",
  "devDependencies": {
    "webpack": "3.10.0"
  }
}
注:package.json中的script会安装一定顺序寻找命令对应位置,本地的node_modules/.bin路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack,你都不需要写前面那指明详细的路径了。

npm的start命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start就可以执行其对于的命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}npm run build,我们在命令行中输入npm start试试,输出结果如下:

使用npm start 打包代码

现在只需要使用npm start就可以打包文件了,有没有觉得webpack也不过如此嘛,不过不要太小瞧webpack,要充分发挥其强大的功能我们需要修改配置文件的其它选项,一项项来看。

Webpack的强大功能

生成Source Maps(使调试更容易)

开发总是离不开调试,方便的调试能极大的提高开发效率,不过有时候通过打包后的文件,你是不容易找到出错了的地方,对应的你写的代码的位置的,Source Maps就是来帮我们解决这个问题的。

通过简单的配置,webpack就可以在打包时为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

webpack的配置文件中配置source maps,需要配置devtool,它有以下四种不同的配置选项,各具优缺点,描述如下:

devtool选项配置结果
source-map在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;
cheap-module-source-map在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;
cheap-module-eval-source-map这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;

正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js,进行如下配置:

module.exports = {
  devtool: 'eval-source-map',
  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  }
}
cheap-module-eval-source-map方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。

使用webpack构建本地服务器

想不想让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现你想要的这些功能,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖

npm install --save-dev webpack-dev-server

devserver作为webpack配置选项中的一项,以下是它的一些配置选项,更多配置可参考这里

devserver的配置选项功能描述
contentBase默认webpack-dev-server会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录)
port设置默认监听端口,如果省略,默认为”8080“
inline设置为true,当源文件改变时会自动刷新页面
historyApiFallback在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html

把这些命令加到webpack的配置文件中,现在的配置文件webpack.config.js如下所示

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  devServer: {
    contentBase: "./public",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  } 
}

package.json中的scripts对象中添加如下命令,用以开启本地服务器:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open"
  },

在终端中输入npm run server即可在本地的8080端口查看结果

开启本地服务器

Loaders

鼎鼎大名的Loaders登场了!

Loaderswebpack提供的最激动人心的功能之一了。通过使用不同的loaderwebpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件(ES6,ES7)转换为现代浏览器兼容的JS文件,对React的开发而言,合适的Loaders可以把React的中用到的JSX文件转换为JS文件。

Loaders需要单独安装并且需要在webpack.config.js中的modules关键字下进行配置,Loaders的配置包括以下几方面:

  • test:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须)
  • loader:loader的名称(必须)
  • include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);
  • query:为loaders提供额外的设置选项(可选)

不过在配置loader之前,我们把Greeter.js里的问候消息放在一个单独的JSON文件里,并通过合适的配置使Greeter.js可以读取该JSON文件的值,各文件修改后的代码如下:

在app文件夹中创建带有问候信息的JSON文件(命名为config.json)

{
  "greetText": "Hi there and greetings from JSON!"
}

更新后的Greeter.js

var config = require('./config.json');

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = config.greetText;
  return greet;
};
由于webpack3.*/webpack2.*已经内置可处理JSON文件,这里我们无需再添加webpack1.*需要的json-loader。在看如何具体使用loader之前我们先看看Babel是什么?

Babel

Babel其实是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:

  • 让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
  • 让你能使用基于JavaScript进行了拓展的语言,比如React的JSX;

Babel的安装与配置

Babel其实是几个模块化的包,其核心功能位于称为babel-core的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析Es6的babel-env-preset包和解析JSX的babel-preset-react包)。

我们先来一次性安装这些依赖包

// npm一次性安装多个依赖模块,模块之间用空格隔开
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react

webpack中配置Babel的方法如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
};

现在你的webpack的配置已经允许你使用ES6以及JSX的语法了。继续用上面的例子进行测试,不过这次我们会使用React,记得先安装 React 和 React-DOM

npm install --save react react-dom

接下来我们使用ES6的语法,更新Greeter.js并返回一个React组件

//Greeter,js
import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

修改main.js如下,使用ES6的模块定义和渲染Greeter模块

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

render(<Greeter />, document.getElementById('root'));

重新使用npm start打包,如果之前打开的本地服务器没有关闭,你应该可以在localhost:8080下看到与之前一样的内容,这说明reactes6被正常打包了。

localhost:8080

Babel的配置

Babel其实可以完全在 webpack.config.js 中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 ".babelrc" 的配置文件中。我们现在的babel的配置并不算复杂,不过之后我们会再加一些东西,因此现在我们就提取出相关部分,分两个配置文件进行配置(webpack会自动调用.babelrc里的babel配置选项),如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }
};
//.babelrc
{
  "presets": ["react", "env"]
}

到目前为止,我们已经知道了,对于模块,Webpack能提供非常强大的处理功能,那那些是模块呢。

一切皆模块

Webpack有一个不可不说的优点,它把所有的文件都都当做模块处理,JavaScript代码,CSS和fonts以及图片等等通过合适的loader都可以被处理。

CSS

webpack提供两个工具处理样式表,css-loaderstyle-loader,二者处理的任务不同,css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

继续上面的例子

//安装
npm install --save-dev style-loader css-loader
//使用
module.exports = {

   ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader"
                    }
                ]
            }
        ]
    }
};
请注意这里对同一个文件引入多个loader的方法。

接下来,在app文件夹里创建一个名字为"main.css"的文件,对一些元素设置样式

/* main.css */
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}

我们这里例子中用到的webpack只有单一的入口,其它的模块需要通过 import, require, url等与入口文件建立其关联,为了让webpack能找到”main.css“文件,我们把它导入”main.js “中,如下

//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';//使用require导入css文件

render(<Greeter />, document.getElementById('root'));
通常情况下,css会和js打包到同一个文件中,并不会打包为一个单独的css文件,不过通过合适的配置webpack也可以把css打包为单独的文件的。

上面的代码说明webpack是怎么把css当做模块看待的,咱们继续看一个更加真实的css模块实践。

CSS module

在过去的一些年里,JavaScript通过一些新的语言特性,更好的工具以及更好的实践方法(比如说模块化)发展得非常迅速。模块使得开发者把复杂的代码转化为小的,干净的,依赖声明明确的单元,配合优化工具,依赖管理和加载管理可以自动完成。

不过前端的另外一部分,CSS发展就相对慢一些,大多的样式表却依旧巨大且充满了全局类名,维护和修改都非常困难。

被称为CSS modules的技术意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,动画名默认都只作用于当前模块。Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。具体的代码如下

module.exports = {

    ...

    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true, // 指定启用css modules
                            localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式
                        }
                    }
                ]
            }
        ]
    }
};

我们在app文件夹下创建一个Greeter.css文件来进行一下测试

/* Greeter.css */
.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}

导入.root到Greeter.js中

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//导入

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}> //使用cssModule添加类名的方法
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

放心使用把,相同的类名也不会造成不同组件之间的污染。

应用了css module后的样式

CSS modules 也是一个很大的主题,有兴趣的话可以去其官方文档了解更多。

CSS预处理器

SassLess 之类的预处理器是对原生CSS的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance等不存在于CSS中的特性来写CSS,CSS预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS语句,

你现在可能都已经熟悉了,在webpack里使用相关loaders进行配置就可以使用了,以下是常用的CSS 处理loaders:

  • Less Loader
  • Sass Loader
  • Stylus Loader

不过其实也存在一个CSS的处理平台-PostCSS,它可以帮助你的CSS实现更多的功能,在其官方文档可了解更多相关知识。

举例来说如何使用PostCSS,我们使用PostCSS来为CSS代码自动添加适应不同浏览器的CSS前缀。

首先安装postcss-loaderautoprefixer(自动添加前缀的插件)

npm install --save-dev postcss-loader autoprefixer

接下来,在webpack配置文件中添加postcss-loader,在根目录新建postcss.config.js,并添加如下代码之后,重新使用npm start打包时,你写的css会自动根据Can i use里的数据添加不同前缀了。

//webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    }
}
// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

至此,本文已经谈论了处理JS的Babel和处理CSS的PostCSS的基本用法,它们其实也是两个单独的平台,配合webpack可以很好的发挥它们的作用。接下来介绍Webpack中另一个非常重要的功能-Plugins

插件(Plugins)

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。

使用插件的方法

要使用某个插件,我们需要通过npm安装它,然后要做的就是在webpack配置中的plugins关键字部分添加该插件的一个实例(plugins是一个数组)继续上面的例子,我们添加了一个给打包后代码添加版权声明的插件

const webpack = require('webpack');

module.exports = {
...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究')
    ],
};

通过这个插件,打包后的JS文件显示如下

版权所有,翻版必究

这就是webpack插件的基础用法了,下面给大家推荐几个常用的插件

HtmlWebpackPlugin

这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)。

安装

npm install --save-dev html-webpack-plugin

这个插件自动完成了我们之前手动做的一些事情,在正式使用之前需要对一直以来的项目结构做一些更改:

  1. 移除public文件夹,利用此插件,index.html文件会自动生成,此外CSS已经通过前面的操作打包到JS中了。
  2. 在app目录下,创建一个index.tmpl.html文件模板,这个模板包含title等必须元素,在编译过程中,插件会依据此模板生成最终的html页面,会自动添加所依赖的 css, js,favicon等文件,index.tmpl.html中的模板源代码如下:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

3.更新webpack的配置文件,方法同上,新建一个build文件夹用来存放最终的输出文件

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        })
    ],
};

再次执行npm start你会发现,build文件夹下面生成了bundle.jsindex.html

build文件夹

Hot Module Replacement

Hot Module Replacement(HMR)也是webpack里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。

在webpack中实现HMR也很简单,只需要做两项配置

  1. 在webpack配置文件中添加HMR插件;
  2. 在Webpack Dev Server中添加“hot”参数;

不过配置完这些后,JS模块其实还是不能自动热加载的,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用,但是如果是React模块,使用我们已经熟悉的Babel可以更方便的实现功能热加载。

整理下我们的思路,具体实现方法如下

  • Babelwebpack是独立的工具
  • 二者可以一起工作
  • 二者都可以通过插件拓展功能
  • HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
  • Babel有一个叫做react-transform-hrm的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;

还是继续上例来实际看看如何配置

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()//热加载插件
    ],
};
   

安装react-transform-hmr

npm install --save-dev babel-plugin-react-transform react-transform-hmr

配置Babel

// .babelrc
{
  "presets": ["react", "env"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         
         "imports": ["react"],
         
         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。

产品阶段的构建

目前为止,我们已经使用webpack构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS和JS。

对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js的文件,在里面加上基本的配置,它和原始的webpack.config.js很像,如下

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'null', //注意修改了这里,这能大大压缩我们的打包代码
    devServer: {
        contentBase: "./public", //本地服务器所加载的页面所在的目录
        historyApiFallback: true, //不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader"
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [{
                    loader: "css-loader",
                    options: {
                        modules: true
                    }
                }, {
                    loader: "postcss-loader"
                }],
            })
        }]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin() //热加载插件
    ],
};
//package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open",
    "build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
...
  },
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
}
注意:如果是window电脑,build需要配置为"build": "set NODE_ENV=production && webpack --config ./webpack.production.config.js --progress".谢谢评论区简友提醒。

优化插件

webpack提供了一些在发布阶段非常有用的优化插件,它们大多来自于webpack社区,可以通过npm安装,通过以下插件可以完成产品发布阶段所需的功能

  • OccurenceOrderPlugin :为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多的模块,并为它们分配最小的ID
  • UglifyJsPlugin:压缩JS代码;
  • ExtractTextPlugin:分离CSS和JS文件

我们继续用例子来看看如何添加它们,OccurenceOrder 和 UglifyJS plugins 都是内置插件,你需要做的只是安装其它非内置插件

npm install --save-dev extract-text-webpack-plugin

在配置文件的plugins后引用它们

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'none',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ],
};

此时执行npm run build可以看见代码是被压缩后的

压缩后的代码

缓存

缓存无处不在,使用缓存的最好方法是保证你的文件名和文件内容是匹配的(内容改变,名称相应改变)

webpack可以把一个哈希值添加到打包的文件名中,使用方法如下,添加特殊的字符串混合体([name], [id] and [hash])到输出文件名前

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
..
    output: {
        path: __dirname + "/build",
        filename: "bundle-[hash].js"
    },
   ...
};

现在用户会有合理的缓存了。

带hash值的js名

去除build文件中的残余文件

添加了hash之后,会导致改变文件内容后重新打包时,文件名不同而内容越来越多,因此这里介绍另外一个很好用的插件clean-webpack-plugin

安装
cnpm install clean-webpack-plugin --save-dev

使用

引入clean-webpack-plugin插件后在配置文件的plugins中做相应配置即可:

const CleanWebpackPlugin = require("clean-webpack-plugin");
  plugins: [
    ...// 这里是之前配置的其它各种插件
    new CleanWebpackPlugin('build/*.*', {
      root: __dirname,
      verbose: true,
      dry: false
  })
  ]

关于clean-webpack-plugin的详细使用可参考这里

总结

其实这是一年前的文章了,趁周末重新运行和修改了一下,现在所有的代码都可以正常运行,所用webpack基于最新的webpack3.5.3。希望依旧能对你有帮助。

这是一篇好长的文章,谢谢你的耐心,能仔细看到了这里,大概半个月前我第一次自己一步步配置项目所需的Webpack后就一直想写一篇笔记做总结,几次动笔都不能让自己满意,总觉得写不清楚。其实关于Webpack本文讲述得仍不完全,不过相信你看完后已经进入Webpack的大门,能够更好的探索其它的关于Webpack的知识了。

欢迎大家在文后发表自己的观点讨论。

更新说明

2017-12-11更新,修改css module部分代码及示例图片,css module真的非常好用,希望大家都能用上。

2017年9月18日更新,添加了一个使用webpack配置多页应用的demo,可以点击此处查看

2017年8月13日更新,本文依据webpack3.5.3将文章涉及代码完全重写,所有代码都在Mac上正常运行过。希望依旧对你学习webpack有帮助。

2017年8月16号更新:
最近在Gitchat上将发起了一场关于webpack的分享,目的在于一起花最短的时间理解和学会webpack,感兴趣的童鞋可以微信扫描注册哈。
webpack从入门到工程实践

查看原文

fsrookie 收藏了问题 · 2019-09-09

axios Cannot read property 'cancelToken' of undefined

在vue2.0中使用axios向后台请求数据

全局配置axios, /src/apis/index.js

import axios from 'axios';
import qs from 'qs';

axios.defaults.baseURL = 'http://usergroup.laile.com/api_group/v1/';
axios.defaults.timeout = 5000;

axios.interceptors.request.use((config) => {
  if(config.method === 'post') {
    config.data = qs.stringify(config.data);
  }
}, (error) => {
  return Promise.reject(error);
});

axios.interceptors.response.use((res) => {
  if(!res.data.success) {
    return Promise.reject(res);
  }
  return res;
}, (error) => {
  return Promise.reject(error);
});

export default axios;

/src/apis/mountedUsers.js 某个页面的接口文件

import axios from './index';
export default {
  getCities() {
    return axios.get(`/city/opened-list`);
  }
}

/src/store/modules/mountedUsers.js

import api from '../../apis/mountedUsers'
import * as types from '../types'
const state = {
  cities: []
}
const getters = {
  cities: state => state.cities
}
const actions = {
  getCities({ commit }) {
    api.getCities().then(res => {
      commit(types.GET_CITIES, res.data)
    });
  }
}
const mutations = {
  [types.GET_CITIES](state, cities) {
    state.cities = cities;
  }
}
export default {
  state,
  getters,
  actions,
  mutations
}

入口文件 main.js

import Vue from 'vue';
import App from './App';
import router from './router';
import store from './store/index';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-default/index.css';    // 默认主题
import '../static/css/pzq.css';  
import "babel-polyfill";
Vue.use(ElementUI);
Vue.config.devtools = true;
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app');

当我在页面中发送this.$store.dispatch('getCities')的时候,报Uncaught (in promise) TypeError: Cannot read property 'cancelToken' of undefined,请问是什么原因呢?

fsrookie 收藏了文章 · 2019-09-09

原生js实现瀑布流效果

image

前言

最近在整理js基础知识,接触到了几个常用的页面特效,其中觉得用原生js实现瀑布流的案例十分有趣,于是与大家分享一下。

瀑布流

瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。

image

1、首先瀑布流所有的图片应该保持宽度一致,高度是由内容决定。

image

左浮动的话,我们可以看到第6个盒子直接就在第4个盒子旁边停下了,因为第4个高度最高,挡住了它左浮动的去路。第6个盒子是第2行的最后一个,所以第7个盒子只能在第3行排列了。当排到第12个盒子的时候,盒子会以第11个盒子的位置为基础左浮动(这就是第12个盒子为什么没有‘跳到’第9个盒子下面的原因),碰到第8个盒子后又被挡住了。

image

通过定位的方式是我们实现瀑布流的最基本的原理,只要我们动态的设置它的top值、left值,就能让它排列。

2、定位后确定浏览器显示区域内,一行能放多少列图片盒子。

  • 获取页面的宽度
  • 获取图片盒子的宽度
  • 显示的列数 = 页面宽度/图片盒子宽度
  • column = pageWidth / itemWidth

image

3、为了美观我们可以加上一个空隙

  • 显示的列数 = 页面宽度/(图片盒子宽度+间隙);
  • column = pageWidth / (itemWidth + gap);

image

4、 确定列数之后,排列第一行

  • 下面还有很多图片盒子,我们先要排列第1行,所以在for循环里就要判断一下,当i(所有图片盒子的索引) < column(显示列数)的时候,说明在第1行;
  • 知道在第1行之后,动态设置每个图片盒子的left值就能排好第1行。
  • left = i * ( itemWidth + gap );

image

5、第1行排列好之后,获取第1行所有图片盒子的高度

  • 需要定义一个数组arr,将获取到的高度存在数组中,因为第2行排列的时候需要考虑top值,此时只能根据第1行图片盒子的高度来设置;
  • 获取图片高度的时候要注意,程序必须写在入口函数onload里面,因为图片的加载特性是:等页面都加载完之后才去请求加载,所以不写在入口函数里可能会出现高度获取不到的情况。

image

6、排列第2行

  • 获取到刚刚数组中,高度最小的那一列,将第2行的第1个图片盒子放置在它的下方;
  • 此时的left值就是高度最小列的offsetLefttop值就是:第1行高度最小列的高度(为了布局美观可以加上上下间隙gap)。
  • 记录下高度最小列的索引index,后面计算会用到;
  • 设置完成之后,会发现后面所有的图片都叠在这个高度最小列的下面,原因就是此时的最小列高度没有改变,应该加上下面图片的高度,得出一个新高度。

image

7、改变最小列当前高度

  • 此时的这一列高度其实已经发生改变了,所以需要将新高度赋值给数组
  • 当前高度最小列的高度 = 当前高度最小列的高度 + 间隙 + 下面图片盒子的高度

image

8、触发resize事件

  • 将整个设置样式的部分封装成一个函数,在onload里面注册一个resize事件,只要页面一发生改变,就触发样式部分的代码。
  • 实时改变pageWidth的宽度,这样瀑布流就会是一个响应式的效果了

9、懒加载效果

  • 目前我们用的是30张图片,假如一个页面中有几百张图片的时候,我们不可能等到它都加载完再显示,所有这里引入一个懒加载的概念,我们规定第30张为显示的最后一张图片,当滚动条滚动到30张的时候,应该加载下一批图片。
  • 即页面可视区高度+滚动条卷去的高度 = 第30图片的offsetTop;的时候加载下面的图片。

image

完整代码:

<!-- 样式部分 -->
<style>
    * {
        margin: 0;
        padding: 0;
        position: relative;
    }
    
    img {
        width: 220px;
        display: block;
    }
    
    .item {
        box-shadow: 2px 2px 2px #999;
        position: absolute;
    }
</style>

<!-- html 部分 -->
<div id="box">
    <div class="item"><img data-original="../image/瀑布流/001.jpg" alt=""></div>
                                .
                                .
                                .
    <div class="item"><img data-original="../image/瀑布流/030.jpg" alt=""></div>
</div>

<!-- js 部分 -->
<script>
    var box = document.getElementById('box');
    var items = box.children;
    // 定义每一列之间的间隙 为10像素
    var gap = 10;

    window.onload = function() {
        // 一进来就调用一次
        waterFall();
        // 封装成一个函数
        function waterFall() {
            // 1- 确定列数  = 页面的宽度 / 图片的宽度
            var pageWidth = getClient().width;
            var itemWidth = items[0].offsetWidth;
            var columns = parseInt(pageWidth / (itemWidth + gap));
            var arr = [];
            for (var i = 0; i < items.length; i++) {
                if (i < columns) {
                    // 2- 确定第一行
                    items[i].style.top = 0;
                    items[i].style.left = (itemWidth + gap) * i + 'px';
                    arr.push(items[i].offsetHeight);

                } else {
                    // 其他行
                    // 3- 找到数组中最小高度  和 它的索引
                    var minHeight = arr[0];
                    var index = 0;
                    for (var j = 0; j < arr.length; j++) {
                        if (minHeight > arr[j]) {
                            minHeight = arr[j];
                            index = j;
                        }
                    }
                    // 4- 设置下一行的第一个盒子位置
                    // top值就是最小列的高度 + gap
                    items[i].style.top = arr[index] + gap + 'px';
                    // left值就是最小列距离左边的距离
                    items[i].style.left = items[index].offsetLeft + 'px';

                    // 5- 修改最小列的高度 
                    // 最小列的高度 = 当前自己的高度 + 拼接过来的高度 + 间隙的高度
                    arr[index] = arr[index] + items[i].offsetHeight + gap;
                }
            }
        }
        // 页面尺寸改变时实时触发
        window.onresize = function() {
            waterFall();
        };
        // 当加载到第30张的时候
        window.onscroll = function() {
            if (getClient().height + getScrollTop() >= items[items.length - 1].offsetTop) {
                // 模拟 ajax 获取数据    
                var datas = [
                    "../image/瀑布流/001.jpg",
                            ...
                    "../image/瀑布流/030.jpg"
                ];
                for (var i = 0; i < datas.length; i++) {
                    var div = document.createElement("div");
                    div.className = "item";
                    div.innerHTML = '<img data-original="' + datas[i] + '" alt="">';
                    box.appendChild(div);
                }
                waterFall();
            }

        };
    };

    // clientWidth 处理兼容性
    function getClient() {
        return {
            width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
            height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
        }
    }
    // scrollTop兼容性处理
    function getScrollTop() {
        return window.pageYOffset || document.documentElement.scrollTop;
    }
</script>

效果图:

image

查看原文

fsrookie 收藏了文章 · 2019-09-09

利用react-router4的react-router-config做路由鉴权

一、react-router-config 是一个帮助我们配置静态路由的小助手。
其源码就是一个高阶函数 利用一个map函数生成静态路由

import React from "react";
import Switch from "react-router/Switch";
import Route from "react-router/Route";
const renderRoutes = (routes, extraProps = {}, switchProps = {}) =>
routes ? (
    <Switch {...switchProps}>
        {routes.map((route, i) => ( 
        <Route
          key={route.key || i}
          path={route.path}
          exact={route.exact}
          strict={route.strict}
          render={props => (
            <route.component {...props} {...extraProps} route={route} />
          )}
        />
      ))}
    </Switch>
  ) : null;
 export default renderRoutes;

//router.js 假设这是我们设置的路由数组(这种写法和vue很相似是不是?)

const routes = [
    { path: '/',
        exact: true,
        component: Home,
    },
    {
        path: '/login',
        component: Login,
    },
    {
        path: '/user',
        component: User,
    },
    {
        path: '*',
        component: NotFound
    }
]

//app.js 那么我们在app.js里这么使用就能帮我生成静态的路由了

import { renderRoutes } from 'react-router-config'
import routes from './router.js'
const App = () => (
   <main>
      <Switch>
         {renderRoutes(routes)}
      </Switch>
   </main>
)

export default App

扯了半天,要如何利用这个插件帮我们路由鉴权呢?
用过vue的小朋友都知道,vue的router.js 里面添加 meta: { requiresAuth: true }
然后利用导航守卫

router.beforeEach((to, from, next) => {
  // 在每次路由进入之前判断requiresAuth的值,如果是true的话呢就先判断是否已登陆
})

二、基于类似vue的路由鉴权想法,我们稍稍改造一下react-router-config
// utils/renderRoutes.js

import React from 'react'
import { Route, Redirect, Switch } from 'react-router-dom'
const renderRoutes = (routes, authed, authPath = '/login', extraProps = {}, switchProps = {}) => routes ? (
  <Switch {...switchProps}>
    {routes.map((route, i) => (
      <Route
        key={route.key || i}
        path={route.path}
        exact={route.exact}
        strict={route.strict}
        render={(props) => {
          if (!route.requiresAuth || authed || route.path === authPath) {
            return <route.component {...props} {...extraProps} route={route} />
          }
          return <Redirect to={{ pathname: authPath, state: { from: props.location } }} />
        }}
      />
    ))}
  </Switch>
) : null

export default renderRoutes

修改后的源码增加了两个参数 authed 、 authPath 和一个属性 route.requiresAuth
然后再来看一下最关键的一段代码

if (!route.requiresAuth || authed || route.path === authPath) {
    return <route.component {...props} {...extraProps} route={route} />
    }
    return <Redirect to={{ pathname: authPath, state: { from: props.location } }} />

很简单 如果 route.requiresAuth = false 或者 authed = true 或者 route.path === authPath(参数默认值'/login')则渲染我们页面,否则就渲染我们设置的authPath页面,并记录从哪个页面跳转。

相应的router.js也要稍微修改一下

const routes = [
    { path: '/',
        exact: true,
        component: Home,
        requiresAuth: false,
    },
    {
        path: '/login',
        component: Login,
        requiresAuth: false,

    },
    {
        path: '/user',
        component: User,
        requiresAuth: true, //需要登陆后才能跳转的页面

    },
    {
        path: '*',
        component: NotFound,
        requiresAuth: false,
    }
]

//app.js

import React from 'react'
import { Switch } from 'react-router-dom'
//import { renderRoutes } from 'react-router-config'
import renderRoutes from './utils/renderRoutes'
import routes from './router.js'

const authed = false // 如果登陆之后可以利用redux修改该值(关于redux不在我们这篇文章的讨论范围之内)
const authPath = '/login' // 默认未登录的时候返回的页面,可以自行设置

const App = () => (
   <main>
      <Switch>
         {renderRoutes(routes, authed, authPath)}
      </Switch>
   </main>
)
export default App
//登陆之后返回原先要去的页面login函数
login(){
    const { from } = this.props.location.state || { from: { pathname: '/' } }
     // authed = true // 这部分逻辑自己写吧。。。
    this.props.history.push(from.pathname)
}

以上~修改了部分源码并完成了我们想要的效果。

查看原文

认证与成就

  • 获得 897 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-25
个人主页被 2.7k 人浏览